PHP中的命令行密码提示


77

我正在编写一个命令行工具来帮助我的Web应用程序。需要密码才能连接到服务。我希望脚本显示密码提示,因此我不必将其作为命令行参数传递。

这很容易,但是我希望它在输入时不将密码回显到屏幕上。我该如何使用PHP?

使用纯PHP(no system('stty'))并用替换字符的好处*

编辑:

该脚本将在Unix之类的系统(Linux或Mac)上运行。该脚本是用PHP编写的,很可能会一直保持这种状态。

另外,记录在案的stty方法是:

echo "Password: ";
system('stty -echo');
$password = trim(fgets(STDIN));
system('stty echo');
// add a new line since the users CR didn't echo
echo "\n";

我希望那里没有system()电话。


命令行脚本将在什么操作系统上运行?命令行脚本将用PHP还是操作系统的批处理脚本语言编写?
布伦丹·基德韦尔

Answers:


42

sitepoint上找到。

function prompt_silent($prompt = "Enter Password:") {
  if (preg_match('/^win/i', PHP_OS)) {
    $vbscript = sys_get_temp_dir() . 'prompt_password.vbs';
    file_put_contents(
      $vbscript, 'wscript.echo(InputBox("'
      . addslashes($prompt)
      . '", "", "password here"))');
    $command = "cscript //nologo " . escapeshellarg($vbscript);
    $password = rtrim(shell_exec($command));
    unlink($vbscript);
    return $password;
  } else {
    $command = "/usr/bin/env bash -c 'echo OK'";
    if (rtrim(shell_exec($command)) !== 'OK') {
      trigger_error("Can't invoke bash");
      return;
    }
    $command = "/usr/bin/env bash -c 'read -s -p \""
      . addslashes($prompt)
      . "\" mypassword && echo \$mypassword'";
    $password = rtrim(shell_exec($command));
    echo "\n";
    return $password;
  }
}

3
在Windows 7上不起作用。根据各种在线论坛,除Windows XP和2003 Server之外,其他任何功能均不起作用。
Tgr

请参阅下面的答案(或直接转到github.com/Seldaek/hidden-input),以获取适用于XP最高7、32 / 64位的解决方案,并且不会弹出难看的提示。
Seldaek 2012年

1
VBS和bash?我们应该再添加几种语言。
伊万·武恰卡(IvanVučica)

1
rtrim可以删除有效字符(即,以字符串结尾的任何空格),但是您可以忽略它而使用echo -n
Synexis

此页面底部的@JMW解决方案适用于Windows 764。可能也适用于Win 7 32。仅需几行,就可以完成工作,只需要安装Powershell。
迈克尔(Michael


10

您可以使用我的hiddeninput.exe文件获取真正的隐藏输入,而不会在屏幕上的任何地方泄漏信息。

<?php

echo 'Enter password: ';
$password = exec('hiddeninput.exe');
echo PHP_EOL;

echo 'Password was: ' . $password . PHP_EOL;

如果删除最后一个回显,则密码永远不会显示,但是您可以将其用于验证。


8
我确信该项目中的hiddeninput.exe不会对安全构成重大威胁。.但是,使用来自互联网的随机二进制Blob处理密码并不是一个好习惯。即使这段代码是安全的,它也只是恶意行为者注入令人讨厌的东西的占位符……
frottter

5

以下方法在Linux CLI下有效,但在Windows CLI或Apache下无效。它也只能与标准Ascii表中的chars一起使用(尽管使其与扩展的char集兼容并不需要很多)。

我放入了一些代码来防止复制和粘贴密码。如果删除了两个注释之间的位,则可以插入/粘贴密码。

我希望这可以帮助别人。

<?php

    echo("Password: ");
    $strPassword=getObscuredText();
    echo("\n");
    echo("You entered: ".$strPassword."\n");

    function getObscuredText($strMaskChar='*')
    {
        if(!is_string($strMaskChar) || $strMaskChar=='')
        {
            $strMaskChar='*';
        }
        $strMaskChar=substr($strMaskChar,0,1);
        readline_callback_handler_install('', function(){});
        $strObscured='';
        while(true)
        {
            $strChar = stream_get_contents(STDIN, 1);
            $intCount=0;
// Protect against copy and paste passwords
// Comment \/\/\/ to remove password injection protection
            $arrRead = array(STDIN);
            $arrWrite = NULL;
            $arrExcept = NULL;
            while (stream_select($arrRead, $arrWrite, $arrExcept, 0,0) && in_array(STDIN, $arrRead))            
            {
                stream_get_contents(STDIN, 1);
                $intCount++;
            }
//        /\/\/\
// End of protection against copy and paste passwords
            if($strChar===chr(10))
            {
                break;
            }
            if ($intCount===0)
            {
                if(ord($strChar)===127)
                {
                    if(strlen($strObscured)>0)
                    {
                        $strObscured=substr($strObscured,0,strlen($strObscured)-1);
                        echo(chr(27).chr(91)."D"." ".chr(27).chr(91)."D");
                    }
                }
                elseif ($strChar>=' ')
                {
                    $strObscured.=$strChar;
                    echo($strMaskChar);
                    //echo(ord($strChar));
                }
            }
        }
        readline_callback_handler_remove();
        return($strObscured);
    }
?>

入门版本将不会打印星号,防止复制粘贴,处理删除等问题,将得到显着改进。正确处理字符串的额外技巧很酷。
Dewi Morgan

3

这是所有平台上最简单的解决方案:

function prompt($message = 'prompt: ', $hidden = false) {
    if (PHP_SAPI !== 'cli') {
        return false;
    }
    echo $message;
    $ret = 
        $hidden
        ? exec(
            PHP_OS === 'WINNT' || PHP_OS === 'WIN32'
            ? __DIR__ . '\prompt_win.bat'
            : 'read -s PW; echo $PW'
        )
        : rtrim(fgets(STDIN), PHP_EOL)
    ;
    if ($hidden) {
        echo PHP_EOL;
    }
    return $ret;
}

然后prompt_win.bat在同一目录中创建:

SetLocal DisableDelayedExpansion
Set "Line="
For /F %%# In ('"Prompt;$H & For %%# in (1) Do Rem"') Do (
    Set "BS=%%#"
)

:loop_start
    Set "Key="
    For /F "delims=" %%# In ('Xcopy /L /W "%~f0" "%~f0" 2^>Nul') Do (
        If Not Defined Key (
            Set "Key=%%#"
        )
    )
    Set "Key=%Key:~-1%"
    SetLocal EnableDelayedExpansion
    If Not Defined Key (
        Goto :loop_end
    )
    If %BS%==^%Key% (
        Set "Key="
        If Defined Line (
            Set "Line=!Line:~0,-1!"
        )
    )
    If Not Defined Line (
        EndLocal
        Set "Line=%Key%"
    ) Else (
        For /F "delims=" %%# In ("!Line!") Do (
            EndLocal
            Set "Line=%%#%Key%"
        )
    )
    Goto :loop_start
:loop_end

Echo;!Line!

rtrim可以删除有效字符(即,以字符串结尾的任何空格),但是您可以忽略它而使用echo -n
Synexis

2

我猜想,如果不使用stty -echo,就没有简单的方法(实际上我想不出任何方法)。如果您打算在Windows上运行它,则可以创建一个批处理脚本,该脚本将为您的php脚本提供未经选择的键入信息。

@echo off
cls
SET /P uname=Enter Username:
echo hP1X500P[PZBBBfh#b##fXf-V@`$fPf]f3/f1/5++u5>in.com
set /p password=Enter password :<nul
for /f “tokens=*” %%i in (’in.com’) do set password=%%i
del in.com
echo.
c:\php\php.exe d:\php\test.php %uname% “%password%”
Pause

示例取自http://www.indiangnu.org/2008/php-hide-user-input-using-batch-script-windows/


2
创建一个纯文本COM文件的好技巧(看起来有点像EICAR防病毒测试;-))不幸的是,这在64位Windows下无法工作...(不再支持16位COM ...并创建一个EXE文件的方式……祝您好运!)
Ale

2

在具有powershell支持的每个Windows系统上均可使用。(来源:http//www.qxs.ch/2013/02/08/php-cli-password-prompts-on-windows-7/

<?php
// please set the path to your powershell, here it is: C:\Windows\system32\WindowsPowerShell\v1.0\powershell.exe
$pwd=shell_exec('C:\Windows\system32\WindowsPowerShell\v1.0\powershell.exe -Command "$Password=Read-Host -assecurestring \"Please enter your password\" ; $PlainPassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Password)) ; echo $PlainPassword;"');
$pwd=explode("\n", $pwd); $pwd=$pwd[0];
echo "You have entered the following password: $pwd\n";

0

为什么不使用SSH连接?您可以将命令抽象出来,重定向输入/输出并拥有完全的控制权。

您可以为某人提供一个纯净的外壳,其权限与所需的权限一样少,并且只需将密码与SSH2 :: Connect()一起发布即可打开该外壳。

我创建了一个不错的类来与php SSH2扩展一起使用,也许它可以为您提供帮助;(它也可以安全地传输文件)

<?php

/**
 * SSH2
 * 
 * @package Pork
 * @author SchizoDuckie
 * @version 1.0
 * @access public
 */
class SSH2
{
    private $host;
    private $port;
    private $connection;
    private $timeout;
    private $debugMode;
    private $debugPointer;
    public $connected; 
    public $error;


    /**
     * SSH2::__construct()
     * 
     * @param mixed $host
     * @param integer $port
     * @param integer $timeout
     * @return
     */
    function __construct($host, $port=22, $timeout=10)
    {
        $this->host = $host;
        $this->port = $port;
        $this->timeout = 10;
        $this->error = 'not connected';
        $this->connection = false;
        $this->debugMode = Settings::Load()->->get('Debug', 'Debugmode');
        $this->debugPointer = ($this->debugMode) ? fopen('./logs/'.date('Y-m-d--H-i-s').'.log', 'w+') : false;
        $this->connected = false;

    }


    /**
     * SSH2::connect()
     * 
     * @param mixed $username
     * @param mixed $password
     * @return
     */
    function connect($username, $password)
    {
        $this->connection = ssh2_connect($this->host, $this->port);
        if (!$this->connection) return $this->error("Could not connect to {$this->host}:{$this->port}");
        $this->debug("Connected to {$this->host}:{$this->port}");
        $authenticated = ssh2_auth_password($this->connection, $username, $password);
        if(!$authenticated) return $this->error("Could not authenticate: {$username}, check your password");
        $this->debug("Authenticated successfully as {$username}");
        $this->connected = true;

        return true;
    }

    /**
     * SSH2::exec()
     *
     * @param mixed $command shell command to execute
     * @param bool $onAvailableFunction a function to handle any available data.
     * @param bool $blocking blocking or non-blocking mode. This 'hangs' php execution until the command has completed if you set it to true. If you just want to start an import and go on, use this icm onAvailableFunction and false
     * @return
     */
    function exec($command, $onAvailableFunction=false, $blocking=true)
    {
        $output = '';
        $stream = ssh2_exec($this->connection, $command);
        $this->debug("Exec: {$command}");
        if($onAvailableFunction !== false)
        {
            $lastReceived = time();
            $timeout =false;
            while (!feof($stream) && !$timeout)
            {
                $input = fgets($stream, 1024);
                if(strlen($input) >0)
                {
                    call_user_func($onAvailableFunction, $input);
                    $this->debug($input);
                    $lastReceived = time();
                }
                else
                {
                    if(time() - $lastReceived >= $this->timeout)
                    {
                        $timeout = true;
                        $this->error('Connection timed out');
                        return($this->error);
                    }
                }
            }
        }
        if($blocking === true && $onAvailableFunction === false)
        {
            stream_set_blocking($stream, true);
            $output = stream_get_contents($stream);
            $this->debug($output);
        }
        fclose($stream);
        return($output);
    }


    /**
     * SSH2::createDirectory()
     *
     * Creates a directory via sftp
     *
     * @param string $dirname
     * @return boolean success
     *  
     */
    function createDirectory($dirname)
    {
        $ftpconnection = ssh2_sftp ($this->connection);
        $dircreated = ssh2_sftp_mkdir($ftpconnection, $dirname, true);
        if(!$dircreated) 
        {
            $this->debug("Directory not created: ".$dirname);
        }
        return $dircreated;
    }

    public function listFiles($dirname)
    {
        $input = $this->exec(escapeshellcmd("ls  {$dirname}"));
        return(explode("\n", trim($input)));

    }

    public function sendFile($filename, $remotename)
    {
        $this->debug("sending {$filename} to {$remotename} ");
        if(file_exists($filename) && is_readable($filename))
        {
            $result = ssh2_scp_send($this->connection, $filename, $remotename, 0664);
        }
        else
        {
            $this->debug("Unable to read file : ".$filename);
            return false;
        }
        if(!$result) $this->debug("Failure uploading {$filename} to {$remotename}");
        return $result;
    }

    public function getFile($remotename, $localfile)
    {
        $this->debug("grabbing {$remotename} to {$localfile}");
        $result = ssh2_scp_recv($this->connection, $remotename, $localfile);

        if(!$result) $this->debug("Failure downloading {$remotename} to {$localfile}");
        return $result;
    }

    /**
     * SSH2::debug()
     * 
     * @param mixed $message
     * @return
     */
    function debug($message) 
    {
        if($this->debugMode)
        {
            fwrite($this->debugPointer, date('Y-m-d H:i:s')." : ".$message."\n");
        }
    }



    /**
     * SSH2::error()
     * 
     * @param mixed $errorMsg
     * @return
     */
    function error($errorMsg) 
    {
        $this->error = $errorMsg;
        $this->debug($errorMsg);
        return false;
    }   

    /**
     * SSH2::__destruct()
     * 
     * @return
     */
    function __destruct() 
    {
        if($this->connection){
            $this->connection = null;
        }
        if($this->debugMode && $this->debugPointer)
        {
            fclose($this->debugPointer);
        }
    }       


}

用法示例:

$settings = Settings::Load()->Get("SecureServer");
$ssh = new SSH2($settings['host']);
if( $ssh->connect($settings['username'], $settings['password']))
{
    echo $ssh->exec("ls -la ".$settings['path'], false, true);  
    flush();    
}

我收到错误:PHP致命错误:在第2行的/home/tester/tools/SSH/conn_ssh3.php中找不到类“设置”,我将ssh2类命名为Settings.php,并且还尝试更改设置: :Load()转换为SSH2 :: Load()
kamal

0

从理论上讲,您可以使用stream_set_blocking()进行此操作,但是看起来有些PHP错误会管理STDIN。

外观:http : //bugs.php.net/bug.php? id = 34972 http://bugs.php.net/bug.php?id=36030

尝试一下:

echo "Enter Password: ";
$stdin = fopen('php://stdin','r');
// Trying to disable stream blocking
stream_set_blocking($stdin, FALSE) or die ('Failed to disable stdin blocking');
// Trying to set stream timeout to 1sec
stream_set_timeout ($stdin, 1) or die ('Failed to enable stdin timeout');

Snippet似乎缺少从stdin读取以获取密码的行;但是此答案至少不是像其他许多Windows Windows CLI那样假设,并且正在回答已编辑问题的“无系统调用”和“纯PHP”部分。
Dewi Morgan

0

接受的答案不够好。首先,Windows解决方案不适用于Windows 7及更高版本。其他操作系统的解决方案取决于Bash和bash内置的“读取”。但是,有些系统不使用Bash(例如OpenBSD),并且显然不起作用。

在此博客中,我讨论了从95到8的几乎所有基于Unix的操作系统和Windows均可使用的解决方案。Windows解决方案使用Win32 API上用C编写的外部程序。其他操作系统的解决方案使用外部命令“ stty”。我还没有看到没有'stty'的基于Unix的系统


1
我认为,如果您在此处提供简洁的文章版本(可能只是每种方法仅提供示例)会更好,因为仅发布链接违背了SE网站的最初想法。
user907860 2013年

是否有通用库可以跨平台提示用户输入密码,普通纯文本和菜单选项?
CMCDragonkai 2013年

@CMCDragonkai不,没有。此类功能未在PHP中实现,因此不能仅使用PHP来完成。有一个ncurses PHP扩展名,但是在Windows上不起作用。
罗伯特·彼得拉诺维奇
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.