Binwalk目录遍历漏洞复现
2023-4-21 08:2:42 Author: ChaMd5安全团队(查看原文) 阅读量:16 收藏

没想到binwalk也会出漏洞,这漏洞用来投毒钓鱼,真的绝啊!

漏洞成因

根据描述问题是出在PFS文件系统提取器当中,通过目录穿越写入恶意的插件。

1

最关键的点是outfile_path获取写入路径的时候没有将路径限制为当前的路径,导致存在目录遍历的可能,接着下面就是将PFS文件的数据写入,导致任意文件写的漏洞。

def extractor(self, fname):
    fname = os.path.abspath(fname)
    out_dir = binwalk.core.common.unique_file_name(os.path.join(os.path.dirname(fname), "pfs-root"))

    try:
        with PFS(fname) as fs:
            # The end of PFS meta data is the start of the actual data
            data = binwalk.core.common.BlockFile(fname, 'rb')
            data.seek(fs.get_end_of_meta_data())
            for entry in fs.entries():
                outfile_path = os.path.join(out_dir, entry.fname)
                if not outfile_path.startswith(out_dir):
                    binwalk.core.common.warning("Unpfs extractor detected directory traversal attempt for file: '%s'. Refusing to extract." % outfile_path)
                else:
                    self._create_dir_from_fname(outfile_path)
                    outfile = binwalk.core.common.BlockFile(outfile_path, 'wb')
                    outfile.write(data.read(entry.fsize))
                    outfile.close()
            data.close()
    except KeyboardInterrupt as e:
        raise e
    except Exception as e:
        return False

所以需要制作一个PFS文件,写入的路径为Binwalk插件的路径,内容就是初始化插件,成功写入之后当Binwalk调用这个恶意插件的时候就可以实现任意代码执行。

PFS文件解析

PFS文件需要去逆unpfs.py这个文件是如何解析PFS文件的,还有少量公开的PFS格式解析。下面是解析PFS解析工具,里面已经透露了很多信息了。

#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <stdlib.h>
#include <assert.h>

#include <unistd.h>

#define u32 uint32_t
#define u16 uint16_t
#define u8 uint8_t

struct pfs_header {
 char magic[8]; /* PFS/0.9, are there others? */
 u32 wtf1;
 u16 wtf2;
 u16 entries;
};

struct pfs_entry {
 char path[128]; /* i've seen 32 and 40-byte variants */
 u32 wtf; /* what this be? it seems it's always non zero. path terminator? */
 u32 offset;
 u32 size;
};

/* so noone with BE machine complains! */
static u16 le16_to_cpu(u16 x)
{
 u16 lower = ((u8*)&x)[0];
 u16 upper = ((u8*)&x)[1];

 return lower + (upper << 8);
}

static u32 le32_to_cpu(u32 x)
{
 u32 b0 = ((u8*)&x)[0];
 u32 b1 = ((u8*)&x)[1];
 u32 b2 = ((u8*)&x)[2];
 u32 b3 = ((u8*)&x)[3];

 return b0 + (b1<<8) + (b2 << 16) + (b3 << 24);
}

/**
 * yes, I know, many checks are missing, and real/lseek is ugly,
 * CBA to clean it up
 **/

int main()
{
 int i;
 struct pfs_entry *entries;
 struct pfs_header header;
 void *filebuf;

 char buf[128];
 int path_len;
 char *tmp;

 /* read header */
 read(0, &header, sizeof(header));

 if (strncmp(header.magic, "PFS"3) != 0) {
  fprintf(stderr"no PFS signature, exiting");
  return 1;
 }
 header.wtf1 = le32_to_cpu(header.wtf1);
 header.wtf2 = le16_to_cpu(header.wtf2);
 header.entries = le16_to_cpu(header.entries);

 printf("sig:\t%s\n", header.magic);

 printf("wtf1:\t0x%08x\n", header.wtf1);
 printf("wtf2:\t0x%04x\n", header.wtf2);
 printf("entries: %i\n", header.entries);

 /* read directory entries */
 entries = malloc(sizeof(entries[0]) * header.entries);
#define MAX_SIZE (128*1024)
 filebuf = malloc(MAX_SIZE);
 assert(entries);
 assert(filebuf);

 read(0, buf, 128);
 tmp = &buf[strlen(buf)];
 while (*tmp == '\0')
  tmp++;
 path_len = tmp - buf;
 lseek(0-128, SEEK_CUR);
 printf("detected path length of %i bytes\n", path_len);

 for (i=0; i<header.entries; i++) {
  read(0, entries[i].path, path_len);
  read(0, &entries[i].wtf, 12); /* remaining 3*4 bytes */

  entries[i].wtf = le32_to_cpu(entries[i].wtf);
  entries[i].offset = le32_to_cpu(entries[i].offset);
  entries[i].size = le32_to_cpu(entries[i].size);
 }

 /* read and write files */
 for (i=0; i<header.entries; i++) {
  FILE *file;
  int r;
  printf("%-36s", entries[i].path);
  printf("?1:0x%08x ", entries[i].wtf);
  printf("offset: %-5i ", entries[i].offset);
  printf("size: %i\n", entries[i].size);

  file = fopen(entries[i].path, "wb");
  assert(entries[i].size <= MAX_SIZE);
  r = read(0, filebuf, entries[i].size);
  fwrite(filebuf, 1, entries[i].size, file);
  fclose(file);
 }

 return 0;
}

Header

header的长度为16,前8个字节是固定的,为PFS/0.9,然后6个字节为\00,剩下两个字节用来标识PFS里面的文件数量。

self.num_files = self._make_short(header[-2:], endianness)

Fname

接下来128字节是PFS的文件名。

Inode_no

这四个字节不知道有啥用,填\00没啥问题。

Foffset

文件偏移也是填\00就行。

Fsize

这个得是写入文件的大小,需要填写入的恶意文件的大小。

Data

写入恶意的插件。


下面是PFS的结构重要部分的划分。

2

完整的POC就不放了,成功触发可以实现任意命令执行。

补丁比对

3

在终端下两种代码的运行结果:

>>> import os 
>>> os.path.join("/var""../etc/passwd")
'/var/../etc/passwd'
>>> os.path.abspath(os.path.join("/var""../etc/passwd"))
'/etc/passwd'

参考链接

  • firmware reverse engineering(http://0entropy.blogspot.com/2011/08/firmware-reverse-engineering.html)

  • fix path traversal in PFS extractor script(https://github.com/ReFirmLabs/binwalk/pull/617)

  • pfstool(https://lekensteyn.nl/files/pfs/pfs.txt)

  • Security Advisory: Remote Command Execution in binwalk(https://onekey.com/blog/security-advisory-remote-command-execution-in-binwalk/?cve=title#bonus)

招新小广告

ChaMd5 Venom 招收大佬入圈

新成立组IOT+工控+样本分析 长期招新

欢迎联系[email protected]


文章来源: http://mp.weixin.qq.com/s?__biz=MzIzMTc1MjExOQ==&mid=2247508590&idx=1&sn=e0a2c15288a1d9060c150e2d62be8478&chksm=e89d8ab6dfea03a0224313cde8150f6b97c4159cf67239c1bfb6ed83f9e35a4e649d2ae1f965#rd
如有侵权请联系:admin#unsafe.sh