0x00 代码

struts.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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>
<package name="S2-001" extends="struts-default">
<action name="login" class="com.demo.action.loginAction" method="execute">
<interceptor-ref name="params"/>
<result name="success">/welcome.jsp</result>
<result name="error">/index.jsp</result>
</action>
</package>
</struts>

loginAction.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.demo.action;

public class loginAction {
private String userName;

public void setUserName(String userName) {
this.userName = userName;
}

public String getUserName() {
return userName;
}

public String execute(){
if(this.userName.equals("admin")){
return "success";
}else {
return "error";
}
}
}

index.jsp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<%@ taglib prefix="s" uri="/struts-tags" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Login</title>
</head>
<body>
<h2>S2-001 Demo</h2>
<s:form action="login">
<s:textfield name="userName" label="username"/>
<s:submit/>
</s:form>
</body>
</html>

welcome.jsp:

1
2
3
4
5
6
7
8
9
10
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
<head>
<title>welcome</title>
</head>
<body>
Hello,<s:property value="userName"/>
</body>
</html>

在调试的时候,为了关联St2的源码,需要在IDEA中配置maven.

0x01 POC

在username输入:

1
%{@java.lang.Runtime@getRuntime().exec("calc")}

0x02 分析

先简单看下Struts2的执行流程:

St2的执行流程分为2条主线:

  1. 初始化阶段.只在web应用启动初执行一次,进行相关初始化.

  2. Struts2处理http请求.这里又可以分为两个阶段

    1. St2对请求进行预处理,这个阶段主要是St2和web容器打交道,把http请求封装成java对象.为真正的业务逻辑执行做必要的数据环境和运行环境的准备

      1. 程序执行的控制权交给XWork,执行具体的业务逻辑.

通过以前对拦截器的学习,我们已经拦截器可以在调用action之前提供预处理逻辑.

其中,params拦截器用于设置action上的请求参数,它是默认被调用的.为了便于分析漏洞,我在struts.xml里手动写了出来,目的是为了在params拦截器处设置断点.

在IDEA内点击进入params,

继续进入ParametersInterceptor,

这里把传入的参数打入到值栈中,我们从这里下断开始调试.

com.opensymphony.xwork2.interceptor.ParametersInterceptor:159,

执行到167行,

步入invocation.invoke(),

步入executeResult(),执行到com.opensymphony.xwork2.DefaultActionInvocation:348,

步入dispatcher.forward(),这里继续步入IDEA会显示不出来源码.

请教了@chybeta之后,解决的办法是多次步入,直到进入org.apache.struts2.views.jsp.ComponentTagSupport:48,

从这里开始解析jsp页面内的标签了.首先是外层的form标签,这层可以直接跳出,直到解析userName,因为我们的payload在这里输入的.

继续步入,执行到org.apache.struts2.views.jsp.ComponentTagSupport:49,

首先解析开始标签,处理完之后,又回到index.jsp,

再次步入,执行到org.apache.struts2.views.jsp.ComponentTagSupport:43,

doEndTag()解析结束标签,继续步入compoent.end(),

执行到org.apache.struts2.components.UIBean:481,

步入evaluateParams(),

不难发现,这个方法的作用是获取标签的各项属性值,我们在index.jsp里只设置了两项属性.

name和label,

继续执行到altSyntax(),这个方法返回true,根据官方文档,altSyntax默认开启,为了动态的改变标签属性的值,它允许执行标签属性中的OGNL表达式.

同时,为了不过多的引入单引号,可以使用”%{…}”的形式来写入表达式.

此时,expr值为%{userName}

步入findValu(),执行到org.apache.struts2.components.Component:313,

步入TextParseUtil.translateVariables(),执行到com.opensymphony.xwork2.util.TextParseUtil:71,

步入translateVariables(),执行到com.opensymphony.xwork2.util.TextParseUtil,

这个方法很重要,里面是一个while循环,

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, ParsedValueEvaluator evaluator) {
// deal with the "pure" expressions first!
//expression = expression.trim();
Object result = expression;

while (true) {
int start = expression.indexOf(open + "{");
int length = expression.length();
int x = start + 2;
int end;
char c;
int count = 1;
while (start != -1 && x < length && count != 0) {
c = expression.charAt(x++);
if (c == '{') {
count++;
} else if (c == '}') {
count--;
}
}
end = x - 1;

if ((start != -1) && (end != -1) && (count == 0)) {
String var = expression.substring(start + 2, end);

Object o = stack.findValue(var, asType);
if (evaluator != null) {
o = evaluator.evaluate(o);
}


String left = expression.substring(0, start);
String right = expression.substring(end + 1);
if (o != null) {
if (TextUtils.stringSet(left)) {
result = left + o;
} else {
result = o;
}

if (TextUtils.stringSet(right)) {
result = result + right;
}

expression = left + o + right;
} else {
// the variable doesn't exist, so don't display anything
result = left + right;
expression = left + right;
}
} else {
break;
}
}

return XWorkConverter.getInstance().convertValue(stack.getContext(), result, asType);
}

跟如stack.findValue(),

来到com.opensymphony.xwork2.util.OgnlValueStack,

通过getValue()执行了OGNL表达式,获取了userName的值,也就是我们输入的payload.

然后赋值给了o,再赋值给result,最终赋值给了expression.

因为是在while循环里面,所以继续从expression中提取OGNL并执行.

所以漏洞就在这里产生了,因为他会递归解析我们的输入,所以只要输入%{OGNL},那么里面的OGNL就会被提取出来,再传入getValue()执行.

0x03 修复

在XWork 2.0.4中,取消了对OGNL的递归解析.

0x04 总结

第一次分析St2,难度还是不大,这次主要是看别人的文章,依葫芦画瓢,主要是为了熟悉调试技巧.

这个洞简单来说就是St2对参数值进行了迭代解析,引发OGNL表达式注入,最终导致命令执行.原理还是很简单.

0x05 参考

https://xz.aliyun.com/t/2044

http://sh3ll.me/archives/201703152213.txt

https://cwiki.apache.org/confluence/display/WW/S2-001