this repo has no description
at main 250 lines 14 kB view raw
1<?php 2/** 3 * WordPress Login Honeypot / Tarpit 4 * 5 * Serves a convincing fake WP login page on /wp-login.php and /wp-admin. 6 * Logs full attacker intelligence — credentials, headers, timing, patterns. 7 * Progressive tarpit delays waste their time before fail2ban drops them. 8 * 9 * Real login is at the custom WPS Hide Login slug (or similar). 10 * 11 * Configuration: edit wp-trap-config.php alongside this file. 12 */ 13 14// --- Load configuration --- 15$config_file = __DIR__ . '/wp-trap-config.php'; 16if (file_exists($config_file)) { 17 require $config_file; 18} else { 19 // Defaults if no config file found 20 $site_name = 'WordPress'; 21 $site_url = ''; 22 $log_file = '/var/log/wp-honeypot.log'; 23 $intel_file = '/var/log/wp-honeypot-intel.jsonl'; 24 $state_dir = '/tmp/wp-honeypot'; 25 $max_delay = 30; 26} 27 28// --- Resolve real IP --- 29$ip = $_SERVER['REMOTE_ADDR']; 30if (!empty($_SERVER['HTTP_CF_CONNECTING_IP'])) { 31 $ip = $_SERVER['HTTP_CF_CONNECTING_IP']; 32} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { 33 $ip = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0]; 34} 35$ip = trim($ip); 36 37// --- State tracking per IP --- 38if (!is_dir($state_dir)) mkdir($state_dir, 0700, true); 39$state_file = $state_dir . '/' . md5($ip) . '.json'; 40$state = file_exists($state_file) ? json_decode(file_get_contents($state_file), true) : []; 41$attempts = ($state['attempts'] ?? 0); 42$first_seen = $state['first_seen'] ?? date('c'); 43$creds_tried = $state['creds_tried'] ?? []; 44 45$error_msg = ''; 46$show_expired = false; 47$submitted_user = ''; 48 49// --- Collect headers for intel --- 50function get_interesting_headers() { 51 $interesting = [ 52 'HTTP_USER_AGENT', 'HTTP_ACCEPT', 'HTTP_ACCEPT_LANGUAGE', 53 'HTTP_ACCEPT_ENCODING', 'HTTP_REFERER', 'HTTP_ORIGIN', 54 'HTTP_CF_CONNECTING_IP', 'HTTP_CF_IPCOUNTRY', 'HTTP_CF_RAY', 55 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED_PROTO', 56 'HTTP_X_REAL_IP', 'HTTP_CONNECTION', 'HTTP_COOKIE', 57 ]; 58 $headers = []; 59 foreach ($interesting as $h) { 60 if (!empty($_SERVER[$h])) { 61 $key = strtolower(str_replace('HTTP_', '', $h)); 62 $headers[$key] = $_SERVER[$h]; 63 } 64 } 65 return $headers; 66} 67 68// --- Handle POST (login attempt) --- 69if ($_SERVER['REQUEST_METHOD'] === 'POST') { 70 $attempts++; 71 $submitted_user = $_POST['log'] ?? ''; 72 $submitted_pass = $_POST['pwd'] ?? ''; 73 $remember_me = isset($_POST['rememberme']); 74 $redirect_to = $_POST['redirect_to'] ?? ''; 75 $ua = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown'; 76 $headers = get_interesting_headers(); 77 78 // Track credential pairs this IP has tried 79 $cred_pair = $submitted_user . ':' . $submitted_pass; 80 $creds_tried[] = $cred_pair; 81 82 // fail2ban log (simple format it can parse) 83 $log_line = sprintf( 84 "[%s] HONEYPOT: %s - attempt %d - user=%s\n", 85 date('Y-m-d H:i:s'), 86 $ip, 87 $attempts, 88 substr($submitted_user, 0, 64) 89 ); 90 file_put_contents($log_file, $log_line, FILE_APPEND | LOCK_EX); 91 92 // Full intel log (JSONL - one JSON object per line) 93 $intel = [ 94 'timestamp' => date('c'), 95 'ip' => $ip, 96 'attempt' => $attempts, 97 'username' => $submitted_user, 98 'password' => $submitted_pass, 99 'remember_me' => $remember_me, 100 'redirect_to' => $redirect_to, 101 'method' => $_SERVER['REQUEST_METHOD'], 102 'uri' => $_SERVER['REQUEST_URI'], 103 'headers' => $headers, 104 'first_seen' => $first_seen, 105 'delay_applied'=> min($attempts * 2, $max_delay), 106 'country' => $headers['cf_ipcountry'] ?? null, 107 'cf_ray' => $headers['cf_ray'] ?? null, 108 ]; 109 file_put_contents($intel_file, json_encode($intel) . "\n", FILE_APPEND | LOCK_EX); 110 111 // Save state 112 $state = [ 113 'attempts' => $attempts, 114 'first_seen' => $first_seen, 115 'last_seen' => date('c'), 116 'last_user' => substr($submitted_user, 0, 64), 117 'creds_tried' => array_slice($creds_tried, -100), // keep last 100 118 ]; 119 file_put_contents($state_file, json_encode($state, JSON_PRETTY_PRINT)); 120 121 // Progressive tarpit: 2, 4, 6... up to max_delay 122 $delay = min($attempts * 2, $max_delay); 123 sleep($delay); 124 125 // Rotate through realistic error messages 126 $errors = [ 127 '<strong>Error:</strong> The password you entered for the username <strong>%s</strong> is incorrect. <a href="#" title="Password Lost and Found">Lost your password?</a>', 128 '<strong>Error:</strong> The password you entered for the username <strong>%s</strong> is incorrect. <a href="#" title="Password Lost and Found">Lost your password?</a>', 129 '<strong>Error:</strong> Unknown username. Check again or try your email address.', 130 '<strong>Error:</strong> The password you entered for the username <strong>%s</strong> is incorrect. <a href="#" title="Password Lost and Found">Lost your password?</a>', 131 '<strong>Error:</strong> There has been a critical error on this website. <a href="#">Learn more about troubleshooting WordPress.</a>', 132 ]; 133 134 // After many attempts, mix in session expired and rate limit messages 135 if ($attempts > 12 && $attempts % 4 === 0) { 136 $show_expired = true; 137 } 138 if ($attempts > 15 && $attempts % 5 === 0) { 139 $error_msg = '<strong>Error:</strong> Too many failed login attempts. Please try again in 15 minutes.'; 140 } else { 141 $error_idx = ($attempts - 1) % count($errors); 142 $error_msg = sprintf($errors[$error_idx], htmlspecialchars($submitted_user)); 143 } 144} else { 145 // GET request — also log reconnaissance 146 $headers = get_interesting_headers(); 147 $intel = [ 148 'timestamp' => date('c'), 149 'ip' => $ip, 150 'attempt' => 0, 151 'type' => 'recon', 152 'method' => 'GET', 153 'uri' => $_SERVER['REQUEST_URI'], 154 'query' => $_SERVER['QUERY_STRING'] ?? '', 155 'headers' => $headers, 156 'first_seen' => $first_seen, 157 'country' => $headers['cf_ipcountry'] ?? null, 158 ]; 159 file_put_contents($intel_file, json_encode($intel) . "\n", FILE_APPEND | LOCK_EX); 160 161 // Returning visitors get a delay even on GET 162 if ($attempts > 0) { 163 sleep(min($attempts, 5)); 164 } 165} 166 167// --- Derive values for the page --- 168$php_version = phpversion(); 169$wp_json_url = rtrim($site_url, '/') . '/wp-json/'; 170 171// --- Render fake login page --- 172http_response_code(200); 173header('X-Frame-Options: SAMEORIGIN'); 174header('Content-Type: text/html; charset=UTF-8'); 175header('X-Powered-By: PHP/' . $php_version); 176if ($site_url) { 177 header('Link: <' . $wp_json_url . '>; rel="https://api.w.org/"'); 178} 179?> 180<!DOCTYPE html> 181<html lang="en-US"> 182<head> 183<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> 184<meta name="viewport" content="width=device-width"> 185<meta name="robots" content="noindex, nofollow"> 186<title>Log In &lsaquo; <?php echo htmlspecialchars($site_name); ?> &#8212; WordPress</title> 187<style> 188html{background:#f0f0f1} 189body{background:#f0f0f1;min-width:0;color:#3c434a;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;font-size:13px;line-height:1.4} 190a{color:#2271b1;transition-property:border,background,color;transition-duration:.05s;transition-timing-function:ease-in-out} 191a:hover{color:#135e96} 192.login .message,.login .success,.login .notice{border-left:4px solid #72aee6;padding:12px;margin-left:0;margin-bottom:20px;background-color:#fff;box-shadow:0 1px 1px 0 rgba(0,0,0,.1)} 193.login #login_error{border-left:4px solid #d63638;padding:12px;margin-left:0;margin-bottom:20px;background-color:#fff;box-shadow:0 1px 1px 0 rgba(0,0,0,.1);word-wrap:break-word} 194.login #login_error a{color:#d63638} 195#login{width:320px;padding:5% 0 0;margin:auto} 196#login_error a,#login .message a,.login .success a{text-decoration:none} 197#login form{margin-top:20px;margin-left:0;padding:26px 24px 34px;font-weight:400;overflow:hidden;background:#fff;border:1px solid #c3c4c7;border-radius:3px;box-shadow:0 1px 3px rgba(0,0,0,.04)} 198#login form .forgetmenot{font-weight:400;float:left;margin-bottom:0} 199#login form p.submit{float:right} 200.login h1{text-align:center} 201.login h1 a{background-image:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA4NCA4NCI+PHJlY3Qgd2lkdGg9Ijg0IiBoZWlnaHQ9Ijg0IiBmaWxsPSIjNDY0NjQ2Ii8+PHBhdGggZD0iTTY5LjE2NiA0NS42MzdDNjkuMTY2IDQ1LjE5NCA2OS4xNjIgNDQuNzc0IDY5LjA1IDQ0LjM3NUw2OS4wNSA0NC4zNzVDNjcuOTE3IDQwLjM0IDY0LjM5OSAzNy4yNDIgNjAuMTI1IDM2LjcwNkw0OS45ODUgMzUuNDgyQzQ4LjYyNSAzNS4zMTkgNDcuNDE4IDM0LjU5NSA0Ni42NjIgMzMuNTAxTDQzLjA0OCAyOC4yNjdDNDIuMjE1IDI3LjA2MSA0MC44MjggMjYuMzI5IDM5LjM0MSAyNi4zMjlMMjYuMTY0IDI2LjMyOUMyMy4xMTcgMjYuMzI5IDIwLjY0NiAyOC44IDIwLjY0NiAzMS44NDdMMjAuNjQ2IDQ1LjYzN0MyMC42NDYgNDUuNjQzIDIwLjY0OCA0NS42NDkgMjAuNjQ4IDQ1LjY1NUMyMC42NDggNDkuMjA4IDIzLjUyOCA1Mi4xMDQgMjcuMDggNTIuMTI5TDI3LjI1MyA1Mi4xMjlDMjcuMjkgNTUuNjYyIDMwLjE2MSA1OC41MTMgMzMuNzAxIDU4LjUxM0MzNy4yNCA1OC41MTMgNDAuMTExIDU1LjY2MiA0MC4xNDggNTIuMTI5TDQ5LjgxNiA1Mi4xMjlDNDkuODUzIDU1LjY2MiA1Mi43MjQgNTguNTEzIDU2LjI2MyA1OC41MTNDNTkuODAzIDU4LjUxMyA2Mi42NzQgNTUuNjYyIDYyLjcxMSA1Mi4xMjlMNjIuODE0IDUyLjEyOUM2Ni4zNTIgNTIuMTI5IDY5LjIyIDQ5LjI2MSA2OS4xNjYgNDUuNzIzTDY5LjE2NiA0NS42MzdaTTMzLjcwMSA1NS41NjRDMzEuNzkyIDU1LjU2NCAzMC4yNDQgNTQuMDE2IDMwLjI0NCA1Mi4xMDdDMzAuMjQ0IDUwLjE5OCAzMS43OTIgNDguNjUgMzMuNzAxIDQ4LjY1QzM1LjYxIDQ4LjY1IDM3LjE1OCA1MC4xOTggMzcuMTU4IDUyLjEwN0MzNy4xNTggNTQuMDE2IDM1LjYxIDU1LjU2NCAzMy43MDEgNTUuNTY0Wk01Ni4yNjMgNTUuNTY0QzU0LjM1NCA1NS41NjQgNTIuODA2IDU0LjAxNiA1Mi44MDYgNTIuMTA3QzUyLjgwNiA1MC4xOTggNTQuMzU0IDQ4LjY1IDU2LjI2MyA0OC42NUM1OC4xNzIgNDguNjUgNTkuNzIgNTAuMTk4IDU5LjcyIDUyLjEwN0M1OS43MiA1NC4wMTYgNTguMTcyIDU1LjU2NCA1Ni4yNjMgNTUuNTY0WiIgZmlsbD0iI2ZmZiIvPjwvc3ZnPg==');background-size:84px;background-position:center;background-repeat:no-repeat;color:#3c434a;height:84px;font-size:0;width:84px;display:block;margin:0 auto 25px} 202.login h1 a:focus{box-shadow:none} 203.login label{font-size:14px;display:block;margin-bottom:3px} 204.login form .input,.login input[type=text],.login input[type=password]{font-size:24px;width:100%;padding:3px;margin:2px 6px 16px 0;border:1px solid #8c8f94;box-sizing:border-box;border-radius:3px;background:#fff;color:#2c3338} 205.login form .input:focus,.login input[type=text]:focus,.login input[type=password]:focus{border-color:#2271b1;box-shadow:0 0 0 1px #2271b1;outline:2px solid transparent} 206.wp-core-ui .button-primary{background:#2271b1;border-color:#2271b1;color:#fff;text-decoration:none;text-shadow:none;border-width:1px;border-style:solid;border-radius:3px;cursor:pointer;font-size:13px;line-height:2.15384615;min-height:30px;padding:0 10px;display:inline-block;white-space:nowrap;box-sizing:border-box;-webkit-appearance:none} 207.wp-core-ui .button-primary:hover{background:#135e96;border-color:#135e96;color:#fff} 208.wp-core-ui .button-primary:focus{background:#135e96;border-color:#135e96;color:#fff;box-shadow:0 0 0 1px #fff,0 0 0 3px #135e96;outline:2px solid transparent;outline-offset:0} 209#login form p.submit .button-primary{float:right;width:100%;text-align:center;margin-top:16px;padding:6px;font-size:14px;line-height:1.5;min-height:40px} 210p#nav,p#backtoblog{margin:24px 0 0;padding:0;text-align:center} 211p#nav a,p#backtoblog a{color:#50575e;font-size:13px} 212p#nav a:hover,p#backtoblog a:hover{color:#135e96} 213.login #backtoblog a{display:inline} 214.login .privacy-policy-page-link{text-align:center;width:320px;margin:24px auto 0} 215.login .privacy-policy-page-link a{font-size:13px;color:#50575e} 216input[type=checkbox]{border:1px solid #8c8f94;border-radius:3px;background:#fff;color:#50575e;clear:none;cursor:pointer;display:inline-block;line-height:0;height:1rem;margin:-0.25rem .25rem 0 0;outline:0;padding:0!important;text-align:center;vertical-align:middle;width:1rem;min-width:1rem;-webkit-appearance:none;transition:border-color .1s ease-in-out} 217.language-switcher{padding:10px;text-align:center;margin:24px auto 0;width:320px} 218</style> 219</head> 220<body class="login js login-action-login wp-core-ui locale-en-us"> 221<div id="login"> 222<h1><a href="https://wordpress.org/" tabindex="-1">Powered by WordPress</a></h1> 223<?php if ($show_expired): ?> 224<div class="message"><p>Session expired. Please log in again.</p></div> 225<?php endif; ?> 226<?php if ($error_msg): ?> 227<div id="login_error"><?php echo $error_msg; ?></div> 228<?php endif; ?> 229<form name="loginform" id="loginform" action="" method="post"> 230<p> 231<label for="user_login">Username or Email Address</label> 232<input type="text" name="log" id="user_login" class="input" value="<?php echo htmlspecialchars($submitted_user); ?>" size="20" autocapitalize="off" autocomplete="username" required> 233</p> 234<p> 235<label for="user_pass">Password</label> 236<input type="password" name="pwd" id="user_pass" class="input" value="" size="20" autocomplete="current-password" spellcheck="false" required> 237</p> 238<p class="forgetmenot"><label for="rememberme"><input name="rememberme" type="checkbox" id="rememberme" value="forever"> Remember Me</label></p> 239<p class="submit"> 240<input type="submit" name="wp-submit" id="wp-submit" class="button button-primary button-large" value="Log In"> 241<input type="hidden" name="redirect_to" value="/wp-admin/"> 242<input type="hidden" name="testcookie" value="1"> 243</p> 244</form> 245<p id="nav"><a href="#">Lost your password?</a></p> 246<p id="backtoblog"><a href="/">&larr; Go to <?php echo htmlspecialchars($site_name); ?></a></p> 247</div> 248<div class="language-switcher"><form id="language-switcher" method="get"><label for="language-switcher-locales"><select id="language-switcher-locales" name="wp_lang"><option value="en_US" lang="en" selected="selected" data-installed="1">English (United States)</option></select></label><input type="submit" class="button" value="Change"></form></div> 249</body> 250</html>