模块开发

需要在 ExpDepos 中运行的模块都必须继承自 ExpDepos.libs.core.base.ExploitBase.ExploitBase 类,且需要重写父类 _exploit_verify 函数。在 ExploitBase 模块中不仅定义了 ExploitBase 类本身,还引入了模块运行时所依赖的其他模块。因此在模块开发时候可以一次性引入所有模块,如下所示:

from ExpDepos.libs.core.base.ExploitBase import *

备注

为方便快速开发模块,可以 点击这里 获取完整模块模板,新的模块文件需要根据分类放到 ExpDepos/modules 目录下。为更好的分类存放模块文件,用户也可以选择新建目录用于存放同一类模块,例如 ExpDepos/modules/exploits/webapp/ 下的 tdoa_exploits

模块定义

模块类型定义请遵循与模块文件同名原则(该规则类似 java 的类型定义),否则将无法正确加载定义的模块。且类型继承自 ExploitBase,并重写父类 _exploit_verify 函数。如下所示创建一个名为 example.py 的模块文件,其定义如下:

from ExpDepos.libs.core.base.ExploitBase import *


class example(ExploitBase):
    def _verify(self):
        pass

    def _exploit(self):
        pass

小技巧

如果不想编写模块的 verify 模式,可以在 _verify 函数中直接返回 _exploit 函数的调用,反之亦然。例如 return self._exploit() 或者在 _exploit 函数中 return self._verify()

_init()

在模块调用 _verify()_exploit() 函数之前默认会先调用初始化函数 _init(),如果有需要在 _verify()_exploit() 函数调用之前准备的业务逻辑,可以重写该函数并在其中实现,例如准备模块所需参数变量值等。

基本属性

模块类型静态属性用于定义模块基本信息,例如模块名称、开发作者、版本号等。这些属性都是固定的名称,开发过程中只允许覆盖这些值来定义模块的基本信息。下面展示了位于 ExploitBase 类中所有基本属性的默认定义:

class ExploitBase(object):
    Name = None
    Alias = None
    Author = None
    VulType = None
    Category = None
    Create_Date = None
    Update_Date = None
    Rank = None
    AppPowerLink = None
    AppName = None
    AppVersion = None
    References = []
    Desc = """
        """
    Description = """
            """

    def _verify(self):
        pass

    def _exploit(self):
        pass

属性名称对应用途如下:

  • Name 模块名称

  • Alias 模块别名,用于 -M 选项或是交互式Shell的 use 命令

  • Author 模块作者,多个请使用 list 列表

  • VulType 漏洞类型,详情定义请参考 漏洞类型定义

  • Category 漏洞分类,详情定义请参考 漏洞分类定义

  • Create_Date 模块开发时间

  • Update_Date 模块更新时间

  • Rank 模块效果分级(可选:RANK.Excellent RANK.Great RANK.Good RANK.Normal RANK.Average RANK.Low)

  • AppPowerLink 漏洞厂商主页地址

  • AppName 漏洞应用名称

  • AppVersion 漏洞影响版本

  • References 参考链接,多个请使用 list 列表

  • Desc 漏洞描述,使用一句话描述该Exploit模块的主要功能(不支持换行或 Markdown 语法)

  • Description 漏洞详情,支持 Markdown 语法

参数设置

要为模块设置参数选项需要重写父类的 _options 函数,并在该函数中返回一个以键值作为参数名的字典。其值有多种类型可选,如下代码所示:

def _options(self):
    options = dict()
    options["name"] = OptString("admin", description="字符类型示例", require=True)
    options["Age"] = OptInteger(20, description="整型参数示例", require=True)
    options["require"] = OptBoolean(False, description="非必选项参数示例", require=False)
    return options

ExpDepos 中为模块参数内置了下列基本数据类型可供选择:

  • OptString 类型用于存储值为 str 类型的模块参数

  • OptInteger 类型用于存储值为 int 类型的模块参数

  • OptFloat 类型用于存储值为 float 类型的模块参数

  • OptBoolean 类型用于存储值为 bool 类型的模块参数

  • OptHost 类型用于存储值为 HOST 数据的模块参数

  • OptPayload 类型用于存储其他 payload 类型的模块参数

  • OptEncoder 类型用于存储值为 encoder 类型的模块参数

这些内置的基本类型都继承自 ExpDepos.libs.core.base.OptionsBase.Option 类,他们构造函数如下所示:

ExpDepos.libs.core.base.OptionsBase.Option.__init__(self, default='', description='', require=False, choices=[])
参数
  • default -- 参数默认值

  • description -- 参数描述信息

  • require -- 是否必选参数

  • choices -- 可选值列表

小技巧

所有内置基本类型的默认值同时也可以使用 str 类型数据,这将会根据字面值转换成对应的数据类型。如果转换失败则会抛出异常,例如: options["require"] = OptBoolean("False", description="非必选项参数示例")

参数获取

要在模块中获取用户在命令行使用 -P 或者 --options 为模块提供的参数值,需要在模块中调用 self.get_option() 函数。该函数参数接收一个字符串作为参数名(不区分大小写),如果参数不存在将抛出异常。此外还可以使用 self.get_options() 函数获取全部参数,该函数返回一个包含所有参数及参数值的字典类型。

get_option 函数定义如下:

ExpDepos.libs.core.base.ExploitBase.ExploitBase.get_option(self, key, default=None)

返回模块用户自定义参数值

参数
  • key -- 需要获取的参数名

  • default -- 默认值

返回

参数名对应值

输出/输入

消息输出

ExpDepos 采用 rich 模块作为整体的消息输出工具,在该模块基础上重新定义了 ExpDepos.libs.core.common.Console.Console 模块。并在全局环境中实例化为 console 的引用,您可以在模块的任意地方直接使用该引用来输出不同级别的消息,各级别消息定义如下所示:

  • EXCEPTION 向终端输出输出异常类消息。

  • ERROR 向终端输出错误类消息。

  • WARNING 向终端输出告警消息。

  • FAILED 向终端输出失败消息,用于漏洞利用失败的情况。

  • SUCCESS 向终端输出成功消息,用于漏洞利用成功的情况。

  • INFO 向终端输出常规消息。

  • DEBUG 向终端输出调试消息。

  • PROGRESS 用于输出进度条过程中的消息(使用是需使用 formatPgString 函数格式化)。

代码示例

def _verify(self):
console.info("这是在_verify中输出的INFO信息,即将跳转至_exploit执行。")
return self._exploit()

def _exploit(self):
    # 在模块中使用统一的消息输出
    console.info("测试INFO级别消息输出")
    console.debug("测试 [bold green]DEBUG[bold green] 级别消息输出")
    console.warning("测试 [bold yellow]WARNING[/bold yellow] 级别消息输出")
    try:
        1 / 0
    except Exception as e:
        console.exception("测试 [bold red]EXCEPTION[bold red] 级别消息输出:{0}".format(e.args[0]))
    with Progress() as progress:
        task = progress.add_task(formatPgString("测试 [green]PROGRESS[green] 级别消息输出..."), total=10)
        for i in range(10):
            progress.print(formatPgString("progress {0} done.".format(i)))
            progress.update(task, advance=1)
            time.sleep(1)

数据输入

console 中另外一个函数 input 用于接收用户输入的数据,该函数使用方式和 Pythoninput 一样。下列代码展示了如何让用户做出规定的选择:

choose = ''
    while choose.lower() != 'y' and choose.lower() != 'n':
        choose = console.input("[bold yellow]是否继续演示其他示例?[bold yellow] [bold green]Y/N:[/bold green]")
    if choose == 'n':
        return
    console.info("example 将继续演示其他示例")

备注

console 引用的对象是对 rich.Console 模块的拓展和封装,该类提供的函数均可使用 rich 语法为消息进行渲染。且输出输入函数与 Python 内置的 printinput 一样。需要输出不带时间前缀的消息可以使用 console.print() 函数,更多有关 rich 的用法请参考 rich官方文档

HTTP请求

要在模块中发起HTTP请求可以使用模块基类已实例化的引用 self.request,该引用为一个继承了 httpx.Client 类的 Request 对象。所以可以像使用 requests 模块一样来发起HTTP请求,如下列代码所示:

# HTTP 请求测试
console.info("正在请求:" + str(self.request.base_url) + "tongda.ico")
response = self.request.get("/tongda.ico")
if response.status_code == 200:
    console.info({"status": response.status_code,
                  "md5": response.md5sum,
                  "hash": response.hash,
                  "server": response.server,
                  "base64": response.base64})

备注

httpx 的使用类似我们熟悉的 requests 模块,同样可以自定义请求方式和请求参数。需要了解更多 httpx 的使用请参阅 httpx官方文档

Response

同样的我们也为 HTTPResponse 对象做了扩展,就像上诉代码中看到的一样,可以使用该对象的 md5sum 属性来获取返回内容的 MD5 值。以下列出的是 Response 对象的全部扩展属性:

  • md5sum 返回 HTTP Response 内容的 MD5 值。

  • hash 返回 HTTP Response 内容的 mmh3 值,类似 fofa 等网络搜索引擎的 favicon.ico 文件搜索功能都基于该算法。

  • base64 返回 HTTP Response 内容的 base64 值。

  • segmentBase64 返回 HTTP Response 内容以每76个字符加入换行的 base64 值。

  • server 返回自动识别到的服务器常用中间件名称和版本。

除此之 Response 对象额外还具有以下几个函数用于常用的内容查找:

ExpDepos.libs.core.Response.Response.contains(self, keyword: str) bool

检查response.text中是否包含keyword

参数

keyword -- 需要查找的关键字

返回

bool 是否含有关键字keyword

ExpDepos.libs.core.Response.Response.bcontains(self, keyword: bytes) bool

检查response.content中是否包含二进制内容keyword

参数

keyword -- 需要查找的关键字

返回

bool 是否含有关键字keyword

ExpDepos.libs.core.Response.Response.search(self, regex: str) match

在response.text中正则匹配regex

参数

regex -- 需要匹配的正则表达式

返回

re.match 匹配对象

异步请求

选择使用 httpx 作为 ExpDepos 内置的HTTP请求模块,主要原因还是 httpx 支持异步请求,这对编写一些高性能模块有着关键性作用。要在模块中使用异步请求接口我们需要用到在基类中已实例化的 self.asyncRequest 引用,该引用的对象为一个继承了 httpx.AsyncClientRequest 对象。以下是一个测试使用异步请求发起 1000 次HTTP请求,并统计其耗时的代码示例:

async def _request(self):
response = await self.asyncRequest.get("/")
assert response.status_code == 200

@asyncTimeit
async def _testAsyncRequest(self, times):
    """
    测试异步请求性能

    :param times: 测试请求次数
    :return:
    """
    task_list = list()
    for x in range(times):
        req = self._request()
        if sys.version_info < (3, 7):
            task = asyncio.ensure_future(req)
        else:
            task = asyncio.create_task(req)
        task_list.append(task)
    await asyncio.gather(*task_list)

def _exploit(self):
    # HTTP 异步请求测试
    console.info("正在执行异步请求测试...")
    loop = None
    try:
        loop = asyncio.get_event_loop()
    except RuntimeError as e:
        if "There is no current event loop in thread" in str(e):
            loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)
    asyncio.get_event_loop().run_until_complete(self._testAsyncRequest(1000))

指纹识别

ExpDepos 中内置了 WhatWeb 增强版 最新的 8000+ 指纹模块。要在模块中使用这些指纹对目标进行识别可以直接调用 self.fpMatches() 函数,并传入被识别目标的 HTTP Response 对象和模式即可,以下是 self.fpMatches() 的函数定义:

ExpDepos.libs.core.base.ExploitBase.ExploitBase.fpMatches(self, response: <module 'ExpDepos.libs.core.Response' from '/home/docs/checkouts/readthedocs.org/user_builds/expdepos/checkouts/latest/ExpDepos/libs/core/Response.py'>, mode='ALL', host='', fp_filter={}, progress=None) list

指纹识别

参数
  • response -- Response 对象

  • mode -- 识别模式 可选ALL、PASS, AGGR, EXP

  • fp_filter -- 指纹过滤器 用于过滤包含指定过滤器的指纹

@param host

指定特定的host

@param progress

进度条对象

返回

list

为了在指纹识别过程中尽可能少的发送HTTP请求,我们将指纹识别分为 aggressive (主动模式)和 passive (被动模式)。 主动模式即是需要对目标主动发起HTTP请求特定URL的方式,被动模式则不需要主动请求特定URL,它只需要从HTTP响应包中进行关键词识别。以下是对几种模式的详细说明:

  • AGGR 主动模式,该模式将提取所有指纹中需要主动请求特定URL的匹配项进行识别(该模式会发送大量的HTTP 请求)。

  • PASS 被动模式,该模式将提取所有指纹中需要被动识别的匹配项对HTTP Response内容进行匹配。

  • EXP Exploit模式,该模式只提取在 ExpDepos 模块中注册的指纹进行识别(有关在模块中编写指纹请参阅 指纹编写 章节)。

  • ALL 使用以上全部模式(这也是默认模式)。

fpFilter 参数过滤了那些只有符合过滤条件的指纹才进行匹配,该参数接收一个字典类型作为值。指纹过滤将从 platform (操作系统平台)、middleware (中间件)、language (脚本语言)、type``(指纹分类) 四个维度进行过滤,每一个维度使用一个 ``list 作为过滤值。

当前模块指纹

如果为当前模块编写了指纹,要使用当前指纹而不是匹配全部指纹库,需要先获取当前指纹属性 self.fingerprint 来获取一个 Fingerprint 对象,并调用他的 Matches() 函数进行识别,以下是 Matches() 的函数定义:

ExpDepos.libs.core.base.Fingerprint.Fingerprint.Matches(self, moduleObj, responseObj: Optional[Response] = None, mode='ALL') list

并发匹配指纹

参数
  • moduleObj -- 已实例化的模块对象

  • responseObj -- 已实例化的ExpDepos.libs.core.Response对象

  • mode -- 匹配模式 可选ALL、PASS, AGGR, EXP

返回

list 匹配成功列表

调用 Matches() 函数需要将模块对象传入第一个参数,第二个参数为一个HTTP Response对象,如下代码展示了全部指纹的主动模式匹配和当前模块指纹的ALL模式匹配:

def _fingers(self):
        """
        当前模块指纹定义

        :return: dict
        """
        fingerprint = {
            "name": self.AppName,                           # 漏洞应用名作为指纹名称
            "author": self.Author,                          # 作者
            "version": "1.0",                               # 版本号
            "type": FINGERPRINT.FP_TYPE.WEBAPP,             # 指纹类型 详情请参考指纹类型表
            "logic": "or",                                  # 匹配逻辑 默认为 or
            "description": "指纹描述信息",                    # 描述信息
            "website": "https://www.tongda2000.com/",
            "filters": {                                    # 过滤属性,用于从操作系统平台、中间件、和脚本语言3个维度进行过滤
                "platform": ['windows', 'Unix'],
                "middleware": ['apache', 'nginx'],
                "language": ['PHP']
            },
            "matches": [{"url": "/tongda.ico?r={randstr()}", "hash": -759108386, "certainty": 100, "status": 200},
                        {"search": "headers", "keyword": "X-Powered-By: PHP/7.2.24-0ubuntu0.18.04.8"},
                        {"search": "headers[set-cookie]", "regex": "(aa)", "offset": 1, "version": "2.2",
                         "aim": "version"},
                        {"name": "matchName", "keyword": "test <||> referer"},
                        {"status": 200}],
            "sets": {                                       # 主动式匹配的一些HTTP Request设置
                "headers": {"testHeader": "testHeader{randstr(10)}"},
                "cookies": {"cname": "cValue{randstr(5,true)}"},
                "params": {"test": "bbbb"},
                "data": ""
            }
        }
        return fingerprint

def _exploit(self):
    # 指纹识别测试
    response = self.request.get("")
    console.info("被动指纹匹配结果: ")
    console.info(self.fpMatches(response, "PASS"))
    console.info("当前模块指纹匹配结果:")
    console.info(self.fingerprint.Matches(self, response))

小技巧

第二参数Response对象为可选参数,如果未提供将不进行被动模式匹配。

获取Payload

要在模块中获取用户设置的 Payload 可以使用 self.payload 直接获取,这将返回 Payload 的字符串类型值(如果用户设置了 encoderpayload 将经过对应编码器编码后返回)。如果需要获取 bytes 类型的 payload 值则需使用 self.payload.bytes 获取。

执行结果

ExpDepos 作为一款运行高质量 Exploit 模块的框架,为了突出模块执行结果,我们专门为其提供了 Result 类用于存储执行结果相关的数据。在模块基类中已实例化为 self.result 引用,该引用可以在模块中直接使用。以下为 Result 模块的定义:

@Author: Castiel @Email: ca3tie1@gmail.com @Blog: https://ca3tie1.github.io @Git: https://github.com/ca3tie1 @Wechat: Ca5tie1 @Date: 2021/7/31 18:00

class ExpDepos.libs.core.common.Result.Result(module)[源代码]

exploit模块执行结果类 用于存储exploit模块执行的各项结果数据

fail(reason: Optional[str] = None, result: Optional[dict] = None)[源代码]

设置失败信息

参数
  • reason -- 失败原因

  • result -- exploit利用失败结果及,必须是键值对应的字典类型

返回

is_success()[源代码]

校验模块是否利用成功

返回

bool

success(message: Optional[str] = None, result: Optional[dict] = None)[源代码]

设置成功信息

参数
  • message -- 成功消息

  • result -- exploit利用成功结果集,必须是键值对应的字典类型

返回

在模块开发过程中我们只需要调用 self.resultsuccess() 或者 fail() 函数来设置模块执行结果,ExpDepos 将在模块执行完后根据 self.result 的结果使用 SUCCESS 还是 FAILED 级别的消息输出。 success()fail() 函数接收一个字符串和一个字典作为参数传入,字符串作为成功或者失败的主消息使用。字典作为成功或者失败的数据存储,用户可根据实际情况存储任何数据。同时该字典中亦可使用 message 键值作为主消息,但优先级低于字符串消息。