diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 129d2a7b..0ca6e00c 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -36,13 +36,13 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -53,7 +53,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v2 # ℹī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -67,4 +67,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 17b0b8e5..4aad0af0 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -15,13 +15,14 @@ jobs: name: Build with Java 11 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up JDK 11 - uses: actions/setup-java@v1 + uses: actions/setup-java@v2 with: + distribution: 'zulu' java-version: 11 - name: Cache Gradle packages - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ~/.gradle/caches key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} @@ -33,13 +34,14 @@ jobs: needs: [java11] runs-on: [ubuntu-latest] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up JDK 11 - uses: actions/setup-java@v1 + uses: actions/setup-java@v2 with: + distribution: 'zulu' java-version: 11 - name: Cache Gradle packages - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ~/.gradle/caches key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8f876255..6ae4e949 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,12 +13,13 @@ jobs: name: Build with Java 12 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Set up JDK 12 - uses: actions/setup-java@v1 + uses: actions/setup-java@v2 with: + distribution: 'zulu' java-version: 12 - name: Grant execute permission for gradlew run: chmod +x gradlew @@ -29,11 +30,12 @@ jobs: needs: [java12] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 0 - - uses: actions/setup-java@v1 + - uses: actions/setup-java@v2 with: + distribution: 'zulu' java-version: 12 - name: Grant execute permission for gradlew run: chmod +x gradlew diff --git a/src/it/docker-image/smb.conf b/src/it/docker-image/smb.conf index 30a0d5e4..18ad2e2c 100644 --- a/src/it/docker-image/smb.conf +++ b/src/it/docker-image/smb.conf @@ -14,10 +14,10 @@ server string = %h server (Samba, Ubuntu) dns proxy = no interfaces = 192.168.2.0/24 eth0 bind interfaces only = yes -log level = 5 -log file = /var/log/samba/log.%m -max log size = 1000 -syslog = 0 +log level = 5 auth:10 +log file = /var/log/samba.log +max log size = 20480 +syslog = 1 panic action = /usr/share/samba/panic-action %d server role = standalone server passdb backend = tdbsam diff --git a/src/it/docker-image/supervisord.conf b/src/it/docker-image/supervisord.conf index d9582786..8b048740 100644 --- a/src/it/docker-image/supervisord.conf +++ b/src/it/docker-image/supervisord.conf @@ -5,10 +5,10 @@ loglevel=info [program:smbd] /* command=/usr/sbin/smbd -i --daemon --foreground --log-stdout */ -command=/usr/sbin/smbd --daemon --foreground --log-stdout -redirect_stderr=true +command=/usr/sbin/smbd --daemon --foreground --configfile=/etc/samba/smb.conf +/*redirect_stderr=true*/ [program:nmbd] /* command=/usr/sbin/nmbd -i --daemon --foreground --log-stdout */ -command=/usr/sbin/nmbd --daemon --foreground --log-stdout -redirect_stderr=true +command=/usr/sbin/nmbd --daemon --foreground +/*redirect_stderr=true*/ diff --git a/src/main/java/com/hierynomus/ntlm/NtlmConfig.java b/src/main/java/com/hierynomus/ntlm/NtlmConfig.java index 49dafb04..3f26a40b 100644 --- a/src/main/java/com/hierynomus/ntlm/NtlmConfig.java +++ b/src/main/java/com/hierynomus/ntlm/NtlmConfig.java @@ -75,7 +75,7 @@ public static class Builder { public Builder(Random r) { config = new NtlmConfig(); config.windowsVersion = new WindowsVersion(ProductMajorVersion.WINDOWS_MAJOR_VERSION_6, ProductMinorVersion.WINDOWS_MINOR_VERSION_1, 7600, NtlmRevisionCurrent.NTLMSSP_REVISION_W2K3); - config.integrity = false; + config.integrity = true; config.omitVersion = false; config.machineID = new byte[32]; r.nextBytes(config.machineID); diff --git a/src/main/java/com/hierynomus/ntlm/functions/NtlmFunctions.java b/src/main/java/com/hierynomus/ntlm/functions/NtlmFunctions.java index db1b2beb..f3c2569a 100644 --- a/src/main/java/com/hierynomus/ntlm/functions/NtlmFunctions.java +++ b/src/main/java/com/hierynomus/ntlm/functions/NtlmFunctions.java @@ -22,6 +22,8 @@ import com.hierynomus.ntlm.NtlmException; import com.hierynomus.protocol.commons.Charsets; import com.hierynomus.security.Cipher; +import com.hierynomus.security.Mac; +import com.hierynomus.security.MessageDigest; import com.hierynomus.security.SecurityException; import com.hierynomus.security.SecurityProvider; @@ -68,7 +70,7 @@ public static String oem(byte[] bytes) { */ static byte[] md4(SecurityProvider securityProvider, byte[] m) { try { - com.hierynomus.security.MessageDigest md4 = securityProvider.getDigest("MD4"); + MessageDigest md4 = securityProvider.getDigest("MD4"); md4.update(m); return md4.digest(); } catch (SecurityException e) { @@ -86,7 +88,7 @@ static byte[] md4(SecurityProvider securityProvider, byte[] m) { @SuppressWarnings("PMD.MethodNamingConventions") public static byte[] hmac_md5(SecurityProvider securityProvider, byte[] key, byte[]... message) { try { - com.hierynomus.security.Mac hmacMD5 = securityProvider.getMac("HmacMD5"); + Mac hmacMD5 = securityProvider.getMac("HMACT64"); hmacMD5.init(key); for (byte[] aMessage : message) { hmacMD5.update(aMessage); @@ -97,6 +99,18 @@ public static byte[] hmac_md5(SecurityProvider securityProvider, byte[] key, byt } } + public static byte[] md5(SecurityProvider securityProvider, byte[]... message) { + try { + MessageDigest md5 = securityProvider.getDigest("MD5"); + for (byte[] aMessage : message) { + md5.update(aMessage); + } + return md5.digest(); + } catch (SecurityException e) { + throw new NtlmException(e); + } + } + /** * [MS-NLMP].pdf 6 Appendix A: Cryptographic Operations Reference * (RC4K(K, D)). diff --git a/src/main/java/com/hierynomus/ntlm/messages/NtlmAuthenticate.java b/src/main/java/com/hierynomus/ntlm/messages/NtlmAuthenticate.java index c5b140db..8dbf2791 100644 --- a/src/main/java/com/hierynomus/ntlm/messages/NtlmAuthenticate.java +++ b/src/main/java/com/hierynomus/ntlm/messages/NtlmAuthenticate.java @@ -39,8 +39,6 @@ public class NtlmAuthenticate extends NtlmMessage { private byte[] workstation; private byte[] encryptedRandomSessionKey; private byte[] mic; - private boolean integrityEnabled; - private boolean omitVersion; public NtlmAuthenticate( byte[] lmResponse, byte[] ntResponse, diff --git a/src/main/java/com/hierynomus/ntlm/messages/WindowsVersion.java b/src/main/java/com/hierynomus/ntlm/messages/WindowsVersion.java index f0250c51..99d62d26 100644 --- a/src/main/java/com/hierynomus/ntlm/messages/WindowsVersion.java +++ b/src/main/java/com/hierynomus/ntlm/messages/WindowsVersion.java @@ -123,4 +123,8 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(majorVersion, minorVersion, productBuild, ntlmRevision); } + + public NtlmRevisionCurrent getNtlmRevision() { + return ntlmRevision; + } } diff --git a/src/main/java/com/hierynomus/security/MessageDigest.java b/src/main/java/com/hierynomus/security/MessageDigest.java index e00f2c96..6a4bbc74 100644 --- a/src/main/java/com/hierynomus/security/MessageDigest.java +++ b/src/main/java/com/hierynomus/security/MessageDigest.java @@ -16,8 +16,12 @@ package com.hierynomus.security; public interface MessageDigest { + void update(byte b); + void update(byte[] bytes); + void update(byte[] bytes, int offset, int len); + byte[] digest(); void reset(); diff --git a/src/main/java/com/hierynomus/security/SecurityException.java b/src/main/java/com/hierynomus/security/SecurityException.java index 34509daa..38784072 100644 --- a/src/main/java/com/hierynomus/security/SecurityException.java +++ b/src/main/java/com/hierynomus/security/SecurityException.java @@ -17,6 +17,10 @@ @SuppressWarnings("serial") public class SecurityException extends Exception { + public SecurityException(String message) { + super(message); + } + public SecurityException(Exception e) { super(e); } diff --git a/src/main/java/com/hierynomus/security/bc/BCMessageDigest.java b/src/main/java/com/hierynomus/security/bc/BCMessageDigest.java index 0b70bc90..114ef4ea 100644 --- a/src/main/java/com/hierynomus/security/bc/BCMessageDigest.java +++ b/src/main/java/com/hierynomus/security/bc/BCMessageDigest.java @@ -19,6 +19,7 @@ import com.hierynomus.security.MessageDigest; import org.bouncycastle.crypto.Digest; import org.bouncycastle.crypto.digests.MD4Digest; +import org.bouncycastle.crypto.digests.MD5Digest; import org.bouncycastle.crypto.digests.SHA256Digest; import org.bouncycastle.crypto.digests.SHA512Digest; @@ -47,6 +48,12 @@ public Digest create() { return new MD4Digest(); } }); + lookup.put("MD5", new Factory() { + @Override + public Digest create() { + return new MD5Digest(); + } + }); } private final Digest digest; @@ -63,11 +70,21 @@ private Digest getDigest(String name) { return digestFactory.create(); } + @Override + public void update(byte b) { + digest.update(b); + } + @Override public void update(byte[] bytes) { digest.update(bytes, 0, bytes.length); } + @Override + public void update(byte[] bytes, int offset, int len) { + digest.update(bytes, offset, len); + } + @Override public byte[] digest() { byte[] output = new byte[digest.getDigestSize()]; diff --git a/src/main/java/com/hierynomus/security/bc/BCSecurityProvider.java b/src/main/java/com/hierynomus/security/bc/BCSecurityProvider.java index 9c9bf560..0e08e135 100644 --- a/src/main/java/com/hierynomus/security/bc/BCSecurityProvider.java +++ b/src/main/java/com/hierynomus/security/bc/BCSecurityProvider.java @@ -15,12 +15,15 @@ */ package com.hierynomus.security.bc; +import java.util.Objects; + import com.hierynomus.security.AEADBlockCipher; import com.hierynomus.security.Cipher; import com.hierynomus.security.DerivationFunction; import com.hierynomus.security.Mac; import com.hierynomus.security.MessageDigest; import com.hierynomus.security.SecurityProvider; +import com.hierynomus.security.mac.HmacT64; /** * Generic BouncyCastle abstraction, in order to use Bouncy Castle directly when available. @@ -35,6 +38,9 @@ public MessageDigest getDigest(String name) { @Override public Mac getMac(String name) { + if (Objects.equals(name, "HMACT64")) { + return new HmacT64(getDigest("MD5")); + } return new BCMac(name); } diff --git a/src/main/java/com/hierynomus/security/jce/JceMessageDigest.java b/src/main/java/com/hierynomus/security/jce/JceMessageDigest.java index b917eb22..a49db33c 100644 --- a/src/main/java/com/hierynomus/security/jce/JceMessageDigest.java +++ b/src/main/java/com/hierynomus/security/jce/JceMessageDigest.java @@ -46,11 +46,21 @@ public class JceMessageDigest implements MessageDigest { } } + @Override + public void update(byte b) { + md.update(b); + } + @Override public void update(byte[] bytes) { md.update(bytes); } + @Override + public void update(byte[] bytes, int offset, int len) { + md.update(bytes, offset, len); + } + @Override public byte[] digest() { return md.digest(); diff --git a/src/main/java/com/hierynomus/security/jce/JceSecurityProvider.java b/src/main/java/com/hierynomus/security/jce/JceSecurityProvider.java index 1d94b985..a1ae6125 100644 --- a/src/main/java/com/hierynomus/security/jce/JceSecurityProvider.java +++ b/src/main/java/com/hierynomus/security/jce/JceSecurityProvider.java @@ -17,8 +17,10 @@ import com.hierynomus.security.*; import com.hierynomus.security.SecurityException; +import com.hierynomus.security.mac.HmacT64; import java.security.Provider; +import java.util.Objects; public class JceSecurityProvider implements SecurityProvider { private final Provider jceProvider; @@ -46,6 +48,9 @@ public MessageDigest getDigest(String name) throws SecurityException { @Override public Mac getMac(String name) throws SecurityException { + if (Objects.equals(name, "HMACT64")) { + return new HmacT64(getDigest("MD5")); + } return new JceMac(name, jceProvider, providerName); } diff --git a/src/main/java/com/hierynomus/security/mac/HmacT64.java b/src/main/java/com/hierynomus/security/mac/HmacT64.java new file mode 100644 index 00000000..5af64299 --- /dev/null +++ b/src/main/java/com/hierynomus/security/mac/HmacT64.java @@ -0,0 +1,112 @@ +/* + * Copyright (C)2016 - SMBJ Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hierynomus.security.mac; + +import com.hierynomus.security.Mac; +import com.hierynomus.security.MessageDigest; +import com.hierynomus.security.SecurityException; + +/** + * This is an implementation of the HMACT64 keyed hashing algorithm. + * HMACT64 is defined by Luke Leighton as a modified HMAC-MD5 (RFC 2104) + * in which the key is truncated at 64 bytes (rather than being hashed + * via MD5). + */ +public class HmacT64 implements Mac { + + private static final int BLOCK_LENGTH = 64; + + private static final byte IPAD = (byte) 0x36; + + private static final byte OPAD = (byte) 0x5c; + + private MessageDigest md5; + + private byte[] ipad = new byte[BLOCK_LENGTH]; + + private byte[] opad = new byte[BLOCK_LENGTH]; + + /** + * Creates an HMACT64 instance which uses the given secret key material. + */ + public HmacT64(MessageDigest md5) { + super(); + this.md5 = md5; + } + + @Override + public void init(byte[] key) throws SecurityException { + if (key == null) { + throw new SecurityException("Missing key data"); + } + + int length = Math.min(key.length, BLOCK_LENGTH); + for (int i = 0; i < length; i++) { + ipad[i] = (byte) (key[i] ^ IPAD); + opad[i] = (byte) (key[i] ^ OPAD); + } + + for (int i = length; i < BLOCK_LENGTH; i++) { + ipad[i] = IPAD; + opad[i] = OPAD; + } + + reset(); + + } + + @Override + public byte[] doFinal() { + try { + // finish the inner digest + byte[] tmp = md5.digest(); + + // compute digest for 2nd pass; start with outer pad + md5.update(opad); + // add result of 1st hash + md5.update(tmp); + + tmp = md5.digest(); + + return tmp; + } finally { + // reset the digest for further use + reset(); + } + } + + @Override + public void update(byte b) { + md5.update(b); + } + + @Override + public void update(byte[] array) { + md5.update(array); + } + + @Override + public void update(byte[] array, int offset, int length) { + md5.update(array, offset, length); + } + + @Override + public void reset() { + md5.reset(); + md5.update(ipad, 0, ipad.length); + } +} + diff --git a/src/main/java/com/hierynomus/smbj/auth/AuthenticateResponse.java b/src/main/java/com/hierynomus/smbj/auth/AuthenticateResponse.java index a60b2de5..4363f5b1 100644 --- a/src/main/java/com/hierynomus/smbj/auth/AuthenticateResponse.java +++ b/src/main/java/com/hierynomus/smbj/auth/AuthenticateResponse.java @@ -15,18 +15,23 @@ */ package com.hierynomus.smbj.auth; +import java.util.Set; + +import com.hierynomus.ntlm.messages.NtlmNegotiateFlag; import com.hierynomus.ntlm.messages.WindowsVersion; +import com.hierynomus.spnego.SpnegoToken; public class AuthenticateResponse { - private byte[] negToken; + private SpnegoToken negToken; private byte[] sessionKey; private WindowsVersion windowsVersion; private String netBiosName; + private Set negotiateFlags; public AuthenticateResponse() { } - public AuthenticateResponse(byte [] negToken) { + public AuthenticateResponse(SpnegoToken negToken) { this.negToken = negToken; } @@ -38,11 +43,11 @@ public void setWindowsVersion(WindowsVersion windowsVersion) { this.windowsVersion = windowsVersion; } - public byte[] getNegToken() { + public SpnegoToken getNegToken() { return negToken; } - public void setNegToken(byte[] negToken) { + public void setNegToken(SpnegoToken negToken) { this.negToken = negToken; } @@ -61,4 +66,12 @@ public String getNetBiosName() { public void setNetBiosName(String netBiosName) { this.netBiosName = netBiosName; } + + public Set getNegotiateFlags() { + return negotiateFlags; + } + + public void setNegotiateFlags(Set negotiateFlags) { + this.negotiateFlags = negotiateFlags; + } } diff --git a/src/main/java/com/hierynomus/smbj/auth/NtlmAuthenticator.java b/src/main/java/com/hierynomus/smbj/auth/NtlmAuthenticator.java index 3277e710..09fb4cbb 100644 --- a/src/main/java/com/hierynomus/smbj/auth/NtlmAuthenticator.java +++ b/src/main/java/com/hierynomus/smbj/auth/NtlmAuthenticator.java @@ -41,10 +41,7 @@ import com.hierynomus.ntlm.NtlmConfig; import com.hierynomus.ntlm.NtlmException; import com.hierynomus.ntlm.av.AvId; -import com.hierynomus.ntlm.av.AvPairChannelBindings; -import com.hierynomus.ntlm.av.AvPairFactory; import com.hierynomus.ntlm.av.AvPairFlags; -import com.hierynomus.ntlm.av.AvPairSingleHost; import com.hierynomus.ntlm.av.AvPairString; import com.hierynomus.ntlm.av.AvPairTimestamp; import com.hierynomus.ntlm.functions.ComputedNtlmV2Response; @@ -65,6 +62,7 @@ import com.hierynomus.spnego.NegTokenInit; import com.hierynomus.spnego.NegTokenTarg; import com.hierynomus.spnego.SpnegoException; +import com.hierynomus.spnego.SpnegoToken; import com.hierynomus.utils.Strings; public class NtlmAuthenticator implements Authenticator { @@ -82,7 +80,7 @@ enum State { NEGOTIATE, AUTHENTICATE, COMPLETE; }; // Context buildup private State state; private Set negotiateFlags; - private NtlmNegotiate negotiateMessage; + private byte[] negotiateMessage; public static class Factory implements com.hierynomus.protocol.commons.Factory.Named { @Override @@ -162,9 +160,10 @@ private AuthenticateResponse doNegotiate(AuthenticationContext context, byte[] g } } - this.negotiateMessage = new NtlmNegotiate(negotiateFlags, context.getDomain(), config.getWorkstationName(), config.getWindowsVersion(), config.isOmitVersion()); + NtlmNegotiate negotiateMessage = new NtlmNegotiate(negotiateFlags, context.getDomain(), config.getWorkstationName(), config.getWindowsVersion(), config.isOmitVersion()); logger.trace("Sending NTLM negotiate message: {}", this.negotiateMessage); response.setNegToken(negTokenInit(negotiateMessage)); + response.setNegotiateFlags(negotiateFlags); return response; } @@ -222,17 +221,15 @@ private AuthenticateResponse doAuthenticate(AuthenticationContext context, NtlmC msg.setMic(new byte[16]); // TODO correct hash should be tested Buffer.PlainBuffer micBuffer = new Buffer.PlainBuffer(Endian.LE); - negotiateMessage.write(micBuffer); // negotiateMessage - micBuffer.putRawBytes(serverNtlmChallenge.getServerChallenge()); // challengeMessage msg.write(micBuffer); // authentificateMessage - byte[] mic = NtlmFunctions.hmac_md5(securityProvider, sessionBaseKey, micBuffer.getCompactData()); + byte[] mic = NtlmFunctions.hmac_md5(securityProvider, exportedSessionKey, negotiateMessage, ntlmChallengeBytes, micBuffer.getCompactData()); msg.setMic(mic); } response.setSessionKey(exportedSessionKey); logger.trace("Sending NTLM authenticate message: {}", msg); response.setNegToken(negTokenTarg(msg)); - + response.setNegotiateFlags(negotiateFlags); return response; } @@ -272,25 +269,22 @@ private TargetInfo createClientTargetInfo(NtlmChallenge serverNtlmChallenge) { return clientTargetInfo; } - private byte[] negTokenInit(NtlmNegotiate ntlmNegotiate) throws SpnegoException { + private SpnegoToken negTokenInit(NtlmNegotiate ntlmNegotiate) throws SpnegoException { NegTokenInit negTokenInit = new NegTokenInit(); negTokenInit.addSupportedMech(NTLMSSP); Buffer.PlainBuffer ntlmBuffer = new Buffer.PlainBuffer(Endian.LE); ntlmNegotiate.write(ntlmBuffer); - negTokenInit.setMechToken(ntlmBuffer.getCompactData()); - Buffer.PlainBuffer negTokenBuffer = new Buffer.PlainBuffer(Endian.LE); - negTokenInit.write(negTokenBuffer); - return negTokenBuffer.getCompactData(); + this.negotiateMessage = ntlmBuffer.getCompactData(); + negTokenInit.setMechToken(this.negotiateMessage); + return negTokenInit; } - private byte[] negTokenTarg(NtlmAuthenticate resp) throws SpnegoException { + private SpnegoToken negTokenTarg(NtlmAuthenticate resp) throws SpnegoException { NegTokenTarg targ = new NegTokenTarg(); Buffer.PlainBuffer ntlmBuffer = new Buffer.PlainBuffer(Endian.LE); resp.write(ntlmBuffer); targ.setResponseToken(ntlmBuffer.getCompactData()); - Buffer.PlainBuffer negTokenBuffer = new Buffer.PlainBuffer(Endian.LE); - targ.write(negTokenBuffer); - return negTokenBuffer.getCompactData(); + return targ; } @Override diff --git a/src/main/java/com/hierynomus/smbj/auth/NtlmSealer.java b/src/main/java/com/hierynomus/smbj/auth/NtlmSealer.java new file mode 100644 index 00000000..51329234 --- /dev/null +++ b/src/main/java/com/hierynomus/smbj/auth/NtlmSealer.java @@ -0,0 +1,192 @@ +/* + * Copyright (C)2016 - SMBJ Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hierynomus.smbj.auth; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.hierynomus.asn1.ASN1OutputStream; +import com.hierynomus.asn1.encodingrules.der.DEREncoder; +import com.hierynomus.asn1.types.ASN1Object; +import com.hierynomus.asn1.types.constructed.ASN1Sequence; +import com.hierynomus.asn1.types.primitive.ASN1ObjectIdentifier; +import com.hierynomus.ntlm.functions.NtlmFunctions; +import com.hierynomus.ntlm.messages.NtlmNegotiateFlag; +import com.hierynomus.ntlm.messages.WindowsVersion; +import com.hierynomus.ntlm.messages.WindowsVersion.NtlmRevisionCurrent; +import com.hierynomus.protocol.commons.buffer.Buffer; +import com.hierynomus.security.SecurityProvider; +import com.hierynomus.smb.SMBBuffer; +import com.hierynomus.smbj.SmbConfig; +import com.hierynomus.smbj.connection.ConnectionContext; +import com.hierynomus.spnego.NegTokenInit; +import com.hierynomus.spnego.NegTokenTarg; + +public class NtlmSealer implements Authenticator { + private static final Logger logger = LoggerFactory.getLogger(NtlmSealer.class); + private static final byte[] C2S_SIGN_CONSTANT = "session key to client-to-server signing key magic constant\0" + .getBytes(StandardCharsets.US_ASCII); + private static final byte[] C2S_SEAL_CONSTANT = "session key to client-to-server sealing key magic constant\0" + .getBytes(StandardCharsets.US_ASCII); + + private NtlmAuthenticator wrapped; + private SecurityProvider securityProvider; + + private byte[] signKey; + private byte[] sealKey; + private AtomicInteger sequenceNumber = new AtomicInteger(0); + private List mechTypes; + + public NtlmSealer(NtlmAuthenticator wrapped) { + this.wrapped = wrapped; + } + + @Override + public AuthenticateResponse authenticate(AuthenticationContext context, byte[] gssToken, + ConnectionContext connectionContext) throws IOException { + AuthenticateResponse resp = wrapped.authenticate(context, gssToken, connectionContext); + if (resp == null) { + return null; + } + + byte[] sessionKey = resp.getSessionKey(); + Set negotiateFlags = resp.getNegotiateFlags(); + if (sessionKey != null) { + logger.debug("Calculating signing and sealing keys for NTLM Extended Session Security"); + this.signKey = deriveSigningKey(sessionKey, negotiateFlags); + this.sealKey = deriveSealingKey(sessionKey, negotiateFlags, resp.getWindowsVersion()); + } + + if (resp.getNegToken() instanceof NegTokenInit) { + NegTokenInit negToken = (NegTokenInit) resp.getNegToken(); + mechTypes = negToken.getSupportedMechTypes(); + } + + if (signKey != null && resp.getNegToken() instanceof NegTokenTarg) { + NegTokenTarg negToken = (NegTokenTarg) resp.getNegToken(); + logger.debug("Signing with NTLM Extended Session Security"); + int sequenceNumber = this.sequenceNumber.getAndIncrement(); + byte[] signature = sign(signKey, sequenceNumber); + + if (negotiateFlags.contains(NtlmNegotiateFlag.NTLMSSP_NEGOTIATE_KEY_EXCH)) { + signature = NtlmFunctions.rc4k(securityProvider, sealKey, signature); + } + + Buffer buffer = new SMBBuffer(); + buffer.putUInt32(1); // Version + buffer.putRawBytes(signature, 0, 8); // Checksum + buffer.putUInt32(sequenceNumber); // Sequence Number + + negToken.setMechListMic(buffer.getCompactData()); + } + + return resp; + } + + private byte[] sign(byte[] signKey, int sequenceNumber) throws IOException { + byte[] seq = uint32(sequenceNumber); + byte[] data = derBytes(mechTypes); + byte[] mac = NtlmFunctions.hmac_md5(securityProvider, signKey, seq, data); + + byte[] signature = new byte[8]; + System.arraycopy(mac, 0, signature, 0, 8); + return signature; + } + + private byte[] uint32(int value) { + return new byte[] { + (byte) (value & 0xFF), + (byte) ((value >> 8) & 0xFF), + (byte) ((value >> 16) & 0xFF), + (byte) ((value >> 24) & 0xFF) + }; + } + + @SuppressWarnings("rawtypes") + private byte[] derBytes(List mechTypes) throws IOException { + List supportedMechVector = new ArrayList(mechTypes); + ASN1Object o = new ASN1Sequence(supportedMechVector); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (ASN1OutputStream out = new ASN1OutputStream(new DEREncoder(), baos)) { + out.writeObject(o); + } + + return baos.toByteArray(); + } + + private byte[] deriveSigningKey(byte[] sessionKey, Set negotiateFlags) { + if (negotiateFlags.contains(NtlmNegotiateFlag.NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY)) { + return NtlmFunctions.md5(securityProvider, sessionKey, C2S_SIGN_CONSTANT); + } + + return null; + } + + private byte[] deriveSealingKey(byte[] sessionKey, Set negotiateFlags, + WindowsVersion windowsVersion) { + byte[] tmpKey; + if (negotiateFlags.contains(NtlmNegotiateFlag.NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY)) { + if (negotiateFlags.contains(NtlmNegotiateFlag.NTLMSSP_NEGOTIATE_128)) { + tmpKey = sessionKey; + } else if (negotiateFlags.contains(NtlmNegotiateFlag.NTLMSSP_NEGOTIATE_56)) { + tmpKey = Arrays.copyOf(sessionKey, 7); + } else { + tmpKey = Arrays.copyOf(sessionKey, 5); + } + + return NtlmFunctions.md5(securityProvider, tmpKey, C2S_SEAL_CONSTANT); + } else if (negotiateFlags.contains(NtlmNegotiateFlag.NTLMSSP_NEGOTIATE_LM_KEY) || + (negotiateFlags.contains(NtlmNegotiateFlag.NTLMSSP_NEGOTIATE_DATAGRAM) && + windowsVersion.getNtlmRevision().getValue() >= NtlmRevisionCurrent.NTLMSSP_REVISION_W2K3 + .getValue())) { + tmpKey = new byte[8]; + if (negotiateFlags.contains(NtlmNegotiateFlag.NTLMSSP_NEGOTIATE_56)) { + System.arraycopy(sessionKey, 0, tmpKey, 0, 7); + tmpKey[7] = (byte) 0xA0; + } else { + System.arraycopy(sessionKey, 0, tmpKey, 0, 5); + tmpKey[5] = (byte) 0xE5; + tmpKey[6] = (byte) 0x38; + tmpKey[7] = (byte) 0xB0; + } + + return tmpKey; + } else { + return Arrays.copyOf(sessionKey, sessionKey.length); + } + } + + @Override + public void init(SmbConfig config) { + wrapped.init(config); + this.securityProvider = config.getSecurityProvider(); + } + + @Override + public boolean supports(AuthenticationContext context) { + return wrapped.supports(context); + } +} diff --git a/src/main/java/com/hierynomus/smbj/auth/SpnegoAuthenticator.java b/src/main/java/com/hierynomus/smbj/auth/SpnegoAuthenticator.java index 4c93b14f..ba0b4de1 100644 --- a/src/main/java/com/hierynomus/smbj/auth/SpnegoAuthenticator.java +++ b/src/main/java/com/hierynomus/smbj/auth/SpnegoAuthenticator.java @@ -29,6 +29,7 @@ import com.hierynomus.smbj.GSSContextConfig; import com.hierynomus.smbj.SmbConfig; import com.hierynomus.smbj.connection.ConnectionContext; +import com.hierynomus.spnego.RawToken; import org.ietf.jgss.GSSContext; import org.ietf.jgss.GSSException; @@ -95,7 +96,7 @@ private AuthenticateResponse authenticateSession(GSSAuthenticationContext contex logger.trace("Received token: {}", ByteArrayUtils.printHex(newToken)); } - AuthenticateResponse response = new AuthenticateResponse(newToken); + AuthenticateResponse response = new AuthenticateResponse(new RawToken(newToken)); if (gssContext.isEstablished()) { Key key = ExtendedGSSContext.krb5GetSessionKey(gssContext); if (key != null) { diff --git a/src/main/java/com/hierynomus/smbj/connection/SMBSessionBuilder.java b/src/main/java/com/hierynomus/smbj/connection/SMBSessionBuilder.java index d5ea8a24..130af485 100644 --- a/src/main/java/com/hierynomus/smbj/connection/SMBSessionBuilder.java +++ b/src/main/java/com/hierynomus/smbj/connection/SMBSessionBuilder.java @@ -22,6 +22,8 @@ import com.hierynomus.mssmb2.SMBApiException; import com.hierynomus.mssmb2.messages.SMB2SessionSetup; import com.hierynomus.protocol.commons.Factory; +import com.hierynomus.protocol.commons.buffer.Buffer; +import com.hierynomus.protocol.commons.buffer.Endian; import com.hierynomus.protocol.transport.TransportException; import com.hierynomus.security.DerivationFunction; import com.hierynomus.security.MessageDigest; @@ -32,6 +34,8 @@ import com.hierynomus.smbj.auth.AuthenticateResponse; import com.hierynomus.smbj.auth.AuthenticationContext; import com.hierynomus.smbj.auth.Authenticator; +import com.hierynomus.smbj.auth.NtlmAuthenticator; +import com.hierynomus.smbj.auth.NtlmSealer; import com.hierynomus.smbj.common.SMBRuntimeException; import com.hierynomus.smbj.session.SMB2GuestSigningRequiredException; import com.hierynomus.smbj.session.Session; @@ -40,6 +44,8 @@ import com.hierynomus.spnego.NegTokenInit; import com.hierynomus.spnego.NegTokenInit2; import com.hierynomus.spnego.SpnegoException; +import com.hierynomus.spnego.SpnegoToken; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -101,6 +107,10 @@ public SMBSessionBuilder(Connection connection, SmbConfig config, SessionFactory public Session establish(AuthenticationContext authContext) { try { Authenticator authenticator = getAuthenticator(authContext); + if (authenticator instanceof NtlmAuthenticator && config.getNtlmConfig().isIntegrityEnabled()) { + authenticator = new NtlmSealer((NtlmAuthenticator) authenticator); + } + BuilderContext ctx = newContext(authContext, authenticator); authenticator.init(config); @@ -183,7 +193,15 @@ private void processAuthenticationToken(BuilderContext ctx, byte[] inputToken) t connectionContext.setNetBiosName(resp.getNetBiosName()); ctx.sessionKey = resp.getSessionKey(); - ctx.securityContext = resp.getNegToken(); + + SpnegoToken token = resp.getNegToken(); + Buffer.PlainBuffer negTokenBuffer = new Buffer.PlainBuffer(Endian.LE); + try { + token.write(negTokenBuffer); + } catch (SpnegoException e) { + throw new IOException(e); + } + ctx.securityContext = negTokenBuffer.getCompactData(); } private BuilderContext initiateSessionSetup(BuilderContext ctx, byte[] securityContext) throws TransportException { diff --git a/src/main/java/com/hierynomus/spnego/RawToken.java b/src/main/java/com/hierynomus/spnego/RawToken.java new file mode 100644 index 00000000..5592e792 --- /dev/null +++ b/src/main/java/com/hierynomus/spnego/RawToken.java @@ -0,0 +1,40 @@ +/* + * Copyright (C)2016 - SMBJ Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hierynomus.spnego; + +import java.io.IOException; + +import com.hierynomus.asn1.types.constructed.ASN1TaggedObject; +import com.hierynomus.protocol.commons.buffer.Buffer; + +public class RawToken extends SpnegoToken { + private byte[] rawToken; + + public RawToken(byte[] rawToken) { + super(0, null); + this.rawToken = rawToken; + } + + @Override + protected void parseTagged(ASN1TaggedObject asn1TaggedObject) throws SpnegoException { + throw new UnsupportedOperationException("RawToken does not support parsing of tagged objects"); + } + + @Override + public void write(Buffer buffer) throws SpnegoException { + buffer.putRawBytes(rawToken); + } +} diff --git a/src/main/java/com/hierynomus/spnego/SpnegoToken.java b/src/main/java/com/hierynomus/spnego/SpnegoToken.java index 3945394d..5bf9cb15 100644 --- a/src/main/java/com/hierynomus/spnego/SpnegoToken.java +++ b/src/main/java/com/hierynomus/spnego/SpnegoToken.java @@ -30,7 +30,7 @@ import static com.hierynomus.spnego.ObjectIdentifiers.SPNEGO; -abstract class SpnegoToken { +public abstract class SpnegoToken { private int tokenTagNo; private String tokenName; @@ -58,7 +58,8 @@ protected void writeGss(Buffer buffer, ASN1Object negToken) throws IOExcep protected void parseSpnegoToken(ASN1Object spnegoToken) throws SpnegoException { if (!(spnegoToken instanceof ASN1TaggedObject) || ((ASN1TaggedObject) spnegoToken).getTagNo() != tokenTagNo) { - throw new SpnegoException("Expected to find the " + tokenName + " (CHOICE [" + tokenTagNo + "]) header, not: " + spnegoToken); + throw new SpnegoException( + "Expected to find the " + tokenName + " (CHOICE [" + tokenTagNo + "]) header, not: " + spnegoToken); } ASN1Object negToken = ((ASN1TaggedObject) spnegoToken).getObject(); @@ -68,13 +69,16 @@ protected void parseSpnegoToken(ASN1Object spnegoToken) throws SpnegoExceptio for (ASN1Object asn1Object : (ASN1Sequence) negToken) { if (!(asn1Object instanceof ASN1TaggedObject)) { - throw new SpnegoException("Expected an ASN.1 TaggedObject as " + tokenName + " contents, not: " + asn1Object); + throw new SpnegoException( + "Expected an ASN.1 TaggedObject as " + tokenName + " contents, not: " + asn1Object); } ASN1TaggedObject asn1TaggedObject = (ASN1TaggedObject) asn1Object; parseTagged(asn1TaggedObject); } } + public abstract void write(Buffer buffer) throws SpnegoException; + protected abstract void parseTagged(ASN1TaggedObject asn1TaggedObject) throws SpnegoException; } diff --git a/src/test/groovy/com/hierynomus/smbj/connection/StubAuthenticator.groovy b/src/test/groovy/com/hierynomus/smbj/connection/StubAuthenticator.groovy index 32bbc8dc..43cc70da 100644 --- a/src/test/groovy/com/hierynomus/smbj/connection/StubAuthenticator.groovy +++ b/src/test/groovy/com/hierynomus/smbj/connection/StubAuthenticator.groovy @@ -22,6 +22,7 @@ import com.hierynomus.smbj.auth.AuthenticateResponse import com.hierynomus.smbj.auth.AuthenticationContext import com.hierynomus.smbj.auth.Authenticator import com.hierynomus.smbj.session.Session +import com.hierynomus.spnego.RawToken class StubAuthenticator implements Authenticator { static class Factory implements com.hierynomus.protocol.commons.Factory.Named { @@ -49,7 +50,7 @@ class StubAuthenticator implements Authenticator { @Override AuthenticateResponse authenticate(AuthenticationContext context, byte[] gssToken, ConnectionContext connectionContext) throws IOException { - def resp = new AuthenticateResponse(new byte[0]) + def resp = new AuthenticateResponse(new RawToken(new byte[0])) resp.sessionKey = ByteArrayUtils.parseHex("09921d4431b171b977370bf8910900f9") return resp }