2

I am attempting to utilize the Python module subprocess to automate a terminal command on Mac. Specifically, I am running a certain command to create port mappings on my machine. However, the command in question requires both root privileges and piping:

echo "
rdr pass inet proto tcp from any to any port 80 -> 127.0.0.1 port 8080
" | sudo pfctl -ef - 

In order to pass my root password to the shell command with subprocess, I followed the code example found here to create a script below:

from subprocess import PIPE, Popen
p = Popen(['echo', '"rdr pass inet proto tcp from any to any port 80 -> 127.0.0.1 port 8080"\n'], stdin=PIPE, stderr=PIPE, universal_newlines=True)
p2 = Popen(['sudo', '-S']+['pfctl', '-ef', '-'], stdin=p.stdout, stderr=PIPE, universal_newlines=True) 
return p2.communicate('my_root_password\n')[1] 

Note that to implement the piping of the echo output to the command pfctl -ef - I have created two Popen objects and have passed the stdout of the first object to the stdin parameter of second, as recommended in the subprocess docs, and am using Popen.communicate to write the root password to the stdin.

However, my script above is not working, as I am still prompted in the terminal to enter my root password. Strangely, I am able to successfully write my root password to stdin when using a command without piping, for instance, when running sudo pfctl -s nat (to display my current port mapping settings):

p = Popen(['sudo', '-S']+'pfctl -s nat'.split(), stdin=PIPE, stderr=PIPE, universal_newlines=True)
print(p.communicate('root_password\n')[1])

The above code works, as the mapping configuration is displayed without any password prompt.

How can my first Python script be changed so that I am not prompted to enter my root password, having already utilized Popen.communicate to write the password to stdin?


I am running this code on macOS Sierra 10.12.5

Ajax1234
  • 69,937
  • 8
  • 61
  • 102
  • Not an answer, but why not remove the sudo and run the script as root? – mVChr Oct 03 '18 at 17:16
  • @mVChr That is a good idea, but I am actually planning on running this as part of a program that queries the user for the password to make changes, in this case, update the port mapping rules. – Ajax1234 Oct 03 '18 at 17:19
  • `sudo` reads your password from `stdin`, your `stdin` is connected to other process `stdout`, `sudo` cannot read your password. – yorodm Oct 03 '18 at 18:00
  • @yorodm That makes sense. Is there any way I could work around that? – Ajax1234 Oct 03 '18 at 18:04
  • 1. User must be in sudoers file. or 2. Use `subprocess.call` – yorodm Oct 03 '18 at 18:05
  • @yorodm Where should `subprocess.call` be used? In place of the first `Popen`, or the second? – Ajax1234 Oct 03 '18 at 18:10
  • After commenting on your question I went out to see if `pexpect` was still around. It is a far better solution that using standard lib modules. [You should check it out](https://github.com/pexpect/pexpect) – yorodm Oct 03 '18 at 18:19

1 Answers1

2

I think this is just a simple case of the pipes not being connected up properly. You don't specify a pipe for the stdout of the first process so by the looks of things the output just gets printed to the terminal then the process finishes.

When second process starts, it will prompt for the password and as far as I can see receive it correctly. However the communicate method then closes the input and waits for the process to finish. As far as I can see the output of the first process never reaches the second, which is why your script isn't working. Instead of creating a separate echo process, why not just send all the text data you need with communicate?

The other problem it looks like you have (I don't have a MAC to check) is that sudo is printing the prompt directly to the terminal (ie via /dev/tty rather than stdout). On my version of sudo (on Debian) adding the -S option causes it to print the prompt to stderr. However it looks like the -S option doesn't do this on a MAC. Instead try disabling the prompt with -p ''.

Putting everything together, this should work:

from subprocess import PIPE, Popen
from getpass import getpass

password = getpass()

cmd = ['sudo', '-k', '-S', '-p', '', 'pfctl', '-ef', '-']
p = Popen(cmd, stdin=PIPE, stderr=PIPE, universal_newlines=True) 
text = password + '\n'
text += 'rdr pass inet proto tcp from any to any port 80 -> 127.0.0.1 port 8080\n'
p.communicate(text)

Security Note

This answer was updated to not use a plain text password. See the comments below for a good example of why this is a bad idea! Note also that storing passwords in memory with Python isn't completely secure as if the memory is swapped to disk, this will include the password in plain text. With a lower level language, the mlock system call would be used to prevent any memory containing the password from being swapped.

Graeme
  • 2,971
  • 21
  • 26
  • Thank you very much! This works, however, it only updates the configuration file when running the code in a new terminal window every time. Do you know why this might be? I am assuming that is simply a feature of `pfctl` and the Mac port manager, however, could the code have something to do with it? – Ajax1234 Oct 05 '18 at 19:00
  • Ah, probably because `sudo` doesn't need a password the second time round, so the password gets sent to `pfctl`. Try adding the `-k` option to `sudo`, which should make sure it always asks for a password. – Graeme Oct 05 '18 at 19:06
  • Thank you again! It works now! Will award bounty once the system lets me (in 21 hours). – Ajax1234 Oct 05 '18 at 19:12