用户
搜索
  • TA的每日心情
    无聊
    2021-3-16 19:57
  • 签到天数: 8 天

    连续签到: 1 天

    [LV.3]经常看看I

    i春秋作家

    Rank: 7Rank: 7Rank: 7

    4

    主题

    16

    帖子

    279

    魔法币
    收听
    0
    粉丝
    1
    注册时间
    2019-10-8

    i春秋签约作者

    发表于 2021-12-2 15:29:08 81933
    本帖最后由 fatmo 于 2021-12-2 15:31 编辑

    本文原创作者fatmo,本文属i春秋原创奖励计划,未经许可禁止转载。

    0x01 前言

    开一个坑,介绍在SRC挖掘过程中常见的信息收集面和手段,并使用python进行自动化实现。整个系列的最终目标是学习到SRC挖掘中的一些基本的信息收集方法,并开发出一个python自动化信息收集脚本集合。

    本文为系列第一篇,主要讲解阅读本系列文章需要的前置知识,将包括:

    1. 多线程与多协程;
    2. 列表、字典、队列;
    3. 类的继承;
    4. 异常处理;
    5. 本项目日志方案、输入方案、结果保存方案;

    注意:因为本系列文章均基于python展开,因此上述知识点均基于python讲述。

    0x02 多线程与协程

    1.CPython的多线程陷阱

    我们知道多线程的编程方式,在I/O密集型的程序中效率极高,恰好信息收集的脚本就是I/O密集型的程序(设计大量文件读写、网络请求)。但是,我不打算选择多线程,为什么?

    因为CPython的多线程是假的多线程

    python是一门解释型语言,我们在执行python文件时,需要解释器去将python解释称CPU能够读懂的字节码。CPython时python官方的,最常用的实现,这里可以简单理解为:CPython就是python的解释器,它把我们执行的python文件解释成字节码。

    在CPython设计的早期,多线程还没有流行,为了解决CPython中垃圾回收时产生的冲突问题,引进了GIL(全局解释锁)。因为GIL的存在,限制了CPython执行的过程中每个CPU在同一时间只能执行一个线程。这个方法在当时是解决了垃圾回收的问题,但随着时间的推移,多线程编程兴起,GIL的存在反而成了CPython最大的问题,很多大神都在尝试去除GIL,但仍没有成功。

    总结下来,就是目前使用CPython解释的python程序,是无法实现真正的多线程的,本质上仍是单线程运行。

    GIL.jpg

    2.解决办法

    既然CPython的多线程存在大坑,那该如何解决?这里提供几个方案:

    1. 换一个解释器,如PyPy;PyPy重新设计了python解释器,解决了CPython存在的许多性能问题,但是因为它不是python官方解释器,很多重要的第三方库无法使用。
    2. 使用多进程或多协程代替多协程。

    本系列将使用多协程代替多线程。

    3.协程

    协程本质上是一套函数,跟进程与线程并不是一个维度的东西,操作系统对于协程是无感知。我们在写python程序时,使用async/await来控制协程的阻塞和运行,实现在密集I/O场景下的复用方案。

    COroutine.jpg

    4.python中协程的基本使用方法

    首先理解以下几个概念:

    1. 事件循环:管理所有事件的一个循环队列,不断地遍历队列找到解除阻塞的事件执行。python中使用asyncio.new_event_loop()来创建事件循环
    2. Future:尚未完成的事件
    3. Task:是Future的子类,作用是在运行某个任务的同时可以并发的运行多个任务。

    然后,讲解几个重要的函数:

    1. #创建事件循环
      newLoop = asyncio.new_event_loop()
      asyncio.set_event_loop(newLoop)
      loop = asyncio.get_event_loop()
    2. #创建Task对象
      asyncio.ensure_future(xxx(xxx)
    3. #开始运行,不断遍历事件运行直到所有协程全部运行完毕
      loop.run_until_complete(asyncio.wait(tasks))
    4. #需要在用到协程的函数前加上async,在调用协程函数前加上await
    5. #阻塞协程,睡眠一秒钟
      await asyncio.sleep(1)

    看一个python下简单的协程例子:

    import asyncio
    import time
    
    async def asyTest(number):
        print("Test A:" + str(number))
        await asyncio.sleep(1)
        print("Test B:" + str(number))
    
    def main():
        tasks = []
        newLoop = asyncio.new_event_loop()
        asyncio.set_event_loop(newLoop)
        loop = asyncio.get_event_loop()
    
        for i in range(3):
            tasks.append(asyncio.ensure_future(asyTest(i)))
    
        startTime = time.time()
        loop.run_until_complete(asyncio.wait(tasks))
        endTime = time.time()
        print("总耗时:" + str(endTime - startTime))
    
    main()

    运行结果为:

    Test A:0
    Test A:1
    Test A:2
    Test B:0
    Test B:1
    Test B:2
    总耗时:1.0083599090576172

    显然,当程序遇到阻塞时,自动跳到下一个事件执行,并没有停留等待阻塞结束。总耗时1秒,低于串行运行时耗时3秒。

    有一点需要注意,协程因为是由用户自主实现的,非操作系统的概念,所以当我们需要使用协程时,必须要有解决方案来支撑,否则,协程是无法运行的

    下面介绍本系列需要用到的几个解决方案:

    01 用协程进行dns解析:

    import aiodns
    
    async def query(name, resolver):
        """
        用协程进行DNS解析
        :param name:
        :param resolver:
        :return:
        """
        return await resolver.query(name, 'CNAME')
    
    def queryWithoutAsyn(domain):
        """
        不用协程进行dns解析
        :param domain:
        :return:
        """
        answer = dns.resolver.resolve(domain, 'CNAME')
    
        def main():
        tasks = []
        newLoop = asyncio.new_event_loop()
        asyncio.set_event_loop(newLoop)
        loop = asyncio.get_event_loop()
    
        resolver = aiodns.DNSResolver(loop=loop)
    
        for i in range(10):
            tasks.append(asyncio.ensure_future(query('www.baidu.com', resolver)))
    
        startTime = time.time()
        result = loop.run_until_complete(asyncio.wait(tasks))
        endTime = time.time()
        print("使用协程-总耗时:" + str(endTime - startTime))
    
        startTime = time.time()
        for i in range(10):
            queryWithoutAsyn('www.baidu.com')
        endTime = time.time()
        print("不使用协程-总耗时:" + str(endTime - startTime))
    
    main()

    我们对比一下使用协程和不使用协程的区别:

    使用协程-总耗时:0.0880880355834961
    不使用协程-总耗时:40.82021713256836

    显然,差距是巨大的。

    02 用协程进行http请求:

    import aiohttp
    
    header = {
                'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) '
                              'Chrome/79.0.3945.130 Safari/537.36'
            }
    
    async def sendRequestAsvnc(url):
        sem = asyncio.Semaphore(1024)
        async with aiohttp.ClientSession(connector=aiohttp.TCPConnector()) as session:
            async with sem:
                async with session.get(url, timeout=20, headers=header) as req:
                    # await asyncio.sleep(1)
                    response = await req.text('utf-8', 'ignore')
                    req.close()
                    return response
    
    def sendRequest(url):
        req = requests.get(url=url, headers=header)
        res = req.text
        return res
    
    def main():
        tasks = []
        newLoop = asyncio.new_event_loop()
        asyncio.set_event_loop(newLoop)
        loop = asyncio.get_event_loop()
    
        resolver = aiodns.DNSResolver(loop=loop)
    
        for i in range(10):
            tasks.append(asyncio.ensure_future(sendRequestAsvnc('http://www.baidu.com')))
    
        startTime = time.time()
        result = loop.run_until_complete(asyncio.wait(tasks))
        endTime = time.time()
        print("使用协程-总耗时:" + str(endTime - startTime))
    
        startTime = time.time()
        for i in range(10):
            sendRequest('http://www.baidu.com')
        endTime = time.time()
        print("不使用协程-总耗时:" + str(endTime - startTime))
    
    main()

    结果如下,协程同样效率更高:

    使用协程-总耗时:1.927349328994751
    不使用协程-总耗时:5.816945314407349

    0x03 列表、字典、队列

    列表、字典、队列为python中几个比较常用的数据结构。

    queue.jpg

    01 列表

    #例化一个列表
    aList = []
    
    #添加数据
    aList.append("1")
    aList.append("2")
    aList.append("3")
    
    #遍历列表
    for item in aList:
        print(item)

    结果:

    1
    2
    3

    02 字典

    #例化一个字典
    aDict = {}
    
    #添加数据
    aDict['a'] = "1"
    aDict['b'] = "2"
    
    #取值
    print(aDict['a'])
    
    #遍历
    for item in aDict.keys():
        print(aDict[item])

    结果:

    1
    1
    2

    03 队列

    #例化一个队列
    aQueue = queue.Queue()
    
    #入队
    aQueue.put('a')
    aQueue.put('b')
    aQueue.put('c')
    
    #出队
    print(aQueue.get())
    print(aQueue.get())

    结果:

    a
    b

    0x04 异常处理

    python中的异常处理格式如下:

    try:
        xxx()
    except:
        xxx()

    在本系列中,会存在多个目标同时扫描的情况,会消耗较多时间,期间可能会有异常导致程序中断,也有可能因我们等得不耐烦了而直接手动终止程序,而在程序终止后,我们需要保存终止前的扫描结果,就必须要上异常处理,处理格式如下:

    try:
        xxx
    except KeyboardInterrupt:
        #捕获用户终止的异常
        xxx
    
    saveResult()
    

    0x05 本项目日志方案、输入方案、结果保存方案

    01 日志方案

    日志会保存程序运行过程中的异常信息、进度信息,而且必须要同时输出在屏幕和文件中,通过以下代码设定:

    def initLog(self):
        """
        日志配置,同时输出在日志文件和屏幕上
        :return:
        """
        self.logger = logging.Logger('log')
        self.logger.setLevel(logging.INFO)
    
        logFileName = os.getcwd() + '/log/' + datetime.datetime.now().strftime('%Y-%m-%d-%H-%M-%S') + '.txt'
        rHandler = RotatingFileHandler(logFileName, maxBytes=100 * 1024, backupCount=1)
        rHandler.setLevel(logging.INFO)
        formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
        rHandler.setFormatter(formatter)
    
        console = logging.StreamHandler()
        console.setLevel(logging.INFO)
        console.setFormatter(formatter)
    
        self.logger.addHandler(rHandler)
        self.logger.addHandler(console)

    程序运行过程中,会在./log/文件夹下生成一个以当前时间命名的日志文件,并会不断写入日志信息,同时日志信息也会输出在屏幕上。

    02 参数方案

    参数方案较为常规:

    def argparser(self):
        """
        解析参数
        :return:参数解析结果
        """
        parser = argparse.ArgumentParser(description='InfoScripts can help you collect target\'s information',
                                         epilog='\tUsage:\npython3 ' + sys.argv[0] + " --target www.baidu.com")
        parser.add_argument('--target', '-t', help='A target like www.example.com or subdomains.txt', required=True)
    
        args = parser.parse_args()
        return args

    跟其他脚本一样,在bash中添加参数作为输入

    03 结果保存方案

    本项目会存在两个结果文件夹,./CheckResult/./result/

    ./result/文件夹下会有数个以域名命名的文件夹,每个域名文件夹下保存有此域名的信息,包括端口扫描结果,子域名收集结果等,多为json格式。

    ./CheckResult/文件夹下有数个以时间命名的文件夹,每个文件夹下保存有此次扫描的结果,多为txt格式。

    def initDir(self):
        """
        文件夹不存在则创建
        :return:
        """
        if os.path.exists(os.getcwd() + '/log/') is False:
            os.mkdir(os.getcwd() + '/log/')
        if os.path.exists(os.getcwd() + '/result/') is False:
            os.mkdir(os.getcwd() + '/result/')
        if os.path.exists(os.getcwd() + '/CheckResult/') is False:
            os.mkdir(os.getcwd() + '/CheckResult/')
        if os.path.exists(os.getcwd() + '/CheckResult/' + self.fileName + '/') is False:
            os.mkdir(os.getcwd() + '/CheckResult/' + self.fileName + '/')

    0x06 类的继承

    python中类的继承与其他语言差不多,先写出一个基类

    class BaseObject():
        def __init__():
            print("6666")
        def hello():
            print("6666666666")

    子类在定义时中加入基类即可继承:

    class ChildObject(BaseObject):
        def __init__():
            #调用父类的构造函数
            BaseObject.__init__()
            xxx
    
    # 还可以调用子类的方法
    childObject = ChildObject()
    childObject.hello()

    0x07 项目基类编写

    有了前面的这些基础知识,我们现在可以着手写项目真正的基类了。

    首先我们明确基类的作用,项目中会有cdn检测、端口扫描、子域名挖掘等众多脚本,这些脚本中共同的代码,可以抽象到基类中实现复用,脚本只需要继承基类就可以使用。

    那么这些脚本有那些可以抽象出来的功能呢?就是:

    1. 日志功能
    2. 保存目录创建功能
    3. 参数解析功能
    4. http请求功能

    明确了目标,我们可以开始写代码了:

    01 首先写出基类框架

    class BaseObject(object):
    
        def __init__(self):
            self.fileName = time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime())
    
            # 初始化文件夹
            self.initDir()
    
            #初始化日志
            self.initLog()
    
            #设置HTTP请求头
            self.headers = {
                'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) '
                              'Chrome/79.0.3945.130 Safari/537.36'
            }
    
        def initLog(self):
            """
            日志配置,同时输出在日志文件和屏幕上
            :return:
            """
            pass
    
        def initDir(self):
            """
            文件夹不存在则创建
            :return:
            """
            pass
    
        def argparser(self):
            """
            解析参数
            :return:参数解析结果
            """
            pass
    
        async def sendRequest(self, url):
            """
            发送http请求
            :param url:
            :return:
            """
            pass

    02 依次把四个功能实现即可:

    日志功能:

        def initLog(self):
            """
            日志配置,同时输出在日志文件和屏幕上
            :return:
            """
            self.logger = logging.Logger('log')
            self.logger.setLevel(logging.INFO)
    
            logFileName = os.getcwd() + '/log/' + datetime.datetime.now().strftime('%Y-%m-%d-%H-%M-%S') + '.txt'
            rHandler = RotatingFileHandler(logFileName, maxBytes=1 * 1024, backupCount=1)
            rHandler.setLevel(logging.INFO)
            formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
            rHandler.setFormatter(formatter)
    
            console = logging.StreamHandler()
            console.setLevel(logging.INFO)
            console.setFormatter(formatter)
    
            self.logger.addHandler(rHandler)
            self.logger.addHandler(console)

    新建保存目录:

        def initDir(self):
            """
            文件夹不存在则创建
            :return:
            """
            if os.path.exists(os.getcwd() + '/log/') is False:
                os.mkdir(os.getcwd() + '/log/')
            if os.path.exists(os.getcwd() + '/result/') is False:
                os.mkdir(os.getcwd() + '/result/')
            if os.path.exists(os.getcwd() + '/CheckResult/') is False:
                os.mkdir(os.getcwd() + '/CheckResult/')
            if os.path.exists(os.getcwd() + '/CheckResult/' + self.fileName + '/') is False:
                os.mkdir(os.getcwd() + '/CheckResult/' + self.fileName + '/')

    解析输入参数:

        def argparser(self):
            """
            解析参数
            :return:参数解析结果
            """
            parser = argparse.ArgumentParser(description='InfoScripts can help you collect target\'s information',
                                             epilog='\tUsage:\npython3 ' + sys.argv[0] + " --target www.baidu.com")
            parser.add_argument('--target', '-t', help='A target like www.example.com or subdomains.txt', required=True)
    
            args = parser.parse_args()
            return args

    发送http请求:

        async def sendRequest(self, url):
            """
            发送http请求
            :param url:
            :return:
            """
            sem = asyncio.Semaphore(1024)
            try:
                async with aiohttp.ClientSession(connector=aiohttp.TCPConnector()) as session:
                    async with sem:
                        async with session.get(url, timeout=20, headers=self.headers) as req:
                            await asyncio.sleep(1)
                            response = await req.text('utf-8', 'ignore')
                            req.close()
                            return response
            except CancelledError:
                pass
            except ConnectionResetError:
                pass
            except Exception as e:
                self.logger.error('[-]Resolve {} fail'.format(url))
                return False

    基类全部代码如下:

    import asyncio, aiohttp
    import datetime
    import logging
    import os
    import sys
    import time
    from asyncio import CancelledError
    from logging.handlers import RotatingFileHandler
    
    import argparse
    
    class BaseObject(object):
    
        def __init__(self):
            self.fileName = time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime())
    
            # 初始化文件夹
            self.initDir()
    
            #初始化日志
            self.initLog()
    
            #设置HTTP请求头
            self.headers = {
                'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) '
                              'Chrome/79.0.3945.130 Safari/537.36'
            }
    
        def initLog(self):
            """
            日志配置,同时输出在日志文件和屏幕上
            :return:
            """
            self.logger = logging.Logger('log')
            self.logger.setLevel(logging.INFO)
    
            logFileName = os.getcwd() + '/log/' + datetime.datetime.now().strftime('%Y-%m-%d-%H-%M-%S') + '.txt'
            rHandler = RotatingFileHandler(logFileName, maxBytes=1 * 1024, backupCount=1)
            rHandler.setLevel(logging.INFO)
            formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
            rHandler.setFormatter(formatter)
    
            console = logging.StreamHandler()
            console.setLevel(logging.INFO)
            console.setFormatter(formatter)
    
            self.logger.addHandler(rHandler)
            self.logger.addHandler(console)
    
        def initDir(self):
            """
            文件夹不存在则创建
            :return:
            """
            if os.path.exists(os.getcwd() + '/log/') is False:
                os.mkdir(os.getcwd() + '/log/')
            if os.path.exists(os.getcwd() + '/result/') is False:
                os.mkdir(os.getcwd() + '/result/')
            if os.path.exists(os.getcwd() + '/CheckResult/') is False:
                os.mkdir(os.getcwd() + '/CheckResult/')
            if os.path.exists(os.getcwd() + '/CheckResult/' + self.fileName + '/') is False:
                os.mkdir(os.getcwd() + '/CheckResult/' + self.fileName + '/')
    
        def argparser(self):
            """
            解析参数
            :return:参数解析结果
            """
            parser = argparse.ArgumentParser(description='InfoScripts can help you collect target\'s information',
                                             epilog='\tUsage:\npython3 ' + sys.argv[0] + " --target www.baidu.com")
            parser.add_argument('--target', '-t', help='A target like www.example.com or subdomains.txt', required=True)
    
            args = parser.parse_args()
            return args
    
        async def sendRequest(self, url):
            """
            发送http请求
            :param url:
            :return:
            """
            sem = asyncio.Semaphore(1024)
            try:
                async with aiohttp.ClientSession(connector=aiohttp.TCPConnector()) as session:
                    async with sem:
                        async with session.get(url, timeout=20, headers=self.headers) as req:
                            await asyncio.sleep(1)
                            response = await req.text('utf-8', 'ignore')
                            req.close()
                            return response
            except CancelledError:
                pass
            except ConnectionResetError:
                pass
            except Exception as e:
                self.logger.error('[-]Resolve {} fail'.format(url))
                return False

    基类代码已上传github:https://github.com/fatmo666/InfoScripts/blob/master/BaseObject.py

    支持作者
    使用道具 举报 回复
    不懂协程,问下任务1协程切换到任务2后,任务1处于阻塞还是正常运行?任务2再协程切换到任务3后,任务1和任务2又处于什么状态?
    另外,对文件的操作,首先建议使用绝对路径而不是相对路径;其次建议使用os.path.abspath()而不是os.getcwd()。因为在不同环境运行相同的代码,取路径时取到的结果可能不同,就可能取不到文件,详细可看我的文章https://blog.csdn.net/fj_changing/article/details/116451405,如果说的不够详细,建议看参考链接1。
    使用道具 举报 回复
    发表于 2021-12-5 16:12:10
    纯白的小白 发表于 2021-12-4 22:49
    不懂协程,问下任务1协程切换到任务2后,任务1处于阻塞还是正常运行?任务2再协程切换到任务3后,任务1和任 ...

    协程本质就是用来实现异步IO。打个比方,用协程实现网络请求,任务1请求了百度后,任务1便处于等待状态,等待百度的响应,这时会立马切换到任务2执行代码,等到百度响应了任务1,又会立马从任务2切换回任务1。同一个线程同一时间只会执行一个协程,其他协程要么处于等待IO的状态,要么处于阻塞状态。
    使用道具 举报 回复
    发表于 2021-12-5 16:13:40
    纯白的小白 发表于 2021-12-4 22:49
    不懂协程,问下任务1协程切换到任务2后,任务1处于阻塞还是正常运行?任务2再协程切换到任务3后,任务1和任 ...

    文件操作我再研究下,多谢大佬指点
    使用道具 举报 回复
    fatmo 发表于 2021-12-5 16:12
    协程本质就是用来实现异步IO。打个比方,用协程实现网络请求,任务1请求了百度后,任务1便处于等待状态, ...

    1.那协程的切换会不会造成CPU忙于任务切换和资源切换,导致效率降低(理论上和实际上)?
    2.协程和真正的多线程有什么区别(不考虑GIL)?
    使用道具 举报 回复
    发表于 2021-12-6 20:38:46
    本帖最后由 fatmo 于 2021-12-6 22:10 编辑
    纯白的小白 发表于 2021-12-5 23:02
    1.那协程的切换会不会造成CPU忙于任务切换和资源切换,导致效率降低(理论上和实际上)?
    2.协程和真正的多 ...

    1.IO等待时间成本高于任务切换成本,效率就会提高;IO等待时间成本低于任务切换成本,效率就会降低。本文对HTTP请求和DNS解析请求都在使用协程和不使用协程的情况下做了测试,使用协程效率显著提高。类似小幅度文件读写,IO等待时间极短,使用协程会导致效率降低。
    2.协程是串行的,同一时间只会执行一个任务;多线程是并行的,同一时间会执行多个任务。一个进程可以有多个线程,一个线程可以有多个协程,多进程、多线程、多协程是可以一起玩的。
    使用道具 举报 回复
    fatmo 发表于 2021-12-6 20:38
    1.IO等待时间成本高于任务切换成本,效率就会提高;IO等待时间成本低于任务切换成本,效率就会降低。本文 ...

    明白了,谢谢。
    之前感觉协程好像也是多任务同时执行,就感觉它跟多线程十分相似,可默认情况下python又不支持真正的多线程,就觉得矛盾。
    现在知道了,协程虽然看上去是多任务同时执行,不过执行的任务是不同维度的,是计算型和IO型同时执行,不是多线程那种多个计算型同时执行(后来即便切换到其他类型也是同时执行,前提是硬件设备支持);执行者也不一样,处理器执行计算型任务,IO设备执行IO型任务,不是多线程那种多个处理器核心(含超线程)同时执行多个不同的计算型任务。概括一下就是,对于处理器来说,协程是并发,多线程是并行;对于不同任务类型的硬件设备来说,协程也可以像多线程那样做到让这些不同任务类型的硬件设备并行工作,例如等待IO结果时让处理器去执行其他任务,此时IO设备和处理器就在并行工作。
    使用道具 举报 回复
    发表于 2021-12-7 11:36:08
    纯白的小白 发表于 2021-12-6 22:12
    明白了,谢谢。
    之前感觉协程好像也是多任务同时执行,就感觉它跟多线程十分相似,可默认情况下python又 ...

    大佬太强了
    使用道具 举报 回复
    发新帖
    您需要登录后才可以回帖 登录 | 立即注册