0X01 起因
在复现分析Wordpress-5.0.0 RCE 的时候,因为在写图片的过程中,根据图片的dirname创建目录,而后根据basename写入图片。在目录创建成功的前提下,应该是可以写入文件的。但是情况却不是如此,过程中我要在写目标图片前,必须还要再写一个辅助图片。其实这个辅助图片不是很重要,而重要的是这个辅助图片的目录创建。
过程中列如需要写入目标文件为 1
/var/www/html/wordpress/wp-content/uploads/2019/03/1.jpg?/../../../../themes/twentynineteen/1.jpg
1
/var/www/html/wordpress/wp-content/uploads/2019/03/1.jpg?/1.jpg
1
@mkdir('/var/www/html/wordpress/wp-content/uploads/2019/03/1.jpg?/../../../../themes/twentynineteen',777,true);
Imagick::writeImage
。这里就会出问题,invaild file path.报错。因为这里不存在 /var/www/html/wordpress/wp-content/uploads/2019/03/1.jpg?
这个目录,这涉及到系统调用,因系统的不同相对于的系统处理函数处理的方式也不同。
列如在kali 下 Imagick::writeImage
写入 ./1?/../1.png
, ./1?
这个目录是会报错的。具体系统调用如下 1
2[pid 10285] stat("./1?/../1.png", 0x561a1fe53a48) = -1 ENOENT (No such file or directory)
[pid 10285] openat(AT_FDCWD, "./1?/../1.png", O_RDWR|O_CREAT|O_TRUNC, 0666) = -1 ENOENT (No such file or directory)openat
打开这个文件并不存在。AT_FDCWD
表示打开的文件位置相对于当前目录。这是我在做的时候遇到的情况。(Linux)
但是在文章 WORDPRESS IMAGE 远程代码执行漏洞分析
一文中,甚至其他另一篇。都没提到两次写图片。难道因为window和linux的不同吗?就这个问题我进行了一次对mkdir的探究。发现其实有很有趣。
0x02 PHP源码 && 系统区别 之mkdir()
2.1 Linux && PHP 7.3.2-3
1 | mkdir('./1?/../1',777,true); |
当第三参数为$recursive
为true
时可以写目录,先说一下这个参数的含义$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-src/main/streams/plain_wrapper.c 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22static int php_plain_files_mkdir(php_stream_wrapper *wrapper, const char *dir, int mode, int options, php_stream_context *context)
{
int ret, recursive = options & PHP_STREAM_MKDIR_RECURSIVE;
char *p;
if (strncasecmp(dir, "file://", sizeof("file://") - 1) == 0) {
dir += sizeof("file://") - 1;
}
if (!recursive) {
ret = php_mkdir(dir, mode);
} else {
/* we look for directory separator from the end of string, thus hopefuly reducing our work load */
char *e;
zend_stat_t sb;
size_t dir_len = strlen(dir), offset = 0;
char buf[MAXPATHLEN];
if (!expand_filepath_with_mode(dir, buf, NULL, 0, CWD_EXPAND )) {
php_error_docref(NULL, E_WARNING, "Invalid path");
return 0;
}若是不需要循环创建则直接进入
php_mkdir`
/php-src/ext/standard/file.c 1
2
3
4PHPAPI int php_mkdir(const char *dir, zend_long mode)
{
return php_mkdir_ex(dir, mode, REPORT_ERRORS);
}php_mkdir_ex
1 | PHPAPI int php_mkdir_ex(const char *dir, zend_long mode, int options) |
首先会检查open_basedir
,接着会进入VCWD_MKDIR
,VCWD_MKDIR
是个宏命令,有三种不同定义: 1
2
3mkdir()
,会直接调用系统的_mkdir()
.
1 | mkdir("./1?/../1", 01411) = -1 ENOENT (No such file or directory) |
会直接报错。在预料之类,linux系统下mkdir是不允许这样创建目录的,会效验每一层目录的有效性。回到第一次出现分叉的时候
2.1.1.2 $recursive = true
1 | else { |
这里会进入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判断了目录名不能存在 *
,?
1
2
3
4
5
6
if (memchr(resolved_path, '*', path_length) ||
memchr(resolved_path, '?', path_length)) {
return 1;
}1
mkdir("/var/www/html/WordPress/wp-content/themes/4/5/6/1", 01411) = 0
1
2
3strace -f -e trace=mkdir mkdir -p ./1?/../1
mkdir("1?", 0777) = 0
mkdir("1", 0777) = 0
2.2 window && PHP 7.0.12
这里是我为什么要探究的一个重要问题点所在,在前面我提到的那篇文章中作者在window下实验当$recursive
为false
才能创建成功,正好是反着的。作者的解释的false
的时候不会去层层判断,但是真的是这样吗?
而后我也做了一个验证性的实验,在window 上用 php 5.6做了这个测试,但是结果让我疑惑了,无论在false
还是 true
的情况都不会创建目录.而且报错也很有意思,在false
的情况下报错 no error 但是就是无法创建。在true
的情况下报错 invaild argument。
难道是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_plain_files_mkdir
这个地方下断,刷新页面,attach到启动的php-cgi 上。
2.2.1 PHP_FUNCTION(mkdir)
2.2.1.1 $recursive == false
还是先分析false
的情况,前面都一样,不同的是在php_mkdir_ex
中VCWD_MKDIR
调用的函数不一样
1 | ret = VCWD_MKDIR(dir, (mode_t)mode) |
这次走到不一样的调用上 1
virtual_mkdir
1 | CWD_API int virtual_mkdir(const char *pathname, mode_t mode) |
同样调用了virtual_file_ex()
,前面有一点没提到,在expand展开路径的过程中最后其实也是进入的这个函数,前面说过在处理的过程中若是win32的情况会判断路径存不存在 *
, ?
.若是存在则会直接返回1,不会进入后面写路径。为什么那篇文章的作者会在false的情况下写成功呢?
2.2.1.1 $recursive == true
这里前面说过这里会进行expand过程,但是同样会判断路径名中存不存在*
, ?
,会报错 Invaild Path。
2.2.2 mkdir in window
这里因为没有都没有执行到写目录。此处我们还无法探究window系统mkdir 函数是如何执行的。
0x03 线程安全与非线程安全
重新梳理一下,现在是三种不一样的情况: linux /true 可写 window/7.0.12 : 1. false 可写 2. true/false 都不可写
window 出现了两种情况。仔细在走一遍window/false的情况,现在我唯一没有考虑到是VCWD_MKDIR
选择情况。前面都是跟着调试流程走的,这是唯一可能出现分叉的地方,重新看一下它的两种种宏定义: 1
2
3
4
5
VIRTUAL_DIR
定义的情况,在它没有定义的情况下,才会走到第二个define,我看看VIRTUAL_DIR
是在哪被定义的
/php-src/Zend/zend_virtual_cwd.h 1
2
3virtual_file_ex()
可以看出来,这个函数的目的在于针对相对路径替换出完整的绝对路径。举很简单的例子,php脚本中写的相对路径,其相对路径一定是针对于该脚本的。在执行脚本的过程中,会进入相应的php 内核里面的php_execute_script()
,其中有一步是VCWD_CHDIR_FILE(filename)
,这是用来根据要执行的脚本位置去切换当前目录,同样这个宏定义有两个不同的函数,一个是在虚拟目录下切换目录,一个是非线程安全环境下单线程切换目录,不同是在线程安全下切换目录,并不是直接调用系统的_chdir()
,而是将执行脚本的目录存储在TSRMG
中,并给定一个cwd_globals_id
,要用的时候再去取,比如创建目录,写文件。因为在多线程环境不能直接修改当前进程的目录,只能预定义一个变量保存各线程的当前目录。
可以看到在线程安全的模式下,若是给的相对路径,都会出现当前目录和相对目录的拼接。且都在win32的环境都会检测目录是否包含*
,?
.
0x04 结论汇总
我有主意到那篇的文章作者是在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,即多进程。利用环境还是比较常见的。
努力在努力,永远不说放弃,终会如愿以偿! --maple