.NETアプリケーションの権限偽装方法

まぁ、今回はセキュリティというよりかは、どちらかというと開発ネタでの How To になるんだけど、ちょっと、前回の日記から間が空いてしまったので書いてみる。.NETのアプリケーションからリモートサーバのリソースを使いたいというときは多々あると思われる。たとえば、共有フォルダのファイルをいぢりたいとか...などなど。
そのときに問題になるのが権限回り。Webアクセスされた匿名ユーザは IUSER_XXXX だし、IISのワーカープロセスの実行権限は NETWROK SERVICE だ(IIS6以降の場合)。このWebアプリケーションの実行権限回りの話はややこしいのが、いずれにしても Guest 並みの低い権限でしかない。
このようなWebアプリケーションからリモートのリソースにアクセスしようとする場合、そのリソースをいぢれる権限に偽装して処理を行わなければならない。ただ単純にこれを実現しようと思ったら、実際のところこれはそれほど難しくない。
まず評価環境の説明から。あるサーバ(192.168.11.146)に共有フォルダ(test_work)を設定。その配下にサブフォルダ (subfolder) を設置。このフォルダにはいくつかのファイルがある。こんな感じ。




で、このサブフォルダには、192.168.11.146 のローカルユーザ(test)のみがアクセス可能となっている。こんな感じ。



では、Webアプリケーションからこの共有フォルダ内のファイル一覧を取得することをやってみよう。結論を言ってしまえば advapi32.dll の LogonUser() を使えばよい。というだけになってしまうんだけど、以外と動くサンプルが少ないみたいで使い方がイマイチ分かりづらいみたい。一応、以下がC#で書いた場合のサンプルコードになる。

  1: <%@ Page Language="C#" AutoEventWireup="true"  CodeFile="Default.aspx.cs" Inherits="_Default" %>
  2: <%@ Import Namespace="System.Security.Principal" %>
  3: <%@ Import Namespace="System.Runtime.InteropServices" %>
  4: <%@ Import Namespace="System.IO" %>
  5:
  6: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  7:
  8: <script runat="server">
  9:  [DllImport("advapi32.dll", SetLastError = true)]
 10:   static extern bool LogonUser(
 11:   string principal,
 12:   string authority,
 13:   string password,
 14:   LogonSessionType logonType,
 15:   LogonProvider logonProvider,
 16:   out IntPtr token);
 17:    
 18:  [DllImport("advapi32.dll", SetLastError = true)]
 19:   static extern bool DuplicateToken(
 20:   IntPtr existingTokenHandle,
 21:   int SECURITY_IMPERSONATION_LEVEL,
 22:   ref IntPtr duplicateTokenHandle);
 23:  
 24:  [DllImport("kernel32.dll", SetLastError = true)]
 25:   static extern bool CloseHandle(IntPtr handle);
 26:   enum LogonSessionType : uint
 27:   {
 28:    Interactive = 2,
 29:    Network,
 30:    Batch,
 31:    Service,
 32:    NetworkCleartext = 8,
 33:    NewCredentials
 34:   }
 35:   enum LogonProvider : uint
 36:   {
 37:     Default = 0,
 38:     WinNT35,
 39:     WinNT40,
 40:     WinNT50
 41:   }
 42:
 43:  protected void getbtn_Click(object sender, EventArgs e)
 44:  {
 45:    IntPtr token = IntPtr.Zero;
 46:    IntPtr duptoken = IntPtr.Zero;
 47:       
 48:    try
 49:    {
 50:      bool ret = LogonUser("test", "192.168.11.146", "testpass", LogonSessionType.NewCredentials, LogonProvider.Default, out token);
 51:      if (ret)
 52:      {
 53:        ret = DuplicateToken(token, 2, ref duptoken);
 54:        if (ret)
 55:        {
 56:            WindowsIdentity newIdentity = new WindowsIdentity(duptoken);
 57:            WindowsImpersonationContext impersonatedUser = newIdentity.Impersonate();
 58:            DirectoryInfo dirInfo = new DirectoryInfo(@"\\\\192.168.11.146\\test_work\\subfolder");
 59:            FileInfo[] files = dirInfo.GetFiles();
 60:            if (impersonatedUser != null)
 61:               impersonatedUser.Undo();
 62:
 63:            Response.Write("</p>Files: <br/>");
 64:
 65:            foreach (FileInfo file in files)
 66:            {
 67:               Response.Write(file.FullName + "<br/>");
 68:            }
 69:         }
 70:         else
 71:         {
 72:            Response.Write("</p>Failed: " +
 73:            Marshal.GetLastWin32Error().ToString());
 74:         }
 75:       }
 76:       else
 77:       {
 78:          Response.Write("</p>Failed: " +
 79:          Marshal.GetLastWin32Error().ToString());
 80:       }
 81:     }
 82:     catch
 83:     {
 84:     }
 85:     finally
 86:     {
 87:       if (token != IntPtr.Zero)
 88:         CloseHandle(token);
 89:     }
 90:   }
 91:   
 92:</script>
 93:
 94:<html xmlns="http://www.w3.org/1999/xhtml" >
 95: <head runat="server">
 96:   <title>Test</title>
 97: </head>
 98: <body>
 99:   <form id="form1" runat="server">
100:   <div>
101:    <asp:Button ID="getbtn" runat="server" OnClick="getbtn_Click" Text="Get file list" />
102:   </div>
103:  </form>
104: </body>
105:</html>


要は50行目の LogonUser() に以下を与えればよい。


1st arg ユーザ名
2nd arg ドメイン名あるいはコンピュータ名
3rd arg パスワード
4th arg ログオン動作タイプ
5th arg ログオンプロバイダ
6th arg トークンハンドル(out)


MSDN:http://msdn.microsoft.com/ja-jp/library/cc447468.aspx

(当然ながら、サーバ側でクレデンシャル情報を保持するのなら暗号化が必要になると思われる)

そして、上のコードでいえば、57行目から Undo() が呼ばれるまでの処理が偽装された権限で動作することになる。今回の場合はサーバの共有フォルダ内のファイル一覧を取得する処理だ。実行した結果が以下で、ファイルリストの取得ができた。



さて、この方法。今回はリモードの共有フォルダのファイル一覧を取得するだけで、偽装するユーザもリモードマシンの一般ユーザだったけど、当然、管理者権限などをもつユーザにも(クレデンシャルがわかっていれば)権限昇格できる。Webアプリケーションに対して、たとえ一部の処理だけとは言え高権限を持たせることは、セキュリティ上好ましくないという向きもある。
そこで、高権限を持たせる処理をWebアプリケーションから分離し、ATL COMインターフェースを持つNTサービスなどの外部プロセスに持たせ、そのプロセスを高権限で実行させ、そのプロセスへのアタッチ権のみをWebアプリケーションに与えるような方法もある。


その方法については.......いずれ機会があれば。