安全测试 基于全流量权限漏洞检测技术

hoozheng · 2020年07月08日 · 最后由 约翰 回复于 2020年12月15日 · 7841 次阅读
本帖已被设为精华帖!

一、背景

关于安全领域内漏洞的发现,技术手段非常多,工具也非常多,大致阶段可分为事前、事中、事后来处理。事前大多采用 SDL、白盒扫描等;事中、事后有 NIDS 及漏洞感知,甚至还有 WAF 来拦截恶意流量等。本文作者主要想尝试通过流量扫描的方式,去发现更多的潜在漏洞。

本文将围绕 “权限问题” 这类漏洞展开讨论,因为权限一旦出问题,很可能导致大规模的敏感信息泄漏,这样的后果可能比一两个跨站要严重的多。权限问题是每个公司非常重要且很难处理好的一种漏洞,这类漏洞是和业务相关性很强的一种漏洞。对安全团队来说,如果对业务不是足够了解,就无法对权限问题有一个很好的治理直到问题收敛。所以本文针对这些困难和问题,尝试去循序渐进地解决互联网公司权限问题收敛,也不可能做到 100% 的检测率,权当抛砖引玉。

权限问题可分为以下几类:

​ 1、未授权访问问题。

​ 2、水平权限问题。

​ 3、垂直权限问题。

主要是对这三类进行扫描和检测。

我们将把整个过程分为三个阶段:

​ 1、数据清洗,清洗出需要的 URL,并通过模型过滤那些不需要检测的 URL。

​ 2、扫描,对这些重点 URL 进行扫描尝试是否存在越权访问的行为。

​ 3、运营阶段,通过运营进一步去除误报的 URL、确认漏洞的危害、是否有进一步利用的可能、以及其他相关接口是否还存在相同的漏洞,用来反哺修正扫描器。

二、漏洞检测

权限问题,顾名思义就是因为对用户权限控制不当导致的问题。为了便于检测可以把它分为二个问题:1、未授权的问题。2、有授权的问题(水平、垂直)。其中对于用户的操作又可分为增、删、改、查,4 个操作的识别。

先看下技术架构:

1592808102_5ef052a6aef72.png!small

技术架构

整个系统分为四层:

​ 流量清洗层:互联网公司每日的流量高达几百亿条,我们不能对全部流量进行检测,也没必要。所以需要清洗出可能存在该类问题的 URL 并且去重,做到精准定位,这样可以节约大量时间用于检测。

​ 模型层:模型层主要过滤那些无法通过规则简单过滤的干扰流量。

​ 扫描层:扫描层通过模型输出的流量逐个进行扫描,并且检测是否存在漏洞。

​ 运营层:最后一层主要是安全运营,逐个查看被扫描器有可能存在的漏洞 URL,并且可以去反推整个系统是否还有其他接口存在此类漏洞用于反哺扫描器。

0x01、收集流量

互联网公司的每日流量几乎都是海量数据,对每个流量都进行检测速度太慢,也没必要,并且全量的数据回放会混着非常多的干扰数据,这种数据本身就不需要做权限控制,或本身就不存在权限问题。这归结于敏感信息的识别,如果这部分内容属于某一个人或某一个群体,被群体之外的人访问了或编辑了,那就是有问题的,所以为了降低后续误报带来的影响和运营困难,我们前期先要对流量进行筛选,把那些重要的流量清洗出来再进行扫描。这样做的优点很明显,就是有的放矢;而缺点也很明显,如果数据选择面太窄就会有遗漏。所以在做数据收集时一定要根据业务不断的迭代,增加敏感数据的维度。

1、流量清洗

流量清洗的主要目标是清洗出具备返回敏感信息的 API 用于后续的检测,当前清洗出了我们比较关心的敏感信息,包含但不限于手机号、、邮箱、组织架构、订单、密码等含有敏感数据的 URL 作为检测目标。

清洗逻辑这里尽量多用 UDF 来判断,具体逻辑就不再这里赘述了,UDF 函数如下:

代码块

SQL

get_phone_number(response) as phone_num,
get_id_card(response) as id_card,
get_bank_card(response) as bank_card,
get_email(response) as email,
get_mark_number(response) as mark_number,

但是清洗出来的敏感信息还需要做第一次误报处理,例如提取出的手机号是包含在一串字符串中的,

样例 1:19f3f34d44c135645909580e99ac

我们需要通过前后字符及上下文来判断,这个是属于真实的手机号、*** 等敏感信息,还是某一个字符串里面的某一部分,如果是截断的字符串那就要作为非手机号过滤掉。

2、归一化并采样

由于流量数据非常大,每日几百亿的 URL 并且绝大部分都是重复的,没必要做重复的扫描和检测,所以这里需要做 2 件事:1、归一化。2、采样。

首先需要做的是归一化。

归一化:归一化的目的是为了合并同类 URL 做更好的采样收录。URL 一般的构成形式如下:

代码块

HTTP

https://www.x.com/abc/cdef?name=ali&id=1#top

其中:https - PROTOCOL ,www.x.com - DOMAIN,/abc/cdef - PATH,name=ali&id=1 - PARAM,#top - FRAGMENT

但绝大部分公司内,很多 URL 的 PATH 部分不会这么规律,而是采取随机字符串的方式。

代码块

HTTP

a.vip.x.com/cloud/x/y/19f3f34d44c0e99ac/e5f85c0875b5643dc37752554eec
a.vip.x.com/cloud/x/y/1c12c3cf727db5e24/e9b61adc14e12d071047d71b143b
a.vip.x.com/cloud/x/y/1c12c3cf727db5e24/4b0ed927c1454e0a2ced373a0863
a.vip.x.com/cloud/x/y/1c12c3cf727db5e24/fed8f52005cc8b4fe2a3d82728f8
a.vip.x.com/cloud/x/y/1c12c3cf727db5e24/59666a1b3d174c21ced72340c94d
a.vip.x.com/cloud/x/y/1c12c3cf727db5e24/aab104ff5ae8ca999ba9b01c7067
a.vip.x.com/cloud/x/y/1c12c3cf727db5e24/365ebe92ff1bc62e3158144a8fe5
a.vip.x.com/cloud/x/y/1c12c3cf727db5e24/c0894925b18cf1c3d71dc9f56945

其实上面这些都是访问的一个资源,扫描器只需要对一个进行检测就可以了,没必要全量检测,所以这类 URL 需要进行归一化,进行采样处理既减少了重复工作,又让处理变得更简单。归一化后的 URL 如下:

代码块

Plain Text

a.vip.x.com/cloud/x/y/{s}/{s}

这里归一化的算法主要采用正则,合并 URL 路径中含有序列码、纯数字、标签、中文等 URL,让他们归为一类:

代码块

SQL

concat(domain, REGEXP_REPLACE(url_path,
 '/([0-9A-Za-z_\\-\\*.@|,]{30,}|[a-zA-Z]+[0-9A-Za-z_\\-\\*.@|,]*[0-9]+|[0-9]+[0-9A-Za-z_\\-\\*.@|,]*[a-zA-Z]+|[0-9\\*.,\\-]+|[\\u4E00-\\u9FA5]+)','/{s}'))
 as req_url_path

如果一家公司没有一个非常统一的编码标准,那么他们的 URL 链接复杂程度,就远远不止上面这种类型。笔者遇到过各种千奇百怪的 URL 形式,有的 URL 里面甚至包含中文,这都可能导致噪音。面对这一状况,目前没有一个很好的处理手段,只能遇到了就修改正则。

采样:这里采样比较简单的是同一类型 URL 每小时取一条数据,因为当前的检测划窗定的是 1 小时。通过 SQL 的 row_number 函数对归一化后的 URL 链接每小时采样一条,采样过程中需要注意:过滤掉返回不成功的流量、扫描器的流量、异常的流量,因为这些流量可能会干扰你的扫描器,因为它本身就不是一个正常流量,在经过你的扫描器修改后,很可能得不到正确的结果。

代码块

SQL

select *
  from (
        select *,
               row_number() over (partition by req_url_path) as row_num
          from (
                select *,
                       concat(domain, REGEXP_REPLACE(url_path, '/([0-9A-Za-z_\\-\\*.@|,]{30,}|[a-zA-Z]+[0-9]+[0-9A-Za-z_\\-\\*.@|,]*|[0-9]+[a-zA-Z]+[0-9A-Za-z_\\-\\*.@|,]*|[0-9\\*.,\\-]+)','/{s}')) as req_url_path
                  from data.sec_ds_x_x_x_x_hh
                 where dt= 'yyyymmdd'
                   and hour = 'hour'
               ) t
       ) t1
 where row_num = 1

3、基于提升树的分类(GBDT)模型

上面通过归一化、采样、去重等手段锁定了扫描器需要检测的目标,并且也缩小了一定范围,但我们这里忽略了一个问题——并非全部手机号码都是重要的,互联网公司都是提供信息的网站,很多卖家信息等都是公开的信息,其中就包括手机号,这在淘宝、京东等的网页就能轻松获取,这部分信息如果作为敏感信息来进行识别权限问题,显然是不合适的,所以需要采用一定方法过滤掉这些卖家息。先来看下息的一种形式如下:

公开卖家数据:

代码块

JSON

{"a":200,"b":{"c":"*27816","d":"*1954900","e":"实木上下铺木床成人高低床双层床二层床子母床多功能儿童床上下床下单立减7000","f":"到店更多惊喜礼品等你拿",
  "g":"https://*.x.com/app/x/x.html?y=*&x=*&z=0","h":true,"i":"实木家具"},"j":0}

真正的敏感手机号:(手机号、*** 这里已做脱敏处理)

代码块

JSON

{"x":2,"a":0,"b":"默认","c":"","d":false,"e":2,"f":"","g":"130****7844","h":"","i":0.0,"j":"**0832173740073","k":""},"l":null,"m":null,"n":null,
  "o":"2020-03-17 08:20"},{"p":"***783755538501","q":"***3813620001","r":"2020-03-18 08:25","s":"2020-03-18 12:58","t":"D7126","u":"ZHQ","v":"*海",
  "w":"ZWQ","x":"*西","y":264.0,"z":"2020-03-16 23:50:25","aa":"300","ab":"出票成功","ac":"260","ad":"xxx票务","ae":"纸质票","af":"E3W5343313","ag":
  "**票务—1号","ah":[{"ai":"****3759745069","aj":"*886119","ak":"陈*","al":"B","am":"**","an":"E*****","ao":264.0,"ap":"1","aq":
  "成人票","ar":"14","as":"二等座","au":264.0,"av":"14","aw":"二等座","ax":"","ay":"4","az":"5D号","ba":null,"bb":"**5343313"}]

所以我们需要做的就是,过滤掉第一类卖家数据,留下第二类敏感数据做检测。

首先简单介绍下 GBDT(Gradient boosting Decision Tree)梯度提升决策树,它的主要思想是采用加法模型的方式不断减小训练过程产生的残差来达到将数据分类或者回归的算法,它的基学习器采用提升树。提升树模型可以表现为决策树的加法模型,

1592808161_5ef052e167325.png!small

其中 T(x;Θm) 表示决策树,Θm 表示树的参数,M 为树的个数。

他的训练过程大致是先构建一个回归决策树,然后用提升的思想拟合上一个模型的残差,结果由训练出来的多棵决策树的结果累加起来产生。这是一种由多个弱分类器构建而成的分类算法是一种典型的集成学习算法(Ensemble)。

1592808139_5ef052cb6b3dd.png!small

图 2

(1)特征工程

俗话说特征决定模型逼近上限的程度,根据需求从业务中提取了 40 多个特征,由于篇幅过长,在这里只能做一个归类,大致分为{访问量,访问行为,参数类型,返回类型,敏感信息占比,特定信息占比,请求成功率}共 40 多个特征用于分类器的学习。当前的项目中训练集采用了 10000 条数据,手工 + 规则进行标注和修正,其中正样本 3400 多条,负样本 6500 多条,正负比例大约是 1:2。

这里 1、3 标为敏感数据、2、4 标为非敏感数据(卖家 *** 息),通过以下特征我们建立第一棵 Tree,

Features 1 构建 Tree 1

请求 URL 访问量 参数类型 返回类型 敏感信息占比 特定信息占比 请求成功率 label
1 *.x.com/x/y/z 37 21 1 0.9459459459459459 1.0 1 1
2 *.x.com/x/{s}/y/z/b 25 17 1 0.84 0.51 0.9 0
3 *.x.com/x/y/z/c 8 4 1 1 1.0 1 1
4 *.x.com/z/y/z/d 9 6 1 0.3 0.2 1 0

1592808189_5ef052fd5ad74.png!small

根据 Tree1 预测结果计算残差,获得一个残差表。

Feature 2 残差表

请求 URL 访问量 参数类型 返回类型 敏感信息占比 特定信息占比 请求成功率 label
1 *.x.com/x/y/z 11 16 0 0.7 0.5 1.0 1
2 *.x.com/x/{s}/y/z/b 16 8 1 0.64 0.0 0.8 0
3 *.x.com/x/y/z/c 13 4 1 0.5 0.0 0.5 1
4 *.x.com/z/y/z/d 3 1 1 0.2 0.0 0.1 0

根据残差构建 Tree2,以此类推

1592808240_5ef05330e9521.png!small

直到达到训练指标便结束训练。最后对所有树进行线性加法计算。

1592808267_5ef0534baba6d.png!small

(2)模型评估

模型评估可以用最简单的方式,这里采用的是精度(precision)和召回率(recall)来评估模型。这里选择另外一天的全量数据作为验证集,一共大约有 1000 多条数据,还是手工标注好正负样本集,过模型后分别统计精度、召回情况。Precision = TP / (TP + FP)、Recall = TP / (TP + FN)。其中 TP(true positive)为真正例,FP(false positive)为假正例,FN(false negtive)假反例。从实验来看,

Precision = 421/ (421+ 43) = 0.907 Recall = 421/ (421 + 11) = 0.97

也就是在另外一天的数据表现来看,精度能做到每日的 URL 是 0.9 左右,召回率能做到 0.97,在这个基础上我们需要去看下哪些漏掉了、什么原因漏掉了,经过对特征重要性进一步分析,模型应该是把很多订单类的文本识别为了 *** 息,主要原因是订单的特征和公开的特征非常像,里面都有类似 shopid、sellerid、http,也存在固定电话等。在这种情况下需要新增一个专门标注订单的特征项 isOrder,如果看到这个字段为 1,就自动标注为非卖家信息,再去训练该模型,最后的结果确实也提升了一些 recall,但还是不尽人意。

这种情况下,就需要另外的手段来弥补不足,我们专门从流量里清洗出了带有订单标志的流量,单独进行检测。这样做既不会增加工作量,也能很好地弥补模型的不足。

最后的效果是,在模型预测前,每日会有 3000 多条报警记录需要人工去看,而经过模型过滤后每日告警减少到 100 多条,不过感觉还是有优化的空间,最好的做法是把很多无法识别或识别错的,用规则过滤掉,尽量控制误报同时降低漏报。

0x02、扫描是否存在越权

1、漏洞扫描

漏洞扫描,主要是基于模型输出的 API 去主动扫描和发现该 API 是否存在漏洞的情况,这是一个主动发现的过程,它和传统的漏洞感知、NIDS 的差别在于,它在不被攻击的情况下也能发现基础漏洞的存在。这里对于权限的扫描主要是通过 Java 的 http 接口重新访问该 URL,类似某些公司的回音墙,然后根据 response 来校验是否获取到了敏感信息,来确定是否有漏洞存在。扫描器支持多种引擎,这里选择 Chrome 和 http 两种引擎,主要是为了解决 js 跳转等问题,不同的引擎优缺点不太一样,要根据适合的场景来选择。目前可以支持的漏洞类型如下:

具有权限问题的 URL

​ JSONP

​ URL 重定向

​ 非预期文件读取

​ 在线数据库异常链接

​ 敏感文件下载等等

先看下扫描器的框架,如下:

1592808360_5ef053a81b9fa.png!small

图 3 漏洞扫描框架

扫描器在扫描权限问题的时候需要具备如下能力:

1、登录态的设置能力,没有登录态连基本的权限都没有,所以这里必须设置。

2、多引擎的能力,不同引擎有不同的优缺点需要切换使用。

3、多线程能力,多线程去运行才能提高检测效率。

4、漏洞的检测能力。

(1)扫描查询是否存在越权

接口的访问形式多种多样,本文就以某一种形式来讨论,例如遇到以下类型的 URL

代码块

Java

https://x.y.com/a/b/getOrderDetail?orderNo=11000603698171

从上面的接口可以看到,这是一个查询订单的接口,很显然上面的模型会把它预测为是敏感信息,接下来数据来到扫描器这一层,扫描器就要对他进行重放一次,看是否能拿到之前的 response 信息,在重放之前我们先要设置下登录态,如果单纯地去渲染可能没办法达到一个很好的效果,这里需要给扫描工具建立一些登录态,能够进入系统内部去调用他们的接口能力。

代码块

Java

private static Map<String, String> headers = new HashMap<>();
static {
    // 初始化header
    headers.put("Referer", Constant.REFERER);
    headers.put("Host", "1.x.com");
    headers.put("X-Requested-With", "XMLHttpRequest");
    headers.put("User-Agent", Constant.UA);
    headers.put("Cookie", Constant.COOKIE_1 + Constant.COOKIE_2 + Constant.COOKIE_3 + Constant.COOKIE_4);
}

有了上面的 header 可能还不够,在适当的时候需要去替换 URL 里面的各种参数,例如 token 信息等等,所以还需要判断这个接口的鉴权是在哪里做的,token 的校验有的是通过 url 传入的,那我们需要通过替换过后再进行重放,否则还是用的老登录态会导致误报。接下来就需要对接口做各种尝试来判断是否具有权限问题或其他漏洞。这个过程主要是通过事先做好的 URL 工具类访问下,访问的主要接口如下:

代码块

Java

String response = HttpUtils.get(url, null, headers,3000, 3000, "UTF-8");

返回值如下

代码块

Java

{"data":{"orderNo":"11000603698171","price":"19.80","quantity":1,"originalPrice":"23.80","stock":0,"remainStock":0,"dailyStock":0,
"dailyRemainStock":0,"salesVolume":0,"startTime":null,"status":0,"offlineTime":null,"productId":613196357,"skuCode":""}],"consignee":
{"buyerNickName":"甜美xxxx","name":"xxx","phone":"11111111","address":"*********xxxxxxx","zipCode":""},"userRemark":""}}

如果能访问成功,这说明这类接口是有问题的,存在水平查询权限问题,反之则不存在越权问题。

当然这还远远不够,单个的访问效率是非常低的,每日可能有好几十万的链接需要回放单线程,这样是没办法满足我们的需求的,所以扫描器需要采用多线程的方式,用 100 个甚至更多的线程来同时执行。

代码块

Java

/**
  * 多线程执行
  * @param urls
  */
public static void execute(List<VulBase> urls) {
  for (VulBase vulBase : urls) {
    futureList.add(executorService.submit(new ProcessThread(vulBase)));
  }
  for (Future<Result> resultFuture : futureList) {
    try {
      Result result = resultFuture.get();
      if(result.getSuccess() == true) {
        System.out.println(result.getSuccess() + "," + result.getMsg());
      }
    } catch (InterruptedException e) {
      e.printStackTrace();
    } catch (ExecutionException e) {
      e.printStackTrace();
    }
  }
  futureList.clear();
}

这样同时运行 100 个任务。效率提升会非常明显,原来 1 万条 URL 需要 3 小时左右的回放时间,采用多线程后只需要 5 分钟。这里也可以根据机器性能,适当调整自己的线程数。

(2)扫描修改类接口是否存在越权

绝大部分增删改的动作都是 POST 请求,这里同样需要过滤掉那些无效的 POST 请求,以免产生大量误报,真正的难点是要找出增删改过数据库的 POST 请求,这个过程比较困难,从表面是无法识别的,我们一般把流量中记录的 traceid 和 db 的 traceid 进行一个关联,如果关联上就说明在这次访问中增删改过数据库,然后就需要构造访问包的方式去访问系统,这个过程也比较危险,因为你很有可能删除掉了非常重要的信息,所以在这里需要控制好登录态就显得非常重要了,否则很可能删除或修改了别人的数据导致线上故障。代码如下:

代码块

Java

String response = HttpUtils.post(url, body, headers,3000, 3000, "UTF-8");

如果能通过自己的登录态去 POST 这个请求并且改变了别人数据库里的内容,那可能就存在问题了。

0x03、运营

检测结果出来后还有一个比较重要的工作就是运营,我们通过安全运营可以去除一些扫描器的误报,并且还可以发现该接口的一些其他问题,或者进一步被利用的可能,比如是否可以被遍历,还可以横向思考是否同系统还有其他接口也存在这类问题,用来发现更多的流量里面没有的 URL,因为有些 URL 非常重要,但是他一天也没几个人访问,甚至没有访问,这种就只能通过运营的能力来发现,有点类似根据扫描结果来做一个有指导的 SDL。还是从上面的 URL 来看,

代码块

Java

https://x.y.com/a/b/getOrderDetail?orderNo=11000603698171

如果他存在权限的问题,接下来运营还需要确认是否可以通过 orderNo 来遍历全部的订单信息,如果可以那这个漏洞的危害就变得非常大了,还可以排查出 y.com 这个域名是否存在其他的重要接口,大概率也会存在问题,从而达到横向、纵向的权限梳理,尽量全的覆盖全域的系统和 URL。

0x04、小结

权限扫描中最难的问题,就是我们对业务的无法理解导致大量误报,最终导致的结果就是不可运营性,这其中的误报包括:

​ 1、返回信息的不确定(是否是敏感信息)

​ 2、对数据库的修改是否是合法操作

笔者主要是通过限定返回信息来缩小敏感信息的范围并配合模型和规则去除误报和无用的返回信息。这里采样起到了一个非常重要的作用,对于全量的数据我们没必要全部进行校验,只需要对同一类接口进行校验就够了,这样可以大大降低引擎的压力同时也能提升效率减少误报。

三、结语

权限问题治理、发现、检测对于每一家公司都是非常困难的,困难点主要源于我们对业务的不理解。我们最好在事前、事中、事后体系化去解决这类问题,没有银弹。事前可以通过架构层,统一开发框架,统一编码规范,结合白盒扫描等等方式,事后的解决办法主要是从具备敏感资产的这个点进入,并做好权限的动态配置和校验,从而达到检测权限漏洞的能力。其中最主要的是我们要具备每一个系统的权限动态配置能力,这样才能进入到系统对 URL 进行扫描。时间仓促本文作为漏洞扫描系统的一个功能和大家做一个技术上的探讨和分析,盲人摸象而已,实际权限问题的实践远比想象复杂,后续有机会再做进一步交流。

附录:

https://www.researchgate.net/publication/221023992_A_pattern_tree-based_approach_to_learning_URL_normalization_rules

https://en..org/wiki/Gradient_boosting#Gradient_tree_boosting*********

https://yq.aliyun.com/articles/673308

共收到 7 条回复 时间 点赞
恒温 将本帖设为了精华贴 07月08日 10:31

没有底子看不太懂。。

我次奥,我看不懂。。。挨个查来啃一波

深奥,确实没怎么看懂

优秀~~
用真实的业务参数 + 决策算法组合生成了检查数据权限场景
不知道用户角色多 token 的匹配怎么落地的

仅楼主可见
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册