CVE-2025-10966:wolfSSH后端缺失SFTP主机密钥验证的安全漏洞分析

本文详细分析了curl在使用wolfSSH后端时存在的SFTP主机密钥验证缺失漏洞。该漏洞导致SSH/SFTP连接无法验证服务器主机身份,可能遭受中间人攻击。文章包含完整的复现步骤和代码分析。

CVE-2025-10966:wolfSSH缺失SFTP主机验证

漏洞概述

当curl使用wolfSSH后端构建时,lib/vssh/wolfssh.c中的SSH/SFTP实现未执行服务器主机密钥验证,且在curl工具中未暴露主机身份选项。通过在本地构建带有wolfSSH的curl(二进制报告wolfssh/1.4.20),观察到SSH主机验证选项在工具中不可用,并检查了wolfSSH代码路径,发现连接时没有任何主机密钥检查或known_hosts处理。

AI使用声明:本报告文本在协助下准备,但以下发现通过本地构建、运行时行为和代码检查手动验证。

受影响版本

本地构建在macOS(Apple Silicon):

1
2
3
$ ./src/curl --version
curl 8.17.0-DEV (aarch64-apple-darwin23.3.0) libcurl/8.17.0-DEV wolfSSL/5.8.2 zlib/1.2.12 brotli/1.1.0 zstd/1.5.7 libidn2/2.3.8 libpsl/0.21.5 wolfssh/1.4.20 nghttp2/1.66.0 librtmp/2.3
Protocols: dict file ftp ftps gopher gophers http https imap imaps ipfs ipns ldap ldaps mqtt pop3 pop3s rtmp rtsp sftp smb smbs smtp smtps telnet tftp ws wss

复现步骤

构建wolfSSH(带SFTP)并在本地安装(macOS/Homebrew示例):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 前置条件(Homebrew)
brew install wolfssl libpsl libidn2 libnghttp2

# 构建并安装启用SFTP的wolfSSH
git clone --depth=1 https://github.com/wolfSSL/wolfssh.git
cd wolfssh
./autogen.sh
./configure \
  --prefix=$HOME/.local/wolfssh \
  --with-wolfssl=/opt/homebrew \
  --enable-sftp \
  --enable-sshclient \
  --enable-examples \
 CPPFLAGS='-I/opt/homebrew/include' \
 LDFLAGS='-L/opt/homebrew/lib'
make -j$(sysctl -n hw.ncpu)
make install

配置并使用wolfSSH构建curl:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
cd /Users/$USER/scanner-repos/curl
autoreconf -fi
PKG_CONFIG_PATH="$HOME/.local/wolfssh/lib/pkgconfig:/opt/homebrew/lib/pkgconfig" \
CPPFLAGS="-I$HOME/.local/wolfssh/include -I/opt/homebrew/include" \
LDFLAGS="-L$HOME/.local/wolfssh/lib -L/opt/homebrew/lib" \
./configure \
  --without-libssh2 \
  --without-libssh \
  --with-wolfssh=$HOME/.local/wolfssh \
  --with-wolfssl=/opt/homebrew \
  --disable-shared
make -j$(sysctl -n hw.ncpu)

验证构建显示wolfSSH:

1
2
./src/curl --version
# 输出包含:wolfssh/1.4.20

观察SSH主机身份选项在此构建中不可用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
./src/curl --help ssh
# 未列出--ssh-knownhosts或--hostpubsha256

./src/curl --ssh-knownhosts /tmp/kh -vvv sftp://localhost:22/
# 观察到错误:
# curl: option --ssh-knownhosts: is unknown


# 观察到错误:
# curl: option --hostpubsha256: the installed libcurl version does not support this

这些运行时观察结果与wolfSSH后端的代码相匹配,该代码未实现主机密钥验证或known_hosts集成。

代码引用

lib/vssh/wolfssh.c和lib/vssh/ssh.h中的代码引用(具体行数):

连接设置:无主机密钥验证回调或known_hosts加载,然后立即进入状态机。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
static CURLcode wssh_connect(struct Curl_easy *data, bool *done)
{
  struct connectdata *conn = data->conn;
  struct ssh_conn *sshc = Curl_conn_meta_get(conn, CURL_META_SSH_CONN);
  struct SSHPROTO *sshp = Curl_meta_get(data, CURL_META_SSH_EASY);
  curl_socket_t sock = conn->sock[FIRSTSOCKET];
  int rc;

  if(!sshc || !sshp)
    return CURLE_FAILED_INIT;

  /* We default to persistent connections. We set this already in this connect
     function to make the reuse checks properly be able to check this bit. */
  connkeep(conn, "SSH default");

  if(conn->handler->protocol & CURLPROTO_SCP) {
    conn->recv[FIRSTSOCKET] = wscp_recv;
    conn->send[FIRSTSOCKET] = wscp_send;
  }
  else {
    conn->recv[FIRSTSOCKET] = wsftp_recv;
    conn->send[FIRSTSOCKET] = wsftp_send;
  }
  sshc->ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL);
  if(!sshc->ctx) {
    failf(data, "No wolfSSH context");
    goto error;
  }

  sshc->ssh_session = wolfSSH_new(sshc->ctx);
  if(!sshc->ssh_session) {
    failf(data, "No wolfSSH session");
    goto error;
  }

  rc = wolfSSH_SetUsername(sshc->ssh_session, conn->user);
  if(rc != WS_SUCCESS) {
    failf(data, "wolfSSH failed to set username");
    goto error;
  }

  /* set callback for authentication */
  wolfSSH_SetUserAuth(sshc->ctx, userauth);
  wolfSSH_SetUserAuthCtx(sshc->ssh_session, data);

  rc = wolfSSH_set_fd(sshc->ssh_session, (int)sock);
  if(rc) {
    failf(data, "wolfSSH failed to set socket");
    goto error;
  }

#if 0
  wolfSSH_Debugging_ON();
#endif

  *done = TRUE;
  if(conn->handler->protocol & CURLPROTO_SCP)
    wssh_state(data, sshc, SSH_INIT);
  else
    wssh_state(data, sshc, SSH_SFTP_INIT);

  return wssh_multi_statemach(data, done);
error:
  wssh_sshc_cleanup(sshc);
  return CURLE_FAILED_INIT;
}

状态机:在wolfSSH_connect()成功后,直接转换到SSH_STOP,没有任何SSH_HOSTKEY验证步骤。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
case SSH_S_STARTUP:
  rc = wolfSSH_connect(sshc->ssh_session);
  if(rc != WS_SUCCESS)
    rc = wolfSSH_get_error(sshc->ssh_session);
  if(rc == WS_WANT_READ) {
    *block = TRUE;
    conn->waitfor = KEEP_RECV;
    return CURLE_OK;
  }
  else if(rc == WS_WANT_WRITE) {
    *block = TRUE;
    conn->waitfor = KEEP_SEND;
    return CURLE_OK;
  }
  else if(rc != WS_SUCCESS) {
    wssh_state(data, sshc, SSH_STOP);
    return CURLE_SSH;
  }
  infof(data, "wolfssh connected");
  wssh_state(data, sshc, SSH_STOP);
  break;

SSH_HOSTKEY状态存在于常见的SSH状态枚举中,但未被上述wolfSSH状态机使用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
typedef enum {
  SSH_NO_STATE = -1,  /* Used for "nextState" so say there is none */
  SSH_STOP = 0,       /* do nothing state, stops the state machine */

  SSH_INIT,           /* First state in SSH-CONNECT */
  SSH_S_STARTUP,      /* Session startup */
  SSH_HOSTKEY,        /* verify hostkey */
  SSH_AUTHLIST,
  SSH_AUTH_PKEY_INIT,
  SSH_AUTH_PKEY,
  SSH_AUTH_PASS_INIT,
  SSH_AUTH_PASS,
  SSH_AUTH_AGENT_INIT, /* initialize then wait for connection to agent */
  SSH_AUTH_AGENT_LIST, /* ask for list then wait for entire list to come */
  SSH_AUTH_AGENT,      /* attempt one key at a time */
  SSH_AUTH_HOST_INIT,
  SSH_AUTH_HOST,
  SSH_AUTH_KEY_INIT,
  SSH_AUTH_KEY,
  SSH_AUTH_GSSAPI,
  SSH_AUTH_DONE,
  SSH_SFTP_INIT,
  SSH_SFTP_REALPATH,   /* Last state in SSH-CONNECT */

支持材料/引用

本地wolfSSH构建的工具帮助和错误输出:

1
2
3
4
5
6
7
8
9
$ ./src/curl --help ssh
ssh: SSH protocol
     --compressed-ssh          Enable SSH compression
     --hostpubmd5 <md5>        Acceptable MD5 hash of host public key
     --hostpubsha256 <sha256>  Acceptable SHA256 hash of host public key
 -k, --insecure                Allow insecure server connections
     --key <key>               Private key filename
     --pass <phrase>           Passphrase for the private key
     --pubkey <key>            SSH Public key filename
1
2
$ ./src/curl --ssh-knownhosts /tmp/kh -vvv sftp://localhost:22/
curl: option --ssh-knownhosts: is unknown
1
2

curl: option --hostpubsha256: the installed libcurl version does not support this

这些观察结果,加上上述wolfSSH连接和状态机代码,表明此后端未实现主机身份验证。

影响

摘要

如果curl/libcurl使用wolfSSH后端构建并用于SFTP,则不会验证服务器主机身份。在使用wolfSSH后端的环境中,这允许对SSH/SFTP连接进行潜在的中间人攻击,因为客户端实际上接受任何呈现的服务器主机密钥(没有known_hosts或指纹强制执行,状态机中没有主机密钥验证步骤)。

时间线

  • 2025年9月23日,下午3:14 UTC:giant_anteater向curl提交报告
  • 2025年9月23日,下午4:35 UTC:bagder(curl工作人员)回复确认收到报告
  • 2025年9月24日,上午4:35 UTC:bagder确认问题正确,建议删除wolfSSH支持
  • 2025年9月24日,上午6:35 UTC:giant_anteater强调这是已发布构建中的具体安全缺陷
  • 2025年9月24日,下午8:57 UTC:bagder通过PR #18700删除了wolfSSH
  • 2025年9月25日,上午4:06 UTC:giant_anteater请求将此作为安全问题处理并分配CVE
  • 2025年9月25日,上午6:48 UTC:bagder将严重性从高调整为低,状态改为已分类
  • 2025年9月25日,上午11:12 UTC:更新CVE引用为CVE-2025-10966
  • 2天前:报告关闭,状态改为已解决,已发布

解决方案

curl项目已通过PR #18700删除了wolfSSH支持。虽然实际使用此后端的用户很少,但这仍然是一个安全漏洞,建议使用旧版本的用户避免启用wolfSSH的构建。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计