Properly escaping SSH command in PHP - php

I have an SSH command that I'd like to execute with libssh2 in PHP:
sh -c '
rm -f /tmp/command.log
sleep 3 &
screen -p 0 -X stuff "\
script -a -c \"ls -l\" /tmp/command.log; kill -USR1 $!
"
wait
cat /tmp/command.log
'
I can't seem to escape it properly though, so SSH receives it exactly as above. I need to wrap it in double quotes so I can get PHP variables in there as well (ls -l will become $command).
I have tried:
"sh -c '
rm -f /tmp/command.log
sleep 3 &
screen -p 0 -X stuff \"\
script -a -c \\"ls -l\\" /tmp/command.log; kill -USR1 $!
\"
wait
cat /tmp/command.log
'"
as well as:
"sh -c '
rm -f /tmp/command.log
sleep 3 &
screen -p 0 -X stuff \"\
script -a -c \\\"ls -l\\\" /tmp/command.log; kill -USR1 $!
\"
wait
cat /tmp/command.log
'"
The first of which returns a PHP error and the second of which doesn't run the command.
The whole function (after the edit Morgan Wilde suggested):
function runShellCommand($command, $host, $user, $pass, $port){
if (!function_exists("ssh2_connect")) die("Fail: function ssh2_connect doesn't exist");
if(!($con = ssh2_connect($host, $port))){
return "Unable to establish connection. Is your server offline?";
} else {
if(!ssh2_auth_password($con, $user, $pass)) {
return "Failed to authenticate. Please ensure your server's password matches our records.";
} else {
$run = <<<HEREDOC
sh -c '
rm -f /tmp/command.log
sleep 3 &
screen -p 0 -X stuff "\
script -a -c \"touch /root/test234\" /tmp/command.log; kill -USR1 $!
"
wait
cat /tmp/command.log
'
HEREDOC;
if (!($stream = ssh2_exec($con, $run ))) {
return "Could not run command.";
} else {
stream_set_blocking($stream, true);
$data = "";
while ($buf = fread($stream,4096)) {
$data .= $buf;
}
fclose($stream);
if(empty($data)){
return "sh-4.1# $command\n\n";
} else {
return "sh-4.1# $command\n$data\n";
}
}
}
}
}

How about using the HEREDOC string quoting? I haven't tried it, but it works for other use cases.
$command = <<<HEREDOC
sh -c '
rm -f /tmp/command.log
sleep 3 &
screen -p 0 -X stuff "\
script -a -c \"ls -l\" /tmp/command.log; kill -USR1 $!
"
wait
cat /tmp/command.log
'
HEREDOC;
More on that here - http://php.net/manual/en/language.types.string.php

Try phpseclib, a pure PHP SSH implementation. eg.
<?php
include('Net/SSH2.php');
$ssh = new Net_SSH2('www.domain.tld');
if (!$ssh->login('username', 'password')) {
exit('Login Failed');
}
echo $ssh->read('username#username:~$');
$ssh->write("rm -f /tmp/command.log\n");
echo $ssh->read('username#username:~$');
$ssh->write("sleep 3 &\n");
echo $ssh->read('username#username:~$');
$ssh->write("screen -p 0 -X stuff \"\
script -a -c \\\"ls -l\\\" /tmp/command.log; kill -USR1 $!
\"");
...
?>

Related

PHP: proc_get_status exitcode is always -1 after pcntl_fork

I'm having troubles with using proc_get_status after a pcntl_fork in the parent process.
Here's an example with docker and PHP 7.4, but note the PHP version does not matter, the result is the same from at least PHP 7.2 to PHP 8.1.
Setup
Dockerfile
FROM php:7.4
ADD https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/
RUN chmod +x /usr/local/bin/install-php-extensions && \
install-php-extensions pcntl
Run: docker build -t php-pcntl-7.4 .
test.php
<?php
if ($argv[1] ?? false) {
pcntl_async_signals(true);
pcntl_signal(SIGCHLD, SIG_IGN);
$pid = pcntl_fork();
if ($pid === -1) {
throw new \RuntimeException('fork failed');
}
if ($pid === 0) {
fclose(STDIN);
fclose(STDOUT);
fclose(STDERR);
throw new \Exception('child should be ignored');
}
pcntl_waitpid($pid, $childStatusCode);
}
function runCommand(string $command): void
{
$pipes = [];
$proc = proc_open($command, [['pipe', 'r'],['pipe', 'w'],['pipe', 'w']], $pipes);
do {
if (isset($status)) {
usleep(300000);
}
$status = proc_get_status($proc);
} while ($status['running']);
echo 'Command: ' . $command . PHP_EOL;
echo 'last proc_get_status exitcode: ' . $status['exitcode'] . PHP_EOL;
echo PHP_EOL;
}
echo 'PHP version: ' . PHP_VERSION . PHP_EOL;
runCommand('/bin/echo test');
runCommand('/bin/false');
Test without running a fork before
Run: docker run -it --rm -v $PWD:/workdir -w /workdir php-pcntl-7.4 php test.php
Output:
PHP version: 7.4.28
Command: /bin/echo test
last proc_get_status exitcode: 0
Command: /bin/false
last proc_get_status exitcode: 1
Test with running a fork before
Run: docker run -it --rm -v $PWD:/workdir -w /workdir php-pcntl-7.4 php test.php 1
Output:
PHP version: 7.4.28
Command: /bin/echo test
last proc_get_status exitcode: -1
Command: /bin/false
last proc_get_status exitcode: -1
Does anyone have an idea of why the exitcode is always -1, and if there is a workaround ?

Script launched from PHP does not work correctly, but it does from command line

I create and run a shell script from a php script. The script has an if. Commands on then don't run when they should, but instead commands in else run when they shouldn't. When I run the same script from the command line, it works as expected.
php file:
<?php
.....
$msg = "#! /bin/sh\n\n";
file_put_contents($scriptfile, $msg, LOCK_EX);
$cmd = "/usr/bin/pdftohtml -noframes -q -p -s -i $renfilenc\n";
file_put_contents($scriptfile, $cmd, FILE_APPEND | LOCK_EX);
$noExtfile = preg_replace('"\.(pdf|PDF)$"', '', $renfilenc);
$cmd = "if [[ \$(grep -c -o \"</p>\" $noExtfile.html) -le 5 ]];
then
/usr/bin/gs -dNOPAUSE -q -r500 -sDEVICE=tiffg4 -dBATCH -sOutputFile=$noExtfile.tiff $renfilenc
/usr/bin/tesseract $noExtfile.tiff $noExtfile -c tessedit_create_hocr=1 -c hocr_font_info=1
else
/usr/bin/unoconv -f docx $noExtfile.html
/usr/bin/curl -i -F \"Profile=jsi\" -F \"Output=url\" -F \"Language=en\" -F \"infile=#$noExtfile.docx\" -F \"submit=Submit\" http://nl.ijs.si/tei/cgi/convert.pl > TEIconvert.log
grep -E -o \"http://.*\" TEIconvert.log > wgetline.log
sed -r \"s/^(.*).$/wget \\1\/tei.xml -O $noExtfile.xml/g\" wgetline.log > wgetTEIfile.sh
/bin/sh wgetTEIfile.sh
fi";
file_put_contents($scriptfile, $cmd, FILE_APPEND | LOCK_EX);
$cmd = "/bin/sh $scriptfile > /dev/null 2>&1 &";
shell_exec($cmd);
.....
?>
created script file:
#! /bin/sh
/usr/bin/pdftohtml -noframes -q -p -s -i USNews_Bostonian_image.pdf
if [[ $(grep -c -o "</p>" USNews_Bostonian_image.html) -le 5 ]];
then
/usr/bin/gs -dNOPAUSE -q -r500 -sDEVICE=tiffg4 -dBATCH -sOutputFile=USNews_Bostonian_image.tiff USNews_Bostonian_image.pdf
/usr/bin/tesseract USNews_Bostonian_image.tiff USNews_Bostonian_image -c tessedit_create_hocr=1 -c hocr_font_info=1
else
/usr/bin/unoconv -f docx USNews_Bostonian_image.html
/usr/bin/curl -i -F "Profile=jsi" -F "Output=url" -F "Language=en" -F "infile=#USNews_Bostonian_image.docx" -F "submit=Submit" http://nl.ijs.si/tei/cgi/convert.pl > TEIconvert.log
grep -E -o "http://.*" TEIconvert.log > wgetline.log
sed -r "s/^(.*).$/wget \1\/tei.xml -O USNews_Bostonian_image.xml/g" wgetline.log > wgetTEIfile.sh
/bin/sh wgetTEIfile.sh
fi
In this case the commands on then should run, not the ones on else.
Any ideas if something is wrong or missing?
I have found the answer here: https://superuser.com/questions/374406/why-do-i-get-not-found-when-running-a-script
So in my php I changed:
$msg = "#! /bin/sh\n\n";
and
$cmd = "/bin/sh $scriptfile > /dev/null 2>&1 &";
to
$msg = "#!/bin/bash \n\n";
and
$cmd = "/bin/bash $scriptfile > /dev/null 2>&1 &";
respectively

Running a PHP Script as a Daemon in Debian

I am trying to start a php script as a daemon in Debian. I also would like it to start on boot as well.
I have been starting using /path/to/php /path/to/script/Insert.php & without issue, and can shell_exec("nohup /path/to/php /path/to/script/Insert.php >/dev/null &") as well. I have tried using the below script, but it does not take the script into an operational state.
Am copying the file to /etc/init.d/ and using update-rc.d without issues. I can use service congen-insert startto 'start' the script, but it doesn't seem to actually run, and it doesn't start doing any work.
What am I missing, or where have I gone wrong with the scripts?
I know there are several ways to work around this, but I am really just trying to understand what I am doing incorrectly or why what I am doing is not working.
Any help or suggestions is extremely appreciated! If there is anything else you need or anything I have missed in my description, please let me know so I can correct it.
Thanks in advance.
Service script
#! /bin/sh
### BEGIN INIT INFO
# Provides: congen-insert
# Required-Start: $local_fs $network
# Required-Stop: $local_fs
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: congen-insert
# Description: DB Insert Daemon
### END INIT INFO
NAME="congen-insert"
DESC=" DB Insert Daemon"
PIDFILE="/var/run/${NAME}.pid"
LOGFILE="/var/log/${NAME}.log"
DAEMON="/path/to/php"
DAEMON_OPTS="/path/to/script/Insert.php"
START_OPTS="--start --background --make-pidfile --pidfile ${PIDFILE} --exec ${DAEMON} ${DAEMON_OPTS}"
STOP_OPTS="--stop --pidfile ${PIDFILE}"
test -x $DAEMON || exit 0
set -e
case "$1" in
start)
echo -n "Starting ${DESC}: "
start-stop-daemon $START_OPTS >> $LOGFILE
echo "$NAME."
;;
stop)
echo -n "Stopping $DESC: "
start-stop-daemon $STOP_OPTS
echo "$NAME."
rm -f $PIDFILE
;;
restart|force-reload)
echo -n "Restarting $DESC: "
start-stop-daemon $STOP_OPTS
sleep 1
start-stop-daemon $START_OPTS >> $LOGFILE
echo "$NAME."
;;
status)
echo -n "Sorry, this isn't implemented yet"
;;
*)
N=/etc/init.d/$NAME
echo "Usage: $N {start|stop|restart|force-reload}" >&2
exit 1
;;
esac
exit 0
Script I am trying to run:
const LoaderPath = __DIR__ . DIRECTORY_SEPARATOR . ".." .DIRECTORY_SEPARATOR . "includes.php";
require_once LoaderPath;
use PhpAmqpLib\Channel\AMQPChannel;
use PhpAmqpLib\Message\AMQPMessage;
use requests\InsertRequest;
$connection = GetRabbitConnection();
$channel = $connection->channel();
$RedisClient = GetRedisClient();
DeclareQueues($connection, $RedisClient);
$MySQLHost = $RedisClient->get(MySQLHostKey);
$MySQLUser = $RedisClient->get(MySQLUserKey);
$MySQLPassword = $RedisClient->get(MySQLPasswordKey);
$MySQLDatabase = $RedisClient->get(MySQLDatabaseKey);
$InsertExchange = $RedisClient->get(Insert.":".Exchange);
$InsertQueue = $RedisClient->get(Insert.":".Queue);
$Prefetch = $RedisClient->get(Insert.":".Prefetch);
$RedisClient->disconnect();
$RedisClient = null;
$mysql= new mysqli($MySQLHost, $MySQLUser, $MySQLPassword, $MySQLDatabase);
$channel->basic_qos(0,$Prefetch,false);
$channel->basic_consume($InsertQueue, $InsertExchange, false, false, false, false, "callback");
echo "Consuming on Exchange $InsertExchange with Queue $InsertQueue\n";
while(true) {
$channel->wait();
}
$channel->close();
function callback(AMQPMessage $message){
global $mysql;
echo "Message received", "\n";
$InsertRequest = new InsertRequest($message->body);
echo "Running Insert Statement\n";
if (!$mysql->query($InsertRequest->SQL)){
echo "Error: ".$mysql->error;
}
/** #type AMQPChannel $channel */
$channel = $message->delivery_info['channel'];
$channel->basic_ack($message->delivery_info['delivery_tag']);
echo "Insert Complete\n";
}
The issue was in the redirection of the output. I also modified the php file with a header for bash so it does not show as multiple php processes in top, but shows the file name instead:
Revised Service Script:
#! /bin/sh
### BEGIN INIT INFO
# Provides: congen-insert
# Required-Start: $local_fs $network
# Required-Stop: $local_fs
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: congen-insert
# Description: ConGen DB Insert Daemon
### END INIT INFO
NAME="congen-insert"
DESC="DB Insert Process for ConGen"
PIDFILE="/var/run/${NAME}.pid"
LOGFILE="/var/log/${NAME}.log"
DAEMON="/var/congen/php/controllers/congen-insert"
DAEMON_OPTS="> /dev/null 2>&1"
START_OPTS="--start --background --make-pidfile --pidfile ${PIDFILE} --exec ${DAEMON} ${DAEMON_OPTS}"
STOP_OPTS="--stop --pidfile ${PIDFILE}"
test -x $DAEMON || exit 0
set -e
case "$1" in
start)
echo -n "Starting ${DESC}: "
start-stop-daemon $START_OPTS >> $LOGFILE
echo "$NAME."
;;
stop)
echo -n "Stopping $DESC: "
start-stop-daemon $STOP_OPTS
echo "$NAME."
rm -f $PIDFILE
;;
restart|force-reload)
echo -n "Restarting $DESC: "
start-stop-daemon $STOP_OPTS
sleep 1
start-stop-daemon $START_OPTS >> $LOGFILE
echo "$NAME."
;;
status)
echo -n "Sorry, this isn't implemented yet"
;;
*)
N=/etc/init.d/$NAME
echo "Usage: $N {start|stop|restart|force-reload}" >&2
exit 1
;;
esac
exit 0
Revised PHP Script to run:
#!/php52/php-5.6.6/bin/php
<?php
const LoaderPath = __DIR__ . DIRECTORY_SEPARATOR . ".." . DIRECTORY_SEPARATOR . "includes.php";
require_once LoaderPath;
use PhpAmqpLib\Channel\AMQPChannel;
use PhpAmqpLib\Message\AMQPMessage;
use requests\InsertRequest;
$connection = GetRabbitConnection();
$channel = $connection->channel();
$RedisClient = GetRedisClient();
DeclareQueues($connection, $RedisClient);
$InsertExchange = $RedisClient->get(Insert.":".Exchange);
$InsertQueue = $RedisClient->get(Insert.":".Queue);
$Prefetch = $RedisClient->get(Insert.":".Prefetch);
$RedisClient->disconnect();
$RedisClient = null;
$mysql= ConnectionBuilder::GetMySQLi();
$channel->basic_qos(0,$Prefetch,false);
$channel->basic_consume($InsertQueue, $InsertExchange, false, false, false, false, "callback");
echo "Consuming on Exchange $InsertExchange with Queue $InsertQueue\n";
while(true) {
$channel->wait();
}
$channel->close();
function callback(AMQPMessage $message){
global $mysql;
echo "Message received", "\n";
$InsertRequest = new InsertRequest($message->body);
echo "Running Insert Statement\n";
if (!$mysql->query($InsertRequest->SQL)){
echo "Error: ".$mysql->error;
}
/** #type AMQPChannel $channel */
$channel = $message->delivery_info['channel'];
$channel->basic_ack($message->delivery_info['delivery_tag']);
echo "Insert Complete\n";
}
After adding the file to /etc/init.d/ and making both the php script and service script executable, I can start the service using service congen-insert start and use the rest of the commands just like any other init.d service.
It should be noted that I am redirecting the console to /dev/null, but you could also redirect to a file by replacing the /dev/null with a writable path.
An explanation of the 2>&1 quoted from another SO post "2 is the stream number for stderr (error messages), 1 is represents [sic] the stdout stream (the standard non-error output stream)." as such I am essentially redirecting stdout to /dev/null and redirecting stderr to stdout
Write a script using interactive shell commands to execute your php script as shown in the php from terminal example. This only works if PHP is compiled to include the --with-readline option
Setup a cron task (the linux task scheduler) to run this script as shown in the cron setup example.

How to pass PHP variables to Shell?

I have two files:
wget.php
<?php
include 'theme.php';
/*ceklogin();*/
css();
if($_POST['wget-send'])
{
$dir=$_POST['dir'];
$link=$_POST['link'];
exec('touch /tmp/wget-download-link.txt',$out);
exec('echo "'.$link.'" >> /tmp/wget-download-link.txt',$out);
exec('/www/wget_download.sh,$out);
echo $out[2];
exit();
}
echo "<form action=\"".$PHP_SELF."\" method=\"post\">";
echo "Download directory:<br><input type=\"text\" name=\"dir\" size=\"15\" value=\"/mnt/usb/\"/><br>";
echo '<br>Download link (one URL per line):<br>';
echo ("<textarea name=\"link\" rows=\"13\" cols=\"60\"></textarea><br><br>");
echo '<input type="submit" name="wget-send" value="Send" />';
echo "</form></div>";
foot();
echo '
</div>
</body>
</div>
</html>';
?>
wget_download.sh
while [ true ] ; do
urlfile=$( ls /tmp/wget-download-link.txt | head -n 1 )
if [ "$urlfile" = "" ] ; then
sleep 30
continue
fi
url=$( head -n 1 $urlfile )
if [ "$url" = "" ] ; then
mv $urlfile $urlfile.invalid
continue
fi
mv $urlfile $urlfile.busy
wget $url -o /tmp/wget.log -P $dir
mv $urlfile.busy $urlfile.done
done
How do I pass the variable from $dir in PHP to $dir in shell? so for example the $dir in my PHP is:
/mnt/usb
I want /mnt/usb to be executed in the wget_download.sh so it's gonna be like this:
wget $url -o /tmp/wget.log -P /mnt/usb
How do I do that?
First off, this is very very dangerous. Make sure you fully trust anyone able to access your site at all with running arbitrary commands on your server. I'd suggest ensuring SSL and proper password authentication.
With that caveat out of the way, you can us putenv("DIR=".$dir); See "http://php.net/manual/en/function.putenv.php" for a list of restrictions and configurations that might be required. You may need to prefix the variable names with "PHP_" in both php and in the shell script.
I second Daniels warning. That said I would just pass it as an argument:
dir="$1"
while [ true ] ; do
urlfile=$( ls /tmp/wget-download-link.txt | head -n 1 )
if [ "$urlfile" = "" ] ; then
sleep 30
continue
fi
url=$( head -n 1 $urlfile )
if [ "$url" = "" ] ; then
mv $urlfile $urlfile.invalid
continue
fi
mv $urlfile $urlfile.busy
wget $url -o /tmp/wget.log -P $dir
mv $urlfile.busy $urlfile.done
done
Which would change you exec call:
// the command would actually look like:
// /www/wget_download.sh /path/to/dir
exec('/www/wget_download.sh ' . escapeshellarg($dir),$out);

popen doesn't run a proces

I have a function that should run a process in background
function execInBackground($cmd) {
if (substr(php_uname(), 0, 7) == "Windows"){
pclose(popen('start /B '.$cmd, "r"));
}
else {
exec($cmd . " > /dev/null &");
}
}
I try to run script
$cmd = "php ..\runffmpeg.php";
execInBackground($cmd);
But it does nothing. When I try to run
$cmd = 'ffmpeg -i video_in.mp4 video_out.avi';
execInBackground($cmd);
Its all right. And when I try to run
exec("php ..\runffmpeg.php");
It's also all right.
So, pclose(popen('start /B php ..\runffmpeg.php', "r")); doesn't run a command. Whats a problem?
I'm using Windows and php 5.4.7
You can use: $cmd= include("php ..\runffmpeg.php");
hmm; forget process mgmt and just popen() the resource.
If it is a daemon, you get access to it, else a subprocess is created/destroyed with the popen()/pclose().

Categories