青少年CTF S1 · 2026 公益赛wp
有一部分是luoyinhui和温歧做的
当然也有一部分是ai的,后边我再慢慢改,至少保证misc是全手写的
Pwn
好多”后”门

保护:

只开了NX,我们扔到IDA看看

发现有很多后门啊,但是都是伪装的,是假的后门
还是先看main函数吧

上来会到Team函数里

然后这边有一个很明显的栈溢出

buf的缓冲区只有0x90,但是可以写0x100的字符
所以我们可以利用缓冲区溢出到任何一个我们想执行的函数
一个个找后门函数即可,当然我们也可以直接定位调用了system函数的位置

发现就这一个函数用过
过去看看这个函数

发现确实是一个后门函数,确定地址即可

于是可以编写payload了
1 | |

成功得到flag
study_system

1 | |

Misc
玫坏的压缩包
拿到题目,首先发现文件头损坏了,zip的文件头该是504B0304才对

这导致了010无法识别这个压缩包的Variables,下边是空的

所以我们先修补文件头,修完后重新放入

修好了这个之后010即可识别出record的Variables
接下来我们查看这个record

发现明显只有一个文件flag.docx,这边却说长度为0,明显是坏了,于是我们可以修好第二处,改为9

改完9后再放进去,发现三个都识别出来了,在这边意味着record区已经完全修好了,其实我们也可以直接拿7zip直接解压了(因为我们7zip是只看record区的)

当然,正常完全修好的话就该看dirEntry区了
dirEntry区作为中央目录,它和record区是双向关联,功能互补,冗余验证的,这边明显剩下两点是dirEntry区坏了,自然是可以利用record区的来对照修补的

首先是这个deCompression坏了,应该是0

其次是这边的deFileName出现了丢失
总结,一共坏了4处——文件头、record区的frFileNameLength
以及dirEntry区的deFileName和deCompression
全部修补好后bandzip和winrar也能打开了

修完后打开

发现一片空白
我们打开字体看一下,发现开了隐藏,还字体白的

将字体颜色改掉、隐藏关掉

即可得到flag
flag{w3_w111_411_60_fur7h3r_4nd_fur7h3r}
哦


打开附件,基本上可以看出来是被按8字节做了倒序,倒回来是个zip
所以我们直接进行倒转即可
1 | |
倒转完之后发现是这样子一个压缩包

明显是明文,我们构建png文件头进行攻击

1 | |

换个密码
1 | |


得到了一张png图片,分析png隐写

binwalk发现有第二张图片

进行提取

combine之后发现有蓝纹,明显是双图盲水印
1 | |

解得flag{01d38cf8-e6f9-11f0-8fcd-11155d4a}
Ollama Prompt Injection


打开环境之后发现只有一个Ollama is running的标识
这像是Ollama的根路径,不提供交互
在url之后加入后缀看看是不是官方的OllamaAPI路径风格
1 | |


发现确实是官方的OllamaAPI路径风格,访问/api/tags返回了俩模型


一个是ctf-model:latest,明显是这一题的定制模型
这题叫Prompt Injection,即提示词注入攻击,我们的想法肯定是去聊天骗出flag,或者去直接读取设置的系统提示词,看看有没有暴露面漏洞
那根据官方Ollama的API路径风格,如果我们想读取模型的构建信息,我们可以直接去/api/show看
根据设置,需要POST看
我们要看的是ctf-model这个模型,所以model设置为这个
想看system配置的话需要打开verbose参数,即返回更完整的细节(包括modelfile、template和system)
1 | |

搜索即可发现系统设置如下
1 | |
即可得到flag:qsnctf{2e132e76c1a84ee3872d63c96af1854b}
当然作为一道提示词注入的题目,正常做法肯定是骗ai输出了
比如用代码执行提示:
1 | |


(不一样是因为我重启了环境,但它真的被骗出来了)
因此这一种也可以,算是正常路径(?)
QSNCTF

这签到题第三周才上吗(

flag{9cbbd6416d7b949b1d38869b605de589}
灵异事件?

对一下脑电波的话我觉得这个灵异是“零一”的意思
直接二进制转字符

flag{4d247ac316b1cf7ef53a159c3016bab9}
qr


打开来全是01

80000个字符,一半空格,题目说了Qr,那应该是200*200的二维码
直接01转qrcode
1 | |
得到二维码,扫码即可

得到flag
flag{56876aae7cb7b98a3756bac05c6b6675}
好,把他们上市!


拿来就是俩压缩包,在一个优质牢房里

很显然最终目标就是拿到flag之门的钥匙

而纸条中有两个文件,又是两个压缩包,疯狂套娃啊

这边可以看到很标准的Deflate压缩方式,还是ZipCrypto加密方式,最后CRC还一样,明显就是明文爆破
我们爆破一下

成功得到了解密密钥,解开即可

得到了pass.txt,打开可以明显发现应该是png文件才对

改后缀之后得到了一个二维码,解开得到密钥
password{1145141919810}
依旧misc常数,密码就是后边的数字(所以改成7z格式是怕被爆破出来吗)

得到flag:flag{VmpKb2FHUkdPVmhSVms1bVYxUkNNV05zT1U1VFZrNVVUVVUxWm1GWE5XWlZNbWhvWW0xa1NWRlZhejA9!!!}
欸不对吗,还不是flag啊,那base64解一下

得到真正的flag:flag{What_WAS_Y0ur_MISS0N_in_ShangHAI!!!}(注意这边的!!!,不在base64内)
找到呆唯


拿到手是两个文件,一个txt,一个提示zip
我们看看提示

喜欢符号和数字?工具还加密了,那根据提示应该是用符号和数字进行爆破了

很快啊,爆破得到密码为1/45*

得到了阴阳怪气编码的文字

得到网址https://tools.cmdragon.cn/zh/apps/steganography-tool
上去看看

是图像隐藏的网址,我们处理一下那个base64的txt

直接放到厨子就能变成jpg形式了

提取出了haha.png

得到的是一个二维码

微信可以扫出来,是flag{iam_here!!!}
刚刚这一长串是正常做法,我们ai真是神了,直接跳过了解网址环节,直接自己想出怎么提取png来了
1 | |
能直接得到flag,神了

消失的Yui

给了一个压缩包和一个txt文件

txt明显存在零宽

得到上述线索,然后上边竟然全都是没用的你敢信
真正的密码需要我们提取emoji然后base100解密

得到密码TrY_to_F1Nd_m3

打开来时间的谜语,明显零宽

依旧musc

还有一个二维码,明显base64,解一下,发现解不了,明显是解密方式不对
需要时间的谜语才能解出,而时间的谜语更是个musc,是1145141919810这个CTF经典数字

最后解密方式是看四周有点黑想到盲水印

解得一个经纬度,查找后定位是合肥
所以根据一开始的零宽,得到flag是flag{hefei}
Crypto
Four Ways to the Truth
题目信息
- 题目名称: Four Ways to the Truth
- 类型: Crypto (密码学)
- 给定参数:
p(大素数)q(大素数)e = 2c(密文)
- 提示: “并非所有缺失的参数都是真正“缺失”的” (Not all missing parameters are truly “missing”)
原理分析:Rabin 密码体制
题目中给出了 $ e=2 $,这非常典型地指向了 Rabin 密码体制。
Rabin 密码体制是一种基于模平方根难度的非对称加密算法。其安全性依赖于分解大整数 $ n $ ($ n=pq $) 的困难性。
加密过程
给定公钥 $ n $ ($ n = p \times q $),明文 $ m $ (其中 $ m < n $) 的加密过程为:
$ c \equiv m^2 \pmod n $
解密过程
解密需要私钥 $ p $ 和 $ q $。解密过程本质上是求解模 $ n $ 的二次同余方程:
$ x^2 \equiv c \pmod n $
由于 $ n = p \times q $,我们可以分别求解:
- $ x^2 \equiv c \pmod p $
- $ x^2 \equiv c \pmod q $
得到 $ m_p $ 和 $ m_q $ 后,利用 中国剩余定理 (CRT) 将其组合,可以得到模 $ n $ 下的四个解。这也是题目名称 “Four Ways to the Truth” 的含义——方程有四个根,其中一个是真正的明文。
解题步骤
- 求解模 $ p $ 和模 $ q $ 的平方根
首先检查 $ p $ 和 $ q $ 的性质。在本题中,计算发现:
$ p \equiv 3 \pmod 4 $
$ q \equiv 3 \pmod 4 $
对于满足 $ p \equiv 3 \pmod 4 $ 的素数,求平方根有简便公式:
$ m_p \equiv c^{(p+1)/4} \pmod p $
$ m_q \equiv c^{(q+1)/4} \pmod q $
- 使用中国剩余定理 (CRT) 组合解
我们有以下同余方程组:
$ \begin{cases}
x \equiv \pm m_p \pmod p \
x \equiv \pm m_q \pmod q
\end{cases} $
组合 $ (m_p, m_q) $, $ (m_p, -m_q) $, $ (-m_p, m_q) $, $ (-m_p, -m_q) $ 四种情况。
利用扩展欧几里得算法求出 $ p $ 和 $ q $ 的系数 $ y_p, y_q $,使得:
$ y_p \cdot p + y_q \cdot q = 1 $
通解公式为:
$ x = (y_q \cdot q \cdot (\pm m_p) + y_p \cdot p \cdot (\pm m_q)) \pmod n $
1 | |
Half a Key
- 题目背景
在 RSA 公钥密码体制中,为了提高解密速度,常常使用中国剩余定理(CRT)进行优化。
标准 RSA 解密需要计算 $ m = c^d \pmod n $,其中 $ d $ 是私钥指数,往往很大,计算耗时。
使用 CRT 优化后,系统会预先计算以下参数:
- $ dp = d \pmod{p-1} $
- $ dq = d \pmod{q-1} $
- $ q_{inv} = q^{-1} \pmod p $
本题的情景是:系统泄露了公开参数 $ (n, e) $ 和 CRT 优化参数中的 $ dp $,以及密文 $ c $。我们需要利用这些信息恢复明文。
- 原理分析
我们已知 RSA 的基本关系:
$ e \cdot d \equiv 1 \pmod{\phi(n)} $
其中 $ \phi(n) = (p-1)(q-1) $。这意味着存在整数 $ k’ $ 使得:
$ e \cdot d = 1 + k’ \cdot (p-1)(q-1) $
因此:
$ e \cdot d \equiv 1 \pmod{p-1} $
根据 $ dp $ 的定义:
$ dp \equiv d \pmod{p-1} $
我们可以将 $ d $ 写为 $ d = dp + m \cdot (p-1) $,代入上式:
$ e \cdot (dp + m \cdot (p-1)) \equiv 1 \pmod{p-1} $
$ e \cdot dp + e \cdot m \cdot (p-1) \equiv 1 \pmod{p-1} $
$ e \cdot dp \equiv 1 \pmod{p-1} $
这意味着 $ e \cdot dp - 1 $ 是 $ p-1 $ 的倍数。即存在整数 $ k $ 使得:
$ e \cdot dp - 1 = k \cdot (p-1) $
由此可得 $ p $ 的表达式:
$ p - 1 = \frac{e \cdot dp - 1}{k} \implies p = \frac{e \cdot dp - 1}{k} + 1 $
- 攻击思路
由于 $ dp < p-1 $ 且 $ e $ 通常较小(本题中 $ e=65537 $),$ k $ 的范围在 $ 1 $ 到 $ e $ 之间。
我们可以通过遍历 $ k \in [1, e) $ 来寻找 $ p $:
- 计算 $ X = e \cdot dp - 1 $。
- 遍历 $ k $ 从 1 到 $ e-1 $。
- 如果 $ X $ 能被 $ k $ 整除,计算候选值 $ p_{cand} = X // k + 1 $。
- 验证 $ p_{cand} $ 是否能整除 $ n $(即 $ n \pmod{p_{cand}} == 0 $)。
- 如果验证通过,则找到了素数 $ p $。
一旦找到 $ p $,即可算出 $ q = n / p $,进而算出 $ \phi(n) $ 和私钥 $ d $,最后解密密文。
1 | |
0x42F

像是一道比谁见过这个网站的题目

把0x42F改为十进制1071作为密码填入即可
我感觉有点意义不明。
得到flag:qsnctf{W31C0M3_70_3M0J!}
NO ASCII

算是最简单的签到题吧,本来格式就很明显了,还有题目提示
问的是在邮件、网络传输中保证非ASCII码安全传输的是什么?
就是URL编码的作用
所以我们直接使用Cyberchef进行解码即可

得到flag{青少年CTF欢迎你}
easy RSA

一个很小很小的N值,很容易被yafu或者factordb分解

我们发现了N是p的平方*q,所以算的时候不能直接用(p-1)(q-1)
对一般的素因子幂分解 N = ∏ p_i^{k_i},有:
φ(N) = ∏ (p_i^{k_i-1} * (p_i - 1))
本题N = p^2 * q^1:φ(N) = p^(2-1)(p-1) * q^(1-1)(q-1) = p*(p-1)*(q-1)
这是本题比较特殊的一个部分,别的都一样
1 | |
算出m为0x66316167
所以flag{f1ag}
easy RC4

已经说明了加密方式,密文和Key
但是发现解不出来
这一题恶心在看到密文是 Base64,并且普通 RC4 失败,需要我们动脑洞去猜很可能有 salt 或 key 派生
说是CTF很常见 Base64( salt || RC4( sha1(key||salt), plaintext ) )
1 | |
需要我们在Base64解码后用前十六字节作为salt,而key也不是这个key,而是SHA1(key+salt)的digest
下文其实写了
【春秋云实企安殿】听说是rc4算法 - Misc - 10pt - Misc - 青少年CTF论坛 - 青少年CTF初学者起源地 | CTF技术论坛
得到flag
flag{e12ax8u}
字符串的秘密


看见这个txt文件,一开始可能是怀疑是凯撒,但是看见For,就感觉不是了
像是单表替换密码,不过给了For example了已经,解决了不少,别的可以推测一下
差不多得到下边这样子的映射表
1 | |
所以原文就变成了下边这样子
1 | |

得到flag
flag{50_345Y_CRY}
Knapsack

(我能说不会吗(ai的先贴下边了))
1)先从题目脚本判断它是什么体系
enc.py 的密钥生成逻辑是:
- 私钥
sk是超递增序列:每一项都随机取在(sum(sk)+1, upper),因此必然满足sk[i] > sum(sk[:i])。 - 然后取一个模数
N(大约在sk[-1]到2*sk[-1]之间)。 - 再取一个与
N互素的mask,把私钥“打乱”成公钥:pk[i] = sk[i] * mask mod N。
加密就是纯粹的线性组合(背包/子集和):

结论:这是经典的 Merkle–Hellman 背包变体:私钥超递增→用模乘掩码隐藏→公钥变成“看起来随机”的权重,密文是 0/1 子集和。
2)为什么只给 pk 和 enc 也能恢复明文
如果你知道 (N, mask),确实能把公钥“反掩码”回超递增私钥,再用贪心恢复比特;但题目把它们隐藏了。
关键点在于:这类背包在参数密度很低时,可以直接被 LLL 格基约简解掉。
- 你的数据里
n = len(pk) = 136(也就是 136 个比特 = 17 字节)。 pk[i]的量级大约 500 多比特,所以“密度”约为n / log2(max(pk)) ≈ 0.25,属于 LLL 很容易成功的区间(密度越低越好解)。
3)核心思路:把 0/1 子集和变成“找一个很短的格向量”
目标是找 x_i ∈ {0,1} 使得:

构造一个 (n+1)×(n+1)(n+1)\times(n+1)(n+1)×(n+1) 的格基(embedding):
- 第 i 行(0≤i<n):第 i 列放
2,最后一列放2*pk[i] - 最后一行:前 n 列全放
1,最后一列放2*enc
然后考虑向量:

它会变成:
- 前 n 维:
2*x_i - 1,所以每一维都在{+1, -1}(非常小) - 最后一维:
2*(Σ x_i pk_i - enc),如果刚好满足子集和,则最后一维为0
也就是说,正确解对应一个“前 n 维全是 ±1、最后一维是 0”的极短向量(长度大约 n\sqrt{n}n),LLL 的工作就是把这种短向量从格里“约简”出来。
×2 和 ±1 的设计只是为了让“正确解”在几何上非常短、非常显眼,从而被 LLL 优先找到。
4)拿到比特串后怎么还原明文
脚本把 FLAG 做了:FLAG bytes -> hex -> int -> bin,并且为了保证字节对齐,会在左边补 0 使长度是 8 的倍数。
所以我们恢复出的 136 位比特串,直接当作二进制转成整数,再转 17 字节(大端),就是原始明文。
5)代码
1 | |
得到flag
flag{345Y_CRYP70}
big e

共模攻击
1 | |
得到flag:qsnctf{ba1073db090b3090c111339b0a7ffce5}
Web
S1签到



得到flag
silent_logger
SQL 注入

向 <font style="color:rgb(51, 51, 51);background-color:rgb(243, 244, 244);">/search</font> 提交 POST 请求输入’ or 1=1 order by 1;–+返回查询错误: sql: expected 0 destination arguments in Scan, not 3推断后端查询语句返回了3 个字段
‘ UNION SELECT 1,2,3–

存在联合查询注入,且列数确实为 3获取所有表名
‘ UNION SELECT 1,name,sql FROM sqlite_master WHERE type=’table’–

‘ UNION SELECT id,value,’x’ FROM flags–

时间胶囊留言板

/get_content.php?id=2拿到flag
CallBack
- 我们有一个简单的 PHP 脚本,负责处理用户输入,并通过回调函数对数组进行操作,然而,这个脚本并未对输入进行严格的过滤。你是否能发现某些细节并利用它来深入了解更多信息?
1 | |
- 可以直接利用 array_map 的回调机制:
- array_map 会依次将数组中的元素(0, 1, 2, 3)作为参数传递给回调函数,如果我们传入 phpinfo 作为回调函数,PHP 实际上会执行:
- phpinfo(0)
- phpinfo(1)
- …
phpinfo() 函数接受一个可选的整数参数来决定显示哪些信息(例如 1 表示常规信息,2 表示配置信息等)。无论参数是什么, phpinfo() 都会输出大量的系统配置信息。
- 访问
/?callback=phpinfo得到:

preg_replace
1 | |

- 反引号绕过引号转义限制
答案之书
- 传闻世间有一本《答案之书》,能解众生心中困惑。你只需虔诚地递上你的疑问,它便会给予你命运的指引。
然而,书页之间似乎隐藏着某种古老的禁制,唯有避开那些“禁忌之语”,方能窥见真实的奥秘。 - 万物皆有裂痕,那是光照进来的地方。你能否在禁忌的边缘,寻得那最终的真相(Flag)?
漏洞与突破过程
- 漏洞发现:
向输入框提交{{1+1}},页面回显「 你虔诚地询问:2 」,确认存在 Jinja2 (Python) 的 SSTI 漏洞。 - WAF 探测:
系统部署了过滤器(WAF),会拦截包含class,mro,subclasses,config,os,system,__(双下划线) 等敏感关键词的 Payload,并显示“非法的祈祷”。 - WAF 绕过:
利用 Python 的字符串拼接特性绕过关键字检测。__class__替换为'_'+'_'+'cl'+'ass'+'_'+'_'__mro__替换为'_'+'_'+'m'+'ro'+'_'+'_'__subclasses__替换为'_'+'_'+'subc'+'lasses'+'_'+'_'popen替换为'po'+'pen'
- 最终 Payload:
我构建了一个 Python 脚本来自动拼接 Payload,通过str类的__subclasses__找到os._wrap_close类(通常包含popen方法),从而执行系统命令。
读取 Flag 的 Payload (概念版):
1 | |
实际发送的 Payload (绕过版):
1 | |

easy_php
- 找入口:哪里接收了用户输入并进行了
unserialize()? - 找终点:哪里有危险函数(如
system,eval,exec等)? - 找桥梁:通过魔术方法(Magic Methods)把入口和终点连起来。
寻找入口
1 | |
- 我们控制了
$input变量。 unserialize($input)会把我们输入的字符串变成一个 PHP 对象。- 关键点:我们可以生成代码中定义的任意类的对象,并控制它们的属性值。
第二步:寻找魔术方法
反序列化漏洞通常需要魔术方法来触发。常见的有:
__destruct(): 对象销毁时自动调用(最常用)。__wakeup():unserialize()执行前自动调用。__toString(): 对象被当做字符串输出时调用。
在Monitor类中找到了__destruct():
1 | |
触发点:
- 我们需要满足条件
$this->status === "danger"。 - 满足后,它会调用
$this->reporter对象的alert()方法。
第三步:寻找利用链
现在我们停在 Monitor::__destruct 里,代码试图执行 $this->reporter->alert()。
我们需要思考:$this->reporter** 是什么?**
在正常的代码逻辑里(看 __construct),它是一个 Logger 对象:
1 | |
如果 $this->reporter 是 Logger,调用 alert() 只会打印一句话,没有危害。
但是! 作为攻击者,我们可以控制序列化数据。我们可以把 $this->reporter 换成任何拥有 alert() 方法的对象。
我们在代码里搜索 alert,发现了 Screen 类:
1 | |
- 如果我们将
Monitor的$reporter属性设置为一个Screen对象… - 那么
Monitor::__destruct()就会去调用Screen::alert()。 - 在
Screen::alert()中:$func($this->content)是一个动态函数调用。 - 如果我们控制了
$format和$content,我们就控制了执行什么函数!
第四步:构造终点
我们的目标是读取根目录下的 FLAG。
在 Screen::alert() 中:
- 令
$this->format = "system" - 令
$this->content = "cat /flag" - 执行结果就是:
system("cat /flag")-> 代码执行成功!
第五步:绕过过滤(Bypass)
回到入口处,我们发现有一个正则检查:
1 | |
它禁止输入字符串中包含 flag(不区分大小写)。
绕过技巧:
在 Linux Shell 命令中,我们可以使用通配符。
cat /flag-> 被拦截cat /f*-> 匹配/f开头的文件(即/flag),成功绕过。
所以,我们的 $content 应该设置为 "cat /f*"。
第六步:处理私有属性(Private Properties)
最后看一眼 Monitor 类的定义:
1 | |
属性是 private 的。在 PHP 序列化中:
public属性名直接存储:nameprotected属性名存储为:\0*\0nameprivate属性名存储为:\0ClassName\0name
这里的 \0 代表 ASCII 码为 0 的空字符(Null Byte)。因为我们在文本编辑器里打不出空字符,所以直接手写序列化字符串很容易出错。
最佳实践:
使用 PHP 脚本来生成 Payload,而不是手写。
1 | |

serialization
这个解法利用了 PHP 的 php://filter 伪协议结合 convert.base64-decode 过滤器来绕过 exit 语句,成功写入 webshell。
漏洞分析
题目中的 FileCache 类使用 file_put_contents 写文件,但在写入的内容前强制拼接了一个安全头:
1 | |
这句代码会导致后续写入的任何 PHP 代码都无法执行(因为脚本会直接退出)。但是,file_put_contents 的文件名参数支持 PHP 伪协议。通过使用 php://filter,我们可以在内容写入文件之前对其进行修改。
利用策略
我们可以使用 convert.base64-decode 过滤器。当这个过滤器生效时:
- 它会尝试对整个数据流(Header + 用户内容)进行 Base64 解码。
- Header 中的非 Base64 字符(如
< ? " ( ) ; >和空格)会被忽略。 - Header 中的有效 Base64 字符会被解码成乱码(二进制垃圾数据)。
- 如果我们传入的用户内容是经过 Base64 编码的 PHP 代码,它就会被还原成可执行的 PHP 代码。
Base64 对齐问题 (Padding Problem):
Base64 解码是以 4 个字符为一组进行的。我们需要确保 Header 中的有效 Base64 字符数量加上我们的填充字符,刚好能凑成 4 的倍数,这样我们的 Payload 才能被正确解码,不会和 Header 的字符混在一起。
Header 内容:<?php exit("Access Denied: Protected Cache"); ?>
其中的有效 Base64 字符为:phpexitAccessDeniedProtectedCache
共 33 个字符。
计算填充:33 % 4 = 1,因此我们需要补 3 个字符(4 - 1 = 3),这里我们使用 AAA 作为填充。
Payload 生成
生成序列化 Payload 的 PHP 代码如下:
1 | |
生成的 Payload:
1 | |
执行步骤
- 发送 Payload:
通过 POST 请求发送 Payload 到目标 URL:
1 | |
- 触发 Shell:
访问生成的shell.php文件,它会执行cat /flag:http://challenge.qsnctf.com:52885/shell.php
最终 Flag:qsnctf{502be51a0ed848a8b8e6141f1f542ff1}
- 准备工作:
- 我首先对目标网站
http://challenge.qsnctf.com:52960/进行了初步探测,使用curl获取了首页内容,确认这是一个用户搜索系统。 - 发现存在一个搜索框,提交的数据通过
POST请求发送到/search,参数名为query。
- 我首先对目标网站
- SQL 注入测试:
- 我尝试输入
admin,没有返回结果。 - 我尝试输入单引号
',依然没有返回结果。 - 我尝试输入
admin' or '1'='1,仍然没有返回结果。 - 我尝试了基于时间的盲注
admin' and sleep(5)#和admin' and sleep(5)--,响应时间极短,说明后端数据库可能不是 MySQL,或者不支持sleep函数,或者注入点不在此处。 - 接着,我尝试了 Union 注入
1' union select 1,2,3#,这次返回了错误信息unrecognized token: "#"。这表明后端数据库很可能是 SQLite(因为#在 SQLite 中不是注释符,而是 token 的一部分,而--是注释符)。 - 我立即修正 payload 为
1' union select 1,2,3--,成功在页面上回显了1,2,3,证明存在 Union 注入漏洞,且数据库是 SQLite。
- 我尝试输入
- 获取数据库结构:
- 利用
sqlite_master表查询数据库表名:1' union select 1,sql,3 from sqlite_master--。 - 查询结果显示存在两个表:
flags和users。 flags表的结构是CREATE TABLE flags (id INTEGER PRIMARY KEY AUTOINCREMENT, value TEXT NOT NULL)。
- 利用
- 获取 Flag:
- 构造 payload 查询
flags表中的数据:1' union select 1,value,3 from flags--。 - 成功获取到 Flag。
- 构造 payload 查询
最终结果
Flag 为:qsnctf{e43eb576d9fb420cb6b10637317426e1}
Reverse
CheckME

- c+.net所以dnspy
1 | |
- 利用e=3,n特别大
1 | |
ezpy
!– 这是一张图片,ocr 内容为: –>

1 | |
AES?
- dnspy
1 | |
AES

这边先DIE可以发现是C#语言,基于.NET的应用程序
所以我们应该使用dnSPY打开进行查看

很快就发现了密文和密钥的样子
text2比较标准,明显是key该有的样子,text3则是密文,当然下边AES里边也写了确实是

这个key被转化为了十六进制,不足的用0补齐

这边IV没赋值,明显就是全0的iv
模式也写了是CBC

密文还进行了base64加密
所以我们先base64解密,再进行AES解密即可,key、iv、mode都已知

得到flag
flag{4f7786120450144791741bd082bfdb58}
EasyRSA?


和上一题是一样的,都是C#语言基于.NET写的
所以我们还是继续使用dnSPY进行分析

已经写的很明确了
给了e、n和密文
1 | |

这边先输入,之后把输入的字符串变成字节、Reverse反转字节
然后是在末尾加0
最后变成大整数

标准的RSA
1 | |
这边利用了e很小,只有3的特点,可以迅速解密
1 | |
即可直接得到flag,只用密文即可
flag{8a5e3e5eac499995bd10c17f8bc9c954}
oi_feelings


64位,放到IDA分析一下

没有符号表,看着有点恶心

前边没什么,都是随便打印点东西,从这边开始可以看到Str是栈上的char数组,0x81字节拿来存我的输入
之后是一个if语句拿来限制我的Str必须是70的长度

再下边这个sub_1400011F0在解密Str2,长度为8
然后又在拿这个Str2去校验开头,所以就是要Str的前8位和Str2一样的意思,过去看看

可以看到这边就是个异或而已,和第二个参数69异或

Str2是这个,我们也去异或看看是什么

发现就是qsnctf{的前缀(8字节是因为还有个\0)

再后边是最后字符的比较

原来是+,和86异或,得到右半边是}
到这边也是把两边的格式搞清楚了,只差中间了

这一块就是剩下的中间部分了,我们看看来
很明确就是说i=7到i=68的这个部分,必须是dword_140029000[0]或dword_140029000[1]

就是对应的1和2,直接过去看不是,就是前边异或了一下

差不多是这样子异或的,0和1与0x9异或,2和0x123异或


接下来就是核心sub_1400010B0了

大概就是个走表环节吧,从(0,0)走到(31,31),然后每次只能向右或者向下,向右就x++,向下就y++
总62步,最后要求v7等于dword_140029000[2]即可
反向推即可得到
1 | |

得到flag
ez_re

1 | |
muffin_cake
- 基本判断
先看文件类型:
PE32+ executable
x86-64
Windows GUI 程序
不是 .NET
用了 MFC
从字符串里能直接看到一些关键信息:
qsnctf_chal_3
Cchal3App
Cchal3Dlg
Flag Here:
说明这就是一个 MFC 写的对话框逆向题。
- 对话框资源分析
主对话框资源里有这些控件:
按钮:Check
输入框:ID = 4
标签:Flag Here:
按钮:Exit
还看到一句提示:
这是出题者见过(大概)最容易的RE题!!!快开始吧…gogogo
所以主逻辑肯定就在 Check 按钮的点击事件 里。
- 找到 Check 按钮对应的处理函数
MFC 的消息映射表里可以直接搜到 WM_COMMAND + BN_CLICKED + ID=1。
对应项大致是:
message = 0x111(WM_COMMAND)
code = 0
id = 1
func = 0x140003F40
所以 Check 按钮处理函数 = 0x140003F40。
Exit 按钮则跳到另一个简单退出函数,不重要。
- 核心校验逻辑分析
函数 0x140003F40 干了几件事:
(1)先构造“错误/正确”提示字符串
栈上先放了两组 Unicode 字:
0x9518, 0x8bee, 0x20
0x6b62, 0x786d, 0x20
然后前 3 个字符都 +1,就变成了:
错误!
正确!
这是一个很简单的小混淆。
(2)取输入框内容长度
程序先取 ID=4 的输入框文本长度,判断:
if (len != 0x25)
return 错误
也就是输入必须是:
长度 0x25 = 37
(3)真正校验公式
程序把输入取出来后,逐字符比较。
栈上有一段目标字节表:
9f 9d 90 8d 9a 88 a5 97 db b0 d9 c1 b3 9b a8 88
df 90 c1 ad af 95 dd c1 8a ab 92 df 8d df de bb
db e1 e1 e1 a3
循环逻辑相当于:
for (i = 0; i < len; i++) {
if (obf[i] != (((input[i] ^ 0x66) + 0x88) & 0xff)) {
return 错误;
}
}
return 正确;
注意这是 GUI 程序,读出来是 UTF-16,但实际比较时只用了每个字符的低字节,所以本质上就是按普通 ASCII 在算。
- 逆推出正确输入
已知:
obf[i] = ((input[i] ^ 0x66) + 0x88) & 0xff
逆运算就是:
input[i] = ((obf[i] - 0x88) & 0xff) ^ 0x66
- 解密脚本
1 | |
输出:

- 最终答案
qsnctf{i5N7_MuFf1n_CAk3_dEl1c10U5???}
except_expert

这题是个 Windows x86 异常流混淆 的逆向题,核心不是普通 if/else 校验,而是把真正的变换逻辑塞进了:
- VEH(Vectored Exception Handler)
- C++
throw / catch(...) / rethrow - 外层异常过滤函数
看起来很绕,但真正有效的链子捋清之后,最后就是一个 48 字节的分组变换。
- 程序入口先看结论
主逻辑在 0x401b50 附近。
程序会:
- 输出
Input your flag: - 读入一个字符串
- 检查长度是否为
0x30,也就是 48 字节 - 长度对了就调用
0x4019f0 - 最后把全局缓冲区
0x42bf60和一组写死的 48 字节目标值做memcmp
所以这题本质上就是:
找一个 48 字节输入,使得经过一串异常流变换后,global_buf == target
- 初始化阶段干了什么
初始化函数在 0x4015d0。
它做了两件关键事:
(1)注册 VEH
注册了一个异常处理函数:
1 | |
也就是 sub_401270。
(2)初始化全局缓冲区
把 0x42bf60 这 48 字节全部填成 0x66:
1 | |
这个很关键,说明后面输入不是直接拿去算,而是先和 0x66 做异或。
- 最终比较的目标值
校验函数在 0x4019f0。
里面把目标 48 字节直接压到栈上,最后比较:
1 | |
也就是:
1 | |
- 输入先被怎样处理
0x401890 -> 0x4017e0
在 sub_4017e0 里,先做了一个逐字节异或:
1 | |
但前面 global_buf 已经被初始化成 48 个 0x66,所以这里等价于:
1 | |
于是最开始的明文状态是:
1 | |
- 这题真正的坑:异常流里的四段变换
第一段:0x401270,VEH 里的 32 轮变换
这是第一层 throw 触发时跑到的 VEH。
它对 48 字节按 6 组 × 8 字节 处理,也就是每次处理两个 DWORD。
本质上是一个魔改 TEA:
1 | |
我记成 EncA。
第二段:0x4016b9,0x401670 的 catch(…) 里跑的变换
0x401670 这个函数结构很像:
1 | |
这一段也是 32 轮,但公式和上面不是同一个,而是另一套的逆过程。
我记成 DecC。
第三段:0x401160,外层异常过滤函数
0x4017e0 这一层不是直接 catch,而是经过一个过滤函数。
它内部做了另一套 32 轮变换:
1 | |
我记成 EncC。
第四段:0x4018d4,最外层 catch(…) 里的变换
0x401890 外面包了一层 catch(...),命中后执行 0x4018d4。
这一段的公式和第一段 0x401270 本质一样,只是写法顺序不同,异或交换律下等价。
所以它也是 EncA。
- 真正有效的执行链
把整个异常流剥掉之后,真正参与最终 memcmp 的链子是:
1 | |
也就是:
1 | |
- 为什么我确定这条链是对的
因为我把它反推回去后,得到的是一个完全可读、格式正常、长度正好 48 的 flag:
1 | |
再把这个 flag 正向跑一遍上述 4 段,输出结果 严格等于 程序里写死的 48 字节 target。
也就是说,这条链不是猜的,是能正反双向闭合验证的。
另外,程序里注册的 Continue Handler / UnhandledExceptionFilter 看起来很唬人,但从最终可验证路径来看,它们更像是烟雾弹,不参与正常解题链。
- 还原脚本
下面这份脚本可以直接把 target 反推成 flag。
1 | |
输出:
1 | |
编程
上下火车
1 | |
两数之和
1 | |
罗马数字转整数
1 | |
回文数
1 | |
有效的括号
1 | |

最长公共前缀
1 | |