Nested invocation outputs ANSI escape codes and hangs
Update Pwntools First
The issue was tested on version 4.2.1, Python3 and Python2 version in ubuntu 18.04 Vagrant box.
Target/Description
Invoking a pwntools-based script using pwntools will cause read* to output ANSI color escapes and then hangs.
To reproduce the bug I've used the following script:
#!/usr/bin/env python3
from pwn import *
# Target script to analyze
target_script = """
import pwn
p = pwn.process(["/bin/echo", "ciao"])
print(p.recvline().decode("ascii"))
p.close()
"""
# Where to put the analyzed script
target_path = "./pexpect-bug.py"
exec_string = "/usr/bin/env python3 {} SILENT=1".format(target_path)
# Write it to the disk
with open(target_path, "w") as f:
f.write(target_script)
p = process(exec_string.split(" "))
while True:
try:
print(p.recv(1))
except:
break
p.close()
Debug Output / Results
The output, launching the script above with
$ python3 test.py DEBUG
Will result in:
[+] Starting local process '/usr/bin/env' argv=[b'/usr/bin/env', b'python3', b'./pexpect-bug.py', b'SILENT=1'] : pid 12679
[DEBUG] Received 0x11 bytes:
00000000 1b 5b 3f 32 35 6c 1b 5b 3f 31 68 1b 3d 1b 5b 36 │·[?2│5l·[│?1h·│=·[6│
00000010 6e │n│
00000011
b'\x1b'
b'['
b'?'
b'2'
b'5'
b'l'
b'\x1b'
b'['
b'?'
b'1'
b'h'
b'\x1b'
b'='
b'\x1b'
b'['
b'6'
b'n'
Executing directly the generated pexpect-bug.py file will return the correct output:
$ python3 pexpect-bug.py SILENT=1
ciao
Workaround
The problem seems to be related with the stdout = PTY default in the process class constructor.
Modifying the process invocation (of the main script) to
p = process(exec_string.split(" "), stdout=PIPE)
Will result in the correct output of the script:
[+] Starting local process '/usr/bin/env' argv=[b'/usr/bin/env', b'python3', b'./pexpect-bug.py', b'SILENT=1'] : pid 12714
[DEBUG] Received 0x6 bytes:
b'ciao\n'
b'\n'
b'c'
b'i'
b'a'
b'o'
b'\n'
b'\n'
[*] Process '/usr/bin/env' stopped with exit code 0 (pid 12714)
Misc
This issue was discovered by @Bonfee during a CTF.
You need to set PWNLIB_NOTERM=1 as environment to the child process, or to parse those escape codes (or assume their meaning) and report cursor position to the child, like this (hope this works, but you get the idea):
p.stdout.write(b'\33[1;1R')
Not stdin, because it is a pipe by default, if you didn't pass stdin=PTY as argument to process creation.
There will be one day an option for escape codes to be interpreted by pwntools itself, but for now it does not emulate the terminal and exposes raw bytestream I/O interface.
I think this is intended. You could try running vim or nano or top as a process, you would probably see the same results. Pwntools-based exploits are ncurses applications (so that they can draw fancy spinners in log.progress() and many more). It would be possible to defer the curses (or maybe just this CSI 6 n / CSI <i> ; <j> R) initialisation until the first fancy term call is made (pwntools.ui or log.progress). Feel free to contribute such a deferring mechanism.