在网络通信安全领域,检测启用SSL/TLS协议的服务所支持的密码套件是保障通信安全的关键环节。这类检测工具能帮我们摸清目标服务的加密配置,识别弱密码套件、过时加密协议等安全风险,今天我们就从底层原理、代码实现和设计思路入手,聊聊这项技术到底是怎么回事。
一、核心概念:密码套件与检测的意义
先说说基础概念------密码套件 其实就是SSL/TLS通信中一套"加密组合拳",包含了密钥交换算法(比如RSA、ECDHE)、对称加密算法(比如AES、3DES)、哈希算法(比如SHA256)等。不同的密码套件安全等级天差地别:比如RC4、3DES属于弱密码套件,容易被破解;而AES-GCM搭配ECDHE的套件则是目前的安全首选。
检测服务支持的密码套件,本质是模拟客户端与目标服务建立SSL/TLS连接,通过"试探"不同的密码套件和协议版本,记录服务端的响应------哪些套件能成功握手,哪些被拒绝,从而梳理出服务的加密配置,找出其中的安全漏洞(比如支持SSLv2/SSLv3这类已被淘汰的协议,或启用了Heartbleed漏洞相关的配置)。
二、底层实现原理:从代码看检测流程
这类检测工具的核心逻辑,本质是"模拟SSL/TLS握手+逐一套件试探",我们结合核心代码来拆解关键步骤:
c
...
struct sslCipher
{
const char *name;
const char *version;
int bits;
char description[512];
const SSL_METHOD *sslMethod;
struct sslCipher *next;
};
struct sslCheckOptions
{
char host[512];
char sniname[512];
int sni_set;
char addrstr[INET6_ADDRSTRLEN];
int port;
int showCertificate;
int showCertificates;
int checkCertificate;
int showTrustedCAs;
int showClientCiphers;
int showCipherIds;
int showTimes;
int ciphersuites;
int reneg;
int fallback;
int compression;
int heartbleed;
int groups;
int signature_algorithms;
int starttls_ftp;
int starttls_imap;
int starttls_irc;
int starttls_ldap;
int starttls_pop3;
int starttls_smtp;
int starttls_mysql;
int starttls_xmpp;
int starttls_psql;
int xmpp_server;
int sslVersion;
int targets;
int sslbugs;
int rdp;
int verbose;
int cipher_details;
int ipv4;
int ipv6;
int ocspStatus;
int ianaNames;
char cipherstring[65536];
FILE *xmlOutput;
short h_addrtype;
struct sockaddr_in serverAddress;
struct sockaddr_in6 serverAddress6;
struct timeval timeout;
int connect_timeout;
unsigned int sleep;
SSL_CTX *ctx;
struct sslCipher *ciphers;
char *clientCertsFile;
char *privateKeyFile;
char *privateKeyPassword;
unsigned int tls10_supported;
unsigned int tls11_supported;
unsigned int tls12_supported;
unsigned int tls13_supported;
};
struct renegotiationOutput
{
int supported;
int secure;
};
struct ocsp_response_st {
ASN1_ENUMERATED *responseStatus;
OCSP_RESPBYTES *responseBytes;
};
struct ocsp_resp_bytes_st {
ASN1_OBJECT *responseType;
ASN1_OCTET_STRING *response;
};
struct ocsp_responder_id_st {
int type;
union {
X509_NAME *byName;
ASN1_OCTET_STRING *byKey;
} value;
};
typedef struct ocsp_responder_id_st OCSP_RESPID;
struct ocsp_response_data_st {
ASN1_INTEGER *version;
OCSP_RESPID responderId;
ASN1_GENERALIZEDTIME *producedAt;
STACK_OF(OCSP_SINGLERESP) *responses;
STACK_OF(X509_EXTENSION) *responseExtensions;
};
typedef struct ocsp_response_data_st OCSP_RESPDATA;
struct ocsp_basic_response_st {
OCSP_RESPDATA tbsResponseData;
X509_ALGOR signatureAlgorithm;
ASN1_BIT_STRING *signature;
STACK_OF(X509) *certs;
};
struct ocsp_single_response_st {
OCSP_CERTID *certId;
OCSP_CERTSTATUS *certStatus;
ASN1_GENERALIZEDTIME *thisUpdate;
ASN1_GENERALIZEDTIME *nextUpdate;
STACK_OF(X509_EXTENSION) *singleExtensions;
};
struct ocsp_cert_status_st {
int type;
union {
ASN1_NULL *good;
OCSP_REVOKEDINFO *revoked;
ASN1_NULL *unknown;
} value;
};
struct ocsp_revoked_info_st {
ASN1_GENERALIZEDTIME *revocationTime;
ASN1_ENUMERATED *revocationReason;
};
struct ocsp_cert_id_st {
X509_ALGOR hashAlgorithm;
ASN1_OCTET_STRING issuerNameHash;
ASN1_OCTET_STRING issuerKeyHash;
ASN1_INTEGER serialNumber;
};
#define BS_DEFAULT_NEW_SIZE 256
struct _bs {
unsigned char *buf;
size_t size;
size_t len;
};
typedef struct _bs bs;
#ifndef SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION
# define SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION 0x00040000L
#endif
#ifndef SSL3_FLAGS_ALLOW_UNSAFE_LEGACY_RENEGOTIATION
# define SSL3_FLAGS_ALLOW_UNSAFE_LEGACY_RENEGOTIATION 0x0010
#endif
void bs_new(bs **);
void bs_new_size(bs **, size_t);
void bs_free(bs **);
void bs_append_bytes(bs *, unsigned char *, size_t);
void bs_append_uint32_t(bs *, uint32_t);
void bs_append_ushort(bs *, unsigned short);
void bs_append_bs(bs *, bs *);
size_t bs_get_len(bs *);
size_t bs_get_size(bs *);
unsigned char *bs_get_bytes(bs *);
unsigned char bs_get_byte(bs *, size_t);
void bs_set_byte(bs *, size_t, unsigned char);
void bs_set_ushort(bs *b, size_t offset, unsigned short length);
int bs_read_socket(bs *b, int s, size_t num_bytes);
unsigned int checkIfTLSVersionIsSupported(struct sslCheckOptions *options, unsigned int tls_version);
unsigned int checkIfTLSVersionIsSupported_Backup(struct sslCheckOptions *options, unsigned int tls_version);
SSL_CTX *CTX_new(const SSL_METHOD *method);
int fileExists(char *);
void findMissingCiphers();
char *getPrintableTLSName(unsigned int tls_version);
bs *getServerHello(int s);
bs *makeCiphersuiteListAll(unsigned int tls_version);
bs *makeCiphersuiteListTLS13All();
bs *makeCiphersuiteListMissing(unsigned int tls_version);
bs *makeClientHello(struct sslCheckOptions *options, unsigned int version, bs *ciphersuite_list, bs *tls_extensions);
bs *makeTLSExtensions(struct sslCheckOptions *options, unsigned int include_signature_algorithms);
void markFoundCiphersuite(unsigned short server_cipher_id, unsigned int tls_version);
int ocsp_certid_print(BIO *bp, OCSP_CERTID *a, int indent);
static int ocsp_resp_cb(SSL *s, void *arg);
void readLine(FILE *, char *, int);
int readOrLogAndClose(int, void *, size_t, const struct sslCheckOptions *);
char *resolveCipherID(unsigned short cipher_id, int *cipher_bits);
static int password_callback(char *, int, int, void *);
const char *printableSslMethod(const SSL_METHOD *);
ssize_t sendString(int, const char[]);
int ssl_print_tmp_key(struct sslCheckOptions *, SSL *s);
void tlsExtensionAddDefaultKeyShare(bs *tls_extensions);
void tlsExtensionAddSupportedGroups(unsigned int tls_version, bs *tls_extensions);
void tlsExtensionAddTLSv1_3(bs *tls_extensions);
void tlsExtensionUpdateLength(bs *tls_extensions);
int tcpConnect(struct sslCheckOptions *);
void tls_reneg_init(struct sslCheckOptions *);
int outputRenegotiation(struct sslCheckOptions *, struct renegotiationOutput *);
struct renegotiationOutput *newRenegotiationOutput(void);
int freeRenegotiationOutput(struct renegotiationOutput *);
int testCompression(struct sslCheckOptions *, const SSL_METHOD *);
int testRenegotiation(struct sslCheckOptions *, const SSL_METHOD *);
#ifdef SSL_MODE_SEND_FALLBACK_SCSV
int testfallback(struct sslCheckOptions *, const SSL_METHOD *);
#endif
int testHeartbleed(struct sslCheckOptions *, const SSL_METHOD *);
int testSupportedGroups(struct sslCheckOptions *options);
int testSignatureAlgorithms(struct sslCheckOptions *options);
int testCipher(struct sslCheckOptions *, const SSL_METHOD *);
int testMissingCiphers(struct sslCheckOptions *options, unsigned int version);
int testProtocolCiphers(struct sslCheckOptions *, const SSL_METHOD *);
int testConnection(struct sslCheckOptions *);
int testHost(struct sslCheckOptions *);
int loadCerts(struct sslCheckOptions *);
int checkCertificateProtocols(struct sslCheckOptions *, const SSL_METHOD *);
int checkCertificate(struct sslCheckOptions *, const SSL_METHOD *);
int showCertificate(struct sslCheckOptions *);
int runSSLv2Test(struct sslCheckOptions *options);
int runSSLv3Test(struct sslCheckOptions *options);
#endif
...
int main(int argc, char *argv[])
{
...
hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
if (!GetConsoleMode(hConsole, &consoleMode)) {
hConsole = CreateFile("CONIN$", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
setvbuf(stdout, NULL, _IOLBF, 80);
if (!GetConsoleMode(hConsole, &consoleMode))
enable_colors = 0;
}
if (enable_colors && ((consoleMode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) == 0)) {
if (!SetConsoleMode(hConsole, consoleMode | ENABLE_VIRTUAL_TERMINAL_PROCESSING))
enable_colors = 0;
}
if (!enable_colors) {
RESET = "";
COL_RED = "";
COL_YELLOW = "";
COL_BLUE = "";
COL_GREEN = "";
COL_PURPLE = "";
COL_GREY = "";
COL_RED_BG = "";
}
wVersionRequested = MAKEWORD(2, 2);
err = WSAStartup(wVersionRequested, &wsaData);
if (err != 0)
{
printf_error("WSAStartup failed: %d", err);
return -1;
}
#endif
for (argLoop = 1; argLoop < argc; argLoop++)
{
if ((strcmp("--help", argv[argLoop]) == 0) || (strcmp("-h", argv[argLoop]) == 0))
mode = mode_help;
else if ((strncmp("--targets=", argv[argLoop], 10) == 0) && (strlen(argv[argLoop]) > 10))
{
mode = mode_multiple;
options->targets = argLoop;
}
else if (strcmp("--show-certificate", argv[argLoop]) == 0)
options->showCertificate = true;
else if (strcmp("--show-certificates", argv[argLoop]) == 0)
options->showCertificates = true;
else if (strcmp("--no-check-certificate", argv[argLoop]) == 0)
options->checkCertificate = false;
else if (strcmp("--show-ciphers", argv[argLoop]) == 0)
options->showClientCiphers = true;
else if (strcmp("--show-cipher-ids", argv[argLoop]) == 0)
{
options->showCipherIds = true;
}
else if (strcmp("--show-times", argv[argLoop]) == 0)
{
options->showTimes = true;
}
else if (strcmp("--show-client-cas", argv[argLoop]) == 0)
options->showTrustedCAs = true;
else if (strcmp("--version", argv[argLoop]) == 0)
mode = mode_version;
else if (strncmp("--xml=", argv[argLoop], 6) == 0)
xmlArg = argLoop;
else if (strcmp("--verbose", argv[argLoop]) == 0)
options->verbose = true;
else if (strcmp("--no-cipher-details", argv[argLoop]) == 0)
options->cipher_details = false;
else if ((strcmp("--no-colour", argv[argLoop]) == 0) || (strcmp("--no-color", argv[argLoop]) == 0))
{
RESET = "";
COL_RED = "";
COL_YELLOW = "";
COL_BLUE = "";
COL_GREEN = "";
COL_PURPLE = "";
COL_RED_BG = "";
COL_GREY = "";
}
// Client Certificates
else if (strncmp("--certs=", argv[argLoop], 8) == 0)
options->clientCertsFile = argv[argLoop] +8;
// Private Key File
else if (strncmp("--pk=", argv[argLoop], 5) == 0)
options->privateKeyFile = argv[argLoop] +5;
// Private Key Password
else if (strncmp("--pkpass=", argv[argLoop], 9) == 0)
options->privateKeyPassword = argv[argLoop] +9;
// Should we check for supported cipher suites
else if (strcmp("--no-ciphersuites", argv[argLoop]) == 0)
options->ciphersuites = false;
else if (strcmp("--no-fallback", argv[argLoop]) == 0)
options->fallback = false;
else if (strcmp("--no-renegotiation", argv[argLoop]) == 0)
options->reneg = false;
else if (strcmp("--no-compression", argv[argLoop]) == 0)
options->compression = false;
else if (strcmp("--no-heartbleed", argv[argLoop]) == 0)
options->heartbleed = false;
else if (strcmp("--no-groups", argv[argLoop]) == 0)
options->groups = false;
else if (strcmp("--show-sigs", argv[argLoop]) == 0)
options->signature_algorithms = true;
else if (strcmp("--iana-names", argv[argLoop]) == 0)
options->ianaNames = true;
else if (strcmp("--starttls-ftp", argv[argLoop]) == 0)
options->starttls_ftp = true;
else if (strcmp("--starttls-imap", argv[argLoop]) == 0)
options->starttls_imap = true;
else if (strcmp("--starttls-irc", argv[argLoop]) == 0)
options->starttls_irc = true;
else if (strcmp("--starttls-ldap", argv[argLoop]) == 0)
options->starttls_ldap = true;
else if (strcmp("--starttls-pop3", argv[argLoop]) == 0)
options->starttls_pop3 = true;
else if (strcmp("--starttls-smtp", argv[argLoop]) == 0)
options->starttls_smtp = true;
else if (strcmp("--starttls-mysql", argv[argLoop]) == 0)
options->starttls_mysql = true;
else if (strcmp("--starttls-xmpp", argv[argLoop]) == 0)
options->starttls_xmpp = true;
else if (strcmp("--starttls-psql", argv[argLoop]) == 0)
options->starttls_psql = true;
else if (strcmp("--ssl2", argv[argLoop]) == 0)
options->sslVersion = ssl_v2;
else if (strcmp("--ssl3", argv[argLoop]) == 0)
options->sslVersion = ssl_v3;
else if (strcmp("--tls10", argv[argLoop]) == 0)
options->sslVersion = tls_v10;
else if (strcmp("--tls11", argv[argLoop]) == 0)
options->sslVersion = tls_v11;
else if (strcmp("--tls12", argv[argLoop]) == 0)
options->sslVersion = tls_v12;
else if (strcmp("--tls13", argv[argLoop]) == 0)
options->sslVersion = tls_v13;
else if (strcmp("--tlsall", argv[argLoop]) == 0)
options->sslVersion = tls_all;
else if (strcmp("--xmpp-server", argv[argLoop]) == 0)
options->xmpp_server = true;
else if (strcmp("--bugs", argv[argLoop]) == 0)
options->sslbugs = 1;
else if (strncmp("--timeout=", argv[argLoop], 10) == 0)
options->timeout.tv_sec = atoi(argv[argLoop] + 10);
else if (strncmp("--connect-timeout=", argv[argLoop], 18) == 0)
options->connect_timeout = atoi(argv[argLoop] + 18);
else if (strncmp("--sleep=", argv[argLoop], 8) == 0)
{
msec = atoi(argv[argLoop] + 8);
if (msec >= 0) {
options->sleep = msec;
}
}
else if (strcmp("--rdp", argv[argLoop]) == 0)
options->rdp = 1;
else if ((strcmp("--ipv4", argv[argLoop]) == 0) || (strcmp("-4", argv[argLoop]) == 0))
options->ipv6 = false;
else if ((strcmp("--ipv6", argv[argLoop]) == 0) || (strcmp("-6", argv[argLoop]) == 0))
options->ipv4 = false;
else if (strcmp("--ocsp", argv[argLoop]) == 0)
options->ocspStatus = true;
else if (strncmp("--sni-name=", argv[argLoop], 11) == 0)
{
strncpy(options->sniname, argv[argLoop]+11, strlen(argv[argLoop])-11);
options->sni_set = 1;
}
else if (argLoop + 1 == argc)
{
mode = mode_single;
tempInt = 0;
char *hostString = argv[argLoop];
maxSize = strlen(hostString);
if (strncmp((char*)hostString, "https://", 8) == 0)
{
memmove(hostString, hostString + 8, (maxSize - 8));
memset(hostString + (maxSize - 8), 0, 8);
maxSize = strlen(hostString);
}
int squareBrackets = false;
if (hostString[0] == '[')
{
squareBrackets = true;
hostString++;
}
while ((hostString[tempInt] != 0) && ((squareBrackets == true && hostString[tempInt] != ']')
|| (squareBrackets == false && hostString[tempInt] != ':' && hostString[tempInt] != '/')))
{
tempInt++;
}
if (squareBrackets == true && hostString[tempInt] == ']')
{
hostString[tempInt] = 0;
if (tempInt < maxSize && (hostString[tempInt + 1] == ':' || hostString[tempInt + 1] == '/'))
{
tempInt++;
hostString[tempInt] = 0;
}
}
else
{
hostString[tempInt] = 0;
}
strncpy(options->host, hostString, sizeof(options->host) -1);
if (!options->sni_set)
{
strncpy(options->sniname, options->host, sizeof(options->host) -1);
}
tempInt++;
if (tempInt < maxSize)
{
errno = 0;
options->port = strtol((hostString + tempInt), NULL, 10);
if (options->port < 1 || options->port > 65535)
{
printf_error("Invalid target specified.");
exit(1);
}
}
else if (options->port == 0) {
if (options->starttls_ftp)
options->port = 21;
else if (options->starttls_imap)
options->port = 143;
else if (options->starttls_irc)
options->port = 6667;
else if (options->starttls_ldap)
options->port = 389;
else if (options->starttls_pop3)
options->port = 110;
else if (options->starttls_smtp)
options->port = 25;
else if (options->starttls_mysql)
options->port = 3306;
else if (options->starttls_xmpp)
options->port = 5222;
else if (options->starttls_psql)
options->port = 5432;
else if (options->rdp)
options->port = 3389;
else
options->port = 443;
}
}
else
mode = mode_help;
}
if ((xmlArg > 0) && (mode != mode_help))
{
if (strcmp(argv[xmlArg] + 6, "-") == 0)
{
options->xmlOutput = stdout;
xml_to_stdout = 1;
}
else
{
options->xmlOutput = fopen(argv[xmlArg] + 6, "w");
if (options->xmlOutput == NULL)
{
printf_error("Could not open XML output file %s.", argv[xmlArg] + 6);
exit(0);
}
}
fprintf(options->xmlOutput, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document title=\"SSLScan Results\" version=\"%s\" web=\"http://github.com/rbsec/sslscan\">\n", VERSION);
}
findMissingCiphers();
switch (mode)
{
case mode_version:
printf("%s\n%s", VERSION, OpenSSL_version(OPENSSL_VERSION));
break;
case mode_help:
printf("%s%s%s\n", COL_BLUE, program_banner, RESET);
printf("%s\t\t%s\n\t\t%s\n%s\n\n", COL_BLUE, VERSION,
OpenSSL_version(OPENSSL_VERSION), RESET);
printf("%sCommand:%s\n", COL_BLUE, RESET);
printf(" %s%s [options] [host:port | host]%s\n\n", COL_GREEN, argv[0], RESET);
printf("%sOptions:%s\n", COL_BLUE, RESET);
printf(" %s--targets=<file>%s A file containing a list of hosts to check.\n", COL_GREEN, RESET);
printf(" Hosts can be supplied with ports (host:port)\n");
printf(" %s--sni-name=<name>%s Hostname for SNI\n", COL_GREEN, RESET);
printf(" %s--ipv4, -4%s Only use IPv4\n", COL_GREEN, RESET);
printf(" %s--ipv6, -6%s Only use IPv6\n", COL_GREEN, RESET);
printf("\n");
printf(" %s--show-certificate%s Show full certificate information\n", COL_GREEN, RESET);
printf(" %s--show-certificates%s Show chain full certificates information\n", COL_GREEN, RESET);
printf(" %s--show-client-cas%s Show trusted CAs for TLS client auth\n", COL_GREEN, RESET);
printf(" %s--no-check-certificate%s Don't warn about weak certificate algorithm or keys\n", COL_GREEN, RESET);
printf(" %s--ocsp%s Request OCSP response from server\n", COL_GREEN, RESET);
printf(" %s--pk=<file>%s A file containing the private key or a PKCS#12 file\n", COL_GREEN, RESET);
printf(" containing a private key/certificate pair\n");
printf(" %s--pkpass=<password>%s The password for the private key or PKCS#12 file\n", COL_GREEN, RESET);
printf(" %s--certs=<file>%s A file containing PEM/ASN1 formatted client certificates\n", COL_GREEN, RESET);
printf("\n");
printf(" %s--ssl2%s Only check if SSLv2 is enabled\n", COL_GREEN, RESET);
printf(" %s--ssl3%s Only check if SSLv3 is enabled\n", COL_GREEN, RESET);
printf(" %s--tls10%s Only check TLSv1.0 ciphers\n", COL_GREEN, RESET);
printf(" %s--tls11%s Only check TLSv1.1 ciphers\n", COL_GREEN, RESET);
printf(" %s--tls12%s Only check TLSv1.2 ciphers\n", COL_GREEN, RESET);
printf(" %s--tls13%s Only check TLSv1.3 ciphers\n", COL_GREEN, RESET);
printf(" %s--tlsall%s Only check TLS ciphers (all versions)\n", COL_GREEN, RESET);
printf(" %s--show-ciphers%s Show supported client ciphers\n", COL_GREEN, RESET);
printf(" %s--show-cipher-ids%s Show cipher ids\n", COL_GREEN, RESET);
printf(" %s--iana-names%s Use IANA/RFC cipher names rather than OpenSSL ones\n", COL_GREEN, RESET);
printf(" %s--show-times%s Show handshake times in milliseconds\n", COL_GREEN, RESET);
printf("\n");
printf(" %s--no-cipher-details%s Disable EC curve names and EDH/RSA key lengths output\n", COL_GREEN, RESET);
printf(" %s--no-ciphersuites%s Do not check for supported ciphersuites\n", COL_GREEN, RESET);
printf(" %s--no-compression%s Do not check for TLS compression (CRIME)\n", COL_GREEN, RESET);
printf(" %s--no-fallback%s Do not check for TLS Fallback SCSV\n", COL_GREEN, RESET);
printf(" %s--no-groups%s Do not enumerate key exchange groups\n", COL_GREEN, RESET);
printf(" %s--no-heartbleed%s Do not check for OpenSSL Heartbleed (CVE-2014-0160)\n", COL_GREEN, RESET);
printf(" %s--no-renegotiation%s Do not check for TLS renegotiation\n", COL_GREEN, RESET);
printf(" %s--show-sigs%s Enumerate signature algorithms\n", COL_GREEN, RESET);
printf("\n");
printf(" %s--starttls-ftp%s STARTTLS setup for FTP\n", COL_GREEN, RESET);
printf(" %s--starttls-imap%s STARTTLS setup for IMAP\n", COL_GREEN, RESET);
printf(" %s--starttls-irc%s STARTTLS setup for IRC\n", COL_GREEN, RESET);
printf(" %s--starttls-ldap%s STARTTLS setup for LDAP\n", COL_GREEN, RESET);
printf(" %s--starttls-mysql%s STARTTLS setup for MYSQL\n", COL_GREEN, RESET);
printf(" %s--starttls-pop3%s STARTTLS setup for POP3\n", COL_GREEN, RESET);
printf(" %s--starttls-psql%s STARTTLS setup for PostgreSQL\n", COL_GREEN, RESET);
printf(" %s--starttls-smtp%s STARTTLS setup for SMTP\n", COL_GREEN, RESET);
printf(" %s--starttls-xmpp%s STARTTLS setup for XMPP\n", COL_GREEN, RESET);
printf(" %s--xmpp-server%s Use a server-to-server XMPP handshake\n", COL_GREEN, RESET);
printf(" %s--rdp%s Send RDP preamble before starting scan\n", COL_GREEN, RESET);
printf("\n");
printf(" %s--bugs%s Enable SSL implementation bug work-arounds\n", COL_GREEN, RESET);
printf(" %s--no-colour%s Disable coloured output\n", COL_GREEN, RESET);
printf(" %s--sleep=<msec>%s Pause between connection request. Default is disabled\n", COL_GREEN, RESET);
printf(" %s--timeout=<sec>%s Set socket timeout. Default is 3s\n", COL_GREEN, RESET);
printf(" %s--connect-timeout=<sec>%s Set connect timeout. Default is 75s\n", COL_GREEN, RESET);
printf(" %s--verbose%s Display verbose output\n", COL_GREEN, RESET);
printf(" %s--version%s Display the program version\n", COL_GREEN, RESET);
printf(" %s--xml=<file>%s Output results to an XML file. Use - for STDOUT.\n", COL_GREEN, RESET);
printf(" %s--help%s Display the help text you are now reading\n\n", COL_GREEN, RESET);
printf("%sExample:%s\n", COL_BLUE, RESET);
printf(" %s%s 127.0.0.1%s\n", COL_GREEN, argv[0], RESET);
printf(" %s%s [::1]%s\n\n", COL_GREEN, argv[0], RESET);
break;
case mode_single:
case mode_multiple:
printf("Version: %s%s%s\n%s\n%s\n", COL_GREEN, VERSION, RESET,
OpenSSL_version(OPENSSL_VERSION), RESET);
if (mode == mode_single)
{
if (testConnection(options))
{
testHost(options);
}
}
else
{
if (fileExists(argv[options->targets] + 10) == true)
{
targetsFile = fopen(argv[options->targets] + 10, "r");
if (targetsFile == NULL)
{
printf_error("Could not open targets file %s.", argv[options->targets] + 10);
}
else
{
readLine(targetsFile, line, sizeof(line));
while (feof(targetsFile) == 0)
{
if (strlen(line) != 0)
{
if (strncmp(line, "https://", 8) == 0)
{
memmove(line, line + 8, (strlen(line) - 8));
memset(line + (strlen(line) - 8), 0, 8);
}
tempInt = 0;
while ((line[tempInt] != 0) && (line[tempInt] != ':'))
tempInt++;
line[tempInt] = 0;
strncpy(options->host, line, sizeof(options->host) -1);
if (!options->sni_set)
{
strncpy(options->sniname, options->host, sizeof(options->host) -1);
}
tempInt++;
if (strlen(line + tempInt) > 0)
{
int port;
port = atoi(line + tempInt);
if (port == 0)
{
printf_error("Invalid port specified.");
exit(1);
}
else
{
options->port = port;
}
}
// Otherwise assume 443
else
{
options->port = 443;
}
// Test the host...
if (testConnection(options))
{
testHost(options);
}
printf("\n\n");
}
readLine(targetsFile, line, sizeof(line));
}
}
}
else
printf_error("Targets file %s does not exist.", argv[options->targets] + 10);
}
while (options->ciphers != 0)
{
sslCipherPointer = options->ciphers->next;
free(options->ciphers);
options->ciphers = sslCipherPointer;
}
break;
}
if ((xmlArg > 0) && (mode != mode_help))
{
fprintf(options->xmlOutput, "</document>\n");
fclose(options->xmlOutput);
}
return 0;
}
If you need the complete source code, please add the WeChat number (c17865354792)
1. 核心数据结构与配置初始化
代码里首先定义了一套配置结构体(sslCheckOptions),用来存储检测的核心参数:
- 目标信息:主机名、端口、SNI(服务器名称指示)、IP协议版本(IPv4/IPv6);
- 检测开关:是否检查压缩、心跳漏洞(Heartbleed)、重协商、密钥交换组等;
- 协议范围:指定只检测SSLv2、TLSv1.3等特定版本,还是全版本扫描;
- 输出配置:是否生成XML报告、是否显示详细信息(证书、密码套件ID)。
初始化时会把这些参数默认值设好(比如默认端口443、超时3秒),再解析命令行参数(比如--tls13指定只扫TLS1.3,--no-heartbleed跳过心跳漏洞检测),覆盖默认配置。
2. 核心检测流程
整个检测过程就像"客户端上门拜访服务器,挨个试不同的'加密暗号'":
是
否
是
否
解析命令行参数
初始化检测配置
建立TCP连接
生成客户端Hello报文
是否指定单一密码套件?
携带该套件发起握手
携带批量套件发起握手
接收服务器Hello响应
解析响应:记录支持的套件/协议
还有未检测的套件?
输出检测结果
关键步骤拆解:
- TCP连接建立 :先和目标服务器建立基础的TCP连接,这是SSL/TLS握手的前提,代码里通过
tcpConnect函数实现,还会处理超时、IPv4/IPv6选择等问题; - 构造Client Hello报文 :这是检测的核心------代码里
makeClientHello函数会根据配置,生成包含指定协议版本(比如TLS1.2)、密码套件列表(比如TLS_AES_128_GCM_SHA256)、TLS扩展(比如SNI、密钥交换组)的客户端握手报文; - 解析Server Hello响应 :服务器收到客户端报文后,会返回支持的密码套件和协议版本,
getServerHello函数读取并解析这个响应,记录下"服务器认哪个暗号"; - 逐一套件试探 :工具会遍历预设的密码套件列表(包括OpenSSL内置的,还有代码里手动补充的
missing_ciphersuites),重复"发Hello报文-读响应"的过程,直到把所有套件试完。
3. 特殊场景处理
代码里还考虑了各种实战场景:
- STARTTLS支持:比如扫描FTP/IMAP/SMTP等服务时,需要先发送STARTTLS命令切换到加密模式,再进行SSL/TLS检测;
- 证书检测:除了密码套件,还会检查证书的算法(比如是否用SHA1签名)、密钥长度(比如RSA是否小于2048位)、是否过期等;
- 漏洞专项检测 :比如Heartbleed漏洞检测(
testHeartbleed函数),会发送特制的心跳报文,看服务器是否会泄露内存数据;重协商检测(testRenegotiation)则验证是否存在重协商漏洞。
三、设计思路:为什么这么做?
1. 脱离OpenSSL版本依赖(核心设计亮点)
早期的检测工具很依赖系统安装的OpenSSL版本------比如老版本OpenSSL不支持TLS1.3,工具就没法扫;新版本不支持SSLv2,工具也扫不了。现在的设计思路是:
- 手动维护TLS1.3密码套件列表(代码里的
TLSV13_CIPHERSUITES宏),不依赖OpenSSL的内置列表; - 自己构造SSL/TLS握手报文(而不是完全用OpenSSL的API),哪怕系统OpenSSL不支持某个协议版本,也能模拟报文进行检测。
2. 兼顾兼容性与安全性
- 兼容性 :支持Windows/Linux/macOS,处理了不同系统的差异(比如Windows的控制台颜色、睡眠函数
SLEEPMS,Linux的select实现延时); - 安全性:检测过程中会主动规避弱密码套件、过时协议的风险提示,比如高亮显示SSLv3(POODLE漏洞)、3DES/RC4等弱套件。
3. 可扩展性设计
代码里把检测逻辑拆分成多个独立函数(比如testCompression检测压缩漏洞、testSupportedGroups检测密钥交换组),新增检测项时只需要加新函数,不用改核心流程,比如想加一个"检测TLS1.3零RTT支持",只需要新增test0RTT函数,在检测流程里调用即可。
四、核心知识点总结(领域视角)
1. 网络安全领域
- SSL/TLS协议分层:握手层(负责协商密码套件)、记录层(负责数据加密)是检测的核心关注层;
- 密码套件的安全等级划分:弱套件(RC4、3DES)、安全套件(AES-GCM+ECDHE)、匿名套件(ADH,无身份验证)的识别是检测的核心目标;
- 常见漏洞:Heartbleed(CVE-2014-0160)、POODLE(SSLv3漏洞)、CRIME(TLS压缩漏洞)都是这类工具重点检测的方向。
2. 编程实现领域
- 网络编程:手动构造/解析TCP报文(而非仅用封装好的SSL库),需要熟悉SSL/TLS报文格式;
- 跨平台开发:处理不同系统的API差异(比如Windows的WSAStartup、Linux的close);
- 内存安全:代码里的
FREE/CLOSE宏(释放内存/文件句柄后置NULL),避免使用后释放(use-after-free)漏洞。
3. 工程设计领域
- 模块化设计:把配置、检测、输出拆分成独立模块,便于维护和扩展;
- 容错设计:处理超时、连接失败、非法端口等异常情况,避免工具崩溃;
- 用户体验:支持彩色输出、XML报告、详细/简化输出模式,适配不同使用场景。
五、测试基础扫描示例
1. 扫描单个HTTPS服务(默认443端口)
bash
# 扫描目标:github.com(默认检测所有SSL/TLS版本、密码套件、核心漏洞)
./ssl_scan github.com
核心检测内容:
- 自动检测支持的SSLv2/SSLv3/TLSv1.0~1.3协议版本;
- 列出服务端支持的所有密码套件(高亮弱套件/安全套件);
- 检查Heartbleed、TLS压缩(CRIME)、重协商等核心漏洞;
- 简要输出证书信息(默认隐藏详细证书,仅提示弱证书问题如SHA1签名、RSA密钥<2048位)。
2. 扫描指定端口的服务
bash
# 扫描目标:192.168.1.100的8443端口(常见的HTTPS管理端口)
./ssl_scan 192.168.1.100:8443
适用场景:目标服务未使用443默认端口(如Tomcat默认8443、Jenkins默认8443等)。
3. 仅使用IPv4/IPv6扫描
bash
# 仅IPv4扫描
./ssl_scan --ipv4 google.com
# 仅IPv6扫描(需目标支持IPv6)
./ssl_scan --ipv6 [2404:6800:4008:800::2004]
关键说明 :v2版本原生支持IPv6,--ipv6参数强制仅使用IPv6协议栈,适合检测双栈服务器的IPv6加密配置。
专项协议/密码套件检测示例
1. 仅检测特定TLS版本
bash
# 仅检测TLSv1.3
./ssl_scan --tls13 www.baidu.com
# 仅检测TLSv1.0(排查弱协议)
./ssl_scan --tls10 www.example.com
# 仅检测SSLv3(排查POODLE漏洞)
./ssl_scan --ssl3 old-server.example.com
核心价值:v2版本脱离OpenSSL依赖,即使系统OpenSSL不支持SSLv2/SSLv3,也能检测这些遗留协议是否开启。
2. 跳过指定漏洞/项检测(减少扫描耗时)
bash
# 扫描时跳过Heartbleed、压缩、重协商检测
./ssl_scan --no-heartbleed --no-compression --no-renegotiation www.taobao.com
适用场景:批量扫描大量服务器时,跳过非核心检测项提升效率。
3. 检测密钥交换组和签名算法(v2新特性)
bash
# 显示服务端支持的密钥交换组+签名算法
./ssl_scan --show-sigs --no-cipher-details www.cloudflare.com
输出说明:
--show-sigs:枚举服务端支持的签名算法(如rsa_pkcs1_sha256、ecdsa_secp256r1_sha256);--no-cipher-details:关闭EC曲线名称、DHE密钥长度的详细输出(简化结果)。
特殊服务(STARTTLS/RDP)扫描示例
1. 扫描启用STARTTLS的邮件服务(SMTP)
bash
# 扫描SMTP服务(25端口)的STARTTLS加密配置
./ssl_scan --starttls-smtp smtp.163.com:25
# 扫描POP3服务(110端口)的STARTTLS
./ssl_scan --starttls-pop3 pop.163.com:110
核心逻辑 :先发送STARTTLS命令切换到加密模式,再执行SSL/TLS握手检测(对应代码中starttls_smtp/starttls_pop3配置项)。
2. 扫描数据库服务的加密配置
bash
# 扫描MySQL(3306端口)的STARTTLS
./ssl_scan --starttls-mysql 192.168.1.200:3306
# 扫描PostgreSQL(5432端口)的STARTTLS
./ssl_scan --starttls-psql 192.168.1.201:5432
3. 扫描RDP服务(Windows远程桌面)
bash
# 扫描Windows服务器RDP的SSL/TLS配置(3389端口)
./ssl_scan --rdp 192.168.1.10:3389
关键说明:发送RDP前置报文后再执行SSL/TLS检测,适配RDP服务的特殊握手逻辑。
输出定制与高级功能示例
1. 生成XML报告(v2格式变更)
bash
# 输出XML报告到文件
./ssl_scan --xml=scan-result.xml www.jd.com
# 输出XML到标准输出(便于管道处理)
./ssl_scan --xml=- www.alipay.com > alipay-ssl.xml
v2 XML变更注意:
- 新增
<certificates>父节点,包含<certificate type="short/full">子节点; <signature-algorithm>不再包含冗余前缀(如"Signature Algorithm: "),解析时无需额外处理。
2. 显示详细证书信息+证书链
bash
# 显示完整证书链+详细证书信息
./ssl_scan --show-certificates --show-certificate www.gov.cn
输出说明:
--show-certificate:显示单证书的完整信息(如颁发机构、有效期、公钥长度);--show-certificates:显示整个证书链(根证书、中间证书、服务器证书)。
3. 批量扫描多个目标(从文件读取)
bash
# 先创建目标文件 targets.txt,内容示例:
# github.com:443
# 192.168.1.1:8443
# smtp.qq.com:25
# 批量扫描目标文件中的所有服务器
./ssl_scan --targets=targets.txt --sleep=100
参数说明:
--targets:指定包含目标列表的文件;--sleep=100:每两次请求间隔100ms(避免触发目标服务器的频率限制)。
兼容性/特殊配置示例
1. 启用SNI(多域名服务器)
bash
# 扫描共享IP的服务器,指定SNI名称
./ssl_scan --sni-name=blog.example.com 192.168.1.50:443
核心作用 :针对一台服务器托管多个域名的场景,指定SNI名称才能获取对应域名的正确加密配置(对应代码中sniname/sni_set参数)。
2. 静态编译版本扫描(跨系统使用)
bash
# 静态编译的ssl_scan (无系统依赖)扫描Windows服务器
./ssl_scan-static --ipv4 --timeout=5 10.0.0.1:443
适用场景:在无OpenSSL依赖的环境(如精简版Linux、容器)中使用。
六、实际应用价值
这类检测工具的核心价值,是帮运维/安全人员:
- 合规检查:满足等保、PCI-DSS等合规要求(比如禁用SSLv3、弱密码套件);
- 风险排查:提前发现服务端的加密配置漏洞,避免被攻击者利用;
- 配置优化:指导管理员关闭弱套件、启用安全套件,提升通信安全性。
简单来说,这项技术就是网络通信安全的"体检仪"------通过模拟真实的加密握手过程,摸清服务端的加密家底,让安全问题无所遁形。而其底层设计的核心,就是"脱离第三方库依赖+精准模拟协议交互+全面覆盖安全检测点",既保证了检测的准确性,又兼顾了工具的兼容性和扩展性。
总结
- 密码套件检测的本质是模拟SSL/TLS握手,通过试探不同加密配置获取服务端支持的套件和协议;
- 核心设计亮点是脱离OpenSSL版本依赖,手动构造协议报文,兼容新旧协议(SSLv2到TLS1.3);
- 实现上采用模块化思路,兼顾跨平台兼容性、漏洞专项检测和用户可配置性,是网络安全检测领域的典型工程实践。
Welcome to follow WeChat official account【程序猿编码】