Skip to content

Commit 90b01ac

Browse files
CVE-2025-10230: s4/tests: check that wins hook sanitizes names
An smb.conf can contain a 'wins hook' parameter, which names a script to run when a WINS name is changed. The man page says The second argument is the NetBIOS name. If the name is not a legal name then the wins hook is not called. Legal names contain only letters, digits, hyphens, underscores and periods. but it turns out the legality check is not performed if the WINS server in question is the source4 nbt one. It is not expected that people will run this server, but they can. This is bad because the name is passed unescaped into a shell command line, allowing command injection. For this test we don't care whether the WINS server is returning an error code, just whether it is running the wins hook. The tests show it often runs the hook it shouldn't, though some characters are incidentally blocked because the name has to fit in a DN before it gets to the hook, and DNs have a few syntactic restrictions (e.g., blocking '<', '>', and ';'). The source3 WINS server that is used by Samba when not run as a DC is not affected and not here tested. BUG: https://bugzilla.samba.org/show_bug.cgi?id=15903 Signed-off-by: Douglas Bagnall <[email protected]> Reviewed-by: Gary Lockyer <[email protected]>
1 parent 2af8904 commit 90b01ac

File tree

5 files changed

+152
-3
lines changed

5 files changed

+152
-3
lines changed

python/samba/tests/usage.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
'lib/ldb/tests/python/api.py',
7474
'source4/selftest/tests.py',
7575
'buildtools/bin/waf',
76+
'testprogs/blackbox/wins_hook_test',
7677
'selftest/tap2subunit',
7778
'script/show_test_time',
7879
'source4/scripting/bin/subunitrun',
@@ -89,6 +90,7 @@
8990
'selftest/tap2subunit',
9091
'wintest/test-s3.py',
9192
'wintest/test-s4-howto.py',
93+
'testprogs/blackbox/wins_hook_test',
9294
}
9395

9496

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
samba4.nbt.wins.wins_bad_names

selftest/target/Samba4.pm

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1637,6 +1637,7 @@ sub provision_ad_dc_ntvfs($$$)
16371637
ldap server require strong auth = allow_sasl_without_tls_channel_bindings
16381638
raw NTLMv2 auth = yes
16391639
lsa over netlogon = yes
1640+
wins hook = $ENV{SRCDIR_ABS}/testprogs/blackbox/wins_hook_test
16401641
rpc server port = 1027
16411642
auth event notification = true
16421643
dsdb event notification = true

source4/torture/nbt/wins.c

Lines changed: 133 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@
3131
#include "torture/nbt/proto.h"
3232
#include "param/param.h"
3333

34+
/* rcode used when you don't want to check the rcode */
35+
#define WINS_TEST_RCODE_WE_DONT_CARE 255
36+
37+
3438
#define CHECK_VALUE(tctx, v, correct) \
3539
torture_assert_int_equal(tctx, v, correct, "Incorrect value")
3640

@@ -137,7 +141,9 @@ static bool nbt_test_wins_name(struct torture_context *tctx, const char *address
137141
address));
138142

139143
CHECK_STRING(tctx, io.out.wins_server, address);
140-
CHECK_VALUE(tctx, io.out.rcode, 0);
144+
if (register_rcode != WINS_TEST_RCODE_WE_DONT_CARE) {
145+
CHECK_VALUE(tctx, io.out.rcode, 0);
146+
}
141147

142148
torture_comment(tctx, "register the name correct address\n");
143149
name_register.in.name = *name;
@@ -185,7 +191,9 @@ static bool nbt_test_wins_name(struct torture_context *tctx, const char *address
185191
talloc_asprintf(tctx, "Bad response from %s for name register\n",
186192
address));
187193

188-
CHECK_VALUE(tctx, name_register.out.rcode, 0);
194+
if (register_rcode != WINS_TEST_RCODE_WE_DONT_CARE) {
195+
CHECK_VALUE(tctx, name_register.out.rcode, 0);
196+
}
189197
CHECK_STRING(tctx, name_register.out.reply_addr, myaddress);
190198
}
191199

@@ -203,7 +211,9 @@ static bool nbt_test_wins_name(struct torture_context *tctx, const char *address
203211
torture_assert_ntstatus_ok(tctx, status, talloc_asprintf(tctx, "Bad response from %s for name register", address));
204212

205213
CHECK_STRING(tctx, io.out.wins_server, address);
206-
CHECK_VALUE(tctx, io.out.rcode, register_rcode);
214+
if (register_rcode != WINS_TEST_RCODE_WE_DONT_CARE) {
215+
CHECK_VALUE(tctx, io.out.rcode, register_rcode);
216+
}
207217

208218
if (register_rcode != NBT_RCODE_OK) {
209219
return true;
@@ -532,6 +542,124 @@ static bool nbt_test_wins(struct torture_context *tctx)
532542
return ret;
533543
}
534544

545+
/*
546+
* Test that the WINS server does not call 'wins hook' when the name
547+
* contains dodgy characters.
548+
*/
549+
static bool nbt_test_wins_bad_names(struct torture_context *tctx)
550+
{
551+
const char *address = NULL;
552+
const char *wins_hook_file = NULL;
553+
bool ret = true;
554+
int err;
555+
bool ok;
556+
struct nbt_name name = {};
557+
size_t i, j;
558+
FILE *fh = NULL;
559+
560+
struct {
561+
const char *name;
562+
bool should_succeed;
563+
} test_cases[] = {
564+
{"NORMAL", true},
565+
{"|look|", false},
566+
{"look&true", false},
567+
{"look\\;false", false},
568+
{"&ls>foo", false}, /* already fails due to DN syntax */
569+
{"has spaces", false},
570+
{"hyphen-dot.0", true},
571+
};
572+
573+
wins_hook_file = talloc_asprintf(tctx, "%s/wins_hook_writes_here",
574+
getenv("SELFTEST_TMPDIR"));
575+
576+
if (!torture_nbt_get_name(tctx, &name, &address)) {
577+
return false;
578+
}
579+
580+
for (i = 0; i < ARRAY_SIZE(test_cases); i++) {
581+
err = unlink(wins_hook_file);
582+
if (err != 0 && errno != ENOENT) {
583+
/* we expect ENOENT, but nothing else */
584+
torture_comment(tctx,
585+
"unlink %zu of '%s' failed\n",
586+
i, wins_hook_file);
587+
}
588+
589+
name.name = test_cases[i].name;
590+
name.type = NBT_NAME_CLIENT;
591+
ok = nbt_test_wins_name(tctx, address,
592+
&name,
593+
NBT_NODE_H,
594+
true,
595+
WINS_TEST_RCODE_WE_DONT_CARE
596+
);
597+
if (ok == false) {
598+
/*
599+
* This happens when the name interferes with
600+
* the DN syntax when it is put in winsdb.
601+
*
602+
* The wins hook will not be reached.
603+
*/
604+
torture_comment(tctx, "tests for '%s' failed\n",
605+
name.name);
606+
}
607+
608+
/*
609+
* poll for the file being created by the wins hook.
610+
*/
611+
for (j = 0; j < 10; j++) {
612+
usleep(200000);
613+
fh = fopen(wins_hook_file, "r");
614+
if (fh != NULL) {
615+
break;
616+
}
617+
}
618+
619+
if (fh == NULL) {
620+
if (errno == ENOENT) {
621+
if (test_cases[i].should_succeed) {
622+
torture_comment(
623+
tctx,
624+
"wins hook for '%s' failed\n",
625+
test_cases[i].name);
626+
ret = false;
627+
}
628+
} else {
629+
torture_comment(
630+
tctx,
631+
"wins hook for '%s' unexpectedly failed with %d\n",
632+
test_cases[i].name,
633+
errno);
634+
ret = false;
635+
}
636+
} else {
637+
char readback[17] = {0};
638+
size_t n = fread(readback, 1, 16, fh);
639+
torture_comment(tctx,
640+
"wins hook wrote '%s' read '%.*s'\n",
641+
test_cases[i].name,
642+
(int)n, readback);
643+
644+
if (! test_cases[i].should_succeed) {
645+
torture_comment(tctx,
646+
"wins hook for '%s' should fail\n",
647+
test_cases[i].name);
648+
ret = false;
649+
}
650+
fclose(fh);
651+
}
652+
}
653+
err = unlink(wins_hook_file);
654+
if (err != 0 && errno != ENOENT) {
655+
torture_comment(tctx, "final unlink of '%s' failed\n",
656+
wins_hook_file);
657+
}
658+
torture_assert(tctx, ret, "wins hook failure\n");
659+
return ret;
660+
}
661+
662+
535663
/*
536664
test WINS operations
537665
*/
@@ -540,6 +668,8 @@ struct torture_suite *torture_nbt_wins(TALLOC_CTX *mem_ctx)
540668
struct torture_suite *suite = torture_suite_create(mem_ctx, "wins");
541669

542670
torture_suite_add_simple_test(suite, "wins", nbt_test_wins);
671+
torture_suite_add_simple_test(suite, "wins_bad_names",
672+
nbt_test_wins_bad_names);
543673

544674
return suite;
545675
}

testprogs/blackbox/wins_hook_test

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#!/usr/bin/python3
2+
3+
import os
4+
import sys
5+
6+
filename = f"{os.environ['SELFTEST_TMPDIR']}/wins_hook_writes_here"
7+
8+
f = open(filename, 'wb')
9+
10+
# Some names may truncate argv (e.g. '&'), for which we leave the file
11+
# empty.
12+
if len(sys.argv) > 2:
13+
f.write(os.fsencode(sys.argv[2]))
14+
15+
f.close()

0 commit comments

Comments
 (0)