在使用一个静态扫描工具时,报了一个SSRF的问题,经过数据流的分析,导致此工具报SSRF的原因是在调用URIBuilder的setPath函数时,参数是从请求里获取的,导致了数据流被污染,因此认为由URIBuilder构造的URL也被污染,最终导致URIBuilder构造出来的URI被污染,所以,认为可能会导致SSRF问题。
为了弄清楚到底是否会导致SSRF问题,针对URIBuilder进行了以下探索。
URIBuilder有多个实现,首先看看apache的【org.apache.http.client.utils.URIBuilder】实现如下:
public URIBuilder setPath(String path) {
return this.setPathSegments(path != null ? URLEncodedUtils.splitPathSegments(path) : null);
}
public URIBuilder setPathSegments(String... pathSegments) {
this.pathSegments = pathSegments.length > 0 ? Arrays.asList(pathSegments) : null;
this.encodedSchemeSpecificPart = null;
this.encodedPath = null;
return this;
}
path变量经过segments处理之后直接就赋值给了pathSegments,没有进行任何处理。从代码的角度可以看出,这里的path如果含有../../之类的字符,URIBuilder应该没有处理。于是写了以下代码进行测试:
public static void testPath(){
URIBuilder uriBuilder = new URIBuilder();
uriBuilder.setPath("../../example/path");
URI uri = null;
try {
uri = uriBuilder.build();
} catch (URISyntaxException e) {
e.printStackTrace();
}
System.out.println("test path result:"+uri.toString());
}
打印的结果如下:
可以看出这里的路径如预料的一样没有被处理。
由于URIBuilder还有setParameter和setParameters方法,于是为了测试设置参数时,是否进行了处理,进一步测试代码如下:
public static void testPatameter(){
URIBuilder uriBuilder = new URIBuilder();
uriBuilder.setScheme("https");
uriBuilder.setHost("www.test.com");
uriBuilder.setPath("../../example/path");
uriBuilder.setParameter("para1", "val1¶2=val2");
uriBuilder.setParameter("para2", "val2");
URI uri = null;
try {
uri = uriBuilder.build();
} catch (URISyntaxException e) {
e.printStackTrace();
}
System.out.println("test parameter result:"+uri.toString());
}
public static void testPatameterList(){
URIBuilder uriBuilder = new URIBuilder();
uriBuilder.setScheme("https");
uriBuilder.setHost("www.test.com");
uriBuilder.setPath("../../example/path");
List<NameValuePair> paraList = new ArrayList<NameValuePair>();
paraList.add(new BasicNameValuePair("para3", "val3¶4=val4"));
paraList.add(new BasicNameValuePair("para4", "val4"));
uriBuilder.setParameters(paraList);
URI uri = null;
try {
uri = uriBuilder.build();
} catch (URISyntaxException e) {
e.printStackTrace();
}
System.out.println("test parameter list result:"+uri.toString());
}
运行结果如下图:
查看代码setParameter时,发现在设置时也没有编码,于是进一步探测在构建URI时的代码如下:
private String buildString() {
StringBuilder sb = new StringBuilder();
if (this.scheme != null) {
sb.append(this.scheme).append(':');
}
if (this.encodedSchemeSpecificPart != null) {
sb.append(this.encodedSchemeSpecificPart);
} else {
if (this.encodedAuthority != null) {
sb.append("//").append(this.encodedAuthority);
} else if (this.host != null) {
sb.append("//");
if (this.encodedUserInfo != null) {
sb.append(this.encodedUserInfo).append("@");
} else if (this.userInfo != null) {
sb.append(this.encodeUserInfo(this.userInfo)).append("@");
}
if (InetAddressUtils.isIPv6Address(this.host)) {
sb.append("[").append(this.host).append("]");
} else {
sb.append(this.host);
}
if (this.port >= 0) {
sb.append(":").append(this.port);
}
}
if (this.encodedPath != null) {
sb.append(normalizePath(this.encodedPath, sb.length() == 0));
} else if (this.pathSegments != null) {
sb.append(this.encodePath(this.pathSegments));
}
if (this.encodedQuery != null) {
sb.append("?").append(this.encodedQuery);
} else if (this.queryParams != null && !this.queryParams.isEmpty()) {
sb.append("?").append(this.encodeUrlForm(this.queryParams));
} else if (this.query != null) {
sb.append("?").append(this.encodeUric(this.query));
}
}
if (this.encodedFragment != null) {
sb.append("#").append(this.encodedFragment);
} else if (this.fragment != null) {
sb.append("#").append(this.encodeUric(this.fragment));
}
return sb.toString();
}
在buildString里,将参数进行了URIEncode,所以,转成的URL字符串里参数的值是URL编码的。
在组装Path时,虽然调用了normalizePath,但是,根据normalizePath的实现代码如下:
private static String normalizePath(String path, boolean relative) {
String s = path;
if (TextUtils.isBlank(path)) {
return "";
} else {
if (!relative && !path.startsWith("/")) {
s = "/" + path;
}
return s;
}
}
可以了解normalizePath只是在不是空的情况下在path前面添加一个斜线,其他并没有对输入的path进行任何处理。
通过以上实现可以看出,针对URL中的Path的处理是不安全的,但是对Parameter的处理是安全的。所以,在使用path之前,需要对path进行有效性验证,否则可能组装出以上例子里给出的路径。
接着,又测试了一下Spring framework的org.springframework.web.util.UriComponentsBuilder,测试代码如下:
public static void testPath(){
UriComponentsBuilder builder = UriComponentsBuilder.newInstance();
builder.scheme("https")
.host("www.example.com")
.port(443)
.path("../../example/path")
.queryParam("page", 1)
.queryParam("size", 10);
System.out.println("Spring test path result:"+builder.toUriString());
}
public static void testPatameter(){
UriComponentsBuilder builder = UriComponentsBuilder.newInstance();
builder.scheme("https")
.host("www.example.com")
.port(443)
.path("../../example/path")
.queryParam("para1", "val1¶2=val2")
.queryParam("para2", "val2");
System.out.println("Spring test parameter result:"+builder.toUriString());
}
测试结果如下:
看来可能各个库对路径和参数的实现还是比较一致的,设置参数时,库会对参数进行编码处理,不会导致HPP问题【HPP的介绍可以参考:应用安全系列之九:HTTP参数污染_java http参数污染怎么解决-CSDN博客】。
但是关于路径,就需要使用者在调用URIBuilder之前对path的值进行验证之后才能使用。
不过,根据SSRF在OWASP TOP10网站的定义如下:
SSRF flaws occur whenever a web application is fetching a remote resource without validating the user-supplied URL. It allows an attacker to coerce the application to send a crafted request to an unexpected destination, even when protected by a firewall, VPN, or another type of network access control list (ACL).
可见,这里定义只是把SSRF定义为访问远程的资源。如果设置host时,输入的参数没有进行白名单范围验证,毫无争议地就是SSRF问题。但是,由于路径遍历导致访问此服务器的资源也已经偏离了这个URL原始的初衷,也可以认为它时一种影响范围比较小的SSRF。无论如何都需要在使用输入的参数作为URL的path的一部分之前,要进行输入验证,避免路径跳跃的问题出现。