Wednesday, March 07, 2012

NTLMv2 Client-side

In my last post I had successfully used the Http "Negotiate" authentication mechanism to send a Kerberos ticket, which allowed me to login to the domain as my user to access a protected resource. However, to do that I had to enable the AllowTgtSessionKey registry entry in Windows 7, which weakened the security posture.

I fiddled with the JRE's implementation of NtlmV2, which we have to use since our lmcompatibility level will be 4, but could not get it to work for some reason. I tried tracking that down but gave up. The only other solution was to go native. For this, there is a package called JNA.

There is a some coverage on the internet of server-based NTLMv2 using JNA (Waffle), but it didn't touch much on client connections. I tried modifying some of Waffle's code to do what I wanted, but it seemed like the variables were all set up to use Negotiate, and it wasn't really getting me anywhere.

I realize this isn't the prettiest code, but it's what I finally came up with, and works for authenticating via NTLMv2 with default credentials (SSO, no username/password). I achieved this by investigating more of what the JRE was doing in NTLMAuthSequence.c as well as another project (JavaSVN), both of which I used to reverse engineer the below client solution.

This does not maintain a connection, it simply performs a one-time handshake, sending all 3 NTLM message types for NTLMv2, and retrieves a cookie value used in later web service calls...


private HttpCookie doWin7NTLM() throws IOException, GSSException, EncryptorException, NTLMEngineException {
String securityPackage = "NTLM";
// client credentials handle
DefaultHttpClient httpclient;
Sspi.CredHandle cred = new Sspi.CredHandle();
Sspi.TimeStamp ltime = new Sspi.TimeStamp();
Sspi.CtxtHandle ctx = new Sspi.CtxtHandle();

// Grab the initial default client credentials handle
int rc = Secur32.INSTANCE.AcquireCredentialsHandle(null,
securityPackage, new NativeLong(Sspi.SECPKG_CRED_OUTBOUND), null,
Pointer.NULL, null, null,
cred, ltime);
if (rc != W32Errors.SEC_I_CONTINUE_NEEDED && rc != W32Errors.SEC_E_OK){
throw new Win32Exception(rc);
}

Sspi.SecBufferDesc outBuffDesc = new Sspi.SecBufferDesc();
Sspi.SecBuffer.ByReference outBuff = new Sspi.SecBuffer.ByReference();
outBuff.cbBuffer = new NativeLong(512);
outBuff.BufferType = new NativeLong(Sspi.SECBUFFER_TOKEN);
outBuff.pvBuffer = new Memory(512);
outBuff.write();
outBuffDesc.ulVersion = new NativeLong(0);
outBuffDesc.cBuffers = new NativeLong(1);
outBuffDesc.pBuffers = (Sspi.SecBuffer.ByReference[])outBuff.toArray(1);
outBuffDesc.write();
NativeLongByReference attr = new NativeLongByReference();

// Use the default credentials handle to generate an NTLMv2 Type 1 Message (just workstation/domain name)
rc = Secur32.INSTANCE.InitializeSecurityContext(cred, null,
null, new NativeLong(0), new NativeLong(0),
new NativeLong(Sspi.SECURITY_NATIVE_DREP), null, new NativeLong(0), ctx, outBuffDesc,
attr, null);
if (rc != W32Errors.SEC_I_CONTINUE_NEEDED && rc != W32Errors.SEC_E_OK){
throw new Win32Exception(rc);
}

// Read buffer containing NTLM Type 1 message data
byte[] result = null;
outBuffDesc.read();
outBuff.read();
if (outBuff.cbBuffer.intValue() > 0) {
result = outBuff.pvBuffer.getByteArray(0, outBuff.cbBuffer.intValue());
}

freeWin7Mem(null, null, outBuffDesc);

//Send NTLM type 1 message to server
httpclient = new DefaultHttpClient();
HttpContext localContext = new BasicHttpContext();
HttpGet request = new HttpGet(clientXmlProperties.getProperty(IWA_SERVER_URL));
request.setHeader("Connection", "keep-alive");
request.setHeader("Authorization", securityPackage + " " + new String(Base64.encodeBase64(result)));
HttpResponse response = httpclient.execute(request, localContext);

//Receive the NTLM type 2 message from the server
String continueToken = response.getFirstHeader("WWW-Authenticate").getValue().substring(securityPackage.length() + 1);
if (continueToken != null)
{
byte[] continueTokenBytes = Base64.decodeBase64(continueToken.getBytes());

Sspi.SecBuffer.ByReference inBuff = new Sspi.SecBuffer.ByReference();
inBuff.cbBuffer = new NativeLong(continueTokenBytes.length);
inBuff.BufferType = new NativeLong(Sspi.SECBUFFER_TOKEN);
inBuff.pvBuffer = new Memory(continueTokenBytes.length);
inBuff.pvBuffer.write(0, continueTokenBytes, 0, continueTokenBytes.length);
inBuff.write();
Sspi.SecBufferDesc inBuffDesc = new Sspi.SecBufferDesc();
inBuffDesc.ulVersion = new NativeLong(0);
inBuffDesc.cBuffers = new NativeLong(1);
inBuffDesc.pBuffers = (Sspi.SecBuffer.ByReference[])inBuff.toArray(1);
inBuffDesc.write();
outBuffDesc = new Sspi.SecBufferDesc();
outBuff = new Sspi.SecBuffer.ByReference();
outBuff.cbBuffer = new NativeLong(1024);
outBuff.BufferType = new NativeLong(Sspi.SECBUFFER_TOKEN);
outBuff.pvBuffer = new Memory(1024);
outBuff.write();
outBuffDesc.ulVersion = new NativeLong(0);
outBuffDesc.cBuffers = new NativeLong(1);
outBuffDesc.pBuffers = (Sspi.SecBuffer.ByReference[])outBuff.toArray(1);
outBuffDesc.write();
attr = new NativeLongByReference();

//Use the received type 2 message to generate the type 3 message which hopefully the server will accept
rc = Secur32.INSTANCE.InitializeSecurityContext(cred, ctx.isNull() ? null : ctx,
null, new NativeLong(0), new NativeLong(0),
new NativeLong(Sspi.SECURITY_NATIVE_DREP), inBuffDesc, new NativeLong(0), ctx, outBuffDesc,
attr, null);
if (rc != W32Errors.SEC_I_CONTINUE_NEEDED && rc != W32Errors.SEC_E_OK){
throw new Win32Exception(rc);
}

//Grab the data from the native call (i.e. the NTLM type 3 message)
byte[] finalResult = null;
outBuffDesc.read();
outBuff.read();
if (outBuff.cbBuffer.intValue() > 0) {
finalResult = outBuff.pvBuffer.getByteArray(0, outBuff.cbBuffer.intValue());
}
freeWin7Mem(null, null, inBuffDesc);
freeWin7Mem(cred, ctx, outBuffDesc);
//Prepare the outgoing NTLM type 3 message in the request header
request.setHeader("Authorization", securityPackage + " " + new String(Base64.encodeBase64(finalResult)));

//Clear data out of httpclient so we can re-use the connection
HttpEntity fakeEntity = response.getEntity();
if (fakeEntity != null) {
InputStream instream = fakeEntity.getContent();
BufferedReader reader = new BufferedReader(
new InputStreamReader(instream));
// do something useful with the response
System.out.println(reader.readLine());
instream.close();
}

//Send the NTLM type 3 message
httpclient.execute(request, localContext);
}
return getCookie(httpclient);
}

private void freeWin7Mem(Sspi.CredHandle credHand, Sspi.CtxtHandle ctxHandle, Sspi.SecBufferDesc outBuff) {
if (null != credHand) {
Secur32.INSTANCE.FreeCredentialsHandle(credHand);
credHand = null;
}

Secur32.INSTANCE.FreeContextBuffer(outBuff.getPointer());

if (null != ctxHandle) {
Secur32.INSTANCE.DeleteSecurityContext(ctxHandle);
ctxHandle = null;
}
}

private HttpCookie getCookie(DefaultHttpClient httpClient) throws IOException {
HttpCookie wamCookie = null;
List cookies = httpClient.getCookieStore().getCookies();
for (Cookie cookie : cookies)
{
if (cookie.getName().equalsIgnoreCase(SECURE_COOKIE_NAME))
{
wamCookie = new HttpCookie(cookie.getName(), cookie.getValue());
wamCookie.setMaxAge(CLIENT_EXPIRATION_TIME);
wamCookie.setComment(String.valueOf(new Date()));
}
}
return wamCookie;
}