File checksums: proving nothing changed, and finding what did
Published · updated
After a break-in — or as a precaution before one — the question is always the same: which files are not what they were? A checksum baseline answers it in minutes, and it costs nothing to keep.
Why a checksum, not a timestamp
A modification time is a claim the file system makes and an attacker can reset with touch. A SHA-256 hash is derived from the bytes: if one character changes, the hash changes and there is no way to make the old value come back without putting the old bytes back.
That is the whole trick behind integrity checks in security scanners. It is also why a hash is only as good as the reference: hashing a compromised file and storing the result on the same compromised server proves that the file did not change afterwards — nothing more. The baseline has to live somewhere the attacker cannot reach.
Build a baseline
# one hash per file, relative paths, sorted — the classic format cd /var/www/site find . -type f -not -path './cache/*' -not -path './uploads/*' -print0 \ | sort -z | xargs -0 sha256sum > /root/site.sha256 # count and date it wc -l /root/site.sha256 date -u +%FT%TZ >> /root/site.sha256
Exclude what legitimately churns: cache directories, session files, uploads, logs. Include everything else — including .htaccess, index.php, and the first two levels of any plugin or module directory, because that is where injected code lives.
Then copy the manifest somewhere else: your laptop, a private bucket, a git repository. A baseline on the same disk as the files it protects is a baseline that disappears with the server.
Compare later, and read the output
cd /var/www/site sha256sum -c /root/site.sha256 2>&1 | grep -v ': OK$'
The output has three shapes and each means something different:
FAILED— the bytes changed since the baseline. Expected after a deploy or an update; suspicious otherwise.No such file or directory— deleted, or added to the exclusions by mistake.- Nothing at all — but silence is the dangerous case. A new file that the baseline never saw produces no line, because
sha256sum -conly checks what is listed. New files are what webshells are.
So the second half of the check lists files that were not in the manifest:
# files present now but absent from the baseline
cd /var/www/site
find . -type f -not -path './cache/*' -not -path './uploads/*' | sort > /tmp/now.txt
awk '{ $1=""; sub(/^ /,""); print }' /root/site.sha256 | sort > /tmp/base.txt
comm -23 /tmp/now.txt /tmp/base.txt
Three other sources of truth
- The package manager.
rpm -Vaon RPM systems anddebsums -con Debian list files whose content no longer matches the package. This is how you find a modified system binary without keeping your own baseline. - Git, for anything you deploy.
git statuson the production checkout is an integrity check with a diff attached:git diffshows the injected line. Sites deployed by rsync or FTP lose this, which is one good reason to move to a repository-driven deploy. - Modification times as a filter, not as proof.
find /var/www -type f -newermt '2026-10-16' -lsnarrows the list to files touched recently; the hash decides whether the change was legitimate.
What an outside check adds
Checksums need the server. From outside, the useful question is different: are you publishing the files that make an attack easy? An exposed .git/ directory hands over the whole source including history; a site archive or database dump in the web root hands over the code and the passwords. Our checker looks for exactly those, because a downloaded backup is the cheapest way into a site — no exploit required.
If you found one exposed, treat it as a disclosure: remove the file, then assume the credentials inside are known and rotate them. Removing a dump after it has been indexed by a crawler is clean-up, not containment.
Questions
Which hash should I use?
SHA-256 is the default answer: fast enough on any modern CPU, no known collisions. MD5 and SHA-1 are fine for detecting accidental corruption and not for anything an attacker might influence.
How often should I re-baseline?
After every deliberate deploy, and never by simply overwriting the old manifest: keep the dated file. Then a change between two baselines is explainable, and an unexplained one stands out.
Is checking checksums useful if the attacker already has root?
Once someone has root, nothing on the host is trustworthy — including the hashing tools. That is why the baseline is stored off-host and the comparison can be run from a rescue system or a fresh copy.
Do I need a commercial file-integrity product?
Not to start. A manifest plus a cron job covers the common case. Commercial tools add real-time watching, central reporting and tamper-resistant storage — worth it when compliance asks, not before.