于新题中悟道

前言
最近没什么小比赛(大比赛根本看不了),所以基本上就是在看buu和找一些国外的比赛
虽然能做出来的题目太少了,但是接触到的知识还是有的,也是借题学习吧。好好总结,好好复现✊
感觉这题出得挺新的,也学到了很多点,故心血来潮写写题目总结(平时没有专门写wp的习惯,很大一部分原因是想要保证质量非常耗费精力,所以基本上是随缘😂)
Prismatic Blogs
Here are API endpoints for a blog website.
Here is the API here (已失效)
Author: White
首先这题非常新,确实不熟悉,云里雾里的不知道想考啥(我太菜),等比赛结束看了其它师傅的wp才慢慢懂了些。
但这篇wp并没有详细地解释这道题涉及到的知识及原理,故我在此承前人之慧,做些拙劣的学习和补充。也想借此机会,说说自己对CTF/网络安全学习的一点理解。
道术一统
理工科,无非就是 “术”,也就是 技术,人们往往执着于追求技术的精进
却常常忽视了与之对应的 “道”,或者说 思维
由此引发开来,中国古代的”道“和”术“,不正如西方所言之 “哲学” 与 “科学” ?
早在古希腊时代,亚里士多德就说过,知识是一个统一的整体,科学知识是追求智慧(哲学)的一种方式。科学和哲学本身就密不可分,科学研究在一定程度上是哲学理念在物理世界的实践,科学本身也蕴含着哲学价值。
而如今”道“这一面却常被我们所抛弃,确实不该……
一个人要想改变,首先必须从思维上改变,思维指导行动
也是这道题目让我对知识学习有了一个新的认识
- 首先这种**“新”**题目(如果你搜不到相关的vul,或者搜索能力不够),你需要做的就是去查阅对应application的 官方文档
- 一定要审计清楚代码,即使是陌生的语言,也要借此机会熟悉其语法和逻辑,才有可能找到vul的点
- 陌生的题目是大有脾益的,很好地模拟在未来真实场景下作为安全师你所需要面对的未知,以及终身学习的能力,而不是靠一味的刷题和旧的vul
题目源码
好了前面废话了这么多,进入正题吧
Folding 源码描述: 点击查看
首先在package.json
我们可以得知
1 | { |
这是node.js搭建的一个blog-api接口,其中它使用了express和prisma,而prisma正是我们所陌生的
其次是seed.js
1 | import { PrismaClient } from "@prisma/client"; |
乍一看,seed.js
是用来生成密码的,而且没办法通过计算得到密码
然后是schema.prisma
,描述数据库结构
1 | datasource db { |
最后是index.js
1 | import express from "express"; |
解题要点
我们审计代码
在seed.js
里面,可知我们的flag是在一篇未发布的文章里面
1 | const FLAG = process.env.FLAG || "uoftctf{FAKEFLAGFAKEFLAG}" |
也就是说,我们只要能看到这篇未发布的文章就能拿到flag,那怎么才能看到未发布的文章呢?
在index.js
里面,我们有一个查询文章的接口,但是我们被query.published = true;
限制了
1 | app.get( |
而在登录的逻辑里面
1 | app.post( |
那么题目的意图就是让我们去登录了,只有这样才能看到flag,而seed.js
里面生成文章的时候flag对应的文章作者是随机的,这也就说明我们4个用户都必须登录进去,去检查有没有flag
而这也就隐含着密码其实我们是可以通过某种方式获取的(不然这题就真不知道怎么做了),但穷举爆破是非常不现实的,思路就在这里卡住了
在赛后参考了WP后,我才知道,let query = req.query;
和let posts = await prisma.post.findMany({where: query});
就是漏洞点,系统对用户输入的查询条件不做任何约束,直接查,这也是sql注入的一种!
知识其实都知道,换了皮就忘了本质,看来做题还不够灵活,对知识理解还不够深刻
那么接下来就是想办法利用这个sink,在询问了ChatGPT之后,它提示我们可以尝试提交请求查询
1 | GET /api/posts?authorId=1 |
然而返回的是{"success":false,"error":{"name":"PrismaClientValidationError","clientVersion":"6.1.0"}}
复现的时候才搞懂,真的一定要去查 官方文档 !
在node.js中?authorId=1
,这种请求的 req.query
解析为:
JavaScript的URL传参解析
?authorId=1
属于简单键值对,解析为
{
authorId: ‘1’ // 这里实际上是解析成了字符串类型
}
而 ?author[name]=user
(后文会提到)则为嵌套结构,查询字符串可以解析为如下
const query = {
author: {
name: ‘user’
}
};
而我们的schema.prisma
中规定了模型元素的数据类型
1 | model Post { |
所以报错了,接下来在了解了查询方式后我们可以构造出正确的查询请求
由于prisma.schema
中密码是直接存储在user下面的
1 | model User { |
这里给出官方的exp(comment generated by ChatGPT)
1 | import requests # 导入 requests 模块,用于发送 HTTP 请求 |
可以看见{BASE_URL}/api/posts?author[name]={user}&author[password][gte]={password}
这条查询请求会被解析为这样的prisma查询语句
1 | prisma.post.findMany({ |
原理解析
官方的脚本的主要利用方式是通过字符串的比较 gte
(greater than equal?)总之是大于等于
比如当前White的用户密码为3pCtWJfabwPlo6qNgGS1P4
注意到我们的枚举字符集ALPH = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
是需要按照ASCII码从小到大的顺序来排列的
官方的脚本是通过逐个枚举来实现的,直到找到那个**“转折点”**的字符的前一个,就是前缀字符
这里我也想说说二分查找,找到相对最小的且满足条件的字符,也就是密码的前缀字符
二分左端:需要posts有内容
1 | GET http://localhost:1234/api/posts?author[name]=White&author[password][gte]=0 |
二分右端:需要posts无内容
1 | GET http://localhost:1234/api/posts?author[name]=White&author[password][gte]=z |
接下来就判断mid=(left+right)/2
的索引对应的字符,查找结果posts是否有内容,来更改二分查找的左右端,重复这样的步骤直到左右端重合,那么此时就找到了密码的前缀字符(如果笔者没能讲清楚,可以搜索 “二分查找算法” 自行加深理解)
在上面的两个例子中,我们接下来就判断处于0
和z
中间位置的那个字符,假设是U
,更改请求部分为author[password][gte]=U
,看是否posts有内容
如果有,那么就继续判断
U
和z
中间位置的字符如果没有,就判断
0
和U
中间位置的字符
在这种情况下显然是0
和U
中间位置的字符,因为密码3pCtWJfabwPlo6qNgGS1P4
的前缀字符3
处于区间0
到U
之间
所以我们可以逐步按照前缀通过 sql盲注 的方式,爆破出整个密码,最后逐个用户登录即可
这里附上自己参考wp后写的用二分查找的exp
1 | import requests # 导入 requests 模块,用于发送 HTTP 请求 |
总结
总而言之这题还是学到很多的,尤其是感觉自己的思维得到了提升,希望在以后的学习中能循序渐进,逐步提升!
- 标题: 于新题中悟道
- 作者: OneZ3r0
- 创建于 : 2025-01-14 17:36:44
- 更新于 : 2025-07-29 18:03:58
- 链接: https://blog.onez3r0.top/2025/01/14/prismatic-blog-wp/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。