Warning
The author does not maintain this report actively. Please see their reports on newer versions of Fedora Linux for up-to-date information and corrections of errors.
by OK Ryoko, revision 2024-11-28.3
Assumed audience: Linux system administrators, Linux utility authors and Fedora Linux package maintainers. Familiarity with process credentials, capabilities, syscalls, strace, Linux PAM and SELinux is assumed.
I dive into all the SUID-root binaries that come with a minimal installation of Fedora Server 38. I also discuss the use of file capabilities to limit the level of privilege attainable by those programs.
Skip ahead to the section titled “The findings at a glance” for a high-level summary of outcomes.
Appendix A expands the abbreviations that appear in this report.
- Introduction
- Characterization of the environment
- Conventions for presenting results
- The SUID-root binaries in detail
- The findings at a glance
- Open questions
- Supporting information
- Feedback and support
- Funding sources
- Licensing
- Revision history
Set-user-ID-root (SUID-root) binaries enable a Linux system’s unprivileged users to perform actions ordinarily reserved for the super user (root). Although important for the day-to-day use of some systems, SUID-root binaries can enable exploits of local privilege escalation vulnerabilities in themselves (e.g., PwnKit) as well as their execution environment (e.g., Looney Tunables).
Most Linux distributions and base container images ship with a handful of SUID-root binaries. A crucial step in securing these systems prior to deployment is to handle these binaries appropriately. Unfortunately, this process is hampered by an absence of specific public data and guidance. To help fill this gap, I examine the SUID-root binaries that come with Fedora Custom OS 38, my current daily driver. For each binary, I answer the following questions with respect to one or two reference use cases:
- Why does it need to be SUID-root?
- Can the SUID bit be substituted by zero or more file capabilities?
- If not, can the SUID bit be supplemented with zero or more file capabilities?
- Does the program drop privileges? If so, when?
- How does the default SELinux policy constrain the level of privilege of the program?
The outcomes of this work aren’t comprehensive. Instead, they establish a baseline of understanding that members of the assumed audience can adapt to their needs.
My findings are for a minimal installation (a “Fedora Custom Operating System” in the Anaconda installer with no additional software selected) of Fedora Server 38 1.6, virtualized using the libvirt 9.0.0 API with the QEMU/KVM driver from the image at the following URL:
https://download.fedoraproject.org/pub/fedora/linux/releases/38/Server/x86_64/iso/Fedora-Server-dvd-x86_64-38-1.6.iso
This image had SHA256 checksum 66b52d7cb39386644cd740930b0bef0a5a2f2be569328fef6b1f9b3679fdc54d.
A minimal installation of Fedora Server isn’t the same thing as an installation of Fedora Minimal, which is outside the scope of this report.
When creating the VM, I added a virtiofs shared file system so that I could extract traces, logs and original source code easily.
Here’s some basic system information:
[user@fedora ~]$ uname -a # a: all
Linux fedora 6.2.9-300.fc38.x86_64 #1 SMP PREEMPT_DYNAMIC Thu Mar 30 22:32:58 UTC 2023 x86_64 GNU/Linux
SELinux is enabled and enforcing:
[user@fedora ~]$ sestatus
SELinux status: enabled
SELinuxfs mount: /sys/fs/selinux
SELinux root directory: /etc/selinux
Loaded policy name: targeted
Current mode: enforcing
Mode from config file: enforcing
Policy MLS status: enabled
Policy deny_unknown status: allowed
Memory protection checking: actual (secure)
Max kernel policy version: 33
The selinux-policy-targeted-38.8-2.fc38.noarch package provides the targeted policy.
During installation, I created a single unprivileged and unconfined user, adding them to the wheel
group:
[user@fedora ~]$ id
uid=1000(user) gid=1000(user) groups=1000(user),10(wheel) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
By default, Anaconda locks the root account:
[user@fedora ~]$ sudo passwd -S root # S: status
root LK 1969-12-30 0 99999 7 -1 (Password locked.)
My shell had a full bounding capability set but no other capabilities:
[user@fedora ~]$ cat /proc/$$/status | grep '^Cap'
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 000001ffffffffff
CapAmb: 0000000000000000
No securebits base or locked flags had been set.
I didn’t upgrade any package on the system explicitly after the installation procedure.
I installed the following packages from the fedora repository to gain access to utilities and development headers for my analyses:
- gcc-13.2.1-4.fc38.x86_64 for the GNU Compiler Collection
- pam-devel-1.5.2-16.fc38.x86_64 for PAM development headers
- strace-6.5-1.fc38.x86_64 for strace(1)
- setools-console-4.4.1-1.fc38.x86_64 for sesearch(1) and seinfo(1)
I also installed the following packages from the fedora-debuginfo repository to help me interpret syscall stack traces:
- libmount-debuginfo-2.38.1-4.fc38.x86_64
- pam-debuginfo-1.5.2-16.fc38.x86_64
- pam-libs-debuginfo-1.5.2-16.fc38.x86_64
My first time using dnf(8) to install packages, I was asked whether to import the following GPG key:
Importing GPG key 0xEB10B464:
Userid : "Fedora (38) <[email protected]>"
Fingerprint: 6A51 BBAB BA3D 5467 B617 1221 809A 8D7C EB10 B464
From : /etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-38-x86_64
Is this ok [y/N]:
I confirmed after comparing the fingerprint to the value listed on https://fedoraproject.org/security/ for equality.
I obfuscated timestamps, IP addresses and UUIDs because they were irrelevant to the analyses.
I omitted all sudo(8) password prompts for brevity.
I normalized all hard tabs to spaces in command output.
I always had strace(1) write output to a file using the --output
option, which I omitted from the reported commands because it doesn’t change the content of the trace.
Likewise, I always redirected the output of tail(1)–grep(1) pipelines and journalctl(1) to a file.
There are 14 such binaries on the system:
[user@fedora ~]$ sudo find / -ignore_readdir_race -perm /u=s -user root
/usr/lib/polkit-1/polkit-agent-helper-1
/usr/bin/gpasswd
/usr/bin/mount
/usr/bin/umount
/usr/bin/chage
/usr/bin/newgrp
/usr/bin/su
/usr/bin/pkexec
/usr/bin/passwd
/usr/bin/sudo
/usr/sbin/grub2-set-bootflag
/usr/sbin/pam_timestamp_check
/usr/sbin/unix_chkpwd
/usr/libexec/openssh/ssh-keysign
Unless stated otherwise, all these binaries have owner root
, group root
and mode -rwsr-xr-x
with no extended attributes other than security.selinux
.
- Program to update authentication tokens using PAM
- Provided by passwd-0.80-14.fc38.x86_64
- Has SHA256 checksum 3baf3bad788ee383b3844096f3cc2e86d31904a70919fb3d4c0d659de7cfcd7d
- Has security context
system_u:object_r:passwd_exec_t:s0
set by the usermanage 1.19.0 module
passwd(1) must be able to update a user’s password via PAM, which involves running the stack defined by the service configuration at /etc/pam.d/passwd:
#%PAM-1.0
# This tool only uses the password stack.
password substack system-auth
-password optional pam_gnome_keyring.so use_authtok
password substack postlogin
Source:
cat /etc/pam.d/passwd
after inserting whitespace for readability
The following modules are included from the system-auth stack:
password requisite pam_pwquality.so local_users_only
password sufficient pam_unix.so yescrypt shadow nullok use_authtok
password sufficient pam_sss.so use_authtok
password required pam_deny.so
Source:
cat /etc/pam.d/system-auth | grep -E '^-?password'
, after truncating whitespace for readability, where-E
is short for--extended-regexp
Of these, the pam_unix
module must be able to read and update /etc/shadow, requiring CAP_CHOWN
, CAP_DAC_OVERRIDE
and, if not root, CAP_FOWNER
to do so.
No modules are included from the postlogin stack:
[user@fedora ~]$ cat /etc/pam.d/postlogin | grep -E '^-?password'
[user@fedora ~]$
In addition, passwd(1) must be able to write to the kernel audit log (CAP_AUDIT_WRITE
).
No. Updating /etc/shadow as a nonroot user requires CAP_FOWNER
to bypass UID-related permission checks. However, the SELinux policy that applies to /usr/bin/passwd doesn’t allow this capability (see discussion below).
I was able to change my password successfully after making the following changes to the binary:
[user@fedora ~]$ sudo setcap cap_chown,cap_dac_override,cap_audit_write=ep /usr/bin/passwd
[user@fedora ~]$ passwd
Changing password for user user.
Current password:
New password:
Retype new password:
passwd: all authentication tokens updated successfully.
Here’s the corresponding entry in the audit log:
type=USER_CHAUTHTOK msg=audit(0.0:402): pid=830 uid=1000 auid=1000 ses=1 subj=unconfined_u:unconfined_r:passwd_t:s0-s0:c0.c1023 msg='op=PAM:chauthtok grantors=pam_pwquality,pam_unix acct="user" exe="/usr/bin/passwd" hostname=fedora addr=? terminal=tty1 res=success'UID="user" AUID="user"
Source:
sudo tail -n 64 /var/log/audit/audit.log | grep 'passwd' | grep 'success'
As far as I can tell from the source code and strace(1) output, passwd(1) doesn’t drop privileges explicitly by calling any of setuid(2), seteuid(2), setreuid(2) or setresuid(2).
The binary /usr/bin/passwd has type passwd_exec_t
, which has an entrypoint to the passwd_t
application domain:
[user@fedora ~]$ sudo sesearch --allow -s passwd_t -t passwd_exec_t -c file -p entrypoint # s: source, t: target, c: class, p: perm
allow passwd_t passwd_exec_t:file { entrypoint execute getattr ioctl lock map open read };
I wanted to determine whether running this process would result in a domain transition from the unconfined_t
domain to the passwd_t
domain. If so, then passwd(1) would be subject to the constraints on the passwd_t
domain. First, I noticed that the targeted SELinux policy does declare such a transition:
[user@fedora ~]$ sudo sesearch -T -s unconfined_t -t passwd_exec_t # T: type_trans
type_transition unconfined_t passwd_exec_t:process passwd_t;
Is this transition allowed? Yes:
[user@fedora ~]$ sudo sesearch --allow -s unconfined_t -t passwd_t -c process -p transition
allow unconfined_t domain:process transition;
What’s more, processes in the unconfined_t
domain can read and execute files with the passwd_exec_t
type:
[user@fedora ~]$ sudo sesearch --allow -s unconfined_t -t passwd_exec_t -c file -p execute,read
allow files_unconfined_type file_type:file { append audit_access create execute execute_no_trans getattr ioctl link lock map mounton open quotaon read relabelfrom relabelto rename setattr swapon unlink watch watch_mount watch_reads watch_sb watch_with_perm write };
Finally, there is an association between the unconfined_r
role and the passwd_t
type:
[user@fedora ~]$ seinfo -x -r unconfined_r | grep -o passwd_t # x: expand, r: role; o: only-matching
passwd_t
On the basis of this information, I concluded that I should be able to run /usr/bin/passwd as an unconfined user, and that the process should transition from the unconfined_t
domain to the passwd_t
domain. I observed this transition using strace(1):
849<strace> [unconfined_t] execve("/usr/bin/passwd" [passwd_exec_t], ["passwd"], 0x7ffd4ba96398 /* 15 vars */) = 0
849<passwd> [passwd_t] access("/etc/suid-debug", F_OK) = -1 ENOENT (No such file or directory)
Source:
sudo strace -u user -fyY --secontext passwd
, where-u
is short for--user
and-fyY
for--follow-forks --decode-fds --decode-pids
Thus, passwd(1) should be subject to the constraints on the passwd_t
domain. In particular, the passwd_t
domain has the following permissions on the capability
and capability2
classes:
[user@fedora ~]$ sudo sesearch --allow -t passwd_t -c capability,capability2
allow passwd_t passwd_t:capability net_bind_service; [ nis_enabled ]:True
allow passwd_t passwd_t:capability net_bind_service; [ nis_enabled ]:True
allow passwd_t passwd_t:capability { audit_write chown dac_override dac_read_search fsetid ipc_lock setgid setuid sys_admin sys_chroot sys_nice sys_resource };
I could now demonstrate that replacing the SUID bit on /usr/bin/passwd with file capabilities wouldn’t work, and also develop a rationale for why setting CAP_FOWNER
would otherwise allow this. I started by making the following changes to the binary:
[user@fedora ~]$ sudo chmod u-s /usr/bin/passwd
[user@fedora ~]$ sudo setcap cap_chown,cap_dac_override,cap_fowner,cap_audit_write=ep /usr/bin/passwd
I then tried changing my password again:
[user@fedora ~]$ passwd
Changing password for user user.
Current password:
New password:
Retype new password:
passwd: Authentication token manipulation error
No dice. strace(1) revealed the failing syscall:
863<passwd> fchmod(5</etc/nshadow>, 0100000) = -1 EPERM (Operation not permitted)
> /usr/lib64/libc.so.6(fchmod+0xb) [0x100d7b]
> /usr/lib64/security/pam_unix.so(pam_sm_chauthtok+0x1120) [0xa260]
> /usr/lib64/libpam.so.0.85.1(_pam_dispatch+0x1d2) [0x9782]
> /usr/lib64/libpam.so.0.85.1(pam_chauthtok+0x71) [0xa171]
> /usr/bin/passwd(main+0x9ef) [0x32df]
> /usr/lib64/libc.so.6(__libc_start_call_main+0x79) [0x27b49]
> /usr/lib64/libc.so.6(__libc_start_main@@GLIBC_2.34+0x8a) [0x27c0a]
> /usr/bin/passwd(_start+0x24) [0x4744]
Source:
sudo strace -u user -fyY -k passwd
, where-k
is short for--stack-traces
, after installing pam-debuginfo-1.5.2-16.fc38.x86_64 and pam-libs-debuginfo-1.5.2-16.fc38.x86_64
passwd(1) made this syscall to set the mode of /etc/nshadow to the mode of /etc/shadow.1 /etc/nshadow was a temporary file with which passwd(1) would later replace /etc/shadow.2 According to the man page for fchmod(2), EPERM
is the error set when either the process doesn’t have CAP_FOWNER
or the file has the append-only (a
) or immutable (i
) extendable attribute. passwd(1) doesn’t create /etc/nshadow with either attribute, so the EPERM
must have been caused by an SELinux denial, as passwd(1) was running confined:
type=AVC msg=audit(0.0:151): avc: denied { fowner } for pid=863 comm="passwd" capability=3 scontext=unconfined_u:unconfined_r:passwd_t:s0-s0:c0.c1023 tcontext=unconfined_u:unconfined_r:passwd_t:s0-s0:c0.c1023 tclass=capability permissive=0
Source:
sudo tail -n 64 /var/log/audit/audit.log | grep 'AVC' | grep 'denied' | grep 'passwd'
It is therefore necessary to retain the SUID bit so that the EUID (really, the FSUID) of passwd(1) matches the UID of /etc/nshadow. Ordinarily, CAP_FOWNER
allows processes to bypass this permission check.
This program shouldn’t be confused with the utility of the same name provided by shadow-utils.
The WITH_SELINUX
macro is assumed to be defined.
1 In linux-pam/modules/pam_unix/[email protected], pam_sm_chauthtok
calls _do_setpass
(lines 860 and 861) to update the password database. Because the password is shadowed, _do_setpass
goes on to call unix_update_shadow
(line 499). There is the possibility that _do_setpass
does not call unix_update_shadow
but delegates to unix_update(8) (line 491); this happens when the unix_selinux_confined
function determines that the process is running confined by SELinux (line 490). In linux-pam/modules/pam_unix/[email protected], unix_selinux_confined
tests for confinement by first establishing whether SELinux is enabled (line 523) and then by attempting to open /etc/shadow via open(2) (line 529). If passwd(1) is running SUID-root or with CAP_DAC_OVERRIDE
or CAP_DAC_READ_SEARCH
, then the call to open(2) will succeed and unix_selinux_confined
will conclude that the process is running unconfined by SELinux. Still in linux-pam/modules/pam_unix/[email protected], unix_update_shadow
calls fopen(3) on SH_TMPFILE
, a macro that expands to "/etc/nshadow"
(line 336), and assigns the result to pwfile
(line 954). unix_update_shadow
later calls fchmod(2) on the file descriptor corresponding to pwfile
to change the mode of /etc/nshadow to the mode stored in st
(line 981). st
contains the result of an fstat(2) call on the file descriptor corresponding to opwfile
(line 968). opwfile
is the result of an fopen(3) call on /etc/shadow (line 961).
2 In linux-pam/modules/pam_unix/[email protected], unix_update_shadow
calls rename(2) to rename /etc/nshadow to /etc/shadow (line 1042)
- Program to display and change user password expiry information
- Provided by shadow-utils-2:4.13-6.fc38.x86_64
- Has SHA256 checksum 555f3514ec66a0e2bc771ce2e05a6c79a02b6bdf856ceefc4a3ef4bc8365ccd2
- Has security context
system_u:object_r:passwd_exec_t:s0
set by the usermanage 1.19.0 module
chage(1) must allow any unprivileged user to view their password expiry information, which requires opening and reading /etc/shadow (CAP_DAC_READ_SEARCH
). If an unprivileged user tries to run chage(1) for any other purpose, then the program must log a security violation by writing to the audit log (CAP_AUDIT_WRITE
).
I was able to get my password expiry information after making the following changes to the binary:
[user@fedora ~]$ sudo chmod u-s /usr/bin/chage
[user@fedora ~]$ sudo setcap cap_dac_read_search,cap_audit_write=ep /usr/bin/chage
[user@fedora ~]$ chage -l user # l: list
Last password change : Jan 01, 1970
Password expires : never
Password inactive : never
Account expires : never
Minimum number of days between password change : 0
Maximum number of days between password change : 99999
Number of days of warning before password expires : 7
When I tried to update my password expiry information, chage(1) refused to run:
[user@fedora ~]$ chage user
chage: Permission denied.
Here’s the corresponding entry in the audit log:
type=USER_MGMT msg=audit(0.0:113): pid=827 uid=1000 auid=1000 ses=1 subj=unconfined_u:unconfined_r:passwd_t:s0-s0:c0.c1023 msg='op=change-age acct="" exe="/usr/bin/chage" hostname=fedora addr=? terminal=tty1 res=failed'UID="user" AUID="user"
Source:
sudo tail -n 64 /var/log/audit/audit.log | grep 'chage' | grep 'failed'
, where-n
is short for--lines
chage(1) drops privileges immediately after opening /etc/shadow:1
838<chage> read(5</etc/shadow>, "root:!::0:99999:7:::\nbin:*:19378"..., 4096) = 694
838<chage> read(5</etc/shadow>, "", 4096) = 0
838<chage> setregid(1000, 1000) = 0
838<chage> setreuid(1000, 1000) = 0
Source:
sudo strace -u user -fyY chage -l user
after truncating whitespace for readability
Please refer to the corresponding discussion for /usr/bin/passwd, which has the same security context as /usr/bin/chage. Observe that the file capabilities I set on the binary form a strict subset of the capabilities allowed by the targeted SELinux policy.
If unprivileged users receive password expiry information out of band or there isn’t a password expiry policy in place, then consider unsetting the SUID bit on /usr/bin/chage.
All line numbers refer to points in source code after applying the patches in src.fedoraproject.org/rpms/shadow-utils@fd05b1c.
1 In shadow/src/[email protected], main
calls open_files
(line 796), which calls spw_open
(line 571). In shadow/lib/[email protected], spw_open
calls commonio_open
, passing the address of shadow_db
(line 151). In shadow/lib/[email protected], commonio_open
boils down to an open(2) call on db->filename
(lines 619–621), where db
is the pointer to shadow_db
. shadow_db
is a global static struct of type commonio_db
whose filename
field is initialized to SHADOW_FILE
(line 81 of shadow/lib/[email protected]). In shadow/lib/[email protected], SHADOW_FILE
is a macro that expands to "/etc/shadow"
(line 261). Back in shadow/src/[email protected], main
calls setregid(2) to restore the EGID to the RGID (line 798) and setreuid(2) to restore the EUID to the RUID (line 799) after open_files
returns.
- Program to administer user groups
- Provided by shadow-utils-2:4.13-6.fc38.x86_64
- Has SHA256 checksum e6681a06c02396f80a7f24c7ea3d274b4433fff87f92bdb1b38920a8b281060b
- Has security context
system_u:object_r:groupadd_exec_t:s0
set by the usermanage 1.19.0 module
gpasswd(1) must allow any nonroot user who is an administrative member of a group to administer that group. To this end, gpasswd(1) must be able to:
- update /etc/group and /etc/gshadow (
CAP_CHOWN
andCAP_DAC_OVERRIDE
); - set the UIDs to 0 (
CAP_SETUID
), and - write to the kernel audit log (
CAP_AUDIT_WRITE
).
I decided to answer this question for the reference use case of adding a user to a group. First, I created a group and a user to add to that group:
[user@fedora ~]$ sudo groupadd -g 2000 gunbuster && sudo useradd noriko # g: gid
I then made the following changes to the binary:
[user@fedora ~]$ sudo chmod u-s /usr/bin/gpasswd
[user@fedora ~]$ sudo setcap cap_chown,cap_dac_override,cap_setuid,cap_audit_write=ep /usr/bin/gpasswd
I ensured that I couldn’t yet add the new user to the new group:
[user@fedora ~]$ gpasswd -a noriko gunbuster # a: add
gpasswd: Permission denied.
gpasswd(1) logged my transgression properly:
type=USER_MGMT msg=audit(0.0:134): pid=846 uid=1000 auid=1000 ses=1 subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 msg='op=add-user-to-group grp="gunbuster" acct="noriko" exe="/usr/bin/gpasswd" hostname=fedora addr=? terminal=tty1 res=failed'UID="user" AUID="user"
Source:
sudo tail -n 64 /var/log/audit/audit.log | grep 'gpasswd' | grep 'failed'
Indeed, I had to first make myself an administrative member of the new group:
[user@fedora ~]$ sudo gpasswd -A user gunbuster # A: administrators
The new file capabilities took me the rest of the way:
[user@fedora ~]$ gpasswd -a noriko gunbuster
Adding user noriko to group gunbuster
[user@fedora ~]$ id noriko
uid=1001(noriko) gid=1001(noriko) groups=1001(noriko),2000(gunbuster)
As far as I can tell from the source code and strace(1) output, gpasswd(1) doesn’t drop privileges explicitly by calling any of setuid(2), seteuid(2), setreuid(2) or setresuid(2).
The targeted SELinux policy doesn’t define a process transition from the unconfined_t
domain to the groupadd_t
domain (the application domain for the groupadd_exec_t
file type):
[user@fedora ~]$ sudo sesearch -T -s unconfined_t -t groupadd_exec_t
[user@fedora ~]$
Thus, gpasswd(1) runs in the unconfined_t
domain, its capabilities unconstrained by SELinux.
If unprivileged users must be able to use the --root
flag to administer groups in a different root file system, then it is necessary to also set CAP_SYS_CHROOT
on /usr/bin/gpasswd.
If there are no use cases that involve group administration by one or more unprivileged users, then consider unsetting the SUID bit on /usr/bin/gpasswd without setting file capabilities.
gpasswd(1) becomes root unconditionally near the end of its lifetime:1
874<gpasswd> setuid(0) = 0
Source:
sudo strace -u user -fyY gpasswd -a noriko gunbuster
after truncating whitespace for readability
… in order to update /etc/group and /etc/gshadow.
gpasswd(1) later forks itself so that it can execute sss_cache(8) to invalidate all user and group records in the SSSD cache before exiting:
874<gpasswd> execve("/usr/sbin/sss_cache", ["sss_cache", "-UG"], 0x7fffef712d38 /* 0 vars */) = 0
Source:
sudo strace -u user -fyY gpasswd -a noriko gunbuster
sss_cache(8) requires CAP_SETUID
and CAP_SETGID
to set its EUID and EGID to 0, respectively:
874<sss_cache> setresuid(-1, 0, -1) = 0
874<sss_cache> setresgid(-1, 0, -1) = 0
Source:
sudo strace -u user -fyY gpasswd -a noriko gunbuster
after truncating whitespace for readability
sss_cache(8) can perform these operations because it is executed by a root process whose bounding capability set is full.
1 In shadow/src/[email protected] after applying the patches in src.fedoraproject.org/rpms/shadow-utils@fd05b1c, main
calls setuid(2) to set all the UIDs to 0 and otherwise returns a nonzero value (lines 1104–1109)
- Program to change the GIDs during a login session
- Provided by shadow-utils-2:4.13-6.fc38.x86_64
- Has SHA256 checksum a582658e9c6c2e930c200c650693f5f10eba8f081896f11546c4cd55b236f4bc
- Has security context
system_u:object_r:bin_t:s0
newgrp(1) must be able to:
- read /etc/shadow and /etc/gshadow (
CAP_DAC_READ_SEARCH
); - set the GIDs (
CAP_SETGID
) and - write to the kernel audit log (
CAP_AUDIT_WRITE
).
I was able to change the GIDs of my session to those of the wheel
group (of which I’m a member) after making the following changes to the binary:
[user@fedora ~]$ sudo chmod u-s /usr/bin/newgrp
[user@fedora ~]$ sudo setcap cap_dac_read_search,cap_setgid,cap_audit_write=ep /usr/bin/newgrp
[user@fedora ~]$ newgrp - wheel
[user@fedora ~]$ id -gnr # g: group, n: name, r: real
wheel
I checked the audit log and found a matching entry:
type=CHGRP_ID msg=audit(0.0:115): pid=829 uid=1000 auid=1000 ses=1 subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 msg='op=changing new_gid=10 id=1000 exe="/usr/bin/newgrp" hostname=fedora addr=? terminal=tty1 res=success'UID="user" AUID="user" NEW_GID="wheel" ID="user"
Source:
sudo tail -n 64 /var/log/audit/audit.log | grep 'newgrp' | grep 'success'
I also confirmed that I could return to the original environment correctly:
[user@fedora ~]$ logout
[user@fedora ~]$ id -gnr
user
newgrp(1) drops privileges immediately after setting the GIDs:1
865<newgrp> setgid(10) = 0
865<newgrp> getuid() = 1000
865<newgrp> setuid(1000) = 0
Source:
sudo strace -u user -fyY newgrp - wheel
after truncating whitespace for readability
The targeted SELinux policy doesn’t restrict the permissions of the bin_t
domain on the capability
and capability2
classes, so newgrp(1) runs unconfined.
If no unprivileged user needs to be able to change the GIDs of a login session, then consider unsetting the SUID bit on /usr/bin/newgrp without setting file capabilities.
1 In shadow/src/[email protected] after applying the patches in src.fedoraproject.org/rpms/shadow-utils@fd05b1c, main
calls setgid(2) to set all the GIDs to the ID of the new group (line 730), then calls setuid(2) to set all the UIDs to the RUID (line 742)
- Program to mount file systems
- Provided by util-linux-core-2.38.1-4.fc38.x86_64
- Has SHA256 checksum ac8efce2afaab784c382bc7ec3843c023e3aa60bcd57467bbce3691a21eda24c
- Has security context
system_u:object_r:mount_exec_t:s0
set by the mount 1.16.1 module
Any unprivileged user must be able to mount any unmounted file system listed in /etc/fstab whose entry contains the user
, users
, owner
or group
option (CAP_SYS_ADMIN
).
mount(8) has an internal security model that depends on the SUID bit. When asked to perform certain operations within a “restricted context” (i.e., when running with RUID nonzero), mount(8) calls the suid_drop
function.1 In turn, suid_drop
calls drop_permissions
, which makes the setgid(2) and setuid(2) syscalls to restore the GIDs and UIDs, respectively.2 However, as a result of C’s short-circuit evaluation, suid_drop
calls drop_permissions
only when the process has RUID nonzero and EUID zero.3 The only way to satisfy these conditions is to run mount(8) SUID-root as a nonroot user.
In effect, when the SUID bit is unset on /usr/bin/mount, mount(8) will never drop permissions. If /usr/bin/mount were to have a nonempty permitted capability set and its effective capability bit were set, then mount(8) would never have its effective capability set cleared during execution, potentially allowing unprivileged users to perform actions they shouldn’t be able to perform. This is because the mount(2) syscall is made unconditionally (with respect to privileges) via libmount.4
To test my hypothesis, I started by inspecting the following entry in /etc/fstab for the boot partition:
UUID=00000000-0000-0000-0000-000000000000 /boot xfs defaults 0 0
Source:
cat /etc/fstab | grep 'boot'
after truncating whitespace for readability
This file system is mounted by default. I first unmounted it:
[user@fedora ~]$ sudo umount /boot && findmnt /boot
[user@fedora ~]$
I confirmed that the binary in question was SUID-root and had no file capabilities:
[user@fedora ~]$ ls -lZ /usr/bin/mount # Z: context
-rwsr-xr-x. 1 root root system_u:object_r:mount_exec_t:s0 49248 Jan 20 2023 /usr/bin/mount
[user@fedora ~]$ getcap /usr/bin/mount
[user@fedora ~]$
Next, I tried to mount /boot:
[user@fedora ~]$ mount /boot
mount: /boot: must be superuser to use mount.
dmesg(1) may have more information after failed mount system call.
I couldn’t. Here’s the failing mount(2) call:
844<mount> mount("/dev/vda2", "/boot", "xfs", 0, NULL) = -1 EPERM (Operation not permitted)
> /usr/lib64/libc.so.6(mount+0xe) [0x113c9e]
> /usr/lib64/libmount.so.1.1.0(do_mount+0xa91) [0x29b91]
> /usr/lib64/libmount.so.1.1.0(mnt_context_do_mount+0x1a1) [0x2a741]
> /usr/lib64/libmount.so.1.1.0(mnt_context_mount+0x1d8) [0x2d778]
> /usr/bin/mount(main+0x171a) [0x686a]
> /usr/lib64/libc.so.6(__libc_start_call_main+0x79) [0x27b49]
> /usr/lib64/libc.so.6(__libc_start_main@@GLIBC_2.34+0x8a) [0x27c0a]
> /usr/bin/mount(_start+0x24) [0x6fb4]
Source:
sudo strace -u user -fyY -k mount /boot
after installing libmount-debuginfo-2.38.1-4.fc38.x86_64
The syscall above failed due to a lack of privileges. Here’s the setuid(2) call made as part of an earlier invocation of suid_drop
:
844<mount> setuid(1000) = 0
> /usr/lib64/libc.so.6(setuid+0x2b) [0xdca5b]
> /usr/bin/mount(suid_drop+0x106) [0x73e6]
> /usr/bin/mount(main+0x1712) [0x6862]
> /usr/lib64/libc.so.6(__libc_start_call_main+0x79) [0x27b49]
> /usr/lib64/libc.so.6(__libc_start_main@@GLIBC_2.34+0x8a) [0x27c0a]
> /usr/bin/mount(_start+0x24) [0x6fb4]
Source:
sudo strace -u user -fyY -k mount /boot
after installing libmount-debuginfo-2.38.1-4.fc38.x86_64 and truncating whitespace for readability
So far, mount(8) was working as intended.
I then unset the SUID bit on the binary and conferred the CAP_SYS_ADMIN
capability:
[user@fedora ~]$ sudo chmod u-s /usr/bin/mount
[user@fedora ~]$ sudo setcap cap_sys_admin=ep /usr/bin/mount
Having satisfied the conditions to avoid calling drop_permissions
, I was able to mount /boot as an unprivileged user:
[user@fedora ~]$ mount /boot && findmnt /boot
TARGET SOURCE FSTYPE OPTIONS
/boot /dev/vda2 xfs rw,relatime,seclabel,attr2,inode64,logbufs=8,logbsize=32k,noquota
This simple demo illustrates how the substitution of a SUID bit with file capabilities can extend the attack surface of a system. It is paramount to appreciate how each individual SUID-root binary has been engineered.
I was able to mount a file system on a virtual disk after making the following changes to the binary:
[user@fedora ~]$ sudo chmod u+s /usr/bin/mount
[user@fedora ~]$ sudo setcap cap_sys_admin=ep /usr/bin/mount
I first added a new VirtIO disk at /dev/vdb. I used fdisk(8) to format the disk and create a Linux partition at /dev/vdb1:
[user@fedora ~]$ (echo o; echo n; echo p; echo 1; echo; echo; echo w) | sudo fdisk /dev/vdb
[user@fedora ~]$ lsblk /dev/vdb
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
vdb 252:16 0 4G 0 disk
└─vdb1 252:17 0 4G 0 part
I then created an XFS file system on the partition:
[user@fedora ~]$ sudo mkfs.xfs -f /dev/vdb1 # f: force
Next, I created the mount point, appended the corresponding entry to /etc/fstab and reloaded the systemd manager configuration:
[user@fedora ~]$ sudo mkdir -p /mnt/upart # p: parents
[user@fedora ~]$ echo '/dev/vdb1 /mnt/upart xfs defaults,noauto,owner 0 0' | sudo tee -a /etc/fstab > /dev/null
[user@fedora ~]$ sudo systemctl daemon-reload
… where I set the owner
option to allow the device’s owner to mount the file system. To enable this use case, I made myself the owner of the partition:
[user@fedora ~]$ sudo chown user /dev/vdb1
I could then mount the file system successfully:
[user@fedora ~]$ mount /mnt/upart && findmnt /mnt/upart
TARGET SOURCE FSTYPE OPTIONS
/mnt/upart /dev/vdb1 xfs rw,nosuid,nodev,relatime,seclabel,attr2,inode64,logbufs=8,logbsize=32k,noquota
Additional capabilities may be needed to mount particular types of file system (with particular options).
As a confidence check, I tried to mount /boot again, this time without success:
[user@fedora ~]$ sudo umount /boot
[user@fedora ~]$ findmnt /boot
[user@fedora ~]$ mount /boot
mount: drop permissions failed.
[user@fedora ~]$ findmnt /boot
[user@fedora ~]$
The targeted SELinux policy doesn’t define a process transition from the unconfined_t
domain to the mount_t
domain through the mount_exec_t
file type:
[user@fedora ~]$ sudo sesearch -T -s unconfined_t -t mount_exec_t
[user@fedora ~]$
Thus, mount(8) runs unconfined when run from the command line.
However, the targeted SELinux policy defines many transitions to the mount_t
domain through the mount_exec_t
type:
[user@fedora ~]$ sudo sesearch -T -t mount_exec_t | wc -l # l: lines
42
Suppose an application were to execve(2) mount(8) and transition to the mount_t
domain. To what extent would the policy limit the capabilities with which mount(8) would run?
[user@fedora ~]$ sudo sesearch --allow -t mount_t -c capability,capability2
allow mount_t mount_t:capability sys_module; [ secure_mode_insmod ]:False
allow mount_t mount_t:capability { audit_control audit_write chown dac_override dac_read_search fowner fsetid ipc_lock ipc_owner kill lease linux_immutable mknod net_admin net_bind_service net_broadcast net_raw setfcap setgid setpcap setuid sys_admin sys_boot sys_chroot sys_nice sys_pacct sys_ptrace sys_rawio sys_resource sys_time sys_tty_config };
allow mount_t mount_t:capability2 { audit_read block_suspend bpf checkpoint_restore epolwakeup perfmon syslog wake_alarm };
It turns out that the policy would hardly limit mount(8)’s capabilities. This is because the mount module makes use of the unconfined_domain
interface, which lifts many restrictions on the corresponding domain.5
If the user
, users
, owner
or group
options don’t need to be set for any entry in /etc/fstab, then consider unsetting the SUID bit without setting file capabilities.
- “Non-superuser mounts” in the mount(8) man page
1 Lines 726, 942, 975, 1003, 1011, 1022 and 1038 of util-linux/sys-utils/[email protected]
2 Lines 367–382 of util-linux/include/[email protected]
3 Line 58 of util-linux/sys-utils/[email protected]
4 Line 1032 of util-linux/sys-utils/[email protected]
5 In selinux-policy/policy/modules/system/[email protected], the mount_t
domain is passed to the unconfined_domain
interface (line 360). In selinux-policy/policy/modules/system/[email protected], unconfined_domain
wraps the unconfined_domain_noaudit
interface (line 136), which grants all permissions to the given domain on the capability
class excepting sys_module
(line 22) and the capability2
class excepting mac_admin
and mac_override
(line 23).
- Program to unmount file systems
- Provided by util-linux-core-2.38.1-4.fc38.x86_64
- Has SHA256 checksum 5a15dcb9c509fe1ac3e89ead094ff1cdb993b6a17dbb2863cb2b396ca0ce1e37
- Has security context
system_u:object_r:mount_exec_t:s0
set by the mount 1.16.1 module
Yes, because both mount(8) and umount(8) depend on the same libmount functions and use an identically implemented suid_drop
function.
- Helper for the
pam_unix
PAM module that verifies the current user’s password - Provided by pam-1.5.2-16.fc38.x86_64
- Has SHA256 checksum 95aa56aba4eaeccd8ba6972b6453725fd0a4a6d25105cd6c61df0eb87c4c2c22
- Has security context
system_u:object_r:chkpwd_exec_t:s0
set by the authlogin 2.5.1 module
unix_chkpwd(8) must be able to read /etc/shadow (CAP_DAC_READ_SEARCH
) and write to the kernel audit log (CAP_AUDIT_WRITE
).
As of Linux PAM v1.5.2, it’s no longer necessary to have this binary installed SUID-root per pull request #373 if one sets CAP_DAC_OVERRIDE
on the binary.
Based on my understanding of the source code, unix_chkpwd(8) doesn’t need to write or execute /etc/shadow, so CAP_DAC_READ_SEARCH
should suffice:
[user@fedora ~]$ sudo chmod u-s /usr/sbin/unix_chkpwd
[user@fedora ~]$ sudo setcap cap_dac_read_search,cap_audit_write=ep /usr/sbin/unix_chkpwd
… where I also set CAP_AUDIT_WRITE
to permit audit logging, as PAM is built with auditing support for Fedora Linux.
unix_chkpwd(8) is intended to be run indirectly, has an undocumented API and logs a security violation when invoked from an interactive terminal. These factors governed my decision to test my changes by writing a small wrapper in C (as unix_chkpwd_wrapper.c) instead of running the pam_unix
module indirectly as part of a PAM application:
/*
* SPDX-FileCopyrightText: Copyright 2023 OK Ryoko
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define CHKPWD_HELPER "/usr/sbin/unix_chkpwd"
int main() {
static char *args[] = { NULL, NULL, NULL, NULL };
static char *envp[] = { NULL };
args[0] = CHKPWD_HELPER;
args[1] = "user";
args[2] = "chkexpiry";
execve(CHKPWD_HELPER, (char *const *) args, envp);
perror("execve");
exit(EXIT_FAILURE);
}
I compiled this program and updated its permissions like so:
[user@fedora ~]$ gcc -Wall unix_chkpwd_wrapper.c -o unix_chkpwd_wrapper
[user@fedora ~]$ chmod 0770 unix_chkpwd_wrapper
strace(1) output revealed that unix_chkpwd(8) was able to read /etc/shadow without the SUID bit:
841<unix_chkpwd> openat(AT_FDCWD</home/user>, "/etc/shadow", O_RDONLY|O_CLOEXEC) = 3</etc/shadow>
841<unix_chkpwd> newfstatat(3</etc/shadow>, "", {st_mode=S_IFREG|000, st_size=728, ...}, AT_EMPTY_PATH) = 0
841<unix_chkpwd> lseek(3</etc/shadow>, 0, SEEK_SET) = 0
841<unix_chkpwd> read(3</etc/shadow>, "root:!::0:99999:7:::\nbin:*:19378"..., 4096) = 728
841<unix_chkpwd> close(3</etc/shadow>) = 0
Source:
sudo strace -u user -fyY ./unix_chkpwd_wrapper
after truncating whitespace for readability
I also tried to run unix_chkpwd(8) inappropriately from a tty:
[user@fedora ~]$ unix_chkpwd
The program slept for 10 seconds before exiting with code 4, which represents a PAM system error. Having granted CAP_AUDIT_WRITE
, I expected to find a matching entry in the audit log, and I did:
type=ANOM_EXEC msg=audit(0.0:123): pid=843 uid=1000 auid=1000 ses=1 subj=unconfined_u:unconfined_r:chkpwd_t:s0-s0:c0.c1023 msg='op=PAM:unix_chkpwd acct="user" exe="/usr/sbin/unix_chkpwd" hostname=? addr=? terminal=? res=failed'UID="user" AUID="user"
Source:
sudo tail -n 64 /var/log/audit/audit.log | grep 'unix_chkpwd' | grep 'failed'
Errors notwithstanding, this program drops privileges only if no password file entry exists for the user corresponding to the process’s RUID or said user doesn’t match the user specified by the invoking application.1
The binary /usr/sbin/unix_chkpwd has type chkpwd_exec_t
, which has an entrypoint to the chkpwd_t
application domain:
[user@fedora ~]$ sudo sesearch --allow -s chkpwd_t -t chkpwd_exec_t -c file -p entrypoint
allow chkpwd_t chkpwd_exec_t:file { entrypoint execute getattr ioctl lock map open read };
I wanted to determine whether running this process would result in a domain transition from the unconfined_t
domain to the chkpwd_t
domain. If so, then unix_chkpwd(8) would be subject to the constraints on the chkpwd_t
domain. First, I noticed that the targeted SELinux policy does declare such a transition:
[user@fedora ~]$ sudo sesearch -T -s unconfined_t -t chkpwd_exec_t
type_transition unconfined_t chkpwd_exec_t:process chkpwd_t;
Is this transition allowed? Yes:
[user@fedora ~]$ sudo sesearch --allow -s unconfined_t -t chkpwd_t -c process -p transition
allow unconfined_t domain:process transition;
What’s more, processes in the unconfined_t
domain can read and execute files with the chkpwd_exec_t
type:
[user@fedora ~]$ sudo sesearch --allow -s unconfined_t -t chkpwd_exec_t -c file -p execute,read
allow files_unconfined_type file_type:file { append audit_access create execute execute_no_trans getattr ioctl link lock map mounton open quotaon read relabelfrom relabelto rename setattr swapon unlink watch watch_mount watch_reads watch_sb watch_with_perm write };
Finally, there is an association between the unconfined_r
role and the chkpwd_t
type:
[user@fedora ~]$ seinfo -x -r unconfined_r | grep -o 'chkpwd_t'
chkpwd_t
On the basis of this information, I concluded that I should be able to run /usr/sbin/unix_chkpwd as an unconfined user, and that the process should transition from the unconfined_t
domain to the chkpwd_t
domain. I observed this transition using strace(1):
1117<unix_chkpwd_wra> [unconfined_t] execve("/usr/sbin/unix_chkpwd" [chkpwd_exec_t], ["/usr/sbin/unix_chkpwd", "user", "chkexpiry"], 0x404060 /* 0 vars */) = 0
1117<unix_chkpwd> [chkpwd_t] access("/etc/suid-debug", F_OK) = -1 ENOENT (No such file or directory)
Source:
sudo strace -u user -fyY --secontext ./unix_chkpwd_wrapper
Thus, the process should be subject to the constraints on the chkpwd_t
domain. In particular, the chkpwd_t
domain has the following permissions on the capability
and capability2
classes:
[user@fedora ~]$ sudo sesearch --allow -t chkpwd_t -c capability,capability2
allow chkpwd_t chkpwd_t:capability net_bind_service; [ nis_enabled ]:True
allow chkpwd_t chkpwd_t:capability net_bind_service; [ nis_enabled ]:True
allow chkpwd_t chkpwd_t:capability { audit_write dac_read_search setuid };
Observe that the file capabilities I set on the binary form a strict subset of the capabilities allowed by the policy.
If a program runs the pam_unix
module but can’t read /etc/shadow (due to the absence of CAP_DAC_READ_SEARCH
or CAP_DAC_OVERRIDE
since /etc/shadow has mode 0000
), then pam_unix
will fall back to unix_chkpwd(8).2 Just before executing this helper, pam_unix
will set the forked process’s UIDs to 0 if the EUID is 0.3 This is relevant because programs such as sudo(8) and pkexec(1) expect to run with EUID 0 unconditionally. Therefore, when the forking process’s bounding capability set is full, unix_chkpwd(8) will run with all capabilities.
The HELPER_COMPILE
macro influences the contents of linux-pam/modules/pam_unix/[email protected], which is used to compile both pam_unix
and unix_chkpwd(8) per lines 42 and 49, respectively, of linux-pam/modules/pam_unix/[email protected]. HELPER_COMPILE
is assumed to be undefined when compiling pam_unix
and defined when compiling unix_chkpwd(8).
1 In linux-pam/modules/pam_unix/[email protected], main
checks whether the RUID is 0 (line 133). If the RUID is not 0 (line 136), then main
calls getuidname
to retrieve the name of the user as which the process is running (line 137). (In linux-pam/modules/pam_unix/[email protected], getuidname
boils down to a call to getpwuid(3) on line 1163.) If getuidname
returns NULL
or the name of the running user doesn’t match the name passed to unix_chkpwd(8), then main
sets the UIDs to the RUID via setuid(2) (line 143).
2 In linux-pam/modules/pam_unix/[email protected], pam_sm_authenticate
calls _unix_blankpasswd
(line 146) and _unix_verify_password
(line 173). In linux-pam/modules/pam_unix/[email protected], _unix_blankpasswd
calls get_pwd_hash
(line 637) twice. If get_pwd_hash
returns PAM_UNIX_RUN_HELPER
(line 639), then _unix_blankpasswd
calls _unix_run_helper_binary
(line 640) which, in turn, execve(2)s CHKPWD_HELPER
(line 535). CHKPWD_HELPER
is a macro that, on Fedora Linux 38, expands to "/usr/sbin/unix_chkpwd"
per line 21 of linux-pam/modules/pam_unix/[email protected]. Similarly, _unix_verify_password
calls get_pwd_hash
(line 684). If get_pwd_hash
returns PAM_UNIX_RUN_HELPER
(line 700), then _unix_verify_password
calls _unix_run_helper_binary
(line 702). When does get_pwd_hash
return PAM_UNIX_RUN_HELPER
? In linux-pam/modules/pam_unix/[email protected], the return value of get_pwd_hash
is either PAM_SUCCESS
(line 279) or the return value of get_account_info
(line 267). get_account_info
returns PAM_UNIX_RUN_HELPER
if either the password is a NIS+ password (lines 200 and 237) or the password is shadowed and pam_modutil_getspnam
returns NULL
(lines 239, 244, 245 and 248). In linux-pam/libpam/[email protected], pam_modutil_getspnam
boils down to a call to getspnam_r(3) (lines 56–58), which retrieves the shadow password structure and returns NULL
if an error occurred, e.g., EACCES
due to a failure to read /etc/shadow.
3 In linux-pam/modules/pam_unix/[email protected], _unix_run_helper_binary
calls setuid(2) to change all the UIDs to 0 when geteuid(2) returns 0 (lines 516–523)
- Program to validate or remove the default timestamp (for cached authentication results)
- Provided by pam-1.5.2-16.fc38.x86_64
- Has SHA256 checksum 5509f331a360183da55b1041540349668b9a52ce8a512340cbb76738cd66e9d5
- Has security context
system_u:object_r:pam_timestamp_exec_t:s0
set by the authlogin 2.5.1 module
pam_timestamp_check(8) expects to run with EUID 0 unconditionally.1 It requires privileges to create and manipulate the directory tree rooted at /var/run/pam_timestamp.
pam_timestamp_check(8) does not require any capabilities as long as it runs with EUID 0. To demonstrate this, I started by creating a service configuration file at /etc/pam.d/timestamp with the following contents, taking after the example in the corresponding man page:
auth sufficient pam_timestamp.so verbose debug
auth required pam_unix.so
session required pam_unix.so
session optional pam_timestamp.so verbose debug
I then wrote a dummy application in timestamper.c to use this configuration. This application authenticates user
, then opens and closes a session without doing any real work:
/*
* SPDX-FileCopyrightText: Copyright 2023 OK Ryoko
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#include <stdio.h>
#include <stdlib.h>
#include <security/pam_appl.h>
#include <security/pam_misc.h>
static struct pam_conv conv = {
misc_conv,
NULL
};
int main(int argc, const char **argv) {
const char *progname = argv[0];
int retval;
pam_handle_t *pamh = NULL;
retval = pam_start("timestamp", "user", &conv, &pamh);
if (retval != PAM_SUCCESS) {
fprintf(
stderr,
"%s: initializing PAM transaction: %s\n",
progname,
pam_strerror(pamh, retval)
);
return 1;
}
retval = pam_authenticate(pamh, 0);
if (retval != PAM_SUCCESS) {
fprintf(
stderr,
"%s: authenticating with PAM: %s\n",
progname,
pam_strerror(pamh, retval)
);
pam_end(pamh, retval);
return 1;
}
retval = pam_open_session(pamh, 0);
if (retval != PAM_SUCCESS) {
fprintf(
stderr,
"%s: opening PAM session: %s\n",
progname,
pam_strerror(pamh, retval)
);
pam_end(pamh, retval);
return 1;
}
retval = pam_close_session(pamh, 0);
if (retval != PAM_SUCCESS) {
fprintf(
stderr,
"%s: closing PAM session: %s\n",
progname,
pam_strerror(pamh, retval)
);
pam_end(pamh, retval);
return 1;
}
retval = pam_end(pamh, PAM_SUCCESS);
return 0;
}
After installing pam-devel-1.5.2-16.fc38.x86_64, I was able to compile this source code like so:
[user@fedora ~]$ gcc -Wall $(pkgconf --cflags --libs pam pam_misc) timestamper.c -o timestamper
… where I linked against libpam.so.0.85.1 and libpam_misc.so.0.82.1 provided by pam-libs-1.5.2-16.fc38.x86_64.
I gave myself execute permissions for the binary:
[user@fedora ~]$ chmod 0770 timestamper
To enable the application to run the pam_timestamp
module successfully, I found it necessary to confer some capabilities to the binary:
[user@fedora ~]$ sudo setcap cap_chown,cap_dac_override=ep timestamper
CAP_DAC_OVERRIDE
lets pam_timestamp
create /var/run/pam_timestamp and subdirectories thereof via mkdir(2) as well as the /var/pam_timestamp/_pam_timestamp_key file. In addition, CAP_CHOWN
lets pam_timestamp
change the owner of these directories and files to root via lchown(2) and fchown(2).
Next, I confirmed that the binary of interest was SUID-root and had no file capabilities of its own:
[user@fedora ~]$ ls -lZ /usr/sbin/pam_timestamp_check
-rwsr-xr-x. 1 root root system_u:object_r:pam_timestamp_exec_t:s0 16168 Jan 18 2023 /usr/sbin/pam_timestamp_check
[user@fedora ~]$ getcap /usr/sbin/pam_timestamp_check
[user@fedora ~]$
I could now test my application. I first checked whether I had a valid timestamp:
[user@fedora ~]$ pam_timestamp_check
[user@fedora ~]$ echo $?
7
The exit code communicated that my timestamp wasn’t valid. I ran my application to change that:
[user@fedora ~]$ ./timestamper
Password:
[user@fedora ~]$ pam_timestamp_check
[user@fedora ~]$ echo $?
0
Success! Here’s what the systemd journal had to say about what had just happened:
Jan 01 00:00:00 fedora timestamper[842]: pam_timestamp(timestamp:auth): becoming more verbose
Jan 01 00:00:00 fedora timestamper[842]: pam_timestamp(timestamp:auth): becoming user `user'
Jan 01 00:00:00 fedora timestamper[842]: pam_timestamp(timestamp:auth): currently user `user'
Jan 01 00:00:00 fedora timestamper[842]: pam_timestamp(timestamp:auth): tty is `/dev/tty1'
Jan 01 00:00:00 fedora timestamper[842]: pam_timestamp(timestamp:auth): using timestamp file `/var/run/pam_timestamp/user/tty1'
Jan 01 00:00:00 fedora timestamper[842]: pam_timestamp(timestamp:auth): cannot open timestamp `/var/run/pam_timestamp/user/tty1': No such file or directory
Jan 01 00:00:00 fedora timestamper[842]: pam_unix(timestamp:session): session opened for user user(uid=1000) by user(uid=1000)
Jan 01 00:00:00 fedora timestamper[842]: pam_timestamp(timestamp:session): becoming user `user'
Jan 01 00:00:00 fedora timestamper[842]: pam_timestamp(timestamp:session): currently user `user'
Jan 01 00:00:00 fedora timestamper[842]: pam_timestamp(timestamp:session): tty is `/dev/tty1'
Jan 01 00:00:00 fedora timestamper[842]: pam_timestamp(timestamp:session): using timestamp file `/var/run/pam_timestamp/user/tty1'
Jan 01 00:00:00 fedora timestamper[842]: pam_timestamp(timestamp:session): updated timestamp file `/var/run/pam_timestamp/user/tty1'
Jan 01 00:00:00 fedora timestamper[842]: pam_unix(timestamp:session): session closed for user user
Source:
sudo journalctl -eb
, where-eb
is short for--pager-end --boot
I had allowed the use of the pam_timestamp
module for authentication in my service configuration file. To validate this functionality, I ran my application again:
[user@fedora ~]$ ./timestamper
Access has been granted (last access was 30 seconds ago).
Thus, I had confirmed that my application was working correctly. To recover my initial state, I cleared the timestamp:
[user@fedora ~]$ pam_timestamp_check -k
[user@fedora ~]$ pam_timestamp_check
[user@fedora ~]$ echo $?
7
I could now experiment with /usr/sbin/pam_timestamp_check. I ensured that the binary was SUID-root and assigned it an empty capability set:
[user@fedora ~]$ sudo chmod u+s /usr/sbin/pam_timestamp_check
[user@fedora ~]$ sudo setcap '' /usr/sbin/pam_timestamp_check
[user@fedora ~]$ getcap /usr/sbin/pam_timestamp_check
/usr/sbin/pam_timestamp_check =
I performed a confidence check to confirm that I didn’t have a valid timestamp:
[user@fedora ~]$ pam_timestamp_check
[user@fedora ~]$ echo $?
7
Now, to put the file capabilities to the test. Could I acquire a valid timestamp through my application?
[user@fedora ~]$ ./timestamper
Password:
[user@fedora ~]$ ./timestamper
Access has been granted (last access was 3 seconds ago).
Yes. And could I remove the timestamp via pam_timestamp_check(8)?
[user@fedora ~]$ pam_timestamp_check -k
[user@fedora ~]$ pam_timestamp_check
[user@fedora ~]$ echo $?
7
Yes. Here’s the relevant strace(1) output, where we see access to and removal of the timestamp via newfstatat(2) and unlink(2), respectively:
865<pam_timestamp_c> newfstatat(AT_FDCWD</home/user>, "/", {st_mode=S_IFDIR|0555, st_size=235, ...}, AT_SYMLINK_NOFOLLOW) = 0
865<pam_timestamp_c> newfstatat(AT_FDCWD</home/user>, "/var/", {st_mode=S_IFDIR|0755, st_size=4096, ...}, AT_SYMLINK_NOFOLLOW) = 0
865<pam_timestamp_c> newfstatat(AT_FDCWD</home/user>, "/var/run/", {st_mode=S_IFDIR|0755, st_size=740, ...}, AT_SYMLINK_NOFOLLOW) = 0
865<pam_timestamp_c> newfstatat(AT_FDCWD</home/user>, "/var/run/pam_timestamp", {st_mode=S_IFDIR|0700, st_size=100, ...}, AT_SYMLINK_NOFOLLOW) = 0
865<pam_timestamp_c> newfstatat(AT_FDCWD</home/user>, "/var/run/pam_timestamp/user/tty1", {st_mode=S_IFREG|0600, st_size=106, ...}, AT_SYMLINK_NOFOLLOW) = 0
865<pam_timestamp_c> unlink("/var/run/pam_timestamp/user/tty1") = 0
Source:
sudo strace -u user -fyY pam_timestamp_check -k
As far as I can tell from the source code and strace(1) output, pam_timestamp_check(8) doesn’t drop privileges explicitly by calling any of setuid(2), seteuid(2), setreuid(2) or setresuid(2).
The targeted SELinux policy doesn’t define a process transition from the unconfined_t
domain to the pam_timestamp_t
domain (the application domain for the pam_timestamp_exec_t
file type):
[user@fedora ~]$ sudo sesearch -T -s unconfined_t -t pam_timestamp_exec_t
[user@fedora ~]$
Thus, pam_timestamp_check(8) runs in the unconfined_t
domain, its capabilities unconstrained by SELinux.
If no application on the system uses the pam_timestamp
module and there is no need to enable unprivileged users to check or clear their authentication timestamp, then consider unsetting the SUID bit on /usr/sbin/pam_timestamp_check.
1 In linux-pam/modules/pam_timestamp/[email protected], main
prints an error message and sets a nonzero return value when geteuid(2) returns a nonzero value (lines 763–767). Although main
continues setting the program up, its efforts don’t effect any changes to the file system since the core functionality is guarded by conditional statements that check for a zero return value (lines 814, 820 and 826).
- Program to run a new shell or a command within a new shell as another user or group
- Provided by util-linux-2.38.1-4.fc38.x86_64
- Has SHA256 checksum 1acc69d71e2b22e6eed04fdfaa0f85f644fc03d3826635cd35586b93664a2de4
- Has security context
system_u:object_r:su_exec_t:s0
set by the su 1.12.0 module
su(1) must enable unprivileged users to start a new and possibly privileged shell as another user or group, possibly in a pseudoterminal. To this end, su(1) must be able to:
- execute all types of module in the PAM stack defined by /etc/pam.d/su or /etc/pam.d/su-l (
CAP_SETGID
andCAP_SETUID
); - set the UIDs, GIDs and supplementary groups for the new shell (
CAP_SETGID
andCAP_SETUID
); - optionally, change the mode, owner and group of the pseudoterminal for the new shell (
CAP_CHOWN
andCAP_FOWNER
), and - write to the kernel audit log (
CAP_AUDIT_WRITE
).
No. Unless su(1) runs SUID-root, multiple PAM session modules will fail to run.
Each PAM stack uses the pam_keyinit
session module, which changes the RUID and RGID to 0 temporarily to enable the calling process to interact with the kernel’s key management facility.1 To demonstrate this, I unlocked the root account and set a password:
[user@fedora ~]$ sudo passwd -fu root # f: force, u: unlock
Unlocking password for user root.
passwd: Success
[user@fedora ~]$ sudo passwd root
New password:
Retype new password:
passwd: all authentication tokens updated successfully.
I then traced the process of running su(1) to get a root shell and extracted the following fragment:
832<su> getuid() = 1000
832<su> getgid() = 1000
832<su> setregid(0, -1) = 0
832<su> setreuid(0, -1) = 0
832<su> keyctl(KEYCTL_JOIN_SESSION_KEYRING, NULL) = 857786151
832<su> keyctl(KEYCTL_LINK, KEY_SPEC_USER_KEYRING, KEY_SPEC_SESSION_KEYRING) = 0
832<su> setreuid(1000, -1) = 0
832<su> setregid(1000, -1) = 0
Source:
sudo strace -u user -fyY su -l
, where-l
is short for--login
, after truncating whitespace for readability
The setreuid(2) and setregid(2) calls that follow the keyctl(2) calls should result in a loss of all capabilities by design when the SUID bit is unset. Quoting the capabilities(7) man page:
“If one or more of the real, effective, or saved set user IDs was previously 0, and as a result of the UID changes all of these IDs have a nonzero value, then all capabilities are cleared from the permitted, effective, and ambient capability sets.”
When the SUID bit is unset, the EUID and saved SUID stay at 1000 while the RUID changes from 1000 to 0 and then back to 1000 (assuming CAP_SETUID
). Therefore, after interacting with the kernel key management facility, su(1) effectively loses all its privileges. To demonstrate this, I started by making the following changes to the binary of interest:
[user@fedora ~]$ sudo chmod u-s /usr/bin/su
[user@fedora ~]$ sudo setcap cap_setgid,cap_setuid,cap_audit_write=ep /usr/bin/su
To get far enough into the execution of su(1) to see the anticipated error messages, I had to ensure that the pam_unix
authentication module would run to completion. I had conferred neither CAP_DAC_OVERRIDE
nor CAP_DAC_READ_SEARCH
to /usr/bin/su, so su(1) would have to delegate to unix_chkpwd(8) for authentication.
I found that su(1) could not authenticate when /usr/sbin/unix_chkpwd had the SUID bit set and no security.capability
extended attribute:
[user@fedora ~]$ ls -l /usr/sbin/unix_chkpwd
-rwsr-xr-x. 1 root root 32792 Jan 18 2023 /usr/sbin/unix_chkpwd
[user@fedora ~]$ getcap /usr/sbin/unix_chkpwd
[user@fedora ~]$ su -l
Password:
su: Authentication failure
874<su> openat(AT_FDCWD</home/user>, "/etc/shadow", O_RDONLY|O_CLOEXEC) = -1 EACCES (Permission denied)
...
874<su> clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f73e6465a50) = 875<su>
...
875<su> geteuid() = 1000
875<su> execve("/usr/sbin/unix_chkpwd", ["/usr/sbin/unix_chkpwd", "root", "nullok"], 0x7f73e6313040 /* 0 vars */) = 0
...
875<unix_chkpwd> getuid() = 1000
875<unix_chkpwd> setuid(1000) = 0
...
875<unix_chkpwd> openat(AT_FDCWD</home/user>, "/etc/shadow", O_RDONLY|O_CLOEXEC) = -1 EACCES (Permission denied)
Source:
sudo strace -u user -fyY su -l
after truncating whitespace for readability and omitting select syscalls
Why couldn’t unix_chkpwd(8) open /etc/shadow? Because su(1) ran unix_chkpwd(8) as the user user
but asked to verify password information for the user root
—a condition under which the program drops privileges via setuid(2). By unsetting the SUID bit on /usr/sbin/unix_chkpwd and setting the capabilities identified earlier, I could make progress with su(1):
[user@fedora ~]$ sudo chmod u-s /usr/sbin/unix_chkpwd
[user@fedora ~]$ sudo setcap cap_dac_read_search,cap_audit_write=ep /usr/sbin/unix_chkpwd
[user@fedora ~]$ su -l
Password:
su: cannot set group id: Operation not permitted
Despite /usr/bin/su having CAP_SETGID
, su(1) wasn’t able to set the GIDs:
901<su> setgid(0) = -1 EPERM (Operation not permitted)
Source:
sudo strace -u user -fyY su -l
after truncating whitespace for readability
The following excerpt from the systemd journal suggests that this failure was in the pam_keyinit
session module. In addition, the pam_systemd
and pam_lastlog
session modules were also foiled:
Jan 01 00:00:00 fedora audit[887]: USER_AUTH pid=887 uid=1000 auid=1000 ses=1 subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 msg='op=PAM:authentication grantors=pam_unix acct="root" exe="/usr/bin/su" hostname=fedora addr=? terminal=/dev/tty1 res=success'
Jan 01 00:00:00 fedora audit[887]: USER_ACCT pid=887 uid=1000 auid=1000 ses=1 subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 msg='op=PAM:accounting grantors=pam_unix,pam_localuser acct="root" exe="/usr/bin/su" hostname=fedora addr=? terminal=/dev/tty1 res=success'
Jan 01 00:00:00 fedora su[887]: (to root) user on tty1
Jan 01 00:00:00 fedora audit[887]: CRED_ACQ pid=887 uid=1000 auid=1000 ses=1 subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 msg='op=PAM:setcred grantors=pam_unix acct="root" exe="/usr/bin/su" hostname=fedora addr=? terminal=/dev/tty1 res=success'
Jan 01 00:00:00 fedora dbus-broker[656]: A security policy denied :1.24 to send method call /org/freedesktop/login1:org.freedesktop.login1.Manager.CreateSession to org.freedesktop.login1.
Jan 01 00:00:00 fedora su[887]: pam_systemd(su-l:session): Failed to create session: Access denied
Jan 01 00:00:00 fedora su[887]: pam_unix(su-l:session): session opened for user root(uid=0) by user(uid=1000)
Jan 01 00:00:00 fedora su[887]: pam_lastlog(su-l:session): unable to open /var/log/lastlog: Permission denied
Jan 01 00:00:00 fedora su[887]: pam_keyinit(su-l:session): Unable to change GID to 0 temporarily
Jan 01 00:00:00 fedora su[887]: pam_unix(su-l:session): session closed for user root
Source:
sudo journalctl -eb
I was able to use su(1) to get a root shell after making the following changes to the binary:
[user@fedora ~]$ sudo chmod u+s /usr/bin/su
[user@fedora ~]$ sudo setcap cap_setgid,cap_setuid,cap_audit_write=ep /usr/bin/su
Here’s the result:
[user@fedora ~]$ su -l
Password:
[root@fedora ~]# id
uid=0(root) gid=0(root) groups=0(root) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
This shell was unrestricted because it had been executed by a root process whose bounding capability set was full:
[root@fedora ~]# getpcaps $$
912: =ep
So, even though I had constrained the level of privilege attained by su(1), I could still use the resulting shell to perform arbitrary administrative operations.
I also confirmed that I could exit the shell correctly…
[root@fedora ~]# logout
[user@fedora ~]$ id
uid=1000(user) gid=1000(user) groups=1000(user),10(wheel) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
… and that there were no failing PAM modules this time around:
Jan 01 00:00:00 fedora audit[911]: USER_AUTH pid=911 uid=1000 auid=1000 ses=1 subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 msg='op=PAM:authentication grantors=pam_unix acct="root" exe="/usr/bin/su" hostname=fedora addr=? terminal=/dev/tty1 res=success'
Jan 01 00:00:00 fedora audit[911]: USER_ACCT pid=911 uid=1000 auid=1000 ses=1 subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 msg='op=PAM:accounting grantors=pam_unix,pam_localuser acct="root" exe="/usr/bin/su" hostname=fedora addr=? terminal=/dev/tty1 res=success'
Jan 01 00:00:00 fedora su[911]: (to root) user on tty1
Jan 01 00:00:00 fedora audit[911]: CRED_ACQ pid=911 uid=1000 auid=1000 ses=1 subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 msg='op=PAM:setcred grantors=pam_unix acct="root" exe="/usr/bin/su" hostname=fedora addr=? terminal=/dev/tty1 res=success'
Jan 01 00:00:00 fedora su[911]: pam_unix(su-l:session): session opened for user root(uid=0) by user(uid=1000)
Jan 01 00:00:00 fedora audit[911]: USER_START pid=911 uid=1000 auid=1000 ses=1 subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 msg='op=PAM:session_open grantors=pam_keyinit,pam_keyinit,pam_limits,pam_systemd,pam_unix,pam_umask,pam_xauth acct="root" exe="/usr/bin/su" hostname=fedora addr=? terminal=/dev/tty1 res=success'
Jan 01 00:00:00 fedora su[911]: pam_unix(su-l:session): session closed for user root
Jan 01 00:00:00 fedora audit[911]: USER_END pid=911 uid=1000 auid=1000 ses=1 subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 msg='op=PAM:session_close grantors=pam_keyinit,pam_keyinit,pam_limits,pam_systemd,pam_unix,pam_umask,pam_xauth acct="root" exe="/usr/bin/su" hostname=fedora addr=? terminal=/dev/tty1 res=success'
Jan 01 00:00:00 fedora audit[911]: CRED_DISP pid=911 uid=1000 auid=1000 ses=1 subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 msg='op=PAM:setcred grantors=pam_unix acct="root" exe="/usr/bin/su" hostname=fedora addr=? terminal=/dev/tty1 res=success'
Source:
sudo journalctl -eb
after omitting messages relating to systemd-hostnamed and BPF
I successfully reproduced the procedure above with the --pty
flag, which tells su(1) to create a pseudoterminal for the session. I didn’t need to set CAP_CHOWN
or CAP_FOWNER
on /usr/bin/su to enable this use case since these capabilities are unnecessary when the binary is SUID-root.
As far as I can tell from the source code and strace(1) output, su(1) doesn’t drop privileges explicitly by calling any of setuid(2), seteuid(2), setreuid(2) or setresuid(2) unless used to impersonate a nonroot user. This behavior is consistent with the purpose of the program.
The targeted SELinux policy doesn’t define a process transition from the unconfined_t
domain to any application domain for the su_exec_t
file type:
[user@fedora ~]$ sudo sesearch -T -s unconfined_t -t su_exec_t
[user@fedora ~]$
Thus, su(1) runs in the unconfined_t
domain, its capabilities unconstrained by SELinux.
The policy allows more capabilities than I set on /usr/bin/su for the various application domain types, e.g.,
[user@fedora ~]$ sudo sesearch --allow -t sysadm_su_t -c capability,capability2
allow sysadm_su_t sysadm_su_t:capability { audit_control audit_write chown dac_read_search fowner net_bind_service setgid setuid sys_nice sys_resource };
I haven’t made an effort to rationalize these capabilities. If I were having trouble running su(1) with the smaller set above for a particular use case, then I would try adding one or more of the other capabilities allowed by the SELinux policy.
By default, it isn’t possible to use su(1) to get a root shell because root is locked. If this is the case, then consider unsetting the SUID bit on /usr/bin/su without setting file capabilities.
1 In linux-pam/modules/pam_keyinit/[email protected], pam_sm_open_session
calls do_keyinit
(line 280). do_keyinit
calls setregid(2) to change the RGID to the RGID of the PAM user (line 218) and setreuid(2) to change the RUID to the RUID of the PAM user (line 223) if the RGID and RUID aren’t already set to the desired values. do_keyinit
then calls init_keyrings
(line 230), which makes the keyctl(2) calls (lines 105–107 and 115–118). After init_keyrings
returns, do_keyinit
performs the inverse UID and GID transitions (lines 233 and 238, respectively).
- Program to execute a command as another user or group in a configurable manner
- Provided by sudo-1.9.13-1.p2.fc38.x86_64
- Installed with mode
---s--x--x
- Has SHA256 checksum 5221d69e5eff19b57e1d285aa0beebd8cc89762811763c87be4a05ca4389643f
- Has security context
system_u:object_r:sudo_exec_t:s0
set by the sudo 1.10.0 module
sudo(8) expects to run with EUID 0 unconditionally.1 As for specific privileges, sudo(8) must be able to:
- execute all types of module in the PAM stack defined by /etc/pam.d/sudo or /etc/pam.d/sudo-i (
CAP_SETGID
andCAP_SETUID
); - set the UIDs, GIDs and supplementary groups for the command (
CAP_SETGID
andCAP_SETUID
); - zero the hard limit on core dump file size by default (
CAP_SYS_RESOURCE
); - write to the kernel audit log (
CAP_AUDIT_WRITE
), and - read and write arbitrary files via /usr/bin/sudoedit (
CAP_DAC_OVERRIDE
andCAP_FOWNER
, alsoCAP_CHOWN
if using a pseudoterminal).
With the exception of accommodating sudoedit(8), this is similar to what I observed for su(1).
I was able to use sudo(8) to acquire an interactive root shell after making the following changes to the binary:
[user@fedora ~]$ sudo setcap cap_setgid,cap_setuid,cap_sys_resource,cap_audit_write=ep /usr/bin/sudo
[user@fedora ~]$ sudo -i
[root@fedora ~]# id
uid=0(root) gid=0(root) groups=0(root) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
As with su(1), the resulting root shell had all capabilities:
[root@fedora ~]# getpcaps $$
828: =ep
I also confirmed that I could exit the shell correctly:
[root@fedora ~]# logout
[user@fedora ~]$ id
uid=1000(user) gid=1000(user) groups=1000(user),10(wheel) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
After extending the permitted capability set, I could also update root-owned files with sudoedit(8):
[user@fedora ~]$ sudo setcap cap_chown,cap_dac_override,cap_fowner,cap_setgid,cap_setuid,cap_sys_resource,cap_audit_write=ep /usr/bin/sudo
[user@fedora ~]$ sudoedit /etc/fstab
"/var/tmp/fstab.ZRR7sWI8" 14L, 554B written
As far as I can tell from the source code and strace(1) output, sudo(8) doesn’t drop privileges explicitly by calling any of setuid(2), seteuid(2), setreuid(2) or setresuid(2) unless used to impersonate a nonroot user. This behavior is consistent with the purpose of the program.
The targeted SELinux policy doesn’t define a process transition from the unconfined_t
domain to any application domain for the sudo_exec_t
file type:
[user@fedora ~]$ sudo sesearch -T -s unconfined_t -t sudo_exec_t
[user@fedora ~]$
Thus, sudo(8) runs in the unconfined_t
domain, its capabilities unconstrained by SELinux.
The policy allows more capabilities than I set on /usr/bin/sudo for the various application domain types, e.g.,
[user@fedora ~]$ sudo sesearch --allow -t sysadm_sudo_t -c capability,capability2
allow sysadm_sudo_t sysadm_sudo_t:capability { audit_control audit_write chown dac_override dac_read_search fowner setgid setuid sys_nice sys_resource };
As with su(1), I haven’t made an effort to rationalize these capabilities. If I were having trouble running sudo(8) with the smaller set above for a particular use case, then I would try adding one or more of the other capabilities allowed by the SELinux policy.
If the system doesn’t need to support any use cases in which unprivileged users perform privileged actions via sudo(8) or another utility such as pkexec(1) or doas(1) is preferred for this purpose, then consider unsetting the SUID bit on /usr/bin/sudo without setting file capabilities.
A typical recommendation for hardening sudo(8) is to ensure that the program uses a pseudoterminal by default, e.g.,
[user@fedora ~]$ sudo visudo /etc/sudoers.d/pty
Defaults use_pty
After creating this file, I was able to reproduce the operations above without any problems. (This is already the default behavior for Sudo 1.9.14; see issue #258.)
The __linux__
and PR_GET_NO_NEW_PRIVS
macros are assumed to be defined. The __TANDEM
macro is assumed to be undefined.
1 In sudo/src/sudo.c@SUDO_1_9_13p2, main
calls sudo_check_suid
(line 186). If geteuid(2) doesn’t return a value equal to ROOT_UID
(line 924), then sudo_check_suid
either exits the program with an error if the no_new_privs
attribute is set on the calling thread (lines 927 and 932) or calls sudo_fatalx
(lines 963, 967 and 972). (ROOT_UID
is a macro that expands to 0
per line 32 of sudo/include/sudo_util.h@SUDO_1_9_13p2.) In sudo/include/sudo_fatal.h@SUDO_1_9_13p2, sudo_fatalx
is defined as a macro that either expands to or wraps sudo_fatalx_nodebug_v1
(lines 42 and 61–65). In sudo/lib/util/fatal.c@SUDO_1_9_13p2, sudo_fatalx_nodebug_v1
runs support code and then exits the program with an error (line 96).
- Program to execute a command as another user following authorization by Polkit
- Provided by polkit-122-3.fc38.x86_64
- Has SHA256 checksum 464f0d701f86a274378458ccc6e6724b846667f814d2bace025e9b12b6ff6112
- Has security context
system_u:object_r:bin_t:s0
pkexec(1) expects to run with EUID 0 unconditionally.1
Following authorization by a separate agent, pkexec(1) must be able to open a PAM session per the service configuration file at /etc/pam.d/polkit-1. This stack includes the session management stack in /etc/authselect/system-auth:
[user@fedora ~]$ cat /etc/pam.d/polkit-1 | grep -E '^-?session'
session include system-auth
In order to run the modules in this stack, pkexec(1) must run with EUID 0 and its effective capability set must contain CAP_SETGID
and CAP_SETUID
(see the dicussion of /usr/bin/su).
To answer this question, I first had to work around issue #17 in a way that would let me trace pkexec(1) without also tracing the authentication agent and helper. I started by writing the following Bash script (as pkwrap.sh):
#!/bin/bash
#
# SPDX-FileCopyrightText: Copyright 2023 OK Ryoko
# SPDX-License-Identifier: GPL-3.0-or-later
set -eu
echo "Bash PID: ${BASHPID}"
read -n 1 -p 'Attempt pkexec? [y]: '
if ! [[ "${REPLY}" == 'y' || -z "${REPLY}" ]]; then
echo 'Bailing...'
exit 0
fi
pkexec true
exit 0
I ensured the script was executable:
[user@fedora ~]$ chmod 0770 pkwrap.sh
I then executed the script in tty1 without entering any input:
[user@fedora ~]$ ./pkwrap.sh
Bash PID: 830
Attempt pkexec? [y]:
I logged into tty2 as the same user and spun up the authentication agent using the provided PID:
[user@fedora ~]$ pkttyagent -p 830 # p: process
pkttyagent(1) is a textual Polkit authentication helper whose SUID bit is unset and that has no file capabilities:
[user@fedora ~]$ ls -lZ /usr/bin/pkttyagent
-rwxr-xr-x. 1 root root system_u:object_r:bin_t:s0 24472 Jan 19 2023 /usr/bin/pkttyagent
[user@fedora ~]$ getcap /usr/bin/pkttyagent
[user@fedora ~]$
Back in tty1, I gave the shell script the go-ahead. In tty2, I was greeted with and completed an authentication prompt:
==== AUTHENTICATING FOR org.freedesktop.policykit.exec ====
Authentication is needed to run `/usr/bin/true' as the super user
Authenticating as: Fedora User (user)
Password:
==== AUTHENTICATION COMPLETE ====
I pressed Ctrl+C
to kill pkttyagent(1) and returned to tty1 to find that the script had succeeded.
Having confirmed the end-to-end functionality of pkwrap.sh, I reproduced this procedure successfully after making the following changes to the binary:
[user@fedora ~]$ sudo setcap cap_setgid,cap_setuid=ep /usr/bin/pkexec
pkexec(1) drops privileges explicitly only when the --user
option is supplied on the command line; it otherwise runs the given program as root.
The targeted SELinux policy doesn’t restrict the permissions of the bin_t
domain on the capability
and capability2
classes, so pkexec(1) runs unconfined.
If the system doesn’t need to support any use cases in which unprivileged users perform privileged actions via pkexec(1) or another utility such as sudo(8) or doas(1) is preferred for this purpose, then consider unsetting the SUID bit on /usr/bin/pkexec without setting file capabilities.
pkexec(1) becomes root before opening a PAM session and changing to the user as which the given program should run:2
919<pkexec> setreuid(0, 0, <unfinished ...>
920<pool-spawner> futex(0x562b40899a50, FUTEX_WAIT_PRIVATE, 0, NULL <unfinished ...>
919<pkexec> <... setreuid resumed>) = 0
919<pkexec> geteuid() = 0
919<pkexec> getuid() = 0
919<pkexec> getegid() = 0
919<pkexec> getgid() = 0
Source:
sudo strace -u user -fyY ./pkwrap.sh
after truncating whitespace for readability
Therefore, unless pkexec(1) drops privileges before running the given program, the new process will have all capabilities:
[user@fedora ~]$ pkexec getpcaps "\${BASHPID}"
${BASHPID}: =ep
All line numbers refer to polkit/src/programs/pkexec.c@122.
1 After parsing the command line, main
errors out if geteuid(2) doesn’t return 0 (lines 577–582)
2 If not executing the given program as root, main
calls setreuid(2) to change the RUID and EUID to 0, erroring out in the event of failure (lines 950–959)
- Polkit agent helper to re-authenticate a user
- Provided by polkit-122-3.fc38.x86_64
- Has SHA256 checksum d0969c74c61ee3d1266c15e7d25b7e4f40f66f36de81253eb668c880f2db4651
- Has security context
system_u:object_r:policykit_auth_exec_t:s0
set by the policykit 1.3.0 module
polkit-agent-helper-1 expects to run with EUID 0 unconditionally.1 As for specific privileges, polkit-agent-helper-1 must be able to write to the audit log (CAP_AUDIT_WRITE
).
polkit-agent-helper-1 is a helper program intended to be run by other applications. It expects those applications to provide an authentication cookie over standard input. To answer this question, I needed to run this program indirectly so as to facilitate tracing. First, I logged into tty2 and collected the shell’s PID:
[user@fedora ~]$ echo $$
830
I then ran pkttyagent(1) in tty1:
[user@fedora ~]$ pkttyagent -p 830
Back in tty2, I tried to use pkexec(1) to run a trivial program as root:
[user@fedora ~]$ pkexec true
In tty1, I was asked by Polkit to authenticate:
==== AUTHENTICATING FOR org.freedesktop.policykit.exec ====
Authentication is needed to run `/usr/bin/true' as the super user
Authenticating as: Fedora User (user)
Password:
==== AUTHENTICATION COMPLETE ====
I pressed Ctrl+C
to kill pkttyagent(1) and returned to tty2 to find that the command had succeeded.
Having validated the end-to-end functionality of pkttyagent(1), I reproduced this procedure successfully after making the following changes to the binary:
[user@fedora ~]$ sudo setcap cap_audit_write=ep /usr/lib/polkit-1/polkit-agent-helper-1
I then repeated the procedure a third time, this time entering an incorrect password to ensure that a corresponding entry would be recorded in the audit log:
type=USER_AUTH msg=audit(0.0:126): pid=914 uid=1000 auid=1000 ses=3 subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 msg='op=PAM:authentication grantors=? acct="user" exe="/usr/lib/polkit-1/polkit-agent-helper-1" hostname=fedora addr=? terminal=tty1 res=failed'UID="user" AUID="user"
Source:
sudo tail -n 64 /var/log/audit/audit.log | grep 'polkit-agent-helper-1' | grep 'failed'
As far as I can tell from the source code and strace(1) output, polkit-agent-helper-1 doesn’t drop privileges explicitly by calling any of setuid(2), seteuid(2), setreuid(2) or setresuid(2).
The targeted SELinux policy defines many transitions to the policykit_auth_t
domain through the policykit_auth_exec_t
file type:
[user@fedora ~]$ sudo sesearch -T -t policykit_auth_exec_t | wc -l
31
Familiar examples of source domains include NetworkManager_t
, system_dbusd_t
and user_t
. However, unconfined_t
is not one of them:
[user@fedora ~]$ sudo sesearch -T -s unconfined_t -t policykit_auth_exec_t
[user@fedora ~]$
Thus, when pkttyagent(1) is used as the authentication agent, polkit-agent-helper-1 runs unconfined.
What capabilities can processes running in the policykit_auth_t
domain have?
[user@fedora ~]$ sudo sesearch --allow -t policykit_auth_t -c capability,capability2
allow policykit_auth_t policykit_auth_t:capability { audit_write ipc_lock setgid setuid sys_nice };
There should therefore not be any issues with setting CAP_AUDIT_WRITE
on /usr/lib/polkit-1/polkit-agent-helper-1.
polkit-agent-helper-1 must be able to authenticate a user per the PAM service configuration file at /etc/pam.d/polkit-1, which includes the authentication stack in /etc/authselect/system-auth:
[user@fedora ~]$ cat /etc/pam.d/polkit-1 | grep -E '^-?auth'
auth include system-auth
polkit-agent-helper-1 doesn’t require any privileges to perform this action (see my comments on /usr/bin/su).
- Issue #168: polkit-agent-helper-1 is setuid root and runnable by ordinary users, does it need to be?
1 In polkit/src/polkitagent/polkitagenthelper-pam.c@122, main
errors out if geteuid(2) doesn’t return 0 (lines 93–105)
- OpenSSH helper for host-based authentication
- Provided by openssh-9.0p1-14.fc38.1.x86_64
- Installed with mode
-r-sr-xr-x
- Has SHA256 checksum 07fba19bd34da8334a211295d3f0ccb19b3a8d40df3d47ef117609fd96ec4d14
- Has security context
system_u:object_r:ssh_keysign_exec_t:s0
set by the ssh 2.4.2 module
ssh-keysign(8) must be able to read the SSH host keys in /etc/ssh (CAP_DAC_READ_SEARCH
).
I was able to perform host-based authentication to another Fedora Custom OS 38 VM after making the following changes to the binary on the client machine:
[user@fedora ~]$ sudo chmod u-s /usr/libexec/openssh/ssh-keysign
[user@fedora ~]$ sudo setcap cap_dac_read_search=ep /usr/libexec/openssh/ssh-keysign
Yes, after opening the host keys and fetching the password entry for the user:1
829<ssh-keysign> openat(AT_FDCWD</home/user>, "/etc/ssh/ssh_host_rsa_key", O_RDONLY) = 6</etc/ssh/ssh_host_rsa_key>
...
829<ssh-keysign> setresgid(1000, 1000, 1000) = 0
829<ssh-keysign> setresuid(1000, 1000, 1000) = 0
Source:
sudo strace -u user -fyY ssh [email protected]
after omitting syscalls related to pwgetuid(3)
Recall that ssh-keysign(8) is intended to be run as a helper by ssh(1).
The targeted SELinux policy declares a process transition from the ssh_t
domain to the ssh_keysign_t
domain (the application domain for the ssh_keysign_exec_t
file type)…
[user@fedora ~]$ sudo sesearch -T -s ssh_t -t ssh_keysign_exec_t
type_transition ssh_t ssh_keysign_exec_t:process ssh_keysign_t; [ ssh_keysign ]:True
… but no transition from unconfined_t
to ssh_t
:
[user@fedora ~]$ sudo sesearch -T -s unconfined_t -t ssh_t
[user@fedora ~]$
Thus, ssh(1) and ssh-keysign(8) run in the unconfined_t
domain, their capabilities unconstrained by SELinux.
Host-based authentication is deemed insecure by the ssh(1) man page and disabled by default. Consider unsetting the SUID bit on /usr/libexec/openssh/ssh-keysign without setting file capabilities.
The NO_UID_RESTORATION_TEST
and __APPLE__
macros are assumed to be undefined.
1 In openssh-portable/ssh-keysign.c@V_9_0_P1, main
tries to open up to five host key files (lines 202–206). main
then calls getpwuid(3) to retrieve the password entry for the invoking user, copying the data to the pw
local variable (lines 208–210). Finally, main
passes pw
to permanently_set_uid
(line 212). In openssh-portable/uidswap.c@V_9_0_P1, permanently_set_uid
calls setresgid(2) (line 195) and setresuid(2) (line 208) to assume the user and primary group stored in pw
.
- Program to set a bootflag in the GRUB environment block
- Provided by grub2-tools-minimal-1:2.06-89.fc38.x86_64
- Has SHA256 checksum c2b68f378343f65fd471aa6a08926214520f7196b737c478f96d74eafc8ead11
- Has security context
system_u:object_r:bootloader_exec_t:s0
set by the bootloader 1.14.0 module
grub2-set-bootflag(1) sets its UIDs and GIDs to 0 unconditionally (CAP_SETUID
and CAP_SETGID
) so that it can read and write the GRUB environment block at /boot/grub2/grubenv while ensuring that the owner and group of the file remain root
. The program also does this to protect itself from kill signals.1
I was able to run the program to set the boot_success
flag after making the following changes to the binary:
[user@fedora ~]$ sudo chmod u-s /usr/sbin/grub2-set-bootflag
[user@fedora ~]$ sudo setcap cap_setgid,cap_setuid=ep /usr/sbin/grub2-set-bootflag
To test these changes, I started by rebooting:
[user@fedora ~]$ shutdown -r now # r: reboot
After logging in, I checked whether the boot_success
flag had been set:
[user@fedora ~]$ sudo cat /boot/grub2/grubenv | grep 'boot_success'
boot_success=0
It hadn’t. This is because the grub-boot-success service doesn’t run until two minutes into the user session:
[user@fedora ~]$ systemctl --no-pager --user show grub-boot-success.timer | grep '^TimersMonotonic'
TimersMonotonic={ OnActiveUSec=2min ; next_elapse=2min 8.612087s }
… giving me a small window in which I could test the file capabilities:
[user@fedora ~]$ grub2-set-bootflag boot_success
[user@fedora ~]$ sudo cat /boot/grub2/grubenv | grep 'boot_success'
boot_success=1
As far as I can tell from the source code and strace(1) output, grub2-set-bootflag(1) becomes root immediately after processing the command line and doesn’t drop privileges explicitly by calling any of setuid(2), seteuid(2), setreuid(2) or setresuid(2).
The targeted SELinux policy doesn’t define a process transition from the unconfined_t
domain to the bootloader_t
domain (the application domain for the bootloader_exec_t
file type):
[user@fedora ~]$ sudo sesearch -T -s unconfined_t -t bootloader_exec_t
[user@fedora ~]$
Thus, grub2-set-bootflag(1) runs in the unconfined_t
domain, its capabilities unconstrained by SELinux.
1 In grub/util/grub-set-bootflag.c after applying the patches in src.fedoraproject.org/rpms/grub2@48cf39d, main
calls setuid(2) (line 99) and setgid(2) (line 106) to become root, erroring out if either syscall fails
I was able to substitute the SUID bit using file capabilities on the following binaries:
- /usr/bin/chage
- /usr/bin/gpasswd
- /usr/bin/newgrp
- /usr/sbin/unix_chkpwd
- /usr/libexec/openssh/ssh-keysign
- /usr/sbin/grub2-set-bootflag
The SUID bit on the remaining binaries had to be set. However, I could still set file capabilities on those binaries to limit the level of privilege attainable by the respective programs:
- /usr/bin/passwd
- /usr/bin/mount
- /usr/bin/umount
- /usr/sbin/pam_timestamp_check
- /usr/bin/su
- /usr/bin/sudo
- /usr/bin/pkexec
- /usr/lib/polkit-1/polkit-agent-helper-1
I determined that none of these programs are capability-aware, i.e., they don’t use the libcap(3) API.
For unconfined processes, I determined that the default targeted SELinux policy constrains the level of privilege attainable by the programs corresponding to the following binaries:
- /usr/bin/passwd
- /usr/sbin/unix_chkpwd
The remaining programs generally run unconfined.
To get a high-level idea of the privileges needed by the SUID-root binaries that come with Fedora Custom OS 38, I tallied the file capabilities that I ended up setting to enable the reference use cases:
CAP_CHOWN ┤██████ 3
CAP_DAC_OVERRIDE ┤██████ 3
CAP_DAC_READ_SEARCH ┤████████ 4
CAP_FOWNER ┤██ 1
CAP_SETGID ┤████████████ 6
CAP_SETUID ┤████████████ 6
CAP_SYS_ADMIN ┤████ 2
CAP_SYS_RESOURCE ┤██ 1
CAP_AUDIT_WRITE ┤██████████████████ 9
My work has led me to pose the following questions:
- Are there any use cases not enabled by the file capabilities that I identified above?
- What additional SUID-root binaries are included in a vanilla installation of Fedora Workstation 38?
- What is the state of SUID-root binaries in other independent Linux distributions such as Debian Linux?
API Application programming interface
BPF Berkeley Packet Filter
CC Creative Commons
EGID Effective GID
EUID Effective UID
FSUID File system UID
GID Group ID
GNU GNU’s Not Unix
GRUB Grand Unified Bootloader
ID Identifier
IP Internet Protocol
NIS Network Information Service
OS Operating system
PAM Pluggable Authentication Modules
PID Process ID
RGID Real GID
RPM RPM Package Manager
RUID Real UID
SELinux Security-Enhanced Linux
SHA Secure Hash Algorithm
SSH Secure Shell
SSSD System Security Services Daemon
SUID Set-UID
UID User ID
UUID Universally unique ID
VM Virtual machine
Please connect with me if you have constructive comments about this article, especially if you have a use case I didn’t cover or are unable to reproduce a procedure in an identical environment.
Fedora Linux 38 reached end of life on May 21, 2024. I am therefore no longer maintaining this article. However, I may incorporate relevant constructive feedback into reports pertaining to newer releases of Fedora Linux.
The author acknowledges private funding by one anonymous sponsor.
The C code for the programs unix_chkpwd_wrapper and timestamper, and the Bash script pkwrap.sh, are provided under the GNU General Public License v3.0 or later.
All other original copyrightable content in this document is marked with CC0 1.0 Universal.
Bump revision number
Shorten title
- Orient report around Fedora Custom OS 38
- Add status header
- Expand OS abbreviation in Appendix A
- Remove section titled “Disadvantages of setting file capabilities”
- State discontinuation of maintenance of this article
- Annotate short options to commands in Characterization of the environment
- Expand CC abbreviation in Appendix A
- Add detail to revision history
Fix broken hyperlink in revision history
- Mention significance of
CAP_SYS_RESOURCE
for /usr/bin/sudo - Clarify use case for sudoedit(8)
- Update capability histogram
- Shorten article name
- Add fdisk(8) command line to procedure for /usr/bin/mount
- Format
fdisk
as fdisk(8) in body text - Annotate short options to commands
- Add SSSD, OpenSSH and GRUB hyperlinks
- Trim stray whitespace
- Provide more exposition for source code references
- Fix source code references for /usr/bin/mount
- Reduce scope of source code reference for /usr/bin/gpasswd and add supporting line from strace(1) output
- Be more careful when making statements about unconditional code execution
- Expand NIS abbreviation in Appendix A
- Add Fedora Linux 38 GPG key prompt
- Link to new article: SUID-root Binaries in Fedora Workstation 38
- Delegate open question about Fedora Linux 39 to new article
- Add section detailing feedback and support
- Expand BPF, GNU, ID, IP, SELinux, SHA and UUID abbreviations in Appendix A
- Update wording to accommodate stylistic preferences
/usr/bin/su:
- Expound su(1)–unix_chkpwd(8) interaction
- Defer
--pty
use case
/usr/bin/mount:
- Add confidence check
- Discuss permissions of
mount_t
domain oncapability
andcapability2
classes
- Check for allowed permissions of
sysadm_su_t
,sysadm_sudo_t
andpolicykit_auth_t
oncapability2
- Compile unix_chkpwd_wrapper.c and timestamper.c with
-Wall
- Mention presence of
security.selinux
extended attribute on binaries - Add libvirt hyperlinks
- Add
SPDX-FileCopyrightText
tag to source code headers - Ensure consistent ordering of strace(1) options
- Avoid potentially ableist language:
s/sanity check/confidence check/g
- Correct typos
- Use common names for Fedora flavors, e.g., “Fedora Server 38” instead of “Fedora Linux 38 Server”
- Be specific about the particular flavor of Fedora in question (Server)
- Distinguish minimal installation of Fedora Server from installation of Fedora Minimal
- Replace text with abbreviation
- Push mention of libcap(3) to “The findings at a glance”
- Call pkgconf(1) when compiling timestamper
- Drop Arch Linux and Gentoo Linux as candidates for investigation because they emphasize choice and control, leading to relatively high variation across installations
- Add statements to clarify how output was captured
- Format SELinux class names as code
- Look up permissions on the
capability2
class for thepasswd_t
andchkpwd_t
domains - Obfuscate IPv4 address per RFC 5737
- Replace list of capability counts with textual histogram
Remove references to file capabilities for processes run as root (all UIDs 0) because they get ignored
Initial revision