Lamperl pt 2(Adding new functions)
Continuing development of Lamperl, adding basic functionality.
Hello again, in this post I’m going to show the process of implementing two functions that I would consider to be essential but were not included in the last post: Terminate, and Sleep. The Perl side of this is very simple, as a result the focus of this post will be on the Adaptix agent config in the ax_config.axs and pl_agent.go files.
In the previous post we built a basic agent with three commands: pwd, cd, and run. While functional, it was missing fundamental C2 agent capabilities. Every practical agent needs the ability to cleanly exit (terminate) and adjust its beacon interval (sleep). Being able to terminate agents avoids leaving orphaned processes, and dynamic sleep adjustment lets operators balance stealth against responsiveness based on the engagement phase.
The main function already loops until should_terminate is true, so the only thing the terminate function will do is set that variable.
1
while (!$should_terminate)
Similarly, we already have a function that uses jitter to compute sleep time included in the loop, so our sleep function will just set the sleep_time and jitter_percent variables.
1
2
3
4
sub calculate_sleep {
return $sleep_time unless $jitter_percent > 0;
return $sleep_time + int(rand($sleep_time * $jitter_percent / 100));
}
The main purpose of this post is to provide instructions for implementing commands, since I feel the last didn’t cover it very well as the code was written before the article was.
For every function added to an Adaptix agent there are three things that must be updated.
ax_config.axsCreateTaskfunction inpl_agent.goProcessTasksResultfunction inpl_agent.go
Implementing Terminate
Lets get into it, the Perl implementation is straightforward. We create a command that sets the should_terminate flag:
1
2
3
4
5
6
7
8
sub cmd_terminate {
my ($task) = @_;
$should_terminate = 1;
return {
command => 'terminate',
status => 'terminating',
};
}
This function:
- Accepts the task object (unused here but required for consistency)
- Sets
$should_terminate = 1to signal the main loop to exit - Returns a status object confirming termination has been initiated
With the function created, we now need to update the dispatch table to include it.
1
2
3
4
5
6
my %COMMANDS = (
pwd => \&cmd_pwd,
cd => \&cmd_cd,
run => \&cmd_run,
terminate=> \&cmd_terminate,
);
And we’re done with the Perl portion of the code for this feature.
Adaptix Configuration
Now we need to register this command with Adaptix so it appears in the UI and can be properly dispatched. This involves three files:
ax_config.axs- UI command definitionpl_agent.go- CreateTask (operator -> agent)pl_agent.go- ProcessTasksResult (agent -> operator)
ax_config.axs
First lets add the new command to the ax_config.axs file:
1
let cmd_terminate = ax.create_command("terminate", "Terminate beacon", "terminate", "Task: terminate beacon");
This creates a command definition with:
- Command name:
terminate - Description: “Terminate beacon”
- Usage example:
terminate - Message: “Task: terminate beacon”
Then we add it to the agent’s command group, located at the bottom of the file.
1
2
3
4
5
if(listenerType == "LamperlHTTP") {
let commands_external = ax.create_commands_group("Lamperl", [cmd_pwd, cmd_cd, cmd_terminate, cmd_run]);
return { commands_linux: commands_external }
}
pl_agent.go - CreateTask - Terminate
Next we move to pl_agent.go. As terminate doesn’t take any parameters, updating CreateTask is straightforward:
1
2
case "terminate":
// No additional parameters needed
Add this case to the switch statement in the CreateTask function. Since terminate requires no parameters, we simply match the command and let the default task creation handle the rest.
pl_agent.go - ProcessTasksResult - Terminate
We also don’t really need any output, so in ProcessTasksResult we’ll just show a simple message:
1
2
case "terminate":
consoleOutput = "Terminating beacon"
This case extracts the termination confirmation from the agent’s response and displays it in the operator console.
Building and Testing
Now we just need to build the plugin and deploy it:
1
make
Restart the Adaptix server, generate a new agent, and test the terminate command.
Adding Context Menu Integration
Right now every time we want to terminate the agent we have to type out the command and send it, which can get tedious when handling a bunch of agents. Let’s add a right-click context menu option for quick termination.
AxScript provides menu functions that let us add custom actions to the agent context menu. The documentation is available here:
Add the following to the top of the ax_config.axs file:
1
2
3
let terminate_action = menu.create_action("Terminate", function(value) { value.forEach(v => ax.execute_command(v, "terminate")) });
menu.add_session_agent(terminate_action, ["Lamperl"])
Breaking this down:
menu.create_action("Terminate", ...)creates a menu item labeled “Terminate”- The function receives selected agents as
valueand iterates through them ax.execute_command(v, "terminate")executes the terminate command on each selected agentmenu.add_session_agent(...)adds this action to the agent context menu["Lamperl"]restricts this action to only Lamperl agents
This links the Terminate function to the agent context menu, allowing you to right-click and terminate agents with one click:
Rebuild the plugin and test:
1
make
Implementing Sleep
Perl Quirks and Design Choices
An interesting thing about Perl’s sleep: it’s in seconds. So if it’s set to 5 seconds with a 10% jitter, like is set by default, it will literally always be 5 seconds because Perl truncates towards 0. For low sleep values, this effectively disables jitter.
Also, the current implementation will always jitter higher than the base sleep time and never lower. That’s how I want it (ensures minimum sleep time), but if you prefer bidirectional jitter you can swap the calculate_sleep function with the following:
1
2
3
4
sub calculate_sleep {
return $sleep_time unless $jitter_percent > 0;
return $sleep_time + int(($sleep_time * $jitter_percent / 100) * (rand(2) - 1));
}
This alternative uses rand(2) - 1 to generate a number between -1 and +1, creating bidirectional jitter.
Perl Agent Side
Here’s how the sleep function is implemented:
1
2
3
4
5
6
7
8
9
10
11
sub cmd_sleep {
my ($task) = @_;
$sleep_time = $task->{duration} || 5;
$jitter_percent = $task->{jitter} || 0;
return {
command => 'sleep',
duration => $sleep_time,
jitter => $jitter_percent,
status => 'completed',
};
}
This function:
- Extracts
durationfrom the task object (defaults to 5 seconds if not provided) - Extracts
jitterpercentage (defaults to 0 if not provided) - Updates the global
$sleep_timeand$jitter_percentvariables - Returns a status object confirming the new sleep configuration
The beauty of this approach is that these are global variables checked at the end of the main beacon loop, so the change takes effect immediately.
Once again, we update the dispatch table:
1
2
3
4
5
6
7
my %COMMANDS = (
pwd => \&cmd_pwd,
cd => \&cmd_cd,
run => \&cmd_run,
sleep => \&cmd_sleep,
terminate=> \&cmd_terminate,
);
Adaptix Sleep Configuration
Now we need to wire this up to Adaptix. Unlike terminate, sleep requires parameters (duration and optional jitter), so the implementation is slightly more complex.
ax_config.axs
First we need to add the sleep command definition to ax_config.axs. We need to define it with parameter inputs for duration and jitter. Both of those are ints, jitter isn’t required to be set:
1
2
3
let cmd_sleep = ax.create_command("sleep", "Sleep for a duration with optional jitter", "sleep 10 20", "Task: sleep");
cmd_sleep.addArgInt("duration", true, "Duration to sleep in seconds");
cmd_sleep.addArgInt("jitter", false, "Jitter percentage (0-100)");
Once again we add it to the agent’s command group.
1
2
3
4
5
if(listenerType == "LamperlHTTP") {
let commands_external = ax.create_commands_group("Lamperl", [cmd_pwd, cmd_cd, cmd_sleep, cmd_terminate, cmd_run]);
return { commands_linux: commands_external }
}
pl_agent.go - CreateTask - Sleep
In CreateTask, we need to extract and validate the parameters:
1
2
3
4
5
6
7
8
9
10
case "sleep":
duration, ok := args["duration"].(float64)
if !ok {
err = errors.New("parameter 'duration' must be set")
return taskData, messageData, err
}
commandData["duration"] = int(duration)
if jitter, ok := args["jitter"].(float64); ok {
commandData["jitter"] = int(jitter)
}
This:
- Extracts
durationfrom args and validates it exists (required parameter) - Optionally extracts
jitterif provided - Packages both into the commandData map for serialization
pl_agent.go - ProcessTasksResult - Sleep
In ProcessTasksResult, we parse the agent’s response and display it:
1
2
3
4
5
case "sleep":
duration := getInt(outputData, "duration")
jitter := getInt(outputData, "jitter")
consoleOutput = fmt.Sprintf("Sleep scheduled for %d seconds with %d%% jitter", duration, jitter)
This extracts the duration and jitter from the agent’s response and formats a confirmation message.
Rebuild and test:
1
make
Which works. The command executes and the beacon sleeps for the spcified time. However, if you test it like this you’ll quickly notice that it does not update the Sleep column in the Adaptix client UI. The agent’s internal sleep has changed, but Adaptix’s metadata is stale.
Updating Agent Metadata
In order to have the change reflected in the client we must also update the agentData object and persist it back to the teamserver:
1
2
3
4
5
6
7
8
9
10
case "sleep":
duration := getInt(outputData, "duration")
jitter := getInt(outputData, "jitter")
// Update agent data with new sleep configuration
agentData.Sleep = uint(duration)
agentData.Jitter = uint(jitter)
_ = ts.TsAgentUpdateData(agentData)
consoleOutput = fmt.Sprintf("Sleep scheduled for %d seconds with %d%% jitter", duration, jitter)
The key additions:
agentData.Sleep = uint(duration)- Update the agent’s sleep metadataagentData.Jitter = uint(jitter)- Update the agent’s jitter valuets.TsAgentUpdateData(agentData)- Persist changes toagentData
This ensures the UI reflects the agent’s current configuration.
Final build and deploy:
1
make
Generate a new payload and test. Once the sleep command is executed, the sleep displayed in the client will update in real-time.
Key Takeaways
For every new Adaptix agent command, remember the pattern of threes:
- ax_config.axs - UI definition, parameter forms, add to command group
- CreateTask - Operator input -> agent task serialization
- ProcessTasksResult - Agent output -> console display + metadata sync
This pattern scales to any command complexity. Even advanced features like file upload/download, socks proxies, or process injection follow this same structure, they just have more complex parameter handling and result processing.
In the next post, we’ll actually tackle more advanced functionality: upload/download support and asynchronous job handling.





