OpenSSL-CVE-2015-1793漏洞分析
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到最新版本
相關文章
- 【漏洞分析】KaoyaSwap 安全事件分析2022-08-28事件
- JSON劫持漏洞分析2018-05-17JSON
- BlueKeep 漏洞利用分析2019-09-20
- 漏洞分析 | Dubbo2.7.7反序列化漏洞繞過分析2020-07-02
- PfSense命令注入漏洞分析2020-08-19
- SSRF漏洞簡單分析2020-07-16
- tp5漏洞分析2024-06-30
- 漏洞分析——變數缺陷漏洞及通用異常捕獲宣告缺陷漏洞2021-09-01變數
- CVE-2017-8890漏洞分析2018-08-15
- Windows PrintDemon提權漏洞分析2020-05-25Windows
- 軟體漏洞分析技巧分享2020-08-19
- CVE-2015-1641漏洞分析2020-08-19
- 關於libStagefright系列漏洞分析2020-08-19
- CVE-2020-1362 漏洞分析2020-07-28
- CVE-2013-3906漏洞分析2018-04-21
- thinkphp3.2.x漏洞分析2024-06-30PHP
- 漏洞分析:CVE-2017-172152021-12-04
- Sunlogin RCE漏洞分析和使用2022-02-19
- Java安全之Axis漏洞分析2021-11-26Java
- 漏洞分析:CVE 2021-31562021-08-11
- Java安全之XStream 漏洞分析2021-07-22Java
- CVE-2020-1247漏洞分析2023-02-27
- 【漏洞分析】ReflectionToken BEVO代幣攻擊事件分析2023-05-09事件
- Apache Tomcat檔案包含漏洞分析2020-02-24ApacheTomcat
- 安卓Bug 17356824 BroadcastAnywhere漏洞分析2020-08-19安卓AST
- 某EXCEL漏洞樣本shellcode分析2020-08-19Excel
- cve-2014-0569 漏洞利用分析2020-08-19
- Android uncovers master-key 漏洞分析2020-08-19AndroidAST
- 某CCTV攝像頭漏洞分析2020-08-19
- Joomla 物件注入漏洞分析報告2020-08-19OOM物件
- CORS漏洞的學習與分析2020-04-18CORS
- pwnkit漏洞分析-CVE-2021-40342022-01-31
- [javaweb]strut2-001漏洞分析2022-01-16JavaWeb
- CVE-2006-3439漏洞原理分析2022-04-27
- 【漏洞復現】Paraluni 安全事件分析2022-03-15事件
- 從0開始fastjson漏洞分析2021-05-17ASTJSON
- 網站漏洞修復服務商關於越權漏洞分析2022-07-15網站
- CVE-2018-4990 漏洞詳情分析2018-05-29