4

I currently am in the process of porting several Windows desktop applications to a single web site.

The current setup includes several SQL server backend databases, configured with Windows Authentication (SSPI) only, and every user/group/role has specific rights on specific objects. Which is convenient, because the application layer doesn't have to implement any access control.

I'd like to keep it the same way with the web server, an Apache on a Windows machine. But every connection to the databases is being made using Apache's account. That's understandable and expected, in fact Apache is deliberately given access to public data, to be able to deliver public content.

But in case a domain user logs in (the login process is already implemented) I'd like the Apache process that handles the request to impersonate that user, and thus act as them during the whole request.

At first, I tried php's fastcgi.impersonate trick, using IIS as the web server. But I eventually gave up on that, mainly because (1) we had to port to Apache anyway and (2) it was php-specific, and it turned out we should be targeting the web server as a whole...

So I redirected my search to Apache modules. Months of research gave no fruits, other than mod_auth_sspi and the like, which apparently isn't what I'm looking for (authentication and impersonation are two different things).

Finally I decided to make my own module. Most of the "101" examples I could find are written in C, but I managed to find 2-3 ones in Lazarus/FPC, which is what I've been using for quite a while now, but never for such a task.

I know I have to build a .dll project, I know (more or less) what units to use and I know functions like LogonUser() and ImpersonateLoggedOnUser() should be in my toolbox.

Has anyone done anything similar? Can anyone point me to the right direction?

An example would be appreciated, even if it's a simple proof of concept. This question is far from asking for a final, definitive solution.

geomagas
  • 3,230
  • 1
  • 17
  • 27
  • Unfortunately I now a little to nothing about SQL server and Apache modules. Sorry :-/ – NaN Nov 09 '13 at 01:38

1 Answers1

3

I eventually came up with the following:

library mod_winimpersonate;

{$mode objfpc}{$H+}

uses SysUtils, Windows, httpd, apr, Classes;

function DefaultHandler(r: Prequest_rec): Integer; cdecl;
  Var
    cookies:TStringList;
    logindata,username,password:String;
    p:Integer;
  begin
  RevertToSelf;
  cookies:=TStringList.Create;
  cookies.Delimiter:=';';
  cookies.DelimitedText:=apr_table_get(r^.headers_in,'COOKIE');
  logindata:=URLDecode(cookies.Values['WinImpersonate']);
  If Length(logindata)>0 then
    Begin
    p:=Pos(':',logindata);
    username:=LeftStr(logindata,p-1);
    password:=RightStr(logindata,Length(logindata)-p);
    ChangeLoggedInUser(username,password,'');
    End;
  Result:=DECLINED;
  end;

procedure RegisterHooks(p: Papr_pool_t); cdecl;
  begin
  ap_hook_handler(@DefaultHandler, nil, nil, APR_HOOK_REALLY_FIRST);
  end;

var
  TheModule: module;

exports TheModule name 'winimpersonate_module';

begin
FillChar(TheModule, sizeof(TheModule), 0);
STANDARD20_MODULE_STUFF(TheModule);
with TheModule do
  begin
  name := 'mod_winimpersonate.dll';
  register_hooks := @RegisterHooks;
  end;
end.

This is by no means a final solution, but it's a start. The logic is the following:

  • Revert to the Apache account. This is a must, in case we are using a recycled Apache thread that previously impersonate someone else.

  • Retrieve the user's credentials from a cookie named 'WinImpersonate', in the form of username:password. This needs more work (maybe encrypt the credentials, or store them in a safe(?) place up on the server or something even more secure)

  • Impersonate the user, with the help of the windows unit.

  • Return DECLINED, so Apache knows we didn't handle the request, and it should continue asking modules for the right handler.

There are many concerns to be addressed, in order for this to achieve a decent security level. Among others, the protection of the credentials, and the browser cache. But as I said, it's a start, as well as a proof of concept.

You'll notice that there are two utility functions missing from the above listing:

URLDecode decodes a url-encoded string:

// Convert URLEncoded string to utf8 string
function URLDecode(const s: String): String;
  var
    sAnsi: String;
    sUtf8: String;
    sWide: WideString;
    i, len: Cardinal;
    ESC: string[2];
    CharCode: integer;
    c: char;
  begin
  sAnsi := PChar(s);
  SetLength(sUtf8, Length(sAnsi));
  i := 1;
  len := 1;
  while (i <= Cardinal(Length(sAnsi))) do 
    begin
    if (sAnsi[i] <> '%') then 
      begin
      if (sAnsi[i] = '+') then c := ' '  else c := sAnsi[i];
      sUtf8[len] := c;
      Inc(len);
      end 
    else 
      begin
      Inc(i);
      ESC := Copy(sAnsi, i, 2);
      Inc(i, 1);
      try
        CharCode := StrToInt('$' + ESC);
        c := Char(CharCode);
        sUtf8[len] := c;
        Inc(len);
      except 
        end;
      end;
    Inc(i);
    end;
  Dec(len);
  SetLength(sUtf8, len);
  sWide := UTF8Decode(sUtf8);
  len := Length(sWide);
  Result := sWide;
  end;

ChangeLoggedInUser tries to login the user using the credentials supplied, and upon success it tries to impersonate him:

Function ChangeLoggedInUser(username, password, domain: string):Boolean;
  var
    creds: Cardinal;
  begin
  Result:=False;
  try
    if LogonUser(PChar(username)
        ,PChar(domain)
        ,PChar(password)
        ,LOGON32_LOGON_NETWORK_CLEARTEXT
        ,LOGON32_PROVIDER_DEFAULT
        ,creds
        ) then
      begin
      ImpersonateLoggedOnUser(creds);
      Result:=True;
      end;
  finally
    //wipe the memory for security
    FillChar(username,SizeOf(username),#0);
    FillChar(password,SizeOf(username),#0);
    FillChar(domain,SizeOf(username),#0);
    end;  //try-finally
  end;

Hope someone finds this useful. Comments are more than welcome.

geomagas
  • 3,230
  • 1
  • 17
  • 27