本文首发于奇安信攻防社区:https://forum.butian.net/share/2117
规则分析 SSRF和常见的JNDI注入、sql注入
JNDI注入: 
1 2 3 Context c = new Context(); c.lookup("ldap://127.0.0.1/xxx" ) --> sink
Sink检测是否调用了lookup函数,且函数参数是否外部可控
SQL注入:
1 2 3 4 5 6 7 8 9 10 11 12 @RequestMapping(value = "/one" ) public List<Student> one(@RequestParam(value = "username" ) String username) { return indexLogic.getStudent(username); } public List<Student> getStudent(String username) { String sql = "select * from students where username like '%" + username + "%'" ; return jdbcTemplate.query(sql, ROW_MAPPER); -> sink }
Sink检测是否调用了jdbcTemplate.query函数,且函数参数是否外部可控
SSRF的常见代码表现形式:(HTTP SSRF)
1 2 3 4 5 URL url = new URL(imageUrl); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); --> sink connection.setRequestMethod("GET" ); return connection.getResponseMessage();
sink函数中并没有外部参数,检测sink需要和前面的代码联系,需要判断URL 对象在构造时是否外部可控。
因此分析ql库中对SSRF漏洞是如何检测的:
Config路径:ql/java/ql/lib/semmle/code/java/security/RequestForgeryConfig.qll
isSource 1 2 3 4 5 source instanceof RemoteFlowSource and not source.asExpr().(MethodAccess).getCallee() instanceof URLConnectionGetInputStreamMethod
source使用了常见的RemoteFlowSource,覆盖常见的远程请求,同时作为Method的source调用中,不能包含URLConnectionGetInputStreamMethod类型的调用
1 2 3 4 5 6 7 8 class URLConnectionGetInputStreamMethod extends Method { URLConnectionGetInputStreamMethod() { this.getDeclaringType() instanceof TypeUrlConnection and this.hasName("getInputStream" ) and this.hasNoParameters() } }
即如下的Source:
1 java.net.URLConnection.getInputStream()
这种不认为是SSRF的Source,可能是因为连接的数据是否可控不能确定
isSink 1 2 3 4 5 6 7 8 9 10 11 12 13 override predicate isSink(DataFlow::Node sink) { sink instanceof RequestForgerySink } abstract class RequestForgerySink extends DataFlow::Node { } private class UrlOpenSinkAsRequestForgerySink extends RequestForgerySink { UrlOpenSinkAsRequestForgerySink() { sinkNode(this, "open-url" ) } } predicate sinkNode(Node node, string kind) { exists(InterpretNode n | isSinkNode(n, kind) and n.asNode() = node) }
其中使用到了ExternalFlow.ql中的sinkNode谓词,该库中表示,该库为内部使用API,处理csv格式的数据,sinkNode(Node node, string kind)属于一个接口,从Node中找符合sinkModelCsv或者SinkModelCsv子类的Node数据,其中最后一个参数用于标识Sink的类型,每一列参数的含义在ExternalFlow.ql中有详细讲解,这里简单介绍几个常用的参数,在RequestForgeryConfig中使用的是sinkNode(this, “open-url”)
package 包名
类名
是否跳转到子类
方法名
签名列,限制选择方法名
ext 不太懂
input 输入的位置
kind 当前sink的类型
就是匹配所有open-url类型的数据类型。
sinkModelCsv谓词数据如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 private predicate sinkModelCsv(string row) { row = [ "java.net;URL;false;openConnection;;;Argument[-1];open-url" , "java.net;URL;false;openStream;;;Argument[-1];open-url" , "java.net.http;HttpRequest;false;newBuilder;;;Argument[0];open-url" , "java.net.http;HttpRequest$Builder;false;uri;;;Argument[0];open-url" , "java.net;URLClassLoader;false;URLClassLoader;(URL[]);;Argument[0];open-url" , "java.net;URLClassLoader;false;URLClassLoader;(URL[],ClassLoader);;Argument[0];open-url" , "java.net;URLClassLoader;false;URLClassLoader;(URL[],ClassLoader,URLStreamHandlerFactory);;Argument[0];open-url" , "java.net;URLClassLoader;false;URLClassLoader;(String,URL[],ClassLoader);;Argument[1];open-url" , "java.net;URLClassLoader;false;URLClassLoader;(String,URL[],ClassLoader,URLStreamHandlerFactory);;Argument[1];open-url" , "java.net;URLClassLoader;false;newInstance;;;Argument[0];open-url" , "javax.validation;ConstraintValidatorContext;true;buildConstraintViolationWithTemplate;;;Argument[0];bean-validation" , "javax.net.ssl;HttpsURLConnection;true;setDefaultHostnameVerifier;;;Argument[0];set-hostname-verifier" , "javax.net.ssl;HttpsURLConnection;true;setHostnameVerifier;;;Argument[0];set-hostname-verifier" ] }
其他还有很多SinkModelCsv的子类,包含了一些第三方库的sink函数。
isAdditionalTaintStep 前面提到,SSRF漏洞不同于JNDI注入或者SQL注入,它的sink检测需要联系sink之前的代码,判断sink的Method调用方是否外部可控。ql库中通过实现isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ)方法进行功能的实现。该谓词的作用是将两个原本不相连的Node强行连在一起
1 2 3 4 5 6 7 8 9 10 11 12 13 14 override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) { any(RequestForgeryAdditionalTaintStep r).propagatesTaint(pred, succ) } private class DefaultRequestForgeryAdditionalTaintStep extends RequestForgeryAdditionalTaintStep { override predicate propagatesTaint(DataFlow::Node pred, DataFlow::Node succ) { exists(UriCreation c | c.getHostArg() = pred.asExpr() | succ.asExpr() = c) or exists(UrlConstructorCall c | c.getHostArg() = pred.asExpr() | succ.asExpr() = c) } }
这里相对有点抽象,举个例子去进行理解
1 2 3 4 5 6 7 8 9 10 11 12 @RequestMapping(value = "/one" ) public String One(@RequestParam(value = "url" ) String imageUrl) { try { URL url = new URL(imageUrl); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET" ); return connection.getResponseMessage(); } catch (IOException var3) { System.out.println (var3); return "Hello" ; } }
此处按照正常的逻辑,
由于url不被认为是污点,因此没有继续向后寻找,isAdditionalTaintStep的作用是:当存在URL url = new URL(imageURL),且imageURL为污点时,认为 url 也是污点。
接着分析它是如何实现的,在函数中,pred代表的是污点,在SSRF中就是用户指定的URL字符串,succ则是代表类似URL(url)这种方法调用或者是对象的实例化。实现中的意思为,succ是一个UriCreation 调用或者一个UriConstructorCall调用,它的getHostArg方法返回的表达式等于污点node 。其中UriCreation和UriConstructorCall的定义都在Networking.ql中,不做过多的分析。总的来说,它们匹配new URL()、URI.create(),并且其中url地址对应的参数需要是外部可控。满足上述条件的,我们认为它也是一个污点。从而成功的把url→new URL(url)链接起来。
isSanitizer 1 2 3 4 5 6 7 8 9 10 11 12 13 14 override predicate isSanitizer(DataFlow::Node node) { node instanceof RequestForgerySanitizer } private class PrimitiveSanitizer extends RequestForgerySanitizer { PrimitiveSanitizer() { this.getType() instanceof PrimitiveType or this.getType() instanceof BoxedType or this.getType() instanceof NumberType } private class HostnameSantizer extends RequestForgerySanitizer { HostnameSantizer() { this.asExpr() = any(HostnameSanitizingPrefix hsp).getAnAppendedExpression() } }
该方法用于净化污点,去除一些误报。PrimitiveSanitizer 是常见的方法,不匹配基础类型、数字类型的节点。重点是第二个HostnameSantizer,HostnameSanitizingPrefix继承自InterestingPrefix,该类在Stringprefixes.qll中,该文件的注释中讲解了该类的作用,简单来说就是可以定义一个字符串前缀,并提供一个getAnAppendedExpression函数,用来匹配任意该前缀+ 污点字符串的节点,简单演示如下,其中suffix标识会被匹配到的节点
1 2 3 4 * "foo:" + suffix1 * "barfoo:" + suffix2 * stringBuilder.append ("foo:" ).append (suffix3); * String.format("%sfoo:%s" , notSuffix, suffix4);
HostnameSanitizingPrefix 用一个正则定义前缀
1 2 3 4 5 6 HostnameSanitizingPrefix() { exists( this.getStringValue() .regexpFind(".*([?#]|[^?#:/\\\\][/\\\\]).*|[/\\\\][^/\\\\].*|^/$" , 0 , offset) ) }
该正则匹配字符串中?、#等符号之后的部分,用来解决如下情况的误报:
1 2 URL url = new URL("http://127.0.0.1?x=" +imageUrl);
此时,imageUrl无法指定目标访问任意的地址,也就不算是SSRF漏洞。
规则实战 使用microserviceseclab进行测试,该项目中的SSRFController.java包含了常见的5种SSRF漏洞。首先直接运行该检测QL,可以发现3处漏洞
漏洞代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 @RequestMapping(value = "/one" ) public String One(@RequestParam(value = "url" ) String imageUrl) { try { URL url = new URL(imageUrl); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET" ); return connection.getResponseMessage(); } catch (IOException var3) { System.out.println (var3); return "Hello" ; } } @RequestMapping(value = "/four" ) public String Four(@RequestParam(value = "url" ) String imageUrl) { try { DefaultHttpClient client = new DefaultHttpClient(); HttpGet get = new HttpGet(imageUrl); HttpResponse response = client.execute(get); return response.toString(); } catch (IOException var1) { System.out.println (var1); return "Hello" ; } } @RequestMapping(value = "five" ) public String Five(@RequestParam(value = "url" ) String imageUrl) { try { URL url = new URL(imageUrl); InputStream inputStream = url.openStream(); return String.valueOf(inputStream.read()); } catch (IOException var1) { System.out.println (var1); return "Hello" ; } }
由于该项目使用了Spring-boot框架, 因此Source都是@RequestParam注解的参数。
在被成功检测的漏洞中,1和5分别使用了url.openConnection和url.openStream,被sinkModelCsv中的如下规则所匹配
\root\codeQL\databases\micro-service-seclab-database\src.zip\root\codeQL\micro_service_seclab\src\main\java\com\l4yn3\microserviceseclab\controller\SSRFController.java
1 2 "java.net;URL;false;openConnection;;;Argument[-1];open-url" "java.net;URL;false;openStream;;;Argument[-1];open-url"
4中使用了第三方的HTTP请求库,同样可以在ApacheHttp.qll中的ApacheHttpOpenUrlSink找到对应的sink定义
1 2 "org.apache.http.client.methods;HttpGet;false;HttpGet;;;Argument[0];open-url"
2和3 没有被检测到,因此接下来分析其原因。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @RequestMapping(value = "/two" ) public String Two(@RequestParam(value = "url" ) String imageUrl) { try { URL url = new URL(imageUrl); HttpResponse response = Request.Get(String.valueOf(url)).execute().returnResponse(); return response.toString(); } catch (IOException var1) { System.out.println (var1); return "Hello" ; } } @RequestMapping(value = "/three" ) public String Three(@RequestParam(value = "url" ) String imageUrl) { try { URL url = new URL(imageUrl); OkHttpClient client = new OkHttpClient(); com.squareup.okhttp.Request request = new com.squareup.okhttp.Request.Builder().get().url(url).build(); Call call = client.newCall(request); Response response = call.execute(); return response.toString(); } catch (IOException var1) { System.out.println (var1); return "Hello" ; } }
漏报1 该漏洞的触发流程如下:
imageUrl→url = new URL(imageUrl)→String.valueOf(url)→Request.Get(String.valueOf(url))
首先可以发现,在现有的规则中,没有 org.apache.http.client.fluent.Request.Get()这个sink。因此我们在Apachehttp.qll中的ApacheHttpOpenUrlSink加入如下行,
1 2 "org.apache.http.client.fluent;Request;false;Get;;;Argument[0];open-url" ,
接着重新运行漏洞查询ql,会发现还是无法检测到该链。原因是什么呢?第一反应是规则是否写的有问题,直接在RequestForgerySink使用Quick Evalutation
会发现该sink点被成功找到了
既然sink点没有问题,那没有扫出来就说明是路径的问题。我们再观察漏洞触发点,会发现,从url到Requests.get()中,存在一个String.valueOf(url)方法,正常情况下程序不会觉得String.valueOf方法返回的仍然是污点。因此我们需要修改Config中的isAdditionalTaintStep方法,将java.net.URL和String.valueOf(url)绑定。为了不影响原有代码,新创建一个ql文件,写如下内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 import javaimport semmle.code.java.security.RequestForgeryConfigimport DataFlow::PathGraphimport semmle.code.java.dataflow.ExternalFlowclass MySinkModelCsv extends SinkModelCsv { override predicate row(string row) { row = [ "org.apache.http.client.fluent;Request;false;Get;;;Argument[0];open-url" , ] } } class TypeStringLib extends RefType { TypeStringLib() { this.hasQualifiedName("java.lang" , "String" ) } } class StringValue extends MethodAccess { StringValue(){ this.getCallee().getDeclaringType() instanceof TypeStringLib and this.getCallee().hasName("valueOf" ) } } private class MyRequestForgeryAdditionalTaintStep extends RequestForgeryAdditionalTaintStep { override predicate propagatesTaint(DataFlow::Node pred, DataFlow::Node succ) { exists(UriCreation c | c.getHostArg() = pred.asExpr() | succ.asExpr() = c) or exists(UrlConstructorCall c | c.getHostArg() = pred.asExpr() | succ.asExpr() = c) or exists(StringValue c | c.getArgument(0 ) = pred.asExpr() | succ.asExpr() = c) } } class MyRequestForgeryConfiguration extends RequestForgeryConfiguration { MyRequestForgeryConfiguration() { this = "Server-Side Request Forgery" } override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) { any(RequestForgeryAdditionalTaintStep r).propagatesTaint(pred, succ) } } from DataFlow::PathNode source, DataFlow::PathNode sink, MyRequestForgeryConfiguration conf where conf.hasFlowPath(source, sink) select sink.getNode(), source, sink, "Potential server-side request forgery due to $@." , source.getNode(), "a user-provided value"
关键代码
1 2 exists(StringValue c | c.getArgument(0 ) = pred.asExpr() | succ.asExpr() = c)
此时成功检测到该漏洞
漏报2 漏洞代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @RequestMapping(value = "/three" ) public String Three(@RequestParam(value = "url" ) String imageUrl) { try { URL url = new URL(imageUrl); OkHttpClient client = new OkHttpClient(); com.squareup.okhttp.Request request = new com.squareup.okhttp.Request.Builder().get().url(url).build(); Call call = client.newCall(request); Response response = call.execute(); return response.toString(); } catch (IOException var1) { System.out.println (var1); return "Hello" ; } }
这种方式使用了Okhttp发起HTTP请求,okhttp是链式调用,常见的请求代码写法如下:
1 new com.squareup.okhttp.Request.Builder().xxx.xxx.xxx.url(url).xxx.build()
这种请求相对更复杂,不适合使用之前的csv格式写漏洞检测规则, 因此需要自行构造规则。
漏洞的构造中我认为有两个关键的定位锚点,一个是url(url),一个是build(),url()确定是否引入污点,build()确定sink的位置。结合这两者,进行检测ql的构造。
首先观察这种结构的语法树
这种链式结构调用在语法树中是包含的关系,当获取到最外层的MethodAccess时,可以使用getAChildExpr()方法返回其子语句,使用getAChildExpr+()可以递归返回全部子语句。结合前面说到的两个关键定位锚点,进行如下代码构造
1 2 3 4 5 6 7 8 9 10 MethodAccess url(MethodAccess ma,DataFlow::Node node){ exists( MethodAccess mc | mc = ma.getAChildExpr()| if mc.getCallee().hasName("url" ) and mc.getArgument(0 ) = node.asExpr() then result = mc else result = url(mc,node) ) } MethodAccess m(DataFlow::Node node){ exists( MethodAccess ma | ma.getCallee().hasName("build" ) and ma.getCallee().getDeclaringType().hasName("Builder" ) |result = url(ma,node) ) }
m方法用来寻找Builder.build()方法,找到之后,调用第一个url方法,并将污点node和找到的MethodAccess作为参数传递进函数。url方法对传入的MethodAccess调用getAChildExpr进行递归检查,查找是否存在url方法的调用,并且污点node是url方法的参数。写好这两个方法后,重写五点分析Configuration中的isSink方法, 在原有的基础上增加对这个m方法的检查,完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class MyRequestForgeryConfiguration extends RequestForgeryConfiguration { MyRequestForgeryConfiguration() { this = "Server-Side Request Forgery" } override predicate isSink(DataFlow::Node sink) { sink instanceof RequestForgerySink or exists (m(sink)) } override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) { any(RequestForgeryAdditionalTaintStep r).propagatesTaint(pred, succ) } }
重新执行ql,成功检测到所有种类的SSRF漏洞