From 72799f0346df3830f1c476152590ec95eeac828c Mon Sep 17 00:00:00 2001 From: Ylian Saint-Hilaire Date: Fri, 2 Apr 2021 17:20:36 -0700 Subject: [PATCH] Added support for user inner server authentication. --- agents/meshcmd.js | 50 +++++++++++++---- meshuser.js | 22 +------- webserver.js | 134 ++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 172 insertions(+), 34 deletions(-) diff --git a/agents/meshcmd.js b/agents/meshcmd.js index 12000f8a..6d562064 100644 --- a/agents/meshcmd.js +++ b/agents/meshcmd.js @@ -68,7 +68,11 @@ var FullSite_IntelAmtLocalWebApp = "H4sIAAAAAAAEAMQ5h3ajvNKvwu/9SnI2JICNa7zn4JLu // Check the server certificate fingerprint function onVerifyServer(clientName, certs) { if (certs == null) { certs = clientName; } // Temporary thing until we fix duktape - settings.meshServerTlsHash = certs[certs.length - 1].fingerprint.split(':').join(''); // This is used to delayed server authentication + + // If we have the serverid, used delayed server authentication + if (settings.serverid != null) { settings.meshServerTlsHash = certs[certs.length - 1].fingerprint.split(':').join(''); return; } + + // Otherwise, use server HTTPS certificate hash try { for (var i in certs) { if (certs[i].fingerprint.replace(/:/g, '') == settings.serverhttpshash) { return; } } } catch (e) { } if (settings.serverhttpshash != null) { console.log('Error: Failed to verify server certificate.'); @@ -1981,14 +1985,17 @@ function startRouter() { // Complete the URL and add a x-meshauth header if needed var xurlargs = []; - if (settings.authcookie != null) { - xurlargs.push('auth=' + settings.authcookie); - if (xtoken != null) { xurlargs.push('token=' + xtoken); } - } else { - if (xtoken != null) { - options.headers = { 'x-meshauth': Buffer.from(settings.username,'binary').toString('base64') + ',' + Buffer.from(settings.password,'binary').toString('base64') + ',' + Buffer.from(xtoken,'binary').toString('base64') }; + if (settings.serverid == null) { + // Authenticate the server using HTTPS cert hash + if (settings.authcookie != null) { + xurlargs.push('auth=' + settings.authcookie); + if (xtoken != null) { xurlargs.push('token=' + xtoken); } } else { - options.headers = { 'x-meshauth': Buffer.from(settings.username,'binary').toString('base64') + ',' + Buffer.from(settings.password,'binary').toString('base64') }; + if (xtoken != null) { + options.headers = { 'x-meshauth': Buffer.from(settings.username, 'binary').toString('base64') + ',' + Buffer.from(settings.password, 'binary').toString('base64') + ',' + Buffer.from(xtoken, 'binary').toString('base64') }; + } else { + options.headers = { 'x-meshauth': Buffer.from(settings.username, 'binary').toString('base64') + ',' + Buffer.from(settings.password, 'binary').toString('base64') }; + } } } if (settings.loginkey) { xurlargs.push('key=' + settings.loginkey); } @@ -2050,7 +2057,26 @@ function OnServerWebSocket(msg, s, head) { var signDataHash = hasher.syncHash(Buffer.concat([Buffer.from(settings.serverAuthClientNonce, 'base64'), Buffer.from(settings.meshServerTlsHash, 'hex'), Buffer.from(command.nonce, 'base64')])); if (require('RSA').verify(require('RSA').TYPES.SHA384, cert, signDataHash, Buffer.from(command.signature, 'base64')) == false) { console.log("Unable to authenticate the server, invalid signature."); process.exit(1); return; } - console.log('Server is authenticated'); // TODO: Send username/password to server. + // Figure out the 2FA token to use if any + var xtoken = null; + if (settings.emailtoken) { xtoken = '**email**'; } + else if (settings.smstoken) { xtoken = '**sms**'; } + else if (settings.token != null) { xtoken = settings.token; } + + // Authenticate the server using HTTPS cert hash + if (settings.authcookie != null) { + if (xtoken != null) { + s.write("{\"action\":\"userAuth\",\"auth\":\"" + settings.authcookie + "\",\"token\":\"" + xtoken + "\"}"); + } else { + s.write("{\"action\":\"userAuth\",\"auth\":\"" + settings.authcookie + "\"}"); + } + } else { + if (xtoken != null) { + s.write("{\"action\":\"userAuth\",\"username\":\"" + Buffer.from(settings.username, 'binary').toString('base64') + "\",\"password\":\"" + Buffer.from(settings.password, 'binary').toString('base64') + "\",\"token\":\"" + xtoken + "\"}"); + } else { + s.write("{\"action\":\"userAuth\",\"username\":\"" + Buffer.from(settings.username, 'binary').toString('base64') + "\",\"password\":\"" + Buffer.from(settings.password, 'binary').toString('base64') + "\"}"); + } + } break; } } @@ -2059,8 +2085,10 @@ function OnServerWebSocket(msg, s, head) { s.on('close', function () { console.log("Server closed the connection."); process.exit(1); return; }); // Perform inner server authentication - //settings.serverAuthClientNonce = require('EncryptionStream').GenerateRandom(48).toString('base64'); - //s.write("{\"action\":\"serverAuth\",\"cnonce\":\"" + settings.serverAuthClientNonce + "\",\"tlshash\":\"" + settings.meshServerTlsHash + "\"}"); // Ask for server authentication + if (settings.serverid != null) { + settings.serverAuthClientNonce = require('EncryptionStream').GenerateRandom(48).toString('base64'); + s.write("{\"action\":\"serverAuth\",\"cnonce\":\"" + settings.serverAuthClientNonce + "\",\"tlshash\":\"" + settings.meshServerTlsHash + "\"}"); // Ask for server authentication + } } function startRouterEx() { diff --git a/meshuser.js b/meshuser.js index 6172a09a..45e8346b 100644 --- a/meshuser.js +++ b/meshuser.js @@ -102,8 +102,8 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use function cleanRemoteAddr(addr) { if (addr.startsWith('::ffff:')) { return addr.substring(7); } else { return addr; } } // Send a PING/PONG message - function sendPing() { obj.ws.send('{"action":"ping"}'); } - function sendPong() { obj.ws.send('{"action":"pong"}'); } + function sendPing() { try { obj.ws.send('{"action":"ping"}'); } catch (ex) { } } + function sendPong() { try { obj.ws.send('{"action":"pong"}'); } catch (ex) { } } // Setup the agent PING/PONG timers if ((typeof args.browserping == 'number') && (obj.pingtimer == null)) { obj.pingtimer = setInterval(sendPing, args.browserping * 1000); } @@ -5544,24 +5544,6 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use //console.log(command, file); break; } - case 'serverAuth': { // This command is used to perform server "inner" authentication. - if (common.validateString(command.cnonce, 1, 256) == false) break; // Check the client nonce - if (common.validateString(command.tlshash, 1, 512) == false) break; // Check the TLS hash - - // Check that the TLS hash is an acceptable one. - var h = Buffer.from(command.tlshash, 'hex').toString('binary'); - if ((parent.webCertificateHashs[domain.id] != h) && (parent.webCertificateFullHashs[domain.id] != h) && (parent.defaultWebCertificateHash != h) && (parent.defaultWebCertificateFullHash != h)) { obj.close(); return; } - - // TLS hash check is a success, sign the request. - // Perform the hash signature using the server agent certificate - var nonce = parent.crypto.randomBytes(48); - var signData = Buffer.from(command.cnonce, 'base64').toString('binary') + h + nonce.toString('binary'); // Client Nonce + TLS Hash + Server Nonce - parent.parent.certificateOperations.acceleratorPerformSignature(0, signData, null, function (tag, signature) { - // Send back our certificate + nonce + signature - ws.send(JSON.stringify({ 'action': 'serverAuth', 'cert': Buffer.from(parent.agentCertificateAsn1, 'binary').toString('base64'), 'nonce': nonce.toString('base64'), 'signature': Buffer.from(signature,'binary').toString('base64') })); - }); - break; - } default: { // Unknown user action console.log('Unknown action from user ' + user.name + ': ' + command.action + '.'); diff --git a/webserver.js b/webserver.js index cd82acd3..44d29a38 100644 --- a/webserver.js +++ b/webserver.js @@ -4585,7 +4585,6 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { res.send(JSON.stringify(meshaction, null, ' ')); return; } else if (req.query.meshaction == 'winrouter') { - console.log('t2'); var p = obj.path.join(__dirname, 'agents', 'MeshCentralRouter.exe'); if (obj.fs.existsSync(p)) { setContentDispositionHeader(res, 'application/octet-stream', 'MeshCentralRouter.exe', null, 'MeshCentralRouter.exe'); @@ -5071,7 +5070,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { name: 'xid', // Recommended security practice to not use the default cookie name httpOnly: true, keys: [obj.args.sessionkey], // If multiple instances of this server are behind a load-balancer, this secret must be the same for all instances - secure: (obj.args.tlsoffload == null) // Use this cookie only over TLS (Check this: https://expressjs.com/en/guide/behind-proxies.html) + secure: true // Use this cookie only over TLS (Check this: https://expressjs.com/en/guide/behind-proxies.html) } if (obj.args.sessionsamesite != null) { sessionOptions.sameSite = obj.args.sessionsamesite; } else { sessionOptions.sameSite = 'strict'; } if (obj.args.sessiontime != null) { sessionOptions.maxAge = (obj.args.sessiontime * 60 * 1000); } @@ -5262,7 +5261,13 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { obj.app.ws(url + 'control.ashx', function (ws, req) { const domain = getDomain(req); if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { ws.close(); return; } // Check 3FA URL key - PerformWSSessionAuth(ws, req, false, function (ws1, req1, domain, user, cookie) { obj.meshUserHandler.CreateMeshUser(obj, obj.db, ws1, req1, obj.args, domain, user); }); + PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie) { + if (user == null) { // User is not authenticated, perform inner server authentication + PerformWSSessionInnerAuth(ws, req, domain, function (ws1, req1, domain, user) { obj.meshUserHandler.CreateMeshUser(obj, obj.db, ws1, req1, obj.args, domain, user); }); // User is authenticated + } else { + obj.meshUserHandler.CreateMeshUser(obj, obj.db, ws1, req1, obj.args, domain, user); // User is authenticated + } + }); }); obj.app.ws(url + 'devicefile.ashx', function (ws, req) { obj.meshDeviceFileHandler.CreateMeshDeviceFile(obj, ws, null, req, domain); }); obj.app.get(url + 'devicefile.ashx', handleDeviceFile); @@ -5735,6 +5740,129 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { if (obj.args.agentport) { CheckListenPort(obj.args.agentport, obj.args.agentportbind, StartAltWebServer); } } + // Perform server inner authentication + // This is a type of server authentication where the client will open the socket regardless of the TLS certificate and request that the server + // sign a client nonce with the server agent cert and return the response. Only after that will the client send the client authentication username + // and password or authentication cookie. + function PerformWSSessionInnerAuth(ws, req, domain, func) { + // When data is received from the web socket + ws.on('message', function (data) { + var command; + try { command = JSON.parse(data.toString('utf8')); } catch (e) { return; } + if (obj.common.validateString(command.action, 3, 32) == false) return; // Action must be a string between 3 and 32 chars + + switch (command.action) { + case 'serverAuth': { // This command is used to perform server "inner" authentication. + if (obj.common.validateString(command.cnonce, 1, 256) == false) break; // Check the client nonce + if (obj.common.validateString(command.tlshash, 1, 512) == false) break; // Check the TLS hash + + // Check that the TLS hash is an acceptable one. + var h = Buffer.from(command.tlshash, 'hex').toString('binary'); + if ((obj.webCertificateHashs[domain.id] != h) && (obj.webCertificateFullHashs[domain.id] != h) && (obj.defaultWebCertificateHash != h) && (obj.defaultWebCertificateFullHash != h)) { try { ws.close(); } catch (ex) { } return; } + + // TLS hash check is a success, sign the request. + // Perform the hash signature using the server agent certificate + var nonce = obj.crypto.randomBytes(48); + var signData = Buffer.from(command.cnonce, 'base64').toString('binary') + h + nonce.toString('binary'); // Client Nonce + TLS Hash + Server Nonce + parent.certificateOperations.acceleratorPerformSignature(0, signData, null, function (tag, signature) { + // Send back our certificate + nonce + signature + ws.send(JSON.stringify({ 'action': 'serverAuth', 'cert': Buffer.from(obj.agentCertificateAsn1, 'binary').toString('base64'), 'nonce': nonce.toString('base64'), 'signature': Buffer.from(signature, 'binary').toString('base64') })); + }); + break; + } + case 'userAuth': { // This command is used to perform user authentication. + // Check username and password authentication + if ((typeof command.username == 'string') && (typeof command.password == 'string')) { + obj.authenticate(Buffer.from(command.username, 'base64').toString(), Buffer.from(command.password, 'base64').toString(), domain, function (err, userid) { + var user = obj.users[userid]; + if ((err == null) && (user)) { + // Check if a 2nd factor is needed + var emailcheck = ((domain.mailserver != null) && (obj.parent.certificates.CommonName != null) && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.lanonly != true) && (domain.auth != 'sspi') && (domain.auth != 'ldap')) + if (checkUserOneTimePasswordRequired(domain, user, req) == true) { + // Figure out if email 2FA is allowed + var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.otpekey != null)); + var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null)); + if ((typeof req.query.token != 'string') || (req.query.token == '**email**') || (req.query.token == '**sms**')) { + if ((req.query.token == '**email**') && (email2fa == true)) { + // Cause a token to be sent to the user's registered email + user.otpekey = { k: obj.common.zeroPad(getRandomEightDigitInteger(), 8), d: Date.now() }; + obj.db.SetUser(user); + parent.debug('web', 'Sending 2FA email to: ' + user.email); + domain.mailserver.sendAccountLoginMail(domain, user.email, user.otpekey.k, obj.getLanguageCodes(req), req.query.key); + // Ask for a login token & confirm email was sent + try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, email2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { } + } else if ((req.query.token == '**sms**') && (sms2fa == true)) { + // Cause a token to be sent to the user's phone number + user.otpsms = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() }; + obj.db.SetUser(user); + parent.debug('web', 'Sending 2FA SMS to: ' + user.phone); + parent.smsserver.sendToken(domain, user.phone, user.otpsms.k, obj.getLanguageCodes(req)); + // Ask for a login token & confirm sms was sent + try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, sms2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { } + } else { + // Ask for a login token + parent.debug('web', 'Asking for login token'); + try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { } + } + } else { + checkUserOneTimePassword(req, domain, user, req.query.token, null, function (result) { + if (result == false) { + // Failed, ask for a login token again + parent.debug('web', 'Invalid login token, asking again'); + try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { } + } else { + // We are authenticated with 2nd factor. + // Check email verification + if (emailcheck && (user.email != null) && (user.emailVerified !== true)) { + parent.debug('web', 'Invalid login, asking for email validation'); + try { ws.send(JSON.stringify({ action: 'close', cause: 'emailvalidation', msg: 'emailvalidationrequired', email2fa: email2fa, sms2fa: sms2fa, email2fasent: true })); ws.close(); } catch (e) { } + } else { + // We are authenticated + ws._socket.pause(); + ws.removeAllListeners(['message', 'close', 'error']); + func(ws, req, domain, user); + } + } + }); + } + } else { + // Check email verification + if (emailcheck && (user.email != null) && (user.emailVerified !== true)) { + parent.debug('web', 'Invalid login, asking for email validation'); + var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.otpekey != null)); + var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null)); + try { ws.send(JSON.stringify({ action: 'close', cause: 'emailvalidation', msg: 'emailvalidationrequired', email2fa: email2fa, sms2fa: sms2fa, email2fasent: true })); ws.close(); } catch (e) { } + } else { + // We are authenticated + ws._socket.pause(); + ws.removeAllListeners(['message', 'close', 'error']); + func(ws, req, domain, user); + } + } + + } + }); + } else { + // Invalid authentication + try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth-2c' })); } catch (ex) { } + try { ws.close(); } catch (ex) { } + } + break; + } + } + + }); + + // If error, do nothing + ws.on('error', function (err) { try { ws.close(); } catch (e) { console.log(e); } }); + + // If the web socket is closed + ws.on('close', function (req) { try { ws.close(); } catch (e) { console.log(e); } }); + + // Resume the socket to perform inner authentication + try { ws._socket.resume(); } catch (ex) { } + } + // Authenticates a session and forwards function PerformWSSessionAuth(ws, req, noAuthOk, func) { // Check if this is a banned ip address