[2021祥云杯]secrets_of_admin
老样子,好多好多代码时,我们进行代码审计
我们先看主路由
1 | router.get('/', (_, res) => res.render('index', { message: `Only admin's function is implemented. 😖 `})) |
它存在get和post两种请求方式
get请求应该就是正常访问该页面
post请求获取了usename和passwd后,判断完数据类型后就把他们传递给DB.login()
1 | export default class DB { |
DB.Login()应该是吧用来判断用户输入的用户名和密码和数据库保存的是否一致来判断登录
回到主路由的代码,,如果登录成功,他会把用户的一些信息放在token里
1 | res.cookie('token', { |
接下来我们来到/admin路由的get方法
1 | router.get('/admin', checkAuth, async (req, res) => { |
我们一请求这个路由就会调用checkAuth方法,我们查看checkAuth方法
1 | const checkAuth = (req: Request, res:Response, next:NextFunction) => { |
这个方法用来禁止superuser使用,我们从注释中也能看到
我们回到/admin路由
它会将username作为参数调用DB.listFile方法
我们来看DB.listfile方法
1 | static listFile(username: string): Promise<any> { |
我们看到这里有一个sql语句,它会获取传入用户的所有所拥有的文件名和对应checksum的值
/admin路由获取了登录用户在数据库中存储的信息,然后写入自己的token中
好了,admin的get方法看完了
我们来看admin路由的post方法
1 | router.post('/admin', checkAuth, (req, res, next) => { |
同样,代码先禁止了superuser用户访问,然后获取content信息,并对content的信息进行了waf过滤
1 | let { content } = req.body; |
接下来这里有个html的模板,如果content能经过前面的过滤,就会把content放进html模板中
然后uuid随机生成一个标识符作为pdf的名字,然后根据上面的html模版生成一个pdf文件,并吧这个文件发送到./files目录下
接下来对pdf的文件名调用getCheckSum方法,总的概括就是根据文件名生成一段随机数
1 | const getCheckSum = (filename: string): Promise<string> => { |
然后将文件名和对应的随机数checksum一起放到数据库里superuser所属下
接下来我们来看/api/files:id路由,这个路由还是先禁止superuser访问
1 | router.get('/api/files/:id', async (req, res) => { |
接着根据用户名与我们在url中输入的id(也就是checksum)去数据库中匹配对应的文件名,并在./files路径下访问该文件名
到这路由就分析完了,我们来看数据库
1 | import * as sqlite3 from 'sqlite3'; |
这些是数据库的初始化,我们可以从中得到admin的密码,并且可以知道flag这个文件名是superuser用户的
所以我们现在知道
- 我们目前知道的flag属与superuser
- 唯一的读文件函数在/api/files/:id路由,他不允许superuser访问,而且只能读取./files目录下的文件,读文件的条件是只要知道用户名和文件对应的checksum就行
- admin知道它拥有的文件,也可以添加任意文件名和对应checksum到他那里,但是得本地访问
- admin路由下生成的pdf文件全归superuser所有,并且会在./files生成相应的文件
- readfile读文件时,它是将文件名拼接到路径上的,而且根据源代码得目录我们知道flag就在files目录下,任何人都可以读取该文件
根据以上信息,我们知道数据库里存的只是一个文件名,而且flag文件就在files目录下,只要文件名和既定路径拼接合利,那么谁都可以读取到flag文件
flag这个文件名又是属与superuser,用superuser去读那是不可能的了,那我们可以构造给admin一个文件名,让他文件名不是flag,和路径拼接到一起时又可以读到flag文件,所以我们可以考虑目录穿越…/files/flag或者是直接用./flag
既然如此那我们得把文件名和对应的checksum写到数据库里去,这就得用到ssrf了
看了大佬的wp知道,我们在 content 中传入一个正常的 HTML 标签理论上也是可以直接解析的。那我们可以在 content 中传入一个可以自动触发 src 属性的标签,用这个 src 属性触发 SSRF 并访问 /api/files 路由为 admin 用户创建 flag 文件记录。
看了另一个大佬WP才知道html-pdf库也存在一个任意文件读取
但不管咋样ssrf利用点就是content这里
贴上两个大佬的payload
1 | content[]=<img+src%3D"http%3A//127.0.0.1:8888/api/files?username%3Dadmin%26filename%3D./flag%26checksum%3D123"> |
至于为什么要用数组形式
因为有个过滤
1 | if ( content == '' || content.includes('<') || content.includes('>') || content.includes('/') || content.includes('script') || content.includes('on')){ |
node.js的弱类型和php的弱类型有所不同。js中数组和字符串拼接的话,比如["hello"]+"world"
,得到的是helloworld
,而php里却是Arrayworld
我们用第一个吧
我们先登录admin账号
在admin处post请求payload后,在api/files/id访问对应的checksum值即可