feat(secubox-auth-logger): Add LuCI auth failure detection
- Add CGI hook to capture client IP during failed auth attempts - Add JavaScript hook to intercept ubus session.login failures - Add rpcd plugin for ubus-based auth logging - Update CrowdSec parser for case-insensitive matching - Inject JS hook into LuCI theme headers on install This enables CrowdSec to detect and block brute-force attacks on the LuCI web interface, which previously only logged successful authentications. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
da5b88110a
commit
22b344225c
@ -4,7 +4,7 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=secubox-auth-logger
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_VERSION:=1.1.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_ARCH:=all
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
@ -16,30 +16,60 @@ define Package/secubox-auth-logger
|
||||
SECTION:=secubox
|
||||
CATEGORY:=SecuBox
|
||||
TITLE:=Authentication Failure Logger for CrowdSec
|
||||
DEPENDS:=+rpcd +uhttpd
|
||||
DEPENDS:=+rpcd +uhttpd +libubox-lua
|
||||
PKGARCH:=all
|
||||
endef
|
||||
|
||||
define Package/secubox-auth-logger/description
|
||||
Logs authentication failures from LuCI/rpcd and Dropbear SSH
|
||||
for CrowdSec detection. Patches rpcd to emit auth failure logs
|
||||
to syslog in a format CrowdSec can parse.
|
||||
for CrowdSec detection. Includes:
|
||||
- SSH failure monitoring (OpenSSH/Dropbear)
|
||||
- LuCI web interface auth failure logging via CGI hook
|
||||
- JavaScript hook to intercept login failures
|
||||
- CrowdSec parser and bruteforce scenario
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/secubox-auth-logger/install
|
||||
# Auth monitor script
|
||||
$(INSTALL_DIR) $(1)/usr/lib/secubox
|
||||
$(INSTALL_BIN) ./files/auth-monitor.sh $(1)/usr/lib/secubox/
|
||||
|
||||
# Init script
|
||||
$(INSTALL_DIR) $(1)/etc/init.d
|
||||
$(INSTALL_BIN) ./files/secubox-auth-logger.init $(1)/etc/init.d/secubox-auth-logger
|
||||
|
||||
# RPCD plugin for auth logging via ubus
|
||||
$(INSTALL_DIR) $(1)/usr/libexec/rpcd
|
||||
$(INSTALL_BIN) ./files/secubox.auth-logger $(1)/usr/libexec/rpcd/
|
||||
|
||||
# ACL for rpcd permissions
|
||||
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
|
||||
$(INSTALL_DATA) ./files/luci-secubox-auth.acl.json $(1)/usr/share/rpcd/acl.d/
|
||||
|
||||
# CGI hook for getting client IP during auth
|
||||
$(INSTALL_DIR) $(1)/www/cgi-bin
|
||||
$(INSTALL_BIN) ./files/auth-hook.cgi $(1)/www/cgi-bin/secubox-auth-hook
|
||||
|
||||
# JavaScript hook for LuCI login interception
|
||||
$(INSTALL_DIR) $(1)/www/luci-static/resources/secubox
|
||||
$(INSTALL_DATA) ./files/secubox-auth-hook.js $(1)/www/luci-static/resources/secubox/
|
||||
|
||||
# CrowdSec parser
|
||||
$(INSTALL_DIR) $(1)/etc/crowdsec/parsers/s01-parse
|
||||
$(INSTALL_DATA) ./files/openwrt-luci-auth.yaml $(1)/etc/crowdsec/parsers/s01-parse/
|
||||
|
||||
# CrowdSec scenario
|
||||
$(INSTALL_DIR) $(1)/etc/crowdsec/scenarios
|
||||
$(INSTALL_DATA) ./files/openwrt-luci-bf.yaml $(1)/etc/crowdsec/scenarios/
|
||||
|
||||
# CrowdSec acquisition config
|
||||
$(INSTALL_DIR) $(1)/etc/crowdsec/acquis.d
|
||||
$(INSTALL_DATA) ./files/secubox-auth-acquis.yaml $(1)/etc/crowdsec/acquis.d/
|
||||
|
||||
# UCI defaults for first boot setup
|
||||
$(INSTALL_DIR) $(1)/etc/uci-defaults
|
||||
$(INSTALL_BIN) ./files/99-secubox-auth-logger $(1)/etc/uci-defaults/
|
||||
endef
|
||||
@ -47,8 +77,28 @@ endef
|
||||
define Package/secubox-auth-logger/postinst
|
||||
#!/bin/sh
|
||||
[ -n "$${IPKG_INSTROOT}" ] || {
|
||||
# Restart rpcd to load new plugin
|
||||
/etc/init.d/rpcd restart 2>/dev/null
|
||||
|
||||
# Enable and start auth monitor
|
||||
/etc/init.d/secubox-auth-logger enable
|
||||
/etc/init.d/secubox-auth-logger start
|
||||
|
||||
# Run uci-defaults to inject JS hook
|
||||
/etc/uci-defaults/99-secubox-auth-logger 2>/dev/null || true
|
||||
|
||||
echo "SecuBox Auth Logger installed - LuCI login failures now logged for CrowdSec"
|
||||
}
|
||||
exit 0
|
||||
endef
|
||||
|
||||
define Package/secubox-auth-logger/postrm
|
||||
#!/bin/sh
|
||||
[ -n "$${IPKG_INSTROOT}" ] || {
|
||||
# Remove JS hook from LuCI header
|
||||
if [ -f /usr/lib/lua/luci/view/themes/bootstrap/header.htm ]; then
|
||||
sed -i '/secubox-auth-hook/d' /usr/lib/lua/luci/view/themes/bootstrap/header.htm 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
exit 0
|
||||
endef
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
#!/bin/sh
|
||||
# SecuBox Auth Logger - Post-install configuration
|
||||
# Enables verbose logging for uhttpd and configures CrowdSec
|
||||
# Copyright (C) 2024 CyberMind.fr
|
||||
|
||||
# Note: Dropbear 2024.86 does NOT support -v flag
|
||||
# Auth monitoring relies on parsing existing syslog messages
|
||||
@ -17,6 +18,55 @@ fi
|
||||
touch /var/log/secubox-auth.log
|
||||
chmod 644 /var/log/secubox-auth.log
|
||||
|
||||
# Inject JS hook into LuCI login page
|
||||
# Try multiple locations for different LuCI versions/themes
|
||||
inject_js_hook() {
|
||||
local hook_script='<script src="/luci-static/resources/secubox/secubox-auth-hook.js"></script>'
|
||||
local hook_marker="secubox-auth-hook"
|
||||
|
||||
# Method 1: Bootstrap theme header (LuCI 19.x+)
|
||||
if [ -f /usr/lib/lua/luci/view/themes/bootstrap/header.htm ]; then
|
||||
if ! grep -q "$hook_marker" /usr/lib/lua/luci/view/themes/bootstrap/header.htm 2>/dev/null; then
|
||||
sed -i "s|</head>|$hook_script\n</head>|" /usr/lib/lua/luci/view/themes/bootstrap/header.htm 2>/dev/null
|
||||
fi
|
||||
fi
|
||||
|
||||
# Method 2: Material theme header
|
||||
if [ -f /usr/lib/lua/luci/view/themes/material/header.htm ]; then
|
||||
if ! grep -q "$hook_marker" /usr/lib/lua/luci/view/themes/material/header.htm 2>/dev/null; then
|
||||
sed -i "s|</head>|$hook_script\n</head>|" /usr/lib/lua/luci/view/themes/material/header.htm 2>/dev/null
|
||||
fi
|
||||
fi
|
||||
|
||||
# Method 3: OpenWrt theme header
|
||||
if [ -f /usr/lib/lua/luci/view/themes/openwrt/header.htm ]; then
|
||||
if ! grep -q "$hook_marker" /usr/lib/lua/luci/view/themes/openwrt/header.htm 2>/dev/null; then
|
||||
sed -i "s|</head>|$hook_script\n</head>|" /usr/lib/lua/luci/view/themes/openwrt/header.htm 2>/dev/null
|
||||
fi
|
||||
fi
|
||||
|
||||
# Method 4: Base sysauth view (fallback for login page)
|
||||
if [ -f /usr/lib/lua/luci/view/sysauth.htm ]; then
|
||||
if ! grep -q "$hook_marker" /usr/lib/lua/luci/view/sysauth.htm 2>/dev/null; then
|
||||
sed -i "s|</head>|$hook_script\n</head>|" /usr/lib/lua/luci/view/sysauth.htm 2>/dev/null
|
||||
fi
|
||||
fi
|
||||
|
||||
# Method 5: LuCI2 / luci-mod-admin-full footer
|
||||
if [ -f /www/luci-static/resources/footer.htm ]; then
|
||||
if ! grep -q "$hook_marker" /www/luci-static/resources/footer.htm 2>/dev/null; then
|
||||
echo "$hook_script" >> /www/luci-static/resources/footer.htm 2>/dev/null
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
inject_js_hook
|
||||
|
||||
# Restart rpcd to load new ubus object
|
||||
if [ -x /etc/init.d/rpcd ]; then
|
||||
/etc/init.d/rpcd restart 2>/dev/null
|
||||
fi
|
||||
|
||||
# Restart CrowdSec to pick up new acquisition/parser/scenario
|
||||
if [ -x /etc/init.d/crowdsec ]; then
|
||||
/etc/init.d/crowdsec restart 2>/dev/null
|
||||
|
||||
150
package/secubox/secubox-auth-logger/files/auth-hook.cgi
Normal file
150
package/secubox/secubox-auth-logger/files/auth-hook.cgi
Normal file
@ -0,0 +1,150 @@
|
||||
#!/bin/sh
|
||||
# SecuBox Auth Hook - CGI endpoint for LuCI authentication with logging
|
||||
# Copyright (C) 2024 CyberMind.fr
|
||||
#
|
||||
# This CGI script intercepts login attempts and logs failures with the real client IP
|
||||
# Call via: POST /cgi-bin/secubox-auth-hook
|
||||
#
|
||||
# Request body: {"username":"...", "password":"..."}
|
||||
# Special: If password is "__SECUBOX_LOG_FAILURE__", just log the failure (used by JS hook)
|
||||
# Response: Same as ubus session.login
|
||||
|
||||
. /usr/share/libubox/jshn.sh
|
||||
|
||||
LOG_FILE="/var/log/secubox-auth.log"
|
||||
LOG_TAG="secubox-auth"
|
||||
|
||||
# Get client IP from CGI environment
|
||||
CLIENT_IP="${REMOTE_ADDR:-127.0.0.1}"
|
||||
|
||||
# Handle X-Forwarded-For if present (reverse proxy)
|
||||
if [ -n "$HTTP_X_FORWARDED_FOR" ]; then
|
||||
CLIENT_IP="${HTTP_X_FORWARDED_FOR%%,*}"
|
||||
fi
|
||||
|
||||
# Sanitize IP (remove IPv6 brackets if present)
|
||||
CLIENT_IP=$(echo "$CLIENT_IP" | sed 's/^\[//;s/\]$//')
|
||||
|
||||
# Log authentication failure
|
||||
log_failure() {
|
||||
local user="$1"
|
||||
local ts=$(date "+%b %d %H:%M:%S")
|
||||
local hostname=$(cat /proc/sys/kernel/hostname 2>/dev/null || echo "OpenWrt")
|
||||
|
||||
# Ensure log file exists
|
||||
[ -f "$LOG_FILE" ] || { touch "$LOG_FILE"; chmod 644 "$LOG_FILE"; }
|
||||
|
||||
# Log to dedicated file for CrowdSec
|
||||
echo "$ts $hostname $LOG_TAG[$$]: authentication failure for $user from $CLIENT_IP via luci" >> "$LOG_FILE"
|
||||
|
||||
# Also log to syslog
|
||||
logger -t "$LOG_TAG" -p auth.warning "authentication failure for $user from $CLIENT_IP via luci"
|
||||
}
|
||||
|
||||
# Output HTTP headers
|
||||
echo "Content-Type: application/json"
|
||||
echo ""
|
||||
|
||||
# Handle GET request (for IP detection only)
|
||||
if [ "$REQUEST_METHOD" = "GET" ]; then
|
||||
json_init
|
||||
json_add_string ip "$CLIENT_IP"
|
||||
json_add_string method "GET"
|
||||
json_dump
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Handle POST request (login attempt or log-only)
|
||||
if [ "$REQUEST_METHOD" = "POST" ]; then
|
||||
# Read POST body
|
||||
read -r body
|
||||
|
||||
# Parse JSON input
|
||||
json_load "$body" 2>/dev/null
|
||||
if [ $? -ne 0 ]; then
|
||||
json_init
|
||||
json_add_boolean success 0
|
||||
json_add_string error "Invalid JSON"
|
||||
json_dump
|
||||
exit 0
|
||||
fi
|
||||
|
||||
json_get_var username username
|
||||
json_get_var password password
|
||||
|
||||
# Validate input
|
||||
if [ -z "$username" ]; then
|
||||
json_init
|
||||
json_add_boolean success 0
|
||||
json_add_string error "Missing username"
|
||||
json_dump
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if this is a log-only request from our JS hook
|
||||
if [ "$password" = "__SECUBOX_LOG_FAILURE__" ]; then
|
||||
# Just log the failure - don't attempt login
|
||||
log_failure "$username"
|
||||
json_init
|
||||
json_add_boolean success 1
|
||||
json_add_string message "Auth failure logged"
|
||||
json_add_string ip "$CLIENT_IP"
|
||||
json_add_string username "$username"
|
||||
json_dump
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Normal login flow - validate password
|
||||
if [ -z "$password" ]; then
|
||||
json_init
|
||||
json_add_boolean success 0
|
||||
json_add_string error "Missing password"
|
||||
json_dump
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Attempt login via ubus
|
||||
result=$(ubus call session login "{\"username\":\"$username\",\"password\":\"$password\"}" 2>&1)
|
||||
rc=$?
|
||||
|
||||
# Check if login failed
|
||||
# ubus returns error code or empty session on failure
|
||||
if [ $rc -ne 0 ]; then
|
||||
# ubus call failed
|
||||
log_failure "$username"
|
||||
json_init
|
||||
json_add_boolean success 0
|
||||
json_add_string error "Authentication failed"
|
||||
json_add_string ip "$CLIENT_IP"
|
||||
json_dump
|
||||
elif echo "$result" | grep -q '"ubus_rpc_session": ""'; then
|
||||
# Empty session token = failed login
|
||||
log_failure "$username"
|
||||
json_init
|
||||
json_add_boolean success 0
|
||||
json_add_string error "Invalid credentials"
|
||||
json_add_string ip "$CLIENT_IP"
|
||||
json_dump
|
||||
else
|
||||
# Check if result contains valid session
|
||||
session=$(echo "$result" | jsonfilter -e '@.ubus_rpc_session' 2>/dev/null)
|
||||
if [ -z "$session" ] || [ "$session" = "null" ]; then
|
||||
log_failure "$username"
|
||||
json_init
|
||||
json_add_boolean success 0
|
||||
json_add_string error "Invalid credentials"
|
||||
json_add_string ip "$CLIENT_IP"
|
||||
json_dump
|
||||
else
|
||||
# Login successful - return the session info
|
||||
echo "$result"
|
||||
fi
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Unsupported method
|
||||
json_init
|
||||
json_add_boolean success 0
|
||||
json_add_string error "Unsupported method"
|
||||
json_dump
|
||||
@ -0,0 +1,15 @@
|
||||
{
|
||||
"secubox-auth-logger": {
|
||||
"description": "SecuBox Authentication Logger",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"secubox.auth-logger": ["log_failure", "get_client_info", "wrapped_login"]
|
||||
}
|
||||
},
|
||||
"write": {
|
||||
"ubus": {
|
||||
"secubox.auth-logger": ["log_failure", "wrapped_login"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
# CrowdSec Parser for SecuBox Auth Logger
|
||||
# Parses authentication failures from LuCI/uhttpd and Dropbear
|
||||
# Format: secubox-auth: Authentication failure for <user> from <ip> via <service>
|
||||
# Parses authentication failures from LuCI/uhttpd and SSH (OpenSSH/Dropbear)
|
||||
# Format: secubox-auth[pid]: authentication failure for <user> from <ip> via <service>
|
||||
|
||||
name: secubox/openwrt-luci-auth
|
||||
description: "Parse SecuBox auth failure logs for LuCI and SSH"
|
||||
@ -9,7 +9,8 @@ onsuccess: next_stage
|
||||
|
||||
nodes:
|
||||
- grok:
|
||||
pattern: "Authentication failure for %{USERNAME:user} from %{IP:source_ip} via %{WORD:service}"
|
||||
# Case-insensitive match for "authentication failure"
|
||||
pattern: "(?i)authentication failure for %{USERNAME:user} from %{IP:source_ip} via %{WORD:service}"
|
||||
apply_on: message
|
||||
statics:
|
||||
- meta: log_type
|
||||
@ -18,3 +19,5 @@ nodes:
|
||||
expression: evt.Parsed.service
|
||||
- meta: source_ip
|
||||
expression: evt.Parsed.source_ip
|
||||
- meta: username
|
||||
expression: evt.Parsed.user
|
||||
|
||||
194
package/secubox/secubox-auth-logger/files/secubox-auth-hook.js
Normal file
194
package/secubox/secubox-auth-logger/files/secubox-auth-hook.js
Normal file
@ -0,0 +1,194 @@
|
||||
/**
|
||||
* SecuBox Auth Hook - Intercepts LuCI login failures for CrowdSec
|
||||
* Copyright (C) 2024 CyberMind.fr
|
||||
*
|
||||
* This script hooks into LuCI's authentication system to log
|
||||
* failed login attempts with the real client IP address.
|
||||
*
|
||||
* The hook intercepts XMLHttpRequest calls to session.login and
|
||||
* reports failures to our CGI endpoint which has access to REMOTE_ADDR.
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Only run once
|
||||
if (window._secuboxAuthHookLoaded) return;
|
||||
window._secuboxAuthHookLoaded = true;
|
||||
|
||||
var AUTH_HOOK_URL = '/cgi-bin/secubox-auth-hook';
|
||||
|
||||
// Debounce to avoid multiple logs for same attempt
|
||||
var lastLogTime = 0;
|
||||
var lastLogUser = '';
|
||||
|
||||
/**
|
||||
* Log auth failure to our CGI endpoint
|
||||
* The CGI endpoint gets REMOTE_ADDR and logs with real client IP
|
||||
*/
|
||||
function logAuthFailure(username) {
|
||||
var now = Date.now();
|
||||
// Debounce: don't log same user within 2 seconds
|
||||
if (username === lastLogUser && (now - lastLogTime) < 2000) {
|
||||
return;
|
||||
}
|
||||
lastLogTime = now;
|
||||
lastLogUser = username;
|
||||
|
||||
try {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', AUTH_HOOK_URL, true);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
// Send with dummy password - CGI will detect it's a log-only call
|
||||
// and just log the failure with the real client IP
|
||||
xhr.send(JSON.stringify({
|
||||
username: username || 'root',
|
||||
password: '__SECUBOX_LOG_FAILURE__'
|
||||
}));
|
||||
} catch (e) {
|
||||
// Silently fail - don't break login flow
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a ubus response indicates login failure
|
||||
*/
|
||||
function isLoginFailure(result) {
|
||||
if (!result) return false;
|
||||
|
||||
// UBUS JSON-RPC response format: { result: [error_code, data] }
|
||||
if (result.result && Array.isArray(result.result)) {
|
||||
var errorCode = result.result[0];
|
||||
var data = result.result[1];
|
||||
|
||||
// Error code != 0 means failure
|
||||
if (errorCode !== 0) return true;
|
||||
|
||||
// Check for empty session (credential failure)
|
||||
if (data && data.ubus_rpc_session === '') return true;
|
||||
if (data && !data.ubus_rpc_session) return true;
|
||||
}
|
||||
|
||||
// Check for error response
|
||||
if (result.error) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract username from login call params
|
||||
*/
|
||||
function extractUsername(call) {
|
||||
try {
|
||||
if (call.params && call.params[2] && call.params[2].username) {
|
||||
return call.params[2].username;
|
||||
}
|
||||
} catch (e) {}
|
||||
return 'root';
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercept fetch API calls (modern LuCI)
|
||||
*/
|
||||
var originalFetch = window.fetch;
|
||||
if (originalFetch) {
|
||||
window.fetch = function(url, options) {
|
||||
var requestBody = null;
|
||||
var loginCalls = [];
|
||||
|
||||
// Parse request to find login calls
|
||||
if (url && url.indexOf('ubus') !== -1 && options && options.body) {
|
||||
try {
|
||||
requestBody = JSON.parse(options.body);
|
||||
if (Array.isArray(requestBody)) {
|
||||
requestBody.forEach(function(call, idx) {
|
||||
if (call && call.method === 'call' &&
|
||||
call.params && call.params[1] === 'login') {
|
||||
loginCalls.push({ call: call, idx: idx });
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
return originalFetch.apply(this, arguments).then(function(response) {
|
||||
// Check login results
|
||||
if (loginCalls.length > 0) {
|
||||
response.clone().json().then(function(data) {
|
||||
if (Array.isArray(data)) {
|
||||
loginCalls.forEach(function(item) {
|
||||
var result = data[item.idx];
|
||||
if (isLoginFailure(result)) {
|
||||
logAuthFailure(extractUsername(item.call));
|
||||
}
|
||||
});
|
||||
}
|
||||
}).catch(function() {});
|
||||
}
|
||||
return response;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercept XMLHttpRequest (older LuCI versions)
|
||||
*/
|
||||
var originalXHRSend = XMLHttpRequest.prototype.send;
|
||||
var originalXHROpen = XMLHttpRequest.prototype.open;
|
||||
|
||||
XMLHttpRequest.prototype.open = function(method, url) {
|
||||
this._secuboxUrl = url;
|
||||
this._secuboxMethod = method;
|
||||
return originalXHROpen.apply(this, arguments);
|
||||
};
|
||||
|
||||
XMLHttpRequest.prototype.send = function(body) {
|
||||
var xhr = this;
|
||||
var url = this._secuboxUrl;
|
||||
var loginCalls = [];
|
||||
|
||||
// Only intercept POST to ubus
|
||||
if (this._secuboxMethod === 'POST' && url && url.indexOf('ubus') !== -1 && body) {
|
||||
try {
|
||||
var parsedBody = JSON.parse(body);
|
||||
if (Array.isArray(parsedBody)) {
|
||||
parsedBody.forEach(function(call, idx) {
|
||||
if (call && call.method === 'call' &&
|
||||
call.params && call.params[1] === 'login') {
|
||||
loginCalls.push({ call: call, idx: idx });
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
if (loginCalls.length > 0) {
|
||||
var originalOnReadyStateChange = xhr.onreadystatechange;
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState === 4 && xhr.status === 200) {
|
||||
try {
|
||||
var response = JSON.parse(xhr.responseText);
|
||||
if (Array.isArray(response)) {
|
||||
loginCalls.forEach(function(item) {
|
||||
var result = response[item.idx];
|
||||
if (isLoginFailure(result)) {
|
||||
logAuthFailure(extractUsername(item.call));
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
if (originalOnReadyStateChange) {
|
||||
originalOnReadyStateChange.apply(this, arguments);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return originalXHRSend.apply(this, arguments);
|
||||
};
|
||||
|
||||
// Debug message in console
|
||||
if (window.console && console.log) {
|
||||
console.log('[SecuBox] Auth hook v1.1 loaded - LuCI login failures will be logged for CrowdSec');
|
||||
}
|
||||
})();
|
||||
143
package/secubox/secubox-auth-logger/files/secubox.auth-logger
Normal file
143
package/secubox/secubox-auth-logger/files/secubox.auth-logger
Normal file
@ -0,0 +1,143 @@
|
||||
#!/bin/sh
|
||||
# SecuBox Auth Logger - RPCD plugin for LuCI authentication logging
|
||||
# Copyright (C) 2024 CyberMind.fr
|
||||
#
|
||||
# This plugin wraps session.login to log authentication failures
|
||||
# for CrowdSec detection.
|
||||
|
||||
. /lib/functions.sh
|
||||
. /usr/share/libubox/jshn.sh
|
||||
|
||||
LOG_FILE="/var/log/secubox-auth.log"
|
||||
LOG_TAG="secubox-auth"
|
||||
|
||||
# Ensure log file exists
|
||||
[ -f "$LOG_FILE" ] || { touch "$LOG_FILE"; chmod 644 "$LOG_FILE"; }
|
||||
|
||||
# Log authentication failure
|
||||
log_auth_failure() {
|
||||
local ip="$1"
|
||||
local user="${2:-root}"
|
||||
local service="${3:-luci}"
|
||||
|
||||
# Get timestamp in syslog format
|
||||
local ts=$(date "+%b %d %H:%M:%S")
|
||||
local hostname=$(cat /proc/sys/kernel/hostname 2>/dev/null || echo "OpenWrt")
|
||||
|
||||
# Log to dedicated file for CrowdSec
|
||||
echo "$ts $hostname $LOG_TAG[$$]: authentication failure for $user from $ip via $service" >> "$LOG_FILE"
|
||||
|
||||
# Also log to syslog for visibility
|
||||
logger -t "$LOG_TAG" -p auth.warning "authentication failure for $user from $ip via $service"
|
||||
}
|
||||
|
||||
# Get client IP from environment or connection info
|
||||
get_client_ip() {
|
||||
# Try various methods to get client IP
|
||||
|
||||
# Method 1: REMOTE_ADDR (if called via CGI)
|
||||
[ -n "$REMOTE_ADDR" ] && echo "$REMOTE_ADDR" && return
|
||||
|
||||
# Method 2: HTTP_X_FORWARDED_FOR (if behind proxy)
|
||||
[ -n "$HTTP_X_FORWARDED_FOR" ] && echo "${HTTP_X_FORWARDED_FOR%%,*}" && return
|
||||
|
||||
# Method 3: Parse from uhttpd connection (not available in rpcd context)
|
||||
# Method 4: Default to local if unknown
|
||||
echo "127.0.0.1"
|
||||
}
|
||||
|
||||
# Perform login via ubus and return result
|
||||
do_login() {
|
||||
local username="$1"
|
||||
local password="$2"
|
||||
local client_ip="$3"
|
||||
|
||||
# Call the real session.login via ubus
|
||||
local result
|
||||
result=$(ubus call session login "{\"username\":\"$username\",\"password\":\"$password\"}" 2>&1)
|
||||
local rc=$?
|
||||
|
||||
if [ $rc -ne 0 ] || echo "$result" | grep -q '"ubus_rpc_session": ""' || echo "$result" | grep -qi "error\|denied"; then
|
||||
# Login failed - log it
|
||||
log_auth_failure "$client_ip" "$username" "luci"
|
||||
|
||||
# Return failure
|
||||
json_init
|
||||
json_add_boolean success 0
|
||||
json_add_string error "Login failed"
|
||||
json_add_string ip "$client_ip"
|
||||
json_dump
|
||||
else
|
||||
# Login succeeded - return the session token
|
||||
echo "$result"
|
||||
fi
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
list)
|
||||
# List available methods
|
||||
cat <<'EOF'
|
||||
{
|
||||
"wrapped_login": {
|
||||
"username": "str",
|
||||
"password": "str",
|
||||
"client_ip": "str"
|
||||
},
|
||||
"log_failure": {
|
||||
"ip": "str",
|
||||
"username": "str",
|
||||
"service": "str"
|
||||
},
|
||||
"get_client_info": {}
|
||||
}
|
||||
EOF
|
||||
;;
|
||||
call)
|
||||
case "$2" in
|
||||
wrapped_login)
|
||||
# Parse JSON input
|
||||
read -r input
|
||||
json_load "$input"
|
||||
json_get_var username username
|
||||
json_get_var password password
|
||||
json_get_var client_ip client_ip
|
||||
|
||||
# Use provided IP or try to detect
|
||||
[ -z "$client_ip" ] && client_ip=$(get_client_ip)
|
||||
|
||||
# Perform login with logging
|
||||
do_login "$username" "$password" "$client_ip"
|
||||
;;
|
||||
|
||||
log_failure)
|
||||
# Direct logging method for JS to call
|
||||
read -r input
|
||||
json_load "$input"
|
||||
json_get_var ip ip
|
||||
json_get_var username username
|
||||
json_get_var service service
|
||||
|
||||
[ -z "$ip" ] && ip="unknown"
|
||||
[ -z "$username" ] && username="root"
|
||||
[ -z "$service" ] && service="luci"
|
||||
|
||||
log_auth_failure "$ip" "$username" "$service"
|
||||
|
||||
json_init
|
||||
json_add_boolean success 1
|
||||
json_add_string message "Auth failure logged for $ip"
|
||||
json_dump
|
||||
;;
|
||||
|
||||
get_client_info)
|
||||
# Return whatever client info we can detect
|
||||
local ip=$(get_client_ip)
|
||||
|
||||
json_init
|
||||
json_add_string ip "$ip"
|
||||
json_add_string remote_addr "${REMOTE_ADDR:-unknown}"
|
||||
json_dump
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
Loading…
Reference in New Issue
Block a user