コース目次 / 第4章
SSTI とパストラバーサル — 式とパスの越境
ユーザー入力がテンプレートの式として評価されるSSTIを起こし、テンプレとデータを分ける直しを確かめます。capstoneで作ったパストラバーサルを注入の一種として復習します。
注入の最後は、入力がテンプレートの式として評価される SSTI(Server-Side Template Injection)と、入力がファイルパスの文法として解釈されるパストラバーサルです。どちらも、これまでと同じ形——データが、コードとして解釈される。
法と倫理ゲート
手を動かす前に
このコースは攻撃の技術に触れます。実習してよい相手は、次の3つだけです。
- (a) 自分で作った練習用のやられ環境(このサイトが配る自作ターゲット)
- (b) あなた自身の許可された環境(自分の Thousand SKY・secret-notes-ctf など)
- (c) CTF 競技の中(規約の範囲で)
実在する第三者のシステムを許可なく触ることは、不正アクセス禁止法に触れます。 このゲートは、その線引きを毎回の入口で確かめるためのものです。詳しくは「セキュリティの法と倫理」へ。
SSTI — 入力が、式になる
多くのテンプレートエンジンは、こんにちは、{{ name }} さん のような雛形に値を埋めて画面を作ります。その内部では、テンプレートを式として評価しています。ここにユーザー入力がテンプレート自体の一部として混ざると、入力が式として実行される——それが SSTI です。最小の再現を作ります。
// vuln-template.js — 学習用「わざと穴を空けた」テンプレ描画。127.0.0.1 だけで動かす。
// 狙う穴: SSTI(ユーザー入力を、評価されるテンプレートの中に埋め込んでいる)。
const http = require('node:http');
const server = http.createServer((req, res) => {
const url = new URL(req.url, 'http://127.0.0.1:3500');
const name = url.searchParams.get('name') || 'ゲスト';
// 【穴: SSTI】入力を、評価されるテンプレート文字列の中に連結している。
const template = 'こんにちは、' + name + ' さん';
let rendered;
try {
// テンプレートを式(テンプレートリテラル)として評価する = エンジンがやっていること。
rendered = Function('return `' + template + '`')();
} catch (e) {
rendered = 'エラー: ' + e.message;
}
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end(rendered);
});
server.listen(3500, '127.0.0.1', () => console.log('やられテンプレ: http://127.0.0.1:3500 (Ctrl+C で停止)'));
node vuln-template.js
正規の動き(http://127.0.0.1:3500/?name=はると → こんにちは、はると さん)を確かめたら、名前に式を入れます。
http://127.0.0.1:3500/?name=${7*7}
返ってきたのは こんにちは、49 さん。あなたが入れた $49 が、文字ではなく式として計算され、49 になった。これが SSTI です。7*7 が計算できるということは、サーバ上で任意の式を評価させられるということ。$v26.3.0 のような式でサーバ内部の情報を引き出したり、さらに踏み込めばコマンド実行に至ったりします(この教材では、危険な完成形の攻撃コードは載せません——式が評価されるという事実だけで、深刻さは十分に分かります)。
SSTI の修正 — テンプレは固定、入力はデータ
原因は、ユーザー入力がテンプレート(コード)の一部になっていること。直しは、これまでと同じ——テンプレートは固定し、入力はそこに“渡す”データとして扱う。
// 直したあと: テンプレートは固定の関数。入力は評価されず、値として連結されるだけ。
const render = (data) => 'こんにちは、' + data.name + ' さん';
res.end(render({ name }));
この name は、もう評価される式ではなく、ただ連結される値です。$49 を入れても こんにちは、$49 さん と文字で出るだけ。実際のテンプレートエンジンでも原則は同じ——ユーザー入力を、テンプレートの“ソース”に混ぜない。テンプレートに“渡す変数”として与える。データとコードを分ける、いつもの一手です。
再起動して ?name=${7*7} をもう一度開けば、今度は こんにちは、${7*7} さん。式は評価されません。
パストラバーサルも、注入の仲間
capstone(sec-make-target)で作ったパストラバーサルを、ここで捉え直します。あれは ../ を使って公開フォルダの外のファイルを読み出す穴でした。
実はこれも注入の一種です——入力が、ファイルパスの“文法”(../ という上位ディレクトリ指定)として解釈される。HTML・SQL・シェル・テンプレートと同じ形が、ファイルパスの層で起きているだけ。
だから直しも同じ発想でした:「.. という文字を消す」(ブラックリスト)ではなく、「連結後の実パスが、公開フォルダの中に収まっているものだけ許す」(許可リスト)。path.resolve で解決してから基準ディレクトリ配下かを確かめる、あの直しです。
未見の人は capstone の「自分だけの脆弱ターゲットを作る」で、実際に作って・攻撃して・直せます。
持ち帰る一言
SSTI も、テンプレとデータを分ければ止まる。パストラバーサルも注入の仲間。 ユーザー入力をテンプレートのソースに混ぜず、渡す変数として与える。パスは許可リストで基準の中に閉じる。層は違っても、直しはいつも「データとコードを分ける」。次はまとめて、五つの穴を一つの原則に畳みます。
こうなっていればOK
卒業まであと1章です。
この章はまだ完了していません。
保存できませんでした(プライベートブラウズ中かもしれません)。この端末に進捗は残りませんが、先へは進めます。