簡單聊聊在 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-17179、 CVE-2018-17181 和 CVE-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
- https://github.com/openemr/openemr/commit/3e22d11
- https://github.com/openemr/openemr/commit/4963fe4
- https://www.open-emr.org/wiki/index.php/Old_Outdated_OpenEMR_Patches#5.0.1_Patch_.281.2F19.2F19.29
- https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-17179
- https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-17180
- https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-17181