Nuclei Fuzzer 实战指南:自动化 Web 应用安全测试的优化与实践

admin 2023年12月12日02:45:11评论38 views字数 11872阅读39分34秒阅读模式
声明

由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,文章作者不为此承担任何责任。

目录


前言

一次网上冲浪的过程中发现了一个项目https://github.com/0xKayala/NucleiFuzzer,抱着看看的心态开始了一段不归路,从有想法,到最后的勉强实现,花费了我整整一天的时间,期间十分的痛苦(现在要写这篇文章就更痛苦了)。整篇文章的思路,我会按照我自己实现的整个流程来,期间可能会有点乱,希望大家理解。

项目研究

NucleiFuzzer 是一款自动化工具,结合了ParamSpider和Nuclei ,以增强Web应用程序安全测试。它使用ParamSpider来识别潜在的入口点,并使用Nuclei的模板来扫描漏洞。NucleiFuzzer简化了这个过程,使安全专业人员和Web开发人员更容易有效地检测和解决安全风险。下载NucleiFuzzer以保护您的Web应用程序免受漏洞和攻击。

以上是项目本身的介绍,通过介绍不然发现,这个项目是一个类似于爬虫+漏扫联动类的项目,很像我们在攻防打点过程中经常使用的爬虫+Xray,这里它使用的Nuclei和自写的简单爬虫工具Paramspider,由于本人对于nuclei的使用较少,理解也很一般,我印象的nuclei就是扫poc的,对于常规的漏洞趋近于不支持,但是它竟然能用来fuzzing嘛?于是引起了我的兴趣,开始研究。

在项目readme中,我们可以看到用到的工具和模板,那么有经验的安服仔在这里基本就能看出来了,它这个项目的本质就是一个工具的联动脚本。

Nuclei Fuzzer 实战指南:自动化 Web 应用安全测试的优化与实践

工具一个是它自己写的爬虫,一个是Nuclei,暂时不看,先看看用到的模板吧!

fuzzing-templates


Nuclei Fuzzer 实战指南:自动化 Web 应用安全测试的优化与实践

跳转来到项目,发现它这是一个fork的项目,原项目是nuclei团队自己的!原来nuclei自己提供了fuzzing的模板,怪我对它的了解太少。

直接进入到原项目,先看readme:

Nuclei Fuzzer 实战指南:自动化 Web 应用安全测试的优化与实践

所以我们要使用这个template需要输入的URL是带有参数的,那么是不是可以认为NucleiFuzzer这个项目中,作者自写的爬虫工具就是为了得到带有参数的URL呢?我们暂时先不管,先点击一个具体的模板分析分析,到底是如何做到fuzzing的。

查看xxe/fuzz-xxe.yaml内容如下:

id: fuzz-xxe
info: name: XXE Fuzzing author: pwnhxl severity: medium reference: - https://github.com/andresriancho/w3af/blob/master/w3af/plugins/audit/xxe.py tags: dast,xxe
variables: rletter: "{{rand_base(6,'abc')}}"
http: - method: GET path: - "{{BaseURL}}"
payloads: xxe: - '<!DOCTYPE {{rletter}} [ <!ENTITY {{rletter}} SYSTEM "file:///c:/windows/win.ini"> ]><x>&{{rletter}};</x>' - '<!DOCTYPE {{rletter}} [ <!ENTITY {{rletter}} SYSTEM "file:////etc/passwd"> ]><x>&{{rletter}};</x>'
fuzzing: - part: query keys-regex: - "(.*?)xml(.*?)" fuzz: - "{{xxe}}"
- part: query values: - "(<!DOCTYPE|<?xml|%3C!DOCTYPE|%3C%3Fxml)(.*?)>" fuzz: - "{{xxe}}"
stop-at-first-match: true matchers-condition: or matchers: - type: regex name: linux part: body regex: - 'root:.*?:[0-9]*:[0-9]*:'
- type: word name: windows part: body words: - 'for 16-bit app support'

只需要看http和matchers部分即可,一个是payload,一个是结果判断。


HTTP标签:

HTTP请求部分

  • 方法:GET

  • 路径:使用变量{{BaseURL}}作为目标URL的基础路径,即传入的带有参数的URL

负载

  • XXE:定义了两个XXE攻击的负载。这些负载使用了DOCTYPE声明来定义一个实体,该实体尝试读取系统文件(如win.ini或/etc/passwd)。

fuzzing

  • part:指定模糊测试应用在HTTP请求的查询部分(query)。

  • keys-regex:定义了用于匹配包含"xml"的查询参数的正则表达式。

  • values:提供了正则表达式,用于匹配可能的XML声明或DOCTYPE声明。

  • fuzz:使用上面定义的xxe负载进行模糊测试。


matchers标签:

  • stop-at-first-match:为真,表示一旦匹配成功就停止。

  • matchers-condition:或,意味着满足任一匹配器就视为成功。

  • type:使用正则表达式(regex)和单词(word)两种类型的匹配器。

  • part:指定在响应体(body)中进行匹配。

  • words:提供了用于检测XXE漏洞的正则表达式和单词列表,如检查是否包含系统文件内容(例如/etc/passwd文件的内容)。


那么在分析完这个模板内容之后就十分清晰了,与一般的nuclei模板不同的地方就是增加了这个fuzzing标签部分,这部分可以粗略的理解为定义了哪些参数或使用正则匹配哪些内容,使用定义的fuzzing payload进行测试,其他的与一般的模板并无不同,其他的fuzzing-templates也是相同的原理。

简单测试下这个模板:

nuclei.exe -t fuzzing -u http://127.0.0.1/test?id=1 -v

Nuclei Fuzzer 实战指南:自动化 Web 应用安全测试的优化与实践

可以看到跟我们分析的基本一致,替换URL中的参数,进行发包FUZZING。

那么我们分析完之后,阶段性总结一下,现在发现的问题:

1.要使用fuzzing-templates需要提供的URL必须是带有参数的,我们要想点子获取带参数的URL;

2.模板本身目前全部仅支持GET请求;

问题2目前来说我并没有解决的能力,毕竟这已经是官方自己的模板文件了,看来要实现POST的fuzzing还是要使用ffuf、burp这一类的工具啊。但此时问题1还是有解决办法的,我们是从NucleiFuzzer这个项目调过来的,工具中存在一个ParamSpider的,那么我们就顺势开始这个工具的分析吧。

ParamSpider

先说结论,这是一个比较简陋的爬虫小工具,甚至于它并不能称为爬虫工具。

项目结构如下:

Nuclei Fuzzer 实战指南:自动化 Web 应用安全测试的优化与实践

直接丢给GPT完成的分析,那么主要的功能部分就是core部分,我们将core部分除init外的3个主要文件进行分析。

按照一般的处理流程:requester(请求)——>extractor(提取)——>save_it(保存),保存函数不进行分析,尝试对请求和提取进行分析。

  • requester:

Nuclei Fuzzer 实战指南:自动化 Web 应用安全测试的优化与实践

上面是定义UA头,并随机选择,下面是对URL发起请求,获取response的内容,试用raise_for_status()处理错误的返回状态码,如404或500,最后是一系列的异常处理操作。得出结论这就是个纯粹的request,可以说是没有进行其他任何操作。

  • extractor:

Nuclei Fuzzer 实战指南:自动化 Web 应用安全测试的优化与实践

传入四个参数:response, level, black_list, placeholder,分别代表要从中提取 URL 的字符串、提取级别,控制提取的详细程度、一个包含不应包含在最终 URL 中的扩展名或词的列表、用于替换 URL 中参数值的占位符,其中response就是requester的结果,其余均是通过主函数的命令行参数进行控制。具体的执行逻辑如下:

1.使用正则表达式提取 URL:

    • set(re.findall(...)) 用于获取所有匹配项的唯一集合(移除重复项)。

    • 函数使用正则表达式 r'.*?://.*?.*=[^$]' 来匹配含有查询参数的 URL。这个表达式匹配形式为 http://someurl.com?param=value 的字符串。


2.处理每个匹配的 URL:

    • 找到第一个和第二个等号(=)的位置,这些位置标识了参数值的开始。

    • 遍历每个提取的 URL。


3.应用黑名单过滤:

    • 如果 black_list 非空,将使用它来构建一个正则表达式,并检查每个 URL 是否包含黑名单中的任何词。如果 URL 包含黑名单中的词,则跳过该 URL。


4.参数替换:

    • 如果 level 设置为 'high',则对第二个参数值也进行替换。

    • 用 placeholder 替换 URL 中的参数值。例如,如果 placeholder 是 XXX,则 URL http://example.com?page=1 会变成 http://example.com?page=XXX。


5.返回结果:

    • 返回处理后的 URL 列表,确保唯一性(移除重复项)。

看完这两个函数之后基本就理解了它整个脚本工具的作用了,请求地址,然后通过正则提取出带有参数的URL,主函数中存在黑名单、等级和占位符等功能,除了黑名单外,实际可有可无。

值得一提的是主函数中提供了—subs参数提供了针对于子域名的查询,作者采用的方法是访问https://web.archive.org/,通过互联网档案馆的查询功能,查询*.domain,来获取子域名,属于是一个我没见过的思路了。

Nuclei Fuzzer 实战指南:自动化 Web 应用安全测试的优化与实践

那么分析到这里,一个想法就出来了,这不是不如我使用rad或者crawlergo进行爬虫获取URL,然后通过判断URL是否存在=?,提取出带有参数的URL,而后调用Nuclei进行扫描呢?

代码实现


目前我个人使用的较多联动脚本是https://github.com/timwhitez/crawlergo_x_XRAY,基本逻辑就是使用crawlergo进行爬虫,工具本身存在--push-to-proxy参数,此时将代理设置为xray被动监听的地址,就实现了爬虫+被动扫描的联动。

Nuclei本身不是一款被动扫描工具,但是这都不是问题,你不能被动扫描,我把爬虫结果保存下来然后主动扫描不就行了。

而后我又想到了另一个问题,把带有参数的URL使用fuzzing-templates进行fuzzing了,那不带有参数的呢?是不是我直接用常用的poc进行一个基础扫描呢?

总体设计


  1. 设置三个参数-u,-f,-m,代表单个url扫描,读取文件获取url进行扫描,m用来控制模式

  2. 功能函数包括爬虫,添加http头和三个模式的扫描函数,分为fuzz、common和all

  3. 创建parma目录存爬取到的参数URL,创建common目录存取不带有参数的URL,创建result存取扫描结果

流程图如下:

Nuclei Fuzzer 实战指南:自动化 Web 应用安全测试的优化与实践

简单、明了、没有难度。但是实际上我在写的过程中碰到了各种各样的问题,也做了各种各样的完善和考虑,我在这里分享出来,希望各位大佬指点。

爬虫函数


  1. 构造crawlergo命令并执行

  1. 获取爬虫结果

  1. 对结果进行进一步过滤并保存到文件中

针对爬虫的处理参考crawlergo自身项目的readme:

Nuclei Fuzzer 实战指南:自动化 Web 应用安全测试的优化与实践

可以看到项目本身提供了一个示例的python调用demo,我们按照这样直接操作即可。

即执行完命令后,获取req_list的结果,而后获取req_list[’url’]的结果,针对url进行判断,是否存在?=,而后按照结果进行保存,保存的文件的格式采用url_parma.txt和url_common.txt用来区分目标和爬取到的url是否带有参数,由于文件名中不能存在://等字符,均直接替换为下划线:

def crawl_url(url):  """爬虫函数"""  print(f"{bcolors.OK}----开始爬虫----{bcolors.ENDC}")    # 构造文件名  param_filename = './parma/' + url.replace('http://', '').replace('https://', '').replace('/', '_').replace(':', '_') + "_parma.txt"  common_filename = './common/' + url.replace('http://', '').replace('https://', '').replace('/', '_').replace(':', '_') + "_common.txt"    # 构造crawlergo命令并执行  cmd = [crawlergo_path, "-c", chrome_path,"-t", "5","-f","smart","--fuzz-path","--custom-headers",json.dumps(get_random_headers()), "--output-mode", "json" , url]  rsp = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)  output, error = rsp.communicate()  try:    # 处理爬虫结果    result = simplejson.loads(output.split("--[Mission Complete]--")[1])  except:    print(f"{bcolors.FAIL}爬虫失败{bcolors.ENDC}")    return None, None  req_list = result["req_list"]  print(f"{bcolors.OK}----爬虫结束,开始处理爬取结果----{bcolors.ENDC}")  if req_list:    for req in req_list:      url = req['url']      if '=' in url and '?' in url:        with open(param_filename, 'a') as f:          f.write(url + 'n')      else:        with open(common_filename, 'a') as f:          f.write(url + 'n')  return param_filename, common_filename

针对输出方面,由于爬虫的输出实在是太多了,输出到控制台我觉得很影响观感,所以直接没有输出,当然大家可以自己进行调整。

工具函数

在写的过程中发现了几点需求问题:

  1. 我的脚本中主要依靠命令行调用外部工具,同时还要指定nuclei的模板路径;

  1. 针对网站的爬虫,并不一定存在带有参数的URL;

  1. 从文件读取的URL可能存在纯domain或者IP:PORT类型,需要手动添加http://或者https://头

  1. 作为一名安服选手,所有的http发包请求都应该添加随机生成UA头。

  1. 针对爬取到的url和结果,需要先判断文件夹是否存在,不存在就创建

我将这些的解决统一都写在这一章节中。

路径问题


路径采用读取配置文件的方式解决,定义配置文件config.json,内容如下:

{    "httpx_path": "",    "crawlergo_path": "",    "nuclei_path": "",    "fuzzing_template_path": "",    "common_template_path": "",    "chrome_path": ""}

按照自己的工具路径位置进行填写,同时需要避免中文目录,配置文件中的httpx并没有使用,至于为什么我会在后面说明。

脚本中使用json.load的方式获取相关路径内容:

def get_config():  with open('config.json', 'r') as f:    config = json.load(f)  return config
config = get_config()httpx_path = config['httpx_path']crawlergo_path = config['crawlergo_path']nuclei_path = config['nuclei_path']fuzzing_template_path = config['fuzzing_template_path']common_template_path = config['common_template_path']chrome_path = config['chrome_path']

爬虫结果问题


爬虫扫描不一定有结果,在运行函数中添加判断,如果存在再去调用相应的扫描函数,不存在就输出提示

if os.path.exists(param_filename) and process_func in [process_url_fuzz, process_url_all]:    process_func(url, *needed_params)elif os.path.exists(common_filename) and process_func in [process_url_common, process_url_all]:    process_func(url, *needed_params)else:    print(f"{bcolors.FAIL}无法以{args.mode}处理{url},因为没有足够的数据{bcolors.ENDHTTC}")

HTTP协议头问题


读取传入的内容添加判断即可

def getAllrequest(target_url):  """添加http头"""  # url检测,传入的url是否为完整的域名,若仅为IP+port需要添加协议头  isHTTPS = True  # 将是否为https首先标志为True  if ("https" not in target_url) & ("http" not in target_url):    try:      url = "https://" + target_url      requests.packages.urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)  # 忽略ssl验证警告      requests.get(url=url, verify=False, timeout=3)  # 设置忽略ssl证书安全性警告,设置3s的延迟保证程序健壮性    except Exception as e:      isHTTPS = False    finally:      if isHTTPS:        target_url = "https://" + target_url      else:        target_url = "http://" + target_url  return target_url

随机UA头


采用fake_useragent获取相应的UA头

from fake_useragent import UserAgent
ua = UserAgent()

def get_random_headers(): headers = {'User-Agent': ua.random}
return headers

保存路径


创建判断函数,而后传入进行判断即可

def ensure_directories_exist(directories):  """Ensure that the given directories exist, create them if not."""  for directory in directories:    if not os.path.exists(directory):      os.makedirs(directory)
# 主函数中# Check if directories exist, create if not ensure_directories_exist(['./parma/', './common/', './result/'])

扫描函数


针对不同的mode模式调用不同的扫描函数,具体内容应该基本一致,唯一需要注意的就是保存nuclei扫描结果的报告命名方式,这里我同样采用url_common.json或者url_fuzz.json的方式进行保存

def process_url_common(url, common_filename):  """common模式扫描"""  # 定义输出文件名  output_common_filename = url.replace('http://', '').replace('https://', '').replace('/', '_').replace(':', '_') + "_common.json"
print(f'{bcolors.OK}----开始扫描{common_filename}----{bcolors.ENDC}') cmd_httpx_nuclei = f"{nuclei_path} -t {common_template_path} -l {common_filename} -o ./result/{output_common_filename}" print(f"{bcolors.OKBLUE}{cmd_httpx_nuclei}{bcolors.ENDC}") try: rsp = subprocess.Popen(cmd_httpx_nuclei, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = rsp.communicate() print(stdout) print(stderr) print(f"{bcolors.OK}----{url},common扫描结束----{bcolors.ENDC}") except FileNotFoundError as e: print(f"{bcolors.FAIL}Error: {e}{bcolors.ENDC}")


nuclei的扫描结果我选择了输出,不然这个不输出内容,感觉脚本一直就是处于卡死的状态。

同时在这里我也回答一下为什么没有httpx的问题,按照我最开始的设想,爬取到的URL,我先使用httpx进行验活处理,提取出200 301 403 401这些返回码,但是很不幸的是subprocess.Popen,在windows环境下,对于管道符的处理存在问题,下面是我本来想执行的命令:

type url_parma.txt | httpx -silent -mc 200,301,302,403|nuclei ...

此时发现第一个问题,我的url_parma.txt的格式是./parma/url_parma.txt,提示语法命令不正确:

Nuclei Fuzzer 实战指南:自动化 Web 应用安全测试的优化与实践

这都是小事,我把/换成呗

Nuclei Fuzzer 实战指南:自动化 Web 应用安全测试的优化与实践

但是在后续调试过程中,我发现了,管道符没有起作用,它不给后面的命令了,我如果直接使用windows的cmd直接运行这个命令是没问题的,但是使用subprocess.Popen,进行一次输出调试,直接输出内容了,并没有往后面执行,因此我选择直接放弃,httpx的任务就交给域名收集之后大家手动完成吧。

我的all模式扫描函数,选择的方式是直接调用fuzz和common两个扫描函数,因此在判断爬虫结果的处理上还是存在问题,我索性在这个函数中重新进行了一次判断:

def process_url_all(url, param_filename, common_filename):  """all模式扫描"""  if os.path.exists(param_filename):    process_url_fuzz(url, param_filename)  else:    print(f"{bcolors.FAIL}{param_filename} does not exist{bcolors.ENDC}")
if os.path.exists(common_filename): process_url_common(url, common_filename) else: print(f"{bcolors.FAIL}{common_filename} does not exist{bcolors.ENDC}")

主函数


主函数主要存在一个问题——不优雅,为什么不优雅?因为我的判断实在太多了,每个扫描函数需要的参数不一样,首先判断需要哪些参数从爬虫的返回里面去取,还要判断文件内容是否存在,如果爬虫没有结果就会导致后面的内容报错。

那么这一部分主要依靠github copilot的建议完成,定义一个字典:

process_funcs = {    'fuzz': (process_url_fuzz, [0]),    'common': (process_url_common, [1]),    'all': (process_url_all, [0, 1])  }process_func, param_indices = process_funcs[args.mode]needed_params = [all_params[i] for i in param_indices]    if os.path.exists(param_filename) and process_func in [process_url_fuzz, process_url_all]:      process_func(url, *needed_params)    elif os.path.exists(common_filename) and process_func in [process_url_common, process_url_all]:      process_func(url, *needed_params)    else:      print(f"{bcolors.FAIL}无法以{args.mode}处理{url},因为没有足够的数据{bcolors.ENDC}")

其余部分就不进行展示,其实基本也已经全部展示完了,大家还可以自己添加多任务功能什么的。

小彩蛋


想着写都写了,直接给输出提示字符写个颜色,一开始是这么写的:

class bcolors:  FAIL = '33[91m'  OK = '33[92m'  INFO = '33[94m'  ENDC = '33[0m'
print(f"{bcolors.OK}----开始爬虫----{bcolors.ENDC}")

发现用这样的方式windows的cmd显示不出来

Nuclei Fuzzer 实战指南:自动化 Web 应用安全测试的优化与实践

那么我就继续问AI吧(没有AI我什么都不是啊),解决方案是使用python的colorama库

from colorama import init, Fore, Back, Style
class bcolors: HEADER = Fore.CYAN OKBLUE = Fore.BLUE OK = Fore.GREEN WARNING = Fore.YELLOW FAIL = Fore.RED ENDC = Fore.RESET BOLD = Style.BRIGHT UNDERLINE = Style.DIM
print(f"{bcolors.OK}----开始爬虫----{bcolors.ENDC}")

Nuclei Fuzzer 实战指南:自动化 Web 应用安全测试的优化与实践

总结


在这篇文章中,我从一个项目的分析作为开始,尝试进行自己的实现与改造。项目是非常简单的项目,思路也是经常碰到的思路,分享自己的问题,也是为了让大家少踩坑,同时存在不足的地方也欢迎大家补充。


继续一个彩蛋


当我想着整理自己的common模板时,我突然想到了攻防中最重要的fastjson和shiro反序列化,当然我也在官方的模板中发现了有大佬提交了。

但是我在调试shiro反序列化的模板时发现了问题,就是扫不出来啊,我首先用fofa提取了1W个ruoyi,它一个没扫出来,然后我受不了了,开了一个vulhub,它竟然也没扫出来,那这样我就觉得不对了。

Nuclei Fuzzer 实战指南:自动化 Web 应用安全测试的优化与实践

模板的内容如上,采用的payload就是shiro_encrypted_keys.txt这个文档中的内容,那么我们看看文档中的内容:

Nuclei Fuzzer 实战指南:自动化 Web 应用安全测试的优化与实践

payload字典每一行实际上是key:payload的内容,我们知道shiro的加密方式是aes然后base64,我们随便取一行用相同的方式解密就能得到加密的原材料,就是原始的payload,然后用你自己的key加密,这样就能扩展这个字典了(都不能用,先想着怎么加字典了)。然后我们发现,模板中是直接读取这里面的内容,也就是说我们发送的payload是rememberMe=key:payload,这明显不对吧,直接删除key:,重新扫一下:

Nuclei Fuzzer 实战指南:自动化 Web 应用安全测试的优化与实践

好了,扫出来了,但是实际上如果真正要用的话,是不是只用shiro-detect.yaml,判断是否存在shiro框架,然后用其他自动化工具再扫描会不会更好呢?



点击下方名片进入公众号,欢迎关注!


往期推荐

栅栏之下的策略:免杀马中的密码艺术

技术幻影-揭开fscan免杀的面纱

红蓝对抗篇-记一次SQL注入上线CS

certutil之巧:绕过防御的艺术

峰回路转之mimikatz通杀杀软

PowerShell无文件落地绕过杀软上线CS

绕过360核晶防护创建用户+计划任务


原文始发于微信公众号(随风安全):Nuclei Fuzzer 实战指南:自动化 Web 应用安全测试的优化与实践

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年12月12日02:45:11
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Nuclei Fuzzer 实战指南:自动化 Web 应用安全测试的优化与实践https://cn-sec.com/archives/2272963.html

发表评论

匿名网友 填写信息