CTFshow S2系列OGNL注入与环境变量泄露实战解析

CTFshow S2系列OGNL注入与环境变量泄露实战解析
1. 项目概述一次对ctfshow S2系列漏洞的深度复盘最近在复盘ctfshow的S2系列题目特别是其中涉及OGNL注入和环境变量泄露的几道经典题感触颇深。这不仅仅是几道CTF题更像是一个精心设计的、从代码审计到漏洞利用的完整实战沙箱。很多刚接触安全的朋友可能对“OGNL注入”这个名词感到陌生或者觉得“环境变量泄露”听起来平平无奇但当你把它们放在一个具体的、层层递进的Web应用场景里你会发现攻击链的构建远比想象中精妙。我花了些时间把整个S2系列中从Web83到Web165等题目的核心思路、踩过的坑以及最终的利用技巧重新梳理了一遍形成这篇实战解析。无论你是正在刷题的新手还是想深入理解Struts2框架漏洞原理的进阶者相信这篇从“黑盒测试”到“白盒审计”的完整推演都能给你带来一些直接的启发。2. 核心漏洞原理与攻击面剖析2.1 OGNL注入Struts2框架的阿喀琉斯之踵OGNLObject-Graph Navigation Language是Struts2框架用于在视图层和控制器层之间绑定数据、执行表达式的一种强大语言。它的强大恰恰成了它最大的安全隐患。简单来说当用户输入的数据未经充分过滤直接被框架当作OGNL表达式解析执行时就发生了OGNL注入。这不同于传统的SQL注入它直接发生在应用逻辑层可以执行任意Java代码危害等级极高。在ctfshow的S2系列题目中OGNL注入的触发点往往非常隐蔽。它可能藏在一个普通的参数传递里比如某个id、name参数也可能通过特定的Struts2标签属性触发。攻击者通过构造特殊的Payload可以达成几个目的一是执行系统命令从而获取服务器权限二是读写服务器上的文件三是调用危险静态方法破坏应用逻辑。理解OGNL的语法是关键比如#号用于访问值栈ValueStack和ActionContext中的对象用于访问静态方法和类{}用于创建集合。一个典型的命令执行Payload可能长这样%{(#ajava.lang.RuntimegetRuntime()).(#a.exec(whoami))}它利用了OGNL表达式调用Runtime.getRuntime().exec()方法。注意现代Struts2版本以及部署了相应安全补丁的环境会对这类简单Payload进行拦截。实战和CTF中往往需要结合上下文进行变形和绕过例如使用Unicode编码、利用OGNL表达式本身的特性如new关键字创建对象、#号引用已存在对象来构造更隐蔽的利用链。2.2 环境变量泄露被忽视的信息金矿环境变量泄露听起来没有远程代码执行RCE那么“刺激”但在渗透测试中它的价值常常被低估。服务器环境变量中可能包含大量敏感信息数据库连接字符串如JDBC_URL、第三方服务的API密钥API_KEY、加密密钥SECRET_KEY、服务器内部路径、甚至是其他系统的访问凭证。在CTF场景中flag常常就被直接设置在某个环境变量里比如FLAG、CTFSHOW_FLAG等。泄露的途径多种多样。最常见的是通过Web应用程序的错误信息反馈例如开启debug模式的应用在报错时可能会将部分环境信息打印到页面上。另一种是通过某些特定的接口或功能点比如/env、/actuator/envSpring Boot、phpinfo()PHP等。在S2系列的题目设计中环境变量泄露有时是独立考点有时则是OGNL注入达成RCE后用于获取最终flag的关键一步。你需要知道在Java中可以通过System.getenv()来获取所有环境变量而在OGNL表达式里可以构造如java.lang.Systemgetenv()这样的调用来实现。将OGNL注入和环境变量泄露结合起来就构成了一条清晰的攻击链首先通过OGNL注入获取一个命令执行的能力然后利用这个能力去读取进程的环境变量从而找到隐藏的flag。这条链考验的是你对漏洞利用的完整性和对目标系统信息收集的全面性。3. 靶场实战ctfshow S2系列题目精解3.1 Web83OGNL注入的“破门”之旅Web83通常被设计为S2系列的入门题它的目的是让你理解最基础的OGNL注入点在哪里以及如何利用。题目界面可能很简单只有一个输入框或几个参数。我们的第一步永远是信息收集尝试输入一些特殊字符如$、#、%、{、}观察返回结果是否有变化或报错信息。Struts2的某些版本在解析错误时会在页面上留下包含“ognl”或“Expression”等关键词的痕迹。假设我们发现某个参数比如username存在注入点。一个经典的测试Payload是${11}。如果返回的页面中这个表达式被计算成了2那么基本可以确定存在OGNL表达式注入。接下来就是尝试执行命令。但直接执行Runtime.getRuntime().exec()可能会被WAF或安全配置拦截。这里有一个小技巧利用OGNL创建ProcessBuilder对象。Payload可以构造为%{(#pnew java.lang.ProcessBuilder(new java.lang.String[]{whoami})).(#p.start())}。这个Payload通过new关键字直接实例化对象有时能绕过基于字符串特征的黑名单。如果执行成功我们会在服务器上执行whoami命令但输出可能不会直接回显到页面上。这时就需要考虑命令回显的技巧。常见的方法有将命令结果写入Web目录下的一个文件然后通过浏览器访问该文件或者利用curl或wget将结果发送到自己的服务器在CTF中通常不允许外连所以文件写入更常用。例如%{(#pnew java.lang.ProcessBuilder(new java.lang.String[]{sh,-c,whoami /tmp/result.txt})).(#p.start())}执行后访问/tmp/result.txt或Web可访问路径查看结果。3.2 Web165与代码审计从黑盒到白盒的思维转换从Web165左右的题目开始单纯的Payload打过去可能就不灵了。题目可能增加了过滤规则或者漏洞点变得更加隐蔽。这时就需要结合“代码审计”的思维。虽然CTF中不一定提供完整源码但我们可以通过报错信息、已知的Struts2漏洞编号如S2-045, S2-052等以及参数名推测后台逻辑。例如题目可能过滤了Runtime、ProcessBuilder、exec等关键词。我们的绕过思路可以有以下几种字符串拼接利用OGNL的字符串连接符。Runtime可以写成Runtimeexec可以写成exec。在OGNL中字符串可以用单引号连接如Runt ime。编码绕过使用URL编码、十六进制编码或Unicode编码。例如exec的Unicode形式是\u0065\u0078\u0065\u0063。OGNL表达式在某些上下文中可以解析这些编码。利用反射这是更高级的技巧。如果Runtime类被禁我们可以通过反射来获取它。Java反射的起点是Class.forName()。一个可能的Payload是%{(#c#this.getClass().forName(java.lang.Runtime)).(#m#c.getMethod(getRuntime)).(#r#m.invoke(null)).(#r.exec(calc))}。这个Payload完全避免了直接出现危险类名和方法名通过字符串形式的类名和方法名动态调用绕过了静态关键词过滤。寻找替代类除了RuntimeProcessBuilder、ScriptEngineManager执行JS等脚本等都可以用来执行命令可以尝试不同的类。在实战审计中你需要关注Struts2的配置文件struts.xml看是否有定义全局的拦截器Interceptor或对某些package启用了严格的安全校验。同时关注Action类中是如何接收参数的。如果使用了模型驱动ModelDriven或直接使用Action的属性并且没有对参数进行类型转换和校验风险就很高。3.3 环境变量泄露的挖掘与利用在成功获取命令执行能力后下一步就是读取环境变量。在Linux系统下直接在命令中执行env即可。所以我们的OGNL Payload可以变为%{(#pnew java.lang.ProcessBuilder(new java.lang.String[]{sh,-c,env /var/www/html/env.txt})).(#p.start())}。然后访问http://靶机地址/env.txt就能看到所有环境变量。这里有几个实操心得路径问题你写入的文件必须位于Web服务器有权限读写且你能访问的目录。/tmp目录通常可写但Web服务器不一定能访问。最好写入网站的根目录或已知的上传目录。如果不知道绝对路径可以尝试一些常见路径或者先执行pwd命令查看当前工作目录。结果过滤有时题目不会让你直接看到完整的env输出可能做了过滤或截断。这时可以尝试用grep命令只筛选包含特定关键词如FLAG、CTF的行env | grep -i flag。Java直接读取更优雅的方式是直接在OGNL表达式中调用System.getenv()。例如%{java.lang.Systemgetenv(FLAG)}或%{java.lang.Systemgetenv()}。后者会返回一个Map对象如果页面能直接显示这个对象的内容就能看到所有变量。但很多时候页面只会显示对象的引用地址这时就需要想办法将其内容输出比如遍历Map的entrySet并拼接成字符串。在S2系列的一些题目中flag可能不在默认的环境变量里而是被程序在启动时动态设置。这时你需要查看的是当前Java进程的所有环境变量System.getenv()正是用于此目的。这也解释了为什么直接找文件找不到flag因为它只存在于内存的环境变量中。4. 漏洞利用链的构建与高级技巧4.1 从注入点到RCE的完整链条构建一个可靠的利用链不仅仅是拼凑一个Payload那么简单。它需要对目标系统进行探测对过滤规则进行试探并准备好备用方案。第一步确认注入点与上下文。你是通过GET参数、POST参数、Cookie还是HTTP头注入的参数的值是被直接放入OGNL表达式还是先经过了某些处理通过输入${#context}或${#request}等OGNL内置对象可以探测当前表达式执行的上下文了解哪些对象是可用的。例如${#_memberAccess[“allowStaticMethodAccess”]}可以用来检查是否允许静态方法访问老版本Struts2的漏洞常涉及修改此属性。第二步绕过基础过滤。如果简单的Payload失败依次尝试上述的字符串拼接、编码、反射等方法。可以写一个简单的脚本用Burp Suite的Intruder模块以集群轰炸的方式测试各种变形Payload观察响应差异。第三步实现命令执行与回显。确定可用的命令执行方式Runtime/ProcessBuilder/ScriptEngine和回显方式写文件、报错信息回显、HTTP响应输出。对于写文件回显要特别注意命令中的空格和特殊字符最好用字符串数组的方式传递参数并用Base64编码命令来避免问题。例如想执行cat /etc/passwd可以将其Base64编码后传递echo Y2F0IC9ldGMvcGFzc3dkCg | base64 -d | sh。第四步定位并获取flag。执行env命令或调用System.getenv()。如果返回内容庞大用grep进行筛选。不仅要找FLAG也要留意CTFSHOW、FLAG_、SECRET等可能的关键词变体。4.2 针对特定Struts2版本的利用差异Struts2的不同版本其OGNL沙箱的安全机制和默认配置有所不同利用方式也需调整。Struts 2.0.x - 2.3.x这些早期版本安全机制相对较弱。著名的S2-016、S2-045等漏洞影响广泛。利用时可能无需特别开启静态方法访问直接调用Runtime即可。Struts 2.5.x版本引入了更强的OGNL沙箱限制默认禁止了静态方法调用和很多危险类的访问。这时利用往往需要先找到一个方法去关闭或绕过沙箱限制。例如在某些漏洞中如S2-052的REST插件漏洞可以通过修改SecurityMemberAccess类的字段值来关闭限制。Payload会复杂很多通常涉及多层反射和属性修改。Struts 2.5.22安全机制进一步加强。单纯的OGNL注入可能很难直接达到RCE可能需要结合其他漏洞如反序列化漏洞S2-057的命名空间遍历可能导致RCE但那是另一类问题。在ctfshow题目中为了降低难度后台通常会使用一个存在已知漏洞的Struts2版本并且可能故意放松了一些安全配置。但了解这些版本差异有助于你在真实环境中判断漏洞的利用价值和方式。4.3 防御视角下的思考与加固建议作为攻击者我们研究漏洞利用作为开发者或安全人员我们更应思考如何防御。针对OGNL注入根本的防御措施在于对用户输入进行严格的控制和过滤。升级与补丁及时将Struts2框架升级到最新稳定版并关注安全公告及时打上补丁。这是最有效的方法。输入验证对所有用户输入进行严格的类型检查和长度限制。使用白名单机制只允许预期的字符集。禁用动态方法调用在struts.xml中配置constant namestruts.enable.DynamicMethodInvocation valuefalse /。限制OGNL表达式能力在struts.xml中配置OGNL的沙箱规则例如设置constant namestruts.ognl.allowStaticMethodAccess valuefalse /。在Struts 2.5中可以配置struts.ognl.excludedClasses和struts.ognl.excludedPackageNames来禁止危险类和包的访问。最小权限原则运行Struts2应用的Web服务器如Tomcat进程应使用非root、低权限的用户身份运行这样即使被RCE攻击者能造成的破坏也有限。错误信息处理自定义错误页面避免将详细的堆栈信息、框架信息泄露给用户。对于环境变量泄露的防御敏感信息隔离不要将数据库密码、API密钥等敏感信息直接硬编码在环境变量中。使用专业的密钥管理服务如Vault或至少在部署时从安全的配置中心读取。关闭调试信息在生产环境中确保所有框架Struts2, Spring Boot等的调试模式和Actuator端点如/env,/heapdump被禁用。访问控制如果某些诊断接口必须开启务必配置严格的网络访问控制如只允许内网IP和身份认证。5. 实战中常见问题与排查实录在反复调试S2系列题目的过程中我记录下了一些最常见的“坑点”和解决方法这往往比漏洞原理本身更有实战价值。问题1Payload执行了但没回显也不知道命令是否成功。这是最让人头疼的情况。排查思路如下检查命令语法在OGNL中构造的命令字符串要特别注意转义。尤其是在Linux下执行带管道|或重定向的命令时最好将整个命令用Base64编码后解码执行或者用字符串数组new String[]{“sh”, “-c”, “your_complex_command”}的方式。检查文件路径和权限命令执行成功了但文件写到了没有Web权限的目录或者当前进程用户没有写权限。尝试写入/tmp目录或者先用whoami和pwd命令确认当前用户和工作目录。尝试DNSLog或HTTP请求外带如果题目环境允许通常CTF不允许可以尝试使用curl或wget将命令结果作为参数请求到你的服务器或者使用DNSLog技术通过ping一个包含结果的域名来外带数据。在CTF中这常用于盲注场景但S2系列通常设计为可回显或可写文件。问题2Payload被WAF或应用防火墙拦截了。拆分与混淆将长Payload拆分成多个参数传递或者利用OGNL的赋值特性分步骤执行。例如先#a‘cat’再#b‘/etc/passwd’最后#pnew ProcessBuilder({#a, #b})。使用冷门类或方法除了Runtime和ProcessBuilder可以尝试Unsafe类、Thread类通过start执行Runnable等或者通过javax.script.ScriptEngineManager执行JavaScript代码来调用Java。静态方法绕过如果静态方法访问被禁尝试通过获取当前类的ClassLoader然后loadClass的方式动态加载类再通过反射调用方法。问题3找到了环境变量但里面没有flag。扩大搜索范围执行env查看全部。也许flag的名字不是FLAG而是CTF_FLAG、FLAG_IN_ENV或者藏在某个很长的配置字符串里。用env | grep -E ‘CTF|FLAG|KEY|SECRET’进行模糊搜索。检查进程参数有时flag可能通过Java的-D参数设置为系统属性System.getProperty()而非环境变量。可以尝试执行ps aux | grep java查看完整的Java启动命令或者用OGNL调用System.getProperties()。检查文件系统可能flag仍然在文件里只是路径比较偏。在拥有命令执行权限后可以进行基本的文件系统遍历例如find / -name ‘*flag*’ 2/dev/null或find / -type f -exec grep -l ‘ctfshow’ {} \\; 2/dev/null。问题4题目提示存在漏洞但所有已知Payload都无效。回归代码审计思维静下心来根据题目名称、参数名推测后台可能使用了哪个Struts2插件或哪个特定的Action。例如涉及文件上传的可能是FileUploadInterceptor涉及JSON的可能是JSON Plugin。不同的组件其参数解析和OGNL表达式执行的点可能不同。利用报错信息故意构造一个错误的OGNL表达式让服务器返回详细的错误信息。从错误信息中你可能会看到使用的Struts2版本号、触发的类名、甚至部分源码堆栈这能给你极大的提示。参数污染尝试将同一个参数名以不同方式多次提交如GET和POST同时提交、数组形式提交param[]value看看是否能触发框架解析的异常逻辑。6. 工具辅助与自动化探索手工构造和测试OGNL Payload效率较低尤其是在需要频繁变形绕过时。掌握一些工具和脚本能事半功倍。Burp Suite Intruder/Repeater这是最主要的测试工具。在Repeater中手动调试Payload在Intruder中使用“Pitchfork”或“Cluster bomb”模式对Payload的多个变形点如类名、方法名、编码方式进行组合爆破。可以加载SecLists中的Fuzz字典或自己整理一份OGNL关键词变形字典。OGNL表达式生成脚本用Python或Go写一个小脚本可以根据模板快速生成各种绕过的Payload。例如输入一个命令cat /flag脚本自动输出字符串拼接、Hex编码、Unicode编码、反射调用等多种形式的Payload。Struts2漏洞扫描器如Struts2-Scan等开源工具可以自动化检测常见的Struts2漏洞如S2-016, S2-045, S2-052等。但工具只是辅助其Payload可能过时或被CTF题目特意防御理解原理后手工调整才是关键。Java反序列化工具链如果漏洞点与反序列化相关如S2-057的某些利用方式可能需要用到ysoserial等工具生成Payload。但ctfshow的S2系列主要聚焦于OGNL注入反序列化涉及较少。最后我想强调的是刷CTF题目的目的不是为了记住几个Payload而是锻炼一种“漏洞思维”。面对一个黑盒系统如何通过有限的信息参数、响应、报错推测其内部实现如何将已知的漏洞模式如OGNL注入与当前上下文结合如何一步步构造、测试、调整利用链直至成功。这种思维无论是在CTF赛场上还是在真实世界的渗透测试和代码审计中都是无比珍贵的。ctfshow的S2系列就像一个优秀的教练它把Struts2这个经典框架中最具代表性的安全问题浓缩在了一道道关卡里。通关之后你收获的绝不仅仅是几个flag而是一套应对同类问题的完整方法论。