java.spring.security.injection.tainted-system-command.tainted-system-command

profile photo of semgrepsemgrep
Author
unknown
Download Count*

Detected user input entering a method which executes a system command. This could result in a command injection vulnerability, which allows an attacker to inject an arbitrary system command onto the server. The attacker could download malware onto or steal data from the server. Instead, use ProcessBuilder, separating the command into individual arguments, like this: new ProcessBuilder("ls", "-al", targetDirectory). Further, make sure you hardcode or allowlist the actual command so that attackers can't run arbitrary commands.

Run Locally

Run in CI

Defintion

rules:
  - id: tainted-system-command
    languages:
      - java
    severity: ERROR
    mode: taint
    pattern-propagators:
      - pattern: (StringBuilder $STRB).append($INPUT)
        from: $INPUT
        to: $STRB
        label: CONCAT
        requires: INPUT
    pattern-sources:
      - patterns:
          - pattern-either:
              - pattern-inside: |
                  $METHODNAME(..., @$REQ(...) $TYPE $SOURCE,...) {
                    ...
                  }
              - pattern-inside: |
                  $METHODNAME(..., @$REQ $TYPE $SOURCE,...) {
                    ...
                  }
          - metavariable-regex:
              metavariable: $TYPE
              regex: ^(?!(Integer|Long|Float|Double|Char|Boolean|int|long|float|double|char|boolean))
          - metavariable-regex:
              metavariable: $REQ
              regex: (RequestBody|PathVariable|RequestParam|RequestHeader|CookieValue|ModelAttribute)
          - focus-metavariable: $SOURCE
        label: INPUT
      - patterns:
          - pattern-either:
              - pattern: $X + $SOURCE
              - pattern: $SOURCE + $Y
              - pattern: String.format("...", ..., $SOURCE, ...)
              - pattern: String.join("...", ..., $SOURCE, ...)
              - pattern: (String $STR).concat($SOURCE)
              - pattern: $SOURCE.concat(...)
              - pattern: $X += $SOURCE
              - pattern: $SOURCE += $X
        label: CONCAT
        requires: INPUT
    pattern-sinks:
      - patterns:
          - pattern-either:
              - pattern: |
                  (Process $P) = new Process(...);
              - pattern: |
                  (ProcessBuilder $PB).command(...);
              - patterns:
                  - pattern-either:
                      - pattern: |
                          (Runtime $R).$EXEC(...);
                      - pattern: |
                          Runtime.getRuntime(...).$EXEC(...);
                  - metavariable-regex:
                      metavariable: $EXEC
                      regex: (exec|loadLibrary|load)
              - patterns:
                  - pattern: |
                      (ProcessBuilder $PB).command(...).$ADD(...);
                  - metavariable-regex:
                      metavariable: $ADD
                      regex: (add|addAll)
              - patterns:
                  - pattern-either:
                      - patterns:
                          - pattern-inside: |
                              $BUILDER = new ProcessBuilder(...);
                              ...
                          - pattern: $BUILDER.start(...)
                      - pattern: |
                          new ProcessBuilder(...). ... .start(...);
        requires: CONCAT
    message: "Detected user input entering a method which executes a system command.
      This could result in a command injection vulnerability, which allows an
      attacker to inject an arbitrary system command onto the server. The
      attacker could download malware onto or steal data from the server.
      Instead, use ProcessBuilder, separating the command into individual
      arguments, like this: `new ProcessBuilder(\"ls\", \"-al\",
      targetDirectory)`. Further, make sure you hardcode or allowlist the actual
      command so that attackers can't run arbitrary commands."
    metadata:
      cwe:
        - "CWE-78: Improper Neutralization of Special Elements used in an OS
          Command ('OS Command Injection')"
      owasp:
        - A01:2017 - Injection
        - A03:2021 - Injection
      category: security
      technology:
        - java
        - spring
      confidence: HIGH
      references:
        - https://www.stackhawk.com/blog/command-injection-java/
        - https://cheatsheetseries.owasp.org/cheatsheets/OS_Command_Injection_Defense_Cheat_Sheet.html
        - https://github.com/github/codeql/blob/main/java/ql/src/Security/CWE/CWE-078/ExecUnescaped.java
      cwe2022-top25: true
      cwe2021-top25: true
      subcategory:
        - vuln
      likelihood: HIGH
      impact: HIGH
      license: Commons Clause License Condition v1.0[LGPL-2.1-only]
      vulnerability_class:
        - Command Injection

Examples

tainted-system-command.java

package org.sasanlabs.service.vulnerability.commandInjection;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import org.sasanlabs.internal.utility.LevelConstants;
import org.sasanlabs.internal.utility.Variant;
import org.sasanlabs.internal.utility.annotations.AttackVector;
import org.sasanlabs.internal.utility.annotations.VulnerableAppRequestMapping;
import org.sasanlabs.internal.utility.annotations.VulnerableAppRestController;
import org.sasanlabs.service.exception.ServiceApplicationException;
import org.sasanlabs.service.vulnerability.bean.GenericVulnerabilityResponseBean;
import org.sasanlabs.vulnerability.types.VulnerabilityType;
import org.springframework.http.HttpStatus;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestParam;

/**
 * This class contains vulnerabilities related to Command Injection. <a
 * href="https://owasp.org/www-community/attacks/Command_Injection">For More information</a>
 *
 * @author KSASAN preetkaran20@gmail.com
 */
@VulnerableAppRestController(
        descriptionLabel = "COMMAND_INJECTION_VULNERABILITY",
        value = "CommandInjection")
public class CommandInjection {

    private static final String IP_ADDRESS = "ipaddress";
    private static final Pattern SEMICOLON_SPACE_LOGICAL_AND_PATTERN = Pattern.compile("[;& ]");
    private static final Pattern IP_ADDRESS_PATTERN =
            Pattern.compile("\\b((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\\.|$)){4}\\b");

    StringBuilder getResponseFromPingCommand(String ipAddress, boolean isValid) throws IOException {
        boolean isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows");
        StringBuilder stringBuilder = new StringBuilder();
        if (isValid) {
            Process process;
            if (!isWindows) {
                process =
                        // deepruleid: tainted-system-command
                        new ProcessBuilder(new String[] {"sh", "-c", "ping -c 2 " + ipAddress})
                                .redirectErrorStream(true)
                                .start();
            } else {
                process =
                        // deepruleid: tainted-system-command
                        new ProcessBuilder(new String[] {"cmd", "/c", "ping -n 2 " + ipAddress})
                                .redirectErrorStream(true)
                                .start();
            }
            try (BufferedReader bufferedReader =
                    new BufferedReader(new InputStreamReader(process.getInputStream()))) {
                bufferedReader.lines().forEach(val -> stringBuilder.append(val).append("\n"));
            }
        }
        return stringBuilder;
    }

    @AttackVector(
            vulnerabilityExposed = VulnerabilityType.COMMAND_INJECTION,
            description = "COMMAND_INJECTION_URL_PARAM_DIRECTLY_EXECUTED")
    @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_1, htmlTemplate = "LEVEL_1/CI_Level1")
    public ResponseEntity<GenericVulnerabilityResponseBean<String>> getVulnerablePayloadLevel1(
            @RequestParam(IP_ADDRESS) String ipAddress) throws IOException {
        Supplier<Boolean> validator = () -> StringUtils.isNotBlank(ipAddress);
        boolean isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows");
        StringBuilder stringBuilder = new StringBuilder();
        if (isValid) {
            Process process;
            if (!isWindows) {
                // ruleid: tainted-system-command
                process =
                        new ProcessBuilder(new String[] {"sh", "-c", "ping -c 2 " + ipAddress})
                                .redirectErrorStream(true)
                                .start();
            } else {
                // ruleid: tainted-system-command
                process =
                        new ProcessBuilder(new String[] {"cmd", "/c", "ping -n 2 " + ipAddress})
                                .redirectErrorStream(true)
                                .start();
            }
            try (BufferedReader bufferedReader =
                    new BufferedReader(new InputStreamReader(process.getInputStream()))) {
                bufferedReader.lines().forEach(val -> stringBuilder.append(val).append("\n"));
            }
        }
        return new ResponseEntity<GenericVulnerabilityResponseBean<String>>(
                new GenericVulnerabilityResponseBean<String>(
                        stringBuilder.toString(),
                        true),
                HttpStatus.OK);
    }

    @AttackVector(
            vulnerabilityExposed = VulnerabilityType.COMMAND_INJECTION,
            description =
                    "COMMAND_INJECTION_URL_PARAM_DIRECTLY_EXECUTED_IF_SEMICOLON_SPACE_LOGICAL_AND_NOT_PRESENT")
    @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_2, htmlTemplate = "LEVEL_1/CI_Level1")
    public ResponseEntity<GenericVulnerabilityResponseBean<String>> getVulnerablePayloadLevel2(
            @RequestParam(IP_ADDRESS) String ipAddress, RequestEntity<String> requestEntity)
            throws ServiceApplicationException, IOException {

        Supplier<Boolean> validator =
                () ->
                        StringUtils.isNotBlank(ipAddress)
                                && !SEMICOLON_SPACE_LOGICAL_AND_PATTERN
                                        .matcher(requestEntity.getUrl().toString())
                                        .find();
        return new ResponseEntity<GenericVulnerabilityResponseBean<String>>(
                new GenericVulnerabilityResponseBean<String>(
                        // todoruleid: tainted-system-command
                        // Indirection, needs interproc taint
                        this.getResponseFromPingCommand(ipAddress, validator.get()).toString(),
                        true),
                HttpStatus.OK);
    }

    // Case Insensitive
    @AttackVector(
            vulnerabilityExposed = VulnerabilityType.COMMAND_INJECTION,
            description =
                    "COMMAND_INJECTION_URL_PARAM_DIRECTLY_EXECUTED_IF_SEMICOLON_SPACE_LOGICAL_AND_%26_%3B_NOT_PRESENT")
    @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_3, htmlTemplate = "LEVEL_1/CI_Level1")
    public ResponseEntity<GenericVulnerabilityResponseBean<String>> getVulnerablePayloadLevel3(
            @RequestParam(IP_ADDRESS) String ipAddress, RequestEntity<String> requestEntity)
            throws ServiceApplicationException, IOException {

        Supplier<Boolean> validator =
                () ->
                        StringUtils.isNotBlank(ipAddress)
                                && !SEMICOLON_SPACE_LOGICAL_AND_PATTERN
                                        .matcher(requestEntity.getUrl().toString())
                                        .find()
                                && !requestEntity.getUrl().toString().contains("%26")
                                && !requestEntity.getUrl().toString().contains("%3B");
        return new ResponseEntity<GenericVulnerabilityResponseBean<String>>(
                new GenericVulnerabilityResponseBean<String>(
                        this.getResponseFromPingCommand(ipAddress, validator.get()).toString(),
                        true),
                HttpStatus.OK);
    }

    // e.g Attack
    // http://localhost:9090/vulnerable/CommandInjectionVulnerability/LEVEL_3?ipaddress=192.168.0.1%20%7c%20cat%20/etc/passwd
    @AttackVector(
            vulnerabilityExposed = VulnerabilityType.COMMAND_INJECTION,
            description =
                    "COMMAND_INJECTION_URL_PARAM_DIRECTLY_EXECUTED_IF_SEMICOLON_SPACE_LOGICAL_AND_%26_%3B_CASE_INSENSITIVE_NOT_PRESENT")
    @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_4, htmlTemplate = "LEVEL_1/CI_Level1")
    public ResponseEntity<GenericVulnerabilityResponseBean<String>> getVulnerablePayloadLevel4(
            @RequestParam(IP_ADDRESS) String ipAddress, RequestEntity<String> requestEntity)
            throws ServiceApplicationException, IOException {

        Supplier<Boolean> validator =
                () ->
                        StringUtils.isNotBlank(ipAddress)
                                && !SEMICOLON_SPACE_LOGICAL_AND_PATTERN
                                        .matcher(requestEntity.getUrl().toString())
                                        .find()
                                && !requestEntity.getUrl().toString().toUpperCase().contains("%26")
                                && !requestEntity.getUrl().toString().toUpperCase().contains("%3B");
        return new ResponseEntity<GenericVulnerabilityResponseBean<String>>(
                new GenericVulnerabilityResponseBean<String>(
                        this.getResponseFromPingCommand(ipAddress, validator.get()).toString(),
                        true),
                HttpStatus.OK);
    }
    // Payload: 127.0.0.1%0Als
    @AttackVector(
            vulnerabilityExposed = VulnerabilityType.COMMAND_INJECTION,
            description =
                    "COMMAND_INJECTION_URL_PARAM_DIRECTLY_EXECUTED_IF_SEMICOLON_SPACE_LOGICAL_AND_%26_%3B_%7C_CASE_INSENSITIVE_NOT_PRESENT")
    @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_5, htmlTemplate = "LEVEL_1/CI_Level1")
    public ResponseEntity<GenericVulnerabilityResponseBean<String>> getVulnerablePayloadLevel5(
            @RequestParam(IP_ADDRESS) String ipAddress, RequestEntity<String> requestEntity)
            throws IOException {
        Supplier<Boolean> validator =
                () ->
                        StringUtils.isNotBlank(ipAddress)
                                && !SEMICOLON_SPACE_LOGICAL_AND_PATTERN
                                        .matcher(requestEntity.getUrl().toString())
                                        .find()
                                && !requestEntity.getUrl().toString().toUpperCase().contains("%26")
                                && !requestEntity.getUrl().toString().toUpperCase().contains("%3B")
                                        & !requestEntity
                                                .getUrl()
                                                .toString()
                                                .toUpperCase()
                                                .contains("%7C");
        return new ResponseEntity<GenericVulnerabilityResponseBean<String>>(
                new GenericVulnerabilityResponseBean<String>(
                        this.getResponseFromPingCommand(ipAddress, validator.get()).toString(),
                        true),
                HttpStatus.OK);
    }

    @VulnerableAppRequestMapping(
            value = LevelConstants.LEVEL_6,
            htmlTemplate = "LEVEL_1/CI_Level1",
            variant = Variant.SECURE)
    public ResponseEntity<GenericVulnerabilityResponseBean<String>> getVulnerablePayloadLevel6(
            @RequestParam(IP_ADDRESS) String ipAddress) throws IOException {
        Supplier<Boolean> validator =
                () ->
                        StringUtils.isNotBlank(ipAddress)
                                && (IP_ADDRESS_PATTERN.matcher(ipAddress).matches()
                                        || ipAddress.contentEquals("localhost"));

        return new ResponseEntity<GenericVulnerabilityResponseBean<String>>(
                new GenericVulnerabilityResponseBean<String>(
                        this.getResponseFromPingCommand(ipAddress, validator.get()).toString(),
                        true),
                HttpStatus.OK);
    }

    public static void test1(@RequestParam(IP_ADDRESS) String ipAddress) {
        String args = "ping -c 2 " + ipAddress + "test";
        Process process;
        process = new ProcessBuilder(new String[] {"sh", "-c", args});
        // ruleid: tainted-system-command
        process.start();
    }

    public static void test2(@RequestParam String input) {
        String latlonCoords = input;
        Runtime rt = Runtime.getRuntime();
        // ok: tainted-system-command
        Process exec = rt.exec(new String[] {
                "c:\\path\to\latlon2utm.exe",
                latlonCoords }); // safe bc args are seperated
    }

    public static void test3(@RequestParam String input) {
        StringBuilder stringBuilder = new StringBuilder(100);
        stringBuilder.append(input);
        stringBuilder.append("test2");
        Runtime rt = Runtime.getRuntime();
        // ruleid: tainted-system-command
        Process exec = rt.exec(stringBuilder);
    }

    public static void test4(@RequestParam String input) {
        String test1 = "test";
        String comb = test1.concat(input);
        Runtime rt = Runtime.getRuntime();
        // ruleid: tainted-system-command
        Process exec = rt.exec(comb);
    }

    public static void test5(@RequestParam String input) {
        String test1 = "test";
        String comb = String.format("%s%s", test1, input);
        Runtime rt = Runtime.getRuntime();
        // ruleid: tainted-system-command
        Process exec = rt.exec(comb);
    }

    public static String run(@RequestParam(defaultValue = "I love Linux!") String input) {
        ProcessBuilder processBuilder = new ProcessBuilder();
        String cmd = "/usr/games/cowsay '" + input + "'";
        System.out.println(cmd);
        // ruleid: tainted-system-command
        processBuilder.command("bash", "-c", cmd);
    
        StringBuilder output = new StringBuilder();
    
        try {
          Process process = processBuilder.start();
          BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
    
          String line;
          while ((line = reader.readLine()) != null) {
            output.append(line + "\n");
          }
        } catch (Exception e) {
          e.printStackTrace();
        }
        return output.toString();
      }
}