查看原文
其他

原创 | 浅谈httpClient组件与ssrf

tkswifty SecIN技术平台 2022-06-18

点击上方蓝字 关注我吧


关于HTTP Client

ssrf是比较常见的漏洞,可以利用存在缺陷的web应用作为代理攻击远程和本地的服务器。一般存在于可以发起网络请求的方法和对应的业务。
HttpClient 是 Apache 的项目,是一个可以用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包。相比传统JDK自带的URLConnection,增加了易用性和灵活性,它不仅使客户端发送Http请求变得容易,而且也方便开发人员测试接口(基于Http协议的),提高了开发的效率,也方便提高代码的健壮性。

1.1 相关依赖

<dependency> <groupId>commons-httpclient</groupId> <artifactId>commons-httpclient</artifactId> <version>3.1</version></dependency>
<dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.12</version></dependency>


关于HTTP Client

在JavaWeb中HttpClient常用于发起HTTP网络请求,以下是相关版本的部分源码实现,部分处理方式结合特定的场景可能导致ssrf安全检查的绕过

2.1 HttpClient3

以发起get请求的GetMethod方式为例,其带参构造方法主要来自其父类HttpMethodBase:
public class GetMethod extends HttpMethodBase{ public GetMethod(String uri){ super(uri); LOG.trace("enter GetMethod(String)"); setFollowRedirects(true); }}

主要通过自实现的 URI 类进行解析:

  • org/apache/commons/httpclient/HttpMethodBase.class

public HttpMethodBase(String uri) throws IllegalArgumentException, IllegalStateException { try { if ((uri == null) || (uri.equals(""))) { uri = "/"; } String charset = getParams().getUriCharset(); setURI(new URI(uri, true, charset)); } catch (URIException e) { throw new IllegalArgumentException("Invalid uri '" + uri + "': " + e.getMessage()); } }
查看URI的具体实现,主要是org/apache/commons/httpclient/URI.class的parseUriReference方法,这里主要是进行一些简单的处理:
protected void parseUriReference(String original, boolean escaped) throws URIException { if (original == null) { throw new URIException("URI-Reference required"); } String tmp = original.trim(); int length = tmp.length(); if (length > 0) { char[] firstDelimiter = { tmp.charAt(0) }; if ((validate(firstDelimiter, delims)) && (length >= 2)) { char[] lastDelimiter = { tmp.charAt(length - 1) }; if (validate(lastDelimiter, delims)) { tmp = tmp.substring(1, length - 1); length -= 2; } } } int from = 0; boolean isStartedFromPath = false; int atColon = tmp.indexOf(':'); int atSlash = tmp.indexOf('/'); if (((atColon <= 0) && (!tmp.startsWith("//"))) || ((atSlash >= 0) && (atSlash < atColon))) { isStartedFromPath = true; } int at = indexFirstOf(tmp, isStartedFromPath ? "/?#" : ":/?#", from); if (at == -1) { at = 0; } if ((at > 0) && (at < length) && (tmp.charAt(at) == ':')) { char[] target = tmp.substring(0, at).toLowerCase().toCharArray(); if (validate(target, scheme)) { this._scheme = target; } else { throw new URIException("incorrect scheme"); } at++;from = at; } this._is_net_path = (this._is_abs_path = this._is_rel_path = this._is_hier_part = 0); if ((0 <= at) && (at < length) && (tmp.charAt(at) == '/')) { this._is_hier_part = true; if ((at + 2 < length) && (tmp.charAt(at + 1) == '/') && (!isStartedFromPath)) { int next = indexFirstOf(tmp, "/?#", at + 2); if (next == -1) { next = tmp.substring(at + 2).length() == 0 ? at + 2 : tmp.length(); } parseAuthority(tmp.substring(at + 2, next), escaped); from = at = next; this._is_net_path = true; } if (from == at) { this._is_abs_path = true; } } if (from < length) { int next = indexFirstOf(tmp, "?#", from); if (next == -1) { next = tmp.length(); } if (!this._is_abs_path) { if (((!escaped) && (prevalidate(tmp.substring(from, next), disallowed_rel_path))) || ((escaped) && (validate(tmp.substring(from, next).toCharArray(), rel_path)))) { this._is_rel_path = true; } else if (((!escaped) && (prevalidate(tmp.substring(from, next), disallowed_opaque_part))) || ((escaped) && (validate(tmp.substring(from, next).toCharArray(), opaque_part)))) { this._is_opaque_part = true; } else { this._path = null; } } String s = tmp.substring(from, next); if (escaped) { setRawPath(s.toCharArray()); } else { setPath(s); } at = next; } String charset = getProtocolCharset(); if ((0 <= at) && (at + 1 < length) && (tmp.charAt(at) == '?')) { int next = tmp.indexOf('#', at + 1); if (next == -1) { next = tmp.length(); } if (escaped) { this._query = tmp.substring(at + 1, next).toCharArray(); if (!validate(this._query, uric)) { throw new URIException("Invalid query"); } } else { this._query = encode(tmp.substring(at + 1, next), allowed_query, charset); } at = next; } if ((0 <= at) && (at + 1 <= length) && (tmp.charAt(at) == '#')) { if (at + 1 == length) { this._fragment = "".toCharArray(); } else { this._fragment = (escaped ? tmp.substring(at + 1).toCharArray() : encode(tmp.substring(at + 1), allowed_fragment, charset)); } } setURI(); }
以看到这里重新创建了URI对象,也就是说再次调用了org/apache/commons/httpclient/URI.class的parseUriReference方法,这里有个关键是会对#进行截断,重新组装后赋值给_query:
if ((0 <= at) && (at + 1 < length) && (tmp.charAt(at) == '?')) { int next = tmp.indexOf('#', at + 1); if (next == -1) { next = tmp.length(); } if (escaped) { this._query = tmp.substring(at + 1, next).toCharArray(); if (!validate(this._query, uric)) { throw new URIException("Invalid query"); } } else { this._query = encode(tmp.substring(at + 1, next), allowed_query, charset); } at = next;
最后将处理后的_query生成新的URI:
protected void setURI() { StringBuffer buf = new StringBuffer(); if (this._scheme != null) { buf.append(this._scheme); buf.append(':'); } if (this._is_net_path) { buf.append("//"); if (this._authority != null) { buf.append(this._authority); } } if ((this._opaque != null) && (this._is_opaque_part)) { buf.append(this._opaque); } else if (this._path != null) { if (this._path.length != 0) { buf.append(this._path); } } if (this._query != null) { buf.append('?'); buf.append(this._query); } this._uri = buf.toString().toCharArray(); this.hash = 0; }
也就是说www.sec-in.com#的host头会经过二次处理变成www.sec-in.com。
最后调用HttpMethodBase自身的setURI方法:
public void setURI(URI uri) throws URIException{ if (uri.isAbsoluteURI()) { this.httphost = new HttpHost(uri); } setPath(uri.getPath() == null ? "/" : uri.getEscapedPath()); setQueryString(uri.getEscapedQuery()); }
这里新建了HttpHost对象,查看其实例化方法,调用了自定义的uri.getHost方法:
public HttpHost(URI uri) throws URIException{ this(uri.getHost(), uri.getPort(), Protocol.getProtocol(uri.getScheme())); }
查看getHost的具体实现,主要进行URL解码操作:
public String getHost() throws URIException { if (this._host != null) { return decode(this._host, getProtocolCharset()); } return null; }
在发起请求时,调用HttpClinet的executeMethod,具体实现如下: 
public int executeMethod(HostConfiguration hostconfig, HttpMethod method, HttpState state) throws IOException, HttpException { LOG.trace("enter HttpClient.executeMethod(HostConfiguration,HttpMethod,HttpState)"); if (method == null) { throw new IllegalArgumentException("HttpMethod parameter may not be null"); } HostConfiguration defaulthostconfig = getHostConfiguration(); if (hostconfig == null) { hostconfig = defaulthostconfig; } URI uri = method.getURI(); if ((hostconfig == defaulthostconfig) || (uri.isAbsoluteURI())) { hostconfig = (HostConfiguration)hostconfig.clone(); if (uri.isAbsoluteURI()) { hostconfig.setHost(uri); } } HttpMethodDirector methodDirector = new HttpMethodDirector(getHttpConnectionManager(), hostconfig, this.params, state == null ? getState() : state); methodDirector.executeMethod(method); return method.getStatusCode(); }
这里调用了HttpMethod的getURI()方法,具体实现如下:
public URI getURI() throws URIException { StringBuffer buffer = new StringBuffer(); if (this.httphost != null) { buffer.append(this.httphost.getProtocol().getScheme()); buffer.append("://"); buffer.append(this.httphost.getHostName()); int port = this.httphost.getPort(); if ((port != -1) && (port != this.httphost.getProtocol().getDefaultPort())) { buffer.append(":"); buffer.append(port); } } buffer.append(this.path); if (this.queryString != null) { buffer.append('?'); buffer.append(this.queryString); } String charset = getParams().getUriCharset(); return new URI(buffer.toString(), true, charset); }
综上,由于parseUriReference方法会对不符合规范的host头进行处理,同时在进行setURI的同时会进行一次URI解码,那么实际上www.evil.com%3a80%23.sec-in.com/的访问也是可行的,再加上request.getParameter()自身会做一层URL解码,所以通过httpclient3访问www.evil.com%253a80%2523.sec-in.com/最终实际是访问www.evil.com
以dnslog进行验证,成功接收到请求:

2.2 HttpClient4(<=4.5.12版本)

以如下代码为例,传入对应的URL,然后调用httpClient的execute方法发起http请求:
public String get(String url) { String result = null; CloseableHttpClient httpClient = HttpClients.createDefault(); HttpGet get = new HttpGet(url); CloseableHttpResponse response = null; try { response = httpClient.execute(get);
在execute方法处下断点进行调试:

 可以看到中途会调用URIUtils.extractHost()进行处理:
查看extractHost的具体实现,在解析时候会先使用自带的 URL 函数获取 port 和 host,如果通过getHost()获取失败的话,会调用getAuthority()方法来进行调整:
  • org/apache/http/client/utils/URIUtils.class
public static HttpHost extractHost(URI uri) { if (uri == null) { return null; } HttpHost target = null; if (uri.isAbsolute()) { int port = uri.getPort(); String host = uri.getHost(); if (host == null) { host = uri.getAuthority(); ...... } return target; }
host是authority 的子集,authority可以包含端口,而host 不含端口。那么如果对应的URL携带的port不规范时,则会抛出对应的异常,例如http://www.evil.com:80.www.sec-in.com,可以利用一点来调度到httpclient的uri.getAuthority():

在uri.getAuthority()后,如果不为null,则进行进一步的处理,首先对@进行截断,获取@后的内容。然后获取:做拆分,一直获取相关的整数,直到为非数字为止:
int at = host.indexOf('@'); if (at >= 0) { if (host.length() > at + 1) { host = host.substring(at + 1); } else { host = null; } } if (host != null) { int colon = host.indexOf(':'); if (colon >= 0) { int pos = colon + 1; int len = 0; for (int i = pos; i < host.length(); i++) { if (!Character.isDigit(host.charAt(i))) { break; } len++; } if (len > 0) { try { port = Integer.parseInt(host.substring(pos, pos + len)); } catch (NumberFormatException ex) {} } host = host.substring(0, colon); } }
也就是说,类似:80.www.sec-in.com最后获取到的值应该为80,作为相关的端口。然后host的内容以:进行substring切割:
host = host.substring(0, colon);
那么若相关的请求为www.evil.com,最终处理的结果为www.evil.com
以dnslog进行验证,成功接收到请求:

在4.5.13版本开始,extractHost方法获取hostname的方式做出了改动,直接通过切割:来实现:
public static HttpHost extractHost(URI uri) { if (uri == null) { return null; } if (uri.isAbsolute()) { if (uri.getHost() == null) { if (uri.getAuthority() == null) { break label155; } String content = uri.getAuthority(); int at = content.indexOf('@'); if (at != -1) { content = content.substring(at + 1); } String scheme = uri.getScheme(); at = content.indexOf(":"); int port; String hostname; if (at != -1) { String hostname = content.substring(0, at); try { String portText = content.substring(at + 1); port = !TextUtils.isEmpty(portText) ? Integer.parseInt(portText) : -1; } catch (NumberFormatException ex) { return null; } } else { hostname = content; port = -1; } try { return new HttpHost(hostname, port, scheme); } catch (IllegalArgumentException ex) { return null; } } return new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()); } label155: return null; }

其他

关于ssrf的绕过有很多姿势,例如IP进制化处理、xip.io/xip.name、利用Enclosed alphanumerics等。此外,我们常用的解析工具类在特定场景下也会有意想不到的安全问题。

相关推荐




原创 | java安全-java类加载器
原创 | java安全-java反射
原创 | java安全-java RMI你要的分享、在看与点赞都在这儿~

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存