Skip to navigation Skip to main content Skip to footer

Samba _netr_ServerPasswordSet Expoitability Analysis

02 March 2015

By Matt Lewis

tl;dr

This is my analysis of the recent pre-auth Samba remote tracked by CVE-2015-0240[1]. It doesn’t appear to be very exploitable to me, but I’d love to be proven wrong.

Note that since the time when I originally did this analysis someone has released their own PoC and analysis [8] showing why they don’t think it’s exploitable on 32-bit.

Introduction

A new remote pre-auth vulnerability, CVE-2015-0240, was announced by Samba on February 23, 2015 This vulnerability obviously would be quite useful if it were exploitable, so I spent a bit of time looking at it. I spent most of my time looking at 4.0.24, however did test on unpatched 3.x as well just to be sure I was seeing similar results. This is a summary of my investigation of the issue on 64-bit and 32-bit and why I think it probably is unlikely to be very exploitable beyond a minor memory revelation. That said, I would love to be proven wrong; if you figure something out, please share!

I explain everything with 64-bit first and then follow up with a much shorter analysis on 32-bit, which is corroborates the analysis that has already been published [8].

Bug overview

The bug is an uninitialized pointer that will be freed in _netr_ServerPasswordSet(). This was decently explained by RedHat in their blog [2], however I’ll reiterate some info here. The _netr_ServerPasswordSet() function passes the address of its local creds variable to the netr_creds_server_step_check() function, without first initializing it to NULL:

NTSTATUS _netr_ServerPasswordSet(struct pipes_struct *p, struct netr_ServerPasswordSet *r) { NTSTATUS status = NT_STATUS_OK; int i; struct netlogon_creds_CredentialState *creds; […] status = netr_creds_server_step_check(p, p->mem_ctx, r->in.computer_name, r->in.credential, r->out.return_authenticator, creds); unbecome_root(); if (!NT_STATUS_IS_OK(status)) { […] TALLOC_FREE(creds); return status; }

The _netr_ServerPasswordSet() function expects netr_creds_server_step_check() to initialize it. You can see above that if the netr_creds_server_step_check() function fails, _netr_ServerPasswordSet() will call TALLOC_FREE() on the pointer. netr_creds_server_step_check() pretty much just calls schannel_check_required() or the schannel_check_creds_state() function.

static NTSTATUS netr_creds_server_step_check(struct pipes_struct *p,
					     TALLOC_CTX *mem_ctx,
					     const char *computer_name,
					     struct netr_Authenticator *received_authenticator,
					     struct netr_Authenticator *return_authenticator,
					     struct netlogon_creds_CredentialState **creds_out)
{
	NTSTATUS status;
	bool schannel_global_required = (lp_server_schannel() == true) ? true:false;
	struct loadparm_context *lp_ctx;

	if (schannel_global_required) {
		status = schannel_check_required( p->auth,
						 computer_name,
						 false, false);
		if (!NT_STATUS_IS_OK(status)) {
			return status;
		}
	}
[...]
	status = schannel_check_creds_state(mem_ctx, lp_ctx,
					    computer_name, received_authenticator,
					    return_authenticator, creds_out);
	talloc_unlink(mem_ctx, lp_ctx);
	return status;
}we'

Above you can see that if schannel_global_required is set, then schannel_check_required() is called. Let’s look at this first.

static NTSTATUS schannel_check_required(struct pipe_auth_data *auth_info,
					const char *computer_name,
					bool integrity, bool privacy)
{
	if (auth_info    auth_info->auth_type == DCERPC_AUTH_TYPE_SCHANNEL) {
		if (!privacy    !integrity) {
			return NT_STATUS_OK;
		}

		if ((!privacy    integrity)   
		    auth_info->auth_level == DCERPC_AUTH_LEVEL_INTEGRITY) {
			return NT_STATUS_OK;
		}

		if ((privacy || integrity)   
		    auth_info->auth_level == DCERPC_AUTH_LEVEL_PRIVACY) {
			return NT_STATUS_OK;
		}
	}

	/* test didn't pass */
	DEBUG(0, ("schannel_check_required: [%s] is not using schanneln",
		  computer_name));

	return NT_STATUS_ACCESS_DENIED;
}

So basically if you’ve bound anonymously to the endpoint, the auth_level variable will be set to DCERPC_AUTH_LEVEL_NONE, meaning you can cause the function to return. This will occur before creds is initialized, so will trigger the bug.

Alternately, as we saw the schannel_check_creds_state() will be called, which we would also like to fail early. What does schannel_check_creds_state() look like:

NTSTATUS schannel_check_creds_state(TALLOC_CTX *mem_ctx,
				    struct loadparm_context *lp_ctx,
				    const char *computer_name,
				    struct netr_Authenticator *received_authenticator,
				    struct netr_Authenticator *return_authenticator,
				    struct netlogon_creds_CredentialState **creds_out)
{
	TALLOC_CTX *tmpctx;
	struct tdb_wrap *tdb_sc;
	struct netlogon_creds_CredentialState *creds;
	NTSTATUS status;
	int ret;

[...]

	/* Because this is a shared structure (even across
	 * disconnects) we must update the database every time we
	 * update the structure */

	status = schannel_fetch_session_key_tdb(tdb_sc, tmpctx, 
						computer_name,  creds);
	if (!NT_STATUS_IS_OK(status)) {
		tdb_transaction_cancel(tdb_sc->tdb);
		goto done;
	}

Still before creds is initialized, the code calls schannel_fetch_session_key_tdb(), passing the computer_name parameter, which we fully control in our DCE-RPC packet. What’s important in our case is that it tries to fetch a key using the computer_name, which we can set to be zeroes (as RedHat noted), meaning it won’t find anything. This will cause the function to return early via the done label.

talloc_free(tmpctx);
	return status;
}

This status will not be NT_STATUS_OK, and therefore we hit the condition we want to.

Triggering

Please refer to [8] for a public PoC, which leverages Impact[3] and includes the structure prototypes it is missing natively. To figure out the structure definitions prior to a PoC being available, simply referencing the Microsoft [4] documentation will tell you what’s needed.

Uninitialized stack variable exploitation primer

As RedHat notes: “It may be possible to control the value of creds, by sending a number of specially-crafted packets.” The idea behind this is influencing the stack such that the uninitialized value is pre-populated with something that can be controlled.

This type of vulnerability has a typical way of being exploited. The idea is basically determine where on the stack the uninitialized variable is at the time of the vulnerable function call and then analyze other function call paths to see if any of them overlap. By isolating an overlapping stack frame where you control the data that is written, you have potential to provide arbitrary data in that location. One of the constraints to this scenario is that between the time you call the overlapping path and the time you trigger the vulnerability, no other stack frames can overlap. If an uncontrolled overlap occurs it makes exploitation extremely difficult, if not impossible, due to lack of control of the stack variable. This non-ideal situation essentially forces you to try to deal with any side effects of however the uncontrolled value is used.

For more background information about this type of vulnerability and approaches to exploitation check out the talk given by Halvar Flake in 2006 [5].

Quick and dirty stack depth analysis

If you just read Halvar’s talk, you might be disgusted about my approach as a warning. But it gets us some answers quickly. I’m using some simple GDB scripting in order to determine what’s touching the stack pointer of interest and when. This can be done fairly easily and samba being a forking daemon allows us to do a test run to figure out where on the stack the value we’re interested in actually is, and then watch how it’s used on subsequent executions.

First we want to compile a vulnerable version with debugging simples. This is easy using the configure options:

$ ./configure --enable-debug
$ make

Then we can run our compiled version like so:

# bin/smbd -s /etc/samba/smb.conf
# ps -ef | grep smbd
root     22599     1  0 18:40 ?        00:00:00 bin/smbd -s /etc/samba/smb.conf
root     22600 22599  0 18:40 ?        00:00:00 bin/smbd -s /etc/samba/smb.conf

We’ll want to debug the daemonized version of the executable, which you can identify by the fact that it has PID 1 as its parent. Samba will fork on new connections, so in order to debug the child that is dispatched to handle the request we can use some gdb options.

# gdb -q -p 22599
(gdb) break smbd_accept_connection
Breakpoint 1 at 0x7f2a0bf7c575: file ../source3/smbd/server.c, line 529.
(gdb) commands 1
Type commands for breakpoint(s) 1, one per line.
End with a line saying just "end".
> silent
> set follow-fork-mode child
> continue
> end
(gdb) break smbd_process
Breakpoint 2 at 0x7f2a0b4ce6a4: file ../source3/smbd/process.c, line 3356.
(gdb) commands 2
Type commands for breakpoint(s) 2, one per line.
End with a line saying just "end".
>silent
>set follow-fork-mode parent
>continue
>end
(gdb) b _netr_ServerPasswordSet
Breakpoint 3 at 0x7f2a0b412a58: file ../source3/rpc_server/netlogon/srv_netlog_nt.c, line 1263.
(gdb) continue

Basically once a connection is accepted we tell gdb to follow the child. The child then calls smbd_process() to handle the packet, at which point we don’t want to follow children anymore, so we change the setting back. We can also set a breakpoint on _netr_ServerPasswordSet(). Now we can run our trigger tool and record the stack address that the creds variable is located at, which will be consistent across all forked children.

(gdb) p  creds
$1 = (struct netlogon_creds_CredentialState **) 0x7fff7d712ca8

Once this packet is handled this smbd process will exit. So we’ll have to re-attach to do our stack analysis. All of the earlier commands can just be loaded into a script that we load on gdb startup to avoiding having to type them more than once.

So how do we actually trace accesses now that we know the stack address? I use the following script, which I just load using source