2.1 SSRF漏洞
SSRF(Server Side Request Forgery,服务端请求伪造)是一种攻击者通过构造数据进而伪造服务器端发起请求的漏洞。因为请求是由内部发起的,所以一般情况下,SSRF漏洞攻击的目标往往是从外网无法访问的内部系统。
SSRF漏洞形成的原因多是服务端提供了从外部服务获取数据的功能,但没有对目标地址、协议等重要参数进行过滤和限制,从而导致攻击者可以自由构造参数,而发起预期外的请求。
2.1.1 SSRF的原理解析
URL的结构如下:
authority组件又分为以下3部分(见图2-1-1):
图2-1-1(图片来源:维基百科)
scheme由一串大小写不敏感的字符组成,表示获取资源所需要的协议。
authority中,userinfo遇到得比较少,这是一个可选项,一般HTTP使用匿名形式来获取数据,如果需要进行身份验证,格式为username:password,以@结尾。
host表示在哪个服务器上获取资源,一般所见的是以域名形式呈现的,如baidu.com,也有以IPv4、IPv6地址形式呈现的。
port为服务器端口。各协议都有默认端口,如HTTP的为80、FTP的为21。使用默认端口时,可以将端口省略。
path为指向资源的路径,一般使用“/”进行分层。
query为查询字符串,用户将用户输入数据传递给服务端,以“?”作为表示。例如,向服务端传递用户名密码为“?username=admin&password=admin123”。
fragment为片段ID,与query不同的是,其内容不会被传递到服务端,一般用于表示页面的锚点。
理解URL构造对如何进行绕过和如何利用会很有帮助。
以PHP为例,假设有如下请求远程图片并输出的服务。
如果URL参数为一个图片的地址,将直接打印该图片,见图2-1-2。
图2-1-2
但是因为获取图片地址的URL参数未做任何过滤,所以攻击者可以通过修改该地址或协议来发起SSRF攻击。例如,将请求的URL修改为file:///etc/passwd,将使用FILE协议读取/etc/passwd的文件内容(最常见的一种攻击方式),见图2-1-3。
图2-1-3
2.1.2 SSRF漏洞的寻找和测试
SSRF漏洞一般出现在有调用外部资源的场景中,如社交服务分享功能、图片识别服务、网站采集服务、远程资源请求(如wordpress xmlrpc.php)、文件处理服务(如XML解析)等。在对存在SSRF漏洞的应用进行测试的时候,可以尝试是否能控制、支持常见的协议,包括但不限于以下协议。
❖ file://:从文件系统中获取文件内容,如file:///etc/passwd。
❖ dict://:字典服务器协议,让客户端能够访问更多字典源。在SSRF中可以获取目标服务器上运行的服务版本等信息,见图2-1-4。
图2-1-4
❖ gopher://:分布式的文档传递服务,在SSRF漏洞攻击中发挥的作用非常大。使用Gopher协议时,通过控制访问的URL可实现向指定的服务器发送任意内容,如HTTP请求、MySQL请求等,所以其攻击面非常广,后面会着重介绍Gopher的利用方法。
2.1.3 SSRF漏洞攻击方式
2.1.3.1 内部服务资产探测
SSRF漏洞可以直接探测网站所在服务器端口的开放情况甚至内网资产情况,如确定该处存在SSRF漏洞,则可以通过确定请求成功与失败的返回信息进行判断服务开放情况。例如,使用Python语言写一个简单的利用程序。
运行结果见图2-1-5。
图2-1-5
2.1.3.2 使用Gopher协议扩展攻击面
1.攻击Redis
Redis一般运行在内网,使用者大多将其绑定于127.0.0.1:6379,且一般是空口令。攻击者通过SSRF漏洞未授权访问内网Redis,可能导致任意增、查、删、改其中的内容,甚至利用导出功能写入Crontab、Webshell和SSH公钥(使用导出功能写入的文件所有者为redis的启动用户,一般启动用户为root,如果启动用户权限较低,将无法完成攻击)。
Redis是一条指令执行一个行为,如果其中一条指令是错误的,那么会继续读取下一条,所以如果发送的报文中可以控制其中一行,就可以将其修改为Redis指令,分批执行指令,完成攻击。如果可以控制多行报文,那么可以在一次连接中完成攻击。
在攻击Redis的时候,一般是写入Crontab反弹shell,通常的攻击流程如下:
此时我们使用socat获取数据包,命令如下:
将本地1234端口转发到6379端口,再依次执行攻击流程的指令,将得到攻击数据,见图2-1-6。
图2-1-6
然后将其中的数据转换成Gopher协议的URL。先舍弃开头为“>”和“<”的数据,这表示请求和返回,再舍弃掉+OK的数据,表示返回的信息。在剩下的数据中,将“\r”替换为“%0d”,将“\n”(换行)替换为“%0a”,其中的“$”进行URL编码,可以得到如下字符串:
如果需要直接在该字符串中修改反弹的IP和端口,则需要同时修改前面的“$56”,“56”为写入Crontab中命令的长度。例如,此时字符串为
要修改反弹的IP为172.28.0.33,则需要将“56”改为“57”(56+1)。将构造好的字符串填入进行一次攻击,见图2-1-7,返回了5个OK,对应5条指令,此时在目标机器上已经写入了一个Crontab,见图2-1-8。
图2-1-7
图2-1-8
写Webshell等与写文件操作同理,修改目录、文件名并写入内容即可。
2.攻击MySQL
攻击内网中的MySQL,我们需要先了解其通信协议。MySQL分为客户端和服务端,由客户端连接服务端有4种方式:UNIX套接字、内存共享、命名管道、TCP/IP套接字。
我们进行攻击依靠第4种方式,MySQL客户端连接时会出现两种情况,即是否需要密码认证。当需要进行密码认证时,服务器先发送salt,然后客户端使用salt加密密码再验证。当不需进行密码认证时,将直接使用第4种方式发送数据包。所以,在非交互模式下登录操作MySQL数据库只能在空密码未授权的情况下进行。
假设想查询目标服务器上数据库中user表的信息,我们先在本地新建一张user表,再使用tcpdump进行抓包,并将抓到的流量写入/pcap/mysql.pcap文件。命令如下:
开始抓包后,登录MySQL服务器进行查询操作,见图2-1-9。
图2-1-9
然后使用wireshark打开/pcap/mysql.pcap数据包,过滤MySQL,再随便选择一个包并单击右键,在弹出的快捷菜单中选择“追踪流 → TCP流”,过滤出客户端到服务端的数据包,最后将格式调整为HEX转储,见图2-1-10。
此时便获得了从客户端到服务端并执行命令完整流程的数据包,然后将其进行URL编码,得到如下数据:
进行攻击,获得user表中的数据,见图2-1-11。
图2-1-10
图2-1-11
3.PHP-FPM攻击
利用条件如下:Libcurl,版本高于7.45.0;PHP-FPM,监听端口,版本高于5.3.3;知道服务器上任意一个PHP文件的绝对路径。
首先,FastCGI本质上是一个协议,在CGI的基础上进行了优化。PHP-FPM是实现和管理FastCGI的进程。在PHP-FPM下如果通过FastCGI模式,通信还可分为两种:TCP和UNIX套接字(socket)。
TCP模式是在本机上监听一个端口,默认端口号为9000,Nginx会把客户端数据通过FastCGI协议传给9000端口,PHP-FPM拿到数据后会调用CGI进程解析。
Nginx配置文件如下所示:
PHP-FPM配置如下所示:
既然通过FastCGI与PHP-FPM通信,那么我们可以伪造FastCGI协议包实现PHP任意代码执行。FastCGI协议中只可以传输配置信息、需要被执行的文件名及客户端传进来的GET、POST、Cookie等数据,然后通过更改配置信息来执行任意代码。
在php.ini中有两个非常有用的配置项。
❖ auto_prepend_file:在执行目标文件前,先包含auto_prepend_file中指定的文件,并且可以使用伪协议如php://input。
❖ auto_append_file:在执行目标文件后,包含auto_append_file指向的文件。
php://input是客户端HTTP请求中POST的原始数据,如果将auto_prepend_file设定为php://input,那么每个文件执行前会包含POST的数据,但php://input需要开启allow_url_include,官方手册虽然规定这个配置规定只能在php.ini中修改,但是FastCGI协议中的PHP_ADMIN_VALUE选项可修改几乎所有配置(disable_functions不可修改),通过设置PHP_ADMIN_VALUE把allow_url_include修改为True,这样就可以通过FastCGI协议实现任意代码执行。
使用网上已公开的Exploit,地址如下:
这里需要前面提到的限制条件:需要知道服务器上一个PHP文件的绝对路径,因为在include时会判断文件是否存在,并且security.limit_extensions配置项的后缀名必须为.php,一般可以使用默认的/var/www/html/index.php,如果无法知道Web目录,可以尝试查看PHP默认安装中的文件列表,见图2-1-12。
使用Exploit进行攻击,结果见图2-1-13。
使用nc监听某个端口,获取攻击流量,见图2-1-14。将其中的数据进行URL编码得到:
图2-1-12
图2-1-13
图2-1-14
其攻击结果见图2-1-15。
图2-1-15
4.攻击内网中的脆弱Web应用
内网中的Web应用因为无法被外网的攻击者访问到,所以往往会忽视其安全威胁。
假设内网中存在一个任意命令执行漏洞的Web应用,代码如下:
在本地监听任意端口,然后对此端口发起一次POST请求,以抓取请求数据包,见图2-1-16。
去掉监听的端口号,得到如下数据包:
图2-1-16
将其改成Gopher协议的URL,改变规则同上。执行uname-a命令:
攻击结果见图2-1-17。
图2-1-17
2.1.3.3 自动组装Gopher
目前已经有人总结出多种协议并写出自动转化的脚本,所以大部分情况下不需要再手动进行抓包与转换。推荐工具https://github.com/tarunkant/Gopherus,使用效果见图2-1-18。
2.1.4 SSRF的绕过
SSRF也存在一些WAF绕过场景,本节将简单进行分析。
2.1.4.1 IP的限制
使用Enclosed alphanumerics代替IP中的数字或网址中的字母(见图2-1-19),或者使用句号代替点(见图2-1-20)。
图2-1-18
图2-1-19
图2-1-20
如果服务端过滤方式使用正则表达式过滤属于内网的IP地址,那么可以尝试将IP地址转换为进制的方式进行绕过,如将127.0.0.1转换为十六进制后进行请求,见图2-1-21。
可以将IP地址转换为十进制、八进制、十六进制,分别为2130706433、17700000001、7F000001。在转换后进行请求时,十六进制前需加0x,八进制前需加0,转换为八进制后开头所加的0可以为多个,见图2-1-22。
图2-1-21
图2-1-22
另外,IP地址有一些特殊的写法,如在Windows下,0代表0.0.0.0,而在Linux下,0代表127.0.0.1,见图2-1-23。所以,某些情况下可以用http://0进行请求127.0.0.1。类似127.0.0.1这种中间部分含有0的地址,可以将0省略,见图2-1-24。
2.1.4.2 302跳转
网络上存在一个名叫xip.io的服务,当访问这个服务的任意子域名时,都会重定向到这个子域名,如127.0.0.1.xip.io,见图2-1-25。
图2-1-23
图2-1-24
图2-1-25
这种方式可能存在一个问题,即在传入的URL中存在关键字127.0.0.1,一般会被过滤,那么,我们可以使用短网址将其重定向到指定的IP地址,如短网址http://dwz.cn/11SMa,见图2-1-26。
有时服务端可能过滤了很多协议,如传入的URL中只允许出现“http”或“https”,那么可以在自己的服务器上写一个302跳转,利用Gopher协议攻击内网的Redis,见图2-1-27。
2.1.4.3 URL的解析问题
CTF线上比赛中出现过一些利用组件解析规则不同而导致绕过的题目,代码如下:
图2-1-26
图2-1-27
如果传入的URL为http://a@127.0.0.1:80@baidu.com,那么进入safe_request_url后,parse_url取到的host其实是baidu.com,而curl取到的是127.0.0.1:80,所以实现了检测IP时是正常的一个网站域名而实际curl请求时却是构造的127.0.0.1,以此实现了SSRF攻击,获取flag时的操作见图2-1-28。
除了PHP,不同语言对URL的解析方式各不相同,进一步了解可以参考:https://www.blackhat.com/docs/us-17/thursday/us-17-Tsai-A-New-Era-Of-SSRF-Exploiting-URL-Parser-In-Trending-Programming-Languages.pdf。
图2-1-28
2.1.4.4 DNS Rebinding
在某些情况下,针对SSRF的过滤可能出现下述情况:通过传入的URL提取出host,随即进行DNS解析,获取IP地址,对此IP地址进行检验,判断是否合法,如果检测通过,则再使用curl进行请求。那么,这里再使用curl请求的时候会做第二次请求,即对DNS服务器重新请求,如果在第一次请求时其DNS解析返回正常地址,第二次请求时的DNS解析却返回了恶意地址,那么就完成了DNS Rebinding攻击
DNS重绑定的攻击首先需要攻击者自己有一个域名,通常有两种方式。第一种是绑定两条记录,见图2-1-29。这时解析是随机的,但不一定会交替返回。所以,这种方式需要一定的概率才能成功。
图2-1-29
第二种方式则比较稳定,自己搭建一个DNS Server,在上面运行自编的解析服务,使其每次返回的都不同。
先给域名添加两条解析,一条A记录指向服务器地址,一条NS记录指向上条记录地址。
DNS Server代码如下:
请求结果见图2-1-30。
图2-1-30
图2-1-30(续)
2.1.5 CTF中的SSRF
1.胖哈勃杯第十三届CUIT校赛Web300短域名工具
本题考察的知识点主要是重绑定绕过WAF和DICT协议的利用。PHP的WAF在进行判断时,第一次会解析域名的IP,然后判断是否为内网IP,如果不是,则用CURL去真正请求该域名。这里涉及CURL请求域名的时候会第二次进行解析,重新对DNS服务器进行请求获取一个内网IP,这样就绕过了限制。实际效果见1.3.4.4节。
在题目中,请求http://域名/tools.php?a=s&u=http://ip:88/_testok等价于http://127.0.0.1/tools.php?a=s&u=http://ip:88/_testok;同时,信息搜集可以从phpinfo中获得很多有用的信息,如redis的主机,见图2-1-31。
图2-1-31
另外,libcurl为7.19.7的老版本,只支持TFTP、FTP、Telnet、DICT、HTTP、FILE协议,一般使用Gopher协议攻击Redis,但其实使用DICT协议同样可以攻击Redis,最后的攻击流程如下:
攻击结果见图2-1-32。
图2-1-32
2.护网杯2019 easy_python
2019年护网杯中有一道SSRF攻击Redis的题目。我们赛后模拟了题目进行复盘,当作实例进行分析。
首先,随意登录,发现存在一个flask的session值,登录后为一个请求的功能,随意对自己的VPS进行请求,会得到图2-1-33所示的信息。
关键信息是使用了Python 3和urllib,查看返回包,可以得到如图2-1-34所示的信息。
图2-1-33
图2-1-34
看到返回包中的Nginx,有经验的参赛者会猜到是Nginx配置错误导致目录穿越的漏洞,而题目虽然没有开目录遍历,但是仍然可以构造从/static../__pycache__/获取pyc文件。由于不知道文件名,遍历常用文件名,可以得到main.cpython-37.pyc和views.cpython-37.pyc,见图2-1-35。
图2-1-35
然后对请求功能进行测试,发现不允许请求本机地址,见图2-1-36。
其实这里针对本地的绕过很简单,查看代码发现过滤并不严格,使用0代表本机即可,见图2-1-37。
图2-1-36
图2-1-37
pyc反编译,得到源码后,可知后端存在一个没有密码的Redis,那么明显需要攻击Redis。这里结合之前得到的信息,猜测使用CVE-2019-9740(Python urllib CRLF injection)应该可以实现攻击目的。而这里无法通过常规的攻击方法反弹shell或者直接写webshell,通过阅读flask-session库的代码可知存入的数据是pickle序列化后的字符串,那么我们可以通过这个CRLF漏洞写入一个恶意的序列化字符串,再访问页面触发反弹回shell,写入恶意序列化字符串代码如下:
通过在弹回来的shell中查看信息,可以知道需要进行提权,见图2-1-38。
图2-1-38
拿到shell后,信息搜集发现,Redis是使用root权限启动的,但写SSH私钥和webshell等不太现实,于是考虑可以利用Redis的主从模式(在2019年的WCTF2019 Final上,LC↯BC战队成员在赛后分享上介绍了由于redis的主从复制而导致的新的RCE利用方式)去RCE读flag。
这里介绍Redis的主从模式。Redis为了应对读写量较大的问题,提供了一种主从模式,使用一个Redis实例作为主机只负责写,其余实例都为从机,只负责读,主从机间数据相同,其次在Redis 4.x后新增加了模块的功能,通过外部的扩展可以实现一条新的Redis命令,因为此时已经完全控制了Redis,所以可以通过将此机设置为自己VPS的从机,在主机上通过FULLSYNC同步备份一个恶意扩展到从机上加载。在Github上可以搜到关于该攻击的exp,如https://github.com/n0b0dyCN/redis-rogue-server。
这里因为触发点的原因,不能完全使用上述exp提供的流程去运行。
先在shell中设置为VPS的从机,再设置dbfilename为exp.so,手动执行完exp中的前两步,见图2-1-39。
图2-1-39
然后去掉加载模块后面的所有功能,在VPS上运行exp。最后在Redis上手动执行剩下的步骤,使用扩展提供的功能读取flag即可,见图2-1-40。
图2-1-40