JAVA反序列化 URLDNS链分析

[TOC]

12_5

最近想跟一下log4j的触发过程和相关绕过.想了想,之前看的java漫谈的相关内容好像也忘得差不多了,先捡一捡之前的内容吧

反射机制已经看过很多遍了,我们就从RMI开始看起吧

首先,RMI,remote method invocation,远程方法调用

到这里花了点时间去找了一下java 8 api 的中文文档,这样在idea就可以看到中文的介绍了.

好了说回来,p神的讲解是直接从一个简单的例子开始的

1
2
3
4
5
6
import  java.rmi.*;
public class RMIServer {
public interface IRemoteHelloWorld extends Remote{

}
}

认识一下remote接口

Remote接口用于标识可以从非本地虚拟机调用其方法的接口。 作为远程对象的任何对象都必须直接或间接实现此接口。

image-20211215200833666

UnicastRemoteObject
用于通过JRMP导出远程对象并获取与远程对象通信的存根。 存根在运行时使用动态代理对象生成,或者在构建时静态生成,通常使用rmic工具。

JRMP

根据wiki所说JRMP全称为Java Remote Method Protocol,也就是Java远程方法协议,通俗点解释,它就是一个协议,一个在TCP/IP之上的线路层协议,一个RMI的过程,是用到JRMP这个协议去组织数据格式然后通过TCP进行传输,从而达到RMI,也就是远程方法调用。

JNDI

JNDI全称为Java Naming and Directory Interface,也就是Java命名和目录接口。总结的来说:JNDI是一个接口,在这个接口下会有多种目录系统服务的实现,我们能通过名称等去找到相关的对象,并把它下载到客户端中来。

走到这里的时候去看了另一篇文章,实现了两个应用,一个应用App用来架设rmi服务,同时拥有一个已经实现的HelloService类,可以返回hello

另一个应用App2用来加载远程的HelloService类的sayhello()方法

我们通过对App2下断点进行跟踪

image-20211215230631550

经过两层调用到达

image-20211215230747938

到达lookup方法

image-20211215231355839

image-20211215232008608

image-20211215231720533

因为是双App调试,所有现在程序暂停在了App对于目录下HelloService.sayhello()方法的调用

image-20220414145257496

整个过程中涉及到多次lookup方法的执行.感觉上这个lookup方法在和远程类通讯,多次进行了传递参数和命令,检视远程类的状态.不知道我理解的对不对.

我们回到p神这边来

LocateRegistry

用于获取对特定主机(包括本地主机)上的引导远程对象注册表的引用,或用于创建接受特定端口上的调用的远程对象注册表。请注意, getRegistry呼叫实际上并不连接到远程主机。 它只是创建对远程注册表的本地引用,并且即使没有在远程主机上运行注册表,也将成功。 因此,作为此方法的结果返回的远程注册表的后续方法调用可能会失败。

LocateRegistry.createRegistry

在本地主机上创建并导出Registry实例,该实例接受指定的port的port 。

Naming

Naming类提供了存储和获取对远程对象注册表中远程对象的引用的方法。 Naming类的每个方法都将其作为参数的一个名称称为java.lang.String为URL格式(不含方案组件)的java.lang.String:
//host:port/name
其中host是注册表所在的主机(远程或本地), port是注册表接受呼叫的端口号, port是注册表name的简单字符串。 host和port都是可选的。 如果省略host ,则主机默认为本地主机。 如果port被省略,则端口默认为1099
绑定远程对象的名称是关联或注册远程对象的名称,以便稍后可以使用它来查找该远程对象。 远程对象可以使用Naming类的bind或rebind方法与名称相关bind 。
一旦远程对象与本地主机上的RMI注册表注册(绑定),远程(或本地)主机上的呼叫者可以通过名称查找远程对象,获取其引用,然后调用该对象上的远程方法。 注册表可能由主机上运行的所有服务器共享,或者单个服务器进程可以根据需要创建并使用自己的注册表

Naming.bind()
将指定的name name到远程对象。
参数:
name - URL格式的名称(不包括方案组件)
obj - 远程对象的引用(通常是存根)

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
import  java.rmi.*;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;


public class RMIServer {
public interface IRemoteHelloWorld extends Remote{
public String hello() throws RemoteException;
}
public class RemoteHelloWorld extends UnicastRemoteObject implements IRemoteHelloWorld{
protected RemoteHelloWorld() throws RemoteException{
super();
}

@Override
public String hello() throws RemoteException {
System.out.println("call from");
return "Hello world";
}
}
private void start() throws Exception{
RemoteHelloWorld h = new RemoteHelloWorld();
LocateRegistry.createRegistry(1099);
Naming.bind("rmi://127.0.0.1:1099/Hello",h);
}

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

一个RMIserver分为三个部分

  1. 一个实现了java.rmi.Remote的接口,其中定义了我们需要远程调用的函数,这里就是hello()方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public interface IRemoteHelloWorld extends Remote{
    public String hello() throws RemoteException;
    }
    public class RemoteHelloWorld extends UnicastRemoteObject implements IRemoteHelloWorld{
    protected RemoteHelloWorld() throws RemoteException{
    super();
    }

    @Override
    public String hello() throws RemoteException {
    System.out.println("call from");
    return "Hello world";
    }
    }
  2. 一个实现了RMIServer的接口类

    1
    2
    3
    public static void main(String[] args) throws Exception {
    new RMIServer().start();
    }
  3. 一个用来创建Registry的主类,并且实例化上述的类绑定到一个地址

    1
    2
    3
    4
    5
    private void start() throws Exception{
    RemoteHelloWorld h = new RemoteHelloWorld();
    LocateRegistry.createRegistry(1099);
    Naming.bind("rmi://127.0.0.1:1099/Hello",h);
    }

    我们通过上述的规则划分一下代码,为了简化p神将所有的代码放在了一个类中,但是功能是不变的

    到这里我们搞定了服务端,下面就要实现客户端了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import java.rmi.*;

    public class TrainMain {
    public static void main(String[] args) throws Exception{
    RMIServer.IRemoteHelloWorld hello = (RMIServer.IRemoteHelloWorld)Naming.lookup("rmi://172.17.1.14:1099/Hello");
    String ret=hello.hello();
    System.out.println("this is client printer");
    }
    }

    我们再来捋一下

    客户端的话,从

    1
    new RMIServer().start();

    然后开始注册registery,注册的时候初始化RemoteHelloWorld h,绑定端口,将绑定的链接和对象h注册到Registry注册中心

    这时候服务端的任务就完成了,只需要等待客户端的请求就可以了

    我们再来看客户端

    进入lookup方法,首先parseURL

    image-20211216114452039

在层层调用之后,走到了transform,和之前看的联系起来了image-20211216114811146

image-20211216115112954

调用到了registry.lookup()方法

在这里用到了反射以及远程方法调用image-20211216115448340

image-20211216115519913

然后我们用wireshark抓包分析一下,这里顺便积累一个tips

tcp.port==xxx 过滤端口

tcp.dstport== 目的端口

tcp.srcport== 源端口

同理

ip==

ip.src==

ip.dst==

条件过滤: a and b

image-20211216171022493

标记处为两次TCP握手,即建立了两次TCP链接

第一次tcp链接是链接172.17.1.14的1099端口,二者通过RMI协议进行交流之后,client向远端发送了Call命令,RMIServer回复了一个returnData,这里面应该是序列化的信息,并不能看到明文内容

然后我们新建了一个tcp链接,连到了服务端的53760端口,这里的端口源自于序列化信息中ip地址之后的一个字节的数据

image-20211216173409120

在我这里看到的是00 00 d2 00,转成十六进制转十进制就是53760.也就是说根据服务端返回的ReturnData,客户端向服务端发起了请求

所以捋⼀捋这整个过程,⾸先客户端连接Registry,并在其中寻找Name是Hello的对象,这个对应数据 流中的Call消息;然后Registry返回⼀个序列化的数据,这个就是找到的Name=Hello的对象,这个对应 数据流中的ReturnData消息;客户端反序列化该对象,发现该对象是⼀个远程对象,地址 在 172.17.1.14:53760,于是再与这个地址建⽴TCP连接;在这个新的连接中,才执⾏真正远程 ⽅法调⽤,也就是 hello()

image-20211216173751923RMI Registry就像⼀个⽹关,他⾃⼰是不会执⾏远程⽅法的,但RMI Server可以在上⾯注册⼀个Name 到对象的绑定关系;RMI Client通过Name向RMI Registry查询,得到这个绑定关系,然后再连接RMI Server;最后,远程⽅法实际上在RMI Server上调⽤。

理清楚了整个的流程,我们来看一下如何进行攻击

RMI利用codebase执行任意代码

classAnnotations是什么?

在序列化Java类的时候用到了一个类,叫 ObjectOutputStream 。这个类内部有一个方法

annotateClass , ObjectOutputStream 的子类有需要向序列化后的数据里放任何内容,都可以重写

这个方法,写入你自己想要写入的数据。然后反序列化时,就可以读取到这个信息并使用。

作者在文中对比了PHP的反序列化与JAVA的反序列化的一些区别,比如PHP的反序列化其实是一个内部的过程,开发者通过unserialize()函数拿到的就是一个完整的对象,只是能对这个对象进行一些初始化的操作,比如赋值之类的,所以大部分PHP反序列化漏洞的攻击点都不在于反序列化,只是通过反序列化控制了对象的一部分属性,再后续的代码中利用这些进行危险操作

Java设计 readObject 的思路和PHP的 __wakeup 不同点在于: readObject 倾向于解决反序列化时如何还原一个完整对象这个问题,而PHP的 __wakeup 更倾向于解决反序列化后如何初始化这个对象的

问题。

而JAVA的反序列化操作多数需要开发者深入参与.所以大量的库会实现readobject(),writeObject()方法

Java在序列化时一个对象,将会调用这个对象中的 writeObject 方法,参数类型是ObjectOutputStream ,开发者可以将任何内容写入这个stream中;反序列化时,会调用readObject ,开发者也可以从中读取出前面写入的内容,并进行处理。

通过读取反序列化的数据我们可以看到,我们写入的字符串 This is a object 被放在 objectAnnotation 的位置。

到了第八篇,开始由最简单的URLDNS链子入门,这里尝试一下能否进行调试

12_18

作者在调试的后面加了个注解

另外,ysoserial为了防止在生成payload的时候也执行也执⾏了URL请求和DNS查询,所以重写了⼀个 SilentURLStreamHandler 类,这不是必须的。

昨天一直在debug,以为是我传入的参数有问题,因为每次执行到getHostaddress的时候,返回值都是null.

image-20211218105216039

我们跟进一下这个方法,发现并没有什么异常

image-20211218105318746

但我们如果调试一下的话就会发现ysoserial在URLDNS类中自己实现了一个getHostAddress返回null,用来防止生成payload 的时候也会产生dns请求

image-20211218105530846

其实这里有一个疑问,两个getHostAddress()方法,一个在URLDNS的SilentURLStreamHandler类中实现,一个在URLStreamhandler中实现,二者的优先级是怎么样的

请教了下刘海霞老师,这里其实涉及到当时学java时的一个知识点,SilentURLStreamHandler继承了URLStreamhandler,重写并覆盖了父类的方法

再往前看,在URLDNS,getObject()方法中

image-20211218123520924

这种定义方式涉及到了JAVA的多态特性,正好花时间复习一下(当时就没学好)

【java基础】——java中父类声明子类实例化_汤庆-CSDN博客_java父类实例化子类

声明父类URLStreamHandler handler指向子类对象new SilentURLStreamHandler()可以保持父类的一些特征,比如如果子类重载了父类的同名方法,父类还是可以保持使用自己的方法,与此同时又可以利用子类的一些具体的方法,例如子类重写了父类的某些方法,那么handler在调用的时候,调用到的就是子类重写之后的方法.我们能看到URL u定义的时候接收到的参数是null,url,handler,所以最后在调用handler.getHostAddress的时候,调用的是子类重写之后的getHostAddress()方法.然后返回null,不会触发dns请求

最后我们来理一下整个的调用过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
传参---->GeneratePayload,main()
final Object object = payload.getObject(command);
------->URLDNS,getObject()
ht.put(u, url);
------->hashMap,put()
return putVal(hash(key), key, value, false, true);
------->hashMap,hash()
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
------->URLDNS,hashCode()
hashCode = handler.hashCode(this);
//这里我们就能看到调用了handler.hashCode(),前文
//URLStreamHandler handler=new SilentURLStreamHandler()
//因为hashCode()没有被子类重写,所以这里调用的是URLStreamHandler,hashCode()
------------------------------------->URLStreamHandler,hashCode()
InetAddress addr = getHostAddress();
//这里调用的是getHostAddress()方法,这个方法被子类重写过,所以调用子类的getHostAddress()
------->SilentURLStreamHandler,getHostAddress()
return null;
//如果我们按正常的调用进去看一下呢?
------->URLStreamHandler,getHostAddress()
u.hostAddress = InetAddress.getByName(host);
在这里就会触发DNS请求

在自己过完一遍以后,去网上看其他人的文章也更好理解了一些

https://www.anquanke.com/post/id/201762

上文中提到在跑通以上代码,有几个注意点:

  1. 1.不能使用ip+端口进行回显,因为此处功能为DNS查询,ip+端口不属于DNS查询。同时在代码底层对于ip的情况做了限制,不会进行DNS查询。
  2. 2.最好不要使用burp自带的dns查询,会过一段时间就会变换了,可能会导致坑。这里使用了ceye查看DNSLOG

确实一开始想用Python 起一个简单的http服务来做测试,看来这样是不可行的

仔细看一下可以知道最终的payload结构是 一个HashMap,里面包含了 一个修改了HashCode为-1的URL

*//通过url获取目标IP地址,再计算hash拼接进入*

InetAddress addr = getHostAddress(u);

但有一处值得提一下,之前说到URL要传入一个域名而不能是一个IP,IP不会触发DNS查询是在

java.net.InetAddress#getAllByName(java.lang.String, java.net.InetAddress)中进行了限制

回看payload生成

总结以上反序列化过程,我们可以得出要成功完成反序列化过程触发DNS请求,payload需要满足以下2个条件

  1. 1.HashMap对象中有一个key为URL对象的键值对
  2. 2.这个URL对象的hashcode需要为-1

其实找了这么多我到底在找什么,主要是想了解getByName()为什么就能像dnslog平台发出请求,然后ip地址就不行,这里实现的原理是什么?

然后我想到再去深挖一下getByName()的底层调用,我们来到了getAllByName(),首先来看一下关于这个方法的文档

Given the name of a host, returns an array of its IP addresses, based on the configured name service on the system.
The host name can either be a machine name, such as “java.sun.com”, or a textual representation of its IP address. If a literal IP address is supplied, only the validity of the address format is checked.
For host specified in literal IPv6 address, either the form defined in RFC 2732 or the literal IPv6 address format defined in RFC 2373 is accepted. A literal IPv6 address may also be qualified by appending a scoped zone identifier or scope_id. The syntax and usage of scope_ids is described here.
If the host is null then an InetAddress representing an address of the loopback interface is returned. See RFC 3330 section 2 and RFC 2373 section 2.5.3.
If there is a security manager and host is not null and host.length() is not equal to zero, the security manager’s checkConnect method is called with the hostname and -1 as its arguments to see if the operation is allowed.

image-20211218165547475

这里再往里深入其实并没有实现的代码了,主要实现都是通过JNI调用native方法了,也就是系统调用

1
JNI:java native interface

关于这部分内容在(24条消息) Java DNS查询内部实现_北岛知寒-CSDN博客这篇文章里有更加详细的分析

然后在这篇文章的reference中我还找到了另一个好玩的链接,Index for the letter J : Java Glossary (mindprod.com),有点名词解释大全的意思了

顺便除了dnslog之外https://log.xn--9tr.com/这个网站也挺好用

文章作者: Ch4n
文章链接: http://example.com/2021/12/18/JAVA反序列化-URLDNS链分析/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Ch4n's field