I recently had a Linux client that was, for whatever odd reason, making infinite recursive HTTP calls to a single script, which was making the server process count skyrocket. I decided to use the same module as I did in my Painless migration from PHP MySQL to MySQLi post, which is to say, overriding base functions for fun and profit using the PHP runkit extension. I did this so I could gather, for debugging, logs of when and where the calls that were causing this to occur.
The below code overrides all functions listed on the line that says “List of functions to intercept” [Line 9]. It works by first renaming these built in functions to “OVERRIDE_$FuncName” [Line 12], and replacing them with a call to “GlobalRunFunc()” [Line 13], which receives the original function name and argument list. The GlobalRunFunc():
Checks to see if it is interested in logging the call
In the case of this example, it will log the call if [Line 20]:
Line 21: curl_setopt is called with the CURLOPT_URL parameter (enum=10002)
Line 22: curl_init is called with a first parameter, which would be a URL
Line 23: file_get_contents or fopen is called and is not an absolute path (Wordpress calls everything absolutely. Normally I would have only checked for http[s] calls).
If it does want to log the call, it stores it in a global array (which holds all the calls we will want to log). The logged data includes [Line 25]:
The function name
The function parameters
2 functions back of backtrace (which can often get quite large when stored in the log file)
It then calls the original function, with parameters intact, and passes through the return [Line 27].
The “GlobalShutdown()” [Line 30] is then called when the script is closing [Line 38] and saves all the logs, if any exist, to “$GlobalLogDir/$DATETIME.srl”.
I have it using “serialize()” to encode the log data [Line 25], as opposed to “json_encode()” or “print_r()” calls, as the latter were getting too large for the logs. You may want to have it use one of these other encoding functions for easier log perusal, if running out of space is not a concern.
<?
//The log data to save is stored here
global $GlobalLogArr, $GlobalLogDir;
$GlobalLogArr=Array();
$GlobalLogDir='./LOG_DIRECTORY_NAME';
//Override the functions here to instead have them call to GlobalRunFunc, which will in turn call the original functions
foreach(Array(
'fopen', 'file_get_contents', 'curl_init', 'curl_setopt', //List of functions to intercept
) as $FuncName)
{
runkit_function_rename($FuncName, "OVERRIDE_$FuncName");
runkit_function_add($FuncName, '', "return GlobalRunFunc('$FuncName', func_get_args());");
}
//This optionally
function GlobalRunFunc($FuncName, $Args)
{
global $GlobalLogArr;
if(
($FuncName=='curl_setopt' && $Args[1]==10002) || //CURLOPT enumeration can be found at https://curl.haxx.se/mail/archive-2004-07/0100.html
($FuncName=='curl_init' && isset($Args[0])) ||
(($FuncName=='file_get_contents' || $FuncName=='fopen') && $Args[0][0]!='/')
)
$GlobalLogArr[]=serialize(Array('FuncName'=>$FuncName, 'Args'=>$Args, 'Trace'=>array_slice(debug_backtrace(), 1, 2)));
return call_user_func_array("OVERRIDE_$FuncName", $Args);
}
function GlobalShutdown()
{
global $GlobalLogArr, $GlobalLogDir;
$Time=microtime(true);
if(count($GlobalLogArr))
file_put_contents($GlobalLogDir.date('Y-m-d_H:i:s.'.substr($Time-floor($Time), 2, 3), floor($Time)).'.srl', implode("\n", $GlobalLogArr));
}
register_shutdown_function('GlobalShutdown');
?>
The PHP MySQL extension is being deprecated in favor of the MySQLi extension in PHP 5.5, and removed as of PHP 7.0. MySQLi was first referenced in PHP v5.0.0 beta 4 on 2004-02-12, with the first stable release in PHP 5.0.0 on 2004-07-13[1]. Before that, the PHP MySQL extension was by far the most popular way of interacting with MySQL on PHP, and still was for a very long time after. This website was opened only 2 years after the first stable release!
With the deprecation, problems from some websites I help host have popped up, many of these sites being very, very old. I needed a quick and dirty solution to monkey-patch these websites to use MySQLi without rewriting all their code. The obvious answer is to overwrite the functions with wrappers for MySQLi. The generally known way of doing this is with the Advanced PHP Debugger (APD). However, using this extension has a lot of requirements that are not appropriate for a production web server. Fortunately, another extension I recently learned of offers the renaming functionality; runkit. It was a super simple install for me.
From the command line, run “pecl install runkit”
Add “extension=runkit.so” and “runkit.internal_override=On” to the php.ini
Besides the ability to override these functions with wrappers, I also needed a way to make sure this file was always loaded before all other PHP files. The simple solution for that is adding “auto_prepend_file=/PATH/TO/FILE” to the “.user.ini” in the user’s root web directory.
The code for this script is as follows. It only contains a limited set of the MySQL functions, including some very esoteric ones that the web site used. This is not a foolproof script, but it gets the job done.
//Override the MySQL functionsforeach(Array('connect', 'error', 'fetch_array', 'fetch_row', 'insert_id', 'num_fields', 'num_rows','query', 'select_db', 'field_len', 'field_name', 'field_type', 'list_dbs', 'list_fields','list_tables', 'tablename') as$FuncName) runkit_function_redefine("mysql_$FuncName", '','return call_user_func_array("mysql_'.$FuncName.'_OVERRIDE", func_get_args());');//If a connection is not explicitely passed to a mysql_ function, use the last created connectionglobal$SQLLink; //The remembered SQL LinkfunctionGetConn($PassedConn){if(isset($PassedConn))return$PassedConn;global$SQLLink;return$SQLLink;}//Override functionsfunctionmysql_connect_OVERRIDE($Host, $Username, $Password) {global$SQLLink;return$SQLLink=mysqli_connect($Host, $Username, $Password);}functionmysql_error_OVERRIDE($SQLConn=NULL) {return mysqli_error(GetConn($SQLConn));}functionmysql_fetch_array_OVERRIDE($Result, $ResultType=MYSQL_BOTH) {returnmysqli_fetch_array($Result, $ResultType);}functionmysql_fetch_row_OVERRIDE($Result) {returnmysqli_fetch_row($Result);}functionmysql_insert_id_OVERRIDE($SQLConn=NULL) {return mysqli_insert_id(GetConn($SQLConn));}functionmysql_num_fields_OVERRIDE($Result) {return mysqli_num_fields($Result);}functionmysql_num_rows_OVERRIDE($Result) {return mysqli_num_rows($Result);}functionmysql_query_OVERRIDE($Query, $SQLConn=NULL) {returnmysqli_query(GetConn($SQLConn), $Query);}functionmysql_select_db_OVERRIDE($DBName, $SQLConn=NULL) {returnmysqli_select_db(GetConn($SQLConn), $DBName);}functionmysql_field_len_OVERRIDE($Result, $Offset) {$Fields=$Result->fetch_fields();return$Fields[$Offset]->length;}functionmysql_field_name_OVERRIDE($Result, $Offset) {$Fields=$Result->fetch_fields();return$Fields[$Offset]->name;}functionmysql_field_type_OVERRIDE($Result, $Offset) {$Fields=$Result->fetch_fields();return$Fields[$Offset]->type;}functionmysql_list_dbs_OVERRIDE($SQLConn=NULL) {$Result=mysql_query('SHOW DATABASES', GetConn($SQLConn));$Tables=Array();while($Row=mysqli_fetch_assoc($Result))$Tables[]=$Row['Database'];return$Tables;}functionmysql_list_fields_OVERRIDE($DBName, $TableName, $SQLConn=NULL) {$SQLConn=GetConn($SQLConn);$CurDB=mysql_fetch_array(mysql_query('SELECT Database()', $SQLConn));$CurDB=$CurDB[0];mysql_select_db($DBName, $SQLConn);$Result=mysql_query("SHOW COLUMNS FROM $TableName", $SQLConn);mysql_select_db($CurDB, $SQLConn);if(!$Result) {print'Could not run query: '.mysql_error($SQLConn);returnArray(); }$Fields=Array();while($Row=mysqli_fetch_assoc($Result))$Fields[]=$Row['Field'];return$Fields;}functionmysql_list_tables_OVERRIDE($DBName, $SQLConn=NULL) {$SQLConn=GetConn($SQLConn);$CurDB=mysql_fetch_array(mysql_query('SELECT Database()', $SQLConn));$CurDB=$CurDB[0];mysql_select_db($DBName, $SQLConn);$Result=mysql_query("SHOW TABLES", $SQLConn);mysql_select_db($CurDB, $SQLConn);if(!$Result) {print'Could not run query: '.mysql_error($SQLConn);returnArray(); }$Tables=Array();while($Row=mysql_fetch_row($Result))$Tables[]=$Row[0];return$Tables;}functionmysql_tablename_OVERRIDE($Result) {$Fields=$Result->fetch_fields();return$Fields[0]->table;}
And here is some test code to confirm functionality:
global$MyConn, $TEST_Table;$TEST_Server='localhost';$TEST_UserName='...';$TEST_Password='...';$TEST_DB='...';$TEST_Table='...';functionGetResult() {global$MyConn, $TEST_Table;returnmysql_query('SELECT*FROM'.$TEST_Table.' LIMIT 1', $MyConn);}var_dump($MyConn=mysql_connect($TEST_Server, $TEST_UserName, $TEST_Password));//Set $MyConn to NULL here if you want to test global $SQLLink functionalityvar_dump(mysql_select_db($TEST_DB, $MyConn));var_dump(mysql_query('SELECT*FROM INVALIDTABLE LIMIT1', $MyConn));var_dump(mysql_error($MyConn));var_dump($Result=GetResult());var_dump(mysql_fetch_array($Result));$Result=GetResult(); var_dump(mysql_fetch_row($Result));$Result=GetResult(); var_dump(mysql_num_fields($Result));var_dump(mysql_num_rows($Result));var_dump(mysql_field_len($Result, 0));var_dump(mysql_field_name($Result, 0));var_dump(mysql_field_type($Result, 0));var_dump(mysql_tablename($Result));var_dump(mysql_list_dbs($MyConn));var_dump(mysql_list_fields($TEST_DB, $TEST_Table, $MyConn));var_dump(mysql_list_tables($TEST_DB, $MyConn));mysql_query('CREATE TEMPORARY TABLE mysqltest (i int auto_increment, primary key (i))', $MyConn);mysql_query('INSERT INTO mysqltest VALUES ()', $MyConn);mysql_query('INSERT INTO mysqltest VALUES ()', $MyConn);var_dump(mysql_insert_id($MyConn));mysql_query('DROP TEMPORARY TABLE mysqltest', $MyConn);