安全路透社
当前位置:安全路透社 > 网络转载 > 正文

TDD 方法开发渗透测试工具:代理扫描器(第一集)

*本文原创作者:VillanCh

前言

之前在 Freebuf 的文章“关于渗透测试工具开发架构探讨”,读者有提出,具体应该怎么写呢?第一步做什么?第二步做什么?现在我觉得自己有一些体会了,也自己觉得自己还是有把握把这个事情解释清楚的,那么我就来写一些东西,来讲一下我很喜欢的一种敏捷开发方法: TDD。

什么是 TDD 以及 我为什么推荐 TDD

做过一些软件开发的朋友们可能会知道这个东西 TDD(Test-Driven Development),诚然我自己之前的开发也并不是 TDD,而是传统的开发,写文档,开发,写文档的“瀑布”开发。
当然自己也是开始尝试进行一些系统的 Web 开发训练的时候深深体会到了 TDD 开发的优势,想到了 Freebuf 这边还有一个坑没填,那就来结合现在的经验谈一下这个事情吧!
Test-Driven Development 顾名思义,就是利用测试来驱动(督促)开发进展中文名也就是测试驱动开发,简称 TDD。是一种敏捷开发模式。当然这么说大家会觉得非常抽象。那我就通俗的来解释一下,可能有不恰当的地方,希望读者在评论区指出:众所周知瀑布式开发一般来说写代码-写文档-写代码-写文档-… 。每一个周期与周期之间唯一的衔接就是文档;但是 TDD 是测试驱动的,就是先有测试用例 (testCase),然后再有代码(听起来是不是非常的奇怪),其实想想非常的常见,不就是现有需求后代码么?没错啊,TDD 可以说是把这个大需求,直接反映在测试上,然后根据测试用例来开发代码,写出满足测试用例的最少的代码。

优势

这样做有什么好处呢?时刻检查自己的代码是不是符合自己的目标,如果符合要求可以通过,如果不符合,则不能通过。除此之外,仔细想想,在未完成开发的时候,你可以随时停下你的开发,下次只需要运行测试用例,查看哪里没有通过,就可以继续之前的代码进行开发。当然,这只是其中的两条显而易见的好处。

缺点

理所当然的,作为一个渗透测试人员,需要一些短平快的脚本的时候,是根本不需要使用 TDD 方法去写代码的,显然啊,TDD 方法开发的代码量其实要比原来的开发要大(主要体现在有时候需要写大量的测试代码),这算是 TDD 一个缺点。不过按照我个人的体验来说,TDD 方法去开发工具的时候,效率不止是提高了一点,就算写了大量的测试代码,但是仍然是效率高于原来。

渗透工具(HTTP 代理扫描)从 0 开始:

提到 HTTP 代理大家都还是挺熟悉的。那么既然要学习渗透工具开发,不妨就来做一个这样的实用的小玩意。在学习 Python 之余坚持完成了,还可以平时自己使用一波,还是挺不错的。
那么关于 HTTP 代理呢?大家应该都并不是特别陌生,我们经常会使用到 HTTP/HTTPS 代理去完成隐藏自己的真实 IP(隐藏自己真实信息这个的可行性我们暂且不谈),那么大家会发现,我们经常上网寻找一些公开的代理网站,(比如快代理啊,xici之类的)

开发环境与重要模块

1. Python2.7+

2. Pylint 用于检查代码规范

3. unittest 用于 TDD 测试驱动开发

说实话,基本 Python 基础知识和 unittest 的基本使用方法我就不介绍了,如果你在阅读这篇文章,就说明你至少还有有一些 Python 基础的。

哦,除此之外,建议在写代码的时候使用 Google 开源项目代码规范 (Python)

0×00 Start Up!

(嗨呀,首先是不是要写 Hello World 啊?)
当然,既然我们想要使用 TDD 的方法来开发,我们首先当然需要写一个测试用例。(暂且撇开测试套件什么的,我们就最简单的做一个测试用例)。
那么要写测试用例了,我们首先得知道,我们想要做出的功能是怎么样的?嗯…既然是代理扫描器嘛,首先我们得知道怎么去扫描 HTTP 代理,其实并不用说的太玄乎,有个最简单的办法就是,我们就去把它当代理用,如果成功了,那么就说明这个 IP 的端口开启了代理模式,如果失败了,那就说明这个端口并不能作为代理来使用。
那么事情就简单了,我们想要的第一个功能就是使用 Python 完成检测目标 IP:PORT 是否是可用代理。
那么就动手吧!

当然我们需要先建立一个文件,根据功能,就叫 check_proxy.py 吧!在新建文件之后,我们需要开始编写测试用例了。(什么?为什么不是编写代码?)毕竟我们尝试的是 TDD 方法开发工具,显然首先写个函数什么的显然并不是符合我们的初衷。那么,我们就开始吧!

#!/usr/bin/env python
#coding:utf-8
"""
  Author:   --<VillanCH>
  Purpose: check proxy available
  Created: 2016/10/31
"""

import unittest

########################################################################
class CheckProxyTest(unittest.case.TestCase):
    """Test CheckProxy"""
    pass

if __name__ == '__main__':
    unittest.main()

在创建了这个段代码之后,显然,大家看到我 import unittest 应该就知道接下来要编写测试用例了吧。那么 unittest 应该怎么使用我觉得我不用向大家解释太多,直接看代码更加直观。
在有了测试文件类之后呢,我们并不着急编写我们的 CheckProxy 的功能代码。我们不妨设想一下我们这个类的功能:姑且就是我们输入一个 IP:PORT 这个形式,运行模块,如果可以用作代理,返回结果表明这个地址可以用作 HTTP 还是 HTTPS 代理,那么我们就规定一下返回的格式吧:

{
    'result':True,
    'proxy':{
        'http':'xxx.xxx.xxx.xx:port',
        'https':'xxx.xxx.xxx.xx:port'
    }
}

如果没有 HTTPS 代理的功能的话那就是:

{
    'result':True,
    'proxy':{
        'http':'xxx.xxx.xxx.xx:port',
    }
}

当然这是理想情况,如果不是代理呢?

{
    'result':False,
    'proxy':None
}

嗯这样的话,我们就可以根据这个来写第一个测试用例了:
我们删除 pass 然后添加 def test_xxxxx 方法:

def test_check_ip(self):
    '''
    result should be
    {
        'result':True,
        'proxy':{
            'http':'xxx.xxx.xxx.xx:port',
         'https':'xxx.xxx.xxx.xx:port'
        }
    }
    '''
    #创建想要测试的实例
    master = CheckProxy('78.6.5.45:8080')
    result = master.test()

    ##对结果进行测试
    #测试结果必须是个 dict 类型
    self.assertIsInstance(result, dict)
    #必须有一个 result 和 proxy 的键
    self.assertTrue(result.has_key('result'))
    self.assertTrue(result.has_key('proxy'))
    #result 键对应的值必须是一个 bool 类型
    self.assertIsInstance(result['result'], bool)        
    #断言测试 result['proxy'] 的类型
    if result['result']:
        self.assertIsInstance(result['proxy'], dict)
    else:
        self.assertIsNone(result['proxy'])  

0×01 测试与修改

显然我们看到我们的测试代码,如果能跑通的话,也就一定是我们想要的结果了(至少接口是符合我们需求的)。那么我们切换到命令行来执行这个测试用例。

E
======================================================================
ERROR: test_check_ip (__main__.CheckProxyTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "check_proxy.py", line 26, in test_check_ip
    master = CheckProxy('78.6.5.45:8080')
NameError: global name 'CheckProxy' is not defined

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (errors=1)  

显然不用过多解释大家也知道这个是失败了,原因是什么呢?我们根本没有建立 CheckProxy 方法或者 CheckProxy 类啊,当然会失败,那么我们看到 Traceback 中告诉我们 CheckProxy 没有定义,那么我们肯定要去先解决这第一个问题了吧。 所以我们新建我们的类:

class CheckProxy(object):
    """Check Proxy

    Attributes:
        target: A str, the target you want to check. 
                Example: 44.4.4.4:44"""

    #----------------------------------------------------------------------
    def __init__(self, target):
        """Constructor"""
        self.target = target

然后我们再来测试我们的代码 python check_proxy.py

E
======================================================================
ERROR: test_check_ip (__main__.CheckProxyTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "check_proxy.py", line 43, in test_check_ip
    result = master.test()
AttributeError: 'CheckProxy' object has no attribute 'test'

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (errors=1)****  

显然又失败了,原因当然 unittest 他已经告诉我们了: 没有一个叫 test 的参数,当然啊我们毕竟之写了 CheckProxy 的构造器,我们并没有创建一个叫 test 的方法。所以我们又需要修改我们的代码…
我觉得到现在了,读者应该觉得挺累的也挺无聊的,不就是这个小东西么?我分分钟写出来啊。而且并不用写这么烦人的测试代码,而且很蠢的一次一次去测试。事实上我在实际做的时候也不会这么蠢,这么慢去写,当然熟练的话,可以根据测试用例直接写出符合规范的代码。嗯,确实是这样。但是既然是刚开始学习这项新的 “技术” 为了领会思想,还是乖乖照做吧。

0×02 第一次测试成功

经过上面各种各样循环,我们终于测试成功了,当然下面的代码可能并不是特别光荣。哈哈 但是至少我们知道成功是什么滋味了对吧?

.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK  

我们的代码现在是什么样子呢?

class CheckProxy(object):
    """Check Proxy

    Attributes:
        target: A str, the target you want to check. 
                Example: 44.4.4.4:44"""

    #----------------------------------------------------------------------
    def __init__(self, target):
        """Constructor"""
        self.target = target

    def test(self):
        '''Test the self.target whether is a http proxy addr

        Returns:
            A dict: if the result is True:
            Example:
              {
                  'result':True,
                  'proxy':{
                      'http':'xxx.xxx.xxx.xx:port',
                      'https':'xxx.xxx.xxx.xx:port'
                   }
              }
            if the target isn' t the http proxy addr, 
            the result['proxy'] will be None
         '''
        result = {}
        result['result'] = False
        result['proxy'] = None

        return result

什么嘛!你这明明是欺骗 unittest 获得的通过测试,一点都不诚实。
为什么这么做呢?简单来说我们先保证接口是统一的,然后再对细节进行一些填充,可以很理所当然的增加新的测试用例,完成更加强大的功能。因为很有可能我们不让这个测试通过的话,会干扰我们后面很多选择,让我们觉得,这个 CheckProxy 的每一个功能似乎都与 test 有关,实际上啊,我们第一个测试用例是在测试接口啊。所以大胆放心吧,我们后面当然会完善。

当然,我们先尽量让它通过,然后在来继续写一些测试用例来完成细节,那么我们可以开始下一个测试用例了。

0×03 正式开始了!

之前的算是熟悉我们需要用到的方法了,接下来我们要做的,肯定就是完成这个测试用例的功能了吧~
那么接下来怎么做呢?继续完善 CheckProxy.test() 方法还是?当然我们要继续补充我们的测试用例啊。

那么问题就来了,我们如果想验证我们的代理检测工具是不是可以正常工作,我们当然需要找到一个可以使用的匿名代理对不对?那么这些东西我们去哪里找呢?当然,笔者自然不会打没有准备的仗,哈~其实这次讲的这个工具,是我之间就做过的一个代理搜集(扫描工具),当时做的有不成熟的地方,那么现在就准备重构一下,带领大家过一遍一个工具的开发流程。所以公开的代理,我就不用满大街去寻找了,就随便从我的旧版的 pr0xy 中寻找一些出来吧。

Pr0xy-shell # proxy show
proxy      : {u'http': u'http://120.52.72.23:80'}
check_time : Wed Aug 03 23:31:44 2016
proxy      : {u'http': u'http://103.27.24.238:80'}
check_time : Wed Aug 03 23:31:44 2016
proxy      : {u'http': u'http://50.31.252.54:8080', u'https': u'https://50.31.252.54:8080'}
check_time : Wed Aug 03 23:31:44 2016
proxy      : {u'http': u'http://82.196.10.29:80', u'https': u'https://82.196.10.29:80'}
check_time : Wed Aug 03 23:31:44 2016
proxy      : {u'http': u'http://119.188.94.145:80', u'https': 
proxy      : {u'http': u'http://108.59.10.129:55555', u'https': u'https://108.59.10.129:55555'}
check_time : Wed Aug 03 23:31:44 2016
proxy      : {u'http': u'http://14.161.21.170:8080', u'https': u'https://14.161.21.170:8080'}
check_time : Wed Aug 03 23:31:44 2016
proxy      : {u'http': u'http://179.242.95.20:8080'}  

当然笔者对上面的代理不保证永久的可用性,以上代理收集途径均为公共代理。很有可能在大家看到这篇文章的时候,上面代理已经没剩下几个活着的了。
自然我也是有办法验证自己的 IP 是不是被很好的隐藏了,我们平时怎么做的呢?就是打开百度,输入 IP:

j0lCfkN.png

那么如果成功了的话

6TRcPDr.png

大家看到红色箭头了么?我们想要在程序中使用这项功能,不妨可以去 ip138 中寻找一下接口看能不能使用~
所以经过一番查找,我们发现了接口是 http://1212.ip138.com/ic.asp, 至于这个网站是干什么的不妨大家自己去看一下。一切顺利,于是我们先添加测试代码吧!

def test_ip_check_function(self):
    #首先测试一个正确的实例(已知一定是个代理)
    addr = '108.59.10.129:55555'
    master = CheckProxy(target=addr)
    result = master.test()

    #在前一个例子中我们验证了接口
    #那么在这里我们只需要验证一下
    #一定是完成了代理检测的,而且成功了
    proxy = result['proxy']
    self.assertTrue(proxy.has_key('http'))

没错我们新添加这么一点东西,运行一下看一下结果 (不用说,肯定会出错的,但是我们先看一下在说):

.E
======================================================================
ERROR: test_ip_check_function (__main__.CheckProxyTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "check_proxy.py", line 92, in test_ip_check_function
    self.assertTrue(proxy.has_key('http'))
AttributeError: 'NoneType' object has no attribute 'has_key'

----------------------------------------------------------------------
Ran 2 tests in 0.001s

我们看到了,断言错误了:这是当然嘛我们还没有做检测呢,那么,我们接下来要去完善 CheckProxy.test() 这里的方法了。

#----------------------------------------------------------------------   
def test(self):
    '''Test the self.target whether is a http proxy addr

    Returns:
        A dict: if the result is True:
        Example:
          {
              'result':True,
              'proxy':{
                  'http':'xxx.xxx.xxx.xx:port',
                  'https':'xxx.xxx.xxx.xx:port'
               }
          }
        if the target isn' t the http proxy addr, 
        the result['proxy'] will be None
     '''
    result = {}
    result['result'] = False
    result['proxy'] = self._check_ip()

    return result

#----------------------------------------------------------------------
def _check_ip(self):
    """Check IP return the proxy or None

    Returns:
        A dict or None: if the result is True(the addr can be a proxy)
            the result is the dict like {'http':'http://xx.xx.xx.xx:xx',
                                         'https':'https://xx.xx.xx.xx:xx'}
            And if the https proxy can't be used the key named 'https',
            Well, if the result is False(the addr can't be used as a proxy)
            the result is None"""

    check_ip_http = 'http://1212.ip138.com/ic.asp'
    check_ip_https = 'https://1212.ip138.com/ic.asp'

    addr_proxy = {}
    addr_proxy['http'] = 'http://' + self.target
    addr_proxy['https'] = 'https://' + self.target

    result = {}

    http_rsp = requests.get(check_ip_http, proxies=addr_proxy, timeout=5)
    if self.target in http_rsp.text:
        result['http'] = 'http://' + self.target

    https_rsp = requests.get(check_ip_https, proxies=addr_proxy,
                             verified=False, timeout=5) # close https verify

    if self.target in https_rsp.text:
        result['https'] = 'https://' + self.target

    if result.has_key('http') or result.has_key('https'):
        return result
    else:
        return None

这样写出来的代码实际上还是非常漂亮的对吧?基本符合 Google 开源 Python 规范,尝试一下 Pylint

C:\xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx>pylint check_proxy.py
No config file found, using default configuration
************* Module pr0xy.lib.check_proxy
C: 15, 0: Trailing whitespace (trailing-whitespace)
C: 17, 0: Trailing whitespace (trailing-whitespace)
C: 24, 0: Trailing whitespace (trailing-whitespace)
C: 25, 0: Trailing whitespace (trailing-whitespace)
...
...
...
...
...

Messages
--------

+-----------------------+------------+
|message id             |occurrences |
+=======================+============+
|trailing-whitespace    |10          |
+-----------------------+------------+
|too-few-public-methods |1           |
+-----------------------+------------+



Global evaluation
-----------------
Your code has been rated at 7.56/10 (previous run: 7.56/10, +0.00)

大致看了一下 trailing-whitespace 指的是结尾无意义的空格,too-few-public-methods 指的是公共方法太少了,实际上无意义空格这个是我的 IDE 导致的,应该在 IDE 的 preferences 中可以设置改掉的,嗨呀可是这导致扣了好多分,只能打 7.56 分,笔者好懒啊~
来运行一下测试用例

EE
======================================================================
ERROR: test_check_ip (__main__.CheckProxyTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "check_proxy.py", line 100, in test_check_ip
    result = master.test()
...
ConnectTimeoutError(<requests.packages.urllib3.connection.HTTPConnection object at 0x000000000333BB00>, 'Connection to 78.6.5.45 timed out. (connect timeout=5)'))

======================================================================
ERROR: test_ip_check_function (__main__.CheckProxyTest)
test function
----------------------------------------------------------------------
Traceback (most recent call last):
  File "check_proxy.py", line 121, in test_ip_check_function
...
object at 0x00000000033C90B8>, 'Connection to 108.59.10.129 timed out. (connect timeout=5)'))

----------------------------------------------------------------------
Ran 2 tests in 10.032s

FAILED (errors=2)

太惨了,全失败,自己看一下原因,好像都是因为 timeout, 那么,我们就去处理一下异常好了,顺便再看一下,好像需要调整一下 timeout 的时间,那么我们就顺手改一下 testCase 什么的。

.E
======================================================================
ERROR: test_ip_check_function (__main__.CheckProxyTest)
test function
----------------------------------------------------------------------
Traceback (most recent call last):
  File "check_proxy.py", line 136, in test_ip_check_function
    self.assertTrue(proxy.has_key('http'))
AttributeError: 'NoneType' object has no attribute 'has_key'

----------------------------------------------------------------------
Ran 2 tests in 12.039s

FAILED (errors=1)

好像又不能通过了,看下错误,别慌张,我们仔细看一下这个错误的原因,这显然就是这个代理已经失效了,所以我们换个能用的代理,总之通过就好了~
最后我们测试通过,这是现在的 check_ip 代码

def _check_ip(self, timeout):
    """Check IP return the proxy or None

    Returns:
        A dict or None: if the result is True(the addr can be a proxy)
            the result is the dict like {'http':'http://xx.xx.xx.xx:xx',
                                         'https':'https://xx.xx.xx.xx:xx'}
            And if the https proxy can't be used the key named 'https',
            Well, if the result is False(the addr can't be used as a proxy)
            the result is None"""

    headers = {}
    headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36'

    check_ip_http = 'http://1212.ip138.com/ic.asp'
    check_ip_https = 'https://1212.ip138.com/ic.asp'

    addr_proxy = {}
    addr_proxy['http'] = 'http://' + self.target
    addr_proxy['https'] = 'https://' + self.target

    result = {}

    http_rsp = ''
    try:
        http_rsp = requests.get(check_ip_http, proxies=addr_proxy, timeout=timeout,
                                headers=headers).text
    except:
        pass

    if self.ip in http_rsp:
        result['http'] = 'http://' + self.target

    https_rsp = ''
    try:
        https_rsp = requests.get(check_ip_https, proxies=addr_proxy,
                                 verify=False, timeout=timeout,
                                 headers=headers).text # close https verify
    except:
        pass

    if self.ip in https_rsp:
        result['https'] = 'https://' + self.target

    if result.has_key('http') or result.has_key('https'):
        return result
    else:
        return None

大家看 已经要比原来的代码看起来美观很多了是不是?下面是我们的测试结果:

C:\xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx>python check_proxy.py
..
----------------------------------------------------------------------
Ran 2 tests in 6.030s

OK

可是适当修改测试用例,添加一些说明:

API test
API test success!
.function test
function test success!
.
----------------------------------------------------------------------
Ran 2 tests in 7.545s

OK

当然 看到最后的测试成功,大家还是有很多细节需要改动的,那么经过轮番的测试,修改循环,我的这个检测模块已经感觉不错了,至少现在是满足我们所有的东西了。 具体的代码呢,可以见 github 地址。

0×04 接下来?

那么收工之后呢,既然我们要做代理扫描器,针对单个的 IP:PORT 的地址进行代理检测已经完成了,那么我们接下来要考虑的事情,就是,提供一个大的 IP 段,然后可以对这个 IP 段中的 IP 进行代理检测,然后再把结果汇总,看起来很简单么?是吧!
其实并不是这样的,我们要考虑的问题还有特别多,比如:并发我们怎么处理?我们如何筛选高质量的 IP 段?(至少我们得知道国外和国内的 IP 段不同对吧?)我们扫描到的代理,如何使用?还有,直接扫描太慢了,有更快的方法么?

当然,上面的问题,我们在后面解决,包括针对渗透测试 Python 编程的各种细节,我都会在后面讲给大家,希望对大家有帮助~

总结:

当然今天讲了看起来很多的东西,其实也并没有多少,同样大多东西需要读者亲手去做了才会有体会。那么我反过来再来说一下之前有读者问到的问题:真正要开始写一个工具的时候,第一步或者说第一行代码应该是什么呢?那么现在,大家应该懂了吧,从测试用例开始一个功能模块一个功能模块开始写。
当然如果想要详细了解 TDD 这种开发思想的话,你可以去看一下《Python Web开发:测试驱动方法》这本书,很详细的讲了 TDD 的开发思想,当然我只是把他用在了渗透工具的开发上了。
那么当然,这一篇文章显然还没有结束,关于 Python 编程的一大堆东西,准备在下一篇文章中分享给大家。

笔者水平有限,希望能对大家有所帮助。

所有代码地址:GITHUB 地址: https://github.com/VillanCh/pr0xy

*本文原创作者:VillanCh

未经允许不得转载:安全路透社 » TDD 方法开发渗透测试工具:代理扫描器(第一集)

赞 (0)
分享到:更多 ()

评论 0

评论前必须登录!

登陆 注册