Other

技术解读

以下内容由 GitRead 中文社区 编辑团队基于项目 README 与官方资料整理, 结合中文开发者的常见使用场景补充技术选型建议。具体以项目官方文档为准。

项目背景与痛点

TLS 早已是互联网的默认底色,但"有多少主机在跑 TLS、跑的是哪个版本、证书多久过期、有没有还在用 RC4 和 SHA-1"这类问题,靠现成脚本答不上来。传统的 sslscan、testssl.sh 面向单目标做精细体检:握手日志、密码套件矩阵、漏洞探针,输出是给人看的报告。这类工具的问题不在质量,而在吞吐——扫一个 B 段就要跑上一天。

做全地址空间的普查,需要的不是"更详细的报告",而是"单位时间吞吐更高的采集管道"。这正是 ZMap 当年提出的范式转向:把扫描当作大数据采集,牺牲交互能力,换取数量级的速度。prbinu/tls-scan 站在这条路线上——它用 Go 写了一个非阻塞、事件驱动的 TLS 采集器,单进程、单二进制,结果以 JSON Lines 流式吐出。与其说它是"又一个 sslscan",不如说它是"专注 TLS 层的 ZGrab 轻量替代品"。

核心技术揭秘

真正的技术难点藏在 Go 标准库的一个语义约束里:crypto/tls 的 Handshake 是阻塞式的。它在 net.Conn 上同步读写,一次完整握手要经历 ClientHello 写出、ServerHello 读入、证书链读入、Finished 交换等多个往返。如果每个目标都开一个 goroutine 做阻塞握手,Go 调度器确实能扛住数万并发,但 goroutine 的栈、runtime 元数据以及随并发线性增长的 GC 压力,会让"百万级目标"变成一场内存噩梦。

tls-scan 的解法是把"连接"抽象成状态机:每个目标对应一个轻量 scanner 对象,只持有 fd、读写缓冲区和当前状态;所有 fd 注册到 epoll(Linux)或 kqueue(BSD/macOS),主循环拿到就绪事件后分发给对应状态机推进——写不完就注册 EPOLLOUT,读不全就等下一次 EPOLLIN,超时由统一的时间堆收割。这样并发上限只与文件描述符和内存挂钩,与 goroutine 数量脱钩。

配套的关键设计,是把 TLS 层套在一个自实现的 net.Conn 上:这个 Conn 的 Read/Write 不真正阻塞,而是以"需要更多数据"的语义返回,由外层状态机重试。如此既复用了标准库久经考验的密码学栈,又不必重写握手协议本身。数据通路同样是流式的——证书解析完立刻编码成 JSON 推送出去,不在内存攒批,保证长时间扫描时 RSS 曲线平稳。

graph TD A[目标输入:CIDR / 文件 / 域名] --> B[目标展开与去重队列] B --> C[异步连接调度器] C --> D[epoll / kqueue 事件循环] D -->|"fd 可读写"| E[TLS 握手状态机] D -->|"超时事件"| F[超时回收] E --> G[证书链与扩展解析] G --> H[JSON Lines 编码器] F --> H H --> I[标准输出 / 结果文件]

功能亮点与差异

零依赖部署。没有 Python 运行时,没有 OpenSSL 版本地狱,交叉编译一个二进制丢到机器上就能跑,这在批量铺开扫描节点时是实打实的优势。

失败也是数据。连接拒绝、握手失败同样写入 error 字段并输出。对普查而言,"这里没有 TLS"和"这里 TLS 配置很差"具有同等的分析价值,很多工具恰恰忽略了这一点。

输出即管道。JSON Lines 格式天然适配 jq、Kafka、ClickHouse,不需要为消费端再写一层转换。

与 zgrab2 对比:zgrab2 模块化、多协议,但模块配置是一坨嵌套 JSON,二进制体积大,还得自己拼 ZMap 做前置发现。tls-scan 只做一件事,CLI 直白,心智负担低。与 testssl.sh 对比:它不做漏洞判定,别指望它告诉你 Heartbleed 是否存在——定位完全不同。

应用场景与落地建议

典型场景包括资产测绘、证书透明度数据的补充采集、云厂商与 CDN 的 TLS 配置基线核查,以及学术测量。

落地时有几件事必须提前处理:

  1. 法律与授权。互联网规模扫描在多数司法辖区属于灰色地带,对政企目标必须拿到书面授权。国内尤其要留意《网络安全法》的边界,别对着 .gov.cn 直接开扫。
  2. 系统调优。ulimit -n 拉到百万级、ip_local_port_range 扩到 1024–65535、打开 tcp_tw_reuse、调大 somaxconn。否则瓶颈在本地内核,不在网络。
  3. 分片与限速。单机跑太猛会被上游运营商或目标侧 IDS 拉黑,建议按 /16 切片、多机多出口。
  4. 超时策略。公网丢包普遍,默认超时别设太短,加一次重试能显著抬高有效率。

综合评价

优点:架构干净,事件循环加状态机的组合把资源效率压到极致;输出即数据,适合流水线;单二进制部署成本近乎为零;代码量不大、可读性好,本身就是一份"如何用 Go 写高性能扫描器"的教材。

缺点:专注面窄,只做采集不做协议发现与漏洞判定;缺少分布式调度,横向扩展得自己搭;文档偏薄,不少行为要翻源码才清楚;社区规模小,长期维护存不确定性。更根本的是,connect 式扫描受限于三次握手和本地端口资源,与 ZMap 的 SYN 扫描不是一个量级的对手——真要做 /0 级普查,仍然是 ZMap 探活加 tls-scan 精扫的组合拳。

一句话总结:想把互联网的 TLS 配置拍成一张可查询的表,它是 Go 生态里最省心的那块拼图;想给一台服务器出体检报告,请左转 testssl.sh。

关于

GitHub 原始描述(英文)

An Internet scale, blazing fast SSL/TLS scanner ( non-blocking, event-driven )

描述取自 GitHub 官方 API,保持仓库作者书写的原文。

简要分析

热度分
451
价值分
215
活跃状态
活跃

语言占比

C
Dockerfile
M4
Makefile
Shell

README

Build Status Release Status

tls-scan

A program to scan TLS based servers and collect X.509 certificates, ciphers and related information. It produces results in JSON format. tls-scan is a single threaded asynchronous/event-based program (powered by libevent) capable of concurrently scan thousands of TLS servers. It can be combined with other tools such as GNU parallel to vertically scale in multi-core machines.

tls-scan helps developers and security engineers to track/test/debug certificates and TLS configurations of servers within their organization.

Features

  • Support for TLSv1.3
  • TLS and StartTLS protocol support: SMTP, IMAP, POP3, FTPS, SIEVE, NNTP, XMPP, LDAP, RDP, POSTGRES, MYSQL
  • Blazing fast - Can operate at scale with the ability to concurrently scan large number of servers (say scan IoT devices at scale)
  • Detect SSLv2, SSLv3, TLSv1, TLSv1.1, TLSv1.2, TLSv1.3 versions and ciphers
  • ALPN protocol id enumeration
  • Cipher and TLS version enumeration
  • Extract X.509 certificate fields from the target server and print it in JSON format
  • Certificate and host name verification checks
  • TLS compression checks
  • Session reuse tests
  • Certificate revocation checks with stapled OCSP response
  • Script friendly output - Can be combined with other tools to analyze the scan results
  • Detailed run time stats for tracking progress and performance/charts

This tool is primarily for collecting TLS cipher and X.509 certificate data. The scan output can be easily combined with related tools to identify TLS misconfigurations.

Installation

You may either use pre-built binary package or build from the source.

Pre-built Binary (x86_64)

Linux and OSX: https://github.com/prbinu/tls-scan/releases/latest

Build From Source

All you need is build-x86-64.sh (or build-arm64.sh for Linux Arm arch). This script pulls dependent packages - PeterMosmans openssl, libevent and GnuTLS, and build those from the scratch. Since the openssl we use is different from stock openssl, it is linked statically to tls-scan program. The build can take approximately twenty minutes to complete.

Build Pre-requisites :

On Ubuntu:

sudo apt-get update
sudo apt-get install make autoconf automake libtool pkg-config gcc unzip -y

Linux

git clone https://github.com/prbinu/tls-scan.git
cd tls-scan

x84_64

./build-x86-64.sh

The newly built tls-scan binary can be found at ./build-root/bin. build-x86-64.sh is a wrapper script that calls ./bootstrap.sh to build all dependent packages. bootstrap.sh also executes the autoreconf -i command to generate configure file. Subsequently it calles the standard ./configure, make && make install.

arm64

./build-arm64.sh

Test :

cd build-root/bin
./tls-scan --connect=yahoo.com --cacert=../etc/tls-scan/ca-bundle.crt --pretty

OSX

If you do not have the pre-requisite packages, you can easily install those packages by following the links below:

git clone https://github.com/prbinu/tls-scan.git
cd tls-scan
./build-x86-64.sh

The tls-scan binary can be found at ./build-root/bin. Another (easy) option is to use our Docker image to build and run tls-scan on OSX.

Running tls-scan on Mac Apple Silicon (Arm/M1/M2):

Currently no native build support, however you may run tls-scan binary using Rosetta2

Docker

Pre-requisite : Docker

Build : Copy the Dockerfile to your machine, and run it:

docker build -t tls-scan .

Test :

docker run --rm tls-scan --connect=example.com:443 --all --pretty

Example

./tls-scan -c search.yahoo.com --all --pretty
{
  "host": "search.yahoo.com",
  "ip": "208.71.45.12",
  "port": 443,
  "elapsedTime": 1600,
  "tlsVersion": "TLSv1.2",
  "cipher": "ECDHE-RSA-AES128-GCM-SHA256 TLSv1.2 Kx=ECDH     Au=RSA  Enc=AESGCM(128) Mac=AEAD",
  "tempPublicKeyAlg": "ECDH prime256v1",
  "tempPublicKeySize": 256,
  "secureRenego": true,
  "compression": "NONE",
  "expansion": "NONE",
  "sessionLifetimeHint": 100800,
  "tlsVersions": [
    "TLSv1",
    "TLSv1_1",
    "TLSv1_2"
  ],
  "x509ChainDepth": 2,
  "verifyCertResult": true,
  "verifyHostResult": true,
  "ocspStapled": true,
  "verifyOcspResult": true,
  "certificateChain": [
  {
    "version": 3,
    "subject": "CN=*.search.yahoo.com; O=Yahoo! Inc.; L=Sunnyvale; ST=CA; C=US",
    "issuer": "CN=DigiCert SHA2 High Assurance Server CA; OU=www.digicert.com; O=DigiCert Inc; C=US",
    "subjectCN": "*.search.yahoo.com",
    "subjectAltName": "DNS:*.search.yahoo.com, DNS:search.yahoo.com, DNS:search.yahoo.net, ...",
    "signatureAlg": "sha256WithRSAEncryption",
    "notBefore": "Dec  9 00:00:00 2016 GMT",
    "notAfter": "Apr 30 12:00:00 2017 GMT",
    "expired": false,
    "serialNo": "0F:45:73:E3:F5:7A:7D:5B:43:57:64:2A:6C:46:F2:1C",
    "keyUsage": "Digital Signature, Key Encipherment critical",
    "extKeyUsage": "TLS Web Server Authentication, TLS Web Client Authentication",
    "publicKeyAlg": "RSA",
    "publicKeySize": 2048,
    "basicConstraints": "CA:FALSE critical",
    "subjectKeyIdentifier": "63:0F:82:DB:F9:B0:64:78:90:C9:16:69:95:84:24:F1:4B:04:6F:E4",
    "sha1Fingerprint": "F7:35:E5:C9:A3:60:62:07:CB:55:74:7E:0F:09:AD:2A:F3:F3:53:F3"
  },  {
    "version": 3,
    "subject": "CN=DigiCert SHA2 High Assurance Server CA; OU=www.digicert.com; O=DigiCert Inc; C=US",
    "issuer": "CN=DigiCert High Assurance EV Root CA; OU=www.digicert.com; O=DigiCert Inc; C=US",
    "subjectCN": "DigiCert SHA2 High Assurance Server CA",
    "signatureAlg": "sha256WithRSAEncryption",
    "notBefore": "Oct 22 12:00:00 2013 GMT",
    "notAfter": "Oct 22 12:00:00 2028 GMT",
    "expired": false,
    "serialNo": "04:E1:E7:A4:DC:5C:F2:F3:6D:C0:2B:42:B8:5D:15:9F",
    "keyUsage": "Digital Signature, Certificate Sign, CRL Sign critical",
    "extKeyUsage": "TLS Web Server Authentication, TLS Web Client Authentication",
    "publicKeyAlg": "RSA",
    "publicKeySize": 2048,
    "basicConstraints": "CA:TRUE, pathlen:0 critical",
    "subjectKeyIdentifier": "51:68:FF:90:AF:02:07:75:3C:CC:D9:65:64:62:A2:12:B8:59:72:3B",
    "sha1Fingerprint": "A0:31:C4:67:82:E6:E6:C6:62:C2:C8:7C:76:DA:9A:A6:2C:CA:BD:8E"
  } ]
}

Useful Tip: Use --concurrency=<n> option if you want to scan multiple target servers in parallel.

Usage

The scan output can be shoved into tools like Splunk or ELK for analysis.

Command-line Query & Filter

By passing tls-scan output to JSON command-line parser like jq, you can do realtime filtering on the scan results.

Examples:

Command to filter out hosts that passed certificate and host name verifications:

cat input.txt | tls-scan --port=443  2>/dev/null | \
jq-linux64 -r 'select(.verifyHostResult == true and .verifyCertResult == true) | [.host, .ip, .verifyHost, .verifyCert] | @tsv'

Command to find hosts with expired certificates :

cat input.txt | tls-scan --port=443 --concurrency=500 --timeout=5 2>/dev/null | \
jq-linux64 -r  'select(.certificateChain[].expired == true) | [.host, .ip] | @tsv'

Command to find weak RSA keys :

cat tlscerts.out | \
jq-linux64 -r  'select(.certificateChain[0].publicKeyAlg == "RSA" and .certificateChain[0].publicKeySize < 2048) | [.host, .ip]'

Command to find hosts that support SSLv2 or SSLv3 :

tls-scan --infile=domains.txt --port=443 --version-enum --concurrency=250 --timeout=3 2>/dev/null | \
jq-linux64 -r 'if (.tlsVersions[] | contains("SSL")) == true then [.host, .ip, .tlsVersions[]] else empty end | @tsv'

NOTE: Avoid frequent scan + filter; instead save the scan output to a file and use it to run queries.

Help

Option Description
-H --help Print a usage message briefly summarizing these command-line options and the bug-reporting address, then exit.
-c --connect=<arg> target[:port] to scan. target = {hostname, IPv4, [IPv6] }. IPv6 example: [::1]:443 (default port 443).
–starttls=<protocol> Supported protocols: smtp, imap, pop3, ftp, sieve, nntp, xmpp, ldap, rdp, postgres, mysql, tls (default)
-c --cacert=<file> Root CA file for certificate validation. By default the program attempts to load ca-bundle.crt file from current directory.
-C --ciphers=<arg> Ciphers to use; try openssl ciphers to see all ciphers. Note that this option will be overwritten by --ssl2, --ssl3, --tls1, --tls1_1, --tls1_2 options, if provided. Example: "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384"
-e --cipher-enum Enumerate supported ciphers. Currently use --tls-old ciphers. Try --meta-info to find predefined cipher suite options.
–show-unsupported-ciphers Include unsupported ciphers in the cipher list to JSON output.
-V --version-enum Enumerate supported TLS versions.
-v --version Print tls-scan version and build information.
-r --session-reuse Enable ssl session reuse.
-u --session-print Print SSL session in PEM format to stderr. This is currently not included in the JSON output, but print seperately. This flag woould be useful if you wanted to pass SSL session to --session-file to test session reuse.
-T --session-file=<file> File that contains SSL session in PEM format.
-a --all Shortcut for --version-enum, --cipher-enum and --session-reuse options. This scan can take longer time to complete. Also note if the server employs some form of rate-limiting, your scan may fail.
-s --sni=<host> Set TLS extension servername in ClientHello. Defaults to input hostname and applied to TLSv1+ only.
-b --concurrency=<number> Number of concurrent requests. The default is 1. This option specify the number of worker objects. Concurrency should be set based on your system capacity (memory, cpu, network) etc. Default: 1.
-t --timeout=<number> Timeout per connection (in seconds). Note that is is per connection and for cipher scans, tls-scan makes several connections to the same server. Default: 10.
-S --sleep=<number> Add milliseconds delay between the connection. Only for --cipher-enum and --version-enum options. Useful to manage server rate-limits. The max sleep value is 60000 (1 minute). Default: 0.
-f --infile=<file> Input file with domains or IPs. This is optional and by default the program accepts input from standard input (stdin).
-o --outfile=<file> Output file where the result in JSON format is stored. The default is standard output (stdout).
-n --pretty Pretty print; add newline (\n) between record fields.
-N --nameserver=<ip> DNS resolver IPs to use and is an optional field. Multiple Namespace IP address can be passed. Format: -N <ip1> -N <ip2> -N <ip3>.. In practice, DNS servers may have tight rate-limit in place.
–ssl2 Use only SSLv2 ciphers.
–ssl3 Use only SSLv3 ciphers.
–tls1 Use only TLSv1 ciphers.
–tls1_1 Use only TLSv1_1 ciphers.
–tls1_2 Use only TLSv1_2 ciphers.
–tls-modern Mozilla’s modern cipher list. See: https://wiki.mozilla.org/Security/Server_Side_TLS.
–tls-interm Mozilla’s intermediate cipher list.
–tls-old Mozilla’s old (backward compatible cipher list).
–no-parallel-enum Disable parallel cipher and tls version enumeration. Parallel scan is performed only with ‘–connect’ option.
–meta-info Print program meta information and exit. Useful if you wanted to see predefined cipher options.
–stats-outfile=<file> Enable run-time scan stats and save it to a file

Caveats

  • The following ciphers are currently disabled: SRP:PSK

TLS 1.3 Support

To support old, insecure cipher scans, we are using an old openssl version that doesn’t have support for TLS 1.3. So to support TLS 1.3, we need a newer openssl version (v1.1.1+). Since linking two openssl libraries to the same process space doesn’t work out of box (duplicate symbols), we chose to use GnuTLS library for TLS 1.3+ support. In short, openssl is used for scanning SSLv2, SSLv3, TLSv1, TLSv1.1 and TLSv1.2 and GnuTLS is used for TLSv1.3.

Contributions

Collaborators and pull requests are welcome!

评论

暂无评论