this repo has no description
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 ‹ <?php echo htmlspecialchars($site_name); ?> — 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="/">← 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>