从escapeshellcmd bypass说起到宽字节注入

1 php 多字节绕过escapeshellcmd

escapeshellcmd()对shell元字符过滤加反斜杠;
反斜线(\)会在以下字符之前插入: #&;`|*?~<>^()[]{}$\, \x0A 和 \xFF,但在php5.2.5及之前存在通过输入多字节绕过escapeshellcmd的问题。5.2.6 已经修复了该问题。

执行 escapeshellcmd(“echo “.chr(0xc0).”;id”);
加上反斜杠之后,也就是echo \xc0\x5c;id,在中文环境中\xc0\x5c是会被认为是gbk字符的。

1
2
3
4
5
6
7
>>> hex(ord('\\'))
'0x5c'
>>> s='\xc0\x5c'
>>> print s.decode('gbk').encode('utf8')
>>> s.decode('gbk').encode('utf8')
'\xe7\xb9\xba'

\被吃掉之后于是就变成了echo 繺;id 了。
gbk是宽字节,两个字节,gbk字符范围:8140-FEFE,首字节在81-FE直接,尾字节在40-FE之间,显然5C在尾字节中。考虑0xbf;id,escape之后就变成了0xbf5c;id,0xbf5c是一个合法的GBK编码,那就变成了[0xbf5c];id了。而utf8表示中文一般三个字节。
同样受影响的还有escapeshellarg(),源码中的处理是一个字节一个字节来处理的。这种漏洞应该有一定普遍性,在当时来说。下面我们看下修复的源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
char *php_escape_shell_cmd(char *str) {
register int x, y, l;
char *cmd;
char *p = NULL;
TSRMLS_FETCH();
l = strlen(str);
cmd = safe_emalloc(2, l, 1); //申请了2倍字符
for (x = 0, y = 0; x < l; x++) {
int mb_len = php_mblen(str + x, (l - x));
//这一段是5.2.6新加的,就是在处理多字节符号的时候,当多字节字符小于0的时候不处理,大于1的时候跳过,等于1的时候执行过滤动作
/* skip non-valid multibyte characters */
if (mb_len < 0) {
continue;
} else if (mb_len > 1) {
memcpy(cmd + y, str + x, mb_len);
y += mb_len;
x += mb_len - 1;
continue;
}
switch (str[x]) {
case '"':
case '\'':
#ifndef PHP_WIN32
if (!p && (p = memchr(str + x + 1, str[x], l - x - 1))) {
/* noop */
} else if (p && *p == str[x]) {
p = NULL;
} else {
cmd[y++] = '\\';
}
cmd[y++] = str[x];
break;
#endif
case '#': /* This is character-set independent */
case '&':
case ';':
case '`':
case '|':
case '*':
case '?':
case '~':
case '<':
case '>':
case '^':
case '(':
case ')':
case '[':
case ']':
case '{':
case '}':
case '$':
case '\\':
case '\x0A': /* excluding these two */
case '\xFF':
#ifdef PHP_WIN32
/* since Windows does not allow us to escape these chars, just remove them */
case '%':
cmd[y++] = ' ';
break;
#endif
cmd[y++] = '\\';
/* fall-through */
default:
cmd[y++] = str[x];
}
}
cmd[y] = '\0';
return cmd;
}

这个bypass已经成为过去时了,但是还是有很大的借鉴意义,就是宽字节注入,这种情况不仅仅发生命令注入时,更多的时候在sql注入,下面来分析一下宽字节注入如下三种情况,都是由于宽字节的问题导致的。

2 宽字节sql注入

1,一种情况 iconv转换,addslashes之后从gbk转到utf8

1
2
3
4
5
6
7
8
9
10
11
12
$user = $_POST[ 'username' ];
$user = addslashes($user);
$user = iconv("gbk", 'utf8', $user);
$pass = $_POST[ 'password' ];
$pass = md5( $pass );
$qry = "SELECT * FROM `users` WHERE user='$user' AND password='$pass';";
print_r($qry);
$result = @mysql_query($qry) or die('<pre>' . mysql_error() . '</pre>' );
var_dump($result);

处理过程如下:
%bf%27—-(addslashes)->%bf%5c%27—–(utf8)—->縗’ 这样单引号就放出来了,大体流程是%bf%27经过addslashes之后变成了%bf%5c%27,再经过iconv从gbk转换为utf8的时候,变成了%e7%b8%97%27,也就是縗’。利用的前提是设置了set names utf8。

2,在php中使用mysql_query(‘set names gbk’),指定了客户端,连接层,结果为gbk编码。构造数据%bf%27,过程和第一种情况类似
%bf%27—(addslashes)–>%bf%5c%27—(set names gbk)—>縗’

3,iconv转换从utf8到gbk,set names字符集为gbk,构造数据如下%e9%8c%a6带入反斜杠\,注释掉单引号
大体数据流程:%e9%8c%a6—–(utf8)—-%e5%5c—-(addslashes)—>%e5%5c%5c

1
2
3
4
5
>>> s = '\xe9\x8c\xa6'
>>> s.decode('utf8')
u'\u9326'
>>> s.decode('utf8').encode('gbk')
'\xe5\\'

总之一条,都是打的%5c的注意,要么转义后转utf8吃掉%5c,要么转utf8后再转义放出%5c

参考:
http://seclists.org/bugtraq/2008/May/61
http://www.sektioneins.de/en/advisories/advisory-032008-php-multibyte-shell-command-escaping-bypass-vulnerability.html
http://php.net/ChangeLog-5.php
http://php.net/releases/