Usurping Mastodon instances - mastodon.so/cial (CVE-2023-42451)

This blog post gives details about the GHSA-v3xf-c9qf-j667 vulnerability (for which GitHub issued CVE-2023-42451) and how it could be exploited. It is the second out of the 2 vulnerabilities that I reported to the Mastodon security team in August 2023 (more context can be found in the first blog post).

tl;dr: under certain conditions, an attacker could have usurped your Mastodon account.

All Mastodon versions were affected by this vulnerability. If you are a Mastodon instance admin and didn’t apply an update since September 19, please upgrade (patched versions are: 3.5.14, 4.0.10, 4.1.8, 4.2.0-rc2).

Summary

From the advisory:

Old domain name normalization code in Mastodon incorrectly stripped / from domain names, removing any occurrence from the string, not just occurrences at the end of the string. This allows attackers to impersonate domains, provided they are able to register a domain name that happens to be a textual prefix of the impersonated domain.

The bug is deadly simple: in some parts of the code, slashes are removed from the domain tied to an account (the presumed original intent is to strip trailing slashes, such as in domain.example/). This behavior could have been harmless if it wasn’t used to validate HTTP signatures. With no surprises, the patch is trivial: domain.delete('/') was replaced with domain.delete_suffix('/').

An attacker can misuse this behavior to spoof Mastodon instances. It requires the registration of a malicious domain which matches an instance domain prefix. As an example, the owner of the mastodon.so DNS (which was created in November 2022 according to whois) can spoof the mastodon.social instance.

This scenario isn’t that unrealistic since:

Impact

Let’s consider these 3 fictional actors:

Role Details Comment
Usurped account donald@mastodon.social Any account on mastodon.social
Attacker mastodon.so DNS owned by the attacker, starting with mastodon.* to match mastodon.social
Targeted instance https://chat.community.io Whatever vulnerable instance

Bypassing the HTTP signature mechanism leads to attacks such as:

Analysis

HTTP Signatures

Mastodon refers to the software, but also to the self-hosted, globally interconnected microblogging community which is composed of Mastodon instances. These instances communicate through HTTP requests which usually embed an HTTP signature to prove their authenticity. From the documentation:

HTTP Signatures is a specification for signing HTTP messages by using a Signature: header with your HTTP request. Mastodon requires the use of HTTP Signatures in order to validate that any activity received was authored by the actor generating it.

Public keys can be retrieved easily to verify HTTP signatures. For instance:

$ wget -q --header 'Accept: application/json' -O- https://mastodon.social/users/mastodon | jq .publicKey
{
  "id": "https://mastodon.social/users/Mastodon#main-key",
  "owner": "https://mastodon.social/users/Mastodon",
  "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtpNfuGPl/WTnSq3dTurF\nMRelAIdvGVkO/VKYZJvIleYA27/YTnpmlY2g+0az4xEhOBtVNA1cTpS63CdXRyNz\ncH/GZtzxkdxN91vZSw0JVy+wG34dzwcq1KWFDz9D/5Tqf16KUJH+TDTlxdOBds91\nIZg+TTkiT+xfnSiC5SLMnn1dTzCW9P0yNJxpn37z7p6pEs63X1wstEEX1qGDUQTO\n1JICpKDjuQZMlioAAA5KG25tg2f+zKlv5M/NI33DblquyJ7TYvIpDN8hsFCRjuvA\nmjtKz/1XIRvQkeKND3UkqX8s6qTGyNOjcT86qt9BqYHYGuppjpRG/QNGoKYalio1\nwwIDAQAB\n-----END PUBLIC KEY-----\n"
}

Replacing the Private Key

Using the redirection mechanisms in the Webfinger code, an attacker can bypass the signature verification with a malicious DNS and eventually create or update an account on a Mastodon instance with arbitrary information.

Let’s consider the previous fictional actors. A simple account search triggers Webfinger requests. Searching for the donald@mastodon.so account on https://chat.community.io leads to the following GET requests issued by chat.community.io and malicious responses issued by mastodon.so:

GET /.well-known/webfinger?resource=acct:donald@mastodon.so
200 {
      "subject": "acct:donald@mastodon.so/cial",
      "links": [ { "href": "https://mastodon.so/cial/users/donald" } ]
    }

GET /cial/.well-known/webfinger?resource=acct:donald@mastodon.so/cial
200 {
      "subject": "acct:donald@mastodon.so/cial",
      "links": [ { "href": "https://mastodon.so/cial/users/donald" } ]
    }

GET /cial/users/donald
200 {
      "id": "https://mastodon.so/cial/users/donald",
      "preferredUsername": "donald",
      "publicKey": {
        "id": "https://mastodon.so/cial/users/donald#main-key",
		"publicKeyPem": "-- ATTACKERKEY --"
      }
    }

Webfinger results in @username: donald and @domain: mastodon.so/cial, and an arbitrary public key generated by the attacker.

The Account is eventually created or updated by ActivityPub::ProcessAccountService on chat.community.io. TagManager.normalize_domain will eventually delete / from the domain, which results in a class Account instance with @domain being mastodon.social.

The Account donal@mastodon.social is created if it doesn’t exist, otherwise it’s updated using the information provided by the attacker (public key, avatar, image, URLs, etc.).

Exploitation

Bypassing the HTTP signature verification allows an attacker to spoof requests from an actor. An exploit has been developed to demonstrate the vulnerability and shared with Mastodon’ security team.

Test environment

To test the exploit, I installed Mastodon on a server whose name is mastodon.local. This DNS has no importance and could be anything (eg. chat.community.io), but it’s the one displayed in screenshots below.

In order to usurp mastodon.social, I modified /etc/hosts and created a self-signed certificate since I don’t own the mastodon.so DNS:

$ openssl req -x509 -newkey rsa:4096 -nodes -out fullchain.pem -keyout privkey.pem -days 365
$ sudo cp fullchain.pem /usr/local/share/ca-certificates/mastodon.so.crt
$ sudo update-ca-certificates
Role Details
Usurped account admin@mastodon.social
Attacker mastodon.so
Targeted instance https://mastodon.local

Toot

The following command line sends a (spoofed) private message from mastodon@mastodon.social. The spoofed profile looks the same as the legit one (avatar, image, URLs, etc.) but it could have been made different:

$ ./mastospoof.py --spoof mastodon@mastodon.so/cial --target mastodon.local --toot 'HACK THE PLANET! [...]'

Spoofed toot from mastodon@mastodon.social

Super careful users can notice that the actor is spoofed because, when displayed, the original profile link is dubious (here, one has to click on menu and show the open original page link).

Dubious original profile link (mastodon.so/cial)

However, after having sent spoofed toots or private messages, attackers can restore the legit actor Account by triggering an action which requires an HTTP signature verification using legit URIs. Which makes spoofed toots and private messages indistinguishable from legit ones.

Follow and Private Message

The following command lines make mastodon@mastodon.social follow admin@mastodon.local, then send a (spoofed) private message:

$ ./mastospoof.py --spoof mastodon@mastodon.so/cial --target admin@mastodon.local --follow
$ ./mastospoof.py --spoof mastodon@mastodon.so/cial --target admin@mastodon.local --pm 'URGENT! [...]'

Spoofed private message from mastodon@mastodon.social

On the Internet

In order to prove that the exploit works outside of my local network, I bought the DNS █████████.co for 10$ and sent a spoofed private message from admin@██████████.com to a test account on https://mastodon.social.

Role Details
Usurped account █████████@█████████.com
Attacker █████████.co
Targeted instance https://mastodon.social

Usurping admin@█████████.com

Afterthoughts

It seems difficult to tell whether the vulnerability was exploited in the wild and if accounts on an instance were spoofed.

I developed a quick script to list potential malicious domains, ordered by account number, which could be used by an attacker to target vulnerable instances. IIRC, I downloaded Mastodon instances lists from https://instances.social and it’s a bit outdated. Output excerpt:

$ ./malicious-domains.py
sleeping.town (214748364): ['sleeping.TO']
mastodon.social (1524743): ['mastodon.SO']
pawoo.net (909218): ['pawoo.NE']
mastodon.firefly.land (900701): ['mastodon.FIRE', 'mastodon.FI', 'mastodon.firefly.LA']
daystorm.netz.org (479023): [daystorm.'NE', 'daystorm.NET']
...

Given a domain name, this Python script lists the TLDs that can used for malicious domains:

$ ./malicious-tld.py mastodon.sdf.org
mastodon.sdf.org: ['SD']
$ ./malicious-tld.py infosec.exchange
infosec.exchange: no TLD exist

If, for a given instance, no TLD exists to create a malicious domain or no malicious domain was ever registered, we are 100% sure that the vulnerability wasn’t exploited against this instance. Otherwise, I don’t know if there’s a way to tell easily if and when a domain was registered, though. Some databases and services exist, but they aren’t comprehensive and there is no data for some TLD.

Conclusion

Once again, if you are a Mastodon instance admin and didn’t apply an update since September 19, please upgrade (patched versions are: 3.5.14, 4.0.10, 4.1.8, 4.2.0-rc2).

Since v4.2.0, Mastodon instances automatically check for available updates and notify admins. It should help them to apply critical security updates in a timely fashion.