$ nmap -p- --min-rate 5000 10.129.29.195
Starting Nmap 7.93 ( https://nmap.org ) at 2023-03-09 00:16 EST
Nmap scan report for 10.129.29.195
Host is up (0.022s latency).
Not shown: 65531 closed tcp ports (conn-refused)
PORT STATE SERVICE
21/tcp open ftp
22/tcp open ssh
5000/tcp open upnp
8000/tcp open http-alt
FTP Anonymous Accss
I checked for anonymous access to the FTP server, and it works. We can download a project.txt file from it. These are the contents:
$ cat project.txt
Flask -> Consumer
Django -> Authorization Server
Might need this information later. Also, we can see that this is the user qtc server.
Port 5000
This webpage just shows a login page:
I registered a user and logged in to see the dashboard.
There are 3 main functions, a Password Change, Documents and the Contact one. The Password Change is not interesting, Documents are only available for the administrator user. That just leaves the Contact function.
This looks like an XSS platform to somehow steal the administrator cookie. When trying to submit a basic XSS payload, this is what I got:
Further testing of this endpoint revealed that the administrator clicks links that are sent in that Contact Form. Could be useful later. I ran a gobuster scan on the machine to see what other endpoints are hidden. I found a weird /oauth endpoint that could be of use: (this took a lot of wordlists to do)
This gives us instructions on how to connet to the OAuth server running on this machine:
When trying to access it, it attempts to access authorization.oouch.htb:8000. So the OAuth server is running on port 8000.
Using the connect options shows us this:
When we click authorize, we just are logged in as the same user it seems.
Authorisation Server
On Port 8000, all we see is this:
When are visiting this port from port 5000 via OAuth, this is the request that gets sent:
GET /oauth/authorize/?client_id=UDBtC8HhZI18nJ53kJVJpXp4IIffRhKEXZ0fSd82&response_type=code&redirect_uri=http://consumer.oouch.htb:5000/oauth/connect/token&scope=read HTTP/1.1Host:authorization.oouch.htb:8000User-Agent:Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8Accept-Language:en-US,en;q=0.5Accept-Encoding:gzip, deflateReferer:http://oouch.htb:5000/Connection:closeCookie:csrftoken=CHNQoemrMXB0fSL1ww9wI1NPq7GSYoCHqWWxW9FW7ZQpfflFeHATfHu00krDHJ8iUpgrade-Insecure-Requests:1
And this is the page that gets shown to us:
When visiting http://authorization.oouch.htb:8000, we can see how to register to this server:
In case you're unaware of OAuth 2.0, you can read this:
It basically is the service that allows us to 'Login with Facebook' or other social media. The exploit here is to somehow use this faulty OAuth implementation to login as the administrator of the website and view the documents, which might contain some useful information.
For now, we can register a user and keep enumerating this website. This is the page we get to after logging in:
Clicking these two options don't seem to do much. I ran a gobuster scan once again.
We can see a new endpont at /applications. Trying to access this requires credentials:
I did not have any credentials for now. After creating this account and re-testing, using the /oauth/connect function earlier now shows a different user profile.
The website recognises my new OAuth account that I created and is considered 'connected'.
OAuth Forced Linking
Here's an overview of how exactly OAuth works:
We can view the HTTP requests throguh Burpsuite to see what exactly is happening.
These 4 requests are essentially the OAuth mechanism, and there is the access token in a POST request. Our goal is to authenticate as qtc, since he probably is the administrator user of this machine. As such, we can use a CSRF attack because this implementation of OAuth does not seem to send the state parameter. Earlier, we found a method to make the administrator qtc click links by sending it in the Contact form.
This can be used to link the qtc account to ours.
To exploit this, we first need to create some kind of new account and intercept the POST request created from accessing /oauth/connect . We need to intercept this response:
POST /oauth/authorize/?client_id=UDBtC8HhZI18nJ53kJVJpXp4IIffRhKEXZ0fSd82&response_type=code&redirect_uri=http://consumer.oouch.htb:5000/oauth/connect/token&scope=read HTTP/1.1Host:authorization.oouch.htb:8000User-Agent:Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8Accept-Language:en-US,en;q=0.5Accept-Encoding:gzip, deflateContent-Type:application/x-www-form-urlencodedContent-Length:266Origin:http://authorization.oouch.htb:8000Connection:closeReferer:http://authorization.oouch.htb:8000/oauth/authorize/?client_id=UDBtC8HhZI18nJ53kJVJpXp4IIffRhKEXZ0fSd82&response_type=code&redirect_uri=http://consumer.oouch.htb:5000/oauth/connect/token&scope=readCookie:csrftoken=aWzHarmWI5F3jiPsMIUB7tlpiFp0RMRAGmsWcW30NkvqWZaSiaYfT8VzwTGQ3vYc; sessionid=170pg3ogrhqjivnxsxn4wvt8ld65rd4gUpgrade-Insecure-Requests:1csrfmiddlewaretoken=v9wy7hWoUCz9Z5GQi5XBkH0zo21kut2aGlNFwNPiNTvAtHvs8N7iq6XU1acH3Y1P&redirect_uri=http%3A%2F%2Fconsumer.oouch.htb%3A5000%2Foauth%2Fconnect%2Ftoken&scope=read&client_id=UDBtC8HhZI18nJ53kJVJpXp4IIffRhKEXZ0fSd82&state=&response_type=code&allow=Authorize
We can forward this request and retrieve the next:
GET /oauth/connect/token?code=IhKT0Gc97DNDoVQp2Urhfun5pcl4Vj HTTP/1.1Host:consumer.oouch.htb:5000User-Agent:Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8Accept-Language:en-US,en;q=0.5Accept-Encoding:gzip, deflateReferer:http://authorization.oouch.htb:8000/Connection:closeCookie:session=.eJwlT8tqAzEM_BXjcyh-y85X9F5CkGQpuzTNlvXmFPLvNRQEwyDNQy971TuORYY9f72sOSbYHxkDb2JP9vMuOMTct5tZH-bYDDLPpTmWdZjfefNhL-_LaZrsMhZ7PvanTLZ2e7Y5S_elq3poEVJTqNqEci0uFQzJKTM4yqFw1NKRcoy--Uqt-hQlEdQguceK2TUlUc2F_RxyiYtn0OnRa0SHoUIsBAqJZ1ymVrLSrM9j1-uxfctj9kFIHiJqIS6BqwhqyJ1gilxJjBywQsp-6p5D9v8ngn3_AcFgVkE.ZA1w3A.rxmnxwIcngbjUG5bfhtMo3YL0G0Upgrade-Insecure-Requests:1
This is request that links accounts together. We need to drop this using Burpsuite and save the code sent. Afterwards, we can create a malicious link for qtc to click:
Send this in the Contact Form and wait for a little bit. Then, we can attempt to use OAuth by accessing /oauth/login on the consumer server. If done correctly, this is what we would see in the Documents section:
Great! Now we have credentials to access some other stuff. There's an SSH key somewhere on this website as well.
Registering -> Steal Token
Now that we have credentials, we can register somewhere. I used gobuster to scan the authorization server, and found another endpoint at /oauth/applications/register.
Based on the documents, it seems that the /api/get_user endpoint supports a GET method, meaning the authorization parameter is probably a token. Problem is, we don't have any credentials or tokens from qtc, we only forced a link. The next step is to steal a token or password, and I'm guessing we need him to click another link.
We can create a fake request that redirects the user to our our machine. This works because we have all the parameters we need on this page alone.
As such, we can fill up the registering form as such:
This article was useful in reading about how these requests are constructed.
Then we can listen on port 80 to capture the request being sent
/oauth/token
After getting this token, I was stuck for a long while. I was back to this page and realised I never really look at the /oauth/token function here:
Viewing the HTTP request, I realised that the sessionid token could be used to login as qtc.
Afterwards, accessing the /oauth/token endpoint did nothing for me until I experimented with sending POST requests:
We can read more about grant_types here:
By controlling the grant type, we can requests for the client_credentials. This can be done by first going to the Register page and changing the grant type:
Then we can send a reuqest specifying the client ID, client secret and the grant type and the token for qtc will be given to us:
This access_token parameter is the API token we need!
API SSH Keys
Now we can test the /api/get_user endpoint with our new access_token and the seessionid.
GET /api/get_ssh?access_token=O0Qwa2XObArpMZDhk3obUT4DLOgvH7 HTTP/1.1Host:authorization.oouch.htb:8000User-Agent:Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8Accept-Language:en-US,en;q=0.5Accept-Encoding:gzip, deflateConnection:closeCookie:csrftoken=aWzHarmWI5F3jiPsMIUB7tlpiFp0RMRAGmsWcW30NkvqWZaSiaYfT8VzwTGQ3vYc; sessionid=u46fo2dio2uyqksl1t3by906cbrcz4teUpgrade-Insecure-Requests:1
Now that we have access to this, we need to find the SSH key. After testing out a few endpoints, I found that get_ssh was the right one.
Then we can just SSH in as qtc.
Privilege Escalation
Docker Enumeration
Checking the home directory of this user reveals some interesting notes.
qtc@oouch:~$ cat .note.txt
Implementing an IPS using DBus and iptables == Genius?
I ran pspy64 on the machine to see what's running. Based on the hint, I should be looking at some kind of Intrusion Prevention System (IPS). There, I found some interesting lines:
There was some kind of cronjob going on, and there are docker containers present in this machine. Checking the output of ip addr reveals some docker containers are indeed present:
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
link/ether 02:42:c6:fa:ee:37 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
valid_lft forever preferred_lft forever
4: br-ac0a6de99daf: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:07:cf:da:64 brd ff:ff:ff:ff:ff:ff
inet 172.18.0.1/16 brd 172.18.255.255 scope global br-ac0a6de99daf
valid_lft forever preferred_lft forever
inet6 fe80::42:7ff:fecf:da64/64 scope link
valid_lft forever preferred_lft forever
We can probably SSH into some of them. Rough guesing let me SSH into 172.18.0.4.
Checking the processes, we see a lot of uswgi processes running:
Interesting. At the start of the machine, the application banned our IP address. Reading routes.py shows us how that function works:
@app.route('/contact', methods=['GET', 'POST'])@login_requireddefcontact():''' 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 contentsif form.validate_on_submit():# First apply our primitive xss filterif 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()returnrender_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.txttry:withopen("/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 sentreturnrender_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 userreturnrender_template('contact.html', title='Contact', send=False, form=form)
dbus is being used to block the IP address. Basically, when an entity is detected to be using SSS, dbus is used to send the client IP to iptables (which was found from the cronjob) and it closes the connection. Then, the attacker is blocked for one minute. We can read the configuration file for this on the main machine in another SSH shell:
Based on the configuration file, there's a severe misconfiguration because the www-data user is able to receive and send messages via dbus. As such, the next step is to somehow get a shell as www-data. This can be done using a uwsgi RCE exploit.
I'm not going to pretend I can understand and read the explanation of the exploit in Chinese. Before sending this file over, we need to remove the import bytes portion. Then, we can import this file into our machine, transfer it to the main machine and then the Docker container.
We can set up a listener port on the main machine, and execute use a simple bash one-liner to get a reverse shell:
As www-data, we have some permissions over dbus. Well, Hacktricks kinda spoiled the exploit for me because it revealed the exact command to use to get RCE as root using dbus-send.
How this exploit works is that the main machine is running iptables to block the IP addresses (remember the cronjob?). As such, we can try to test whether the input being sent there is sanitised. If it is not, we have a chance to inject code. To do this, we would need a method to send dbus messages, and this can be done with dbus-send.
We can append the RCE exploit at the back. Since the iptables portion is being handled by the root user on the main machine, the command is run as root, giving us a root shell!