macOS Persistence via iTerm

Written by noncetonic


macOS’s default is garbage, and everyone knows it; they also know iTerm is by and large the most popular terminal replacement for macOS. But did you know iTerm is also a fantastic way to persist on a macOS host?

I’ve been sitting on this for far too long and quite honestly I had completely forgotten I had never shared this fun little technique. So here it is.


There are two directories which iTerm2 checks for AutoLaunch scripts during execution. These AutoLaunch scripts are intended to allow users to automate a number of start-up tasks via iTerm2’s AppleScript Scripting Definition. While this is the intention, iTerm2 will blindly execute whatever code is in the Autolaunch.scpt file regardless of whether or not the code bothers to interact with iTerm2.

Autolaunch.scpt locations:

  • ~/Library/Application Support/iTerm2/Scripts/AutoLaunch.scpt
    • This location is checked first and may not exist on your system
    • Go ahead and create the ~/Library/Application Support/iTerm2/Scripts/ directory if it doesn’t already exist.
    • If this directory doesn’t exist the next path is checked as a legacy fallback.
  • ~/Library/Application Support/iTerm/Scripts/AutoLaunch.scpt
    • !! DEPRECATION NOTICE !! This path is only being checked due to legacy reasons and should be considered deprecated. If you rely on this path to always be an option it may eventually come back to bite you.

Note: This only kicks off during application launch, spawning an extra tab in your terminal won’t do much and neither will spawning a new window.


This one is pretty easy. Honestly it’s almost too easy which makes me wonder why I haven’t heard of other people using this technique.

BASHing buttons

Need to show this off to your security team and don’t want to invest a whole lot of time on a PoC? This one is for you.

# Useless payload but it gets the point across
say "iTerm, uTerm, we all Term for code exec"

Go ahead and toss that into ~/Library/Application Support/iTerm2/Scripts/AutoLaunch.scpt ( please make sure you name is AutoLaunch.scpt :3 ), unmute your speakers, and restart iTerm2.

What’s great is that this will run regardless of whether or not it has executable permissions set. Perfect.

Wait whut?

At first I was pretty excited having instantly assumed the file extension made no difference to iTerm. That would’ve been nice.

I honestly haven’t had enough spare fscks to dig into this in order to be certain but I tested with a simple python script to no avail.

When I sat back I realized the reason the bash script worked was due to the fact that say is part of the AppleScript language and all the other lines are comments…


Snakes on a plane

Not all is lost if you don’t want to write AppleScript though. Luckily AppleScript allows us to run shell commands which opens up a whole host of built-in languages for us.

 * bsrl_iterm-01.scpt
 * Downloads a random image from XKCD
 * and opens it with whichever app is
 * registered as its opener 
 * ( by default)
set theCommand to quoted form of "import urllib;urllib.urlretrieve(\"\", \"/tmp/xkcd.png\")"
do shell script "/usr/local/bin/python " & "-c " & theCommand
do shell script "/usr/bin/open " & "/tmp/xkcd.png"

Kinda cool? Not really…

But wait; there’s more!

What if I told you that in addition to learning something fun about persistence, today you were also blessed with the gift of command execution within a real life shell :O. Just think of all the fun you could have with the ability to wait for a user to run sudo or ssh to a server and then inject your dirty bits right into their console.

This script will work as an AutoLaunch.scpt but it can also be run at any time to inject keystrokes into any window, any session, and any tab.

tell application "iTerm"
	tell current session of current window
		set thePath to quoted form of "/tmp/bsrl_iterm-02.txt"
		write text "echo " & "'yay RCE'" & " >> " & thePath
		write text "say we did it"
	end tell
end tell

Note On macOS 10.14, Mojave has locked down the otherwise wide open Apple Events system and now requires that applications be given express permission to send Apple Events to other applications. Because of this security enhancement it is currently not possible to inject commands into the context of iTerm without prompting the user for permission –unless they have previously allowed it.

The above technique will allow you a small window for injecting commands into iTerm. Ambitious readers can surely devise schemes which take advantage of this window for more nefarious deeds. If you wind up writing something and would like to share or discuss it, feel free to reach out to me via twitter @noncetonic


Welp that’s all for now folks. Have fun with the new toys in your arsenal.

Further Reading

For those of you who are interested in learning more about some of the topics I glossed over in this post I’ve included some links.

  • iTerm2’s Scripting Documentation :
  • Everyday AppleScriptObjC 3rd Edition :
  • AppleScript Language Guide :

Scriptless Identification of Browsers

Written by noncetonic


This is a fairly short post illustrating a technique for identifying a browser based on request headers. This technique is not reliant on any scripting support and sucessfully identifies browsers regardless of User-Agent spoofing or other such obfuscation techniques.

Provided alongside this post is a PoC tool that implements the checks mentioned in this post.


Request headers sent by browsers are fairly standard but due to implementation details in individual browser codebases provide metadata unique to the browser which can be used to positively identify the browser.

As request headers can be read server-side without the need for any client-side scripting capabilities this technique is useful for identifying a browser which has extentions/plugins installed aimed at disrupting fingerprinting.

Where them headers at?

Here are some examples of different request headers sent by Chrome, Firefox, and Safari.


GET / HTTP/1.1
Host: localhost:8081
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9


GET / HTTP/1.1
Host: localhost:8081
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:62.0) Gecko/20100101 Firefox/62.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1


GET / HTTP/1.1
Host: localhost:10101
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0.3 Safari/605.1.15',
Accept-Language: en-us
Accept-Encoding: gzip, deflate

GG noncetonic, so what?

Astute readers may notice the following:

  • After the GET request and Host header all browsers use the same handful of headers but with unique order.
  • Some headers contain details in one browser that are not in other browsers.
  • Some browsers use different case in headers which other browsers do not.

When these points are combined, the following fingerprints are derived.

Identifying Chrome

  • Unique order of request headers
  • Accept-Encoding lists br as an encoding which allows support for brotli encoding
  • Accept adds two mime-types not typically seen explictly stated with other browsers (despite other browsers having support for these mime-types) : image/webp and image/apng.
  • Accept-Language has a modified q value (currently unsure of the significance of the q variable) of 0.9 contrasting the 0.5 of Firefox and the complete omission of the q variable in Safari

Identifying Safari

  • Unique order of request headers
  • Accept-Language lists only en-us and omits the ,en;q=0.X format of Chrome and Firefox. Additionally, both Chrome and Firefox use the spelling en-US but Safari uses en-us instead.

Identifying Firefox

  • Unique order of request headers

Damn, Daniel!

Taking the above rules and creating fingerprints based on them is extremely easy. Here are arrays for each of these three browsers containing the order of request headers unique to that browser.

chrome = ["host","connection","upgrade-insecure-requests","user-agent","accept","accept-encoding","accept-language"]
firefox= ["host","user-agent","accept","accept-language","accept-encoding","connection","upgrade-insecure-requests"]
safari= ["host","connection","upgrade-insecure-requests","accept","user-agent","accept-language","accept-encoding"]

One thing to note is the inclusion of certain headers such as DNT and Pragma which are not seen unless settings are enabled in the browser or a browser is explictly forcing a non-cached request to a page. For these reasons certain headers have been ignored.

My favorite rapper is 2-PoC

(Ok, so there aren’t really 2 PoCs and I don’t really like 2-Pac but I needed a pun for this second title.)

I’ve shared a PoC node.js server that outputs the detected browser using both request header order as well as unique header details. You can check it out at .


As a simple PoC there are definitely browsers missing and more research that could be done; that is an exercise left to the reader. Something worth noting is that as many browsers base their code on the open-source Chromium project (Opera, Vidalia, etc.) this technique will inaccurately assume these browsers to be Chrome. This is a limitation of this technique and other identification methods must be leveraged in order to accurately determine whether the browser is in fact Chrome or a branched code base.