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_keysmonitorées (changement = alerte) -
PermitRootLogin=no(pasprohibit-password) -
PasswordAuthentication=no(clés uniquement) - Audit des connexions SSH (IP, heure, clé utilisée)
- Fichiers
.wp_shell_secretou 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)