Ugra CTF Quals 2023 Writeup

この大会は2023/1/14 16:00(JST)~2023/1/16 4:00(JST)に開催されました。
今回もチームで参戦。結果は1260点で347チーム中60位でした。
自分で解けた問題をWriteupとして書いておきます。

warmup (MISC 10)

ルールのページにフラグが書いてあるような問題文。
https://2023.ugractf.ru/qualsにフラグのサンプルが書いてあった。

ugra_ex4mpl3

depth (PPC 150)

サブディレクトリを含め、全探索し、フラグを探す。

#!/usr/bin/env python3
import requests
import re

def get_urls(url):
    pattern = '<A HREF=(\w+/)>'
    r = requests.get(url)
    paths = re.findall(pattern, r.text)
    if len(paths) == 0 and 'ugra_' in r.text:
        print('[+] url:', url)
        print('[*] content:', r.text)
    else:
        for path in paths:
            get_urls(url + path)

url = 'https://depth.q.2023.ugractf.ru/08qqtqnzzi4k1den/'

get_urls(url)

実行結果は以下の通り。

[+] url: https://depth.q.2023.ugractf.ru/08qqtqnzzi4k1den/sapphire_vacuum/threatening_mermaid/wild_mare/hunting_cartridge/coral_fairy/covert_wrench/jade_battery/untouchable_hail/hunting_transistor/onyx_chef/red_captain/unexpected_keyboard/amber_zebra/yellow_dragon/sapphire_lobster/covert_trumpet/tarnished_drum/unexpected_display/unknown_pegasus/wild_python/inconceivable_trumpet/rowdy_saxophone/warring_gelding/inconceivable_griffin/rowdy_pegasus/draconic_lion/uncanny_rhythm/urban_trumpet/draconic_pilot/killer_general/killer_general/tarnished_deer/ivory_grizzly/obsidian_cartridge/red_memory/rowdy_screwdriver/wild_battery/searching_warning/bad_horn/killer_screwdriver/sapphire_guitar/pearl_trombone/mountain_griffin/spinning_pilot/pearl_wrench/wireless_captain/draconic_clarinet/obsidian_viper/decisive_compressor/deadly_cartridge/flying_inspector/opal_cyborg/agate_beat/mountain_ink/searching_fairy/obsidian_harp/untouchable_orca/jade_drill/spinning_octopus/jet_presence/unnecessary_gelding/waning_panther/rowdy_door/unnecessary_cello/warring_general/untouchable_banjo/ivory_sidewinder/threatening_orca/green_camera/ruby_python/urban_stag/destroyed_general/amber_piccolo/unexpected_nomad/tundra_mixer/green_lightning/unknown_case/dangerous_nomad/agate_zebra/covert_display/unexpected_mixer/killer_viper/onyx_cheetah/bad_flute/field_wildebeest/urban_gelding/onyx_mixer/tundra_memory/diamond_projector/emerald_compressor/scheming_unicorn/warring_foal/insane_robot/space_wildcat/spinning_cobra/ruby_sound/blue_hail/deadly_welder/bone_commander/desert_ink/insane_trombone/waning_major/desert_jackal/tarnished_general/inconceivable_falcon/opal_beat/scheming_grizzly/ruby_flute/scheming_cougar/unknown_mill/nacre_piranha/red_griffin/beryl_android/orbiting_motherboard/warring_tiger/emerald_violin/desert_gelding/tarnished_elk/falling_mermaid/inconceivable_octopus/pearl_sidewinder/obsidian_android/orange_deer/insane_violin/jet_banjo/covert_admiral/hunting_mare/threatening_boa/hidden_elk/blue_cougar/unexpected_chain/unnecessary_mainframe/hunting_sander/nacre_rain/diamond_troll/deadly_general/ruby_drill/wireless_boa/flying_piano/rowdy_foal/agate_zebra/covert_cornet/tundra_viper/unexpected_transistor/wild_display/bone_warning/scheming_display/warring_projector/tarnished_saxophone/obsidian_cello/ruby_orca/unknown_mare/urban_deer/stalking_tape/warring_sound/violet_cyborg/glass_major/orange_admiral/destroyed_memory/explosive_colt/jet_sun/orbiting_camera/scheming_cartridge/deadly_sidewinder/decisive_wrench/explosive_thunder/stalking_commander/ruby_piranha/agate_mixer/insane_piccolo/killer_piccolo/waning_mainframe/diamond_fairy/insane_stag/decisive_piano/opal_mill/draconic_display/scheming_yearling/spinning_python/inconceivable_stag/tarnished_stag/draconic_fairy/sapphire_nomad/sapphire_chef/destroyed_rhythm/unexpected_tape/desert_storm/insane_griffin/sapphire_violin/jet_device/unexpected_screwdriver/opal_horn/inconceivable_drum/ivory_yearling/unknown_guitar/destroyed_dragon/spinning_snow/field_sloth/obsidian_motherboard/chasing_koala/flying_troll/orange_weapon/violet_cleric/amber_troll/unnecessary_wrench/hidden_yearling/inconceivable_mermaid/hidden_fairy/green_pilot/obsidian_pegasus/pearl_troll/tarnished_flute/tundra_cougar/glass_device/urban_mill/tarnished_compressor/opal_door/amber_flute/ruby_packet/desert_player/ivory_lathe/draconic_mixer/onyx_player/unexpected_door/decisive_cottonmouth/urban_motherboard/obsidian_player/falling_projector/killer_snow/stalking_door/tundra_pony/wild_troll/nacre_zebra/unexpected_lightning/threatening_sander/wild_clarinet/hunting_cleric/opal_cyborg/threatening_leopard/jet_commander/sapphire_compressor/jade_drought/decisive_rhythm/nacre_falcon/hunting_piano/ivory_clarinet/agate_fairy/ivory_sloth/unexpected_pilot/waning_dragon/red_rhythm/scheming_chef/onyx_warning/destroyed_transistor/obsidian_welder/wireless_unicorn/violet_sound/stalking_koala/jade_welder/ruby_lobster/opal_camera/draconic_inspector/insane_keyboard/desert_filly/sapphire_rhythm/pearl_hammer/opal_camera/spinning_tape/chasing_rhythm/stalking_song/ruby_welder/searching_chef/blue_lion/tarnished_rain/unexpected_gazelle/opal_leopard/bone_song/draconic_display/killer_panther/sapphire_mixer/destroyed_trumpet/unknown_mare/sapphire_horse/diamond_ink/diamond_lion/threatening_drill/decisive_zebra/hidden_unicorn/hidden_leopard/spinning_guitar/amber_horn/hunting_battery/emerald_weapon/unknown_weapon/revealing_captain/opal_storm/wild_saxophone/killer_major/decisive_leopard/red_unicorn/killer_pegasus/killer_trombone/obsidian_storm/coral_chef/nacre_leopard/hunting_drum/obsidian_lobster/draconic_griffin/pearl_lion/jet_deer/warring_sound/threatening_router/spinning_sloth/unexpected_cobra/bone_general/glass_router/threatening_horn/spinning_troll/space_elk/bone_drought/tarnished_mask/threatening_inspector/decisive_sun/stalking_cobra/wild_lightning/explosive_orca/agate_door/tarnished_cup/inconceivable_cougar/wireless_welder/agate_commander/inconceivable_lion/stalking_filly/glass_mainframe/covert_sidewinder/waning_song/killer_drill/obsidian_beat/sapphire_commander/violet_griffin/violet_trumpet/glass_cartridge/urban_welder/yellow_stag/beryl_display/falling_stallion/stalking_wildebeest/bad_captain/beryl_drought/obsidian_yearling/urban_yearling/sapphire_trumpet/orange_drought/rowdy_foal/inconceivable_cougar/hidden_beat/agate_fairy/hidden_sidewinder/hidden_packet/urban_drum/violet_griffin/red_storm/decisive_leopard/unexpected_orca/decisive_stallion/unknown_mermaid/stalking_organ/inconceivable_cottonmouth/ivory_python/flying_packet/covert_disk/red_python/desert_mill/beryl_guitar/emerald_cornet/chasing_drizzle/bad_piccolo/jet_cougar/hunting_cobra/onyx_device/destroyed_stag/hidden_foal/hidden_commander/dangerous_piano/yellow_horn/bone_troll/bad_falcon/wireless_commander/unknown_colt/dangerous_hail/jade_viper/obsidian_horse/warring_drought/inconceivable_disk/draconic_drum/threatening_tape/space_tiger/waning_cornet/orbiting_cottonmouth/dangerous_griffin/inconceivable_troll/wild_mermaid/wireless_unicorn/searching_clarinet/rowdy_horse/ivory_cello/unnecessary_storm/rowdy_beat/insane_hail/revealing_transistor/orange_tiger/stalking_beat/draconic_camera/coral_battery/unnecessary_packet/diamond_moose/violet_weapon/unnecessary_commander/decisive_android/chasing_general/wireless_mainframe/ivory_player/beryl_lobster/covert_lobster/draconic_rhythm/covert_chef/threatening_drum/bone_router/hunting_griffin/ruby_major/ruby_grizzly/jet_tape/agate_leopard/opal_violin/ivory_compressor/falling_transistor/insane_cyborg/unnecessary_lobster/agate_dragon/hidden_harp/pearl_viper/obsidian_violin/wild_jackal/jade_deer/threatening_cartridge/searching_device/jade_foal/space_nomad/destroyed_panther/flying_cobra/beryl_pony/unnecessary_banjo/covert_cyborg/waning_door/ruby_panther/pearl_saxophone/wireless_presence/tundra_wildebeest/opal_stallion/revealing_boa/decisive_cello/untouchable_grizzly/hunting_cello/urban_harp/unexpected_saxophone/chasing_moose/dangerous_pegasus/bad_foal/searching_welder/green_song/inconceivable_door/explosive_cartridge/wild_sander/sapphire_pony/threatening_router/emerald_filly/threatening_hail/onyx_guitar/inconceivable_pegasus/wild_python/insane_commander/warring_camera/jet_major/mountain_cyborg/searching_tuba/mountain_cottonmouth/bone_disk/covert_projector/revealing_cornet/waning_motherboard/agate_stallion/scheming_python/orange_general/tarnished_drill/searching_stallion/draconic_weapon/unnecessary_cartridge/searching_yeti/bone_keyboard/wild_guitar/
[*] content: ugra_i_have_always_imagined_that_paradise_will_be_a_kind_of_library_bjhlbqomhtc8
ugra_i_have_always_imagined_that_paradise_will_be_a_kind_of_library_bjhlbqomhtc8

safestr (PWN 100)

バッファとして、512バイトが確保され、後半256バイトのメモリにフラグが設定される。
任意の文字を256バイト入力すれば、フラグが表示されるはず。

$ nc safestr.q.2023.ugractf.ru 11667
Enter token: watmo5ffmja9ssy8
Enter input size: 256
Enter string: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
You entered: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Bitcoin key is ugra_safe_0r_no7_5afe_u9lzsdd51x6a
ugra_safe_0r_no7_5afe_u9lzsdd51x6a

elementary (REVERSE 100)

Pythonスクリプトを読むと、フラグは以下の条件を満たすことがわかる。

・flagの長さが29バイト
・flag[17] == 'j'
・flag[:5] == 'ugra_'
・flag[9:3:-2] == 'nta'
・flag[-2:-15:-3].encode().hex() == '65646f6e67'
・int.from_bytes(flag[6:18:2].encode(), "little") == 104927802781555
・sum(ord(x) * 1000 ** i for i, x in enumerate(flag[19:-4])) == 100118104111105101
・base64.b64encode(flag[-4:].encode()) == b'eGFlNA=='
・hashlib.sha256(flag.encode()).hexdigest() == 'b6ff072c4096218622ae1f744f57c38e62edea4d9e2bcad751681c37e9fd62b1'

flag[9:3:-2] == 'nta' から以下のことが言える。

・flag[9] = 'n'
・flag[7] = 't'
・flag[5] = 'a'

flagの長さが29バイト、flag[-2:-15:-3].encode().hex() == '65646f6e67' から以下のことが言える。

・flag[-2] = flag[27] = chr(0x65) = 'e'
・flag[-5] = flag[24] = chr(0x64) = 'd'
・flag[-8] = flag[21] = chr(0x6f) = 'o'
・flag[-11] = flag[18] = chr(0x6e) = 'n'
・flag[-14] = flag[15] = chr(0x67) = 'g'

int.from_bytes(flag[6:18:2].encode(), "little") == 104927802781555 から言えることを考える。

>>> (104927802781555).to_bytes(6, "little")
b'soihn_'

・flag[6] = 's'
・flag[8] = 'o'
・flag[10] = 'i'
・flag[12] = 'h'
・flag[14] = 'n'
・flag[16] = '_'

sum(ord(x) * 1000 ** i for i, x in enumerate(flag[19:-4])) == 100118104111105101 から言えることを考える。

flag[19] + flag[20] * 1000 + flag[21] * (1000)**2 + ... + flag[24] * (1000)**5 == 100118104111105101
→この条件を満たすものを探す。
→flag[19:-4] = 'eiohvd'

base64.b64encode(flag[-4:].encode()) == b'eGFlNA==' から言えることを考える。

>>> base64.b64decode(b'eGFlNA==')
b'xae4'

flag[-4:] = 'xae4'

ここまでわかっている範囲でフラグ文字を並べていく。

          1111111111222222222
01234567890123456789012345678
ugra_astoni h ng_jneiohvdxae4

hashlib.sha256(flag.encode()).hexdigest() == 'b6ff072c4096218622ae1f744f57c38e62edea4d9e2bcad751681c37e9fd62b1' の条件を満たすよう、インデックス11と13をブルートフォースして探す。

#!/usr/bin/env python3
import hashlib

target = 100118104111105101

flag19_24 = ''
remain = target
for i in range(5, -1, -1):
    c = remain // (1000)**i
    flag19_24 += chr(c)
    remain = remain % (1000)**i

flag19_24 = flag19_24[::-1]

flag = 'ugra_astoni h ng_jneiohvdxae4'
assert flag[19:25] == flag19_24
flag_format = flag.replace(' ', '%s')

target = 'b6ff072c4096218622ae1f744f57c38e62edea4d9e2bcad751681c37e9fd62b1'

found = False
for c1 in range(33, 127):
    for c2 in range(33, 127):
       flag = flag_format % (chr(c1), chr(c2))
       if hashlib.sha256(flag.encode()).hexdigest() == target:
           found = True
           print(flag)
           break
    if found:
        break
ugra_astonishing_jneiohvdxae4

pebcak (REVERSE 150)

スクリプトの処理の概要は以下の通り。

・code = "ANEcwu4fYegObF1i2VXyvJHxKd0qBkMl36ILRaUjPG8rToSZzpCDt7n9mWsh5Q"
・flag: 入力
・s = ""
・flagの長さだけ以下繰り返し(i)
 ・numbers(i) = flagのi番目の文字のASCIIコード
 ・iが0の場合
  ・Encode(numbers(0))をsに結合
 ・iが1, 2の場合
  ・"_" + Encode(numbers(i))をsに結合
 ・iが3以上の場合
  ・numbers(i) = (numbers(i) + numbers(i-1) + numbers(i-2)) mod 179179
  ・"_" + Encode(numbers(i))をsに結合
・sを出力

Encodeの前の数値は暗号化文字列をcodeのテーブルを元に62進数のようにしてデコードすればわかる。あとはその数値からnumbers(i)を順に求めればよい。

#!/usr/bin/env python3
code = 'ANEcwu4fYegObF1i2VXyvJHxKd0qBkMl36ILRaUjPG8rToSZzpCDt7n9mWsh5Q'
b = len(code)

def get_number(s):
    n = 0
    for c in s[::-1]:
        n *= b
        n += code.index(c)
    return n

enc = '9N_GN_tN_wu_qY_xi_Md_08_zfN_ctN_HQE_O7w_vnf_xZb_1Gv_VB6_y6f_lNG_H5N_0Nr_xNo_dBG_09j_rZI_QwB_122_CHT_tE1_qDO_emd_0Za_xuV_I2Y_Bxd_WG6_Okb_sgS_7cb_GPO_cSx_v0L_ERb_b0N_mN1_5Bi_z3k_jAo_Ehq_BH0_HTf_k4I_SDG'

enc = enc.split('_')

flag = ''
numbers = []
for i in range(len(enc)):
    number = get_number(enc[i])
    assert number < 179179
    numbers.append(number)
    if i < 3:
        flag += chr(number)
    else:
        number = (number - numbers[i-1] - numbers[i-2]) % 179179
        flag += chr(number)

print(flag)
ugra_thats_not_how_access_control_works_dfqh6v7d05g9

trisection (WEB 100)

HTMLソースを見たら、コメントにこう書いてある。

<!--ugra_triangles_are_cool_-->

https://trisection.q.2023.ugractf.ru/robots.txtにアクセスしたら、こう書いてある。

User-Agent: *
Disallow: /secret-page/

https://trisection.q.2023.ugractf.ru/secret-page/にアクセスしたら、こう書いてある。

but_triflags_are_

HTTPヘッダを見てみる。

$ curl -v https://trisection.q.2023.ugractf.ru/63561fade0188a1c/ -X HEAD
Warning: Setting custom HTTP method to HEAD with -X/--request may not work the 
Warning: way you want. Consider using -I/--head instead.
*   Trying 95.217.155.244:443...
* Connected to trisection.q.2023.ugractf.ru (95.217.155.244) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
*  CAfile: /etc/ssl/certs/ca-certificates.crt
*  CApath: /etc/ssl/certs
* TLSv1.0 (OUT), TLS header, Certificate Status (22):
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS header, Certificate Status (22):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS header, Finished (20):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.2 (OUT), TLS header, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN, server accepted to use h2
* Server certificate:
*  subject: CN=q.2023.ugractf.ru
*  start date: Jan  7 05:03:47 2023 GMT
*  expire date: Apr  7 05:03:46 2023 GMT
*  subjectAltName: host "trisection.q.2023.ugractf.ru" matched cert's "*.q.2023.ugractf.ru"
*  issuer: C=US; O=Let's Encrypt; CN=R3
*  SSL certificate verify ok.
* Using HTTP2, server supports multiplexing
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* Using Stream ID: 1 (easy handle 0x558dc33b1e80)
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
> HEAD /63561fade0188a1c/ HTTP/2
> Host: trisection.q.2023.ugractf.ru
> user-agent: curl/7.81.0
> accept: */*
> 
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* old SSL session ID is stale, removing
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* Connection state changed (MAX_CONCURRENT_STREAMS == 128)!
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
< HTTP/2 200 
< server: nginx
< date: Sat, 14 Jan 2023 09:54:13 GMT
< content-type: text/html; charset=utf-8
< content-length: 5684
< x-flag-part3: way_cooler_a50d7deb2558
< 
* transfer closed with 5684 bytes remaining to read
* stopped the pause stream!
* Connection #0 to host trisection.q.2023.ugractf.ru left intact
curl: (18) transfer closed with 5684 bytes remaining to read

HTTPレスポンスヘッダのx-flag-part3にフラグの断片が設定されていた。
見つけたフラグの断片を結合すると、フラグになる。

ugra_triangles_are_cool_but_triflags_are_way_cooler_a50d7deb2558

capture (FORENSICS 100)

httpでフィルタリングする。No.32パケットで GET /hacker.jpg のレスポンスがあるので、エクスポートする。エクスポートした画像ファイルの画像にフラグが書いてあった。

ugra_traffic_extractor_53f2a01112bb

takefive (STEGO 50)

Mp3tagでカバー画像を抽出する。画像に書いてある文字を線が引いてある順番に読むとフラグになる。

ugra_we_support_local_artists_c8b5b1

bookkeeping (STEGANO 100)

Excelの各セルを見ると、数値が入っているセルと、数式が入っているセルがある。条件付き書式で以下を設定すると、文字が浮き上がってくる。

=ISFORMULA(A1)

ugra_you_asked_for_lattices_5zgp8ddrnjxe

whirlpool (CRYPTO 50)

ROT13と推測し、CyberChefで復号する。

                :

Nam rutrum ex non sapien facilisis, id cursus ante sollicitudin. Integer fringilla ugra_double_security_for_only_50_more_bucks_bu45q00imu6n consequat pellentesque. Aliquam elementum, neque in euismod luctus, nisi urna dictum ante, non imperdiet dui lorem accumsan leo. Curabitur metus arcu, vestibulum eget rhoncus a, luctus id velit. Nulla egestas libero nisi, vitae dictum risus ultricies vitae. Sed sit amet iaculis lorem. Ut mi purus, porttitor at euismod sed, porta ac massa. Nulla aliquet vel felis ac mattis. Duis erat urna, consectetur id sollicitudin at, vehicula et sapien. Morbi id mauris finibus, ullamcorper sem sed, ultrices libero.

                :

文中にフラグが含まれていた。

ugra_double_security_for_only_50_more_bucks_bu45q00imu6n

espionage (CRYPTO 150)

以下の順にデコード、解凍していく。

・base64デコード
・Zstandard compressed dataの解凍
・base32デコード
・bzip2アーカイブの解凍
・hexデコード
・xzアーカイブの解凍
・gzipアーカイブの解凍
#!/usr/bin/env python3
from base64 import *
from zstd import *
import bz2
import lzma
import uu
import gzip

with open('correspondence.txt', 'r') as f:
    enc = f.read()

enc = b64decode(enc)
enc = ZSTD_uncompress(enc)
enc = b32decode(enc)
enc = bz2.decompress(enc)
enc = bytes.fromhex(enc.decode())
enc = lzma.decompress(enc)

with open('uu.enc', 'w') as f:
    f.write(enc.decode())

uu.decode('uu.enc', 'uu.gz')

with gzip.open('uu.gz', 'rt') as f:
    enc = f.read()
print(enc)

解凍した結果以下のメッセージが表示された。

Robert A. logged on

Josh logged on
Josh: one two one two do you read?

Robert A.: Yes sir!

Josh: finally, the damn thing works
Josh: It's been what, two months?

Robert A.: That's about right.

Josh: So how secure is this device anyway?

Robert A.: Certifications are still in progress, sir, but our cryptologists say even NSA wouldn't be able to crack it.
Robert A.: Von Stierlitz confirmed that too.

Josh: ah, Stierlitz.. I'm glad our security is in good hands
Josh: alright, transmitting the key now
Josh: ugra_you_guys_are_getting_encryption_1querub3nqst

Robert A.: Roger that, I will get back to you in five minutes, sir.
Robert A. left

Josh: von Stierlitz... I totally saw that name somewhere else
Josh: reminds me of Russia for some reason
Josh: nah, looks like a german name
Josh: maybe i just need to get some sleep

Josh left

この中にフラグが含まれていた。

ugra_you_guys_are_getting_encryption_1querub3nqst

cryptoneliner (CRYPTO 200)

bash_historyの中に以下の履歴がある。

openssl enc -aes-256-cbc -pbkdf2 -in secret.data -out secret.enc -k $my_key
echo -n $my_key | base64 | tr '[A-Za-z0-9]' '[N-ZA-Mn-za-m3-90-2]' | rev | xxd -p | xargs -I {} python3 -c "import sys;print(f'{int(sys.argv[1],16)^int((sys.argv[2]*(len(sys.argv[1])//len(sys.argv[2])+1))[:len(sys.argv[1])],16):x}')" {} deadbeef | awk '{print substr($0,length/2+1) substr($0,1,length/2)}'

$my_keyがわかれば、復号できる。問題にある"bf78bb5a4fec4a3d4e9d5f9a2eae18baee"は上記の2つ目のコマンドの結果と思われる。逆にたどっていき、my_keyを求める。

#!/usr/bin/env python3
from string import *
from base64 import *

def caesar_decrypt(s):
    d = ''
    for c in s:
        if c in ascii_uppercase:
            index = (ascii_uppercase.index(c) + 13) % 26
            d += ascii_uppercase[index]
        elif c in ascii_lowercase:
            index = (ascii_lowercase.index(c) + 13) % 26
            d += ascii_lowercase[index]
        else:
            index = (digits.index(c) + 7) % 10
            d += digits[index]
    return d

enc = 'bf78bb5a4fec4a3d4e9d5f9a2eae18baee'

l = len(enc)
enc = enc[l//2:] + enc[:l//2]

key = 'deadbeef'
key = (key * (l // len(key) + 1))[:l]
enc = hex(int(enc, 16) ^ int(key, 16))[2:]
enc = bytes.fromhex(enc).rstrip().decode()
enc = enc[::-1]
enc = caesar_decrypt(enc)
key = b64decode(enc).decode()
print(key)

結果、my_keyは"baf3c67f5e98"であることがわかるので、これを鍵として復号する。

$ openssl enc -aes-256-cbc -pbkdf2 -d -in secret.enc -out secret.data -k baf3c67f5e98
$ cat secret.data
We're no strangers to love
You know the rules and so do I
A full commitment's what I'm thinking of
You wouldn't get this from any other guy

I just wanna tell you how I'm feeling
Gotta make you understand

                :

Never gonna give you up
Never gonna let you down
Never gonna run around and desert you
Never gonna make you cry
Never gonna say goodbye
Never gonna tell a lie and hurt you 
ugra_oneliners_rule_58f5596f3b02

この文末にフラグがあった。

ugra_oneliners_rule_58f5596f3b02