问题描述
我一直在尝试了解如何使用PHP://fd/<n>
包装器。 documentation声明以下内容:
在我看来,这意味着PHP://fd
包装器提供了对底层文件描述符的访问,这在操作系统进程的上下文中可以理解。例如,我希望以下信件能够成立:
但是,在测试中,我实际上无法使该包装器正常工作。考虑以下测试PHP代码,该代码打开/etc/passwd
文件并查找500个字节。它还具有一些实用程序代码,用于列出目录并包括文件内容。
<?PHP
ini_set('display_errors',1);
ini_set('display_startup_errors',1);
error_reporting(E_ALL);
$f = fopen("/etc/passwd","r");
fread($f,500);
echo "$f\n";
function listdir($d) {
foreach (scandir($d) as $f) {
if (file_exists("$d/$f")) {
$s=lstat("$d/$f");
$l=($s[2] & 0120000) == 0120000 ? readlink("$d/$f") : '';
printf("%-25s %6d %6d %6o %8d %s %s\n",$f,$s[4],$s[5],$s[2],$s[7],strftime('%F %T',$s[9]),$l);
}
}
}
if (isset($_GET["dir"])) {
listdir($_GET["dir"]);
}
if (isset($_GET["file"])) {
include_once($_GET["file"]);
} elseif (isset($_GET["content"])) {
echo file_get_contents($_GET["content"]);
} elseif (isset($_GET["fopen"])) {
echo fread(fopen($_GET["fopen"],"r"),1024);
}
?>
由于上述代码在/etc/passwd
文件中的位置500处保留了打开的文件描述符,因此我希望可以使用PHP://fd
包装器从该位置继续读取文件。但是,这似乎是不可能的。
在第一个示例中,它打印/proc/self/fd
目录的内容(可以看到open fd为12),然后尝试使用PHP://fd/12
语法打开此fd。请注意,目录中列出的字段为:name
,uid
,gid
,mode
,size
,modify date
和readlink
$ curl '192.168.56.47?dir=/proc/self/fd&fopen=PHP://fd/12'
Resource id #2
. 33 33 40500 0 2020-09-24 09:57:46
.. 33 33 40555 0 2020-09-24 09:53:22
0 33 33 120500 64 2020-09-24 10:09:08 /dev/null
1 33 33 120300 64 2020-09-24 10:09:08 /dev/null
10 33 33 120700 64 2020-09-24 10:09:08 anon_inode:[eventpoll]
11 33 33 120700 64 2020-09-24 10:27:43 socket:[50335]
12 33 33 120500 64 2020-09-24 14:16:50 /etc/passwd
2 33 33 120300 64 2020-09-24 10:09:08 /var/log/apache2/error.log
3 33 33 120700 64 2020-09-24 10:09:08 socket:[46732]
4 33 33 120700 64 2020-09-24 10:09:08 socket:[46733]
5 33 33 120500 64 2020-09-24 10:09:08 pipe:[47544]
6 33 33 120300 64 2020-09-24 10:09:08 pipe:[47544]
7 33 33 120300 64 2020-09-24 10:09:08 /var/log/apache2/other_vhosts_access.log
8 33 33 120300 64 2020-09-24 10:09:08 /var/log/apache2/access.log
9 33 33 120700 64 2020-09-24 10:09:08 /tmp/.ZendSem.C011w0 (deleted)
<br />
<b>Warning</b>: fopen(PHP://fd/12): Failed to open stream: operation Failed in <b>/var/www/html/index.PHP</b> on line <b>25</b><br />
<br />
<b>Warning</b>: fread() expects parameter 1 to be resource,bool given in <b>/var/www/html/index.PHP</b> on line <b>25</b><br />
如果使用file_get_contents
代替fread(fopen(...
,结果是相同的。
作为第二个示例,我想知道文档是否意味着PHP资源号而不是操作系统文件描述符,因此将其更改为使用数字2(如输出的第一行所示)而不是12。但是再次,结果是一样的:
$ curl '192.168.56.47?dir=/proc/self/fd&fopen=PHP://fd/2'
Resource id #2
. 33 33 40500 0 2020-09-24 09:54:03
.. 33 33 40555 0 2020-09-24 09:53:22
0 33 33 120500 64 2020-09-24 10:09:08 /dev/null
1 33 33 120300 64 2020-09-24 10:09:08 /dev/null
10 33 33 120700 64 2020-09-24 10:09:08 anon_inode:[eventpoll]
11 33 33 120700 64 2020-09-24 10:27:58 socket:[50337]
12 33 33 120500 64 2020-09-24 14:13:27 /etc/passwd
2 33 33 120300 64 2020-09-24 10:09:08 /var/log/apache2/error.log
3 33 33 120700 64 2020-09-24 10:09:08 socket:[46732]
4 33 33 120700 64 2020-09-24 10:09:08 socket:[46733]
5 33 33 120500 64 2020-09-24 10:09:08 pipe:[47544]
6 33 33 120300 64 2020-09-24 10:09:08 pipe:[47544]
7 33 33 120300 64 2020-09-24 10:09:08 /var/log/apache2/other_vhosts_access.log
8 33 33 120300 64 2020-09-24 10:09:08 /var/log/apache2/access.log
9 33 33 120700 64 2020-09-24 10:09:08 /tmp/.ZendSem.C011w0 (deleted)
<br />
<b>Warning</b>: fopen(PHP://fd/2): Failed to open stream: operation Failed in <b>/var/www/html/index.PHP</b> on line <b>25</b><br />
<br />
<b>Warning</b>: fread() expects parameter 1 to be resource,bool given in <b>/var/www/html/index.PHP</b> on line <b>25</b><br />
但是,由于/proc/self/fd
的内容是符号链接,因此以下内容是可能的,但我所希望理解的不是它,因为它打开了一个新的文件描述符而不重用现有的文件描述符:
$ curl '192.168.56.47?fopen=/proc/self/fd/12'
Resource id #2
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
...snip...
我尝试在Internet上搜索有关打算使用此包装器的示例,但没有找到任何可行的示例。任何帮助将不胜感激。
此外,我并不是真正在尝试解决特定问题,而是在尝试理解其工作原理。
以上测试是在Apache 2.4.41
和PHP 7.4.3
上进行的。
更新:答案
在cli
解释器中测试以下文件后,我注意到PHP://fd/
包装程序的行为符合预期:
<?PHP
ini_set('display_errors',1);
error_reporting(E_ALL);
// below taken from: https://stackoverflow.com/a/7033247/5660642
function fd($realpath) {
$dir = '/proc/self/fd/';
if ($dh = opendir($dir)) {
while (($file = readdir($dh)) !== false) {
$filename = $dir . $file;
if (filetype($filename) == 'link' && realpath($filename) == $realpath) {
closedir($dh);
return $file;
}
}
closedir($dh);
}
return FALSE;
}
$f = fopen('/etc/passwd','r');
$fd = fd('/etc/passwd');
echo "opened fd: $fd\n";
stream_set_read_buffer($f,0); // disable buffering before reading
echo fread($f,10)."\n";
$g = fopen("PHP://fd/$fd",'r');
echo ftell($f)."\n";
echo ftell($g)."\n";
这将提供以下输出:
$ PHP script.PHP
opened fd: 3
root:x:0:0
10
10
$ curl '192.168.56.47/script.PHP'
opened fd: 12
root:x:0:0
<br />
<b>Warning</b>: fopen(PHP://fd/12): Failed to open stream: operation Failed in <b>/var/www/html/script.PHP</b> on line <b>25</b><br />
10
<br />
<b>Warning</b>: ftell() expects parameter 1 to be resource,bool given in <b>/var/www/html/script.PHP</b> on line <b>27</b><br />
硬编码
比较cli
和Apache模块之间的配置后,我最终检查了源代码。看来此行为在php source code中是硬编码的:
} else if (!strncasecmp(path,"fd/",3)) {
const char *start;
char *end;
zend_long fildes_ori;
int dtablesize;
if (strcmp(sapi_module.name,"cli")) {
if (options & REPORT_ERRORS) {
PHP_error_docref(NULL,E_WARNING,"Direct access to file descriptors is only available from command-line PHP");
}
return NULL;
}
此实现的落实时间可追溯到2012年:https://github.com/php/php-src/commit/df2a38e7f8603f51afa4c2257b3369067817d818
但是,我没有在文档中看到此限制,但是在更改日志中。但尽管如此,它还是个好消息。
解决方法
不是100%回答正在做的事情,但希望能提供一些见识。
玩了一会儿,最终打开了第二个文件,所以我可以使用ftell()
来查看文件指针是什么。所以
$f = fopen("/etc/passwd","r");
fread($f,500);
echo "$f\n";
echo ftell($f).PHP_EOL;
给予
Resource id #86
500
和第二部分
} elseif (isset($_GET["fopen"])) {
$f1 = fopen($_GET["fopen"],"r");
echo ftell($f1).PHP_EOL;
echo ">".fread($f1,10)."<".PHP_EOL;
}
在我的机器上给出...
3087
><
这使我相信系统实际上已经缓冲了读取,并且文件指针(在这种情况下)当前位于文件的末尾,因此没有内容。
所以接下来要做的是尝试一个大文件(5.1MB,不确定我从何处下载)...
$f = fopen("annual-enterprise-survey-2019-financial-year-provisional-csv.csv",500);
echo "$f\n";
echo ftell($f).PHP_EOL;
再次击掌...
Resource id #86
500
第二部分给出...
8192
>),H10,Indi<
因此它必须以8K块为单位缓冲。