top of page
Post: Blog2 Post

Cap

  • Writer: Vosman
    Vosman
  • Oct 2, 2021
  • 8 min read

This was a nice easy box with a simple initial foothold and an easy escalation to root, but not so simple as to be completely obvious. So with that said let's jump on into it.

Enumeration

NMAP

# Nmap 7.91 scan initiated Sun Jul 25 17:22:34 2021 as: nmap -sCV -vv -n -oA nmap/nmap_CapInitial 10.10.10.245
Nmap scan report for 10.10.10.245
Host is up, received reset ttl 63 (0.017s latency).
Scanned at 2021-07-25 17:22:34 BST for 128s
Not shown: 997 closed ports
Reason: 997 resets
PORT   STATE SERVICE REASON         VERSION
21/tcp open  ftp     syn-ack ttl 63 vsftpd 3.0.3
22/tcp open  ssh     syn-ack ttl 63 OpenSSH 8.2p1 Ubuntu 4ubuntu0.2 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 fa:80:a9:b2:ca:3b:88:69:a4:28:9e:39:0d:27:d5:75 (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC2vrva1a+HtV5SnbxxtZSs+D8/EXPL2wiqOUG2ngq9zaPlF6cuLX3P2QYvGfh5bcAIVjIqNUmmc1eSHVxtbmNEQjyJdjZOP4i2IfX/RZUA18dWTfEWlNaoVDGBsc8zunvFk3nkyaynnXmlH7n3BLb1nRNyxtouW+q7VzhA6YK3ziOD6tXT7MMnDU7CfG1PfMqdU297OVP35BODg1gZawthjxMi5i5R1g3nyODudFoWaHu9GZ3D/dSQbMAxsly98L1Wr6YJ6M6xfqDurgOAl9i6TZ4zx93c/h1MO+mKH7EobPR/ZWrFGLeVFZbB6jYEflCty8W8Dwr7HOdF1gULr+Mj+BcykLlzPoEhD7YqjRBm8SHdicPP1huq+/3tN7Q/IOf68NNJDdeq6QuGKh1CKqloT/+QZzZcJRubxULUg8YLGsYUHd1umySv4cHHEXRl7vcZJst78eBqnYUtN3MweQr4ga1kQP4YZK5qUQCTPPmrKMa9NPh1sjHSdS8IwiH12V0=
|   256 96:d8:f8:e3:e8:f7:71:36:c5:49:d5:9d:b6:a4:c9:0c (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBDqG/RCH23t5Pr9sw6dCqvySMHEjxwCfMzBDypoNIMIa8iKYAe84s/X7vDbA9T/vtGDYzS+fw8I5MAGpX8deeKI=
|   256 3f:d0:ff:91:eb:3b:f6:e1:9f:2e:8d:de:b3:de:b2:18 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPbLTiQl+6W0EOi8vS+sByUiZdBsuz0v/7zITtSuaTFH
80/tcp open  http    syn-ack ttl 63 gunicorn
| fingerprint-strings: 
|   FourOhFourRequest: 
|     HTTP/1.0 404 NOT FOUND
|     Server: gunicorn
|     Date: Sun, 25 Jul 2021 16:22:46 GMT
|     Connection: close
|     Content-Type: text/html; charset=utf-8
|     Content-Length: 232
|     <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//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.0 200 OK
|     Server: gunicorn
|     Date: Sun, 25 Jul 2021 16:22:40 GMT
|     Connection: close
|     Content-Type: text/html; charset=utf-8
|     Content-Length: 19386
|     <!DOCTYPE html>
|     <html class="no-js" lang="en">
|     <head>
|     <meta charset="utf-8">
|     <meta http-equiv="x-ua-compatible" content="ie=edge">
|     <title>Security Dashboard</title>
|     <meta name="viewport" content="width=device-width, initial-scale=1">
|     <link rel="shortcut icon" type="image/png" href="/static/images/icon/favicon.ico">
|     <link rel="stylesheet" href="/static/css/bootstrap.min.css">
|     <link rel="stylesheet" href="/static/css/font-awesome.min.css">
|     <link rel="stylesheet" href="/static/css/themify-icons.css">
|     <link rel="stylesheet" href="/static/css/metisMenu.css">
|     <link rel="stylesheet" href="/static/css/owl.carousel.min.css">
|     <link rel="stylesheet" href="/static/css/slicknav.min.css">
|     <!-- amchar
|   HTTPOptions: 
|     HTTP/1.0 200 OK
|     Server: gunicorn
|     Date: Sun, 25 Jul 2021 16:22:41 GMT
|     Connection: close
|     Content-Type: text/html; charset=utf-8
|     Allow: GET, HEAD, OPTIONS
|     Content-Length: 0
|   RTSPRequest: 
|     HTTP/1.1 400 Bad Request
|     Connection: close
|     Content-Type: text/html
|     Content-Length: 196
|     <html>
|     <head>
|     <title>Bad Request</title>
|     </head>
|     <body>
|     <h1><p>Bad Request</p></h1>
|     Invalid HTTP Version &#x27;Invalid HTTP Version: &#x27;RTSP/1.0&#x27;&#x27;
|     </body>
|_    </html>
| http-methods: 
|_  Supported Methods: GET HEAD OPTIONS
|_http-server-header: gunicorn
|_http-title: Security Dashboard
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port80-TCP:V=7.91%I=7%D=7/25%Time=60FD8FD1%P=x86_64-pc-linux-gnu%r(GetR
SF:equest,2FE5,"HTTP/1\.0\x20200\x20OK\r\nServer:\x20gunicorn\r\nDate:\x20
SF:Sun,\x2025\x20Jul\x202021\x2016:22:40\x20GMT\r\nConnection:\x20close\r\
SF:nContent-Type:\x20text/html;\x20charset=utf-8\r\nContent-Length:\x20193
SF:86\r\n\r\n<!DOCTYPE\x20html>\n<html\x20class=\"no-js\"\x20lang=\"en\">\
SF:n\n<head>\n\x20\x20\x20\x20<meta\x20charset=\"utf-8\">\n\x20\x20\x20\x2
SF:0<meta\x20http-equiv=\"x-ua-compatible\"\x20content=\"ie=edge\">\n\x20\
SF:x20\x20\x20<title>Security\x20Dashboard</title>\n\x20\x20\x20\x20<meta\
SF:x20name=\"viewport\"\x20content=\"width=device-width,\x20initial-scale=
SF:1\">\n\x20\x20\x20\x20<link\x20rel=\"shortcut\x20icon\"\x20type=\"image
SF:/png\"\x20href=\"/static/images/icon/favicon\.ico\">\n\x20\x20\x20\x20<
SF:link\x20rel=\"stylesheet\"\x20href=\"/static/css/bootstrap\.min\.css\">
SF:\n\x20\x20\x20\x20<link\x20rel=\"stylesheet\"\x20href=\"/static/css/fon
SF:t-awesome\.min\.css\">\n\x20\x20\x20\x20<link\x20rel=\"stylesheet\"\x20
SF:href=\"/static/css/themify-icons\.css\">\n\x20\x20\x20\x20<link\x20rel=
SF:\"stylesheet\"\x20href=\"/static/css/metisMenu\.css\">\n\x20\x20\x20\x2
SF:0<link\x20rel=\"stylesheet\"\x20href=\"/static/css/owl\.carousel\.min\.
SF:css\">\n\x20\x20\x20\x20<link\x20rel=\"stylesheet\"\x20href=\"/static/c
SF:ss/slicknav\.min\.css\">\n\x20\x20\x20\x20<!--\x20amchar")%r(HTTPOption
SF:s,B3,"HTTP/1\.0\x20200\x20OK\r\nServer:\x20gunicorn\r\nDate:\x20Sun,\x2
SF:025\x20Jul\x202021\x2016:22:41\x20GMT\r\nConnection:\x20close\r\nConten
SF:t-Type:\x20text/html;\x20charset=utf-8\r\nAllow:\x20GET,\x20HEAD,\x20OP
SF:TIONS\r\nContent-Length:\x200\r\n\r\n")%r(RTSPRequest,121,"HTTP/1\.1\x2
SF:0400\x20Bad\x20Request\r\nConnection:\x20close\r\nContent-Type:\x20text
SF:/html\r\nContent-Length:\x20196\r\n\r\n<html>\n\x20\x20<head>\n\x20\x20
SF:\x20\x20<title>Bad\x20Request</title>\n\x20\x20</head>\n\x20\x20<body>\
SF:n\x20\x20\x20\x20<h1><p>Bad\x20Request</p></h1>\n\x20\x20\x20\x20Invali
SF:d\x20HTTP\x20Version\x20&#x27;Invalid\x20HTTP\x20Version:\x20&#x27;RTSP
SF:/1\.0&#x27;&#x27;\n\x20\x20</body>\n</html>\n")%r(FourOhFourRequest,189
SF:,"HTTP/1\.0\x20404\x20NOT\x20FOUND\r\nServer:\x20gunicorn\r\nDate:\x20S
SF:un,\x2025\x20Jul\x202021\x2016:22:46\x20GMT\r\nConnection:\x20close\r\n
SF:Content-Type:\x20text/html;\x20charset=utf-8\r\nContent-Length:\x20232\
SF:r\n\r\n<!DOCTYPE\x20HTML\x20PUBLIC\x20\"-//W3C//DTD\x20HTML\x203\.2\x20
SF:Final//EN\">\n<title>404\x20Not\x20Found</title>\n<h1>Not\x20Found</h1>
SF:\n<p>The\x20requested\x20URL\x20was\x20not\x20found\x20on\x20the\x20ser
SF:ver\.\x20If\x20you\x20entered\x20the\x20URL\x20manually\x20please\x20ch
SF:eck\x20your\x20spelling\x20and\x20try\x20again\.</p>\n");
Service Info: OSs: Unix, Linux; CPE: cpe:/o:linux:linux_kernel

Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Sun Jul 25 17:24:42 2021 -- 1 IP address (1 host up) scanned in 128.31 seconds

OK, so three ports open:

21 - FTP

22 - SSH

80 - HTTP


FTP and a web-server are going to be interesting. SSH is there too, but maybe this will come into play a bit later.


Getting User

FTP

Let's take a look at FTP first as that can be an easy win if anonymous logins are enabled:

Connected to 10.10.10.245.
220 (vsFTPd 3.0.3)
Name (10.10.10.245:root): anonymous
331 Please specify the password.
Password:
530 Login incorrect.
Login failed.

OK, no joy there. I'm sure we'll be back later.


Web Application

Right let's take a look at what is on port 80:

The initial look at the web page being presented on the web-server

Looks like some sort of system monitoring tool. These kind of things are common in Security Operation Centers (SOC) that are in larger companies. They help the network and security people keep an eye out for suspicious behavior. However, after looking around this application a bit more it's just a static page and doesn't really help much, apart from noticing the username is "Nathan". Let's have a look into what else we do have.

I ran GoBuster against this web-server but it only brought up the three URL's we find in the menu so nothing more there.

Clicking on the menu bars button we get a couple of options that appear to show information about the host itself such as the IP configuration. There's not much we can do with that but looking at the Security Snapshot option we get sent to the /data/1 URL but there's no captures here so let's try selecting a few more numbers. If there's a number in the URL or argument I always try the number below it first and zero is before one so let's try that:

When looking at this page we do see some packets have been captured. Let's try downloading them and see what we can find out about these network packets.


When the file is downloaded we can see that it is a .pcap file. These can be examined in Wireshark and maybe other network packet inspection tools but Wireshark is definitely my preferred choice when it comes to this sort of thing.


Let's open Wireshark and import the captures, there's usually something interesting in these sorts of things especially when they are from a security monitoring tool.


OK, as we can see in the first couple of lines the capture has FTP packets stored. FTP packets can be a goldmine of information, actually any plain-text protocol packets (such as HTTP) can be too. Right clicking on the first FTP line and selecting Follow > TCP Stream we can see the following:


Awesome! We can see the user Nathan's password! This is exactly why using plain-text protocols on an untrusted network is a bad idea. OK so let's try logging in via FTP:

ftp nathan@10.10.10.245

Enter the password and we're in! Nice, looking at the directory we can see the user.txt file. So let's download it with the command get user.txt.


I always like to see if the FTP server has been configured to restrict a user to the one directory. Trying the cd .. command, we find we can get out of the directory and browse the whole file-system. This isn't best practice as it now means an attacker could potentially gain access to configurations files on the system. However, doing this via FTP is a little time consuming. A good thing to remember is that users often use the same password for different services so let's try logging in with those same user credentials with SSH:

Excellent, it worked! Now we have a lot more flexibility to look around the box and see what's going on.


Getting Root

Right, let's have a look at what processes are running on this box:

Hmm, OK so nothing there. Another command I like to run when jumping on to a box is sudo -l to see if there's any commands we can run as root:

Nope, nothing...



Well we know there is a web server running so lets go take a look around for that. First place to try is the usual /var/www/html directory, and we find app.py:

That could be interesting, let's take a closer look.


Here's the code:

#!/usr/bin/python3

import os
from flask import *
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
import tempfile
import dpkt
from werkzeug.utils import append_slash_redirect

app = Flask(__name__)
app.config['TEMPLATES_AUTO_RELOAD'] = True
app.secret_key = b'\x81\x02&\x18\\a0ej\x06\xec\x917y*\x04Y\x83e\xebC\xee\xab\xcf\xac;\x8dx\x8bf\xc4\x15'
limiter = Limiter(app, key_func=get_remote_address, default_limits=["99999999999999999 per day", "99999999999999999999 per hour"])
pcapid = 0
lock = False

@app.before_first_request
def get_file_id():
        global pcapid
        path = os.path.join(app.root_path, "upload")
        onlyfiles = [f for f in os.listdir(path) if os.path.isfile(os.path.join(path, f))]
        ints = []
        for x in onlyfiles:
                try:
                        ints.append(int(x.replace(".pcap", "")))
                except:
                        pass
        try:
                pcapid = max(ints)+1
        except:
                pcapid = 0


def get_appid():
        global pcapid
        return pcapid

def increment_appid():
        global pcapid
        pcapid += 1

def get_lock():
        global lock
        while lock:
                pass
        lock = True

def release_lock():
        global lock
        lock = False

def process_pcap(pcap_path):
        reader = dpkt.pcap.Reader(open(pcap_path, "rb"))
        counter=0
        ipcounter=0
        tcpcounter=0
        udpcounter=0

        for ts, pkt in reader:
                counter+=1
                eth=dpkt.ethernet.Ethernet(pkt)

                try:
                        ip=dpkt.ip.IP(eth.data)
                except:
                        continue

                ipcounter+=1

                if ip.p==0:
                        tcpcounter+=1

                if ip.p==dpkt.ip.IP_PROTO_UDP:
                        udpcounter+=1

        data = {}
        data['Number of Packets'] = counter
        data['Number of IP Packets'] = ipcounter
        data['Number of TCP Packets']  = tcpcounter
        data['Number of UDP Packets']  = udpcounter
        return data


@app.route("/")
def index():
        return render_template("index.html")

PCAP_MAGIC_BYTES = [b"\xa1\xb2\xc3\xd4", b"\xd4\xc3\xb2\xa1", b"\x0a\x0d\x0d\x0a"]

@app.route("/capture")
@limiter.limit("10 per minute")
def capture():

        get_lock()
        pcapid = get_appid()
        increment_appid()
        release_lock()

        path = os.path.join(app.root_path, "upload", str(pcapid) + ".pcap")
        ip = request.remote_addr
        # permissions issues with gunicorn and threads. hacky solution for now.
        #os.setuid(0)
        #command = f"timeout 5 tcpdump -w {path} -i any host {ip}"
        command = f"""python3 -c 'import os; os.setuid(0); os.system("timeout 5 tcpdump -w {path} -i any host {ip}")'"""
        os.system(command)
        #os.setuid(1000)

        return redirect("/data/" + str(pcapid))

@app.route("/ip")
def ifconfig():
	d = os.popen("ifconfig").read().strip()
	print(d)
	return render_template("index.html", rawtext=d)

@app.route("/netstat")
def netstat():
	d = os.popen("netstat -aneop").read().strip()
	print(d)
	return render_template("index.html", rawtext=d)

@app.route("/data")
def data():
        if "data" not in session:
                return redirect("/")
        data = session.pop("data")
        path = session.pop("path")
        return render_template("data.html", data=data, path=path)

@app.route("/data/<id>")
def data_id(id):
        try:
                id = int(id)
        except:
                return redirect("/")
        try:
                data = process_pcap(os.path.join(app.root_path, "upload", str(id) + ".pcap"))
                path = str(id) + ".pcap"
                return render_template("index.html", data=data, path=path)
        except Exception as e:
                print(e)
                return redirect("/")

@app.route("/download/<id>")
def download(id):
        try:
                id = int(id)
        except:
                return redirect("/")
        uploads = os.path.join(app.root_path, "upload")
        return send_from_directory(uploads, str(id) + ".pcap", as_attachment=True)

if __name__ == "__main__":
        app.run("0.0.0.0", 80, debug=True)

Looking through this code we see this little nugget:

This is interesting, so it looks like there is a problem capturing the network packets as tcpdump needs to be run as root and the application is not running with sufficient privileges. Or there is just some other problem with threading? Either way what they have done is set the UserID to zero (root) and then run the command to capture the data they want.

Now, normally the setuid() command can only be used by the root user, so for it to work inside this script either the script/app has to be running as root or something else is going on. Because the comments in the script say they're using a "hacky" way to get this working makes me think there is something else going on here.


A Possible Rabbit-Hole

Now I think most people when they see this might think "OK, let's run this app and see what happens" which is fine and I would do that too most of the time however, I didn't do this until after I got the root flag, but we'll come back to why this was the case in a moment.


So, this is where you may fall down a small rabbit-hole, if you run this app you'll get an error because it tries to use port 80 which is already in use, so you can just change it to another port e.g. 1500. When you browse to your version of the app you'll see it probably errored and that you can run python commands if you enter the pin which is displayed on the command line:

You may also notice that it gives the same PIN each time you run it so you may think ah-ha! Maybe I can get to the debug console on the main app, it might be running as root because it's on port 80. When you do a bit of research about the debug console you'll see if it is enabled you can browse to /console and maybe get access to the system through that and use the same PIN codes. If not the same PIN codes maybe you can figure out the code by running some scripts you can find online. But this doesn't work as there is no debug console available on the running app, so that's a dead end anyway.

 

So, back to why I didn't mess around with the app straight way. I was looking at the code where it captures the network packets and thought:

This is a Python script, why would you need to run another instance of Python from within the code to setuid(0) and run the command you want if the app was already running as root?

Then the thought occurred to me that "they need a hacky way to capture packets, if the app was running as root it wouldn't need to be a hack, so the app doesn't run as root!"


OK so now I'm fairly certain the app isn't running as root so how does this work because you shouldn't be able to use setuid(), so... and BOOM! "Linux Capabilities!!" popped into my head; what if the Python binary has the CAP_SETUID setting enabled then it could be used to set the UID to zero and run system commands as root! So let's take a look:

Yes, and it has the Effective, Inherited and Permitted flags set so as far as I understand it I can just run Python from the command line and get it to execute bash as root. We can just use the command in the app.py code to do just that. Let's try:

Sweet! Now we have the user ID of zero (root) and we can grab the flag from /root/root.txt.


Job Done!

Vosman @vosNET-Cyber

Recent Posts

See All

Comments


bottom of page