PHP を使ってブラウザから sudo 命令を実行する

公開日:
最終更新日:
PHPを使ってブラウザからsudo命令を実行する

今回は PHP を使ってブラウザから管理者権限が必要な sudo 命令を実行する方法を紹介したいと思います.
基本的に管理者権限が必要な命令を気軽に実行できるようにするのは危険なので気をつけてください.

また,今回は Raspberry Pi OS (Raspbian): 10.9の上で試していますが,Linux 系の OS であれば同じように実現できると思います.
Windows や Mac ではわかりませんが,参考になる部分はあるのではないかと思います.

1. モチベーション

なぜ管理者権限が必要な命令をわざわざ PHP を使って実行したいと思ったかというと,自宅用 Web サーバーにボタンを実装して,そのボタンを使ってデーモンの起動などを行ったりしたいなぁと思ったからです.

ssh すればいいのでは?と言われればそうなのですが,わざわざパソコンを開いて ssh をして…とするのが面倒だったので,スマホでブラウザを開いてボタン一つで処理したいなぁと思った次第です.

2. とりあえず PHP で実行してみる

とりあえず,sudo が必要な systemctl start hoge.service を実行してみましょう.

<?php
exec("systemctl start hoge.service 2>&1",$dum,$rtn);
echo('<pre>');
var_dump($dum);
echo('<pre>');
echo("return: $rtn");
?>

デバッグの方法などは以下の記事に書いているので,参考にしてください.

この test.php にブラウザからアクセスしてみると以下のように表示されました.

array(2) {
  [0]=>
  string(68) "Failed to start hoge.service: Interactive authentication required."
  [1]=>
  string(66) "See system logs and 'systemctl status hoge.service' for details."
}
return: 1

要するに,実行する権限がないよと言われてしまいました.

じゃあ sudo をつけて実行してみればいいのではないかということで,test.phpの2行目を exec("sudo systemctl start hoge.service 2>&1",$dum,$rtn); に変更して,ブラウザからアクセスすると以下のようなエラーが表示されました.

array(9) {
  [0]=>
  string(0) ""
  [1]=>
  string(66) "We trust you have received the usual lecture from the local System"
  [2]=>
  string(59) "Administrator. It usually boils down to these three things:"
  [3]=>
  string(0) ""
  [4]=>
  string(38) "    #1) Respect the privacy of others."
  [5]=>
  string(30) "    #2) Think before you type."
  [6]=>
  string(52) "    #3) With great power comes great responsibility."
  [7]=>
  string(0) ""
  [8]=>
  string(53) "sudo: no tty present and no askpass program specified"
}
return: 1

少し怖そうなメッセージが並んだ後に,tty を割り当てるか,askpass プログラムを指定しろと言われています.
今回のようなケースでは tty は割り当てられないため,これを解決するためにはこのコマンドをパスワードなしで実行できるように以下のような記事に従って指定すれば良いと思います (試していないので,本当にできるかはわかりませんが…).
しかし,ユーザーの権限を変えるのは少し怖かったため,今回は少し違う方法で試してみます.

3. C言語を使って解決する

まず,以下のようなコードを生成します.

#include <stdlib.h>
#include <unistd.h>
int main(){
	setuid(geteuid());
	system("systemctl start hoge.service");
	return 0;
}

どのようなことをしているかというと,3行目で setuid というシステムコール (man ページ) を使用して,プロセスの実 (real) ユーザID実効 (effective) ユーザID と同じものにしています.
つまり,コマンドを実行したのがルートユーザでなくても,実効ユーザがルートなら実ユーザをルートに変更してルート権限でその後の命令を実行できるようになるということです.
このように実ユーザID を変更した後に systemctl start hoge.service をしてあげることで,ルート権限でこの命令を実行できます.

次に生成したコードをコンパイルします.

$ gcc test-hoge-start.c -o test-hoge-start

これで,実行できるバイナリファイル test-hoge-startが生成できました.
この test-hoge-startを実行すればめでたく動く…と言いたいところですが,後一手間加えないといけません.

4. ファイルの権限を変更する

先程の章で 「実効ユーザがルートなら」と言ったので,実効ユーザをルートに変えてあげないといけません.

どうしてそうなるかの説明は後回しにして,まずはコマンドを実行してみましょう.

$ sudo chown root:root test-hoge-start
$ sudo chmod 4755 test-hoge-start
$ ls -l
-rwsr-xr-x 1 root root 8104 Jun 13 18:12 test-hoge-start

具体的にどういうことをしているかというと,まず1行目で実行ファイルの所有者をルートに変更しています.
そして,次の行で実行ファイルの権限を変更しています.
権限は rwsr-xr- となるのですが,あまり見慣れない s という権限がルートの実行権限のところに入っているのがわかると思います.

まず,この s という実行権限は何かというと他のユーザが実行した場合に,ファイルの所有者の権限で実行するという意味になります.
ここでは,所有者をルートに変更しているため,このファイルはルート権限で実行されるということになります.

この辺りの権限の詳しい説明は 6.補足 に載せておくので,興味のある方はじっくり読んでみてください.

4. 実際に試してみる

まず,起動したいシステムが起動していたら停止して,起動していないことを確かめておきましょう.

$ sudo systemctl stop hoge.service # 動いているなら停止する
$ sudo systemctl status hoge.service # 停止しているか確認する
● hoge.service - "Hoge"
   Loaded: loaded (/etc/systemd/system/hoge.service; disabled; vendor preset:
   Active: inactive (dead)

Active: inactive (dead)となっているので,停止しているとわかります.

次に先程の test.phpを使って試してみましょう.

<?php
exec("/path/to/test-hoge-start 2>&1",$dum,$rtn);
echo('<pre>');
var_dump($dum);
echo('<pre>');
echo("return: $rtn");
?>

2行目を test-hoge-startへのパスに変更しただけです.
この状態でブラウザからアクセスしてみると以下のように表示されました.

array(0) {
}
return: 0

エラーがなく実行が終了していることがわかると思います.
実際に動いているか確認してみましょう.

$ sudo systemctl status hoge.service # 動いているか確認する
● hoge.service - "Hoge"
   Loaded: loaded (/etc/systemd/system/hoge.service; disabled; vendor preset:
   Active: active (running) since Sun 2021-06-13 22:42:57 JST; 1min ago

Active: active (running)になってめでたく動いていることがわかると思います 👏

5. まとめ

PHP でブラウザから sudo命令を実行してみようというところから始まりましたが,OS のシステム周りの命令や権限の話まで入ってきてすごく勉強になりました.
権限をうまく使いこなして,安全に便利に使えるように頑張りたいと思います 👍

6. 補足

3 で setuid(geteuid()); のシステムコールを呼んで実ユーザを実効ユーザと同じに変更するとことと,4 でsudo chmod 4755 test-hoge-startをしてこの命令を実効ユーザをルートにすることはなんとなく同じようなことをしているような気になってどちらか一方でいいのではないかという気もしますが,実はそうではありません.
試してみるとわかると思いますが,どちらか一方でも抜くと権限がないと言われて実行できません.

そこで,どういう仕組みになっているのか少し掘り下げて解説します.

まず,何かの命令を実行するときは基本的に実ユーザの権限で実行されます.
その例外は実行ファイルの権限に sがついている時で,このときは実行ファイルの所有者の権限で実行されます.
このように実ユーザと実効ユーザは毎回一致しているとは限らないため,実効ユーザと呼ばれる概念があります.
つまり,実効ユーザはその命令がどのユーザの権限で実行されているかを表しています.

まとめると

  • 実ユーザ:命令を呼び出したユーザ
  • 実効ユーザ:命令を実行しているユーザ

と大まかに捉えられると思います.

ここで,setuid(geteuid());が実行できるかどうかについて考えます.
この命令は実効ユーザ,つまりルートの権限で実行されているので,実ユーザをルートに変更することも可能です.
一方で,sudo chmod 4755 test-hoge-startをしていなかった場合には,実ユーザ,つまり命令を呼び出したルートでないユーザの権限で実行されているので,実ユーザをルートに変更することはできません
そのため,sudo chmod 4755 test-hoge-startが必要であるということがわかると思います.

次に,setuid(geteuid());をする必要があるのかについて考えます.
test-hoge-start 自体はルートの権限で実行していますが,実ユーザは命令を呼び出したルートでないユーザのままなので,次の行の system("systemctl start hoge.service");実ユーザの権限で実行されてしまい,ルート権限を持たないため実行ができないということになります.
そのため,setuid(geteuid());をして実ユーザをルートに変更してから次の命令を実行しないとルート権限では実行できないということになるとわかっていただけるのではないかと思います.

簡単に図にまとめてみると以下のようになります (命令を実行している実ユーザは hugaとしています).

権限の遷移図
権限の遷移図

このあたりの権限の設定方法については,以下の記事なども参考になると思います.


コメントを残す

メールアドレスが公開されることはありません。

CAPTCHA