186 lines
6.1 KiB
PHP
186 lines
6.1 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Models\AuditEvent;
|
|
use App\Services\BruteforceGuard;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\Response;
|
|
use Illuminate\Support\Str;
|
|
|
|
/**
|
|
* Honeypot deception layer. Handles requests to decoy paths (routes/web.php, above the guest
|
|
* group) that no legitimate client ever visits — /.env, /wp-login.php, /phpmyadmin, … A hit is
|
|
* proof of a hostile probe, so we: audit it, instant-ban the source IP (BruteforceGuard::banNow,
|
|
* exempt/loopback are skipped), and return a small, self-contained DECEPTIVE body so the attacker
|
|
* keeps chasing the bait instead of noticing a real panel behind it. Attacker-facing strings are
|
|
* intentionally NOT localized (they impersonate generic third-party software).
|
|
*/
|
|
class HoneypotController extends Controller
|
|
{
|
|
public function trap(Request $request): Response
|
|
{
|
|
// Feature off → behave exactly like an ordinary 404 for these paths (no ban, no audit).
|
|
if (! config('clusev.honeypot.enabled')) {
|
|
abort(404);
|
|
}
|
|
|
|
$ip = (string) $request->ip();
|
|
|
|
AuditEvent::create([
|
|
'user_id' => null,
|
|
'actor' => 'system',
|
|
'action' => 'security.honeypot_hit',
|
|
'target' => Str::limit($request->path(), 190),
|
|
'ip' => $ip,
|
|
'meta' => [
|
|
'ua' => Str::limit((string) $request->userAgent(), 190),
|
|
'method' => $request->method(),
|
|
'query' => Str::limit((string) $request->getQueryString(), 190),
|
|
],
|
|
]);
|
|
|
|
app(BruteforceGuard::class)->banNow($ip, 'honeypot');
|
|
|
|
return $this->deceive($request);
|
|
}
|
|
|
|
/** Pick a convincing fake response for the probed path. */
|
|
private function deceive(Request $request): Response
|
|
{
|
|
$path = strtolower($request->path());
|
|
|
|
if ($path === '.env') {
|
|
return response($this->fakeDotenv(), 200, ['Content-Type' => 'text/plain; charset=UTF-8']);
|
|
}
|
|
|
|
if (str_starts_with($path, 'wp-login.php') || str_starts_with($path, 'wp-admin')) {
|
|
return response($this->fakeWordPress(), 200, ['Content-Type' => 'text/html; charset=UTF-8']);
|
|
}
|
|
|
|
if (str_starts_with($path, 'phpmyadmin')) {
|
|
return response($this->fakePhpMyAdmin(), 200, ['Content-Type' => 'text/html; charset=UTF-8']);
|
|
}
|
|
|
|
return response($this->fakeGenericLogin(), 200, ['Content-Type' => 'text/html; charset=UTF-8']);
|
|
}
|
|
|
|
/** Plausible-looking dotenv seeded with the three config canaries (the bait). */
|
|
private function fakeDotenv(): string
|
|
{
|
|
$c = (array) config('clusev.honeypot.canaries');
|
|
|
|
return implode("\n", [
|
|
'APP_NAME=Application',
|
|
'APP_ENV=production',
|
|
'APP_KEY='.($c['app_key'] ?? ''),
|
|
'APP_DEBUG=false',
|
|
'APP_URL=https://app.internal.example.com',
|
|
'',
|
|
'DB_CONNECTION=mysql',
|
|
'DB_HOST=10.20.0.14',
|
|
'DB_PORT=3306',
|
|
'DB_DATABASE=app_production',
|
|
'DB_USERNAME=app_prod',
|
|
'DB_PASSWORD='.($c['db_password'] ?? ''),
|
|
'',
|
|
'AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE',
|
|
'AWS_SECRET_ACCESS_KEY='.($c['aws_secret'] ?? ''),
|
|
'AWS_DEFAULT_REGION=eu-central-1',
|
|
'',
|
|
]);
|
|
}
|
|
|
|
private function fakeWordPress(): string
|
|
{
|
|
return <<<'HTML'
|
|
<!DOCTYPE html>
|
|
<html lang="en-US">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Log In ‹ Blog — WordPress</title>
|
|
</head>
|
|
<body class="login">
|
|
<div id="login">
|
|
<h1><a href="https://wordpress.org/">Powered by WordPress</a></h1>
|
|
<form name="loginform" id="loginform" action="wp-login.php" method="post">
|
|
<p><label for="user_login">Username or Email Address</label>
|
|
<input type="text" name="log" id="user_login" class="input" value="" size="20"></p>
|
|
<p><label for="user_pass">Password</label>
|
|
<input type="password" name="pwd" id="user_pass" class="input" value="" size="20"></p>
|
|
<p class="forgetmenot"><label for="rememberme"><input name="rememberme" type="checkbox" id="rememberme" value="forever"> Remember Me</label></p>
|
|
<p class="submit"><input type="submit" name="wp-submit" id="wp-submit" class="button button-primary" value="Log In"></p>
|
|
</form>
|
|
<p id="nav"><a href="wp-login.php?action=lostpassword">Lost your password?</a></p>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
HTML;
|
|
}
|
|
|
|
private function fakePhpMyAdmin(): string
|
|
{
|
|
return <<<'HTML'
|
|
<!DOCTYPE html>
|
|
<html lang="en" dir="ltr">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>phpMyAdmin</title>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<a href="index.php" class="logo">phpMyAdmin</a>
|
|
<h1>Welcome to phpMyAdmin</h1>
|
|
<form method="post" action="index.php" name="login_form" class="login">
|
|
<fieldset>
|
|
<legend>Log in</legend>
|
|
<div class="item">
|
|
<label for="input_username">Username:</label>
|
|
<input type="text" name="pma_username" id="input_username" value="" size="24" class="textfield">
|
|
</div>
|
|
<div class="item">
|
|
<label for="input_password">Password:</label>
|
|
<input type="password" name="pma_password" id="input_password" value="" size="24" class="textfield">
|
|
</div>
|
|
<div class="item">
|
|
<label for="select_server">Server Choice:</label>
|
|
<select name="server" id="select_server"><option value="1">localhost</option></select>
|
|
</div>
|
|
<input type="submit" name="submit" value="Go" class="button">
|
|
</fieldset>
|
|
</form>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
HTML;
|
|
}
|
|
|
|
private function fakeGenericLogin(): string
|
|
{
|
|
return <<<'HTML'
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Sign in</title>
|
|
</head>
|
|
<body>
|
|
<main>
|
|
<h1>Sign in to continue</h1>
|
|
<form method="post" action="login">
|
|
<label for="username">Username</label>
|
|
<input type="text" name="username" id="username" autocomplete="username">
|
|
<label for="password">Password</label>
|
|
<input type="password" name="password" id="password" autocomplete="current-password">
|
|
<button type="submit">Sign in</button>
|
|
</form>
|
|
</main>
|
|
</body>
|
|
</html>
|
|
HTML;
|
|
}
|
|
}
|