ÿØÿà JFIF    ÿÛ „ !.%+&8&+/1555$;@;4?.451 4,$,44444444444414444444444444444444444444444444444444ÿÀ  á á" ÿÄ     ÿÄ ?    !1AQaq"2‘¡±ÁðBRbrÑá#‚’¢²3S CñÿÄ   ÿÄ !    !1QAa‘2ÿÚ   ? 5˜Z¯V¦cø)›t/? z¨±>Õ5€¶‹Á¤·¼z¼Ü¬+ñ®v¤¨_ˆR­BFn©—˜ý®ç̝P8gýt·ÉSTŦˆìät?þé¼íìN/Þa)ì–í6ô… Ï¿øÃj´¿KÇü]ÿ ªô¹-eKànëÕHTx}ýSÜ›ÿ ”7Ø×&µ<¦  ¥ÑO¶[Ù¯ä¨ÞÃÿ PZ-¬;#õ|•oaÿ ©CìÞz3˜öː/¤­ñTûIØ}š^ mÓ%ªxˆ¥ÉŸu=Z+ISe¿45™¼u;ú&WØ÷€æßQ™®{|íx*TC“#ZŠìZ§²‹ 6pv…³¿¡äª*áZÐ%ÒOáˆo"x«OHk w±æ+¬V(kMúŸ5Vö«$ ÁrÏbàb57/luR ¸ÑÛj Òµì`Мq­û žICÀÊ•©4€Âcà¨Ï€O´<èÐ:›ù(Ë^L8þ‘ÍÌ#¸Ð_Ì©ÙK(Öz 4¬û+¸;ü’V’84‘¬ÃŽ:[â‡ÔÌáõp¢~§ªlæ£ö{®G>J¼"°‡7¯ÆÉèßû ‹É‹§ÁòÃýâßî ^ƾÙõ‹×óH#«LP½ïX=xÑÍ$|W?•~• îëÔ©ª‹ {ÝT…Kÿ ”hûâá)J*ö˜–ÔU;iÇ€/ ÆþjóZ\ýwØ=Ìm ºèËL9 ýèÆð/¨’¥öo=nË.%Îì ŽÕ¯È|{Oj²ƒE6e/ßdÄõ²Ìâ1O®ò×TsəԸhOMýíMˆ¿¼H˜l²,7Â¥#MF/Úf°Ö½± ¸–dr‹NýÊ íjqx{œÉ ä-È ¦ øÄër¨q°ð †nцýÑÄÆ’mä…n<0È™;ÁÝá¯ÁZƒ7FÀmì­ É&9ˆîéi¶ùN§Y• ÃZãAâ?•‡©‰ , ó¾IŸŠc1 4â&y­&pŠ­6;M À 0¹qç»p.á …ŸÅáK@%6·y6ƒ‰3?”úºŽ‰éX5ªPT §µ!=Mž«Ú½‹ÅgÂSâÉaþÓoö–¯ÁÔìR>5éÿ üs¶ÆUcÌ kÇR ]ÿ ù¬¼«VŽ;Â|‡~¢¦”ÏŰæ {L™Õ°Óv¹ò¸írޡעCÃ!íVÕ {¶»sŒNPg/ "uÕbkm²“$ďå¿é¹§°½æz¯6 †s¿!s–wÚÝ“™Œ °.ûj>·+™Òa…©Œ&rÝÎtÛë긪Ît’LAVp%c Úý[ÄzJ¾ÇàXXç@˜ó<êL]·T˜¾¥1Ó©V‡g´æ½¦Ý@¹óø!_@´ÞâSÁ —S3™•& ]@JHÚý©ZŽ €×æÔr»Áf!‡yÞ4Mv*èÓã_{‘åóUuљØ«Oïé*®EvÑ Œ÷‡U \"㪒ÍK+À 4“M¡ï:0¥5í!'<@î´”>Ç»&Z–ïCCV˜Ì5Šo&îhè.žû |ÓK©h$s6KìŒëã)¹hI¦GïOåóI;ììü#É$Š0…Ææ¥TØ.5­¾gn´ “ÂÖ\:hœ89G)J@„}œ:’Ò{/Š"¦_Æ×7Æ3VÇŠÊa]ÚŒÙ€Ä–=®uÁßâACZƒ§§£ Qnâ:«,×{tyø¬iÛcœÜÄ€H½ÄÍCk´÷šß .W'b¤Íåh]÷€=,Žv×cÚEÚHXJX¶îo¨FÒtèöŸ>ªª6[J®Fµ£sGÁeqõfe\íjÒÐïÄÐGˆe1Ø‹.Ø”‘Ëuø Y­ˆÜ ŽG|zùªüMpDnQWÄ”%JŠ™)â*p@Örš«ÕT2Ð%ˆG#ª„ ·¤!°ŸOTÂT¸aÚ%4&h™LµšØüÐ.F¿²ÐÞ_Ç‚¾ÅÃaÜ÷09Æ q€öy˜v‡85õN÷]¬äѼóS{°_MެúÔ#°Ç¸0åÞè2ëôPcvÆw9®ií1Ä8F™˜à‰´+‰Ik1òÝ7“Ñ×ÒsÝ\x‚h`ÞÑ`ó"|µEcý£n˜h`}GÞ !±ù²Ápü²ß6 0ïi󜵩SÈÇ7˜-ÕURO˜¦´f$ªž-Í6(œ}<„ éc øs]ŽŽ„*—¾ ìdŽ„)méª\¿êÎIg¾ØÞ~I#C/¼¼´EÁÈŽi8“©õådô·>euä ƒ'Ê×लR1ÉJE1ÐAát`t;ÇР%Ý<‡¥„ÍÆ`×Oyó)õiI€ñQaŸ4Ûù\áàaÃÔ¹HÃu¹*k€¦<„e S‡&õÏ B!ŽhüÞ`yj}mªf×\¿ Ç~æ­9‡û\՞Ǖg²1Žû5V7 !àöšm° c`ܬøÇìµÒ'P"?…´Ö,"§^•õލsÔ)6˜sæéÍR¼ ò|Sl”‹7 nPW Gòú÷½§O¯‡„l¡kSÞŒr½PÊ@æ¢pŽ-mÿ #Ÿ˜Àº¶Áä¦;ïÔæ$1££`“Õ>„—·ž)ßð³ñ#Ï Ô$¶œ‰ÊE‹À;÷º ¯«P:Ñ”8–IÊtpÞ3ª“>ê“þës4ò2OÏÕ­±zô†Õ§‰.÷ä¸;¿˜“'œ›žª}«Œ{ª±Ì 9ÔóÞÕ‡0 $íWV3Üì¬ —@kÝ4@¿r¼±½¬™›?øØæ´'Áé®CË3-g$˜ö‡×auÚi´Žp/êÛ æF›Ú2v‹ã¿¿,nB1̨ƃqÞa5͝@&Æû“él÷ \C²½UÍc ¯k×¢U ÖéQå™—-r wô ÞÏ<Ò=&=ÿ Ôê Òêˈt,i—;LîÜ á¸*ÚÃ1$êL•LÍ <É)ýÐà’ ;F™{ƒ™˜€&'}‚ãÄK`¡ÞT@I;®žZóè‚s’7®°›+§O­Åq©é»²9<Ô J ¼9O’HL»Ùïì¸rk¼Ž_ý‘TŸu[²ßÚŒ·ü÷B%¯E ŸÔX5êO´ Ç•€’I0 ÉJX` ñ¹õ%;µŸD‘«´€àwÒ™U ûئžÖö\×®×´8 ½‡ºÐÆÓ§?Àkmœ=;d5*@-ì0F Rªýš[Ü6âö̃ڸr*KA9· u*µæ£?U¸Âêí†8@¦X4 e-ò„0s{ HâUpU?¼mñRa°®a%Ð'tÉ×’\¾ÊÉ]t›h>·(Ë@R¼¡Ãt h}’O÷au<+nT…Ö…MӐ??Óe95 q>í/;&JSû °¯ÊéÞ øƒ*Ã2½Ài&:nôUl=¾¿5eˆ3”ñc|Ú2V”>„»&eE;«ÚäC p¢Û úy 9š[ŒÌx¼擼A&DåÒ¯ˆ¤ÀÌ;"˜ ÏQä¸åhÊ}Ûq«Û0WžÒ|»€ø®öCm5•\ÇÀ§Pe3£]0ÃàLDÉ‰1øªxjgwT‚÷¿LΨK‹›ùs—xˆÜ±µ kæ¸f‰‰ÜGk/LÛØ6d9ò¶ùA{ƒA3š/¬D¬khÓk‰`˜"㯒r¿±Óã jx‡°e}<Ñø\3y:'À•/h½Í€Ç4~g ?Û(¼]v‘ªlKÎâ~?O‚W%{Ì:“'©úNq¾›úo(X’¥¯ˆ nFê{Ç€ü?º'ë ø‹ì Þ09ŒÌç9Æ —ËC`j@ÓÄ(+a‹un¸#ÂꟋ{K`‘ÑÍÍ'à´»/Û,KW;Þ4²þð ï Nm|~fGÏ(…³Ã)«1ö­Õ ¥‡¨©ƒÃ™ü-s=à=U66Ï«Ýc蓦W¹íž®›nÔ%êÇìŒ<#Ü×84ån®Ð ÒåOC` ñânÑs‡¢ç 1õ%Îhì½Ã½® e:ݼUZo™`  ÅZŸŒÊ«ê1ÏÄo$q¹Þ€©ˆhÐÉä¯ñ[!…Ú˜àJ:x2$Íß&PåT£6ç— ‡Í*4Ýšçjÿ ‰É nófÐ ó(L5C•åÆ\rMÒ@ò }y-W}™üýVù—ú¢=Ù”c®‘< M ž ´Phr ¦©TD ‘ù.$´÷O‡‘V2Æò.=IUŒ=ž‡â¬i™aþÓåÙ?òUø'ØÖ•.~* šTŒ!•-×áºTâ®ä#õü'´ eýlYÅÓeÕKÂrT"CÚ@u!Óxƒ{š3€}1¿(r}%«nËamjÑ%ÑNEò v ˜à  σöK³,*º.àzù¨™Ó ÚçâU¦*¿ 9{%Ö¹ njûdaXöb) kÛÆ±ûÓ\°M7ˆÂ=û›ç¿Ã‚­V»Cg–8ÙêE- j)k$º`Ã-ùEýeBÆÇ]c¡°ñty&Òd0nõ'¡W+ƒ*|–øµFa\GQªEAÔp5\Ǽ·¼Ç8·õ -â§Ú[ ‡ uZeÖ 3}×d'+¹:ð+K†Û®s!Ï$úe€<Û”x)1»a­¡LC]¸µík…ÚàA»AYº{†ªS[¦5HÒ7ù --,ísòDØ€èk ÞÀîÜ ò@â( ËNˆë›4ô½•/¦o‡€Û7 ê•ÆêòðÜy'Án½µ á˜ݦ ndeo…[ì¶Ê,¥R³Ä=À±—–ß;£™´ñSâ*g§”ïaið‘Jå~™ÓÞ ß³Õ¢»8x埒²52>AÊb&-÷\7´éÄù€T˜,w;3{ï˜k…à¹ÄqÀ«œ{€\ ˆ¾[´¨јr &Úé„Ívˆ±8†¿]|¬ņ4I×pÞS1ÈÖz‰#Ìv‡G!YNògñ:màTz¢Ý1ô©^O=~ë|5Bã™ç•¼µõ•bÆ@úÕS¬ÈŒ#¬zünrŸ û” Z²•èðV"ÁHÚý©wÝ €7¼Ìu1hÑa3Éä û f$o¿É ™Ú›ÝçnpÒ3äÌ3†Í§,Äï]$‰/pê †«À¼¸e9­Æê_C]žƒ·ý·frÁN«, E=›Çq -‰öŒ:aÏ¿±í&£Í:-} 84‘ÿ eƒQÑeëSsuiA ³g㟥ú£?ÿ ʼn*”“÷aühe:ÊWa@ÒÞk±eØ] F Ô—r.åä˜ @ö¥ªZoÐýYL·¥S²G/‡ñ <~*ZÆ´è>JlòàÛÆ½ÿ 窘ìGN¢:I®KšJp/`íIÁÀõ#Ä-€ö­šµŒoF4|ÆQØÆ@Ì|£Ô…¢À{9˜è½Üó›€ôYÒÎYsið;ís¤€à²ˆ‚4qÉVŒI$ ‰"° æµ8cXGjœˏ¡Aâý•ËÜ¢ûï e·çLx']á"oÅÎê3¯Ç—¹”ó0nå‚âg{Œñ> S´˜îè°g238‚ãköÝfÚd´6Ò€;ò÷±¢™¼›º ¢Æ'¥Ðx'e¬ç ]bÈÆV¢ó‹kýBO ðÊâ$Ÿ!×T 3Mýמ žìٍàÌü‘8÷€àæØ8æ©6‰©L´«…oãpð„~Çk‰!ñ;‹”ÛžÍ àž±z Ÿôû øŸÝužÏ;ÿ #|u6™Þ¬ÚˆÐõA4¶â|ôl|Ê2ŽÇ¤ÝÅÇY.<#Aí.k§hóF‚”Y; M½Ö4hŸ4&›­¿tès´%FìL¥£Ãk‰ÇT¤haÁ¤ÚxfÉ`ÑìË›>i 3t‚:,–+^÷´–{Û–Nxi"x‘Ûg î¨>¥Õ܁ùZH,2Û“:8xÊ¢Çí9.É-Ìâã-=çjwµS˜dütžçwýGòú®®ûº_ˆýx$–¡ãøO EÚÛÏ÷R„×w+3£Á£öUMyR²¹âŒ°š›¸Ñãò9§Ó_Dl+Ùßc›úšGÅÌc†Ž!Ko=¶.‘Îÿ c²(2®V mª.ÿ ¹B›¹å ù„öŸSV>™ü¯$y:G¢Z×àøúdî¹û­·ýÇ´:•c LÍõi_‹ö+ÎæGÊè>OŠ•äž´§Þ{X}¨1ÚTc›»Qþ•êô°t¿OP?eæ~É{5]•ÙR£r5†nZ\ã@ &îJõ ¾àC°þV>fé¥/ü5ñÊIº_é5 ;e­h<@ Ä&æÃëE%;X,ÒãÆÞ`Oò¦kŸm#˜!ÀyÄ¢| óLšò¥Ä` ¶R=|ÈCâh5ò3DˆïF†ðÒ#ÅìÛœ?¸yhBãœí ZxßÎÄhºRK„`Þödvײ™ÀÈÑÒgŒuY w³%†ƒÓzõ ÖÏp‚dH®¦A´ù§»ÓÇMæ~)ˆð‡û:ù&Ä •vGD´À n ݇¼Ö8Fö óáà£~Ë¥x`oK|Ä?fxiØü%pìR>éò+Û±éÎ>núlFŤ'tq8LZÏvÃ?„¡ß±È⽆¯³íü@x|PöUäèØã¡ð‚ŒAìÏ"vÍwóŸÍ{ ý0.z È•Ö{,N¡£¡ŸKÕÙž>Ýœþ ÍÀ°<×EA!Å‚D™IúOÍ¡>ôG}Â` ÍßkÜL™Ž Þð™ {IøF²¹òQ3&!ÃÂÞz.d&Ï-sH¸,Ôõ˜ŽP€ 77ˆÝ¼ÊëÜw =cÕ Ú,ØÐ5ÎYÐ)ì´öœgŒ[¤ßv㙑8心>h]§µháYš£²ºÑ.{Ï7Sð•?´~×SÃKýJÛ˜ ™Íäiúu<µX¶1õ^kâçIÑ£sZ4h>j*ÔšD:4­¿_ ÷¸ Õxæÿ ¸?Mù _•­ÊÐ ä ÷ý ÑwL œ­ïnTkÛUÍN©ë:¦fV ¶ÜÔÜMªÅâA½–¿R×TXš-%iTÊT•‡Ù‚JôϐZxWÑè‰f‰òG º ×Õû2aZ7OU3[“×AT–ÞŒ…-‘¤”Ì ì&(ˆ¿­•ƒkï’:ðY¦W‘ Å)“†‘˜³Åtcø˜ñTÂwÚÇ4|üLÇªí–v- qˆèU qPE.†â‘˜µ Æ,ÐÅs]8¾„oúÑ i>ÜxxÈó)ƒ ´æÁâØ$À‰vžŸf$Ž |ãw;ÀÁIJ»b` {¦Ó¤Ú$©YÀ‘n@Óïž«9J¼êG m¤ ܯ¹ÌW4€ÐÒÅÛ‡#褕Ÿn-?í|с¥÷Ú¹¬'´ÞÜ9ÓK `hê£SÄSà?7—Wí_´…óB›»:=Ãïq`<8ñÓŒÑlú2d¬ê³£hÖ[l|$vÝro~'R®‰§°ñmY ͧäP |PUª¹·:3Œ[Û{Xÿ ºâ@‚W–Äé u‚ ¯´*=íή.pûÒdt @G‰¬ s¸ ëÉücr ÞæÑ¨Ê@>¤¢Ö±. Þ'¯°ÌME[YéïĵÂCå½ Ué©Áû'Ê9%eÔðNU”ë‘ÌsD3/®+UI˜9h.WC”빓$#:pz:YÓ ¿xž* ³$Í +$kñAŠ‹†¢ Uê>¸)_š¬÷©ßAÂÔb9ÇU ¯¾á•9¯ÏÏ÷O÷¼¼Fähal1‰3Ì[Ïr•´UCksNÐ] R‘¸¥H+§Šé†c©vÖÞ0iÓ76s†î!§=ß ¼~Ô'°Ãmäoäš³ªøi1úÉ)³yV8 CLÄØÁ‘WYïi€H6ÖÑiámø^ÈY´°Ñ7¥Û*—Ñ©L«Qƒï—Ùrÿ ›£Ð*š¸ˆL©ˆ$ˆ ÷¾D§9È®«qbqC)–ˆïv´çñsÑVT­Ø, <àïºÀO«Jý·õ àfPìð .wFšir´þ’2_Y *Æ€x\« ì€9š@ Ž|F⇥ˆkZ@hÖÄ0t¿-<“‹qµ¾*ZL¤Ú)&BJpÓF5=$„at*Zš$’ÑtdûÝRI1 2މ$€$I$#‰SÞ’Hë¬ï;Á$¡t$’`<(ñÇt)$‡Ð.Êf¢X’Kt=Éé$‚ˆªè¢oÝëòI%Rgcª÷ŠyI%¡‰ÿ !ñ)´õ $¤ Ô’IIGÿÙ--- -- Library methods for handling LDAP. -- -- @author Patrik Karlsson -- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html -- -- Credit goes out to Martin Swende who provided me with the initial code that got me started writing this. -- -- Version 0.8 -- Created 01/12/2010 - v0.1 - Created by Patrik Karlsson -- Revised 01/28/2010 - v0.2 - Revised to fit better fit ASN.1 library -- Revised 02/02/2010 - v0.3 - Revised to fit OO ASN.1 Library -- Revised 09/05/2011 - v0.4 - Revised to include support for writing output to file, added decoding certain time -- formats -- Revised 10/29/2011 - v0.5 - Added support for performing wildcard searches via the substring filter. -- Revised 10/30/2011 - v0.6 - Added support for the ldap extensibleMatch filter type for searches -- Revised 04/04/2016 - v0.7 - Added support for searchRequest over upd ( udpSearchRequest ) - Tom Sellers -- Revised 07/11/2017 - v0.8 - Added support for decoding the objectSID Active Directory attribute - Tom Sellers -- local asn1 = require "asn1" local datetime = require "datetime" local io = require "io" local nmap = require "nmap" local stdnse = require "stdnse" local string = require "string" local stringaux = require "stringaux" local table = require "table" local comm = require "comm" _ENV = stdnse.module("ldap", stdnse.seeall) local ldapMessageId = 1 ERROR_MSG = {} ERROR_MSG[1] = "Initialization of LDAP library failed." ERROR_MSG[4] = "Size limit exceeded." ERROR_MSG[13] = "Confidentiality required" ERROR_MSG[32] = "No such object" ERROR_MSG[34] = "Invalid DN" ERROR_MSG[49] = "The supplied credential is invalid." ERRORS = { LDAP_SUCCESS = 0, LDAP_SIZELIMIT_EXCEEDED = 4 } --- Application constants -- @class table -- @name APPNO APPNO = { BindRequest = 0, BindResponse = 1, UnbindRequest = 2, SearchRequest = 3, SearchResponse = 4, SearchResDone = 5 } -- Filter operation constants FILTER = { _and = 0, _or = 1, _not = 2, equalityMatch = 3, substrings = 4, greaterOrEqual = 5, lessOrEqual = 6, present = 7, approxMatch = 8, extensibleMatch = 9 } -- Scope constants SCOPE = { base=0, one=1, sub= 2, children=3, default = 0 } -- Deref policy constants DEREFPOLICY = { never=0, searching=1, finding = 2, always=3, default = 0 } -- LDAP specific tag encoders local tagEncoder = {} tagEncoder['table'] = function(self, val) if (val._ldap == '\x0A') then local ival = self.encodeInt(val[1]) local len = self.encodeLength(#ival) return val._ldap .. len .. ival end if (val._ldaptype) then local len if val[1] == nil or #val[1] == 0 then return val._ldaptype .. '\0' else len = self.encodeLength(#val[1]) return val._ldaptype .. len .. val[1] end end local encVal = "" for _, v in ipairs(val) do encVal = encVal .. encode(v) -- todo: buffer? end local tableType = val._snmp or "\x30" return tableType .. self.encodeLength(#encVal) .. encVal end --- -- Encodes a given value according to ASN.1 basic encoding rules for SNMP -- packet creation. -- @param val Value to be encoded. -- @return Encoded value. function encode(val) local encoder = asn1.ASN1Encoder:new() local encValue encoder:registerTagEncoders(tagEncoder) encValue = encoder:encode(val) if encValue then return encValue end return '' end -- LDAP specific tag decoders local tagDecoder = {} tagDecoder["\x0A"] = function( self, encStr, elen, pos ) return self.decodeInt(encStr, elen, pos) end tagDecoder["\x8A"] = function( self, encStr, elen, pos ) return string.unpack("c" .. elen, encStr, pos) end -- null decoder tagDecoder["\x31"] = function( self, encStr, elen, pos ) return nil, pos end --- -- Decodes an LDAP packet or a part of it according to ASN.1 basic encoding -- rules. -- @param encStr Encoded string. -- @param pos Current position in the string. -- @return The decoded value(s). -- @return The position after decoding function decode(encStr, pos) -- register the LDAP specific tag decoders local decoder = asn1.ASN1Decoder:new() decoder:registerTagDecoders( tagDecoder ) return decoder:decode( encStr, pos ) end --- -- Decodes a sequence according to ASN.1 basic encoding rules. -- @param encStr Encoded string. -- @param len Length of sequence in bytes. -- @param pos Current position in the string. -- @return The decoded sequence as a table. -- @return The position after decoding. local function decodeSeq(encStr, len, pos) local seq = {} local sPos = 1 if #encStr - pos + 1 < len then return seq, nil end local sStr, newpos = string.unpack("c" .. len, encStr, pos) while (sPos < len) do local newSeq newSeq, sPos = decode(sStr, sPos) table.insert(seq, newSeq) end return seq, newpos end -- Encodes an LDAP Application operation and its data as a sequence -- -- @param appno LDAP application number -- @see APPNO -- @param isConstructed boolean true if constructed, false if primitive -- @param data string containing the LDAP operation content -- @return string containing the encoded LDAP operation function encodeLDAPOp( appno, isConstructed, data ) local encoded_str = "" local asn1_type = asn1.BERtoInt( asn1.BERCLASS.Application, isConstructed, appno ) encoded_str = encode( { _ldaptype = string.pack("B", asn1_type), data } ) return encoded_str end --- Performs an LDAP Search request -- -- This function has a concept of softerrors which populates the return tables error information -- while returning a true status. The reason for this is that LDAP may return a number of records -- and then finish off with an error like SIZE LIMIT EXCEEDED. We still want to return the records -- that were received prior to the error. In order to achieve this and not terminating the script -- by returning a false status a true status is returned together with a table containing all searchentries. -- This table has the errorMessage and resultCode entries set with the error information. -- As a try won't catch this error it's up to the script to do so. See ldap-search.nse for an example. -- -- @param socket socket already connected to the ldap server -- @param params table containing at least scope, derefPolicy, baseObject -- the field maxObjects may also be included to restrict the amount of records returned -- @return success true or false. -- @return searchResEntries containing results or a string containing error message function searchRequest( socket, params ) local searchResEntries = { errorMessage="", resultCode = 0} local catch = function() socket:close() stdnse.debug1("SearchRequest failed") end local try = nmap.new_try(catch) local attributes = params.attributes local request = encode(params.baseObject) local attrSeq = '' local requestData, messageSeq, data local maxObjects = params.maxObjects or -1 local encoder = asn1.ASN1Encoder:new() local decoder = asn1.ASN1Decoder:new() encoder:registerTagEncoders(tagEncoder) decoder:registerTagDecoders(tagDecoder) request = request .. encode( { _ldap='\x0A', params.scope } )--scope request = request .. encode( { _ldap='\x0A', params.derefPolicy } )--derefpolicy request = request .. encode( params.sizeLimit or 0)--sizelimit request = request .. encode( params.timeLimit or 0)--timelimit request = request .. encode( params.typesOnly or false)--TypesOnly if params.filter then request = request .. createFilter( params.filter ) else request = request .. encode( { _ldaptype='\x87', "objectclass" } )-- filter : string, presence end if attributes~= nil then for _,attr in ipairs(attributes) do attrSeq = attrSeq .. encode(attr) end end request = request .. encoder:encodeSeq(attrSeq) requestData = encodeLDAPOp(APPNO.SearchRequest, true, request) messageSeq = encode(ldapMessageId) ldapMessageId = ldapMessageId +1 messageSeq = messageSeq .. requestData data = encoder:encodeSeq(messageSeq) try( socket:send( data ) ) data = "" while true do local len, pos, messageId = 0, 2, -1 local tmp = "" local _, objectName, attributes, ldapOp local attributes local searchResEntry = {} if ( maxObjects == 0 ) then break elseif ( maxObjects > 0 ) then maxObjects = maxObjects - 1 end if data:len() > 6 then len, pos = decoder.decodeLength( data, pos ) else data = data .. try( socket:receive() ) len, pos = decoder.decodeLength( data, pos ) end -- pos should be at the right position regardless if length is specified in 1 or 2 bytes while ( len + pos - 1 > data:len() ) do data = data .. try( socket:receive() ) end messageId, pos = decode( data, pos ) tmp, pos = string.unpack("B", data, pos) len, pos = decoder.decodeLength( data, pos ) ldapOp = asn1.intToBER( tmp ) searchResEntry = {} if ldapOp.number == APPNO.SearchResDone then searchResEntry.resultCode, pos = decode( data, pos ) -- errors may occur after a large amount of data has been received (eg. size limit exceeded) -- we want to be able to return the data received prior to this error to the user -- however, we also need to alert the user of the error. This is achieved through "softerrors" -- softerrors populate the error fields of the table while returning a true status -- this allows for the caller to output data while still being able to catch the error if ( searchResEntry.resultCode ~= 0 ) then local error_msg searchResEntry.matchedDN, pos = decode( data, pos ) searchResEntry.errorMessage, pos = decode( data, pos ) error_msg = ERROR_MSG[searchResEntry.resultCode] -- if the table is empty return a hard error if #searchResEntries == 0 then return false, string.format("Code: %d|Error: %s|Details: %s", searchResEntry.resultCode, error_msg or "", searchResEntry.errorMessage or "" ) else searchResEntries.errorMessage = string.format("Code: %d|Error: %s|Details: %s", searchResEntry.resultCode, error_msg or "", searchResEntry.errorMessage or "" ) searchResEntries.resultCode = searchResEntry.resultCode return true, searchResEntries end end break end searchResEntry.objectName, pos = decode( data, pos ) if ldapOp.number == APPNO.SearchResponse then searchResEntry.attributes, pos = decode( data, pos ) table.insert( searchResEntries, searchResEntry ) end if data:len() > pos then data = data:sub(pos) else data = "" end end return true, searchResEntries end --- Performs an LDAP Search request over UDP -- -- This function has a concept of softerrors which populates the return tables error information -- while returning a true status. The reason for this is that LDAP may return a number of records -- and then finish off with an error like SIZE LIMIT EXCEEDED. We still want to return the records -- that were received prior to the error. In order to achieve this and not terminating the script -- by returning a false status a true status is returned together with a table containing all searchentries. -- This table has the errorMessage and resultCode entries set with the error information. -- As a try won't catch this error it's up to the script to do so. See ldap-search.nse for an example. -- -- @param host The host to connect to -- @param port The port on the host -- @param params table containing at least scope, derefPolicy, baseObject -- the field maxObjects may also be included to restrict the amount of records returned -- @return success true or false. -- @return searchResEntries containing results or a string containing error message function udpSearchRequest( host, port, params ) local searchResEntries = { errorMessage="", resultCode = 0} local catch = function() stdnse.debug1("udpSearchRequest failed") end local try = nmap.new_try(catch) local attributes = params.attributes local request = encode(params.baseObject) local attrSeq = '' local requestData, messageSeq, data local maxObjects = params.maxObjects or -1 local encoder = asn1.ASN1Encoder:new() local decoder = asn1.ASN1Decoder:new() encoder:registerTagEncoders(tagEncoder) decoder:registerTagDecoders(tagDecoder) request = request .. encode( { _ldap='\x0A', params.scope } )--scope request = request .. encode( { _ldap='\x0A', params.derefPolicy } )--derefpolicy request = request .. encode( params.sizeLimit or 0)--sizelimit request = request .. encode( params.timeLimit or 0)--timelimit request = request .. encode( params.typesOnly or false)--TypesOnly if params.filter then request = request .. createFilter( params.filter ) else request = request .. encode( { _ldaptype='\x87', "objectclass" } )-- filter : string, presence end if attributes~= nil then for _,attr in ipairs(attributes) do attrSeq = attrSeq .. encode(attr) end end request = request .. encoder:encodeSeq(attrSeq) requestData = encodeLDAPOp(APPNO.SearchRequest, true, request) messageSeq = encode(ldapMessageId) ldapMessageId = ldapMessageId +1 messageSeq = messageSeq .. requestData data = encoder:encodeSeq(messageSeq) local status, response = comm.exchange(host, port, data) while true do local len, pos, messageId = 0, 0, -1 local tmp = "" local _, objectName, attributes, ldapOp local attributes local searchResEntry = {} if ( maxObjects == 0 ) then break elseif ( maxObjects > 0 ) then maxObjects = maxObjects - 1 end tmp, pos = string.unpack("B", response, pos) len, pos = decoder.decodeLength( response, pos ) messageId, pos = decode( response, pos ) tmp, pos = string.unpack("B", response, pos) len, pos = decoder.decodeLength( response, pos ) ldapOp = asn1.intToBER( tmp ) searchResEntry = {} if ldapOp.number == APPNO.SearchResDone then searchResEntry.resultCode, pos = decode( response, pos ) -- errors may occur after a large amount of response has been received (eg. size limit exceeded) -- we want to be able to return the response received prior to this error to the user -- however, we also need to alert the user of the error. This is achieved through "softerrors" -- softerrors populate the error fields of the table while returning a true status -- this allows for the caller to output response while still being able to catch the error if ( searchResEntry.resultCode ~= 0 ) then local error_msg searchResEntry.matchedDN, pos = decode( response, pos ) searchResEntry.errorMessage, pos = decode( response, pos ) error_msg = ERROR_MSG[searchResEntry.resultCode] -- if the table is empty return a hard error if #searchResEntries == 0 then return false, string.format("Code: %d|Error: %s|Details: %s", searchResEntry.resultCode, error_msg or "", searchResEntry.errorMessage or "" ) else searchResEntries.errorMessage = string.format("Code: %d|Error: %s|Details: %s", searchResEntry.resultCode, error_msg or "", searchResEntry.errorMessage or "" ) searchResEntries.resultCode = searchResEntry.resultCode return true, searchResEntries end end break end searchResEntry.objectName, pos = decode( response, pos ) if ldapOp.number == APPNO.SearchResponse then searchResEntry.attributes, pos = decode( response, pos ) table.insert( searchResEntries, searchResEntry ) end if response:len() > pos then response = response:sub(pos) else response = "" end end return true, searchResEntries end --- Attempts to bind to the server using the credentials given -- -- @param socket socket already connected to the ldap server -- @param params table containing version, username and password -- @return success true or false -- @return err string containing error message function bindRequest( socket, params ) local catch = function() socket:close() stdnse.debug1("bindRequest failed") end local try = nmap.new_try(catch) local ldapAuth = encode( { _ldaptype = '\x80', params.password } ) local bindReq = encode( params.version ) .. encode( params.username ) .. ldapAuth local ldapMsg = encode(ldapMessageId) .. encodeLDAPOp( APPNO.BindRequest, true, bindReq ) local packet local pos, packet_len, resultCode, tmp, len, _ local response = {} local encoder = asn1.ASN1Encoder:new() local decoder = asn1.ASN1Decoder:new() encoder:registerTagEncoders(tagEncoder) decoder:registerTagDecoders(tagDecoder) packet = encoder:encodeSeq( ldapMsg ) ldapMessageId = ldapMessageId +1 try( socket:send( packet ) ) packet = try( socket:receive() ) packet_len, pos = decoder.decodeLength( packet, 2 ) response.messageID, pos = decode( packet, pos ) tmp, pos = string.unpack("B", packet, pos) len, pos = decoder.decodeLength( packet, pos ) response.protocolOp = asn1.intToBER( tmp ) if response.protocolOp.number ~= APPNO.BindResponse then return false, string.format("Received incorrect Op in packet: %d, expected %d", response.protocolOp.number, APPNO.BindResponse) end response.resultCode, pos = decode( packet, pos ) if ( response.resultCode ~= 0 ) then local error_msg response.matchedDN, pos = decode( packet, pos ) response.errorMessage, pos = decode( packet, pos ) error_msg = ERROR_MSG[response.resultCode] return false, string.format("\n Error: %s\n Details: %s", error_msg or "Unknown error occurred (code: " .. response.resultCode .. ")", response.errorMessage or "" ) else return true, "Success" end end --- Performs an LDAP Unbind -- -- @param socket socket already connected to the ldap server -- @return success true or false -- @return err string containing error message function unbindRequest( socket ) local ldapMsg, packet local catch = function() socket:close() stdnse.debug1("bindRequest failed") end local try = nmap.new_try(catch) local encoder = asn1.ASN1Encoder:new() encoder:registerTagEncoders(tagEncoder) ldapMessageId = ldapMessageId +1 ldapMsg = encode( ldapMessageId ) .. encodeLDAPOp( APPNO.UnbindRequest, false, nil) packet = encoder:encodeSeq( ldapMsg ) try( socket:send( packet ) ) return true, "" end --- Creates an ASN1 structure from a filter table -- -- @param filter table containing the filter to be created -- @return string containing the ASN1 byte sequence function createFilter( filter ) local asn1_type = asn1.BERtoInt( asn1.BERCLASS.ContextSpecific, true, filter.op ) local filter_str = "" if type(filter.val) == 'table' then for _, v in ipairs( filter.val ) do filter_str = filter_str .. createFilter( v ) end else local obj = encode( filter.obj ) local val = '' if ( filter.op == FILTER['substrings'] ) then local tmptable = stringaux.strsplit('*', filter.val) local tmp_result = '' if (#tmptable <= 1 ) then -- 0x81 = 10000001 = 10 0 00001 -- hex binary Context Primitive value Field: Sequence Value: 1 (any / any position in string) tmp_result = string.pack('Bs1', 0x81, filter.val) else for indexval, substr in ipairs(tmptable) do if (indexval == 1) and (substr ~= '') then -- 0x81 = 10000000 = 10 0 00000 -- hex binary Context Primitive value Field: Sequence Value: 0 (initial / match at start of string) tmp_result = '\x80' .. string.char(#substr) .. substr end if (indexval ~= #tmptable) and (indexval ~= 1) and (substr ~= '') then -- 0x81 = 10000001 = 10 0 00001 -- hex binary Context Primitive value Field: Sequence Value: 1 (any / match in any position in string) tmp_result = tmp_result .. string.pack('Bs1', 0x81, substr) end if (indexval == #tmptable) and (substr ~= '') then -- 0x82 = 10000010 = 10 0 00010 -- hex binary Context Primitive value Field: Sequence Value: 2 (final / match at end of string) tmp_result = tmp_result .. string.pack('Bs1', 0x82, substr) end end end val = asn1.ASN1Encoder:encodeSeq( tmp_result ) elseif ( filter.op == FILTER['extensibleMatch'] ) then local tmptable = stringaux.strsplit(':=', filter.val) local tmp_result = '' local OID, bitmask if ( tmptable[1] ~= nil ) then OID = tmptable[1] else return false, ("ERROR: Invalid extensibleMatch query format") end if ( tmptable[2] ~= nil ) then bitmask = tmptable[2] else return false, ("ERROR: Invalid extensibleMatch query format") end tmp_result = string.pack('Bs1 Bs1 Bs1 Bs1', -- Format and create matchingRule using OID -- 0x81 = 10000001 = 10 0 00001 -- hex binary Context Primitive value Field: matchingRule Value: 1 0x81, OID, -- Format and create type using ldap attribute -- 0x82 = 10000010 = 10 0 00010 -- hex binary Context Primitive value Field: Type Value: 2 0x82, filter.obj, -- Format and create matchValue using bitmask -- 0x83 = 10000011 = 10 0 00011 -- hex binary Context Primitive value Field: matchValue Value: 3 0x83, bitmask, -- Format and create dnAttributes, defaulting to false -- 0x84 = 10000100 = 10 0 00100 -- hex binary Context Primitive value Field: dnAttributes Value: 4 -- 0x00 = boolean value, in this case false 0x84, '\x00') -- Format the overall extensibleMatch block -- 0xa9 = 10101001 = 10 1 01001 -- hex binary Context Constructed Field: Filter Value: 9 (extensibleMatch) return '\xa9' .. asn1.ASN1Encoder.encodeLength(#tmp_result) .. tmp_result else val = encode( filter.val ) end filter_str = filter_str .. obj .. val end return encode( { _ldaptype=string.pack("B", asn1_type), filter_str } ) end --- Converts a search result as received from searchRequest to a "result" table -- -- Does some limited decoding of LDAP attributes -- -- TODO: Add decoding of missing attributes -- TODO: Add decoding of userParameters -- TODO: Add decoding of loginHours -- -- @param searchEntries table as returned from searchRequest -- @return table suitable for stdnse.format_output function searchResultToTable( searchEntries ) local result = {} for _, v in ipairs( searchEntries ) do local result_part = {} if v.objectName and v.objectName:len() > 0 then result_part.name = string.format("dn: %s", v.objectName) else result_part.name = "" end local attribs = {} if ( v.attributes ~= nil ) then for _, attrib in ipairs( v.attributes ) do for i=2, #attrib do -- do some additional Windows decoding if ( attrib[1] == "objectSid" ) then table.insert( attribs, string.format( "%s: %s", attrib[1], convertObjectSid( attrib[i] ) ) ) elseif ( attrib[1] == "objectGUID") then local o = {string.unpack(("B"):rep(16), attrib[i] )} table.insert( attribs, string.format( "%s: %x%x%x%x-%x%x-%x%x-%x%x-%x%x%x%x%x%x", attrib[1], o[4], o[3], o[2], o[1], table.unpack(o, 5, 16))) elseif ( attrib[1] == "lastLogon" or attrib[1] == "lastLogonTimestamp" or attrib[1] == "pwdLastSet" or attrib[1] == "accountExpires" or attrib[1] == "badPasswordTime" ) then table.insert( attribs, string.format( "%s: %s", attrib[1], convertADTimeStamp(attrib[i]) ) ) elseif ( attrib[1] == "whenChanged" or attrib[1] == "whenCreated" or attrib[1] == "dSCorePropagationData" ) then table.insert( attribs, string.format( "%s: %s", attrib[1], convertZuluTimeStamp(attrib[i]) ) ) else table.insert( attribs, string.format( "%s: %s", attrib[1], attrib[i] ) ) end end end table.insert( result_part, attribs ) end table.insert( result, result_part ) end return result end --- Saves a search result as received from searchRequest to a file -- -- Does some limited decoding of LDAP attributes -- -- TODO: Add decoding of missing attributes -- TODO: Add decoding of userParameters -- TODO: Add decoding of loginHours -- -- @param searchEntries table as returned from searchRequest -- @param filename the name of a save to save results to -- @return table suitable for stdnse.format_output function searchResultToFile( searchEntries, filename ) local f = io.open( filename, "w") if ( not(f) ) then return false, ("ERROR: Failed to open file (%s)"):format(filename) end -- Build table structure. Using a multi pass approach ( build table then populate table ) -- because the objects returned may not necessarily have the same number of attributes -- making single pass CSV output generation problematic. -- Unfortunately the searchEntries table passed to this function is not organized in a -- way that make particular attributes for a given hostname directly addressable. -- -- At some point restructuring the searchEntries table may be a good optimization target -- build table of attributes local attrib_table = {} for _, v in ipairs( searchEntries ) do if ( v.attributes ~= nil ) then for _, attrib in ipairs( v.attributes ) do for i=2, #attrib do if ( attrib_table[attrib[1]] == nil ) then attrib_table[attrib[1]] = '' end end end end end -- build table of hosts local host_table = {} for _, v in ipairs( searchEntries ) do if v.objectName and v.objectName:len() > 0 then local host = {} if v.objectName and v.objectName:len() > 0 then -- use a copy of the table here, assigning attrib_table into host_table -- links the values so setting it for one host changes the specific attribute -- values for all hosts. host_table[v.objectName] = {attributes = copyTable(attrib_table) } end end end -- populate the host table with values for each attribute that has valid data for _, v in ipairs( searchEntries ) do if ( v.attributes ~= nil ) then for _, attrib in ipairs( v.attributes ) do for i=2, #attrib do -- do some additional Windows decoding if ( attrib[1] == "objectSid" ) then host_table[string.format("%s", v.objectName)].attributes[attrib[1]] = string.format( "%s", convertObjectSid(attrib[i])) elseif ( attrib[1] == "objectGUID") then local o = {string.unpack(("B"):rep(16), attrib[i] )} host_table[string.format("%s", v.objectName)].attributes[attrib[1]] = string.format( "%s: %x%x%x%x-%x%x-%x%x-%x%x-%x%x%x%x%x%x", attrib[1], o[4], o[3], o[2], o[1], table.unpack(o, 5, 16)) elseif ( attrib[1] == "lastLogon" or attrib[1] == "lastLogonTimestamp" or attrib[1] == "pwdLastSet" or attrib[1] == "accountExpires" or attrib[1] == "badPasswordTime" ) then host_table[string.format("%s", v.objectName)].attributes[attrib[1]] = convertADTimeStamp(attrib[i]) elseif ( attrib[1] == "whenChanged" or attrib[1] == "whenCreated" or attrib[1] == "dSCorePropagationData" ) then host_table[string.format("%s", v.objectName)].attributes[attrib[1]] = convertZuluTimeStamp(attrib[i]) else host_table[v.objectName].attributes[attrib[1]] = string.format( "%s", attrib[i] ) end end end end end -- write the new, fully populated table out to CSV -- initialize header row local output = "\"name\"" for attribute, value in pairs(attrib_table) do output = output .. ",\"" .. attribute .. "\"" end output = output .. "\n" -- gather host data from fields, add to output. for name, attribs in pairs(host_table) do output = output .. "\"" .. name .. "\"" local host_attribs = attribs.attributes for attribute, value in pairs(attrib_table) do output = output .. ",\"" .. host_attribs[attribute] .. "\"" end output = output .. "\n" end -- write the output to file if ( not(f:write( output .."\n" ) ) ) then f:close() return false, ("ERROR: Failed to write file (%s)"):format(filename) end f:close() return true end --- Extract naming context from a search response -- -- @param searchEntries table containing searchEntries from a searchResponse -- @param attributeName string containing the attribute to extract -- @return table containing the attribute values function extractAttribute( searchEntries, attributeName ) local attributeTbl = {} for _, v in ipairs( searchEntries ) do if ( v.attributes ~= nil ) then for _, attrib in ipairs( v.attributes ) do local attribType = attrib[1] for i=2, #attrib do if ( attribType:upper() == attributeName:upper() ) then table.insert( attributeTbl, attrib[i]) end end end end end return ( #attributeTbl > 0 and attributeTbl or nil ) end --- Convert Microsoft Active Directory timestamp format to a human readable form -- These values store time values in 100 nanoseconds segments from 01-Jan-1601 -- -- @param timestamp Microsoft Active Directory timestamp value -- @return string containing human readable form function convertADTimeStamp(timestamp) local result = 0 -- Windows cannot represent this time, so we pre-calculated it: -- seconds since 1601/1/1 adjusted for local offset local base_time = -11644473600 - datetime.utc_offset() timestamp = tonumber(timestamp) if (timestamp and timestamp > 0) then -- The result value was 3036 seconds off what Microsoft says it should be. -- I have been unable to find an explanation for this, and have resorted to -- manually adjusting the formula. result = ( timestamp // 10000000 ) - 3036 result = result + base_time result = datetime.format_timestamp(result, 0) else result = 'Never' end return result end --- Converts a non-delimited Zulu timestamp format to a human readable form -- For example 20110904003302.0Z becomes 2001/09/04 00:33:02 UTC -- -- -- @param timestamp in Zulu format without separators -- @return string containing human readable form function convertZuluTimeStamp(timestamp) if ( type(timestamp) == 'string' and string.sub(timestamp,-3) == '.0Z' ) then local year = string.sub(timestamp,1,4) local month = string.sub(timestamp,5,6) local day = string.sub(timestamp,7,8) local hour = string.sub(timestamp,9,10) local mins = string.sub(timestamp,11,12) local secs = string.sub(timestamp,13,14) local result = year .. "/" .. month .. "/" .. day .. " " .. hour .. ":" .. mins .. ":" .. secs .. " UTC" return result else return 'Invalid date format' end end --- Converts the objectSid Active Directory attribute -- from hex to a human readable string -- -- Example: 1-5-21-542885397-2936741293-3965599772-500 -- -- @param hex string form of objectSid from LDAP response -- @return string containing human readable form function convertObjectSid(data) local pos, revision, auth, sub_auth_size, sub_auth, result revision, pos = string.unpack('I1', data, 1) sub_auth_size, pos = string.unpack('I1', data, pos) auth, pos = string.unpack('>I6', data, pos) sub_auth = '' local tmp local cnt = 0 while (cnt < sub_auth_size) do tmp, pos = string.unpack('