Summary

An unprivileged local user may trigger a NULL dereference bug in Samba’s Winbind service leading to Denial of Service (DoS).

Product

Samba

Tested Version

samba-4.7.6+dfsg~ubuntu

Details

Issue 1: NULL dereference in winbindd_lookupsids_recv

Samba’s Winbind service exposes two UNIX sockets through which winbind client requests are received and answered.

One of these sockets handles unprivileged commands and the other handles privileged commands.

The unprivileged socket is available to unprivileged local users. In our Samba installation this unprivileged socket could be reached via /var/run/samba/winbindd/pipe and does not require any special permissions to open.

anticomputer@dc1:~$ ls -alrt /var/run/samba/winbindd
total 0
drwxr-xr-x 7 root root 440 Jul  8 22:11 ..
srwxrwxrwx 1 root root   0 Jul  8 22:11 pipe
drwxr-xr-x 2 root root  60 Jul  8 22:11 .
anticomputer@dc1:~$

This presents an interesting attack surface from an attacker perspective as the Winbind service generally runs with root privileges. The available Winbind commands on the unprivileged socket are enumerated in winbindd.c:async_nonpriv_table. Requests for these commands are supplied in the form of a user supplied (thus attacker controlled) winbind_request data structure.

One of the available unprivileged commands is WINBINDD_LOOKUPSIDS. This command’s incoming request handler is defined in winbindd_lookupsids.c:winbindd_lookupsids_send as follows:

struct tevent_req *winbindd_lookupsids_send(TALLOC_CTX *mem_ctx,
					    struct tevent_context *ev,
					   struct winbindd_cli_state *cli,
					   struct winbindd_request *request)
{
	struct tevent_req *req, *subreq;
	struct winbindd_lookupsids_state *state;

	req = tevent_req_create(mem_ctx, &state,
				struct winbindd_lookupsids_state);
	if (req == NULL) {
		return NULL;
	}

	DEBUG(3, ("lookupsids\n"));

	if (request->extra_len == 0) {
		tevent_req_done(req);
[1]
		return tevent_req_post(req, ev);
	}
	if (request->extra_data.data[request->extra_len-1] != '\0') {
		DEBUG(10, ("Got invalid sids list\n"));
		tevent_req_nterror(req, NT_STATUS_INVALID_PARAMETER);
		return tevent_req_post(req, ev);
	}
	if (!parse_sidlist(state, request->extra_data.data,
			   &state->sids, &state->num_sids)) {
		DEBUG(10, ("parse_sidlist failed\n"));
		tevent_req_nterror(req, NT_STATUS_INVALID_PARAMETER);
		return tevent_req_post(req, ev);
	}
[2]
	subreq = wb_lookupsids_send(state, ev, state->sids, state->num_sids);
	if (tevent_req_nomem(subreq, req)) {
		return tevent_req_post(req, ev);
	}
	tevent_req_set_callback(subreq, winbindd_lookupsids_done, req);
	return req;
}

We note that this command handler normally expects to receive an extra set of data tagged onto the client request, based on the request->extra_len variable. When this data is available, it is subsequently parsed via parse_sidlist and the result state for the client request is updated via wb_lookupsids_send at [2].

wb_lookupsids_send will lookup any domains associated to the parsed sid list and subsequently update the request result state to include those domain results via the state->domains pointer. This occurs via a call to wb_lookupsids_get_domain which will allocate memory to store any domain results and initialize the state->domains pointer accordingly.

However, if there is no extra data available in the request, i.e. request->extra_len is 0, or there are no sids available in the sid list, i.e. state->num_sids is 0, then the state->domains pointer is not explicitly initialized.

In the case where there is extra data (i.e. request->extra_len != 0) but the resulting state->num_sids remains 0 this is alleviated by the fact that wb_lookupsids_send allocates memory into state->res_domains, which is moved into state->domains via a call to talloc_move in wb_lookupsids_recv by way of the winbindd_lookupsids_done callback prior to any dereference of the state->domains pointer.

However, when request->extra_len is 0, this code path is never invoked and state->domains remains NULL.

The initial memory for the state structure is allocated via talloc_zero_size, which provides memory allocations that are initialized to zero. This ensures that even when state->domains and state->res_domains are not properly initialized, they will always be NULL.

After the initial request has been parsed and the request state has been updated with any pending results, result delivery for the WINBINDD_LOOKUPSIDS is handled by winbindd_lookupsids.c:winbindd_lookupsids_recv:

NTSTATUS winbindd_lookupsids_recv(struct tevent_req *req,
				  struct winbindd_response *response)
{
	struct winbindd_lookupsids_state *state = tevent_req_data(
		req, struct winbindd_lookupsids_state);
	NTSTATUS status;
	char *result;
	uint32_t i;

	if (tevent_req_is_nterror(req, &status)) {
		DEBUG(5, ("wb_lookupsids failed: %s\n", nt_errstr(status)));
		return status;
	}

[1]
	result = talloc_asprintf(response, "%d\n", (int)state->domains->count);
	if (result == NULL) {
		return NT_STATUS_NO_MEMORY;
	}

	for (i=0; i<state->domains->count; i++) {
		fstring sid_str;

		result = talloc_asprintf_append_buffer(
			result, "%s %s\n",
			sid_to_fstring(sid_str,
				       state->domains->domains[i].sid),
			state->domains->domains[i].name.string);
		if (result == NULL) {
			return NT_STATUS_NO_MEMORY;
		}
	}

	result = talloc_asprintf_append_buffer(
		result, "%d\n", (int)state->names->count);
	if (result == NULL) {
		return NT_STATUS_NO_MEMORY;
	}

	for (i=0; i<state->names->count; i++) {
		struct lsa_TranslatedName *name;

		name = &state->names->names[i];

		result = talloc_asprintf_append_buffer(
			result, "%d %d %s\n",
			(int)name->sid_index, (int)name->sid_type,
			name->name.string);
		if (result == NULL) {
			return NT_STATUS_NO_MEMORY;
		}
	}

	response->extra_data.data = result;
	response->length += talloc_get_size(result);
	return NT_STATUS_OK;
}

At [1] we then notice a dereference of state->domains, which means that there exists an opportunity to trigger a NULL dereference, as we can craft a result state in which state->domains remains unitialized by sending a WINBINDD_LOOKUPSIDS command with request->extra_len set to 0.

Doing so results in the following crash:

Program received signal SIGSEGV, Segmentation fault.
0x000055687ae13f61 in winbindd_lookupsids_recv (req=0x55687cc3ccd0,
    response=0x55687d3c5470) at ../source3/winbindd/winbindd_lookupsids.c:103
103             result = talloc_asprintf(response, "%d\n", (int)state->domains->cou
nt);
(gdb) x/i$pc
=> 0x55687ae13f61 <winbindd_lookupsids_recv+177>:       mov    (%rax),%edx
(gdb) i r rax
rax            0x0      0
(gdb) p state->domains
$5 = (struct lsa_RefDomainList *) 0x0
(gdb) bt
#0  0x0000561efefb6f61 in winbindd_lookupsids_recv (req=0x561f00f42930, response=0x561f018977d0)
    at ../source3/winbindd/winbindd_lookupsids.c:103
#1  0x0000561efef7d1dd in wb_request_done (req=0x561f00f42930)
    at ../source3/winbindd/winbindd.c:755
#2  0x00007facf6e191a4 in tevent_common_loop_immediate ()

On NULL dereference the Winbind service will segfault and its signal handling will then abort the process. This results in a Denial of Service against any functionality of the local system that depends on the Winbind service.

Impact

This issue may lead to Denial of Service.

Remediation

Ensure the state->domains pointer is verified to be initialized prior to any use in winbindd_lookupsids_recv.

CVE

Coordinated Disclosure Timeline

Resources

Credit

This issue was discovered and reported by GHSL team member @anticomputer (Bas Alberts).

Contact

You can contact the GHSL team at securitylab@github.com, please include a reference to GHSL-2020-134 in any communication regarding this issue.