TCTF 2021——buggyLoader 题目分析

阅读量    97966 |

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

 

0x01 题目简介

前段时间TCTF 2021总决赛上出了一道java反序列化题目,碰巧前不久刚分析过shiro反序列化漏洞,如果在了解过shiro反序列化漏洞的重难点之后再看此题就会感觉比较简单,因为他们考察的知识点都是classloader相关内容。本篇文章将使用对比学习的方式一步步解决buggyLoader这道题目中遇到的问题,最后汇总知识点以及回答一些目前网上存在的问题。

题目默认页面如下图所示,主要考察Java反序列化的一些知识点,通过basic路由进行触发

 

0x02 环境搭建

0x1 docker搭建

为了更加方便的学习该题目,笔者首先进行调试环境搭建部署。好在该比赛提供了赛后复现docker

https://github.com/waderwu/My-CTF-Challenges/tree/master/0ctf-2021-final/buggyLoader

根据题目docker文件了解到,该java服务部署在nginx服务之后,官方提供的nginx docker做了一层转发,而且java题目所在的虚拟机网络接口为internal_network,这样不方便调试分析。在实际做题过程中可以修改docker-compose.yml将服务端口和调试直接映射出来,像下面一样修改题目部署文件。

version: '2.4'
services:
  web:
    build: ./
    ports:
      - "8811:8080"
      - "5566:5566"
    restart: always
    networks:
      - out_network
networks:
    out_network:
        driver_opts:
            com.docker.network.driver.mtu: 1400
        ipam:
            driver: default

0x2 调试环境

笔者采用此方法进行调试,直接将buggyloader.jar放在idea中,在项目配置中添加依赖即可

关于jar包的启动信息都存放在MANIFEST.MF文件中

通过该文件笔者得知该jar包启动类为org.springframework.boot.loader.JarLauncher

Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Implementation-Title: javaDeserializeLabs
Implementation-Version: 0.0.1-SNAPSHOT
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
Start-Class: com.yxxx.javasec.deserialize.JavaDeserializeLabsApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.4.4
Created-By: Maven Jar Plugin 3.2.0
Main-Class: org.springframework.boot.loader.JarLauncher

在jar包中找到该类,在配置好JDK环境后点击调试该类。

对于Spring Web服务就不多讲了,可读性比较高架构本身并不是很难,可以很轻松的在class文件中找到RequestMapping注解标识的路由。比如在indexController.class中的greeting函数是一个路由处理函数,如下图所示。

一开始将断点下在greeting函数上并不生效,最后用如下指令将jar包解开后,添加依赖进行调试

jar -xvf buggyloader.jar

 

0x03 题目分析

在做题目的过程中存在了很多问题,不过最后都一一解决了,有以下几个问题

1.该题目反序列化过程中不能存在数组的反序列化
2.之前分析shiro时的无数组型commons-collections5利用链在该题中报错了
3.新编写的无数组型commons-collections6利用链同样报错了

首先编写方便反序列化的封装函数

ByteArrayOutputStream btout = new ByteArrayOutputStream();
ObjectOutputStream objOut = new ObjectOutputStream(btout);
objOut.writeUTF("SJTU");
objOut.writeInt(1896);
objOut.writeObject(obj);
objOut.close();
byte[] exp = btout.toByteArray();
String data = Utils.bytesTohexString(exp);
System.out.println(data);

0x1 自定义ObjectInputStream

很快啊就找到了反序列化点,但是仔细看代码发现这个方法使用了MyObjectInputStream类进行反序列化

自定义的ObjectInputStream类如下,该类主要重写了resolveClass方法,将原先ObjectInputStream类采用的Class.forName方法替换成了classloader的加载方式,这就引来了很多问题

public class MyObjectInputStream extends ObjectInputStream {
    private ClassLoader classLoader;
    public MyObjectInputStream(InputStream inputStream) throws Exception {
        super(inputStream);
        URL[] urls = ((URLClassLoader)Transformer.class.getClassLoader()).getURLs();
        this.classLoader = new URLClassLoader(urls);
    }
    protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
        Class clazz = this.classLoader.loadClass(desc.getName());
        return clazz;
    }
}

对比TCTF、Shiro和原生ObjectInputStream的resolveClass如下图所示

在ObjectInputStream和Shiro中都采用了Class.forName的类加载机制,但是TCTF这道题仅仅使用了classloader加载。和之前分析Shiro反序列化过程一样需要引入一些类加载知识点

1.Class.forName不支持原生类型,但其他类型都是支持的
2.Class.loadClass不能加载原生类型和数组类型,其他类型都是支持的
3.类加载和当前调用类的Classloader有关

这里的原生类型指的是byte、short、int、long、float、double、char、boolean。需要特别注意的是Class.loadClass不支持加载数组类型

关于共同之处简单的讲这两个类加载都是基于ClassLoader