escapeshellcmd描述
escapeshellcmd归于程序执行类函数,对字符串中可能会欺骗 shell 命令执行任意命令的字符进行转义。
escapeshellcmd(string $command): string
非Windows使用反斜杠\ 转义以下字符:&#;|*?~<>^()[]{}$\ 、\x0A 、\xFF 和反引号`,其中单双引号只在不成对、独立时转义
Windows使用脱字符^ 也转义上述字符再加上%! ,对于单双引号均转义
勘误
PHP Manual中文版本将Windows处理的脱字符翻译为空格
静态调试
PHP_FUNCTION(escapeshellcmd)
PHP函数定义为PHP_FUNCTION(foo) ,在vscode搜索PHP_FUNCTION(escapeshellcmd) 定位到了标准函数库的exec.c,这也反映了其属于程序执行类函数。来到478行: 首先是接收传参:通过宏ZEND_PARSE_PARAMETERS_START(1, 1) 接收一个参数command,并通过Z_PARAM_STRING(command, command_len) 存储接收的字符串和其长度
char *command;
size_t command_len;
ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_STRING(command, command_len)
ZEND_PARSE_PARAMETERS_END();
接着就是if判断传入字符串的有效性:非空判断、非法输入NULL,然后就进入php_escape_shell_cmd(command) 实现功能,传入字符串
if (command_len) {
if (command_len != strlen(command)) {
php_error_docref(NULL, E_ERROR, "Input string contains NULL bytes");
return;
}
RETVAL_STR(php_escape_shell_cmd(command));
} else {
RETVAL_EMPTY_STRING();
}
最后通过RETVAL 返回值
PHPAPI zend_string *php_escape_shell_cmd(char *str)
有效性判断后就进入此PHPAPI进一步实现功能,参数command被传入,来到286行: 注释里提供了此函数的功能介绍:
转义所有可能被用于逃逸shell的字符 这个函数对一个字符串进行了emalloc,并返回指针 用完后记得将其释放 对二进制字符串不安全
首先是一些变量的声明,函数对不同平台有不同的处理方式:此处非Windows多定义了一个指针,下文会提到它的用处。其中estimate是假定全部字符都需要转义再加一个结束符的长度
PHPAPI zend_string *php_escape_shell_cmd(char *str)
{
register size_t x, y;
size_t l = strlen(str);
uint64_t estimate = (2 * (uint64_t)l) + 1;
zend_string *cmd;
#ifndef PHP_WIN32
char *p = NULL;
#endif
然后是判断传入command长度是否超出最大范围,最大长度减去字符串自身一对单引号和结束符;如果长度非法抛错,并返回一个空zend_string(的指针)
if (l > cmd_max_len - 2 - 1) {
php_error_docref(NULL, E_ERROR, "Command exceeds the allowed length of %zu bytes", cmd_max_len);
return ZSTR_EMPTY_ALLOC();
}
为cmd分配内存,类型为zend_string ;它是返回值,用于存储处理好的字符串
cmd = zend_string_safe_alloc(2, l, 0, 0);
处理完了就进入遍历、转义部分:首先跳过传入cmd中的无效多字节字符,也就是说escapeshellcmd是忽略多字节字符处理(如:中文),然后再进行逐字符转义
跳过多字节字符
这里选用两个变量x和y,其中:x是索引当前字符在传入字符串str的下标、y是cmd字符串的末尾下标。当遇到多字节字符时mb_len>1,然后函数就会将该字符通过memcpy的方式全部复制到cmd中,然后x和y向后移动,读取下一个字符,x、y增加该多字节字符的长度。如果不是多字节字符mb_len<0,进入下面的转义判断处理
细节见动态调试分析
for (x = 0, y = 0; x < l; x++) {
int mb_len = php_mblen(str + x, (l - x));
if (mb_len < 0) {
continue;
} else if (mb_len > 1) {
memcpy(ZSTR_VAL(cmd) + y, str + x, mb_len);
y += mb_len;
x += mb_len - 1;
continue;
}
PHP历史漏洞:宽字节注入
PHP5.2.6之前未对多字节字符的处理,会导致GBK宽字节注入,因此添加了此部分以修复漏洞
对有效字符转义
当字符不是多字节字符时进入switch判断转义,这里有分系统的处理。
- 第一步:
#ifndef 是对非Windows平台(可认为就是Linux)有效的函数。这一步是处理Linux平台的单双引号成对性判断的部分:仅转义不成对的引号,之前的p指针就是为此而声明的。
memchr原型是void *memchr(const void *str, int c, size_t n) ,在字符串str的前n个字节中搜索第一次出现字符 c(一个无符号字符)的位置,该函数返回一个指向匹配字节的指针,如果在给定的内存区域未出现字符,则返回 NULL。
第一次匹配进入noop,第二次若匹配到了进入NULL也就是判定为成对、p置空;若未匹配到进入ZSTR_VAL(cmd)[y++] = '\\' 也就是判定为不成对,此时先向cmd写入转义符再写入引号。判断是否成对是从后往前判定。
switch (str[x]) {
#ifndef PHP_WIN32
case '"':
case '\'':
if (!p && (p = memchr(str + x + 1, str[x], l - x - 1))) {
} else if (p && *p == str[x]) {
p = NULL;
} else {
ZSTR_VAL(cmd)[y++] = '\\';
}
ZSTR_VAL(cmd)[y++] = str[x];
break;
对Windows平台特别转义%! ,注意看注释
%是环境变量的标识符,windows会解析^%PATH%,而不解析^%PATH^%
#else
case '%':
case '!':
case '"':
case '\'':
#endif
- 第二步:
声明了Windows和Linux共用的转义符号表
case '#':
case '&':
case ';':
case '`':
case '|':
case '*':
case '?':
case '~':
case '<':
case '>':
case '^':
case '(':
case ')':
case '[':
case ']':
case '{':
case '}':
case '$':
case '\\':
case '\x0A':
case '\xFF':
- 第三步:
分系统转义:如果需要转义,首先先写入转义符号,Windows使用脱字符,Linux使用反斜杠;然后再进入default写入字符。
简而言之就是:如果字符在转义表中:分平台向cmd写入转义符再写入字符,否则只写入字符
最后在cmd末尾写入结束符
#ifdef PHP_WIN32
ZSTR_VAL(cmd)[y++] = '^';
#else
ZSTR_VAL(cmd)[y++] = '\\';
#endif
default:
ZSTR_VAL(cmd)[y++] = str[x];
}
}
ZSTR_VAL(cmd)[y] = '\0';
检查结果有效性并返回
最后检查转义结果cmd是否有效:判断长度是否有效和分配空间是否溢出,然后登记cmd的长度。
if (y > cmd_max_len + 1) {
php_error_docref(NULL, E_ERROR, "Escaped command exceeds the allowed length of %zu bytes", cmd_max_len);
zend_string_release(cmd);
return ZSTR_EMPTY_ALLOC();
}
if ((estimate - y) > 4096) {
cmd = zend_string_truncate(cmd, y, 0);
}
ZSTR_LEN(cmd) = y;
return cmd;
}
返回cmd,完成escapeshellcmd的调用
动态调试
动态调试基于Windows
常规输入
测试代码:
<?php
echo escapeshellcmd('1"');
?>
断点: 初始化:假设所有都转义,再加上结束符estimate就是5,读入1" 两字节
读取字符1,判断属于单字符,进入switch判断是否需要转义 无需转义 x和y都+1,进入第二个循环 需要转义,添加转义符 最后写入结束符截断字符串 完成转义,返回
输入多字节字符
测试代码:
<?php
echo escapeshellcmd('"我1');
?>
断点: 初始变量,由于还暂未对y赋值,是奇奇怪怪的值;UTF8中:中文占三字节 双引号是有效的单字符,进入switch switch判定为需要转义:因此向cmd先写入转义符再写入双引号,此时y==2 第一个循环结束前x+1,x==1,之后进入下一个循环读取下一个字符 由于UTF8是三字节,因此这里只读入了前两个字节 y由于此时写入了四个字符:转义的双引号和两个UTF8的字节,所以是4;然后因为循环每次都x++,这里加多了1,要减去,相当于只是多写了1个字节 然后下一个循环开始前,x++,现在x指向UTF8的第三个字节 这一步读取UTF8的第三个字节和后面的一个字符1,将其视为一个多字节字符了,读取过程同上,然后循环结束,写入一个结束符\0 最后转义完毕,返回cmd cmd的内容是:转义的双引号(2) + UTF中文前两个字节(2) + UTF8中文第三个字节加字符1(2) + 结束符(0),因此y==6。
完
欢迎在评论区留言,欢迎关注我的CSDN @Ho1aAs
|