识别fscan扫描结果 生成excel表格

一、使用

将fscan扫描后的结果文件进行信息识别、汇总,输出生成excel表格文档。

用法举例:
简单使用:python fscan_result_merge_toExcel.py -i ./result.txt -o ./内网资产信息.xlsx
屏蔽特定IP:python fscan_result_merge_toExcel.py -i ./result.txt -n “10.217.35.158,10.23.32.0/24”
只整合特定IP:python fscan_result_merge_toExcel.py -i ./result.txt -a “192.168.0.0/16”

本脚本用于整合fscan探测结果,生成excel表格文档。(仅作合法用途)

optional arguments:
-h, --help show this help message and exit
-i filename, --input filename
fscan生成的探测结果 文本文件(.txt)
-o filename, --output filename
整合信息后所生成的表格文档 保存路径(含文件名)
-n IP地址(段), --neglect IP地址(段)
传入需要排除的IP地址(段),如: -n 192.168.1.1,192.168.23.0/24
-nf filename, --neglect_file filename
包含需要排除的IP地址(段)的可读文件,其中每个IP地址(段)独占一行
-a IP地址, --allow IP地址
限制程序只处理 该参数所传入的IP地址(段)相关信息。

二、代码

#! /usr/bin/env python3
# -*- coding: UTF-8 -*-
# File: fscan_result_merge_toExcel.py
# Author: learning
# History: 2023/7/21
"""fscan结果整合成excel表格"""
import argparse
import os
import re
import ipaddress
import pandas as pd
from colorama import init, Fore, Style
init(autoreset=True)


def color_text(text, color):
    """
    终端字体颜色调整

    :param text:
    :param color:
    :return:
    """
    return f"{color}{text}{Style.RESET_ALL}"


def readfile(filename: str) -> list:
    """
    读取(文本)文件

    """
    with open(filename, 'r', encoding='utf-8') as f:
        lines = [line.strip(' ') for line in f
                 if 'Unsolicited response received' not in line
                 and '[->]' not in line]
    return lines


def is_valid_ipv4(ip):
    """
    判断是否为合法的IP地址(段)

    :param ip:
    :return:
    """
    pattern = re.compile(r'^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}'
                         r'(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(/\d{1,2})?$')
    match = pattern.match(ip.strip())
    return match is not None


def get_black_ip_range(ip_string='', ip_file=''):
    ip_list = []
    if len(ip_string) > 0:
        ip_string_list = ip_string.split(',')
        for ip in ip_string_list:
            ip = ip.strip()
            if is_valid_ipv4(ip):
                ip_list.append(ip)
            else:
                raise ValueError('传入的(用于屏蔽的)IP地址(段)格式存在问题。')

    if ip_list:
        return ip_list

    if ip_file == '':
        return []

    if not os.path.isfile(ip_file):
        raise FileExistsError('传入的用于屏蔽相关IP地址(段)的文件路径不存在!')
    with open(ip_file, 'r') as f:
        for line in f:
            line = line.strip()
            if is_valid_ipv4(line):
                ip_list.append(line)
            else:
                raise ValueError('传入的(用于屏蔽的)IP地址(段)格式存在问题。')
    return ip_list


def get_black_ip_network(ip_string, ip_file):
    ip_list = get_black_ip_range(ip_string, ip_file)
    black_networks = [ipaddress.ip_network(ip) for ip in ip_list]
    return black_networks


def check_ip_in_black_network(ip, black_networks):
    ip_obj = ipaddress.ip_address(ip)
    for n in black_networks:
        if ip_obj in n:
            return True
    return False


def get_white_ip_range(ip_string):
    ip_list = []
    if len(ip_string) > 0:
        ip_string_list = ip_string.split(',')
        for ip in ip_string_list:
            ip = ip.strip()
            if is_valid_ipv4(ip):
                ip_list.append(ip)
            else:
                raise ValueError('传入的(用于屏蔽的)IP地址(段)格式存在问题。')

    if ip_list:
        return ip_list

    return ip_list


def check_ip_in_white_network(ip, black_networks):
    ip_obj = ipaddress.ip_address(ip)
    for n in black_networks:
        if ip_obj in n:
            return True
    return False


def get_white_ip_network(ip_string):
    ip_list = get_white_ip_range(ip_string)
    white_networks = [ipaddress.ip_network(ip) for ip in ip_list]
    return white_networks


def get_ip_ports(content) -> dict:
    """
    获取:url, code, length, title

    :param content:
    :return: dict{ ip: [port1, port2, ...] }
    """
    ip_port_dict = {}
    # 使用正则表达式匹配IP和端口信息
    for line in content:
        match = re.search(r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(\d+)', line)
        if match:
            ip, port = match.group(1), match.group(2)
            if ip not in ip_port_dict:
                ip_port_dict[ip] = []
            if port in ip_port_dict[ip]:
                continue

            ip_port_dict[ip].append(port)

    return ip_port_dict


def get_url_info(content) -> dict:
    """
    获取:url, code, length, title, finger

    :param content:
    :return: dict{ url: [title, code, length, finger] }
    """
    url_info_dict = {}
    text = '[INFO] 对可能存在【多次访问但数据不一致】的情况' \
           ' (title, 响应码, 数据长度, 指纹),将会在下方打印输出,以供比较...'
    message = color_text(text, Fore.YELLOW)
    print(message)
    for line in content:
        match = re.search(
            r'WebTitle: (http.*?)\s+code:(\d+)\s+len:(.*?)\s+title:(.*?)\n',
            line)
        if match:
            url, code, length, title = \
                match.group(1), match.group(2), \
                match.group(3), match.group(4)
            finger = ''
            if url not in url_info_dict:
                url_info_dict[url] = [title, code, length, finger]
                continue
            if [title, code, length, finger] == url_info_dict[url]:
                continue

            # 通过比较,去除title出现的异常空格
            count1 = url_info_dict[url][0].count(' ')
            count2 = title.count(' ')
            if count1 > count2:
                url_info_dict[url][0] = title
            if [code, length, finger] == url_info_dict[url][1:] \
                    and abs(len(title) - len(url_info_dict[url][0])) < 2:
                continue
            if [title, code, length] == url_info_dict[url][:-1] \
                    and len(finger) <= len(url_info_dict[url][-1]):
                continue

            print(url, url_info_dict[url], '|', [title, code, length, finger])
        else:
            match = re.search(r'InfoScan:\s*(http.*?)\s+\[(.*?)]', line)
            if match:
                url, finger = match.group(1), match.group(2)
                if url in url_info_dict:
                    url_info_dict[url][-1] = finger

    return url_info_dict


def get_ip_exp(content):
    """
    获取:IP、对应的主机系统漏洞

    :param content:
    :return: list[[ip, exp]]
    """
    data = []
    # 使用正则表达式匹配IP和端口信息
    for line in content:
        match = re.search(
            r'\[\+]\s*(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s+(.*)',
            line.strip('\n '))
        if match:
            ip, exp = match.group(1), match.group(2)
            exp = re.sub(r'\s+', ' ', exp)

            ip_exp_dict = {'IP': ip, '主机漏洞': exp}

            if ip_exp_dict in data:
                continue

            data.append(ip_exp_dict)
    data = sorted(data, key=lambda _: ipaddress.ip_address(_['IP']))
    data = sorted(data, key=lambda _: _['主机漏洞'])

    return data


def get_url_poc(content):
    """
    获取:url、对应poc

    """
    data = []
    # 使用正则表达式匹配IP和端口信息
    for line in content:
        match = re.search(r'(http.*?) (poc-.*)', line)
        if match:
            url, poc = match.group(1), match.group(2)
            url_poc_dict = {'URL': url, 'POC': poc.strip()}

            if url_poc_dict in data:
                continue

            data.append(url_poc_dict)

    return data


def format_url_dict(url: str, url_info: list):
    if url == '':
        return {
            'url': '',
            'title': '',
            'code': '',
            'length': '',
            'finger': ''
        }
    title, code, length, finger = url_info
    url_dict = {
        'url': url,
        'title': title,
        'code': int(code),
        'length': int(length),
        'finger': finger
    }
    return url_dict


def search_url(ip, port, url_info_dict):
    if str(port) == '80':
        pattern = re.compile(fr"^http://{ip}[\D]*$")
        for url in url_info_dict.keys():
            if pattern.match(url):
                return format_url_dict(url, url_info_dict[url])

    elif str(port) == '443':
        pattern = re.compile(fr"^https://{ip}[\D]*$")
        for url in url_info_dict.keys():
            if pattern.match(url):
                return format_url_dict(url, url_info_dict[url])

    pattern = re.compile(fr"^https?://{ip}:{port}[\D]*$")
    for url in url_info_dict.keys():
        if pattern.match(url):
            return format_url_dict(url, url_info_dict[url])
    return format_url_dict('', [])


def get_weak_auth(content):
    """
    获取:弱口令、无认证等信息

    (ip, port, service, username, password, more_info)

    """
    data = []
    # 使用正则表达式匹配IP和端口信息
    for line in content:
        if 'handshake failed' in line:
            continue
        info_dict = {'IP': '', '端口': '', '服务': '',
                     '用户名': '', '密码': '', '附加信息': ''}
        # mysql, mssql...
        match = re.search(
            r'(\S+):(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(\d+):(\S+)\s+(\S+)',
            line)
        if match:
            service, ip, port, username, password = \
                match.group(1), match.group(2), match.group(3), \
                match.group(4), match.group(5)
            info_dict['IP'] = ip
            info_dict['端口'] = int(port)
            info_dict['服务'] = service
            info_dict['用户名'] = username
            info_dict['密码'] = password
            info_dict['附加信息'] = ''
            data.append(info_dict)
            continue

        # ftp...
        match = re.search(
            r'(\S+)://(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(\d+):(\S+)(.*)',
            line
        )
        if match:
            service, ip, port, username, password = \
                match.group(1), match.group(2), match.group(3), \
                match.group(4), match.group(5)

            if len(password) >= 1:
                password = password.strip()

            info_dict['IP'] = ip
            info_dict['端口'] = int(port)
            info_dict['服务'] = service
            info_dict['用户名'] = username
            info_dict['密码'] = password
            info_dict['附加信息'] = ''
            data.append(info_dict)
            continue

        # redis...
        match = re.search(
            r'(\S+)[:\s](\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(\d+)\s+(.*)',
            line
        )
        if match:
            service, ip, port, more_info = \
                match.group(1), match.group(2), \
                match.group(3), match.group(4)

            info_dict['IP'] = ip
            info_dict['端口'] = int(port)
            info_dict['服务'] = service
            info_dict['用户名'] = ''
            info_dict['密码'] = ''
            info_dict['附加信息'] = more_info
            data.append(info_dict)
            continue
    data = [i for index, i in enumerate(data) if i not in data[:index]]
    data = sorted(data, key=lambda _: ipaddress.ip_address(_['IP']))
    data = sorted(data, key=lambda _: int(_['端口']))

    return data


def clean_content(content, black_networks, white_networks):
    if len(black_networks) == 0 and len(white_networks) == 0:
        return content
    new_content = []

    if len(white_networks) == 0:
        for line in content:
            if '[*] LiveTop ' in line:
                continue
            ip = re.search(r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\D', line)
            if ip:
                ip = ip.group(1)
                if check_ip_in_black_network(ip, black_networks):
                    continue
            new_content.append(line)
        return new_content
    else:
        for line in content:
            if '[*] LiveTop ' in line:
                continue
            ip = re.search(r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\D', line)
            if ip:
                ip = ip.group(1)
                if not check_ip_in_white_network(ip, white_networks):
                    continue
                if check_ip_in_black_network(ip, black_networks):
                    continue
            new_content.append(line)
        return new_content


def get_domain_os(content):
    """
    获取:各域内大体的关系

    """
    data = []
    # 使用正则表达式匹配IP和端口信息
    for line in content:
        match = re.search(r'NetBios: (.*?)\s+(.*?\S)\s+(.*)', line)
        if match:
            ip, domain, OS = match.group(1), match.group(2), match.group(3)
            if len(OS) > 1 and not OS[0].isalnum():
                OS = OS[1:]
            info_dict = {'IP': ip, '域名': domain, '操作系统': OS.strip()}
            data.append(info_dict)

    data = sorted(data, key=lambda _: ipaddress.ip_address(_['IP']))

    return data


def get_collated_info(input_filename='./result.txt',
                      output_filename='内网资产探测信息表.xlsx',
                      black_networks=(),
                      white_networks=()):
    """
    获取各类信息,并合并成一个表格文档。
    IP  端口  协议  服务  URL  Title  指纹  返回码
    [IP, port, protocol(默认tcp), server(无法识别:""), url, title, finger, code]

    :return: list[[IP, port, protocol, server, url, title, finger, code]]
    """
    content = readfile(input_filename)
    content = list(dict.fromkeys(content))
    content = clean_content(content, black_networks, white_networks)

    # 常规资产信息
    ip_ports = get_ip_ports(content)
    url_info_dict = get_url_info(content)
    domain_os_list = get_domain_os(content)

    # 可能性高的脆弱资产
    weak_auth_list = get_weak_auth(content)
    ip_exp_list = get_ip_exp(content)
    url_poc_list = get_url_poc(content)

    data = []
    ip_data = []
    # IP排序(小 --> 大)
    ip_ports = sorted(ip_ports.items(),
                      key=lambda _: ipaddress.ip_address(_[0]))

    for ip, ports in ip_ports:
        # 端口排序(小 --> 大)
        ports.sort(key=int)
        ip_data.append({'IP': ip, '端口': ' '.join(ports)})
        for p in ports:
            d_row = {'IP': '', '端口': '', '协议': 'tcp', '服务': '',
                     'URL': '', 'Title': '', '指纹': '', '返回码': ''}
            url_info = search_url(ip, p, url_info_dict)
            d_row['IP'] = ip
            d_row['端口'] = int(p)
            d_row['协议'] = 'tcp'
            d_row['URL'] = url_info['url']
            d_row['Title'] = url_info['title']
            d_row['指纹'] = url_info['finger']
            d_row['返回码'] = url_info['code']
            data.append(d_row)

    data = sorted(data, key=lambda _: int(_['端口']))
    data = sorted(data, key=lambda _: ipaddress.ip_address(_['IP']))
    df_web = pd.DataFrame(data)
    df_domain = pd.DataFrame(domain_os_list)
    df_ip = pd.DataFrame(ip_data)

    df_weak = pd.DataFrame(weak_auth_list)
    df_poc = pd.DataFrame(url_poc_list)
    df_exp = pd.DataFrame(ip_exp_list)

    name, suffix = os.path.splitext(output_filename)
    weak_filename = name + '_脆弱资产' + suffix
    with pd.ExcelWriter(output_filename) as writer:
        df_web.to_excel(writer, sheet_name='资产信息', index=False)
        df_domain.to_excel(writer, sheet_name='域信息', index=False)
        df_ip.to_excel(writer, sheet_name='存活IP', index=False)
    with pd.ExcelWriter(name + '_脆弱资产' + suffix) as writer:
        df_weak.to_excel(writer, sheet_name='脆弱认证', index=False)
        df_poc.to_excel(writer, sheet_name='POC', index=False)
        df_exp.to_excel(writer, sheet_name='主机漏洞', index=False)
    text = f'[INFO] 已导出:{os.path.realpath(output_filename)}'
    message = color_text(text, Fore.GREEN)
    print(message)
    text = f'[INFO] 已导出:{os.path.realpath(weak_filename)}'
    message = color_text(text, Fore.GREEN)
    print(message + '\n')


def main():
    parser = argparse.ArgumentParser()
    parser.description = "本脚本用于整合fscan探测结果,生成excel表格文档。" \
                         "(仅作合法用途)\t\t--DJH"
    parser.usage = "\n用法举例:\n" \
                   "\t简单使用:python fscan_result_merge_toExcel.py " \
                   "-i ./result.txt -o ./内网资产信息.xlsx\n" \
                   "\t屏蔽特定IP:python fscan_result_merge_toExcel.py " \
                   "-i ./result.txt " \
                   "-n \"10.217.35.158,10.23.32.0/24\"\n" \
                   "\t只整合特定IP:python fscan_result_merge_toExcel.py " \
                   "-i ./result.txt -a \"192.168.0.0/16\""
    parser.add_argument('-i', '--input', default='./result.txt',
                        metavar="filename",
                        help='fscan生成的探测结果 文本文件(.txt)')
    parser.add_argument('-o', '--output', default='./内网资产信息.xlsx',
                        metavar="filename",
                        help='整合信息后所生成的表格文档 保存路径(含文件名)')
    parser.add_argument('-n', '--neglect', default='',
                        metavar='IP地址(段)',
                        help='传入需要排除的IP地址(段),'
                             '如: -n 192.168.1.1,192.168.23.0/24')
    parser.add_argument('-nf', '--neglect_file', default='',
                        metavar='filename',
                        help='包含需要排除的IP地址(段)的可读文件,'
                             '其中每个IP地址(段)独占一行')
    parser.add_argument('-a', '--allow', default='',
                        metavar='IP地址',
                        help='限制程序只处理 该参数所传入的IP地址(段)相关信息。')
    args = parser.parse_args()
    if not os.path.isfile(args.input):
        print("[ERROR] 输入的fscan结果文件路径不存在,请检查。")
        return
    if ' ' in args.neglect:
        print("[ERROR] 输入的(用于屏蔽的)IP地址(段)参数格式不合法:不能含有空格。")
    white_networks = get_white_ip_network(
        ip_string=args.allow
    )
    black_networks = get_black_ip_network(
        ip_string=args.neglect, ip_file=args.neglect_file
    )

    text = "[INFO] 程序开始运行..."
    message = color_text(text, Fore.GREEN)
    print(message)
    get_collated_info(input_filename=args.input,
                      output_filename=args.output,
                      black_networks=black_networks,
                      white_networks=white_networks)


if __name__ == '__main__':
    main()




一、使用
将fscan扫描后的结果文件进行信息识别、汇总,输出生成excel表格文档。


用法举例:
        简单使用:python fscan_result_merge_toExcel.py -i ./result.txt -o ./内网资产信息.xlsx
        屏蔽特定IP:python fscan_result_merge_toExcel.py -i ./result.txt -n "10.217.35.158,10.23.32.0/24"
        只整合特定IP:python fscan_result_merge_toExcel.py -i ./result.txt -a "192.168.0.0/16"

本脚本用于整合fscan探测结果,生成excel表格文档。(仅作合法用途)

optional arguments:
  -h, --help            show this help message and exit
  -i filename, --input filename
                        fscan生成的探测结果 文本文件(.txt)
  -o filename, --output filename
                        整合信息后所生成的表格文档 保存路径(含文件名)
  -n IP地址(), --neglect IP地址()
                        传入需要排除的IP地址(段),如: -n 192.168.1.1,192.168.23.0/24
  -nf filename, --neglect_file filename
                        包含需要排除的IP地址(段)的可读文件,其中每个IP地址(段)独占一行
  -a IP地址, --allow IP地址
                        限制程序只处理 该参数所传入的IP地址(段)相关信息。



二、代码
#! /usr/bin/env python3
# -*- coding: UTF-8 -*-
# File: fscan_result_merge_toExcel.py
# Author: DJH
# History: 2023/7/21
"""fscan结果整合成excel表格"""
import argparse
import os
import re
import ipaddress
import pandas as pd
from colorama import init, Fore, Style
init(autoreset=True)


def color_text(text, color):
    """
    终端字体颜色调整

    :param text:
    :param color:
    :return:
    """
    return f"{color}{text}{Style.RESET_ALL}"


def readfile(filename: str) -> list:
    """
    读取(文本)文件

    """
    with open(filename, 'r', encoding='utf-8') as f:
        lines = [line.strip(' ') for line in f
                 if 'Unsolicited response received' not in line
                 and '[->]' not in line]
    return lines


def is_valid_ipv4(ip):
    """
    判断是否为合法的IP地址(段)

    :param ip:
    :return:
    """
    pattern = re.compile(r'^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}'
                         r'(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(/\d{1,2})?$')
    match = pattern.match(ip.strip())
    return match is not None


def get_black_ip_range(ip_string='', ip_file=''):
    ip_list = []
    if len(ip_string) > 0:
        ip_string_list = ip_string.split(',')
        for ip in ip_string_list:
            ip = ip.strip()
            if is_valid_ipv4(ip):
                ip_list.append(ip)
            else:
                raise ValueError('传入的(用于屏蔽的)IP地址(段)格式存在问题。')

    if ip_list:
        return ip_list

    if ip_file == '':
        return []

    if not os.path.isfile(ip_file):
        raise FileExistsError('传入的用于屏蔽相关IP地址(段)的文件路径不存在!')
    with open(ip_file, 'r') as f:
        for line in f:
            line = line.strip()
            if is_valid_ipv4(line):
                ip_list.append(line)
            else:
                raise ValueError('传入的(用于屏蔽的)IP地址(段)格式存在问题。')
    return ip_list


def get_black_ip_network(ip_string, ip_file):
    ip_list = get_black_ip_range(ip_string, ip_file)
    black_networks = [ipaddress.ip_network(ip) for ip in ip_list]
    return black_networks


def check_ip_in_black_network(ip, black_networks):
    ip_obj = ipaddress.ip_address(ip)
    for n in black_networks:
        if ip_obj in n:
            return True
    return False


def get_white_ip_range(ip_string):
    ip_list = []
    if len(ip_string) > 0:
        ip_string_list = ip_string.split(',')
        for ip in ip_string_list:
            ip = ip.strip()
            if is_valid_ipv4(ip):
                ip_list.append(ip)
            else:
                raise ValueError('传入的(用于屏蔽的)IP地址(段)格式存在问题。')

    if ip_list:
        return ip_list

    return ip_list


def check_ip_in_white_network(ip, black_networks):
    ip_obj = ipaddress.ip_address(ip)
    for n in black_networks:
        if ip_obj in n:
            return True
    return False


def get_white_ip_network(ip_string):
    ip_list = get_white_ip_range(ip_string)
    white_networks = [ipaddress.ip_network(ip) for ip in ip_list]
    return white_networks


def get_ip_ports(content) -> dict:
    """
    获取:url, code, length, title

    :param content:
    :return: dict{ ip: [port1, port2, ...] }
    """
    ip_port_dict = {}
    # 使用正则表达式匹配IP和端口信息
    for line in content:
        match = re.search(r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(\d+)', line)
        if match:
            ip, port = match.group(1), match.group(2)
            if ip not in ip_port_dict:
                ip_port_dict[ip] = []
            if port in ip_port_dict[ip]:
                continue

            ip_port_dict[ip].append(port)

    return ip_port_dict


def get_url_info(content) -> dict:
    """
    获取:url, code, length, title, finger

    :param content:
    :return: dict{ url: [title, code, length, finger] }
    """
    url_info_dict = {}
    text = '[INFO] 对可能存在【多次访问但数据不一致】的情况' \
           ' (title, 响应码, 数据长度, 指纹),将会在下方打印输出,以供比较...'
    message = color_text(text, Fore.YELLOW)
    print(message)
    for line in content:
        match = re.search(
            r'WebTitle: (http.*?)\s+code:(\d+)\s+len:(.*?)\s+title:(.*?)\n',
            line)
        if match:
            url, code, length, title = \
                match.group(1), match.group(2), \
                match.group(3), match.group(4)
            finger = ''
            if url not in url_info_dict:
                url_info_dict[url] = [title, code, length, finger]
                continue
            if [title, code, length, finger] == url_info_dict[url]:
                continue

            # 通过比较,去除title出现的异常空格
            count1 = url_info_dict[url][0].count(' ')
            count2 = title.count(' ')
            if count1 > count2:
                url_info_dict[url][0] = title
            if [code, length, finger] == url_info_dict[url][1:] \
                    and abs(len(title) - len(url_info_dict[url][0])) < 2:
                continue
            if [title, code, length] == url_info_dict[url][:-1] \
                    and len(finger) <= len(url_info_dict[url][-1]):
                continue

            print(url, url_info_dict[url], '|', [title, code, length, finger])
        else:
            match = re.search(r'InfoScan:\s*(http.*?)\s+\[(.*?)]', line)
            if match:
                url, finger = match.group(1), match.group(2)
                if url in url_info_dict:
                    url_info_dict[url][-1] = finger

    return url_info_dict


def get_ip_exp(content):
    """
    获取:IP、对应的主机系统漏洞

    :param content:
    :return: list[[ip, exp]]
    """
    data = []
    # 使用正则表达式匹配IP和端口信息
    for line in content:
        match = re.search(
            r'\[\+]\s*(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s+(.*)',
            line.strip('\n '))
        if match:
            ip, exp = match.group(1), match.group(2)
            exp = re.sub(r'\s+', ' ', exp)

            ip_exp_dict = {'IP': ip, '主机漏洞': exp}

            if ip_exp_dict in data:
                continue

            data.append(ip_exp_dict)
    data = sorted(data, key=lambda _: ipaddress.ip_address(_['IP']))
    data = sorted(data, key=lambda _: _['主机漏洞'])

    return data


def get_url_poc(content):
    """
    获取:url、对应poc

    """
    data = []
    # 使用正则表达式匹配IP和端口信息
    for line in content:
        match = re.search(r'(http.*?) (poc-.*)', line)
        if match:
            url, poc = match.group(1), match.group(2)
            url_poc_dict = {'URL': url, 'POC': poc.strip()}

            if url_poc_dict in data:
                continue

            data.append(url_poc_dict)

    return data


def format_url_dict(url: str, url_info: list):
    if url == '':
        return {
            'url': '',
            'title': '',
            'code': '',
            'length': '',
            'finger': ''
        }
    title, code, length, finger = url_info
    url_dict = {
        'url': url,
        'title': title,
        'code': int(code),
        'length': int(length),
        'finger': finger
    }
    return url_dict


def search_url(ip, port, url_info_dict):
    if str(port) == '80':
        pattern = re.compile(fr"^http://{ip}[\D]*$")
        for url in url_info_dict.keys():
            if pattern.match(url):
                return format_url_dict(url, url_info_dict[url])

    elif str(port) == '443':
        pattern = re.compile(fr"^https://{ip}[\D]*$")
        for url in url_info_dict.keys():
            if pattern.match(url):
                return format_url_dict(url, url_info_dict[url])

    pattern = re.compile(fr"^https?://{ip}:{port}[\D]*$")
    for url in url_info_dict.keys():
        if pattern.match(url):
            return format_url_dict(url, url_info_dict[url])
    return format_url_dict('', [])


def get_weak_auth(content):
    """
    获取:弱口令、无认证等信息

    (ip, port, service, username, password, more_info)

    """
    data = []
    # 使用正则表达式匹配IP和端口信息
    for line in content:
        if 'handshake failed' in line:
            continue
        info_dict = {'IP': '', '端口': '', '服务': '',
                     '用户名': '', '密码': '', '附加信息': ''}
        # mysql, mssql...
        match = re.search(
            r'(\S+):(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(\d+):(\S+)\s+(\S+)',
            line)
        if match:
            service, ip, port, username, password = \
                match.group(1), match.group(2), match.group(3), \
                match.group(4), match.group(5)
            info_dict['IP'] = ip
            info_dict['端口'] = int(port)
            info_dict['服务'] = service
            info_dict['用户名'] = username
            info_dict['密码'] = password
            info_dict['附加信息'] = ''
            data.append(info_dict)
            continue

        # ftp...
        match = re.search(
            r'(\S+)://(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(\d+):(\S+)(.*)',
            line
        )
        if match:
            service, ip, port, username, password = \
                match.group(1), match.group(2), match.group(3), \
                match.group(4), match.group(5)

            if len(password) >= 1:
                password = password.strip()

            info_dict['IP'] = ip
            info_dict['端口'] = int(port)
            info_dict['服务'] = service
            info_dict['用户名'] = username
            info_dict['密码'] = password
            info_dict['附加信息'] = ''
            data.append(info_dict)
            continue

        # redis...
        match = re.search(
            r'(\S+)[:\s](\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(\d+)\s+(.*)',
            line
        )
        if match:
            service, ip, port, more_info = \
                match.group(1), match.group(2), \
                match.group(3), match.group(4)

            info_dict['IP'] = ip
            info_dict['端口'] = int(port)
            info_dict['服务'] = service
            info_dict['用户名'] = ''
            info_dict['密码'] = ''
            info_dict['附加信息'] = more_info
            data.append(info_dict)
            continue
    data = [i for index, i in enumerate(data) if i not in data[:index]]
    data = sorted(data, key=lambda _: ipaddress.ip_address(_['IP']))
    data = sorted(data, key=lambda _: int(_['端口']))

    return data


def clean_content(content, black_networks, white_networks):
    if len(black_networks) == 0 and len(white_networks) == 0:
        return content
    new_content = []

    if len(white_networks) == 0:
        for line in content:
            if '[*] LiveTop ' in line:
                continue
            ip = re.search(r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\D', line)
            if ip:
                ip = ip.group(1)
                if check_ip_in_black_network(ip, black_networks):
                    continue
            new_content.append(line)
        return new_content
    else:
        for line in content:
            if '[*] LiveTop ' in line:
                continue
            ip = re.search(r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\D', line)
            if ip:
                ip = ip.group(1)
                if not check_ip_in_white_network(ip, white_networks):
                    continue
                if check_ip_in_black_network(ip, black_networks):
                    continue
            new_content.append(line)
        return new_content


def get_domain_os(content):
    """
    获取:各域内大体的关系

    """
    data = []
    # 使用正则表达式匹配IP和端口信息
    for line in content:
        match = re.search(r'NetBios: (.*?)\s+(.*?\S)\s+(.*)', line)
        if match:
            ip, domain, OS = match.group(1), match.group(2), match.group(3)
            if len(OS) > 1 and not OS[0].isalnum():
                OS = OS[1:]
            info_dict = {'IP': ip, '域名': domain, '操作系统': OS.strip()}
            data.append(info_dict)

    data = sorted(data, key=lambda _: ipaddress.ip_address(_['IP']))

    return data


def get_collated_info(input_filename='./result.txt',
                      output_filename='内网资产探测信息表.xlsx',
                      black_networks=(),
                      white_networks=()):
    """
    获取各类信息,并合并成一个表格文档。
    IP  端口  协议  服务  URL  Title  指纹  返回码
    [IP, port, protocol(默认tcp), server(无法识别:""), url, title, finger, code]

    :return: list[[IP, port, protocol, server, url, title, finger, code]]
    """
    content = readfile(input_filename)
    content = list(dict.fromkeys(content))
    content = clean_content(content, black_networks, white_networks)

    # 常规资产信息
    ip_ports = get_ip_ports(content)
    url_info_dict = get_url_info(content)
    domain_os_list = get_domain_os(content)

    # 可能性高的脆弱资产
    weak_auth_list = get_weak_auth(content)
    ip_exp_list = get_ip_exp(content)
    url_poc_list = get_url_poc(content)

    data = []
    ip_data = []
    # IP排序(小 --> 大)
    ip_ports = sorted(ip_ports.items(),
                      key=lambda _: ipaddress.ip_address(_[0]))

    for ip, ports in ip_ports:
        # 端口排序(小 --> 大)
        ports.sort(key=int)
        ip_data.append({'IP': ip, '端口': ' '.join(ports)})
        for p in ports:
            d_row = {'IP': '', '端口': '', '协议': 'tcp', '服务': '',
                     'URL': '', 'Title': '', '指纹': '', '返回码': ''}
            url_info = search_url(ip, p, url_info_dict)
            d_row['IP'] = ip
            d_row['端口'] = int(p)
            d_row['协议'] = 'tcp'
            d_row['URL'] = url_info['url']
            d_row['Title'] = url_info['title']
            d_row['指纹'] = url_info['finger']
            d_row['返回码'] = url_info['code']
            data.append(d_row)

    data = sorted(data, key=lambda _: int(_['端口']))
    data = sorted(data, key=lambda _: ipaddress.ip_address(_['IP']))
    df_web = pd.DataFrame(data)
    df_domain = pd.DataFrame(domain_os_list)
    df_ip = pd.DataFrame(ip_data)

    df_weak = pd.DataFrame(weak_auth_list)
    df_poc = pd.DataFrame(url_poc_list)
    df_exp = pd.DataFrame(ip_exp_list)

    name, suffix = os.path.splitext(output_filename)
    weak_filename = name + '_脆弱资产' + suffix
    with pd.ExcelWriter(output_filename) as writer:
        df_web.to_excel(writer, sheet_name='资产信息', index=False)
        df_domain.to_excel(writer, sheet_name='域信息', index=False)
        df_ip.to_excel(writer, sheet_name='存活IP', index=False)
    with pd.ExcelWriter(name + '_脆弱资产' + suffix) as writer:
        df_weak.to_excel(writer, sheet_name='脆弱认证', index=False)
        df_poc.to_excel(writer, sheet_name='POC', index=False)
        df_exp.to_excel(writer, sheet_name='主机漏洞', index=False)
    text = f'[INFO] 已导出:{os.path.realpath(output_filename)}'
    message = color_text(text, Fore.GREEN)
    print(message)
    text = f'[INFO] 已导出:{os.path.realpath(weak_filename)}'
    message = color_text(text, Fore.GREEN)
    print(message + '\n')


def main():
    parser = argparse.ArgumentParser()
    parser.description = "本脚本用于整合fscan探测结果,生成excel表格文档。" \
                         "(仅作合法用途)"
    parser.usage = "\n用法举例:\n" \
                   "\t简单使用:python fscan_result_merge_toExcel.py " \
                   "-i ./result.txt -o ./内网资产信息.xlsx\n" \
                   "\t屏蔽特定IP:python fscan_result_merge_toExcel.py " \
                   "-i ./result.txt " \
                   "-n \"10.217.35.158,10.23.32.0/24\"\n" \
                   "\t只整合特定IP:python fscan_result_merge_toExcel.py " \
                   "-i ./result.txt -a \"192.168.0.0/16\""
    parser.add_argument('-i', '--input', default='./result.txt',
                        metavar="filename",
                        help='fscan生成的探测结果 文本文件(.txt)')
    parser.add_argument('-o', '--output', default='./内网资产信息.xlsx',
                        metavar="filename",
                        help='整合信息后所生成的表格文档 保存路径(含文件名)')
    parser.add_argument('-n', '--neglect', default='',
                        metavar='IP地址(段)',
                        help='传入需要排除的IP地址(段),'
                             '如: -n 192.168.1.1,192.168.23.0/24')
    parser.add_argument('-nf', '--neglect_file', default='',
                        metavar='filename',
                        help='包含需要排除的IP地址(段)的可读文件,'
                             '其中每个IP地址(段)独占一行')
    parser.add_argument('-a', '--allow', default='',
                        metavar='IP地址',
                        help='限制程序只处理 该参数所传入的IP地址(段)相关信息。')
    args = parser.parse_args()
    if not os.path.isfile(args.input):
        print("[ERROR] 输入的fscan结果文件路径不存在,请检查。")
        return
    if ' ' in args.neglect:
        print("[ERROR] 输入的(用于屏蔽的)IP地址(段)参数格式不合法:不能含有空格。")
    white_networks = get_white_ip_network(
        ip_string=args.allow
    )
    black_networks = get_black_ip_network(
        ip_string=args.neglect, ip_file=args.neglect_file
    )

    text = "[INFO] 程序开始运行..."
    message = color_text(text, Fore.GREEN)
    print(message)
    get_collated_info(input_filename=args.input,
                      output_filename=args.output,
                      black_networks=black_networks,
                      white_networks=white_networks)


if __name__ == '__main__':
    main()
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值