A dead server in the SPF record that passes DMARC for a subdomain
The apex SPF record still authorized an abandoned dev server. A customer-facing subdomain included that record and had no DKIM, so mail from the server would pass SPF, align, and pass DMARC p=reject as the subdomain.
This started as a small one. The domain’s SPF record ends in -all, so every IP in it is a statement that this host sends our mail and nothing else does. One of those IPs belonged to a development server nobody had touched in years.
The abandoned SPF host
v=spf1
ip4:[addr-1]
ip4:[addr-2]
...
ip4:[abandoned-host]
...
include:[esp-provider]
include:[crm-provider]
-allThe host ran Ubuntu 16.04, out of support since April 2021, behind a TLS certificate that expired in August 2021. There was nothing on it beyond Apache’s /icons/ directory. It had no mail server either: 25, 465, 587 and 2525 were closed, and so was everything else across the nmap top 1,000 plus more than 2,000 targeted ports, apart from SSH, HTTP and HTTPS (nginx 1.13.5 in front of Apache 2.4.10) and two filtered ports that answered SYN-ACK and nothing more.
On its own that is a small thing: an IP with no reason to be trusted, and no way I could find to make it send mail. The vendor removed the IP.
The subdomain include
Then I looked at what else trusts the apex record. A customer-facing subdomain, with live MX records through a transactional mail provider, had this:
sub.example.com TXT v=spf1 include:example.com include:[esp-provider] -all
example.com TXT v=spf1 ... ip4:[abandoned-host] ... -all
_dmarc.example.com TXT v=DMARC1; p=reject; rua=mailto:[redacted]
_dmarc.sub none, so the parent policy applies
sub DKIM none on any common selectorinclude: pulls in the whole apex record, so the abandoned server was also allowed to send as the subdomain. pyspf agrees:
$ python3 -c "import spf; print(spf.check(i='[abandoned-host]', s='test@sub.example.com', h='dev-host.example'))"
('pass', 250, 'sender SPF authorized') DMARC alignment
DMARC passes when either leg passes and aligns:
pass = (SPF pass AND SPF aligned) OR (DKIM pass AND DKIM aligned)Mail from that server with a From: address on the subdomain passes SPF for the subdomain, and the From domain matches the SPF domain, so it’s aligned. DMARC passes on the SPF leg alone and p=reject never comes into play. The missing DKIM isn’t what causes this, and adding DKIM wouldn’t fix it, because one passing leg is enough.
The only thing between this and spoofed transactional mail landing in customers’ inboxes was the server staying uncompromised.
Limitations
I didn’t get into the host, and the report said so. Autoindex RCE (CVE-2017-20005) needed a module that wasn’t loaded, there was no FastCGI for CVE-2014-3583, path traversal was blocked, and there was no MTA to relay through. Request smuggling between nginx and Apache did work, and went in as its own report. So this finding needs a second bug before anyone can use it.
Remediation
The root cause, the IP in the apex SPF record, was gone before the subdomain half was looked at, which closes both.
Two things worth doing on any domain like this. Add sp=reject to the parent DMARC record so the subdomain policy is written down instead of inherited by accident. And take an IP out of SPF the day its server is retired, because every include: that points at your record inherits whatever you forgot.
Disclosure
This went through a coordinated disclosure program. The domain isn’t named here, and hosts, IPs and providers are replaced with placeholders.