一、关于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进行验证,成功接收到请求:
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方法处下断点进行调试:
可以看到中途会调用URIUtils.extractHost()进行处理:
查看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():
在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进行验证,成功接收到请求:
在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等。此外,我们常用的解析工具类在特定场景下也会有意想不到的安全问题。
原文信息 地址:https://www.hackingarticles.in/mssql-for-pentester-abusing-linked-database/ 作者:Raj Chandel 标题:MSSQL for Pentester: Abusing…
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论