mkdir函数php,从php源码分析mkdir()函数

阅读:

1,011

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

0x01 缘起

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

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

9da04828649ddb5ffcf4aa76b80c0a98.png

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

af928993d62ae3b12de3024dfc2b3ba4.png

可以看到只有在非线程安全下并且recursive=false时才成功创建,总结如下表所示Windowsthread-safenon-thread saferecursive=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()这个方法

7ecad1a8325b6a174e2afbf86d0d9bfe.png

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

2e5c41382e2347cd9d9947a9ab6aa7c4.png

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

a9ec63cc9d88c0874d2822842fb88545.png

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

回到上面,virtual_file_ex()没有通过验证,最终抛出的异常是"Invalid path"if (!expand_filepath_with_mode(dir, buf, NULL, 0, CWD_EXPAND )) {

php_error_docref(NULL, E_WARNING, "Invalid path");

return 0;

}

non-thread safe

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

2. recursive=false

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

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

e87fa772cfc9215d682154c22cae5088.png

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

dd82549583bbfce28bf04a129a809769.png

注意这三个定义是根据不同的条件执行的,看一下逻辑#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(),分别进入两个方法

0b16a96b73541d9d348ee783d1496c93.png

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

3101304d279c38277165e85f2b10182d.png

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

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

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

8240d85a752e1d1e65ea63af95dc3f14.png

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

0x04 流程图

d65109c683d45377a134d50b0de734d7.png

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的过程:#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,初始化时的调用栈如下图所示

32307afcf99d7e6844a17a4e295c9a83.png

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

0x06 总结

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

参考:

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值