✓ProveIt
Examples / Report #3470095 Saved analysis

Heap Buffer Over-Read via Malicious SMB Server READ_ANDX Response

curl · 2025-12-1843.1s analysis · $0.033 in model calls

What the report claims

A heap buffer over-read in libcurl's SMB implementation allows a malicious server to cause curl to read and output up to 28KB of uninitialized heap memory per request.

Compare with curl's recorded outcome

Labeled AI slop

curl's maintainer lists this report as AI slop

Shown for comparison after analysis. The pipeline did not see this label.

ProveIt's finding

Needs human review

The available evidence does not settle the report. A maintainer should investigate.

Why this finding?
  • The bounds check in SMB_DOWNLOAD (line 1121) correctly uses smbc->got as the limit, but smbc->got is set in smb_send_and_recv which is not provided.
  • If smb_send_and_recv sets smbc->got from the NetBIOS header length before fully receiving the message, or if the receive buffer is larger than smbc->got and not zeroed, the over-read could occur. The provided code does not settle this.
  • The report is detailed and the code location is accurate, but the critical receive-path logic is missing from the evidence.
What still needs checking
  • The smb_send_and_recv function implementation (how msg buffer is allocated, how smbc->got is updated, whether NetBIOS header length is trusted before full receive).
How this analysis ran 5 claims · 9 citations verified

Checked against curl master on 2025-12-18 (d7bde803ee83). Dev evaluation split. Citation verification checks that a referenced line was in the evidence; it does not prove the model interpreted it correctly.

1 · Grounding
partially grounded
no model · 11 references checked out, 2 did not
2 · Claims
NVIDIA-Nemotron-3-Nano-30B-A3B
5 claims · 3123 tokens · $0.0003
3 · Evidence
6 code snippets
real curl source at d7bde803ee83 · 3 docs via Tavily
4 · Judgment
Nemotron-3-Ultra-550b-a55b
18547 tokens · 32.6s · $0.0324
5 · Receipts
9 of 9 citations verified
every citation must point at code the judge was shown
Review a draft reply to the reporter

Edit and verify before sending.

Thank you for the detailed report. The SMB_DOWNLOAD bounds check (lib/smb.c:1121) uses smbc->got as the limit, but we need to see smb_send_and_recv to verify whether an attacker can inflate smbc->got via the NetBIOS header while the receive buffer msg contains uninitialized heap memory. Could you point to that function or explain how smbc->got is updated relative to the actual bytes received?

Inspect all claims and source code 5 claims · 6 code excerpts

Report claims

C1flaw not verifiable

A malicious SMB server can send a crafted READ_ANDX response that causes curl to read and output up to 28KB of uninitialized heap memory per request.

Read the reasoning

The claim depends on whether smbc->got can be made larger than the actual valid data in the receive buffer msg. The provided code shows the bounds check (line 1121) and the read (line 1126), but the critical function smb_send_and_recv that populates msg and sets smbc->got is not in the provided source. Without it, we cannot confirm whether an attacker can inflate smbc->got (e.g., via NetBIOS header length) while msg contains uninitialized heap memory beyond the bytes actually received.

C2location supported

The vulnerability exists in lib/smb.c within the smb_request_state() function, specifically in the SMB_DOWNLOAD state handler at lines 1110-1137.

Read the reasoning

The vulnerable code location matches exactly: smb_request_state function, SMB_DOWNLOAD case at lines 1110-1137 in the provided lib/smb.c source.

C3trigger not verifiable

A malicious SMB server sends a specially crafted READ_ANDX response that claims a large message size and pads with null bytes to match the claimed size.

Read the reasoning

The trigger describes attacker control of NetBIOS header length and padding to manipulate smbc->got. This logic resides in smb_send_and_recv, which is not in the provided code. Cannot verify from given source.

C4impact not verifiable

curl outputs approximately 28KB of leaked heap memory when the server only sent 256 bytes of legitimate data.

Read the reasoning

The 28KB figure depends on C1 being true (that len can be large while reading uninitialized memory). Since C1 is not verifiable, the impact magnitude cannot be confirmed from provided code.

C5reproduction not verifiable

The PoC demonstrates that a server sending 256 bytes of data results in curl writing 28,672 bytes to a file, with the extra bytes being heap memory.

Read the reasoning

The PoC scripts (run_exploit.sh, smb_exploit_server.py) are not included in the provided context. Cannot verify reproduction claims.

Deterministic checks that failed

  • file lib/smb.c existed in curl's history but not at bc677a8f16a5
  • file lib/smb.c existed in curl's history but not at bc677a8f16a5
Read the original report (excerpt)

Full report on HackerOne ↗

================================================================================ DESCRIPTION: ================================================================================ ## Summary: I discovered a heap buffer over-read vulnerability in libcurl's SMB protocol implementation. A malicious SMB server can send a specially crafted READ_ANDX response that causes curl to read and output up to 28KB of uninitialized heap memory per request. This results in information disclosure to the attacker. The vulnerability exists in lib/smb.c within the smb_request_state() function, specifically in the SMB_DOWNLOAD state handler at lines 1110-1137. No AI was used to discover this vulnerability. AI was used only to assist with report formatting. ## Technical Analysis: **Vulnerable Code (lib/smb.c lines 1116-1128):** ```c case SMB_DOWNLOAD: if(h->status || smbc->got < sizeof(struct smb_header) + 15) { req->result = CURLE_RECV_ERROR; next_state = SMB_CLOSE; break; } // [1] Values read directly from attacker-controlled server response len = Curl_read16_le(((const unsigned char *)msg) + sizeof(struct smb_header) + 11); off = Curl_read16_le(((const unsigned char *)msg) + sizeof(struct smb_header) + 13); if(len > 0) { // [2] Bounds check validates against smbc->got (total received bytes) if(off + sizeof(unsigned int) + len > smbc->got) { failf(data, "Invalid input packet"); result = CURLE_RECV_ERROR; } else // [3] Out-of-bounds read occurs here - reads heap memory result = Curl_client_write(data, CLIENTWRITE_BODY, (char *)msg + off + sizeof(unsigned int), len); ``` **Root Cause:** 1. The attacker controls the NetBIOS header length field, allowing them to claim a large message size (e.g., 0x7100 bytes) 2. The attacker pads the response with null bytes to make smbc->got match the claimed size 3. The attacker sets data_offset and data_length to read from any position within the receive buffer 4. The bounds check at [2] passes because off + 4 + len <= smbc->got 5. curl reads len bytes from msg + off + 4, which includes uninitialized heap memory beyond the actual SMB response data **Result:** curl outputs ~28KB when the serv …

Source code

lib/smb.clines 1023–1183 · lines 1116-1128 cited by the report; definition of smb_request_state (truncated)
1023static CURLcode smb_request_state(struct Curl_easy *data, bool *done)
1024{
1025 struct connectdata *conn = data->conn;
1026 struct smb_conn *smbc = Curl_conn_meta_get(conn, CURL_META_SMB_CONN);
1027 struct smb_request *req = Curl_meta_get(data, CURL_META_SMB_EASY);
1028 struct smb_header *h;
1029 enum smb_req_state next_state = SMB_DONE;
1030 unsigned short len;
1031 unsigned short off;
1032 CURLcode result;
1033 void *msg = NULL;
1034 const struct smb_nt_create_response *smb_m;
1035
1036 if(!smbc || !req)
1037 return CURLE_FAILED_INIT;
1038
1039 if(data->state.upload && (data->state.infilesize < 0)) {
1040 failf(data, "SMB upload needs to know the size up front");
1041 return CURLE_SEND_ERROR;
1042 }
1043
1044 /* Start the request */
1045 if(req->state == SMB_REQUESTING) {
1046 result = smb_send_tree_connect(data, smbc, req);
1047 if(result) {
1048 connclose(conn, "SMB: failed to send tree connect message");
1049 return result;
1050 }
1051
1052 request_state(data, SMB_TREE_CONNECT);
1053 }
1054
1055 /* Send the previous message and check for a response */
1056 result = smb_send_and_recv(data, smbc, &msg);
1057 if(result && result != CURLE_AGAIN) {
1058 connclose(conn, "SMB: failed to communicate");
1059 return result;
1060 }
1061
1062 if(!msg)
1063 return CURLE_OK;
C3: smb_send_and_recv called to receive msg; NetBIOS header parsing and smbc->got update happen there (not provided)
1064
1065 h = msg;
1066
1067 switch(req->state) {
1068 case SMB_TREE_CONNECT:
1069 if(h->status) {
1070 req->result = CURLE_REMOTE_FILE_NOT_FOUND;
1071 if(h->status == smb_swap32(SMB_ERR_NOACCESS))
1072 req->result = CURLE_REMOTE_ACCESS_DENIED;
1073 break;
1074 }
1075 req->tid = smb_swap16(h->tid);
1076 next_state = SMB_OPEN;
1077 break;
1078
1079 case SMB_OPEN:
1080 if(h->status || smbc->got < sizeof(struct smb_nt_create_response)) {
1081 req->result = CURLE_REMOTE_FILE_NOT_FOUND;
1082 if(h->status == smb_swap32(SMB_ERR_NOACCESS))
1083 req->result = CURLE_REMOTE_ACCESS_DENIED;
1084 next_state = SMB_TREE_DISCONNECT;
1085 break;
1086 }
1087 smb_m = (const struct smb_nt_create_response *)msg;
1088 req->fid = smb_swap16(smb_m->fid);
1089 data->req.offset = 0;
1090 if(data->state.upload) {
1091 data->req.size = data->state.infilesize;
1092 Curl_pgrsSetUploadSize(data, data->req.size);
1093 next_state = SMB_UPLOAD;
1094 }
1095 else {
1096 data->req.size = smb_swap64(smb_m->end_of_file);
1097 if(data->req.size < 0) {
1098 req->result = CURLE_WEIRD_SERVER_REPLY;
1099 next_state = SMB_CLOSE;
1100 }
1101 else {
1102 Curl_pgrsSetDownloadSize(data, data->req.size);
1103 if(data->set.get_filetime)
1104 get_posix_time(&data->info.filetime, smb_m->last_change_time);
1105 next_state = SMB_DOWNLOAD;
1106 }
1107 }
1108 break;
1109
1110 case SMB_DOWNLOAD:
C2: SMB_DOWNLOAD case starts at line 1110
1111 if(h->status || smbc->got < sizeof(struct smb_header) + 15) {
1112 req->result = CURLE_RECV_ERROR;
1113 next_state = SMB_CLOSE;
1114 break;
1115 }
1116 len = Curl_read16_le(((const unsigned char *)msg) +
C2: len read from offset sizeof(struct smb_header) + 11
1117 sizeof(struct smb_header) + 11);
1118 off = Curl_read16_le(((const unsigned char *)msg) +
C2: off read from offset sizeof(struct smb_header) + 13
1119 sizeof(struct smb_header) + 13);
1120 if(len > 0) {
1121 if(off + sizeof(unsigned int) + len > smbc->got) {
C1: Bounds check uses smbc->got as the limit, but smbc->got is set in smb_send_and_recv (not provided). Cannot verify if smbc->got can exceed actual initialized data in msg.
C2: Bounds check against smbc->got
1122 failf(data, "Invalid input packet");
1123 result = CURLE_RECV_ERROR;
1124 }
1125 else
1126 result = Curl_client_write(data, CLIENTWRITE_BODY,
C1: Read uses msg + off + 4 with length len, bounded by smbc->got per line 1121. If smbc->got accurately reflects received bytes, no over-read occurs.
C2: Curl_client_write call with computed pointer and len
C4: Output length is len, which comes from attacker-controlled response at line 1116
1127 (char *)msg + off + sizeof(unsigned int),
1128 len);
1129 if(result) {
1130 req->result = result;
1131 next_state = SMB_CLOSE;
1132 break;
1133 }
1134 }
1135 data->req.offset += len;
1136 next_state = (len < MAX_PAYLOAD_SIZE) ? SMB_CLOSE : SMB_DOWNLOAD;
1137 break;
1138
1139 case SMB_UPLOAD:
1140 if(h->status || smbc->got < sizeof(struct smb_header) + 7) {
1141 req->result = CURLE_UPLOAD_FAILED;
1142 next_state = SMB_CLOSE;
1143 break;
1144 }
1145 len = Curl_read16_le(((const unsigned char *)msg) +
1146 sizeof(struct smb_header) + 5);
1147 data->req.bytecount += len;
1148 data->req.offset += len;
1149 Curl_pgrs_upload_inc(data, len);
1150 if(data->req.bytecount >= data->req.size)
1151 next_state = SMB_CLOSE;
1152 else
1153 next_state = SMB_UPLOAD;
1154 break;
1155
1156 case SMB_CLOSE:
1157 /* We do not care if the close failed, proceed to tree disconnect anyway */
1158 next_state = SMB_TREE_DISCONNECT;
1159 break;
1160
1161 case SMB_TREE_DISCONNECT:
1162 next_state = SMB_DONE;
1163 break;
1164
1165 default:
1166 smb_pop_message(smbc);
1167 return CURLE_OK; /* ignore */
1168 }
1169
1170 smb_pop_message(smbc);
1171
1172 switch(next_state) {
1173 case SMB_OPEN:
1174 result = smb_send_open(data, smbc, req);
1175 break;
1176
1177 case SMB_DOWNLOAD:
1178 result = smb_send_read(data, smbc, req);
1179 break;
1180
1181 case SMB_UPLOAD:
1182 result = smb_send_write(data, smbc, req);
1183 break;
lib/curl_endian.clines 42–46 · definition of Curl_read16_le
42unsigned short Curl_read16_le(const unsigned char *buf)
43{
44 return (unsigned short)(((unsigned short)buf[0]) |
45 ((unsigned short)buf[1] << 8));
46}
lib/sendf.clines 66–92 · definition of Curl_client_write
Show 27 lines (not cited by the judge)
66CURLcode Curl_client_write(struct Curl_easy *data,
67 int type, const char *buf, size_t blen)
68{
69 CURLcode result;
70
71 /* it is one of those, at least */
72 DEBUGASSERT(type &
73 (CLIENTWRITE_BODY | CLIENTWRITE_HEADER | CLIENTWRITE_INFO));
74 /* BODY is only BODY (with optional EOS) */
75 DEBUGASSERT(!(type & CLIENTWRITE_BODY) ||
76 ((type & ~(CLIENTWRITE_BODY | CLIENTWRITE_EOS)) == 0));
77 /* INFO is only INFO (with optional EOS) */
78 DEBUGASSERT(!(type & CLIENTWRITE_INFO) ||
79 ((type & ~(CLIENTWRITE_INFO | CLIENTWRITE_EOS)) == 0));
80
81 if(!data->req.writer_stack) {
82 result = do_init_writer_stack(data);
83 if(result)
84 return result;
85 DEBUGASSERT(data->req.writer_stack);
86 }
87
88 result = Curl_cwriter_write(data, data->req.writer_stack, type, buf, blen);
89 CURL_TRC_WRITE(data, "client_write(type=%x, len=%zu) -> %d",
90 type, blen, result);
91 return result;
92}
lib/sendf.c@curl-8_17_0lines 70–95 · definition of Curl_client_write as in release curl-8_17_0; differs from master; commits that changed it since: c3b030b860 lib: fix formatting nits (part 3)
Show 26 lines (not cited by the judge)
70CURLcode Curl_client_write(struct Curl_easy *data,
71 int type, const char *buf, size_t blen)
72{
73 CURLcode result;
74
75 /* it is one of those, at least */
76 DEBUGASSERT(type & (CLIENTWRITE_BODY|CLIENTWRITE_HEADER|CLIENTWRITE_INFO));
77 /* BODY is only BODY (with optional EOS) */
78 DEBUGASSERT(!(type & CLIENTWRITE_BODY) ||
79 ((type & ~(CLIENTWRITE_BODY|CLIENTWRITE_EOS)) == 0));
80 /* INFO is only INFO (with optional EOS) */
81 DEBUGASSERT(!(type & CLIENTWRITE_INFO) ||
82 ((type & ~(CLIENTWRITE_INFO|CLIENTWRITE_EOS)) == 0));
83
84 if(!data->req.writer_stack) {
85 result = do_init_writer_stack(data);
86 if(result)
87 return result;
88 DEBUGASSERT(data->req.writer_stack);
89 }
90
91 result = Curl_cwriter_write(data, data->req.writer_stack, type, buf, blen);
92 CURL_TRC_WRITE(data, "client_write(type=%x, len=%zu) -> %d",
93 type, blen, result);
94 return result;
95}
lib/smb.c@curl-8_17_0lines 1030–1190 · definition of smb_request_state as in release curl-8_17_0; differs from master
Show 161 lines (not cited by the judge)
1030static CURLcode smb_request_state(struct Curl_easy *data, bool *done)
1031{
1032 struct connectdata *conn = data->conn;
1033 struct smb_conn *smbc = Curl_conn_meta_get(conn, CURL_META_SMB_CONN);
1034 struct smb_request *req = Curl_meta_get(data, CURL_META_SMB_EASY);
1035 struct smb_header *h;
1036 enum smb_req_state next_state = SMB_DONE;
1037 unsigned short len;
1038 unsigned short off;
1039 CURLcode result;
1040 void *msg = NULL;
1041 const struct smb_nt_create_response *smb_m;
1042
1043 if(!smbc || !req)
1044 return CURLE_FAILED_INIT;
1045
1046 if(data->state.upload && (data->state.infilesize < 0)) {
1047 failf(data, "SMB upload needs to know the size up front");
1048 return CURLE_SEND_ERROR;
1049 }
1050
1051 /* Start the request */
1052 if(req->state == SMB_REQUESTING) {
1053 result = smb_send_tree_connect(data, smbc, req);
1054 if(result) {
1055 connclose(conn, "SMB: failed to send tree connect message");
1056 return result;
1057 }
1058
1059 request_state(data, SMB_TREE_CONNECT);
1060 }
1061
1062 /* Send the previous message and check for a response */
1063 result = smb_send_and_recv(data, smbc, &msg);
1064 if(result && result != CURLE_AGAIN) {
1065 connclose(conn, "SMB: failed to communicate");
1066 return result;
1067 }
1068
1069 if(!msg)
1070 return CURLE_OK;
1071
1072 h = msg;
1073
1074 switch(req->state) {
1075 case SMB_TREE_CONNECT:
1076 if(h->status) {
1077 req->result = CURLE_REMOTE_FILE_NOT_FOUND;
1078 if(h->status == smb_swap32(SMB_ERR_NOACCESS))
1079 req->result = CURLE_REMOTE_ACCESS_DENIED;
1080 break;
1081 }
1082 req->tid = smb_swap16(h->tid);
1083 next_state = SMB_OPEN;
1084 break;
1085
1086 case SMB_OPEN:
1087 if(h->status || smbc->got < sizeof(struct smb_nt_create_response)) {
1088 req->result = CURLE_REMOTE_FILE_NOT_FOUND;
1089 if(h->status == smb_swap32(SMB_ERR_NOACCESS))
1090 req->result = CURLE_REMOTE_ACCESS_DENIED;
1091 next_state = SMB_TREE_DISCONNECT;
1092 break;
1093 }
1094 smb_m = (const struct smb_nt_create_response*) msg;
1095 req->fid = smb_swap16(smb_m->fid);
1096 data->req.offset = 0;
1097 if(data->state.upload) {
1098 data->req.size = data->state.infilesize;
1099 Curl_pgrsSetUploadSize(data, data->req.size);
1100 next_state = SMB_UPLOAD;
1101 }
1102 else {
1103 data->req.size = smb_swap64(smb_m->end_of_file);
1104 if(data->req.size < 0) {
1105 req->result = CURLE_WEIRD_SERVER_REPLY;
1106 next_state = SMB_CLOSE;
1107 }
1108 else {
1109 Curl_pgrsSetDownloadSize(data, data->req.size);
1110 if(data->set.get_filetime)
1111 get_posix_time(&data->info.filetime, smb_m->last_change_time);
1112 next_state = SMB_DOWNLOAD;
1113 }
1114 }
1115 break;
1116
1117 case SMB_DOWNLOAD:
1118 if(h->status || smbc->got < sizeof(struct smb_header) + 15) {
1119 req->result = CURLE_RECV_ERROR;
1120 next_state = SMB_CLOSE;
1121 break;
1122 }
1123 len = Curl_read16_le(((const unsigned char *) msg) +
1124 sizeof(struct smb_header) + 11);
1125 off = Curl_read16_le(((const unsigned char *) msg) +
1126 sizeof(struct smb_header) + 13);
1127 if(len > 0) {
1128 if(off + sizeof(unsigned int) + len > smbc->got) {
1129 failf(data, "Invalid input packet");
1130 result = CURLE_RECV_ERROR;
1131 }
1132 else
1133 result = Curl_client_write(data, CLIENTWRITE_BODY,
1134 (char *)msg + off + sizeof(unsigned int),
1135 len);
1136 if(result) {
1137 req->result = result;
1138 next_state = SMB_CLOSE;
1139 break;
1140 }
1141 }
1142 data->req.offset += len;
1143 next_state = (len < MAX_PAYLOAD_SIZE) ? SMB_CLOSE : SMB_DOWNLOAD;
1144 break;
1145
1146 case SMB_UPLOAD:
1147 if(h->status || smbc->got < sizeof(struct smb_header) + 7) {
1148 req->result = CURLE_UPLOAD_FAILED;
1149 next_state = SMB_CLOSE;
1150 break;
1151 }
1152 len = Curl_read16_le(((const unsigned char *) msg) +
1153 sizeof(struct smb_header) + 5);
1154 data->req.bytecount += len;
1155 data->req.offset += len;
1156 Curl_pgrsSetUploadCounter(data, data->req.bytecount);
1157 if(data->req.bytecount >= data->req.size)
1158 next_state = SMB_CLOSE;
1159 else
1160 next_state = SMB_UPLOAD;
1161 break;
1162
1163 case SMB_CLOSE:
1164 /* We do not care if the close failed, proceed to tree disconnect anyway */
1165 next_state = SMB_TREE_DISCONNECT;
1166 break;
1167
1168 case SMB_TREE_DISCONNECT:
1169 next_state = SMB_DONE;
1170 break;
1171
1172 default:
1173 smb_pop_message(smbc);
1174 return CURLE_OK; /* ignore */
1175 }
1176
1177 smb_pop_message(smbc);
1178
1179 switch(next_state) {
1180 case SMB_OPEN:
1181 result = smb_send_open(data, smbc, req);
1182 break;
1183
1184 case SMB_DOWNLOAD:
1185 result = smb_send_read(data, smbc, req);
1186 break;
1187
1188 case SMB_UPLOAD:
1189 result = smb_send_write(data, smbc, req);
1190 break;
lib/curl_trc.hlines 63–63 · definition of failf
Show 1 lines (not cited by the judge)
63#define failf Curl_failf