Open aszx87410 opened 3 years ago
One could also use the type variable of the "log" command to transfer the payload
case "log": {
log("[log]: ", e.data.message.text, type=e.data.message.type);
break;
}
<iframe onload="setTypeVar();logType()" src="https://challenge-0721.intigriti.io/console.php"></iframe>
<script>
function setTypeVar(){
frames[0].postMessage({
cmd: 'log',
message: {
type: `<iframe srcdoc='<script src=/analytics/..%2f/htmledit.php?code=a%0aalert(document.domain)/*></`+`script>'</iframe>`,
text: "a",
},
credentials: {
username: 'root',
password: 'toor'
}
},"*")
}
function logType(v){
frames[0].postMessage({
cmd:"logv",
message: 'type',
credentials: {
username: 'root',
password: 'toor'
}
},"*")
}
</script>
@h43z Thanks for pointing it out, brilliant!
Challenge link: https://challenge-0721.intigriti.io/
Working POC: https://randomstuffhuli.s3.amazonaws.com/xss_poc_both.html
Analysis
This project is a bit complex because there are two frames and a lot of
postMessage
andonmessage
, make it harder to know how it works at first glance.To find a XSS vulnerability, there must be a place to inject malicious payload, like
innerHTML
oreval
, so I started from finding this place.There are three pages:
Let's check it one by one.
index.html
Besides the weird variable in the comment,
DEV
andstore
, nothing special.htmledit.php
htmledit.php
reflects the query stringcode
but there is a strict CSP:script-src 'nonce-...';frame-src https:;object-src 'none';base-uri 'none';
, it's impossible to run JS. But it's worth noting that embed an iframe is allow. Maybe it's a hint for the player?console.php
It's the most interesting one.
First, I found a
eval
command here for changing variable's value:Is it where I can inject my payload? Probably not, because it allows limited alphanumeric and symbol(only
-
and+
).Another interesting part is here:
If
safe
is true, thedata
won't be sanitized and we can inject arbitrary HTML. I believe here is the key, so my goal is to execute log function with arbitrary data and letsafe
be true.Before that, we need to know that JavaScript has no named parameters, don't be confused!
For example, when we call
log("[logv]: ", window[m.message], safe=false, type='info');
, the argument is actually by order, soprefix
is"[logv]: "
,data
iswindow[m.message]
,type
isfalse
andsafe
is'info'
Anyway, I decided to start from find a way to run
log
function, and it's obviously that I canpostMessage
to it's window to run the command.But I need to bypass some checks first.
Bypass "top" check
First, I need to embed this page in an iframe:
Second, there are two more checks I need to bypass:
top.DEV
should be truthy, and the credentials I send in should matchtop.store.users.admin.username
andtop.store.users.admin.password
.It's easy, just write my own HTML page and set these variables, embed
console.php
in an iframe, and then post message to it, right?Nope, it's not gonna work because of Same-Origin Policy. When
console.php
tries to accesstop.DEV
, it's blocked by browser because top window is in another domain.So we need a same origin page where we can embed an iframe and also set global variables.
htmledit.php
is the one.DOM clobbering
There is a technique called DOM clobbering, it utilizes a feature which turns a DOM element with id to global variable.
For example, when you have
<div id="a"></div>
in your HTML, you can access it in JS viawindow.a
or justa
.If you can read Mandarin, you can check my blog post ζ·Ίθ« DOM Clobbering ηεηεζη¨ and another great article by Zeddy: δ½Ώη¨ Dom Clobbering ζ©ε± XSS. If you can't, check this: DOM Clobbering strikes back
It's a little bit troublesome to achieve multi-level DOM clobbering, you need to use iframe + srcdoc, here is my payload:
So
top.DEV
isa
element,store
is the iframe,store.users
is HTML collections of<a>
,store.users.admin
is thea
, andstore.users.admin.username
is the URL username inhref
, which isa
, it's the same for password.I built a simple page to open a new window, so that
htmledit.php
is the top window and I can still post message to it:By far, I can send message to
console.php
. But, it's only the beginning.Pass arbitrary data and safe=true
In order to let
safe
be true, I need to find a function call with 4 parameters:log("[logv]: ", window[m.message], safe=false, type='info')
is what I need, the fourth parameter isinfo
which is truthy.data
iswindow[m.message]
, so I need to set my payload to a global variable.I stuck here for a long time because I can't find one.
window.name
is usually a good candidate but this page set it's window name so I can't use it.location
is another candidate butlog
checks ifdata
is string, if not, it turns it into a string viaJSON.stringify
, which encoded<>
.I checked the code again and again, try to find out the missing puzzle. Finally, I found one.
Can you find a bug in the code above?
for (x of access) {
, it's a common bug for newbie, when you forgot to declarex
, it will be a global variable. In this case,x
istop.store.users.admin
, which is the<a>
element.Build payload
If we cast an
<a>
element to string, the return value isa.href
. It's a common technique in DOM clobbering. So we can pass our payload insidehref
.But, remember that
log
checks the type of data? The type ofx
is DOM element, hence failed the check. I need to find a way to make it a string.Fortunately, there is another command I can utilize:
I can do this:
Because of JS "coercion",
x+1
returns a string, so nowZ
is a string contains ourhref
. Now, I can send whatever data I want.But wait, it's encoded because it's a URL,
<
will be%3C
.What should I do?
In
log
function, there is one linedata = parse(data)
, and here is the parse function:If
e
is string, it returnss(e)
where s islet s = (s) => s.normalize('NFC');
When I reviewed the source code of reassign command, I noticed this regexp:
RegExp = /^[s-zA-Z-+0-9]+$/;
, and I also noticed these four functions:s
,u
andt
is allowed to use. So, we can utilizereassign
command again, to lets=u
, so our data can be unescaped!Full source code is like this:
So the data is
ftp://a:a@a#<img src=x onerror=alert(1)>
, and the data is assigned totext_tag.innerHTML
, XSS triggered!Oh...not that easy, I forgot CSP.
Bypass CSP
Indeed, I can inject anything to HTML for now, but there is one more thing I need to do: bypass CSP.
The CSP is:
There is no
unsafe-inline
so inline event won't work.https://challenge-0721.intigriti.io/analytics/
is suspicious, what is this?This JS
https://challenge-0721.intigriti.io/analytics/main.js
is included but almost nothing inside.Actually, when I saw this CSP rule, I know what to do instantly. Because I know there is a way to bypass CSP path using
%2f
(url encoded/
).Take this URL:
https://challenge-0721.intigriti.io/analytics/..%2fhtmledit.php
as an example, to browser, it's underanalytics
path so pass CSP, but for server it'sanalytics/../htmledit.php
, so we actually load resource from different path!But what should I include?
htmledit.php
is HTML, not JS...really?If you look carefully,
htmledit.php
prints escaped input in HTML comment, like this:In some cases, HTML comment is also a valid JS comment, as per ECMAScript:
In other words, we can make this HTML a valid JS script!
Here is the url I used:
https://challenge-0721.intigriti.io/htmledit.php?code=1;%0atop.alert(document.domain);/*
, it respond following HTML:After
/*
it's all comment, so the whole script istop.alert(document.domain);
basically. So now, I can include this url as JS script to run arbitrary code and bypass CSP.Please note that the content type of
htmledit.php
is stilltext/html
, but it's fine since it's same origin. If you want to include a page with content typetext/html
as JS , you will get a CORB error.It seems great, now we can inject an script to pop alert, right?
Unfortunately, not yet.
Final step
I thought I solve the challenge after I found this clear way to inject script, but somehow it doesn't work.
According to this stack overflow thread, the
<script>
tag won't load if you inserted with innerHTML.I don't know how to do so I googled
innerhtml import script
,innerhtml script run
and so on, but found nothing useful.After a while, It occurred to me that how about our old friend
<iframe srcdoc>
? What if I put the script tag inside srcdoc?So, I tried this way and it works like a charm.
Put it all together
Just one small thing to say, before I submit the answer I found that my exploit doesn't work on Firefox.
For
window.users
, Chrome returns HTMLCollection while Firefox returns first<a>
only, sousers.admin
is undefined on Firefox.It's not a big deal, just use another iframe:
Following is my exploit in the end:
Working POC: https://randomstuffhuli.s3.amazonaws.com/xss_poc_both.html
It's a great and awesome challenge, to me it's like a game with 5 levels, I need to solve every levels and put it together to really win this game.
I spent about 2 days on this challenge and every time I stuck, I checked the source code again, reviewed one line after another until I found something new. Surprisingly, there is always something new!
Thanks @RootEval for creating such amazing challenge, and also thanks Intigriti for hosting this event.