From 117622257b70e61d6b65e29ac9db6964910ff01c Mon Sep 17 00:00:00 2001 From: Victor Golovanenko Date: Sun, 27 Jul 2025 21:04:50 +0300 Subject: [PATCH] Initial commit --- README.md | 97 ++++++++++++++++ enable_shell_access.py | 173 ++++++++++++++++++++++++++++ extract_credentials.py | 60 ++++++++++ img/C-Data FD511G-X front side.webp | Bin 0 -> 7336 bytes requirements.txt | 1 + 5 files changed, 331 insertions(+) create mode 100644 README.md create mode 100644 enable_shell_access.py create mode 100644 extract_credentials.py create mode 100644 img/C-Data FD511G-X front side.webp create mode 100644 requirements.txt diff --git a/README.md b/README.md new file mode 100644 index 0000000..7c21deb --- /dev/null +++ b/README.md @@ -0,0 +1,97 @@ +# C-Data ONU tools +Tools for managing settings of the C-Data FD511G-X xPON ONU (and possibly other models), which are not available from the web interface: +enabling/disabling shell access, getting SSH/Telnet password. + +![C-Data FD511G-X ONU](img/C-Data%20FD511G-X%20front%20side.webp) + +## Abstract +The ONU web interface (usually available at http://192.168.101.1) allows you to download a configuration backup as a file. This is a regular XML file that is encrypted using XOR encryption with a key of `0x26`. +The tools presented here are designed to load, read, and modify this configuration file. + +## Tools +- `enable_shell_access.py` helps to enable/disable SSH/Telnet root shell access from LAN/WAN and change username & password. + Usage: + ``` + $ python enable_shell_access.py --help + usage: enable_shell_access.py [-h] [-a AUTHORIZATION] [-d] [-i {LAN,WAN}] [-p {SSH,Telnet}] [--change-username CHANGE_USERNAME] [--change-password CHANGE_PASSWORD] [host] + + Program to enable or disable Telnet or SSH access to the C-Data FD511G-X ONU + + positional arguments: + host ONU host (IP address or domain). Default: 192.168.101.1. + + options: + -h, --help show this help message and exit + -a, --authorization AUTHORIZATION + Web interface authorization credentials, separated by colon. Default: adminisp:adminisp. + -d, --disable Disable access. If this argument is not passed, it enables access. + -i, --interface {LAN,WAN} + Interface for enabling access: LAN or WAN. Default: LAN. + -p, --protocol {SSH,Telnet} + Protocol for enabling access: SSH or Telnet. Default: SSH. + --change-username CHANGE_USERNAME + Set new username + --change-password CHANGE_PASSWORD + Set new password + ``` + Usage examples: + - enable LAN SSH access: + ``` + $ python enable_shell_access.py 192.168.101.1 + Logging in to http://192.168.101.1 with adminisp:adminisp... + Current SSH credentials: + username: manu + password: c8a9b413 + + SSH access from LAN enabled + Uploading new configuration to the ONU... + Config successfully uploaded. Do you want to reboot ONU now? (Y/n) : Y + ONU reboot error. The device is probably already rebooting. + ``` + - disable LAN Telnet access: + ``` + $ python enable_shell_access.py 192.168.101.1 -p Telnet -d + Logging in to http://192.168.101.1 with adminisp:adminisp... + Current Telnet credentials: + username: manu + password: c8a9b413 + + Telnet access from LAN disabled + Uploading new configuration to the ONU... + Config successfully uploaded. Do you want to reboot ONU now? (Y/n) : Y + Device is rebooting. Please wait. + ``` + - login with custom web interface credentials: + ``` + $ python enable_shell_access.py 192.168.101.1 -a adminisp:admin1234 + Logging in to http://192.168.101.1 with adminisp:admin1234... + Current SSH credentials: + username: manu + password: c8a9b413 + + SSH access from LAN enabled + Uploading new configuration to the ONU... + Config successfully uploaded. Do you want to reboot ONU now? (Y/n) : Y + ONU reboot error. The device is probably already rebooting. + ``` + +- `extract_credentials.py` extracts credentials from the binary config file, including SSH and Telnet username & password, and also saves decrypted version of the config file. + Usage: + ``` + $ python extract_credentials.py ~/config.bin + File decrypted and saved to "/home/user/config.xml" + + Web interface credentials: + root-level account: + username: adminisp + password: adminisp + + user-level account: + username: admin + password: admin + + + Shell credentials (Telnet and SSH): + username: manu + password: c8a9b413 + ``` diff --git a/enable_shell_access.py b/enable_shell_access.py new file mode 100644 index 0000000..5be149b --- /dev/null +++ b/enable_shell_access.py @@ -0,0 +1,173 @@ +import argparse +import hashlib +import io +import sys +import xml.etree.ElementTree as ET +from typing import Literal, Optional, Union + +import httpx + + +CONFIG_ENC_KEY = 0x26 + + +def xor_xcrypt(data: bytes, key: int, result_as_string: bool = False) -> Union[bytes, str]: + if result_as_string: + return ''.join(chr(c ^ key) for c in data) + else: + return bytes(c ^ key for c in data) + + +def get_shell_access_creds(config: ET.Element, protocol: Literal['SSH', 'Telnet'] = 'SSH') -> str: + remote_management = config.find(f'.//RemoteManagement/{protocol.upper()}') + username = remote_management.find('TelnetUserName' if protocol == 'Telnet' else 'UserName').text + password = remote_management.find('TelnetPassword' if protocol == 'Telnet' else 'Password').text + return (f'username: {username}\n' + f'password: {password}\n') + + +def set_shell_access_creds(config: ET.Element, protocol: Literal['SSH', 'Telnet'] = 'SSH', + username: Optional[str] = None, password: Optional[str] = None) -> bool: + remote_management = config.find(f'.//RemoteManagement/{protocol.upper()}') + if remote_management is None: + return False + if username: + remote_management.find('TelnetUserName' if protocol == 'Telnet' else 'UserName').text = username + if password: + remote_management.find('TelnetPassword' if protocol == 'Telnet' else 'Password').text = password + return True + + +def is_access_enabled(config: ET.Element, + interface: Literal['LAN', 'WAN'] = 'LAN', protocol: Literal['SSH', 'Telnet'] = 'SSH') -> bool: + ssh_policy = config.find(f".//ServiceControl/*[ServiceList='{protocol.upper()}'][Interface='{interface}']/Policy") + return ssh_policy.text == "Permit" + + +def enable_access(config: ET.Element, interface: Literal['LAN', 'WAN'] = 'LAN', + protocol: Literal['SSH', 'Telnet'] = 'SSH', disable: bool = False) -> bool: + service_policy = config.find( + f".//ServiceControl/*[ServiceList='{protocol.upper()}'][Interface='{interface}']/Policy") + remote_management = config.find(f'.//RemoteManagement/{protocol.upper()}/{protocol}Enable') + if (service_policy is not None) and (remote_management is not None): + service_policy.text = "Discard" if disable else "Permit" + if not disable: + remote_management.text = '1' + return True + return False + + +def create_arg_parser(): + arg_parser = argparse.ArgumentParser( + description='Program to enable or disable Telnet or SSH access to the C-Data FD511G-X ONU') + arg_parser.add_argument( + 'host', + nargs='?', + default='192.168.101.1', + help='ONU host (IP address or domain). Default: %(default)s.') + arg_parser.add_argument( + '-a', '--authorization', + default='adminisp:adminisp', + help='Web interface authorization credentials, separated by colon. Default: %(default)s.') + arg_parser.add_argument( + '-d', '--disable', + action='store_true', + help='Disable access. If this argument is not passed, it enables access.') + arg_parser.add_argument( + '-i', '--interface', + choices=('LAN', 'WAN'), + default='LAN', + type=str, + help='Interface for enabling access: LAN or WAN. Default: %(default)s.') + arg_parser.add_argument( + '-p', '--protocol', + choices=('SSH', 'Telnet'), + default='SSH', + type=str, + help='Protocol for enabling access: SSH or Telnet. Default: %(default)s.') + arg_parser.add_argument( + '--change-username', + type=str, + help='Set new username') + arg_parser.add_argument( + '--change-password', + type=str, + help='Set new password') + return arg_parser + + +if __name__ == '__main__': + arguments = create_arg_parser().parse_args() + host = arguments.host + username, password = arguments.authorization.split(':', maxsplit=1) + disable = arguments.disable + interface = arguments.interface + protocol = arguments.protocol + change_username = arguments.change_username + change_password = arguments.change_password + + http = httpx.Client(base_url=f'http://{host}') + hashed_password = hashlib.md5(password.encode('utf-8')).hexdigest() + print(f'Logging in to http://{host} with {username}:{password}...', file=sys.stderr) + resp = http.post( + '/post.json', + json={'module': 'login', 'username': username, 'encryPassword': hashed_password} + ) + if resp.json()['code'] != 0: + sys.exit(f'Authorization error:\n{resp.text}') + + resp = http.post('/boaform/formSaveConfig', data={'save_cs': 'backup_config'}) + resp.raise_for_status() + encrypted_config = resp.content + decrypted_config = xor_xcrypt(encrypted_config, CONFIG_ENC_KEY, result_as_string=True).replace(',', '\n') + + config = ET.fromstring(decrypted_config) + config_changed = False + + print(f'Current {protocol} credentials:\n', get_shell_access_creds(config, protocol), sep='') + if change_username or change_password: + if set_shell_access_creds(config, protocol, change_username, change_password): + print(f'{protocol} changed to:\n', get_shell_access_creds(config, protocol), sep='') + config_changed = True + else: + print(f'Failed to change {protocol} credentials', file=sys.stderr) + + enabled = is_access_enabled(config, interface, protocol) + if disable == enabled: + if enable_access(config, interface, protocol, disable=disable): + print(f'{protocol} access from {interface}', 'disabled' if disable else 'enabled') + config_changed = True + else: + print(f'Failed to {"disable" if disable else "enable"} {protocol}. Check configuration', file=sys.stderr) + else: + _tip = [f'{protocol} access from {interface} is already {"enabled" if enabled else "disabled"}.'] + if enabled: + _tip.append(f"To disable, run \"{' '.join(sys.argv)} --disable\"") + print(' '.join(_tip)) + + if config_changed: + modified_config = ET.tostring(config, encoding='utf-8', xml_declaration=True, short_empty_elements=False) + encrypted_modified_config = xor_xcrypt(modified_config.replace(b'\n', b','), CONFIG_ENC_KEY) + + print('Uploading new configuration to the ONU...', file=sys.stderr) + resp = http.post('/boaform/formSaveConfig', files={ + 'binary': ('config.bin', io.BytesIO(encrypted_modified_config), 'application/octet-stream')}) + resp.raise_for_status() + if resp.json()['code'] != 0: + sys.exit(f'Error uploading config:\n{resp.text}') + resp = http.get('/get.json', params={'module': 'restore_info'}) + resp.raise_for_status() + if resp.json()['code'] != 0: + sys.exit(f'Error applying config:\n{resp.text}') + + if input('Config successfully uploaded. Do you want to reboot ONU now? (Y/n) : ').lower() != 'n': + try: + resp = http.post('/post.json', json={'module': 'dev_config', 'reboot': 1}) + except (httpx.ConnectError, httpx.ReadError): + print('ONU reboot error. The device is probably already rebooting.') + sys.exit(0) + else: + resp.raise_for_status() + print('Device is rebooting. Please wait.', file=sys.stderr) + else: + print('Configuration has not been changed.') diff --git a/extract_credentials.py b/extract_credentials.py new file mode 100644 index 0000000..6e5088f --- /dev/null +++ b/extract_credentials.py @@ -0,0 +1,60 @@ +import sys +import xml.etree.ElementTree as ET +from pathlib import Path + +key = 0x26 + + +def extract_web_interface_creds(tree: ET) -> str: + x_gc_login = tree.find('.//X_GC_LOGIIN') + result = [] + for account in x_gc_login: + level = account.find('UserLevel').text + username = account.find('Username').text + password = account.find('Passwd').text + result.append(f'{level}-level account:\nusername: {username}\npassword: {password}') + return '\n\n'.join(result) + + +def extract_shell_access_creds(tree: ET) -> str: + telnet = tree.find('.//RemoteManagement/TELNET') + ssh = tree.find('.//RemoteManagement/SSH') + telnet_username = telnet.find('TelnetUserName').text + telnet_password = telnet.find('TelnetPassword').text + ssh_username = ssh.find('UserName').text + ssh_password = ssh.find('Password').text + if (telnet_username == ssh_username) and (telnet_password == ssh_password): + return f'Shell credentials (Telnet and SSH):\nusername: {ssh_username}\npassword: {ssh_password}' + else: + return f'''Shell credentials: +Telnet: +username: {telnet_username} +password: {telnet_password} + +SSH: +username: {ssh_username} +password: {ssh_password}''' + + +try: + config_filename = Path(sys.argv[1]) +except IndexError: + print(f'Usage: {sys.argv[0]} ', file=sys.stderr) + sys.exit(1) + +with open(config_filename, 'rb') as infile: + test_chunk = infile.read(7) + infile.seek(0) + if test_chunk.startswith(b'3lE%(|oA;N0@n*iuaHL0j5?9NoZ+NBBHxB=@>&Y&k=ifKTG15mUIkgU?Y>30BOcDqx zovFBECCuwaqnE$^9~RokhtJS^JXB9TfcHTJ4kYwJCyqlO8MI$iF7Nqoc8@{1;tT1V zIYp^b3Oo#b#c@ks?SP4Lw|geaGnQJN!_2<}c)Ff1Sks&D)Hbn~NUO{lgYZxP2@vb8 zP;5-sTjMjY9Jz8*TGgz|^#iyrD>whWV&hm}B`)H4L06MCM1l6Ii`YrMdGqUaVEwJf zWQ+1SL=Jy>v%FcEd0Rnf3{@_hB;$~tFjo{RvR=tHy>?xxY8hMugf-Z*V;P+?iNAd_ z-%ce|3l<8$X-*JUVH#w{6V*FkKxCGzVrS1Zd42k>#gMvJvi^yUmz*vNyoq5lX~O-BKFcpj3pvYv?Y;- zhDzf(MF>y=*A9WpuSBlBPNoM2JHXUTki6cr%pHxt zIonw7l1XDn??QWIi2D*+{1)qlUrp!Cx6Yax7O9IW2@UVwv}-sa?O2D`nX}rLfFLwn z(^k1YqPE2S_563hFX$o8fDc=aNHIz!tbX0XTGA6GIo?&8DtzzH3R01vls2OLw^x!u z-eR@9V*T?E4Fq9D#hy>zw4l+*`R>I--c15tr=I5=Bt>PxdD_;MQg1Tl>6&uaa;F2H zxPG$KH4lk~O1Dhh11z{ngbBI&(N;TLJ_h?aZ@i&y%CcO}-RUJC-mci3&E7+iFEnTf z0P;q)gTuG`lcA^+f)r~apGJCdF`Fwikiph7aF}3?sxUp{*Cu>w36IW=VmZRl#q-JT2@EAzctKf0oYRyN`?5(yec5zLYWy5V}v@h^g5Pa2K< zf00rCFH#L(+tN$-q%QRGhlQ_~YRV^`yj~ujiF_{uV>|J?6)c+dbjS>wcI%Xwqmtd-d&a>S*f$ z7A7-ncT-}qdDYlHf<=7A&Fwe{^TAzUNB=#^I<}6t??Uba8s?xPPAiNvSdUe^&4@rp z=Q=(EUO2G7-{ZwNwyWPj@<@LKRS@?7uqmYd>$?b(AN@WNC;f0@u@*wA=X0Xjt})=uZKi=B$pkXh zu{X|$ywbd5>POd*ggbB7c=vPL#S+v9!xz{mIuLfy<;Tc}HuFLs6pTs{AAP`h*>D-b zfygF%Y15b@$rNeH?#A3)H>uJcjQl_UW!Dv{VBLW1@)paO^?V8lMtU!hTo|JeY>T;&((~a4CpQ~wb4}V0?QxM0eZrZ&t`TkvwY3rZ z0JU;MsUU7~TKg0GATdmKrU!GqS?{5nKim~>!;hkSzFNOssOJUJ zG(>*IA4f-Niz(EHu`;l39t4XY*MteG7)Gfo38==hX1hfMnsozX!K5R?v$#e>KZLj! zuiGkt$5VJ1^VM*OdIAzuMtD?A&WhG;jGu5NVwBSmP2MgOEaROpC;yFcht9^m^m?1i z7mcGzLBwmkZH1=dRtGbvo#rnk!*uzx-em{s%gbecJ>w%QF~y>cMo0zN;BzOqDXv#fgr8?w z21iDC+Mp*-YCOe8_q&X*ij5c|m?@>A5USK&P&cvXwzK=Dw5( zpQS1hQnXXeHJr!}KY1`h+0%F|C2=d~A#w_k+&>QOMRfE5f?-QoxRo9Y`JZxcq&0#M z22N-!_Yn+7e`yp36_J3jd943F{Y{Ki3ZL7`Ds(wXqRJO1olOJQuU8At$^pVI`={+J+z3jen8?^msdwjlN(hsL(7Nk-O zkL@<-tN8J@lm|l?huFVHo5bQU^EzvdNiJUHD(BTpApN^q(kHOYb$b9lP1EtBOU|E2}0jl259Jc=eBb%zFfOADfwT!1PkD)Vc#vVq^GHR{je?;ULUj>pn3 zp&-ntq7BZpwmgCvo);drS7V06Fb-3B4a*IyUNe2ZQYe;WFl}_>8DeRDCT3(pGlZiI zM7Q>2?+r4muU9oc@@4|Yl#lg!K$!b+sau}%3o=lWWwcba*bI5}6S-`JnLIoDUh4WY z_PWz(%$wFyCg@RTHz%T?Fp#efy-(lMhk9t+G;h$#&EASH+3eGen_9;#YG01+S5q3C zo^$I=p2n6iY!vdjJlV5IL)I>7|2#N=6nDGS(CcnTsgH-Pp@vF>ZFSfD@|(brNUxqEmlVEeebF+?sRJc zV`N(OhW5qRkR*ELWNd2Hv+|TsOT?j8y16wXp8!K7Z;wvA1X-6+Ar88X zajNOB#MSD&Zkys<)cg9!JI=^)r3?LE?<)Q>Z#)MK$6YyRx;4(YMh5>ZE_t;D8}ebU zarW2S2EM#`+-#OLOix>(aNh>ZA5igGKKt1p%-~hBh!1l9%*RM?@B~b%UiIk#S!1^| z(vUz^ex_!bWNN!P^rk!taE&$j-V?>qbbEVi2uS9U|5}sG_>tBvt98b|@9g_dsN6~x zo3||W<&mXv*yv~gVpM*)%RMeEX+;Z#pIgOTF$e&Rz3;3E9$#pN57xgd!&t}SnX>+T zia(}v&V*Y~qaAS1txV76+Mh62LtpGq7VF{;a7wa;DLH=4 z_VhQXZq69I@F}kr;36>g%zw!haH`tKmOHLD(_E_>e%-PT@mAo>h&)KnM&V3{Jug<8?$JJ=^>efX=Zip6jzP#B^XJh&BU;)ahM}wrS?ibg!Lq3qDJ5Kk z_(Qc}z76~(h8*oLRGi8`zqLynbw3IyV|`FmHmkcdaf7?l^`S4y`&;9(Oxqt*`hBN~ zqWT{287YHsWF1v5Z61FL_c(p~xIXLl3qQrFNetOmW{1j{@6X@#;obnz(29HH1DmMk zqZO6~oLKKIruO5BwP==+H_@DevoY#{(afOB9~)3G)|*-)r-y^3+S7eYdL1yfCzd@d z?(oz~CH>i{vGz?`o}o2TLN<|j_A|x6Iu8jw_NLMNN9TA}hW4!ma<6c$_C$kDUGp-1 zp4eT}m6kKf=~g6quUSXFSphk3eK1CK~q@ zM`PSpr!GW3CH8lp4%P=L9;f1mfd+~#xgYV1^PilU4DB->!W<0dwBU?3Ia18MMTtQ( zRc8AJ8wZdrj!v%-f%aPpwQ-(F)T`*8C*92U_iLp(NsBq?tvz@MqJ$X9TA;&u4r+?x zdfvoRD`&IDSkk6yd4kW#YsV@SwR4R zPtA~-;?@eRe01qPAYc^7g=5d9z)A9ouVjB`rD=-pWJusf1%>=}_5z*>^l?d3POntz3% zg}76t#D3~n$yeJXfG}mc)@jk-Lo3Bgj%|l#Lq8qxTqOyWP}e4(o~+mrHFoJ{WECxP zZ_*Kx+=a#MqeS4UX9dEk(qBu4z_Bd~`zqKGUdgi99isTjRQXcBJIC3dVaY-c7i2Wm z%>QZ((GY}hd$Zp89Cs>%hZvlnx(i17JW1Pyl5+WIJ6lPXumYtQ@}wbLaY`CPB21SE zcRQJW&b1;+6E1m9xB?eOw$_1BO#Mtv-?&H+rt6;CQ()|0yn@Wm9fR73pGB;okVO&P*8V1~x6mY!n34Xe=f}7VJ zK&e$UTplV8$?E4Q@k7TUHfNzWmwrm@hkPt5vH0&Ro-p#E(n8i~)-rXis$}9V#2-pT zl46PBTZs?f&Sw*TT?bvA*09Z8zzPfM_9QI>OtXND4oH2lf>x_Ntw_d-<-slXoRq{{67ZMC|e_IlG!WC+~tiNnG_q7bf6? zETeOM73!tFmTOr@qhcyRW@sgnMHbmA(_oz~8b{G4#12Y78IT}-UOP$fwM&@(occ!D6+m{a0RLkBBXi*qWB_@a63tYuFBmU#jyu7 z=bJ>rl9gBt9Kw29#VFiS&GAj<_$DsF1y$Lw-HQGS@N){pp!~>v*(BoGzdpV(=iUSk zv+CB@f25tg4p^YW1fKp<6)}N>#j{&xK=#)jZp{qqw5y@0&rN}H2XVu&f3`U-b^cC8 z!G1~CZ3TTcR^Q$5P3t*-@?FGy6tOJ%P&XNkh*(wSY=@u_8GhkAvr8(y-|yW=XI*1# zCe!IXalAk7EI9H#;PyKDSGN0NTKO5AwvkTQJ}Kf?%Ww@tv$tq5g4r{qhxtmS06>Ed zgyK@Fn9Nd=ne-Dw)c+~VqO7NJ9= zz}6&z6+Q_M^$DxIDV>D=^fN}tFp0C0rN7ON3V0&V`fWF?iFQSLVEs1cxHnpY2C~ja z?nSt8TY!$}mdXiJY~6^nq%=G@sTyJX&H{IdPB{w$g-XisnIjnZHnwS9d)rp*fBEwr ze~G8F%aP&%{A$={w=GWt0Aouu6vGR|eqv-HqWt+8d?Qp9%pz!EX!fVZ$iQwaYA_0o z;^XbKfS<{I!oOom*Zc6%n?ys@580(ClFv)r>QN`k9{FX(iHU`)sj1M{*p53v6i7z) zFHf_8;tB!yE)?nQI@?GQ=ABzMSNkDS;MN};G&LzwBvMM1aJapK;dMIga!4OTX-tXD z99IhDTWzoifV6V+awpQX0yLq1xE);zPK~m0gRoyx{9UsT3}LyrYqP26(1W&Z$!DJVzosU4N@(j3Xp9Kh7cv4k z!d81z2?oy7xaGt>@VU~nTyWE6LU27UU|jT`BGo(h@Z!cXC)-rEL~a=exUxC1z(VER znEu(?lFT>N$bQ>UjZ?JxQ9jE;H7hcN-1V&VNY;=QK!Dnkd8iRPY7KjsAK7P?@8JID zU*UZfLDNJpH@>(}>zs~ng{c>+r+<036^BJO;~L82n|oMuSb8~p=(ApemV01}d83ta zHEL?g4JpkV%W(8o4`Nkz4fLSdqJk03(Tm>AfPUVbR~hyY9IrJ+RUt|#sedl^V?Bz- zx&09b@xiBfKHrW!paZ;I_jsON&@7B%nUMA^iQj+I1(5)WwHZcBpIu)oP5R!4JjoY| zX0-sdyNS7r3>W0=mmkFu?aZHws=tSvS(I4MzB+Maq1yTO=*9NtdBtNTP2Ujd5@8dg zQKFzT=cA=HJ2JRsLyhA8Hen38;?>R7;;2W>SxX;tjUYZtepp^U;uppkSsx`HcfSB^2R?fc(mG5D@#Nc|-V@_~5xBg1g^<7+TyQk*w?&_T&W3vVTXx<&)g8oHQamOKXzQpq`J0l-Z&) zeIsLJ*#6D?$4smC9)2^EBr%uvOF@2J_9q&D?X8O6=8c~(@+*Cq`GC@6NSy=Pq`X+l z;0vS8fJa;zG<^LrjQZY?x*tGootEd-IDDTG+Mb{|c!dyHY$huJ6A_`ZTKhap%IIyz zwqbrjCbmo0>7xllt&Ai7W_EA664eY`8C5&S`RyC0VKc)rmqG_*MFsvv39FVCVvmP& z5U$Ztv1)0Kt7~ZG3+KV`&$ns^oXFswB-z?Q044{3RX379U$#N{1~w_fFeuog@dsi$ z!dsE)2?0nbeI1R-!_*qVm$2+RaSuxqwKj3?H{c;R$n4?ETJ(m%VvfH((};_6J$WBP z!Db%KvlvGpriHqxQ$&yh74j)=jWT0n!qn27DlEF6BOa}IM$cWDf(zj{npymG&B-07 zWHgttBH79Ve2f^sc#mdeP`pf54o_l!CIdsXeqVZhD7FY1Ogus#7m~NVmn8qxWF}~1 zQ|?C6^a-~zsbkmw$gsD9>hXfnHvbPHagpGuNdjCfXOS*-{K4*-s=)>#J&NkRr10O1J6F@X9rQeJI>W}K-S``&hMPY<8w(}EZ_1zixmWdy z4Sso2L6yn%W~6+08^}rXG-dLqFvpq>H7@(rIYPFQv8g)cj z6i5t4kkEpUzLA9?UG)*xqa22f(W1gAtH`!3E60|j{8UNuW3inIID26u`{gvwCD!F^ zR`#QpAO5GNtik*fI98%)=-XkmoNRp*G1zCxPl|#EMN4A z9m|(d&xe%Sky$(49;s|8-w2!ppiLB_#YwI-S#+=e(mEL!G~u_m7OiaQ_^N5h9ntoa zj&?NJRJ7>Q08$BWD`;C4 zOHXf`b@^zlq>Riu18%A5h4VZadYru&O>B%iMzZmN1gnvg=;<;P#i?leALC`af5*w~ zO=xmY=&Xidom?9G2AOp?0Xk9^nC~P