前言
继续看三个链
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
进一步跟进,调用了toString
,而这里的_obj
可控,所以是一条完全可控的toString
链那这样就可以替代CC5的BadAttributeValueExpException#readObject
,从而绕过其jdk>8u76的限制(多提一嘴),但ROME这条链子使用的是ROME依赖中的其他类来连接toString链
ToStringBean
前面我们也分析到了这个类是用来被委托实现对java bean调用toString方法的具体逻辑的,也就是会该类的toString方法会决定java bean如何输出,我们来看一下具体的实现:
有两个toString方法,我们前面调用的是无参的,先看一下无参的toString方法
前半部分主要是获取类名部分,这里是TemplatesImpl
,然后作为prefix传入带参的toString方法。
- 先调用
BeanIntrospector.getPropertyDescriptors(_beanClass)
获取_beanClass
的所有getter和setter方法的PropertyDescriptor
获取getter/setter的逻辑是,先从_introspected
这个HashMap中获取(相当于一个缓存),一开始肯定获取不到,所以会调用getPDs
方法去获取,获取到之后再缓存到_introspected
中,获取的逻辑比较清晰,和大部分获取java bean的getter/setter的逻辑基本一样:先获取所有方法,然后根据方法名来获取getter和setter,比如getter就是开头是”get”或”is”,由于这里不是根据属性名匹配的getter,所以只需要考虑开头就行了,不需要匹配属性名,setter同理。获取到getter的PropertyDescriptor
后,建立PropertyName
(getter名字去掉get且第四个字符小写)和PropertyDescriptor
的映射关系放入HashMap中
假设我们设置的_beanClass
是Templates
,那么获取到的getter就只有getOutputProperties
了。
- 反射调用获取的getter
由于是按顺序调用的pds数组中保存的getter,所以如果获取到有多个getter,而我们想要调用的getter在靠后的位置,这时候可能就容易出错,因此尽量选用父类或接口等定义了仅定义了我们需要的getter来作为_beanClass
利用链
1 | HashMap.readObject() |
-
表示这部分可以省略(跳过)
- Source:
java.util.HashMap#readObject()
- Chain:
ObjectBean.hashCode() -> EqualsBean.beanHashCode() -> ToStringBean.toString()
- Sink:
TemplatesImpl.getOutputProperties()
JdbcRowSetImpl.getDatabaseMetaData()
这里简单提一下为什么JdbcRowSetImpl.getDatabaseMetaData()
,其实自己试一下就知道了,前面说了会获取所有的getter来根据方法数组中的先后顺序来执行,会经过
1 | getQueryTimeout |
而在getDatabaseMetaData
之前的三个方法执行时都不会报错。而且,从getter
方法本身的作用来看,就是设置值,如果没有值的话,就设置为null,所以大部分的getter方法都是不会报错的,除非有其他的处理逻辑。而前面的这三个方法就是简单的设置值,所以不会报错,直接执行到getDatabaseMetaData
触发JNDI注入。
POC
1 | public class ROME1 { |
BeanShell
前置
Interpreter
bsh.Interpreter
是beanshell脚本解释器,可以解析执行beanshell的命令以及java代码。其成员变量中globalNameSpace
是NameSpace
类型的,用于保存全局变量信息,包括要执行的脚本、设置的变量等,我们可以通过eval
方法来注册或解析执行脚本(方法),通过set
来设置要传递给脚本方法的变量等。在beanshell中,方法被封装为BshMethod
、变量被封装为Variable
,下列代码可以注册exec
方法,然后条用exec方法弹出计算机:
1 | Interpreter interpreter = new Interpreter(); |
Interpreter
和其中的NameSpace
构成脚本执行的上下文,有了这两个类保存的信息,便可以调用beanshell解释器来执行java代码
XThis
XThis
继承与This
,This
是对前面说的上下文的封装,从其构造方法就可看出
而这里选取XThis
的原因是,其内部有一个Handler
内部类,实现了InvocationHandler
,可以作为代理类的handler来使用。该handler就是用来拦截Interpreter注册的函数的。
具体的处理逻辑和大部分的handler差不多,在invoke -> invokeImpl
方法中,先是单独处理equals、toString
等方法,其他无法直接处理的方法则调用反射去执行,执行的返回值用Primitive.unwrap
去封装
继续跟进看一下invokeMethod
方法,这个方法可以用来调用执行java代码,demo如下:
1 | Interpreter interpreter = new Interpreter(); |
在这个实例中,我们注册了xxx
方法,并调用XThis.invokeMethod
来执行xxx
方法,便会触发XThis$Handler#invoke
方法,实现代理劫持xxx
方法,而xxx
方法的处理逻辑根据我们传入XThis的NameSpace
和Interpreter
构成的上下文,获取BshMethod
后再使用反射来完成从beanshell注册方法到实际代码的调用。
所以我们根据XThis#Handler
动态代理就可以很简单的构造调用链了。至于Source的选择,常见的连接动态代理的点有HashMap#readObject -> key.hashCode
或PriorityQueue#readObject -> comparator.compare
,这两个都可以。
利用链
1 | PriorityQueue.readObject() |
- 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 | public class BeanShell1 { |
后续
不能使用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的子实现类的对应关系还不太明晰。
笔者只调试知道当是ProcessBuilder
时,会获取到BSHAllocationExpression
和BSHPrimarySuffix
,会先调用BSHPrimarySuffix#doSuffix
去解析,再调用BSHAllocationExpression#eval
。而这其中并不需要interpreter
去参与
当是Runtime
时,获取到的是BSHMethodInvocation
和BSHPrimarySuffix
,而当调用BSHMethodInvocation#eval
方法时,就会因为Interpreter
为null而报错了。
C3P0
c3p0
是用于创建和管理连接,利用“池”的方式复用连接减少资源开销,和其他数据源一样,也具有连接数控制、连接可靠性测试、连接泄露控制、缓存语句等功能。目前,hibernate
自带的连接池就是c3p0
,除此之外,还有Spring
也使用了。
具体关于C3P0连接池架构和各类功能的详解,可以参考https://www.cnblogs.com/ZhangZiSheng001/p/12080533.html
这里先放上一个基于ComboPooledDataSource
的类图
以及各类大致的作用:
类名 | 描述 |
---|---|
DataSource |
用于创建原生的Connection |
ConnectionPoolDataSource |
用于创建PooledConnection |
PooledDataSource |
用于支持对c3p0 连接池中连接数量和状态等的监控 |
IdentityTokenized |
用于支持注册功能。每个DataSource 实例都有一个identityToken ,用于在C3P0Registry 中注册 |
PoolBackedDataSourceBase |
实现了IdentityTokenized 接口,还持有PropertyChangeSupport 和VetoableChangeSupport 对象,并提供了添加和移除监听器的方法 |
AbstractPoolBackedDataSource |
实现了PooledDataSource 和DataSource |
AbstractComboPooledDataSource |
提供了数据源参数配置的setter/getter 方法 |
DriverManagerDataSource |
DataSource 实现类,用于创建原生的Connection |
WrapperConnectionPoolDataSource |
ConnectionPoolDataSource 实现类,用于创建PooledConnection |
C3P0PooledConnectionPoolManager |
连接池管理器,非常重要。用于创建连接池,并持有连接池的Map(根据账号密码匹配连接池)。 |
前置
URLClassLoader远程加载类
有以下两种写法来作为URLClassLoader进行远程类加载(需要外网),可以作为代码审计的Sink点,
- 第一种:
Class.forName
- 第二种:
loadClass
其实两种是一样的,Class.forName在底层调用了loadClass
1 | package gadgets.c3p0; |
PoolBackedDataSourceBase
根据前面的类图介绍,com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase
这个类用于支持监听器的功能,存放了PropertyChangeSupport 和 VetoableChangeSupport 对象这两个java原生的用于监控属性变化的监听器类。该类实现了Serializable接口,且writeObject方法中对特殊的属性进行的处理。
根据上面类图知道,我们需要保存connectionPoolDataSource
,如果其是不可反序列化的对象,会调用ReferenceIndirector #indirectForm
方法对其进行封装
跟进看一下com.mchange.v2.naming.ReferenceIndirector#indirectForm
,先调用getReference
获取Reference对象,再外层封装一个ReferenceSerialized
看完了readObject方法,我们知道对于不可反序列化的connectionPoolDataSource
(其实就是WrapperConnectionPoolDataSource
),对其进行特殊的处理,因此在readObject方法中必然会有相应的方法将其恢复
在readObject中调用((IndirectlySerialized) o).getObject()
来恢复,而前面封装返回的ReferenceSerialized
是该接口的实现类。
看一下ReferenceSerialized#getObject
方法,如果env和contextName都为空的情况下,会调用ReferenceableUtils#referenceToObject
来进一步处理,如果contextName可以设置为恶意的jndi地址的话,可以触发jndi注入。
回看一下writeObject处,可控点是connectionPoolDataSource
成员变量,是ConnectionPoolDataSource
接口类型,且要调用ReferenceIndirector#indirectForm
,其中会cast为Referenceable
然后再调用getReference获取reference,所以我们可以创建一个自定义类实现ConnectionPoolDataSource
和Referenceable
这两个接口,然后重写getReference
方法返回一个自定义的Reference
,然后再实例化ReferenceSerialized
,但是可惜的是,实例化ReferenceSerialized时传入的contextName
和我们可控的Reference
并没有关系,所以我们无法控制contextName
为我们想要的jndi地址。
所以我们只能跟进ReferenceableUtils#referenceToObject
,这里就和reference扯上关系了,看一下下面框框的代码就知道了
利用链
1 | PoolBackedDataSourceBase.readObject() |
Source:
PoolBackedDataSourceBase#readObject()
Chain:
ReferenceIndirector#getObject
ReferenceableUtils#referenceToObject()
Sink:
Class#forName() -> URLClassLoader#loadClass()
POC
1 | public class C3P0 { |
其他利用点
http://redteam.today/2020/04/18/c3p0%E7%9A%84%E4%B8%89%E4%B8%AAgadget/