Challenge

A Discord calculator bot. Its !add command passes your expression into eval. The server wraps the input like this and runs it in a subprocess:

1
2
text = f"print(eval(\"{text}\"))"
proc = subprocess.Popen(['python3', '-c', text], stdout=subprocess.PIPE, preexec_fn=os.setsid)

The " inside the input is not escaped, so you are not stuck inside the string literal. The only thing standing between you and code execution is a substring blacklist:

1
SHELL_ESCAPE_CHARS = [":", "curl", "bash", "bin", "sh", "exec", "eval,", "|", "import", "chr", "subprocess", "pty", "popen", "read", "get_data", "echo", "builtins", "getattr"]

Approach

To poke at it without spamming the public bot, I invited it to a private server I controlled, gave myself the roles to reach the command, and worked from there.

The blacklist matches raw substrings, so the trick is to express the same code without ever spelling the banned words. os itself is not on the list, which helps. A few ways through:

  • Split the token across a concatenation so the substring never appears: 'imp'+'ort os'. The expression is evaluated, so the pieces rejoin at runtime.
  • Hand eval a base64 string and decode it, moving the real payload out of the matched alphabet entirely.
  • Write the blocked identifier with an escape. read is banned, but rea\x64 is the same name, since \x64 is the byte d. Source code escapes resolve before the lexer sees the identifier.

Solution

open and the file read are both reachable once read is hidden:

!add open("flag.txt").rea\x64()

The bot replying with the flag after the rea\x64 payload

Flag

csawctf{Y0u_4r3_th3_fl4g_t0_my_pyj4il_ch4ll3ng3}