KBeezie

There's no place like ::1

Menu
  • Home
  • Start Here
  • Security Series
  • About

Detecting Compromised PHP Sites with a Squid Proxy

2026/05/13 in Security

Step 5: Add a threat intelligence blocklist

Squid can block connections to known C2 domains before they ever leave your server. Create /etc/squid/malicious_domains.txt with one domain per line in Squid ACL format (dot-prefixed for wildcard matching):

.c2-server.example.com
.exfil-collector.net
.botnet-mothership.org

An automated update script fetches fresh lists and reloads Squid. Save as /etc/squid/update_blacklist.sh:

#!/bin/bash
URL="https://raw.githubusercontent.com/example/threat-feed/main/domains.txt"
TMP_FILE="/tmp/tif_raw.txt"
TARGET_FILE="/etc/squid/malicious_domains.txt"
PROXY="http://127.0.0.1:3128"

echo "Downloading latest malicious domain feeds via local Squid proxy..."
curl -x "$PROXY" -s -S "$URL" -o "$TMP_FILE"

if [ -s "$TMP_FILE" ]; then
    echo "Processing format maps for Squid ACL engine..."
    grep -v '^#' "$TMP_FILE" | grep -v '^$' | sed 's/^\./ /g' | awk '{print "."$1}' > "$TARGET_FILE"

    echo "Enforcing Squid verification checks..."
    squid -k parse

    if [ $? -eq 0 ]; then
        echo "Reloading configuration layers..."
        squid -k reconfigure
        echo "Malicious domain database update successful."
    else
        echo "Warning: Format check failed. Reverting changes."
    fi
else
    echo "Network connection failure. Upstream list could not be fetched via proxy."
fi

rm -f "$TMP_FILE"
sudo chmod +x /etc/squid/update_blacklist.sh

Replace the URL with an actual threat feed. Squid's dstdomain ACL with dot-prefixed entries (.example.com) matches the domain and all subdomains — evil.example.com, www.evil.example.com, and so on.


Step 6: The audit script

Save as /usr/local/sbin/squid_list.py:

#!/usr/bin/env python3
import re
import sys
from datetime import datetime

SQUID_LOG = "/var/log/squid/access.log"
PHP_LOG = "/var/log/php8.5-fpm.access.log"

# Domains your infrastructure legitimately connects to.
# These are filtered out so the output only shows connections
# worth investigating — unexpected domains, C2 callbacks, exfiltration.
TRUSTED_DOMAINS = [
    "api.wordpress.org", "wordpress.org",
    "githubusercontent.com", "perfops.one",
]

def parse_squid():
    """Extract timestamp and destination domain from Squid CONNECT logs."""
    squid_events = []
    squid_regex = re.compile(
        r"^(\d+\.\d+)\s+\d+\s+127\.0\.0\.1\s+\S+\s+\d+\s+CONNECT\s+([^:]+)"
    )
    try:
        with open(SQUID_LOG, "r") as f:
            for line in f:
                match = squid_regex.match(line)
                if match:
                    unix_ts = float(match.group(1))
                    dt = datetime.fromtimestamp(unix_ts).replace(microsecond=0)
                    squid_events.append({"time": dt, "domain": match.group(2)})
    except FileNotFoundError:
        pass
    return squid_events


def parse_php():
    """Extract timestamp, script path, and site host from PHP-FPM access log."""
    php_events = []
    # Log format: [%t] %m %{REQUEST_METHOD}e %f %{HTTP_HOST}e
    php_regex = re.compile(
        r"^\[([^\]]+)\]\s+\S+\s+\S+\s+(\S+)\s+(\S+)"
    )
    try:
        with open(PHP_LOG, "r") as f:
            for line in f:
                match = php_regex.match(line)
                if match:
                    time_str = match.group(1).split(" ")[0]
                    dt = datetime.strptime(time_str, "%d/%b/%Y:%H:%M:%S")
                    script_path = match.group(2)
                    vhost = match.group(3)
                    php_events.append({
                        "time": dt,
                        "script": script_path,
                        "vhost": vhost,
                    })
    except FileNotFoundError:
        pass
    return php_events


def extract_readable_path(full_path):
    """Strip the document root to show a clean site/script pair."""
    match = re.search(r"/www/([^/]+)/public_html/(.+\.php)$", full_path)
    if match:
        return match.group(1), match.group(2)
    return None, full_path[-40:]


def cross_reference(filter_vhost=None):
    """Correlate Squid outbound connections with the PHP script that made them."""
    squid = parse_squid()
    php = parse_php()

    if not squid or not php:
        return "No log data available to correlate."

    lines = []
    for p_ev in php:
        for s_ev in squid:
            # Skip trusted infrastructure domains — these are expected
            if any(trusted in s_ev["domain"] for trusted in TRUSTED_DOMAINS):
                continue

            # Match events within 2 seconds of each other
            if abs((p_ev["time"] - s_ev["time"]).total_seconds()) <= 2:
                vhost = p_ev["vhost"] if p_ev["vhost"] else "UNKNOWN"
                _, script = extract_readable_path(p_ev["script"])

                if filter_vhost and filter_vhost not in vhost:
                    continue

                time_str = p_ev["time"].strftime("%m-%d %H:%M:%S")
                lines.append(
                    f"{time_str} | {vhost} -> {script} | {s_ev['domain']}"
                )

    return "\n".join(lines[-20:]) if lines else "No suspicious outbound connections detected."


if __name__ == "__main__":
    search_arg = sys.argv[1] if len(sys.argv) > 1 else None
    print(cross_reference(search_arg))
sudo chmod +x /usr/local/sbin/squid_list.py

Run it to see the last 20 unexpected outbound connections:

sudo /usr/local/sbin/squid_list.py

Filter by a specific site:

sudo /usr/local/sbin/squid_list.py mysite.com

Sample output:

05-13 10:30:45 | mysite.com -> wp-content/plugins/analytics-plus/collector.php | tracking.example.com
05-13 10:31:02 | mysite.com -> wp-admin/admin-ajax.php | cdn.malicious.net
05-13 10:31:15 | anothersite.com -> wp-cron.php | api.phone-home.io

Each line tells you when it happened, which site, which script initiated the connection, and where it went. A domain that doesn't show up in your trusted list is worth investigating. A domain that appears in the threat-intelligence blocklist was already dropped at the Squid level before the connection left the machine — but the log entry is still there for you to review during your next audit pass.

  • ← Previous
  • 1
  • 2
  • 3
  • 4
  • Next →
Tags: squid, php-fpm, python, security, monitoring, outbound, compromise
©2026 KBeezie | Disclaimer | Privacy Notice