TDD 方法开发渗透测试工具:代理扫描器(第一集)
作者:admin | 时间:2016-12-10 19:00:46 | 分类:黑客技术 隐藏侧边栏展开侧边栏
前言
之前在 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:
那么如果成功了的话
大家看到红色箭头了么?我们想要在程序中使用这项功能,不妨可以去 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,