Skip to content

Commit 0896536

Browse files
committed
pam-ssh-add: Fix insecure killing of session ssh-agent [CVE-2024-6126]
Some distributions like Debian 12, or possibly some administrators enable pam_env's deprecated `user_readenv` option [1]. The user session can change the `$SSH_AGENT_PID`, so that it can pass an arbitrary pid to `pam_sm_close_session()`. This is a local authenticated DoS. Avoid this by storing the agent pid in a global variable. The cockpit-session process stays around for the entire session time, so we don't need to put the pid into the PAM data. It can also happen that the user session's ssh-agent gets killed, and some other process later on recycles the PID. Temporarily drop privileges to the target user so that we at least don't kill anyone else's process. Add an integration test which checks that changing the env variable works, pointing it to a different process doesn't kill that, and ssh-agent (the original pid) is still cleaned up correctly. However, as pam_so.env in Fedora crashes hard, skip the test there. Many thanks to Paolo Perego <[email protected]> for discovering, and Luna Dragon <[email protected]> for reporting this issue! [1] https://man7.org/linux/man-pages/man8/pam_env.8.html CVE-2024-6126 https://bugzilla.redhat.com/show_bug.cgi?id=2290859
1 parent c4fa007 commit 0896536

File tree

2 files changed

+70
-9
lines changed

2 files changed

+70
-9
lines changed

src/pam-ssh-add/pam-ssh-add.c

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ const char *pam_ssh_agent_arg = NULL;
5454
const char *pam_ssh_add_program = PATH_SSH_ADD;
5555
const char *pam_ssh_add_arg = NULL;
5656

57+
static unsigned long ssh_agent_pid;
58+
static uid_t ssh_agent_uid;
59+
5760
/* Environment */
5861
#define ENVIRON_SIZE 5
5962
#define PATH "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
@@ -866,6 +869,25 @@ start_agent (pam_handle_t *pamh,
866869
error ("couldn't set agent environment: %s",
867870
pam_strerror (pamh, res));
868871
}
872+
873+
/* parse and store the agent pid for later cleanup */
874+
if (strncmp (auth_pid, "SSH_AGENT_PID=", 14) == 0)
875+
{
876+
unsigned long pid = strtoul (auth_pid + 14, NULL, 10);
877+
if (pid > 0 && pid != ULONG_MAX)
878+
{
879+
ssh_agent_pid = pid;
880+
ssh_agent_uid = auth_pwd->pw_uid;
881+
}
882+
else
883+
{
884+
error ("invalid SSH_AGENT_PID value: %s", auth_pid);
885+
}
886+
}
887+
else
888+
{
889+
error ("unexpected agent pid format: %s", auth_pid);
890+
}
869891
}
870892

871893
free (auth_socket);
@@ -952,19 +974,25 @@ pam_sm_close_session (pam_handle_t *pamh,
952974
int argc,
953975
const char *argv[])
954976
{
955-
const char *s_pid;
956-
int pid = 0;
957977
parse_args (argc, argv);
958978

959979
/* Kill the ssh agent we started */
960-
s_pid = pam_getenv (pamh, "SSH_AGENT_PID");
961-
if (s_pid)
962-
pid = atoi (s_pid);
963-
964-
if (pid > 0)
980+
if (ssh_agent_pid > 0)
965981
{
966-
debug ("Closing %d", pid);
967-
kill (pid, SIGTERM);
982+
debug ("Closing %lu", ssh_agent_pid);
983+
/* kill as user to guard against crashing ssh-agent and PID reuse */
984+
if (setresuid (ssh_agent_uid, ssh_agent_uid, -1) < 0)
985+
{
986+
error ("could not drop privileges for killing ssh agent: %m");
987+
return PAM_SESSION_ERR;
988+
}
989+
if (kill (ssh_agent_pid, SIGTERM) < 0 && errno != ESRCH)
990+
message ("could not kill ssh agent %lu: %m", ssh_agent_pid);
991+
if (setresuid (0, 0, -1) < 0)
992+
{
993+
error ("could not restore privileges after killing ssh agent: %m");
994+
return PAM_SESSION_ERR;
995+
}
968996
}
969997
return PAM_SUCCESS;
970998
}

test/verify/check-session

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,39 @@ class TestSession(testlib.MachineCase):
8686
b.logout()
8787
wait_session(should_exist=False)
8888

89+
# try to pwn $SSH_AGENT_PID via pam_env's user_readenv=1 (CVE-2024-6126)
90+
91+
if m.image in ["fedora-39", "fedora-40", "centos-10", "rhel-10-0"]:
92+
# pam_env user_readenv crashes in Fedora/RHEL 10, skip the test
93+
# https://bugzilla.redhat.com/show_bug.cgi?id=2293045
94+
return
95+
if m.ostree_image:
96+
# not using cockpit's PAM config
97+
return
98+
99+
# this is enabled by default in tools/cockpit.debian.pam, as well as
100+
# Debian/Ubuntu's /etc/pam.d/sshd; but not in Fedora/RHEL
101+
if "debian" not in m.image and "ubuntu" not in m.image:
102+
self.write_file("/etc/pam.d/cockpit", "session required pam_env.so user_readenv=1\n", append=True)
103+
victim_pid = m.spawn("sleep infinity", "sleep.log")
104+
self.addCleanup(m.execute, f"kill {victim_pid} || true")
105+
self.write_file("/home/admin/.pam_environment", f"SSH_AGENT_PID={victim_pid}\n", owner="admin")
106+
107+
b.login_and_go()
108+
wait_session(should_exist=True)
109+
# starts ssh-agent in session
110+
m.execute("pgrep -u admin ssh-agent")
111+
# but the session has the modified SSH_AGENT_PID
112+
bridge = m.execute("pgrep -u admin cockpit-bridge").strip()
113+
agent = m.execute(f"grep --null-data SSH_AGENT_PID /proc/{bridge}/environ | xargs -0 | sed 's/.*=//'").strip()
114+
self.assertEqual(agent, str(victim_pid))
115+
116+
# logging out still kills the actual ssh-agent, not the victim pid
117+
b.logout()
118+
wait_session(should_exist=False)
119+
m.execute("while pgrep -u admin ssh-agent; do sleep 1; done", timeout=10)
120+
m.execute(f"test -e /proc/{victim_pid}")
121+
89122

90123
if __name__ == '__main__':
91124
testlib.test_main()

0 commit comments

Comments
 (0)