Percona Responds to MySQL LOCAL INFILE Security Issues

LOCAL INFILE SecurityIn this post, we’ll cover Percona’s thoughts about the current MySQL community discussion happening around MySQL LOCAL INFILE security issues.

This post is released given the already public discussion of this particular issue, with the exploitation code currently redacted to ensure forks of MySQL client libraries have sufficient time to implement their response strategies.

This post has been updated to now include previously redacted content, in line with responsible disclosure sufficient time has passed to allow forks to update and get those updates out for circulation.

Background

MySQL’s LOCAL INFILE  feature is fully documented by Oracle MySQL, and there is a legitimate use for the LOCAL INFILE feature to upload data to a MySQL server in a single statement from a file on the client system.

However, some MySQL clients can be coerced into sending contents local to the machine they are running upon, without having issued a LOCAL INFILE directive. This appears to be linked to how Adminer php web interface was attacked to point to a MALICIOUSLY crafted MySQL service to extract file data from the host on which Adminer was deployed. This malicious “server” has, it would appear, existed since early 2013.

The attack requires the use of a malicious/crafted MySQL “server”, to send a request for the file in place of the expected response to the SQL query in the normal query response flow.

IF however the client checks for the expected response, there is no file ex-filtration without further additional effort. This was noted with Java & ProxySQL testing, as a specific response was expected, and not sending the expected response would cause the client to retry.

I use the term “server” loosely here ,as often this is simply a service emulating the MySQL v10 protocol, and does not actually provide complete MySQL interaction capability—though this is theoretically possible, given enough effort or the adaption of a proxy to carry out this attack whilst backing onto a real MySQL server for the interaction capability.

For example, the “server” always responds OK to any auth attempt, regardless of credentials used, and doesn’t interpret any SQL sent. Consequently, you can send any string as a query, and the “server” responds with the request for a file on the client, which the client dutifully provides if local_infile is enabled.

There is potential, no doubt, for a far more sophisticated “server”. However, in my testing I did not go to this length, and instead produced the bare minimum required to test this theory—which proved to be true where local_infile was enabled.

The attack flow is as follows:

  1. The client connects to MySQL server, performs MySQL protocol handshaking to agree on capabilities.
  2. Authentication handshake (“server” often accepts any credentials passed to it).
  3. The client issues a query, e.g. SET NAMES, or other SQL (“server ignores this and immediately responds with file request response in 4.”).
  4. The server responds with a packet that is normally reserved when it is issued a “LOAD LOCAL DATA IN FILE…” SQL statement (0xFB…)
  5. IF Vulnerable the client responds with the full content of the file path if present on the local file system and if permissions allow this file to be read.
    1. Client’s handling here varies, the client may drop the connection with malformed packet error, or continue.

Exploitation testing

The following MySQL  clients were tested via their respective docker containers; and default configurations, the bash script which orchestrated this is as follows: <REDACTED>

This tests the various forks of the MySQL client; along with some manual testing the results were:

  • Percona Server for MySQL 5.7.24-26 (Not vulnerable)
    • PS 5.7.x aborts after server greeting
  • Percona Server for MySQL 5.6.42-64.2  (Not vulnerable)
    • PS 5.6 accepts the server greeting, proceeds to log in, aborts without handling malicious payload.
  • MariaDB 5.5
    • Susceptible to LOCAL INFILE abuse in testing
      • MariaDB has stated they will release a fix that tracks in the client to ensure the SQL for LOAD LOCAL INFILE was requested and otherwise drops the server request without handling.
  • MariaDB 10.0
    • Susceptible to LOCAL INFILE abuse in testing
      • MariaDB has stated they will release a fix that tracks in the client to ensure the SQL for LOAD LOCAL INFILE was requested and otherwise drops the server request without handling.
  • MariaDB 10.1.37
    • susceptible to LOCAL INFILE abuse in testing
      • MariaDB has stated they will release a fix that tracks in the client to ensure the SQL for LOAD LOCAL INFILE was requested and otherwise drops the server request without handling.
  • MariaDB 10.4.1
    • susceptible to LOCAL INFILE abuse in testing
      • MariaDB has stated they will release a fix that tracks in the client to ensure the SQL for LOAD LOCAL INFILE was requested and otherwise drops the server request without handling.
  • MySQL 5.7. (Not vulnerable by default)
    • Not susceptible to LOCAL INFILE abuse by default, enabling local_infile however makes this susceptible
  • MySQL 5.6. (Not vulnerable)
    • Not susceptible to LOCAL INFILE abuse by default, enabling local_infile however makes this susceptible
  • MySQL 8.0.14 (Not vulnerable)
    • Not susceptible to LOCAL INFILE abuse, enabling local_infile however makes this susceptible.
  • PHP 7 mysqli
    • Depends on libmysqlclient in use (As PHP’s mysqli is a C wrapper of the underlying library).
  • Ruby
    • Depends on libmysqlclient in use
    • Note: I couldn’t get this to build on my laptop due to a reported syntax error in mysql.c. However, given this wraps libmysqlclient, I would suggest the result to likely mirror PHP’s test.
  • ProxySQL
    • Underlying library is known susceptible to LOCAL INFILE abuse.
    • ProxySQL issues SQL to the backend MySQL server, and protocol commands such as PING, and expects a specific result in for queries issued by ProxySQL. This leads to difficulty for the malicious server being generic, a targeted client that specifically seeks to target ProxySQL is likely possible however this has not been explored at this time.
  • Java
    • com.mysql.jdbc.Driver
      • As with ProxySQL, testing this drive issues “background” SQL, and expects a specific response. While theoretically possible to have a malicious service target on this drive, this has not been explored at this time.
  • Connector/J

There are many more clients out there ranging from protocol compatible implementations to wrappers of the underlying c library.

Your own research will ensure you are taking appropriate measures should you choose/need to mitigate this risk in your controls.

Can/Should this be fixed?

This is a particularly tricky issue to correct in code, as the MySQL client needs to be aware of a LOAD LOCAL INFILE SQL statement getting sent. MariaDB’s proposed path implements this. Even then, if a stored procedure issues a file request via LOAD LOCAL INFILE..., the client has no awareness of this even being needed until the packet is received with the request, and local_infile can be abused. However, the intent is to allow the feature to load data, and as such DBAs/Admins should seek to employ compensating controls to reduce the risk to their organization:

Mitigation

  • DO NOT implement any stored procedures which trigger a LOAD INFILE.
  • Close/remove/secure access to ANY web admin interfaces.
    • Remember, security through obscurity is no security at all. This only delays time to access, it does not prevent access.
  • Deploy mandatory access controls
    • SELinux, AppArmor, GRSecurity, etc. can all help to ensure your client is not reading anything unexpected, lowering your risk of exposure through proper configuration.
  • Deploy Egress controls on your application nodes to ensure your application server can only reach your MySQL service(s) and does not attempt to connect elsewhere (As the exploit requires a malicious MySQL service).
    • Iptables/firewalld/ufw/pfsense/other firewall/etc.
    • This ensures that your vulnerable clients are not connecting to anything you do not know about.
    • This does not protect against a skilled adversary. Your application needs to communicate out to the internet to server pages. Running a malicious MySQL service on a suitably high random port can aid to “hide” this network traffic.
  • Be aware of Domain Name Service (DNS) rebinding attacks if you are using a Fully Qualified Domain Name (FQDN) to connect between application and database server. Use an IP address or socket in configurations if possible to negate this attack.
  • Deploy MySQL Transport Layer Security (TLS) configuration to ensure the server you expect requires the use of TLS during connection, set your client (if possible) to VERIFY_IDENTITY to ensure TLS “fails closed” if the client fails to negotiate TLS, and to perform basic identity checking of the server being connected to.
    • This will NOT dissuade a determined adversary who has a presence in your network long enough to perform certificate spoofing (in theory), and nothing but time to carry this out.
    • mysslstrip can also lead to issues if your configuration does “fail open” as such it is imperative you have:
      • In my.cnf: ssl_mode=VERIFY_IDENTITY
      • On the cli: –ssl_mode=VERIFY_IDENTITY
      • Be aware: This performs verification of the CA (Certificate Authority) and certificate hostname, this can lead to issues if you are using self-signed certificates and the CA is not trusted.
    • This is ONLY an issue if an adversary has the capability of being able to Man in the middle your Application <-> MySQL servers;
      • If they have this capability; this feature abuse is only a single avenue of data ex-filtration they can perform.
  • Deploy a Network Intrusion Detection System
    • There are many open source software (OSS) options, for example:
    • Set alerts on the logs, curate a response process to handle these alerts.
  • Client option mitigation may be possible; however, this varies from client to client and from underlying library to library.
    • MariaDB client binary.
      • Add to my.cnf: local_infile = 0
      • Or set –local_infile=0 on the command line
    • PHP / Ruby / Anything that relies on libmysqlclient
      • Replace libmysqlclient with a version that does not enable local_infile by default
        • This can be difficult, so ensure you test your process before running anything on production!
      • Switch to use PDO MySQL over MySQLi (PDO implementation implicitly sets, local_infile to 0 at the time of writing in php’s C code).
        • Authors note: mysqli_options($conn, MYSQLI_OPT_LOCAL_INFILE, false); failed to mitigate this in testing, YMMV (Your Mileage May Vary).
        • Attempting to set a custom handler to return nothing also failed to mitigate this. Again, YMMV.

IDS Rule example

Here I provide an example “FAST” format rule for your IDS/IPS system;

Note however YMMV; this works with Snort, Suricata, and _may_ work with Zeek (formerly Bro), OSSEC, etc. However, please test and adapt as needed;

alert tcp any any <> any any (msg: “MySQL LOCAL INFILE request packet detected”; “content:”|00 00 01 FB|”; rawbytes)

Note this is only an example, this doesn’t detect any packets flowing over TLS connections.

If you are running an Intrusion Prevention System (IPS), you should change the rule action from alert to drop.

Here the rule is set to any any as an adversary may wish to not use 3306 in an attempt to avoid detection you can of course change this as desired to suit your needs.

You must also assess if your applications are running local_infile legitimately and conduct your own threat modeling as well as impact analysis, prior to implementing such a rule.

Note increasing the “noise” threshold for your team, will likely only result in your team becoming desensitized to the “noise” and potentially missing an important alert as a result.

For example, you could modify the left and right side any any, to be anything not in your internal network range communicating to anything not in your internal network range:

alert tcp 192.168.1.0/24 any <> !192.168.1.0/24 any  (msg:”MySQL LOCAL INFILE request packet detected”; “content:”|00 00 01 FB|”; rawbytes)

Adapting to your environment is key for this IDS rule to be effective.

Further reading

As noted this issue is already being publicly discussed, as such I add links here to sources relevant to this discussion and exploitation.

Exploitation Network flow

Here I am using wireshark to show the tcp communication flow client to MySQL ‘server’, here the ‘server’ is of course malicious and set to sent the file request on each receipt of ‘Request Query’: as can be seen here the ‘server’ masquerades as a 5.1.66 MySQL server running on Debian squeeze.

 

Now we jump to the malicious response packet, sent in reply to the earlier ‘Request Query’; which through legitimate use of SQL where  LOCAL INFILE is issued this request packet would be the expected response from the ‘server’. In this case however the ‘server’ is requesting the file: /proc/self/environ this file can contain a wealth of information including anything you may have stored in an environment variable, in practise this can be set to any full filepath.

I’ve highlighted the two key parts of this packet, the tail of mysql.packet_length (blue) which heads the request and the file path being requested (red).

Here the client responds with the content of the requested file path, and displays no indication the file content has been sent to the MySQL ‘server’.

As shown here the complete TCP conversation flow between the MySQL binary client and MySQL ‘server’ (content has been redacted).

Note the SQL sent was  I AM MYSQL BINARY, this is not valid SQL; however this also demonstrates that the MySQL client does not parse SQL before sending it to the ‘server’.

If you’re one whom enjoys CTF’s (Capture the flag) challenges, you may find a version of the above here: https://github.com/Oneiroi/ctf/tree/master/pcap/mysql/data_exfil this includes a pcap file showing a similar packet flow.

I have shared my modified version of the rogue mysql server on Github here: https://github.com/Oneiroi/Rogue-MySql-Server/tree/local_infile_abuse_test some minor edits were required to make functional (there are ~40 some forks of the original project on GH, some with additional functionality)

Thanks

This assessment was not a single person effort, here I would like to link to and give thanks where appropriate to the following individuals whom have helped with this investigation:

Willem de Groot – For sharing insights into the Adminer exploitation and for graciously responding to an inquiry from myself (this helped me get the PoC working, thank you).

Gifts – original author of evil mysql server implementation (in 2013!), from which I was able to adapt to function for this investigation.

Ceri Williams – for helping me with proxySQL testing.

Marcelo Altman – for discussing MySQL protocol in depth.

Sergei Golubchik – for responding to my email notice for MariaDB, and implementing a workaround mitigation so quickly, as well providing me with a notice on the Connector/J announcement url.

Peter Zaitsev – for linking me to the original reddit discussion and for feedback.

Share this post

Comments (4)

  • Federico Reply

    I’m happy that this ancient problem is getting proper attention now. Thank you David. A couple of comments:
    1) You don’t mention the LOAD XML statement. Is this a mistake or is it more secure for some reason?
    2) LOAD DATA is not allowed in stored procedures/functions. And cannot be used as a prepared statement (I mention this because prepared statements can always run in stored procedures).

    February 6, 2019 at 4:23 pm
  • David Busby Reply

    Hi Federico,

    1) The ‘feature abuse’occurs in the client handling of the INFILE packet request from the server, this is made clear in the network flows which unfortunately are redacted at this moment in time, once we’ve had response from the one fork whim has not provided indicate they have made an appropriate response or Feb 28th is reached I will update this post with the redacted detail in line this will provide more clarity on where the abuse / exploitation occurs. As the SQL issues by the client is ignored, the client need not actually send a LOAD XML / LOAD DATA … the malicious server will always just respond with the file request packet and where local_infile is enabled the client will respond with the file contents if accessible. I’ve taken this approach to ensure responsible disclosure practises are followed the rest of this content however is already in the public domain, hence this post has been released with this detail adding testing and mitigation recomendations.

    2) This was added as a consideration based on feedback from one of our dev leads, this consideration was for the fix only I’ll follow up with them and query this. As noted in post and in response above, the exploitation ocurrs when the client has local_infile enabled, it will always respond with the file contents if accessible; this allows the ‘server’ to send the packet in responds to any SQL sent in testing.

    February 7, 2019 at 5:20 am
  • Andrew Reply

    This seems to paint Percona in a particularly good light, but ultimately PXC / Percona Server seems to be in the same situation as upstream MySQL. If you disable local-infile at the client level you’re fine, but once local_infile is enabled it seems just as susceptible. The default is off in *some* pre-compiled distribution releases but not if you compile from source or apparently you use the ubuntu packages. Is MariaDB much different if one takes care to disable local_infile?

    There is a bug in the rogue_mysql_server.py implementation which underspecifies the salt in the handshake packet which may explain this discrepancy.

    February 8, 2019 at 12:00 am
  • David Busby Reply

    Hi Andrew,

    Percona Server being downstream of Oracle MySQL (as well as PXC) it makes sense that this would mirror upstream by default (and yes if you enabled local_infile and re-run the tests they are vulnerable to this issue, however what I have highlighted in this post is the default OTB state of each fork and version tested).

    There’s no intent to paint Percona Server nor PXC in a positive light, had they been vulnerable by default this would have been noted here regardless, we do not (and should not) show bias to even our own products at Percona.

    The difference here was MariaDB was allowing local_infile by default, there should now be workaround fix in place such that if there is no SQL statement issues which would require the server to send the file request packet, if a packet is received it is instead ignored (as noted from MariaDB to myself in email).

    w.r.t rogue_mysql_server.py I’d suggest such discrepancies were intentional to ensure only those whom understand the protocol can re-arm the rogue_mysql_server to achieve the desired result, as stated I am waiting on response from a fork at this time and can not go into much detail at the moment on this.

    w.r.t the disabling of local_infile; and the vulnerabilities of enabling it again, that is true and for the record I personally think MariaDB has the best proposed workaround here in having the client respond only if SQL is known to have been issued with LOAD … and ignore all other requests if this has not been the case.

    _if_ you compile from source and ensure that local_infile support is disabled, and this fits your use case in your deployments, excellent; of course this will protect against this particular issue.

    However you have to weigh this against maintaining your own builds for future releases which patch other security vulnerabilities.

    The analysis here has been intended to cover all forks, not to paint one or another more or less favourably by any means.

    February 8, 2019 at 6:13 am

Leave a Reply