记一次CMS系统通杀0day审计

admin 2024年1月4日17:02:14评论34 views字数 4363阅读14分32秒阅读模式

阅读须知

亲爱的读者,我们诚挚地提醒您,WebSec实验室的技术文章仅供个人研究学习参考。任何因传播或利用本实验室提供的信息而造成的直接或间接后果及损失,均由使用者自行承担责任。WebSec实验室及作者对此概不负责。如有侵权,请立即告知,我们将立即删除并致歉。感谢您的理解与支持!

01

SQL注入审计基本思路

首先看下基本 SQL 注入漏洞原理的示例:

<?php
$id=$_GET['user_id'];
try{
    $pdo=new PDO('mysql:host=localhost;port=3306','root','root');
    $sql="select * from dvwa.users where user_id={$id}";
    $row=$pdo->query($sql);
    foreach ($row as $key => $value){
        print_r($value);
    }
}
catch(POOException $e){
    echo $e->getMessage();
}
?>

我们知道通过 GET 方式来获取 user_id,在 sql 语句中,直接把用户所输入的 user_id 值当作查询的条件,也没有任何的过滤,是可以直接构造 Payload,看下面示例:

http://127.0.0.1/sqltest.php?user_id=1 or 1=1

SQL 语句为:

---select * from dvwa.users where user_id=1 or 1=1

原本的语句中没有加单引号或者加转义的函数,所以并不需要单引号

记一次CMS系统通杀0day审计

当然这是最基本的,目前市面上的设备或者系统基本不会这样写,因为这种无过滤的 SQL 语句大多被框架和书写规范给杜绝掉了。很多师傅在面试的时候大多面试官会问,怎么防御 SQL 注入,大多人会回答进行预编译处理,下来看一个使用预编译用法错误的例子:

<?php
$id=$_GET['user_id'];
try{
    $pdo=new PDO('mysql:host=localhost;port=3306','root','root');
    $sql="select * from dvwa.users where user_id={$id}";
    $stmt=$pdo->prepare($sql);
    $stmt->execute();
    foreach ($stmt as $key => $value){
        print_r($value);
    }
}
catch(POOException $e){
    echo $e->getMessage();
}
?>

这里使用 PDO 的 prepare 方法准备 SQL 查询语句,但是 SQL 执行语句还是把用户输入的点直接带到查询条件中,从而存在 sql 注入漏洞。下面看一个可以防御 SQL 注入的最基本的写法:

<?php
$id=$_GET['user_id'];
try{
      $id=$pdo->quote($id);
      $sql="select * from dvwa.users where user_id={$id}";
      $row=$pdo->query($sql);
      foreach ($row as $key => $value){
          print_r($value);
      }
}
catch(POOException $e){
    echo $e->getMessage();
}
?>

$pdo->quote($id) 是 PDO 对象的一个方法,用于对字符串进行安全的转义和引号处理。例如,如果 $id 的值是 O'Reilly,那么调用 $pdo->quote($id) 后返回的结果将是 'O'Reilly'。转义后的字符串可以直接用于构建 SQL 查询中的参数。

记一次CMS系统通杀0day审计

这是一种方法,还有一种预编译可防御 SQL 注入的写法:

<?php
$id=$_GET['user_id'];
try{
    $pdo=new PDO('mysql:host=localhost;port=3306','root','root');
    $sql = "SELECT * FROM dvwa.users WHERE user_id = :id";
    $stmt = $pdo->prepare($sql);
    $stmt->bindParam(':id', $id, PDO::PARAM_INT);
    $stmt->execute();
    foreach ($stmt as $key => $value){
        print_r($value);
    }
}
catch(POOException $e){
    echo $e->getMessage();
}
?>

$stmt->bindParam(':id', $id, PDO::PARAM_INT): 这个方法用于将参数绑定到准备好的 SQL 查询语句中。在这个例子中,将变量 $id 绑定到查询语句的 :id 参数位置上,并指定参数类型为整数 (PDO::PARAM_INT)

记一次CMS系统通杀0day审计

02

实战审计通用SQL注入

在审计某 cms 系统时,在后台发现执行 SQL 语句的模块,抓包看看路由怎么走

记一次CMS系统通杀0day审计

可以看到是 sysCheckFile_deal.php 文件,执行 SQL 语句的参数为 sqlContent

记一次CMS系统通杀0day审计

其中,swith 是一个控制变量,当 mudi 的值为 sql 时,执行 SqlDeal 函数

记一次CMS系统通杀0day审计

记一次CMS系统通杀0day审计

跟踪一下 SqlDeal 函数

记一次CMS系统通杀0day审计

  if (strlen($sqlContent) == 0){
    JS::AlertBackEnd('SQL语句不能为空.');
  }

  $newSql = strtolower($sqlContent);
  if (strpos($newSql,'into outfile') !== false){
    JS::AlertBackEnd('SQL语句中不能含有内容“into outfile”.'
    );
  }elseif (strpos($newSql,'global general_log') !== false){
    JS::AlertBackEnd('SQL语句中不能含有内容“global general_log”.');
  }elseif (strpos($newSql,'0x') !== false){
    JS::AlertBackEnd('SQL语句中不能含有内容“0x”.');
  }

第一个 if 过滤掉了 sql 语句为空的情况,strtolower 函数将 $sqlContent(SQL语句)中的所有字符转换为小写字母,并将结果保存在 $newSql 变量中。

strpos 函数的作用是查看 $newsql 变量中是否包含 'into outfile',如果包含,则为 ture,剩下的 if 语句是为了过滤掉 sql 语句中的 'into outfile''global general_log''0x'

  $urow=$DB->GetRow('select MB_userpwd,MB_userKey from '. OT_dbPref .'member where MB_ID='. $MB->mUserID);
  $userpwd  = OT::DePwdData($userpwd, $pwdMode, $pwdKey);
  $userpwd = md5(md5($userpwd) . $urow['MB_userKey']);
    if ($urow['MB_userpwd'] != $userpwd){
      JS::AlertBackEnd('登录密码错误!');
    }
  unset($urow);

该语句使用了一个名为 $DB 的对象,通过调用对象的 GetRow() 方法来查询数据库中的一行数据。查询的 SQL 语句是 'select MB_userpwd,MB_userKey from '.OT_dbPref .'member where MB_ID='. $MB->mUserID,其中包含了表名和查询条件。

OT_dbPref 是一个常量,表示数据表前缀。$MB->mUserID 表示当前用户的 ID。查询结果将会返回一行记录,包含了两个字段 MB_userpwd 和 MB_userKey 的值。这一行记录被赋值给变量 $urow,可以通过 $urow['MB_userpwd'] 和 $urow['MB_userKey'] 分别获取这两个字段的值。

之后则是核对密码,密码核对成功则开始执行 sql 语句

  $sqlArr = array();
  if (strpos($sqlContent, ';') !== false){
    preg_match_all( "@([sS]+?;)h*[nr]@" , $sqlContent . PHP_EOL , $sqlArr ); // 数据以分号;nr换行  为分段标记
    !empty( $sqlArr[1] ) && $sqlArr = $sqlArr[1];
    $sqlArr = array_filter($sqlArr);
  }else{
    $sqlArr[] = $sqlContent;
  }

定义 $sqlArr 为空数组,之后使用 strpos 函数查找 sql 语句中的分号,如果没有分号,直接把 $sqlContent 的值赋给数组;如果有分号,则使用 preg_match_all 函数使用正则表达式来匹配 SQL 语句中的分号,并将分号前面的内容作为一个完整的语句存储到 $sqlArr 数组中。

由于 preg_match_all() 函数的返回值是一个二维数组,需要将其中的第二维提取出来。如果 $sqlArr[1] 不为空,则将其赋值给 $sqlArr 变量。最后使用 array_filter 函数,由于没有给他传递回调函数,所以默认情况下,函数会过滤掉所有值为 nullfalse'' 和 0 的元素,并返回一个新的数组。

可以发现可以使用 query 来执行 sql 语句,复现时构造恶意的 SQL 语句即可。

记一次CMS系统通杀0day审计

SET GLOBAL/**/general_log='on'   等

原文始发于微信公众号(刨洞安全团队):记一次CMS系统通杀0day审计

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年1月4日17:02:14
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   记一次CMS系统通杀0day审计http://cn-sec.com/archives/2363897.html

发表评论

匿名网友 填写信息