Improper Control of Generation of Code ('Code Injection')
Description
Server-Side Template Injection in pwntools shellcraft generator allows remote code execution before version 4.3.1.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Server-Side Template Injection in pwntools shellcraft generator allows remote code execution before version 4.3.1.
Vulnerability
Details
The shellcraft generator in pwntools prior to version 4.3.1 is vulnerable to Server-Side Template Injection (SSTI) [1][3]. This flaw occurs because user-supplied input is passed unsanitized to the template engine during shellcode generation, allowing an attacker to inject malicious template expressions [2].
Exploitation
An attacker can exploit this vulnerability by providing crafted input to any application that uses the pwntools shellcraft generator with untrusted data. The SSTI can be triggered remotely over the network if the application processes attacker-controlled input [3]. No special privileges are required, as the vulnerability is accessible through standard library interfaces.
Impact
Successful exploitation leads to arbitrary code execution in the context of the Python process running pwntools [3]. This could allow an attacker to execute system commands, access sensitive data, or pivot to other systems.
Mitigation
The issue is fixed in pwntools version 4.3.1 [2]. Users are strongly advised to update to this version or later. No known workarounds exist for earlier versions.
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
pwntoolsPyPI | < 4.3.1 | 4.3.1 |
Affected products
2- pwntools/pwntoolsdescription
Patches
1138188eb1c02Fix pwntools shellcraft SSTI vulnerability
29 files changed · +95 −71
pwnlib/constants/__init__.py+7 −1 modified@@ -145,7 +145,13 @@ def eval(self, string): if key not in self._env_store: self._env_store[key] = {key: getattr(self, key) for key in dir(self) if not key.endswith('__')} - return Constant('(%s)' % string, safeeval.values(string, self._env_store[key])) + val = safeeval.values(string, self._env_store[key]) + + # if the expression is not assembly-safe, it is not so vital to preserve it + if set(string) & (set(bytearray(range(32)).decode()) | set('"#$\',.;@[\\]`{}')): + string = val + + return Constant('(%s)' % string, val) # To prevent garbage collection
pwnlib/data/syscalls/generate.py+11 −9 modified@@ -1,6 +1,7 @@ #!/usr/bin/env python2 from __future__ import division import argparse +import keyword import os from pwnlib import constants @@ -62,7 +63,7 @@ for name, arg in zip(argument_names, argument_values): if arg is not None: - syscall_repr.append('%s=%r' % (name, arg)) + syscall_repr.append('%s=%s' % (name, pwnlib.shellcraft.pretty(arg, False))) # If the argument itself (input) is a register... if arg in allregs: @@ -75,8 +76,8 @@ # The argument is not a register. It is a string value, and we # are expecting a string value - elif name in can_pushstr and isinstance(arg, (bytes, six.text_type)): - if not isinstance(arg, bytes): + elif name in can_pushstr and isinstance(arg, (six.binary_type, six.text_type)): + if isinstance(arg, six.text_type): arg = arg.encode('utf-8') string_arguments[name] = arg @@ -144,12 +145,11 @@ def can_be_array(arg): def fix_bad_arg_names(func, arg): - if arg.name == 'str': - return 'str_' if arg.name == 'len': return 'length' - if arg.name == 'repr': - return 'repr_' + + if arg.name in ('str', 'repr') or keyword.iskeyword(arg.name): + return arg.name + '_' if func.name == 'open' and arg.name == 'vararg': return 'mode' @@ -277,8 +277,10 @@ def generate_one(target): CALL.format(**template_variables) ] - with open(os.path.join(target, name + '.asm'), 'wt+') as f: - f.write('\n'.join(map(str.strip, lines))) + if keyword.iskeyword(name): + name += '_' + with open(os.path.join(target, name + '.asm'), 'wt') as f: + f.write('\n'.join(map(str.strip, lines)) + '\n') if __name__ == '__main__': p = argparse.ArgumentParser()
pwnlib/shellcraft/__init__.py+5 −2 modified@@ -142,8 +142,11 @@ def eval(self, item): return constants.eval(item) def pretty(self, n, comment=True): - if isinstance(n, str): - return repr(n) + if isinstance(n, (str, bytes, list, tuple, dict)): + r = repr(n) + if not comment: # then it can be inside a comment! + r = r.replace('*/', r'\x2a/') + return r if not isinstance(n, six.integer_types): return n if isinstance(n, constants.Constant):
pwnlib/shellcraft/templates/aarch64/freebsd/syscall.asm+1 −1 modified@@ -36,7 +36,7 @@ Example: if syscall is None: args = ['?'] else: - args = [repr(syscall)] + args = [pretty(syscall, False)] for arg in [arg0, arg1, arg2, arg3, arg4, arg5]: if arg is None:
pwnlib/shellcraft/templates/aarch64/linux/syscall.asm+4 −4 modified@@ -1,5 +1,5 @@ <% - from pwnlib.shellcraft import aarch64 + from pwnlib.shellcraft import aarch64, pretty from pwnlib.constants import eval from pwnlib.abi import linux_aarch64_syscall as abi from six import text_type @@ -14,7 +14,7 @@ Any of the arguments can be expressions to be evaluated by :func:`pwnlib.constan Example: >>> print(shellcraft.aarch64.linux.syscall(11, 1, 'sp', 2, 0).rstrip()) - /* call syscall(11, 1, 'sp', 2, 0) */ + /* call syscall(0xb, 1, 'sp', 2, 0) */ mov x0, #1 mov x1, sp mov x2, #2 @@ -59,13 +59,13 @@ Example: if syscall is None: args = ['?'] else: - args = [repr(syscall)] + args = [pretty(syscall, False)] for arg in [arg0, arg1, arg2, arg3, arg4, arg5]: if arg is None: args.append('?') else: - args.append(repr(arg)) + args.append(pretty(arg, False)) while args and args[-1] == '?': args.pop() syscall_repr = syscall_repr % ', '.join(args)
pwnlib/shellcraft/templates/aarch64/pushstr_array.asm+1 −1 modified@@ -39,7 +39,7 @@ string = b''.join(array) if len(array) * 8 > 4095: raise Exception("Array size is too large (%i), max=4095" % len(array)) %>\ - /* push argument array ${repr(array)} */ + /* push argument array ${shellcraft.pretty(array, False)} */ ${shellcraft.pushstr(string, register1=register1, register2=register2)} /* push null terminator */
pwnlib/shellcraft/templates/amd64/freebsd/syscall.asm+1 −1 modified@@ -79,7 +79,7 @@ Example: if syscall is None: args = ['?'] else: - args = [repr(syscall)] + args = [pretty(syscall, False)] for arg in [arg0, arg1, arg2, arg3, arg4, arg5]: if arg is None:
pwnlib/shellcraft/templates/amd64/linux/syscall.asm+18 −4 modified@@ -1,5 +1,5 @@ <% - from pwnlib.shellcraft import amd64 + from pwnlib.shellcraft import amd64, pretty from pwnlib.constants import Constant from pwnlib.abi import linux_amd64_syscall as abi from six import text_type @@ -54,7 +54,7 @@ Example: ... 'PROT_READ | PROT_WRITE | PROT_EXEC', ... 'MAP_PRIVATE | MAP_ANONYMOUS', ... -1, 0).rstrip()) - /* call mmap(0, 4096, 'PROT_READ | PROT_WRITE | PROT_EXEC', 'MAP_PRIVATE | MAP_ANONYMOUS', -1, 0) */ + /* call mmap(0, 0x1000, 'PROT_READ | PROT_WRITE | PROT_EXEC', 'MAP_PRIVATE | MAP_ANONYMOUS', -1, 0) */ push (MAP_PRIVATE | MAP_ANONYMOUS) /* 0x22 */ pop r10 push -1 @@ -84,6 +84,20 @@ Example: push SYS_open /* 2 */ pop rax syscall + >>> print(shellcraft.amd64.write(0, '*/', 2).rstrip()) + /* write(fd=0, buf='\x2a/', n=2) */ + /* push b'\x2a/\x00' */ + push 0x1010101 ^ 0x2f2a + xor dword ptr [rsp], 0x1010101 + mov rsi, rsp + xor edi, edi /* 0 */ + push 2 + pop rdx + /* call write() */ + push SYS_write /* 1 */ + pop rax + syscall + </%docstring> <% append_cdq = False @@ -95,13 +109,13 @@ Example: if syscall is None: args = ['?'] else: - args = [repr(syscall)] + args = [pretty(syscall, False)] for arg in [arg0, arg1, arg2, arg3, arg4, arg5]: if arg is None: args.append('?') else: - args.append(repr(arg)) + args.append(pretty(arg, False)) while args and args[-1] == '?': args.pop() syscall_repr = syscall_repr % ', '.join(args)
pwnlib/shellcraft/templates/amd64/push.asm+3 −3 modified@@ -1,6 +1,6 @@ <% from pwnlib.util import packing - from pwnlib.shellcraft import amd64 + from pwnlib.shellcraft import amd64, pretty from pwnlib.shellcraft.amd64 import pushstr from pwnlib import constants from pwnlib.shellcraft.registers import amd64 as regs @@ -31,7 +31,7 @@ Example: /* push 1 */ push 1 >>> print(pwnlib.shellcraft.amd64.push(256).rstrip()) - /* push 256 */ + /* push 0x100 */ push 0x1010201 ^ 0x100 xor dword ptr [rsp], 0x1010201 >>> with context.local(os = 'linux'): @@ -58,7 +58,7 @@ Example: pass %> %if not is_reg: - /* push ${repr(value_orig)} */ + /* push ${pretty(value_orig, False)} */ ${re.sub(r'^\s*/.*\n', '', amd64.pushstr(packing.pack(value), False), 1)} % else: push ${value}
pwnlib/shellcraft/templates/amd64/pushstr_array.asm+3 −3 modified@@ -1,4 +1,4 @@ -<% from pwnlib.shellcraft import amd64 %> +<% from pwnlib.shellcraft import amd64, pretty %> <%docstring> Pushes an array/envp-style array of pointers onto the stack. @@ -25,14 +25,14 @@ word_size = 8 offset = len(array_str) + word_size %>\ - /* push argument array ${repr(array)} */ + /* push argument array ${pretty(array, False)} */ ${amd64.pushstr(array_str)} ${amd64.mov(reg, 0)} push ${reg} /* null terminate */ % for i,arg in enumerate(reversed(array)): ${amd64.mov(reg, offset + word_size*i - len(arg))} add ${reg}, rsp - push ${reg} /* ${repr(arg)} */ + push ${reg} /* ${pretty(arg, False)} */ <% offset -= len(arg) %>\ % endfor ${amd64.mov(reg,'rsp')}
pwnlib/shellcraft/templates/amd64/pushstr.asm+1 −1 modified@@ -77,7 +77,7 @@ Args: else: extend = b'\x00' %>\ - /* push ${repr(string)} */ + /* push ${pretty(string, False)} */ % for word in lists.group(8, string, 'fill', extend)[::-1]: <% sign = packing.u64(word, endian='little', sign='signed')
pwnlib/shellcraft/templates/arm/freebsd/syscall.asm+1 −1 modified@@ -36,7 +36,7 @@ Example: if syscall is None: args = ['?'] else: - args = [repr(syscall)] + args = [pretty(syscall, False)] for arg in [arg0, arg1, arg2, arg3, arg4, arg5]: if arg is None:
pwnlib/shellcraft/templates/arm/linux/open_file.asm+2 −1 modified@@ -12,6 +12,7 @@ Args: from pwnlib.asm import cpp from pwnlib.util.safeeval import expr from pwnlib.constants.linux import arm as consts + from pwnlib.shellcraft import pretty filepath_lab, after = label("filepath"), label("after") filepath_out = [hex(ord(c)) for c in filepath] while True: @@ -31,7 +32,7 @@ Args: svc SYS_open b ${after} - /* The string ${repr(str(filepath))} */ + /* The string ${pretty(str(filepath), False)} */ ${filepath_lab}: .byte ${filepath_out} ${after}:
pwnlib/shellcraft/templates/arm/linux/syscall.asm+4 −4 modified@@ -1,5 +1,5 @@ <% - from pwnlib.shellcraft import arm + from pwnlib.shellcraft import arm, pretty from pwnlib.constants import eval from pwnlib.abi import linux_arm_syscall as abi from six import text_type @@ -14,7 +14,7 @@ Any of the arguments can be expressions to be evaluated by :func:`pwnlib.constan Example: >>> print(shellcraft.arm.linux.syscall(11, 1, 'sp', 2, 0).rstrip()) - /* call syscall(11, 1, 'sp', 2, 0) */ + /* call syscall(0xb, 1, 'sp', 2, 0) */ mov r0, #1 mov r1, sp mov r2, #2 @@ -57,13 +57,13 @@ Example: if syscall is None: args = ['?'] else: - args = [repr(syscall)] + args = [pretty(syscall, False)] for arg in [arg0, arg1, arg2, arg3, arg4, arg5]: if arg is None: args.append('?') else: - args.append(repr(arg)) + args.append(pretty(arg, False)) while args and args[-1] == '?': args.pop() syscall_repr = syscall_repr % ', '.join(args)
pwnlib/shellcraft/templates/arm/pushstr_array.asm+3 −3 modified@@ -1,4 +1,4 @@ -<% from pwnlib.shellcraft import arm %> +<% from pwnlib.shellcraft import arm, pretty %> <%docstring> Pushes an array/envp-style array of pointers onto the stack. @@ -25,13 +25,13 @@ word_size = 4 offset = len(array_str) + word_size %>\ - /* push argument array ${repr(array)} */ + /* push argument array ${pretty(array, False)} */ ${arm.pushstr(array_str)} ${arm.push(0)} /* null terminate */ % for i,arg in enumerate(reversed(array)): ${arm.mov(reg, offset + word_size*i - len(arg))} add ${reg}, sp - ${arm.push(reg)} /* ${repr(arg)} */ + ${arm.push(reg)} /* ${pretty(arg, False)} */ <% offset -= len(arg) %>\ % endfor ${arm.mov(reg,'sp')}
pwnlib/shellcraft/templates/arm/pushstr.asm+2 −1 modified@@ -1,5 +1,6 @@ <% from pwnlib.util import lists, packing, fiddling %> <% from pwnlib.shellcraft.arm import push %> +<% from pwnlib.shellcraft import pretty %> <% import six %> <%page args="string, append_null = True, register='r7'"/> <%docstring> @@ -32,7 +33,7 @@ Examples: while len(string) % 4: string += b'\x41' %>\ - /* push ${repr(string)} */ + /* push ${pretty(string, False)} */ % for word in packing.unpack_many(string, 32)[::-1]: ${push(word, register)} % endfor
pwnlib/shellcraft/templates/i386/cgc/syscall.asm+2 −2 modified@@ -21,13 +21,13 @@ Any of the arguments can be expressions to be evaluated by :func:`pwnlib.constan if syscall is None: args = ['?'] else: - args = [repr(syscall)] + args = [pretty(syscall, False)] for arg in [arg0, arg1, arg2, arg3, arg4, arg5]: if arg is None: args.append('?') else: - args.append(repr(arg)) + args.append(pretty(arg, False)) while args and args[-1] == '?': args.pop() syscall_repr = syscall_repr % ', '.join(args)
pwnlib/shellcraft/templates/i386/freebsd/syscall.asm+1 −1 modified@@ -68,7 +68,7 @@ Example: if syscall is None: args = ['?'] else: - args = [repr(syscall)] + args = [pretty(syscall, False)] for arg in [arg0, arg1, arg2, arg3, arg4, arg5]: if arg is None:
pwnlib/shellcraft/templates/i386/linux/syscall.asm+1 −1 modified@@ -93,7 +93,7 @@ Example: if syscall is None: args = ['?'] else: - args = [repr(syscall)] + args = [pretty(syscall, False)] for arg in [arg0, arg1, arg2, arg3, arg4, arg5]: if arg is None:
pwnlib/shellcraft/templates/i386/pushstr_array.asm+3 −3 modified@@ -1,4 +1,4 @@ -<% from pwnlib.shellcraft import i386 %> +<% from pwnlib.shellcraft import i386, pretty %> <%docstring> Pushes an array/envp-style array of pointers onto the stack. @@ -25,14 +25,14 @@ word_size = 4 offset = len(array_str) + word_size %>\ - /* push argument array ${repr(array)} */ + /* push argument array ${pretty(array, False)} */ ${i386.pushstr(array_str)} ${i386.mov(reg, 0)} push ${reg} /* null terminate */ % for i,arg in enumerate(reversed(array)): ${i386.mov(reg, offset + word_size*i - len(arg))} add ${reg}, esp - push ${reg} /* ${repr(arg)} */ + push ${reg} /* ${pretty(arg, False)} */ <% offset -= len(arg) %>\ % endfor ${i386.mov(reg,'esp')}
pwnlib/shellcraft/templates/mips/freebsd/syscall.asm+1 −1 modified@@ -61,7 +61,7 @@ Example: if syscall is None: args = ['?'] else: - args = [repr(syscall)] + args = [pretty(syscall, False)] for arg in [arg0, arg1, arg2, arg3, arg4, arg5]: if arg is None:
pwnlib/shellcraft/templates/mips/linux/syscall.asm+1 −1 modified@@ -91,7 +91,7 @@ Example: if syscall is None: args = ['?'] else: - args = [repr(syscall)] + args = [pretty(syscall, False)] for arg in [arg0, arg1, arg2, arg3, arg4, arg5]: if arg is None:
pwnlib/shellcraft/templates/mips/pushstr_array.asm+4 −4 modified@@ -1,4 +1,4 @@ -<% from pwnlib.shellcraft import mips %> +<% from pwnlib.shellcraft import mips, pretty %> <%docstring> Pushes an array/envp-style array of pointers onto the stack. @@ -25,14 +25,14 @@ word_size = 4 offset = len(array_str) + word_size %>\ - /* push argument array ${repr(array)} */ + /* push argument array ${pretty(array, False)} */ ${mips.pushstr(array_str)} ${mips.mov(reg, 0)} ${mips.push(reg)} /* null terminate */ % for i,arg in enumerate(reversed(array)): ${mips.mov(reg, offset + word_size*i - len(arg))} - add ${reg}, $sp - ${mips.push(reg)} /* ${repr(arg)} */ + add ${reg}, $sp, ${reg} + ${mips.push(reg)} /* ${pretty(arg, False)} */ <% offset -= len(arg) %>\ % endfor ${mips.mov(reg,'$sp')}
pwnlib/shellcraft/templates/mips/pushstr.asm+2 −5 modified@@ -1,6 +1,6 @@ <% from pwnlib.util import lists, packing, fiddling - from pwnlib.shellcraft import mips + from pwnlib.shellcraft import mips, pretty import six %>\ <%page args="string, append_null = True"/> @@ -90,13 +90,10 @@ Args: num = 0x101 return num - def pretty(n): - return hex(n & (2 ** 32 - 1)) - split_string = lists.group(4, string, 'fill', b'\x00') stack_offset = len(split_string) * -4 %>\ - /* push ${repr(string)} */ + /* push ${pretty(string, False)} */ % for index, word in enumerate(split_string): % if word == b'\x00\x00\x00\x00': sw $zero, ${stack_offset+(4 * index)}($sp)
pwnlib/shellcraft/templates/thumb/freebsd/syscall.asm+1 −1 modified@@ -36,7 +36,7 @@ Example: if syscall is None: args = ['?'] else: - args = [repr(syscall)] + args = [pretty(syscall, False)] for arg in [arg0, arg1, arg2, arg3, arg4, arg5]: if arg is None:
pwnlib/shellcraft/templates/thumb/linux/syscall.asm+4 −4 modified@@ -1,5 +1,5 @@ <% - from pwnlib.shellcraft import thumb + from pwnlib.shellcraft import thumb, pretty from pwnlib.constants import eval from pwnlib.abi import linux_arm_syscall as abi from six import text_type @@ -14,7 +14,7 @@ Any of the arguments can be expressions to be evaluated by :func:`pwnlib.constan Example: >>> print(shellcraft.thumb.linux.syscall(11, 1, 'sp', 2, 0).rstrip()) - /* call syscall(11, 1, 'sp', 2, 0) */ + /* call syscall(0xb, 1, 'sp', 2, 0) */ mov r0, #1 mov r1, sp mov r2, #2 @@ -65,13 +65,13 @@ Example: if syscall is None: args = ['?'] else: - args = [repr(syscall)] + args = [pretty(syscall, False)] for arg in [arg0, arg1, arg2, arg3, arg4, arg5]: if arg is None: args.append('?') else: - args.append(repr(arg)) + args.append(pretty(arg, False)) while args and args[-1] == '?': args.pop() syscall_repr = syscall_repr % ', '.join(args)
pwnlib/shellcraft/templates/thumb/push.asm+3 −3 modified@@ -1,6 +1,6 @@ <% from pwnlib.util import packing - from pwnlib.shellcraft import thumb, registers + from pwnlib.shellcraft import thumb, registers, pretty from pwnlib import constants from pwnlib.context import context as ctx # Ugly hack, mako will not let it be called context import six @@ -32,7 +32,7 @@ Example: mov r7, #1 push {r7} >>> print(pwnlib.shellcraft.thumb.push(256).rstrip()) - /* push 256 */ + /* push 0x100 */ mov r7, #0x100 push {r7} >>> print(pwnlib.shellcraft.thumb.push('SYS_execve').rstrip()) @@ -61,7 +61,7 @@ if not is_register and isinstance(value, (six.binary_type, six.text_type)): % if is_register: push {${value}} % elif isinstance(value, six.integer_types): - /* push ${repr(value_orig)} */ + /* push ${pretty(value_orig, False)} */ ${re.sub(r'^\s*/.*\n', '', thumb.pushstr(packing.pack(value), False), 1)} % else: push ${value}
pwnlib/shellcraft/templates/thumb/pushstr_array.asm+3 −3 modified@@ -1,4 +1,4 @@ -<% from pwnlib.shellcraft import thumb %> +<% from pwnlib.shellcraft import thumb, pretty %> <%docstring> Pushes an array/envp-style array of pointers onto the stack. @@ -25,13 +25,13 @@ word_size = 4 offset = len(array_str) + word_size %>\ - /* push argument array ${repr(array)} */ + /* push argument array ${pretty(array, False)} */ ${thumb.pushstr(array_str)} ${thumb.push(0)} /* null terminate */ % for i,arg in enumerate(reversed(array)): ${thumb.mov(reg, offset + word_size*i - len(arg))} add ${reg}, sp - ${thumb.push(reg)} /* ${repr(arg)} */ + ${thumb.push(reg)} /* ${pretty(arg, False)} */ <% offset -= len(arg) %>\ % endfor ${thumb.mov(reg,'sp')}
pwnlib/shellcraft/templates/thumb/pushstr.asm+2 −2 modified@@ -1,5 +1,5 @@ <% - from pwnlib.shellcraft import thumb + from pwnlib.shellcraft import thumb, pretty from pwnlib.util import lists, packing import six %> @@ -46,7 +46,7 @@ on your version of binutils. while offset % 4: offset += 1 %>\ - /* push ${repr(string)} */ + /* push ${pretty(string, False)} */ % for word in lists.group(4, string, 'fill', b'\x00')[::-1]: ${thumb.mov(register, packing.unpack(word))} push {${register}}
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-7xc5-ggpp-g249ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2020-28468ghsaADVISORY
- github.com/Gallopsled/pwntools/commit/138188eb1c027a2d0ffa4151511c407d3a001660ghsaWEB
- github.com/Gallopsled/pwntools/issues/1427ghsax_refsource_MISCWEB
- github.com/Gallopsled/pwntools/pull/1732ghsax_refsource_MISCWEB
- github.com/pypa/advisory-database/tree/main/vulns/pwntools/PYSEC-2021-72.yamlghsaWEB
- snyk.io/vuln/SNYK-PYTHON-PWNTOOLS-1047345ghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.