PHP中的命令行密码提示

Posted

技术标签:

【中文标题】PHP中的命令行密码提示【英文标题】:Command Line Password Prompt in PHP 【发布时间】:2010-09-16 07:24:21 【问题描述】:

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

这很简单,但我希望它不会在输入密码时将密码回显到屏幕上。我怎样才能用 php 做到这一点?

使用纯 PHP(没有 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 还是操作系统的批处理脚本语言编写的? 【参考方案1】:

发现于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;
  

【讨论】:

不适用于 Windows 7。根据各种在线论坛的说法,除了 Windows XP 和 2003 Server 之外,其他任何东西都无法使用。 在下面查看我的答案(或直接转至github.com/Seldaek/hidden-input)以获取适用于 XP 至 7、32/64 位且不会弹出丑陋提示的解决方案。 VBS 和 bash?我们应该添加更多的语言。 rtrim 可以去除有效字符(即字符串结尾的任何空格),但您可以省略它并改用echo -n @JMW 在本页底部的解决方案适用于 Windows 7 64。可能也适用于 Win 7 32。只需几行,就可以完成工作,并且只需要安装 Powershell。【参考方案2】:

根据您的环境(即,不在 Windows 上),您可以使用 ncurses 库(特别是 ncurses_noecho() 函数停止键盘回显和 ncurses_getch() 读取输入)来获取密码而不显示它屏幕。

【讨论】:

【参考方案3】:

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

<?php

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

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

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

【讨论】:

我确信项目中的 hiddeninput.exe 不是一个巨大的安全威胁。但使用来自互联网的随机二进制 blob 来处理密码并不是一个好习惯。即使这段代码是安全的,它也只是恶意行为者注入恶意内容的占位符...... 哈哈哈哈哈哈哈哈哈.exe【参考方案4】:

以下方法适用于 Linux CLI,但不适用于 Windows CLI 或 Apache。它也仅适用于标准 Ascii 表中的字符(不过,使其与扩展字符集兼容并不需要太多时间)。

我已经添加了一些代码来防止复制和粘贴密码。如果两个 cmets 之间的位被删除,则可以注入/粘贴密码。

我希望这对某人有所帮助。

<?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);
    
?>

【讨论】:

将通过一个不打印星号、防止复制粘贴、处理删除等的介绍性最小版本得到显着改进:但是,考虑到已编辑的问题,这仍然应该是公认的答案;并且正确处理字符串的额外复杂性很酷。【参考方案5】:

这是适用于所有平台的最简单的解决方案:

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【参考方案6】:

我想如果不使用 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/

【讨论】:

创建纯文本 COM 文件的好技巧(看起来有点像 EICAR 防病毒测试 ;-))不幸的是,这在 64 位 Windows 下不起作用......(不再是 16-位 COM 支持...并以这种方式创建 EXE 文件...祝你好运!)【参考方案7】:

适用于所有支持 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";

【讨论】:

【参考方案8】:

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

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

【讨论】:

我认为如果您在此处提供您的帖子的简明版本会更好(可能只有每种方法的示例),因为简单地发布链接违背了 SE 网站的最初想法。 是否有一个通用库提供提示用户输入密码、普通纯文本和跨平台菜单选项的能力? @CMCDragonkai 不,没有。此类功能在 PHP 中未实现,因此仅使用 PHP 无法完成。有一个 ncurses PHP 扩展,但它不适用于 Windows。【参考方案9】:

为什么不使用 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,并尝试将 Settings::Load() 更改为 SSH2::Load()【参考方案10】:

理论上您可以使用 stream_set_blocking() 来完成,但看起来管理 STDIN 时存在一些 PHP 错误。

看: 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');

【讨论】:

片段似乎缺少从标准输入读取以获取密码的行;但这个答案至少不像许多其他人那样假设 Windows CLI,而是回答编辑问题的“无系统调用”和“纯 PHP”部分。【参考方案11】:
system('stty -echo');

禁用当前终端回显,并且:

system('stty echo');

重新启用它。在fgets前后设置。

【讨论】:

以上是关于PHP中的命令行密码提示的主要内容,如果未能解决你的问题,请参考以下文章

如何从需要SUDO的php调用shell脚本?

Vigenere Cipher使用命令行提示

UBuntu 命令行登录总是提示login incorrect 的解决办法

mysql 命令行操作

linux 如何用命令行启动程序?

交换机重置密码