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 的核心依赖

<dependency>
    <groupId>org.apache.struts</groupId>
    <artifactId>struts2-core</artifactId>
    <version>2.0.8</version>
</dependency>

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

<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 目录并创建类

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

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

<%@ 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,内容为:

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

<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

<dependency>
    <groupId>ognl</groupId>
    <artifactId>ognl</artifactId>
    <version>3.1.19</version>
</dependency>

我们先创建两个实体类

Address.java

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

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 方法的写法:

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

对应的代码

VisitRoot.java

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

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

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

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

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

方法的调用

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

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

MethodCall.java

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 支持使用键进行访问。

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 表达式当中支持操作符的简单运算。有如下所示:

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

投影与选择

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

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

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

    选择操作符有 3 种:

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

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

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

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

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"}
  • 构造任意对象:直接使用已知的对象的构造方法进行构造。
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

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. 基本对象树的访问
对象树的访问就是通过使用点号将对象的引用串联起来进行。
例如: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/


文章作者: Drun1baby
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Drun1baby !
评论
  目录