HoneyLabs

Blog · · HoneyLabs

Meet Akin, our open-source HTTP fingerprint

One visitor has sent the same two requests from 21,076 addresses in 32 weeks, and no fingerprint we had could say so. Akin is the open HTTP request fingerprint we built so the token itself says how many headers two clients differ by.

ShareLinkedIn
Meet Akin, our open-source HTTP fingerprint

We run honeypots: machines that exist to be scanned, so that we can watch who scans and how. Everything below is what those sensors received without asking for it, 934,175 HTTP requests from 17,695 addresses in the week of 23 to 29 September 2026, plus 32 weeks of stored requests for the longer look. This post is about one visitor in that pile, and the tool we built because nothing we had could name it.

The visitor

It turns up most days. An address from DigitalOcean's network sends GET / to one of our ports, sends GET /favicon.ico to the same port a second or so later, and almost never comes back. Then another address does the same to another port. Last week 1,242 addresses did this, 1,167 of them with those two requests and nothing else, across 131 ports from 22 and 80 through 1337 to 11111, and no port got more than thirteen visits. None of the addresses were on any of the public scanner lists we check.

Here are the two requests, exactly as one address sent them, with our sensor's name taken out:

GET / HTTP/1.1
Host: [sensor]
Connection: keep-alive
sec-ch-ua: "Chromium";v="153", "Not:A-Brand";v="24", "Brave";v="153"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Linux"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8
Sec-GPC: 1
Accept-Language: en-US,en;q=0.5
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
GET /favicon.ico HTTP/1.1
Host: [sensor]
Connection: keep-alive
sec-ch-ua: "Chromium";v="153", "Not:A-Brand";v="24", "Brave";v="153"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Linux"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8
Sec-GPC: 1
Accept-Language: en-US,en;q=0.5
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Referer: http://[sensor]/

A real browser, or a good imitation of one: sec-ch-ua says Brave, and Sec-GPC: 1 is Brave's default, so the "Chrome" in the user agent is a small lie already. The second request is the first plus one line, Referer, which any browser adds when it fetches a favicon after a page. About a third of the addresses send a Firefox version of the same pair.

A name that survives the address

Everything we do with this traffic starts with a verdict on an address: is this a research scanner, a crawler, an exploit campaign, or somebody probing one target on purpose. Customers ask us about addresses they see in their own logs, and the feeds we publish are lists of addresses with a reason attached. That only works if we know who is behind the address, and this visitor throws each one away after two requests. So we need a name for the tool that stays the same across the addresses. Concretely, we want to say, on the first request from an address we have never seen, "this is the favicon sweeper, seen 21,076 times", we want to tell a customer who finds those two requests in a log what they are, and we want to tell apart the operators who run the same library with one option changed. That name has to come from the request alone, because the request is all a log keeps.

The address is not the thing

Blocking or tagging the addresses does nothing; each one is used for two requests and dropped, and over 32 weeks 21,076 of them have done this. The User-Agent is worse. It said Chrome on 817 of last week's addresses and Firefox on 425, it has taken five different values over the 32 weeks, and elsewhere in the same week one plain scripting library arrived carrying 863 different user agents from 388 addresses. The string a client uses to name itself costs nothing to change.

What costs something to change is the rest of the request: which headers the HTTP library sends, how the values in Accept and Accept-Encoding are written, whether the names are capitalised. Those come from the library, not from a config file, and they travel with the tool to every address it runs from. That is why request fingerprints exist.

Hashing it

The standard construction is to take the header names, in order, run them through a digest, and keep the digest. We did that to the two requests above:

GET /            42d7418375b7
GET /favicon.ico a8408eb060fd

Two strings with nothing in common, and that is the whole problem. A hash answers "is this request shaped exactly like that one" and nothing else. You cannot read it back, a client that adds one header gets a value unrelated to the old one, and reordering the headers gives a third. The operator's four request shapes become four unrelated fingerprints, and "is this the same tool with one option changed" is exactly the question the token cannot answer.

Reading the header set back

So we changed the construction. Instead of hashing the header names, we keep a fixed list of 32 of them and record which are present as 32 bits. The list is the 32 names that best separate clients at our sensors, chosen by presence entropy over 107,559 addresses between June and September, and it is frozen. A header that is not on it is not thrown away: its name is hashed to four hex digits and the codes are appended, sorted, up to nine. Around that sits a short readable prefix and a hash of the signal that is too varied to print, the grammar of the negotiation values and the casing of the names.

We called it Akin, and this is what it says about the page request:

$ akin -decode b11cun150_0004f3ff_28f866c0
http      1.1
eol       CRLF
duplicate false
body      none
headers   15
core      connection accept-encoding accept accept-language user-agent upgrade-insecure-requests sec-fetch-mode sec-fetch-site sec-fetch-dest sec-fetch-user sec-ch-ua-platform sec-ch-ua sec-ch-ua-mobile sec-gpc host
detail    28f866c0

The prefix b11cun150 reads left to right: format letter, HTTP/1.1, CRLF line endings, no repeated header name, no body, 15 headers, 0 outside the list. The middle, 0004f3ff, is the 32 bits. The tail, 28f866c0, is the hash of grammar and casing.

One bit

Now the favicon request:

$ akin -decode b11cun160_0004f7ff_3f747703 | grep -E 'headers|core'
headers   16
core      connection accept-encoding accept accept-language user-agent upgrade-insecure-requests sec-fetch-mode sec-fetch-site sec-fetch-dest sec-fetch-user referer sec-ch-ua-platform sec-ch-ua sec-ch-ua-mobile sec-gpc host
$ akin -distance "b11cun150_0004f3ff_28f866c0 b11cun160_0004f7ff_3f747703"
1

0004f3ff against 0004f7ff. XOR them and you get 00000400, which is one bit, bit 10, referer. The distance is one, and the token says which header, and it does that with no table and no copy of the original requests. Do the same for the Firefox pair, 000c03ff and 000c07ff, and it is the same bit. Between the Chrome pair and the Firefox pair the distance is five: the three sec-ch-ua headers and Sec-GPC on the Brave side, Priority on the Firefox side.

What separates the sweeper's four Akin tokens

The four hashes we started with became four tokens that are visibly one client with two browser identities, each fetching a page and then its favicon.

Pulling the thread

Once four tokens are one thing, you can ask how long the thing has been around. We had 32 weeks of stored request heads, so we computed the token for all of them and looked for those four from that network.

Thirty-two weeks of the same four Akin tokens from one network

They are in every single week: 367 addresses in the first, 999 in the last, 45,017 requests in total. The port list was widened from 59 to 67 in late April, to 95 in August and to 131 in the week of 21 September, so somebody is maintaining this. The user agent took five different values over those months and the request shape did not change once, which is the entire case for fingerprinting the shape instead of the string. On the TLS ports the addresses also present one TLS fingerprint (JA4 t13d301300_1d37bd780c83_5ae85263eb3b) that no other network sent us that week.

The favicon is the tell. Hashing a site's favicon is how web-application catalogues identify what is running behind a port, and this looks like one being built by someone who prefers not to be listed. If those four tokens are in your logs, that is what is looking at you.

The rest of the traffic

We then ran it over everything else from the week, 686 tokens in all, to see whether the distance meant anything beyond one operator.

Each of the five most common HTTP header sets is one header from the next

The five most common header sets at the sensors form a chain in which each set is one header from the next: 1,716 addresses sent only Accept and User-Agent in HTTP/1.0 with no Host, most of them the Palo Alto Networks scanner; add Host and you have default curl, 2,711; add Accept-Encoding and you have Censys, 3,625; drop Accept and you have Go's net/http with nothing configured, 2,689; add Connection and you have the last set, 2,825. Under each set in the figure is the digest of its header list, which is what hashing sees: five unrelated strings. Of the 211 tokens seen from three or more addresses, 182 have a neighbour within one header. Scanner traffic is a handful of HTTP libraries with one option flipped, and a new token will usually land one header from something you have already seen.

A map of the 26 most common HTTP header sets

Drawn as a network, with a line between sets one header apart, 19 of the 26 sets seen from 150 or more addresses connect into one component with the chain near the middle. The seven outside it are our sweeper's two pairs, each joined to itself by the Referer step and to nothing else, one more pair, and one WinRM client. No browser-shaped set sits in the main component: browsers differ from plain clients by several headers at once, so the distance separates the two populations without being told what any header means.

Two things we left out on purpose

Header order never reaches the token: the bits and codes have no order and the tail sorts by header name before hashing, so an exploit tool we see that shuffles its header order on every request gets one token instead of many. The User-Agent value is left out, and we tested keeping it: a variant that also hashed its shape produced, against one botnet of 6,910 addresses all sending the same exploit, 327 fingerprints where the correct answer is one. Without it the count is one. The request path is out for the same reason. Both are what a disguise changes.

Try it on your logs

The Go library, a command-line tool, a Python reference implementation and the specification are at github.com/honeylabshq/akin, Apache-2.0. Every token in this post was recomputed from a raw sample of its request with the reference implementation and all 686 matched.

$ go install github.com/honeylabshq/akin/cmd/akin@latest
$ akin -hex < payloads.hex          # one hex-encoded request head per line
$ akin -decode <token>
$ akin -distance "<token> <token>"

The input is the raw request head up to the blank line. Most access logs keep neither header order nor capitalisation, so from a log you get the 32 bits and the codes but not the tail, which is still enough to find the sweeper: its four core maps are 0004f3ff, 0004f7ff, 000c03ff and 000c07ff. If you find it, or a fingerprint of your own that Akin does something useful with, we would like to hear about it.

Found this useful?

Pass it to whoever should see it next.

LinkedIn

New research, by email

Get new HoneyLabs research by email as it publishes: write-ups like this one, plus the threat reports on which ports moved, which KEV and recent CVEs are being probed in the wild, and the attack paths worth grepping your own logs for. Here is a recent threat report.

Double opt-in: we send one confirmation email and nothing else until you click it. Unsubscribe in one click, any time.