OATH Toolkit pam_oath usersfile ${HOME}
privilege escalation (CVE-2024-47191)
Security Vulnerability
OATH Toolkit provides two components liboath
and pam_oath
.
Pam_oath is normally run as root and assumes that the usersfile
path
setting value points to a root-controlled and protected file
containing the OATH secrets (cryptographic HMAC keys) for users.
The documentation suggests using a root-owned /etc/users.oath
and to
do chmod go-rw /etc/users.oath
on it. The design assumes that file
permissions are set up to prevent malicious read or writes to the
credential file by unauthorized users. Whether file permissions are
setup correctly or not is not checked by the code.
On every successful authentication, the file is rewritten to prevent
OTP replay attacks. The rewriting logic works by acquiring a POSIX
file lock on a newly created *.lock
file (in the same directory),
and then writing file content to a newly created *.new
file (also in
the same directory). File ownership of the new file is set to the
same as the original file. The original usersfile is replaced
atomically with the newly created file.
With the introduction of the ${HOME}
indirection variable in the
usersfile parameter the design assumptions no longer holds. The
feature was added in version 2.6.7 released on 2021-05-01.
A typical setup when ${HOME}
is used in a usersfile value such as
usersfile=${HOME}/.config/oath.secrets
is to allow users to manage
their own credentials. The file is owned by the user who is
responsible for adding the secret to it, and to set read/write
permissions on the file appropriately.
The security problem is easy to exploit. To demonstrate the
vulnerability with a vulnerable version and configuration, create a
symbolic link $HOME/.config/oath.secrets.new
that points to a
privileged file such as /etc/shadow
. After successful login as the
user, pam_oath
/liboath
has followed the symbolic link and rewrote
the target file with new updated OATH credentials and sets ownership
of that file to the user. The user is now able to modify
/etc/shadow
.
We are not aware of any active exploits in the wild of this flaw.
Affected versions and configurations
OATH Toolkit pam_oath and liboath version 2.6.7 to version 2.6.11 are affected. Version 2.6.12 prevents the attack.
The attack requires that the "usersfile" setting has a file or path
component that is in a vulnerable location. The common setup with a
write/read-protected usersfile=/etc/users.oath
setup is not
vulnerable.
While admin’s may specify a "usersfile" in a world-writeable directory
like /tmp
we regard that as a configuration error. In most
scenarios, only "usersfile" with ${HOME}
in them should be regarded
as a vulnerable configuration.
One vulnerable setting is usersfile=/home/joe/.config/system.oath
giving joe the ability to modify root-owned files, assuming a non-root
user joe with write access to anything below /home/joe
. This is
somewhat similar to having a system SSH configuration of HostKey
/home/joe/ssh_hostkey
which we believe is unlikely and also consider
to be a configuration error.
Another example is via the ${USER}
setting as in
usersfile=/home/${USER}/.config/oath.secrets
giving any non-root
user the ability to control system files, assuming they have write
access to anything below /home/${USER}
. We have improved the
documentation regarding ${USER}
to be for those settings where
root-controlled per-user files are desired. The intended use of
${USER}
strings are for setups like
usersfile=/var/oath/oath.${USER}.secrets
where the files are
per-user but owned by root and have file permissions setup to prevent
read/write from the user. The root-ownership and file permission
should prevent users from being able to reach their OATH credentials
when ${USER}
is used.
The pam_oath
fix only address configurations that uses ${HOME}
and
not any other vulnerable configurations. The liboath fix prevent
direct attacks via *.new
and *.lock
symlinks, but other scenarios
are possible and the input to oath_authenticate_usersfile()
MUST be
a trusted filename — suitable sanitization is application-dependent.
Details
The vulnerable code in liboath is inside
oath_authenticate_usersfile()
, quoting code from version 2.6.11
which can be reviewed here:
The lock file is acquired:
/* Open lockfile. */
{
int l;
l = asprintf (&lockfile, "%s.lock", usersfile);
if (lockfile == NULL || ((size_t) l) != strlen (usersfile) + 5)
return OATH_PRINTF_ERROR;
lockfh = fopen (lockfile, "w");
if (!lockfh)
{
free (lockfile);
return OATH_FILE_CREATE_ERROR;
}
}
Since fopen("w")
is used any existing file with the predictable
*.lock
filename will be opened, including if that file happens to be
a symbolic link that points elsewhere. The code is run as root, so a
symbolic link pointing to confidential files under /etc
are happily
opened. Note that since "w" is used, the file will be automatically
truncated. So this allows non-root to truncate root-owned files.
Reading on to the *.new
file handling:
outfh = fopen (newfilename, "w");
if (!outfh)
{
free (newfilename);
fclose (lockfh);
free (lockfile);
return OATH_FILE_CREATE_ERROR;
}
Similarily, this will open a file for writing, and later code will essentially copy data from the existing file into the new one. This new file is moved back into the original place.
The PAM module invokes oath_authenticate_usersfile()
in
pam_oath.c
, which can be shown here:
The code is running as root so it may work on an untrustworthy
filename. The design used to be that admin’s specify a trusted
pathname here, but the ${HOME}
use-case broke this design assumption
and the pathnames should then no longer be considered trusted.
Solution
Version 2.6.12 contains the following liboath patch to use fopen(wx)
:
Some non-glibc and non-ISO C11 platforms needs the following patch to
enable gnulib’s fopen(wx)
workaround:
Then pam_oath.c
is modified to call seteuid()/setegid() as follows:
A patch that applies cleanly to version 2.6.7 found in Debian 12.x bookworm is available here:
We recommend you to upgrade to version 2.6.12.
If that is unpractical we recommended you to apply the patches on top of your earlier version.
Reproducer
Included in the 2.6.12 release is a C program to test if a liboath is vulnerable or not. It can be built as follows on a system with liboath properly installed:
git clone https://gitlab.com/oath-toolkit/oath-toolkit.git
cd oath-toolkit
git checkout oath-toolkit-2.6.12
cd liboath/tests
cc -o tst_fopen-wx tst_fopen-wx.c $(pkg-config --libs --cflags liboath)
rm -f cve.oath cve.oath.new cve.sshd-config cve.oath.lock
printf 'HOTP/E/8\tsilver\t4711\t3132333435363738393031323334353637383930313233343536373839303132\n' > cve.oath
echo my-magic-cookie > cve.sshd-config
ln -s cve.sshd-config cve.oath.new
./tst_fopen-wx cve.oath silver 670691 4711
When invoked on a Trisquel 11 system with liboath0 and liboath-dev version 2.6.7-3build1 installed, it will print the following:
Liboath fopen(wx) bug test for oath.h 2.6.7 liboath.so 2.6.7
FAIL: Liboath VULNERABLE to fopen(wx) bug.
To test a particular liboath use LD_PRELOAD as follows:
LD_PRELOAD=/usr/local/lib/x86_64-linux-gnu/liboath.so.0 ./tst_fopen-wx cve.oath silver 670691 4711
The liboath/tests/tst_fopen-wx.sh script can be used to setup and invoke the C program testing two different vulnerable configurations.
Related work
The logic for the usersfile handling was inspired by earlier versions of mod-authn-otp and pam_google_authenticator although their modern design appears to be somewhat different from pam_oath’s current code.
While the liboath oath_authenticate_usersfile() vulnerability is not a typical "time-of-check, time-of-use" race condition (since there is no check happening in the code) it exhibits the same pattern in the code since fopen(w) is used instead of fopen(wx). Running GitLab semantic analysis (SAST) on the vulnerable code flagged it as problematic:
We have enabled GitLab SAST and Coverity scanning of OATH Toolkit and will review the findings.
SUSE’s alternative patch and advisory can be found via:
It rely on Linux kernel specific features and uses fork() which was determined to be contrary to the liboath design, which aims to be portable to macOS and *BSD and beyond. This alternative patch may be used by some vendors, and it is assumed to fix the security problem.
History
Fabian Vogt reported this issue in private e-mail on 2024-08-08. Matthias Gerstner reported the issue as a GitLab confidential issue on 2024-08-20. These reports came in during vacation time and were not read by the maintainer. Salvatore Bonaccorso reached out on 2024-09-29 via SMS, and the progress since then has been tracked in the bug tracker:
Credits
The problem was discovered by Fabian Vogt of SUSE. An initial patch against liboath was developed by Matthias Gerstner of SUSE Security Team. An alternative and portable patch to liboath and pam_oath were developed by Simon Josefsson.