开端
在拜读了Sam等人的博客文章之后,我们也开始针对Apple进行新一轮攻击测试。此次攻击的目标在于发现一些严重漏洞,例如 PII exposure、Get shell、打入内网权限等等。这些是Apple公司所关注和感兴趣的漏洞类型。
探测和识别
前期我们收集了大量相关资产信息,针对信息进行有效的指纹检测和服务识别,最终发现了3台运行着的基于Lucee开发的CMS网站主机。
由于CMS和Lucee都支持本地托管,因此它们是我们入侵的很好目标。我们选择审计Lucee作为重点突破口,因为它公开了管理员面板并且有很多历史漏洞记录可以参考。
运气好的是,在这三台不同的Apple主机上均可访问Lucee的管理面员板。两台主机运行的Lucee版本已过时,另一台运行的是Lucee新版本。
- https://facilities.apple.com/(最新版本)
- https://booktravel.apple.com/(旧版本)
- https://booktravel-uat.apple.com/(旧版本)
磨刀霍霍,Apple的WAF策略
要利用我们将在后面讨论的漏洞,这里我们首先需要了解一下Apple所使用的WAF策略,最为重要的是,facilities.apple.com
网站的前端是如何与WAF交互的。
Apple的WAF非常强大,它几乎可以100%的阻止通过URL参数传递的恶意代码,例如路径遍历/SQL注入等。
facilities.apple.com
的前端做了反向代理,通过反代配置Response响应仅显示后端传来的200和404状态码。如果在后端获得任何其他类型的状态码,则前端将其改为403 ,与触发WAF时的响应相同。
举一反三,基于Lucee的错误配置审计
在本地搭建Lucee测试和审计时,我们发现了严重的错误配置漏洞,它使攻击者可以直接访问经过身份验证的CFM(ColdFusion)文件。这使我们可以在完全没有认证的情况下去执行已认证的操作。
一旦在CFM文件中触发了 request.admintype
变量/属性,整个执行流程将停止,因为我们并没有通过admin身份验证。但是,该检查之前的任何代码都将执行。所以我们必须在触发request.admintype
之前找到存在某种漏洞错误的文件。
接下来我们将通过分析和利用以下三个CFM文件,获得完整的RCE链攻击:
- imgProcess.cfm(在较早版本中不可用)
- admin.search.index.cfm
- ext.applications.upload.cfm
小试牛刀,陷入失败的尝试
imgProcess.cfm 中的 Sweet&Simple RCE
为了搭建和Apple资产一样的环境,我们在本地部署运行了相同版本的Lucee。首先在不传递任何参数的情况下本地打开 imgProcess.cfm
,后端直接抛给我们一个异常,同样的在Apple网站上打开该文件,我们得到了403返回,表示该文件存在。此时我们只需要指定正确的参数和值即可。否则后端服务器会和我们本地直接访问一样,抛出一个异常,然后Apple的前端将其处理为403。
错误的参数:
正确的参数:
该文件存在路径遍历漏洞,我们可以在服务器的任何位置上创建我们指定内容的文件。
<cfoutput>
<cffile action="write" file="#expandPath('{temp-directory}/admin-ext-thumbnails/')#\__#url.file#" Output="#form.imgSrc#" createPath="true">
</cfoutput>
它接受查询参数file,并通过以下代码将其创建为一个文件:
{temp-directory}/admin-ext-thumbnails/__{our-input}
我们可以通过 post 的参数imgSrc
来自定义输入内容。
正如你已经看到的,在执行路径遍历之前必须存在目录,因为Linux要求在进行遍历之前必须存在路径。对我们来说幸运的是,如果路径不存在,expandPath
则创建路径,并以字符串形式返回路径。因此,构造 file=/../../../context/pwn.cfm
将创建``目录并遍历webroot中的上下文目录,从而在此处为我们提供了一个 ezz RCE。
然而,即使存在这个漏洞,我们也不能在Apple的这个网站中利用它,因为Apple的 WAF 过滤了查询参数中的恶意参数 ../
。
该处明确要求file
参数为查询参数(url.file
,form.imgSrc
),但是如果两者都为表单参数或者post参数,这种情况下就不会触发WAF,我们就可以在指定目录下写入我们指定内容的文件和控制文件的名称。
绝处逢生,巧妙的绕过WAF
狡猾的对抗
admin.search.index.cfm
允许我们指定目录并将其内容复制到预期位置。但是,复制函数非常狡猾,实际上不会复制任何文件内容,也不会保留文件的扩展名。
该 url 采用两个参数:
- dataDir
- luceeArchiveZipPath
dataDir
是通过 luceeArchiveZipPath
参数指定的文件复制的路径。如果该路径不存在,它将被创建。这里我们可以通过绝对路径 :
<cfif not directoryExists(dataDir)>
<cfdirectory action="create" directory="#dataDir#" mode="777" recurse="true" />
</cfif>
请求示例:
GET /lucee/admin/admin.search.index.cfm?dataDir=/copy/to/path/here/&LUCEEARCHIVEZIPPATH=/copy/from/path/here HTTP/1.1 Host: facilities.apple.com User-Agent: Mozilla/5.0 Connection: close
现在我们知道了复制函数并不是标准的,那么让我们更深入地了解一下负责执行此操作的代码,审计和发现任何有用的线索。
我们注意到了这个有趣的CFML标签:
<cfdirectory action="list" directory="#luceeArchiveZipPath#" filter="*.*.cfm" name="qFiles" sort="name" />
它列出了 luceeArchiveZipPath
目录中的文件。filter
属性表示只列出 \*.\*.cfm
格式的文件。该查询的结果存储在 qFiles
变量中。
接下来,它遍历每个文件(存储在变量currFile
中),替换文件名中的.cfm
为空字符串''
,并将这个更新后的文件名存储在currAction
变量中。因此,如果我们有一个文件test.xyz.cfm
,处理后它将变为test.xyz
<cfset currAction = replace(qFiles.name, '.cfm', '') />
然后,它检查dataDir
目录中是否存在诸如test.xyz.en.txt
或test.xyz.de.txt
之类的文件名。同样,dataDir
变量是用户控制的。如果此文件不存在,将用空格替换文件名中的点(’.’),并将其保存到pageContents.lng.currAction
变量中。
<cfif fileExists('#dataDir##currAction.
lng#.txt')>
<cfset pageContents[lng][currAction] = fileRead('#dataDir##currAction.
lng#.txt', 'utf-8') />
<cfelse> <!--- make sure we will also find this page when searching for the file name--->
<cfset pageContents[lng][currAction] = "#replace(currAction, '.', ' ')# " /> </cfif>
之后将创建文件test.xyz.<lang> .txt
,pageContents.lng.currAction
变量的值将成为其内容。
对于我们来说不幸的是,它最终只创建了.txt
文件,尽管我们可以控制文件的内容,因为它来自文件名本身。但是,随着进一步的研究,我们将看到如何利用文件名本身来完成工作。
紧接着,它将currFile
的内容存储在data
变量中,过滤出内容与正则表达式[''"##]stText\..+?[''"##]
不匹配的文件 ,然后将它们放入finds
数组中。
<cfset data = fileread(currFile) />
<cfset finds = rematchNoCase('[''"##]stText\..+?[''"##]', data) />
然后循环遍历finds
数组并检查每个项是否作为 key 存在。如果没有,则将其创建为 key ,并将其存储在searchresults
变量中。
<cfloop array="#finds#" index="str">
<cfset str=r ereplace(listRest(str, '.'), '.$', '') />
[..snip..]
<cfif structKeyExists(translations.en, str)>
<cfif not structKeyExists(searchresults[str], currAction)>
<cfset searchresults[str][currAction]=1 />
<cfelse>
<cfset searchresults[str][currAction]++ />
</cfif>
</cfif>
</cfloop>
最后,这些 key(即searchresults
变量)以 JSON 格式存储在dataDir
目录内名为searchindex.cfm
的文件中。
<cffile action="write" file="#dataDir#searchindex.cfm" charset="utf-8" output="#serialize(searchresults)#" mode="644" />