/** * Patch virtuali: regole del feed applicate solo se il plugin/tema è installato in una versione vulnerabile. * Ogni regola descrive un controllo GENERICO su un parametro (niente firme di exploit): * {"id":"TWP-0001","titolo":"...","tipo":"plugin","slug":"nome-plugin","versione_da":"1.0","versione_a":"2.3.4", * "metodo":"POST","path":"/wp-admin/admin-ajax.php","azione_ajax":"nome_azione","param":"id", * "controllo":"solo_numeri" | "solo_alfanumerico" | "lunghezza_max" | "vietato" | "solo_valori", "valore":"...", "punti":10} * $installati = ['slug' => 'versione', ...] (plugin e temi WordPress rilevati) */ function tw_patch_colpite(string $path, array $installati): array { $feed = tw_feed(); $colpi = []; foreach (($feed['regole'] ?? []) as $r) { $slug = (string)($r['slug'] ?? ''); if ($slug !== '') { if (!isset($installati[$slug])) continue; $v = (string)$installati[$slug]; if (!empty($r['versione_da']) && version_compare($v, (string)$r['versione_da'], '<')) continue; if (!empty($r['versione_a']) && version_compare($v, (string)$r['versione_a'], '>')) continue; } if (!empty($r['metodo']) && strtoupper((string)$r['metodo']) !== ($_SERVER['REQUEST_METHOD'] ?? 'GET')) continue; $p = (string)($r['path'] ?? '*'); if ($p !== '*' && $p !== '' && strpos($path, $p) === false) continue; if (!empty($r['azione_ajax']) && (string)($_REQUEST['action'] ?? '') !== (string)$r['azione_ajax']) continue; $param = (string)($r['param'] ?? ''); if ($param === '' || !isset($_REQUEST[$param])) continue; $val = $_REQUEST[$param]; $val = is_scalar($val) ? (string)$val : json_encode($val); $viola = false; switch ((string)($r['controllo'] ?? '')) { case 'solo_numeri': $viola = !preg_match('/^\d*$/', $val); break; case 'solo_alfanumerico': $viola = !preg_match('/^[A-Za-z0-9_\-]*$/', $val); break; case 'lunghezza_max': $viola = strlen($val) > (int)($r['valore'] ?? 100); break; case 'vietato': $viola = true; break; case 'solo_valori': $viola = !in_array($val, array_map('strval', (array)($r['valore'] ?? [])), true); break; } if ($viola) { $colpi[] = ['id' => (string)($r['id'] ?? 'PATCH'), 'cat' => 'patch', 'd' => (string)($r['titolo'] ?? 'falla nota di un plugin'), 'dove' => 'patch:' . ($slug ?: 'sito') . ':' . $param, 'campione' => tw_tronca($val, 120), 'p' => (int)($r['punti'] ?? 10)]; } } return $colpi; } /* ============================================================ RETE CONDIVISA IP */ function tw_rete_aggiorna(): array { if (!tw_on('rete')) return ['ok' => false, 'msg' => 'Rete condivisa disattivata.']; $url = (string)tw_conf('rete_url', ''); if ($url === '') return ['ok' => false, 'msg' => 'URL rete non impostato.']; $raw = tw_http_get($url, 20); if (!$raw) return ['ok' => false, 'msg' => 'Lista IP non raggiungibile.']; $dati = json_decode($raw, true); $ips = is_array($dati) ? ($dati['ip'] ?? $dati) : []; if (!is_array($ips)) return ['ok' => false, 'msg' => 'Lista IP non valida.']; $puliti = []; foreach ($ips as $x) { $ip = is_array($x) ? ($x['ip'] ?? '') : $x; if (tw_valida_ip_o_cidr((string)$ip)) $puliti[] = (string)$ip; } tw_store()->kv_set('rete_ip', $puliti); tw_store()->kv_set('rete_quando', time()); return ['ok' => true, 'msg' => 'Rete condivisa aggiornata: ' . count($puliti) . ' IP.', 'n' => count($puliti)]; } function tw_rete_ip(): array { $v = tw_store()->kv_get('rete_ip'); return is_array($v) ? $v : []; } /** Invia al VPS gli IP che abbiamo bannato (facoltativo). */ function tw_rete_invia(): array { if (!tw_conf('rete_invia')) return ['ok' => false, 'msg' => 'Invio disattivato.']; $url = (string)tw_conf('rete_invia_url', ''); $da = tw_store()->kv_prefisso('rete_da_inviare:'); if (!$da) return ['ok' => true, 'msg' => 'Niente da inviare.', 'n' => 0]; $lista = []; foreach ($da as $k => $motivo) $lista[] = ['ip' => substr($k, strlen('rete_da_inviare:')), 'motivo' => $motivo]; $corpo = json_encode(['sito' => tw_conf('sito_nome'), 'ip' => $lista], JSON_UNESCAPED_UNICODE); $r = tw_http_post($url, $corpo); foreach ($da as $k => $v) tw_store()->kv_del($k); return ['ok' => (bool)$r, 'msg' => $r ? ('Inviati ' . count($lista) . ' IP alla rete CGFA.') : 'Invio non riuscito.', 'n' => count($lista)]; } function tw_http_post(string $url, string $corpo): ?string { $h = ['Content-Type: application/json']; if (function_exists('curl_init')) { $ch = curl_init($url); curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15, CURLOPT_POST => true, CURLOPT_POSTFIELDS => $corpo, CURLOPT_HTTPHEADER => $h, CURLOPT_USERAGENT => 'TheWall-CGFA/1.0']); $r = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); return ($r !== false && $code < 400) ? $r : null; } $ctx = stream_context_create(['http' => ['method' => 'POST', 'header' => implode("\r\n", $h), 'content' => $corpo, 'timeout' => 15]]); $r = @file_get_contents($url, false, $ctx); return $r === false ? null : $r; } /* ============================================================ INTEGRITÀ FILE */ function tw_integrita_radice(): string { $r = (string)tw_conf('integrita_radice', ''); if ($r !== '' && is_dir($r)) return rtrim(str_replace('\\', '/', $r), '/'); $doc = $_SERVER['DOCUMENT_ROOT'] ?? dirname(TW_DIR); return rtrim(str_replace('\\', '/', $doc), '/'); } function tw_integrita_scansiona(bool $salva = false): array { $radice = tw_integrita_radice(); $vecchie = (array)tw_store()->kv_get('integrita'); $nuove = []; $modificati = []; $nuovi = []; $spariti = []; $n = 0; $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($radice, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::LEAVES_ONLY); foreach ($it as $f) { if ($n > 20000) break; $path = str_replace('\\', '/', $f->getPathname()); if (strpos($path, '/data/') !== false && strpos($path, TW_DIR) !== false) continue; if (!preg_match('/\.(php|phtml|php5|php7|inc|js)$/i', $path)) continue; if ($f->getSize() > 8000000) continue; $rel = ltrim(substr($path, strlen($radice)), '/'); $h = @hash_file('sha256', $path); if ($h === false) continue; $nuove[$rel] = $h; $n++; if (!empty($vecchie)) { if (!isset($vecchie[$rel])) $nuovi[] = $rel; elseif ($vecchie[$rel] !== $h) $modificati[] = $rel; } } if (!empty($vecchie)) { foreach ($vecchie as $rel => $h) if (!isset($nuove[$rel])) $spariti[] = $rel; } if ($salva || empty($vecchie)) { tw_store()->kv_set('integrita', $nuove); tw_store()->kv_set('integrita_quando', time()); } return ['totale' => $n, 'modificati' => $modificati, 'nuovi' => $nuovi, 'spariti' => $spariti, 'prima_volta' => empty($vecchie)]; } /* ============================================================ SCANNER MALWARE */ function tw_malware_firme(): array { return [ 'webshell-eval' => '/\b(eval|assert)\s*\(\s*(base64_decode|gzinflate|gzuncompress|str_rot13|rawurldecode)\s*\(/i', 'post-eval' => '/\b(eval|assert|system|passthru|shell_exec)\s*\(\s*\$_(POST|GET|REQUEST|COOKIE)\b/i', 'preg-e' => '/preg_replace\s*\(\s*[\'"][^\'"]*\/e[\'"]/i', 'create-func' => '/create_function\s*\(/i', 'offuscato' => '/(\\\\x[0-9a-f]{2}){8,}|chr\(\d+\)\s*\.\s*chr\(\d+\)\s*\.\s*chr\(\d+\)\s*\.\s*chr\(/i', 'callback' => '/\b(call_user_func(_array)?|array_map|array_filter|usort)\s*\(\s*[\'"]?(assert|system|eval|exec|passthru|shell_exec)/i', 'uploader' => '/move_uploaded_file\s*\([^)]*\$_(FILES|POST|GET)/i', 'base64-lungo' => '/[\'"][A-Za-z0-9+\/]{260,}={0,2}[\'"]/', 'global-var-func' => '/\$\{?\$?_(GET|POST|REQUEST)\}?\[[^\]]+\]\s*\(/i', 'c99-wso' => '/(c99|wso|r57|b374k|FilesMan|\bphpshell\b|WSO\s|Shell by)/i', ]; } function tw_malware_scansiona(): array { $radice = tw_integrita_radice(); $trovati = []; $n = 0; $firme = tw_malware_firme(); $feed_firme = tw_feed()['malware'] ?? []; $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($radice, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::LEAVES_ONLY); foreach ($it as $f) { if ($n > 15000) break; $path = str_replace('\\', '/', $f->getPathname()); if (!preg_match('/\.(php|phtml|php5|php7|inc)$/i', $path)) continue; if (strpos($path, TW_DIR) === 0) continue; if ($f->getSize() > 3000000) continue; $n++; $c = (string)@file_get_contents($path); if ($c === '') continue; $rel = ltrim(substr($path, strlen($radice)), '/'); foreach ($firme as $nome => $re) { if (@preg_match($re, $c)) { $trovati[] = ['file' => $rel, 'firma' => $nome]; break; } } foreach ($feed_firme as $ff) { if (!empty($ff['re']) && @preg_match($ff['re'], $c)) { $trovati[] = ['file' => $rel, 'firma' => $ff['id'] ?? 'feed']; break; } } } tw_store()->kv_set('malware', $trovati); tw_store()->kv_set('malware_quando', time()); return ['totale' => $n, 'trovati' => $trovati]; } /* ============================================================ 2FA TOTP (RFC 6238) */ function tw_base32_decode(string $b32): string { $alf = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; $b32 = strtoupper(preg_replace('/[^A-Z2-7]/', '', $b32)); $bin = ''; $buf = 0; $bits = 0; for ($i = 0, $l = strlen($b32); $i < $l; $i++) { $buf = ($buf << 5) | strpos($alf, $b32[$i]); $bits += 5; if ($bits >= 8) { $bits -= 8; $bin .= chr(($buf >> $bits) & 0xFF); } } return $bin; } function tw_base32_encode(string $bin): string { $alf = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; $out = ''; $buf = 0; $bits = 0; for ($i = 0, $l = strlen($bin); $i < $l; $i++) { $buf = ($buf << 8) | ord($bin[$i]); $bits += 8; while ($bits >= 5) { $bits -= 5; $out .= $alf[($buf >> $bits) & 0x1F]; } } if ($bits > 0) $out .= $alf[($buf << (5 - $bits)) & 0x1F]; return $out; } function tw_totp_segreto(): string { return tw_base32_encode(random_bytes(20)); } function tw_totp_codice(string $segreto, ?int $t = null): string { $t = $t ?? time(); $key = tw_base32_decode($segreto); $c = intdiv($t, 30); $bin = pack('N*', 0) . pack('N*', $c); $hash = hash_hmac('sha1', $bin, $key, true); $off = ord($hash[19]) & 0xF; $num = ((ord($hash[$off]) & 0x7F) << 24) | ((ord($hash[$off + 1]) & 0xFF) << 16) | ((ord($hash[$off + 2]) & 0xFF) << 8) | (ord($hash[$off + 3]) & 0xFF); return str_pad((string)($num % 1000000), 6, '0', STR_PAD_LEFT); } function tw_totp_verifica(string $segreto, string $codice): bool { $codice = preg_replace('/\D/', '', $codice); if (strlen($codice) !== 6) return false; for ($d = -1; $d <= 1; $d++) { if (hash_equals(tw_totp_codice($segreto, time() + $d * 30), $codice)) return true; } return false; } function tw_totp_uri(string $segreto, string $utente, string $emittente): string { return 'otpauth://totp/' . rawurlencode($emittente . ':' . $utente) . '?secret=' . $segreto . '&issuer=' . rawurlencode($emittente) . '&period=30&digits=6'; } /** * QR in SVG puro per il 2FA. * NOTA: il generatore in lib/qr.php (finder/timing/formato corretti) ha ancora un residuo * nella posa dei dati e non è garantito leggibile da tutti i lettori; per questo il pannello * usa come metodo principale la CHIAVE manuale + il link otpauth://. Quando qr.php sarà * completato basterà togliere il "return ''" qui sotto. */ function tw_qr_svg(string $testo): string { return ''; } /* ============================================================ HAVE I BEEN PWNED (k-anonymity) */ function tw_password_violata(string $password): bool { if ($password === '') return false; $sha = strtoupper(sha1($password)); $pre = substr($sha, 0, 5); $suf = substr($sha, 5); $r = tw_http_get('https://api.pwnedpasswords.com/range/' . $pre, 6, ['Add-Padding: true']); if (!$r) return false; // in dubbio, non blocca foreach (explode("\n", $r) as $riga) { $p = explode(':', trim($riga)); if (isset($p[0]) && $p[0] === $suf && (int)($p[1] ?? 0) > 0) return true; } return false; } /* ============================================================ UPLOAD */ function tw_controlla_upload(): ?array { if (empty($_FILES) || !tw_conf('uploads_no_php', true)) return null; $cattivi = '/\.(php\d?|phtml|phar|phps|pht|php[0-9]|cgi|pl|asp|aspx|jsp|exe|sh|htaccess)(\.|$)/i'; foreach ($_FILES as $campo => $f) { $nomi = (array)($f['name'] ?? []); $tmp = (array)($f['tmp_name'] ?? []); if (!is_array($f['name'])) { $nomi = [$f['name']]; $tmp = [$f['tmp_name']]; } foreach ($nomi as $i => $nome) { $nome = (string)$nome; if ($nome === '') continue; // doppia estensione o estensione pericolosa if (preg_match($cattivi, $nome)) { return ['cat' => 'upload', 'd' => 'file non permesso: ' . tw_tronca($nome, 60), 'campione' => $nome, 'dove' => 'FILE:' . $campo]; } // contenuto PHP dentro il file (anche se chiamato .jpg) $t = $tmp[$i] ?? ''; if ($t && is_uploaded_file($t) || ($t && is_file($t))) { $testa = (string)@file_get_contents($t, false, null, 0, 4096); if (preg_match('/<\?php|<\?=| 'upload', 'd' => 'codice PHP dentro un file caricato', 'campione' => $nome, 'dove' => 'FILE:' . $campo]; } } } } return null; } /* ============================================================ HEADER DI SICUREZZA */ function tw_header_sicurezza(): void { if (headers_sent()) return; if (tw_conf('nascondi_powered', true)) { @header_remove('X-Powered-By'); @ini_set('expose_php', '0'); } if (!tw_on('header')) return; if (!tw_conf('frame_ok', false)) header('X-Frame-Options: SAMEORIGIN'); header('X-Content-Type-Options: nosniff'); header('Referrer-Policy: strict-origin-when-cross-origin'); header('Permissions-Policy: geolocation=(), camera=(), microphone=(), payment=()'); if (tw_conf('hsts', false) && !empty($_SERVER['HTTPS'])) header('Strict-Transport-Security: max-age=15552000; includeSubDomains'); $csp = (string)tw_conf('csp', ''); if ($csp !== '') header((tw_conf('csp_solo_report', true) ? 'Content-Security-Policy-Report-Only: ' : 'Content-Security-Policy: ') . $csp); } /* ============================================================ EMAIL */ function tw_email(string $oggetto, string $corpo, bool $grave = false): void { if (defined('TW_DEMO') || defined('TW_PROVA')) return; if (!tw_on('email')) return; if ($grave && !tw_conf('email_gravi', true)) return; $a = (string)tw_conf('email', ''); if ($a === '' || !filter_var($a, FILTER_VALIDATE_EMAIL)) return; // anti-flood: max 1 email dello stesso tipo ogni 15 min $ck = 'email:' . md5($oggetto); if (tw_store()->kv_get($ck)) return; tw_store()->kv_set($ck, 1, 900); $mitt = 'the-wall@' . preg_replace('/[^a-z0-9.\-]/i', '', (string)($_SERVER['SERVER_NAME'] ?? 'localhost')); $h = "From: The Wall <$mitt>\r\nContent-Type: text/plain; charset=UTF-8\r\n"; @mail($a, '[' . tw_conf('marchio_nome', 'The Wall') . '] ' . $oggetto, $corpo, $h); } /* ============================================================ WORDPRESS: FILE ORIGINALI */ /** Versione e lingua di WordPress lette da wp-includes/version.php (senza caricare WP). */ function tw_wp_versione(?string $radice = null): array { $radice = $radice ?? tw_integrita_radice(); $f = $radice . '/wp-includes/version.php'; if (!is_file($f)) return ['versione' => '', 'lingua' => 'en_US']; $t = (string)@file_get_contents($f); preg_match('/\$wp_version\s*=\s*\'([^\']+)\'/', $t, $v); preg_match('/\$wp_local_package\s*=\s*\'([^\']+)\'/', $t, $l); return ['versione' => $v[1] ?? '', 'lingua' => $l[1] ?? 'en_US']; } /** Confronta i file del core con le impronte ufficiali di api.wordpress.org. */ function tw_wp_core_controlla(): array { $radice = tw_integrita_radice(); $wv = tw_wp_versione($radice); if ($wv['versione'] === '') return ['ok' => false, 'msg' => 'WordPress non trovato in questa cartella.']; $url = 'https://api.wordpress.org/core/checksums/1.0/?version=' . rawurlencode($wv['versione']) . '&locale=' . rawurlencode($wv['lingua']); $j = json_decode((string)tw_http_get($url, 25), true); $sum = $j['checksums'] ?? null; if (isset($sum[$wv['versione']])) $sum = $sum[$wv['versione']]; if (!is_array($sum) || !$sum) return ['ok' => false, 'msg' => 'Impronte ufficiali non disponibili per la versione ' . $wv['versione'] . '.']; $modificati = []; $mancanti = []; $n = 0; foreach ($sum as $file => $md5) { if (strpos($file, 'wp-content/') === 0) continue; $p = $radice . '/' . $file; $n++; if (!is_file($p)) { $mancanti[] = $file; continue; } if (md5_file($p) !== $md5) $modificati[] = $file; } // file PHP in più dentro wp-admin / wp-includes (spesso backdoor) $extra = []; foreach (['wp-admin', 'wp-includes'] as $dir) { if (!is_dir($radice . '/' . $dir)) continue; $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($radice . '/' . $dir, FilesystemIterator::SKIP_DOTS)); foreach ($it as $f) { $rel = ltrim(str_replace('\\', '/', substr($f->getPathname(), strlen($radice))), '/'); if (substr($rel, -4) === '.php' && !isset($sum[$rel])) $extra[] = $rel; } } $r = ['ok' => true, 'versione' => $wv['versione'], 'controllati' => $n, 'modificati' => $modificati, 'mancanti' => $mancanti, 'extra' => array_slice($extra, 0, 200), 'quando' => time()]; tw_store()->kv_set('wp_core', $r); $r['msg'] = 'Controllati ' . $n . ' file di WordPress ' . $wv['versione'] . ': ' . count($modificati) . ' modificati, ' . count($mancanti) . ' mancanti, ' . count($extra) . ' in più.'; return $r; } /** Rimette il file originale di WordPress (scaricato dal repository ufficiale e verificato), con copia di sicurezza. */ function tw_wp_core_ripristina(string $file): array { $file = ltrim(str_replace('\\', '/', $file), '/'); if ($file === '' || strpos($file, '..') !== false || strpos($file, 'wp-content/') === 0) return ['ok' => false, 'msg' => 'File non valido.']; $radice = tw_integrita_radice(); $wv = tw_wp_versione($radice); $j = json_decode((string)tw_http_get('https://api.wordpress.org/core/checksums/1.0/?version=' . rawurlencode($wv['versione']) . '&locale=' . rawurlencode($wv['lingua']), 25), true); $sum = $j['checksums'] ?? []; if (isset($sum[$wv['versione']])) $sum = $sum[$wv['versione']]; if (!isset($sum[$file])) return ['ok' => false, 'msg' => 'Il file non fa parte di WordPress ' . $wv['versione'] . '.']; $orig = tw_http_get('https://core.svn.wordpress.org/tags/' . rawurlencode($wv['versione']) . '/' . str_replace('%2F', '/', rawurlencode($file)), 25); if ($orig === null || md5($orig) !== $sum[$file]) return ['ok' => false, 'msg' => 'Non riesco a scaricare l\'originale verificato (può succedere con le versioni tradotte): ripristina aggiornando WordPress.']; $dest = $radice . '/' . $file; $bak = tw_data('ripristino'); if (!is_dir($bak)) @mkdir($bak, 0700, true); if (is_file($dest)) @copy($dest, $bak . '/' . str_replace('/', '__', $file) . '.' . date('Ymd-His') . '.bak'); if (@file_put_contents($dest, $orig, LOCK_EX) === false) return ['ok' => false, 'msg' => 'Permessi insufficienti per scrivere il file.']; tw_store()->attivita('admin', 'Ripristinato file originale: ' . $file); return ['ok' => true, 'msg' => 'Ripristinato: ' . $file . ' (copia del vecchio file in data/ripristino).']; } The Wall CGFA · Bacheca
Stai guardando una DEMO con dati di esempio. Le modifiche non vengono salvate.Richiedi The Wall per il tuo sito

Bacheca

Lo stato della protezione del sito Sito di prova (demo)

51
Fermati oggi
186
Ultimi 7 giorni
590
Ultimi 30 giorni
88/100
Voto sicurezza

Attacchi degli ultimi 7 giorni

28
29
30
31
01
02
03

Come migliorare (88/100)

  • Imposta un indirizzo email per ricevere gli avvisi.
  • Aggiungi il tuo IP tra gli IP amministratore (non verrai mai bloccato).

Tipi di attacco

  • Scanner e bot cattivi30
  • Cross-site scripting (XSS)24
  • File trappola24
  • SQL injection19
  • Crawler AI18
  • Pagine inesistenti17
  • Esecuzione comandi13
  • PHP object injection11
  • Lettura file di sistema11
  • Troppe richieste9
  • Exploit noto (patch)7
  • Forza bruta login3

Paesi di provenienza

  • 🇨🇳 Cina52
  • 🇷🇺 Russia38
  • 🇺🇸 Stati Uniti28
  • 🇻🇳 Vietnam18
  • 🇮🇳 India15
  • 🇧🇷 Brasile8
  • 🇩🇪 Germania6
  • 🇮🇷 Iran6

IP più aggressivi

  • 66.249.150.1431
  • 66.249.85.211
  • 66.249.214.1931
  • 114.114.197.1661
  • 95.173.61.1031
  • 114.114.2.141
  • 177.54.133.1191
  • 113.161.6.1461
  • 114.114.11.501
  • 114.114.228.1601
Realizzato da CGFA · The Wall 1.0.0