A “Simple” OS Command Injection Challenge

Eileen Tay
CSG @ GovTech
Published in
6 min readOct 29, 2020

Introduction

This article will recount how I solved a custom-made Capture-The-Flag (CTF) challenge with an innovative solution that gave me an opportunity to give back to the open source repository — “swisskyrepo/PayloadsAllTheThings”.

TLDR; I made use of environment variables and Bash brace expansions to bypass a character blacklisting filter.

It all began one fine weekend when my friend wanted me to test his “simple” custom-made OS command injection CTF challenge in PHP. He had attempted to “secure” his vulnerable app with blacklisting techniques to thwart attackers from getting an easy injection.

What is OS Command Injection?

OWASP’s description provides one of the better definitions:

Command injection is an attack in which the goal is the execution of arbitrary commands on the host operating system via a vulnerable application. Command injection attacks are possible when an application passes unsafe user supplied data (forms, cookies, HTTP headers etc.) to a system shell.

The challenge

The application was a simple PHP ping webpage that accepts IP addresses utilising an underlying Linux “ping” command to test if the device is reachable.

Figure 1 — Simple Ping webpage

The goal of the challenge was to obtain the two flags my friend had planted: one in the same directory, and the other in another directory.

Figure 2 — Windows vs Linux PING output

Starting from the PING output, I derived that the underlying OS was most likely Linux as a Window’s OS “ping” starts with the word “Pinging” while Linux’s “ping” starts with the capitalised word “PING” as shown in Figure 2.

Figure 3 — Command Injection failed

It quickly occurred to me that the challenge was not as simple as placing Linux commands behind a semi-colon. When attempting to perform a list command, it failed to return any output as seen in Figure 3.

Figure 4 — Source code with multiple filters including blacklisting commands

The provided source code revealed multiple layers of filters to thwart an attacker with multiple common commands blacklisted as seen on line 21 of Figure 4. Without these commands, it was difficult to find the location of the flags and retrieve their content.

#1 — Bypass blacklisted commands

Figure 5 — Bypass hardcoded commands with quotes

With a bit of Google “kungfu”, I easily found some useful bypasses documented on one of my favourite GitHub repositories — “swisskyrepo/PayloadsAllTheThings”.

Figure 6 — Experiment on Linux

After experimenting on my own local Kali Linux machine, I observed that single quotes without spaces in between led to string concatenation. This explained the bypasses from the repository.

Figure 7 — Successfully performed directory listing

Feeling confident with this new knowledge, I proceeded to bypass the blacklisted commands check and successfully listed files in the current directory, bringing me a step closer to the first flag.

#2 — Bypass blacklisted characters (whitespace)

Figure 8 — Failed injection of Commands with options (requires whitespace)

The same directory contained a “user.txt” file which would have awarded me the first flag. However, I realised that commands requiring more than a single word could not be injected. In Figure 8, the long format option for “ls” failed because it required 2 words — “ls” and “-la”.

Figure 9 — Blacklisting of characters

Revisiting the source code revealed that the developer had blocked many special characters, such as whitespace on line 17 of Figure 9. Whitespace is crucial to split commands into words and with this filter, I was limited to single-word commands.

Figure 10 — Whitespace bypass with $IFS

So I went back to the same GitHub repository and was intrigued by some interesting whitespace bypasses I found. The third bypass utilised the character ‘$’ (see Figure 10), that fortunately was not found within the blacklisted character set.

Figure 11 — Bash documentation on $IFS

Curious to find out how the bypass works, I read further on other bypass techniques and came across online Bash documentation about “$IFS” as a delimiter. It described “$IFS” as a special shell environment that splits up commands into words in Bash.

Figure 12 — Successful injection of command with options (with whitespace)
Figure 13 — Obtained the 1st Flag

Using the “$IFS” environment variable, I successfully bypassed the whitespace requirement and retrieved the first flag. The contents of the file provided a hint to the location of the second flag.

#3 — Bypass blacklisted characters (slashes)

Figure 14 — Failed to read second file

Working towards retrieving the second flag proved to not be as simple as bypassing the “/” character found within the blacklisted character set! There were no convenient bypass techniques for slashes online. Other bypass techniques required the use of the “\”, “|”, or “<” characters, all of which were blacklisted.

Figure 15 — Reconnaissance of Environment variables

I began investigating the environment variables found on the server and realised that the flag was located in the home directory of the user. This led to my next question:

“Could I use the $HOME directory to read the flag?”

Figure 16 — Insufficient slash

My attempt was unsuccessful as the “$HOME” environment variable lacked an additional “/” character to read the “root.txt” file.

Figure 17 — String splicing in Bash

Undeterred, I spent a few hours researching when an idea sparked dawned upon me: what if I tried using string manipulation! Upon conducting further research on Bash, I learnt that Bash allows string splicing to extract substring from a variable. Since the “$HOME” directory contains “/”, I could therefore splice the “/” character from the variable itself!

Figure 18 — Forming the full path with string splicing

Combining all the knowledge I acquired, I pieced together the entire path to the second flag!

Figure 19 — Successfully obtained 2nd Flag

I successfully retrieved the second flag and overcame the challenge!

My payload impressed my friend; he had not expected me to bypass the “/” character because there were no public write ups on this bypass.

Contribution back to the community

Figure 20 — GitHub pull request to PayloadsAllTheThings

We then discussed on how my payload could be useful in bypassing character filter and decided to share my experience in the PayloadAllTheThings repository to benefit the Pen-testing community.

# Tips for defenders

When filtering special characters to solve OS command injection, remember to include special characters such as quotes and “;${}”, that can be cleverly manipulated to craft bypasses.

# Other Defences for command injection attacks

1. The best defence is to avoid calling the OS system directly.

2. Depending on your program’s context, validate and restrict inputs to good, whitelisted, and known inputs. In the Ping example, one can use regular expressions to ensure the inputs fits the format of IPv4 or IPv6. All mismatched inputs should otherwise be rejected.

3. Use language-specific secure functions. For example, in PHP use escapeshellarg() or escapeshellcmd() instead of exec(), system(), passthru().

Find out more here:
https://cheatsheetseries.owasp.org/cheatsheets/OS_Command_Injection_Defense_Cheat_Sheet.html

Conclusion

I find that such challenges broaden my knowledge of command injections and strengthen my thought process when facing difficulties. In general, we may know how command injections work. In reality, they pose a different challenge when you exploit one that claims to be “well-secured” by filters.

--

--