墨魇博客 墨魇博客
  • 注册
  • 登录
  • 首页
  • 分类
    • Linux
    • Python
    • WEB前端
    • PHP
    • SEO
    • Mysql
  • 资源
    • 源码
    • 素材
  • 碎碎念
  • 音乐
  • 专题
  • 留言板
首页 › PHP › 从php源码分析mkdir()函数

从php源码分析mkdir()函数

墨魇
6月 28, 2019发布在 PHP
1,736 0 1

本文通过php中mkdir()函数在不同环境下表现结果不一致的现象,分析了php内核对mkdir()函数的实现,引申出php中线程安全与非线程安全两个重要的机制,抛砖引玉,如有表述不妥或者错误之处欢迎指正。

0x01 缘起

在前阵子分析 WORDPRESS IMAGE 远程代码执行漏洞的过程中,在文末提到一点关于php中的mkdir()函数,在触发漏洞时这个地方存在一点疑惑,即当mkdir()第三个参数分别为false和true时,分别是能成功创建文件夹和创建失败,后来有同学发现和他的测试结果有偏差,两种情况都无法创建,在互相确认了php版本后,对mkdir()函数进行了深入的研究,发现里面大有文章。

当时的测试结果是这样的,环境是Windows+php-7.0.12-nts,在recursive=false时成功穿越目录并创建了文件夹

从php源码分析mkdir()函数-墨魇博客

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

从php源码分析mkdir()函数-墨魇博客

可以看到只有在非线程安全下并且recursive=false时才成功创建,总结如下表所示

Windowsthread-safenon-thread safe
recursive=falsefail (No error)success
recursive=truefail (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()这个方法

从php源码分析mkdir()函数-墨魇博客

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

从php源码分析mkdir()函数-墨魇博客

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

从php源码分析mkdir()函数-墨魇博客

如果是Windows系统且路径中包含了*或?,则直接返回错误,这也就是为什么在复现wordpress漏洞时构造的PoC中含有?无法创建目录的原因 (wordpress指定了recursive=true),当时使用#绕过了这个限制

回到上面,virtual_file_ex()没有通过验证,最终抛出的异常是"Invalid path"

1
2
3
4
5
if(!expand_filepath_with_mode(dir,buf,NULL,0,CWD_EXPAND)){
   php_error_docref(NULL,E_WARNING,“Invalid path”);
   return0;
}
non-thread safe

在非线程安全模式下,流程是完全一样的,最终也会因为无法通过*或?的检查,抛出"Invalid path"

2. recursive=false

接下来看一下recursive=false的情况,在这个情况下,线程安全与非线程安全产生了不一样的结果。

recursive=false时进入php_mkdir()方法,随后进入php_mkdir_ex()

从php源码分析mkdir()函数-墨魇博客

在进行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)

从php源码分析mkdir()函数-墨魇博客

注意这三个定义是根据不同的条件执行的,看一下逻辑

1
2
3
4
5
6
7
8
9
#ifdef VIRTUAL_DIR
#define VCWD_MKDIR(pathname, mode) virtual_mkdir(pathname, mode)
#endif
#if defined(ZEND_WIN32)
#define VCWD_MKDIR(pathname, mode) php_win32_ioutil_mkdir(pathname, mode)
#else
#define VCWD_MKDIR(pathname, mode) mkdir(pathname, mode)

也就是说,如果定义了VIRTUAL_DIR,那么执行的是virtual_mkdir(),否则如果是Windows系统,就执行php_win32_ioutil_mkdir()创建目录,linux下则是mkdir命令

那么,既然在recursive=false的情况下,线程安全与非线程安全出现了不一样的结果,肯定是此处走的分支不一样,一个使用了virtual_mkdir(),另一个使用了php_win32_ioutil_mkdir(),分别进入两个方法

从php源码分析mkdir()函数-墨魇博客

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

从php源码分析mkdir()函数-墨魇博客

CreateDirectoryW是Windows下创建目录的API,走到这个分支并不检查*和?,因此能够成功创建目录。

有关CreateDirectoryW参考 Microsoft Doc

与CreateDirectoryW对应的还有CreateDirectoryA,两个函数功能一样,只是第一个参数的类型不同,一个是LPCWSTR 另一个是LPCSTR ,这两者是CHAR 和WCHAR的区别 ,详细可以参考 StackOverflow

现在剩下最关键的一个问题,什么情况下会走virtual_mkdir()的流程,也就是说VIRTUAL_DIR是在何处定义的?

从php源码分析mkdir()函数-墨魇博客

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

0x04 流程图

从php源码分析mkdir()函数-墨魇博客

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的过程:

1
2
3
4
5
#define TSRMG(id, type, element)(TSRMG_BULK(id, type)->element)
#define TSRMG_BULK(id, type)((type) (*((void ***) tsrm_get_ls_cache()))[TSRM_UNSHUFFLE_RSRC_ID(id)])
# define tsrm_tls_get()pthread_getspecific(tls_key)

在启动cli或者cgi时,都会通过SAPI调用tsrm_startup()启动TSRM ,随后进行模块初始化,在这个过程中分配资源 id,初始化时的调用栈如下图所示

从php源码分析mkdir()函数-墨魇博客

当非 ZTS 模式时,线程直接调用全局变量的属性, 而 ZTS 模式设置 “虚拟目录” 的概念其实就是 “根据资源 id 查找所需的全局变量” 的过程,本质上是为了避免线程间资源读取出现竞争,保证了线程安全。

0x06 总结

本文通过php中mkdir()函数在不同环境下表现结果不一致的现象,分析了php内核对mkdir()函数的实现,引申出php中线程安全与非线程安全两个重要的机制,抛砖引玉,如有表述不妥或者错误之处欢迎指正,最后感谢 @maple 提出最初的问题以及探讨过程中给予的莫大的帮助。

#PHP#
1
没有到不了的明天
上一篇
活着
下一篇
评论 (0)
再想想
贴标签
HTMLJavaScriptjsLinuxMariaDBMySQLPHPPHP数组PythonSEOSQLswooleswoole +thinkPHP5thinkPHP5墨魇墨魇SEO墨魇个人博客心情记程序员随笔
1
相关文章
架构设计之「 CAP 定理 」
swoole +thinkPHP5.x 接入方法
PHP基础之什么是Phar?
PHP7扩展开发之数组处理

他那时还太年轻,不懂所有命运馈赠的礼物早已在暗中标好了价格

HomePHPSQLPythonLinuxSEOHTML碎碎念音乐留言小伙伴
钟华博客 松鼠乐园
Copyright © 2016-2021 墨魇博客. Designed by 墨魇. 黔ICP备17010868号
未登录
现在登录 / 注册,享受更多福利
  • Home
  • PHP
  • SQL
  • Python
  • Linux
  • SEO
  • HTML
  • 碎碎念
  • 音乐
  • 留言
  • 小伙伴
热门搜索
  • 随笔
  • 心情记
  • 墨魇个人博客
  • Python
  • PHP
  • 墨魇
  • 程序员
  • Linux
  • MySQL
  • HTML
  • SEO
  • 墨魇SEO
  • SQL
  • js
  • swoole
  • thinkPHP5
  • swoole +thinkPHP5
  • PHP数组
墨魇
作为一个出色的精神病患者,我的理想是至少要杀死一个奥特曼
114 文章
33 评论
163 喜欢
  • 1
  • 0
  • Top