BBScan3.0,一个高并发的Web漏洞扫描工具,辅助API安全测试

本文链接: https://mp.weixin.qq.com/s/AvLpKKsqRkzdzatvqqvfuQ

背景

随着时间推移,BBScan作为一款漏洞扫描工具已经过时,在9年前,这样的工具还能够以数量和速度优势,捡到一些中低危漏洞,但如今,确实是扫不到什么有价值的东西了。

常见Web框架引入了更多的默认安全设计、WAF/RASP等防护技术更加成熟,曾经常见的SQL注入/XSS/命令注入等,现在反而见得不多了。取而代之的,各大SRC现在收到的漏洞中,有相当比例是逻辑越权类的。

于是,笔者对这款古董级的BBScan进行了如下改造

  • 支持Web指纹识别,帮助安全工程师快速定位到感兴趣的应用
  • 支持Javascript 解析功能
    • 目前很多站点是单页应用,对扫描器可见的仅仅是静态资源打包后的少数几个.js。扫描器需要解析js,扫描诸如 Token/Secrets/Password/Key 泄露,发现API接口
  • 支持从javascript文件中,正则提取疑似API接口
    • 在当前版本中,考虑到速度,并没有对提取的URL进行自动化测试,但计划增加一个heavy mode ,扫描API接口
  • 减少漏报:  优化减少DNS查询次数,提高稳定性。在家庭网络环境下,观察到DNS设施相对脆弱,容易出现漏报问题
  • 减少误报:优化了误报验证逻辑
  • 界面优化:对输出的HTML报告,进行了简单优化,提升可读性

改造效果

BBScan项目地址:https://github.com/lijiejie/BBScan

笔者首先利用另一个工具 subDomainsBrute暴力枚举了3个域名 

*.baidu.com *.qq.com *.bytedance.com

随后将发现的域名文件丢给BBScan,扫描生成3份报告(请复制后打开)

如上图所示,扫描器利用默认的内置规则,能够识别到这是一个管理后台。同时,扫描器还从js文件中,提取到12个疑似API接口。

针对这样的一份报告,可以进行如下处理

  • 首先检查通过内置规则扫出的漏洞,一般是信息泄露、各类后台、JS中的秘钥泄露
  • 检查web指纹是否识别到你可能感兴趣的通用应用、框架、开源或商业产品
  • 对发现API接口的站点,进行重点分析和测试,点开感兴趣的接口重点分析和扫描
  • 对API网关进行测试和评估
  • 对报告中泄露的内网域名、IP进行简单分析
  • 通过关键词搜索你感兴趣的关键词,如Header Name、特殊的Cookie

使用问题

对大量目标快速指纹识别

  • python BBScan.py –fingerprint -f urls.txt –api

请注意, 这个 –fingerprint 开关,是指定只扫描web指纹,其他规则反而会被禁用。

如下图所示,本工具可以在较短时间内扫描完上万网站

完整扫描模式

  • python BBScan.py -f baidu.com_full.txt –full –api

–network MASK  任何时候都可以使用该参数,把子网中的其他相邻IP,一并添加到扫描任务中。虽然不建议这么做,但下面的命令是真的可以工作,你或许会得到1个超大HTML

  • –host 10.1.1.1 –network 8 –fingerprint

上面的命令指定扫描 10.1.1.1/8 整个内网,打开文件时,可能出现Chrome浏览器Out Of Memeory,所以,建议指定为更小的指,建议不小于16。

–skip, –skip-intranet  排除内网IP的扫描,这对白帽子比较实用,避免浪费扫描资源。

未来优化

BBScan未来可能进一步优化API接口的扫描

  • 增加对API接口的重扫描、分析支持
  • 正则优化,解决API接口提取不够精准的问题
  • 优化对隐蔽API接口的暴力枚举发现、source map泄露发现等
  • 子域名/Email的收集整理、证书的收集整理

因时间有限,部分功能还未开发完成,同时,开发过程测试不充分,请大家反馈功能建议和Bug问题。  🙂

古董扫描器BBScan的复活,希望它对你有一定用。虽然,显然还不够有用。  🙂

项目地址:https://github.com/lijiejie/BBScan

MisConfig HTTP Proxy Scanner:发现配置不当的HTTP网关(正反向代理),突破边界访问企业内网应用

MisConfig HTTP Proxy Scanner是一个web漏洞扫描器,用于扫描发现配置不当的HTTP正反代理:

  • 错误配置的外网反向代理,内网系统被意外暴露到外网
  • 错误配置的外网正向代理,攻击者可以暴力枚举内网域名,访问内网的生产运维系统、办公系统

本项目地址:https://github.com/lijiejie/MisConfig_HTTP_Proxy_Scanner

本工具介绍: https://mp.weixin.qq.com/s/hnv4HUntZtApjNCbhv34ow

Django bulk_update可能卡死问题

在一个多年前的历史项目中,我们发现,django cron job出现了卡死现象。 应用逻辑没有被完整执行直到结束,程序已经僵死了。

调试分析卡死的Python进程,请参考: https://wiki.python.org/moin/DebuggingWithGdb

通过调试进程,我们发现应用卡死在了

Domain.objects.bulk_update(update_domain_list, [‘ips_from_fuxi’])

如下图所示:

回到源代码中,在上下文找到了原因,原来,需要bulk update的列表,没有在循环体内部正确被初始为空。 需要update的列表出现了持续增长,最终导致bulk update 超大的列表,卡死应用程序。

正确初始化列表,即可解决bulk update hung死的问题。

Python子进程logging config在Win和Linux下表现不一致的问题

Python父进程通过logging.basicConfig,设定日志文件的路径,子进程再次通过basicConfig设定另一个新的路径。这个操作,在Windows系统和Linux下表现不一致。

区别在于,Windows下将打开不同的文件句柄,写入2个不同的文件 。而在Linux下,则config看起来无效,子进程和父进程使用同一个文件描述符,第二次config无效。(解决的方法文末补充)

import logging
import os
import multiprocessing
import threading

cwd = os.getcwd()


def set_log(log_file_path):
    logging.basicConfig(filename=log_file_path,
                        filemode='a+',
                        format='%(message)s',
                        level=logging.INFO)
    logging.info('%s:  %s' % (logging.root, log_file_path) )


def test_func():
    set_log(os.path.join(cwd, 'test2.log'))


class TestThread(threading.Thread):
    def __init__(self):
        p = multiprocessing.Process(target=test_func)
        p.start()


if __name__ == '__main__':
    set_log(os.path.join(cwd, 'test.log'))
    TestThread()

这个程序在Windows的执行结果是

logging.RootLogger object at 0x0000000002610C18>:  C:\test\test.log
logging.RootLogger object at 0x000000000276C7B8>:  C:\test\test2.log

可以看到,日志写入了两个不同的文件。并且Logger ID不一样

在Linux下的执行结果是

[root@cyberivy-web-dev002-whdx test]# ls *.log
test.log
[root@cyberivy-web-dev002-whdx test]# cat test.log 
:  /root/test/test.log
:  /root/test/test2.log

两个进程的日志都写到了test.log中。 并且 Logger ID 和 fd是一样的

解决的办法

子进程在set_log之前,先 reload(logging), test_func变成

def test_func():
    reload(logging)
    set_log(os.path.join(cwd, 'test2.log'))

修改之后,在Linux执行,日志同样会写入不同的文件

[root@cyberivy-web-dev002-whdx test]# ls *.log
test2.log  test.log
[root@cyberivy-web-dev002-whdx test]# cat *.log
logging.RootLogger object at 0x1df28d0>:  /root/test/test2.log
logging.RootLogger object at 0x1d80bd0>:  /root/test/test.log

Python浅复制导致for loop删除剩下偶数位元素的问题

昨天群里,大家聊到了一个面试题,关于 浅复制 (shallow copy )的。 相信很多人都写过类似的错误代码。

items = list(range(1, 11))

while items:
    tmp_items = items     # tmp_items referred to items
    for i in tmp_items:
        items.remove(i)
    print items

通过一个for loop遍历删除列表中的元素(这里示例1到10)。最终执行的结果是

[2, 4, 6, 8, 10]
[4, 8]
[8]
[]

可以看到,每次循环,只有奇数位的数字被从列表移除了,偶数位的数字被保留下来。

这跟列表是一个链表结构有关系。

第二,这里的 tmp_items = items 是浅复制, tmp_items只是items的引用

迭代器在遍历列表的时候,从head开始

head -> pointer1 -> 1
        pointer2 -> 2
        ...
        pointer10 -> 10

循环开始,首先删除第一个元素(1),此时,对应的head指针移动到指针2

head -> pointer2 -> 2
        ...
        pointer10 -> 10

继续遍历时, head->next 指向的是已经是元素3。    于是,最终只有奇数位置的元素被移除了。

requests.get 异常hang住

这两天优化端口扫描脚本,在获取HTTP页面内容时,发现requests.get hang住了(之前httplib不会)。

requests.get 提供了超时参数timeout,但在实际使用过程中,仍发现get请求可能被hang住。

原因在于非HTTP  Server,response中一直没有出现换行符\n,导致_read_status()在获取状态码的时候就hang住了。

一个简单的解决方法,可以创建新的线程,示例代码参考如下,这部分代码是无法直接工作的:

def do_get_thread(url, ret):
    try:
        headers = {
            'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 6_0 like Mac OS X) AppleWebKit/536.26 '
                          '(KHTML, like Gecko) Version/6.0 Mobile/10A5376e Safari/8536.25',
            'Range': 'bytes=0-10240',
            'Connection': 'Close'}
        ret.append(requests.get(url, headers=headers, timeout=(4, 30)))
    except:
        pass


def get_http_title(ip, port, service, log_queue):
    if service.lower().find('https') >= 0 or service.lower().find('ssl') >= 0:
        service = 'https'
    else:
        service = 'http'
    url = '%s://%s:%s' % (service, ip, port)
    msg = 'Get http title %s' % url
    log_queue.put(msg)

    try:
        start_time = time.time()
        ret = []
        t = threading.Thread(target=do_get_thread, args=(url, ret))
        t.daemon = True
        t.start()
        while t.isAlive():
            if time.time() - start_time > 31:
                log_queue.put('[get_http_title timed out] %s' % url)
                return {}, ''
            else:
                time.sleep(1.0)
        if not ret:
            return {}, ''
        resp = ret[0]
        headers = resp.headers
        m = re.search('<title>(.*?)</title>', resp.text, re.IGNORECASE)
        http_title = m.group(1).strip() if m else ''
        if http_title:
            http_title = decode_response_text(http_title)
        else:
            http_title = resp.text.replace('\n', ' ').strip()[0:200]

        return headers, http_title
    except requests.exceptions.RequestException, e:
        pass
    except Exception, e:
        log_queue.put('[get_http_title.exception] %s' % str(e))
    return {}, ''