The Man Who Fell From The Wrong Side Of The Sky:2019年5月27日分

2019/5/27(Mon)

[Windows] PowerShellで生活するために - New-TemporaryFileコマンドレット編(その2)

前回はNew-TemporaryFileコマンドレットを紹介し、これで一時ディレクトリに作業用ファイルは作れるけど作業用ディレクトリは掘れないやんけ!ってお話。

無いならNew-TemporaryDirectoryを作ればいいじゃない、でもどうやってセキュリティを担保すればいい?

@予測不能な名前でディレクトリを作成する

なんか難しそうだなーと思うかもしれないけど実はそう大したもんじゃない。

予測可能な名前というのは

  • 連番
    workdir_0001
    workdir_0002
    ...
    
  • 現在時刻
    workdir_2019-05-24-00:00:00:000
    workdir_2019-05-25-23:59:59:999
    …
    

みたいな攻撃者が容易に次に作成されるファイルの名前を、金曜日の海軍の夕食メニューは?レベルで的中させられるような法則性を避けろって程度の話。

なので前回の失敗例でとりあげたSystem.IO.Path::GetRandomFileNameだけども、この条件であれば満たしてはいるのだ。

$workdir = [System.IO.Path]::GetRandomFileName()
Write-Host $workdir

これを実行すると

23zgemoa.mqf

と乱数から生成した8.3形式のファイル名(英数小文字)を返すので、可能性は36^11通りとなる。

あるいはInstallShieldなんかのインストーラーのようにGUIDを使ってもいい、.Net Frameworkの GUID構造体を使えばかんたん。

$workdir =  [Guid]::NewGuid().ToString("B")
Write-Host $workdir

これを実行すると

{efd60a90-6ba7-489d-af7b-39c755cb7f87}

と128ビットのうちバリアントとバージョンのためビットを除いた122ビットを乱数で埋め、さらに16進を[0-9a-f]の文字に置換えた文字列を出力するので、可能性は2^122通りになる。

あとは重篤なパラノイアを患っていると乱数が本当に乱数かどうか心配になって夜も眠れなくなるけどさすがにそこまでは面倒みきれん、ハードウェア乱数生成器でも買ってください。

なお前回ちらっと触れたmktemp(3)というタッパーウェアにあった古い関数は「XXXXX」で表される6桁のテンプレート部分を

  • プロセスID
  • アルファベット小文字から1文字

で埋めるだけなんですな。

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>

int
main(void)
{
	char buf[BUFSIZ];
	snprintf(buf, sizeof(buf), "/tmp/%s.XXXXXX", getprogname());
	printf("current pid: %d\n", getpid());
	printf("mktemp(3) generated: %s\n", mktemp(buf));
}

これを実行すると

$ ./unko
current pid: 11451
mktemp(3) generated: /tmp/unko.11451a

という結果になる、プロセスIDなんぞ余裕でバレるので実質26通りの組合わせしか無いわけで総当り攻撃も余裕。

ちなみに改良されたmkstemp(3)とmkdtemp(3)は乱数を元に62^6通り、さらにテンプレートのXXXXXXを6桁よりも増やして更に可能性を増やすこともできる。

ただ注意が必要なのは、一部のlibcにおけるmkstemp(3)とmkdtemp(3)の実装は、mktemp(3)とまったく同じ生成規則な実質26通りのままで、総当り攻撃が有効なものもある(N6以前とかね)。 とはいえファイル名が予測可能だったとしてもその他の条件で攻撃は防げてるはずなので、ただちに危険というわけではないから無視してもいい。

@適切なアクセス権限でディレクトリを作成する

これPOSIX:2008でmktemp(3)が仕様から削除に至った脆弱性としては予測可能性よりこっちの方が重篤なのだ、こいつは名前を生成するだけの関数なので

char path[PATH_MAX];
snprintf(path, sizeof(buf), "/tmp/%s.XXXXXX", getprogname());
mktemp(path)
mkdir(path, 0777)

のようにプログラマの無知によって不適切なアクセス権限でファイルやディレクトリを作成することをどうやっても防げないのも理由の一つ。 mkstemp(3)やmkdtemp(3)は内部で

  • ファイルなら0600
  • ディレクトリなら0700

で作成したものを返すので、たとえ予測可能な名前であっても権限無ければ攻撃できないのだ。

140                 if (doopen) {
141                         if ((*doopen =
142                             open(path, O_CREAT | O_EXCL | O_RDWR, 0600)) >= 0)
...
146                 } else if (domkdir) {
147                         if (mkdir(path, 0700) >= 0)
...

ではタッパーウェアではなくWindows、それもPowerShellの場合はどうやって適切なアクセス権限でディレクトリを作成すればいいのか。

ディレクトリを作成するコマンドレットは

New-Item -Path 親ディレクトリ -Name ディレクトリ名 -ItemType Directory

とNew-Itemコマンドレットを使用する(mkdirもコマンドでなくこいつのaliasになる)のだけど、こいつのソース Microsoft.PowerShell.Commands.FileSystemProviderを確認すると、実際に実行されるのは.Net FrameworkのSystem.IO.Directory::CreateDirectoryとなっている。

  11 using System.IO;
...
2196         protected override void NewItem(
2197             string path,
2198             string type,
2199             object value)
2200         {
...
2224             itemType = GetItemType(type);
2225
2226             if (itemType == ItemType.Directory)
2227             {
2228                 CreateDirectory(path, true);
2229             }
...
2697         private void CreateDirectory(string path, bool streamOutput)
2698         {
...
2738                     var result = Directory.CreateDirectory(Path.Combine(parentPath, childName));

そんで System.IO.Directoryクラスのドキュメントを確認するとCreateDirectoryには

  • CreateDirectory(String) … Creates all directories and subdirectories in the specified path unless they already exist.
  • CreateDirectory(String, DirectorySecurity) … Creates all the directories in the specified path, unless the already exist, applying the specified Windows security.

という2種類の狂い咲きオーバーロードがある、それぞれの違いは

  • 前者は作成するディレクトリに対してデフォルトのアクセス権を設定する
  • 後者は明示的にアクセス権を設定する

という違いがあるのだけど、New-Item -ItemType Directoryは前者しか呼んでいないので明示的にアクセス権を設定することができないのだ、なんてこったい。

ただこれもまた直ちに影響というわけではない、Windows NT系の場合一時ディレクトリは各ユーザー毎にご用意されるいわゆる「per user tmp」というやつで、環境変数TMP(あるいはTEMP)には

  • %USERPROFILE%\Local Settings\Temp … XP以前
  • %USERPROFILE%\App\Local\Temp … Vista以降

以下が指定されている、そしてこれとは別にSYSTEMユーザーなどが使う環境変数TMP(あるいはTEMP)には

  • %SYSTEMROOT%\Temp

以下が指定されている。

前者の一時ディレクトリを含む%USERPROFILE%(例えばC:\Users\ユーザー名)以下はデフォルトでは

  • SYSTEMユーザ
  • Administratorsグループ
  • 当該ユーザ

以外にはアクセス権限が無いので、CreateDirectory(String)を使って親ディレクトリのアクセス権限を引継いだまま作成してれば、いちいちSet-Acl呼ばなくても攻撃者からは参照できないので安全だと主張もできなくはない。

ただしSYSTEMユーザなどが使う%SYSTEMROOT%\Temp以下は上記に加えて

  • Creator Ownerユーザ(ファイル作成者)
  • Usersグループ

にもファイルやディレクトリ作成と読み書きが許可されていて(若干の制限はある)、セキュリティは緩めなのよね。

なので%SYSTEMROOT%\Tempを使うユーザー権限で動作するスクリプトを書く場合、ファイル名の予測不可能性が破られた時の事を考えてきっちりアクセス権を設定しておかないとアウトなのだ。

それに環境変数なんていくらでも汚染できるので、TMPあるいはTEMPがWindows 9x系までの頃の流儀であるC:\Tempに書き換えられてたり、SSDの寿命を延ばすためD:\Tempとか自分で掘ってそっち使うようにレジストリ含めて変更してる人もいるしな…そういう人アクセス権限とその継承を正しく設定してるとは思えん。

世の中のPowerShell使いたちはどうしてるのか、我々はその謎を解き明かすべくStackOverflowやQiitaといった未開人の住まうジャングルの奥地へと向かった、そこで目にした光景は

$workdir = [System.IO.Path]::GetTempPath() + [System.IO.Path]::GetRandomFileName()
New-Item -Path $workdir -ItemType Directory
$alc = Get-Acl -Path $workdir
$alc.SetAccessRuleProtection($true, $false)
$alc.Access | ForEach-Object {
	$alc.RemoveAccessRule($_)
}
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule (
	[System.Security.Principal.WindowsIdentity]::GetCurrent().Name,
	[System.Security.AccessControl.FileSystemRights]::FullControl,
	([System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor [System.Security.AccessControl.InheritanceFlags]::ObjectInherit),
	[System.Security.AccessControl.PropagationFlags]::NoPropagateInherit,
	[System.Security.AccessControl.AccessControlType]::Allow
)
$alc.AddAccessRule($rule)
# New-Item -> Set-Acl is not Atomic ops, there's TOCTTOU race condition security problem.
Set-Acl -Path $workdir -AclObject $acl
(Get-Acl -Path $workdir).Access
...
Remove-Item -Force -Path $workdir

とディレクトリを作成した後に、Set-Aclコマンドレット(あるいはicalcsコマンド)でアクセス権限を設定し直すというコード例であった。

賢明なプログラマならお気づきだろうけど、New-ItemとSet-Aclに操作が分割されているのでアトミックではない、よってこのわずかな時間を利用してシンボリックリンク攻撃などを成功させてしまう可能性があるのだよね。 まぁアクセス制限を緩める方向性(例えばファイル共有用に誰でも読み書きできるようにするとか)ならええけどさあ…

なのでアトミックに作業用ディレクトリを掘るのであれば内部的にCreateFile(String)を使ってるNew-Itemは禁止、オーバーロードのCreateFile(String, DirectorySecurity)の方を呼ぶことでディレクトリ作成とアクセス権設定を同時にやらんとアカン。

$workdir = [System.IO.Path]::GetTempPath() + [System.IO.Path]::GetRandomFileName()
$acl = New-Object System.Security.AccessControl.DirectorySecurity
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule (
	[System.Security.Principal.WindowsIdentity]::GetCurrent().Name,
	[System.Security.AccessControl.FileSystemRights]::FullControl,
	([System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor [System.Security.AccessControl.InheritanceFlags]::ObjectInherit),
	[System.Security.AccessControl.PropagationFlags]::NoPropagateInherit,
	[System.Security.AccessControl.AccessControlType]::Allow
)
$acl.AddAccessRule($rule)
# Atomic ops, there's no TOCTTOU race condition.
[System.IO.Directory]::CreateDirectory($workdir, $acl)
(Get-Acl -Path $workdir).Access
...
Remove-Item -Force -Path $workdir

うーんこの、WindowsのACL複雑すぎんよー…というかCreateDirectory(String, DirectorySecurity)がアトミック操作なのか心配になってきたゾ。

@次回

残りの「競合状態の回避」をどうやって実装するか、そしてNew-TemporaryFileに対してNew-TemporaryDirectoryとでも名付ければいいのか、コマンドレットっぽく使える関数を作るところまで書けたらいいですね…(かなり飽きた)。