RSS Feed
更好更安全的互联网
  • 从 0 开始入门 Chrome Ext 安全(一) — 了解一个 Chrome Ext

    2019-11-29

    作者:LoRexxar'@知道创宇404实验室
    时间:2019年11月21日

    在2019年初,微软正式选择了Chromium作为默认浏览器,并放弃edge的发展。并在19年4月8日,Edge正式放出了基于Chromium开发的Edge Dev浏览器,并提供了兼容Chrome Ext的配套插件管理。再加上国内的大小国产浏览器大多都是基于Chromium开发的,Chrome的插件体系越来越影响着广大的人群。

    在这种背景下,Chrome Ext的安全问题也应该受到应有的关注,《从0开始入门Chrome Ext安全》就会从最基础的插件开发开始,逐步研究插件本身的恶意安全问题,恶意网页如何利用插件漏洞攻击浏览器等各种视角下的安全问题。

    第一部分我们就主要来聊聊关于Chrome Ext的一些基础。

    获取一个插件的代码

    Chrome Ext的存在模式类似于在浏览器层新加了一层解释器,在我们访问网页的时候,插件会加载相应的html、js、css,并解释执行。

    所以Chrome Ext的代码也就是html、js、css这类,那我们如何获取插件的代码呢?

    当我们访问扩展程序的页面可以获得相应的插件id

    然后我们可以在https://chrome-extension-downloader.com/中下载相应的crx包。

    把crx改名成zip之后解压缩就可以了

    manifest.json

    在插件的代码中,有一个重要的文件是manifest.json,在manifest.json中包含了整个插件的各种配置,在配置文件中,我们可以找到一个插件最重要的部分。

    首先是比较重要的几个字段

    • browser_action
      • 这个字段主要负责扩展图标点击后的弹出内容,一般为popup.html
    • content_scripts
      • matches 代表scripts插入的时机,默认为document_idle,代表页面空闲时
      • js 代表插入的scripts文件路径
      • run_at 定义了哪些页面需要插入scripts
    • permissions
      • 这个字段定义了插件的权限,其中包括从浏览器tab、历史纪录、cookie、页面数据等多个维度的权限定义
    • content_security_policy
      • 这个字段定义了插件页面的CSP
      • 但这个字段不影响content_scripts里的脚本
    • background
      • 这个字段定义插件的后台页面,这个页面在默认设置下是在后台持续运行的,只随浏览器的开启和关闭
      • persistent 定义了后台页面对应的路径
      • page 定义了后台的html页面
      • scripts 当值为false时,background的页面不会在后台一直运行

    在开始Chrome插件的研究之前,除了manifest.json的配置以外,我们还需要了解一下围绕chrome建立的插件结构。

    Chrome Ext的主要展现方式

    browserAction - 浏览器右上角

    浏览器的右上角点击触发的就是mainfest.json中的browser_action

    其中页面内容来自popup.html

    pageAction

    pageAction和browserAction类似,只不过其中的区别是,pageAction是在满足一定的条件下才会触发的插件,在不触发的情况下会始终保持灰色。

    contextMenus 右键菜单

    通过在chrome中调用chrome.contextMenus这个API,我们可以定义在浏览器中的右键菜单。

    当然,要控制这个api首先你必须申请控制contextMenus的权限。

    一般来说,这个api会在background中被定义,因为background会一直在后台加载。

    https://developer.chrome.com/extensions/contextMenus

    override - 覆盖页面

    chrome提供了override用来覆盖chrome的一些特定页面。其中包括历史记录、新标签页、书签等...

    比如Toby for Chrome就是一个覆盖新标签页的插件

    devtools - 开发者工具

    chrome允许插件重构开发者工具,并且相应的操作。

    插件中关于devtools的生命周期和F12打开的窗口时一致的,当F12关闭时,插件也会自动结束。

    而在devtools页面中,插件有权访问一组特殊的API,这组API只有devtools页面中可以访问。

    https://developer.chrome.com/extensions/devtools

    option - 选项

    option代表着插件的设置页面,当选中图标之后右键选项可以进入这个页面。

    omnibox - 搜索建议

    在chrome中,如果你在地址栏输入非url时,会将内容自动传到google搜索上。

    omnibox就是提供了对于这个功能的魔改,我们可以通过设置关键字触发插件,然后就可以在插件的帮助下完成搜索了。

    这个功能通过chrome.omnibox这个api来定义。

    notifications - 提醒

    notifications代表右下角弹出的提示框

    权限体系和api

    在了解了各类型的插件的形式之后,还有一个比较重要的就是Chrome插件相关的权限体系和api。

    Chrome发展到这个时代,其相关的权限体系划分已经算是非常细致了,具体的细节可以翻阅文档。

    https://developer.chrome.com/extensions/declare_permissions

    抛开Chrome插件的多种表现形式不谈,插件的功能主要集中在js的代码里,而js的部分主要可以划分为5种injected script、content-script、popup js、background js和devtools js.

    • injected script 是直接插入到页面中的js,和普通的js一致,不能访问任何扩展API.
    • content-script 只能访问extension、runtime等几个有限的API,也可以访问dom.
    • popup js 可以访问大部分API,除了devtools,支持跨域访问
    • background js 可以访问大部分API,除了devtools,支持跨域访问
    • devtools js 只能访问devtools、extension、runtime等部分API,可以访问dom
    JS是否能访问DOM是否能访问JS是否可以跨域
    injected script可以访问可以访问不可以
    content script可以访问不可以不可以
    popup js不可直接访问不可以可以
    background js不可直接访问不可以可以
    devtools js可以访问可以访问不可以

    同样的,针对这多种js,我们也需要特殊的方式进行调试

    • injected script: 直接F12就可以调试
    • content-script:在F12中console选择相应的域
    • popup js: 在插件右键的列表中有审查弹出内容
    • background js: 需要在插件管理页面点击背景页然后调试

    通信方式

    在前面介绍过各类js之后,我们提到一个重要的问题就是,在大部分的js中,都没有给与访问js的权限,包括其中比较关键的content script.

    那么插件怎么和浏览器前台以及相互之间进行通信呢?

    -injected-scriptcontent-scriptpopup-jsbackground-js
    injected-script-window.postMessage--
    content-scriptwindow.postMessage-chrome.runtime.sendMessage chrome.runtime.connectchrome.runtime.sendMessage chrome.runtime.connect
    popup-js-chrome.tabs.sendMessage chrome.tabs.connect-chrome.extension. getBackgroundPage()
    background-js-chrome.tabs.sendMessage chrome.tabs.connectchrome.extension.getViews-
    devtools-jschrome.devtools.inspectedWindow.eval-chrome.runtime.sendMessagechrome.runtime.sendMessage

    popup和background两个域互相直接可以调用js并且访问页面的dom。

    popup可以直接用chrome.extension.getBackgroundPage()获取background页面的对象,而background可以直接用chrome.extension.getViews({type:'popup'})获取popup页面的对象。

    popup\background 和 content js

    popup\background 和 content js之间沟通的方式主要依赖chrome.tabs.sendMessagechrome.runtime.onMessage.addListener这种有关事件监听的交流方式。

    发送方使用chrome.tabs.sendMessage,接收方使用chrome.runtime.onMessage.addListener监听事件。

    接收方

    injected script 和 content-script

    由于injected script就相当于页面内执行的js,所以它没权限访问chrome对象,所以他们直接的沟通方式主要是利用window.postMessage或者通过DOM事件来实现。

    injected-script中:

    content script中:

    popup\background 动态注入js

    popup\background没办法直接访问页面DOM,但是可以通过chrome.tabs.executeScript来执行脚本,从而实现对页面DOM的操作。

    要注意这种操作要求必须有页面权限

    js

    chrome.storage

    chrome 插件还有专门的储存位置,其中包括chrome.storage和chrome.storage.sync两种,其中的区别是:

    • chrome.storage 针对插件全局,在插件各个位置保存的数据都会同步。
    • chrome.storage.sync 根据账户自动同步,不同的电脑登陆同一个账户都会同步。

    插件想访问这个api需要提前声明storage权限。

    总结

    这篇文章主要描述了关于Chrome ext插件相关的许多入门知识,在谈及Chrome ext的安全问题之前,我们可能需要先了解一些关于Chrome ext开发的问题。

    在下一篇文章中,我们将会围绕Chrome ext多个维度的安全问题进行探讨,在现代浏览器体系中,Chrome ext到底可能会带来什么样的安全问题。

    re


    Paper

    本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1082/

    作者:吴烦恼 | Categories:安全研究技术分享 | Tags:
  • 代码分析引擎 CodeQL 初体验

    2019-11-19

    作者:w7ay@知道创宇404实验室
    日期:2019年11月18日

    QL是一种查询语言,支持对C++,C#,Java,JavaScript,Python,go等多种语言进行分析,可用于分析代码,查找代码中控制流等信息。

    之前笔者有简单的研究通过JavaScript语义分析来查找XSS,所以对于这款引擎有浓厚的研究兴趣 。

    安装

    1.下载分析程序:https://github.com/github/codeql-cli-binaries/releases/latest/download/codeql.zip

    分析程序支持主流的操作系统,Windows,Mac,Linux

    2.下载相关库文件:https://github.com/Semmle/ql

    库文件是开源的,我们要做的是根据这些库文件来编写QL脚本。

    3.下载最新版的VScode,安装CodeQL扩展程序:https://marketplace.visualstudio.com/items?itemName=GitHub.vscode-codeql

    • 用vscode的扩展可以方便我们看代码
    • 然后到扩展中心配置相关参数
    image-20191116223514188
    image-20191116223649659

    4.

    • cli填写下载的分析程序路径就行了,windows可以填写codeql.cmd
    • 其他地方默认就行

    建立数据库

    以JavaScript为例,建立分析数据库,建立数据库其实就是用分析程序来分析源码。到要分析源码的根目录,执行codeql database create jstest --language=javascript

    image-20191117111305487

    接下来会在该目录下生成一个jstest的文件夹,就是数据库的文件夹了。

    接着用vscode打开之前下载的ql库文件,在ql选择夹中添加刚才的数据库文件,并设置为当前数据库。

    image-20191117111940680

    接着在QL/javascript/ql/src目录下新建一个test.ql,用来编写我们的ql脚本。为什么要在这个目录下建立文件呢,因为在其他地方测试的时候import javascript导入不进来,在这个目录下,有个javascript.qll就是基础类库,就可以直接引入import javascript,当然可能也有其他的方法。

    看它的库文件,它基本把JavaScript中用到的库,或者其他语言的定义语法都支持了。

    image-20191117113240934

    输出一段hello world试试?

    image-20191118130324959

    语义分析查找的原理

    刚开始接触ql语法的时候可能会感到它的语法有些奇怪,它为什么要这样设计?我先说说自己之前研究基于JavaScript语义分析查找dom-xss是怎样做的。

    首先一段类似这样的javascript代码

    常规的思路是,我们先找到document.write函数,由这个函数的第一个参数回溯寻找,如果发现它最后是location.hash.split("#")[1];,就寻找成功了。我们可以称document.writesink,称location.hash.splitsource。基于语义分析就是由sink找到source的过程(当然反过来找也是可以的)。

    而基于这个目标,就需要我们设计一款理解代码上下文的工具,传统的正则搜索已经无法完成了。

    第一步要将JavaScript的代码转换为语法树,通过pyjsparser可以进行转换

    最终就得到了如下一个树结构

    image-20191118131714042

    这些树结构的一些定义可以参考:https://esprima.readthedocs.io/en/3.1/syntax-tree-format.html

    大概意思可以这样理解:变量param是一个Identifier类型,它的初始化定义的是一个MemberExpression表达式,该表达式其实也是一个CallExpression表达式,CallExpression表达式的参数是一个Literal类型,而它具体的定义又是一个MemberExpression表达式。

    第二步,我们需要设计一个递归来找到每个表达式,每一个Identifier,每个Literal类型等等。我们要将之前的document.write转换为语法树的形式

    location.hash也是同理

    在找到了这些sinksource后,再进行正向或反向的回溯分析。回溯分析也会遇到不少问题,如何处理对象的传递,参数的传递等等很多问题。之前也基于这些设计写了一个在线基于语义分析的demo

    QL语法

    QL语法虽然隐藏了语法树的细节,但其实它提供了很多类似,函数的概念来帮助我们查找相关'语法'。

    依旧是这段代码为例子

    上文我们已经建立好了查询的数据库,现在我们分别来看如何查找sink,source,以及怎样将它们关联起来。

    我也是看它的文档:https://help.semmle.com/QL/learn-ql/javascript/introduce-libraries-js.html 学习的,它提供了很多方便的函数,我没有仔细看。我的查询语句都是基于语法树的查询思想,可能官方已经给出了更好的查询方式,所以看看就行了,反正也能用。

    查询 document.write

    这段语句的意思是查找document.write,并输出它的第一个参数

    image-20191118134431944

    查找 location.hash.split

    查找location.hash.split并输出

    image-20191118134554200

    数据流分析

    接着从sink来找到source,将上面语句组合下,按照官方的文档来就行

    image-20191118134945286

    将source和sink输出,就能找到它们具体的定义。

    我们找到查询到的样本

    image-20191118135549113

    可以发现它的回溯是会根据变量,函数的返回值一起走的。

    当然从source到sink也不可能是一马平川的,中间肯定也会有阻挡的条件,ql官方有给出解决方案。总之就是要求我们更加细化完善ql查询代码。

    接下来放出几个查询还不精确的样本,大家可以自己尝试如何进行查询变得精确。

    最后

    CodeQL将语法树抽离出来,提供了一种用代码查询代码的方案,更增强了基于数据分析的灵活度。唯一的遗憾是它并没有提供很多查询漏洞的规则,它让我们自己写。这也不由得让我想起另一款强大的基于语义的代码审计工具fortify,它的规则库是公开的,将这两者结合一下说不定会有不一样的火花。

    Github公告说将用它来搜索开源项目中的问题,而作为安全研究员的我们来说,也可以用它来做类似的事情?


    Paper

    本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1078/

    作者:吴烦恼 | Categories:技术分享 | Tags:
  • 协议层的攻击——HTTP请求走私

    2019-10-11

    作者:mengchen@知道创宇404实验室
    日期:2019年10月10日

    1. 前言

    最近在学习研究BlackHat的议题,其中有一篇议题——"HTTP Desync Attacks: Smashing into the Cell Next Door"引起了我极大地兴趣,在其中,作者讲述了HTTP走私攻击这一攻击手段,并且分享了他的一些攻击案例。我之前从未听说过这一攻击方式,决定对这一攻击方式进行一个完整的学习梳理,于是就有了这一篇文章。

    当然了,作为这一攻击方式的初学者,难免会有一些错误,还请诸位斧正。

    2. 发展时间线

    最早在2005年,由Chaim Linhart,Amit Klein,Ronen Heled和Steve Orrin共同完成了一篇关于HTTP Request Smuggling这一攻击方式的报告。通过对整个RFC文档的分析以及丰富的实例,证明了这一攻击方式的危害性。

    https://www.cgisecurity.com/lib/HTTP-Request-Smuggling.pdf

    在2016年的DEFCON 24 上,@regilero在他的议题——Hiding Wookiees in HTTP中对前面报告中的攻击方式进行了丰富和扩充。

    https://media.defcon.org/DEF%20CON%2024/DEF%20CON%2024%20presentations/DEF%20CON%2024%20-%20Regilero-Hiding-Wookiees-In-Http.pdf

    在2019年的BlackHat USA 2019上,PortSwigger的James Kettle在他的议题——HTTP Desync Attacks: Smashing into the Cell Next Door中针对当前的网络环境,展示了使用分块编码来进行攻击的攻击方式,扩展了攻击面,并且提出了完整的一套检测利用流程。

    https://www.blackhat.com/us-19/briefings/schedule/#http-desync-attacks-smashing-into-the-cell-next-door-15153

    3. 产生原因

    HTTP请求走私这一攻击方式很特殊,它不像其他的Web攻击方式那样比较直观,它更多的是在复杂网络环境下,不同的服务器对RFC标准实现的方式不同,程度不同。这样一来,对同一个HTTP请求,不同的服务器可能会产生不同的处理结果,这样就产生了了安全风险。

    在进行后续的学习研究前,我们先来认识一下如今使用最为广泛的HTTP 1.1的协议特性——Keep-Alive&Pipeline

    HTTP1.0之前的协议设计中,客户端每进行一次HTTP请求,就需要同服务器建立一个TCP链接。而现代的Web网站页面是由多种资源组成的,我们要获取一个网页的内容,不仅要请求HTML文档,还有JS、CSS、图片等各种各样的资源,这样如果按照之前的协议设计,就会导致HTTP服务器的负载开销增大。于是在HTTP1.1中,增加了Keep-AlivePipeline这两个特性。

    所谓Keep-Alive,就是在HTTP请求中增加一个特殊的请求头Connection: Keep-Alive,告诉服务器,接收完这次HTTP请求后,不要关闭TCP链接,后面对相同目标服务器的HTTP请求,重用这一个TCP链接,这样只需要进行一次TCP握手的过程,可以减少服务器的开销,节约资源,还能加快访问速度。当然,这个特性在HTTP1.1中是默认开启的。

    有了Keep-Alive之后,后续就有了Pipeline,在这里呢,客户端可以像流水线一样发送自己的HTTP请求,而不需要等待服务器的响应,服务器那边接收到请求后,需要遵循先入先出机制,将请求和响应严格对应起来,再将响应发送给客户端。

    现如今,浏览器默认是不启用Pipeline的,但是一般的服务器都提供了对Pipleline的支持。

    为了提升用户的浏览速度,提高使用体验,减轻服务器的负担,很多网站都用上了CDN加速服务,最简单的加速服务,就是在源站的前面加上一个具有缓存功能的反向代理服务器,用户在请求某些静态资源时,直接从代理服务器中就可以获取到,不用再从源站所在服务器获取。这就有了一个很典型的拓扑结构。

    Topology

    一般来说,反向代理服务器与后端的源站服务器之间,会重用TCP链接。这也很容易理解,用户的分布范围是十分广泛,建立连接的时间也是不确定的,这样TCP链接就很难重用,而代理服务器与后端的源站服务器的IP地址是相对固定,不同用户的请求通过代理服务器与源站服务器建立链接,这两者之间的TCP链接进行重用,也就顺理成章了。

    当我们向代理服务器发送一个比较模糊的HTTP请求时,由于两者服务器的实现方式不同,可能代理服务器认为这是一个HTTP请求,然后将其转发给了后端的源站服务器,但源站服务器经过解析处理后,只认为其中的一部分为正常请求,剩下的那一部分,就算是走私的请求,当该部分对正常用户的请求造成了影响之后,就实现了HTTP走私攻击。

    3.1 CL不为0的GET请求

    其实在这里,影响到的并不仅仅是GET请求,所有不携带请求体的HTTP请求都有可能受此影响,只因为GET比较典型,我们把它作为一个例子。

    RFC2616中,没有对GET请求像POST请求那样携带请求体做出规定,在最新的RFC7231的4.3.1节中也仅仅提了一句。

    https://tools.ietf.org/html/rfc7231#section-4.3.1

    sending a payload body on a GET request might cause some existing implementations to reject the request

    假设前端代理服务器允许GET请求携带请求体,而后端服务器不允许GET请求携带请求体,它会直接忽略掉GET请求中的Content-Length头,不进行处理。这就有可能导致请求走私。

    比如我们构造请求

    前端服务器收到该请求,通过读取Content-Length,判断这是一个完整的请求,然后转发给后端服务器,而后端服务器收到后,因为它不对Content-Length进行处理,由于Pipeline的存在,它就认为这是收到了两个请求,分别是

    这就导致了请求走私。在本文的4.3.1小节有一个类似于这一攻击方式的实例,推荐结合起来看下。

    3.2 CL-CL

    RFC7230的第3.3.3节中的第四条中,规定当服务器收到的请求中包含两个Content-Length,而且两者的值不同时,需要返回400错误。

    https://tools.ietf.org/html/rfc7230#section-3.3.3

    但是总有服务器不会严格的实现该规范,假设中间的代理服务器和后端的源站服务器在收到类似的请求时,都不会返回400错误,但是中间代理服务器按照第一个Content-Length的值对请求进行处理,而后端源站服务器按照第二个Content-Length的值进行处理。

    此时恶意攻击者可以构造一个特殊的请求

    中间代理服务器获取到的数据包的长度为8,将上述整个数据包原封不动的转发给后端的源站服务器,而后端服务器获取到的数据包长度为7。当读取完前7个字符后,后端服务器认为已经读取完毕,然后生成对应的响应,发送出去。而此时的缓冲区去还剩余一个字母a,对于后端服务器来说,这个a是下一个请求的一部分,但是还没有传输完毕。此时恰巧有一个其他的正常用户对服务器进行了请求,假设请求如图所示。

    从前面我们也知道了,代理服务器与源站服务器之间一般会重用TCP连接。

    这时候正常用户的请求就拼接到了字母a的后面,当后端服务器接收完毕后,它实际处理的请求其实是

    这时候用户就会收到一个类似于aGET request method not found的报错。这样就实现了一次HTTP走私攻击,而且还对正常用户的行为造成了影响,而且后续可以扩展成类似于CSRF的攻击方式。

    但是两个Content-Length这种请求包还是太过于理想化了,一般的服务器都不会接受这种存在两个请求头的请求包。但是在RFC2616的第4.4节中,规定:如果收到同时存在Content-Length和Transfer-Encoding这两个请求头的请求包时,在处理的时候必须忽略Content-Length,这其实也就意味着请求包中同时包含这两个请求头并不算违规,服务器也不需要返回400错误。服务器在这里的实现更容易出问题。

    https://tools.ietf.org/html/rfc2616#section-4.4

    3.3 CL-TE

    所谓CL-TE,就是当收到存在两个请求头的请求包时,前端代理服务器只处理Content-Length这一请求头,而后端服务器会遵守RFC2616的规定,忽略掉Content-Length,处理Transfer-Encoding这一请求头。

    chunk传输数据格式如下,其中size的值由16进制表示。

    Lab 地址:https://portswigger.net/web-security/request-smuggling/lab-basic-cl-te

    构造数据包

    连续发送几次请求就可以获得该响应。

    image-20191009002040605

    由于前端服务器处理Content-Length,所以这个请求对于它来说是一个完整的请求,请求体的长度为6,也就是

    当请求包经过代理服务器转发给后端服务器时,后端服务器处理Transfer-Encoding,当它读取到0\r\n\r\n时,认为已经读取到结尾了,但是剩下的字母G就被留在了缓冲区中,等待后续请求的到来。当我们重复发送请求后,发送的请求在后端服务器拼接成了类似下面这种请求。

    服务器在解析时当然会产生报错了。

    3.4 TE-CL

    所谓TE-CL,就是当收到存在两个请求头的请求包时,前端代理服务器处理Transfer-Encoding这一请求头,而后端服务器处理Content-Length请求头。

    Lab地址:https://portswigger.net/web-security/request-smuggling/lab-basic-te-cl

    构造数据包

    image-20191009095101287

    由于前端服务器处理Transfer-Encoding,当其读取到0\r\n\r\n时,认为是读取完毕了,此时这个请求对代理服务器来说是一个完整的请求,然后转发给后端服务器,后端服务器处理Content-Length请求头,当它读取完12\r\n之后,就认为这个请求已经结束了,后面的数据就认为是另一个请求了,也就是

    成功报错。

    3.5 TE-TE

    TE-TE,也很容易理解,当收到存在两个请求头的请求包时,前后端服务器都处理Transfer-Encoding请求头,这确实是实现了RFC的标准。不过前后端服务器毕竟不是同一种,这就有了一种方法,我们可以对发送的请求包中的Transfer-Encoding进行某种混淆操作,从而使其中一个服务器不处理Transfer-Encoding请求头。从某种意义上还是CL-TE或者TE-CL

    Lab地址:https://portswigger.net/web-security/request-smuggling/lab-ofuscating-te-header

    构造数据包

    image-20191009111046828

    4. HTTP走私攻击实例——CVE-2018-8004

    4.1 漏洞概述

    Apache Traffic Server(ATS)是美国阿帕奇(Apache)软件基金会的一款高效、可扩展的HTTP代理和缓存服务器。

    Apache ATS 6.0.0版本至6.2.2版本和7.0.0版本至7.1.3版本中存在安全漏洞。攻击者可利用该漏洞实施HTTP请求走私攻击或造成缓存中毒。

    在美国国家信息安全漏洞库中,我们可以找到关于该漏洞的四个补丁,接下来我们详细看一下。

    CVE-2018-8004 补丁列表

    注:虽然漏洞通告中描述该漏洞影响范围到7.1.3版本,但从github上补丁归档的版本中看,在7.1.3版本中已经修复了大部分的漏洞。

    4.2 测试环境

    4.2.1 简介

    在这里,我们以ATS 7.1.2为例,搭建一个简单的测试环境。

    环境组件介绍

    环境拓扑图

    ats-topology

    Apache Traffic Server 一般用作HTTP代理和缓存服务器,在这个测试环境中,我将其运行在了本地的Ubuntu虚拟机中,把它配置为后端服务器LAMP&LNMP的反向代理,然后修改本机HOST文件,将域名ats.mengsec.comlnmp.mengsec,com解析到这个IP,然后在ATS上配置映射,最终实现的效果就是,我们在本机访问域名ats.mengsec.com通过中间的代理服务器,获得LAMP的响应,在本机访问域名lnmp.mengsec,com,获得LNMP的响应。

    为了方便查看请求的数据包,我在LNMP和LAMP的Web目录下都放置了输出请求头的脚本。

    LNMP:

    LAMP:

    4.2.2 搭建过程

    在GIthub上下载源码编译安装ATS。

    安装依赖&常用工具。

    然后解压源码,进行编译&安装。

    安装完毕后,配置反向代理和映射。

    编辑records.config配置文件,在这里暂时把ATS的缓存功能关闭。

    编辑remap.config配置文件,在末尾添加要映射的规则表。

    配置完毕后重启一下服务器使配置生效,我们可以正常访问来测试一下。

    为了准确获得服务器的响应,我们使用管道符和nc来与服务器建立链接。

    image-20191007225109915

    可以看到我们成功的访问到了后端的LAMP服务器。

    同样的可以测试,代理服务器与后端LNMP服务器的连通性。

    image-20191007225230629

    4.3 漏洞测试

    来看下四个补丁以及它的描述

    https://github.com/apache/trafficserver/pull/3192 # 3192 如果字段名称后面和冒号前面有空格,则返回400 https://github.com/apache/trafficserver/pull/3201 # 3201 当返回400错误时,关闭链接https://github.com/apache/trafficserver/pull/3231 # 3231 验证请求中的Content-Length头https://github.com/apache/trafficserver/pull/3251 # 3251 当缓存命中时,清空请求体

    4.3.1 第一个补丁

    https://github.com/apache/trafficserver/pull/3192 # 3192 如果字段名称后面和冒号前面有空格,则返回400

    看介绍是给ATS增加了RFC72303.2.4章的实现,

    https://tools.ietf.org/html/rfc7230#section-3.2.4

    在其中,规定了HTTP的请求包中,请求头字段与后续的冒号之间不能有空白字符,如果存在空白字符的话,服务器必须返回400,从补丁中来看的话,在ATS 7.1.2中,并没有对该标准进行一个详细的实现。当ATS服务器接收到的请求中存在请求字段与:之间存在空格的字段时,并不会对其进行修改,也不会按照RFC标准所描述的那样返回400错误,而是直接将其转发给后端服务器。

    而当后端服务器也没有对该标准进行严格的实现时,就有可能导致HTTP走私攻击。比如Nginx服务器,在收到请求头字段与冒号之间存在空格的请求时,会忽略该请求头,而不是返回400错误。

    在这时,我们可以构造一个特殊的HTTP请求,进行走私。

    image-20191008113819748

    很明显,请求包中下面的数据部分在传输过程中被后端服务器解析成了请求头。

    来看下Wireshark中的数据包,ATS在与后端Nginx服务器进行数据传输的过程中,重用了TCP连接。

    image-20191008114247036

    只看一下请求,如图所示:

    image-20191008114411337

    阴影部分为第一个请求,剩下的部分为第二个请求。

    在我们发送的请求中,存在特殊构造的请求头Content-Length : 56,56就是后续数据的长度。

    在数据的末尾,不存在\r\n这个结尾。

    当我们的请求到达ATS服务器时,因为ATS服务器可以解析Content-Length : 56这个中间存在空格的请求头,它认为这个请求头是有效的。这样一来,后续的数据也被当做这个请求的一部分。总的来看,对于ATS服务器,这个请求就是完整的一个请求。

    ATS收到这个请求之后,根据Host字段的值,将这个请求包转发给对应的后端服务器。在这里是转发到了Nginx服务器上。

    而Nginx服务器在遇到类似于这种Content-Length : 56的请求头时,会认为其是无效的,然后将其忽略掉。但并不会返回400错误,对于Nginx来说,收到的请求为

    因为最后的末尾没有\r\n,这就相当于收到了一个完整的GET请求和一个不完整的GET请求。

    完整的:

    不完整的:

    在这时,Nginx就会将第一个请求包对应的响应发送给ATS服务器,然后等待后续的第二个请求传输完毕再进行响应。

    当ATS转发的下一个请求到达时,对于Nginx来说,就直接拼接到了刚刚收到的那个不完整的请求包的后面。也就相当于

    然后Nginx将这个请求包的响应发送给ATS服务器,我们收到的响应中就存在了attack: 1foo: GET / HTTP/1.1这两个键值对了。

    那这会造成什么危害呢?可以想一下,如果ATS转发的第二个请求不是我们发送的呢?让我们试一下。

    假设在Nginx服务器下存在一个admin.php,代码内容如下:

    由于HTTP协议本身是无状态的,很多网站都是使用Cookie来判断用户的身份信息。通过这个漏洞,我们可以盗用管理员的身份信息。在这个例子中,管理员的请求中会携带这个一个Cookie的键值对admin=1,当拥有管理员身份时,就能通过GET方式传入要删除的用户名称,然后删除对应的用户。

    在前面我们也知道了,通过构造特殊的请求包,可以使Nginx服务器把收到的某个请求作为上一个请求的一部分。这样一来,我们就能盗用管理员的Cookie了。

    构造数据包

    然后是管理员的正常请求

    让我们看一下效果如何。

    image-20191008123056679

    在Wireshark的数据包中看的很直观,阴影部分为管理员发送的正常请求。

    image-20191008123343584

    在Nginx服务器上拼接到了上一个请求中, 成功删除了用户mengchen。

    4.3.2 第二个补丁

    https://github.com/apache/trafficserver/pull/3201 # 3201 当返回400错误时,关闭连接

    这个补丁说明了,在ATS 7.1.2中,如果请求导致了400错误,建立的TCP链接也不会关闭。在regilero的对CVE-2018-8004的分析文章中,说明了如何利用这个漏洞进行攻击。

    一共能够获得2个响应,都是400错误。

    image-20191009161111039

    ATS在解析HTTP请求时,如果遇到NULL,会导致一个截断操作,我们发送的这一个请求,对于ATS服务器来说,算是两个请求。

    第一个

    第二个

    第一个请求在解析的时候遇到了NULL,ATS服务器响应了第一个400错误,后面的bb\r\n成了后面请求的开头,不符合HTTP请求的规范,这就响应了第二个400错误。

    再进行修改下进行测试

    image-20191009161651556

    一个400响应,一个200响应,在Wireshark中也能看到,ATS把第二个请求转发给了后端Apache服务器。

    image-20191009161916024

    那么由此就已经算是一个HTTP请求拆分攻击了,

    但是这个请求包,怎么看都是两个请求,中间的GET /1.html HTTP/1.1\r\n不符合HTTP数据包中请求头Name:Value的格式。在这里我们可以使用absoluteURI,在RFC2616中第5.1.2节中规定了它的详细格式。

    https://tools.ietf.org/html/rfc2616#section-5.1.2

    我们可以使用类似GET http://www.w3.org/pub/WWW/TheProject.html HTTP/1.1的请求头进行请求。

    构造数据包

    本质上来说,这是两个HTTP请求,第一个为

    其中GET http://ats.mengsec.com/1.html HTTP/1.1为名为GET http,值为//ats.mengsec.com/1.html HTTP/1.1的请求头。

    第二个为

    当该请求发送给ATS服务器之后,我们可以获取到三个HTTP响应,第一个为400,第二个为200,第三个为404。多出来的那个响应就是ATS中间对服务器1.html的请求的响应。

    image-20191009170232529

    根据HTTP Pipepline的先入先出规则,假设攻击者向ATS服务器发送了第一个恶意请求,然后受害者向ATS服务器发送了一个正常的请求,受害者获取到的响应,就会是攻击者发送的恶意请求中的GET http://evil.mengsec.com/evil.html HTTP/1.1中的内容。这种攻击方式理论上是可以成功的,但是利用条件还是太苛刻了。

    对于该漏洞的修复方式,ATS服务器选择了,当遇到400错误时,关闭TCP链接,这样无论后续有什么请求,都不会对其他用户造成影响了。

    4.3.3 第三个补丁

    https://github.com/apache/trafficserver/pull/3231 # 3231 验证请求中的Content-Length头

    在该补丁中,bryancall 的描述是

    从这里我们可以知道,ATS 7.1.2版本中,并没有对RFC2616的标准进行完全实现,我们或许可以进行CL-TE走私攻击。

    构造请求

    多次发送后就能获得405 Not Allowed响应。

    image-20191009173844024

    我们可以认为,后续的多个请求在Nginx服务器上被组合成了类似如下所示的请求。

    对于Nginx来说,GGET这种请求方法是不存在的,当然会返回405报错了。

    接下来尝试攻击下admin.php,构造请求

    多次请求后获得了响应You are not Admin,说明服务器对admin.php进行了请求。

    image-20191009175211574

    如果此时管理员已经登录了,然后想要访问一下网站的主页。他的请求为

    效果如下

    image-20191009175454128

    我们可以看一下Wireshark的流量,其实还是很好理解的。

    image-20191009180032415

    阴影所示部分就是管理员发送的请求,在Nginx服务器中组合进入了上一个请求中,就相当于

    携带着管理员的Cookie进行了删除用户的操作。这个与前面4.3.1中的利用方式在某种意义上其实是相同的。

    4.3.3 第四个补丁

    https://github.com/apache/trafficserver/pull/3251 # 3251 当缓存命中时,清空请求体

    当时看这个补丁时,感觉是一脸懵逼,只知道应该和缓存有关,但一直想不到哪里会出问题。看代码也没找到,在9月17号的时候regilero的分析文章出来才知道问题在哪。

    当缓存命中之后,ATS服务器会忽略请求中的Content-Length请求头,此时请求体中的数据会被ATS当做另外的HTTP请求来处理,这就导致了一个非常容易利用的请求走私漏洞。

    在进行测试之前,把测试环境中ATS服务器的缓存功能打开,对默认配置进行一下修改,方便我们进行测试。

    然后重启服务器即可生效。

    为了方便测试,我在Nginx网站目录下写了一个生成随机字符串的脚本random_str.php

    构造请求包

    第一次请求

    image-20191009222245467

    第二次请求

    image-20191009222313671

    可以看到,当缓存命中时,请求体中的数据变成了下一个请求,并且成功的获得了响应。

    而且在整个请求中,所有的请求头都是符合RFC规范的,这就意味着,在ATS前方的代理服务器,哪怕严格实现了RFC标准,也无法避免该攻击行为对其他用户造成影响。

    ATS的修复措施也是简单粗暴,当缓存命中时,把整个请求体清空就好了。

    5. 其他攻击实例

    在前面,我们已经看到了不同种代理服务器组合所产生的HTTP请求走私漏洞,也成功模拟了使用HTTP请求走私这一攻击手段来进行会话劫持,但它能做的不仅仅是这些,在PortSwigger中提供了利用HTTP请求走私攻击的实验,可以说是很典型了。

    5.1 绕过前端服务器的安全控制

    在这个网络环境中,前端服务器负责实现安全控制,只有被允许的请求才能转发给后端服务器,而后端服务器无条件的相信前端服务器转发过来的全部请求,对每个请求都进行响应。因此我们可以利用HTTP请求走私,将无法访问的请求走私给后端服务器并获得响应。在这里有两个实验,分别是使用CL-TETE-CL绕过前端的访问控制。

    5.1.1 使用CL-TE绕过前端服务器安全控制

    Lab地址:https://portswigger.net/web-security/request-smuggling/exploiting/lab-bypass-front-end-controls-cl-te

    实验的最终目的是获取admin权限并删除用户carlos

    我们直接访问/admin,会返回提示Path /admin is blocked,看样子是被前端服务器阻止了,根据题目的提示CL-TE,我们可以尝试构造数据包

    进行多次请求之后,我们可以获得走私过去的请求的响应。

    image-20191010000428090

    提示只有是以管理员身份访问或者在本地登录才可以访问/admin接口。

    在下方走私的请求中,添加一个Host: localhost请求头,然后重新进行请求,一次不成功多试几次。

    如图所示,我们成功访问了admin界面。也知道了如何删除一个用户,也就是对/admin/delete?username=carlos进行请求。

    image-20191010000749732

    修改下走私的请求包再发送几次即可成功删除用户carlos

    image-20191010000957520

    需要注意的一点是在这里,不需要我们对其他用户造成影响,因此走私过去的请求也必须是一个完整的请求,最后的两个\r\n不能丢弃。

    5.1.1 使用TE-CL绕过前端服务器安全控制

    Lab地址:https://portswigger.net/web-security/request-smuggling/exploiting/lab-bypass-front-end-controls-te-cl

    这个实验与上一个就十分类似了,具体攻击过程就不在赘述了。

    image-20190903111613344

    5.2 获取前端服务器重写请求字段

    在有的网络环境下,前端代理服务器在收到请求后,不会直接转发给后端服务器,而是先添加一些必要的字段,然后再转发给后端服务器。这些字段是后端服务器对请求进行处理所必须的,比如:

    • 描述TLS连接所使用的协议和密码
    • 包含用户IP地址的XFF头
    • 用户的会话令牌ID

    总之,如果不能获取到代理服务器添加或者重写的字段,我们走私过去的请求就不能被后端服务器进行正确的处理。那么我们该如何获取这些值呢。PortSwigger提供了一个很简单的方法,主要是三大步骤:

    • 找一个能够将请求参数的值输出到响应中的POST请求
    • 把该POST请求中,找到的这个特殊的参数放在消息的最后面
    • 然后走私这一个请求,然后直接发送一个普通的请求,前端服务器对这个请求重写的一些字段就会显示出来。

    怎么理解呢,还是做一下实验来一起来学习下吧。

    Lab地址:https://portswigger.net/web-security/request-smuggling/exploiting/lab-reveal-front-end-request-rewriting

    实验的最终目的还是删除用户 carlos

    我们首先进行第一步骤,找一个能够将请求参数的值输出到响应中的POST请求。

    在网页上方的搜索功能就符合要求

    image-20191010003510203

    构造数据包

    多次请求之后就可以获得前端服务器添加的请求头

    image-20190903114123823

    这是如何获取的呢,可以从我们构造的数据包来入手,可以看到,我们走私过去的请求为

    其中Content-Length的值为70,显然下面携带的数据的长度是不够70的,因此后端服务器在接收到这个走私的请求之后,会认为这个请求还没传输完毕,继续等待传输。

    接着我们又继续发送相同的数据包,后端服务器接收到的是前端代理服务器已经处理好的请求,当接收的数据的总长度到达70时,后端服务器认为这个请求已经传输完毕了,然后进行响应。这样一来,后来的请求的一部分被作为了走私的请求的参数的一部分,然后从响应中表示了出来,我们就能获取到了前端服务器重写的字段。

    在走私的请求上添加这个字段,然后走私一个删除用户的请求就好了。

    image-20190903114641180

    5.3 获取其他用户的请求

    在上一个实验中,我们通过走私一个不完整的请求来获取前端服务器添加的字段,而字段来自于我们后续发送的请求。换句话说,我们通过请求走私获取到了我们走私请求之后的请求。如果在我们的恶意请求之后,其他用户也进行了请求呢?我们寻找的这个POST请求会将获得的数据存储并展示出来呢?这样一来,我们可以走私一个恶意请求,将其他用户的请求的信息拼接到走私请求之后,并存储到网站中,我们再查看这些数据,就能获取用户的请求了。这可以用来偷取用户的敏感信息,比如账号密码等信息。

    Lab地址:https://portswigger.net/web-security/request-smuggling/exploiting/lab-capture-other-users-requests

    实验的最终目的是获取其他用户的Cookie用来访问其他账号。

    我们首先去寻找一个能够将传入的信息存储到网站中的POST请求表单,很容易就能发现网站中有一个用户评论的地方。

    抓取POST请求并构造数据包

    这样其实就足够了,但是有可能是实验环境的问题,我无论怎么等都不会获取到其他用户的请求,反而抓了一堆我自己的请求信息。不过原理就是这样,还是比较容易理解的,最重要的一点是,走私的请求是不完整的。

    image-20191010011955268

    5.4 利用反射型XSS

    我们可以使用HTTP走私请求搭配反射型XSS进行攻击,这样不需要与受害者进行交互,还能利用漏洞点在请求头中的XSS漏洞。

    Lab地址:https://portswigger.net/web-security/request-smuggling/exploiting/lab-deliver-reflected-xss

    在实验介绍中已经告诉了前端服务器不支持分块编码,目标是执行alert(1)

    首先根据UA出现的位置构造Payload

    image-20190903144329596

    然后构造数据包

    此时在浏览器中访问,就会触发弹框

    image-20190903162524009

    再重新发一下,等一会刷新,可以看到这个实验已经解决了。

    5.5 进行缓存投毒

    一般来说,前端服务器出于性能原因,会对后端服务器的一些资源进行缓存,如果存在HTTP请求走私漏洞,则有可能使用重定向来进行缓存投毒,从而影响后续访问的所有用户。

    Lab地址:https://portswigger.net/web-security/request-smuggling/exploiting/lab-perform-web-cache-poisoning

    实验环境中提供了漏洞利用的辅助服务器。

    需要添加两个请求包,一个POST,携带要走私的请求包,另一个是正常的对JS文件发起的GET请求。

    以下面这个JS文件为例

    编辑响应服务器

    image-20190903170042395

    构造POST走私数据包

    然后构造GET数据包

    POST请求和GET请求交替进行,多进行几次,然后访问js文件,响应为缓存的漏洞利用服务器上的文件。

    image-20190903172338456

    访问主页,成功弹窗,可以知道,js文件成功的被前端服务器进行了缓存。

    image-20190903172544103

    6. 如何防御

    从前面的大量案例中,我们已经知道了HTTP请求走私的危害性,那么该如何防御呢?不针对特定的服务器,通用的防御措施大概有三种。

    • 禁用代理服务器与后端服务器之间的TCP连接重用。
    • 使用HTTP/2协议。
    • 前后端使用相同的服务器。

    以上的措施有的不能从根本上解决问题,而且有着很多不足,就比如禁用代理服务器和后端服务器之间的TCP连接重用,会增大后端服务器的压力。使用HTTP/2在现在的网络条件下根本无法推广使用,哪怕支持HTTP/2协议的服务器也会兼容HTTP/1.1。从本质上来说,HTTP请求走私出现的原因并不是协议设计的问题,而是不同服务器实现的问题,个人认为最好的解决方案就是严格的实现RFC7230-7235中所规定的的标准,但这也是最难做到的。

    参考链接


    Paper

    本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1048/

    作者:吴烦恼 | Categories:安全研究技术分享 | Tags:
  • BlueKeep 漏洞利用分析

    2019-09-20

    作者:SungLin@知道创宇404实验室
    时间:2019年9月18日

    0x00 信道的创建、连接与释放

    通道的数据包定义在MCS Connect Inittial PDU with GCC Conference Create Request中,在rdp连接过程如下图所示:

    信道创建数据包格式如下:

    在MCS Connect Inittial中属于Client Network Data数据段,MS_T120将会在连接一开始的时候通过函数termdd!_IcaRegisterVcBin创建一个虚拟通道id是0x1f大小为0x18的结构体,之后就调用termdd!IcaCreateChannel开始创建大小为0x8c的信道结构体之后将会与虚拟通道id是0x1f绑定,也就是这个结构体将会被我们利用

    信道的定义字段主要是名字加上配置,配置主要包括了优先级等

    在server对MCS Connect Inittial应答包,将会依次给出对应虚拟通道的id值:

    在rdp内核中依次注册的值对应应该是0、1、2、3, MS_T120信道将会通过我们发送的用户虚拟id为3的值再一次绑定,首先通过termdd!_IcaFindVcBind找到了刚开始注册的虚拟通道id是0x1f,如下所示:

    但是在termdd!_IcaBindChannel时,却将我们自定义的id值为3与信道结构体再一次绑定在一起了,此信道结构体就是MS_T120

    同时我们自己的用户id将内部绑定的0x1f给覆盖了

    我们往信道MS_T120发送数据主动释放其分配的结构体,其传入虚拟通道id值为3通过函数termdd!IcaFindChannel在channeltable中查找返回对应的信道结构体:

    下图为返回的MS_T120信道结构体,其中0xf77b4300为此信道可调用的函数指针数组:

    在这个函数指针数组中主要存放了三个函数,其中对应了termdd!IcaCloseChanneltermdd!IcaReadChanneltermdd!IcaWriteChannel

    我们传入释放MS_T120信道的数据如下,字节大小为0x12,主要数据对应了0x02

    之后将会进入nt! IofCompleteRequest函数,通过apc注入后,将会通过nt! IopCompleteRequestnt!IopAbortRequest进行数据请求的响应,最终在termdd!IcaDispatch完成我们发送数据的的请求,_BYTE v2就是我们发送的数据,所以我们发送的数据0x02将会最终调用到IcaClose函数进入IcaCloseChannel函数,最后主动释放掉了MS_T120信道结构体

    0x01 通过RDPDR信道进行数据占位

    我们先来了解下rdpdr信道,首先rdpdr信道是文件系统虚拟通道扩展,该扩展在名为rdpdr的静态虚拟通道上运行。目的是将访问从服务器重定向到客户端文件系统,其数据头部将会主要是两种标识和PacketId字段组成:

    在这里我们刚好利用到了rdpde客户端name响应的数据来进行池内存的占位

    在完全建立连接后,将会创建rdpdr信道的结构体

    在window7中,在建立完成后接收到server的rdpdr请求后,通过发送客户端name响应数据,将会调用到termdd! IcaChannelInputInternal中的ExAllocatePoolWithTag分配非分页池内存,并且其长度是我们可以控制的,基本满足了UAF利用的需求:

    可是在windowsxp中,直接发送client name request将会导致内存分配失败,直接进入termdd! _IcaCopyDataToUserBuffer,并且在Tao Yan and Jin Chen[1]一文中也提到了通过发送client name request在触发一定的条件后将会绕过termdd!_IcaCopyDataToUserBuffer而进入ExAllocatePoolWithTag分配我们想要的非分页内存,而打破条件如下:

    我们先来看看最开始信道结构体的创建,我们可以发现从一开始创建信道结构体的时候,将会出现两个标志,而这两个标志是按照地址顺序排列的,而在上面需要打破的条件中,只要channelstruct +0x108的地址存放的是同一个地址,循环就会被break

    我们发送一个正常的rdpdr的name request数据包,头部标识是0x7244和0x4e43

    经过termdd!_IcaCopyDataToUserBuffer之后,将会进入nt!IofCompleteRequest,在响应请求后进入rdpdr!DrSession::ReadCompletion,此函数处理逻辑如下,其将会遍历一个链表,从链表中取出对应的vftable函数数组

    遍历第一次取出第一张函数数组

    传入我们发送的数据后,通过函数数组调用rdpdr!DrSession::RecognizePacket进行读取

    判断头部标志是否为(RDPDR_CTYP_CORE)0x7244

    接着将会读取函数vftable第二个地址,进行转发

    如下图可以看到rdpdr的数据包处理逻辑

    rdpdr经过一系列数据包处理后最终进入了我们关心的地方,将会传入channelstruct通过调用termdd! _IcaQueueReadChannelRequest进行标志位的处理

    最初rdpdr的channelstruct的标志位如下

    经过函数termdd! _IcaQueueReadChannelRequest对此标志的处理后变成如下,所以下一个数据依然会进入termdd!_IcaCopyDataToUserBuffer,导致我们进行池喷射的失败

    回到rdpdr头部处理函数rdpdr!DrSession::RecognizePacket,我们发现在链表遍历失败后将会进行跳转,最后将会进入读取失败处理函数rdpdr!DrSession::ChannelIoFailed,然后直接return了

    我们构造一个头部异常的数据包发送,头部标志我们构造的是0x7240,将会导致rdpdr!DrSession::RecognizePacket判断失败,之后将会继续遍历链表依次再取出两张函数数组

    最后两个函数数组依次调用rdpdr!DrExchangeManager::RecognizePacketrdpdr!DrDeviceManager::RecognizePacket,都会判断错误的头部标志0x7240,最后导致链表遍历完后进行错误跳转,直接绕过了termdd! _IcaQueueReadChannelRequest对标志位的修改,将会打破循环

    最后我们连续构造多个错误的数据包后将会进入ExAllocatePoolWithTag,分配到我们需要的非分页内存!

    0x02 win7 EXP 池喷射简要分析

    首先被释放的MS_T120池大小包括是0x170,池的标志是TSic

    分析Win7 exp 可以知道数据占位是用的rdpsnd信道,作者没有采用rdpdr信道,应该也和喷射的稳定性有关,rdpsnd喷射是再建立完了rdpdr初始化后开始的,在free掉MS_T120结构体前,发送了1044个数据包去申请0x170大小的池内存,这样做可以说应该是为了防止之后被free掉的内存被其他程序占用了,提高free后内存被我们占用的生存几率

    占位被free的实际数据大小为0x128,利用的中转地址是0xfffffa80ec000948

    之后开始池喷射,将payload喷射到可以call [rax] == 0xfffffa80ec000948的地方,喷射的payload大小基本是0x400,总共喷射了200mb的数据大小,我们先来看下喷射前带标志TSic总共占用池内存大小是58kib左右

    喷射完后带TSic标志池内存大小大约就是201mb,池内存喷射基本是成功的,我的win7是sp1,总共内存大小是1GB,再喷射过程中也没有其他干扰的,所以喷射很顺利

    图中可以发现基本已经很稳定的0x400大小的池喷射payload,地址越高0x400大小的内存基本就很稳定了

    最后断开连接时候,被free的内存已经被我们喷射的0x128大小的数据给占用了

    执行call指令后稳定跳转到了我们的payload,成功执行!

    参考链接:
    [0] https://github.com/rapid7/metasploit-framework/pull/12283
    [1] https://unit42.paloaltonetworks.com/exploitation-of-windows-cve-2019-0708-bluekeep-three-ways-to-write-data-into-the-kernel-with-rdp-pdu/
    [2] https://wooyun.js.org/drops/%E7%BE%8A%E5%B9%B4%E5%86%85%E6%A0%B8%E5%A0%86%E9%A3%8E%E6%B0%B4%EF%BC%9A%20%E2%80%9CBig%20Kids%E2%80%99%20Pool%E2%80%9D%E4%B8%AD%E7%9A%84%E5%A0%86%E5%96%B7%E6%8A%80%E6%9C%AF.html

    作者:吴烦恼 | Categories:安全研究技术分享 | Tags:
  • Java 反序列化工具 gadgetinspector 初窥

    2019-09-17

    作者:Longofo@知道创宇404实验室 
    时间:2019年9月4日

    起因

    一开始是听@Badcode师傅说的这个工具,在Black Hat 2018的一个议题提出来的。这是一个基于字节码静态分析的、利用已知技巧自动查找从source到sink的反序列化利用链工具。看了几遍作者在Black Hat上的演讲视频PPT,想从作者的演讲与PPT中获取更多关于这个工具的原理性的东西,可是有些地方真的很费解。不过作者开源了这个工具,但没有给出详细的说明文档,对这个工具的分析文章也很少,看到一篇平安集团对这个工具的分析,从文中描述来看,他们对这个工具应该有一定的认识并做了一些改进,但是在文章中对某些细节没有做过多的阐释。后面尝试了调试这个工具,大致理清了这个工具的工作原理,下面是对这个工具的分析过程,以及对未来工作与改进的设想。

    关于这个工具

    • 这个工具不是用来寻找漏洞,而是利用已知的source->...->sink链或其相似特征发现分支利用链或新的利用链。
    • 这个工具是在整个应用的classpath中寻找利用链。
    • 这个工具进行了一些合理的预估风险判断(污点判断、污点传递等)。
    • 这个工具会产生误报不是漏报(其实这里还是会漏报,这是作者使用的策略决定的,在后面的分析中可以看到)。
    • 这个工具是基于字节码分析的,对于Java应用来说,很多时候我们并没有源码,而只有War包、Jar包或class文件。
    • 这个工具不会生成能直接利用的Payload,具体的利用构造还需要人工参与。

    序列化与反序列化

    序列化(Serialization)是将对象的状态信息转化为可以存储或者传输形式的过程,转化后的信息可以存储在磁盘上,在网络传输过程中,可以是字节、XML、JSON等格式;而将字节、XML、JSON等格式的信息还原成对象这个相反的过程称为反序列化。

    在JAVA中,对象的序列化和反序列化被广泛的应用到RMI(远程方法调用)及网络传输中。

    Java中的序列化与反序列化库

    • JDK(ObjectInputStream)
    • XStream(XML,JSON)
    • Jackson(XML,JSON)
    • Genson(JSON)
    • JSON-IO(JSON)
    • FlexSON(JSON)
    • Fastjson(JSON)
    • ...

    不同的反序列化库在反序列化不同的类时有不同的行为、被反序列化类的不同"魔术方法"会被自动调用,这些被自动调用的方法就能够作为反序列化的入口点(source)。如果这些被自动调用的方法又调用了其他子方法,那么在调用链中某一个子方法也可以作为source,就相当于已知了调用链的前部分,从某个子方法开始寻找不同的分支。通过方法的层层调用,可能到达某些危险的方法(sink)。

    • ObjectInputStream

    例如某个类实现了Serializable接口,ObjectInputStream.readobject在反序列化类得到其对象时会自动查找这个类的readObject、readResolve等方法并调用。

    例如某个类实现了Externalizable接口,ObjectInputStream.readobject在反序列化类得到其对象时会自动查找这个类的readExternal等方法并调用。

    • Jackson

    ObjectMapper.readValue在反序列化类得到其对象时,会自动查找反序列化类的无参构造方法、包含一个基础类型参数的构造方法、属性的setter、属性的getter等方法并调用。

    • ...

    在后面的分析中,都使用JDK自带的ObjectInputStream作为样例。

    控制数据类型=>控制代码

    作者说,在反序列化漏洞中,如果控制了数据类型,我们就控制了代码。这是什么意思呢?按我的理解,写了下面的一个例子:

    为了方便我把所有类写在一个类中进行测试。在Person类中,有一个Animal类的属性pet,它是Cat和Dog的接口。在序列化时,我们能够控制Person的pet具体是Cat对象或者Dog对象,因此在反序列化时,在readObject中pet.eat()具体的走向就不一样了。如果是pet是Cat类对象,就不会走到执行有害代码Runtime.getRuntime().exec("calc");这一步,但是如果pet是Dog类的对象,就会走到有害代码。

    即使有时候类属性在声明时已经为它赋值了某个具体的对象,但是在Java中通过反射等方式依然能修改。如下:

    在Person类中,不能通过构造器或setter方法或其他方式对pet赋值,属性在声明时已经被定义为Cat类的对象,但是通过反射能将pet修改为Dog类的对象,因此在反序列化时依然会走到有害代码处。

    这只是我自己对作者"控制了数据类型,就控制了代码"的理解,在Java反序列化漏洞中,很多时候是利用到了Java的多态特性来控制代码走向最后达到恶意执行目的。

    魔术方法

    在上面的例子中,能看到在反序列化时没有调用Person的readobject方法,它是ObjectInputStream在反序列化对象时自动调用的。作者将在反序列化中会自动调用的方法称为"魔术方法"。

    使用ObjectInputStream反序列化时几个常见的魔术方法:

    • Object.readObject()
    • Object.readResolve()
    • Object.finalize()
    • ...

    一些可序列化的JDK类实现了上面这些方法并且还自动调用了其他方法(可以作为已知的入口点):

    • HashMap
    • Object.hashCode()
    • Object.equals()
    • PriorityQueue
    • Comparator.compare()
    • Comparable.CompareTo()
    • ...

    一些sink:

    • Runtime.exec(),这种最为简单直接,即直接在目标环境中执行命令
    • Method.invoke(),这种需要适当地选择方法和参数,通过反射执行Java方法
    • RMI/JNDI/JRMP等,通过引用远程对象,间接实现任意代码执行的效果
    • ...

    作者给出了一个从Magic Methods(source)->Gadget Chains->Runtime.exec(sink)的例子:

    上面的HashMap实现了readObject这个"魔术方法",并且调用了hashCode方法。某些类为了比较对象之间是否相等会实现equals方法(一般是equals和hashCode方法同时实现)。从图中可以看到AbstractTableModel$ff19274a正好实现了hashCode方法,其中又调用了f.invoke方法,f是IFn对象,并且f能通过属性__clojureFnMap获取到。IFn是一个接口,上面说到,如果控制了数据类型,就控制了代码走向。所以如果我们在序列化时,在__clojureFnMap放置IFn接口的实现类FnCompose的一个对象,那么就能控制f.invokeFnCompose.invoke方法,接着控制FnCompose.invoke中的f1、f2为FnConstant就能到达FnEval.invoke了(关于AbstractTableModel$ff19274a.hashcode中的f.invoke具体选择IFn的哪个实现类,根据后面对这个工具的测试以及对决策原理的分析,广度优先会选择短的路径,也就是选择了FnEval.invoke,所以这也是为什么要人为参与,在后面的样例分析中也可以看到)。

    有了这条链,只需要找到触发这个链的漏洞点就行了。Payload使用JSON格式表示如下:

    gadgetinspector工作流程

    如作者所说,正好使用了五个步骤:

    Step1 枚举全部类以及每个类的所有方法

    要进行调用链的搜索,首先得有所有类及所有类方法的相关信息:

    来看下classes.dat、methods.dat分别长什么样子:

    • classes.dat

    找了两个比较有特征的

    第一个类com/sun/deploy/jardiff/JarDiffPatcher:

    和上面的表格信息对应一下,是吻合的

    • 类名:com/sun/deploy/jardiff/JarDiffPatcher
    • 父类: java/lang/Object,如果一类没有显式继承其他类,默认隐式继承java/lang/Object,并且java中不允许多继承,所以每个类只有一个父类
    • 所有接口:com/sun/deploy/jardiff/JarDiffConstants、com/sun/deploy/jardiff/Patcher
    • 是否是接口:false
    • 成员:newBytes!2![B,newBytes成员,Byte类型。为什么没有将static/final类型的成员加进去呢?这里还没有研究如何操作字节码,所以作者这里的判断实现部分暂且跳过。不过猜测应该是这种类型的变量并不能成为污点所以忽略了

    第二个类com/sun/corba/se/impl/presentation/rmi/InvocationHandlerFactoryImpl$CustomCompositeInvocationHandlerImpl:

    和上面的表格信息对应一下,也是吻合的

    • 类名:com/sun/corba/se/impl/presentation/rmi/InvocationHandlerFactoryImpl$CustomCompositeInvocationHandlerImpl,是一个内部类
    • 父类: com/sun/corba/se/spi/orbutil/proxy/CompositeInvocationHandlerImpl
    • 所有接口:com/sun/corba/se/spi/orbutil/proxy/LinkedInvocationHandler,java/io/Serializable
    • 是否是接口:false
    • 成员:stub!130!com/sun/corba/se/spi/presentation/rmi/DynamicStub!this$0!4112!com/sun/corba/se/impl/presentation/rmi/InvocationHandlerFactoryImpl,!*!这里可以暂时理解为分割符,有一个成员stub,类型com/sun/corba/se/spi/presentation/rmi/DynamicStub。因为是内部类,所以多了个this成员,这个this指向的是外部类
    • methods.dat

    同样找几个比较有特征的

    sun/nio/cs/ext/Big5#newEncoder:

    • 类名:sun/nio/cs/ext/Big5
    • 方法名: newEncoder
    • 方法描述信息: ()Ljava/nio/charset/CharsetEncoder; 无参,返回java/nio/charset/CharsetEncoder对象
    • 是否是静态方法:false

    sun/nio/cs/ext/Big5_HKSCS$Decoder#\<init>:

    • 类名:sun/nio/cs/ext/Big5_HKSCS$Decoder
    • 方法名:\<init>
    • 方法描述信息: (Ljava/nio/charset/Charset;Lsun/nio/cs/ext/Big5_HKSCS1;)V参数1是java/nio/charset/Charset类型,参数2是sun/nio/cs/ext/Big5HKSCS1;)V参数1是java/nio/charset/Charset类型,参数2是sun/nio/cs/ext/Big5HKSCS1类型,返回值void
    • 是否是静态方法:false

    继承关系的生成:

    继承关系在后面用来判断一个类是否能被某个库序列化、以及搜索子类方法实现等会用到。

    这一步的结果保存到了inheritanceMap.dat:

    Step2 生成passthrough数据流

    这里的passthrough数据流指的是每个方法的返回结果与方法参数的关系,这一步生成的数据会在生成passthrough调用图时用到。

    以作者给出的demo为例,先从宏观层面判断下:

    FnConstant.invoke返回值与参数this(参数0,因为序列化时类的所有成员我们都能控制,所以所有成员变量都视为0参)、arg(参数1)的关系:

    • 与this的关系:返回了this.value,即与0参有关系
    • 与arg的关系:返回值与arg没有任何关系,即与1参没有关系
    • 结论就是FnConstant.invoke与参数0有关,表示为FnConstant.invoke()->0

    Fndefault.invoke返回值与参数this(参数0)、arg(参数1)的关系:

    • 与this的关系:返回条件的第二个分支与this.f有关系,即与0参有关系
    • 与arg的关系:返回条件的第一个分支与arg有关系,即与1参有关系
    • 结论就是FnConstant.invoke与0参,1参都有关系,表示为Fndefault.invoke()->0、Fndefault.invoke()->1

    在这一步中,gadgetinspector是利用ASM来进行方法字节码的分析,主要逻辑是在类PassthroughDiscovery和TaintTrackingMethodVisitor中。特别是TaintTrackingMethodVisitor,它通过标记追踪JVM虚拟机在执行方法时的stack和localvar,并最终得到返回结果是否可以被参数标记污染。

    核心实现代码(TaintTrackingMethodVisitor涉及到字节码分析,暂时先不看):

    拓扑排序

    有向无环图(DAG)才有拓扑排序,非 DAG 图没有拓扑排序。 当有向无环图满足以下条件时:

    • 每一个顶点出现且只出现一次
    • 若A在序列中排在B的前面,则在图中不存在从B到A的路径

    这样的图,是一个拓扑排序的图。树结构其实可以转化为拓扑排序,而拓扑排序 不一定能够转化为树。

    以上面的拓扑排序图为例,用一个字典表示图结构

    代码实现

    但是在方法的调用中,我们希望最后的结果是c、b、e、d、a,这一步需要逆拓扑排序,正向排序使用的BFS,那么得到相反结果可以使用DFS。为什么在方法调用中需要使用逆拓扑排序呢,这与生成passthrough数据流有关。看下面一个例子:

    那么这里arg与返回值到底有没有关系呢?假设Obj.childMethod为

    由于childMethod的返回值carg与有关,那么可以判定parentMethod的返回值与参数arg是有关系的。所以如果存在子方法调用并传递了父方法参数给子方法时,需要先判断子方法返回值与子方法参数的关系。因此需要让子方法的判断在前面,这就是为什么要进行逆拓扑排序。

    从下图可以看出outgoingReferences的数据结构为:

    而这个结构正好适合逆拓扑排序

    但是上面说拓扑排序时不能形成环,但是在方法调用中肯定是会存在环的。作者是如何避免的呢?

    在上面的dfsTsort实现代码中可以看到使用了stack和visitedNodes,stack保证了在进行逆拓扑排序时不会形成环,visitedNodes避免了重复排序。使用如下一个调用图来演示过程:

    从图中可以看到有环med1->med2->med6->med1,并且有重复的调用med3,严格来说并不能进行逆拓扑排序,但是通过stack、visited记录访问过的方法,就能实现逆拓扑排序。为了方便解释把上面的图用一个树来表示:

    对上图进行逆拓扑排序(DFS方式):

    从med1开始,先将med1加入stack中,此时stack、visited、sortedmethods状态如下:

    med1还有子方法?有,继续深度遍历。将med2放入stack,此时的状态:

    med2有子方法吗?有,继续深度遍历。将med3放入stack,此时的状态:

    med3有子方法吗?有,继续深度遍历。将med7放入stack,此时的状态:

    med7有子方法吗?没有,从stack中弹出med7并加入visited和sortedmethods,此时的状态:

    回溯到上一层,med3还有其他子方法吗?有,med8,将med8放入stack,此时的状态:

    med8还有子方法吗?没有,弹出stack,加入visited与sortedmethods,此时的状态:

    回溯到上一层,med3还有其他子方法吗?没有了,弹出stack,加入visited与sortedmethods,此时的状态:

    回溯到上一层,med2还有其他子方法吗?有,med6,将med6加入stack,此时的状态:

    med6还有子方法吗?有,med1,med1在stack中?不加入,抛弃。此时状态和上一步一样

    回溯到上一层,med6还有其他子方法吗?没有了,弹出stack,加入visited和sortedmethods,此时的状态:

    回溯到上一层,med2还有其他子方法吗?没有了,弹出stack,加入visited和sortedmethods,此时的状态:

    回溯到上一层,med1还有其他子方法吗?有,med3,med3在visited中?在,抛弃。

    回溯到上一层,med1还有其他子方法吗?有,med4,将med4加入stack,此时的状态:

    med4还有其他子方法吗?没有,弹出stack,加入visited和sortedmethods中,此时的状态:

    回溯到上一层,med1还有其他子方法吗?没有了,弹出stack,加入visited和sortedmethods中,此时的状态(即最终状态):

    所以最后的逆拓扑排序结果为:med7、med8、med3、med6、med2、med4、med1。

    生成passthrough数据流

    在calculatePassthroughDataflow中遍历了sortedmethods,并通过字节码分析,生成了方法返回值与参数关系的passthrough数据流。注意到下面的序列化决定器,作者内置了三种:JDK、Jackson、Xstream,会根据具体的序列化决定器判定决策过程中的类是否符合对应库的反序列化要求,不符合的就跳过:

    • 对于JDK(ObjectInputStream),类否继承了Serializable接口
    • 对于Jackson,类是否存在0参构造器
    • 对于Xstream,类名能否作为有效的XML标签

    生成passthrough数据流代码:

    最后生成了passthrough.dat:

    Step3 枚举passthrough调用图

    这一步和上一步类似,gadgetinspector 会再次扫描全部的Java方法,但检查的不再是参数与返回结果的关系,而是方法的参数与其所调用的子方法的关系,即子方法的参数是否可以被父方法的参数所影响。那么为什么要进行上一步的生成passthrough数据流呢?由于这一步的判断也是在字节码分析中,所以这里只能先进行一些猜测,如下面这个例子:

    如果不进行生成passthrough数据流操作,就无法判断TestObject.childMethod1的返回值是否会受到参数1的影响,也就无法继续判断parentMethod的arg参数与子方法MyObject.childmethod的参数传递关系。

    作者给出的例子:

    AbstractTableModel$ff19274a.hashcode与子方法IFn.invoke:

    • AbstractTableModel$ff19274a.hashcode的this(0参)传递给了IFn.invoke的1参,表示为0->IFn.invoke()@1
    • 由于f是通过this.__clojureFnMap(0参)获取的,而f又为IFn.invoke()的this(0参),即AbstractTableModel$ff19274a.hashcode的0参传递给了IFn.invoke的0参,表示为0->IFn.invoke()@0

    FnCompose.invoke与子方法IFn.invoke:

    • FnCompose.invoked的arg(1参)传递给了IFn.invoke的1参,表示为1->IFn.invoke()@1
    • f1为FnCompose的属性(this,0参),被做为了IFn.invoke的this(0参数)传递,表示为0->IFn.invoke()@1
    • f1.invoke(arg)做为一个整体被当作1参传递给了IFn.invoke,由于f1在序列化时我们可以控制具体是IFn的哪个实现类,所以具体调用哪个实现类的invoke也相当于能够控制,即f1.invoke(arg)这个整体可以视为0参数传递给了IFn.invoke的1参(这里只是进行的简单猜测,具体实现在字节码分析中,可能也体现了作者说的合理的风险判断吧),表示为0->IFn.invoke()@1

    在这一步中,gadgetinspector也是利用ASM来进行字节码的分析,主要逻辑是在类CallGraphDiscovery和ModelGeneratorClassVisitor中。在ModelGeneratorClassVisitor中通过标记追踪JVM虚拟机在执行方法时的stack和localvar,最终得到方法的参数与其所调用的子方法的参数传递关系。

    生成passthrough调用图代码(暂时省略ModelGeneratorClassVisitor的实现,涉及到字节码分析):

    最后生成了passthrough.dat:


    Step4 搜索可用的source

    这一步会根据已知的反序列化漏洞的入口,检查所有可以被触发的方法。例如,在利用链中使用代理时,任何可序列化并且是java/lang/reflect/InvocationHandler子类的invoke方法都可以视为source。这里还会根据具体的反序列化库决定类是否能被序列化。

    搜索可用的source:

    这一步的结果会保存在文件sources.dat中:

    Step5 搜索生成调用链

    这一步会遍历全部的source,并在callgraph.dat中递归查找所有可以继续传递污点参数的子方法调用,直至遇到sink中的方法。

    搜索生成调用链:

    作者给出的sink方法:

    对于每个入口节点来说,其全部子方法调用、孙子方法调用等等递归下去,就构成了一棵树。之前的步骤所做的,就相当于生成了这颗树,而这一步所做的,就是从根节点出发,找到一条通往叶子节点的道路,使得这个叶子节点正好是我们所期望的sink方法。gadgetinspector对树的遍历采用的是广度优先(BFS),而且对于已经检查过的节点会直接跳过,这样减少了运行开销,避免了环路,但是丢掉了很多其他链。

    这个过程看起来就像下面这样:

    通过污点的传递,最终找到从source->sink的利用链

    :targ表示污染参数的index,0->1这样的表示父方法的0参传递给了子方法的1参

    样例分析

    现在根据作者的样例写个具体的demo实例来测试下上面这些步骤。

    demo如下: