Skip to content

Instantly share code, notes, and snippets.

@chtg
Last active August 29, 2015 14:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save chtg/dd3f92f7f221bebc4db0 to your computer and use it in GitHub Desktop.
Save chtg/dd3f92f7f221bebc4db0 to your computer and use it in GitHub Desktop.
PHP 脚本多字节字符解析模式带来的安全隐患
PHP 脚本多字节字符解析模式带来的安全隐患
> Taoguang Chen <github.com/chtg> - 2014.12.15
多字节字符解析模式
========
PHP 从 5.3 起引入了多字节字符解析模式,在 5.3 版本中开启该模式较为麻烦,需要在编译时开启相应参数,并在 php.ini 文件和脚本中进行配置。但 PHP 从 5.4 起默认支持多字节字符解析模式,只需通过 php.ini 文件中配置即可开启该模式。
我们先来看看 PHP 提供的一些配置选项:
zend.multibyte "0" PHP_INI_PERDIR
zend.script_encoding NULL PHP_INI_ALL
配置选项 zend.multibyte 用于开启多字节字符解析模式,配置选项 zend.script_encoding 用于设置脚本的编码。比如我们想让 PHP 开启多字节字符解析模式并把脚本的字符编码设置为 CP936 编码,只需要在 php.ini 文件中做如下设置:
zend.multibyte = On
zend.script_encoding = CP936
这里还有一个重要的配置选项:
mbstring.internal_encoding NULL PHP_INI_ALL
该选项用来设置 mbstring 模块函数内部处理的默认字符集编码,在多字节字符解析模式下作为内部字符集编码。如果该选项没有配置,内部字符集编码将受其他一些配置选项的影响,PHP 5.5 及之前的版本内部字符集编码默认为 ISO-8859-1 编码,PHP 5.6 默认为 UTF-8 编码。
接下来我们简单了解下 PHP 是如何实现多字节字符解析模式的。
ZEND_API int open_file_for_scanning(zend_file_handle *file_handle TSRMLS_DC)
{
if (CG(multibyte)) {
SCNG(script_org) = (unsigned char*)buf;
SCNG(script_org_size) = size;
SCNG(script_filtered) = NULL;
zend_multibyte_set_filter(NULL TSRMLS_CC);
if (SCNG(input_filter)) {
if ((size_t)-1 == SCNG(input_filter)(&SCNG(script_filtered), &SCNG(script_filtered_size), SCNG(script_org), SCNG(script_org_size) TSRMLS_CC)) {
zend_error_noreturn(E_COMPILE_ERROR, "Could not convert the script from the detected "
"encoding \"%s\" to a compatible encoding", zend_multibyte_get_encoding_name(LANG_SCNG(script_encoding)));
}
buf = (char*)SCNG(script_filtered);
size = SCNG(script_filtered_size);
}
}
SCNG(yy_start) = (unsigned char *)buf - offset;
yy_scan_buffer(buf, size TSRMLS_CC);
PHP 的词法分析器在分析脚本代码输入流前,会判断是否开启了多字节字符解析模式,如果开启,则会调用 zend_multibyte_set_filter 设置输入流的过滤器。当内部字符集编码和 zend.script_encoding 所设置的脚本字符集编码不相同时,会调用 encoding_filter_script_to_internal 过滤器进行字符集编码转换。
static size_t encoding_filter_script_to_internal(unsigned char **to, size_t *to_length, const unsigned char *from, size_t from_length TSRMLS_DC)
{
const zend_encoding *internal_encoding = zend_multibyte_get_internal_encoding(TSRMLS_C);
ZEND_ASSERT(internal_encoding);
return zend_multibyte_encoding_converter(to, to_length, from, from_length, internal_encoding, LANG_SCNG(script_encoding) TSRMLS_CC);
}
该过滤器调用 zend_multibyte_encoding_converter 把输入流按照 zend.script_encoding 所设置的脚本字符集编码识别,并转换为内部字符集编码的输入流,然后进行词法分析。
简单来说,在多字节字符解析模式下,PHP 会把脚本中的代码按照设置的多字节字符集编码进行识别,并转换成内部字符集编码的代码,然后进行词法分析并执行脚本。
潜在问题及安全隐患
========
由上面的分析可以看到,多字节字符解析模式下,涉及到了对脚本的字符集编码识别及转换的过程,而这一过程可能会导致一些问题。
比如下面这段代码:
$str = '{0xab}\'';
// 这里 {0xab} 表示一个十六进制编码为 ab 的单字节字符
当在开启多字节字符解析模式并且脚本的字符集编码设置为 CP936 时,因为 \ 的十六进制编码是 5c,这时 {0xab}\ 会被识别为一个有效的 CP396 编码的多字节字符 {0xab0x5c},再被转换为一个 utf8 编码的多字节字符 {0xe70x8e0x95}(内部字符集编码并没有限制,这里只是以 UTF-8 为例),因为 PHP 词法分析器是以字符为单位进行扫描并分析代码的,这时 PHP 分析并执行的代码段就变成了:
$str = '{0xe70x8e0x95}'';
// {0xe70x8e0x95} 表示为一个有效的 utf-8 编码的多字节字符
显而易见,这两段数据流的词法解析结果是完全不同的,后面的代码执行时会出现语法错误。
事实上,PHP 提供了一些处理多字节编码数据的函数,但同样也有很多函数只能处理单字节编码数据,比如 var_export() 函数以及 addslashes() 函数。这两个函数在处理数据时始终把输入和输出的数据按照单字节字符进行处理,这时如果在 PHP 多字节字符解析解析模式下使用并依赖于这些函数所提供的功能,可能会导致一些严重的安全问题。
实例 - MyBB <= 1.8.3 代码执行漏洞
========
class diskCacheHandler
{
...
function put($name, $contents)
{
global $mybb;
if(!is_writable(MYBB_ROOT."cache"))
{
$mybb->trigger_generic_error("cache_no_write");
return false;
}
$cache_file = fopen(MYBB_ROOT."cache/{$name}.php", "w") or $mybb->trigger_generic_error("cache_no_write");
flock($cache_file, LOCK_EX);
$cache_contents = "<?php\n\n/** MyBB Generated Cache - Do Not Alter\n * Cache Name: $name\n * Generated: ".gmdate("r")."\n*/\n\n";
$cache_contents .= "\$$name = ".var_export($contents, true).";\n\n?>";
fwrite($cache_file, $cache_contents);
flock($cache_file, LOCK_UN);
fclose($cache_file);
return true;
}
MyBB 的 put() 方法用于写缓存文件操作,这个方法使用了 var_export() 函数,在默认的 PHP 配置环境下,这样的处理是安全可靠的,但从上面的分析我们可以知道,如果开启了多字节字符解析模式,可能就会存在安全问题了。
比如在 zend.multibyte = On 和 zend.script_encoding = CP936 的配置环境下,我们提交如下的字符串数据:
{0xab}';phpinfo();/*
这个字符串数据经过 put() 方法写入缓存文件后,会变为如下代码:
'{0xab}\';phpinfo();/*';
这时 PHP 词法分析器在分析这段代码前,会进行字符集编码识别和转换,{0xab}\ 会被识别为一个有效的 CP936 编码的多字节字符 {0xab0x5c},并被转换为一个 utf8 编码的多字节字符 {0xe70x8e0x95}(为了方便说明,这里依旧假设默认的内部字符集编码为 UTF-8,实际上这个并不影响执行结果),因此 PHP 实际执行的代码如下:
'{0xe70x8e0x95}';phpinfo();/*';
你会发现,可爱的 phpinfo() 执行了:)
PoC or EXP: 缺
EOF
========
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment