52

探究PHP中的Mkdir函数

 5 years ago
source link: https://www.freebuf.com/vuls/198700.html?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

一、起因

在复现分析Wordpress-5.0.0 RCE 的时候,因为在写图片的过程中,根据图片的dirname创建目录,而后根据basename写入图片。在目录创建成功的前提下,应该是可以写入文件的。但是情况却不是如此,过程中我要在写目标图片前,必须还要再写一个辅助图片。其实这个辅助图片不是很重要,而重要的是这个辅助图片的目录创建。

此过程中例如需要写入目标文件为:

Z3qaaeF.jpg!web

首先需要先写一张

3QJj2mB.jpg!web

为什么会这样?假设直接写目标文件,过程中会首先创建目录:

eu2ueyY.jpg!web

其实这个过程是没有创建任何目录的,因为判断是directoryalready ,到下一步写入图片这里是 Imagick::writeImage ,在这里就会出问题。invaildfile path.报错。因为这里不存在  /var/www/html/wordpress/wp-content/uploads/2019/03/1.jpg? 这个目录,这涉及到系统调用,因系统的不同相对于的系统处理函数处理的方式也不同。

列如在kali 下  Imagick::writeImage 写入  ./1?/../1.png  ,  ./1?   这个目录是会报错的。具体系统调用如下:

IjU3iqb.jpg!web

首先判断了这个文件的状态,而后调用 openat   打开这个文件并不存在。 AT_FDCWD 表示打开的文件位置相对于当前目录。这是我在做的时候遇到的情况。

但是在  WORDPRESS IMAGE 远程代码执行漏洞分析 一文中,甚至其他另一篇。都没提到两次写图片。难道因为window和linux的不同吗?就这个问题我进行了一次对mkdir的探究。发现其实有很有趣。

二、PHP 内核 && 系统差异 之mkdir()

2.1 Linux &&PHP 7.3.2-3

mkdir ( ‘./1?/../1′ , 777 , true )

mkdir ( ‘./1 ?/../1′ , 777 false )

当第三参数为 $recursive true   时可以写目录,先说一下这个参数的含义 $recursive 用来循环创建目录。什么意思呢,当 false 时只能创建1级目录,即目录连接符最后的一个目录。而当 true 时是可以创建多级目录至到最后一个目录。列如 ./a/b/c 当abc都不存在时,会通过系统函数 mkdir 循环创建目录,abc都会被创建,但若为 false 会因为走到a处目录不存在,则不回去创建最后一个c。

但是第一个 mkdir 即使为 true 却也没有创建 1? 目录,这里我们从php内部 mkdir 执行情况和 系统  mkdir   执行情况来探究。

2.1.1PHP_FUNTCION(mkdir)

PHP 内调过程如下图:

ii2miyb.jpg!web

我们在出现分支的地方细分 
/php-src/main/streams/plain_wrapper.c

qYFzi2J.jpg!web

2.1.1.1 $recursive =fasle

其中出现的分支的地方在判断 $recursive   若是不需要循环创建则直接进入 php_mkdir

/php-src/ext/standard/file.c

BJ367nU.jpg!web

跟进 php_mkdir_ex

VFz6ruI.jpg!web

首先会检查 open_basedir , 接着会进入 VCWD_MKDIR , VCWD_MKDIR 是个宏命令,有三种不同定义:

i6JFzim.jpg!web

在这里我刚开始并没有考虑太多,跟着gdb的流程走,直接执行 mkdir() ,会直接调用系统的 _mkdir() . mkdir ( “./1?/../1″ , 01411 ) = - 1 ENOENT ( No such file or directory )

会直接报错。在预料之类,linux系统下mkdir是不允许这样创建目录的,会效验每一层目录的有效性。回到第一次出现分叉的时候。

2.1.1.2 $recursive =true

f2EZJ3Y.jpg!web

这里会进入 expand_filepath_with_mode ,

这里其实很熟悉,之前也是在看路径处理的时候看到过这个函数,它是一个展开函数,会通过递归的方式展开需要被创建的目录。在其过程会先把相对目录和当前脚本执行目录评价起来,若是绝对目录则忽略. 

其中我们的相对目录为 

./1?/../1 会变成  /var/www/html/WordPress/wp-content/themes/4/5/6/./1?/../1 当前我所在的目录为  /var/www/html/WordPress/wp-content/themes/4/5/6 然后通过递归的方式 去掉  ../ ./  , // . 并且对应目录前移,会变成  /var/www/html/WordPress/wp-content/themes/4/5/6/1 然后在传递给系统的mkdir函数。

在这个函数里面存在win32 和linux的不同分支,但在具体处理之前win32判断了目录名不能存在  *

2.6.png

注意一下此处! 

附上strace ,也是验证上诉分析过程

1. mkdir ( “/var/www/html/WordPress/wp-content/themes/4/5/6/1″ , 01411 ) = 0

2.1.2 Mkdir In Linux

在linux中单纯的mkdir是会层层验证目录,而后在创建一级目录。mkdir 也可以带参 -p,代表系统层面循环的创建目录。 
当执行mkdir -p 时 :

strace - f - e trace = mkdir  mkdir - ./ 1 ?/../ 1

mkdir ( “1?” , 0777 )   = 0

mkdir ( “1″ , 0777 )    = 0

我们能看到它并不像php内部那样,展开而后处理 。它会层层按照输入的目录创建。

2.2 window&& PHP 7.0.12

这里是我为什么要探究的一个重要问题点所在,在前面我提到的那篇文章中作者在window下实验当 $recursive false 才能创建成功,正好是反着的。作者的解释的 false 的时候不会去层层判断,但是真的是这样吗?

而后我也做了一个验证性的实验,在 window 上用  php 5.6 做了这个测试,但是结果让我疑惑了,无论在 false 还是  true 的情况都不会创建目录.而且报错也很有意思,在 false 的情况下报错 no error 但是就是无法创建。在 true 的情况下报错 invaild path。

难道是php-cli 问题?我又用cgi测了一遍,发现同样是这样。有意思,而后我通过邮件联系了那篇文章作者,询问其版本号。很快,得到了他的答复, php-7.0.12

于是下载php-7.0.12源码 重新编译加debug,此处省略1000字… 
在编译完成后我迫不及待的试了一下,同样如此和我的php5.6 一摸一样,无论在cli 模式 或者 cgi 模式下都是无法复现作者文中的情况。这到底问题出在哪呢?

先调了再说,VS调试php 网上基本上没有详细的介绍,有的都是Vscode。我不知道如何启动并调试,只好想了个attach的办法。在 mkdir 前面写上 sleep(10) , 但是这样做,其实是有一点鸡肋的,php内核初始化过程你其实抓不到的,但是用在这里够了,还是在 php_plain_files_mkdir 这个地方下断,刷新页面,attach到启动的php-cgi 上。

2.2.1PHP_FUNCTION(mkdir)

2.2.1.1 $recursive== false

还是先分析 false 的情况,前面都一样,不同的是在 php_mkdir_ex VCWD_MKDIR 调用的函数不一样

MbAFjma.jpg!web

这次走到不一样的调用上

vaqm2uv.jpg!web

跟进 virtual_mkdir

eQjQRfb.jpg!web

同样调用了 virtual_file_ex() , 前面有一点没提到,在 expand 展开路径的过程中最后其实也是进入的这个函数,前面说过在处理的过程中若是win32的情况会判断路径存不存在  * ? . 若是存在则会直接返回1,不会进入后面写路径。为什么那篇文章的作者会在false的情况下写成功呢?

2.2.1.1 $recursive== true

这里前面说过这里会进行expand过程,但是同样会判断路径名中存不存在 * ? , 会报错 Invaild Path。

2.2.2 mkdir inwindow

这里因为没有都没有执行到写目录。此处我们还无法探究window系统mkdir 函数是如何执行的。

三、程安全与非线程安全

重新梳理一下,现在是三种不一样的情况: 
linux /true 可写 
window/7.0.12 : 

1. false 可写 
2. true/false 都不可写

window 出现了两种情况。仔细在走一遍window/false的情况,现在我唯一没有考虑到是 VCWD_MKDIR   选择情况。前面都是跟着调试流程走的,这是唯一可能出现分叉的地方,重新看一下它的两种种宏定义:

YFJRVrb.jpg!web

若非那片文章作者,是走的第二个define,于是我把第一个define先注释掉了,换上了第二个define,再重新编译一边,结果竟然出现了和那篇作者一样的情况。但是这里有一个小小不同,写入的目录是相对于php-cgi.exe解释器的,不是相对于WWW的网站根目录下的,当你看了下面的分析以后,应该会给你一个答案,那么很显然问题现在出现在  VIRTUAL_DIR   定义的情况,在它没有定义的情况下,才会走到第二个define,我看看 VIRTUAL_DIR  

是在哪被定义的

/php-src/Zend/zend_virtual_cwd.h

mQnyaeU.jpg!web

熟悉php内核的朋友不会陌生ZTS,这是php 线程安全的标志。用来应对那些使用线程来处理并发请求的Web服务器,列如window下的IIS,worker_mpm模式下的apahce,生活在线程里面的php需要考虑线程间的读写同时也要保证线程间是安全,所以php需要自己提供ZTS层来管理线程间的操作。当定义了ZTS时候,就也同时定义了虚拟目录(VIRTUAL_DIR)。 为什么会存在虚拟目录这一说法呢,其实很简单你通过对应 virtual_file_ex() 可以看出来,这个函数的目的在于针对相对路径替换出完整的绝对路径。举很简单的例子,php脚本中写的相对路径,其相对路径一定是针对于该脚本的。在执行脚本的过程中,会进入相应的php 内核里面的 php_execute_script() , 其中有一步是 VCWD_CHDIR_FILE(filename) , 这是用来根据要执行的脚本位置去切换当前目录,同样这个宏定义有两个不同的函数,一个是在虚拟目录下切换目录,一个是非线程安全环境下单线程切换目录,不同是在线程安全下切换目录,并不是直接调用系统的 _chdir() , 而是将执行脚本的目录存储在 TSRMG 中,并给定一个 cwd_globals_id ,要用的时候再去取,比如创建目录,写文件。因为在 多线程 环境不能直接修改当前 进程 的目录,只能预定义一个变量保存各线程的当前目录。 可以看到在线程安全的模式下,若是给的相对路径,都会出现当前目录和相对目录的拼接。且都在win32的环境都会检测目录是否包含 *  , ? .

四、结论汇总

我有注意到那篇的文章作者是在window 上用的phpstudy,我也去看了一下phpstudy的是否有7.0.12的版本,存在一个  php-7.0.12-nts+Apache   确实也是非线程安全。也印证上面我修改php7.0.12 重新编译的结果,但是一个很有趣的东西是,window的系统调用API  _mkdir()   是存在和php内部一样的路径展开功能,即他是允许这样写的 ./1?/../1   可以在当前目录下写入文件夹 1 的,这和linux不一样,linux的系统函数是逐层判断。在php7.1之后,改变了系统创建目录的API,从 _mkdir   变成了 CreateDirectoryW ,但是不变的是还是可以存在路径展开的功能。即便你这样写: @@#@$@#$^%$&&**/@!#@!$!%/../../evil 也是可以创建目录 evil 的,可以算是一个小技巧。 但是条件是在 windowphp非线程安全 模式和 PHP_FUNCTION(mkdir) 第三个参数为 false 的情况下是可以这样写目录的。可以算是一个小tips吧。结合相应的应用特点,是可以用到的,而且php版本一般都是非线程安全的,在nginx下都是多进程处理php,即非线程安全。apache只有在worker_mpm才是多线程的,一般也不常用。一般都是prefork_mpm + php_mod,即便是fastcgi也是多进程。利用环境还是比较常见的。

73EBVfa.jpg!web*本文作者:alphalab,转载请注明来自FreeBuf.COM


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK