Explorer
KNOW-PAT-071

Webshells de persistance - WordPress HMAC auth, Windows svcMonitor, SSH authorized_keys

Domaine
cybersecu
Type
pattern
Priorité
P2

Parent : [[INDEX-CYBERSECU]]

Webshells de persistance - WordPress HMAC auth, Windows svcMonitor, SSH authorized_keys

Problème

Après compromission initiale, trois mécanismes de persistance ont été déployés sur l'infrastructure YGG : un webshell PHP HMAC authentifié dans WordPress, un compte Windows de service factice, et une clé SSH root. Ces mécanismes ont permis un accès permanent même après changement des credentials initiaux.

Contexte archive Ygg

  • Source : https://yggleak.top/fr/home/ygg-dossier#phase-14-persistence
  • Phase 14 — Persistence
  • Serveurs compromis : X.X.X.X (Windows), X.X.X.X (Linux tracker), X.X.X.X (Ubuntu web)

Mécanismes de persistance identifiés

1. Windows svcMonitor (X.X.X.X)

Compte svcMonitor — un compte administrateur local nommé pour ressembler à un service de monitoring Windows.
Mot de passe fort, membre du groupe Administrators.
Un second chemin d'accès en cas de changement du mot de passe Administrator.

2. Webshell health.php (X.X.X.X)

Déployé dans le répertoire XAMPP, authentifié par paramètre GET.
Un POST avec la commande en paramètre retourne le résultat.
Sans le bon paramètre, le fichier retourne une page vide banale.
Le webshell tourne en tant qu'Administrator.

3. Clé SSH root (X.X.X.X)

Keypair ed25519 déployée dans /root/.ssh/authorized_keys.
PermitRootLogin passé de "no" à "prohibit-password", sshd rechargé.
Accès root permanent, brute-force toujours bloqué.

4. Webshell WordPress (X.X.X.X)

class-wp-cache-validator.php — nommé pour se fondre dans les fichiers core WordPress.
Timestamp aligné avec les fichiers voisins.
Authentification par HMAC-SHA256 :

CMD="id" CMD_B64=$(echo -n "$CMD" | base64)
SECRET=$(cat .wp_shell_secret)
HMAC=$(echo -n "$CMD_B64" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')

curl -sk https://rakaxa.net/wp-includes/class-wp-cache-validator.php \
  -H "X-Elementor-Nonce: $CMD_B64" \
  -H "X-Elementor-Cache: $HMAC"
  
# uid=1004(rakaxa) gid=1005(rakaxa) groups=1005(rakaxa)

Caractéristiques :

  • Sans les headers valides, retourne un 404 standard
  • Nom de fichier se fondant dans WordPress core
  • Timestamp aligné pour éviter la détection
  • Headers usurpant Elementor (plugin WordPress courant)

Solution protectif — Détection et prévention de persistance

1. Détection de webshells PHP

// detect-webshell.php — Scanner de webshells
function scanWebshells($directory) {
    $suspiciousPatterns = [
        // Exécution de commandes système
        '/\b(system|exec|passthru|shell_exec|proc_open|popen)\s*\(/i',
        // Fonctions dangereuses
        '/\b(eval|assert|base64_decode|str_rot13|gzinflate)\s*\(/i',
        // Headers suspects
        '/getallheaders\(\)|$_SERVER\[.HTTP_/i',
        // Fichiers avec des paramètres GET exécutant du code
        '/\$_GET\[.+\].*\b(eval|system|exec)\b/i',
        // Encodage base64 dans le code
        '/[\'"]\s*[a-zA-Z0-9+\/]{100,}\s*={0,2}\s*[\'"]/i',
        // Fonctions de fichier camouflées
        '/file_put_contents|fwrite.*fopen/i',
    ];
    
    $results = [];
    
    foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory)) as $file) {
        if ($file->getExtension() !== 'php') continue;
        
        $content = file_get_contents($file->getPathname());
        $score = 0;
        $matches = [];
        
        foreach ($suspiciousPatterns as $pattern) {
            if (preg_match_all($pattern, $content, $m)) {
                $score += count($m[0]) * 10;
                $matches = array_merge($matches, $m[0]);
            }
        }
        
        // Fichiers suspects : timestamps récents dans des répertoires anciens
        $mtime = $file->getMTime();
        $ctime = filectime($file->getPathname());
        
        if ($score >= 30 || ($mtime > time() - 86400 && $file->getPathname() !== __FILE__)) {
            $results[] = [
                'file' => $file->getPathname(),
                'score' => $score,
                'matches' => array_slice($matches, 0, 5),
                'mtime' => date('Y-m-d H:i:s', $mtime),
                'size' => $file->getSize()
            ];
        }
    }
    
    return $results;
}

// Exécuter le scan
$directory = '/var/www/html';
$suspicious = scanWebshells($directory);

foreach ($suspicious as $file) {
    echo "[SUSPICIOUS] {$file['file']} (score: {$file['score']})\n";
}

2. Monitoring des fichiers WordPress core

#!/bin/bash
# wordpress-integrity-check.sh

WP_ROOT="/var/www/html"
EXPECTED_HASHES="/var/lib/wp-hashes/known-hashes.txt"

# Générer les hashes actuels
find "$WP_ROOT/wp-includes" "$WP_ROOT/wp-admin" -name "*.php" -exec md5sum {} \; | sort > /tmp/current-hashes.txt

# Comparer avec les hashes connus (WordPress core)
if [ -f "$EXPECTED_HASHES" ]; then
    diff "$EXPECTED_HASHES" /tmp/current-hashes.txt > /tmp/hash-diff.txt
    
    if [ -s /tmp/hash-diff.txt ]; then
        echo "[ALERT] Fichiers WordPress modifiés ou ajoutés:"
        cat /tmp/hash-diff.txt
        
        # Envoyer une alerte
        mail -s "[SECURITY] WordPress integrity check failed" admin@example.com < /tmp/hash-diff.txt
    fi
fi

# Vérifier les nouveaux fichiers dans wp-includes (zone critique)
NEW_FILES=$(find "$WP_ROOT/wp-includes" -name "*.php" -mtime -1 | grep -v "$(basename $0)")
if [ -n "$NEW_FILES" ]; then
    echo "[ALERT] Nouveaux fichiers PHP dans wp-includes:"
    echo "$NEW_FILES"
fi

3. Détection de comptes de service suspects Windows

# detect-service-accounts.ps1
# Vérifier les comptes locaux suspects

$localUsers = Get-LocalUser | Where-Object { $_.Enabled -eq $true }

foreach ($user in $localUsers) {
    $userName = $user.Name
    
    # Vérifier si le nom ressemble à un service
    $servicePatterns = @(
        '^svc', '^service', '^monitor', '^backup', '^update',
        '^support', '^helpdesk', '^admin', '^sys', '^net'
    )
    
    $isSuspicious = $false
    foreach ($pattern in $servicePatterns) {
        if ($userName -match $pattern) {
            $isSuspicious = $true
            break
        }
    }
    
    # Vérifier les groupes
    $groups = (Get-LocalGroupMember -Group "Administrators" -ErrorAction SilentlyContinue | 
               Where-Object { $_.Name -like "*$userName" })
    
    if ($isSuspicious -and $groups) {
        Write-Warning "[ALERT] Compte suspect avec privilèges admin: $userName"
        Write-Warning "  Groupes: $($groups.Name -join ', ')"
        Write-Warning "  Créé le: $($user.PasswordLastSet)"
    }
}

# Vérifier les comptes créés récemment (moins de 30 jours)
$recentUsers = $localUsers | Where-Object { 
    $_.PasswordLastSet -and ($_.PasswordLastSet -gt (Get-Date).AddDays(-30))
}

foreach ($user in $recentUsers) {
    if ($user.Name -notmatch "^(Administrator|Guest|DefaultAccount|WDAGUtilityAccount)$") {
        Write-Warning "[ALERT] Compte récemment créé: $($user.Name) (créé: $($user.PasswordLastSet))"
    }
}

4. Monitoring des clés SSH authorized_keys

#!/bin/bash
# ssh-authorized-keys-monitor.sh

AUTHORIZED_KEYS_FILES=$(find /home -name "authorized_keys" -o -name "authorized_keys2" 2>/dev/null)
AUTHORIZED_KEYS_FILES="$AUTHORIZED_KEYS_FILES /root/.ssh/authorized_keys"

KNOWN_KEYS_FILE="/etc/security/known-ssh-keys.txt"

# Sauvegarder les clés connues
if [ ! -f "$KNOWN_KEYS_FILE" ]; then
    for file in $AUTHORIZED_KEYS_FILES; do
        if [ -f "$file" ]; then
            echo "=== $file ===" >> "$KNOWN_KEYS_FILE"
            cat "$file" >> "$KNOWN_KEYS_FILE"
        fi
    done
fi

# Vérifier les changements
for file in $AUTHORIZED_KEYS_FILES; do
    if [ -f "$file" ]; then
        CURRENT_HASH=$(md5sum "$file" | awk '{print $1}')
        EXPECTED_HASH=$(grep -A1 "=== $file ===" "$KNOWN_KEYS_FILE" 2>/dev/null | tail -1 | md5sum | awk '{print $1}')
        
        if [ "$CURRENT_HASH" != "$EXPECTED_HASH" ]; then
            echo "[ALERT] Clés SSH modifiées dans: $file"
            echo "  Clés actuelles:"
            cat "$file" | while read line; do
                KEY_TYPE=$(echo "$line" | awk '{print $1}')
                KEY_COMMENT=$(echo "$line" | awk '{print $3}')
                echo "    - Type: $KEY_TYPE, Comment: $KEY_COMMENT"
            done
        fi
    fi
done

5. Détection de PermitRootLogin modifié

#!/bin/bash
# ssh-config-monitor.sh

SSH_CONFIG="/etc/ssh/sshd_config"
KNOWN_CONFIG="/etc/security/known-sshd-config.txt"

# Sauvegarder la config connue
if [ ! -f "$KNOWN_CONFIG" ]; then
    cp "$SSH_CONFIG" "$KNOWN_CONFIG"
fi

# Vérifier les changements critiques
if ! diff "$KNOWN_CONFIG" "$SSH_CONFIG" > /dev/null 2>&1; then
    echo "[ALERT] Configuration SSH modifiée!"
    
    # Vérifier les changements suspects
    CURRENT_ROOT=$(grep "^PermitRootLogin" "$SSH_CONFIG" | awk '{print $2}')
    KNOWN_ROOT=$(grep "^PermitRootLogin" "$KNOWN_CONFIG" | awk '{print $2}')
    
    if [ "$CURRENT_ROOT" != "$KNOWN_ROOT" ]; then
        echo "[CRITICAL] PermitRootLogin changé de '$KNOWN_ROOT' à '$CURRENT_ROOT'"
    fi
    
    CURRENT_AUTH=$(grep "^PasswordAuthentication" "$SSH_CONFIG" | awk '{print $2}')
    KNOWN_AUTH=$(grep "^PasswordAuthentication" "$KNOWN_CONFIG" | awk '{print $2}')
    
    if [ "$CURRENT_AUTH" != "$KNOWN_AUTH" ]; then
        echo "[CRITICAL] PasswordAuthentication changé de '$KNOWN_AUTH' à '$CURRENT_AUTH'"
    fi
fi

Checklist de validation

  • Scan régulier des webshells dans les répertoires web
  • Monitoring des fichiers WordPress core (hash integrity)
  • Pas de nouveaux fichiers PHP dans wp-includes/
  • Comptes de service audités (noms ressemblant à svc*, monitor*, backup*)
  • Pas de comptes locaux admin créés récemment
  • Clés SSH authorized_keys monitorées (changement = alerte)
  • PermitRootLogin = no (pas prohibit-password)
  • PasswordAuthentication = no (clés uniquement)
  • Audit des connexions SSH (IP, heure, clé utilisée)
  • Fichiers .wp_shell_secret ou secrets webshell recherchés
  • Timestamps des fichiers PHP comparés avec les fichiers voisins
  • Headers HTTP suspects monitorés (X-Elementor-Nonce, etc.)

Anti-pattern associé

  • KNOW-ANT-015 — Unmonitored persistence mechanisms (webshells, service accounts, SSH keys)

Références

  • Archive Ygg : Phase 14 — Persistence (YGGLeak)
  • OWASP : Web Shell Detection
  • MITRE ATT&CK : T1505 (Server Software Component)