diff --git a/package/secubox/secubox-auth-logger/Makefile b/package/secubox/secubox-auth-logger/Makefile index 8a01d23c..cce49c31 100644 --- a/package/secubox/secubox-auth-logger/Makefile +++ b/package/secubox/secubox-auth-logger/Makefile @@ -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 diff --git a/package/secubox/secubox-auth-logger/files/99-secubox-auth-logger b/package/secubox/secubox-auth-logger/files/99-secubox-auth-logger index c8b8af42..e60087cc 100644 --- a/package/secubox/secubox-auth-logger/files/99-secubox-auth-logger +++ b/package/secubox/secubox-auth-logger/files/99-secubox-auth-logger @@ -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='' + 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||$hook_script\n|" /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||$hook_script\n|" /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||$hook_script\n|" /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||$hook_script\n|" /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 diff --git a/package/secubox/secubox-auth-logger/files/auth-hook.cgi b/package/secubox/secubox-auth-logger/files/auth-hook.cgi new file mode 100644 index 00000000..18f31b04 --- /dev/null +++ b/package/secubox/secubox-auth-logger/files/auth-hook.cgi @@ -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 diff --git a/package/secubox/secubox-auth-logger/files/luci-secubox-auth.acl.json b/package/secubox/secubox-auth-logger/files/luci-secubox-auth.acl.json new file mode 100644 index 00000000..68a72ce7 --- /dev/null +++ b/package/secubox/secubox-auth-logger/files/luci-secubox-auth.acl.json @@ -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"] + } + } + } +} diff --git a/package/secubox/secubox-auth-logger/files/openwrt-luci-auth.yaml b/package/secubox/secubox-auth-logger/files/openwrt-luci-auth.yaml index 379011e0..3064060f 100644 --- a/package/secubox/secubox-auth-logger/files/openwrt-luci-auth.yaml +++ b/package/secubox/secubox-auth-logger/files/openwrt-luci-auth.yaml @@ -1,6 +1,6 @@ # CrowdSec Parser for SecuBox Auth Logger -# Parses authentication failures from LuCI/uhttpd and Dropbear -# Format: secubox-auth: Authentication failure for from via +# Parses authentication failures from LuCI/uhttpd and SSH (OpenSSH/Dropbear) +# Format: secubox-auth[pid]: authentication failure for from via 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 diff --git a/package/secubox/secubox-auth-logger/files/secubox-auth-hook.js b/package/secubox/secubox-auth-logger/files/secubox-auth-hook.js new file mode 100644 index 00000000..169baf87 --- /dev/null +++ b/package/secubox/secubox-auth-logger/files/secubox-auth-hook.js @@ -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'); + } +})(); diff --git a/package/secubox/secubox-auth-logger/files/secubox.auth-logger b/package/secubox/secubox-auth-logger/files/secubox.auth-logger new file mode 100644 index 00000000..6a043419 --- /dev/null +++ b/package/secubox/secubox-auth-logger/files/secubox.auth-logger @@ -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