PowerShell Web Access (PSWA) mit MultiOTP sichern

PSWA ist ein relativ mächtiges Werkzeug, da liegt es auf der Hand den Zugang dazu mit 2FA zu sichern – hier am Beispiel MultiOTP, eine Open Source Geschichte.

Voraussetzung: MultiOTP ist lokal am PSWA Server eingerichtet (was im Normalfall nur Copy/Paste der Windows Version – mit entsprechender .ini Datei – bedeutet).
Vorsicht: PSWA Application Pool Account muss Schreibrechte auf das MultiOTP Verzeichnis haben wenn Logging enabled ist!

Umsetzung: PSWA mißbraucht Forms based Authentication ein wenig, daher muss man ein wenig kreativ eingreifen (PSWA spielt sich in C:\Windows\Web\PowerShellWebAccess\wwwroot ab): FBA Login Url ist Default.aspx welche aber leer ist und nur dazu dient auf die richtige Sprache umzuleiten (en-US in unserem Fall) – dort wird dann je nachdem ob eine Session da ist console.aspx oder logon.aspx (mit User/PW/Zielgerät etc. aufgerufen).

Wir kopieren daher im en-US Verzeichnis das Original logon.aspx auf logonMS.aspx und erzeugen ein neues logon.aspx. Die logon.aspx ist durch die lokale web.config für alle Benutzer freigegeben, unsere logonMS.aspx nicht – daher kann man unsere neue logon.aspx nicht umgehen weil durch die FBA Mechanik sofort auf Default.aspx und damit auf logon.aspx umgeleitet wird. Unsere neue logon.aspx leitet bei erfolgreicher OTP Authentifizerung auf logonMS.aspx weiter (konfigurierbar, s.u.).

Den Code für die Seite (OTPAuth.dll, Source: OTPAuth-Source) laden wir im übergeordneten \bin ab.

Die neue logon.aspx Seite kann über vier Parameter gesteuert werden die in die web.config direkt in …\wwwroot im Abschnitt appSettings einzufügen sind:

<add key="MultiOTPPathAndName" value="C:\\Pfad\\zu\\multiotp\\multiotp.exe" />
<add key="OTPUsernameRegex" value="^[a-zA-Z0-9]*$"/>
<add key="OTPRegex" value="^[0-9]{6}$"/>
<add key="TargetPageOnSuccessfullAuthentication" value="logonMS.aspx"/>

Und das wars schon.

Ablauf:
1) User surft Seite an, egal wo. FBA leitet auf Default.aspx weiter.
2) Default.aspx leitet auf logon.aspx im Sprachverzeichnis weiter (unsere OTP Seite).
3) Bei erfolgreicher OTP Authentifizerung leitet unser logon.aspx gemäß Konfiguration auf logonMS.aspx weiter.
4) PSWA wie ohne OTP.

Wie üblich der Warnhinweis: Das alles ist natürlich weit jenseits jeglichen Supports und MS Support wird (zurecht) das Weite suchen wenn er/sie/es sowas sieht und ein Problem daraus entstanden ist….zusätzlich sollte man die geänderten Dateien auch sichern, obwohl die Seiten seit 2012 praktisch unverändert geblieben sind weiß man nie was der nächste CU so bringt 😀

IIS: Windows Auth aber keine Impersonation/Delegation

Szenario:
IIS >= 7.5, ASP oder ASP.NET Applikation, Windows Authentication (damit man in der Applikation weiß wer da daher kommt) aber hinten raus soll beim Zugriff auf weitere (Remote-)Resourcen die Identität des Application Pools und nicht der authentifizierte Benutzer verwendet werden.

Hintergrund:
a) Minimalrechte für Application Pool Identity und nicht die Notwendigkeit die authentifizierten Benutzer auf die Files und andere Resourcen die die WebApp braucht zu berechtigen.
b) Application Pool Identity hat Rechte auf Resourcen die die authentifizierten Benutzer nicht haben – die Applikation muss dann natürlich checken ob der aufrufende Benutzer überhaupt das darf was er da gerne hätte.

Problem:
IIS nutzt per Default den authentifizierten Benutzer für die Zugriffe (egal ob lokal oder remote) – was bei Remotezugriffen zum double-hop-Problem führt und damit auf dem direkten Weg in die Kerberos Hölle (oder der aufrufende Benutzer hat überhaupt nicht die notwendigen Rechte auf der Remoteresource, selbst wenn Kerberos Delegation mal ausnahmsweise funktionieren würde).

Lösung:
a) Application Pool Identity setzen.
b) Windows Auth einschalten (alle anderen ab)
c) URL Authorization Regeln für die Benutzer erstellen die dürfen erstellen
OPTIONAL: Über ServerVariables Collection kann mit Item „LOGON_USER“ auf den authentifizierten Benutzer zugegriffen werden (nicht „REMOTE_USER“ – der ist fix mit der AppPool Identity belegt) – und damit können in der Applikation dann Zugriffsentscheidungen getroffen werden. Der ASP.NET Role Provider funktioniert NICHT!
d) Im IIS Manager/Site/Configuration Editor unter system.webServer/serverRuntime den Eintrag authenticatedUserOverride auf "UseWorkerProcessUser" umstellen

Credits: Scott Forsyth’s Blog

VEDATAMODEL.EDB von App-Leichen befreien

Wenn man im Enterpriseumfeld nach einem Windows 10 Update wieder all die schönen Nicht-Enterprise Apps hat will man die gerne löschen (Remove-AppxPackage bzw. Remove-AppxProvisionedPackage) – kann man auch, nur bleiben dann bei allen Benutzern die auf der Kiste schon mal angemeldet waren Leichen im Startmenü. Warum? Das weiß offenbar nicht mal Microsoft wenn man die hilflosen Antworten in diversen Foren liest.

Wers wirklich Hardcore will kann in die Standard ESENT DB die die Grundlage für das neue (tolle) Startmenü bildet reinfummeln und die nicht gewollten Apps rauslöschen (Api ist in Microsoft.Isam.Esent.Interop Namespace von ESENT Managed Interface):

JET_INSTANCE oInstance;
JET_SESID oSession;
JET_DBID oDB;
JET_TABLEID oTable;
JET_COLUMNDEF oColDefAppRef, oColDefID;
string sTable = "Tile";
string sAppsToDelete = "WindowsFeedbackHub|Messaging|WindowsReadingList|CommsPhone|Microsoft.SkypeApp|OneNote|ConnectivityStore|Microsoft.MicrosoftOfficeHub|Microsoft.Office.Sway|Microsoft.MicrosoftSolitaireCollection|Microsoft.XboxApp|Microsoft.Getstarted|Microsoft.BingSports|Microsoft.BingNews|Microsoft.BingFinance|Microsoft.3DBuilder|Microsoft.OneConnect|Microsoft.3DBuilder|microsoft.windowscommunicationsapps|Microsoft.BingFoodAndDrink|Microsoft.BingHealthAndFitness|Microsoft.BingTravel|Windows.ContactSupport"
string sIDstoDelete = "PreInstalled.DefaultStartLayout";
string sAppRef;
string sID;
string sDB=@"C:\Users\whatever\AppData\Local\TileDataLayer\Database\vedatamodel.edb";

Console.WriteLine($"Doing {sDB}");

Api.JetCreateInstance(out oInstance, "instance");
Api.JetSetSystemParameter(oInstance, JET_SESID.Nil, JET_param.CircularLog, 1, null);
Api.JetInit(ref oInstance);
Api.JetBeginSession(oInstance, out oSession, null, null);


Api.JetAttachDatabase(oSession, sDB, AttachDatabaseGrbit.None);
Api.JetOpenDatabase(oSession, sDB, null, out oDB, OpenDatabaseGrbit.None);

Api.JetOpenTable(oSession, oDB, sTable, null, 0, OpenTableGrbit.None, out oTable);
Api.JetGetColumnInfo(oSession, oDB, sTable, "APPREF", out oColDefAppRef);
Api.JetGetColumnInfo(oSession, oDB, sTable, "ID", out oColDefID);

if (!Api.TryMoveFirst(oSession, oTable))
{
   Console.WriteLine("   ===> Tile table is empty");
}
else
{
   do
   {
      sAppRef = Api.RetrieveColumnAsString(oSession, oTable, oColDefAppRef.columnid);
      sID = Api.RetrieveColumnAsString(oSession, oTable, oColDefID.columnid);
      if (Regex.Match(sAppRef, sAppsToDelete, RegexOptions.IgnoreCase).Success || Regex.Match(sID, sIDstoDelete, RegexOptions.IgnoreCase).Success)
      {
         Console.WriteLine($"    ===> DELETING {sID}/{sAppRef}");
         Api.JetDelete(oSession, oTable);
      }
   }
   while (Api.TryMoveNext(oSession, oTable));
}

Api.JetCloseTable(oSession, oTable);
Api.JetCloseDatabase(oSession, oDB, CloseDatabaseGrbit.None);
Api.JetDetachDatabase(oSession, sDB);
Api.JetEndSession(oSession, EndSessionGrbit.None);
Api.JetTerm(oInstance);

Btw: Mit ESEDatabaseViewer kann man sich die Datenbank auch ansehen, steht zwar nicht allzuviel Sinnvolles/Lesbares drin aber he, manchmal will man es genau wissen 😀

 

Update: Ab Windows 10 1703 speichert Microsoft die Sachen wo anders (%LOCALAPPDATA%\Microsoft\Windows\Caches, CloudStore und was weiß der Teufel sonst noch wo)….schaut fast wie eine SQLite3 DB aus….

Spaß mit Zertifikaten am TV

Also drehen sie DVB-T ab. Klar, war ja zu einfach mit Kabel rein und gut ists. Klar steht bei LG dass das TV Gerät DVB-T/T2 kann. Klar steht viel weiter unten ganz klein „nicht alle Modelle“. Klar war unseres so eines welches nur T und kein T2 kann.

Also ein SAT CI+ Irdeto CAM mit ORF Zertifizierung gekauft (um den halben Preis den das ORF-Teil kostet) und reingestopft – was kommt?

Irdeto Access
CI+ Error Host Zertifikat ungültig
abgelaufen, bitten wählen sie den DTV-
Service -16

Nach kurzer Recherche erkannt dass der das wörtlich meint – also auf Kanal 16 (egal was dort programmiert ist) und er beginnt mit der Authentifizierung. Bleibt bei 3/5 hängen.

Weiter recherchiert – und diese Seite gefunden (offensichtlich arbeiten da eh _alle_ CAMs mit dem Chipsatz oder wie man das immer nennt was da drinsteckt) – und was lesen wir da?

Q: If the authentication failed just after step 3/5 or pop up message of “CI+ ERROR: Host Certificate Invalid-Expired, Please tune to DTV service – 16”, what can be done?

A: Please check whether the signal connection is OK, and check whether the TV time setting is the current time. If the TV has set to current time, that means the CI+ certification has something wrong, please contact TV service center.

Holy hell – die Zertifikatshölle ist auf die TV Geräte (und vermutlich Receiver) gekommen. Datum/Uhrzeit manuell eingestellt und siehe da…..zum Glück hat das Teil keine Internetanbindung sonst könnts auch noch die CRL checken wollen.

 

SharePoint Subdirectory einer Document Library kopieren

Da man mit Export-SPWeb bzw. Import-SPWeb bzw. im GUI über granular Backup/Restore nur ganze Document Libraries kopieren kann hab ich mich auf die Suche gemacht und bin hierauf gestoßen. Da das dort aufgeführte „Programm“ eher nicht so 1:1 zu verwenden war (und auch bei den Foldern beispielsweise Author/Timestamp nicht setzt) und ich die Geschichte mit Excel, Versionen und verschiedenen Modi etc. nicht benötigte habe ich es entsprechend umgeschrieben (mit Hilfe). Ohne wirklich Errorhandling und kaum Logging – aber es hat einwandfrei funktioniert (SharePoint ist offenbar relativ relaxed wenn man Dinge added die es schon gibt):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Data;
using Microsoft.SharePoint;

namespace Gallauner
{
   class SharepointListCopy
   {
      static void Main(string[] args)
      {
         string sSourceSite = "https://oldsite.domain.com";
         string sSourceWebName = "SomeWeb";
         string sSourceListName = "SomeList";
         string sSourceFolderName = "SomePath/MorePath/LastPath";
         string sDestSiteName = "https://newsite.domain.com";
         string sDestWebName = "SomeNewWeb/SubWeb";
         string sDestListName = "SomeNewList";
         string sDestFolderName = "";
         SPSite oSourceSite;
         SPList oSourceList;
         SPWeb oSourceWeb;
         SPListItemCollection oFolders;
         Microsoft.SharePoint.Administration.SPWebApplication oWebApp;

         // open source site/web/list
         oSourceSite = new SPSite(sSourceSite);
         oWebApp = oSourceSite.WebApplication;
         oWebApp.FormDigestSettings.Enabled = false;

         if (sSourceWebName == "")
            oSourceWeb = oSourceSite.RootWeb;
         else
            oSourceWeb = oSourceSite.OpenWeb(sSourceWebName);

         oSourceList = oSourceWeb.Lists[sSourceListName];
         if (oSourceList != null)
         {
            // search source folder
            oFolders = oSourceList.Folders;
            foreach (SPListItem oFolder in oFolders)
            {
               if (oFolder.Folder.Url == sSourceFolderName)
               {
                  // copy folder 
                  CopyFilesAndSubFolders(oLog, oFolder.Folder, oSourceWeb, 
                     sDestSiteName, sDestWebName, sDestListName, sDestFolderName);
               }
            }
         }
         oWebApp.FormDigestSettings.Enabled = true;
      }

      private static void CopyFilesAndSubFolders(Logfile oLog, SPFolder oFolder,
                                                 SPWeb oSourceWeb,string sDestSiteName,
                                                 string sDestWebName,string sDestListName,
                                                 string sDestFolderName)
      {
         SPSite oDestSite;
         SPWeb oDestWeb;
         SPList oDestList;
         SPFolder oDestFolder, oNewDestFolder;
         SPFile oFileCopy;
         SPUser oUser;

         // open site/web/list
         oDestSite = new SPSite(sDestSiteName);
         if (sDestWebName == "")
            oDestWeb = oDestSite.RootWeb;
         else
            oDestWeb = oDestSite.OpenWeb(sDestWebName);
         oDestList = oDestWeb.Lists[sDestListName];

         // find folder
         oDestFolder = null;
         if (sDestFolderName == "")
            oDestFolder = oDestList.RootFolder;
         else
         {
            foreach (SPListItem oTmpFolder in oDestList.Folders)
            {
               if (oTmpFolder.Folder.Url == sDestFolderName)
               {
                  oDestFolder = oTmpFolder.Folder;
                  break;
               }
            }
         }
         if (oDestFolder == null)
         {
            Console.WriteLine($"Cannot find destination folder '{sDestFolderName}'");
            return;
         }

         // copy files
         foreach (SPFile oFile in oFolder.Files)
         {
            Console.WriteLine($"FILE       {oFile.Name}");
            try
            {
               // add modifying user to destination web
               oUser=oDestWeb.EnsureUser(
                      (string)oFile.Properties["vti_modifiedby"]
                     );
            }
            catch (Exception)
            {
               // add failed, use me as modifying user
               oUser = oDestWeb.CurrentUser;
            }


            oFileCopy = oDestFolder.Files.Add(
                            $"{oDestWeb.Url}/{oDestFolder.Url}/{oFile.Name}", 
                            oFile.OpenBinaryStream(), 
                            oFile.Properties, 
                            oUser, 
                            oUser, 
                            (DateTime)oFile.Properties["vti_timelastmodified"],
                            (DateTime)oFile.Properties["vti_timelastmodified"],
                            oFile.CheckInComment, 
                            true);
         }

         // copy folders
         foreach (SPFolder oSubFolder in oFolder.SubFolders)
         {
            Console.WriteLine($"FOLDER {oSubFolder.Url}");

            try
            {
               // add modifying user to destination web
               oUser = oDestWeb.EnsureUser(
                        (string)oSubFolder.Properties["vti_modifiedby"]
                      );
            }
            catch (Exception)
            {
               // add failed, use me as modifying user
               oUser = oDestWeb.CurrentUser;
            }

            // first create on destination list
            oNewDestFolder = oDestFolder.SubFolders.Add(oSubFolder.Name);

            // set author/editor and timestamps
            SPListItem oTmp = oDestList.GetItemByUniqueId(oNewDestFolder.UniqueId);
            oTmp["Author"] = oUser;
            oTmp["Editor"] = oUser;
            oTmp["Created"] = oSubFolder.Properties["vti_nexttolasttimemodified"];
            oTmp["Modified"] = oSubFolder.Properties["vti_nexttolasttimemodified"];
            oTmp.Update();

            // call ourself recursively
            CopyFilesAndSubFolders(oLog, oSubFolder, oSourceWeb, 
                                   sDestSiteName, sDestWebName, 
                                   sDestListName, oNewDestFolder.Url);
         }

         // cleanup objects
         oDestWeb.Dispose();
         oDestSite.Dispose();
      }
   }
}

Die Reference Assembly bekommt man über das Nuget Package „Microsoft.SharePoint“, laufen lassen kann man das Ding dann allerdings nur auf einem SharePoint Server – mit Remote Debugger lässts sich dann aber bequem testen/debuggen.

Acer Reparaturstatus mit Powershell auslesen

Weil ich gerade ein Gerät bei Acer zur Reparatur habe und nicht jedes mal auf der Website die Daten für die Statusabfrage eingeben wollte hab ich mal mit den F12 Tools des Browser geschaut was der da macht – und siehe da, die Daten kommen von einem REST-Service (https://customercare.acer-euro.com/customerselfservice/AjaxCall.aspx/GetCaseSearch, geht auch mit HTTP – die aufrufende Webseite nutzt das auch so….). Die Requestdaten kann man leicht aus den F12 Tools rauskopieren und Powershell kann seit Version 3.0 Invoke-RestMethod, das Ergebnis:

Invoke-RestMethod https://customercare.acer-euro.com/customerselfservice/AjaxCall.aspx/GetCaseSearch -Method Post -Body "{'RequestType': 'CaseId','RequestValue': 'meine Fallnummer','Zipcode': 'PLZ so wie beim Eröffnen eingegeben'}" -ContentType "application/json"|Select-Object -ExpandProperty d|ConvertFrom-Json|Format-Table SerialNumber,Status

Ergebnis in meinem Fall:
acerstatus

Linux: Trusted CA einfügen

Aus der Serie: Dinge die ich nicht nochmal suchen will: Wie füge ich auf Debian systemweit (für die Applikationen die sich daran halten) eine neue trusted Root CA hinzu:

  • CA Cert muss Base64 encoded vorliegen
  • Filename muss mit .crt enden und darf keine Leerzeichen (und vermutlich andere Sonderzeichen) enthalten, – und _ sind ok
  • File nach /usr/local/share/ca-certificates kopieren
  • sudo update-ca-certificates, gerne auch mit –fresh dann wird alles neu gebaut (Links in /etc/ssl/certs)
  • wenn alles richtig gelaufen ist sollte der Output „1 added“  oder sowas in der Art enthalten

Zertifikat am Office Web Apps Server tauschen

Geht ja voll easy mit Set-OfficeWebAppsFarm -certificateName .... (bei Wildcard Zert muss man halt Friendly Name vorher vergeben und den hier benutzen, kennt man eh schon) – nur sollte man vor dem Restart des Servers oder des Office Web Apps Services sicherstellen dass _ALLE_ CA Zertifikate des neuen Sitezertifikats in den _RICHTIGEN_ Stores des Rechners vorhanden sind, also nicht nur das Root CA Zert im Trusted Root CA Store sondern auch alle intermediate/issuing CA Zerts im Intermediate CA Store….die Fehlermeldungen im Log sind natürlich wie üblich völlig unbrauchbar 😀

Vielleicht auch hilfreich: Link

TiWorker und die CPUs

Stell dir mal vor du hast eine Maschine mit 48 logischen CPUs (2 Sockel zu je 12 Cores mit HT). Stell dir vor du installierst da 212 Updates drauf (frisch nach 2012 R2 U2 Installation). So sieht das dann aus:

TiWorker

Die Kiste läuft original auf 2-3% CPU Last. Ernsthaft.