diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..149a1e5 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,22 @@ +# This file is part of masscanned. +# Copyright 2021 - The IVRE project +# +# Masscanned is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Masscanned is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +# License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Masscanned. If not, see . + +version: 2 +updates: +- package-ecosystem: cargo + directory: "/" + schedule: + interval: daily diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..62cf039 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,120 @@ +# This file is part of masscanned. +# Copyright 2021 - 2024 The IVRE project +# +# Masscanned is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Masscanned is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +# License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Masscanned. If not, see . + +name: Build masscanned + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + steps: + + - name: Git checkout + uses: actions/checkout@v2 + + - name: Get Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + + - name: Run cargo fmt + uses: actions-rs/cargo@v1 + with: + command: fmt + args: -- --check + + - name: Install packages for build + run: sudo apt-get -q update && sudo apt-get -qy install libpcap-dev + + - name: Run cargo build + uses: actions-rs/cargo@v1 + with: + command: build + + - name: Run cargo test + uses: actions-rs/cargo@v1 + with: + command: test + + - name: Create build archive + run: tar cf masscanned.tar target/debug/masscanned + + - name: Upload binary + uses: actions/upload-artifact@v4 + with: + name: masscanned.tar + path: masscanned.tar + + test: + needs: build + runs-on: ubuntu-latest + steps: + + - name: Git checkout + uses: actions/checkout@v2 + + - name: Get binary + uses: actions/download-artifact@v4 + with: + name: masscanned.tar + + - name: Extract build archive + run: tar xf masscanned.tar + + - name: Use Python + uses: actions/setup-python@v2 + with: + python-version: 3.9 + + - name: Install dependencies + run: sudo pip install -U -r test/requirements.txt + + - name: Install linting tools + run: sudo pip install -U flake8 black + + - name: Install packages for tests + run: sudo apt-get -q update && sudo apt-get -qy install nmap rpcbind smbclient + + - name: Run black + run: black -t py36 --check test/test_masscanned.py test/src/ + + - name: Run flake8 + run: flake8 --ignore=E266,E501,W503 test/test_masscanned.py test/src/ + + - name: Run tests + run: sudo python test/test_masscanned.py + + - name: Display logs + run: echo STDOUT; cat test/res/masscanned.stdout && echo && echo STDERR && cat test/res/masscanned.stderr + if: failure() + + docker: + runs-on: ubuntu-latest + steps: + + - name: Git checkout + uses: actions/checkout@v2 + + - name: Build archive + run: git archive --format=tar --prefix=masscanned-master/ HEAD -o docker/masscanned.tar + + - name: Build image + uses: docker/build-push-action@v5 + with: + push: false + context: docker/ + file: docker/Dockerfile-local diff --git a/.gitignore b/.gitignore index f07cd65..b40b324 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,11 @@ /target/ -Cargo.lock **/*.rs.bk # Vim temporary files *.swp *.swo +# Emacs temporary files +*~ *__pycache__* test/res/* diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..30cd324 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1139 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd2405b3ac1faab2990b74d728624cd9fd115651fcecc7c2d8daf01376275ba" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anstyle-parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" + +[[package]] +name = "bumpalo" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder_slice" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b294e30387378958e8bf8f4242131b930ea615ff81e8cac2440cea0a6013190" +dependencies = [ + "byteorder", +] + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.5.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "cxx" +version = "1.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90d59d9acd2a682b4e40605a242f6670eaa58c5957471cbf85e8aa6a0b97a5e8" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebfa40bda659dd5c864e65f4c9a2b0aff19bea56b017b9b77c73d3766a453a38" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn 1.0.107", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "457ce6757c5c70dc6ecdbda6925b958aae7f959bda7d8fb9bde889e34a09dc03" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebf883b7aacd7b2aeb2a7b338648ee19f57c140d4ee8e52c68979c6b2f7f2263" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.107", +] + +[[package]] +name = "derive-into-owned" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d94d81e3819a7b06a8638f448bc6339371ca9b6076a99d4a43eece3c4c923" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.107", +] + +[[package]] +name = "dns-parser" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4d33be9473d06f75f58220f71f7a9317aca647dc061dbd3c361b0bef505fbea" +dependencies = [ + "byteorder", + "quick-error", +] + +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "flate2" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "getrandom" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d3d0e0f38255e7fa3cf31335b3a56f05febd18025f4db5ef7a0cfb4f8da651f" + +[[package]] +name = "iana-time-zone" +version = "0.1.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +dependencies = [ + "cxx", + "cxx-build", +] + +[[package]] +name = "ipnetwork" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" +dependencies = [ + "serde", +] + +[[package]] +name = "is-terminal" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455" +dependencies = [ + "hermit-abi", + "rustix", + "windows-sys 0.52.0", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "js-sys" +version = "0.3.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "libloading" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" +dependencies = [ + "cfg-if", + "windows-targets 0.52.0", +] + +[[package]] +name = "link-cplusplus" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" +dependencies = [ + "cc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "masscanned" +version = "0.2.0" +dependencies = [ + "bitflags 2.9.4", + "byteorder", + "chrono", + "clap", + "dns-parser", + "flate2", + "itertools", + "lazy_static", + "log", + "pcap", + "pcap-file", + "pnet", + "rand", + "siphasher", + "stderrlog", + "strum", + "strum_macros", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "miniz_oxide" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +dependencies = [ + "adler2", +] + +[[package]] +name = "no-std-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" + +[[package]] +name = "pcap" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83cdabc34a80d9ec3563694cc31423fba6bb9bab4f31a9a5d5b85f29bd6d660a" +dependencies = [ + "bitflags 1.3.2", + "errno 0.2.8", + "libc", + "libloading", + "pkg-config", + "regex", + "windows-sys 0.36.1", +] + +[[package]] +name = "pcap-file" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc1f139757b058f9f37b76c48501799d12c9aa0aa4c0d4c980b062ee925d1b2" +dependencies = [ + "byteorder_slice", + "derive-into-owned", + "thiserror", +] + +[[package]] +name = "pkg-config" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" + +[[package]] +name = "pnet" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd959a8268165518e2bf5546ba84c7b3222744435616381df3c456fe8d983576" +dependencies = [ + "ipnetwork", + "pnet_base", + "pnet_datalink", + "pnet_packet", + "pnet_sys", + "pnet_transport", +] + +[[package]] +name = "pnet_base" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872e46346144ebf35219ccaa64b1dffacd9c6f188cd7d012bd6977a2a838f42e" +dependencies = [ + "no-std-net", +] + +[[package]] +name = "pnet_datalink" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c302da22118d2793c312a35fb3da6846cb0fab6c3ad53fd67e37809b06cdafce" +dependencies = [ + "ipnetwork", + "libc", + "pnet_base", + "pnet_sys", + "winapi", +] + +[[package]] +name = "pnet_macros" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a780e80005c2e463ec25a6e9f928630049a10b43945fea83207207d4a7606f4" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn 1.0.107", +] + +[[package]] +name = "pnet_macros_support" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d932134f32efd7834eb8b16d42418dac87086347d1bc7d142370ef078582bc" +dependencies = [ + "pnet_base", +] + +[[package]] +name = "pnet_packet" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bde678bbd85cb1c2d99dc9fc596e57f03aa725f84f3168b0eaf33eeccb41706" +dependencies = [ + "glob", + "pnet_base", + "pnet_macros", + "pnet_macros_support", +] + +[[package]] +name = "pnet_sys" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf7a58b2803d818a374be9278a1fe8f88fce14b936afbe225000cfcd9c73f16" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "pnet_transport" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "813d1c0e4defbe7ee22f6fe1755f122b77bfb5abe77145b1b5baaf463cab9249" +dependencies = [ + "libc", + "pnet_base", + "pnet_packet", + "pnet_sys", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom", +] + +[[package]] +name = "regex" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" + +[[package]] +name = "rustix" +version = "0.38.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" +dependencies = [ + "bitflags 2.9.4", + "errno 0.3.8", + "libc", + "linux-raw-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "scratch" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" + +[[package]] +name = "serde" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "stderrlog" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c910772f992ab17d32d6760e167d2353f4130ed50e796752689556af07dc6b" +dependencies = [ + "chrono", + "is-terminal", + "log", + "termcolor", + "thread_local", +] + +[[package]] +name = "strsim" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "syn" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.107", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 1.0.107", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.107", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + +[[package]] +name = "windows-sys" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +dependencies = [ + "windows_aarch64_msvc 0.36.1", + "windows_i686_gnu 0.36.1", + "windows_i686_msvc 0.36.1", + "windows_x86_64_gnu 0.36.1", + "windows_x86_64_msvc 0.36.1", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.0", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-targets" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +dependencies = [ + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[package]] +name = "windows_i686_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[package]] +name = "windows_i686_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.4", +] diff --git a/Cargo.toml b/Cargo.toml index 789f87f..6cd59b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,22 +21,23 @@ authors = ["_Frky <3105926+Frky@users.noreply.github.com>"] edition = "2018" [dependencies] -pcap = "0.7.0" -pcap-file = "1.1.1" -pnet = "0.26.0" -# pnet = { path = "libpnet" } -clap = "2.33.3" -log = "0.4.11" -stderrlog = "0.5.0" -itertools = "0.9.0" -rand = "0.7.3" +bitflags = "2.9.4" +byteorder = "1.5.0" +chrono = "0.4.42" +clap = "4.5.47" dns-parser = "0.8.0" -netdevice = "0.1.1" -bitflags = "1.2.1" -lazy_static = "1.4.0" -siphasher = "0.3" -chrono = "0.4.19" -byteorder = "1.4.3" +flate2 = "1.1" +itertools = "0.14.0" +lazy_static = "1.5.0" +log = "0.4.28" +pcap = "2.3.0" +pcap-file = "2.0.0" +pnet = { version = "0.33.0", features = ["std"] } +rand = "0.9.2" +siphasher = "1.0" +stderrlog = "0.6.0" +strum = "0.27.2" +strum_macros = "0.27.2" [[bin]] name = "masscanned" diff --git a/README.md b/README.md index 8f5a1f6..d61c5f9 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ +[![Build masscanned](https://github.com/ivre/masscanned/actions/workflows/test.yml/badge.svg?branch=master)](https://github.com/ivre/masscanned/actions/workflows/test.yml?branch=master) + # Masscanned **Masscanned** (name inspired, of course, by [masscan](https://github.com/robertdavidgraham/masscan)) is a network responder. Its purpose is to provide generic answers to as many protocols as possible, -and with as few asumptions as possible on the client's intentions. +and with as few assumptions as possible on the client's intentions. > *Let them talk first.* @@ -17,22 +19,32 @@ For example, when it receives network packets: * **masscanned** answers to `TCP SYN` (any port) with `TCP SYN/ACK` on any port, * **masscanned** answers to `HTTP` requests (any verb) over `TCP/UDP` (any port) with a `HTTP 401` web page. -![demo](doc/demo.gif) +![demo](doc/img/demo.gif) + +## Overview **Masscanned** currently supports most common protocols at layers 2-3-4, and a few application -protocols: +protocols. -* `Eth::ARP::REQ`, -* `Eth::IPv{4,6}::ICMP::ECHO-REQ`, -* `Eth::IPv{4,6}::TCP::SYN` (all ports), -* `Eth::IPv{4,6}::TCP::PSHACK` (all ports), -* `Eth::IPv6::ICMP::ND_NS`. -* `Eth::IPv{4,6}::{TCP,UDP}::HTTP` (all HTTP verbs), -* `Eth::IPv{4,6}::{TCP,UDP}::STUN`, -* `Eth::IPv{4,6}::{TCP,UDP}::SSH` (Server Protocol only). +### Network protocols + +* ARP (answers to ARP requests) +* ICMP (answers to ping) +* ICMPv6 (answers to ND NS) +* TCP (answers to SYN and PUSH) + +### Application protocols + +* HTTP (answers to all verbs) +* SSH (answers to the client banner) +* STUN (answers to binding requests) +* SMB +* DNS (answers to IN/A queries) ## Try it locally +### On your host + 1. Build **masscanned** ``` $ cargo build @@ -64,13 +76,77 @@ $ cargo build ... ``` -## Protocols +### In a Docker + +1. Install docker: +``` +# apt install docker.io +``` +1. Build docker container: +``` +$ cd masscanned/docker && docker build -t masscanned:test . +``` +1. Run docker container: +``` +$ docker run --cap-add=NET_ADMIN masscanned:test +``` +1. Send packets to **masscanned** +``` +# arping 172.17.0.2 +# ping 172.17.0.2 +# nc -n -v 172.17.0.2 80 +# nc -n -v -u 172.17.0.2 80 +... +``` + +## Use it + +A good use of **masscanned** is to deploy it on a VPS with one or more public IP addresses. + +To use the results, the best way is to capture all network traffic on the interface **masscanned** is listening to/responding on. +The pcaps can then be analyzed using [zeek](https://zeek.org/) and the output files can typically be pushed in an instance of **IVRE**. + +A documentation on how to deploy an instance of **masscanned** on a VPS is coming (see [Issue #2](https://github.com/ivre/masscanned/issues/2)). + +### Supported options + +``` +Network answering machine for various network protocols (L2-L3-L4 + applications) + +Usage: masscanned [OPTIONS] --iface + +Options: + -i, --iface + the interface to use for receiving/sending packets + -m, --mac-addr + MAC address to use in the response packets + --self-ip-file + File with the list of IP addresses handled by masscanned + --self-ip-list + Inline list of IP addresses handled by masscanned, comma-separated + --remote-ip-deny-file + File with the list of IP addresses from which masscanned will ignore packets + --remote-ip-deny-list + Inline list of IP addresses from which masscanned will ignore packets + -v... + Increase message verbosity + -q, --quiet + Quiet mode: do not output anything on stdout + --format + Format in which to output logs [default: console] [possible values: console, logfmt] + -h, --help + Print help information + -V, --version + Print version information +``` + +## Supported protocols - details ### Layer 2 #### ARP -`masscanned` anwsers to `ARP` requests, for requests that target an `IPv4` address +`masscanned` answers to `ARP` requests, for requests that target an `IPv4` address that is handled by `masscanned` (*i.e.*, an address that is in the IP address file given with option `-f`). @@ -112,7 +188,7 @@ An additionnal requirement is that the next layer protocol is supported - see be #### IPv4 -The following L4 protocols are suppported for an `IPv4` packet: +The following L3+/4 protocols are supported for an `IPv4` packet: * `ICMPv4` * `UDP` @@ -122,7 +198,7 @@ If the next layer protocol is not one of them, the packet is dropped. #### IPv6 -The following L4 protocols are suppported for an `IPv6` packet: +The following L3+/4 protocols are supported for an `IPv6` packet: * `ICMPv6` * `UDP` @@ -148,7 +224,7 @@ code `0` and the same payload as the incoming packet, as specified by [RFC 792]( * the `ICMP` type is `NeighborSol` (`135`) **and**: * no IP (v4 or v6) was speficied for `masscanned` - * **or** the target address of the Neighbor Solicitation is one of `masccanned` + * **or** the target address of the Neighbor Solicitation is one of `masscanned` *In that case, the answer is a `Neighbor Advertisement` (`136`) packet with `masscanned` `MAC` address* @@ -166,19 +242,68 @@ code `0` and the same payload as the incoming packet, as specified by [RFC 792]( a supported protocol (Layer 5/6/7) has been detected, * if the received packet has flag `ACK`, it is ignored, * if the received packet has flag `RST` or `FIN-ACK`, it is ignored, -* if the received packet has flag `SYN`, then `masscanned` answers with a `SYN-ACK` packet, setting a **SYNACK-cookie** in the sequence number. +* if the received packet has flag `SYN`, then `masscanned` tries to imitate the behaviour +of a standard Linux stack - which is: + * if there are additional flags that are not among `PSH`, `URG`, `CWR`, `ECE`, then the `SYN` is ignored, + * if the flags `CWR` and`ECE` are simultaneously set, then the `SYN` is ignored, + * in any other case, `masscanned` answers with a `SYN-ACK` packet, setting a **SYNACK-cookie** in the sequence number. #### UDP `masscanned` answers to an `UDP` packet if and only if the upper-layer protocol is handled and provides an answer. -### Protocols +### Application protocols #### HTTP +`masscanned` answers to any `HTTP` request (any **valid** verb) with a `401 Authorization Required`. +Note that `HTTP` requests with an invalid verb will not be answered. + +Example: + +``` +$ curl -X GET 10.11.10.129 + +401 Authorization Required + +

401 Authorization Required

+
nginx/1.14.2
+ + +$ curl -X OPTIONS 10.11.10.129 + +401 Authorization Required + +

401 Authorization Required

+
nginx/1.14.2
+ + +$ curl -X HEAD 10.11.10.129 +Warning: Setting custom HTTP method to HEAD with -X/--request may not work the +Warning: way you want. Consider using -I/--head instead. + +401 Authorization Required + +

401 Authorization Required

+
nginx/1.14.2
+ + +$ curl -X XXX 10.11.10.129 +[timeout] +``` + #### STUN +Example: + +``` +$ stun 10.11.10.129 +STUN client version 0.97 +Primary: Open +Return value is 0x000001 +``` + #### SSH `masscanned` answers to `SSH` `Client: Protocol` messages with the following `Server: Protocol` message: @@ -187,6 +312,57 @@ is handled and provides an answer. SSH-2.0-1\r\n ``` +#### SMB + +`masscanned` answers to `Negotiate Protocol Request` packets in order for the +client to send a `NTLMSSP_NEGOTIATE`, to which `masscanned` answers with a challenge. + +Example: + +``` +##$ smbclient -U user \\\\10.11.10.129\\shared +Enter WORKGROUP\user's password: +``` + +#### DNS + +`masscanned` answers to `DNS` queries of class `IN` and type `A` (for now). +The answer it provides always contains the IP address the query was sent to. + +Example: + +``` +$ host -t A masscan.ned 10.11.10.129 +Using domain server: +Name: 10.11.10.129 +Address: 10.11.10.129#53 +Aliases: + +masscan.ned has address 10.11.10.129 +$ host -t A masscan.ned 10.11.10.130 +Using domain server: +Name: 10.11.10.130 +Address: 10.11.10.130#53 +Aliases: + +masscan.ned has address 10.11.10.130 +$ host -t A masscan.ned 10.11.10.131 +Using domain server: +Name: 10.11.10.131 +Address: 10.11.10.131#53 +Aliases: + +masscan.ned has address 10.11.10.131 +$ host -t A masscan.ned 10.11.10.132 +Using domain server: +Name: 10.11.10.132 +Address: 10.11.10.132#53 +Aliases: + +masscan.ned has address 10.11.10.132 + +``` + ## Internals ### Tests @@ -196,68 +372,118 @@ SSH-2.0-1\r\n ``` $ cargo test Compiling masscanned v0.2.0 (/zdata/workdir/masscanned) - Finished test [unoptimized + debuginfo] target(s) in 2.34s - Running target/debug/deps/masscanned-b86211a090e50323 + Finished test [unoptimized + debuginfo] target(s) in 3.83s + Running unittests (target/debug/deps/masscanned-f9292f8600038978) -running 36 tests +running 92 tests test client::client_info::tests::test_client_info_eq ... ok test layer_2::arp::tests::test_arp_reply ... ok -test layer_3::ipv4::tests::test_ipv4_reply ... ok -test layer_3::ipv6::tests::test_ipv6_reply ... ok -test layer_4::icmpv6::tests::test_icmpv6_reply ... ok +test layer_2::tests::test_eth_empty ... ok test layer_2::tests::test_eth_reply ... ok -test layer_4::icmpv6::tests::test_nd_na_reply ... ok -test layer_4::tcp::tests::test_synack_cookie_ipv4 ... ok +test layer_3::ipv4::tests::test_ipv4_reply ... ok +test layer_3::ipv4::tests::test_ipv4_empty ... ok +test layer_3::ipv6::tests::test_ipv6_empty ... ok +test layer_3::ipv6::tests::test_ipv6_reply ... ok test layer_4::icmpv4::tests::test_icmpv4_reply ... ok +test layer_4::icmpv6::tests::test_icmpv6_reply ... ok +test layer_4::icmpv6::tests::test_nd_na_reply ... ok test layer_4::tcp::tests::test_synack_cookie_ipv6 ... ok -test proto::http::test_http_request_field ... ok -test proto::http::test_http_request_no_field ... ok -test proto::http::test_http_request_line ... ok -test proto::http::test_http_verb ... ok -test proto::stun::tests::test_change_request_port ... ok -test proto::stun::tests::test_proto_stun_ipv6 ... ok -test proto::stun::tests::test_proto_stun_ipv4 ... ok +test layer_4::tcp::tests::test_tcp_fin_ack_wrap ... ok +test proto::dns::cst::tests::class_parse ... ok +test layer_4::tcp::tests::test_tcp_fin_ack ... ok +test layer_4::tcp::tests::test_synack_cookie_ipv4 ... ok +test proto::dns::cst::tests::type_parse ... ok +test proto::dns::header::tests::parse_byte_by_byte ... ok +test proto::dns::header::tests::repl_id ... ok +test proto::dns::header::tests::repl_opcode ... ok +test proto::dns::header::tests::repl_ancount ... ok +test proto::dns::header::tests::repl_rd ... ok +test proto::dns::query::tests::parse_in_a_all ... ok +test proto::dns::header::tests::parse_all ... ok +test proto::dns::query::tests::repl ... ok +test proto::dns::query::tests::reply_in_a ... ok +test proto::dns::rr::tests::parse_all ... ok +test proto::dns::rr::tests::parse_byte_by_byte ... ok +test proto::dns::query::tests::parse_in_a_byte_by_byte ... ok +test proto::dns::tests::parse_qd_all ... ok +test proto::dns::tests::parse_qd_byte_by_byte ... ok +test proto::dns::rr::tests::build ... ok +test proto::dns::tests::parse_qd_rr_all ... ok +test proto::dns::tests::parse_qr_rr_byte_by_byte ... ok +test proto::dns::tests::parse_rr_byte_by_byte ... ok +test proto::dns::tests::parse_rr_all ... ok +test proto::dns::tests::reply_in_a ... ok +test proto::http::tests::test_http_request_line ... ok +test proto::http::tests::test_http_request_no_field ... ok +test proto::http::tests::test_http_request_field ... ok +test proto::http::tests::test_http_verb ... ok +test proto::rpc::tests::test_probe_nmap ... ok +test proto::rpc::tests::test_probe_nmap_split1 ... ok +test proto::rpc::tests::test_probe_portmap_v4_dump ... ok +test proto::rpc::tests::test_probe_nmap_split2 ... ok +test proto::rpc::tests::test_probe_nmap_udp ... ok +test proto::smb::tests::test_smb1_session_setup_request_parse ... ok +test proto::smb::tests::test_smb1_protocol_nego_parsing ... ok +test proto::smb::tests::test_smb1_protocol_nego_reply ... ok +test proto::smb::tests::test_smb1_session_setup_request_reply ... ok +test proto::smb::tests::test_smb2_protocol_nego_parsing ... ok +test proto::smb::tests::test_smb2_protocol_nego_reply ... ok +test proto::smb::tests::test_smb2_session_setup_request_reply ... ok +test proto::smb::tests::test_smb2_session_setup_request_parse ... ok +test proto::ssh::tests::ssh_1_banner_cr ... ok +test proto::ssh::tests::ssh_1_banner_crlf ... ok +test proto::ssh::tests::ssh_1_banner_lf ... ok +test proto::ssh::tests::ssh_1_banner_space ... ok +test proto::ssh::tests::ssh_2_banner_cr ... ok +test proto::ssh::tests::ssh_1_banner_parse ... ok +test proto::ssh::tests::ssh_2_banner_parse ... ok +test proto::ssh::tests::ssh_2_banner_lf ... ok +test proto::ssh::tests::ssh_2_banner_crlf ... ok test proto::stun::tests::test_change_request_port_overflow ... ok -test smack::smack::tests::test_anchor_end ... ok -test smack::smack::tests::test_anchor_begin ... ok -test smack::smack::tests::test_multiple_matches ... ok -test smack::smack::tests::test_http_banner ... ok -test smack::smack::tests::test_multiple_matches_wildcard ... ok -test smack::smack::tests::test_proto ... ok -test smack::smack::tests::test_wildcard ... ok +test proto::stun::tests::test_proto_stun_ipv4 ... ok +test proto::stun::tests::test_change_request_port ... ok +test proto::ssh::tests::ssh_2_banner_space ... ok +test proto::stun::tests::test_proto_stun_ipv6 ... ok +test proto::tcb::tests::test_proto_tcb_proto_state_http ... ok +test proto::tests::dispatch_dns ... ok +test proto::tcb::tests::test_proto_tcb_proto_state_rpc ... ok +test proto::tcb::tests::test_proto_tcb_proto_id ... ok +test proto::tests::test_proto_dispatch_http ... ok test proto::tests::test_proto_dispatch_ssh ... ok +test proto::tests::test_proto_dispatch_ghost ... ok test proto::tests::test_proto_dispatch_stun ... ok +test smack::smack::tests::test_anchor_end ... ok +test smack::smack::tests::test_multiple_matches_wildcard ... ok +test smack::smack::tests::test_multiple_matches ... ok +test smack::smack::tests::test_anchor_begin ... ok +test smack::smack::tests::test_http_banner ... ok test synackcookie::tests::test_clientinfo ... ok +test synackcookie::tests::test_ip4 ... ok test synackcookie::tests::test_ip4_dst ... ok test synackcookie::tests::test_ip4_src ... ok -test synackcookie::tests::test_ip4 ... ok test synackcookie::tests::test_ip6 ... ok test synackcookie::tests::test_key ... ok test synackcookie::tests::test_tcp_dst ... ok test synackcookie::tests::test_tcp_src ... ok +test smack::smack::tests::test_wildcard ... ok +test smack::smack::tests::test_proto ... ok test smack::smack::tests::test_pattern ... ok -test result: ok. 36 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out +test result: ok. 92 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.41s ``` #### Functional tests ``` # ./test/test_masscanned.py -tcpdump: listening on tap0, link-type EN10MB (Ethernet), snapshot length 262144 bytes INFO test_arp_req......................................OK INFO test_arp_req_other_ip.............................OK -INFO test_ipv4_req.....................................OK -INFO test_eth_req_other_mac............................OK -INFO test_ipv4_req_other_ip............................OK -INFO test_icmpv4_echo_req..............................OK -INFO test_icmpv6_neighbor_solicitation.................OK -INFO test_icmpv6_neighbor_solicitation_other_ip........OK -INFO test_icmpv6_echo_req..............................OK -INFO test_tcp_syn......................................OK -INFO test_ipv4_tcp_psh_ack.............................OK -INFO test_ipv6_tcp_psh_ack.............................OK +INFO test_ipv4_udp_dns_in_a............................OK +INFO test_ipv4_udp_dns_in_a_multiple_queries...........OK +INFO test_ipv4_tcp_ghost...............................OK INFO test_ipv4_tcp_http................................OK +INFO test_ipv4_tcp_http_segmented......................OK +INFO test_ipv4_tcp_http_incomplete.....................OK INFO test_ipv6_tcp_http................................OK INFO test_ipv4_udp_http................................OK INFO test_ipv6_udp_http................................OK @@ -265,26 +491,64 @@ INFO test_ipv4_tcp_http_ko.............................OK INFO test_ipv4_udp_http_ko.............................OK INFO test_ipv6_tcp_http_ko.............................OK INFO test_ipv6_udp_http_ko.............................OK -INFO test_ipv4_udp_stun................................OK -INFO test_ipv6_udp_stun................................OK -INFO test_ipv4_udp_stun_change_port....................OK -INFO test_ipv6_udp_stun_change_port....................OK +INFO test_icmpv4_echo_req..............................OK +INFO test_icmpv6_neighbor_solicitation.................OK +INFO test_icmpv6_neighbor_solicitation_other_ip........OK +INFO test_icmpv6_echo_req..............................OK +INFO test_ipv4_req.....................................OK +INFO test_eth_req_other_mac............................OK +INFO test_ipv4_req_other_ip............................OK +INFO test_rpc_nmap.....................................OK +INFO test_rpcinfo......................................OK +INFO test_smb1_network_req.............................OK +INFO test_smb2_network_req.............................OK INFO test_ipv4_tcp_ssh.................................OK INFO test_ipv4_udp_ssh.................................OK INFO test_ipv6_tcp_ssh.................................OK INFO test_ipv6_udp_ssh.................................OK -tcpdump: pcap_loop: The interface disappeared -604 packets captured -604 packets received by filter -0 packets dropped by kernel +INFO test_ipv4_udp_stun................................OK +INFO test_ipv6_udp_stun................................OK +INFO test_ipv4_udp_stun_change_port....................OK +INFO test_ipv6_udp_stun_change_port....................OK +INFO test_ipv4_tcp_empty...............................OK +INFO test_ipv6_tcp_empty...............................OK +INFO test_tcp_syn......................................OK +INFO test_ipv4_tcp_psh_ack.............................OK +INFO test_ipv6_tcp_psh_ack.............................OK +INFO test_ipv4_udp_empty...............................OK +INFO test_ipv6_udp_empty...............................OK +INFO Ran 41 tests with 0 errors ``` -### Logging Policy +You can also chose what tests to run using the `TESTS` environment variable +``` +TESTS=smb ./test/test_masscanned.py +INFO test_smb1_network_req.............................OK +INFO test_smb2_network_req.............................OK +INFO Ran 2 tests with 0 errors +``` -* `ERR`: any error - will always be displayed. -* `WARN`, `-v`: responses sent by `masscanned`. -* `INFO`, `-vv`: packets not handled, packets ignored. -* `DEBUG`, `-vvv`: all packets received and sent by `masscanned`. +## Logging + +### Console Logger + +**Verbs**: +* `init` +* `recv` +* `send` +* `drop` + +#### ARP + +``` +$ts arp $verb $operation $client_mac $client_ip $masscanned_mac $masscanned_ip +``` + +#### Ethernet + +``` +$ts eth $verb $ethertype $client_mac $masscanned_mac +``` ## To Do diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 0000000..5128596 --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,19 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/doc/conf.py b/doc/conf.py new file mode 100644 index 0000000..bbedcb5 --- /dev/null +++ b/doc/conf.py @@ -0,0 +1,53 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# http://www.sphinx-doc.org/en/master/config + +from ast import literal_eval +import configparser +import os + +# -- Path setup -------------------------------------------------------------- + +# -- Project information ----------------------------------------------------- + +project = "IVRE" +copyright = "2021, The IVRE project" +html_logo = "img/logo.png" +master_doc = "index" + +def parse_cargo(): + config = configparser.ConfigParser() + config.read(os.path.join("..", "Cargo.toml")) + if "package" not in config: + return None, None, None + package = config["package"] + try: + author = literal_eval(package.get("authors"))[0].split("<", 1)[0].strip() + except KeyError: + authors = None + return literal_eval(package.get("name")), author, literal_eval(package.get("version")) + +project, author, version = parse_cargo() + +# -- General configuration --------------------------------------------------- + +extensions = [] + +autosectionlabel_prefix_document = True + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = "sphinx_rtd_theme" diff --git a/doc/demo.gif b/doc/img/demo.gif similarity index 100% rename from doc/demo.gif rename to doc/img/demo.gif diff --git a/doc/img/logo.png b/doc/img/logo.png new file mode 100644 index 0000000..b9b5199 Binary files /dev/null and b/doc/img/logo.png differ diff --git a/doc/index.rst b/doc/index.rst new file mode 100644 index 0000000..991b3ac --- /dev/null +++ b/doc/index.rst @@ -0,0 +1,35 @@ +Welcome to Masscanned documentation! +==================================== + +Introduction +------------ + +Masscanned is a low-interaction honeypot, primarily designed to help +gather intelligence about network scanners and bots. + +It has been built as a companion tool for `IVRE +`_ but can be used independently. + +The code is on `GitHub `_. + +Here is a quick demo: + +|demo| + +Status of this documentation +---------------------------- + +This documentation is a work in progress! + +Content +------- + +.. toctree:: + :maxdepth: 3 + :caption: Usage: + :glob: + + usage + + +.. |demo| image:: img/demo.gif diff --git a/doc/usage.rst b/doc/usage.rst new file mode 100644 index 0000000..91e2c74 --- /dev/null +++ b/doc/usage.rst @@ -0,0 +1,92 @@ +Using Masscanned +================ + +Dedicated addresses +~~~~~~~~~~~~~~~~~~~ + +Masscanned is designed to handle its own IP addresses, which means +that the host should not have those addresses configured, and +Masscanned will answer ``ARP`` requests (or ``ICMPv6`` ``ND`` neighbor +sollicitations). + +The host may have one or more (``IPv4`` and/or ``IPv6``) addresses configured +on an interface also used by masscanned, but those addresses must be +different from those configured to be used by masscanned. + +In that situation (dedicated addresses), just run: + +:: + + # masscanned -i -f + +where ```` is the path of a text file with one address (``IPv4`` +or ``IPv6``) per line. + +Addresses shared with the host +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sometimes it is desirable to have an IP address used by the host +(*e.g.*, for administration tasks) and by masscanned (to handle all +other incoming packets). + +Since this is not implemented in masscanned, a tiny hack is needed: we +are going to run it on a ``veth`` interface. + +For this example, we suppose: + +- The interface is ``eth0``, the address is ``192.168.0.10``. +- We want masscanned to handle all the traffic except for incoming SSH + connections on TCP/22 port. + +We create a ``veth`` pair of interfaces, on which we are going to use +the 0.255.0.0/31 network (which should not be a problem since +0.0.0.0/8 is reserved as "Current Network"): + +:: + + # ip link add to_masscanned type veth peer masscanned + # ip link set masscanned up + # ip link set to_masscanned up + # ip addr add 0.255.0.0/31 dev to_masscanned + # masscanned -i masscanned + +Masscanned can now be used, but only from the host where it runs: + +:: + + # ping -c 1 0.255.0.1 + PING 0.255.0.1 (0.255.0.1) 56(84) octets de données. + 64 octets de 0.255.0.1 : icmp_seq=1 ttl=64 temps=0.442 ms + + --- statistiques ping 0.255.0.1 --- + 1 paquets transmis, 1 reçus, 0% packet loss, time 0ms + rtt min/avg/max/mdev = 0.442/0.442/0.442/0.000 ms + +Now, we are going to use Netfilter / ``iptables`` to redirect incoming +traffic to masscanned: + +:: + + # sysctl -w net.ipv4.ip_forward=1 + # iptables -t nat -A PREROUTING -i eth0 -d 192.168.0.10 -p tcp --dport 22 -j ACCEPT + # iptables -t nat -A PREROUTING -i eth0 -d 192.168.0.10/32 -j DNAT --to-destination 0.255.0.1 + +And, from another host on the 192.168.0.0/24 network: + +:: + + # ping -c 1 192.168.0.10 + PING 192.168.0.10 (192.168.0.10) 56(84) octets de données. + 64 octets de 192.168.0.10 : icmp_seq=1 ttl=63 temps=0.366 ms + + --- statistiques ping 192.168.0.10 --- + 1 paquets transmis, 1 reçus, 0% packet loss, time 0ms + rtt min/avg/max/mdev = 0.366/0.366/0.366/0.000 ms + + +The masscanned output: + +:: + + WARN - ARP-Reply to ea:c0:d6:20:0c:6a for IP 0.255.0.1 + WARN - ICMP-Echo-Reply to ICMP-Echo-Request diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..bbd46d9 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,45 @@ +# This file is part of masscanned. +# Copyright 2021 - 2024 The IVRE project +# +# Masscanned is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Masscanned is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +# License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Masscanned. If not, see . + +FROM debian:12 AS fetcher + +RUN apt-get -q update && \ + apt-get -qy --no-install-recommends install ca-certificates curl && \ + curl -L https://github.com/ivre/masscanned/archive/refs/heads/master.tar.gz | tar zxf - + + +FROM rust AS builder + +COPY --from=fetcher /masscanned-master /masscanned-master + +RUN cd masscanned-master && \ + cargo build --release + + +FROM debian:12 +LABEL maintainer="Pierre LALET " + +COPY --from=builder /masscanned-master/target/release/masscanned /usr/local/bin/masscanned + +ENV DEBIAN_FRONTEND noninteractive + +RUN apt-get -q update && \ + apt-get -qy --no-install-recommends install iproute2 iptables && \ + apt-get clean && rm -rf /var/lib/apt/lists/* + +COPY runmasscanned /usr/local/bin/runmasscanned + +CMD /usr/local/bin/runmasscanned diff --git a/docker/Dockerfile-local b/docker/Dockerfile-local new file mode 100644 index 0000000..e046e01 --- /dev/null +++ b/docker/Dockerfile-local @@ -0,0 +1,38 @@ +# This file is part of masscanned. +# Copyright 2021 - 2024 The IVRE project +# +# Masscanned is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Masscanned is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +# License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Masscanned. If not, see . + +FROM rust AS builder + +ADD masscanned.tar ./ + +RUN cd masscanned-master && \ + cargo build --release + + +FROM debian:12 +LABEL maintainer="Pierre LALET " + +COPY --from=builder /masscanned-master/target/release/masscanned /usr/local/bin/masscanned + +ENV DEBIAN_FRONTEND noninteractive + +RUN apt-get -q update && \ + apt-get -qy --no-install-recommends install iproute2 iptables && \ + apt-get clean && rm -rf /var/lib/apt/lists/* + +COPY runmasscanned /usr/local/bin/runmasscanned + +CMD /usr/local/bin/runmasscanned diff --git a/docker/runmasscanned b/docker/runmasscanned new file mode 100755 index 0000000..7953cb6 --- /dev/null +++ b/docker/runmasscanned @@ -0,0 +1,35 @@ +#! /bin/bash +# This file is part of masscanned. +# Copyright 2021 - 2023 The IVRE project +# +# Masscanned is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Masscanned is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +# License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Masscanned. If not, see . + +iface="$(ip route get 0.0.0.1 | awk '/^0\.0\.0\.1 via / {print $5}')" +addrs="$(ip a show eth0 | awk '/ inet6? / {print $2}' | sed 's#/.*##' | tr '\n' ',' | sed 's#,$##')" + +if ! capsh --print | awk '/^Current: / {print $2}' | tr ',' '\n' | grep -q '^cap_net_admin$'; then + echo "WARNING: cannot run iptables (need capability cap_net_admin)" >&2 + exit 1 +fi + +for v in '' 6; do + for c in INPUT OUTPUT FORWARD; do + ip${v}tables -P $c DROP + done +done + +echo Interface: "$iface" +echo Addresses: "$addrs" + +/usr/local/bin/masscanned -i "$iface" --self-ip-list "$addrs" diff --git a/src/client/client_info.rs b/src/client/client_info.rs index 3005a8e..a6d33a0 100644 --- a/src/client/client_info.rs +++ b/src/client/client_info.rs @@ -21,7 +21,7 @@ use std::net::IpAddr; use pnet::packet::ip::IpNextHeaderProtocol; use pnet::util::MacAddr; -#[derive(PartialEq, Hash, Copy, Clone)] +#[derive(PartialEq, Hash, Copy, Clone, Debug)] pub struct ClientInfoSrcDst { pub src: Option, pub dst: Option, @@ -35,7 +35,7 @@ pub struct ClientInfoSrcDst { * - source and dest. transport port * - syn cookie **/ -#[derive(Copy, Clone)] +#[derive(Copy, Clone, PartialEq, Debug)] pub struct ClientInfo { pub mac: ClientInfoSrcDst, pub ip: ClientInfoSrcDst, @@ -65,30 +65,6 @@ impl ClientInfo { } } -impl PartialEq for ClientInfo { - fn eq(&self, other: &Self) -> bool { - if self.mac != other.mac { - return false; - } - if self.ip != other.ip { - return false; - } - if self.transport != other.transport { - return false; - } - if self.port != other.port { - return false; - } - /* this next case should never occur with TCP and UDP, - * but this implementation tries to remain transport-protocol-agnostic - **/ - if self.cookie != other.cookie { - return false; - } - true - } -} - impl Eq for ClientInfo {} impl Display for ClientInfo { diff --git a/src/layer_2/arp.rs b/src/layer_2/arp.rs index 4798c22..36ec6fe 100644 --- a/src/layer_2/arp.rs +++ b/src/layer_2/arp.rs @@ -29,20 +29,18 @@ pub fn repl<'a, 'b>( arp_req: &'a ArpPacket, masscanned: &Masscanned, ) -> Option> { + masscanned.log.arp_recv(arp_req); let mut arp_repl = MutableArpPacket::owned(arp_req.packet().to_vec()).expect("error parsing ARP packet"); /* Build ARP answer depending of the type of request */ match arp_req.get_operation() { ArpOperations::Request => { + masscanned.log.arp_recv(arp_req); let ip = IpAddr::V4(arp_req.get_target_proto_addr()); /* Ignore ARP requests for IP addresses not handled by masscanned */ - if let Some(ip_addr_list) = masscanned.ip_addresses { + if let Some(ip_addr_list) = masscanned.self_ip_list { if !ip_addr_list.contains(&ip) { - info!( - "Ignoring ARP request from {} for IP {}", - arp_req.get_sender_hw_addr(), - ip - ); + masscanned.log.arp_drop(arp_req); return None; } } @@ -53,17 +51,15 @@ pub fn repl<'a, 'b>( arp_repl.set_target_hw_addr(arp_req.get_sender_hw_addr().to_owned()); arp_repl.set_target_proto_addr(arp_req.get_sender_proto_addr().to_owned()); arp_repl.set_sender_proto_addr(arp_req.get_target_proto_addr().to_owned()); - warn!( - "ARP-Reply to {} for IP {}", - arp_req.get_sender_hw_addr(), - arp_repl.get_sender_proto_addr() - ); + masscanned.log.arp_send(&arp_repl); } _ => { info!("ARP Operation not handled: {:?}", arp_repl.get_operation()); + masscanned.log.arp_drop(arp_req); return None; } }; + masscanned.log.arp_send(&arp_repl); Some(arp_repl) } @@ -76,6 +72,8 @@ mod tests { use pnet::util::MacAddr; + use crate::logger::MetaLogger; + #[test] fn test_arp_reply() { let mut ips = HashSet::new(); @@ -85,7 +83,9 @@ mod tests { synack_key: [0, 0], mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"), iface: None, - ip_addresses: Some(&ips), + self_ip_list: Some(&ips), + remote_ip_deny_list: None, + log: MetaLogger::new(), }; let mut arp_req = MutableArpPacket::owned([0; 28].to_vec()).expect("error constructing ARP request"); diff --git a/src/layer_2/mod.rs b/src/layer_2/mod.rs index 75d4af8..82f2acb 100644 --- a/src/layer_2/mod.rs +++ b/src/layer_2/mod.rs @@ -104,30 +104,32 @@ pub fn reply<'a, 'b>( masscanned: &Masscanned, mut client_info: &mut ClientInfo, ) -> Option> { - debug!("receiving Ethernet packet: {:?}", eth_req); + /* Fill client information for this packet with MAC addresses (src and dst) */ + client_info.mac.src = Some(eth_req.get_source()); + client_info.mac.dst = Some(eth_req.get_destination()); + masscanned.log.eth_recv(eth_req, &client_info); let mut eth_repl; /* First, check if the destination MAC address is one of those masscanned * is authorized to answer to (avoid answering to packets addressed to * other machines) **/ - if !get_authorized_eth_addr(&masscanned.mac, masscanned.ip_addresses) + if !get_authorized_eth_addr(&masscanned.mac, masscanned.self_ip_list) .contains(ð_req.get_destination()) { - info!( - "Ignoring Ethernet packet from {} to {}", - eth_req.get_source(), - eth_req.get_destination(), - ); + masscanned.log.eth_drop(eth_req, &client_info); return None; } - /* Fill client information for this packet with MAC addresses (src and dst) */ - client_info.mac.src = Some(eth_req.get_source()); - client_info.mac.dst = Some(eth_req.get_destination()); /* Build next layer payload for answer depending on the incoming packet */ match eth_req.get_ethertype() { /* Construct answer to ARP request */ EtherTypes::Arp => { - let arp_req = ArpPacket::new(eth_req.payload()).expect("error parsing ARP packet"); + let arp_req = if let Some(p) = ArpPacket::new(eth_req.payload()) { + p + } else { + warn!("error parsing ARP packet"); + masscanned.log.eth_drop(eth_req, &client_info); + return None; + }; if let Some(arp_repl) = arp::repl(&arp_req, masscanned) { let arp_len = arp_repl.packet().len(); let eth_len = EthernetPacket::minimum_packet_size() + arp_len; @@ -136,6 +138,7 @@ pub fn reply<'a, 'b>( eth_repl.set_ethertype(EtherTypes::Arp); eth_repl.set_payload(arp_repl.packet()); } else { + masscanned.log.eth_drop(eth_req, &client_info); return None; } } @@ -145,6 +148,7 @@ pub fn reply<'a, 'b>( p } else { warn!("error parsing IPv4 packet"); + masscanned.log.eth_drop(eth_req, &client_info); return None; }; if let Some(mut ipv4_repl) = @@ -158,12 +162,19 @@ pub fn reply<'a, 'b>( eth_repl.set_ethertype(EtherTypes::Ipv4); eth_repl.set_payload(ipv4_repl.packet()); } else { + masscanned.log.eth_drop(eth_req, &client_info); return None; } } /* Construct answer to IPv6 packet */ EtherTypes::Ipv6 => { - let ipv6_req = Ipv6Packet::new(eth_req.payload()).expect("error parsing IPv6 packet"); + let ipv6_req = if let Some(p) = Ipv6Packet::new(eth_req.payload()) { + p + } else { + warn!("error parsing IPv6 packet"); + masscanned.log.eth_drop(eth_req, &client_info); + return None; + }; if let Some(ipv6_repl) = layer_3::ipv6::repl(&ipv6_req, masscanned, &mut client_info) { let ipv6_len = ipv6_repl.packet().len(); let eth_len = EthernetPacket::minimum_packet_size() + ipv6_len; @@ -172,18 +183,20 @@ pub fn reply<'a, 'b>( eth_repl.set_ethertype(EtherTypes::Ipv6); eth_repl.set_payload(ipv6_repl.packet()); } else { + masscanned.log.eth_drop(eth_req, &client_info); return None; } } /* Log & drop unknown network protocol */ _ => { info!("Ethernet type not handled: {:?}", eth_req.get_ethertype()); + masscanned.log.eth_drop(eth_req, &client_info); return None; } }; eth_repl.set_source(masscanned.mac); eth_repl.set_destination(eth_req.get_source()); - debug!("sending Ethernet packet: {:?}", eth_repl); + masscanned.log.eth_send(ð_repl, &client_info); Some(eth_repl) } @@ -193,6 +206,46 @@ mod tests { use std::net::{Ipv4Addr, Ipv6Addr}; use std::str::FromStr; + use crate::logger::MetaLogger; + + #[test] + fn test_eth_empty() { + let payload = b""; + let test_mac_addr = + MacAddr::from_str("55:44:33:22:11:00").expect("error parsing MAC address"); + let mac = MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"); + let mut client_info = ClientInfo::new(); + let mut ips = HashSet::new(); + ips.insert(IpAddr::V4(Ipv4Addr::new(0xaa, 0x99, 0x88, 0x77))); + ips.insert(IpAddr::V6(Ipv6Addr::new( + 0x7777, 0x7777, 0x7777, 0x7777, 0x7777, 0x7777, 0xaabb, 0xccdd, + ))); + /* Construct masscanned context object */ + let masscanned = Masscanned { + synack_key: [0, 0], + mac: mac, + iface: None, + self_ip_list: Some(&ips), + remote_ip_deny_list: None, + log: MetaLogger::new(), + }; + for proto in [EtherTypes::Ipv4, EtherTypes::Ipv6, EtherTypes::Arp] { + let mut eth_req = MutableEthernetPacket::owned(vec![ + 0; + EthernetPacket::minimum_packet_size( + ) + payload.len() + ]) + .expect("error constructing ethernet packet"); + eth_req.set_source(test_mac_addr); + eth_req.set_payload(payload); + eth_req.set_ethertype(proto); + eth_req.set_destination(mac); + if let Some(_) = reply(ð_req.to_immutable(), &masscanned, &mut client_info) { + panic!("expected no Ethernet answer, got one"); + } + } + } + #[test] fn test_eth_reply() { /* test payload is IP(src="3.2.1.0", dst=".".join(str(b) for b in [0xaa, 0x99, @@ -211,7 +264,9 @@ mod tests { synack_key: [0, 0], mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"), iface: None, - ip_addresses: Some(&ips), + self_ip_list: Some(&ips), + remote_ip_deny_list: None, + log: MetaLogger::new(), }; let mut eth_req = MutableEthernetPacket::owned(vec![ 0; diff --git a/src/layer_3/ipv4.rs b/src/layer_3/ipv4.rs index 6be97ba..5bff7a9 100644 --- a/src/layer_3/ipv4.rs +++ b/src/layer_3/ipv4.rs @@ -39,31 +39,43 @@ pub fn repl<'a, 'b>( masscanned: &Masscanned, mut client_info: &mut ClientInfo, ) -> Option> { - debug!("receiving IPv4 packet: {:?}", ip_req); + /* Fill client info with source and dest. IP addresses */ + client_info.ip.src = Some(IpAddr::V4(ip_req.get_source())); + client_info.ip.dst = Some(IpAddr::V4(ip_req.get_destination())); + masscanned.log.ipv4_recv(&ip_req, &client_info); /* If masscanned is configured with IP addresses, then * check that the dest. IP address of the packet is one of * those handled by masscanned - otherwise, drop the packet. **/ - if let Some(ip_addr_list) = masscanned.ip_addresses { + if let Some(ip_addr_list) = masscanned.self_ip_list { if !ip_addr_list.contains(&IpAddr::V4(ip_req.get_destination())) { - info!( - "Ignoring IP packet from {} for {}", - ip_req.get_source(), - ip_req.get_destination() - ); + masscanned.log.ipv4_drop(&ip_req, &client_info); + return None; + } + } + /* If masscanned is configured with a remote ip deny list, then + * check if the src. IP address of the packet is one of + * those ignored by masscanned - if so, drop the packet. + **/ + if let Some(remote_ip_deny_list) = masscanned.remote_ip_deny_list { + if remote_ip_deny_list.contains(&IpAddr::V4(ip_req.get_source())) { + masscanned.log.ipv4_drop(&ip_req, &client_info); return None; } } - /* Fill client info with source and dest. IP addresses */ - client_info.ip.src = Some(IpAddr::V4(ip_req.get_source())); - client_info.ip.dst = Some(IpAddr::V4(ip_req.get_destination())); /* Fill client info with transport layer procotol */ client_info.transport = Some(ip_req.get_next_level_protocol()); let mut ip_repl; match ip_req.get_next_level_protocol() { /* Answer to an ICMP packet */ IpNextHeaderProtocols::Icmp => { - let icmp_req = IcmpPacket::new(ip_req.payload()).expect("error parsing ICMP packet"); + let icmp_req = if let Some(p) = IcmpPacket::new(ip_req.payload()) { + p + } else { + warn!("error parsing ICMP packet"); + masscanned.log.ipv4_drop(&ip_req, &client_info); + return None; + }; if let Some(mut icmp_repl) = layer_4::icmpv4::repl(&icmp_req, masscanned, &client_info) { icmp_repl.set_checksum(ipv4_checksum_icmp(&icmp_repl.to_immutable())); @@ -77,12 +89,19 @@ pub fn repl<'a, 'b>( ip_repl.set_payload(icmp_repl.packet()); ip_repl.set_next_level_protocol(IpNextHeaderProtocols::Icmp); } else { + masscanned.log.ipv4_drop(&ip_req, &client_info); return None; } } /* Answer to a TCP packet */ IpNextHeaderProtocols::Tcp => { - let tcp_req = TcpPacket::new(ip_req.payload()).expect("error parsing TCP packet"); + let tcp_req = if let Some(p) = TcpPacket::new(ip_req.payload()) { + p + } else { + warn!("error parsing TCP packet"); + masscanned.log.ipv4_drop(&ip_req, &client_info); + return None; + }; if let Some(mut tcp_repl) = layer_4::tcp::repl(&tcp_req, masscanned, &mut client_info) { tcp_repl.set_checksum(ipv4_checksum_tcp( &tcp_repl.to_immutable(), @@ -99,12 +118,19 @@ pub fn repl<'a, 'b>( ip_repl.set_payload(tcp_repl.packet()); ip_repl.set_next_level_protocol(IpNextHeaderProtocols::Tcp); } else { + masscanned.log.ipv4_drop(&ip_req, &client_info); return None; } } /* Answer to an UDP packet */ IpNextHeaderProtocols::Udp => { - let udp_req = UdpPacket::new(ip_req.payload()).expect("error parsing UDP packet"); + let udp_req = if let Some(p) = UdpPacket::new(ip_req.payload()) { + p + } else { + warn!("error parsing UDP packet"); + masscanned.log.ipv4_drop(&ip_req, &client_info); + return None; + }; if let Some(mut udp_repl) = layer_4::udp::repl(&udp_req, masscanned, &mut client_info) { udp_repl.set_checksum(ipv4_checksum_udp( &udp_repl.to_immutable(), @@ -123,15 +149,13 @@ pub fn repl<'a, 'b>( ip_repl.set_payload(udp_repl.packet()); ip_repl.set_next_level_protocol(IpNextHeaderProtocols::Udp); } else { + masscanned.log.ipv4_drop(&ip_req, &client_info); return None; } } /* Next layer protocol not handled (yet) - dropping packet */ _ => { - info!( - "IPv4 upper layer not handled: {:?}", - ip_req.get_next_level_protocol() - ); + masscanned.log.ipv4_drop(&ip_req, &client_info); return None; } }; @@ -150,7 +174,7 @@ pub fn repl<'a, 'b>( /* FIXME when dest. was a multicast IP address */ ip_repl.set_source(ip_req.get_destination()); ip_repl.set_destination(ip_req.get_source()); - debug!("sending IPv4 packet: {:?}", ip_repl); + masscanned.log.ipv4_send(&ip_repl, &client_info); Some(ip_repl) } @@ -163,10 +187,11 @@ mod tests { use pnet::util::MacAddr; + use crate::logger::MetaLogger; + #[test] - fn test_ipv4_reply() { - /* test payload is scapy> ICMP() */ - let payload = b"\x08\x00\xf7\xff\x00\x00\x00\x00"; + fn test_ipv4_empty() { + let payload = b""; let mut client_info = ClientInfo::new(); let test_ip_addr = Ipv4Addr::new(3, 2, 1, 0); let masscanned_ip_addr = Ipv4Addr::new(0, 1, 2, 3); @@ -177,7 +202,60 @@ mod tests { synack_key: [0, 0], mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"), iface: None, - ip_addresses: Some(&ips), + self_ip_list: Some(&ips), + remote_ip_deny_list: None, + log: MetaLogger::new(), + }; + for proto in [ + IpNextHeaderProtocols::Tcp, + IpNextHeaderProtocols::Udp, + IpNextHeaderProtocols::Icmp, + ] { + let mut ip_req = MutableIpv4Packet::owned(vec![ + 0; + Ipv4Packet::minimum_packet_size() + + payload.len() + ]) + .expect("error constructing IPv4 packet"); + ip_req.set_version(4); + ip_req.set_ttl(64); + ip_req.set_identification(0); + ip_req.set_flags(Ipv4Flags::DontFragment); + ip_req.set_source(test_ip_addr); + ip_req.set_header_length(5); + /* Set test payload for layer 4 */ + ip_req.set_total_length(ip_req.packet().len() as u16); + ip_req.set_payload(payload); + /* Set next protocol */ + ip_req.set_next_level_protocol(proto); + /* Send to a legitimate IP address */ + ip_req.set_destination(masscanned_ip_addr); + if let Some(_) = repl(&ip_req.to_immutable(), &masscanned, &mut client_info) { + panic!("expected no IP answer, got one"); + } + } + } + + #[test] + fn test_ipv4_reply() { + /* test payload is scapy> ICMP() */ + let payload = b"\x08\x00\xf7\xff\x00\x00\x00\x00"; + let mut client_info = ClientInfo::new(); + let test_ip_addr = Ipv4Addr::new(3, 2, 1, 0); + let masscanned_ip_addr = Ipv4Addr::new(0, 1, 2, 3); + let blacklist_ip_addr = Ipv4Addr::new(3, 3, 3, 3); + let mut ips = HashSet::new(); + ips.insert(IpAddr::V4(masscanned_ip_addr)); + let mut blacklist_ips = HashSet::new(); + blacklist_ips.insert(IpAddr::V4(blacklist_ip_addr)); + /* Construct masscanned context object */ + let masscanned = Masscanned { + synack_key: [0, 0], + mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"), + iface: None, + self_ip_list: Some(&ips), + remote_ip_deny_list: Some(&blacklist_ips), + log: MetaLogger::new(), }; let mut ip_req = MutableIpv4Packet::owned(vec![0; Ipv4Packet::minimum_packet_size() + payload.len()]) @@ -206,5 +284,9 @@ mod tests { /* Send to a non-legitimate IP address */ ip_req.set_destination(Ipv4Addr::new(2, 2, 2, 2)); assert!(repl(&ip_req.to_immutable(), &masscanned, &mut client_info) == None); + /* Send from a non-legitimate IP address */ + ip_req.set_source(blacklist_ip_addr); + ip_req.set_destination(masscanned_ip_addr); + assert!(repl(&ip_req.to_immutable(), &masscanned, &mut client_info) == None); } } diff --git a/src/layer_3/ipv6.rs b/src/layer_3/ipv6.rs index edc5390..f482475 100644 --- a/src/layer_3/ipv6.rs +++ b/src/layer_3/ipv6.rs @@ -35,18 +35,31 @@ pub fn repl<'a, 'b>( masscanned: &Masscanned, mut client_info: &mut ClientInfo, ) -> Option> { - debug!("receiving IPv6 packet: {:?}", ip_req); + /* Fill client info with source and dest. IP address */ + client_info.ip.src = Some(IpAddr::V6(ip_req.get_source())); + client_info.ip.dst = Some(IpAddr::V6(ip_req.get_destination())); + masscanned.log.ipv6_recv(ip_req, client_info); let src = ip_req.get_source(); let mut dst = ip_req.get_destination(); - /* If masscanned is configured with IP addresses, check that - * the dest. IP address corresponds to one of those - * Otherwise, drop the packet. + /* If masscanned is configured with IP addresses, then + * check that the dest. IP address of the packet is one of + * those handled by masscanned - otherwise, drop the packet. **/ - if let Some(ip_addr_list) = masscanned.ip_addresses { + if let Some(ip_addr_list) = masscanned.self_ip_list { if !ip_addr_list.contains(&IpAddr::V6(dst)) && ip_req.get_next_header() != IpNextHeaderProtocols::Icmpv6 { - info!("Ignoring IP packet from {} for {}", &src, &dst); + masscanned.log.ipv6_drop(ip_req, client_info); + return None; + } + } + /* If masscanned is configured with a remote ip deny list, then + * check if the src. IP address of the packet is one of + * those ignored by masscanned - if so, drop the packet. + **/ + if let Some(remote_ip_deny_list) = masscanned.remote_ip_deny_list { + if remote_ip_deny_list.contains(&IpAddr::V6(src)) { + masscanned.log.ipv6_drop(ip_req, client_info); return None; } } @@ -59,8 +72,13 @@ pub fn repl<'a, 'b>( match ip_req.get_next_header() { /* Answer to ICMPv6 */ IpNextHeaderProtocols::Icmpv6 => { - let icmp_req = - Icmpv6Packet::new(ip_req.payload()).expect("error parsing ICMPv6 packet"); + let icmp_req = if let Some(p) = Icmpv6Packet::new(ip_req.payload()) { + p + } else { + warn!("error parsing ICMPv6 packet"); + masscanned.log.ipv6_drop(&ip_req, &client_info); + return None; + }; if let (Some(mut icmp_repl), dst_addr) = layer_4::icmpv6::repl(&icmp_req, masscanned, &client_info) { @@ -84,12 +102,19 @@ pub fn repl<'a, 'b>( ip_repl.set_hop_limit(255); }; } else { + masscanned.log.ipv6_drop(ip_req, client_info); return None; } } /* Answer to TCP */ IpNextHeaderProtocols::Tcp => { - let tcp_req = TcpPacket::new(ip_req.payload()).expect("error parsing TCP packet"); + let tcp_req = if let Some(p) = TcpPacket::new(ip_req.payload()) { + p + } else { + warn!("error parsing TCP packet"); + masscanned.log.ipv6_drop(&ip_req, &client_info); + return None; + }; if let Some(mut tcp_repl) = layer_4::tcp::repl(&tcp_req, masscanned, &mut client_info) { /* Compute and set TCP checksum */ tcp_repl.set_checksum(ipv6_checksum_tcp( @@ -108,12 +133,19 @@ pub fn repl<'a, 'b>( ip_repl.set_payload_length(tcp_len as u16); ip_repl.set_payload(&tcp_repl.packet()); } else { + masscanned.log.ipv6_drop(ip_req, client_info); return None; } } /* Answer to UDP */ IpNextHeaderProtocols::Udp => { - let udp_req = UdpPacket::new(ip_req.payload()).expect("error parsing UDP packet"); + let udp_req = if let Some(p) = UdpPacket::new(ip_req.payload()) { + p + } else { + warn!("error parsing UDP packet"); + masscanned.log.ipv6_drop(&ip_req, &client_info); + return None; + }; if let Some(mut udp_repl) = layer_4::udp::repl(&udp_req, masscanned, &mut client_info) { /* Compute and set UDP checksum */ udp_repl.set_checksum(ipv6_checksum_udp( @@ -132,15 +164,13 @@ pub fn repl<'a, 'b>( ip_repl.set_payload_length(udp_len as u16); ip_repl.set_payload(&udp_repl.packet()); } else { + masscanned.log.ipv6_drop(ip_req, client_info); return None; } } /* Other protocols are not handled (yet) - dropping */ _ => { - info!( - "IPv6 upper layer not handled: {:?}", - ip_req.get_next_header() - ); + masscanned.log.ipv6_drop(ip_req, client_info); return None; } }; @@ -153,7 +183,7 @@ pub fn repl<'a, 'b>( /* Set packet source and dest. */ ip_repl.set_source(dst); ip_repl.set_destination(src); - debug!("sending IPv6 packet: {:?}", ip_repl); + masscanned.log.ipv6_send(&ip_repl, client_info); Some(ip_repl) } @@ -166,12 +196,11 @@ mod tests { use pnet::util::MacAddr; + use crate::logger::MetaLogger; + #[test] - fn test_ipv6_reply() { - /* test payload is scapy> IPv6(src="7777:6666:5555:4444:3333:2222:1111:0000", - * dst="0000:1111:2222:3333:4444:5555:6666:7777")/TCP(sport=12345, dport=54321, - * flags="S"))[TCP] */ - let payload = b"09\xd41\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00\xcf\xbc\x00\x00"; + fn test_ipv6_empty() { + let payload = b""; let mut client_info = ClientInfo::new(); let test_ip_addr = Ipv6Addr::new( 0x7777, 0x6666, 0x5555, 0x4444, 0x3333, 0x2222, 0x1111, 0x0000, @@ -186,7 +215,64 @@ mod tests { synack_key: [0, 0], mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"), iface: None, - ip_addresses: Some(&ips), + self_ip_list: Some(&ips), + remote_ip_deny_list: None, + log: MetaLogger::new(), + }; + for proto in [ + IpNextHeaderProtocols::Tcp, + IpNextHeaderProtocols::Udp, + IpNextHeaderProtocols::Icmp, + ] { + let mut ip_req = MutableIpv6Packet::owned(vec![ + 0; + Ipv6Packet::minimum_packet_size() + + payload.len() + ]) + .expect("error constructing IPv6 packet"); + ip_req.set_version(6); + ip_req.set_source(test_ip_addr); + /* Set test payload for layer 4 */ + ip_req.set_payload_length(payload.len() as u16); + ip_req.set_payload(payload); + /* Set next protocol */ + ip_req.set_next_header(proto); + /* Send to a legitimate IP address */ + ip_req.set_destination(masscanned_ip_addr); + if let Some(_) = repl(&ip_req.to_immutable(), &masscanned, &mut client_info) { + panic!("expected no IP answer, got one"); + } + } + } + + #[test] + fn test_ipv6_reply() { + /* test payload is scapy> IPv6(src="7777:6666:5555:4444:3333:2222:1111:0000", + * dst="0000:1111:2222:3333:4444:5555:6666:7777")/TCP(sport=12345, dport=54321, + * flags="S"))[TCP] */ + let payload = b"09\xd41\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00\xcf\xbc\x00\x00"; + let mut client_info = ClientInfo::new(); + let test_ip_addr = Ipv6Addr::new( + 0x7777, 0x6666, 0x5555, 0x4444, 0x3333, 0x2222, 0x1111, 0x0000, + ); + let masscanned_ip_addr = Ipv6Addr::new( + 0x0000, 0x1111, 0x2222, 0x3333, 0x4444, 0x5555, 0x6666, 0x7777, + ); + let blacklist_ip_addr = Ipv6Addr::new( + 0x1111, 0x1111, 0x1111, 0x1111, 0x1111, 0x1111, 0x1111, 0x1111, + ); + let mut ips = HashSet::new(); + ips.insert(IpAddr::V6(masscanned_ip_addr)); + let mut blacklist_ips = HashSet::new(); + blacklist_ips.insert(IpAddr::V6(blacklist_ip_addr)); + /* Construct masscanned context object */ + let masscanned = Masscanned { + synack_key: [0, 0], + mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"), + iface: None, + self_ip_list: Some(&ips), + remote_ip_deny_list: Some(&blacklist_ips), + log: MetaLogger::new(), }; let mut ip_req = MutableIpv6Packet::owned(vec![0; Ipv6Packet::minimum_packet_size() + payload.len()]) @@ -213,5 +299,9 @@ mod tests { 0x0000, 0x1111, 0x2222, 0x3333, 0x4444, 0x5555, 0x6666, 0x7778, )); assert!(repl(&ip_req.to_immutable(), &masscanned, &mut client_info) == None); + /* Send from a non-legitimate IP address */ + ip_req.set_source(blacklist_ip_addr); + ip_req.set_destination(masscanned_ip_addr); + assert!(repl(&ip_req.to_immutable(), &masscanned, &mut client_info) == None); } } diff --git a/src/layer_4/icmpv4.rs b/src/layer_4/icmpv4.rs index 95cdf7c..becef8d 100644 --- a/src/layer_4/icmpv4.rs +++ b/src/layer_4/icmpv4.rs @@ -14,8 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Masscanned. If not, see . -use log::*; - use pnet::packet::{ icmp::{IcmpCode, IcmpPacket, IcmpTypes, MutableIcmpPacket}, Packet, @@ -26,16 +24,16 @@ use crate::Masscanned; pub fn repl<'a, 'b>( icmp_req: &'a IcmpPacket, - _masscanned: &Masscanned, - mut _client_info: &ClientInfo, + masscanned: &Masscanned, + client_info: &ClientInfo, ) -> Option> { - debug!("receiving ICMPv4 packet: {:?}", icmp_req); + masscanned.log.icmpv4_recv(icmp_req, client_info); let mut icmp_repl; match icmp_req.get_icmp_type() { IcmpTypes::EchoRequest => { /* Check code of ICMP packet */ if icmp_req.get_icmp_code() != IcmpCode(0) { - info!("ICMP code not handled: {:?}", icmp_req.get_icmp_code()); + masscanned.log.icmpv4_drop(icmp_req, client_info); return None; } /* Compute answer length */ @@ -53,13 +51,13 @@ pub fn repl<'a, 'b>( * reply message." **/ icmp_repl.set_payload(icmp_req.payload()); - warn!("ICMP-Echo-Reply to ICMP-Echo-Request"); } _ => { + masscanned.log.icmpv4_drop(icmp_req, client_info); return None; } }; - debug!("sending ICMPv4 packet: {:?}", icmp_repl); + masscanned.log.icmpv4_send(&icmp_repl, client_info); Some(icmp_repl) } @@ -70,6 +68,8 @@ mod tests { use pnet::util::MacAddr; + use crate::logger::MetaLogger; + #[test] fn test_icmpv4_reply() { /* test payload is scapy> ICMP() */ @@ -80,7 +80,9 @@ mod tests { synack_key: [0, 0], mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"), iface: None, - ip_addresses: None, + self_ip_list: None, + remote_ip_deny_list: None, + log: MetaLogger::new(), }; let mut icmp_req = MutableIcmpPacket::owned(vec![0; IcmpPacket::minimum_packet_size() + payload.len()]) diff --git a/src/layer_4/icmpv6.rs b/src/layer_4/icmpv6.rs index 6db03d2..00c53f4 100644 --- a/src/layer_4/icmpv6.rs +++ b/src/layer_4/icmpv6.rs @@ -40,7 +40,7 @@ pub fn nd_ns_repl<'a, 'b>( * check that the dest. IP address of the packet is one of * those handled by masscanned - otherwise, drop the packet. **/ - if let Some(addresses) = masscanned.ip_addresses { + if let Some(addresses) = masscanned.self_ip_list { if !addresses.contains(&IpAddr::V6(nd_ns_req.get_target_addr())) { return None; } @@ -103,7 +103,7 @@ pub fn repl<'a, 'b>( masscanned: &Masscanned, client_info: &ClientInfo, ) -> (Option>, Option) { - debug!("receiving ICMPv6 packet: {:?}", icmp_req); + masscanned.log.icmpv6_recv(icmp_req, client_info); let mut dst_ip = None; if icmp_req.get_icmpv6_code() != Icmpv6Codes::NoCode { return (None, None); @@ -120,6 +120,7 @@ pub fn repl<'a, 'b>( icmp_repl = MutableIcmpv6Packet::owned(nd_na_repl.packet().to_vec()) .expect("error constructing an ICMPv6 packet"); } else { + masscanned.log.icmpv6_drop(icmp_req, client_info); return (None, None); } } @@ -136,17 +137,13 @@ pub fn repl<'a, 'b>( icmp_repl = MutableIcmpv6Packet::owned(vec![0; Icmpv6Packet::packet_size(&echo_repl)]) .expect("error constructing an ICMPv6 packet"); icmp_repl.populate(&echo_repl); - warn!("ICMPv6-Echo-Reply to ICMPv6-Echo-Request"); } _ => { - info!( - "ICMPv6 packet not handled: {:?}", - icmp_req.get_icmpv6_type() - ); + masscanned.log.icmpv6_drop(icmp_req, client_info); return (None, None); } }; - debug!("sending ICMPv6 packet: {:?}", icmp_repl); + masscanned.log.icmpv6_send(&icmp_repl, client_info); (Some(icmp_repl), dst_ip) } @@ -160,6 +157,8 @@ mod tests { use pnet::packet::icmpv6::ndp::{MutableNeighborSolicitPacket, NeighborSolicit}; use pnet::util::MacAddr; + use crate::logger::MetaLogger; + #[test] fn test_nd_na_reply() { let client_info = ClientInfo::new(); @@ -173,7 +172,9 @@ mod tests { synack_key: [0, 0], mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"), iface: None, - ip_addresses: Some(&ips), + self_ip_list: Some(&ips), + remote_ip_deny_list: None, + log: MetaLogger::new(), }; /* Legitimate solicitation */ let ndp_ns = NeighborSolicit { @@ -245,7 +246,9 @@ mod tests { synack_key: [0, 0], mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"), iface: None, - ip_addresses: Some(&ips), + self_ip_list: Some(&ips), + remote_ip_deny_list: None, + log: MetaLogger::new(), }; let mut icmpv6_echo_req = MutableIcmpv6Packet::owned(vec![ 0; diff --git a/src/layer_4/tcp.rs b/src/layer_4/tcp.rs index d93585f..82ad0c2 100644 --- a/src/layer_4/tcp.rs +++ b/src/layer_4/tcp.rs @@ -31,10 +31,10 @@ pub fn repl<'a, 'b>( masscanned: &Masscanned, mut client_info: &mut ClientInfo, ) -> Option> { - debug!("receiving TCP packet: {:?}", tcp_req); /* Fill client info with source and dest. TCP port */ client_info.port.src = Some(tcp_req.get_source()); client_info.port.dst = Some(tcp_req.get_destination()); + masscanned.log.tcp_recv(tcp_req, client_info); /* Construct response TCP packet */ let mut tcp_repl; match tcp_req.get_flags() { @@ -49,16 +49,24 @@ pub fn repl<'a, 'b>( }; /* Compute syncookie */ if let Ok(cookie) = synackcookie::generate(&client_info, &masscanned.synack_key) { - if cookie != ackno { - info!("PSH-ACK ignored: synackcookie not valid"); - return None; - } client_info.cookie = Some(cookie); + if !proto::is_tcb_set(cookie) { + /* First Ack: check syncookie, create tcb */ + if cookie != ackno { + masscanned.log.tcp_drop(tcp_req, client_info); + return None; + } + proto::add_tcb(cookie); + } } warn!("ACK to PSH-ACK on port {}", tcp_req.get_destination()); let payload = tcp_req.payload(); /* Any answer to upper-layer protocol? */ - if let Some(repl) = proto::repl(&payload, masscanned, &mut client_info) { + let mut payload_repl = None; + proto::get_tcb(client_info.cookie.unwrap(), |tcb| { + payload_repl = proto::repl(&payload, masscanned, &mut client_info, tcb); + }); + if let Some(repl) = payload_repl { tcp_repl = MutableTcpPacket::owned( [vec![0; MutableTcpPacket::minimum_packet_size()], repl].concat(), ) @@ -70,33 +78,52 @@ pub fn repl<'a, 'b>( .expect("error constructing a TCP packet"); tcp_repl.set_flags(TcpFlags::ACK); } - tcp_repl.set_acknowledgement(tcp_req.get_sequence() + (tcp_req.payload().len() as u32)); + tcp_repl.set_acknowledgement( + tcp_req + .get_sequence() + .wrapping_add(tcp_req.payload().len() as u32), + ); tcp_repl.set_sequence(tcp_req.get_acknowledgement()); } /* Answer to ACK: nothing */ flags if flags == TcpFlags::ACK => { /* answer here when server needs to speak first after handshake */ + masscanned.log.tcp_drop(tcp_req, client_info); return None; } - /* Answer to RST and FIN: nothing */ - flags if (flags == TcpFlags::RST || flags == (TcpFlags::FIN | TcpFlags::ACK)) => { + /* Answer to RST: nothing */ + flags if flags == TcpFlags::RST => { + masscanned.log.tcp_drop(tcp_req, client_info); return None; } - /* Answer to SYN */ - flags if flags & TcpFlags::SYN == TcpFlags::SYN => { + /* Answer to FIN,ACK with FIN,ACK */ + flags if flags == (TcpFlags::FIN | TcpFlags::ACK) => { + tcp_repl = MutableTcpPacket::owned(vec![0; MutableTcpPacket::minimum_packet_size()]) + .expect("error constructing a TCP packet"); + tcp_repl.set_flags(TcpFlags::FIN | TcpFlags::ACK); + tcp_repl.set_acknowledgement(tcp_req.get_sequence().wrapping_add(1)); + tcp_repl.set_sequence(tcp_req.get_acknowledgement()); + } + /* Answer to SYN + P|U|C|E + !(C && E) to imitate Linux network stack */ + flags + if (flags & TcpFlags::SYN) == TcpFlags::SYN && + /* no other flag than S,P,U,C,E */ + (flags & !(TcpFlags::SYN | TcpFlags::PSH | TcpFlags::URG | TcpFlags::CWR | TcpFlags::ECE)) == 0 && + /* not C && E */ + ((flags & TcpFlags::CWR == 0) || (flags & TcpFlags::ECE == 0)) => + { tcp_repl = MutableTcpPacket::owned(vec![0; MutableTcpPacket::minimum_packet_size()]) .expect("error constructing a TCP packet"); tcp_repl.set_flags(TcpFlags::ACK); tcp_repl.set_flags(TcpFlags::SYN | TcpFlags::ACK); - tcp_repl.set_acknowledgement(tcp_req.get_sequence() + 1); + tcp_repl.set_acknowledgement(tcp_req.get_sequence().wrapping_add(1)); /* generate a SYNACK-cookie (same as masscan) */ tcp_repl.set_sequence( synackcookie::generate(&client_info, &masscanned.synack_key).unwrap(), ); - warn!("SYN-ACK to ACK on port {}", tcp_req.get_destination()); } _ => { - info!("TCP flag not handled: {}", tcp_req.get_flags()); + masscanned.log.tcp_drop(tcp_req, client_info); return None; } } @@ -107,7 +134,7 @@ pub fn repl<'a, 'b>( /* Set TCP headers */ tcp_repl.set_data_offset(5); tcp_repl.set_window(65535); - debug!("sending TCP packet: {:?}", tcp_repl); + masscanned.log.tcp_send(&tcp_repl, client_info); Some(tcp_repl) } @@ -118,13 +145,212 @@ mod tests { use pnet::util::MacAddr; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + use crate::logger::MetaLogger; + + #[test] + fn test_tcp_syn() { + let masscanned = Masscanned { + mac: MacAddr(0, 0, 0, 0, 0, 0), + self_ip_list: None, + remote_ip_deny_list: None, + synack_key: [0x06a0a1d63f305e9b, 0xd4d4bcbb7304875f], + iface: None, + log: MetaLogger::new(), + }; + /* reference */ + let ip_src = IpAddr::V4(Ipv4Addr::new(27, 198, 143, 1)); + let ip_dst = IpAddr::V4(Ipv4Addr::new(90, 64, 122, 203)); + let tcp_sport = 65500; + let tcp_dport = 80; + let seq = 1234567; + let ack = 0; + let mut client_info = ClientInfo { + mac: ClientInfoSrcDst { + src: None, + dst: None, + }, + ip: ClientInfoSrcDst { + src: Some(ip_src), + dst: Some(ip_dst), + }, + transport: None, + port: ClientInfoSrcDst { + src: Some(tcp_sport), + dst: Some(tcp_dport), + }, + cookie: None, + }; + /* flags OK - list is exhaustive */ + /* aim at imitating a Linux network stack */ + let flags_ok = [ + TcpFlags::SYN, + TcpFlags::SYN | TcpFlags::PSH, + TcpFlags::SYN | TcpFlags::URG, + TcpFlags::SYN | TcpFlags::CWR, + TcpFlags::SYN | TcpFlags::ECE, + TcpFlags::SYN | TcpFlags::PSH | TcpFlags::URG, + TcpFlags::SYN | TcpFlags::PSH | TcpFlags::CWR, + TcpFlags::SYN | TcpFlags::PSH | TcpFlags::ECE, + TcpFlags::SYN | TcpFlags::PSH | TcpFlags::ECE, + TcpFlags::SYN | TcpFlags::URG | TcpFlags::CWR, + TcpFlags::SYN | TcpFlags::URG | TcpFlags::ECE, + TcpFlags::SYN | TcpFlags::PSH | TcpFlags::URG | TcpFlags::CWR, + TcpFlags::SYN | TcpFlags::PSH | TcpFlags::URG | TcpFlags::ECE, + ]; + for flags in flags_ok { + let mut tcp_req = + MutableTcpPacket::owned(vec![0; MutableTcpPacket::minimum_packet_size()]).unwrap(); + tcp_req.set_source(tcp_sport); + tcp_req.set_destination(tcp_dport); + tcp_req.set_sequence(seq); + tcp_req.set_acknowledgement(ack); + tcp_req.set_flags(flags); + let some_tcp_repl = repl(&tcp_req.to_immutable(), &masscanned, &mut client_info); + if some_tcp_repl == None { + panic!("expected a reply, got none for flags: {:?}", flags); + } + let tcp_repl = some_tcp_repl.unwrap(); + /* check reply flags */ + assert!(tcp_repl.get_flags() == (TcpFlags::SYN | TcpFlags::ACK)); + /* check reply seq and ack */ + assert!(tcp_repl.get_acknowledgement() == seq.wrapping_add(1)); + } + /* flags KO - list is *not* exhaustive */ + let flags_ko = [ + TcpFlags::SYN | TcpFlags::ACK, + TcpFlags::SYN | TcpFlags::FIN, + TcpFlags::SYN | TcpFlags::CWR | TcpFlags::ECE, + TcpFlags::SYN | TcpFlags::PSH | TcpFlags::URG | TcpFlags::CWR | TcpFlags::ECE, + TcpFlags::PSH, + ]; + for flags in flags_ko { + let mut tcp_req = + MutableTcpPacket::owned(vec![0; MutableTcpPacket::minimum_packet_size()]).unwrap(); + tcp_req.set_source(tcp_sport); + tcp_req.set_destination(tcp_dport); + tcp_req.set_sequence(seq); + tcp_req.set_acknowledgement(ack); + tcp_req.set_flags(flags); + let some_tcp_repl = repl(&tcp_req.to_immutable(), &masscanned, &mut client_info); + if some_tcp_repl != None { + panic!("expected no reply, got one"); + } + } + } + + #[test] + fn test_tcp_fin_ack() { + let masscanned = Masscanned { + mac: MacAddr(0, 0, 0, 0, 0, 0), + self_ip_list: None, + remote_ip_deny_list: None, + synack_key: [0x06a0a1d63f305e9b, 0xd4d4bcbb7304875f], + iface: None, + log: MetaLogger::new(), + }; + /* reference */ + let ip_src = IpAddr::V4(Ipv4Addr::new(27, 198, 143, 1)); + let ip_dst = IpAddr::V4(Ipv4Addr::new(90, 64, 122, 203)); + let tcp_sport = 65500; + let tcp_dport = 80; + let seq = 1234567; + let ack = 7654321; + let mut client_info = ClientInfo { + mac: ClientInfoSrcDst { + src: None, + dst: None, + }, + ip: ClientInfoSrcDst { + src: Some(ip_src), + dst: Some(ip_dst), + }, + transport: None, + port: ClientInfoSrcDst { + src: Some(tcp_sport), + dst: Some(tcp_dport), + }, + cookie: None, + }; + let mut tcp_req = + MutableTcpPacket::owned(vec![0; MutableTcpPacket::minimum_packet_size()]).unwrap(); + tcp_req.set_source(tcp_sport); + tcp_req.set_destination(tcp_dport); + tcp_req.set_sequence(seq); + tcp_req.set_acknowledgement(ack); + tcp_req.set_flags(TcpFlags::FIN | TcpFlags::ACK); + let some_tcp_repl = repl(&tcp_req.to_immutable(), &masscanned, &mut client_info); + if some_tcp_repl == None { + panic!("expected a reply, got none"); + } + let tcp_repl = some_tcp_repl.unwrap(); + /* check reply flags */ + assert!(tcp_repl.get_flags() == (TcpFlags::FIN | TcpFlags::ACK)); + /* check reply seq and ack */ + assert!(tcp_repl.get_sequence() == ack); + assert!(tcp_repl.get_acknowledgement() == seq.wrapping_add(1)); + } + + #[test] + fn test_tcp_fin_ack_wrap() { + let masscanned = Masscanned { + mac: MacAddr(0, 0, 0, 0, 0, 0), + self_ip_list: None, + remote_ip_deny_list: None, + synack_key: [0x06a0a1d63f305e9b, 0xd4d4bcbb7304875f], + iface: None, + log: MetaLogger::new(), + }; + /* reference */ + let ip_src = IpAddr::V4(Ipv4Addr::new(27, 198, 143, 1)); + let ip_dst = IpAddr::V4(Ipv4Addr::new(90, 64, 122, 203)); + let tcp_sport = 65500; + let tcp_dport = 80; + let seq = 0xffffffff; + let ack = 0xffffffff; + let mut client_info = ClientInfo { + mac: ClientInfoSrcDst { + src: None, + dst: None, + }, + ip: ClientInfoSrcDst { + src: Some(ip_src), + dst: Some(ip_dst), + }, + transport: None, + port: ClientInfoSrcDst { + src: Some(tcp_sport), + dst: Some(tcp_dport), + }, + cookie: None, + }; + let mut tcp_req = + MutableTcpPacket::owned(vec![0; MutableTcpPacket::minimum_packet_size()]).unwrap(); + tcp_req.set_source(tcp_sport); + tcp_req.set_destination(tcp_dport); + tcp_req.set_sequence(seq); + tcp_req.set_acknowledgement(ack); + tcp_req.set_flags(TcpFlags::FIN | TcpFlags::ACK); + let some_tcp_repl = repl(&tcp_req.to_immutable(), &masscanned, &mut client_info); + if some_tcp_repl == None { + panic!("expected a reply, got none"); + } + let tcp_repl = some_tcp_repl.unwrap(); + /* check reply flags */ + assert!(tcp_repl.get_flags() == (TcpFlags::FIN | TcpFlags::ACK)); + /* check reply seq and ack */ + assert!(tcp_repl.get_sequence() == ack); + assert!(tcp_repl.get_acknowledgement() == seq.wrapping_add(1)); + } + #[test] fn test_synack_cookie_ipv4() { let masscanned = Masscanned { mac: MacAddr(0, 0, 0, 0, 0, 0), - ip_addresses: None, + self_ip_list: None, + remote_ip_deny_list: None, synack_key: [0x06a0a1d63f305e9b, 0xd4d4bcbb7304875f], iface: None, + log: MetaLogger::new(), }; /* reference */ let ip_src = IpAddr::V4(Ipv4Addr::new(27, 198, 143, 1)); @@ -171,9 +397,11 @@ mod tests { fn test_synack_cookie_ipv6() { let masscanned = Masscanned { mac: MacAddr(0, 0, 0, 0, 0, 0), - ip_addresses: None, + self_ip_list: None, + remote_ip_deny_list: None, synack_key: [0x06a0a1d63f305e9b, 0xd4d4bcbb7304875f], iface: None, + log: MetaLogger::new(), }; /* reference */ let ip_src = IpAddr::V6(Ipv6Addr::new(234, 52, 183, 47, 184, 172, 64, 141)); diff --git a/src/layer_4/udp.rs b/src/layer_4/udp.rs index cdc1d47..57d147f 100644 --- a/src/layer_4/udp.rs +++ b/src/layer_4/udp.rs @@ -14,8 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Masscanned. If not, see . -use log::*; - use pnet::packet::{ udp::{MutableUdpPacket, UdpPacket}, Packet, @@ -30,25 +28,26 @@ pub fn repl<'a, 'b>( masscanned: &Masscanned, mut client_info: &mut ClientInfo, ) -> Option> { - debug!("receiving UDP packet: {:?}", udp_req); /* Fill client info with source and dest. UDP port */ client_info.port.src = Some(udp_req.get_source()); client_info.port.dst = Some(udp_req.get_destination()); + masscanned.log.udp_recv(udp_req, client_info); let payload = udp_req.payload(); let mut udp_repl; - if let Some(repl) = proto::repl(&payload, masscanned, &mut client_info) { + if let Some(repl) = proto::repl(&payload, masscanned, &mut client_info, None) { udp_repl = MutableUdpPacket::owned( [vec![0; MutableUdpPacket::minimum_packet_size()], repl].concat(), ) .expect("error constructing a UDP packet"); udp_repl.set_length(udp_repl.packet().len() as u16); } else { + masscanned.log.udp_drop(udp_req, client_info); return None; } /* Set source and dest. port for response packet from client info */ /* Note: client info could have been modified by upper layers (e.g., STUN) */ udp_repl.set_source(client_info.port.dst.unwrap()); udp_repl.set_destination(client_info.port.src.unwrap()); - debug!("sending UDP packet: {:?}", udp_repl); + masscanned.log.udp_send(&udp_repl, client_info); Some(udp_repl) } diff --git a/src/logger/console.rs b/src/logger/console.rs new file mode 100644 index 0000000..fe7f7ec --- /dev/null +++ b/src/logger/console.rs @@ -0,0 +1,308 @@ +// This file is part of masscanned. +// Copyright 2021 - The IVRE project +// +// Masscanned is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Masscanned is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +// License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Masscanned. If not, see . + +use std::time::SystemTime; + +use pnet::packet::{ + arp::{ArpPacket, MutableArpPacket}, + ethernet::{EthernetPacket, MutableEthernetPacket}, + icmp::{IcmpPacket, MutableIcmpPacket}, + icmpv6::{Icmpv6Packet, MutableIcmpv6Packet}, + ipv4::{Ipv4Packet, MutableIpv4Packet}, + ipv6::{Ipv6Packet, MutableIpv6Packet}, + tcp::{MutableTcpPacket, TcpPacket}, + udp::{MutableUdpPacket, UdpPacket}, +}; + +use crate::client::ClientInfo; +use crate::logger::Logger; + +pub struct ConsoleLogger { + arp: bool, + eth: bool, + ipv4: bool, + ipv6: bool, + icmpv4: bool, + icmpv6: bool, + tcp: bool, + udp: bool, +} + +impl ConsoleLogger { + pub fn new() -> Self { + ConsoleLogger { + arp: true, + eth: true, + ipv4: true, + ipv6: true, + icmpv4: true, + icmpv6: true, + tcp: true, + udp: true, + } + } + fn prolog(&self, proto: &str, verb: &str, crlf: bool) { + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(); + print!( + "{}.{}\t{}\t{}{}", + now.as_secs(), + now.subsec_millis(), + proto, + verb, + if crlf { "\n" } else { "\t" }, + ); + } + fn client_info(&self, c: &ClientInfo) { + print!( + "{}\t{}\t{}\t{}\t{}\t{}\t{}\t", + if let Some(m) = c.mac.src { + format!("{}", m) + } else { + "".to_string() + }, + if let Some(m) = c.mac.dst { + format!("{}", m) + } else { + "".to_string() + }, + if let Some(i) = c.ip.src { + format!("{}", i) + } else { + "".to_string() + }, + if let Some(i) = c.ip.dst { + format!("{}", i) + } else { + "".to_string() + }, + if let Some(t) = c.transport { + format!("{}", t) + } else { + "".to_string() + }, + if let Some(p) = c.port.src { + format!("{}", p) + } else { + "".to_string() + }, + if let Some(p) = c.port.dst { + format!("{}", p) + } else { + "".to_string() + }, + ); + } +} + +impl Logger for ConsoleLogger { + fn init(&self) { + self.prolog("arp", "init", true); + self.prolog("eth", "init", true); + self.prolog("ipv4", "init", true); + self.prolog("ipv6", "init", true); + self.prolog("icmpv4", "init", true); + self.prolog("icmpv6", "init", true); + self.prolog("tcp", "init", true); + self.prolog("udp", "init", true); + } + /* ARP */ + fn arp_enabled(&self) -> bool { + self.arp + } + fn arp_recv(&self, p: &ArpPacket) { + self.prolog("arp", "recv", false); + println!( + "{:}\t{:}\t{:}\t{:}\t{:?}", + p.get_sender_hw_addr(), + p.get_target_hw_addr(), + p.get_sender_proto_addr(), + p.get_target_proto_addr(), + p.get_operation(), + ); + } + fn arp_drop(&self, p: &ArpPacket) { + self.prolog("arp", "drop", false); + println!( + "{:}\t{:}\t{:}\t{:}\t{:?}", + p.get_sender_hw_addr(), + p.get_target_hw_addr(), + p.get_sender_proto_addr(), + p.get_target_proto_addr(), + p.get_operation(), + ); + } + fn arp_send(&self, p: &MutableArpPacket) { + self.prolog("arp", "send", false); + println!( + "{:}\t{:}\t{:}\t{:}\t{:?}", + p.get_target_hw_addr(), + p.get_sender_hw_addr(), + p.get_target_proto_addr(), + p.get_sender_proto_addr(), + p.get_operation(), + ); + } + /* Ethernet */ + fn eth_enabled(&self) -> bool { + self.eth + } + fn eth_recv(&self, p: &EthernetPacket, c: &ClientInfo) { + self.prolog("eth", "recv", false); + self.client_info(c); + println!("{:}", p.get_ethertype(),); + } + fn eth_drop(&self, p: &EthernetPacket, c: &ClientInfo) { + self.prolog("eth", "drop", false); + self.client_info(c); + println!("{:}", p.get_ethertype(),); + } + fn eth_send(&self, p: &MutableEthernetPacket, c: &ClientInfo) { + self.prolog("eth", "send", false); + self.client_info(c); + println!("{:}", p.get_ethertype(),); + } + /* IPv4 */ + fn ipv4_enabled(&self) -> bool { + self.ipv4 + } + fn ipv4_recv(&self, p: &Ipv4Packet, c: &ClientInfo) { + self.prolog("ipv4", "recv", false); + self.client_info(c); + println!("{:}", p.get_next_level_protocol(),); + } + fn ipv4_drop(&self, p: &Ipv4Packet, c: &ClientInfo) { + self.prolog("ipv4", "drop", false); + self.client_info(c); + println!("{:}", p.get_next_level_protocol(),); + } + fn ipv4_send(&self, p: &MutableIpv4Packet, c: &ClientInfo) { + self.prolog("ipv4", "send", false); + self.client_info(c); + println!("{:}", p.get_next_level_protocol(),); + } + /* IPv6 */ + fn ipv6_enabled(&self) -> bool { + self.ipv6 + } + fn ipv6_recv(&self, p: &Ipv6Packet, c: &ClientInfo) { + self.prolog("ipv6", "recv", false); + self.client_info(c); + println!("{:}", p.get_next_header(),); + } + fn ipv6_drop(&self, p: &Ipv6Packet, c: &ClientInfo) { + self.prolog("ipv6", "drop", false); + self.client_info(c); + println!("{:}", p.get_next_header(),); + } + fn ipv6_send(&self, p: &MutableIpv6Packet, c: &ClientInfo) { + self.prolog("ipv6", "send", false); + self.client_info(c); + println!("{:}", p.get_next_header(),); + } + /* ICMPv4 */ + fn icmpv4_enabled(&self) -> bool { + self.icmpv4 + } + fn icmpv4_recv(&self, p: &IcmpPacket, c: &ClientInfo) { + self.prolog("icmpv4", "recv", false); + self.client_info(c); + println!("{:?}\t{:?}", p.get_icmp_type(), p.get_icmp_code(),); + } + fn icmpv4_drop(&self, p: &IcmpPacket, c: &ClientInfo) { + self.prolog("icmpv4", "drop", false); + self.client_info(c); + println!("{:?}\t{:?}", p.get_icmp_type(), p.get_icmp_code(),); + } + fn icmpv4_send(&self, p: &MutableIcmpPacket, c: &ClientInfo) { + self.prolog("icmpv4", "send", false); + self.client_info(c); + println!("{:?}\t{:?}", p.get_icmp_type(), p.get_icmp_code(),); + } + /* ICMPv6 */ + fn icmpv6_enabled(&self) -> bool { + self.icmpv6 + } + fn icmpv6_recv(&self, p: &Icmpv6Packet, c: &ClientInfo) { + self.prolog("icmpv6", "recv", false); + self.client_info(c); + println!("{:?}\t{:?}", p.get_icmpv6_type(), p.get_icmpv6_code(),); + } + fn icmpv6_drop(&self, p: &Icmpv6Packet, c: &ClientInfo) { + self.prolog("icmpv6", "drop", false); + self.client_info(c); + println!("{:?}\t{:?}", p.get_icmpv6_type(), p.get_icmpv6_code(),); + } + fn icmpv6_send(&self, p: &MutableIcmpv6Packet, c: &ClientInfo) { + self.prolog("icmpv6", "send", false); + self.client_info(c); + println!("{:?}\t{:?}", p.get_icmpv6_type(), p.get_icmpv6_code(),); + } + /* TCP */ + fn tcp_enabled(&self) -> bool { + self.tcp + } + fn tcp_recv(&self, p: &TcpPacket, c: &ClientInfo) { + self.prolog("tcp", "recv", false); + self.client_info(c); + println!( + "{:?}\t{:}\t{:}", + p.get_flags(), + p.get_sequence(), + p.get_acknowledgement(), + ); + } + fn tcp_drop(&self, p: &TcpPacket, c: &ClientInfo) { + self.prolog("tcp", "drop", false); + self.client_info(c); + println!( + "{:?}\t{:}\t{:}", + p.get_flags(), + p.get_sequence(), + p.get_acknowledgement(), + ); + } + fn tcp_send(&self, p: &MutableTcpPacket, c: &ClientInfo) { + self.prolog("tcp", "send", false); + self.client_info(c); + println!( + "{:?}\t{:}\t{:}", + p.get_flags(), + p.get_sequence(), + p.get_acknowledgement(), + ); + } + /* UDP */ + fn udp_enabled(&self) -> bool { + self.udp + } + fn udp_recv(&self, _p: &UdpPacket, c: &ClientInfo) { + self.prolog("udp", "recv", false); + self.client_info(c); + println!(""); + } + fn udp_drop(&self, _p: &UdpPacket, c: &ClientInfo) { + self.prolog("udp", "drop", false); + self.client_info(c); + println!(""); + } + fn udp_send(&self, _p: &MutableUdpPacket, c: &ClientInfo) { + self.prolog("udp", "send", false); + self.client_info(c); + println!(""); + } +} diff --git a/src/logger/logfmt.rs b/src/logger/logfmt.rs new file mode 100644 index 0000000..5c3e02a --- /dev/null +++ b/src/logger/logfmt.rs @@ -0,0 +1,332 @@ +// This file is part of masscanned. +// Copyright 2021 - The IVRE project +// +// Masscanned is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Masscanned is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +// License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Masscanned. If not, see . + +use std::time::SystemTime; + +use pnet::packet::{ + arp::{ArpPacket, MutableArpPacket}, + ethernet::{EthernetPacket, MutableEthernetPacket}, + icmp::{IcmpPacket, MutableIcmpPacket}, + icmpv6::{Icmpv6Packet, MutableIcmpv6Packet}, + ipv4::{Ipv4Packet, MutableIpv4Packet}, + ipv6::{Ipv6Packet, MutableIpv6Packet}, + tcp::{MutableTcpPacket, TcpPacket}, + udp::{MutableUdpPacket, UdpPacket}, +}; + +use crate::client::ClientInfo; +use crate::logger::Logger; + +pub struct LogfmtLogger { + arp: bool, + eth: bool, + ipv4: bool, + ipv6: bool, + icmpv4: bool, + icmpv6: bool, + tcp: bool, + udp: bool, +} + +impl LogfmtLogger { + pub fn new() -> Self { + LogfmtLogger { + arp: true, + eth: true, + ipv4: true, + ipv6: true, + icmpv4: true, + icmpv6: true, + tcp: true, + udp: true, + } + } + fn prolog(&self, proto: &str, verb: &str, crlf: bool) { + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(); + print!( + "ts={}.{} proto={} verb={}{}", + now.as_secs(), + now.subsec_millis(), + proto, + verb, + if crlf { "\n" } else { " " }, + ); + } + fn client_info(&self, c: &ClientInfo) { + print!( + "{}{}{}{}{}{}{}", + if let Some(m) = c.mac.src { + format!(" mac_src={}", m) + } else { + "".to_string() + }, + if let Some(m) = c.mac.dst { + format!(" mac_dst={}", m) + } else { + "".to_string() + }, + if let Some(i) = c.ip.src { + format!(" ip_src={}", i) + } else { + "".to_string() + }, + if let Some(i) = c.ip.dst { + format!(" ip_dst={}", i) + } else { + "".to_string() + }, + if let Some(t) = c.transport { + format!(" transport={}", t) + } else { + "".to_string() + }, + if let Some(p) = c.port.src { + format!(" port_src={}", p) + } else { + "".to_string() + }, + if let Some(p) = c.port.dst { + format!(" port_dst={}", p) + } else { + "".to_string() + }, + ); + } +} + +impl Logger for LogfmtLogger { + fn init(&self) { + self.prolog("arp", "init", true); + self.prolog("eth", "init", true); + self.prolog("ipv4", "init", true); + self.prolog("ipv6", "init", true); + self.prolog("icmpv4", "init", true); + self.prolog("icmpv6", "init", true); + self.prolog("tcp", "init", true); + self.prolog("udp", "init", true); + } + /* ARP */ + fn arp_enabled(&self) -> bool { + self.arp + } + fn arp_recv(&self, p: &ArpPacket) { + self.prolog("arp", "recv", false); + println!( + " mac_src={:} mac_dst={:} ip_src={:} ip_dst={:} op={:?}", + p.get_sender_hw_addr(), + p.get_target_hw_addr(), + p.get_sender_proto_addr(), + p.get_target_proto_addr(), + p.get_operation(), + ); + } + fn arp_drop(&self, p: &ArpPacket) { + self.prolog("arp", "drop", false); + println!( + " mac_src={:} mac_dst={:} ip_src={:} ip_dst={:} op={:?}", + p.get_sender_hw_addr(), + p.get_target_hw_addr(), + p.get_sender_proto_addr(), + p.get_target_proto_addr(), + p.get_operation(), + ); + } + fn arp_send(&self, p: &MutableArpPacket) { + self.prolog("arp", "send", false); + println!( + " mac_dst={:} mac_src={:} ip_dst={:} ip_src={:} op={:?}", + p.get_target_hw_addr(), + p.get_sender_hw_addr(), + p.get_target_proto_addr(), + p.get_sender_proto_addr(), + p.get_operation(), + ); + } + /* Ethernet */ + fn eth_enabled(&self) -> bool { + self.eth + } + fn eth_recv(&self, p: &EthernetPacket, c: &ClientInfo) { + self.prolog("eth", "recv", false); + self.client_info(c); + println!(" eth_type={:}", p.get_ethertype(),); + } + fn eth_drop(&self, p: &EthernetPacket, c: &ClientInfo) { + self.prolog("eth", "drop", false); + self.client_info(c); + println!(" eth_type={:}", p.get_ethertype(),); + } + fn eth_send(&self, p: &MutableEthernetPacket, c: &ClientInfo) { + self.prolog("eth", "send", false); + self.client_info(c); + println!(" eth_type={:}", p.get_ethertype(),); + } + /* IPv4 */ + fn ipv4_enabled(&self) -> bool { + self.ipv4 + } + fn ipv4_recv(&self, p: &Ipv4Packet, c: &ClientInfo) { + self.prolog("ipv4", "recv", false); + self.client_info(c); + println!(" next_proto={:}", p.get_next_level_protocol(),); + } + fn ipv4_drop(&self, p: &Ipv4Packet, c: &ClientInfo) { + self.prolog("ipv4", "drop", false); + self.client_info(c); + println!(" next_proto={:}", p.get_next_level_protocol(),); + } + fn ipv4_send(&self, p: &MutableIpv4Packet, c: &ClientInfo) { + self.prolog("ipv4", "send", false); + self.client_info(c); + println!(" next_proto={:}", p.get_next_level_protocol(),); + } + /* IPv6 */ + fn ipv6_enabled(&self) -> bool { + self.ipv6 + } + fn ipv6_recv(&self, p: &Ipv6Packet, c: &ClientInfo) { + self.prolog("ipv6", "recv", false); + self.client_info(c); + println!(" next_proto={:}", p.get_next_header(),); + } + fn ipv6_drop(&self, p: &Ipv6Packet, c: &ClientInfo) { + self.prolog("ipv6", "drop", false); + self.client_info(c); + println!(" next_proto={:}", p.get_next_header(),); + } + fn ipv6_send(&self, p: &MutableIpv6Packet, c: &ClientInfo) { + self.prolog("ipv6", "send", false); + self.client_info(c); + println!(" next_proto={:}", p.get_next_header(),); + } + /* ICMPv4 */ + fn icmpv4_enabled(&self) -> bool { + self.icmpv4 + } + fn icmpv4_recv(&self, p: &IcmpPacket, c: &ClientInfo) { + self.prolog("icmpv4", "recv", false); + self.client_info(c); + println!( + " icmp_type={:?} icmp_code={:?}", + p.get_icmp_type(), + p.get_icmp_code(), + ); + } + fn icmpv4_drop(&self, p: &IcmpPacket, c: &ClientInfo) { + self.prolog("icmpv4", "drop", false); + self.client_info(c); + println!( + " icmp_type={:?} icmp_code={:?}", + p.get_icmp_type(), + p.get_icmp_code(), + ); + } + fn icmpv4_send(&self, p: &MutableIcmpPacket, c: &ClientInfo) { + self.prolog("icmpv4", "send", false); + self.client_info(c); + println!( + " icmp_type={:?} icmp_code={:?}", + p.get_icmp_type(), + p.get_icmp_code(), + ); + } + /* ICMPv6 */ + fn icmpv6_enabled(&self) -> bool { + self.icmpv6 + } + fn icmpv6_recv(&self, p: &Icmpv6Packet, c: &ClientInfo) { + self.prolog("icmpv6", "recv", false); + self.client_info(c); + println!( + " icmpv6_type={:?} icmpv6_code={:?}", + p.get_icmpv6_type(), + p.get_icmpv6_code(), + ); + } + fn icmpv6_drop(&self, p: &Icmpv6Packet, c: &ClientInfo) { + self.prolog("icmpv6", "drop", false); + self.client_info(c); + println!( + " icmpv6_type={:?} icmpv6_code={:?}", + p.get_icmpv6_type(), + p.get_icmpv6_code(), + ); + } + fn icmpv6_send(&self, p: &MutableIcmpv6Packet, c: &ClientInfo) { + self.prolog("icmpv6", "send", false); + self.client_info(c); + println!( + " icmpv6_type={:?} icmpv6_code={:?}", + p.get_icmpv6_type(), + p.get_icmpv6_code(), + ); + } + /* TCP */ + fn tcp_enabled(&self) -> bool { + self.tcp + } + fn tcp_recv(&self, p: &TcpPacket, c: &ClientInfo) { + self.prolog("tcp", "recv", false); + self.client_info(c); + println!( + " flags={:?} seq={:} ack={:}", + p.get_flags(), + p.get_sequence(), + p.get_acknowledgement(), + ); + } + fn tcp_drop(&self, p: &TcpPacket, c: &ClientInfo) { + self.prolog("tcp", "drop", false); + self.client_info(c); + println!( + " flags={:?} seq={:} ack={:}", + p.get_flags(), + p.get_sequence(), + p.get_acknowledgement(), + ); + } + fn tcp_send(&self, p: &MutableTcpPacket, c: &ClientInfo) { + self.prolog("tcp", "send", false); + self.client_info(c); + println!( + " flags={:?} seq={:} ack={:}", + p.get_flags(), + p.get_sequence(), + p.get_acknowledgement(), + ); + } + /* UDP */ + fn udp_enabled(&self) -> bool { + self.udp + } + fn udp_recv(&self, _p: &UdpPacket, c: &ClientInfo) { + self.prolog("udp", "recv", false); + self.client_info(c); + println!(""); + } + fn udp_drop(&self, _p: &UdpPacket, c: &ClientInfo) { + self.prolog("udp", "drop", false); + self.client_info(c); + println!(""); + } + fn udp_send(&self, _p: &MutableUdpPacket, c: &ClientInfo) { + self.prolog("udp", "send", false); + self.client_info(c); + println!(""); + } +} diff --git a/src/logger/meta.rs b/src/logger/meta.rs new file mode 100644 index 0000000..94c752b --- /dev/null +++ b/src/logger/meta.rs @@ -0,0 +1,225 @@ +// This file is part of masscanned. +// Copyright 2021 - The IVRE project +// +// Masscanned is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Masscanned is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +// License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Masscanned. If not, see . + +use pnet::packet::{ + arp::{ArpPacket, MutableArpPacket}, + ethernet::{EthernetPacket, MutableEthernetPacket}, + icmp::{IcmpPacket, MutableIcmpPacket}, + icmpv6::{Icmpv6Packet, MutableIcmpv6Packet}, + ipv4::{Ipv4Packet, MutableIpv4Packet}, + ipv6::{Ipv6Packet, MutableIpv6Packet}, + tcp::{MutableTcpPacket, TcpPacket}, + udp::{MutableUdpPacket, UdpPacket}, +}; + +use crate::client::ClientInfo; +use crate::logger::Logger; + +pub struct MetaLogger { + loggers: Vec>, +} + +impl MetaLogger { + pub fn new() -> Self { + MetaLogger { + loggers: Vec::new(), + } + } + pub fn add(&mut self, log: Box) { + self.loggers.push(log); + } + pub fn init(&self) { + for l in &self.loggers { + l.init(); + } + } + /* ARP */ + pub fn arp_recv(&self, p: &ArpPacket) { + for l in &self.loggers { + if l.arp_enabled() { + l.arp_recv(p); + } + } + } + pub fn arp_drop(&self, p: &ArpPacket) { + for l in &self.loggers { + if l.arp_enabled() { + l.arp_drop(p); + } + } + } + pub fn arp_send(&self, p: &MutableArpPacket) { + for l in &self.loggers { + if l.arp_enabled() { + l.arp_send(p); + } + } + } + /* Ethernet */ + pub fn eth_recv(&self, p: &EthernetPacket, c: &ClientInfo) { + for l in &self.loggers { + if l.eth_enabled() { + l.eth_recv(p, c); + } + } + } + pub fn eth_drop(&self, p: &EthernetPacket, c: &ClientInfo) { + for l in &self.loggers { + if l.eth_enabled() { + l.eth_drop(p, c); + } + } + } + pub fn eth_send(&self, p: &MutableEthernetPacket, c: &ClientInfo) { + for l in &self.loggers { + if l.eth_enabled() { + l.eth_send(p, c); + } + } + } + /* IPv4 */ + pub fn ipv4_recv(&self, p: &Ipv4Packet, c: &ClientInfo) { + for l in &self.loggers { + if l.ipv4_enabled() { + l.ipv4_recv(p, c); + } + } + } + pub fn ipv4_drop(&self, p: &Ipv4Packet, c: &ClientInfo) { + for l in &self.loggers { + if l.ipv4_enabled() { + l.ipv4_drop(p, c); + } + } + } + pub fn ipv4_send(&self, p: &MutableIpv4Packet, c: &ClientInfo) { + for l in &self.loggers { + if l.ipv4_enabled() { + l.ipv4_send(p, c); + } + } + } + /* IPv6 */ + pub fn ipv6_recv(&self, p: &Ipv6Packet, c: &ClientInfo) { + for l in &self.loggers { + if l.ipv6_enabled() { + l.ipv6_recv(p, c); + } + } + } + pub fn ipv6_drop(&self, p: &Ipv6Packet, c: &ClientInfo) { + for l in &self.loggers { + if l.ipv6_enabled() { + l.ipv6_drop(p, c); + } + } + } + pub fn ipv6_send(&self, p: &MutableIpv6Packet, c: &ClientInfo) { + for l in &self.loggers { + if l.ipv6_enabled() { + l.ipv6_send(p, c); + } + } + } + /* ICMPv4 */ + pub fn icmpv4_recv(&self, p: &IcmpPacket, c: &ClientInfo) { + for l in &self.loggers { + if l.icmpv4_enabled() { + l.icmpv4_recv(p, c); + } + } + } + pub fn icmpv4_drop(&self, p: &IcmpPacket, c: &ClientInfo) { + for l in &self.loggers { + if l.icmpv4_enabled() { + l.icmpv4_drop(p, c); + } + } + } + pub fn icmpv4_send(&self, p: &MutableIcmpPacket, c: &ClientInfo) { + for l in &self.loggers { + if l.icmpv4_enabled() { + l.icmpv4_send(p, c); + } + } + } + /* ICMPv6 */ + pub fn icmpv6_recv(&self, p: &Icmpv6Packet, c: &ClientInfo) { + for l in &self.loggers { + if l.icmpv6_enabled() { + l.icmpv6_recv(p, c); + } + } + } + pub fn icmpv6_drop(&self, p: &Icmpv6Packet, c: &ClientInfo) { + for l in &self.loggers { + if l.icmpv6_enabled() { + l.icmpv6_drop(p, c); + } + } + } + pub fn icmpv6_send(&self, p: &MutableIcmpv6Packet, c: &ClientInfo) { + for l in &self.loggers { + if l.icmpv6_enabled() { + l.icmpv6_send(p, c); + } + } + } + /* TCP */ + pub fn tcp_recv(&self, p: &TcpPacket, c: &ClientInfo) { + for l in &self.loggers { + if l.tcp_enabled() { + l.tcp_recv(p, c); + } + } + } + pub fn tcp_drop(&self, p: &TcpPacket, c: &ClientInfo) { + for l in &self.loggers { + if l.tcp_enabled() { + l.tcp_drop(p, c); + } + } + } + pub fn tcp_send(&self, p: &MutableTcpPacket, c: &ClientInfo) { + for l in &self.loggers { + if l.tcp_enabled() { + l.tcp_send(p, c); + } + } + } + /* UDP */ + pub fn udp_recv(&self, p: &UdpPacket, c: &ClientInfo) { + for l in &self.loggers { + if l.udp_enabled() { + l.udp_recv(p, c); + } + } + } + pub fn udp_drop(&self, p: &UdpPacket, c: &ClientInfo) { + for l in &self.loggers { + if l.udp_enabled() { + l.udp_drop(p, c); + } + } + } + pub fn udp_send(&self, p: &MutableUdpPacket, c: &ClientInfo) { + for l in &self.loggers { + if l.udp_enabled() { + l.udp_send(p, c); + } + } + } +} diff --git a/src/logger/mod.rs b/src/logger/mod.rs new file mode 100644 index 0000000..8a6fb5e --- /dev/null +++ b/src/logger/mod.rs @@ -0,0 +1,97 @@ +// This file is part of masscanned. +// Copyright 2021 - The IVRE project +// +// Masscanned is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Masscanned is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +// License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Masscanned. If not, see . + +use pnet::packet::{ + arp::{ArpPacket, MutableArpPacket}, + ethernet::{EthernetPacket, MutableEthernetPacket}, + icmp::{IcmpPacket, MutableIcmpPacket}, + icmpv6::{Icmpv6Packet, MutableIcmpv6Packet}, + ipv4::{Ipv4Packet, MutableIpv4Packet}, + ipv6::{Ipv6Packet, MutableIpv6Packet}, + tcp::{MutableTcpPacket, TcpPacket}, + udp::{MutableUdpPacket, UdpPacket}, +}; + +use crate::client::ClientInfo; + +mod console; +mod logfmt; +mod meta; + +pub use console::ConsoleLogger; +pub use logfmt::LogfmtLogger; +pub use meta::MetaLogger; + +pub trait Logger { + fn init(&self); + /* list of notifications that a logger might or might not implement */ + /* ARP */ + fn arp_enabled(&self) -> bool { + true + } + fn arp_recv(&self, _p: &ArpPacket) {} + fn arp_drop(&self, _p: &ArpPacket) {} + fn arp_send(&self, _p: &MutableArpPacket) {} + /* Ethernet */ + fn eth_enabled(&self) -> bool { + true + } + fn eth_recv(&self, _p: &EthernetPacket, _c: &ClientInfo) {} + fn eth_drop(&self, _p: &EthernetPacket, _c: &ClientInfo) {} + fn eth_send(&self, _p: &MutableEthernetPacket, _c: &ClientInfo) {} + /* IPv4 */ + fn ipv4_enabled(&self) -> bool { + true + } + fn ipv4_recv(&self, _p: &Ipv4Packet, _c: &ClientInfo) {} + fn ipv4_drop(&self, _p: &Ipv4Packet, _c: &ClientInfo) {} + fn ipv4_send(&self, _p: &MutableIpv4Packet, _c: &ClientInfo) {} + /* IPv6 */ + fn ipv6_enabled(&self) -> bool { + true + } + fn ipv6_recv(&self, _p: &Ipv6Packet, _c: &ClientInfo) {} + fn ipv6_drop(&self, _p: &Ipv6Packet, _c: &ClientInfo) {} + fn ipv6_send(&self, _p: &MutableIpv6Packet, _c: &ClientInfo) {} + /* ICMPv4 */ + fn icmpv4_enabled(&self) -> bool { + true + } + fn icmpv4_recv(&self, _p: &IcmpPacket, _c: &ClientInfo) {} + fn icmpv4_drop(&self, _p: &IcmpPacket, _c: &ClientInfo) {} + fn icmpv4_send(&self, _p: &MutableIcmpPacket, _c: &ClientInfo) {} + /* ICMPv6 */ + fn icmpv6_enabled(&self) -> bool { + true + } + fn icmpv6_recv(&self, _p: &Icmpv6Packet, _c: &ClientInfo) {} + fn icmpv6_drop(&self, _p: &Icmpv6Packet, _c: &ClientInfo) {} + fn icmpv6_send(&self, _p: &MutableIcmpv6Packet, _c: &ClientInfo) {} + /* TCP */ + fn tcp_enabled(&self) -> bool { + true + } + fn tcp_recv(&self, _p: &TcpPacket, _c: &ClientInfo) {} + fn tcp_drop(&self, _p: &TcpPacket, _c: &ClientInfo) {} + fn tcp_send(&self, _p: &MutableTcpPacket, _c: &ClientInfo) {} + /* UDP */ + fn udp_enabled(&self) -> bool { + true + } + fn udp_recv(&self, _p: &UdpPacket, _c: &ClientInfo) {} + fn udp_drop(&self, _p: &UdpPacket, _c: &ClientInfo) {} + fn udp_send(&self, _p: &MutableUdpPacket, _c: &ClientInfo) {} +} diff --git a/src/masscanned.rs b/src/masscanned.rs index 5794357..5198dec 100644 --- a/src/masscanned.rs +++ b/src/masscanned.rs @@ -1,5 +1,5 @@ // This file is part of masscanned. -// Copyright 2021 - The IVRE project +// Copyright 2021 - 2022 The IVRE project // // Masscanned is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by @@ -24,7 +24,7 @@ use std::fs::File; use std::net::IpAddr; use std::str::FromStr; -use clap::{App, Arg}; +use clap::{builder::PossibleValuesParser, Arg, ArgAction, Command}; use log::*; use pnet::{ datalink::{self, Channel::Ethernet, DataLinkReceiver, DataLinkSender, NetworkInterface}, @@ -35,12 +35,14 @@ use pnet::{ util::MacAddr, }; +use crate::logger::{ConsoleLogger, LogfmtLogger, Logger, MetaLogger}; use crate::utils::IpAddrParser; mod client; mod layer_2; mod layer_3; mod layer_4; +mod logger; mod proto; mod smack; mod synackcookie; @@ -54,7 +56,10 @@ pub struct Masscanned<'a> { pub mac: MacAddr, /* iface is an Option to make tests easier */ pub iface: Option<&'a NetworkInterface>, - pub ip_addresses: Option<&'a HashSet>, + pub self_ip_list: Option<&'a HashSet>, + pub remote_ip_deny_list: Option<&'a HashSet>, + /* loggers */ + pub log: MetaLogger, } /* Get the L2 network interface from its name */ @@ -98,40 +103,75 @@ fn reply<'a, 'b>(packet: &'a [u8], masscanned: &Masscanned) -> Option("interface") .expect("error parsing iface argument"), ) { i } else { error!( "Cannot open interface \"{}\" - are you sure it exists?", - args.value_of("interface") + args.get_one::("interface") .expect("error parsing iface argument") ); return; }; - if iface.flags & (netdevice::IFF_UP.bits() as u32) == 0 { + if !iface.is_up() { error!("specified interface is DOWN"); return; } - let mac = if let Some(m) = args.value_of("mac") { + let mac = if let Some(m) = args.get_one::("mac") { MacAddr::from_str(m).expect("error parsing provided MAC address") } else if let Some(m) = iface.mac { m @@ -172,9 +209,9 @@ fn main() { }; /* Parse ip address file specified */ /* FIXME: .and_then(|path| File::open(path).map(|file| )).unwrap_or_default() ? */ - let ip_list = if let Some(ref path) = args.value_of("ip") { + let mut ip_list = if let Some(ref path) = args.get_one::("selfipfile") { if let Ok(file) = File::open(path) { - info!("parsing ip address file: {}", &path); + info!("parsing self ip file: {}", &path); file.extract_ip_addresses_only(None) } else { HashSet::new() @@ -182,23 +219,74 @@ fn main() { } else { HashSet::new() }; - let ip_addresses = if !ip_list.is_empty() { + if let Some(ip_inline) = args.get_one::("selfiplist") { + ip_list.extend(ip_inline.extract_ip_addresses_only(None)); + } + let self_ip_list = if !ip_list.is_empty() { + for ip in &ip_list { + info!("binding........{}", ip); + } + Some(&ip_list) + } else { + info!("binding........0.0.0.0"); + info!("binding........::"); + None + }; + /* Parse remote ip deny file specified */ + let mut ip_list = if let Some(ref path) = args.get_one::("remoteipdenyfile") { + if let Ok(file) = File::open(path) { + info!("parsing remote ip deny file: {}", &path); + file.extract_ip_addresses_only(None) + } else { + HashSet::new() + } + } else { + HashSet::new() + }; + if let Some(ip_inline) = args.get_one::("remoteipdenylist") { + ip_list.extend(ip_inline.extract_ip_addresses_only(None)); + } + let remote_ip_deny_list = if !ip_list.is_empty() { + for ip in &ip_list { + info!("ignoring.......{}", ip); + } Some(&ip_list) } else { None }; - let masscanned = Masscanned { + + let mut masscanned = Masscanned { synack_key: [0, 0], mac, iface: Some(&iface), - ip_addresses, + self_ip_list, + remote_ip_deny_list, + log: MetaLogger::new(), }; info!("interface......{}", masscanned.iface.unwrap().name); info!("mac address....{}", masscanned.mac); + if !args + .get_one::("quiet") + .expect("unexpected error parsing argument") + { + if let Some(format) = args.get_one::("format") { + let chosen_logger: Box = match format.as_str() { + "console" => Box::new(ConsoleLogger::new()), + "logfmt" => Box::new(LogfmtLogger::new()), + + // clap should already ensure we're using a valid format + _ => panic!("illegal format"), + }; + masscanned.log.add(chosen_logger); + } else { + masscanned.log.add(Box::new(ConsoleLogger::new())); + } + masscanned.log.init(); + } let (mut tx, mut rx) = get_channel(masscanned.iface.unwrap()); loop { /* check if network interface is still up */ - if masscanned.iface.unwrap().flags & (netdevice::IFF_UP.bits() as u32) == 0 { + if !masscanned.iface.unwrap().is_up() { error!("interface is DOWN - aborting"); break; } diff --git a/src/proto/dissector.rs b/src/proto/dissector.rs new file mode 100644 index 0000000..a370e12 --- /dev/null +++ b/src/proto/dissector.rs @@ -0,0 +1,92 @@ +// This file is part of masscanned. +// Copyright 2021 - The IVRE project +// +// Masscanned is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Masscanned is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +// License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Masscanned. If not, see . + +use crate::proto::ClientInfo; +use crate::proto::TCPControlBlock; +use crate::Masscanned; + +//////////// +// Common // +//////////// + +/// ### PacketDissector +/// A util class used to dissect fields. +#[derive(Debug, Clone)] +pub struct PacketDissector { + pub i: usize, + pub state: T, +} + +impl PacketDissector { + pub fn new(initial_state: T) -> PacketDissector { + return PacketDissector { + i: 0, + state: initial_state, + }; + } + pub fn next_state(&mut self, state: T) { + self.state = state; + self.i = 0; + } + pub fn next_state_when_i_reaches(&mut self, state: T, i: usize) { + if self.i == i { + self.next_state(state); + } + } + fn _read_usize(&mut self, byte: &u8, value: usize, next_state: T, size: usize) -> usize { + self.i += 1; + self.next_state_when_i_reaches(next_state, size); + (value << 8) + *byte as usize + } + fn _read_ulesize(&mut self, byte: &u8, value: usize, next_state: T, size: usize) -> usize { + let ret = value + ((*byte as usize) << (8 * self.i)); + self.i += 1; + self.next_state_when_i_reaches(next_state, size); + ret + } + pub fn read_u16(&mut self, byte: &u8, value: u16, next_state: T) -> u16 { + self._read_usize(byte, value as usize, next_state, 2) as u16 + } + pub fn read_ule16(&mut self, byte: &u8, value: u16, next_state: T) -> u16 { + self._read_ulesize(byte, value as usize, next_state, 2) as u16 + } + pub fn read_u32(&mut self, byte: &u8, value: u32, next_state: T) -> u32 { + self._read_usize(byte, value as usize, next_state, 4) as u32 + } + pub fn read_ule32(&mut self, byte: &u8, value: u32, next_state: T) -> u32 { + self._read_ulesize(byte, value as usize, next_state, 4) as u32 + } + pub fn read_ule64(&mut self, byte: &u8, value: u64, next_state: T) -> u64 { + self._read_ulesize(byte, value as usize, next_state, 8) as u64 + } +} + +pub trait MPacket { + fn new() -> Self; + fn repl( + &self, + _masscanned: &Masscanned, + _client_info: &ClientInfo, + _tcb: Option<&mut TCPControlBlock>, + ) -> Option>; + fn parse(&mut self, byte: &u8); + + fn parse_all(&mut self, bytes: &[u8]) { + for byte in bytes { + self.parse(byte); + } + } +} diff --git a/src/proto/dns/cst.rs b/src/proto/dns/cst.rs new file mode 100644 index 0000000..ff306c4 --- /dev/null +++ b/src/proto/dns/cst.rs @@ -0,0 +1,93 @@ +// This file is part of masscanned. +// Copyright 2022 - The IVRE project +// +// Masscanned is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Masscanned is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +// License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Masscanned. If not, see . + +use strum_macros::EnumIter; + +#[derive(PartialEq, Debug, Clone, Copy, EnumIter)] +pub enum DNSType { + NONE, + A, + TXT, // value: 16 - text strings +} + +impl From for DNSType { + fn from(item: u16) -> Self { + match item { + 1 => DNSType::A, + 16 => DNSType::TXT, + _ => DNSType::NONE, + } + } +} + +impl From for u16 { + fn from(item: DNSType) -> Self { + match item { + DNSType::A => 1, + DNSType::TXT => 16, + _ => 0, + } + } +} + +#[derive(PartialEq, Debug, Clone, Copy, EnumIter)] +pub enum DNSClass { + NONE, + IN, // value: 1 - the Internet + CH, // value: 3 - the CHAOS class +} + +impl From for DNSClass { + fn from(item: u16) -> Self { + match item { + 1 => DNSClass::IN, + 3 => DNSClass::CH, + _ => DNSClass::NONE, + } + } +} + +impl From for u16 { + fn from(item: DNSClass) -> Self { + match item { + DNSClass::IN => 1, + DNSClass::CH => 3, + _ => 0, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn type_parse() { + /* type TXT */ + assert!(DNSType::from(1) == DNSType::A); + assert!(1 as u16 == DNSType::A.into()); + assert!(DNSType::from(16) == DNSType::TXT); + assert!(16 as u16 == DNSType::TXT.into()); + } + + #[test] + fn class_parse() { + assert!(DNSClass::from(1) == DNSClass::IN); + assert!(1 as u16 == DNSClass::IN.into()); + assert!(DNSClass::from(3) == DNSClass::CH); + assert!(3 as u16 == DNSClass::CH.into()); + } +} diff --git a/src/proto/dns/header.rs b/src/proto/dns/header.rs new file mode 100644 index 0000000..e8a8d6e --- /dev/null +++ b/src/proto/dns/header.rs @@ -0,0 +1,387 @@ +// This file is part of masscanned. +// Copyright 2022 - The IVRE project +// +// Masscanned is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Masscanned is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +// License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Masscanned. If not, see . + +use std::convert::TryFrom; + +use crate::proto::dissector::{MPacket, PacketDissector}; +use crate::proto::ClientInfo; +use crate::proto::TCPControlBlock; +use crate::Masscanned; + +#[derive(PartialEq)] +pub enum DNSHeaderState { + Id, + Flags, + QDCount, + ANCount, + NSCount, + ARCount, + End, +} + +pub struct DNSHeader { + pub d: PacketDissector, + pub id: u16, + pub flags: u16, + pub _qr: bool, + pub _opcode: u8, + pub _aa: bool, + pub _tc: bool, + pub _rd: bool, + pub _ra: bool, + pub _z: u8, + pub _rcode: u8, + pub qdcount: u16, + pub ancount: u16, + pub nscount: u16, + pub arcount: u16, +} + +impl TryFrom> for DNSHeader { + type Error = &'static str; + + fn try_from(item: Vec) -> Result { + let mut hdr = DNSHeader::new(); + for b in item { + hdr.parse(&b); + } + if hdr.d.state == DNSHeaderState::End { + Ok(hdr) + } else { + Err("packet is incomplete") + } + } +} + +impl From<&DNSHeader> for Vec { + fn from(item: &DNSHeader) -> Self { + let mut v = Vec::new(); + /* id */ + v.push((item.id >> 8) as u8); + v.push((item.id & 0xFF) as u8); + + /* flags */ + /* QR | OPCODE | AA | TC | RD */ + v.push( + ((item._qr as u8) << 7) + | (item._opcode << 3) + | ((item._aa as u8) << 2) + | ((item._tc as u8) << 1) + | (item._rd as u8), + ); + /* AA | ZZZ | RCODE */ + v.push(0); + + /* qdcount */ + v.push((item.qdcount >> 8) as u8); + v.push((item.qdcount & 0xFF) as u8); + + /* ancount */ + v.push((item.ancount >> 8) as u8); + v.push((item.ancount & 0xFF) as u8); + + /* nscount */ + v.push((item.nscount >> 8) as u8); + v.push((item.nscount & 0xFF) as u8); + + /* arcount */ + v.push((item.arcount >> 8) as u8); + v.push((item.arcount & 0xFF) as u8); + + v + } +} + +impl MPacket for DNSHeader { + fn new() -> Self { + DNSHeader { + d: PacketDissector::new(DNSHeaderState::Id), + id: 0, + flags: 0, + _qr: false, + _opcode: 0, + _aa: false, + _tc: false, + _rd: false, + _ra: false, + _z: 0, + _rcode: 0, + qdcount: 0, + ancount: 0, + nscount: 0, + arcount: 0, + } + } + + fn parse(&mut self, byte: &u8) { + match self.d.state { + DNSHeaderState::Id => { + self.id = self.d.read_u16(byte, self.id, DNSHeaderState::Flags); + } + DNSHeaderState::Flags => { + self.flags = self.d.read_u16(byte, self.flags, DNSHeaderState::QDCount); + } + DNSHeaderState::QDCount => { + self.qdcount = self.d.read_u16(byte, self.qdcount, DNSHeaderState::ANCount); + } + DNSHeaderState::ANCount => { + self.ancount = self.d.read_u16(byte, self.ancount, DNSHeaderState::NSCount); + } + DNSHeaderState::NSCount => { + self.nscount = self.d.read_u16(byte, self.nscount, DNSHeaderState::ARCount); + } + DNSHeaderState::ARCount => { + self.arcount = self.d.read_u16(byte, self.arcount, DNSHeaderState::End); + } + DNSHeaderState::End => {} + } + /* we need this to be executed at the same call + * the state changes to End, hence it is not in the + * match structure + **/ + if self.d.state == DNSHeaderState::End { + self._qr = (self.flags >> 15) == 1; + self._opcode = ((self.flags >> 11) & 0x0F) as u8; + self._aa = (self.flags >> 10) & 0x01 == 1; + self._tc = (self.flags >> 9) & 0x01 == 1; + self._rd = (self.flags >> 8) & 0x01 == 1; + self._ra = (self.flags >> 7) & 0x01 == 1; + self._z = ((self.flags >> 4) & 0x07) as u8; + self._rcode = (self.flags & 0x0F) as u8; + } + } + + fn repl( + &self, + _masscanned: &Masscanned, + _client_info: &ClientInfo, + _tcb: Option<&mut TCPControlBlock>, + ) -> Option> { + let mut r = DNSHeader::new(); + r.id = self.id; + r._qr = true; + r._opcode = self._opcode; + r._aa = true; + r._tc = false; + /* RFC1035 + * Recursion Desired - this bit may be set in a query and + * is copied into the response. */ + r._rd = self._rd; + r._ra = false; + r.qdcount = self.qdcount; + r.ancount = self.qdcount; + Some(Vec::::from(&r)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use pnet::util::MacAddr; + use std::str::FromStr; + + use crate::logger::MetaLogger; + + #[test] + fn parse_all() { + let payload = b"\xb3\x07\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00"; + let hdr = match DNSHeader::try_from(payload.to_vec()) { + Ok(_hdr) => _hdr, + Err(e) => panic!("error while parsing DNS header: {}", e), + }; + assert!(hdr.d.state == DNSHeaderState::End); + assert!(hdr.id == 0xb307); + assert!(hdr.flags == 0x0100); + assert!(hdr._qr == false); + assert!(hdr._opcode == 0); + assert!(hdr._aa == false); + assert!(hdr._tc == false); + assert!(hdr._rd == true); + assert!(hdr._ra == false); + assert!(hdr._z == 0); + assert!(hdr._rcode == 0); + assert!(hdr.qdcount == 1); + assert!(hdr.ancount == 0); + assert!(hdr.nscount == 0); + assert!(hdr.arcount == 0); + assert!(Vec::::from(&hdr) == payload.to_vec()); + /* KO */ + let payload = b"\xb3\x07\x01\x00\x00\x01\x00\x00\x00\x00\x00"; + match DNSHeader::try_from(payload.to_vec()) { + Ok(_) => panic!("parsing should have failed"), + Err(_) => {} + }; + } + + #[test] + fn parse_byte_by_byte() { + /* OK */ + let payload = b"\xb3\x07\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00"; + let mut hdr = DNSHeader::new(); + for b in payload { + assert!(hdr.d.state != DNSHeaderState::End); + hdr.parse(b); + } + assert!(hdr.d.state == DNSHeaderState::End); + assert!(hdr.id == 0xb307); + assert!(hdr.flags == 0x0100); + assert!(hdr._qr == false); + assert!(hdr._opcode == 0); + assert!(hdr._aa == false); + assert!(hdr._tc == false); + assert!(hdr._rd == true); + assert!(hdr._ra == false); + assert!(hdr._z == 0); + assert!(hdr._rcode == 0); + assert!(hdr.qdcount == 1); + assert!(hdr.ancount == 0); + assert!(hdr.nscount == 0); + assert!(hdr.arcount == 0); + assert!(Vec::::from(&hdr) == payload.to_vec()); + /* KO */ + let payload = b"\xb3\x07\x01\x00\x00\x01\x00\x00\x00\x00\x00"; + let mut hdr = DNSHeader::new(); + for b in payload { + hdr.parse(b); + } + assert!(hdr.d.state != DNSHeaderState::End); + } + + fn consistency_qd_rr(qd: &DNSHeader, rr: &DNSHeader) { + assert!(rr.id == qd.id); + assert!(rr._qr == true); + assert!(rr._opcode == qd._opcode); + assert!(rr._aa == true); + assert!(rr._tc == false); + assert!(rr._rd == qd._rd); + assert!(rr._ra == false); + assert!(rr._z == 0); + assert!(rr._rcode == 0); + /* check flags */ + assert!(rr.flags >> 15 == rr._qr as u16); + assert!((rr.flags >> 11) & 0xF == rr._opcode as u16); + assert!((rr.flags >> 10) & 0x1 == rr._aa as u16); + assert!((rr.flags >> 9) & 0x1 == rr._tc as u16); + assert!((rr.flags >> 8) & 0x1 == rr._rd as u16); + assert!((rr.flags >> 7) & 0x1 == rr._ra as u16); + assert!((rr.flags >> 4) & 0x7 == rr._z as u16); + assert!(rr.flags & 0xF == rr._rcode as u16); + assert!(rr.qdcount == qd.qdcount); + assert!(rr.ancount == qd.qdcount); + assert!(rr.nscount == 0); + assert!(rr.arcount == 0); + } + + #[test] + fn repl_id() { + let masscanned = Masscanned { + synack_key: [0, 0], + mac: MacAddr::from_str("00:00:00:00:00:00").expect("error parsing default MAC address"), + iface: None, + self_ip_list: None, + remote_ip_deny_list: None, + log: MetaLogger::new(), + }; + let client_info = ClientInfo::new(); + let mut hdr = DNSHeader::new(); + hdr._qr = false; + for id in [0x1234, 0x4321, 0xffff, 0x0, 0x1337] { + hdr.id = id; + let hdr_repl = if let Some(r) = hdr.repl(&masscanned, &client_info, None) { + DNSHeader::try_from(r).unwrap() + } else { + panic!("expected DNS header answer, got None"); + }; + consistency_qd_rr(&hdr, &hdr_repl); + } + } + + #[test] + fn repl_opcode() { + let masscanned = Masscanned { + synack_key: [0, 0], + mac: MacAddr::from_str("00:00:00:00:00:00").expect("error parsing default MAC address"), + iface: None, + self_ip_list: None, + remote_ip_deny_list: None, + log: MetaLogger::new(), + }; + let client_info = ClientInfo::new(); + let mut hdr = DNSHeader::new(); + hdr._qr = false; + /* opcode */ + for opcode in 0..3 { + hdr._opcode = opcode; + let hdr_repl = if let Some(r) = hdr.repl(&masscanned, &client_info, None) { + DNSHeader::try_from(r).unwrap() + } else { + panic!("expected DNS header answer, got None"); + }; + consistency_qd_rr(&hdr, &hdr_repl); + } + } + + #[test] + fn repl_rd() { + let masscanned = Masscanned { + synack_key: [0, 0], + mac: MacAddr::from_str("00:00:00:00:00:00").expect("error parsing default MAC address"), + iface: None, + self_ip_list: None, + remote_ip_deny_list: None, + log: MetaLogger::new(), + }; + let client_info = ClientInfo::new(); + let mut hdr = DNSHeader::new(); + hdr._qr = false; + /* rd */ + for rd in [false, true] { + hdr._rd = rd; + let hdr_repl = if let Some(r) = hdr.repl(&masscanned, &client_info, None) { + DNSHeader::try_from(r).unwrap() + } else { + panic!("expected DNS header answer, got None"); + }; + consistency_qd_rr(&hdr, &hdr_repl); + } + } + + #[test] + fn repl_ancount() { + let masscanned = Masscanned { + synack_key: [0, 0], + mac: MacAddr::from_str("00:00:00:00:00:00").expect("error parsing default MAC address"), + iface: None, + self_ip_list: None, + remote_ip_deny_list: None, + log: MetaLogger::new(), + }; + let client_info = ClientInfo::new(); + let mut hdr = DNSHeader::new(); + hdr._qr = false; + /* rd */ + for qdcount in 0..16 { + hdr.qdcount = qdcount; + let hdr_repl = if let Some(r) = hdr.repl(&masscanned, &client_info, None) { + DNSHeader::try_from(r).unwrap() + } else { + panic!("expected DNS header answer, got None"); + }; + consistency_qd_rr(&hdr, &hdr_repl); + } + } +} diff --git a/src/proto/dns/mod.rs b/src/proto/dns/mod.rs new file mode 100644 index 0000000..0498791 --- /dev/null +++ b/src/proto/dns/mod.rs @@ -0,0 +1,688 @@ +// This file is part of masscanned. +// Copyright 2022 - The IVRE project +// +// Masscanned is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Masscanned is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +// License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Masscanned. If not, see . + +use std::convert::TryFrom; + +mod cst; + +mod header; +use header::{DNSHeader, DNSHeaderState}; + +mod query; +use query::{DNSQuery, DNSQueryState}; + +mod rr; +use rr::{DNSRRState, DNSRR}; + +use crate::proto::dissector::{MPacket, PacketDissector}; +use crate::proto::ClientInfo; +use crate::proto::TCPControlBlock; +use crate::Masscanned; + +#[derive(PartialEq, Debug)] +enum DNSState { + Header, + Query, + Answer, + Authority, + Additional, + End, +} + +pub struct DNSPacket { + d: PacketDissector, + header: DNSHeader, + qd: Vec, + rr: Vec, + ns: Vec, + ar: Vec, +} + +impl TryFrom> for DNSPacket { + type Error = &'static str; + + fn try_from(item: Vec) -> Result { + let mut dns = DNSPacket::new(); + for b in item { + dns.parse(&b); + } + if dns.d.state == DNSState::End { + Ok(dns) + } else { + Err("packet is incomplete") + } + } +} + +impl From<&DNSPacket> for Vec { + fn from(item: &DNSPacket) -> Self { + let mut v = Vec::new(); + v.extend(Vec::::from(&item.header)); + for qd in &item.qd { + v.extend(Vec::::from(qd)); + } + for rr in &item.rr { + v.extend(Vec::::from(rr)); + } + for ns in &item.ns { + v.extend(Vec::::from(ns)); + } + for ar in &item.ar { + v.extend(Vec::::from(ar)); + } + v + } +} + +impl MPacket for DNSPacket { + fn new() -> Self { + DNSPacket { + d: PacketDissector::new(DNSState::Header), + header: DNSHeader::new(), + qd: Vec::new(), + rr: Vec::new(), + ns: Vec::new(), + ar: Vec::new(), + } + } + + fn parse(&mut self, byte: &u8) { + match self.d.state { + DNSState::Header => { + self.header.parse(byte); + if self.header.d.state == DNSHeaderState::End { + if self.header.qdcount > 0 { + self.qd.push(DNSQuery::new()); + self.d.next_state(DNSState::Query); + } else if self.header.ancount > 0 { + self.rr.push(DNSRR::new()); + self.d.next_state(DNSState::Answer); + } else if self.header.nscount > 0 { + self.d.next_state(DNSState::Authority); + } else if self.header.arcount > 0 { + self.d.next_state(DNSState::Additional); + } else { + self.d.next_state(DNSState::End); + } + } + } + DNSState::Query => { + let qdcount = self.qd.len(); + self.qd[qdcount - 1].parse(byte); + if self.qd[qdcount - 1].d.state == DNSQueryState::End { + if self.header.qdcount as usize > self.qd.len() { + self.qd.push(DNSQuery::new()); + } else if self.header.ancount > 0 { + self.rr.push(DNSRR::new()); + self.d.next_state(DNSState::Answer); + } else if self.header.nscount > 0 { + self.d.next_state(DNSState::Authority); + } else if self.header.arcount > 0 { + self.d.next_state(DNSState::Additional); + } else { + self.d.next_state(DNSState::End); + } + } + } + DNSState::Answer => { + let ancount = self.rr.len(); + self.rr[ancount - 1].parse(byte); + if self.rr[ancount - 1].d.state == DNSRRState::End { + if self.header.ancount as usize > self.rr.len() { + self.rr.push(DNSRR::new()); + } else if self.header.nscount > 0 { + self.d.next_state(DNSState::Authority); + } else if self.header.arcount > 0 { + self.d.next_state(DNSState::Additional); + } else { + self.d.next_state(DNSState::End); + } + } + } + _ => {} + } + } + + fn repl( + &self, + masscanned: &Masscanned, + client_info: &ClientInfo, + _tcb: Option<&mut TCPControlBlock>, + ) -> Option> { + let mut ans = DNSPacket::new(); + ans.header = if let Some(hdr) = self.header.repl(&masscanned, &client_info, None) { + if let Ok(h) = DNSHeader::try_from(hdr) { + h + } else { + return None; + } + } else { + return None; + }; + /* reply to qd */ + for qd in &self.qd { + if let Ok(q) = DNSQuery::try_from(Vec::::from(qd)) { + ans.qd.push(q); + } else { + return None; + } + if let Some(raw_rr) = qd.repl(&masscanned, &client_info, None) { + if let Ok(rr) = DNSRR::try_from(raw_rr) { + ans.rr.push(rr); + } else { + return None; + } + } else { + return None; + } + } + Some(Vec::::from(&ans)) + } +} + +#[cfg(test)] +mod tests { + use super::cst::{DNSClass, DNSType}; + use super::*; + + use pnet::util::MacAddr; + use std::net::{IpAddr, Ipv4Addr}; + use std::str::FromStr; + + use crate::logger::MetaLogger; + + #[test] + fn parse_qd_all() { + /* OK */ + /* scapy: DNS(id=0x1337, + * qd=DNSQR(qname="www.example1.com")/DNSQR(qname="www.example2.com")/DNSQR(qname="www.example3.com")) + **/ + let payload = b"\x137\x01\x00\x00\x03\x00\x00\x00\x00\x00\x00\x03www\x08example1\x03com\x00\x00\x01\x00\x01\x03www\x08example2\x03com\x00\x00\x01\x00\x01\x03www\x08example3\x03com\x00\x00\x01\x00\x01"; + let dns = match DNSPacket::try_from(payload.to_vec()) { + Ok(_dns) => _dns, + Err(e) => panic!("error while parsing DNS packet: {}", e), + }; + assert!(dns.header.id == 0x1337); + assert!(dns.header._qr == false); + assert!(dns.header._opcode == 0); + assert!(dns.header._aa == false); + assert!(dns.header._tc == false); + assert!(dns.header._rd == true); + assert!(dns.header._ra == false); + assert!(dns.header._z == 0); + assert!(dns.header._rcode == 0); + assert!(dns.header.qdcount == 3); + assert!(dns.header.ancount == 0); + assert!(dns.header.nscount == 0); + assert!(dns.header.arcount == 0); + assert!(dns.qd.len() == 3); + assert!( + dns.qd[0].name + == [ + 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x31, + 0x03, 0x63, 0x6f, 0x6d, 0x00 + ] + ); + assert!(dns.qd[0].type_ == DNSType::A); + assert!(dns.qd[0].class == DNSClass::IN); + assert!( + dns.qd[1].name + == [ + 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x32, + 0x03, 0x63, 0x6f, 0x6d, 0x00 + ] + ); + assert!(dns.qd[1].type_ == DNSType::A); + assert!(dns.qd[1].class == DNSClass::IN); + assert!( + dns.qd[2].name + == [ + 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x33, + 0x03, 0x63, 0x6f, 0x6d, 0x00 + ] + ); + assert!(dns.qd[2].type_ == DNSType::A); + assert!(dns.qd[2].class == DNSClass::IN); + /* KO */ + let payload = b"\x137\x01\x00\x00\x03\x00\x00\x00\x00\x00\x00\x03www\x08example1\x03com\x00\x00\x01\x00\x01\x03www\x08example2\x03com\x00\x00\x01\x00\x01\x03www\x08example3\x03com\x00\x00\x01\x00"; + match DNSPacket::try_from(payload.to_vec()) { + Ok(_) => panic!("parsing should have failed"), + Err(_) => {} + } + let payload = b"xxx"; + match DNSPacket::try_from(payload.to_vec()) { + Ok(_) => panic!("parsing should have failed"), + Err(_) => {} + } + } + + #[test] + fn parse_qd_byte_by_byte() { + /* scapy: DNS(id=0x1337, + * qd=DNSQR(qname="www.example1.com")/DNSQR(qname="www.example2.com")/DNSQR(qname="www.example3.com")) + **/ + let payload = b"\x137\x01\x00\x00\x03\x00\x00\x00\x00\x00\x00\x03www\x08example1\x03com\x00\x00\x01\x00\x01\x03www\x08example2\x03com\x00\x00\x01\x00\x01\x03www\x08example3\x03com\x00\x00\x01\x00\x01"; + let mut dns = DNSPacket::new(); + for b in payload { + assert!(dns.d.state != DNSState::End); + dns.parse(&b); + } + assert!(dns.d.state == DNSState::End); + assert!(dns.header.id == 0x1337); + assert!(dns.header._qr == false); + assert!(dns.header._opcode == 0); + assert!(dns.header._aa == false); + assert!(dns.header._tc == false); + assert!(dns.header._rd == true); + assert!(dns.header._ra == false); + assert!(dns.header._z == 0); + assert!(dns.header._rcode == 0); + assert!(dns.header.qdcount == 3); + assert!(dns.header.ancount == 0); + assert!(dns.header.nscount == 0); + assert!(dns.header.arcount == 0); + assert!(dns.qd.len() == 3); + assert!( + dns.qd[0].name + == [ + 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x31, + 0x03, 0x63, 0x6f, 0x6d, 0x00 + ] + ); + assert!(dns.qd[0].type_ == DNSType::A); + assert!(dns.qd[0].class == DNSClass::IN); + assert!( + dns.qd[1].name + == [ + 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x32, + 0x03, 0x63, 0x6f, 0x6d, 0x00 + ] + ); + assert!(dns.qd[1].type_ == DNSType::A); + assert!(dns.qd[1].class == DNSClass::IN); + assert!( + dns.qd[2].name + == [ + 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x33, + 0x03, 0x63, 0x6f, 0x6d, 0x00 + ] + ); + assert!(dns.qd[2].type_ == DNSType::A); + assert!(dns.qd[2].class == DNSClass::IN); + } + + #[test] + fn parse_rr_all() { + /* OK */ + /* scapy: DNS(id=1234, qr=True, aa=True, qd=None, + * an=DNSRR(rrname="www.example1.com", rdata="127.0.0.1")/DNSRR(rrname="www.example2.com", rdata="127.0.0.2")/DNSRR(rrname="www.example3.com", rdata="127.0.0.3")) + **/ + let payload = b"\x04\xd2\x85\x00\x00\x00\x00\x03\x00\x00\x00\x00\x03www\x08example1\x03com\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x7f\x00\x00\x01\x03www\x08example2\x03com\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x7f\x00\x00\x02\x03www\x08example3\x03com\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x7f\x00\x00\x03"; + let dns = match DNSPacket::try_from(payload.to_vec()) { + Ok(_dns) => _dns, + Err(e) => panic!("error while parsing DNS packet: {}", e), + }; + assert!(dns.header.id == 1234); + assert!(dns.header._qr == true); + assert!(dns.header._opcode == 0); + assert!(dns.header._aa == true); + assert!(dns.header._tc == false); + assert!(dns.header._rd == true); + assert!(dns.header._ra == false); + assert!(dns.header._z == 0); + assert!(dns.header._rcode == 0); + assert!(dns.header.qdcount == 0); + assert!(dns.header.ancount == 3); + assert!(dns.header.nscount == 0); + assert!(dns.header.arcount == 0); + assert!(dns.rr.len() == 3); + assert!( + dns.rr[0].name + == [ + 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x31, + 0x03, 0x63, 0x6f, 0x6d, 0x00 + ] + ); + assert!(dns.rr[0].type_ == DNSType::A); + assert!(dns.rr[0].class == DNSClass::IN); + assert!(dns.rr[0].rdata == [0x7f, 0x00, 0x00, 0x01]); + assert!( + dns.rr[1].name + == [ + 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x32, + 0x03, 0x63, 0x6f, 0x6d, 0x00 + ] + ); + assert!(dns.rr[1].type_ == DNSType::A); + assert!(dns.rr[1].class == DNSClass::IN); + assert!(dns.rr[1].rdata == [0x7f, 0x00, 0x00, 0x02]); + assert!( + dns.rr[2].name + == [ + 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x33, + 0x03, 0x63, 0x6f, 0x6d, 0x00 + ] + ); + assert!(dns.rr[2].type_ == DNSType::A); + assert!(dns.rr[2].class == DNSClass::IN); + assert!(dns.rr[2].rdata == [0x7f, 0x00, 0x00, 0x03]); + /* KO */ + let payload = b"\x04\xd2\x85\x00\x00\x00\x00\x04\x00\x00\x00\x00\x03www\x08example1\x03com\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x7f\x00\x00\x01\x03www\x08example2\x03com\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x7f\x00\x00\x02\x03www\x08example3\x03com\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x7f\x00\x00\x03"; + match DNSPacket::try_from(payload.to_vec()) { + Ok(_) => panic!("parsing should have failed"), + Err(_) => {} + } + let payload = b"xxx"; + match DNSPacket::try_from(payload.to_vec()) { + Ok(_) => panic!("parsing should have failed"), + Err(_) => {} + } + } + + #[test] + fn parse_rr_byte_by_byte() { + /* scapy: DNS(id=1234, qr=True, aa=True, qd=None, + * an=DNSRR(rrname="www.example1.com", rdata="127.0.0.1")/DNSRR(rrname="www.example2.com", rdata="127.0.0.2")/DNSRR(rrname="www.example3.com", rdata="127.0.0.3")) + **/ + let payload = b"\x04\xd2\x85\x00\x00\x00\x00\x03\x00\x00\x00\x00\x03www\x08example1\x03com\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x7f\x00\x00\x01\x03www\x08example2\x03com\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x7f\x00\x00\x02\x03www\x08example3\x03com\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x7f\x00\x00\x03"; + let mut dns = DNSPacket::new(); + for b in payload { + assert!(dns.d.state != DNSState::End); + dns.parse(&b); + } + assert!(dns.d.state == DNSState::End); + assert!(dns.header.id == 1234); + assert!(dns.header._qr == true); + assert!(dns.header._opcode == 0); + assert!(dns.header._aa == true); + assert!(dns.header._tc == false); + assert!(dns.header._rd == true); + assert!(dns.header._ra == false); + assert!(dns.header._z == 0); + assert!(dns.header._rcode == 0); + assert!(dns.header.qdcount == 0); + assert!(dns.header.ancount == 3); + assert!(dns.header.nscount == 0); + assert!(dns.header.arcount == 0); + assert!(dns.rr.len() == 3); + assert!( + dns.rr[0].name + == [ + 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x31, + 0x03, 0x63, 0x6f, 0x6d, 0x00 + ] + ); + assert!(dns.rr[0].type_ == DNSType::A); + assert!(dns.rr[0].class == DNSClass::IN); + assert!(dns.rr[0].rdata == [0x7f, 0x00, 0x00, 0x01]); + assert!( + dns.rr[1].name + == [ + 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x32, + 0x03, 0x63, 0x6f, 0x6d, 0x00 + ] + ); + assert!(dns.rr[1].type_ == DNSType::A); + assert!(dns.rr[1].class == DNSClass::IN); + assert!(dns.rr[1].rdata == [0x7f, 0x00, 0x00, 0x02]); + assert!( + dns.rr[2].name + == [ + 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x33, + 0x03, 0x63, 0x6f, 0x6d, 0x00 + ] + ); + assert!(dns.rr[2].type_ == DNSType::A); + assert!(dns.rr[2].class == DNSClass::IN); + assert!(dns.rr[2].rdata == [0x7f, 0x00, 0x00, 0x03]); + } + + #[test] + fn parse_qd_rr_all() { + /* scapy: DNS(id=1234, qr=True, aa=True, + * qd=DNSQR(qname="www.example1.com")/DNSQR(qname="www.example2.com")/DNSQR(qname="www.example3.com"), + * an=DNSRR(rrname="www.example1.com", rdata="127.0.0.1")/DNSRR(rrname="www.example2.com", rdata="127.0.0.2")/DNSRR(rrname="www.example3.com", rdata="127.0.0.3")) + */ + let payload = b"\x04\xd2\x85\x00\x00\x03\x00\x03\x00\x00\x00\x00\x03www\x08example1\x03com\x00\x00\x01\x00\x01\x03www\x08example2\x03com\x00\x00\x01\x00\x01\x03www\x08example3\x03com\x00\x00\x01\x00\x01\x03www\x08example1\x03com\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x7f\x00\x00\x01\x03www\x08example2\x03com\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x7f\x00\x00\x02\x03www\x08example3\x03com\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x7f\x00\x00\x03"; + let dns = match DNSPacket::try_from(payload.to_vec()) { + Ok(_dns) => _dns, + Err(e) => panic!("error while parsing DNS packet: {}", e), + }; + assert!(dns.header.id == 1234); + assert!(dns.header._qr == true); + assert!(dns.header._opcode == 0); + assert!(dns.header._aa == true); + assert!(dns.header._tc == false); + assert!(dns.header._rd == true); + assert!(dns.header._ra == false); + assert!(dns.header._z == 0); + assert!(dns.header._rcode == 0); + assert!(dns.header.qdcount == 3); + assert!(dns.header.ancount == 3); + assert!(dns.header.nscount == 0); + assert!(dns.header.arcount == 0); + assert!(dns.qd.len() == 3); + assert!( + dns.qd[0].name + == [ + 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x31, + 0x03, 0x63, 0x6f, 0x6d, 0x00 + ] + ); + assert!(dns.qd[0].type_ == DNSType::A); + assert!(dns.qd[0].class == DNSClass::IN); + assert!( + dns.qd[1].name + == [ + 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x32, + 0x03, 0x63, 0x6f, 0x6d, 0x00 + ] + ); + assert!(dns.qd[1].type_ == DNSType::A); + assert!(dns.qd[1].class == DNSClass::IN); + assert!( + dns.qd[2].name + == [ + 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x33, + 0x03, 0x63, 0x6f, 0x6d, 0x00 + ] + ); + assert!(dns.qd[2].type_ == DNSType::A); + assert!(dns.qd[2].class == DNSClass::IN); + assert!(dns.rr.len() == 3); + assert!( + dns.rr[0].name + == [ + 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x31, + 0x03, 0x63, 0x6f, 0x6d, 0x00 + ] + ); + assert!(dns.rr[0].type_ == DNSType::A); + assert!(dns.rr[0].class == DNSClass::IN); + assert!(dns.rr[0].rdata == [0x7f, 0x00, 0x00, 0x01]); + assert!( + dns.rr[1].name + == [ + 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x32, + 0x03, 0x63, 0x6f, 0x6d, 0x00 + ] + ); + assert!(dns.rr[1].type_ == DNSType::A); + assert!(dns.rr[1].class == DNSClass::IN); + assert!(dns.rr[1].rdata == [0x7f, 0x00, 0x00, 0x02]); + assert!( + dns.rr[2].name + == [ + 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x33, + 0x03, 0x63, 0x6f, 0x6d, 0x00 + ] + ); + assert!(dns.rr[2].type_ == DNSType::A); + assert!(dns.rr[2].class == DNSClass::IN); + assert!(dns.rr[2].rdata == [0x7f, 0x00, 0x00, 0x03]); + } + + #[test] + fn parse_qr_rr_byte_by_byte() { + /* scapy: DNS(id=1234, qr=True, aa=True, + * qd=DNSQR(qname="www.example1.com")/DNSQR(qname="www.example2.com")/DNSQR(qname="www.example3.com"), + * an=DNSRR(rrname="www.example1.com", rdata="127.0.0.1")/DNSRR(rrname="www.example2.com", rdata="127.0.0.2")/DNSRR(rrname="www.example3.com", rdata="127.0.0.3")) + */ + let payload = b"\x04\xd2\x85\x00\x00\x03\x00\x03\x00\x00\x00\x00\x03www\x08example1\x03com\x00\x00\x01\x00\x01\x03www\x08example2\x03com\x00\x00\x01\x00\x01\x03www\x08example3\x03com\x00\x00\x01\x00\x01\x03www\x08example1\x03com\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x7f\x00\x00\x01\x03www\x08example2\x03com\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x7f\x00\x00\x02\x03www\x08example3\x03com\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x7f\x00\x00\x03"; + let mut dns = DNSPacket::new(); + for b in payload { + assert!(dns.d.state != DNSState::End); + dns.parse(&b); + } + assert!(dns.d.state == DNSState::End); + assert!(dns.header.id == 1234); + assert!(dns.header._qr == true); + assert!(dns.header._opcode == 0); + assert!(dns.header._aa == true); + assert!(dns.header._tc == false); + assert!(dns.header._rd == true); + assert!(dns.header._ra == false); + assert!(dns.header._z == 0); + assert!(dns.header._rcode == 0); + assert!(dns.header.qdcount == 3); + assert!(dns.header.ancount == 3); + assert!(dns.header.nscount == 0); + assert!(dns.header.arcount == 0); + assert!(dns.qd.len() == 3); + assert!( + dns.qd[0].name + == [ + 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x31, + 0x03, 0x63, 0x6f, 0x6d, 0x00 + ] + ); + assert!(dns.qd[0].type_ == DNSType::A); + assert!(dns.qd[0].class == DNSClass::IN); + assert!( + dns.qd[1].name + == [ + 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x32, + 0x03, 0x63, 0x6f, 0x6d, 0x00 + ] + ); + assert!(dns.qd[1].type_ == DNSType::A); + assert!(dns.qd[1].class == DNSClass::IN); + assert!( + dns.qd[2].name + == [ + 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x33, + 0x03, 0x63, 0x6f, 0x6d, 0x00 + ] + ); + assert!(dns.qd[2].type_ == DNSType::A); + assert!(dns.qd[2].class == DNSClass::IN); + assert!(dns.rr.len() == 3); + assert!( + dns.rr[0].name + == [ + 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x31, + 0x03, 0x63, 0x6f, 0x6d, 0x00 + ] + ); + assert!(dns.rr[0].type_ == DNSType::A); + assert!(dns.rr[0].class == DNSClass::IN); + assert!(dns.rr[0].rdata == [0x7f, 0x00, 0x00, 0x01]); + assert!( + dns.rr[1].name + == [ + 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x32, + 0x03, 0x63, 0x6f, 0x6d, 0x00 + ] + ); + assert!(dns.rr[1].type_ == DNSType::A); + assert!(dns.rr[1].class == DNSClass::IN); + assert!(dns.rr[1].rdata == [0x7f, 0x00, 0x00, 0x02]); + assert!( + dns.rr[2].name + == [ + 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x33, + 0x03, 0x63, 0x6f, 0x6d, 0x00 + ] + ); + assert!(dns.rr[2].type_ == DNSType::A); + assert!(dns.rr[2].class == DNSClass::IN); + assert!(dns.rr[2].rdata == [0x7f, 0x00, 0x00, 0x03]); + } + + #[test] + fn reply_in_a() { + let masscanned = Masscanned { + synack_key: [0, 0], + mac: MacAddr::from_str("00:00:00:00:00:00").expect("error parsing default MAC address"), + iface: None, + self_ip_list: None, + remote_ip_deny_list: None, + log: MetaLogger::new(), + }; + let mut client_info = ClientInfo::new(); + /* scapy: DNS(id=0x1337, + * qd=DNSQR(qname="www.example.com")) + **/ + let payload = b"\x137\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x03www\x07example\x03com\x00\x00\x01\x00\x01"; + let dns = DNSPacket::try_from(payload.to_vec()).unwrap(); + for ip in [ + Ipv4Addr::new(127, 0, 0, 1), + Ipv4Addr::new(0, 0, 0, 0), + Ipv4Addr::new(4, 3, 2, 1), + ] { + client_info.ip.dst = Some(IpAddr::V4(ip)); + let ans = if let Some(a) = dns.repl(&masscanned, &client_info, None) { + DNSPacket::try_from(a).unwrap() + } else { + panic!("expected a reply, got None"); + }; + assert!(ans.header.id == 0x1337); + assert!(ans.header._qr == true); + assert!(ans.header._opcode == 0); + assert!(ans.header._aa == true); + assert!(ans.header._tc == false); + assert!(ans.header._rd == dns.header._rd); + assert!(ans.header._ra == false); + assert!(ans.header._z == 0); + assert!(ans.header._rcode == 0); + assert!(ans.header.qdcount == 1); + assert!(ans.header.ancount == 1); + assert!(ans.header.nscount == 0); + assert!(ans.header.arcount == 0); + assert!(ans.qd.len() == 1); + assert!( + ans.qd[0].name + == [ + 0x03, 0x77, 0x77, 0x77, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, + 0x03, 0x63, 0x6f, 0x6d, 0x00 + ] + ); + assert!(ans.qd[0].type_ == DNSType::A); + assert!(ans.qd[0].class == DNSClass::IN); + assert!(ans.rr.len() == 1); + assert!( + ans.rr[0].name + == [ + 0x03, 0x77, 0x77, 0x77, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, + 0x03, 0x63, 0x6f, 0x6d, 0x00 + ] + ); + assert!(ans.rr[0].type_ == DNSType::A); + assert!(ans.rr[0].class == DNSClass::IN); + assert!(ans.rr[0].rdata == ip.octets()); + } + } +} diff --git a/src/proto/dns/query.rs b/src/proto/dns/query.rs new file mode 100644 index 0000000..95fb205 --- /dev/null +++ b/src/proto/dns/query.rs @@ -0,0 +1,337 @@ +// This file is part of masscanned. +// Copyright 2022 - The IVRE project +// +// Masscanned is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Masscanned is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +// License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Masscanned. If not, see . + +use super::cst::{DNSClass, DNSType}; +use super::rr::DNSRR; + +use std::convert::TryFrom; +use std::net::IpAddr; + +use crate::proto::dissector::{MPacket, PacketDissector}; +use crate::proto::ClientInfo; +use crate::proto::TCPControlBlock; +use crate::Masscanned; + +#[derive(PartialEq)] +pub enum DNSQueryState { + Name, + Type, + Class, + End, +} + +pub struct DNSQuery { + pub d: PacketDissector, + /* RFC 1035 - Section 4.1.2 */ + pub name: Vec, + _u_type: u16, + pub type_: DNSType, + _u_class: u16, + pub class: DNSClass, +} + +impl TryFrom> for DNSQuery { + type Error = &'static str; + + fn try_from(item: Vec) -> Result { + let mut query = DNSQuery::new(); + for b in item { + query.parse(&b); + } + if query.d.state == DNSQueryState::End { + Ok(query) + } else { + Err("packet is incomplete") + } + } +} + +impl From<&DNSQuery> for Vec { + fn from(item: &DNSQuery) -> Self { + let mut v = Vec::new(); + /* name */ + v.extend(&item.name); + /* type */ + v.push(((u16::from(item.type_)) >> 8) as u8); + v.push(((u16::from(item.type_)) & 0xFF) as u8); + /* class */ + v.push(((u16::from(item.class)) >> 8) as u8); + v.push(((u16::from(item.class)) & 0xFF) as u8); + /* return */ + v + } +} + +impl MPacket for DNSQuery { + fn new() -> Self { + DNSQuery { + d: PacketDissector::new(DNSQueryState::Name), + name: Vec::new(), + _u_type: 0, + type_: DNSType::NONE, + _u_class: 0, + class: DNSClass::NONE, + } + } + + fn parse(&mut self, byte: &u8) { + match self.d.state { + DNSQueryState::Name => { + self.name.push(*byte); + if *byte == 0 { + self.d.next_state(DNSQueryState::Type); + } + } + DNSQueryState::Type => { + self._u_type = self.d.read_u16(byte, self._u_type, DNSQueryState::Class); + } + DNSQueryState::Class => { + self._u_class = self.d.read_u16(byte, self._u_class, DNSQueryState::End); + } + DNSQueryState::End => {} + } + /* we need this to be executed at the same call + * the state changes to End, hence it is not in the + * match structure + **/ + if self.d.state == DNSQueryState::End { + self.type_ = DNSType::from(self._u_type); + self.class = DNSClass::from(self._u_class); + } + } + + fn repl( + &self, + _masscanned: &Masscanned, + client_info: &ClientInfo, + _tcb: Option<&mut TCPControlBlock>, + ) -> Option> { + match self.class { + DNSClass::IN => { + match self.type_ { + DNSType::A => { + let mut rr = DNSRR::new(); + /* copy request */ + for b in &self.name { + rr.name.push(*b); + } + rr.type_ = DNSType::A; + rr.class = DNSClass::IN; + rr.ttl = 43200; + rr.rdata = match client_info.ip.dst { + Some(IpAddr::V4(ip)) => ip.octets().to_vec(), + Some(IpAddr::V6(_)) => Vec::new(), + None => Vec::new(), + }; + rr.rdlen = rr.rdata.len() as u16; + Some(Vec::::from(&rr)) + } + _ => None, + } + } + _ => None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use pnet::util::MacAddr; + use std::net::{IpAddr, Ipv4Addr}; + use std::str::FromStr; + use strum::IntoEnumIterator; + + use crate::client::ClientInfoSrcDst; + use crate::logger::MetaLogger; + + #[test] + fn parse_in_a_all() { + /* A */ + let payload = b"\x03www\x07example\x03com\x00\x00\x01\x00\x01"; + let qr = match DNSQuery::try_from(payload.to_vec()) { + Ok(_qr) => _qr, + Err(e) => panic!("error while parsing DNS query: {}", e), + }; + assert!( + qr.name + == [ + 0x03, 0x77, 0x77, 0x77, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03, + 0x63, 0x6f, 0x6d, 0x00 + ] + ); + assert!(qr.type_ == DNSType::A); + assert!(qr.class == DNSClass::IN); + assert!(Vec::::from(&qr) == payload.to_vec()); + /* TXT */ + let payload = b"\x07version\x04bind\x00\x00\x10\x00\x03"; + let qr = match DNSQuery::try_from(payload.to_vec()) { + Ok(_qr) => _qr, + Err(e) => panic!("error while parsing DNS query: {}", e), + }; + assert!(qr.type_ == DNSType::TXT); + assert!(qr.class == DNSClass::CH); + assert!(Vec::::from(&qr) == payload.to_vec()); + /* KO */ + let payload = b"xxx"; + match DNSQuery::try_from(payload.to_vec()) { + Ok(_) => panic!("parsing should have failed"), + Err(_) => {} + } + } + + #[test] + fn parse_in_a_byte_by_byte() { + /* A */ + let payload = b"\x03www\x07example\x03com\x00\x00\x01\x00\x01"; + let mut qr = DNSQuery::new(); + for b in payload { + qr.parse(b); + } + assert!(qr.d.state == DNSQueryState::End); + assert!( + qr.name + == [ + 0x03, 0x77, 0x77, 0x77, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03, + 0x63, 0x6f, 0x6d, 0x00 + ] + ); + assert!(qr.type_ == DNSType::A); + assert!(qr.class == DNSClass::IN); + assert!(Vec::::from(&qr) == payload.to_vec()); + /* TXT */ + let payload = b"\x07version\x04bind\x00\x00\x10\x00\x03"; + let mut qr = DNSQuery::new(); + for b in payload { + qr.parse(b); + } + assert!(qr.d.state == DNSQueryState::End); + assert!(qr.type_ == DNSType::TXT); + assert!(qr.class == DNSClass::CH); + assert!(Vec::::from(&qr) == payload.to_vec()); + /* KO */ + let payload = b"xxx"; + let mut qr = DNSQuery::new(); + for b in payload { + qr.parse(b); + } + assert!(qr.d.state != DNSQueryState::End); + } + + #[test] + fn reply_in_a() { + let masscanned = Masscanned { + synack_key: [0, 0], + mac: MacAddr::from_str("00:00:00:00:00:00").expect("error parsing default MAC address"), + iface: None, + self_ip_list: None, + remote_ip_deny_list: None, + log: MetaLogger::new(), + }; + let ip_src = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); + let ip_dst = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2)); + let client_info = ClientInfo { + mac: ClientInfoSrcDst { + src: None, + dst: None, + }, + ip: ClientInfoSrcDst { + src: Some(ip_src), + dst: Some(ip_dst), + }, + transport: None, + port: ClientInfoSrcDst { + src: None, + dst: None, + }, + cookie: None, + }; + /* TXT */ + let payload = b"\x07version\x04bind\x00\x00\x10\x00\x03"; + let mut qr = DNSQuery::new(); + for b in payload { + qr.parse(b); + } + assert!(qr.type_ == DNSType::TXT); + assert!(qr.class == DNSClass::CH); + /* A */ + let payload = b"\x03www\x07example\x03com\x00\x00\x01\x00\x01"; + let mut qr = DNSQuery::new(); + for b in payload { + qr.parse(b); + } + assert!( + qr.name + == [ + 0x03, 0x77, 0x77, 0x77, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03, + 0x63, 0x6f, 0x6d, 0x00 + ] + ); + assert!(qr.type_ == DNSType::A); + assert!(qr.class == DNSClass::IN); + let rr_raw = match qr.repl(&masscanned, &client_info, None) { + None => { + panic!() + } + Some(r) => r, + }; + let mut rr = DNSRR::new(); + for b in rr_raw { + rr.parse(&b); + } + assert!(rr.name == qr.name); + assert!(rr.type_ == DNSType::A); + assert!(rr.class == DNSClass::IN); + assert!(rr.ttl == 43200); + assert!(rr.rdata == [127, 0, 0, 2]); + } + + #[test] + fn repl() { + let masscanned = Masscanned { + synack_key: [0, 0], + mac: MacAddr::from_str("00:00:00:00:00:00").expect("error parsing default MAC address"), + iface: None, + self_ip_list: None, + remote_ip_deny_list: None, + log: MetaLogger::new(), + }; + let client_info = ClientInfo::new(); + /* exhaustive tests */ + let supported: Vec<(DNSClass, DNSType)> = vec![(DNSClass::IN, DNSType::A)]; + let mut qd = DNSQuery::new(); + qd.name = vec![ + 0x03, 0x77, 0x77, 0x77, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03, 0x63, + 0x6f, 0x6d, 0x00, + ]; + for c in DNSClass::iter() { + qd.class = c; + for t in DNSType::iter() { + qd.type_ = t; + if supported.contains(&(c, t)) { + if qd.repl(&masscanned, &client_info, None) == None { + panic!("expected reply, got None"); + } + } else { + if qd.repl(&masscanned, &client_info, None) != None { + panic!("expected no reply, got one for {:?}, {:?}", c, t); + } + } + } + } + } +} diff --git a/src/proto/dns/rr.rs b/src/proto/dns/rr.rs new file mode 100644 index 0000000..db188bb --- /dev/null +++ b/src/proto/dns/rr.rs @@ -0,0 +1,251 @@ +// This file is part of masscanned. +// Copyright 2022 - The IVRE project +// +// Masscanned is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Masscanned is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +// License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Masscanned. If not, see . + +use super::cst::{DNSClass, DNSType}; + +use std::convert::TryFrom; + +use crate::proto::dissector::{MPacket, PacketDissector}; +use crate::proto::ClientInfo; +use crate::proto::TCPControlBlock; +use crate::Masscanned; + +#[derive(PartialEq, Debug)] +pub enum DNSRRState { + Name, + Type, + Class, + TTL, + RDLength, + RData, + End, +} + +pub struct DNSRR { + pub d: PacketDissector, + /* RFC 1035 - Section 3.2.1 */ + pub name: Vec, + _u_type: u16, + pub type_: DNSType, + _u_class: u16, + pub class: DNSClass, + pub ttl: u32, + pub rdlen: u16, + pub rdata: Vec, +} + +impl From<&DNSRR> for Vec { + fn from(item: &DNSRR) -> Self { + /* CAUTION: for the rdlen field: + * - if item.rdlen is not 0, its value is packed + * - if item.rdlen = 0, then the length of item.rdata is used instead + */ + let mut v = Vec::new(); + /* name */ + for b in &item.name { + v.push(b.clone()); + } + /* type */ + let type_: u16 = item.type_.into(); + v.push((type_ >> 8) as u8); + v.push((type_ & 0xFF) as u8); + /* class */ + let class: u16 = item.class.into(); + v.push((class >> 8) as u8); + v.push((class & 0xFF) as u8); + /* ttl */ + v.push((item.ttl >> 24) as u8); + v.push((item.ttl >> 16) as u8); + v.push((item.ttl >> 8) as u8); + v.push((item.ttl & 0xFF) as u8); + /* rdlen */ + let rdlen = if item.rdlen == 0 { + item.rdata.len() as u16 + } else { + item.rdlen + }; + v.push((rdlen >> 8) as u8); + v.push((rdlen & 0xFF) as u8); + /* rdata */ + for b in &item.rdata { + v.push(b.clone()); + } + v + } +} + +impl TryFrom> for DNSRR { + type Error = &'static str; + + fn try_from(item: Vec) -> Result { + let mut rr = DNSRR::new(); + for b in item { + rr.parse(&b); + } + if rr.d.state == DNSRRState::End { + Ok(rr) + } else { + Err("packet is incomplete") + } + } +} + +impl MPacket for DNSRR { + fn new() -> Self { + DNSRR { + d: PacketDissector::new(DNSRRState::Name), + name: Vec::new(), + _u_type: 0, + type_: DNSType::NONE, + _u_class: 0, + class: DNSClass::NONE, + rdlen: 0, + ttl: 0, + rdata: Vec::new(), + } + } + + fn parse(&mut self, byte: &u8) { + match self.d.state { + DNSRRState::Name => { + self.name.push(*byte); + if *byte == 0 { + self.d.next_state(DNSRRState::Type); + } + } + DNSRRState::Type => { + self._u_type = self.d.read_u16(byte, self._u_type, DNSRRState::Class); + } + DNSRRState::Class => { + self._u_class = self.d.read_u16(byte, self._u_class, DNSRRState::TTL); + } + DNSRRState::TTL => { + self.ttl = self.d.read_u32(byte, self.ttl, DNSRRState::RDLength); + } + DNSRRState::RDLength => { + self.rdlen = self.d.read_u16(byte, self.rdlen, DNSRRState::RData); + /* when read the rdlen, check if len is 0 */ + if self.d.state == DNSRRState::RData && self.rdlen == 0 { + self.d.state = DNSRRState::End; + } + } + DNSRRState::RData => { + self.rdata.push(*byte); + if self.rdata.len() == self.rdlen as usize { + self.d.next_state(DNSRRState::End); + } + } + DNSRRState::End => {} + } + /* we need this to be executed at the same call + * the state changes to End, hence it is not in the + * match structure + **/ + if self.d.state == DNSRRState::End { + self.type_ = DNSType::from(self._u_type); + self.class = DNSClass::from(self._u_class); + } + } + + fn repl( + &self, + _masscanned: &Masscanned, + _client_info: &ClientInfo, + _tcb: Option<&mut TCPControlBlock>, + ) -> Option> { + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn build() { + let mut rr = DNSRR::new(); + rr.name = b"\x03www\x07example\x03com\x00".to_vec(); + rr.class = DNSClass::IN; + rr.type_ = DNSType::A; + rr.ttl = 1234; + rr.rdlen = 4; + rr.rdata = b"\x7f\x00\x00\x01".to_vec(); + assert!(Vec::::from(&rr) == b"\x03www\x07example\x03com\x00\x00\x01\x00\x01\x00\x00\x04\xd2\x00\x04\x7f\x00\x00\x01"); + } + + #[test] + fn parse_all() { + /* + * raw(DNSRR(rrname="www.example.com", rdata="127.0.0.1")) + */ + let payload = b"\x03www\x07example\x03com\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x7f\x00\x00\x01"; + let rr = match DNSRR::try_from(payload.to_vec()) { + Ok(r) => r, + Err(e) => panic!("error while parsing DNS RR: {}", e), + }; + assert!(rr.name == b"\x03www\x07example\x03com\x00"); + assert!(rr.class == DNSClass::IN); + assert!(rr.type_ == DNSType::A); + assert!(rr.rdata == b"\x7f\x00\x00\x01"); + assert!(Vec::::from(&rr) == payload.to_vec()); + /* + * empty data + */ + let payload = b"\x03www\x07example\x03com\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x00"; + let rr = match DNSRR::try_from(payload.to_vec()) { + Ok(r) => r, + Err(e) => panic!("error while parsing DNS RR: {}", e), + }; + assert!(rr.name == b"\x03www\x07example\x03com\x00"); + assert!(rr.class == DNSClass::IN); + assert!(rr.type_ == DNSType::A); + assert!(rr.rdata == b""); + assert!(Vec::::from(&rr) == payload.to_vec()); + } + + #[test] + fn parse_byte_by_byte() { + /* + * raw(DNSRR(rrname="www.example.com", rdata="127.0.0.1")) + */ + let payload = b"\x03www\x07example\x03com\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x7f\x00\x00\x01"; + let mut rr = DNSRR::new(); + for b in payload { + assert!(rr.d.state != DNSRRState::End); + rr.parse(b); + } + assert!(rr.d.state == DNSRRState::End); + assert!(rr.name == b"\x03www\x07example\x03com\x00"); + assert!(rr.class == DNSClass::IN); + assert!(rr.type_ == DNSType::A); + assert!(rr.rdata == b"\x7f\x00\x00\x01"); + assert!(Vec::::from(&rr) == payload.to_vec()); + /* + * empty data + */ + let payload = b"\x03www\x07example\x03com\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x00"; + let mut rr = DNSRR::new(); + for b in payload { + assert!(rr.d.state != DNSRRState::End); + rr.parse(b); + } + assert!(rr.name == b"\x03www\x07example\x03com\x00"); + assert!(rr.class == DNSClass::IN); + assert!(rr.type_ == DNSType::A); + assert!(rr.rdata == b""); + assert!(Vec::::from(&rr) == payload.to_vec()); + } +} diff --git a/src/proto/ghost.rs b/src/proto/ghost.rs new file mode 100644 index 0000000..73d482e --- /dev/null +++ b/src/proto/ghost.rs @@ -0,0 +1,59 @@ +// This file is part of masscanned. +// Copyright 2021 - The IVRE project +// +// Masscanned is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Masscanned is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +// License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Masscanned. If not, see . + +use log::*; +use std::io::Write; + +use flate2::write::ZlibEncoder; +use flate2::Compression; + +use crate::client::ClientInfo; +use crate::proto::TCPControlBlock; +use crate::Masscanned; + +pub const GHOST_PATTERN_SIGNATURE: &[u8; 5] = b"Gh0st"; + +pub fn repl<'a>( + _data: &'a [u8], + _masscanned: &Masscanned, + _client_info: &mut ClientInfo, + _tcb: Option<&mut TCPControlBlock>, +) -> Option> { + debug!("receiving Gh0st data, sending one null byte payload"); + // Packet structure: + // GHOST_PATTERN_SIGNATURE + [ packet size ] + [ uncompressed payload size ] + payload + let mut result = GHOST_PATTERN_SIGNATURE.to_vec(); + let uncompressed_data = b"\x00"; + let mut compressed_data = ZlibEncoder::new(Vec::new(), Compression::default()); + compressed_data + .write_all(uncompressed_data) + .expect("Ghost: cannot decompress payload"); + let mut compressed_data = compressed_data + .finish() + .expect("Ghost: cannot decompress payload"); + let mut packet_len = compressed_data.len() + GHOST_PATTERN_SIGNATURE.len() + 4 * 2; + for _ in 0..4 { + result.push((packet_len % 256) as u8); + packet_len /= 256; + } + let mut uncompressed_len = uncompressed_data.len(); + for _ in 0..4 { + result.push((uncompressed_len % 256) as u8); + uncompressed_len /= 256; + } + result.append(&mut compressed_data); + Some(result) +} diff --git a/src/proto/http.rs b/src/proto/http.rs index 8a3da01..5f28e8a 100644 --- a/src/proto/http.rs +++ b/src/proto/http.rs @@ -21,6 +21,7 @@ use lazy_static::lazy_static; use std::str; use crate::client::ClientInfo; +use crate::proto::{ProtocolState as GenericProtocolState, TCPControlBlock}; use crate::smack::{ Smack, SmackFlags, BASE_STATE, NO_MATCH, SMACK_CASE_INSENSITIVE, UNANCHORED_STATE, }; @@ -62,7 +63,7 @@ const HTTP_STATE_CONTENT: usize = 64; const HTTP_STATE_FAIL: usize = 0xFFFF; -struct ProtocolState { +pub struct ProtocolState { state: usize, state_bis: usize, smack_state: usize, @@ -223,15 +224,39 @@ pub fn repl<'a>( data: &'a [u8], _masscanned: &Masscanned, _client_info: &ClientInfo, + tcb: Option<&mut TCPControlBlock>, ) -> Option> { debug!("receiving HTTP data"); - let mut pstate = ProtocolState::new(); + let mut state = ProtocolState::new(); + let mut pstate = { + if let Some(t) = tcb { + match t.proto_state { + None => t.proto_state = Some(GenericProtocolState::HTTP(ProtocolState::new())), + Some(GenericProtocolState::HTTP(_)) => {} + _ => { + panic!() + } + }; + if let Some(GenericProtocolState::HTTP(p)) = &mut t.proto_state { + p + } else { + panic!(); + } + } else { + &mut state + } + }; http_parse(&mut pstate, data); if pstate.state == HTTP_STATE_FAIL { debug!("data in not correctly formatted - not responding"); debug!("pstate: {}", pstate.state); return None; } + /* if not in CONTENT state, not responding yet (it means the client + * has not finished sending headers yet) */ + if pstate.state != HTTP_STATE_CONTENT { + return None; + } let content = "\ 401 Authorization Required @@ -267,122 +292,127 @@ WWW-Authenticate: Basic realm=\"Access to admin page\" Some(repl_data) } -#[test] -fn test_http_verb() { - /* all at once */ - for verb in HTTP_VERBS.iter() { - let mut pstate = ProtocolState::new(); - assert!(pstate.state == HTTP_STATE_START); - assert!(pstate.smack_state == BASE_STATE); - assert!(pstate.smack_id == NO_MATCH); - http_parse(&mut pstate, &verb.as_bytes()); - assert!(pstate.state == HTTP_STATE_SPACE); - assert!(pstate.smack_id == (HttpField::Verb as usize)); - assert!(pstate.http_verb == verb.as_bytes()); - } - /* byte by byte */ - for verb in HTTP_VERBS.iter() { - let mut pstate = ProtocolState::new(); - assert!(pstate.state == HTTP_STATE_START); - assert!(pstate.smack_state == BASE_STATE); - assert!(pstate.smack_id == NO_MATCH); - for i in 0..verb.len() { - if i > 0 { - assert!(pstate.state == HTTP_STATE_VERB); - assert!(pstate.smack_id == NO_MATCH); - } - http_parse(&mut pstate, &verb.as_bytes()[i..i + 1]); - } - assert!(pstate.state == HTTP_STATE_SPACE); - assert!(pstate.smack_id == (HttpField::Verb as usize)); - assert!(pstate.http_verb == verb.as_bytes()); - } - /* KO test: XXX */ - let mut pstate = ProtocolState::new(); - assert!(pstate.state == HTTP_STATE_START); - assert!(pstate.smack_state == BASE_STATE); - assert!(pstate.smack_id == NO_MATCH); - http_parse(&mut pstate, "XXX".as_bytes()); - assert!(pstate.state == HTTP_STATE_FAIL); - assert!(pstate.smack_state == UNANCHORED_STATE); - assert!(pstate.smack_id == NO_MATCH); - /* KO test: XGET */ - let mut pstate = ProtocolState::new(); - assert!(pstate.state == HTTP_STATE_START); - assert!(pstate.smack_state == BASE_STATE); - assert!(pstate.smack_id == NO_MATCH); - http_parse(&mut pstate, "XGET".as_bytes()); - assert!(pstate.state == HTTP_STATE_FAIL); - assert!(pstate.smack_state == UNANCHORED_STATE); - assert!(pstate.smack_id == NO_MATCH); - /* KO test: GEX */ - let mut pstate = ProtocolState::new(); - assert!(pstate.state == HTTP_STATE_START); - assert!(pstate.smack_state == BASE_STATE); - assert!(pstate.smack_id == NO_MATCH); - http_parse(&mut pstate, "GEX".as_bytes()); - assert!(pstate.state == HTTP_STATE_FAIL); - assert!(pstate.smack_state == UNANCHORED_STATE); - assert!(pstate.smack_id == NO_MATCH); - /* KO test: GE T */ - let mut pstate = ProtocolState::new(); - assert!(pstate.state == HTTP_STATE_START); - assert!(pstate.smack_state == BASE_STATE); - assert!(pstate.smack_id == NO_MATCH); - http_parse(&mut pstate, "GE T".as_bytes()); - assert!(pstate.state == HTTP_STATE_FAIL); - assert!(pstate.smack_state == UNANCHORED_STATE); - assert!(pstate.smack_id == NO_MATCH); -} +#[cfg(test)] +mod tests { + use super::*; -#[test] -fn test_http_request_line() { - let mut pstate = ProtocolState::new(); - let data = "GET /index.php HTTP/1.1\r\n".as_bytes(); - for i in 0..data.len() { - http_parse(&mut pstate, &data[i..i + 1]); - if i < 2 { - assert!(pstate.state == HTTP_STATE_VERB); - } else if i == 2 { + #[test] + fn test_http_verb() { + /* all at once */ + for verb in HTTP_VERBS.iter() { + let mut pstate = ProtocolState::new(); + assert!(pstate.state == HTTP_STATE_START); + assert!(pstate.smack_state == BASE_STATE); + assert!(pstate.smack_id == NO_MATCH); + http_parse(&mut pstate, &verb.as_bytes()); assert!(pstate.state == HTTP_STATE_SPACE); - } else if 3 <= i && i <= 13 { - assert!(pstate.state == HTTP_STATE_URI); - } else if 14 <= i && i <= 19 { - assert!(pstate.state == HTTP_STATE_H + (i - 14)); - } else if i == 20 { - assert!(pstate.state == HTTP_STATE_VERSION_MAJ); - } else if 21 <= i && i <= 23 { - assert!(pstate.state == HTTP_STATE_VERSION_MIN); - } else if i == 24 { - assert!(pstate.state == HTTP_STATE_FIELD_START); + assert!(pstate.smack_id == (HttpField::Verb as usize)); + assert!(pstate.http_verb == verb.as_bytes()); + } + /* byte by byte */ + for verb in HTTP_VERBS.iter() { + let mut pstate = ProtocolState::new(); + assert!(pstate.state == HTTP_STATE_START); + assert!(pstate.smack_state == BASE_STATE); + assert!(pstate.smack_id == NO_MATCH); + for i in 0..verb.len() { + if i > 0 { + assert!(pstate.state == HTTP_STATE_VERB); + assert!(pstate.smack_id == NO_MATCH); + } + http_parse(&mut pstate, &verb.as_bytes()[i..i + 1]); + } + assert!(pstate.state == HTTP_STATE_SPACE); + assert!(pstate.smack_id == (HttpField::Verb as usize)); + assert!(pstate.http_verb == verb.as_bytes()); + } + /* KO test: XXX */ + let mut pstate = ProtocolState::new(); + assert!(pstate.state == HTTP_STATE_START); + assert!(pstate.smack_state == BASE_STATE); + assert!(pstate.smack_id == NO_MATCH); + http_parse(&mut pstate, "XXX".as_bytes()); + assert!(pstate.state == HTTP_STATE_FAIL); + assert!(pstate.smack_state == UNANCHORED_STATE); + assert!(pstate.smack_id == NO_MATCH); + /* KO test: XGET */ + let mut pstate = ProtocolState::new(); + assert!(pstate.state == HTTP_STATE_START); + assert!(pstate.smack_state == BASE_STATE); + assert!(pstate.smack_id == NO_MATCH); + http_parse(&mut pstate, "XGET".as_bytes()); + assert!(pstate.state == HTTP_STATE_FAIL); + assert!(pstate.smack_state == UNANCHORED_STATE); + assert!(pstate.smack_id == NO_MATCH); + /* KO test: GEX */ + let mut pstate = ProtocolState::new(); + assert!(pstate.state == HTTP_STATE_START); + assert!(pstate.smack_state == BASE_STATE); + assert!(pstate.smack_id == NO_MATCH); + http_parse(&mut pstate, "GEX".as_bytes()); + assert!(pstate.state == HTTP_STATE_FAIL); + assert!(pstate.smack_state == UNANCHORED_STATE); + assert!(pstate.smack_id == NO_MATCH); + /* KO test: GE T */ + let mut pstate = ProtocolState::new(); + assert!(pstate.state == HTTP_STATE_START); + assert!(pstate.smack_state == BASE_STATE); + assert!(pstate.smack_id == NO_MATCH); + http_parse(&mut pstate, "GE T".as_bytes()); + assert!(pstate.state == HTTP_STATE_FAIL); + assert!(pstate.smack_state == UNANCHORED_STATE); + assert!(pstate.smack_id == NO_MATCH); + } + + #[test] + fn test_http_request_line() { + let mut pstate = ProtocolState::new(); + let data = "GET /index.php HTTP/1.1\r\n".as_bytes(); + for i in 0..data.len() { + http_parse(&mut pstate, &data[i..i + 1]); + if i < 2 { + assert!(pstate.state == HTTP_STATE_VERB); + } else if i == 2 { + assert!(pstate.state == HTTP_STATE_SPACE); + } else if 3 <= i && i <= 13 { + assert!(pstate.state == HTTP_STATE_URI); + } else if 14 <= i && i <= 19 { + assert!(pstate.state == HTTP_STATE_H + (i - 14)); + } else if i == 20 { + assert!(pstate.state == HTTP_STATE_VERSION_MAJ); + } else if 21 <= i && i <= 23 { + assert!(pstate.state == HTTP_STATE_VERSION_MIN); + } else if i == 24 { + assert!(pstate.state == HTTP_STATE_FIELD_START); + } } } -} -#[test] -fn test_http_request_field() { - let mut pstate = ProtocolState::new(); - let req = "POST /index.php HTTP/2.0\r\n".as_bytes(); - http_parse(&mut pstate, req); - assert!(pstate.state == HTTP_STATE_FIELD_START); - let field = b"Content-Length"; - http_parse(&mut pstate, field); - assert!(pstate.state == HTTP_STATE_FIELD_NAME); - let dot = b": "; - http_parse(&mut pstate, dot); - assert!(pstate.state == HTTP_STATE_FIELD_VALUE); - let value = b": 0\r\n"; - http_parse(&mut pstate, value); - assert!(pstate.state == HTTP_STATE_FIELD_START); -} + #[test] + fn test_http_request_field() { + let mut pstate = ProtocolState::new(); + let req = "POST /index.php HTTP/2.0\r\n".as_bytes(); + http_parse(&mut pstate, req); + assert!(pstate.state == HTTP_STATE_FIELD_START); + let field = b"Content-Length"; + http_parse(&mut pstate, field); + assert!(pstate.state == HTTP_STATE_FIELD_NAME); + let dot = b": "; + http_parse(&mut pstate, dot); + assert!(pstate.state == HTTP_STATE_FIELD_VALUE); + let value = b": 0\r\n"; + http_parse(&mut pstate, value); + assert!(pstate.state == HTTP_STATE_FIELD_START); + } -#[test] -fn test_http_request_no_field() { - let mut pstate = ProtocolState::new(); - let req = "POST /index.php HTTP/2.0\r\n".as_bytes(); - http_parse(&mut pstate, req); - assert!(pstate.state == HTTP_STATE_FIELD_START); - let crlf = "\r\n".as_bytes(); - http_parse(&mut pstate, crlf); - assert!(pstate.state == HTTP_STATE_CONTENT); + #[test] + fn test_http_request_no_field() { + let mut pstate = ProtocolState::new(); + let req = "POST /index.php HTTP/2.0\r\n".as_bytes(); + http_parse(&mut pstate, req); + assert!(pstate.state == HTTP_STATE_FIELD_START); + let crlf = "\r\n".as_bytes(); + http_parse(&mut pstate, crlf); + assert!(pstate.state == HTTP_STATE_CONTENT); + } } diff --git a/src/proto/mod.rs b/src/proto/mod.rs index 5094c75..87cfe3c 100644 --- a/src/proto/mod.rs +++ b/src/proto/mod.rs @@ -17,13 +17,15 @@ use lazy_static::lazy_static; use log::*; use pnet::packet::ip::IpNextHeaderProtocols; -use std::collections::HashMap; -use std::sync::Mutex; +use std::convert::TryFrom; use crate::client::ClientInfo; use crate::smack::{Smack, SmackFlags, BASE_STATE, NO_MATCH, SMACK_CASE_SENSITIVE}; use crate::Masscanned; +mod dns; +use dns::DNSPacket; + mod http; use http::HTTP_VERBS; @@ -31,19 +33,38 @@ mod stun; use stun::{STUN_PATTERN_CHANGE_REQUEST, STUN_PATTERN_EMPTY, STUN_PATTERN_MAGIC}; mod ssh; -use ssh::SSH_PATTERN_CLIENT_PROTOCOL; +use ssh::{SSH_PATTERN_CLIENT_PROTOCOL_1, SSH_PATTERN_CLIENT_PROTOCOL_2}; +mod ghost; +use ghost::GHOST_PATTERN_SIGNATURE; + +mod rpc; +use rpc::{RPC_CALL_TCP, RPC_CALL_UDP}; + +mod smb; +use smb::{SMB1_PATTERN_MAGIC, SMB2_PATTERN_MAGIC}; + +mod dissector; +use dissector::MPacket; + +// mod dissector; +// pub use dissector::PacketDissector; +// +mod tcb; +pub use tcb::{add_tcb, get_tcb, is_tcb_set, ProtocolState, TCPControlBlock}; + +const PROTO_NONE: usize = 0; const PROTO_HTTP: usize = 1; const PROTO_STUN: usize = 2; const PROTO_SSH: usize = 3; - -struct TCPControlBlock { - proto_state: usize, -} +const PROTO_GHOST: usize = 4; +const PROTO_RPC_TCP: usize = 5; +const PROTO_RPC_UDP: usize = 6; +const PROTO_SMB1: usize = 7; +const PROTO_SMB2: usize = 8; lazy_static! { static ref PROTO_SMACK: Smack = proto_init(); - static ref CONTABLE: Mutex> = Mutex::new(HashMap::new()); } fn proto_init() -> Smack { @@ -72,10 +93,40 @@ fn proto_init() -> Smack { SmackFlags::ANCHOR_BEGIN | SmackFlags::ANCHOR_END | SmackFlags::WILDCARDS, ); smack.add_pattern( - SSH_PATTERN_CLIENT_PROTOCOL, + SSH_PATTERN_CLIENT_PROTOCOL_2, PROTO_SSH, SmackFlags::ANCHOR_BEGIN, ); + smack.add_pattern( + SSH_PATTERN_CLIENT_PROTOCOL_1, + PROTO_SSH, + SmackFlags::ANCHOR_BEGIN, + ); + smack.add_pattern( + GHOST_PATTERN_SIGNATURE, + PROTO_GHOST, + SmackFlags::ANCHOR_BEGIN, + ); + smack.add_pattern( + RPC_CALL_TCP, + PROTO_RPC_TCP, + SmackFlags::ANCHOR_BEGIN | SmackFlags::WILDCARDS, + ); + smack.add_pattern( + RPC_CALL_UDP, + PROTO_RPC_UDP, + SmackFlags::ANCHOR_BEGIN | SmackFlags::WILDCARDS, + ); + smack.add_pattern( + SMB1_PATTERN_MAGIC, + PROTO_SMB1, + SmackFlags::ANCHOR_BEGIN | SmackFlags::WILDCARDS, + ); + smack.add_pattern( + SMB2_PATTERN_MAGIC, + PROTO_SMB2, + SmackFlags::ANCHOR_BEGIN | SmackFlags::WILDCARDS, + ); smack.compile(); smack } @@ -84,50 +135,59 @@ pub fn repl<'a>( data: &'a [u8], masscanned: &Masscanned, mut client_info: &mut ClientInfo, + mut tcb: Option<&mut TCPControlBlock>, ) -> Option> { debug!("packet payload: {:?}", data); let mut id; if client_info.transport == Some(IpNextHeaderProtocols::Tcp) && client_info.cookie == None { error!("Unexpected empty cookie"); return None; - } else if client_info.cookie != None { + } else if let Some(t) = &mut tcb { /* proto over TCP */ - let cookie = client_info.cookie.unwrap(); - let mut ct = CONTABLE.lock().unwrap(); - if !ct.contains_key(&cookie) { - ct.insert( - cookie, - TCPControlBlock { - proto_state: BASE_STATE, - }, - ); - } let mut i = 0; - let mut tcb = ct.get_mut(&cookie).unwrap(); - let mut state = tcb.proto_state; - id = PROTO_SMACK.search_next(&mut state, &data.to_vec(), &mut i); - tcb.proto_state = state; + if t.proto_id == PROTO_NONE { + let mut state = t.smack_state; + t.proto_id = PROTO_SMACK.search_next(&mut state, data, &mut i); + t.smack_state = state; + } + id = t.proto_id; } else { /* proto over else (e.g., UDP) */ let mut i = 0; let mut state = BASE_STATE; - id = PROTO_SMACK.search_next(&mut state, &data.to_vec(), &mut i); + id = PROTO_SMACK.search_next(&mut state, data, &mut i); /* because we are not over TCP, we can afford to assume end of pattern */ if id == NO_MATCH { id = PROTO_SMACK.search_next_end(&mut state); } + /* still no match: let us try to parse packet with protocoles + * that are not matched with a regex */ + if id == NO_MATCH { + /* try to parse data as a DNS packet */ + if let Ok(dns) = DNSPacket::try_from(data.to_vec()) { + if let Some(r) = dns.repl(&masscanned, &client_info, None) { + return Some(r); + } + } + } } /* proto over else (e.g., UDP) */ - if id == PROTO_HTTP { - return http::repl(data, masscanned, client_info); - } else if id == PROTO_STUN { - return stun::repl(data, masscanned, &mut client_info); - } else if id == PROTO_SSH { - return ssh::repl(data, masscanned, &mut client_info); - } else { - debug!("id: {}", id); + match id { + PROTO_HTTP => http::repl(data, masscanned, client_info, tcb), + PROTO_STUN => stun::repl(data, masscanned, &mut client_info, tcb), + PROTO_SSH => ssh::repl(data, masscanned, &mut client_info, tcb), + PROTO_GHOST => ghost::repl(data, masscanned, &mut client_info, tcb), + PROTO_RPC_TCP => rpc::repl_tcp(data, masscanned, &mut client_info, tcb), + PROTO_RPC_UDP => rpc::repl_udp(data, masscanned, &mut client_info, tcb), + PROTO_SMB1 => smb::repl_smb1(data, masscanned, &mut client_info, tcb), + PROTO_SMB2 => smb::repl_smb2(data, masscanned, &mut client_info, tcb), + _ => { + if let Some(t) = &mut tcb { + t.proto_id = PROTO_NONE; + } + None + } } - None } #[cfg(test)] @@ -139,6 +199,8 @@ mod tests { use pnet::util::MacAddr; + use crate::logger::MetaLogger; + #[test] fn test_proto_dispatch_stun() { let mut client_info = ClientInfo::new(); @@ -153,7 +215,9 @@ mod tests { synack_key: [0, 0], mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"), iface: None, - ip_addresses: Some(&ips), + self_ip_list: Some(&ips), + remote_ip_deny_list: None, + log: MetaLogger::new(), }; /***** TEST STUN - MAGIC *****/ /* test payload is: @@ -164,7 +228,7 @@ mod tests { */ let payload = b"\x00\x01\x00\x00\x21\x12\xa4\x42\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"; - let _stun_resp = if let Some(r) = repl(payload, &masscanned, &mut client_info) { + let _stun_resp = if let Some(r) = repl(payload, &masscanned, &mut client_info, None) { r } else { panic!("expected an answer, got nothing"); @@ -178,7 +242,7 @@ mod tests { */ let payload = b"\x00\x01\x00\x00\xaa\xbb\xcc\xdd\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"; - let _stun_resp = if let Some(r) = repl(payload, &masscanned, &mut client_info) { + let _stun_resp = if let Some(r) = repl(payload, &masscanned, &mut client_info, None) { r } else { panic!("expected an answer, got nothing"); @@ -191,7 +255,7 @@ mod tests { */ let payload = b"\x00\x01\x00\x08\x01\xdb\xd4]4\x9f\xe2RQ\x19\x05,\x93\x14f4\x00\x03\x00\x04\x00\x00\x00\x00"; - let _stun_resp = if let Some(r) = repl(payload, &masscanned, &mut client_info) { + let _stun_resp = if let Some(r) = repl(payload, &masscanned, &mut client_info, None) { r } else { panic!("expected an answer, got nothing"); @@ -212,28 +276,123 @@ mod tests { synack_key: [0, 0], mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"), iface: None, - ip_addresses: Some(&ips), + self_ip_list: Some(&ips), + remote_ip_deny_list: None, + log: MetaLogger::new(), }; /***** TEST SSH *****/ let payloads = [ - "SSH-2.0-PUTTY", - "SSH-2.0-Go", - "SSH-2.0-libssh2_1.4.3", - "SSH-2.0-PuTTY", - "SSH-2.0-AsyncSSH_2.1.0", - "SSH-2.0-libssh2_1.9.0", - "SSH-2.0-libssh2_1.7.0", - "SSH-2.0-8.35 FlowSsh: FlowSshNet_SftpStress54.38.116.473", - "SSH-2.0-libssh_0.9.5", - "SSH-2.0-OpenSSH_6.7p1 Raspbian-5+deb8u3", + "SSH-2.0-PUTTY\r\n", + "SSH-2.0-Go\r\n", + "SSH-2.0-libssh2_1.4.3\r\n", + "SSH-2.0-PuTTY\r\n", + "SSH-2.0-AsyncSSH_2.1.0\r\n", + "SSH-2.0-libssh2_1.9.0\r\n", + "SSH-2.0-libssh2_1.7.0\r\n", + "SSH-2.0-8.35 FlowSsh: FlowSshNet_SftpStress54.38.116.473\r\n", + "SSH-2.0-libssh_0.9.5\r\n", + "SSH-2.0-OpenSSH_6.7p1 Raspbian-5+deb8u3\r\n", + "SSH-1.99-Cisco-1.25\r\n", ]; for payload in payloads.iter() { - let _ssh_resp = if let Some(r) = repl(payload.as_bytes(), &masscanned, &mut client_info) - { - r - } else { - panic!("expected an answer, got nothing"); - }; + let _ssh_resp = + if let Some(r) = repl(payload.as_bytes(), &masscanned, &mut client_info, None) { + r + } else { + panic!("expected an answer, got nothing"); + }; + } + } + + #[test] + fn test_proto_dispatch_ghost() { + let mut client_info = ClientInfo::new(); + let test_ip_addr = Ipv4Addr::new(3, 2, 1, 0); + client_info.ip.src = Some(IpAddr::V4(test_ip_addr)); + client_info.port.src = Some(65000); + let masscanned_ip_addr = Ipv4Addr::new(0, 1, 2, 3); + let mut ips = HashSet::new(); + ips.insert(IpAddr::V4(masscanned_ip_addr)); + /* Construct masscanned context object */ + let masscanned = Masscanned { + synack_key: [0, 0], + mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"), + iface: None, + self_ip_list: Some(&ips), + remote_ip_deny_list: None, + log: MetaLogger::new(), + }; + /***** TEST GHOST *****/ + let payloads = [ + b"Gh0st\xad\x00\x00\x00\xe0\x00\x00\x00x\x9cKS``\x98\xc3\xc0\xc0\xc0\x06\xc4\x8c@\xbcQ\x96\x81\x81\tH\x07\xa7\x16\x95e&\xa7*\x04$&g+\x182\x94\xf6\xb000\xac\xa8rc\x00\x01\x11\xa0\x82\x1f\\`&\x83\xc7K7\x86\x19\xe5n\x0c9\x95n\x0c;\x84\x0f3\xac\xe8sch\xa8^\xcf4'J\x97\xa9\x82\xe30\xc3\x91h]&\x90\xf8\xce\x97S\xcbA4L?2=\xe1\xc4\x92\x86\x0b@\xf5`\x0cT\x1f\xae\xaf]\nr\x0b\x03#\xa3\xdc\x02~\x06\x86\x03+\x18m\xc2=\xfdtC,C\xfdL<<==\\\x9d\x19\x88\x00\xe5 \x02\x00T\xf5+\\" + ]; + for payload in payloads.iter() { + let _ghost_resp = + if let Some(r) = repl(&payload.to_vec(), &masscanned, &mut client_info, None) { + r + } else { + panic!("expected an answer, got nothing"); + }; + } + } + + #[test] + fn test_proto_dispatch_http() { + /* ensure that HTTP FSM does not answer until completion of request + * (at least headers) */ + let mut client_info = ClientInfo::new(); + let test_ip_addr = Ipv4Addr::new(3, 2, 1, 0); + client_info.ip.src = Some(IpAddr::V4(test_ip_addr)); + client_info.port.src = Some(65000); + let masscanned_ip_addr = Ipv4Addr::new(0, 1, 2, 3); + let mut ips = HashSet::new(); + ips.insert(IpAddr::V4(masscanned_ip_addr)); + /* Construct masscanned context object */ + let masscanned = Masscanned { + synack_key: [0, 0], + mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"), + iface: None, + self_ip_list: Some(&ips), + remote_ip_deny_list: None, + log: MetaLogger::new(), + }; + /***** TEST COMPLETE REQUEST *****/ + let payload = b"GET / HTTP/1.1\r\n\r\n"; + if let None = repl(&payload.to_vec(), &masscanned, &mut client_info, None) { + panic!("expected an answer, got nothing"); + } + /***** TEST INCOMPLETE REQUEST *****/ + let payload = b"GET / HTTP/1.1\r\n"; + if let Some(_) = repl(&payload.to_vec(), &masscanned, &mut client_info, None) { + panic!("expected no answer, got one"); + } + } + + #[test] + fn dispatch_dns() { + let masscanned = Masscanned { + synack_key: [0, 0], + mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"), + iface: None, + self_ip_list: None, + remote_ip_deny_list: None, + log: MetaLogger::new(), + }; + let mut client_info = ClientInfo::new(); + client_info.ip.dst = Some(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4))); + let payloads = [ + b"\x04\xd2\x01\x00\x00\x03\x00\x00\x00\x00\x00\x00\x03www\x08example1\x03com\x00\x00\x01\x00\x01\x03www\x08example2\x03com\x00\x00\x01\x00\x01\x03www\x08example3\x03com\x00\x00\x01\x00\x01", + ]; + for payload in payloads.iter() { + let dns_resp = + if let Some(r) = repl(&payload.to_vec(), &masscanned, &mut client_info, None) { + r + } else { + panic!("expected an answer, got nothing"); + }; + if let Err(e) = DNSPacket::try_from(dns_resp) { + panic!("error trying to parse the DNS answer: {}", e); + } } } } diff --git a/src/proto/rpc.rs b/src/proto/rpc.rs new file mode 100644 index 0000000..932b2f5 --- /dev/null +++ b/src/proto/rpc.rs @@ -0,0 +1,561 @@ +// This file is part of masscanned. +// Copyright 2021 - The IVRE project +// +// Masscanned is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Masscanned is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +// License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Masscanned. If not, see . + +use log::warn; +use std::convert::TryInto; +use std::net::IpAddr; + +use crate::client::ClientInfo; +use crate::proto::{ProtocolState as GenericProtocolState, TCPControlBlock}; +use crate::Masscanned; + +// last fragment (1 bit) + fragment len (31 bits) / length XID (random) / message type: call (0) / RPC version (0-255) / Program: Portmap (99840 - 100095) / Program version (*, random versions used, see below) / / Procedure: ??? (0-255) +pub const RPC_CALL_TCP: &[u8; 28] = + b"********\x00\x00\x00\x00\x00\x00\x00*\x00\x01\x86*****\x00\x00\x00*"; +// UDP: last fragment and fragment len are missing +pub const RPC_CALL_UDP: &[u8; 24] = + b"****\x00\x00\x00\x00\x00\x00\x00*\x00\x01\x86*****\x00\x00\x00*"; + +#[derive(Debug)] +enum RpcState { + Frag, + Xid, + MessageType, + RpcVersion, + Program, + ProgramVersion, + Procedure, + CredsFlavor, + CredsLen, + Creds, + VerifFlavor, + VerifLen, + Verif, + End, +} + +#[derive(Debug)] +pub struct ProtocolState { + state: RpcState, + last_frag: bool, + frag_len: u32, + xid: u32, + message_type: u32, + rpc_version: u32, + program: u32, + prog_version: u32, + procedure: u32, + creds_flavor: u32, + creds_data: Vec, + verif_flavor: u32, + verif_data: Vec, + payload: Vec, + cur_len: u32, + data_len: u32, +} + +struct Rpcb { + program: u32, + version: u32, + netid: String, + addr: String, + port: u16, + owner: String, +} + +impl ProtocolState { + fn new() -> Self { + ProtocolState { + state: RpcState::Frag, + last_frag: false, + frag_len: 0, + xid: 0, + message_type: 0, + rpc_version: 0, + program: 0, + prog_version: 0, + procedure: 0, + creds_flavor: 0, + creds_data: Vec::::new(), + verif_flavor: 0, + verif_data: Vec::::new(), + payload: Vec::::new(), + cur_len: 0, + data_len: 0, + } + } +} + +fn read_u32(pstate: &mut ProtocolState, byte: u8, value: u32, next_state: RpcState) -> u32 { + pstate.cur_len += 1; + if pstate.cur_len == 4 { + pstate.state = next_state; + pstate.cur_len = 0; + } + value * 256 + byte as u32 +} + +fn read_string(pstate: &mut ProtocolState, next_state: RpcState) { + pstate.data_len -= 1; + if pstate.data_len == 0 { + pstate.state = next_state; + } +} + +fn rpc_parse(pstate: &mut ProtocolState, data: &[u8]) { + for byte in data { + match pstate.state { + RpcState::Frag => { + if pstate.cur_len == 0 { + match byte & 128 { + 0 => pstate.last_frag = false, + _ => pstate.last_frag = true, + }; + pstate.frag_len = (*byte & 127) as u32; + } else { + pstate.frag_len = *byte as u32; + } + pstate.cur_len += 1; + if pstate.cur_len == 4 { + pstate.state = RpcState::Xid; + pstate.cur_len = 0; + } + } + RpcState::Xid => { + pstate.xid = read_u32(pstate, *byte, pstate.xid, RpcState::MessageType) + } + RpcState::MessageType => { + pstate.message_type = + read_u32(pstate, *byte, pstate.message_type, RpcState::RpcVersion) + } + RpcState::RpcVersion => { + pstate.rpc_version = read_u32(pstate, *byte, pstate.rpc_version, RpcState::Program) + } + RpcState::Program => { + pstate.program = read_u32(pstate, *byte, pstate.program, RpcState::ProgramVersion) + } + RpcState::ProgramVersion => { + pstate.prog_version = + read_u32(pstate, *byte, pstate.prog_version, RpcState::Procedure) + } + RpcState::Procedure => { + pstate.procedure = read_u32(pstate, *byte, pstate.procedure, RpcState::CredsFlavor) + } + RpcState::CredsFlavor => { + pstate.creds_flavor = + read_u32(pstate, *byte, pstate.creds_flavor, RpcState::CredsLen) + } + RpcState::CredsLen => { + pstate.data_len = read_u32(pstate, *byte, pstate.data_len, RpcState::Creds); + if matches!(pstate.state, RpcState::Creds) && pstate.data_len == 0 { + pstate.state = RpcState::VerifFlavor + } + } + RpcState::Creds => { + pstate.creds_data.push(*byte); + read_string(pstate, RpcState::VerifFlavor) + } + RpcState::VerifFlavor => { + pstate.verif_flavor = + read_u32(pstate, *byte, pstate.verif_flavor, RpcState::VerifLen) + } + RpcState::VerifLen => { + pstate.data_len = read_u32(pstate, *byte, pstate.data_len, RpcState::Verif); + if matches!(pstate.state, RpcState::Verif) && pstate.cur_len == 0 { + pstate.state = RpcState::End + } + } + RpcState::Verif => { + pstate.verif_data.push(*byte); + read_string(pstate, RpcState::End) + } + RpcState::End => { + pstate.payload.push(*byte); + } + }; + } +} + +fn get_nth_byte(value: u32, nth: u8) -> u8 { + let shift = 8 * (3 - nth); + ((value & (0xff << shift)) >> shift).try_into().unwrap() +} + +fn push_u32(buffer: &mut Vec, data: u32) { + for i in 0..4 { + buffer.push(get_nth_byte(data, i)); + } +} + +fn push_string_pad(buffer: &mut Vec, data: String) { + let len: u32 = data.len().try_into().unwrap(); + push_u32(buffer, len); + buffer.append(&mut data.as_bytes().to_vec()); + if len % 4 != 0 { + for _ in 0..(4 - (len % 4)) { + buffer.append(&mut b"\x00".to_vec()); + } + } +} + +fn build_repl_portmap(pstate: &mut ProtocolState, client_info: &ClientInfo) -> Vec { + let mut resp = Vec::::new(); + match pstate.procedure { + // 0 => {} + 3 => { + // getaddr / getport + // accepted state: 0 (RPC executed successfully) + resp.extend([0, 0, 0, 0]); + let localport = client_info.port.dst.unwrap(); + match pstate.prog_version { + 2 => { + push_u32(&mut resp, localport as u32); + } + 3 | 4 => { + let addr = format!( + "{}.{}.{}", + client_info.ip.dst.unwrap(), + localport >> 8, + localport % 256 + ); + push_string_pad(&mut resp, addr); + } + _ => panic!("Wrong RPC version"), + } + } + 4 => { + // dump + // accepted state: 0 (RPC executed successfully) + resp.extend([0, 0, 0, 0]); + let localaddr = client_info.ip.dst.unwrap(); + let localport = client_info.port.dst.unwrap(); + let netid = match localaddr { + IpAddr::V4(_) => "tcp", + IpAddr::V6(_) => "tcp6", + }; + for rpcb in [ + Rpcb { + program: 100000, + version: 2, + netid: netid.to_string(), + addr: format!("{}", localaddr), + port: localport, + owner: "superuser".to_string(), + }, + Rpcb { + program: 100000, + version: 3, + netid: netid.to_string(), + addr: format!("{}", localaddr), + port: localport, + owner: "superuser".to_string(), + }, + Rpcb { + program: 100000, + version: 4, + netid: netid.to_string(), + addr: format!("{}", localaddr), + port: localport, + owner: "superuser".to_string(), + }, + ] { + resp.append(&mut b"\x00\x00\x00\x01".to_vec()); // value follows: yes + push_u32(&mut resp, rpcb.program); + push_u32(&mut resp, rpcb.version); + match pstate.prog_version { + 2 => { + push_u32( + &mut resp, + match rpcb.netid.as_str() { + "tcp" => 6, + "tcp6" => 6, + "udp" => 17, + "udp6" => 17, + _ => 0, + }, + ); + push_u32(&mut resp, localport as u32); + } + 3 | 4 => { + push_string_pad(&mut resp, rpcb.netid); + push_string_pad( + &mut resp, + format!("{}.{}.{}", rpcb.addr, rpcb.port >> 8, rpcb.port & 0xff), + ); + push_string_pad(&mut resp, rpcb.owner); + } + _ => panic!("Wrong RPC version"), + } + } + resp.append(&mut b"\x00\x00\x00\x00".to_vec()); // value follows: no + } + _ => { + // accepted state: 5 (program can't support procedure) + resp.extend([0, 0, 0, 5]); + } + } + warn!( + "RPC: Portmap version {}, procedure {}", + pstate.prog_version, pstate.procedure + ); + resp +} + +fn build_repl_unknownprog(pstate: &mut ProtocolState, _client_info: &ClientInfo) -> Vec { + warn!( + "Unknown program {}, procedure {}: accepted state 1", + pstate.program, pstate.procedure + ); + // accepted state: 1 (remote hasn't exported program) + vec![0, 0, 0, 1] +} + +fn build_repl(pstate: &mut ProtocolState, client_info: &ClientInfo) -> Vec { + // TODO: test RPC versions, drop non calls? + let mut resp = Vec::::new(); + push_u32(&mut resp, pstate.xid); + // message_type: 1 (reply) + // reply_state: 0 (accepted) + // verifier: 0 (auth null) + // verifier length: 0 + resp.extend([0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + if pstate.prog_version < 2 || pstate.prog_version > 4 { + /* + * Scanners (e.g., Nmap script rpc-grind) often use random + * values for program version to find out if a program is + * supported, so for any program, we answer with "remote can't + * support version" accepted state. + */ + // accepted state: 2 (remote can't support version) + // prog_version min: 2 + // prog_version max: 4 + let prog_version = match pstate.prog_version { + 104316 => "104316 (Nmap probe TCP RPCCheck)".to_string(), + x => x.to_string(), + }; + warn!( + "RPC: unsupported version {} for program {}", + prog_version, pstate.program + ); + resp.extend([0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 4]); + } else if pstate.procedure == 0 { + /* + * RPC clients (e.g., Linux kernel NFS client, rpcbind CLI + * tool) would often send a NULL procedure (0) call before any + * real operation . + */ + // accepted state: 0 (RPC executed successfully) + warn!("RPC: NULL procedure call for program {}", pstate.program); + resp.extend([0, 0, 0, 0]); + } else { + let mut specif_resp = match pstate.program { + 100000 => build_repl_portmap(pstate, client_info), + _ => build_repl_unknownprog(pstate, client_info), + }; + resp.append(&mut specif_resp); + } + resp +} + +pub fn repl_tcp<'a>( + data: &'a [u8], + _masscanned: &Masscanned, + client_info: &ClientInfo, + tcb: Option<&mut TCPControlBlock>, +) -> Option> { + let mut state = ProtocolState::new(); + let mut pstate = { + if let Some(t) = tcb { + match t.proto_state { + None => t.proto_state = Some(GenericProtocolState::RPC(ProtocolState::new())), + Some(GenericProtocolState::RPC(_)) => {} + _ => { + panic!() + } + }; + if let Some(GenericProtocolState::RPC(p)) = &mut t.proto_state { + p + } else { + panic!(); + } + } else { + &mut state + } + }; + rpc_parse(&mut pstate, data); + // warn!("RPC {:#?}", pstate); + let resp = match pstate.state { + RpcState::End => Some(build_repl(pstate, client_info)), + _ => None, + }; + match resp { + Some(mut resp) => { + let length: u32 = resp.len().try_into().unwrap(); + let mut final_resp = Vec::::new(); + for i in 0..4 { + match i { + 0 => final_resp.push(get_nth_byte(length, i) | 0x80), + _ => final_resp.push(get_nth_byte(length, i)), + }; + } + final_resp.append(&mut resp); + Some(final_resp) + } + _ => None, + } +} + +pub fn repl_udp<'a>( + data: &'a [u8], + _masscanned: &Masscanned, + client_info: &ClientInfo, + _tcb: Option<&mut TCPControlBlock>, +) -> Option> { + let mut pstate = ProtocolState::new(); + pstate.state = RpcState::Xid; + pstate.last_frag = true; + pstate.frag_len = data.len().try_into().unwrap(); + rpc_parse(&mut pstate, data); + // warn!("RPC {:#?}", pstate); + match pstate.state { + RpcState::End => Some(build_repl(&mut pstate, client_info)), + _ => None, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::client::ClientInfoSrcDst; + use std::net::Ipv4Addr; + + const CLIENT_INFO: ClientInfo = ClientInfo { + mac: ClientInfoSrcDst { + src: None, + dst: None, + }, + ip: ClientInfoSrcDst { + src: Some(IpAddr::V4(Ipv4Addr::new(192, 0, 0, 0))), + dst: Some(IpAddr::V4(Ipv4Addr::new(192, 0, 0, 1))), + }, + transport: None, + port: ClientInfoSrcDst { + src: Some(12345), + dst: Some(111), + }, + cookie: None, + }; + + #[test] + fn test_probe_nmap() { + let mut pstate = ProtocolState::new(); + rpc_parse(&mut pstate, b"\x80\x00\x00\x28\x72\xfe\x1d\x13\x00\x00\x00\x00\x00\x00\x00\x02\x00\x01\x86\xa0\x00\x01\x97\x7c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"); + assert!(matches!(pstate.state, RpcState::End)); + assert!(pstate.xid == 0x72fe1d13); + assert!(pstate.rpc_version == 2); + assert!(pstate.program == 100000); + assert!(pstate.prog_version == 104316); + assert!(pstate.procedure == 0); + assert!(pstate.creds_flavor == 0); + assert!(pstate.creds_data.len() == 0); + assert!(pstate.verif_flavor == 0); + assert!(pstate.verif_data.len() == 0); + let resp = build_repl(&mut pstate, &CLIENT_INFO); + assert!(resp == b"\x72\xfe\x1d\x13\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x04"); + } + + #[test] + fn test_probe_nmap_udp() { + let mut pstate = ProtocolState::new(); + pstate.state = RpcState::Xid; + rpc_parse(&mut pstate, b"\x72\xfe\x1d\x13\x00\x00\x00\x00\x00\x00\x00\x02\x00\x01\x86\xa0\x00\x01\x97\x7c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"); + assert!(matches!(pstate.state, RpcState::End)); + assert!(pstate.xid == 0x72fe1d13); + assert!(pstate.rpc_version == 2); + assert!(pstate.program == 100000); + assert!(pstate.prog_version == 104316); + assert!(pstate.procedure == 0); + assert!(pstate.creds_flavor == 0); + assert!(pstate.creds_data.len() == 0); + assert!(pstate.verif_flavor == 0); + assert!(pstate.verif_data.len() == 0); + let resp = build_repl(&mut pstate, &CLIENT_INFO); + assert!(resp == b"\x72\xfe\x1d\x13\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x04"); + } + + #[test] + fn test_probe_nmap_split1() { + let mut pstate = ProtocolState::new(); + for byte in b"\x80\x00\x00\x28\x72\xfe\x1d\x13\x00\x00\x00\x00\x00\x00\x00\x02\x00\x01\x86\xa0\x00\x01\x97\x7c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" { + rpc_parse(&mut pstate, &[*byte]); + } + assert!(matches!(pstate.state, RpcState::End)); + assert!(pstate.xid == 0x72fe1d13); + assert!(pstate.rpc_version == 2); + assert!(pstate.program == 100000); + assert!(pstate.prog_version == 104316); + assert!(pstate.procedure == 0); + assert!(pstate.creds_flavor == 0); + assert!(pstate.creds_data.len() == 0); + assert!(pstate.verif_flavor == 0); + assert!(pstate.verif_data.len() == 0); + let resp = build_repl(&mut pstate, &CLIENT_INFO); + assert!(resp == b"\x72\xfe\x1d\x13\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x04"); + } + + #[test] + fn test_probe_nmap_split2() { + let mut pstate = ProtocolState::new(); + for data in [ + b"\x80\x00\x00\x28\x72\xfe\x1d", + b"\x13\x00\x00\x00\x00\x00\x00", + b"\x00\x02\x00\x01\x86\xa0\x00", + b"\x01\x97\x7c\x00\x00\x00\x00", + b"\x00\x00\x00\x00\x00\x00\x00", + b"\x00\x00\x00\x00\x00\x00\x00", + ] { + rpc_parse(&mut pstate, data); + } + rpc_parse(&mut pstate, b"\x00\x00"); + assert!(matches!(pstate.state, RpcState::End)); + assert!(pstate.xid == 0x72fe1d13); + assert!(pstate.rpc_version == 2); + assert!(pstate.program == 100000); + assert!(pstate.prog_version == 104316); + assert!(pstate.procedure == 0); + assert!(pstate.creds_flavor == 0); + assert!(pstate.creds_data.len() == 0); + assert!(pstate.verif_flavor == 0); + assert!(pstate.verif_data.len() == 0); + let resp = build_repl(&mut pstate, &CLIENT_INFO); + assert!(resp == b"\x72\xfe\x1d\x13\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x04"); + } + + #[test] + fn test_probe_portmap_v4_dump() { + let mut pstate = ProtocolState::new(); + rpc_parse(&mut pstate, b"\x80\x00\x00\x28\x01\x1b\x60\xa6\x00\x00\x00\x00\x00\x00\x00\x02\x00\x01\x86\xa0\x00\x00\x00\x04\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"); + assert!(matches!(pstate.state, RpcState::End)); + assert!(pstate.rpc_version == 2); + assert!(pstate.program == 100000); + assert!(pstate.prog_version == 4); + assert!(pstate.procedure == 4); // dump + assert!(pstate.creds_flavor == 0); + assert!(pstate.creds_data.len() == 0); + assert!(pstate.verif_flavor == 0); + assert!(pstate.verif_data.len() == 0); + } +} diff --git a/src/proto/smb.rs b/src/proto/smb.rs new file mode 100644 index 0000000..41f9519 --- /dev/null +++ b/src/proto/smb.rs @@ -0,0 +1,1418 @@ +// This file is part of masscanned. +// Copyright 2021 - The IVRE project +// +// Masscanned is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Masscanned is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +// License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Masscanned. If not, see . + +use log::*; +use std::collections::HashSet; +use std::convert::TryInto; +use std::time::SystemTime; + +use crate::client::ClientInfo; +use crate::proto::TCPControlBlock; +use crate::Masscanned; + +use crate::proto::dissector::{MPacket, PacketDissector}; + +// NBTSession + SMB Header +// netbios type (1 byte) + reserved (1 byte) + length (2 bytes) + SMB MAGIC (4 bytes) +// +pub const SMB1_PATTERN_MAGIC: &[u8; 8] = b"\x00\x00**\xffSMB"; +pub const SMB2_PATTERN_MAGIC: &[u8; 8] = b"\x00\x00**\xfeSMB"; + +// Build/Dissect secblob with Scapy using: GSSAPI_BLOB(b"`\x82.....") +const SECURITY_BLOB_NEG_PROTO: &[u8] = b"`\x82\x01<\x06\x06+\x06\x01\x05\x05\x02\xa0\x82\x0100\x82\x01,\xa0\x1a0\x18\x06\n+\x06\x01\x04\x01\x827\x02\x02\x1e\x06\n+\x06\x01\x04\x01\x827\x02\x02\n\xa2\x82\x01\x0c\x04\x82\x01\x08NEGOEXTS\x01\x00\x00\x00\x00\x00\x00\x00`\x00\x00\x00p\x00\x00\x001<*:\xc7+<\xa9m\xac8t\xa7\xdd\x1d[\xf4Rk\x17\x03\x8aK\x91\xc2\t}\x9a\x8f\xe6,\x96\\Q$/\x90MG\xc7\xad\x8f\x87k\"\x02\xbf\xc6\x00\x00\x00\x00\x00\x00\x00\x00`\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\\3S\r\xea\xf9\rM\xb2\xecJ\xe3xn\xc3\x08NEGOEXTS\x03\x00\x00\x00\x01\x00\x00\x00@\x00\x00\x00\x98\x00\x00\x001<*:\xc7+<\xa9m\xac8t\xa7\xdd\x1d[\\3S\r\xea\xf9\rM\xb2\xecJ\xe3xn\xc3\x08@\x00\x00\x00X\x00\x00\x000V\xa0T0R0'\x80%0#1!0\x1f\x06\x03U\x04\x03\x13\x18Token Signing Public Key0'\x80%0#1!0\x1f\x06\x03U\x04\x03\x13\x18Token Signing Public Key"; +const SECURITY_BLOB_CHALLENGE: &[u8] = b"\xa1\x81\x9c0\x81\x99\xa0\x03\n\x01\x01\xa1\x0c\x06\n+\x06\x01\x04\x01\x827\x02\x02\n\xa2\x81\x83\x04\x81\x80NTLMSSP\x00\x02\x00\x00\x00\x08\x00\x08\x008\x00\x00\x00\x15\x82\x8a\xe2$\x91\xa8\xf6\xf3\x89-4\x00\x00\x00\x00\x00\x00\x00\x00@\x00@\x00@\x00\x00\x00\n\x00aJ\x00\x00\x00\x0fW\x00I\x00N\x001\x00\x02\x00\x08\x00W\x00I\x00N\x001\x00\x01\x00\x08\x00W\x00I\x00N\x001\x00\x04\x00\x08\x00W\x00I\x00N\x001\x00\x03\x00\x08\x00W\x00I\x00N\x001\x00\x07\x00\x08\x00\xff&9\xf5B\x1d\xd8\x01\x00\x00\x00\x00"; + +///////////// +// Netbios // +///////////// + +#[derive(Debug, Clone, Copy)] +enum NBTSessionState { + NBType, + Reserved, + Length, + End, +} + +#[derive(Debug, Clone)] +struct NBTSession { + // DISSECTION + d: PacketDissector, + // STRUCT + nb_type: u8, + length: u16, + payload: Option, +} + +impl MPacket for NBTSession { + fn new() -> NBTSession { + Self { + d: PacketDissector::new(NBTSessionState::NBType), + nb_type: 0, + length: 0, + payload: None, + } + } + + fn parse(&mut self, byte: &u8) { + match self.d.state { + NBTSessionState::NBType => { + self.nb_type = *byte; + self.d.next_state(NBTSessionState::Reserved); + } + NBTSessionState::Reserved => { + self.d.next_state(NBTSessionState::Length); + } + NBTSessionState::Length => { + self.length = self.d.read_u16(byte, self.length, NBTSessionState::End) + } + NBTSessionState::End => match self.get_payload() { + Some(pay) => pay.parse(byte), + None => return, + }, + } + } + + fn repl( + &self, + masscanned: &Masscanned, + client_info: &ClientInfo, + tcb: Option<&mut TCPControlBlock>, + ) -> Option> { + let payload_resp = self.payload.as_ref()?.repl(masscanned, client_info, tcb)?; + let mut resp: Vec = Vec::new(); + let size = payload_resp.len() & 0x1ffff; // 7 first bits are 0 + resp.push(0x0); + // 7 bits reserved + 17 bits length + resp.push(((size as u32 >> 16) & 0xff).try_into().unwrap()); + resp.extend_from_slice(&((size & 0xffff) as u16).to_be_bytes()); + resp.extend(payload_resp); + Some(resp) + } +} + +impl NBTSession { + fn get_payload(&mut self) -> Option<&mut T> { + if self.payload.is_some() { + return self.payload.as_mut(); + } + self.payload = Some(T::new()); + self.payload.as_mut() + } +} + +////////// +// SMB1 // +////////// + +#[derive(Debug, Clone, Copy)] +enum SMB1HeaderState { + Start, + Command, + Status, + Flags, + Flags2, + PIDHigh, + SecuritySignature, + Reserved, + TID, + PIDLow, + UID, + MID, + End, +} + +#[derive(Debug, Clone)] +struct SMB1Header { + // DISSECTION + d: PacketDissector, + // STRUCT + start: [u8; 4], + command: u8, + status: u32, + flags: u8, + flags2: u16, + pid_high: u16, + security_signature: [u8; 8], + tid: u16, + pid_low: u16, + uid: u16, + mid: u16, + payload: Option, +} + +impl MPacket for SMB1Header { + fn new() -> SMB1Header { + Self { + d: PacketDissector::new(SMB1HeaderState::Start), + start: [0; 4], + command: 0, + status: 0, + flags: 0, + flags2: 0, + pid_high: 0, + security_signature: [0; 8], + tid: 0, + pid_low: 0, + uid: 0, + mid: 0, + payload: None, + } + } + + fn parse(&mut self, byte: &u8) { + match self.d.state { + SMB1HeaderState::Start => { + self.start[self.d.i] = *byte; + self.d.i += 1; + self.d + .next_state_when_i_reaches(SMB1HeaderState::Command, 4); + } + SMB1HeaderState::Command => { + self.command = *byte; + self.d.next_state(SMB1HeaderState::Status); + } + SMB1HeaderState::Status => { + self.status = self.d.read_ule32(byte, self.status, SMB1HeaderState::Flags); + } + SMB1HeaderState::Flags => { + self.flags = *byte; + self.d.next_state(SMB1HeaderState::Flags2); + } + SMB1HeaderState::Flags2 => { + self.flags2 = self + .d + .read_ule16(byte, self.flags2, SMB1HeaderState::PIDHigh); + } + SMB1HeaderState::PIDHigh => { + self.pid_high = + self.d + .read_ule16(byte, self.pid_high, SMB1HeaderState::SecuritySignature); + } + SMB1HeaderState::SecuritySignature => { + self.security_signature[self.d.i] = *byte; + self.d.i += 1; + self.d + .next_state_when_i_reaches(SMB1HeaderState::Reserved, 8); + } + SMB1HeaderState::Reserved => { + self.d.i += 1; + self.d.next_state_when_i_reaches(SMB1HeaderState::TID, 2); + } + SMB1HeaderState::TID => { + self.tid = self.d.read_ule16(byte, self.tid, SMB1HeaderState::PIDLow); + } + SMB1HeaderState::PIDLow => { + self.pid_low = self.d.read_ule16(byte, self.pid_low, SMB1HeaderState::UID); + } + SMB1HeaderState::UID => { + self.uid = self.d.read_ule16(byte, self.uid, SMB1HeaderState::MID); + } + SMB1HeaderState::MID => { + self.mid = self.d.read_ule16(byte, self.mid, SMB1HeaderState::End); + } + SMB1HeaderState::End => match self.get_payload() { + Some(pay) => pay.parse(byte), + None => return, + }, + } + } + + fn repl( + &self, + masscanned: &Masscanned, + client_info: &ClientInfo, + tcb: Option<&mut TCPControlBlock>, + ) -> Option> { + let payload_resp = self.payload.as_ref()?.repl(masscanned, client_info, tcb)?; + let mut resp: Vec = Vec::new(); + resp.extend_from_slice(b"\xffSMB"); // Start + resp.push(self.command); // Command + resp.extend_from_slice(&0_u32.to_le_bytes()); // Status + resp.push(0x98); // Flags = CASE_INSENSITIVE+CANONICALIZED_PATHS+REPLY + resp.extend_from_slice(&0xc807_u16.to_le_bytes()); // Flags2 = LONG_NAMES+EAS+SMB_SECURITY_SIGNATURE+EXTENDED_SECURITY+NT_STATUS+UNICODE + resp.extend_from_slice(&self.pid_high.to_le_bytes()); // PIDHigh + resp.extend_from_slice(&[0; 8]); // SecuritySignature + resp.extend_from_slice(&[0; 2]); // Reserved + resp.extend_from_slice(&self.tid.to_le_bytes()); // TID + resp.extend_from_slice(&self.pid_low.to_le_bytes()); // PIDLOW + resp.extend_from_slice(&self.uid.to_le_bytes()); // UID + resp.extend_from_slice(&self.mid.to_le_bytes()); // MID + resp.extend(payload_resp); + Some(resp) + } +} + +impl SMB1Header { + fn get_payload(&mut self) -> Option<&mut SMB1Payload> { + if self.payload.is_some() { + return self.payload.as_mut(); + } + if self.flags & 0x80 == 0x80 { + // Response + return None; + } + self.payload = Some(match self.command { + 0x72 => { + // Negotiate + SMB1Payload::NegotiateRequest(SMB1NegotiateRequest::new()) + } + 0x73 => { + // Setup + SMB1Payload::SessionSetupRequest(SMB1SessionSetupRequest::new()) + } + _ => None?, + }); + self.payload.as_mut() + } +} + +#[derive(Debug, Clone, PartialEq)] +struct SMB1Dialect { + buffer_format: u8, + dialect_string: String, +} + +#[derive(Debug, Clone, Copy)] +enum SMB1NegotiateRequestState { + WordCount, + ByteCount, + Dialects, + End, +} + +#[derive(Debug, Clone)] +struct SMB1NegotiateRequest { + // DISSECTION + d: PacketDissector, + _tmp_dialect: Option, + // STRUCT + word_count: u8, + byte_count: u16, + dialects: Vec, +} + +impl MPacket for SMB1NegotiateRequest { + fn new() -> SMB1NegotiateRequest { + Self { + d: PacketDissector::new(SMB1NegotiateRequestState::WordCount), + _tmp_dialect: None, + word_count: 0, + byte_count: 0, + dialects: Vec::new(), + } + } + + fn parse(&mut self, byte: &u8) { + match self.d.state { + SMB1NegotiateRequestState::WordCount => { + self.word_count = *byte; + self.d.next_state(SMB1NegotiateRequestState::ByteCount); + } + SMB1NegotiateRequestState::ByteCount => { + self.byte_count = + self.d + .read_ule16(byte, self.byte_count, SMB1NegotiateRequestState::Dialects); + } + SMB1NegotiateRequestState::Dialects => { + self.d.i += 1; + match self._tmp_dialect.as_mut() { + Some(dial) => { + if *byte == 0 { + // Final nul byte: dialect is finished + self.dialects.push(dial.clone()); + self._tmp_dialect = None; + self.d.next_state_when_i_reaches( + SMB1NegotiateRequestState::End, + self.byte_count as usize, + ); + } else { + dial.dialect_string.push(*byte as char); + } + } + None => { + self._tmp_dialect = Some(SMB1Dialect { + buffer_format: *byte, + dialect_string: String::new(), + }); + } + } + } + SMB1NegotiateRequestState::End => {} + } + } + + fn repl( + &self, + _masscanned: &Masscanned, + _client_info: &ClientInfo, + _tcb: Option<&mut TCPControlBlock>, + ) -> Option> { + if !matches!(self.d.state, SMB1NegotiateRequestState::End) { + return None; + } + let mut resp: Vec = Vec::new(); + let time: u64 = (EPOCH_1601 + + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs()) + * (1e7 as u64); + let mut dialect_index: u16 = 0; + let mut dialect_name = "Unknown"; + for dialect in ["NT LM 0.12", "SMB 2.???", "SMB 2.002"] { + dialect_index = match self + .dialects + .iter() + .position(|x| x.dialect_string.eq(dialect)) + { + Some(x) => { + dialect_name = dialect; + x as u16 + } + None => continue, + }; + break; + } + resp.push(17); // WordCount + resp.extend_from_slice(&dialect_index.to_le_bytes()); // DialectIndex + resp.push(3); // SecurityMode + resp.extend_from_slice(&50_u16.to_le_bytes()); // MaxMPXCount + resp.extend_from_slice(&50_u16.to_le_bytes()); // MaxNumberVC + resp.extend_from_slice(&0x10000_u32.to_le_bytes()); // MaxBufferSize + resp.extend_from_slice(&0x10000_u32.to_le_bytes()); // MaxRawSize + resp.extend_from_slice(&0x0_u32.to_le_bytes()); // SessionKey + resp.extend_from_slice(&0x8001e3fc_u32.to_le_bytes()); // ServerCapabilities = UNICODE+LARGE_FILES+NT_SMBS+RPC_REMOTE_APIS+STATUS32+LEVEL_II_OPLOCKS+LOCK_AND_READ+NT_FIND+INFOLEVEL_PASSTHRU+LARGE_READX+LARGE_WRITEX+LWIO+EXTENDED_SECURITY + resp.extend_from_slice(&time.to_le_bytes()); // ServerTime + resp.extend_from_slice(&0x3c_u16.to_le_bytes()); // ServerTimeZone + resp.push(0); // ChallengeLength + resp.extend_from_slice(&((SECURITY_BLOB_NEG_PROTO.len() + 16) as u16).to_le_bytes()); // ByteCount + // Challenge: Empty + resp.extend_from_slice(&[0_u8; 16]); // GUID + resp.extend_from_slice(SECURITY_BLOB_NEG_PROTO); // SecurityBlob + warn!("SMB1 Negotiate-Protocol-Reply ({})", dialect_name); + Some(resp) + } +} + +#[derive(Debug, Clone, Copy)] +enum SMB1SessionSetupRequestState { + WordCount, + AndXCommand, + AndXReserved, + AndXOffset, + MaxBufferSize, + MaxMPXCount, + VcNumber, + SessionKey, + SecurityBlobLength, + Reserved, + ServerCapabilities, + ByteCount, + SecurityBlob, + End, +} + +#[derive(Debug, Clone)] +struct SMB1SessionSetupRequest { + // DISSECTION + d: PacketDissector, + // STRUCT + word_count: u8, + and_x_command: u8, + and_x_offset: u16, + max_buffer_size: u16, + max_mpx_count: u16, + vc_number: u16, + session_key: u32, + security_len: u16, + server_capabilities: u32, + byte_count: u16, +} + +impl MPacket for SMB1SessionSetupRequest { + fn new() -> SMB1SessionSetupRequest { + Self { + d: PacketDissector::new(SMB1SessionSetupRequestState::WordCount), + word_count: 0, + and_x_command: 0, + and_x_offset: 0, + max_buffer_size: 0, + max_mpx_count: 0, + vc_number: 0, + session_key: 0, + security_len: 0, + server_capabilities: 0, + byte_count: 0, + } + } + fn parse(&mut self, byte: &u8) { + // We expect extended security because that's what we asked for in the NegotiateRequest + match self.d.state { + SMB1SessionSetupRequestState::WordCount => { + self.word_count = *byte; + self.d.next_state(SMB1SessionSetupRequestState::AndXCommand); + } + SMB1SessionSetupRequestState::AndXCommand => { + self.and_x_command = *byte; + self.d + .next_state(SMB1SessionSetupRequestState::AndXReserved); + } + SMB1SessionSetupRequestState::AndXReserved => { + self.d.next_state(SMB1SessionSetupRequestState::AndXOffset); + } + SMB1SessionSetupRequestState::AndXOffset => { + self.and_x_offset = self.d.read_ule16( + byte, + self.and_x_offset, + SMB1SessionSetupRequestState::MaxBufferSize, + ); + } + SMB1SessionSetupRequestState::MaxBufferSize => { + self.max_buffer_size = self.d.read_ule16( + byte, + self.max_buffer_size, + SMB1SessionSetupRequestState::MaxMPXCount, + ); + } + SMB1SessionSetupRequestState::MaxMPXCount => { + self.max_mpx_count = self.d.read_ule16( + byte, + self.max_mpx_count, + SMB1SessionSetupRequestState::VcNumber, + ); + } + SMB1SessionSetupRequestState::VcNumber => { + self.vc_number = self.d.read_ule16( + byte, + self.vc_number, + SMB1SessionSetupRequestState::SessionKey, + ); + } + SMB1SessionSetupRequestState::SessionKey => { + self.session_key = self.d.read_ule32( + byte, + self.session_key, + SMB1SessionSetupRequestState::SecurityBlobLength, + ); + } + SMB1SessionSetupRequestState::SecurityBlobLength => { + self.security_len = self.d.read_ule16( + byte, + self.security_len, + SMB1SessionSetupRequestState::Reserved, + ); + } + SMB1SessionSetupRequestState::Reserved => { + self.d.i += 1; + self.d + .next_state_when_i_reaches(SMB1SessionSetupRequestState::ServerCapabilities, 4); + } + SMB1SessionSetupRequestState::ServerCapabilities => { + self.server_capabilities = self.d.read_ule32( + byte, + self.server_capabilities, + SMB1SessionSetupRequestState::ByteCount, + ); + } + SMB1SessionSetupRequestState::ByteCount => { + self.byte_count = self.d.read_ule16( + byte, + self.byte_count, + SMB1SessionSetupRequestState::SecurityBlob, + ); + } + SMB1SessionSetupRequestState::SecurityBlob => { + self.d.i += 1; + self.d.next_state_when_i_reaches( + SMB1SessionSetupRequestState::End, + self.security_len as usize, + ); + } + SMB1SessionSetupRequestState::End => {} + } + } + fn repl( + &self, + _masscanned: &Masscanned, + _client_info: &ClientInfo, + _tcb: Option<&mut TCPControlBlock>, + ) -> Option> { + if !matches!(self.d.state, SMB1SessionSetupRequestState::End) { + return None; + } + // "Windows 4.0" in UTF-16 + two null bytes + let native_os = b"W\x00i\x00n\x00d\x00o\x00w\x00s\x00 \x004\x00.\x000\x00\x00\x00"; + let native_man_lan = native_os; + let mut resp: Vec = Vec::new(); + resp.push(0x4); // WordCount + resp.push(0xff); // AndXCommand + resp.push(0x0); // AndXReserved + resp.extend_from_slice(&0x44_u16.to_le_bytes()); // AndXOffset + resp.extend_from_slice(&0x0_u16.to_le_bytes()); // Action + resp.extend_from_slice(&(SECURITY_BLOB_CHALLENGE.len() as u16).to_le_bytes()); // SecurityLen + resp.extend_from_slice( + &((SECURITY_BLOB_CHALLENGE.len() + native_os.len() + native_man_lan.len()) as u16) + .to_le_bytes(), + ); // ByteCount + resp.extend_from_slice(SECURITY_BLOB_CHALLENGE); // SecurityBlob + resp.extend_from_slice(native_os); + resp.extend_from_slice(native_man_lan); + warn!("SMB1 SessionSetup-Reply"); + Some(resp) + } +} + +#[derive(Debug, Clone)] +enum SMB1Payload { + NegotiateRequest(SMB1NegotiateRequest), + SessionSetupRequest(SMB1SessionSetupRequest), +} + +impl SMB1Payload { + fn repl( + &self, + masscanned: &Masscanned, + client_info: &ClientInfo, + tcb: Option<&mut TCPControlBlock>, + ) -> Option> { + match self { + SMB1Payload::NegotiateRequest(x) => x.repl(masscanned, client_info, tcb), + SMB1Payload::SessionSetupRequest(x) => x.repl(masscanned, client_info, tcb), + } + } + fn parse(&mut self, byte: &u8) { + match self { + SMB1Payload::NegotiateRequest(x) => x.parse(byte), + SMB1Payload::SessionSetupRequest(x) => x.parse(byte), + } + } +} + +////////// +// SMB2 // +////////// + +#[derive(Debug, Clone, Copy)] +enum SMB2HeaderState { + Start, + StructureSize, + CreditsCharge, + Status, + Command, + CreditsRequested, + Flags, + NextCommand, + MessageId, + AsyncId, + SessionId, + SecuritySignature, + End, +} + +#[derive(Debug, Clone)] +struct SMB2Header { + // DISSECTION + d: PacketDissector, + // STRUCT + start: [u8; 4], + structure_size: u16, + credit_charge: u16, + status: u32, + command: u16, + credits_requested: u16, + flags: u32, + next_command: u32, + message_id: u64, + async_id: u64, + session_id: u64, + security_signature: [u8; 16], + // Payload + payload: Option, +} + +impl MPacket for SMB2Header { + fn new() -> SMB2Header { + SMB2Header { + d: PacketDissector::new(SMB2HeaderState::Start), + start: [0; 4], + structure_size: 0, + credit_charge: 0, + status: 0, + command: 0, + credits_requested: 0, + flags: 0, + next_command: 0, + message_id: 0, + async_id: 0, + session_id: 0, + security_signature: [0; 16], + payload: None, + } + } + + fn parse(&mut self, byte: &u8) { + match self.d.state { + SMB2HeaderState::Start => { + self.start[self.d.i] = *byte; + self.d.i += 1; + self.d + .next_state_when_i_reaches(SMB2HeaderState::StructureSize, 4); + } + SMB2HeaderState::StructureSize => { + self.structure_size = + self.d + .read_ule16(byte, self.structure_size, SMB2HeaderState::CreditsCharge) + } + SMB2HeaderState::CreditsCharge => { + self.credit_charge = + self.d + .read_ule16(byte, self.credit_charge, SMB2HeaderState::Status) + } + SMB2HeaderState::Status => { + self.status = self + .d + .read_ule32(byte, self.status, SMB2HeaderState::Command) + } + SMB2HeaderState::Command => { + self.command = + self.d + .read_ule16(byte, self.command, SMB2HeaderState::CreditsRequested) + } + SMB2HeaderState::CreditsRequested => { + self.credits_requested = + self.d + .read_ule16(byte, self.credits_requested, SMB2HeaderState::Flags) + } + SMB2HeaderState::Flags => { + self.flags = self + .d + .read_ule32(byte, self.flags, SMB2HeaderState::NextCommand) + } + SMB2HeaderState::NextCommand => { + self.next_command = + self.d + .read_ule32(byte, self.next_command, SMB2HeaderState::MessageId) + } + SMB2HeaderState::MessageId => { + self.message_id = self + .d + .read_ule64(byte, self.message_id, SMB2HeaderState::AsyncId) + } + SMB2HeaderState::AsyncId => { + self.async_id = self + .d + .read_ule64(byte, self.async_id, SMB2HeaderState::SessionId) + } + SMB2HeaderState::SessionId => { + self.session_id = + self.d + .read_ule64(byte, self.session_id, SMB2HeaderState::SecuritySignature) + } + SMB2HeaderState::SecuritySignature => { + self.security_signature[self.d.i] = *byte; + self.d.i += 1; + self.d.next_state_when_i_reaches(SMB2HeaderState::End, 16); + } + SMB2HeaderState::End => match self.get_payload() { + Some(pay) => pay.parse(byte), + None => return, + }, + } + } + + fn repl( + &self, + masscanned: &Masscanned, + client_info: &ClientInfo, + tcb: Option<&mut TCPControlBlock>, + ) -> Option> { + let payload_resp = self.payload.as_ref()?.repl(masscanned, client_info, tcb)?; + let mut resp: Vec = Vec::new(); + resp.extend_from_slice(b"\xfeSMB"); // Start + resp.extend_from_slice(&64_u16.to_le_bytes()); // StructureSize + resp.extend_from_slice(&0_u16.to_le_bytes()); // CreditCharge + resp.extend_from_slice(&0_u32.to_le_bytes()); // Status + resp.extend_from_slice(&self.command.to_le_bytes()); // Command + resp.extend_from_slice(&1_u16.to_le_bytes()); // CreditsRequested + resp.extend_from_slice(&1_u32.to_le_bytes()); // Flags = Response + resp.extend_from_slice(&0_u32.to_le_bytes()); // NextCommand + resp.extend_from_slice(&self.message_id.to_le_bytes()); // MessageId + resp.extend_from_slice(&self.async_id.to_le_bytes()); // AsyncId + resp.extend_from_slice(&self.session_id.to_le_bytes()); // SessionId + resp.extend_from_slice(&[0; 16]); // SecuritySignature + // Payload + resp.extend(payload_resp); + Some(resp) + } +} + +impl SMB2Header { + fn get_payload(&mut self) -> Option<&mut SMB2Payload> { + if let Some(_) = &self.payload { + return self.payload.as_mut(); + } + if self.flags & 1 == 1 { + // Response + return None; + } + self.payload = Some(match self.command { + 0x0000 => { + // Negotiate + SMB2Payload::NegotiateRequest(SMB2NegotiateRequest::new()) + } + 0x0001 => { + // Setup + SMB2Payload::SessionSetupRequest(SMB2SessionSetupRequest::new()) + } + _ => None?, + }); + self.payload.as_mut() + } +} + +#[derive(Debug, Clone, Copy)] +enum SMB2NegotiateRequestState { + StructureSize, + DialectCount, + SecurityMode, + Reserved, + Capabilities, + ClientGUID, + NegotiateAndReserved2, + Dialects, + End, +} + +#[derive(Debug, Clone)] +struct SMB2NegotiateRequest { + // DISSECTION + d: PacketDissector, + _tmp_dialect: u16, + // STRUCT + structure_size: u16, + dialect_count: u16, + security_mode: u16, + capabilities: u32, + client_guid: [u8; 16], + dialects: HashSet, +} +const EPOCH_1601: u64 = 11644473600; + +impl MPacket for SMB2NegotiateRequest { + fn new() -> Self { + SMB2NegotiateRequest { + d: PacketDissector::new(SMB2NegotiateRequestState::StructureSize), + _tmp_dialect: 0, + structure_size: 0, + dialect_count: 0, + security_mode: 0, + capabilities: 0, + client_guid: [0; 16], + dialects: HashSet::new(), + } + } + + fn parse(&mut self, byte: &u8) { + match self.d.state { + SMB2NegotiateRequestState::StructureSize => { + self.structure_size = self.d.read_ule16( + byte, + self.structure_size, + SMB2NegotiateRequestState::DialectCount, + ); + } + SMB2NegotiateRequestState::DialectCount => { + self.dialect_count = self.d.read_ule16( + byte, + self.dialect_count, + SMB2NegotiateRequestState::SecurityMode, + ); + } + SMB2NegotiateRequestState::SecurityMode => { + self.security_mode = self.d.read_ule16( + byte, + self.security_mode, + SMB2NegotiateRequestState::Reserved, + ); + } + SMB2NegotiateRequestState::Reserved => { + self.d.i += 1; + self.d + .next_state_when_i_reaches(SMB2NegotiateRequestState::Capabilities, 2); + } + SMB2NegotiateRequestState::Capabilities => { + self.capabilities = self.d.read_ule32( + byte, + self.capabilities, + SMB2NegotiateRequestState::ClientGUID, + ); + } + SMB2NegotiateRequestState::ClientGUID => { + self.client_guid[self.d.i] = *byte; + self.d.i += 1; + self.d.next_state_when_i_reaches( + SMB2NegotiateRequestState::NegotiateAndReserved2, + 16, + ); + } + SMB2NegotiateRequestState::NegotiateAndReserved2 => { + self.d.i += 1; + self.d + .next_state_when_i_reaches(SMB2NegotiateRequestState::Dialects, 8); + } + SMB2NegotiateRequestState::Dialects => { + self._tmp_dialect = + self.d + .read_ule16(byte, self._tmp_dialect, SMB2NegotiateRequestState::Dialects); + if self.d.i == 0 { + // Add to dialects list when finished + self.dialects.insert(self._tmp_dialect); + self._tmp_dialect = 0; + // Check if dialects list is finished + if self.dialects.len() == self.dialect_count as usize { + self.d.state = SMB2NegotiateRequestState::End; + } + } + } + SMB2NegotiateRequestState::End => { + return; + } + } + } + fn repl( + &self, + _masscanned: &Masscanned, + _client_info: &ClientInfo, + _tcb: Option<&mut TCPControlBlock>, + ) -> Option> { + if !matches!(self.d.state, SMB2NegotiateRequestState::End) { + return None; + } + let mut resp: Vec = Vec::new(); + let time: u64 = (EPOCH_1601 + + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs()) + * (1e7 as u64); + // Chose dialect + let smb2_versions = [ + (0x0202, "SMB 2.002"), + (0x0210, "SMB 2.1"), + (0x02ff, "SMB 2.???"), + (0x0300, "SMB 3.0"), + (0x0302, "SMB 2.0.2"), + (0x0310, "SMB 3.1.0"), + (0x0311, "SMB 3.1.1"), + ]; + let mut dialect = None; + let mut dialect_name = "Unknown"; + if let Some(smb_ver) = smb2_versions + .iter() + .find(|(d, _)| self.dialects.contains(d)) + { + dialect = Some(smb_ver.0); + dialect_name = smb_ver.1; + } + resp.extend_from_slice(&0x41_u16.to_le_bytes()); // StructureSize + resp.extend_from_slice(&0x1_u16.to_le_bytes()); // SecurityMode + resp.extend_from_slice(&dialect?.to_le_bytes()); // DialectRevision + resp.extend_from_slice(&0x1_u16.to_le_bytes()); // NegotiateCount + resp.extend_from_slice(&self.client_guid); // GUID + resp.extend_from_slice(&0x1_u32.to_le_bytes()); // Capabilities + resp.extend_from_slice(&0x10000_u32.to_le_bytes()); // MaxTransactionSize + resp.extend_from_slice(&0x10000_u32.to_le_bytes()); // MaxReadSize + resp.extend_from_slice(&0x10000_u32.to_le_bytes()); // MaxWriteSize + resp.extend_from_slice(&time.to_le_bytes()); // ServerTime + resp.extend_from_slice(&time.to_le_bytes()); // ServerStartTime + resp.extend_from_slice(&0x80_u16.to_le_bytes()); // SecurityBloboffset + resp.extend_from_slice(&(SECURITY_BLOB_NEG_PROTO.len() as u16).to_le_bytes()); // SecurityBlobLength + resp.extend_from_slice(&0x0_u32.to_le_bytes()); // NegotiateContextOffset + resp.extend_from_slice(SECURITY_BLOB_NEG_PROTO); // SecurityBlob + warn!("SMB2 Negotiate-Protocol-Reply ({})", dialect_name); + Some(resp) + } +} + +#[derive(Debug, Clone, Copy)] +enum SMB2SetupRequestState { + StructureSize, + Flags, + SecurityMode, + Capabilities, + Channel, + SecurityBufferOffset, + SecurityLen, + PreviousSessionId, + SecurityBlob, + End, +} + +#[derive(Debug, Clone)] +struct SMB2SessionSetupRequest { + // DISSECTION + d: PacketDissector, + // STRUCT + structure_size: u16, + flags: u8, + security_mode: u8, + capabilities: u32, + channel: u32, + security_buffer_offset: u16, + security_len: u16, + previous_session_id: u64, +} +impl MPacket for SMB2SessionSetupRequest { + fn new() -> Self { + SMB2SessionSetupRequest { + d: PacketDissector::new(SMB2SetupRequestState::StructureSize), + structure_size: 0, + flags: 0, + security_mode: 0, + capabilities: 0, + channel: 0, + security_buffer_offset: 0, + security_len: 0, + previous_session_id: 0, + } + } + + fn parse(&mut self, byte: &u8) { + match self.d.state { + SMB2SetupRequestState::StructureSize => { + self.structure_size = + self.d + .read_ule16(byte, self.structure_size, SMB2SetupRequestState::Flags); + } + SMB2SetupRequestState::Flags => { + self.flags = *byte; + self.d.next_state(SMB2SetupRequestState::SecurityMode); + } + SMB2SetupRequestState::SecurityMode => { + self.security_mode = *byte; + self.d.next_state(SMB2SetupRequestState::Capabilities); + } + SMB2SetupRequestState::Capabilities => { + self.capabilities = + self.d + .read_ule32(byte, self.capabilities, SMB2SetupRequestState::Channel); + } + SMB2SetupRequestState::Channel => { + self.channel = self.d.read_ule32( + byte, + self.channel, + SMB2SetupRequestState::SecurityBufferOffset, + ); + } + SMB2SetupRequestState::SecurityBufferOffset => { + self.security_buffer_offset = self.d.read_ule16( + byte, + self.security_buffer_offset, + SMB2SetupRequestState::SecurityLen, + ); + } + SMB2SetupRequestState::SecurityLen => { + self.security_len = self.d.read_ule16( + byte, + self.security_len, + SMB2SetupRequestState::PreviousSessionId, + ); + } + SMB2SetupRequestState::PreviousSessionId => { + self.previous_session_id = self.d.read_ule64( + byte, + self.previous_session_id, + SMB2SetupRequestState::SecurityBlob, + ); + } + SMB2SetupRequestState::SecurityBlob => { + self.d.i += 1; + self.d.next_state_when_i_reaches( + SMB2SetupRequestState::End, + self.security_len as usize, + ); + } + SMB2SetupRequestState::End => {} + } + } + + fn repl( + &self, + _masscanned: &Masscanned, + _client_info: &ClientInfo, + _tcb: Option<&mut TCPControlBlock>, + ) -> Option> { + if !matches!(self.d.state, SMB2SetupRequestState::End) { + return None; + } + let mut resp: Vec = Vec::new(); + resp.extend_from_slice(&0x9_u16.to_le_bytes()); // StructureSize + resp.extend_from_slice(&0x0_u16.to_le_bytes()); // SessionFlags + resp.extend_from_slice(&0x48_u16.to_le_bytes()); // SecurityBufferOffset + resp.extend_from_slice(&(SECURITY_BLOB_CHALLENGE.len() as u16).to_le_bytes()); // SecurityLen + resp.extend_from_slice(SECURITY_BLOB_CHALLENGE); // SecurityBlob + warn!("SMB2 SessionSetup-Reply"); + Some(resp) + } +} + +#[derive(Debug, Clone)] +enum SMB2Payload { + NegotiateRequest(SMB2NegotiateRequest), + SessionSetupRequest(SMB2SessionSetupRequest), +} + +impl SMB2Payload { + fn repl( + &self, + masscanned: &Masscanned, + client_info: &ClientInfo, + tcb: Option<&mut TCPControlBlock>, + ) -> Option> { + match self { + SMB2Payload::NegotiateRequest(x) => x.repl(masscanned, client_info, tcb), + SMB2Payload::SessionSetupRequest(x) => x.repl(masscanned, client_info, tcb), + } + } + fn parse(&mut self, byte: &u8) { + match self { + SMB2Payload::NegotiateRequest(x) => x.parse(byte), + SMB2Payload::SessionSetupRequest(x) => x.parse(byte), + } + } +} + +////////////// +// Handlers // +////////////// + +pub fn repl_smb1<'a>( + data: &'a [u8], + masscanned: &Masscanned, + client_info: &ClientInfo, + tcb: Option<&mut TCPControlBlock>, +) -> Option> { + let mut nbtsession: NBTSession = NBTSession::new(); + for byte in data { + nbtsession.parse(byte); + } + nbtsession.repl(masscanned, client_info, tcb) +} + +pub fn repl_smb2<'a>( + data: &'a [u8], + masscanned: &Masscanned, + client_info: &ClientInfo, + tcb: Option<&mut TCPControlBlock>, +) -> Option> { + let mut nbtsession: NBTSession = NBTSession::new(); + for byte in data { + nbtsession.parse(byte); + } + nbtsession.repl(masscanned, client_info, tcb) +} + +/////////// +// Tests // +/////////// + +#[cfg(test)] +mod tests { + use super::*; + use crate::logger::MetaLogger; + + use itertools::assert_equal; + use pnet::util::MacAddr; + use std::str::FromStr; + + // Sent by `smbclient -U "" -N -L 10.1.1.1 -d10 --option='client min protocol=NT1'` + const SMB1_REQ_NEGOTIATE: &[u8] = b"\x00\x00\x00T\xffSMBr\x00\x00\x00\x00\x18C\xc8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfe\xff\x00\x00\x00\x00\x001\x00\x02NT LANMAN 1.0\x00\x02NT LM 0.12\x00\x02SMB 2.002\x00\x02SMB 2.???\x00"; + const SMB1_REQ_SESSION_SETUP: &[u8] = b"\x00\x00\x00\x9c\xffSMBs\x00\x00\x00\x00\x18C\xc8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x89T\x00\x00\x01\x00\x0c\xff\x00\x00\x00\xff\xff\x02\x00\x01\x00\x00\x00\x00\x00J\x00\x00\x00\x00\x00T\xc0\x00\x80a\x00`H\x06\x06+\x06\x01\x05\x05\x02\xa0>0<\xa0\x0e0\x0c\x06\n+\x06\x01\x04\x01\x827\x02\x02\n\xa2*\x04(NTLMSSP\x00\x01\x00\x00\x00\x15\x82\x08b\x00\x00\x00\x00(\x00\x00\x00\x00\x00\x00\x00(\x00\x00\x00\x06\x01\x00\x00\x00\x00\x00\x0f\x00U\x00n\x00i\x00x\x00\x00\x00S\x00a\x00m\x00b\x00a\x00\x00\x00"; + // Sent by `smbclient -U "" -N -L 10.1.1.1 -d10` + const SMB2_REQ_NEGOTIATE: &[u8] = b"\x00\x00\x00\xd0\xfeSMB@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\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\x08\x00\x01\x00\x00\x00\x7f\x00\x00\x00\rr3\x97\"c\x8fA\x9f\xe0\xbawQ\x87rbx\x00\x00\x00\x03\x00\x00\x00\x02\x02\x10\x02\"\x02$\x02\x00\x03\x02\x03\x10\x03\x11\x03\x00\x00\x00\x00\x01\x00&\x00\x00\x00\x00\x00\x01\x00 \x00\x01\x00\xd5Z\x89\x87>\x80\xcd\x02\xc2\xab\x08\xa3\xf4\x94\xb6A\x05\x11V\xeeE\x19p\x19\xed\x17v\xda\x9b\x08\x99V\x00\x00\x02\x00\x06\x00\x00\x00\x00\x00\x02\x00\x02\x00\x01\x00\x00\x00\x05\x00\x10\x00\x00\x00\x00\x001\x000\x00.\x001\x00.\x001\x00.\x001\x00"; + const SMB2_REQ_SESSION_SETUP: &[u8] = b"\x00\x00\x00\xa2\xfeSMB@\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x01\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\x19\x00\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00X\x00J\x00\x00\x00\x00\x00\x00\x00\x00\x00`H\x06\x06+\x06\x01\x05\x05\x02\xa0>0<\xa0\x0e0\x0c\x06\n+\x06\x01\x04\x01\x827\x02\x02\n\xa2*\x04(NTLMSSP\x00\x01\x00\x00\x00\x15\x82\x08b\x00\x00\x00\x00(\x00\x00\x00\x00\x00\x00\x00(\x00\x00\x00\x06\x01\x00\x00\x00\x00\x00\x0f"; + // You can dissect any of those payloads with Scapy using NBTSession(b"...") + + #[test] + fn test_smb1_protocol_nego_parsing() { + let mut nbtsession: NBTSession = NBTSession::new(); + nbtsession.parse_all(SMB1_REQ_NEGOTIATE); + assert_eq!(nbtsession.nb_type, 0); + assert_eq!(nbtsession.length, 0x54); + let smb1 = nbtsession.payload.expect("Error while unpacking SMB"); + assert_eq!(&smb1.start, b"\xffSMB"); + assert_eq!(smb1.command, 0x72); + assert_eq!(smb1.status, 0); + assert_eq!(smb1.flags, 24); + assert_eq!(smb1.flags2, 51267); + assert_eq!(smb1.pid_high, 0); + assert_eq!(smb1.security_signature, [0; 8]); + assert_eq!(smb1.tid, 0); + assert_eq!(smb1.pid_low, 65534); + assert_eq!(smb1.uid, 0); + assert_eq!(smb1.mid, 0); + let neg_request = match smb1.payload.expect("Error while reading payload") { + SMB1Payload::NegotiateRequest(x) => x, + _ => panic!("Bad payload"), + }; + assert_eq!(neg_request.word_count, 0); + assert_eq!(neg_request.byte_count, 49); + assert_equal( + neg_request.dialects, + Vec::from([ + SMB1Dialect { + buffer_format: 2, + dialect_string: "NT LANMAN 1.0".to_string(), + }, + SMB1Dialect { + buffer_format: 2, + dialect_string: "NT LM 0.12".to_string(), + }, + SMB1Dialect { + buffer_format: 2, + dialect_string: "SMB 2.002".to_string(), + }, + SMB1Dialect { + buffer_format: 2, + dialect_string: "SMB 2.???".to_string(), + }, + ]), + ); + } + #[test] + fn test_smb1_protocol_nego_reply() { + let masscanned = Masscanned { + synack_key: [0, 0], + mac: MacAddr::from_str("00:00:00:00:00:00").expect("error parsing default MAC address"), + iface: None, + self_ip_list: None, + remote_ip_deny_list: None, + log: MetaLogger::new(), + }; + let client_info = ClientInfo::new(); + let answer = repl_smb1(SMB1_REQ_NEGOTIATE, &masscanned, &client_info, None) + .expect("Error: no answer"); + let expected = [ + 0, 0, 1, 149, 255, 83, 77, 66, 114, 0, 0, 0, 0, 152, 7, 200, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 254, 255, 0, 0, 0, 0, 17, 1, 0, 3, 50, 0, 50, 0, 0, 0, 1, 0, 0, 0, 1, 0, + 0, 0, 0, 0, 252, 227, 1, 128, 0, 250, 218, 34, 238, 28, 216, 1, 60, 0, 0, 80, 1, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 96, 130, 1, 60, 6, 6, 43, 6, 1, 5, 5, 2, 160, + 130, 1, 48, 48, 130, 1, 44, 160, 26, 48, 24, 6, 10, 43, 6, 1, 4, 1, 130, 55, 2, 2, 30, + 6, 10, 43, 6, 1, 4, 1, 130, 55, 2, 2, 10, 162, 130, 1, 12, 4, 130, 1, 8, 78, 69, 71, + 79, 69, 88, 84, 83, 1, 0, 0, 0, 0, 0, 0, 0, 96, 0, 0, 0, 112, 0, 0, 0, 49, 60, 42, 58, + 199, 43, 60, 169, 109, 172, 56, 116, 167, 221, 29, 91, 244, 82, 107, 23, 3, 138, 75, + 145, 194, 9, 125, 154, 143, 230, 44, 150, 92, 81, 36, 47, 144, 77, 71, 199, 173, 143, + 135, 107, 34, 2, 191, 198, 0, 0, 0, 0, 0, 0, 0, 0, 96, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 92, 51, 83, 13, 234, 249, 13, 77, 178, 236, 74, 227, 120, 110, 195, 8, 78, + 69, 71, 79, 69, 88, 84, 83, 3, 0, 0, 0, 1, 0, 0, 0, 64, 0, 0, 0, 152, 0, 0, 0, 49, 60, + 42, 58, 199, 43, 60, 169, 109, 172, 56, 116, 167, 221, 29, 91, 92, 51, 83, 13, 234, + 249, 13, 77, 178, 236, 74, 227, 120, 110, 195, 8, 64, 0, 0, 0, 88, 0, 0, 0, 48, 86, + 160, 84, 48, 82, 48, 39, 128, 37, 48, 35, 49, 33, 48, 31, 6, 3, 85, 4, 3, 19, 24, 84, + 111, 107, 101, 110, 32, 83, 105, 103, 110, 105, 110, 103, 32, 80, 117, 98, 108, 105, + 99, 32, 75, 101, 121, 48, 39, 128, 37, 48, 35, 49, 33, 48, 31, 6, 3, 85, 4, 3, 19, 24, + 84, 111, 107, 101, 110, 32, 83, 105, 103, 110, 105, 110, 103, 32, 80, 117, 98, 108, + 105, 99, 32, 75, 101, 121, + ]; + assert_eq!(answer[..0x3c], expected[..0x3c]); // Test equality except "ServerTime" field + assert_eq!(answer[0x3c + 8..], expected[0x3c + 8..]); + } + #[test] + fn test_smb1_session_setup_request_parse() { + let mut nbtsession: NBTSession = NBTSession::new(); + nbtsession.parse_all(SMB1_REQ_SESSION_SETUP); + assert_eq!(nbtsession.nb_type, 0); + assert_eq!(nbtsession.length, 0x9c); + let smb1 = nbtsession.payload.expect("Error while unpacking SMB"); + assert_eq!(&smb1.start, b"\xffSMB"); + assert_eq!(smb1.command, 0x73); + assert_eq!(smb1.status, 0); + assert_eq!(smb1.flags, 24); + assert_eq!(smb1.flags2, 0xc843); + assert_eq!(smb1.pid_high, 0); + assert_eq!(smb1.tid, 0); + assert_eq!(smb1.pid_low, 21641); + assert_eq!(smb1.uid, 0); + assert_eq!(smb1.mid, 1); + let sess_setup_req = match smb1.payload.expect("Error while reading payload") { + SMB1Payload::SessionSetupRequest(x) => x, + _ => panic!("Bad type"), + }; + assert_eq!(sess_setup_req.word_count, 12); + assert_eq!(sess_setup_req.and_x_command, 0xff); + assert_eq!(sess_setup_req.and_x_offset, 0); + assert_eq!(sess_setup_req.max_buffer_size, 0xffff); + assert_eq!(sess_setup_req.max_mpx_count, 2); + assert_eq!(sess_setup_req.vc_number, 1); + assert_eq!(sess_setup_req.session_key, 0); + assert_eq!(sess_setup_req.security_len, 74); + assert_eq!(sess_setup_req.server_capabilities, 0x8000c054); + assert_eq!(sess_setup_req.server_capabilities, 0x8000c054); + } + #[test] + fn test_smb1_session_setup_request_reply() { + let masscanned = Masscanned { + synack_key: [0, 0], + mac: MacAddr::from_str("00:00:00:00:00:00").expect("error parsing default MAC address"), + iface: None, + self_ip_list: None, + remote_ip_deny_list: None, + log: MetaLogger::new(), + }; + let client_info = ClientInfo::new(); + let answer = repl_smb1(SMB1_REQ_SESSION_SETUP, &masscanned, &client_info, None) + .expect("Error: no answer"); + let expected = [ + 0, 0, 0, 250, 255, 83, 77, 66, 115, 0, 0, 0, 0, 152, 7, 200, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 137, 84, 0, 0, 1, 0, 4, 255, 0, 68, 0, 0, 0, 159, 0, 207, 0, 161, 129, + 156, 48, 129, 153, 160, 3, 10, 1, 1, 161, 12, 6, 10, 43, 6, 1, 4, 1, 130, 55, 2, 2, 10, + 162, 129, 131, 4, 129, 128, 78, 84, 76, 77, 83, 83, 80, 0, 2, 0, 0, 0, 8, 0, 8, 0, 56, + 0, 0, 0, 21, 130, 138, 226, 36, 145, 168, 246, 243, 137, 45, 52, 0, 0, 0, 0, 0, 0, 0, + 0, 64, 0, 64, 0, 64, 0, 0, 0, 10, 0, 97, 74, 0, 0, 0, 15, 87, 0, 73, 0, 78, 0, 49, 0, + 2, 0, 8, 0, 87, 0, 73, 0, 78, 0, 49, 0, 1, 0, 8, 0, 87, 0, 73, 0, 78, 0, 49, 0, 4, 0, + 8, 0, 87, 0, 73, 0, 78, 0, 49, 0, 3, 0, 8, 0, 87, 0, 73, 0, 78, 0, 49, 0, 7, 0, 8, 0, + 255, 38, 57, 245, 66, 29, 216, 1, 0, 0, 0, 0, 87, 0, 105, 0, 110, 0, 100, 0, 111, 0, + 119, 0, 115, 0, 32, 0, 52, 0, 46, 0, 48, 0, 0, 0, 87, 0, 105, 0, 110, 0, 100, 0, 111, + 0, 119, 0, 115, 0, 32, 0, 52, 0, 46, 0, 48, 0, 0, 0, + ]; + assert_eq!(answer, expected); + } + #[test] + fn test_smb2_protocol_nego_parsing() { + let mut nbtsession: NBTSession = NBTSession::new(); + nbtsession.parse_all(SMB2_REQ_NEGOTIATE); + assert_eq!(nbtsession.nb_type, 0); + assert_eq!(nbtsession.length, 0xd0); + let smb2 = nbtsession.payload.expect("No SMB2 payload found !"); + assert_eq!(&smb2.start, b"\xfeSMB"); + assert_eq!(smb2.structure_size, 64); + assert_eq!(smb2.credit_charge, 0); + assert_eq!(smb2.status, 0); + assert_eq!(smb2.command, 0); + assert_eq!(smb2.credits_requested, 31); + assert_eq!(smb2.flags, 0); + assert_eq!(smb2.next_command, 0); + assert_eq!(smb2.message_id, 0); + assert_eq!(smb2.async_id, 0); + assert_eq!(smb2.session_id, 0); + assert_eq!(smb2.security_signature, [0; 16]); + let neg_request = match smb2.payload.expect("Error while reading payload") { + SMB2Payload::NegotiateRequest(x) => x, + _ => panic!("Invalid payload type"), + }; + assert_eq!(neg_request.structure_size, 36); + assert_eq!(neg_request.dialect_count, 8); + assert_eq!(neg_request.security_mode, 1); + assert_eq!(neg_request.capabilities, 127); + assert_eq!( + neg_request.client_guid, + [13, 114, 51, 151, 34, 99, 143, 65, 159, 224, 186, 119, 81, 135, 114, 98] + ); + assert_eq!( + neg_request.dialects, + HashSet::from([514, 528, 546, 548, 768, 770, 784, 785]) + ); + } + #[test] + fn test_smb2_protocol_nego_reply() { + let masscanned = Masscanned { + synack_key: [0, 0], + mac: MacAddr::from_str("00:00:00:00:00:00").expect("error parsing default MAC address"), + iface: None, + self_ip_list: None, + remote_ip_deny_list: None, + log: MetaLogger::new(), + }; + let client_info = ClientInfo::new(); + let answer = repl_smb2(SMB2_REQ_NEGOTIATE, &masscanned, &client_info, None) + .expect("Error: no answer"); + let expected = [ + 0, 0, 1, 192, 254, 83, 77, 66, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 0, 1, 0, 2, 2, 1, 0, 13, 114, 51, 151, 34, + 99, 143, 65, 159, 224, 186, 119, 81, 135, 114, 98, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, + 0, 0, 1, 0, 0, 103, 222, 3, 242, 28, 216, 1, 0, 103, 222, 3, 242, 28, 216, 1, 128, 0, + 64, 1, 0, 0, 0, 0, 96, 130, 1, 60, 6, 6, 43, 6, 1, 5, 5, 2, 160, 130, 1, 48, 48, 130, + 1, 44, 160, 26, 48, 24, 6, 10, 43, 6, 1, 4, 1, 130, 55, 2, 2, 30, 6, 10, 43, 6, 1, 4, + 1, 130, 55, 2, 2, 10, 162, 130, 1, 12, 4, 130, 1, 8, 78, 69, 71, 79, 69, 88, 84, 83, 1, + 0, 0, 0, 0, 0, 0, 0, 96, 0, 0, 0, 112, 0, 0, 0, 49, 60, 42, 58, 199, 43, 60, 169, 109, + 172, 56, 116, 167, 221, 29, 91, 244, 82, 107, 23, 3, 138, 75, 145, 194, 9, 125, 154, + 143, 230, 44, 150, 92, 81, 36, 47, 144, 77, 71, 199, 173, 143, 135, 107, 34, 2, 191, + 198, 0, 0, 0, 0, 0, 0, 0, 0, 96, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 92, 51, + 83, 13, 234, 249, 13, 77, 178, 236, 74, 227, 120, 110, 195, 8, 78, 69, 71, 79, 69, 88, + 84, 83, 3, 0, 0, 0, 1, 0, 0, 0, 64, 0, 0, 0, 152, 0, 0, 0, 49, 60, 42, 58, 199, 43, 60, + 169, 109, 172, 56, 116, 167, 221, 29, 91, 92, 51, 83, 13, 234, 249, 13, 77, 178, 236, + 74, 227, 120, 110, 195, 8, 64, 0, 0, 0, 88, 0, 0, 0, 48, 86, 160, 84, 48, 82, 48, 39, + 128, 37, 48, 35, 49, 33, 48, 31, 6, 3, 85, 4, 3, 19, 24, 84, 111, 107, 101, 110, 32, + 83, 105, 103, 110, 105, 110, 103, 32, 80, 117, 98, 108, 105, 99, 32, 75, 101, 121, 48, + 39, 128, 37, 48, 35, 49, 33, 48, 31, 6, 3, 85, 4, 3, 19, 24, 84, 111, 107, 101, 110, + 32, 83, 105, 103, 110, 105, 110, 103, 32, 80, 117, 98, 108, 105, 99, 32, 75, 101, 121, + ]; + assert_eq!(answer[..0x6c], expected[..0x6c]); // Test equality except the 2 "ServerTime" fields + assert_eq!(answer[0x6c + 16..], expected[0x6c + 16..]); + } + #[test] + fn test_smb2_session_setup_request_parse() { + let mut nbtsession: NBTSession = NBTSession::new(); + nbtsession.parse_all(SMB2_REQ_SESSION_SETUP); + assert_eq!(nbtsession.nb_type, 0); + assert_eq!(nbtsession.length, 0xa2); + let smb2 = nbtsession.payload.expect("Error while unpacking SMB"); + assert_eq!(&smb2.start, b"\xfeSMB"); + assert_eq!(smb2.command, 1); + assert_eq!(smb2.status, 0); + assert_eq!(smb2.flags, 0); + let sess_setup_req = match smb2.payload.expect("Error while reading payload") { + SMB2Payload::SessionSetupRequest(x) => x, + _ => panic!("Bad type"), + }; + assert_eq!(sess_setup_req.structure_size, 0x19); + assert_eq!(sess_setup_req.flags, 0); + assert_eq!(sess_setup_req.security_mode, 1); + assert_eq!(sess_setup_req.capabilities, 1); + assert_eq!(sess_setup_req.channel, 0); + assert_eq!(sess_setup_req.security_buffer_offset, 0x58); + assert_eq!(sess_setup_req.security_len, 74); + assert_eq!(sess_setup_req.previous_session_id, 0); + } + #[test] + fn test_smb2_session_setup_request_reply() { + let masscanned = Masscanned { + synack_key: [0, 0], + mac: MacAddr::from_str("00:00:00:00:00:00").expect("error parsing default MAC address"), + iface: None, + self_ip_list: None, + remote_ip_deny_list: None, + log: MetaLogger::new(), + }; + let client_info = ClientInfo::new(); + let answer = repl_smb2(SMB2_REQ_SESSION_SETUP, &masscanned, &client_info, None) + .expect("Error: no answer"); + let expected = [ + 0, 0, 0, 231, 254, 83, 77, 66, 64, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, + 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 72, 0, 159, 0, 161, 129, 156, 48, + 129, 153, 160, 3, 10, 1, 1, 161, 12, 6, 10, 43, 6, 1, 4, 1, 130, 55, 2, 2, 10, 162, + 129, 131, 4, 129, 128, 78, 84, 76, 77, 83, 83, 80, 0, 2, 0, 0, 0, 8, 0, 8, 0, 56, 0, 0, + 0, 21, 130, 138, 226, 36, 145, 168, 246, 243, 137, 45, 52, 0, 0, 0, 0, 0, 0, 0, 0, 64, + 0, 64, 0, 64, 0, 0, 0, 10, 0, 97, 74, 0, 0, 0, 15, 87, 0, 73, 0, 78, 0, 49, 0, 2, 0, 8, + 0, 87, 0, 73, 0, 78, 0, 49, 0, 1, 0, 8, 0, 87, 0, 73, 0, 78, 0, 49, 0, 4, 0, 8, 0, 87, + 0, 73, 0, 78, 0, 49, 0, 3, 0, 8, 0, 87, 0, 73, 0, 78, 0, 49, 0, 7, 0, 8, 0, 255, 38, + 57, 245, 66, 29, 216, 1, 0, 0, 0, 0, + ]; + assert_eq!(answer, expected); + } +} diff --git a/src/proto/ssh.rs b/src/proto/ssh.rs index a14c6d9..82ecd1b 100644 --- a/src/proto/ssh.rs +++ b/src/proto/ssh.rs @@ -16,21 +16,635 @@ use log::*; -use std::str; - use crate::client::ClientInfo; +use crate::proto::TCPControlBlock; +use crate::utils::byte2str; use crate::Masscanned; -pub const SSH_PATTERN_CLIENT_PROTOCOL: &[u8; 7] = b"SSH-2.0"; +pub const SSH_PATTERN_CLIENT_PROTOCOL_2: &[u8; 7] = b"SSH-2.0"; +pub const SSH_PATTERN_CLIENT_PROTOCOL_1: &[u8; 8] = b"SSH-1.99"; + +const SSH_STATE_START: usize = 0; +const SSH_STATE_S1: usize = 1; +const SSH_STATE_S2: usize = 2; +const SSH_STATE_H: usize = 3; +const SSH_STATE_DASH: usize = 4; +const SSH_STATE_VERSION: usize = 5; +const SSH_STATE_SOFTWARE: usize = 6; +const SSH_STATE_COMMENT: usize = 7; +const SSH_STATE_EOB: usize = 8; +const SSH_STATE_LF: usize = 9; + +const SSH_STATE_FAIL: usize = 0xFFFF; + +struct ProtocolState { + state: usize, + prev_state: usize, + ssh_version: Vec, + ssh_software: Vec, + ssh_comment: Vec, +} + +impl ProtocolState { + fn new() -> Self { + ProtocolState { + state: SSH_STATE_START, + prev_state: SSH_STATE_START, + ssh_version: Vec::::new(), + ssh_software: Vec::::new(), + ssh_comment: Vec::::new(), + } + } +} + +fn ssh_parse(pstate: &mut ProtocolState, data: &[u8]) { + /* RFC 4253: + * + * 4.2. Protocol Version Exchange + * + * When the connection has been established, both sides MUST send an + * identification string. This identification string MUST be + * + * SSH-protoversion-softwareversion SP comments CR LF + * + * Since the protocol being defined in this set of documents is version + * 2.0, the 'protoversion' MUST be "2.0". The 'comments' string is + * OPTIONAL. If the 'comments' string is included, a 'space' character + * (denoted above as SP, ASCII 32) MUST separate the 'softwareversion' + * and 'comments' strings. The identification MUST be terminated by a + * single Carriage Return (CR) and a single Line Feed (LF) character + * (ASCII 13 and 10, respectively). Implementers who wish to maintain + * compatibility with older, undocumented versions of this protocol may + * want to process the identification string without expecting the + * presence of the carriage return character for reasons described in + * Section 5 of this document. The null character MUST NOT be sent. + * The maximum length of the string is 255 characters, including the + * Carriage Return and Line Feed. + */ + let mut i = 0; + while i < data.len() { + match pstate.state { + SSH_STATE_START => { + pstate.state = SSH_STATE_S1; + continue; + } + /* first bytes should be "SSH-" */ + SSH_STATE_S1 | SSH_STATE_S2 | SSH_STATE_H | SSH_STATE_DASH => { + if data[i] != b"SSH-"[pstate.state - SSH_STATE_S1] { + pstate.state = SSH_STATE_FAIL; + } else { + pstate.state += 1; + } + } + /* expect LF after a CR was read */ + SSH_STATE_LF => { + if data[i] == b'\n' { + pstate.state = SSH_STATE_EOB; + } else { + if pstate.prev_state == SSH_STATE_SOFTWARE { + /* when reading software, \r can be followed by something else than \n */ + pstate.state = pstate.prev_state; + /* cancel the read of this char */ + i -= 1; + /* add the previously read \r to the software string */ + pstate.ssh_software.push(b'\r'); + } else if pstate.prev_state == SSH_STATE_COMMENT { + /* when reading comment, \r can be followed by something else than \n */ + pstate.state = pstate.prev_state; + /* cancel the read of this char */ + i -= 1; + /* add the previously read \r to the software string */ + pstate.ssh_comment.push(b'\r'); + } else { + /* in some other cases, it fails */ + pstate.state = SSH_STATE_FAIL; + } + } + } + SSH_STATE_VERSION => { + if data[i] == b'-' { + pstate.state = SSH_STATE_SOFTWARE; + } else if !data[i].is_ascii_digit() && data[i] != b'.' { + pstate.state = SSH_STATE_FAIL; + } else { + pstate.ssh_version.push(data[i]); + } + } + SSH_STATE_SOFTWARE => { + if data[i] == b'\r' { + /* look for LF in the next char */ + pstate.prev_state = pstate.state; + pstate.state = SSH_STATE_LF; + } else if data[i] == b' ' { + pstate.state = SSH_STATE_COMMENT; + } else { + pstate.ssh_software.push(data[i]); + } + } + SSH_STATE_COMMENT => { + if data[i] == b'\r' { + /* look for LF in the next char */ + pstate.prev_state = pstate.state; + pstate.state = SSH_STATE_LF; + } else { + pstate.ssh_comment.push(data[i]); + } + } + SSH_STATE_FAIL => { + return; + } + SSH_STATE_EOB => { /* so far, do not parse after banner */ } + _ => {} + }; + i += 1; + } +} pub fn repl<'a>( data: &'a [u8], _masscanned: &Masscanned, - mut _client_info: &mut ClientInfo, + mut _client_info: &ClientInfo, + _tcb: Option<&mut TCPControlBlock>, ) -> Option> { debug!("receiving SSH data"); + let mut pstate = ProtocolState::new(); + ssh_parse(&mut pstate, data); + if pstate.state != SSH_STATE_EOB { + debug!("data in not correctly formatted - not responding"); + debug!("pstate: {}", pstate.state); + return None; + } let repl_data = b"SSH-2.0-1\r\n".to_vec(); debug!("sending SSH answer"); - warn!("SSH server banner to {}", str::from_utf8(&data).unwrap().trim_end()); - return Some(repl_data); + warn!("SSH server banner to {}", byte2str(&pstate.ssh_software)); + Some(repl_data) +} + +#[cfg(test)] +mod tests { + use super::*; + + /* Reconstruct client's banner from the parsed information */ + fn ssh_banner(pstate: &ProtocolState) -> Vec { + let mut banner = b"SSH-".to_vec(); + for b in &pstate.ssh_version { + banner.push(*b); + } + banner.push(b'-'); + for b in &pstate.ssh_software { + banner.push(*b); + } + if pstate.ssh_comment.len() > 0 { + banner.push(b' '); + for b in &pstate.ssh_comment { + banner.push(*b); + } + } + banner.push(b'\r'); + banner.push(b'\n'); + banner + } + + #[test] + fn ssh_2_banner_parse() { + /* all at once */ + let test_banner = b"SSH-2.0-SOFTWARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_EOB); + assert!(pstate.ssh_version == b"2.0"); + assert!(pstate.ssh_software == b"SOFTWARE"); + assert!(pstate.ssh_comment == b"COMMENT"); + /* byte by byte */ + let test_banner = b"SSH-2.0-SOFTWARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + for i in 0..test_banner.len() { + if i == 0 { + assert!(pstate.state == SSH_STATE_START); + } else if i > 0 && i < 4 { + assert!(pstate.state == SSH_STATE_S1 + i); + } else if i >= 4 && i < 8 { + assert!(pstate.state == SSH_STATE_VERSION); + } else if i >= 8 && i < 17 { + assert!(pstate.state == SSH_STATE_SOFTWARE); + } else if i >= 17 && i < test_banner.len() - 1 { + assert!(pstate.state == SSH_STATE_COMMENT); + } else { + assert!(pstate.state == SSH_STATE_LF); + } + ssh_parse(&mut pstate, &test_banner[i..i + 1]); + } + assert!(pstate.state == SSH_STATE_EOB); + assert!(pstate.ssh_version == b"2.0"); + assert!(pstate.ssh_software == b"SOFTWARE"); + assert!(pstate.ssh_comment == b"COMMENT"); + assert!(ssh_banner(&pstate) == test_banner); + } + + #[test] + fn ssh_1_banner_parse() { + /* all at once */ + let test_banner = b"SSH-1.99-SOFTWARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_EOB); + assert!(pstate.ssh_version == b"1.99"); + assert!(pstate.ssh_software == b"SOFTWARE"); + assert!(pstate.ssh_comment == b"COMMENT"); + assert!(ssh_banner(&pstate) == test_banner); + /* byte by byte */ + let test_banner = b"SSH-1.99-SOFTWARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + for i in 0..test_banner.len() { + if i == 0 { + assert!(pstate.state == SSH_STATE_START); + } else if i > 0 && i < 4 { + assert!(pstate.state == SSH_STATE_S1 + i); + } else if i >= 4 && i < 9 { + assert!(pstate.state == SSH_STATE_VERSION); + } else if i >= 9 && i < 18 { + assert!(pstate.state == SSH_STATE_SOFTWARE); + } else if i >= 18 && i < test_banner.len() - 1 { + assert!(pstate.state == SSH_STATE_COMMENT); + } else { + assert!(pstate.state == SSH_STATE_LF); + } + ssh_parse(&mut pstate, &test_banner[i..i + 1]); + } + assert!(pstate.state == SSH_STATE_EOB); + assert!(pstate.ssh_version == b"1.99"); + assert!(pstate.ssh_software == b"SOFTWARE"); + assert!(pstate.ssh_comment == b"COMMENT"); + assert!(ssh_banner(&pstate) == test_banner); + } + + #[test] + fn ssh_2_banner_space() { + /* space in SSH */ + let test_banner = b"S SH-2.0-SOFTWARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_FAIL); + /* space in VERSION */ + let test_banner = b"SSH-2. 0-SOFTWARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_FAIL); + /* space in software */ + let test_banner = b"SSH-2.0-SOFT WARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_EOB); + assert!(pstate.ssh_version == b"2.0"); + assert!(pstate.ssh_software == b"SOFT"); + assert!(pstate.ssh_comment == b"WARE COMMENT"); + assert!(ssh_banner(&pstate) == test_banner); + /* space in comment */ + let test_banner = b"SSH-2.0-SOFTWARE COM MENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_EOB); + assert!(pstate.ssh_version == b"2.0"); + assert!(pstate.ssh_software == b"SOFTWARE"); + assert!(pstate.ssh_comment == b"COM MENT"); + assert!(ssh_banner(&pstate) == test_banner); + /* double space */ + let test_banner = b"SSH-2.0-SOFTWARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_EOB); + assert!(pstate.ssh_version == b"2.0"); + assert!(pstate.ssh_software == b"SOFTWARE"); + assert!(pstate.ssh_comment == b" COMMENT"); + assert!(ssh_banner(&pstate) == test_banner); + } + + #[test] + fn ssh_1_banner_space() { + /* space in SSH */ + let test_banner = b"S SH-1.99-SOFTWARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_FAIL); + /* space in VERSION */ + let test_banner = b"SSH-1. 99-SOFTWARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_FAIL); + /* space in software */ + let test_banner = b"SSH-1.99-SOFT WARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_EOB); + assert!(pstate.ssh_version == b"1.99"); + assert!(pstate.ssh_software == b"SOFT"); + assert!(pstate.ssh_comment == b"WARE COMMENT"); + assert!(ssh_banner(&pstate) == test_banner); + /* space in comment */ + let test_banner = b"SSH-1.99-SOFTWARE COM MENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_EOB); + assert!(pstate.ssh_version == b"1.99"); + assert!(pstate.ssh_software == b"SOFTWARE"); + assert!(pstate.ssh_comment == b"COM MENT"); + assert!(ssh_banner(&pstate) == test_banner); + /* double space */ + let test_banner = b"SSH-1.99-SOFTWARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_EOB); + assert!(pstate.ssh_version == b"1.99"); + assert!(pstate.ssh_software == b"SOFTWARE"); + assert!(pstate.ssh_comment == b" COMMENT"); + assert!(ssh_banner(&pstate) == test_banner); + } + + #[test] + fn ssh_2_banner_cr() { + /* CR in SSH */ + let test_banner = b"S\rSH-2.0-SOFTWARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_FAIL); + /* CR in VERSION */ + let test_banner = b"SSH-2.\r0-SOFTWARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_FAIL); + /* CR in SOFTWARE */ + let test_banner = b"SSH-2.0-SOFT\rWARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_EOB); + assert!(pstate.ssh_version == b"2.0"); + assert!(pstate.ssh_software == b"SOFT\rWARE"); + assert!(pstate.ssh_comment == b"COMMENT"); + assert!(ssh_banner(&pstate) == test_banner); + /* CR in COMMENT */ + let test_banner = b"SSH-2.0-SOFTWARE COM\rMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_EOB); + assert!(pstate.ssh_version == b"2.0"); + assert!(pstate.ssh_software == b"SOFTWARE"); + assert!(pstate.ssh_comment == b"COM\rMENT"); + assert!(ssh_banner(&pstate) == test_banner); + /* CR at the end */ + let test_banner = b"SSH-2.0-SOFTWARE COMMENT\r\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_EOB); + assert!(pstate.ssh_version == b"2.0"); + assert!(pstate.ssh_software == b"SOFTWARE"); + assert!(pstate.ssh_comment == b"COMMENT\r"); + assert!(ssh_banner(&pstate) == test_banner); + } + + #[test] + fn ssh_1_banner_cr() { + /* CR in SSH */ + let test_banner = b"S\rSH-1.99-SOFTWARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_FAIL); + /* CR in VERSION */ + let test_banner = b"SSH-1.\r99-SOFTWARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_FAIL); + /* CR in SOFTWARE */ + let test_banner = b"SSH-1.99-SOFT\rWARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_EOB); + assert!(pstate.ssh_version == b"1.99"); + assert!(pstate.ssh_software == b"SOFT\rWARE"); + assert!(pstate.ssh_comment == b"COMMENT"); + assert!(ssh_banner(&pstate) == test_banner); + /* CR in COMMENT */ + let test_banner = b"SSH-1.99-SOFTWARE COM\rMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_EOB); + assert!(pstate.ssh_version == b"1.99"); + assert!(pstate.ssh_software == b"SOFTWARE"); + assert!(pstate.ssh_comment == b"COM\rMENT"); + assert!(ssh_banner(&pstate) == test_banner); + /* CR at the end */ + let test_banner = b"SSH-1.99-SOFTWARE COMMENT\r\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_EOB); + assert!(pstate.ssh_version == b"1.99"); + assert!(pstate.ssh_software == b"SOFTWARE"); + assert!(pstate.ssh_comment == b"COMMENT\r"); + assert!(ssh_banner(&pstate) == test_banner); + } + + #[test] + fn ssh_2_banner_lf() { + /* LF in SSH */ + let test_banner = b"S\nSH-2.0-SOFTWARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_FAIL); + /* LF in VERSION */ + let test_banner = b"SSH-2.\n0-SOFTWARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_FAIL); + /* LF in SOFTWARE */ + let test_banner = b"SSH-2.0-SOFT\nWARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_EOB); + assert!(pstate.ssh_version == b"2.0"); + assert!(pstate.ssh_software == b"SOFT\nWARE"); + assert!(pstate.ssh_comment == b"COMMENT"); + assert!(ssh_banner(&pstate) == test_banner); + /* LF in COMMENT */ + let test_banner = b"SSH-2.0-SOFTWARE COM\nMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_EOB); + assert!(pstate.ssh_version == b"2.0"); + assert!(pstate.ssh_software == b"SOFTWARE"); + assert!(pstate.ssh_comment == b"COM\nMENT"); + assert!(ssh_banner(&pstate) == test_banner); + /* LF at the end */ + let test_banner = b"SSH-2.0-SOFTWARE COMMENT\n\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_EOB); + assert!(pstate.ssh_version == b"2.0"); + assert!(pstate.ssh_software == b"SOFTWARE"); + assert!(pstate.ssh_comment == b"COMMENT\n"); + assert!(ssh_banner(&pstate) == test_banner); + } + + #[test] + fn ssh_1_banner_lf() { + /* LF in SSH */ + let test_banner = b"S\nSH-1.99-SOFTWARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_FAIL); + /* LF in VERSION */ + let test_banner = b"SSH-1.\n99-SOFTWARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_FAIL); + /* LF in SOFTWARE */ + let test_banner = b"SSH-1.99-SOFT\nWARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_EOB); + assert!(pstate.ssh_version == b"1.99"); + assert!(pstate.ssh_software == b"SOFT\nWARE"); + assert!(pstate.ssh_comment == b"COMMENT"); + assert!(ssh_banner(&pstate) == test_banner); + /* LF in COMMENT */ + let test_banner = b"SSH-1.99-SOFTWARE COM\nMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_EOB); + assert!(pstate.ssh_version == b"1.99"); + assert!(pstate.ssh_software == b"SOFTWARE"); + assert!(pstate.ssh_comment == b"COM\nMENT"); + assert!(ssh_banner(&pstate) == test_banner); + /* LF at the end */ + let test_banner = b"SSH-1.99-SOFTWARE COMMENT\n\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_EOB); + assert!(pstate.ssh_version == b"1.99"); + assert!(pstate.ssh_software == b"SOFTWARE"); + assert!(pstate.ssh_comment == b"COMMENT\n"); + assert!(ssh_banner(&pstate) == test_banner); + } + + #[test] + fn ssh_2_banner_crlf() { + /* CRLF in SSH */ + let test_banner = b"S\r\nSH-2.0-SOFTWARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_FAIL); + /* CRLF in VERSION */ + let test_banner = b"SSH-2.\r\n0-SOFTWARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_FAIL); + /* CRLF in SOFTWARE */ + let test_banner = b"SSH-2.0-SOFT\r\nWARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_EOB); + assert!(pstate.ssh_version == b"2.0"); + assert!(pstate.ssh_software == b"SOFT"); + assert!(pstate.ssh_comment == b""); + assert!(ssh_banner(&pstate) == b"SSH-2.0-SOFT\r\n"); + /* CRLF in COMMENT */ + let test_banner = b"SSH-2.0-SOFTWARE COM\r\nMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_EOB); + assert!(pstate.ssh_version == b"2.0"); + assert!(pstate.ssh_software == b"SOFTWARE"); + assert!(pstate.ssh_comment == b"COM"); + assert!(ssh_banner(&pstate) == b"SSH-2.0-SOFTWARE COM\r\n"); + /* CRLF at the end */ + let test_banner = b"SSH-2.0-SOFTWARE COMMENT\r\n\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_EOB); + assert!(pstate.ssh_version == b"2.0"); + assert!(pstate.ssh_software == b"SOFTWARE"); + assert!(pstate.ssh_comment == b"COMMENT"); + assert!(ssh_banner(&pstate) == b"SSH-2.0-SOFTWARE COMMENT\r\n"); + } + + #[test] + fn ssh_1_banner_crlf() { + /* CRLF in SSH */ + let test_banner = b"S\r\nSH-1.99-SOFTWARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_FAIL); + /* CRLF in VERSION */ + let test_banner = b"SSH-1.\r\n99-SOFTWARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_FAIL); + /* CRLF in SOFTWARE */ + let test_banner = b"SSH-1.99-SOFT\r\nWARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_EOB); + assert!(pstate.ssh_version == b"1.99"); + assert!(pstate.ssh_software == b"SOFT"); + assert!(pstate.ssh_comment == b""); + assert!(ssh_banner(&pstate) == b"SSH-1.99-SOFT\r\n"); + /* CRLF in COMMENT */ + let test_banner = b"SSH-1.99-SOFTWARE COM\r\nMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_EOB); + assert!(pstate.ssh_version == b"1.99"); + assert!(pstate.ssh_software == b"SOFTWARE"); + assert!(pstate.ssh_comment == b"COM"); + assert!(ssh_banner(&pstate) == b"SSH-1.99-SOFTWARE COM\r\n"); + /* CRLF at the end */ + let test_banner = b"SSH-1.99-SOFTWARE COMMENT\r\n\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_EOB); + assert!(pstate.ssh_version == b"1.99"); + assert!(pstate.ssh_software == b"SOFTWARE"); + assert!(pstate.ssh_comment == b"COMMENT"); + assert!(ssh_banner(&pstate) == b"SSH-1.99-SOFTWARE COMMENT\r\n"); + } } diff --git a/src/proto/stun.rs b/src/proto/stun.rs index a5c7ac4..24b209c 100644 --- a/src/proto/stun.rs +++ b/src/proto/stun.rs @@ -24,6 +24,7 @@ use byteorder::{BigEndian, ByteOrder}; use std::io; use crate::client::ClientInfo; +use crate::proto::TCPControlBlock; use crate::Masscanned; /* RFC 5389: The magic cookie field MUST contain the fixed value 0x2112A442 in @@ -257,107 +258,6 @@ impl Into> for &StunAttribute { } } -/* -struct StunPacket { - class: u8, - method: u16, - length: u16, - magic: u32, - id: u128, - data: Vec, - attributes: Vec, -} - -impl StunPacket { - fn new(data: &[u8]) -> Result { - if data.len() < 20 { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "not enough data", - )); - } - let class: u8 = ((data[0] & 0x01) << 1) | ((data[1] & 0x10) >> 4); - let method: u16 = (((data[0] & 0b00111110) << 7) as u16) | ((data[1] & 0b11101111) as u16); - let length: u16 = BigEndian::read_u16(&data[2..4]); - let magic: u32 = BigEndian::read_u32(&data[4..8]); - let id: u128 = ((BigEndian::read_u64(&data[8..16]) as u128) << 32) - | (BigEndian::read_u32(&data[16..20]) as u128); - if data.len() < 20 + length as usize { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "not enough data", - )); - } - let data: Vec = data[20..(20 + length) as usize].to_vec(); - let mut stun = StunPacket { - class, - method, - length, - magic, - id, - data, - attributes: Vec::::new(), - }; - stun.attributes = stun.get_attributes(); - Ok(stun) - } - - fn empty() -> Self { - StunPacket { - class: 0, - method: 0, - length: 0, - magic: 0, - id: 0, - data: Vec::new(), - attributes: Vec::new(), - } - } - - fn get_attributes(&self) -> Vec { - let mut i = 0; - let mut attributes = Vec::::new(); - while i + 4 < self.data.len() { - let attr = StunAttribute::from(self.data[i..].to_vec()); - i += 4 + attr.len() as usize; - attributes.push(attr); - } - attributes - } - - fn set_length(&mut self) { - self.length = 0; - for attr in &self.attributes { - self.length += 4 + attr.len(); - } - } -} - -impl Into> for StunPacket { - fn into(self) -> Vec { - let mut v = Vec::::new(); - // first cocktail with class and method bits - v.push( - TryInto::::try_into((self.method >> 7) & 0b00111110).unwrap() - | TryInto::::try_into((self.class & 0b10) >> 1).unwrap(), - ); - // second cocktail with class and method bits - v.push( - TryInto::::try_into((self.method & 0b01110000) << 1).unwrap() - | TryInto::::try_into((self.class & 0b01) << 4).unwrap() - | TryInto::::try_into(self.method & 0b00001111).unwrap(), - ); - v.append(&mut self.length.to_be_bytes().to_vec()); - v.append(&mut self.magic.to_be_bytes().to_vec()); - v.append(&mut self.id.to_be_bytes()[4..].to_vec()); - for attr in &self.attributes { - v.append(&mut attr.into()); - } - v - } -} -*/ - struct StunPacket { class: u8, method: u16, @@ -451,67 +351,11 @@ impl Into> for StunPacket { } } -/* pub fn repl<'a>( data: &'a [u8], _masscanned: &Masscanned, - client_info: ClientInfo, -) -> Option> { - debug!("receiving STUN data"); - let stun_req: StunPacket = if let Ok(s) = StunPacket::new(&data) { - s - } else { - return None; - }; - if stun_req.class != STUN_CLASS_REQUEST { - info!( - "STUN packet not handled (class unknown: 0b{:b})", - stun_req.class - ); - return None; - } - if stun_req.method != STUN_METHOD_BINDING { - info!( - "STUN packet not handled (method unknown: 0x{:03x})", - stun_req.method - ); - return None; - } - /* - * To be compatible with RFC3489: ignore magic - if stun_req.magic != STUN_MAGIC { - info!( - "STUN packet not handled (magic unknown: 0x{:04x})", - stun_req.magic - ); - return None; - } - */ - if client_info.ip.src == None { - error!("STUN packet not handled (expected client ip address not found)"); - return None; - } - if client_info.port.src == None { - error!("STUN packet not handled (expected client port address not found)"); - return None; - } - let mut stun_resp: StunPacket = StunPacket::empty(); - stun_resp.class = STUN_CLASS_SUCCESS_RESPONSE; - stun_resp.method = STUN_METHOD_BINDING; - stun_resp.id = stun_req.id; - stun_resp.attributes = Vec::::new(); - stun_resp.attributes.push(StunAttribute::MappedAddress( - StunMappedAddressAttribute::new(client_info.ip.src.unwrap(), client_info.port.src.unwrap()), - )); - stun_resp.set_length(); - return Some(stun_resp.into()); -} -*/ - -pub fn repl<'a>( - data: &'a [u8], - _masscanned: &Masscanned, - mut client_info: &mut ClientInfo, + client_info: &mut ClientInfo, + _tcb: Option<&mut TCPControlBlock>, ) -> Option> { debug!("receiving STUN data"); let stun_req: StunPacket = if let Ok(s) = StunPacket::new(&data) { @@ -571,6 +415,8 @@ mod tests { use pnet::util::MacAddr; + use crate::logger::MetaLogger; + #[test] fn test_proto_stun_ipv4() { /* test payload is: @@ -596,9 +442,11 @@ mod tests { synack_key: [0, 0], mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"), iface: None, - ip_addresses: Some(&ips), + self_ip_list: Some(&ips), + remote_ip_deny_list: None, + log: MetaLogger::new(), }; - let payload_resp = if let Some(r) = repl(payload, &masscanned, &mut client_info) { + let payload_resp = if let Some(r) = repl(payload, &masscanned, &mut client_info, None) { r } else { panic!("expected an answer, got None"); @@ -655,13 +503,15 @@ mod tests { synack_key: [0, 0], mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"), iface: None, - ip_addresses: Some(&ips), + self_ip_list: Some(&ips), + remote_ip_deny_list: None, + log: MetaLogger::new(), }; client_info.ip.src = Some(IpAddr::V6(test_ip_addr)); client_info.ip.dst = Some(IpAddr::V6(masscanned_ip_addr)); client_info.port.src = Some(55000); client_info.port.dst = Some(65000); - let payload_resp = if let Some(r) = repl(payload, &masscanned, &mut client_info) { + let payload_resp = if let Some(r) = repl(payload, &masscanned, &mut client_info, None) { r } else { panic!("expected an answer, got None"); @@ -706,13 +556,15 @@ mod tests { synack_key: [0, 0], mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"), iface: None, - ip_addresses: Some(&ips), + self_ip_list: Some(&ips), + remote_ip_deny_list: None, + log: MetaLogger::new(), }; client_info.ip.src = Some(IpAddr::V4(test_ip_addr)); client_info.ip.dst = Some(IpAddr::V4(masscanned_ip_addr)); client_info.port.src = Some(55000); client_info.port.dst = Some(65000); - let payload_resp = if let Some(r) = repl(payload, &masscanned, &mut client_info) { + let payload_resp = if let Some(r) = repl(payload, &masscanned, &mut client_info, None) { r } else { panic!("expected an answer, got None"); @@ -755,13 +607,15 @@ mod tests { synack_key: [0, 0], mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"), iface: None, - ip_addresses: Some(&ips), + self_ip_list: Some(&ips), + remote_ip_deny_list: None, + log: MetaLogger::new(), }; client_info.ip.src = Some(IpAddr::V4(test_ip_addr)); client_info.ip.dst = Some(IpAddr::V4(masscanned_ip_addr)); client_info.port.src = Some(55000); client_info.port.dst = Some(65535); - let payload_resp = if let Some(r) = repl(payload, &masscanned, &mut client_info) { + let payload_resp = if let Some(r) = repl(payload, &masscanned, &mut client_info, None) { r } else { panic!("expected an answer, got None"); diff --git a/src/proto/tcb.rs b/src/proto/tcb.rs new file mode 100644 index 0000000..0f2a815 --- /dev/null +++ b/src/proto/tcb.rs @@ -0,0 +1,270 @@ +// This file is part of masscanned. +// Copyright 2021 - The IVRE project +// +// Masscanned is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Masscanned is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +// License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Masscanned. If not, see . + +use lazy_static::lazy_static; + +use std::collections::HashMap; +use std::sync::Mutex; + +use super::http::ProtocolState as HTTPProtocolState; +use super::rpc::ProtocolState as RPCProtocolState; +use crate::proto::{BASE_STATE, PROTO_NONE}; + +pub enum ProtocolState { + HTTP(HTTPProtocolState), + RPC(RPCProtocolState), +} + +pub struct TCPControlBlock { + /* state used to detect protocols (not specific) */ + pub smack_state: usize, + /* detected protocol */ + pub proto_id: usize, + /* internal state of protocol parser (e.g., HTTP parsing) */ + pub proto_state: Option, +} + +lazy_static! { + static ref CONTABLE: Mutex> = Mutex::new(HashMap::new()); +} + +pub fn is_tcb_set(cookie: u32) -> bool { + CONTABLE.lock().unwrap().contains_key(&cookie) +} + +pub fn get_tcb(cookie: u32, mut f: F) +where + F: FnMut(Option<&mut TCPControlBlock>), +{ + f(CONTABLE.lock().unwrap().get_mut(&cookie)); +} + +pub fn add_tcb(cookie: u32) { + let mut ct = CONTABLE.lock().unwrap(); + let tcb = TCPControlBlock { + smack_state: BASE_STATE, + proto_id: PROTO_NONE, + proto_state: None, + }; + if !ct.contains_key(&cookie) { + ct.insert(cookie, tcb); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashSet; + use std::net::{IpAddr, Ipv4Addr}; + use std::str::FromStr; + + use pnet::{ + packet::{ip::IpNextHeaderProtocols, tcp::TcpPacket}, + util::MacAddr, + }; + + use crate::client::ClientInfo; + use crate::layer_4::tcp; + use crate::logger::MetaLogger; + use crate::proto::{PROTO_HTTP, PROTO_RPC_TCP}; + use crate::synackcookie; + use crate::Masscanned; + + fn get_dummy_tcp(&client_info: &ClientInfo) -> Vec { + /* Craft a TCP ACK+PUSH packet with correct ports and ack */ + let mut pkt = Vec::new(); + pkt.extend_from_slice(&client_info.port.src.unwrap().to_be_bytes()); + pkt.extend_from_slice(&client_info.port.dst.unwrap().to_be_bytes()); + pkt.extend_from_slice(b"\x00\x00\x00\x00"); + pkt.extend_from_slice(&(client_info.cookie.unwrap() + 1).to_be_bytes()); + pkt.extend_from_slice(b"P\x18 \x00\x00\x00\x00\x00"); + pkt + } + + #[test] + fn test_proto_tcb_proto_id() { + let mut client_info = ClientInfo::new(); + let test_ip_addr = Ipv4Addr::new(3, 2, 1, 0); + client_info.ip.src = Some(IpAddr::V4(test_ip_addr)); + client_info.port.src = Some(65000); + client_info.port.dst = Some(80); + client_info.transport = Some(IpNextHeaderProtocols::Tcp); + let masscanned_ip_addr = Ipv4Addr::new(0, 1, 2, 3); + client_info.ip.dst = Some(IpAddr::V4(masscanned_ip_addr)); + let mut ips = HashSet::new(); + ips.insert(IpAddr::V4(masscanned_ip_addr)); + /* Construct masscanned context object */ + let masscanned = Masscanned { + synack_key: [0, 0], + mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"), + iface: None, + self_ip_list: Some(&ips), + remote_ip_deny_list: None, + log: MetaLogger::new(), + }; + let cookie = synackcookie::generate(&client_info, &masscanned.synack_key).unwrap(); + client_info.cookie = Some(cookie); + assert!(!is_tcb_set(cookie), "expected no TCB entry, found one"); + /***** TEST PROTOCOL ID IN TCB *****/ + let payload = [get_dummy_tcp(&client_info), b"GET / HTTP/1.1\r\n".to_vec()].concat(); + tcp::repl( + &TcpPacket::new(&payload).unwrap(), + &masscanned, + &mut client_info, + ); + assert!(is_tcb_set(cookie), "expected a TCB entry, not found"); + get_tcb(cookie, |t| { + let t = t.unwrap(); + assert!(t.proto_id == PROTO_HTTP); + }); + + /***** SENDING MORE DATA *****/ + let payload = [ + get_dummy_tcp(&client_info), + b"garbage data with no specific format (no protocol)\r\n\r\n".to_vec(), + ] + .concat(); + tcp::repl( + &TcpPacket::new(&payload).unwrap(), + &masscanned, + &mut client_info, + ); + assert!(is_tcb_set(cookie), "expected a TCB entry, not found"); + get_tcb(cookie, |t| { + let t = t.unwrap(); + assert!(t.proto_id == PROTO_HTTP); + }); + } + + #[test] + fn test_proto_tcb_proto_state_http() { + let mut client_info = ClientInfo::new(); + let test_ip_addr = Ipv4Addr::new(3, 2, 1, 0); + client_info.ip.src = Some(IpAddr::V4(test_ip_addr)); + client_info.port.src = Some(65001); + client_info.port.dst = Some(80); + client_info.transport = Some(IpNextHeaderProtocols::Tcp); + let masscanned_ip_addr = Ipv4Addr::new(0, 1, 2, 3); + client_info.ip.dst = Some(IpAddr::V4(masscanned_ip_addr)); + let mut ips = HashSet::new(); + ips.insert(IpAddr::V4(masscanned_ip_addr)); + /* Construct masscanned context object */ + let masscanned = Masscanned { + synack_key: [0, 0], + mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"), + iface: None, + self_ip_list: Some(&ips), + remote_ip_deny_list: None, + log: MetaLogger::new(), + }; + let cookie = synackcookie::generate(&client_info, &masscanned.synack_key).unwrap(); + client_info.cookie = Some(cookie); + assert!(!is_tcb_set(cookie), "expected no TCB entry, found one"); + /***** TEST PROTOCOL ID IN TCB *****/ + let payload = [get_dummy_tcp(&client_info), b"GET / HTTP/1.1\r\n".to_vec()].concat(); + tcp::repl( + &TcpPacket::new(&payload).unwrap(), + &masscanned, + &mut client_info, + ); + assert!(is_tcb_set(cookie), "expected a TCB entry, not found"); + get_tcb(cookie, |t| { + let t = t.unwrap(); + assert!(t.proto_id == PROTO_HTTP); + if let Some(ProtocolState::HTTP(_)) = t.proto_state { + } else { + panic!("expected a HTTP protocole state, found None"); + } + }); + /***** SENDING MORE DATA *****/ + let payload = [ + get_dummy_tcp(&client_info), + b"Field: empty\r\n\r\n".to_vec(), + ] + .concat(); + /* Should have an answer here */ + if let None = tcp::repl( + &TcpPacket::new(&payload).unwrap(), + &masscanned, + &mut client_info, + ) { + panic!("expected an HTTP response, got nothing"); + } + assert!(is_tcb_set(cookie), "expected a TCB entry, not found"); + get_tcb(cookie, |t| { + let t = t.unwrap(); + assert!(t.proto_id == PROTO_HTTP); + }) + } + + #[test] + fn test_proto_tcb_proto_state_rpc() { + let mut client_info = ClientInfo::new(); + let test_ip_addr = Ipv4Addr::new(3, 2, 1, 0); + client_info.ip.src = Some(IpAddr::V4(test_ip_addr)); + client_info.port.src = Some(65002); + client_info.port.dst = Some(80); + client_info.transport = Some(IpNextHeaderProtocols::Tcp); + let masscanned_ip_addr = Ipv4Addr::new(0, 1, 2, 3); + client_info.ip.dst = Some(IpAddr::V4(masscanned_ip_addr)); + let mut ips = HashSet::new(); + ips.insert(IpAddr::V4(masscanned_ip_addr)); + /* Construct masscanned context object */ + let masscanned = Masscanned { + synack_key: [0, 0], + mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"), + iface: None, + self_ip_list: Some(&ips), + remote_ip_deny_list: None, + log: MetaLogger::new(), + }; + let cookie = synackcookie::generate(&client_info, &masscanned.synack_key).unwrap(); + client_info.cookie = Some(cookie); + assert!(!is_tcb_set(cookie), "expected no TCB entry, found one"); + /***** TEST PROTOCOL ID IN TCB *****/ + let full_payload = b"\x80\x00\x00\x28\x72\xfe\x1d\x13\x00\x00\x00\x00\x00\x00\x00\x02\x00\x01\x86\xa0\x00\x01\x97\x7c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"; + let payload = [get_dummy_tcp(&client_info), full_payload[0..28].to_vec()].concat(); + tcp::repl( + &TcpPacket::new(&payload).unwrap(), + &masscanned, + &mut client_info, + ); + assert!(is_tcb_set(cookie), "expected a TCB entry, not found"); + get_tcb(cookie, |t| { + let t = t.unwrap(); + assert!(t.proto_id == PROTO_RPC_TCP); + if let Some(ProtocolState::RPC(_)) = t.proto_state { + } else { + panic!("expected a RPC protocole state, found None"); + } + }); + /***** SENDING MORE DATA *****/ + /* Should have an answer here */ + let payload = [get_dummy_tcp(&client_info), full_payload[28..].to_vec()].concat(); + if let None = tcp::repl( + &TcpPacket::new(&payload).unwrap(), + &masscanned, + &mut client_info, + ) { + panic!("expected a RPC response, got nothing"); + } + assert!(is_tcb_set(cookie), "expected a TCB entry, not found"); + get_tcb(cookie, |t| { + let t = t.unwrap(); + assert!(t.proto_id == PROTO_RPC_TCP); + }); + } +} diff --git a/src/smack/smack.rs b/src/smack/smack.rs index bb37358..8248ec1 100644 --- a/src/smack/smack.rs +++ b/src/smack/smack.rs @@ -61,7 +61,7 @@ pub struct Smack { } fn make_copy_of_pattern(pattern: &[u8], is_nocase: bool) -> Vec { - let mut p = pattern.clone().to_vec(); + let mut p = pattern.to_vec(); for i in 0..p.len() { if is_nocase { p[i] = p[i].to_ascii_lowercase(); diff --git a/src/smack/smack_utils.rs b/src/smack/smack_utils.rs index 79bb459..bed3b6e 100644 --- a/src/smack/smack_utils.rs +++ b/src/smack/smack_utils.rs @@ -1,4 +1,5 @@ bitflags! { + #[derive(Clone, Copy)] pub struct SmackFlags: usize { const EMPTY = 0x00; const ANCHOR_BEGIN = 0x01; diff --git a/src/utils/display.rs b/src/utils/display.rs new file mode 100644 index 0000000..45e3d8f --- /dev/null +++ b/src/utils/display.rs @@ -0,0 +1,48 @@ +// This file is part of masscanned. +// Copyright 2021 - 2022 - The IVRE project +// +// Masscanned is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Masscanned is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +// License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Masscanned. If not, see . + +static CHARS: [&'static str; 256] = [ + "\\x00", "\\x01", "\\x02", "\\x03", "\\x04", "\\x05", "\\x06", "\\x07", "\\x08", "\\x09", + "\\x0a", "\\x0b", "\\x0c", "\\x0d", "\\x0e", "\\x0f", "\\x10", "\\x11", "\\x12", "\\x13", + "\\x14", "\\x15", "\\x16", "\\x17", "\\x18", "\\x19", "\\x1a", "\\x1b", "\\x1c", "\\x1d", + "\\x1e", "\\x1f", " ", "!", "\"", "#", "$", "%", "&", "'", "(", ")", "*", "+", ",", "-", ".", + "/", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ":", ";", "<", "=", ">", "?", "@", "A", + "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", + "U", "V", "W", "X", "Y", "Z", "[", "\\", "]", "^", "_", "`", "a", "b", "c", "d", "e", "f", "g", + "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", + "{", "|", "}", "~", "\\x7f", "\\x80", "\\x81", "\\x82", "\\x83", "\\x84", "\\x85", "\\x86", + "\\x87", "\\x88", "\\x89", "\\x8a", "\\x8b", "\\x8c", "\\x8d", "\\x8e", "\\x8f", "\\x90", + "\\x91", "\\x92", "\\x93", "\\x94", "\\x95", "\\x96", "\\x97", "\\x98", "\\x99", "\\x9a", + "\\x9b", "\\x9c", "\\x9d", "\\x9e", "\\x9f", "\\xa0", "\\xa1", "\\xa2", "\\xa3", "\\xa4", + "\\xa5", "\\xa6", "\\xa7", "\\xa8", "\\xa9", "\\xaa", "\\xab", "\\xac", "\\xad", "\\xae", + "\\xaf", "\\xb0", "\\xb1", "\\xb2", "\\xb3", "\\xb4", "\\xb5", "\\xb6", "\\xb7", "\\xb8", + "\\xb9", "\\xba", "\\xbb", "\\xbc", "\\xbd", "\\xbe", "\\xbf", "\\xc0", "\\xc1", "\\xc2", + "\\xc3", "\\xc4", "\\xc5", "\\xc6", "\\xc7", "\\xc8", "\\xc9", "\\xca", "\\xcb", "\\xcc", + "\\xcd", "\\xce", "\\xcf", "\\xd0", "\\xd1", "\\xd2", "\\xd3", "\\xd4", "\\xd5", "\\xd6", + "\\xd7", "\\xd8", "\\xd9", "\\xda", "\\xdb", "\\xdc", "\\xdd", "\\xde", "\\xdf", "\\xe0", + "\\xe1", "\\xe2", "\\xe3", "\\xe4", "\\xe5", "\\xe6", "\\xe7", "\\xe8", "\\xe9", "\\xea", + "\\xeb", "\\xec", "\\xed", "\\xee", "\\xef", "\\xf0", "\\xf1", "\\xf2", "\\xf3", "\\xf4", + "\\xf5", "\\xf6", "\\xf7", "\\xf8", "\\xf9", "\\xfa", "\\xfb", "\\xfc", "\\xfd", "\\xfe", + "\\xff", +]; + +pub fn byte2str(data: &[u8]) -> String { + let mut result = String::new(); + for byte in data { + result.push_str(CHARS[usize::from(*byte)]); + } + return result; +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 3c788a6..f25ca3d 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,3 +1,7 @@ mod parsers; pub use parsers::IpAddrParser; + +mod display; + +pub use display::byte2str; diff --git a/src/utils/parsers.rs b/src/utils/parsers.rs index a17210d..e2a9ac7 100644 --- a/src/utils/parsers.rs +++ b/src/utils/parsers.rs @@ -5,12 +5,12 @@ use std::io::BufReader; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use log::*; -use pcap_file::pcap::{Packet, PcapReader}; +use pcap_file::pcap::{PcapPacket, PcapReader}; use pnet::packet::{ ethernet::{EtherTypes, EthernetPacket}, ipv4::Ipv4Packet, ipv6::Ipv6Packet, - Packet as Pkt, + Packet, }; /* Generic IP packet (either IPv4 or IPv6) */ @@ -134,9 +134,46 @@ impl IpAddrParser for File { } } +/* Parse IP addresses from a comma-separated list in a string */ +impl IpAddrParser for &str { + fn extract_ip_addresses_with_count( + self, + _blacklist: Option>, + ) -> HashMap { + panic!("not implemented"); + } + + fn extract_ip_addresses_only(self, blacklist: Option>) -> HashSet { + let mut ip_addresses = HashSet::new(); + for line in self.split(",") { + /* Should never occur */ + if line.is_empty() { + warn!("cannot parse line: {}", line); + continue; + } + let ip: IpAddr; + if let Ok(val) = line.parse::() { + ip = IpAddr::V4(val); + } else if let Ok(val) = line.parse::() { + ip = IpAddr::V6(val); + } else { + warn!("cannot parse IP address from line: {}", line); + continue; + } + if let Some(ref b) = blacklist { + if b.contains(&ip) { + info!("[blacklist] ignoring {}", &ip); + continue; + } + } + ip_addresses.insert(ip); + } + ip_addresses + } +} /* Get the IP address of source and dest. from an IP packet. * works with both IPv4 and IPv6 packets/addresses */ -fn extract_ip(pkt: Packet) -> Option<(IpAddr, IpAddr)> { +fn extract_ip(pkt: PcapPacket) -> Option<(IpAddr, IpAddr)> { let eth = EthernetPacket::new(&pkt.data).expect("error parsing Ethernet packet"); let payload = eth.payload(); let ip = match eth.get_ethertype() { @@ -169,13 +206,13 @@ impl IpAddrParser for PcapReader { /* Extract IP addresses (v4 and v6) from a capture and count occurrences of * each. */ fn extract_ip_addresses_with_count( - self: PcapReader, + mut self: PcapReader, blacklist: Option>, ) -> HashMap { let mut ip_addresses = HashMap::new(); // pcap.map(fn) , map_Ok // .iter, into_iter - for pkt in self { + while let Some(pkt) = self.next_packet() { match pkt { Ok(pkt) => { // map_Some map_None @@ -209,13 +246,13 @@ impl IpAddrParser for PcapReader { ip_addresses } fn extract_ip_addresses_only( - self: PcapReader, + mut self: PcapReader, blacklist: Option>, ) -> HashSet { let mut ip_addresses = HashSet::new(); // pcap.map(fn) , map_Ok // .iter, into_iter - for pkt in self { + while let Some(pkt) = self.next_packet() { match pkt { Ok(pkt) => { // map_Some map_None diff --git a/test/requirements.txt b/test/requirements.txt new file mode 100644 index 0000000..fd8f3e8 --- /dev/null +++ b/test/requirements.txt @@ -0,0 +1,3 @@ +ivre +scapy +requests diff --git a/test/src/all.py b/test/src/all.py index e031228..561993e 100644 --- a/test/src/all.py +++ b/test/src/all.py @@ -14,580 +14,31 @@ # You should have received a copy of the GNU General Public License # along with Masscanned. If not, see . -from scapy.all import * -import requests -import requests.packages.urllib3.util.connection as urllib3_cn -import logging +import importlib +import os -from .conf import * +# Export / other tests +from .core import test_all # noqa: F401 -fmt = logging.Formatter("%(levelname)s\t%(message)s") -ch = logging.StreamHandler() -ch.setFormatter(fmt) -ch.setLevel(logging.DEBUG) -LOG = logging.getLogger(__name__) -LOG.setLevel(logging.DEBUG) -LOG.addHandler(ch) +DEFAULT_TESTS = [ + "arp", + "dns", + "ghost", + "http", + "icmpv4", + "icmpv6", + "ip", + "rpc", + "smb", + "ssh", + "stun", + "tcp", + "udp", +] -tests = list() +ENABLED_TESTS = DEFAULT_TESTS +if tests := os.environ.get("TESTS"): + ENABLED_TESTS = [x.strip() for x in tests.split(",")] -# decorator to automatically add a function to tests -def test(f): - OK = "\033[1mOK\033[0m" - KO = "\033[1m\033[1;%dmKO\033[0m" % 31 - global tests - fname = f.__name__.ljust(50, '.') - def w(iface): - try: - f(iface) - LOG.info("{}{}".format(fname, OK)) - except AssertionError as e: - LOG.info("{}{}: {}".format(fname, KO, e)) - tests.append(w) - return w - -def multicast(ip6): - a, b = ip6.split(":")[-2:] - mac = ["33", "33", "ff"] - if len(a) == 4: - mac.append(a[2:]) - else: - mac.append("00") - if len(b) >= 2: - mac.append(b[:2]) - else: - mac.append("00") - if len(b) >= 4: - mac.append(b[2:]) - else: - mac.append("00") - return ":".join(mac) - -def check_ip_checksum(pkt): - assert(IP in pkt), "no IP layer found" - ip_pkt = pkt[IP] - chksum = ip_pkt.chksum - del ip_pkt.chksum - assert(IP(raw(ip_pkt)).chksum == chksum), "bad IPv4 checksum" - -def check_ipv6_checksum(pkt): - assert(IPv6 in pkt), "no IP layer found" - ip_pkt = pkt[IPv6] - chksum = ip_pkt.chksum - del ip_pkt.chksum - assert(IPv6(raw(ip_pkt)).chksum == chksum), "bad IPv6 checksum" - -@test -def test_arp_req(iface): - ##### ARP ##### - arp_req = Ether()/ARP(psrc='192.0.0.2', pdst=IPV4_ADDR) - arp_repl = iface.sr1(arp_req, timeout=1) - assert(arp_repl is not None), "expecting answer, got nothing" - assert(ARP in arp_repl), "no ARP layer found" - arp_repl = arp_repl[ARP] - # check answer - ## op is "is-at" - assert(arp_repl.op == 2), "unexpected ARP op: {}".format(arp_repl.op) - ## answer for the requested IP - assert(arp_repl.psrc == arp_req.pdst), "unexpected ARP psrc: {}".format(arp_repl.psrc) - assert(arp_repl.pdst == arp_req.psrc), "unexpected ARP pdst: {}".format(arp_repl.pdst) - ## answer is expected MAC address - assert(arp_repl.hwsrc == MAC_ADDR), "unexpected ARP hwsrc: {}".format(arp_repl.hwsrc) - -@test -def test_arp_req_other_ip(iface): - ##### ARP ##### - arp_req = Ether()/ARP(psrc='192.0.0.2', pdst='1.2.3.4') - arp_repl = iface.sr1(arp_req, timeout=1) - assert(arp_repl is None), "responding to ARP requests for other IP addresses" - -@test -def test_ipv4_req(iface): - ##### IP ##### - ip_req = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR, id=0x1337)/ICMP(type=8, code=0) - ip_repl = iface.sr1(ip_req, timeout=1) - assert(ip_repl is not None), "expecting answer, got nothing" - check_ip_checksum(ip_repl) - assert(IP in ip_repl), "no IP layer in response" - ip_repl = ip_repl[IP] - assert(ip_repl.id == 0), "IP identification unexpected" - -@test -def test_eth_req_other_mac(iface): - #### ETH #### - ip_req = Ether(dst="00:00:00:11:11:11")/IP(dst=IPV4_ADDR)/ICMP(type=8, code=0) - ip_repl = iface.sr1(ip_req, timeout=1) - assert(ip_repl is None), "responding to other MAC addresses" - -@test -def test_ipv4_req_other_ip(iface): - ##### IP ##### - ip_req = Ether(dst=MAC_ADDR)/IP(dst="1.2.3.4")/ICMP(type=8, code=0) - ip_repl = iface.sr1(ip_req, timeout=1) - assert(ip_repl is None), "responding to other IP addresses" - -@test -def test_icmpv4_echo_req(iface): - ##### ICMPv4 ##### - icmp_req = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/ICMP(type=8, code=0)/Raw("idrinkwaytoomuchcoffee") - icmp_repl = iface.sr1(icmp_req, timeout=1) - assert(icmp_repl is not None), "expecting answer, got nothing" - check_ip_checksum(icmp_repl) - assert(ICMP in icmp_repl) - icmp_repl = icmp_repl[ICMP] - # check answer - ## type is "echo-reply" - assert(icmp_repl.type == 0) - assert(icmp_repl.code == 0) - ## data is the same as sent - assert(icmp_repl.load == icmp_req.load) - -@test -def test_icmpv6_neighbor_solicitation(iface): - ##### IPv6 Neighbor Solicitation ##### - for mac in ["ff:ff:ff:ff:ff:ff", "33:33:00:00:00:01", MAC_ADDR, multicast(IPV6_ADDR)]: - nd_ns = Ether(dst=mac)/IPv6()/ICMPv6ND_NS(tgt=IPV6_ADDR) - nd_na = iface.sr1(nd_ns, timeout=1) - assert(nd_na is not None), "expecting answer, got nothing" - assert(ICMPv6ND_NA in nd_na) - nd_na = nd_na[ICMPv6ND_NA] - # check answer content - assert(nd_na.code == 0) - assert(nd_na.R == 0) - assert(nd_na.S == 1) - assert(nd_na.O == 1) - assert(nd_na.tgt == IPV6_ADDR) - # check ND Option - assert(nd_na.haslayer(ICMPv6NDOptDstLLAddr)) - assert(nd_na.getlayer(ICMPv6NDOptDstLLAddr).lladdr == MAC_ADDR) - for mac in ["00:00:00:00:00:00", "33:33:33:00:00:01"]: - nd_ns = Ether(dst="ff:ff:ff:ff:ff:ff")/IPv6()/ICMPv6ND_NS(tgt=IPV6_ADDR) - nd_na = iface.sr1(nd_ns, timeout=1) - assert(nd_na is not None), "expecting no answer, got one" - -@test -def test_icmpv6_neighbor_solicitation_other_ip(iface): - ##### IPv6 Neighbor Solicitation ##### - nd_ns = Ether(dst="ff:ff:ff:ff:ff:ff")/IPv6()/ICMPv6ND_NS(tgt="2020:4141:3030:2020::bdbd") - nd_na = iface.sr1(nd_ns, timeout=1) - assert(nd_na is None), "responding to ND_NS for other IP addresses" - -@test -def test_icmpv6_echo_req(iface): - ##### IPv6 Ping ##### - echo_req = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/ICMPv6EchoRequest(data="waytoomanynapkins") - echo_repl = iface.sr1(echo_req, timeout=1) - assert(echo_repl is not None), "expecting answer, got nothing" - assert(ICMPv6EchoReply in echo_repl) - echo_repl = echo_repl[ICMPv6EchoReply] - # check answer content - assert(echo_repl.code == 0) - assert(echo_repl.data == echo_req.data) - -@test -def test_tcp_syn(iface): - ##### SYN-ACK ##### - # test a list of ports, randomly generated once - ports_to_test = [ - 1152, 2003, 2193, 3709, 4054, 6605, 6737, 6875, 7320, 8898, 9513, 9738, 10623, 10723, - 11253, 12125, 12189, 12873, 14648, 14659, 16242, 16243, 17209, 17492, 17667, 17838, - 18081, 18682, 18790, 19124, 19288, 19558, 19628, 19789, 20093, 21014, 21459, 21740, - 24070, 24312, 24576, 26939, 27136, 27165, 27361, 29971, 31088, 33011, 33068, 34990, - 35093, 35958, 36626, 36789, 37130, 37238, 37256, 37697, 37890, 38958, 42131, 43864, - 44420, 44655, 44868, 45157, 46213, 46497, 46955, 49049, 49067, 49452, 49480, 50498, - 50945, 51181, 52890, 53301, 53407, 53417, 53980, 55827, 56483, 58552, 58713, 58836, - 59362, 59560, 60534, 60555, 60660, 61615, 62402, 62533, 62941, 63240, 63339, 63616, - 64380, 65438, - ] - for p in ports_to_test: - syn = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="S", dport=p) - syn_ack = iface.sr1(syn, timeout=1) - assert(syn_ack is not None), "expecting answer, got nothing" - check_ip_checksum(syn_ack) - assert(TCP in syn_ack) - syn_ack = syn_ack[TCP] - assert(syn_ack.flags == "SA") - -@test -def test_ipv4_tcp_psh_ack(iface): - ##### PSH-ACK ##### - sport = 26695 - port = 445 - # send PSH-ACK first - psh_ack = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="PA", dport=port)/Raw("payload") - syn_ack = iface.sr1(psh_ack, timeout=1) - assert(syn_ack is None), "no answer expected, got one" - # test the anti-injection mechanism - syn = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="S", dport=port) - syn_ack = iface.sr1(syn, timeout=1) - assert(syn_ack is not None), "expecting answer, got nothing" - check_ip_checksum(syn_ack) - assert(TCP in syn_ack) - syn_ack = syn_ack[TCP] - assert(syn_ack.flags == "SA") - ack = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="A", dport=port) - # should fail because no ack given - psh_ack = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="PA", dport=port) - ack = iface.sr1(psh_ack, timeout=1) - assert(ack is None), "no answer expected, got one" - # should get an answer this time - psh_ack = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="PA", dport=port, ack=syn_ack.seq + 1) - ack = iface.sr1(psh_ack, timeout=1) - assert(ack is not None), "expecting answer, got nothing" - check_ip_checksum(ack) - assert(TCP in ack) - ack = ack[TCP] - assert(ack.flags == "A") - -@test -def test_ipv6_tcp_psh_ack(iface): - ##### PSH-ACK ##### - sport = 26695 - port = 445 - # send PSH-ACK first - psh_ack = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/TCP(flags="PA", dport=port)/Raw("payload") - syn_ack = iface.sr1(psh_ack, timeout=1) - assert(syn_ack is None), "no answer expected, got one" - # test the anti-injection mechanism - syn = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/TCP(flags="S", dport=port) - syn_ack = iface.sr1(syn, timeout=1) - assert(syn_ack is not None), "expecting answer, got nothing" - check_ipv6_checksum(syn_ack) - assert(TCP in syn_ack) - syn_ack = syn_ack[TCP] - assert(syn_ack.flags == "SA") - ack = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/TCP(flags="A", dport=port) - # should fail because no ack given - psh_ack = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/TCP(flags="PA", dport=port) - ack = iface.sr1(psh_ack, timeout=1) - assert(ack is None), "no answer expected, got one" - # should get an answer this time - psh_ack = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/TCP(flags="PA", dport=port, ack=syn_ack.seq + 1) - ack = iface.sr1(psh_ack, timeout=1) - assert(ack is not None), "expecting answer, got nothing" - check_ipv6_checksum(ack) - assert(TCP in ack) - ack = ack[TCP] - assert(ack.flags == "A") - -@test -def test_ipv4_tcp_http(iface): - sport = 24592 - dports = [80, 443, 5000, 53228] - for dport in dports: - syn = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="S", sport=sport, dport=dport) - syn_ack = iface.sr1(syn, timeout=1) - assert(syn_ack is not None), "expecting answer, got nothing" - check_ip_checksum(syn_ack) - assert(TCP in syn_ack) - syn_ack = syn_ack[TCP] - assert(syn_ack.flags == "SA") - ack = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="A", sport=sport, dport=dport, ack=syn_ack.seq + 1) - _ = iface.sr1(ack, timeout=1) - req = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="PA", ack=syn_ack.seq + 1, sport=sport, dport=dport)/Raw("GET / HTTP/1.1\r\n\r\n") - resp = iface.sr1(req, timeout=1) - assert(resp is not None), "expecting answer, got nothing" - check_ip_checksum(resp) - assert(TCP in resp) - tcp = resp[TCP] - assert(tcp.payload.load.startswith(b"HTTP/1.1 401 Unauthorized\n")) - -@test -def test_ipv6_tcp_http(iface): - sport = 24592 - dports = [80, 443, 5000, 53228] - for dport in dports: - syn = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/TCP(flags="S", sport=sport, dport=dport) - syn_ack = iface.sr1(syn, timeout=1) - assert(syn_ack is not None), "expecting answer, got nothing" - check_ipv6_checksum(syn_ack) - assert(TCP in syn_ack) - syn_ack = syn_ack[TCP] - assert(syn_ack.flags == "SA") - ack = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/TCP(flags="A", sport=sport, dport=dport, ack=syn_ack.seq + 1) - _ = iface.sr1(ack, timeout=1) - req = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/TCP(flags="PA", ack=syn_ack.seq + 1, sport=sport, dport=dport)/Raw("GET / HTTP/1.1\r\n\r\n") - resp = iface.sr1(req, timeout=1) - assert(resp is not None), "expecting answer, got nothing" - check_ipv6_checksum(resp) - assert(TCP in resp) - tcp = resp[TCP] - assert(tcp.payload.load.startswith(b"HTTP/1.1 401 Unauthorized\n")) - -@test -def test_ipv4_udp_http(iface): - sport = 24592 - dports = [80, 443, 5000, 53228] - for dport in dports: - req = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/UDP(sport=sport, dport=dport)/Raw("GET / HTTP/1.1\r\n\r\n") - resp = iface.sr1(req, timeout=1) - assert(resp is not None), "expecting answer, got nothing" - check_ip_checksum(resp) - assert(UDP in resp) - udp = resp[UDP] - assert(udp.payload.load.startswith(b"HTTP/1.1 401 Unauthorized\n")) - -@test -def test_ipv6_udp_http(iface): - sport = 24592 - dports = [80, 443, 5000, 53228] - for dport in dports: - req = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/UDP(sport=sport, dport=dport)/Raw("GET / HTTP/1.1\r\n\r\n") - resp = iface.sr1(req, timeout=1) - assert(resp is not None), "expecting answer, got nothing" - check_ipv6_checksum(resp) - assert(UDP in resp) - udp = resp[UDP] - assert(udp.payload.load.startswith(b"HTTP/1.1 401 Unauthorized\n")) - -@test -def test_ipv4_tcp_http_ko(iface): - sport = 24592 - dports = [80, 443, 5000, 53228] - for dport in dports: - syn = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="S", sport=sport, dport=dport) - syn_ack = iface.sr1(syn, timeout=1) - assert(syn_ack is not None), "expecting answer, got nothing" - check_ip_checksum(syn_ack) - assert(TCP in syn_ack) - syn_ack = syn_ack[TCP] - assert(syn_ack.flags == "SA") - ack = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="A", sport=sport, dport=dport, ack=syn_ack.seq + 1) - _ = iface.sr1(ack, timeout=1) - req = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="PA", ack=syn_ack.seq + 1, sport=sport, dport=dport)/Raw(bytes.fromhex("4f5054494f4e53")) - resp = iface.sr1(req, timeout=1) - assert(resp is not None), "expecting answer, got nothing" - check_ip_checksum(resp) - assert(TCP in resp) - assert("P" not in resp[TCP].flags) - assert(len(resp[TCP].payload) == 0) - -@test -def test_ipv4_udp_http_ko(iface): - sport = 24592 - dports = [80, 443, 5000, 53228] - for dport in dports: - req = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/UDP(sport=sport, dport=dport)/Raw(bytes.fromhex("4f5054494f4e53")) - resp = iface.sr1(req, timeout=1) - assert(resp is None), "expecting no answer, got one" - -@test -def test_ipv6_tcp_http_ko(iface): - sport = 24592 - dports = [80, 443, 5000, 53228] - for dport in dports: - syn = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/TCP(flags="S", sport=sport, dport=dport) - syn_ack = iface.sr1(syn, timeout=1) - assert(syn_ack is not None), "expecting answer, got nothing" - check_ipv6_checksum(syn_ack) - assert(TCP in syn_ack) - syn_ack = syn_ack[TCP] - assert(syn_ack.flags == "SA") - ack = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/TCP(flags="A", sport=sport, dport=dport, ack=syn_ack.seq + 1) - _ = iface.sr1(ack, timeout=1) - req = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/TCP(flags="PA", ack=syn_ack.seq + 1, sport=sport, dport=dport)/Raw(bytes.fromhex("4f5054494f4e53")) - resp = iface.sr1(req, timeout=1) - assert(resp is not None), "expecting answer, got nothing" - check_ipv6_checksum(resp) - assert(TCP in resp) - assert("P" not in resp[TCP].flags) - assert(len(resp[TCP].payload) == 0) - -@test -def test_ipv6_udp_http_ko(iface): - sport = 24592 - dports = [80, 443, 5000, 53228] - for dport in dports: - req = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/UDP(sport=sport, dport=dport)/Raw(bytes.fromhex("4f5054494f4e53")) - resp = iface.sr1(req, timeout=1) - assert(resp is None), "expecting no answer, got one" - -@test -def test_ipv4_udp_stun(iface): - sports = [12345, 55555, 80, 43273] - dports = [80, 800, 8000, 3478] - payload = bytes.fromhex("000100002112a442000000000000000000000000") - for sport in sports: - for dport in dports: - req = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/UDP(sport=sport, dport=dport)/Raw(payload) - resp = iface.sr1(req, timeout=1) - assert(resp is not None), "expecting answer, got nothing" - check_ip_checksum(resp) - assert(UDP in resp), "no UDP layer found" - udp = resp[UDP] - assert(udp.sport == dport), "unexpected UDP sport: {}".format(udp.sport) - assert(udp.dport == sport), "unexpected UDP dport: {}".format(udp.dport) - resp_payload = udp.payload.load - type_, length, magic = struct.unpack(">HHI", resp_payload[:8]) - tid = resp_payload[8:20] - data = resp_payload[20:] - assert(type_ == 0x0101), "expected type 0X0101, got 0x{:04x}".format(type_) - assert(length == 12), "expected length 12, got {}".format(length) - assert(magic == 0x2112a442), "expected magic 0x2112a442, got 0x{:08x}".format(magic) - assert(tid == b'\x00' * 12), "expected tid 0x000000000000000000000000, got {:x}".format(tid) - assert(data == bytes.fromhex("000100080001") + struct.pack(">H", sport) + bytes.fromhex("00000000")), "unexpected data" - -@test -def test_ipv6_udp_stun(iface): - sports = [12345, 55555, 80, 43273] - dports = [80, 800, 8000, 3478] - payload = bytes.fromhex("000100002112a442000000000000000000000000") - for sport in sports: - for dport in dports: - req = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/UDP(sport=sport, dport=dport)/Raw(payload) - resp = iface.sr1(req, timeout=1) - assert(resp is not None), "expecting answer, got nothing" - check_ipv6_checksum(resp) - assert(UDP in resp) - udp = resp[UDP] - assert(udp.sport == dport) - assert(udp.dport == sport) - resp_payload = udp.payload.load - type_, length, magic = struct.unpack(">HHI", resp_payload[:8]) - tid = resp_payload[8:20] - data = resp_payload[20:] - assert(type_ == 0x0101), "expected type 0X0101, got 0x{:04x}".format(type_) - assert(length == 24), "expected length 24, got {}".format(length) - assert(magic == 0x2112a442), "expected magic 0x2112a442, got 0x{:08x}".format(magic) - assert(tid == b'\x00' * 12), "expected tid 0x000000000000000000000000, got {:x}".format(tid) - assert(data == bytes.fromhex("000100140002") + struct.pack(">H", sport) + bytes.fromhex("00000000" * 4)), "unexpected data: {}".format(data) - -@test -def test_ipv4_udp_stun_change_port(iface): - sports = [12345, 55555, 80, 43273] - dports = [80, 800, 8000, 3478, 65535] - payload = bytes.fromhex("0001000803a3b9464dd8eb75e19481474293845c0003000400000002") - for sport in sports: - for dport in dports: - req = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/UDP(sport=sport, dport=dport)/Raw(payload) - resp = iface.sr1(req, timeout=1) - assert(resp is not None), "expecting answer, got nothing" - check_ip_checksum(resp) - assert(UDP in resp), "no UDP layer found" - udp = resp[UDP] - assert(udp.sport == (dport + 1) % 2**16), "expected answer from UDP/{}, got it from UDP/{}".format((dport + 1) % 2**16, udp.sport) - assert(udp.dport == sport), "expected answer to UDP/{}, got it to UDP/{}".format(sport, udp.dport) - resp_payload = udp.payload.load - type_, length = struct.unpack(">HH", resp_payload[:4]) - tid = resp_payload[4:20] - data = resp_payload[20:] - assert(type_ == 0x0101), "expected type 0X0101, got 0x{:04x}".format(type_) - assert(length == 12), "expected length 12, got {}".format(length) - assert(tid == bytes.fromhex("03a3b9464dd8eb75e19481474293845c")), "expected tid 0x03a3b9464dd8eb75e19481474293845c, got %r" % tid - assert(data == bytes.fromhex("000100080001") + struct.pack(">H", sport) + bytes.fromhex("00000000")), "unexpected data" - -@test -def test_ipv6_udp_stun_change_port(iface): - sports = [12345, 55555, 80, 43273] - dports = [80, 800, 8000, 3478, 65535] - payload = bytes.fromhex("0001000803a3b9464dd8eb75e19481474293845c0003000400000002") - for sport in sports: - for dport in dports: - req = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/UDP(sport=sport, dport=dport)/Raw(payload) - resp = iface.sr1(req, timeout=1) - assert(resp is not None), "expecting answer, got nothing" - check_ipv6_checksum(resp) - assert(UDP in resp), "expecting UDP layer in answer, got nothing" - udp = resp[UDP] - assert(udp.sport == (dport + 1) % 2**16), "expected answer from UDP/{}, got it from UDP/{}".format((dport + 1) % 2**16, udp.sport) - assert(udp.dport == sport), "expected answer to UDP/{}, got it to UDP/{}".format(sport, udp.dport) - resp_payload = udp.payload.load - type_, length = struct.unpack(">HH", resp_payload[:4]) - tid = resp_payload[4:20] - data = resp_payload[20:] - assert(type_ == 0x0101), "expected type 0X0101, got 0x{:04x}".format(type_) - assert(length == 24), "expected length 12, got {}".format(length) - assert(tid == bytes.fromhex("03a3b9464dd8eb75e19481474293845c")), "expected tid 0x03a3b9464dd8eb75e19481474293845c, got %r" % tid - assert(data == bytes.fromhex("000100140002") + struct.pack(">H", sport) + bytes.fromhex("00000000" * 4)) - -@test -def test_ipv4_tcp_ssh(iface): - sport = 37183 - dports = [22, 80, 2222, 2022, 23874, 50000] - for i, dport in enumerate(dports): - banner = [b"SSH-2.0-AsyncSSH_2.1.0", b"SSH-2.0-PuTTY", b"SSH-2.0-libssh2_1.4.3", b"SSH-2.0-Go", b"SSH-2.0-PUTTY"][i % 5] - syn = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="S", sport=sport, dport=dport) - syn_ack = iface.sr1(syn, timeout=1) - assert(syn_ack is not None), "expecting answer, got nothing" - check_ip_checksum(syn_ack) - assert(TCP in syn_ack) - syn_ack = syn_ack[TCP] - assert(syn_ack.flags == "SA") - ack = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="A", sport=sport, dport=dport, ack=syn_ack.seq + 1) - _ = iface.sr1(ack, timeout=1) - req = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="PA", ack=syn_ack.seq + 1, sport=sport, dport=dport)/Raw(banner + b"\r\n") - resp = iface.sr1(req, timeout=1) - assert(resp is not None), "expecting answer, got nothing" - check_ip_checksum(resp) - assert(TCP in resp) - tcp = resp[TCP] - assert("A" in tcp.flags), "expecting ACK flag, not set (%r)" % tcp.flags - assert("P" in tcp.flags), "expecting PSH flag, not set (%r)" % tcp.flags - assert(len(tcp.payload) > 0), "expecting payload, got none" - assert(tcp.payload.load.startswith(b"SSH-2.0-")), "unexpected banner: %r" % tcp.payload.load - assert(tcp.payload.load.endswith(b"\r\n")), "unexpected banner: %r" % tcp.payload.load - -@test -def test_ipv4_udp_ssh(iface): - sport = 37183 - dports = [22, 80, 2222, 2022, 23874, 50000] - for i, dport in enumerate(dports): - banner = [b"SSH-2.0-AsyncSSH_2.1.0", b"SSH-2.0-PuTTY", b"SSH-2.0-libssh2_1.4.3", b"SSH-2.0-Go", b"SSH-2.0-PUTTY"][i % 5] - req = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/UDP(sport=sport, dport=dport)/Raw(banner + b"\r\n") - resp = iface.sr1(req, timeout=1) - assert(resp is not None), "expecting answer, got nothing" - check_ip_checksum(resp) - assert(UDP in resp) - udp = resp[UDP] - assert(len(udp.payload) > 0), "expecting payload, got none" - assert(udp.payload.load.startswith(b"SSH-2.0-")), "unexpected banner: %r" % udp.payload.load - assert(udp.payload.load.endswith(b"\r\n")), "unexpected banner: %r" % udp.payload.load - -@test -def test_ipv6_tcp_ssh(iface): - sport = 37183 - dports = [22, 80, 2222, 2022, 23874, 50000] - for i, dport in enumerate(dports): - banner = [b"SSH-2.0-AsyncSSH_2.1.0", b"SSH-2.0-PuTTY", b"SSH-2.0-libssh2_1.4.3", b"SSH-2.0-Go", b"SSH-2.0-PUTTY"][i % 5] - syn = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/TCP(flags="S", sport=sport, dport=dport) - syn_ack = iface.sr1(syn, timeout=1) - assert(syn_ack is not None), "expecting answer, got nothing" - check_ipv6_checksum(syn_ack) - assert(TCP in syn_ack) - syn_ack = syn_ack[TCP] - assert(syn_ack.flags == "SA") - ack = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/TCP(flags="A", sport=sport, dport=dport, ack=syn_ack.seq + 1) - _ = iface.sr1(ack, timeout=1) - req = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/TCP(flags="PA", ack=syn_ack.seq + 1, sport=sport, dport=dport)/Raw(banner + b"\r\n") - resp = iface.sr1(req, timeout=1) - assert(resp is not None), "expecting answer, got nothing" - check_ipv6_checksum(resp) - assert(TCP in resp) - tcp = resp[TCP] - assert("A" in tcp.flags), "expecting ACK flag, not set (%r)" % tcp.flags - assert("P" in tcp.flags), "expecting PSH flag, not set (%r)" % tcp.flags - assert(len(tcp.payload) > 0), "expecting payload, got none" - assert(tcp.payload.load.startswith(b"SSH-2.0-")), "unexpected banner: %r" % tcp.payload.load - assert(tcp.payload.load.endswith(b"\r\n")), "unexpected banner: %r" % tcp.payload.load - -@test -def test_ipv6_udp_ssh(iface): - sport = 37183 - dports = [22, 80, 2222, 2022, 23874, 50000] - for i, dport in enumerate(dports): - banner = [b"SSH-2.0-AsyncSSH_2.1.0", b"SSH-2.0-PuTTY", b"SSH-2.0-libssh2_1.4.3", b"SSH-2.0-Go", b"SSH-2.0-PUTTY"][i % 5] - req = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/UDP(sport=sport, dport=dport)/Raw(banner + b"\r\n") - resp = iface.sr1(req, timeout=1) - assert(resp is not None), "expecting answer, got nothing" - check_ipv6_checksum(resp) - assert(UDP in resp) - udp = resp[UDP] - assert(len(udp.payload) > 0), "expecting payload, got none" - assert(udp.payload.load.startswith(b"SSH-2.0-")), "unexpected banner: %r" % udp.payload.load - assert(udp.payload.load.endswith(b"\r\n")), "unexpected banner: %r" % udp.payload.load - -def test_all(iface): - global tests - # execute tests - for t in tests: - t(iface) +for test in ENABLED_TESTS: + importlib.import_module(".tests." + test, package="src") diff --git a/test/src/conf.py b/test/src/conf.py index f7fbd2b..18affd9 100644 --- a/test/src/conf.py +++ b/test/src/conf.py @@ -16,5 +16,5 @@ IPV4_ADDR = "192.0.0.1" IPV6_ADDR = "2001:41d0::ab32:bdb8" -MAC_ADDR = "52:1c:4e:c2:a4:1f" +MAC_ADDR = "52:1c:4e:c2:a4:1f" OUTDIR = "test/res/" diff --git a/test/src/core.py b/test/src/core.py new file mode 100644 index 0000000..829f314 --- /dev/null +++ b/test/src/core.py @@ -0,0 +1,102 @@ +# This file is part of masscanned. +# Copyright 2021 - 2025 - The IVRE project +# +# Masscanned is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Masscanned is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +# License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Masscanned. If not, see . + +import logging + +from scapy.compat import raw +from scapy.layers.inet import IP +from scapy.layers.inet6 import IPv6 + + +def setup_logs(): + log = logging.getLogger() + log.setLevel(logging.DEBUG) + if not log.handlers: + ch = logging.StreamHandler() + ch.setFormatter(logging.Formatter("%(levelname)s\t%(message)s")) + ch.setLevel(logging.DEBUG) + log.addHandler(ch) + return log + + +LOG = setup_logs() +TESTS = [] +ERRORS = [] + + +# decorator to automatically add a function to tests +def test(f): + OK = "\033[1mOK\033[0m" + KO = "\033[1m\033[1;%dmKO\033[0m" % 31 + fname = f.__name__.ljust(50, ".") + + def w(m): + try: + # check that masscanned is still running + assert m.poll() is None, "masscanned not running" + f() + # check that masscanned is still running + assert m.poll() is None, "masscanned terminated unexpectedly" + LOG.info("{}{}".format(fname, OK)) + except AssertionError as e: + LOG.error("{}{}: {}".format(fname, KO, e)) + ERRORS.append(fname) + + TESTS.append(w) + return w + + +def test_all(m): + # execute tests + for t in TESTS: + # perform unit test + t(m) + LOG.info(f"\033[1mRan {len(TESTS)} tests with {len(ERRORS)} errors\033[0m") + return len(ERRORS) + + +def multicast(ip6): + a, b = ip6.split(":")[-2:] + mac = ["33", "33", "ff"] + if len(a) == 4: + mac.append(a[2:]) + else: + mac.append("00") + if len(b) >= 2: + mac.append(b[:2]) + else: + mac.append("00") + if len(b) >= 4: + mac.append(b[2:]) + else: + mac.append("00") + return ":".join(mac) + + +def check_ip_checksum(pkt): + assert IP in pkt, "no IP layer found" + ip_pkt = pkt[IP] + chksum = ip_pkt.chksum + del ip_pkt.chksum + assert IP(raw(ip_pkt)).chksum == chksum, "bad IPv4 checksum" + + +def check_ipv6_checksum(pkt): + assert IPv6 in pkt, "no IP layer found" + ip_pkt = pkt[IPv6] + chksum = ip_pkt.chksum + del ip_pkt.chksum + assert IPv6(raw(ip_pkt)).chksum == chksum, "bad IPv6 checksum" diff --git a/test/src/tests/__init__.py b/test/src/tests/__init__.py new file mode 100644 index 0000000..831f613 --- /dev/null +++ b/test/src/tests/__init__.py @@ -0,0 +1,15 @@ +# This file is part of masscanned. +# Copyright 2021 - The IVRE project +# +# Masscanned is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Masscanned is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +# License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Masscanned. If not, see . diff --git a/test/src/tests/arp.py b/test/src/tests/arp.py new file mode 100644 index 0000000..4f65cb7 --- /dev/null +++ b/test/src/tests/arp.py @@ -0,0 +1,51 @@ +# This file is part of masscanned. +# Copyright 2021 - The IVRE project +# +# Masscanned is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Masscanned is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +# License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Masscanned. If not, see . + +from scapy.layers.l2 import Ether, ARP, ETHER_BROADCAST +from scapy.sendrecv import srp1 + +from ..conf import IPV4_ADDR, MAC_ADDR +from ..core import test + + +@test +def test_arp_req(): + ##### ARP ##### + arp_req = Ether(dst=ETHER_BROADCAST) / ARP(pdst=IPV4_ADDR) + arp_repl = srp1(arp_req, timeout=1) + assert arp_repl is not None, "expecting answer, got nothing" + assert ARP in arp_repl, "no ARP layer found" + arp_repl = arp_repl[ARP] + # check answer + ## op is "is-at" + assert arp_repl.op == 2, "unexpected ARP op: {}".format(arp_repl.op) + ## answer for the requested IP + assert arp_repl.psrc == arp_req.pdst, "unexpected ARP psrc: {}".format( + arp_repl.psrc + ) + assert arp_repl.pdst == arp_req.psrc, "unexpected ARP pdst: {}".format( + arp_repl.pdst + ) + ## answer is expected MAC address + assert arp_repl.hwsrc == MAC_ADDR, "unexpected ARP hwsrc: {}".format(arp_repl.hwsrc) + + +@test +def test_arp_req_other_ip(): + ##### ARP ##### + arp_req = Ether(dst=ETHER_BROADCAST) / ARP(pdst="1.2.3.4") + arp_repl = srp1(arp_req, timeout=1) + assert arp_repl is None, "responding to ARP requests for other IP addresses" diff --git a/test/src/tests/dns.py b/test/src/tests/dns.py new file mode 100644 index 0000000..93ab1ac --- /dev/null +++ b/test/src/tests/dns.py @@ -0,0 +1,158 @@ +# This file is part of masscanned. +# Copyright 2022 - The IVRE project +# +# Masscanned is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Masscanned is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +# License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Masscanned. If not, see . + +from scapy.compat import raw +from scapy.layers.dns import DNS, DNSQR +from scapy.layers.inet import IP, UDP +from scapy.layers.l2 import Ether +from scapy.sendrecv import srp1 + +from ..conf import IPV4_ADDR, MAC_ADDR +from ..core import test, check_ip_checksum + + +@test +def test_ipv4_udp_dns_in_a(): + sports = [53, 13274, 0] + dports = [53, 5353, 80, 161, 24732] + for sport in sports: + for dport in dports: + for domain in ["example.com", "www.example.com", "masscan.ned"]: + qd = DNSQR(qname=domain, qtype="A", qclass="IN") + dns_req = DNS(id=1234, rd=False, opcode=0, qd=qd) + req = ( + Ether(dst=MAC_ADDR) + / IP(dst=IPV4_ADDR) + / UDP(sport=sport, dport=dport) + / dns_req + ) + resp = srp1(req, timeout=1) + assert resp is not None, "expecting answer, got nothing" + check_ip_checksum(resp) + assert UDP in resp, "no UDP layer found" + udp = resp[UDP] + assert ( + udp.sport == dport + ), f"unexpected UDP sport: {udp.sport!r} ({domain})" + assert ( + udp.dport == sport + ), f"unexpected UDP dport: {udp.dport!r} ({domain})" + if DNS not in udp: + try: + dns_rep = DNS(udp.load) + except Exception: + raise AssertionError("no DNS layer found") + else: + dns_rep = udp[DNS] + assert ( + dns_rep.id == 1234 + ), f"unexpected id value: {dns_rep.id!r} ({domain})" + assert dns_rep.qr, "unexpected qr value" + assert dns_rep.opcode == 0, "unexpected opcode value" + assert dns_rep.aa, "unexpected aa value" + assert not dns_rep.tc, "unexpected tc value" + assert not dns_rep.rd, "unexpected rd value" + assert not dns_rep.ra, "unexpected ra value" + assert dns_rep.z == 0, "unexpected z value" + assert dns_rep.rcode == 0, "unexpected rcode value" + assert ( + dns_rep.qdcount == 1 + ), f"unexpected qdcount value: {dns_rep.qdcount!r} vs 1 ({domain})" + assert dns_rep.ancount == 1, "unexpected ancount value" + assert dns_rep.nscount == 0, "unexpected nscount value" + assert dns_rep.arcount == 0, "unexpected arcount value" + assert raw(dns_rep.qd[0]) == raw( + dns_req.qd[0] + ), "query in request and response do not match" + assert raw(dns_rep.qd[0].qname) == raw( + dns_req.qd[0].qname + ), "qname query in request and response do not match" + assert ( + dns_rep.an[0].rrname == dns_req.qd[0].qname + ), "rrname in answer does not match qname in request" + assert ( + dns_rep.an[0].rclass == dns_req.qd[0].qclass + ), "class in answer does not match query" + assert ( + dns_rep.an[0].type == dns_req.qd[0].qtype + ), "type in answer does not match query" + assert dns_rep.an[0].rdata == IPV4_ADDR + + +@test +def test_ipv4_udp_dns_in_a_multiple_queries(): + sports = [53, 13274, 12198, 888, 0] + dports = [53, 5353, 80, 161, 24732] + for sport in sports: + for dport in dports: + qd = [ + DNSQR(qname="www.example1.com", qtype="A", qclass="IN"), + DNSQR(qname="www.example2.com", qtype="A", qclass="IN"), + DNSQR(qname="www.example3.com", qtype="A", qclass="IN"), + ] + dns_req = DNS(id=1234, rd=False, opcode=0, qd=qd) + req = ( + Ether(dst=MAC_ADDR) + / IP(dst=IPV4_ADDR) + / UDP(sport=sport, dport=dport) + / dns_req + ) + resp = srp1(req, timeout=1) + assert resp is not None, "expecting answer, got nothing" + check_ip_checksum(resp) + assert UDP in resp, "no UDP layer found" + udp = resp[UDP] + assert udp.sport == dport, "unexpected UDP sport: {}".format(udp.sport) + assert udp.dport == sport, "unexpected UDP dport: {}".format(udp.dport) + if DNS not in udp: + try: + dns_rep = DNS(udp.load) + except Exception: + raise AssertionError("no DNS layer found") + else: + dns_rep = udp[DNS] + assert dns_rep.id == 1234, f"unexpected id value: {dns_rep.id}" + assert dns_rep.qr, "unexpected qr value" + assert dns_rep.opcode == 0, "unexpected opcode value" + assert dns_rep.aa, "unexpected aa value" + assert not dns_rep.tc, "unexpected tc value" + assert not dns_rep.rd, "unexpected rd value" + assert not dns_rep.ra, "unexpected ra value" + assert dns_rep.z == 0, "unexpected z value" + assert dns_rep.rcode == 0, "unexpected rcode value" + assert ( + dns_rep.qdcount == 3 + ), f"unexpected qdcount value: {dns_rep.qdcount} vs 3" + assert dns_rep.ancount == 3, "unexpected ancount value" + assert dns_rep.nscount == 0, "unexpected nscount value" + assert dns_rep.arcount == 0, "unexpected arcount value" + for i, q in enumerate(qd): + assert raw(dns_rep.qd[i]) == raw( + dns_req.qd[i] + ), f"query in request and response do not match ({i})" + assert raw(dns_rep.qd[i].qname) == raw( + dns_req.qd[i].qname + ), f"qname query in request and response do not match ({i})" + assert ( + dns_rep.an[i].rrname == dns_req.qd[i].qname + ), f"rrname in answer does not match qname in request ({i})" + assert ( + dns_rep.an[i].rclass == dns_req.qd[i].qclass + ), f"class in answer does not match query ({i})" + assert ( + dns_rep.an[i].type == dns_req.qd[i].qtype + ), f"type in answer does not match query ({i})" + assert dns_rep.an[i].rdata == IPV4_ADDR diff --git a/test/src/tests/ghost.py b/test/src/tests/ghost.py new file mode 100644 index 0000000..303e58e --- /dev/null +++ b/test/src/tests/ghost.py @@ -0,0 +1,87 @@ +# This file is part of masscanned. +# Copyright 2021 - The IVRE project +# +# Masscanned is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Masscanned is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +# License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Masscanned. If not, see . + +import struct +import zlib + +from scapy.compat import raw +from scapy.layers.inet import IP, TCP +from scapy.layers.l2 import Ether +from scapy.packet import Raw +from scapy.sendrecv import srp1 +from scapy.volatile import RandInt + +from ..conf import IPV4_ADDR, MAC_ADDR +from ..core import test, check_ip_checksum + + +@test +def test_ipv4_tcp_ghost(): + sport = 37184 + dports = [22, 23874] + for dport in dports: + seq_init = int(RandInt()) + banner = b"Gh0st\xad\x00\x00\x00\xe0\x00\x00\x00x\x9cKS``\x98\xc3\xc0\xc0\xc0\x06\xc4\x8c@\xbcQ\x96\x81\x81\tH\x07\xa7\x16\x95e&\xa7*\x04$&g+\x182\x94\xf6\xb000\xac\xa8rc\x00\x01\x11\xa0\x82\x1f\\`&\x83\xc7K7\x86\x19\xe5n\x0c9\x95n\x0c;\x84\x0f3\xac\xe8sch\xa8^\xcf4'J\x97\xa9\x82\xe30\xc3\x91h]&\x90\xf8\xce\x97S\xcbA4L?2=\xe1\xc4\x92\x86\x0b@\xf5`\x0cT\x1f\xae\xaf]\nr\x0b\x03#\xa3\xdc\x02~\x06\x86\x03+\x18m\xc2=\xfdtC,C\xfdL<<==\\\x9d\x19\x88\x00\xe5 \x02\x00T\xf5+\\" + syn = ( + Ether(dst=MAC_ADDR) + / IP(dst=IPV4_ADDR) + / TCP(flags="S", sport=sport, dport=dport, seq=seq_init) + ) + syn_ack = srp1(syn, timeout=1) + assert syn_ack is not None, "expecting answer, got nothing" + check_ip_checksum(syn_ack) + assert TCP in syn_ack, "expecting TCP, got %r" % syn_ack.summary() + syn_ack = syn_ack[TCP] + assert syn_ack.flags == "SA" + ack = ( + Ether(dst=MAC_ADDR) + / IP(dst=IPV4_ADDR) + / TCP( + flags="A", + sport=sport, + dport=dport, + seq=seq_init + 1, + ack=syn_ack.seq + 1, + ) + ) + _ = srp1(ack, timeout=1) + req = ( + Ether(dst=MAC_ADDR) + / IP(dst=IPV4_ADDR) + / TCP( + flags="PA", + sport=sport, + dport=dport, + seq=seq_init + 1, + ack=syn_ack.seq + 1, + ) + / Raw(banner) + ) + resp = srp1(req, timeout=1) + assert resp is not None, "expecting answer, got nothing" + check_ip_checksum(resp) + assert TCP in resp, "expecting TCP, got %r" % resp.summary() + tcp = resp[TCP] + assert "A" in tcp.flags, "expecting ACK flag, not set (%r)" % tcp.flags + assert "P" in tcp.flags, "expecting PSH flag, not set (%r)" % tcp.flags + data = raw(tcp.payload) + assert data, "expecting payload, got none" + assert data.startswith(b"Gh0st"), "unexpected banner: %r" % tcp.payload.load + data_len, uncompressed_len = struct.unpack(". + +from scapy.layers.inet import IP, TCP, UDP +from scapy.layers.inet6 import IPv6 +from scapy.layers.l2 import Ether +from scapy.packet import Raw +from scapy.sendrecv import srp1 +from scapy.volatile import RandInt + +from ..conf import IPV4_ADDR, IPV6_ADDR, MAC_ADDR +from ..core import test, check_ip_checksum, check_ipv6_checksum + + +@test +def test_ipv4_tcp_http(): + sport = 24592 + dports = [80, 443, 5000, 53228] + for dport in dports: + seq_init = int(RandInt()) + syn = ( + Ether(dst=MAC_ADDR) + / IP(dst=IPV4_ADDR) + / TCP(flags="S", sport=sport, dport=dport, seq=seq_init) + ) + syn_ack = srp1(syn, timeout=1) + assert syn_ack is not None, "expecting answer, got nothing" + check_ip_checksum(syn_ack) + assert TCP in syn_ack, "expecting TCP, got %r" % syn_ack.summary() + syn_ack = syn_ack[TCP] + assert syn_ack.flags == "SA", "expecting TCP SA, got %r" % syn_ack.flags + ack = ( + Ether(dst=MAC_ADDR) + / IP(dst=IPV4_ADDR) + / TCP( + flags="A", + sport=sport, + dport=dport, + seq=seq_init + 1, + ack=syn_ack.seq + 1, + ) + ) + _ = srp1(ack, timeout=1) + req = ( + Ether(dst=MAC_ADDR) + / IP(dst=IPV4_ADDR) + / TCP( + flags="PA", + sport=sport, + dport=dport, + seq=seq_init + 1, + ack=syn_ack.seq + 1, + ) + / Raw("GET / HTTP/1.1\r\n\r\n") + ) + resp = srp1(req, timeout=1) + assert resp is not None, "expecting answer, got nothing" + check_ip_checksum(resp) + assert TCP in resp, "expecting TCP, got %r" % resp.summary() + tcp = resp[TCP] + assert tcp.payload.load.startswith(b"HTTP/1.1 401 Unauthorized\n") + + +@test +def test_ipv4_tcp_http_segmented(): + sport = 24593 + dports = [80, 443, 5000, 53228] + for dport in dports: + seq_init = int(RandInt()) + syn = ( + Ether(dst=MAC_ADDR) + / IP(dst=IPV4_ADDR) + / TCP(flags="S", sport=sport, dport=dport, seq=seq_init) + ) + syn_ack = srp1(syn, timeout=1) + assert syn_ack is not None, "expecting answer, got nothing" + check_ip_checksum(syn_ack) + assert TCP in syn_ack, "expecting TCP, got %r" % syn_ack.summary() + syn_ack = syn_ack[TCP] + assert syn_ack.flags == "SA", "expecting TCP SA, got %r" % syn_ack.flags + ack = ( + Ether(dst=MAC_ADDR) + / IP(dst=IPV4_ADDR) + / TCP( + flags="A", + sport=sport, + dport=dport, + seq=seq_init + 1, + ack=syn_ack.seq + 1, + ) + ) + _ = srp1(ack, timeout=1) + # request is not complete yet + req = ( + Ether(dst=MAC_ADDR) + / IP(dst=IPV4_ADDR) + / TCP( + flags="PA", + sport=sport, + dport=dport, + seq=seq_init + 1, + ack=syn_ack.seq + 1, + ) + / Raw("GET / HTTP/1.1\r\n") + ) + resp = srp1(req, timeout=1) + assert resp is not None, "expecting answer, got nothing" + check_ip_checksum(resp) + assert TCP in resp, "expecting TCP, got %r" % resp.summary() + assert resp[TCP].flags == "A", ( + 'expecting TCP flag "A", got %r' % resp[TCP].flags + ) + req = ( + Ether(dst=MAC_ADDR) + / IP(dst=IPV4_ADDR) + / TCP( + flags="PA", + sport=sport, + dport=dport, + seq=seq_init + len(req) + 1, + ack=syn_ack.seq + 1, + ) + / Raw("\r\n") + ) + resp = srp1(req, timeout=1) + assert resp is not None, "expecting answer, got nothing" + check_ip_checksum(resp) + assert TCP in resp, "expecting TCP, got %r" % resp.summary() + tcp = resp[TCP] + assert tcp.flags == "PA" + assert tcp.payload.load.startswith(b"HTTP/1.1 401 Unauthorized\n") + + +@test +def test_ipv4_tcp_http_incomplete(): + sport = 24595 + dports = [80, 443, 5000, 53228] + for dport in dports: + seq_init = int(RandInt()) + syn = ( + Ether(dst=MAC_ADDR) + / IP(dst=IPV4_ADDR) + / TCP(flags="S", sport=sport, dport=dport, seq=seq_init) + ) + syn_ack = srp1(syn, timeout=1) + assert syn_ack is not None, "expecting answer, got nothing" + check_ip_checksum(syn_ack) + assert TCP in syn_ack, "expecting TCP, got %r" % syn_ack.summary() + syn_ack = syn_ack[TCP] + assert syn_ack.flags == "SA", "expecting TCP SA, got %r" % syn_ack.flags + ack = ( + Ether(dst=MAC_ADDR) + / IP(dst=IPV4_ADDR) + / TCP( + flags="A", + sport=sport, + dport=dport, + seq=seq_init + 1, + ack=syn_ack.seq + 1, + ) + ) + _ = srp1(ack, timeout=1) + req = ( + Ether(dst=MAC_ADDR) + / IP(dst=IPV4_ADDR) + / TCP( + flags="PA", + sport=sport, + dport=dport, + seq=seq_init + 1, + ack=syn_ack.seq + 1, + ) + # purposedly incomplete request (missing additionnal ending \r\n) + / Raw("GET / HTTP/1.1\r\n") + ) + resp = srp1(req, timeout=1) + assert resp is not None, "expecting an answer, got none" + check_ip_checksum(resp) + assert TCP in resp, "expecting TCP, got %r" % resp.summary() + tcp = resp[TCP] + assert tcp.flags == "A", "expecting TCP flag A, got {}".format(tcp.flags) + + +@test +def test_ipv6_tcp_http(): + sport = 24594 + dports = [80, 443, 5000, 53228] + for dport in dports: + seq_init = int(RandInt()) + syn = ( + Ether(dst=MAC_ADDR) + / IPv6(dst=IPV6_ADDR) + / TCP(flags="S", sport=sport, dport=dport, seq=seq_init) + ) + syn_ack = srp1(syn, timeout=1) + assert syn_ack is not None, "expecting answer, got nothing" + check_ipv6_checksum(syn_ack) + assert TCP in syn_ack, "expecting TCP, got %r" % syn_ack.summary() + syn_ack = syn_ack[TCP] + assert syn_ack.flags == "SA" + ack = ( + Ether(dst=MAC_ADDR) + / IPv6(dst=IPV6_ADDR) + / TCP( + flags="A", + sport=sport, + dport=dport, + seq=seq_init + 1, + ack=syn_ack.seq + 1, + ) + ) + _ = srp1(ack, timeout=1) + req = ( + Ether(dst=MAC_ADDR) + / IPv6(dst=IPV6_ADDR) + / TCP( + flags="PA", + sport=sport, + dport=dport, + seq=seq_init + 1, + ack=syn_ack.seq + 1, + ) + / Raw("GET / HTTP/1.1\r\n\r\n") + ) + resp = srp1(req, timeout=1) + assert resp is not None, "expecting answer, got nothing" + check_ipv6_checksum(resp) + assert TCP in resp, "expecting TCP, got %r" % resp.summary() + tcp = resp[TCP] + assert tcp.payload.load.startswith(b"HTTP/1.1 401 Unauthorized\n") + + +@test +def test_ipv4_udp_http(): + sport = 24592 + dports = [80, 443, 5000, 53228] + for dport in dports: + req = ( + Ether(dst=MAC_ADDR) + / IP(dst=IPV4_ADDR) + / UDP(sport=sport, dport=dport) + / Raw("GET / HTTP/1.1\r\n\r\n") + ) + resp = srp1(req, timeout=1) + assert resp is not None, "expecting answer, got nothing" + check_ip_checksum(resp) + assert UDP in resp + udp = resp[UDP] + assert udp.payload.load.startswith(b"HTTP/1.1 401 Unauthorized\n") + + +@test +def test_ipv6_udp_http(): + sport = 24592 + dports = [80, 443, 5000, 53228] + for dport in dports: + req = ( + Ether(dst=MAC_ADDR) + / IPv6(dst=IPV6_ADDR) + / UDP(sport=sport, dport=dport) + / Raw("GET / HTTP/1.1\r\n\r\n") + ) + resp = srp1(req, timeout=1) + assert resp is not None, "expecting answer, got nothing" + check_ipv6_checksum(resp) + assert UDP in resp + udp = resp[UDP] + assert udp.payload.load.startswith(b"HTTP/1.1 401 Unauthorized\n") + + +@test +def test_ipv4_tcp_http_ko(): + sport = 24596 + dports = [80, 443, 5000, 53228] + for dport in dports: + seq_init = int(RandInt()) + syn = ( + Ether(dst=MAC_ADDR) + / IP(dst=IPV4_ADDR) + / TCP(flags="S", sport=sport, dport=dport, seq=seq_init) + ) + syn_ack = srp1(syn, timeout=1) + assert syn_ack is not None, "expecting answer, got nothing" + check_ip_checksum(syn_ack) + assert TCP in syn_ack, "expecting TCP, got %r" % syn_ack.summary() + syn_ack = syn_ack[TCP] + assert syn_ack.flags == "SA" + ack = ( + Ether(dst=MAC_ADDR) + / IP(dst=IPV4_ADDR) + / TCP( + flags="A", + sport=sport, + dport=dport, + seq=seq_init + 1, + ack=syn_ack.seq + 1, + ) + ) + _ = srp1(ack, timeout=1) + req = ( + Ether(dst=MAC_ADDR) + / IP(dst=IPV4_ADDR) + / TCP( + flags="PA", + sport=sport, + dport=dport, + seq=seq_init + 1, + ack=syn_ack.seq + 1, + ) + / Raw(bytes.fromhex("4f5054494f4e53")) + ) + resp = srp1(req, timeout=1) + assert resp is not None, "expecting answer, got nothing" + check_ip_checksum(resp) + assert TCP in resp, "expecting TCP, got %r" % resp.summary() + assert "P" not in resp[TCP].flags + assert len(resp[TCP].payload) == 0 + + +@test +def test_ipv4_udp_http_ko(): + sport = 24592 + dports = [80, 443, 5000, 53228] + for dport in dports: + req = ( + Ether(dst=MAC_ADDR) + / IP(dst=IPV4_ADDR) + / UDP(sport=sport, dport=dport) + / Raw(bytes.fromhex("4f5054494f4e53")) + ) + resp = srp1(req, timeout=1) + assert resp is None, "expecting no answer, got one" + + +@test +def test_ipv6_tcp_http_ko(): + sport = 24597 + dports = [80, 443, 5000, 53228] + for dport in dports: + seq_init = int(RandInt()) + syn = ( + Ether(dst=MAC_ADDR) + / IPv6(dst=IPV6_ADDR) + / TCP(flags="S", sport=sport, dport=dport, seq=seq_init) + ) + syn_ack = srp1(syn, timeout=1) + assert syn_ack is not None, "expecting answer, got nothing" + check_ipv6_checksum(syn_ack) + assert TCP in syn_ack, "expecting TCP, got %r" % syn_ack.summary() + syn_ack = syn_ack[TCP] + assert syn_ack.flags == "SA" + ack = ( + Ether(dst=MAC_ADDR) + / IPv6(dst=IPV6_ADDR) + / TCP( + flags="A", + sport=sport, + dport=dport, + seq=seq_init + 1, + ack=syn_ack.seq + 1, + ) + ) + _ = srp1(ack, timeout=1) + req = ( + Ether(dst=MAC_ADDR) + / IPv6(dst=IPV6_ADDR) + / TCP( + flags="PA", + sport=sport, + dport=dport, + seq=seq_init + 1, + ack=syn_ack.seq + 1, + ) + / Raw(bytes.fromhex("4f5054494f4e53")) + ) + resp = srp1(req, timeout=1) + assert resp is not None, "expecting answer, got nothing" + check_ipv6_checksum(resp) + assert TCP in resp, "expecting TCP, got %r" % resp.summary() + assert "P" not in resp[TCP].flags + assert len(resp[TCP].payload) == 0 + + +@test +def test_ipv6_udp_http_ko(): + sport = 24592 + dports = [80, 443, 5000, 53228] + for dport in dports: + req = ( + Ether(dst=MAC_ADDR) + / IPv6(dst=IPV6_ADDR) + / UDP(sport=sport, dport=dport) + / Raw(bytes.fromhex("4f5054494f4e53")) + ) + resp = srp1(req, timeout=1) + assert resp is None, "expecting no answer, got one" diff --git a/test/src/tests/icmpv4.py b/test/src/tests/icmpv4.py new file mode 100644 index 0000000..7722148 --- /dev/null +++ b/test/src/tests/icmpv4.py @@ -0,0 +1,45 @@ +# This file is part of masscanned. +# Copyright 2021 - The IVRE project +# +# Masscanned is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Masscanned is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +# License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Masscanned. If not, see . + +from scapy.layers.inet import IP, ICMP +from scapy.layers.l2 import Ether +from scapy.packet import Raw +from scapy.sendrecv import srp1 + +from ..conf import IPV4_ADDR, MAC_ADDR +from ..core import test, check_ip_checksum + + +@test +def test_icmpv4_echo_req(): + ##### ICMPv4 ##### + icmp_req = ( + Ether(dst=MAC_ADDR) + / IP(dst=IPV4_ADDR) + / ICMP(type=8, code=0) + / Raw("idrinkwaytoomuchcoffee") + ) + icmp_repl = srp1(icmp_req, timeout=1) + assert icmp_repl is not None, "expecting answer, got nothing" + check_ip_checksum(icmp_repl) + assert ICMP in icmp_repl + icmp_repl = icmp_repl[ICMP] + # check answer + ## type is "echo-reply" + assert icmp_repl.type == 0 + assert icmp_repl.code == 0 + ## data is the same as sent + assert icmp_repl.load == icmp_req.load diff --git a/test/src/tests/icmpv6.py b/test/src/tests/icmpv6.py new file mode 100644 index 0000000..13a8817 --- /dev/null +++ b/test/src/tests/icmpv6.py @@ -0,0 +1,87 @@ +# This file is part of masscanned. +# Copyright 2021 - The IVRE project +# +# Masscanned is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Masscanned is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +# License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Masscanned. If not, see . + +from scapy.layers.inet6 import ( + ICMPv6EchoReply, + ICMPv6EchoRequest, + ICMPv6NDOptDstLLAddr, + ICMPv6ND_NA, + ICMPv6ND_NS, + IPv6, +) +from scapy.layers.l2 import Ether +from scapy.sendrecv import srp1 + +from ..conf import IPV6_ADDR, MAC_ADDR +from ..core import test, multicast + + +@test +def test_icmpv6_neighbor_solicitation(): + ##### IPv6 Neighbor Solicitation ##### + for mac in [ + "ff:ff:ff:ff:ff:ff", + "33:33:00:00:00:01", + MAC_ADDR, + multicast(IPV6_ADDR), + ]: + nd_ns = Ether(dst=mac) / IPv6() / ICMPv6ND_NS(tgt=IPV6_ADDR) + nd_na = srp1(nd_ns, timeout=1) + assert nd_na is not None, "expecting answer, got nothing" + assert ICMPv6ND_NA in nd_na + nd_na = nd_na[ICMPv6ND_NA] + # check answer content + assert nd_na.code == 0 + assert nd_na.R == 0 + assert nd_na.S == 1 + assert nd_na.O == 1 # noqa: E741 + assert nd_na.tgt == IPV6_ADDR + # check ND Option + assert nd_na.haslayer(ICMPv6NDOptDstLLAddr) + assert nd_na.getlayer(ICMPv6NDOptDstLLAddr).lladdr == MAC_ADDR + for mac in ["00:00:00:00:00:00", "33:33:33:00:00:01"]: + nd_ns = Ether(dst="ff:ff:ff:ff:ff:ff") / IPv6() / ICMPv6ND_NS(tgt=IPV6_ADDR) + nd_na = srp1(nd_ns, timeout=1) + assert nd_na is not None, "expecting no answer, got one" + + +@test +def test_icmpv6_neighbor_solicitation_other_ip(): + ##### IPv6 Neighbor Solicitation ##### + nd_ns = ( + Ether(dst="ff:ff:ff:ff:ff:ff") + / IPv6() + / ICMPv6ND_NS(tgt="2020:4141:3030:2020::bdbd") + ) + nd_na = srp1(nd_ns, timeout=1) + assert nd_na is None, "responding to ND_NS for other IP addresses" + + +@test +def test_icmpv6_echo_req(): + ##### IPv6 Ping ##### + echo_req = ( + Ether(dst=MAC_ADDR) + / IPv6(dst=IPV6_ADDR) + / ICMPv6EchoRequest(data="waytoomanynapkins") + ) + echo_repl = srp1(echo_req, timeout=1) + assert echo_repl is not None, "expecting answer, got nothing" + assert ICMPv6EchoReply in echo_repl + echo_repl = echo_repl[ICMPv6EchoReply] + # check answer content + assert echo_repl.code == 0 + assert echo_repl.data == echo_req.data diff --git a/test/src/tests/ip.py b/test/src/tests/ip.py new file mode 100644 index 0000000..f87fb75 --- /dev/null +++ b/test/src/tests/ip.py @@ -0,0 +1,50 @@ +# This file is part of masscanned. +# Copyright 2021 - The IVRE project +# +# Masscanned is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Masscanned is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +# License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Masscanned. If not, see . + +from scapy.layers.inet import IP, ICMP +from scapy.layers.l2 import Ether +from scapy.sendrecv import srp1 + +from ..conf import IPV4_ADDR, MAC_ADDR +from ..core import test, check_ip_checksum + + +@test +def test_ipv4_req(): + ##### IP ##### + ip_req = Ether(dst=MAC_ADDR) / IP(dst=IPV4_ADDR, id=0x1337) / ICMP(type=8, code=0) + ip_repl = srp1(ip_req, timeout=1) + assert ip_repl is not None, "expecting answer, got nothing" + check_ip_checksum(ip_repl) + assert IP in ip_repl, "no IP layer in response" + ip_repl = ip_repl[IP] + assert ip_repl.id == 0, "IP identification unexpected" + + +@test +def test_eth_req_other_mac(): + #### ETH #### + ip_req = Ether(dst="00:00:00:11:11:11") / IP(dst=IPV4_ADDR) / ICMP(type=8, code=0) + ip_repl = srp1(ip_req, timeout=1) + assert ip_repl is None, "responding to other MAC addresses" + + +@test +def test_ipv4_req_other_ip(): + ##### IP ##### + ip_req = Ether(dst=MAC_ADDR) / IP(dst="1.2.3.4") / ICMP(type=8, code=0) + ip_repl = srp1(ip_req, timeout=1) + assert ip_repl is None, "responding to other IP addresses" diff --git a/test/src/tests/rpc.py b/test/src/tests/rpc.py new file mode 100644 index 0000000..82c97a6 --- /dev/null +++ b/test/src/tests/rpc.py @@ -0,0 +1,112 @@ +# This file is part of masscanned. +# Copyright 2021 - The IVRE project +# +# Masscanned is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Masscanned is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +# License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Masscanned. If not, see . + +from subprocess import check_call +from tempfile import NamedTemporaryFile +import json +import os +import re + +from ivre.db import DBNmap + +from ..conf import IPV4_ADDR +from ..core import test + + +@test +def test_rpc_nmap(): + for scan in "SU": + with NamedTemporaryFile(delete=False) as xml_result: + check_call( + [ + "nmap", + "-n", + "-vv", + "-oX", + "-", + IPV4_ADDR, + f"-s{scan}V", + "-p", + "111", + "--script", + "rpcinfo,rpc-grind", + ], + stdout=xml_result, + ) + with NamedTemporaryFile(delete=False, mode="w") as json_result: + DBNmap(output=json_result).store_scan(xml_result.name) + os.unlink(xml_result.name) + with open(json_result.name) as fdesc: + results = [json.loads(line) for line in fdesc] + os.unlink(json_result.name) + assert len(results) == 1, f"Expected 1 result, got {len(results)}" + result = results[0] + assert len(result["ports"]) == 1, f"Expected 1 port, got {len(result['ports'])}" + port = result["ports"][0] + assert port["port"] == 111, f"Expected port 111, got {port['port']}" + assert port["protocol"] == ( + "tcp" if scan == "S" else "udp" + ), f"Unexpected proto {port['protocol']} for scan {scan}" + assert port["service_name"] in { + "nfs", + "rpcbind", + "rstatd", + "rusersd", + }, f"Unexpected service_name: {port['service_name']}" + assert port["service_extrainfo"] in { + "RPC #100000", + "RPC #100001", + "RPC #100002", + "RPC #100003", + }, f"Unexpected service_extrainfo: {port['service_extrainfo']}" + assert ( + len(port["scripts"]) == 1 + ), f"Expected 1 script, got {len(port['scripts'])}" + script = port["scripts"][0] + assert script["id"] == "rpcinfo", "Expected rpcinfo script, not found" + assert ( + len(script["rpcinfo"]) == 1 + ), f"Expected 1 rpcinfo, got {len(script['rpcinfo'])}" + + +@test +def test_rpcinfo(): + with NamedTemporaryFile(delete=False) as rpcout: + check_call(["rpcinfo", "-p", IPV4_ADDR], stdout=rpcout) + with open(rpcout.name) as fdesc: + found = [] + for line in fdesc: + line = line.split() + if line[0] == "program": + # header + continue + assert line[0] == "100000", f"Expected program 100000, got {line[0]}" + found.append(int(line[1])) + assert len(found) == 3, f"Expected three versions, got {found}" + for i in range(2, 5): + assert i in found, f"Missing version {i} in {found}" + os.unlink(rpcout.name) + with NamedTemporaryFile(delete=False) as rpcout: + check_call(["rpcinfo", "-u", IPV4_ADDR, "100000"], stdout=rpcout) + with open(rpcout.name) as fdesc: + found = [] + expr = re.compile("^program 100000 version ([0-9]) ready and waiting$") + for line in fdesc: + found.append(int(expr.search(line.strip()).group(1))) + assert len(found) == 3, f"Expected three versions, got {found}" + for i in range(2, 5): + assert i in found, f"Missing version {i} in {found}" + os.unlink(rpcout.name) diff --git a/test/src/tests/smb.py b/test/src/tests/smb.py new file mode 100644 index 0000000..3ce421d --- /dev/null +++ b/test/src/tests/smb.py @@ -0,0 +1,65 @@ +# This file is part of masscanned. +# Copyright 2021 - The IVRE project +# +# Masscanned is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Masscanned is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +# License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Masscanned. If not, see . + +import subprocess + +from ..core import test +from ..conf import IPV4_ADDR + + +@test +def test_smb1_network_req(): + proc = subprocess.Popen( + [ + "smbclient", + "-U ''", + "-N", + "-d 6", + "-t 1", + "-L", + IPV4_ADDR, + "--option=client min protocol=NT1", + ], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + out, _ = proc.communicate() + assert f"Connecting to {IPV4_ADDR} at port 445" in out, "\n" + out + assert "session request ok" in out, "\n" + out + assert f"negotiated dialect[NT1] against server[{IPV4_ADDR}]" in out, "\n" + out + + +@test +def test_smb2_network_req(): + proc = subprocess.Popen( + [ + "smbclient", + "-U ''", + "-N", + "-d 5", + "-t 1", + "-L", + IPV4_ADDR, + ], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + out, _ = proc.communicate() + assert f"Connecting to {IPV4_ADDR} at port 445" in out, "\n" + out + assert "session request ok" in out, "\n" + out + assert f"negotiated dialect[SMB2_02] against server[{IPV4_ADDR}]" in out, "\n" + out diff --git a/test/src/tests/ssh.py b/test/src/tests/ssh.py new file mode 100644 index 0000000..31501e4 --- /dev/null +++ b/test/src/tests/ssh.py @@ -0,0 +1,217 @@ +# This file is part of masscanned. +# Copyright 2021 - The IVRE project +# +# Masscanned is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Masscanned is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +# License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Masscanned. If not, see . + +from scapy.layers.inet import IP, TCP, UDP +from scapy.layers.inet6 import IPv6 +from scapy.layers.l2 import Ether +from scapy.packet import Raw +from scapy.sendrecv import srp1 +from scapy.volatile import RandInt + +from ..conf import IPV4_ADDR, IPV6_ADDR, MAC_ADDR +from ..core import test, check_ip_checksum, check_ipv6_checksum + + +@test +def test_ipv4_tcp_ssh(): + sport = 37183 + dports = [22, 80, 2222, 2022, 23874, 50000] + for i, dport in enumerate(dports): + seq_init = int(RandInt()) + banner = [ + b"SSH-2.0-AsyncSSH_2.1.0", + b"SSH-2.0-PuTTY", + b"SSH-2.0-libssh2_1.4.3", + b"SSH-2.0-Go", + b"SSH-2.0-PUTTY", + ][i % 5] + syn = ( + Ether(dst=MAC_ADDR) + / IP(dst=IPV4_ADDR) + / TCP(flags="S", sport=sport, dport=dport, seq=seq_init) + ) + syn_ack = srp1(syn, timeout=1) + assert syn_ack is not None, "expecting answer, got nothing" + check_ip_checksum(syn_ack) + assert TCP in syn_ack, "expecting TCP, got %r" % syn_ack.summary() + syn_ack = syn_ack[TCP] + assert syn_ack.flags == "SA" + ack = ( + Ether(dst=MAC_ADDR) + / IP(dst=IPV4_ADDR) + / TCP( + flags="A", + sport=sport, + dport=dport, + seq=seq_init + 1, + ack=syn_ack.seq + 1, + ) + ) + _ = srp1(ack, timeout=1) + req = ( + Ether(dst=MAC_ADDR) + / IP(dst=IPV4_ADDR) + / TCP( + flags="PA", + sport=sport, + dport=dport, + seq=seq_init + 1, + ack=syn_ack.seq + 1, + ) + / Raw(banner + b"\r\n") + ) + resp = srp1(req, timeout=1) + assert resp is not None, "expecting answer, got nothing" + check_ip_checksum(resp) + assert TCP in resp, "expecting TCP, got %r" % resp.summary() + tcp = resp[TCP] + assert "A" in tcp.flags, "expecting ACK flag, not set (%r)" % tcp.flags + assert "P" in tcp.flags, "expecting PSH flag, not set (%r)" % tcp.flags + assert len(tcp.payload) > 0, "expecting payload, got none" + assert tcp.payload.load.startswith(b"SSH-2.0-"), ( + "unexpected banner: %r" % tcp.payload.load + ) + assert tcp.payload.load.endswith(b"\r\n"), ( + "unexpected banner: %r" % tcp.payload.load + ) + + +@test +def test_ipv4_udp_ssh(): + sport = 37183 + dports = [22, 80, 2222, 2022, 23874, 50000] + for i, dport in enumerate(dports): + banner = [ + b"SSH-2.0-AsyncSSH_2.1.0", + b"SSH-2.0-PuTTY", + b"SSH-2.0-libssh2_1.4.3", + b"SSH-2.0-Go", + b"SSH-2.0-PUTTY", + ][i % 5] + req = ( + Ether(dst=MAC_ADDR) + / IP(dst=IPV4_ADDR) + / UDP(sport=sport, dport=dport) + / Raw(banner + b"\r\n") + ) + resp = srp1(req, timeout=1) + assert resp is not None, "expecting answer, got nothing" + check_ip_checksum(resp) + assert UDP in resp + udp = resp[UDP] + assert len(udp.payload) > 0, "expecting payload, got none" + assert udp.payload.load.startswith(b"SSH-2.0-"), ( + "unexpected banner: %r" % udp.payload.load + ) + assert udp.payload.load.endswith(b"\r\n"), ( + "unexpected banner: %r" % udp.payload.load + ) + + +@test +def test_ipv6_tcp_ssh(): + sport = 37183 + dports = [22, 80, 2222, 2022, 23874, 50000] + for i, dport in enumerate(dports): + seq_init = int(RandInt()) + banner = [ + b"SSH-2.0-AsyncSSH_2.1.0", + b"SSH-2.0-PuTTY", + b"SSH-2.0-libssh2_1.4.3", + b"SSH-2.0-Go", + b"SSH-2.0-PUTTY", + ][i % 5] + syn = ( + Ether(dst=MAC_ADDR) + / IPv6(dst=IPV6_ADDR) + / TCP(flags="S", sport=sport, dport=dport, seq=seq_init) + ) + syn_ack = srp1(syn, timeout=1) + assert syn_ack is not None, "expecting answer, got nothing" + check_ipv6_checksum(syn_ack) + assert TCP in syn_ack, "expecting TCP, got %r" % syn_ack.summary() + syn_ack = syn_ack[TCP] + assert syn_ack.flags == "SA" + ack = ( + Ether(dst=MAC_ADDR) + / IPv6(dst=IPV6_ADDR) + / TCP( + flags="A", + sport=sport, + dport=dport, + seq=seq_init + 1, + ack=syn_ack.seq + 1, + ) + ) + _ = srp1(ack, timeout=1) + req = ( + Ether(dst=MAC_ADDR) + / IPv6(dst=IPV6_ADDR) + / TCP( + flags="PA", + sport=sport, + dport=dport, + seq=seq_init + 1, + ack=syn_ack.seq + 1, + ) + / Raw(banner + b"\r\n") + ) + resp = srp1(req, timeout=1) + assert resp is not None, "expecting answer, got nothing" + check_ipv6_checksum(resp) + assert TCP in resp, "expecting TCP, got %r" % resp.summary() + tcp = resp[TCP] + assert "A" in tcp.flags, "expecting ACK flag, not set (%r)" % tcp.flags + assert "P" in tcp.flags, "expecting PSH flag, not set (%r)" % tcp.flags + assert len(tcp.payload) > 0, "expecting payload, got none" + assert tcp.payload.load.startswith(b"SSH-2.0-"), ( + "unexpected banner: %r" % tcp.payload.load + ) + assert tcp.payload.load.endswith(b"\r\n"), ( + "unexpected banner: %r" % tcp.payload.load + ) + + +@test +def test_ipv6_udp_ssh(): + sport = 37183 + dports = [22, 80, 2222, 2022, 23874, 50000] + for i, dport in enumerate(dports): + banner = [ + b"SSH-2.0-AsyncSSH_2.1.0", + b"SSH-2.0-PuTTY", + b"SSH-2.0-libssh2_1.4.3", + b"SSH-2.0-Go", + b"SSH-2.0-PUTTY", + ][i % 5] + req = ( + Ether(dst=MAC_ADDR) + / IPv6(dst=IPV6_ADDR) + / UDP(sport=sport, dport=dport) + / Raw(banner + b"\r\n") + ) + resp = srp1(req, timeout=1) + assert resp is not None, "expecting answer, got nothing" + check_ipv6_checksum(resp) + assert UDP in resp + udp = resp[UDP] + assert len(udp.payload) > 0, "expecting payload, got none" + assert udp.payload.load.startswith(b"SSH-2.0-"), ( + "unexpected banner: %r" % udp.payload.load + ) + assert udp.payload.load.endswith(b"\r\n"), ( + "unexpected banner: %r" % udp.payload.load + ) diff --git a/test/src/tests/stun.py b/test/src/tests/stun.py new file mode 100644 index 0000000..4062ec9 --- /dev/null +++ b/test/src/tests/stun.py @@ -0,0 +1,196 @@ +# This file is part of masscanned. +# Copyright 2021 - The IVRE project +# +# Masscanned is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Masscanned is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +# License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Masscanned. If not, see . + +from socket import AF_INET6 +import struct + +from scapy.layers.inet import IP, UDP +from scapy.layers.inet6 import IPv6 +from scapy.layers.l2 import Ether +from scapy.packet import Raw +from scapy.pton_ntop import inet_pton +from scapy.sendrecv import srp1 + +from ..conf import IPV4_ADDR, IPV6_ADDR, MAC_ADDR +from ..core import test, check_ip_checksum, check_ipv6_checksum + + +@test +def test_ipv4_udp_stun(): + sports = [12345, 55555, 80, 43273] + dports = [80, 800, 8000, 3478] + payload = bytes.fromhex("000100002112a442000000000000000000000000") + for sport in sports: + for dport in dports: + req = ( + Ether(dst=MAC_ADDR) + / IP(dst=IPV4_ADDR) + / UDP(sport=sport, dport=dport) + / Raw(payload) + ) + resp = srp1(req, timeout=1) + assert resp is not None, "expecting answer, got nothing" + check_ip_checksum(resp) + assert UDP in resp, "no UDP layer found" + udp = resp[UDP] + assert udp.sport == dport, "unexpected UDP sport: {}".format(udp.sport) + assert udp.dport == sport, "unexpected UDP dport: {}".format(udp.dport) + resp_payload = udp.payload.load + type_, length, magic = struct.unpack(">HHI", resp_payload[:8]) + tid = resp_payload[8:20] + data = resp_payload[20:] + assert type_ == 0x0101, "expected type 0X0101, got 0x{:04x}".format(type_) + assert length == 12, "expected length 12, got {}".format(length) + assert ( + magic == 0x2112A442 + ), "expected magic 0x2112a442, got 0x{:08x}".format(magic) + assert ( + tid == b"\x00" * 12 + ), "expected tid 0x000000000000000000000000, got {:x}".format(tid) + expected_data = b"\x00\x01\x00\x08\x00\x01" + struct.pack( + ">HBBBB", sport, 192, 0, 0, 0 + ) + assert ( + data == expected_data + ), f"unexpected data {data!r} != {expected_data!r}" + + +@test +def test_ipv6_udp_stun(): + sports = [12345, 55555, 80, 43273] + dports = [80, 800, 8000, 3478] + payload = bytes.fromhex("000100002112a442000000000000000000000000") + for sport in sports: + for dport in dports: + req = ( + Ether(dst=MAC_ADDR) + / IPv6(dst=IPV6_ADDR) + / UDP(sport=sport, dport=dport) + / Raw(payload) + ) + resp = srp1(req, timeout=1) + assert resp is not None, "expecting answer, got nothing" + check_ipv6_checksum(resp) + assert UDP in resp + udp = resp[UDP] + assert udp.sport == dport + assert udp.dport == sport + resp_payload = udp.payload.load + type_, length, magic = struct.unpack(">HHI", resp_payload[:8]) + tid = resp_payload[8:20] + data = resp_payload[20:] + assert type_ == 0x0101, "expected type 0X0101, got 0x{:04x}".format(type_) + assert length == 24, "expected length 24, got {}".format(length) + assert ( + magic == 0x2112A442 + ), "expected magic 0x2112a442, got 0x{:08x}".format(magic) + assert ( + tid == b"\x00" * 12 + ), "expected tid 0x000000000000000000000000, got {:x}".format(tid) + expected_data = ( + bytes.fromhex("000100140002") + + struct.pack(">H", sport) + + inet_pton(AF_INET6, "2001:41d0::1234:5678") + ) + assert data == expected_data, "unexpected data: {}".format(data) + + +@test +def test_ipv4_udp_stun_change_port(): + sports = [12345, 55555, 80, 43273] + dports = [80, 800, 8000, 3478, 65535] + payload = bytes.fromhex("0001000803a3b9464dd8eb75e19481474293845c0003000400000002") + for sport in sports: + for dport in dports: + req = ( + Ether(dst=MAC_ADDR) + / IP(dst=IPV4_ADDR) + / UDP(sport=sport, dport=dport) + / Raw(payload) + ) + resp = srp1(req, timeout=1) + assert resp is not None, "expecting answer, got nothing" + check_ip_checksum(resp) + assert UDP in resp, "no UDP layer found" + udp = resp[UDP] + assert ( + udp.sport == (dport + 1) % 2**16 + ), "expected answer from UDP/{}, got it from UDP/{}".format( + (dport + 1) % 2**16, udp.sport + ) + assert ( + udp.dport == sport + ), "expected answer to UDP/{}, got it to UDP/{}".format(sport, udp.dport) + resp_payload = udp.payload.load + type_, length = struct.unpack(">HH", resp_payload[:4]) + tid = resp_payload[4:20] + data = resp_payload[20:] + assert type_ == 0x0101, "expected type 0X0101, got 0x{:04x}".format(type_) + assert length == 12, "expected length 12, got {}".format(length) + assert tid == bytes.fromhex("03a3b9464dd8eb75e19481474293845c"), ( + "expected tid 0x03a3b9464dd8eb75e19481474293845c, got %r" % tid + ) + expected_data = b"\x00\x01\x00\x08\x00\x01" + struct.pack( + ">HBBBB", sport, 192, 0, 0, 0 + ) + assert ( + data == expected_data + ), f"unexpected data {data!r} != {expected_data!r}" + + +@test +def test_ipv6_udp_stun_change_port(): + sports = [12345, 55555, 80, 43273] + dports = [80, 800, 8000, 3478, 65535] + payload = bytes.fromhex("0001000803a3b9464dd8eb75e19481474293845c0003000400000002") + for sport in sports: + for dport in dports: + req = ( + Ether(dst=MAC_ADDR) + / IPv6(dst=IPV6_ADDR) + / UDP(sport=sport, dport=dport) + / Raw(payload) + ) + resp = srp1(req, timeout=1) + assert resp is not None, "expecting answer, got nothing" + check_ipv6_checksum(resp) + assert UDP in resp, "expecting UDP layer in answer, got nothing" + udp = resp[UDP] + assert ( + udp.sport == (dport + 1) % 2**16 + ), "expected answer from UDP/{}, got it from UDP/{}".format( + (dport + 1) % 2**16, udp.sport + ) + assert ( + udp.dport == sport + ), "expected answer to UDP/{}, got it to UDP/{}".format(sport, udp.dport) + resp_payload = udp.payload.load + type_, length = struct.unpack(">HH", resp_payload[:4]) + tid = resp_payload[4:20] + data = resp_payload[20:] + assert type_ == 0x0101, "expected type 0X0101, got 0x{:04x}".format(type_) + assert length == 24, "expected length 12, got {}".format(length) + assert tid == bytes.fromhex("03a3b9464dd8eb75e19481474293845c"), ( + "expected tid 0x03a3b9464dd8eb75e19481474293845c, got %r" % tid + ) + expected_data = ( + bytes.fromhex("000100140002") + + struct.pack(">H", sport) + + inet_pton(AF_INET6, "2001:41d0::1234:5678") + ) + assert ( + data == expected_data + ), f"unexpected data {data!r} != {expected_data!r}" diff --git a/test/src/tests/tcp.py b/test/src/tests/tcp.py new file mode 100644 index 0000000..1eef702 --- /dev/null +++ b/test/src/tests/tcp.py @@ -0,0 +1,303 @@ +# This file is part of masscanned. +# Copyright 2021 - The IVRE project +# +# Masscanned is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Masscanned is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +# License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Masscanned. If not, see . + +from scapy.layers.inet import IP, TCP +from scapy.layers.inet6 import IPv6 +from scapy.layers.l2 import Ether +from scapy.packet import Raw +from scapy.sendrecv import srp1 +from scapy.volatile import RandInt + +from ..conf import IPV4_ADDR, IPV6_ADDR, MAC_ADDR +from ..core import test, check_ip_checksum, check_ipv6_checksum + + +@test +def test_ipv4_tcp_empty(): + for p in [0, 80, 443]: + req = Ether(dst=MAC_ADDR) / IP(dst=IPV4_ADDR, proto=6) / Raw() # UDP + repl = srp1(req, timeout=1) + assert repl is None, "expecting no answer, got one" + + +@test +def test_ipv6_tcp_empty(): + for p in [0, 80, 443]: + req = Ether(dst=MAC_ADDR) / IPv6(dst=IPV6_ADDR, nh=6) / Raw() # UDP + repl = srp1(req, timeout=1) + assert repl is None, "expecting no answer, got one" + + +@test +def test_tcp_syn(): + ##### SYN-ACK ##### + # test a list of ports, randomly generated once + ports_to_test = [ + 1152, + 2003, + 2193, + 3709, + 4054, + 6605, + 6737, + 6875, + 7320, + 8898, + 9513, + 9738, + 10623, + 10723, + 11253, + 12125, + 12189, + 12873, + 14648, + 14659, + 16242, + 16243, + 17209, + 17492, + 17667, + 17838, + 18081, + 18682, + 18790, + 19124, + 19288, + 19558, + 19628, + 19789, + 20093, + 21014, + 21459, + 21740, + 24070, + 24312, + 24576, + 26939, + 27136, + 27165, + 27361, + 29971, + 31088, + 33011, + 33068, + 34990, + 35093, + 35958, + 36626, + 36789, + 37130, + 37238, + 37256, + 37697, + 37890, + 38958, + 42131, + 43864, + 44420, + 44655, + 44868, + 45157, + 46213, + 46497, + 46955, + 49049, + 49067, + 49452, + 49480, + 50498, + 50945, + 51181, + 52890, + 53301, + 53407, + 53417, + 53980, + 55827, + 56483, + 58552, + 58713, + 58836, + 59362, + 59560, + 60534, + 60555, + 60660, + 61615, + 62402, + 62533, + 62941, + 63240, + 63339, + 63616, + 64380, + 65438, + ] + for p in ports_to_test: + seq_init = int(RandInt()) + syn = ( + Ether(dst=MAC_ADDR) + / IP(dst=IPV4_ADDR) + / TCP(flags="S", dport=p, seq=seq_init) + ) + syn_ack = srp1(syn, timeout=1) + assert syn_ack is not None, "expecting answer, got nothing" + check_ip_checksum(syn_ack) + assert TCP in syn_ack, "expecting TCP, got %r" % syn_ack.summary() + syn_ack = syn_ack[TCP] + assert syn_ack.flags == "SA", "expecting TCP SA, got %r" % syn_ack.flags + assert syn_ack.ack == seq_init + 1, "wrong TCP ack value (%r != %r)" % ( + syn_ack.ack, + seq_init + 1, + ) + + +@test +def test_ipv4_tcp_psh_ack(): + ##### PSH-ACK ##### + sport = 26695 + port = 445 + seq_init = int(RandInt()) + # send PSH-ACK first + psh_ack = ( + Ether(dst=MAC_ADDR) + / IP(dst=IPV4_ADDR) + / TCP(flags="PA", sport=sport, dport=port, seq=seq_init) + / Raw("payload") + ) + syn_ack = srp1(psh_ack, timeout=1) + assert syn_ack is None, "no answer expected, got one" + # test the anti-injection mechanism + seq_init = int(RandInt()) + syn = ( + Ether(dst=MAC_ADDR) + / IP(dst=IPV4_ADDR) + / TCP(flags="S", sport=sport, dport=port, seq=seq_init) + ) + syn_ack = srp1(syn, timeout=1) + assert syn_ack is not None, "expecting answer, got nothing" + check_ip_checksum(syn_ack) + assert TCP in syn_ack, "expecting TCP, got %r" % syn_ack.summary() + syn_ack = syn_ack[TCP] + assert syn_ack.flags == "SA", "expecting TCP SA, got %r" % syn_ack.flags + assert syn_ack.ack == seq_init + 1, "wrong TCP ack value (%r != %r)" % ( + syn_ack.ack, + seq_init + 1, + ) + ack = Ether(dst=MAC_ADDR) / IP(dst=IPV4_ADDR) / TCP(flags="A", dport=port) + # should fail because no ack given + psh_ack = ( + Ether(dst=MAC_ADDR) + / IP(dst=IPV4_ADDR) + / TCP(flags="PA", sport=sport, dport=port, ack=0, seq=seq_init + 1) + ) + ack = srp1(psh_ack, timeout=1) + assert ack is None, "no answer expected, got one" + # should get an answer this time + psh_ack = ( + Ether(dst=MAC_ADDR) + / IP(dst=IPV4_ADDR) + / TCP( + flags="PA", sport=sport, dport=port, ack=syn_ack.seq + 1, seq=seq_init + 1 + ) + ) + ack = srp1(psh_ack, timeout=1) + assert ack is not None, "expecting answer, got nothing" + check_ip_checksum(ack) + assert TCP in ack, "expecting TCP, got %r" % ack.summary() + ack = ack[TCP] + assert ack.flags == "A", "expecting TCP A, got %r" % syn_ack.flags + + +@test +def test_ipv6_tcp_psh_ack(): + ##### PSH-ACK ##### + sport = 26695 + port = 445 + seq_init = int(RandInt()) + # send PSH-ACK first + psh_ack = ( + Ether(dst=MAC_ADDR) + / IPv6(dst=IPV6_ADDR) + / TCP(flags="PA", sport=sport, dport=port, seq=seq_init) + / Raw("payload") + ) + syn_ack = srp1(psh_ack, timeout=1) + assert syn_ack is None, "no answer expected, got one" + # test the anti-injection mechanism + syn = ( + Ether(dst=MAC_ADDR) + / IPv6(dst=IPV6_ADDR) + / TCP(flags="S", sport=sport, dport=port, seq=seq_init) + ) + syn_ack = srp1(syn, timeout=1) + assert syn_ack is not None, "expecting answer, got nothing" + check_ipv6_checksum(syn_ack) + assert TCP in syn_ack, "expecting TCP, got %r" % syn_ack.summary() + syn_ack = syn_ack[TCP] + assert syn_ack.flags == "SA", "expecting TCP SA, got %r" % syn_ack.flags + assert syn_ack.ack == seq_init + 1, "wrong TCP ack value (%r != %r)" % ( + syn_ack.ack, + seq_init + 1, + ) + ack = Ether(dst=MAC_ADDR) / IPv6(dst=IPV6_ADDR) / TCP(flags="A", dport=port) + # should fail because no ack given + psh_ack = ( + Ether(dst=MAC_ADDR) + / IPv6(dst=IPV6_ADDR) + / TCP(flags="PA", sport=sport, dport=port, ack=0, seq=seq_init + 1) + ) + ack = srp1(psh_ack, timeout=1) + assert ack is None, "no answer expected, got one" + # should get an answer this time + psh_ack = ( + Ether(dst=MAC_ADDR) + / IPv6(dst=IPV6_ADDR) + / TCP( + flags="PA", sport=sport, dport=port, ack=syn_ack.seq + 1, seq=seq_init + 1 + ) + ) + ack = srp1(psh_ack, timeout=1) + assert ack is not None, "expecting answer, got nothing" + check_ipv6_checksum(ack) + assert TCP in ack, "expecting TCP, got %r" % ack.summary() + ack = ack[TCP] + assert ack.flags == "A", "expecting TCP A, got %r" % syn_ack.flags + + +@test +def test_tcp_syn_with_flags(): + # send a SYN packet with other TCP flags, should not be answered + for flags in ["SA", "SR", "SF", "SPUCE"]: + seq_init = int(RandInt()) + syn = ( + Ether(dst=MAC_ADDR) + / IP(dst=IPV4_ADDR) + / TCP(flags=flags, dport=80, seq=seq_init) + ) + syn_ack = srp1(syn, timeout=1) + assert syn_ack is None, "expecting no answer, got one" + # some should be accepted to imitate a Linux network stack + for flags in ["SP", "SU", "SC", "SE", "SPU", "SPC", "SPE", "SPUC", "SPUE"]: + seq_init = int(RandInt()) + syn = ( + Ether(dst=MAC_ADDR) + / IP(dst=IPV4_ADDR) + / TCP(flags=flags, dport=80, seq=seq_init) + ) + syn_ack = srp1(syn, timeout=1) + assert syn_ack is not None, "expecting answer, got None" diff --git a/test/src/tests/udp.py b/test/src/tests/udp.py new file mode 100644 index 0000000..55522f5 --- /dev/null +++ b/test/src/tests/udp.py @@ -0,0 +1,40 @@ +# This file is part of masscanned. +# Copyright 2021 - The IVRE project +# +# Masscanned is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Masscanned is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +# License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Masscanned. If not, see . + +from scapy.layers.inet import IP +from scapy.layers.inet6 import IPv6 +from scapy.layers.l2 import Ether +from scapy.packet import Raw +from scapy.sendrecv import srp1 + +from ..conf import IPV4_ADDR, IPV6_ADDR, MAC_ADDR +from ..core import test + + +@test +def test_ipv4_udp_empty(): + for p in [0, 53, 1000]: + req = Ether(dst=MAC_ADDR) / IP(dst=IPV4_ADDR, proto=17) / Raw() # UDP + repl = srp1(req, timeout=1) + assert repl is None, "expecting no answer, got one" + + +@test +def test_ipv6_udp_empty(): + for p in [0, 53, 1000]: + req = Ether(dst=MAC_ADDR) / IPv6(dst=IPV6_ADDR, nh=17) / Raw() # UDP + repl = srp1(req, timeout=1) + assert repl is None, "expecting no answer, got one" diff --git a/test/test_masscanned.py b/test/test_masscanned.py index 7384005..e3b37a6 100755 --- a/test/test_masscanned.py +++ b/test/test_masscanned.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # This file is part of masscanned. -# Copyright 2021 - The IVRE project +# Copyright 2021 - 2025 - The IVRE project # # Masscanned is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by @@ -16,63 +16,180 @@ # You should have received a copy of the GNU General Public License # along with Masscanned. If not, see . -from scapy.all import * -from time import sleep -from tempfile import _get_candidate_names as gen_tmp_filename -from tempfile import gettempdir -import subprocess -import logging -import sys +import atexit +import functools import os +from signal import SIGINT +import subprocess +import sys +from time import sleep +from tempfile import NamedTemporaryFile + +try: + from ivre.config import guess_prefix +except ImportError: + HAS_IVRE = False +else: + HAS_IVRE = True +from scapy.config import conf +from scapy.interfaces import resolve_iface from src.all import test_all -from src.conf import * +from src.conf import IPV4_ADDR, IPV6_ADDR, MAC_ADDR, OUTDIR -# if args in CLI, they are passed to masscanned -if len(sys.argv) > 1: - args = " ".join(sys.argv[1:]) + +def cleanup_net(iface): + subprocess.check_call(["ip", "link", "delete", iface]) + subprocess.check_call( + [ + "iptables", + "-D", + "INPUT", + "-i", + iface, + "-m", + "state", + "--state", + "ESTABLISHED", + "-j", + "ACCEPT", + ] + ) + subprocess.check_call(["iptables", "-D", "INPUT", "-i", iface, "-j", "DROP"]) + try: + os.unlink(ipfile.name) + except NameError: + pass + + +def setup_net(iface): + # create the interfaces pair + atexit.register(functools.partial(cleanup_net, f"{iface}a")) + subprocess.check_call( + ["ip", "link", "add", f"{iface}a", "type", "veth", "peer", f"{iface}b"] + ) + for sub in "a", "b": + subprocess.check_call(["ip", "link", "set", f"{iface}{sub}", "up"]) + subprocess.check_call(["ip", "addr", "add", "dev", f"{iface}a", "192.0.0.0/31"]) + subprocess.check_call( + ["ip", "addr", "add", "dev", f"{iface}a", "2001:41d0::1234:5678/96"] + ) + subprocess.check_call(["ip", "route", "add", "1.2.3.4/32", "via", IPV4_ADDR]) + # prevent problems between raw scanners (Scapy, Nmap, Masscan) and + # the host IP stack + subprocess.check_call( + [ + "iptables", + "-A", + "INPUT", + "-i", + f"{iface}a", + "-m", + "state", + "--state", + "ESTABLISHED", + "-j", + "ACCEPT", + ] + ) + subprocess.check_call(["iptables", "-A", "INPUT", "-i", f"{iface}a", "-j", "DROP"]) + conf.route.resync() + conf.route6.resync() + + +IFACE = "masscanned" +setup_net(IFACE) +TCPDUMP = bool(os.environ.get("USE_TCPDUMP")) +if HAS_IVRE: + ZEEK_PASSIVERECON = bool(os.environ.get("USE_ZEEK")) else: - args = "" - -fmt = logging.Formatter("%(levelname)s\t%(message)s") -ch = logging.StreamHandler() -ch.setFormatter(fmt) -ch.setLevel(logging.INFO) -LOG = logging.getLogger(__name__) -LOG.setLevel(logging.INFO) -LOG.addHandler(ch) - -conf.iface = 'tap0' + ZEEK_PASSIVERECON = False +P0F = bool(os.environ.get("USE_P0F")) conf.verb = 0 # prepare configuration file for masscanned -ipfile = os.path.join(gettempdir(), next(gen_tmp_filename())) -with open(ipfile, "w") as f: - f.write("{}\n".format(IPV4_ADDR)) - f.write("{}\n".format(IPV6_ADDR)) +with NamedTemporaryFile(delete=False, mode="w") as ipfile: + ipfile.write(f"{IPV4_ADDR}\n") + ipfile.write(f"{IPV6_ADDR}\n") # create test interface -tap = TunTapInterface(resolve_iface(conf.iface)) - -# set interface -subprocess.run("ip a a dev {} 192.0.0.2".format(conf.iface), shell=True) -subprocess.run("ip link set {} up".format(conf.iface), shell=True) +conf.iface = resolve_iface(f"{IFACE}a") # start capture -tcpdump = subprocess.Popen("tcpdump -enli {} -w {}".format(conf.iface, os.path.join(OUTDIR, "test_capture.pcap")), shell=True, - stdin=None, stdout=None, stderr=None, close_fds=True) +if TCPDUMP: + tcpdump = subprocess.Popen( + [ + "tcpdump", + "-enli", + f"{IFACE}a", + "-w", + os.path.join(OUTDIR, "test_capture.pcap"), + ] + ) +if ZEEK_PASSIVERECON: + zeek = subprocess.Popen( + [ + "zeek", + "-C", + "-b", + "-i", + f"{IFACE}a", + os.path.join( + guess_prefix("zeek"), + "ivre", + "passiverecon", + "bare.zeek", + ), + "-e", + "redef tcp_content_deliver_all_resp = T; " + "redef tcp_content_deliver_all_orig = T; " + f"redef PassiveRecon::HONEYPOTS += {{ {IPV4_ADDR}, [{IPV6_ADDR}] }}", + ], + stdout=open(os.path.join(OUTDIR, "zeek_passiverecon.stdout"), "w"), + stderr=open(os.path.join(OUTDIR, "zeek_passiverecon.stderr"), "w"), + ) +if P0F: + p0f = subprocess.Popen( + ["p0f", "-i", f"{IFACE}a", "-o", os.path.join(OUTDIR, "p0f_log.txt")], + stdout=open(os.path.join(OUTDIR, "p0f.stdout"), "w"), + stderr=open(os.path.join(OUTDIR, "p0f.stderr"), "w"), + ) # run masscanned -masscanned = subprocess.Popen("RUST_BACKTRACE=1 ./target/debug/masscanned -vvvvv -i {} -f {} -a {} {}".format(conf.iface, ipfile, MAC_ADDR, args), shell=True, - stdin=None, stdout=open("test/res/masscanned.stdout", "w"), stderr=open("test/res/masscanned.stderr", "w"), close_fds=True) +masscanned = subprocess.Popen( + [ + "./target/debug/masscanned", + "-vvvvv", + "-i", + f"{IFACE}b", + "--self-ip-file", + ipfile.name, + "-m", + MAC_ADDR, + ] + # if args in CLI, they are passed to masscanned + + sys.argv[1:], + env=dict(os.environ, RUST_BACKTRACE="1"), + stdout=open(os.path.join(OUTDIR, "masscanned.stdout"), "w"), + stderr=open(os.path.join(OUTDIR, "masscanned.stderr"), "w"), +) sleep(1) try: - test_all(tap) + result = test_all(masscanned) except AssertionError: - pass + result = -1 # terminate masscanned -masscanned.kill() +masscanned.send_signal(SIGINT) +masscanned.wait() # terminate capture -sleep(2) -tcpdump.kill() +if TCPDUMP: + tcpdump.send_signal(SIGINT) + tcpdump.wait() +if ZEEK_PASSIVERECON: + zeek.send_signal(SIGINT) + zeek.wait() +if P0F: + p0f.send_signal(SIGINT) + p0f.wait() +sys.exit(result)