簡單聊聊在 OpenEMR 5.0.1 <Patch 5 中發現的 CVE-2018-17179、CVE-2018-17180 及 CVE-2018-17181

最近武漢肺炎疫情不停延燒、愈發嚴重,每個人都只能盡力做好自主管理、照顧好身子,也得盡量別在這期間感冒而造成額外擔憂,台灣目前仍能防治住疫情也得特別感謝政府、第一線醫護人員、防疫人員、第三方人員等等許多人的努力!

筆者我身為技術宅,不是醫護人員,只能作為普通民眾盡可能地遵從一些政策、規定以降低醫療人員的負擔,但即使如此也還是希望能替醫療界盡一份心力,所以只好發揮所長試著在醫療界不為人知、沒有直接關聯的角落作出貢獻,因此今天就來聊聊很久之前曾在開源醫療管理系統 OpenEMR 所發現的 3 個 Pre-Auth 弱點。

過去資策會資安科技研究所曾經發表過一篇文章 OpenEMR 弱點分析,分析關於他們在 OpenEMR 發現的 RCE 弱點,RCE 可以讓攻擊者直接控制整台主機,是非常嚴重的風險,值得慶幸的是弱點必須要先登入擁有權限的帳號才可觸發。但倘若攻擊者發現可以在不知道帳號密碼的前提下從匿名使用者(未登入狀態)提升權限(登入)的弱點,就能進一步擴大風險!因此我決定挖掘 Pre-Auth 的弱點並將之回報給開發者作修復以預防此類風險發生,最終發現 2 個 Pre-Auth SQL Injection 和 1 個 Pre-Auth Arbitrary File Read 弱點,其對應 CVE 編號分別為 CVE-2018-17179CVE-2018-17181CVE-2018-17180

這邊也感謝開發者 Brady Miller 回應及修復真的非常地迅速。
Thanks to the developer Brady Miller for very quick respond and fix.

CVE-2018-17179 : SQL Injection

此弱點已在 commit 3e22d11 中獲得了修復。

此弱點的進入點是 taskman.php 這個檔案,這個路徑不需要登入、任何人均可存取。當請求動作是 make_task 時會轉交給 make_task 函式作處裡。

File: /interface/forms/eye_mag/taskman.php
100: $ajax_req = $_REQUEST;
101: 
102: if ($_REQUEST['action']=='make_task') {
103:     make_task($ajax_req);
104: }

而 make_task 函式中,進行 SELECT 查詢時都有過濾,唯獨在 INSERT 時遺漏了,因此這邊很單純就能直接進行注入,恰好此處會顯示 SQL 錯誤訊息,於是可以利用 Error-Based 獲取資料庫內容,包括取得使用者帳號密碼以登入系統進行更多操作。

File: /interface/forms/eye_mag/php/taskman_functions.php
35: function make_task($ajax_req)
36: {
37:     global $send;
38:     $from_id    = $ajax_req['from_id'];
39:     $to_id      = $ajax_req['to_id'];
40:     $patient_id = $ajax_req['pid'];
41:     $doc_type   = $ajax_req['doc_type'];
42:     $doc_id     = $ajax_req['doc_id'];
43:     $enc        = $ajax_req['enc'];

93:     } else if (!$task['ID']) {
94:         $sql = "INSERT into form_taskman
95: 				(REQ_DATE, FROM_ID,  TO_ID,  PATIENT_ID,  DOC_TYPE,  DOC_ID,  ENC_ID) VALUES
96: 				(NOW(), '$from_id', '$to_id','$patient_id','$doc_type','$doc_id','$enc')";
97:         sqlQuery($sql);

CVE-2018-17180 : Arbitrary File Read

此弱點在 commit 4963fe4 中獲得了修復。

進入點 download_template.php 接收一個參數 docid,此參數隨後會代入路徑,並利用 file_get_contents 函式讀取檔案內容後作輸出。此檔案除了要求必須啟用「Patient Portal」的功能才可存取以外,在檔案開頭還引入了 verify_session.php,從檔案名稱來看似乎有進行一些驗證的感覺。

File: /portal/lib/download_template.php
27: require_once(dirname(__file__) . "/../verify_session.php");

36: $form_filename = $_POST['docid'];

371: $templatepath = "$templatedir/$form_filename";
    
440:     // Not a zip archive.
441:     $edata = file_get_contents($templatepath);
442:     $edata = doSubs($edata);
443:     if ($html_flag) { // return raw html template
444:         $html = $edata;
445:     } else { // add br for lf in text template
446:         $html = nl2br($edata);
447:     }
448: }
449: 
450: echo $html;

因此讓我們追進 verify_session.php 看看,在這邊驗證了變數 $_SESSION['pid']$_SESSION['patient_portal_onsite_two'] 是否存在,若變數不存在則會強制跳轉到登入頁面。

File: /portal/verify_session.php
41: // kick out if patient not authenticated
42: if (isset($_SESSION['pid']) && isset($_SESSION['patient_portal_onsite_two'])) {
43:     $pid = $_SESSION['pid'];
44: } else {
45:     session_destroy();
46:     header('Location: '.$landingpage.'&w');
47:     exit;
48: }

本來預想是不可能繞過此限制,但在閱覽其他檔案的程式碼後意外發現當 index.php 收到特定請求時,會將 $_SESSION['pid']$_SESSION['patient_portal_onsite_two'] 同時設定為 true,因此就能繞過原 verify_session.php 中的限制,達成 Pre-Auth Arbitrary File Read。

File: /portal/index.php
351: if (isset($_GET['requestNew'])) {
352:     $_SESSION['patient_portal_onsite_two'] = true;
353:     $_SESSION['authUser'] = 'portal-user';
354:     $_SESSION['pid'] = true;

因為這個意外的發現,所以有些透過 verify_session.php 保護的檔案也成為了需要檢查的目標之一,而隨後也確實在「Patient Portal」系列功能中發現了另一個 Pre-Auth SQL Injection。

CVE-2018-17181 : SQL Injectiion

此弱點在同樣 commit 4963fe4 中獲得了修復。

這個弱點的進入點檔案是 paylib.php,收到 mode 為 portal-save 的請求後會轉交給 SaveAudit 函式進行處理,而該函式又會呼叫到 ApplicationTable 物件的 portalAudit 函式。

File: /portal/lib/paylib.php
47: if ($_POST['mode'] == 'portal-save') {
48:     $form_pid = $_POST['form_pid'];
49:     $form_method = trim($_POST['form_method']);
50:     $form_source = trim($_POST['form_source']);
51:     $upay = isset($_POST['form_upay']) ? $_POST['form_upay'] : '';
52:     $cc = isset($_POST['extra_values']) ? $_POST['extra_values'] : '';
53:     $amts = isset($_POST['inv_values']) ? $_POST['inv_values'] : '';
54:     $s = SaveAudit($form_pid, $amts, $cc);

75: function SaveAudit($pid, $amts, $cc)
76: {
77:     $appsql = new ApplicationTable();
78:     try {
79:         $audit = array ();
80:         $audit['patient_id'] = $pid;

93:         $edata = $appsql->getPortalAudit($pid, 'review', 'payment');
94:         $audit['date'] = $edata['date'];
95:         if ($edata['id'] > 0) {
96:             $appsql->portalAudit('update', $edata['id'], $audit);
97:         } else {
98:             $appsql->portalAudit('insert', '', $audit);
99:         }

portalAudit 函式過濾了幾乎所有參數,唯獨遺漏了 $audit['patient_id'],而此參數恰好又是使用者能控制的參數,因此就能進行 SQL 的注入,而此處同樣也會顯示 SQL 錯誤訊息,因此也能利用 Error-Based 取得資料庫內容。

File: /portal/lib/appsql.class.php
135:     public function portalAudit($type = 'insert', $rec = '', array $auditvals, $oelog = true, $error = true)
136:     {

162:         try {
163:             if ($type != 'update') {
164:                 $logsql = "INSERT INTO onsite_portal_activity".
165:                         "( date, patient_id, activity, require_audit, pending_action, action_taken, status, narrative,".
166:                             "table_action, table_args, action_user, action_taken_time, checksum) ".
167:                                 "VALUES (NOW(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
168:             } else {
169:                 $logsql = "update onsite_portal_activity set date=?, patient_id=?, activity=?, require_audit=?,".
170:                         "            pending_action=?, action_taken=?,status=?, narrative=?, table_action=?, table_args=?,".
171:                                         "action_user=?, action_taken_time=?, checksum=? ";
172:                 $logsql .= "where id=".$rec ." And patient_id=".$audit['patient_id'];
173:             }

此弱點同樣仰賴變數 $_SESSION['patient_portal_onsite_two'] 必須存在才可以存取,而錯誤設置 $_SESSION['patient_portal_onsite_two'] 的問題也在同一個 commit 中獲得了修復,在修復後的版本中,未登入使用者是無法訪問這些檔案的。

References