親スクリプト・プロセスの標準入力に書き込んで、高bashプロセスでコマンドを実行します。

親スクリプト・プロセスの標準入力に書き込んで、高bashプロセスでコマンドを実行します。

bash.sh.NETを使用して別のbashインスタンスを起動する単純なbashスクリプトがありますpkexec

#!/bin/bash
bash -c 'pkexec bash'

実行すると、ユーザーにパスワードの入力を求められます。デフォルトスクリプトはbash.sh通常のユーザーとして実行されますが、それによって開始されたbashインスタンスは昇格された権限を持つルートとして実行されます。

ターミナルウィンドウを開き、上昇したbashプロセスのstdinにいくつかのコマンドを書き込もうとすると、期待どおりに許可エラーが発生します。

echo 'echo hello' > /proc/<child-bash-pid>/fd/0

問題は、親プロセス()に書き込むときに子bash.shbashプロセスに渡され、コマンドを実行することです。

echo 'echo hello' > /proc/<parent-bash.sh-pid>/fd/0

これがどのように可能であるか理解できませんか?親プロセスは通常のユーザーとして実行されますが、なぜ一般ユーザーはより高い権限で実行されている子プロセスにコマンドを渡すことができますか?

子プロセスの stdin が親スクリプトの stdin にリンクされていることがわかっていますが、許可されている場合、通常のプロセスは root bash プロセスの親プロセスに書き込むことで root コマンドを実行できます。

これは論理的ではないようです。私は何を見逃していますか?

/usr/share注:rootman実行権限を持つファイルを削除して、親に渡されたコマンドを子が実行していることを確認しました。

sudo touch /usr/share/testfile

echo 'rm -f /usr/share/testfile' > /proc/<parent-bash.sh-pid>/fd/0

ファイルが削除されました。

答え1

これは正常です。これを理解するために、ファイル記述子がどのように機能するのか、プロセス間でどのように伝達されるのかを見てみましょう。

GLib.spawn_async()ビルドシェルスクリプトを使用していると述べました。関数はおそらくデータを子の標準入力に送信するパイプを生成します(またはパイプを直接生成して関数に渡すこともできます)。子プロセスを生成するために、この関数は新しいプロセスを終了し、fork()stdinパイプがfdになるようにファイル記述子を並べ替え、スクリプトを作成します。スクリプトはで始まるので、カーネルはbashシェルを介してそれを解釈してからシェルスクリプトを実行します。シェルスクリプトは別のbashを分岐して実行します(これは冗長ですが、実際にはbashは必要ありません)。ファイル記述子は再配置されないため、新しいプロセスはstdinファイル記述子と同じパイプを継承します。これはそれ自体が親プロセスに「接続」されないことに注意してください。実際、ファイル記述子は同じパイプ、つまりによって作成または割り当てられたパイプを参照します。実際、我々はパイプのエイリアスを作成します。 fd 0このプロセスはすべてパイプを参照します。0exec()#!/bin/bashexec()bash -cGLib.spawn_async()

pkexec呼び出されるとプロセスが繰り返されますが、pkexecこれは suid ルートバイナリです。これは、バイナリがexec()編集されるとルートとして実行されますが、標準入力は依然として元のパイプに接続されていることを意味します。pkexec次に、権限の確認(パスワード要求を含む)を実行し、最後にexec()bashを実行します。これで、パイプから入力を受け取るルートシェルがあり、ユーザーが所有する他の多くのプロセスもそのパイプを参照します。

理解すべき重要な点は、POSIXセマンティクスのファイル記述子に権限がないことです。ファイルには権限がありますが、ファイル記述子はファイル(またはパイプなどの抽象バッファ)にアクセスする権限を表します。ファイル記述子を新しいプロセスまたは既存のプロセス(UNIXソケットを介して)に渡すことができ、ファイルにアクセスする権限がファイル記述子と共に渡されます。ファイルを開いて所有者を別のユーザーに変更することもできますが、ファイルを開くときにのみ権限が確認されるため、古い所有者として元のfdを介してファイルにアクセスできます。これにより、ファイル記述子は権限の境界を越えて通信できます。ユーザーが所有するプロセスと root が所有するプロセスが同じファイル記述子を共有できるようにすることで、両方のプロセスがファイル記述子に対して同じ権限を付与します。また、fd はパイプであり、root プロセスはそのパイプからコマンドを受け取るため、ユーザーが所有する他のプロセスは root でコマンドを実行できます。パイプ自体には所有者の概念はなく、開かれたファイル記述子を持つ一連のプロセスのみがあります。

さらに、基本的なLinuxセキュリティモデルでは、ユーザーがすべてのプロセスに対して完全な制御を持っていると仮定しているので、あなたがしたように/procfdにアクセスするためにスヌープできることを意味します。ルートとして実行されるbashプロセスのエントリではこれを行うことはできませんが/proc(ルートではないため)、自分のプロセスに対して実行して実行できるものとまったく同じパイプファイル記述子を取得できます。ルートとして実行される子プロセス。したがって、パイプにデータをエコーすると、カーネルはパイプからコマンドを読み取るプロセスにデータを返します。この場合、サブルートシェルだけがパイプからコマンドを積極的に読み込んでいます。

シェルスクリプトが端末から呼び出される場合、データを標準入力ファイル記述子にエコーすると実際にデータが書き込まれます。到着ユーザーには表示されますが、シェルでは実行されない端末です。これは、端末装置が双方向であり、実際に端末がstdinとstdout(stderrも含む)に接続されているためです。ただし、端末には入力データを注入するための特別なioctlメソッドがあるため、ユーザーとしてルートシェルにコマンドを注入することはまだ可能です(単純なioctlのみが必要ですecho)。

一般的に、権限の上昇について不幸な事実を発見しました。ユーザーが何らかの方法でルートシェルに効果的に立ち上がることを許可した場合、そのユーザーが実行しているすべてのアプリケーションはその上昇を乱用する可能性があると想定する必要があります。 )。セキュリティ目的と目的のために、ユーザーはrootになります。たとえば、端末でスクリプトを実行している場合など、このstdinの挿入が不可能な場合でも、Xサーバーのキーボード挿入サポートを使用してグラフィックレベルで直接コマンドを送信できます。あるいは、gdbオープンパイプを使用してプロセスに接続して書き込みを注入することもできます。この脆弱性を解決する唯一の方法は、権限のないプロセスが操作できない(物理)ユーザーのセキュアI / Oチャネルにルートシェルを直接接続することです。これは、可用性を厳しく制限することなく実行することが困難である。

最後に注目すべき点は次のとおりです。通常、(匿名)パイプには読み出し終了と書き込み終了があります.つまり、2つの別々のファイル記述子があります。 stdinで子プロセスに渡された端は読み取り側であり、書き込み側は呼び出し元のプロセスに残りますGLib.spawn_async()。これは、子プロセスが実際にデータを自分自身bashに送り返すか、rootとして実行するためにstdinに書き込むことができないことを意味します(もちろん、プロセスは通常stdinに書き込めないとは言いませんが、stdinに書き込むことはできません。では動作しません)。しかし、他のプロセスのファイル記述子にアクセスするカーネル/procのメカニズムはこれを裏返します。 1つのプロセスがパイプの読み取りの終わりに開いているfdを持っていますが、書き込みのためにそのfdファイルを開こうとすると、/procカーネルは実際には同じパイプの最後に書き込みます。あるいは、/proc呼び出しの元のプロセスに対応する項目をGLib.spawn_async()参照するために開いているパイプの端を見つけ、その端に書き込むこともできます。これはこの特別なカーネルの動作には依存しません。これはほとんど好奇心です。ただし、セキュリティの問題は実際には変更されません。

関連情報