skipfish-2.10b/0000750036502000116100000000000012057401034012363 5ustar heinennengskipfish-2.10b/assets/0000750036502000116100000000000012057375163013702 5ustar heinennengskipfish-2.10b/assets/p_serv.png0000440036502000116100000000455512057375131015711 0ustar heinennengPNG  IHDR szzgAMAܲbKGD pHYsHHFk>IDATXåil\o7ό3ۉmbY4`Jn*~@$EJUPR -%@4$&N6}3@6+]齣s;<\5jھ~[ssSiBOY7ǽ^p4mo{ܿt$]ד/zq<鸞-{}"v^T#ٵۿq=I/X27bA.23e*SsK [<\nyEK٫/~߻Kp@QLv=٥!T!$Ų|Pq=Ar([.44 MUu愑lCN ;5tUZt{~/?}pBH|!<hHFO,N&gR6]`Uqzw<񡮗T#ڼQSYlף. j-7_ @B+7f 9 xmkv5dr&;[R,2ʦ $h22VU\MQe*Ua.UD$:|mޱp*UUhnS,9|a:q|DALEQ@EQ gرi %EU4Uamk'á!fsaZ)pYtMl:4$]@䐈_>Uw7R K-l.S#18:OR,;M_S#`M;杛>T:Gp\B2-]J] Nv Tq:nhcdbC]Cضiyx҃M78ñq?>N"q|קq=\d& rV%2ݩ)>jx@U&Rşu3IF@#7ÔLk]Dyꃋ0|HJ5XjXUQp]ߗ{PUi<_KQU`Pcf!G`>)H0vjb)@^nhh[v/JOVfuZښGX:|2/O87b>A~0gp=/ ,-aAC6&1Y\CmM;DW5j!:,e/A:zGx͓4&#c0+͢zUq4UAUUZkij6f`:#ciΌ.P4TQ,9Bk*(*Ê(;77=Az srTTF){2M5֦(6\瘘ɡk MQN4dz.=]S"aumIzƙ:f>Pާ "ό5&h2.溭+H3;_1az@@0mZVc5 kTQc/? b/#1+F \5$"ab&k$aZV4Z8NE. IfOtZY[]^5!"a kY:I2FTp`e}Y볩ѷ Rp驸+Yi#n~wwwܵ{- R y>+(mf򢳳sd<rrW@hD׮hjȏx./9v`vz߶J9 Cs73M͍w[toٽBfr,='Պ_)?֭WW'mt,gytCCb>/[o˥A+n]-749s OKKƝ;l>b+Os)w}Be6mںֶp*?~jرL0O*OPVbZ<>r>۶g*_ ?H'NdIENDB`skipfish-2.10b/assets/i_high.png0000440036502000116100000000141512057375131015632 0ustar heinennengPNG  IHDRagAMA7bKGD pHYsHHFk>oIDAT8˥OSQカ.` @qM1qb3q qą @ ,`B@-ZZx={~|JD ך2dwR ҼӀSaC?''HDIDAT8=JAgCZp 6nAM, K{ 6MQC"L)Xù??&L8"!l6hhֳVn&''gW]1Q[FB)aAtK& RtBi#6WI*[UBZ< @[Li>>;ޅMMraUR1/|]IENDB`skipfish-2.10b/assets/i_note.png0000440036502000116100000000651112057375131015662 0ustar heinennengPNG  IHDRagAMA|Q =iCCPiccxڝSgTS=BKKoR RBTi@숨"q"Ay((6T}7o9g}>F`DdJ<6.'w T @- m@n8P $ B2r22 t%[j;eOv$(S*@@&X`(ʑs`̔`)d` SGE3(xW\!Sd咔Tn!\]x87CP؄ ee3FvD9;:;8:|?E񋖴e /B_TBfgk+ m_ _׃  2r<[&q?.wL'bPGKĹi ˒$ IHk`~B[P. %w߂1w0hْ 4P6h>؀#;x̆P8XBHLC.,UP%BZF8-p<^0 o`A2DX6b"ֈ#Ef!~H0!H "ERd5R#U^9E.!==F~C>@٨jڡ\ Bh G h%ZBѳڋ>G03l0.Bx,c˱b6b#{";!0 $,",'̈́ Ba$nD>1B%+uc[!\H8Ri D:C!d6ٚA% ry;4:yBP)xR@\ RƩjTS5*.Qkmԫ8MfNEhhFyC+:nDw%JaEz=Ca1J~=+&ib3 z9c; _EBZY U| գWUGԨjfj<5rjjwY/i544D4i01VjYYlۜgK߱٣34545Ojr0qpns>Lћ=Ejkh4km8ndn4ר1͘klŸx$dI}S)4ti[3sf-fCZ||L OE57-I\t˝׬P+'Tj֨zu44ii50lmrlll9-/L6u}wϰ0ۡ7G+GcWLor ]3:B:;}rvq;7:$pesø܋DW'\߻9)܎n~}hLڙFY4xx>2yy z[zy~c#9[;vi{o?$L 10(pS_ȯvlvG#(2*IU<- 999-(yr`GryPGTԊ OR%y;mzh􉌘LJfbq4]ڑ#z-ںhT$Fg* Ki\˙S.7:hz4k]BX"\Ҿp骥}˼],OZ޾xEኁ+J_S}Ay1 W XPR$/}uuu맯߾sr}IERaofbC2]Ioot\<s--.zbFmmmMo*VOuw)y}׮zKv#swo}}9Fv~N~:],k@ Ç]FƽMpXy>t(h?8:V܌4/nmImmk9>x{{۱mDI͓eh OM?=vFvflŞ}> uzwq%K/s/\qu'u;w7_uzZ[̞S={M+=; wz˸~+?R{TXqϖ?7:zA?q)iŠ`Љak=x.{>>R/;^XW_FcG^_NVJ3^=~fm;wsw~08姶ANdNL%c3 cHRMz%u0`:o_FbKGD pHYs  dIDAT8˥MHTQsf掎̤#j֢l#eFA+W)ejIh#BUi&~"G;C4*33㽧Ew\'E@a泐BF֫gHw@|nj3L:z`۾گt ӂqb3/*䲣B:WAH1L̏_Zh?{ Qå ΍mcG!.}t-MN {A XÑJg$оW-C2&l2XؘdsQMS ʱFj0pԝ`e9I4yf HnDehі!QY3_E/ !5J|'G߀ FG(*#M5@E4 ʶ3"`7 T2 K m6JnVf0 xNbEb 0^}NMX KPģAs)wާ)p]䙪]Jc@сf]'KC"ٮ0% (:Vޔ1!ԮeKh­"K|IQƅ]^/#3M)saIENDB`skipfish-2.10b/assets/n_missing.png0000440036502000116100000000157612057375131016401 0ustar heinennengPNG  IHDRagAMAܲbKGD pHYsHHFk>IDAT8u_lSvkenM$,c<6%2qbf*8_D}3:!!ÆlL^%005"h1!X]- O$|sr,\j ?NwKݻ5B-z@O䏛78>~?t @g祧zzHmBJ%TAظgSgО9p(CCb/6А? rK[?/2:*in~)uteM û l6jN8,ϤZלpI9qBi[ +ѨnN%ٵKn.5RW;;++HD>OUT|rXx!H,&K)ʃ'E@U^>' USXXbs=Z&`.$Vm6o^/n͋Պ԰L2H|GJ0 6v;c tԬJcHKH$"%o~ $/۴s >KvjkTu܆ 4mxިrMM]I7 @9x>ჭ;<4@!d]7nt=Y ɤӎ`6P5:eDǙ(?=3g슲Mhj ?tHI}Nx0u >_EFP&IENDB`skipfish-2.10b/assets/n_clone.png0000440036502000116100000000156212057375131016023 0ustar heinennengPNG  IHDRagAMAܲbKGD pHYsHHFk>IDAT8˅_lSu?O/hu[;.\3 F^'!1& _45Đ"$0LPؘ &X6Ү{d*r~s=pD{qKDﻟf m@ߴu}PXSº>( _\~#AO;ɞdO+K^^yn[Zc/O%Qp:fna:VlS]k92iʉ\}X5aJ"=T«ٴ{7k,KL=1q~C! 2pK5Wdn-LTvw٭Y\;Ox%[,;EoՁZ]4Ndje qD*pOIZ6޼y"BBw~"%HfꂃgCǾ(|x״8s!,02hV@?ɥ2v9$爵ud"5kr;SF 8,A ,];K}6˰P4I,(j >6DJ #\iF~">_R]nJYXtdYZLRfc#߸z.?7帀$uNEQ$Jeta8{ډ7^Ľ(kٓ>q4E"[H$+ؕmRii*NSlٹ.+a'qg *<"4IENDB`skipfish-2.10b/assets/n_failed.png0000440036502000116100000000124512057375131016145 0ustar heinennengPNG  IHDRagAMAܲbKGD pHYsHHFk>5IDAT8˕KQ;FyiXvv}-҅k nR(gJЅP*f!mS, aFɌ1ɼnI|u.;߅KKK/l 3F unnN*iuQ.vL\Vn|o?w M ( (> }k#Q}pqj_3V‡o|n PՊSqف}@0HJALT*3sp`#~п0ӳrwҺ/HF6aڥni@ "9N8T*qi i D TbX H >sAG" D՞=/q[lnŢc񳭢w"$ 4pxUQ"ԻśaB?^@4)vڡWzkyX0(zR+j( ,݇`YV>L&,=%q ?\7`lhgt:9/S?1Ms;J \pP(UU\=B'5 IENDB`skipfish-2.10b/assets/n_unlinked.png0000440036502000116100000000132412057375131016530 0ustar heinennengPNG  IHDRagAMAܲbKGD pHYsHHFk>dIDAT8˝SKa~pOs#JuJ."bUtqs(d;8H]zS%4p5r_+Vl>Syd2"NOOQT@c JYZZz_\\|[__^%8(J8::eYp]1(csqqqX,pAVs4P(mxP(EQ`fnaa  hr9I 2cB@UՆē9PUUoooY91Bz?ӓjiidYi殢(!޿~  B0MW6xrrad2D|ppptSssst:A)@rt:1??CïhMRi?pImIENDB`skipfish-2.10b/assets/n_collapsed.png0000440036502000116100000000103512057375131016664 0ustar heinennengPNG  IHDRagAMAܲbKGD pHYsHHFk>IDAT8˥MkSAwfromV*TL"tPu!Bp] ťK7BqnP0P*PKĴ;HRr,f1sys璢OWnEQBW0/ߘ04a}Eamysީ*i0rm&D4R9>pX6TK'z=8m}oWa}Ipf~{b̓hU~f@Zl,[#Bazb"UJLQP)xEkd?nmswpÁ0JH#p(CIENDB`skipfish-2.10b/assets/n_children.png0000440036502000116100000000153512057375131016513 0ustar heinennengPNG  IHDRagAMAܲbKGD pHYsHHFk>IDAT8ˍ]lSu?֮nuFa@SP2/t#sxǕfHbą+oD\S\$,a.es`EI{MN0C+fi9.MԄ\pcM苖hfrkh,Yگ En^82aheE%aN"[OܿE+{w7 ,_}h]eMeڂT9zz+9 x*#>OwUo}mVJđ*kb;7~{g1eSՇW5k!oS$SWdwA'"0 w`i.}ʶ2^l,^;T2rZ2smѭMS[SrvKGlp](#w޿;g/CFA_nC9Yd!ORf[I.͌؜"…ۜ{۪Eb1H?d8ral cؗsXH(evJM Lq0JT{ů/X$H"YIDATXõMkX}Xر+pHKt,/(t($Bi lU}00P$LЦNjNcXu,R )rltW{$7[^^8ޫh/^K0MB^&84)%AiWNG]]]}5Ll|A{vdYeiiG~))%BTUeee<ǓV) fI\N`Vb}1Ld||\{Zq E,J\W^Ų,0M˲"yP(Ԟ={v"pA"'۷oG AViϟ?_w^Oz/l9==q,.m\Bu]fE9v=ydͶm^_RJE\.ST888`jj ۶uV>Z !类eYQOA@ѠZjw^ Mi|~,ad2 @QTUEA.#S*o ĉ%F% ht!B8zZP.#`0NaZ-Z\J>|L&yAMQ4Md2r9T5=k׮%$8hy^s kLLL<) PThZm7o&3Lt .¤y&c%^Bn*w |!jO>E}6|xxJ©xrrRJ R εmׯ'|}Oa1::(yX ~;w9:a$aRJTUMwMW !Ƕm8lvq,N$B\q|avݓ@$nh( .|\.62M3Z,ܭeYFGGS=j5:$pppnGGGQ/qLJoRDPݻwEIfffa0111{}$yK fSz ]pǥRVn" :| !XXXGsӏ$q!!^BbA\M9F666q?aṍn_BRH~1@@bNߖ* HIENDB`skipfish-2.10b/assets/COPYING0000440036502000116100000001724012057375131014733 0ustar heinennengIcons used in HTML reports are copyrighted by the Crystal Project, and distributed under terms and conditions of the GNU Lesser General Public License. See http://www.everaldo.com/crystal/ for details. GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. skipfish-2.10b/assets/i_low.png0000440036502000116100000000137612057375131015522 0ustar heinennengPNG  IHDRagAMA7bKGD pHYsHHFk>`IDAT8˥KTQ?LoriFK( ZEj m"~%ml$nIbZF :3c~5̻-rp>{~1@s<<2@.G=i1)! U V9=" #Ooܼdv(6~B4*6تT0YWjGMJ6mFwg+oW.T܎巼9klI~r;+`shӇmHpo`&a]Հ <7GXL V3qν) ޖ(AoPNWC}ą2$ ( r dBȄPX& D kBbi!\g75!x hV`T@(Nm5̍ױ ``VeTK7 `L'I$2.Cϰɮ`t`(>ŦhbpdS s+F?V ]ST[ @Dr{ݎQS'T-&~M HmR qUu@~XGbuuZ&&^2זIw$hՏ"zTXtSoftwarexsLOJUMLO JML/Ԯ MIENDB`skipfish-2.10b/assets/p_pinfo.png0000440036502000116100000000417512057375131016043 0ustar heinennengPNG  IHDR szzgAMA cHRMz%%m_l<XxbKGD pHYs  IDATXÕmlSml'v8䲌I% ,J*kHВJDtӪCDU5Ak4UP[bS4 Ѡ4(H0v}}#cҽҫcy?{rs]ץ'~rTUj*L$߿Xk}HdQw'1IoyB(0 @UUdY-ˊܸq㯣'13E]y;o؏x|V_ǶmΆ ~d6m#@aBUUEAeğ?wttU[[;.^n{/oln mbbmr4>3g P,G_@yy9㩧?ڵk!HR8k c*\n5\SSuv;vb&QW^yM8v^CQ(wYQ7yO:}`xxteY($INee%mrA&''9t\e{d 'w;RJ.'Dfxx!vK/<.\ HL&Qe:۶kض]uFW=ǒcƍꫯm6z{{B066m+rBy;UUYOdYZx7 RSSիWĚuuu_MOO8 erHD  L&ywnFҵ|c֭XU\0mffor(//p8'9m_ .ErKs;::ղ,"LFIlذ׋0 dY&͢*ibY^%O?w#j'#NnW着ӹUQ4MCu"_چ^5KKKo/]Z xht" /~(e-s\!֭[Y(T*XPaF ) pq5L@ͫrxn(@A"4͂&I,|Ϥi(`we-ɓ Ƌ5L&$ !D%; s!Ċ8B RY 111eYHyx-_JI|r$i@PT xoCCpӲ!/Nid2`An߾v*:#D#K#166ѿ?x,kU&UUUƍs눔X\\$ )X̕CIPD~9UUCeJ yrJRw[{T~?_~2#H._|$g{iii|iiiFӴz]ןl֕fSB۷y^kѣ|>-u5=t@o^/^TJ[Oܹs۷o,wM@ukk^ӯ\-߻~}ܠIENDB`skipfish-2.10b/assets/p_dir.png0000440036502000116100000000277012057375131015505 0ustar heinennengPNG  IHDR szzgAMAܲbKGD pHYsHHFk>IDATXýK\G>gznL 0RAfV ر` .HH R1e3tg=}Tv J:۷z:NoZ`x9˛w677o{^u;w޽ky;nܻwi~immnZ˲+$I~w}!ÇSE 6m4x<(( +ʲk !XTTj=ɠS'*D9E&򪸪 8 0N`0 @j "x0'I$b^: Gn4tpDLU1B$8G:Z<1U"Y%91QPUF8 y.>1,&"مǤk#DC6榠6{R7"ip]Z1:3u>ShwBYڼvfΝs@;: ¦  !4J-84Qw?[SRpr~,F;4W0(*wY{}YԋJqV@Y+g}j65N!Ħ`Wv)p|ZRG@ ؂-̣qӧAYy$o#f0D3bUcɓ'p{{ms2K=.T}z$Σ@`xZѨ*˗<~G;;;?zNAA2=>&C^E Io3L&!vNF.@2U e'x [TTis4."ዯf@Yg/ K\8at}я|<,X&x A1P`oܹ6"1v94iR@s̏)a0 +4g+}.@+ 35L՘)1beģ_Q.S0UL JJ2v"98 CM_يaQP'PG}~ Bāpx%i A PQbќHJ q,HJt!i+z*qinKXg@@!u w'S8 ^E*Bygor}GH-R,V:8\ՆEEiT''z?[?@|w>;4QLb4eJu6 Et:.V.i\͛_J]ʖNZ|ӲjUϫҷ&qK?_Ufʬ R ڱqV>ekiڲd7ivhp t _dڧSs؛Fo6oh∐/OIENDB`skipfish-2.10b/assets/n_maybe_missing.png0000440036502000116100000000163012057375131017545 0ustar heinennengPNG  IHDRagAMAܲbKGDC pHYsHHFk>(IDAT8˅kH 43xif*,,bJ*0'eUB$]&Qlae Y)l$ Tf= 2"sK~:3}=sxyωHm/H%@ "FۅrYǠ.ARͮhČ bA.fm3l[Mc2ZHi7W6cׅ]6ϫNZCDm9 az._xvq3܁J 3>]@o/YLs]0@d}:rtdcX4(ٱtc){#ek_gKtM߻OueEK p8xmx7 ?dxRp- n7C!|Ϋ$,~rMaWfׇ0Gh :a\Uk J@ƭm(v @J.x|!9a9jBT3Ɂl^H:ֳII1e2TWz(`w(П:+R3D寀8%@? Apٮ|C=({Ee%8XJz]K:+(䱧M޸~UJa<9ӔlJ!I}-GͷCM_'3y/;HTlpIENDB`skipfish-2.10b/assets/p_value.png0000440036502000116100000000410012057375131016030 0ustar heinennengPNG  IHDR szzgAMAܲbKGD pHYsHHFk>IDATXŗk]U{γP*E R- EBB? 4&H"ABBAL$@E RLJ2i;δ~;U P#}ַZ{u,ٔ[oeYeǍ|uQ8+$-,uQzѪˮ\h+)ov?l;O@P{tzkGJgk*RB@ByahcwOr'G6|ՙ>:@@J#p}hh;ǧع}ǃ9O {2):# 4/m6`& 9|`:$Y x]:dot%s-gcKI+=$0*U,V;@)a 3J:?O1sy#s-J?%'yqVď=9rQ &qLJ)a1 %8yo;p ha,I1}'fONQ'<4y-Yop9M7RFx"2MfSHQ,0tԶ3J]7`^arvA~Z:( NFnrXjC֌7!5-pt LDd!Ae7P\e7c;w_A6 zÊ-;G)E(`zN!^ Q"pÀy-7=寭^vy}o؞},Ҭۯ-+]u-ы>H3XZ DB Q@͇ GhDVu3ʒϗΙ/cGNU YUxÆHB.(q{gvaRrF'6iEUI2:2g}TEv2D/ _c/<;3cJ1ƨn7YW2 yr!O;y^F/MMhR 4cHtS1M Y;$: 2t 9 I o8'Yopۛ֐_ܞ oEtPR ! RUHaYl_C hC5!w61 D?%:?-Z{ ;B=uF`Z[mw9_@)4̇0m]SBPU qQ0B:6(@jD# HbMMXaݾF} 8qA u@Q N kAA @h#9d##nL4!COזr]f7cP @O"7((ǐWK%pP$4.$‰V%3N_e26Z QSM7I'% g!%)G'H #*`BFRAvҨ%UH36aJRuNHpc0s q TZV%~YD#܄d. 3,JJ -J L U >:sNP-5V3h@˃%@H" RG!T޲J9)HPP DH#|MsbxRVueKk Qz fo{{[fsFNCWcJDN0u:S{^t &5 9Y˩tngT)*hЬk1oO^ևyD<#%{]]{7-p_`Q# RB!27`S{Nw=L/ڷ]ih*ZWrj6ַl 6@wOo>qv'횑Ͽ2#!yͫ} HjPBoVbu's&_V>x׀(+o lucc`\ ,@X {jˁPYP4#?7> , PIENDB`skipfish-2.10b/assets/p_unknown.png0000440036502000116100000000336012057375131016422 0ustar heinennengPNG  IHDR szzgAMAܲbKGD pHYsHHFk>IDATXõWMh=w(,YeFV[&6pl</.PYn--WH6 <(׋FƉRlXNl9v #UDG3]أJv E0~|wg(>(z:;;-˅bqljnnxϝ;w0 g&p΍"'`0u0$N!gaaᗪ( wwwJ)8'|[sƱDQt_p^__w޷!n )<X6]( /::222 F cB͛7qU "Cu>cz{{ !e21??0Յ^ttt R*($Ic\G={vTeB)!&ms $ >>}]]] x<(!t:} VQB888cDp+"k@)E4N OZ$600/rNOƾrD0`@)E8 x"B|> I*055r\Y(";0<|4 a4M ܹsPcbb=,ֆF躎URrQA)6@ p3,D"~@$A<fX__ǫWL&dP.+ekoo1MӢo޼;(J󘘘\h4L&#`ssHSB*ٚ 0 I0={x<^Ts133T*۷ocgg/_cJ} ?R)ܽ{E)N/_F>Ç%(^~ LV:1 0cKR}T*arrx.] "H|cq^H$]T& TB9GX'w66YϭUrRK7M3mƑkC6 6 1X3'd W4 PunB&"EMMMPbq?С`kqαbL\Pm;1En pݐejRA@>חH$~k,G !gnwnj*]4Mlll?{UU re:FT `#/krNX`rp̙lŋ.---Jz/{ޕJD:$Q!"\.^oVgL&omm ժ J_\.dxCCyInGcccEըtssss. ?(|]l{!Y%[:D~jj/kkk A rNwvveY۷oD^NOO&Ahh07n$;;;p 1OqZ9i:'r~IENDB`skipfish-2.10b/assets/i_medium.png0000440036502000116100000000140212057375131016167 0ustar heinennengPNG  IHDRagAMA7bKGD pHYsHHFk>dIDAT8˥MHTQ}̧c3XhT\$R`DrhWm*ڈPhU)(v}ltƑty3o|x3\{9sΟ{4)etbMh/Қ<"Dv%P(X# *db uhG6}9L:/ ]͝>h#j)(~W BQ'P]wzE)A P}vmRZЖ=7# PVNrtXrrqO NLv AV;3633OdN۟ y?J҅<}Lj66sތDQlJ#}T~@1g!6_NkYҹqE1 !=~\Wl"zTXtSoftwarexsLOJUMLO JML/Ԯ MIENDB`skipfish-2.10b/assets/i_warn.png0000440036502000116100000000632212057375131015664 0ustar heinennengPNG  IHDRagAMA|Q =iCCPiccxڝSgTS=BKKoR RBTi@숨"q"Ay((6T}7o9g}>F`DdJ<6.'w T @- m@n8P $ B2r22 t%[j;eOv$(S*@@&X`(ʑs`̔`)d` SGE3(xW\!Sd咔Tn!\]x87CP؄ ee3FvD9;:;8:|?E񋖴e /B_TBfgk+ m_ _׃  2r<[&q?.wL'bPGKĹi ˒$ IHk`~B[P. %w߂1w0hْ 4P6h>؀#;x̆P8XBHLC.,UP%BZF8-p<^0 o`A2DX6b"ֈ#Ef!~H0!H "ERd5R#U^9E.!==F~C>@٨jڡ\ Bh G h%ZBѳڋ>G03l0.Bx,c˱b6b#{";!0 $,",'̈́ Ba$nD>1B%+uc[!\H8Ri D:C!d6ٚA% ry;4:yBP)xR@\ RƩjTS5*.Qkmԫ8MfNEhhFyC+:nDw%JaEz=Ca1J~=+&ib3 z9c; _EBZY U| գWUGԨjfj<5rjjwY/i544D4i01VjYYlۜgK߱٣34545Ojr0qpns>Lћ=Ejkh4km8ndn4ר1͘klŸx$dI}S)4ti[3sf-fCZ||L OE57-I\t˝׬P+'Tj֨zu44ii50lmrlll9-/L6u}wϰ0ۡ7G+GcWLor ]3:B:;}rvq;7:$pesø܋DW'\߻9)܎n~}hLڙFY4xx>2yy z[zy~c#9[;vi{o?$L 10(pS_ȯvlvG#(2*IU<- 999-(yr`GryPGTԊ OR%y;mzh􉌘LJfbq4]ڑ#z-ںhT$Fg* Ki\˙S.7:hz4k]BX"\Ҿp骥}˼],OZ޾xEኁ+J_S}Ay1 W XPR$/}uuu맯߾sr}IERaofbC2]Ioot\<s--.zbFmmmMo*VOuw)y}׮zKv#swo}}9Fv~N~:],k@ Ç]FƽMpXy>t(h?8:V܌4/nmImmk9>x{{۱mDI͓eh OM?=vFvflŞ}> uzwq%K/s/\qu'u;w7_uzZ[̞S={M+=; wz˸~+?R{TXqϖ?7:zA?q)iŠ`Љak=x.{>>R/;^XW_FcG^_NVJ3^=~fm;wsw~08姶ANdNL%c3 cHRMz%u0`:o_FbKGD pHYs  IDAT8˥@el~6"Pn'IY)dЦS$ZTI73 m^#=),Hܟ!)%":N쑔]6}Uן ˲~ !3%p8 HeRvfb}6_ m_c^jPQch40 q>LK>v6c! @I!p8j+zRu8p]dX0Τ\.T*aZs]!TU{Abr MӠi9O  gqLӄmۨV>?MQ&LxNΟ(A(cB,Ӏk!ĽQuQ |f|~KD{neE 1l68NMh8ͺtKD@UU00nM|KDW[ezqrP(n"u IENDB`skipfish-2.10b/assets/p_param.png0000440036502000116100000000341312057375131016022 0ustar heinennengPNG  IHDR szzgAMA7bKGD pHYsHHFk>mIDATXý_l1_ 8#" 8MUUQQF}K_(M,5R5 `0ηw (GpGV3ffDO*UUu]$G).JI)M1Q88D"Gy商c$sRB0 (buչrsϿ8v{HWwy=55Ԭj~5m*yxh* Lmk׾o_K{"1pwGA\.ÓL;SSY-ukkyQ i h6؀ sLmmnN>gOs̙s(l۶С=VuRum@w[*^VWogrv>q؛=~TU hjS څ,1„]lE) WvbKՁ(*S(,_C& ow쨫ٿ`I-[:~bFJzq V(:7?YI@F!pq ik֔1Sf%o_dT}XmJe!Ix0 J`;ɵ4y\( 2 Zhjjl ^O}/@|2¡t@7 iʭw]k) É_ࣿ$$2bu!/Q2~+rO?œR2޽o=r[\ԋ SO)E ,ufStww)eu ~d25WBϟ}K[" }sj,WR*|2 C3C஥R986Ss[oY}wP`山^ `dwz9k5wzo|D6*J:wv>[0×e8.DZxxq\m:a퍟"qo_x ٹϼ=`3:zΝUUюC^o@&ff&&&g2K."  !4vC(+'Yi7g/_g/j*|Rс`'}ɟLLNG2B4 Da g"^E/P " |)'#"p&~`~R&LlSV gBu7`6[:ɦeIh U1! J#n6`#0}sowM{`f#5'% 韵d8<pYyP"zTXtSoftwarexsLOJUMLO JML/Ԯ MIENDB`skipfish-2.10b/assets/sf_name.png0000440036502000116100000003356312057375131016024 0ustar heinennengPNG  IHDRH pHYs   MiCCPPhotoshop ICC profilexڝSwX>eVBl"#Ya@Ņ VHUĂ H(gAZU\8ܧ}zy&j9R<:OHɽH gyx~t?op.$P&W " R.TSd ly|B" I>ةآ(G$@`UR,@".Y2GvX@`B, 8C L0ҿ_pH˕͗K3w!lBa)f "#HL 8?flŢko">!N_puk[Vh]3 Z zy8@P< %b0>3o~@zq@qanvRB1n#Dž)4\,XP"MyRD!ɕ2 w ONl~Xv@~- g42y@+͗\LD*A aD@ $<B AT:18 \p` Aa!:b""aH4 Q"rBj]H#-r9\@ 2G1Qu@Ơst4]k=Kut}c1fa\E`X&cX5V5cX7va$^lGXLXC%#W 1'"O%zxb:XF&!!%^'_H$ɒN !%2I IkHH-S>iL&m O:ňL $RJ5e?2BQͩ:ZImvP/S4u%͛Cˤ-Кigih/t ݃EЗkw Hb(k{/LӗT02goUX**|:V~TUsU?y TU^V}FUP թU6RwRPQ__c FHTc!2eXBrV,kMb[Lvv/{LSCsfffqƱ9ٜJ! {--?-jf~7zھbrup@,:m:u 6Qu>cy Gm7046l18c̐ckihhI'&g5x>fob4ekVyVV׬I\,mWlPW :˶vm))Sn1 9a%m;t;|rtuvlp4éĩWggs5KvSmnz˕ҵܭm=}M.]=AXq㝧/^v^Y^O&0m[{`:>=e>>z"=#~~~;yN`k5/ >B Yroc3g,Z0&L~oL̶Gli})*2.QStqt,֬Yg񏩌;jrvgjlRlc웸xEt$ =sl3Ttcܢ˞w|/%ҟ3 cHRMz%u0`:o_F,IDATxb?! 4)%K`Q}w :߅#Kڵɽ[n!ԗ/F@(ۚE3Y&lcǎ\0XbKwO300p 1$s}U4|tO7} ,OzOw [VVptƻ000Ͱa``8x uUSvYӄ w&N8V9200=.ι| M yyyC|.ζ6D63?~ѣ]ݫW~!''Gdbm2az%K;w4z+Jrci{ĉa#s֝!DXaE~02<{d4s14 qܸq#ji,UnHXauݤI6xڿ}al_p:Gw3002008qѣG {ʽ! D8𬐐ОP+++!p222ʕ+[ \\:200%޼ySNe``u4\$">A {n߾MZ26ʕ+'cݾ}?)U_HZa?v5k%ۘYz(FrBc({ S1F&n1v-;/bztwwc|uvvaۭjJ%BA( o7ۛbY㗯Saa͓('1zoaj*777D'mEF-Fqrr666tT1 j8A+·*Fa $IRKRGRd.fR@\.av ;&YM):UjawCߏȨ%ɠ G[-}1>'yjIR{yR4qH'Q` X=KN333"kfhD{PeU2llѾ(:fENp8|鐑'5|9tZs^1v:M$Ik8p!fQ ?w5);ț(z@&i>ݭH*H7)X,L&S(Zmmm}}}4MbﻃGozIfVBQOOy @ӴJxng3cFl`ˈ4K\&1kqFI\xmƘ|x=<_pc۳#3d^&3LOGqv:b #_S \s2(z=ykii j:BЗYVpOBVUU agSXYYa"IYNps{{cgiAӴ#IRӥFh0p}c "M4Monn&F>w]x7,/,,!IAxvw|&M/Y1SeD"k:i\Bʫ\_]( Ý3|2أiZvw9CSZ)tuugz^|H$b X'~Σ?  #FKƷ 'N6_'OА>p8@BpPjjMooAX*v%R,}6`'h''' ;7TRD %Iq<=u;4YYY bA}JP&:qia {ii)2=ϪZ[[oW_yAXg' n``= |>˟DXXX8N;<<||gcc9`Lw` myRˇUx˵?D"[x#!dvK~^K;qjiiA߸#hmq2ni7tݵSg@.DJ8OMM)Jн|zzrP(DUkz鬮 ^W$xm&x4u:eeeD(ǃaaA5F#B6~yY Xx 3N:%?$IZoek1M]Q-6mǢłeh M -fL:L $YȘJ]K#VkHceIȫeR eRj4;;{>т-VG-;;[$+JF^L&%nVM&Zz}ttxe20 ?x tjY:::/w)..f@P^^5W,3 p8H;'X,d2 ‹/"c>a-..~H;*2;ԙoa11zXvvUZ]Qu,_:;;×/_hD|Ï/,ksZwލǾ\~D.?bGFh4 Ӓ,ɒRih4}ōc\=A?#C"LcX!۾}Y߄}p8NAP'FDs:O׻呑_P(tR|2ÅObRd qXϺ5`12޷sKJJH$z?[&e?Z ᮑTQL TTTtm?>/ cGY閬H$c'f;4{bb.3͒ף$e~g#G?e/FAg X\u_bccͮ97/DF]W'4"c[Ǐ֔3+rϢ^o߾c5B4 ޭp%0sÆ fEP/ԃG[]]m)hOy%fsAhZ=@AɷVу}Gz\u=iw"`nܸɧHݦrtMpyrS5PHHHll?PYYP_F8qe<Ɓ8x`˗bm'{׮]0-go3q2i+߿osOĄL#W2ykϟQtiZ777`pr}\=0;-W2m@7/(lZd`>UE fEXb_ ںulЭ[tP( By}>>>V۪=}?mor%r法JC,stOje8۪/S*v$q%%%:2MiXeNK$ [3|Tz2**5֬Y"˫iko)~.ڗ=x.5}a/bbb 1c H,))}e鴿A$%kpA ܙ:Us*J& T*moo'G!vL;j 0@Nxx7Zoo/D@>'=|>#) U&MGoUt'kP@ HII{**44%50BGk5;UMFc9{.444 9=i %!̚=[H$٘WQH$AP6[Ńzƽ]t)z lVc&ƽ.xIRk򹌺.F]:CգMe,鼽,>Fo_}6ocn|_ 999ww2Y㳱T3T200v{ φ $oKy܏e|]8Sjժau5swMM>0 w2Y+ᙨKKK s.^@ T*NommMJJhWGKRP]6l6% e2Z tqqɁb lI .3MMM4MIrrrDEGFFi8 $qUUU[ljlN7fp8Jb nrb9Η/k}g}'6a6:]344k]~2vvv*QHh֐Qʕ+~~~ )00-7yj[kꫯd2%Wf@і-[?r+!;Vٯ?ReV;~ v茓nѺkz`pXe+6Ŧ "3sV,ں7ewܙ7owss?k߆;Ǝ/͚?޵5qɐIH"a$@5l%rBbAAk`RlۣB=նET(G="W%!@$# 114D&pQOy.|;k}}k\e51q/:P8n֏:;;766655EGGoݺuX?~ o e2T*Uvtt޺ukΝՠ4T*UNNӧ qPX[[ [nmvZzz\.wvv Bpyu&.\dɒI&d2 ccc1%%%%@$TUJddv߽L&/--MJJJOOM%wΝ;׬Y ٳgD*t&F9osEum. x6۷oѢEpZ\N$G@Tgs碢<<<Bhii9t_H׬T*)))Sgff>|0!!A8m @ܖ...UUUwà&}͆ p8\ggΕLZVwtumo~-111** ~jX,>s&(M8<-m,;;رc:Ղl:z(s@H)~8ϥK+t!tztttWWG30ADf@zTF悂W (wfAVoя=Vi4A&[r8,+eF ''.)JoS1h&LP՝m+Pq"p~yjo3R-'xJ 3 56~jZQTVͧnj2f]X>>>okvwwST[xzzn޼y.Ξ=;22 \.˗/ w2)$g{*J&Q(T*rJnnY|97ݰa`u:]wwwQQQsssBBNkmm5ڄf F}}}nnfvvv"LBú;X,JP(^jll(ʗ_~Yf-]PӐSDi4 ֜yaFے'[@LܸRVDQmk_NS(FSYYn&t~3gXp]. RA >U*Ƴ+Bi4Z޳gO``%:kjjPt=tM=jؚ[[|K,))1ӌ,TPW`B W^:n52$p8wccc LXQ+VϷ嘜d&\0OB>JNN1Addd'rDAeb뫓[D~R̶2oRK/|{ƌ'OϿ K9!,Yfutt$d_0襒!4qj)(Mi'<10bߌm:.!!-JȓOH${#ᖭ@X1zH$1cFaaaUUUHH+ңAŋl+i75<_.lS_4 [vFFjYt:= `۶mٺu :mڴ5P(@ h4z0I7ڒZ,w AɊ 7gϞܹo߾]P,_|cP rNhlp8'---$$dK̛7/44p&%>_BVPNd}|Pa S&]M=<H NDIIIr*glarFEEEeff|&y$l1e&əNU}9|AӱXlrrMÿfNreZ Q9Mdt*u:H,4:zܹsc E>NNvy|?/$: (??˖-7oޜ:uٍ L6>'y 4mdhr;++::N;F1jכ%IQ6tɓ=zP(֯__^^>7?;! la,ZZ1<eH^STfBnQ&&Jcbb\_Wp8Lf8aS8ӧu>&8sL?s$,̌>0)kEͯUNB-R,Z;w.)) >G?yݻ^^^+d,`zT*=St2OK[D%IH%>5p@feeMoo/D _zy4MppF),4a?@Pc/ dP#ȿ|㲌O &%%%}wt:"!ED+++J BL!THB00mmmAܾ*0%515@jƧXU{#a,X&H$GsfbKbXRԒFȿDE.|oW!I']LRر#11`444[a('+ 3f/h5PHaZ[XދP(T}}CKw7DPjgLP2Y+ꁊz6 c`UAFFƛ{B䟯2J@ɥ'̘X "FQ+L`eeϟ?cϊ#pd S~dj*Z\\\]]mybXvmE/T #1#mL& iӦt;wVJBԊUal6ollܵk^\EșNf綶9̞1(uuuc v$`u'_! Y &mϛ%$$lذaҐhNVK$HE[a&x~eգtkip_WWw)St'R{5rlKAP*ʈ={$''s-6V %}-{!eRH"lufRǮblmH$۷o'''Ϝ9n`NӁkkIVh4WTU=<͍嘆G"ܺuhѢE񢢢LکhRŋŃ;!M+++-LIB|>p:u*ͦP(-|~MM <5ϟ~-x2?&ojIDATXMoUs8SPQUmUTJ~ mo-T |Dj]棩|x왱=sYN8"-;ss{!׮ts$qѢ\.b%b*fDບ޹upzO'|}}FEƸ&baO?0TqN. ](LA4M(%3 Skipfish - scan results browser
HTTP trace - click this bar or hit ESC to close
Scanner version: Scan date:
Random seed: Total time:
Problems with this scan? Click here for advice.

Crawl results - click to expand:

Document type overview - click to expand:

Issue type overview - click to expand:

NOTE: 100 samples maximum per issue or document type. skipfish-2.10b/Makefile0000440036502000116100000000533212057375132014036 0ustar heinenneng# # skipfish - Makefile # ------------------- # # Author: Michal Zalewski , # Niels Heinen # # Copyright 2009, 2010, 2011 by Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # PROGNAME = skipfish VERSION = 2.10b SRCDIR = src SFILES = http_client.c database.c crawler.c analysis.c report.c \ checks.c signatures.c auth.c options.c IFILES = alloc-inl.h string-inl.h debug.h types.h http_client.h \ database.h crawler.h analysis.h config.h report.h \ checks.h signatures.h auth.h options.h OBJFILES = $(patsubst %,$(SRCDIR)/%,$(SFILES)) INCFILES = $(patsubst %,$(SRCDIR)/%,$(IFILES)) CFLAGS_GEN = -Wall -funsigned-char -g -ggdb -I/usr/local/include/ \ -I/opt/local/include/ $(CFLAGS) -DVERSION=\"$(VERSION)\" CFLAGS_DBG = -DLOG_STDERR=1 -DDEBUG_ALLOCATOR=1 \ $(CFLAGS_GEN) CFLAGS_OPT = -O3 -Wno-format $(CFLAGS_GEN) LDFLAGS += -L/usr/local/lib/ -L/opt/local/lib LIBS += -lcrypto -lssl -lidn -lz -lpcre all: $(PROGNAME) $(PROGNAME): $(SRCDIR)/$(PROGNAME).c $(OBJFILES) $(INCFILES) $(CC) $(LDFLAGS) $(SRCDIR)/$(PROGNAME).c -o $(PROGNAME) \ $(CFLAGS_OPT) $(OBJFILES) $(LIBS) @echo @echo "See doc/dictionaries.txt to pick a dictionary for the tool." @echo @echo "Having problems with your scans? Be sure to visit:" @echo "http://code.google.com/p/skipfish/wiki/KnownIssues" @echo debug: $(SRCDIR)/$(PROGNAME).c $(OBJFILES) $(INCFILES) $(CC) $(LDFLAGS) $(SRCDIR)/$(PROGNAME).c -o $(PROGNAME) \ $(CFLAGS_DBG) $(OBJFILES) $(LIBS) @echo @echo "The debug build prints runtime information to stderr. You" @echo "probably want to redirect this output to a file. like:" @echo @echo " $ ./skipfish [.option.] 2> debug.log" @echo clean: rm -f $(PROGNAME) *.exe *.o *~ a.out core core.[1-9][0-9]* *.stackdump \ LOG same_test rm -rf tmpdir same_test: $(SRCDIR)/same_test.c $(OBJFILES) $(INCFILES) $(CC) $(SRCDIR)/same_test.c -o same_test $(CFLAGS_DBG) $(OBJFILES) $(LDFLAGS) \ $(LIBS) publish: clean cd ..; rm -rf skipfish-$(VERSION); \ cp -pr skipfish-release skipfish-$(VERSION); \ tar cfvz ~/www/skipfish.tgz skipfish-$(VERSION); \ chmod 644 ~/www/skipfish.tgz skipfish-2.10b/tools/0000750036502000116100000000000012057375163013540 5ustar heinennengskipfish-2.10b/tools/sfscandiff0000550036502000116100000000544012057375132015571 0ustar heinenneng#!/bin/bash echo "sfscandiff - skipfish scan result comparator (lcamtuf@google.com)" 1>&2 if [ ! "$#" = "2" ]; then echo "Usage: $0 /path/to/old/scan/ /path/to/new/scan/" 1>&2 exit 1 fi if [ ! -s "$1/summary.js" ]; then echo "ERROR: First parameter does not point to a valid skipfish scan directory." 1>&2 exit 1 fi if [ ! -s "$2/summary.js" ]; then echo "ERROR: Second parameter does not point to a valid skipfish scan directory." 1>&2 exit 1 fi OLD_SCAN="$1" NEW_SCAN="$2" # Takes two parameters: old scan subdir and new scan subdir function check_dir { # echo "Comparing: old=[$1] new=[$2]..." echo "0" >"$2/.diff_cnt" echo "var diff_data = {" >"$2/diff_data.js" grep "'dir':" "$2/child_index.js" | awk -F "'dir': " '{print $2}' | \ sed "s/,.*'sig'://" | sed "s/[,}]*$//" |sed "s/'//g" | \ while read -r dir sig; do # echo " Checking dir=[$dir] sig=[$sig]" # Find matching child node first. MATCH_DIR=`grep -E "'sig': $sig[, ]" "$1/child_index.js" 2>/dev/null | \ awk -F "'dir': " '{print $2}' | cut -d"'" -f2 | head -1` test "$MATCH_DIR" = "" && MATCH_DIR="not_found" # Recurse into children first, to get an accurate count of differences # for all descendants. check_dir "$1/$MATCH_DIR" "$2/$dir" # Read difference count from descendands. If node does not appear in # old scan, add 1 to the count. Store count. DIFF_CNT=`cat "$2/$dir/.diff_cnt" 2>/dev/null` test "$DIFF_CNT" = "" && DIFF_CNT=0 test "$MATCH_DIR" = "not_found" && DIFF_CNT=$[DIFF_CNT+1] echo " '$dir': $DIFF_CNT," >>"$2/diff_data.js" # Update total count for parent node ($2) TOTAL_DIFF_CNT=`cat "$2/.diff_cnt" 2>/dev/null` TOTAL_DIFF_CNT=$[TOTAL_DIFF_CNT+DIFF_CNT] echo "$TOTAL_DIFF_CNT" >"$2/.diff_cnt" done # Now, for every issue, see if a matching issue appears in old scan. # If not, add it to diff_data. grep "'severity':" "$2/issue_index.js" | while read -r line; do LOOK_FOR=`echo "$line" | awk -F"'fetched':" '{print $1}'` ISSUE_DIR=`echo "$line" | awk -F"'dir':" '{print $2}'|cut -d"'" -f2` # echo " Checking issue=[$ISSUE_DIR]" if ! grep -qF "$LOOK_FOR" "$1/issue_index.js" 2>/dev/null; then echo " '$ISSUE_DIR': 1," >>"$2/diff_data.js" fi done echo " '_eof': 0" >>"$2/diff_data.js" echo "};" >>"$2/diff_data.js" } echo -n "Finding new results in $NEW_SCAN... " check_dir "$OLD_SCAN" "$NEW_SCAN" TOTAL=`cat "$NEW_SCAN/.diff_cnt"` if [ "$TOTAL" = "0" ]; then echo "no new findings." elif [ "$TOTAL" = "1" ]; then echo "one new or modified node found." else echo "$TOTAL new or modified nodes found." fi grep -qF "var diff_mode" "$NEW_SCAN/summary.js" || echo "var diff_mode = true;" >>"$NEW_SCAN/summary.js" exit 0 skipfish-2.10b/README0000440036502000116100000006361212057375131013262 0ustar heinenneng=========================================== skipfish - web application security scanner =========================================== http://code.google.com/p/skipfish/ * Written and maintained by: Michal Zalewski Niels Heinen Sebastian Roschke * Copyright 2009 - 2012 Google Inc, rights reserved. * Released under terms and conditions of the Apache License, version 2.0. -------------------- 1. What is skipfish? -------------------- Skipfish is an active web application security reconnaissance tool. It prepares an interactive sitemap for the targeted site by carrying out a recursive crawl and dictionary-based probes. The resulting map is then annotated with the output from a number of active (but hopefully non-disruptive) security checks. The final report generated by the tool is meant to serve as a foundation for professional web application security assessments. ------------------------------------------------- 2. Why should I bother with this particular tool? ------------------------------------------------- A number of commercial and open source tools with analogous functionality is readily available (e.g., Nikto, Nessus); stick to the one that suits you best. That said, skipfish tries to address some of the common problems associated with web security scanners. Specific advantages include: * High performance: 500+ requests per second against responsive Internet targets, 2000+ requests per second on LAN / MAN networks, and 7000+ requests against local instances have been observed, with a very modest CPU, network, and memory footprint. This can be attributed to: * Multiplexing single-thread, fully asynchronous network I/O and data processing model that eliminates memory management, scheduling, and IPC inefficiencies present in some multi-threaded clients. * Advanced HTTP/1.1 features such as range requests, content compression, and keep-alive connections, as well as forced response size limiting, to keep network-level overhead in check. * Smart response caching and advanced server behavior heuristics are used to minimize unnecessary traffic. * Performance-oriented, pure C implementation, including a custom HTTP stack. * Ease of use: skipfish is highly adaptive and reliable. The scanner features: * Heuristic recognition of obscure path- and query-based parameter handling schemes. * Graceful handling of multi-framework sites where certain paths obey completely different semantics, or are subject to different filtering rules. * Automatic wordlist construction based on site content analysis. * Probabilistic scanning features to allow periodic, time-bound assessments of arbitrarily complex sites. * Well-designed security checks: the tool is meant to provide accurate and meaningful results: * Handcrafted dictionaries offer excellent coverage and permit thorough $keyword.$extension testing in a reasonable timeframe. * Three-step differential probes are preferred to signature checks for detecting vulnerabilities. * Ratproxy-style logic is used to spot subtle security problems: cross-site request forgery, cross-site script inclusion, mixed content, issues MIME- and charset mismatches, incorrect caching directives, etc. * Bundled security checks are designed to handle tricky scenarios: stored XSS (path, parameters, headers), blind SQL or XML injection, or blind shell injection. * Snort style content signatures which will highlight server errors, information leaks or potentially dangerous web applications. * Report post-processing drastically reduces the noise caused by any remaining false positives or server gimmicks by identifying repetitive patterns. That said, skipfish is not a silver bullet, and may be unsuitable for certain purposes. For example, it does not satisfy most of the requirements outlined in WASC Web Application Security Scanner Evaluation Criteria (some of them on purpose, some out of necessity); and unlike most other projects of this type, it does not come with an extensive database of known vulnerabilities for banner-type checks. ----------------------------------------------------- 3. Most curious! What specific tests are implemented? ----------------------------------------------------- A rough list of the security checks offered by the tool is outlined below. * High risk flaws (potentially leading to system compromise): * Server-side query injection (including blind vectors, numerical parameters). * Explicit SQL-like syntax in GET or POST parameters. * Server-side shell command injection (including blind vectors). * Server-side XML / XPath injection (including blind vectors). * Format string vulnerabilities. * Integer overflow vulnerabilities. * Locations accepting HTTP PUT. * Medium risk flaws (potentially leading to data compromise): * Stored and reflected XSS vectors in document body (minimal JS XSS support). * Stored and reflected XSS vectors via HTTP redirects. * Stored and reflected XSS vectors via HTTP header splitting. * Directory traversal / LFI / RFI (including constrained vectors). * Assorted file POIs (server-side sources, configs, etc). * Attacker-supplied script and CSS inclusion vectors (stored and reflected). * External untrusted script and CSS inclusion vectors. * Mixed content problems on script and CSS resources (optional). * Password forms submitting from or to non-SSL pages (optional). * Incorrect or missing MIME types on renderables. * Generic MIME types on renderables. * Incorrect or missing charsets on renderables. * Conflicting MIME / charset info on renderables. * Bad caching directives on cookie setting responses. * Low risk issues (limited impact or low specificity): * Directory listing bypass vectors. * Redirection to attacker-supplied URLs (stored and reflected). * Attacker-supplied embedded content (stored and reflected). * External untrusted embedded content. * Mixed content on non-scriptable subresources (optional). * HTTPS -> HTTP submission of HTML forms (optional). * HTTP credentials in URLs. * Expired or not-yet-valid SSL certificates. * HTML forms with no XSRF protection. * Self-signed SSL certificates. * SSL certificate host name mismatches. * Bad caching directives on less sensitive content. * Internal warnings: * Failed resource fetch attempts. * Exceeded crawl limits. * Failed 404 behavior checks. * IPS filtering detected. * Unexpected response variations. * Seemingly misclassified crawl nodes. * Non-specific informational entries: * General SSL certificate information. * Significantly changing HTTP cookies. * Changing Server, Via, or X-... headers. * New 404 signatures. * Resources that cannot be accessed. * Resources requiring HTTP authentication. * Broken links. * Server errors. * All external links not classified otherwise (optional). * All external e-mails (optional). * All external URL redirectors (optional). * Links to unknown protocols. * Form fields that could not be autocompleted. * Password entry forms (for external brute-force). * File upload forms. * Other HTML forms (not classified otherwise). * Numerical file names (for external brute-force). * User-supplied links otherwise rendered on a page. * Incorrect or missing MIME type on less significant content. * Generic MIME type on less significant content. * Incorrect or missing charset on less significant content. * Conflicting MIME / charset information on less significant content. * OGNL-like parameter passing conventions. Along with a list of identified issues, skipfish also provides summary overviews of document types and issue types found; and an interactive sitemap, with nodes discovered through brute-force denoted in a distinctive way. NOTE: As a conscious design decision, skipfish will not redundantly complain about highly non-specific issues, including but not limited to: * Non-httponly or non-secure cookies, * Non-HTTPS or autocomplete-enabled forms, * HTML comments detected on a page, * Filesystem path disclosure in error messages, * Server of framework version disclosure, * Servers supporting TRACE or OPTIONS requests, * Mere presence of certain technologies, such as WebDAV. Most of these aspects are easy to inspect in a report if so desired - for example, all the HTML forms are listed separately, so are new cookies or interesting HTTP headers - and the expectation is that the auditor may opt to make certain design recommendations based on this data where appropriate. That said, these occurrences are not highlighted as a specific security flaw. ----------------------------------------------------------- 4. All right, I want to try it out. What do I need to know? ----------------------------------------------------------- First and foremost, please do not be evil. Use skipfish only against services you own, or have a permission to test. Keep in mind that all types of security testing can be disruptive. Although the scanner is designed not to carry out malicious attacks, it may accidentally interfere with the operations of the site. You must accept the risk, and plan accordingly. Run the scanner against test instances where feasible, and be prepared to deal with the consequences if things go wrong. Also note that the tool is meant to be used by security professionals, and is experimental in nature. It may return false positives or miss obvious security problems - and even when it operates perfectly, it is simply not meant to be a point-and-click application. Do not take its output at face value. Running the tool against vendor-supplied demo sites is not a good way to evaluate it, as they usually approximate vulnerabilities very imperfectly; we made no effort to accommodate these cases. Lastly, the scanner is simply not designed for dealing with rogue and misbehaving HTTP servers - and offers no guarantees of safe (or sane) behavior there. -------------------------- 5. How to run the scanner? -------------------------- To compile it, simply unpack the archive and try make. Chances are, you will need to install libidn first. Next, you need to read the instructions provided in doc/dictionaries.txt to select the right dictionary file and configure it correctly. This step has a profound impact on the quality of scan results later on, so don't skip it. Once you have the dictionary selected, you can use -S to load that dictionary, and -W to specify an initially empty file for any newly learned site-specific keywords (which will come handy in future assessments): $ touch new_dict.wl $ ./skipfish -o output_dir -S existing_dictionary.wl -W new_dict.wl \ http://www.example.com/some/starting/path.txt You can use -W- if you don't want to store auto-learned keywords anywhere. Note that you can provide more than one starting URL if so desired; all of them will be crawled. It is also possible to read URLs from file, using the following syntax: $ ./skipfish [...other options...] @../path/to/url_list.txt The tool will display some helpful stats while the scan is in progress. You can also switch to a list of in-flight HTTP requests by pressing return. In the example above, skipfish will scan the entire www.example.com (including services on other ports, if linked to from the main page), and write a report to output_dir/index.html. You can then view this report with your favorite browser (JavaScript must be enabled; and because of recent file:/// security improvements in certain browsers, you might need to access results over HTTP). The index.html file is static; actual results are stored as a hierarchy of JSON files, suitable for machine processing or different presentation frontends if needs be. In addition, a list of all the discovered URLs will be saved to a single file, pivots.txt, for easy postprocessing. A simple companion script, sfscandiff, can be used to compute a delta for two scans executed against the same target with the same flags. The newer report will be non-destructively annotated by adding red background to all new or changed nodes; and blue background to all new or changed issues found. Some sites may require authentication for which our support is described in doc/authentication.txt. In most cases, you'll be wanting to use the form authentication method which is capable of detecting broken sessions in order to re-authenticate. Once authenticated, certain URLs on the site may log out your session; you can combat this in two ways: by using the -N option, which causes the scanner to reject attempts to set or delete cookies; or with the -X parameter, which prevents matching URLs from being fetched: $ ./skipfish -X /logout/logout.aspx ...other parameters... The -X option is also useful for speeding up your scans by excluding /icons/, /doc/, /manuals/, and other standard, mundane locations along these lines. In general, you can use -X and -I (only spider URLs matching a substring) to limit the scope of a scan any way you like - including restricting it only to a specific protocol and port: $ ./skipfish -I http://example.com:1234/ ...other parameters... A related function, -K, allows you to specify parameter names not to fuzz (useful for applications that put session IDs in the URL, to minimize noise). Another useful scoping option is -D - allowing you to specify additional hosts or domains to consider in-scope for the test. By default, all hosts appearing in the command-line URLs are added to the list - but you can use -D to broaden these rules, for example: $ ./skipfish -D test2.example.com -o output-dir http://test1.example.com/ ...or, for a domain wildcard match, use: $ ./skipfish -D .example.com -o output-dir http://test1.example.com/ In some cases, you do not want to actually crawl a third-party domain, but you trust the owner of that domain enough not to worry about cross-domain content inclusion from that location. To suppress warnings, you can use the -B option, for example: $ ./skipfish -B .google-analytics.com -B .googleapis.com ...other parameters... By default, skipfish sends minimalistic HTTP headers to reduce the amount of data exchanged over the wire; some sites examine User-Agent strings or header ordering to reject unsupported clients, however. In such a case, you can use -b ie, -b ffox, or -b phone to mimic one of the two popular browsers (or iPhone). When it comes to customizing your HTTP requests, you can also use the -H option to insert any additional, non-standard headers; or -F to define a custom mapping between a host and an IP (bypassing the resolver). The latter feature is particularly useful for not-yet-launched or legacy services. Some sites may be too big to scan in a reasonable timeframe. If the site features well-defined tarpits - for example, 100,000 nearly identical user profiles as a part of a social network - these specific locations can be excluded with -X or -S. In other cases, you may need to resort to other settings: -d limits crawl depth to a specified number of subdirectories; -c limits the number of children per directory; -x limits the total number of descendants per crawl tree branch; and -r limits the total number of requests to send in a scan. An interesting option is available for repeated assessments: -p. By specifying a percentage between 1 and 100%, it is possible to tell the crawler to follow fewer than 100% of all links, and try fewer than 100% of all dictionary entries. This - naturally - limits the completeness of a scan, but unlike most other settings, it does so in a balanced, non-deterministic manner. It is extremely useful when you are setting up time-bound, but periodic assessments of your infrastructure. Another related option is -q, which sets the initial random seed for the crawler to a specified value. This can be used to exactly reproduce a previous scan to compare results. Randomness is relied upon most heavily in the -p mode, but also for making a couple of other scan management decisions elsewhere. Some particularly complex (or broken) services may involve a very high number of identical or nearly identical pages. Although these occurrences are by default grayed out in the report, they still use up some screen estate and take a while to process on JavaScript level. In such extreme cases, you may use the -Q option to suppress reporting of duplicate nodes altogether, before the report is written. This may give you a less comprehensive understanding of how the site is organized, but has no impact on test coverage. In certain quick assessments, you might also have no interest in paying any particular attention to the desired functionality of the site - hoping to explore non-linked secrets only. In such a case, you may specify -P to inhibit all HTML parsing. This limits the coverage and takes away the ability for the scanner to learn new keywords by looking at the HTML, but speeds up the test dramatically. Another similarly crippling option that reduces the risk of persistent effects of a scan is -O, which inhibits all form parsing and submission steps. Some sites that handle sensitive user data care about SSL - and about getting it right. Skipfish may optionally assist you in figuring out problematic mixed content or password submission scenarios - use the -M option to enable this. The scanner will complain about situations such as http:// scripts being loaded on https:// pages - but will disregard non-risk scenarios such as images. Likewise, certain pedantic sites may care about cases where caching is restricted on HTTP/1.1 level, but no explicit HTTP/1.0 caching directive is given on specifying -E in the command-line causes skipfish to log all such cases carefully. In some occasions, you want to limit the requests per second to limit the load on the targets server (or possibly bypass DoS protection). The -l flag can be used to set this limit and the value given is the maximum amount of requests per second you want skipfish to perform. Scans typically should not take weeks. In many cases, you probably want to limit the scan duration so that it fits within a certain time window. This can be done with the -k flag, which allows the amount of hours, minutes and seconds to be specified in a H:M:S format. Use of this flag can affect the scan coverage if the scan timeout occurs before testing all pages. Lastly, in some assessments that involve self-contained sites without extensive user content, the auditor may care about any external e-mails or HTTP links seen, even if they have no immediate security impact. Use the -U option to have these logged. Dictionary management is a special topic, and - as mentioned - is covered in more detail in doc/dictionaries.txt. Please read that file before proceeding. Some of the relevant options include -S and -W (covered earlier), -L to suppress auto-learning, -G to limit the keyword guess jar size, -R to drop old dictionary entries, and -Y to inhibit expensive $keyword.$extension fuzzing. Skipfish also features a form auto-completion mechanism in order to maximize scan coverage. The values should be non-malicious, as they are not meant to implement security checks - but rather, to get past input validation logic. You can define additional rules, or override existing ones, with the -T option (-T form_field_name=field_value, e.g. -T login=test123 -T password=test321 - although note that -C and -A are a much better method of logging in). There is also a handful of performance-related options. Use -g to set the maximum number of connections to maintain, globally, to all targets (it is sensible to keep this under 50 or so to avoid overwhelming the TCP/IP stack on your system or on the nearby NAT / firewall devices); and -m to set the per-IP limit (experiment a bit: 2-4 is usually good for localhost, 4-8 for local networks, 10-20 for external targets, 30+ for really lagged or non-keep-alive hosts). You can also use -w to set the I/O timeout (i.e., skipfish will wait only so long for an individual read or write), and -t to set the total request timeout, to account for really slow or really fast sites. Lastly, -f controls the maximum number of consecutive HTTP errors you are willing to see before aborting the scan; and -s sets the maximum length of a response to fetch and parse (longer responses will be truncated). When scanning large, multimedia-heavy sites, you may also want to specify -e. This prevents binary documents from being kept in memory for reporting purposes, and frees up a lot of RAM. Further rate-limiting is available through third-party user mode tools such as trickle, or kernel-level traffic shaping. Oh, and real-time scan statistics can be suppressed with -u. -------------------------------- 6. But seriously, how to run it? -------------------------------- A standard, authenticated scan of a well-designed and self-contained site (warns about all external links, e-mails, mixed content, and caching header issues), including gentle brute-force: $ touch new_dict.wl $ ./skipfish -MEU -S dictionaries/minimal.wl -W new_dict.wl \ -C "AuthCookie=value" -X /logout.aspx -o output_dir \ http://www.example.com/ Five-connection crawl, but no brute-force; pretending to be MSIE and trusting example.com content: $ ./skipfish -m 5 -L -W- -o output_dir -b ie -B example.com \ http://www.example.com/ Heavy brute force only (no HTML link extraction), limited to a single directory and timing out after 5 seconds: $ touch new_dict.wl $ ./skipfish -S dictionaries/complete.wl -W new_dict.wl \ -P -I http://www.example.com/dir1/ -o output_dir -t 5 -I \ http://www.example.com/dir1/ For a short list of all command-line options, try ./skipfish -h. ---------------------------------------------------- 7. How to interpret and address the issues reported? ---------------------------------------------------- Most of the problems reported by skipfish should self-explanatory, assuming you have a good gasp of the fundamentals of web security. If you need a quick refresher on some of the more complicated topics, such as MIME sniffing, you may enjoy our comprehensive Browser Security Handbook as a starting point: http://code.google.com/p/browsersec/ If you still need assistance, there are several organizations that put a considerable effort into documenting and explaining many of the common web security threats, and advising the public on how to address them. I encourage you to refer to the materials published by OWASP and Web Application Security Consortium, amongst others: * http://www.owasp.org/index.php/Category:Principle * http://www.owasp.org/index.php/Category:OWASP_Guide_Project * http://www.webappsec.org/projects/articles/ Although I am happy to diagnose problems with the scanner itself, I regrettably cannot offer any assistance with the inner wokings of third-party web applications. --------------------------------------- 8. Known limitations / feature wishlist --------------------------------------- Below is a list of features currently missing in skipfish. If you wish to improve the tool by contributing code in one of these areas, please let me know: * Buffer overflow checks: after careful consideration, I suspect there is no reliable way to test for buffer overflows remotely. Much like the actual fault condition we are looking for, proper buffer size checks may also result in uncaught exceptions, 500 messages, etc. I would love to be proved wrong, though. * Fully-fledged JavaScript XSS detection: several rudimentary checks are present in the code, but there is no proper script engine to evaluate expressions and DOM access built in. * Variable length encoding character consumption / injection bugs: these problems seem to be largely addressed on browser level at this point, so they were much lower priority at the time of this writing. * Security checks and link extraction for third-party, plugin-based content (Flash, Java, PDF, etc). * Password brute-force and numerical filename brute-force probes. * Search engine integration (vhosts, starting paths). * VIEWSTATE decoding. * NTLM and digest authentication. * More specific PHP tests (eval injection, RFI). * Proxy support: an experimental HTTP proxy support is available through a #define directive in config.h. Adding support for HTTPS proxying is more complicated, and still in the works. * Scan resume option, better runtime info. * Standalone installation (make install) support. * Scheduling and management web UI. ------------------------------------- 9. Oy! Something went horribly wrong! ------------------------------------- There is no web crawler so good that there wouldn't be a web framework to one day set it on fire. If you encounter what appears to be bad behavior (e.g., a scan that takes forever and generates too many requests, completely bogus nodes in scan output, or outright crashes), please first check our known issues page: http://code.google.com/p/skipfish/wiki/KnownIssues If you can't find a satisfactory answer there, recompile the scanner with: $ make clean debug ...and re-run it this way: $ ./skipfish [...previous options...] 2>logfile.txt You can then inspect logfile.txt to get an idea what went wrong; if it looks like a scanner problem, please scrub any sensitive information from the log file and send it to the author. If the scanner crashed, please recompile it as indicated above, and then type: $ ulimit -c unlimited $ ./skipfish [...previous options...] 2>logfile.txt $ gdb --batch -ex back ./skipfish core ...and be sure to send the author the output of that last command as well. ------------------------ 10. Credits and feedback ------------------------ Skipfish is made possible thanks to the contributions of, and valuable feedback from, Google's information security engineering team. If you have any bug reports, questions, suggestions, or concerns regarding the application, the primary author can be reached at lcamtuf@google.com. skipfish-2.10b/doc/0000750036502000116100000000000012057377026013145 5ustar heinennengskipfish-2.10b/doc/authentication.txt0000440036502000116100000000632212057375132016723 0ustar heinenneng This document describes 3 different methods you can use to run authenticated skipfish scans. 1) Form authentication 2) Cookie authentication 3) Basic HTTP authentication ----------------------- 1. Form authentication ---------------------- With form authentication, skipfish will submit credentials using the given login form. The server is expected to reply with authenticated cookies which will than be used during the rest of the scan. An example to login using this feature: $ ./skipfish --auth-form http://example.org/login \ --auth-user myuser \ --auth-pass mypass \ --auth-verify-url http://example.org/profile \ [...other options...] This is how it works: 1. Upon start of the scan, the authentication form at /login will be fetched by skipfish. We will try to complete the username and password fields and submit the form. 2. Once a server response is obtained, skipfish will fetch the verification URL twice: once with the new session cookies and once without any cookies. Both responses are expected to be different. 3. During the scan, the verification URL will be used many times to test whether we are authenticated. If at some point our session has been terminated server-side, skipfish will re-authenticate using the --auth-form (/login in our example) . Verifying whether the session is still active requires a good verification URL where an authenticated request is going to get a different response than an anonymous request. For example a 'profile' or 'my account' page. Troubleshooting: ---------------- 1. Login field names not recognized If the username and password form fields are not recognized, skipfish will complain. In this case, you should specify the field names using the --auth-user-field and --auth-pass-field flags. 2. The form is not submitted to the right location If the login form doesn't specify an action="" location, skipfish will submit the form's content to the form URL. This will fail in some occasions. For example, when the login page uses Javascript to submit the form to a different location. Use the --auth-form-target flag to specify the URL where you want skipfish to submit the form to. 3. Skipfish keeps getting logged out Make sure you blacklist any URLs that will log you out. For example, using the " -X /logout" ------------------------- 2. Cookie authentication ------------------------- Alternatively, if the site relies on HTTP cookies you can also feed these to skipfish manually. To do this log in using your browser or using a simple curl script, and then provide skipfish with a session cookie: $ ./skipfish -C name=val [...other options...] Other session cookies may be passed the same way, one per each -C option. The -N option, which causes new cookies to be rejected by skipfish, is almost always a good choice when running cookie authenticated scans (e.g. to avoid your precious cookies from being overwritten). $ ./skipfish -N -C name=val [...other options...] ----------------------------- 3. Basic HTTP authentication ----------------------------- For simple HTTP credentials, you can use the -A option to pass the credentials. $ ./skipfish -A user:pass [...other options...] skipfish-2.10b/doc/signatures.txt0000440036502000116100000001550112057375132016067 0ustar heinenneng ----------------- 1. Introduction ----------------- With skipfish signatures it is possible to find interesting content, or even vulnerabilities, in server responses. The signatures follow a Snort-like syntax and most keywords behave similarly as well. Signatures focus on detecting web application vulnerabilities, information leaks and can recognize interesting web applications, such as phpmyadmin or phpinfo() pages. Signatures could also detect vulnerable software packages (e.g. old WordPress instances) but this is a task that fits vulnerability scanners, like Nessus and Nikto, better. ----------------- 2. Contributing ----------------- The current signature list is nice but far from complete. If you have new signatures or can optimize existing ones, please help out by reporting this via our issue tracker: https://code.google.com/p/skipfish/issues/entry?template=Content%20signatures ----------------------- 3. Signature keywords ----------------------- === content:[!]"" The content keyword is used to specify a string that we try to match against the server response. The value can either be a static string or a regular expression (the latter requires the type:regex; modifier). Multiple content strings can be specified per signature and, unless the signature specifies a mime type, there should be at least one. Modifiers can be specified per content keyword to influence how the string is matches against the payload. For example, with the 'depth' keyword you can specify how far in the payload we should look for the string. When ! is specified before the content string, the test is positive when the string is NOT present. This is mainly useful in case your signature has multiple content values. Note: content string modifiers should be specified _after_ the content string to which they apply. === content modifier: depth: With depth you can limit the amount of bytes we should search for the string. Initially the depth is relative to the beginning of the payload. However, when multiple 'content' strings are used, the depth is relative to the first byte after the previous content match. Using the depth keyword has two advantages: increase performance and increase signature accuracy. 1) Performance: A signature that matches on a tag doesn't need to be applied to the whole payload. Instead, a depth of 512 or even 1024 bytes will help to improve performance. 2) Accuracy: In a signature with two 'content' keywords, you can force the second keyword to be searched within a very short depth of the previous content match. === content modifier: offset:<int> The content string searching will start at the given offset value. For the first content string this is relative to the beginning of the payload. For the following content strings, this is relative to the first byte of the last match. === content modifier: type:["regex|static"] Indicates whether the content string should be treated as a regular expression or a static string. Content strings are treated as static by default so you can leave this keyword out unless you're using a regular expression. In a signature that has multiple content strings, static strings can be mixed with regular expressions. You'll likely get the best performance by starting with a static string before applying a regular expression. === content modifier: regex_match:"<string>" Regular expressions can capture substrings and with regex_match, it is possible to compare <string> with the first substring that is returned by the regular expression. Given "Apache/2.2.14" as payload and "Apache\/([\d\.]+)" as regex, you could use regex_match:"2.2.14" to find this specific Apache version. === content modifier: nocase When "nocase" is specified, the content string is matched without case sensitivity. This keyword requires no value. === header:"<string>" By default signature matching is performed on the respose body. By specifying a header name using the "header" keyword, this behavior is changed: the matching will occur on the header value. The header name is not case sensitive and header signatures are treated exactly the same as content signatures meaning that you can use multiple content strings and their modifiers. === mime:"<string>" The given value will be compared with the MIME type specified by the server. This is a "begins with" comparison so a partial MIME string, like "javascript/" will match with a server value of "javascript/foo". === memo:"<string>" The memo message is displayed in the report when the signature matches. The content should be a short but meaningful problem title. === sev:[1-4] The severity with which a signature match should be reported where: - 1 is High - 2 is Medium - 3 is Low - 4 is Info (default) === prob:"<string>" All issue types are defined in database.h and, by default, signature matches are reported with generic (signature) issue types. Using the prob keyword, a signature match can be reported as any other known issue. For example, issue 40401 stands for interesting files and is already used for several signatures. The advantage of using an existing issue ID is that it's severity and description will be used to report the signature match. === check:<int> Injection tests have their own ID which are specified in checks.h. Using the "check" keyword, it is possible to bind a signature to a specific injection test. The idea is to allow context specific signatures to be written. Take the following scenario as an example: During a scan, file disclosure tests might not fully succeed to highlight a vulnerability. Errors thrown during these tests can still reveal that there is more than likely a file disclosure problem. While generic server error detection will highlight these errors, it is more useful if we can detect that these errors are related to our tests and report them as such. === id:<int> The unique signature ID. This is for documentation purpose and for using the depend keyword which allows signature chaining. Note that the signature ID is also included in the report files (e.g. samples.js). === depend:<int> A signature can be made dependent on another signature by specifying it's signature ID as the value of this keyword. This means that the signature will be skipped unless the dependent signature was successfully matched already. One example use case could be a global signature that identifies a framework, say Wordpress, and dependent signatures that detect wordpress specific issues. === proto:"[http|https]" The "proto" keyword can be used to make a signature only applicable for either "http" or "https" type URLs. This changes the default behavior where every signature is applied to both http and https URLs. === report:"[once|always]" Some signatures are to find host specific problems and only need to be reported once. This can be acchieved by using report:"once"; This keywords default value is "always". �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������skipfish-2.10b/doc/dictionaries.txt�����������������������������������������������������������������0000440�0365020�0011610�00000023540�12057375132�016362� 0����������������������������������������������������������������������������������������������������ustar �heinenn�������������������������eng��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������This directory contains four alternative, hand-picked Skipfish dictionaries. PLEASE READ THESE INSTRUCTIONS CAREFULLY BEFORE PICKING ONE. This is *critical* to getting decent results in your scans. ------------------------ Key command-line options ------------------------ Skipfish automatically builds and maintain dictionaries based on the URLs and HTML content encountered while crawling the site. These dictionaries are extremely useful for subsequent scans of the same target, or for future assessments of other platforms belonging to the same customer. Exactly one read-write dictionary needs to be specified for every scan. To create a blank one and feed it to skipfish, you can use this syntax: $ touch new_dict.wl $ ./skipfish -W new_dict.wl [...other options...] In addition, it is almost always beneficial to seed the scanner with any number of supplemental, read-only dictionaries with common keywords useful for brute-force attacks against any website. Supplemental dictionaries can be loaded using the following syntax: $ ./skipfish -S supplemental_dict1.wl -S supplemental_dict2.wl \ -W new_dict.wl [...other options...] Note that the -W option should be used with care. The target file will be modified at the end of the scan. The -S dictionaries, on the other hand, are purely read-only. If you don't want to create a dictionary and store discovered keywords, you can use -W- (an alias for -W /dev/null). You can and should share read-only dictionaries across unrelated scans, but a separate read-write dictionary should be used for scans of unrelated targets. Otherwise, undesirable interference may occur. With this out of the way, let's quickly review the options that may be used to fine-tune various aspects of dictionary handling: -L - do not automatically learn new keywords based on site content. The scanner will still use keywords found in the specified dictionaries, if any; but will not go beyond that set. This option should not be normally used in most scanning modes; if supplied, the scanner will not be able to discover and leverage technology-specific terms and file extensions unique to the architecture of the targeted site. -G num - change jar size for keyword candidates. Up to <num> candidates are randomly selected from site content, and periodically retried during brute-force checks; when one of them results in a unique non-404 response, it is promoted to the dictionary proper. Unsuccessful candidates are gradually replaced with new picks, and then discarded at the end of the scan. The default jar size is 256. -R num - purge all entries in the read-write that had no non-404 hits for the last <num> scans. This option prevents dictionary creep in repeated assessments, but needs to be used with care: it will permanently nuke a part of the dictionary. -Y - inhibit full ${filename}.${extension} brute-force. In this mode, the scanner will only brute-force one component at a time, trying all possible keywords without any extension, and then trying to append extensions to any otherwise discovered content. This greatly improves scan times, but reduces coverage. Scan modes 2 and 3 shown in the next section make use of this flag. -------------- Scanning modes -------------- The basic dictionary-dependent modes you should be aware of (in order of the associated request cost): 1) Orderly crawl with no DirBuster-like brute-force at all. In this mode, the scanner will not discover non-linked resources such as /admin, /index.php.old, etc: $ ./skipfish -W- -L [...other options...] This mode is very fast, but *NOT* recommended for general use because the lack of dictionary bruteforcing will limited the coverage. Use only where absolutely necessary. 2) Orderly scan with minimal extension brute-force. In this mode, the scanner will not discover resources such as /admin, but will discover cases such as /index.php.old (once index.php itself is spotted during an orderly crawl): $ touch new_dict.wl $ ./skipfish -S dictionaries/extensions-only.wl -W new_dict.wl -Y \ [...other options...] This method is only slightly more request-intensive than #1, and therefore, is a marginally better alternative in cases where time is of essence. It's still not recommended for most uses. The cost is about 100 requests per fuzzed location. 3) Directory OR extension brute-force only. In this mode, the scanner will only try fuzzing the file name, or the extension, at any given time - but will not try every possible ${filename}.${extension} pair from the dictionary. $ touch new_dict.wl $ ./skipfish -S dictionaries/complete.wl -W new_dict.wl -Y \ [...other options...] This method has a cost of about 2,000 requests per fuzzed location, and is recommended for rapid assessments, especially when working with slow servers or very large services. 4) Normal dictionary fuzzing. In this mode, every ${filename}.${extension} pair will be attempted. This mode is significantly slower, but offers superior coverage, and should be your starting point. $ touch new_dict.wl $ ./skipfish -S dictionaries/XXX.wl -W new_dict.wl [...other options...] Replace XXX with: minimal - recommended starter dictionary, mostly focusing on backup and source files, about 60,000 requests per fuzzed location. medium - more thorough dictionary, focusing on common frameworks, about 140,000 requests. complete - all-inclusive dictionary, over 210,000 requests. Normal fuzzing mode is recommended when doing thorough assessments of reasonably responsive servers; but it may be prohibitively expensive when dealing with very large or very slow sites. ---------------------------- More about dictionary design ---------------------------- Each dictionary may consist of a number of extensions, and a number of "regular" keywords. Extensions are considered just a special subset of the keyword list. You can create custom dictionaries, conforming to this format: type hits total_age last_age keyword ...where 'type' is either 'e' or 'w' (extension or keyword), followed by a qualifier (explained below); 'hits' is the total number of times this keyword resulted in a non-404 hit in all previous scans; 'total_age' is the number of scan cycles this word is in the dictionary; 'last_age' is the number of scan cycles since the last 'hit'; and 'keyword' is the actual keyword. Qualifiers alter the meaning of an entry in the following way: wg - generic keyword that is not associated with any specific server-side technology. Examples include 'backup', 'accounting', or 'logs'. These will be indiscriminately combined with every known extension (e.g., 'backup.php') during the fuzzing process. ws - technology-specific keyword that are unlikely to have a random extension; for example, with 'cgi-bin', testing for 'cgi-bin.php' is usually a waste of time. Keywords tagged this way will be combined only with a small set of technology-agnostic extensions - e.g., 'cgi-bin.old'. NOTE: Technology-specific keywords that in the real world, are always paired with a single, specific extension, should be combined with said extension in the 'ws' entry itself, rather than trying to accommodate them with 'wg' rules. For example, 'MANIFEST.MF' is OK. eg - generic extension that is not specific to any well-defined technology, or may pop-up in administrator- or developer-created auxiliary content. Examples include 'bak', 'old', 'txt', or 'log'. es - technology-specific extension, such as 'php', or 'cgi', that are unlikely to spontaneously accompany random 'ws' keywords. Skipfish leverages this distinction by only trying the following brute-force combinations: /some/path/wg_keyword ('index') /some/path/ws_keyword ('cgi-bin') /some/path/wg_extension ('old') /some/path/ws_extension ('php') /some/path/wg_keyword.wg_extension ('index.old') /some/path/wg_keyword.ws_extension ('index.php') /some/path/ws_keyword.ws_extension ('cgi-bin.old') To decide between 'wg' and 'ws', consider if you are likely to ever encounter files such as ${this_word}.php or ${this_word}.class. If not, tag the keyword as 'ws'. Similarly, to decide between 'eg' and 'es', think about the possibility of encountering cgi-bin.${this_ext} or zencart.${this_ext}. If it seems unlikely, choose 'es'. For your convenience, all legacy keywords and extensions, as well as any entries detected automatically, will be stored in the dictionary with a '?' qualifier. This is equivalent to 'g', and is meant to assist the user in reviewing and triaging any automatically acquired dictionary data. Other notes about dictionaries: - Do not duplicate extensions as keywords - if you already have 'html' as an 'e' entry, there is no need to also create a 'w' one. - There must be no empty or malformed lines, or comments, in the wordlist file. Extension keywords must have no leading dot (e.g., 'exe', not '.exe'), and all keywords should be NOT url-encoded (e.g., 'Program Files', not 'Program%20Files'). No keyword should exceed 64 characters. - Any valuable dictionary can be tagged with an optional '#ro' line at the beginning. This prevents it from being loaded in read-write mode. - Tread carefully; poor wordlists are one of the reasons why some web security scanners perform worse than expected. You will almost always be better off narrowing down or selectively extending the supplied set (and possibly contributing back your changes upstream!), than importing a giant wordlist scored elsewhere. ����������������������������������������������������������������������������������������������������������������������������������������������������������������skipfish-2.10b/doc/skipfish.1�����������������������������������������������������������������������0000440�0365020�0011610�00000041601�12057377026�015050� 0����������������������������������������������������������������������������������������������������ustar �heinenn�������������������������eng��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������.\" vi:set wm=5 .TH SKIPFISH 1 "May 6, 2012" .SH NAME skipfish \- web application security scanner .SH SYNOPSIS .B skipfish .RI [ options ] " -o output-directory [ start-url | @url-file [ start-url2 ... ]]" .br .SH DESCRIPTION .PP \fBskipfish\fP is an active web application security reconnaissance tool. It prepares an interactive sitemap for the targeted site by carrying out a recursive crawl and dictionary-based probes. The resulting map is then annotated with the output from a number of active (but hopefully non-disruptive) security checks. The final report generated by the tool is meant to serve as a foundation for professional web application security assessments. .SH OPTIONS SUMMARY .PP .sp .if n \{\ .RS 4 .\} .fam C .ps -1 .nf .BB lightgray Authentication and access options: \-A user:pass \- use specified HTTP authentication credentials \-F host=IP \- pretend that \'host\' resolves to \'IP\' \-C name=val \- append a custom cookie to all requests \-H name=val \- append a custom HTTP header to all requests \-b (i|f|p) \- use headers consistent with MSIE / Firefox / iPhone \-N \- do not accept any new cookies Crawl scope options: \-d max_depth \- maximum crawl tree depth (16) \-c max_child \- maximum children to index per node (512) \-x max_desc \- maximum descendants to index per branch (8192) \-r r_limit \- max total number of requests to send (100000000) \-p crawl% \- node and link crawl probability (100%) \-q hex \- repeat probabilistic scan with given seed \-I string \- only follow URLs matching \'string\' \-X string \- exclude URLs matching \'string\' \-K string \- do not fuzz parameters named \'string\' \-D domain \- crawl cross\-site links to another domain \-B domain \- trust, but do not crawl, another domain \-Z \- do not descend into 5xx locations \-O \- do not submit any forms \-P \- do not parse HTML, etc, to find new links Reporting options: \-o dir \- write output to specified directory (required) \-M \- log warnings about mixed content / non\-SSL passwords \-E \- log all caching intent mismatches \-U \- log all external URLs and e\-mails seen \-Q \- completely suppress duplicate nodes in reports \-u \- be quiet, disable realtime progress stats Dictionary management options: \-W wordlist \- use a specified read\-write wordlist (required) \-S wordlist \- load a supplemental read\-only wordlist \-L \- do not auto\-learn new keywords for the site \-Y \- do not fuzz extensions in directory brute\-force \-R age \- purge words hit more than \'age\' scans ago \-T name=val \- add new form auto\-fill rule \-G max_guess \- maximum number of keyword guesses to keep (256) Performance settings: \-l max_req \- max requests per second (0\..000000) \-g max_conn \- max simultaneous TCP connections, global (40) \-m host_conn \- max simultaneous connections, per target IP (10) \-f max_fail \- max number of consecutive HTTP errors (100) \-t req_tmout \- total request response timeout (20 s) \-w rw_tmout \- individual network I/O timeout (10 s) \-i idle_tmout \- timeout on idle HTTP connections (10 s) \-s s_limit \- response size limit (200000 B) \-e \- do not keep binary responses for reporting Other settings: \-k duration \- stop scanning after the given duration h:m:s \--config file \- load specified configuration file .SH AUTHENTICATION AND ACCESS .PP Some sites require authentication, and skipfish supports this in different ways. First there is basic HTTP authentication, for which you can use the \-A flag. Second, and more common, are sites that require authentication on a web application level. For these sites, the best approach is to capture authenticated session cookies and provide them to skipfish using the \-C flag (multiple if needed). Last, you'll need to put some effort in protecting the session from being destroyed by excluding logout links with \-X and/or by rejecting new cookies with \-N. .IP "-F/--host <ip:hostname>" Using this flag, you can set the \'\fIHost:\fP\' header value to define a custom mapping between a host and an IP (bypassing the resolver). This feature is particularly useful for not-yet-launched or legacy services that don't have the necessary DNS entries. .IP "-H/--header <header:value>" When it comes to customizing your HTTP requests, you can also use the -H option to insert any additional, non-standard headers. This flag also allows the default headers to be overwritten. .IP "-C/--cookie <cookie:value>" This flag can be used to add a cookie to the skipfish HTTP requests; This is particularly useful to perform authenticated scans by providing session cookies. When doing so, keep in mind that cetain URLs (e.g. /logout) may destroy your session; you can combat this in two ways: by using the -N option, which causes the scanner to reject attempts to set or delete cookies; or by using the -X option to exclude logout URLs. .IP "-b/--user-agent <i|f|p>" This flag allows the user-agent to be specified where \'\fIi\fP\' stands for Internet Explorer, \'\fIf\fP\' for Firefox and \'\fIp\fP\' for iPhone. Using this flag is recommended in case the target site shows different behavior based on the user-agent (e.g some sites use different templates for mobiles and desktop clients). .IP "-N/--reject-cookies" This flag causes skipfish to ignore cookies that are being set by the site. This helps to enforce stateless tests and also prevent that cookies set with \'-C\' are not overwritten. .IP "-A/--auth <username:password>" For sites requiring basic HTTP authentication, you can use this flag to specify your credentials. .IP "--auth-form <URL>" The login form to use with form authentication. By default skipfish will use the form's action URL to submit the credentials. If this is missing than the login data is send to the form URL. In case that is wrong, you can set the form handler URL with --auth-form-target <URL> . .IP "--auth-user <username>" The username to be used during form authentication. Skipfish will try to detect the correct form field to use but if it fails to do so (and gives an error), then you can specify the form field name with --auth-user-field. .IP "--auth-pass <password>" The password to be used during form authentication. Similar to auth-user, the form field name can (optionally) be set with --auth-pass-field. .IP "--auth-verify-url <URL>" This URL allows skipfish to verify whether authentication was successful. This requires a URL where anonymous and authenticated requests are answered with a different response. .SH CRAWLING SCOPE .PP Some sites may be too big to scan in a reasonable timeframe. If the site features well-defined tarpits - for example, 100,000 nearly identical user profiles as a part of a social network - these specific locations can be excluded with -X or -S. In other cases, you may need to resort to other settings: -d limits crawl depth to a specified number of subdirectories; -c limits the number of children per directory; -x limits the total number of descendants per crawl tree branch; and -r limits the total number of requests to send in a scan. .IP "-d/--max-crawl-depth <depth>" Limit the depth of subdirectories being crawled (see above). .IP "-c/--max-crawl-child <childs>" Limit the amount of subdirectories per directory we crawl into (see above). .IP "-x/--max-crawl-descendants <descendants>" Limit the total number of descendants per crawl tree branch (see above). .IP "-r/--max-request-total <request>" The maximum number of requests can be limited with this flag. .IP "-p/--crawl-probability <0-100>" By specifying a percentage between 1 and 100%, it is possible to tell the crawler to follow fewer than 100% of all links, and try fewer than 100% of all dictionary entries. This \- naturally \- limits the completeness of a scan, but unlike most other settings, it does so in a balanced, non-deterministic manner. It is extremely useful when you are setting up time-bound, but periodic assessments of your infrastructure. .IP "-q/--seed <seed>" This flag sets the initial random seed for the crawler to a specified value. This can be used to exactly reproduce a previous scan to compare results. Randomness is relied upon most heavily in the -p mode, but also influences a couple of other scan management decisions. .IP "-I/--include-string <domain/path>" With this flag, you can tell skipfish to only crawl and test URLs that match a certain string. This can help to narrow down the scope of a scan by only whitelisting certain sections of a web site (e.g. \-I /shop). .IP "-X/--exclude-string <domain/path>" The \-X option can be used to exclude files / directories from the scan. This is useful to avoid session termination (i.e. by excluding /logout) or just for speeding up your scans by excluding static content directories like /icons/, /doc/, /manuals/, and other standard, mundane locations along these lines. .IP "-K/--skip-parameter <parameter name>" This flag allows you to specify parameter names not to fuzz. (useful for applications that put session IDs in the URL, to minimize noise). .IP "-D/--include-domain <domain>" Allows you to specify additional hosts or domains to be in-scope for the test. By default, all hosts appearing in the command-line URLs are added to the list - but you can use -D to broaden these rules. The result of this will be that the crawler will follow links and tests links that point to these additional hosts. .IP "-B/--trust-domain <domain>" In some cases, you do not want to actually crawl a third-party domain, but you trust the owner of that domain enough not to worry about cross-domain content inclusion from that location. To suppress warnings, you can use the \-B option .IP "-Z/--skip-error-pages" Do not crawl into pages / directories that give an error 5XX. .IP "-O/--no-form-submits" Using this flag will cause forms to be ignored during the scan. .IP "-P/--no-html-parsing" This flag will disable link extracting and effectively disables crawling. Using \-P is useful when you want to test one specific URL or when you want to feed skipfish a list of URLs that were collected with an external crawler. .SH TESTING SCOPE .PP .IP "--checks" EXPERIMENTAL: Displays the crawler injection tests. The output shows the index number (useful for \-\-checks\-toggle), the check name and whether the check is enabled. .IP "--checks-toggle <check1,check2,..>" EXPERIMENTAL: Every injection test can be enabled/disabled with using this flag. As value, you need to provide the check numbers which can be obtained with the \-\-checks flag. Multiple checks can be toggled via a comma separated value (i.e. \-\-checks\-toggle 1,2 ) .IP "--no-injection-tests" EXPERIMENTAL: Disables all injection tests for this scan and limits the scan to crawling and, optionally, bruteforcing. As with all scans, the output directory will contain a pivots.txt file. This file can be used to feed future scans. .SH REPORTING OPTIONS .PP .IP "-o/--output <dir>" The report wil be written to this location. The directory is one of the two mandatory options and must not exist upon starting the scan. .IP "-M/--log-mixed-content" Enable the logging of mixed content. This is highly recommended when scanning SSL-only sites to detect insecure content inclusion via non-SSL protected links. .IP "-E/--log-cache-mismatches" This will cause additonal content caching error to be reported. .IP "-U/--log-external-urls" Log all external URLs and email addresses that were seen during the scan. .IP "-Q/--log-unique-nodes" Enable this to completely suppress duplicate nodes in reports. .IP "-u/--quiet" This will cause skipfish to suppress all console output during the scan. .IP "-v/--verbose" EXPERIMENTAL: Use this flag to enable runtime reporting of, for example, problems that are detected. Can be used multiple times to increase verbosity and should be used in combination with \-u unless you run skipfish with stderr redirected to a file. .SH DICTIONARY MANAGEMENT .PP Make sure you've read the instructions provided in doc/dictionaries.txt to select the right dictionary file and configure it correctly. This step has a profound impact on the quality of scan results later on. .IP "-S/--wordlist <file>" Load the specified (read-only) wordlist for use during the scan. This flag is optional but use of a dictionary is highly recommended when performing a blackbox scan as it will highlight hidden files and directories. .IP "-W/--rw-wordlist <file>" Specify an initially empty file for any newly learned site-specific keywords (which will come handy in future assessments). You can use \-W\- or \-W /dev/null if you don't want to store auto-learned keywords anywhere. Typically you will want to use one of the packaged dictonaries (i.e. complete.wl) and possibly add a custom dictionary. .IP "-L/--no-keyword-learning" During the scan, skipfish will try to learn and use new keywords. This flag disables that behavior and should be used when any form of brute-forcing is not desired. .IP "-Y/--no-extension-brute" This flag will disable extension guessing during directory bruteforcing. .IP "-R <age>" Use of this flag allows old words to be purged from wordlists. It is intended to help keeping dictionaries clean when used in recurring scans. .IP "-T/--form-value <name=value>" Skipfish also features a form auto-completion mechanism in order to maximize scan coverage. The values should be non-malicious, as they are not meant to implement security checks \- but rather, to get past input validation logic. You can define additional rules, or override existing ones, with the \-T option (\-T form_field_name=field_value, e.g. \-T login=test123 \-T password=test321 - although note that \-C and \-A are a much better method of logging in). .IP "-G <max guesses>" During the scan, a temporary buffer of newly detected keywords is maintained. The size of this buffer can be changed with this flag and doing so influences bruteforcing. .SH PERFORMANCE OPTIONS The default performance setting should be fine for most servers but when the report indicates there were connection problems, you might want to tweak some of the values here. For unstable servers, the scan coverage is likely to improve when using low values for rate and connection flags. .IP "-l/--max-request-rate <rate>" This flag can be used to limit the amount of requests per second. This is very useful when the target server can't keep up with the high amount of requests that are generated by skipfish. Keeping the amount requests per second low can also help preventing some rate-based DoS protection mechanisms from kicking in and ruining the scan. .IP "-g/--max-connections <number>" The max simultaneous TCP connections (global) can be set with this flag. .IP "-m/--max-host-connections <number>" The max simultaneous TCP connections, per target IP, can be set with this flag. .IP "-f/--max-failed-requests <number>" Controls the maximum number of consecutive HTTP errors you are willing to see before aborting the scan. For large scans, you probably want to set a higher value here. .IP "-t/--request-timeout <timeout>" Set the total request timeout, to account for really slow or really fast sites. .IP "-w/--network-timeout <timeout>" Set the network I/O timeout. .IP "-i/--idle-timeout <timeout>" Specify the timeout for idle HTTP connections. .IP "-s/--response-size <size>" Sets the maximum length of a response to fetch and parse (longer responses will be truncated). .IP "-e/--discard-binary" This prevents binary documents from being kept in memory for reporting purposes, and frees up a lot of RAM. .IP "--flush-to-disk" This causes request / response data to be flushed to disk instead of being kept in memory. As a result, the memory usage for large scans will be significant lower. .SH EXAMPLES \fBScan type: config\fP .br skipfish \-\-config config/example.conf http://example.com .br .br \fBScan type: quick\fP .br skipfish \-o output/dir/ http://example.com .br .br \fBScan type: extensive bruteforce\fP .br skipfish [...other options..] \fI\-S dictionaries/complete.wl\fP http://example.com .br .br \fBScan type: without bruteforcing\fP .br skipfish [...other options..] -LY http://example.com .br \fBScan type: authenticated (basic)\fP .br skipfish [...other options..] \fI-A username:password\fP http://example.com .br \fBScan type: authenticated (cookie)\fP .br skipfish [...other options..] \-C jsession=myauthcookiehere \-X /logout http://example.com .br \fBScan type: flaky server\fP .br skipfish [...other options..] -l 5 -g 2 -t 30 -i 15 http://example.com .br .SH NOTES The default values for all flags can be viewed by running \'./skipfish -h\' . .SH AUTHOR skipfish was written by Michal Zalewski <lcamtuf@google.com>, with contributions from Niels Heinen <heinenn@google.com>, Sebastian Roschke <s.roschke@googlemail.com>, and other parties. .PP This manual page was written with the help of Thorsten Schifferdecker <tsd@debian.systs.org>. �������������������������������������������������������������������������������������������������������������������������������skipfish-2.10b/ChangeLog����������������������������������������������������������������������������0000640�0365020�0011610�00000044471�12057375132�014161� 0����������������������������������������������������������������������������������������������������ustar �heinenn�������������������������eng��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Version 2.10b: - Updated HTML tags and attributes that are checked for URL XSS injections to also include a few HTML5 specific ones - Updated test and description for semi-colon injection in HTML meta refresh tags (this is IE6 specific) - Relaxed HTML parsing a bit to allow spaces between HTML tag attributes and their values (e.g. "foo =bar"). - Major update of LFI tests by adding more dynamic tests (double encoding, dynamic amount of ../'s for web.xml). The total amount of tests for this vulnerability is now 40 per injection point. - The RFI test is now a separate test and no longer requires special compile options. The default RFI URL and it's payload check are still defined in src/config.h. - Using the --flush-to-disk flag will cause requests and responses to be flushed to disk which reduces the memory footprint. (especially noticable in large scans) - Fixed a bug where in some conditions (e.g. a page looks similar to another) links were not scraped from responses which lead to links to be missed (thanks to Anurag Chaurasia for reporting) - Added configuration file support with the --config flag. In config/example.conf you can find flags and examples. - Several signature keyword enhancements have been made. Most significant are the "header" keyword, which allows header matching and the "depend" keyword which allows signature chaining. - Fixed basic authentication which was broken per 2.08b. Cheers to Michael Stevens for reporting. - Fixed -k scheduling where 1:0:0 would count as a second in stead of an hour (also visa versa). Cheers to Claudio Criscione for reporting. - Small fix to compile time warnings Version 2.09b: - Fixed a crash that could be triggered during 404 fingerprint failures - Signature IDs for detected issues are now stored in the report JSON files. - Added mod_status, mod_info, MySQL dump, phpMyAdmin SQL dump and robots.txt signatures. - Improved the Flash and Silverlight crossdomain policy signatures to only warn about them when they use wildcards. Version 2.08b: - Added Host header XSS testing. - Added HTML encoding XSS tests to detect scenarios where our injection string ends up in an attributes that execute HTML encoded Javascript. For example: onclick. - Bruteforcing is now disabled for URLs that gave a directory listing. - Added subject alternate name checking for SSL certificates (cheers to Matt Caroll for his feedback) - Added signature matching (see doc/signatures.txt) which means a lot of the content based issues are no longer hardcoded. - Added active XSSI test. The passive XSSI stays (for now) but this active check is more acurate and will remove issues detected by the passive one if they cannot be confirmed. This reduces false positives - Added HTML tag XSS test which triggers when our payload is used as a tag attribute value but without quotes (courtesy of wavsep). - Added javascript: scheme XSS testing (courtesy of wavsep). - Added form based authentication. During these authenticated scans, skipfish will check if the session has ended and re-authenticates if necessary. - Fixed a bug where in slow scans the console output could mess up due to the high(er) refresh rate. - Fixed a bug where a missed response during the injection tests could result in a crash. (courtesy of Sebastian Roschke) - Restructure the source package a bit by adding a src/, doc/ and tools/ directory. Version 2.07b: -------------- - A bugfix to fprint_response() will help reduce false positives that could occur for differential tests (i.e. the query and shell injection tests) - We now suppress implicit cache warnings when dealing with 302, 303 and 307 redirects. - Added --no-checks which allows a scan to be run without any injection tests. This still allows bruteforcing and combines well with the new ability to load URLs from previous scan results. - We can now parse the pivots.txt, which can be found in the output directory of older scans. All URLs will be loaded which seriously speeds up recurring scans. - Directory bruteforcing now includes a content negotiation trick where a using a fake mime in the Accept: header will cause some servers to propose us files via a 406 response. - A horrible bug fix which caused instable pages not be marked as such. The result: false positives. Version 2.06b: -------------- - Crawler update which gives more control over the injection test scheduling. This comes with the --checks and --checks-toggle flags to display and enable/disable checks. - Pages where the response varies are no longer completely discarded. Instead now we only disable tests that require stability which increases scan coverage. - Split the traversal and disclosure test to increase coverage: traversal checks require stable pages, the disclosure checks can be performed on all. - Updated dictionaries and converted them to use the dictionary optimisations we introduced in 2.03b - Fixed offline report viewing (thanks to Sebastian Roschke) - Added NULL byte file disclosure tests - Added JSP inclusion error check to analyse.c - Added XSS injection tests for cookies - Directory listings are now reported as individual (info-type) issues - Added warning in case the negotiated SSL cipher turns out to be a weak one (leaving the cipher enumeration to network scanners) - Added experimental -v flag which can be used to enable (limited) runtime reporting. This output is written to stderr and should be redirected to a file, unless you use the -u flag. - The man page has been rewritten and now includes detailed descriptions and examples. - A whole bunch of small bug fixes Version 2.05b: -------------- - Fixed a NULL pointer crash when adding "callback" tests to JavaScript URLs that have a parameter with no value. - Bug fix in the redirect callback which expected 2 responses but since 2.04b actually should process 4. Version 2.04b: -------------- - Option -V eliminated in favor of -W / -S. - Option -l added to limit the maximum requests per second (contributed by Sebastian Roschke) - Option -k added to limit the maximum duration of a scan (contributed by Sebastian Roschke) - Support for #ro, -W-; related documentation changes. - HTTPS -> HTTP form detection. - Added more diverse traversal and file disclosure tests (including file:// scheme tests) - Improved injection detection in <script> sections, where a ' or " is all we need to inject js code. - Added check to see if our injection strings end up server Set-Cookie, Set-Cookie2 and Content-Type reponse headers - URLs that give us a Javascript response are now tested with a "callback=" parameter to find JSONP issues. - Fixed "response varies" bug in 404 detection where a stable page would be marked unstable. - Bugfix to es / eg handling in dictionaries. Version 2.03b: -------------- - Fixed a minor glitch in form parsing in analysis.c, courtesy of Niloufar Pahlevan Sadegh. - Two database.c bugfixes to wordlist handler, courtesy of Shaojie Wang. Version 2.02b: -------------- - Fixed a minor NULL pointer crash in -Y mode. Version 2.01b: -------------- - Substantial improvement to SQL injection checks. - Improvements to directory traversal checks (courtesy of Niels Heinen). - Fix to numerical brute-force logic. - Major improvement to directory brute force: much better duplicate elimination in some webserver configurations. - Added a check for attacker-controlled prefixes on inline responses. This currently leads to UTF-7 BOM XSS, Flash, Java attacks (thanks to Niels Heinen). Version 2.00b: -------------- - Minor bug fix to path parsing to avoid problems with /.$foo/, - Improved PHP error detection (courtesy of Niels Heinen), - Improved dictionary logic (courtesy of Niels Heinen) and new documentation of the same, - Improved support for file.ext keywords in the dictionary, - Fixed missing content_checks() in unknown_check_callback() (courtesy of Niels Heinen), - Improved an oversight in dictionary case sensitivity, - Improved pivots.txt data, - Support for supplementary read-only dictionaries (-W +dict), - Change to directory detection to work around a certain sneaky server behavior. - TODO: Revise dictionaries!!! Version 1.94b: -------------- - Proxy support! Currently only works for HTTP, put behind #ifdef PROXY_SUPPORT. - Change to prefix() and change_prefix() macros to limit the risk of bugs. Version 1.93b: -------------- - Major fix to URL XSS detection logic (courtesy of Niels Heinen). Version 1.92b: -------------- - Reading starting URLs from file is now supported (@ prefix). Version 1.90b / 1.91b: ---------------------- - Minor fix to pivots.txt. Version 1.89b: -------------- - Skipfish now saves all discovered URLs in a single file for third-party tools: pivots.txt. Version 1.88b: -------------- - Dictionary improvements, contd. Version 1.87b: -------------- - Dictionary improvements. Version 1.86b: -------------- - HTTP auth header value changed from "basic" to "Basic" to compensate for picky web frameworks. - Minor fix to time display code. Version 1.85b: -------------- - Minor refinements to the content analysis module. Version 1.84b: -------------- - Option -S removed. Version 1.83b: -------------- - Minor fix to -e behavior. Version 1.82b: -------------- - NULL pointer in is_javascript() fixed. Version 1.81b: -------------- - Fix to numerical SQL injection detector logic. Version 1.80b: -------------- - New option (-e) to delete binary payloads. - -J option is now obsolete (on by default). Version 1.79b: -------------- - Improvement to directory listing detector. Version 1.78b: -------------- - Fix to -J logic. Version 1.77b: -------------- - Further minor documentation and presentation tweaks. Version 1.76b: -------------- - Major clean-up of dictionary instructions. Version 1.75b: -------------- - iPhone U-A support added. Version 1.74b: -------------- - Non-HTTPS password form analysis added. Version 1.73b: -------------- - Silence some pointless compiler warnings on newer systems. Version 1.72b: -------------- - Minor beautification stuff. Version 1.71b: -------------- - Child signatures now exposed in the report, - Improvements to duplicate node detection, - sfscandiff tool added to compare reports. Version 1.70b: -------------- - Improved SQL syntax detection slightly to avoid phone number FP. - Removed obsolete allocator flags. Version 1.69b: -------------- - Minor improvements to parameter encoding, User-Agent controls. Version 1.68b: -------------- - Password detector improvement. Version 1.67b: -------------- - Improved directory detection logic. - Some dictionary updates. Version 1.65b: -------------- - Relaxed MIME matching on claimed CSS/JS that fails MIME sniffing logic. - Proper detection of @media in CSS. Version 1.64b: -------------- - Changed param injection check slightly to work better with WordPress. Version 1.62b: -------------- - Further refinements to content classifier. Version 1.60b: -------------- - Minor sniffer fix to better handle CSV file checks. Version 1.59b: -------------- - Fixed several file POI checks that depended on MIME information. Version 1.58b: -------------- - Descendant limit checks added. Version 1.57b: -------------- - Splash screen added (grr). Version 1.56b: -------------- - Path-based injection attacks now also carried out on file / pathinfo nodes. - Minor bugfix to try_list logic. - Slight tweak to form parsing to properly handle specified but empty action= strings. Version 1.55b: -------------- - Improved 404 directory no-parse checks. Version 1.54b: -------------- - Improved loop detector on mappings that only look at the last path segment. Version 1.53b: -------------- - Slight improvement to JSON discriminator. Version 1.52b: -------------- - Fixed HTTP read loop after 1.48b. Version 1.51b: -------------- - abort() instead of exit() in several places. - Cleaned up mem leak, incorrect use of ck_free() in IDN handling. Version 1.49b: -------------- - Minor improvement to the allocator, - Several directory listing signatures added. Version 1.48b: -------------- - A fix to SSL handling to avoid mystery fetch failures when talking to certain servers. Version 1.47b: -------------- - Minor tweaks around compiler warnings, etc. - Versioned directories now in use. - malloc_usable_size ditched in favor of djm's trick. - Minor performance tweaks as suggested by Jeff Johnson. Version 1.46b: -------------- - Security: fixed a potential read past EOB in scrape_response() on zero-sized payloads. Credit to Jeff Johnson. - Removed redundant fdopen() in dictionary management, Version 1.45b: -------------- - Minor aesthetic tweaks to the report viewer. - Report subnode ordering now a bit saner. Version 1.44b: -------------- - Significant improvement to numerical SQL injection detector. - Minor tweak to SQL message detection rules. Version 1.43b: -------------- - Improvement to reduce the likelihood of crawl loops: do not extract links if current page identical to parent. Version 1.42b: -------------- - Fix to SQL injection detection with empty parameters. Version 1.41b: -------------- - Logic change: if response varies, directory brute force is also skipped. Version 1.40b: -------------- - Command-line option not to descend into 5xx directories. Version 1.39b: -------------- - Option to override 'Range' header from the command line. Version 1.38b: -------------- - Decompression now honors user-specified size limits more reliably. - Retry logic corrected to account for certain Oracle servers. - Terminal I/O fix for debug mode. Version 1.37b: -------------- - NULL ptr with -F fixed. Version 1.36b: -------------- - Command-line support for parameters that should not be fuzzed. - In-flight URLs can be previewed by hitting 'return'. Version 1.35b: -------------- - Several new form autocomplete rules. Version 1.34b: -------------- - A small tweak to file / dir discriminator logic to accommodate quirky frameworks. Version 1.33b: -------------- - New SQL error signature added. - Improved tolerance for tabs in text page detector. Version 1.32b: -------------- - A minor fix for embedded URL auth detection. Version 1.31b: -------------- - Compilation with USE_COLOR commented out now works as expected. - Fix to detect <frame> tags. Version 1.30b: -------------- - Support for the (rare) <button> tag in forms. - Fixed compiler warning on some platforms. Version 1.29b: -------------- - Forms with no action= URL are now handled correctly. - New option (-u) to suppress realtime info, - Destination host displayed on stats screen. Version 1.27b: -------------- - Tweak to CFLAGS ordering to always enforce FORTIFY_SOURCE. - Man page added. Version 1.26b: -------------- - phtml added to the dictionary. - Yet another workaround for MALLOC_CHECK_. Grr. Version 1.25b: -------------- - A limit on the number of identically named path elements added. This is a last-resort check against endless recursion (e.g., for 'subdir' -> '.' symlinks). Version 1.24b: -------------- - XSS detection now accounts for commented out text. Version 1.23b: -------------- - A minor improvement to XHTML detection. - HTML vs XHTML mismatches no longer trigger a warning. Version 1.22b: -------------- - URL parser now accounts for its own \.\ injection pattern. Version 1.19b: -------------- - New ODBC POI added. - Apache config file detection tightened up. Version 1.18b: -------------- - Fix a potential NULL ptr deref with malformed Set-Cookie. - Another last-resort HTML detection pattern added. Version 1.17b: -------------- - JS detector refined not to trigger on certain text/plain inputs. Version 1.16b: -------------- - Fixed a typo introduced in 1.16 to index.html (d'oh). - Further refinements to Makefile CFLAGS / LIBS / LDFLAGS to keep package maintainers happy. Version 1.15b: -------------- - Better documentation on why certain issues are not reported by skipfish. - Another minor tweak to improve path mapping detection logic. Version 1.14b: -------------- - Several new wordlist entries, courtesy of Glastopf Honeypot: http://glastopf.org/index.php - A tweak to path mapping detection logic to detect certain path mappings that may result in crawl loops. - Makefile now honors external LDFLAGS, CFLAGS. - Some more documentation tweaks and rewrites. - PUT detection logic added. Version 1.13b: -------------- - Improved password, file form detection slightly. Version 1.12b: -------------- - Improved visibility of the KnownIssues page (reports, Makefile). - The location of assets/ directory is now configurable. Version 1.11b: -------------- - SIGWINCH support: you can now cleanly resize your window while scanning. - Typo in report category name fixed. - Terminal color fix (for users with non-standard color themes). - Corrected icons license (GPL -> LGPL). - Fixed a typo in -b ffox headers. - Fixed a potential NULL ptr deref when doing form parsing. Version 1.10b: -------------- - Fix to extensions-only.wl (some bad keywords removed). Version 1.09b: -------------- - Fix for a potential NULL ptr deref in probabilistic scan mode (<100%). Version 1.08b: -------------- - A minor improvement to XHTML / XML detection. Version 1.07b: -------------- - Several build fixes for FreeBSD, MacOS X (-I, -L paths). Version 1.06b: -------------- - Minor documentation updates, typos fixed, etc. Version 1.05b: -------------- - A more robust workaround for FORTIFY_SOURCE (MacOS X). Version 1.04b: -------------- - Workaround for *BSD systems with malloc J or Z options set by default (0x5a5a5a5a deref after realloc()). - A minor tweak to reject certain not-quite-URLs extracted from JS. Version 1.01b: -------------- - Workaround for a glitch in FORTIFY_SOURCE on Linux (causing crash on startup). Version 1.00b: -------------- - Initial public release. �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������skipfish-2.10b/signatures/��������������������������������������������������������������������������0000750�0365020�0011610�00000000000�12057375163�014564� 5����������������������������������������������������������������������������������������������������ustar �heinenn�������������������������eng��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������skipfish-2.10b/signatures/signatures.conf�����������������������������������������������������������0000440�0365020�0011610�00000002034�12057375131�017610� 0����������������������������������������������������������������������������������������������������ustar �heinenn�������������������������eng��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������############################################# ## ## Master signature file. ## # The mime signatures warn about server responses that have an interesting # mime. For example anything that is presented as php-source will likely # be interesting include signatures/mime.sigs # The files signature will use the content to determine if a response # is an interesting file. For example, a SVN file. include signatures/files.sigs # The messages signatures look for interesting server messages. Most # are based on errors, such as caused by incorrect SQL queries or PHP # execution failures. include signatures/messages.sigs # The apps signatures will help to find pages and applications who's # functionality is a security risk by default. For example, phpinfo() # pages that leak information or CMS admin interfaces. include signatures/apps.sigs # Context signatures are linked to injection tests. They look for strings # that are relevant to the current injection test and help to highlight # potential vulnerabilities. include signatures/context.sigs ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������skipfish-2.10b/signatures/files.sigs����������������������������������������������������������������0000440�0365020�0011610�00000010020�12057375132�016541� 0����������������������������������������������������������������������������������������������������ustar �heinenn�������������������������eng�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������� #################################### # INTERESTING PAGES / FILES # Detect private keys id:31001; sev:2; memo:"DSA private key"; \ mime:"text/plain"; \ content:"-----BEGIN DSA PRIVATE KEY-----"; depth:100; id:31002; sev:2; memo:"RSA private key"; \ mime:"text/plain"; \ content:"-----BEGIN RSA PRIVATE KEY-----"; depth:100; # SQL credentials id:31003; sev:3; memo:"SQL configuration or logs"; \ content:'ADDRESS=(PROTOCOL='; id:31004; sev:3; memo:"ODBC connect string"; \ content:";pwd="; \ content:";database="; depth:512; id:31005; sev:3; memo:"ODBC connect string"; \ content:"Data Source="; \ content:";Password="; depth:512; id:31006; sev:3; memo:"ODBC connect string"; \ content:"Provider="; \ content:";Password="; depth:512; id:31007; sev:3; memo:"ODBC connect string"; \ content:"Driver="; \ content:";Pwd="; depth:512; # Crossdomain & access policy files id:31008; sev:2; memo:"Flash cross-domain policy with wildcard"; \ content:"<cross-domain-policy>"; depth:512; \ content:'<allow-access-from domain="*"'; depth:50; id:31009; sev:4; memo:"Silverlight cross-domain policy with wildcard"; \ content:"<access-policy>"; depth:512; \ content:'<domain uri="*"/>'; depth:512; # Web.xml config file id:31010; sev:3; memo:"web.xml config file"; \ content:"<web-app"; depth:512; # SVN RCS data id:31011; sev:3; memo:"SVN RCS data"; \ mime:"text/plain"; \ content:"svn:special svn"; depth:256; id:31012; sev:3; memo:"SVN RCS data"; \ mime:"text/plain"; \ content:"SVN RCS data"; depth:256; id:31018; sev:3; memo:"SVN entries file"; \ mime:"text/plain"; \ content:"10"; depth:1; \ content:"dir"; depth:4; # Log files id:31013; sev:3; memo:"Apache access log"; \ content:"0] \"GET /"; depth:1024; id:31014; sev:3; memo:"Apache error log"; \ content:"[error] [client "; depth:1024; id:31015; sev:3; memo:"Microsoft IIS access log"; \ content:"0, GET, /"; depth:1024; # Generic robots.txt file id:31016; sev:4; memo:"robots.txt file"; \ content:"User-agent:"; depth:100; \ content:"Disallow: /"; # Libcurl cookie files are created by some PHP apps (e.g. wordpress plugins) id:31017; sev:3; memo:"libcurl cookie jar"; \ mime:"text/plain"; \ content:"# Netscape HTTP Cookie File"; depth:10; \ content:"# This file was generated by libcurl"; depth:200; # Signatures to detect SQL dumps id:31101; sev:2; memo:"MySQL dump database file"; \ mime:"text/plain"; \ content:"-- MySQL dump"; depth:1; \ content:"-- Host"; depth:256; \ content:"-- Server version"; id:31103; sev:2; memo:"phpMyAdmin database dump file"; \ mime:"text/plain"; \ content:" phpMyAdmin SQL Dump"; depth:3; \ content:" version"; \ content:"CREATE TABLE"; id:31104; sev:2; memo:"SQL script detected"; \ mime:"text/plain"; \ content:"DECLARE @"; \ content:"SET @"; \ content:"(SELECT|INSERT|DELETE|BACKUP|CREATE)"; type:"regex"; depth:1024; # Source code and scripts id:32001; sev:3; memo:"Java source"; \ content:"\nimport java."; depth:512; id:32002; sev:3; memo:"C/C++ source"; \ content:"\n#include"; depth:512; id:32003; sev:3; memo:"Shell script"; \ content:"#!/"; depth:1; id:32004; sev:3; memo:"PHP source"; \ content:!"# ?>"; \ content:!"<?import"; \ content:"<?"; \ content:!"xml"; depth:1; \ content:"?>"; id:32005; sev:3; memo:"JSP source"; \ content:"<%@"; \ content:"%>"; id:32006; sev:3; memo:"ASP source"; \ content:"<%"; \ content:"%>"; # These two need to be improved! id:32007; sev:3; memo:"DOS batch script"; \ content:"@echo "; depth:256; id:32008; sev:3; memo:"Windows shell script"; \ content:"(\"Wscript."; depth:256; ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������skipfish-2.10b/signatures/messages.sigs�������������������������������������������������������������0000440�0365020�0011610�00000007216�12057375132�017263� 0����������������������������������������������������������������������������������������������������ustar �heinenn�������������������������eng�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������� ##################################### # INTERESTING SERVER ERRORS # SQL related error strings id:21001; prob:40402; content:"<b>Warning</b>: MySQL: "; memo:"MySQL error string"; id:21002; prob:40402; content:"Unclosed quotation mark"; memo:"SQL error string"; id:21003; prob:40402; content:"java.sql.SQLException:"; memo:"Java SQL exception"; id:21004; prob:40402; content:"SqlClient.SqlException: Syntax error"; memo:"SqlClient exception"; id:21005; prob:40402; content:"PostgreSQL query failed"; memo:"PostgreSQL query failed"; id:21006; prob:40402; content:"Dynamic SQL Error"; memo:"SQL error string"; id:21007; prob:40402; content:"unable to perform query"; memo:"Possible SQL error string"; id:21008; prob:40402; content:"Microsoft OLE DB Provider for ODBC Drivers</font>"; memo:"OLE SQL error"; id:21009; prob:40402; content:"[Microsoft][ODBC SQL Server Driver]"; memo:"Microsoft SQL error"; id:21010; prob:40402; content:"Syntax error in string in query expression"; memo:"SQL syntax string"; id:21011; prob:40402; content:"You have an error in your SQL syntax; "; memo:"SQL syntax error"; id:21012; prob:40402; content:"Incorrect syntax near"; memo:"SQL syntax error"; id:21013; prob:40402; content:"[DM_QUERY_E_SYNTAX]"; memo:"SQL syntax error"; # Stacktraces and server errors id:22002; prob:40402; content:"<font face=\"Arial\" size=2>error '"; memo:"Microsoft runtime error"; id:22003; prob:40402; content:"[an error occurred while processing"; memo:"SHTML error"; id:22004; prob:40402; content:"Traceback (most recent call last):"; memo:"Python error"; id:22005; prob:40402; content:"<title>JRun Servlet Error"; memo:"JRun servlet error"; # Java exceptions id:22006; prob:40402; content:"Stacktrace:"; \ content:"javax.servlet."; \ content:"note The full stack trace"; \ memo:"Java server stacktrace"; id:22007; prob:40402; content:"at java.lang.Thread.run"; \ content:".java:"; \ memo:"Java runtime stacktrace"; id:22020; prob:40402; content:"type Exception report

"; \ content:"

description The server "; depth:512; \ memo:"Java server exception"; # PHP HTML and text errors. The text and HTML sigs can perhaps be merged, id:22008; prob:40402; content:"Fatal error: "; content:" on line "; depth:512; memo:"PHP error (HTML)"; id:22009; prob:40402; content:"Fatal error: "; content:" on line "; depth:512; memo:"PHP error (text)"; id:22010; prob:40402; content:"Parse error: "; content:" on line "; depth:512; memo:"PHP parse error (HTML)"; id:22011; prob:40402; content:"Parse error: "; content:" on line "; depth:512; memo:"PHP parse error (text)"; id:22012; prob:40402; content:"Notice: "; content:" on line "; depth:512; memo:"PHP notice (HTML)"; id:22013; prob:40402; content:"Notice: "; content:" on line "; depth:512; memo:"PHP notice (text)"; id:22014; prob:40402; content:"Strict Standards: "; content:" on line "; depth:512; memo:"PHP warning (HTML)"; id:22015; prob:40402; content:"Strict Standards: "; content:" on line "; depth:512; memo:"PHP warning (text)"; id:22016; prob:40402; content:"Catchable fatal error: "; content:" on line "; depth:512; memo:"PHP error (HTML)"; id:22017; prob:40402; content:"Catchable fatal error: "; content:" on line "; depth:512; memo:"PHP error (text)"; id:22018; prob:40402; content:"Warning: "; content:" on line "; depth:512; memo:"PHP warning (HTML)"; id:22019; prob:40402; content:"Warning: "; content:" on line "; depth:512; memo:"PHP warning (text)"; skipfish-2.10b/signatures/context.sigs0000440036502000116100000000246212057375131017135 0ustar heinenneng ############################### # CONTEXT SPECIFIC SIGNATURES # # During some injection tests we might not be able to determine that # the server is vulnerable. However, some server messages can give away # the fact that the underlying logic can likely be abused. The signatures # in this file look for such messages and are all linked to individual # injection tests. For example: we look for I/O errors during local file # disclosure attacks. ##################################################### # Signatures for check 8: File inclusion / disclosure # PHP errors for include(), fopen(), include_once(), file_get_contents(), etc id:51001; sev:2; check:8; content:"[phpMyAdmin '; depth:1024; \ content:'

, - are handled in a generic way. */ FIND_AND_MOVE(dirty_url, cur_str, "href"); if (!dirty_url) FIND_AND_MOVE(dirty_url, cur_str, "src"); } /* If we found no URL to speak of, we're done. */ if (!dirty_url) { ck_free(meta_url); goto next_tag; } /* De-quotify and decode the value. */ EXTRACT_ALLOC_VAL(dirty_url, dirty_url); clean_url = html_decode_param(dirty_url, 0); ck_free(dirty_url); ck_free(delete_dirty); ck_free(meta_url); if (!*clean_url) goto next_tag; test_add_link(clean_url, base ? base : req, res, link_type, 1); /* If we are dealing with a tag, we need to create a new dummy request to use as a referrer. */ if (set_base) { struct http_request* n = ck_alloc(sizeof(struct http_request)); n->pivot = req->pivot; if (!parse_url(clean_url, n, base ? base : req)) base = n; } next_tag: *tag_end = '>'; if (clean_url) ck_free(clean_url); } else tag_end = cur_str; /* Skip to next tag. */ if (*tag_end) cur_str = (u8*)strchr((char*)tag_end + 1, '<'); else cur_str = 0; } while (cur_str); cur_str = res->payload; /* PASS 2: Extract links from non-HTML body, JS, etc; add keywords. */ do { u32 clean_len, alpha_cnt = 0, lower_cnt = 0, lead = 0, seg_len; u8 *ext, *token, *clean_url, *tmp, *pos_at; u8 last = 0, saved; /* Skip leading whitespaces, terminators. */ seg_len = strspn((char*)cur_str, " \t\r\n<>\"'"); cur_str += seg_len; /* If there's a = character preceeded only by alnums or underscores, skip this chunk (to handle something=http://www.example.com/ neatly) */ tmp = cur_str; while (*tmp && (isalnum(*tmp) || *tmp == '_')) tmp++; if (*tmp == '=') cur_str = tmp + 1; if (!*cur_str) break; seg_len = strcspn((char*)cur_str + 1, " \t\r\n<>\"'") + 1; /* Extract the segment, decoding JS and HTML on the go. */ saved = cur_str[seg_len]; cur_str[seg_len] = 0; clean_url = html_decode_param(cur_str, 1); cur_str[seg_len] = saved; tmp = clean_url; /* We want the entire extracted segment to consist only of nice characters we would expect in a URL. If not, panic. */ while (*tmp) { if (!isalnum(*tmp) && !isspace(*tmp) && !strchr("_-.:@/?&=#%;$!+~()[]{}\\|^*", *tmp)) goto url_done; tmp++; } clean_len = tmp - clean_url; /* Strip trailing characters that are unlikely to appear in valid URLs anyway, and could be a part of some message. */ while (clean_len && strchr(".,:?!-$&", clean_url[clean_len-1])) clean_len--; clean_url[clean_len] = 0; /* URL CHECK 1: Things that start with ./ or ../ are obviously URLs. We do not make assumptins about syntax such as /foo/, though, as it could very well be a regex in a JS block. */ if (!prefix(clean_url, "./") || !prefix(clean_url, "../")) { add_link: test_add_link(clean_url, base ? base : req, res, 0, 0); goto url_done; } /* URL CHECK 2: Things that start with :// are quite clearly URLs. */ while (clean_url[lead] && (isalnum(clean_url[lead]))) lead++; if (lead && !prefix(clean_url + lead, "://") && clean_url[lead + 3]) goto add_link; /* URL CHECK 3: If the result ends with ., and contains a slash anywhere, assume URL (without that slash check, we would get duped by 'domain.com'. */ if (strchr((char*)clean_url, '/')) { i = 0; while ((ext = wordlist_get_extension(i++, 0))) { u32 ext_len = strlen((char*)ext); if (clean_len > ext_len + 2 && !strncasecmp((char*)clean_url + clean_len - ext_len, (char*)ext, ext_len) && clean_url[clean_len - ext_len - 1] == '.') goto add_link; } } if (!(pos_at = (u8*)strchr((char*)clean_url, '@'))) { /* URL CHECK 4: ?= syntax is strongly indicative of an URL (only if not e-mail). */ u8 *pos_qmark = (u8*)strchr((char*)clean_url, '?'), *pos_eq = (u8*)strchr((char*)clean_url, '='), *pos_amp = (u8*)strchr((char*)clean_url, '&'); if (pos_qmark && pos_eq && pos_qmark + 1 < pos_eq && pos_eq[1] && (!pos_amp || pos_amp > pos_eq) && pos_eq[1] != '=' && !strchr((char*)clean_url, '(') && !strchr((char*)clean_url, '[') && (u8*)strchr((char*)clean_url, ':') < pos_eq) goto add_link; } else if (log_ext_urls) { /* EMAIL CHECK: If the string uses a limited set of characters, starts with alpha, ahs at least one period after @, and both @ and the period are immediately followed by alpha - assume e-mail. */ u8 *pos_dot, *pos_qmark = (u8*)strchr((char*)clean_url, '?'); if (pos_qmark && pos_qmark > pos_at) *pos_qmark = 0; lead = 0; while (clean_url[lead] && (isalnum(clean_url[lead]) || strchr("._-+@", clean_url[lead]))) lead++; pos_dot = (u8*)strchr((char*)pos_at + 1, '.'); if (!clean_url[lead] && pos_at && pos_dot && isalpha(clean_url[0]) && isalpha(pos_at[1]) && isalpha(pos_dot[1])) { problem(PROB_MAIL_ADDR, req, res, clean_url, host_pivot(req->pivot), 0); goto url_done; } } /* LAST CHANCE: Try to detect base64; if the segment does not look like base64, add each segment to try_list. */ tmp = clean_url; while (*tmp) { if (isalpha(*tmp)) { alpha_cnt++; if (islower(*tmp)) lower_cnt++; } tmp++; } if (alpha_cnt > 20 && (lower_cnt * 100 / alpha_cnt) > 35 && (lower_cnt * 100 / alpha_cnt) < 65) goto url_done; token = clean_url; do { while (*token && !isalnum(*token)) token++; tmp = token; while (*tmp && isalnum(*tmp)) tmp++; if (!*tmp) last = 1; *tmp = 0; if (R(100) < GUESS_PROB) wordlist_add_guess(token); token = tmp + 1; } while (!last); url_done: ck_free(clean_url); cur_str += seg_len; } while (*cur_str); if (base) destroy_request(base); /* Phew! */ } /* Returns 1 if document looks like standalone CSS. */ static u8 is_css(struct http_response* res) { u8* text = res->payload; u8 first = 0, last = 0; if (res->css_type) return (res->css_type == 2); if (!text || !is_mostly_ascii(res)) return 0; do { /* Skip whitespaces... */ while (isspace(*text)) text++; /* Skip HTML, CSS comments. */ if (!prefix(text, ""); if (next) { tmp = next + 3; continue; } } /* Grab tag name. */ tag_name = ck_memdup(tmp, len + 1); tag_name[len] = 0; tmp += len; /* Handle all parameters. */ while (*tmp && *tmp != '>') { u8* param_name; u8* clean_val = NULL; u8* sfi_pos; /* Shoo, whitespaces. */ space_len = strspn((char*)tmp, " \t\r\n"); tmp += space_len; /* Grab parameter name. */ len = strcspn((char*)tmp, "=> \t\r\n"); param_name = ck_memdup(tmp, len + 1); param_name[len] = 0; tmp += len; /* Name followed by '='? Grab value. */ u8 quote = 0; if (*tmp == '=') { u32 vlen; u8 save; tmp++; if (*tmp == '\'') { quote = 1; vlen = strcspn((char*)++tmp, "'"); } else if (*tmp == '"') { quote = 1; vlen = strcspn((char*)++tmp, "\""); } else vlen = strcspn((char*)tmp, " \t\r\n>"); save = tmp[vlen]; tmp[vlen] = 0; clean_val = html_decode_param(tmp, 0); tmp[vlen] = save; tmp += vlen + quote; } /* CHECK X.X: Unquoted value can allow parameter XSS */ if (!quote && clean_val && !case_prefix(clean_val, "skipfish:")) problem(PROB_TAG_XSS, req, res, tag_name, req->pivot, 0); if (!strcasecmp((char*)tag_name, "script") && !strcasecmp((char*)param_name, "src")) remote_script = 1; /* CHECK 3.1: URL XSS and redirection issues. */ if ((!strcasecmp((char*)param_name, "href") || !strcasecmp((char*)param_name, "src") || !strcasecmp((char*)param_name, "action") || !strcasecmp((char*)param_name, "cite") || !strcasecmp((char*)param_name, "longdesc") || /* pivot, 0); /* A bit hairy, but in essence, links to attacker-supplied stylesheets or scripts are super-bad; OBJECTs and IFRAMEs are sorta noteworthy, depending on context; and A links are usually of little relevance. */ if (!case_prefix(clean_val, "http://skipfish.invalid/") || !case_prefix(clean_val, "//skipfish.invalid/")) { if (!strcasecmp((char*)tag_name, "script") || !strcasecmp((char*)tag_name, "link")) problem(PROB_USER_URL_ACT, req, res, tag_name, req->pivot, 0); else if (!strcasecmp((char*)tag_name, "a")) problem(PROB_USER_LINK, req, res, tag_name, req->pivot, 0); else problem(PROB_USER_URL, req, res, tag_name, req->pivot, 0); } } /* CHECK 3.2: META REFRESH XSSes, redirection. Also extract charset, if available */ if (!strcasecmp((char*)tag_name, "meta") && !strcasecmp((char*)param_name, "content") && clean_val) { u8* url = inl_strcasestr(clean_val, (u8*)"URL="); u8 semi_safe = 0; if (url) { url += 4; if (*url == '\'' || *url == '"') { url++; semi_safe = 1; } if (!case_prefix(url, "http://skipfish.invalid/") || !case_prefix(url, "//skipfish.invalid/") || !case_prefix(url, "skipfish:")) problem(PROB_URL_REDIR, req, res, (u8*)"injected URL in META refresh", req->pivot, 0); /* Unescaped semicolon in Refresh headers is unsafe with MSIE6: injecting an extra URL with javascript: scheme can lead to an XSS */ if ((strstr((char*)url, "/invalid/;") || inl_strcasestr(url, (u8*)"%2finvalid%2f;")) && !semi_safe) problem(PROB_URL_REDIR, req, res, (u8*)"unescaped semi-colon in META refresh (IE6)", req->pivot, 0); } else { u8* cset = inl_strcasestr(clean_val, (u8*)"charset="); if (cset) { if (res->meta_charset) { if (strcasecmp((char*)cset+8, (char*)res->meta_charset)) res->warn |= WARN_CFL_HDR; } else res->meta_charset = ck_strdup(cset + 8); } } } /* CHECK 3.3: JavaScript on*=, CSS style= parameters. */ if ((!case_prefix(param_name, "on") || !strcasecmp((char*)param_name, "style")) && clean_val) check_js_xss(req, res, clean_val); /* CHECK 3.4: What looks like our sfi tags, not fully escaped. */ if ((sfi_pos = (u8*)strstr((char*)param_name, "sfi")) && sscanf((char*)sfi_pos, "sfi%06uv%06u", &tag_id, &scan_id) == 2) { struct http_request* orig = get_xss_request(tag_id, scan_id); if (orig) problem(PROB_BODY_XSS, orig, res, (u8*) "injected 'sfi..' parameter value in a tag", req->pivot, 0); else problem(PROB_BODY_XSS, req, res, (u8*) "injected 'sfi...' parameter value in a tag (from previous" " scans)", req->pivot, 0); } ck_free(clean_val); ck_free(param_name); } /* CHECK 3.5: Phew. Parameters analyzed. Let's check for XSS tags... */ if (sscanf((char*)tag_name, "sfi%06uv%06u", &tag_id, &scan_id) == 2) { struct http_request* orig = get_xss_request(tag_id, scan_id); if (orig) problem(PROB_BODY_XSS, orig, res, (u8*) "injected '' tag seen in HTML", req->pivot, 0); else problem(PROB_BODY_XSS, req, res, (u8*) "injected '' tag seen in HTML (from previous scans)", req->pivot, 0); } /* CHECK 3.6: Non-remote SCRIPTs are of interest to JS XSS logic. */ if (!strcasecmp((char*)tag_name, "script") && !remote_script) { u8* next = inl_strcasestr(tmp, (u8*)""); if (next) *next = 0; check_js_xss(req, res, tmp); if (next) *next = '<'; /* Don't skip right away, as there might be some nested HTML inside. */ } /* CHECK 3.7: ...and so are stylesheets. */ if (!strcasecmp((char*)tag_name, "style")) { u8* next = inl_strcasestr(tmp, (u8*)""); if (next) *next = 0; check_js_xss(req, res, tmp); if (next) *next = '<'; } ck_free(tag_name); } else tmp = (u8*)strchr((char*)tmp, '<'); } while (tmp && *tmp); /* CHECK 4: Known exceptions / error pages, etc. */ detect_mime(req, res); res->sniffed_mime = (u8*)mime_map[res->sniff_mime_id][0]; check_for_stuff(req, res); binary_checks: detect_mime(req, res); res->sniffed_mime = (u8*)mime_map[res->sniff_mime_id][0]; /* No MIME checks on Content-Disposition: attachment responses. */ if ((tmp = GET_HDR((u8*)"Content-Disposition", &res->hdr)) && inl_strcasestr(tmp, (u8*)"attachment")) return 0; // if (!relaxed_mime) { // // /* CHECK 5A: Renderable documents that are not CSS or static JS are of // particular interest when it comes to MIME / charset mistakes. */ // // if (is_mostly_ascii(res) && !is_css(res) && (!is_javascript(res) || // (!strstr((char*)res->payload, "function ") && // !strstr((char*)res->payload, "function(")))) high_risk = 1; // // } else { /* CHECK 5B: Documents with skipfish signature strings echoed back are of particular interest when it comes to MIME / charset mistakes. */ u8* tmp = (u8*)strstr((char*)res->payload, "sfi"); if ((tmp && isdigit(tmp[3]) && tmp[9] == 'v') || strstr((char*)res->payload, "sfish") || strstr((char*)res->payload, "skipfish")) high_risk = 1; } /* CHECK 6: MIME mismatch? Ignore cases where the response had a valid MIME type declared in headers, but we failed to map it to a known value... and also failed to sniff. Mismatch between MIME_ASC_HTML and MIME_XML_XHTML is not worth complaining about; the same about JS or CSS responses being sniffed as "unknown ASCII". */ if (res->sniff_mime_id != res->decl_mime_id && !((res->decl_mime_id == MIME_ASC_JAVASCRIPT || res->decl_mime_id == MIME_ASC_CSS) && res->sniff_mime_id == MIME_ASC_GENERIC) && !(res->decl_mime_id == MIME_ASC_HTML && res->sniff_mime_id == MIME_XML_XHTML) && !(res->decl_mime_id == MIME_XML_XHTML && res->sniff_mime_id == MIME_ASC_HTML) && !(res->header_mime && !res->decl_mime_id && (res->sniff_mime_id == MIME_ASC_GENERIC || res->sniff_mime_id == MIME_BIN_GENERIC))) problem(high_risk ? PROB_BAD_MIME_DYN : PROB_BAD_MIME_STAT, req, res, res->sniffed_mime, req->pivot, 0); /* CHECK 7: application/octet-stream or text/plain; both have unintended consequences (but complain only if 3 didn't fire). */ else if (res->header_mime && (!strcasecmp((char*)res->header_mime, "application/octet-stream") || !strcasecmp((char*)res->header_mime, "text/plain"))) problem(high_risk ? PROB_GEN_MIME_DYN : PROB_GEN_MIME_STAT, req, res, res->sniffed_mime, req->pivot, 0); /* CHECK 8: Missing charset? */ if (is_mostly_ascii(res) && !res->meta_charset && !res->header_charset) problem(high_risk ? PROB_BAD_CSET_DYN : PROB_BAD_CSET_STAT, req, res, 0, req->pivot, 0); /* CHECK 9: Duplicate, inconsistent C-T or charset? */ if (is_mostly_ascii(res) && (res->warn & WARN_CFL_HDR || (res->meta_charset && res->header_charset && strcasecmp((char*)res->meta_charset, (char*)res->header_charset)))) problem(high_risk ? PROB_CFL_HDRS_DYN : PROB_CFL_HDRS_STAT, req, res, 0, req->pivot, 0); /* CHECK 10: Made up charset? */ if (res->header_charset || res->meta_charset) { u32 i = 0; while (valid_charsets[i]) { if (!strcasecmp((char*)valid_charsets[i], (char*)(res->header_charset ? res->header_charset : res->meta_charset))) break; i++; } if (!valid_charsets[i]) problem(high_risk ? PROB_BAD_CSET_DYN : PROB_BAD_CSET_STAT, req, res, res->header_charset ? res->header_charset : res->meta_charset, req->pivot, 0); } return 0; } /* Does MIME detection on a message. Most of this logic is reused from ratproxy, with some improvements and additions. */ static void detect_mime(struct http_request* req, struct http_response* res) { u8 sniffbuf[SNIFF_LEN]; s32 fuzzy_match = -1; if (res->sniff_mime_id) return; /* First, classify declared response MIME, if any. */ if (res->header_mime) { u32 i; for (i=0;iheader_mime, strlen((char*)mime_map[i][j] + 1))) fuzzy_match = i; } else { if (!strcasecmp((char*)mime_map[i][j], (char*)res->header_mime)) break; } j++; } if (mime_map[i][j]) break; } if (i != MIME_COUNT) { res->decl_mime_id = i; } else if (fuzzy_match != -1) { res->decl_mime_id = fuzzy_match; } } /* Next, work out the actual MIME that should be set. Mostly self-explanatory. */ memcpy(sniffbuf, res->payload, (res->pay_len > SNIFF_LEN - 1) ? (SNIFF_LEN - 1) : res->pay_len); sniffbuf[(res->pay_len > SNIFF_LEN - 1) ? (SNIFF_LEN - 1) : res->pay_len] = 0; if (is_mostly_ascii(res)) { /* ASCII checks. */ if (is_javascript(res)) { res->sniff_mime_id = MIME_ASC_JAVASCRIPT; return; } if (is_css(res)) { res->sniff_mime_id = MIME_ASC_CSS; return; } if (!prefix(sniffbuf, "%!PS")) { res->sniff_mime_id = MIME_ASC_POSTSCRIPT; return; } if (!prefix(sniffbuf, "{\\rtf")) { res->sniff_mime_id = MIME_ASC_RTF; return; } /* Adobe PDF (may be mostly ASCII in some cases). */ if (!prefix(sniffbuf, "%PDF")) { res->sniff_mime_id = MIME_EXT_PDF; return; } /* Several types of XML documents, taking into account that they might be missing their xmlns=, etc: */ if (strstr((char*)sniffbuf, "sniff_mime_id = MIME_XML_OPENSEARCH; return; } if (strstr((char*)sniffbuf, "") || strstr((char*)sniffbuf, "") || strstr((char*)sniffbuf, "") || strstr((char*)sniffbuf, "sniff_mime_id = MIME_XML_RSS; return; } if (strstr((char*)sniffbuf, "")) { res->sniff_mime_id = MIME_XML_ATOM; return; } if (strstr((char*)sniffbuf, "sniff_mime_id = MIME_XML_WML; return; } if (strstr((char*)sniffbuf, "sniff_mime_id = MIME_XML_SVG; return; } if (strstr((char*)sniffbuf, "")) { res->sniff_mime_id = MIME_XML_CROSSDOMAIN; return; } if (strstr((char*)sniffbuf, "sniff_mime_id = MIME_XML_XHTML; else res->sniff_mime_id = MIME_XML_GENERIC; return; } /* Do an unconvincing check for HTML once we ruled out known XML cases. */ if (inl_strcasestr(sniffbuf, (u8*)"") || inl_strcasestr(sniffbuf, (u8*)"href=")) { res->sniff_mime_id = MIME_ASC_HTML; return; } /* OK, we're out of ideas. Let's do a last-resort check for XML again, now that HTML is also off the table. */ if (strstr((char*)sniffbuf, "")) { res->sniff_mime_id = MIME_XML_GENERIC; return; } res->sniff_mime_id = MIME_ASC_GENERIC; } else { /* Binary checks. Start with simple images (JPG, GIF, PNG, TIFF, BMP). */ if (sniffbuf[0] == 0xFF && sniffbuf[1] == 0xD8 && sniffbuf[2] == 0xFF) { res->sniff_mime_id = MIME_IMG_JPEG; return; } if (!prefix(sniffbuf, "GIF8")) { res->sniff_mime_id = MIME_IMG_GIF; return; } if (sniffbuf[0] == 0x89 && !prefix(sniffbuf + 1, "PNG")) { res->sniff_mime_id = MIME_IMG_PNG; return; } if (!prefix(sniffbuf, "BM")) { res->sniff_mime_id = MIME_IMG_BMP; return; } if (!prefix(sniffbuf, "II") && sniffbuf[2] == 42 /* dec */) { res->sniff_mime_id = MIME_IMG_TIFF; return; } /* Next: RIFF containers (AVI, ANI, WAV). */ if (!prefix(sniffbuf, "RIFF")) { if (sniffbuf[8] == 'A') { if (sniffbuf[9] == 'C') res->sniff_mime_id = MIME_IMG_ANI; else res->sniff_mime_id = MIME_AV_AVI; } else res->sniff_mime_id = MIME_AV_WAV; return; } /* Cursor / ICO drama (we roll it back into BMP, because few sites make the distinction anyway, and cursors are unlikely to be attacker-supplied)... */ if (res->pay_len > 3 && !sniffbuf[0] && !sniffbuf[1] && sniffbuf[2] && !sniffbuf[3]) { res->sniff_mime_id = MIME_IMG_BMP; return; } /* Windows Media container (WMV, WMA, ASF). */ if (sniffbuf[0] == 0x30 && sniffbuf[1] == 0x26 && sniffbuf[2] == 0xB2) { res->sniff_mime_id = MIME_AV_WMEDIA; return; } /* MPEG formats, Ogg Vorbis, QuickTime, RealAudio, RealVideo. */ if (sniffbuf[0] == 0xFF && sniffbuf[1] == 0xFB) { res->sniff_mime_id = MIME_AV_MP3; return; } if (sniffbuf[0] == 0x00 && sniffbuf[1] == 0x00 && sniffbuf[2] == 0x01 && (sniffbuf[3] >> 4) == 0x0B) { res->sniff_mime_id = MIME_AV_MPEG; return; } if (!prefix(sniffbuf, "OggS")) { res->sniff_mime_id = MIME_AV_OGG; return; } if (sniffbuf[0] == 0x28 && !prefix(sniffbuf + 1, "RMF")) { res->sniff_mime_id = MIME_AV_RA; return; } if (sniffbuf[0] == 0x2E && !prefix(sniffbuf + 1, "RMF")) { res->sniff_mime_id = MIME_AV_RV; return; } if (!prefix(sniffbuf + 4, "free") || !prefix(sniffbuf + 4, "mdat") || !prefix(sniffbuf + 4, "wide") || !prefix(sniffbuf + 4, "pnot") || !prefix(sniffbuf + 4, "skip") || !prefix(sniffbuf + 4, "moov")) { /* Oookay, that was weird... */ res->sniff_mime_id = MIME_AV_QT; return; } /* Flash and FLV. */ if (!prefix(sniffbuf, "FLV")) { res->sniff_mime_id = MIME_AV_FLV; return; } if (!prefix(sniffbuf, "FCWS") || !prefix(sniffbuf, "CWS")) { res->sniff_mime_id = MIME_EXT_FLASH; return; } /* Adobe PDF. */ if (!prefix(sniffbuf, "%PDF")) { res->sniff_mime_id = MIME_EXT_PDF; return; } /* JAR versus ZIP. A bit tricky, because, well, they are both just ZIP archives. */ if (!prefix(sniffbuf, "PK") && sniffbuf[2] < 6 && sniffbuf[3] < 7) { if (inl_memmem(res->payload, res->pay_len, "META-INF/", 9)) res->sniff_mime_id = MIME_EXT_JAR; else res->sniff_mime_id = MIME_BIN_ZIP; return; } /* Java class files. */ if (sniffbuf[0] == 0xCA && sniffbuf[1] == 0xFE && sniffbuf[2] == 0xBA && sniffbuf[3] == 0xBE) { res->sniff_mime_id = MIME_EXT_CLASS; return; } /* The joy of Microsoft Office containers. */ if (res->pay_len > 512 && sniffbuf[0] == 0xD0 && sniffbuf[1] == 0xCF && sniffbuf[2] == 0x11 && sniffbuf[3] == 0xE0) { switch (sniffbuf[512]) { case 0xEC: res->sniff_mime_id = MIME_EXT_WORD; return; case 0xFD: case 0x09: res->sniff_mime_id = MIME_EXT_EXCEL; return; case 0x00: case 0x0F: case 0xA0: res->sniff_mime_id = MIME_EXT_PPNT; return; } } /* GZIP. Unfortunately, tar has no discernible header to speak of, so we just let it slide - few sites are serving tars on purpose anyway. */ if (sniffbuf[0] == 0x1F && sniffbuf[1] == 0x8B && sniffbuf[2] == 0x08) { res->sniff_mime_id = MIME_BIN_GZIP; return; } /* CAB. */ if (sniffbuf[0] == 'M' && sniffbuf[1] == 'S' && sniffbuf[2] == 'C' && sniffbuf[3] == 'F' && !sniffbuf[4]) { res->sniff_mime_id = MIME_BIN_CAB; return; } res->sniff_mime_id = MIME_BIN_GENERIC; } /* No more ideas? */ } /* "Stuff" means various error messages and cool, unusual content. For large files, this function may be sort-of expensive, but we need to look through the entire response, as not all messages are full-page errors, etc. */ static void check_for_stuff(struct http_request* req, struct http_response* res) { u8 sniffbuf[SNIFF_LEN]; u8* tmp; if (!res->pay_len || !is_mostly_ascii(res) || res->stuff_checked) return; /* We will use sniffbuf for checks that do not need to look through the entire file. */ memcpy(sniffbuf, res->payload, (res->pay_len > SNIFF_LEN - 1) ? (SNIFF_LEN - 1) : res->pay_len); sniffbuf[(res->pay_len > SNIFF_LEN - 1) ? (SNIFF_LEN - 1) : res->pay_len] = 0; res->stuff_checked = 1; /* Assorted interesting error messages. */ if (((tmp = (u8*)strstr((char*)res->payload, "ORA-")) || (tmp = (u8*)strstr((char*)res->payload, "FRM-"))) && isdigit(tmp[4]) && tmp[9] == ':') { problem(PROB_ERROR_POI, req, res, (u8*)"Oracle server error", req->pivot, 0); return; } if (inl_strcasestr(sniffbuf, (u8*)"\nAuthType ") || (inl_strcasestr(sniffbuf, (u8*)"\nOptions ") && ( inl_strcasestr(sniffbuf, (u8*)"\nOptions +") || inl_strcasestr(sniffbuf, (u8*)"\nOptions -") || inl_strcasestr(sniffbuf, (u8*)"\nOptions All") || inl_strcasestr(sniffbuf, (u8*)"\nOptions Exec") || inl_strcasestr(sniffbuf, (u8*)"\nOptions Follow") || inl_strcasestr(sniffbuf, (u8*)"\nOptions In") || inl_strcasestr(sniffbuf, (u8*)"\nOptions Mult") || inl_strcasestr(sniffbuf, (u8*)"\nOptions Sym")) ) || inl_strcasestr(sniffbuf, (u8*)"\npivot, 0); return; } if (res->sniff_mime_id == MIME_ASC_GENERIC) { u8* x = sniffbuf; /* Generic something:something[:...] password syntax. */ while (*x && (isalnum(*x) || strchr("._-+$", *x)) && (x - sniffbuf) < 64) x++; if (x != sniffbuf && *x == ':' && x[1] != '/' && x[1] != '.') { u8* start_x = ++x; while (*x && (isalnum(*x) || strchr("./*!+=$", *x)) && (x - sniffbuf) < 128) x++; if (*x == ':' || ((start_x != x) && (!*x || *x == '\r' || *x == '\n'))) problem(PROB_FILE_POI, req, res, (u8*) "Possible password file", req->pivot, 0); } } /* Add more directory signatures here... */ if (strstr((char*)sniffbuf, "") || strstr((char*)sniffbuf, "") || strstr((char*)sniffbuf, "

Index of /") || strstr((char*)sniffbuf, ">[To Parent Directory]<")) { problem(PROB_DIR_LIST, req, res, (u8*)"Directory listing", req->pivot, 0); /* Since we have the listing, we'll skip bruteforcing directory */ req->pivot->no_fuzz = 3; return; } if (res->sniff_mime_id == MIME_ASC_GENERIC) { u8* x = sniffbuf; u32 slashes = 0; /* Five slashes in the first line in a plaintext file should be a reasonably good check for CVS. */ while (*x && *x != '\n' && (x - sniffbuf) < 256) { if (*x == '/') slashes++; x++; } if (slashes == 5) { problem(PROB_FILE_POI, req, res, (u8*)"CVS RCS data", req->pivot, 0); return; } } if (strstr((char*)res->payload, "End Sub\n") || strstr((char*)res->payload, "End Sub\r")) { problem(PROB_FILE_POI, req, res, (u8*)"Visual Basic source", req->pivot, 0); return; } /* Plain text, and every line contains ;, comma, or |? */ if (res->sniff_mime_id == MIME_ASC_GENERIC) { u8* cur = res->payload; u8 all_delim = 0; u8* eol; do { u32 del = strcspn((char*)cur, ",|;\n"); eol = (u8*)strchr((char*)cur, '\n'); if(!eol) break; if (!cur[del] || cur[del] == '\n' || (cur[del] == ',' && cur[del+1] == ' ')) { all_delim = 0; break; } all_delim = 1; cur = eol + 1; } while (eol && cur && *cur); if (all_delim) { problem(PROB_FILE_POI, req, res, (u8*)"Delimited database dump", req->pivot, 0); return; } } /* This is a bit dodgy, but the most prominent sign of non-browser JS on Windows is the instantiation of obscure ActiveX objects to access local filesystem, create documents, etc. Unfortunately, some sites may also be creating obscure ActiveX objects; these would likely need to be just blacklisted here. */ if (is_javascript(res) && strstr((char*)res->payload, "new ActiveXObject(") && !strstr((char*)res->payload, "XMLHTTP") && !strstr((char*)res->payload, "ShockwaveFlash")) { problem(PROB_FILE_POI, req, res, (u8*)"server-side JavaScript source", req->pivot, 0); return; } } /* This is called when pivot enters PSTATE_DONE. It deletes payload of binary responses if requested and can flush pivot payloads to disk. */ void maybe_delete_payload(struct pivot_desc* pv) { u8 tmp[64]; u32 i; /* Return if there is nothing we should do */ if (!delete_bin && !flush_pivot_data) return; /* Delete binary payload when desired */ if (delete_bin && pv->res && pv->res->pay_len > 256 && !is_mostly_ascii(pv->res)) { ck_free(pv->res->payload); sprintf((char*)tmp, "[Deleted binary payload (%u bytes)]", pv->res->pay_len); pv->res->payload = ck_strdup(tmp); pv->res->pay_len = strlen((char*)tmp); } /* Flush issue req/res payloads */ if (flush_pivot_data) flush_payload(pv->req, pv->res); for (i=0;iissue_cnt;i++) { if (delete_bin && pv->issue[i].res && pv->issue[i].res->pay_len > 256 && !is_mostly_ascii(pv->issue[i].res)) { ck_free(pv->issue[i].res->payload); sprintf((char*)tmp, "[Deleted binary payload (%u bytes)]", pv->issue[i].res->pay_len); pv->issue[i].res->payload = ck_strdup(tmp); pv->issue[i].res->pay_len = strlen((char*)tmp); } /* Flush pivot req/res payloads */ if (flush_pivot_data) flush_payload(pv->issue[i].req, pv->issue[i].res); } } skipfish-2.10b/src/analysis.h0000440036502000116100000002331312057375131015157 0ustar heinenneng/* skipfish - content analysis --------------------------- Author: Michal Zalewski Copyright 2009, 2010, 2011 by Google Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ #ifndef _HAVE_ANALYSIS_C #include "types.h" #include "http_client.h" #include "database.h" extern u8 no_parse, /* Disable HTML link detection */ warn_mixed, /* Warn on mixed content */ log_ext_urls, /* Log all external URLs */ no_forms, /* Do not submit forms */ pedantic_cache; /* Match HTTP/1.0 and HTTP/1.1 */ /* Helper macros to group various useful checks: */ #define PIVOT_CHECKS(_req, _res) do { \ pivot_header_checks(_req, _res); \ content_checks(_req, _res); \ scrape_response(_req, _res); \ } while (0) #define RESP_CHECKS(_req, _res) do { \ content_checks(_req, _res); \ scrape_response(_req, _res); \ } while (0) /* Form autofill hints: */ extern u8** addl_form_name; extern u8** addl_form_value; extern u32 addl_form_cnt; /* Runs some rudimentary checks on top-level pivot HTTP responses. */ void pivot_header_checks(struct http_request* req, struct http_response* res); /* Adds a new item to the form hint system. */ void add_form_hint(u8* name, u8* value); /* Analyzes response headers (Location, etc), body to extract new links, keyword guesses, examine forms, mixed content issues, etc. */ void scrape_response(struct http_request* req, struct http_response* res); /* Analyzes response headers and body to detect stored XSS, redirection, 401, 500 codes, exception messages, source code, offensive comments, etc. */ u8 content_checks(struct http_request* req, struct http_response* res); /* Deletes payload of binary responses if requested. */ void maybe_delete_payload(struct pivot_desc* pv); /* Examines all tags up until , then adds them as parameters to current request. */ void collect_form_data(struct http_request* req, struct http_request* orig_req, struct http_response* orig_res, u8* cur_str, u8 is_post); /* Create a http_request from an HTML form structure */ struct http_request* make_form_req(struct http_request *req, struct http_request *base, struct http_response* res, u8* cur_str, u8* target); /* MIME detector output codes: */ #define MIME_NONE 0 /* Checks missing or failed */ #define MIME_ASC_GENERIC 1 /* Unknown, but mostly 7bit */ #define MIME_ASC_HTML 2 /* Plain, non-XML HTML */ #define MIME_ASC_JAVASCRIPT 3 /* JavaScript or JSON */ #define MIME_ASC_CSS 4 /* Cascading Style Sheets */ #define MIME_ASC_POSTSCRIPT 5 /* PostScript */ #define MIME_ASC_RTF 6 /* Rich Text Format */ #define MIME_XML_GENERIC 7 /* XML not recognized otherwise */ #define MIME_XML_OPENSEARCH 8 /* OpenSearch specification */ #define MIME_XML_RSS 9 /* Real Simple Syndication */ #define MIME_XML_ATOM 10 /* Atom feeds */ #define MIME_XML_WML 11 /* WAP WML */ #define MIME_XML_CROSSDOMAIN 12 /* crossdomain.xml (Flash) */ #define MIME_XML_SVG 13 /* Scalable Vector Graphics */ #define MIME_XML_XHTML 14 /* XML-based XHTML */ #define MIME_IMG_JPEG 15 /* JPEG */ #define MIME_IMG_GIF 16 /* GIF */ #define MIME_IMG_PNG 17 /* PNG */ #define MIME_IMG_BMP 18 /* Windows BMP (including ICO) */ #define MIME_IMG_TIFF 19 /* TIFF */ #define MIME_IMG_ANI 20 /* RIFF: ANI animated cursor */ #define MIME_AV_WAV 21 /* RIFF: WAV sound file */ #define MIME_AV_MP3 22 /* MPEG audio (commonly MP3) */ #define MIME_AV_OGG 23 /* Ogg Vorbis */ #define MIME_AV_RA 24 /* Real audio */ #define MIME_AV_AVI 25 /* RIFF: AVI container */ #define MIME_AV_MPEG 26 /* MPEG video */ #define MIME_AV_QT 27 /* QuickTime */ #define MIME_AV_FLV 28 /* Flash video */ #define MIME_AV_RV 29 /* Real video */ #define MIME_AV_WMEDIA 30 /* Windows Media audio */ #define MIME_EXT_FLASH 31 /* Adobe Flash */ #define MIME_EXT_PDF 32 /* Adobe PDF */ #define MIME_EXT_JAR 33 /* Sun Java archive */ #define MIME_EXT_CLASS 34 /* Sun Java class */ #define MIME_EXT_WORD 35 /* Microsoft Word */ #define MIME_EXT_EXCEL 36 /* Microsoft Excel */ #define MIME_EXT_PPNT 37 /* Microsoft Powerpoint */ #define MIME_BIN_ZIP 38 /* ZIP not recognized otherwise */ #define MIME_BIN_GZIP 39 /* GZIP */ #define MIME_BIN_CAB 40 /* CAB */ #define MIME_BIN_GENERIC 41 /* Binary, unknown type */ #define MIME_COUNT (MIME_BIN_GENERIC + 1) /* NULL-terminated MIME mapping sets. Canonical name should go first; do not put misspelled or made up entries here. This is used to match server intent with the outcome of MIME sniffing. */ #ifdef _VIA_ANALYSIS_C static char* mime_map[MIME_COUNT][8] = { /* MIME_NONE */ { 0 }, /* MIME_ASC_GENERIC */ { "text/plain", "?text/x-", "?text/vnd.", "?application/x-httpd-", "text/csv", 0 }, /* MIME_ASC_HTML */ { "text/html", 0 }, /* MIME_ASC_JAVASCRIPT */ { "application/javascript", "application/x-javascript", "application/json", "text/javascript", 0 }, /* MIME_ASC_CSS */ { "text/css", 0 }, /* MIME_ASC_POSTSCRIPT */ { "application/postscript", 0 }, /* MIME_ASC_RTF */ { "text/rtf", "application/rtf", 0 }, /* MIME_XML_GENERIC */ { "text/xml", "application/xml", 0 }, /* MIME_XML_OPENSEARCH */ { "application/opensearchdescription+xml", 0 }, /* MIME_XML_RSS */ { "application/rss+xml", 0 }, /* MIME_XML_ATOM */ { "application/atom+xml", 0 }, /* MIME_XML_WML */ { "text/vnd.wap.wml", 0 }, /* MIME_XML_CROSSDOMAIN */ { "text/x-cross-domain-policy", 0 }, /* MIME_XML_SVG */ { "image/svg+xml", 0 }, /* MIME_XML_XHTML */ { "application/xhtml+xml", 0 }, /* MIME_IMG_JPEG */ { "image/jpeg", 0 }, /* MIME_IMG_GIF */ { "image/gif", 0 }, /* MIME_IMG_PNG */ { "image/png", 0 }, /* MIME_IMG_BMP */ { "image/x-ms-bmp", "image/bmp", "image/x-icon", 0 }, /* MIME_IMG_TIFF */ { "image/tiff", 0 }, /* MIME_IMG_ANI */ { "application/x-navi-animation", 0 }, /* MIME_AV_WAV */ { "audio/x-wav", "audio/wav", 0 }, /* MIME_AV_MP3 */ { "audio/mpeg", 0 }, /* MIME_AV_OGG */ { "application/ogg", 0 }, /* MIME_AV_RA */ { "audio/vnd.rn-realaudio", "audio/x-pn-realaudio", "audio/x-realaudio", 0 }, /* MIME_AV_AVI */ { "video/avi", 0 }, /* MIME_AV_MPEG */ { "video/mpeg", "video/mp4", 0 }, /* MIME_AV_QT */ { "video/quicktime", 0 }, /* MIME_AV_FLV */ { "video/flv", "video/x-flv", 0 }, /* MIME_AV_RV */ { "video/vnd.rn-realvideo", 0 }, /* MIME_AV_WMEDIA */ { "video/x-ms-wmv", "audio/x-ms-wma", "video/x-ms-asf", 0 }, /* MIME_EXT_FLASH */ { "application/x-shockwave-flash", 0 }, /* MIME_EXT_PDF */ { "application/pdf", 0 }, /* MIME_EXT_JAR */ { "application/java-archive", 0 }, /* MIME_EXT_CLASS */ { "application/java-vm", 0 }, /* MIME_EXT_WORD */ { "application/msword", 0 }, /* MIME_EXT_EXCEL */ { "application/vnd.ms-excel", 0 }, /* MIME_EXT_PPNT */ { "application/vnd.ms-powerpoint", 0 }, /* MIME_BIN_ZIP */ { "application/zip", "application/x-zip-compressed", 0 }, /* MIME_BIN_GZIP */ { "application/x-gzip", "application/x-gunzip", "application/x-tar-gz", 0 }, /* MIME_BIN_CAB */ { "application/vnd.ms-cab-compressed", 0 }, /* MIME_BIN_GENERIC */ { "application/binary", "application/octet-stream", 0 } }; /* A set of headers that we check to see if our injection string ended up in their value. This list should only contain headers where control over the value could potentially be exploited. */ static const char* injection_headers[] = { "Set-Cookie", "Set-Cookie2", "Content-Type", 0, }; #endif /* _VIA_ANALYSIS_C */ #endif /* !_HAVE_ANALYSIS_H */ skipfish-2.10b/src/signatures.c0000640036502000116100000004644312057375131015526 0ustar heinenneng /* skipfish - Signature Matching ---------------------------------------- Author: Niels Heinen , Sebastian Roschke Copyright 2011, 2012 by Google Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ #include #include "pcre.h" #define _VIA_SIGNATURE_C #include "types.h" #include "config.h" #include "debug.h" #include "alloc-inl.h" #include "database.h" #include "signatures.h" struct signature** sig_list; u32 slist_max_cnt = 0, slist_cnt = 0; void dump_sig(struct signature *sig); /* Helper function to lookup a signature keyword. This makes it easier switch() signature keywords for, for example, storing their values in a struct */ static s32 lookup(u8* key) { u32 i; for (i=0; lookuptable[i].name; i++) { if (!strcmp((char*)key, lookuptable[i].name)) return lookuptable[i].id; } return -1; } /* Helper function for parse_sig which takes a string where some characters might be escaped. It returns a copy with the \'s removed so \"aa\" becomes "aa" */ static u8* unescape_str(u8* str) { u32 k = 0, i = 0; u32 len = strlen((char*)str) + 1; u8* ret = ck_alloc(len); for (i=0; iseverity < 0 || sig->severity > 4) { WARN("Signature %d has an invalid severity: %d\n", sig->id, sig->severity); ret = 1; } if (!sig->content_cnt && !sig->mime) { WARN("Signature %d has no \"content\" nor \"mime\" string\n", sig->id); ret = 1; } if (!sig->memo) { WARN("Signature %d has no memo string\n", sig->id); ret = 1; } return ret; } static u8 compile_content(struct content_struct *content) { const char* pcre_err; int pcre_err_offset; u32 options = PCRE_MULTILINE; if (content->type != TYPE_REGEX) return 1; if (content->nocase) options |= PCRE_CASELESS; content->pcre_sig = pcre_compile((char*)content->match_str, options, &pcre_err, &pcre_err_offset, 0); if (!content->pcre_sig) return 1; /* This is extra */ content->pcre_extra_sig = pcre_study(content->pcre_sig, 0, &pcre_err); return 0; } /* Look up a signature. */ static struct signature* get_signature(u32 id) { u32 i; for (i=0; iid == id) return sig_list[i]; } return NULL; } /* Parses the signature string that is given as the first parameter and returns a signature struct */ struct signature* parse_sig(u8* tline) { u8 *val, *name, *line; u8 in_quot = 0; u8 no = 0; struct content_struct *lcontent = NULL; line = tline = ck_strdup(tline); struct signature* sig = ck_alloc(sizeof(struct signature)); while (line) { /* Skip spaces */ name = line; no = 0; while (name && isspace(*name)) name++; /* Split on the value and, for now, return NULL when there is no value. We check for keyworks without value, like nocase. */ val = name; while (*val && val++) { if (*val == ':') { *val = 0; val++; break; } if (*val == ';') break; } if(!*val) { ck_free(sig); ck_free(tline); return 0; } /* Check if ! is present and set 'not' */ if (*val == '!') { no = 1; val++; } /* Move to value and check if quoted */ if (*val && (*val == '\'' || *val == '"')) { in_quot = *val; val++; } /* Find the end of the value string */ line = val; while (*line && (in_quot || *line != ';') && line++) { if(*line == '\\') { line++; continue; } /* End of quotation? */ if (in_quot && *line == in_quot) { in_quot = 0; *line = 0; line++; continue; } } *line = 0; switch (lookup(name)) { case SIG_ID: sig->id = atoi((char*)val); break; case SIG_CHK: sig->check = atoi((char*)val); break; case SIG_CONTENT: /* If we already have a content struct, try to compile it (when regex) and create a new content struct */ if (lcontent) { /* Compile and bail out on failure */ if (lcontent->type == TYPE_REGEX && compile_content(lcontent)) FATAL("Unable to compile regex in: %s", tline); } if (sig->content_cnt > MAX_CONTENT) FATAL("Too many content's in line: %s", tline); sig->content[sig->content_cnt] = ck_alloc(sizeof(struct content_struct)); lcontent = sig->content[sig->content_cnt]; lcontent->match_str = unescape_str(val); lcontent->match_str_len = strlen((char*)lcontent->match_str); lcontent->no = no; sig->content_cnt++; break; case SIG_MEMO: sig->memo = unescape_str(val); break; case SIG_PCRE_MATCH: if (!lcontent) { WARN("Found 'regex_match' before 'content', skipping.."); break; } lcontent->cap_match_str = unescape_str(val); break; case SIG_TYPE: if (!lcontent) { WARN("Found 'type' before 'content', skipping.."); break; } /* 0 is plain to which we default so this only checks if the rule wants a regex */ if (!strcmp((char*)val, "regex")) lcontent->type = (u8)TYPE_REGEX; break; case SIG_DIST: if (!lcontent) { WARN("Found 'distance' before 'content', skipping.."); break; } lcontent->distance = atoi((char*)val); break; case SIG_OFFSET: if (!lcontent) { WARN("Found 'offset' before 'content', skipping.."); break; } lcontent->offset = atoi((char*)val); break; case SIG_REPORT: /* TODO(heinenn): support "never" */ if (!strcmp((char*)val, "once")) { sig->report = REPORT_ONCE; } else { sig->report = REPORT_ALWAYS; } break; case SIG_DEPEND: /* Chain to another signature */ sig->depend = get_signature(atoi((char*)val)); break; case SIG_SEV: sig->severity = atoi((char*)val); break; case SIG_PROB: sig->prob = atoi((char*)val); break; case SIG_DEPTH: if (!lcontent) { WARN("Found 'depth' before 'content', skipping.."); break; } lcontent->depth = atoi((char*)val); break; case SIG_CASE: if (!lcontent) { WARN("Found 'case' before 'content', skipping.."); break; } lcontent->nocase = 1; break; case SIG_PROTO: if (!strcmp((char*)val, "https")) { sig->proto = PROTO_HTTPS; } else if (!strcmp((char*)val, "http")) { sig->proto = PROTO_HTTP; } else { WARN("Unknown proto specified: %s (skipping)", val); } break; case SIG_MIME: sig->mime = unescape_str(val); break; case SIG_HEADER: if (sig->header) { FATAL("Found multiple 'header' keywords, skipping.."); break; } sig->header = unescape_str(val); break; case SIG_CODE: sig->rcode = atoi((char*)val); break; default: FATAL("Unknown keyword: %s", name); } /* Proceed, or stop when we're at the end of the line. Since 'line' still points to ; , we'll increase it first */ if(line) line++; /* Now if we're at EOF or EOL, we'll stop */ if (!line || (*line == '\r' || *line == '\n')) break; } ck_free(tline); /* Compile the last content entry */ if (lcontent) compile_content(lcontent); /* Done parsing! Now validate the signature before returning it */ if (check_signature(sig)) { DEBUG("Skipping signature (didn't validate)\n"); destroy_signature(sig); return NULL; } /* Dump the signature when debugging */ #ifdef LOG_STDERR dump_sig(sig); #endif /* !LOG_STDERR */ return sig; } /* Loads the signature list from file 'fname' and parses each line. Whenever a * line starts with #include , the file will be parsed as well to make signature * management really easy. */ void load_signatures(u8* fname) { FILE* in; u8 tmp[MAX_SIG_LEN + 1]; u8 include[MAX_SIG_FNAME + 1]; u32 in_cnt = 0; u8 fmt[20]; struct signature *sig; in = fopen((char*)fname, "r"); if (!in) { PFATAL("Unable to open signature list '%s'", fname); return; } /* Create a signature list */ if (!sig_list) sig_list = ck_alloc(sizeof(struct signature*) * MAX_SIG_CNT); u32 sig_off = 0; s32 tmp_off = 0; while (fgets((char*)tmp + sig_off, MAX_SIG_LEN - sig_off, in)) { if (tmp[0] == '#' || tmp[0] == '\n' || tmp[0] == '\r') continue; /* We concat signature lines that end with a trailing \ */ tmp_off = strlen((char*)tmp) - 1; while (tmp_off && isspace(tmp[tmp_off])) tmp_off--; if (tmp[tmp_off] == '\\') { sig_off = tmp_off; continue; } /* When the include directive is present, we'll follow it */ if (!strncmp((char*)tmp, "include ", 8)) { /* Check the amount of files included already. This is mostly to protect * against include loops */ if (in_cnt++ > MAX_SIG_INCS) FATAL("Too many signature includes (max: %d)\n", MAX_SIG_INCS); sprintf((char*)fmt, "%%%u[^\x01-\x1f]", MAX_SIG_FNAME); sscanf((char*)tmp + 8,(char*)fmt, (char*)include); DEBUG("- Including signature file: %s\n", include); load_signatures(include); continue; } sig = parse_sig(tmp); sig_off = 0; if(sig == NULL) continue; if (slist_cnt >= MAX_SIG_CNT) FATAL("* Signature list is too large (max = %d)\n", MAX_SIG_CNT); sig_list[slist_cnt++] = sig; } DEBUG("*- Signatures processed: %s (total sigs %d)\n", fname, slist_cnt); fclose(in); } /* Helper function to check if a certain signature matched. This is, for example, useful to chain signatures. */ static u8 matched_sig(struct pivot_desc *pv, u32 sid) { u32 i; if (!pv->issue_cnt) return 0; /* Will optimise this later by changing the way signature match information is stored per pivot */ for (i=0; iissue_cnt; i++) { if (pv->issue[i].sid == sid) return 1; } return 0; } u8 match_signatures(struct http_request *req, struct http_response *res) { u8 pcre_ret, matches = 0; u8 *payload, *match = NULL; u32 ovector[PCRE_VECTOR]; struct pivot_desc *tpv; u32 ccnt, pay_len, j = 0, i = 0; struct content_struct *content = NULL; for ( j = 0; j < slist_cnt; j++ ) { /* Check the signature is protocol specific (e.g. SSL-only) */ if (sig_list[j]->proto && (sig_list[j]->proto != req->proto)) continue; /* Check if the signature is only intended for one of the active tests. */ if (sig_list[j]->check && (req->pivot->check_id > 0 && req->pivot->check_id != sig_list[j]->check)) { continue; } /* Compare response code */ if (sig_list[j]->rcode && sig_list[j]->rcode != res->code) continue; /* If dependent on another signature, first check if that signature matches. If it than we're done with this sig */ if (sig_list[j]->depend) { tpv = req->pivot; /* If report == 1 then we need to look at the host pivot */ if (sig_list[j]->depend->report == REPORT_ONCE) tpv = host_pivot(req->pivot); /* Do the check */ if(!matched_sig(tpv, sig_list[j]->depend->id)) continue; } /* Compare the mime types */ if (sig_list[j]->mime && res->header_mime) { /* Skip if the mime doesn't match */ if (strncmp((char*)res->header_mime, (char*)sig_list[j]->mime, strlen((char*)sig_list[j]->mime))) continue; /* We've got a signature match with the mime is the same and no content * string exists. This is useful for reporting interesting mime types, * such as application/x-httpd-php-source */ if (!sig_list[j]->content_cnt) { signature_problem(sig_list[j], req, res); continue; } } /* Nice, so here the matching will start! Unless... there are not content * strings, or when the response is mainly binary data. */ if (res->doc_type == 1 || !sig_list[j]->content_cnt) continue; /* If this is a header signature, than this is our payload for matching. Else, we'll take the response body */ if (!sig_list[j]->header) { payload = res->payload; pay_len = res->pay_len; } else { /* Header is the payload */ payload = GET_HDR(sig_list[j]->header, &res->hdr); /* A header might very well not be present which means we can continue with the next signature */ if (!payload) continue; pay_len = strlen((char*)payload); } matches = 0; for (i=0; pay_len > 0 && icontent_cnt; i++) { content = sig_list[j]->content[i]; /* If there is an offset, we will apply it to the current payload pointer */ if (content->offset) { if (pay_len < content->offset) break; payload = payload + content->offset; } /* Use the specified maximum depth to search the string. If no depth is specified, we search the entire buffer. Note that this is relative to the beginning of the buffer _or_ the previous content match */ if (content->depth) pay_len = content->depth; if (content->distance && pay_len > content->distance) { payload += content->distance; pay_len -= content->distance; } match = 0; if (content->type == TYPE_PLAIN) { if (content->nocase) { match = inl_findstrcase(payload, content->match_str, pay_len); } else { match = inl_findstr(payload, content->match_str, pay_len); } if (match && !content->no) { /* Move the match pointer to allow offset to be applied relative to the previous content-match */ payload = match + content->match_str_len; pay_len -= content->match_str_len; matches++; } else if(!match && content->no) { matches++; } else break; } else if(content->type == TYPE_REGEX) { /* Lets do the pcre matching */ pcre_ret = (pcre_exec(content->pcre_sig, content->pcre_extra_sig, (char*)payload, pay_len, 0, 0, (int*)ovector, PCRE_VECTOR) >= 0); if (!content->no && pcre_ret) { /* We care about the first match and update the match pointer to the first byte that follows the matching string */ /* Check if a string was captured */ pcre_fullinfo(content->pcre_sig, NULL, PCRE_INFO_CAPTURECOUNT, &ccnt); if (ccnt > 0 && content->cap_match_str) { /* In pcre we trust.. We only allow one string to be captured so while we could loop over ccnt: we just grab the first string. */ u32 cap_size = ovector[3] - ovector[2]; if (cap_size > MAX_PCRE_CSTR_SIZE) cap_size = MAX_PCRE_CSTR_SIZE; u8 *pcre_cap_str = ck_alloc(cap_size + 1); if (pcre_copy_substring((char*)payload, (int*)ovector, 2, 1, (char*)pcre_cap_str, cap_size)) { /* No match? break the loop */ if (inl_strcasestr(pcre_cap_str, content->cap_match_str)) { ck_free(pcre_cap_str); break; } } ck_free(pcre_cap_str); } /* Move to the first byte after the match */ payload = payload + ovector[1]; pay_len -= (ovector[1] - ovector[0]); /* pay_len is checked in the next match */ matches++; } else if(!pcre_ret && content->no) { matches++; } else break; } } /* for i loop */ if (matches == sig_list[j]->content_cnt) signature_problem(sig_list[j], req, res); } /* for j loop */ return 0; } /* Wrapper for reporting a signature problem */ void signature_problem(struct signature *sig, struct http_request *req, struct http_response *res) { #ifdef _SIGNATURE_TEST DEBUG("signature_problem() called for %d (%s)\n", sig->id, sig->memo); #else /* Register the problem, together with the sid */ register_problem((sig->prob ? sig->prob : sig_serv[sig->severity]), sig->id, req, res, (sig->memo ? sig->memo : (u8*)""), sig->report ? host_pivot(req->pivot) : req->pivot, 0); #endif } void destroy_signature(struct signature *sig) { u32 i; if (sig->memo) ck_free(sig->memo); if (sig->mime) ck_free(sig->mime); if (sig->header) ck_free(sig->header); for (i=0; icontent_cnt; i++) { ck_free(sig->content[i]->match_str); if (sig->content[i]->pcre_sig) free(sig->content[i]->pcre_sig); if (sig->content[i]->pcre_extra_sig) free(sig->content[i]->pcre_extra_sig); ck_free(sig->content[i]); } ck_free(sig); } void destroy_signature_lists() { u32 i; for (i = 0; i < slist_cnt; i++) destroy_signature(sig_list[i]); ck_free(sig_list); } /* For debugging: dump a signature */ void dump_sig(struct signature *sig) { u32 i; DEBUG("\n=== New signature loaded ===\n"); DEBUG(" id = %d\n", sig->id); DEBUG(" severity = %d\n", sig->severity); DEBUG(" content # = %d\n", sig->content_cnt); for (i=0; icontent_cnt; i++) { DEBUG(" %d. match_str = %s\n", i, sig->content[i]->match_str); DEBUG(" %d. type = %s\n", i, sig->content[i]->type ? "REGEX" : "STRING"); DEBUG(" %d. offset = %d\n", i, sig->content[i]->offset); DEBUG(" %d. depth = %d\n", i, sig->content[i]->depth); DEBUG(" %d. position = %d\n", i, sig->content[i]->distance); DEBUG(" %d. nocase = %d\n", i, sig->content[i]->nocase); DEBUG(" %d. no = %d\n", i, sig->content[i]->no); } /* And now the optional fields */ if (sig->memo) DEBUG(" memo = %s\n", sig->memo); if (sig->mime) DEBUG(" mime = %s\n", sig->mime); if (sig->rcode) DEBUG(" code = %d\n", sig->rcode); DEBUG(" depend = %d\n", sig->depend ? sig->depend->id : 0); DEBUG(" header = %s\n", sig->header ? (char*)sig->header : (char*)""); switch (sig->proto) { case '0': DEBUG(" proto = HTTP/HTTPS\n"); break; case PROTO_HTTP: DEBUG(" proto = HTTP\n"); break; case PROTO_HTTPS: DEBUG(" proto = HTTPS\n"); } } skipfish-2.10b/src/auth.c0000440036502000116100000002024712057375131014273 0ustar heinenneng/* skipfish - form authentication ------------------------------ Author: Niels Heinen Copyright 2012 by Google Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ #include #define _VIA_AUTH_C #include "debug.h" #include "config.h" #include "types.h" #include "http_client.h" #include "database.h" #include "crawler.h" #include "analysis.h" #include "auth.h" u8 *auth_form; /* Auth form location */ u8 *auth_form_target; /* Auth form submit target */ u8 *auth_user; /* User name */ u8 *auth_user_field; /* Username field id */ u8 *auth_pass; /* Password */ u8 *auth_pass_field; /* Password input field id */ u8 *auth_verify_url; /* Auth verify URL */ u8 auth_state; /* Stores the auth state */ void authenticate() { struct http_request* req; DEBUGC(L1, "*- Authentication starts\n"); if (!auth_form || !auth_user || !auth_pass) return; struct pivot_desc *fake = ck_alloc(sizeof(struct pivot_desc)); fake->type = PIVOT_FILE; fake->state = PSTATE_FETCH; /* When in session, do nothing */ if (auth_state != ASTATE_NONE) return; auth_state = ASTATE_START; req = ck_alloc(sizeof(struct http_request)); /* Create a request struct. Note that in this case, we don't care about * whether the URL is whitelisted or not */ if (parse_url(auth_form, req, NULL)) FATAL("Auth form URL could not be parsed\n"); req->pivot = fake; req->callback = submit_auth_form; async_request(req); } /* Helper function to find form fields to put the credentials in */ static u8 find_and_set_field(struct param_array *par, const char **test_fields, u32 par_type, u8* par_value) { u32 i, k; /* Try to find the field */ for (i=0; ic; i++) { if (!par->n[i] || par->t[i] != par_type) continue; /* Match it with the strings */ for (k=0; test_fields[k]; k++) { if (inl_strcasestr(par->n[i], (u8*)test_fields[k])) { DEBUGC(L1, "*-- Authentication - found login field: %s\n", par->n[i]); if (par->v[i]) ck_free(par->v[i]); par->v[i] = ck_strdup(par_value); return 1; } } } /* None found..*/ return 0; } /* Main function to submit the authentication, login form. This function will try find the right form and , unless form fields are specified on command-line, try to find the right fields in order to store the username and password. */ u8 submit_auth_form(struct http_request* req, struct http_response* res) { u8 *form, *form_ptr; u8 *vurl = NULL; u8 is_post = 1; u8 par_type = PARAM_POST; struct http_request* n = NULL; DEBUG_CALLBACK(req, res); /* Loop over the forms till we get our password form */ form_ptr = res->payload; do { form = inl_strcasestr(form_ptr, (u8*)"method && !strcmp((char*)n->method, "POST")); par_type = is_post ? PARAM_POST : PARAM_QUERY; n->pivot = req->pivot; collect_form_data(n, req, res, form, is_post); /* If the form field was specified per command-line, we'll check if it's present. When it's not present: move on to next form. Now when no form field was specified via command-line: try to find one by using the strings from the "user_fields" array (defined in auth.h). */ if (auth_user_field) { if(!get_value(par_type, auth_user_field, 0, &n->par)) continue; set_value(par_type, auth_user_field, ck_strdup(auth_user), 0, &n->par); DEBUGC(L1, "*-- Authentication - auth_user field set (%s)\n", auth_user_field); } else if (!find_and_set_field(&n->par, user_fields, par_type, auth_user)) { continue; } if (auth_pass_field) { if(!get_value(par_type, auth_pass_field, 0, &n->par)) continue; set_value(par_type, auth_pass_field, ck_strdup(auth_pass), 0, &n->par); DEBUGC(L1, "*-- Authentication - auth_pass field set (%s)\n", auth_pass_field); } else if (!find_and_set_field(&n->par, pass_fields, par_type, auth_pass)) { continue; } /* If we get here: credentials are set */ n->callback = auth_form_callback; DEBUGC(L1, "*-- Submitting authentication form\n"); #ifdef LOG_STDERR dump_http_request(n); #endif async_request(n); auth_state = ASTATE_SEND; break; } while (form); if (auth_state != ASTATE_SEND) DEBUGC(L1, "*-- Could not login. Please check the URL and form fields\n"); return 0; } /* After submitting the form and receiving a response, this is called */ u8 auth_form_callback(struct http_request* req, struct http_response* res) { DEBUG_CALLBACK(req, res); DEBUGC(L1, "*-- Received form response\n"); /* Parse the payload which will make sure cookies are stored. */ content_checks(req, res); /* Compare an authenticated and anonymous request to the verification URL. The * response should be different in order to determine that we are indeed * authenticated */ if (!auth_verify_url) { auth_state = ASTATE_DONE; return 0; } auth_state = ASTATE_VERIFY; auth_verify_tests(req->pivot); return 0; } /* Sends two requests to the verification URL. The first request is authenticated (or should be) while the second request is anonymous */ u8 auth_verify_tests(struct pivot_desc* pivot) { /* When we have no verification URL or the scan is no authenticated: return */ DEBUG("In auth verify\n"); if (!auth_verify_url || (auth_state != ASTATE_DONE && auth_state != ASTATE_VERIFY)) return 1; u8* vurl = ck_strdup(auth_verify_url); struct http_request *n = ck_alloc(sizeof(struct http_request)); n->pivot = pivot; if (parse_url(vurl, n, NULL)) FATAL("Unable to parse verification URL: %s\n", vurl); /* One: authenticated request */ n->callback = auth_verify_checks; n->user_val = 0; async_request(n); /* Two: anonymous request */ n = req_copy(n, pivot, 1); n->no_cookies = 1; n->user_val = 1; n->callback = auth_verify_checks; async_request(n); return 0; } /* Receives two requests to the verification URL. If there is a difference, than we'll trust that it's because one request was authenticated while the other wasn't */ u8 auth_verify_checks(struct http_request* req, struct http_response* res) { DEBUG_CALLBACK(req, res); if (FETCH_FAIL(res)) { handle_error(req, res, (u8*)"during auth verification tests", 0); return 0; } req->pivot->misc_req[req->user_val] = req; req->pivot->misc_res[req->user_val] = res; /* We need two responses */ if ((++req->pivot->misc_cnt) != 2) return 1; /* Compare the two response. The authenticates response should be different to the anonymous request */ if (same_page(&MRES(0)->sig, &MRES(1)->sig)) { DEBUGC(L1, "*- Unable to verify authentication using provided URL.\n"); dump_signature(&MRES(0)->sig); dump_signature(&MRES(1)->sig); auth_state = ASTATE_FAIL; } destroy_misc_data(req->pivot, req); /* Re-authenticate upon failure */ if (auth_state == ASTATE_FAIL) { authenticate(); DEBUG("* Going to re-authenticate\n"); } else { auth_state = ASTATE_DONE; DEBUGC(L1, "*- Authenticated\n"); } return 0; } skipfish-2.10b/src/types.h0000440036502000116100000000232312057375131014476 0ustar heinenneng/* skipfish - type definitions --------------------------- Author: Michal Zalewski Copyright 2009, 2010, 2011 by Google Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ #ifndef _HAVE_TYPES_H #define _HAVE_TYPES_H #include typedef uint8_t u8; typedef uint16_t u16; typedef uint32_t u32; typedef uint64_t u64; typedef int8_t s8; typedef int16_t s16; typedef int32_t s32; typedef int64_t s64; /* PRNG wrapper, of no better place to put it. */ #define R(_ceil) ((u32)(random() % (_ceil))) #ifndef MIN # define MIN(_a,_b) ((_a) > (_b) ? (_b) : (_a)) # define MAX(_a,_b) ((_a) > (_b) ? (_a) : (_b)) #endif /* !MIN */ #endif /* ! _HAVE_TYPES_H */ skipfish-2.10b/src/same_test.c0000440036502000116100000000356112057375131015316 0ustar heinenneng/* skipfish - same_page() test utility ----------------------------------- Author: Michal Zalewski Copyright 2009, 2010, 2011 by Google Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ #include #include #include #include #include #include #include #include #include #include #include #include "types.h" #include "alloc-inl.h" #include "string-inl.h" #include "crawler.h" #include "analysis.h" #include "database.h" #include "http_client.h" #include "report.h" #ifdef DEBUG_ALLOCATOR struct TRK_obj* TRK[ALLOC_BUCKETS]; u32 TRK_cnt[ALLOC_BUCKETS]; #endif /* DEBUG_ALLOCATOR */ #define MAX_LEN (1024*1024) u8 p1[MAX_LEN], p2[MAX_LEN]; int main(int argc, char** argv) { static struct http_response r1, r2; s32 l1, l2; l1 = read(8, p1, MAX_LEN); l2 = read(9, p2, MAX_LEN); if (l1 < 0 || l2 < 0) FATAL("Usage: ./same_test 8end_time - MREQ(_r)->start_time) /* The test/check struct with pointers to callback functions */ struct cb_handle { u32 res_num; /* Amount of expected responses */ u32 res_keep; /* Bool for keeping req/res */ u8 allow_varies; /* Bool to accept pivots with res_varies */ u8 time_sensitive; /* Bool for time sensitive tests */ u8 scrape; /* Scrape links, or not.. */ u32 pv_flag; /* Flag to match pivot type */ u32 id; /* Flag to match pivot type */ u8* name; /* Name or title of the check */ u8 (*tests)(struct pivot_desc* pivot); u8 (*checks)(struct http_request*, struct http_response*); u32 skip; /* Bool to disable the check */ }; /* Strings for traversal and file disclosure tests. The order should not be changed */ struct lfi_test { const char *vectors[10]; const char *test_string; const char *description; }; #define MAX_LFI_INDEX 2 struct lfi_test lfi_tests[] = { {{"/../../../../../../../../../etc/hosts", "file:///etc/hosts", 0 }, "127.0.0.1", "File /etc/hosts was disclosed." }, {{"/../../../../../../../../../etc/passwd", "file:///etc/passwd", 0 }, "root:x:0:0:root", "File /etc/passwd was disclosed."}, {{"..\\..\\..\\..\\..\\..\\..\\..\\boot.ini", "file:///boot.ini", 0 }, "[boot loader]", "File boot.ini was disclosed."}, }; #endif /* _VIA_CHECKS_C */ #endif /* _HAVE_CHECKS_H */ skipfish-2.10b/src/crawler.c0000440036502000116100000014125612057375131014775 0ustar heinenneng/* skipfish - crawler state machine -------------------------------- Includes dictionary and security injection logic. Author: Michal Zalewski Copyright 2009, 2010, 2011 by Google Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ #define _VIA_CRAWLER_C #include "debug.h" #include "config.h" #include "types.h" #include "http_client.h" #include "database.h" #include "crawler.h" #include "checks.h" #include "analysis.h" u32 crawl_prob = 100; /* Crawl probability (1-100%) */ u8 no_fuzz_ext; /* Don't fuzz extensions for dirs */ u8 no_500_dir; /* Don't crawl 500 directories */ u8 delete_bin; /* Don't keep binary responses */ u8 flush_pivot_data; /* Flush pivot data to disk */ /* ************************* **** GENERAL HELPERS **** ************************* Assorted functions used by all the crawl callbacks for manipulating requests, parsing responses, etc. */ /* Dumps request, response (for debugging only). */ u8 show_response(struct http_request* req, struct http_response* res) { dump_http_request(req); if (FETCH_FAIL(res)) { SAY("^^^ REQUEST SHOWN ABOVE CAUSED ERROR: %d ^^^\n", res->state); return 0; } dump_http_response(res); return 0; /* Do not keep req/res */ } /* Strips trailing / from a directory request, optionally replaces it with a new value. */ void replace_slash(struct http_request* req, u8* new_val) { u32 i; for (i=0;ipar.c;i++) if (req->par.t[i] == PARAM_PATH && !req->par.n[i] && !req->par.v[i][0]) { if (new_val) { ck_free(req->par.v[i]); req->par.v[i] = ck_strdup(new_val); } else req->par.t[i] = PARAM_NONE; return; } /* Could not find a slash segment - create a new segment instead. */ set_value(PARAM_PATH, 0, new_val, -1, &req->par); } /* Releases children for crawling (called once parent node had 404, IPS probes done, etc). Note that non-directories might have locked children too. */ static void unlock_children(struct pivot_desc* pv) { u32 i; DEBUG_HELPER(pv); for (i=0;ichild_cnt;i++) if (pv->child[i]->state == PSTATE_PENDING) { DEBUG_PIVOT("Unlocking", pv->child[i]); pv->child[i]->state = PSTATE_FETCH; if (!pv->child[i]->res) async_request(pv->child[i]->req); else switch (pv->child[i]->type) { case PIVOT_DIR: dir_retrieve_check(pv->req, pv->res); break; case PIVOT_PARAM: case PIVOT_FILE: file_retrieve_check(pv->req, pv->res); break; case PIVOT_UNKNOWN: unknown_retrieve_check(pv->req, pv->res); break; default: FATAL("Unknown pivot type '%u'", pv->type); } } } /* Handles response error for callbacks in a generalized manner. If 'stop' is 1, marks the entire pivot as busted, unlocks children. */ void handle_error(struct http_request* req, struct http_response* res, u8* desc, u8 stop) { DEBUG_CALLBACK(req, res); if (res->state == STATE_SUPPRESS) { problem(PROB_LIMITS, req, res, (u8*)"Too many previous fetch failures", req->pivot, 0); } else { problem(PROB_FETCH_FAIL, req, res, desc, req->pivot, 0); } if (stop) { req->pivot->state = PSTATE_DONE; unlock_children(req->pivot); } } /* Finds nearest "real" directory parent, so that we can consult it for 404 signatures, etc. Return NULL also if dir found, but signature-less. */ struct pivot_desc* dir_parent(struct pivot_desc* pv) { struct pivot_desc* ret; ret = pv->parent; while (ret && ret->type != PIVOT_DIR && ret->type != PIVOT_SERV) ret = ret->parent; if (ret && !ret->r404_cnt) return NULL; return ret; } /* Deletes any cached requests and responses stored by injection probes. */ void destroy_misc_data(struct pivot_desc* pv, struct http_request* self) { u32 i; for (i=0;imisc_req[i] != self) { if (pv->misc_req[i]) destroy_request(pv->misc_req[i]); if (pv->misc_res[i]) destroy_response(pv->misc_res[i]); } pv->misc_req[i] = NULL; pv->misc_res[i] = NULL; } pv->misc_cnt = 0; } /* *************************************** **** ASSORTED FORWARD DECLARATIONS **** *************************************** */ /* Flow control functions */ static void inject_start(struct pivot_desc*); static void param_start(struct pivot_desc*); static void dir_dict_start(struct pivot_desc*); static void param_numerical_start(struct pivot_desc*); static void param_dict_start(struct pivot_desc*); static u8 dir_case_check(struct http_request* req, struct http_response* res); static u8 dir_dict_check(struct http_request*, struct http_response*); static u8 dir_dict_bogus_check(struct http_request*, struct http_response*); static u8 dir_404_check(struct http_request*, struct http_response*); static u8 dir_up_behavior_check(struct http_request*, struct http_response*); static u8 param_numerical_check(struct http_request*, struct http_response*); static u8 param_dict_check(struct http_request*, struct http_response*); static u8 param_trylist_check(struct http_request*, struct http_response*); static u8 unknown_retrieve_check2(struct http_request*, struct http_response*); /* ****************************** **** ACTUAL STATE MACHINE **** ****************************** The following is a rough sketch of what's going on here. == Pivot creation states == Path elements: root - PSTATE_DONE, no callback server - PSTATE_FETCH, dir_retrieve_check dir - PSTATE_FETCH, dir_retrieve_check PSTATE_PENDING if parent state <= PSTATE_IPS_CHECK last seg - PSTATE_FETCH, unknown_retrieve_check PSTATE_PENDING if parent state <= PSTATE_IPS_CHECK file - PSTATE_FETCH, file_retrieve_check PSTATE_PENDING if parent state <= PSTATE_IPS_CHECK If element in name=value format, also add value to pivot's trylist. Call param_trylist_start if pivot already in PSTATE_DONE. Query elements: PSTATE_FETCH, file_retrieve_check PSTATE_PENDING if parent dir state <= PSTATE_IPS_CHECK Add value to pivot's trylist. Call param_trylist_start if pivot already in PSTATE_DONE. == Initial fetch actions == unknown_retrieve_check: Initial retrieval of an unknown path element. File not found: unlock_children, -> param_behavior_tests Otherwise: -> file_retrieve_check or -> unknown_retrieve_check2 unknown_retrieve_check2: Secondary check to detect dir-like behavior (for unknown_retrieve_check). -> dir_retrieve_check or -> file_retrieve_check file_retrieve_check: Initial retrieval of a file, query parameter, or so. -> secondary_ext_start (async) -> dir_case_start (async) unlock_children Query value pivot: -> param_behavior_tests Other pivots: PSTATE_CHILD_INJECT, -> inject_start dir_retrieve_check: Initial retrival of a directory or PATHINFO resource. -> secondary_ext_start (async) PSTATE_404_CHECK, -> dir_404_check == Basic directory checks == dir_404_check: Performs basic 404 signature detection. Calls itself in a loop. -> dir_case_start (async) PSTATE_PARENT_CHECK, -> dir_up_behavior_check dir_up_behavior_check: Checks if path hierarchy is honored by the server. unlock_children PSTATE_CHILD_INJECT, -> inject_start dir_case_start: Asynchronous handler to check directory case-sensitivity. -> dir_case_check dir_case_check: Case sensitivity callback. No further branching. == Injection tests == The injection checks are handled by the inject_state_manager in checks.c but will return control to inject_done() when done. inject_done: Injection testing wrap-up. Path element: PSTATE_CHILD_DICT, -> dir_dict_start if fuzzable dir -> inject_state_manager if not dir or no 404 sigs PSTATE_DONE if not allowed or varies randomly Other parametric: -> param_numerical_start PSTATE_DONE if varies randomly == Parameter brute-force (name=val only) == param_numerical_start: Begin numerical brute-force if applicable. Numerical: PSTATE_PAR_NUMBER, -> param_numerical_check Otherwise: PSTATE_PAR_DICT, -> param_dict_start param_numerical_check: Numerical brute-force callback. May store results as PIVOT_VALUE / PSTATE_DONE nodes. -> secondary_ext_start (async) PSTATE_PAR_DICT, -> param_dict_start param_dict_start: Dictionary brute-force init / resume. Out of keywords: -> param_trylist_start Otherwise: -> param_dict_check param_dict_check: Dictionary brute-force callback. May store results as PIVOT_VALUE / PSTATE_DONE nodes. -> secondary_ext_start (async) Loops to -> param_trylist_start if not called via secondary_ext_check param_trylist_start: Begins trylist fuzzing, or resumes from offset. Bad pivot or no more keywords: PSTATE_DONE Otherwise: PSTATE_PAR_TRYLIST, -> param_trylist_check param_trylist_check: Trylist dictionary callback. May store results as PIVOT_VALUE / PSTATE_DONE nodes. -> secondary_ext_start (async) PSTATE_DONE == Directory brute-force == dir_dict_start: Dictionary brute-force init / resume. Bad pivot or no more keywords: -> param_behavior_tests Otherwise: -> dir_dict_bogus_check dir_dict_bogus_check: Check for good keyword candidates, proceed with extension fuzzing. -> dir_dict_check Loops over to -> dir_dict_start dir_dict_check: Dictionary brute-force callback. Loops over to -> dir_dict_start if not called via secondary_ext_start. == Secondary extension brute-force == secondary_ext_start: Asynchronous secondary extension check Query: -> param_dict_check Path: -> dir_dict_check */ static void dir_case_start(struct pivot_desc* pv) { u32 i, len; s32 last = -1; struct http_request* n; if (pv->parent->c_checked) return; DEBUG_HELPER(pv); for (i=0;ireq->par.c;i++) if (PATH_SUBTYPE(pv->req->par.t[i]) && pv->req->par.v[i][0]) last = i; if (last < 0) return; len = strlen((char*)pv->req->par.v[last]); for (i=0;ireq->par.v[last][i])) break; if (i == len) return; pv->parent->c_checked = 1; n = req_copy(pv->req, pv, 1); n->callback = dir_case_check; /* Change case. */ n->par.v[last][i] = islower(n->par.v[last][i]) ? toupper(n->par.v[last][i]) : tolower(n->par.v[last][i]); DEBUG("* candidate parameter: %s -> %s\n", pv->req->par.v[last], n->par.v[last]); async_request(n); } static u8 dir_case_check(struct http_request* req, struct http_response* res) { DEBUG_CALLBACK(req, res); if (FETCH_FAIL(res)) { RPAR(req)->c_checked = 0; return 0; } if (!same_page(&res->sig, &RPRES(req)->sig)) RPAR(req)->csens = 1; return 0; } static void secondary_ext_start(struct pivot_desc* pv, struct http_request* req, struct http_response* res, u8 is_param) { u8 *base_name, *fpos, *lpos, *ex; s32 tpar = -1, spar = -1; u32 i = 0; DEBUG_HELPER(req->pivot); DEBUG_HELPER(pv); if (is_param) { tpar = pv->fuzz_par; } else { /* Find last path segment other than NULL-''. */ for (i=0;ipar.c;i++) if (PATH_SUBTYPE(req->par.t[i])) { if ((req->par.t[i] == PARAM_PATH && !req->par.n[i] && !req->par.v[i][0])) spar = i; else tpar = i; } } if (tpar < 0) return; base_name = req->par.v[tpar]; /* Reject parameters with no '.' (unless in no_fuzz_ext mode), with too many '.'s, or '.' in an odd location. */ fpos = (u8*)strchr((char*)base_name, '.'); if (!no_fuzz_ext || fpos) if (!fpos || fpos == base_name || !fpos[1]) return; lpos = (u8*)strrchr((char*)base_name, '.'); if (fpos != lpos) return; i = 0; while ((ex = wordlist_get_extension(i, 0))) { u8* tmp = ck_alloc(strlen((char*)base_name) + strlen((char*)ex) + 2); u32 c; /* Avoid foo.bar.bar. */ if (lpos && !strcasecmp((char*)lpos + 1, (char*)ex)) { i++; ck_free(tmp); continue; } sprintf((char*)tmp, "%s.%s", base_name, ex); /* Matching child? If yes, don't bother. */ for (c=0;cchild_cnt;c++) if (!((is_c_sens(pv) ? strcmp : strcasecmp)((char*)tmp, (char*)pv->child[c]->name))) break; /* Matching current node? */ if (pv->fuzz_par != -1 && !((is_c_sens(pv) ? strcmp : strcasecmp)((char*)tmp, (char*)pv->req->par.v[pv->fuzz_par]))) c = ~pv->child_cnt; if (c == pv->child_cnt) { struct http_request* n = req_copy(req, pv, 1); /* Remove trailing slash if present. */ if (spar >= 0) n->par.t[spar] = PARAM_NONE; ck_free(n->par.v[tpar]); n->par.v[tpar] = tmp; n->user_val = 1; memcpy(&n->same_sig, &res->sig, sizeof(struct http_sig)); n->callback = is_param ? param_dict_check : dir_dict_check; /* Both handlers recognize user_val == 1 as a special indicator. */ async_request(n); } else ck_free(tmp); i++; } } static void inject_start(struct pivot_desc* pv) { DEBUG_HELPER(pv); /* Set the injection check counter to 0. This is important since pivots loop through the injection tests at least twice */ pv->check_idx = -1; /* Tell the inject state manager that we like to start with our first test by putting res = NULL */ inject_state_manager(pv->req, NULL); } static void param_start(struct pivot_desc* pv) { if (pv->fuzz_par < 0 || !url_allowed(pv->req) || !param_allowed(pv->name)) { pv->state = PSTATE_DONE; maybe_delete_payload(pv); return; } DEBUG_HELPER(pv); pv->state = PSTATE_PAR_CHECK; pv->check_idx = -1; inject_state_manager(pv->req, NULL); } void inject_done(struct pivot_desc* pv) { if (pv->state == PSTATE_CHILD_INJECT) { if (url_allowed(pv->req) && !pv->res_varies) { if ((pv->type == PIVOT_DIR || pv->type == PIVOT_SERV) && pv->r404_cnt && !pv->bad_parent) { pv->state = PSTATE_CHILD_DICT; pv->cur_key = 0; dir_dict_start(pv); } else { /* Continue with parameter tests */ param_start(pv); } } else { pv->state = PSTATE_DONE; maybe_delete_payload(pv); return; } } else { if (pv->bogus_par || pv->res_varies) { pv->state = PSTATE_DONE; maybe_delete_payload(pv); } else { param_numerical_start(pv); } } } static void param_numerical_start(struct pivot_desc* pv) { u8 *val = TPAR(pv->req), *out, fmt[16]; u32 i, dig, tail; s32 val_i, range_st, range_en; u8 zero_padded = 0; DEBUG_HELPER(pv); if (!descendants_ok(pv)) goto schedule_next; /* Skip to the first digit, then to first non-digit. */ i = 0; while (val[i] && !isdigit(val[i])) i++; if (!val[i]) goto schedule_next; dig = i; while (val[i] && isdigit(val[i])) i++; tail = i; /* Too many digits is a no-go. */ if (tail - dig > PAR_MAX_DIGITS) goto schedule_next; if (val[dig] == '0' && tail - dig > 1) zero_padded = 1; val_i = atoi((char*)val + dig); range_st = val_i - PAR_INT_FUZZ; range_en = val_i + PAR_INT_FUZZ; if (range_st < 0) range_st = 0; if (zero_padded) sprintf((char*)fmt, "%%.%us%%0%uu%%s", dig, tail - dig); else sprintf((char*)fmt, "%%.%us%%%uu%%s", dig, tail - dig); out = ck_alloc(strlen((char*)val) + 16); /* Let's roll! */ pv->state = PSTATE_PAR_NUMBER; pv->num_pending = range_en - range_st + 1; for (i=range_st;i<=range_en;i++) { struct http_request* n; if (i == val_i) { pv->num_pending--; continue; } sprintf((char*)out, (char*)fmt, val, i, val + tail); n = req_copy(pv->req, pv, 1); ck_free(TPAR(n)); TPAR(n) = ck_strdup((u8*)out); n->callback = param_numerical_check; async_request(n); } ck_free(out); if (!pv->num_pending) goto schedule_next; return; schedule_next: pv->state = PSTATE_PAR_DICT; param_dict_start(pv); /* Pew pew! */ } static u8 param_numerical_check(struct http_request* req, struct http_response* res) { struct pivot_desc *par, *n = NULL, *orig_pv = req->pivot; u32 i; DEBUG_CALLBACK(req, res); if (FETCH_FAIL(res)) { handle_error(req, res, (u8*)"during numerical brute-force tests", 0); goto schedule_next; } /* Looks like parent, or like its 404 signature? */ if (same_page(&res->sig, &req->pivot->r404[0]) || same_page(&res->sig, &req->pivot->res->sig)) goto schedule_next; par = dir_parent(req->pivot); /* Check with parent if sigs available, but if not - no biggie. */ if (par) for (i=0;ir404_cnt;i++) if (same_page(&res->sig, &par->r404[i])) goto schedule_next; /* Matching child? If yes, don't bother. */ for (i=0;ipivot->child_cnt;i++) if (req->pivot->child[i]->type == PIVOT_VALUE && !((is_c_sens(req->pivot) ? strcmp : strcasecmp)((char*)TPAR(req), (char*)req->pivot->child[i]->name))) goto schedule_next; if (!descendants_ok(req->pivot)) goto schedule_next; /* Hmm, looks like we're onto something. Let's manually create a dummy pivot and attach it to current node, without any activity planned. Attach any response notes to that pivot. */ n = ck_alloc(sizeof(struct pivot_desc)); n->type = PIVOT_VALUE; n->state = PSTATE_DONE; n->name = ck_strdup(TPAR(req)); n->req = req; n->res = res; n->fuzz_par = req->pivot->fuzz_par; n->parent = req->pivot; DEBUG("--- New pivot (value): %s ---\n", n->name); req->pivot->child = ck_realloc(req->pivot->child, (req->pivot->child_cnt + 1) * sizeof(struct pivot_desc*)); req->pivot->child[req->pivot->child_cnt++] = n; add_descendant(req->pivot); req->pivot = n; secondary_ext_start(orig_pv, req, res, 1); maybe_delete_payload(n); schedule_next: RESP_CHECKS(req, res); if (!(--(orig_pv->num_pending))) { orig_pv->state = PSTATE_PAR_DICT; param_dict_start(orig_pv); } /* Copied over to pivot. */ return n ? 1 : 0; } static void param_dict_start(struct pivot_desc* pv) { static u8 in_dict_init; struct http_request* n; u8 *kw, *ex; u32 i, c; u8 specific; /* Too many requests still pending, or already done? */ if (in_dict_init || pv->pdic_pending > DICT_BATCH || pv->state != PSTATE_PAR_DICT) return; DEBUG_HELPER(pv); restart_dict: if (!descendants_ok(pv)) { param_trylist_start(pv); return; } i = 0; kw = (pv->pdic_guess ? wordlist_get_guess : wordlist_get_word) (pv->pdic_cur_key, &specific); if (!kw) { /* No more keywords. Move to guesswords if not there already, or advance to try list otherwise. */ if (pv->pdic_guess) { param_trylist_start(pv); return; } pv->pdic_guess = 1; pv->pdic_cur_key = 0; goto restart_dict; } /* Use crawl_prob/100 dictionary entries. */ if (R(100) < crawl_prob) { /* Schedule extension-less probe, if the keyword is not on the child list. */ for (c=0;cchild_cnt;c++) if (pv->type == PIVOT_VALUE && !((is_c_sens(pv) ? strcmp : strcasecmp)((char*)kw, (char*)pv->child[c]->name))) break; /* ...and does not match the node itself. */ if (pv->fuzz_par != -1 && !((is_c_sens(pv) ? strcmp : strcasecmp)((char*)kw, (char*)pv->req->par.v[pv->fuzz_par]))) c = ~pv->child_cnt; if (c == pv->child_cnt) { n = req_copy(pv->req, pv, 1); ck_free(TPAR(n)); TPAR(n) = ck_strdup(kw); n->callback = param_dict_check; n->user_val = 0; pv->pdic_pending++; in_dict_init = 1; async_request(n); in_dict_init = 0; } /* Schedule probes for all extensions for the current word, but only if the original parameter contained '.' somewhere, and only if string is not on the try list. Special handling for specific keywords with '.' inside. */ if (!no_fuzz_ext && strchr((char*)TPAR(pv->req), '.')) while ((ex = wordlist_get_extension(i, specific))) { u8* tmp = ck_alloc(strlen((char*)kw) + strlen((char*)ex) + 2); sprintf((char*)tmp, "%s.%s", kw, ex); for (c=0;cchild_cnt;c++) if (pv->type == PIVOT_VALUE && !((is_c_sens(pv) ? strcmp : strcasecmp)((char*)tmp, (char*)pv->child[c]->name))) break; if (pv->fuzz_par != -1 && !((is_c_sens(pv) ? strcmp : strcasecmp)((char*)tmp, (char*)pv->req->par.v[pv->fuzz_par]))) c = ~pv->child_cnt; if (c == pv->child_cnt) { n = req_copy(pv->req, pv, 1); ck_free(TPAR(n)); TPAR(n) = tmp; n->user_val = 0; n->callback = param_dict_check; pv->pdic_pending++; in_dict_init = 1; async_request(n); in_dict_init = 0; } else ck_free(tmp); i++; } } pv->pdic_cur_key++; if (pv->pdic_pending < DICT_BATCH) goto restart_dict; } static u8 param_dict_check(struct http_request* req, struct http_response* res) { struct pivot_desc *par, *n = NULL, *orig_pv = req->pivot; u8 keep = 0; u32 i; DEBUG_CALLBACK(req, res); if (!req->user_val) req->pivot->pdic_pending--; if (FETCH_FAIL(res)) { handle_error(req, res, (u8*)"during parameter brute-force tests", 0); goto schedule_next; } /* Same as parent or parent's 404? Don't bother. */ if (same_page(&res->sig, &req->pivot->r404[0]) || same_page(&res->sig, &RPRES(req)->sig)) goto schedule_next; par = dir_parent(req->pivot); if (par) for (i=0;ir404_cnt;i++) if (same_page(&res->sig, &par->r404[i])) goto schedule_next; /* Matching child? If yes, don't bother. */ for (i=0;ipivot->child_cnt;i++) if (req->pivot->child[i]->type == PIVOT_VALUE && !((is_c_sens(req->pivot) ? strcmp : strcasecmp)((char*)TPAR(req), (char*)req->pivot->child[i]->name))) goto schedule_next; if (!descendants_ok(req->pivot)) goto schedule_next; n = ck_alloc(sizeof(struct pivot_desc)); n->type = PIVOT_VALUE; n->state = PSTATE_DONE; n->name = ck_strdup(TPAR(req)); n->req = req; n->res = res; n->fuzz_par = req->pivot->fuzz_par; n->parent = req->pivot; DEBUG("--- New pivot (value): %s ---\n", n->name); req->pivot->child = ck_realloc(req->pivot->child, (req->pivot->child_cnt + 1) * sizeof(struct pivot_desc*)); req->pivot->child[req->pivot->child_cnt++] = n; add_descendant(req->pivot); req->pivot = n; keep = 1; if (!req->user_val) secondary_ext_start(orig_pv, req, res, 1); maybe_delete_payload(n); schedule_next: RESP_CHECKS(req, res); if (!req->user_val) param_dict_start(orig_pv); return keep; } void param_trylist_start(struct pivot_desc* pv) { u32 i; /* If the parameter does not seem to be doing anything, there is no point in going through the try list if restarted. */ if (pv->fuzz_par == -1 || pv->bogus_par || pv->res_varies || !descendants_ok(pv)) { pv->state = PSTATE_DONE; maybe_delete_payload(pv); return; } else pv->state = PSTATE_PAR_TRYLIST; DEBUG_HELPER(pv); for (i=pv->try_cur;itry_cnt;i++) { u32 c; /* If we already have a child by this name, don't poke it again. */ for (c=0;cchild_cnt;c++) if (!((is_c_sens(pv) ? strcmp : strcasecmp)((char*)pv->try_list[i], (char*)pv->child[c]->name))) continue; /* Matching current node? Ditto. */ if (pv->fuzz_par != -1 && !((is_c_sens(pv) ? strcmp : strcasecmp)((char*)pv->try_list[i], (char*)pv->req->par.v[pv->fuzz_par]))) continue; if (c == pv->child_cnt) { if (R(100) < crawl_prob) { struct http_request* n; pv->try_pending++; n = req_copy(pv->req, pv, 1); ck_free(TPAR(n)); TPAR(n) = ck_strdup(pv->try_list[i]); n->callback = param_trylist_check; async_request(n); } } else { if (!pv->child[c]->linked) pv->child[c]->linked = 1; } } pv->try_cur = i; if (!pv->try_pending) { pv->state = PSTATE_DONE; maybe_delete_payload(pv); return; } } static u8 param_trylist_check(struct http_request* req, struct http_response* res) { struct pivot_desc *par, *n = NULL; struct pivot_desc* orig_pv = req->pivot; u32 i; DEBUG_CALLBACK(req, res); if (FETCH_FAIL(res)) { handle_error(req, res, (u8*)"during try list fetches", 0); goto schedule_next; } /* Same as parent or parent's 404? Don't bother. */ if (same_page(&res->sig, &req->pivot->r404[0]) || same_page(&res->sig, &RPRES(req)->sig)) goto schedule_next; par = dir_parent(req->pivot); if (par) for (i=0;ir404_cnt;i++) if (same_page(&res->sig, &par->r404[i])) goto schedule_next; /* Name matching known child? If yes, don't bother. */ for (i=0;ipivot->child_cnt;i++) if (req->pivot->child[i]->type == PIVOT_VALUE && !((is_c_sens(req->pivot) ? strcmp : strcasecmp)((char*)TPAR(req), (char*)req->pivot->child[i]->name))) goto schedule_next; if (!descendants_ok(req->pivot)) goto schedule_next; n = ck_alloc(sizeof(struct pivot_desc)); n->type = PIVOT_VALUE; n->state = PSTATE_DONE; n->name = ck_strdup(TPAR(req)); n->req = req; n->res = res; n->fuzz_par = req->pivot->fuzz_par; n->parent = req->pivot; DEBUG("--- New pivot (value): %s ---\n", n->name); req->pivot->child = ck_realloc(req->pivot->child, (req->pivot->child_cnt + 1) * sizeof(struct pivot_desc*)); req->pivot->child[req->pivot->child_cnt++] = n; add_descendant(req->pivot); req->pivot = n; secondary_ext_start(orig_pv, req, res, 1); maybe_delete_payload(n); schedule_next: RESP_CHECKS(req, res); if (!(--(orig_pv->try_pending))) { orig_pv->state = PSTATE_DONE; maybe_delete_payload(orig_pv); } /* Copied over to pivot. */ return n ? 1 : 0; } u8 file_retrieve_check(struct http_request* req, struct http_response* res) { u32 i = 0; struct pivot_desc* par; RPRES(req) = res; DEBUG_CALLBACK(req, res); if (FETCH_FAIL(res)) { handle_error(req, res, (u8*)"during initial file fetch", 1); return 1; } /* Matches parent's 404? */ par = dir_parent(req->pivot); if (par) for (i=0;ir404_cnt;i++) if (same_page(&res->sig, &par->r404[i])) break; /* If no signatures on parents, fall back to a basic 404 check, it's the least we could do. */ if ((!par && res->code == 404) || (par && i != par->r404_cnt)) { RESP_CHECKS(req, res); req->pivot->missing = 1; } else { if (res->code > 400) problem(PROB_NO_ACCESS, req, res, NULL, req->pivot, 0); /* Do not bother with checks on files or params if content identical to parent. */ if (!RPAR(req)->res || !same_page(&res->sig, &RPAR(req)->res->sig)) { RESP_CHECKS(req, res); if (par && req->pivot->type != PIVOT_PARAM) secondary_ext_start(par, req, res, 0); } if (req->pivot->type == PIVOT_FILE) dir_case_start(req->pivot); } /* On non-param nodes, we want to proceed with path-based injection checks. On param nodes, we want to proceed straght to parametric testng, instead. */ unlock_children(req->pivot); if (req->pivot->type == PIVOT_PARAM) { param_start(req->pivot); } else { req->pivot->state = PSTATE_CHILD_INJECT; inject_start(req->pivot); } /* This is the initial callback, keep the response. */ return 1; } u8 dir_retrieve_check(struct http_request* req, struct http_response* res) { struct http_request* n; struct pivot_desc* par; RPRES(req) = res; DEBUG_CALLBACK(req, res); /* Error at this point means we should give up on other probes in this directory. */ if (FETCH_FAIL(res)) { handle_error(req, res, (u8*)"during initial directory fetch", 1); return 1; } if (req->pivot->type == PIVOT_SERV) PIVOT_CHECKS(req, res); /* The next step is checking 404 responses for all extensions (starting with an empty one), which would also determine if the directory exists at all, etc. We make an exception for server pivot, though, which is presumed to be a directory (so we do PIVOT_CHECKS right away). */ req->pivot->state = PSTATE_404_CHECK; n = req_copy(req, req->pivot, 1); replace_slash(n, (u8*)BOGUS_FILE); n->user_val = 0; n->callback = dir_404_check; req->pivot->r404_pending++; async_request(n); par = dir_parent(req->pivot); if (par) secondary_ext_start(par, req, res, 0); /* Header, response belong to pivot - keep. */ return 1; } static u8 dir_404_check(struct http_request* req, struct http_response* res) { struct http_request* n; u32 i; s32 ppval = -1, pval = -1, val = -1; DEBUG_CALLBACK(req, res); if (req->pivot->r404_skip) goto schedule_next; if (FETCH_FAIL(res)) { handle_error(req, res, (u8*)"during 404 response checks", 0); goto schedule_next; } /* If the first 404 probe returned something that looks like the "root" page for the currently tested directory, panic. But don't do that check on server pivots, or if valid redirect detected earlier. */ if (!req->user_val && !req->pivot->sure_dir && req->pivot->type != PIVOT_SERV && RPRES(req) && same_page(&res->sig, &RPRES(req)->sig)) { DEBUG("* First 404 probe identical with parent!\n"); goto schedule_next; } else if (!req->user_val) { DEBUG("* First 404 probe differs from parent (%d)\n", RPRES(req) ? RPRES(req)->code : 0); } /* Check if this is a new signature. */ for (i=0;ipivot->r404_cnt;i++) if (same_page(&res->sig, &req->pivot->r404[i])) break; if (i == req->pivot->r404_cnt) { struct pivot_desc* par; DEBUG("* New signature found (%u).\n", req->pivot->r404_cnt); /* Need to add a new one. Make sure we're not over the limit. */ if (req->pivot->r404_cnt >= MAX_404) { req->pivot->r404_skip = 1; problem(PROB_404_FAIL, RPREQ(req), RPRES(req), (u8*)"too many 404 signatures found", req->pivot, 0); goto schedule_next; } memcpy(&req->pivot->r404[i], &res->sig, sizeof(struct http_sig)); req->pivot->r404_cnt++; /* Is this a new signature not seen on parent? Notify if so, and check it thoroughly. */ par = dir_parent(req->pivot); if (par) { for (i=0;ir404_cnt;i++) if (same_page(&res->sig, &par->r404[i])) break; } if (!par || i == par->r404_cnt) { problem(PROB_NEW_404, req, res, NULL, req->pivot, 1); RESP_CHECKS(req, res); } } schedule_next: /* First probe OK? */ if (!req->user_val) { u8* nk; u32 cur_ext = 0; /* First probe should already yield a 404 signature. */ if (!req->pivot->r404_cnt) { DEBUG("* First probe failed to yield a signature.\n"); goto bad_404; } DEBUG("* First probe yielded a valid signature.\n"); /* At this point, we can be reasonably sure the response is meaningful. */ PIVOT_CHECKS(req->pivot->req, req->pivot->res); dir_case_start(req->pivot); /* Aaand schedule all the remaining probes. Repeat BH_CHECKS times to also catch random variations. */ while ((nk = wordlist_get_extension(cur_ext++, 0))) { u8* tmp = ck_alloc(strlen(BOGUS_FILE) + strlen((char*)nk) + 2); sprintf((char*)tmp, "%s.%s", BOGUS_FILE, nk); for (i=0;ipivot, 1); replace_slash(n, tmp); n->callback = dir_404_check; n->user_val = 1; /* r404_pending is at least 1 to begin with, so this is safe even if async_request() has a synchronous effect. */ req->pivot->r404_pending++; async_request(n); } ck_free(tmp); } /* Also issue 404 probe for "lpt9", as "con", "prn", "nul", "lpt#", etc, are handled in a really annoying way by IIS. */ n = req_copy(RPREQ(req), req->pivot, 1); replace_slash(n, (u8*)"lpt9"); n->callback = dir_404_check; n->user_val = 1; req->pivot->r404_pending++; async_request(n); /* ...and for ~user, since this sometimes has a custom response, too. */ n = req_copy(RPREQ(req), req->pivot, 1); replace_slash(n, (u8*)"~" BOGUS_FILE); n->callback = dir_404_check; n->user_val = 1; req->pivot->r404_pending++; async_request(n); /* Lastly, make sure that directory 404 is on file. */ n = req_copy(RPREQ(req), req->pivot, 1); replace_slash(n, (u8*)BOGUS_FILE); set_value(PARAM_PATH, 0, (u8*)"", -1, &n->par); n->callback = dir_404_check; n->user_val = 1; req->pivot->r404_pending++; async_request(n); } if (--(req->pivot->r404_pending)) return 0; /* If we're here, all probes completed, and we had no major errors. If no signatures gathered, try to offer useful advice. */ bad_404: if (!req->pivot->r404_cnt || req->pivot->r404_skip) { DEBUG("* 404 detection failed.\n"); if (RPRES(req)->code == 404) { req->pivot->missing = 1; } else if (RPRES(req)->code >= 400) { problem(PROB_NO_ACCESS, RPREQ(req), RPRES(req), NULL, req->pivot, 0); /* Additional check for 401, 500 codes, as we're not calling content_checks() otherwise. */ if (RPRES(req)->code == 401) problem(PROB_AUTH_REQ, RPREQ(req), RPRES(req), NULL, req->pivot, 0); else if (RPRES(req)->code >= 500) problem(PROB_SERV_ERR, RPREQ(req), RPRES(req), NULL, req->pivot, 0); } else { if (req->pivot->type != PIVOT_SERV) { req->pivot->type = PIVOT_PATHINFO; replace_slash(req->pivot->req, NULL); /* XXX Update request */ } else problem(PROB_404_FAIL, RPREQ(req), RPRES(req), (u8*)"no distinctive 404 behavior detected", req->pivot, 0); } req->pivot->r404_cnt = 0; /* We can still try parsing the response, if it differs from parent and is not on parent's 404 list. */ if (!RPAR(req)->res) { PIVOT_CHECKS(req->pivot->req, req->pivot->res); } else { if (!same_page(&RPRES(req)->sig, &RPAR(req)->res->sig)) { struct pivot_desc* par; par = dir_parent(req->pivot); if (par) { for (i=0;ir404_cnt;i++) if (same_page(&res->sig, &par->r404[i])) break; } if (!par || i == par->r404_cnt) PIVOT_CHECKS(req->pivot->req, req->pivot->res); } } } else DEBUG("* 404 detection successful: %u signatures.\n", req->pivot->r404_cnt); /* Note that per-extension 404 probes coupled with a limit on the number of 404 signatures largely eliminates the need for BH_COUNT identical probes to confirm sane behavior here. */ /* The next probe is checking if /foo/current_path/ returns the same response as /bar/current_path/. If yes, then the directory probably should not be fuzzed. */ req->pivot->state = PSTATE_PARENT_CHECK; n = req_copy(RPREQ(req), req->pivot, 1); n->callback = dir_up_behavior_check; n->user_val = 0; /* Last path element is /; previous path element is current dir name; previous previous element is parent dir name. Find and replace it. */ for (i=0;ipar.c;i++) { if (PATH_SUBTYPE(n->par.t[i])) { ppval = pval; pval = val; val = i; } } if (ppval != -1 && req->pivot->r404_cnt) { ck_free(n->par.v[ppval]); n->par.v[ppval] = ck_strdup((u8*)BOGUS_FILE); async_request(n); } else { /* Top-level dir - nothing to replace. Do a dummy call to dir_up_behavior_check() to proceed directly to IPS checks. */ n->user_val = 1; dir_up_behavior_check(n, res); destroy_request(n); } return 0; } static u8 dir_up_behavior_check(struct http_request* req, struct http_response* res) { DEBUG_CALLBACK(req, res); if (req->user_val || req->pivot->r404_skip) { DEBUG("* Check not carried out (non-existent / bad parent).\n"); goto schedule_next; } if (FETCH_FAIL(res)) { handle_error(req, res, (u8*)"during parent checks", 0); goto schedule_next; } if (same_page(&res->sig, &RPRES(req)->sig)) { problem(PROB_PARENT_FAIL, req, res, 0, req->pivot, 0); DEBUG("* Parent may be bogus, skipping.\n"); req->pivot->bad_parent = 1; } else { DEBUG("* Parent behaves OK.\n"); } schedule_next: unlock_children(req->pivot); req->pivot->state = PSTATE_CHILD_INJECT; inject_start(req->pivot); return 0; } static void dir_dict_start(struct pivot_desc* pv) { static u8 in_dict_init; struct http_request* n; u8 *kw; u8 specific; /* Too many requests still pending, or already moved on to parametric tests? */ if (in_dict_init || pv->pending > DICT_BATCH || pv->state != PSTATE_CHILD_DICT) return; if (!descendants_ok(pv)) { param_start(pv); return; } if (pv->no_fuzz) { switch(pv->no_fuzz) { case 1: problem(PROB_LIMITS, pv->req, pv->res, (u8*)"Recursion limit reached, not fuzzing", pv, 0); break; case 2: problem(PROB_LIMITS, pv->req, pv->res, (u8*)"Directory out of scope, not fuzzing", pv, 0); param_start(pv); break; case 3: DEBUG("Skipping directory bruteforce (allows listing)"); break; } return; } DEBUG_HELPER(pv); restart_dict: kw = (pv->guess ? wordlist_get_guess : wordlist_get_word) (pv->cur_key, &specific); if (!kw) { /* No more keywords. Move to guesswords if not there already, or advance to parametric tests otherwise. */ if (pv->guess) { param_start(pv); return; } pv->guess = 1; pv->cur_key = 0; goto restart_dict; } /* Only schedule crawl_prob% dictionary entries. */ if (R(100) < crawl_prob) { /* First, schedule a request for /foo.bogus to see if extension fuzzing is advisable. */ u8* tmp = ck_alloc(strlen((char*)kw) + strlen((char*)BOGUS_EXT) + 2); sprintf((char*)tmp, "%s.%s", kw, BOGUS_EXT); n = req_copy(pv->req, pv, 1); replace_slash(n, tmp); n->callback = dir_dict_bogus_check; n->trying_key = kw; n->trying_spec = specific; pv->pending++; in_dict_init = 1; async_request(n); in_dict_init = 0; ck_free(tmp); } pv->cur_key++; /* Grab more keywords until we have a reasonable number of parallel requests scheduled. */ if (pv->pending < DICT_BATCH) goto restart_dict; } static u8 dir_dict_bogus_check(struct http_request* req, struct http_response* res) { struct http_request* n; u32 i, c; DEBUG_CALLBACK(req, res); if (FETCH_FAIL(res)) { handle_error(req, res, (u8*)"during path-based dictionary probes", 0); i = ~req->pivot->r404_cnt; } else { if (!req->pivot->r404_cnt) DEBUG("Bad pivot with no sigs! Pivot name = '%s'\n", req->pivot->name); for (i=0;ipivot->r404_cnt;i++) if (same_page(&res->sig, &req->pivot->r404[i])) break; } /* Do not schedule probes for .ht* files if default Apache config spotted. */ if (i == req->pivot->r404_cnt && res->code == 403 && prefix(req->trying_key, ".ht")) goto schedule_next; /* New file? Add pivot for the extension. */ if (i == req->pivot->r404_cnt) maybe_add_pivot(req, res, 0); /* Schedule extension probes only if bogus extension resulted in known 404. */ if (i != req->pivot->r404_cnt && !no_fuzz_ext) { u8* ex; i = 0; while ((ex = wordlist_get_extension(i, req->trying_spec))) { u8* tmp = ck_alloc(strlen((char*)req->trying_key) + strlen((char*)ex) + 2); sprintf((char*)tmp, "%s.%s", req->trying_key, ex); /* See if that file is already known... */ for (c=0;cpivot->child_cnt;c++) if (!((is_c_sens(req->pivot) ? strcmp : strcasecmp)((char*)tmp, (char*)req->pivot->child[c]->name))) break; /* When dealing with name=value pairs, also compare to currently fuzzed value string. */ if (req->pivot->fuzz_par != -1 && !((is_c_sens(req->pivot) ? strcmp : strcasecmp)((char*)tmp, (char*)req->pivot->req->par.v[req->pivot->fuzz_par]))) c = ~req->pivot->child_cnt; /* Not found - schedule a probe. */ if (c == req->pivot->child_cnt) { n = req_copy(req->pivot->req, req->pivot, 1); replace_slash(n, tmp); n->callback = dir_dict_check; n->user_val = 0; req->pivot->pending++; async_request(n); } ck_free(tmp); i++; } } /* Regardless of this, also schedule requests for /$name and /$name/. */ for (c=0;cpivot->child_cnt;c++) if (!((is_c_sens(req->pivot) ? strcmp : strcasecmp)((char*)req->trying_key, (char*)req->pivot->child[c]->name))) break; if (req->pivot->fuzz_par != -1 && !((is_c_sens(req->pivot) ? strcmp : strcasecmp)((char*)req->trying_key, (char*)req->pivot->req->par.v[req->pivot->fuzz_par]))) c = ~req->pivot->child_cnt; if (c == req->pivot->child_cnt) { n = req_copy(req->pivot->req, req->pivot, 1); replace_slash(n, req->trying_key); n->callback = dir_dict_check; n->user_val = 0; req->pivot->pending++; /* While bruteforcing, we explicitly set a fake Accept value to force a 406 response from servers that support content negotiation */ set_value(PARAM_HEADER, (u8*)"Accept", (u8*)"skip/fish;", 0, &n->par); async_request(n); if (prefix(req->trying_key, (u8*)".ht")) { n = req_copy(req->pivot->req, req->pivot, 1); replace_slash(n, req->trying_key); set_value(PARAM_PATH, NULL, (u8*)"", -1, &n->par); n->user_val = 0; n->callback = dir_dict_check; req->pivot->pending++; async_request(n); } } schedule_next: /* Calling dir_dict_start() ensures that, if no new requests were scheduled earlier on and nothing else is pending, that we will still advance to parametric checks. */ req->pivot->pending--; dir_dict_start(req->pivot); return 0; } static u8 dir_dict_check(struct http_request* req, struct http_response* res) { u32 i; DEBUG_CALLBACK(req, res); /* When we get a 406, than our content negotiation trick worked. It means the body contains alternatives and also that this pivot doesn't exist. So we scrape the content and return immediately. */ if(res->code == 406) { RESP_CHECKS(req, res); return 0; } if (FETCH_FAIL(res)) { handle_error(req, res, (u8*)"during path-based dictionary probes", 0); } else { /* Check if 404... */ for (i=0;ipivot->r404_cnt;i++) if (same_page(&res->sig, &req->pivot->r404[i])) break; /* Special case for secondary extension fuzzing - skip secondary extensions that seemingly return the same document. */ if (req->user_val && same_page(&res->sig, &req->same_sig)) i = ~req->pivot->r404_cnt; /* If not 404, do response, and does not look like parent's original file signature, add pivot. */ if (i == req->pivot->r404_cnt) maybe_add_pivot(req, res, 0); } /* Try replenishing the queue. */ if (!req->user_val) { req->pivot->pending--; dir_dict_start(req->pivot); } return 0; } u8 unknown_retrieve_check(struct http_request* req, struct http_response* res) { u32 i = 0 /* bad gcc */; struct pivot_desc *par; struct http_request* n; u8* name = NULL; RPRES(req) = res; DEBUG_CALLBACK(req, res); if (FETCH_FAIL(res)) { handle_error(req, res, (u8*)"during initial resource fetch", 1); return 1; } /* Matches parent's 404? */ par = dir_parent(req->pivot); if (par) for (i=0;ir404_cnt;i++) if (same_page(&res->sig, &par->r404[i])) break; /* Again, 404 is the least we could do. */ if ((!par && res->code == 404) || (par && i != par->r404_cnt)) { req->pivot->missing = 1; unlock_children(req->pivot); param_start(req->pivot); return 1; } /* If the response looks like parent's original unknown_callback() response, assume file. This is a workaround for some really quirky architectures. */ if (par && res->pay_len && res->code == 200 && same_page(&par->unk_sig, &res->sig)) { req->pivot->type = PIVOT_FILE; return file_retrieve_check(req, res); } /* Another workaround for quirky frameworks: identical signature as parent's both probes, and 3xx code. */ if (par && res->code >= 300 && res->code < 400 && same_page(&par->unk_sig, &res->sig) && same_page(&par->res->sig, &res->sig)) { req->pivot->type = PIVOT_FILE; return file_retrieve_check(req, res); } /* Special handling for .ht* */ if (req->pivot->type < PIVOT_PARAM) { u32 i; /* Find last path segment. */ for (i=0;ipar.c;i++) if (PATH_SUBTYPE(req->par.t[i])) name = req->par.v[i]; if (name && !prefix(name, (u8*)".ht")) { req->pivot->type = PIVOT_FILE; return file_retrieve_check(req, res); } } /* Schedule a request to settle the type of this pivot point. */ n = req_copy(req, req->pivot, 1); set_value(PARAM_PATH, NULL, (u8*)"", -1, &n->par); n->callback = unknown_retrieve_check2; n->user_val = 0; if (name) { u8* ppos = (u8*) strrchr((char*)name, '.'); if (!ppos || ppos == name) n->user_val = 1; } async_request(n); /* This is the initial callback, keep the response. */ return 1; } static u8 unknown_retrieve_check2(struct http_request* req, struct http_response* res) { u8 keep = 0; DEBUG_CALLBACK(req, res); if (FETCH_FAIL(res)) { handle_error(req, res, (u8*)"during node type checks", 0); goto schedule_next; } /* If pivot == res, we are probably dealing with PATH_INFO-style plot device, which is best approached as a directory anyway (worst-case scenario, dir handlers will dismiss it as misbehaving and demote it to PIVOT_PATHINFO after some extra checks). If pivot != res, and res is not a 404 response, assume dir; and if it is 404, assume file, except if pivot redirected to res. We also have a special case if the original request returned a non-empty 2xx response, but the new one returned 3xx-5xx - this is likely a file, too. */ if (same_page(&RPRES(req)->sig, &res->sig)) goto assume_dir; else { u32 i = 0; struct pivot_desc* par = dir_parent(req->pivot); if (res->code == 404 && RPRES(req)->code >= 300 && RPRES(req)->code < 400) { u8 *loc = GET_HDR((u8*)"Location", &RPRES(req)->hdr); if (loc) { u8* path = serialize_path(req, 1, 0); if (!strcasecmp((char*)path, (char*)loc)) { ck_free(path); req->pivot->sure_dir = 1; goto assume_dir; } ck_free(path); } } if (par) { for (i=0;ir404_cnt;i++) if (same_page(&res->sig, &par->r404[i])) break; /* Do not use extension-originating signatures for settling non-extension cases. */ if (i && req->user_val) i = par->r404_cnt; } if ((!par && res->code == 404) || (par && i != par->r404_cnt) || (RPRES(req)->code < 300 && res->code >= 300 && RPRES(req)->pay_len)) { req->pivot->type = PIVOT_FILE; } else { assume_dir: /* If any of the responses is 500, and the user asked for 500 to be treated specially to work around quirky frameworks, assume file right away. */ if (no_500_dir && (res->code >= 500 || RPRES(req)->code >= 500)) { DEBUG("Feels like a directory, but assuming file pivot as per -Z flag.\n"); req->pivot->type = PIVOT_FILE; goto schedule_next; } req->pivot->type = PIVOT_DIR; /* Perform content checks before discarding the old payload. */ if (!same_page(&RPRES(req)->sig, &res->sig)) content_checks(RPREQ(req), RPRES(req)); /* Replace original request, response with new data. */ destroy_request(RPREQ(req)); if (RPRES(req)) { memcpy(&req->pivot->unk_sig, &RPRES(req)->sig, sizeof(struct http_sig)); destroy_response(RPRES(req)); } RPREQ(req) = req; RPRES(req) = res; keep = 1; } } schedule_next: /* Well, we need to do something. */ if (req->pivot->type == PIVOT_DIR || req->pivot->type == PIVOT_SERV) dir_retrieve_check(RPREQ(req), RPRES(req)); else file_retrieve_check(RPREQ(req), RPRES(req)); return keep; } skipfish-2.10b/src/http_client.c0000440036502000116100000021526012057375131015650 0ustar heinenneng/* skipfish - high-performance, single-process asynchronous HTTP client -------------------------------------------------------------------- Author: Michal Zalewski Copyright 2009, 2010, 2011 by Google Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "types.h" #include "alloc-inl.h" #include "string-inl.h" #include "database.h" #include "http_client.h" /* Assorted exported settings: */ u32 max_connections = MAX_CONNECTIONS, max_conn_host = MAX_CONN_HOST, max_requests = MAX_REQUESTS, max_fail = MAX_FAIL, idle_tmout = IDLE_TMOUT, resp_tmout = RESP_TMOUT, rw_tmout = RW_TMOUT, size_limit = SIZE_LIMIT; u8 browser_type = BROWSER_FAST; u8 auth_type = AUTH_NONE; float max_requests_sec = MAX_REQUESTS_SEC; struct param_array global_http_par; /* Counters: */ float req_sec; u32 req_errors_net, req_errors_http, req_errors_cur, req_count, req_dropped, queue_cur, conn_cur, conn_count, conn_idle_tmout, conn_busy_tmout, conn_failed, req_retried, url_scope; u64 bytes_sent, bytes_recv, bytes_deflated, bytes_inflated, iterations_cnt = 0; u8 *auth_user, *auth_pass; #ifdef PROXY_SUPPORT u8* use_proxy; u32 use_proxy_addr; u16 use_proxy_port; #endif /* PROXY_SUPPORT */ u8 ignore_cookies, idle; /* Internal globals for queue management: */ static struct queue_entry* queue; static struct conn_entry* conn; static struct dns_entry* dns; #ifdef QUEUE_FILO static struct queue_entry* q_tail; #endif /* QUEUE_FILO */ static u8 tear_down_idle; /* Extracts parameter value from param_array. Name is matched if non-NULL. Returns pointer to value data, not a duplicate string; NULL if no match found. */ u8* get_value(u8 type, u8* name, u32 offset, struct param_array* par) { u32 i, coff = 0; for (i=0;ic;i++) { if (type != par->t[i]) continue; if (name && (!par->n[i] || strcasecmp((char*)par->n[i], (char*)name))) continue; if (offset != coff) { coff++; continue; } return par->v[i]; } return NULL; } /* Inserts or overwrites parameter value in param_array. If offset == -1, will append parameter to list. Duplicates strings, name and val can be NULL. */ void set_value(u8 type, u8* name, u8* val, s32 offset, struct param_array* par) { u32 i, coff = 0; s32 matched = -1; /* If offset specified, try to find an entry to replace. */ if (offset >= 0) for (i=0;ic;i++) { if (type != par->t[i]) continue; if (name && (!par->n[i] || strcasecmp((char*)par->n[i], (char*)name))) continue; if (offset != coff) { coff++; continue; } matched = i; break; } if (matched == -1) { /* No offset or no match - append to the end of list. */ par->t = ck_realloc(par->t, (par->c + 1) * sizeof(u8)); par->n = ck_realloc(par->n, (par->c + 1) * sizeof(u8*)); par->v = ck_realloc(par->v, (par->c + 1) * sizeof(u8*)); par->t[par->c] = type; par->n[par->c] = ck_strdup(name); par->v[par->c] = ck_strdup(val); par->c++; } else { /* Matched - replace name & value. */ ck_free(par->n[matched]); ck_free(par->v[matched]); par->n[matched] = ck_strdup(name); par->v[matched] = ck_strdup(val); } } /* Convert a fully-qualified or relative URL string to a proper http_request representation. Returns 0 on success, 1 on format error. */ u8 parse_url(u8* url, struct http_request* req, struct http_request* ref) { u8* cur = url; u32 maybe_proto = strcspn((char*)url, ":/?#@"); u8 has_host = 0, add_slash = 1; if (strlen((char*)url) > MAX_URL_LEN) return 1; req->orig_url = ck_strdup(url); /* Interpret, skip protocol string if the URL seems to be fully-qualified; otherwise, copy from referring URL. We could be stricter here, as browsers bail out on seemingly invalid chars in proto names, but... */ if (maybe_proto && url[maybe_proto] == ':') { if (!case_prefix(url, "http:")) { req->proto = PROTO_HTTP; cur += 5; } else if (!case_prefix(url, "https:")) { req->proto = PROTO_HTTPS; cur += 6; } else return 1; } else { if (!ref || !ref->proto) return 1; req->proto = ref->proto; } /* Interpret, skip //[login[:pass@](\[ipv4\]|\[ipv6\]|host)[:port] part of the URL, if present. Note that "http:blarg" is a valid relative URL to most browsers, and "//example.com/blarg" is a valid non-FQ absolute one. We need to mimick this, which complicates the code a bit. We only accept /, ?, #, and : to mark the end of a host name. Some browsers also allow \ or ;, but it's unlikely that we need to obey this. */ if (cur[0] == '/' && cur[1] == '/') { u32 path_st; u8 *at_sign, *host, *x; u8 has_utf = 0; cur += 2; /* Detect, skip login[:pass]@; we only use cmdline-supplied credentials or wordlists into account. Be sure to report any embedded auth, though. Trivia: Firefox takes the rightmost, not the leftmost @ char into account. Not very important, but amusing. */ at_sign = (u8*)strchr((char*)cur, '@'); path_st = strcspn((char*)cur, "/?#"); if (at_sign && path_st > (at_sign - cur)) { cur = at_sign + 1; if (!req->pivot) return 1; problem(PROB_URL_AUTH, ref, 0, url, req->pivot, 0); } path_st = strcspn((char*)cur, ":/?#"); /* No support for IPv6 or [ip] notation for now, so let's just refuse to parse the URL. Also, refuse excessively long domain names for sanity. */ if (*cur == '[') return 1; if (path_st > MAX_DNS_LEN) return 1; x = host = ck_memdup(cur, path_st + 1); host[path_st] = 0; /* Scan, normalize extracted host name. */ while (*x) { switch (*x) { case 'A' ... 'Z': *x = tolower(*x); break; case 'a' ... 'z': case '0' ... '9': case '.': case '-': case '_': break; case 0x80 ... 0xff: has_utf = 1; break; default: /* Uh-oh, invalid characters in a host name - abandon ship. */ ck_free(host); return 1; } x++; } /* Host names that contained high bits need to be converted to Punycode in order to resolve properly. */ if (has_utf) { char* output = 0; if (idna_to_ascii_8z((char*)host, &output, 0) != IDNA_SUCCESS || strlen(output) > MAX_DNS_LEN) { ck_free(host); free(output); return 1; } ck_free(host); host = ck_strdup((u8*)output); free(output); } req->host = host; cur += path_st; /* All right, moving on: if host name is followed by :, let's try to parse and validate port number; otherwise, assume 80 / 443, depending on protocol. */ if (*cur == ':') { u32 digit_cnt = strspn((char*)++cur, "0123456789"); u32 port = atoi((char*)cur); if (!digit_cnt || (cur[digit_cnt] && !strchr("/?#", cur[digit_cnt]))) return 1; req->port = port; cur += digit_cnt; } else { if (req->proto == PROTO_HTTPS) req->port = 443; else req->port = 80; } has_host = 1; } else { /* No host name found - copy from referring request instead. */ if (!ref || !ref->host) return 1; req->host = ck_strdup(ref->host); req->addr = ref->addr; req->port = ref->port; } if (!*cur || *cur == '#') { u32 i; /* No-op path. If the URL does not specify host (e.g., #foo), copy everything from referring request, call it a day. Otherwise (e.g., http://example.com#foo), let tokenize_path() run to add NULL-"" entry to the list. */ if (!has_host) { for (i=0;ipar.c;i++) if (PATH_SUBTYPE(ref->par.t[i]) || QUERY_SUBTYPE(ref->par.t[i])) set_value(ref->par.t[i], ref->par.n[i], ref->par.v[i], -1, &req->par); return 0; } } if (!has_host && *cur == '?') { u32 i; /* URL begins with ? and does not specify host (e.g., ?foo=bar). Copy all path segments, but no query, then fall through to parse the query string. */ for (i=0;ipar.c;i++) if (PATH_SUBTYPE(ref->par.t[i])) set_value(ref->par.t[i], ref->par.n[i], ref->par.v[i], -1, &req->par); /* In this case, we do not want tokenize_path() to tinker with the path in any way. */ add_slash = 0; } else if (!has_host && *cur != '/') { /* The URL does not begin with / or ?, and does not specify host (e.g., foo/bar?baz). Copy path from referrer, but drop the last "proper" path segment and everything that follows it. This mimicks browser behavior (for URLs ending with /, it just drops the final NULL-"" pair). */ u32 i; u32 path_cnt = 0, path_cur = 0; for (i=0;ipar.c;i++) if (ref->par.t[i] == PARAM_PATH) path_cnt++; for (i=0;ipar.c;i++) { if (ref->par.t[i] == PARAM_PATH) path_cur++; if (path_cur < path_cnt && PATH_SUBTYPE(ref->par.t[i])) set_value(ref->par.t[i], ref->par.n[i], ref->par.v[i], -1, &req->par); } } /* Tokenize the remaining path on top of what we parsed / copied over. */ tokenize_path(cur, req, add_slash); return 0; } /* URL-decodes a string. 'Plus' parameter governs the behavior on + signs (as they have a special meaning only in query params, not in path). */ u8* url_decode_token(u8* str, u32 len, u8 plus) { u8 *ret = ck_alloc(len + 1); u8 *src = str, *dst = ret; char *hex_str = "0123456789abcdef"; while (len--) { u8 c = *(src++); char *f, *s; if (plus && c == '+') c = ' '; if (c == '%' && len >= 2 && (f = strchr(hex_str, tolower(src[0]))) && (s = strchr(hex_str, tolower(src[1])))) { c = ((f - hex_str) << 4) | (s - hex_str); src += 2; len -= 2; } /* We can't handle NUL-terminators gracefully when deserializing request parameters, because param_array values are NUL-terminated themselves. Let's encode \0 as \xFF instead, and hope nobody notices. */ if (!c) c = 0xff; *(dst++) = c; } *(dst++) = 0; ret = ck_realloc(ret, dst - ret); return ret; } /* URL-encodes a string according to custom rules. The assumption here is that the data is already tokenized at "special" boundaries such as ?, =, &, /, ;, !, $, and , so these characters must always be escaped if present in tokens. We otherwise let pretty much everything else go through, as it may help with the exploitation of certain vulnerabilities. */ u8* url_encode_token(u8* str, u32 len, u8* enc_set) { u8 *ret = ck_alloc(len * 3 + 1); u8 *src = str, *dst = ret; while (len--) { u8 c = *(src++); if (c <= 0x20 || c >= 0x80 || strchr((char*)enc_set, c)) { if (c == 0xFF) c = 0; sprintf((char*)dst, "%%%02X", c); dst += 3; } else *(dst++) = c; } *(dst++) = 0; ret = ck_realloc(ret, dst - ret); return ret; } /* Split path at known "special" character boundaries, URL decode values, then put them in the provided http_request struct. */ void tokenize_path(u8* str, struct http_request* req, u8 add_slash) { u8* cur; u8 know_dir = 0; while (*str == '/') str++; cur = str; /* Parse path elements first. */ while (*cur && !strchr("?#", *cur)) { u32 next_seg, next_eq; u8 *name = NULL, *value = NULL; u8 first_el = (str == cur); if (first_el || *cur == '/') { /* Optimize out //, /\0, /./, and /.\0. They do indicate we are looking at a directory, so mark this. */ if (!first_el && (cur[1] == '/' || !cur[1])) { cur++; know_dir = 1; continue; } if (cur[0 + !first_el] == '.' && (cur[1 + !first_el] == '/' || !cur[1 + !first_el])) { cur += 1 + !first_el; know_dir = 1; continue; } /* Also optimize out our own \.\ prefix injected in directory probes. This is to avoid recursion if it actually worked in some way. */ if (!prefix(cur, "/\\.\\") && (cur[4] == '/' || !cur[4])) { cur += 4; continue; } if (!case_prefix(cur, "/%5c.%5c") && (cur[8] == '/' || !cur[8])) { cur += 8; continue; } /* If we encountered /../ or /..\0, remove everything up to and including the last "true" path element. It's also indicative of a directory, by the way. */ if (cur[0 + !first_el] == '.' && cur[1 + !first_el] == '.' && (cur[2 + !first_el] == '/' || !cur[2 + !first_el])) { u32 i, last_p = req->par.c; for (i=0;ipar.c;i++) if (req->par.t[i] == PARAM_PATH) last_p = i; for (i=last_p;ipar.c;i++) { req->par.t[i] = PARAM_NONE; } cur += 2 + !first_el; know_dir = 1; continue; } } /* If we're here, we have an actual item to add; cur points to the string if it's the first element, or to field separator if one of the subsequent ones. */ next_seg = strcspn((char*)cur + 1, "/;,!$?#") + 1, next_eq = strcspn((char*)cur + 1, "=/;,!$?#") + 1; know_dir = 0; if (next_eq < next_seg) { name = url_decode_token(cur + !first_el, next_eq - !first_el, 0); value = url_decode_token(cur + next_eq + 1, next_seg - next_eq - 1, 0); } else { value = url_decode_token(cur + !first_el, next_seg - !first_el, 0); } /* If the extracted segment is just '.' or '..', but is followed by something else than '/', skip one separator. */ if (!name && cur[next_seg] && cur[next_seg] != '/' && (!strcmp((char*)value, ".") || !strcmp((char*)value, ".."))) { next_seg = strcspn((char*)cur + next_seg + 1, "/;,!$?#") + next_seg + 1, ck_free(name); ck_free(value); value = url_decode_token(cur + !first_el, next_seg - !first_el, 0); } switch (first_el ? '/' : *cur) { case ';': set_value(PARAM_PATH_S, name, value, -1, &req->par); break; case ',': set_value(PARAM_PATH_C, name, value, -1, &req->par); break; case '!': set_value(PARAM_PATH_E, name, value, -1, &req->par); break; case '$': set_value(PARAM_PATH_D, name, value, -1, &req->par); break; default: set_value(PARAM_PATH, name, value, -1, &req->par); } ck_free(name); ck_free(value); cur += next_seg; } /* If the last segment was /, /./, or /../, *or* if we never added anything to the path to begin with, we want to store a NULL-"" entry to denote it's a directory. */ if (know_dir || (add_slash && (!*str || strchr("?#", *str)))) set_value(PARAM_PATH, NULL, (u8*)"", -1, &req->par); /* Deal with regular query parameters now. This is much simpler, obviously. */ while (*cur && !strchr("#", *cur)) { u32 next_seg = strcspn((char*)cur + 1, "#&;,!$") + 1; u32 next_eq = strcspn((char*)cur + 1, "=#&;,!$") + 1; u8 *name = NULL, *value = NULL; /* foo=bar syntax... */ if (next_eq < next_seg) { name = url_decode_token(cur + 1, next_eq - 1, 1); value = url_decode_token(cur + next_eq + 1, next_seg - next_eq - 1, 1); } else { value = url_decode_token(cur + 1, next_seg - 1, 1); } switch (*cur) { case ';': set_value(PARAM_QUERY_S, name, value, -1, &req->par); break; case ',': set_value(PARAM_QUERY_C, name, value, -1, &req->par); break; case '!': set_value(PARAM_QUERY_E, name, value, -1, &req->par); break; case '$': set_value(PARAM_QUERY_D, name, value, -1, &req->par); break; default: set_value(PARAM_QUERY, name, value, -1, &req->par); } ck_free(name); ck_free(value); cur += next_seg; } } /* Reconstructs URI from http_request data. Includes protocol and host if with_host is non-zero. */ u8* serialize_path(struct http_request* req, u8 with_host, u8 with_post) { u32 i, cur_pos; u8 got_search = 0; u8* ret; NEW_STR(ret, cur_pos); #define ASD(_p3) ADD_STR_DATA(ret, cur_pos, _p3) /* For human-readable uses... */ if (with_host) { ASD("http"); if (req->proto == PROTO_HTTPS) ASD("s"); ASD("://"); ASD(req->host); if ((req->proto == PROTO_HTTP && req->port != 80) || (req->proto == PROTO_HTTPS && req->port != 443)) { u8 port[7]; sprintf((char*)port, ":%u", req->port); ASD(port); } } /* First print path... */ for (i=0;ipar.c;i++) { u8 *enc = (u8*)ENC_PATH; if(req->pivot && req->fuzz_par_enc && i == req->pivot->fuzz_par) enc = req->fuzz_par_enc; if (PATH_SUBTYPE(req->par.t[i])) { switch (req->par.t[i]) { case PARAM_PATH_S: ASD(";"); break; case PARAM_PATH_C: ASD(","); break; case PARAM_PATH_E: ASD("!"); break; case PARAM_PATH_D: ASD("$"); break; default: ASD("/"); } if (req->par.n[i]) { u32 len = strlen((char*)req->par.n[i]); u8* str = url_encode_token(req->par.n[i], len, enc); ASD(str); ASD("="); ck_free(str); } if (req->par.v[i]) { u32 len = strlen((char*)req->par.v[i]); u8* str = url_encode_token(req->par.v[i], len, enc); ASD(str); ck_free(str); } } } /* Then actual parameters. */ for (i=0;ipar.c;i++) { u8 *enc = (u8*)ENC_DEFAULT; if(req->pivot && req->fuzz_par_enc && i == req->pivot->fuzz_par) enc = req->fuzz_par_enc; if (QUERY_SUBTYPE(req->par.t[i])) { if (!got_search) { ASD("?"); got_search = 1; } else switch (req->par.t[i]) { case PARAM_QUERY_S: ASD(";"); break; case PARAM_QUERY_C: ASD(","); break; case PARAM_QUERY_E: ASD("!"); break; case PARAM_QUERY_D: ASD("$"); break; default: ASD("&"); } if (req->par.n[i]) { u32 len = strlen((char*)req->par.n[i]); u8* str = url_encode_token(req->par.n[i], len, enc); ASD(str); ASD("="); ck_free(str); } if (req->par.v[i]) { u32 len = strlen((char*)req->par.v[i]); u8* str = url_encode_token(req->par.v[i], len, enc); ASD(str); ck_free(str); } } } got_search = 0; if (with_post) for (i=0;ipar.c;i++) { u8 *enc = (u8*)ENC_DEFAULT; if(req->pivot && req->fuzz_par_enc && i == req->pivot->fuzz_par) enc = req->fuzz_par_enc; if (POST_SUBTYPE(req->par.t[i])) { if (!got_search) { ASD(" DATA:"); got_search = 1; } else ASD("&"); if (req->par.n[i]) { u32 len = strlen((char*)req->par.n[i]); u8* str = url_encode_token(req->par.n[i], len, enc); ASD(str); ASD("="); ck_free(str); } if (req->par.v[i]) { u32 len = strlen((char*)req->par.v[i]); u8* str = url_encode_token(req->par.v[i], len, enc); ASD(str); ck_free(str); } } } #undef ASD TRIM_STR(ret, cur_pos); return ret; } /* Looks up IP for a particular host, returns data in network order. Uses standard resolver, so it is slow and blocking, but we only expect to call it a couple of times during a typical assessment. There are some good async DNS libraries to consider in the long run. */ u32 maybe_lookup_host(u8* name) { struct hostent* h; struct dns_entry *d = dns, *prev = NULL; u32 ret_addr = 0; struct in_addr in; #ifdef PROXY_SUPPORT /* If configured to use proxy, look up proxy IP once; and return that address for all host names. */ if (use_proxy) { if (!use_proxy_addr) { /* Don't bother resolving raw IP addresses, naturally. */ if (inet_aton((char*)use_proxy, &in)) return (use_proxy_addr = (u32)in.s_addr); h = gethostbyname((char*)use_proxy); /* If lookup fails with a transient error, be nice - try again. */ if (!h && h_errno == TRY_AGAIN) h = gethostbyname((char*)name); if (!h || !(use_proxy_addr = *(u32*)h->h_addr_list[0])) FATAL("Unable to resolve proxy host name '%s'.", use_proxy); } return use_proxy_addr; } /* If no proxy... */ #endif /* PROXY_SUPPORT */ /* Don't bother resolving raw IP addresses, naturally. */ if (inet_aton((char*)name, &in)) return (u32)in.s_addr; while (d) { if (!strcasecmp((char*)name, (char*)d->name)) return d->addr; prev = d; d = d->next; } h = gethostbyname((char*)name); /* If lookup fails with a transient error, be nice - try again. */ if (!h && h_errno == TRY_AGAIN) h = gethostbyname((char*)name); if (h) { u32 i = 0; /* For each address associated with the host, see if we have any other hosts that resolved to that same IP. If yes, return that address; otherwise, just return first. This is for HTTP performance and bookkeeping reasons. */ while (h->h_addr_list[i]) { d = dns; while (d) { if (d->addr == *(u32*)h->h_addr_list[i]) { ret_addr = d->addr; goto dns_got_name; } d = d->next; } i++; } ret_addr = *(u32*)h->h_addr_list[0]; } dns_got_name: if (!prev) d = dns = ck_alloc(sizeof(struct dns_entry)); else d = prev->next = ck_alloc(sizeof(struct dns_entry)); d->name = ck_strdup(name); d->addr = ret_addr; return ret_addr; } /* Creates an ad hoc DNS cache entry, to override NS lookups. */ void fake_host(u8* name, u32 addr) { struct dns_entry *d = dns, *prev = dns; while (d && d->next) { prev = d ; d = d->next;} if (!dns) d = dns = ck_alloc(sizeof(struct dns_entry)); else d = prev->next = ck_alloc(sizeof(struct dns_entry)); d->name = ck_strdup(name); d->addr = addr; } /* Prepares a serialized HTTP buffer to be sent over the network. */ u8* build_request_data(struct http_request* req) { u8 *ret_buf, *ck_buf, *pay_buf, *path; u32 ret_pos, ck_pos, pay_pos, i; u8 req_type = PARAM_NONE; u8 browser = browser_type; if (req->proto == PROTO_NONE) FATAL("uninitialized http_request"); NEW_STR(ret_buf, ret_pos); path = serialize_path(req, 0, 0); #define ASD(_p3) ADD_STR_DATA(ret_buf, ret_pos, _p3) if (req->method) ASD(req->method); else ASD((u8*)"GET"); ASD(" "); #ifdef PROXY_SUPPORT /* For non-CONNECT proxy requests, insert http://host[:port] too. */ if (use_proxy && req->proto == PROTO_HTTP) { ASD("http://"); ASD(req->host); if (req->port != 80) { char port[7]; sprintf((char*)port, ":%u", req->port); ASD(port); } } #endif /* PROXY_SUPPORT */ ASD(path); ASD(" HTTP/1.1\r\n"); ck_free(path); ASD("Host: "); ASD(req->host); if ((req->proto == PROTO_HTTP && req->port != 80) || (req->proto == PROTO_HTTPS && req->port != 443)) { char port[7]; sprintf((char*)port, ":%u", req->port); ASD(port); } ASD("\r\n"); /* Insert generic browser headers first. If the request is for a specific browser, we use that. Else we use the pivot browser or, when that is not set, fall back on the default browser (e.g. set with -b). */ if (req->browser) { browser = req->browser; } else if (req->pivot->browser) { browser = req->browser; } if (browser == BROWSER_FAST) { ASD("Accept-Encoding: gzip\r\n"); ASD("Connection: keep-alive\r\n"); if (!GET_HDR((u8*)"User-Agent", &req->par)) ASD("User-Agent: Mozilla/5.0 SF/" VERSION "\r\n"); /* Some servers will reject to gzip responses unless "Mozilla/..." is seen in User-Agent. Bleh. */ } else if (browser == BROWSER_FFOX) { if (!GET_HDR((u8*)"User-Agent", &req->par)) ASD("User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; " "rv:1.9.1.2) Gecko/20090729 Firefox/3.5.2 SF/" VERSION "\r\n"); ASD("Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;" "q=0.8\r\n"); if (!GET_HDR((u8*)"Accept-Language", &req->par)) ASD("Accept-Language: en-us,en\r\n"); ASD("Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n"); ASD("Keep-Alive: 300\r\n"); ASD("Connection: keep-alive\r\n"); } else if (browser == BROWSER_MSIE) { ASD("Accept: */*\r\n"); if (!GET_HDR((u8*)"Accept-Language", &req->par)) ASD("Accept-Language: en,en-US;q=0.5\r\n"); if (!GET_HDR((u8*)"User-Agent", &req->par)) ASD("User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; " "Trident/4.0; .NET CLR 1.1.4322; InfoPath.1; .NET CLR " "2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; SF/" VERSION ")\r\n"); ASD("Accept-Encoding: gzip, deflate\r\n"); ASD("Connection: Keep-Alive\r\n"); } else /* iPhone */ { if (!GET_HDR((u8*)"User-Agent", &req->par)) ASD("User-Agent: Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_1 like Mac OS " "X; en-us) AppleWebKit/532.9 (KHTML, like Gecko) Version/4.0.5 " "Mobile/8B117 Safari/6531.22.7 SF/" VERSION "\r\n"); ASD("Accept: application/xml,application/xhtml+xml,text/html;q=0.9," "text/plain;q=0.8,image/png,*/*;q=0.5\r\n"); if (!GET_HDR((u8*)"Accept-Language", &req->par)) ASD("Accept-Language: en-us\r\n"); ASD("Accept-Encoding: gzip, deflate\r\n"); ASD("Connection: keep-alive\r\n"); } /* Request a limited range up front to minimize unwanted traffic. Note that some Oracle servers apparently fail on certain ranged requests, so allowing -H override seems like a good idea. */ if (!GET_HDR((u8*)"Range", &global_http_par)) { u8 limit[32]; sprintf((char*)limit, "Range: bytes=0-%u\r\n", size_limit - 1); ASD(limit); } /* Include a dummy "Referer" header, to avoid certain XSRF checks. */ if (!GET_HDR((u8*)"Referer", &req->par)) { ASD("Referer: http"); if (req->proto == PROTO_HTTPS) ASD("s"); ASD("://"); ASD(req->host); ASD("/\r\n"); } /* Take care of HTTP authentication next. */ if (auth_type == AUTH_BASIC) { u8* lp = ck_alloc(strlen((char*)auth_user) + strlen((char*)auth_pass) + 2); u8* lpb64; sprintf((char*)lp, "%s:%s", auth_user, auth_pass); lpb64 = b64_encode(lp, strlen((char*)lp)); ASD("Authorization: Basic "); ASD(lpb64); ASD("\r\n"); ck_free(lpb64); ck_free(lp); } /* Append any other requested headers and cookies. */ NEW_STR(ck_buf, ck_pos); for (i=0;ipar.c;i++) { if (req->par.t[i] == PARAM_HEADER) { ASD(req->par.n[i]); ASD(": "); ASD(req->par.v[i]); ASD("\r\n"); } else if (req->par.t[i] == PARAM_COOKIE) { if (ck_pos) ADD_STR_DATA(ck_buf, ck_pos, ";"); ADD_STR_DATA(ck_buf, ck_pos, req->par.n[i]); ADD_STR_DATA(ck_buf, ck_pos, "="); ADD_STR_DATA(ck_buf, ck_pos, req->par.v[i]); } } /* Also include extra globals, if any (but avoid dupes). */ for (i=0;ipar)) { ASD(global_http_par.n[i]); ASD(": "); ASD(global_http_par.v[i]); ASD("\r\n"); } else if (global_http_par.t[i] == PARAM_COOKIE && !GET_CK(global_http_par.n[i], &req->par)) { if (ck_pos) ADD_STR_DATA(ck_buf, ck_pos, "; "); ADD_STR_DATA(ck_buf, ck_pos, global_http_par.n[i]); ADD_STR_DATA(ck_buf, ck_pos, "="); ADD_STR_DATA(ck_buf, ck_pos, global_http_par.v[i]); } } if (ck_pos && !req->no_cookies) { ASD("Cookie: "); ASD(ck_buf); ASD("\r\n"); } ck_free(ck_buf); /* Now, let's serialize the payload, if necessary. */ for (i=0;ipar.c;i++) { switch (req->par.t[i]) { case PARAM_POST_F: case PARAM_POST_O: req_type = req->par.t[i]; break; case PARAM_POST: if (req_type == PARAM_NONE) req_type = PARAM_POST; break; } } NEW_STR(pay_buf, pay_pos); if (req_type == PARAM_POST) { /* The default case: application/x-www-form-urlencoded. */ for (i=0;ipar.c;i++) { u8 *enc = (u8*)ENC_DEFAULT; if(req->pivot && req->fuzz_par_enc && i == req->pivot->fuzz_par) enc = req->fuzz_par_enc; if (req->par.t[i] == PARAM_POST) { if (pay_pos) ADD_STR_DATA(pay_buf, pay_pos, "&"); if (req->par.n[i]) { u32 len = strlen((char*)req->par.n[i]); u8* str = url_encode_token(req->par.n[i], len, enc); ADD_STR_DATA(pay_buf, pay_pos, str); ADD_STR_DATA(pay_buf, pay_pos, "="); ck_free(str); } if (req->par.v[i]) { u32 len = strlen((char*)req->par.v[i]); u8* str = url_encode_token(req->par.v[i], len, enc); ADD_STR_DATA(pay_buf, pay_pos, str); ck_free(str); } } } ASD("Content-Type: application/x-www-form-urlencoded\r\n"); } else if (req_type == PARAM_POST_O) { /* Opaque, non-escaped data of some sort. */ for (i=0;ipar.c;i++) if (req->par.t[i] == PARAM_POST_O && req->par.v[i]) ADD_STR_DATA(pay_buf, pay_pos, req->par.v[i]); ASD("Content-Type: text/plain\r\n"); } else if (req_type == PARAM_POST_F) { u8 bound[20]; /* MIME envelopes: multipart/form-data */ sprintf((char*)bound, "sf%u", R(1000000)); for (i=0;ipar.c;i++) if (req->par.t[i] == PARAM_POST || req->par.t[i] == PARAM_POST_F) { ADD_STR_DATA(pay_buf, pay_pos, "--"); ADD_STR_DATA(pay_buf, pay_pos, bound); ADD_STR_DATA(pay_buf, pay_pos, "\r\n" "Content-Disposition: form-data; name=\""); if (req->par.n[i]) ADD_STR_DATA(pay_buf, pay_pos, req->par.n[i]); if (req->par.t[i] == PARAM_POST_F) { u8 tmp[64]; sprintf((char*)tmp, "\"; filename=\"sfish%u." DUMMY_EXT "\"\r\n" "Content-Type: " DUMMY_MIME "\r\n\r\n", R(16)); ADD_STR_DATA(pay_buf, pay_pos, tmp); ADD_STR_DATA(pay_buf, pay_pos, new_xss_tag((u8*)DUMMY_FILE)); register_xss_tag(req); } else { ADD_STR_DATA(pay_buf, pay_pos, "\"\r\n\r\n"); if (req->par.v[i]) ADD_STR_DATA(pay_buf, pay_pos, req->par.v[i]); } ADD_STR_DATA(pay_buf, pay_pos, "\r\n"); } ADD_STR_DATA(pay_buf, pay_pos, "--"); ADD_STR_DATA(pay_buf, pay_pos, bound); ADD_STR_DATA(pay_buf, pay_pos, "--\r\n"); ASD("Content-Type: multipart/form-data; boundary="); ASD(bound); ASD("\r\n"); } else if (req_type == 0) ASD("\r\n"); /* Finalize HTTP payload... */ for (i=0;i start_ptr && strchr("\r\n", *(cur_ptr-1))) cur_ptr--; ret = ck_alloc(cur_ptr - start_ptr + 1); memcpy(ret, start_ptr, cur_ptr - start_ptr); ret[cur_ptr - start_ptr] = 0; return ret; } /* Builds response fingerprint data. These fingerprints are used to find "roughly comparable" pages based on their word length distributions (divided into FP_SIZE buckets). */ void fprint_response(struct http_response* res) { u32 i, c_len = 0, in_space = 0; res->sig.code = res->code; for (i=0;ipay_len;i++) if (res->payload[i] <= 0x20 || strchr("<>\"'&:\\", (char)res->payload[i])) { if (!in_space) { in_space = 1; if (c_len && ++c_len <= FP_MAX_LEN) res->sig.data[c_len % FP_SIZE]++; c_len = 0; } else c_len++; if (res->payload[i] == '&') do { i++; } while (i < res->pay_len && (isalnum(res->payload[i]) || strchr("#;", (char)res->payload[i]))); } else { if (in_space) { in_space = 0; if (c_len && ++c_len <= FP_MAX_LEN) res->sig.data[c_len % FP_SIZE]++; c_len = 0; } else { res->sig.has_text = 1; c_len++; } } if (c_len) res->sig.data[c_len % FP_SIZE]++; } /* Parses a network buffer containing raw HTTP response received over the network ('more' == the socket is still available for reading). Returns 0 if response parses OK, 1 if more data should be read from the socket, 2 if the response seems invalid, 3 if response OK but connection must be closed. */ u8 parse_response(struct http_request* req, struct http_response* res, u8* data, u32 data_len, u8 more) { u8* cur_line = 0; s32 pay_len = -1; u32 cur_data_off = 0, total_chunk = 0, http_ver; u8 chunked = 0, compressed = 0, must_close = 0; if (res->code) FATAL("struct http_response reused! Original code '%u'.", res->code); #define NEXT_LINE() do { \ if (cur_line) ck_free(cur_line); \ cur_line = grab_line(data, &cur_data_off, data_len); \ } while (0) /* First, let's do a superficial request completeness check. Be prepared for a premature end at any point. */ NEXT_LINE(); /* HTTP/1.x xxx ... */ if (!cur_line) return more ? 1 : 2; if (strlen((char*)cur_line) < 7 && more) { ck_free(cur_line); return 1; } if (prefix(cur_line, "HTTP/1.")) { ck_free(cur_line); return 2; } /* Scan headers for Content-Length, Transfer-Encoding, etc. */ while (1) { NEXT_LINE(); /* Next header or empty line. */ /* If headers end prematurely, and more data might arrive, ask for it; otherwise, just assume end of headers and continue. */ if (!cur_line) { if (more) return 1; res->warn |= WARN_PARTIAL; break; } /* Empty line indicates the beginning of a payload. */ if (!cur_line[0]) break; if (!case_prefix(cur_line, "Content-Length:")) { /* The value in Content-Length header would be useful for seeing if we have all the requested data already. Reject invalid values to avoid integer overflows, etc, though. */ if (sscanf((char*)cur_line + 15, "%d", &pay_len) == 1) { if (pay_len < 0 || pay_len > 1000000000 /* 1 GB */) { ck_free(cur_line); return 2; } } else pay_len = -1; } else if (!case_prefix(cur_line, "Transfer-Encoding:")) { /* Transfer-Encoding: chunked must be accounted for to properly determine if we received all the data when Content-Length not found. */ u8* x = cur_line + 18; while (isspace(*x)) x++; if (!strcasecmp((char*)x, "chunked")) chunked = 1; } else if (!case_prefix(cur_line, "Content-Encoding:")) { /* Content-Encoding is good to know, too. */ u8* x = cur_line + 17; while (isspace(*x)) x++; if (!strcasecmp((char*)x, "deflate") || !strcasecmp((char*)x, "gzip")) compressed = 1; } else if (!case_prefix(cur_line, "Connection:")) { u8* x = cur_line + 11; while (isspace(*x)) x++; if (!strcasecmp((char*)x, "close")) must_close = 1; } } /* We are now at the beginning of the payload. Firstly, how about decoding 'chunked' to see if we received a complete 0-byte terminator chunk already? */ if (chunked) { while (1) { u32 chunk_len; NEXT_LINE(); /* Should be chunk size, hex. */ if (!cur_line || sscanf((char*)cur_line, "%x", &chunk_len) != 1) { if (more) { ck_free(cur_line); return 1; } res->warn |= WARN_PARTIAL; break; } if (chunk_len > 1000000000 || total_chunk > 1000000000 /* 1 GB */) { ck_free(cur_line); return 2; } /* See if we actually enough buffer to skip the chunk. Bail out if not and more data might be coming; otherwise, adjust chunk size accordingly. */ if (cur_data_off + chunk_len > data_len) { if (more) { ck_free(cur_line); return 1; } chunk_len = data_len - cur_data_off; total_chunk += chunk_len; res->warn |= WARN_PARTIAL; break; } total_chunk += chunk_len; cur_data_off += chunk_len; NEXT_LINE(); /* No newline? */ if (!cur_line) { if (more) return 1; res->warn |= WARN_PARTIAL; } /* All right, so that was the last, complete 0-size chunk? Exit the loop if so. */ if (!chunk_len) break; } if (cur_data_off != data_len) res->warn |= WARN_TRAIL; } else if (pay_len == -1 && more) { /* If in a mode other than 'chunked', and C-L not received, but more data might be available - try to request it. */ ck_free(cur_line); return 1; } else if (pay_len != 1) { if (cur_data_off + pay_len > data_len) { /* If C-L seen, but not nough data in the buffer, try to request more if possible, otherwise tag the response as partial. */ if (more) { ck_free(cur_line); return 1; } res->warn |= WARN_PARTIAL; } else if (cur_data_off + pay_len < data_len) res->warn |= WARN_TRAIL; } /* Rewind, then properly parse HTTP headers, parsing cookies. */ cur_data_off = 0; NEXT_LINE(); if (strlen((char*)cur_line) < 13 || sscanf((char*)cur_line, "HTTP/1.%u %u ", &http_ver, &res->code) != 2 || res->code < 100 || res->code > 999) { ck_free(cur_line); return 2; } /* Some servers, when presented with 'Range' header, will return 200 on some queries for a particular resource, and 206 on other queries (e.g., with query string), despite returning exactly as much data. As an ugly workaround... */ if (res->code == 206) res->code = 200; if (http_ver == 0) must_close = 1; res->msg = ck_strdup(cur_line + 13); while (1) { u8* val; NEXT_LINE(); /* Next header or empty line. */ if (!cur_line) return 2; if (!cur_line[0]) break; /* Split field name and value */ val = (u8*) strchr((char*)cur_line, ':'); if (!val) { ck_free(cur_line); return 2; } *val = 0; while (isspace(*(++val))); SET_HDR(cur_line, val, &res->hdr); if (!strcasecmp((char*)cur_line, "Set-Cookie") || !strcasecmp((char*)cur_line, "Set-Cookie2")) { /* We could bother with a proper tokenizer here, but contrary to "teh standards", browsers generally don't accept multiple cookies in Set-Cookie headers, handle quoted-string encoding inconsistently, etc. So let's just grab the first value naively and move on. */ u8* cval; u8* orig_val; cval = (u8*) strchr((char*)val, ';'); if (cval) *cval = 0; cval = (u8*) strchr((char*)val, '='); if (cval) { *cval = 0; cval++; } /* If proper value not found, use NULL name and put whatever was found in the value field. */ if (!cval) { cval = val; val = 0; } if (cval) SET_CK(val, cval, &res->hdr); if (val) { /* New or drastically changed cookies are noteworthy. */ orig_val = GET_CK(val, &global_http_par); if (!orig_val || (strlen((char*)orig_val) != strlen((char*)cval) && strncmp((char*)cval, (char*)orig_val, 3))) { res->cookies_set = 1; problem(PROB_NEW_COOKIE, req, res, val, req->pivot, 0); } /* Set cookie globally, but ignore obvious attempts to delete existing ones. */ if (!ignore_cookies && val && cval[0]) SET_CK(val, cval, &global_http_par); } } /* Content-Type is worth mining for MIME, charset data at this point. */ if (!strcasecmp((char*)cur_line, "Content-Type")) { if (res->header_mime) { /* Duplicate Content-Type. Fetch previous value, if different, complain. */ u8* tmp = GET_HDR((u8*)"Content-Type", &res->hdr); if (strcasecmp((char*)tmp, (char*)val)) res->warn |= WARN_CFL_HDR; } else { u8 *tmp = (u8*)strchr((char*)val, ';'), *cset; if (tmp) { *tmp = 0; if ((cset = (u8*)strchr((char*)tmp + 1, '='))) res->header_charset = ck_strdup(cset + 1); } res->header_mime = ck_strdup(val); if (tmp) *tmp = ';'; } } } /* At the beginning of the payload again! */ if (!chunked) { /* Identity. Ignore actual C-L data, use just as much as we collected. */ res->pay_len = data_len - cur_data_off; res->payload = ck_alloc(res->pay_len + 1); res->payload[res->pay_len] = 0; /* NUL-terminate for safer parsing. */ memcpy(res->payload, data + cur_data_off, res->pay_len); } else { u32 chunk_off = 0; /* Chunked - we should have the authoritative length of chunk contents in total_chunk already, and the overall structure validated, so let's just reparse quickly. */ res->pay_len = total_chunk; res->payload = ck_alloc(total_chunk + 1); res->payload[res->pay_len] = 0; while (1) { u32 chunk_len; NEXT_LINE(); if (!cur_line || sscanf((char*)cur_line, "%x", &chunk_len) != 1) break; if (cur_data_off + chunk_len > data_len) chunk_len = data_len - cur_data_off; memcpy(res->payload + chunk_off, data + cur_data_off, chunk_len); chunk_off += chunk_len; cur_data_off += chunk_len; NEXT_LINE(); if (!chunk_len) break; } } ck_free(cur_line); if (compressed) { u8* tmp_buf; /* Deflate or gzip - zlib can handle both the same way. We lazily allocate a size_limit output buffer, then truncate it if necessary. */ z_stream d; s32 err; tmp_buf = ck_alloc(size_limit + 1); d.zalloc = 0; d.zfree = 0; d.opaque = 0; d.next_in = res->payload; d.avail_in = res->pay_len; d.next_out = tmp_buf; d.avail_out = size_limit; /* Say hello to third-party vulnerabilities! */ if (inflateInit2(&d, 32 + 15) != Z_OK) { inflateEnd(&d); ck_free(tmp_buf); return 2; } err = inflate(&d, Z_FINISH); inflateEnd(&d); if (err != Z_BUF_ERROR && err != Z_OK && err != Z_STREAM_END) { ck_free(tmp_buf); return 2; } ck_free(res->payload); bytes_deflated += res->pay_len; res->pay_len = size_limit - d.avail_out; res->payload = ck_realloc(tmp_buf, res->pay_len + 1); res->payload[res->pay_len] = 0; bytes_inflated += res->pay_len; } #undef NEXT_LINE fprint_response(res); return must_close ? 3 : 0; } /* Performs a deep free() of struct http_request */ void destroy_request(struct http_request* req) { u32 i; for (i=0;ipar.c;i++) { ck_free(req->par.n[i]); ck_free(req->par.v[i]); } ck_free(req->par.t); ck_free(req->par.n); ck_free(req->par.v); ck_free(req->method); ck_free(req->host); ck_free(req->orig_url); if (req->flushed && req->flush_dir) ck_free(req->flush_dir); ck_free(req); } /* Performs a deep free() of struct http_response */ void destroy_response(struct http_response* res) { u32 i; for (i=0;ihdr.c;i++) { ck_free(res->hdr.n[i]); ck_free(res->hdr.v[i]); } ck_free(res->hdr.t); ck_free(res->hdr.n); ck_free(res->hdr.v); ck_free(res->meta_charset); ck_free(res->header_charset); ck_free(res->header_mime); ck_free(res->msg); /* Payload might have been flushed */ if (res->payload) ck_free(res->payload); if (res->flushed && res->flush_dir) ck_free(res->flush_dir); ck_free(res); } /* Performs a deep free(), unlinking of struct queue_entry, and the underlying request / response pair. */ static void destroy_unlink_queue(struct queue_entry* q, u8 keep) { if (!keep) { if (q->req) destroy_request(q->req); if (q->res) destroy_response(q->res); } if (!q->prev) queue = q->next; else q->prev->next = q->next; #ifdef QUEUE_FILO if (!q->next) q_tail = q->prev; #endif /* QUEUE_FILO */ if (q->next) q->next->prev = q->prev; ck_free(q); queue_cur--; } /* Performs a deep free(), unlinking, network shutdown for struct conn_entry, as well as the underlying queue entry, request and response structs. */ static void destroy_unlink_conn(struct conn_entry* c, u8 keep) { if (c->q) destroy_unlink_queue(c->q, keep); if (!c->prev) conn = c->next; else c->prev->next = c->next; if (c->next) c->next->prev = c->prev; if (c->srv_ssl) SSL_free(c->srv_ssl); if (c->srv_ctx) SSL_CTX_free(c->srv_ctx); ck_free(c->write_buf); ck_free(c->read_buf); close(c->fd); ck_free(c); conn_cur--; } /* Performs struct conn_entry for reuse following a clean shutdown. */ static void reuse_conn(struct conn_entry* c, u8 keep) { if (c->q) destroy_unlink_queue(c->q, keep); c->q = 0; ck_free(c->read_buf); ck_free(c->write_buf); c->read_buf = c->write_buf = NULL; c->read_len = c->write_len = c->write_off = 0; c->SSL_rd_w_wr = c->SSL_wr_w_rd = 0; } /* Schedules a new asynchronous request (does not make a copy of the original http_request struct, may deallocate it immediately or later on); req->callback() will be invoked when the request is completed (or fails - maybe right away). */ void async_request(struct http_request* req) { struct queue_entry *qe; struct http_response *res; if (req->proto == PROTO_NONE || !req->callback) FATAL("uninitialized http_request"); res = ck_alloc(sizeof(struct http_response)); req->addr = maybe_lookup_host(req->host); /* Don't try to issue extra requests if max_fail consecutive failures exceeded; but still try to wrap up the (partial) scan. */ if (req_errors_cur > max_fail) { DEBUG("!!! Too many subsequent request failures!\n"); res->state = STATE_SUPPRESS; if (!req->callback(req, res)) { destroy_request(req); destroy_response(res); } req_dropped++; return; } /* DNS errors mean instant fail. */ if (!req->addr) { DEBUG("!!! DNS error!\n"); res->state = STATE_DNSERR; if (!req->callback(req, res)) { destroy_request(req); destroy_response(res); } req_errors_net++; conn_count++; conn_failed++; return; } /* Enforce user limits. */ if (req_count > max_requests) { DEBUG("!!! Total request limit exceeded!\n"); res->state = STATE_SUPPRESS; if (!req->callback(req, res)) { destroy_request(req); destroy_response(res); } req_dropped++; return; } /* OK, looks like we're good to go. Insert the request into the the queue. */ #ifdef QUEUE_FILO qe = q_tail; q_tail = ck_alloc(sizeof(struct queue_entry)); q_tail->req = req; q_tail->res = res; q_tail->prev = qe; if (q_tail->prev) q_tail->prev->next = q_tail; if (!queue) queue = q_tail; #else qe = queue; queue = ck_alloc(sizeof(struct queue_entry)); queue->req = req; queue->res = res; queue->next = qe; if (queue->next) queue->next->prev = queue; #endif /* ^QUEUE_FILO */ queue_cur++; req_count++; } /* A helper function to compare the CN / altname with our host name */ static u8 match_cert_name(char* req_host, char* host) { if (!host) return 0; /* For matching, we update our pointer from *.example.org to .example.org */ if (host[0] == '*' && host[1] == '.') { host++; if (strlen(req_host) > strlen(host)) { /* The cert name is a wild card which counts for the first level * subdomain. We for comparison, strip the first section: * * foo.bar.example.org must not match .example.org * bar.example.org must match .example.org * * */ while(req_host && req_host[0] != '.') req_host++; } } if (host) DEBUG("Comparing: %s %s\n", host, req_host); if (!host || strcasecmp(host, req_host)) return 0; return 1; } /* Check SSL properties, raise security alerts if necessary. We do not perform a very thorough validation - we do not check for valid root CAs, bad ciphers, SSLv2 support, etc - as these are covered well by network-level security assessment tools anyway. We might eventually want to check aliases or support TLS SNI. */ static void check_ssl(struct conn_entry* c) { X509 *p; const SSL_CIPHER *cp; /* Test if a weak cipher has been negotiated */ cp = SSL_get_current_cipher(c->srv_ssl); if(!(cp->algo_strength & SSL_MEDIUM) && !(cp->algo_strength & SSL_HIGH)) problem(PROB_SSL_WEAK_CIPHER, c->q->req, 0, (u8*)SSL_CIPHER_get_name(cp),host_pivot(c->q->req->pivot), 0); p = SSL_get_peer_certificate(c->srv_ssl); if (p) { u32 cur_time = time(0); u32 i, acnt; char *issuer, *host, *req_host; STACK_OF(GENERAL_NAME) *altnames; char *buf = 0; u8 found = 0; /* Check for certificate expiration... */ if (ASN1_UTCTIME_cmp_time_t(p->cert_info->validity->notBefore, cur_time) != -1 || ASN1_UTCTIME_cmp_time_t(p->cert_info->validity->notAfter, cur_time) != 1) problem(PROB_SSL_CERT_DATE, c->q->req, 0, 0, host_pivot(c->q->req->pivot), 0); /* Check for self-signed certs or no issuer data. */ issuer = X509_NAME_oneline(p->cert_info->issuer,NULL,0); if (!issuer || !p->name || !strcmp(issuer, p->name)) problem(PROB_SSL_SELF_CERT, c->q->req, 0, (u8*)issuer, host_pivot(c->q->req->pivot), 0); else problem(PROB_SSL_CERT, c->q->req, 0, (u8*)issuer, host_pivot(c->q->req->pivot), 0); free(issuer); /* Extract CN= from certificate name, compare to destination host. If it doesn't match, step 2 is to look for alternate names and compare those to the hostname */ host = strrchr(p->name, '='); if (host) host++; /* Strip the = */ req_host = (char*)c->q->req->host; /* Step 1: compare the common name value */ found = match_cert_name(req_host, host); /* Step 2: compare the alternate names */ if (!found) { altnames = X509_get_ext_d2i(p, NID_subject_alt_name, NULL, NULL); if (altnames) { acnt = sk_GENERAL_NAME_num(altnames); DEBUG("*-- Certificate has %d altnames\n", acnt); for (i=0; !found && itype != GEN_DNS) continue; buf = (char*)ASN1_STRING_data(name->d.dNSName); /* No string, no match */ if (!buf) continue; /* Not falling for the \0 trick so we only compare when the length matches with the string */ if (strlen(buf) != ASN1_STRING_length(name->d.dNSName)) { problem(PROB_SSL_HOST_LEN, c->q->req, 0, (u8*)host, host_pivot(c->q->req->pivot), 0); } else { found = match_cert_name(req_host, buf); } } GENERAL_NAMES_free(altnames); } } if (!found) problem(PROB_SSL_BAD_HOST, c->q->req, 0, (u8*)host, host_pivot(c->q->req->pivot), 0); X509_free(p); } else problem(PROB_SSL_NO_CERT, c->q->req, 0, 0, host_pivot(c->q->req->pivot), 0); c->ssl_checked = 1; } /* Associates a queue entry with an existing connection (if 'use_c' is non-NULL), or creates a new connection to host (if 'use_c' NULL). */ static void conn_associate(struct conn_entry* use_c, struct queue_entry* q) { struct conn_entry* c; if (use_c) { c = use_c; c->reused = 1; } else { struct sockaddr_in sin; /* OK, we need to create a new connection list entry and connect it to a target host. */ c = ck_alloc(sizeof(struct conn_entry)); conn_count++; c->proto = q->req->proto; c->addr = q->req->addr; c->port = q->req->port; c->fd = socket(PF_INET, SOCK_STREAM, 0); if (c->fd < 0) { connect_error: if (c->fd >=0) close(c->fd); q->res->state = STATE_LOCALERR; destroy_unlink_queue(q, q->req->callback(q->req, q->res)); req_errors_net++; req_errors_cur++; ck_free(c); conn_failed++; return; } sin.sin_family = PF_INET; #ifdef PROXY_SUPPORT sin.sin_port = htons(use_proxy ? use_proxy_port : c->port); #else sin.sin_port = htons(c->port); #endif /* ^PROXY_SUPPORT */ memcpy(&sin.sin_addr, &q->req->addr, 4); fcntl(c->fd, F_SETFL, O_NONBLOCK); if (connect(c->fd, (struct sockaddr*) &sin, sizeof(struct sockaddr_in)) && (errno != EINPROGRESS)) goto connect_error; /* HTTPS also requires SSL state to be initialized at this point. */ if (c->proto == PROTO_HTTPS) { c->srv_ctx = SSL_CTX_new(SSLv23_client_method()); if (!c->srv_ctx) goto connect_error; SSL_CTX_set_mode(c->srv_ctx, SSL_MODE_ENABLE_PARTIAL_WRITE | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER); c->srv_ssl = SSL_new(c->srv_ctx); if (!c->srv_ssl) { SSL_CTX_free(c->srv_ctx); goto connect_error; } SSL_set_fd(c->srv_ssl, c->fd); SSL_set_connect_state(c->srv_ssl); } /* Make it official. */ c->next = conn; conn = c; if (c->next) c->next->prev = c; conn_cur++; } c->q = q; q->c = c; q->res->state = STATE_CONNECT; c->req_start = c->last_rw = time(0); c->write_buf = build_request_data(q->req); c->write_len = strlen((char*)c->write_buf); /* Time the request */ q->req->start_time = c->req_start; } /* Processes the queue. Returns the number of queue entries remaining, 0 if none. Will do a blocking select() to wait for socket state changes (or timeouts) if no data available to process. This is the main routine for the scanning loop. */ u32 next_from_queue(void) { u32 cur_time = time(0); if (conn_cur) { static struct pollfd* p; struct conn_entry* c = conn; u32 i = 0; /* First, go through all connections, handle connects, SSL handshakes, data reads and writes, and exceptions. */ if (!p) p = __DFL_ck_alloc(sizeof(struct pollfd) * max_connections); while (c) { p[i].fd = c->fd; p[i].events = POLLIN | POLLERR | POLLHUP; if (c->write_len - c->write_off || c->SSL_rd_w_wr) p[i].events |= POLLOUT; p[i].revents = 0; c = c->next; i++; } poll(p, conn_cur, 100); c = conn; for (i=0;inext; /* Connection closed: see if we have any pending data to write. If yes, fail. If not, try parse_response() to see if we have all the data. Clean up. */ if (p[i].revents & (POLLERR|POLLHUP)) { u8 keep; network_error: keep = 0; /* Retry requests that were sent on old keep-alive connections and failed instantly with no data read; might be just that the server got bored. */ if (c->q && !c->q->retrying && c->reused && !c->read_len) { c->q->res->state = STATE_NOTINIT; c->q->retrying = 1; c->q->c = 0; c->q = 0; req_retried++; } else if (c->q) { if (c->write_len - c->write_off || !c->read_len) { c->q->res->state = STATE_CONNERR; keep = c->q->req->callback(c->q->req, c->q->res); req_errors_net++; req_errors_cur++; } else { if (parse_response(c->q->req, c->q->res, c->read_buf, c->read_len, 0) != 2) { c->q->res->state = STATE_OK; keep = c->q->req->callback(c->q->req, c->q->res); if (req_errors_cur <= max_fail) req_errors_cur = 0; } else { c->q->res->state = STATE_CONNERR; keep = c->q->req->callback(c->q->req, c->q->res); req_errors_net++; req_errors_cur++; } } } destroy_unlink_conn(c, keep); } else /* Incoming data (when SSL_write() did not request a read) or continuation of SSL_read() possible (if SSL_read() wanted to write). Process data, call parse_response() to see if w have all we wanted. Update event timers. */ if (((p[i].revents & POLLIN) && !c->SSL_wr_w_rd) || ((p[i].revents & POLLOUT) && c->SSL_rd_w_wr)) { if (c->q) { s32 read_res; u8 p_ret; SSL_read_more: c->read_buf = ck_realloc(c->read_buf, c->read_len + READ_CHUNK + 1); if (c->proto == PROTO_HTTPS) { s32 ssl_err; c->SSL_rd_w_wr = 0; read_res = SSL_read(c->srv_ssl, c->read_buf + c->read_len, READ_CHUNK); if (!read_res) goto network_error; if (read_res < 0) { ssl_err = SSL_get_error(c->srv_ssl, read_res); if (ssl_err == SSL_ERROR_WANT_WRITE) c->SSL_rd_w_wr = 1; else if (ssl_err != SSL_ERROR_WANT_READ) goto network_error; read_res = 0; } } else { read_res = read(c->fd, c->read_buf + c->read_len, READ_CHUNK); if (read_res <= 0) goto network_error; } bytes_recv += read_res; c->read_len += read_res; c->read_buf = ck_realloc(c->read_buf, c->read_len + 1); /* Retry reading until SSL_ERROR_WANT_READ. */ if (c->proto == PROTO_HTTPS && read_res && c->read_len < size_limit) goto SSL_read_more; c->read_buf[c->read_len] = 0; /* NUL-terminate for sanity. */ /* We force final parse_response() if response length exceeded size_limit by more than 4 kB. The assumption here is that it is less expensive to redo the connection than it is to continue receiving an unknown amount of extra data. */ p_ret = parse_response(c->q->req, c->q->res, c->read_buf, c->read_len, (c->read_len > (size_limit + READ_CHUNK)) ? 0 : 1); c->q->req->end_time = time(0); if (!p_ret || p_ret == 3) { u8 keep; c->q->res->state = STATE_OK; keep = c->q->req->callback(c->q->req, c->q->res); /* If we got all data without hitting the limit, and if "Connection: close" is not indicated, we might want to keep the connection for future use. */ if (c->read_len > (size_limit + READ_CHUNK) || p_ret) destroy_unlink_conn(c, keep); else reuse_conn(c, keep); if (req_errors_cur <= max_fail) req_errors_cur = 0; } else if (p_ret == 2) { c->q->res->state = STATE_RESPERR; destroy_unlink_conn(c, c->q->req->callback(c->q->req, c->q->res)); req_errors_http++; req_errors_cur++; } else { c->last_rw = cur_time; c->q->res->state = STATE_RECEIVE; } } else destroy_unlink_conn(c, 0); /* Unsolicited response! */ } else /* Write possible (if SSL_read() did not request a write), or continuation of SSL_write() possible (if SSL_write() wanted to read). Send data, update timers, etc. */ if (((p[i].revents & POLLOUT) && !c->SSL_rd_w_wr) || ((p[i].revents & POLLIN) && c->SSL_wr_w_rd)) { if (c->write_len - c->write_off) { s32 write_res; if (c->proto == PROTO_HTTPS) { s32 ssl_err; c->SSL_wr_w_rd = 0; write_res = SSL_write(c->srv_ssl, c->write_buf + c->write_off, c->write_len - c->write_off); if (!write_res) goto network_error; if (write_res < 0) { ssl_err = SSL_get_error(c->srv_ssl, write_res); if (ssl_err == SSL_ERROR_WANT_READ) c->SSL_wr_w_rd = 1; else if (ssl_err != SSL_ERROR_WANT_WRITE) goto network_error; write_res = 0; } else if (!c->ssl_checked) check_ssl(c); } else { write_res = write(c->fd, c->write_buf + c->write_off, c->write_len - c->write_off); if (write_res <= 0) goto network_error; } bytes_sent += write_res; c->write_off += write_res; c->q->res->state = STATE_SEND; c->last_rw = cur_time; } } else /* Nothing happened. Check timeouts, kill stale connections. Active (c->q) connections get checked for total and last I/O timeouts. Non-active connections must just not exceed idle_tmout. */ if (!p[i].revents) { u8 keep = 0; if ((c->q && (cur_time - c->last_rw > rw_tmout || cur_time - c->req_start > resp_tmout)) || (!c->q && (cur_time - c->last_rw > idle_tmout)) || (!c->q && tear_down_idle)) { if (c->q) { c->q->res->state = STATE_CONNERR; keep = c->q->req->callback(c->q->req, c->q->res); req_errors_net++; req_errors_cur++; conn_busy_tmout++; } else { conn_idle_tmout++; tear_down_idle = 0; } destroy_unlink_conn(c, keep); } } c = next; } } /* OK, connection-handling affairs taken care of! Next, let's go through all queue entries NOT currently associated with a connection, and try to pair them up with something. */ if (queue_cur) { struct queue_entry *q = queue; while (q) { u32 to_host = 0; /* enforce the max requests per seconds requirement */ if (max_requests_sec && req_sec > max_requests_sec) { u32 diff = req_sec - max_requests_sec; if ((iterations_cnt++)%(diff + 1) != 0) { idle = 1; return queue_cur; } } idle = 0; struct queue_entry* next = q->next; if (!q->c) { struct conn_entry* c = conn; /* Let's try to find a matching, idle connection first. */ while (c) { struct conn_entry* cnext = c->next; if (c->addr == q->req->addr && (++to_host) && c->port == q->req->port && c->proto == q->req->proto && !c->q) { conn_associate(c, q); goto next_q_entry; } c = cnext; } /* No match. If we are out of slots, request some other idle connection to be nuked soon. */ if (to_host < max_conn_host && conn_cur < max_connections) { conn_associate(0, q); goto next_q_entry; } else tear_down_idle = 1; } next_q_entry: q = next; } } return queue_cur; } /* Helper function for request / response dumpers: */ static void dump_params(struct param_array* par) { u32 i; for (i=0;ic;i++) { switch (par->t[i]) { case PARAM_NONE: SAY(" <<<<"); break; case PARAM_PATH: SAY(" PATH"); break; case PARAM_PATH_S: SAY(" PT_S"); break; case PARAM_PATH_C: SAY(" PT_C"); break; case PARAM_PATH_E: SAY(" PT_E"); break; case PARAM_PATH_D: SAY(" PT_D"); break; case PARAM_QUERY: SAY(" QUER"); break; case PARAM_QUERY_S: SAY(" QR_S"); break; case PARAM_QUERY_C: SAY(" QR_C"); break; case PARAM_QUERY_E: SAY(" QR_E"); break; case PARAM_QUERY_D: SAY(" QR_D"); break; case PARAM_POST: SAY(" POST"); break; case PARAM_POST_F: SAY(" FILE"); break; case PARAM_POST_O: SAY(" OPAQ"); break; case PARAM_HEADER: SAY(" head"); break; case PARAM_COOKIE: SAY(" cook"); break; default: SAY(" ????"); } SAY(":%-20s = '%s'\n", par->n[i] ? par->n[i] : (u8*)"-", par->v[i] ? par->v[i] : (u8*)"-"); } } /* Creates a working copy of a request. If all is 0, does not copy path, query parameters, or POST data (but still copies headers). */ struct http_request* req_copy(struct http_request* req, struct pivot_desc* pv, u8 all) { struct http_request* ret; u32 i; if (!req) return NULL; ret = ck_alloc(sizeof(struct http_request)); ret->proto = req->proto; if (all) ret->method = ck_strdup(req->method); else ret->method = ck_strdup((u8*)"GET"); ret->host = ck_strdup(req->host); ret->addr = req->addr; ret->port = req->port; ret->pivot = pv; ret->user_val = req->user_val; /* Copy all the requested data. */ for (i=0;ipar.c;i++) if (all || HEADER_SUBTYPE(req->par.t[i])) set_value(req->par.t[i], req->par.n[i], req->par.v[i], -1, &ret->par); memcpy(&ret->same_sig, &req->same_sig, sizeof(struct http_sig)); return ret; } /* Creates a copy of a response. */ struct http_response* res_copy(struct http_response* res) { struct http_response* ret; u32 i; if (!res) return NULL; ret = ck_alloc(sizeof(struct http_response)); ret->state = res->state; ret->code = res->code; ret->msg = res->msg ? ck_strdup(res->msg) : NULL; ret->warn = res->warn; for (i=0;ihdr.c;i++) set_value(res->hdr.t[i], res->hdr.n[i], res->hdr.v[i], -1, &ret->hdr); ret->pay_len = res->pay_len; if (res->pay_len) { if (res->flushed && res->flush_dir) { ret->flushed = 1; ret->flush_dir = ck_strdup(res->flush_dir); } else { ret->payload = ck_alloc(res->pay_len); memcpy(ret->payload, res->payload, res->pay_len); } } memcpy(&ret->sig, &res->sig, sizeof(struct http_sig)); ret->sniff_mime_id = res->sniff_mime_id; ret->decl_mime_id = res->decl_mime_id; ret->doc_type = res->doc_type; ret->css_type = res->css_type; ret->js_type = res->js_type; ret->json_safe = res->json_safe; ret->stuff_checked = res->stuff_checked; ret->scraped = res->scraped; if (res->meta_charset) ret->meta_charset = ck_strdup(res->meta_charset); if (res->header_charset) ret->header_charset = ck_strdup(res->header_charset); if (res->header_mime) ret->header_mime = ck_strdup(res->header_mime); ret->sniffed_mime = res->sniffed_mime; return ret; } /* Dumps HTTP request data, for diagnostic purposes: */ void dump_http_request(struct http_request* r) { u8 *new_url, *tmp; SAY("\n== HTTP REQUEST %p ==\n\nBasic values:\n", r); SAY(" Proto = %u\n", r->proto); SAY(" Method = %s\n", r->method ? r->method : (u8*)"(GET)"); SAY(" Host = %s\n", r->host); SAY(" Addr = %u.%u.%u.%u\n", ((u8*)&r->addr)[0], ((u8*)&r->addr)[1], ((u8*)&r->addr)[2], ((u8*)&r->addr)[3]); SAY(" Port = %d\n", r->port); SAY(" Xrefs = pivot %p, handler %p, user %d\n", r->pivot, r->callback, r->user_val); new_url = serialize_path(r, 1, 0); SAY("\nURLs:\n Original = %s\n" " Synthetic = %s\n", r->orig_url ? r->orig_url : (u8*)"[none]", new_url); ck_free(new_url); SAY("\nParameter array:\n"); dump_params(&r->par); SAY("\nRaw request data:\n\n"); tmp = build_request_data(r); SAY("%s\n",tmp); ck_free(tmp); SAY("\n== END OF REQUEST ==\n"); } /* Dumps HTTP response data, likewise: */ void dump_http_response(struct http_response* r) { SAY("\n== HTTP RESPONSE %p ==\n\nBasic values:\n", r); SAY(" State = %u\n", r->state); SAY(" Response = %u ('%s')\n", r->code, r->msg); SAY(" Flags = %08x\n", r->warn); SAY(" Data len = %u\n", r->pay_len); SAY("\nParameter array:\n"); dump_params(&r->hdr); if (r->payload) SAY("\nPayload data (%u):\n\n%s\n", r->pay_len, r->payload); SAY("\n== END OF RESPONSE ==\n"); } /* Destroys http state information, for memory profiling. */ void destroy_http() { u32 i; struct dns_entry* cur; for (i=0;inext; ck_free(cur->name); ck_free(cur); cur = next; } } /* Shows some pretty statistics. */ void http_stats(u64 st_time) { u64 en_time; struct timeval tv; gettimeofday(&tv, NULL); en_time = tv.tv_sec * 1000LL + tv.tv_usec / 1000; SAY(cLBL "Scan statistics:\n\n" cGRA " Scan time : " cNOR "%u:%02u:%02u.%03u\n" cGRA " HTTP requests : " cNOR "%u (%.01f/s), %llu kB in, " "%llu kB out (%.01f kB/s) \n" cGRA " Compression : " cNOR "%llu kB in, %llu kB out " "(%.01f%% gain) \n" cGRA " HTTP faults : " cNOR "%u net errors, %u proto errors, " "%u retried, %u drops\n" cGRA " TCP handshakes : " cNOR "%u total (%.01f req/conn) \n" cGRA " TCP faults : " cNOR "%u failures, %u timeouts, %u purged\n" cGRA " External links : " cNOR "%u skipped\n" cGRA " Reqs pending : " cNOR "%u \n", /* hrs */ (u32)((en_time - st_time) / 1000 / 60 / 60), /* min */ (u32)((en_time - st_time) / 1000 / 60) % 60, /* sec */ (u32)((en_time - st_time) / 1000) % 60, /* ms */ (u32)((en_time - st_time) % 1000), req_count - queue_cur, (float) (req_count - queue_cur / 1.15) * 1000 / (en_time - st_time + 1), (unsigned long long int) bytes_recv / 1024, (unsigned long long int) bytes_sent / 1024, (float) (bytes_recv + bytes_sent) / 1.024 / (en_time - st_time + 1), (unsigned long long int) bytes_deflated / 1024, (unsigned long long int) bytes_inflated / 1024, ((float) bytes_inflated - bytes_deflated) / (bytes_inflated + bytes_deflated + 1) * 100, req_errors_net, req_errors_http, req_retried, req_dropped, conn_count, (float) req_count / conn_count, conn_failed, conn_busy_tmout, conn_idle_tmout, url_scope, queue_cur); } /* Show currently handled requests. */ #define SP70 \ " " void http_req_list(void) { u32 i; struct conn_entry* c = conn; SAY(cLBL "In-flight requests (max 15 shown):\n\n"); for (i=0;i<15;i++) { SAY(" " cGRA "[" cBLU "%02d" cGRA "] " cBRI, i + 1); if (c && c->q) { u8* p = serialize_path(c->q->req, 1, 0); u32 l = strlen((char*)p); if (l > 70) { SAY("%.30s" cGRA "..." cBRI "%.37s\n", p, p + l - 37); } else { SAY("%s%s\n", p, SP70 + l); } ck_free(p); } else SAY(cLGN "%s\n", SP70 + 11); if (c) c = c->next; } } skipfish-2.10b/src/skipfish.c0000440036502000116100000006047612057400054015153 0ustar heinenneng/* skipfish - main entry point --------------------------- Author: Michal Zalewski Copyright 2009, 2010, 2011 by Google Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "types.h" #include "alloc-inl.h" #include "string-inl.h" #include "crawler.h" #include "checks.h" #include "analysis.h" #include "database.h" #include "http_client.h" #include "report.h" #include "options.h" #include "signatures.h" #include "auth.h" #ifdef DEBUG_ALLOCATOR struct TRK_obj* TRK[ALLOC_BUCKETS]; u32 TRK_cnt[ALLOC_BUCKETS]; #endif /* DEBUG_ALLOCATOR */ /* Ctrl-C handler... */ static u8 stop_soon, clear_screen; static void ctrlc_handler(int sig) { stop_soon = 1; } /* Screen resizing handler. */ static void resize_handler(int sig) { clear_screen = 1; } /* Usage info. */ static void usage(char* argv0) { SAY("Usage: %s [ options ... ] -W wordlist -o output_dir start_url [ start_url2 ... ]\n\n" "Authentication and access options:\n\n" " -A user:pass - use specified HTTP authentication credentials\n" " -F host=IP - pretend that 'host' resolves to 'IP'\n" " -C name=val - append a custom cookie to all requests\n" " -H name=val - append a custom HTTP header to all requests\n" " -b (i|f|p) - use headers consistent with MSIE / Firefox / iPhone\n" #ifdef PROXY_SUPPORT " -J proxy - use a specified HTTP proxy server\n" #endif /* PROXY_SUPPORT */ " -N - do not accept any new cookies\n" " --auth-form url - form authentication URL\n" " --auth-user user - form authentication user\n" " --auth-pass pass - form authentication password\n" " --auth-verify-url - URL for in-session detection\n\n" "Crawl scope options:\n\n" " -d max_depth - maximum crawl tree depth (%u)\n" " -c max_child - maximum children to index per node (%u)\n" " -x max_desc - maximum descendants to index per branch (%u)\n" " -r r_limit - max total number of requests to send (%u)\n" " -p crawl%% - node and link crawl probability (100%%)\n" " -q hex - repeat probabilistic scan with given seed\n" " -I string - only follow URLs matching 'string'\n" " -X string - exclude URLs matching 'string'\n" " -K string - do not fuzz parameters named 'string'\n" " -D domain - crawl cross-site links to another domain\n" " -B domain - trust, but do not crawl, another domain\n" " -Z - do not descend into 5xx locations\n" " -O - do not submit any forms\n" " -P - do not parse HTML, etc, to find new links\n\n" "Reporting options:\n\n" " -o dir - write output to specified directory (required)\n" " -M - log warnings about mixed content / non-SSL passwords\n" " -E - log all HTTP/1.0 / HTTP/1.1 caching intent mismatches\n" " -U - log all external URLs and e-mails seen\n" " -Q - completely suppress duplicate nodes in reports\n" " -u - be quiet, disable realtime progress stats\n" " -v - enable runtime logging (to stderr)\n\n" "Dictionary management options:\n\n" " -W wordlist - use a specified read-write wordlist (required)\n" " -S wordlist - load a supplemental read-only wordlist\n" " -L - do not auto-learn new keywords for the site\n" " -Y - do not fuzz extensions in directory brute-force\n" " -R age - purge words hit more than 'age' scans ago\n" " -T name=val - add new form auto-fill rule\n" " -G max_guess - maximum number of keyword guesses to keep (%d)\n\n" " -z sigfile - load signatures from this file\n\n" "Performance settings:\n\n" " -g max_conn - max simultaneous TCP connections, global (%u)\n" " -m host_conn - max simultaneous connections, per target IP (%u)\n" " -f max_fail - max number of consecutive HTTP errors (%u)\n" " -t req_tmout - total request response timeout (%u s)\n" " -w rw_tmout - individual network I/O timeout (%u s)\n" " -i idle_tmout - timeout on idle HTTP connections (%u s)\n" " -s s_limit - response size limit (%u B)\n" " -e - do not keep binary responses for reporting\n\n" "Other settings:\n\n" " -l max_req - max requests per second (%f)\n" " -k duration - stop scanning after the given duration h:m:s\n" " --config file - load the specified configuration file\n\n" "Send comments and complaints to .\n", argv0, max_depth, max_children, max_descendants, max_requests, MAX_GUESSES, max_connections, max_conn_host, max_fail, resp_tmout, rw_tmout, idle_tmout, size_limit, max_requests_sec); exit(1); } /* Welcome screen. */ #ifdef SHOW_SPLASH void splash_screen(void) { char keybuf[8]; u32 time_cnt = 0; SAY("\x1b[H\x1b[J"); SAY(cBRI "Welcome to " cYEL "skipfish" cBRI ". Here are some useful tips:\n\n" "1) To abort the scan at any time, press " cCYA "Ctrl-C" cBRI ". A partial report will be written\n" " to the specified location. To view a list of currently scanned URLs, you can\n" " press " cCYA "space" cBRI " at any time during the scan.\n\n" "2) Watch the number requests per second shown on the main screen. If this figure\n" " drops below 100-200, the scan will likely take a very long time.\n\n" "3) The scanner does not auto-limit the scope of the scan; on complex sites, you\n" " may need to specify locations to exclude, or limit brute-force steps.\n\n" "4) There are several new releases of the scanner every month. If you run into\n" " trouble, check for a newer version first, let the author know next.\n\n" "More info: " cYEL "http://code.google.com/p/skipfish/wiki/KnownIssues\n\n" cBRI); if (!no_fuzz_ext && (keyword_orig_cnt * wg_extension_cnt) > 1000) { SAY(cLRD "NOTE: The scanner is currently configured for directory brute-force attacks,\n" "and will make about " cBRI "%u" cLRD " requests per every fuzzable location. If this is\n" "not what you wanted, stop now and consult the documentation.\n\n", keyword_orig_cnt * wg_extension_cnt); } SAY(cLBL "Press any key to continue (or wait 60 seconds)... "); while (!stop_soon && fread(keybuf, 1, sizeof(keybuf), stdin) == 0 && time_cnt++ < 600) usleep(100000); } #endif /* SHOW_SPLASH */ /* Load URLs from file. */ static void read_urls(u8* fn) { FILE* f = fopen((char*)fn, "r"); u8 tmp[MAX_URL_LEN]; u32 loaded = 0; if (!f) FATAL("Unable to open '%s'.", fn); while (fgets((char*)tmp, MAX_URL_LEN, f)) { struct http_request *req; u8* url = tmp; u8* ptr; u32 l; while (isspace(*url)) url++; /* Check if we're reading a pivots.txt file and grab the URL */ if(!strncmp((char*)url,"GET ", 4) || !strncmp((char*)url,"POST ", 4)) { url += 4; /* If the server response code is a 404, than we'll skip the URL to avoid wasting time on things that are likely not to exist. */ if(inl_findstr(url, (u8*)"code=404", MAX_URL_LEN)) continue; ptr = url; /* Find the next space to terminate the URL */ while(ptr && !isspace(*ptr)) ptr++; if(!ptr) FATAL("URL parsing failed at line: %s", tmp); *ptr = 0; /* Else we expect a simple flat file with URLs */ } else { l = strlen((char*)url); while (l && isspace(url[l-1])) l--; url[l] = 0; } if (*url == '#' || !*url) continue; req = ck_alloc(sizeof(struct http_request)); if (parse_url(url, req, NULL)) FATAL("Scan target '%s' in file '%s' is not a valid absolute URL.", url, fn); if (!url_allowed_host(req)) APPEND_STRING(allow_domains, num_allow_domains, __DFL_ck_strdup(req->host)); if (!url_allowed(req)) FATAL("URL '%s' in file '%s' explicitly excluded by -I / -X rules.", url, fn); maybe_add_pivot(req, NULL, 2); destroy_request(req); loaded++; } fclose(f); if (!loaded) FATAL("No valid URLs found in '%s'.", fn); } /* Main entry point */ int main(int argc, char** argv) { s32 opt; u32 loop_cnt = 0, purge_age = 0, seed; u8 sig_loaded = 0, show_once = 0, no_statistics = 0, display_mode = 0; s32 oindex = 0; u8 *wordlist = NULL; u8 *sig_list_strg = NULL; u8 *gtimeout_str = NULL; const char *config_file = NULL; u32 gtimeout = 0; #ifdef PROXY_SUPPORT /* A bool to track whether a fake Host header is set which doesn't work with the proxy support. */ u8 has_fake = 0; #endif /* PROXY_SUPPORT */ struct termios term; struct timeval tv; u64 st_time, en_time; signal(SIGINT, ctrlc_handler); signal(SIGWINCH, resize_handler); signal(SIGPIPE, SIG_IGN); SSL_library_init(); /* Come up with a quasi-decent random seed. */ gettimeofday(&tv, NULL); seed = tv.tv_usec ^ (tv.tv_sec << 16) ^ getpid(); SAY("skipfish web application scanner - version " VERSION "\n"); /* We either parse command-line arguments or read them from a config file. First we check if a config file was specified and read it content into the argc and argv pointers */ while ((opt = getopt_long(argc, argv, OPT_STRING, long_options, &oindex)) >= 0 && !config_file) { if (!opt && !strcmp("config", long_options[oindex].name )) config_file = optarg; } /* Reset the index */ oindex = 0; optind = 1; if (config_file) { DEBUG("Reading configuration file: %s\n", config_file); read_config_file(config_file, &argc, &argv); } /* Parse the command-line flags. If a configuration file was specified, the options loaded from it are now present in argv and will therefore be parsed here all together with the CMD options. */ while ((opt = getopt_long(argc, argv, OPT_STRING, long_options, &oindex)) >= 0) switch (opt) { case 'A': { u8* x = (u8*)strchr(optarg, ':'); if (!x) FATAL("Credentials must be in 'user:pass' form."); *(x++) = 0; auth_user = (u8*)optarg; auth_pass = x; auth_type = AUTH_BASIC; break; } #ifdef PROXY_SUPPORT case 'J': { u8* x = (u8*)strchr(optarg, ':'); if (!x) FATAL("Proxy data must be in 'host:port' form."); *(x++) = 0; use_proxy = (u8*)optarg; use_proxy_port = atoi((char*)x); if (!use_proxy_port) FATAL("Incorrect proxy port number."); break; } #endif /* PROXY_SUPPORT */ case 'F': { u8* x = (u8*)strchr(optarg, '='); u32 fake_addr; if (!x) FATAL("Fake mappings must be in 'host=IP' form."); *x = 0; fake_addr = inet_addr((char*)x + 1); if (fake_addr == (u32)-1) FATAL("Could not parse IP address '%s'.", x + 1); fake_host((u8*)optarg, fake_addr); #ifdef PROXY_SUPPORT has_fake = 1; #endif /* PROXY_SUPPORT */ break; } case 'H': { u8* x = (u8*)strchr(optarg, '='); if (!x) FATAL("Extra headers must be in 'name=value' form."); *x = 0; if (!strcasecmp(optarg, "Cookie")) FATAL("Do not use -H to set cookies (try -C instead)."); SET_HDR((u8*)optarg, x + 1, &global_http_par); break; } case 'C': { u8* x = (u8*)strchr(optarg, '='); if (!x) FATAL("Cookies must be in 'name=value' form."); if (strchr(optarg, ';')) FATAL("Split multiple cookies into separate -C options."); *x = 0; SET_CK((u8*)optarg, x + 1, &global_http_par); break; } case 'D': if (*optarg == '*') optarg++; APPEND_STRING(allow_domains, num_allow_domains, optarg); break; case 'K': APPEND_STRING(skip_params, num_skip_params, optarg); break; case 'B': if (*optarg == '*') optarg++; APPEND_STRING(trust_domains, num_trust_domains, optarg); break; case 'I': if (*optarg == '*') optarg++; APPEND_STRING(allow_urls, num_allow_urls, optarg); break; case 'X': if (*optarg == '*') optarg++; APPEND_STRING(deny_urls, num_deny_urls, optarg); break; case 'T': { u8* x = (u8*)strchr(optarg, '='); if (!x) FATAL("Rules must be in 'name=value' form."); *x = 0; add_form_hint((u8*)optarg, x + 1); break; } case 'N': ignore_cookies = 1; break; case 'Y': no_fuzz_ext = 1; break; case 'q': if (sscanf(optarg, "0x%08x", &seed) != 1) FATAL("Invalid seed format."); srandom(seed); break; case 'Q': suppress_dupes = 1; break; case 'P': no_parse = 1; break; case 'M': warn_mixed = 1; break; case 'U': log_ext_urls = 1; break; case 'L': dont_add_words = 1; break; case 'E': pedantic_cache = 1; break; case 'O': no_forms = 1; break; case 'R': purge_age = atoi(optarg); if (purge_age < 3) FATAL("Purge age invalid or too low (min 3)."); break; case 'd': max_depth = atoi(optarg); if (max_depth < 2) FATAL("Invalid value '%s'.", optarg); break; case 'c': max_children = atoi(optarg); if (!max_children) FATAL("Invalid value '%s'.", optarg); break; case 'x': max_descendants = atoi(optarg); if (!max_descendants) FATAL("Invalid value '%s'.", optarg); break; case 'p': crawl_prob = atoi(optarg); if (!crawl_prob) FATAL("Invalid value '%s'.", optarg); break; case 'W': if (wordlist) FATAL("Only one -W parameter permitted (use -S to load supplemental dictionaries)."); if (!strcmp(optarg, "-")) wordlist = (u8*)"/dev/null"; else wordlist = (u8*)optarg; break; case 'S': load_keywords((u8*)optarg, 1, 0); break; case 'z': load_signatures((u8*)optarg); sig_loaded = 1; break; case 'b': if (optarg[0] == 'i') browser_type = BROWSER_MSIE; else if (optarg[0] == 'f') browser_type = BROWSER_FFOX; else if (optarg[0] == 'p') browser_type = BROWSER_PHONE; else usage(argv[0]); break; case 'g': max_connections = atoi(optarg); if (!max_connections) FATAL("Invalid value '%s'.", optarg); break; case 'm': max_conn_host = atoi(optarg); if (!max_conn_host) FATAL("Invalid value '%s'.", optarg); break; case 'G': max_guesses = atoi(optarg); if (!max_guesses) FATAL("Invalid value '%s'.", optarg); break; case 'r': max_requests = atoi(optarg); if (!max_requests) FATAL("Invalid value '%s'.", optarg); break; case 'l': max_requests_sec = atof(optarg); if (!max_requests_sec) FATAL("Invalid value '%s'.", optarg); break; case 'f': max_fail = atoi(optarg); if (!max_fail) FATAL("Invalid value '%s'.", optarg); break; case 't': resp_tmout = atoi(optarg); if (!resp_tmout) FATAL("Invalid value '%s'.", optarg); break; case 'w': rw_tmout = atoi(optarg); if (!rw_tmout) FATAL("Invalid value '%s'.", optarg); break; case 'i': idle_tmout = atoi(optarg); if (!idle_tmout) FATAL("Invalid value '%s'.", optarg); break; case 's': size_limit = atoi(optarg); if (!size_limit) FATAL("Invalid value '%s'.", optarg); break; case 'o': if (output_dir) FATAL("Multiple -o options not allowed."); output_dir = (u8*)optarg; rmdir(optarg); if (mkdir(optarg, 0755)) PFATAL("Unable to create '%s'.", output_dir); break; case 'u': no_statistics = 1; break; case 'v': verbosity++; break; case 'e': delete_bin = 1; break; case 'k': if (gtimeout_str) FATAL("Multiple -k options not allowed."); gtimeout_str = (u8*)optarg; break; case 'Z': no_500_dir = 1; break; case 0: if (!strcmp("checks", long_options[oindex].name )) { display_injection_checks(); } else if (!strcmp("checks-toggle", long_options[oindex].name )) { toggle_injection_checks((u8*)optarg, 1, 1); } else if (!strcmp("no-injection-tests", long_options[oindex].name )) { no_checks = 1; } else if(!strcmp("flush-to-disk", long_options[oindex].name )) { flush_pivot_data = 1; } else if (!strcmp("signatures", long_options[oindex].name )) { load_signatures((u8*)optarg); } else if(!strcmp("fast", long_options[oindex].name )) { toggle_injection_checks((u8*)"2,4,6,15,16,17", 0, 0); } else if (!strcmp("auth-form", long_options[oindex].name )) { auth_form = (u8*)optarg; auth_type = AUTH_FORM; } else if (!strcmp("auth-user", long_options[oindex].name )) { auth_user = (u8*)optarg; } else if (!strcmp("auth-pass", long_options[oindex].name )) { auth_pass = (u8*)optarg; } else if (!strcmp("auth-pass-field", long_options[oindex].name )) { auth_pass_field = (u8*)optarg; } else if (!strcmp("auth-user-field", long_options[oindex].name )) { auth_user_field = (u8*)optarg; } else if (!strcmp("auth-form-target", long_options[oindex].name )) { auth_form_target = (u8*)optarg; } else if (!strcmp("auth-verify-url", long_options[oindex].name )) { auth_verify_url = (u8*)optarg; } break; default: usage(argv[0]); } #ifdef PROXY_SUPPORT if (has_fake && use_proxy) FATAL("-F and -J should not be used together."); #endif /* PROXY_SUPPORT */ if (access(ASSETS_DIR "/index.html", R_OK)) PFATAL("Unable to access '%s/index.html' - wrong directory?", ASSETS_DIR); srandom(seed); if (optind == argc) FATAL("Scan target not specified (try -h for help)."); if (!output_dir) FATAL("Output directory not specified (try -h for help)."); if(verbosity && !no_statistics && isatty(2)) FATAL("Please use -v in combination with the -u flag or, " "run skipfish while redirecting stderr to a file. "); if (resp_tmout < rw_tmout) resp_tmout = rw_tmout; if (max_connections < max_conn_host) max_connections = max_conn_host; /* Parse the timeout string - format h:m:s */ if (gtimeout_str) { int i = 0; int m[3] = { 3600, 60, 1 }; u8* tok = (u8*)strtok((char*)gtimeout_str, ":"); while(tok && i <= 2) { gtimeout += atoi((char*)tok) * m[i]; tok = (u8*)strtok(NULL, ":"); i++; } if(!gtimeout) FATAL("Wrong timeout format, please use h:m:s (hours, minutes, seconds)"); DEBUG("* Scan timeout is set to %d seconds\n", gtimeout); } if (!wordlist) { wordlist = (u8*)"/dev/null"; DEBUG("* No wordlist specified with -W: defaulting to /dev/null\n"); } /* If no signature files have been specified via command-line: load the default file */ if (!sig_loaded) load_signatures((u8*)SIG_FILE); load_keywords(wordlist, 0, purge_age); /* Load the signatures list for the matching */ if (sig_list_strg) load_signatures(sig_list_strg); /* Try to authenticate when the auth_user and auth_pass fields are set. */ if (auth_type == AUTH_FORM) { if (!auth_user || !auth_pass) FATAL("Authentication requires a username and password."); /* Fire off the requests */ authenticate(); while (next_from_queue()) { usleep(1000); } switch (auth_state) { case ASTATE_DONE: DEBUGC(L1, "*- Authentication succeeded!\n"); break; default: DEBUG("Auth state: %d\n", auth_state); FATAL("Authentication failed (use -uv for more info)\n"); break; } DEBUG("Authentication done!\n"); } /* Schedule all URLs in the command line for scanning. */ while (optind < argc) { struct http_request *req; /* Support @ notation for reading URL lists from files. */ if (argv[optind][0] == '@') { read_urls((u8*)argv[optind++] + 1); continue; } req = ck_alloc(sizeof(struct http_request)); if (parse_url((u8*)argv[optind], req, NULL)) FATAL("Scan target '%s' is not a valid absolute URL.", argv[optind]); if (!url_allowed_host(req)) APPEND_STRING(allow_domains, num_allow_domains, __DFL_ck_strdup(req->host)); if (!url_allowed(req)) FATAL("URL '%s' explicitly excluded by -I / -X rules.", argv[optind]); maybe_add_pivot(req, NULL, 2); destroy_request(req); optind++; } /* Char-by char stdin. */ tcgetattr(0, &term); term.c_lflag &= ~ICANON; tcsetattr(0, TCSANOW, &term); fcntl(0, F_SETFL, O_NONBLOCK); gettimeofday(&tv, NULL); st_time = tv.tv_sec * 1000LL + tv.tv_usec / 1000; #ifdef SHOW_SPLASH if (!no_statistics) splash_screen(); #endif /* SHOW_SPLASH */ if (!no_statistics) SAY("\x1b[H\x1b[J"); else SAY(cLGN "[*] " cBRI "Scan in progress, please stay tuned...\n"); u64 refresh_time = 0; /* Enter the crawler loop */ while ((next_from_queue() && !stop_soon) || (!show_once++)) { u8 keybuf[8]; u64 end_time; u64 run_time; struct timeval tv_tmp; gettimeofday(&tv_tmp, NULL); end_time = tv_tmp.tv_sec * 1000LL + tv_tmp.tv_usec / 1000; run_time = end_time - st_time; if (gtimeout > 0 && run_time && run_time/1000 > gtimeout) { DEBUG("* Stopping scan due to timeout\n"); stop_soon = 1; } req_sec = (req_count - queue_cur / 1.15) * 1000 / (run_time + 1); if (no_statistics || ((loop_cnt++ % 100) && !show_once && idle == 0)) continue; if (end_time > refresh_time) { refresh_time = (end_time + 10); if (clear_screen) { SAY("\x1b[H\x1b[2J"); clear_screen = 0; } SAY(cYEL "\x1b[H" "skipfish version " VERSION " by lcamtuf@google.com\n\n" cBRI " -" cPIN " %s " cBRI "-\n\n" cNOR, allow_domains[0]); if (!display_mode) { http_stats(st_time); SAY("\n"); database_stats(); } else { http_req_list(); } SAY(" \r"); } if (fread(keybuf, 1, sizeof(keybuf), stdin) > 0) { display_mode ^= 1; clear_screen = 1; } } gettimeofday(&tv, NULL); en_time = tv.tv_sec * 1000LL + tv.tv_usec / 1000; SAY("\n"); if (stop_soon) SAY(cYEL "[!] " cBRI "Scan aborted by user, bailing out!" cNOR "\n"); term.c_lflag |= ICANON; tcsetattr(0, TCSANOW, &term); fcntl(0, F_SETFL, O_SYNC); save_keywords((u8*)wordlist); write_report(output_dir, en_time - st_time, seed); #ifdef LOG_STDERR SAY("\n== PIVOT DEBUG ==\n"); dump_pivots(0, 0); SAY("\n== END OF DUMP ==\n\n"); #endif /* LOG_STDERR */ SAY(cLGN "[+] " cBRI "This was a great day for science!" cRST "\n\n"); #ifdef DEBUG_ALLOCATOR if (!stop_soon) { destroy_database(); destroy_signature_lists(); destroy_http(); destroy_signatures(); destroy_config(); __TRK_report(); } #endif /* DEBUG_ALLOCATOR */ fflush(0); EVP_cleanup(); CRYPTO_cleanup_all_ex_data(); return 0; } skipfish-2.10b/src/options.h0000440036502000116100000001010112057375131015016 0ustar heinenneng/* skipfish - option and config parsing ------------------------------------ Author: Niels Heinen Copyright 2012 by Google Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ #ifndef __OPTIONS_H #define __OPTIONS_H #include #include /* Config file reader function */ int read_config_file(const char *filename, int *_argc, char ***_argv); /* Config parsing cleanup function that releases memory */ void destroy_config(); /* Long flags */ #ifdef _VIA_OPTIONS_C #define MAX_LINE_LEN 2048 #define MAX_ARGS 100 #else /* The option string for getopt_long */ #define OPT_STRING "+A:B:C:D:EF:G:H:I:J:K:LMNOPQR:S:T:UW:X:YZ" \ "b:c:d:ef:g:hi:k:l:m:o:p:q:r:s:t:uvw:x:z:" struct option long_options[] = { {"auth", required_argument, 0, 'A' }, {"host", required_argument, 0, 'F' }, {"cookie", required_argument, 0, 'C' }, {"reject-cookies", no_argument, 0, 'N' }, {"header", required_argument, 0, 'H' }, {"user-agent", required_argument, 0, 'b' }, #ifdef PROXY_SUPPORT {"proxy", required_argument, 0, 'J' }, #endif /* PROXY_SUPPORT */ {"max-crawl-depth", required_argument, 0, 'd' }, {"max-crawl-child", required_argument, 0, 'c' }, {"max-crawl-descendants", required_argument, 0, 'x' }, {"max-request-total", required_argument, 0, 'r' }, {"max-request-rate", required_argument, 0, 'l'}, {"crawl-probability", required_argument, 0, 'p' }, {"seed", required_argument, 0, 'q' }, {"include-string", required_argument, 0, 'I' }, {"exclude-string", required_argument, 0, 'X' }, {"skip-parameter", required_argument, 0, 'K' }, {"no-form-submits", no_argument, 0, 'O' }, {"include-domain", required_argument, 0, 'D' }, {"no-html-parsing", no_argument, 0, 'P' }, {"no-extension-brute", no_argument, 0, 'Y' }, {"log-mixed-content", no_argument, 0, 'M' }, {"skip-error-pages", no_argument, 0, 'Z' }, {"log-external-urls", no_argument, 0, 'U' }, {"log-cache-mismatches", no_argument, 0, 'E' }, {"form-value", required_argument, 0, 'T' }, {"rw-wordlist", required_argument, 0, 'W' }, {"no-keyword-learning", no_argument, 0, 'L' }, {"wordlist", required_argument, 0, 'S'}, {"trust-domain", required_argument, 0, 'B' }, {"max-connections", required_argument, 0, 'g' }, {"max-host-connections", required_argument, 0, 'm' }, {"max-failed-requests", required_argument, 0, 'f' }, {"request-timeout", required_argument, 0, 't' }, {"network-timeout", required_argument, 0, 'w' }, {"idle-timeout", required_argument, 0, 'i' }, {"response-size", required_argument, 0, 's' }, {"discard-binary", no_argument, 0, 'e' }, {"output", required_argument, 0, 'o' }, {"help", no_argument, 0, 'h' }, {"quiet", no_argument, 0, 'u' }, {"verbose", no_argument, 0, 'v' }, {"scan-timeout", required_argument, 0, 'k'}, {"signatures", required_argument, 0, 'z'}, {"checks", no_argument, 0, 0}, {"checks-toggle", required_argument, 0, 0}, {"no-injection-tests", no_argument, 0, 0}, {"fast", no_argument, 0, 0}, {"flush-to-disk", no_argument, 0, 0}, {"config", required_argument, 0, 0}, {"auth-form", required_argument, 0, 0}, {"auth-form-target", required_argument, 0, 0}, {"auth-user", required_argument, 0, 0}, {"auth-user-field", required_argument, 0, 0}, {"auth-pass", required_argument, 0, 0}, {"auth-pass-field", required_argument, 0, 0}, {"auth-verify-url", required_argument, 0, 0}, {0, 0, 0, 0 } }; #endif /* !__VIA_OPTIONS_C */ #endif /* __OPTIONS_H */ skipfish-2.10b/src/auth.h0000440036502000116100000000404712057375131014300 0ustar heinenneng/* skipfish - form authentication matching ---------------------------------------- Author: Niels Heinen Copyright 2012 by Google Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ #ifndef _HAVE_AUTH_H void authenticate(); u8 submit_auth_form(struct http_request* req, struct http_response* res); u8 auth_form_callback(struct http_request* req, struct http_response* res); u8 auth_verify_tests(struct pivot_desc* pivot); u8 auth_verify_checks(struct http_request* req, struct http_response* res); extern u8 *auth_form, /* Auth form location */ *auth_form_target, /* Auth form submit target */ *auth_user, /* User name */ *auth_pass, /* Password */ *auth_user_field, /* Username field id */ *auth_pass_field, /* Password input field id */ *auth_verify_url; /* Auth verify URL */ extern u8 auth_state; #define ASTATE_NONE 0 #define ASTATE_START 1 #define ASTATE_SEND 2 #define ASTATE_VERIFY 3 #define ASTATE_DONE 4 #define ASTATE_FAIL 5 #ifdef _VIA_AUTH_C /* These strings are used to find the username field */ static const char* user_fields[] = { "user", "name", "email", 0 }; /* These strings are used to find the password field */ static const char* pass_fields[] = { "pass", "secret", "pin", 0 }; #endif /* !_VIA_AUTH_C */ #endif /* !_HAVE_AUTH_H */ skipfish-2.10b/src/alloc-inl.h0000440036502000116100000002457212057375131015216 0ustar heinenneng/* skipfish - error-checking, memory-zeroing alloc routines -------------------------------------------------------- Note: when DEBUG_ALLOCATOR is set, a horribly slow but pedantic allocation tracker is used. Don't enable this in production. Author: Michal Zalewski Copyright 2009 - 2012 by Google Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ #ifndef _HAVE_ALLOC_INL_H #define _HAVE_ALLOC_INL_H #include #include #include "config.h" #include "types.h" #include "debug.h" #define ALLOC_CHECK_SIZE(_s) do { \ if ((_s) > MAX_ALLOC) \ ABORT("Bad alloc request: %u bytes", (_s)); \ } while (0) #define ALLOC_CHECK_RESULT(_r,_s) do { \ if (!(_r)) \ ABORT("Out of memory: can't allocate %u bytes", (_s)); \ } while (0) #define ALLOC_MAGIC 0xFF00 #define ALLOC_MAGIC_F 0xFE00 #define ALLOC_C(_ptr) (((u16*)(_ptr))[-3]) #define ALLOC_S(_ptr) (((u32*)(_ptr))[-1]) #define CHECK_PTR(_p) do { \ if ((_p) && ALLOC_C(_p) != ALLOC_MAGIC) {\ if (ALLOC_C(_p) == ALLOC_MAGIC_F) \ ABORT("Use after free."); \ else \ ABORT("Bad alloc canary."); \ } \ } while (0) #define CHECK_PTR_EXPR(_p) ({ \ typeof (_p) _tmp = (_p); \ CHECK_PTR(_tmp); \ _tmp; \ }) #ifdef CHECK_UAF # define CP(_p) CHECK_PTR_EXPR(_p) #else # define CP(_p) (_p) #endif /* ^CHECK_UAF */ #ifdef ALIGN_ACCESS # define ALLOC_OFF 8 #else # define ALLOC_OFF 6 #endif /* ^ALIGN_ACCESS */ static inline void* __DFL_ck_alloc(u32 size) { void* ret; if (!size) return NULL; ALLOC_CHECK_SIZE(size); ret = malloc(size + ALLOC_OFF); ALLOC_CHECK_RESULT(ret, size); ret += ALLOC_OFF; ALLOC_C(ret) = ALLOC_MAGIC; ALLOC_S(ret) = size; return memset(ret, 0, size); } static inline void* __DFL_ck_realloc(void* orig, u32 size) { void* ret; u32 old_size = 0; if (!size) { if (orig) { CHECK_PTR(orig); /* Catch pointer issues sooner. */ #ifdef DEBUG_ALLOCATOR memset(orig - ALLOC_OFF, 0xFF, ALLOC_S(orig) + ALLOC_OFF); ALLOC_C(orig) = ALLOC_MAGIC_F; #endif /* DEBUG_ALLOCATOR */ free(orig - ALLOC_OFF); } return NULL; } if (orig) { CHECK_PTR(orig); #ifndef DEBUG_ALLOCATOR ALLOC_C(orig) = ALLOC_MAGIC_F; #endif /* !DEBUG_ALLOCATOR */ old_size = ALLOC_S(orig); orig -= ALLOC_OFF; ALLOC_CHECK_SIZE(old_size); } ALLOC_CHECK_SIZE(size); #ifndef DEBUG_ALLOCATOR ret = realloc(orig, size + ALLOC_OFF); ALLOC_CHECK_RESULT(ret, size); #else /* Catch pointer issues sooner: force relocation and make sure that the original buffer is wiped. */ ret = malloc(size + ALLOC_OFF); ALLOC_CHECK_RESULT(ret, size); if (orig) { memcpy(ret + ALLOC_OFF, orig + ALLOC_OFF, MIN(size, old_size)); memset(orig, 0xFF, old_size + ALLOC_OFF); ALLOC_C(orig + ALLOC_OFF) = ALLOC_MAGIC_F; free(orig); } #endif /* ^!DEBUG_ALLOCATOR */ ret += ALLOC_OFF; ALLOC_C(ret) = ALLOC_MAGIC; ALLOC_S(ret) = size; if (size > old_size) memset(ret + old_size, 0, size - old_size); return ret; } static inline void* __DFL_ck_realloc_kb(void* orig, u32 size) { #ifndef DEBUG_ALLOCATOR if (orig) { CHECK_PTR(orig); if (ALLOC_S(orig) >= size) return orig; size = ((size >> 10) + 1) << 10; } #endif /* !DEBUG_ALLOCATOR */ return __DFL_ck_realloc(orig, size); } static inline u8* __DFL_ck_strdup(u8* str) { void* ret; u32 size; if (!str) return NULL; size = strlen((char*)str) + 1; ALLOC_CHECK_SIZE(size); ret = malloc(size + ALLOC_OFF); ALLOC_CHECK_RESULT(ret, size); ret += ALLOC_OFF; ALLOC_C(ret) = ALLOC_MAGIC; ALLOC_S(ret) = size; return memcpy(ret, str, size); } static inline void* __DFL_ck_memdup(void* mem, u32 size) { void* ret; if (!mem || !size) return NULL; ALLOC_CHECK_SIZE(size); ret = malloc(size + ALLOC_OFF); ALLOC_CHECK_RESULT(ret, size); ret += ALLOC_OFF; ALLOC_C(ret) = ALLOC_MAGIC; ALLOC_S(ret) = size; return memcpy(ret, mem, size); } static inline u8* __DFL_ck_memdup_str(u8* mem, u32 size) { u8* ret; if (!mem || !size) return NULL; ALLOC_CHECK_SIZE(size); ret = malloc(size + ALLOC_OFF + 1); ALLOC_CHECK_RESULT(ret, size); ret += ALLOC_OFF; ALLOC_C(ret) = ALLOC_MAGIC; ALLOC_S(ret) = size; memcpy(ret, mem, size); ret[size] = 0; return ret; } static inline void __DFL_ck_free(void* mem) { if (mem) { CHECK_PTR(mem); #ifdef DEBUG_ALLOCATOR /* Catch pointer issues sooner. */ memset(mem - ALLOC_OFF, 0xFF, ALLOC_S(mem) + ALLOC_OFF); #endif /* DEBUG_ALLOCATOR */ ALLOC_C(mem) = ALLOC_MAGIC_F; free(mem - ALLOC_OFF); } } #ifndef DEBUG_ALLOCATOR /* Non-debugging mode - straightforward aliasing. */ #define ck_alloc __DFL_ck_alloc #define ck_realloc __DFL_ck_realloc #define ck_realloc_kb __DFL_ck_realloc_kb #define ck_strdup __DFL_ck_strdup #define ck_memdup __DFL_ck_memdup #define ck_memdup_str __DFL_ck_memdup_str #define ck_free __DFL_ck_free #else /* Debugging mode - include additional structures and support code. */ #define ALLOC_BUCKETS 4096 #define ALLOC_TRK_CHUNK 256 struct TRK_obj { void *ptr; char *file, *func; u32 line; }; extern struct TRK_obj* TRK[ALLOC_BUCKETS]; extern u32 TRK_cnt[ALLOC_BUCKETS]; #ifndef __LP64__ #define TRKH(_ptr) (((((u32)_ptr) >> 16) ^ ((u32)_ptr)) % ALLOC_BUCKETS) #else #define TRKH(_ptr) (((((u64)_ptr) >> 16) ^ ((u64)_ptr)) % ALLOC_BUCKETS) #endif /* Adds a new entry to the list of allocated objects. */ static inline void TRK_alloc_buf(void* ptr, const char* file, const char* func, u32 line) { u32 i, bucket; if (!ptr) return; bucket = TRKH(ptr); for (i = 0; i < TRK_cnt[bucket]; i++) if (!TRK[bucket][i].ptr) { TRK[bucket][i].ptr = ptr; TRK[bucket][i].file = (char*)file; TRK[bucket][i].func = (char*)func; TRK[bucket][i].line = line; return; } /* No space available. */ //TRK[bucket] = __DFL_ck_realloc(TRK[bucket], // (TRK_cnt[bucket] + 1) * sizeof(struct TRK_obj)); if (!(i % ALLOC_TRK_CHUNK)) { TRK[bucket] = __DFL_ck_realloc(TRK[bucket], TRK_cnt[bucket] + ALLOC_TRK_CHUNK * sizeof(struct TRK_obj)); } TRK[bucket][i].ptr = ptr; TRK[bucket][i].file = (char*)file; TRK[bucket][i].func = (char*)func; TRK[bucket][i].line = line; TRK_cnt[bucket]++; } /* Removes entry from the list of allocated objects. */ static inline void TRK_free_buf(void* ptr, const char* file, const char* func, u32 line) { u32 i, bucket; if (!ptr) return; bucket = TRKH(ptr); for (i = 0; i < TRK_cnt[bucket]; i++) if (TRK[bucket][i].ptr == ptr) { TRK[bucket][i].ptr = 0; return; } WARN("ALLOC: Attempt to free non-allocated memory in %s (%s:%u)", func, file, line); } /* Does a final report on all non-deallocated objects. */ static inline void __TRK_report(void) { u32 i, bucket; fflush(0); for (bucket = 0; bucket < ALLOC_BUCKETS; bucket++) for (i = 0; i < TRK_cnt[bucket]; i++) if (TRK[bucket][i].ptr) WARN("ALLOC: Memory never freed, created in %s (%s:%u)", TRK[bucket][i].func, TRK[bucket][i].file, TRK[bucket][i].line); } /* Simple wrappers for non-debugging functions: */ static inline void* TRK_ck_alloc(u32 size, const char* file, const char* func, u32 line) { void* ret = __DFL_ck_alloc(size); TRK_alloc_buf(ret, file, func, line); return ret; } static inline void* TRK_ck_realloc(void* orig, u32 size, const char* file, const char* func, u32 line) { void* ret = __DFL_ck_realloc(orig, size); TRK_free_buf(orig, file, func, line); TRK_alloc_buf(ret, file, func, line); return ret; } static inline void* TRK_ck_realloc_kb(void* orig, u32 size, const char* file, const char* func, u32 line) { void* ret = __DFL_ck_realloc_kb(orig, size); TRK_free_buf(orig, file, func, line); TRK_alloc_buf(ret, file, func, line); return ret; } static inline void* TRK_ck_strdup(u8* str, const char* file, const char* func, u32 line) { void* ret = __DFL_ck_strdup(str); TRK_alloc_buf(ret, file, func, line); return ret; } static inline void* TRK_ck_memdup(void* mem, u32 size, const char* file, const char* func, u32 line) { void* ret = __DFL_ck_memdup(mem, size); TRK_alloc_buf(ret, file, func, line); return ret; } static inline void* TRK_ck_memdup_str(void* mem, u32 size, const char* file, const char* func, u32 line) { void* ret = __DFL_ck_memdup_str(mem, size); TRK_alloc_buf(ret, file, func, line); return ret; } static inline void TRK_ck_free(void* ptr, const char* file, const char* func, u32 line) { TRK_free_buf(ptr, file, func, line); __DFL_ck_free(ptr); } /* Alias user-facing names to tracking functions: */ #define ck_alloc(_p1) \ TRK_ck_alloc(_p1, __FILE__, __FUNCTION__, __LINE__) #define ck_realloc(_p1, _p2) \ TRK_ck_realloc(_p1, _p2, __FILE__, __FUNCTION__, __LINE__) #define ck_realloc_kb(_p1, _p2) \ TRK_ck_realloc_kb(_p1, _p2, __FILE__, __FUNCTION__, __LINE__) #define ck_strdup(_p1) \ TRK_ck_strdup(_p1, __FILE__, __FUNCTION__, __LINE__) #define ck_memdup(_p1, _p2) \ TRK_ck_memdup(_p1, _p2, __FILE__, __FUNCTION__, __LINE__) #define ck_memdup_str(_p1, _p2) \ TRK_ck_memdup_str(_p1, _p2, __FILE__, __FUNCTION__, __LINE__) #define ck_free(_p1) \ TRK_ck_free(_p1, __FILE__, __FUNCTION__, __LINE__) #endif /* ^!DEBUG_ALLOCATOR */ #define alloc_printf(_str...) ({ \ u8* _tmp; \ s32 _len = snprintf(NULL, 0, _str); \ if (_len < 0) FATAL("Whoa, snprintf() fails?!"); \ _tmp = ck_alloc(_len + 1); \ snprintf((char*)_tmp, _len + 1, _str); \ _tmp; \ }) #endif /* ! _HAVE_ALLOC_INL_H */ skipfish-2.10b/src/signatures.h0000440036502000116100000001275712057375131015532 0ustar heinenneng /* skipfish - signature matching ---------------------------------------- Author: Niels Heinen Copyright 2011 - 2012 by Google Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ #include "pcre.h" #ifndef _SIGNATURE_H #define _SIGNATURE_H #define MAX_CONTENT 10 #define PCRE_VECTOR 30 #define MAX_PCRE_CSTR_SIZE 256 struct content_struct { u8* match_str; /* The content string to find */ u32 match_str_len; /* Length of the content string */ pcre* pcre_sig; /* Regex: compiled */ pcre_extra* pcre_extra_sig; /* Regex: extra */ u8 *cap_match_str; /* To compare with pcre cap string */ u8 no; /* 1 = string should not be there */ u8 nocase; /* 1 = case insensitive matching */ u8 type; /* regex or static string */ u32 depth; /* Depth of bytes to search */ u32 distance; /* Relative distance to search */ u32 offset; /* Search starts after offset */ }; struct signature { u32 id; /* Unique ID for documentation */ u8* memo; /* Message displayed when found */ u8 severity; /* Severity */ u32 prob; /* Problem ID from analysis.h */ u8* mime; /* Match with this mime type */ u8* header; /* Match with this mime type */ u32 rcode; /* Match with HTTP resp code */ u32 content_cnt; /* Amount of contenrt structs */ u32 check; /* The check ID */ u8 report; /* 0 = always, 1 = once */ u8 proto; /* 0, PROTO_HTTP or PROTO_HTTPS */ struct signature *depend; /* Chain/depend on this sig */ struct content_struct* content[MAX_CONTENT]; }; /* The signature matching function */ u8 match_signatures(struct http_request *req, struct http_response *res); /* Load the passwords from a file */ void load_signatures(u8* fname); /* Destroy the wordlists and free all memory */ void destroy_signature_lists(void); /* Wrapper for reporting a signature problem */ void signature_problem(struct signature *sig, struct http_request *req, struct http_response *res); struct signature** sig_list; /* The one and only: signature list */ extern u32 slist_max_cnt; /* Allocated space in the signature lists */ u32 slist_cnt; /* Actual elements in the signature lists */ #define TYPE_PLAIN 0 /* Content type: static string */ #define TYPE_REGEX 1 /* Content type: regular expression */ #define MAX_SIG_LEN 2048 /* Signature line length */ #define MAX_SIG_CNT 1024 /* Max amount of signatures to load */ #define MAX_SIG_FNAME 512 /* Maximum signature filename */ #define MAX_SIG_INCS 64 /* Maximum files to include. */ #ifdef _VIA_SIGNATURE_C u32 sig_serv[] = { PROB_SIG_DETECT, /* Default: info level */ PROB_SIG_DETECT_H, /* High risk */ PROB_SIG_DETECT_M, /* Medium risk */ PROB_SIG_DETECT_L, /* Low risk */ PROB_SIG_DETECT /* info risk */ }; /* Destroy an individual signature */ void destroy_signature(struct signature *sig); #define SIG_ID 1 #define SIG_CONTENT 2 #define SIG_MEMO 3 #define SIG_TYPE 4 #define SIG_SEV 5 #define SIG_CONST 6 #define SIG_PROB 7 #define SIG_TAG 8 #define SIG_MIME 9 #define SIG_CODE 10 #define SIG_CASE 11 #define SIG_DEPTH 12 #define SIG_OFFSET 13 #define SIG_DIST 14 #define SIG_CHK 15 #define SIG_PROTO 16 #define SIG_HEADER 17 #define SIG_REPORT 18 #define SIG_DEPEND 29 #define SIG_PCRE_MATCH 30 /* The structs below are to for helping the signature parser */ struct sig_key { u32 id; const char *name; }; struct sig_key lookuptable[] = { { SIG_ID, "id" }, { SIG_CONTENT, "content" }, { SIG_MEMO, "memo" }, { SIG_TYPE, "type" }, { SIG_SEV, "sev" }, { SIG_PROB, "prob" }, { SIG_TAG, "tag" }, { SIG_MIME, "mime" }, { SIG_CODE, "code" }, { SIG_CASE, "nocase" }, { SIG_DEPTH, "depth" }, { SIG_OFFSET, "offset" }, { SIG_PCRE_MATCH, "regex_match" }, { SIG_DIST, "distance" }, { SIG_CHK, "check" }, { SIG_PROTO, "proto" }, { SIG_HEADER, "header" }, { SIG_REPORT, "report" }, { SIG_DEPEND, "depend" }, { 0, 0} }; /* Specified whether and when a match should be reported */ #define REPORT_ALWAYS 0 #define REPORT_ONCE 1 #define REPORT_NEVER 2 #endif /* !_VIA_SIGNATURE_C */ #endif /* !_SIGNATURE_H */ skipfish-2.10b/src/options.c0000440036502000116100000000601712057375131015024 0ustar heinenneng/* skipfish - Config parsing ---------------------------------------- Author: Niels Heinen , Copyright 2012 by Google Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ #include #define _VIA_OPTIONS_C #include "options.h" #include "types.h" #include "debug.h" #include "alloc-inl.h" #include "string-inl.h" u8 **fargv; u32 fargc = 0; /* This function reads the configuration file turns them into flags that are given to getopt_long. */ int read_config_file(const char *filename, int *_argc, char ***_argv) { FILE *fh; char line[MAX_LINE_LEN + 1]; char *val, *ptr; u8 *tmp; u32 idx, i; APPEND_STRING(fargv, fargc, ck_strdup((u8*)*_argv[0])); fh = fopen(filename, "r"); if (!fh) PFATAL("Unable to read config from: %s", filename); while (!feof(fh) && fargc < MAX_ARGS && fgets(line, MAX_LINE_LEN, fh)) { /* Skip comments and empty lines */ if (line[0] == '\n' || line[0] == '\r' || line[0] == '#') continue; /* NULL terminate the key */ idx = strcspn(line, " \t="); if (idx == strlen(line)) FATAL("Config key error at line: %s", line); line[idx] = '\0'; /* Find the beginning of the value. */ val = line + (idx + 1); idx = strspn(val, " \t="); if (idx == strlen(val)) FATAL("Config value error at line: %s", line); val = val + idx; /* Trim the unwanted characters from the value */ ptr = val + (strlen(val) - 1); while(*ptr && *ptr < 0x21) { *ptr = 0; ptr--; } /* Done! Now we have a key/value pair. If the flag is set to 'false' we will disregard this line. If the value is 'true', we will set the flag without a value. In any other case, we will set the flag and value */ if (val[0] == '\0') FATAL("Empty value in config line: %s", line); if (strcasecmp("false", val) == 0) continue; tmp = ck_alloc(strlen(line) + 3); sprintf((char*)tmp, "--%s", line); APPEND_STRING(fargv, fargc, tmp); if (strncasecmp("true", val, 3) != 0) APPEND_STRING(fargv, fargc, ck_strdup((u8*)val)); } /* Copy arguments from command line into our array */ for (i=1; i<*_argc && fargc < MAX_ARGS; ++i) APPEND_STRING(fargv, fargc, ck_strdup((u8*)(*_argv)[i])); /* Replace original flags */ *_argc = fargc; *_argv = (char **)fargv; fclose(fh); return 0; } /* Helper function to cleanup memory */ void destroy_config() { if (fargc == 0) return; while (fargc-- != 0) ck_free(fargv[fargc]); ck_free(fargv); } skipfish-2.10b/src/string-inl.h0000440036502000116100000001473312057375131015430 0ustar heinenneng/* skipfish - various string manipulation helpers ---------------------------------------------- Some modern operating systems still ship with no strcasestr() or memmem() implementations in place, for reasons beyond comprehension. This file includes a simplified version of these routines, copied from NetBSD, plus several minor, custom string manipulation macros and inline functions. The original NetBSD code is licensed under a BSD license, as follows: Copyright (c) 1990, 1993 The Regents of the University of California. All rights reserved. This code is derived from software contributed to Berkeley by Chris Torek. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the University nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef _HAVE_STRING_INL_H #define _HAVE_STRING_INL_H #include #include #include "types.h" /* Macros for easy string prefix matching */ #define prefix(_long, _short) \ strncmp((const char*)(_long), (const char*)(_short), \ strlen((const char*)(_short))) #define case_prefix(_long, _short) \ strncasecmp((const char*)(_long), (const char*)(_short), \ strlen((const char*)(_short))) /* Appends a string to a dynamic array by first extending it. */ #define APPEND_STRING(_ptr, _cnt, _val) do { \ (_ptr) = ck_realloc(_ptr, ((_cnt) + 1) * sizeof(u8*)); \ (_ptr)[_cnt] = (u8*)(_val); \ (_cnt)++; \ } while (0) /* Modified NetBSD strcasestr() implementation (rolling strncasecmp). */ static inline u8* inl_strcasestr(const u8* haystack, const u8* needle) { register u8 c, sc; register u32 len; if (!haystack || !needle) return 0; if ((c = *needle++)) { c = tolower(c); len = strlen((char*)needle); do { do { if (!(sc = *haystack++)) return 0; } while (tolower(sc) != c); } while (strncasecmp((char*)haystack, (char*)needle, len)); haystack--; } return (u8*)haystack; } /* Modified NetBSD memmem() implementation (rolling memcmp). */ static inline void* inl_memmem(const void* haystack, u32 h_len, const void* needle, u32 n_len) { register u8* sp = (u8*)haystack; register u8* pp = (u8*)needle; register u8* eos = sp + h_len - n_len; if (!(haystack && needle && h_len && n_len)) return 0; while (sp <= eos) { if (*sp == *pp) if (memcmp(sp, pp, n_len) == 0) return sp; sp++; } return 0; } /* Distance-limited strstr. */ static inline u8* inl_findstr(const u8* haystack, const u8* needle, u32 max_len) { register u8 c, sc; register u32 len; if (!haystack || !needle) return 0; max_len++; if ((c = *needle++)) { len = strlen((char*)needle); do { do { if (!(sc = *haystack++) || !max_len--) return 0; } while (sc != c); } while (strncmp((char*)haystack, (char*)needle, len)); haystack--; } return (u8*)haystack; } /* Distance-limited and case-insensitive strstr. */ static inline u8* inl_findstrcase(const u8* haystack, const u8* needle, u32 max_len) { register u8 c, sc; register u32 len; if (!haystack || !needle) return 0; max_len++; if ((c = *needle++)) { len = strlen((char*)needle); do { do { if (!(sc = *haystack++) || !max_len--) return 0; } while (tolower(sc) != c); } while (strncasecmp((char*)haystack, (char*)needle, len)); haystack--; } return (u8*)haystack; } /* String manipulation macros for operating on a dynamic buffer. */ #define NEW_STR(_buf_ptr, _buf_len) do { \ (_buf_ptr) = ck_alloc(1024); \ (_buf_len) = 0; \ } while (0) #define ADD_STR_DATA(_buf_ptr, _buf_len, _str) do { \ u32 _sl = strlen((char*)_str); \ if ((_buf_len) + (_sl) + 1 > ALLOC_S(_buf_ptr)) { \ u32 _nsiz = ((_buf_len) + _sl + 1024) >> 10 << 10; \ (_buf_ptr) = ck_realloc(_buf_ptr, _nsiz); \ } \ memcpy((_buf_ptr) + (_buf_len), _str, _sl + 1); \ (_buf_len) += _sl; \ } while (0) #define TRIM_STR(_buf_ptr, _buf_len) do { \ (_buf_ptr) = ck_realloc(_buf_ptr, _buf_len + 1); \ (_buf_ptr)[_buf_len] = 0; \ } while (0) /* Simple base64 encoder */ static inline u8* b64_encode(u8* str, u32 len) { const u8 b64[64] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz" "0123456789+/"; u8 *ret, *cur; ret = cur = ck_alloc((len + 3) * 4 / 3 + 1); while (len > 0) { if (len >= 3) { u32 comp = (str[0] << 16) | (str[1] << 8) | str[2]; *(cur++) = b64[comp >> 18]; *(cur++) = b64[(comp >> 12) & 0x3F]; *(cur++) = b64[(comp >> 6) & 0x3F]; *(cur++) = b64[comp & 0x3F]; len -= 3; str += 3; } else if (len == 2) { u32 comp = (str[0] << 16) | (str[1] << 8); *(cur++) = b64[comp >> 18]; *(cur++) = b64[(comp >> 12) & 0x3F]; *(cur++) = b64[(comp >> 6) & 0x3D]; *(cur++) = '='; len -= 2; str += 2; } else { u32 comp = (str[0] << 16);; *(cur++) = b64[comp >> 18]; *(cur++) = b64[(comp >> 12) & 0x3F]; *(cur++) = '='; *(cur++) = '='; len--; str++; } } *cur = 0; return ret; } #endif /* !_HAVE_STRING_INL_H */ skipfish-2.10b/src/http_client.h0000440036502000116100000004112612057375131015653 0ustar heinenneng/* skipfish - high-performance, single-process asynchronous HTTP client -------------------------------------------------------------------- Author: Michal Zalewski Copyright 2009, 2010, 2011 by Google Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ #ifndef _HAVE_HTTP_CLIENT_H #define _HAVE_HTTP_CLIENT_H #include #include "config.h" #include "types.h" #include "alloc-inl.h" #include "string-inl.h" /* Generic type-name-value array, used for HTTP headers, etc: */ struct param_array { u8* t; /* Type */ u8** n; /* Name */ u8** v; /* Value */ u8** h; /* Host */ u32 c; /* Count */ }; /* Flags for http_request protocol: */ #define PROTO_NONE 0 /* Illegal value */ #define PROTO_HTTP 1 /* Plain-text HTTP */ #define PROTO_HTTPS 2 /* TLS/SSL wrapper */ /* Flags for http_request parameter list entries: */ #define PARAM_NONE 0 /* Empty parameter slot */ #define PARAM_PATH 10 /* Path or parametrized path */ #define PARAM_PATH_S 11 /* - Semicolon element */ #define PARAM_PATH_C 12 /* - Comma element */ #define PARAM_PATH_E 13 /* - Exclamation mark element */ #define PARAM_PATH_D 14 /* - Dollar sign element */ #define PATH_SUBTYPE(_x) ((_x) >= PARAM_PATH && (_x) < PARAM_QUERY) #define PARAM_QUERY 20 /* Query parameter */ #define PARAM_QUERY_S 21 /* - Semicolon element */ #define PARAM_QUERY_C 22 /* - Comma element */ #define PARAM_QUERY_E 23 /* - Exclamation mark element */ #define PARAM_QUERY_D 24 /* - Dollar sign element */ #define QUERY_SUBTYPE(_x) ((_x) >= PARAM_QUERY && (_x) < PARAM_POST) #define PARAM_POST 50 /* Post parameter */ #define PARAM_POST_F 51 /* - File field */ #define PARAM_POST_O 52 /* - Non-standard (e.g., JSON) */ #define POST_SUBTYPE(_x) ((_x) >= PARAM_POST && (_x) < PARAM_HEADER) #define PARAM_HEADER 100 /* Generic HTTP header */ #define PARAM_COOKIE 101 /* - HTTP cookie */ #define HEADER_SUBTYPE(_x) ((_x) >= PARAM_HEADER) /* Different character sets to feed the encoding function */ #define ENC_DEFAULT "#&=+;,!$?%" /* Default encoding */ #define ENC_PATH "#&=+;,!$?%/" /* Path encoding with slash */ #define ENC_NULL "#&=+;,!$?" /* Encoding without % */ /* SSL Cipher strengths */ #define SSL_MEDIUM 0x00000040L #define SSL_HIGH 0x00000080L struct http_response; struct queue_entry; /* HTTP response signature. */ struct http_sig { u32 code; /* HTTP response code */ u32 data[FP_SIZE]; /* Response fingerprint data */ u8 has_text; /* Does the page have text */ }; /* HTTP request descriptor: */ struct http_request { u8 proto; /* Protocol (PROTO_*) */ u8* method; /* HTTP method (GET, POST, ...) */ u8* host; /* Host name */ u32 addr; /* Resolved IP address */ u16 port; /* Port number to connect to */ u8* orig_url; /* Copy of the original URL */ struct param_array par; /* Parameters, headers, cookies */ struct pivot_desc *pivot; /* Pivot descriptor */ u32 user_val; /* Can be used freely */ u8 (*callback)(struct http_request*, struct http_response*); /* Callback to invoke when done */ struct http_sig same_sig; /* Used by secondary ext fuzz. */ /* Used by directory brute-force: */ u8* trying_key; /* Current keyword ptr */ u8 trying_spec; /* Keyword specificity info */ u32 check_id; /* Injection test ID */ u32 check_subid; /* Injection test subid */ /* Used by injection tests: */ u8* fuzz_par_enc; /* Fuzz target encoding */ u8 no_cookies; /* Don't send cookies */ u8 browser; /* Use specified user-agent */ u32 start_time; /* Request start time */ u32 end_time; /* Request end time */ u8 flushed; /* Data is flushed to disk? */ u8* flush_dir; /* Location of data on disk */ }; /* Flags for http_response completion state: */ #define STATE_NOTINIT 0 /* Request not sent */ #define STATE_CONNECT 1 /* Connecting... */ #define STATE_SEND 2 /* Sending request */ #define STATE_RECEIVE 3 /* Waiting for response */ #define STATE_OK 100 /* Proper fetch */ #define STATE_DNSERR 101 /* DNS error */ #define STATE_LOCALERR 102 /* Socket or routing error */ #define STATE_CONNERR 103 /* Connection failed */ #define STATE_RESPERR 104 /* Response not valid */ #define STATE_SUPPRESS 200 /* Dropped (limits / errors) */ /* Flags for http_response warnings: */ #define WARN_NONE 0 /* No warnings */ #define WARN_PARTIAL 1 /* Incomplete read */ #define WARN_TRAIL 2 /* Trailing request garbage */ #define WARN_CFL_HDR 4 /* Conflicting headers */ /* HTTP response descriptor: */ struct http_response { u32 state; /* HTTP convo state (STATE_*) */ u32 code; /* HTTP response code */ u8* msg; /* HTTP response message */ u32 warn; /* Warning flags */ u8 cookies_set; /* Sets cookies? */ struct param_array hdr; /* Server header, cookie list */ u32 pay_len; /* Response payload length */ u8* payload; /* Response payload data */ struct http_sig sig; /* Response signature data */ /* Various information populated by content checks: */ u8 sniff_mime_id; /* Sniffed MIME (MIME_*) */ u8 decl_mime_id; /* Declared MIME (MIME_*) */ u8* meta_charset; /* META tag charset value */ u8* header_charset; /* Content-Type charset value */ u8* header_mime; /* Content-Type MIME type */ u8* sniffed_mime; /* Detected MIME type (ref) */ /* Everything below is of interest to scrape_response() only: */ u8 doc_type; /* 0 - tbd, 1 - bin, 2 - ascii */ u8 css_type; /* 0 - tbd, 1 - other, 2 - css */ u8 js_type; /* 0 - tbd, 1 - other, 2 - js */ u8 json_safe; /* 0 - no, 1 - yes */ u8 stuff_checked; /* check_stuff() called? */ u8 scraped; /* scrape_response() called? */ u8 flushed; /* Data is flushed to disk? */ u8* flush_dir; /* Location of data on disk */ }; /* Open keep-alive connection descriptor: */ struct conn_entry { s32 fd; /* The actual file descriptor */ u8 proto; /* Protocol (PROTO_*) */ u32 addr; /* Destination IP */ u32 port; /* Destination port */ u8 reused; /* Used for earier requests? */ u32 req_start; /* Unix time: request start */ u32 last_rw; /* Unix time: last read / write */ SSL_CTX *srv_ctx; /* SSL context */ SSL *srv_ssl; u8 SSL_rd_w_wr; /* SSL_read() wants to write? */ u8 SSL_wr_w_rd; /* SSL_write() wants to read? */ u8 ssl_checked; /* SSL state checked? */ u8* read_buf; /* Current read buffer */ u32 read_len; u8* write_buf; /* Pending write buffer */ u32 write_off; /* Current write offset */ u32 write_len; u8* origin; /* Connection origin */ struct queue_entry* q; /* Current queue entry */ struct conn_entry* prev; /* Previous connection entry */ struct conn_entry* next; /* Next connection entry */ }; /* Request queue descriptor: */ struct queue_entry { struct http_request* req; /* Request descriptor */ struct http_response* res; /* Response descriptor */ struct conn_entry* c; /* Connection currently used */ struct queue_entry* prev; /* Previous queue entry */ struct queue_entry* next; /* Next queue entry */ u8 retrying; /* Request being retried? */ }; /* DNS cache item: */ struct dns_entry { u8* name; /* Name requested */ u32 addr; /* IP address (0 = bad host) */ struct dns_entry* next; /* Next cache entry */ }; /* Simplified macros to manipulate param_arrays: */ #define ADD(_ar,_t,_n,_v) do { \ u32 _cur = (_ar)->c++; \ (_ar)->t = ck_realloc((_ar)->t, (_ar)->c); \ (_ar)->n = ck_realloc((_ar)->n, (_ar)->c * sizeof(u8*)); \ (_ar)->v = ck_realloc((_ar)->v, (_ar)->c * sizeof(u8*)); \ (_ar)->t[cur] = _t; \ (_ar)->n[cur] = (_n) ? ck_strdup(_n) : 0; \ (_ar)->v[cur] = (_v) ? ck_strdup(_v) : 0; \ } while (0) #define FREE(_ar) do { \ while ((_ar)->c--) { \ ck_free((_ar)->n[(_ar)->c]); \ ck_free((_ar)->v[(_ar)->c]); \ } \ ck_free((_ar)->t); \ ck_free((_ar)->n); \ ck_free((_ar)->v); \ } while (0) /* Extracts parameter value from param_array. Name is matched if non-NULL. Returns pointer to value data, not a duplicate string; NULL if no match found. */ u8* get_value(u8 type, u8* name, u32 offset, struct param_array* par); /* Inserts or overwrites parameter value in param_array. If offset == -1, will append parameter to list. Duplicates strings, name and val can be NULL. */ void set_value(u8 type, u8* name, u8* val, s32 offset, struct param_array* par); /* Simplified macros for value table access: */ #define GET_CK(_name, _p) get_value(PARAM_COOKIE, _name, 0, _p) #define SET_CK(_name, _val, _p) set_value(PARAM_COOKIE, _name, _val, 0, _p) #define GET_PAR(_name, _p) get_value(PARAM_QUERY, _name, 0, _p) #define SET_PAR(_name, _val, _p) set_value(PARAM_QUERY, _name, _val, -1, _p) #define GET_HDR(_name, _p) get_value(PARAM_HEADER, _name, 0, _p) #define SET_HDR(_name, _val, _p) set_value(PARAM_HEADER, _name, _val, -1, _p) #define GET_HDR_OFF(_name, _p, _o) get_value(PARAM_HEADER, _name, _o, _p) void tokenize_path(u8* str, struct http_request* req, u8 add_slash); /* Convert a fully-qualified or relative URL string to a proper http_request representation. Returns 0 on success, 1 on format error. */ u8 parse_url(u8* url, struct http_request* req, struct http_request* ref); /* URL-decodes a string. 'Plus' parameter governs the behavior on + signs (as they have a special meaning only in query params, not in path). */ u8* url_decode_token(u8* str, u32 len, u8 plus); /* URL-encodes a string according to custom rules. The assumption here is that the data is already tokenized as "special" boundaries such as ?, =, &, /, ;, so these characters must always be escaped if present in tokens. We otherwise let pretty much everything else go through, as it may help with the exploitation of certain vulnerabilities. */ u8* url_encode_token(u8* str, u32 len, u8* enc_set); /* Reconstructs URI from http_request data. Includes protocol and host if with_host is non-zero. */ u8* serialize_path(struct http_request* req, u8 with_host, u8 with_post); /* Looks up IP for a particular host, returns data in network order. Uses standard resolver, so it is slow and blocking, but we only expect to call it a couple of times. */ u32 maybe_lookup_host(u8* name); /* Creates an ad hoc DNS cache entry, to override NS lookups. */ void fake_host(u8* name, u32 addr); /* Schedules a new asynchronous request; req->callback() will be invoked when the request is completed. */ void async_request(struct http_request* req); /* Prepares a serialized HTTP buffer to be sent over the network. */ u8* build_request_data(struct http_request* req); /* Parses a network buffer containing raw HTTP response received over the network ('more' == the socket is still available for reading). Returns 0 if response parses OK, 1 if more data should be read from the socket, 2 if the response seems invalid. */ u8 parse_response(struct http_request* req, struct http_response* res, u8* data, u32 data_len, u8 more); /* Helper function for grabbing lines when parsing requests and responses */ u8* grab_line(u8* data, u32* cur_pos, u32 data_len); /* Processes the queue. Returns the number of queue entries remaining, 0 if none. Will do a blocking select() to wait for socket state changes (or timeouts) if no data available to process. This is the main routine for the scanning loop. */ u32 next_from_queue(void); /* Dumps HTTP request stats, for debugging purposes: */ void dump_http_request(struct http_request* r); /* Dumps HTTP response stats, for debugging purposes: */ void dump_http_response(struct http_response* r); /* Fingerprints a response: */ void fprint_response(struct http_response* res); /* Performs a deep free() of sturct http_request */ void destroy_request(struct http_request* req); /* Performs a deep free() of sturct http_response */ void destroy_response(struct http_response* res); /* Creates a working copy of a request. If all is 0, does not copy path, query parameters, or POST data (but still copies headers). */ struct http_request* req_copy(struct http_request* req, struct pivot_desc* pv, u8 all); /* Creates a copy of a response. */ struct http_response* res_copy(struct http_response* res); /* Various settings and counters exported to other modules: */ extern u32 max_connections, max_conn_host, max_requests, max_fail, idle_tmout, resp_tmout, rw_tmout, size_limit, req_errors_net, req_errors_http, req_errors_cur, req_count, req_dropped, req_retried, url_scope, conn_count, conn_idle_tmout, conn_busy_tmout, conn_failed, queue_cur; extern float req_sec, max_requests_sec; extern u64 bytes_sent, bytes_recv, bytes_deflated, bytes_inflated, iterations_cnt; extern u8 ignore_cookies, idle; /* Flags for browser type: */ #define BROWSER_FAST 0 /* Minimimal HTTP headers */ #define BROWSER_MSIE 1 /* Try to mimic MSIE */ #define BROWSER_FFOX 2 /* Try to mimic Firefox */ #define BROWSER_PHONE 4 /* Try to mimic iPhone */ extern u8 browser_type; /* Flags for authentication type: */ #define AUTH_NONE 0 /* No authentication */ #define AUTH_BASIC 1 /* 'Basic' HTTP auth */ #define AUTH_FORM 2 /* Form HTTP auth */ extern u8 auth_type; extern u8 *auth_user, *auth_pass; #ifdef PROXY_SUPPORT extern u8* use_proxy; extern u32 use_proxy_addr; extern u16 use_proxy_port; #endif /* PROXY_SUPPORT */ /* Global HTTP cookies, extra headers: */ extern struct param_array global_http_par; /* Destroys http state information, for memory profiling. */ void destroy_http(); /* Shows some pretty statistics. */ void http_stats(u64 st_time); void http_req_list(void); #endif /* !_HAVE_HTTP_CLIENT_H */ skipfish-2.10b/src/crawler.h0000440036502000116100000001261312057375131014774 0ustar heinenneng/* skipfish - crawler state machine -------------------------------- Author: Michal Zalewski Copyright 2009, 2010, 2011 by Google Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ #ifndef _HAVE_CRAWLER_H #include "types.h" #include "http_client.h" #include "database.h" /* Function called during startup to build the test/check structure */ void replace_slash(struct http_request* req, u8* new_val); void handle_error(struct http_request* req, struct http_response* res, u8* desc, u8 stop); void inject_done(struct pivot_desc*); void destroy_misc_data(struct pivot_desc* pv, struct http_request* self); struct pivot_desc* dir_parent(struct pivot_desc* pv); void authenticate(); /* Internal helper macros: */ #define TPAR(_req) ((_req)->par.v[(_req)->pivot->fuzz_par]) #define SET_VECTOR(_state, _req, _str) do { \ if (_state == PSTATE_CHILD_INJECT) { \ replace_slash((_req), (u8*)_str); \ } else { \ ck_free(TPAR(_req)); \ TPAR(_req) = ck_strdup((u8*)_str); \ } \ } while (0) #define APPEND_VECTOR(_state, _req, _str) do { \ if (_state == PSTATE_CHILD_INJECT) { \ replace_slash((_req), (u8*)_str); \ } else { \ u8* _n = ck_alloc(strlen((char*)TPAR(_req)) + strlen((char*)_str) + 1); \ sprintf((char*)_n, "%s%s", TPAR(_req), _str); \ ck_free(TPAR(_req)); \ TPAR(_req) = _n; \ } \ } while (0) #define PREFIX_STRING(_orig, _str) do { \ u8* tmp = ck_alloc(strlen((char*)_orig) + strlen((char*)_str) + 1); \ sprintf((char*)tmp, "%s%s", (char*)_str, (char*)_orig); \ ck_free(_orig); \ _orig = tmp; \ } while (0) /* Classifies a response, with a special handling of "unavailable" and "gateway timeout" codes. */ #define FETCH_FAIL(_res) ((_res)->state != STATE_OK || (_res)->code == 503 || \ (_res)->code == 504) extern u32 crawl_prob; /* Crawl probability (1-100%) */ extern u8 no_parse, /* Disable HTML link detection */ warn_mixed, /* Warn on mixed content? */ no_fuzz_ext, /* Don't fuzz ext in dirs? */ no_500_dir, /* Don't assume dirs on 500 */ delete_bin, /* Don't keep binary responses */ flush_pivot_data, /* Flush pivot data to disk */ log_ext_urls; /* Log external URLs? */ /* Provisional debugging callback. */ u8 show_response(struct http_request* req, struct http_response* res); /* Asynchronous request callback for the initial PSTATE_FETCH request of PIVOT_UNKNOWN resources. */ u8 unknown_retrieve_check(struct http_request* req, struct http_response* res); /* Asynchronous request callback for the initial PSTATE_FETCH request of PIVOT_FILE resources. */ u8 file_retrieve_check(struct http_request* req, struct http_response* res); /* Asynchronous request callback for the initial PSTATE_FETCH request of PIVOT_DIR resources. */ u8 dir_retrieve_check(struct http_request* req, struct http_response* res); /* Initializes the crawl of try_list items for a pivot point (if any still not crawled). */ void param_trylist_start(struct pivot_desc* pv); /* Adds new name=value to form hints list. */ void add_form_hint(u8* name, u8* value); /* Macros to access various useful pivot points: */ #define RPAR(_req) ((_req)->pivot->parent) #define RPREQ(_req) ((_req)->pivot->req) #define RPRES(_req) ((_req)->pivot->res) #define MREQ(_x) (req->pivot->misc_req[_x]) #define MRES(_x) (req->pivot->misc_res[_x]) /* Debugging instrumentation for callbacks and callback helpers: */ #ifdef LOG_STDERR #define DEBUG_CALLBACK(_req, _res) do { \ u8* _url = serialize_path(_req, 1, 1); \ DEBUG("* %s: URL %s (%u, len %u)\n", __FUNCTION__, _url, (_res) ? \ (_res)->code : 0, (_res) ? (_res)->pay_len : 0); \ ck_free(_url); \ } while (0) #define DEBUG_MISC_CALLBACK(_req, _res) do { \ int i; \ for (i = 0; i < req->pivot->misc_cnt; i++) \ DEBUG_CALLBACK(MREQ(i), MRES(i)); \ } while (0) #define DEBUG_PIVOT(_text, _pv) do { \ u8* _url = serialize_path((_pv)->req, 1, 1); \ DEBUG("* %s: %s\n", _text, _url); \ ck_free(_url); \ } while (0) #define DEBUG_STATE_CALLBACK(_req, _state, _type) do { \ u8* _url = serialize_path(_req, 1, 1); \ DEBUG("* %s::%s: URL %s (running: %s)\n", __FUNCTION__, _state, _url, \ _type ? "checks" : "tests"); \ ck_free(_url); \ } while (0) #define DEBUG_HELPER(_pv) do { \ u8* _url = serialize_path((_pv)->req, 1, 1); \ DEBUG("* %s: URL %s (%u, len %u)\n", __FUNCTION__, _url, (_pv)->res ? \ (_pv)->res->code : 0, (_pv)->res ? (_pv)->res->pay_len : 0); \ ck_free(_url); \ } while (0) #else #define DEBUG_CALLBACK(_req, _res) #define DEBUG_MISC_CALLBACK(_req, _res) #define DEBUG_STATE_CALLBACK(_req, _res, _cb) #define DEBUG_HELPER(_pv) #define DEBUG_PIVOT(_text, _pv) #endif /* ^LOG_STDERR */ #endif /* !_HAVE_CRAWLER_H */ skipfish-2.10b/src/checks.c0000440036502000116100000016774312057375131014607 0ustar heinenneng/* skipfish - injection tests --------------------------- Author: Niels Heinen , Michal Zalewski Copyright 2009 - 2012 by Google Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ #define _VIA_CHECKS_C #include "crawler.h" #include "analysis.h" #include "http_client.h" #include "checks.h" #include "auth.h" static u8 inject_prologue_tests(struct pivot_desc* pivot); static u8 inject_prologue_check(struct http_request*, struct http_response*); static u8 dir_ips_tests(struct pivot_desc* pivot); static u8 dir_ips_check(struct http_request*, struct http_response*); static u8 inject_xml_tests(struct pivot_desc* pivot); static u8 inject_xml_check(struct http_request*, struct http_response*); static u8 inject_xss_tests(struct pivot_desc* pivot); static u8 inject_xss_check(struct http_request*, struct http_response*); static u8 inject_shell_tests(struct pivot_desc* pivot); static u8 inject_shell_check(struct http_request*, struct http_response*); static u8 inject_diff_shell_tests(struct pivot_desc* pivot); static u8 inject_diff_shell_check(struct http_request*, struct http_response*); static u8 inject_dir_listing_tests(struct pivot_desc* pivot); static u8 inject_dir_listing_check(struct http_request*, struct http_response*); static u8 inject_lfi_tests(struct pivot_desc* pivot); static u8 inject_lfi_check(struct http_request*, struct http_response*); static u8 inject_rfi_tests(struct pivot_desc* pivot); static u8 inject_rfi_check(struct http_request*, struct http_response*); static u8 inject_split_tests(struct pivot_desc* pivot); static u8 inject_split_check(struct http_request*, struct http_response*); static u8 inject_redir_tests(struct pivot_desc* pivot); static u8 inject_redir_check(struct http_request*, struct http_response*); static u8 inject_sql_tests(struct pivot_desc* pivot); static u8 inject_sql_check(struct http_request*, struct http_response*); static u8 inject_format_tests(struct pivot_desc* pivot); static u8 inject_format_check(struct http_request*, struct http_response*); static u8 inject_integer_tests(struct pivot_desc* pivot); static u8 inject_integer_check(struct http_request*, struct http_response*); static u8 put_upload_tests(struct pivot_desc* pivot); static u8 put_upload_check(struct http_request*, struct http_response*); static u8 inject_behavior_tests(struct pivot_desc* pivot); static u8 inject_behavior_check(struct http_request*, struct http_response*); static u8 param_behavior_tests(struct pivot_desc* pivot); static u8 param_behavior_check(struct http_request*, struct http_response*); static u8 agent_behavior_tests(struct pivot_desc* pivot); static u8 agent_behavior_check(struct http_request*, struct http_response*); static u8 param_ognl_tests(struct pivot_desc* pivot); static u8 param_ognl_check(struct http_request*, struct http_response*); static u8 xssi_tests(struct pivot_desc* pivot); static u8 xssi_check(struct http_request*, struct http_response*); /* The crawl structure defines the tests by combining function pointers and flags. The values given indicate the following: 1- Amount of responses expected 2- Whether to keep requests and responses before calling the check 3- Whether the check accepted pivots with res_varies set 4- Whether the check is time sensitive 5- Whether we should scrape the response for links. 6- The type of PIVOT that the test/check accepts 7- Pointer to the function that scheduled the test(s) requests 8- Pointer to the function that checks the result 9- Whether to skip this test At the end, inject_done() is called: - we move on with additional tests (e.g. parameter) - or continue with the next pivot Point 8 allows command-line flags to toggle enabled/disabled tests. For example, shell injection tests are not so relevant on Windows environments so this allow them to be disabled. */ u32 cb_handle_cnt = 21; /* Total of checks */ u32 cb_handle_off = 4; /* Checks after the offset are optional */ static struct cb_handle cb_handles[] = { /* Authentication check */ { 2, 0, 0, 0, 0, 0, CHK_SESSION, (u8*)"session check", auth_verify_tests, auth_verify_checks, 0 }, /* Behavior checks for dirs/params */ { BH_CHECKS, 1, 0, 0, 2, PIVOT_PARAM, CHK_BEHAVE, (u8*)"param behavior", param_behavior_tests, param_behavior_check, 0 }, { 2, 1, 0, 0, 2, PIVOT_PARAM, CHK_OGNL, (u8*)"param OGNL", param_ognl_tests, param_ognl_check, 0 }, { BH_CHECKS, 1, 0, 0, 2, PIVOT_DIR|PIVOT_FILE, CHK_BEHAVE, (u8*)"inject behavior", inject_behavior_tests, inject_behavior_check, 0 }, /* All the injection tests */ { 2, 1, 0, 0, 2, PIVOT_DIR, CHK_IPS, (u8*)"IPS check", dir_ips_tests, dir_ips_check, 0 }, { 3, 1, 0, 0, 0, PIVOT_DIR|PIVOT_FILE, CHK_AGENT, (u8*)"User agent behavior", agent_behavior_tests, agent_behavior_check, 0 }, { 2, 1, 0, 0, 0, PIVOT_DIR|PIVOT_SERV, CHK_PUT, (u8*)"PUT upload", put_upload_tests, put_upload_check, 0 }, { 4, 1, 0, 0, 1, PIVOT_DIR|PIVOT_PARAM, CHK_DIR_LIST, (u8*)"dir traversal", inject_dir_listing_tests, inject_dir_listing_check, 0 }, { 48, 1, 1, 0, 1, 0, CHK_LFI, (u8*)"local file inclusion", inject_lfi_tests, inject_lfi_check, 0 }, { 1, 0, 1, 0, 1, 0, CHK_RFI, (u8*)"remote file inclusion", inject_rfi_tests, inject_rfi_check, 0 }, { 4, 0, 1, 0, 1, 0, CHK_XSS, (u8*)"XSS injection", inject_xss_tests, inject_xss_check, 0 }, { 1, 1, 1, 0, 1, 0, CHK_XSSI, (u8*)"XSSI protection", xssi_tests, xssi_check, 0 }, { 1, 0, 1, 0, 1, 0, CHK_PROLOG, (u8*)"prologue injection", inject_prologue_tests, inject_prologue_check, 0 }, { 2, 1, 1, 0, 1, 0, CHK_RSPLIT, (u8*)"Header injection", inject_split_tests, inject_split_check, 0 }, { 5, 1, 1, 0, 1, PIVOT_PARAM, CHK_REDIR, (u8*)"Redirect injection", inject_redir_tests, inject_redir_check, 0 }, { 10, 1, 0, 0, 1, 0, CHK_SQL, (u8*)"SQL injection", inject_sql_tests, inject_sql_check, 0 }, { 2, 1, 0, 0, 1, 0, CHK_XML, (u8*)"XML injection", inject_xml_tests, inject_xml_check, 0 }, { 12, 1, 0, 0, 1, 0, CHK_SHELL_DIFF, (u8*)"Shell injection (diff)", inject_diff_shell_tests, inject_diff_shell_check, 0 }, { 12, 1, 1, 1, 1, 0, CHK_SHELL_SPEC, (u8*)"Shell injection (spec)", inject_shell_tests, inject_shell_check, 0 }, { 2, 1, 0, 0, 1, 0, CHK_FORMAT, (u8*)"format string", inject_format_tests, inject_format_check, 1 }, { 9, 1, 0, 0, 1, 0, CHK_INTEGER, (u8*)"integer handling", inject_integer_tests, inject_integer_check, 1 } }; /* Dump the checks to stdout */ void display_injection_checks(void) { u32 i; SAY("\n[*] Available injection tests:\n\n"); for (i=cb_handle_off; i cb_handle_cnt) FATAL("Unable to parse checks toggle string"); tnr += offset; /* User values are array index nr + 1 */ if (enable && cb_handles[tnr].skip) { cb_handles[tnr].skip = 0; DEBUG(" Enabled test: %d : %s\n", tnr, cb_handles[tnr].name); } else { cb_handles[tnr].skip = 1; DEBUG(" Disabled test: %d : %s\n", tnr, cb_handles[tnr].name); } ptr = (u8*)strtok(NULL, ","); } ck_free(ids); } /* The inject state manager which uses the list ot check structs to decide what test to schedule next */ u8 inject_state_manager(struct http_request* req, struct http_response* res) { u32 i; s32 check = req->pivot->check_idx; DEBUG_CALLBACK(req, res); /* If we are in crawler only more, jump to inject_done to effectively disable all checks for the pivot */ if(no_checks) goto inject_done; /* First test that gets us in the loop? This means we'll immediately go and schedule some tests */ if (check == -1) goto schedule_tests; /* Safety check */ if (check > cb_handle_cnt) FATAL("Check number %d exceeds handle count %d!",check,cb_handle_cnt); /* If requests failed for a test than we might have chosen to not proceed with it by adding the check to i_skip. Here we check if this is the case. */ if (req->pivot->i_skip[check]) return 0; /* For simple injection tests, we do not abort at 503, 504's. But for differential tests, we have to. */ if (res->state != STATE_OK || (!cb_handles[check].allow_varies && (res->code == 503 || res->code == 504))) { handle_error(req, res, (u8*)cb_handles[check].name, 0); content_checks(req, res); req->pivot->i_skip[check] = 1; return 0; } /* Store req/res which is used by checks that like to have multiple req/res pairs before getting called. */ if (cb_handles[check].res_keep) { req->pivot->misc_req[req->user_val] = req; req->pivot->misc_res[req->user_val] = res; req->pivot->misc_cnt++; /* Check and return if we need more responses. */ if (cb_handles[check].res_num && req->pivot->misc_cnt != cb_handles[check].res_num) return 1; } /* Check the results of previously scheduled tests and, if that goes well, schedule new tests. When the callback returns 1, this means more requests are needed before we can can do the final checks. */ DEBUG_STATE_CALLBACK(req, cb_handles[check].name, 1); /* Check if we got all responses to avoid handing over NULL poiners to the * checks() functions */ if (cb_handles[check].res_keep) { for (i=0; ipivot->misc_cnt; i++) { if (!MREQ(i) || !MRES(i)) { DEBUG("-- Misc request #%d failed\n", i); problem(PROB_FETCH_FAIL, req, res, (u8*)"During injection testing", req->pivot, 0); /* Today, we'll give up on this test. In the next release: reschedule */ goto content_checks; } } } if (cb_handles[check].checks(req,res)) return 1; if (!cb_handles[check].res_keep && (cb_handles[check].res_num && ++req->pivot->misc_cnt != cb_handles[check].res_num)) return 0; content_checks: /* If we get here, we're done and can move on. First make sure that all responses have been checked. Than free memory and schedule the next test */ if (cb_handles[check].res_keep && req->pivot->misc_cnt) { for (i=0; ipivot->misc_cnt; i++) { /* Only check content once */ if (!MRES(i) || !MREQ(i) || MRES(i)->stuff_checked) continue; /* Only scrape for checks that want it 0 = don't scrape 1 = check content 2 = check content and extract links */ if (cb_handles[check].scrape > 0) { content_checks(MREQ(i), MRES(i)); if (cb_handles[check].scrape == 2) scrape_response(MREQ(i), MRES(i)); } } } schedule_tests: destroy_misc_data(req->pivot, req); check = ++req->pivot->check_idx; if (check < cb_handle_cnt) { /* Move to the next test in case it's marked... */ if (cb_handles[check].skip) goto schedule_tests; /* Move to the next test in case the page is unstable and the test doesn't want it. */ if ((req->pivot->res_varies && !cb_handles[check].allow_varies) || (req->pivot->res_time_exceeds && cb_handles[check].time_sensitive)) goto schedule_tests; /* Move to the next test in case of pivot type mismatch */ if (cb_handles[check].pv_flag > 0 && !(cb_handles[check].pv_flag & req->pivot->type)) goto schedule_tests; DEBUG_STATE_CALLBACK(req, cb_handles[check].name, 0); /* Do the tests and return upon success or move on to the next upon a return value of 1. We store the ID of the check in the pivot to allow other functions, that use the pivot, to find out the current injection test */ req->pivot->check_id = cb_handles[check].id; if (cb_handles[check].tests(req->pivot) == 1) goto schedule_tests; return 0; } inject_done: /* All injection tests done. Reset the counter and call inject_done() to finish (or proceed with param tests */ DEBUG_STATE_CALLBACK(req, "inject_done", 1); req->pivot->check_idx = -1; inject_done(req->pivot); return 0; } static u8 xssi_tests(struct pivot_desc* pv) { struct http_request* n; DEBUG_HELPER(pv); /* We only want Javascript that does not have inclusion protection. This * test, should be moved to the injection manager whenever we have more * content specific tests (e.g. css ones) */ if(pv->res->js_type != 2 || pv->res->json_safe) return 1; n = req_copy(pv->req, pv, 1); n->callback = inject_state_manager; n->no_cookies = 1; async_request(n); return 0; } static u8 xssi_check(struct http_request* req, struct http_response* res) { DEBUG_MISC_CALLBACK(req, res); /* When the response with cookie is different from the cookie-less response, * than the content is session depended. In case of Javascript without XSSI * protection, this is more than likely an issue. */ if (!same_page(&RPRES(req)->sig, &MRES(0)->sig)) { /* Responses that do not contain the term "function", "if", "for", "while", etc, are much more likely to be dynamic JSON than just static scripts. Let's try to highlight these. */ if ((!req->method || !strcmp((char*)req->method, "GET")) && !inl_findstr(res->payload, (u8*)"if (", 2048) && !inl_findstr(res->payload, (u8*)"if(", 2048) && !inl_findstr(res->payload, (u8*)"for (", 2048) && !inl_findstr(res->payload, (u8*)"for(", 2048) && !inl_findstr(res->payload, (u8*)"while (", 2048) && !inl_findstr(res->payload, (u8*)"while(", 2048) && !inl_findstr(res->payload, (u8*)"function ", 2048) && !inl_findstr(res->payload, (u8*)"function(", 2048)) { problem(PROB_JS_XSSI, req, res, (u8*)"Cookie-less JSON is different", req->pivot, 0); } else { problem(PROB_JS_XSSI, req, res, (u8*)"Cookie-less Javascript response is different", req->pivot, 0); } } /* Now this is interesting. We can lookup the issues in the pivot and if * analysis.c thinks this page has an XSSI, we can kill that assumption */ remove_issue(req->pivot, PROB_JS_XSSI); return 0; } static u8 inject_behavior_tests(struct pivot_desc* pv) { struct http_request* n; u32 i; DEBUG_HELPER(pv); for (i=0;ireq, pv, 1); n->callback = inject_state_manager; n->user_val = i; async_request(n); } return 0; } static u8 inject_behavior_check(struct http_request* req, struct http_response* res) { u32 i; /* pv->state may change after async_request() calls in insta-fail mode, so we should cache accordingly. */ DEBUG_CALLBACK(req, res); for (i=0; ipivot->misc_cnt; i++) { if (!same_page(&RPRES(req)->sig, &MRES(i)->sig)) { problem(PROB_VARIES, MREQ(i), MRES(i), 0, MREQ(i)->pivot, 0); return 0; } } return 0; } static u8 put_upload_tests(struct pivot_desc* pv) { struct http_request* n; DEBUG_HELPER(pv); /* First a PUT request */ n = req_copy(pv->req, pv, 1); if (n->method) ck_free(n->method); n->method = ck_strdup((u8*)"PUT"); n->user_val = 0; n->callback = inject_state_manager; replace_slash(n, (u8*)("PUT-" BOGUS_FILE)); async_request(n); /* Second a FOO for false positives */ n = req_copy(pv->req, pv, 1); if (n->method) ck_free(n->method); n->method = ck_strdup((u8*)"FOO"); n->user_val = 1; n->callback = inject_state_manager; replace_slash(n, (u8*)("FOO-" BOGUS_FILE)); async_request(n); return 0; } static u8 put_upload_check(struct http_request* req, struct http_response* res) { DEBUG_MISC_CALLBACK(req, res); /* If PUT and FOO of the page does not give the same result. And if additionally we get a 2xx code, than we'll mark the issue as detected */ if (!same_page(&MRES(0)->sig, &MRES(1)->sig) && MRES(0)->code >= 200 && MRES(1)->code < 300) problem(PROB_PUT_DIR, MREQ(0), MRES(0), 0, req->pivot, 0); return 0; } /* The prologue test checks whether it is possible to inject a string in the first bytes of the response because this can lead to utf-7 or third party browser plugin attacks */ static u8 inject_prologue_tests(struct pivot_desc* pivot) { u32 orig_state = pivot->state; struct http_request* n; n = req_copy(pivot->req, pivot, 1); SET_VECTOR(orig_state, n, (u8*)"+/skipfish-bom"); n->callback = inject_state_manager; async_request(n); return 0; } static u8 inject_prologue_check(struct http_request* req, struct http_response* res) { DEBUG_CALLBACK(req, res); if (res->pay_len && !prefix(res->payload, (u8*)"+/skipfish-bom") && !GET_HDR((u8*)"Content-Disposition", &res->hdr)) problem(PROB_PROLOGUE, req, res, NULL, req->pivot, 0); return 0; } /* XML injection checks evaluates multiple server responses and determined whether the injected string caused a difference in behavior/reponse */ static u8 inject_xml_tests(struct pivot_desc* pivot) { /* Backend XML injection - 2 requests. */ u32 orig_state = pivot->state; struct http_request* n; n = req_copy(pivot->req, pivot, 1); SET_VECTOR(orig_state, n, "sfish>'>\">"); n->callback = inject_state_manager; n->user_val = 0; async_request(n); n = req_copy(pivot->req, pivot, 1); SET_VECTOR(orig_state, n, "sfish>'>\">"); n->callback = inject_state_manager; n->user_val = 1; async_request(n); return 0; } static u8 inject_xml_check(struct http_request* req, struct http_response* res) { DEBUG_MISC_CALLBACK(req, res); /* Got all responses: misc[0] = valid XML misc[1] = bad XML If misc[0] != misc[1], we probably have XML injection on backend side. */ if (!same_page(&MRES(0)->sig, &MRES(1)->sig)) { problem(PROB_XML_INJECT, MREQ(0), MRES(0), (u8*)"responses for and look different", req->pivot, 0); RESP_CHECKS(MREQ(1), MRES(1)); } return 0; } static u8 inject_shell_tests(struct pivot_desc* pivot) { /* Shell command injection - 12 requests. */ u32 orig_state = pivot->state; u8* tmp; struct http_request* n; n = req_copy(pivot->req, pivot, 1); SET_VECTOR(orig_state, n, (u8*)"`echo skip12``echo 34fish`"); n->callback = inject_state_manager; n->user_val = 0; async_request(n); n = req_copy(pivot->req, pivot, 1); APPEND_VECTOR(orig_state, n, (u8*)"`echo skip12``echo 34fish`"); n->callback = inject_state_manager; n->user_val = 1; async_request(n); n = req_copy(pivot->req, pivot, 1); SET_VECTOR(orig_state, n, (u8*)"`echo${IFS}skip12``echo${IFS}34fish`"); n->callback = inject_state_manager; n->user_val = 2; async_request(n); n = req_copy(pivot->req, pivot, 1); APPEND_VECTOR(orig_state, n, (u8*)"`echo${IFS}skip12``echo${IFS}34fish`"); n->callback = inject_state_manager; n->user_val = 3; async_request(n); /* We use the measured time_base as an offset for the sleep test. The value is limited to MAX_RES_DURATION and the result is < 10 */ tmp = ck_alloc(10); sprintf((char*)tmp, (char*)"`sleep %d`", pivot->res_time_base + SLEEP_TEST_ONE); n = req_copy(pivot->req, pivot, 1); APPEND_VECTOR(orig_state, n,tmp ); n->callback = inject_state_manager; n->user_val = 4; async_request(n); n = req_copy(pivot->req, pivot, 1); SET_VECTOR(orig_state, n, tmp); n->callback = inject_state_manager; n->user_val = 5; async_request(n); sprintf((char*)tmp, (char*)"`sleep %d`", pivot->res_time_base + SLEEP_TEST_TWO); n = req_copy(pivot->req, pivot, 1); APPEND_VECTOR(orig_state, n, tmp); n->callback = inject_state_manager; n->user_val = 6; async_request(n); n = req_copy(pivot->req, pivot, 1); SET_VECTOR(orig_state, n, tmp); n->callback = inject_state_manager; n->user_val = 7; async_request(n); tmp = ck_realloc(tmp, 15); sprintf((char*)tmp, (char*)"`sleep${IFS}%d`", pivot->res_time_base + SLEEP_TEST_ONE); n = req_copy(pivot->req, pivot, 1); APPEND_VECTOR(orig_state, n, tmp); n->callback = inject_state_manager; n->user_val = 8; async_request(n); n = req_copy(pivot->req, pivot, 1); SET_VECTOR(orig_state, n, tmp); n->callback = inject_state_manager; n->user_val = 9; async_request(n); sprintf((char*)tmp, (char*)"`sleep${IFS}%d`", pivot->res_time_base + SLEEP_TEST_TWO); n = req_copy(pivot->req, pivot, 1); APPEND_VECTOR(orig_state, n, tmp); n->callback = inject_state_manager; n->user_val = 10; async_request(n); n = req_copy(pivot->req, pivot, 1); SET_VECTOR(orig_state, n, tmp); n->callback = inject_state_manager; n->user_val = 11; async_request(n); ck_free(tmp); return 0; } static u8 inject_shell_check(struct http_request* req, struct http_response* res) { u32 i; DEBUG_MISC_CALLBACK(req, res); /* Look in the first 4 requests to find our concatenated string */ for (i = 0; i < 3; i++) { if (inl_findstr(MRES(i)->payload, (u8*)"skip1234fish", 1024)) problem(PROB_SH_INJECT, MREQ(i), MRES(i), (u8*)"Confirmed shell injection (echo test)", req->pivot, 0); } /* Check that the request was delayed by our sleep. The sleep delay is calculated by using the time_base in order to avoid FPs */ u32 test_one = req->pivot->res_time_base + SLEEP_TEST_ONE; u32 test_two = req->pivot->res_time_base + SLEEP_TEST_TWO; /* Now we check if the request duration was influenced by the sleep. We do this by testing if the total request time was longer (or equal) to: the average request time + the sleep time (3 or 5 seconds). We allow the `sleep` request to take 1 second longer than expected which is the final measure to reduce FPs. */ if ((RTIME(4) >= test_one && RTIME(4) < test_one + 1) && (RTIME(6) >= test_two && RTIME(6) < test_two + 1)) { problem(PROB_SH_INJECT, MREQ(4), MRES(4), (u8*)"Confirmed shell injection (sleep test)", req->pivot, 0); } if ((RTIME(5) >= test_one && RTIME(5) < test_one + 1) && (RTIME(7) >= test_two && RTIME(7) < test_two + 1)) { problem(PROB_SH_INJECT, MREQ(5), MRES(5), (u8*)"Confirmed shell injection (sleep test)", req->pivot, 0); } if ((RTIME(8) >= test_one && RTIME(8) < test_one + 1) && (RTIME(10) >= test_two && RTIME(10) < test_two + 1)) { problem(PROB_SH_INJECT, MREQ(8), MRES(8), (u8*)"Confirmed shell injection (sleep test)", req->pivot, 0); } if ((RTIME(9) >= test_one && RTIME(9) < test_one + 1) && (RTIME(11) >= test_two && RTIME(11) < test_two + 1)) { problem(PROB_SH_INJECT, MREQ(9), MRES(9), (u8*)"Confirmed shell injection (sleep test)", req->pivot, 0); } return 0; } static u8 inject_diff_shell_tests(struct pivot_desc* pivot) { /* Shell command injection - 12 requests. */ u32 orig_state = pivot->state; struct http_request* n; n = req_copy(pivot->req, pivot, 1); APPEND_VECTOR(orig_state, n, "`true`"); n->callback = inject_state_manager; n->user_val = 0; async_request(n); n = req_copy(pivot->req, pivot, 1); APPEND_VECTOR(orig_state, n, "`false`"); n->callback = inject_state_manager; n->user_val = 1; async_request(n); n = req_copy(pivot->req, pivot, 1); APPEND_VECTOR(orig_state, n, "`uname`"); n->callback = inject_state_manager; n->user_val = 2; async_request(n); n = req_copy(pivot->req, pivot, 1); APPEND_VECTOR(orig_state, n, "\"`true`\""); n->callback = inject_state_manager; n->user_val = 3; async_request(n); n = req_copy(pivot->req, pivot, 1); APPEND_VECTOR(orig_state, n, "\"`false`\""); n->callback = inject_state_manager; n->user_val = 4; async_request(n); n = req_copy(pivot->req, pivot, 1); APPEND_VECTOR(orig_state, n, "\"`uname`\""); n->callback = inject_state_manager; n->user_val = 5; async_request(n); n = req_copy(pivot->req, pivot, 1); APPEND_VECTOR(orig_state, n, "'`true`'"); n->callback = inject_state_manager; n->user_val = 6; async_request(n); n = req_copy(pivot->req, pivot, 1); APPEND_VECTOR(orig_state, n, "'`false`'"); n->callback = inject_state_manager; n->user_val = 7; async_request(n); n = req_copy(pivot->req, pivot, 1); APPEND_VECTOR(orig_state, n, "'`uname`'"); n->callback = inject_state_manager; n->user_val = 8; async_request(n); n = req_copy(pivot->req, pivot, 1); SET_VECTOR(orig_state, n, "`true`"); n->callback = inject_state_manager; n->user_val = 9; async_request(n); n = req_copy(pivot->req, pivot, 1); SET_VECTOR(orig_state, n, "`false`"); n->callback = inject_state_manager; n->user_val = 10; async_request(n); n = req_copy(pivot->req, pivot, 1); SET_VECTOR(orig_state, n, "`uname`"); n->callback = inject_state_manager; n->user_val = 11; async_request(n); return 0; } static u8 inject_diff_shell_check(struct http_request* req, struct http_response* res) { DEBUG_MISC_CALLBACK(req, res); /* Got all responses: misc[0] = `true` misc[1] = `false` misc[2] = `uname` misc[3] = "`true`" misc[4] = "`false`" misc[5] = "`uname`" misc[6] = '`true`' misc[7] = "`false`" misc[8] = '`uname`' And a variant that replaces the original values (instead of appending) misc[9] = `true` misc[10] = `false` misc[11] = `uname` If misc[0] == misc[1], but misc[0] != misc[2], we probably have shell injection. Ditto for the remaining triplets. We use the `false` case to avoid errors on search fields, etc. */ if (same_page(&MRES(0)->sig, &MRES(1)->sig) && !same_page(&MRES(1)->sig, &MRES(2)->sig)) { problem(PROB_SH_INJECT, MREQ(1), MRES(1), (u8*)"responses to `true` and `false` different than to `uname`", req->pivot, 0); } if (same_page(&MRES(3)->sig, &MRES(4)->sig) && !same_page(&MRES(4)->sig, &MRES(5)->sig)) { problem(PROB_SH_INJECT, MREQ(3), MRES(3), (u8*)"responses to `true` and `false` different than to `uname`", req->pivot, 0); } if (same_page(&MRES(6)->sig, &MRES(7)->sig) && !same_page(&MRES(6)->sig, &MRES(8)->sig)) { problem(PROB_SH_INJECT, MREQ(6), MRES(6), (u8*)"responses to `true` and `false` different than to `uname`", req->pivot, 0); } if (same_page(&MRES(9)->sig, &MRES(10)->sig) && !same_page(&MRES(10)->sig, &MRES(11)->sig)) { problem(PROB_SH_INJECT, MREQ(9), MRES(9), (u8*)"responses to `true` and `false` different than to `uname`", req->pivot, 0); } return 0; } static u8 inject_xss_tests(struct pivot_desc* pivot) { /* Cross-site scripting - three requests (also test common "special" error pages). */ struct http_request* n; u32 orig_state = pivot->state; u32 i, uval; n = req_copy(pivot->req, pivot, 1); APPEND_VECTOR(orig_state, n, new_xss_tag(NULL)); set_value(PARAM_HEADER, (u8*)"Referer", new_xss_tag(NULL), 0, &n->par); register_xss_tag(n); n->callback = inject_state_manager; n->user_val = 0; async_request(n); n = req_copy(pivot->req, pivot, 1); SET_VECTOR(orig_state, n, new_xss_tag((u8*)".htaccess.aspx")); register_xss_tag(n); n->callback = inject_state_manager; n->user_val = 1; async_request(n); /* A last ones with only header injections. The User-Agent injection doesn't seems to be very useful for reflective XSS scenario's but could reveal persistant XSS problems (i.e. in log / backend interfaces) */ n = req_copy(pivot->req, pivot, 1); set_value(PARAM_HEADER, (u8*)"Referer", new_xss_tag(NULL), 0, &n->par); set_value(PARAM_HEADER, (u8*)"User-Agent", new_xss_tag(NULL), 0, &n->par); register_xss_tag(n); n->callback = inject_state_manager; n->user_val = 2; async_request(n); /* One for testing HTTP_HOST XSS types which are somewhat unlikely but still have abuse potential (e.g. stored XSS') */ n = req_copy(pivot->req, pivot, 1); set_value(PARAM_HEADER, (u8*)"Host", new_xss_tag(NULL), 0, &n->par); register_xss_tag(n); n->callback = inject_state_manager; n->user_val = 3; async_request(n); /* Finally we tests the cookies, one by one to avoid breaking the session */ uval = 3; for (i=0;ireq, pivot, 1); set_value(PARAM_COOKIE, global_http_par.n[i], new_xss_tag(NULL), 0, &n->par); register_xss_tag(n); n->callback = inject_xss_check; n->user_val = ++uval; async_request(n); } return 0; } static u8 inject_xss_check(struct http_request* req, struct http_response* res) { DEBUG_CALLBACK(req, res); if (!req || !res || FETCH_FAIL(res)) return 0; /* Content checks do automatic HTML parsing and XSS detection. scrape_page() is generally not advisable here. This is not a very exiting check and we'll be able to get rid of it in future updated. */ content_checks(req, res); return 0; } static u8 inject_dir_listing_tests(struct pivot_desc* pivot) { struct http_request* n; u8* tmp = NULL; u32 orig_state = pivot->state; /* Directory listing - 4 requests. The logic here is a bit different for parametric targets (which are easy to examine with a ./ trick) and directories (which require a more complex comparison). */ pivot->misc_cnt = 0; n = req_copy(pivot->req, pivot, 1); if (orig_state == PSTATE_CHILD_INJECT) { replace_slash(n, (u8*)"."); set_value(PARAM_PATH, NULL, (u8*)"", -1, &n->par); } else { tmp = ck_alloc(strlen((char*)TPAR(n)) + 5); sprintf((char*)tmp, ".../%s", TPAR(n)); ck_free(TPAR(n)); TPAR(n) = ck_strdup(tmp); } n->callback = inject_state_manager; n->user_val = 0; async_request(n); n = req_copy(pivot->req, pivot, 1); if (orig_state == PSTATE_CHILD_INJECT) { replace_slash(n, (u8*)".sf"); set_value(PARAM_PATH, NULL, (u8*)"", -1, &n->par); } else { ck_free(TPAR(n)); TPAR(n) = ck_strdup(tmp + 2); } n->callback = inject_state_manager; n->user_val = 1; async_request(n); n = req_copy(pivot->req, pivot, 1); if (orig_state == PSTATE_CHILD_INJECT) { replace_slash(n, (u8*)"\\.\\"); } else { tmp[3] = '\\'; ck_free(TPAR(n)); TPAR(n) = ck_strdup(tmp); } n->callback = inject_state_manager; n->user_val = 2; async_request(n); n = req_copy(pivot->req, pivot, 1); if (orig_state == PSTATE_CHILD_INJECT) { replace_slash(n, (u8*)"\\.sf\\"); } else { ck_free(TPAR(n)); TPAR(n) = ck_strdup(tmp + 2); ck_free(tmp); } n->callback = inject_state_manager; n->user_val = 3; async_request(n); return 0; } static u8 inject_dir_listing_check(struct http_request* req, struct http_response* res) { u32 orig_state = req->pivot->state; DEBUG_MISC_CALLBACK(req, res); /* Got all responses. For directories, this is: pivot = / misc[0] = /./ misc[1] = /.sf/ misc[2] = \.\ misc[3] = \.sf\ Here, if pivot != misc[0], and misc[0] != misc[1], we probably managed to list a hidden dir. The same test is carried out for misc[2] and misc[3]. For parameters, this is: misc[0] = .../known_val misc[1] = ./known_val misc[2] = ...\known_val misc[3] = .\known_val Here, the test is simpler: if misc[1] != misc[0], or misc[3] != misc[2], we probably have a bug. The same if misc[4] or misc[5] contain magic strings, but misc[0] doesn't. */ if (orig_state == PSTATE_CHILD_INJECT) { if (MRES(0)->code < 300 && !same_page(&MRES(0)->sig, &RPRES(req)->sig) && !same_page(&MRES(0)->sig, &MRES(1)->sig)) { problem(PROB_DIR_LIST_BYPASS, MREQ(0), MRES(0), (u8*)"unique response for /./", req->pivot, 0); /* Use pivot's request, rather than MREQ(0), for link scraping; MREQ(0) contains an "illegal" manually constructed path. */ RESP_CHECKS(RPREQ(req), MRES(0)); } if (MRES(2)->code < 300 && !same_page(&MRES(2)->sig, &RPRES(req)->sig) && !same_page(&MRES(2)->sig, &MRES(3)->sig)) { problem(PROB_DIR_LIST_BYPASS, MREQ(2), MRES(2), (u8*)"unique response for \\.\\", req->pivot, 0); RESP_CHECKS(MREQ(2), MRES(2)); } } else { if (!same_page(&MRES(0)->sig, &MRES(1)->sig)) { problem(PROB_DIR_TRAVERSAL, MREQ(1), MRES(1), (u8*)"responses for ./val and .../val look different", req->pivot, 0); RESP_CHECKS(MREQ(0), MRES(0)); } if (!same_page(&MRES(2)->sig, &MRES(3)->sig)) { problem(PROB_DIR_TRAVERSAL, MREQ(3), MRES(3), (u8*)"responses for .\\val and ...\\val look different", req->pivot, 0); RESP_CHECKS(MREQ(2), MRES(2)); } } return 0; } static u8 inject_lfi_tests(struct pivot_desc* pivot) { struct http_request* n; u32 i, v; u32 count = 0; /* Perhaps do this in state manager ?*/ if (pivot->state == PSTATE_CHILD_INJECT) return 1; /* We combine the traversal and file disclosure attacks here since the checks are almost identical */ i = 0; while (i <= MAX_LFI_INDEX) { v = 0; while (lfi_tests[i].vectors[v]) { /* ONE: no encoding */ n = req_copy(pivot->req, pivot, 1); n->fuzz_par_enc = (u8*)ENC_NULL; ck_free(TPAR(n)); TPAR(n) = ck_strdup((u8*)lfi_tests[i].vectors[v]); n->callback = inject_state_manager; n->check_subid = i; n->user_val = count++; async_request(n); /* TWO: path encoding used */ n = req_copy(pivot->req, pivot, 1); n->fuzz_par_enc = (u8*)ENC_PATH; ck_free(TPAR(n)); TPAR(n) = ck_strdup((u8*)lfi_tests[i].vectors[v]); n->callback = inject_state_manager; n->check_subid = i; n->user_val = count++; async_request(n); /* THREE: double path encoding */ n = req_copy(pivot->req, pivot, 1); n->fuzz_par_enc = (u8*)ENC_PATH; ck_free(TPAR(n)); TPAR(n) = url_encode_token((u8*)lfi_tests[i].vectors[v], strlen(lfi_tests[i].vectors[v]), (u8*)ENC_PATH); n->callback = inject_state_manager; n->check_subid = i; n->user_val = count++; async_request(n); /* FOUR: path encoding, with NULL byte and extension */ n = req_copy(pivot->req, pivot, 1); ck_free(TPAR(n)); u8 *tmp = url_encode_token((u8*)lfi_tests[i].vectors[v], strlen(lfi_tests[i].vectors[v]), (u8*)ENC_PATH); TPAR(n) = ck_alloc(strlen((char*)tmp) + 10); sprintf((char*)TPAR(n), "%s%%00%%2ejs", (char*)tmp); ck_free(tmp); n->fuzz_par_enc = (u8*)ENC_NULL; n->callback = inject_state_manager; n->check_subid = i; n->user_val = count++; async_request(n); v++; } i++; } u8 *web1 = ck_strdup((u8*)"/WEB-INF/web.xml"); u8 *web2 = ck_strdup((u8*)"/WEB-INF%2fweb%2exml"); u8 *web3 = ck_strdup((u8*)"/WEB-INF%2fweb%2exml%00.js"); for (v=0; v<8; v++) { PREFIX_STRING(web1, "/.."); PREFIX_STRING(web2, "%2f%2e%2e"); PREFIX_STRING(web3, "%2f%2e%2e"); DEBUG("WEB: %s\n", web1); n = req_copy(pivot->req, pivot, 1); n->fuzz_par_enc = (u8*)ENC_NULL; ck_free(TPAR(n)); TPAR(n) = ck_strdup(web1); n->callback = inject_state_manager; n->check_subid = i; n->user_val = count++; async_request(n); n = req_copy(pivot->req, pivot, 1); /* No % encoding for these requests */ n->fuzz_par_enc = (u8*)ENC_NULL; ck_free(TPAR(n)); TPAR(n) = ck_strdup(web2); n->callback = inject_state_manager; n->check_subid = i; n->user_val = count++; async_request(n); n = req_copy(pivot->req, pivot, 1); /* No % encoding for these requests */ n->fuzz_par_enc = (u8*)ENC_NULL; ck_free(TPAR(n)); TPAR(n) = ck_strdup(web3); n->callback = inject_state_manager; n->check_subid = i; n->user_val = count++; async_request(n); } ck_free(web1); ck_free(web2); ck_free(web3); pivot->pending = count; return 0; } static u8 inject_lfi_check(struct http_request* req, struct http_response* res) { DEBUG_MISC_CALLBACK(req, res); u8 found = 0; u32 p = 0; /* Perform directory traveral and file inclusion tests. In every request, the check_subid points to the relevant traversal test string. We look up the string and compare it with the response. Exception to this are the web.xml requests for which the injection strings are dynamically generated. We don't know where the web.xml is located on the file system (unlike /etc/passwd), we cannot use an arbitrary amount of ../'s. Instead, we need to use the exact amount in order to be able to disclose the file. */ for (p=0; p < req->pivot->pending; p++) { if (MREQ(p)->check_subid <= MAX_LFI_INDEX) { /* Test the parent and current response */ if (!inl_findstr(RPRES(req)->payload, (u8*)lfi_tests[MREQ(p)->check_subid].test_string, 1024) && inl_findstr(MRES(p)->payload, (u8*)lfi_tests[MREQ(p)->check_subid].test_string, 1024)) { problem(PROB_FI_LOCAL, MREQ(p), MRES(p), (u8*)lfi_tests[MREQ(p)->check_subid].description, req->pivot, 0); found = 1; } } else if (MREQ(p)->check_subid == MAX_LFI_INDEX + 1) { /* Check the web.xml disclosure */ if (!inl_findstr(RPRES(req)->payload, (u8*)"payload, (u8*)"payload, (u8*)"", 2048) || inl_findstr(MRES(p)->payload, (u8*)"", 2048) || inl_findstr(MRES(p)->payload, (u8*)"", 2048)) { problem(PROB_FI_LOCAL, MREQ(p), MRES(p), (u8*)"response resembles web.xml.", req->pivot, 0); found = 1; } } } } /* If we disclosed something: suppress the more generic traversal warnings by removing the issue. */ if (found) remove_issue(req->pivot, PROB_DIR_TRAVERSAL); return 0; } static u8 inject_rfi_tests(struct pivot_desc* pivot) { struct http_request* n; /* Perhaps do this in state manager ?*/ if (pivot->state == PSTATE_CHILD_INJECT) return 1; n = req_copy(pivot->req, pivot, 1); ck_free(TPAR(n)); TPAR(n) = ck_strdup((u8*)RFI_HOST); n->callback = inject_state_manager; async_request(n); return 0; } static u8 inject_rfi_check(struct http_request* req, struct http_response* res) { DEBUG_CALLBACK(req, res); if (!inl_findstr(RPRES(req)->payload, (u8*)RFI_STRING, 1024) && inl_findstr(res->payload, (u8*)RFI_STRING, 1024)) { problem(PROB_FI_REMOTE, req, res, (u8*)"remote file inclusion", req->pivot, 0); } return 0; } static u8 inject_redir_tests(struct pivot_desc* pivot) { struct http_request* n; u32 orig_state = pivot->state; /* XSS checks - 5 requests */ n = req_copy(pivot->req, pivot, 1); SET_VECTOR(orig_state, n, "http://skipfish.invalid/;?"); n->callback = inject_state_manager; n->user_val = 0; async_request(n); n = req_copy(pivot->req, pivot, 1); SET_VECTOR(orig_state, n, "//skipfish.invalid/;?"); n->callback = inject_state_manager; n->user_val = 1; async_request(n); n = req_copy(pivot->req, pivot, 1); SET_VECTOR(orig_state, n, "skipfish://invalid/;?"); n->callback = inject_state_manager; n->user_val = 2; async_request(n); n = req_copy(pivot->req, pivot, 1); SET_VECTOR(orig_state, n, "'skip'''\"fish\"\"\""); n->callback = inject_state_manager; n->user_val = 3; async_request(n); /* Finally an encoded version which is aimed to detect injection problems in JS handlers, such as onclick, which executes HTML encoded strings. */ n = req_copy(pivot->req, pivot, 1); SET_VECTOR(orig_state, n, "'skip'''"fish""""); n->callback = inject_state_manager; n->user_val = 4; async_request(n); return 0; } static u8 inject_redir_check(struct http_request* req, struct http_response* res) { u8* val; u32 i; DEBUG_MISC_CALLBACK(req, res); /* Check Location, Refresh headers. */ for (i=0; i < req->pivot->misc_cnt; i++) { val = GET_HDR((u8*)"Location", &MRES(i)->hdr); if (val) { if (!case_prefix(val, "http://skipfish.invalid/") || !case_prefix(val, "//skipfish.invalid/")) problem(PROB_URL_REDIR, MREQ(i), MRES(i), (u8*)"injected URL in 'Location' header", req->pivot, 0); if (!case_prefix(val, "skipfish:")) problem(PROB_URL_XSS, MREQ(i), MRES(i), (u8*)"injected URL in 'Location' header", req->pivot, 0); } val = GET_HDR((u8*)"Refresh", &MRES(i)->hdr); if (val && (val = (u8*)strchr((char*)val, '=')) && val++) { u8 semi_safe = 0; if (*val == '\'' || *val == '"') { val++; semi_safe++; } if (!case_prefix(val, "http://skipfish.invalid/") || !case_prefix(val, "//skipfish.invalid/")) problem(PROB_URL_REDIR, MREQ(i), MRES(i), (u8*)"injected URL in 'Refresh' header", req->pivot, 0); /* Unescaped semicolon in Refresh headers is unsafe with MSIE6. */ if (!case_prefix(val, "skipfish:") || (!semi_safe && strchr((char*)val, ';'))) problem(PROB_URL_XSS, MREQ(i), MRES(i), (u8*)"injected URL in 'Refresh' header", req->pivot, 0); } /* META tags and JS will be checked by content_checks(). We're not calling scrape_page(), because we don't want to accumulate bogus, injected links. */ content_checks(MREQ(i), MRES(i)); } return 0; } static u8 inject_split_tests(struct pivot_desc* pivot) { struct http_request* n; u32 orig_state = pivot->state; /* Header splitting - 2 requests */ n = req_copy(pivot->req, pivot, 1); APPEND_VECTOR(orig_state, n, "bogus\nSkipfish-Inject:bogus"); n->callback = inject_state_manager; n->user_val = 0; async_request(n); n = req_copy(pivot->req, pivot, 1); APPEND_VECTOR(orig_state, n, "bogus\rSkipfish-Inject:bogus"); n->callback = inject_state_manager; n->user_val = 1; async_request(n); return 0; } static u8 inject_split_check(struct http_request* req, struct http_response* res) { DEBUG_MISC_CALLBACK(req, res); /* Not differential. */ if (res->state != STATE_OK) { handle_error(req, res, (u8*)"during header injection attacks", 0); return 0; } /* Check headers - that's all! */ if (GET_HDR((u8*)"Skipfish-Inject", &MRES(0)->hdr)) problem(PROB_HTTP_INJECT, MREQ(0), MRES(0), (u8*)"successfully injected 'Skipfish-Inject' header into response", req->pivot, 0); if (GET_HDR((u8*)"Skipfish-Inject", &MRES(1)->hdr)) problem(PROB_HTTP_INJECT, MREQ(1), MRES(1), (u8*)"successfully injected 'Skipfish-Inject' header into response", req->pivot, 0); return 0; } static u8 inject_sql_tests(struct pivot_desc* pivot) { struct http_request* n; u32 orig_state = pivot->state; u8 is_num = 0; /* SQL injection - 10 requests */ if (orig_state != PSTATE_CHILD_INJECT) { u8* pstr = TPAR(pivot->req); u32 c = strspn((char*)pstr, "01234567890.+-"); if (pstr[0] && !pstr[c]) is_num = 1; } n = req_copy(pivot->req, pivot, 1); if (!is_num) SET_VECTOR(orig_state, n, "9-8"); else APPEND_VECTOR(orig_state, n, "-0"); n->callback = inject_state_manager; n->user_val = 0; async_request(n); n = req_copy(pivot->req, pivot, 1); if (!is_num) SET_VECTOR(orig_state, n, "8-7"); else APPEND_VECTOR(orig_state, n, "-0-0"); n->callback = inject_state_manager; n->user_val = 1; async_request(n); n = req_copy(pivot->req, pivot, 1); if (!is_num) SET_VECTOR(orig_state, n, "9-1"); else APPEND_VECTOR(orig_state, n, "-0-9"); n->callback = inject_state_manager; n->user_val = 2; async_request(n); n = req_copy(pivot->req, pivot, 1); APPEND_VECTOR(orig_state, n, "\\\'\\\""); set_value(PARAM_HEADER, (u8*)"User-Agent", (u8*)"sfish\\\'\\\"", 0, &n->par); set_value(PARAM_HEADER, (u8*)"Referer", (u8*)"sfish\\\'\\\"", 0, &n->par); set_value(PARAM_HEADER, (u8*)"Accept-Language", (u8*)"sfish\\\'\\\",en", 0, &n->par); n->callback = inject_state_manager; n->user_val = 3; async_request(n); n = req_copy(pivot->req, pivot, 1); APPEND_VECTOR(orig_state, n, "\'\""); set_value(PARAM_HEADER, (u8*)"User-Agent", (u8*)"sfish\'\"", 0, &n->par); set_value(PARAM_HEADER, (u8*)"Referer", (u8*)"sfish\'\"", 0, &n->par); set_value(PARAM_HEADER, (u8*)"Accept-Language", (u8*)"sfish\'\",en", 0, &n->par); n->callback = inject_state_manager; n->user_val = 4; async_request(n); n = req_copy(pivot->req, pivot, 1); APPEND_VECTOR(orig_state, n, "\\\\\'\\\\\""); set_value(PARAM_HEADER, (u8*)"User-Agent", (u8*)"sfish\\\\\'\\\\\"", 0, &n->par); set_value(PARAM_HEADER, (u8*)"Referer", (u8*)"sfish\\\\\'\\\\\"", 0, &n->par); set_value(PARAM_HEADER, (u8*)"Accept-Language", (u8*)"sfish\\\\\'\\\\\",en", 0, &n->par); n->callback = inject_state_manager; n->user_val = 5; async_request(n); /* This is a special case to trigger fault on blind numerical injection. */ n = req_copy(pivot->req, pivot, 1); if (!is_num) SET_VECTOR(orig_state, n, "9 - 1"); else APPEND_VECTOR(orig_state, n, " - 0 - 0"); n->callback = inject_state_manager; n->user_val = 6; async_request(n); n = req_copy(pivot->req, pivot, 1); if (!is_num) SET_VECTOR(orig_state, n, "9 1 -"); else APPEND_VECTOR(orig_state, n, " 0 0 - -"); n->callback = inject_state_manager; n->user_val = 7; async_request(n); /* Another round of SQL injection checks for a different escaping style. */ n = req_copy(pivot->req, pivot, 1); APPEND_VECTOR(orig_state, n, "''''\"\"\"\""); set_value(PARAM_HEADER, (u8*)"User-Agent", (u8*)"sfish''''\"\"\"\"", 0, &n->par); set_value(PARAM_HEADER, (u8*)"Referer", (u8*)"sfish''''\"\"\"\"", 0, &n->par); set_value(PARAM_HEADER, (u8*)"Accept-Language", (u8*)"sfish''''\"\"\"\",en", 0, &n->par); n->callback = inject_state_manager; n->user_val = 8; async_request(n); n = req_copy(pivot->req, pivot, 1); APPEND_VECTOR(orig_state, n, "'\"'\"'\"'\""); set_value(PARAM_HEADER, (u8*)"User-Agent", (u8*)"sfish'\"'\"'\"'\"", 0, &n->par); set_value(PARAM_HEADER, (u8*)"Referer", (u8*)"sfish'\"'\"'\"'\"", 0, &n->par); set_value(PARAM_HEADER, (u8*)"Accept-Language", (u8*)"sfish'\"'\"'\"'\",en", 0, &n->par); n->callback = inject_state_manager; n->user_val = 9; async_request(n); /* Todo: cookies */ return 0; } static u8 inject_sql_check(struct http_request* req, struct http_response* res) { DEBUG_MISC_CALLBACK(req, res); /* Got all data: misc[0] = 9-8 (or orig-0) misc[1] = 8-7 (or orig-0-0) misc[2] = 9-1 (or orig-0-9) misc[3] = [orig]\'\" misc[4] = [orig]'" misc[5] = [orig]\\'\\" misc[6] = 9 - 1 (or orig - 0 - 0) misc[7] = 9 1 - (or orig 0 0 - -) misc[8] == [orig]''''"""" misc[9] == [orig]'"'"'"'" If misc[0] == misc[1], but misc[0] != misc[2], probable (numeric) SQL injection. Ditto for misc[1] == misc[6], but misc[6] != misc[7]. If misc[3] != misc[4] and misc[3] != misc[5], probable text SQL injection. If misc[4] == misc[9], and misc[8] != misc[9], probable text SQL injection. */ if (same_page(&MRES(0)->sig, &MRES(1)->sig) && !same_page(&MRES(1)->sig, &MRES(2)->sig)) { problem(PROB_SQL_INJECT, MREQ(0), MRES(0), (u8*)"response suggests arithmetic evaluation on server side (type 1)", req->pivot, 0); } if (same_page(&MRES(1)->sig, &MRES(6)->sig) && !same_page(&MRES(6)->sig, &MRES(7)->sig)) { problem(PROB_SQL_INJECT, MREQ(7), MRES(7), (u8*)"response suggests arithmetic evaluation on server side (type 2)", req->pivot, 0); } if (same_page(&MRES(3)->sig, &MRES(4)->sig) && !same_page(&MRES(4)->sig, &MRES(5)->sig)) { problem(PROB_SQL_INJECT, MREQ(4), MRES(4), (u8*)"response to '\" different than to \\'\\\"", req->pivot, 0); } if (same_page(&MRES(4)->sig, &MRES(9)->sig) && !same_page(&MRES(8)->sig, &MRES(9)->sig)) { problem(PROB_SQL_INJECT, MREQ(4), MRES(4), (u8*)"response to ''''\"\"\"\" different than to '\"'\"'\"'\"", req->pivot, 0); } return 0; } static u8 inject_format_tests(struct pivot_desc* pivot) { struct http_request* n; u32 orig_state = pivot->state; /* Format string attacks - 2 requests. */ n = req_copy(pivot->req, pivot, 1); SET_VECTOR(orig_state, n, "sfish%dn%dn%dn%dn%dn%dn%dn%dn"); n->callback = inject_state_manager; n->user_val = 0; async_request(n); n = req_copy(pivot->req, pivot, 1); SET_VECTOR(orig_state, n, "sfish%nd%nd%nd%nd%nd%nd%nd%nd"); n->callback = inject_state_manager; n->user_val = 1; async_request(n); return 0; } static u8 inject_format_check(struct http_request* req, struct http_response* res) { DEBUG_MISC_CALLBACK(req, res); /* Got all data: misc[0] = %dn... (harmless) misc[1] = %nd... (crashy) If misc[0] != misc[1], probable format string vuln. */ if (!same_page(&MRES(0)->sig, &MRES(1)->sig)) { problem(PROB_FMT_STRING, MREQ(1), MRES(1), (u8*)"response to %dn%dn%dn... different than to %nd%nd%nd...", req->pivot, 0); RESP_CHECKS(MREQ(1), MRES(1)); } return 0; } static u8 inject_integer_tests(struct pivot_desc* pivot) { struct http_request* n; u32 orig_state = pivot->state; /* Integer overflow bugs - 9 requests. */ n = req_copy(pivot->req, pivot, 1); SET_VECTOR(orig_state, n, "-0000012345"); n->callback = inject_state_manager; n->user_val = 0; async_request(n); n = req_copy(pivot->req, pivot, 1); SET_VECTOR(orig_state, n, "-2147483649"); n->callback = inject_state_manager; n->user_val = 1; async_request(n); n = req_copy(pivot->req, pivot, 1); SET_VECTOR(orig_state, n, "-2147483648"); n->callback = inject_state_manager; n->user_val = 2; async_request(n); n = req_copy(pivot->req, pivot, 1); SET_VECTOR(orig_state, n, "0000012345"); n->callback = inject_state_manager; n->user_val = 3; async_request(n); n = req_copy(pivot->req, pivot, 1); SET_VECTOR(orig_state, n, "2147483647"); n->callback = inject_state_manager; n->user_val = 4; async_request(n); n = req_copy(pivot->req, pivot, 1); SET_VECTOR(orig_state, n, "2147483648"); n->callback = inject_state_manager; n->user_val = 5; async_request(n); n = req_copy(pivot->req, pivot, 1); SET_VECTOR(orig_state, n, "4294967295"); n->callback = inject_state_manager; n->user_val = 6; async_request(n); n = req_copy(pivot->req, pivot, 1); SET_VECTOR(orig_state, n, "4294967296"); n->callback = inject_state_manager; n->user_val = 7; async_request(n); n = req_copy(pivot->req, pivot, 1); SET_VECTOR(orig_state, n, "0000023456"); n->callback = inject_state_manager; n->user_val = 8; async_request(n); return 0; } static u8 inject_integer_check(struct http_request* req, struct http_response* res) { DEBUG_MISC_CALLBACK(req, res); /* Got all data: misc[0] = -12345 (baseline) misc[1] = -(2^31-1) misc[2] = -2^31 misc[3] = 12345 (baseline) misc[4] = 2^31-1 misc[5] = 2^31 misc[6] = 2^32-1 misc[7] = 2^32 misc[8] = 23456 (validation) If misc[3] != misc[8], skip tests - we're likely dealing with a search field instead. If misc[0] != misc[1] or misc[2], probable integer overflow; ditto for 3 vs 4, 5, 6, 7. */ if (!same_page(&MRES(3)->sig, &MRES(8)->sig)) return 0; if (!same_page(&MRES(0)->sig, &MRES(1)->sig)) { problem(PROB_INT_OVER, MREQ(1), MRES(1), (u8*)"response to -(2^31-1) different than to -12345", req->pivot, 0); RESP_CHECKS(MREQ(1), MRES(1)); } if (!same_page(&MRES(0)->sig, &MRES(2)->sig)) { problem(PROB_INT_OVER, MREQ(2), MRES(2), (u8*)"response to -2^31 different than to -12345", req->pivot, 0); RESP_CHECKS(MREQ(2), MRES(2)); } if (!same_page(&MRES(3)->sig, &MRES(4)->sig)) { problem(PROB_INT_OVER, MREQ(4), MRES(4), (u8*)"response to 2^31-1 different than to 12345", req->pivot, 0); RESP_CHECKS(MREQ(4), MRES(4)); } if (!same_page(&MRES(3)->sig, &MRES(5)->sig)) { problem(PROB_INT_OVER, MREQ(5), MRES(5), (u8*)"response to 2^31 different than to 12345", req->pivot, 0); RESP_CHECKS(MREQ(5), MRES(5)); } if (!same_page(&MRES(3)->sig, &MRES(6)->sig)) { problem(PROB_INT_OVER, MREQ(6), MRES(6), (u8*)"response to 2^32-1 different than to 12345", req->pivot, 0); RESP_CHECKS(MREQ(6), MRES(6)); } if (!same_page(&MRES(3)->sig, &MRES(7)->sig)) { problem(PROB_INT_OVER, MREQ(7), MRES(7), (u8*)"response to 2^32 different than to 12345", req->pivot, 0); RESP_CHECKS(MREQ(7), MRES(7)); } return 0; } static u8 param_behavior_tests(struct pivot_desc* pivot) { struct http_request* n; u32 i; if (pivot->fuzz_par < 0 || !url_allowed(pivot->req) || !param_allowed(pivot->name)) { pivot->state = PSTATE_DONE; maybe_delete_payload(pivot); return 0; } DEBUG_HELPER(pivot); /* Parameter behavior. */ for (i=0;ireq, pivot, 1); ck_free(TPAR(n)); TPAR(n) = ck_strdup((u8*)BOGUS_PARAM); n->callback = inject_state_manager; n->user_val = i; async_request(n); } return 0; } static u8 param_behavior_check(struct http_request* req, struct http_response* res) { u32 i; u32 res_diff; u32 page_diff = 0; DEBUG_MISC_CALLBACK(req, res); for (i=0; ipivot->misc_cnt; i++) { /* Store the biggest response time */ res_diff = MREQ(i)->end_time - MREQ(i)->start_time; if(res_diff > req->pivot->res_time_base) req->pivot->res_time_base = res_diff; /* Compare the page responses */ if (!page_diff && !same_page(&MRES(i)->sig, &RPRES(req)->sig)) page_diff = i; } /* If the largest response time exceeded our threshold, we'll skip the timing related tests */ if(req->pivot->res_time_base > MAX_RES_DURATION) { problem(PROB_VARIES, req, res, (u8*)"Responses too slow for time sensitive tests", req->pivot, 0); req->pivot->res_time_exceeds = 1; } if (page_diff == req->pivot->misc_cnt) { DEBUG("* Parameter seems to have no effect.\n"); req->pivot->bogus_par = 1; return 0; } DEBUG("* Parameter seems to have some effect:\n"); debug_same_page(&res->sig, &RPRES(req)->sig); if (req->pivot->bogus_par) { DEBUG("* We already classified it as having no effect, whoops.\n"); req->pivot->res_varies = 1; problem(PROB_VARIES, req, res, 0, req->pivot, 0); return 0; } /* If we do not have a signature yet, record it. Otherwise, make sure it did not change. */ if (!req->pivot->r404_cnt) { DEBUG("* New signature, recorded.\n"); memcpy(&req->pivot->r404[0], &res->sig, sizeof(struct http_sig)); req->pivot->r404_cnt = 1; } else { if (!same_page(&res->sig, &req->pivot->r404[0])) { DEBUG("* Signature does not match previous responses, whoops.\n"); req->pivot->res_varies = 1; problem(PROB_VARIES, req, res, 0, req->pivot, 0); } return 0; } req->pivot->state = PSTATE_PAR_CHECK; return 0; } /* Request this same URL with different user agents. Detect if the response is different so that additional injection tests can be scheduled. */ static u8 agent_behavior_tests(struct pivot_desc* pivot) { struct http_request* n; u32 i, j = 0; /* Schedule browser specific requests. */ for (i=0; ibrowser) continue; n = req_copy(pivot->req, pivot, 1); n->callback = inject_state_manager; n->browser = browser_types[i]; n->user_val = j++; async_request(n); } return 0; } static u8 agent_behavior_check(struct http_request* req, struct http_response* res) { u32 i; DEBUG_MISC_CALLBACK(req, res); for (i=0; isig, &MRES(i)->sig)) { req->pivot->browsers |= MREQ(i)->browser; maybe_add_pivot(MREQ(i),NULL, 2); } } return 0; } static u8 param_ognl_tests(struct pivot_desc* pivot) { struct http_request* n; u32 ret = 1; u8* tmp; /* All probes failed? Assume bogus parameter, what else to do... */ if (!pivot->r404_cnt) pivot->bogus_par = 1; /* If the parameter has an effect, schedule OGNL checks. */ if (!pivot->bogus_par && !pivot->res_varies && pivot->req->par.n[pivot->fuzz_par]) { n = req_copy(pivot->req, pivot, 1); tmp = ck_alloc(strlen((char*)n->par.n[pivot->fuzz_par]) + 8); sprintf((char*)tmp, "[0]['%s']", n->par.n[pivot->fuzz_par]); ck_free(n->par.n[pivot->fuzz_par]); n->par.n[pivot->fuzz_par] = tmp; n->callback = inject_state_manager; n->user_val = 0; async_request(n); n = req_copy(pivot->req, pivot, 1); ck_free(n->par.n[pivot->fuzz_par]); n->par.n[pivot->fuzz_par] = ck_strdup((u8*)"[0]['sfish']"); n->callback = inject_state_manager; n->user_val = 1; async_request(n); ret = 0; } /* Injection attacks should be carried out even if we think this parameter has no visible effect; but injection checks will not proceed to dictionary fuzzing if bogus_par or res_varies is set. */ pivot->state = PSTATE_PAR_INJECT; return ret; } static u8 param_ognl_check(struct http_request* req, struct http_response* res) { DEBUG_MISC_CALLBACK(req, res); /* First response is meant to give the same result. Second is meant to give a different one. */ if (same_page(&MREQ(0)->pivot->res->sig, &MRES(0)->sig) && !same_page(&MREQ(1)->pivot->res->sig, &MRES(1)->sig)) { problem(PROB_OGNL, req, res, (u8*)"response to [0]['name']=... identical to name=...", req->pivot, 0); } return 0; } static u8 dir_ips_tests(struct pivot_desc* pivot) { struct http_request* n; pivot->state = PSTATE_IPS_CHECK; n = req_copy(pivot->req, pivot, 1); tokenize_path((u8*)IPS_TEST, n, 0); n->callback = inject_state_manager; n->user_val = 0; async_request(n); n = req_copy(pivot->req, pivot, 1); tokenize_path((u8*)IPS_SAFE, n, 0); n->callback = inject_state_manager; n->user_val = 1; async_request(n); return 0; } static u8 dir_ips_check(struct http_request* req, struct http_response* res) { struct pivot_desc* par; DEBUG_MISC_CALLBACK(req, res); par = dir_parent(req->pivot); if (!par || !par->uses_ips) { if (MRES(0)->state != STATE_OK) problem(PROB_IPS_FILTER, MREQ(0), MRES(0), (u8*)"request timed out (could also be a flaky server)", req->pivot, 0); else if (!same_page(&MRES(0)->sig, &MRES(1)->sig)) problem(PROB_IPS_FILTER, MREQ(0), MRES(0), NULL, req->pivot, 0); } else { if (MRES(0)->state == STATE_OK && same_page(&MRES(0)->sig, &MRES(1)->sig)) problem(PROB_IPS_FILTER_OFF, MREQ(0), MRES(0), NULL, req->pivot, 0); } destroy_misc_data(req->pivot, req); req->pivot->state = PSTATE_CHILD_INJECT; return 0; } skipfish-2.10b/src/database.c0000440036502000116100000012337612057375131015105 0ustar heinenneng/* skipfish - database & crawl management -------------------------------------- Author: Michal Zalewski Copyright 2009, 2010, 2011 by Google Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ #define _VIA_DATABASE_C #include #include #include #include #include "debug.h" #include "config.h" #include "types.h" #include "http_client.h" #include "database.h" #include "crawler.h" #include "analysis.h" #include "string-inl.h" struct pivot_desc root_pivot; u8 **deny_urls, /* List of banned URL substrings */ **allow_urls, /* List of required URL substrings */ **allow_domains, /* List of allowed vhosts */ **trust_domains, /* List of trusted vhosts */ **skip_params; /* List of parameters to ignore */ u32 num_deny_urls, num_allow_urls, num_allow_domains, num_trust_domains, num_skip_params; u32 max_depth = MAX_DEPTH, max_children = MAX_CHILDREN, max_descendants = MAX_DESCENDANTS, max_guesses = MAX_GUESSES; u8 dont_add_words; /* No auto dictionary building */ #define KW_SPECIFIC 0 #define KW_GENERIC 1 #define KW_GEN_AUTO 2 struct kw_entry { u8* word; /* Keyword itself */ u32 hit_cnt; /* Number of confirmed sightings */ u8 is_ext; /* Is an extension? */ u8 hit_already; /* Had its hit count bumped up? */ u8 read_only; /* Read-only dictionary? */ u8 class; /* KW_* */ u32 total_age; /* Total age (in scan cycles) */ u32 last_age; /* Age since last hit */ }; static struct kw_entry* keyword[WORD_HASH]; /* Keyword collection (bucketed) */ static u32 keyword_cnt[WORD_HASH]; /* Per-bucket keyword counts */ struct ext_entry { u32 bucket; u32 index; }; static struct ext_entry *wg_extension, /* Extension list */ *ws_extension; static u8 **guess; /* Keyword candidate list */ u32 guess_cnt, /* Number of keyword candidates */ ws_extension_cnt, /* Number of specific extensions */ wg_extension_cnt, /* Number of extensions */ keyword_total_cnt, /* Current keyword count */ keyword_orig_cnt; /* At-boot keyword count */ static u32 cur_xss_id, scan_id; /* Stored XSS manager IDs */ static struct http_request** xss_req; /* Stored XSS manager req cache */ /* Checks descendant counts. */ u8 descendants_ok(struct pivot_desc* pv) { if (pv->child_cnt > max_children) return 0; while (pv) { if (pv->desc_cnt > max_descendants) return 0; pv = pv->parent; } return 1; } void add_descendant(struct pivot_desc* pv) { while (pv) { pv->desc_cnt++; pv = pv->parent; } } /* Maps a parsed URL (in req) to the pivot tree, creating or modifying nodes as necessary, and scheduling them for crawl. This should be called only on requests that were *not* yet retrieved. */ void maybe_add_pivot(struct http_request* req, struct http_response* res, u8 via_link) { struct pivot_desc *cur = NULL; u32 i, par_cnt = 0, path_cnt = 0, last_val_cnt = 0, pno; u8 ends_with_slash = 0; u8* last_val = 0; #ifdef LOG_STDERR u8* url = serialize_path(req, 1, 1); DEBUG("--- New pivot requested: %s (%d,%d)\n", url, via_link, req->browser); ck_free(url); #endif /* LOG_STDERR */ if (!req) FATAL("Invalid request data."); /* Initialize root pivot if not done already. */ if (!root_pivot.type) { root_pivot.type = PIVOT_ROOT; root_pivot.state = PSTATE_DONE; root_pivot.linked = 2; root_pivot.fuzz_par = -1; root_pivot.name = ck_strdup((u8*)"[root]"); } if (!url_allowed(req)) { url_scope++; return; } /* Count the number of path and query parameters in the request. */ for (i=0;ipar.c;i++) { if (QUERY_SUBTYPE(req->par.t[i]) || POST_SUBTYPE(req->par.t[i])) par_cnt++; if (PATH_SUBTYPE(req->par.t[i])) { if (req->par.t[i] == PARAM_PATH && !req->par.n[i] && req->par.v[i] && !req->par.v[i][0]) ends_with_slash = 1; else ends_with_slash = 0; if (req->par.v[i][0]) last_val = req->par.v[i]; path_cnt++; } /* While we're at it, try to learn new keywords. */ if (PATH_SUBTYPE(req->par.t[i]) || QUERY_SUBTYPE(req->par.t[i])) { if (req->par.n[i]) wordlist_confirm_word(req->par.n[i]); wordlist_confirm_word(req->par.v[i]); } } /* Try to find pivot point for the host. */ for (i=0;ireq->host, (char*)req->host) && cur->req->port == req->port && cur->req->proto == req->proto) break; } if (i == root_pivot.child_cnt) { /* No server pivot found, we need to create one. */ cur = ck_alloc(sizeof(struct pivot_desc)); root_pivot.child = ck_realloc(root_pivot.child, (root_pivot.child_cnt + 1) * sizeof(struct pivot_desc*)); root_pivot.child[root_pivot.child_cnt++] = cur; add_descendant(&root_pivot); cur->type = PIVOT_SERV; cur->state = PSTATE_FETCH; cur->linked = 2; cur->fuzz_par = -1; cur->parent = &root_pivot; /* Copy the original request, sans path. Create a dummy root dir entry instead. Derive pivot name by serializing the URL of the associated stub request. */ cur->req = req_copy(req, cur, 0); set_value(PARAM_PATH, NULL, (u8*)"", -1, &cur->req->par); cur->name = serialize_path(cur->req, 1, 0); cur->req->callback = dir_retrieve_check; /* If matching response not provided, schedule request. */ if (res && !par_cnt && path_cnt == 1) { cur->res = res_copy(res); dir_retrieve_check(req, cur->res); } else async_request(cur->req); wordlist_confirm_word(req->host); } /* One way or the other, 'cur' now points to server pivot. Let's walk through all path elements, and follow or create sub-pivots for them. */ pno = 0; for (i=0;ipar.t[pno])) pno++; /* Bail out on the trailing NULL-'' indicator, if present. It is used to denote a directory, and will always be the last path element. */ if (i == path_cnt - 1 && req->par.t[pno] == PARAM_PATH && !req->par.n[pno] && !req->par.v[pno][0]) break; pname = req->par.n[pno] ? req->par.n[pno] : req->par.v[pno]; ccnt = cur->child_cnt; /* Try to find a matching node. */ for (c=0;cchild[c]->name) && (!req->browser || req->browser == cur->child[c]->browser)) { cur = cur->child[c]; if (cur->linked < via_link) cur->linked = via_link; break; } if (c == ccnt) { /* Node not found. We need to create one. */ struct pivot_desc* n; /* Enforce user limits. */ if ((i + 1) >= max_depth || !descendants_ok(cur)) { problem(PROB_LIMITS, req, res, (u8*)"Child node limit exceeded", cur, 0); return; } /* Enforce duplicate name limits as a last-ditch effort to prevent endless recursion. */ if (last_val && !strcmp((char*)last_val, (char*)req->par.v[pno])) last_val_cnt++; if (last_val_cnt > MAX_SAMENAME) { problem(PROB_LIMITS, req, res, (u8*)"Duplicate name recursion limit exceeded", cur, 0); return; } /* Create and link back to parent. */ n = ck_alloc(sizeof(struct pivot_desc)); cur->child = ck_realloc(cur->child, (cur->child_cnt + 1) * sizeof(struct pivot_desc*)); cur->child[cur->child_cnt++] = n; add_descendant(cur); n->parent = cur; n->linked = via_link; n->name = ck_strdup(pname); n->browser = req->browser; /* Copy the original request, then copy over path up to the current point. */ n->req = req_copy(req, n, 0); for (c=0;c<=pno;c++) if (PATH_SUBTYPE(req->par.t[c])) set_value(req->par.t[c], req->par.n[c], req->par.v[c], -1, &n->req->par); /* If name is parametric, indicate which parameter to fuzz. */ if (req->par.n[pno]) n->fuzz_par = n->req->par.c - 1; else n->fuzz_par = -1; /* Do not fuzz out-of-scope or limit exceeded dirs... */ if ((i + 1) == max_depth - 1) n->no_fuzz = 1; if (i != path_cnt - 1) { /* This is not the last path segment, so let's assume a "directory" (hierarchy node, to be more accurate), and schedule directory tests. */ set_value(PARAM_PATH, NULL, (u8*)"", -1, &n->req->par); n->type = PIVOT_DIR; n->req->callback = dir_retrieve_check; if (!url_allowed(n->req)) n->no_fuzz = 2; /* Subdirectory tests require parent directory 404 testing to complete first. If these are still pending, wait a bit. */ if (cur->state > PSTATE_IPS_CHECK) { n->state = PSTATE_FETCH; /* If this actually *is* the last parameter, taking into account the early-out hack mentioned above, and we were offered a response - make use of it and don't schedule a new request. */ if (i == path_cnt - 2 && ends_with_slash && res) { n->res = res_copy(res); dir_retrieve_check(n->req, n->res); } else async_request(n->req); } else n->state = PSTATE_PENDING; } else { /* Last segment. If no parameters, copy response body, mark type as "unknown", schedule extra checks. */ if (!url_allowed(n->req)) n->no_fuzz = 2; if (!par_cnt) { n->type = PIVOT_UNKNOWN; n->res = res_copy(res); n->req->callback = unknown_retrieve_check; if (cur->state > PSTATE_IPS_CHECK) { n->state = PSTATE_FETCH; /* If we already have a response, call the callback directly (it will schedule further requests on its own). */ if (!res) { n->state = PSTATE_FETCH; async_request(n->req); } else unknown_retrieve_check(n->req, n->res); } else n->state = PSTATE_PENDING; } else { /* Parameters found. Assume file, schedule a fetch. */ n->type = PIVOT_FILE; n->req->callback = file_retrieve_check; if (cur->state > PSTATE_IPS_CHECK) { n->state = PSTATE_FETCH; async_request(n->req); } else n->state = PSTATE_PENDING; } } cur = n; } /* At this point, 'cur' points to a newly created or existing node for the path element. If this element is parametric, make sure that its value is on the 'try' list. */ if (req->par.n[pno]) { for (c=0;ctry_cnt;c++) if (cur->try_list[c] && !(is_c_sens(cur) ? strcmp : strcasecmp) ((char*)req->par.v[pno], (char*)cur->try_list[c])) break; /* Not found on the list - try adding. */ if (c == cur->try_cnt) { cur->try_list = ck_realloc(cur->try_list, (cur->try_cnt + 1) * sizeof(u8*)); cur->try_list[cur->try_cnt++] = ck_strdup(req->par.v[pno]); if (cur->state == PSTATE_DONE) param_trylist_start(cur); } } pno++; } /* Phew! At this point, 'cur' points to the final path element, and now, we just need to take care of parameters. Each parameter has its own pivot point, and a full copy of the request - unless on the param_skip list. */ pno = 0; for (i=0;ipar.t[pno]) && !POST_SUBTYPE(req->par.t[pno])) pno++; pname = req->par.n[pno] ? req->par.n[pno] : (u8*)"[blank]"; ccnt = cur->child_cnt; /* Try to find a matching node. */ for (c=0;cchild[c]->name) && (!req->browser || req->browser == cur->child[c]->browser)) { cur = cur->child[c]; if (cur->linked < via_link) cur->linked = via_link; break; } if (c == ccnt) { /* Node not found. We need to create one. */ struct pivot_desc* n; /* Enforce user limits. */ if (!descendants_ok(cur)) { problem(PROB_LIMITS, req, res, (u8*)"Child node limit exceeded", cur, 0); return; } /* Create and link back to parent. */ n = ck_alloc(sizeof(struct pivot_desc)); cur->child = ck_realloc(cur->child, (cur->child_cnt + 1) * sizeof(struct pivot_desc*)); cur->child[cur->child_cnt++] = n; add_descendant(cur); n->parent = cur; n->type = PIVOT_PARAM; n->linked = via_link; n->name = ck_strdup(pname); n->browser = req->browser; /* Copy the original request, in full. Remember not to fuzz file inputs. */ n->req = req_copy(req, n, 1); n->fuzz_par = req->par.t[pno] == PARAM_POST_F ? -1 : pno; n->res = res_copy(res); /* File fetcher does everything we need. */ n->req->callback = file_retrieve_check; if (cur->state > PSTATE_IPS_CHECK) { n->state = PSTATE_FETCH; if (res) file_retrieve_check(n->req, n->res); else async_request(n->req); } else n->state = PSTATE_PENDING; cur = n; } /* Ok, again, 'cur' is at the appropriate node. Make sure the current value is on the 'try' list. */ for (c=0;ctry_cnt;c++) if (cur->try_list[c] && !(is_c_sens(cur) ? strcmp : strcasecmp) ((char*)req->par.v[pno], (char*)cur->try_list[c])) break; /* Not found on the list - try adding. */ if (c == cur->try_cnt) { cur->try_list = ck_realloc(cur->try_list, (cur->try_cnt + 1) * sizeof(u8*)); cur->try_list[cur->try_cnt++] = ck_strdup(req->par.v[pno]); if (cur->state == PSTATE_DONE) param_trylist_start(cur); } /* Parameters are not hierarchical, so go back to the parent node. */ cur = cur->parent; pno++; } /* Done, at last! */ } /* Finds the host-level pivot point for global issues. */ struct pivot_desc* host_pivot(struct pivot_desc* pv) { while (pv->parent && pv->parent->parent) pv = pv->parent; return pv; } /* Gets case sensitivity info from the nearest DIR / SERV node. */ u8 is_c_sens(struct pivot_desc* pv) { while (pv->parent && (pv->type != PIVOT_DIR || pv->type != PIVOT_SERV)) pv = pv->parent; return pv->csens; } /* Lookup an issue title */ u8* lookup_issue_title(u32 id) { u32 i = 0; while(pstructs[i].id && pstructs[i].id != id) i++; return pstructs[i].title; } /* Remove issue(s) of with the specified 'type' */ void remove_issue(struct pivot_desc *pv, u32 type) { u32 i, cnt = 0; struct issue_desc *tmp = NULL; if (!pv->issue_cnt) return; for (i=0; iissue_cnt; i++) { if (pv->issue[i].type == type) { DEBUG("* Removing issue of type: %d\n", type); if (pv->issue[i].req) destroy_request(pv->issue[i].req); if (pv->issue[i].res) destroy_response(pv->issue[i].res); } else { tmp = ck_realloc(tmp, (cnt + 1) * sizeof(struct issue_desc)); tmp[cnt].type = pv->issue[i].type; tmp[cnt].extra = pv->issue[i].extra; tmp[cnt].req = pv->issue[i].req; tmp[cnt].res = pv->issue[i].res; tmp[cnt].sid = pv->issue[i].sid; cnt++; } } ck_free(pv->issue); pv->issue = tmp; pv->issue_cnt = cnt; } void problem(u32 type, struct http_request* req, struct http_response* res, u8* extra, struct pivot_desc* pv, u8 allow_dup) { /* Small wrapper for all those problem() calls that do not need to specify a sid */ register_problem(type, 0, req, res, extra, pv, allow_dup); } /* Registers a problem, if not duplicate (res, extra may be NULL): */ void register_problem(u32 type, u32 sid, struct http_request* req, struct http_response* res, u8* extra, struct pivot_desc* pv, u8 allow_dup) { u32 i; if (pv->type == PIVOT_NONE) FATAL("Uninitialized pivot point"); if (type == PROB_NONE || !req) FATAL("Invalid issue data"); #ifdef LOG_STDERR DEBUG("--- NEW PROBLEM - type: %u, extra: '%s' ---\n", type, extra); #endif /* LOG_STDERR */ /* Check for duplicates */ if (!allow_dup) for (i=0;iissue_cnt;i++) if (type == pv->issue[i].type && !strcmp(extra ? (char*)extra : "", pv->issue[i].extra ? (char*)pv->issue[i].extra : "")) return; pv->issue = ck_realloc(pv->issue, (pv->issue_cnt + 1) * sizeof(struct issue_desc)); pv->issue[pv->issue_cnt].type = type; pv->issue[pv->issue_cnt].extra = extra ? ck_strdup(extra) : NULL; pv->issue[pv->issue_cnt].req = req_copy(req, pv, 1); pv->issue[pv->issue_cnt].res = res_copy(res); pv->issue[pv->issue_cnt].sid = sid; #ifndef LOG_STDERR u8* url = serialize_path(req, 1, 1); u8* title = lookup_issue_title(type); DEBUGC(L2, "\n--- NEW PROBLEM\n"); DEBUGC(L2, " - type: %u, %s\n", type, title); DEBUGC(L2, " - url: %s\n", url); DEBUGC(L3, " - extra: %s\n", extra); ck_free(url); #endif /* LOG_STDERR */ /* Mark copies of half-baked requests as done. */ if (res && res->state < STATE_OK) { pv->issue[pv->issue_cnt].res->state = STATE_OK; ck_free(pv->issue[pv->issue_cnt].res->payload); pv->issue[pv->issue_cnt].res->payload = ck_strdup((u8*)"[...truncated...]\n"); pv->issue[pv->issue_cnt].res->pay_len = 18; } pv->issue_cnt++; /* Propagate parent issue counts. */ do { pv->desc_issue_cnt++; } while ((pv = pv->parent)); } /* Three functions to check if the URL is permitted under current rules (0 = no, 1 = yes): */ u8 url_allowed_host(struct http_request* req) { u32 i; for (i=0;ihost, allow_domains[i]); if (pos && strlen((char*)req->host) == strlen((char*)allow_domains[i]) + (pos - req->host)) return 1; } else if (!strcasecmp((char*)req->host, (char*)allow_domains[i])) return 1; } return 0; } u8 url_trusted_host(struct http_request* req) { u32 i; i = 0; while (always_trust_domains[i]) { if (always_trust_domains[i][0] == '.') { u8* pos = inl_strcasestr(req->host, (u8*)always_trust_domains[i]); if (pos && strlen((char*)req->host) == strlen(always_trust_domains[i]) + (pos - req->host)) return 1; } else if (!strcasecmp((char*)req->host, (char*)always_trust_domains[i])) return 1; i++; } for (i=0;ihost, trust_domains[i]); if (pos && strlen((char*)req->host) == strlen((char*)trust_domains[i]) + (pos - req->host)) return 1; } return 0; } u8 url_allowed(struct http_request* req) { u8* url = serialize_path(req, 1, 0); u32 i; /* Check blacklist first */ for (i=0;icode != sig2->code) return 0; /* One has text and the other hasnt: different page */ if (sig1->has_text != sig2->has_text) return 0; for (i=0;idata[i] - sig2->data[i]; u32 scale = sig1->data[i] + sig2->data[i]; if (abs(diff) > 1 + (scale * FP_T_REL / 100)) if (++bucket_fail > FP_B_FAIL) return 0; total_diff += diff; total_scale += scale; } if (abs(total_diff) > 1 + (total_scale * FP_T_REL / 100)) return 0; return 1; } /* Dumps signature data: */ void dump_signature(struct http_sig* sig) { u32 i; DEBUG("SIG %03d: ", sig->code); for (i=0;idata[i]); DEBUG("\n"); } /* Debugs signature comparison: */ void debug_same_page(struct http_sig* sig1, struct http_sig* sig2) { #ifdef LOG_STDERR u32 i; s32 total_diff = 0; u32 total_scale = 0; dump_signature(sig1); dump_signature(sig2); DEBUG(" "); for (i=0;idata[i] - sig2->data[i]; DEBUG("[%04d] ", diff); } DEBUG("(diff)\n "); for (i=0;idata[i] - sig2->data[i]; u32 scale = sig1->data[i] + sig2->data[i]; if (abs(diff) > 1 + (scale * FP_T_REL / 100)) DEBUG("[FAIL] "); else DEBUG("[pass] "); total_diff += diff; total_scale += scale; } DEBUG("\n "); for (i=0;idata[i] + sig2->data[i]; DEBUG("[%04d] ", (u32)( 1 + (scale * FP_T_REL / 100))); } DEBUG("(allow)\n"); DEBUG("Total diff: %d, scale %d, allow %d\n", abs(total_diff), total_scale, 1 + (u32)(total_scale * FP_T_REL / 100)); #endif /* LOG_STDERR */ } /* Keyword management: */ /* Word hashing helper. */ static inline u32 hash_word(u8* str) { register u32 ret = 0; register u8 cur; if (str) while ((cur=*str)) { ret = ~ret ^ (cur) ^ (cur << 5) ^ (~cur >> 5) ^ (cur << 10) ^ (~cur << 15) ^ (cur << 20) ^ (~cur << 25) ^ (cur << 30); str++; } return ret % WORD_HASH; } /* Adds a new keyword candidate to the global "guess" list. This list is case-sensitive. */ void wordlist_add_guess(u8* text) { u32 target, i, kh; if (dont_add_words) return; /* Check if this is a bad or known guess or keyword. */ if (!text || !text[0] || strlen((char*)text) > MAX_WORD) return; for (i=0;i= max_guesses) target = R(max_guesses); else target = guess_cnt++; ck_free(guess[target]); guess[target] = ck_strdup(text); } /* Adds a single, sanitized keyword to the list, or increases its hit count. Keyword list is case-sensitive. */ static void wordlist_confirm_single(u8* text, u8 is_ext, u8 class, u8 read_only, u32 add_hits, u32 total_age, u32 last_age) { u32 kh, i; if (!text || !text[0] || strlen((char*)text) > MAX_WORD) return; /* Check if this is a known keyword. */ kh = hash_word(text); for (i=0;i 4) return; if (ppos != -1) { /* Period only? Too long? */ if (tlen == 1 || tlen - ppos > 12) return; if (ppos && ppos != tlen - 1 && !isdigit(text[ppos + 1])) { wordlist_confirm_single(text + ppos + 1, 1, KW_GEN_AUTO, 0, 1, 0, 0); text[ppos] = 0; wordlist_confirm_single(text, 0, KW_GEN_AUTO, 0, 1, 0, 0); text[ppos] = '.'; return; } } wordlist_confirm_single(text, 0, KW_GEN_AUTO, 0, 1, 0, 0); } /* Returns wordlist item at a specified offset (NULL if no more available). */ u8* wordlist_get_word(u32 offset, u8* specific) { u32 cur_off = 0, kh; for (kh=0;kh offset) break; cur_off += keyword_cnt[kh]; } if (kh == WORD_HASH) return NULL; *specific = (keyword[kh][offset - cur_off].is_ext == 0 && keyword[kh][offset - cur_off].class == KW_SPECIFIC); return keyword[kh][offset - cur_off].word; } /* Returns keyword candidate at a specified offset (or NULL). */ u8* wordlist_get_guess(u32 offset, u8* specific) { if (offset >= guess_cnt) return NULL; *specific = 0; return guess[offset]; } /* Returns extension at a specified offset (or NULL). */ u8* wordlist_get_extension(u32 offset, u8 specific) { if (!specific) { if (offset >= wg_extension_cnt) return NULL; return keyword[wg_extension[offset].bucket][wg_extension[offset].index].word; } if (offset >= ws_extension_cnt) return NULL; return keyword[ws_extension[offset].bucket][ws_extension[offset].index].word; } /* Loads keywords from file. */ void load_keywords(u8* fname, u8 read_only, u32 purge_age) { FILE* in; u32 hits, total_age, last_age, lines = 0; u8 type[3]; s32 fields; u8 kword[MAX_WORD + 1]; char fmt[32]; kword[MAX_WORD] = 0; in = fopen((char*)fname, "r"); if (!in) { if (read_only) PFATAL("Unable to open read-only wordlist '%s'.", fname); else PFATAL("Unable to open read-write wordlist '%s' (see doc/dictionaries.txt).", fname); } sprintf(fmt, "%%2s %%u %%u %%u %%%u[^\x01-\x1f]", MAX_WORD); wordlist_retry: while ((fields = fscanf(in, fmt, type, &hits, &total_age, &last_age, kword)) == 5) { u8 class = KW_GEN_AUTO; if (type[0] != 'e' && type[0] != 'w') FATAL("Wordlist '%s': bad keyword type in line %u.\n", fname, lines + 1); if (type[1] == 's') class = KW_SPECIFIC; else if (type[1] == 'g') class = KW_GENERIC; if (!purge_age || last_age < purge_age) wordlist_confirm_single(kword, (type[0] == 'e'), class, read_only, hits, total_age + 1, last_age + 1); lines++; fgetc(in); /* sink \n */ } if (fields == 1 && !strcmp((char*)type, "#r")) { DEBUG("Found %s (readonly:%d)\n", type, read_only); if (!read_only) FATAL("Attempt to load read-only wordlist '%s' via -W (use -S instead).\n", fname); fgetc(in); /* sink \n */ goto wordlist_retry; } if (fields != -1 && fields != 5) FATAL("Wordlist '%s': syntax error in line %u.\n", fname, lines); if (!lines && (read_only || !keyword_total_cnt)) WARN("Wordlist '%s' contained no valid entries.", fname); DEBUG("* Read %d lines from dictionary '%s' (read-only = %d).\n", lines, fname, read_only); keyword_orig_cnt = keyword_total_cnt; fclose(in); } /* qsort() callback for sorting keywords in save_keywords(). */ static int keyword_sorter(const void* word1, const void* word2) { if (((struct kw_entry*)word1)->hit_cnt < ((struct kw_entry*)word2)->hit_cnt) return 1; else if (((struct kw_entry*)word1)->hit_cnt == ((struct kw_entry*)word2)->hit_cnt) return 0; else return -1; } /* Saves all keywords to a file. */ void save_keywords(u8* fname) { struct stat st; FILE* out; s32 fd; u32 i, kh; u8* old; #ifndef O_NOFOLLOW #define O_NOFOLLOW 0 #endif /* !O_NOFOLLOW */ /* Don't save keywords for /dev/null and other weird files. */ if (stat((char*)fname, &st) || !S_ISREG(st.st_mode)) return; /* First, sort the list. */ for (kh=0;khtype) { case PIVOT_SERV: pivot_serv++; /* Fall through */ case PIVOT_DIR: pivot_dir++; break; case PIVOT_FILE: pivot_file++; break; case PIVOT_PATHINFO: pivot_pinfo++; break; case PIVOT_UNKNOWN: pivot_unknown++; break; case PIVOT_PARAM: pivot_param++; break; case PIVOT_VALUE: pivot_value++; break; } if (pv->missing) pivot_missing++; switch (pv->state) { case PSTATE_PENDING: pivot_pending++; break; case PSTATE_FETCH ... PSTATE_IPS_CHECK: pivot_init++; break; case PSTATE_CHILD_INJECT: case PSTATE_PAR_INJECT: pivot_attack++; break; case PSTATE_DONE: pivot_done++; break; default: pivot_bf++; } for (i=0;iissue_cnt;i++) issue_cnt[PSEV(pv->issue[i].type)]++; for (i=0;ichild_cnt;i++) pv_stat_crawl(pv->child[i]); } void database_stats() { pivot_pending = pivot_init = pivot_attack = pivot_bf = pivot_pinfo = pivot_done = pivot_serv = pivot_dir = pivot_file = pivot_param = pivot_value = pivot_missing = pivot_unknown = pivot_cnt = 0; memset(issue_cnt, 0, sizeof(issue_cnt)); pv_stat_crawl(&root_pivot); SAY(cLBL "Database statistics:\n\n" cGRA " Pivots : " cNOR "%u total, %u done (%.02f%%) \n" cGRA " In progress : " cNOR "%u pending, %u init, %u attacks, " "%u dict \n" cGRA " Missing nodes : " cNOR "%u spotted\n" cGRA " Node types : " cNOR "%u serv, %u dir, %u file, %u pinfo, " "%u unkn, %u par, %u val\n" cGRA " Issues found : " cNOR "%u info, %u warn, %u low, %u medium, " "%u high impact\n" cGRA " Dict size : " cNOR "%u words (%u new), %u extensions, " "%u candidates\n" cGRA " Signatures : " cNOR "%u total\n", pivot_cnt, pivot_done, pivot_cnt ? ((100.0 * pivot_done) / (pivot_cnt)) : 0, pivot_pending, pivot_init, pivot_attack, pivot_bf, pivot_missing, pivot_serv, pivot_dir, pivot_file, pivot_pinfo, pivot_unknown, pivot_param, pivot_value, issue_cnt[1], issue_cnt[2], issue_cnt[3], issue_cnt[4], issue_cnt[5], keyword_total_cnt, keyword_total_cnt - keyword_orig_cnt, wg_extension_cnt, guess_cnt, slist_cnt); } /* Dumps pivot database, for debugging purposes. */ void dump_pivots(struct pivot_desc* cur, u8 nest) { u8* indent = ck_alloc(nest + 1); u8* url; u32 i; if (!cur) cur = &root_pivot; memset(indent, ' ', nest); SAY(cBRI "\n%s== Pivot " cLGN "%s" cBRI " [%d] ==\n", indent, cur->name, cur->dupe); SAY(cGRA "%sType : " cNOR, indent); switch (cur->type) { case PIVOT_NONE: SAY(cLRD "PIVOT_NONE (bad!)\n" cNOR); break; case PIVOT_ROOT: SAY("PIVOT_ROOT\n"); break; case PIVOT_SERV: SAY("PIVOT_SERV\n"); break; case PIVOT_DIR: SAY("PIVOT_DIR\n"); break; case PIVOT_FILE: SAY("PIVOT_FILE\n"); break; case PIVOT_PATHINFO: SAY("PIVOT_PATHINFO\n"); break; case PIVOT_VALUE: SAY("PIVOT_VALUE\n"); break; case PIVOT_UNKNOWN: SAY("PIVOT_UNKNOWN\n"); break; case PIVOT_PARAM: SAY("PIVOT_PARAM\n"); break; default: SAY(cLRD " (bad!)\n" cNOR, cur->type); } SAY(cGRA "%sState : " cNOR, indent); switch (cur->state) { case PSTATE_NONE: SAY(cLRD "PSTATE_NONE (bad!)\n" cNOR); break; case PSTATE_PENDING: SAY("PSTATE_PENDING\n"); break; case PSTATE_FETCH: SAY("PSTATE_FETCH\n"); break; case PSTATE_TYPE_CHECK: SAY("PSTATE_TYPE_CHECK\n"); break; case PSTATE_404_CHECK: SAY("PSTATE_404_CHECK\n"); break; case PSTATE_PARENT_CHECK: SAY("PSTATE_PARENT_CHECK\n"); break; case PSTATE_IPS_CHECK: SAY("PSTATE_IPS_CHECK\n"); break; case PSTATE_CHILD_INJECT: SAY("PSTATE_CHILD_INJECT\n"); break; case PSTATE_CHILD_DICT: SAY("PSTATE_CHILD_DICT\n"); break; case PSTATE_PAR_CHECK: SAY("PSTATE_PAR_CHECK\n"); break; case PSTATE_PAR_INJECT: SAY("PSTATE_PAR_INJECT\n"); break; case PSTATE_PAR_NUMBER: SAY("PSTATE_PAR_NUMBER\n"); break; case PSTATE_PAR_DICT: SAY("PSTATE_PAR_DICT\n"); break; case PSTATE_PAR_TRYLIST: SAY("PSTATE_PAR_TRYLIST\n"); break; case PSTATE_DONE: SAY("PSTATE_DONE\n"); break; default: SAY(cLRD " (bad!)\n" cNOR, cur->state); } if (cur->missing) { if (cur->linked == 2) SAY(cGRA "%sMissing : " cMGN "YES\n" cNOR, indent); else SAY(cGRA "%sMissing : " cLBL "YES (followed a dodgy link)\n" cNOR, indent); } SAY(cGRA "%sFlags : " cNOR "linked %u, case %u/%u, fuzz_par %d, ips %u, " "sigs %u, reqs %u, desc %u/%u\n", indent, cur->linked, cur->csens, cur->c_checked, cur->fuzz_par, cur->uses_ips, cur->r404_cnt, cur->pending, cur->child_cnt, cur->desc_cnt); if (cur->req) { url = serialize_path(cur->req, 1, 0); SAY(cGRA "%sTarget : " cNOR "%s (" cYEL "%d" cNOR ")\n", indent, url, cur->res ? cur->res->code : 0); ck_free(url); if (cur->res) SAY(cGRA "%sMIME : " cNOR "%s -> %s [" "%s:%s]\n", indent, cur->res->header_mime ? cur->res->header_mime : (u8*)"-", cur->res->sniffed_mime ? cur->res->sniffed_mime : (u8*)"-", cur->res->header_charset ? cur->res->header_charset : (u8*)"-", cur->res->meta_charset ? cur->res->meta_charset : (u8*)"-"); } if (cur->try_cnt) { SAY(cGRA "%sTry : " cNOR, indent); for (i=0;itry_cnt;i++) SAY("%s%s", cur->try_list[i], (i == cur->try_cnt - 1) ? "" : ", "); SAY("\n"); } /* Dump issues. */ for (i=0;iissue_cnt;i++) { if (cur->issue[i].req) url = serialize_path(cur->issue[i].req, 0, 0); else url = ck_strdup((u8*)"[none]"); SAY(cGRA "%s-> Issue : " cNOR "type %d, extra '%s', URL: " cLGN "%s" cNOR " (" cYEL "%u" cNOR ")\n", indent, cur->issue[i].type, cur->issue[i].extra, url, cur->issue[i].res ? cur->issue[i].res->code : 0); ck_free(url); } ck_free(indent); for (i=0;ichild_cnt;i++) dump_pivots(cur->child[i], nest + 1); } /* Cleans up pivot structure for memory debugging. */ static void dealloc_pivots(struct pivot_desc* cur) { u32 i; if (!cur) cur = &root_pivot; if (cur->req) destroy_request(cur->req); if (cur->res) destroy_response(cur->res); ck_free(cur->name); if (cur->try_cnt) { for (i=0;itry_cnt;i++) ck_free(cur->try_list[i]); ck_free(cur->try_list); } if (cur->issue) { for (i=0;iissue_cnt;i++) { ck_free(cur->issue[i].extra); if (cur->issue[i].req) destroy_request(cur->issue[i].req); if (cur->issue[i].res) destroy_response(cur->issue[i].res); } ck_free(cur->issue); } for (i=0;ichild_cnt;i++) dealloc_pivots(cur->child[i]); ck_free(cur->child); if (cur != &root_pivot) ck_free(cur); } /* Creates a new XSS location tag. */ u8* new_xss_tag(u8* prefix) { static u8* ret; if (ret) __DFL_ck_free(ret); ret = __DFL_ck_alloc((prefix ? strlen((char*)prefix) : 0) + 32); if (!scan_id) scan_id = R(999999) + 1; sprintf((char*)ret, "%s-->\">'>'\"", prefix ? prefix : (u8*)"", cur_xss_id, scan_id); return ret; } /* Registers last XSS tag along with a completed http_request */ void register_xss_tag(struct http_request* req) { xss_req = ck_realloc(xss_req, (cur_xss_id + 1) * (sizeof(struct http_request*))); xss_req[cur_xss_id] = req_copy(req, 0, 1); cur_xss_id++; } /* Gets the request that submitted the tag in the first place */ struct http_request* get_xss_request(u32 xid, u32 sid) { if (sid != scan_id || xid >= cur_xss_id) return NULL; return xss_req[xid]; } /* Cleans up other database entries, for memory profiling purposes. */ void destroy_database() { u32 i, kh; dealloc_pivots(0); ck_free(deny_urls); ck_free(allow_urls); ck_free(allow_domains); ck_free(trust_domains); ck_free(addl_form_name); ck_free(addl_form_value); ck_free(skip_params); for (kh=0;kh Copyright 2009, 2010, 2011 by Google Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ #ifndef _HAVE_CONFIG_H #define _HAVE_CONFIG_H #define USE_COLOR 1 /* Use terminal colors */ #define SHOW_SPLASH 1 /* Annoy user with a splash screen */ /* Default paths to runtime files: */ #define ASSETS_DIR "assets" /* Default signature file */ #define SIG_FILE "signatures/signatures.conf" /* Various default settings for HTTP client (cmdline override): */ #define MAX_CONNECTIONS 40 /* Simultaneous connection cap */ #define MAX_CONN_HOST 10 /* Per-host connction cap */ #define MAX_REQUESTS 1e8 /* Total request count cap */ #define MAX_REQUESTS_SEC 0.0 /* Max requests per second */ #define MAX_FAIL 100 /* Max consecutive failed requests */ #define RW_TMOUT 10 /* Individual network R/W timeout */ #define RESP_TMOUT 20 /* Total request time limit */ #define IDLE_TMOUT 10 /* Connection tear down threshold */ #define SIZE_LIMIT 400000 /* Response size cap */ #define MAX_GUESSES 256 /* Guess-based wordlist size limit */ /* HTTP client constants: */ #define MAX_URL_LEN 2048 /* Maximum length of an URL */ #define MAX_DNS_LEN 255 /* Maximum length of a host name */ #define READ_CHUNK 4096 /* Read buffer size */ /* Define this to use FILO, rather than FIFO, scheduling for new requests. FILO ensures a more uniform distribution of requests when fuzzing multiple directories at once, but may reduce the odds of spotting some stored XSSes, and increase memory usage a bit. */ // #define QUEUE_FILO 1 /* Define this to enable experimental HTTP proxy support, through the -J option in the command line. This mode will not work as expected for HTTPS requests at this time. */ // #define PROXY_SUPPORT 1 /* Dummy file to upload to the server where possible. */ #define DUMMY_EXT "gif" #define DUMMY_FILE "GIF89a,\x01" #define DUMMY_MIME "image/gif" /* Allocator settings: */ #define MAX_ALLOC 0x50000000 /* Refuse larger allocations. */ /* Detect use-after-free, at the expense of some performance cost: */ #ifdef DEBUG_ALLOCATOR #define CHECK_UAF 1 #endif /* DEBUG_ALLOCATOR */ /* Configurable settings for crawl database (cmdline override): */ #define MAX_DEPTH 16 /* Maximum crawl tree depth */ #define MAX_CHILDREN 512 /* Maximum children per tree node */ #define MAX_DESCENDANTS 8192 /* Maximum descendants per branch */ #define MAX_SAMENAME 3 /* Identically named path nodes */ /* Crawl / analysis constants: */ #define MAX_WORD 64 /* Maximum wordlist item length */ #define GUESS_PROB 50 /* Guess word addition probability */ #define WORD_HASH 256 /* Hash table for wordlists */ #define SNIFF_LEN 1024 /* MIME sniffing buffer size */ #define MAX_SAMPLES 1024 /* Max issue / MIME samples */ #define MAX_JS_WHITE 16 /* Maximum JS wspaces before id */ /* Page fingerprinting constants: */ #define FP_SIZE 10 /* Page fingerprint size */ #define FP_MAX_LEN 15 /* Maximum word length to count */ #define FP_T_REL 5 /* Relative matching tolerance (%) */ #define FP_T_ABS 6 /* Absolute matching tolerance */ #define FP_B_FAIL 3 /* Max number of failed buckets */ #define BH_CHECKS 15 /* Page verification check count */ /* Crawler / probe constants: */ #define BOGUS_FILE "sfi9876" /* Name that should not exist */ #define BOGUS_EXT "sfish" /* Nonsensical file extension */ #define BOGUS_PARAM "9876sfi" /* Meaningless parameter */ #define MAX_404 4 /* Maximum number of 404 sigs */ #define PAR_MAX_DIGITS 6 /* Max digits in a fuzzable int */ #define PAR_INT_FUZZ 100 /* Fuzz by + / - this much */ #ifdef QUEUE_FILO #define DICT_BATCH 100 /* Brute-force queue block */ #else #define DICT_BATCH 300 /* Brute-force queue block */ #endif /* ^QUEUE_FILO */ /* Single query for IPS detection - Evil Query of Doom (tm). */ #define IPS_TEST \ "?_test1=c:\\windows\\system32\\cmd.exe" \ "&_test2=/etc/passwd" \ "&_test3=|/bin/sh" \ "&_test4=(SELECT * FROM nonexistent) --" \ "&_test5=>/no/such/file" \ "&_test6=" \ "&_test7=javascript:alert(1)" /* A benign query with a similar character set to compare with EQoD. */ #define IPS_SAFE \ "?_test1=ccddeeeimmnossstwwxy.:\\\\\\" \ "&_test2=acdepsstw//" \ "&_test3=bhins//" \ "&_test4=CEEFLMORSTeeinnnosttx-*" \ "&_test5=cefhilnosu///" \ "&_test6=acceiilpprrrssttt1)(" \ "&_test7=aaaceijlprrsttv1):(" /* XSRF token detector settings: */ #define XSRF_B16_MIN 8 /* Minimum base10/16 token length */ #define XSRF_B16_MAX 45 /* Maximum base10/16 token length */ #define XSRF_B16_NUM 2 /* ...minimum digit count */ #define XSRF_B64_MIN 6 /* Minimum base32/64 token length */ #define XSRF_B64_MAX 32 /* Maximum base32/64 token length */ #define XSRF_B64_NUM 1 /* ...minimum digit count && */ #define XSRF_B64_CASE 2 /* ...minimum uppercase count */ #define XSRF_B64_NUM2 3 /* ...digit count override */ #define XSRF_B64_SLASH 2 /* ...maximum slash count */ #ifdef _VIA_CHECKS_C /* The URL and string we use in the RFI test (disabled by default) */ #define RFI_HOST "http://www.google.com/humans.txt#foo=" #define RFI_STRING "we can shake a stick" #endif /* _VIA_CHECKS_C */ #ifdef _VIA_DATABASE_C /* Domains we always trust (identical to -B options). These entries do not generate cross-domain content inclusion warnings. NULL-terminated. */ static const char* always_trust_domains[] = { ".google-analytics.com", ".googleapis.com", ".googleadservices.com", ".googlesyndication.com", "www.w3.org", 0 }; #endif /* _VIA_DATABASE_C */ #ifdef _VIA_ANALYSIS_C /* NULL-terminated list of JSON-like response prefixes we consider to be sufficiently safe against cross-site script inclusion (courtesy ratproxy). */ static const char* json_safe[] = { "while(1);", /* Parser looping */ "while (1);", /* ... */ "while(true);", /* ... */ "while (true);", /* ... */ "&&&", /* Parser breaking */ "//OK[", /* Line commenting */ "{\"", /* Serialized object */ "{{\"", /* Serialized object */ "throw 1; <", /* Magical combo */ ")]}'", /* Recommended magic */ 0 }; /* NULL-terminated list of known valid charsets. Charsets not on the list are considered dangerous (as they may trigger charset sniffing). Note that many common misspellings, such as "utf8", are not valid and NOT RECOGNIZED by browsers, leading to content sniffing. Do not add them here. Also note that SF does not support encoding not compatible with US ASCII transport (e.g., UTF-16, UTF-32). Lastly, variable-length encodings other than utf-8 may have character consumption issues that are not tested for at this point. */ static const char* valid_charsets[] = { "utf-8", /* Valid 8-bit safe Unicode */ "iso8859-1", /* Western Europe */ "iso8859-2", /* Central Europe */ "iso8859-15", /* New flavor of ISO8859-1 */ "iso8859-16", /* New flavor of ISO8859-2 */ "iso-8859-1", /* Browser-supported misspellings */ "iso-8859-2", /* - */ "iso-8859-15", /* - */ "iso-8859-16", /* - */ "windows-1252", /* Microsoft's Western Europe */ "windows-1250", /* Microsoft's Central Europe */ "us-ascii", /* Old school but generally safe */ "koi8-r", /* 8-bit and US ASCII compatible */ 0 }; /* Default form auto-fill rules - used to pair up form fields with fun values! Do not attempt security attacks here, though - this is to maximize crawl coverage, not to exploit anything. The last item must have a name of NULL, and the value will be used as a default option when no other matches found. */ static const char* form_suggestion[][2] = { { "phone" , "6505550100" }, /* Reserved */ { "zip" , "94043" }, { "first" , "John" }, { "last" , "Smith" }, { "name" , "Smith" }, { "mail" , "skipfish@example.com" }, { "street" , "1600 Amphitheatre Pkwy" }, { "city" , "Mountain View" }, { "state" , "CA" }, { "country" , "US" }, { "language" , "en" }, { "company" , "ACME" }, { "search" , "skipfish" }, { "login" , "skipfish" }, { "user" , "skipfish" }, { "nick" , "skipfish" }, { "pass" , "skipfish" }, { "pwd" , "skipfish" }, { "year" , "2010" }, { "card" , "4111111111111111" }, /* Reserved */ { "code" , "000" }, { "cvv" , "000" }, { "expir" , "1212" }, { "ssn" , "987654320" }, /* Reserved */ { "url" , "http://example.com/?sfish_form_test" }, { "site" , "http://example.com/?sfish_form_test" }, { "domain" , "example.com" }, { "search" , "a" }, { "comment" , "skipfish" }, { "desc" , "skipfish" }, { "title" , "skipfish" }, { "subject" , "skipfish" }, { "message" , "skipfish" }, { NULL , "1" } }; #endif /* _VIA_ANALYSIS_C */ #endif /* ! _HAVE_CONFIG_H */ skipfish-2.10b/src/database.h0000440036502000116100000006071212057375131015104 0ustar heinenneng/* skipfish - database & crawl management -------------------------------------- Author: Michal Zalewski Copyright 2009, 2010, 2011 by Google Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ #ifndef _HAVE_DATABASE_H #define _HAVE_DATABASE_H #include "debug.h" #include "config.h" #include "types.h" #include "http_client.h" /* Testing pivot points - used to organize the scan: */ /* - Pivot types: */ #define PIVOT_NONE 0 /* Invalid */ #define PIVOT_ROOT 1 /* Root pivot */ #define PIVOT_SERV 2 /* Top-level host pivot */ #define PIVOT_DIR 4 /* Directory pivot */ #define PIVOT_FILE 8 /* File pivot */ #define PIVOT_PATHINFO 16 /* PATH_INFO script */ #define PIVOT_UNKNOWN 32 /* (Currently) unknown type */ #define PIVOT_PARAM 64 /* Parameter fuzzing pivot */ #define PIVOT_VALUE 128 /* Parameter value pivot */ /* - Pivot states (initialized to PENDING or FETCH by database.c, then advanced by crawler.c): */ #define PSTATE_NONE 0 /* Invalid */ #define PSTATE_PENDING 1 /* Pending parent tests */ #define PSTATE_FETCH 10 /* Initial data fetch */ #define PSTATE_TYPE_CHECK 20 /* Type check (unknown only) */ #define PSTATE_404_CHECK 22 /* 404 check (dir only) */ #define PSTATE_PARENT_CHECK 24 /* Parent check (dir only) */ #define PSTATE_IPS_CHECK 26 /* IPS filtering check */ /* For directories only (injecting children nodes): */ #define PSTATE_CHILD_INJECT 50 /* Common security attacks */ #define PSTATE_CHILD_DICT 55 /* Dictionary brute-force */ /* For parametric nodes only (replacing parameter value): */ #define PSTATE_PAR_CHECK 60 /* Parameter works at all? */ #define PSTATE_PAR_INJECT 65 /* Common security attacks */ #define PSTATE_PAR_NUMBER 70 /* Numeric ID traversal */ #define PSTATE_PAR_DICT 75 /* Dictionary brute-force */ #define PSTATE_PAR_TRYLIST 99 /* 'Try list' fetches */ #define PSTATE_DONE 100 /* Analysis done */ /* - Descriptor of a pivot point: */ struct pivot_desc { u8 type; /* PIVOT_* */ u8 state; /* PSTATE_* */ u8 linked; /* Linked to? (0/1/2) */ u8 missing; /* Determined to be missing? */ u8 csens; /* Case sensitive names? */ u8 c_checked; /* csens check done? */ u8* name; /* Directory / script name */ struct http_request* req; /* Prototype HTTP request */ u8 browsers; /* Discovered user-agents */ u8 browser; /* The used user-agent */ s32 fuzz_par; /* Fuzz target parameter */ u8** try_list; /* Values to try */ u32 try_cnt; /* Number of values to try */ u32 try_cur; /* Last tested try list offs */ struct pivot_desc* parent; /* Parent pivot, if any */ struct pivot_desc** child; /* List of children */ u32 child_cnt; /* Number of children */ u32 desc_cnt; /* Number of descendants */ struct issue_desc* issue; /* List of issues found */ u32 issue_cnt; /* Number of issues */ u32 desc_issue_cnt; /* Number of child issues */ struct http_response* res; /* HTTP response seen */ u8 res_varies; /* Response varies? */ u8 bad_parent; /* Parent is well-behaved? */ u8 res_time_exceeds; /* Response time too long? */ u32 res_time_base; /* Base response time */ /* Fuzzer and probe state data: */ u8 no_fuzz; /* Do not attepmt fuzzing. */ u8 sure_dir; /* Very sure it's a dir? */ u8 uses_ips; /* Uses IPS filtering? */ u32 cur_key; /* Current keyword */ u32 pdic_cur_key; /* ...for param dict */ u8 guess; /* Guess list keywords? */ u8 pdic_guess; /* ...for param dict */ u32 pending; /* Number of pending reqs */ u32 pdic_pending; /* ...for param dict */ u32 num_pending; /* ...for numerical enum */ u32 try_pending; /* ...for try list */ u32 r404_pending; /* ...for 404 probes */ u32 ck_pending; /* ...for behavior checks */ s32 check_idx; /* Current test index */ u32 check_state; /* Current test state */ u32 check_id; /* Current test id */ struct http_sig r404[MAX_404]; /* 404 response signatures */ u32 r404_cnt; /* Number of sigs collected */ struct http_sig unk_sig; /* Original "unknown" sig. */ /* Injection attack logic scratchpad: */ #define MISC_ENTRIES 64 struct http_request* misc_req[MISC_ENTRIES]; /* Saved requests */ struct http_response* misc_res[MISC_ENTRIES]; /* Saved responses */ u8 misc_cnt; /* Request / response count */ #define MAX_CHECKS 32 u8 i_skip[MAX_CHECKS]; /* Injection step skip flags */ u8 i_skip_add; u8 r404_skip; u8 bogus_par; /* fuzz_par does nothing? */ u8 ognl_check; /* OGNL check flags */ /* Reporting information: */ u32 total_child_cnt; /* All children */ u32 total_issues[6]; /* Issues by severity */ u8 dupe; /* Looks like a duplicate? */ u32 pv_sig; /* Simple pivot signature */ }; extern struct pivot_desc root_pivot; extern u32 verbosity; extern u32 slist_cnt; /* Checks child / descendant limits. */ u8 descendants_ok(struct pivot_desc* pv); /* Increases descendant count. */ void add_descendant(struct pivot_desc* pv); /* Maps a parsed URL (in req) to the pivot tree, creating or modifying nodes as necessary, and scheduling them for crawl; via_link should be 1 if the URL came from an explicit link or user input, 0 if brute-forced. Always makes a copy of req, res; they can be destroyed safely; via_link set to 2 means we're sure it's a valid link; 1 means "probably". */ void maybe_add_pivot(struct http_request* req, struct http_response* res, u8 via_link); /* Creates a working copy of a request for use in db and crawl functions. If all is 0, does not copy path, query parameters, or POST data (but still copies headers); and forces GET method. */ struct http_request* req_copy(struct http_request* req, struct pivot_desc* pv, u8 all); /* Finds the host-level pivot point for global issues. */ struct pivot_desc* host_pivot(struct pivot_desc* pv); /* Case sensitivity helper. */ u8 is_c_sens(struct pivot_desc* pv); /* Lookup an issue title */ u8* lookup_issue_title(u32 id); /* Remove issues from a pivot */ void remove_issue(struct pivot_desc *pv, u32 type); /* Recorded security issues */ /* - Informational data (non-specific security-relevant notes): */ #define PROB_NONE 0 /* Invalid */ #define PROB_SSL_CERT 10101 /* SSL issuer data */ #define PROB_SSL_CERT_EXPIRE 10102 /* SSL cert will expire */ #define PROB_NEW_COOKIE 10201 /* New cookie added */ #define PROB_SERVER_CHANGE 10202 /* New Server: value seen */ #define PROB_VIA_CHANGE 10203 /* New Via: value seen */ #define PROB_X_CHANGE 10204 /* New X-*: value seen */ #define PROB_NEW_404 10205 /* New 404 signatures seen */ #define PROB_NO_ACCESS 10401 /* Resource not accessible */ #define PROB_AUTH_REQ 10402 /* Authentication requires */ #define PROB_SERV_ERR 10403 /* Server error */ #define PROB_DIR_LIST 10404 /* Directory listing */ #define PROB_HIDDEN_NODE 10405 /* Hidden resource found */ #define PROB_EXT_LINK 10501 /* External link */ #define PROB_EXT_REDIR 10502 /* External redirector */ #define PROB_MAIL_ADDR 10503 /* E-mail address seen */ #define PROB_UNKNOWN_PROTO 10504 /* Unknown protocol in URL */ #define PROB_UNKNOWN_FIELD 10505 /* Unknown form field */ #define PROB_FORM 10601 /* XSRF-safe form */ #define PROB_PASS_FORM 10602 /* Password form */ #define PROB_FILE_FORM 10603 /* File upload form */ #define PROB_USER_LINK 10701 /* User-supplied A link */ #define PROB_BAD_MIME_STAT 10801 /* Bad MIME type, low risk */ #define PROB_GEN_MIME_STAT 10802 /* Generic MIME, low risk */ #define PROB_BAD_CSET_STAT 10803 /* Bad charset, low risk */ #define PROB_CFL_HDRS_STAT 10804 /* Conflicting hdr, low risk */ #define PROB_FUZZ_DIGIT 10901 /* Try fuzzing file name */ #define PROB_OGNL 10902 /* OGNL-like parameter */ #define PROB_SIG_DETECT 10909 /* Signature detected info */ /* - Internal warnings (scan failures, etc): */ #define PROB_FETCH_FAIL 20101 /* Fetch failed. */ #define PROB_LIMITS 20102 /* Crawl limits exceeded. */ #define PROB_404_FAIL 20201 /* Behavior probe failed. */ #define PROB_PARENT_FAIL 20202 /* Parent behavior problem */ #define PROB_IPS_FILTER 20203 /* IPS behavior detected. */ #define PROB_IPS_FILTER_OFF 20204 /* IPS no longer active. */ #define PROB_VARIES 20205 /* Response varies. */ #define PROB_NOT_DIR 20301 /* Node should be a dir. */ /* - Low severity issues (limited impact or check specificity): */ #define PROB_URL_AUTH 30101 /* HTTP credentials in URL */ #define PROB_SSL_CERT_DATE 30201 /* SSL cert date invalid */ #define PROB_SSL_SELF_CERT 30202 /* Self-signed SSL cert */ #define PROB_SSL_BAD_HOST 30203 /* Certificate host mismatch */ #define PROB_SSL_NO_CERT 30204 /* No certificate data? */ #define PROB_SSL_WEAK_CIPHER 30205 /* Weak cipher negotiated */ #define PROB_SSL_HOST_LEN 30206 /* Possible \0 in host name */ #define PROB_DIR_LIST_BYPASS 30301 /* Dir listing bypass */ #define PROB_URL_REDIR 30401 /* URL redirection */ #define PROB_USER_URL 30402 /* URL content inclusion */ #define PROB_EXT_OBJ 30501 /* External obj standalone */ #define PROB_MIXED_OBJ 30502 /* Mixed content standalone */ #define PROB_MIXED_FORM 30503 /* HTTPS -> HTTP form */ #define PROB_VULN_FORM 30601 /* Form w/o anti-XSRF token */ #define PROB_JS_XSSI 30602 /* Script with no XSSI prot */ #define PROB_CACHE_LOW 30701 /* Cache nit-picking */ #define PROB_PROLOGUE 30801 /* User-supplied prologue */ #define PROB_XSS_VECTOR 30802 /* XSS vector, lower risk */ #define PROB_HEADER_INJECT 30901 /* Injected string in header */ #define PROB_SIG_DETECT_L 30909 /* Signature detected low */ /* - Moderate severity issues (data compromise): */ #define PROB_BODY_XSS 40101 /* Document body XSS */ #define PROB_URL_XSS 40102 /* URL-based XSS */ #define PROB_HTTP_INJECT 40103 /* Header splitting */ #define PROB_USER_URL_ACT 40104 /* Active user content */ #define PROB_TAG_XSS 40105 /* TAG attribute XSS */ #define PROB_EXT_SUB 40201 /* External subresource */ #define PROB_MIXED_SUB 40202 /* Mixed content subresource */ #define PROB_BAD_MIME_DYN 40301 /* Bad MIME type, hi risk */ #define PROB_GEN_MIME_DYN 40302 /* Generic MIME, hi risk */ #define PROB_BAD_CSET_DYN 40304 /* Bad charset, hi risk */ #define PROB_CFL_HDRS_DYN 40305 /* Conflicting hdr, hi risk */ #define PROB_FILE_POI 40401 /* Interesting file */ #define PROB_ERROR_POI 40402 /* Interesting error message */ #define PROB_DIR_TRAVERSAL 40501 /* Directory traversal */ #define PROB_CACHE_HI 40601 /* Serious caching issues */ #define PROB_PASS_NOSSL 40701 /* Password form, no HTTPS */ #define PROB_SIG_DETECT_M 40909 /* Signature detected moderate*/ /* - High severity issues (system compromise): */ #define PROB_XML_INJECT 50101 /* Backend XML injection */ #define PROB_SH_INJECT 50102 /* Shell cmd injection */ #define PROB_SQL_INJECT 50103 /* SQL injection */ #define PROB_FMT_STRING 50104 /* Format string attack */ #define PROB_INT_OVER 50105 /* Integer overflow attack */ #define PROB_FI_LOCAL 50106 /* Local file inclusion */ #define PROB_FI_REMOTE 50107 /* Remote file inclusion */ #define PROB_SQL_PARAM 50201 /* SQL-like parameter */ #define PROB_PUT_DIR 50301 /* HTTP PUT accepted */ #define PROB_SIG_DETECT_H 50909 /* Signature detected high */ #ifdef _VIA_DATABASE_C /* The definitions below are used to make problems, which are displayed during runtime, more informational */ struct pstruct { u32 id; u8* title; }; struct pstruct pstructs[] = { /* - Informational data (non-specific security-relevant notes): */ { PROB_SSL_CERT, (u8*)"SSL certificate issuer information" }, { PROB_NEW_COOKIE, (u8*)"New HTTP cookie added" }, { PROB_SERVER_CHANGE, (u8*)"New 'Server' header value seen" }, { PROB_VIA_CHANGE, (u8*)"New 'Via' header value seen" }, { PROB_X_CHANGE, (u8*)"New 'X-*' header value seen" }, { PROB_NEW_404, (u8*)"New 404 signature seen" }, { PROB_NO_ACCESS, (u8*)"Resource not directly accessible" }, { PROB_AUTH_REQ, (u8*)"HTTP authentication required" }, { PROB_SERV_ERR, (u8*)"Server error triggered" }, { PROB_DIR_LIST, (u8*)"Directory listing found" }, { PROB_EXT_LINK, (u8*)"All external links" }, { PROB_EXT_REDIR, (u8*)"External URL redirector" }, { PROB_MAIL_ADDR, (u8*)"All e-mail addresses" }, { PROB_UNKNOWN_PROTO, (u8*)"Links to unknown protocols" }, { PROB_UNKNOWN_FIELD, (u8*)"Unknown form field (can't autocomplete)" }, { PROB_FORM, (u8*)"HTML form (not classified otherwise)" }, { PROB_PASS_FORM, (u8*)"Password entry form - consider brute-force" }, { PROB_FILE_FORM, (u8*)"File upload form" }, { PROB_USER_LINK, (u8*)"User-supplied link rendered on a page" }, { PROB_BAD_MIME_STAT, (u8*)"Incorrect or missing MIME type (low risk)" }, { PROB_GEN_MIME_STAT, (u8*)"Generic MIME used (low risk)" }, { PROB_BAD_CSET_STAT, (u8*)"Incorrect or missing charset (low risk)" }, { PROB_CFL_HDRS_STAT, (u8*)"Conflicting MIME / charset info (low risk)" }, { PROB_FUZZ_DIGIT, (u8*)"Numerical filename - consider enumerating" }, { PROB_OGNL, (u8*)"OGNL-like parameter behavior" }, /* - Internal warnings (scan failures, etc): */ { PROB_FETCH_FAIL, (u8*)"Resource fetch failed" }, { PROB_LIMITS, (u8*)"Limits exceeded, fetch suppressed" }, { PROB_404_FAIL, (u8*)"Directory behavior checks failed (no brute force)" }, { PROB_PARENT_FAIL, (u8*)"Parent behavior checks failed (no brute force)" }, { PROB_IPS_FILTER, (u8*)"IPS filtering enabled" }, { PROB_IPS_FILTER_OFF, (u8*)"IPS filtering disabled again" }, { PROB_VARIES, (u8*)"Response varies randomly, skipping checks" }, { PROB_NOT_DIR, (u8*)"Node should be a directory, detection error?" }, /* - Low severity issues (limited impact or check specificity): */ { PROB_URL_AUTH, (u8*)"HTTP credentials seen in URLs" }, { PROB_SSL_CERT_DATE, (u8*)"SSL certificate expired or not yet valid" }, { PROB_SSL_SELF_CERT, (u8*)"Self-signed SSL certificate" }, { PROB_SSL_BAD_HOST, (u8*)"SSL certificate host name mismatch" }, { PROB_SSL_NO_CERT, (u8*)"No SSL certificate data found" }, { PROB_SSL_WEAK_CIPHER, (u8*)"Weak SSL cipher negotiated" }, { PROB_DIR_LIST, (u8*)"Directory listing restrictions bypassed" }, { PROB_URL_REDIR, (u8*)"Redirection to attacker-supplied URLs" }, { PROB_USER_URL, (u8*)"Attacker-supplied URLs in embedded content (lower risk)" }, { PROB_EXT_OBJ, (u8*)"External content embedded on a page (lower risk)" }, { PROB_MIXED_OBJ, (u8*)"Mixed content embedded on a page (lower risk)" }, { PROB_MIXED_FORM, (u8*)"HTTPS form submitting to a HTTP URL" }, { PROB_VULN_FORM, (u8*)"HTML form with no apparent XSRF protection" }, { PROB_JS_XSSI, (u8*)"JSON response with no apparent XSSI protection" }, { PROB_CACHE_LOW, (u8*)"Incorrect caching directives (lower risk)" }, { PROB_PROLOGUE, (u8*)"User-controlled response prefix (BOM / plugin attacks)" }, { PROB_HEADER_INJECT, (u8*)"HTTP header injection vector" }, /* - Moderate severity issues (data compromise): */ { PROB_BODY_XSS, (u8*)"XSS vector in document body" }, { PROB_URL_XSS, (u8*)"XSS vector via arbitrary URLs" }, { PROB_HTTP_INJECT, (u8*)"HTTP response header splitting" }, { PROB_USER_URL_ACT, (u8*)"Attacker-supplied URLs in embedded content (higher risk)" }, { PROB_EXT_SUB, (u8*)"External content embedded on a page (higher risk)" }, { PROB_MIXED_SUB, (u8*)"Mixed content embedded on a page (higher risk)" }, { PROB_BAD_MIME_DYN, (u8*)"Incorrect or missing MIME type (higher risk)" }, { PROB_GEN_MIME_DYN, (u8*)"Generic MIME type (higher risk)" }, { PROB_BAD_CSET_DYN, (u8*)"Incorrect or missing charset (higher risk)" }, { PROB_CFL_HDRS_DYN, (u8*)"Conflicting MIME / charset info (higher risk)" }, { PROB_FILE_POI, (u8*)"Interesting file" }, { PROB_ERROR_POI, (u8*)"Interesting server message" }, { PROB_DIR_TRAVERSAL, (u8*)"Directory traversal / file inclusion possible" }, { PROB_CACHE_HI, (u8*)"Incorrect caching directives (higher risk)" }, { PROB_PASS_NOSSL, (u8*)"Password form submits from or to non-HTTPS page" }, /* - High severity issues (system compromise): */ { PROB_XML_INJECT, (u8*)"Server-side XML injection vector" }, { PROB_SH_INJECT, (u8*)"Shell injection vector" }, { PROB_SQL_INJECT, (u8*)"Query injection vector" }, { PROB_FMT_STRING, (u8*)"Format string vector" }, { PROB_INT_OVER, (u8*)"Integer overflow vector" }, { PROB_FI_LOCAL, (u8*)"File inclusion" }, { PROB_SQL_PARAM, (u8*)"SQL query or similar syntax in parameters" }, { PROB_PUT_DIR, (u8*)"PUT request accepted" }, { PROB_NONE, (u8*)"Invalid" } }; #endif /* _VIA_DATABASE_C */ /* - Severity macros: */ #define PSEV(_x) ((_x) / 10000) #define PSEV_INFO 1 #define PSEV_WARN 2 #define PSEV_LOW 3 #define PSEV_MED 4 #define PSEV_HI 5 /* Issue descriptor: */ struct issue_desc { u32 type; /* PROB_* */ u8* extra; /* Problem-specific string */ u32 sid; /* Source ID, if any */ struct http_request* req; /* HTTP request sent */ struct http_response* res; /* HTTP response seen */ }; /* Register a problem, if not duplicate (res, extra may be NULL): */ void register_problem(u32 type, u32 sid, struct http_request* req, struct http_response* res, u8* extra, struct pivot_desc* pv, u8 allow_dup); /* Wrapper for register_problem */ void problem(u32 type, struct http_request* req, struct http_response* res, u8* extra, struct pivot_desc* pv, u8 allow_dup); /* Compare the checksums for two responses: */ u8 same_page(struct http_sig* sig1, struct http_sig* sig2); extern u8 **deny_urls, **allow_urls, **allow_domains, **trust_domains, **skip_params; extern u32 num_deny_urls, num_allow_urls, num_allow_domains, num_trust_domains, num_skip_params; extern u32 max_depth, max_children, max_descendants, max_trylist, max_guesses; extern u32 guess_cnt, wg_extension_cnt, keyword_total_cnt, keyword_orig_cnt; /* Check if the URL is permitted under current rules (0 = no, 1 = yes): */ u8 url_allowed_host(struct http_request* req); u8 url_trusted_host(struct http_request* req); u8 url_allowed(struct http_request* req); u8 param_allowed(u8* pname); /* Keyword management: */ extern u8 dont_add_words; /* Adds a new keyword candidate to the "guess" list. */ void wordlist_add_guess(u8* text); /* Adds non-sanitized keywords to the list. */ void wordlist_confirm_word(u8* text); /* Returns wordlist item at a specified offset (NULL if no more available). */ u8* wordlist_get_word(u32 offset, u8* specific); /* Returns keyword candidate at a specified offset (or NULL). */ u8* wordlist_get_guess(u32 offset, u8* specific); /* Returns extension at a specified offset (or NULL). */ u8* wordlist_get_extension(u32 offset, u8 specific); /* Loads keywords from file. */ void load_keywords(u8* fname, u8 read_only, u32 purge_age); /* Saves all keywords to a file. */ void save_keywords(u8* fname); /* Database maintenance: */ /* Dumps pivot database, for debugging purposes. */ void dump_pivots(struct pivot_desc* cur, u8 nest); /* Deallocates all data, for debugging purposes. */ void destroy_database(); /* Prints DB stats. */ void database_stats(); /* XSS manager: */ /* Creates a new stored XSS id (buffer valid only until next call). */ u8* new_xss_tag(u8* prefix); /* Registers last XSS tag along with a completed http_request. */ void register_xss_tag(struct http_request* req); /* Returns request associated with a stored XSS id. */ struct http_request* get_xss_request(u32 xid, u32 sid); /* Dumps signature data: */ void dump_signature(struct http_sig* sig); /* Displays debug information for same_page() checks. */ void debug_same_page(struct http_sig* sig1, struct http_sig* sig2); #endif /* _HAVE_DATABASE_H */