前言:

在分析007的过程中发现007和001有一定的相似程度,但是在搭建环境的时候被网上不少文章给误导了,很明显这些文章并不是很清楚整个漏洞的触发条件以及原理,大部分的文章都会添加一个[ActionName]-validation.xml的验证表单,好像这个东西是整个漏洞触发的必要条件.后来发现这个是vulhub中带的,大家没有自己搭建环境就直接下载现成环境自然而然也不会去追究为什么会有这么一个东西以及他究竟是不是有必要的所以自己搭建环境进行复现是非常有必要的

0X01:环境搭建

S2-007究竟会在什么样的一个环境下触发,首先就需要直到为什么会造成这个漏洞

很多文章一开头就说了,这个漏洞发生在类型转换时如果发生错误(比如abcd不能转换为int类型)的时候会将错误保存起来,然后将发生错误的参数传入input视图(struct.xml中配置),input视图如果存在标签解析,而这个标签恰巧是我们的可控数值,这个时候就struct就会默认尝试先去使用ognl解析这个标签的值,如果此时我们的值是一个ognl表达式那么就会被解析了

所以能够造成此漏洞的需要的必要环境条件是

1.于存在类型转换错误

2.配置input视图

3.input视图需要存在标签解析

而不是什么[ActionName]-validation.xml

这里给出核心配置文件内容

struts.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.5//EN"
"http://struts.apache.org/dtds/struts-2.5.dtd">

<struts>

<constant name="struts.devMode" value="true"/>
<constant name="struts.enable.DynamicMethodInvocation" value="false"/>

<package name="myPackage" extends="struts-default">

<default-action-ref name="index" />

<action name="index" class="org.example.HelloWorldAction">
<result>/WEB-INF/jsp/index.jsp</result>
</action>

<action name="demo" class="org.example.HelloWorldAction">
<result name="success">/welcome.jsp</result>
<result name="input">/welcome.jsp</result>
</action>

</package>

</struts>

HelloWorldAction.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/*
* Copyright 2006 The Apache Software Foundation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.example;

import com.opensymphony.xwork2.ActionSupport;

public class HelloWorldAction extends ActionSupport {

private Integer payload = null;

public HelloWorldAction() {
}

public void setPayload(Integer payload) {
this.payload = payload;
}

public Integer setPayload() {
return this.payload;
}

public String execute() throws Exception {
return "success";
}
}

index.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>S2-007</title>
</head>
<body>
<h2>S2-007 Demo</h2>

<s:form action="demo">
<s:textfield name="payload" label="payload" />
<s:submit></s:submit>
</s:form>
</body>
</html>

welcome.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>S2-007</title>
</head>
<body>
<h2>S2-007 Demo</h2>

<s:form action="demo">
<s:textfield name="payload" label="payload" />
<s:submit></s:submit>
</s:form>
</body>
</html>

0X02:报错处理导致的字符串拼接

可以看到我的HelloWorldAction.java里面定义的payload是int类型的,而我们传入到Action里面的参数默认都是字符串,我传参123,其实是个字符串”123”,但是字符串123是可以被强制类型转换为int类型的,不会报错.但当我们输入的是字符的时候,便会触发报错,而恰巧就是这个地方,造成了漏洞

验证payload:

1
'+(#application)+'

首先带着我们的payload请求

我们在xwork.com.opensymphony.xwork2.conversion.impl.XWorkConverter.class中的handleConversionExpection方法处下断点,该方法是处理一个转换异常的句柄

可以看到创建了一个conversionErrors的map并且将造成异常的参数名和参数值存入,

realProperty=payload

value=’+(#application)+’

然后我们就来到了拦截器

将刚刚的conversionErrors取出来,然后进入while循环,if判断此时的propertyName和value是否为空,很明显他们不为空,propertyName是参数名,value是参数值

然后进入if,将对应的错误信息取出,message和action的值为

然后会进入下面的fakie.put方法

跟进getOverrideExpr

可以发现这里将value压入stake,然后又取出stake的top也就是又把value取出来进行拼接,拼接两个单引号,然后返回

这个地方就出了大问题了,假设这个地方没有进行字符串拼接,那无论如何我们的stak.findvalue取出来的值都会是一个单独的字符串,但是这个地方进行了拼接,拼接过后的payload就变成了’’+(#application)+’’这是一个可以被ognl解析的表达式,等等会被解析ASTAdd类型,然后执行ognl表达式导致注入

现在fakie中已经有我们的拼接后的表达式了

接着便会new一个PreResultListener,并且将fakie中的值取出来.

接着就要重新渲染一次页面了,这个时候会和S2-001一样调用dostarttag,doendtag,S2-001是由于循环解析导致了我们可以直接控制ognl解析内容,而S2-007是由于类型转换出错,现在需要通过input视图输出错误数据,而这个时候错误数据已经不是我们原本输入的错误数据了,而是经过字符串拼接过后的错误数据,而ognl又会解析这个拼接后的错误数据,所以直接执行ognl表达式了

来到xwork.com.opensymphony.xwork2.ognl.OgnlValueStack的tryFindValue方法

此时expr的值还为payload,现在需要去lookupForOverrides方法查询这个方法的值,跟进

从overrides中查找payload,而之前错误处理的时候已经将拼接过后的字符串传入进去了,所以这个地方会直接获取到

expr为拼接后的恶意payload

然后直接调用getValue->ognlUtil.getValue->Ognl.getValue

通过ASTAdd拆分为其他的ASTNode,分别调用

之后的过程就是ognl解析AST树的过程了,在对S2-003的文章中有详细介绍过

完整payload:

1
' + (#_memberAccess["allowStaticMethodAccess"]=true ,#context["xwork.MethodAccessor.denyMethodExecution"]=new java.lang.Boolean("false"),@org.apache.commons.io.IOUtils@toString(@java.lang.Runtime@getRuntime().exec('calc').getInputStream())) + '