Skip to main content

Detect Hacked Files via CRON/PHP

By DK Lynn



Free JavaScript Book!

Write powerful, clean and maintainable JavaScript.

RRP $11.95

As a Certified Ethical Hacker, I searched for a script which would help me to detect unauthorized file changes. I found a script (probably in the User Contributed Notes at which I modified to have working very nicely on my “test server” (Windows) as well as on my “production” server (Linux).

The logic is simple: “Build a database of hashed values for vulnerable files (those which hackers will modify to execute code on your server) and compare those values to the actual hashes on a regular basis and report added, changed and deleted files.”

Obviously, the code to traverse a server’s directory structure and provide hash values is far more complex than the statement above. I will go through the code for the production server.

Database Setup

For security, use a separate database for this which does not share access credentials with any other database. Use cPanel to create the new database and the new user with a strong password (I recommend a 16 character password generated by and an innocuous name like baseline. Then use PHPMyAdmin’s SQL to create two tables:

    CREATE TABLE baseline (
        file_path VARCHAR(200) NOT NULL,
        file_hash CHAR(40) NOT NULL,
        acct VARCHAR(40) NOT NULL
        PRIMARY KEY (file_path)
    CREATE TABLE tested (
        tested DATETIME NOT NULL,
        account VARCHAR(40) NOT NULL
        PRIMARY KEY (tested)

The first table, “baseline,” contains a large field for your path/to/filenames, a fixed field for the file_hash (40 characters are required for SHA1) and acct to allow me to monitor accounts or domains separately. Set the file_path as the Primary Key.

The “tested” table will hold the DATETIME of every scan and the account is the same as baseline’s acct field so it will allow you to scan various accounts or domains and keep their data separate.

Initialize the PHP File:

First, DEFINE several constants

  • PATH is the physical path to the start of your scan, which is usually the DocumentRoot. Just remember not to use Windows’ backslashes because both Apache and PHP will be looking for forward slashes.
  • Database access constants SERVER ('localhost'), USER, PASSWORD and DATABASE.

and several variables

  • An array of the file extensions to examine. Because not all files are executable on the server, I only scan .php, .htm, .html and .js files and these need to be specified in an array. Note than an empty array will force ALL files to be scanned (best for security but uses the most server resources).
  • Directories to exclude. Iif you have a directory containing malware, shame on you! In any event, if you need to exclude a directory for any reason, you have the opportunity to list them in an array. Don’t omit any directories just because you only stored images or pdf files, though, there because a hacker can put his files in there, too!
  • Initialize the variables you’re about to use: The $file array as an empty array(), the $report string as an empty string and the $acct string (use the account/acct name from your database tables) need to be initialized.


Let’s get started!


//          initialize

$dir = new RecursiveDirectoryIterator(PATH);

$iter = new RecursiveIteratorIterator($dir);

while ($iter->valid())


    //          skip unwanted directories

    if (!$iter->isDot() && !in_array($iter->getSubPath(), $skip))


        //          get specific file extensions
        if (!empty($ext))


            //          PHP 5.3.4: if (in_array($iter->getExtension(), $ext))

            if (in_array(pathinfo($iter->key(), PATHINFO_EXTENSION), $ext))

                $files[$iter->key()] = hash_file("sha1", $iter->key());


        } else {

            //          ignore file extensions

            $files[$iter->key()] = hash_file("sha1", $iter->key());





What we’ve just done is use the RecursiveIteratorIterator() function (a function used to iterate through recursive iterators)  on the directory ($dir) as it iterates through the directory structure. The first thing it does is check whether a directory has been banned from the iteration then branch depending upon whether file extensions had been specified. The result is a two-dimensional matrix of files, ($files), with path/name.ext as the index and corresponding SHA1 hash value.

I’ll note here that the commented echo statements were used on my Windows test server without linking to the SMTP server but you’ll need to uncomment them if you need to verify the correct functionality.

The file count can be provided immediately by the files array:

$report .= "Files has " . count($files) . " records.rn";

The output, whether to your test monitor or email, has just been given its first non-empty value: the hashed file count.


Last Hash Scan

The next thing to do is fetch the data/time the last hash scan was accomplished and get the stored path/file and hash set from the database.

$results = mysqli_query($db,"SELECT tested FROM tested WHERE acct = '$acct'
    ORDER BY tested DESC LIMIT 1");

if ($results)




        $tested = $result['tested'];


$report .= "Last tested $tested.rn";



Compare Hashed Files with Database Records

So far, we’ve only learned the current file count and datetime of the last scan. The value we’re looking for is to identify the changed files, i.e., those added, changed or deleted. Let’s create an array of the differences.

//          identify differences

if (!empty($files))


    $result = mysqli_query($db,"SELECT * FROM baseline");

    if (!empty($result))


        foreach ($result as $value)


            $baseline[$value["file_path"]] = $value["file_hash"];


            $diffs = array_diff_assoc($files, $baseline);





//          sort differences into Deleted, Altered and Added arrays

if (!empty($files))


    $results = mysqli_query($db,"SELECT file_path, file_hash FROM baseline WHERE acct = '$acct'");

    if (!empty($results))


        $baseline = array();      //          from database

        $diffs = array();         //          differences between $files and $baseline

                                  //          $files is current array of file_path => file_hash

        while ($value = mysqli_fetch_array($results))


            if (!array_key_exists($value["file_path"], $files))


                //          Deleted files

                $diffs["Deleted"][$value["file_path"]] = $value["file_path"];

                $baseline[$value["file_path"]] = $value["file_hash"];

            } else {

                    //          Altered files

                    if ($files[$value["file_path"]] <> $value["file_hash"])


                        $diffs["Altered"][$value["file_path"]] = $value["file_path"];

                        $baseline[$value["file_path"]] = $value["file_path"];

                    } else {

                            //          Unchanged files

                            $baseline[$value["file_path"]] = $value["file_hash"];




        if (count($baseline) < count($files))


            //          Added files

            $diffs["Added"] = array_diff_assoc($files, $baseline);





When completed, the $diffs array will either be empty or it will contain any discrepancies found in the multi-dimensional array sorted by Deleted, Altered and Added along with the path/file and associated hash pairs for each.


Email Results

You will need to add the discrepancies to the report and email.


//          display discrepancies

if (!empty($diffs)) {

$report .= "The following discrepancies were found:rnrn";

foreach ($diffs as $status => $affected)


    if (is_array($affected) && !empty($affected))


        ($test) ? echo "<li>" . $status . "</li>" : $report .= "* $status *rnrn";

        ($test) ? echo "<ol>" : '';
        foreach($affected as $path => $hash) $report .= " • $pathrn";



} else {

    $report .= "File structure is intact.rn";



$mailed = mail('', $acct . ' Integrity Monitor Report',$report);


Update the Database

You’re not finished yet!

//          update database

//          clear old records

mysqli_query($db,"DELETE FROM baseline WHERE acct = '$acct'");


//          insert updated records

foreach ($files as $path => $hash)


    mysqli_query($db,"INSERT INTO baseline (file_path, file_hash, acct)
        VALUES ('$path','$hash', '$acct')");



mysqli_query($db,"INSERT INTO tested (tested, acct) VALUES (NOW(), '$acct')");




On the first pass, there will be nothing in the database’s baseline table and ALL files will display as Added so don’t be alarmed.

Now that you have the code, where do you upload it? Don’t even consider placing this code in your webspace (under the DocumentRoot) as that will mean that anyone can access your file and delete the saved information to invalidate your hash scans. For simplicity, put it in the same directory of your account which holds public_html (or similar) directory.



Now that you have the code, you need to have it activated on a regular basis. That’s where the CRON function of the server excels! Simply use your cPanel to create a new CRON job, set the time in the middle of the night when your server should be nearly idle (you don’t want to interfere with or delay visitors’ activities, which also means you should limit yourself to a single scan per day) and use the following directive:

/usr/local/bin/php -q /home/account/hashscan.php

where /usr/local/bin/php is the location of the server’s PHP executable and /home/account/hashscan.php is the path to your hashscan.php script (or whatever name you gave it).



We have created a new database with two tables, one to hold the dates and one to hold the baseline hashes. We have initiated every scan by identifying the file types (by extension) that we need to track and identified the start point (DocumentRoot) for our scan.

We’ve scanned the files avoiding the unwanted directories and compared the hashes against the baseline in the database. Closing the process, we’ve updated the database tables and either displayed (on a test server) or emailed (from the production server) the results. Our CRON job will then activate your hash scan on a regular basis.

This ZIP file contains the above CreateTable.sql, hashscan.php and CRON.txt files.

This is but one part of securing your website, though, as it will only inform you of changes to the types of files you’ve specified. Before you get this far, you must ensure that your files are malware free (maldet scans established by your host can do this but be sure that you keep a clean master copy off-line), ensure that no one but you can upload via FTP (by using VERY strong passwords) and keep “canned apps” up to date (because their patches are closing vulnerabilities found and exploited by hackers and their legions of “script kiddies”).

In summary, BE PARANOID! There may be no-one out to get you but there are those out for “kicks” who are looking for easy prey. Your objective is to avoid that classification.

DK Lynn is a former instructor pilot and "rocket scientist" now living in New Zealand where he operates a small business developing and hosting web sites.

New books out now!

Get practical advice to start your career in programming!

Master complex transitions, transformations and animations in CSS!