diff --git a/License.md b/License.md index 6946f2a..2ee26f1 100644 --- a/License.md +++ b/License.md @@ -1,7 +1,7 @@ -Copyright 2018 University of Maryland - College Park | Advanced Cybersecurity experience for Students +Copyright 2018 University of Maryland - College Park | Advanced Cybersecurity Experience for Students Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 637ab2d..d2023da 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,8 @@ -# Man-in-the-middle SSH Server +# Man-in-the-Middle (MITM) SSH Server ## Objective -Provide students the ability to collect SSH related data (login attempts, keystrokes) without the need for them to build their own SSH server. - -See this [wiki page](https://github.com/UMD-ACES/MITM/wiki/Data-Collection) about the information collected by the MITM SSH server. +Provide students with the ability to collect SSH related data (login attempts, keystrokes) without the need to build their own SSH server. ## Expectations @@ -16,39 +14,21 @@ This program is not meant to facilitate the following: * Monitoring * Data Analysis -However, students may modify this program as they wish including faciliting the above while following the [rules](https://github.com/UMD-ACES/MITM/blob/master/README.md#rules) described at a later section. +However, students may modify this program as they wish to add or change desired functionality. -## Resources +# Data Collection +This program will collect 3 main types of data: +1. Authentication attempts - including client IP, username, and password +2. Successful logins - client IP +3. Session stream - raw session stream between the client & SSH server -Please see the rest of this README page and check out the [wiki](https://github.com/UMD-ACES/MITM/wiki) pages. +## Start the MITM server +Run `node mitm.js -n -i -p ` to start the MITM server. ## Configuration -| Setting | Type | Explanation | -| :--------:| :----: | :------------| -| local | Boolean | Runs the MITM SSH Server without requiring a container. Warning messages will display and there will be limitations (e.g. pty mode is disabled). | -| debug | Boolean | MITM Debug Output. Good option to have enabled when building your honeypot ecosystem. Provides detailed logs of the actions that the MITM takes in real time. | -| logToInstructor.enabled | Boolean | Logging the MITM operations into a DB (must be **enabled** unless otherwise stated by an instructor or TA) | -| logging.streamOutput | String | Folder where the attacker streams are placed (keystrokes, screen display) | -| logging.loginAttempts | String | Folder where all login attempts are being logged | -| logging.logins | String | Folder where all logins are being logged | -| server.maxAttemptsPerConnection | Integer | Number of login attempts before the server force closes on the SSH client | -| server.listenIP | String | The IP address to listen on | -| server.identifier | String | The SSH server identifier string sent to the SSH client | -| server.banner | String | A message sent to clients upon connection to the MITM | -| autoAccess.enabled | Boolean | If true, then enable automatic access to the honeypot after a certain number of login attempts (normal distribution using mean and standard deviation values). Can be manually set in the command line. | -| autoAccess.cacheSize | Integer | Number of attacker IPs to hold when autoAccess is turned "on" . This value is required to not overwhelm the host memory. | -| autoAccess.barrier.normalDist.enabled | Boolean | Enable normal distribution to calculate the login attempt threshold per attacker | -| autoAccess.barrier.normalDist.mean | Integer | Mean number of login attempts before automatic access | -| autoAccess.barrier.normalDist.standardDeviation | Integer | Standard Deviation. Automatic access follows a normal distribution. | -| autoAccess.barrier.fixed.enabled | Boolean | Enable fixed login attempts threshold | -| autoAccess.barrier.fixed.attempts | Number | Number of login attempts | - - -## Start the MITM server - -View this wiki page to learn about starting the MITM SSH Server (https://github.com/UMD-ACES/MITM/wiki/Spawn-a-MITM-SSH-Server-instance#launch-a-mitm-ssh-server) +Run with the `--help` option to see full list of configurable options and defaults. ## Running MITM in the background @@ -56,25 +36,23 @@ Please check this [wiki page](https://github.com/UMD-ACES/MITM/wiki/Running-in-t ## Automatic Access -Allows an attacker to successfully authenticate after a certain number of login attempts. +This feature allows an attacker to successfully authenticate after a certain number of login attempts. -Before using automatic access, please read the following [wiki page](https://github.com/UMD-ACES/MITM/wiki/Automatic-Access) +Auto-access will only be available for 1 automatic access per MITM process, meaning that once MITM is triggered once, it will be disabled. -## Rules -1. Do not add/edit/delete any code that are in the instructor blocks. -2. You must enable the logToInstructor functionality. -3. If you are having issues with a particular MITM instance, please make sure to communicate the session id +Enable auto-access by toggling the `--auto-access` option, then you must configure one of the two strategies available: +1. normal distribution +2. fixed attempt -## Stay up to date -Run `git pull origin master` inside the /root/MITM directory. +For normal distribution strategy, the server will allow auto-access after `--auto-access-normal-distribution-mean` number of attempts with the consideration of `--auto-access-normal-distribution-std-dev` to randomize the number of attempts required. -## Documentation -[Wiki Page](https://github.com/UMD-ACES/MITM/wiki) +For fixed attempt strategy, the server will simply allow auto-access after --auto-access-fixed` number of attempts. + +## Stay up to date +Run `git pull origin main` inside the /root/MITM directory. -## Authors -Louis-Henri Merino -Franz Payer -Zhi Xiang Lin +## Additional Documentation +Some of the [Wiki Page](https://github.com/UMD-ACES/MITM/wiki) may be out of date, please review the information carefully. ## License MIT License diff --git a/config/mitm_example.js b/config/mitm_example.js deleted file mode 100644 index 1fb890f..0000000 --- a/config/mitm_example.js +++ /dev/null @@ -1,47 +0,0 @@ -'use strict'; - -module.exports = { - local: false, - debug : true, - logToInstructor: { - enabled: false, - host: '172.30.125.124', - user: 'students', - password: 'ebJAHqWx.d?&Zh*qX|r*{X+k6vMb', - database: 'ssh_mitm_f19', - connectionLimit : 5 - }, - container : { - mountPath: { - prefix: '/var/lib/lxc/', - suffix: 'rootfs' - }, - }, - logging : { - streamOutput : '/root/MITM_data/sessions', - loginAttempts : '/root/MITM_data/login_attempts', - logins : '/root/MITM_data/logins' - }, - server : { - maxAttemptsPerConnection: 6, - listenIP : '0.0.0.0', - identifier : 'SSH-2.0-OpenSSH_6.6.1p1 Ubuntu-2ubuntu2', - banner : '' - }, - autoAccess : { - enabled: false, - cacheSize : 5000, - barrier: { - normalDist: { - enabled: false, - mean: 6, - standardDeviation: 1, - }, - fixed: { - enabled: true, - attempts: 3, - } - } - - } -}; diff --git a/install.sh b/install.sh index 7e7c101..4a3c5b1 100755 --- a/install.sh +++ b/install.sh @@ -1,12 +1,12 @@ #!/usr/bin/env bash -sudo apt-get update +sudo apt update -sudo apt-get install -y sudo build-essential curl php-cli gcc g++ make +sudo apt install -y build-essential curl gcc g++ make curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash - -sudo apt-get install -y nodejs +sudo apt install -y nodejs cd "$(dirname "$0")" diff --git a/lxc/add_user.php b/lxc/add_user.php deleted file mode 100644 index d2b5b90..0000000 --- a/lxc/add_user.php +++ /dev/null @@ -1,11 +0,0 @@ - /dev/null 2>&1 || true"; -exec($cmd, $output, $ret); - -exit(0); -?> diff --git a/lxc/add_user.sh b/lxc/add_user.sh new file mode 100644 index 0000000..d7d1c0c --- /dev/null +++ b/lxc/add_user.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +CONTAINER=$1 +USERNAME=$2 + +lxc-attach -n "$CONTAINER" -- useradd "$USERNAME" -m -s /bin/bash > /dev/null 2>&1 || true diff --git a/lxc/ensure_mount.py b/lxc/ensure_mount.py deleted file mode 100644 index d4a0dd8..0000000 --- a/lxc/ensure_mount.py +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env python3 - -import subprocess -import argparse -import sys -import os - -def mount_container(name): - # Create mount point (/media/) - os.makedirs('/media/'+name, exist_ok=True) - - # Assume it's already mounted if things exist in the directory - if len(os.listdir('/media/'+name)) > 0: - return - - retcode = subprocess.call(['lvchange', '-ay', '/dev/pve/vm-'+name+'-disk-1']) - - if retcode != 0: - print("Error with lvchange", file=sys.stderr) - sys.exit(1) - - # Mount image file to /media/ if necessary - retcode = subprocess.call(['mount', - '/dev/pve/vm-'+name+'-disk-1', - '/media/'+name]) - if retcode != 0: - print("Error mounting container", file=sys.stderr) - sys.exit(1) - -if __name__ == '__main__': - # Set up argument parser - parser = argparse.ArgumentParser() - parser.add_argument('-n', '--name', - help='Name of container (ID)') - #read arguments - args = parser.parse_args() - - #Check for correct parameters - if not args.name: - print('You must provide the container name') - sys.exit(1) - - # Ensure container has been mounted - mount_container(args.name) diff --git a/lxc/execute_command.py b/lxc/execute_command.py deleted file mode 100644 index fdd845f..0000000 --- a/lxc/execute_command.py +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env python3 - -import subprocess -import sys - -if __name__ == '__main__': - args = sys.argv - cid = args[1] - command = args[2:] - retcode = subprocess.call(['lxc-attach', '-n', cid, '--'] + command) - sys.exit(retcode) diff --git a/lxc/load_credentials.php b/lxc/load_credentials.php deleted file mode 100644 index 2e82d6b..0000000 --- a/lxc/load_credentials.php +++ /dev/null @@ -1,15 +0,0 @@ - diff --git a/lxc/load_credentials.sh b/lxc/load_credentials.sh new file mode 100644 index 0000000..ade84a6 --- /dev/null +++ b/lxc/load_credentials.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +CONTAINER=$1 +USERNAME=$2 +PASSWORD=$3 + +lxc-attach -n "$CONTAINER" -- usermod -p "$(openssl passwd "$PASSWORD")" "$USERNAME" diff --git a/mitm.js b/mitm.js new file mode 100644 index 0000000..d54c17c --- /dev/null +++ b/mitm.js @@ -0,0 +1 @@ +require('./server'); diff --git a/package-lock.json b/package-lock.json index 8f769ed..502b455 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,8 +1,251 @@ { - "name": "mitm-ssh-server", + "name": "mitm", "version": "0.9.0", - "lockfileVersion": 1, + "lockfileVersion": 2, "requires": true, + "packages": { + "": { + "name": "mitm", + "version": "0.9.0", + "dependencies": { + "@idango/crypt3": "^1.0.0", + "commander": "^9.2.0", + "d3-random": "^1.1.0", + "fixedqueue": "0.0.1", + "locutus": "^2.0.16", + "moment": "^2.22.2", + "mysql": "^2.16.0", + "print-ascii": "0.0.2", + "seedrandom": "^2.4.3", + "ssh2": "^0.8.9", + "uuid": "^3.2.1" + }, + "engines": { + "node": "4.4.x", + "npm": "2.15.x" + } + }, + "node_modules/@idango/crypt3": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@idango/crypt3/-/crypt3-1.0.0.tgz", + "integrity": "sha512-urD2r6SD5xyl81I9CeQiw5hD78LF06ezqqgFyNOQ5OsJx3gXPMMa20TvTWxJnu+YbvcFq7ha0ku9EpvawI6G9w==", + "hasInstallScript": true, + "dependencies": { + "nan": "^2.14.1" + }, + "optionalDependencies": { + "q": "^1.0.1" + } + }, + "node_modules/asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/bignumber.js": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-7.2.1.tgz", + "integrity": "sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ==", + "engines": { + "node": "*" + } + }, + "node_modules/commander": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.2.0.tgz", + "integrity": "sha512-e2i4wANQiSXgnrBlIatyHtP1odfUp0BbV5Y5nEGbxtIrStkEOAAzCUirvLBNXHLr7kwLvJl6V+4V3XV9x7Wd9w==", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "node_modules/d3-random": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-1.1.0.tgz", + "integrity": "sha1-ZkLlBsb6OmSFldKyRpeIqNElKdM=" + }, + "node_modules/fixedqueue": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/fixedqueue/-/fixedqueue-0.0.1.tgz", + "integrity": "sha1-QMyCf0wtlsUtO5hOXIrL5Dszkps=" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "node_modules/locutus": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/locutus/-/locutus-2.0.16.tgz", + "integrity": "sha512-pGfl6Hb/1mXLzrX5kl5lH7gz25ey0vwQssZp8Qo2CEF59di6KrAgdFm+0pW8ghLnvNzzJGj5tlWhhv2QbK3jeQ==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/moment": { + "version": "2.22.2", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz", + "integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y=", + "engines": { + "node": "*" + } + }, + "node_modules/mysql": { + "version": "2.17.1", + "resolved": "https://registry.npmjs.org/mysql/-/mysql-2.17.1.tgz", + "integrity": "sha512-7vMqHQ673SAk5C8fOzTG2LpPcf3bNt0oL3sFpxPEEFp1mdlDcrLK0On7z8ZYKaaHrHwNcQ/MTUz7/oobZ2OyyA==", + "dependencies": { + "bignumber.js": "7.2.1", + "readable-stream": "2.3.6", + "safe-buffer": "5.1.2", + "sqlstring": "2.3.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nan": { + "version": "2.14.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", + "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==" + }, + "node_modules/print-ascii": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/print-ascii/-/print-ascii-0.0.2.tgz", + "integrity": "sha1-oSEDbitZDxLnalf8Y4eT9gwZ7xc=" + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "optional": true, + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, + "node_modules/readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/seedrandom": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-2.4.3.tgz", + "integrity": "sha1-JDhQTa0zkXMUv/GKxNeU8W1qrsw=" + }, + "node_modules/sqlstring": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz", + "integrity": "sha1-R1OT/56RR5rqYtyvDKPRSYOn+0A=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ssh2": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-0.8.9.tgz", + "integrity": "sha512-GmoNPxWDMkVpMFa9LVVzQZHF6EW3WKmBwL+4/GeILf2hFmix5Isxm7Amamo8o7bHiU0tC+wXsGcUXOxp8ChPaw==", + "dependencies": { + "ssh2-streams": "~0.4.10" + }, + "engines": { + "node": ">=5.2.0" + } + }, + "node_modules/ssh2-streams": { + "version": "0.4.10", + "resolved": "https://registry.npmjs.org/ssh2-streams/-/ssh2-streams-0.4.10.tgz", + "integrity": "sha512-8pnlMjvnIZJvmTzUIIA5nT4jr2ZWNNVHwyXfMGdRJbug9TpI3kd99ffglgfSWqujVv/0gxwMsDn9j9RVst8yhQ==", + "dependencies": { + "asn1": "~0.2.0", + "bcrypt-pbkdf": "^1.0.2", + "streamsearch": "~0.1.2" + }, + "engines": { + "node": ">=5.2.0" + } + }, + "node_modules/streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "node_modules/uuid": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", + "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "bin": { + "uuid": "bin/uuid" + } + } + }, "dependencies": { "@idango/crypt3": { "version": "1.0.0", @@ -34,6 +277,11 @@ "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-7.2.1.tgz", "integrity": "sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ==" }, + "commander": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.2.0.tgz", + "integrity": "sha512-e2i4wANQiSXgnrBlIatyHtP1odfUp0BbV5Y5nEGbxtIrStkEOAAzCUirvLBNXHLr7kwLvJl6V+4V3XV9x7Wd9w==" + }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -59,6 +307,11 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, + "locutus": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/locutus/-/locutus-2.0.16.tgz", + "integrity": "sha512-pGfl6Hb/1mXLzrX5kl5lH7gz25ey0vwQssZp8Qo2CEF59di6KrAgdFm+0pW8ghLnvNzzJGj5tlWhhv2QbK3jeQ==" + }, "moment": { "version": "2.22.2", "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz", diff --git a/package.json b/package.json index 65cc74c..c9a57eb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "mitm-ssh-server", - "description": "Man-in-the-middle SSH server", + "name": "mitm", + "description": "Man-in-the-Middle SSH server", "version": "0.9.0", "author": "Louis-Henri Merino (https://aces.umd.edu/)", "contributors": [ @@ -8,13 +8,14 @@ "Franz Payer" ], "engines": { - "node": "4.4.x", - "npm": "2.15.x" + "node": "16.x" }, "dependencies": { "@idango/crypt3": "^1.0.0", + "commander": "^9.2.0", "d3-random": "^1.1.0", "fixedqueue": "0.0.1", + "locutus": "^2.0.16", "moment": "^2.22.2", "mysql": "^2.16.0", "print-ascii": "0.0.2", diff --git a/mitm/defaultKey b/server/defaultKey similarity index 100% rename from mitm/defaultKey rename to server/defaultKey diff --git a/mitm/defaultKey.pub b/server/defaultKey.pub similarity index 100% rename from mitm/defaultKey.pub rename to server/defaultKey.pub diff --git a/mitm/index.js b/server/index.js similarity index 57% rename from mitm/index.js rename to server/index.js index c30dfd3..f5b22f4 100644 --- a/mitm/index.js +++ b/server/index.js @@ -1,32 +1,29 @@ -'use strict'; - /************************************************************************************ * ---------------------- Required Packages START Block ----------------------------- ************************************************************************************/ -let path = require('path'), - fs = require('fs'), - zlib = require('zlib'), - initialize = require('./keys'), - readline = require('readline'), - child_process = require('child_process'), - Stream = require('stream'), - ssh2 = require('ssh2'), - uuid = require('uuid'), - mysql = require('mysql'), - os = require('os'), - printAscii = require('print-ascii'), - d3_random = require("d3-random"), - seedrandom = require("seedrandom"), - moment = require('moment'), - fixedQueue = require('fixedqueue').FixedQueue, - crypt3 = require('@idango/crypt3/sync'); - -let config; -let version = 1.4; - -const {spawnSync} = require('child_process'); -const {execSync} = require('child_process'); +const path = require('path'), + fs = require('fs'), + zlib = require('zlib'), + initialize = require('./keys'), + readline = require('readline'), + child_process = require('child_process'), + Stream = require('stream'), + ssh2 = require('ssh2'), + uuid = require('uuid'), + os = require('os'), + printAscii = require('print-ascii'), + d3_random = require('d3-random'), + seedrandom = require('seedrandom'), + moment = require('moment'), + fixedQueue = require('fixedqueue').FixedQueue, + crypt3 = require('@idango/crypt3/sync'), + commander = require('commander'), + escapeShellArg = require('locutus/php/exec/escapeshellarg'); + +const version = 2; + +const { spawnSync, execSync } = child_process; /************************************************************************************ * ---------------------- Required Packages END Block ------------------------------- @@ -36,9 +33,6 @@ const {execSync} = require('child_process'); * ---------------------- MITM Global Variables START Block ------------------------- ************************************************************************************/ -// Critical Variables -let className, groupId, runID, containerIP, containerID, containerMountPath; - // Keep track of lxc streams let lxcStreams = [] @@ -51,145 +45,113 @@ let DEFAULT_KEYS = { PUBLIC: fs.readFileSync(path.resolve(__dirname, 'defaultKey.pub')), }; -// Automatic Access Variables -let autoAccess = false; -let autoBarrier = true; // false indicates that the barrier has been taken down so the login attempt will be successful. -let autoIPs; // Queue for the IPs -let autoRandomNormal = null; - -// MySQL Pool (Instructor Use) -let pool = null; - // Logging files -var loginAttempts, logins, delimiter = ';'; +let loginAttempts, logins, delimiter = ';'; /************************************************************************************ * ---------------------- MITM Global Variables END Block --------------------------- ************************************************************************************/ +commander.program + .option('-d, --debug', 'Debug mode', false) + .requiredOption('-n, --container-name ', 'Container name') + .requiredOption('-i, --container-ip ', 'Container internal IP address') + .requiredOption('-p, --mitm-port ', 'MITM server listening port', parseInt) + .option('-l, --mitm-ip ', 'MITM server listening ip address', '0.0.0.0') + .option('-a, --auto-access', 'Toggle to enable auto-access, must configure one of the auto-access strategies below') + .option('--auto-access-normal-distribution-mean ', 'Auto-Access Normal Distribution Strategy: Mean number of attempts before allowing attacker', parseInt) + .option('--auto-access-normal-distribution-std-dev ', 'Auto-Access Normal Distribution Strategy: Standard deviation from the mean to randomize', parseInt) + .option('--auto-access-fixed ', 'Auto-Access Fixed Strategy: Number of attempts before allowing attacker', parseInt) + .option('--auto-access-cache ', 'Size of the cache to track IP addresses', 5000) + .option('--max-attempts-per-connection ', 'Number of credential attempts to allow per single SSH connection', 6) + .option('--ssh-server-identifier ', 'SSH Server Identifier field to advertise to SSH clients', 'SSH-2.0-OpenSSH_6.6.1p1 Ubuntu-2ubuntu2') + .option('--ssh-server-banner-file ', 'File path to the SSH server banner to show SSH clients when they connect') + .option('--container-mount-path-prefix ', 'The base directory for where all containers are mounted', '/var/lib/lxc') + .option('--container-mount-path-suffix ', 'The sub directory name where the container file system is located', 'rootfs/') + .option('--logging-attacker-streams ', 'The directory to log all attacker session streams', path.resolve(__dirname, 'logs/session_streams')) + .option('--logging-authentication-attempts ', 'The directory to log all attacker authentication attempts', path.resolve(__dirname, 'logs/authentication_attempts')) + .option('--logging-logins ', 'The directory to log all successful attacker logins', path.resolve(__dirname, 'logs/logins')) +; + +const options = commander.program.parse(); + +const { + debug, + containerName, + containerIp, + mitmPort, + mitmIp, + autoAccess, + autoAccessNormalDistributionMean, + autoAccessNormalDistributionStdDev, + autoAccessEnabledCache, + maxAttemptsPerConnection, + sshServerIdentifier, + sshServerBannerFile, + containerMountPathPrefix, + containerMountPathSuffix, + loggingAttackerStreams, + loggingAuthenticationAttempts, + loggingLogins, +} = options; + /************************************************************************************ * ---------------------- Logging START Block --------------------------------------- ************************************************************************************/ -function debugLog(message, DBLog_ = true) { - if (config.debug) { - message = moment().format("YYYY-MM-DD HH:mm:ss.SSS") + ' - [Debug] ' + message; +function debugLog(message) { + if (debug) { + message = moment().format('YYYY-MM-DD HH:mm:ss.SSS') + ' - [Debug] ' + message; console.log(message); - - if (DBLog_) { - DBLog('debug', message); - } } } -function infoLog(message, DBLog_ = true) { - message = moment().format("YYYY-MM-DD HH:mm:ss.SSS") + ' - [Info] ' + message; +function infoLog(message) { + message = moment().format('YYYY-MM-DD HH:mm:ss.SSS') + ' - [Info] ' + message; console.log(message); - - if (DBLog_) { - DBLog('info', message); - } } -function errorLog(message, DBLog_ = true) { - message = moment().format("YYYY-MM-DD HH:mm:ss.SSS") + ' - [Error] ' + message; +function errorLog(message) { + message = moment().format('YYYY-MM-DD HH:mm:ss.SSS') + ' - [Error] ' + message; console.error(message); - - if (DBLog_) { - DBLog('error', message); - } } /************************************************************************************ * ---------------------- Logging END Block ----------------------------------------- ************************************************************************************/ -// argv[2] = Class_GroupID (e.g. HACS200_2A), argv[3] = Host MITM port, argv[4] = Container IP, argv[5] = Container ID, argv[6] = Force Enable/Disable Auto Access (Boolean), argv[7] = Specify MITM config file -if (!process.argv[2] || (!(process.argv[2] && process.argv[3] && process.argv[4]) && process.argv[5])) { - console.error('Usage: node %s [autoAccess] [config file]', path.basename(process.argv[1])); - process.exit(1); -} - -groupId = process.argv[2]; -containerIP = process.argv[4]; -containerID = process.argv[5]; - - -// Load MITM config file -if (process.argv[7]) { - config = require('../config/' + process.argv[7]); -} else { - config = require('../config/mitm.js'); -} - infoLog('MITM Version: ' + version); -// ------ Instructor Block START ------- -if (config.logToInstructor.enabled && (groupId.indexOf("HACS") === -1 || (groupId.split("_")).length !== 2)) { - errorLog("Incorrect Class_GroupID (e.g. HACS200_2A)"); - process.exit(1); -} -className = (groupId.split("_"))[0]; -groupId = (groupId.split("_"))[1]; -// ------ Instructor Block END --------- - -autoIPs = new fixedQueue(config.autoAccess.cacheSize); -containerMountPath = path.resolve(config.container.mountPath.prefix, process.argv[5], config.container.mountPath.suffix); - -// Load Auto Access value from the config file -autoAccess = config.autoAccess.enabled; -if (process.argv[6]) { - // Overwritten using the CLI - autoAccess = (process.argv[6] === 'true'); -} - -// Barrier active is autoAccess active -autoBarrier = autoAccess; +// Automatic Access Variables +let autoAccessEnabled = autoAccess; +let autoAccessThresholdAchieved = false; +let autoRandomNormal = null; +const autoIPs = autoAccessEnabled ? new fixedQueue(autoAccessEnabledCache) : null; +const containerMountPath = path.join(containerMountPathPrefix, containerName, containerMountPathSuffix); -infoLog("Auto Access Enabled: " + autoAccess); -debugLog("[Init] Auto Access Barrier: " + autoBarrier); +infoLog('Auto Access Enabled: ' + autoAccessEnabled); +debugLog('[Init] Auto Access Theshold Achieved: ' + autoAccessThresholdAchieved); // Set up Normal Distribution Random Generator if enabled -if (config.autoAccess.barrier.normalDist.enabled) { - autoRandomNormal = d3_random.randomNormal.source(seedrandom())( - config.autoAccess.barrier.normalDist.mean, config.autoAccess.barrier.normalDist.standardDeviation); -} else if (!config.autoAccess.barrier.fixed.enabled) { - errorLog("[Auto Access] Auto Access is enabled but none of the barriers are!"); - process.exit(); -} - -// ------ Instructor Block ----- -if (config.logToInstructor.enabled) { - pool = mysql.createPool({ - connectionLimit : config.logToInstructor.connectionLimit, - host : config.logToInstructor.host, - user : config.logToInstructor.user, - password : config.logToInstructor.password, - database : config.logToInstructor.database - }) +if (autoAccessNormalDistributionMean >= 0) { + autoRandomNormal = d3_random.randomNormal.source(seedrandom())(autoAccessNormalDistributionMean, autoAccessNormalDistributionStdDev); +} else if (!(autoAccessFixed >= 0)) { + commander.program.help(); + console.log('[Auto Access] Auto Access is enabled but none of the threshold settings are configured'); + process.exit(1); } -// ------ Instructor Block ----- - -// Do not do the following locally -if (config.local === false) { - // Mount container if required - //execSync("python3 " + path.resolve(__dirname, '../lxc/ensure_mount.py') + " -n " + containerID + "", (error, stdout, stderr) => {}); - //spawnSync("python3", [path.resolve(__dirname, '../lxc/ensure_mount.py'), "-n", containerID]); +// makes the attacker session screen output folder if not already created +initialize.makeOutputFolder(loggingAttackerStreams); +initialize.makeOutputFolder(loggingAuthenticationAttempts); +initialize.makeOutputFolder(loggingLogins); - - // makes the attacker session screen output folder if not already created - initialize.makeOutputFolder(config.logging.streamOutput); - initialize.makeOutputFolder(config.logging.loginAttempts); - initialize.makeOutputFolder(config.logging.logins); - - loginAttempts = fs.createWriteStream(path.resolve(config.logging.loginAttempts, containerID + ".txt"), {flags:'a'}); - logins = fs.createWriteStream(path.resolve(config.logging.logins, containerID + ".txt"), {flags:'a'}); -} +loginAttempts = fs.createWriteStream(path.resolve(loggingAuthenticationAttempts, containerName + '.txt'), { flags:'a' }); +logins = fs.createWriteStream(path.resolve(loggingLogins, containerName + '.txt'), { flags:'a' }); // loads private and public keys from container if possible -initialize.loadKeys(containerMountPath, containerID, function (hostKeys) { - startServer(hostKeys, parseInt(process.argv[3])); +initialize.loadKeys(containerMountPath, containerName, function (hostKeys) { + startServer(hostKeys, mitmPort); }); /** @@ -204,26 +166,19 @@ function startServer(hostKeys, port) { // Initialize the SSH server. Upon receiving a connection, handleAttackerConnection function will be called let banner = ''; - if (config.server.banner !== '') { - banner = fs.readFileSync(config.server.banner, "utf8"); + if (sshServerBannerFile) { + banner = fs.readFileSync(path.resolve(sshServerBannerFile), 'utf8'); } let server = new ssh2.Server({ hostKeys: hostKeys, - ident: config.server.identifier, // Identifier sent to the client - banner: banner + ident: sshServerIdentifier, // Identifier sent to the client + banner, }, handleAttackerConnection); // Bind SSH server to IP address and port - server.listen(port, config.server.listenIP, function () { // function called when the server has successfully set up - infoLog('SSH man-in-the-middle server for ' + containerIP + ' listening on ' + - config.server.listenIP + ':' + this.address().port); - - // ---- Instructor Block START ------- - if (config.logToInstructor.enabled) { - logStartMITM(port); - } - // ---- Instructor Block END --------- + server.listen(port, mitmIp, function () { // function called when the server has successfully set up + infoLog('SSH man-in-the-middle server for ' + containerIp + ' listening on ' + mitmIp + ':' + this.address().port); }); } @@ -251,13 +206,13 @@ function handleAttackerConnection(attacker, info) { // Sanity check if (attacker._sock._peername === undefined || attacker._sock._peername === null || info.ip === null) { - debugLog("[Connection] Socket Error"); + debugLog('[Connection] Socket Error'); return; } // Get the IP address of the attacker (the client end of the connection) let ipAddress = info.ip; - debugLog('[Connection] Attacker connected: ' + ipAddress + " | Client Identification: " + info.header.identRaw); + debugLog('[Connection] Attacker connected: ' + ipAddress + ' | Client Identification: ' + info.header.identRaw); // When attacker exits before he or she has authenticated attacker.on('end', attackerEndBeforeAuthenticated); @@ -269,7 +224,7 @@ function handleAttackerConnection(attacker, info) { attacker.ipAddress = ipAddress; // Handle Attacker Authentication method. handleAttackerAuthCallback is called when the - // the function handleAttackerAuth calls it using "cb(param1, param2, etc...)" + // the function handleAttackerAuth calls it using 'cb(param1, param2, etc...)' handleAttackerAuth(attacker, handleAttackerAuthCallback); } @@ -279,7 +234,7 @@ function handleAttackerConnection(attacker, info) { * */ function attackerEndBeforeAuthenticated() { - debugLog("[Connection] Attacker closed the connection"); + debugLog('[Connection] Attacker closed the connection'); } /** @@ -292,54 +247,51 @@ function attackerEndBeforeAuthenticated() { */ function handleAttackerAuth(attacker, cb) { - // Binds the "authentication" event to the attacker object. Now, whenever the attacker tries to authenticate, this + // Binds the 'authentication' event to the attacker object. Now, whenever the attacker tries to authenticate, this // anonymous function will be called. attacker.on('authentication', function (ctx) { - debugLog('[Auth] Attacker ' + attacker.ipAddress + " trying to authenticate with \"" + ctx.method + "\""); - - // Logging to instructor DB - logLoginAttempt(attacker, ctx); + debugLog('[Auth] Attacker ' + attacker.ipAddress + ' trying to authenticate with \'' + ctx.method + '\''); if (ctx.method === 'password' && ctx.username) { - // The attacker is trying to authenticate using the "password" authentication method + // The attacker is trying to authenticate using the 'password' authentication method // Logging to student file - loginAttempts.write(moment().format("YYYY-MM-DD HH:mm:ss.SSS") + delimiter + attacker.ipAddress + delimiter + - ctx.method + delimiter + ctx.username + delimiter + ctx.password + "\n"); + loginAttempts.write(moment().format('YYYY-MM-DD HH:mm:ss.SSS') + delimiter + attacker.ipAddress + delimiter + + ctx.method + delimiter + ctx.username + delimiter + ctx.password + '\n'); // ----------- Automatic Access START Block -------------- // Handle Attempt if automatic access is enabled - if (autoAccess === true && autoBarrier === true) { + if (autoAccessEnabled && !autoAccessThresholdAchieved) { handleAttempt(attacker); } // If automatic access is enabled and the barrier is down, then compromise the honeypot by // adding the user to the container if it does not exist and modifying the password for // specified user supplied by the attacker (ctx.username) - if (autoAccess === true && autoBarrier === false && ctx.username != '' && ctx.password != '') { - autoAccess = false; + if (autoAccessEnabled && autoAccessThresholdAchieved && ctx.username != '' && ctx.password != '') { + autoAccessEnabled = false; - debugLog("[Auto Access] Compromising the honeypot"); + debugLog('[Auto Access] Compromising the honeypot'); - debugLog("[Auto Access] Adding the following credentials: \"" - + ctx.username + ":" + ctx.password +"\""); + debugLog('[Auto Access] Adding the following credentials: \'' + + ctx.username + ':' + ctx.password +'\''); - ctx.username = ctx.username.replace(';', '').replace("'", ''); // Preliminary Caution - ctx.password = ctx.password.replace(';', '').replace("'", ''); // Preliminary Caution + ctx.username = ctx.username.replace(';', '').replace(`'`, ''); // Preliminary Caution + ctx.password = ctx.password.replace(';', '').replace(`'`, ''); // Preliminary Caution // Add user to the container if it does not exist // Not successful if the user tries to do command injection // SpawnSync and php script handles command injection - spawnSync("php", [path.resolve(__dirname, '../lxc/add_user.php'), containerID, ctx.username]); + spawnSync('bash', [ path.join(__dirname, '../lxc/add_user.sh'), containerName, escapeShellArg(ctx.username) ]); // Load the credentials // Again not successful if the attacker uses command injection - spawnSync("php", [path.resolve(__dirname, '../lxc/load_credentials.php'), containerID, ctx.username, ctx.password]); + spawnSync('bash', [ path.join(__dirname, '../lxc/load_credentials.sh'), containerName, escapeShellArg(ctx.username), escapeShellArg(ctx.password.replace(/`/g, '')) ]); - } else if (autoAccess === true && autoBarrier === true) { + } else if (autoAccessEnabled === true && !autoAccessThresholdAchieved === true) { // Barrier has not yet been broken - cb("Not yet compromised", null, ctx, attacker); + cb('Not yet compromised', null, ctx, attacker); return; } @@ -350,38 +302,38 @@ function handleAttackerAuth(attacker, cb) { let passwordEntry = getPassEntry(ctx.username); - //debugLog("[Auth] Password Field on container: " + passwordEntry); + //debugLog('[Auth] Password Field on container: ' + passwordEntry); if (passwordEntry === null) { - cb("Invalid credentials - User does not exist", undefined, ctx, attacker); + cb('Invalid credentials - User does not exist', undefined, ctx, attacker); return; } if (passwordEntry === '*' || passwordEntry === '!') { - cb("Invalid credentials - Container user is disabled", undefined, ctx, attacker); + cb('Invalid credentials - Container user is disabled', undefined, ctx, attacker); return; } try { if (crypt3(ctx.password, passwordEntry) !== passwordEntry) { - cb("Invalid credentials - Password Authentication Failure", undefined, ctx, attacker); + cb('Invalid credentials - Password Authentication Failure', undefined, ctx, attacker); return; } else if (crypt3(ctx.password, passwordEntry) === passwordEntry) { - debugLog("[Auth] Valid credentials - Password Authentication"); + debugLog('[Auth] Valid credentials - Password Authentication'); } } catch(err) { // If authentication threw an exception - debugLog("[Auth] Exception thrown by crypt: " + err); + debugLog('[Auth] Exception thrown by crypt: ' + err); } // ----------- END Preliminary Authentication -------------- // Preliminary Authentication is successful, let's try to login using the attacker's credentials // Note: It may still fail because of the settings (/etc/ssh/sshd_config) that are put on the container SSH server - debugLog('[LXC] Attempting to connect to the honeypot: ' + containerIP); + debugLog('[LXC] Attempting to connect to the honeypot: ' + containerIp); connectToLXC({ - host: containerIP, + host: containerIp, port: 22, username: ctx.username, password: ctx.password @@ -390,7 +342,7 @@ function handleAttackerAuth(attacker, cb) { if (err.toString().indexOf('EHOSTUNREACH') !== -1) { errorLog('[LXC] Cannot reach the container!'); } else if (err.toString() === 'Error: All configured authentication methods failed') { - debugLog("[LXC] Authentication Failed"); + debugLog('[LXC] Authentication Failed'); } cb(err.toString(), lxc, ctx, attacker); @@ -398,13 +350,12 @@ function handleAttackerAuth(attacker, cb) { cb(err, lxc, ctx, attacker); }); - } else if (ctx.method === 'publickey' && config.local === false) { - // Cannot fetch public keys from container when container does not exist (config.local = true) - // The attacker is trying to authenticate using the "publickey" authentication method + } else if (ctx.method === 'publickey') { + // The attacker is trying to authenticate using the 'publickey' authentication method // Logging to student file - loginAttempts.write(moment().format("YYYY-MM-DD HH:mm:ss.SSS") + delimiter + attacker.ipAddress + delimiter + - ctx.method + delimiter + ctx.username + delimiter + ctx.key.data.toString('base64') + "\n"); + loginAttempts.write(moment().format('YYYY-MM-DD HH:mm:ss.SSS') + delimiter + attacker.ipAddress + delimiter + + ctx.method + delimiter + ctx.username + delimiter + ctx.key.data.toString('base64') + '\n'); // Verify that the public key sent by the attacker matches one of the public keys in the // ~/.ssh/authorized_keys. Note: ~ is the home directory of the supplied username @@ -421,12 +372,12 @@ function handleAttackerAuth(attacker, cb) { // access to the honeypot system for the attacker. insertAuthKeys(homeDir, DEFAULT_KEYS.PUBLIC); connectToLXC({ - host: containerIP, + host: containerIp, port: 22, username: ctx.username, key: DEFAULT_KEYS.PRIVATE, }, function (err, lxc) { // function called after the login attempt to the container - // Once we have successfully connected, restore the original "authorized_keys" file + // Once we have successfully connected, restore the original 'authorized_keys' file setAuthKeys(homeDir, origAuthKeys); // Set the time back to make it look like we didn't work with this file setFileTimes(authKeysPath, stats.atime, stats.mtime); @@ -434,16 +385,16 @@ function handleAttackerAuth(attacker, cb) { }); } else { - cb("Publickey authentication failed", undefined, ctx, attacker); + cb('Publickey authentication failed', undefined, ctx, attacker); } - } else if (ctx.method === "keyboard-interactive") { + } else if (ctx.method === 'keyboard-interactive') { // Reject keyboard-interactive authentication. - // This SSH server can simply do "password" and "publickey" authentication - cb("Keyboard-interactive is not supported", undefined, ctx, attacker); - } else if (ctx.method === "none") { + // This SSH server can simply do 'password' and 'publickey' authentication + cb('Keyboard-interactive is not supported', undefined, ctx, attacker); + } else if (ctx.method === 'none') { // Clients use this authentication method to determine the available authentication methods on the SSH server // since the SSH server will reject the response with the available authentication methods. - cb("No authentication method provided", undefined, ctx, attacker); + cb('No authentication method provided', undefined, ctx, attacker); } else { // ??? What is this attacker trying to do? cb('Unknown authentication method', undefined, ctx, attacker); @@ -452,12 +403,11 @@ function handleAttackerAuth(attacker, cb) { } /** - * Used when autoAccess is enabled. Determines if the attacker is allowed automatic access to the honeypot + * Used when autoAccessEnabled is enabled. Determines if the attacker is allowed automatic access to the honeypot * @param attacker */ function handleAttempt(attacker) { - // If autoAccess is disabled, what are we doing here? - if (autoAccess === false) { + if (!autoAccessEnabled) { return; } @@ -478,17 +428,17 @@ function handleAttempt(attacker) { let randomAllowCalculation = null; // Normal Distribution Barrier - if (config.autoAccess.barrier.normalDist.enabled) { + if (autoAccessNormalDistributionMean >= 0) { randomAllowCalculation = Math.round(autoRandomNormal()); } // Fixed Number of Attempts Barrier - else if (config.autoAccess.barrier.fixed.enabled) { - randomAllowCalculation = config.autoAccess.barrier.fixed.attempts; + else if (autoAccessFixed >= 0) { + randomAllowCalculation = autoAccessFixed; } // No way to calculate randomAllow... else { - errorLog("[Auto Access] Unknown calculation for randomAllow!"); + errorLog('[Auto Access] Unknown calculation for randomAllow!'); randomAllowCalculation = Number.MAX_VALUE; } @@ -510,10 +460,10 @@ function handleAttempt(attacker) { // If the number of attempts is greater than or equal to the set threshold for this attacker if (previouslySeen.attempts >= previouslySeen.randomAllow) { - autoBarrier = false; + autoAccessThresholdAchieved = true; } - debugLog("[Auto Access] Attacker: " + ipAddress + ", Threshold: " + previouslySeen.randomAllow + ", Attempts: " + previouslySeen.attempts); + debugLog('[Auto Access] Attacker: ' + ipAddress + ', Threshold: ' + previouslySeen.randomAllow + ', Attempts: ' + previouslySeen.attempts); } @@ -534,18 +484,18 @@ function handleAttackerAuthCallback(err, lxc, authCtx, attacker) { // -------- Attacker Limit Number of Attempts per Connection START ------------ - // If the authentication method was not "none", then increment the login attempts count + // If the authentication method was not 'none', then increment the login attempts count if (authCtx.method !== 'none') { attacker.numberOfAttempts++; - debugLog("[Auth] Attacker: " + attacker.ipAddress + " has so far made " + attacker.numberOfAttempts + - " attempts. Remaining: " + - (config.server.maxAttemptsPerConnection - attacker.numberOfAttempts) + " attempts"); + debugLog('[Auth] Attacker: ' + attacker.ipAddress + ' has so far made ' + attacker.numberOfAttempts + + ' attempts. Remaining: ' + + (maxAttemptsPerConnection - attacker.numberOfAttempts) + ' attempts'); } // If the number of attempts for this attacker connection is equal to // the maximum number of attempts allowed per connection, then close the connection on the attacker - if (attacker.numberOfAttempts === config.server.maxAttemptsPerConnection) { - debugLog("[Connection] Max Login Attempts Reached - Closing connection on attacker"); + if (attacker.numberOfAttempts === maxAttemptsPerConnection) { + debugLog('[Connection] Max Login Attempts Reached - Closing connection on attacker'); attacker.end(); } @@ -560,9 +510,7 @@ function handleAttackerAuthCallback(err, lxc, authCtx, attacker) { let sessionId = uuid.v1(); // assign UUID // make a session screen output stream - let screenWriteOutputStream = fs.createWriteStream( - path.resolve(config.logging.streamOutput, sessionId + '.gz') - ); + let screenWriteOutputStream = fs.createWriteStream(path.resolve(loggingAttackerStreams, sessionId + '.gz')); // Make a Gzip handler to automatically compress the file on the fly let screenWriteGZIP = zlib.createGzip({ @@ -570,9 +518,9 @@ function handleAttackerAuthCallback(err, lxc, authCtx, attacker) { }); screenWriteGZIP.pipe(screenWriteOutputStream); - /*let year = dateTime.getFullYear(), month = ("0" + dateTime.getMonth()).slice(-2), - date = ("0" + dateTime.getDate()).slice(-2), hour = ("0" + dateTime.getHours()).slice(-2), - minutes = ("0" + dateTime.getMinutes()).slice(-2), seconds = ("0" + dateTime.getSeconds()).slice(-2), + /*let year = dateTime.getFullYear(), month = ('0' + dateTime.getMonth()).slice(-2), + date = ('0' + dateTime.getDate()).slice(-2), hour = ('0' + dateTime.getHours()).slice(-2), + minutes = ('0' + dateTime.getMinutes()).slice(-2), seconds = ('0' + dateTime.getSeconds()).slice(-2), milliseconds = dateTime.getMilliseconds();*/ let credential = null; @@ -583,27 +531,24 @@ function handleAttackerAuthCallback(err, lxc, authCtx, attacker) { credential = authCtx.key.data.toString('base64'); } - let metadata = containerIP + '_' + containerID + "_" + attacker.ipAddress + "_" + - moment().format("YYYY_MM_DD_HH_mm_ss_SSS") + "_" + sessionId + "\n" + - "Container SSH Server: " + containerIP + "\n" + - "Container ID: " + containerID + "\n" + - "Attacker IP Address: " + attacker.ipAddress + "\n" + - "Login Method: " + authCtx.method + "\n" + - "Attacker Username: " + authCtx.username + "\n" + - "Attacker Password: " + credential + "\n" + - "Date: " + moment().format("YYYY-MM-DD HH:mm:ss.SSS") + "\n" + - "Session ID: " + sessionId + "\n" + - "-------- Attacker Stream Below ---------\n"; - - let metadataBuffer = new Buffer.from(metadata, "utf-8"); + let metadata = containerIp + '_' + containerName + '_' + attacker.ipAddress + '_' + + moment().format('YYYY_MM_DD_HH_mm_ss_SSS') + '_' + sessionId + '\n' + + 'Container SSH Server: ' + containerIp + '\n' + + 'Container Name: ' + containerName + '\n' + + 'Attacker IP Address: ' + attacker.ipAddress + '\n' + + 'Login Method: ' + authCtx.method + '\n' + + 'Attacker Username: ' + authCtx.username + '\n' + + 'Attacker Password: ' + credential + '\n' + + 'Date: ' + moment().format('YYYY-MM-DD HH:mm:ss.SSS') + '\n' + + 'Session ID: ' + sessionId + '\n' + + '-------- Attacker Stream Below ---------\n'; + + let metadataBuffer = new Buffer.from(metadata, 'utf-8'); screenWriteGZIP.write(metadataBuffer); - // Log to instructor DB - logLogin(attacker, authCtx, sessionId); - // Log to student file - logins.write(moment().format("YYYY-MM-DD HH:mm:ss.SSS") + delimiter + attacker.ipAddress + delimiter + - sessionId + "\n"); + logins.write(moment().format('YYYY-MM-DD HH:mm:ss.SSS') + delimiter + attacker.ipAddress + delimiter + + sessionId + '\n'); attacker.once('session', function (accept) { let session = accept(); @@ -670,7 +615,7 @@ function handleAttackerSession(attacker, lxc, sessionId, screenWriteStream) { let execStatement = 'Noninteractive mode attacker command: ' + info.command + '\n--------- Output Below -------\n'; - let execStatementBuffer = new Buffer.from(execStatement, "utf-8"); + let execStatementBuffer = new Buffer.from(execStatement, 'utf-8'); screenWriteStream.write(execStatementBuffer); lxc.exec(info.command, function (err, lxcStream) { @@ -731,7 +676,7 @@ function handleAttackerSession(attacker, lxc, sessionId, screenWriteStream) { }); attackerStream.on('data', function (data) { debugLog('[SHELL] Attacker Keystroke: ' + printAscii(data.toString())); - keystrokeFullBuffer += moment().format('YYYY-MM-DD HH:mm:ss.SSS') + ': ' + printAscii(data.toString()) + "\n"; + keystrokeFullBuffer += moment().format('YYYY-MM-DD HH:mm:ss.SSS') + ': ' + printAscii(data.toString()) + '\n'; lxcStream.write(data); // record all char code of keystrokes @@ -752,7 +697,7 @@ function handleAttackerSession(attacker, lxc, sessionId, screenWriteStream) { debugLog('[SHELL] Attacker ended the shell'); // Keystroke Writing - screenWriteStream.write("-------- Attacker Keystrokes ----------\n"); + screenWriteStream.write('-------- Attacker Keystrokes ----------\n'); screenWriteStream.write(keystrokeFullBuffer); lxcStream.end(); }); @@ -761,7 +706,7 @@ function handleAttackerSession(attacker, lxc, sessionId, screenWriteStream) { let position = lxcStreams.indexOf(lxcStream); if (position > -1) { lxcStreams.splice(position, 1); - debugLog("[LXC Streams] Removed Stream | Total streams: " + lxcStreams.length); + debugLog('[LXC Streams] Removed Stream | Total streams: ' + lxcStreams.length); } debugLog('[SHELL] Honeypot ended shell'); attackerStream.end(); @@ -769,7 +714,7 @@ function handleAttackerSession(attacker, lxc, sessionId, screenWriteStream) { // Keep track of LXC Streams lxcStreams.push(lxcStream); - debugLog("[LXC Streams] New Stream | Total Streams: " + lxcStreams.length); + debugLog('[LXC Streams] New Stream | Total Streams: ' + lxcStreams.length); }); }); } @@ -810,7 +755,7 @@ function connectToLXC(opts, cb) { } lxc.on('ready', function () { // allow authenticate - autoAccess = false; // Attacker is successfully getting inside the container + autoAccessEnabled = false; // Attacker is successfully getting inside the container return cb(undefined, lxc); }); @@ -971,268 +916,42 @@ function setFileTimes(file, atime, mtime) { * ----------------------- Authentication END Block --------------------------------- ************************************************************************************/ -/************************************************************************************ - * ----------- Do NOT modify anything below - Instructor Use ------------------------ - ************************************************************************************/ - -/************************************************************************************ - * ----------- Do NOT modify anything below - Instructor Use ------------------------ - ************************************************************************************/ - -/************************************************************************************ - * ----------- Do NOT modify anything below - Instructor Use ------------------------ - ************************************************************************************/ - -/************************************************************************************ - * ----------- Do NOT modify anything below - Instructor Use ------------------------ - ************************************************************************************/ - -function getNetworkInterfaceDetails() { - let networkInterfaces = os.networkInterfaces(); - let interfaceDetailsShort = {}; - - // Iterate through each interface name - Object.keys(networkInterfaces).forEach(function(interfaceName) { - - // Iterate through each interface address - networkInterfaces[interfaceName].forEach(function(interface_) { - - // If interface is not IPv4 and/or is internal - if (interface_.family !== 'IPv4' || interface_.internal !== false) { - return; - } - - if (interfaceDetailsShort[interfaceName] === undefined) { - interfaceDetailsShort[interfaceName] = [{ - 'cidr' : interface_.cidr, - 'mac' : interface_.mac, - }] - } - else - { - interfaceDetailsShort[interfaceName].push({ - 'cidr' : interface_.cidr, - 'mac' : interface_.mac, - }); - } - }); - }); - - return interfaceDetailsShort; -} - - -/** - * Logs Group ID, Destination Server - * @param port - */ -function logStartMITM(port) { - if (pool === null) { - errorLog("DB connection failed - contact an instructor or a TA"); - process.exit(); - } - - let networkInterfaceDetails = getNetworkInterfaceDetails(); - - let query = 'INSERT INTO ' + - 'mitm_start(class_name, group_id, host_interfaces, mitm_listen_ip, mitm_port, auto_access, auto_access_details, container_id, container_ip, container_mount, started_at)' + - 'VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; - - let values = [ - className, - groupId, - JSON.stringify(networkInterfaceDetails), - config.server.listenIP, - port, - autoAccess, - JSON.stringify(config.autoAccess), - containerID, - containerIP, - containerMountPath, - moment().format("YYYY-MM-DD HH:mm:ss.SSS") - ]; - - pool.query(query, values, function(error, result) { - if (error) { - errorLog("The following is a DB Error, please contact an instructor or a TA: ", false); - errorLog(error, false); - return; - } - - runID = result.insertId; - debugLog("Your session ID: " + runID); - }); -} - -function DBLog(type, message) { - // Error checking - if (pool === null || !runID) { - return; - } - - let query = 'INSERT INTO ' + - 'mitm_log(mitm_start_id, type, message)' + - 'VALUES(?, ?, ?)'; - - let values = [ - runID, - type, - message - ]; - - logToDB(query, values); -} - - -function logLoginAttempt(attacker, ctx) { - // Error checking - if (pool === null || !runID) { - return; - } - - let query = 'INSERT INTO ' + - 'mitm_login_attempts(mitm_start_id, attacker_ip, method, username, password, public_key, number_of_attempts, attempted_at) ' + - 'VALUES(?, ?, ?, ?, ?, ?, ?,?)'; - - let password = null; - let publicKey = null; - - if (ctx.method === 'password') { - password = ctx.password; - } else if (ctx.method === 'publickey') { - publicKey = ctx.key.data.toString('base64'); - } - - let values = [ - runID, - attacker.ipAddress, - ctx.method, - ctx.username, - password, - publicKey, - attacker.numberOfAttempts, - moment().format("YYYY-MM-DD HH:mm:ss.SSS"), - ]; - - logToDB(query, values); -} - -function logLogin(attacker, ctx, sessionId) { - // Error checking - if (pool === null || !runID) { - return; - } - - let query = 'INSERT INTO ' + - 'mitm_logins(mitm_start_id, attacker_ip, session_id, method, username, password, public_key, number_of_attempts, login_at) ' + - 'VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)'; - - let password = null; - let publicKey = null; - - if (ctx.method === 'password') { - password = ctx.password; - } else if (ctx.method === 'publickey') { - publicKey = ctx.key.data.toString('base64'); - } - - let values = [ - runID, - attacker.ipAddress, - sessionId, - ctx.method, - ctx.username, - password, - publicKey, - attacker.numberOfAttempts, - moment().format("YYYY-MM-DD HH:mm:ss.SSS"), - ]; - - logToDB(query, values); -} - - -function logToDB(query, values) { - pool.query(query, values, function(error) { - if (error) { - errorLog("The following is a DB Error, please contact an instructor or a TA: ", false); - errorLog(error, false); - } - }); -} - // Some housekeeping on exit process.on('exit', function() { - housekeeping("exit"); + housekeeping('exit'); }); process.on('SIGINT', function() { - housekeeping("SIGINT"); + housekeeping('SIGINT'); }); process.on('SIGUSR1', function() { - housekeeping("SIGUSR1"); + housekeeping('SIGUSR1'); }); process.on('SIGUSR2', function() { - housekeeping("SIGUSR2"); + housekeeping('SIGUSR2'); }); process.on('SIGTERM', function() { - housekeeping("SIGTERM"); + housekeeping('SIGTERM'); }); process.on('uncaughtException', function(err) { - housekeeping("UncaughtException", err.message) + housekeeping('UncaughtException', err.message) }); function housekeeping(type, details = null) { if (cleanup === false) { - infoLog("Exiting..."); + infoLog('Exiting...'); cleanup = true; - debugLog("Cleaning up...", false); + debugLog('Cleaning up...', false); if (details !== null) { - errorLog("Exception occurred: ", false); + errorLog('Exception occurred: ', false); console.log(details); } // Cleanup open LXC Streams - debugLog("Cleaning up LXC Streams: " + lxcStreams.length); + debugLog('Cleaning up LXC Streams: ' + lxcStreams.length); lxcStreams.forEach(function(lxcStream) { lxcStream.close(); }); - - setTimeout(function() { - cleanupPool(type, details, function() { - process.exit(); - logins.end(); - loginAttempts.end(); - })}, 1000); - } -} - - -function cleanupPool(type, details, cb) { - if (pool === null) { - cb(); - return; } - - let query = 'INSERT INTO ' + - 'mitm_stop (mitm_start_id, exit_type, exit_details, stopped_at) ' + - 'VALUES(?, ?, ?, ?)'; - - let values = [ - runID, - type, - details, - moment().format("YYYY-MM-DD HH:mm:ss.SSS"), - ]; - - pool.query(query, values, function(error) { - if (error) { - errorLog("DB Error:" + error); - } - - pool.end(function() { - cb(); - }); - }); } diff --git a/mitm/keys.js b/server/keys.js similarity index 87% rename from mitm/keys.js rename to server/keys.js index b6ca229..26fc48b 100644 --- a/mitm/keys.js +++ b/server/keys.js @@ -16,7 +16,7 @@ function readDefaultKeys() { } function readKeys(filename) { - const key = fs.readFileSync(path.resolve(__dirname, filename)); + const key = fs.readFileSync(path.join(__dirname, filename)); return key; } @@ -25,7 +25,7 @@ function readCTKeys(mountPath, ctID) { let keys = []; for (let i = 0; i < keyLocations.length; i++) { - const targetPath = path.resolve(mountPath, keyLocations[i]); + const targetPath = path.join(mountPath, keyLocations[i]); keys[i] = readKeys(targetPath); } @@ -68,8 +68,9 @@ function loadKeys(mountPath, ctID, cb) { * @throws {Error} - if mkdirSync fails */ function makeOutputFolder(pathname) { - if (!fs.existsSync(path.resolve(pathname))) { - fs.mkdirSync(path.resolve(pathname), { recursive: true }); + const dir = path.join(pathname); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); } }