0x00 前言
OpenSSL官方在7月9日发布了编号为 CVE-2015-1793 的交叉证书验证绕过漏洞,其中主要影响了OpenSSL的1.0.1和1.0.2分支。1.0.0和0.9.8分支不受影响。
360安全研究员au2o3t对该漏洞进行了原理上的分析,确认是一个绕过交叉链类型证书验证的高危漏洞,可以让攻击者构造证书来绕过交叉验证,用来形成诸如“中间人”等形式的攻击。
0x01 漏洞基本原理
直接看最简单的利用方法(利用方法包括但不限于此):
攻击者从一公共可信的 CA (C)处签得一证书 X,并以此证书签发另一证书 V(含对X的交叉引用),那么攻击者发出的证书链 V, R (R为任意证书)对信任 C 的用户将是可信的。
显然用户对 V, R 链的验证会返回失败。
对不支持交叉链认证的老版本来说,验证过程将以失败结束。
对支持交叉认证的版本,则将会尝试构建交叉链 V, X, C,并继续进行验证。
虽然 V, X, C 链能通过可信认证,但会因 X 的用法不包括 CA 而导致验证失败。
但在 openssl-1.0.2c 版本,因在对交叉链的处理中,对最后一个不可信证书位置计数的错误,导致本应对 V, X 记为不可信并验证,错记为了仅对 V 做验证,而没有验证攻击者的证书 X,返回验证成功。
0x02 具体漏洞分析
漏洞代码位于文件:openssl-1.0.2c/crypto/x509/x509_vfy.c
函数:X509_verify_cert() 中
第 392 行:ctx->last_untrusted–;
对问题函数 X509_verify_cert 的简单分析:
( 为方便阅读,仅保留与证书验证强相关的代码,去掉了诸如变量定义、错误处理、资源释放等非主要代码)
问题在于由 <1> 处加入颁发者时及 <2> 处验证(颁发者)后,证书链计数增加,但 最后一个不可信证书位置计数 并未增加, 而在 <4> 处去除过程中 最后一个不可信证书位置计数 额外减少了,导致后面验证过程中少验。
(上述 V, X, C 链中应验 V, X 但少验了 X)
代码分析如下
#!c++ int X509_verify_cert(X509_STORE_CTX *ctx) { // 将 ctx->cert 做为不信任证书压入需验证链 ctx->chain // STACK_OF(X509) *chain 将被构造为证书链,并最终送到 internal_verify() 中去验证 sk_X509_push(ctx->chain,ctx->cert); // 当前链长度(==1) num = sk_X509_num(ctx->chain); // 取出第 num 个证书 x = sk_X509_value(ctx->chain, num - 1); // 存在不信任链则复制之 if (ctx->untrusted != NULL && (sktmp = sk_X509_dup(ctx->untrusted)) == NULL) { X509err(X509_F_X509_VERIFY_CERT, ERR_R_MALLOC_FAILURE); goto end; } // 预设定的最大链深度(100) depth = param->depth; // 构造需验证证书链 for (;;) { // 超长退出 if (depth < num) break; // 遇自签退出(链顶) if (cert_self_signed(x)) break; if (ctx->untrusted != NULL) { xtmp = find_issuer(ctx, sktmp, x); // 当前证书为不信任颁发者(应需CA标志)颁发 if (xtmp != NULL) { // 则加入需验证链 if (!sk_X509_push(ctx->chain, xtmp)) { X509err(X509_F_X509_VERIFY_CERT, ERR_R_MALLOC_FAILURE); goto end; } CRYPTO_add(&xtmp->references, 1, CRYPTO_LOCK_X509); (void)sk_X509_delete_ptr(sktmp, xtmp); // 最后一个不可信证书位置计数 自增1 ctx->last_untrusted++; x = xtmp; num++; continue; } } break; } do { i = sk_X509_num(ctx->chain); x = sk_X509_value(ctx->chain, i - 1); // 若最顶证书是自签的 if (cert_self_signed(x)) { // 若需验证链长度 == 1 if (sk_X509_num(ctx->chain) == 1) { // 在可信链中查找其颁发者(找自己) ok = ctx->get_issuer(&xtmp, ctx, x); // 没找到或不是相同证书 if ((ok <= 0) || X509_cmp(x, xtmp)) { ctx->error = X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT; ctx->current_cert = x; ctx->error_depth = i - 1; if (ok == 1) X509_free(xtmp); bad_chain = 1; ok = cb(0, ctx); if (!ok) goto end; // 找到 } else { X509_free(x); x = xtmp; // 入到可信链 (void)sk_X509_set(ctx->chain, i - 1, x); // 最后一个不可信证书位置计数 置0 ctx->last_untrusted = 0; } // 最顶为自签证书 且 证书链长度>1 } else { // 弹出 chain_ss = sk_X509_pop(ctx->chain); // 最后一个不可信证书位置计数 自减 ctx->last_untrusted--; num--; j--; // 保持指向当前最顶证书 x = sk_X509_value(ctx->chain, num - 1); } } // <1> // 继续构造证书链(加入颁发者) for (;;) { // 自签退出 if (cert_self_signed(x)) break; // 在可信链中查找其颁发者 ok = ctx->get_issuer(&xtmp, ctx, x); // 出错 if (ok < 0) return ok; // 没找到 if (ok == 0) break; x = xtmp; // 将不可信证书的颁发者(证书)加入需验证证书链 if (!sk_X509_push(ctx->chain, x)) { X509_free(xtmp); X509err(X509_F_X509_VERIFY_CERT, ERR_R_MALLOC_FAILURE); return 0; } num++; } // <2> // 验证 for(;;) 中加入的颁发者链 i = check_trust(ctx); if (i == X509_TRUST_REJECTED) goto end; retry = 0; // <3> // 检查交叉链 if (i != X509_TRUST_TRUSTED && !(ctx->param->flags & X509_V_FLAG_TRUSTED_FIRST) && !(ctx->param->flags & X509_V_FLAG_NO_ALT_CHAINS)) { while (j-- > 1) { xtmp2 = sk_X509_value(ctx->chain, j - 1); // 其实得到一个“看似合理”的证书就返回,这里实际上仅仅根据 CN域 查找颁发者 ok = ctx->get_issuer(&xtmp, ctx, xtmp2); if (ok < 0) goto end; // 存在交叉链 if (ok > 0) { X509_free(xtmp); // 去除交叉链以上部分 while (num > j) { xtmp = sk_X509_pop(ctx->chain); X509_free(xtmp); num--; // <4> // 问题所在 ctx->last_untrusted--; } // <5> retry = 1; break; } } } } while (retry); …… }
官方的解决方法是在 <5> 处重新计算 最后一个不可信证书位置计数 的值为链长:
ctx->last_untrusted = sk_X509_num(ctx->chain);
并去掉 <4> 处的 最后一个不可信证书位置计数 自减运算(其实去不去掉都无所谓)。 另一个解决办法可以是在 <1> <2> 后,在 <3> 处重置 最后一个不可信证书位置计数,加一行:
ctx->last_untrusted = num;
这样 <4> 处不用删除,而逻辑也是合理并前后一致的。
0x03 漏洞验证
笔者修改了部分代码并做了个Poc 。 修改代码:
#!c++ int X509_verify_cert(X509_STORE_CTX *ctx) { X509 *x, *xtmp, *xtmp2, *chain_ss = NULL; int bad_chain = 0; X509_VERIFY_PARAM *param = ctx->param; int depth, i, ok = 0; int num, j, retry; int (*cb) (int xok, X509_STORE_CTX *xctx); STACK_OF(X509) *sktmp = NULL; if (ctx->cert == NULL) { X509err(X509_F_X509_VERIFY_CERT, X509_R_NO_CERT_SET_FOR_US_TO_VERIFY); return -1; } cb = ctx->verify_cb; /* * first we make sure the chain we are going to build is present and that * the first entry is in place */ if (ctx->chain == NULL) { if (((ctx->chain = sk_X509_new_null()) == NULL) || (!sk_X509_push(ctx->chain, ctx->cert))) { X509err(X509_F_X509_VERIFY_CERT, ERR_R_MALLOC_FAILURE); goto end; } CRYPTO_add(&ctx->cert->references, 1, CRYPTO_LOCK_X509); ctx->last_untrusted = 1; } /* We use a temporary STACK so we can chop and hack at it */ if (ctx->untrusted != NULL && (sktmp = sk_X509_dup(ctx->untrusted)) == NULL) { X509err(X509_F_X509_VERIFY_CERT, ERR_R_MALLOC_FAILURE); goto end; } num = sk_X509_num(ctx->chain); x = sk_X509_value(ctx->chain, num - 1); depth = param->depth; for (;;) { /* If we have enough, we break */ if (depth < num) break; /* FIXME: If this happens, we should take * note of it and, if appropriate, use the * X509_V_ERR_CERT_CHAIN_TOO_LONG error code * later. */ /* If we are self signed, we break */ if (cert_self_signed(x)) break; /* * If asked see if we can find issuer in trusted store first */ if (ctx->param->flags & X509_V_FLAG_TRUSTED_FIRST) { ok = ctx->get_issuer(&xtmp, ctx, x); if (ok < 0) return ok; /* * If successful for now free up cert so it will be picked up * again later. */ if (ok > 0) { X509_free(xtmp); break; } } /* If we were passed a cert chain, use it first */ if (ctx->untrusted != NULL) { xtmp = find_issuer(ctx, sktmp, x); if (xtmp != NULL) { if (!sk_X509_push(ctx->chain, xtmp)) { X509err(X509_F_X509_VERIFY_CERT, ERR_R_MALLOC_FAILURE); goto end; } CRYPTO_add(&xtmp->references, 1, CRYPTO_LOCK_X509); (void)sk_X509_delete_ptr(sktmp, xtmp); ctx->last_untrusted++; x = xtmp; num++; /* * reparse the full chain for the next one */ continue; } } break; } /* Remember how many untrusted certs we have */ j = num; /* * at this point, chain should contain a list of untrusted certificates. * We now need to add at least one trusted one, if possible, otherwise we * complain. */ do { /* * Examine last certificate in chain and see if it is self signed. */ i = sk_X509_num(ctx->chain); x = sk_X509_value(ctx->chain, i - 1); if (cert_self_signed(x)) { /* we have a self signed certificate */ if (sk_X509_num(ctx->chain) == 1) { /* * We have a single self signed certificate: see if we can * find it in the store. We must have an exact match to avoid * possible impersonation. */ ok = ctx->get_issuer(&xtmp, ctx, x); if ((ok <= 0) || X509_cmp(x, xtmp)) { ctx->error = X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT; ctx->current_cert = x; ctx->error_depth = i - 1; if (ok == 1) X509_free(xtmp); bad_chain = 1; ok = cb(0, ctx); if (!ok) goto end; } else { /* * We have a match: replace certificate with store * version so we get any trust settings. */ X509_free(x); x = xtmp; (void)sk_X509_set(ctx->chain, i - 1, x); ctx->last_untrusted = 0; } } else { /* * extract and save self signed certificate for later use */ chain_ss = sk_X509_pop(ctx->chain); ctx->last_untrusted--; num--; j--; x = sk_X509_value(ctx->chain, num - 1); } } /* We now lookup certs from the certificate store */ for (;;) { /* If we have enough, we break */ if (depth < num) break; /* If we are self signed, we break */ if (cert_self_signed(x)) break; ok = ctx->get_issuer(&xtmp, ctx, x); if (ok < 0) return ok; if (ok == 0) break; x = xtmp; if (!sk_X509_push(ctx->chain, x)) { X509_free(xtmp); X509err(X509_F_X509_VERIFY_CERT, ERR_R_MALLOC_FAILURE); return 0; } num++; } /* we now have our chain, lets check it... */ i = check_trust(ctx); /* If explicitly rejected error */ if (i == X509_TRUST_REJECTED) goto end; /* * If it's not explicitly trusted then check if there is an alternative * chain that could be used. We only do this if we haven't already * checked via TRUSTED_FIRST and the user hasn't switched off alternate * chain checking */ retry = 0; // <1> //ctx->last_untrusted = num; if (i != X509_TRUST_TRUSTED && !(ctx->param->flags & X509_V_FLAG_TRUSTED_FIRST) && !(ctx->param->flags & X509_V_FLAG_NO_ALT_CHAINS)) { while (j-- > 1) { xtmp2 = sk_X509_value(ctx->chain, j - 1); ok = ctx->get_issuer(&xtmp, ctx, xtmp2); if (ok < 0) goto end; /* Check if we found an alternate chain */ if (ok > 0) { /* * Free up the found cert we'll add it again later */ X509_free(xtmp); /* * Dump all the certs above this point - we've found an * alternate chain */ while (num > j) { xtmp = sk_X509_pop(ctx->chain); X509_free(xtmp); num--; ctx->last_untrusted--; } retry = 1; break; } } } } while (retry); printf(" num=%d, real-num=%d/n", ctx->last_untrusted, sk_X509_num(ctx->chain) ); /* * If not explicitly trusted then indicate error unless it's a single * self signed certificate in which case we've indicated an error already * and set bad_chain == 1 */ if (i != X509_TRUST_TRUSTED && !bad_chain) { if ((chain_ss == NULL) || !ctx->check_issued(ctx, x, chain_ss)) { if (ctx->last_untrusted >= num) ctx->error = X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY; else ctx->error = X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT; ctx->current_cert = x; } else { sk_X509_push(ctx->chain, chain_ss); num++; ctx->last_untrusted = num; ctx->current_cert = chain_ss; ctx->error = X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN; chain_ss = NULL; } ctx->error_depth = num - 1; bad_chain = 1; ok = cb(0, ctx); if (!ok) goto end; } printf("flag=1/n"); /* We have the chain complete: now we need to check its purpose */ ok = check_chain_extensions(ctx); if (!ok) goto end; printf("flag=2/n"); /* Check name constraints */ ok = check_name_constraints(ctx); if (!ok) goto end; printf("flag=3/n"); ok = check_id(ctx); if (!ok) goto end; printf("flag=4/n"); /* We may as well copy down any DSA parameters that are required */ X509_get_pubkey_parameters(NULL, ctx->chain); /* * Check revocation status: we do this after copying parameters because * they may be needed for CRL signature verification. */ ok = ctx->check_revocation(ctx); if (!ok) goto end; printf("flag=5/n"); i = X509_chain_check_suiteb(&ctx->error_depth, NULL, ctx->chain, ctx->param->flags); if (i != X509_V_OK) { ctx->error = i; ctx->current_cert = sk_X509_value(ctx->chain, ctx->error_depth); ok = cb(0, ctx); if (!ok) goto end; } printf("flag=6/n"); /* At this point, we have a chain and need to verify it */ if (ctx->verify != NULL) ok = ctx->verify(ctx); else ok = internal_verify(ctx); if (!ok) goto end; printf("flag=7/n"); #ifndef OPENSSL_NO_RFC3779 /* RFC 3779 path validation, now that CRL check has been done */ ok = v3_asid_validate_path(ctx); if (!ok) goto end; ok = v3_addr_validate_path(ctx); if (!ok) goto end; #endif printf("flag=8/n"); /* If we get this far evaluate policies */ if (!bad_chain && (ctx->param->flags & X509_V_FLAG_POLICY_CHECK)) ok = ctx->check_policy(ctx); if (!ok) goto end; if (0) { end: X509_get_pubkey_parameters(NULL, ctx->chain); } if (sktmp != NULL) sk_X509_free(sktmp); if (chain_ss != NULL) X509_free(chain_ss); printf("ok=%d/n", ok ); return ok; } Poc: ? // //里头的证书文件自己去找一个,这个不提供了 // #include <stdio.h> #include <openssl/crypto.h> #include <openssl/bio.h> #include <openssl/x509.h> #include <openssl/pem.h> STACK_OF(X509) *load_certs_from_file(const char *file) { STACK_OF(X509) *certs; BIO *bio; X509 *x; bio = BIO_new_file( file, "r"); certs = sk_X509_new_null(); do { x = PEM_read_bio_X509(bio, NULL, 0, NULL); sk_X509_push(certs, x); }while( x != NULL ); return certs; } void test(void) { X509 *x = NULL; STACK_OF(X509) *untrusted = NULL; BIO *bio = NULL; X509_STORE_CTX *sctx = NULL; X509_STORE *store = NULL; X509_LOOKUP *lookup = NULL; store = X509_STORE_new(); lookup = X509_STORE_add_lookup( store, X509_LOOKUP_file() ); X509_LOOKUP_load_file(lookup, "roots.pem", X509_FILETYPE_PEM); untrusted = load_certs_from_file("untrusted.pem"); bio = BIO_new_file("bad.pem", "r"); x = PEM_read_bio_X509(bio, NULL, 0, NULL); sctx = X509_STORE_CTX_new(); X509_STORE_CTX_init(sctx, store, x, untrusted); X509_verify_cert(sctx); } int main(void) { test(); return 0; }
将代码中 X509_verify_cert() 函数加入输出信息如下: 编译,以伪造证书测试,程序输出信息为:
num=1, real-num=3 flag=1 flag=2 flag=3 flag=4 flag=5 flag=6 flag=7 flag=8 ok=1
认证成功 将 <1> 处注释代码去掉,编译,再以伪造证书测试,程序输出信息为:
num=3, real-num=3 flag=1 ok=0
认证失败
0x04 安全建议
建议使用受影响版本(OpenSSL 1.0.2b/1.0.2c 和 OpenSSL 1.0.1n/1.0.1o)的 产品或代码升级OpenSSL到最新版本
原创文章,作者:kepupublish,如若转载,请注明出处:https://blog.ytso.com/56032.html