导语:大概两年前,我得到了一个.DS_Store这个文件,然后想详细的了解这一文件中包含的信息。在研究了它的文件格式以及编写完它的解析工具之后,我将了解到的知识以及用python和GO语言编写的解析器分享给大家。

这篇文章会讲解该文件的生成方式以及如何利用其在Alexa Top 1000中的网站中发现我们感兴趣的文件,比如:.sql/.db/.swp/.tgz。

介绍

我们先对这一篇文章做一个简单的介绍以及我是如何看待苹果公司的文件格式的。如果你想直接阅读关于技术的部分,请跳过这一板块,进入到下一部分当中。大概两年前,在对web服务器上敏感文件做研究时,我遇到了一个名为.DS_Store的文件。然后我就使用GO语言扫描Alexa Top1000的网站是否存在这一文件,并且从这一文件中发现网站的问题。但是Go语言没有针对.DS_Store文件的解析器,而python中存在,但是解析器并没有正常的工作,并且我想要的是在Go语言中调用一个函数或者类去解决这一问题,而不是使用其他语言之外的东西。
因此,我认为在Go语言中实现这一功能是一个不错的练习机会,会让我GO语言的功底提升不少,经过努力,终于实现了,最终代码地址:https://github.com/gehaxelt/ds_store 。不过有点尴尬的是,我在写代码的过程中并没有加注释,所以读起来有点吃力。

在去年莱比锡举行的34C3大会上,我和一位同事决定再次研究一下这一格式文件。并且目前为止,我们已经完成了,我感觉我应该向大家分享一下我的收获。由于我不能完全理解我两年前写的代码,于是我又开始深入到了细节当中,并且决定在python环境下实现解析器。

到现在,我仍然记不起来当时得到的一些细节上的东西,但是新的解析器应该具有相同的功能,并且我现在会尝试去介绍文件格式。

什么是.DS_Store文件?

在解析.DS_Store文件之前,我先告诉你一些关于它的信息。你能从使用MACOS系统的同事U盘的隐藏文件中发现他,或者在其他的地方你也见过。苹果的操作系统会在每一个文件夹中产生这个文件记录这个文件夹中的相关信息。实际上,这一文件包含了文件夹中所有的文件名和子文件夹名。和windows相比,等同于desktop.ini和Thumbs.db两个文件。
由于.DS_Store这一文件前面包含了一个“.”所以他在文件夹中是以隐藏文件的格式存在的。MacOS用户也许并没有发现他的存在。并且在网上并没有太多关于它的介绍文档。

如何去解析.DS_Store文件

我不是第一个去写这一文件解析器的人,所以我不想声明这一点,我想说的是为它写解析器是一个很好的学习经验,我借助了以下资源去学习它:

https://wiki.mozilla.org/DS_Store_File_Format 
http://search.cpan.org/~wiml/Mac-Finder-DSStore/DSStoreFormat.pod
https://digi.ninja/projects/fdb.php

我建议阅读完上方三个资料之后在阅读这篇文章,这样会对这一文件有一个粗略的了解。
无论如何,让我们开始吧,我下面会用一个实例的.DS_Store文件来解释它的结构,解析器的工作步骤同理。

文件头部

这是大端格式下的文件头部占用的36个字节:

dsstore_header.png

前四个字节的整数总是0x01,是用来做对齐方式的,所以他是在文件最头部的。蓝色部分被称为魔术字节(0x42756431)。

两个红色的部分是四个字节的整数(0x1000),用来定义文件的根块的位置(偏移量),根块是用来去解析其他文件的信息。这两个偏移量必须是相同的值,否则该文件会被视为无效。之前的绿色的四个字节整数是表示前面提到的根块的大小。

剩余的16个灰色字节并没有被反转,可以被认为是不确定数据,所以解析器可以直接跳过他。

根块介绍

我们现在了解到了根块的基本信息:位置和大小。我们可以在0x1004至0x1804之间进行探索。值得注意的是我们使用先前获得位置时要附加上块对齐的0x04.

rootnode.png

相关文件名的信息存储在树状结构当中,其中根块包含关于树以及其他块的重要元数据,通常元数据可以被分为三个不同的部分:

1. 偏移部分
2. 内容表
3. 空闲表

偏移部分

偏移部分包含有关文件中树(叶)块的偏移量信息,这些块存储的都是目录的实际信息,如文件名。获得偏移量需要遍历这个树。

dsstore_offsets-1.png

蓝色整数(0x03)告诉我们需要去读多少偏移量,跳过四个灰色字节(总是为0),接下来的12个绿色字节是三个4字节整数,构成了偏移量列表:

0x0000100B
0x00000045
0x00000209

这三个数字的顺序很重要,因为我们会根据这些变量去索引访问。这些偏移量是文件中树的块位置。该部分的剩余字节都是用0进行填充的(红色字节),并且填充到0x140c。

内容表

在偏移部分结束后,内容表部分就会呈现出来。他通常存在只有一个名为DSDB的表,并且值为1。这个特殊的表通常引用了我们将要遍历的第一个块的ID。

dsstore_toc.png

红色字节来自偏移量部分的填充,并且TOC从0x140c开始,四个蓝字节表示要解析的TOC的数量。在我们的例子中,只有一个。后面跟着一个绿色的字节,表示TOC(tables of content)的名称长度为0x04个字节。TOC名称可以通过黄色标记的自己的作为ASCII字符串来检索。在名称后面,紫色的四个字节就是TOC存储的值。
综上,可以将TOC表示为:

['DSDB']=0x01

空闲表

最后一部分是空闲表,也就是在树中还有哪些地方是没有使用的或者是空闲的模块。在实践中,我没有使用空闲表中的任何一个变量,但是在其他情况下可能会有用。

dsstore_freelist.png

它包含了通过2^n(0,31)定义的字典变量,在上图的例子中,空闲表的开端是0x1419。蓝色的四个字节整数代表一个部分已经被读取。这个整数代表着我们需要读取的偏移量。
在上图hexdump界面中,前五个部分的地址是0x1419到0x142d,其中存了0个元素。在0x142d之后的第六个部分的值是0x02,所以我们读出两个元素:0x00000020和0x00000060。

在进行相同的步骤之后,我们得到了空闲队列类似于:

{
1: [],
2: [],
4: [],
8: [],
16: [],
32: [32, 96],
64: [],
128: [128],
256: [256],
512: [],
1024: [1024],
2048: [2048, 6144],
4096: [],
8192: [8192],
16384: [16384],
32768: [32768],
65536: [65536],
131072: [131072],
262144: [262144],
524288: [524288],
1048576: [1048576],
2097152: [2097152],
4194304: [4194304],
8388608: [8388608],
16777216: [16777216],
33554432: [33554432],
67108864: [67108864],
134217728: [134217728],
268435456: [268435456],
536870912: [536870912],
1073741824: [1073741824],
2147483648: []
}

解析完这三个部分之后,我们关于根块的工作已经完成,接下来就是解析关于树的工作了。

正如我之前所说的,信息在文件当中成树状排列。我们需要遍历这个树来获取在文件中存储的文件名和其他信息。

块号和偏移量

在之前,我已经说过内容表中是存在块号的,特别是DSDB内容表中通过它的块号对第一个块进行引用,然后对内容表进行遍历。在我们这个例子中,块号是0x01.
我们通过块号作为索引从我们之前的列表中获取地址,比如:

offsets[0x01] => 0x00000045

但是我们无法利用0x00000045这个位置简单的计算出我们想要得到的数据,因为真正的偏移量以及块的大小是通过这一个值进行编码的:

1. 2^k 其中k是5个最低有效位,表示块的大小。应该小于32个字节
2. 当5个最低有效位全部设置为0时,表示块的偏移量。

使用实例地址0x00000045进行计算,我们得到了下面的结果:

1. 偏移量: int(0x00000045) >> 0x5 << 0x5 = 0x40
2. 大小:1 << (int(0x00000045) & 0x1f) = 0x20

于是,块号为1的块起始位置为0x40+0x4=0x44,大小为0x20字节。

解析树

为了得到文件名,我们需要从树的根块去解析。在之前的描述中,DSDB内容表中记录的是第一个根块的块号,并且它开始于0x44位置。

dsstore_rootblock.png

这个块包含了五个整数,其中红色部分是最有趣的,因为它包含了具有实际数据的第一个块的块号。其他颜色的整数意思是:

绿色:内部块的级别(0x00)
黄色:树中存在多少记录(0x06)
蓝色:树中的块数(0x01)
棕色:不变的值(0x1000)

于是对0x02这个块进行解析,然后遍历整个树就可以得到文件名。
数据块的地址是

offsets[0x02] => 0x00000209

通过解析,可以获得:
1. 偏移量:int(0x00000209) >> 0x5 << 0x5 = 0x200
2. 大小:1 << (int(0x00000209) & 0x1f) = 0x200

得到数据块的起始位置是0x204,继续通过hexdump追踪:

dsstore_block.png

块中包含了两个重要的整数:

红色:块模式(0x00)
绿色:记录数量(0x06)

如果模式为0x00,则紧接着是计数记录。否则,跟随下一块ID的记录对,其中遍历函数可以通过下一个块的块号进行递归调用。
因为我们的实例块处于模式0x00,因此只需要解析0x06记录。

记录

现在让我们看看在块中的记录是什么样子的。
记录以蓝色字节(文件名长度)以及黄色字节(文件名)开始。文件名之后是一个四个字节整数(棕色),代表着结构号(现在我还不确定这个是干啥的),之后表示的是结构类型(红色)。根据结构的不同,在到达当前块末尾之前,需要跳过不同数量的字节。可以在http://search.cpan.org/~wiml/Mac-Finder-DSStore/DSStoreFormat.pod找到不同结构类型对应的数量。
一旦解析完成,六个文件名就会呈现出来:

favicon.ico
flag
static
templates
vulnerable.py
vulnerable.wsgi

代码

我会给大家分享我这几年写的代码,不过并不能保证代码不存在bug,也不能保证这是一份完美的代码。我之前也提到过,这不是我第一次写这一类型的解析器,以及代码也是基于工作研究的成果,并且功能也许不是很完整。非常欢迎大家提出问题和进行修复!

如果你想挑战一下,你可以在下面两个链接找到相应的版本:

https://github.com/gehaxelt/ds_store
https://github.com/gehaxelt/Python-dsstore

如果你只是想简单的解析一下文件,你只需使用web版本进行解析:
https://labs.internetwache.org/ds_store/

已知的问题

在开发代码和写博客的过程中,发现了我在实现代码和解析逻辑上面的一些问题。我想在这简单的讨论一下。也许你会找到解决办法!

根块偏移量

我遇到过一个.DS_STORE文件,其中根块与最初的头部解析的偏移量向后推了4个字节。这就导致后面的步骤出错。这是一个非常罕见的事情,我不知道它是由于什么原因发生的。

文件名长度不正确

我遇到的另一个问题就是记录中文件名长度有错误,例如,它显示的长度为0x0a(10*2个字节),但是UTF-16文件名实际上大于20个字节。通常这会导致无法匹配结构类型,然后产生错误。使用hexeditor修改为正确的长度就可以修复这一错误。但是我还是不确定这种很明显的错误是如何发生的。

尽管如此,我已经采用了类似暴力破解的方式解决这一问题:重新读取文件名的后两个字节,知道出现已知的结构号。虽然采用这种方法解决了这个问题,但我觉得它并不是一个好方法。

它会带来什么安全影响?

到目前为止,我们只讨论了.DS_Store文件的结构和内容,但是这是一个关于安全的博客,所以我下面要说的是他会带来什么安全影响。当这一文件上传到了web服务器时,往往会带来一定的危害。

在我看来,它带来的危害是他包含的文件名。MacOs在几乎所有文件夹都创建了一个.DS_Store文件。

信息泄漏(敏感文件)

通过我参与的Internetwache.org网站项目,对Alexa Top 1000的网站的根目录进行扫描,证明在有的网站中的确存在这一文件,导致信息泄漏。通过解析这一文件,我们发现了数据库备份,配置文件,以及一些缓存文件,甚至是密钥。

你可以在https://en.internetwache.org/scanning-the-alexa-top-1m-for-ds-store-files-12-03-2018/这里查看具体的信息。

如何检测并且保护好自身呢?

需要澄清的一个事实是存储在.DS_Store文件中的文件名仅代表本地MacOS系统上的目录内容。这就意味着解析出来的文件列表中有些文件可能不存在。
你可能会在以下几种情况下上传这一文件:

1. 将文件提交给版本控制系统(git/svn/etc)
2. 未删除这一文件,就使用rsync/sftp上传到服务器中。
3. 服务器系统也许就是MACOS

如果你想现在检测你的服务器是否处在危险当中,你可以执行以下命令(linux):

cd /var/www/ #wherever your webserver's document root is 
find . -type f -iname "*.DS_Store*"

这一命令就是在/var/www/文件夹下找到.DS_Store文件,然后打印出他们的位置。
如果你没有找到任何文件,说明你的服务器是安全的。如果你找到了文件,那么你可以对他们进行删除。

另外,你可以通过设置你的服务器去禁止这些文件访问:
Apache下,在http.conf文件中加入以下代码:

<Files ~ "\.DS_Store$">
    Order allow,deny
    Deny from all
</Files>

Nginx下,在server block中添加以下代码:

location ~ \.DS_Store$ {
      deny all;
}

源链接

Hacking more

...