原创 | 浅谈httpClient组件与ssrf

admin 2024年12月18日22:42:23评论12 views字数 9482阅读31分36秒阅读模式

原创 | 浅谈httpClient组件与ssrf

点击上方蓝字 关注我吧

原创 | 浅谈httpClient组件与ssrf

关于HTTP Client

原创 | 浅谈httpClient组件与ssrf
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>
原创 | 浅谈httpClient组件与ssrf

关于HTTP Client

原创 | 浅谈httpClient组件与ssrf
在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进行验证,成功接收到请求:
原创 | 浅谈httpClient组件与ssrf

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方法处下断点进行调试:

原创 | 浅谈httpClient组件与ssrf

 可以看到中途会调用URIUtils.extractHost()进行处理:
原创 | 浅谈httpClient组件与ssrf
查看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():

原创 | 浅谈httpClient组件与ssrf

在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进行验证,成功接收到请求:

原创 | 浅谈httpClient组件与ssrf

在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;  }
原创 | 浅谈httpClient组件与ssrf

其他

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

原创 | java安全-java类加载器

原创 | java安全-java反射
原创 | java安全-java RMI

原创 | 浅谈httpClient组件与ssrf
你要的分享、在看与点赞都在这儿~

原文始发于微信公众号(SecIN技术平台):原创 | 浅谈httpClient组件与ssrf

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年12月18日22:42:23
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   原创 | 浅谈httpClient组件与ssrfhttps://cn-sec.com/archives/657109.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息