版权信息

COPYRIGHT

书名：Python黑帽子：黑客与渗透测试编程之道（第2版）
作者：【美】Justin Seitz，Tim Arnold
译者：林修乐
出版社：电子工业出版社
出版时间：2022年4月
ISBN：9787121430695
字数：268千字

版权方：电子工业出版社有限公司

版权所有·侵权必究

内容简介

本书是畅销书《Python黑帽子：黑客与渗透测试编程之道》的再版，介绍Python是如何被运用在黑客和渗透测试的各个领域的：从基本的网络扫描到数据包捕获，从Web爬虫到编写Burp插件，从编写木马到权限提升等。书中的很多实例都很有启发意义，比如HTTP通信数据中的人脸图像检测，基于GitHub进行C&C通信的模块化木马，浏览器的中间人攻击，利用COM组件自动化技术窃取数据，通过进程监视和代码插入进行提权，通过向虚拟机内存快照插入shellcode实现木马驻留和权限提升等。学习这些实例，读者不仅能掌握各种Python库的应用和编程技术，还能拓宽视野，培养和锻炼自己的黑客思维。阅读本书时，几乎感受不到一般技术图书常有的枯燥和乏味。

与第1版相比，本次再版最大的工程在于对示例代码的全方位升级。两位作者不仅将示例代码从Python 2升级到Python 3，还对编码风格进行了改善。此外，他们还用了一些从Python 3.6版本引入的新语法，并增补了一些新的知识点，比如上下文管理器的用法、BPF语法，等等。

本书适合有一定Python编程基础的安全爱好者、计算机从业人员阅读。对于正在学习计算机安全专业，立志从事计算机安全行业，成为渗透测试人员的人来说，本书更是不可多得的参考书。

译者序

在网络安全的世界里，需求、场景、时机，往往瞬息万变。一个今天还能打遍天下的0day漏洞，可能明天就会被全网紧急修复；一套一小时前还在线的业务系统，可能下一秒就被蓝队拔了网线。在这种争分夺秒的场景下，Python这样的语言堪称“最好的伙伴”。

Python简单、快捷，效率惊人，写一行代码就能直接看到结果。它拥有非常活跃的开发者社区和丰富的第三方生态，用几行代码就能跨平台实现各种复杂功能。它的动态类型设计虽然在构建大型项目时会导致你写出很烂的代码，但是在攻防场景中却会给你美好顺滑的编程体验——写百十来行的攻击代码就罢，今天打完这一场，明天这些代码就进垃圾桶，管它参数类型是str还是int呢？

对于畅销书《Python黑帽子：黑客与渗透测试编程之道》的再版，两位作者投入了很大的精力，将书中的示例代码进行了全方位的升级，除了引入的Python 3新语法、新框架之外，你能明显地感受到示例代码的质量有了很大提升。前一版中的示例代码看起来像是为了应付需求而随手写的小脚本，而这一版中的示例代码则更像是正经的教学代码或开源项目代码。

本书讨论的话题都非常基础，但也都相当实用，适合有一定Python基础、初学信息安全的同学阅读。亲手实现书里的示例能使你感受到快乐和成就感，但是，如果你想备战CTF拿名次的话，不推荐选择这本书作为参考，因为其定位更偏向于拓展视野和培养兴趣，内容跟CTF的比赛范围几乎没什么重叠。建议“赛棍们”还是好好刷题，专心学习“赛棍”专用读物。

由于水平有限，翻译中难免出现一些错漏和表达不准确的地方，欢迎读者批评指正。

Gh0u1L5
腾讯玄武实验室研究员
2021年7月于北京

中文版推荐序一

20年前，我刚开始研究网络安全的时候，社区里最流行的脚本语言是Perl。当时，一个Exploit如果不是用C写的，八成就是用Perl写的，没人听说过Python。

那时候Python已经诞生近10年了。其实Python几乎和Perl一样古老。Perl的第一个版本是1987年发布的，而Python的发布时间只比它晚了4年。

但20年后的今天，网络安全社区里已经几乎看不到Perl的影子，人人都在用Python。

一种编程语言的兴衰，固然和其自身有关，但也和技术环境的变化有关，还有一点运气在里面。无论如何，一旦天平开始向一边倾斜，就会产生很大的势能。因为编程语言不仅是人和计算机对话的语言，也是编程者之间沟通的语言。语言的力量是巨大的，可以让我们建起巴别塔。所以技术生态一旦成熟，就会具有强大的生命力。

今天，Python已经成为网络安全社区里最重要的语言之一，而且地位非常稳固。无论是研究Web、操作系统、网络协议，还是硬件或者无线，都可以在社区里找到现成的模块和代码样例。不仅如此，甚至Python自身都已经成为网络攻击的目标——出现了向PyPI仓库投毒的攻击方式。

很多讲编程的书多少都会有些枯燥，但《Python黑帽子：黑客与渗透测试编程之道》（第2版）这本书完全不会。因为它其实并不是在谈Python语言本身，而是以Python作为线索在讲网络安全攻防。也正因为是以Python作为线索的，所以阅读这本书不仅可以学到攻防知识，还能学到怎么用Python去实现。

我还记得7年前《Python黑帽子：黑客与渗透测试编程之道》的第1版发行后，很快就在网络安全社区里流行起来。除了初学者们将其看作必备的入门书籍，一些已经工作了很多年的老家伙也会读。因为即使书中的网络安全知识对我们来说已经并不陌生，但知道如何以Python为工具去运用这些知识，可以帮助我们更高效地完成工作，也能使我们更顺畅地和社区成员交流。

tombkeeper

中文版推荐序二

本书读起来很顺畅，覆盖了黑客或渗透工程师常用的很多技巧。其特点是，剖析技巧的本质，然后用Python的内置模块或优秀的第三方模块来实现。

Python是一门非常酷的主流语言，拥有优美的编码风格、顽强的社区与海量优质的模块，如果看到一段代码写得很好，我们会说：“Pythonic！”这本书用Python来实现渗透测试中用到的各类技巧与工具，让人不得不说一句：“Pythonic！”

可以看出作者有丰富的渗透测试经验与Python编程经验，感谢作者能把自己的经验如此清晰地分享出来，也感谢出版社能将这本书引入国内。

这本书的发行，会让更多人投身Python黑客领域，不再是只会使用他人工具的“脚本小子”，在必要时刻，也能用Python打造属于自己的漏洞利用工具。

关于Python有句流传甚广的话：“人生苦短，快学Python”。是的，人生苦短，如果你立志成为一名真正的黑客，Python值得你掌握，这本书是一个非常好的切入点。

余弦
知道创宇技术副总裁

中文版推荐序三

在日常工作中，Python已经成为我最常用的语言，其代码简洁、高效，同时拥有强大、丰富的第三方库，往往起到事半功倍的效果，极大提高了我的工作效率。

在渗透测试过程中，收集目标的信息、对漏洞进行模糊测试、利用漏洞、提升权限、部署后门等，对渗透测试人员来说都是重复繁杂的工作。幸运的是，这些基本上都可以利用Python来实现自动化、工具化。在这本《Python黑帽子：黑客与渗透测试编程之道》（第2版）中，作者通过渗透实战，从多个维度向读者阐述了Python如何被用在黑客和渗透测试的各个领域。相信本书能够给那些想要利用Python来提升自身水平的读者带来收获。

从Python开始，培养和锻炼自己的黑客思维。本书值得拥有！

张瑞冬（Only_Guest）
无糖信息CEO

中文版推荐序四

曾经去高校宣讲，被问得最多的问题就是，如何成为一名黑客。而成为一名黑客高手，是我们这批从事安全技术的人的梦想。

那么，如何成为高手呢？两个秘诀：持之以恒和动手实践。

记得刚刚接触计算机时，机缘巧合之下我买了本安全技术杂志月刊，但是由于水平所限，里面的技术文章一篇都看不懂。不过我每期都买来看，大约持续了半年，慢慢地发现自己能够看懂了，后来甚至还可以在杂志上发表文章、发布黑客工具。就这样坚持着，我终于走进了安全行业。

“纸上得来终觉浅，绝知此事要躬行”，成为黑客高手的另一个秘诀就是要多实践。实践就一定会涉及开发自己的工具或者优化别人的代码，所以我们必须精通一门甚至多门脚本语言。Python就是这样一门强大的脚本语言，很多知名的黑客工具、安全系统框架都是用Python开发的。比如，大名鼎鼎的渗透测试框架Metasploit、功能强大的fuzz框架Sulley、交互式数据包处理程序Scapy都是用Python开发的，基于这些框架，我们可以扩展出自己的工具（多学一些总是好的，在这里也不必争论是Python好还是Perl好这样的问题）。

就我个人的经验来看，与实践结合是快速学习相关能力的路径。这本书就从实战出发，基于实际攻防场景讲解代码思路，能够让读者快速了解和上手Python，进行黑客攻防实战，所以特别推荐给大家。

知易行难，大家在读书的同时不要忘记实践：先搞懂原理，再根据实际需求写出一个强大的Python工具。

胡珀（lake2）
腾讯安全中心副总监

中文版推荐序五

几乎所有网络安全经典工具，包括调试器、渗透工具、取证工具、报文分析等都支持使用Python语言编写功能插件。不会编写程序的黑客是一个假黑客，最多是一个顶级的工具小子；而最适合网络安全的编程语言，莫过于Python，可以说不会用Python编程的渗透测试工程师是没有前途的。其实，知道创宇公司从2007年成立之时，就要求除了前端使用JavaScript和少量例外，全公司只允许使用Python语言。这门语言简单易学，各种第三方功能包十分丰富且强大。这本书的编程知识涉及网络安全的方方面，从漏洞的POC到网络通信，从攻击取证到数据报文分析，非常全面，十分实用，是网络安全从业者不可多得的一本好书。鄙人不敢藏私，推荐给大家。

杨冀龙
知道创宇CTO

中文版推荐序六

Python是网络安全领域的编程利器，在分秒必争的CTF赛场中拥有统治地位，在学术型白帽研究团队和业界安全研究团队中也已经成为主流编程语言。本书作者在其畅销书《Python灰帽子：黑客与逆向工程师的Python编程之道》之后，再次强力推出姊妹篇《Python黑帽子：黑客与渗透测试编程之道》，以其在网络安全领域，特别是漏洞研究与渗透测试方向上浸淫十数年的经验，献上又一本经典的Python黑客养成手册。我非常高兴地看到译者以精准的翻译、专业的表达将本书内容原汁原味地奉献给国内的读者。

诸葛建伟
清华大学副研究员
蓝莲花战队联合创始人及领队
XCTF联赛联合发起人及执行组织者

中文版推荐序七

我们一直认为，一个合格的安全从业者必须有自己动手编写工具和代码的意愿与能力。在这个安全攻防和业务一样日趋大数据化、对抗激烈化又隐蔽化的年代，攻防双方都必须有快速实现或验证自己想法的能力。选择并学习使用一个好的工具会起到事半功倍的效果。

Python则是目前特别适合这种需求的语言。平缓的学习曲线、胶水语言的灵活性和丰富的支持库使其天然成为攻防双方均可使用及快速迭代的利器，几乎可以覆盖安全测试的方方面面。求学时，我使用Scapy（本书中有详细的介绍）和PyQt库编写了Wifi嗅探工具WifiMonster；在我参加的CTF比赛中，基本上所有的漏洞利用工具都是用基于Python的pwntools和zio库编写的；在Keen，我们的很多fuzzer和静态分析器也都是用Python编写的。

但令人遗憾的是，目前国内的高校很少有将Python及其在安全领域方面的应用列入计算机和信息安全专业培养计划的，市面上也缺乏相关图书供从业人员学习。本书填补了这个空白：作者从逆向和漏洞分析挖掘的角度编写了《Python灰帽子：黑客与逆向工程师的Python编程之道》后，又从渗透测试和嗅探、取证的角度编写了这本书，介绍Python在这些方面的应用和相关库的使用。本书译者在安全领域有丰富经验，能保证翻译质量。

相信读者会从本书中受益良多。

何淇丹
a.k.a Flanker，Keen Team高级研究员

中文版推荐序八

接触信息安全之前，Python就已经是我的常用语言了，它能满足我日常工作的所有需求。因为对Python已经有一定了解，接触信息安全以后，借助这一利器，我在信息安全领域的探索进行得很顺利。

老牌大黑客查理·米勒说的没错，脚本小子和职业黑客的区别是黑客会多编写自己的工具而少用别人开发的工具。从事Web渗透测试相关工作以及参加CTF竞赛的时候，我基本上都是用自己写的Python脚本来实现目的的：扫描及收集目标的信息，测试大量已知漏洞是否存在，自动发现SQL注入、XSS攻击点，对攻击进行抓取、截获和重放，在比赛中大量部署后门进行控制等。

Python中有大量第三方库，可以让你从无关的工作中脱身，专心实现自己所需要的功能（有时你甚至会发现有人已经很好地实现了你所需要的功能），不被杂乱的事务所困扰。在Web渗透测试这种重视效率的工作中，用Python快速地把自己的需求转换成能运行的程序，实在是令人兴奋的一件事。

作者在本书中所给出的大量样例和技巧，足以让那些想利用Python迅速提高Web渗透测试水平的人得到很大的帮助。但请记住，一定要动手实践。

只有动手实践，才能真正体会到本书的精华所在。

Hacking the planet by Python！

陈宇森
北京长亭科技有限公司联合创始人
蓝莲花战队核心成员
BlackHat 2015讲者

致我美丽的妻子Clare，我爱你。

——Justin

对第1版的赞誉

“又一本Python方面的力作！书中的很多程序只要稍加拓展，至少能用十年，对于一本信息安全著作来说非常难得。”

Stephen Northcutt，SANS Technology Institute联合创始人

“将Python活用于网络安全领域的一部杰作。”

Andrew Case，Volatility项目核心开发者，The Art of Memory Forensics作者

“若你真的掌握了黑客式思维，那么一丝灵感就足以让你大有所成。而Justin Seitz正是能为你提供一大把灵感的人。”

白帽黑客

“不管你是想成为专业的黑客/渗透测试工程师，还是想简单了解一下他们的工作，都应该读一读这本书。它紧凑、专业，令人大开眼界。”

Sandra Heny-Stocker，IT World

“绝对是一本值得推荐的书，非常适合有一定Python基础的信息安全从业人员。”

Richard Austin，IEEE Cipher

关于作者

Justin Seitz是一位业界知名的信息安全研究员、开源情报（OSINT）分析师，以及加拿大信息安全公司Dark River Systems的联合创始人。他所做的工作曾被Popular Science、Motherboard、《福布斯》等杂志报道。Justin撰写过两本讨论黑客工具开发的书，创立了开源情报训练平台AutomatingOSINT.com，还开发了一套开源情报收集软件Hunchly。此外，Justin还是独立调查组织Bellingcat的调查员、国际刑事法院的技术顾问，以及华盛顿高级国防研究中心（C4ADS）的受邀专家。

Tim Arnold是一位专业的Python程序员、统计学家。他曾在北卡罗来纳州立大学工作过很长时间，是一位备受尊敬的国际演讲者与教育工作者。除了个人事业取得建树，他还通过撰写盲文数学文档等方式，帮助世界各地缺乏教育资源的群体获得更好的教育。

之后，Tim前往SAS公司工作，担任了数年的首席软件工程师，负责开发一款技术/数学文档分发系统。他还是Raleigh ISSA的协会成员、国际统计学会的专家顾问。在业余时间，Tim喜欢进行独立科普工作，向新用户讲解各种信息安全和Python知识，帮助他们掌握更艰深的技术。Tim与妻子Treva一起居住在北卡罗来纳州，家里养着一只臭脾气的鹦鹉Sidney。如果想要找Tim的话，可以通过他的Twitter账号@jtimarnold联系。

关于技术编辑

从Commodore公司还在兜售PET计算机和VIC-20计算机的古老年代开始，Cliff Janzen就已经与信息技术日夜为伴了，甚至可以说是沉迷其中！Cliff工作日的大部分时间都花在管理和指导手下的那支优秀的安全团队上，他如饥似渴，流连于技术之中，解决各种各样的难题——从安全策略审计和渗透测试，到各种应急响应任务。他常感慨自己如此幸运：既能找到一份合乎兴趣的工作，又能遇到一位支持自己事业的妻子。他很感激Justin从《Python黑帽子：黑客与渗透测试编程之道》（第1版）起就邀请他参与这部杰出的作品，也感谢Tim费尽功夫带他用上Python 3。最后，他还要向No Starch出版社优秀的工作人员表达特别的感谢。

推荐序

从我为轰动一时的《Python黑帽子：黑客与渗透测试编程之道》（第1版）作序以来，已经过去6年了。这些年，世界变了不少，但有一件事始终没变：我每天还是要写一大堆Python代码。在计算机安全领域，为了应付各式各样的任务，你每天仍要跟用各种语言编写的工具打交道，比如用C语言写的内核漏洞利用程序，用JavaScript写的JavaScript fuzzer，或是用Rust之类的“花哨”语言写的代理。然而，Python依然是这个圈子里的“得力干将”。在我看来，它仍是最易上手的编程语言，有数不胜数的第三方库；如果需要快速编写代码完成复杂任务，轻松地化繁为简，那么Python是最佳选择。有大量的安全工具和漏洞利用程序仍然是用Python编写的，比如CANVAS这样的漏洞利用框架，以及Sulley这样经典的fuzzer。

在《Python黑帽子：黑客与渗透测试编程之道》（第1版）发行之前，我就已经用Python写过许多fuzzer和漏洞利用程序（攻击代码），攻击过的目标包括Mac OS上的Safari浏览器、苹果手机、安卓手机，甚至还有游戏《第二人生》（你可能需要上网搜一搜这个游戏）。

我还跟Chris Valasek一起写过一段挺特别的攻击代码，它能够远程感染包括2014款Jeep切诺基在内的多款车型。这段攻击代码当然也是用Python写的（基于dbus-python模块）。我们开发了大量工具，能够在感染车辆之后远程控制它的方向盘、刹车和油门。上述这些工具，也全部是用Python开发的。从某种意义上，你甚至可以说是Python害菲亚特克莱斯勒公司召回了140万辆汽车。

如果你喜欢做一些“修修补补”类的信息安全小项目，那么Python是一门非常值得学习的语言，因为Python中有大量的逆向工程框架、漏洞利用框架供你使用。现在，只要那帮Metasploit开发者能恢复神智，弃Ruby投Python，我们的开发者社区就能迎来统一。

此次对这本经典书的升级，Justin和Tim把书里的所有代码都更新到Python 3版本。就个人而言，我是个想要死守Python 2的老顽固，但是当所有第三方库都升级到Python 3时，我也得去接受它。这个新版很好地覆盖了大量知识点，它们都是摩拳擦掌的年轻黑客们入门必备的知识，从如何收发网络数据包，到网络应用审计/攻击所需的各项技能，应有尽有。

总之，《Python黑帽子：黑客与渗透测试编程之道》（第2版）是一本由从业多年的专家精心撰写的好书，无私分享了他们一路走来学到的许多诀窍。也许它不会让你马上变成像我一样的绝世高手，但它绝对能够让你走上一条正确的道路。

记住，脚本小子和职业黑客之间最大的区别，就是前者只会用别人写的工具；而后者，能创造自己想要的一切。

Charlie Miller
安全研究员
密苏里州圣路易斯

前言

Python黑客、Python程序员，随便你怎么称呼我们。Justin大部分的时间都在做渗透测试，这项工作要求快速开发出各种Python工具，并以最终成果为导向，不一定兼顾美观、性能，甚至稳定性之类的细节。而Tim的口头禅则是“先让代码能用，然后使其易懂、高效”。如果你的代码写得简洁漂亮，不仅能方便别人读懂，你自己隔了几个月再看也会很轻松。通过阅读本书，你将了解我们的编程风格：我们以实现各种又快又脏的巧妙方案为最终目标，而编写干净易懂的代码是我们抵达这个目标的手段。希望这种编程哲学和风格也能对你有所帮助。

自《Python黑帽子：黑客与渗透测试编程之道》（第1版）问世以来，Python世界发生了不小的变化。Python 2于2020年1月被停止维护，Python 3成为目前编程与教学的推荐环境。因此，在本书中我们将所有代码迁移到Python 3，并用上了最新的包与第三方库。我们还用了一些Python 3.6和Python 3以上的版本才引入的语法，比如Unicode字符串、上下文管理器、f-string等。最后，我们还在本书中增补了一些编码和网络编程的知识点，比如上下文管理器的用法、BPF语法，以及ctypes和struct库的比较等。

在阅读本书的过程中，你会发现每个知识点都没有讲太深，这是我们有意为之的。我们希望教你一些基础知识，再加一点简单的技巧，为你进入黑客开发领域打下基础。与此同时，我们在本书里塞了不少拓展阅读材料、有趣的想法和课后作业，以此来启发你找到自己的方向。我们鼓励你去实践这些想法，也欢迎你分享“造轮子”的体验。

像所有技术图书一样，本书会带给不同水平的读者相当不同的体验。有的人可能只需要翻看自己急需的内容，而有的人则需要从头到尾读完全书。如果你是一名初级至中级水平的Python程序员，建议你按章节顺序通读本书，这个过程能让你学到不少东西。

作为开场，我们将在第2章介绍网络方面的基础知识，在第3章仔细讲原始socket，在第4章介绍如何使用Scapy开发有趣的网络工具。之后我们将讨论如何攻击Web应用。在第5章我们会先教你编写一些典型的Web黑客工具，然后在第6章用鼎鼎大名的Burp Suite来编写一些攻击插件。再接下来，我们花大量的篇幅讨论木马，从第7章的基于GitHub服务的C&C通信，一直讲到第10章的Windows提权技术。在最后一章我们学习Volatility内存取证库，它既能帮你理解防守方是如何思考的，又能让你明白如何以子之矛，攻子之盾。

我们会尽可能地让书中的代码样例及解释说明文字保持简明扼要的风格。如果你刚刚开始接触Python，建议你动手敲书里的每一行代码，好好锻炼一下写代码的手感。书中的所有源代码都可以在链接1所指的页面[1]上找到。

现在，让我们出发吧！

[1]请访问http://www.broadview.com.cn/43069下载本书提供的附加参考资料，如正文中提及参见“链接1”“链接2”等时，可在下载的“参考资料.pdf”文件中查询。

致谢

Tim在此特别感谢妻子Treva，感谢她长久以来的支持。如果不是若干巧合，他不会有机会参与本书的写作。感谢Raleigh ISSA，尤其是协会里的Don Elsner和Nathan Kim，他们支持并鼓励他用《Python黑帽子：黑客与渗透测试编程之道》（第1版）作为教材开设了一门课程。正是这门课的授课经历，让他喜欢上这本书。感谢当地的黑客社区，特别是Oak City Locksports的朋友们，感谢你们的鼓励，感谢你们提供给Tim一份益于思辨的环境。

Justin想要在此感谢他的家人——他美丽的妻子Clare，5个孩子Emily、Carter、Cohen、Brady和Mason，感谢他们在他写书的一年半时间里给予的鼓励与宽容。Justin深爱着他们。感谢各位网友，还有一起喝酒、欢笑和互发推文的OSINT社区的朋友们，感谢他们容忍Justin一天到晚不停抱怨。

另一个大大的感谢留给No Starch出版社的Bill Pollock，还有耐心的编辑Frances Saux，感谢他们对本书的完善。感谢No Starch出版社的其他工作人员，尤其是Tyler、Serena和Leigh，感谢他们为本书付出的辛勤劳动，Tim和Justin都对此非常感激。感谢本书的技术编辑Cliff Janzen，在整个过程中他提供了绝对顶尖的技术支援。想写信息安全类图书的人，都应该邀请他来进行技术审校。他的水平真的是超凡脱俗。

1　设置你的Python环境

这是本书最无聊却不可或缺的一章，我们会快速讲一下如何搭建一个Python编程测试环境。在这一节速成课上，我们将讲授如何搭建Kali Linux虚拟机（VM），如何创建Python 3虚拟环境，以及如何安装一套好用的集成开发环境（IDE），帮你备齐开发代码所需的所有东西。学完本章之后，你应该就能尝试实践后续章节的习题和示例了。

在开始学习之前，如果你还没有安装VMware Player、VirtualBox或者Hyper-V这样的Hypervisor虚拟机软件，请任选一款，下载并安装。我们也建议你准备好Windows 10的虚拟机，Windows 10的试用版虚拟机镜像可以从微软开发者网站[1]下载。

安装Kali Linux虚拟机

Kali脱胎于BackTrack Linux，是由Offensive Security团队设计的一款渗透测试操作系统。Kali里面预装了一批常用工具，而且由于它是基于Debian Linux开发的，所以有非常丰富的软件和第三方库可供使用。

我们会搭建一台Kali访客虚拟机（Guest VM）。也就是说，我们会下载一份Kali虚拟机镜像，将它运行在主机（Host）上。你可以从Kali的官网[2]下载Kali虚拟机镜像，并把它安装到你之前选择的虚拟机软件里。具体的安装过程可以参考Kali的官方文档[3]。

等你完成所有的安装步骤后，应该就能进入完整的Kali桌面环境了，图1-1所示为Kali Linux桌面。

图1-1　Kali Linux桌面

下载的镜像可能没有包含打包后所公布的重要更新，所以我们先更新一下Kali系统。打开Kali的shell窗口（Applications ► Accessories ► Terminal），执行以下命令：

配置Python 3

首先，我们需要确保Kali中安装了正确的Python版本（本书使用的是Python 3.6及以上版本）。在Kali的shell里启动Python，查看输出结果：

我们的Kali虚拟机应该会输出这样的结果：

这个结果并不完全是我们想要的。我们写本书时，Kali里搭载的默认Python版本还是2.7.18。但这个问题其实很好解决，因为Kali里面同时也预装了Python 3。

这里列出的Python版本是3.7.5，如果你电脑上显示的版本低于3.6，就用以下命令更新一下Python 3：

接下来，我们将在一个虚拟环境里使用Python 3。所谓的虚拟环境其实就是一个文件夹，里面存放了完整的Python软件包和你额外安装的第三方包。它是Python开发人员最基础的工具之一。有了它，就可以把需求不同的软件项目隔离开来。比如，你可以给所有网络流量解析项目开设一个虚拟环境，给所有二进制文件分析项目再另开一个虚拟环境。

这些相互隔绝的虚拟环境，能让你的软件项目更加整洁，每个环境有自己的一套模块和依赖关系，不会干扰其他项目的依赖管理。

现在，我们来创建一套虚拟环境。首先，安装python3-venv软件包：

接下来就可以创建虚拟环境了。创建一个新目录，然后把虚拟环境放在里面：

第一行命令在当前目录下创建了一个名为bhp的文件夹。第三行命令向Python 3传递了-m选项来调用venv包，接着传递了要创建的环境名，这样就能创建一个新的虚拟环境。在示例中使用的环境名是venv3，但你可以使用任何想要的名字。整个环境的所有脚本、包和Python可执行文件，都会被放在刚才创建的venv3文件夹下。接着，运行activate脚本激活这个环境。注意，当环境被激活后，命令行界面的提示符也跟着变了，提示符的开头多了环境的名字（venv3）。之后如果想退出虚拟环境，只需要执行deactivate命令。

现在，你已经安装了Python，并且激活了一个虚拟环境。因为创建虚拟环境时用的是Python 3，所以启动Python的时候，不再需要完整地输入“python3”，只需要输入“python”就够了，毕竟我们的虚拟环境里安装的版本就是Python 3。换句话说，在激活环境后，你执行的每行Python命令都是与当前的虚拟环境相匹配的。请注意，如果你使用的Python版本与这里介绍的不同，书里的一些示例代码可能会无法运行。

我们可以使用pip命令在虚拟环境里安装Python包。此命令用起来跟apt包管理器差不多，允许你直接把Python包安装到虚拟环境里，无须费劲地手动下载、解压及安装。

你可以像这样用pip命令搜索并安装软件包：

我们来做一个简单的测试——安装lxml库（在第5章会用它来编写网页爬虫）。在你的终端里执行以下命令：

你应该会看到终端里输出一堆信息，告诉你这个库已经下载及安装完了。接下来，打开Python的shell，验证lxml库是不是真的装好了：

如果看到了报错，或者终端里显示的Python版本是Python 2，那就检查一下前面的步骤，并检查你的Kali Linux是不是最新版本。

请留意，本书大部分的示例代码，都可以在macOS、Linux、Windows等不同的系统里编写与调试。你可能想给其他独立项目或其他章节创建单独的虚拟环境。本书有几章的代码是仅限于Windows环境的，我们会在章节的开头提醒你。

既然我们已经设置好攻击虚拟机和Python 3虚拟环境，接下来就来安装Python集成开发环境吧。

安装IDE

IDE（集成开发环境）会提供一整套编程专用工具，里面一般包含代码编辑器（能够对语法做高亮标记或自动检查错误）和调试器。IDE能够让你更轻松地编写和调试代码，但它并不是Python编程所必需的工具。编写一些简单的测试程序时，你大可以选择任何文本编辑工具（比如vim、nano、Notepad或emacs）。但对于更大、更复杂的项目，IDE于你将会是一个巨大的助力，它能帮你找到定义后未使用的变量，找到拼写错误的变量名，又或是找到你忘记导入的包。

在最近一次的Python开发者问卷调查中，最受欢迎的IDE前两名分别是PyCharm（既有商业版也有免费版）和Visual Studio Code（免费）。Justin特别爱用WingIDE（既有商业版也有免费版），而Tim用的是Visual Studio Code（简称VS Code）。以上三种IDE都支持Windows、macOS和Linux平台。

你可以从Jetbrains公司的网站[4]下载PyCharm，从WingIDE的官网[5]下载WingIDE，还可以直接在Kali里执行命令安装VS Code：

或者，从官网[6]下载最新版本的VS Code，然后用apt-get命令来安装：

这里文件名中的版本号可能和你下载的文件的不一样，所以务必检查你安装的VS Code包的文件名与下载的VS Code包的文件名是否对得上。

保持代码整洁

不管你选用什么工具来开发程序，都应该遵守代码格式规范。代码格式规范会提供各种建议，帮助你提高所写的Python代码的可读性和风格一致性，使你日后能更轻松地读懂自己的代码，或是在你分享代码时帮助他人更好地理解你的代码。Python社区已经有一份成文的格式规范，名叫PEP 8。你可以在Python的官网[7]上看到它的全文。

除了几处小小的不同之外，本书代码大体上都遵循PEP 8规范。你会发现本书代码遵循以下模式：

在程序的开头，我们会导入需要的包。第一个导入块❶将全是from XXX import YYY的格式，每行按字母顺序排列。

同样，对于整包导入❷我们也会按照字母顺序排列。这个顺序能让你一眼就看出某个包有没有被导入，而且保证不会重复导入相同的包。这种形式能保持代码整洁，减少重读代码时所需的思考时间。

接下来是函数定义❸，以及类定义❹（如果有的话）。有的程序员倾向于只用函数不用类。这里倒是没有什么硬性规定，但如果你发现自己正在尝试用全局变量记录某些状态，或是需要把相同的数据结构传递给多个函数，那么写一个类来重构程序可能会让代码变得更简洁。

最后一部分是放在结尾的main代码块❺，它能让你以两种不同的方式调用程序。第一种是从命令行里启动程序，这时候模块的内部名是__main__，因此main代码块就会执行。例如，你把代码保存到名为scan.py的文件中，就可以像下面这样直接在命令行里运行它：

这条命令会加载scan.py里的所有函数和类，然后执行main代码块。你将会看到屏幕上输出hello。

第二种方式，就是从另一个程序导入你的代码，这不会带来任何副作用（即main代码块不会执行）。例如，你可以在另一个程序里写：

因为这时模块的内部名是Python包的名字scan，而不是__main__，所以你能调用它里面定义的所有函数和类，但main代码块却不会执行。

你可能还注意到了，我们尽量避免使用含义太宽泛的名字来命名变量。变量名的含义越具体，程序读起来也就越好懂。

现在，你应该已经备齐了虚拟机、Python 3、虚拟环境和IDE。让我们进入真正有意思的部分吧！

[1]链接2。

[2]链接3。

[3]链接4。

[4]链接5。

[5]链接6。

[6]链接7。

[7]链接8。

2　基础的网络编程工具

无论现在还是将来，网络对于黑客而言永远是最重要的竞技场。攻击者总是想方设法通过简单的网络访问做成几乎任何事情，比如扫描主机、注入数据包、嗅探数据、远程攻击主机等等。但是，闯入某个目标企业内网的深处后，你可能并不能为所欲为：执行网络攻击离不开必要的工具。这里没有netcat，没有Wireshark，没有编译器，甚至没有办法去安装编译器。然而有很多时候，黑客可能会惊讶地发现目标环境里安装了Python。此时，Python就是开路利器。

本章将讲授使用Python的socket库进行网络编程的基本知识（完整的socket文档请参见Python官网中的相关页面[1]）。我们会一路编写出客户端、服务端，以及TCP代理。之后我们会把这些组件组装成独创的、自带远程命令功能的netcat。本章是后续章节的基础，在后面的章节里，我们还会开发主机发现工具、跨平台嗅探工具、远程木马框架等等。我们开始吧。

Python网络编程简介

Python开发人员可以使用各种第三方工具来创建网络客户端和服务端，但所有这些第三方工具的核心其实都是socket模块。这个模块提供了所有必需的接口，让你可以快速开发出TCP/UDP客户端、服务端，直接调用原始socket等等。想要攻破目标机器并保持对其的访问权限，其实靠这个socket模块就够了。我们先从编写客户端和服务端开始吧，这两者将会是你最常编写的网络程序。

TCP客户端

在渗透测试过程中，经常需要创建一个TCP客户端，用来测试服务、发送垃圾数据、进行fuzz等等。如果黑客潜伏在某大型企业的内网环境中，则不太可能直接获取网络工具或编译器，有时甚至连复制/粘贴或者连接外网这种最基本的功能都用不了。在这种情况下，能快速创建一个TCP客户端将会是一项极其有用的能力。多说无益，我们开始编写代码。下面是一段简单的TCP客户端代码：

首先创建一个带有AF_INET和SOCK_STREAM参数的socket对象❶。AF_INET参数表示我们将使用标准的IPv4地址或主机名，SOCK_STREAM表示这是一个TCP客户端。然后，我们将该客户端连接到服务器❷，并发送一些bytes类型的数据❸。最后一步，接收返回的数据并将其打印到屏幕上❹，再关闭socket。这是一个最简单的TCP客户端，也将是你最常写的一段代码。

以上代码隐含的几个关于socket的重要假设是你一定要了解的：第一，我们假设所有的连接都会成功，不会出错或发生异常；第二，我们假设服务器期望客户端先发送数据（有的服务器会期望自己先发数据，然后等客户端回复）；第三，我们假设服务器总是能在合理的时间内回应我们。做这些假设主要是为了简化问题，尽管程序员们对于如何处理阻塞、异常之类的情况很有讲究，但渗透测试工程师却很少在其又脏又快的攻击代码里纠结于这些花里胡哨的细节，所以本章也会省略这些内容。

UDP客户端

Python UDP客户端和TCP客户端相去不远，我们只需要做两处小改动，就可以发送UDP形式的数据了：

正如你所看到的，在创建socket对象时，我们把socket类型改成了SOCK_DGRAM❶，之后只要简单调用sendto()函数❷，填好要发送的数据和接收数据的服务器地址就可以了。因为UDP是一个无连接协议，所以开始通信前不需要用connect()函数建立连接。最后一步，是调用recvfrom()函数接收数据❸。你可能注意到了，这个函数不仅会返回接收到的数据，还会返回详细的数据来源信息（主机名与端口号）。

再说一遍，我们不打算成为娴熟的网络编程工程师，只想快速、简单、可靠地处理我们每天的黑客任务。接下来，我们创建几个简单的服务端。

TCP服务端

用Python编写TCP服务端和编写客户端一样简单。你可能会想用自己的TCP服务端做一个远程代码执行工具或者代理工具（这两个需求我们将在后面实现）。我们先来编写一个标准的多线程TCP服务器，大体的代码结构如下：

作为开场，我们先指定服务器应该监听哪个IP地址和端口❶。接着，让服务器开始监听❷，并把最大连接数设为5。下一步，让服务器进入主循环中，并在该循环中等待外来连接。当一个客户端成功建立连接的时候❸，将接收到的客户端socket对象保存到client变量中，将远程连接的详细信息保存到address变量中。然后，创建一个新的线程，让它指向handle_client函数，并传入client变量。创建好后，我们启动这个线程来处理刚才收到的连接❹，与此同时服务端的主循环也已经准备好处理下一个外来连接。而handle_client函数❺会调用recv()接收数据，并给客户端发送一段简单的回复。

如果用我们之前编写的TCP客户端给服务端发送几个测试数据包，应该会看到以下输出：

就是这样！这段代码虽然非常简单，但却非常有用。在后面的几节中，我们将拓展它的功能，用它来编写一套netcat的替代品和一套TCP代理工具。

取代netcat

netcat是网络编程界的“瑞士军刀”，理所当然，很多精明的系统管理员都会将它从系统里移除。这个好用的工具对于闯入系统的攻击者来说是不错的资源。你可以用它在网络中任意读/写数据，也就意味着你能远程执行命令，四处传送文件，甚至留下一个远程shell。

我们不止一次遇到过没装netcat，却装了Python的服务器。在这种情况下，要是能创建一个简单的网络客户端和服务端用来传送文件、远程执行命令，则大有用武之地。如果你通过某个Web应用攻入了系统，那么在你“玩炸”自己的某个木马或后门之前，先“反弹”一个Python会话作为备用通道也绝对是明智之举。

编写这样的工具也是一次不错的Python编程练习，让我们开始编写netcat.py吧。

这里，我们导入了需要的所有Python库，并创建了execute函数。这个函数将会接受一条命令并执行，然后将结果作为一段字符串返回。它用到了一个我们还没讲的新库：subprocess。这个库提供了一组强大的进程创建接口，让你可以通过多种方式调用其他程序。在刚才的示例中❶，我们用到了它的check_output函数，这个函数会在本机运行一条命令，并返回该命令的输出。

现在我们创建main代码块，用来解析命令行参数并调用其他函数。

我们使用标准库里的argparse库创建了一个带命令行界面的程序❶，传递不同的参数，就能控制这个程序执行不同的操作，比如上传文件、远程执行命令，或是打开一个命令行shell。

我们编写了一段帮助信息❷，程序启动的时候如果发现--help参数，就会显示这段信息。我们还添加了6个参数，用来控制程序的行为❸。程序收到-c参数，就会打开一个交互式的命令行shell；收到-e参数，就会执行一条命令；收到-l参数，就会创建一个监听器；-p参数用来指定要通信的端口；-t参数用来指定要通信的目标IP地址；-u参数用来指定要上传的文件。因为发送方和接收方都会运行这个程序，所以传进来的参数会决定这个程序接下来是要发送数据还是要进行监听。使用了-c、-e和-u这三个参数，就意味着要使用-l参数，因为这些行为都只能由接收方来完成。而发送方只需要向接收方发起连接，所以它只需要用-t和-p两个参数来指定接收方。

如果确定了程序要进行监听❹，我们就在缓冲区里填上空白数据，把空白缓冲区传给NetCat对象。反之，我们就把stdin里的数据通过缓冲区传进去。最后调用NetCat类的run函数来启动它。

现在该下点功夫实现这些功能了，先从客户端代码开始吧。在main代码块的上方，添加如下代码：

我们用main代码块传进来的命令行参数和缓冲区数据，初始化一个NetCat对象❶，然后创建一个socket对象❷。

run函数作为NetCat对象的执行入口，它的逻辑其实相当简单：直接把后续的执行移交给其他两个函数。如果我们的NetCat对象是接收方，run就执行listen函数❸；如果是发送方，run就执行send函数❹。

现在，我们来编写send函数。

先连接到target和port❶，如果这时缓冲区里有数据的话，就先把这些数据发过去。接下来，创建一个try/catch块，这样就能直接用Ctrl+C组合键手动关闭连接❷。然后，创建一个大循环，一轮一轮地接收target返回的数据❸。在大循环里，我们建了一个小循环，用来读取socket本轮返回的数据。如果socket里的数据目前已经读到头，就退出小循环❹。接下来检查刚才有没有实际读出什么东西来，如果读出了什么，就输出到屏幕上，并暂停，等待用户输入新的内容，再把新的内容发给target❺，接着开始下一轮大循环。

这个大循环将一直持续下去，直到你按下Ctrl+C组合键触发KeyboardInterrupt中断循环❻，这时也会关闭socket对象。

接下来，我们来编写程序进行监听时所用到的listen函数：

listen函数把socket对象绑定到target和port上❶，然后开始用一个循环监听新连接❷，并把已连接的socket对象传递给handle函数。

现在我们来实现上传文件、执行命令和创建交互式命令行等功能。这样，当程序“听”到我们的指令后，就能执行相应的任务。

handle函数会根据它收到的命令行参数来执行相应的任务：执行命令、上传文件，或是打开一个shell。如果要执行命令❶，handle函数就会把该命令传递给execute函数，然后把输出结果通过socket发回去。如果要上传文件❷，我们就建一个循环来接收socket传来的文件内容，再将收到的全部数据写到参数指定的文件里。最后，如果要创建一个shell❸，我们还是创建一个循环，向发送方发一个提示符，然后等待其发回命令。每收到一条命令，就用execute函数执行它，然后把结果发回发送方。

你可能注意到，shell是收到换行符后才执行命令的，这使它能兼容原版的netcat。也就是说，你可以用它来做接收方，用原版的netcat做发送方。但是，如果你用自己写的Python客户端做发送方的话，一定不要忘记加换行符。在send函数里，你会发现我们每次读取用户输入之后，都会在结尾加一个换行符（\n）。

小试牛刀

现在我们来玩一玩刚才的程序，看看输出的情况。在终端或者cmd.exe中，以--help参数启动程序：

在Kali虚拟机上，启动一个接收方，让它在虚拟机IP地址的5555端口提供一个命令行shell[2]：

接着，在本机打开另一个终端，以客户端的模式运行脚本。注意，我们的程序会一直从stdin读取数据，读到文件结束符（EOF）才会停止。想要输入EOF的话，可以按下键盘上的Ctrl+D组合键。

可以看到，我们得到了一个典型的命令行shell。因为我们现在用的是一台UNIX主机，所以能够运行一些本地命令并收到输出结果，就如同我们正在通过SSH登录或是直接操作物理机一样。接着，使用相同的网络配置，但这次指定-e参数，让Kali虚拟机单独执行一条命令：

当你从本机连到Kali虚拟机上时，就能得到这条命令的结果：

也可以在本机上用原版netcat来连接Kali虚拟机：

最后，还可以用老派的方式，用客户端直接发送一个HTTP请求：

成了！这些虽然不是什么高级技术，但能为你打下很好的基础，让你可以“魔改”各种Python客户端和服务端socket。当然，这个程序仅涉及最基本的内容，你还需要发挥想象力来拓展和改进它。接下来，我们要编写一个TCP代理，它会在很多攻击场景里派上用场。

开发一个TCP代理

在工具箱里常备TCP代理的理由有很多：你也许会用它在主机之间转发流量，又或者用它检测一些网络软件。在企业环境里进行渗透测试时，你可能无法使用Wireshark，也无法在Windows上加载驱动嗅探本地回环流量，而网段的阻隔让你无法直接在目标机器上使用手头的工具。我们用Python构建过不少简单的TCP代理，比如接下来的这个，我们常常用它来分析未知的协议，篡改应用的网络流量，或者为fuzzer创建测试用例。

这个代理有几个可拆卸部件。总的来说，我们要编写4个主要功能：把本地设备和远程设备之间的通信过程显示到屏幕上（hexdump函数）；从本地设备或远程设备的入口socket接收数据（receive_from函数）；控制远程设备和本地设备之间的流量方向（proxy_handler函数）；最后，还需要创建一个监听socket，并把它传给我们的proxy_handler（server_loop函数）。

让我们开始吧。打开一个新文件，将其命名为proxy.py：

在开头我们导入了几个包，然后定义了一个hexdump函数，它能接收bytes或string类型的输入，并将其转换为十六进制格式输出到屏幕上。也就是说，它能同时以十六进制数和ASCII可打印字符的格式，输出数据包的详细内容。这有助于理解未知协议的格式，或是在明文协议里查找用户的身份凭证等。我们创建了一个HEXFILTER字符串❶，在所有可打印字符的位置上，保持原有的字符不变；在所有不可打印字符的位置上，放一个句点“.”。为了举例说明这个字符串里都有什么，我们来看看数字“30”和“65”对应的字符表达式。在一个Python的交互式shell里，输入：

“65”对应的字符是可打印的，而“30”对应的字符不可打印。如你所见，可打印字符的字符表示长为3个字符。我们可以利用这个性质来构造最终的HEXFILTER字符串：如果能提供可打印字符，就提供字符；如果提供不了，就给一个句点（.）。

构建HEXFILTER字符串的那条列表推导式（list comprehension）里用了一个布尔短路求值的技巧。听着可能有点花哨，下面我们详细拆解一下：对于0到255之间的每个整数，如果其对应的字符表示长度等于3，我们就直接用这个字符（chr(i)）；否则，就用一个句点（.）表示。接着，把所有内容拼接成如下字符串：

这个列表推导式给出了前256个整数对应的可打印字符[3]。现在我们可以编写hexdump函数了。首先，要保证接下来处理的是string类型的数据。如果传进来的参数是bytes类型的话，就调用decode函数将它转换为string类型❷。接着，切一小段数据，把它放到word变量里❸。然后调用内置的translate函数把整段数据转换成可打印字符的格式，保存到printable变量里❹。同样，我们把这段数据转换成十六进制格式，保存到hexa变量里。最后，把word变量起始点的偏移、其十六进制表示和可打印字符表示形式打包成一行字符串，塞进results数组❺，输出的结果如下所示：

这个函数给我们提供了实时观察代理内数据流通的方法。接下来，我们编写从代理两端接收数据的函数：

要想接收本地或远程数据，必须先传入一个socket对象。创建一个空的bytes变量buffer，用来存储socket对象返回的数据❶。我们设定的超时时间默认为5秒，如果跨国转发流量，或者网络状况很差的话，这个超时时间可能不太合适，所以，如有必要可以延长超时时间。然后创建一个循环，不断把返回的数据写进buffer，直到数据读完或者连接超时为止。最后，把buffer返回给调用方，这个调用方可能是本地设备，也可能是远程设备。

有时，你可能想在代理转发数据包之前，修改一下回复的数据包或请求的数据包。我们添加一对函数（request_handler和response_handler）来处理这种情况：

在这些函数里，可以修改数据包内容，进行模糊测试，挖权限校验漏洞，做你想做的任何事。例如，你发现某个应用在用明文传输用户凭证，就可以用这些函数把自己的用户名换成admin，看看能不能提升权限。

现在，我们插入如下代码，潜入proxy_handler函数：

这个函数实现了整个代理的大部分逻辑。首先，连接远程主机❶。接着，进入主循环之前，先确认一下是否需要先从服务器那边接收一段数据❷。有的服务器会要求你做这样的操作（比如FTP服务器，会先发给你一条欢迎消息，你收到后才能发送数据给它）。之后我们会对通信两端分别调用receive_from函数，它会从已连接的socket对象中收取数据。我们把收到的数据都输出到屏幕上，检查里面有没有什么有趣的东西。然后，把数据交给response_handler函数❸，等它处理数据后再转发给本地客户端。剩下的代理代码就很简单了：开启一个循环，不断地从本地客户端读取数据，处理数据，转发给远程服务器，从远程服务器读取数据，处理数据，转发给本地客户端，直到再也读不到任何数据为止。当通信两端都没有任何数据时❹，关闭两端的socket，退出代理循环。

我们再来编写server_loop函数，用来创建和管理连接：

server_loop函数创建了一个socket❶，将它绑定到本地主机并开始监听❷。在主循环里❸，每出现一个新连接，我们就新开一个线程，将新连接交给proxy_handler函数❹，由它来给数据流的两端收发数据。

最后就只剩main函数了：

在main函数里，我们会读取一些命令行参数，然后启动服务器循环，开始监听接入的连接。

小试牛刀

既然代理中的主循环核心代码和辅助函数都已经准备就绪，我们试着用它来连接一台FTP服务器吧。用如下参数启动该代理：

这里使用sudo，是因为端口21是一个高权限端口，需要具备管理员权限或root权限才能启动监听。接着，启动任意FTP客户端，设定localhost为远程服务器IP地址，服务器端口为21。当然，这个代理要指向一台能够实际响应请求的FTP服务器。当我们通过代理连接一台FTP测试服务器时，将获得如下输出：

在Kali虚拟机上另开一个终端，新建一个FTP会话，连接到Kali虚拟机的默认FTP端口（21）：

可以清楚地看到，我们成功地收到FTP服务器的欢迎信息，向它发送了用户名和密码，最后干净地退出了FTP服务器。

基于Paramiko的SSH通信

使用我们编写的BHNET工具（netcat的替代品）四处游荡是件挺方便的事情，但有时你还需要加密自己的流量以逃避检测。一个比较常用的方法是通过SSH（Secure Shell）进行加密通信。但要是你控制的目标根本没装SSH客户端，就像99.81943%的Windows系统一样呢？

虽然Windows平台有很多非常好的SSH客户端，比如PuTTY，但这本书是讨论Python的，在Python的世界里，你可以利用原始的socket和一堆密码学魔术来创建自己的SSH客户端或服务端。不过，有现成的模块干嘛要自己从头写呢？Paramiko，一个基于PyCrypto开发的第三方库，可以让你轻松地用上SSH2协议。

为了了解这个库的运作原理，我们将：使用Paramiko连接到一台有SSH的机器，在上面执行命令；利用Paramiko编写SSH服务器和客户端，用它们在Windows系统上远程执行命令；讲解如何用Paramiko自带的反向隧道示例程序，来实现与BHNET工具的代理功能相同的效果。我们开始吧。

首先，用pip安装器安装Paramiko（或者从官网下载[4]）：

我们之后会用到Paramiko的一些官方示例代码，所以不要忘记从Paramiko的GitHub官方仓库下载一份[5]。

创建一个新文件，命名为ssh_cmd.py，然后输入以下内容：

我们创建一个名为ssh_command的函数❶，它会向SSH服务器发起连接并执行一条命令。注意，Paramiko支持用密钥认证❷来代替密码认证。在真实环境中应该使用SSH密钥认证，但为了便于演示，我们这里仍然使用传统的用户名—密码认证的方式登录。

因为整个连接的两端都在我们的控制之下，所以当服务器发来一个没有记录的公钥时，我们设定的策略是自动信任并记住这个公钥，然后开始连接。如果连接成功，我们就会运行最开始传给ssh_command函数的那条命令❸。如果这条命令产生了任何输出数据，就一行一行地把它打印出来。

在main代码块里，我们用了一个新的第三方库：getpass❹。虽然可以使用getpass库来获取当前设备上登录用户的用户名，但因为服务器和当前设备上的用户名不同，所以这里明确要求用户输入用户名。接着我们调用getpass函数，让用户输入密码（这样用户敲击的字符不会出现在屏幕上，避免被人偷窥密码）。然后我们依次读取IP地址、端口、要执行的命令（cmd），把它们交给ssh_command函数执行❺。

我们连接到Linux服务器上来简单测试一下：

可以看到，我们成功连接并执行了这条命令。如果简单修改这个脚本，还能让它在一台SSH服务器上执行多条命令，或者是在多台SSH服务器上执行命令。

有了以上的基础之后，我们可以再修改这个脚本，让它能够在Windows设备上通过SSH执行命令。当然，在使用SSH的时候，正常来讲应该用一个SSH客户端连接到SSH服务器上。但是由于大部分Windows发行版没有自带SSH服务器，所以我们需要反过来：让一台SSH服务器给SSH客户端发送命令。[6]

创建一个新文件，命名为ssh_rcmd.py，并输入以下内容：

这个程序的开头跟上一个程序差不多，从while True循环开始则是新加的内容。在这个循环里，我们不再像之前那样命令服务器执行命令，而是从SSH连接里不断读取命令❶，在本地执行❷，再把结果发回服务器❸。

另外还有一点，我们给服务器发送的第一条命令是ClientConnected。等到写服务器代码的时候，你就会明白为什么要发这么一条命令。

现在，我们来编写SSH服务器，我们的客户端（指实际运行命令的机器）之后会去连接它。服务器上运行的可以是Linux系统、Windows系统，甚至是macOS系统，只要上面装了Python和Paramiko就可以。创建一个名为ssh_server.py的新文件，在里面输入以下内容：

这里借用了Paramiko官方示例里提供的SSH密钥❶。接着，像前面讲过的那样，我们打开一个socket监听器❸，然后把这个监听器“SSH化”❷，并设置好它的权限认证方式❹。当一个客户端通过认证❺，并向我们发送ClientConnected命令后❻，在SSH服务器（即运行ssh_server.py脚本的机器）上运行的任何命令，都会被发送到SSH客户端（即运行ssh_rcmd.py脚本的机器），并且在该客户端上执行，执行的结果会回传给SSH服务器。我们来试一下吧。

小试牛刀

在这个演示中，我们会在一台Windows电脑上运行客户端，在一台Mac电脑上运行服务器。首先，启动服务器：

在Windows电脑上运行客户端：

回到服务器上，就能够看到客户端的连接了：

如你所见，客户端连接成功后，我们执行了几条命令。从SSH客户端上什么也看不出来，但其实我们发出的命令已经在客户端上运行了，输出结果也发回了SSH服务器。

SSH隧道

在上一节，我们写了一个可以把命令发给SSH服务器执行的工具。接下来，我们要讲另一项技术：SSH隧道。SSH隧道发给服务器的不是一连串命令，而是用SSH加密过的网络流量，服务器收到后会将它们解密，然后转发给真正的接收者。

想象这样一种情况：你可以远程访问某个内网里的某台SSH服务器，但你真正的目标却是该内网里的一台Web服务器；你无法直接访问那台Web服务器，而能访问的那台SSH服务器上又没有安装你需要的工具。

解决问题的办法之一，就是创建一条正向SSH隧道。例如，你可以运行ssh -L 8008:web:80 justin@sshserver命令，以用户justin的身份连接SSH服务器，将8008端口的数据转发到你的本机上。接下来，你发往本机8008端口的所有数据，都会通过这个建好的SSH隧道发送到SSH服务器，再由SSH服务器转发到Web服务器上，如图2-1所示。

干得不错，但不要忘了，大部分Windows系统上是没有SSH服务的。这倒不是说无计可施了。我们可以配置一条反向SSH隧道连接。要搭建这种隧道，必须先像往常一样，从内网的Windows客户端连到外网的SSH服务器上。通过这条SSH连接，我们可以在服务器上的某个远程端口和本地端口之间建立一条隧道，如图2-2所示。通过这个本地端口，我们可以暴露某个内部系统的3389端口并通过远程桌面协议访问，或者访问这台Windows设备能访问的任何系统（比如，之前举例时提到的Web服务器）。

图2-1　SSH转发隧道

图2-2　SSH反向隧道

在Paramiko的官方示例中有个rforward.py程序，恰好能够实现这个功能。它本身已经很完善了，所以我们不会在这里把它照抄一遍，只会指出几个重要的点，并且简单演示一下它的使用方法。打开

rforward.py文件，跳到main()函数，里面的内容如下：

开头几行代码❶会检查所有必需的参数是否都传进来了，然后创建一个Paramiko的SSH客户端连接❷（应该看起来很眼熟）。main()函数的最后一段调用了reverse_forward_tunnel函数。

我们来看看这个函数：

在Paramiko库里，有两个主要的通信模式：一个是transport，负责建立和维持加密通信；另一个是channel，它用起来就像socket一样，主要用来在加密的transport会话中收发数据。这里我们先用Paramiko的request_port_forward函数，把服务器上某个端口的TCP连接全部转发过来❶，并由此建立一个新的channel❷。接着，调用handler函数处理这个channel❸。

但是，到这里还没结束，还需要编写handler函数来管理每个线程的通信。

最终，我们完成了数据的收发❶。下一节我们来测试一下。

小试牛刀

在Windows系统上运行rforward.py程序，让它来扮演某台Web服务器和SSH服务器之间的中间人：

你可以从Windows设备上看到，我们连接了SSH服务器192.168.1.203（Kali虚拟机），并在上面打开了8081端口，这个端口会把流量都转发到192.168.1.207的3000端口上。现在，如果在Kali虚拟机上访问http://127.0.0.1:8081/，就会通过SSH隧道连接到位于192.168.1.207:3000的Web服务器上，如图2-3所示。

图2-3　反向SSH隧道示例

如果切换回Windows设备，可以看到Paramiko里建立了如下连接：

SSH和SSH隧道都是相当重要的概念，需要深刻理解和动手实践。黑帽黑客必须知道何时使用以及到底如何使用SSH和SSH隧道，而Paramiko保证了你能够随时在已有的Python代码中添加SSH服务。

在本章中，我们编写了一些非常简单却非常有用的工具。我们鼓励大家随意地拓展、修改这些工具，更深入地掌握Python的网络编程特性。你还可以在渗透测试、后渗透攻击或漏洞挖掘的过程中使用这些工具。下面我们将介绍如何使用原始socket，以及如何进行网络嗅探，然后结合这两个知识点开发一款纯Python的主机扫描器。

[1]链接9。

[2]译者注：这里出现的“192.168.1.203”和后文所有的“192.168.1.203”，都是作者自己的Kali虚拟机IP地址，你搭建的虚拟机的IP地址很可能不是这个，不要照抄。如果不知道自己Kali虚拟机的IP地址，可以运行“ifconfig”命令或“ip address”命令确认一下。

[3]译者注：也就是整个ASCII码表+拓展ASCII码表。

[4]链接10。

[5]链接11。

[6]译者注：如果只是想解决“Windows上没有SSH服务器”的问题，其实最简单的解决思路应该是用Python写一个普通的SSH服务器然后部署上去。作者的思路舍近求远，其实是用了一个SSH反弹shell。跟普通shell相比，反弹shell可以绕过各种防火墙策略，穿透复杂的内网环境，在渗透测试场景里会更加趁手。

3　编写流量嗅探器

借助网络嗅探工具，我们可以捕获目标机器接收和发送的数据包。因此，在准备攻击前和完成攻击后，这些工具都有很多切实的用途。大部分情况下你可以使用Wireshark[1]这样现成的嗅探工具，或者Scapy这样的Python风格的解决方案（第4章会讨论它）。但不管怎样，学会自己编写简单的嗅探器来浏览和解码流量仍然很有好处。

自己编写嗅探器后，就会对业界的成熟工具产生由衷的敬意——它们竟然能如此妥善地处理种种细节，而不需要用户做任何复杂的操作。你还可能会学到一些新的Python技巧，加深对底层网络实现的理解。

上一章讲过如何使用TCP或UDP收发数据，你应该能通过这种方式处理绝大部分的网络服务。但是在这些高层协议之下，还有一些底层的模块在控制数据包的收发。在本章中，我们将使用原始socket来读/写原始IP头或ICMP头等底层网络信息。本章并不会讨论Ethernet层的数据，但是如果想了解任何涉及底层网络的攻击（比如ARP投毒）或开发无线安全审计工具的话，就需要非常熟悉Ethernet帧和它的用法。

下面，我们就从发现网段中的存活主机开始吧。

编写基于UDP的主机发现工具

嗅探工具的主要目标，是发现目标网络里存活的主机。攻击者希望能找出网络里的所有潜在目标，以便有针对性地开展侦察和渗透。

我们可以利用绝大部分操作系统都会做的一个操作来判断某个IP地址上有没有存活的主机。当我们向主机发送一个UDP数据包时，如果主机上的UDP端口没有开启，一般会返回一个ICMP包来提示目标端口不可访问。这条ICMP消息也就暗示了主机是存活的，因为如果主机根本不存在的话，我们应该不会收到任何信息。当然，这个方法奏效的前提是我们选中的UDP端口没有被使用。所以，为了尽可能地扫描出所有主机，应该一口气探测多个端口，以免正好将数据发送到在线的UDP服务上。

为什么使用UDP？因为在整个子网里滥发UDP数据包并等待对方回复ICMP消息的开销很小。这个扫描器写起来挺简单的，其主要的工作就是解码并分析各种网络协议的数据头而已。我们将编写这个扫描器的Windows版本和Linux版本，尽最大努力保证它在企业网络环境里稳定可用。

我们还可以在扫描器中添加额外的功能，比如让它对发现的主机自动发起Nmap完整端口扫描。这样一来，就能确定这些主机上有没有可用的网络攻击面。这个功能留给你作为课后练习，非常期待你能基于这个核心概念想出什么创意来。下面我们就开始吧。

Windows和Linux上的包嗅探

在Windows和Linux上操作原始socket的步骤不太相同，但嗅探工具需要具备足够的灵活性以便部署到不同平台。考虑到这一点，我们在创建socket对象后会检测系统环境。如果是Windows系统，就需要通过socket输入/输出控制（IOCTL）机制来设定一些标志，启用网卡的混杂模式。IOCTL是用户程序和系统内核组件通信的一种方式，更多细节可以参考维基百科中的解释[2]。

现在我们来写第一个例子——一个简单的原始socket嗅探器，它只会读一个数据包，然后直接退出：

先把HOST变量设定成本机IP地址，然后构建一个socket对象，传入嗅探网卡数据所需的参数❶。这里Windows和Linux的区别是，前者允许我们嗅探任何协议的所有流入数据，而后者强制我们指定一个协议来嗅探，这里指定的是ICMP。注意，你需要拥有Windows的管理员权限或Linux的root权限才能启用网卡的混杂模式。启用混杂模式后，就能嗅探到流经网卡的所有数据包，包括那些不归我们接收的数据包。接着，修改socket的设置❷，让它抓包时包含IP头。下一步❸，判断程序是不是运行在Windows上，如果是，就额外发送一条IOCTL消息启用网卡的混杂模式。如果是在虚拟机上运行Windows，可能会收到一条通知问你“是否允许客户机启用混杂模式”，当然，这里要选择“允许”。现在我们就做好嗅探数据的准备了。在本例中只输出了原始数据包的全部内容❹，没有实际解码里面的信息，因为目前我们只想确认核心代码都能正常工作。嗅探完一个数据包后，我们会再次检测现在是不是在Windows平台，关闭网卡的混杂模式❺，然后退出。

小试牛刀

打开一个新的终端或Windows命令提示符窗口，运行以下命令：

在另一个终端或命令提示符窗口中，任选一台主机ping一下。这里我们ping一下nostarch.com：

在运行嗅探器的第一个窗口中，你会看到类似于下面内容的乱七八糟的输出：

可以看到，我们捕获了发送给nostarch.com的第一个ICMP请求包（可以在输出内容的结尾看到nostarch.com的IP地址104.20.209.3）。如果在Linux上运行这个程序，你还能看到nostarch.com发回的响应。

说实话，嗅探一个数据包用处不大，所以我们添加一些新功能，处理更多的数据包，并解码其中的信息。

解码IP层

在当前的模式下，我们的嗅探器可以捕获到TCP、UDP、ICMP等任何高层协议的IP头，但里面的信息是以二进制形式封装的，很难直接读懂（就像上面的“小试牛刀”所示）。我们下一步的工作就是解码数据包中的IP头部分，提取诸如协议类型（TCP、UDP或ICMP）、源IP地址和目的IP地址等有用信息。这是接下来进行更深层次协议分析的基础。

如果你分析过网络里实际的数据包，应该能明白为什么我们需要对数据解码。图3-1所示为典型的IPv4头结构。

图3-1　典型IPv4头结构

我们需要解码整个IP头（除了可选参数部分），并提取协议类型、源IP地址和目的IP地址等信息。这就意味着要直接跟二进制数据打交道，因此我们要找出一套用Python分割IP头各个数据段的方案。

在Python中，要把外来的二进制数据分解成数据结构有不少办法。比如，你可以用ctypes库或struct库来定义所需的数据结构。ctypes是Python的一个外部函数库，它提供了一条与C语言家族沟通的渠道，让你能够使用各种兼容C语言的数据结构，调用符合C语言标准的共享库里的函数。另一个库struct则能将数据在Python类型和C结构的二进制数据（bytes类型的Python对象）之间来回转换。换句话说，除了操作二进制数据，ctypes库还提供了一堆额外功能；而struct库则专注于操作二进制数据。

在网上浏览各种开源工具时，你会发现这两种方法各有不少项目在用。本节会分别演示如何使用它们读取IPv4头，你可以自己决定选哪一种，反正两种用着都很趁手。

ctypes库

以下代码定义了一个名为IP的类，它能读入一个数据包，然后把IP头拆解成一个个单独的字段。

这个类创建了一个名为_fields_的结构，用于定义IP头的各个部分。该结构使用了ctypes里定义的C数据类型，例如代表unsigned char类型的c_ubyte，代表unsigned short的c_ushort等。可以看到，每一个字段都和图3-1中所示的完全契合。各字段的定义由三个参数组成：字段名称（例如ihl或offset）、数据类型（例如c_ubyte或c_ushort）以及字段位数（例如ihl和version，长度为4个二进制位）。能够以位为单位指定数据长度是非常方便的，这意味着我们可以自由指定想要的长度，而不用强制对齐到整数字节（“强制对齐到整数字节”就是说“每个字段的位数必须是8的整数倍”）。

IP类继承自ctypes库的Structure类，它要求我们创建对象前必须定义那个_fields_结构。为了往_fields_结构里填数据，Structure类利用了__new__函数。此函数的第一个参数是一个指向当前类的引用，__new__函数会用这个引用创建当前类的一个对象。之后这个对象会被传给__init__函数进行初始化。在创建IP对象的时候，我们只要像平常一样操作就可以。在Python底层，解释器会调用__new__函数，__new__函数会把数据填入_fields_结构，然后再传给__init__函数，完成对象的创建。所以只要提前定义好数据结构，就可以直接把网络数据包传给__new__函数，接着IP头的那些字段就会神奇地自动变成IP对象的成员变量。

现在，你应该已经大致了解如何把C数据类型映射到IP头数据上了。在生成Python对象时，C语言代码会是很有用的参考，因为它们可以像这样平滑转换成纯Python实现。想要掌握更多ctypes的使用技巧，可以阅读ctypes的文档。

struct库

struct库提供了一些格式字符，用来定义二进制数据的结构。在下面的示例中，我们将再次定义一个IP类来存储IP头信息。只是这次，我们会使用格式字符来表示IP头的各部分：

第一个格式字符（示例中的<）永远都是用来表示数据的字节序的。C数据类型一般是按照设备中的原生格式和字节序来存储的。在这个例子中，我们使用的是Kali系统（x64架构），它使用的是小端序（little-endian）。在小端序设备上，低位字节会被放在较低的内存地址上，高位字节会被放在较高的内存地址上。

接下来的格式字符是用来表示IP头的各部分的。struct库提供了若干格式字符。对于IP头来说，我们需要用到的只有B（1字节，unsigned char）、H（2字节，unsigned short）和s（一个字节数组，数组长度需要另外指定，比如4s就代表长度为4字节的字节数组）。留意一下我们的格式字符串和图3-1中所示的IP头结构是如何一一对应的。[3]

前面讲过，使用ctypes库时，可以指定开头几个字段的位数。在struct库里，不存在对应于nybble格式（即4个二进制位组成的数据块，也叫作nibble）的格式字符，所以我们需要额外做一些操作，把ver和hdrlen变量从IP头的第一个字节里提取出来。

对于IP头的第一个字节，我们只想取高位nybble（整个字节里的第一个nybble）作为ver的值。取某字节高位nybble的常规方法是将其向右位移4位，相当于在该字节的开头填4个0，把其尾部的4位挤出去❶。这样我们就得到了原字节的第一个nybble。这一行Python代码基本上就是做了如下操作：

我们想把低位nybble（或者说原字节的最后4个二进制位）填进hdrlen里，取某个字节低位nybble的常规方法是将其与数字0xF（00001111）进行按位与运算❷。它利用了0 AND 1 = 0的特性（0代表假，1代表真）。想要AND表达式为真，表达式两边都必须为真。所以这个操作相当于删除前4个二进制位，因为任何数AND 0都得0；它保持了最后4个二进制位不变，因为任何数AND 1还是原数字。所以，基本上这一行Python代码做的就是如下操作：

解析IP头其实不需要你掌握太多位运算知识，但是有一些特定的技巧，比如“位移”和“与运算”会频繁出现于其他黑客的代码里，所以还是值得一学的。

在这种需要位移操作的情况下，解析二进制数据需要费点心思。但其他情况（比如解析ICMP消息）大多没这么麻烦：ICMP消息里的每一个字段位数都是8的整数倍，struct的格式字符位数也都是8的整数倍，所以就不需要再把某个字节切割成单独的nybble了。在图3-2所示的Echo Reply ICMP消息结构中，你可以看到ICMP头的每个字段都可以用一个格式字符组合来表示（BBHHH）。

图3-2　ICMP Echo Reply消息示例

因此，解析ICMP头结构的办法非常简单，只要为前两个成员变量分配1字节，为后三个成员变量分配2字节就可以了。

想要掌握更多的使用技巧，可以阅读struct库的官方文档[4]。

可以在ctypes库和struct库之间任选一个来读取和解析二进制数据。不管你选用的是哪个库，都可以像这样直接初始化一个对象：

在这个例子里，我们用buff变量里的数据包初始化了一个IP对象。

编写IP解码器

现在，把刚才设计的IP头解码代码写下来，文件名就叫sniffer_ip_header_decode.py，文件内容如下所示：

首先，我们写下刚才的IP类❶，它定义了一个Python结构，可以把数据包的前20字节映射到一个便于读/写的IP头对象里。如你所见，我们辨识出的所有字段都和标准的IP头结构完美契合。然后整理数据，将其输出为人类可读的形式，展示目前的通信协议和通信双方的IP地址❷。用上了新打造的IP头结构后，我们把抓包的逻辑改成持续抓包和解析。每读入一个包❸，就将它的前20字节转换成IP头对象❹。接着，只需要把抓取到的信息打印到屏幕上就可以了。让我们来试试看。

小试牛刀

我们来测试刚才写的代码，看看能从原始数据包中提取出什么样的信息。建议在Windows设备上测试这些代码，因为这样就能同时看到TCP、UDP和ICMP等协议的数据，易于进行一些简便的测试（比如直接打开浏览器浏览网页）。如果你不得不使用Linux系统，那就再做一次之前的ping测试吧。

打开一个终端，输入以下内容：

因为Windows是个挺“健谈”的系统，所以你很可能立即就看到了测试结果。我们是通过打开浏览器访问google网站来测试的，脚本输出了这样的测试结果：

因为我们没有深入地解码数据包的内容，所以这里只能猜测整个数据流的含义。开头的几个UDP数据包，应该是域名系统（DNS）在查询Google网站的IP地址，而之后的TCP会话应该是我们的设备实际连接和下载了Web服务器上的内容。

为了在Linux上进行这个测试，我们可以ping google.com，结果应该是这样的：

你应该已经发现了局限之处：我们只能看到响应数据包，而且只能看到ICMP协议的数据。但是因为我们的目标是构建主机扫描工具，所以这点局限是完全可以接受的。接下来，我们要运用解码IP头的技术来解码ICMP消息。

解码ICMP

现在我们已经可以完整解码数据包的IP层，接下来还需要解码扫描器向非开放端口发UDP包时触发的ICMP响应。不同的ICMP消息之间千差万别，但有三个字段是一定存在的：类型（type）、代码（code）和校验和（checksum）。类型和代码两个字段告诉接收者，接下来要接收的ICMP信息是什么类型的，也就指明了如何正确地解码里面的数据。

为了实现扫描功能，我们需要检查类型为3、代码为3的ICMP消息。类型为3表示目标不可达（Destination Unreachable)，而代码为3表示导致目标不可达的具体原因是端口不可达（Port Unreachable）。图3-3展示的就是Destination Unreachable类型的ICMP消息结构。

图3-3　Destination Unreachable类型的ICMP消息结构

可以看到，数据包开头的8个二进制位代表类型，其后的8个二进制位代表ICMP代码。这里注意一个有意思的细节：当一台主机发送出ICMP消息的时候，会把触发ICMP消息的原始数据包的IP头附在消息末尾。另外，为了确认这个ICMP消息真的是被扫描器触发的，我们还可以自定义8字节的特征数据放在UDP数据包的开头，然后与接收到的ICMP消息的最后8字节进行对比。

下面给之前的嗅探器添加解码ICMP消息的功能。把之前的文件另存为sniffer_with_icmp.py，然后添加以下代码：

这段简单的代码在之前的IP结构下方又创建了一个ICMP结构❶。在负责接收数据包的主循环中，我们会判断接收到的数据包是否为ICMP数据包❷，然后计算出ICMP数据在原始数据包中的偏移❸，最后将数据按照ICMP结构进行解析❹，输出其中的类型（type）和代码（code）字段。IP头的长度是基于IP头中的ihl字段计算的，该字段记录了IP头中有多少个32位（4字节）长的数据块。所以只需要将这个字段乘4，就能计算出IP头的大小，以及数据包中下一个网络层（这里指ICMP）开始的位置。

如果用这段代码再做一次之前的ping测试，输出结果应该稍有不同：

这表明ping（ICMP Echo）响应数据被正确地接收并解码了。现在，我们准备实现整个扫描过程的最后一部分逻辑——群发UDP数据包，然后解析它们的结果。

我们引入ipaddress库，这样就能对整个子网进行主机发现扫描。将sniffer_with_icmp.py脚本另存为scanner.py，添加如下代码：

最后添加的这点代码应该很好理解。我们定义了一个简单的字符串作为“签名”❶，用于确认收到的ICMP响应是否是由我们发送的UDP包所触发的。udp_sender函数❷会读取程序开头设定的那个子网地址，往这个子网上的每一个IP地址发送UDP数据包。

接着，我们定义一个名叫Scanner的类❸。想要初始化它，就要向它传入扫描器所在主机的IP地址。在它初始化的过程中，我们会创建一个socket对象，启用网卡的混杂模式（如果程序运行在Windows平台上），并把这个socket对象设定为Scanner类的成员变量。

sniff函数❹会嗅探网络上的数据，步骤跟之前的例子差不多，唯一的区别就是这次它会把在线的主机记录下来。接收到预期的ICMP消息时，我们首先检查这个响应是不是来自我们正在扫描的子网❺，然后检查ICMP消息里有没有我们自定义的签名❻。如果所有检查都通过了，就把发送这条ICMP消息的主机IP地址打印出来❼。如果使用Ctrl+C组合键中断扫描过程的话❽，程序就会关闭网卡混杂模式（如果是Windows平台），并且把迄今为止扫描出来的主机都打印到屏幕上。

__main__代码块负责统筹所有的模块：它会创建一个Scanner对象，休眠几秒。接着，在调用sniff函数之前，它会先为udp_sender函数开启一个独立的线程❾，以免干扰嗅探的效果。我们来试一试吧。

小试牛刀

现在，用我们的扫描器扫描一下本地网络。你可以任选Linux或Windows系统进行测试，结果应该是一样的。在笔者的测试环境里，本地设备的IP地址是192.168.0.187，所以我们用扫描器扫描192.168.0.0/24网段。如果扫描的过程中脚本输出的垃圾信息太多，可以把不需要的print语句都注释掉，只留下最后输出结果的那一行print语句。

ipaddress库

我们的扫描器用到了一个名为ipaddress的库，它能帮助扫描器正确处理192.168.0.0/24这样的子网地址。

这个ipaddress库极大地减轻了处理子网或寻址工作的难度。例如，你可以使用Ipv4Network对象像这样进行简单的检查：

如果你想给整个子网群发数据包的话，可以这样创建简单的迭代器：

它能大大简化处理网段时的编程工作，是理想的主机扫描工具。

像刚才这样简单的扫描任务，往往只需要几秒就能得到结果。通过比对家用路由器上的DHCP表，我们就能检查这份结果的准确性。你可以将本章所学的内容轻松拓展到解码TCP/UDP数据包上，或者在这个扫描器的基础上开发更多工具。在第7章编写木马框架时还会用到这个工具，我们部署的木马可以用它扫描出更多攻击目标。

现在我们对网络上层协议和底层协议都有了基本的了解，接着来了解一下名为Scapy的成熟Python框架吧。

[1]链接12。

[2]链接13。

[3]译者注：这三个字母和对应的数据结构乍一看毫无规律，但其实了解了三个字母的含义就很好理解了。B是byte的首字母，H是half-word的首字母，s是string的首字母。C语言里string的本质就是byte array，4s与其说是长度为4的byte array，不如说是长度为4的string更好理解。

[4]链接14。

4　Scapy：网络的掌控者

有时候，你会遇到这样一种Python库，它构思精巧，令人惊叹，即便我们用一整章的篇幅来讲解它都稍显不足。Philippe Biondi开发的数据包处理库Scapy就属此类。你可能会在学完本章后，突然意识到我们在前两章里费了那么多功夫才完成的任务，用Scapy只需要一两行代码就能解决。

Scapy功能强大且灵活，它的潜力几乎是无穷无尽的。我们会先简单尝试一下，用Scapy嗅探流量，从中窃取明文的邮箱身份凭证。然后对网络中的攻击目标进行ARP投毒，以此嗅探它们的网络流量。最后，我们会演示如何借助Scapy的pcap数据处理能力，从嗅探到的HTTP流量中提取图片，并运用面部识别算法来判断其是否为人像照片。

建议你在Linux上使用Scapy，因为它最初就是按照兼容Linux来设计的。最新版本的Scapy虽然支持Windows，但是本章会假设你使用的是一台完整安装Scapy的Kali虚拟机。如果你还没有Scapy的话，可以去其官网[1]下载并安装。

现在，假设你已经渗透进了某个攻击目标的局域网（LAN）。学习本章的知识后，你就能用这些技术嗅探局域网中的流量。

窃取邮箱身份凭证

我们已经花了不少时间学习基于Python的嗅探技术，接下来学习如何使用Scapy的接口嗅探数据包并提取其中的内容。我们将编写一个非常简单的嗅探器来捕捉主流邮箱协议（SMTP、POP3和IMAP）的身份凭证。之后，用这个嗅探器配合基于ARP投毒的中间人（MITM）攻击，我们就能窃取网络中其他设备的身份凭证。当然，这个技巧也能用于其他任何协议，还可以不管具体是什么协议，直接把所有流量都记录到一个pcap文件里，以备后续分析。这个技巧我们之后也会进行演示。

为了感受一下Scapy的用法，我们先来编写一个仅仅能分解并输出数据包内容的基础嗅探器。Scapy提供了一个名字简明扼要的接口函数sniff，它的定义是这样的：

filter参数允许你指定一个Berkeley数据包过滤器（Berkeley Packet Filter，BPF），用于过滤Scapy嗅探到的数据包，也可以将此参数留空，表示要嗅探所有的数据包。例如，如果想嗅探所有HTTP数据包，可以指定BPF为tcp port 80。

iface参数用于指定嗅探器要嗅探的网卡，如果不设置的话，默认会嗅探所有网卡。prn参数用于指定一个回调函数，每当遇到符合过滤条件的数据包时，嗅探器就会将该数据包传给这个回调函数，这是该函数接受的唯一参数。count参数可以用来指定你想嗅探多少包，如果留空的话，Scapy就会一直嗅探下去。

我们先编写一个简单的嗅探器，嗅探一个数据包并输出里面的内容。然后我们对它进行改造，让它只嗅探邮箱协议的相关命令。打开mail_sniffer.py文件，敲出这些代码：

首先我们定义了一个回调函数来接收嗅探到的数据包❶，然后告诉Scapy开始嗅探❷所有网卡，不带任何过滤条件。运行这个脚本，就能看到如下输出：

是不是简单得难以置信？可以看到，收到网络上的第一个数据包之后，回调函数就会调用内置的packet.show函数展示数据包的内容，还会解析一部分协议信息。这个show函数是调试程序的好帮手，可以用它检查捕获到的数据包是不是你想要的。

现在我们已经实现了一个基础的嗅探器，接下来我们将添加过滤器和回调函数代码，有针对性地捕获和邮箱账号认证相关的数据。

在接下来的例子中，我们将设置一个包过滤器，确保嗅探器只展示我们感兴趣的包。我们会使用BPF语法（也被称为Wireshark风格的语法）来编写过滤器。你可能会在tcpdump、Wireshark等工具中用到这种语法。

先来讲一下基本的BPF语法。在BPF语法中，可以使用三种类型的信息：描述词（比如一个具体的主机地址、网卡名称或端口号）、数据流方向和通信协议，如表4-1所示。你可以根据自己想找的数据，自由地添加或省略某个类型、方向或协议。

表4-1　BPF语法

举例来说，用表达式src 192.168.1.100构造的过滤器会捕获所有来自192.168.1.100的数据包。与之反向的过滤器是dst 192.168.1.100，它会捕获所有发给192.168.1.100的数据包。类似的，表达式tcp port 110 or tcp port 25构造的过滤器只会放行在端口110或25上进出的TCP数据包。现在我们写一个BPF：

这里的逻辑相当简单明了。我们修改了sniff函数，增加了一个BPF，这个过滤器只会监听常用邮件协议端口上接收到的流量，也就是110（POP3）、143（IMAP）和25（SMTP）等端口❹。我们还增加了一个新参数store，把它设为0以后，Scapy就不会将任何数据包保留在内存里。如果想长时间进行嗅探，最好用上这个参数，这样就能确保你不会消耗掉过多的内存。当回调函数被调用时，我们会检查收到的数据包里有没有数据载荷❶，然后检查数据载荷里有没有USER或PASS这两条邮件协议命令❷。如果发现了任何认证数据，就把服务器地址和具体的认证数据打印出来❸。

小试牛刀

以下是一些示例输出，是笔者用测试账号登录邮箱客户端时截取出来的：

可以看到，我们的邮箱客户端试图登录192.168.1.207这台服务器，并且发送了明文账号和密码。本节所讲的仅仅是一个非常简单的例子，它展示了如何将一段Scapy嗅探脚本改造成渗透测试中实际可用的工具。这个脚本只会嗅探邮件流量，因为我们设计的BPF只关注邮件协议端口。可以通过修改这个过滤器来监控其他类型的流量，比如把它改成tcp port 21，我们就能嗅探FTP连接和登录凭证。

你可能会觉得嗅探自己的流量颇为有趣，但是嗅探身边朋友的流量或许更加“好玩”。接下来，我们来看看如何发动ARP投毒攻击，嗅探同一网络里目标设备的流量。

ARP投毒

ARP投毒是最古老但最有效的黑客攻击技术之一。它的逻辑相当简单：欺骗目标设备，使其相信我们是它的网关；然后欺骗网关，告诉它要发给目标设备的所有流量必须交给我们转发。网络上的每一台设备，都维护着一段ARP缓存，里面记录着最近一段时间本地网络上的MAC地址和IP地址的对应关系。为了实现这一攻击，我们会往这些ARP缓存中投毒，即在缓存中插入我们编造的记录。由于已经有数不胜数的资料讲过ARP和ARP投毒攻击，所以关于这个攻击的底层细节就留给你自学吧。

现在大方向已经明确，是时候着手落实了。在测试本节内容的时候，笔者用一台Kali虚拟机攻击了现实中的Mac电脑，还对连着同一个WiFi接入点的多台移动设备进行了测试，攻击力非常强。我们要做的第一件事，是检查那台Mac电脑的ARP缓存，以便之后进行对比查看攻击效果。在Mac电脑上执行以下命令来确认如何检查ARP缓存：

ifconfig命令能够展示出某个特定网卡（这里检查的是en0）的网络配置，如果不指定网卡，就会展示出所有网卡的网络配置。输出结果显示这台设备的inet（IPv4）地址是192.168.1.193。它还列出了MAC地址（38:f9:d3:63:5c:48，开头的标记为ether）和几个IPv6地址。ARP投毒只对IPv4地址有效，所以可以无视IPv6地址。

现在我们来看看这台Mac电脑的ARP缓存里有什么。以下就是它所记录的网络中各个邻居的MAC地址：

可以看到，攻击者掌控的Kali虚拟机的IP地址是192.168.1.203，MAC地址是a4:5e:60:ee:17:5d。攻击者和受害者都通过这个网关连接到互联网。该网关的IP地址是192.168.1.254，MAC地址是20:e5:64:c0:76:d0。把这些值记下来，这样当攻击发生时，我们就能查看自己是否成功修改了网关的MAC地址。搞清了网关和攻击目标的IP地址后，我们来编写ARP投毒的代码。打开一个新的Python文件，命名为arper.py，然后输入以下代码（先搭建一个骨架，让你大致理解我们的思路）：

如你所见，我们定义了一个辅助函数来获取任意设备的MAC地址❶，然后定义了一个Arper类，用来投毒❷、嗅探❸和恢复❹网络设置。先编写get_mac函数的代码，这个函数能够找出任意IP地址所对应的MAC地址。我们会用它来找受害者和网关的MAC地址。

我们传入目标IP地址并创建了一个查询数据包❶。Ether函数表示这个数据包将会被全网广播，ARP函数则构造了一个MAC地址查询请求，用来询问每个节点其地址是否为这个目标IP地址。我们会使用Scapy的srp函数❷来发送这个数据包，这样就能在网络协议栈的第二层上收发数据。接收到的应答数据会被存到resp变量中，里面记录着目标IP地址在Ether层的源地址（MAC地址）。

下面，我们来编写Arper类：

我们用受害者IP地址、网关IP地址，以及要使用的网卡（默认是en0）来初始化这个类❶。有了这些信息后，填充interface、victim、victimmac、gateway、gatewaymac等成员变量，并把它们输出到屏幕上❷。

在Arper类中，编写一个run函数作为攻击的入口点：

run函数负责执行Arper对象的主要工作。它配置并启动了两个进程：一个用来毒害ARP缓存❶，另一个用来嗅探网络流量、实时监控攻击过程❷。

poison函数会创建恶意数据包，把它们发送给受害者和网关：

我们在poison函数中构建用来攻击受害者和网关的恶意数据。首先，构建出毒害受害者的恶意ARP数据包❶。然后，用类似的方法构建出毒害网关的恶意ARP数据包❷。我们毒害网关时会发送受害者的IP地址与攻击者的MAC地址。而毒害受害者时，会发送网关的IP地址与攻击者的MAC地址。把所有这些信息都打印到屏幕上，这样就能确认数据包的接收者和攻击载荷了。

接着，我们用一个无限循环不停地把恶意数据包发往它们的目的地，以确保攻击过程中ARP缓存一直处于中毒状态❸。这个循环会一直持续到Ctrl+C组合键被按下为止（KeyboardInterrupt）❹，这时我们会将网络状态恢复为原样（把正确信息发送给受害者和网关，还原投毒攻击前的状态）。

为了能在攻击过程中观察和记录效果，我们编写一个sniff函数来嗅探网络上的流量：

这个sniff函数会在开始嗅探前休眠5秒❶，给投毒线程留下足够的启动时间。它只嗅探那些带有受害者IP地址的数据包❷，并且仅嗅探指定的个数（默认为100个）❸。嗅探完这些数据包后，sniff函数会把它们存进一个名为arper.pcap的文件里❹，将ARP表中的数据还原为原来的值❺，然后终止投毒线程。

最后，restore函数会给受害者和网关发送正确的ARP信息，将它们恢复为原本的状态：

restore函数可能被poison函数调用（如果你按下了Ctrl+C组合键），也可能被sniff函数调用（如果抓到的包的数量已经满足要求）。它会把网关原本的IP地址和MAC地址发给受害者❶，再把受害者原本的IP地址和MAC地址发给网关❷。

下面我们来测一测这个坏小子的破坏力吧！

小试牛刀

在开始之前，我们需要先对本地主机进行设置，开启对网关和目标设备的流量转发的功能。如果你使用的是Kali虚拟机，就在终端里输入以下内容：

如果你是苹果粉丝，试试这条命令：

开启IP转发后，启动脚本并检查目标设备的ARP缓存。在你的攻击设备上，以root权限运行以下命令：

很好！没有报错，没有其他幺蛾子。现在我们去目标设备上验证攻击效果。在我们的脚本忙着嗅探100个数据包的时候，用arp命令检查一下受害设备的ARP表：

可以看到，可怜的受害者拿着一份被投了毒的ARP表，而其中网关的MAC地址其实是攻击设备的。通过网关上方的那行记录，可以清楚地看到攻击是从192.168.1.203上发动的。当攻击脚本抓完所有包之后，应该能在脚本所在的目录下看到一个名为arper.pcap的文件。当然，你也可以操纵受害者把数据包都发到某个本地Burp实例，或是玩些别的小把戏。刚才抓到的pacp文件或许应该留到下一节“pcap文件处理”再打开看看——你可说不准自己会找到些什么！

pcap文件处理

使用Wireshark或Network Miner之类的工具可以很方便地浏览pcap文件的内容，但有时你可能想用Python和Scapy对pcap文件进行解析和处理。这项技术有很多实用的场景，比如利用捕获的流量生成模糊测试语料，或是单纯地重放你之前捕获的流量。

但这次我们会玩点与众不同的花样。我们会试着从HTTP流量中提取出图片文件，然后使用OpenCV[2]这样的计算机视觉工具对它们进行处理，尝试识别出带有人脸的图像，以此来锁定我们可能感兴趣的内容。你可以用之前的ARP投毒脚本生成一些可供分析的pcap文件，也可以把之前的ARP嗅探器改造成一个实时抓取流量进行人脸识别的工具。

这个示例将分别执行两项任务：从HTTP流量中提取图片，以及从这些图片中检测出人脸。我们将为此编写两个独立的程序。你可以根据手头的工作需求单独选用某个程序，也可以像我们一样把它们连在一起调用。第一个程序，recapper.py，会分析pcap文件，定位数据流中出现的所有图片，并把它们保存到磁盘上。第二个程序，detector.py，会分析每一张图片以判定里面是否出现人脸。如果图片里确实有人脸的话，这个程序就会用一个方框把图片中的每张人脸圈出来，再保存修改后的图片。

首先，我们来编写分析pcap文件所需的代码。在接下来的代码中，我们将用到namedtuple（命名元组），它是Python的一种数据结构，可以通过名称来访问某个字段。标准的元组（tuple）是用来存储一串不可变的值的，跟列表（list）差不多，只是没法修改里面的数据。使用标准元组时，要用数字索引来访问其内部的字段：

而命名元组跟标准元组基本相同，唯一的区别是可以通过属性名来访问它的字段。它能让你写出可读性更高的代码，却又比字典（dictionary）消耗的内存更少。创建命名元组需要两个参数：元组本身的名字，以及由逗号分隔的若干字段名。举个例子，假设你想定义一个名为Point的数据结构，它有两个属性：x和y。你可以这样定义：

接着你可以创建一个，比方说叫p的Point命名元组，p = Point(35, 65)，然后使用p.x和p.y来访问它的x和y属性，就像是在访问一个类的属性一样。这比使用一堆数字索引来访问标准元组要好懂得多。在接下来的示例代码中，我们将创建一个名为Response的命名元组：

现在，无须用数字索引，直接用Response.header和Response.payload就能访问Response的成员数据，大大改善了代码的可读性。

接下来我们学以致用，读取一份pcap文件，重构其中的图片，并把它们保存到磁盘上。创建recapper.py文件，在其中输入以下代码：

这是整个脚本的主要框架，之后我们会往里面填充辅助函数。首先，导入所需的库，然后指定保存图片的目录和pcap文件的路径❶。接着，定义一个名为Response的命名元组，它有两个属性：数据包的头（header）和载荷（payload）❷。我们编写两个辅助函数，分别负责获取数据包头❸和提取数据包内容❹。这两个函数将用在Recapper类里，而这个类负责重构在数据包流中出现过的图片。除了__init__函数外，Recapper类还有两个函数：get_responses，负责从pcap文件中读取响应数据❺；write，负责把在响应数据中找到的图片写到输出目录里❻。

现在我们开始写get_header函数：

get_header函数会读取原始的HTTP流量，并把HTTP头数据单独切出来。我们提取HTTP头的方式是，从数据包开头一路往下找两个连续的“\r\n”，把这整段数据切出来❶。如果拿到的数据里不存在两个连续的“\r\n”，就会产生一个ValueError异常，这时我们只会在屏幕上输出一个横杠（-），然后返回❷。如果没有发生异常，我们就创建一个名为header的字典，把HTTP头里的每一行以冒号分割，冒号左边是字段名，右边是字段值，按这样的方式存进header字典里❸。如果HTTP头里面没有名为Content-Type的字段，就返回None，表示数据包里没有我们感兴趣的内容❹。现在写一个函数，从响应数据里提取内容：

extract_content函数会接受一段HTTP响应数据（Response），以及我们想提取的数据类型的名字作为参数。这段响应数据是一个命名元组，里面有两个部分：header和payload。

如果我们检测到响应数据被gzip或deflate之类的工具压缩过❹，就调用zlib库来解压这段数据。任何一个含有图片的响应包，其数据头的Content-Type属性里都会有image字样（例如image/png或image/jpg）❶。遇到这种数据头，我们就创建一个content_type变量，将数据头里指定的实际数据类型保存下来❷。我们还会创建另一个变量content来保存数据内容，也就是payload中HTTP头之后的全部数据❸。最后，将content和content_type打包成一个元组返回。

写完这两个辅助函数后，就可以开始编写Recapper类了：

首先，初始化这个对象，把要读取的pcap文件路径传给它❶。接着我们会用到Scapy的一个美妙功能，自动切分每个TCP会话，并保存到一个字典里❷，字典里面的每个会话都是一段完整的TCP数据流。最后，创建一个名为responses的空列表，之后我们会把在pcap文件中读到的响应填进去❸。

get_responses函数会遍历整个pcap文件，将找到的每个单独的Response都填入刚才的responses列表：

get_responses函数先遍历整个sessions字典中的每个会话❶，以及每个会话中的每个数据包❷。然后过滤这些数据，只处理发往80端口或者从80端口接收的数据❸。接着，把从所有流量中读取到的数据载荷，拼接成一个单独的名为payload的缓冲区。这个操作相当于在Wireshark中右键单击一个数据包，选择“Follow TCP Stream”选项。如果没能成功地拼接payload缓冲区（最有可能的情况是，数据包里没有出现TCP数据），就在屏幕上打印一个“x”然后继续❹。

重组HTTP数据后，如果payload缓冲区里有数据，我们就把它交给解析HTTP头的函数get_header❺，它能帮我们逐个检视HTTP头的内容。然后，我们把构造出的Response对象附加到responses列表里❻。

最后，遍历整个responses列表，如果发现任何含有图片的响应，就用write函数将这些图片写到磁盘上：

由于我们已经提取完所有响应，所以write函数只需要遍历这些响应❶，提取其中的内容❷，并将内容写到一个文件里就可以了❸。这个文件会被创建到指定的输出目录里，文件名由enumerate函数提供的计数和content_type两个值拼接而成，例如ex_2.jpg就是一个可能出现的图片文件名。当我们运行这个程序时，会创建一个Recapper对象，调用它的get_responses函数来搜索pcap文件中的所有响应，然后将提取出的图片写入磁盘。

在下一个程序中，我们将检查每张图片来确认里面是否存在人脸。对每张含有人脸的图片，我们会在人脸周围画一个方框，然后另存为一张新图片。新建一个文件，命名为detector.py：

detect函数会接受源目录、目标目录和训练目录作为参数。它会遍历源目录中的每张JPG图片（因为我们要找的是人脸，而人脸往往出现在照片中，所以我们想要的图片很可能被保存为.jpg文件❶）。接着，使用OpenCV的计算机视觉库cv2来读取图片❷，加载检测器的XML文件配置，然后创建一个cv2面部检测对象❸。这个检测器是一个预训练好的分类器，可以用来检测人脸正面。OpenCV还提供了其他一些识别算法，能够帮你识别人脸、人手、水果等一大堆东西，你可以自己试试。对于那些检测出包含人脸的图片❹，分类器会返回一个长方形的坐标，对应于检测到人脸的图片区域。在这种情况下，我们会打印一条提示信息，在那张脸周围画一圈绿色的方框❻，然后把图片写到输出目录里❼。

检测器返回的rects数据结构格式是(x, y, width, height)，其中x和y是长方形左下角顶点的坐标，width和height则代表长方形的宽和高。

我们用Python的切片（slice）语法❺将这种坐标格式转换为实际坐标，也就是(x1, y1, x1+width, y1+height)或者说(x1, y1, x2, y2)。这种坐标格式是cv2.rectangle函数所需要的输入形式。

以上代码由Chris Fidao无私分享在其网站上[3]。本示例代码在原版基础上做了一些小改动。现在让我们在Kali虚拟机上测试一下。

小试牛刀

如果你事先没有安装OpenCV库，在Kali虚拟机里打开终端，运行以下命令（再次感谢Chris Fidao）：

这条命令应该能帮你安装好人脸识别算法所需的所有文件。我们还需要下载人脸识别算法的训练文件，例如：

将下载的文件复制到detector.py指定的TRAIN目录中。现在，创建输出目录，放入一个pcap文件，并运行脚本。它应当会产生以下输出：

你可能会看到OpenCV库输出一连串错误信息，因为我们提供的图片中可能有部分已损坏、未下载全，或是使用了不支持的格式（就把编写可靠的图片抽取检验程序当作作业留给你吧）。如果这时快速打开faces目录瞅一眼，你应当能看到好几张带有人脸的图片，以及那些人脸周围神奇的绿色方框。

这项技术可以用来确定你的攻击目标正在浏览什么内容，也可以帮你通过社会工程学找出可能的突破口。当然，你也可以进一步拓展这个脚本，让它不仅能从pcap文件中提取图片，还能和后续章节讲的网页爬取和解析技术结合起来。

[1]链接15。

[2]链接16。

[3]链接17。

5　Web攻击

对所有攻击者和渗透测试工程师来说，分析Web应用绝对是一项至关重要的技能。在现代网络架构中，Web应用往往暴露出最大的攻击面，因此也成为黑客攻破网络的首选途径。

有很多强大的Web应用攻击工具是用Python编写的，比如w3af和sqlmap。坦白地讲，SQL注入这类手法已经过时，而且可用的工具也已经非常成熟，所以不需要再重复造轮子。在本章我们将学习如何使用Python进行基本的Web交互，然后用这些知识编写侦察工具和暴力破解工具。通过编写这几种不同的工具，你应该能学到不少基本技能，让你能够因时制宜地开发出攻击场景中所需的Web应用审计工具。

本章会讨论Web应用攻击的三种场景。在第一种场景中，我们知道目标在使用什么Web框架，而且这个框架恰巧是开源的。一个Web应用框架会有许多文件和层层嵌套的目录结构。我们会在本地分析这个Web应用框架，记录下它的整个文件结构，然后依据这些信息去探测在线目标上实际存在哪些文件和目录。

在第二种场景中，我们只知道攻击目标的URL，所以只好用穷举法构造出可能的文件结构，也就是用暴破字典生成所有可能存在的文件路径和目录名，然后尝试去在线目标上访问这些路径。[1]

在第三种场景中，我们知道某个攻击目标的网站地址和它的登录页面。我们将审计这个登录页面，并尝试暴力破解登录密码。

Python中的网络库

先来看一下Web服务交互可能用到的Python库。在进行基于网络的攻击时，黑客用的可能是自己的机器，也可能是目标网络里某台沦陷的设备。如果使用的是沦陷的设备，它上面可能只安装了最基本的Python 2或Python 3。我们会讲一讲在这种情况下，只用Python标准库能做些什么。但在后面的内容里，我们会假设你使用的是自己的攻击机器，上面装了各种最新版本的第三方Python包。

Python 2中的urllib2库

你一般会在用Python 2.x编写的代码中看到urllib2库。它是Python标准库的一部分，就像在写基础网络工具时一般会用socket库一样，大家写与Web服务交互的工具时就会使用urllib2库。我们来看看如何向No Starch Press网站发起一个简单的GET请求：

这是一个最简单的向网站发起GET请求的例子。向urlopen函数传入一条URL❶，它将返回一个类似文件的对象，让我们可以读取远程服务器返回的数据体❷。由于我们只是在拉取No Starch Press网站的原始页面数据，所以JavaScript之类的端侧脚本都不会执行。

然而在大多数情况下，你会希望对网络请求进行更细粒度的控制，比如自定义HTTP头，处理cookies，或是发起POST请求。urllib2库提供了一个Request类，可以实现这种程度的精细控制。以下示例发送的是相同的GET请求，但这次使用Request类设定了自定义的User-Agent HTTP请求头：

这个Request对象的构造方式和刚才的例子不太一样。为了自定义HTTP头，我们定义了一个headers字典❶，在字典中设定了想要的HTTP头字段名和字段值。这里我们把脚本伪装成了Google爬虫（Googlebot）。接着，我们创建Request对象，向它传入url和headers字典❷，然后将这个Request对象传给urlopen函数❸。它会返回一个普通的类文件对象，用来读取网站返回的数据。

Python 3中的urllib库

在Python 3.x中，标准库提供了urllib库，它将urllib2库分割成urllib.request和urllib.error两个子库。此外，它还增添了urllib.parse库来解析URL。

要用urllib库发起网络请求，你可以用with语法把请求过程写成上下文管理器（context manager）的形式。返回的响应数据读出来应该是bytes类型的。以下就是发起GET请求的方法：

这里我们导入了需要的包❶，并定义了目标URL❷。接着，我们以上下文管理器的模式调用urlopen函数，发起网络请求❸并获得了响应数据❹。

如果想创建一个POST请求，就需要将一个数据字典编码成bytes数据，然后传给request对象。这个数据字典里应该存放了Web应用想要接受的键值对。比如，下面这个例子中info字典里就包含了登录目标网站所需的登录凭证（user和passwd）。

我们将包含登录凭证的字典编码成bytes对象❶，放进传递凭证的POST请求里❷，然后接受Web应用对我们所做的登录尝试的回应❸。

requests库

即使是Python官方文档，也会推荐你用requests库来处理上层的HTTP客户端接口。它不属于标准库，所以你需要另行安装。以下就是安装它所需的pip命令：

requests库的一个有用之处在于它能为你自动处理cookies，你会在下文的许多示例中见识到这一妙处（尤其是在“暴力破解HTML登录表单”小节中攻击WordPress网站的那个案例里）。想用requests库发起HTTP请求的话，可以参考以下代码：

我们创建了url变量、request变量和一个包含user与passwd的data字典。接着，我们发送POST请求❶，并输出响应数据的text字段（一段str类型的字符串）❷。如果你更想要一份bytes类型的结果，可以改用响应数据的content字段。在“暴力破解HTTP登录表单”小节里，你会看到处理bytes类型数据的例子。

lxml与BeautifulSoup库

收到HTTP响应时，lxml库或BeautifulSoup库能帮你解析其中的数据。在过去的几年里，这两个库变得越来越相像。你可以在BeautifulSoup库中调用lxml库的解析器，也能在lxml库中调用BeautifulSoup库的解析器。在其他黑客的代码里你看到的基本都是两者之一。lxml库提供的解析器略快一点，而BeautifulSoup库能够自动解析页面字符编码。这里我们会使用lxml库。这两个库都可以使用pip安装：

假设你将请求到的目标HTML数据存在content变量里。使用lxml库，可以像下面这样读取其中内容并解析出现的链接：

我们要导入io库的BytesIO类❶，因为在解析响应时，需要用BytesIO把bytes数据打包成一个文件对象。接着，正常发起GET请求❷，并用lxml库的HTML解析器解析响应数据。解析器期望读入类似文件的对象或文件名。而BytesIO类能帮我们把服务器返回的bytes数据打包成类似文件的对象传给解析器❸。我们用一条简单的查询来搜索返回的数据中所有包含链接的a（anchor）标签❹，并把它们打印到屏幕上。每个a标签都定义了一条链接，而它们的href属性则指定了该链接的URL。

注意，我们在打印链接时使用的是f-string❺。在Python 3.6及以上版本中，可以使用f-string来构造一些含有花括号的字符串，并在花括号里引用各种变量值。通过这种语法，我们就能在字符串中轻松地引用函数调用结果（例如link.get('href')）或者纯粹的数据（例如link.text）。

如果你选择使用BeautifulSoup，以下代码能够实现相同的解析效果。如你所见，这里的技法跟刚才的lxml示例十分相似：

这些语法基本是相同的。我们将页面内容解析成树结构❶，遍历所有的链接（即a标签，或者说archor标签）❷，然后将链接目标（href属性）和链接文本（link.text）打印出来❸。

如果你操控的是一台攻陷下来的设备，可能会希望尽量避免安装第三方包，以免在网络里引发太多响动，就只好老老实实地用手头已有的东西，比如一套纯净如新的Python 2或Python 3。这就意味着你只能靠标准库（分别指urllib2和urllib）发起攻击。

在后面的例子里，我们会假设你使用的是自己的攻击设备，也就是说你能用requests包连接服务器，用lxml库解析响应数据。

现在你已经掌握了和网络服务或网站对话的基本方法，我们来编写一些可以用在Web应用攻击和渗透测试中的工具吧。

拓印开源网站系统的初始结构

内容管理系统（CMS）和博客平台，例如Joomla、WordPress和Drupal，大大降低了创建博客或网站的难度，因此在共享主机环境甚至企业网络中都已变得相当流行。所有系统在其安装、配置和补丁管理过程中都有自己的难点，这些CMS系统也不例外。当一名劳累不堪的系统管理员或者倒霉的Web开发人员没有严格遵循所有的安全守则或安装规范时，攻击者就有可能轻易地攻入系统。

因为我们可以直接下载任何开源的Web应用，然后在本地分析它的文件和目录结构，所以我们可以用这些信息编写出有针对性的扫描工具，用来扫描远程目标上所有可访问的文件。它能深挖出残留的安装文件、本应被.htaccess文件保护的目录，或其他有助于攻击者突破系统的有用信息。

这个项目还会教你如何使用Python的队列对象（Queue），它可以以线程安全的方式存放一大堆数据，然后让若干线程各自从中取走数据进行处理。这能大大提高扫描器的效率。另外，我们可以放心地认定代码中不会出现竞争条件，因为我们使用的是队列（它是线程安全的）而不是列表。

拓印WordPress系统结构

假设你已经知道要攻击的Web应用使用的是WordPress框架。我们来看新安装的WordPress应该是什么样子。下载并解压一份WordPress的本地拷贝，你可以在其官网[2]上找到最新版本，本书使用的是5.4版本。即使下载的WordPress版本跟欲攻击的目标所使用的不同，它仍能提供一个合适的落脚点，让我们扫描出在大部分版本中普遍存在的常见文件与目录。

为了拓印标准WordPress发行版中的目录与文件，我们会创建一个名为mapper.py的脚本。然后，编写名为gather_paths的函数遍历整个发行版目录，再将遍历得到的每条路径插入名为web_paths的队列中：

首先设定远程目标的网址❶，然后设定一份不想扫描的文件扩展名列表。这个列表可以根据不同的攻击目标而有所不同，这里我们跳过了图片和样式表（CSS）文件。比起它们，我们更关注HTML文件和文本文件，它们之中更有可能藏着有助于我们攻陷服务器的信息。answers变量是一个队列对象，我们会在其中存储最后实际扫描到的路径。web_paths变量❷是另一个队列对象，我们会在其中存储准备扫描的路径。在gather_paths函数中，我们使用os.walk函数❸来遍历本地Web应用安装目录里的所有文件和目录。在遍历的时候，我们构建目标文件的完整路径，并且检查FILTER列表以确保扫描的都是我们感兴趣的文件类型。在本地每找到一个有效文件，我们就把它添加到web_paths队列里。

那个名为chdir的上下文管理器❹需要花点时间额外讲解一下。上下文管理器提供了一种很不错的编程模式，如果你比较健忘，或是你的程序里有太多元素需要管理，上下文管理器都会很有帮助。你会发现它可以用在各种“打开之后需要关闭”、“锁上之后需要释放”或是“修改之后需要还原”的场景里。你可能已经很熟悉Python自带的一些上下文管理器了，比如打开文件用的open函数，或是创建socket用的socket函数。

一般来说，要创建一个上下文管理器的话，需要创建一个带有__enter__和__exit__函数的类。__enter__函数负责返回要管理的资源（例如文件或socket），__exit__函数负责执行清理工作（例如关闭文件）。

但是，如果你的应用场景不需要进行这么细致的管理，也可以使用@contextlib.contextmanager来创建简单的上下文管理器，用它把一个生成器函数转换为上下文管理器。

chdir函数能让你在另一个目录下执行代码，并保证当你退出时，会回到原本的目录。chdir的生成器函数在初始化上下文时，会把当前目录保存下来并跳转到新的目录，将控制权移交给gather_paths❺，之后再回到原本的目录❻。

留意一下chdir函数定义中出现的try和finally代码块。你可能经常遇到try/except语句，但是try/finally却不那么常见。不管try中出现什么异常，finally代码块最后一定会被执行。这里我们需要这个特性，是因为不管目录切换成功与否，我们都希望上下文能还原回原本的目录。以下这个简单的玩具示例，完整展示了try代码块在不同情况下的行为[3]：

回到我们的拓印代码，可以看到在__main__代码块中，我们在一行with语句❼里调用了chdir上下文管理器。这行with语句调用了生成器，向它传递了我们想要执行的代码的目录路径。这里我们传递的是解压WordPress压缩包的目录。在你的电脑上这个路径可能会不一样，所以请确定你传递的是自己的路径。执行chdir函数后，当前目录会被保存，工作目录会切换到参数指定的路径上。接着控制权会被交还给主执行线程，也就是执行gather_paths函数的地方。一旦gather_paths函数执行完，我们就会退出上下文管理器，finally代码块会被执行，工作目录也就还原成原本的目录。

你当然也可以选择手动调用os.chdir函数，但要是你忘记还原相应的改动，就会发现自己的程序在某个意料之外的目录里运行。使用我们新定义的chdir上下文管理器，你就能确信程序自动运行在正确的上下文中，而当你返回时，也会回到原本所在的位置。你可以把这段上下文管理器代码存到自己的工具箱里，以便在其他脚本中使用。花点精力来编写这种清晰易懂的工具代码能够带来源源不断的红利，因为之后你可以一次又一次地在程序中用到它们。

执行程序，遍历WordPress发行版的目录结构，你会看到屏幕上出现如下路径：

现在我们的web_paths队列中塞满了要扫描的路径。你会注意到我们这里特意列出了一些有趣的结果：一些本地WordPress安装目录中存在的，可以去在线目标上尝试访问的文件，其中包括若干.txt，.js和.xml文件。当然，你还可以在脚本中添加额外的识别逻辑，专门筛选出那些特别感兴趣的文件——比如含有关键词install的文件。

扫描在线目标

现在你手里有了WordPress的文件与目录的路径，是时候拿它们来做点什么了，也就是说，扫描远程目标来检查本地拓印下的文件有哪些是实际存在的。在我们后续的攻击中，可能会用这些文件来暴力破解用户密码或搜寻配置中的疏漏。在mapper.py文件中，添加test_remote函数：

test_remote函数是这个扫描器的工作主力。它会一直循环执行代码，直到web_paths队列中的路径被全部取完❶。每次循环时，我们都会从这个队列里取出一条路径❷，把它附加到目标网站的主路径后面，然后尝试访问这个位置。如果访问成功（也就是遇到了HTTP响应码200），就将这个URL添加到answers队列中❹，并在终端输出一个“+”；否则，就输出一个“x”然后继续循环。

有些网站遇到大量请求时会把请求者拉黑，因此我们使用time.sleep函数让每个请求之间相隔2秒❸，希望通过降低发送请求的频率来避免被拉黑。

一旦了解了目标网站的响应情况，就可以删除那些“+”和“x”的输出了。但第一次接触某个目标时，输出那些“+”和“x”字符能帮你确认扫描是否在顺利进行。

最后，我们编写一个run函数作为扫描器的入口：

这个run函数会仔细编排扫描过程，调用我们刚才所写的函数。开启10个线程（这个THREADS = 10是在脚本的开头定义的）❶，并且让每个线程都执行test_remote函数❷。我们会一直等到这10个线程结束（利用thread.join）再退出函数❸。

现在，可以往__main__代码块里填充逻辑来补全整个脚本了。将文件原本的__main__代码块替换成以下代码：

在调用gather_paths之前，先用上下文管理器chdir❶切换到正确的目录。接着，添加一个暂停点，以便在开始扫描之前检查屏幕上的输出❷。这时，我们已经从本地安装目录中搜集了所有感兴趣的文件路径。接着，对远程目标执行主扫描任务❸，并把结果输出到文件中。由于我们很可能得到大量的扫描结果，如果把这些结果输出到屏幕上，我们可能会因为输出太快而错过许多内容。为了避免这一点，我们写了一块代码❹来保存结果。注意，这里使用上下文管理器来打开文件，这能保证我们退出这段代码块时，文件也会被正确关闭。

小试牛刀

笔者维护着一个靶机网站[4]，我们接下来会用它作为演示的目标。你可以自己搭建一个网站，或者在Kali虚拟机上安装一份WordPress进行测试。当然，你也可以使用其他任何易于搭建或已经装好的开源Web应用进行测试。执行mapper.py时，你应该会看到如下输出：

进程结束时，被扫描到的文件路径就会被保存到新建的myanswers.txt文件中。

暴力破解目录和文件位置

在上一个例子中，我们假设自己已经掌握了目标的大量信息。可是如果要攻击的是一个单独定制的Web应用，或者大型电子商务网站，你基本不会有机会掌握网站上所有可访问的文件路径。一般来说，你可以部署一段爬虫程序（比如Burp Suite里附带的那个），用它爬取目标网站，尽可能多地发掘有用信息。但是在大部分情况下，你想要访问的是配置文件、残留的开发文件、调试脚本等关乎信息安全的“面包屑”，它们能够泄露敏感信息，或是暴露出开发者本没打算暴露的功能。想要发掘出这种内容，唯一的办法就是编写一个暴力破解工具来扫描各种常见的文件名和目录。

我们将编写一个简单的工具，它可以兼容常见暴力破解工具的字典文件，比如gobuster[5]和SVNDigger[6]等，并尝试发掘出目标服务器上能够访问的目录和文件。你可以在网上找到很多这样的暴破字典，而Kali发行版里也已经自带了几份（位于/usr/share/wordlists中）。在这里，我们会使用SVNDigger的一份暴破字典。你可以执行以下命令来下载这些文件：

解压后，all.txt暴破字典文件就会被解压到Downloads文件夹中。

像之前一样，我们会创建一整池的线程来主动探测文件。先编写“使用暴破字典生成队列”的功能。新建一个文件，命名为bruter.py，然后输入以下代码：

这个get_words辅助函数❶能够生成一段需要扫描的路径队列。使用它的时候有个小技巧。在这个函数里我们会读取一份暴破字典文件❹，然后遍历文件中的每一行。如果事先将resume参数设定成上次扫描时扫到的最后路径❺，就能从上次扫描中断的位置继续。这样就能妥善地应对网络中断或网站下线等状况。解析完整个文件后，我们就能得到满满一个队列的待扫描路径，留待之后实际暴力破解时使用❻。

注意，这个函数有一个内部函数extend_paths❷。内部函数是指在某个函数内部定义的函数。其实我们也可以把它写在get_words函数的外面，但是因为extend_words函数永远都会在get_words函数这个上下文中运行，所以把前者放在后者里面可以保持全局命名空间干净整洁，让代码更加易懂。

这个内部函数的功能是在路径结尾附加各种扩展名。在很多情况下，你想要扫描的不仅仅是/admin路径，还有admin.php、admin.inc和admin.html等等❸。在这里我们可以来一次头脑风暴，想想除了各种编程语言中的文件扩展名，还有什么常见拓展名的文件是开发者常用却又常常忘记删除的，比如.orig和.bak。这个extend_words内部函数还使用了以下规则：如果某个单词中出现了点号（.），就把它直接附加到URL后面（例如，/test.php）；否则，就把它当作目录名来处理（例如，/admin/）。

不管是哪种情况，我们都会将每个可能的扩展名附加到结果末尾。例如，假设暴破字典里有两个单词，test.php和admin，我们将在单词队列中生成以下额外的内容：

/test.php.bak, /test.php.inc, /test.php.orig, /test.php.php

/admin/admin.bak, /admin/admin.inc, /admin/admin.orig, /admin/admin.php

现在我们来编写主暴破函数：

dir_bruter函数会读取一段由get_words函数生成的路径队列。在程序的开头，我们设定了发送请求时要用的一个User-Agent字符串，这样就将我们的请求伪装成善良用户发送的正常请求。我们会将这段User-Agent字符串添加到headers变量中❶。接着，遍历整个路径队列word。每一轮循环会生成一条指向远程目标的URL❷，然后向远程Web服务器发送请求。

这个函数会将一部分输出直接打印到终端上，一部分输出打印到stderr中。我们可以利用这个技巧更加灵活地输出结果，根据自己想看到的内容，自行控制程序显示哪部分内容。

如果能够看到扫描过程中出现的连接错误，会是件不错的事❸，这种时候我们就会在stderr上打印一个“x”。否则，如果连接成功（收到了HTTP状态码200），我们就将完整的URL打印到终端上❹。你也可以创建一个队列来存放结果，就像我们上一节做的那样。如果遇到了404响应，我们就在stderr中打印一个点“.”然后继续❺。如果遇到其他响应结果（也就是“文件未找到”以外的错误），我们也将URL打印出来，因为这意味着远程Web服务器上可能有什么有意思的东西。你应当对程序的输出结果全程保持关注，因为根据远程Web服务器的配置情况，你可能需要过滤掉一些HTTP错误码才能保证最后的扫描结果干净整洁。

在__main__代码块中，我们生成了要进行暴力扫描的路径列表❻，然后创建了一大群线程来执行扫描任务。

小试牛刀

OWASP（开放式Web应用程序安全项目）维护了一份Web靶场应用名单[7]，里面既有在线靶场也有本地靶场（打包为虚拟机和磁盘镜像）。你可以用这份名单上的应用来测试你的工具。在这里，我们会使用由Acunetix维护的靶场应用进行测试。攻击这些靶场应用的好处在于，它能直观地向你展示暴力破解有多么有效。

建议你将THREADS参数设定为一个合理的数字，比如5，然后再运行脚本。THREADS的值设得过小会让你花很长时间才能扫描完；而设得过大，则会使服务器压力过载。脚本启动不久，你应该就会开始看到类似如下的输出：

如果你只想看到执行成功的结果，可以在调用脚本时将stderr重定向到/dev/null，由于那些“x”和“.”都是输出到stderr的，重定向后，终端上就只剩你扫描到的文件了：

我们从远程网站上能发现一些很有意思的结果，有的结果可能会令人相当惊讶。比如，你可能会发现某个勤勉加班的Web开发工程师留下的备份文件或代码片段。那个index.bak文件里会保存着什么呢？有了这些信息，你就能从你的Web应用中删除那些可能会引狼入室的文件了。

暴力破解HTML登录表单

黑客在实施Web攻击时，有时会需要获取某个目标的访问权限，或者你做安全顾问的时候，可能需要评估某个现存网站系统的密码强度。现在越来越多的网站系统配备了防暴破保护措施，比如一张验证码图片，一行简单的数学算式，或是一段需要附在请求里的登录token数据。虽然目前已经有不少可以破解POST登录请求之类的暴力破解工具，但是大部分情况下它们都不够灵活，以至于无法处理各种动态的内容，甚至无法响应一个简单的“你是人类吗？”这样的复选框。

我们将编写一个简单的暴力破解工具，用于暴力破解流行的CMS系统WordPress。现在的WordPress系统附带了一些基本的反暴破技术，但在默认配置下仍然缺少账号锁定和强验证码机制。

为了暴力破解WordPress，我们编写的工具需要满足以下两点：在登录前，必须从登录表单中提取出隐藏的token数据；必须确保我们的HTTP会话接受cookies。这个远程应用会在用户第一次访问时设定一个或多个cookies记录，并期望用户登录时附上这些cookies记录。为了解析登录表单的值，我们会用到之前在“lxml与BeautifulSoup库”章节中介绍的lxml库。

我们先来看看WordPress的登录表单吧。你可以访问http://<yourtarget>/ wp-login.php找到这个表单，并利用浏览器工具“查看页面源代码”，找出网页的HTML结构。比如，你使用的是火狐浏览器，就可以选择工具 ► Web开发者工具 ► 查看器。为了简便起见，我们这里只展示登录相关的表单元素：

通过阅读这张表单，我们能够发掘出暴力破解工具要用到的那些重要信息。首先，这个表单是以HTTP POST的形式提交给/wp-login.php的❶。接下来的元素全都是成功提交表单所必需的字段：

log变量❷表示用户名，pwd变量❸表示密码，wp-submit变量❹表示“提交”按钮，还有testcookie变量❺表示一个测试cookie。注意，这个testcookie元素在表单上标记的类型是“隐藏”（hidden）。

服务器还会在你访问表单的时候设定几条cookie记录，它期望在你提交表单时，从你那里收到这些记录。这是WordPress反破解对策中最基本的一条。WordPress站点会检查当前用户会话的cookie，所以即使你向登录页面提交了正确的用户名和密码，如果没有这些cookie，照样会登录失败。当一个正常用户登录的时候，浏览器会自动附上这些cookie。我们必须在暴力破解程序中模仿这样的行为，使用requests库的Session对象来自动处理这些cookie。

我们会在暴力破解程序中采用以下请求流程，以求通过WordPress的检查：

1. 拉取登录页面，并接受页面返回的所有cookie。

2. 解析页面HTML数据中的所有表单元素。

3. 将用户名和密码设定为暴破字典中的某个猜测值。

4. 向登录程序发送HTTP POST请求，其中包含所有HTML表单字段和之前保存的cookie。

5. 检查是否已经成功登录Web应用。

Cain & Abel是Windows平台下的一个密码恢复工具，其附带了一个巨大的密码暴破字典，名叫cain.txt。我们将用它来进行破解。你可以从Daniel Miessler的GitHub仓库SecLists里直接下载这个字典[8]：

顺便提一下，SecLists仓库里还存放了一堆其他的字典文件。建议你翻阅一下这个仓库，对你之后的黑客项目会很有帮助。

我们将在这个脚本里用一些新鲜实用的技术。顺便提醒一句，永远不要在在线目标上直接测试你的脚本，而应该安装一套相同的Web应用，在上面设定你自己知道的账号和密码，然后验证脚本能否得到你想要的结果。新建一个Python脚本，命名为wordpress_killer.py，然后输入以下代码：

这个脚本的基本设定需要稍微解释一下。TARGET变量❷中填写的URL指向的是脚本最先下载并解析的HTML页面。SUCCESS变量❶是一个字符串，我们通过检查响应数据中是否存在这个字符串，来确定登录是否成功。

get_words函数❸看起来应该很眼熟，因为和上一小节中的差不多。get_params函数❹会接收一段HTTP响应数据，解析它，然后遍历其中所有的input元素❺，生成一个包含我们要填写的所有参数的字典。现在我们来编写暴力破解工具的底层代码，里面的很多代码应该跟之前的扫描工具很像，所以我们只会着重讲解其中新添加的内容：

这就是我们的主暴力破解类了，它将处理所有的HTTP请求并管理那些cookie。web_bruter函数的主要工作，就是进行暴力破解攻击，具体分为三个阶段：

在初始化阶段❶，初始化一个来自requests库的Session对象，它会自动处理好我们的cookie。接着，发送一个初始请求来获取登录表单。获取原始的HTML数据后，我们将它传递给get_params函数，它会从数据中解析出所有参数，并返回一个包含所有表单元素的字典。成功解析HTML数据后，我们会填好username参数。下面就进入循环猜密码的阶段。

在循环阶段❷，首先休眠几秒以免账户被拉黑。接着，从队列中取出一个密码，把它填进参数里。如果队列里没有密码了，这个线程就会结束。

在请求阶段❸，我们用填好的参数来发送请求。拿到登录请求的结果后，检查这次登录是否成功——也就是返回的内容里是否包含我们之前设定的SUCCESS字符串。如果登录成功，字符串出现在返回的数据里，就清空密码队列，让其他线程赶快退出。

为了将刚才的类打包成暴力破解工具，我们再来添加以下代码：

好了！我们将username和url参数传入Bruter类❶，并且使用words列表创建了一个队列❷，用以暴力破解目标应用。现在我们可以坐下来观赏魔法表演了。

HtmlParser 101

在本节的示例中，我们使用了requests库和lxml库来发送HTTP请求并解析相应结果。但是如果无法安装这些库，只能使用标准库，要怎么办呢？正如我们在本章开头所说，发送请求的部分可以用urllib库来实现，但接下来你需要用标准库中的html.parser.HTMLParser来自己构造解析器。

使用HTMLParser类时，要实现三个主要函数：handle_starttag、handle_endtag和handle_data。handle_starttag函数会在遇到HTML起始标签时被调用，而handle_endtag则正相反，它会在遇到HTML结束标签时被调用。handle_data则是在处理标签之间的原始数据时被调用。每个函数的函数原型都稍有不同，如下所示：

这里有一个简单的示例：

对HTMLParser类有了这样基本的理解后，你就可以完成例如解析表单、爬取网络链接、提取页面中所有的纯文本进行数据挖掘，或是从页面中提取所有图片等任务了。

小试牛刀

如果你的Kali虚拟机上没有安装WordPress，那就现在安装一下。在我们临时搭建的WordPress网站[9]上，我们将用户名和密码预先设定成tim和1234567，以确保脚本能够正常工作——这个密码恰好在cain.txt字典文件第30行左右的位置。运行脚本，就会看到如下输出：

可以看到这个脚本成功破解并登录了WordPress后台。为了验证它能否正常工作，你应该用这些凭证手动登录一次。在完成本地测试，确定脚本能正常工作后，你才可以用它去攻击所选定的WordPress目标。

[1]译者注：作者在之前章节中所提到的“字典”一般指dictionary，是Python中的一种数据结构。但由于暴力破解技术中的“word list”对应的中文术语也是字典，为避免混淆，当作者混用这两个术语时，我会把word list翻译为“暴破字典”以示区分。

[2]链接18。

[3]译者注：新手使用finally时要小心，finally代码块不仅在抛出异常后会执行，正常返回时也会执行。比如你在try代码块中执行了return true，但是finally代码块里写了return false，你的返回结果就会被覆盖，而你一头雾水地调试半天都不知道false是从哪儿来的。

[4]链接19。

[5]链接20。

[6]链接21。

[7]链接22。

[8]链接23。

[9]链接24。

6　编写Burp插件

如果你曾试过攻击Web应用，那么应该已经试过用Burp Suite来爬取网页、代理流量，甚至进行过其他若干攻击了。除了提供标准攻击工具，Burp Suite还允许你构建自己的工具，也就是“插件”（extension）。使用Python、Ruby或是纯Java，可以在Burp图形界面上添加面板菜单，为Burp Suite添加各种自动攻击能力。我们将利用这一点来编写一些好用的工具，用于实施攻击或增强侦查能力。我们要写的第一个插件会用Burp Proxy捕获的HTTP请求作为种子，借助Burp Intruder模块进行基于变异的模糊测试；第二个插件会借助微软Bing搜索引擎的API，对攻击目标进行旁站查询以及子域名查询；第三个插件会从目标网站中提取文本，生成暴破字典，以便之后进行暴力破解密码的攻击。

本章假设你有使用Burp Suite的经验，知道如何使用它的Proxy工具捕获请求，也知道如何使用Burp Intruder将捕获的请求发送出去。如果你需要一份教程来学习这些技巧，可以参阅PortSwigger Web Security官网[1]上的入门指引。

不得不承认，刚接触Burp Extender的API时，我们花了不少时间才搞懂它是如何运作的。这东西对我们来说稍微有点费解，毕竟我们是一帮专注于Python开发的人，基本没什么Java开发经验。但我们在Burp的官网上找到了一堆别人写的插件，从中学到了他们的开发技巧。利用这些现有技术，我们逐步上手编写了自己的代码。本章只涵盖Burp插件的一些基本知识，但我们将教会你如何利用Burp Extender的API文档为自己引路。

配置Burp Suite

Kali Linux里已经预装了Burp Suite。如果你用的是不同的机器，可以从PortSwigger Web Security官网下载并安装Burp。

作为一本讨论Python的书，我们不得不伤心地承认现在你必须安装一份新版Java。Kali Linux上已经预装好了Java。如果你用的不是Kali系统，那就用系统对应的安装管理器（比如apt、yum，或是rpm）安装一份Java。然后，安装Jython—— 一个用Java编写的Python 2实现。直到目前为止，我们编写的所有代码用的都是Python 3语法，但是在本章我们会退回到Python 2，因为这是Jython所要求的语法。你可以在Jython的官网找到它的JAR包[2]。选择Jython 2.7 Standalone Installer，将JAR包下载到一个简单好记的位置，比如你电脑的桌面上。

接下来，直接双击Kali机器上的Burp图标，或是在命令行中执行以下命令：

这样就能启动Burp，你会看到它的图形界面，里面塞满了各种美妙的标签页，如图6-1所示。

图6-1　Burp Suite图形界面加载成功

接着，给Burp添加Jython解释器。单击Extender标签页，然后单击Options子标签页。在Python Environment部分，选中你下载的Jython JAR包，如图6-2所示，其他选项可以保持不变。这样就可以编写第一个插件了。让我们开始吧！

图6-2　配置Jython解释器的位置

Burp模糊测试插件

在职业生涯的某个时刻，你可能会发现没法使用常见的Web应用审计工具对付要攻击的Web应用或服务。比如，某个Web应用可能使用了太多的参数，或是采用某种混淆进行保护，使得人工审计太费时间。我们被这些标准工具坑过很多次，它们总是无法应对那些陌生的通信协议，很多时候甚至连处理JSON都会出错。

这时一个比较有用的解决方法是：先抓包建立一个HTTP流量的基线（抓的时候不要漏掉账户认证cookie），并将基线中的请求数据传给自己编写的一个fuzzer。接下来，就可以在这个fuzzer里随意操作攻击载荷了。我们将从编写世界上最简单的Web应用fuzzer开始，之后你可以在其基础上拓展更多功能。

Burp Suite中有大量工具可供网络审计使用。一般来讲，你会用Proxy捕捉所有的网络请求，如果在其中发现了某个有意思的请求，就会把它转发给Burp Suite的其他工具。一个常用技巧是把它们转发给Repeater工具，这样你就可以手动修改一些有趣的位置，然后重发这个请求。想要对请求参数实现更自动化的攻击的话，可以把请求转发给Intruder工具，它会试着自动找出网络流量中哪些区域是你应该修改的，并允许你使用各式各样的攻击来触发程序崩溃或找出漏洞。我们编写的Burp插件可以通过多种方式和Burp Suite中的任何工具交互。在这里，我们会直接在Intruder工具的基础上开发这些新功能。

首先打开API文档，看看应该利用哪个Burp类来编写这个插件。想要查看文档，你可以单击Extender标签页，然后单击APIs子标签页。这份文档看起来会有点晦涩，因为它写得太偏Java风格了。但是仔细看看，你会发现Burp开发团队对每个类的命名都非常恰当，让人轻轻松松就能搞明白该从哪里开始。这里因为我们想要在Intruder里对Web请求做模糊测试，所以应该关注IIntruderPayloadGeneratorFactory类和IIntruderPayloadGenerator类。我们来看看文档里是怎么描述IIntruderPayloadGeneratorFactory类的：

文档的开头❶讲解了如何在Burp中注册我们的插件。除了IIntruderPayloadGeneratorFactory类，我们还需要拓展主Burp类。接着，可以看到Burp期望我们的主类中实现两个方法。Burp会调用getGeneratorName函数❷来获取插件的名称，并要求我们返回一段字符串。而createNewInstance函数❸则要求我们返回一个IIntruderPayloadGenerator实例，也就是要编写的下一个类。

让我们编写Python代码来满足这些要求。接着我们会搞清楚如何添加IIntruderPayloadGenerator类。新建一个Python文件，命名为bhp_fuzzer.py，然后敲出以下代码：

这个简单的框架勾勒出了满足第一部分要求所需的一切。首先需要导入IBurpExtender类❶，这是编写任何插件所必需的类。随后，导入编写Intruder载荷生成器所需的类。然后，定义BurpExtender类❷，它拓展了IBurpExtender和IIntruderPayloadGeneratorFactory类。接着，使用registerIntruderPayloadGeneratorFactory函数❸来注册我们的类，这样Intruder工具就知道这个类可以生成攻击载荷。然后，实现getGeneratorName函数❹，它仅返回我们的载荷生成器的名字。最后，实现createNewInstance函数❺，它会读取攻击参数然后返回一个IIntruderPayloadGenerator类，这个类命名为BHPFuzzer。

现在我们来看一眼IInturderPayloadGenerator类的文档，搞清楚需要实现什么：

很好！现在我们知道了需要实现这个基类，而这个基类需要提供三个接口函数。第一个函数是hasMorePayloads❶，用来判定是否继续给Burp Intruder发送变异请求。我们会用一个计数器来实现这个功能。一旦计数器达到了设定的最大值，我们就返回False来停止生成模糊测试用例。getNextPayload函数❷接收所捕获的HTTP请求中的原始载荷作为参数。如果在HTTP请求包中选定了多个区域作为载荷，这个函数只会收到要进行模糊测试的那部分数据（后面我们会进一步解释）。这个函数可以使原始数据变异，然后将变异后的数据交还给Burp发送。最后一个函数，reset❸，一般只有在“预先生成了一批模糊测试数据”的情况下才会用到。每当Intruder指定一个载荷位置，fuzzer就可以将预生成的测试数据全部试一遍，当前位置测试结束后，Intruder会调用reset函数通知fuzzer回到开头，等待Intruder指定下一个载荷的位置。但是我们的fuzzer不需要写得那么麻烦，只需要不停地随机变异拿到的每一个HTTP请求就行。

现在我们来看如何在Python中实现这个类。在bhp_fuzzer.py的末尾增加如下代码：

首先定义BHPFuzzer类❶，由它实现IItruderPayloadGenerator类。设置必需的成员变量，然后添加max_payloads❷和num_iterations变量来控制模糊测试结束的时间。当然，如果你想的话也可以让这个插件一直运行下去。但是对于测试来说，还是应该有时间的限制。接着，实现hasMorePayloads函数❸，它仅仅检查是否达到最大测试次数。如果让它固定返回True，就能让插件一直运行下去。getNextPayload函数❹会接收原始HTTP载荷作为参数，这里就是我们要执行模糊测试的地方。current_payload变量的数据类型是bytes，所以要先把它转换为string❺类型，然后传给mutate_payload函数❻。接着，给变量num_iterations加1❼，并返回修改后的载荷。最后一个函数是reset，它直接返回，不做任何事情。

现在我们来编写世界上最简单的模糊测试函数，之后你可以按自己的意愿进行修改。比如，既然这个函数知道载荷数据，那么如果你遇到稍微棘手一点的协议，需要计算CRC校验码或是长度字段的话，就可以把这些计算过程放在这个函数里。将以下代码添加到bhp_fuzzer.py的BHPFuzzer类中：

首先，将载荷数据从某个随机的位置一分为二，切分成front和back两块数据❶。接着，从以下三种变异器中随机选取一种：一种是在front块结尾添加单引号的简单SQL注入检查器❷；一种是在front块结尾添加script标签的跨站脚本（XSS）检查器❸；还有一种变异器是从原始载荷中随机抽取一段数据，将其重复任意次后，附加到front块的结尾❹。现在，我们已经有一个可供使用的Burp Intruder插件了。我们来看如何加载它。

小试牛刀

首先，我们需要加载插件并确定它没有报错。单击Burp的Extender标签页，单击Add按钮。这时应当会弹出一个窗口，让你选择自己编写的fuzzer。请确保你设定的选项与图6-3所示的一致。

单击Next按钮，Burp就会开始加载你的插件了。如果此时出现报错，单击Errors标签页，检查有没有拼写错误，然后单击Close按钮。你的Extender标签界面此时应该与图6-4所示的一样。

图6-3　让Burp加载我们编写的插件

图6-4　Burp Extender显示我们的插件已被加载

如你所见，插件已经加载，Burp也识别出我们注册的Intruder载荷生成器。现在可以用这个插件发起真正的攻击了。确保你的浏览器设定了Burp Proxy（地址为localhost，端口为8080）。我们来攻击第5章攻击过的Acunetix靶机[3]。

作为示例，笔者向这个网站的搜索栏提交了“test”字符串。图6-5展示了我们是如何在Proxy的HTTP history标签页中浏览这个请求的。在这个请求上单击右键，就可以把它发送给Intruder。

图6-5　选中一条HTTP请求将其发送给Intruder

切换到Intruder标签页，单击Positions子标签页，屏幕上应该高亮显示出每个请求参数。这些就是Burp识别出需要进行模糊测试的地方。你可以移动载荷光标，甚至选中整个载荷进行模糊测试，但是目前先让Burp来决定模糊测试哪些数据。图6-6清晰展示了载荷高亮的效果。

单击Payloads子标签页，在这个界面里，单击Payload type下拉菜单，选中Extension-generated选项。在Payload Options块中，单击Select generator按钮，然后在下拉菜单中选择BHP Payload Generator。你的载荷配置界面此时应该如图6-7所示。

图6-6　Burp Intruder高亮显示出载荷参数

现在我们准备好发送请求了。在Burp菜单栏单击Intruder，选择Start Attack。Burp此时应该就开始发送模糊测试请求了，很快就能看到攻击结果。笔者运行这个fuzzer时，得到了如图6-8所示的结果。

图6-7　使用我们的模糊测试插件作为载荷生成器

图6-8　我们的fuzzer在Intruder中发动攻击

正如请求7中的那个加粗警告所示，我们发现了一处疑似SQL注入的漏洞。

即使我们编写的这个fuzzer仅仅只是做演示，你仍然会惊讶地发现它能如此高效地在Web应用中引发报错、暴露应用路径，触发各种常规扫描器无法捕获的行为。最重要的是，我们成功地用自制插件辅助Intruder进行了攻击。现在我们编写下一个插件，来增强对Web服务器的侦察能力。

在Burp中调用Bing搜索

一台Web服务器对外提供多个Web应用并不是件稀奇的事，其中的一些应用可能并不为你所知。如果要攻击某台服务器，应该尽可能地找出上面的所有旁站，因为它们可能会为你提供一个更为脆弱的突破口。在要攻击的目标设备上发现不安全的Web应用并不罕见，甚至有时上面还会残留一些开发数据。微软的Bing搜索引擎可以通过“IP”关键字来搜索所有源于同一IP地址的网站。使用“domain”关键字，Bing还可以告诉你某个域名下的所有子域名。

我们当然可以使用简陋的爬虫代码直接向Bing发送搜索请求，然后解析返回的HTML数据，但是这种行为不太恰当（并且有可能违反了大部分搜索引擎的用户协议）。为了避免惹上麻烦，我们会调用Bing的API来提交搜索请求，然后自己解析结果（访问微软官网页面[4]，免费注册你自己的Bing API密钥）。除了一个右键菜单，我们不会在这个插件里编写任何花哨的图形界面，只会在每次执行查询的时候在Burp中输出查询结果，并自动将查到的URL添加到Burp的目标范围里。

因为我们已经演示过如何阅读Burp API文档，以及如何将它们转换成Python代码，所以这次直接从代码开始。打开bhp_bing.py文件，敲出以下代码：

这是我们的Bing插件的第一部分。请确定在开头正确地粘贴了自己的Bing API密钥❶。你每个月可以免费执行1000次查询。我们先定义一个BurpExtender类❷，来实现标准的IBurpExtender接口。这个类还实现了IContextMenuFactory接口，可以在用户右键单击一条请求时提供相应的右键菜单项。这个菜单项的文本内容是“Send to Bing”。我们还注册了一个菜单处理器❸，它能够判定用户单击了哪个网站，以便我们构造相应的Bing请求。接着，我们编写createMenuItem函数，它会接收一个IContextMenuInvocation对象作为参数，用来判断用户选中的是哪个HTTP请求。最后一步，是渲染菜单项，并使用bing_menu函数响应鼠标单击事件。

现在执行Bing请求，输出结果，并将发现的所有虚拟主机添加到Burp的目标范围中：

用户单击我们刚才定义的菜单项时，就会触发bing_menu函数。我们会先获取当前选中的HTTP请求❶，然后取出每个请求的host数据，把它发给bing_search函数做进一步的处理。bing_search函数首先会辨别host数据是一个IP地址还是域名❷，接着请求Bing去查询同一IP地址的所有虚拟主机❸。如果我们的插件收到的是一个域名，它还会额外查询一下这个域名在Bing上记录过的所有子域名❹。

现在，我们来干点累活，借助Burp的HTTP API来发送和解析Bing的搜索结果。在BurpExtender类中添加如下代码：

Burp的HTTP API要求我们将整个请求拼接成一个完整的字符串再发送。另外，我们还需要添加Bing API密钥才能调用API❶。接着，将HTTP请求❷发往微软服务器。当响应数据返回时，将HTTP头分离出去❸，把剩下的数据体发往JSON解析器❹。每找到一组结果，我们就将其中找到的站点的基本信息输出来❺。如果找到的这些站点不在Burp的目标范围内❻，我们就自动把它们添加进去。

为了实现这部分功能，我们在这个插件里混用了Jython API和纯Python。这个插件应该能在我们攻击某个目标时帮助执行额外的侦查工作。我们来试试吧。

小试牛刀

开启Bing搜索插件的步骤跟之前的模糊测试插件差不多。在它加载成功之后，浏览靶机页面[5]，然后在你刚刚触发的GET请求上单击右键。如果插件加载正确的话，应该能在菜单里看到“Send to Bing”选项，如图6-9所示。

单击这个菜单选项，应该会开始看到Bing返回的结果，如图6-10所示。你看到的结果类型取决于你加载插件时选择的输出。

如果单击Target标签页，然后选中Scope子标签页，应该能看到新记录被自动添加到目标范围（target scope）中，如图6-11所示。这个目标范围圈定了Burp的活动范围，使得攻击、爬取和扫描等行为被限定在目标范围中指定的那批域名上。

图6-9　新的菜单选项显示了我们写的插件

图6-10　我们的插件输出了Bing的查询结果

图6-11　新发现的主机被自动添加到目标范围里

利用网页内容生成暴破字典

很多时候，信息安全总会归结到一样东西：用户密码。这是一条可悲的真理。更糟糕的是，Web应用，尤其是那些定制的Web应用，我们总会发现它们不锁死那些频繁输错密码的账户，又或是不强制要求用户使用强密码。在这些情况下，一个像第5章中那样简单的在线密码暴力破解工具，就足以成为攻破网站的门票。

而在线密码暴破成功的关键是拿到合适的暴破字典。如果时间紧迫，你可没机会试上1000万个密码，所以你需要能够针对目标站点创建暴破字典。当然，Kali Linux里有现成的脚本可以爬取网站并根据页面内容生成暴破字典。但是既然我们已经在用Burp扫描网站了，为什么还要再把一堆流量转发到别处去生成字典呢？另外，那些自带的脚本往往需要你背下堆积如山的命令行参数。像我们这样的人，迄今为止背下来的那堆命令行参数已经足够让朋友们钦佩不已了，所以这种无脑的累活还是直接交给Burp吧。

打开bhp_wordlist.py文件，输入以下内容：

以上代码你应该已经相当眼熟了。首先导入需要的库，接着定义辅助类TagStripper，在之后处理HTTP响应时，这个辅助类会负责将里面的HTML标签剥离出来。TagStripper类的handle_data函数会将页面文本❶保存到一个成员变量中。我们还定义了handle_comment函数，因为想把开发者写的注释也加到暴破字典里。在内部，handle_comment函数实际上会调用handle_data函数❷。这样一来，要是后面想要修改页面文本的处理逻辑，就只需要修改handle_data了。

strip函数会将HTML数据提供给基类HTMLParser，并返回解析出的页面文本❸，这些文本之后会派上用场。剩下的部分和之前写过的bhp_bing.py开头基本相同。和上次一样，我们的目标是在Burp用户界面里添加一个菜单项。这里唯一的新知识点就是，我们会把字典数据保存到一个集合（set）里，这样就能保证扫描过程中不会记录下重复的词。我们初始化这个集合时，在里面放入了大家最喜欢用的密码“password”❹，好让这个单词出现在最终的暴破字典里。

现在我们来添加从Burp中选中HTTP流量并转换为暴破字典的逻辑：

我们的第一个任务，是定义负责响应菜单单击事件的wordlist_menu函数。它能够将用户选中的主机名保存下来❶留待后用，然后读取HTTP响应数据，提供给get_words函数❷。接着，get_words函数会检查HTTP响应头，确保我们处理的都是文本类型的响应❸。TagStripper类❹随即从文本数据中剥离HTML标签。然后，我们用正则表达式匹配所有“以字母开头，后接两个或更多word类型字符（即\w{2,}）”的文本❺。[6]我们会将匹配到的文本以小写形式保存到wordlist变量中❻。

现在，我们为这个脚本再添加最后两个功能：生成更多样的密码，以及呈现最终的暴破字典。

非常好！mangle函数会读取一个基础单词，然后根据一些常见的密码设置策略将它拓展成一堆可能的密码。在这个简单的例子里，我们创建了一串常见的后缀（比如今年的年份❶），然后遍历每个后缀，把它们加到基础单词的结尾❷来生成新密码。之后我们还会用基础单词的大写形式加上后缀再来一次，以保证得到更好的效果。在display_wordlist函数中，我们打印一条“John the Ripper”风格的提示❸，来提醒我们生成暴破字典时使用了哪些站点的数据。接着我们用mangle函数处理每个基础单词，并输出结果。现在，看看这个小家伙的本事吧。

小试牛刀

单击Burp的Extender标签页，单击Add按钮，然后使用跟之前相同的步骤加载我们的字典插件。

在Dashboard标签页，选中New live task，如图6-12所示。

图6-12　在Burp中发起一次在线被动扫描

当对话框出现时，选择Add all links observed in traffic through proxy to site map下拉菜单项，如图6-13所示，然后单击OK按钮。

图6-13　在Burp中配置本次在线被动扫描

配置好扫描参数后，浏览靶机站点[7]来触发扫描。等到Burp访问过目标站点上的每一条链接后，在Target标签页右上角的面板中选中所有的请求，单击鼠标右键，在弹出的菜单中选择Create Wordlist，如图6-14所示。

图6-14　将所有请求发送给BHP Wordlist插件

现在检查插件的Output标签页。在实际应用场景中，我们会把这些数据保存到一个文件里，但是这里为了方便展示，选择直接将暴破字典输出到Burp里，如图6-15所示。

现在可以将这个字典文件发给Intruder模块，让它来执行实际的暴破攻击。

图6-15　根据目标站点文本生成的一份暴破字典

通过生成自己的攻击载荷，以及构造自己的图形界面插件，我们已经领略了Burp API的一小部分功能。在渗透测试过程中，我们经常会遇到一些很具体的问题或需求，而Burp Extender API能为你提供一套绝佳的编程解决方案，或者，最起码能保证你不用一遍一遍地手动把数据从Burp复制粘贴到其他工具里。

[1]链接25。

[2]链接26。

[3]链接27。

[4]链接28。

[5]链接29。

[6]译者注：正则表达式中的word类型字符指小写字母、大写字母、数字和下画线等字符。

[7]链接30。

7　基于GitHub服务的C&C通信

假设你已经攻陷了一台电脑，现在想让它自动执行一些攻击任务，并向你汇报结果。本章我们将编写一套木马框架，在远程机器上它看起来完全无害，但它能接受指令，执行攻击任务。

想要开发一套实用的木马框架，最大的挑战之一是设计合理的控制、更新和传输数据的方案。其中至关重要的一点在于，要有一个比较通用的手段给远程木马推送代码片段：一来普适性较高，可以在各种系统平台上完成多变的任务；二来也便于命令木马有针对性地在某类目标系统上执行代码，而不干扰其他种类的系统运行。

虽然这些年黑客们想出了花样百出的命令与控制（Command and Control，C&C）方案，例如借助互联网中继聊天（IRC）协议甚至Twitter等技术进行C&C通信，但本章要尝试利用一个原本就为分享代码而设计的服务。我们将利用GitHub作为存放木马配置、窃取受害者系统数据的通道，另外，我们也会把木马执行任务所需的所有模块都托管到GitHub上。完成以上工作后，我们还将“魔改”Python原生的导入机制，这样创建新的木马模块后，你的木马就能自动从仓库里直接拉取这些模块以及任何需要依赖的第三方库。

用GitHub执行这些任务是明智之举：一来你和GitHub之间的通信流量都是经过SSL加密的，二来笔者到目前为止还没见过几个公司内网会禁用GitHub。我们将使用私有仓库，以防有人窥探我们的行动。等写完木马的全部功能代码，就可以将它打包成一个可执行文件，植入被攻陷的电脑，理想情况下它能无限期地运行下去。接下来你就能通过GitHub仓库操纵它，看看能发现些什么好东西。

创建GitHub账号

如果你还没有GitHub账号，请打开GitHub网站，注册账号。然后，创建一个名叫bhptrojan的新仓库。接着，安装Python版本的GitHub API库[1]，这样就能写代码来自动化各种GitHub仓库操作了。

现在我们创建这个仓库的基本结构。在命令行中输入以下内容：

这样，就创建了仓库的初始结构。其中config目录存放着每种木马独有的配置文件。部署木马时，你会希望每种木马执行不同的任务，所以每种木马应该检查各自的配置文件。modules目录存放着各种模块可供木马选用。我们会修改Python的导入机制，让木马能够从GitHub仓库里直接导入这些模块。有了这种远程导入能力，你就能通过GitHub下发各种第三方库，避免每次添加新功能或依赖时都重新编译打包可执行木马文件。最后的data目录则是木马用来上交它们收集到的数据的地方。

你可以创建一个GitHub个人访问令牌，当通过HTTPS与GitHub API交互时，可以用令牌代替你的密码。这个令牌应该向木马提供读权限和写权限，因为它既要读取配置信息，又要写入任务的结果。你可以遵照GitHub官网上的文档[2]创建令牌，然后将令牌文本保存成本地文件mytoken.txt。接着，将mytoken.txt这个文件添加到.gitignore中，这样就不会失手把自己的身份凭证推送到仓库里了。

现在，让我们来编写一些简单的模块和一份样例配置文件。

编写木马模块

我们会在之后的章节里用这个木马做些攻击者常做的事情，比如记录击键情况或是抓拍屏幕等。但是作为开场，我们先写点能轻松测试和部署的简单模块。在modules文件夹里创建一个新文件，命名为dirlister.py，并且输入以下代码：

这一小段代码定义了一个名为run的函数，它会把当前目录下的所有文件列出来，并将结果拼成一个字符串返回。你开发的每个模块都应该提供一个接受若干参数的run函数，这样既能以一个相对统一的形式来加载每个模块，又能通过定制配置文件给每个模块传递不同的参数。

现在，我们来编写另一个模块，文件名为environment.py：

这个模块仅仅收集远程设备上设定的所有环境变量。

我们将这些代码推到GitHub仓库上，以便木马使用。在命令行中，进入仓库目录并执行以下命令：

你应该能看到代码被推到GitHub仓库里，你可以登录GitHub确认。这就是你日后开发木马的工作流程。建议你尝试添加一些更复杂的木马模块，以巩固在本章所学的知识。

如果你想调试某个新写的模块，可以把它推送到GitHub上，然后在某个本地木马的配置里启用这个模块。通过这种方式，你就能先在虚拟机或是本地设备上测试新模块，等到测试无误之后，再让远程木马去拉取及调用这些模块。

编写木马配置文件

如果想命令木马执行一些特定行为，就要用某种方式告诉它具体要执行什么行为，以及调用哪些模块能实现这些行为。使用配置文件就能实现这种程度的控制。此外，配置文件还能让我们变相地控制木马休眠（即不下发任何任务给它）。要让整个系统正常运转，所部署的每个木马还应该有一个独一无二的ID。这样一来，我们就能根据木马ID整理收集到的数据，或是控制哪些木马该执行特定的任务。

我们会设定让每个木马去检查config目录中的TROJANID.json文件，它是一个简单的JSON文档，可以解析它、把它转换成Python字典，然后根据里面的信息操控木马的行为。JSON格式使我们能够轻松地修改配置内容。进入config目录，创建abc.json，并输入以下内容：

这只是简单列出了木马应该运行哪些模块。之后，你会看到我们如何读取这个JSON文件，然后遍历每个选项并加载模块。

这里你可以尽情发挥想象力，添加一些额外的实用选项，比如模块执行的时间、模块执行的次数，或者是传递给模块的参数。要是做渗透测试的话，你还可以添加多种窃取数据的手段，我们将在第9章进行更深入的介绍。

打开一个命令行，在你的主仓库目录下执行以下命令：

现在有了配置文件和一些简单的木马模块，可以构建木马主体了。

构建基于GitHub通信的木马

木马主体会从GitHub获取配置信息和要执行的代码。我们首先编写一批函数，用于连接、鉴权以及调用GitHub API。打开一个新文件，命名为git_trojan.py并输入以下代码：

这段简单的初始代码只包含了必要的库引用，能让之后编译的木马文件保持较小的尺寸。这里我们说“较小”，是因为绝大部分由pyinstaller编译的Python可执行文件都有7 MB左右（你可以从官网页面[3]了解更多关于pyinstaller的信息）。我们会把这个编译后的可执行文件部署到已沦陷的设备上。

如果你想利用这一技术搭建大型的僵尸网络（由大量受控设备组成的网络），就需要编写代码来自动生成木马，设定木马ID，创建并部署配置文件，编译打包可执行木马文件等。但是我们这里不打算搭建僵尸网络，所以只好让你自行想象这一切是如何实现的。

现在，我们来编写与木马相关的GitHub代码：

这两个函数会处理和GitHub仓库之间的交互。github_connect函数会读取在GitHub上创建的令牌❶。在创建令牌时，我们将它保存到了一个名为mytoken.txt的文件中。现在我们从该文件中读取令牌，并创建一个GitHub仓库连接。你可能会希望给不同的木马创建不同的令牌，这样就能控制每个木马有权访问哪些数据。即使受害者捕获了木马，也无法溯源并删除你的所有数据。

get_file_contents函数会接受目录名、模块名以及一个GitHub连接作为参数，并返回相应模块的内容❷。这个函数负责从远程仓库里抓取文件并读取里面的数据。我们会用它读取配置文件和模块源代码。

现在我们来编写Trojan类，它负责执行基本的木马任务：

初始化木马对象时❶，我们会设定好它的配置文件和数据目录路径（用于上报木马的输出结果）❷，然后连接GitHub仓库❸。下面编写和GitHub仓库通信时所需的函数：

get_config函数❶会从仓库中读取远程配置文件，这样木马才会知道该运行哪些模块，并通过调用exec函数将模块内容引入木马对象❷。module_runner函数会调用刚才所引入模块的run函数❸。我们会在下一节讨论更多调用run函数的细节。store_module_result函数❹会创建一个文件，其文件名包含当前的日期和时间，然后将模块的输出结果存到这个文件中。我们的木马会利用以上三个函数，把从目标设备上收集到的数据推送到GitHub上。

在run函数❺中，我们开始执行这些任务。第一步是从仓库中拉取配置文件。接着，把模块扔给一个独立的线程去执行。进入module_runner函数后，我们会调用模块的run函数执行其中的代码。当它执行结束后，应该会输出一个字符串，随后我们会把这个字符串推送到GitHub上。

每执行完一个任务，木马都会随机休眠一段时间❻，以尝试绕过防守方的流量特征分析。你当然也可以创建一堆指向Google的流量，或者是访问一些别的正常网站，将你的木马伪装起来。

现在我们来改造Python导入机制，让它能从GitHub仓库远程引用文件。

深入探索Python的import功能

读到这里，你应该会很清楚import功能就是把外部代码库复制到当前程序里，这样就能直接调用它们的代码。我们想在木马里实现同样的功能，但是我们操纵的是一台远程设备，难免会用到这个设备上不存在的包，然而远程安装软件包是件很麻烦的事。除此之外，我们还希望每次添加一项依赖（比如Scapy）之后，可以确保每个模块都能用上这个依赖。

Python允许我们改动导入模块的过程，如果它没能在本地找到某个模块，就会调用我们定义的import类，这样就能从我们的仓库远程拉取代码。我们要把自己编写的这个类添加到sys.meta_path列表中。现在，编写如下代码构建这个类：

每当解释器尝试加载一个不存在的模块时，都会调用GitImporter类。首先，解释器会调用find_module函数尝试找到这个模块。我们将这个调用交给远程文件加载器来处理。如果能从仓库里找到这个文件，就将其中的代码以base64解码，存储到我们的类里❶（GitHub给我们的数据默认是经过base64编码的）。接着我们返回self，告知解释器找到了这个模块，而且解释器可以通过调用self的load_module函数来实际加载模块。在这个函数里，我们调用Python原生的importlib库创建了一个空白的模块对象❷，并将我们从GitHub上拉取到的代码填进去。最后一步是将新创建的模块插入sys.modules列表❸，这样未来的任何import语句都能直接找到它。

现在我们给这个木马加上点睛之笔：

在__main__代码块里，将GitImporter添加到sys.meta_path列表中，创建木马对象，并调用它的run函数。

下面让我们来试一下这个木马吧！

小试牛刀

很好，我们在命令行里运行这个木马来看一看：

警告：如果你的文件或环境变量里保存了任何敏感信息，而你却没有使用私有仓库，那么你的敏感信息就会被推到GitHub服务器上公开给全世界了。不要说我们没警告过你。当然，你也可以使用第9章的加密技术来保护自己。

完美！它连接上仓库，读取了配置文件，拉取了配置文件中设定的两个模块，并且运行了它们。

现在在你的木马文件夹里，运行以下命令：

很棒！木马提交了两个模块的执行结果。

你可以对核心的C&C机制进行若干改进。比如对你的模块代码、配置文件和窃取的数据进行加密就是个不错的着手点。如果还想模拟黑客攻击，感染大量设备的话，还需要自动化整个拉取数据、更新配置和发布木马的流程。随着添加的功能越来越多，你可能还会需要拓展Python加载预编译的动态库（dll或so库）的能力。

接下来，我们编写一些独立的木马功能，你可以自己试着把它们整合到上面的木马项目中。

[1]链接31。

[2]链接32。

[3]链接33。

8　Windows下的木马常用功能

部署木马后，你可能会想用它执行一些常见任务，比如记录键盘输入，截取屏幕图像，或是执行shellcode以便给CANVAS或Metasploit等工具弹回一个交互会话等。本章将集中讲解如何在Windows平台上实现这些能力。我们还会附带介绍一些沙箱检测技术，用来确定木马是否在一个反病毒或取证沙箱中运行。这些模块易于改进，并且能够兼容第7章中所开发的木马框架。在之后的章节中，我们还会介绍一些木马中能用的提权技术。注意，我们所讲的每项技术都有其自身的疑难之处，并有可能被终端用户或杀毒软件抓到马脚。

建议你在植入木马后，仔细地对目标设备建模，这样就可以先在本地测试环境中对模块进行充分的测试，再把它们投放到在线目标上。现在我们来编写一个简单的键盘记录器吧。

键盘记录

键盘记录（keylogging），即利用一段隐蔽的程序连续记录键盘敲击事件，是本书中出现的最古老的技术之一，但它仍然频繁出现在各种级别的信息窃取活动中。攻击者之所以还在使用这项技术，是因为它能极其高效地捕捉到各种敏感信息，比如账号、密码或聊天记录等。

有一款第三方库，PyWinHook[1]，能截获所有的键盘事件。PyWinHook是PyHook的一个分支，并且已经支持Python 3。它利用Windows的原生函数SetWindowsHookEx，让我们能够挂载自定义的钩子函数来监听特定的Windows事件。注册键盘事件的钩子函数，就能截获目标的每一次按键。除此之外，我们还想知道用户按键时在使用什么程序，以确定用户何时输入了用户名、密码或其他敏感信息。

PyWinHook为我们处理好了所有底层编程细节，让我们可以专注于键盘记录器的核心逻辑。创建一个文件keylogger.py，输入以下内容：

我们定义了常量TIMEOUT，创建了新类KeyLogger，并写了get_current_process函数用来抓取活跃窗口和相应的进程ID。在这个函数中，我们首先调用GetForeGroundWindow函数❶，它会返回当前目标桌面上活跃窗口的句柄。接着，将这个句柄交给GetWindowThreadProcessId函数❷，获取这个窗口对应的进程ID。然后，打开这个进程❸，利用打开的进程句柄，可以找到进程实际的程序名❹。最后一步是使用GetWindowTextA函数❺来抓取窗口标题栏的完整文本。在这个辅助函数的结尾，我们将抓取到的所有信息❻都输出到一个漂亮的文本头里，这样就能清楚地看到每次按键属于哪个进程和窗口。现在我们来填充键盘记录器的代码：

我们从run函数开始一步一步讲。第7章我们编写了许多木马模块，每个模块都有一个名为run的函数作为入口点，这里使用相同的模式来编写键盘记录器，以便能以同样的方式调用run函数。第7章中C&C系统的run函数不接收任何参数，并且会返回它的输出结果。为了做出相符的行为，我们也临时把stdout输出重定向到一个类似文件的对象中（StringIO对象）。现在，写进stdout的所有输出都会写进这个对象里，以供之后调取。

重定向stdout之后，创建KeyLogger对象和PyWinHook的HookManager❹。然后，将KeyDown事件绑定到KeyLogger的mykeystroke回调函数上❺。接着，命令PyWinHook钩住所有的按键事件❻，一直执行到我们设定的结束时间为止。每次目标在键盘上按下一个键，mykeystroke函数都会被调起，并且收到一个event对象作为其参数。在mykeystroke函数中，我们要做的第一件事是检查用户是否切换了窗口❶，如果是，就抓取新窗口的名称和进程信息。接着我们会查看用户按下的键❷，如果是ASCII可打印字符，就简单地将它打印出来；如果是一个修饰键（如Shift、Ctrl、Alt等按键）或其他非标准按键，就从事件对象里拉取按键的名称。我们也会检查用户是否在进行粘贴操作❸，如果是，就把剪切板里的内容都记录下来。mykeystroke函数的结尾会返回True，这样就能让排在其后的键盘钩子（如果存在的话）继续处理这些事件。让我们来试试它的效果吧！

小试牛刀

对这个键盘记录器的测试非常简单，只需要运行它，然后像往常一样使用Windows系统就行。你可以尝试运行一下浏览器、计算器或其他应用，然后在终端查看结果：

可以看到，我们在键盘记录器运行的主窗口输入了单词test，还打开了Firefox浏览器，浏览了nostarch.com，然后使用了一堆杂七杂八的软件。现在可以放心地宣称，我们的木马增加了一项“键盘记录”功能！下面我们来学习如何截取屏幕。

截取屏幕

大部分恶意软件和渗透测试框架都有在远程目标上截取屏幕的能力。它能帮我们抓取图片、视频片段，或是获取其他通过抓包或键盘记录获取不到的敏感信息。多亏有pywin32库，我们可以用它来原生调用Windows API，实现截取屏幕的功能。用pip安装这个库：

图像抓取器可以通过Windows图形设备接口（GDI）来获取屏幕大小等必要信息，并进行截图操作。有些截图软件只会截取当前活跃的窗口或应用的屏幕，但这里我们会抓取整个屏幕。下面开始吧！打开screenshotter.py，输入以下代码：

来看看这段代码做了什么操作。我们获取了整个桌面的句柄❷，它包含了所有显示器的全部可显示区域。我们还获取了屏幕的尺寸❶，这样就能知道截图所需的尺寸。然后我们以之前获取的桌面句柄为参数，调用GetWindowDC函数❸创建了一个设备上下文（关于设备上下文和GDI编程的更多信息，可以参考MSDN）。下一步，创建一个基于内存的设备上下文❹，在将截图数据写入文件之前，我们都会把截图数据缓存在这个内存设备上下文里。接着，创建一个位图对象❺，将它的格式、长度、宽度设置成和桌面设备上下文相符，然后调用SelectObject函数，将内存设备上下文指向我们要捕获的这个位图对象。再下一步，我们使用BitBlt函数❻将桌面图片逐位复制并保存到内存设备上下文中，可以把这个过程想成是对GDI对象调用memcpy函数。最后一步，把内存中的图片数据保存到磁盘上❼。

这个脚本测试起来很简单，你只需要在命令行里运行它，然后检查文件夹里有没有出现一个screenshot.bmp文件就行了。也可以把这个脚本推到GitHub C&C仓库里，因为run函数❽会调用screenshot函数来截图，再读取并返回图片数据。

接下来我们来讨论如何执行shellcode。

以Python风格执行shellcode

有时候，你可能会想和某台被攻陷的目标设备进行交互，或是从最爱的渗透测试框架或漏洞利用框架里挑个新鲜有趣的漏洞利用模块运行一下。对于这种场景，我们一般会想办法执行一段shellcode。为了能够在无文件落地的情况下执行二进制shellcode代码，我们需要在内存里创建一段缓冲区来存储shellcode，并调用ctypes库创建一个指向这段缓冲区的函数指针，然后就可以直接调用这个函数了。

在这里，我们会调用urllib库从Web服务器上拉取一段base64编码的shellcode，然后将它运行起来。下面我们开始吧！打开shell_exec.py，输入以下代码：

这段代码不错吧？我们的主代码块从get_code函数开始，它会从Web服务器上拉取base64编码的shellcode❶。接着，调用run函数将shellcode写入内存并执行。

在run函数中，我们分配一段缓冲区❺来存储解码后的shellcode。接着，调用write_memory函数将这段缓冲区写入内存❷。[2]

要把shellcode写入内存，首先必须分配所需的内存（VirtualAlloc），然后将缓冲区中的数据转移到新分配的内存中（RtlMoveMemory）。为了保证我们的shellcode在32位和64位Python上都能正常执行，必须明确指定VirtualAlloc返回的数据类型是指针。RtlMoveMemory函数的参数是两个指针和一个size对象，也就是要设定VirtualAlloc.restype和RtlMoveMemory.argtypes❸。少了这一步，VirtualAlloc函数返回的内存地址长度可能会和RtlMoveMemory所需的长度不吻合。

调用VirtualAlloc函数时❹，参数0x40表明了这段内存应该同时具有读/写和执行权限；否则，我们会无法写入并执行shellcode。接着，将缓冲区中的数据移到新分配的内存，并返回相应的指针。回到run函数，我们调用ctypes.cast函数将指向这段缓冲区的指针转换成函数指针❻，这样就可以像调用普通的Python函数一样调用shellcode。最后，调用这个函数指针，让这段shellcode开始执行❼。

小试牛刀

你可以手写一些shellcode，或是使用喜欢的渗透测试框架（如CANVAS或Metasploit）来生成shellcode。由于CANVAS是商业软件（没有公开文档），所以不妨看看Metasploit的载荷生成教程[3]。我们从Metasploit的载荷生成器中挑选了几个Windows x86的shellcode（例如msfvenom）。输入以下命令，在你的Linux设备上生成shellcode，并保存到/tmp/shellcode.raw下：

我们使用msfvenom创建了shellcode，然后用标准Linux命令base64对它进行base64编码。另一个小技巧是使用http.server模块将当前的工作目录（这里是/tmp/）转换成Web根目录。这样，所有发到端口8100的HTTP文件请求，它都会为你自动处理。现在将我们的shell_exec.py模块下发到Windows机器上执行，应该会在Linux机器上看到如下输出：

这就表示你的脚本从你搭建的服务器上拿到了shellcode，如果一切正常，你应该能从渗透测试框架里收到回弹的shell，并且看到设备运行了计算器（calc.exe），拿到了反向TCP shell，弹出了对话框，或是执行你在shellcode里指定的别的动作。

沙箱检测

越来越多的杀毒软件开始运用沙箱技术来检查可疑样本的行为。不管这个沙箱是运行在网络边界上（近年来的流行趋势）还是运行在目标设备上，都应该尽可能地避免直接撞到目标网络的防护系统的“枪口”上。

我们可以利用一些特征来判断木马是否运行在沙箱里。我们会监控目标设备最近的用户输入，然后添加一些简单的逻辑来分析用户的键盘输入、鼠标单击和双击情况。一台正常的电脑每天启动后都会与用户有很多交互，而在沙箱里却不会有，因为沙箱一般是由自动化的恶意软件分析程序所操控的。

我们的脚本还会尝试判断沙箱的操作者是否在重复某些相同的输入（比如，鼠标快速地连续单击就比较可疑）以绕过一些初级的沙箱检测方案。最后，我们还会比较用户上一次操作的时间和系统运行的时长，这应该能帮我们很好地判断现在木马是不是在一个沙箱里。

做出判断后，就能决定木马是否还要继续执行了。先完成沙箱检测代码吧。打开sandbox_detect.py，输入以下代码：

写下必要的导入语句，并创建LASTINPUTINFO结构体，来存储精确到毫秒的时间戳，它对应的就是上一次在系统中检测到输入的时间。接着，创建函数get_last_input来检查上一次输入时的时间。请注意，必须先把cbSize变量❶初始化为这个结构体的尺寸，然后才能调用GetLastInputInfo函数，将时间戳填进struct_lastinputinfo.dwTime里。下一步是调用GetTickCount函数❷来判断系统运行了多久。中间的耗时等于系统运行的时间减去上一次输入时的时间。最后一小段代码❸只是一段简单的测试代码，你可以运行这个脚本，然后移动鼠标、敲击键盘，观察这段新代码的运行情况。

值得注意的是，系统的总运行时间和上一次检测到用户输入的时间可能会随着你采用的感染手段而有所变化。比如，你是通过钓鱼战术传播木马的，用户可能需要单击链接或者执行一些其他操作才能被感染。这意味着你应该在最近的一两分钟内就检测到用户输入。但如果你发现机器在10分钟前开机，而这10分钟里都没有任何用户输入的话，那么你的木马就很可能运行在一个沙箱里。这些都是一个优质木马所必备的判断能力。

你也可以使用上述技巧来检查用户是否在“摸鱼”，因为你可能只想在他们频繁操作电脑的时候才去截屏。类似地，你可能会想等他们下线后才去执行传输数据等操作。你还可以记录用户每天的操作，以此确定他们平时的在线时段。

为了达成这样的目标，我们来定义三个阈值，如果用户的输入满足这三个阈值，我们就判定木马不在沙箱里。删除上一个文件结尾的三行测试代码，加入几行代码来监控键盘输入和鼠标单击事件。这次我们会使用一个纯ctypes解决方案，而非PyWinHook方案。你也可以使用PyWinHook轻松完成本次任务，但多学两个差异化的技巧总是有好处的，因为那些杀毒软件和沙箱各自都有侦测我们这些技巧的手段。我们开始编程吧：

创建一个Detector类，并将鼠标单击和按键计数设置为0。get_key_press函数会告诉我们鼠标单击的次数和时间，以及按键的次数。它的工作原理是遍历所有可用的键❶；对于每个键，都调用GetAsyncKeyState函数❷来检查它是否被按下。如果这个键的状态显示为被按下（state & 0x0001为真），就检查键值是否为0x1❸，这是代表鼠标左键被单击的虚拟键值。如果是，就将鼠标单击次数加1，并返回当前的时间戳以备之后计算时间。我们也会检查这个键是否是键盘上的ASCII按键❹，如果是，就把按键总次数加1。

现在，我们将这些函数整合到主沙箱检测循环中。在sandbox_detect.py中添加如下代码：

很好。留意一下代码块中的缩进！我们首先定义一些变量❶，用来追踪单击鼠标的时间，存储关于敲击键盘、单击鼠标、双击鼠标等三个操作的沙箱检测阈值。这里我们每次运行代码都会随机生成这些阈值，但你也可以将它们设定成自己实验所得的阈值。

接着获取上一次用户输入以来所经过的时间❷，一旦认为这个时间明显过长（取决于你采用的感染方式，像我们前面讨论过的那样），就强制退出，结束木马的执行。当然，除了当场结束，木马也可以做一些假装无辜的伪装动作，比如随机读取一些注册表值，或是检查一些文件。完成最初的检查后，我们就会进入检测键盘敲击和鼠标单击事件的主循环。

我们首先检查是否发生按键或鼠标单击事件❸，如果函数返回了一个值，那么它就是发生这些事件时的时间戳。接着，计算两次鼠标单击之间的时间❹，并且将其与阈值❺相比较来确定是否是一次双击事件。除了检测双击事件，我们还会检查沙箱操作者是否在连续发送一连串单击事件❻，以试图骗过沙箱检测机制。例如，在常规的电脑操作过程中看到100次连续鼠标双击可能有点奇怪。如果短时间内鼠标双击事件的计数达到了最大值❼，我们就强制退出。最后一步是检查我们是否通过了所有检测，并且按键次数、鼠标单击和双击数都达到了最大值❽；如果是的话，就退出沙箱检测函数。

建议你平时调整和测试一下这些设置，或是添加一些新功能，比如虚拟机检测等。你还可以持续追踪多台设备的日常使用情况，记录它们的按键、鼠标单击和双击等事件，以找到你认为最合适的设置。当然，我们说的是在你自己的设备上（即你持有的设备——而不是你黑下来的设备）！根据要攻击的目标，你可能要设置更灵敏的阈值，也可能完全无须关心沙箱检测。

本章中所开发的工具为你将来开发的木马提供了基础功能，而且由于我们的木马框架是高度模块化的，所以你可以从这些工具中任意挑拣出需要的模块进行部署。

[1]链接34。

[2]译者注：不理解这里为什么把shellcode写进内存缓冲区后还要再写入一次内存的读者，可以先耐心读完后文，然后系统学习一下Windows的DEP机制。

[3]链接35。

9　数据渗漏

获取目标网络的访问权限仅仅是攻击中的一环。为了能充分利用手中的访问权限，你还需要从目标系统中窃取文档、表格，或是其他有效数据。如果对方部署的防卫机制很严格，比如有本地防御系统或远程防御系统（或两者都有）在检查所有的联网进程，并审核这些进程是否应该有发送信息或连接外网的权限，那么攻击的最后一步可能会变得相当棘手。

本章我们将编写一些用于渗漏加密数据的工具。首先，我们会编写一个脚本来加密和解密文件。接着我们会用这个脚本加密信息，并利用三种不同的手段将它传递出去，分别是：电子邮件、文件传输和Web服务接口。对于每一种手段，我们都会同时编写跨平台和纯Windows两种方案。

对于纯Windows函数，我们主要会用到第8章所介绍的各种pyWin32库，尤其是win32com库。借助Windows COM（组件对象模型），我们可以实现许多实用的自动化功能——从网络服务交互，到在应用中嵌入Excel表格。自Windows XP之后，所有Windows系统都允许用户在应用中嵌入COM对象，而我们会在本章中好好利用这项能力。

文件的加密与解密

我们将使用pycryptodomex包来完成加密任务。可以使用以下命令安装这个包：

现在，打开cryptor.py文件，并引用我们所需的库：

我们将创建一套混合加密流程，同时使用对称加密和非对称加密来实现最好的加密效果。AES算法❶就是一种对称加密算法：它被称作对称加密，是因为它使用同一枚密钥来进行加密和解密。AES算法的加密速度非常快，可以用于处理大量的文本数据。我们将用它来加密要渗漏出去的数据。

我们也会引入非对称的RSA算法❷，它会用到一枚公钥和一枚私钥，其中一枚密钥会被用于加密（一般选公钥），而另一枚密钥会被用于解密（一般选私钥）。我们将使用这个加密算法来加密AES算法所使用的密钥。非对称算法非常适合用来加密少量信息，是加密AES密钥的完美之选。

这种同时使用两种加密算法的方案被称作混合加密系统（hybrid system），是一种非常常用的加密方案。例如，你的浏览器和Web服务器之间建立的TLS通信就用到了混合加密系统。

在开始加密或解密前，需要先为RSA算法生成一对公钥和私钥。也就是说，我们需要编写一个RSA密钥生成函数。在cryptor.py中添加一个generate函数：

这样就可以了——Python就是这么无敌，这么短短几行代码就能完成任务。这段代码会把私钥和公钥分别写入key.pri和key.pub文件中。现在编写一个辅助函数来帮助加载私钥或者公钥：

给这个函数传一个密钥类型（pub或pri）作为参数，它会读取相应的文件，返回密码对象和RSA密钥的长度。

目前我们已经生成了两个密钥，并且编写了一个可以由密钥生成RSA密码对象的函数，可以试着加密数据了：

将明文数据以bytes类型传入并压缩❶。接着，随机生成一枚会话密钥作为AES密码对象所使用的密钥❷，并使用该密码对象对压缩过的明文加密❸。现在信息已经加密，我们还需要将会话密钥和密文一同附在返回的载荷里传回去，这样接收方才能解密这些内容。要添加这个会话密钥，需要用之前生成的RSA公钥将它加密❹，然后将解密所需的全部信息都打包在一段载荷里❺，用base64编码，保存成名为encrypted的字符串返回❻。

现在我们来写decrypt函数的代码：

解密时要把之前的加密步骤反向执行一遍。首先，用base64将字符串解码为bytes数据❶。接着，从这段数据中读取加密后的会话密钥，以及解密所需的其他参数❷。我们会使用RSA私钥解密这个会话密钥❸，并使用这个密钥执行AES算法，解密数据正文❹。最后，将它解压为消息明文❺并返回。

写一段主代码块以便测试这些函数：

这段代码只有一个步骤，生成公钥与私钥❶。这里只是简单调用了generate函数，因为在使用密钥前得先生成密钥。接着，修改主代码块，以调用这些密钥：

生成这些密钥后，我们加密并解密了一小段bytes数据，而且打印出最终结果❶。

基于电子邮件的数据渗漏

既然实现了加/解密数据，就写几个函数把刚才加密的数据渗漏出去吧。打开文件email_exfil.py，用它实现基于电子邮件的数据渗漏：

我们会用smtplib库实现跨平台的邮件收发函数❶；用win32com库实现Windows平台专用的邮件收发函数❷。要想使用SMTP邮件客户端，得先连接一台SMTP服务器（例如smtp.gmail.com，如果你用的是Gmail的话）。所以，在脚本里我们指定了SMTP服务器的地址、连接的端口、用户名和密码❸。下面我们编写一个平台无关的函数plain_email：

这个函数会接收subject（标题）和contents（内容）两个参数，然后生成一条消息❶，其中包含SMTP服务器信息和正文内容。subject将会是我们窃取到的文件的名称，contents将会是加密函数返回的加密文件数据。想要进一步保密的话，可以对标题也进行加密。

接着，连接服务器，输入账号和密码，登录邮箱❷。然后，调用sendmail函数，向它传入我们的账号、收件箱地址，以及邮件消息❸。如果在调用此函数时遇到了问题，可以修改debuglevel属性，这样就能在终端看到连接过程。

现在，我们编写Windows平台专用的邮件收发函数来实现同一功能：

outlook函数和plain_email函数接收的是一样的参数：subject与contents(。我们会使用win32com包创建一个Outlook应用实例❷，并确保发送邮件后立即将其删除❸。这一步是为了确保沦陷设备上的用户不会在自己的发件箱和垃圾邮件里看到我们的渗漏邮件。接着，我们填写邮件的标题、内容和收件箱地址，然后将邮件发送出去❹。

在主代码块里，我们会调用plain_email函数简单测试一下它的功能：

使用这些函数将加密文件发送到你的攻击设备后，你打开自己的邮件客户端，选中邮件正文，将它复制粘贴到一个新文件里；接着，就可以使用cryptor.py中的decrypt函数解密并浏览这个文件了。

基于文件传输的数据渗漏

创建一个新文件transmit_exfil.py，我们将用它实现基于文件传输的数据渗漏。

我们会用ftplib库实现跨平台函数，用win32file库实现Windows平台专用函数。

笔者在Kali虚拟机上搭建了FTP服务器，用于接受匿名文件上传。在plain_ftp函数中，我们会传入要上传的文件的路径（docpath参数），以及FTP服务器的IP地址，也就是Kali虚拟机的IP地址（server参数）❶。

使用Python的ftplib库，我们可以轻松地连接并登录服务器❷，定位目标目录❸，最后把文件写入目标目录❹。

对于Windows专用版本的解决方案，我们会编写一个transmit函数，向它传入我们要上传的文件的路径（document_path）：

就像在第2章所做的一样，我们会创建一个socket，用来连接攻击设备上准备好的端口，这里使用的是10000端口❶。接着，调用win32file.TransmitFile函数来传输文件❷。

主代码块只是做了一个简单测试，将一个文件（这里是mysecrets.txt）传输到目标服务器上：

一旦收到加密的文件，就可以读取并将其解密。

基于Web服务器的数据渗漏

接下来，我们编写一个新文件，paste_exfil.py，来实现基于Web服务器的数据渗漏。这个脚本能够自动将加密文档上传到Pastebin网站[1]的账号上。这样，我们就能用公用网站中转文档，在需要的时候随时访问，而又不会被第三方解密。利用Pastebin这样的知名网站，我们应该能绕过防火墙和代理设备的黑名单规则，否则使用我们自己的IP地址或Web服务器很可能会被拦下。下面先为paste_exfil.py编写一些辅助函数。打开文件，输入以下代码：

我们用requests包来实现跨平台函数❷，用win32com的client类来实现Windows专用函数❶。我们将登录Pastebin网站的服务器，并上传加密后的字符串。为了登录该网站，要设定用户名（username）、密码（password）和API Key（api_dev_key）❸。

现在我们已经写好了库引用和配置信息，下面开始编写跨平台的函数plain_paste：

就像之前的plain_email函数一样，plain_paste函数会接收文件名（用作标题）和加密文件内容作为参数❶。你需要发送两次请求才能在自己的账户中创建一个便签（paste）。第一次，是向login API发送POST请求，提交你的username、api_dev_key和password❷。服务器会返回一个api_user_key，这正是你创建便签所需的凭证❸。第二次请求是发送给post API的❹。你需要向它提交便签的标题（文件名）和内容，并附上api_dev_key和api_user_key❺。这个函数执行完以后，你应该就能登录自己的Pastebin账号，并看到加密数据了。你可以将这个便签下载下来以便解密。

接着，我们来编写Windows专用的版本，利用IE浏览器来上传便签。“IE浏览器？”没错，虽然现在Chrome、Edge和火狐等浏览器更为流行，但是很多企业环境仍然使用IE作为默认浏览器。而且理所当然地，很多Windows版本都卸不掉IE浏览器——所以几乎可以保证我们永远能在Windows木马里用这项技术。

我们来看看如何利用IE浏览器从目标网络里渗漏信息。加拿大信息安全研究员Karim Nathoo曾经指出，利用IE浏览器的COM组件进行信息渗漏有着一个绝佳的优点——它利用的是Iexplore.exe进程，而这个进程大部分时候都是受信任的，不会遭到任何拦截。我们先写几个辅助函数：

第一个函数，wait_for_browser，是用来等待浏览器完成当前操作的❶。第二个函数，random_sleep❷，则能让浏览器的行为多一些随机性，避免它看起来像是预先编程的。这个函数会随机休眠一段时间，这样即使某个任务没有在DOM中注册事件，无法通过wait_for_browser函数等待它完成，我们也可以通过这段随机休眠确保有足够的时间完成该任务。同时，它也能让浏览器表现得更像人类在操作一样。

写完这些辅助函数，我们再来增加登录和打开Pastebin主页的逻辑。不幸的是，没有什么办法能在网页中快速而简单地定位UI元素（笔者靠火狐浏览器和它的开发者工具找了30分钟才找齐了要交互的所有HTML元素）。如果你想利用其他的Web服务传递数据，也需要找出准确的操作时间、DOM交互动作以及所需的HTML元素——万幸，Python能让剩下的工作自动化，变得非常简单。我们再来添些代码：

login函数首先读取DOM中的所有元素❶，并在其中找出账号框和密码框❷，填写了我们提供的账号和密码（不要忘了注册账号）。这段代码执行完后，你应该登录了Pastebin主页，准备好粘贴数据了。我们现在来写这部分代码：

以上代码应该看着都不陌生。我们只是简单地搜寻整个DOM，找出了标题和正文的输入框。submit函数接收的参数是一个浏览器实例、文件名和要粘贴的加密文件内容。

现在我们已经能够登录Pastebin并在上面发便签了，接下来给脚本加上点睛之笔吧：

每当我们想要将某个文档上传至Pastebin时，就调用这个ie_paste函数。它会首先创建一个IE浏览器COM对象的实例❶。有个好消息是，你可以设定这个进程是否在屏幕上显示❷。在调试程序时，这个选项应该设定为1，但需要尽可能保持悄声无息的时候，要把它设定为0。这个选项极其有用，比如当木马检测到用户正在做某个操作时，就可以悄悄开启数据渗漏任务，将你的操作混到用户的操作之中。调用完所有的辅助函数后，就可以直接杀死IE浏览器实例❸，然后退出。

融会贯通

最后，我们将所有的数据渗漏手段都融合到exfil.py中，这样就能通过它来调用前面实现的任意一种渗漏方案了：

首先，导入刚才写的模块和函数❶。接着，创建一个名为EXFIL的字典，将它的值设定为刚才导入的函数❷。这能大大简化调用不同渗漏函数的过程。设定这些值的时候，我们写的是函数名，因为在Python里函数是一等公民，可以被直接当作参数使用。这个技巧有时被称作字典调度（dictionary dispatch），用着很像其他语言的switch/case语句。

现在，我们需要编写一个函数来找出要渗漏的文档：

find_docs生成器会遍历整个文件系统来查找PDF文件❶。找到一个PDF文件后，它就会返回该文件的完整路径，并将控制权临时移交给调用方❷。

接下来，我们写一个主函数来调度整个渗漏过程：

我们会向exfiltrate函数传递要渗漏的文档和要用的渗漏手段❶作为参数。如果这个手段涉及文件传输的话（如transmit和plain_ftp），就需要提供实际的文件，而不是一个编码后的字符串。在这种情况下，我们会从源文件中读取数据，将其加密，然后写到一个临时文件夹中❷。我们会调用EXFIL字典来调度对应的函数，将刚才加密的文件路径传给它❸，等文件渗漏后将其从临时文件夹中删除。

而其他渗漏方案则不需要我们创建文件，只需要读取要渗漏的文件内容❹，将其加密，然后调用EXFIL字典来发送渗漏邮件或是创建渗漏便签❺。

在主代码块里，我们将遍历找到的所有文件。在接下来的测试中，我们会使用plain_paste函数来渗漏文件，当然你也可以在我们编写的6个函数中任选一个：

小试牛刀

这段程序中有大量灵活的构件，但是它的使用方式却相当简单。只需要在主机上运行exfil.py脚本，然后等它提示你，数据已经经由邮件、FTP或Pastebin渗漏，就可以了。

如果你在paste_exfile.ie_paste函数中设定了显示IE浏览器窗口，应该能观看整个渗漏过程。这个过程结束后，浏览你的Pastebin页面应该能看到如图9-1所示的内容。

图9-1　Pastebin上渗漏的加密数据

完美！我们的exfil.py脚本选中了一个名为topo_post.pdf的文档，加密了它的内容，然后将其上传到Pastebin网站。可以将这个便签下载下来，提供给解密函数，从而成功解密其中内容，如下所示：

这一小段代码会打开你下载的便签文件❶，解密其中的内容并写入一个新文件❷。你可以使用PDF阅读器打开这个新文件，查看从受害者机器上窃取到的解密后的原始文档。

现在你的工具箱里已经增添了好几样数据渗漏工具。在实战中选取哪一样，取决于目标的网络环境和网络安全级别。

[1]链接36。

10　Windows系统提权

假设你刚刚攻破了某个Windows内网，在其中执行了恶意代码——也许是利用了一个远程堆溢出，也许是借助了钓鱼攻击，总之现在是时候看看该如何提升权限了。

即使已经获取了SYSTEM或Administrator权限，你可能仍想找出几种获取这些权限的不同途径，以防将来某次系统更新封杀之前采用的提权方案。另外，常备一份周详的提权方案清单也是很重要的，因为某些企业网络中可能运行着一些在你自己的环境里难以分析的软件，这种软件可能只有在同等规模或同样结构的企业网络中才能遇到。

典型的提权攻击一般会选择攻击某个写得稀烂的驱动程序，或是攻击Windows内核的原生漏洞。可一旦攻击代码的质量不过关，或是攻击过程中出了什么意外，我们就会面临系统崩溃的风险。所以这次，我们来探索一些不同的Windows提权思路。一般来说，大型企业的系统管理员都会编写一些计划任务或服务来启动子进程，或是执行VBS或PowerShell脚本来自动执行某些任务。同样，设备供应商也常常会在设备中内置一些类似的自动化任务。某些高权限的任务所依赖的文件或程序有时能被低权限用户修改，因此它们也就能被用来提权。在Windows中这种提权手段数不胜数，我们只会列举其中的几个。但是一旦掌握其中的核心技巧，你就能举一反三，从其他幽暗隐蔽的角落发起提权攻击。

首先，我们会学习如何利用Windows管理规范（Windows Management Instrumentation，WMI）来编写一套灵活的监控接口，用于监控新进程的创建，从中我们能获取一些有用的信息，比如文件路径、创建进程的用户、进程的执行权限等等。接着，我们会将所有文件路径传递给一段文件监控脚本，让它持续监控新文件的创建以及对这些文件的写入操作，这样就能找出哪些文件被高权限进程所依赖。最后，我们会干涉文件创建过程，将自己的代码插入刚刚创建的文件，以此操纵高权限进程执行我们的命令。我们设计的这整个流程不依赖于API劫持之类的敏感操作，所以能避开杀毒软件的侦察雷达。

安装依赖库

为了编写本章中的工具，我们需要安装几个库。在Windows的cmd.exe终端中执行以下命令：

你可能已经在第8章编写键盘记录器和截屏工具时安装过pyinstaller，如果没有的话，那就现在安装吧（你可以用pip）。下面创建一个示例服务，之后用它来测试我们的监视脚本。

模拟受害服务

我们接下来编写的服务将模拟一些大型企业网络中常常出现的安全漏洞。本章后面会讲解如何攻击这些漏洞。这个服务将周期性地将脚本复制到某个临时目录中，然后在该目录中执行脚本。先创建bhservice.py文件：

这里，我们导入了第三方包，设定了脚本的源目录，以及执行脚本的目标目录。接着，编写一个后台服务类：

这个类是编写任何服务都应具备的一个基本骨架。它继承自win32serviceutil.ServiceFramework类，并定义了3个函数。在__init__函数中，我们初始化整个框架，设定脚本运行的位置，设定1分钟的超时时间，并创建了一个事件对象❶。在SvcStop函数中，我们设定服务状态并停止服务❷。在SvcDoRun函数中，我们启动服务并调用服务要运行的main函数❸。下面定义这个main函数：

在main函数中，我们会启动一个循环❶，让它每分钟运行一次（即self.timeout参数设定为1分钟），一直运行到服务收到停止信号为止❷。这个循环运行的时候，会将脚本复制到目标目录执行，然后删除❸。

在主代码块中，我们会处理所有可能的命令行参数。

有时你可能需要在受害者的设备上编写后台服务，这个基础框架就能提供很好的参考。

你可以在No Starch网站的页面[1]找到一段名为bhservice_tasks.vbs的脚本，将它放到bhservice.py程序所在的目录下，并将SRCDIR变量改为当前这个目录。这个目录中现在应该有如下内容：

调用pyinstaller创建一个服务程序：

这个命令创建一个dist子目录，并将bservice.exe文件保存进去。我们进入这个子目录，安装并启动这个服务。也就是以管理员权限运行以下命令：

每过1分钟，这个服务就会将脚本复制到临时目录中执行，然后删除，一直到你执行stop命令为止：

现在可以随心所欲地启动或停止服务了。注意，如果你修改了bhservice.py的代码，就需要用pyinstall重新创建程序，并使用bhservice update命令让Windows系统加载新程序。玩腻了这个服务后，可以执行bhservice remove命令将其删除。

现在你应该已经准备好继续前进了，我们来讲些有意思的内容吧！

编写进程监视器

几年以前，Justin（本书的作者之一）参与了El Jefe（信息安全承包商Immunity的一个项目）的开发。El Jefe的核心是一个非常简单的进程监控系统，设计它的目的是帮助蓝队（防守方）追踪进程的创建和恶意软件的植入。

在某一天的讨论中，Justin的同事Mark Wuergler意识到他们可以将El Jefe系统用于攻击：可以利用El Jefe监控那些以SYSTEM权限运行在目标Windows系统中的进程，深入观察那些潜藏的不安全的文件操作或子进程的创建。这个方案奏效了，他们由此挖掘出一大批权限提升漏洞，拿到了一大串打开“秘境”的钥匙。

原版El Jefe的最大缺陷在于，它会使用DLL注入其他进程，监控对原生CreateProcess函数的调用。然后，它会通过一个命名管道和客户端通信，让客户端把进程创建的详细信息转发给日志服务器。不幸的是，大部分杀毒软件也会钩住CreateProcess函数，所以要么这些杀毒软件会把El Jefe判定成病毒而将其杀掉，要么两者共存而导致你的系统变得很不稳定。

我们将会以无钩子的形式重写El Jefe系统的监控功能，将它改装成真正的攻击型软件，这样就能改善监控功能的可移植性，使它和杀毒软件相安无事。

利用WMI监视进程

WMI（Windows管理规范）接口能够帮助程序员监控系统中的某些特定事件，并且在事件发生时收到系统的回调。我们将利用这个接口，在系统每次创建进程时接收回调，并记录下一些有价值的信息：进程的创建时间，创建进程的用户，进程对应的程序路径和命令行参数，进程ID和父进程ID。WMI接口能够展示出所有高权限用户创建的进程，以及更重要的信息——哪些进程会调用外部文件（比如VBS或批处理脚本）。掌握以上所有信息之后，我们还需要进一步确认进程令牌实际获得的权限。在某些不太常见的情况下，你可能会发现某个进程是以普通用户身份创建的，却持有一堆超出预期的Windows权限可供我们利用。

我们先编写一个非常简单的监控脚本来搜集基础的进程信息，然后在上面添加检查权限的功能。这段代码摘抄自Python WMI官网[2]，我们做了一些修改。注意，为了捕获SYSTEM等用户创建的高权限进程的信息，这个监控脚本也必须以Administrators权限运行。创建process_monitor.py脚本，并添加如下代码：

首先创建一个WMI类的实例❶，并命令它监控进程创建事件❷。接着，进入一个循环，这个循环会等待process_watcher返回一个新的进程事件❸，这个事件是WMI类中一个名为Win32_Process的类，其中包含我们所需的全部信息（查阅MSDN在线文档可以获取更多关于Win32_Process的信息）。这个类中有一个名为GetOwner的函数，我们可以通过调用它❹来确定是谁创建了这个进程。将获取的所有信息打印到屏幕上，然后记录到日志文件里。

小试牛刀

启动这个进程监控脚本，然后运行一些程序，看看会输出什么结果：

启动这个脚本后，我们运行了notepad.exe和calc.exe。如你所见，这个脚本正确输出了进程的相关信息。现在可以给自己放个假，让这个脚本运行一整天，记录下所有运行的进程、计划任务以及各种后台更新服务的信息。如果你运气够“好”的话，没准还能抓到自己电脑里的病毒。你也可以试试登录/退出系统，这些行为更有可能产生一些涉及高权限进程的事件。

有了基本的进程监控系统，我们来进一步完善日志中的权限记录。但是首先，我们需要学习Windows权限的工作原理，了解这些权限为何如此重要。

Windows系统的令牌权限

依照微软的官方文档，一枚“Windows令牌”指的是“一个用于描述进程或线程安全上下文的对象”（请见MSDN上[3]的“Access Tokens”文档）。换句话说，这枚令牌授予的许可权限，决定了一个进程或线程可以执行什么样的任务。

没有吃透这套令牌机制可能会给你带来不小的麻烦。比如在开发某款安全产品时，一名用心良善的开发者可能随手编写了某个系统托盘程序，让普通用户可以用它来控制核心Windows服务（比如驱动程序）。开发者在进程中调用了原生Windows API函数AdjustTokenPrivileges，并且毫无恶意地给系统托盘程序授予了SeLoadDriver权限。这名开发者所不知道的是，如果攻击者侵入了这款系统托盘程序，就能借助这个权限随意加载或卸载任何驱动程序，这基本上相当于在系统里开了一个内核权限的后门（rootkit）——换句话说就是将军了。

注意，如果无法以SYSTEM或Administrators权限运行你的监控脚本，就需要专注观察你当前权限下能够监控的进程。它们之中有没有一些额外的权限可供利用？一个进程如果以普通身份运行，但却被配置了错误的权限，那么它就会是一条获取SYSTEM权限或是在内核中执行代码的绝佳捷径。表10-1列举了一些笔者每次都会检查的重要权限。它没有囊括所有可利用的权限，但却是一个很好的起点。在MSDN的官网上你能找到更完整的Windows权限列表。

表10-1　重要权限

既然已经知道该去查哪些权限，我们就来编写Python脚本自动抓取所监控的进程的权限吧。我们将用到win32security、win32api和win32con等库，如果你所处的环境不允许调用这些库，可以试着将下文的这些代码改写成通过ctypes调用Windows原生API的形式。这个思路肯定是可行的，只是工作量很大罢了。

在process_monitor.py中，径直在log_to_file函数上方添加如下代码：

我们使用进程ID来获取指向目标进程的句柄❶。接着，打开一个进程令牌❷并查询该进程的令牌信息❸，查询时要指定我们查询的是win32security.TokenPrivilege结构。这个函数调用会返回一个元组列表，其中每个元组的第一个成员是权限ID，第二个成员是权限状态（启用或禁用）。由于我们只关心被启用的权限，所以首先会查看启用标志位❹，如果权限被启用，再去查找这个权限ID对应的权限名称❺。

接着，修改目前的代码，让它能够正确地输出并记录权限信息。将这行代码：

修改成：

添加完权限记录代码后，再运行一次process_monitor.py脚本并检查输出结果，应该会看到如下输出：

可以看到，我们成功地记录了这些进程的权限。现在只需要为代码添加一点小小的“智能”，让它只记录那些身为普通用户却拥有敏感权限的进程，就能利用进程监控，发掘出那些以不安全的方式依赖外部文件的进程了。

“赢得”条件竞争

批处理、VBS和PowerShell脚本能够自动处理各种单调的任务，例如持续在Central Inventory服务中注册数据，或从软件仓库里强制下载并安装各种更新。它们简化了系统管理员的工作。而管理员常犯的一个错误，就是没有控制好这些脚本的访问权限。我们就曾多次遇到过这样的情况——一台原本固若金汤的服务器，上面却有一段每天都会以SYSTEM权限运行的批处理或PowerShell脚本，开放给所有用户任意编辑。

如果你在企业环境中运行进程监视脚本足够长的时间（或者你只是安装了本章开头编写的那个示例服务），应该会看到类似这样的进程记录：

可以看到，某个SYSTEM进程启动了wscript.exe程序，并向它传递了C:\WINDOWS\TEMP\bhservice_task.vbs参数。本章开头编写的bhservice应该每分钟都会生成一遍类似的记录。

但如果你检查那个目录中的内容，却并不会看到上面的这个VBS脚本。因为这个服务在创建并执行完该VBS脚本后就会将其删除。我们在各种商业软件中多次见过这种操作，这些软件一般会在某个临时位置上创建文件，将命令写入文件，然后执行这些文件，最后再将它们删除。

想要利用这个时机，就需要与执行脚本的代码赛跑。当软件或计划任务创建了某个文件后，我们需要赶在它执行并删除文件前，把我们自己的代码注入进去。这里的诀窍在于Windows API ReadDirectoryChangesW，通过这个接口，我们能够监控某个目录下所有文件或子目录的变化。我们还可以过滤这些事件，以便确定文件是在何时被保存的。这样一来，我们就能在文件执行前快速地把自己的代码注入进去。你或许会发现单单是持续监控磁盘上的所有临时目录，就能得到很多信息；有时，除了潜在的权限提升漏洞，你可能还会找到其他bug或信息泄露漏洞。

我们先编写一段文件监控脚本，随后在其中实现自动注入代码的功能。保存一个新文件file_monitor.py，然后输入如下代码：

我们设定了要监控的文件夹列表❶，这里写的是两个比较常见的临时文件目录。你可能还想监控其他目录，尽管按自己的想法修改代码就好。

对于列表中的每个路径，我们都会创建一个监控线程。这个线程会调用start_monitor函数。该函数的第一个任务是获取指向被监控目录的句柄❷。接着，调用ReadDirectoryChangesW函数❸，它会在目录中出现改动时通知我们，告知被修改的文件名和改动的具体类型❹。这样，我们就能打印出有用的相关信息。另外，每当检测到文件被修改时，我们还会输出文件的内容以供参考❺。

小试牛刀

打开一个cmd.exe终端，运行file_monitor.py脚本：

在第二个cmd.exe终端里，执行以下命令：

你应该能看到如下输出：

如果这个脚本能够顺利运行，建议你将它放在目标系统上持续运行24小时。你可能会惊讶地看到大量的文件被创建、执行与删除。你还可以运行进程监控脚本来发掘其他值得监控的文件路径，比如监控软件更新的过程，应该会特别有趣。

下面我们来添加代码注入的功能。

代码注入

目前我们已经能够监控进程和文件路径，接下来会尝试将代码自动注入到目标文件中。我们会创建一段非常简单的代码片段，以原服务的权限运行一段编译好的netcat.py程序。使用VBS、批处理或PowerShell脚本，能做的事其实数不胜数，但在这里我们只会编写一段比较空泛的框架，之后你可以在此基础上拓展。修改file_monitor.py脚本，在常量参数后面增加如下代码：

我们要注入的代码会用到这些常量：TGT_IP是受害者的IP地址（即我们注入代码的Windows设备），而TGT_PORT是我们要连接的端口。NETCAT变量指向的是我们在第2章编写的Netcat替代品的路径。如果你还没有把它打包成可执行程序的话，可以现在打包：

接着，将打包生成的netcat.exe程序放到你的目录中，并确保NETCAT变量指向该程序。

我们接下来注入的代码将执行一条命令，弹回一个反向命令shell：

首先设定一个字典，用来记录各个文件后缀名对应的脚本代码片段❶。这些片段中包含一段独特的标记和我们要注入的代码。设定这个标记，是为了避免注入代码时再次触发文件修改事件，再次注入代码，再次触发事件，如此反复操作，陷入死循环中。如果不做任何处理的话，这个死循环会将文件写爆，把硬盘摧残得吱呀作响。而有了这个标记，我们的程序就能检查这个标记是否存在，避免重复修改文件。

接下来，inject_code函数负责处理实际的代码注入和标记检查过程。确认标记尚不存在后❷，我们就会在文件中写入标记和想让目标进程运行的代码❸。现在，我们需要修改主事件循环，在其中检查文件的后缀名，并调用inject_code函数：

这是一段颇为直白的附加代码。我们简单地切下文件的后缀名❶，然后检查字典中是否有关于它的记录❷。如果字典中确实存在该后缀名，我们就会调用inject_code函数。现在来试试看吧。

小试牛刀

如果你已经在本章的开头安装了bhservice，现在就能很方便地测试新写的这个代码注入工具了。首先检查服务是否正在运行，然后启动file_monitor.py脚本。运行到最后，你应该能看到一段输出，显示.vbs文件已经被创建并修改，代码也已经注入成功。在下面的示例中，我们注释了输出文件内容的代码，以节省一些空间：

如果你打开一个新的cmd窗口，应该能看到目标端口已经处于开放状态：

如果一切顺利，你就能使用nc命令或第2章编写的netcat.py连上刚才启动的监听程序。为了检查提权过程是否顺利，可以从Kali虚拟机连接监听程序，然后检查当前的用户名：

这应该足以说明你获取了神圣的SYSTEM权限。代码注入攻击奏效了。

读到本章结尾，你可能会觉得这些攻击有点太生僻了。但如果你在各种大型企业环境中潜伏足够长的时间，就会发现这些攻击手法其实颇为可行。你可以轻松地拓展本章编写的工具，或者有针对性地将它们改造成攻击特定本地账户或应用的工具。WMI本身也是非常有用的本地侦察数据源，攻入某个网络后，可以借助WMI进一步拓展攻击成果。此外，提权能力也是一款木马程序所必备的功能。

[1]链接37。

[2]链接38。

[3]链接39。

11　攻击取证

取证人员就是那些被叫来处理数据泄露事件，或是确认到底有没有发生数据泄露的专业人员。他们通常需要给受害设备的内存拍摄快照，以抓取密钥这类仅存于内存中的信息。幸运的是，有一个才干过人的开发团队已经为他们编写了一套专门的Python框架，Volatility，它是一款高级内存取证框架。入侵事件的响应人员、取证人员，以及恶意软件分析员可以用Volatility框架执行各式各样的任务，例如剖析内核对象，检查并记录内存状态等等。

虽然Volatility是一款防御性的软件，但任何工具只要足够强大，就能攻防两用。我们会使用Volatility探查目标用户环境，并编写一款攻击插件来搜寻虚拟机上防御薄弱的进程。

假设你已经渗透了某台设备，并且发现用户在上面部署了一台虚拟机用于执行各种敏感的工作，那么对方很有可能已经创建过一份虚拟机快照，作为发生意外事故后的保险措施。我们可以使用Volatility内存分析框架来分析这份快照，搞清楚这台虚拟机的用途以及里面运行着什么进程。我们还会调查这台虚拟机里可能存在的安全问题，辅助我们进一步渗透。

开始吧！

安装Volatility

Volatility已经发布好几年了，最近刚刚进行了一次彻底的重写，不仅整套代码迁移到Python 3，整体框架还被重构成一个个独立的模块，保证每个插件运行所需的状态信息都封装在这个插件本身之中。

我们来为Volatility相关项目创建一个单独的虚拟环境。接下来的示例会在Windows的PowerShell终端中调用Python 3。如果你使用的也是Windows系统，请确保安装了git。你可以从其官网[1]下载。

首先，创建并激活一个名为vol3的虚拟环境❶。接着，进入虚拟环境目录，克隆Volatility的GitHub仓库❷，并将它安装到虚拟环境中，最后安装pycryptodome包❸，我们之后要用到它。

想查看Volatility提供的插件及参数列表，可以在Windows上执行以下命令：

在Linux或Mac上，你需要调用虚拟环境中的Python程序，如下所示：

在本章中，我们主要使用命令行来调用Volatility，但事实上还有很多其他方式可以调用这个框架。例如Volatility自带的Volumetric项目——一套开源的Volatility网页图形用户界面[2]。你可以深入学习Volumetric的示例代码，以了解Volatility的各种调用方法。此外，你还可以通过volshell界面访问Volatility框架，它的用法就像常见的Python交互shell一样。

在接下来的示例中，我们会使用命令行界面调用Volatility。为了节省页面空间，我们删减了输出结果中的一些无关内容，所以你看到的实际输出结果应该会更多。

现在，让我们潜入代码，看看框架内部是什么样子的：

这段列表展示的是Windows plugins目录里的Python文件。我们强烈建议你花点时间读这些文件里的代码，你能从中发现Volatility插件的共有结构。这能帮你更好地理解整个框架，更重要的是，能帮你初步把握防守方的思维模式与动机。通过学习防守方的能力与手段，你将更深入地理解如何反制对方的侦察。

现在我们已经准备好了分析框架，接下来还需要找些内存镜像来进行分析。最简单的办法，就是给你自己的Windows 10虚拟机拍个快照。

首先，启动Windows 10虚拟机，打开几个进程（例如记事本、计算器和浏览器等），检视这段内存，追踪这些进程的启动过程。接着，用虚拟机软件拍摄快照。然后打开虚拟机文件的存储目录，你会找到一份以.vmem或.mem为后缀名的快照文件。下面我们来分析它的内容吧。

顺便提一句，你也可以在网上找到一些内存镜像文件。我们本章分析的镜像文件之一，是由PassMark Software提供的[3]。Volatility Foundation官网也提供了一些镜像文件[4]。

探查基本情况

先看看待分析设备的基本信息。使用windows.info插件能够获取内存样本的操作系统和内核信息：

我们使用-f参数指定快照文件的路径，以及要使用的Windows插件windows.info❶。Volatility会读取内存文件并分析，输出该Windows设备的基本信息。可以看到，我们处理的是一台Windows 10.0虚拟机，它只有一颗CPU和一层内存层。

你可能会发现，一边阅读插件代码一边在内存镜像文件上测试会有很好的学习效果。花些时间阅读代码并观察对应的输出，能够帮你更好地理解代码的原理以及防守方的思维定式。

使用registry.printkey插件，我们可以打印出某个注册表键的所有键值。注册表中有着非常丰富的信息，而Volatility能帮我们找出想要的任何键值。这里我们想要检查一下系统中安装的服务。注册表键/ControlSet001/Services中保存着服务控制管理器的数据库，里面记录了系统安装的所有服务：

以上输出内容列出的就是设备上安装的所有服务（我们缩写了部分文本以节省空间）。

探查用户信息

现在，我们来探查虚拟机中用户的信息。cmdline插件能够列出拍摄快照时每个进程的命令行参数。这些进程能够反映用户当时的行为与意图。

这个列表列出了进程ID、进程名，以及进程启动时的命令行参数。可以看到，大部分进程应该都是刚开机时由系统本身启动的。而cmd.exe❶与notepad.exe❷则是常由用户启动的进程。

下面我们来更深入地调查运行中的进程。使用pslist插件，我们能够列出拍摄快照时在运行的所有进程的详细信息：

这里我们能够看到具体进程和内存偏移。为了节省空间，我们省略了几栏信息，列出来的也主要是一些比较有趣的进程，比如刚才在cmdline插件的输出中见过的cmd.exe和notepad.exe进程。

如果能把进程按继承关系呈现出来也很不错，这样就能看清楚是哪些进程启动了其他进程。想实现这一效果就要用到pstree插件：

这样我们就看得更清楚了。每行开头的星号是用来标识进程的父子关系的。例如，userinit.exe进程（PID 4704）启动了explorer.exe进程。类似的，explorer.exe进程（PID 4732）启动了cmd.exe进程（PID 7092）。由这个进程，用户启动了notepad.exe和nc64.exe进程。

现在我们使用hashdump插件检查一下用户密码：

这段输出列出了账号的用户名，以及它们的密码对应的LM与NT哈希值。渗透一台Windows电脑后，攻击者常常会提取里面的密码哈希值。这些哈希值可以被离线暴力破解还原成明文用户密码，也可以用于发动哈希传递攻击（Pass-the-Hash Attack）攻入其他网络系统。不管用户是因为多疑才在虚拟机中执行高危操作，还是公司要求用户在虚拟机中进行某些操作，只要你在攻陷的主机中发现了虚拟机或相关快照，都应该抓住这个机会提取出其中的密码哈希值。

Volatility使这个提取过程变得极其轻松。

我们的示例结果中的哈希值是打过码的，但你可以把自己测试时拿到的密码哈希值放到密码暴破工具中，尝试攻破这台虚拟机。网上能够找到一些在线破解密码哈希值的网站，你也可以试试Kali虚拟机上自带的John the Ripper。

探查潜在漏洞

现在，我们试着用Volatility发掘目标虚拟机上有没有可供利用的安全漏洞。malfind插件能够找出所有进程中可能被注入了代码的内存区域。注意这个关键词“可能”——malfind查找的其实是同时具有读、写、执行三个权限的内存区域。持有这种内存区域的进程是很值得调查的，我们没准能从中发现可以利用的已有病毒。即使没有现成的病毒，我们也可以用自己的病毒去覆写这些内存。

这里我们发现了几个潜在的问题。timeserv.exe进程（PID 1336）属于某个已知的开源软件FreeDesktopClock（PID 6484）。如果这两个进程都在C:\Program Files下的话，倒不一定是个问题，但如果出现在其他目录下，没准是病毒伪装成了那款开源时钟软件。

通过搜索引擎，你还能发现MsMpEng.exe（PID 2160）进程是一款杀毒软件的后台服务程序。即使这种进程中出现了可写/可执行的内存区域，也不太可能是危险分子。当然，我们或许可以用shellcode感染这些内存区域，让它们变成危险分子，所以这些进程还是值得注意的。

netscan插件能够列出机器拍摄快照时持有的所有网络连接，如下所示，其中存在的可疑连接兴许可以用在我们接下来的攻击中。

我们可以看到本地设备（192.168.28.128）发起的一些连接，这些连接显然是连接到若干Web服务器上的❷，已经被关闭了。相比之下，更重要的还是那些标着LISTENING的连接，由已知的Windows进程（如svchost.exe、lsass.exe、wininit.exe）维护的连接可能没什么问题，但是nc64.exe是个未知进程❶。它正在监听4444端口，值得我们用第2章编写的netcat工具试探一下。

volshell控制界面

除了刚才展示的命令行界面，你还可以运行volshell命令，在一款特制的Python shell中调用Volatility。它不仅有Volatility的全部功能，还是一套完整的Python shell。以下示例展示了如何在volshell中调用pslist插件分析Windows镜像：

在刚才这个小例子中，-w参数告知Volatility我们要分析的是Windows镜像，-f参数指定了镜像的位置❶。进入volshell界面后，就可以像使用普通的Python shell一样操作了，也就是说，能够像往常一样导入第三方包，或是编写函数，只不过这个shell里面内置了一套Volatility框架。这里我们导入了pslist插件❷，然后打印了插件的输出结果（使用dpo函数）❸。

你可以运行volshell --help命令学习更多关于volshell的知识。

编写Volatility插件

我们刚刚已经看过如何使用Volatility插件来分析虚拟机快照，发掘其中的安全漏洞，通过命令和运行的进程对用户行为进行侧写，以及提取用户的密码哈希值等等。但既然你能自己编写新的自定义插件，那么Volatility的功能有多强大完全取决于你有多敢想。如果需要在标准插件的基础上获取更多信息，就可以着手编写自己的插件。

Volatility开发团队将插件开发过程设计得非常简单易懂，只要遵循他们的设计模式开发就能行。你甚至可以在自己的插件里调用其他插件，让工作更为轻松。

我们来看看一个插件的一般架构：

编写插件的主要步骤有，继承PluginInterface创建一个新类❶，设定该插件有哪些依赖❷，定义run函数❸，定义generator函数❹。这个generator函数并不是一定要写的，但是通过编写单独的generator函数，可以把这部分逻辑从run函数里分离出去。这是一个好用又常见的设计模式。将generator函数独立出去，并且以Python的generator语法调用它，可以更快地得到结果，并且代码更易懂。

我们用这种基本模式来开发一款插件，用它找出那些没有被地址空间布局随机化（ASLR）保护的进程。ASLR保护可以打乱进程的地址空间，改动堆、栈和其他系统分配空间的虚拟内存地址。这就意味着，漏洞利用代码的开发者在进行攻击时搞不清楚受害进程的内存空间布局。Windows Vista是第一款支持了ASLR的Windows系统。在Windows XP这种老旧的系统内存镜像里，默认是不会出现ASLR保护的。而如今，在现代设备（Windows 10）上几乎所有的进程都受到ASLR保护。

有了ASLR并不意味着攻击者就束手无策，但是它能显著提高攻击难度。所以，作为进程侦察过程的第一步，我们将写一个插件来检查这些进程有没有被ASLR保护。

先创建一个名叫plugins的文件夹。在这个目录中，创建一个名为windows的文件夹，用来保存为Windows编写的所有插件。如果你想编写针对Mac或Linux系统的插件，创建相应的mac和linux目录即可。

现在，在plugins/windows目录中，创建我们的ASLR检查插件aslrcheck.py：

先导入所需的所有包，其中有个pefile包是用来分析PE文件的（Portable Executable，可移植的可执行文件）。接着，编写一个辅助函数来实现分析功能：

将一个PE文件对象传递给check_aslr函数❶，解析它，然后检查它是不是以DYNAMIC模式编译的❷，以及文件中的重定位表是不是被擦除了❸。如果这个文件不是DYNAMIC的，又或是编译的时候虽然设定了DYNAMIC，但重定位表却被擦除，那么这个文件就无法受ASLR保护。

有了这个check_aslr函数，我们就能编写AslrCheck类了：

创建插件的第一步，是从PluginInterface对象继承一个新类❶。接着，设定插件依赖的组件。参考其他插件的代码，就能搞清楚我们的插件需要些什么。首先每个插件都需要有内存层，所以我们先设定一个内存层❷。有了内存层之后，还需要符号表❸。你会发现几乎所有的插件都需要用到这两个组件。

我们还需要依赖pslist插件，以获取内存中的所有进程，并从这些进程中重组出PE文件来❹。接着，我们会将重组出的PE文件传给检查函数，检查它们有没有受到ASLR保护。

某些时候我们可能会想通过指定PID来检查某个特定进程，所以这里创建一个设置选项，可以由此传入一段PID列表，让插件只检查列表里的进程❺。

为了处理这段可选进程ID列表，我们使用类函数（class method）创建了一个过滤函数，每次遇到列表中存在的PID，过滤函数就会返回False；也就是说，我们向过滤函数提问“我应该过滤掉这个进程吗？”，只要PID不在列表里，该函数就应该返回True。

我们创建了一个特殊的数据结构pe_table_name❶，在遍历进程时会用到它。接着，读取每个进程的进程环境块（Process Environment Block，PEB），并将它存到一个对象中❷。PEB是一种数据结构，里面存储了当前进程的大量信息。我们会将这块区域写入一个类似文件的对象（pe_data）❸，使用pefile库将它转换为一个PE对象❹，再将它传给check_aslr辅助函数❺。最后，将进程ID、进程名、进程内存地址、是否受ASLR保护等信息打包成一个元组，通过yield传递出去❻。

现在我们编写run函数，它不需要任何参数，因为所有的设置都写在config对象里了：

使用pslist插件拿到进程列表❶，然后将generator返回的数据传给TreeGrid渲染器❷。很多插件都用到了TreeGrid渲染器，它能确保每个进程都有一行单独的输出结果。

小试牛刀

这次我们来分析Volatility官网上发布的一份镜像文件：Malware - Cridex。为了调用自定义插件，我们需要用-p参数来指定插件所在的文件夹：

如你所见，这是一台Windows XP设备，上面的所有进程都没有ASLR保护。

以下是干净的、最新版本的Windows 10系统的输出结果：

这里面没有太多有效信息，列出的每个进程都得到了ASLR的保护。但我们发现了内存涂抹（memory smear）的痕迹。内存涂抹是指拍摄内存镜像的时候，内存刚好被修改。这就导致内存表的描述和内存实际内容不符，换句话说，虚拟内存指针可能会指向无效的数据。这种情况处理起来是很麻烦的。但正如错误描述文字所说，你可以试试重新搞一份镜像（另找一份或创建一份新的快照）。

我们再来看一下PassMark的Windows 10示例镜像：

几乎所有的进程都得到了ASLR的保护，只有一个叫SynTPEnh.exe的进程例外。通过在线搜索，我们可以查到它是Synaptics Pointing Device的一部分软件组件，可能是用来实现触摸屏功能的。这个进程只要是装在C:\Program Files目录下，应该就没什么猫腻，但我们之后可以去试着挖它的漏洞。

在本章中，我们学习了如何利用Volatility框架来调查用户的行为、网络连接，分析任意进程的内存数据。你可以利用这些知识更深入地调查目标用户或设备，从中学习防守方的思维模式。

出发！

至此，你应该已经意识到Python是一门绝佳的黑客编程语言，尤其是还有这么多第三方库和基于Python的框架可用。虽然黑客们手里的工具已经泛滥成灾，但自己编写工具仍然是一项不可或缺的能力，因为这能让你更深入地理解其他工具是如何运作的。

行动起来吧，去写一个满足你自己独特需求的特制工具。不管你想写的是Windows下的SSH客户端、网站爬虫，还是一个C&C系统，Python都能为你所用。

[1]链接40。

[2]链接41。

[3]链接42。

[4]链接43。