Link Search Menu Expand Document

OS Command Injection in .NET

Play SecureFlag Play .NET Labs on this vulnerability with SecureFlag!

Vulnerable example

The following snippet is an ASP.NET controller action that uses gzip to compress and download the file path passed in the GET parameter filePath.

public async Task<IActionResult> compressAndDownloadFile()
{

  string filePath = Request.Query["filePath"];
  procStartInfo = new System.Diagnostics.ProcessStartInfo(
    "/bin/bash",
    $"-c \"cat {filePath} | gzip > {filePath}.gz\""
  );
  procStartInfo.UseShellExecute = false;
  procStartInfo.CreateNoWindow = true;
  System.Diagnostics.Process proc = new System.Diagnostics.Process();
  proc.StartInfo = procStartInfo;
  proc.Start();
  proc.WaitForExit();

  return File($"{filePath}.gz", "application/gzip");
}

Because the command passed to System.Diagnostics.ProcessStartInfo() uses bash -c concatenate with user-provided data, this code is susceptible to a command injection attack.

An attacker could invoke the controller action with /compressAndDownloadFile?filePath=x; rm -rf .; to inject the command rm -rf to delete every file. More complex attacks can be used to fully compromise the application and the underlying operating system.

Prevention

Solution #1 (Avoid running external commands)

When the task performed by executing a system command can be accomplished by some other means, it is almost always advisable to do so. The example above could be rewritten using the System.IO.Compression native C# library to handle gzip files without the need of running external commands.

Solution #2 (Avoid running shell commands)

Never run commands using system shells such as cmd.exe or /bin/bash. Try to rework the code to avoid using shell functionalities such as redirection or pipes. The command execution in the vulnerable example can be rewritten as the following to resolve the injection vulnerability.

  procStartInfo = new System.Diagnostics.ProcessStartInfo(
    "/bin/gzip",
    filePath
  );

Solution #3 (Allow using regular expressions)

This solution sanitizes the untrusted user input by permitting only a small group of allowed characters in the argument to be passed to the command execution. All other characters are excluded.

Regex regex = new Regex(@"^[0-9A-Za-z_/-]+$");
Match match = regex.Match(filePath);
if (!match.Success) {
  return BadRequest("Error, some characters in the file path are not allowed.");
}

Solution #4 (Allow against a list of choices)

This solution prevents command injection by checking the user-provided against a list of trusted command execution strings. The user has control over which string is used but cannot provide unexpected data to build the executed command.

List<string> allowedFiles = new List<string>() { "wwwroot/files/db.backup", "wwwroot/files/archive.backup" };

if (!allowedFiles.Contains(filePath)) {
  return BadRequest("Error, this file path is not allowed.");
}

This solution can quickly becomes unmanageable if you have many available choices or want to process dynamic input.

References

MSDN - Process class

Mitre - CWE-78: Improper Neutralization of Special Elements used in an OS Command

OWASP - Command Injection