浅谈httpClient组件与ssrf

  • Comments Off on 浅谈httpClient组件与ssrf
  • 20 views
  • A+

一、关于HttpClient

  ssrf是比较常见的漏洞,可以利用存在缺陷的web应用作为代理攻击远程和本地的服务器。一般存在于可以发起网络请求的方法和对应的业务。

  HttpClient 是 Apache 的项目,是一个可以用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包。相比传统JDK自带的URLConnection,增加了易用性和灵活性,它不仅使客户端发送Http请求变得容易,而且也方便开发人员测试接口(基于Http协议的),提高了开发的效率,也方便提高代码的健壮性。

1.1 相关依赖

```xml

commons-httpclient
commons-httpclient
3.1

```

xml
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.12</version>
</dependency>

二、HttpClient的解析方式

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

2.1 HttpClient3

  以发起get请求的GetMethod方式为例,其带参构造方法主要来自其父类HttpMethodBase:

java
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

java
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方法,这里主要是进行一些简单的处理:

```java
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:

java
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:

java
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方法:

```java
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方法:

java
public HttpHost(URI uri) throws URIException
{
this(uri.getHost(), uri.getPort(), Protocol.getProtocol(uri.getScheme()));
}

  查看getHost的具体实现,主要进行URL解码操作:

java
public String getHost() throws URIException
{
if (this._host != null) {
return decode(this._host, getProtocolCharset());
}
return null;
}

  在发起请求时,调用HttpClinet的executeMethod,具体实现如下:

```java
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()方法,具体实现如下:

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

wKg0C2EP4KiAISgoAAABTdfQBw836.png

2.2 HttpClient4(<=4.5.12版本)

  以如下代码为例,传入对应的URL,然后调用httpClient的execute方法发起http请求:

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

wKg0C2ESZDKAVXsqAABexCLZum0294.png

  可以看到中途会调用URIUtils.extractHost()进行处理:

wKg0C2ESYWKAcZaAABiBsrr434998.png

  查看extractHost的具体实现,在解析时候会先使用自带的 URL 函数获取 port 和 host,如果通过getHost()获取失败的话,会调用getAuthority()方法来进行调整:

  • org/apache/http/client/utils/URIUtils.class

java
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():

wKg0C2EPZFqAbIePAAEPbXe1OI351.png

  在uri.getAuthority()后,如果不为null,则进行进一步的处理,首先对@进行截断,获取@后的内容。然后获取:做拆分,一直获取相关的整数,直到为非数字为止:

java
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切割:

java
host = host.substring(0, colon);

  那么若相关的请求为www.evil.com:80.www.sec-in.com,最终处理的结果为www.evil.com:80。

  以dnslog进行验证,成功接收到请求:

wKg0C2EP4PqAU2g6AAAPoLaW9U514.png

  在4.5.13版本开始,extractHost方法获取hostname的方式做出了改动,直接通过切割:来实现:

```java
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等。此外,我们常用的解析工具类在特定场景下也会有意想不到的安全问题。

相关推荐: MSSQL渗透测试:违规使用链接数据库

原文信息 地址:https://www.hackingarticles.in/mssql-for-pentester-abusing-linked-database/ 作者:Raj Chandel 标题:MSSQL for Pentester: Abusing…