DEV Community

Antony Garand
Antony Garand

Posted on

PHP: Return true to win - WriteUp (Part 1)

Introduction

Returntrue.win is a website containing 16 PHP challenges where we must return true using the least amount of characters in the given context.

The challenges demonstrate many interesting quirks from PHP and are of greatly varying difficulty, which is why solving and understanding those can help us gain a better understanding of the language.

This post will cover the first 8 solutions, while an upcoming part 2 will cover the remaining ones.

Now is the time to stop reading and try to solve the challenges yourself before reading on the solutions!

Website: https://returntrue.win

Level 1

Challenge

function foo($x)
{
    return $x;
}
Enter fullscreen mode Exit fullscreen mode

Solution

foo(!0);
Enter fullscreen mode Exit fullscreen mode

Explanation

The obvious solution would be to enter true, but that would be 4 bytes and the shortest answer uses only half of those! We can save two bytes by using !0 instead.
The reason !0 works is as the logical not (!) operator will type cast the value to a boolean first, and return the opposite of this result.

As 0 is converted to false, we get !false, which is true.

See: Boolean type casting for a list of values converted to false.

Level 2

Challenge

function foo($x)
{
    return $x('gehr') === 'true';
}
Enter fullscreen mode Exit fullscreen mode

Solution

foo(str_rot13)
Enter fullscreen mode Exit fullscreen mode

Explanation

Our first guess could be to create an anonymous function which always returns 'true', such as the following:

foo(function(){return'true';})
Enter fullscreen mode Exit fullscreen mode

But this isn't the shortest solution.

The key to the shortest 9 bytes score is the 'gehr' argument given to the function, which is exactly true with each letter shifted by 13 characters.

As it turns out, php has the str_rot13 function which performs this exact operation!

PHP doesn't like crashing, and it does so by trying to be smart with its conversions.
When we're using a constant which is not defined, the following behavior is used:

If you use an undefined constant, PHP assumes that you mean the name of the constant itself, just as if you called it as a string (CONSTANT vs "CONSTANT").
From: PHP Constant syntax

Combine this with Variable Functions

This means that if a variable name has parentheses appended to it, PHP will look for a function with the same name as whatever the variable evaluates to, and will attempt to execute it.

And we've got a working solution!

Level 3

Challenge

function foo($x)
{
    return ($x >= 1) && ($x <= 1) && ($x !== 1);
}
Enter fullscreen mode Exit fullscreen mode

Solution

foo(!0)
// Or 
foo(1.)
Enter fullscreen mode Exit fullscreen mode

Explanation

The first requirement here is get a variable which is <= and >= than 1.
As those are numeric comparisons, PHP will convert both sides of the equation to numbers.

The second requirement is for our variable to not be the integer 1.

Booleans converted to numbers will manage to pass this condition:

FALSE will yield 0 (zero), and TRUE will yield 1 (one).

From the doc: Integer: Casting from boolean

This gives us our first answer, passing true will succeed in all the checks.

The second solution is based on PHP's handling of its different numeric types.

As you may know, PHP has both floats and integers.

As they are different types, 1.0 and 1 returns true to the Not identical operator.

Our solution is therefore the float value of 1, which can be written 1.

References:
Comparison operators

Integer: Casting from boolean

Level 4

Challenge

function foo($x)
{
    $a = 0;
    switch ($a) {
        case $x:
            return true;
        case 0:
            return false;
    }
    return false;
}
Enter fullscreen mode Exit fullscreen mode

Solution

foo(0);
Enter fullscreen mode Exit fullscreen mode

Explanation

As $a is 0 an we need the program flow to enter our case $x, the solution is to set $x to 0.

Not much of a challenge here!

Level 5

Challenge

function foo($x)
{
    return strlen($x) === 4;
}
Enter fullscreen mode Exit fullscreen mode

Solution

foo(💩);
Enter fullscreen mode Exit fullscreen mode

Explanation

The tricky part here is getting the shortest solution, which is only 1 character long yet strlen will return 4.
The solution resides in the difference between strlen and mb_strlen, which handles multibyte strings differently:

strlen() returns the number of bytes rather than the number of characters in a string.

[mb_strlen()] A multi-byte character is counted as 1.

As utf8 supports up to 4 bytes per character, our poop emoji returns 4 on the strlen function while being only one character.

References:
strlen documentation

mb_strlen documentation

Level 6

Challenge

function foo($x)
{
    return $x === $x();
}
Enter fullscreen mode Exit fullscreen mode

Solution

// Not working anymore, 22 characters
foo(($x=session_id)($x).$x);
// Shortest? working solution: 33 characters
foo($x=function()use(&$x){return $x;})
// Alternative working solution, 42 characters
foo($GLOBALS[x]=function(){return$GLOBALS[x];})
// Runner up, 44 characters
foo(new class{function __invoke(){return$this;}})
Enter fullscreen mode Exit fullscreen mode

Explanation

This one is among the most interesting challenges of the lot, and I've spent a lot of time finding the shortest solution.

Note that at the time of this writing, the shortest solution will NOT work due to additional sandboxing done on the website, but it does work locally if you have sessions enabled.

There are four different solutions which I'd like to explore here.

Shortest
($x=session_id)($x).$x
// Which is equivalent to
$x = 'session_id';
$x($x);
foo($x);
Enter fullscreen mode Exit fullscreen mode

Firstly, the essence of this solution is the session_id function.

This function is used as an getter/setter:

  • When called without an argument, it will return the stored value.
  • When called with an argument, it will assign its inner value and return an empty string.

The first part of this solution is setting the $x variable to 'session_id' (As string, because of PHP's handling of undefined constants).

The second part is calling session_id, which works once again thanks to PHP's Variable functions, with $x as argument. As $x is a string, this will set the content of session_id to 'session_id'.

The result of our session_id($x) call is an empty string, which isn't what we want to send to foo.

In order to send session_id to foo, we concatenate $x to the empty string we previously had, which results in the 'session_id' string.

Finally, the foo function will compare our entry variable $x, aka. 'session_id', to the result of $x(), aka. session_id().
As we did set our session_id to 'session_id', both strings are the same and we passed the comparison!

Self returning function using use
$x=function()use(&$x){return $x;}
Enter fullscreen mode Exit fullscreen mode

This solution is pretty straightforward: Create a function which uses itself, and returns itself.

Reference: Anonymous functions, specifically example #3

Globals
$GLOBALS[x]=function(){return$GLOBALS[x];}
// Which is equivalent to

$GLOBALS[x] = function() {
    return $GLOBALS[x];
};
foo($GLOBALS[x]);
Enter fullscreen mode Exit fullscreen mode

This solution is similar to the previous one, but uses the $GLOBALS array instead of inheriting variables from the parent scope.

Callable class returning itself
new class {
    function __invoke()
    {
        return $this;
    }
}
Enter fullscreen mode Exit fullscreen mode

Since PHP7, we can create Anonymous Classes.

This lets us create a new class with the __invoke magic method implemented.

This magic method lets us return $this when we call our object as a function to solve the challenge!

Level 7

Challenge

function foo(stdClass $x)
{
    $x = (array) $x;
    return $x[0];
}
Enter fullscreen mode Exit fullscreen mode

Solution

foo((object)[!0]);
Enter fullscreen mode Exit fullscreen mode

Explanation

This solution resides in how PHP type casts to object.

As we are typecasting an array with [0 => true] to an object, we are creating a generic stdClass with the 0 property having the true value.

Once this is type casted back to an array with the $x = (array) $x; line, the property 0 of our object gets back to the new array's index 0.

Level 8

Challenge

class Bar {}

function foo(Bar $x)
{
    return get_class($x) != 'Bar';
}
Enter fullscreen mode Exit fullscreen mode

Solution

foo(new class extends Bar{});
Enter fullscreen mode Exit fullscreen mode

Explanation

This solution depends on Object Inheritance in PHP.

We can create a new child class which extends Bar as an argument. As the given object is a new class, get-class will return the name of the new anonymous class and not 'Bar'.

Conclusion

Those were lots of original solutions to the challenge!

Most of those solutions did produce warnings and notices, therefore most of the odd behavior should be detected in a proper development environment.

I would like to thank:

If you do know similar oddities or challenge websites, please do send them my way so I can solve and write about them!

References

https://returntrue.win
https://gist.github.com/odan/0799dfa59d40acdb18e8a1fa9611a996
https://www.rpkamp.com/2018/02/15/all-answers-to-returntrue.win-with-explanations/
https://alf.nu/ReturnTrue - Similar website for JavaScript

Top comments (6)

Collapse
 
joshcheek profile image
Josh Cheek

You can't really figure out when user code is malicious, and escaping won't work here b/c the input itself must be evaluated. IDK how it's implemented, my guess is that it's running on a sandboxed server (eg has memory/processor/duration thresholds set, which will kill the program if you exceed them, has abusable features like http and system commands disabled). The comment about session_id would support this hypothesis. There are other options, though. I've done things like this by shipping them off to eval.in, which does its own sandboxing. You could also compile php to web assembly and run it in the user's browser (guessing this would take quite a bit of work, but it should be possible).

Collapse
 
antogarand profile image
Antony Garand

This all depends on how you validate your user input!

This may not be a simple eval, but perhaps a docker container launched specifically for this test and destroyed afterwards.

I know that's how pwnfixrepe.at does evaluate untrusted code safely, and therefore there are most likely similar mechanisms in place here.

Collapse
 
joshcheek profile image
Josh Cheek • Edited

Really enjoyed this 😊

Collapse
 
antogarand profile image
Antony Garand

Thanks!

Collapse
 
robencom profile image
robencom

Hehe I like a PHP challenge when I see one, but when one of the answers is a POOP emoji, then you'll know that this challenge has passed the limits of logic :)

Collapse
 
moopet profile image
Ben Sinclair

I'm glad you explained this because I had no clue what I was supposed to do from the website itself!