关于Phar的深入探索与发现

相信大多数师傅应该都是和我之前一样, 对pahr的了解主要就是局限于它的压缩包功能和反序列化的使用, 但其实除了这两个功能外phar还提供了很多的其他函数, 甚至可以直接将一个phar文件作为一个简单的php服务运行.这几天因为一点别的原因我就对phar的使用做了更多的探索, 相信看了这篇文章之后可以解决不少小伙伴对phar的一些疑问

0x00 前言

经过几天的php源码调试和phar功能学习, 在这里写一个关于phar进一步探索的小总结吧

0x01 文件结构

我们先看一下官方对phar文件结构的说明:

image-20221008022712956

Stub存根

Stub部分我们之前一般都是使用以下代码设置:

  1. $phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub

这里的php格式代码在第一次学习phar文件的时候就激发了我的好奇心, 这段代码什么时候会被执行, __HALT_COMPILER又有什么用, 这里的代码能不能被我们利用,如果可以被利用的话什么时候可以使用, 这些问题一直以来都困扰着我, 直到最近才解决

__HALT_COMPILER

我们先来看一下__HALT_COMPILER这个函数的作用

image-20221008024527648

这个函数是用于物理截断文件, 什么意思呢, 就相当于我们执行代码的时候后面的内容直接全部忽略, 一旦读取到__HALT_COMPILER就不再获取后面的内容

来个简单示例:

image-20221008024846072

了解了它的作用之后我们再回到phar文件中, 我们需要知道为什么要用它,

在pahr的stub存根中, __HALT_COMPILER();是必须要有的, 至于其他的内容其实是可有可无的, 在它后面的数据会直接被丢弃, 前面的内容则是直接原封不动的获取然后放到文件头中并且在后面加上?>

image-20221008030224707

为什么要使用__HALT_COMPILER

之前我对stub存根数据格式定义为php代码格式一直都是很不理解的, 直到我这几天看到了它作为web服务的.php以及.phps用法的时候我才明白

实际上phar格式除了定义为压缩包的格式之外还可以变为.php格式或者.phps格式的文件, 并且可以直接作为一个服务目录直接部署, 而__HALT_COMPILER()就是为了避免.php或者.phps格式的文件执行stub存根的代码的时候受到文件内容数据的影响,详细的继续往后看吧

文件描述和条目数据

从学习中可以知道这段数据主要由两个部分组成: 别名+Metadata反序列化元数据

  1. <?php
  2. class TestObject {
  3. }
  4. @unlink("phar.phar");
  5. $phar = new Phar("phar.phar"); //后缀名必须为phar
  6. $phar->setAlias('h0cksr');
  7. $phar->startBuffering();
  8. $phar->setStub("<? __HALT_COMPILER();?>"); //设置stub
  9. $o = new TestObject();
  10. $phar->setMetadata($o); //将自定义的meta-data存入manifest
  11. $phar->addFromString("test.txt", "test"); //添加要压缩的文件
  12. $phar->addFromString("flag.txt", "flag{test}"); //添加要压缩的文件
  13. //签名自动计算
  14. $phar->stopBuffering();
  15. system("hexdump phar.phar")
  16. ?>

生成的phar.phar文件16进制输出如下:

  1. 00000000: 3C 3F 20 5F 5F 48 41 4C 54 5F 43 4F 4D 50 49 4C <? __HALT_COMPIL
  2. 00000010: 45 52 28 29 3B 20 3F 3E 0D 0A 76 00 00 00 02 00 ER(); ?>..v.....
  3. 00000020: 00 00 11 00 00 00 01 00 06 00 00 00 68 30 63 6B ............h0ck
  4. 00000030: 73 72 16 00 00 00 4F 3A 31 30 3A 22 54 65 73 74 sr....O:10:"Test
  5. 00000040: 4F 62 6A 65 63 74 22 3A 30 3A 7B 7D 08 00 00 00 Object":0:{}....
  6. 00000050: 74 65 73 74 2E 74 78 74 04 00 00 00 CD 7C 40 63 test.txt.....|@c
  7. 00000060: 04 00 00 00 0C 7E 7F D8 B6 01 00 00 00 00 00 00 .....~..........
  8. 00000070: 08 00 00 00 66 6C 61 67 2E 74 78 74 0A 00 00 00 ....flag.txt....
  9. 00000080: CD 7C 40 63 0A 00 00 00 F9 67 6B 85 B6 01 00 00 .|@c.....gk.....
  10. 00000090: 00 00 00 00 74 65 73 74 66 6C 61 67 7B 74 65 73 ....testflag{tes
  11. 000000A0: 74 7D 78 A3 03 F7 CB 91 1F 44 FC 0E 7E 76 80 70 t}x......D..~v.p
  12. 000000B0: 3C DC 7A 6F 6D 14 02 00 00 00 47 42 4D 42 <.zom.....GBMB

可以看到在stub存根<? __HALT_COMPILER(); ?>之后跟着的数据中有我们设置的别名h0cksr,还有反序列化Matedata元数据O:10:"TestObject":0:{}

至于其它的不可见数据, 还没从源码仔细深挖过, 但是我想应该是别名以及Metadata原数据的一些16进制标识符以及一些相关信息吧

然后找手册翻了一会之后至于找到那些不可见字符格式的16进制数据所代表的含义了:

image-20221008034004681

文件数据

文件数据部分我们的文件名和文件内容的数据可以说是原封不动的可以看到了, 简单观察可以看到它是先有全部的文件名在前面(我想这应该是以phar的某种格式, 通过特定的格式定义还原拿到目录结构)然后后面接的就是文件内容了,而且这些文件的内容是无缝衔接直接连在一起的, 所以我们可以合理推断前面的文件名那部分数据不仅定义了文件结构, 而且文件大小也是在里面的

之后回去看一下可以发现确实如此, 在原始的文件名数据部分中, 它的两个文件数据部分结构均为文件名+文件长度+7字节的16进制数据000000CD7C4063(这些都是使用pack压缩打包的数据,文章后面我们会说到使用unpack将其解压缩)

文件签名

Phar支持的签名格式有下面几种:

MD5/SHA1/SHA256/SHA512/OpenSSL

pahr默认使用sha1加密就是有20字节的签名生成结果, 在签名后面还有8字节,前4字节表示文件使用的签名算法,最后四字节固定用于表示该文件存在签名

因为签名是整个pahr最后的数据段, 所以默认情况下我们可以知道文件最后的28字节为:

签名(默认sha1有20字节大小)+签名方式(4字节)+声明文件有无签名(4字节)

了解了phar的整个文件结构之后, 就让我们看一下phar的文件都有哪些使用方法吧:

0x02 作为普通的压缩文件使用

作为一般的压缩文件可以说是我们最基本的使用方法了, 对于这点, 如果是常规的单个文件的存储获取这些我并不想多说, 一般的用法就是通过phar:///path/to/phar.phar/pahrdir/pharfile读出文件内容

0x03 作为一个服务压缩包启用

我们需要先明白一点, 那就是在pahr压缩包中的文件如果被包含了的话那么对于里面的代码来说他们是在一个全新的目录结构中的, 并且phar压缩包中的文件之间是可以相互包含的

image-20221008040543031

可以看到,我们在一个文件中包含一个pahr压缩包中的文件和普通的文件包含并没有什么区别, 都可以传递全局变量

但是有一点是很大的不同, 那就是输出__FILE__可以看到显示的文件路径直接就是phar://协议的文件路径, 使用include文件包含的时候默认包含的是当前phar压缩包内同目录下的文件

另外还有一点是需要区别的, 那就是在test1.php中对test2.php./test2.php的解析效果是不一样的

  • test2.php指向的是pahr://phar.phar/test2.php
  • 表示test2.php指向的是C:\phpstudy_pro\WWW\test\test2.php

我们先继续包含默认的test2.php, 然后在test2.php添加一个var_dump(scandir("."))扫描当前目录并且输出看一下输出的结果是phar://phar.phar的目录还是C:\phpstudy_pro\WWW\test的目录

image-20221008041427445

可以看到输出显示的是C:\phpstudy_pro\WWW\test目录扫描结果,这时候我们再将test1.php中包含的文件改为./test2.php看一下会不会成功包含解析

image-20221008041420881

从结果可以看到并没有执行phar://phar.phar/test2.php的代码, 这就说明其寻找解析的应该是C:\phpstudy_pro\WWW\test\test2.php(但是不存在这个文件)

0x04 作为一个服务页面启用

下面我们就看一下一个phar结构的文件是如何以.php的格式直接运行或者.phps被包含如何直接启动一个服务的.

在使用.php.phps之前先继续使用.pahr格式的数据看一下默认的Stub存根数据

我们上面了解文件结构的时候使用setStub函数设置了存根数据, 那么如果我们不设置的话会不会报错呢?

答案是不会的, 并且会使用原生默认的stub数据, 执行一下可以看到():

image-20221008034744813

我们这时候可以看到, 在注释了setStub函数之后还是会有存根代码, 依旧为一段php代码, 而且还很长, 将这段代码拉出来看一下吧:

image-20221008035039396

存根数据会被放到stub.php

这里的代码说长不长说短不短, 这段代码放在phar里面我们或许并没有太多的感觉, 但是如果我们将它放在一个.php文件中呢???

修改一下phar的生成代码:

  1. <?php
  2. class TestObject {
  3. }
  4. @unlink("phar.phar");
  5. $phar = new Phar("phar.phar"); //后缀名必须为phar
  6. $phar->setAlias('h0cksr');
  7. $phar->startBuffering();
  8. /*$phar->setStub("<? __HALT_COMPILER();?>"); //设置stub*/
  9. $o = new TestObject();
  10. $phar->setMetadata($o); //将自定义的meta-data存入manifest
  11. $phar->addFromString("index.php","<?php echo 'this is index.php';?>");
  12. //签名自动计算
  13. $phar->stopBuffering();
  14. ?>

我们直接给phar.phar添加一个.php后缀名,变为phar.phar.php, 复制的数据如下:

  1. <?php
  2. $web = 'index.php';
  3. if (in_array('phar', stream_get_wrappers()) &amp;&amp; class_exists('Phar', 0)) {
  4. Phar::interceptFileFuncs();
  5. set_include_path('phar://' . __FILE__ . PATH_SEPARATOR . get_include_path());
  6. Phar::webPhar(null, $web);
  7. include 'phar://' . __FILE__ . '/' . Extract_Phar::START;
  8. return;
  9. }
  10. if (@(isset($_SERVER['REQUEST_URI']) &amp;&amp; isset($_SERVER['REQUEST_METHOD']) &amp;&amp; ($_SERVER['REQUEST_METHOD'] == 'GET' || $_SERVER['REQUEST_METHOD'] == 'POST'))) {
  11. Extract_Phar::go(true);
  12. $mimes = array(
  13. 'phps' => 2,
  14. 'c' => 'text/plain',
  15. 'cc' => 'text/plain',
  16. 'cpp' => 'text/plain',
  17. 'c++' => 'text/plain',
  18. 'dtd' => 'text/plain',
  19. 'h' => 'text/plain',
  20. 'log' => 'text/plain',
  21. 'rng' => 'text/plain',
  22. 'txt' => 'text/plain',
  23. 'xsd' => 'text/plain',
  24. 'php' => 1,
  25. 'inc' => 1,
  26. 'avi' => 'video/avi',
  27. 'bmp' => 'image/bmp',
  28. 'css' => 'text/css',
  29. 'gif' => 'image/gif',
  30. 'htm' => 'text/html',
  31. 'html' => 'text/html',
  32. 'htmls' => 'text/html',
  33. 'ico' => 'image/x-ico',
  34. 'jpe' => 'image/jpeg',
  35. 'jpg' => 'image/jpeg',
  36. 'jpeg' => 'image/jpeg',
  37. 'js' => 'application/x-javascript',
  38. 'midi' => 'audio/midi',
  39. 'mid' => 'audio/midi',
  40. 'mod' => 'audio/mod',
  41. 'mov' => 'movie/quicktime',
  42. 'mp3' => 'audio/mp3',
  43. 'mpg' => 'video/mpeg',
  44. 'mpeg' => 'video/mpeg',
  45. 'pdf' => 'application/pdf',
  46. 'png' => 'image/png',
  47. 'swf' => 'application/shockwave-flash',
  48. 'tif' => 'image/tiff',
  49. 'tiff' => 'image/tiff',
  50. 'wav' => 'audio/wav',
  51. 'xbm' => 'image/xbm',
  52. 'xml' => 'text/xml',
  53. );
  54. header("Cache-Control: no-cache, must-revalidate");
  55. header("Pragma: no-cache");
  56. $basename = basename(__FILE__);
  57. if (!strpos($_SERVER['REQUEST_URI'], $basename)) {
  58. chdir(Extract_Phar::$temp);
  59. include $web;
  60. return;
  61. }
  62. $pt = substr($_SERVER['REQUEST_URI'], strpos($_SERVER['REQUEST_URI'], $basename) + strlen($basename));
  63. if (!$pt || $pt == '/') {
  64. $pt = $web;
  65. header('HTTP/1.1 301 Moved Permanently');
  66. header('Location: ' . $_SERVER['REQUEST_URI'] . '/' . $pt);
  67. exit;
  68. }
  69. $a = realpath(Extract_Phar::$temp . DIRECTORY_SEPARATOR . $pt);
  70. if (!$a || strlen(dirname($a)) < strlen(Extract_Phar::$temp)) {
  71. header('HTTP/1.0 404 Not Found');
  72. echo "<html>\n <head>\n <title>File Not Found<title>\n </head>\n <body>\n <h1>404 - File Not Found</h1>\n </body>\n</html>";
  73. exit;
  74. }
  75. $b = pathinfo($a);
  76. if (!isset($b['extension'])) {
  77. header('Content-Type: text/plain');
  78. header('Content-Length: ' . filesize($a));
  79. readfile($a);
  80. exit;
  81. }
  82. if (isset($mimes[$b['extension']])) {
  83. if ($mimes[$b['extension']] === 1) {
  84. include $a;
  85. exit;
  86. }
  87. if ($mimes[$b['extension']] === 2) {
  88. highlight_file($a);
  89. exit;
  90. }
  91. header('Content-Type: ' .$mimes[$b['extension']]);
  92. header('Content-Length: ' . filesize($a));
  93. readfile($a);
  94. exit;
  95. }
  96. }
  97. class Extract_Phar
  98. {
  99. static $temp;
  100. static $origdir;
  101. const GZ = 0x1000;
  102. const BZ2 = 0x2000;
  103. const MASK = 0x3000;
  104. const START = 'index.php';
  105. const LEN = 6643;
  106. static function go($return = false)
  107. {
  108. $fp = fopen(__FILE__, 'rb');
  109. fseek($fp, self::LEN);
  110. $L = unpack('V', $a = fread($fp, 4));
  111. $m = '';
  112. do {
  113. $read = 8192;
  114. if ($L[1] - strlen($m) < 8192) {
  115. $read = $L[1] - strlen($m);
  116. }
  117. $last = fread($fp, $read);
  118. $m .= $last;
  119. } while (strlen($last) &amp;&amp; strlen($m) < $L[1]);
  120. if (strlen($m) < $L[1]) {
  121. die('ERROR: manifest length read was "' .
  122. strlen($m) .'" should be "' .
  123. $L[1] . '"');
  124. }
  125. $info = self::_unpack($m);
  126. $f = $info['c'];
  127. if ($f &amp; self::GZ) {
  128. if (!function_exists('gzinflate')) {
  129. die('Error: zlib extension is not enabled -' .
  130. ' gzinflate() function needed for zlib-compressed .phars');
  131. }
  132. }
  133. if ($f &amp; self::BZ2) {
  134. if (!function_exists('bzdecompress')) {
  135. die('Error: bzip2 extension is not enabled -' .
  136. ' bzdecompress() function needed for bz2-compressed .phars');
  137. }
  138. }
  139. $temp = self::tmpdir();
  140. if (!$temp || !is_writable($temp)) {
  141. $sessionpath = session_save_path();
  142. if (strpos ($sessionpath, ";") !== false)
  143. $sessionpath = substr ($sessionpath, strpos ($sessionpath, ";")+1);
  144. if (!file_exists($sessionpath) || !is_dir($sessionpath)) {
  145. die('Could not locate temporary directory to extract phar');
  146. }
  147. $temp = $sessionpath;
  148. }
  149. $temp .= '/pharextract/'.basename(__FILE__, '.phar');
  150. self::$temp = $temp;
  151. self::$origdir = getcwd();
  152. @mkdir($temp, 0777, true);
  153. $temp = realpath($temp);
  154. if (!file_exists($temp . DIRECTORY_SEPARATOR . md5_file(__FILE__))) {
  155. self::_removeTmpFiles($temp, getcwd());
  156. @mkdir($temp, 0777, true);
  157. @file_put_contents($temp . '/' . md5_file(__FILE__), '');
  158. foreach ($info['m'] as $path => $file) {
  159. $a = !file_exists(dirname($temp . '/' . $path));
  160. @mkdir(dirname($temp . '/' . $path), 0777, true);
  161. clearstatcache();
  162. if ($path[strlen($path) - 1] == '/') {
  163. @mkdir($temp . '/' . $path, 0777);
  164. } else {
  165. file_put_contents($temp . '/' . $path, self::extractFile($path, $file, $fp));
  166. @chmod($temp . '/' . $path, 0666);
  167. }
  168. }
  169. }
  170. chdir($temp);
  171. if (!$return) {
  172. include self::START;
  173. }
  174. }
  175. static function tmpdir()
  176. {
  177. if (strpos(PHP_OS, 'WIN') !== false) {
  178. if ($var = getenv('TMP') ? getenv('TMP') : getenv('TEMP')) {
  179. return $var;
  180. }
  181. if (is_dir('/temp') || mkdir('/temp')) {
  182. return realpath('/temp');
  183. }
  184. return false;
  185. }
  186. if ($var = getenv('TMPDIR')) {
  187. return $var;
  188. }
  189. return realpath('/tmp');
  190. }
  191. static function _unpack($m)
  192. {
  193. $info = unpack('V', substr($m, 0, 4));
  194. $l = unpack('V', substr($m, 10, 4));
  195. $m = substr($m, 14 + $l[1]);
  196. $s = unpack('V', substr($m, 0, 4));
  197. $o = 0;
  198. $start = 4 + $s[1];
  199. $ret['c'] = 0;
  200. for ($i = 0; $i < $info[1]; $i++) {
  201. $len = unpack('V', substr($m, $start, 4));
  202. $start += 4;
  203. $savepath = substr($m, $start, $len[1]);
  204. $start += $len[1];
  205. $ret['m'][$savepath] = array_values(unpack('Va/Vb/Vc/Vd/Ve/Vf', substr($m, $start, 24)));
  206. $ret['m'][$savepath][3] = sprintf('%u', $ret['m'][$savepath][3]
  207. &amp; 0xffffffff);
  208. $ret['m'][$savepath][7] = $o;
  209. $o += $ret['m'][$savepath][2];
  210. $start += 24 + $ret['m'][$savepath][5];
  211. $ret['c'] |= $ret['m'][$savepath][4] &amp; self::MASK;
  212. }
  213. return $ret;
  214. }
  215. static function extractFile($path, $entry, $fp)
  216. {
  217. $data = '';
  218. $c = $entry[2];
  219. while ($c) {
  220. if ($c < 8192) {
  221. $data .= @fread($fp, $c);
  222. $c = 0;
  223. } else {
  224. $c -= 8192;
  225. $data .= @fread($fp, 8192);
  226. }
  227. }
  228. if ($entry[4] &amp; self::GZ) {
  229. $data = gzinflate($data);
  230. } elseif ($entry[4] &amp; self::BZ2) {
  231. $data = bzdecompress($data);
  232. }
  233. if (strlen($data) != $entry[0]) {
  234. die("Invalid internal .phar file (size error " . strlen($data) . " != " .
  235. $stat[7] . ")");
  236. }
  237. if ($entry[3] != sprintf("%u", crc32($data) &amp; 0xffffffff)) {
  238. die("Invalid internal .phar file (checksum error)");
  239. }
  240. return $data;
  241. }
  242. static function _removeTmpFiles($temp, $origdir)
  243. {
  244. chdir($temp);
  245. foreach (glob('*') as $f) {
  246. if (file_exists($f)) {
  247. is_dir($f) ? @rmdir($f) : @unlink($f);
  248. if (file_exists($f) &amp;&amp; is_dir($f)) {
  249. self::_removeTmpFiles($f, getcwd());
  250. }
  251. }
  252. }
  253. @rmdir($temp);
  254. clearstatcache();
  255. chdir($origdir);
  256. }
  257. }
  258. Extract_Phar::go();
  259. __HALT_COMPILER(); ?>S h0cksr O:10:"TestObject":0:{} index.php! _�@c! �(�'� <?php echo 'this is index.php';?>�Vpi����Ӿ�\v����x GBMB

可以看到有两百多行代码, 但是它的结构是很简单的,就是两个if判断语句还有一个Extract_Phar::go();(第二个if语句也会执行这个go函数)

image-20221008053517106

第一个if(事实上一般我们都是在第一个if就结束了):

判断pahr在不在当前运行系统中已经注册并可使用的流类型列表中, 并且判断是否存在Phar类, 如果都有的话进入第一个if:

前面几行代码中我们只需要关注两行代码:

  1. set_include_path('phar://' . __FILE__ . PATH_SEPARATOR . get_include_path());
  2. include 'phar://' . __FILE__ . '/' . Extract_Phar::START;//Extract_Phar::START默认就是index.php

第一行代码先是将当前文件经过phar://解析后作为第一优先级的包含目录,

第二行代码使用phar://解析当前文件并且包含pahr包里的index.php文件

第二个if

第二个if先是执行Extract_Phar::go(true);,然后就是进行服务逻辑判断, 中间进行了很多的请求判断最终返回相应的结果到客户端

Extract_Phar::go(true);

这个go函数就是用于解压phar文件的, 因为后面这些代码都是在第一个if判断不满足, 也就是当前拓展不直接顺利解压pahr文件的时候执行的, 所以可以说代码就是使用php代码的形式实现pahr压缩包的解压了.
可以看到里面使用的_unpack函数对phar的原始数据进行了多次多点的unpack数据解压, 从而得到数据的目录结构,文件信息以及文件内容,将其一一对应起来

此外有一个变量参数是值得我们注意的, 那就是Extract_Phar::LEN
这个变量可以在代码中看到在读取数据进行解压之前先是读取了这个LEN并且移动指针偏移LEN个字节,然后再进行数据读取和解压工作
默认情况下我们之所以任意在Stub添加数据的原因就是因为这个LEN,在pahr格式的文件中有对应的LEN大小pack压缩数据指定文件头偏移

一次失败的尝试小记

虽然说没有成功但也算是一次简单的思维发散了, 所以就记一下吧

在整个默认的stub存根代码中最引起我关注的便是Extract_Phar::go的解压写入文件的过程, 因为开始的时候我认为后面的代码是在默认不支持phar文件格式的解压的情况下执行的php代码, 正是这些php代码的功能完成了phar文件的解压, 所以我就认为默认的phar解释器对其进行phar协议数据解析的时候操作的实际解压过程也和这里的php源码一样, 如果是一样的话, 那么我们就可以任意文件写了, 因为它的文件名是从原始数据unpack解压拿到的, 而且会将这个文件名直接拼接到临时文件夹目录后,然后将原始文件数据写入到临时文件夹下的历史文件中, 那么这时候我们是否可以将文件名修改为../../../多个上级目录/tmp/php然后往/tmp/php写文件(或者像之前p牛一样通过非法文件名让文件驻留在解压后的目录下)?

但是其实不然, 在我使用010editor修改pahr文件中的文件名, 然后又进行哈希修复后发现并没有成功, 从始至终我们都没有看到phar往/tmp生成任何文件, 我的尝试步骤如下:

  1. 生成有两个文件012345678901234567890123456789012.php1.php的pahr压缩包pahr.phar
  2. 使用010editor012345678901234567890123456789012.php修改为../../../../../../../../../../tmp/php
  3. 使用签名修复脚本更新签名
  4. 使用fswatch监控/tmp目录文件的变化
  5. 执行php测试代码执行file_get_contents使用pahr://协议解析修改后的pahr压缩包取出里面的文件输出文件内容
  6. 最后显示结果是/tmp下面并没有解压生成/tmp/php文件, 而pahr压缩包中的1.php被成功输出说明签名修正是没有问题的, 但是输出原本的012345678901234567890123456789012.php三种方式均失败了

image-20221008072218482

之后看到默认的stub存根代码中会在临时目录下创建文件夹pharextract, 然后我就到php源码中检索了一下这个关键字, 结果是None, 这就说明php解释器对phar文件的解析确实是与默认stub存根中Extract_Phar::go函数的代码是有所区别的

对于文件解压的过程我并没有去debug到详细的函数点, 但是对解析报错的语句进行代码溯源确定到了PHP解释器源代码中的ext/phar/util.c:342 phar_get_entry_data, 正是在这个函数中获取资源失败返回了FAILURE才导致输出警告pahr error:"xxx" is not a file in phar "phar.phar"但是往回走找到解压函数以及接续往下走找到更深更详细的函数代码点我并没有做, 在这里放一个调试的调用栈感兴趣的师傅可以去跟一下:

image-20221008074419462

0x05 更多其他…

至于其他一些有意思的用法这里简单的说两个吧:

别名使用

image-20221008075012654

但是这个别名的使用只能在生成文件的当前以及phar的内部, 如果将这个生成的pahr.phar放到一个新的目录下, 任何在一个test.php中写入以下测试代码:

  1. <?php
  2. var_dump(file_get_contents("phar://h0cksr/1.php"));

此时执行test.php就会失败了,因为此时并不知道这个别名,只会将h0cksr作为文件查找

image-20221008075335492

Phar::webPhar

  1. <?php
  2. // creating the phar archive:
  3. try {
  4. $phar = new Phar('myphar.phar');
  5. $phar['index.php'] = '<?php echo "Hello World,this is index.php"; ?>';
  6. $phar['hello.php'] = '<?php echo "Hello World,this is hello.php"; ?>';
  7. $phar['default.php'] = '<?php echo "Hello World,this is default.php"; ?>';
  8. $phar->setStub('<?php
  9. Phar::webPhar("","index.php","default.php");
  10. __HALT_COMPILER(); ?>');
  11. } catch (Exception $e) {
  12. // handle error here
  13. }
  14. ?>

生成myphar.phar后重命名为myphar.phar.php,任何就可以将其作为一个服务访问了

在Stub存根中的Phar::webPhar函数作用就是指定服务的默认加载文件, 指定如果访问的服务资源不存在时加载的文件

以上demo中默认首页加载index.php, 访问的资源不存在时加载default.php, 我们定义的hello.php可以当做一个资源访问:

/myphar.phar.php

image-20221008080615821

/myphar.phar.php/hello.php

image-20221008080645778

/myphar.phar.php/xxx.php

image-20221008080707578

此外Phar::webPhar的第一个参数可以使用别名指定phar压缩包, 指定之后后面的首页资源路径和默认资源路径均是别名对应压缩包下的资源文件

0x06 相关类和函数

  • Phar

    - Phar 类

  1. - [Phar::addEmptyDir](https://www.php.net/manual/zh/phar.addemptydir.php) — 添加一个空目录到 phar 档案
  2. - [Phar::addFile](https://www.php.net/manual/zh/phar.addfile.php) — 将一个文件从文件系统添加到 phar 档案中
  3. - [Phar::addFromString](https://www.php.net/manual/zh/phar.addfromstring.php) — 以字符串的形式添加一个文件到 phar 档案
  4. - [Phar::apiVersion](https://www.php.net/manual/zh/phar.apiversion.php) — 返回 api 版本
  5. - [Phar::buildFromDirectory](https://www.php.net/manual/zh/phar.buildfromdirectory.php) — 从目录中的文件构建 phar 归档
  6. - [Phar::buildFromIterator](https://www.php.net/manual/zh/phar.buildfromiterator.php) — 从迭代器构造 phar 档案
  7. - [Phar::canCompress](https://www.php.net/manual/zh/phar.cancompress.php) — 返回 phar 扩展是否支持使用 zlib 或 bzip2 进行压缩
  8. - [Phar::canWrite](https://www.php.net/manual/zh/phar.canwrite.php) — 返回 phar 扩展是否支持写入和创建 phas
  9. - [Phar::compress](https://www.php.net/manual/zh/phar.compress.php) — 使用 Gzip 或 Bzip2 压缩压缩整个 Phar 存档
  10. - [Phar::compressFiles](https://www.php.net/manual/zh/phar.compressfiles.php) — 压缩当前 Phar 存档中的所有文件
  11. - [Phar::\_\_construct](https://www.php.net/manual/zh/phar.construct.php) — 构造一个 Phar 归档对象
  12. - [Phar::convertToData](https://www.php.net/manual/zh/phar.converttodata.php) — 将 phar 存档转换为不可执行的 tar 或 zip 文件
  13. - [Phar::convertToExecutable](https://www.php.net/manual/zh/phar.converttoexecutable.php) — 将 phar 存档转换为另一种可执行的 phar 存档文件格式
  14. - [Phar::copy](https://www.php.net/manual/zh/phar.copy.php) — 将 phar 存档内部的文件复制到 phar 中的另一个新文件
  15. - [Phar::count](https://www.php.net/manual/zh/phar.count.php) — 返回 Phar 存档中的条目(文件)数
  16. - [Phar::createDefaultStub](https://www.php.net/manual/zh/phar.createdefaultstub.php) — 创建一个 phar 文件格式特定的存根
  17. - [Phar::decompress](https://www.php.net/manual/zh/phar.decompress.php) — 解压整个 Phar 档案
  18. - [Phar::decompressFiles](https://www.php.net/manual/zh/phar.decompressfiles.php) — 解压当前 Phar 存档中的所有文件
  19. - [Phar::delMetadata](https://www.php.net/manual/zh/phar.delmetadata.php) — 删除 phar 的全局元数据
  20. - [Phar::delete](https://www.php.net/manual/zh/phar.delete.php) — 删除 phar 档案中的一个文件
  21. - [Phar::\_\_destruct](https://www.php.net/manual/zh/phar.destruct.php) — Phar 归档对象的析构函数
  22. - [Phar::extractTo](https://www.php.net/manual/zh/phar.extractto.php) — 将 phar 存档的内容提取到目录
  23. - [Phar::getAlias](https://www.php.net/manual/zh/phar.getalias.php) — 获取 Phar 的别名
  24. - [Phar::getMetadata](https://www.php.net/manual/zh/phar.getmetadata.php) — 返回 phar 存档元数据
  25. - [Phar::getModified](https://www.php.net/manual/zh/phar.getmodified.php) — 返回 phar 是否被修改
  26. - [Phar::getPath](https://www.php.net/manual/zh/phar.getpath.php) — 获取磁盘上 Phar 存档的真实路径
  27. - [Phar::getSignature](https://www.php.net/manual/zh/phar.getsignature.php) — 返回 Phar 存档的 MD5/SHA1/SHA256/SHA512/OpenSSL 签名
  28. - [Phar::getStub](https://www.php.net/manual/zh/phar.getstub.php) — 返回 Phar 存档的 PHP 加载器或引导存根
  29. - [Phar::getSupportedCompression](https://www.php.net/manual/zh/phar.getsupportedcompression.php) — 返回支持的压缩算法数组
  30. - [Phar::getSupportedSignatures](https://www.php.net/manual/zh/phar.getsupportedsignatures.php) — 返回支持的签名类型数组
  31. - [Phar::getVersion](https://www.php.net/manual/zh/phar.getversion.php) — 返回 Phar 存档的版本信息
  32. - [Phar::hasMetadata](https://www.php.net/manual/zh/phar.hasmetadata.php) — 返回 phar 是否有全局元数据
  33. - [Phar::interceptFileFuncs](https://www.php.net/manual/zh/phar.interceptfilefuncs.php) — 指示 phar 拦截 fopen、file\_get\_contents、opendir 和所有与 stat 相关的函数
  34. - [Phar::isBuffering](https://www.php.net/manual/zh/phar.isbuffering.php) — 用于确定 Phar 写操作是被缓冲,还是直接刷新到磁盘
  35. - [Phar::isCompressed](https://www.php.net/manual/zh/phar.iscompressed.php) — 如果整个 phar 存档已压缩(.tar.gz/tar.bz 等),则返回 Phar::GZ 或 PHAR::BZ2
  36. - [Phar::isFileFormat](https://www.php.net/manual/zh/phar.isfileformat.php) — 如果 phar 存档基于 tar/phar/zip 文件格式,则返回 true,具体取决于参数
  37. - [Phar::isValidPharFilename](https://www.php.net/manual/zh/phar.isvalidpharfilename.php) — 返回给定文件名是否是有效的 phar 文件名
  38. - [Phar::isWritable](https://www.php.net/manual/zh/phar.iswritable.php) — 如果 phar 存档可以修改,则返回 true
  39. - [Phar::loadPhar](https://www.php.net/manual/zh/phar.loadphar.php) — 使用别名加载任何 phar 存档
  40. - [Phar::mapPhar](https://www.php.net/manual/zh/phar.mapphar.php) — 读取当前执行的文件(一个 phar)并注册它的清单
  41. - [Phar::mount](https://www.php.net/manual/zh/phar.mount.php) — 将外部路径或文件挂载到 phar 存档中的虚拟位置
  42. - [Phar::mungServer](https://www.php.net/manual/zh/phar.mungserver.php) — 定义最多 4 个 $\_SERVER 变量的列表,这些变量应该被修改以执行
  43. - [Phar::offsetExists](https://www.php.net/manual/zh/phar.offsetexists.php) — 确定 phar 中是否存在文件
  44. - [Phar::offsetGet](https://www.php.net/manual/zh/phar.offsetget.php) — 获取特定文件的 PharFileInfo 对象
  45. - [Phar::offsetSet](https://www.php.net/manual/zh/phar.offsetset.php) — 将内部文件的内容设置为外部文件的内容
  46. - [Phar::offsetUnset](https://www.php.net/manual/zh/phar.offsetunset.php) — 从 phar 中删除文件
  47. - [Phar::running](https://www.php.net/manual/zh/phar.running.php) — 返回磁盘上的完整路径或当前执行的 Phar 存档的完整 phar URL
  48. - [Phar::setAlias](https://www.php.net/manual/zh/phar.setalias.php) — 设置 Phar 档案的别名
  49. - [Phar::setDefaultStub](https://www.php.net/manual/zh/phar.setdefaultstub.php) — 用于将 Phar 存档的 PHP 加载器或引导存根设置为默认加载器
  50. - [Phar::setMetadata](https://www.php.net/manual/zh/phar.setmetadata.php) — 设置 phar 存档元数据
  51. - [Phar::setSignatureAlgorithm](https://www.php.net/manual/zh/phar.setsignaturealgorithm.php) — 设置 phar 的签名算法并应用它
  52. - [Phar::setStub](https://www.php.net/manual/zh/phar.setstub.php) — 用于设置 Phar 存档的 PHP 加载器或引导存根
  53. - [Phar::startBuffering](https://www.php.net/manual/zh/phar.startbuffering.php) — 开始缓冲 Phar 写操作,不要修改磁盘上的 Phar 对象
  54. - [Phar::stopBuffering](https://www.php.net/manual/zh/phar.stopbuffering.php) — 停止缓冲对 Phar 归档的写入请求,并将更改保存到磁盘
  55. - [Phar::unlinkArchive](https://www.php.net/manual/zh/phar.unlinkarchive.php) — 从磁盘和内存中完全删除 phar 存档
  56. - [Phar::webPhar](https://www.php.net/manual/zh/phar.webphar.php) — 将来自 Web 浏览器的请求路由到 phar 存档中的内部文件
  • PharData

    — PharData 类

  1. - [PharData::addEmptyDir](https://www.php.net/manual/zh/phardata.addemptydir.php) — 将一个空目录添加到 tar/zip 存档中
  2. - [PharData::addFile](https://www.php.net/manual/zh/phardata.addfile.php) — 将文件系统中的文件添加到 tar/zip 存档
  3. - [PharData::addFromString](https://www.php.net/manual/zh/phardata.addfromstring.php) — 将文件系统中的文件添加到 tar/zip 存档
  4. - [PharData::buildFromDirectory](https://www.php.net/manual/zh/phardata.buildfromdirectory.php) — 从目录中的文件构造一个 tar/zip 存档
  5. - [PharData::buildFromIterator](https://www.php.net/manual/zh/phardata.buildfromiterator.php) — 从迭代器构造 tar 或 zip 存档
  6. - [PharData::compress](https://www.php.net/manual/zh/phardata.compress.php) — 使用 Gzip 或 Bzip2 压缩压缩整个 tar/zip 存档
  7. - [PharData::compressFiles](https://www.php.net/manual/zh/phardata.compressfiles.php) — 压缩当前 tar/zip 存档中的所有文件
  8. - [PharData::\_\_construct](https://www.php.net/manual/zh/phardata.construct.php) — 构造一个不可执行的 tar 或 zip 归档对象
  9. - [PharData::convertToData](https://www.php.net/manual/zh/phardata.converttodata.php) — 将 phar 存档转换为不可执行的 tar 或 zip 文件
  10. - [PharData::convertToExecutable](https://www.php.net/manual/zh/phardata.converttoexecutable.php) — 将不可执行的 tar/zip 存档转换为可执行的 phar 存档
  11. - [PharData::copy](https://www.php.net/manual/zh/phardata.copy.php) — 将 phar 存档内部的文件复制到 phar 中的另一个新文件
  12. - [PharData::decompress](https://www.php.net/manual/zh/phardata.decompress.php) — 解压整个 Phar 档案
  13. - [PharData::decompressFiles](https://www.php.net/manual/zh/phardata.decompressfiles.php) — 解压缩当前 zip 存档中的所有文件
  14. - [PharData::delMetadata](https://www.php.net/manual/zh/phardata.delmetadata.php) — 删除 zip 存档的全局元数据
  15. - [PharData::delete](https://www.php.net/manual/zh/phardata.delete.php) — 删除 tar/zip 存档中的文件
  16. - [PharData::\_\_destruct](https://www.php.net/manual/zh/phardata.destruct.php) — 破坏一个不可执行的 tar 或 zip 归档对象
  17. - [PharData::extractTo](https://www.php.net/manual/zh/phardata.extractto.php) — 将 tar/zip 存档的内容提取到目录
  18. - [PharData::isWritable](https://www.php.net/manual/zh/phardata.iswritable.php) — 如果可以修改 tar/zip 存档,则返回 true
  19. - [PharData::offsetSet](https://www.php.net/manual/zh/phardata.offsetset.php) — 将 tar/zip 中的文件内容设置为外部文件或字符串的内容
  20. - [PharData::offsetUnset](https://www.php.net/manual/zh/phardata.offsetunset.php) — 从 tar/zip 存档中删除文件
  21. - [PharData::setAlias](https://www.php.net/manual/zh/phardata.setalias.php) — 虚拟函数(Phar::setAlias 对 PharData 无效)
  22. - [PharData::setDefaultStub](https://www.php.net/manual/zh/phardata.setdefaultstub.php) — 虚拟函数(Phar::setDefaultStub 对 PharData 无效)
  23. - [PharData::setMetadata](https://www.php.net/manual/zh/phardata.setmetadata.php) — 设置 phar 存档元数据
  24. - [PharData::setSignatureAlgorithm](https://www.php.net/manual/zh/phardata.setsignaturealgorithm.php) — 设置 phar 的签名算法并应用它
  25. - [PharData::setStub](https://www.php.net/manual/zh/phardata.setstub.php) — 虚拟函数(Phar::setStub 对 PharData 无效)
  • PharFileInfo

    — PharFileInfo 类

  1. - [PharFileInfo::chmod](https://www.php.net/manual/zh/pharfileinfo.chmod.php) — 设置文件特定的权限位
  2. - [PharFileInfo::compress](https://www.php.net/manual/zh/pharfileinfo.compress.php) — 使用 zlib 或 bzip2 压缩压缩当前 Phar 条目
  3. - [PharFileInfo::\_\_construct](https://www.php.net/manual/zh/pharfileinfo.construct.php) — 构造一个 Phar 入口对象
  4. - [PharFileInfo::decompress](https://www.php.net/manual/zh/pharfileinfo.decompress.php) — 解压缩 phar 中的当前 Phar 条目
  5. - [PharFileInfo::delMetadata](https://www.php.net/manual/zh/pharfileinfo.delmetadata.php) — 删除条目的元数据
  6. - [PharFileInfo::\_\_destruct](https://www.php.net/manual/zh/pharfileinfo.destruct.php) — 破坏一个 Phar 入口对象
  7. - [PharFileInfo::getCRC32](https://www.php.net/manual/zh/pharfileinfo.getcrc32.php) — 返回 CRC32 代码或在 CRC 未验证时抛出异常
  8. - [PharFileInfo::getCompressedSize](https://www.php.net/manual/zh/pharfileinfo.getcompressedsize.php) — 返回 Phar 存档中文件的实际大小(带压缩)
  9. - [PharFileInfo::getContent](https://www.php.net/manual/zh/pharfileinfo.getcontent.php) — 获取条目的完整文件内容
  10. - [PharFileInfo::getMetadata](https://www.php.net/manual/zh/pharfileinfo.getmetadata.php) — 返回与文件一起保存的特定于文件的元数据
  11. - [PharFileInfo::getPharFlags](https://www.php.net/manual/zh/pharfileinfo.getpharflags.php) — 返回 Phar 文件条目标志
  12. - [PharFileInfo::hasMetadata](https://www.php.net/manual/zh/pharfileinfo.hasmetadata.php) — 返回条目的元数据
  13. - [PharFileInfo::isCRCChecked](https://www.php.net/manual/zh/pharfileinfo.iscrcchecked.php) — 返回文件条目是否已验证其 CRC
  14. - [PharFileInfo::isCompressed](https://www.php.net/manual/zh/pharfileinfo.iscompressed.php) — 返回条目是否被压缩
  15. - [PharFileInfo::setMetadata](https://www.php.net/manual/zh/pharfileinfo.setmetadata.php) — 设置与文件一起保存的文件特定元数据
  16. - [PharException](https://www.php.net/manual/zh/class.pharexception.php) — PharException 类

</body></html>

  • 发表于 2022-10-17 10:03:52
  • 阅读 ( 7387 )
  • 分类:WEB安全

0 条评论

markin
markin

10 篇文章

站长统计