mirror of
https://codeberg.org/vlw/honeypot.git
synced 2025-11-05 04:22:43 +01:00
wip: 2025-09-23T19:29:57+0200 (1758648597)
This commit is contained in:
parent
1c2153552c
commit
3edfd6c164
17 changed files with 240 additions and 97 deletions
1
.env.example.ini
Normal file
1
.env.example.ini
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
LOG_ENABLED = true
|
||||||
1
.env.ini
Normal file
1
.env.ini
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
LOG_ENABLED = false
|
||||||
5
assets/css/pages/dashboard.css
Normal file
5
assets/css/pages/dashboard.css
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
vv-shell {
|
||||||
|
display: grid;
|
||||||
|
justify-items: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
5
assets/css/pages/index.css
Normal file
5
assets/css/pages/index.css
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
vv-shell {
|
||||||
|
display: grid;
|
||||||
|
justify-items: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
@ -59,6 +59,10 @@ button {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dialog {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
/* Sections */
|
/* Sections */
|
||||||
|
|
||||||
vv-shell {
|
vv-shell {
|
||||||
|
|
@ -71,7 +75,7 @@ vv-shell {
|
||||||
border-radius: 9px;
|
border-radius: 9px;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
|
||||||
&[vv-loading="true"] {
|
&[vv-loading="true"] ::not(dialog) {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
99
assets/js/modules/Logger.js
Normal file
99
assets/js/modules/Logger.js
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
const MOUSE_MOVE_TIMEOUT_MS = 100;
|
||||||
|
|
||||||
|
globalThis.Logger = class Logger {
|
||||||
|
#abort;
|
||||||
|
#mouseMoveTimeout;
|
||||||
|
|
||||||
|
static get url() {
|
||||||
|
const url = new URL(window.location);
|
||||||
|
url.pathname = "/log";
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a best-effort unique string that identifies this client
|
||||||
|
* @returns {String}
|
||||||
|
*/
|
||||||
|
static async #fingerprint() {
|
||||||
|
// Create a hash from various identifying browser data
|
||||||
|
const buffer = await window.crypto.subtle.digest("SHA-1", new TextEncoder().encode(JSON.stringify([
|
||||||
|
navigator.userAgent,
|
||||||
|
navigator.buildId,
|
||||||
|
navigator.languages
|
||||||
|
])));
|
||||||
|
|
||||||
|
return new DataView(buffer).getBigUint64().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract desired MouseEvent data into an object literal
|
||||||
|
* @param {MouseEvent} event
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
static #mouseEvent(event) {
|
||||||
|
return {
|
||||||
|
e: event.type,
|
||||||
|
w: window.innerWidth,
|
||||||
|
h: window.innerHeight,
|
||||||
|
x: event.x,
|
||||||
|
y: event.y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract desired KeyboardEvent data into an object literal
|
||||||
|
* @param {KeyboardEvent} event
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
static #keyEvent(event) {
|
||||||
|
return {
|
||||||
|
e: event.type,
|
||||||
|
c: event.key,
|
||||||
|
s: event.shiftKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start logging user activities
|
||||||
|
*/
|
||||||
|
start() {
|
||||||
|
this.#abort = new AbortController();
|
||||||
|
|
||||||
|
document.addEventListener("keyup", (event) => this.log(Logger.#keyEvent(event)), { signal: this.#abort.signal });
|
||||||
|
document.addEventListener("keydown", (event) => this.log(Logger.#keyEvent(event)), { signal: this.#abort.signal });
|
||||||
|
document.addEventListener("click", (event) => this.log(Logger.#mouseEvent(event)), { signal: this.#abort.signal });
|
||||||
|
document.addEventListener("mousemove", (event) => {
|
||||||
|
// Throttle mousemove events
|
||||||
|
clearTimeout(this.#mouseMoveTimeout);
|
||||||
|
//this.#mouseMoveTimeout = setTimeout(() => this.#log(mouseEvent(event)), MOUSE_MOVE_TIMEOUT_MS);
|
||||||
|
}, { signal: this.#abort.signal });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop logging user activitiers
|
||||||
|
*/
|
||||||
|
stop() {
|
||||||
|
this.#abort.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log user data
|
||||||
|
* @param {Object} data
|
||||||
|
* @returns {Response}
|
||||||
|
*/
|
||||||
|
async log(data) {
|
||||||
|
return await fetch(Logger.url, {
|
||||||
|
body: JSON.stringify({
|
||||||
|
client: data,
|
||||||
|
fingerprint: (await Logger.#fingerprint())
|
||||||
|
}),
|
||||||
|
method: "POST",
|
||||||
|
headers: Object.assign({
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}, VV.header)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
5
assets/js/pages/dashboard.js
Normal file
5
assets/js/pages/dashboard.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
// Clear all content and display the loading spinner for now. I want to add more stuff here later!
|
||||||
|
setTimeout(() => {
|
||||||
|
VV.shell.innerHTML = "";
|
||||||
|
VV.shell.setAttribute("vv-loading", true);
|
||||||
|
}, VV.delay);
|
||||||
|
|
@ -1,23 +1,57 @@
|
||||||
const WHITELIST_USERNAMES = [
|
// Simulate a fake login page
|
||||||
"user",
|
{
|
||||||
"root",
|
const WHITELIST_USERNAMES = [
|
||||||
"admin",
|
"user",
|
||||||
"mydlink"
|
"root",
|
||||||
];
|
"admin",
|
||||||
const WHITELIST_PASSWORDS = [
|
"mydlink"
|
||||||
"root",
|
];
|
||||||
"admin",
|
const WHITELIST_PASSWORDS = [
|
||||||
"12345",
|
"root",
|
||||||
"mydlink",
|
"admin",
|
||||||
"password",
|
"12345",
|
||||||
"123456789"
|
"mydlink",
|
||||||
]
|
"password",
|
||||||
|
"123456789"
|
||||||
|
];
|
||||||
|
const INPUT_NAME_USERNAME = "username";
|
||||||
|
const INPUT_NAME_PASSWORD = "password";
|
||||||
|
|
||||||
document.querySelector("form button").addEventListener("click", (event) => {
|
document.querySelector("form button").addEventListener("click", (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
VV.shell.setAttribute("vv-loading", true);
|
VV.shell.setAttribute("vv-loading", true);
|
||||||
const form = new FormData(event.target.closest("form"));
|
const form = new FormData(event.target.closest("form"));
|
||||||
|
|
||||||
console.log("Hello");
|
// Invalid fake username or password derp
|
||||||
});
|
if (
|
||||||
|
!WHITELIST_USERNAMES.includes(form.get(INPUT_NAME_USERNAME))
|
||||||
|
|| !WHITELIST_PASSWORDS.includes(form.get(INPUT_NAME_PASSWORD))
|
||||||
|
) {
|
||||||
|
// Show "incorrect credentials" dialog after global Vegvisir delay
|
||||||
|
setTimeout(() => {
|
||||||
|
VV.shell.setAttribute("vv-loading", false);
|
||||||
|
document.querySelector("dialog").showModal();
|
||||||
|
}, VV.delay);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
new VV().navigate("/dashboard");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only start logging if the user does something with the input fields
|
||||||
|
{
|
||||||
|
const abortInitialInputChange = new AbortController();
|
||||||
|
|
||||||
|
const startLogging = () =>{
|
||||||
|
abortInitialInputChange.abort();
|
||||||
|
new globalThis.Logger().start();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.querySelector("button").addEventListener("click", () => startLogging(), { signal: abortInitialInputChange.signal });
|
||||||
|
document.querySelectorAll("input").forEach(element => element.addEventListener("click", () => startLogging(), { signal: abortInitialInputChange.signal }));
|
||||||
|
document.querySelectorAll("input").forEach(element => element.addEventListener("keydown", () => startLogging(), { signal: abortInitialInputChange.signal }));
|
||||||
|
document.querySelectorAll("input").forEach(element => element.addEventListener("change", () => startLogging(), { signal: abortInitialInputChange.signal }));
|
||||||
|
}
|
||||||
|
|
@ -1,73 +1,30 @@
|
||||||
// Set a global delay to simulate crappy web software
|
const LOGIN_PAGE = "/login";
|
||||||
VV.delay = 200;
|
const STORAGE_KEY_LOGGEDIN = "mydlink_dashboard_login";
|
||||||
|
|
||||||
// Log user activities
|
// Set a generous global navigation delay to simulate crappy web software
|
||||||
{
|
VV.delay = 3500;
|
||||||
const MOUSE_MOVE_TIMEOUT_MS = 100;
|
|
||||||
|
|
||||||
const logUrl = new URL(window.location);
|
// Redirect the user to the login page if session storage key is not set
|
||||||
logUrl.pathname = "/log";
|
if (!sessionStorage.getItem(STORAGE_KEY_LOGGEDIN) && window.location.pathname !== LOGIN_PAGE) {
|
||||||
|
const getRandomString = (length = 16) => {
|
||||||
|
const CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
let string = "";
|
||||||
|
|
||||||
let mouseMoveTimeout;
|
for (let i = 0; i < length; i++) string += CHARSET[Math.floor(Math.random() * CHARSET.length)];
|
||||||
|
|
||||||
// Return a fingerprint for this browser
|
return string;
|
||||||
const fingerprint = async () => {
|
|
||||||
const buffer = await window.crypto.subtle.digest("SHA-1", new TextEncoder().encode(JSON.stringify([
|
|
||||||
navigator.userAgent,
|
|
||||||
navigator.buildId,
|
|
||||||
navigator.languages
|
|
||||||
])));
|
|
||||||
|
|
||||||
let fingerprint;
|
|
||||||
|
|
||||||
for (let i = 0; i < buffer.byteLength; i++) {
|
|
||||||
fingerprint += buffer[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
return fingerprint;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Log data
|
const url = new URL(window.location);
|
||||||
const log = async (data) => {
|
|
||||||
console.log(JSON.stringify({
|
|
||||||
data: data,
|
|
||||||
fingerprint: await fingerprint()
|
|
||||||
}));
|
|
||||||
|
|
||||||
return await fetch(logUrl, {
|
// Set some legit looking overcomplicated search parameters
|
||||||
body: JSON.stringify({
|
url.searchParams.set("mydl_sid", getRandomString());
|
||||||
data: data,
|
// This is our fake "user is logged in" Storage API key
|
||||||
fingerprint: await fingerprint()
|
url.searchParams.set("action", STORAGE_KEY_LOGGEDIN);
|
||||||
}),
|
url.searchParams.set(`mydl_${getRandomString(3)}`, "dashboard");
|
||||||
method: "POST",
|
url.searchParams.set(`mydl_asas_${getRandomString(4)}_${getRandomString(8)}`, "login_cgi");
|
||||||
headers: VV.header
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const mouseEvent = (event) => {
|
url.pathname = LOGIN_PAGE;
|
||||||
return {
|
|
||||||
e: event.type,
|
|
||||||
w: window.innerWidth,
|
|
||||||
h: window.innerHeight,
|
|
||||||
x: event.x,
|
|
||||||
y: event.y
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const keyEvent = (event) => {
|
new VV().navigate(url);
|
||||||
return {
|
|
||||||
e: event.type,
|
|
||||||
c: event.key,
|
|
||||||
s: event.shiftKey
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener("keyup", (event) => log(keyEvent(event)));
|
|
||||||
document.addEventListener("keydown", (event) => log(keyEvent(event)));
|
|
||||||
document.addEventListener("click", (event) => log(mouseEvent(event)));
|
|
||||||
document.addEventListener("mousemove", (event) => {
|
|
||||||
// Throttle mousemove events
|
|
||||||
clearTimeout(mouseMoveTimeout);
|
|
||||||
//mouseMoveTimeout = setTimeout(() => log(mouseEvent(event)), MOUSE_MOVE_TIMEOUT_MS);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
3
public/dashboard.php
Normal file
3
public/dashboard.php
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<style><?= VV::css("assets/css/pages/dashboard") ?></style>
|
||||||
|
<img src="/assets/media/loading.gif">
|
||||||
|
<script><?= VV::js("assets/js/pages/dashboard") ?></script>
|
||||||
1
public/error.php
Normal file
1
public/error.php
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<h1>404 Not Found</h1>
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
<?= VV::include("public/login") ?>
|
<style><?= VV::css("assets/css/pages/index") ?></style>
|
||||||
|
<img src="/assets/media/loading.gif">
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
require_once VV::root("src/Log.php");
|
require_once VV::root("src/Log.php");
|
||||||
|
|
||||||
if ($_SERVER["REQUEST_METHOD"] === "POST" && !empty($_POST)) {
|
if ($_SERVER["REQUEST_METHOD"] === "POST" && !empty($_POST)) {
|
||||||
save_log($_POST);
|
save_log((object) $_POST);
|
||||||
}
|
}
|
||||||
|
|
||||||
?>
|
?>
|
||||||
|
|
@ -2,11 +2,11 @@
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
<label>
|
<label>
|
||||||
Username
|
Username
|
||||||
<input type="text" required></input>
|
<input name="username" type="text" required></input>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Password
|
Password
|
||||||
<input type="password" required></input>
|
<input name="password" type="password" required></input>
|
||||||
</label>
|
</label>
|
||||||
<button type="submit">Log in</button>
|
<button type="submit">Log in</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -16,9 +16,9 @@
|
||||||
<p>Please follow these steps in order to register your mdlink-enabled product and get access to both mydlink.com and our mobile apps. Learn more details here.</p>
|
<p>Please follow these steps in order to register your mdlink-enabled product and get access to both mydlink.com and our mobile apps. Learn more details here.</p>
|
||||||
</aside>
|
</aside>
|
||||||
<dialog>
|
<dialog>
|
||||||
<p>Incorrect username or password</p>
|
|
||||||
<form method="dialog">
|
<form method="dialog">
|
||||||
<button>OK</button>
|
<p>Incorrect username or password</p>
|
||||||
|
<button>Try again</button>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
<script><?= VV::js("assets/js/pages/login") ?></script>
|
<script type="module"><?= VV::js("assets/js/pages/login") ?></script>
|
||||||
|
|
@ -2,14 +2,15 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>mydlink</title>
|
<title>mydlink</title>
|
||||||
<link rel="icon" href="/assets/media/favicon.ico">
|
<link rel="icon" href="/assets/media/favicon.ico">
|
||||||
|
|
||||||
<style><?= VV::css("assets/css/shell") ?></style>
|
<style><?= VV::css("assets/css/shell") ?></style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<img src="/assets/media/logo.gif">
|
<img src="/assets/media/logo.gif">
|
||||||
|
<p>DIR-880L</p>
|
||||||
<nav>
|
<nav>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="">Home</a></li>
|
<li><a href="">Home</a></li>
|
||||||
|
|
@ -55,6 +56,7 @@
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<?= VV::init() ?>
|
<?= VV::init() ?>
|
||||||
|
<script><?= VV::js("assets/js/modules/Logger.js") ?></script>
|
||||||
<script><?= VV::js("assets/js/shell") ?></script>
|
<script><?= VV::js("assets/js/shell") ?></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
29
src/Log.php
29
src/Log.php
|
|
@ -4,6 +4,31 @@
|
||||||
|
|
||||||
use \VV;
|
use \VV;
|
||||||
|
|
||||||
function save_log(array $data): bool {
|
// Save logs to this directory
|
||||||
$log_dir = VV::root("logs");
|
const LOG_DIR = "logs/";
|
||||||
|
// Use this as the default fingerprint if we don't get one from the client
|
||||||
|
const DEFAULT_FINGERPRINT = "undefined";
|
||||||
|
|
||||||
|
function save_log(object $data): bool {
|
||||||
|
// Logging is explicitly disabled. No environment variable is treated as enabled
|
||||||
|
if (!($_ENV["LOG_ENABLED"] ?? true)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data->fingerprint ??= DEFAULT_FINGERPRINT;
|
||||||
|
$log_dir = VV::root(LOG_DIR . $data->fingerprint);
|
||||||
|
|
||||||
|
// Create log directory for this fingerprint if it's the first time we've seen it
|
||||||
|
if (!is_dir($log_dir)) {
|
||||||
|
if (!mkdir($log_dir)) {
|
||||||
|
// Failed to create directory
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include server data
|
||||||
|
$data->server = (object) $_SERVER;
|
||||||
|
|
||||||
|
// Write log file
|
||||||
|
return file_put_contents($log_dir . "/" . microtime() . ".json", json_encode($data));
|
||||||
}
|
}
|
||||||
Loading…
Add table
Reference in a new issue