Pythonのsubprocessモジュールの使い方を間違えるとOSコマンドインジェクションの脆弱性を作ってしまうことになります。
HackTheBox SAUの攻略で利用したMaltrail v0.53を例に脆弱性のソースコードを見ていきたいと思います。
Hack The BoxのWriteup
これからやること
この記事ではHackTheBoxの攻略後、root権限を取得した状態でマシン内の脆弱性の箇所(=ソースコード)を見ようと思います。
今回学べること
- Pythonのsubprocessモジュールを使う時のセキュリティー上の注意点
- PythonでOSコマンドを実行できるメソッド
Maltrialのソースコードの公開場所
今回の対象バージョン(Maltrial v0.53)のソースコードはかGithubに公開されています。
脆弱性があるソースファイルの特定
今回の脆弱性は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 — サブプロセス管理

解決策
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もやってみてください。





