JavaWeb中的SSRF审计
漏洞原理
漏洞成因
web应用提供了从其他的服务器上获取数据的功能。例如通过用户指定的URL,web应用可以获取图片,下载文件,读取文件内容等。如果存在缺陷,可以利用存在缺陷的web应用作为代理攻击远程和本地的服务器。
支持的协议
Java网络请求支持的协议有:
审计思路
主要关注可以发起网络请求的方法和对应的业务。
业务定位
常见的从服务端获取其他服务器信息的的功能:
- 通过URL地址分享网页内容
- 在线转码服务
- 在线翻译
- 通过URL地址加载或下载图片
- app更新时从远端服务器下载更新包、皮肤等
- 加载远端配置的文件
- …
常见class
- HttpClient
- HttpURLConnection
- URLConnection
- URL
- okhttp
- …
发起网络请求的类是带HTTP开头的,那只支持HTTP、HTTPS协议。URL和URLConnection是支持sun.net.www.protocol
所有协议的。例如如果URL的参数可控的话,可以尝试使用file协议读取系统文件:
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
public class UrlClassTest {
public static void main(String[] args) {
URL u;
try {
u = new URL("file:///etc/passwd");
URLConnection uc = u.openConnection();
InputStream in = uc.getInputStream();
byte[] b = new byte[1024];
int len;
while ((len = in.read(b)) != -1) {
System.out.println(new String(b, 0, len));
}
in.close();
} catch (Exception e) {
}
}
}
相关案例
以UEditor的SSRF漏洞为例,查看commit发现1.4.3.1版本修复了SSRF漏洞,也就是说1.4.3及之前版本都存在问题:
本身该版本是存在一定的防护的,在config.json的catcherLocalDomain参数可以配置filter的黑名单:
"catcherLocalDomain": ["127.0.0.1", "localhost", "img.baidu.com"],
在实际运行时,通过ConfigManager加载对应的配置,将catcherLocalDomain参数对应的值存储到Map对象中:
case 5:
conf.put("filename", "remote");
conf.put("filter", getArray("catcherLocalDomain"));
conf.put("maxSize", Long.valueOf(this.jsonConfig.getLong("catcherMaxSize")));
conf.put("allowFiles", getArray("catcherAllowFiles"));
conf.put("fieldName", this.jsonConfig.getString("catcherFieldName") + "[]");
savePath = this.jsonConfig.getString("catcherPathFormat");
break;
到实际请求远端图片的com.baidu.ueditor.hunter.ImageHunter.java来看,发起网络请求的方法主要是captureRemoteData():
public State captureRemoteData(String urlStr)
{
HttpURLConnection connection = null;
URL url = null;
String suffix = null;
try
{
url = new URL(urlStr);
if (!validHost(url.getHost())) {
return new BaseState(false, 201);
}
connection = (HttpURLConnection)url.openConnection();
connection.setInstanceFollowRedirects(true);
connection.setUseCaches(true);
if (!validContentState(connection.getResponseCode())) {
return new BaseState(false, 202);
}
suffix = MIMEType.getSuffix(connection.getContentType());
if (!validFileType(suffix)) {
return new BaseState(false, 8);
}
if (!validFileSize(connection.getContentLength())) {
return new BaseState(false, 1);
}
String savePath = getPath(this.savePath, this.filename, suffix);
String physicalPath = this.rootPath + savePath;
State state = StorageManager.saveFileByInputStream(connection.getInputStream(), physicalPath);
if (state.isSuccess())
{
state.putInfo("url", PathFormat.format(savePath));
state.putInfo("source", urlStr);
}
return state;
}
catch (Exception e) {}
return new BaseState(false, 203);
}
可以看到,通过validHost()方法进行判断是否是合法的host,然后通过HttpURLConnection的方法进行网络请求,validHost()方法是之前查看filters中是否包含当前域名,如果包含则返回false,拒绝发起请求。并且filters的值是通过前面ConfigManager的conf获得的:
public ImageHunter(Map<String, Object> conf)
{
this.filename = ((String)conf.get("filename"));
this.savePath = ((String)conf.get("savePath"));
this.rootPath = ((String)conf.get("rootPath"));
this.maxSize = ((Long)conf.get("maxSize")).longValue();
this.allowTypes = Arrays.asList((String[])conf.get("allowFiles"));
this.filters = Arrays.asList((String[])conf.get("filter"));
}
private boolean validHost(String hostname)
{
return !this.filters.contains(hostname);
}
也就是说,具体防御还是依赖于config.json,具体效果如下,正常情况下,访问外网dnslog是没问题的(不在默认的黑名单内):
尝试请求127.0.0.1,此时匹配默认的黑名单,拒绝发起网络请求:
因为只是contains匹配,所以很简单就可以绕过了,这里用127.0.0.1.xip.io代替127.0.0.1,成功绕过黑名单发起网络请求:
同理,因为是默认黑名单,且黑名单覆盖面不全很容易导致绕过,这里可以直接修改成内网地址尝试进行端口探测等攻击利用行为。
再看看1.4.3.1版本是怎么修复的,主要的改动还是在validHost方法,通过InetAddress对象的isSiteLocalAddress()方法进行判断,禁止内网地址的网络请求。
private boolean validHost(String hostname)
{
try
{
InetAddress ip = InetAddress.getByName(hostname);
if (ip.isSiteLocalAddress()) {
return false;
}
}
catch (UnknownHostException e)
{
return false;
}
return !this.filters.contains(hostname);
}
查看文档,isSiteLocalAddress方法作用是当IP地址是地区本地地址(SiteLocalAddress)时返回true,否则返回false。
如果是IPv4地址,主要是这三个段:10.0.0.0~ 10.255.255.255、172.16.0.0 ~ 172.31.255.255、192.168.0.0 ~192.168.255.255。简单的对192.168.0.1fuzz一下:
- 8进制格式:0300.0250.0.1
- 16进制格式:0xC0.0xA8.0.1
- 10进制整数格式:3232235521
- 16进制整数格式:0xC0A80001
- www.baidu.com@192.168.0.1
结果如下:
修复方案
-
使用白名单校验HTTP请求url地址,例如通过InetAddress对象的isSiteLocalAddress()方法进行判断,禁止内网地址的网络请求。。
-
避免将请求响应及错误信息返回给用户。
-
禁用不需要的协议及限制请求端口,仅仅允许http和https请求等。
<h1><a id="JavaWebSSRF_0"></a>JavaWeb中的SSRF审计</h1>
<h2><a id="_2"></a>漏洞原理</h2>
<h3><a id="_4"></a>漏洞成因</h3>
<p> web应用提供了从其他的服务器上获取数据的功能。例如通过用户指定的URL,web应用可以获取图片,下载文件,读取文件内容等。如果存在缺陷,可以利用存在缺陷的web应用作为代理攻击远程和本地的服务器。</p>
<h3><a id="_8"></a>支持的协议</h3>
<p> Java网络请求支持的协议有:</p>
<p><img src="https://www.redhatzone.com/img/sin/M00/00/19/wKg0C16OjhSABl1sAACj5f2GCtg232.png" alt="ssrfCode_1.png" /></p>
<h2><a id="_14"></a>审计思路</h2>
<p> 主要关注可以发起网络请求的方法和对应的业务。</p>
<h3><a id="_18"></a>业务定位</h3>
<p> 常见的从服务端获取其他服务器信息的的功能:</p>
<ul>
<li>通过URL地址分享网页内容</li>
<li>在线转码服务</li>
<li>在线翻译</li>
<li>通过URL地址加载或下载图片</li>
<li>app更新时从远端服务器下载更新包、皮肤等</li>
<li>加载远端配置的文件</li>
<li>…</li>
</ul>
<h3><a id="class_30"></a>常见class</h3>
<ul>
<li>HttpClient</li>
<li>HttpURLConnection</li>
<li>URLConnection</li>
<li>URL</li>
<li>okhttp</li>
<li>…</li>
</ul>
<p> 发起网络请求的类是带HTTP开头的,那只支持HTTP、HTTPS协议。URL和URLConnection是支持<code>sun.net.www.protocol</code>所有协议的。例如如果URL的参数可控的话,可以尝试使用file协议读取系统文件:</p>
<pre><div class="hljs"><code class="lang-java"><span class="hljs-keyword">import</span> java.io.InputStream;
<span class="hljs-keyword">import</span> java.net.URL;
<span class="hljs-keyword">import</span> java.net.URLConnection;
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-">UrlClassTest</span> </span>{
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-">main</span><span class="hljs-params">(String[] args)</span> </span>{
URL u;
<span class="hljs-keyword">try</span> {
u = <span class="hljs-keyword">new</span> URL(<span class="hljs-string">"file:///etc/passwd"</span>);
URLConnection uc = u.openConnection();
InputStream in = uc.getInputStream();
<span class="hljs-keyword">byte</span>[] b = <span class="hljs-keyword">new</span> <span class="hljs-keyword">byte</span>[<span class="hljs-number">1024</span>];
<span class="hljs-keyword">int</span> len;
<span class="hljs-keyword">while</span> ((len = in.read(b)) != -<span class="hljs-number">1</span>) {
System.out.println(<span class="hljs-keyword">new</span> String(b, <span class="hljs-number">0</span>, len));
}
in.close();
} <span class="hljs-keyword">catch</span> (Exception e) { <span class="hljs-comment">// TODO Auto-generated catch block e.printStackTrace(); };</span>
}
}
}
</code></div></pre>
<p><img src="https://www.redhatzone.com/img/sin/M00/00/19/wKg0C16OjieAUCEQAAF1SMKGo6o203.png" alt="ssrfCode_2.png" /></p>
<h2><a id="_67"></a>相关案例</h2>
<p> 以UEditor的SSRF漏洞为例,查看commit发现1.4.3.1版本修复了SSRF漏洞,也就是说1.4.3及之前版本都存在问题:</p>
<p><img src="https://www.redhatzone.com/img/sin/M00/00/19/wKg0C16OjjOAFkPPAABDOjr_CeM211.png" alt="ssrfCode_3.png" /></p>
<p> 本身该版本是存在一定的防护的,在config.json的catcherLocalDomain参数可以配置filter的黑名单:</p>
<pre><div class="hljs"><code class="lang-json"><span class="hljs-string">"catcherLocalDomain"</span>: [<span class="hljs-string">"127.0.0.1"</span>, <span class="hljs-string">"localhost"</span>, <span class="hljs-string">"img.baidu.com"</span>],
</code></div></pre>
<p> 在实际运行时,通过ConfigManager加载对应的配置,将catcherLocalDomain参数对应的值存储到Map对象中:</p>
<pre><div class="hljs"><code class="lang-java"><span class="hljs-keyword">case</span> <span class="hljs-number">5</span>:
conf.put(<span class="hljs-string">"filename"</span>, <span class="hljs-string">"remote"</span>);
conf.put(<span class="hljs-string">"filter"</span>, getArray(<span class="hljs-string">"catcherLocalDomain"</span>));
conf.put(<span class="hljs-string">"maxSize"</span>, Long.valueOf(<span class="hljs-keyword">this</span>.jsonConfig.getLong(<span class="hljs-string">"catcherMaxSize"</span>)));
conf.put(<span class="hljs-string">"allowFiles"</span>, getArray(<span class="hljs-string">"catcherAllowFiles"</span>));
conf.put(<span class="hljs-string">"fieldName"</span>, <span class="hljs-keyword">this</span>.jsonConfig.getString(<span class="hljs-string">"catcherFieldName"</span>) + <span class="hljs-string">"[]"</span>);
savePath = <span class="hljs-keyword">this</span>.jsonConfig.getString(<span class="hljs-string">"catcherPathFormat"</span>);
<span class="hljs-keyword">break</span>;
</code></div></pre>
<p> 到实际请求远端图片的com.baidu.ueditor.hunter.ImageHunter.java来看,发起网络请求的方法主要是captureRemoteData():</p>
<pre><div class="hljs"><code class="lang-java"><span class="hljs-function"><span class="hljs-keyword">public</span> State <span class="hljs-">captureRemoteData</span><span class="hljs-params">(String urlStr)</span>
</span>{
HttpURLConnection connection = <span class="hljs-keyword">null</span>;
URL url = <span class="hljs-keyword">null</span>;
String suffix = <span class="hljs-keyword">null</span>;
<span class="hljs-keyword">try</span>
{
url = <span class="hljs-keyword">new</span> URL(urlStr);
<span class="hljs-keyword">if</span> (!validHost(url.getHost())) {
<span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> BaseState(<span class="hljs-keyword">false</span>, <span class="hljs-number">201</span>);
}
connection = (HttpURLConnection)url.openConnection();
connection.setInstanceFollowRedirects(<span class="hljs-keyword">true</span>);
connection.setUseCaches(<span class="hljs-keyword">true</span>);
<span class="hljs-keyword">if</span> (!validContentState(connection.getResponseCode())) {
<span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> BaseState(<span class="hljs-keyword">false</span>, <span class="hljs-number">202</span>);
}
suffix = MIMEType.getSuffix(connection.getContentType());
<span class="hljs-keyword">if</span> (!validFileType(suffix)) {
<span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> BaseState(<span class="hljs-keyword">false</span>, <span class="hljs-number">8</span>);
}
<span class="hljs-keyword">if</span> (!validFileSize(connection.getContentLength())) {
<span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> BaseState(<span class="hljs-keyword">false</span>, <span class="hljs-number">1</span>);
}
String savePath = getPath(<span class="hljs-keyword">this</span>.savePath, <span class="hljs-keyword">this</span>.filename, suffix);
String physicalPath = <span class="hljs-keyword">this</span>.rootPath + savePath;
State state = StorageManager.saveFileByInputStream(connection.getInputStream(), physicalPath);
<span class="hljs-keyword">if</span> (state.isSuccess())
{
state.putInfo(<span class="hljs-string">"url"</span>, PathFormat.format(savePath));
state.putInfo(<span class="hljs-string">"source"</span>, urlStr);
}
<span class="hljs-keyword">return</span> state;
}
<span class="hljs-keyword">catch</span> (Exception e) {}
<span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> BaseState(<span class="hljs-keyword">false</span>, <span class="hljs-number">203</span>);
}
</code></div></pre>
<p> 可以看到,通过validHost()方法进行判断是否是合法的host,然后通过HttpURLConnection的方法进行网络请求,validHost()方法是之前查看filters中是否包含当前域名,如果包含则返回false,拒绝发起请求。并且filters的值是通过前面ConfigManager的conf获得的:</p>
<pre><div class="hljs"><code class="lang-java"> <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-">ImageHunter</span><span class="hljs-params">(Map<String, Object> conf)</span>
</span>{
<span class="hljs-keyword">this</span>.filename = ((String)conf.get(<span class="hljs-string">"filename"</span>));
<span class="hljs-keyword">this</span>.savePath = ((String)conf.get(<span class="hljs-string">"savePath"</span>));
<span class="hljs-keyword">this</span>.rootPath = ((String)conf.get(<span class="hljs-string">"rootPath"</span>));
<span class="hljs-keyword">this</span>.maxSize = ((Long)conf.get(<span class="hljs-string">"maxSize"</span>)).longValue();
<span class="hljs-keyword">this</span>.allowTypes = Arrays.asList((String[])conf.get(<span class="hljs-string">"allowFiles"</span>));
<span class="hljs-keyword">this</span>.filters = Arrays.asList((String[])conf.get(<span class="hljs-string">"filter"</span>));
}
<span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">boolean</span> <span class="hljs-">validHost</span><span class="hljs-params">(String hostname)</span>
</span>{
<span class="hljs-keyword">return</span> !<span class="hljs-keyword">this</span>.filters.contains(hostname);
}
</code></div></pre>
<p> 也就是说,具体防御还是依赖于config.json,具体效果如下,正常情况下,访问外网dnslog是没问题的(不在默认的黑名单内):</p>
<p><img src="https://www.redhatzone.com/img/sin/M00/00/19/wKg0C16OjkOATYX0AACNx_RCNEM095.png" alt="ssrfCode_4.png" /></p>
<p> 尝试请求127.0.0.1,此时匹配默认的黑名单,拒绝发起网络请求:</p>
<p><img src="https://www.redhatzone.com/img/sin/M00/00/19/wKg0C16OjkmAfeQNAACOcpAE2fM556.png" alt="ssrfCode_5.png" /></p>
<p> 因为只是contains匹配,所以很简单就可以绕过了,这里用127.0.0.1.xip.io代替127.0.0.1,成功绕过黑名单发起网络请求:</p>
<p><img src="https://www.redhatzone.com/img/sin/M00/00/19/wKg0C16OjlKAfgmaAACNIK2V6Nk222.png" alt="ssrfCode_6.png" /></p>
<p> 同理,因为是默认黑名单,且黑名单覆盖面不全很容易导致绕过,这里可以直接修改成内网地址尝试进行端口探测等攻击利用行为。</p>
<p><img src="https://www.redhatzone.com/img/sin/M00/00/19/wKg0C16OjlmAOt6HAACdMRxSBBQ497.png" alt="ssrfCode_7.png" /></p>
<p> 再看看1.4.3.1版本是怎么修复的,主要的改动还是在validHost方法,通过InetAddress对象的isSiteLocalAddress()方法进行判断,禁止内网地址的网络请求。</p>
<pre><div class="hljs"><code class="lang-java"><span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">boolean</span> <span class="hljs-">validHost</span><span class="hljs-params">(String hostname)</span>
</span>{
<span class="hljs-keyword">try</span>
{
InetAddress ip = InetAddress.getByName(hostname);
<span class="hljs-keyword">if</span> (ip.isSiteLocalAddress()) {
<span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
}
}
<span class="hljs-keyword">catch</span> (UnknownHostException e)
{
<span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
}
<span class="hljs-keyword">return</span> !<span class="hljs-keyword">this</span>.filters.contains(hostname);
}
</code></div></pre>
<p> 查看文档,isSiteLocalAddress方法作用是当IP地址是地区本地地址(SiteLocalAddress)时返回true,否则返回false。</p>
<p><img src="https://www.redhatzone.com/img/sin/M00/00/19/wKg0C16OjmOAA0kdAACG8vxR51U842.png" alt="ssrfCode_8.png" /></p>
<p> 如果是IPv4地址,主要是这三个段:10.0.0.0~ 10.255.255.255、172.16.0.0 ~ 172.31.255.255、192.168.0.0 ~192.168.255.255。简单的对192.168.0.1fuzz一下:</p>
<ul>
<li>8进制格式:0300.0250.0.1</li>
<li>16进制格式:0xC0.0xA8.0.1</li>
<li>10进制整数格式:3232235521</li>
<li>16进制整数格式:0xC0A80001</li>
<li>www.baidu.com@192.168.0.1</li>
</ul>
<p> 结果如下:</p>
<p><img src="https://www.redhatzone.com/img/sin/M00/00/19/wKg0C16OjmyAXK2SAADLo7SIjL0874.png" alt="ssrfCode_9.png" /></p>
<h2><a id="_206"></a>修复方案</h2>
<ul>
<li>
<p>使用白名单校验HTTP请求url地址,例如通过InetAddress对象的isSiteLocalAddress()方法进行判断,禁止内网地址的网络请求。。</p>
</li>
<li>
<p>避免将请求响应及错误信息返回给用户。</p>
</li>
<li>
<p>禁用不需要的协议及限制请求端口,仅仅允许http和https请求等。</p>
</li>
</ul>
责任编辑:
声明:本平台发布的内容(图片、视频和文字)以原创、转载和分享网络内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。