├── .gitignore ├── .landscape.yaml ├── .style.yapf ├── .travis.yml ├── .yapfignore ├── COPYING.txt ├── Dockerfile ├── INSTALL.md ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── bin ├── __init__.py └── heralding ├── changelog.txt ├── ez_setup.py ├── heralding ├── __init__.py ├── capabilities │ ├── __init__.py │ ├── ftp.py │ ├── handlerbase.py │ ├── http.py │ ├── https.py │ ├── imap.py │ ├── imaps.py │ ├── mysql.py │ ├── pop3.py │ ├── pop3s.py │ ├── postgresql.py │ ├── rdp.py │ ├── smtp.py │ ├── smtps.py │ ├── socks5.py │ ├── ssh.py │ ├── telnet.py │ └── vnc.py ├── heralding.yml ├── honeypot.py ├── libs │ ├── __init__.py │ ├── aiobaserequest.py │ ├── cracker │ │ └── vnc.py │ ├── http │ │ ├── __init__.py │ │ ├── aioclient.py │ │ └── aioserver.py │ ├── msrdp │ │ ├── __init__.py │ │ ├── packer.py │ │ ├── parser.py │ │ ├── pdu.py │ │ ├── security.py │ │ └── tls.py │ └── telnetsrv │ │ ├── __init__.py │ │ └── telnetsrvlib.py ├── misc │ ├── __init__.py │ ├── common.py │ ├── session.py │ └── socket_names.py ├── reporting │ ├── __init__.py │ ├── base_logger.py │ ├── curiosum_integration.py │ ├── file_logger.py │ ├── hpfeeds_logger.py │ ├── reporting_relay.py │ └── syslog_logger.py ├── tests │ ├── __init__.py │ ├── test_config.py │ ├── test_encoding.py │ ├── test_ftp.py │ ├── test_hpfeeds.py │ ├── test_http.py │ ├── test_imap.py │ ├── test_mysql.py │ ├── test_pop3.py │ ├── test_postgresql.py │ ├── test_rdp.py │ ├── test_smtp.py │ ├── test_socks5.py │ ├── test_ssh.py │ ├── test_telnet.py │ └── test_vnc.py └── wordlist.txt ├── requirements-test.txt ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | 3 | .idea 4 | .idea/libraries 5 | .vscode 6 | .eggs 7 | 8 | # Packages 9 | *.egg 10 | *.egg-info 11 | dist 12 | build 13 | eggs 14 | parts 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | 19 | # Installer logs 20 | pip-log.txt 21 | 22 | # Unit test / coverage reports 23 | .coverage 24 | .tox 25 | 26 | #Translations 27 | *.mo 28 | 29 | #Mr Developer 30 | .mr.developer.cfg 31 | 32 | .DS_store 33 | *.db 34 | server.crt 35 | server.key 36 | 37 | beeswarm.cfg 38 | 39 | *.log 40 | *.key 41 | #pydev 42 | .project 43 | .pydevproject 44 | 45 | # exceptions for vfs files 46 | !honeypot/data/vfs/* 47 | 48 | *.gz 49 | 50 | docs/_build 51 | .noseids 52 | admin_passwd_hash 53 | *.pem 54 | *.csv 55 | 56 | venv/* 57 | heralding/heralding.yml 58 | -------------------------------------------------------------------------------- /.landscape.yaml: -------------------------------------------------------------------------------- 1 | strictness: medium 2 | autodetect: yes 3 | 4 | ignore-paths: 5 | - ez_setup.py 6 | 7 | pylint: 8 | disable: 9 | - W1202 -------------------------------------------------------------------------------- /.style.yapf: -------------------------------------------------------------------------------- 1 | 2 | [style] 3 | # YAPF uses the chromium style 4 | based_on_style = chromium 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: python 3 | python: 4 | - 3.6.3 5 | - 3.6.5 6 | - 3.6.7 7 | - 3.7.1 8 | - 3.7.2 9 | - 3.8.0 10 | before_install: 11 | - rvm install 2.2.5 12 | install: 13 | - pip3 install --upgrade setuptools 14 | - pip3 install coveralls 15 | script: 16 | - python3 setup.py test 17 | after_success: 18 | - coverage run --source=heralding setup.py -q nosetests 19 | - coveralls 20 | deploy: 21 | provider: pypi 22 | user: Johnny.Vestergaard 23 | password: 24 | secure: dJNWYkbaKTl6zCvtSq8YCNAJCnd+vs36/5dBojBqpf1xuUQgodPTBYJuiEJYODq4O7DIuepyo4K982gd6TnGRf5YaR+DdbFCD+mIt7WKRbuFm64Typwgcdw4kaqdwIMtDoTn4XZK3u5RHOuCs30+Ly8pSN+j19p/vqMqypVN6nNYcBp8eANmWjmAAijFSC58yqs+sD2SSadh2Z4GWb5c4+KwqakL3g1vY56po+QUNLBOgcJ/wXTZ9QJbjWh+i9K2vzQD8sun0W4Hh/ztQdLeCUEOrxZPiFu1nQSjVeV0OAj55/ihXC09zjSUmgNLi20J3KGQ4cM81eG22bQj9M39ROz+HiRjT3U0aIgo3HShBTQpRylKAEW+mHo7NrRtNIKoVLI8Mw3utovlnyZcG9NBV02RROKlzUqJ8XgDbXZz8/Yzxt7bvNl5f28z4gl1ozst+OYYkcpAdJGSGbLv73s7ZIILblU0M0ifSGMeTcNCccqDWhKknEh8ccIRSZRmeK5RIQdjGUVMLYnIoq7uqXIwqk8ilEoUxhWXUIqTcnymY5QjsR6b6T1groVSC16HIPYY/52ii/V5uZ3EoRLsQyDoeDojs5e50HSUx54OwRvm+y9himeI6G7CsUDS+lDtTJYvckeE2QDKQLAQcBH9HN1F8gkm+WIf1pimld4c8dKXTHQ= 25 | on: 26 | tags: true 27 | distributions: sdist bdist_wheel 28 | repo: johnnykv/heralding 29 | condition: "$TRAVIS_TAG =~ ^Release_[0-9]+[.][0-9]+[.][0-9]+" 30 | -------------------------------------------------------------------------------- /.yapfignore: -------------------------------------------------------------------------------- 1 | ez_setup.py 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9-slim-bullseye as base 2 | 3 | FROM base as build 4 | 5 | # Install dependencies 6 | COPY requirements.txt requirements.txt 7 | RUN apt-get update && apt-get install -y libpq-dev gcc \ 8 | && pip install --user --no-cache-dir -r requirements.txt \ 9 | && rm -rf /var/lib/apt/lists/* 10 | 11 | # Install Heralding 12 | COPY . . 13 | RUN python setup.py install --user 14 | 15 | FROM base 16 | COPY --from=build /root/.local /root/.local 17 | 18 | ENV PATH=/root/.local/bin:$PATH 19 | 20 | CMD ["heralding" ] 21 | EXPOSE 21 22 23 25 80 110 143 443 465 993 995 1080 2222 3306 3389 5432 5900 22 | -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | # Installing Heralding in five to seven steps. 2 | 3 | * [Step 1: Install dependencies](#step-1-install-dependencies) 4 | * [Step 2: Checkout the code](#step-2-checkout-the-code) 5 | * [Step 3: Setup Virtual Environment](#step-3-setup-virtual-environment) 6 | * [Step 4: Install Heralding](#step-4-install-heralding) 7 | * [Step 5: Customize the configuration (optional)](#step-5-customize-the-configuration-optional) 8 | * [Step 6: Start the honeypot](#step-6-start-the-honeypot) 9 | * [Step 7: Run Heralding as a service (optional)](#step-7-run-heralding-as-a-service-optional) 10 | 11 | This guide is a more comprehensive version of the [README](https://github.com/johnnykv/heralding/blob/master/README.rst) geared towards installing Heralding on Ubuntu 16.04 in a Python virtual environment. 12 | 13 | ## Step 1: Install dependencies 14 | First, we install support for Python virtual environments and other dependencies. The actual Python packages are installed later. 15 | ``` 16 | $ sudo apt-get install python3-pip python3-dev python3-venv build-essential libssl-dev libffi-dev libpq-dev 17 | ``` 18 | 19 | ## Step 2: Checkout the code 20 | This example puts things in the `user`'s home directory: 21 | ``` 22 | $ cd ~ 23 | $ git clone https://github.com/johnnykv/heralding.git 24 | Cloning into 'heralding'... 25 | remote: Counting objects: 10199, done. 26 | remote: Total 10199 (delta 0), reused 0 (delta 0), pack-reused 10199 27 | Receiving objects: 100% (10199/10199), 5.62 MiB | 0 bytes/s, done. 28 | Resolving deltas: 100% (7532/7532), done. 29 | Checking connectivity... done. 30 | $ cd heralding 31 | ``` 32 | 33 | ## Step 3: Setup Virtual Environment 34 | Next, you need to create your virtual environment: 35 | ``` 36 | $ pwd 37 | /home/user/heralding 38 | $ python3 -m venv heralding-env 39 | ``` 40 | 41 | Activate the virtual environment: 42 | ``` 43 | $ source heralding-env/bin/activate 44 | ``` 45 | 46 | ## Step 4: Install Heralding 47 | Install the required packages and Heralding itself into your new virtual environment: 48 | ``` 49 | (heralding-env) $ pip install -r requirements.txt 50 | [...] 51 | (heralding-env) $ pip install heralding 52 | Collecting heralding 53 | Requirement already satisfied: aiosmtpd in ./heralding-env/lib/python3.5/site-packages (from heralding) 54 | Requirement already satisfied: pyzmq in ./heralding-env/lib/python3.5/site-packages (from heralding) 55 | Requirement already satisfied: pyaml in ./heralding-env/lib/python3.5/site-packages (from heralding) 56 | Requirement already satisfied: pycrypto>=2.6.0 in ./heralding-env/lib/python3.5/site-packages (from heralding) 57 | Requirement already satisfied: nose in ./heralding-env/lib/python3.5/site-packages (from heralding) 58 | Requirement already satisfied: pyOpenSSL in ./heralding-env/lib/python3.5/site-packages (from heralding) 59 | Requirement already satisfied: Enum34 in ./heralding-env/lib/python3.5/site-packages (from heralding) 60 | Requirement already satisfied: asyncssh in ./heralding-env/lib/python3.5/site-packages (from heralding) 61 | Requirement already satisfied: ipify in ./heralding-env/lib/python3.5/site-packages (from heralding) 62 | Requirement already satisfied: atpublic in ./heralding-env/lib/python3.5/site-packages (from aiosmtpd->heralding) 63 | Requirement already satisfied: PyYAML in ./heralding-env/lib/python3.5/site-packages (from pyaml->heralding) 64 | Requirement already satisfied: six>=1.5.2 in ./heralding-env/lib/python3.5/site-packages (from pyOpenSSL->heralding) 65 | Requirement already satisfied: cryptography>=1.9 in ./heralding-env/lib/python3.5/site-packages (from pyOpenSSL->heralding) 66 | Requirement already satisfied: backoff>=1.0.7 in ./heralding-env/lib/python3.5/site-packages (from ipify->heralding) 67 | Requirement already satisfied: requests>=2.7.0 in ./heralding-env/lib/python3.5/site-packages (from ipify->heralding) 68 | Requirement already satisfied: idna>=2.1 in ./heralding-env/lib/python3.5/site-packages (from cryptography>=1.9->pyOpenSSL->heralding) 69 | Requirement already satisfied: cffi>=1.7 in ./heralding-env/lib/python3.5/site-packages (from cryptography>=1.9->pyOpenSSL->heralding) 70 | Requirement already satisfied: asn1crypto>=0.21.0 in ./heralding-env/lib/python3.5/site-packages (from cryptography>=1.9->pyOpenSSL->heralding) 71 | Requirement already satisfied: certifi>=2017.4.17 in ./heralding-env/lib/python3.5/site-packages (from requests>=2.7.0->ipify->heralding) 72 | Requirement already satisfied: urllib3<1.23,>=1.21.1 in ./heralding-env/lib/python3.5/site-packages (from requests>=2.7.0->ipify->heralding) 73 | Requirement already satisfied: chardet<3.1.0,>=3.0.2 in ./heralding-env/lib/python3.5/site-packages (from requests>=2.7.0->ipify->heralding) 74 | Requirement already satisfied: pycparser in ./heralding-env/lib/python3.5/site-packages (from cffi>=1.7->cryptography>=1.9->pyOpenSSL->heralding) 75 | Installing collected packages: heralding 76 | Successfully installed heralding-0.2.1 77 | ``` 78 | 79 | ## Step 5: Customize the configuration (optional) 80 | You can customize the default configuration file `heralding.yml` located in the github repo's folder by first making a copy: 81 | ``` 82 | (heralding-env) $ cp heralding/heralding.yml . 83 | ``` 84 | Then make your changes & save: 85 | ``` 86 | (heralding-env) $ nano heralding.yml 87 | ``` 88 | 89 | ## Step 6: Start the honeypot 90 | Start the honeypot using the command below. We run it in the background by appending `&` to the command `sudo ./heralding-env/bin/heralding`: 91 | ``` 92 | (heralding-env) $ sudo ./heralding-env/bin/heralding & 93 | 2017-09-18 22:30:08,707 (root) Initializing Heralding version 0.2.1 94 | 2017-09-18 22:30:08,724 (heralding.reporting.file_logger) File logger started, using file: heralding_activity.log 95 | 2017-09-18 22:30:08,749 (heralding.honeypot) Found public ip: x.x.x.x 96 | 2017-09-18 22:30:08,750 (heralding.honeypot) Started Pop3S capability listening on port 995 97 | 2017-09-18 22:30:08,751 (heralding.honeypot) Started Imaps capability listening on port 993 98 | 2017-09-18 22:30:08,752 (heralding.honeypot) Started Imap capability listening on port 143 99 | 2017-09-18 22:30:08,753 (heralding.honeypot) Started https capability listening on port 443 100 | 2017-09-18 22:30:08,753 (heralding.honeypot) Started smtp capability listening on port 25 101 | 2017-09-18 22:30:08,754 (heralding.honeypot) Started Pop3 capability listening on port 110 102 | 2017-09-18 22:30:08,754 (heralding.honeypot) Started Telnet capability listening on port 23 103 | 2017-09-18 22:30:08,755 (heralding.honeypot) Started Http capability listening on port 80 104 | 2017-09-18 22:30:08,756 (heralding.honeypot) Started SSH capability listening on port 22 105 | 2017-09-18 22:30:08,757 (heralding.honeypot) Started ftp capability listening on port 21 106 | 2017-09-18 22:30:08,757 (root) Privileges dropped, running as nobody/nogroup. 107 | ``` 108 | 109 | ## Step 7: Run Heralding as a service (optional) 110 | If heralding is already running, you might want to stop it before proceeding. 111 | 112 | Instead of running heralding interactively or in the background, you can run it as a service using `systemd` in Ubuntu 16.04. Below is an example service file you can use. It should work fine if you followed all of the previous steps above. 113 | 114 | ``` 115 | [Unit] 116 | Description=heralding 117 | Documentation=https://github.com/johnnykv/heralding 118 | After=network.target 119 | 120 | [Service] 121 | Type=simple 122 | WorkingDirectory=/home/user/heralding/ 123 | Environment=VIRTUAL_ENV="/home/user/heralding/heralding-env/" 124 | Environment=PATH="$VIRTUAL_ENV/bin:$PATH" 125 | ExecStart=/home/user/heralding/heralding-env/bin/heralding 126 | ExecReload=/bin/kill -s TERM $MAINPID 127 | ExecStop=/bin/kill -s TERM $MAINPID 128 | Restart=on-failure 129 | 130 | [Install] 131 | WantedBy=multi-user.target 132 | ``` 133 | 134 | Create the unit file for our heralding service, copy/pasting the example above and changing your `user` name if applicable: 135 | ``` 136 | $ sudo nano /etc/systemd/system/heralding.service 137 | ``` 138 | 139 | Reload `systemd`: 140 | ``` 141 | $ sudo systemctl daemon-reload 142 | ``` 143 | 144 | Then, activate the launch of the service at boot: 145 | ``` 146 | $ sudo systemctl enable heralding 147 | ``` 148 | 149 | Finally, start heralding: 150 | ``` 151 | $ sudo service heralding start 152 | ``` 153 | 154 | You can also check the status to see if its running and some log messages: 155 | ``` 156 | $ sudo service heralding status 157 | ``` 158 | 159 | That's it! 160 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include heralding * 2 | include ez_setup.py 3 | include requirements.txt 4 | include requirements-test.txt 5 | prune *.pyc 6 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Heralding |travis badge| |version badge| |codacy badge| 2 | ======================================================= 3 | 4 | .. |travis badge| image:: https://img.shields.io/travis/johnnykv/heralding/master.svg 5 | :target: https://travis-ci.org/johnnykv/heralding 6 | .. |codacy badge| image:: https://api.codacy.com/project/badge/Grade/e9419eb118dc4741ae230aa6bcc8a015 7 | :target: https://www.codacy.com/app/johnnykv/heralding?utm_source=github.com&utm_medium=referral&utm_content=johnnykv/heralding&utm_campaign=Badge_Grade 8 | .. |version badge| image:: https://img.shields.io/pypi/v/heralding.svg 9 | :target: https://pypi.python.org/pypi/Heralding/ 10 | 11 | 12 | About 13 | ----- 14 | 15 | Sometimes you just want a simple honeypot that collects credentials, nothing more. Heralding is that honeypot! 16 | Currently the following protocols are supported: ftp, telnet, ssh, http, https, pop3, pop3s, imap, imaps, smtp, vnc, postgresql and socks5. 17 | 18 | **You need Python 3.7.0 or higher.** 19 | 20 | Starting the honeypot 21 | ----------------------- 22 | 23 | .. code-block:: shell 24 | 25 | 2019-04-14 13:10:11,854 (root) Initializing Heralding version 1.0.4 26 | 2019-04-14 13:10:11,879 (heralding.reporting.file_logger) File logger: Using log_auth.csv to log authentication attempts in CSV format. 27 | 2019-04-14 13:10:11,879 (heralding.reporting.file_logger) File logger: Using log_session.csv to log unified session data in CSV format. 28 | 2019-04-14 13:10:11,879 (heralding.reporting.file_logger) File logger: Using log_session.json to log complete session data in JSON format. 29 | 2019-04-14 13:10:11,880 (heralding.honeypot) Started Pop3 capability listening on port 110 30 | 2019-04-14 13:10:11,882 (heralding.honeypot) Started Pop3S capability listening on port 995 31 | 2019-04-14 13:10:11,883 (heralding.honeypot) Started smtp capability listening on port 25 32 | 2019-04-14 13:10:11,883 (heralding.honeypot) Started Http capability listening on port 80 33 | 2019-04-14 13:10:11,885 (heralding.honeypot) Started https capability listening on port 443 34 | 2019-04-14 13:10:11,885 (heralding.honeypot) Started Vnc capability listening on port 5900 35 | 2019-04-14 13:10:11,885 (heralding.honeypot) Started Telnet capability listening on port 23 36 | 2019-04-14 13:10:11,886 (heralding.honeypot) Started ftp capability listening on port 21 37 | 2019-04-14 13:10:11,886 (heralding.honeypot) Started Imap capability listening on port 143 38 | 2019-04-14 13:10:11,886 (heralding.honeypot) Started MySQL capability listening on port 3306 39 | 2019-04-14 13:10:11,887 (heralding.honeypot) Started Socks5 capability listening on port 1080 40 | 2019-04-14 13:10:11,946 (asyncssh) Creating SSH server on 0.0.0.0, port 2222 41 | 2019-04-14 13:10:11,946 (heralding.honeypot) Started SSH capability listening on port 2222 42 | 2019-04-14 13:10:11,946 (heralding.honeypot) Started PostgreSQL capability listening on port 5432 43 | 2019-04-14 13:10:11,947 (heralding.honeypot) Started Imaps capability listening on port 993 44 | 45 | 46 | Viewing the collected data 47 | -------------------------- 48 | 49 | Heralding logs relevant data in three files, log_session.json, log_auth.csv and log_session.csv. 50 | 51 | **log_session.json** 52 | 53 | This log file contains all available information for a given activity to the honeypot. This included timestamp, authentication attempts and protocol specific information (auxiliary data) - and a bunch of other information. Be aware that the log entry for a specific session will appear in the log file **after** the session has ended. The format is jsonlines. 54 | 55 | .. code-block:: json 56 | 57 | { 58 | "timestamp":"2019-04-13 08:29:09.019394", 59 | "duration":9, 60 | "session_id":"4ba1fc0a-872c-46bb-a2f8-80c38453c74f", 61 | "source_ip":"127.0.0.1", 62 | "source_port":52192, 63 | "destination_ip":"127.0.0.1", 64 | "destination_port":2222, 65 | "protocol":"ssh", 66 | "num_auth_attempts":2, 67 | "auth_attempts":[ 68 | { 69 | "timestamp":"2019-04-13 08:29:12.732530", 70 | "username":"rewt", 71 | "password":"PASSWORD" 72 | }, 73 | { 74 | "timestamp":"2019-04-13 08:29:15.686619", 75 | "username":"rewt", 76 | "password":"P@ssw0rd12345" 77 | }, 78 | ], 79 | "session_ended":true, 80 | "auxiliary_data":{ 81 | "client_version":"SSH-2.0-OpenSSH_7.7p1 Ubuntu-4ubuntu0.3", 82 | "recv_cipher":"aes128-ctr", 83 | "recv_mac":"umac-64-etm@openssh.com", 84 | "recv_compression":"none" 85 | } 86 | } 87 | 88 | 89 | **log_session.csv** 90 | 91 | This log file contains entries for all connections to the honeypot. The data includes timestamp, duration, IP information and the number of authentication attempts. Be aware that the log entry for a specific session will appear in the log file **after** the session has ended. 92 | 93 | .. code-block:: shell 94 | 95 | $ tail log_session.csv 96 | timestamp,duration,session_id,source_ip,source_port,destination_ip,destination_port,protocol,auth_attempts 97 | 2017-12-26 20:38:19.683713,16,0841e3aa-241b-4da0-b85e-e5a5524cc836,127.0.0.1,53161,,23,telnet,3 98 | 2017-12-26 22:17:33.140742,6,d20c30c1-6765-4ab5-9144-a8be02385018,127.0.0.1,55149,,21,ftp,1 99 | 2017-12-26 22:17:48.088281,0,e0f50505-af93-4234-b82c-5477d8d88546,127.0.0.1,55151,,22,ssh,0 100 | 2017-12-26 22:18:06.284689,0,6c7d653f-d02d-4717-9973-d9b2e4a41d24,127.0.0.1,55153,,22,ssh,0 101 | 2017-12-26 22:18:13.043327,30,f3af2c8c-b63f-4873-ac7f-28c73b9e3e92,127.0.0.1,55155,,22,ssh,3 102 | 103 | **log_auth.csv** 104 | 105 | This log file contains information for all authentication attempts where it was possible to log a username and plaintext password. Log entries will appear in this file as soon as the password has been transmitted. 106 | 107 | .. code-block:: shell 108 | 109 | $ tail log_auth.csv 110 | timestamp,auth_id,session_id,source_ip,source_port,destination_port,protocol,username,password 111 | 2016-03-12 20:35:02.258198,192.168.2.129,51551,23,telnet,bond,james 112 | 2016-03-12 20:35:09.658593,192.168.2.129,51551,23,telnet,clark,P@SSw0rd123 113 | 2016-03-18 19:31:38.064700,192.168.2.129,53416,22,ssh,NOP_Manden,M@MS3 114 | 2016-03-18 19:31:38.521047,192.168.2.129,53416,22,ssh,guest,guest 115 | 2016-03-18 19:31:39.376768,192.168.2.129,53416,22,ssh,HundeMad,katNIPkat 116 | 2016-03-18 19:33:07.064504,192.168.2.129,53431,110,pop3,charles,N00P1SH 117 | 2016-03-18 19:33:12.504483,192.168.2.129,53431,110,pop3,NektarManden,mANDENnEktar 118 | 2016-03-18 19:33:24.952645,192.168.2.129,53433,21,ftp,Jamie,brainfreeze 119 | 2016-03-18 19:33:47.008562,192.168.2.129,53436,21,ftp,NektarKongen,SuperS@cretP4ssw0rd1 120 | 2016-03-18 19:36:56.077840,192.168.2.129,53445,21,ftp,Joooop,Pooop 121 | 122 | 123 | Installing Heralding 124 | --------------------- 125 | 126 | For step by step instructions on how to install and run heralding in a Python virtual environment using Ubuntu, see this `guide `_. Otherwise, the basic installation instructions are below. 127 | 128 | To install the latest stable (well, semi-stable) version, use pip: 129 | 130 | .. code-block:: shell 131 | 132 | pip install heralding 133 | 134 | Make sure that requirements and pip is installed. 135 | Simple way to do this on a Debian-based OS is: 136 | 137 | .. code-block:: shell 138 | 139 | sudo apt-get install python-pip python-dev build-essential libssl-dev libffi-dev 140 | sudo pip install -r requirements.txt 141 | 142 | And finally start the honeypot: 143 | 144 | .. code-block:: shell 145 | 146 | mkdir tmp 147 | cd tmp 148 | sudo heralding 149 | 150 | Docker Build 151 | ------------- 152 | 1.Checkout the code: 153 | 154 | .. code-block:: shell 155 | 156 | cd ~ 157 | git clone https://github.com/johnnykv/heralding.git 158 | cd heralding 159 | 160 | 2.Build new Docker image and run it (Http localhost expose example of port 80 to localhost:8080): 161 | 162 | .. code-block:: shell 163 | 164 | sudo docker build -t heralding . 165 | 166 | sudo docker run -p 8080:80 heralding 167 | 168 | Visit your application in a browser at http://localhost:8080 169 | 170 | 3.Check the log files: 171 | 172 | .. code-block:: shell 173 | 174 | sudo docker ps 175 | 176 | 177 | We need to copy the CONTAINER ID of our heralding image. Looking like 0beb67f1e92c. 178 | 179 | .. code-block:: shell 180 | 181 | sudo docker exec -it 0beb67f1e92c bash 182 | 183 | 184 | And now you are in the work directory, you can read the log files by typing cat and the name of the file. Example: 185 | 186 | .. code-block:: shell 187 | 188 | cat log_auth.csv 189 | 190 | 191 | Pcaps 192 | ----- 193 | 194 | Want a seperate pcap for each heralding session? Sure, take a look at the Curisoum_ project. Make sure to enable Curisoum in Heralding.yml! 195 | 196 | .. _Curisoum: https://github.com/johnnykv/curiosum 197 | 198 | 199 | Submitting code 200 | --------------- 201 | 202 | The project uses Chromium_ code style, please make sure to follow this before submitting. You can use tools like yapf to autoformat - the config file can be found at the root of the repo (.style.yapf). 203 | 204 | .. _Chromium: https://chromium.googlesource.com/chromiumos/docs/+/master/styleguide/python.md 205 | -------------------------------------------------------------------------------- /bin/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnnykv/heralding/ac12724ab38c4e2fe78f07d1bc35e6e586ba69c0/bin/__init__.py -------------------------------------------------------------------------------- /bin/heralding: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (C) 2017 Johnny Vestergaard 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import asyncio 18 | import logging 19 | import logging.handlers 20 | import os, pwd, grp, yaml, sys 21 | from argparse import ArgumentParser 22 | 23 | import heralding 24 | import heralding.honeypot 25 | import heralding.reporting.reporting_relay 26 | from heralding.misc.common import on_unhandled_task_exception 27 | 28 | logger = logging.getLogger() 29 | 30 | 31 | def setup_logging(logfile, verbose): 32 | """ 33 | Sets up logging to the logfiles/console. 34 | :param logfile: Path of the file to write logs to. 35 | :param verbose: If True, enables verbose logging. 36 | """ 37 | root_logger = logging.getLogger() 38 | 39 | default_formatter = logging.Formatter('%(asctime)-15s (%(name)s) %(message)s') 40 | 41 | if verbose: 42 | loglevel = logging.DEBUG 43 | else: 44 | loglevel = logging.INFO 45 | root_logger.setLevel(loglevel) 46 | 47 | console_log = logging.StreamHandler() 48 | console_log.setFormatter(default_formatter) 49 | console_log.setLevel(loglevel) 50 | root_logger.addHandler(console_log) 51 | 52 | if logfile in ('/dev/log', '/dev/syslog', '/var/run/syslog', '/var/run/log'): 53 | file_log = logging.handlers.SysLogHandler( 54 | address=logfile, facility='local1') 55 | syslog_formatter = logging.Formatter('heralding[%(process)d]: %(message)s') 56 | file_log.setFormatter(syslog_formatter) 57 | else: 58 | file_log = logging.FileHandler(logfile) 59 | file_log.setFormatter(default_formatter) 60 | file_log.setLevel(loglevel) 61 | root_logger.addHandler(file_log) 62 | 63 | # ensure the filer is applied to all handlers 64 | for handler in root_logger.handlers: 65 | handler.addFilter(LogFilter()) 66 | 67 | 68 | class LogFilter(logging.Filter): 69 | 70 | def filter(self, rec): 71 | """Filtering internal logs of aiosmtpd, asyncssh""" 72 | if rec.name == "asyncssh": 73 | if logging.getLogger().level == logging.DEBUG: 74 | return True 75 | if rec.levelno <= 20: 76 | return False 77 | else: 78 | return True 79 | 80 | if rec.name == 'mail.log': 81 | return False 82 | else: 83 | return True 84 | 85 | 86 | def break_if_python_not_supported(): 87 | if sys.version_info[0:2] < (3, 7): 88 | raise Exception( 89 | "Wrong python version! Your Python interpreter must be 3.6.0 or above!") 90 | 91 | 92 | def drop_privileges(uid_name='nobody', gid_name='nogroup'): 93 | """Drops current privileges to the privileges of selected user.""" 94 | if os.getuid() != 0: 95 | return 96 | 97 | wanted_uid = pwd.getpwnam(uid_name)[2] 98 | wanted_gid = grp.getgrnam(gid_name)[2] 99 | 100 | os.setgid(wanted_gid) 101 | os.setuid(wanted_uid) 102 | 103 | new_uid_name = pwd.getpwuid(os.getuid())[0] 104 | new_gid_name = grp.getgrgid(os.getgid())[0] 105 | 106 | logger.info("Privileges dropped, running as {0}/{1}.".format( 107 | new_uid_name, new_gid_name)) 108 | 109 | 110 | if __name__ == '__main__': 111 | break_if_python_not_supported() 112 | parser = ArgumentParser(description='Heralding') 113 | 114 | parser.add_argument( 115 | '-v', 116 | '--verbose', 117 | action='store_true', 118 | default=False, 119 | help='Logs debug messages.') 120 | parser.add_argument( 121 | '-l', 122 | '--logfile', 123 | dest='logfile', 124 | default='heralding.log', 125 | help='Heralding log file') 126 | parser.add_argument( 127 | '-c', 128 | '--config', 129 | dest='config', 130 | default='heralding.yml', 131 | help='Heralding config file') 132 | args = parser.parse_args() 133 | 134 | setup_logging(args.logfile, args.verbose) 135 | 136 | logger.info('Initializing Heralding version {0}'.format(heralding.version)) 137 | 138 | config_file = args.config 139 | if not os.path.isfile(config_file): 140 | package_directory = os.path.dirname(os.path.abspath(heralding.__file__)) 141 | config_file = os.path.join(package_directory, config_file) 142 | logger.warning( 143 | 'Using default config file: "{0}", if you want to customize values please ' 144 | 'copy this file to the current working directory'.format(config_file)) 145 | 146 | try: 147 | with open(config_file, "r") as _file: 148 | config = yaml.safe_load(_file.read()) 149 | except Exception as ex: 150 | error_message = "Error while reading config file {0}: {1}.".format( 151 | config_file, ex) 152 | logger.error(error_message) 153 | sys.exit(error_message) 154 | 155 | loop = asyncio.get_event_loop() 156 | # startup reporting relay 157 | reporting_relay = heralding.reporting.reporting_relay.ReportingRelay() 158 | reporting_relay_task = loop.run_in_executor(None, reporting_relay.start) 159 | reporting_relay_task.add_done_callback(on_unhandled_task_exception) 160 | 161 | honeypot = heralding.honeypot.Honeypot(config, loop) 162 | try: 163 | honeypot.start() 164 | except Exception as ex: 165 | logger.exception(ex) 166 | honeypot.stop() 167 | reporting_relay.stop() 168 | # We give reporting_relay a chance to be finished. 169 | loop.run_until_complete(asyncio.sleep(0.5)) 170 | loop.close() 171 | sys.exit(ex) 172 | 173 | drop_privileges() 174 | 175 | try: 176 | loop.run_forever() 177 | except KeyboardInterrupt: 178 | pass 179 | finally: 180 | honeypot.stop() 181 | reporting_relay.stop() 182 | # We give reporting_relay a chance to be finished. 183 | loop.run_until_complete(asyncio.sleep(0.5)) 184 | loop.close() 185 | -------------------------------------------------------------------------------- /changelog.txt: -------------------------------------------------------------------------------- 1 | 1.0.7 (2020-12-27) 2 | - Added custom POP3 banner (#143) 3 | - Disabled RDP until we have an stable implementation. 4 | - Various minor fixes and improvements 5 | 6 | 1.0.6 (2019-10-12) 7 | - Added RDP capability (#25) 8 | - Added MySQL capability (#76) 9 | - Fixed HPFeeds bug (#123) 10 | - Added basic password cracker 11 | - Various minor fixes and improvements 12 | 13 | 1.0.5 (2019-04-18) 14 | - Fixed asynssh issue (#111) 15 | 16 | 1.0.4 (2019-04-13) 17 | - Added logging for auxiliary data for socks5, http and ssh (#85, #93, #95, #100) 18 | - Added mysql capability (#95) 19 | - Various fixes 20 | 21 | 1.0.3 (2018-11-20) 22 | - Get destination ip from socket (#88) 23 | 24 | 1.0.2 (2018-09-10) 25 | - HPFeeds issue fixed (#86, #87) 26 | 27 | 1.0.1 (2018-04-27) 28 | - Socks5 capability added (#72) 29 | - Added feature that allows pcap recording of session (Curiosum integration) 30 | - Various minor fixes and improvements 31 | 32 | 1.0.0 (2017-12-28) 33 | - Added VNC support (#68) 34 | - New logging scheme (log_session.csv and log_auth.csv) 35 | - Various minor fixes and improvements 36 | 37 | 0.2.4 (2017-12-3) 38 | - Fixed issue with MANIFEST.IN (#69) 39 | 40 | 0.2.3 (2017-12-3) 41 | - Added requirements-test.txt to MANIFEST.IN (#69) 42 | 43 | 0.2.2 (2017-10-19) 44 | - Added PostgreSQL support. 45 | - Added preliminary HPFriends support. 46 | 47 | 0.2.1 (2017-08-23) 48 | - Gevent replaced with asyncio 49 | - Various fixes 50 | 51 | 0.2.0 (2017-06-23) 52 | - Heralding converted to Python 3 53 | 54 | 0.1.17 (2017-06-20): 55 | - NOTICE: This will be the last python 2 version of Heralding. All 56 | future versions will be Python 3. 57 | - Imap capability added 58 | 59 | 0.1.16 (2017-04-25): 60 | - Added ability to customize SSH banner 61 | - Fixed issue with encoding in CSV logger (#17) 62 | - Fixed issue when no shared ciphers found (#22) 63 | 64 | 0.1.15 (2016-10-31): 65 | - improvements to logger shutdown flow 66 | 67 | 0.1.14 (2016-08-17): 68 | - fixed premature ending of SSH sessions 69 | 70 | 0.1.13 (2016-05-20): 71 | - fixed decoding issue in smtp capability 72 | 73 | 0.1.12 (2016-05-15): 74 | - https and pop3s fixed and enabled by default 75 | 76 | 0.1.11 (2016-05-6): 77 | - Fixed bug that involved source_port not getting logged in CSV files 78 | 79 | 0.1.10 (2016-04-17): 80 | - Application will now be taken down if a logger fails 81 | 82 | 0.1.9 (2016-04-15): 83 | - Fixed typo in CSV reporter (souce_port -> source_port) 84 | 85 | 0.1.8 (2016-04-08): 86 | - Application will now exit if a capability cannot bind to a port 87 | - Added option to lookup public ip 88 | 89 | 0.1.7 (2016-04-03): 90 | - Configurable timeouts 91 | 92 | 0.1.6 (2016-04-03): 93 | - Fixed issue with sessions not getting closed 94 | - Introduced hard limit on number of concurrent sessions 95 | 96 | 0.1.5 (2016-04-03): 97 | - Added some debug logging 98 | 99 | 0.1.4 (2016-04-02): 100 | - Added ZMQ logger 101 | - Added auth_id to file logger 102 | 103 | 0.1.3 (2016-03-20): 104 | - Fixed CSV logging issue 105 | 106 | 0.1.2 (2016-03-19): 107 | - Fixed issue where config file was not included in module 108 | 109 | 0.1.1 (2016-03-18): 110 | - Minor build update 111 | 112 | 0.1.0 (2016-03-18): 113 | - Support for the following protocols: SSH, telnet, smtp, ftp, http, pop3 114 | - Stripped away unneeded components from the Beeswarm project (https://github.com/honeynet/beeswarm) 115 | -------------------------------------------------------------------------------- /heralding/__init__.py: -------------------------------------------------------------------------------- 1 | version = "1.0.7" 2 | -------------------------------------------------------------------------------- /heralding/capabilities/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import glob 3 | 4 | # Detect all modules 5 | for fullname in glob.glob(os.path.dirname(__file__) + "/*.py"): 6 | name = os.path.basename(fullname) 7 | # __init__ and handlerbase are not capabilities, so ignore them 8 | if name[:-3] != "__init__" or name[:-3] != "handlerbase": 9 | __import__("heralding.capabilities." + name[:-3]) 10 | -------------------------------------------------------------------------------- /heralding/capabilities/ftp.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017 Johnny Vestergaard 2 | # 3 | # Rewritten by Aniket Panse 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | # Aniket Panse grants Johnny Vestergaard 19 | # a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable 20 | # copyright license to reproduce, prepare derivative works of, publicly 21 | # display, publicly perform, sublicense, relicense, and distribute [the] Contributions 22 | # and such derivative works. 23 | 24 | import logging 25 | 26 | from heralding.capabilities.handlerbase import HandlerBase 27 | 28 | logger = logging.getLogger(__name__) 29 | 30 | TERMINATOR = '\r\n' 31 | 32 | 33 | class FtpHandler: 34 | """Handles a single FTP connection""" 35 | 36 | def __init__(self, reader, writer, options, session): 37 | self.banner = options['protocol_specific_data']['banner'] 38 | self.max_loggins = int(options['protocol_specific_data']['max_attempts']) 39 | self.syst_type = options['protocol_specific_data']['syst_type'] 40 | self.authenticated = False 41 | self.writer = writer 42 | self.reader = reader 43 | self.serve_flag = True 44 | self.session = session 45 | 46 | self.state = None 47 | self.user = None 48 | 49 | async def getcmd(self): 50 | cmd = await self.reader.readline() 51 | return str(cmd, 'utf-8') 52 | 53 | async def serve(self): 54 | await self.respond('220 ' + self.banner) 55 | 56 | while self.serve_flag: 57 | resp = await self.getcmd() 58 | if not resp: 59 | self.stop() 60 | break 61 | else: 62 | try: 63 | cmd, args = resp.split(' ', 1) 64 | except ValueError: 65 | cmd = resp 66 | args = None 67 | else: 68 | args = args.strip('\r\n') 69 | cmd = cmd.strip('\r\n') 70 | cmd = cmd.upper() 71 | # List of commands allowed before a login 72 | unauth_cmds = ['USER', 'PASS', 'QUIT', 'SYST'] 73 | meth = getattr(self, 'do_' + cmd, None) 74 | if not meth: 75 | await self.respond('500 Unknown Command.') 76 | else: 77 | if not self.authenticated: 78 | if cmd not in unauth_cmds: 79 | await self.respond('503 Login with USER first.') 80 | continue 81 | await meth(args) 82 | self.state = cmd 83 | 84 | async def do_USER(self, arg): 85 | self.user = arg 86 | await self.respond('331 Now specify the Password.') 87 | 88 | async def do_PASS(self, arg): 89 | if self.state != 'USER': 90 | await self.respond('503 Login with USER first.') 91 | return 92 | passwd = arg 93 | self.session.add_auth_attempt( 94 | 'plaintext', username=self.user, password=passwd) 95 | await self.respond('530 Authentication Failed.') 96 | if self.session.get_number_of_login_attempts() >= self.max_loggins: 97 | self.stop() 98 | 99 | async def do_SYST(self, arg): 100 | await self.respond('215 {0}'.format(self.syst_type)) 101 | 102 | async def do_QUIT(self, arg): 103 | await self.respond('221 Bye.') 104 | self.serve_flag = False 105 | self.stop() 106 | 107 | async def respond(self, msg): 108 | msg += TERMINATOR 109 | msg_bytes = bytes(msg, 'utf-8') 110 | self.writer.write(msg_bytes) 111 | await self.writer.drain() 112 | 113 | def stop(self): 114 | self.session.end_session() 115 | 116 | 117 | class ftp(HandlerBase): 118 | 119 | def __init__(self, options): 120 | super().__init__(options) 121 | self._options = options 122 | 123 | async def execute_capability(self, reader, writer, session): 124 | ftp_cap = FtpHandler(reader, writer, self._options, session) 125 | await ftp_cap.serve() 126 | -------------------------------------------------------------------------------- /heralding/capabilities/handlerbase.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017 Johnny Vestergaard 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import asyncio 17 | import logging 18 | 19 | from heralding.misc.session import Session 20 | 21 | logger = logging.getLogger(__name__) 22 | 23 | class HandlerBase: 24 | MAX_GLOBAL_SESSIONS = 800 25 | global_sessions = 0 26 | 27 | def __init__(self, options): 28 | """ 29 | Base class that all capabilities must inherit from. 30 | 31 | :param options: a dictionary of configuration options. 32 | """ 33 | self.options = options 34 | self.sessions = {} 35 | self.users = {} 36 | 37 | self.port = int(options['port']) 38 | if 'timeout' in options: 39 | self.timeout = options['timeout'] 40 | else: 41 | self.timeout = 30 42 | 43 | def create_session(self, address, dest_address): 44 | protocol = self.__class__.__name__.lower() 45 | session = Session(address[0], address[1], protocol, self.users, 46 | dest_address[1], dest_address[0]) 47 | self.sessions[session.id] = session 48 | HandlerBase.global_sessions += 1 49 | logger.debug('Accepted %s session on %s:%s from %s:%s. (%s)', protocol, 50 | dest_address[0], dest_address[1], address[0], address[1], 51 | str(session.id)) 52 | logger.debug('Size of session list for %s: %s', protocol, 53 | len(self.sessions)) 54 | return session 55 | 56 | def close_session(self, session): 57 | logger.debug('Closing session. (%s)', str(session.id)) 58 | session.end_session() 59 | if session.id in self.sessions: 60 | del self.sessions[session.id] 61 | else: 62 | assert False 63 | HandlerBase.global_sessions -= 1 64 | 65 | async def execute_capability(self, reader, writer, session): 66 | reader = None # NOQA 67 | writer = None # NOQA 68 | session = None # NOQA 69 | raise Exception("Must be implemented by child") 70 | 71 | async def handle_session(self, reader, writer): 72 | address = writer.get_extra_info('peername') 73 | dest_address = writer.get_extra_info('sockname') 74 | 75 | if HandlerBase.global_sessions > HandlerBase.MAX_GLOBAL_SESSIONS: 76 | protocol = self.__class__.__name__.lower() 77 | logger.warning( 78 | 'Got %s session on port %s from %s:%s, but not handling it because the global session limit has ' 79 | 'been reached', protocol, self.port, *address) 80 | else: 81 | session = self.create_session(address, dest_address) 82 | try: 83 | await asyncio.wait_for( 84 | self.execute_capability(reader, writer, session), 85 | timeout=self.timeout) 86 | except asyncio.TimeoutError: 87 | logger.debug('Session timed out. (%s)', session.id) 88 | except (BrokenPipeError, ConnectionError) as err: 89 | logger.debug('Unexpected end of session: %s, errno: %s. (%s)', err, 90 | err.errno, session.id) 91 | except (UnicodeDecodeError, KeyboardInterrupt): 92 | pass 93 | finally: 94 | self.close_session(session) 95 | writer.close() 96 | -------------------------------------------------------------------------------- /heralding/capabilities/http.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2013 Aniket Panse 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | # Aniket Panse grants Johnny Vestergaard 17 | # a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable 18 | # copyright license to reproduce, prepare derivative works of, publicly 19 | # display, publicly perform, sublicense, relicense, and distribute [the] Contributions 20 | # and such derivative works. 21 | 22 | import base64 23 | import logging 24 | 25 | from heralding.capabilities.handlerbase import HandlerBase 26 | from heralding.libs.http.aioserver import AsyncBaseHTTPRequestHandler 27 | 28 | logger = logging.getLogger(__name__) 29 | 30 | 31 | class HTTPHandler(AsyncBaseHTTPRequestHandler): 32 | 33 | def __init__(self, reader, writer, httpsession, options): 34 | self._options = options 35 | if 'banner' in self._options: 36 | self._banner = self._options['banner'] 37 | else: 38 | self._banner = 'Microsoft-IIS/5.0' 39 | self._session = httpsession 40 | address = writer.get_extra_info('address') 41 | super().__init__(reader, writer, address) 42 | 43 | def do_HEAD(self): 44 | self.send_response(200) 45 | self.send_header('Content-type', 'text/html') 46 | self.end_headers() 47 | 48 | def do_AUTHHEAD(self): 49 | self.send_response(401) 50 | # TODO: Value for basic realm... 51 | self.send_header('WWW-Authenticate', 'Basic realm=\"\"') 52 | self.send_header('Content-type', 'text/html') 53 | self.end_headers() 54 | 55 | def do_GET(self): 56 | if self.headers['Authorization'] is None: 57 | self.do_AUTHHEAD() 58 | else: 59 | hdr = self.headers['Authorization'] 60 | _, enc_uname_pwd = hdr.split(' ') 61 | dec_uname_pwd = str(base64.b64decode(enc_uname_pwd), 'utf-8') 62 | pos = dec_uname_pwd.find(':') 63 | uname, pwd = dec_uname_pwd[:pos], dec_uname_pwd[pos + 64 | 1:len(dec_uname_pwd)] 65 | self._session.add_auth_attempt('plaintext', username=uname, password=pwd) 66 | self.do_AUTHHEAD() 67 | headers_bytes = bytes(self.headers['Authorization'], 'utf-8') 68 | self.wfile.write(headers_bytes) 69 | self.wfile.write(b'not authenticated') 70 | aux_data = self.get_auxiliary_info() 71 | self._session.set_auxiliary_data(aux_data) 72 | # Disable logging provided by BaseHTTPServer 73 | def log_message(self, format_, *args): 74 | pass 75 | 76 | def get_auxiliary_info(self): 77 | data = {} 78 | for field in self.headers.keys(): 79 | data.update({str(field): str(self.headers[str(field)])}) 80 | 81 | return data 82 | 83 | 84 | class Http(HandlerBase): 85 | 86 | def __init__(self, options): 87 | super().__init__(options) 88 | self._options = options 89 | 90 | async def execute_capability(self, reader, writer, session): 91 | http_cap = HTTPHandler( 92 | reader, writer, httpsession=session, options=self._options) 93 | await http_cap.run() 94 | session.end_session() 95 | -------------------------------------------------------------------------------- /heralding/capabilities/https.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017 Johnny Vestergaard 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import logging 17 | 18 | from heralding.capabilities.handlerbase import HandlerBase 19 | from heralding.capabilities.http import Http 20 | 21 | logger = logging.getLogger(__name__) 22 | 23 | 24 | class https(Http, HandlerBase): 25 | """ 26 | This class will get wrapped in SSL. This is possible because we by convention wrap 27 | all capabilities that ends with the letter 's' in SSL.""" 28 | -------------------------------------------------------------------------------- /heralding/capabilities/imap.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017 Johnny Vestergaard 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import base64 17 | import logging 18 | import binascii 19 | 20 | from heralding.capabilities.handlerbase import HandlerBase 21 | 22 | logger = logging.getLogger(__name__) 23 | 24 | CRLF = '\r\n' 25 | 26 | 27 | class Imap(HandlerBase): 28 | 29 | def __init__(self, options): 30 | super().__init__(options) 31 | self.max_tries = int(self.options['protocol_specific_data']['max_attempts']) 32 | self.banner = self.options['protocol_specific_data']['banner'] 33 | 34 | self.available_commands = [ 35 | 'authenticate', 'capability', 'login', 'logout', 'noop' 36 | ] 37 | self.available_mechanisms = ['plain'] 38 | 39 | async def execute_capability(self, reader, writer, session): 40 | await self._handle_session(session, reader, writer) 41 | 42 | async def _handle_session( 43 | self, 44 | session, 45 | reader, 46 | writer, 47 | ): 48 | await self.send_message(writer, self.banner) 49 | 50 | state = "Not Authenticated" 51 | while state != 'Logout' and session.connected: 52 | # An exception is raised inside await reader.readline() in case of 53 | # sudden connection reset. 54 | raw_msg = await reader.readline() 55 | if not raw_msg: 56 | break 57 | 58 | raw_msg_str = str(raw_msg, 'utf-8') 59 | 60 | cmd_msg = raw_msg_str.rstrip().split(' ', 2) 61 | if len(cmd_msg) == 0: 62 | continue 63 | elif len(cmd_msg) == 1: 64 | await self.send_message(writer, "* BAD invalid command") 65 | continue 66 | elif len(cmd_msg) == 2: 67 | tag = cmd_msg[0] 68 | cmd = cmd_msg[1] 69 | args = '' 70 | else: 71 | tag = cmd_msg[0] 72 | cmd = cmd_msg[1] 73 | args = cmd_msg[2] 74 | 75 | cmd = cmd.lower() 76 | if cmd not in self.available_commands: 77 | await self.send_message(writer, tag + " BAD invalid command") 78 | else: 79 | func_to_call = getattr(self, 'cmd_{0}'.format(cmd), None) 80 | if func_to_call: 81 | return_value = await func_to_call(session, reader, writer, tag, args) 82 | state = return_value 83 | else: 84 | await self.send_message(writer, tag + " BAD invalid command") 85 | session.end_session() 86 | 87 | async def cmd_authenticate(self, session, reader, writer, tag, args): 88 | mechanism = args.split() 89 | if len(mechanism) == 1: 90 | auth_mechanism = mechanism[0].lower() 91 | else: 92 | await self.send_message(writer, tag + ' BAD invalid command') 93 | return "Not Authenticated" 94 | 95 | if auth_mechanism in self.available_mechanisms: 96 | # the space after '+' is needed according to RFC 97 | await self.send_message(writer, '+ ') 98 | raw_msg = await reader.read(512) 99 | 100 | if auth_mechanism == 'plain': 101 | success, credentials = self.try_b64decode(raw_msg, session) 102 | # \x00 is a separator between authorization identity, 103 | # username and password. Authorization identity isn't used in 104 | # this auth mechanism, so we must have 2 \x00 symbols.(RFC 4616) 105 | if success and credentials.count('\x00') == 2: 106 | raw_msg_dec = str(base64.b64decode(raw_msg), 'utf-8') 107 | _, user, password = raw_msg_dec.split('\x00') 108 | session.add_auth_attempt( 109 | 'plaintext', username=user, password=password) 110 | await self.send_message(writer, tag + ' NO Authentication failed') 111 | else: 112 | await self.send_message(writer, tag + ' BAD invalid command') 113 | else: 114 | await self.send_message(writer, tag + ' BAD invalid command') 115 | self.stop_if_too_many_attempts(session) 116 | return 'Not Authenticated' 117 | 118 | async def cmd_capability(self, session, reader, writer, tag, args): 119 | await self.send_message(writer, '* CAPABILITY IMAP4rev1 AUTH=PLAIN') 120 | await self.send_message(writer, tag + ' OK CAPABILITY completed') 121 | return 'Not Authenticated' 122 | 123 | async def cmd_login(self, session, reader, writer, tag, args): 124 | if args: 125 | user_cred = args.split(' ', 1) 126 | else: 127 | await self.send_message(writer, tag + ' BAD invalid command') 128 | return 'Not Authenticated' 129 | 130 | # Delete first and last quote, 131 | # because login and password can be sent as quoted strings 132 | if len(user_cred) == 1: 133 | user = self.strip_quotes(user_cred[0]) 134 | password = '' 135 | else: 136 | user = self.strip_quotes(user_cred[0]) 137 | password = self.strip_quotes(user_cred[1]) 138 | 139 | session.add_auth_attempt('plaintext', username=user, password=password) 140 | await self.send_message(writer, tag + ' NO Authentication failed') 141 | self.stop_if_too_many_attempts(session) 142 | return 'Not Authenticated' 143 | 144 | async def cmd_logout(self, session, reader, writer, tag, args): 145 | await self.send_message(writer, '* BYE IMAP4rev1 Server logging out') 146 | await self.send_message(writer, tag + ' OK LOGOUT completed') 147 | return 'Logout' 148 | 149 | async def cmd_noop(self, session, reader, writer, tag, args): 150 | await self.send_message(writer, tag + ' OK NOOP completed') 151 | return 'Not Authenticated' 152 | 153 | def stop_if_too_many_attempts(self, session): 154 | if self.max_tries < session.get_number_of_login_attempts(): 155 | session.end_session() 156 | 157 | @staticmethod 158 | async def send_message(writer, msg): 159 | message_bytes = bytes(msg + CRLF, 'utf-8') 160 | writer.write(message_bytes) 161 | await writer.drain() 162 | 163 | @staticmethod 164 | def try_b64decode(b64_str, session): 165 | try: 166 | result = base64.b64decode(b64_str) 167 | return True, str(result, 'utf-8') 168 | except binascii.Error: 169 | logger.warning('Error decoding base64: {0} ({1})'.format( 170 | binascii.hexlify(b64_str), session.id)) 171 | return False, '' 172 | 173 | @staticmethod 174 | def strip_quotes(quoted_str): 175 | if quoted_str.startswith('\"') and quoted_str.endswith('\"'): 176 | nonquoted_str = quoted_str[1:-1] 177 | else: 178 | nonquoted_str = quoted_str 179 | return nonquoted_str 180 | -------------------------------------------------------------------------------- /heralding/capabilities/imaps.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017 Johnny Vestergaard 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import logging 17 | 18 | from heralding.capabilities.handlerbase import HandlerBase 19 | from heralding.capabilities.imap import Imap 20 | 21 | logger = logging.getLogger(__name__) 22 | 23 | 24 | class Imaps(Imap, HandlerBase): 25 | """ 26 | This class will get wrapped in SSL. This is possible because we by convention wrap 27 | all capabilities that ends with the letter 's' in SSL.""" 28 | -------------------------------------------------------------------------------- /heralding/capabilities/mysql.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019 Sudipta Pandit 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import os 17 | import struct 18 | import logging 19 | 20 | from heralding.capabilities.handlerbase import HandlerBase 21 | 22 | logger = logging.getLogger(__name__) 23 | 24 | 25 | # Implemented according to https://dev.mysql.com/doc/internals/en/connection-phase-packets.html 26 | class MySQL(HandlerBase): 27 | 28 | def __init__(self, options): 29 | super().__init__(options) 30 | self.PROTO_VER = b'\x0a' 31 | self.SERVER_VER = b'5.7.16\x00' 32 | self.AUTH_PLUGIN = b'mysql_native_password\x00' # SHA-1 hash of password 33 | 34 | @staticmethod 35 | def convert4To3Byte(num): 36 | # MySQL protocol requires 3 byte integers for payload length 37 | return struct.pack(" 0 else "NO" 179 | plugin_offset = username_end_pos + 1 + 1 # start offset of auth_plugin 180 | seq_no = 2 # if no auth_switch_request 181 | password_enc = '' 182 | 183 | if password_len > 0: 184 | # for logging coverted to printable hex 185 | password_enc = data[plugin_offset:plugin_offset + password_len].hex() 186 | plugin_offset = plugin_offset + password_len 187 | except ValueError: 188 | logger.warning("Malformed packet received. Ending the session") 189 | session.end_session() 190 | return 191 | 192 | # check if schema(db) is present 193 | if caps & 0x00000008: 194 | schema_offset = plugin_offset # start offset of db 195 | try: 196 | schema_end_pos = data.index(b'\x00', schema_offset, 197 | max_size - schema_offset) 198 | plugin_offset = schema_end_pos + 1 199 | except ValueError: 200 | logger.warning("Could not find the schema. Ending the session") 201 | session.end_session() 202 | return 203 | 204 | # check if plugin_auth enabled 205 | if caps & 0x00080000: 206 | try: 207 | plugin_auth_offset = plugin_offset 208 | plugin_auth_pos = data.index(b'\x00', plugin_auth_offset, 209 | max_size - plugin_auth_offset) 210 | plugin_auth = data[plugin_auth_offset:plugin_auth_pos] 211 | except ValueError: 212 | logger.warning( 213 | "Cloud not find the plugin_auth data. Ending the session") 214 | session.end_session() 215 | return 216 | 217 | if "mysql_native_password" != str(plugin_auth, 'utf-8'): 218 | writer.write(self.auth_switch_request(seq_no)) 219 | await writer.drain() 220 | if (await reader.read(1024)): 221 | seq_no = 4 222 | 223 | session.add_auth_attempt( 224 | 'encrypted', username=username, password=password_enc) 225 | writer.write(self.auth_failed(seq_no, username, address, using_password)) 226 | await writer.drain() 227 | -------------------------------------------------------------------------------- /heralding/capabilities/pop3.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017 Johnny Vestergaard 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import logging 17 | 18 | from heralding.capabilities.handlerbase import HandlerBase 19 | 20 | logger = logging.getLogger(__name__) 21 | 22 | 23 | class Pop3(HandlerBase): 24 | max_tries = 10 25 | cmds = {} 26 | 27 | def __init__(self, options): 28 | super().__init__(options) 29 | Pop3.max_tries = int(self.options['protocol_specific_data']['max_attempts']) 30 | self.banner = self.options['protocol_specific_data']['banner'] 31 | 32 | async def execute_capability(self, reader, writer, session): 33 | await self._handle_session(session, reader, writer) 34 | 35 | async def _handle_session(self, session, reader, writer): 36 | await self.send_message(writer, self.banner) 37 | 38 | state = 'AUTHORIZATION' 39 | while state != '' and session.connected: 40 | raw_msg = await reader.readline() 41 | if not raw_msg: 42 | break 43 | 44 | raw_msg_str = str(raw_msg, 'utf-8') 45 | 46 | session.activity() 47 | cmd_msg = raw_msg_str.rstrip().split(' ', 1) 48 | if len(cmd_msg) == 0: 49 | continue 50 | elif len(cmd_msg) == 1: 51 | cmd = cmd_msg[0] 52 | msg = '' 53 | else: 54 | cmd = cmd_msg[0] 55 | msg = cmd_msg[1] 56 | 57 | cmd = cmd.lower() 58 | 59 | if cmd not in ['user', 'pass', 'quit', 'noop']: 60 | await self.send_message(writer, '-ERR Unknown command') 61 | else: 62 | func_to_call = getattr(self, 'cmd_{0}'.format(cmd), None) 63 | return_value = await func_to_call(session, reader, writer, msg) 64 | # state changers! 65 | if state == 'AUTHORIZATION' or cmd == 'quit': 66 | state = return_value 67 | 68 | session.end_session() 69 | 70 | # APOP mrose c4c9334bac560ecc979e58001b3e22fb 71 | # +OK mrose's maildrop has 2 messages (320 octets) 72 | def auth_apop(self, session, gsocket, msg): 73 | raise Exception('Not implemented yet!') 74 | 75 | # USER mrose 76 | # +OK User accepted 77 | # PASS tanstaaf 78 | # +OK Pass accepted 79 | # or: "-ERR Authentication failed." 80 | # or: "-ERR No username given." 81 | async def cmd_user(self, session, reader, writer, msg): 82 | session.vdata['USER'] = msg 83 | await self.send_message(writer, '+OK User accepted') 84 | return 'AUTHORIZATION' 85 | 86 | async def cmd_pass(self, session, reader, writer, msg): 87 | if 'USER' not in session.vdata: 88 | await self.send_message(writer, '-ERR No username given.') 89 | else: 90 | session.add_auth_attempt( 91 | 'plaintext', username=session.vdata['USER'], password=msg) 92 | await self.send_message(writer, "-ERR Authentication failed.") 93 | 94 | if 'USER' in session.vdata: 95 | del session.vdata['USER'] 96 | return 'AUTHORIZATION' 97 | 98 | async def cmd_noop(self, session, reader, writer, msg): 99 | await self.send_message(writer, '+OK') 100 | 101 | async def cmd_quit(self, session, reader, writer, msg): 102 | await self.send_message(writer, '+OK Logging out') 103 | return '' 104 | 105 | @staticmethod 106 | async def send_message(writer, msg): 107 | message_bytes = bytes(msg + "\n", 'utf-8') 108 | writer.write(message_bytes) 109 | await writer.drain() 110 | -------------------------------------------------------------------------------- /heralding/capabilities/pop3s.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017 Johnny Vestergaard 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import logging 17 | 18 | from heralding.capabilities.handlerbase import HandlerBase 19 | from heralding.capabilities.pop3 import Pop3 20 | 21 | logger = logging.getLogger(__name__) 22 | 23 | 24 | class Pop3S(Pop3, HandlerBase): 25 | """ 26 | This class will get wrapped in SSL. This is possible because we by convention wrap 27 | all capabilities that ends with the letter 's' in SSL.""" 28 | -------------------------------------------------------------------------------- /heralding/capabilities/postgresql.py: -------------------------------------------------------------------------------- 1 | import struct 2 | import logging 3 | 4 | from heralding.capabilities.handlerbase import HandlerBase 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | class PostgreSQL(HandlerBase): 10 | 11 | async def execute_capability(self, reader, writer, session): 12 | try: 13 | await self._handle_session(session, reader, writer) 14 | except struct.error as exc: 15 | logger.debug('PostgreSQL connection error: %s', exc) 16 | session.end_session() 17 | 18 | async def _handle_session(self, session, reader, writer): 19 | # Read (we blindly assume) SSL request and deny it 20 | await read_msg(reader, writer) 21 | 22 | writer.write(b'N') 23 | 24 | session.activity() 25 | 26 | # Read login details 27 | data = await read_msg(reader, writer) 28 | login_dict = parse_dict(data) 29 | 30 | # Request plain text password login 31 | password_request = [b'R', 8, 3] 32 | writer.write(struct.pack('>c I I', *password_request)) 33 | await writer.drain() 34 | 35 | # Read password 36 | data = await read_msg(reader, writer) 37 | password = parse_str(data) 38 | username = login_dict['user'] 39 | session.add_auth_attempt('plaintext', username=username, password=password) 40 | 41 | # Report login failure 42 | writer.write(b'E') 43 | fail = [ 44 | b'SFATAL\x00C28P01\x00', 45 | 'Mpassword authentication failed for user "{}"'.format(username).encode( 46 | 'utf-8'), 47 | b'\x00Fauth.c\x00L288\x00Rauth_failed\x00\x00', 48 | ] 49 | 50 | length = sum([len(f) for f in fail]) 51 | writer.write(struct.pack('>I', length + 4)) 52 | for f in fail: 53 | writer.write(f) 54 | 55 | await writer.drain() 56 | 57 | session.end_session() 58 | 59 | 60 | async def read_msg(reader, writer): 61 | i = await reader.read(4) 62 | length = struct.unpack('>I', i)[0] 63 | data = await reader.read(length) 64 | return data 65 | 66 | 67 | def parse_dict(data): 68 | dct = {} 69 | mode = 'pad' 70 | key = [] 71 | value = [] 72 | 73 | for c in struct.iter_unpack('c', data): 74 | c = c[0] 75 | 76 | if mode == 'pad': 77 | if c in (bytes([0]), bytes([3])): 78 | continue 79 | else: 80 | mode = 'key' 81 | 82 | if mode == 'key': 83 | if c == bytes([0]): 84 | mode = 'value' 85 | else: 86 | key.append(c.decode()) 87 | 88 | elif mode == 'value': 89 | if c == bytes([0]): 90 | dct[''.join(key)] = ''.join(value) 91 | key = [] 92 | value = [] 93 | mode = 'pad' 94 | else: 95 | value.append(c.decode()) 96 | 97 | return dct 98 | 99 | 100 | def parse_str(data): 101 | data_array = bytearray(data) 102 | return data_array[1:-1].decode('utf-8') 103 | -------------------------------------------------------------------------------- /heralding/capabilities/rdp.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019 Sudipta Pandit 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import struct 17 | import logging 18 | 19 | from heralding.capabilities.handlerbase import HandlerBase 20 | from heralding.libs.msrdp.pdu import x224ConnectionConfirmPDU, MCSConnectResponsePDU, MCSAttachUserConfirmPDU, MCSChannelJoinConfirmPDU 21 | from heralding.libs.msrdp.parser import x224ConnectionRequestPDU, MCSChannelJoinRequestPDU, ClientInfoPDU 22 | from heralding.libs.msrdp.parser import ErectDomainRequestPDU, tpktPDUParser 23 | from heralding.libs.msrdp.security import ServerSecurity 24 | from heralding.libs.msrdp.parser import InvalidExpectedData 25 | from heralding.libs.msrdp.tls import TLS, TLSHandshakeError 26 | logger = logging.getLogger(__name__) 27 | 28 | 29 | class RDP(HandlerBase): 30 | # will parse the TPKT header and read the entire packet (TPKT + payload) 31 | async def recv_next_tpkt(self, reader, tlsObj=None): 32 | # data buffer 33 | data = b"" 34 | if tlsObj: 35 | # read TPKT header 36 | data += await tlsObj.read_tls(4) 37 | tpkt = tpktPDUParser() 38 | tpkt.parse(data) 39 | # calculate the remaining bytes we need to read 40 | read_len = tpkt.length - 4 41 | # read remaining byets 42 | data += await tlsObj.read_tls(read_len) 43 | else: 44 | data = await reader.read(2048) 45 | 46 | return data 47 | 48 | async def send_data(self, writer, data, tlsObj=None): 49 | if tlsObj: 50 | await tlsObj.write_tls(data) 51 | return 52 | writer.write(data) 53 | await writer.drain() 54 | 55 | async def execute_capability(self, reader, writer, session): 56 | try: 57 | await self._handle_session(reader, writer, session) 58 | except struct.error as exc: 59 | logger.debug('RDP connection error: %s', exc) 60 | session.end_session() 61 | 62 | async def _handle_session(self, reader, writer, session): 63 | try: 64 | data = await self.recv_next_tpkt(reader) 65 | cr_pdu = x224ConnectionRequestPDU() 66 | cr_pdu.parse(data) 67 | 68 | client_reqProto = 1 # set default to tls 69 | if cr_pdu.reqProtocols: 70 | client_reqProto = cr_pdu.reqProtocols 71 | else: 72 | # if no nego request was made, then it is rdp security 73 | client_reqProto = 0 74 | 75 | cc_pdu_obj = x224ConnectionConfirmPDU(client_reqProto) 76 | cc_pdu = cc_pdu_obj.getFullPacket() 77 | await self.send_data(writer, cc_pdu) 78 | if cc_pdu_obj.sentNegoFail: 79 | logger.debug("Sent x224 RDP Negotiation Failure PDU") 80 | session.end_session() 81 | return 82 | logger.debug("Sent x244CLinetConnectionConfirm PDU") 83 | 84 | # TLS Upgrade start 85 | logger.debug("RDP TLS initilization") 86 | tls_obj = TLS(writer, reader, 'rdp.pem') 87 | await tls_obj.do_tls_handshake() 88 | 89 | # Now using send_data and recv_next_tpkt 90 | data = await self.recv_next_tpkt(reader, tls_obj) 91 | 92 | # This packet includes ServerSecurity data 93 | server_sec = ServerSecurity() 94 | mcs_cres = MCSConnectResponsePDU(client_reqProto, 95 | server_sec).getFullPacket() 96 | await self.send_data(writer, mcs_cres, tls_obj) 97 | 98 | data = await self.recv_next_tpkt(reader, tls_obj) 99 | if not data: 100 | logger.debug("Expected ErectDomainRequest. Got Nothing.") 101 | return 102 | if not ErectDomainRequestPDU.checkPDU(data): 103 | logger.debug("Malformed Packet Received. Expected ErectDomainRequest.") 104 | session.end_session() 105 | return 106 | 107 | logger.debug("Received: ErectDomainRequest" + repr(data)) 108 | 109 | data = await self.recv_next_tpkt(reader, tls_obj) 110 | logger.debug("Received: Attach User request : " + repr(data)) 111 | 112 | mcs_usrcnf = MCSAttachUserConfirmPDU().getFullPacket() 113 | await self.send_data(writer, mcs_usrcnf, tls_obj) 114 | logger.debug("Sent: Attach User Confirm") 115 | 116 | # Handle multiple Channel Join request PUDs 117 | for _ in range(7): 118 | # data = await reader.read(2048) 119 | data = await self.recv_next_tpkt(reader, tls_obj) 120 | if not data: 121 | logger.debug( 122 | "Expected: Channel Join/Client Security Packet.Got Nothing.") 123 | return 124 | channel_req = MCSChannelJoinRequestPDU() 125 | v = channel_req.parse(data) 126 | if v < 0: 127 | break 128 | channel_id = channel_req.channelID 129 | channel_init = channel_req.initiator 130 | channel_cnf = MCSChannelJoinConfirmPDU(channel_init, 131 | channel_id).getFullPacket() 132 | 133 | await self.send_data(writer, channel_cnf, tls_obj) 134 | logger.debug("Sent: MCS Channel Join Confirm of channel %s" % 135 | (channel_id)) 136 | 137 | # Handle Client Security Exchange PDU 138 | if not data: 139 | data = await self.recv_next_tpkt(reader, tls_obj) 140 | 141 | # There is no client security exchange in TLS Security 142 | client_info = ClientInfoPDU() 143 | client_info.parseTLS(data) 144 | username = client_info.rdpUsername 145 | password = client_info.rdpPassword 146 | session.add_auth_attempt( 147 | 'plaintext', username=username, password=password) 148 | 149 | session.end_session() 150 | except (InvalidExpectedData, TLSHandshakeError): 151 | logger.debug("Malformed packet detected. Closing session.") 152 | session.end_session() 153 | return 154 | -------------------------------------------------------------------------------- /heralding/capabilities/smtp.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2013 Aniket Panse 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | # Parts of this code are from secure-smtpd (https://github.com/bcoe/secure-smtpd) 17 | 18 | # Aniket Panse grants Johnny Vestergaard 19 | # a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable 20 | # copyright license to reproduce, prepare derivative works of, publicly 21 | # display, publicly perform, sublicense, relicense, and distribute [the] Contributions 22 | # and such derivative works. 23 | 24 | import time 25 | import random 26 | import base64 27 | import socket 28 | import asyncio 29 | import logging 30 | 31 | from aiosmtpd.smtp import SMTP, MISSING, syntax 32 | 33 | from heralding.capabilities.handlerbase import HandlerBase 34 | 35 | log = logging.getLogger(__name__) 36 | 37 | 38 | class SMTPHandler(SMTP): 39 | fqdn = '' 40 | 41 | def __init__(self, reader, writer, session, options): 42 | self.banner = options['protocol_specific_data']['banner'] 43 | super().__init__(None, hostname=self.banner) 44 | # Reset standard banner. 45 | self.__ident__ = "" 46 | self._reader = reader 47 | self._writer = writer 48 | self.transport = writer 49 | 50 | self._set_rset_state() 51 | self.session = session 52 | self.session.peer = self.transport.get_extra_info('peername') 53 | self.session.extended_smtp = None 54 | self.session.host_name = None 55 | 56 | async def push(self, status): 57 | response = bytes(status + '\r\n', 58 | 'utf-8' if self.enable_SMTPUTF8 else 'ascii') 59 | self._writer.write(response) 60 | log.debug(response) 61 | try: 62 | await self._writer.drain() 63 | except ConnectionResetError: 64 | self.stop() 65 | if self._reader.at_eof(): 66 | self.stop() 67 | 68 | @syntax('EHLO hostname') 69 | async def smtp_EHLO(self, hostname): 70 | if not hostname: 71 | await self.push('501 Syntax: EHLO hostname') 72 | return 73 | self._set_rset_state() 74 | await self.push('250-{0} Hello {1}'.format(self.hostname, hostname)) 75 | await self.push('250-AUTH PLAIN LOGIN CRAM-MD5') 76 | await self.push('250 EHLO') 77 | 78 | @syntax("AUTH mechanism [initial-response]") 79 | async def smtp_AUTH(self, arg): 80 | if not arg: 81 | await self.push('500 Not enough values') 82 | return 83 | args = arg.split() 84 | if len(args) > 2: 85 | await self.push('500 Too many values') 86 | return 87 | mechanism = args[0] 88 | if mechanism == 'PLAIN': 89 | if len(args) == 1: 90 | await self.push('334 ') # wait for client login/password 91 | line = await self.readline() 92 | if not line: 93 | return 94 | blob = line.strip() 95 | else: 96 | blob = args[1].encode() 97 | 98 | try: 99 | loginpassword = base64.b64decode(blob) 100 | except Exception: 101 | await self.push("501 Can't decode base64") 102 | return 103 | try: 104 | _, login, password = loginpassword.split(b"\x00") 105 | except ValueError: # not enough args 106 | await self.push("500 Can't split auth value") 107 | return 108 | self.session.add_auth_attempt( 109 | 'PLAIN', 110 | username=str(login, 'utf-8'), 111 | password=str(password, 'utf-8')) 112 | elif mechanism == 'LOGIN': 113 | if len(args) > 1: 114 | username = str(base64.b64decode(args[1]), 'utf-8') 115 | await self.push('334 ' + str(base64.b64encode(b'Password:'), 'utf-8')) 116 | 117 | password_bytes = await self.readline() 118 | if not password_bytes: 119 | return 120 | password = str(base64.b64decode(password_bytes), 'utf-8') 121 | self.session.add_auth_attempt( 122 | 'LOGIN', username=username, password=password) 123 | else: 124 | await self.push('334 ' + str(base64.b64encode(b'Username:'), 'utf-8')) 125 | 126 | username_bytes = await self.readline() 127 | if not username_bytes: 128 | return 129 | 130 | await self.push('334 ' + str(base64.b64encode(b'Password:'), 'utf-8')) 131 | 132 | password_bytes = await self.readline() 133 | if not password_bytes: 134 | return 135 | self.session.add_auth_attempt( 136 | 'LOGIN', 137 | username=str(base64.b64decode(username_bytes), 'utf-8'), 138 | password=str(base64.b64decode(password_bytes), 'utf-8')) 139 | elif mechanism == 'CRAM-MD5': 140 | r = random.randint(5000, 20000) 141 | t = int(time.time()) 142 | 143 | # challenge is of the form '<24609.1047914046@awesome.host.com>' 144 | sent_cram_challenge = "<" + str(r) + "." + str( 145 | t) + "@" + SMTPHandler.fqdn + ">" 146 | cram_challenge_bytes = bytes(sent_cram_challenge, 'utf-8') 147 | await self.push("334 " + 148 | str(base64.b64encode(cram_challenge_bytes), 'utf-8')) 149 | 150 | credentials_bytes = await self.readline() 151 | if not credentials_bytes: 152 | return 153 | credentials = str(base64.b64decode(credentials_bytes), 'utf-8') 154 | if sent_cram_challenge is None or ' ' not in credentials: 155 | await self.push('451 Internal confusion') 156 | return 157 | username, digest = credentials.split() 158 | self.session.add_auth_attempt( 159 | 'cram_md5', 160 | username=username, 161 | digest=digest, 162 | challenge=sent_cram_challenge) 163 | await self.push('535 authentication failed') 164 | else: 165 | await self.push('500 incorrect AUTH mechanism') 166 | return 167 | status = '535 authentication failed' 168 | await self.push(status) 169 | 170 | @syntax('QUIT') 171 | async def smtp_QUIT(self, arg): 172 | if arg: 173 | await self.push('501 Syntax: QUIT') 174 | else: 175 | status = await self._call_handler_hook('QUIT') 176 | await self.push('221 Bye' if status is MISSING else status) 177 | self.stop() 178 | 179 | async def readline(self): 180 | line = b'' 181 | try: 182 | line = await self._reader.readline() 183 | except ConnectionResetError: 184 | self.stop() 185 | else: 186 | return line 187 | 188 | def stop(self): 189 | if self.transport is not None: 190 | self.transport.close() 191 | self.transport = None 192 | 193 | def _timeout_cb(self): 194 | if self.transport is not None: 195 | super()._timeout_cb() 196 | 197 | class smtp(HandlerBase): 198 | 199 | def __init__(self, options): 200 | super().__init__(options) 201 | self._options = options 202 | 203 | async def execute_capability(self, reader, writer, session): 204 | fqdn_task = asyncio.ensure_future(self.setfqdn()) 205 | 206 | smtp_cap = SMTPHandler(reader, writer, session, self._options) 207 | smtp_task = asyncio.ensure_future(smtp_cap._handle_client()) 208 | 209 | await smtp_task 210 | 211 | fqdn_task.cancel() 212 | try: 213 | await fqdn_task 214 | except asyncio.CancelledError: 215 | pass 216 | 217 | async def setfqdn(self): 218 | loop = asyncio.get_running_loop() 219 | if 'fqdn' in self._options['protocol_specific_data'] and self._options[ 220 | 'protocol_specific_data']['fqdn']: 221 | SMTPHandler.fqdn = self._options['protocol_specific_data']['fqdn'] 222 | else: 223 | while True: 224 | fqdn = await loop.run_in_executor(None, socket.getfqdn) 225 | SMTPHandler.fqdn = fqdn 226 | await asyncio.sleep(1800) 227 | -------------------------------------------------------------------------------- /heralding/capabilities/smtps.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2020 Satoshi Mimura 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import logging 17 | 18 | from heralding.capabilities.handlerbase import HandlerBase 19 | from heralding.capabilities.smtp import smtp 20 | 21 | logger = logging.getLogger(__name__) 22 | 23 | 24 | class smtps(smtp, HandlerBase): 25 | """ 26 | This class will get wrapped in SSL. This is possible because we by convention wrap 27 | all capabilities that ends with the letter 's' in SSL.""" 28 | -------------------------------------------------------------------------------- /heralding/capabilities/socks5.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2018 Roman Samoilenko 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import logging 17 | 18 | from heralding.capabilities.handlerbase import HandlerBase 19 | 20 | # Socks constants 21 | SOCKS_VERSION = b'\x05' 22 | AUTH_METHOD = b'\x02' # username/password authentication. RFC 1929 23 | SOCKS_FAIL = b'\xFF' 24 | 25 | logger = logging.getLogger(__name__) 26 | 27 | 28 | class Socks5(HandlerBase): 29 | 30 | async def execute_capability(self, reader, writer, session): 31 | await self._handle_session(reader, writer, session) 32 | 33 | async def _handle_session(self, reader, writer, session): 34 | # 257 - max bytes number for greeting according to RFC 1928 35 | greeting = await reader.read(257) 36 | if len(greeting) > 2: 37 | await self.try_authenticate(reader, writer, session, greeting) 38 | else: 39 | logger.debug("Incorrect client greeting string: %r" % greeting) 40 | session.end_session() 41 | 42 | async def try_authenticate(self, reader, writer, session, greeting): 43 | version, authmethods = self.unpack_msg(greeting) 44 | if version == SOCKS_VERSION: 45 | if AUTH_METHOD in authmethods: 46 | await self.do_authenticate(reader, writer, session) 47 | else: 48 | writer.write(SOCKS_VERSION + SOCKS_FAIL) 49 | await writer.drain() 50 | session.set_auxiliary_data(self.get_auxiliary_data(authmethods)) 51 | else: 52 | logger.debug("Wrong socks version: %r" % version) 53 | 54 | async def do_authenticate(self, reader, writer, session): 55 | writer.write(SOCKS_VERSION + AUTH_METHOD) 56 | await writer.drain() 57 | # 513 - max bytes number for username/password auth according to RFC 1929 58 | auth_data = await reader.read(513) 59 | if len(auth_data) > 2: 60 | username, password = self.unpack_auth(auth_data) 61 | session.add_auth_attempt( 62 | 'plaintext', username=username.decode(), password=password.decode()) 63 | writer.write(AUTH_METHOD + SOCKS_FAIL) 64 | await writer.drain() 65 | else: 66 | logger.debug("Wrong authentication data: %r" % auth_data) 67 | 68 | def get_auxiliary_data(self, authmethods): 69 | _methods = [] 70 | for m in authmethods: 71 | if m == 2: 72 | _methods.append("USERNAME/PASSWORD") 73 | elif m == 0: 74 | _methods.append("NO AUTHENTICATION REQUIRED") 75 | elif m == 1: 76 | _methods.append("GSSAPI") 77 | elif 3 <= m <= 127: 78 | _methods.append("IANA ASSIGNED(%s)" % hex(m)) 79 | elif 128 <= m <= 254: 80 | _methods.append("PRIVATE METHOD(%s)" % hex(m)) 81 | elif m == 255: 82 | _methods.append("NO ACCEPTABLE METHODS") 83 | 84 | data = {"client_auth_methods": _methods} 85 | return data 86 | 87 | @staticmethod 88 | def unpack_msg(data): 89 | socks_version = data[:1] # we need byte representation 90 | authmethods_n = data[1] 91 | authmethods = data[2:2 + authmethods_n] 92 | return socks_version, authmethods 93 | 94 | @staticmethod 95 | def unpack_auth(auth_data): 96 | ulen = auth_data[1] 97 | username = auth_data[2:2 + ulen] 98 | password_part = auth_data[2 + ulen:] 99 | if password_part: 100 | plen = password_part[0] 101 | password = auth_data[-plen:] 102 | else: 103 | password = b"" 104 | return username, password 105 | -------------------------------------------------------------------------------- /heralding/capabilities/ssh.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017 Roman Samoilenko 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import os 17 | import logging 18 | import functools 19 | 20 | from heralding.capabilities.handlerbase import HandlerBase 21 | 22 | import asyncssh 23 | from Crypto.PublicKey import RSA 24 | 25 | logger = logging.getLogger(__name__) 26 | 27 | 28 | class SSH(asyncssh.SSHServer, HandlerBase): 29 | connections_list = [] 30 | 31 | def __init__(self, options): 32 | asyncssh.SSHServer.__init__(self) 33 | HandlerBase.__init__(self, options) 34 | 35 | def connection_made(self, conn): 36 | SSH.connections_list.append(conn) 37 | self.address = conn.get_extra_info('peername') 38 | self.dest_address = conn.get_extra_info('sockname') 39 | self.connection = conn 40 | self.handle_connection() 41 | logger.debug('SSH connection received from %s.' % 42 | conn.get_extra_info('peername')[0]) 43 | 44 | def connection_lost(self, exc): 45 | self.session.set_auxiliary_data(self.get_auxiliary_data()) 46 | self.close_session(self.session) 47 | if exc: 48 | logger.debug('SSH connection error: ' + str(exc)) 49 | else: 50 | logger.debug('SSH connection closed.') 51 | 52 | def begin_auth(self, username): 53 | return True 54 | 55 | def password_auth_supported(self): 56 | return True 57 | 58 | def validate_password(self, username, password): 59 | self.session.add_auth_attempt( 60 | 'plaintext', username=username, password=password) 61 | return False 62 | 63 | def handle_connection(self): 64 | if HandlerBase.global_sessions > HandlerBase.MAX_GLOBAL_SESSIONS: 65 | protocol = self.__class__.__name__.lower() 66 | logger.warning( 67 | 'Got {0} session on port {1} from {2}:{3}, but not handling it because the global session limit has ' 68 | 'been reached'.format(protocol, self.port, *self.address)) 69 | else: 70 | self.session = self.create_session(self.address, self.dest_address) 71 | 72 | def get_auxiliary_data(self): 73 | data_fields = [ 74 | 'client_version', 'recv_cipher', 'recv_mac', 'recv_compression' 75 | ] 76 | data = {f: self.connection.get_extra_info(f) for f in data_fields} 77 | return data 78 | 79 | @staticmethod 80 | def change_server_banner(banner): 81 | """_send_version code was copied from asyncssh.connection in order to change 82 | internal local variable 'version', providing custom banner.""" 83 | 84 | @functools.wraps(asyncssh.connection.SSHConnection._send_version) 85 | def _send_version(self): 86 | """Start the SSH handshake""" 87 | 88 | version = bytes(banner, 'utf-8') 89 | 90 | if self.is_client(): 91 | self._client_version = version 92 | self._extra.update(client_version=version.decode('ascii')) 93 | else: 94 | self._server_version = version 95 | self._extra.update(server_version=version.decode('ascii')) 96 | 97 | self._send(version + b'\r\n') 98 | 99 | asyncssh.connection.SSHConnection._send_version = _send_version 100 | 101 | @staticmethod 102 | def generate_ssh_key(ssh_key_file): 103 | if not os.path.isfile(ssh_key_file): 104 | with open(ssh_key_file, 'w') as _file: 105 | rsa_key = RSA.generate(2048) 106 | priv_key_text = str(rsa_key.exportKey('PEM', pkcs=1), 'utf-8') 107 | _file.write(priv_key_text) 108 | -------------------------------------------------------------------------------- /heralding/capabilities/telnet.py: -------------------------------------------------------------------------------- 1 | # pylint: disable-msg=E1101 2 | # Copyright (C) 2017 Johnny Vestergaard 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import asyncio 18 | import curses 19 | import logging 20 | 21 | from heralding.capabilities.handlerbase import HandlerBase 22 | from heralding.libs.telnetsrv.telnetsrvlib import TelnetHandlerBase 23 | 24 | logger = logging.getLogger(__name__) 25 | 26 | 27 | class Telnet(HandlerBase): 28 | 29 | def __init__(self, options): 30 | super().__init__(options) 31 | TelnetWrapper.max_tries = int( 32 | self.options['protocol_specific_data']['max_attempts']) 33 | 34 | async def execute_capability(self, reader, writer, session): 35 | telnet_cap = TelnetWrapper(reader, writer, session) 36 | await telnet_cap.run() 37 | 38 | 39 | class TelnetWrapper(TelnetHandlerBase): 40 | """ 41 | Wraps the telnetsrv module to fit the Honeypot architecture. 42 | """ 43 | PROMPT = b'$ ' 44 | max_tries = 3 45 | TERM = 'ansi' 46 | 47 | authNeedUser = True 48 | authNeedPass = True 49 | 50 | def __init__(self, reader, writer, session): 51 | self.auth_count = 0 52 | self.username = None 53 | self.session = session 54 | address = writer.get_extra_info('address') 55 | loop = asyncio.get_running_loop() 56 | super().__init__(reader, writer, address, loop=loop) 57 | 58 | async def authentication_ok(self): 59 | while self.auth_count < TelnetWrapper.max_tries: 60 | username = await self.readline(prompt=b"Username: ", use_history=False) 61 | password = await self.readline( 62 | echo=False, prompt=b"Password: ", use_history=False) 63 | self.session.add_auth_attempt( 64 | _type='plaintext', 65 | username=str(username, 'utf-8'), 66 | password=str(password, 'utf-8')) 67 | if self.DOECHO: 68 | self.write(b"\n") 69 | self.auth_count += 1 70 | self.writeline(b'Username: ') # It fixes a problem with Hydra bruteforcer. 71 | return False 72 | 73 | def setterm(self, term): 74 | # Dummy file for the purpose of tests. 75 | with open('/dev/null', 'w') as f: 76 | curses.setupterm( 77 | term, f.fileno()) # This will raise if the termtype is not supported 78 | self.TERM = term 79 | self.ESCSEQ = {} 80 | for k in self.KEYS.keys(): 81 | str_ = curses.tigetstr(curses.has_key._capability_names[k]) 82 | if str_: 83 | self.ESCSEQ[str_] = k 84 | self.CODES['DEOL'] = curses.tigetstr('el') 85 | self.CODES['DEL'] = curses.tigetstr('dch1') 86 | self.CODES['INS'] = curses.tigetstr('ich1') 87 | self.CODES['CSRLEFT'] = curses.tigetstr('cub1') 88 | self.CODES['CSRRIGHT'] = curses.tigetstr('cuf1') 89 | 90 | def session_end(self): 91 | self.session.end_session() 92 | -------------------------------------------------------------------------------- /heralding/capabilities/vnc.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2013 Aniket Panse 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import os 17 | import logging 18 | import binascii 19 | 20 | from heralding.capabilities.handlerbase import HandlerBase 21 | from heralding.libs.cracker.vnc import crack_hash 22 | 23 | # VNC constants 24 | RFB_VERSION = b'RFB 003.007\n' 25 | AUTH_METHODS = b'\x01\x02' 26 | VNC_AUTH = b'\x02' 27 | AUTH_FAILED = b'\x00\x00\x00\x01' 28 | 29 | logger = logging.getLogger(__name__) 30 | 31 | 32 | class Vnc(HandlerBase): 33 | 34 | async def execute_capability(self, reader, writer, session): 35 | await self._handle_session(reader, writer, session) 36 | 37 | async def _handle_session(self, reader, writer, session): 38 | writer.write(RFB_VERSION) 39 | client_version = await reader.read(1024) 40 | 41 | if client_version == RFB_VERSION: 42 | await self.security_handshake(reader, writer, session) 43 | else: 44 | session.end_session() 45 | 46 | async def security_handshake(self, reader, writer, session): 47 | writer.write(AUTH_METHODS) 48 | sec_method = await reader.read(1024) 49 | 50 | if sec_method == VNC_AUTH: 51 | await self.do_vnc_authentication(reader, writer, session) 52 | else: 53 | session.end_session() 54 | 55 | async def do_vnc_authentication(self, reader, writer, session): 56 | challenge = os.urandom(16) 57 | writer.write(challenge) 58 | 59 | client_response = await reader.read(1024) 60 | writer.write(AUTH_FAILED) 61 | 62 | # try to decrypt the hash 63 | dkey = crack_hash(challenge, client_response) 64 | if dkey: 65 | session.add_auth_attempt('cracked', password=dkey) 66 | else: 67 | hash_data = { 68 | 'challenge': binascii.hexlify(challenge).decode(), 69 | 'response': binascii.hexlify(client_response).decode() 70 | } 71 | session.add_auth_attempt('des_challenge', password_hash=hash_data) 72 | 73 | session.end_session() 74 | -------------------------------------------------------------------------------- /heralding/heralding.yml: -------------------------------------------------------------------------------- 1 | # will request and log the public ip every hours from ipify 2 | public_ip_as_destination_ip: false 3 | 4 | # ip address to listen on 5 | bind_host: 0.0.0.0 6 | 7 | # logging of sessions and authentication attempts 8 | activity_logging: 9 | file: 10 | enabled: true 11 | # Session details common for all protocols (capabilities) in CSV format, 12 | # written to file when the session ends. Set to "" to disable. 13 | session_csv_log_file: "log_session.csv" 14 | # Complete session details (including protocol specific data) in JSONL format, 15 | # written to file when the session ends. Set to "" to disable 16 | session_json_log_file: "log_session.json" 17 | # Writes each authentication attempt to file, including credentials, 18 | # set to "" to disable 19 | authentication_log_file: "log_auth.csv" 20 | 21 | syslog: 22 | enabled: false 23 | 24 | hpfeeds: 25 | enabled: false 26 | session_channel: "heralding.session" 27 | auth_channel: "heralding.auth" 28 | host: 29 | port: 20000 30 | ident: 31 | secret: 32 | 33 | curiosum: 34 | enabled: false 35 | port: 23400 36 | 37 | hash_cracker: 38 | enabled: true 39 | wordlist_file: 'wordlist.txt' 40 | 41 | # protocols to enable 42 | capabilities: 43 | ftp: 44 | enabled: true 45 | port: 21 46 | timeout: 30 47 | protocol_specific_data: 48 | max_attempts: 3 49 | banner: "Microsoft FTP Server" 50 | syst_type: "Windows-NT" 51 | 52 | telnet: 53 | enabled: true 54 | port: 23 55 | timeout: 30 56 | protocol_specific_data: 57 | max_attempts: 3 58 | 59 | pop3: 60 | enabled: true 61 | port: 110 62 | timeout: 30 63 | protocol_specific_data: 64 | max_attempts: 3 65 | banner: "+OK POP3 server ready" 66 | 67 | pop3s: 68 | enabled: true 69 | port: 995 70 | timeout: 30 71 | protocol_specific_data: 72 | max_attempts: 3 73 | banner: "+OK POP3 server ready" 74 | # if a .pem file is not found in work dir, a new pem file will be created 75 | # using these values 76 | cert: 77 | common_name: "*" 78 | country: "US" 79 | state: None 80 | locality: None 81 | organization: None 82 | organizational_unit: None 83 | # how many days should the certificate be valid for 84 | valid_days: 365 85 | serial_number: 0 86 | 87 | postgresql: 88 | enabled: true 89 | port: 5432 90 | timeout: 30 91 | 92 | imap: 93 | enabled: true 94 | port: 143 95 | timeout: 30 96 | protocol_specific_data: 97 | max_attempts: 3 98 | banner: "* OK IMAP4rev1 Server Ready" 99 | 100 | imaps: 101 | enabled: true 102 | port: 993 103 | timeout: 30 104 | protocol_specific_data: 105 | max_attempts: 3 106 | banner: "* OK IMAP4rev1 Server Ready" 107 | # if a .pem file is not found in work dir, a new pem file will be created 108 | # using these values 109 | cert: 110 | common_name: "*" 111 | country: "US" 112 | state: None 113 | locality: None 114 | organization: None 115 | organizational_unit: None 116 | # how many days should the certificate be valid for 117 | valid_days: 365 118 | serial_number: 0 119 | 120 | ssh: 121 | enabled: true 122 | port: 22 123 | timeout: 30 124 | protocol_specific_data: 125 | banner: "SSH-2.0-OpenSSH_6.6.1p1 Ubuntu-2ubuntu2.8" 126 | 127 | http: 128 | enabled: true 129 | port: 80 130 | timeout: 30 131 | protocol_specific_data: 132 | banner: "" 133 | 134 | https: 135 | enabled: true 136 | port: 443 137 | timeout: 30 138 | protocol_specific_data: 139 | banner: "" 140 | # if a .pem file is not found in work dir, a new pem file will be created 141 | # using these values 142 | cert: 143 | common_name: "*" 144 | country: "US" 145 | state: None 146 | locality: None 147 | organization: None 148 | organizational_unit: None 149 | # how many days should the certificate be valid for 150 | valid_days: 365 151 | serial_number: 0 152 | 153 | smtp: 154 | enabled: true 155 | port: 25 156 | timeout: 30 157 | protocol_specific_data: 158 | banner: "Microsoft ESMTP MAIL service ready" 159 | # If the fqdn option is commented out or empty, then fqdn of the host will be used 160 | fqdn: "" 161 | 162 | smtps: 163 | enabled: true 164 | port: 465 165 | timeout: 30 166 | protocol_specific_data: 167 | banner: "Microsoft ESMTP MAIL service ready" 168 | # If the fqdn option is commented out or empty, then fqdn of the host will be used 169 | fqdn: "" 170 | cert: 171 | common_name: "*" 172 | country: "US" 173 | state: None 174 | locality: None 175 | organization: None 176 | organizational_unit: None 177 | # how many days should the certificate be valid for 178 | valid_days: 365 179 | serial_number: 0 180 | 181 | vnc: 182 | enabled: true 183 | port: 5900 184 | timeout: 30 185 | 186 | socks5: 187 | enabled: true 188 | port: 1080 189 | timeout: 30 190 | 191 | mysql: 192 | enabled: true 193 | port: 3306 194 | timeout: 30 195 | 196 | 197 | rdp: 198 | enabled: true 199 | port: 3389 200 | timeout: 30 201 | protocol_specific_data: 202 | banner: "" 203 | # if a .pem file is not found in work dir, a new pem file will be created 204 | # using these values 205 | cert: 206 | common_name: "*" 207 | country: "US" 208 | state: None 209 | locality: None 210 | organization: None 211 | organizational_unit: None 212 | # how many days should the certificate be valid for 213 | valid_days: 365 214 | serial_number: 0 215 | -------------------------------------------------------------------------------- /heralding/libs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnnykv/heralding/ac12724ab38c4e2fe78f07d1bc35e6e586ba69c0/heralding/libs/__init__.py -------------------------------------------------------------------------------- /heralding/libs/aiobaserequest.py: -------------------------------------------------------------------------------- 1 | # We need this in order to reduce the number of third-party modules. 2 | # It is a part of code from socketserver, that adjusted to work with 3 | # asyncio in our specific case. 4 | 5 | 6 | class AsyncBaseRequestHandler: 7 | """Asynchronous analogue of socketserver.BaseRequestHandler.""" 8 | 9 | def __init__(self, reader, writer, client_address): 10 | self.rfile = reader 11 | self.wfile = writer 12 | self.client_address = client_address 13 | 14 | async def run(self): 15 | self.setup() 16 | try: 17 | await self.handle() 18 | finally: 19 | self.finish() 20 | 21 | def setup(self): 22 | pass 23 | 24 | async def handle(self): 25 | pass 26 | 27 | def finish(self): 28 | pass 29 | -------------------------------------------------------------------------------- /heralding/libs/cracker/vnc.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | from Crypto.Cipher import DES 3 | 4 | import heralding 5 | 6 | 7 | def get_vnc_key(key): 8 | bit_flip = [ 9 | 0x00, 0x80, 0x40, 0xC0, 0x20, 0xA0, 0x60, 0xE0, 0x10, 0x90, 0x50, 0xD0, 10 | 0x30, 0xB0, 0x70, 0xF0, 0x08, 0x88, 0x48, 0xC8, 0x28, 0xA8, 0x68, 0xE8, 11 | 0x18, 0x98, 0x58, 0xD8, 0x38, 0xB8, 0x78, 0xF8, 0x04, 0x84, 0x44, 0xC4, 12 | 0x24, 0xA4, 0x64, 0xE4, 0x14, 0x94, 0x54, 0xD4, 0x34, 0xB4, 0x74, 0xF4, 13 | 0x0C, 0x8C, 0x4C, 0xCC, 0x2C, 0xAC, 0x6C, 0xEC, 0x1C, 0x9C, 0x5C, 0xDC, 14 | 0x3C, 0xBC, 0x7C, 0xFC, 0x02, 0x82, 0x42, 0xC2, 0x22, 0xA2, 0x62, 0xE2, 15 | 0x12, 0x92, 0x52, 0xD2, 0x32, 0xB2, 0x72, 0xF2, 0x0A, 0x8A, 0x4A, 0xCA, 16 | 0x2A, 0xAA, 0x6A, 0xEA, 0x1A, 0x9A, 0x5A, 0xDA, 0x3A, 0xBA, 0x7A, 0xFA, 17 | 0x06, 0x86, 0x46, 0xC6, 0x26, 0xA6, 0x66, 0xE6, 0x16, 0x96, 0x56, 0xD6, 18 | 0x36, 0xB6, 0x76, 0xF6, 0x0E, 0x8E, 0x4E, 0xCE, 0x2E, 0xAE, 0x6E, 0xEE, 19 | 0x1E, 0x9E, 0x5E, 0xDE, 0x3E, 0xBE, 0x7E, 0xFE, 0x01, 0x81, 0x41, 0xC1, 20 | 0x21, 0xA1, 0x61, 0xE1, 0x11, 0x91, 0x51, 0xD1, 0x31, 0xB1, 0x71, 0xF1, 21 | 0x09, 0x89, 0x49, 0xC9, 0x29, 0xA9, 0x69, 0xE9, 0x19, 0x99, 0x59, 0xD9, 22 | 0x39, 0xB9, 0x79, 0xF9, 0x05, 0x85, 0x45, 0xC5, 0x25, 0xA5, 0x65, 0xE5, 23 | 0x15, 0x95, 0x55, 0xD5, 0x35, 0xB5, 0x75, 0xF5, 0x0D, 0x8D, 0x4D, 0xCD, 24 | 0x2D, 0xAD, 0x6D, 0xED, 0x1D, 0x9D, 0x5D, 0xDD, 0x3D, 0xBD, 0x7D, 0xFD, 25 | 0x03, 0x83, 0x43, 0xC3, 0x23, 0xA3, 0x63, 0xE3, 0x13, 0x93, 0x53, 0xD3, 26 | 0x33, 0xB3, 0x73, 0xF3, 0x0B, 0x8B, 0x4B, 0xCB, 0x2B, 0xAB, 0x6B, 0xEB, 27 | 0x1B, 0x9B, 0x5B, 0xDB, 0x3B, 0xBB, 0x7B, 0xFB, 0x07, 0x87, 0x47, 0xC7, 28 | 0x27, 0xA7, 0x67, 0xE7, 0x17, 0x97, 0x57, 0xD7, 0x37, 0xB7, 0x77, 0xF7, 29 | 0x0F, 0x8F, 0x4F, 0xCF, 0x2F, 0xAF, 0x6F, 0xEF, 0x1F, 0x9F, 0x5F, 0xDF, 30 | 0x3F, 0xBF, 0x7F, 0xFF 31 | ] 32 | 33 | # pad or trucate upto 8 bytes 34 | key = (key + bytes(8))[:8] 35 | res = bytearray(8) 36 | i = 0 37 | for b in key: 38 | res[i] = bit_flip[b] 39 | i += 1 40 | 41 | return bytes(res) 42 | 43 | 44 | def vnc_hash_check(challenge, response, vnc_key): 45 | """ @param: all params in byte encoded 46 | @return: True if decrypted response matches with challenge""" 47 | cipher = DES.new(vnc_key, DES.MODE_ECB) 48 | drs = cipher.decrypt(response) 49 | if drs == challenge: 50 | return True 51 | else: 52 | return False 53 | 54 | 55 | def crack_hash(challenge, response): 56 | # to overcome circular import 57 | password_list = heralding.honeypot.Honeypot.wordlist 58 | 59 | if password_list: 60 | for potential_password in password_list: 61 | vnc_key = get_vnc_key(potential_password.encode('ascii')) 62 | if (vnc_hash_check(challenge, response, vnc_key)): 63 | return potential_password 64 | return None 65 | -------------------------------------------------------------------------------- /heralding/libs/http/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnnykv/heralding/ac12724ab38c4e2fe78f07d1bc35e6e586ba69c0/heralding/libs/http/__init__.py -------------------------------------------------------------------------------- /heralding/libs/http/aioclient.py: -------------------------------------------------------------------------------- 1 | # We need this in order to reduce the number of third-party modules. 2 | # It is a part of code from http.client, that adjusted to work with 3 | # asyncio in our specific case. 4 | 5 | import email.parser 6 | from http.client import HTTPMessage 7 | 8 | 9 | async def parse_headers(fp, _class=HTTPMessage): 10 | headers = [] 11 | while True: 12 | line = await fp.readline() 13 | headers.append(line) 14 | if line in (b'\r\n', b'\n', b''): 15 | break 16 | hstring = b''.join(headers).decode('iso-8859-1') 17 | return email.parser.Parser(_class=_class).parsestr(hstring) 18 | -------------------------------------------------------------------------------- /heralding/libs/http/aioserver.py: -------------------------------------------------------------------------------- 1 | # We need this in order to reduce the number of third-party modules. 2 | # The main idea is to replace rfile/wfile with reader/writer and to 3 | # add async/await syntax. So, it is http.server.BaseHTTPRequestHandler 4 | # code, but adjusted to work with asyncio in our specific case. 5 | 6 | import socket 7 | import http.client 8 | 9 | from http import HTTPStatus 10 | from http.server import BaseHTTPRequestHandler 11 | 12 | from .aioclient import parse_headers 13 | from heralding.libs.aiobaserequest import AsyncBaseRequestHandler 14 | 15 | # It is a hack in order to get the same class as BaseHTTPRequestHandler, 16 | # but which inherits our AsyncBaseRequestHandler. 17 | AsyncHttpHandler = type('AsyncHttpHandler', (AsyncBaseRequestHandler,), 18 | dict(BaseHTTPRequestHandler.__dict__)) 19 | 20 | 21 | class AsyncBaseHTTPRequestHandler(AsyncHttpHandler): 22 | """Asynchronous analogue of http.server.BaseHTTPRequestHandler.""" 23 | 24 | async def parse_request(self): 25 | self.command = None # set in case of error on the first line 26 | self.request_version = version = self.default_request_version 27 | self.close_connection = True 28 | requestline = str(self.raw_requestline, 'iso-8859-1') 29 | requestline = requestline.rstrip('\r\n') 30 | self.requestline = requestline 31 | words = requestline.split() 32 | if len(words) == 3: 33 | command, path, version = words 34 | if version[:5] != 'HTTP/': 35 | self.send_error(HTTPStatus.BAD_REQUEST, 36 | "Bad request version (%r)" % version) 37 | return False 38 | try: 39 | base_version_number = version.split('/', 1)[1] 40 | version_number = base_version_number.split(".") 41 | if len(version_number) != 2: 42 | raise ValueError 43 | version_number = int(version_number[0]), int(version_number[1]) 44 | except (ValueError, IndexError): 45 | self.send_error(HTTPStatus.BAD_REQUEST, 46 | "Bad request version (%r)" % version) 47 | return False 48 | if version_number >= (1, 1) and self.protocol_version >= "HTTP/1.1": 49 | self.close_connection = False 50 | if version_number >= (2, 0): 51 | self.send_error(HTTPStatus.HTTP_VERSION_NOT_SUPPORTED, 52 | "Invalid HTTP Version (%s)" % base_version_number) 53 | return False 54 | elif len(words) == 2: 55 | command, path = words 56 | self.close_connection = True 57 | if command != 'GET': 58 | self.send_error(HTTPStatus.BAD_REQUEST, 59 | "Bad HTTP/0.9 request type (%r)" % command) 60 | return False 61 | elif not words: 62 | return False 63 | else: 64 | self.send_error(HTTPStatus.BAD_REQUEST, 65 | "Bad request syntax (%r)" % requestline) 66 | return False 67 | self.command, self.path, self.request_version = command, path, version 68 | 69 | # Examine the headers and look for a Connection directive. 70 | try: 71 | self.headers = await parse_headers(self.rfile, _class=self.MessageClass) 72 | except http.client.LineTooLong: 73 | self.send_error(HTTPStatus.BAD_REQUEST, "Line too long") 74 | return False 75 | except http.client.HTTPException as err: 76 | self.send_error(HTTPStatus.REQUEST_HEADER_FIELDS_TOO_LARGE, 77 | "Too many headers", str(err)) 78 | return False 79 | 80 | conntype = self.headers.get('Connection', "") 81 | if conntype.lower() == 'close': 82 | self.close_connection = True 83 | elif (conntype.lower() == 'keep-alive' and 84 | self.protocol_version >= "HTTP/1.1"): 85 | self.close_connection = False 86 | # Examine the headers and look for an Expect directive 87 | expect = self.headers.get('Expect', "") 88 | if (expect.lower() == "100-continue" and 89 | self.protocol_version >= "HTTP/1.1" and 90 | self.request_version >= "HTTP/1.1"): 91 | if not self.handle_expect_100(): 92 | return False 93 | return True 94 | 95 | async def handle_one_request(self): 96 | try: 97 | self.raw_requestline = await self.rfile.readline() 98 | if len(self.raw_requestline) > 65536: 99 | self.requestline = '' 100 | self.request_version = '' 101 | self.command = '' 102 | self.send_error(HTTPStatus.REQUEST_URI_TOO_LONG) 103 | return 104 | if not self.raw_requestline: 105 | self.close_connection = True 106 | return 107 | if not await self.parse_request(): 108 | # An error code has been sent, just exit 109 | return 110 | mname = 'do_' + self.command 111 | if not hasattr(self, mname): 112 | self.send_error(HTTPStatus.NOT_IMPLEMENTED, 113 | "Unsupported method (%r)" % self.command) 114 | return 115 | method = getattr(self, mname) 116 | method() 117 | except socket.timeout as e: 118 | #a read or a write timed out. Discard this connection 119 | self.log_error("Request timed out: %r", e) 120 | self.close_connection = True 121 | return 122 | 123 | async def handle(self): 124 | self.close_connection = True 125 | 126 | await self.handle_one_request() 127 | while not self.close_connection: 128 | await self.handle_one_request() 129 | -------------------------------------------------------------------------------- /heralding/libs/msrdp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnnykv/heralding/ac12724ab38c4e2fe78f07d1bc35e6e586ba69c0/heralding/libs/msrdp/__init__.py -------------------------------------------------------------------------------- /heralding/libs/msrdp/packer.py: -------------------------------------------------------------------------------- 1 | # 2 | # Some contents of this file is part of the PyRDP project. 3 | # Copyright (C) 2018 GoSecure Inc. 4 | # Licensed under the GPLv3 or later. 5 | # 6 | 7 | import struct 8 | 9 | 10 | class Integer: 11 | FORMAT = "" 12 | 13 | @classmethod 14 | def pack(cls, value): 15 | data_bytes = struct.pack(cls.FORMAT, value) 16 | 17 | return data_bytes 18 | 19 | 20 | # 8 bits 21 | class Int8(Integer): 22 | FORMAT = " 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import struct 17 | import logging 18 | 19 | logger = logging.getLogger(__name__) 20 | 21 | 22 | class InvalidExpectedData(Exception): 23 | 24 | def __init__(self, message=""): 25 | Exception.__init__(self, message) 26 | 27 | 28 | class RawBytes(): 29 | """ Read/Consume raw bytes """ 30 | 31 | def __init__(self, data, structFormat, typeSize, pos=0, optional=False): 32 | self.data = data 33 | self._pos = pos 34 | self._structFormat = structFormat 35 | self._typeSize = typeSize 36 | self._optional = optional 37 | 38 | def dataLen(self): 39 | return len(self.data[self._pos:]) 40 | 41 | def read(self): 42 | if self.dataLen() < self._typeSize: 43 | if self._optional: 44 | logger.debug("No optional data present in the PDU") 45 | return (b'', self._pos) 46 | else: 47 | raise InvalidExpectedData("Bytes Stream is too small to read") 48 | self.value = struct.unpack( 49 | self._structFormat, self.data[self._pos:self._pos + self._typeSize])[0] 50 | self._pos += self._typeSize 51 | 52 | return self.value, self._pos 53 | 54 | def readRaw(self): 55 | if self.dataLen() < self._typeSize: 56 | if self._optional: 57 | logger.debug("No optional data present in the PDU") 58 | return (b'', self._pos) 59 | else: 60 | raise InvalidExpectedData("Bytes Stream is too small to read") 61 | self.value = self.data[self._pos:self._pos + self._typeSize] 62 | self._pos += self._typeSize 63 | 64 | return self.value, self._pos 65 | 66 | def readUntil(self, until): 67 | if self.dataLen() < len(until) + 1: 68 | if self._optional: 69 | logger.debug("No optional data present in the PDU") 70 | return (b'', self._pos) 71 | else: 72 | raise InvalidExpectedData("Bytes Stream is too small to read") 73 | self.value = b'' 74 | _data = self.data[self._pos:self._pos + len(until) + 1] 75 | while _data[-len(until):] != until: 76 | i = _data[0] 77 | self.value += i.to_bytes(1, byteorder='big') 78 | self._pos += 1 79 | _data = self.data[self._pos:self._pos + len(until) + 1] 80 | 81 | # insert the last char before mactching bytes 82 | i = _data[0] 83 | self.value += i.to_bytes(1, byteorder='big') 84 | self._pos += 1 85 | 86 | self._pos += len(until) 87 | return self.value, self._pos 88 | 89 | 90 | class UInt8(RawBytes): 91 | 92 | def __init__(self, data, pos, optional=False): 93 | RawBytes.__init__(self, data, "B", 1, pos, optional) 94 | 95 | 96 | class SInt8(RawBytes): 97 | 98 | def __init__(self, data, pos, optional=False): 99 | RawBytes.__init__(self, data, "b", 1, pos, optional) 100 | 101 | 102 | class UInt16Be(RawBytes): 103 | 104 | def __init__(self, data, pos, optional=False): 105 | RawBytes.__init__(self, data, ">H", 2, pos, optional) 106 | 107 | 108 | class UInt16Le(RawBytes): 109 | 110 | def __init__(self, data, pos, optional=False): 111 | RawBytes.__init__(self, data, "I", 4, pos, optional) 118 | 119 | 120 | class UInt32Le(RawBytes): 121 | 122 | def __init__(self, data, pos, optional=False): 123 | RawBytes.__init__(self, data, " 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | # *** This file contains all the PDU required for RDP Protocol *** 17 | # each top tier PDU class will have a payload and generate method 18 | 19 | from .packer import Uint16BE, Int16BE, Uint32LE, Uint32BE 20 | from .parser import x224ConnectionRequestPDU 21 | from .security import ServerSecurity 22 | 23 | 24 | class tpktPDU: 25 | 26 | def __init__(self, payload): 27 | self.version = 3 28 | self.reserved = 0 29 | self.payload = payload 30 | 31 | def generate(self): 32 | payload_len = Int16BE.pack(1 + 1 + 2 + len(self.payload)) 33 | # print(repr(payload_len)) 34 | return bytes([self.version, self.reserved]) + payload_len + self.payload 35 | 36 | 37 | class x224DataPDU: 38 | 39 | @staticmethod 40 | def generate(): 41 | return b'\x02\xf0\x80' 42 | 43 | 44 | class x224ConnectionConfirmPDU(): 45 | 46 | def __init__(self, reqProto=None): 47 | self.type = b'\xd0' 48 | self.dest_ref = b'\x00\x00' # 0 bytes 49 | self.src_ref = b'\x12\x34' # bogus value 50 | self.options = b'\x00' 51 | self.reqProto = reqProto 52 | self.sentNegoFail = False 53 | 54 | def generate(self): 55 | PROTOCOL_SSL = 0x00000001 56 | 57 | data = self.type + self.dest_ref + self.src_ref + self.options 58 | len_indicator = len(data) # 1byte length of PDU without indicator byte 59 | 60 | if self.reqProto: 61 | selectedProto = self.reqProto & PROTOCOL_SSL # for now only tls 62 | proto_bytes = Uint32LE.pack(selectedProto) 63 | nego_res = b'\x02\x00\x08\x00' + proto_bytes 64 | 65 | # send NegoFail when a non-TLS method is requested 66 | if not (selectedProto == PROTOCOL_SSL): 67 | self.sentNegoFail = True 68 | nego_res = b'\x03\x00\x08\x00' + b'\x01' + bytes( 69 | 3) # SSL required by server 70 | len_indicator += len(nego_res) 71 | data += nego_res 72 | else: 73 | # RDP sec, if no proto requested. Currently not supported so send a Nego Fail 74 | self.sentNegoFail = True 75 | nego_res = b'\x03\x00\x08\x00' + b'\x01' + bytes(3) 76 | len_indicator += len(nego_res) 77 | data += nego_res 78 | 79 | return bytes([len_indicator]) + data 80 | 81 | def getFullPacket(self): 82 | return tpktPDU(self.generate()).generate() 83 | 84 | 85 | class ServerData: 86 | 87 | @classmethod 88 | def generate(cls, cReqproto, serverSec): 89 | # Servr Core data 90 | coreData_1 = b'\x01\x0c\x0c\x00\x04\x00\x08\x00' 91 | clientReqPro = Uint32LE.pack(cReqproto) 92 | 93 | # ServerNetwork Data 94 | netData = b'\x03\x0c\x08\x00\xeb\x03\x00\x00' # here channel count 0 95 | 96 | # ServerSec Data 97 | # here choosing encryption of 128bit by default method \x02\x00\x00\x00 98 | part_1 = b'\x02\x0c\xec\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\x00\x00\x00\xb8\x00\x00\x00' 99 | serverRandom = ServerSecurity.SERVER_RANDOM 100 | 101 | # an instance of ServerSecurity class required 102 | certData = serverSec.getServerCertBytes() 103 | # ADD all 104 | serverCoreData = coreData_1 + clientReqPro 105 | serverNetData = netData 106 | serverSecData = part_1 + serverRandom + certData 107 | 108 | return serverCoreData + serverNetData + serverSecData 109 | 110 | 111 | class MCSConnectResponsePDU(): 112 | """ Server MCS Connect Response PDU with GCC Conference Create Response """ 113 | 114 | def __init__(self, cReqproto, serverSec): 115 | self.cReqproto = cReqproto 116 | self.serverSec = serverSec 117 | 118 | def generate(self): 119 | serverData = ServerData.generate(self.cReqproto, self.serverSec) 120 | serverDataLen = Uint16BE.pack(len(serverData) | 0x8000) 121 | gccCreateRes = b'\x00\x05\x00\x14\x7c\x00\x01\x2a\x14\x76\x0a\x01\x01\x00\x01\xc0\x00\x4d\x63\x44\x6e' 122 | cc_userDataLen = Uint16BE.pack(len(serverData) + 2 + len(gccCreateRes)) 123 | cc_userData_1 = b'\x04\x82' 124 | domainParams = b'\x30\x1a\x02\x01\x22\x02\x01\x03\x02\x01\x00\x02\x01\x01\x02\x01\x00\x02\x01\x01\x02\x03\x00\xff\xf8\x02\x01\x02' 125 | cc_res = b'\x0a\x01\x00\x02\x01\x00' 126 | berLen = Uint16BE.pack( 127 | len(cc_res) + len(domainParams) + 2 + 2 + len(serverData) + 2 + 128 | len(gccCreateRes)) 129 | bertype = b'\x7f\x66\x82' 130 | 131 | return bertype + berLen + cc_res + domainParams + cc_userData_1 + cc_userDataLen + gccCreateRes + serverDataLen + serverData 132 | 133 | def getFullPacket(self): 134 | return tpktPDU(x224DataPDU.generate() + self.generate()).generate() 135 | 136 | 137 | class MCSAttachUserConfirmPDU(): 138 | 139 | def generate(self): 140 | return b'\x2e\x00\x00\x06' 141 | 142 | # This is full static but just to be sillimar to other methods 143 | def getFullPacket(self): 144 | return tpktPDU(x224DataPDU.generate() + self.generate()).generate() 145 | 146 | 147 | class MCSChannelJoinConfirmPDU(): 148 | 149 | def __init__(self, initiator, channelID): 150 | self.initiator = Uint16BE.pack(initiator) 151 | self.channelID = Uint16BE.pack(channelID) 152 | 153 | def generate(self): 154 | res = b'\x3e\x00' 155 | return res + self.initiator + self.channelID + self.channelID 156 | 157 | def getFullPacket(self): 158 | return tpktPDU(x224DataPDU.generate() + self.generate()).generate() 159 | -------------------------------------------------------------------------------- /heralding/libs/msrdp/security.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2014-2015 Sylvain Peyrefitte 3 | # 4 | # Some contents of this file is part of rdpy. 5 | # 6 | # rdpy is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | # 19 | 20 | import rsa 21 | import hashlib 22 | 23 | # Generate New RSA Keys 24 | rsa_key = rsa.newkeys(512) 25 | 26 | 27 | def getRSAKeys(): 28 | """Returns a 512-Bit RSA keys""" 29 | return rsa_key 30 | 31 | 32 | def PrivateKey(d, n): 33 | """ 34 | @param d: {long | str}private exponent 35 | @param n: {long | str}modulus 36 | """ 37 | if isinstance(d, bytes): 38 | d = rsa.transform.bytes2int(d) 39 | if isinstance(n, bytes): 40 | n = rsa.transform.bytes2int(n) 41 | return {'d': d, 'n': n} 42 | 43 | 44 | def int2bytes(i, fill_size=0): 45 | """wrapper of rsa.transform.int2bytes""" 46 | return rsa.transform.int2bytes(i, fill_size) 47 | 48 | 49 | def signRSA(message, privateKey): 50 | """ 51 | @summary: sign message with private key 52 | @param message: {str} message to sign 53 | @param privateKey : {rsa.privateKey} key use to sugn 54 | """ 55 | return rsa.transform.int2bytes( 56 | rsa.core.encrypt_int( 57 | rsa.transform.bytes2int(message), privateKey['d'], privateKey['n']), 58 | rsa.common.byte_size(privateKey['n'])) 59 | 60 | 61 | def decryptRSA(message, privateKey): 62 | """ 63 | @summary: wrapper around rsa.core.decrypt_int function 64 | @param message: {str} source message 65 | @param publicKey: {rsa.PrivateKey} 66 | """ 67 | return rsa.transform.int2bytes( 68 | rsa.core.decrypt_int( 69 | rsa.transform.bytes2int(message), privateKey['d'], privateKey['n'])) 70 | 71 | 72 | class ServerSecurity(): 73 | # http://msdn.microsoft.com/en-us/library/cc240776.aspx 74 | _TERMINAL_SERVICES_MODULUS_ = b"\x3d\x3a\x5e\xbd\x72\x43\x3e\xc9\x4d\xbb\xc1\x1e\x4a\xba\x5f\xcb\x3e\x88\x20\x87\xef\xf5\xc1\xe2\xd7\xb7\x6b\x9a\xf2\x52\x45\x95\xce\x63\x65\x6b\x58\x3a\xfe\xef\x7c\xe7\xbf\xfe\x3d\xf6\x5c\x7d\x6c\x5e\x06\x09\x1a\xf5\x61\xbb\x20\x93\x09\x5f\x05\x6d\xea\x87" 75 | _TERMINAL_SERVICES_PRIVATE_EXPONENT_ = b"\x87\xa7\x19\x32\xda\x11\x87\x55\x58\x00\x16\x16\x25\x65\x68\xf8\x24\x3e\xe6\xfa\xe9\x67\x49\x94\xcf\x92\xcc\x33\x99\xe8\x08\x60\x17\x9a\x12\x9f\x24\xdd\xb1\x24\x99\xc7\x3a\xb8\x0a\x7b\x0d\xdd\x35\x07\x79\x17\x0b\x51\x9b\xb3\xc7\x10\x01\x13\xe7\x3f\xf3\x5f" 76 | _TERMINAL_SERVICES_PUBLIC_EXPONENT_ = b"\x5b\x7b\x88\xc0" 77 | 78 | SERVER_RANDOM = b'\xf9\xdf\xc4p\x8a\x08\xcds5Q:\xa3!_\x8f\xa1\xeeQ\xd6Nj\x95Zy0\x12\xbf\xf5&\xfb!(' 79 | SERVER_PUBKEY_PROPS_1 = b'\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x06\x00\x5c\x00' 80 | SERVER_PUBKEY_PROPS_2 = b'RSA1H\x00\x00\x00\x00\x02\x00\x00?\x00\x00\x00' 81 | PADDING = b'\x00\x00\x00\x00\x00\x00\x00\x00' 82 | SERVER_PUBLIC_EXPONENT = b'\x01\x00\x01\x00' 83 | 84 | # SERVER_MODULUS = b'g3\x9c\xd5\x94\xa0\tO\xbfrVt`\xab\x7f\xf1+\xf7J\x86\xa3(ZK\x12\xc44(\xcd\xec"E6a\xf3\x1d&\t\x8fL\xb6\xc8f\x9c\xd0\xa3\xaf8\xaa\xe1\xfb\xcb\\\r\xfc\xbb\xd8\x93\x1cZ\xce\x1b\x8f\x92\x00\x00\x00\x00\x00\x00\x00\x00' 85 | 86 | def __init__(self): 87 | self._pubKey, self._privKey = getRSAKeys() 88 | self._modulusBytes = int2bytes(self._pubKey.n) 89 | # self._exponentBytes = int2bytes(self._pubKey.e) 90 | self._exponentBytes = ServerSecurity.SERVER_PUBLIC_EXPONENT 91 | self._clientRandom = None # set it 92 | 93 | def getSignatureHash(self): 94 | # this is a signature of first 6 fields in ServerCert 95 | DATA = ServerSecurity.SERVER_PUBKEY_PROPS_1+ServerSecurity.SERVER_PUBKEY_PROPS_2 + \ 96 | self._exponentBytes+self._modulusBytes+ServerSecurity.PADDING 97 | 98 | md5Digest = hashlib.md5() 99 | md5Digest.update(DATA) 100 | return md5Digest.digest() + b"\x00" + b"\xff" * 45 + b"\x01" 101 | 102 | def getServerCertBytes(self): 103 | sigHash = signRSA( 104 | self.getSignatureHash()[::-1], 105 | PrivateKey( 106 | d=ServerSecurity._TERMINAL_SERVICES_PRIVATE_EXPONENT_[::-1], 107 | n=ServerSecurity._TERMINAL_SERVICES_MODULUS_[::-1]))[::-1] 108 | sigBlobProps = b'\x08\x00\x48\x00' 109 | return ServerSecurity.SERVER_PUBKEY_PROPS_1+ServerSecurity.SERVER_PUBKEY_PROPS_2 + \ 110 | self._exponentBytes+self._modulusBytes+ServerSecurity.PADDING+sigBlobProps+sigHash+ServerSecurity.PADDING 111 | 112 | def decryptClientRandom(self, encRandom): 113 | self._decClientRandom = decryptRSA(encRandom[::-1], self._privKey)[::-1] 114 | return self._decClientRandom 115 | -------------------------------------------------------------------------------- /heralding/libs/msrdp/tls.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019 Sudipta Pandit 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import ssl 17 | import logging 18 | 19 | logger = logging.getLogger(__name__) 20 | 21 | 22 | class TLSHandshakeError(Exception): 23 | 24 | def __init__(self, message=""): 25 | Exception.__init__(self, message) 26 | 27 | 28 | class TLS: 29 | """ TLS implamentation using memory BIO """ 30 | 31 | def __init__(self, writer, reader, pem_file): 32 | """@param: writer and reader are asyncio stream writer and reader objects""" 33 | self._tlsInBuff = ssl.MemoryBIO() 34 | self._tlsOutBuff = ssl.MemoryBIO() 35 | ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1_1) 36 | ctx.set_ciphers('RSA:!aNULL') 37 | ctx.check_hostname = False 38 | ctx.load_cert_chain(pem_file) 39 | self._tlsObj = ctx.wrap_bio( 40 | self._tlsInBuff, self._tlsOutBuff, server_side=True) 41 | self.writer = writer 42 | self.reader = reader 43 | 44 | async def do_tls_handshake(self): 45 | client_hello = await self.reader.read(4096) 46 | self._tlsInBuff.write(client_hello) 47 | try: 48 | self._tlsObj.do_handshake() 49 | except ssl.SSLWantReadError: 50 | server_hello = self._tlsOutBuff.read() 51 | self.writer.write(server_hello) 52 | await self.writer.drain() 53 | except ssl.SSLError as e: 54 | if "WRONG_VERSION_NUMBER" in e.args[1]: 55 | logger.debug("Client tried to connect with wrong SSL version") 56 | else: 57 | logger.debug(e.args[1]) 58 | 59 | client_fin = await self.reader.read(4096) 60 | self._tlsInBuff.write(client_fin) 61 | try: 62 | self._tlsObj.do_handshake() 63 | except ssl.SSLWantReadError: 64 | raise TLSHandshakeError("Expected more data in Clinet FIN") 65 | 66 | server_fin = self._tlsOutBuff.read() 67 | self.writer.write(server_fin) 68 | await self.writer.drain() 69 | 70 | async def write_tls(self, data): 71 | self._tlsObj.write(data) 72 | _data = self._tlsOutBuff.read() 73 | _res = self.writer.write(_data) 74 | await self.writer.drain() 75 | return _res 76 | 77 | async def read_tls(self, size): 78 | data = b"" 79 | # Check if we have any leftover data in the buffer 80 | try: 81 | data += self._tlsObj.read(size) 82 | except ssl.SSLWantReadError: 83 | pass 84 | 85 | # iterate until we have all needed plaintext 86 | while len(data) < size: 87 | if self.reader.at_eof(): 88 | break 89 | try: 90 | # read ciphertext 91 | _rData = await self.reader.read(1) 92 | # put ciphertext into SSL machine 93 | self._tlsInBuff.write(_rData) 94 | # try to fill plaintext buffer 95 | data += self._tlsObj.read(size) 96 | except ssl.SSLWantReadError: 97 | pass 98 | 99 | return data 100 | -------------------------------------------------------------------------------- /heralding/libs/telnetsrv/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [] 2 | -------------------------------------------------------------------------------- /heralding/misc/__init__.py: -------------------------------------------------------------------------------- 1 | import zmq 2 | 3 | zmq_context = zmq.Context() 4 | -------------------------------------------------------------------------------- /heralding/misc/common.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017 Johnny Vestergaard 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import os 17 | import logging 18 | import asyncio 19 | import requests 20 | 21 | from OpenSSL import crypto 22 | from Crypto.PublicKey import RSA 23 | 24 | logger = logging.getLogger(__name__) 25 | 26 | 27 | def on_unhandled_task_exception(task): 28 | if not task.cancelled(): 29 | task_exc = task.exception() 30 | if task_exc: 31 | logger.exception('Stopping because %s died: %s', task, task_exc) 32 | os._exit(1) 33 | 34 | 35 | async def cancel_all_pending_tasks(loop=None): 36 | if loop is None: 37 | loop = asyncio.get_event_loop() 38 | pending = asyncio.all_tasks(loop=loop) 39 | pending.remove(asyncio.current_task(loop=loop)) 40 | for task in pending: 41 | # We give task only 1 second to die. 42 | if not task.done(): 43 | task.cancel() 44 | try: 45 | await asyncio.wait_for(task, timeout=5) 46 | except (asyncio.CancelledError, KeyboardInterrupt, ConnectionResetError): 47 | pass 48 | 49 | 50 | def generate_self_signed_cert(cert_country, cert_state, cert_organization, 51 | cert_locality, cert_organizational_unit, 52 | cert_common_name, valid_days, serial_number): 53 | rsa_key = RSA.generate(2048) 54 | 55 | pk = crypto.load_privatekey(crypto.FILETYPE_PEM, 56 | rsa_key.exportKey('PEM', pkcs=1)) 57 | cert = crypto.X509() 58 | sub = cert.get_subject() 59 | sub.CN = cert_common_name 60 | sub.C = cert_country 61 | sub.ST = cert_state 62 | sub.L = cert_locality 63 | sub.O = cert_organization 64 | 65 | # optional 66 | if cert_organizational_unit: 67 | sub.OU = cert_organizational_unit 68 | 69 | cert.set_serial_number(serial_number) 70 | cert.gmtime_adj_notBefore(0) 71 | cert.gmtime_adj_notAfter(valid_days * 24 * 60 * 60) # Valid for a year 72 | cert.set_issuer(sub) 73 | cert.set_pubkey(pk) 74 | cert.sign(pk, 'sha1') 75 | 76 | cert_text = crypto.dump_certificate(crypto.FILETYPE_PEM, cert) 77 | priv_key_text = rsa_key.exportKey('PEM', pkcs=1) 78 | 79 | return cert_text, priv_key_text 80 | 81 | 82 | def get_public_ip(): 83 | r = requests.get('https://api.ipify.org') 84 | r.raise_for_status() 85 | return r.text 86 | -------------------------------------------------------------------------------- /heralding/misc/session.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017 Johnny Vestergaard 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import json 17 | import uuid 18 | import logging 19 | from datetime import datetime 20 | 21 | import heralding.honeypot 22 | from heralding.reporting.reporting_relay import ReportingRelay 23 | 24 | logger = logging.getLogger(__name__) 25 | 26 | 27 | class Session: 28 | 29 | def __init__(self, 30 | source_ip, 31 | source_port, 32 | protocol, 33 | users, 34 | destination_port=None, 35 | destination_ip=''): 36 | 37 | self.id = uuid.uuid4() 38 | self.source_ip = source_ip 39 | self.source_port = source_port 40 | self.protocol = protocol 41 | if heralding.honeypot.Honeypot.public_ip: 42 | self.destination_ip = heralding.honeypot.Honeypot.public_ip 43 | else: 44 | self.destination_ip = destination_ip 45 | self.destination_port = destination_port 46 | self.timestamp = datetime.utcnow() 47 | self.num_ = 0 48 | self.session_ended = False 49 | # protocol specific data 50 | self.auxiliary_data = {} 51 | 52 | self.connected = True 53 | 54 | # for session specific volatile data (will not get logged) 55 | self.vdata = {} 56 | 57 | self.auth_attempts = [] 58 | 59 | self.last_activity = datetime.utcnow() 60 | self.log_start_session() 61 | 62 | def log_start_session(self): 63 | entry = self.get_session_info(False) 64 | ReportingRelay.logSessionInfo(entry) 65 | 66 | def activity(self): 67 | self.last_activity = datetime.utcnow() 68 | 69 | def is_connected(self): 70 | return self.connected 71 | 72 | def get_auxiliary_data(self): 73 | return {} 74 | 75 | def get_number_of_login_attempts(self): 76 | return len(self.auth_attempts) 77 | 78 | def add_auth_attempt(self, _type, **kwargs): 79 | 80 | # constructs dict to transmitted right away. 81 | entry = { 82 | 'timestamp': datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S.%f'), 83 | 'session_id': str(self.id), 84 | 'auth_id': str(uuid.uuid4()), 85 | 'source_ip': self.source_ip, 86 | 'source_port': self.source_port, 87 | 'destination_ip': self.destination_ip, 88 | 'destination_port': self.destination_port, 89 | 'protocol': self.protocol, 90 | 'username': None, 91 | 'password': None, 92 | 'password_hash': None 93 | } 94 | if 'username' in kwargs: 95 | entry['username'] = kwargs['username'] 96 | if 'password' in kwargs: 97 | entry['password'] = kwargs['password'] 98 | if 'password_hash' in kwargs: 99 | entry['password_hash'] = kwargs['password_hash'] 100 | ReportingRelay.logAuthAttempt(entry) 101 | 102 | # add to internal dict used for reporting when the session ends 103 | self.auth_attempts.append({ 104 | 'timestamp': entry['timestamp'], 105 | 'username': entry['username'], 106 | 'password': entry['password'], 107 | }) 108 | 109 | self.activity() 110 | logger.debug( 111 | '%s authentication attempt from %s:%s. Auth mechanism: %s, session id %s ' 112 | 'Credentials: %s', self.protocol, self.source_ip, self.source_port, 113 | _type, self.id, json.dumps(kwargs)) 114 | 115 | def get_session_info(self, session_ended): 116 | entry = { 117 | 'timestamp': self.timestamp.strftime('%Y-%m-%d %H:%M:%S.%f'), 118 | 'duration': int((datetime.utcnow() - self.timestamp).total_seconds()), 119 | 'session_id': str(self.id), 120 | 'source_ip': self.source_ip, 121 | 'source_port': self.source_port, 122 | 'destination_ip': self.destination_ip, 123 | 'destination_port': self.destination_port, 124 | 'protocol': self.protocol, 125 | 'num_auth_attempts': len(self.auth_attempts), 126 | 'auth_attempts': self.auth_attempts, 127 | 'session_ended': session_ended, 128 | 'auxiliary_data': self.auxiliary_data 129 | } 130 | return entry 131 | 132 | def set_auxiliary_data(self, data): 133 | self.auxiliary_data = data 134 | 135 | def end_session(self): 136 | if not self.session_ended: 137 | self.session_ended = True 138 | self.connected = False 139 | entry = self.get_session_info(True) 140 | 141 | ReportingRelay.logSessionInfo(entry) 142 | logger.debug('Session with session id %s ended', self.id) 143 | -------------------------------------------------------------------------------- /heralding/misc/socket_names.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017 Johnny Vestergaard 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | from enum import Enum 17 | 18 | 19 | class SocketNames(Enum): 20 | # when relay receives messages it will publish them on INTERNAL_REPORTING 21 | INTERNAL_REPORTING = 'inproc://internalReporting' 22 | -------------------------------------------------------------------------------- /heralding/reporting/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnnykv/heralding/ac12724ab38c4e2fe78f07d1bc35e6e586ba69c0/heralding/reporting/__init__.py -------------------------------------------------------------------------------- /heralding/reporting/base_logger.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017 Johnny Vestergaard 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import zmq 17 | import logging 18 | 19 | import heralding.misc 20 | from heralding.misc.socket_names import SocketNames 21 | 22 | logger = logging.getLogger(__name__) 23 | 24 | 25 | class BaseLogger: 26 | 27 | def __init__(self): 28 | self.enabled = True 29 | 30 | def start(self): 31 | context = heralding.misc.zmq_context 32 | 33 | internal_reporting_socket = context.socket(zmq.SUB) 34 | internal_reporting_socket.connect(SocketNames.INTERNAL_REPORTING.value) 35 | internal_reporting_socket.setsockopt(zmq.SUBSCRIBE, b'') 36 | 37 | poller = zmq.Poller() 38 | poller.register(internal_reporting_socket, zmq.POLLIN) 39 | while self.enabled: 40 | socks = dict(poller.poll(500)) 41 | self._execute_regulary() 42 | if internal_reporting_socket in socks and socks[ 43 | internal_reporting_socket] == zmq.POLLIN: 44 | data = internal_reporting_socket.recv_pyobj() 45 | # if None is received, this means that ReportingRelay is going down 46 | if not data: 47 | self.stop() 48 | elif data['message_type'] == 'auth': 49 | self.handle_auth_log(data['content']) 50 | elif data['message_type'] == 'session_info': 51 | self.handle_session_log(data['content']) 52 | elif data['message_type'] == 'listen_ports': 53 | self.handle_listen_ports(data['content']) 54 | elif data['message_type'] == 'aux_info': 55 | self.handle_auxiliary_log(data['content']) 56 | internal_reporting_socket.close() 57 | # at this point we know no more data will arrive. 58 | self.loggerStopped() 59 | 60 | def stop(self): 61 | self.enabled = False 62 | 63 | def handle_auth_log(self, data): 64 | # should be handled in child class 65 | pass 66 | 67 | def handle_session_log(self, data): 68 | # implement if needed 69 | pass 70 | 71 | def handle_listen_ports(self, data): 72 | # implement if needed 73 | pass 74 | 75 | def handle_auxiliary_log(self, data): 76 | # implement if needed 77 | pass 78 | 79 | def _execute_regulary(self): 80 | # if implemented this method will get called regulary 81 | pass 82 | 83 | # called after we are sure no more data is received 84 | # override this to close filesockets, etc. 85 | def loggerStopped(self): 86 | pass 87 | -------------------------------------------------------------------------------- /heralding/reporting/curiosum_integration.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2018 Johnny Vestergaard 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import heralding.misc 17 | 18 | import logging 19 | import zmq 20 | import json 21 | from datetime import datetime 22 | 23 | from heralding.reporting.base_logger import BaseLogger 24 | 25 | logger = logging.getLogger(__name__) 26 | 27 | 28 | class CuriosumIntegration(BaseLogger): 29 | 30 | def __init__(self, port): 31 | super().__init__() 32 | 33 | zmq_socket = 'tcp://127.0.0.1:{0}'.format(port) 34 | 35 | context = heralding.misc.zmq_context 36 | self.socket = context.socket(zmq.PUSH) 37 | self.socket.bind(zmq_socket) 38 | self.listen_ports = [] 39 | self.last_listen_ports_transmit = datetime.now() 40 | 41 | logger.info('Curiosum logger started using files: %s', zmq_socket) 42 | 43 | def loggerStopped(self): 44 | self.socket.close() 45 | 46 | def _no_block_send(self, topic, data): 47 | try: 48 | self.socket.send_string('{0} {1}'.format(topic, json.dumps(data)), 49 | zmq.NOBLOCK) 50 | except zmq.ZMQError as e: 51 | logger.warning('Error while sending: %s', e) 52 | 53 | def handle_session_log(self, data): 54 | message = { 55 | 'SessionID': str(data['session_id']), 56 | 'DstPort': data['destination_port'], 57 | 'SrcIP': data['source_ip'], 58 | 'SrcPort': data['source_port'], 59 | 'SessionEnded': data['session_ended'] 60 | } 61 | self._no_block_send('session_ended', message) 62 | 63 | def _execute_regulary(self): 64 | if (datetime.now() - self.last_listen_ports_transmit).total_seconds() > 5: 65 | self._no_block_send('listen_ports', self.listen_ports) 66 | self.last_listen_ports_transmit = datetime.now() 67 | 68 | def handle_listen_ports(self, data): 69 | self.listen_ports = data 70 | -------------------------------------------------------------------------------- /heralding/reporting/file_logger.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017 Johnny Vestergaard 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import os 17 | import csv 18 | import logging 19 | import json 20 | 21 | from heralding.reporting.base_logger import BaseLogger 22 | 23 | logger = logging.getLogger(__name__) 24 | 25 | 26 | class FileLogger(BaseLogger): 27 | 28 | def __init__(self, session_csv_logfile, sessions_json_logfile, auth_logfile): 29 | super().__init__() 30 | 31 | self.auth_log_filehandler = None 32 | self.auth_log_writer = None 33 | self.session_csv_log_filehandler = None 34 | self.session_csv_log_writer = None 35 | self.session_json_log_filehandler = None 36 | 37 | if auth_logfile != "": 38 | # Setup CSV logging for auth attempts 39 | auth_field_names = [ 40 | 'timestamp', 'auth_id', 'session_id', 'source_ip', 'source_port', 41 | 'destination_ip', 'destination_port', 'protocol', 'username', 42 | 'password', 'password_hash' 43 | ] 44 | 45 | self.auth_log_filehandler, self.auth_log_writer = self.setup_csv_files( 46 | auth_logfile, auth_field_names) 47 | 48 | logger.info( 49 | 'File logger: Using %s to log authentication attempts in CSV format.', 50 | auth_logfile) 51 | 52 | if session_csv_logfile != "": 53 | # Setup CSV logging for sessions 54 | session_field_names = [ 55 | 'timestamp', 'duration', 'session_id', 'source_ip', 'source_port', 56 | 'destination_ip', 'destination_port', 'protocol', 'num_auth_attempts' 57 | ] 58 | self.session_csv_log_filehandler, self.session_csv_log_writer = self.setup_csv_files( 59 | session_csv_logfile, session_field_names) 60 | 61 | logger.info( 62 | 'File logger: Using %s to log unified session data in CSV format.', 63 | session_csv_logfile) 64 | 65 | if sessions_json_logfile != "": 66 | # Setup json logging for logging complete sessions 67 | if not os.path.isfile(sessions_json_logfile): 68 | self.session_json_log_filehandler = open( 69 | sessions_json_logfile, 'w', encoding='utf-8') 70 | else: 71 | self.session_json_log_filehandler = open( 72 | sessions_json_logfile, 'a', encoding='utf-8') 73 | 74 | logger.info( 75 | 'File logger: Using %s to log complete session data in JSON format.', 76 | sessions_json_logfile) 77 | 78 | def setup_csv_files(self, filename, field_names): 79 | handler = writer = None 80 | 81 | if not os.path.isfile(filename): 82 | handler = open(filename, 'w', encoding='utf-8') 83 | else: 84 | handler = open(filename, 'a', encoding='utf-8') 85 | 86 | writer = csv.DictWriter( 87 | handler, fieldnames=field_names, extrasaction='ignore') 88 | 89 | # empty file, write csv header 90 | if os.path.getsize(filename) == 0: 91 | writer.writeheader() 92 | handler.flush() 93 | 94 | return handler, writer 95 | 96 | def loggerStopped(self): 97 | for handler in [ 98 | self.auth_log_filehandler, self.session_csv_log_filehandler, 99 | self.session_json_log_filehandler 100 | ]: 101 | if handler != None: 102 | handler.flush() 103 | handler.close() 104 | 105 | def handle_auth_log(self, data): 106 | # for now this logger only handles authentication attempts where we are able 107 | # to log both username and password 108 | if self.auth_log_filehandler != None: 109 | if 'username' in data and 'password' in data: 110 | self.auth_log_writer.writerow(data) 111 | # meh 112 | self.auth_log_filehandler.flush() 113 | 114 | def handle_session_log(self, data): 115 | if data['session_ended']: 116 | if self.session_csv_log_filehandler != None: 117 | self.session_csv_log_writer.writerow(data) 118 | self.session_csv_log_filehandler.flush() 119 | if self.session_json_log_filehandler != None: 120 | self.session_json_log_filehandler.write(json.dumps(data) + "\n") 121 | self.session_json_log_filehandler.flush() 122 | -------------------------------------------------------------------------------- /heralding/reporting/hpfeeds_logger.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017 Johnny Vestergaard 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import logging 17 | import hpfeeds 18 | import json 19 | 20 | from heralding.reporting.base_logger import BaseLogger 21 | 22 | logger = logging.getLogger(__name__) 23 | 24 | 25 | class HpFeedsLogger(BaseLogger): 26 | 27 | def __init__(self, session_channel, auth_channel, host, port, ident, secret): 28 | super().__init__() 29 | self.session_channel = session_channel 30 | self.auth_channel = auth_channel 31 | self.host = host 32 | self.port = port 33 | self.ident = ident 34 | self.secret = secret 35 | self._initial_connection_happend = False 36 | logger.info('HpFeeds logger started.') 37 | 38 | def start(self): 39 | if not self._initial_connection_happend: 40 | self.hp_connection = hpfeeds.new(self.host, self.port, self.ident, 41 | self.secret, True) 42 | self._initial_connection_happend = True 43 | logger.info('HpFeeds logger connected to %s:%s.', self.host, self.port) 44 | # after we established that we can connect enter the subscribe and enter the polling loop 45 | super().start() 46 | 47 | def loggerStopped(self): 48 | self.stop() 49 | self.close() 50 | 51 | def handle_auth_log(self, data): 52 | if self._initial_connection_happend: 53 | self.hp_connection.publish(self.auth_channel, json.dumps(data).encode()) 54 | 55 | def handle_session_log(self, data): 56 | if self._initial_connection_happend: 57 | self.hp_connection.publish(self.session_channel, 58 | json.dumps(data).encode()) 59 | -------------------------------------------------------------------------------- /heralding/reporting/reporting_relay.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017 Johnny Vestergaard 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import zmq 17 | import queue 18 | import logging 19 | 20 | import heralding.misc 21 | from heralding.misc.socket_names import SocketNames 22 | 23 | logger = logging.getLogger(__name__) 24 | 25 | 26 | class ReportingRelay: 27 | _logQueue = None 28 | 29 | def __init__(self): 30 | # we are singleton 31 | assert ReportingRelay._logQueue is None 32 | ReportingRelay._logQueue = queue.Queue(maxsize=10000) 33 | 34 | self.enabled = True 35 | 36 | context = heralding.misc.zmq_context 37 | self.internalReportingPublisher = context.socket(zmq.PUB) 38 | 39 | @staticmethod 40 | def logAuthAttempt(data): 41 | ReportingRelay._logQueue.put({'message_type': 'auth', 'content': data}) 42 | 43 | @staticmethod 44 | def logSessionInfo(data): 45 | if ReportingRelay._logQueue is not None: 46 | ReportingRelay._logQueue.put({ 47 | 'message_type': 'session_info', 48 | 'content': data 49 | }) 50 | 51 | @staticmethod 52 | def logListenPorts(data): 53 | if ReportingRelay._logQueue is not None: 54 | ReportingRelay._logQueue.put({ 55 | 'message_type': 'listen_ports', 56 | 'content': data 57 | }) 58 | 59 | @staticmethod 60 | def logAuxiliaryData(data): 61 | if ReportingRelay._logQueue is not None: 62 | ReportingRelay._logQueue.put({ 63 | 'message_type': 'aux_info', 64 | 'content': data 65 | }) 66 | 67 | def start(self): 68 | self.internalReportingPublisher.bind(SocketNames.INTERNAL_REPORTING.value) 69 | 70 | while self.enabled or ReportingRelay._logQueue.qsize() > 0: 71 | try: 72 | data = ReportingRelay._logQueue.get(timeout=0.5) 73 | self.internalReportingPublisher.send_pyobj(data) 74 | except queue.Empty: 75 | pass 76 | 77 | # None signals 'going down' to listeners 78 | self.internalReportingPublisher.send_pyobj(None) 79 | self.internalReportingPublisher.close() 80 | 81 | # None is also used to signal we are all done 82 | ReportingRelay._logQueue = None 83 | 84 | def stop(self): 85 | self.enabled = False 86 | -------------------------------------------------------------------------------- /heralding/reporting/syslog_logger.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017 Johnny Vestergaard 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import syslog 17 | import logging 18 | 19 | from heralding.reporting.base_logger import BaseLogger 20 | 21 | logger = logging.getLogger(__name__) 22 | 23 | 24 | class SyslogLogger(BaseLogger): 25 | 26 | def __init__(self): 27 | super().__init__() 28 | logger.debug('Syslog logger started') 29 | 30 | def handle_auth_log(self, data): 31 | # for now this logger only handles authentication attempts where we are able 32 | # to log both username and password 33 | if 'username' in data and 'password' in data: 34 | message = "Authentication from {0}:{1}, with username: {2} " \ 35 | "and password: {3}.".format(data['source_ip'], data['source_port'], 36 | data['username'], data['password']) 37 | syslog.syslog(syslog.LOG_ALERT, message) 38 | -------------------------------------------------------------------------------- /heralding/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnnykv/heralding/ac12724ab38c4e2fe78f07d1bc35e6e586ba69c0/heralding/tests/__init__.py -------------------------------------------------------------------------------- /heralding/tests/test_config.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017 Johnny Vestergaard 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import unittest 17 | import yaml 18 | 19 | 20 | class ConfigFileTests(unittest.TestCase): 21 | 22 | def test_config_file(self): 23 | """Tests that the yaml config file is valid YAML""" 24 | with open("heralding/heralding.yml", "r") as f: 25 | file_content = f.read() 26 | # excepts if not valid 27 | yaml.safe_load(file_content) 28 | -------------------------------------------------------------------------------- /heralding/tests/test_encoding.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) 2017 Johnny Vestergaard 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import os 18 | import unittest 19 | 20 | from heralding.reporting.file_logger import FileLogger 21 | 22 | 23 | class EncodingTests(unittest.TestCase): 24 | 25 | @classmethod 26 | def setUpClass(cls): 27 | cls.auth_log_filename = "test_auth_log.log" 28 | cls.session_log_filename = "test_session_log.log" 29 | cls.session_json_filename = "test_session_log.json" 30 | cls.flogger = FileLogger(cls.session_log_filename, 31 | cls.session_json_filename, cls.auth_log_filename) 32 | 33 | def test_ascii(self): 34 | test_login = test_password = "girls_like_python".encode('ascii') 35 | test_data = {'username': test_login, 'password': test_password} 36 | self.flogger.handle_auth_log(test_data) 37 | 38 | def test_unicode(self): 39 | # word 'python' in russian spelling 40 | test_login = test_password = "пайтон" 41 | test_data = {'username': test_login, 'password': test_password} 42 | self.flogger.handle_auth_log(test_data) 43 | 44 | def test_invalid_utf(self): 45 | test_login = test_password = "пайт\x80он" 46 | test_data = {'username': test_login, 'password': test_password} 47 | self.flogger.handle_auth_log(test_data) 48 | 49 | @classmethod 50 | def tearDownClass(cls): 51 | cls.flogger.auth_log_filehandler.close() 52 | os.remove(cls.auth_log_filename) 53 | os.remove(cls.session_log_filename) 54 | os.remove(cls.session_json_filename) 55 | -------------------------------------------------------------------------------- /heralding/tests/test_ftp.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017 Johnny Vestergaard 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import asyncio 17 | import unittest 18 | 19 | import ftplib 20 | from ftplib import FTP 21 | 22 | from heralding.capabilities import ftp 23 | from heralding.reporting.reporting_relay import ReportingRelay 24 | 25 | 26 | class FtpTests(unittest.TestCase): 27 | 28 | def setUp(self): 29 | self.loop = asyncio.new_event_loop() 30 | asyncio.set_event_loop(None) 31 | 32 | self.reporting_relay = ReportingRelay() 33 | self.reporting_relay_task = self.loop.run_in_executor( 34 | None, self.reporting_relay.start) 35 | 36 | def tearDown(self): 37 | self.reporting_relay.stop() 38 | # We give reporting_relay a chance to be finished 39 | self.loop.run_until_complete(self.reporting_relay_task) 40 | 41 | self.server.close() 42 | self.loop.run_until_complete(self.server.wait_closed()) 43 | 44 | self.loop.close() 45 | 46 | def test_login(self): 47 | """Testing different login combinations""" 48 | 49 | def ftp_login(): 50 | ftp_client = FTP() 51 | ftp_client.connect('127.0.0.1', 8888, 1) 52 | # expect perm exception 53 | try: 54 | ftp_client.login('james', 'bond') 55 | _ = ftp_client.getresp() # NOQA 56 | except ftplib.error_perm: 57 | ftp_client.quit() 58 | 59 | options = { 60 | 'enabled': 'True', 61 | 'port': 0, 62 | 'banner': 'Test Banner', 63 | 'users': { 64 | 'test': 'test' 65 | }, 66 | 'protocol_specific_data': { 67 | 'max_attempts': 3, 68 | 'banner': 'test banner', 69 | 'syst_type': 'Test Type' 70 | } 71 | } 72 | 73 | ftp_capability = ftp.ftp(options, self.loop) 74 | 75 | server_coro = asyncio.start_server( 76 | ftp_capability.handle_session, '0.0.0.0', 8888, loop=self.loop) 77 | self.server = self.loop.run_until_complete(server_coro) 78 | 79 | ftp_task = self.loop.run_in_executor(None, ftp_login) 80 | self.loop.run_until_complete(ftp_task) 81 | -------------------------------------------------------------------------------- /heralding/tests/test_hpfeeds.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017 Johnny Vestergaard 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import unittest 17 | 18 | from heralding.reporting.hpfeeds_logger import HpFeedsLogger 19 | 20 | 21 | class FtpTests(unittest.TestCase): 22 | 23 | def test_hpfeeds(self): 24 | """Basic test for hpfeeds reporter""" 25 | 26 | session_channel = "heralding.session" 27 | auth_channel = "heraldign.auth" 28 | host = "127.0.0.1" 29 | port = 12345 30 | ident = "atzqøl" 31 | secret = "toosecret" 32 | 33 | HpFeedsLogger(session_channel, auth_channel, host, port, ident, secret) 34 | -------------------------------------------------------------------------------- /heralding/tests/test_http.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012 Aniket Panse . 15 | 16 | # Aniket Panse grants Johnny Vestergaard 17 | # a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable 18 | # copyright license to reproduce, prepare derivative works of, publicly 19 | # display, publicly perform, sublicense, relicense, and distribute [the] Contributions 20 | # and such derivative works. 21 | 22 | import asyncio 23 | import unittest 24 | 25 | from heralding.capabilities import http 26 | from heralding.reporting.reporting_relay import ReportingRelay 27 | 28 | import http.client as httpclient 29 | 30 | 31 | class HttpTests(unittest.TestCase): 32 | 33 | def setUp(self): 34 | self.loop = asyncio.new_event_loop() 35 | asyncio.set_event_loop(None) 36 | 37 | self.reporting_relay = ReportingRelay() 38 | self.reporting_relay_task = self.loop.run_in_executor( 39 | None, self.reporting_relay.start) 40 | 41 | def tearDown(self): 42 | self.reporting_relay.stop() 43 | # We give reporting_relay a chance to be finished 44 | self.loop.run_until_complete(self.reporting_relay_task) 45 | 46 | self.server.close() 47 | self.loop.run_until_complete(self.server.wait_closed()) 48 | 49 | self.loop.close() 50 | 51 | def test_connection(self): 52 | """ Tests if the capability is up, and sending 53 | HTTP 401 (Unauthorized) headers. 54 | """ 55 | 56 | def http_request(): 57 | client = httpclient.HTTPConnection('127.0.0.1', 8888) 58 | client.request('GET', '/') 59 | response = client.getresponse() 60 | self.assertEqual(response.status, 401) 61 | 62 | options = {'enabled': 'True', 'port': 8888, 'users': {'test': 'test'}} 63 | http_cap = http.Http(options, self.loop) 64 | 65 | server_coro = asyncio.start_server( 66 | http_cap.handle_session, '0.0.0.0', 8888, loop=self.loop) 67 | self.server = self.loop.run_until_complete(server_coro) 68 | 69 | http_task = self.loop.run_in_executor(None, http_request) 70 | self.loop.run_until_complete(http_task) 71 | -------------------------------------------------------------------------------- /heralding/tests/test_imap.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) 2017 Roman Samoilenko 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import sys 18 | import imaplib 19 | import asyncio 20 | import unittest 21 | 22 | from heralding.capabilities.imap import Imap 23 | from heralding.reporting.reporting_relay import ReportingRelay 24 | 25 | 26 | class ImapTests(unittest.TestCase): 27 | 28 | def setUp(self): 29 | self.loop = asyncio.new_event_loop() 30 | asyncio.set_event_loop(None) 31 | 32 | self.reporting_relay = ReportingRelay() 33 | self.reporting_relay_task = self.loop.run_in_executor( 34 | None, self.reporting_relay.start) 35 | 36 | def tearDown(self): 37 | self.reporting_relay.stop() 38 | # We give reporting_relay a chance to be finished 39 | self.loop.run_until_complete(self.reporting_relay_task) 40 | 41 | self.server.close() 42 | self.loop.run_until_complete(self.server.wait_closed()) 43 | 44 | self.loop.close() 45 | 46 | def test_LOGIN(self): 47 | """Testing different login combinations using simple login auth mechanism.""" 48 | 49 | def imap_login(): 50 | login_sequences = [('kajoj_admin', 'thebestpassword'), 51 | ('\"kajoj_admin\"', 'the best password')] 52 | 53 | imap_obj = imaplib.IMAP4('127.0.0.1', port=8888) 54 | for sequence in login_sequences: 55 | with self.assertRaises(imaplib.IMAP4.error) as error: 56 | imap_obj.login(sequence[0], sequence[1]) 57 | imap_exception = error.exception 58 | self.assertEqual(imap_exception.args[0], b'Authentication failed') 59 | imap_obj.logout() 60 | 61 | options = { 62 | 'enabled': 'True', 63 | 'port': 143, 64 | 'timeout': 30, 65 | 'protocol_specific_data': { 66 | 'max_attempts': 3, 67 | 'banner': '* OK IMAP4rev1 Server Ready' 68 | } 69 | } 70 | capability = Imap(options, self.loop) 71 | server_coro = asyncio.start_server( 72 | capability.handle_session, '0.0.0.0', 8888, loop=self.loop) 73 | self.server = self.loop.run_until_complete(server_coro) 74 | 75 | imap_task = self.loop.run_in_executor(None, imap_login) 76 | self.loop.run_until_complete(imap_task) 77 | 78 | def test_AUTHENTICATE_PLAIN(self): 79 | """Testing different login combinations using plain auth mechanism.""" 80 | 81 | def imap_authenticate(): 82 | # imaplib in Python 3.5.3 and higher returns str representation of auth failure 83 | # But imaplib in Python 3.5.2 and lower returns bytes. 84 | # This is a sad hack to get around this problem. 85 | pyversion = sys.version_info[:3] 86 | if pyversion < (3, 5, 3): 87 | auth_failure_msg = b'Authentication failed' 88 | else: 89 | auth_failure_msg = 'Authentication failed' 90 | login_sequences = [ 91 | ('\0kajoj_admin\0thebestpassword', auth_failure_msg), 92 | ('\0пайтон\0наилучшийпароль', auth_failure_msg), 93 | ('kajoj_admin\0the best password', 94 | 'AUTHENTICATE command error: BAD [b\'invalid command\']') 95 | ] 96 | 97 | imap_obj = imaplib.IMAP4('127.0.0.1', port=8888) 98 | for sequence in login_sequences: 99 | with self.assertRaises(imaplib.IMAP4.error) as error: 100 | imap_obj.authenticate('PLAIN', lambda x: sequence[0]) 101 | imap_exception = error.exception 102 | self.assertEqual(imap_exception.args[0], sequence[1]) 103 | imap_obj.logout() 104 | 105 | options = { 106 | 'enabled': 'True', 107 | 'port': 143, 108 | 'timeout': 30, 109 | 'protocol_specific_data': { 110 | 'max_attempts': 3, 111 | 'banner': '* OK IMAP4rev1 Server Ready' 112 | } 113 | } 114 | capability = Imap(options, self.loop) 115 | 116 | server_coro = asyncio.start_server( 117 | capability.handle_session, '0.0.0.0', 8888, loop=self.loop) 118 | self.server = self.loop.run_until_complete(server_coro) 119 | 120 | imap_task = self.loop.run_in_executor(None, imap_authenticate) 121 | self.loop.run_until_complete(imap_task) 122 | 123 | 124 | if __name__ == '__main__': 125 | unittest.main() 126 | -------------------------------------------------------------------------------- /heralding/tests/test_mysql.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import unittest 3 | 4 | import pymysql 5 | from heralding.capabilities import mysql 6 | from heralding.misc.common import cancel_all_pending_tasks 7 | from heralding.reporting.reporting_relay import ReportingRelay 8 | 9 | 10 | class MySQLTests(unittest.TestCase): 11 | 12 | def setUp(self): 13 | self.loop = asyncio.new_event_loop() 14 | asyncio.set_event_loop(None) 15 | 16 | self.reporting_relay = ReportingRelay() 17 | self.reporting_relay_task = self.loop.run_in_executor( 18 | None, self.reporting_relay.start) 19 | 20 | def tearDown(self): 21 | self.reporting_relay.stop() 22 | # We give reporting_relay a chance to be finished 23 | self.loop.run_until_complete(self.reporting_relay_task) 24 | 25 | self.server.close() 26 | self.loop.run_until_complete(self.server.wait_closed()) 27 | 28 | self.loop.run_until_complete(cancel_all_pending_tasks(self.loop)) 29 | self.loop.close() 30 | 31 | def test_invalid_login(self): 32 | """Tests if mysql server responds correctly to a invalid login attempt.""" 33 | 34 | def mysql_login(): 35 | try: 36 | pymysql.connect( 37 | host="0.0.0.0", 38 | port=8306, 39 | user="tuser", 40 | password="tpass", 41 | db="testdb") 42 | except pymysql.err.OperationalError as e: 43 | return e 44 | return None 45 | 46 | options = {'enabled': 'True', 'port': 8306} 47 | mysql_cap = mysql.MySQL(options, self.loop) 48 | 49 | server_coro = asyncio.start_server( 50 | mysql_cap.handle_session, '0.0.0.0', 8306, loop=self.loop) 51 | self.server = self.loop.run_until_complete(server_coro) 52 | 53 | mysql_task = self.loop.run_in_executor(None, mysql_login) 54 | login_exception = self.loop.run_until_complete(mysql_task) 55 | 56 | self.assertIsInstance(login_exception, pymysql.err.OperationalError) 57 | self.assertEqual( 58 | str(login_exception), 59 | '(1045, "Access denied for user \'tuser\'@\'127.0.0.1\' (using password: YES)")' 60 | ) 61 | 62 | def cb(self, socket, command, option): 63 | return 64 | -------------------------------------------------------------------------------- /heralding/tests/test_pop3.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017 Johnny Vestergaard 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import asyncio 17 | import unittest 18 | 19 | from heralding.capabilities.pop3 import Pop3 20 | from heralding.misc.common import cancel_all_pending_tasks 21 | from heralding.reporting.reporting_relay import ReportingRelay 22 | 23 | 24 | class Pop3Tests(unittest.TestCase): 25 | 26 | def setUp(self): 27 | self.loop = asyncio.new_event_loop() 28 | asyncio.set_event_loop(None) 29 | 30 | self.reporting_relay = ReportingRelay() 31 | self.reporting_relay_task = self.loop.run_in_executor( 32 | None, self.reporting_relay.start) 33 | 34 | def tearDown(self): 35 | self.reporting_relay.stop() 36 | # We give reporting_relay a chance to be finished 37 | self.loop.run_until_complete(self.reporting_relay_task) 38 | 39 | self.server.close() 40 | self.loop.run_until_complete(self.server.wait_closed()) 41 | 42 | self.loop.run_until_complete(cancel_all_pending_tasks(self.loop)) 43 | 44 | self.loop.close() 45 | 46 | def test_login(self): 47 | """Testing different login combinations""" 48 | 49 | async def pop3_login(): 50 | login_sequences = [ 51 | # invalid login, invalid password 52 | (('USER wakkwakk', b'+OK User accepted'), 53 | ('PASS wakkwakk', b'-ERR Authentication failed.')), 54 | # PASS without user 55 | ( 56 | ('PASS bond', b'-ERR No username given.'),), 57 | # Try to run a TRANSACITON state command in AUTHORIZATION state 58 | ( 59 | ('RETR', b'-ERR Unknown command'),), 60 | ] 61 | for sequence in login_sequences: 62 | reader, writer = await asyncio.open_connection( 63 | '127.0.0.1', 8888, loop=self.loop) 64 | # skip banner 65 | await reader.readline() 66 | 67 | for pair in sequence: 68 | writer.write(bytes(pair[0] + "\r\n", 'utf-8')) 69 | response = await reader.readline() 70 | self.assertEqual(response.rstrip(), pair[1]) 71 | 72 | options = { 73 | 'port': 110, 74 | 'protocol_specific_data': { 75 | 'banner': '+OK POP3 server ready', 76 | 'max_attempts': 3 77 | }, 78 | 'users': { 79 | 'james': 'bond' 80 | } 81 | } 82 | sut = Pop3(options, self.loop) 83 | 84 | server_coro = asyncio.start_server( 85 | sut.handle_session, '0.0.0.0', 8888, loop=self.loop) 86 | self.server = self.loop.run_until_complete(server_coro) 87 | 88 | self.loop.run_until_complete(pop3_login()) 89 | -------------------------------------------------------------------------------- /heralding/tests/test_postgresql.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import unittest 3 | 4 | import psycopg2 5 | from heralding.capabilities import postgresql 6 | from heralding.misc.common import cancel_all_pending_tasks 7 | from heralding.reporting.reporting_relay import ReportingRelay 8 | 9 | 10 | class PostgreSQLTests(unittest.TestCase): 11 | 12 | def setUp(self): 13 | self.loop = asyncio.new_event_loop() 14 | asyncio.set_event_loop(None) 15 | 16 | self.reporting_relay = ReportingRelay() 17 | self.reporting_relay_task = self.loop.run_in_executor( 18 | None, self.reporting_relay.start) 19 | 20 | def tearDown(self): 21 | self.reporting_relay.stop() 22 | # We give reporting_relay a chance to be finished 23 | self.loop.run_until_complete(self.reporting_relay_task) 24 | 25 | self.server.close() 26 | self.loop.run_until_complete(self.server.wait_closed()) 27 | 28 | self.loop.run_until_complete(cancel_all_pending_tasks(self.loop)) 29 | self.loop.close() 30 | 31 | def test_invalid_login(self): 32 | """Tests if postgres server responds correctly to a invalid login attempt.""" 33 | 34 | def postgresql_login(): 35 | try: 36 | psycopg2.connect("postgres://scott:tiger@0.0.0.0:2504/") 37 | except psycopg2.OperationalError as e: 38 | return e 39 | return None 40 | 41 | options = {'enabled': 'True', 'port': 2504} 42 | postgresql_cap = postgresql.PostgreSQL(options, self.loop) 43 | 44 | server_coro = asyncio.start_server( 45 | postgresql_cap.handle_session, '0.0.0.0', 2504, loop=self.loop) 46 | self.server = self.loop.run_until_complete(server_coro) 47 | 48 | postgresql_task = self.loop.run_in_executor(None, postgresql_login) 49 | login_exception = self.loop.run_until_complete(postgresql_task) 50 | 51 | self.assertIsInstance(login_exception, psycopg2.OperationalError) 52 | self.assertEqual( 53 | str(login_exception), 54 | 'FATAL: password authentication failed for user "scott"\n') 55 | 56 | def cb(self, socket, command, option): 57 | return 58 | -------------------------------------------------------------------------------- /heralding/tests/test_rdp.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import unittest 3 | import socket 4 | import ssl 5 | import os 6 | 7 | from heralding.capabilities import rdp 8 | from heralding.misc.common import cancel_all_pending_tasks, generate_self_signed_cert 9 | from heralding.reporting.reporting_relay import ReportingRelay 10 | 11 | 12 | class RDPClient(): 13 | 14 | @classmethod 15 | def ConnectionRequestPDU(cls): 16 | # selects tls security as highest supported method 17 | return b'\x03\x00\x00)$\xe0\x00\x00\x00\x00\x00Cookie: mstshash=xyz\r\n\x01\x00\x08\x00\x01\x00\x00\x00' 18 | 19 | @classmethod 20 | def ClientDataPDU(cls): 21 | tpkt = b'\x03\x00\x01\x8b' 22 | cc_header = b'\x02\xf0\x80' 23 | data = b'\x7fe\x82\x01\x7f\x04\x01\x01\x04\x01\x01\x01\x01\xff0\x1a\x02\x01"\x02\x01\x02\x02\x01\x00\x02\x01\x01\x02\x01\x00\x02\x01\x01\x02\x03\x00\xff\xff\x02\x01\x020\x19\x02\x01\x01\x02\x01\x01\x02\x01\x01\x02\x01\x01\x02\x01\x00\x02\x01\x01\x02\x02\x04 \x02\x01\x020 \x02\x03\x00\xff\xff\x02\x03\x00\xfc\x17\x02\x03\x00\xff\xff\x02\x01\x01\x02\x01\x00\x02\x01\x01\x02\x03\x00\xff\xff\x02\x01\x02\x04\x82\x01\x19\x00\x05\x00\x14|\x00\x01\x81\x10\x00\x08\x00\x10\x00\x01\xc0\x00Duca\x81\x02\x01\xc0\xea\x00\x04\x00\x08\x00\x00\x04\x00\x03\x01\xca\x03\xaa\t\x04\x00\x00(\n\x00\x00p\x00o\x00p\x00-\x00o\x00s\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xca\x01\x00\x00\x00\x00\x00\x10\x00\x07\x00!\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\xc0\x0c\x00\r\x00\x00\x00\x00\x00\x00\x00\x02\xc0\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00' 24 | return tpkt + cc_header + data 25 | 26 | @classmethod 27 | def ErectDomainRequest(cls): 28 | tpkt = b'\x03\x00\x00\x0c' 29 | cc_header = b'\x02\xf0\x80' 30 | data = b'\x04\x01\x00\x01\x00' 31 | return tpkt + cc_header + data 32 | 33 | @classmethod 34 | def AttactUserRequest(cls): 35 | tpkt = b'\x03\x00\x00\x08' 36 | cc_header = b'\x02\xf0\x80' 37 | data = b'\x28' 38 | return tpkt + cc_header + data 39 | 40 | @classmethod 41 | def ChannelJoinRequest(cls, channel): 42 | tpkt = b'\x03\x00\x00\x0c' 43 | cc_header = b'\x02\xf0\x80' 44 | if channel == 1007: 45 | data = b'8\x00\x06\x03\xef' 46 | if channel == 1003: 47 | data = b'8\x00\x06\x03\xeb' 48 | return tpkt + cc_header + data 49 | 50 | @classmethod 51 | def ClientInfoPDU(cls): 52 | # This conatins credentials 53 | tpkt = b'\x03\x00\x01\x5f' 54 | cc_header = b'\x02\xf0\x80' 55 | data = b'd\x00\x06\x03\xebp\x81P@\x00\x00\x00\x00\x00\x00\x00\xfb\x07\t\x00\x02\x00\x08\x00\x12\x00\x00\x00\x00\x00\x00\x00\x00\x00x\x00x\x00x\x00\x00\x00\x00\x00m\x00y\x00p\x00a\x00s\x00s\x001\x002\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x16\x001\x002\x007\x00.\x000\x00.\x000\x00.\x001\x00\x00\x00\x00\x00B\x00C\x00:\x00\\\x00W\x00i\x00n\x00d\x00o\x00w\x00s\x00\\\x00S\x00y\x00s\x00t\x00e\x00m\x003\x002\x00\\\x00m\x00s\x00t\x00s\x00c\x00a\x00x\x00.\x00d\x00l\x00l\x00\x00\x00\x00\x00\xb6\xfe\xff\xffC\x00\x00\x00l\x00\x00\x00i\x00\x00\x00e\x00\x00\x00n\x00\x00\x00t\x00\x00\x00 \x00\x00\x00L\x00\x00\x00o\x00\x00\x00c\x00\x00\x00a\x00\x00\x00l\x00\x00\x00 \x00\x00\x00T\x00\x00\x00i\x00\x00\x00m\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00C\x00\x00\x00l\x00\x00\x00i\x00\x00\x00e\x00\x00\x00n\x00\x00\x00t\x00\x00\x00 \x00\x00\x00L\x00\x00\x00o\x00\x00\x00c\x00\x00\x00a\x00\x00\x00l\x00\x00\x00 \x00\x00\x00T\x00\x00\x00i\x00\x00\x00m\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x00\x00' 56 | return tpkt + cc_header + data 57 | 58 | 59 | class RDPTests(unittest.TestCase): 60 | 61 | def create_cert_if_not_exists(self, pem_file): 62 | if not os.path.isfile(pem_file): 63 | cert, key = generate_self_signed_cert("US", "None", "None", "None", 64 | "None", "*", 365, 0) 65 | with open(pem_file, 'wb') as _pem_file: 66 | _pem_file.write(cert) 67 | _pem_file.write(key) 68 | 69 | def rdp_connect(self, ip_addr, port): 70 | s = socket.socket() 71 | s.connect((ip_addr, port)) 72 | s.sendall(RDPClient.ConnectionRequestPDU()) 73 | s.recv(1024) 74 | 75 | tls = ssl.wrap_socket(s, ssl_version=ssl.PROTOCOL_TLSv1_1) 76 | tls.sendall(RDPClient.ClientDataPDU()) 77 | tls.recv(4096) 78 | 79 | tls.send(RDPClient.ErectDomainRequest()) 80 | tls.sendall(RDPClient.AttactUserRequest()) 81 | tls.recv(512) 82 | 83 | tls.sendall(RDPClient.ChannelJoinRequest(1007)) 84 | tls.recv(1024) 85 | tls.sendall(RDPClient.ChannelJoinRequest(1003)) 86 | tls.recv(1024) 87 | 88 | tls.sendall(RDPClient.ClientInfoPDU()) 89 | res = b'' 90 | res = tls.recv(512) 91 | 92 | if res != b'': 93 | return False 94 | return True 95 | 96 | def setUp(self): 97 | self.loop = asyncio.new_event_loop() 98 | asyncio.set_event_loop(None) 99 | 100 | self.reporting_relay = ReportingRelay() 101 | self.reporting_relay_task = self.loop.run_in_executor( 102 | None, self.reporting_relay.start) 103 | 104 | def tearDown(self): 105 | self.reporting_relay.stop() 106 | # We give reporting_relay a chance to be finished 107 | self.loop.run_until_complete(self.reporting_relay_task) 108 | 109 | self.server.close() 110 | self.loop.run_until_complete(self.server.wait_closed()) 111 | 112 | self.loop.run_until_complete(cancel_all_pending_tasks(self.loop)) 113 | self.loop.close() 114 | 115 | def test_invalid_login(self): 116 | """Tests if rdp server responds correctly to a invalid login attempt.""" 117 | 118 | def rdp_login(): 119 | try: 120 | res = self.rdp_connect('0.0.0.0', 8389) 121 | except Exception as e: 122 | print("+++EXCEPTION+++ \n", e) 123 | return e 124 | return res 125 | 126 | options = { 127 | "enabled": "True", 128 | "port": 3389, 129 | "protocol_specific_data": { 130 | "banner": "", 131 | "cert": { 132 | "common_name": "*", 133 | "country": "US", 134 | "state": "None", 135 | "locality": "None", 136 | "organization": "None", 137 | "organizational_unit": "None", 138 | "valid_days": 365, 139 | "serial_number": 0 140 | } 141 | } 142 | } 143 | rdp_cap = rdp.RDP(options, self.loop) 144 | self.create_cert_if_not_exists('rdp.pem') 145 | server_coro = asyncio.start_server( 146 | rdp_cap.handle_session, '0.0.0.0', 8389, loop=self.loop) 147 | self.server = self.loop.run_until_complete(server_coro) 148 | 149 | rdp_task = self.loop.run_in_executor(None, rdp_login) 150 | login_res = self.loop.run_until_complete(rdp_task) 151 | 152 | self.assertEqual(True, login_res) 153 | -------------------------------------------------------------------------------- /heralding/tests/test_smtp.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012 Aniket Panse . 15 | 16 | # Aniket Panse grants Johnny Vestergaard 17 | # a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable 18 | # copyright license to reproduce, prepare derivative works of, publicly 19 | # display, publicly perform, sublicense, relicense, and distribute [the] Contributions 20 | # and such derivative works. 21 | 22 | import hmac 23 | import base64 24 | import asyncio 25 | import smtplib 26 | import unittest 27 | 28 | from heralding.capabilities import smtp 29 | from heralding.reporting.reporting_relay import ReportingRelay 30 | 31 | 32 | class SmtpTests(unittest.TestCase): 33 | 34 | def setUp(self): 35 | self.loop = asyncio.new_event_loop() 36 | asyncio.set_event_loop(None) 37 | 38 | self.reporting_relay = ReportingRelay() 39 | self.reporting_relay_task = self.loop.run_in_executor( 40 | None, self.reporting_relay.start) 41 | 42 | def tearDown(self): 43 | self.reporting_relay.stop() 44 | # We give reporting_relay a chance to be finished 45 | self.loop.run_until_complete(self.reporting_relay_task) 46 | 47 | self.server.close() 48 | self.loop.run_until_complete(self.server.wait_closed()) 49 | 50 | self.loop.close() 51 | 52 | def test_connection(self): 53 | """ Tries to connect and run a EHLO command. Very basic test. 54 | """ 55 | 56 | def smtp_connection(): 57 | smtp_ = smtplib.SMTP( 58 | '127.0.0.1', 8888, local_hostname='localhost', timeout=15) 59 | smtp_.ehlo() 60 | smtp_.quit() 61 | 62 | # Use uncommon port so that we can run test even if the Honeypot is running. 63 | options = { 64 | 'enabled': 'True', 65 | 'port': 8888, 66 | 'protocol_specific_data': { 67 | 'banner': 'test' 68 | }, 69 | 'users': { 70 | 'test': 'test' 71 | }, 72 | } 73 | smtp_cap = smtp.smtp(options, self.loop) 74 | 75 | server_coro = asyncio.start_server( 76 | smtp_cap.handle_session, '0.0.0.0', 8888, loop=self.loop) 77 | self.server = self.loop.run_until_complete(server_coro) 78 | 79 | smtp_task = self.loop.run_in_executor(None, smtp_connection) 80 | self.loop.run_until_complete(smtp_task) 81 | 82 | def test_AUTH_CRAM_MD5_reject(self): 83 | """ Makes sure the server rejects all invalid login attempts that use the 84 | CRAM-MD5 Authentication method. 85 | """ 86 | 87 | def encode_cram_md5(challenge, user, password): 88 | challenge = base64.decodebytes(challenge) 89 | response = user + b' ' + bytes( 90 | hmac.HMAC(password, challenge, digestmod="md5").hexdigest(), 'utf-8') 91 | return str(base64.b64encode(response), 'utf-8') 92 | 93 | def smtp_auth_cram_md5(): 94 | smtp_ = smtplib.SMTP( 95 | '127.0.0.1', 8888, local_hostname='localhost', timeout=15) 96 | _, resp = smtp_.docmd('AUTH', 'CRAM-MD5') 97 | code, resp = smtp_.docmd(encode_cram_md5(resp, b'test', b'test')) 98 | smtp_.quit() 99 | # For now, the server's going to return a 535 code. 100 | self.assertEqual(code, 535) 101 | 102 | options = { 103 | 'enabled': 'True', 104 | 'port': 8888, 105 | 'protocol_specific_data': { 106 | 'banner': 'Test' 107 | }, 108 | 'users': { 109 | 'someguy': 'test' 110 | } 111 | } 112 | smtp_cap = smtp.smtp(options, self.loop) 113 | 114 | server_coro = asyncio.start_server( 115 | smtp_cap.handle_session, '0.0.0.0', 8888, loop=self.loop) 116 | self.server = self.loop.run_until_complete(server_coro) 117 | 118 | smtp_task = self.loop.run_in_executor(None, smtp_auth_cram_md5) 119 | self.loop.run_until_complete(smtp_task) 120 | 121 | def test_AUTH_PLAIN_reject(self): 122 | """ Makes sure the server rejects all invalid login attempts that use the PLAIN Authentication method. 123 | """ 124 | 125 | def smtp_auth_plain_reject(): 126 | smtp_ = smtplib.SMTP( 127 | '127.0.0.1', 8888, local_hostname='localhost', timeout=15) 128 | arg = bytes('\0{0}\0{1}'.format('test', 'test'), 'utf-8') 129 | code, _ = smtp_.docmd('AUTH', 130 | 'PLAIN ' + str(base64.b64encode(arg), 'utf-8')) 131 | smtp_.quit() 132 | self.assertEqual(code, 535) 133 | 134 | options = { 135 | 'enabled': 'True', 136 | 'port': 0, 137 | 'protocol_specific_data': { 138 | 'banner': 'Test' 139 | }, 140 | 'users': { 141 | 'someguy': 'test' 142 | } 143 | } 144 | 145 | smtp_cap = smtp.smtp(options, self.loop) 146 | 147 | server_coro = asyncio.start_server( 148 | smtp_cap.handle_session, '0.0.0.0', 8888, loop=self.loop) 149 | self.server = self.loop.run_until_complete(server_coro) 150 | 151 | smtp_task = self.loop.run_in_executor(None, smtp_auth_plain_reject) 152 | self.loop.run_until_complete(smtp_task) 153 | 154 | def test_AUTH_LOGIN_reject(self): 155 | """ Makes sure the server rejects all invalid login attempts that use the LOGIN Authentication method. 156 | """ 157 | 158 | def smtp_auth_login_reject(): 159 | smtp_ = smtplib.SMTP( 160 | '127.0.0.1', 8888, local_hostname='localhost', timeout=15) 161 | smtp_.docmd('AUTH', 'LOGIN') 162 | smtp_.docmd(str(base64.b64encode(b'test'), 'utf-8')) 163 | code, _ = smtp_.docmd(str(base64.b64encode(b'test'), 'utf-8')) 164 | smtp_.quit() 165 | self.assertEqual(code, 535) 166 | 167 | options = { 168 | 'enabled': 'True', 169 | 'port': 0, 170 | 'protocol_specific_data': { 171 | 'banner': 'Test' 172 | }, 173 | 'users': { 174 | 'someguy': 'test' 175 | } 176 | } 177 | 178 | smtp_cap = smtp.smtp(options, self.loop) 179 | 180 | server_coro = asyncio.start_server( 181 | smtp_cap.handle_session, '0.0.0.0', 8888, loop=self.loop) 182 | self.server = self.loop.run_until_complete(server_coro) 183 | 184 | smtp_task = self.loop.run_in_executor(None, smtp_auth_login_reject) 185 | self.loop.run_until_complete(smtp_task) 186 | 187 | 188 | if __name__ == '__main__': 189 | unittest.main() 190 | -------------------------------------------------------------------------------- /heralding/tests/test_socks5.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2018 Roman Samoilenko 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import asyncio 17 | import unittest 18 | 19 | import heralding.capabilities.socks5 as socks 20 | from heralding.reporting.reporting_relay import ReportingRelay 21 | 22 | 23 | class Socks5Tests(unittest.TestCase): 24 | 25 | def setUp(self): 26 | self.loop = asyncio.new_event_loop() 27 | asyncio.set_event_loop(None) 28 | 29 | self.reporting_relay = ReportingRelay() 30 | self.reporting_relay_task = self.loop.run_in_executor( 31 | None, self.reporting_relay.start) 32 | 33 | def tearDown(self): 34 | self.reporting_relay.stop() 35 | # We give reporting_relay a chance to be finished 36 | self.loop.run_until_complete(self.reporting_relay_task) 37 | 38 | self.server.close() 39 | self.loop.run_until_complete(self.server.wait_closed()) 40 | 41 | self.loop.close() 42 | 43 | def test_socks_authentication(self): 44 | 45 | async def socks_auth(): 46 | reader, writer = await asyncio.open_connection( 47 | '127.0.0.1', 8888, loop=self.loop) 48 | 49 | # Greeting to the server. version+authmethod number+authmethod 50 | client_greeting = socks.SOCKS_VERSION + b"\x01" + socks.AUTH_METHOD 51 | writer.write(client_greeting) 52 | 53 | # Receive version+chosen authmethod 54 | _ = await reader.read(2) 55 | 56 | # Send credentials. 57 | # version+username len+username+password len+password 58 | credentials = b"\x05\x08username\x08password" 59 | writer.write(credentials) 60 | 61 | # Receive authmethod+\xff 62 | res = await reader.read(2) 63 | self.assertEqual(res, socks.AUTH_METHOD + socks.SOCKS_FAIL) 64 | 65 | options = {'enabled': 'True', 'port': 8888, 'timeout': 30} 66 | capability = socks.Socks5(options, self.loop) 67 | 68 | server_coro = asyncio.start_server( 69 | capability.handle_session, '127.0.0.1', 8888, loop=self.loop) 70 | self.server = self.loop.run_until_complete(server_coro) 71 | self.loop.run_until_complete(socks_auth()) 72 | -------------------------------------------------------------------------------- /heralding/tests/test_ssh.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017 Johnny Vestergaard 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import asyncio 17 | import unittest 18 | 19 | from heralding.capabilities.ssh import SSH 20 | from heralding.reporting.reporting_relay import ReportingRelay 21 | 22 | import asyncssh 23 | 24 | 25 | class SshTests(unittest.TestCase): 26 | 27 | def setUp(self): 28 | self.loop = asyncio.new_event_loop() 29 | asyncio.set_event_loop(None) 30 | 31 | self.reporting_relay = ReportingRelay() 32 | self.reporting_relay_task = self.loop.run_in_executor( 33 | None, self.reporting_relay.start) 34 | 35 | def tearDown(self): 36 | self.reporting_relay.stop() 37 | # We give reporting_relay a chance to be finished 38 | self.loop.run_until_complete(self.reporting_relay_task) 39 | 40 | self.server.close() 41 | self.loop.run_until_complete(self.server.wait_closed()) 42 | 43 | self.loop.close() 44 | 45 | def test_basic_login(self): 46 | 47 | async def run_client(): 48 | async with asyncssh.connect( 49 | 'localhost', 50 | port=8888, 51 | username='johnny', 52 | password='secretpw', 53 | known_hosts=None) as _: 54 | pass 55 | 56 | ssh_key_file = 'ssh.key' 57 | SSH.generate_ssh_key(ssh_key_file) 58 | 59 | options = {'enabled': 'True', 'port': 8888} 60 | server_coro = asyncssh.create_server( 61 | lambda: SSH(options, self.loop), 62 | '0.0.0.0', 63 | 8888, 64 | server_host_keys=['ssh.key']) 65 | self.server = self.loop.run_until_complete(server_coro) 66 | 67 | try: 68 | self.loop.run_until_complete(run_client()) 69 | except asyncssh.misc.PermissionDenied: 70 | pass 71 | -------------------------------------------------------------------------------- /heralding/tests/test_telnet.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017 Johnny Vestergaard 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import asyncio 17 | import unittest 18 | 19 | import telnetlib 20 | 21 | from heralding.capabilities import telnet 22 | from heralding.misc.common import cancel_all_pending_tasks 23 | from heralding.reporting.reporting_relay import ReportingRelay 24 | 25 | 26 | class TelnetTests(unittest.TestCase): 27 | 28 | def setUp(self): 29 | self.loop = asyncio.new_event_loop() 30 | asyncio.set_event_loop(None) 31 | 32 | self.reporting_relay = ReportingRelay() 33 | self.reporting_relay_task = self.loop.run_in_executor( 34 | None, self.reporting_relay.start) 35 | 36 | def tearDown(self): 37 | self.reporting_relay.stop() 38 | # We give reporting_relay a chance to be finished 39 | self.loop.run_until_complete(self.reporting_relay_task) 40 | 41 | self.server.close() 42 | self.loop.run_until_complete(self.server.wait_closed()) 43 | 44 | self.loop.run_until_complete(cancel_all_pending_tasks(self.loop)) 45 | self.loop.close() 46 | 47 | def test_invalid_login(self): 48 | """Tests if telnet server responds correctly to a invalid login attempt.""" 49 | 50 | def telnet_login(): 51 | client = telnetlib.Telnet('localhost', 2503) 52 | # set this to 1 if having problems with this test 53 | client.set_debuglevel(0) 54 | # this disables all command negotiation. 55 | client.set_option_negotiation_callback(self.cb) 56 | # Expect username as first output 57 | 58 | reply = client.read_until(b'Username: ', 1) 59 | self.assertEqual(b'Username: ', reply) 60 | 61 | client.write(b'someuser' + b'\r\n') 62 | reply = client.read_until(b'Password: ', 5) 63 | self.assertTrue(reply.endswith(b'Password: ')) 64 | 65 | client.write(b'somepass' + b'\r\n') 66 | reply = client.read_until(b'\n', 5) 67 | self.assertTrue(b'\n' in reply) 68 | 69 | client.close() 70 | 71 | options = { 72 | 'enabled': 'True', 73 | 'port': 2503, 74 | 'protocol_specific_data': { 75 | 'max_attempts': 3 76 | }, 77 | 'users': { 78 | 'test': 'test' 79 | } 80 | } 81 | telnet_cap = telnet.Telnet(options, self.loop) 82 | 83 | server_coro = asyncio.start_server( 84 | telnet_cap.handle_session, '0.0.0.0', 2503, loop=self.loop) 85 | self.server = self.loop.run_until_complete(server_coro) 86 | 87 | telnet_task = self.loop.run_in_executor(None, telnet_login) 88 | self.loop.run_until_complete(telnet_task) 89 | 90 | def cb(self, socket, command, option): 91 | return 92 | -------------------------------------------------------------------------------- /heralding/tests/test_vnc.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017 Roman Samoilenko 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import os 17 | import asyncio 18 | import unittest 19 | 20 | from heralding.capabilities.vnc import Vnc, RFB_VERSION, VNC_AUTH 21 | from heralding.reporting.reporting_relay import ReportingRelay 22 | 23 | 24 | class VncTests(unittest.TestCase): 25 | 26 | def setUp(self): 27 | self.loop = asyncio.new_event_loop() 28 | asyncio.set_event_loop(None) 29 | 30 | self.reporting_relay = ReportingRelay() 31 | self.reporting_relay_task = self.loop.run_in_executor( 32 | None, self.reporting_relay.start) 33 | 34 | def tearDown(self): 35 | self.reporting_relay.stop() 36 | # We give reporting_relay a chance to be finished 37 | self.loop.run_until_complete(self.reporting_relay_task) 38 | 39 | self.server.close() 40 | self.loop.run_until_complete(self.server.wait_closed()) 41 | 42 | self.loop.close() 43 | 44 | def test_vnc_authentication(self): 45 | 46 | async def vnc_auth(): 47 | reader, writer = await asyncio.open_connection( 48 | '127.0.0.1', 8888, loop=self.loop) 49 | # server rfb version 50 | _ = await reader.readline() 51 | writer.write(RFB_VERSION) 52 | 53 | # available auth methods 54 | _ = await reader.read(1024) 55 | writer.write(VNC_AUTH) 56 | 57 | # challenge 58 | _ = await reader.read(1024) 59 | # Pretending, that we encrypt received challenge with DES and send back the result. 60 | client_response = os.urandom(16) 61 | writer.write(client_response) 62 | 63 | # security result 64 | _ = await reader.read(1024) 65 | 66 | options = {'enabled': 'True', 'port': 8888, 'timeout': 30} 67 | capability = Vnc(options, self.loop) 68 | 69 | server_coro = asyncio.start_server( 70 | capability.handle_session, '0.0.0.0', 8888, loop=self.loop) 71 | self.server = self.loop.run_until_complete(server_coro) 72 | 73 | self.loop.run_until_complete(vnc_auth()) 74 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | psycopg2-binary 2 | PyMySQL==0.9.3 3 | pymysql 4 | pycryptodome 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pycryptodome==3.20.0 2 | nose==1.3.7 3 | aiosmtpd==1.4.4.post2 4 | asyncssh==2.14.2 5 | pyOpenSSL==23.3.0 6 | pyaml==23.12.0 7 | pyzmq==25.1.2 8 | psycopg2-binary==2.9.9 9 | hpfeeds3==0.9.10 10 | rsa==4.9 11 | requests==2.31.0 12 | pymysql==1.1.0 13 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from ez_setup import use_setuptools 2 | use_setuptools() # NOQA 3 | from setuptools import setup, find_packages 4 | from heralding import version as heralding_version 5 | 6 | setup( 7 | name='Heralding', 8 | version=heralding_version, 9 | packages=find_packages(exclude=['bin', 'docs', '*.pyc']), 10 | scripts=['bin/heralding'], 11 | url='https://github.com/johnnykv/heralding', 12 | license='GPL 3', 13 | author='Johnny Vestergaard', 14 | author_email='jkv@unixcluster.dk', 15 | include_package_data=True, 16 | long_description=open('README.rst').read(), 17 | description='Credentials catching honeypot.', 18 | test_suite='nose.collector', 19 | install_requires=open('requirements.txt').read(), 20 | tests_require=open('requirements-test.txt').read(), 21 | package_data={ 22 | "heralding": ["heralding.yml", "wordlist.txt"], 23 | }, 24 | ) 25 | --------------------------------------------------------------------------------