Java Struts2 学习与环境搭建
Drunkbaby Lv6

Java Struts2 学习与环境搭建

0x01 前言

写一篇防止其他师傅们踩坑的环境搭建文章哈哈,因为网上关于 struts2 的搭建坑比较多

0x02 Struts2 基础

Struts2 简介

Apache Struts2 是一个非常优秀的 JavaWeb MVC 框架,2007年2月第一个 full release 版本发布,直到今天,Struts 发布至 2.5.26 版本,而在这些版本中,安全更新已经更新至 S2-061,其中包含了非常多的 RCE 漏洞修复。

关于 Struts2 开发的教程其实很少,因为 Struts2 已经处于一个濒临淘汰的阶段,用的人已是甚少。下面说一说我个人对于 Struts2 的一些理解:

参考资料:Struts2入门这一篇就够了,写的非常好。

  • Struts2 比较像是 Spring 和 SpringMVC 的一个中间产物,它需要写一些比较复杂的 Spring 配置 xml(这种 xml 很容易写吐……);而它又具有 SpringMVC 的特性,但是并未较好的实现。所以有了我前面的思考 ———— Struts2 比较像是 Spring 和 SpringMVC 的一个中间产物。

Struts1 和 Struts2 在技术上是没有很大的关联的。 Struts2 其实基于 Web Work 框架的,只不过它的推广没有 Struts1 好,因此就拿着 Struts 这个名气推出了 Struts2 框架。

Struts2 执行流程

Struts2 是一个基于 MVC 设计模式的Web应用框架,它的本质就相当于一个 servlet,在 MVC 设计模式中,Struts2 作为控制器(Controller)来建立模型与视图的数据交互。Struts2 是在 Struts 和WebWork 的技术的基础上进行合并的全新的框架。Struts2 以 WebWork 为核心,采用拦截器的机制来处理的请求。这样的设计使得业务逻辑控制器能够与 ServletAPI 完全脱离开。

  • 对 Struts2 的执行流程简单说明
  1. Filter:首先经过核心的过滤器,即在 web.xml 中配置的 filter 及 filter-mapping,这部分通常会配置 /* 全部的路由交给 struts2 来处理。
  2. Interceptor-stack:执行拦截器,应用程序通常会在拦截器中实现一部分功能。也包括在 struts-core 包中 struts-default.xml 文件配置的默认的一些拦截器。
  3. 配置Action:根据访问路径,找到处理这个请求对应的 Action 控制类,通常配置在 struts.xml 中的 package 中。
  4. 最后由 Action 控制类执行请求的处理,执行结果可能是视图文件,可能是去访问另一个 Action,结果通过 HTTPServletResponse 响应。

如何实现 Action 控制类

通常有以下的方式

  • Action 写为一个 POJO 类,并且包含 excute() 方法。
  • Action 类实现 Action 接口。
  • Action 类继承 ActionSupport 类

0x03 环境搭建

IDEA 选中 web-app,一路 yes

导入 Struts2 的核心依赖

1
2
3
4
5
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-core</artifactId>
<version>2.0.8</version>
</dependency>

再修改 web.xml,在这里主要是配置 Struts2 的过滤器。

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

后续内容都是摘自 Y4tacker 师傅的文章了 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

main 下添加 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
package com.test.s2001.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";
}
}

然后,在 webapp 目录下创建&修改两个文件 —— index.jsp&welcome.jsp,内容如下。

index.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>S2-001</title>
</head>
<body>
<h2>S2-001 Demo</h2>
<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" %>

<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>

然后在 main 文件夹下创建一个 resources 文件夹,内部添加一个 struts.xml,内容为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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.test.s2001.action.LoginAction">
<result name="success">welcome.jsp</result>
<result name="error">index.jsp</result>
</action>
</package>
</struts>

最后配置 web.xml

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

再配置 tomcat,就起来了。

测试成功

0x04 OGNL 表达式

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

  • 这是很官方的说法,说白了就是 EL 表达式,因为之前学过一点 EL 表达式,其实 OGNL 表达式,还有 S2 系列漏洞的 payload 都非常像 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 对象也会被添加到上下文环境当中去。

说白了上下文就是一个 MAP 结构,它实现了 java.utils.Map 的接口。

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

OGNL 使用 getValue() 方法来获取对象,并且访问对象当中的值,在后续的代码块当中,师傅们可以自行打断点进行调试,只是一些简单的 getter 与 setter 数据处理与赋值。

对 Root 对象的访问

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

所谓的链式编程,则是类似与 StringBuffer 的 append 方法的写法:

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

对应的代码

VisitRoot.java

1
2
3
4
5
6
7
8
9
10
11
public class VisitRoot {  
public static void main(String[] args) throws Exception{
User user = new User("Drunkbaby", 20);
Address address = new Address("330108", "杭州市滨江区");
user.setAddress(address);
System.out.println(Ognl.getValue("name", user)); // Drunkbaby
System.out.println(Ognl.getValue("name.length()", user)); // 9
System.out.println(Ognl.getValue("address", user).toString()); // Address(port=330108, address=杭州市滨江区)
System.out.println(Ognl.getValue("address.port", user)); // 330108
}
}

对上下文对象的访问

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

当访问上下文环境当中的参数时候,需要在表达式前面加上 ‘#’ ,表示了与访问 Root 对象的区别。

VisitContext.java

1
2
3
4
5
6
7
8
9
10
11
12
13
public class VisitContext {  
public static void main(String[] args) throws Exception{
User user = new User("Drunkbaby", 20);
Address address = new Address("330108", "杭州市滨江区");
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)); // hello
System.out.println(Ognl.getValue("#user.name", context, user)); // test
System.out.println(Ognl.getValue("name", context, user)); // test
}
}

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

在 OGNL 表达式当中也可以访问静态变量或者调用静态方法,格式如 \@[class]@[field/method ()],猜测在后续的 S2 系列漏洞中会存在这种方式的攻击手法。

VisitStatic.java

1
2
3
4
5
6
7
8
9
10
11
12
13
public class VisitStatic {  

public static String ONE = "VisitStatic Success";

public static void main(String[] args) throws Exception{
AtVisit();
}
public static void AtVisit() throws OgnlException {
Object object1 = Ognl.getValue("@com.drunkbaby.OGNLGrammar.VisitStatic@ONE", null);
Object object2 = Ognl.getValue("@com.drunkbaby.OGNLGrammar.VisitContext@VisitContextMethod()", null); // hello、Drunkbaby、Drunkbaby
System.out.println(object1); // 访问 static 的 ONE System.out.println(object2); // 访问 VisitContext 的 VisitContextMethod() 方法
}
}

此处存在一个很有意思的现象,在这句语句执行的时候,会多返回一个 null

1
Object object2 = Ognl.getValue("@com.drunkbaby.OGNLGrammar.VisitContext@VisitContextMethod()", null);

实际上 object2 那里是不会得到返回值的,因为 VisitContextMethod() 方法是一个 void 方法,但是通过这一点,能明确的看到,在调用 getValue() 调用任意静态方法的时候,是和反射一样存在攻击面的,不过比较窄。

方法的调用

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

赋值的时候可以选择上下文当中的元素进行给 Root 对象的 name 属性赋值。

MethodCall.java

1
2
3
4
5
6
7
8
9
10
11
public class MethodCall {  
public static void main(String[] args) throws Exception{
User user = new User();
Map<String, Object> context = new HashMap<String, Object>();
context.put("name", "Drunkbaby");
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)); // Drunkbaby
}
}

对数组和集合的访问

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class VisitMaps {  
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 表达式当中支持操作符的简单运算。有如下所示:

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

投影与选择

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

  • 投影:选出集合当中的相同属性组合成一个新的集合。语法为 collection.{XXX},XXX 就是集合中每个元素的公共属性。

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

    选择操作符有 3 种:

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

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

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

说是投影与选择,实际上更像是元素截图,类似于 substr 这种。

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 SelectorAndProjection {  
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 对象:使用 {}, 中间使用 ‘,’ 进行分割如 {"aa", "bb", "cc"}
  • 构造 Map 对象:使用 #{},中间使用 ‘, 进行分割键值对,键值对使用 ‘:’ 区分,如 #{"key1" : "value1", "key2" : "value2"}
  • 构造任意对象:直接使用已知的对象的构造方法进行构造。
1
2
3
4
5
6
7
8
public class CreateClass {  
public static void main(String[] args) throws Exception{
System.out.println(Ognl.getValue("#{'key1':'value1'}", null)); // {key1=value1}
System.out.println(Ognl.getValue("{'key1','value1'}", null)); // [key1, value1]
System.out.println(Ognl.getValue("new com.drunkbaby.pojo.User()", null));
// User(name=null, age=0, address=null)
}
}
  • 不论是之前的调用静态方法,还是现在的创建对象,都是很有攻击面的存在。

弹计算器的 EXP

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

0x05 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
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. 对静态方法或变量的访问
要引用类的静态方法和字段,他们的表达方式是一样的@class@member或者@class@method(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的

0x06 参考资料

https://jueee.github.io/2020/08/2020-08-15-Ognl%E8%A1%A8%E8%BE%BE%E5%BC%8F%E7%9A%84%E5%9F%BA%E6%9C%AC%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95/

 评论