在Python脚本中使用sudo

61
我正在尝试编写一个小脚本,每次执行该脚本时都会挂载VirtualBox共享文件夹。 我想使用Python来完成,因为我正在学习它用于脚本编写。
问题在于我需要特权来启动mount命令。 我可以作为sudo运行脚本,但我更喜欢让它自己做sudo。
我已经知道将密码写入.py文件是不安全的,但我们谈论的是一台完全不重要的虚拟机:我只想点击.py脚本并使其工作。
这是我的尝试:
#!/usr/bin/env python
import subprocess

sudoPassword = 'mypass'
command = 'mount -t vboxsf myfolder /home/myuser/myfolder'

subprocess.Popen('sudo -S' , shell=True,stdout=subprocess.PIPE)
subprocess.Popen(sudoPassword , shell=True,stdout=subprocess.PIPE)
subprocess.Popen(command , shell=True,stdout=subprocess.PIPE)

我的Python版本是2.6


不使用“/etc/fstab”的原因是什么? - mensi
1
@mensi 是的,我正在练习使用Python来实现这种目的。 - Roman Rdgz
1
你需要通过标准输入传递密码,查看此链接 https://dev59.com/HHVC5IYBdhLWcg3w2lGI#165662。 - Eun
2
如果你不知道自己在做什么,就避免使用shell=True。如果你不能在没有它的情况下使事情正常工作,那就学习它是什么以及它是如何工作的(然后通常你就可以了)。 - tripleee
这个回答解决了你的问题吗?从Python脚本以超级用户身份运行命令 - miken32
14个回答

88
许多答案都侧重于如何让您的解决方案起作用,而很少有人建议您的解决方案是一种非常糟糕的方法。如果您真的想要“练习学习”,为什么不练习使用好的解决方案呢?硬编码密码是学习“错误”的方法!
如果您真正想要一个无需密码的卷挂载,也许根本不需要sudo!那我可以建议其他方法吗?
  • 按照 mensi 的建议,使用 /etc/fstab。使用选项usernoauto,让普通用户挂载该卷。

  • 使用 Polkit 实现无需密码的操作:为您的脚本配置一个 .policy 文件,并添加 <allow_any>yes</allow_any>,将其放置在 /usr/share/polkit-1/actions 中。

  • 编辑 /etc/sudoers 文件,允许您的用户在不输入密码的情况下使用 sudo。如 @Anders 所建议的那样,您可以限制此类用法仅适用于特定命令,从而避免在您的帐户中拥有无限制的无需密码的 root 特权。请参见 this answer 以获取有关 /etc/sudoers 的更多详细信息。

以上所有方法都允许无需密码的 root 特权,但没有一种需要硬编码密码。选择任何一种方法,我都可以详细解释它。

至于为什么在编程中硬编码密码是一个非常糟糕的主意,以下是一些进一步阅读的好链接:


2
最后一点,编辑sudoers在https://askubuntu.com/a/155827/42796非常清楚地解释了。 - Pablo Marin-Garcia
3
为了新手更好地理解,你可以解释一下为什么硬编码用户密码是一种非常糟糕的方法。 - pdoherty926
1
@pdoherty926:出于安全考虑,我认为这是显而易见的,但你说得对,向人们解释“为什么”也可能是一个好主意。这超出了这个答案的范围,所以我将编辑它以添加一些进一步阅读的链接。 - MestreLion
1
@balu 请注意,当您将用户添加到 /etc/sudoers 文件时,您可以限制其访问某些命令并使用其他控件。将用户添加到 /etc/sudoers 文件中并不一定意味着启用了 root 访问权限。虽然对于简单的卷挂载操作,像 /etc/fstab 这样专门的工具当然更好用。 - Anders
@Anders 好观点!虽然我会说,一旦你允许以root身份执行任何一个指令,你就已经将攻击面扩大了数倍。(这些指令可能允许任意代码执行,它们可能会随时间而改变...)无论如何,我的意思并不是说提供可靠的安全保证是不可能的;我的意图只是想指出,在许多地方推荐的标准sudo解决方案并不像人们想象的那样安全,并且存在许多陷阱。(@MestreLion:感谢您相应更新回答!) - balu
显示剩余2条评论

52
sudoPassword = 'mypass'
command = 'mount -t vboxsf myfolder /home/myuser/myfolder'
p = os.system('echo %s|sudo -S %s' % (sudoPassword, command))

请尝试这个方法,并让我知道是否可行。 :-)

还有这个:

os.popen("sudo -S %s"%(命令), 'w').write('我的密码')


1
我导入了os,然后复制粘贴,但它不起作用:一直要求输入密码。实际上,如果我等待并且在被询问时不写任何内容,输出看起来像代码尝试错误地输入密码3次,连续3次显示“对不起,再试一次”。 - Roman Rdgz
1
@Roman Rdgz:这会将密码放入命令行中,可能会被其他用户看到。你可以直接写入子进程的stdin,而不运行shell。 - jfs
25
使用这行代码os.system('echo %s|sudo -S %s' % (sudoPassword, command))是非常不安全的,因为它存在安全漏洞。将密码作为shell命令写入会使其可以通过.bash_history文件和运行history shell 命令进行访问。更加安全可靠的方式是通过标准输入(stdin)传递密码。 - thodnev
4
这个回答有三个负评,我要再踩一下。这段代码增加了两个漏洞:1)将密码记录在进程表中,任何其他进程都可以看到,就像上面所说的那样;2)Shell注入,如果有其他东西可以设置该密码并将其设置为“foo $(rm -rf / *)bar”,你能看出其中的问题吗? - ulidtko
1
@AniketInge,我完全同意!那些令人讨厌的安全障碍在无关上下文中变得很烦人(“拒绝许可”到我的文件,呵呵),确实非常烦人。但你必须认识到:你根本不知道 SO 代码片段被用于多么广泛的上下文中。其中一些是非常相关的!你刚才已经同意了:这是一种糟糕的方法。紧接着,有更好的方法使用它们你不会失去任何东西,但可以获得安全性。为什么不在答案中提到这一点呢?只是为了那些忽略其他答案和评论部分的可怜的复制粘贴猎人。 - ulidtko
显示剩余9条评论

21
将密码传递给sudo的标准输入方式如下:
#!/usr/bin/env python
from subprocess import Popen, PIPE

sudo_password = 'mypass'
command = 'mount -t vboxsf myfolder /home/myuser/myfolder'.split()

p = Popen(['sudo', '-S'] + command, stdin=PIPE, stderr=PIPE,
          universal_newlines=True)
sudo_prompt = p.communicate(sudo_password + '\n')[1]

注意:你可以配置无密码sudo或SUDO_ASKPASS命令,而不是在源代码中硬编码密码。

你所描述的Popen会抛出一个错误can only concatenate list (not “str”) to list。我将其更改为Popen(['sudo -S ' + command],这对我有效。似乎在回答时隐式地将其添加到列表中,但现在不再允许或支持此操作。 - Piotr Kula
1
@ppumkin 错了。看看答案中的代码。它有 .split()。与你的代码进行比较。 - jfs
哦,是的。我错过了split()函数..哇,深夜编程。我睡了一晚上,决定不再像这样做了,而是采用无密码路线:D 我只是想让某些东西工作,并且感到绝望。 - Piotr Kula
你如何高效地通过sudo解决多个命令? 我想按照以下顺序执行这些命令:sudo mkdir Filestore sudo mount [filestore-info] Filestore sudo chmod 777 Filestore基本上是3个sudo命令。 - DUDANF

5
  • 在sudo命令中使用-S选项,它告诉程序从'stdin'而不是终端设备读取密码。

  • 让Popen从PIPE读取stdin。

  • 将密码作为通信方法的参数发送到进程的stdin PIPE。别忘了在密码末尾添加一个换行符'\n'。

sp = Popen(cmd , shell=True, stdin=PIPE)
out, err = sp.communicate(_user_pass+'\n')   

4
subprocess.Popen 创建进程并打开管道等。你现在正在做的是:
  • 启动一个进程 sudo -S
  • 启动一个进程 mypass
  • 启动一个进程 mount -t vboxsf myfolder /home/myuser/myfolder
显然这不会起作用。你需要将参数传递给 Popen。如果你查看文档,你会注意到第一个参数实际上是参数列表。

1
好的,我明白我做错了什么,但我不认为可以在这里将sudo密码作为参数传递,使用subprocess.Popen(['sudo', '-S', password, command], shell=True, stdin=subprocess.PIPE)。那么我该怎么办? - Roman Rdgz
请查看链接的SO问题。 - mensi

2
我使用的是 Python 3.5 版本。我使用了subprocess 模块。像这样使用密码非常不安全。 subprocess 模块将命令作为字符串列表处理,因此可以预先使用 split() 创建列表,或者稍后传递整个列表。请阅读文档以获取更多信息。
#!/usr/bin/env python
import subprocess

sudoPassword = 'mypass'
command = 'mount -t vboxsf myfolder /home/myuser/myfolder'.split()

cmd1 = subprocess.Popen(['echo',sudoPassword], stdout=subprocess.PIPE)
cmd2 = subprocess.Popen(['sudo','-S'] + command, stdin=cmd1.stdout, stdout=subprocess.PIPE)

output = cmd2.stdout.read.decode()

1

为了限制您以sudo身份运行的内容,您可以运行以下命令:

python non_sudo_stuff.py
sudo -E python -c "import os; os.system('sudo echo 1')"

无需存储密码。 -E 参数将您当前用户的环境传递给进程。请注意,第二个命令后,您的 shell 将具有 sudo 权限,因此请谨慎使用!


1
有时需要回车换行:

os.popen("sudo -S %s"%(command), 'w').write('mypass\n')

1
请尝试使用pexpect模块。这是我的代码:
import pexpect
remove = pexpect.spawn('sudo dpkg --purge mytool.deb')
remove.logfile = open('log/expect-uninstall-deb.log', 'w')
remove.logfile.write('try to dpkg --purge mytool\n')
if remove.expect(['(?i)password.*']) == 0:
    # print "successfull"
    remove.sendline('mypassword')
    time.sleep(2)
    remove.expect(pexpect.EOF,5)
else:
    raise AssertionError("Fail to Uninstall deb package !")

0

您可以使用SSHScript。以下是示例代码:

## filename: example.spy 
sudoPassword = 'mypass'
command = 'mount -t vboxsf myfolder /home/myuser/myfolder'
$$echo @{sudoPassword} | sudo -S @{command}

或者,只需一行代码(几乎与在控制台上运行相同)

## filename: example.spy 
$$echo mypass | sudo -S mount -t vboxsf myfolder /home/myuser/myfolder

然后,在控制台上运行它

sshscript example.spy

“sshscript” 是 SSHScript 的 CLI(通过 pip 安装)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接