3.搭建环境
平台: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>
4.原理分析
漏洞部分代码
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表达式时就会进入
5.漏洞修复
判断了循环的次数,从而在解析到%{333*666}的时候不会继续向下递归,相当于限制了解析ongl的次数。
也就不会对用户提交的参数进行ongl解析
if (loopCount > maxLoopCount) {
// translateVariables prevent infinite loop / expression recursive evaluation
break;
}
6.OGNL表达式
这里搬运大佬总结好的东西。
OGNL は、Object-Graph Navigation Language の略称です。強力な式言語 (Expression Language、EL と呼ばれます) です。そのシンプルで一貫した式構文を通じて、オブジェクトの任意のプロパティにアクセスし、メソッドを呼び出すことができます。オブジェクトの構造図はオブジェクト全体の構造図を横断し、フィールド型変換などの機能を実装します。同じ式を使用してオブジェクトのプロパティにアクセスします。 OGNL の 3 つの要素: (以下の部分はインターネット上のどこかから抜粋しました。よく言われていると思います)
1. Expression
Expression は OGNL 全体の中核であり、 OGNL 操作は、式を解析した後に実行されます。式は、この OGNL 操作が何を行うかを指定します。上記のテストでは、名前、部門.名前などはすべて式であり、名前または部門内の名前の値が取得されることを示していることがわかります。 OGNL は多くの種類の式をサポートしています。詳細については後ほど説明します。
2. ルート オブジェクト
ルート オブジェクトは、OGNL の操作オブジェクトとして理解できます。式で「何をするか」を指定した後、それを「誰に」行うかを指定する必要もあります。上記のテスト コードでは、user がルート オブジェクトです。これは、ユーザー オブジェクトの name 属性の値を取得する (そしてユーザー オブジェクトの部門に name 属性値を設定する) 必要があることを意味します。
3. コンテキスト
式とルート オブジェクトを使用すると、実際に OGNL の基本機能を使用できます。たとえば、式に基づいてルート オブジェクトの値を取得または設定します。しかし実際には、OGNL 内ではすべての操作が特定の環境 (OGNL のコンテキスト) で実行されます。より明確に言うと、OGNL の操作を「どこで行うか」を指定するのはこのコンテキストです。
OGN L のコンテキストは、OgnlContext と呼ばれる Map 構造です。上で説明したルート オブジェクト (Root
Object) は実際にはコンテキスト環境に追加され、これは特別な変数として処理されます。特にルート オブジェクト (Root
Object) のアクセス操作式は必要ありません。 # 記号を追加することで区別できます。
# 式関数操作リスト: |
1. アクセス基本オブジェクト ツリーへ オブジェクト ツリーへのアクセスは、オブジェクト参照をドットで連結することによって実行されます。 例: xxxx、xxxx.xxxx、xxxx.xxxx。 例: #xxxx、#xxxx. xxxx、#xxxx.xxxxx. xxxx -、*、/、、--、==、!=、= などの演算子を使用することもできます。 mod、in、not in など。 4. コンテナ、配列、オブジェクト OGNL は、配列や ArrayList などのコンテナへの順次アクセスをサポートします。例: group.users[0] 同時に、OGNL はマップのキー値検索をサポートします。例: # session['mySessionPropKey'] それだけでなく、OGNL はコンテナ構築のための式もサポートしています: 例: {"green", "red", "blue"} は List, #{ "key1": "value1", "key2" : "value2", "key3" : "value3"}マップの構築 任意のクラス オブジェクトのコンストラクターを使用して新しいオブジェクトを作成することもできます 例: new Java.net.URL ("xxxxxx/")5. 静的メソッドまたは変数へのアクセス クラスの静的メソッドとフィールドを参照するには、それらの式は同じ @class@member または @class@method(args ): 6. メソッド 呼び出し は、Java のようなメソッド呼び出しを通じて直接実行されます。パラメーターを渡すこともできます: 例: user.getName()、group.users.size()、group.containsUser (#requestUser) 7. 投影と選択 OGNL は、データベースと同様の投影と選択をサポートします。 射影とは、** の各要素の同じ属性を選択して新しい ** を形成することであり、リレーショナル データベースのフィールド操作に似ています。射影操作の構文は collection.{XXX} です。XXX は、この ** 内の各要素のパブリック属性です。 例: group.userList.{username} は、グループ内のすべてのユーザーの名前のリストを取得します。 選択とは、リレーショナルデータベースのレコード操作と同様に、選択条件を満たす**要素をフィルタリングすることです。選択操作の構文は次のとおりです: collection.{X YYY}。X は選択演算子で、その後に選択の論理式が続きます。 3 つの選択演算子があります: ? 条件を満たすすべての要素を選択します ^ 条件を満たす最初の要素を選択します $ 条件を満たす最後の要素を選択します 例: group.userList .{ ? #txxx.xxx != null} は、特定のグループ内で名前が空ではないユーザーのリストを取得します。
|