Callbacks

A callback is used to allow the user to get access to the result and do additional processing on the information. Cobalt Strike and Aggressor Script uses the concept of callbacks because of the asynchronous behavior of sending a task to beacon and the response being received sometime in the future based on the current sleep time. They are also used when dealing with custom dialogs in order to perform additional actions based on information from the dialog input and action button.

Once your asynchronous callback is executed you can then perform the necessary operations to process the result for your use case. Here are some examples of what you can do with the result:

  • Communicate with a running post ex task

  • Process the result and trigger an additional task

  • Format the result before displaying in the Beacon Console

A callback function will have arguments and in most cases will have the same arguments, however there are some exceptions. You should always refer to the aggressor script function documentation to understand what arguments are being passed to your callback.

Callback Request and Response Processing

The following describes at a high level what goes on when a callback is used in an aggressor script command.

  • The client executes an aggressor script command with a callback
    • A request is created and saved in a queue to be retrieved later
    • The request is sent to the teamserver
  • The teamserver receives the request
    • The request is saved in a queue to be retrieved later
    • The request is sent to a beacon
  • The Beacon receives the request and processes the task
    • A response is generated and sent to the teamserver
  • The teamserver receives the response
    • The request is retrieved from the teamserver queue using an id from the response
    • A reply is generated and sent to the originating client
  • The originating client receives the response
    • The request is retrieved from the client queue using an id from the response
    • The client will execute the callback

Both the client and teamserver save requests that have associated callbacks in a queue. A request is eventually removed in order to maintain the number of request in the queue. A request is removed when these two conditions occur.

The first condition is when the originating client disconnects from the teamserver. When this happens the queue managed by the client is removed as the queue is per teamserver connection. The queue on the teamserver will see the originating client has disconnected and flag any requests for that client to be removed. This means the originating client needs to stay connected to the teamserver until the command with a callback has completed. Otherwise, any responses from Beacon after a disconnection from the originating client will be lost.

The second condition is when there is no responses for a request after a period of time. There are two timeout settings that determine if a request should be removed. The first setting is the limits.callback_max_timeout which defaults to 1 day, which is used to wait for the initial response. The second setting is the limits.callback_keep_timeout which defaults to 1 hour, which is used to wait for subsequent responses. These settings can be modified by updating the TeamServer.prop file. In most use cases the defaults should be fine, however if you create a command that is a long-running job/task then these settings may need to be adjusted. The adjusted settings need to be based on how often data will be received, which needs to account for beacon's sleep time and how often the job/task sends data.

If you see error(s) like the following in the teamserver console window then this can indicate the settings need to be adjusted or the originating client has disconnected from the teamserver.

`"Callback #/# has no pending request"`

The TeamServer.prop file is not included in the Cobalt Strike distribution. The current default file can be found on Github (https://github.com/Cobalt-Strike/teamserver-prop).

Callback Implementation

Aggressor script callbacks can be implemented using a few different techniques and in many cases the technique used is based on personal preference. There are some use cases where you will want to choose a particular technique in order to accomplish the task. The following type of techniques can be used followed by simple snippets of code:

  • Anonymous Closure
  • Named Closure
  • Lambda Closure

Examples of aggressor script functions that support the use of a callback function can be found on Github (https://github.com/Cobalt-Strike/callback_examples).

Job Callbacks

It is possible to use callbacks with various different post exploitation tasks. For example, the portscanner. Callbacks use the %infomap structure to provide relevant information to the user. This structure can be seen below.

  • type: A string value to represent the reason for the call. Possible values are output, error, job_registered, and job_completed.

  • type_id: The integer value of the output type: CALLBACK_OUTPUT, CALLBACK_OUTPUT_OEM, etc.

  • jid: The job id.

  • chunk_num: The id number of the output chunk.

  • is_final: The flag to identify the final chunk.

Anonymous Closure Example

An anonymous closure is useful when you have a small amount of code that can be kept inline with the caller. In this example the closure is executed in the future when data is returned from a BOF, which simply logs the output to the beacon console.

alias cs_example {
# User setup code removed for brevity
beacon_inline_execute($bid, $data, "go", $args, { blog($1, $2); }); }

Named Closure Example

A named closure is useful when you have a lot of code and may want to reuse the code with other aggressor functions. In this example the closure named `bof_cb` is executed in the future when data is returned from a BOF.

# $1 - bid, $2 - result, $3 - info map
sub bof_cb {
# User defined code removed for brevity
}
alias cs_example {
local('$bid $data $args');
# User setup code removed for brevity
beacon_inline_execute($bid, $data, "go", $args, &bof_cb));
}

Lambda Closure Example

A lambda closure is useful when you want to pass variable(s) that would not be in scope using the previous methods. This example shows how you can get access to the $test_num variable which is in the scope of the cs_example alias.

# $1 - bid, $2 - result, $3 - info map, $4 - test_num
sub bof_cb {
# User defined code removed for brevity
}
alias cs_example {
local('$bid $file $test_num');
# User setup code removed for brevity
binline_execute($bid, $file, $test_num, lambda({ bof_cb ($1, $2, $3, $test_num); }, \$test_num);
}