# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/agent/protect/rule/base_service' require 'contrast/agent/request_context' require 'contrast/utils/object_share' require 'contrast/agent/protect/rule/cmdi/cmdi_base_rule' require 'contrast/agent/protect/rule/cmd_injection' module Contrast module Agent module Protect module Rule # The Ruby implementation of the Protect Command Injection Command # Backdoors sub-rule. class CmdiBackdoors < Contrast::Agent::Protect::Rule::CmdiBaseRule NAME = 'cmd-injection-command-backdoors' MATCHES = %w[/bin/bash-c /bin/sh-c sh-c bash-c].cs__freeze def rule_name NAME end # CMDI Backdoors infilter: # This rule does not have input classification. # If a value matches the CMDI applicable input types and it's length is > 2 # we can check if it's used as command backdoors. # # @param context [Contrast::Agent::RequestContext] current request contest # @param classname [String] Name of the class # @param method [String] name of the method triggering the rule # @param command [String] potential dangerous command executed. # @raise [Contrast::SecurityException] if the rule mode ise set # to BLOCK and valid cdmi is detected. def infilter context, classname, method, command return unless (ia_results = gather_ia_results(context)) return unless backdoors_match?(command) return unless (likely_attacker = match_applicable_input_type(ia_results, command)) return if protect_excluded_by_code? return unless (result = build_attack_with_match(context, likely_attacker, nil, command, **{ classname: classname, method: method })) append_to_activity(context, result) cef_logging(result, :successful_attack) return unless blocked? raise_error(classname, method) end # @param context [Contrast::Agent::RequestContext] def infilter? context return false unless enabled? # This rule does not have input tracing stage so we need to check the results of # the main rule instead. return false unless context&.agent_input_analysis&.results&.any? do |result| result.rule_id == Contrast::Agent::Protect::Rule::CmdInjection::NAME end return false if protect_excluded_by_code? true end protected # Used to customize the raised error message. # # @param classname [String] Name of the class # @param method [String] name of the method triggering the rule # @raise [Contrast::SecurityException] def raise_error classname, method raise(Contrast::SecurityException.new(self, 'Command Injection Command Backdoor rule triggered. ' \ "Call to #{ classname }.#{ method } blocked.")) end private # Check to see if value is backdoor match # # @param potential_attack_string [String] def backdoors_match? potential_attack_string return false unless potential_attack_string && potential_attack_string.length > 2 return false unless matches_shell_parameter?(potential_attack_string) true end # Checks to see if input is used as parameter for a shell cmd. # # @param cmd [String] # @return [Boolean] def matches_shell_parameter? cmd normalize_cmd = cmd.delete(Contrast::Utils::ObjectShare::SPACE) MATCHES.each do |match| return true if normalize_cmd.include?(match) end false end # Check to see if the user input is one of the applicable types. # With Agent_ia if we are here and CMDI is being analyzed then # the applicable input type is already checked before input # classification. Only thing left is the match check. # # @param ia_results [Contrast::Agent::Reporting::Settings::InputAnalysisResult] gathered results # @param cmd [String] potential attack vector # @return [Contrast::Agent::Reporting::Settings::InputAnalysisResult, nil] matched input_type def match_applicable_input_type ia_results, cmd ia_results.each do |ia_result| return ia_result if ia_result.value == cmd end nil end # Backdoors does not have input classification so we check for match within the # result for the main rule - CMDI. # # @param context [Contrast::Agent::RequestContext] def gather_ia_results context return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless context&.agent_input_analysis&.results context.agent_input_analysis.results.select do |ia_result| ia_result.rule_id == Contrast::Agent::Protect::Rule::CmdInjection::NAME && ia_result.score_level != Contrast::Agent::Reporting::ScoreLevel::IGNORE end end end end end end end