Skip to content

Commit c77eec4

Browse files
authored
Merge pull request ossec#2467 from atomicturtle/fix/ant-2026-pttdw7pw-ipv6-case
Give one spelling to an IPv6 address before ossec-execd blocks it
2 parents 20026d7 + e297cce commit c77eec4

7 files changed

Lines changed: 156 additions & 0 deletions

File tree

‎.github/workflows/make-multi-platform.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ jobs:
101101
./src/issue_c82ab02v_location_id
102102
make -C src -f tests/regressions/Makefile issue_see999rq_srcip_quote
103103
./src/issue_see999rq_srcip_quote
104+
make -C src -f tests/regressions/Makefile issue_pttdw7pw_ipv6_case
105+
./src/issue_pttdw7pw_ipv6_case
104106
make -C src -f tests/regressions/Makefile issue_rxknzj1n_smtp_dot_escape
105107
./src/issue_rxknzj1n_smtp_dot_escape
106108
make -C src -f tests/regressions/Makefile issue_fbj3la25_rids_counter_reload

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Unrelease 4.5.0 changes
2929

3030
**Security Fixes**
3131

32+
- @atomicturtle - Give ``ossec-execd`` one spelling of a literal address before it compares, stores, or runs a block. ``2001:DB8::7`` and ``2001:db8::7`` were two timeouts, so ``pf.sh`` deleted the one PF entry at the first timeout while execd still counted the other spelling as blocked. Later alerts then skipped the add. A ``::ffff:`` address is handed to the script as its IPv4 form, which is what analysisd already does. Host names, ``-``, CIDR, and a leading ``!`` are unchanged (ANT-2026-PTTDW7PW)
3233
- @atomicturtle - Keep the closing quote when ``ossec-dbd`` stores a long ``srcip`` or ``dstip``. ``sql_nullable_str()`` wrote the address into a 49-byte buffer, so 47 characters or more cut off the quote and both MySQL and PostgreSQL rejected the ``INSERT``. The address is now cut to the 46-character column width and the alert is stored. The full log line is unchanged (ANT-2026-SEE999RQ)
3334
- @atomicturtle - Widen the MySQL location id from ``SMALLINT UNSIGNED`` to ``INT UNSIGNED``. ``location.id`` stopped at 65,535, so one agent sending a new location per alert filled the counter and ``ossec-dbd`` then dropped every alert whose location was not already stored. Existing databases stay 16-bit until ``upgrade-location-id-mysql.sql`` is applied. PostgreSQL already uses ``SERIAL`` (ANT-2026-C82AB02V)
3435
- @atomicturtle - Do not follow a symlink when ``ossec-dbd`` reads ``queue/agent-info``. That directory belongs to ``ossecr`` and ``ossec-dbd`` runs as root, so ``stat`` and ``fopen`` copied the first line of the link target into the agent table, including a root-only file. A keepalive is stored only when it is one regular file (ANT-2026-5Z4OX04E)

‎src/os_execd/execd.c‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,43 @@ void FreeTimeoutEntry(timeout_data *timeout_entry)
203203
free(timeout_entry);
204204
}
205205

206+
/* One spelling for a literal address. Host names, "-", CIDR, and a
207+
* leading "!" are left for the caller to use unchanged. pf stores the
208+
* parsed address, so 2001:DB8::7 and 2001:db8::7 must not be two blocks.
209+
*/
210+
int CanonicalIP(const char *ip, char *dst, size_t dstlen)
211+
{
212+
os_ip parsed;
213+
char canon[INET6_ADDRSTRLEN];
214+
size_t n;
215+
216+
if (!ip || !dst || dstlen == 0) {
217+
return (0);
218+
}
219+
if (ip[0] == '\0' || ip[0] == '!' || strcmp(ip, "-") == 0 || strchr(ip, '/')) {
220+
return (0);
221+
}
222+
223+
memset(&parsed, 0, sizeof(parsed));
224+
if (OS_IsValidIP(ip, &parsed) != 1) {
225+
free(parsed.ip);
226+
return (0);
227+
}
228+
if (satop((struct sockaddr *)&parsed.ss, canon, sizeof(canon)) != 0 ||
229+
canon[0] == '\0') {
230+
free(parsed.ip);
231+
return (0);
232+
}
233+
free(parsed.ip);
234+
235+
n = strlen(canon);
236+
if (n + 1 > dstlen) {
237+
return (0);
238+
}
239+
memcpy(dst, canon, n + 1);
240+
return (1);
241+
}
242+
206243
#ifndef WIN32
207244

208245
/* Main function on the execd. Does all the data receiving, etc. */
@@ -391,6 +428,19 @@ static void ExecdStart(int q)
391428
i++;
392429
}
393430

431+
/* One spelling before the list compare, the repeated-offender
432+
* key, the add command, and the stored delete. */
433+
if (timeout_args[3]) {
434+
char canon[INET6_ADDRSTRLEN];
435+
436+
if (CanonicalIP(timeout_args[3], canon, sizeof(canon)) &&
437+
strcmp(timeout_args[3], canon) != 0) {
438+
free(timeout_args[3]);
439+
os_strdup(canon, timeout_args[3]);
440+
}
441+
cmd_args[3] = timeout_args[3];
442+
}
443+
394444
/* Check if this command was already executed (or incomplete). */
395445
added_before = ExecdTimeoutDupCheck(timeout_list, timeout_args,
396446
curr_time, repeated_hash);

‎src/os_execd/execd.h‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ typedef struct _timeout_data {
4949

5050
void FreeTimeoutEntry(timeout_data *timeout_entry);
5151

52+
/* 1 if ip is a literal address and dst holds its canonical text.
53+
* Host names, "-", CIDR, and a leading "!" return 0.
54+
*/
55+
int CanonicalIP(const char *ip, char *dst, size_t dstlen);
56+
5257
/* Returns 1 if request is incomplete or already on the timeout list. */
5358
int ExecdTimeoutDupCheck(OSList *timeout_list, char **timeout_args,
5459
time_t curr_time, OSHash *repeated_hash);

‎src/os_execd/win_execd.c‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ void WinExecdRun(char *exec_msg)
104104
char *cmd_user;
105105
char *cmd_ip;
106106
char buffer[OS_MAXSTR + 1];
107+
char canon[INET6_ADDRSTRLEN];
107108

108109
timeout_data *timeout_entry;
109110

@@ -147,6 +148,10 @@ void WinExecdRun(char *exec_msg)
147148
OS_WinExecdCleanArg(cmd_ip);
148149
OS_WinExecdCleanArg(tmp_msg);
149150

151+
if (CanonicalIP(cmd_ip, canon, sizeof(canon))) {
152+
cmd_ip = canon;
153+
}
154+
150155
/* Get the command to execute (valid name) */
151156
command = GetCommandbyName(name, &timeout_value);
152157
if (!command) {

‎src/tests/regressions/Makefile‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ REGRESSION_BINS = \
5858
issue_5z4ox04e_agent_info_symlink \
5959
issue_c82ab02v_location_id \
6060
issue_see999rq_srcip_quote \
61+
issue_pttdw7pw_ipv6_case \
6162
multiline_underflow \
6263
win_network_path \
6364
checksum_short_message \
@@ -1239,6 +1240,18 @@ issue_see999rq_srcip_quote: \
12391240
-Wl,--start-group shared.a os_regex.a -Wl,--end-group \
12401241
-lpcre2-8 -lm -lpthread -lssl -lcrypto
12411242

1243+
# ANT-2026-PTTDW7PW: two spellings of one IPv6 address are one block.
1244+
issue_pttdw7pw_ipv6_case: \
1245+
tests/regressions/issue_pttdw7pw_ipv6_case.c \
1246+
os_execd/execd.c shared.a
1247+
$(CC) $(CFLAGS) -Dmain=execd_main -c os_execd/execd.c -o os_execd/execd-ptt.o
1248+
$(CC) $(CFLAGS) -o $@ \
1249+
tests/regressions/issue_pttdw7pw_ipv6_case.c \
1250+
os_execd/execd-ptt.o \
1251+
os_execd/exec.c os_execd/config.c os_execd/timeout_dup.c \
1252+
-Wl,--start-group shared.a os_regex.a os_net.a os_xml.a os_crypto.a -Wl,--end-group \
1253+
-lpcre2-8 -lm -lpthread -lssl -lcrypto
1254+
12421255
# ANT-2026-5Z4OX04E: ossec-dbd must not follow an agent-info symlink.
12431256
# KEYS_FILE and AGENTINFO_DIR point at the harness tree.
12441257
issue_5z4ox04e_agent_info_symlink: \
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Regression: ossec-execd treats two IPv6 spellings as two blocks
3+
*
4+
* ANT-2026-PTTDW7PW / CWE-178
5+
*
6+
* ExecdStart() compares the srcip text with strcmp(). 2001:DB8::7 and
7+
* 2001:db8::7 are one address to pf, so the first timeout deletes the
8+
* table entry while execd still counts the other spelling as blocked.
9+
* A literal address must have one spelling before it is compared,
10+
* stored, or handed to the script. Host names, "-", CIDR, and a
11+
* leading "!" stay as written.
12+
*
13+
* Build (from src/):
14+
* make -f tests/regressions/Makefile issue_pttdw7pw_ipv6_case
15+
*
16+
* Run:
17+
* ./issue_pttdw7pw_ipv6_case
18+
*/
19+
20+
#include <stdio.h>
21+
#include <string.h>
22+
23+
#include "shared.h"
24+
#include "os_execd/execd.h"
25+
26+
static int failures = 0;
27+
28+
static void expect(int cond, const char *msg)
29+
{
30+
if (cond) {
31+
printf(" ok: %s\n", msg);
32+
} else {
33+
printf(" FAIL: %s\n", msg);
34+
failures++;
35+
}
36+
}
37+
38+
static void same_spelling(const char *a, const char *b, const char *want, const char *msg)
39+
{
40+
char ca[64];
41+
char cb[64];
42+
int ra;
43+
int rb;
44+
45+
ca[0] = '\0';
46+
cb[0] = '\0';
47+
ra = CanonicalIP(a, ca, sizeof(ca));
48+
rb = CanonicalIP(b, cb, sizeof(cb));
49+
expect(ra == 1 && rb == 1 && strcmp(ca, cb) == 0 && strcmp(ca, want) == 0, msg);
50+
}
51+
52+
int main(void)
53+
{
54+
char dst[64];
55+
56+
printf("execd IPv6 address spelling\n");
57+
58+
same_spelling("2001:DB8::7", "2001:db8::7", "2001:db8::7",
59+
"IPv6 letter case is one spelling");
60+
same_spelling("::ffff:192.0.2.1", "192.0.2.1", "192.0.2.1",
61+
"an IPv4-mapped address is the IPv4 address");
62+
same_spelling("192.0.2.1", "192.0.2.1", "192.0.2.1",
63+
"an IPv4 address stays dotted decimal");
64+
65+
expect(CanonicalIP("mail.example", dst, sizeof(dst)) == 0,
66+
"a host name is left unchanged");
67+
expect(CanonicalIP("-", dst, sizeof(dst)) == 0,
68+
"a dash user placeholder is left unchanged");
69+
expect(CanonicalIP("2001:db8::/64", dst, sizeof(dst)) == 0,
70+
"a CIDR prefix is left unchanged");
71+
expect(CanonicalIP("!192.0.2.1", dst, sizeof(dst)) == 0,
72+
"a negated address is left unchanged");
73+
74+
if (failures) {
75+
printf("FAIL: %d IPv6 spelling check(s)\n", failures);
76+
return 1;
77+
}
78+
printf("ok\n");
79+
return 0;
80+
}

0 commit comments

Comments
 (0)