Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,13 @@ RUST_BACKTRACE=full
# PHP2Shell (CVE-2026-109618, CVE-2026-109619): alert on PHP builds older than
# the fixed releases, on the host and in running containers. Alert only.
#STACKDOG_PHP_VERSION_CHECK_ENABLED=true
# SSHole (CVE-2026-123158): flag OpenSSH builds older than 10.6p2 on musl
# systems (Alpine's default sshd is the vulnerable configuration), and ban
# password logins to key-only accounts. That login line is what a successful
# SSHole attack leaves behind.
#STACKDOG_SSHOLE_ENABLED=true
# Accounts that should never accept a password login. Defaults to root,alpine.
#STACKDOG_SSHOLE_KEYONLY_USERS=root,alpine

# ---------------------------------------------------------------------------
# Dashboard container (docker-compose.yml / docker-compose.app.yml)
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- SSHole detection (CVE-2026-123158). `vuln.sshole` flags OpenSSH builds older
than 10.6p2 in the apk package database, on the host and in running
containers: that is the vulnerable class (musl, passwords checked without
PAM, Alpine's default). The PAM flavor and glibc distributions are not
affected and are not reported. `ssh.password-login` alerts on password
logins to key-only accounts (root, alpine by default,
`STACKDOG_SSHOLE_KEYONLY_USERS` extends the list) and feeds the ban engine:
that line is what a successful SSHole login leaves in the logs. A single
line bans the address on the spot (allowlists and protected addresses
exempt): a foothold must not wait for the offense threshold.
`STACKDOG_SSHOLE_ENABLED=false` turns both off.

## [0.2.9] - 2026-10-31

### Added
Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,25 @@ exploit traffic. Limits:
Crash lines come from php-fpm and Apache error logs too; add host logs such as
`/var/log/php8.3-fpm.log`. Stackdog cannot patch PHP: upgrading is the fix.

#### SSHole exposure (CVE-2026-123158)

SSHole is an authentication bypass in OpenSSH on musl systems with PAM disabled
(Alpine's default server): an oversized password logs the attacker into accounts
whose password field is `*`, such as root. Fixed in OpenSSH 10.6p2.

| Finding | When | Action |
|---|---|---|
| `vuln.sshole` | an `openssh-server` build older than 10.6p2 in the apk package database, on the host or in a running container | Critical alert naming the version; upgrade to 10.6p2 (`10.6_p2` on Alpine) and restart sshd. Never bans: a version names no attacker |
| `ssh.password-login` | a password login to a key-only account (root, alpine by default) | alert + an immediate ban on the first line (allowlists and protected addresses still apply). One login is already a foothold: waiting for five offenses would let the intruder switch the monitoring off |

The version check reads apk databases only: musl is the affected half of the
configuration, and the PAM flavor (`openssh-server-pam`) and glibc
distributions are not affected by the CVE. The log half reads the auth and
syslog sources the brute-force bans come from — on Alpine, sshd logs to
`/var/log/messages`. Key-only accounts are `root` and `alpine`; add yours with
`STACKDOG_SSHOLE_KEYONLY_USERS`. Stackdog cannot patch OpenSSH: upgrading is
the fix.

---

## 📦 Installation
Expand Down
17 changes: 17 additions & 0 deletions src/detectors/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ mod integrity;
mod ml;
pub mod php2shell;
pub mod php_shell;
pub mod sshole;

use std::collections::HashSet;

Expand All @@ -26,6 +27,7 @@ use self::php2shell::{Php2ShellAttemptDetector, PhpVersionMonitor, PhpWorkerCras
use self::php_shell::{
ContainerFilesystem, PhpShellAccessDetector, PhpShellOptions, PhpShellScanner, ScanRoot,
};
use self::sshole::{SshPasswordLoginDetector, SshVersionMonitor};
use crate::database::connection::DbPool;
use crate::sniff::analyzer::{AnomalySeverity, LogAnomaly};
use crate::sniff::reader::LogEntry;
Expand Down Expand Up @@ -103,6 +105,7 @@ pub struct DetectorRegistry {
docker_posture_monitor: DockerPostureMonitor,
php_shell_scanner: PhpShellScanner,
php_version_monitor: PhpVersionMonitor,
sshole_monitor: SshVersionMonitor,
}

impl DetectorRegistry {
Expand All @@ -115,6 +118,7 @@ impl DetectorRegistry {
docker_posture_monitor: DockerPostureMonitor,
php_shell_scanner: PhpShellScanner::new(),
php_version_monitor: PhpVersionMonitor::new(),
sshole_monitor: SshVersionMonitor::new(),
}
}

Expand Down Expand Up @@ -147,6 +151,7 @@ impl DetectorRegistry {
));
self.register(PhpWorkerCrashDetector);
self.register(Php2ShellAttemptDetector);
self.register(SshPasswordLoginDetector::new());
}

pub fn detect_log_anomalies(&self, entries: &[LogEntry]) -> Vec<LogAnomaly> {
Expand Down Expand Up @@ -207,6 +212,18 @@ impl DetectorRegistry {
.collect()
}

pub fn detect_sshole_anomalies(
&self,
containers: &[ContainerFilesystem],
host_root: Option<&str>,
) -> Vec<LogAnomaly> {
self.sshole_monitor
.detect(containers, host_root)
.into_iter()
.map(|finding| finding.to_log_anomaly())
.collect()
}

pub fn detect_config_assessment_anomalies(&self, paths: &[String]) -> Result<Vec<LogAnomaly>> {
Ok(self
.config_assessment_monitor
Expand Down
2 changes: 1 addition & 1 deletion src/detectors/php2shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ pub fn findings(installs: &[PhpInstall]) -> Vec<DetectorFinding> {
),
};
let description = format!(
"{}. Until then, session.upload_progress.enabled = 0 closes the confirmed php-fpm path",
"{}. Until then, session.upload_progress.enabled = 0 closes the confirmed php-fpm path, and Stackdog keeps defending this box: it bans clients whose requests trip the startup out-of-memory step, scans the web roots for shells, and bans whoever drives a flagged file",
description
);

Expand Down
Loading
Loading