コース目次 / 第3章
コマンドインジェクション — 入力が、コマンドになる
入力をシェルコマンドに連結する処理に、区切り記号で別コマンドを注入します。原因を理解し、そもそもシェルを介さない・引数配列で渡すという直しで、再検証します。
次は、入力が OS のシェルコマンドとして解釈される穴です。SQL インジェクションと双子のような穴で、直しの発想もそっくり——データとコードを混ぜない。
法と倫理ゲート
手を動かす前に
このコースは攻撃の技術に触れます。実習してよい相手は、次の3つだけです。
- (a) 自分で作った練習用のやられ環境(このサイトが配る自作ターゲット)
- (b) あなた自身の許可された環境(自分の Thousand SKY・secret-notes-ctf など)
- (c) CTF 競技の中(規約の範囲で)
実在する第三者のシステムを許可なく触ることは、不正アクセス禁止法に触れます。 このゲートは、その線引きを毎回の入口で確かめるためのものです。詳しくは「セキュリティの法と倫理」へ。
標的 — シェルであいさつを作る(悪い例)
「入力された名前であいさつ文を作る」だけの機能を、わざとシェル経由で書いた例です。vuln-greet.js として保存します。
// vuln-greet.js — 学習用「わざと穴を空けた」あいさつ生成。127.0.0.1 だけで動かす。
// 狙う穴: コマンドインジェクション(入力をシェルコマンドに連結している)。
const http = require('node:http');
const { exec } = require('node:child_process');
const server = http.createServer((req, res) => {
const url = new URL(req.url, 'http://127.0.0.1:3400');
if (url.pathname === '/greet') {
const name = url.searchParams.get('name') || 'ゲスト';
// 【穴: コマンドインジェクション】入力を、そのままシェルに渡す文字列へ連結。
// exec はシェル経由で実行するので、区切り記号が「効いて」しまう。
exec('echo こんにちは、' + name, (err, stdout, stderr) => {
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('出力:\n' + stdout + (stderr ? 'エラー:\n' + stderr : ''));
});
return;
}
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('例: /greet?name=はると');
});
server.listen(3400, '127.0.0.1', () => console.log('やられあいさつ: http://127.0.0.1:3400 (Ctrl+C で停止)'));
node vuln-greet.js
正規の動きを確かめます。
http://127.0.0.1:3400/greet?name=はると
出力: こんにちは、はると と返ります。サーバは内部で echo こんにちは、はると というコマンドをシェルで実行しています。
攻撃 — 別のコマンドを、くっつける
シェルは、&& や ; のような区切り記号で複数のコマンドをつなげられます。名前欄にそれを混ぜたら?
http://127.0.0.1:3400/greet?name=はると && whoami
サーバがシェルに渡した文字列は、こう化けます:
echo こんにちは、はると && whoami
シェルは && を「前が成功したら次も実行」と解釈し、echo に続けて whoami(いまのユーザー名を表示するコマンド)を実行します。出力に、あいさつと一緒にあなたのユーザー名が出たはずです。whoami が動いたということは——任意のコマンドを、サーバ上で実行できるということ。これがコマンドインジェクションで、注入の中でも特に危険(サーバそのものを乗っ取られうる)です。
区切り記号は OS で少し違います。&& は Mac・Linux・Windows のどのシェルでも効きます。Mac・Linux ではさらに ;(前の成否に関係なく次を実行)も使えます:?name=はると; whoami。
修正 — そもそも、シェルを介さない
いちばん素直な直しは、「そもそもシェルを使わない」ことです。あいさつ文を作るのに、外部コマンドは要りませんでした。文字列は JavaScript の中で組み立てれば済みます。
// 直したあと(本命): 外部コマンドを使わず、文字列は自分で組み立てる。
const greeting = 'こんにちは、' + name;
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('出力:\n' + greeting);
「その処理に、本当にシェルが要るのか?」——コマンドインジェクションの多くは、要らないのにシェルを呼んでいたことが原因です。まず、外部コマンド自体を無くせないかを疑う。
では、どうしても外部プログラムを動かす必要があるとき(画像変換など)は? そのときも、SQL のプレースホルダと同じ発想を使います。
シェルを介す exec ではなく、execFile / spawn にプログラム名と引数の配列を渡す。たとえば execFile(‘convert’, [inputPath, outputPath]) のように。こうすると、引数はシェルを通らず、プログラムに「データ」として直接渡る。&& や ; を含んでいても、それは1個の引数の中身にすぎず、コマンドの区切りとして解釈されません。
データ(引数)とコード(コマンド)を、別のルートで渡す——SQL のプレースホルダと、まったく同じ原理です。
再検証 — もう一度、&& whoami を
「シェルを使わない」版に直してサーバを再起動し、さっきの攻撃をもう一度。
http://127.0.0.1:3400/greet?name=はると && whoami
今度は whoami は実行されず、出力: こんにちは、はると && whoami と、入力が丸ごと文字としてあいさつに入るだけです。&& はただの文字になりました。注入する先(シェル)が無くなったので、注入も成立しません。
持ち帰る一言
まずシェルを疑い、要るなら引数配列で。 コマンドインジェクションの直しは、①そもそもシェルを介さない、②必要なら execFile/spawn に引数配列で渡す。SQL のプレースホルダと同じ「データとコードを分ける」発想です。次は、入力がテンプレートの式になる SSTI と、パス越境の復習です。
こうなっていればOK
卒業まであと2章です。
この章はまだ完了していません。
保存できませんでした(プライベートブラウズ中かもしれません)。この端末に進捗は残りませんが、先へは進めます。