Developer API

Shodan提供了一个开发者的API(https://developer.shdan.io/api)来编程,获取所需要的信息。可以通过网站完成的事情都可以通过代码完成。

该API分为两部分:REST API和Streaming API。REST API提供搜索Shodan的方法,查找主机,获取关于查询的信息的摘要。Streaming API提供Shodan当前收集的数据的原始实时返回。有几个不同的套餐可以获取,该API获取的数据不能被搜索或用其他方式进行交互,适用于需要获取大量数据的人。

只有购买开发者API计划的人才能获得Streaming API。

使用限制

根据API的套餐不同,API会有不同的限制:

  1. 搜索:每月的搜索次数有不同的限制,且需要使用查询积分。如果直接查询不会消耗查询积分,若进行过滤器或者是超过一页的搜索就需要消耗查询积分。搜索apache不需要消耗查询积分,搜apache country:"US"将会消耗一个查询积分,就算查询第二页也只会消耗一个查询积分。
  2. 扫描:按需获得的API也会根据积分限制每月扫描主机的数量。对于每个主机的扫描都需要一个扫描积分才能扫描。
  3. 网络提醒:根据不同的API的使用次数可以使用提醒功能监视所查询的IP。只有付费客户才能使用此功能,且无法在账户中创建超过100条提醒。

Facets

过滤器缩小搜索结果。而Facets提供关注的banner字段的聚合信息,会得到一个大的结果视图。例如,Shodan网站使用Facets来提供搜索的统计信息,类似于在左侧显示的:

Alt text

Facets有很多可以选择的参数。比如port:22Facetsssh.fingerprint,输出的时候就能显示出网络上SSH的详细条目。

目前Facet只能在API和Shodan的命令行上使用

入门

所有的例子使用python来演示,当然其他的语言也有Shodan的库/客户端

为python环境安装Shodan库:

easy_install shodan

如果已经安装shodan,可以使用:

easy_install -U shodan

初始化

首先要做的是初始化:

import shodan
api = shodan.Shodan('YOUR API KEY')

https://account.shodan.io获取API密钥

搜索

已经拥有API密钥之后,准备进行搜索:

#将请求包放在一个try/except块中,捕获抛出的异常
try:
        # Search Shodan
        results = api.search('apache')

        # Show the results
        print 'Results found: %s' % results['total']
        for result in results['matches']:
                print 'IP: %s' % result['ip_str']
                print result['data']
                print ''
except shodan.APIError, e:
        print 'Error: %s' % e

首先调用api对象的Shodan.search()方法,该方法返回的结果放入字典之中。然后,打印出搜索结果数量,最后将返回的结果进行遍历循环,并打印其IP和banner。每一页的搜索结果多达100个。

Alt text

当然查询的时候还有很多返回信息。下面是Shodan.search返回的部分信息:

{
    'total': 23199543,
    'matches': [
        {
            'data': 'HTTP/1.1 301 Moved Permanently\r\nDate: Thu, 01 Feb 2018 02:43:53 GMT',
            'hostnames':['mypage.ponparemall.com'],
            'ip': 2685469827L,
            'ip_str': '160.17.4.131',
            'port': 80,443,
            'isp': Recruit Holdings Co.,Ltd.,
            'timestamp': '2018-02-01T02:47:34.036371'
        },
        ...
    ]
}

有关banner可能包含的属性的完整列表请参阅附录A

默认情况下,为了节省带宽使用量,banner中的一些大的字段会被截断(比如HTML的)。若想要检索所有的信息,只需使用minify=False禁用概要。例如,用以下代码可以将匿名VNC服务的查询结果全部返回:

results = api.search('has_screenshot:true', minify=False)

任何错误都会引发异常,将API请求封装在try/except是一种良好的代码习惯,不过为了简单起见,例子暂时不用try/except


以上脚本仅输出第一页的搜索结果。要想获得第二页或更多的结果,可以使用page参数执行请求:

results = api.search('apache',page=2)

如果想遍历所有的结果,使用search_cursor()方法会更加便捷 :

for banner in api.search_cursor('apache'):
    print(banner['ip_str']) # 打印出该IP的每个banner

search_cursor()方法只能循环结果,返回banner,不能使用Facets

主机查找

要用Shodan查找特定的IP,可用Shodan.host()函数:

# 查询主机
host = api.host('217.140.75.46')

# 打印一般信息
print """
    IP: %s
    Organization: %s
    Operating System: %s
""" % (host['ip_str'], host.get('org', 'n/a'), host.get('os', 'n/a'))

# 打印所有的banner
for item in host['data']:
    print """
        Port: %s
        Banner: %s
        """ % (item['port'], item['data'])

默认情况下,Shodan只返回最近收集的主机的信息。如果想获取IP地址的完整历史记录,就使用history参数。

host = api.host('217.140.75.46', history=True)

虽然以上代码可以返回所有banner,但其中也包括一些可能不再在主机上活动的服务。

扫描

Shodan每月至少爬取一次,但是如果想用Shodan立即扫描网络,就可以使用API​​扫描。(订购的不同API有不同的扫描次数)

与Nmap等工具扫描不同,使用Shodan进行的扫描是异步完成的,在进行Shodan扫描之后不会马上就收到扫描结果。这取决于开发人员是如何收集扫描结果的,是直接查找IP信息,是用Shodan直接搜索,还是订购实时信息流查找。Shodan命令行界面在启动扫描后,创建临时网络提醒,然后根据实时数据流出扫描结果。

scan = api.scan('198.20.69.0/24')

也可以使用CIDR表示法的地址来提供扫描目标:

scan = api.scan(['198.20.49.30', '198.20.74.0/24'])

在向API提交了扫描请求之后,会返回以下信息:

{
    'id': 'R2XRT5HH6X67PFAB',
    'count': 1,
    'credits_left': 5119
}
  • id:唯一的扫描标识符
  • count: 提交的IP扫描数
  • credits: 剩余的扫描积分

实时数据流

Streaming API是一种基于HTTP的服务,可返回Shodan收集的实时数据流。它不提供任何搜索或查找功能,它只是抓取工具收集的所有内容的概要。

例如,以下是一个脚本的部分代码,可以从容易受到FREAK(CVE-2015-0204)攻击的设备输出banner:

def has_vuln(banner, vuln):
    if 'vulns' in banner['opts'] and vuln in banner['opts']['vulns']:
        return True
    return False

for banner in api.stream.banners():
    if has_vuln(banner, 'CVE-2015-0204'):
        print banner

在上面的例子中,has_vuln()方法检查服务是否可能存在CVE-2015-0204

banner中的许多属性是可选的,这样可以节省空间和带宽。为了使可选属性更容易处理,最好在函数中封装对属性的访问。

注意:常规的API能访问的数量仅仅是订购的API数量的1%,只有订购了API具有许可证的客户才有100%的访问权限

网络提醒

要创建网络提醒,需要提供一个提醒名称和一个IP范围。名称应该具有概述性,当受到提醒的时候,就大概知道是什么监视内容被返回了。

alert = api.create_alert('Production network', '198.20.69.0/24')

正如scan()方法一样,可以提供监视的网络范围列表:

alert = api.create_alert('Production and Staging network', [
    '198.20.69.0/24',
    '198.20.70.0/24',
])

只能使用有限数量的监视IP,并且每个帐户不能有超过100个监视提醒在活动。

将网络提醒与扫描API结合使用时,一个有用的技巧是设置提醒的时间:

alert = api.create_alert('Temporary alert', '198.20.69.0/24', expires=60)

上述代码可以使得提醒会激活使用60s,60s之后就会停止,不再进行监视。

成功创建提醒之后,API将返回以下对象:

{
    "name": "Production network", 
    "created": "2015-10-17T08:13:58.924581", 
    "expires": 0, 
    "expiration": null, 
    "filters": {
        "ip": ["198.20.69.0/24"]
    },
    "id": "EPGWQG5GEELV4799",
    "size": 256
}

订阅

一旦创建了提醒,就可以将其用作实时数据流的监视。

for banner in api.stream.alert(alert['id']):
    print banner

与常规实时流一样,该alert()方法提供了一个迭代器,其中每个项目都是由Shodan爬虫收集的banner。idalert()方法需要的唯一参数,在有网络提醒时会返回提醒ID。

使用Shodan命令行界面

接下来讲述如何使用Shodan的命令行界面快速清除基于Python代码创建的提醒。

clear命令将会清除电脑上创建的所有alert。

清除所有alert:

$ shodan alert clear
Removing Scan: 198.20.69.0/24 (ZFPSZCYUKVZLUT4F)
Alerts deleted

列出alert列表,确认有无alert:

$ shodan alert list
You haven't created any alerts yet.

创建一个新的alert:

$ shodan alert create "Temporary alert" 198.20.69.0/24
Successfully created network alert!
Alert ID: ODMD34NFPLJBRSTC

最后一步是订阅监视提醒,并存储返回的数据。要为创建的警报传输结果,将ODMD34NFPLJBRSTC的警报ID提供给stream命令:

$ mkdir alert-data
$ shodan stream --alert=ODMD34NFPLJBRSTC --datadir=alert-data

上面的命令中,使用ID为ODMD34NFPLJBRSTC的alert提醒,并且使用数据流传输结果。结果将存储在名为alert-data的目录中。每天都会在alert-data目录中生成具有一个当天收集banner的新文件。也就是说,不用关心轮转文件,stream命令会处理这些。几天后,目录将如下所示:

$ ls alert-data
2016-06-05.json.gz
2016-06-06.json.gz
2016-06-07.json.gz

例子

常用 Shodan 库函数

  • shodan.Shodan(key):初始化连接API
  • Shodan.count(query, facets=None):返回查询结果数量
  • Shodan.host(ip, history=False):返回一个IP的详细信息
  • Shodan.ports():返回Shodan可查询的端口号
  • Shodan.protocols():返回Shodan可查询的协议
  • Shodan.services():返回Shodan可查询的服务
  • Shodan.queries(page=1, sort='timestamp', order='desc'):查询其他用户分享的查询规则
  • Shodan.scan(ips, force=False):使用Shodan进行扫描,ips可以为字符或字典类型
  • Shodan.search(query, page=1, limit=None, offset=None, ufacets=None, minify=True):查询Shodan数据

例子——基本的搜索

#!/usr/bin/env python
#
# shodan_ips.py
# Search SHODAN and print a list of IPs matching the query
#
# Author: achillean

import shodan
import sys

# Configuration
API_KEY = "VZbVJyrIrn8SCSxHhS1YM1XPeKt5nTuy"

# Input validation
if len(sys.argv) == 1:
        print 'Usage: %s <search query>' % sys.argv[0]
        sys.exit(1)

try:
        # Setup the api
        api = shodan.Shodan(API_KEY)

        # Perform the search
        query = ' '.join(sys.argv[1:])
        result = api.search(query)

        # Loop through the matches and print each IP
        for service in result['matches']:
                print service['ip_str']
except Exception as e:
        print 'Error: %s' % e
        sys.exit(1)

例子——使用Facets收集概要信息

Shodan API的强大功能其中之一是获取各种属性的概要信息。若想了解哪些国家的Apache服务器最多,那么可以使用Facets;若想知道哪个版本的nginx最受欢迎,可以使用Facets;若想查看Microsoft-IIS服务器的正常运行时间,那么可以使用Facets

以下脚本显示了如何使用shodan.Shodan.count()方法在使用Shodan API进行搜索时候不返回任何详细结果,只返回组织、域、端口、ASN和国家的信息:

#!/usr/bin/env python
#
# query-summary.py
# Search Shodan and print summary information for the query.
#
# Author: achillean

import shodan
import sys

# Configuration
API_KEY = 'YOUR API KEY'

# 键入想获取的信息
FACETS = [
    'org',
    'domain',
    'port',
    'asn',

    # 只关注前三城市,('country', 3)让Shodan返回前三城市
    # Facet默认五个搜索区域(比如搜索1000个国家,就键入('country', 1000)
]

FACET_TITLES = {
    'org': 'Top 5 Organizations',
    'domain': 'Top 5 Domains',
    'port': 'Top 5 Ports',
    'asn': 'Top 5 Autonomous Systems',
    'country': 'Top 3 Countries',
}

# Input validation
if len(sys.argv) == 1:
    print 'Usage: %s <search query>' % sys.argv[0]
    sys.exit(1)

try:
    # 键入Shodan API
    api = shodan.Shodan(API_KEY)

    # Generate a query string out of the command-line arguments
    query = ' '.join(sys.argv[1:])

    # 使用count()方法,因为它不返回结果,不需要通过付费API才能使用
    # count()运行速度快于search().
    result = api.count(query, facets=FACETS)

    print 'Shodan Summary Information'
    print 'Query: %s' % query
    print 'Total Results: %s\n' % result['total']

    # 从Facets打印摘要信息。
    for facet in result['facets']:
        print FACET_TITLES[facet]

        for term in result['facets'][facet]:
            print '%s: %s' % (term['value'], term['count'])

        # 在摘要信息之间打印一条空行
        print ''

except Exception, e:
    print 'Error: %s' % e
    sys.exit(1)

Alt text

例子——利用API编写GIF图片

Shodan很好得保存了爬取的IP的完整历史记录,可以通过API编写Python脚本,输出Shodan爬虫收集的屏幕截图的。

环境:

需要的Python包:

  • shodan:sudo easy_install shodan安装
  • arrow: sudo easy_install arrow安装。arrow包用于将banner的时间戳字段解析为Python datetime对象。

需要的软件包:

sudo apt-get install imagemagick //将多个图像合并成一个GIF动画所需要

easy_install -U requests simplejson

代码参见:Timelapse GIF Creator using the Shodan API

import arrow
import os
import shodan
import shodan.helpers as helpers
import sys


# Settings
API_KEY = ''

# The user has to provide at least 1 Shodan data file
if len(sys.argv) < 2:
    print('Usage: {} <shodan-data.json.gz> ...'.format(sys.argv[0]))
    sys.exit(1)

# GIFs are stored in the local "data" directory
os.mkdir('data')

# Setup the Shodan API object
api = shodan.Shodan(API_KEY)

# Loop over all of the Shodan data files the user provided
for banner in helpers.iterate_files(sys.argv[1:]):
    # See whether the current banner has a screenshot, if it does then lets lookup
    # more information about this IP
    has_screenshot = helpers.get_screenshot(banner)
    if has_screenshot:
        ip = helpers.get_ip(banner)
        print('Looking up {}'.format(ip))
        host = api.host(ip, history=True)

        # Store all the historical screenshots for this IP
        screenshots = []
        for tmp_banner in host['data']:
            # Try to extract the image from the banner data
            screenshot = helpers.get_screenshot(tmp_banner)
            if screenshot:
                # Sort the images by the time they were collected so the GIF will loop
                # based on the local time regardless of which day the banner was taken.
                timestamp = arrow.get(banner['timestamp']).time()
                sort_key = timestamp.hour

                # Add the screenshot to the list of screenshots which we'll use to create the timelapse
                screenshots.append((
                    sort_key,
                    screenshot['data']
                ))

        # Extract the screenshots and turn them into a GIF if we've got more than a few images
        if len(screenshots) >= 3:
            # screenshots is a list where each item is a tuple of:
            # (sort key, screenshot in base64 encoding)
            # 
            # Lets sort that list based on the sort key and then use Python's enumerate
            # to generate sequential numbers for the temporary image filenames
            for (i, screenshot) in enumerate(sorted(screenshots, key=lambda x: x[0], reverse=True)):
                # Create a temporary image file
                # TODO: don't assume that all images are "jpg", use the mimetype instead
                open('/tmp/gif-image-{}.jpg'.format(i), 'w').write(screenshot[1].decode('base64'))

            # Create the actual GIF using the  ImageMagick "convert" command
            # The resulting GIFs are stored in the local data/ directory
            os.system('convert -layers OptimizePlus -delay 5x10 /tmp/gif-image-*.jpg -loop 0 +dither -colors 256 -depth 8 data/{}.gif'.format(ip))

            # Clean up the temporary files
            os.system('rm -f /tmp/gif-image-*.jpg')

            # Show a progress indicator
            print('GIF created for {}'.format(ip))

使用iterate_files()遍历Shodan数据文件。 在shodan.Shodan.host()方法中获取所有Shodan收集的IP的banner历史记录。

下载屏幕截图的JSON数据包:

shodan download --limit -1 screenshots.json.gz has_screenshot:true

Alt text

*例子——公开的MongoDB数据

MongoDB是一个流行的NoSQL数据库。在很长一段时间内,MongoDB默认是不需要输入用户名和密码,就可以登录。导致了许多MongoDB的服务在互联网上可以被公开访问。(见shodan扫描结果:3万个MongoDB可以无密码访问 大概包含595Tb数据

使用Shodan抓取这些数据库的banner,其中包含了大量关于存储数据的信息。以下是banner的概要:

IP: 115.231.180.54
MongoDB Server Information
Authentication partially enabled
{
    "storageEngines": [
        "devnull", 
        "ephemeralForTest", 
        "mmapv1", 
        "wiredTiger"
    ], 
    "maxBsonObjectSize": 16777216, 
    "ok": 1.0, 
    "bits": 64, 
    "modules": [], 
    "openssl": {
        "compiled": "OpenSSL 1.0.1f 6 Jan 2014", 
        "running": "OpenSSL 1.0.1f 6 Jan 2014"
    }, 
    "javascriptEngine": "mozjs", 
    "version": "3.2.18", 
    "gitVersion": "4c1bae566c0c00f996a2feb16febf84936ecaf6f", 
    "versionArray": [
        3, 
        2, 
        18, 
        0
    ], 
    "debug": false, 
    "buildEnvironment": {
        "cxxflags": "-Wnon-virtual-dtor -Woverloaded-virtual -Wno-maybe-uninitialized -std=c++11", 
        "cc": "/opt/mongodbtoolchain/bin/gcc: gcc (GCC) 4.8.2", 
        "linkflags": "-fPIC -pthread -Wl,-z,now -rdynamic -fuse-ld=gold -Wl,-z,noexecstack -Wl,--warn-execstack", 
        "distarch": "x86_64", 
        "cxx": "/opt/mongodbtoolchain/bin/g++: g++ (GCC) 4.8.2", 
        "ccflags": "-fno-omit-frame-pointer -fPIC -fno-strict-aliasing -ggdb -pthread -Wall -Wsign-compare -Wno-unknown-pragmas -Winvalid-pch -Werror -O2 -Wno-unused-local-typedefs -Wno-unused-function -Wno-deprecated-declarations -Wno-unused-but-set-variable -Wno-missing-braces -fno-builtin-memcmp", 
        "target_arch": "x86_64", 
        "distmod": "ubuntu1404", 
        "target_os": "linux"
    }, 
    "sysInfo": "deprecated", 
    "allocator": "tcmalloc"
},
....

Alt text

基本上,banner是MongoDB服务器信息和及其3个JSON对象用逗号分隔而组成。或者有的banner里有需要登陆凭据的验证字符——“authentication enabled”。每个JSON对象都包含有关数据库的不同信息,建议在Shodan网站上查看完整的头信息,方法是搜索:

product:MongoDB metrics

Alt text

metrics字符保证了搜索时只搜索不需要认证的MongoDB服务


接下来使用banner信息统计最流行的数据库名称,以及在互联网上暴露的数据量。基本的工作流程:

  1. 下载所有的MongoDB banner数据
  2. 处理下载的文件,并输出前十个数据库名称的列表以及总数据大小
shodan download --limit -1 mongodb-servers.json.gz product:mongodb

上述命令功能是,利用Shodan搜索关于mongodb的设备信息,并使用--limit -1指令下载所有的查询结果,输出到mongodb-servers.json.gz文件。


使用Python脚本处理刚才所下载的数据。

要轻松遍历文件,将使用shodan.helpers.iterate_files()方法:

import shodan.helpers as helpers
import sys

# datafile变量是命令行的第一个参数
datafile = sys.argv[1]

for banner in helpers.iterate_files(datafile):
    # 获得banner

由于每个banner仅仅是带有头信息的JSON,因此可以使用simplejson库将banner处理为本地的Python字典:

# 去除Shodan加入的MongoDB的头信息
data = banner['data'].replace('MongoDB Server Information\n', '').split('\n},\n'\
)[2]
# 加载数据库信息
data = simplejson.loads(data + '}')

接下来的事情就是记录暴露的数据总量和最流行的数据库名称:

total_data = 0
databases = collections.defaultdict(int)

...

    # 然后循环
    # 跟踪可访问的数据
    total_data += data['totalSize']

    # 跟踪哪些数据库使用最多
    for db in data['databases']:
        databases[db['name']] += 1

Python有一个有用的collections.defaultdict类,该类的功能是,如果字典的键尚不存在,这个类将自动为字典的键创建一个默认值。只需访问 MongoDB banner的totalSizedatabases属性即可收集的信息。 最后,我们只需要输出实际结果:

print('Total: {}'.format(humanize_bytes(total_data)))

counter = 1
for name, count in sorted(databases.iteritems(), key=operator.itemgetter(1),reverse=True[:10]:
    print('#{}\t{}: {}'.format(counter, name, count))
    counter += 1

首先打印数据总量,然后使用humanize_bytes()方法将字节存储转换为易读的MB、GB等存储格式。

其次,对数据库集合进行多次逆序遍历排序(key=operator.itemgetter(1)),取结果的TOP10([:10])。

以下是读取Shodan数据文件并分析banner的完整脚本:

#!C:\Python27\python.exe
# -*- coding: utf-8 -*-
# @Time    : 2018/2/2 10:01
# @Author  : b404
# @File    : MongDB_Shodan.py
# @Software: PyCharm

import collections
import operator
import shodan.helpers as helpers
import sys
import simplejson

def humanize_bytes(bytes, precision=1):
    """将字节转换为易读的存储单位MB、GB等

    Assumes `from __future__ import division`.

    >>> humanize_bytes(1)
    '1 byte'
    >>> humanize_bytes(1024)
    '1.0 kB'
    >>> humanize_bytes(1024*123)
    '123.0 kB'
    >>> humanize_bytes(1024*12342)
    '12.1 MB'
    >>> humanize_bytes(1024*12342,2)
    '12.05 MB'
    >>> humanize_bytes(1024*1234,2)
    '1.21 MB'
    >>> humanize_bytes(1024*1234*1111,2)
    '1.31 GB'
     >>> humanize_bytes(1024*1234*1111,1)
        '1.3 GB'
        """
    abbrevs = (
        (1 << 50L, 'PB'),
        (1 << 40L, 'TB'),
        (1 << 30L, 'GB'),
        (1 << 20L, 'MB'),
        (1 << 10L, 'kB'),
        (1, 'bytes')
    )
    if bytes == 1:
        return '1 byte'
    for factor, suffix in abbrevs:
        if bytes >= factor:
            break
    return '%.*f %s' % (precision, bytes / factor, suffix)

total_data = 0
databases = collections.defaultdict(int)
for banner in helpers.iterate_files(sys.argv[1]):
    try:
        # 去除Shodan加入的MongoDB的头信息
        data = banner['data'].replace('MongoDB Server Information\n', '').split('\n},\n')[2]

        # 加载数据库信息
        data = simplejson.loads(data + '}')
        # 记录公开访问的数据量
        total_data += data['totalSize']

        # 跟踪哪些数据库名称最常见
        for db in data['databases']:
            databases[db['name']] += 1
    except Exception, e:
        pass

print('Total: {}'.format(humanize_bytes(total_data)))

counter = 1
for name, count in sorted(databases.iteritems(), key=operator.itemgetter(1), reverse=True)[:10]:
    print('#{}\t{}: {}'.format(counter, name, count))
counter += 1

该脚本输出:

Total: 569.7 GB
#1    Warning: 1448
#1    local: 1172
#1    admin: 488
#1    README: 113
#1    config: 67
#1    test: 43
#1    DATA_HAS_BEEN_BACKED_UP: 19
#1    logs: 19
#1    db_has_been_backed_up: 15
#1    gpsreal: 11

Alt text

results matching ""

    No results matching ""