Skip to Content

Decoding and Encoding an Active Directory objectSid With PHP

I've seen some examples for decoding an objectSid from LDAP, but I could not find any examples for PHP for re-encoding that SID from a string to hex form to pop it back into a LDAP query for searches. So after much frustration (and maybe a beer or two) I managed to put something together that seems to work. For completeness I've included both functions to decode and re-encode a SID. The 'fromLdap' function decodes a binary SID to a string like you'd see in Windows. The 'toLdap' function takes a SID string and re-encodes it for use in an LDAP query using the objectSid attribute.

  1. function toLdap($sid)
  2. {
  3. $sid = ltrim($sid, 'S-');
  4. $sid = explode('-', $sid);
  6. $revLevel = array_shift($sid);
  7. $authIdent = array_shift($sid);
  8. $id = array_shift($sid);
  10. $sidHex = str_pad(dechex($revLevel), 2, '0', STR_PAD_LEFT);
  11. $sidHex .= str_pad(dechex($authIdent), 2, '0', STR_PAD_LEFT);
  12. $sidHex .= str_pad(dechex($authIdent), 12, '0', STR_PAD_LEFT);
  13. $sidHex .= str_pad(dechex($id), 8, '0', STR_PAD_RIGHT);
  15. foreach ($sid as $subAuth) {
  16. // little endian, so reverse the hex order.
  17. $sidHex .= implode('', array_reverse(
  18. // After going from dec to hex, pad it and split it into hex chunks so it can be reversed.
  19. str_split(str_pad(dechex($subAuth), 8, '0', STR_PAD_LEFT), 2))
  20. );
  21. }
  22. // All hex parts must have a leading backslash for the search.
  23. $sidHex = str_split($sidHex, '2');
  25. return '\\'.implode('\\', $sidHex);
  26. }
  28. function fromLdap($sid)
  29. {
  30. // How to unpack all of this in one statement to avoid resorting to hexdec? Is it even possible?
  31. $sidHex = unpack('H*hex', $sid)['hex'];
  32. $subAuths = unpack('H2/H2/n/N/V*', $sid);
  34. $revLevel = hexdec(substr($sidHex, 0, 2));
  35. $authIdent = hexdec(substr($sidHex, 4, 12));
  37. return 'S-'.$revLevel.'-'.$authIdent.'-'.implode('-', $subAuths);
  38. }

Jury Duty and Group Decision Making

If you ever want to see groupthink in action, be sure not to skip out of jury duty! I considered my recent trip to jury duty more interesting from the perspective of human psychology and cognitive biases than I did from it being a civic duty. This is one civic duty that could certainly benefit from research into the dynamics that flaw the decision making process. Take 12 people, make them spend the better part of a few days with each other, then throw them in a room and expect them to reach a unanimous decision with virtually no instructions. What could possibly go wrong...right?

Well, it sure doesn't take a whole lot. All it takes is a few vocal people to lead the conversation in a certain direction, and once that gets going you can expect others to rationalize away most doubts, no matter how reasonable, to reach a group consensus. Once people have conformed to the group's prevailing decision it's pretty much impossible to get them back. It then becomes less of a decision making process and more of an exercise into pressuring the rest of the group into compliance. Confirmation bias, the bandwagon effect, the false consensus effect...not to mention people building straw man arguments and appeals to emotion. Oh, and then there's the people that tell you straight out at the beginning of deliberations that they've already reached a conclusion and wont change their mind. Don't forget about them. Jury duty...I hate you.

Oh well, such is our system. But hey, they paid me a whole $15 a day...woohoo!

SCCM Task Sequence with LDAP Password Protection

UPDATE (10/14/13): There was a bit of an issue with the script as it was originally published. I must have copy/pasted an old version, as the one in my environment had the correction already. The script will now obey the LDAP group you specify for authorization.

This HTA is kind of a mishmash of various examples I found online...mostly because I hate VBScript with a passion. I was just surprised I couldn't find anyone else doing this. Basically I just wanted a way to password protect a task sequence in the "Run Advertised Programs" area in the control panel. That way Help-Desk and other IT staff can run it from within Windows without having to worry about a user running it on their own.

I recently created a HTA for SCCM that you can stick at the beginning of a task sequence to password protect it with LDAP and a group requirement. Just fill in the 3 constants from line 18 to 20 to make it specific to your domain. If the user is in the group defined by LDAP_AUTH_GROUP it will place a "Y" for a value in HKCU\Software\SCCM_TS\Authorized. Just wrap the rest of your task sequence in a group with an option set to check that registry value and only continue if it is set to "Y". The reason it writes to HKCU is because if UAC is turned on it will not be able to write to HKLM. The "SMS.TSEnvironment" object didn't seem to work for me HKCU it was.

Anyway, to run the below HTA itself, stick it in the "scripts" folder of your MDT package. Then add a "Use Toolkit Package" step at the start of your task sequence, then directly followed by a "Run Command Line" step. The command line should look like the following...

  1. %ToolRoot%\ServiceUI.exe -process:tsprogressui.exe %SYSTEMROOT%\system32\mshta.exe %SCRIPTROOT%\ldap.hta

Just replace the "ldap.hta" with whatever you name the HTA file as in the MDT scripts folder. Copy and paste the below for the HTA file...

  1. <html>
  2. <head>
  3. <title>SCCM Authentication</title>
  5. ID="objTest"
  6. APPLICATIONNAME="SCCM Authentication"
  7. SCROLL="no"
  9. >
  10. </head>
  11. <script language="javascript" type="text/javascript">
  12. var oTSProgressUI = new ActiveXObject("Microsoft.SMS.TSProgressUI");
  13. oTSProgressUI.CloseProgressDialog();
  14. </script>
  16. <script language="vbscript">
  18. Const LDAP_DOMAIN_NAME = "mydomain"
  19. Const LDAP_OU_PATH = "dc=mydomain,dc=com"
  20. Const LDAP_AUTH_GROUP = "cn=My Special Group,ou=Some Place,dc=mydomain,dc=com"
  23. Const ADS_SERVER_BIND = &H200
  25. Dim IsAuthenticated
  26. IsAuthenticated = False
  28. Public Sub OnLoadTasks()
  29. window.resizeTo 375,200
  30. document.GetElementByID("username").focus()
  31. End Sub
  33. Function DoAuthAttempt()
  34. Dim strAdmin
  35. Dim strPW
  37. strAdmin = document.GetElementByID("username").Value
  38. strPW = document.GetElementByID("password").Value
  40. if strAdmin = "" Then
  41. msgbox ("ERROR: The username cannot be blank!")
  42. Elseif strPW = "" Then
  43. msgbox ("ERROR: The password cannot be blank!")
  44. Else
  45. Authenticate strAdmin, strPW
  46. End If
  47. End Function
  49. Public Sub Authenticate (Byref strAdmin, Byref strPW)
  50. On Error Resume Next
  52. Dim strPath 'LDAP path where the Administrator accounts are listed
  53. Dim LDAP 'Directory Service Object reference variable
  54. Dim strAuth 'Parses the User Name and Password through the DSObject
  56. set WshShell = CreateObject("WScript.Shell")
  57. strPath = "LDAP://" & LDAP_OU_PATH
  59. Set LDAP = GetObject("LDAP:")
  60. Set strAuth = LDAP.OpenDSObject(strPath, strAdmin, strPW, ADS_SECURE_AUTHENTICATION Or ADS_SERVER_BIND)
  61. If Err.number <> 0 Then
  62. intTemp = msgbox("Username or password incorrect.", vbYES)
  63. Else
  64. userDN = findUserDN(strAdmin, LDAP_DOMAIN_NAME)
  65. Set objUser = GetObject("LDAP://" & userDN)
  66. Set objGroup = GetObject("LDAP://" & LDAP_AUTH_GROUP)
  68. If objGroup.IsMember(objUser.AdsPath) = true Then
  69. WshShell.RegWrite "HKCU\Software\SCCM_TS\",""
  70. WshShell.RegWrite "HKCU\Software\SCCM_TS\Authorized","Y","REG_SZ"
  71. Set WshShell = Nothing
  72. IsAuthenticated = True
  73. msgbox ("Success! " & strAdmin & " has been authenticated.")
  74. Self.Close
  75. Else
  76. intTemp = msgbox ("Username specified is not authorized for access.", vbYES)
  77. End If
  78. End If
  79. End Sub
  81. Public Function findUserDN(samAccountName, domain)
  82. Set objConnection = CreateObject("ADODB.Connection")
  83. objConnection.Open "Provider=ADsDSOObject;"
  84. Set objCommand = CreateObject("ADODB.Command")
  85. objCommand.ActiveConnection = objConnection
  86. objCommand.CommandText = "<LDAP://" & domain & ">;(&(objectCategory=User)(samAccountName=" & samAccountName & "));distinguishedname;subtree"
  87. Set objRecordSet = objCommand.Execute
  88. If objRecordset.RecordCount = 0 Then
  89. findUserDN = ""
  90. Else
  91. findUserDN = objRecordset.Fields("distinguishedName").Value
  92. End If
  93. objConnection.Close
  94. End Function
  96. Public Sub Window_onbeforeunload()
  97. If IsAuthenticated = False Then
  98. set WshShell = CreateObject("WScript.Shell")
  99. WshShell.RegWrite "HKCU\Software\SCCM_TS\",""
  100. WshShell.RegWrite "HKCU\Software\SCCM_TS\Authorized","N","REG_SZ"
  101. Set WshShell = Nothing
  102. End If
  103. End Sub
  105. Public Sub IsEnter
  106. If Window.Event.Keycode = 13 Then
  107. document.GetElementByID("authsubmit").click()
  108. End If
  109. End Sub
  111. </script>
  113. <body onload="vbs:OnLoadTasks()">
  114. <div class="style2">
  115. <h3>SCCM LDAP Authentication</h3>
  116. </div>
  117. <div>
  118. <span class="style3"><strong>User Name:&nbsp; </strong></span>
  119. <input id="username" name="username" type="text" style="width: 150px" /><br>
  120. <span class="style3">
  121. <strong>Password:&nbsp;&nbsp;&nbsp;&nbsp; </strong></span>
  122. <input id="password" name="password" type="password" style="width: 150px" onKeyPress="vbs:IsEnter" /><br><br>
  123. &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
  124. <input id="authsubmit" type="button" value="Submit" name="cmdSubmit" onClick="vbs:DoAuthAttempt" />
  125. </div>
  126. </body>
  127. </html>

A Steven Pinker Book I Actually Finished

I admit it...every time I start reading a Steven Pinker book I have a tendency to never make it all the way through. He writes on some interesting subjects...but his explanations and material is often far too verbose. It's like he doesn't know what to exclude on a topic, so out of risk of missing something he just includes everything. Sometimes less really is more. Well, depending on how you word things anyway.

However, not too long ago I actually finished reading The Better Angels of Our Nature. It's a tome of a book, but well worth the effort. The historical context it provides is great, and it contains an enormous amount of graphs/data/statistics. One of the most interesting parts of the book though was what he termed "The Pacifist's Dilemma", which is a twist on the classic Prisoner's Dilemma. It does an excellent job at characterizing the tragedy of a violent situation, and why aggression could often be construed as a rational choice (in terms of "payoffs") and creates a large asymmetry in the payoff and losses between the aggressor and the victim.

Directly afterward I also finished Moral Origins, which actually turned out to be a great complimentary book. Well, if you're interested in science of human morality anyway. There wasn't anything earth-shattering in the book, but it did put all the data together nicely and was fairly insightful. He placed a heavy emphasis on what he termed LPA (Late Pleistocene Appropriate) foraging societies, which I haven't really seen anyone do yet. My only complaint is that I often felt he was far too dismissive about the similar work of others, but it's not really that big of an issue I guess. It's definitely worth a read.

jQueryUI Tab Cache Frustration

Every so often I seem to run into a bug or edge case with a library I'm using that makes me want to gouge out my eyes with a rusty spoon. This weeks winner: jQuery UI tab caching when loading via ajax. And in all actuality, getting it to work wasn't that difficulty. But getting it to work as I expected was a different issue. The problem was that when I initialized the tabs the default tab would be loaded as expected, but if you switched to another tab and back to the default tab it would attempt to reload the content of the default tab via ajax again (even when caching was enabled in every spot I could find). Quit infuriating. Subsequent selections of the tab however would not cause any more load events.

I eventually stumbled upon a workaround to the issue, though I think it only applies to jQueryUI 1.9+. The trick is to define a "beforeLoad" event function for the tabs and check the panel for HTML content. If it is there, simply cancel the ajax event to stop it from needlessly reloading it again. The fix looks like this...

  1. $("#tabs").tabs({
  2. cache: true,
  3. beforeLoad: function(event, ui) {
  4. if ($(ui.panel).html()) {
  5. event.preventDefault()
  6. }
  7. }
  8. });

Nagios Plugin to Check a SMB File

I recently wanted to have a check in Nagios to look for the existence of a file, and it's age, that resides on a Windows server. I know there are various NRPE related plugins to do this, but I don't like to have to rely on additional client-side installs for the check to work. So I wrote a plugin in Perl that uses the Filesys::SmbClient module to check for a file/directory directly from a Linux machine. It's also possible to check for a specific size, last modified/accessed date, or a regex of the file contents. This was actually more difficult than I anticipated because the libsmbclient that the Filesys::SmbClient module uses for SMB doesn't like to play well under the very limited environment that Nagios is often run under. Anyways, I have the plugin out on GitHub...

There are still some enhancements to be had, but the basic functionality of it is in place. It still needs to be modified so it returns performance data, and there are probably a lot of additional useful checks that could be added to it. Gotta have something to keep me busy :) Also, my Perl is more than a little rusty so the coding is a little crude...oh well.

Symfony2 Doctrine DataTables.js Implementation

This implementation now exists as a bundle with a service:

Edit #3: The code is now on Gist:
Edit #2: Added support for remaining DataTable.js variables: "sSearch_x" for individual column filtering (tested fine) and it now honors "bSearchable_x". The constructor now requires the entity manager and does association field name checks. Changed some method names, reflected in the example of usage. Added some additional comments to the code.
Edit #1: Minor mishap with the original code. Kinda helps if DataTable searches work properly :) That has been fixed and reflected below.

I decided to use DataTables for displaying some table data in a Symfony2 project, but I wasn't able to find a sever side processing script that did what I really wanted it to. However, I did run across the following code: . And actually, that works great for simple entities. But what if you want to access associated entities? In effect, you can have a "Ticket" entity that has an associated "Customer" entity, and you want to fetch a DataTable that contains the list of Tickets with the associated customers first name and their last name. So the defined jQuery/javascript for the DataTable columns would look something like this...

  1. "aoColumns": [
  2. { "mData": "id" },
  3. { "mData": "description" },
  4. { "mData": "customer.first_name" },
  5. { "mData": "customer.last_name" }
  6. ]

To solve this, I made some modifications to the linked example code. I parse the "mData" and the period becomes a delimiter. So if a period is detected it treats the first half as the entity name and the second half as the entities field name. While I was at it I also modified it so it uses reflection to call the entities predefined setters/getters for the different properties so you don't have to manually define some sort of magic get method on the entity. Yeah, I'm lazy like that. Anyways, the modified code makes a few assumptions, which are...

  • I modified prepareResults() method to only return an array. I'm using the FOSRestBundle which uses the JMS Serializer, so I found no need to have a pre-serialized JSON response.
  • It expects the properties/fields on your entities to be defined with camel-case. I camelize the "mData" sent to the server if it is sent with underscores.

Of course, these assumptions can always be changed/modified with not too much effort. Below is the code, which isn't very heavy tested, just something I finished rather quickly so it could probably use some clean up or be done in a more elegant fashion, but this is what I came up with (Thanks again to Félix-Antoine Paradis for the working implementation).

The code:

Limits of Human Knowledge

I stumbled across a video recently with Patricia Churchland and Noam Chomsky on explanatory gaps and mysteries in science. I think this best summarizes, in a general sense, the distinction to be made when thinking about the limits of our knowledge. I would actually have thought that Patricia Churchland, having a background in both neuroscience and philosophy, would have been more likely to recognize this distinction (at least as it pertains the the way our brain works). People seem to often confuse the gaps we see in scientific knowledge and the limits imposed on us that simply stem from us as a biological system molded by evolutionary processes.

Biologically, we are structured to fill a niche in a specific environment. That we have gained all the scientific knowledge we have is impressive, but I think it's overly optimistic and short-sighted to think there are no practical limits or boundaries to it. There is no law that requires nature to be understandable by us. We constantly fight flaws of our cognition to form a better understanding of the world and those cognitive flaws are likely by-products of potentially beneficial adaptations that will always be with us. We will always be fighting against these limits, and they likely cause a complete, full understanding of certain systems or aspects of the world to be out of our reach.

It should also be noted that it isn't just our biology that sets limits on us, it's our place and time in the Universe as well. As Lawrence Krauss points out in “A Universe From Nothing”, we live at a time roughly 13.8 billion years after the big bang, without access to knowledge that may have been only available either much earlier or much later in its timeline. If we lived sufficiently far enough in the future of the Universe, at a time when the rest of the galaxies have receded to a point where light from them could not reach us, we would possibly have a very different view of things. Likely one that would be completely mistaken. We are also confined to living within a single Universe when the idea of many is often a generally accepted possibility from some well established theories in physics. Certain empirical questions may be unverifiable in principle as they require access to a system/place we can never hope to reach.

While there are lots of genuine gaps in our understanding that can be answered, I think at least recognizing we have limits is important. Given the constraints imposed on us, it's interesting to reflect on how much we have already learned. But perhaps even more interesting is to reflect on how much we may never be able to learn in the first place.

Mixing Encoders on a FOSUserBundle Entity

Interesting Symfony2 password encoder issue I ran into recently...

So imagine you're using the FOSUserBundle to create and manage application users. You want 2 separate user types that store and authenticate users quite differently. How can you accomplish this? In my case, I wanted to tie some of the logins to LDAP and some to normal SQL based authentication. So I have 2 user providers in a chain. The first in the chain is the FOSUserBundle provider. If it fails to find a user in there it tries a custom LDAP user provider. On successful retrieval and authentication of a LDAP user it creates a user to persist to the database via the FOSUserBundle. The users are differentiated by extending the base FOSUserBundle user then adding a custom field that defines a user type for either SQL or LDAP users.

Now, all of this technically works fine and you can store both LDAP users and local SQL based users from the FOSUserBundle using the same FOSUserBundle entity. Howerver, the LDAP password requirement means you need to set the password encoder to plaintext. This will work, but it isn't exactly secure. This means that the passwords for local SQL based FOSUserBundle users get stored in plaintext in the database. So somehow you need to be able to conditionally select a password encoder based on what type of user they are. There is no way to do this with standard Symfony2. However, there exists a nice bundle to solve this issue: FOSAdvancedEncoder.

With the FOSAdvancedEncoder bundle you just need to implement the EncoderAwareInterface on your entity and define a method called getEncoderName(). Then all you have to do is determine which encoder you want to use within that method and return it as a string, as easy as that! You also need to define the encoders in the config. See the docs for the bundle for details, it is very easy to use. The end result being that I can now safely store the passwords for the SQL based users while at the same time being able to use a plaintext encoder for LDAP users since the password never gets stored in the database for them in the first place.

Maybe some other time I'll write up something on the the custom LDAP user provider/authentication provider and how to get it working with the FOSUserBundle. There exists a bundle to tie together the FOSUserBundle and LDAP auth, but I never got it working the way I wanted for some reason.

Symfony2 and Javascript Base URL Paths

When I'm writing web application code, I always hate the feeling of hard coding URLs and paths. One example of this issue from Javascript is when including TinyMCE. When you initialize it you have to supply it the path to the TinyMCE JS file. Sure, you could give it an absolute path based on your current structure, but that isn't very flexible if you decide to change it. There are a few ways around this. You could define some Javascript within your base twig template and define a global Javascript variable using Symfony2's Request class method getBasePath(). However, to me that doesn't feel right.

There's a way to get it directly using Javascript and a little regex magic if you're using the FOS JsRoutingBundle. This is an awesome bundle on its own right, but it actually exposes the value of Symfony2's Request class getBaseUrl() method. The only real difference from getBasePath() is that this method might have the front controller script name appended to the string it returns (depends on your web server setup and how you're accessing the pages..). The string is exposed within the FosJsRoutingBundle's "Routing" Javascript object. To extract the base path and save it to a variable you can do something like this...

  1. var base_path = Routing.getBaseUrl().replace(/\w+\.php$/gi,'');

Syndicate content