Python subprocessモジュールで脆弱性を作り込む

OSコマンドインジェクション

Pythonのsubprocessモジュールの使い方を間違えるとOSコマンドインジェクションの脆弱性を作ってしまうことになります。
HackTheBox SAUの攻略で利用したMaltrail v0.53を例に脆弱性のソースコードを見ていきたいと思います。

Hack The BoxのWriteup

これからやること

この記事ではHackTheBoxの攻略後、root権限を取得した状態でマシン内の脆弱性の箇所(=ソースコード)を見ようと思います。

今回学べること

  • Pythonのsubprocessモジュールを使う時のセキュリティー上の注意点
  • PythonでOSコマンドを実行できるメソッド

Maltrialのソースコードの公開場所

今回の対象バージョン(Maltrial v0.53)のソースコードはかGithubに公開されています。

Release 0.53 ?? stamparm/maltrail
Start-of-month release

脆弱性があるソースファイルの特定

今回の脆弱性はusernameというHTTPの内容を解析する際の処理に問題がありました。

そのためusernameを処理しているところをgrepで探してみます。

root@sau:/opt/maltrail# grep username */*
grep username */*
core/httpd.py:                retval = AttribDict({"username": "?"})
core/httpd.py:            if params.get("username") and params.get("hash") and params.get("nonce"):
core/httpd.py:                        username, stored_hash, uid, netfilter = entry.split(':')
core/httpd.py:                        if username == params.get("username"):
core/httpd.py:                SESSIONS[session_id] = AttribDict({"username": username, "uid": uid, "netfilters": netfilters, "mask_custom": config.ENABLE_MASK_CUSTOM and uid >= 1000, "expiration": expiration, "client_ip": self.client_address[0]})
core/httpd.py:                    subprocess.check_output("logger -p auth.info -t \"%s[%d]\" \"%s password for %s from %s port %s\"" % (NAME.lower(), os.getpid(), "Accepted" if valid else "Failed", params.get("username"), self.client_address[0], self.client_address[1]), stderr=subprocess.STDOUT, shell=True)
core/httpd.py:            username = session.username if session else ""
core/httpd.py:            return username

脆弱性の箇所を確認

core/httpd.pyの398行目でsubprocess.check_output()の引数にparams.get(“username”)として渡している箇所がありました。

subprocess.check_output("logger -p auth.info -t \"%s[%d]\" \"%s password for %s from %s port %s\"" % (NAME.lower(), os.getpid(), "Accepted" if valid else "Failed", params.get("username"), self.client_address[0], self.client_address[1]), stderr=subprocess.STDOUT, shell=True)

なぜコマンドインジェクションが発生するのか

shell=True により、subprocess のコマンドが シェルの文法で解釈 されます。
そのため、params.get("username") の値に セミコロン (;) やパイプ (|)、バッククオート (\``)、$()` などが含まれていると、それが新たな OS コマンドとして実行されてしまう可能性があります。

上記ソースコードの各ブロックの意味を説明いたします。

ソース意味
subprocess.check_output()指定した内容を実行して標準出力を取得するメソッド
“logger 〜 self.client_address[1])実行するOSコマンドを可変引数で動的に作成
stderr=subprocess.STDOUT実行時の標準エラーも標準出力に統合する
shell=Trueコマンド文字列がシェル経由で実行される

subprocess.check_output()の使い方

1. 簡単なコマンドの実行
import subprocess

output = subprocess.check_output(["echo", "Hello, world!"])
print(output.decode()) # "Hello, world!"
  • ["echo", "Hello, world!"] のようにリスト形式でコマンドを渡すことで、shell=False(デフォルト)となり、安全に実行される。
  • check_output()標準出力を取得して返す ため、print(output.decode()) すると "Hello, world!" が表示される。

2. shell=True を使う(非推奨)
output = subprocess.check_output("echo Hello, world!", shell=True)
print(output.decode()) # "Hello, world!"
  • shell=True を指定すると、コマンド文字列が シェルを通じて解釈 される。
  • セキュリティリスクが高いため、ユーザー入力を含む場合は避けるべき

Python subprocess — サブプロセス管理

subprocess --- サブプロセス管理
ソースコード: Lib/subprocess.py subprocess モジュールは新しいプロセスの開始、入力/出力/エラーパイプの接続、リターンコードの取得を可能とします。このモジュールは以下の古いモジュールや関数を置き換えることを目的...

解決策

shell=False を指定すると、リストで渡した各引数が そのまま文字列として扱われる ため、コマンドインジェクションが発生しません。
例えば params.get("username")john; id を入れても、それは単なる文字列として扱われ、logger コマンドに渡されるため、安全です。

最新版のソースを確認

最新版のhttpd.pyを確認すると解決策に記載したとおりshell=Falseに修正されています。

※クリックするとソースコードが開きます

Python で OS コマンドを実行できる主な方法

Python には subprocess 以外にも OSコマンドを実行できる モジュールや関数がいくつかあります。

1. os.system(command) (⚠️ 危険)

pythonコピーする編集するimport os
os.system("ls -l")  # 外部コマンドを実行
  • リスク:
    • shell=True 相当の動作で コマンドインジェクションの危険性 あり!
    • 標準出力の取得が困難(結果を変数に格納できない)。
  • 対策: 基本的に subprocess.run() を使うべき

2. subprocess モジュール(推奨 ✅)

  • 安全な方法:pythonコピーする編集するimport subprocess subprocess.run(["ls", "-l"]) # shell=False にすることで安全
  • 危険な方法(⚠️ 非推奨):pythonコピーする編集するsubprocess.run("ls -l", shell=True) # コマンドインジェクションのリスクあり
  • リスク:
    • shell=True を使うと ユーザー入力がそのまま解釈される ため危険。
    • shell=False を使えばリスクを回避可能。

3. subprocess.Popen()

  • より細かくプロセスを制御したい場合に使用pythonコピーする編集するprocess = subprocess.Popen(["ls", "-l"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = process.communicate() print(stdout.decode())
  • リスク:
    • shell=True を使うと インジェクションリスクあり
    • 安全な使い方をしないと ゾンビプロセスが発生する 可能性がある。

4. subprocess.call()

  • subprocess.run() の簡易版(Python 3.5 以降は subprocess.run() を推奨)pythonコピーする編集するimport subprocess subprocess.call(["ls", "-l"]) # 推奨 subprocess.call("ls -l", shell=True) # ⚠️ 非推奨
  • リスク:
    • shell=True で使うと インジェクションの危険 あり。

5. subprocess.check_output()

  • コマンドの出力を取得するpythonコピーする編集するimport subprocess output = subprocess.check_output(["ls", "-l"]) print(output.decode()) # 出力を取得して処理できる
  • リスク:
    • shell=True を使うと コマンドインジェクションの危険あり

6. commands.getoutput()(Python 2 のみ ⚠️ 非推奨)

  • Python 2 の機能(Python 3 では subprocess を使用)pythonコピーする編集するimport commands output = commands.getoutput("ls -l") # Python 3 では使えない print(output)
  • リスク:
    • shell=True 相当なので インジェクションのリスク あり。
    • Python 2 自体が非推奨 なので、この方法は使わない。

7. os.popen()(⚠️ 非推奨)

  • 簡単に出力を取得できるが、subprocess の方が安全pythonコピーする編集するimport os output = os.popen("ls -l").read() print(output)
  • リスク:
    • shell=True 相当の動作なので、コマンドインジェクションのリスクあり
    • subprocess.Popen() を使うべき

8. sh モジュール(外部ライブラリ)

  • sh モジュールを使うと よりPythonらしく コマンドを扱える。pythonコピーする編集するimport sh print(sh.ls("-l"))
  • リスク:
    • sh モジュール自体は 安全な作り だが、ユーザー入力をそのまま渡さないこと が重要。

9. pty.spawn()(疑似ターミナル実行)

  • 対話的なコマンドを実行する場合に使用pythonコピーする編集するimport pty pty.spawn("/bin/bash")
  • リスク:
    • bash を起動するため、悪用されると シェルを奪われる可能性がある
    • セキュリティ的には推奨されない

まとめ

方法安全性コメント
subprocess.run(["cmd"])✅ 安全推奨方法shell=False で使う)
subprocess.Popen(["cmd"])✅ 安全プロセス制御が必要な場合に推奨
subprocess.check_output(["cmd"])✅ 安全出力を取得する場合に推奨
subprocess.run("cmd", shell=True)⚠️ 危険コマンドインジェクションのリスク
os.system("cmd")❌ 非推奨shell=True 相当で危険
os.popen("cmd")❌ 非推奨shell=True 相当で危険
commands.getoutput("cmd")❌ 非推奨Python 2 限定 & 危険
sh.cmd()⚠️ 注意sh モジュールは安全だが、入力チェックが必要
pty.spawn("/bin/bash")⚠️ 危険シェルを開くためリスク大

結論:Python で OS コマンドを実行する際のポイント

推奨される方法

  • subprocess.run(["cmd"]) (安全にコマンドを実行する)
  • subprocess.Popen()(プロセス制御が必要なら)
  • subprocess.check_output()(出力を取得するなら)

⚠️ 注意すべき方法

  • subprocess.run("cmd", shell=True)shell=True は極力避ける)
  • sh モジュール(安全だが、入力チェック必須)

非推奨な方法

  • os.system("cmd")(常に shell=True で危険)
  • os.popen("cmd")(非推奨)
  • commands.getoutput("cmd")(Python 2 限定で非推奨)

ペンテスト視点での考察

  • os.system()subprocess.run(..., shell=True) が使われていると コマンドインジェクションの可能性 があるので、テスト時に入力値を工夫してみるといいですね。
  • pty.spawn() などを悪用して 疑似ターミナルを開く と、システムの制御を奪える可能性があるので、脆弱性調査の際にチェックすると面白い発見があるかもしれません。

subprocessの脆弱性がSAUというマシンに実装されています。
興味を持った方はHackTheBoxもやってみてください。

Hack The Box: The #1 Cybersecurity Performance Center
HTB is the leading Cybersecurity Performance Center for advanced frontline teams to aspiring security professionals & st...
タイトルとURLをコピーしました