I recently had some great fun using CSP in a way that I've been really excited to talk about. We are starting to utilise the full power of CSP reports to find a way to hunt down malware infected endpoints on a corporate network!

Building on previous work

I have spoken in the past about using CSP to combat ad-injectors where nasty browser extensions were doing naughty things and inserting adverts into pages you visited. CSP of course has the ability to stop these because they're loading content into the DOM that isn't whitelisted in the CSP so the browser blocks it. Blocking the nasty things is great and of course you can enable reporting with https://report-uri.com to be notified when these things happen to get real-time feedback. In most cases though all you find out is that one of your visitors has a malicious browser extension installed and there's not really much you can do about that. Some random person on the Internet has come by your site and you have no way to notify them of their infection so the reports are largely just noise and Report URI actually has filters to remove them if you wish. Things can be a little different though.

Monitoring internal websites

That scenario changes somewhat when you aren't moitoring CSP reports on a public website that anyone can visit but an internal website, like a support ticket system or an intranet site, and the only people that can visit it are employees on your internal network. I recently came across this scenario with a customer of Report URI who got in touch with me directly and provided a few sample reports that they just couldn't figure out, let's call them ACME Corp. I will show one of the reports here but most values will be altered/redacted to shield the identity of those involved.

    "csp-report": {
        "document-uri": "https://acme-corp-intranet.com/some-page",
        "effective-directive": "script-src",
        "original-policy": "*policy here*; report-uri https://acme-corp.report-uri.com/r/d/csp/enforce",
        "blocked-uri": "https://nasty-malware-site.com/guid.js",
        "disposition": "enforce"

The report payload is quite heavily modified as I say, but, the point still remains clear. There is a page on the ACME Corp intranet site that is trying to load a JavaScript file from a nasty, external domain and the CSP has blocked that action. The browser sent a POST request containing the JSON payload out to Report URI and it showed up in the dashboard on their account. The first reaction of the admin who spotted this report was to go and check that internal page to see if this JS file was indeed being loaded but they couldn't find anything. They had it checked and double checked but couldn't find anything wrong with the page on the intranet site, yet the report was pretty clear about what had happened. That's when they reached out to me and provided me with a copy of the raw report to analyse. For me there are only two ways that this could happen. Either someone had crafted a forged report to send to their reporting address or a browser had indeed come across this problem and correctly sent a report after taking a block action. Given that there seems to be little benefit to the first possibility we started working on addressing the second possibility, that the report was genuine. After talking about the blog I mentioned above, ACME Corp understood that this could have been caused by one of their own internal endpoints being infected with some kind of malware, and so the hunt began. Whilst this was happening the reports were still coming in, not many of them, but fairly reliably during their office hours a small handful of them would appear. The problem now is that ACME Corp know one of their internal hosts is infected with malware, the CSP reports are a reliable indicator of that, but the problem is how to identify which one. The CSP report payload here can't help us identify an internal host like that and the only other information I receive at Report URI is the source IP of the report and the browser UA string. I don't store the source IP, and it'd only be ACME Corp's public IP anyway, and I only store the name of the browser extracted from the UA string, but even the full UA string probably wouldn't help anyway. We needed a way to tie CSP reports to a unique, internal host.

A simple idea

I bounced some ideas around about how we could try to tackle this and came up with an idea that might just do it. It would only work because of the fairly unique scenario we found ourselves in and it wasn't exactly clean, but it seemed like the only way. The JSON payload was all that we had to work with from the client so we had to get something into the payload to uniquely identify the client that we could also correlate on their side. The document-uri field contains the address the browser was visiting and it also includes any query string present in the URL. If the site could place a random token into the query string it would be present in the CSP report document-uri field and in the server logs for the site with the associated interal IP of the endpoint. The only easy option that ACME Corp had to do this was after the user had logged in. When they authenticated and were redirected to the homepage of the intranet site it was pretty easy for them to just add a random GET parameter to the URL and this would be reflected into the CSP report when it was fired. All they would then have to do is wait for the infected endpoint to trigger a report and grep the server logs for the unique ID to find the offending endpoint. Sadly, before we got chance to deploy our magnificent solution another member of ACME Corp had wondered how these endpoints had been infected and based on some of the pages that were firing reports they took a guess as to what department they may have been in. He checked around and some devices had local admin rights for a legacy app and, unsurprisingly, he'd found the offending endpoits. Of course that was great news but I'd really wanted to see how our CSP detection strategy would have worked out so I did a PoC myself and I'm pretty happy to say that it did indeed work.

The CSP report:

    "csp-report": {
        "document-uri": "https://home.scotthelme.co.uk/csp-test/?v=abc123",

The server log: - - [14/Dec/2017:17:57:23 +0000] "GET /csp-test/?v=abc123 HTTP/2.0" 200

Et voila! I think this is a really awesome way to take a tool like CSP and use it creatively to solve new problems. Yes it was a little clunky and requires favourable circumstances but I genuinely enjoyed the challenge of trying to take the tools at our disposal and craft them into something useful. Who knows, perhaps this could help someone else one day but I thought it'd be an interesting story to share nonetheless.