Postmortem: Server compromised due to publicly accessible Redis

Lessons learned analyzing a really stupid oversight

Summary

My server was compromised through Redis and used as part of a DDOS.

Analysis

Background

I had previously played around with an app that used Redis, and installed Redis using both the package manager and direct download from redis.io. When I was done, I only remembered to uninstall the one from the package manager.

On 3 Nov 2015, Salvatore Sanfilippo, the creator of Redis, posted about how to gain access to a server running Redis. Here’s the important bit: out of the box, Redis accepts connections from anywhere and has no password. This makes it “basically an on-demand-write-this-file server,” which includes writing to .ssh/authorized_keys.

Problem

On 10 Nov 2015 at 12:50 PM and 23 Nov 2015 at 10:53 AM, an attacker connected to Redis and wrote their SSH public key to /root/.ssh/authorized_keys. We can watch this happen in the Redis logs:

[603] 10 Nov 12:50:17.435 * DB saved on disk
[603] 10 Nov 12:50:19.743 * DB saved on disk
[603] 10 Nov 12:50:21.568 * DB saved on disk

[...]

[603] 23 Nov 10:53:09.967 # User requested shutdown...
[603] 23 Nov 10:53:09.968 * Saving the final RDB snapshot before exiting.
[603] 23 Nov 10:53:09.973 * DB saved on disk
[603] 23 Nov 10:53:09.973 * Removing the pid file.
[603] 23 Nov 10:53:09.974 # Redis is now ready to exit, bye bye...

Here’s an example of an authorized_keys file created:

REDIS0006\0xfe\0x00\0x00\0x03abcA\0x93

ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDN2S[...] root@iZ94tq5pdslZ


\0xff\0xf7n\0xd90\0xf3>\0xcc\0x93

After gaining access to the server, the attackers created /root/8ucx.1, which appears to be the “installer” for the malware. It creates executables in /boot/ with random filenames, and corresponding services in /etc/init.d/ to run those executables. It also adds a cronjob in /etc/cron.hourly/cron.sh which brings up all the interfaces every hour (in case you tried to stop the DDOS traffic with ifdown.)

At 5:26 AM, the malware began sending DDOS traffic.

At 5:36 AM, DigitalOcean disabled networking after my instance had sent at 2 Gbps for 10 minutes.

Investigation

I deployed my website to a fresh instance and pointed the DNS away from the bad machine.

The first thing I did was check htop for suspicious processes. Fortunately, they picked very suspicious names: .lz followed by a random number.

Unfortunately, their executable copies itself to /tmp/ before running. But they have to be launched somewhere, right? If I were a malware developer, I’d probably install a cron job or init script.

They didn’t add any new crontab entries, but they left some more files with very suspicious names in /etc/init.d/: pyqxacywjm and .zl (not pictured).

The executable in /boot/ is the same as the /root/8ucx.1 executable, and /etc/.zl is the same as the /tmp/ files running.

So we’ll just delete these files, kill the running process, and be done with it, right? Not so fast: several minutes after I deleted everything, the files in /boot/ came back under different names, and with corresponding init scripts. (For some reason, .zl wasn’t put back — maybe because I put another file in its place.)

I scrolled through the processes again, and found another suspicious thing: multithreaded pwd?!

Process names are not trustworthy, because applications can overwrite argv[0]. Fortunately, Linux puts a symlink to the original executable in /proc/$PID/exe. Following this symlink led to the executable in /boot/!

So it looks like the first instance of the malware changes its name to something innocuous like pwd, sh, or cat. The first instance launches another instance and acts as its watchdog — if someone tampers with the other instance, it recreates the files under a different name and starts it again.

One last problem: occasionally, I would see a bunch of suspicious commands run, including ifconfig, netstat, and route:

The process is parented to init, which means the real parent exited before htop could observe it. Fortunately, we have Brendan Gregg’s excellent perf-tools scripts. They wrap the Linux kernel debugging framework ftrace (similar to DTrace on Solaris and BSD).

I wanted to use execsnoop to show calls to exec(), but DigitalOcean disabled my instance’s network connection. Rather than type the program in like an animal, I found David Butler’s JS console one-liner that automates typing into DigitalOcean’s web VNC client:

(function () {
  var t = prompt("Enter text to be sent to console").split("");
  function f() {
    window.rfb.sendKey(t.shift().charCodeAt());
    if (t.length > 0) { setTimeout(f, 10); }
  }
  f();
})();

It doesn’t support any characters that require the shift key, so I encoded the script as hex, then wrote this Python script on the server to decode it:

import binascii
hex_string = open("execsnoop.hex", "r").read().strip()
print binascii.unhexlify(hex_string)

VFTP: Vim File Transfer Protocol

It took awhile for execsnoop to catch anything, but when it did, I felt like an idiot:

Tracing exec()s. Ctrl-C to end.
Instrumenting sys_execve
   PID   PPID ARGS
  4030   4025 gawk -v o=0 -v opt_name=0 -v name= -v opt_duration=0 [...]
  4031   4029 cat -v trace_pipe
[...]
  4095   4094 /bin/sh -c /etc/cron.hourly/cron.sh
  4096   4095 /etc/cron.hourly/cron.sh
  4100   4097 awk -F: {print $1}
  4099   4097 grep :
  4098   4097 cat /proc/net/dev
  4103   4096 cp /lib/udev/udev /lib/udev/debug
  4102   4096 ifconfig eth0 up
  4101   4096 ifconfig lo up
  4104   4096 /lib/udev/debug

They put the script in /etc/cron.hourly/, not a user’s crontab! In case you’re curious, here’s what it looks like:

#!/bin/sh
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:/usr/X11R6/bin
for i in `cat /proc/net/dev|grep :|awk -F {'print $1'}`; do ifconfig $i up& done
cp /lib/udev/udev /lib/udev/debug
/lib/udev/debug

So if a sysadmin tries to stop the outgoing DDOS traffic with ifdown, it doesn’t matter because the interface will be turned on again the next hour! What assholes.

And to confirm that it’s actually being called by cron, I piped execsnoop into this Python script:

import os

while True:
  s = raw_input()
  if "sh -c" in s and ".hourly" in s:
    ppid = s.split()[1]
    os.system("ps -ocommand= -p " + ppid)
    # Output: "CRON"

Lessons Learned

Appendix

After I cleaned the server, DigitalOcean turned on networking and I uploaded the files to VirusTotal. VirusTotal says this is XorDDOS.

Because the malware customizes its binaries, each installation is slightly different and the hashes won’t match.

Attacker public keys

10 Nov 2015:

ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDrZmvmtjOOLDxlK0nlwmCocl3gWdfr9D8A8IZwWLflAOmVxVfLIvqGAi/pI4xyI8zTz60zTd9DDDm0kxRFYxLkDLFpkNjuNQe0VyrWlbWVVnSze+f2pDNvZ3hF+kfNoe80PHKUYkUaaENseQ63ndBmC9mC4s1EVADhyVVSesq6QdMBf+XVOQG/ERQtZm7FdDnLSBjKLMHjzhXRqYinBxUzM8YQynL+ptKRLXyCltwY+e9aTpA7np44mStrtrYc5t9EaVpzCoIFGV9n+/nvbrP7asnr2lWcw1l11AdYLkYRLPIYrmnGxHddT+bClpaemIcO7xLRAoUdAkOqgQYibv69

23 Nov 2015:

ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDN2Sqatf2OeAV+kKChp0VpT4+c0yBY+54lSymDjoNYsraxLbcs5e92sPVXcjF2iiaDH23wNHlpYrcr+/8HPRcvPJOpI+WUjeKfowxj/TcQmdO2fs3zy54uqT5s0RiO4SwERyR63aoXmLflhaOoFcj4NNTRDCeZB2FMC8jWaTxmYCe9iAIgzlJjamjubzUP4WNOPnxj5wrSibhUgGiOkDTkXDY7yim9dOcxpAvTw24rFZS8SzJqJvW5YjPmAm/V6iQurnu1VSv5eoUC/f7eNARwpKx1E470V3b2oz2Znx4zyrttp/2HAXTI92QpRM/cai3QCNPJQlVKGDW651kuTopB root@iZ94tq5pdslZ

Other fun finds

The attackers were not very good at programming: one of their scripts kept crashing and printing this to the first virtual terminal.

At one point, an automated SSH cracker guessed harrypotter as a username. Also very revealing about our industry: they only bother trying male first names.

Thanks for reading! If you’re enjoying my writing, I’d love to send you infrequent notifications for new posts via my newsletter. You’ll receive the full text of each post, plus occasional bonus content.

You can also follow me on Twitter (@kevinchen) or subscribe via RSS.