文章归档

置顶文章

Web安全

Web安全基础

PHP相关

Writeups

靶机系列

HackTheBox

VulnHub

代码审计

PHP代码审计

流量分析

机器学习

基础学习

Python

Python编程

Java

Java编程

算法

Leetcode

随笔

经验

技术

 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
// 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工具扫描一遍,有一个大概的方向。

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再拼接网站目录得到的,而且没有任何过滤,那么就很容易利用路径遍历来删除任何文件。利用方式:

首先通过一个危险函数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的方式传参。利用方式如下:

  1. 第一步首先在自己的VPS中写一个shell.php文件:

    1
    2
    3
    4
    5
    6
    7
    <?php
    echo "
    <?php
    system('ipconfig');
    ?>
    ";
    ?>

    因为源代码中是通过file_get_contents()函数读取文件内容,所以我们必须把真正的shell文件内容echo到页面上。

  2. 发起请求

  3. 访问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中的两个常量TPLSTYLECSTART的值分别替换成$style$start

1
2
3
4
5
6
<?php 
define("CSTART","0"); //是否开启缓存 1开启 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()函数:

image-20200608151404395

利用方式:

0x03 常见函数

代码执行函数

文件相关函数

Copyright © ca01h 2019-2020 | 本站总访问量