In 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.
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 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:
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.
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:
mysqli_options($conn, MYSQLI_OPT_LOCAL_INFILE, false); failed to mitigate this in testing, YMMV (Your Mileage May Vary).
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.
As noted this issue is already being publicly discussed, as such I add links here to sources relevant to this discussion and exploitation.
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)
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.