一、起因

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

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

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

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

例如在kali 下 Imagick::writeImage 写入  ./1?/../1.png./1? 这个目录是会报错的。具体系统调用如下 首先判断了这个文件的状态,而后调用 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);

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

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

2.1.1 PHP_FUNTCION(mkdir)

PHP内调过程如下图:

我们在出现分支的地方细分

/php-src/main/streams/plain_wrapper.c

2.1.1.1 $recursive = fasle

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

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

跟进 php_mkdir_ex

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

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

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

2.1.1.2 $recursive = true

这里会进入 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判断了目录名不能存在  *

注意一下此处!

附上strace ,也是验证上诉分析过程: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 时 :

  1. strace -f -e trace=mkdir  mkdir -p  ./1?/../1
  2. mkdir(“1?”, 0777)  = 0
  3. mkdir(“1″, 0777)   = 0

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

2.2 window && PHP 7.0.12

这里是我为什么要探究的一个重要问题点所在,在前面我提到的那篇文章中作者在window下实验当 $recursivefalse 才能创建成功,正好是反着的。作者的解释的 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.1 PHP_FUNCTION(mkdir)

2.2.1.1 $recursive == false

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

这次走到不一样的调用上

跟进 virtual_mkdir

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

2.2.1.1 $recursive == true

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

2.2.2 mkdir in window

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

三、 线程安全与非线程安全

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

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

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

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

熟悉 php 内核的朋友不会陌生 ZTS ,这是 php 线程安全的标志。用来应对那些使用线程来处理并发请求的 Web 服务器,列如 window 下的 IISworker_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   确实也是非线程安全。也印证上面我修改 php 7.0.12 重新编译的结果,但是一个很有趣的东西是 ,window 的系统调用 API  _mkdir()   是存在和 php 内部一样的路径展开功能,即他是允许这样写的 ./1?/../1   可以在当前目录下写入文件夹 1 的,这和 linux 不一样, linux 的系统函数是逐层判断。在 php7.1 之后,改变了系统创建目录的 API ,从 _mkdir   变成了 CreateDirectoryW ,但是不变的是还是可以存在路径展开的功能。即便你这样写: @@#@$@#$^%$&&**/@!#@!$!%/../../evil 也是可以创建目录 evil 的,可以算是一个小技巧。

但是条件是在 window php 非线程安全 模式和 PHP_FUNCTION(mkdir) 第三个参数为 false 的情况下是可以这样写目录的。可以算是一个小 tips 吧。结合相应的应用特点,是可以用到的,而且 php 版本一般都是非线程安全的,在 nginx 下都是多进程处理 php ,即非线程安全。 apache 只有在 worker_mpm 才是多线程的,一般也不常用。一般都是 prefork_mpm + php_mod ,即便是 fastcgi 也是多进程。利用环境还是比较常见的。

相关文章