Is there an existing issue for this?
Empire Version
v6.6.0
Python Version
docker
Operating System
Ubuntu 24.04.4 LTS
Database
MySQL
Current Behavior
Summary
Running any background-job module that streams output via job_message_buffer(...) on a Linux/macOS CPython agent (multi/launcher, Language=python) aborts with:
[!] Error executing script: name 'job_message_buffer' is not defined
Affected modules include python/collection/linux/keylogger,
python/collection/linux/xkeylogger,
python/collection/linux/sniffer,
python/collection/linux/port_scanner, and any custom module that calls
job_message_buffer(msg) from inside the script body it hands to the
agent.
Root cause
PR #821 ("Fix agent background python jobs", 354741f0) rewrote MainAgent.python_job_func in empire/server/data/agent/agent.py to exec the module-supplied script inside a purpose-built globals dict containing only print:
output_capture = io.StringIO()
script_globals = {'print': lambda *a, **k: print(*a, **k, file=output_capture)}
try:
exec(data, script_globals)
Several built-in modules stream output by calling job_message_buffer as a bare name from inside that script. For example, empire/server/modules/python/collection/linux/xkeylogger.yaml references the name at lines 554, 625, 628, 654, 685, 689, and 755. Under the new exec scope the name is unresolved, the script raises NameError, and the operator sees [!] Error executing script: name 'job_message_buffer' is not defined.
Additional complication that informs the fix shape: MainAgent keeps a string instance attribute self.job_message_buffer = "" (the accumulating buffer) and defines a method def job_message_buffer(self, message) with the same name. The instance attribute shadows the method, so even self.job_message_buffer(message) raises 'str' object is not callable. That tangle predates the regressions discussed here and is best fixed separately. This patch sidesteps it.
Expected Behavior
No error.
Steps To Reproduce
- Start Empire
v6.6.0.
- Deploy a Linux CPython agent:
multi/launcher with Language=python,
against a multi/http listener.
- (Apply the separate task-50/51 dispatch fix first so the module can
be observed end-to-end; without it, the operator hits the
'MainAgent' object has no attribute 'job_list' error before
reaching this one.)
- Run
python/collection/linux/xkeylogger against the agent.
Expected: xkeylogger registers as a running job and streams keystrokes
via job_message_buffer.
Actual on stock v6.6.0: the task output shows
[!] Error executing script: name 'job_message_buffer' is not defined.
Anything else?
Inject a small closure named job_message_buffer into script_globals that appends to the string buffer instance attribute directly, bypassing the shadowed method:
output_capture = io.StringIO()
- script_globals = {'print': lambda *a, **k: print(*a, **k, file=output_capture)}
+ def _append_job_message(msg):
+ try:
+ self.job_message_buffer += str(msg)
+ except Exception as e:
+ print("[!] Error adding job output to buffer: %s" % e, file=output_capture)
+ script_globals = {
+ 'print': lambda *a, **k: print(*a, **k, file=output_capture),
+ 'job_message_buffer': _append_job_message,
+ }
The existing checkin path (get_job_message_buffer) already flushes self.job_message_buffer back to the server on every poll and resets it; that side is unchanged.
Only python_job_func (task 112, background python jobs) is touched. dynamic_code_execute_wait_nosave (task 110) and dynamic_code_execution_wait_save (task 111) — immediate-execute paths — are intentionally left as-is; they do not run modules that stream output via job_message_buffer.
See attached patch: 0002-expose-job_message_buffer-in-script-globals.patch
Debug and PR summary with Claude Opus 4.7; Tested fix locally by extending Dockerfile.
Dockerfile
ARG EMPIRE_IMAGE_VERSION
FROM bcsecurity/empire:${EMPIRE_IMAGE_VERSION:-v6.6.0}
# from 826
COPY 0001-fix-agent-task-list-stop-task-dispatch.patch /tmp/p1.patch
COPY 0002-expose-job_message_buffer-in-script-globals.patch /tmp/p2.patch
RUN set -eu; \
for p in /tmp/p1.patch /tmp/p2.patch; do \
patch -p1 --forward --silent -d /empire < "$p" \
|| (echo "[empire-patch] $p did not apply cleanly; check upstream state" && exit 1); \
done; \
rm -f /tmp/p1.patch /tmp/p2.patch
Is there an existing issue for this?
Empire Version
v6.6.0
Python Version
docker
Operating System
Ubuntu 24.04.4 LTS
Database
MySQL
Current Behavior
Summary
Running any background-job module that streams output via
job_message_buffer(...)on a Linux/macOS CPython agent (multi/launcher, Language=python) aborts with:Affected modules include
python/collection/linux/keylogger,python/collection/linux/xkeylogger,python/collection/linux/sniffer,python/collection/linux/port_scanner, and any custom module that callsjob_message_buffer(msg)from inside the script body it hands to theagent.
Root cause
PR #821 ("Fix agent background python jobs",
354741f0) rewroteMainAgent.python_job_funcinempire/server/data/agent/agent.pyto exec the module-supplied script inside a purpose-built globals dict containing onlyprint:Several built-in modules stream output by calling
job_message_bufferas a bare name from inside that script. For example,empire/server/modules/python/collection/linux/xkeylogger.yamlreferences the name at lines 554, 625, 628, 654, 685, 689, and 755. Under the new exec scope the name is unresolved, the script raisesNameError, and the operator sees[!] Error executing script: name 'job_message_buffer' is not defined.Additional complication that informs the fix shape:
MainAgentkeeps a string instance attributeself.job_message_buffer = ""(the accumulating buffer) and defines a methoddef job_message_buffer(self, message)with the same name. The instance attribute shadows the method, so evenself.job_message_buffer(message)raises'str' object is not callable. That tangle predates the regressions discussed here and is best fixed separately. This patch sidesteps it.Expected Behavior
No error.
Steps To Reproduce
v6.6.0.multi/launcherwith Language=python,against a
multi/httplistener.be observed end-to-end; without it, the operator hits the
'MainAgent' object has no attribute 'job_list'error beforereaching this one.)
python/collection/linux/xkeyloggeragainst the agent.Expected: xkeylogger registers as a running job and streams keystrokes
via
job_message_buffer.Actual on stock
v6.6.0: the task output shows[!] Error executing script: name 'job_message_buffer' is not defined.Anything else?
Inject a small closure named
job_message_bufferintoscript_globalsthat appends to the string buffer instance attribute directly, bypassing the shadowed method:The existing checkin path (
get_job_message_buffer) already flushesself.job_message_bufferback to the server on every poll and resets it; that side is unchanged.Only
python_job_func(task 112, background python jobs) is touched.dynamic_code_execute_wait_nosave(task 110) anddynamic_code_execution_wait_save(task 111) — immediate-execute paths — are intentionally left as-is; they do not run modules that stream output viajob_message_buffer.See attached patch: 0002-expose-job_message_buffer-in-script-globals.patch
Debug and PR summary with Claude Opus 4.7; Tested fix locally by extending Dockerfile.
Dockerfile