【Java 代码审计入门-04】SSRF 漏洞原理与实际案例介绍
0x00 写在前面
为什么会有这一些列的文章呢?因为我发现网上没有成系列的文章或者教程,基本上是 Java 代码审计中某个点来阐述的,对于新人来说可能不是那么友好,加上本人也在学习 Java 审计,想做个学习历程的记录和总结,因此有了本系列的文章。
本系列的文章面向人群主要是拥有 Java 基本语法基础的朋友,系列文章的内容主要包括,审计环境介绍、SQL 漏洞原理与实际案例介绍、XSS 漏洞原理与实际案例介绍、SSRF 漏洞原理与实际案例介绍、RCE 漏洞原理与实际案例介绍、包含漏洞原理与实际案例介绍、序列化漏洞原理与实际案例介绍、S2系列经典漏洞分析、WebLogic 系列经典漏洞分析、fastjson系列经典漏洞分析、jackson系列经典漏洞分析等,可能内容顺序会略有调整,但是总体内容不会改变,最后希望这系列的文章能够给你带来一点收获。
目前已完成内容如下:
【Java 代码审计入门-01】审计前的准备 【Java 代码审计入门-01】审计前的准备 - Panda | 热爱安全的理想少年
【Java 代码审计入门-02】SQL 漏洞原理与实际案例介绍 【Java 代码审计入门-02】SQL 漏洞原理与实际案例介绍 - Panda | 热爱安全的理想少年
【Java 代码审计入门-03】XSS 漏洞原理与实际案例介绍 【Java 代码审计入门-03】XSS 漏洞原理与实际案例介绍 - Panda | 热爱安全的理想少年
0x01 前戏
下载 SSRF 测试源码:GitHub - cn-panda/JavaCodeAudit: Getting started with java code auditing 代码审计入门的小项目
导入项目,可以得到以下目录:
项目是一个简单模拟HTTP请求的实现。
0x02 漏洞原理
服务端请求伪造(Server-Side Request Forge)简称 SSRF,是OWASP TOP之一,它是由攻击者构造的payload传给服务端,服务端执行后造成了漏洞,一般用于在外网探测或攻击内网服务。Java网络请求支持的协议很多,包括:http,https,file,ftp,mailto,jar,netdoc。如下图所示:
但是和 PHP 相比,java 中的SSRF的利用是有局限性的,实际场景中,一般利用http/https协议来探测端口、暴力穷举等,还可以利用file协议读取/下载任意文件等。
本文针对端口探测和任意文件下载/读取进行了实例说明。
1、端口探测
String url = request.getParameter("url");
String htmlContent;
try {
URL u = new URL(url);
URLConnection urlConnection = u.openConnection();
HttpURLConnection httpUrl = (HttpURLConnection) urlConnection;
BufferedReader base = new BufferedReader(new InputStreamReader(httpUrl.getInputStream(), "UTF-8"));
StringBuffer html = new StringBuffer();
while ((htmlContent = base.readLine()) != null) {
html.append(htmlContent);
}
base.close();
print.println("<b>端口探测</b></br>");
print.println("<b>url:" + url + "</b></br>");
print.println(html.toString());
print.flush();
} catch (Exception e) {
e.printStackTrace();
print.println("ERROR!");
print.flush();
}
以上代码大致意义如下:
- URL对象用
openconnection()
打开连接,获得URLConnection类对象。 - 用
InputStream()
获取字节流 - 然后
InputStreamReader()
将字节流转化成字符流 BufferedReader()
将字符流以缓存形式输出的方式来快速获取网络数据流- 最终一行一行的输入到 html 变量中,输出到浏览器
代码的主要功能即是模拟一个 http 请求,如果没有对请求地址进行限制和过滤,即可以利用来进行 SSRF 攻击。
本机环境如下:
地址:127.0.0.1
环境: java+tomcat
虚拟机环境如下:
地址:192.168.159.134
环境:php+apache
假设外网可以访问本机地址,但不能访问虚拟机地址。
以上,因为本机地址存在 SSRF 漏洞,那么久可以利用该漏洞去探测虚拟机开放的端口,如下图所示:
如果该端口没有开放 http/https 协议,那么返回:
根据不同的返回结果,就可以判断开放的 http/https 端口
2、任意文件读取/下载
我们将上述代码删除一行,如下:
String url = request.getParameter("url");
String htmlContent;
try {
URL u = new URL(url);
URLConnection urlConnection = u.openConnection();
BufferedReader base = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
StringBuffer html = new StringBuffer();
while ((htmlContent = base.readLine()) != null) {
html.append(htmlContent);
}
base.close();
print.println(html.toString());
print.flush();
} catch (Exception e) {
e.printStackTrace();
print.println("ERROR!");
print.flush();
}
HttpURLconnection()
是基于http协议的,而我们要用的是 file
协议,删除后即可利用file
协议去读取任意文件 ,如下图所示:
如果我们知道了网站的路径,可以直接读取其数据库连接的相关信息:
任意文件下载同理,只不过是将数据流写入到了文件中,如下代码:
String downLoadImgFileName = "SsrfFileDownTest.txt";
InputStream inputStream = null;
OutputStream outputStream = null;
String url = req.getParameter("url");
try {
resp.setHeader("content-disposition", "attachment;fileName=" + downLoadImgFileName);
URL file = new URL(url);
byte[] bytes = new byte[1024];
inputStream = file.openStream();
outputStream = resp.getOutputStream();
while ((length = inputStream.read(bytes)) > 0) {
outputStream.write(bytes, 0, length);
}
}
将获取的内容写入到SsrfFileDownTest.txt
文件中,测试如下:
0x03 修复方案
实际场景中可能出现 SSRF 的功能点有很多,比如获取远程 URL 图片、webmail收取其他邮箱邮件、从远程服务器请求资源等等,针对这些问题,可以进行过滤判断,设置白名单等,相关策略如下:
- 统一错误信息,避免用户可以根据错误信息来判断远端服务器的端口状态。
- 限制请求的端口为http常用的端口,比如,80,443,8080,8090等。
- 禁用不需要的协议,仅仅允许http和https请求。
- 根据业务需求,判定所需的域名是否是常用的几个,若是,将这几个特定的域名加入到白名单,拒绝白名单域名之外的请求,。
- 根据请求来源,判定请求地址是否是固定请求来源,若是,将这几个特定的域名/IP加入到白名单,拒绝白名单域名/IP之外的请求。
- 若业务需求和请求来源并非固定,那么可以自己写一个 ssrfCheck 函数,如:https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/security/SSRFChecker.java
0x04 实际案例(CVE-2019-9827)分析
1、案例介绍
CVE 地址:CVE -CVE-2019-9827
Hawtio是用于管理Java应用程序的轻型模块化Web控制台。Hawt Hawtio小于2.5.0版本都容易受到SSRF的攻击,远程攻击者可以通过 /proxy/地址发送特定的字符串,可以影响服务器到任意主机的HTTP请求。
2、案例搭建
首先进入搭建好的 tomcat 首页,输入账号密码进入manage app 管理界面(需要提前设置账号密码,具体可以百度,此处不再赘述):
然后选择WAR file to deply
栏目,点击选择hawtio-default-2.5.0.war
上传,最后deplay即可:
布置以后,上方会出现布置好的应用,点击应用进入即可。
3、案例漏洞分析
可以通过反编译获取本程序的源码,或者通过 github 的 tree 分支来获取源码。
通过反编译hawtio-system-2.5.0.jar
包找到相关文件:hawtio-system/src/main/java/io/hawt/web/proxy/ProxyServlet.java
进入service
函数
protected void service(HttpServletRequest servletRequest, HttpServletResponse servletResponse)
throws ServletException, IOException {
// Make the Request
//note: we won't transfer the protocol version because I'm not sure it would truly be compatible
ProxyAddress proxyAddress = parseProxyAddress(servletRequest);
if (proxyAddress == null || proxyAddress.getFullProxyUrl() == null) {
servletResponse.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
// TODO Implement whitelist protection for Kubernetes services as well
if (proxyAddress instanceof ProxyDetails) {
ProxyDetails details = (ProxyDetails) proxyAddress;
if (!whitelist.isAllowed(details)) {
LOG.debug("Rejecting {}", proxyAddress);
ServletHelpers.doForbidden(servletResponse, ForbiddenReason.HOST_NOT_ALLOWED);
return;
}
}
通过parseProxyAddress
函数获取 URL 地址,然后判断其是否为空,如果不为空,通过whitelist.isAllowed()
判断该 URL 是否在白名单里,跟进 whitelist
:
public ProxyWhitelist(String whitelistStr, boolean probeLocal) {
if (Strings.isBlank(whitelistStr)) {
whitelist = new CopyOnWriteArraySet<>();
regexWhitelist = Collections.emptyList();
} else {
whitelist = new CopyOnWriteArraySet<>(filterRegex(Strings.split(whitelistStr, ",")));
regexWhitelist = buildRegexWhitelist(Strings.split(whitelistStr, ","));
}
if (probeLocal) {
LOG.info("Probing local addresses ...");
initialiseWhitelist();
} else {
LOG.info("Probing local addresses disabled");
whitelist.add("localhost");
whitelist.add("127.0.0.1");
}
LOG.info("Initial proxy whitelist: {}", whitelist);
mBeanServer = ManagementFactory.getPlatformMBeanServer();
try {
fabricMBean = new ObjectName(FABRIC_MBEAN);
} catch (MalformedObjectNameException e) {
throw new RuntimeException(e);
}
}
...
public boolean isAllowed(ProxyDetails details) {
if (details.isAllowed(whitelist)) {
return true;
}
// Update whitelist and check again
LOG.debug("Updating proxy whitelist: {}, {}", whitelist, details);
if (update() && details.isAllowed(whitelist)) {
return true;
}
// test against the regex as last resort
if (details.isAllowed(regexWhitelist)) {
return true;
} else {
return false;
}
}
public boolean update() {
if (!mBeanServer.isRegistered(fabricMBean)) {
LOG.debug("Whitelist MBean not available");
return false;
}
Set<String> newWhitelist = invokeMBean();
int previousSize = whitelist.size();
whitelist.addAll(newWhitelist);
if (whitelist.size() == previousSize) {
LOG.debug("No new proxy whitelist to update");
return false;
} else {
LOG.info("Updated proxy whitelist: {}", whitelist);
return true;
}
}
判断 URL 是否为 localhost、127.0.0.1或者用户自己更新的白名单列表,如果不是返回 false。
返回到 service()
,向下走:
if (servletRequest.getHeader(HttpHeaders.CONTENT_LENGTH) != null ||
servletRequest.getHeader(HttpHeaders.TRANSFER_ENCODING) != null) {
HttpEntityEnclosingRequest eProxyRequest = new BasicHttpEntityEnclosingRequest(method, proxyRequestUri);
// Add the input entity (streamed)
// note: we don't bother ensuring we close the servletInputStream since the container handles it
eProxyRequest.setEntity(new InputStreamEntity(servletRequest.getInputStream(), servletRequest.getContentLength()));
proxyRequest = eProxyRequest;
} else {
proxyRequest = new BasicHttpRequest(method, proxyRequestUri);
}
copyRequestHeaders(servletRequest, proxyRequest, targetUriObj);
BasicHttpEntityEnclosingRequest()
拥有RequestLine
、HttpEntity
以及Header
,这里用的是 entity,HttpEntity即为消息体,包含了三种类型:数据流方式、自我包含方式以及封装模式(包含上述两种方式),这里就是一个基于HttpEntity的, HttpRequest接口实现,类似于上文中的urlConnection。
所以这个 service()
的主要作用就是获取请求,然后HttpService
把HttpClient
传来的请求通过向下转型成BasicHttpEntityEnclosingRequest
,再调用HttpEntity
,最终得到请求流内容。
这里虽然对传入的 URL 进行了限制,但是没有对端口、协议进行相应的限制,从而导致了 SSRF 漏洞。
证明如下:
4、修复方案
通过比对最新版的源码,发现该漏洞的修复方式为加了页面访问权限,如下图:
未经验证的用户禁止访问该页面,测试如下:
0x05 总结
本文主要讨论了 Java 中的 SSRF 漏洞,包括其原理、简单的 Java 代码示例、修复方案以及 CVE 实例,希望对初入Java代码审计的朋友有所帮助。另外对于 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
0x06 参考
jdk8u-jdk/src/share/classes/sun/net/www/protocol at master · frohoff/jdk8u-jdk · GitHub
papers/build_your_ssrf_exp_autowork--20160711.pdf at master · ring04h/papers · GitHub
SSRF安全威胁在JAVA代码中的应用 - RunforLove - 博客园
GitHub - hawtio/hawtio at hawtio-2.5.0
apache- httpcomponents-core4.4.6 学习笔记_dosendrequest 和 doreceiveresponse-CSDN博客
Comparing hawtio-2.5.0...hawtio-2.9.1 · hawtio/hawtio · GitHub
SSRF 漏洞原理的详细探讨
场景设定
假设我们有一个Java Web应用程序,它允许用户输入URL地址来获取远程资源。例如,一个功能可能是让用户输入图片链接以显示该图片。如果开发者没有正确地验证或限制用户的输入,那么攻击者就可以利用这个功能来进行恶意操作。
实现案例中的风险点
- 端口探测
-
- 在给定的例子中,代码接收了一个
url
参数并直接用它创建了URL
对象。由于没有对输入做任何过滤或验证,这意味着攻击者可以构造如http://192.168.1.1:8080/
这样的请求来尝试访问内网服务。 - 这种行为可能被用来扫描内部网络的服务端口,特别是当外部网络无法直接访问这些服务时。
- 在给定的例子中,代码接收了一个
- 任意文件读取/下载
-
- 如果允许使用
file://
协议,攻击者可以构造类似file:///etc/passwd
的路径来读取服务器上的敏感文件。 - 即使是简单的HTTP请求处理不当,也可能导致下载任意文件的风险,比如通过设置响应头让浏览器下载特定文件。
- 如果允许使用
- 缺乏适当的错误处理
-
- 当请求失败时,程序返回了详细的错误信息(如堆栈跟踪),这可能会泄露有关服务器配置的信息,进一步帮助攻击者进行攻击。
补充的相关知识与最佳实践
- 协议与端口控制
-
- Java应用程序通常支持多种协议,但并非所有都是安全的。应该只启用必要的协议(如HTTP/HTTPS),并且严格限制可访问的端口范围。
- 输入验证
-
- 对所有来自用户的输入都要进行严格的验证,确保它们符合预期格式,并且不会引入额外的安全风险。
- 白名单策略
-
- 使用白名单来限定允许访问的目标主机和IP地址。只有经过明确许可的地址才能被接受作为请求的一部分。
- 日志记录与监控
-
- 记录所有的外部请求,包括源IP、目标URL等信息,以便事后分析和检测异常活动。
- 最小权限原则
-
- 应用程序应以最低权限运行,即使存在漏洞也能减少潜在损害的程度。
- 更新依赖库
-
- 定期检查并更新所使用的第三方库,因为旧版本可能存在已知的安全漏洞。
实际案例 (CVE-2019-9827) 的深度解析
Hawtio SSRF 漏洞分析
Hawtio是一个用于管理Java应用程序的Web控制台。在其早期版本中,存在一个名为ProxyServlet
的组件,它负责代理HTTP请求。然而,这个组件未能充分验证传入的URL,从而允许攻击者发送特制的字符串到/proxy/
路径,使得服务器能够向任意主机发起HTTP请求。
修复措施
- 增加身份验证:确保只有经过认证的用户才能触发代理请求。
- 加强URL验证:引入了更严格的规则来检查URL是否合法,并且只允许访问预定义的白名单内的地址。
- 改进日志记录:增强了日志系统,以便更好地跟踪和审计每一次代理请求。
高级防御策略
细粒度白名单管理
- 动态调整:基于业务逻辑的变化或新发现的安全威胁,动态更新白名单规则。
- 上下文感知:根据用户角色、操作类型等因素来决定允许访问哪些URL,实现更加灵活和智能的控制。
- 机器学习辅助:利用机器学习算法分析历史流量数据,自动识别并添加可信的新IP地址或域名到白名单中。
引入服务网格安全性
- mTLS (相互传输层安全):确保所有微服务之间的通信都是加密且经过验证的,防止中间人攻击。
- 流量拦截与分析:利用Istio等服务网格平台提供的功能,监控并分析进出每个服务的流量,及时检测异常行为。
- 策略执行点分离:将安全策略的定义和执行分离,使得安全团队可以独立于开发团队进行策略调整,提高了响应速度。
实施API网关保护
- 统一入口点:将所有对外暴露的服务接口集中到一个API网关上进行管理和保护。
- 速率限制与配额管理:设置每秒请求数量上限及每日总请求数量,避免恶意扫描或其他滥用行为。
- 自定义认证机制:为API网关配置OAuth2.0或其他强认证方式,确保只有授权用户才能访问敏感资源。
SSRF 攻击技术的发展
内部网络探测技巧
- IP地址枚举:通过构造特殊的DNS查询或者HTTP请求,尝试解析内部网络中的主机名/IP地址。
- 端口扫描优化:结合多线程技术和异步I/O模型,快速高效地完成大量端口的状态检查。
- 时间差异分析:测量不同请求之间的时间差,推测目标系统是否对某些特定的内部资源进行了处理。
旁路绕过防护机制
- 协议混淆:使用非标准端口或自定义协议来规避传统的端口过滤规则。
- 代理链路构建:创建多级代理服务器链路,使得最终目标难以被直接追踪到。
- HTTP头部注入:利用HTTP头部注入技术,改变请求的行为或路径,从而绕过现有的防护措施。
云原生环境下的SSRF挑战
容器化应用的安全性
- 网络命名空间隔离:确保容器之间拥有独立的网络栈,即使发生SSRF也无法轻易突破容器边界。
- CNI插件选择:选用具备严格访问控制能力的容器网络接口(CNI)插件,增强网络层面的安全性。
- 镜像签名验证:在部署前对容器镜像进行数字签名验证,确保其来源可靠,未被篡改。
Serverless架构中的考虑
- 函数权限最小化:为每个无服务器函数分配尽可能小的执行权限,减少因SSRF导致的数据泄露风险。
- 冷启动时间影响:考虑到Serverless函数可能存在的冷启动延迟,在设计时应尽量缩短初始化过程所需的时间,降低被攻击窗口期。
- 事件驱动型安全监控:基于事件触发的安全监控机制,能够实时响应潜在的SSRF活动,并采取相应的措施。
自动化安全工具的应用
源代码静态分析工具
- 规则引擎定制化:针对特定框架或语言特性编写专属的安全检查规则,提高发现问题的概率。
- 持续集成/持续交付(CI/CD)集成:将静态分析工具无缝融入CI/CD管道中,保证每次代码变更都能得到及时审查。
- 深度依赖分析:不仅检查直接使用的库,还递归分析所有间接依赖项,确保整个依赖树的安全性。
动态应用程序安全测试(DAST)
- 模拟真实攻击场景:通过重现常见的攻击向量,如SQL注入、XSS、SSRF等,评估应用的安全状况。
- 性能与准确性平衡:寻找既能覆盖广泛又不影响生产环境稳定性的测试方法,确保结果的有效性和可靠性。
- 模糊测试(Fuzzing):采用随机输入生成的方式,试图引发程序崩溃或不正确行为,以此发现潜在漏洞。
SSRF 审计的最佳实践
知识共享与社区协作
- 参与开源项目贡献:加入相关的开源社区,共同维护和完善各种安全工具和资源库。
- 定期参加培训课程:跟上最新的安全趋势和技术发展,不断更新自己的技能集。
- 撰写博客文章或教程:分享个人经验和见解,帮助更多开发者了解并掌握SSRF防范技巧。
构建内部安全文化
- 鼓励报告潜在问题:建立奖励机制,激励员工主动发现并上报安全隐患。
- 组织内部竞赛活动:举办黑客马拉松等形式的比赛,激发团队成员对安全的兴趣和热情。
- 设立红蓝队对抗演练:定期开展攻防演练,模拟真实的网络攻击情境,提升全员的安全意识和应急响应能力。
案例分析
案例一:Fastjson 反序列化漏洞中的SSRF风险
Fastjson是中国广泛使用的JSON库之一,但过去曾暴露出严重的反序列化漏洞,这些漏洞有时也会伴随SSRF风险。例如,在某些版本中,如果攻击者能够控制输入的数据结构,他们不仅可以执行任意代码,还可以构造出含有恶意URL的请求,进而发起SSRF攻击。修复这类问题的关键在于严格限制可反序列化的对象类型,并且对任何外部传入的URL进行彻底验证。
案例二:Hawtio SSRF 漏洞的实际影响
正如之前提到过的CVE-2019-9827,Hawtio这个用于Java应用管理的Web控制台存在SSRF漏洞。攻击者可以通过/proxy/
路径发送特制字符串,让服务器向任意主机发起HTTP请求。该漏洞的影响包括但不限于:
- 内网扫描:攻击者可以利用此漏洞扫描内部网络的服务端口。
- 敏感信息泄露:读取服务器上的文件,如数据库配置文件,可能导致敏感信息泄露。
- 横向移动:一旦获取了内部网络的信息,攻击者可能会进一步渗透其他系统。
通过对上述案例的学习,我们可以更好地理解SSRF漏洞的实际危害,并从中吸取教训,强化我们的防御措施。
总结
通过对SSRF漏洞的深入研究,我们可以看到它是一个复杂且多维度的问题,涉及从编码习惯到部署运维等多个方面。采用多层次的安全防御策略,结合先进的技术和工具,可以有效地降低SSRF带来的风险。同时,持续学习最新的安全研究成果和技术进展,保持警惕并积极应对新的威胁,是保障系统长期安全的关键所在。