Credit. I stole the banner image from the background image of UIUCTF. I like the picture and their unique themes for these two CTFs…

This is another time @blackb6a unites and plays UIUCTF together. It was my fourth time playing UIUCTF, and I still found the challenges fun. Although the crypto challenges are relatively easy, I had a lot of fun solving phpfuck with @02E774.

Challenge Summary

// Flag is inside ./flag.php :)
($x=str_replace("`","",strval($_REQUEST["x"])))&&strlen(count_chars($x,3))<=5?print(eval("return $x;")):show_source(__FILE__)&&phpinfo();

We are given a PHP webpage (running on PHP 7.4.3) that evaluates a payload by us. However, the payload should consists of at most five distinct characters.

What is the deliverable? I am now able to write PHP codes solely with the character set (^.9).


Part I: Background story

TWY and Ozetta looked into the challenge earlier than I do. While I was solving the crypto challenges, they had various ideas on the set of characters to be used (for example '().^). I hopped into the challenge after the hint is released:

HINT: Look, he has a monocle (^.9)

Ozetta said that we can form a character set with five letters from (^.9) and an additional character. We gotta decide what the character is.

No Way… If you are counting carefully, you should be able to notice that (^.9) is already five-letter long. I did not notice that until I crafted a payload.

Although I was misled, some of the insights we had paved the way towards the flag. Anyway, while I was researching on the challenge, I stumbled across a Stack Overflow question that might be asked during the contest period. This is exactly what I need!

Cloudsourcing the solution online.

Of course, you can see that I was baited because the link was already purple. Unfortunately the question does not lead me anywhere closer to the solution, since there are no answer - and it is not accepting any answers as well.

The question is closed. Credits: TWY.

Part II: Building blocks for arbitrary command

All of the trick comes from Ozetta. He is an absolute legend. If you are already a lengndary like Ozetta, you can just skip to the third part.

[Strings from numbers] This is a way to build strings from numbers without quotes:

var_dump((9).(9)); // string(2) "99"

[Exclusive OR] Unlike other languages, PHP defined the ^ operator between two strings. If the lengths of the inputs are different, the length of the output is taken from the shorter string.

var_dump("y[" ^ "12"); // string(2) "Hi"
var_dump("abcd" ^ "123456"); // string(4) "PPPP"

[From nine and beyond] We are able to build more numbers with 9, ^ and . - that may not consist solely of nines.

// float(INF)

var_dump(.999999999999999); //float(1)
var_dump(9.999999999999999); // float(10)

var_dump(9^9); // int(0)
var_dump(9.9999999999999999^9); // int(3)
var_dump(9.9999999999999999^9^99.999999999999999^99); // int(4)

[chr Overflow] When the parameter of chr is not lying in $[0, 256)$, it will be taken modulo 256. For instance, $5678 \equiv 46\ (\text{mod}\ 256)$ and they returns the same character.

var_dump(chr(5678)); // string(1) "."
var_dump(chr(46)); // string(1) "."

[Case-insensitive function names] Functions are not case-sensitive.

var_dump(1); // int(1)
VaR_dUmP(1); // int(1)

[Calling functions from strings] One last thing that amazes me is that the strings can be called.

"var_dump"(1); // int(1)

Part III: Building chr for arbitrary strings

This may not be necessary. We can yet easily generate arbitrary characters if we are able to construct c, h and r (in any cases), as well as the digits. This can be achieved by constructing the string chr and supply the corresponding ASCII value as the parameter. Note that the parameter need not be a number - it can be a string:

var_dump("chr"(97)); // string(1) "a"
var_dump("chr"("97")); // string(1) "a"

We build "CHR" by xorring a bunch of strings those we are able to craft. This is definitely suboptimal, but we had finite time during the CTF:

var_dump(("INF" ^ "333" ^ "999" ^ "910" ^ "040" ^ "000" ^ "99.") == "CHR");
// bool(true)

This is how we built INF, 333, ..., 99.:

// string(4) "INF9"

// string(3) "333"

// string(3) "999"

// string(3) "910"

// string(3) "040"

// string(3) "000"

// string(4) "99.9"

Wait - I did not build the exact strings. I made INF9 instead of INF, and 99.9 instead of 99.. However, it is fine because the exclusive OR property. Combining the things above, we are able to build CHR from (^.9) for our further action!

// string(3) "CHR"

Part IV: Building a number basis

In part II, I mentioned how to build some numbers. We are going to construct seven numbers which respectively lies on $[64, 128)$, $[32, 64)$, $[16, 32)$, $[8, 16)$, $[4, 8)$, $[2, 4)$ and $[1, 2)$ under modulo 256. With the seven numbers, we are able to build an arbitrary number in $[0, 128)$ (which covers the printable characters) under modulo 256. Here are the seven numbers:

9999999                        // mod 256 = 127
999999                         // mod 256 =  63
((.99999999999999999).(9))^9^9 //         =  19
9999                           // mod 256 =  15
99.999999999999999^99          //         =   7
9.9999999999999999^9           //         =   3
.99999999999999999^9^9         //         =   1

We are able to build an arbitrary number within $[0, 128)$. For example, 89 can be constructed as 127 ^ 32 ^ 7 ^ 1. 6 = 7 ^ 1 and 40 = 32 ^ 15 ^ 7, etc.

Part V: Crafting the final payload

Now we have prepared the whole thing. I wrote a little Python script to craft the payload by itself and got the flag: uiuctf{pl3as3_n0_m0rE_pHpee_9f4e3058}. 🎉

I am curious how short could the payload be - I think there are a lot of places that the payload can be shortened and optimized. We could find that out!