@ Author: Patrilic
@ Time: 2019-8-15 02:11:50

0x00 前言

太菜了,马上进入大三了,最近java的中间件也陆陆续续的爆了很多洞,然后之前也经常接触类似于Weblogic反序列化、struts2命令执行之类的漏洞。借着重刷了一遍Fate Stay Night 06版,睡不着觉,干脆就来开始人生中第一个S2漏洞分析

0x01 漏洞链接

https://cwiki.apache.org/confluence/display/WW/S2-001
影响版本:

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

0x02 环境搭建

struts2包下载地址:http://archive.apache.org/dist/struts/binaries/struts-2.0.8-all.zip

搭建平台: MacOS Mojave 10.14.5
使用工具: IntelliJ IDEA
Tomcat版本:Apache Tomcat 8.5.16 - MxSrvs自带

首先New一个Project
ddca0c177f31ccb51326857cc63de34e

将需要的jar包放入/WEB-INF/lib目录下

1
2
3
4
5
commons-logging-1.0.4.jar
freemarker-2.3.8.jar
ognl-2.6.11.jar
struts2-core-2.0.8.jar
xwork-2.0.3.jar

19046de56e4a9f83e5eeee434f3ef471

创建index.jsp文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<%@ 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>
<h2>S2-001 Demo</h2>
<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文件

1
2
3
4
5
6
7
8
9
10
11
12
13
<%@ 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>

在src目录下创建Package com.demo.action
然后创建class

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
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";
}
}

导入包
File->Project Structure
c2721572cd14b75b50bd62cccab3e8dd
21aff5f519ea4ea23e7ee17e32e7dfaa

然后设置好tomcat配置就可以Run了
c9de8ac5327705aa2b9825e2f5e0786a

e5e2c3d3929818faaf2e41f45264a79d

如果遇到java.lang.IllegalStateException: struts.properties missing的问题
检查一下自己创建的包名或者类名是否与struts.xml中配置的一致

漏洞环境就已经搭好了

0x03 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对象也会被添加到上下文环境当中去。

Ognl表达式语言的作用

jsp页面取值用
EL表达式语言,也用于页面取值,是jsp页面取值的标准(默认就可以使用)
Ognl表达式语言,Struts标签默认支持的表达式语言,必须配置Struts标签用,不能离开Struts标签直接使用,就是说Ognl必须在Struts中使用
对比来看,EL使用范围更广,项目中不限制使用哪一种,哪一种熟悉就使用哪一种

OGNL 的基本语法

  1. 对Root对象的访问
    OGNL使用的是一种链式的风格进行对象的访问

    1
    2
    User user = new User("rcx", "123");
    System.out.println(Ognl.getValue("name", user));
  2. 对上下文对象的访问
    使用OGNL的时候如果不设置上下文对象,系统会自动创建一个上下文对象,如果传入的参数当中包含了上下文对象则会使用传入的上下文对象。当访问上下文环境当中的参数时候,需要在表达式前面加上’#’,表示了与访问Root对象的区别

    1
    2
    3
    4
    User user = new User("rcx", "123");
    Map<String, Object> context = new HashMap<String, Object>();
    context.put("user", user);
    System.out.println(Ognl.getValue("#user.name", context, user));
  3. 对静态变量的访问
    在OGNL表达式当中也可以访问静态变量或者调用静态方法,格式如@[class]@[field/method()]。

    1
    2
    Object object = Ognl.getValue("@com.rcx.ognl.Constant@ONE", null);
    System.out.println(object);
  4. 方法的调用
    如果需要调用Root对象或者上下文对象当中的方法也可以使用.+方法的方式来调用。甚至可以传入参数。

    1
    2
    3
    4
    5
    6
    7
    8
    User user = new User();
    Map<String, Object> context = new HashMap<String, Object>();
    context.put("name", "rcx");
    context.put("password", "password");

    System.out.println(Ognl.getValue("getName()", context, user));
    Ognl.getValue("setName(#name)", context, user);
    System.out.println(Ognl.getValue("getName()", context, user));
  5. 对数组和集合的访问
    OGNL支持对数组按照数组下标的顺序进行访问。此方式也适用于对集合的访问,对于Map支持使用键进行访问

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    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);
    try
    {
    System.out.println(Ognl.getValue("#strings[0]", context, user));
    System.out.println(Ognl.getValue("#list[0]", context, user));
    System.out.println(Ognl.getValue("#list[0 + 1]", context, user));
    System.out.println(Ognl.getValue("#map['key1']", context, user));
    System.out.println(Ognl.getValue("#map['key' + '2']", context, user));
    } ...
  6. 投影与选择
    OGNL支持类似数据库当中的选择与投影功能。
    投影:选出集合当中的相同属性组合成一个新的集合。语法为collection.{XXX},XXX就是集合中每个元素的公共属性。

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

    选择操作符有3种:

      ? :选择满足条件的所有元素

      ^:选择满足条件的第一个元素

      $:选择满足条件的最后一个元素

1
2
3
4
5
6
7
8
Person p1 = new Person(1, "name1");
Map<String, Object> context = new HashMap<String, Object>();
ArrayList<Person> list = new ArrayList<Person>();
list.add(p1);
context.put("list", list);

ArrayList<Integer> list2 = (ArrayList<Integer>) Ognl.getValue("#list.{id}",context,list);
System.out.println(list2);
  1. 创建对象
    OGNL支持直接使用表达式来创建对象。主要有三种情况:
    构造List对象:使用{},中间使用’,’进行分割如{“aa”, “bb”, “cc”}
    构造Map对象:使用#{},中间使用’,进行分割键值对,键值对使用’:’区分,如#{“key1” : “value1”, “key2” : “value2”}
    构造任意对象:直接使用已知的对象的构造方法进行构造。
1
2
3
4
5
6
Map<String, String> map = (Map<String, String>)Ognl.getValue("#{'key1':'value1'}", null);
System.out.println(map);
List<String> list = (List<String>)Ognl.getValue("{'key1','value1'}", null);
System.out.println(list);
Object object = Ognl.getValue("new java.lang.Object()", null);
System.out.println(object);

0x04 漏洞验证

173a3723c16631be26264844cdbf62f1

d984073fd34482b1ab9fdcbde28db701

0x05 漏洞分析

7b8b8f885d3938a55ac1f182230f389b.jpeg

Tomcat容器处理后,将http请求发送至struts2,然后分两个阶段:

  • 第一阶段: St2对请求进行预处理,这个阶段主要是St2和web容器打交道,把http请求封装成java对象.为真正的业务逻辑执行做必要的数据环境和运行环境的准备
  • 第二阶段:XWork,执行具体的业务逻辑.

首先来了解一下拦截器

java里的拦截器是动态拦截Action调用的对象,它提供了一种机制可以使开发者在一个Action执行的前后执行一段代码,也可以在一个Action执行前阻止其执行,同时也提供了一种可以提取Action中可重用部分代码的方式。在AOP中,拦截器用于在某个方法或者字段被访问之前,进行拦截然后再之前或者之后加入某些操作。目前,我们需要掌握的主要是Spring的拦截器,Struts2的拦截器不用深究,知道即可。

48f11e0431d2c25296631c524ecb17dc
在struts.xml里下一个拦截器,然后在struts_defalut.xml里能找到它拦截的class
6244e2c9092e27087d80960fd067f86c

11ae2cbbc91813e4be305fcd62fcd2f7
第87行、把传入的参数打入到值栈中,我们在这里下断点
210a7716d4e72acc34ea0c9e4debb639
继续往下,97行,带入到invocation.invoke()

c95f5460a33e0a3cce226cb863a704fe
继续跟
40d89332841b0919edbc30728ba0dff4
步入executeResult()

然后步入dispatcher.forward()

4d1c0453520932caa0527b00105e2931
调试到这里又到了index.jsp
然后就是在org.apache.struts2.views.jsp.ComponentTagSupport解析标签
83ab3028a4d3ce9e570b2dd77b6c06d4
然后步入evaluateParams()
148f0c15f9f6c13af04ed5bcf3357e66
继续执行到altSyntax(),这个方法返回true,根据https://cwiki.apache.org/confluence/display/WW/Alt+Syntax
,altSyntax默认开启,为了动态的改变标签属性的值,它允许执行标签属性中的OGNL表达式.

同时,为了不过多的引入单引号,可以使用”%{…}”的形式来写入表达式.

此后o为%{1+1},再对o进行了一番处理后,payload经过result变量,最终成为expression的值:
f079a36c37e194f8835ce649747acfae

2e1134a2480a0576252c8bffc59beea3
29bb80f0510fa9b3c240536a175a3c7b
最后Object o = stack.findValue(var, asType);执行payload
69e61401ac66f42f5b7f17f42870d541

0x06 总结

其实这个漏洞还是挺简单的,写的很乱,但是确实是跟着几篇文章一步一步自己跳出来的,也算是第一个调的java漏洞了,学到很多调试技巧,膜一下chybeta师傅..

0x07 链接

https://03i0.com/2018/04/08/S2-001%E8%B0%83%E8%AF%95%E5%88%86%E6%9E%90/

https://www.kingkk.com/2018/08/%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E5%AD%A6%E4%B9%A0struts2-S2-001/#%E6%BC%8F%E6%B4%9E%E5%88%A9%E7%94%A8

https://xz.aliyun.com/t/2044