non-interactive 进程的密码管理

Password Management for non-interactive process

挑战

我需要一个密码管理工具,它将被其他进程(各种脚本:python、php、perl 等)调用,并且能够识别和验证密码调用者脚本以执行访问控制:return 返回密码或退出 -1

当前实现

在研究了各种框架之后,我决定使用 pythonkeepassdb,它能够处理 Keepass V1.X 后端数据库文件,并构建我自己的访问控制覆盖层(因为稍后可以对其进行自定义并集成到我们的 LDAP 中以进行 user/group 访问)。访问控制是通过重载每个条目的 notes 字段来完成的,以包含允许访问密码的 SHA-256 哈希列表。 (注意,这也验证了脚本没有被任何人更改)

使用 -p 参数调用密码管理器,该参数是被调用者的 PID script/application,并将执行以下步骤:

  1. 递归查找"up" 从它自己的 PID 开始并查找 parent。在我们到达进程 1 之前必须找到调用者 PID,它是 init 和 parent 0。这样我们就可以确定我们知道谁调用了这个密码管理器实例。
  2. 获取该 (parent) 进程的完整命令行并对其进行分析以寻找脚本语言,包括 python、perl、php、bash、bat、 groovy,等(shlex用于此)
  3. 找出脚本的绝对路径并计算其 SHA
  4. 将它与数据库值进行比较,看看它是否存在,如果存在,则允许脚本使用 return 以标准格式在 stdout 中编辑的密码。如果不是,则以 -1 退出。

问题

上面的实现对于合法的脚本来说效果很好,但是很容易混淆。设 caller.py 为允许访问特定条目 e 的脚本。 运行 命令行看起来像 python /path/to/caller.py arg1 arg2。解析命令行的代码是:

cmd = walk_ppids(pid)
lg.debug(cmd)
if cmd is False:
    lg.error("PID %s is not my parent process or not a process at all!" % pid)
    sys.exit(-1)

cmd_parts = shlex.split(cmd)
running_script = ""
for p in cmd_parts:
    if re.search("\.(php|py|pl|sh|groovy|bat)$", p, re.I):
        running_script = p
        break

if not running_script:
    lg.error("Cannot identify this script's name/path!")
    sys.exit(-1)

running_script = os.path.abspath(running_script)
lg.debug("Found "+running_script)

phash = hash_file(open(running_script, 'rb'), hashlib.sha256())

parent 进程的命令行是通过以下方式获取的:

os.popen("ps -p %s -o args=" % ppid).read().strip()

现在,混淆上述函数的最简单方法是创建一个没有 .sh 扩展名的 shell 脚本,该脚本将 caller.py 作为第一个参数。 sh 不使用其参数,而是调用密码管理器查询条目 e。命令行看起来像 fake_sh ./caller.py,因此上面的代码 return 是通过...这是错误的做法。

问题

人们会认为这是很久以前没有程序员解决的一个常见问题 hard-coding 传递给 scripts/apps 但我做了几天的研究但我似乎无法找到任何以类似方式工作的东西。我明白这个问题比较open-ended所以我会接受以下答案:

改进:使规则更加严格

第一步是确认正确的解释器 运行 的扩展名是否正确,这意味着 caller.py 不能 运行 在 /bin/bash 上。

python 可以利用类似的漏洞,例如 命令 python -W ./caller.py ./myUberHack.py。查找解释器的第一个 .py 参数的命令行分析器会认为 caller.py 是 运行ning... 而不是。

为所有解释器构建所有调用规则太费时了,所以我硬编码了这些假设。这些存储在 tuple 中,每行是:

(file extension, positional argument, interpreter first letters)
exts = (
    (".py", 1, "python"), 
    (".php", 2, "php"),
    (".pl", 1, "perl"),
    (".sh", 1, "/bin/bash"), # Assumption, we accept only bash 
    (".groovy", 1, "groovy"),
    (".rb", 1, "ruby"),
)
"""Matching extensions to positional arguments and interpreters"""

现在的验证码是:

for i in exts:
    # Check the specified cmdline position and extension
    if cmd_parts[i[1]].strip().endswith(i[0]):
        lg.debug("Checking "+cmd_parts[i[1]])
        running_script = cmd_parts[i[1]]

        # Make sure that the interpretter matches the extension
        if running_script.endswith(i[0]) and not cmd_parts[0].startswith(i[2]):
            lg.error("Wrong interpretter... go away...")
            sys.exit(-1)

        break

目前想不出更好的...