This week, I tried out OSCTF, which had a particularly easy web category. I managed to actually solve all the challenges. Here are the writeups for my first cleared category:

INTROSPECTION

We are given a small input box to submit a flag:

Introspection

If I submit a random string like “test”, I just get a message saying “Incorrect flag. Try again.”

Looking at the HTML, I saw something interesting:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Web CTF Challenge</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <div class="container">
        <h1>Welcome to the Secret Agents portal</h1>
        <p>Find the hidden flag in the secrets of the Universe</p>
        <input type="text" id="flagInput" placeholder="Enter flag here">
        <button onclick="checkFlag()">Submit</button>
        <p id="result"></p>
    </div>
    <script src="script.js"></script>
</body>
</html>

As you can see, when we click the button to submit text, the function checkFlag() is called. Let’s check out script.js:

function checkFlag() {
    const flagInput = document.getElementById('flagInput').value;
    const result = document.getElementById('result');
    const flag = "OSCTF{Cr4zY_In5P3c71On}";

    if (flagInput === flag) {
        result.textContent = "Congratulations! You found the flag!";
        result.style.color = "green";
    } else {
        result.textContent = "Incorrect flag. Try again.";
        result.style.color = "red";
    }
}

The flag is literally right there: OSCTF{Cr4zY_In5P3c71On}.

STYLE QUERY LISTING

The letters of this challenge spell out SQL - at first, I thought this was something involving an SQL injection.

We open the web app to see a simple login portal:

Style Query Listing

We are also told that the “admin” is hiding secrets.

If this is a simple SQL injection, it would work something like this:

Let’s say in the SQL database that the application queries, there is an account (that was registered) with the credentials “user1” and “password123”. If a user submits the username “user1” and the password “password123”, the application would take those credentials and make the following SQL query:

SELECT * FROM users WHERE username = 'user1' AND password = 'password123'

This is querying the users table for a specific user with those credentials, and in that case, would return true. However, an attacker could login without even knowing the password (they could just submit something blank) by removing the password check from the WHERE clause of the query. They could do this by submitting the username “user1’–” which leads to the following query:

SELECT * FROM users WHERE username = 'user1'--' AND password = ''

This query returns the user whose username is administrator and successfully logs the attacker in as that user. How? The string '--' is sort of like a comment in SQL - whatever comes after it is ignored. Therefore, it just performs the username check for user1, which returns true, and the attacker bypasses the login check.

Since I am trying to get into the admin account, I made the following query:

Style Query Listing

This took me to a /profile endpoint with a button to view profile information. I clicked on it, and got a fake flag. However, I noticed something interesting when submitting the SQL injection to log in:

Style Query Listing

There are no session cookies (or cookies of any kind): when we successfully log in, the app just redirects us to /profile. In other words, there is no authorization or authentication required to go to the /profile endpoint, it is essentially exposed.

I got the idea to check for any other exposed endpoints, specifically /admin, and got the flag:

Style Query Listing

HEADS OR TAILS

This was a weird challenge. This is all we see when we open the website:

Heads or Tails

This site had NOTHING. No javascript, no network requests, no robots.txt. After a long, long time of looking, I found a hidden endpoint called /get-flag. However, when I went to the url, I saw this:

Heads or Tails

I checked the network requests, and it essentially reflected what was being rendered. In the response headers, I did see that HEAD requests were allowed, so I made a simple cURL request:

curl -I http://34.16.207.52:4789/get-flag
HTTP/1.1 200 OK
Server: Werkzeug/3.0.3 Python/3.8.19
Date: Mon, 15 Jul 2024 19:47:05 GMT
Content-Type: text/html; charset=utf-8
Flag: OSCTF{Und3Rr47Ed_H3aD_M3Th0D}
Content-Length: 0
Connection: close

Again, pretty simple. HEAD requests only lead to response headers being returned, while GET requests lead to a response body AND headers.

INDOOR WEBAPP

Upon opening the app, we see this:

IDOR

I clicked on view profile and was taken to this URL: http://34.16.207.52:2546/profile?user_id=1.

Considering the title of this challenge, and the fact there were not any session cookies again, I simply changed the user_id to 2 and got the flag:

IDOR

ACTION NOTES

We are given a page to register/login. I made an account, logged in, and I was taken to a basic note-taking endpoint. Essentially, we can type some text, and it is rendered as note on screen.

I thought this could be something related to XSS, but nothing was really working, and there was no place to send the notes.

I then tried directory fuzzing and found a /console hidden directory:

Action Notes

This suggested to me that it was a Flask app. I also happened to find a cookie: “eyJ1c2VybmFtZSI6ImQwMHZpdCJ9.ZpK0dw.9fGdJGcTEBqF7a3goO3U3Od6BkI”.

From here, I figured that the best move would be to try and decode this cookie. I found a tool for this called “flask-unsign”. I then decoded the cookie:

flask-unsign --decode --cookie 'eyJ1c2VybmFtZSI6ImQwMHZpdCJ9.ZpK0dw.9fGdJGcTEBqF7a3goO3U3Od6BkI'
{'username': 'd00vit'}

Looks like all we need to do is change the username from “d00vit” to “admin” and we can can some unauthorized access. To do this, we need to sign the spoofed cookie with the secret key that was used to sign the original cookie. Of course, this only exists server side. Fortunately, the tool has an option for bruteforcing the key:

flask-unsign --unsign --cookie 'eyJ1c2VybmFtZSI6ImQwMHZpdCJ9.ZpK0dw.9fGdJGcTEBqF7a3goO3U3Od6BkI'
[*] Session decodes to: {'username': 'd00vit'}
[*] No wordlist selected, falling back to default wordlist..
[*] Starting brute-forcer with 8 threads..
[*] Attempted (2176): -----BEGIN PRIVATE KEY-----ECR
[+] Found secret key after 21760 attempts

The secret key was, humorously, “supersecretkey”. I used this to sign our new, spoofed cookie:

flask-unsign --sign --cookie "{'username': 'admin'}" --secret 'supersecretkey'
eyJ1c2VybmFtZSI6ImFkbWluIn0.ZpK-OQ.b0pXO2bnMPo0mBBrW1pEzZYwnqM

I changed the cookie and then went to the /admin endpoint, and the flag was displayed:

Action Notes

While these were defintely more beginner-oriented, it was still cool to clear a category. These also strangely included one of the first SQL related challenge I’ve ever posted about.