从php源码分析mkdir()函数
本文通过php中mkdir()函数在不同环境下表现结果不一致的现象,分析了php内核对mkdir()函数的实现,引申出php中线程安全与非线程安全两个重要的机制,抛砖引玉,如有表述不妥或者错误之处欢迎指正。
0x01 缘起
在前阵子分析 WORDPRESS IMAGE 远程代码执行漏洞的过程中,在文末提到一点关于php中的mkdir()函数,在触发漏洞时这个地方存在一点疑惑,即当mkdir()第三个参数分别为false和true时,分别是能成功创建文件夹和创建失败,后来有同学发现和他的测试结果有偏差,两种情况都无法创建,在互相确认了php版本后,对mkdir()函数进行了深入的研究,发现里面大有文章。
当时的测试结果是这样的,环境是Windows+php-7.0.12-nts,在recursive=false时成功穿越目录并创建了文件夹

本文重新编译了 php 方便调试,版本是php-7.2.16-ts和php-7.2.16-nts,测试结果如下

可以看到只有在非线程安全下并且recursive=false时才成功创建,总结如下表所示
| Windows | thread-safe | non-thread safe |
|---|---|---|
| recursive=false | fail (No error) | success |
| recursive=true | fail (Invalid path) | fail (Invalid path) |
接下来从源码角度看看php如何实现mkdir()函数,探究一下为何会出现差异
0x02 调试
用Visual Studio 2017打开项目,定位到php-7.2.16-src/main/streams/plain_wrapper.cline 1234,方法php_plain_files_mkdir()即mkdir()的实现,在此处下个断点,然后运行脚本,接着选择调试-附加到进程,选择编译好的php.exe进程,成功命中断点。
0x03 源码分析
1. recursive=true
thread-safe
首先分析在recursive=true的情况,跟随断点来看一下php_plain_files_mkdir()这个方法

看到对recursive进行了判断,进了不同的分支,分别执行php_mkdir()和expand_filepath_with_mode()。recursive=true时进入expand_filepath_with_mode()

这个expand_filepath_with_mode()方法会判断当前路径是相对路径还是绝对路径,然后把路径传入virtual_file_ex(),如果是相对路径的话会在该方法中拼接成完整的路径,随后进行一个重要的判断

如果是Windows系统且路径中包含了*或?,则直接返回错误,这也就是为什么在复现wordpress漏洞时构造的PoC中含有?无法创建目录的原因 (wordpress指定了recursive=true),当时使用#绕过了这个限制
回到上面,virtual_file_ex()没有通过验证,最终抛出的异常是"Invalid path"
non-thread safe
在非线程安全模式下,流程是完全一样的,最终也会因为无法通过*或?的检查,抛出"Invalid path"
2. recursive=false
接下来看一下recursive=false的情况,在这个情况下,线程安全与非线程安全产生了不一样的结果。
recursive=false时进入php_mkdir()方法,随后进入php_mkdir_ex()

在进行basedir检查后进入VCWD_MKDIR,这是一个宏命令,在源码中有三处定义,在php-7.2.16-src/Zend/zend_virtual_cwd.h中,分别是
mkdir(pathname, mode)php_win32_ioutil_mkdir(pathname, mode)virtual_mkdir(pathname, mode)

注意这三个定义是根据不同的条件执行的,看一下逻辑
也就是说,如果定义了VIRTUAL_DIR,那么执行的是virtual_mkdir(),否则如果是Windows系统,就执行php_win32_ioutil_mkdir()创建目录,linux下则是mkdir命令
那么,既然在recursive=false的情况下,线程安全与非线程安全出现了不一样的结果,肯定是此处走的分支不一样,一个使用了virtual_mkdir(),另一个使用了php_win32_ioutil_mkdir(),分别进入两个方法

在virtual_mkdir()中,同上面的情况一样,进行了virtual_file_ex()判断,因此也会走到对*和?的判断,同样因为通不过检查而抛出"Invalid path",而在php_win32_ioutil_mkdir()中则是调用了CreateDirectoryW创建目录

CreateDirectoryW是Windows下创建目录的API,走到这个分支并不检查*和?,因此能够成功创建目录。
有关
CreateDirectoryW参考 Microsoft Doc与
CreateDirectoryW对应的还有CreateDirectoryA,两个函数功能一样,只是第一个参数的类型不同,一个是LPCWSTR另一个是LPCSTR,这两者是CHAR和WCHAR的区别 ,详细可以参考 StackOverflow
现在剩下最关键的一个问题,什么情况下会走virtual_mkdir()的流程,也就是说VIRTUAL_DIR是在何处定义的?

在php-7.2.16-src/Zend?zend_virtual_cwd.hline 41 定义了这个变量,前置条件是ZTS,也就是线程安全的标识,只有在线程安全模式下,才使用virtual_mkdir()创建目录,调用的系统函数同样是CreateDirectoryW,但是在此之前得先通过virtual_file_ex()校验,含有*和?则无法创建成功。
0x04 流程图

0x05 深入
现在清楚了php对mkdir()的实现,之所以结果不一样是因为ZTS与NTS下的两种不同的处理流程,那么为什么在 TS 模式下,在调用Windows的API创建目录之前,需要设置一个 “虚拟目录” 呢?
这里涉及到php内核中的TSRM机制,也就是线程安全资源管理器(Thread Safe Resource Manager) ,这个机制的引入是为了解决线程并发的问题,我们知道,如果线程访问的内存地址空间相同,当一个线程修改资源时会影响其它线程,所以为了确保不会出现资源竞争,php将多个资源复制为多份,每个线程需要的资源在当前进程空间中各有一份,各取所取,这样就不会出现竞争问题。
那么不同线程怎么获取自身所需要的资源呢?php中通过ts_allocate_id()函数实现, 这个函数的作用就是遍历所有线程,为每一个分配一个线程安全资源id,每一次调用ts_allocate_id()函数时,都会执行这个操作,而为了避免重复分配,这个过程是在调用模块初始化的时候就完成了
TSRMG的定义如下,其中tsrm_get_ls_cache()有多个定义,但功能是一样的,就是根据资源 id 的tls_key取出相应value的过程:
在启动cli或者cgi时,都会通过SAPI调用tsrm_startup()启动TSRM ,随后进行模块初始化,在这个过程中分配资源 id,初始化时的调用栈如下图所示

当非 ZTS 模式时,线程直接调用全局变量的属性, 而 ZTS 模式设置 “虚拟目录” 的概念其实就是 “根据资源 id 查找所需的全局变量” 的过程,本质上是为了避免线程间资源读取出现竞争,保证了线程安全。
0x06 总结
本文通过php中mkdir()函数在不同环境下表现结果不一致的现象,分析了php内核对mkdir()函数的实现,引申出php中线程安全与非线程安全两个重要的机制,抛砖引玉,如有表述不妥或者错误之处欢迎指正,最后感谢 @maple 提出最初的问题以及探讨过程中给予的莫大的帮助。
参考:
http://php.net/manual/en/function.mkdir.php
https://segmentfault.com/a/1190000010004035
建议继续学习:
- Nginx源码分析-事件循环 (阅读:5692)
- Hive的入口 -- Hive源码解析 (阅读:5486)
- Storm源码浅析之topology的提交 (阅读:5363)
- Hive源码解析-之-语法解析器 (阅读:5098)
- Nginx源码分析-内存池 (阅读:4894)
- Nginx源码分析-Epoll模块 (阅读:4631)
- Lua GC 的源码剖析 (2) (阅读:4604)
- Lua GC 的源码剖析 (4) (阅读:4208)
- ExtJS源码研究笔记之总评 (阅读:3915)
- Lua GC 的源码剖析 (1) (阅读:3826)
扫一扫订阅我的微信号:IT技术博客大学习
- 作者:kylinking 来源: 绿盟科技博客
- 标签: mkdir 源码
- 发布时间:2019-03-25 23:34:37
-
[928] WordPress插件开发 -- 在插件使用 -
[134] 解决 nginx 反向代理网页首尾出现神秘字 -
[53] 整理了一份招PHP高级工程师的面试题 -
[52] 如何保证一个程序在单台服务器上只有唯一实例( -
[51] 海量小文件存储 -
[51] 用 Jquery 模拟 select -
[50] 全站换域名时利用nginx和javascri -
[50] Innodb分表太多或者表分区太多,会导致内 -
[49] CloudSMS:免费匿名的云短信 -
[47] jQuery性能优化指南
