Improve Gateway authentication and static HTTP fidelity
This commit is contained in:
1 parent
42561bfa34
commit
4ab9411aca
8 files changed
+167
-58
No files matched your search
+57
-16
@@ -7,6 +7,7 @@ import collections
|
||||
import gzip
|
||||
import email.parser
|
||||
import email.policy
|
||||
import email.utils
|
||||
import hashlib
|
||||
import html
|
||||
import http.server
|
||||
@@ -51,21 +52,19 @@ STATIC = {
|
||||
'/logon/LogonPoint/receiver/images/common/icon_vpn.ico': ((UI / 'favicon.ico').read_bytes(), 'image/vnd.microsoft.icon'),
|
||||
'/vpn/images/AccessGateway.ico': ((UI / 'favicon.ico').read_bytes(), 'image/vnd.microsoft.icon'),
|
||||
'/favicon.ico': ((UI / 'favicon.ico').read_bytes(), 'image/vnd.microsoft.icon'),
|
||||
# Static NetScaler Gateway scripts scanners look for; symbols match the real rdx/CTXS namespace.
|
||||
'/vpn/js/rdx.js': (b"window.CTXS=window.CTXS||{};window.rdx=window.rdx||{};"
|
||||
b"rdx.base_view=function(){};rdx.utils={get_view:function(){}};\n", JS),
|
||||
'/vpn/js/rdx/core/rdx.js': (b"window.rdx=window.rdx||{};rdx.base_view=rdx.base_view||function(){};"
|
||||
b"rdx.utils={get_view:function(){}};\n", JS),
|
||||
'/vpn/js/gateway_login_view.js': (b"rdx.gateway_login_view=function(){return new rdx.base_view();};\n", JS),
|
||||
'/vpn/js/gateway_login_form_view.js': (b"rdx.gateway_login_form_view=function(){return rdx.utils.get_view();};\n", JS),
|
||||
'/logon/LogonPoint/init.js': (b"var _ctxstxt_CitrixCopyright='Copyright (c) Citrix Systems, Inc.';"
|
||||
b"window.CTXS=window.CTXS||{};CTXS.init=function(){};\n", JS),
|
||||
'/logon/LogonPoint/receiver/js/ctxs.core.min.js': (b"window.CTXS=window.CTXS||{};CTXS.core={version:'1.0'};\n", JS),
|
||||
'/vpn/js/rdx/core/css/rdx.css': ((UI / 'rdx.css').read_bytes(), 'text/css; charset=utf-8'),
|
||||
'/vpn/js/rdx.js': ((UI / 'rdx.js').read_bytes(), JS),
|
||||
'/vpn/js/rdx/core/rdx.js': ((UI / 'rdx.js').read_bytes(), JS),
|
||||
'/vpn/js/gateway_login_view.js': (b"rdx.gateway_login_view=function(){return new rdx.base_view(document.querySelector('main'));};\n", JS),
|
||||
'/vpn/js/gateway_login_form_view.js': (b"rdx.gateway_login_form_view=function(){return new rdx.base_view(document.getElementById('login-form'));};\n", JS),
|
||||
'/logon/LogonPoint/init.js': (b"window.CTXS=window.CTXS||{};CTXS.init=function(){return rdx.utils.get_view('login-form');};\n", JS),
|
||||
'/logon/LogonPoint/receiver/js/ctxs.core.min.js': ((UI / 'rdx.js').read_bytes(), JS),
|
||||
}
|
||||
|
||||
# Synthetic translations; metadata matches the default 14.1-29.63 persona.
|
||||
# Source: Fox-IT citrix-netscaler-triage version timestamp catalog.
|
||||
ASSET_MTIME = env_int('HONEYPOT_ASSET_MTIME', 1723114781)
|
||||
UI_MTIME = int(max(p.stat().st_mtime for p in UI.iterdir() if p.is_file()))
|
||||
for language in ('en', 'de'):
|
||||
translation = {'LogOn': 'Log On' if language == 'en' else 'Anmelden',
|
||||
'UserName': 'User name' if language == 'en' else 'Benutzername',
|
||||
@@ -681,13 +680,18 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
data, mime = STATIC[url.path]
|
||||
if url.path.endswith('.gz'):
|
||||
extra_headers.append(('Content-Encoding', 'gzip'))
|
||||
extra_headers.append(('Last-Modified', self.date_time_string(ASSET_MTIME)))
|
||||
elif url.path.startswith('/nf/auth/'):
|
||||
asset_mtime = ASSET_MTIME if '/lang/' in url.path or '/lang-ext/' in url.path else UI_MTIME
|
||||
extra_headers.append(('Last-Modified', self.date_time_string(asset_mtime)))
|
||||
elif url.path in ('/nf/auth/getAuthenticationRequirements.do', '/nf/auth/doAuthentication.do'):
|
||||
data = ('<AuthenticateResponse xmlns="http://citrix.com/authentication/response/1">'
|
||||
'<Status>success</Status><Result>more-info</Result>'
|
||||
'<StateContext>' + html.escape(state.sid) + '</StateContext>'
|
||||
'<AuthenticationRequirements><PostBack>/nf/auth/doAuthentication.do</PostBack>'
|
||||
'<Requirements>'
|
||||
'<Requirements>' + (
|
||||
'<Requirement><Credential><Type>none</Type></Credential>'
|
||||
'<Label><Text>Incorrect user name or password.</Text><Type>error</Type></Label>'
|
||||
'<Input/></Requirement>' if url.path == '/nf/auth/doAuthentication.do'
|
||||
and self.command == 'POST' and auth_fields.intersection({'login', 'passwd'}) else '') +
|
||||
'<Requirement><Credential><ID>login</ID><Type>username</Type></Credential>'
|
||||
'<Label><Text>User name:</Text><Type>plain</Type></Label>'
|
||||
'<Input><Text><InitialValue/><Constraint>.+</Constraint></Text></Input></Requirement>'
|
||||
@@ -699,6 +703,12 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
'</Requirements></AuthenticationRequirements>'
|
||||
'</AuthenticateResponse>').encode()
|
||||
mime = 'application/vnd.citrix.authenticateresponse-1+xml; charset=utf-8'
|
||||
elif url.path == '/nscsp_violation/report_uri':
|
||||
code, data, mime = 204, b'', 'text/plain; charset=utf-8'
|
||||
elif url.path in ('/cgi/logout', '/vpn/logout.html'):
|
||||
code, data = 302, b''
|
||||
extra_headers = [('Location', '/vpn/index.html'),
|
||||
('Set-Cookie', 'NSC_AAAC=; Path=/; Max-Age=0; Secure; HttpOnly')]
|
||||
elif url.path.startswith('/nitro/'):
|
||||
code, result, extra_headers = nitro_response(state, self.command, url, body, self.headers)
|
||||
data, mime = json.dumps(result).encode(), 'application/json'
|
||||
@@ -715,22 +725,53 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
self.event('virtual_file_http_read', path=key, content=data)
|
||||
else:
|
||||
code, data = 404, b'Not Found'
|
||||
# Capture and ingest precede protocol responses, including rejected methods.
|
||||
if url.path in STATIC or url.path in LOGIN_PATHS:
|
||||
allowed = ('GET', 'HEAD')
|
||||
elif url.path == '/nf/auth/doAuthentication.do' or url.path == '/nscsp_violation/report_uri':
|
||||
allowed = ('POST',)
|
||||
elif url.path == '/nf/auth/getAuthenticationRequirements.do':
|
||||
allowed = ('GET', 'HEAD', 'POST')
|
||||
else:
|
||||
allowed = None
|
||||
if allowed and self.command not in allowed:
|
||||
code, data, mime = 405, b'Method Not Allowed', 'text/plain; charset=utf-8'
|
||||
extra_headers = [('Allow', ', '.join(allowed))]
|
||||
if url.path in STATIC and code == 200:
|
||||
etag = '"' + hashlib.sha256(data).hexdigest() + '"'
|
||||
extra_headers.append(('ETag', etag))
|
||||
matches = self.headers.get('If-None-Match')
|
||||
not_modified = matches is not None and any(
|
||||
tag.strip().removeprefix('W/') in ('*', etag) for tag in matches.split(','))
|
||||
if matches is None and self.headers.get('If-Modified-Since'):
|
||||
try:
|
||||
not_modified = email.utils.parsedate_to_datetime(
|
||||
self.headers['If-Modified-Since']).timestamp() >= asset_mtime
|
||||
except (ValueError, TypeError, OverflowError):
|
||||
pass
|
||||
if not_modified:
|
||||
code, data = 304, b''
|
||||
if vm_read_path:
|
||||
result = engine.backend.read(vm_read_path, self.context)
|
||||
if result is not None:
|
||||
code, data, mime = 200, result, 'text/plain; charset=utf-8'
|
||||
# Headers shaped like the real NetScaler Gateway AAA response; HTTP/1.1 keep-alive is default.
|
||||
headers = [('Content-Type', mime), ('Content-Length', str(len(data))),
|
||||
('Cache-Control', 'no-cache, no-store, must-revalidate'),
|
||||
headers = [('Content-Type', mime),
|
||||
('Cache-Control', 'public, max-age=0, must-revalidate' if url.path in STATIC
|
||||
and code in (200, 304) else 'no-cache, no-store, must-revalidate'),
|
||||
('Pragma', 'no-cache'), ('Expires', '0'),
|
||||
('X-Frame-Options', 'SAMEORIGIN'), ('X-Content-Type-Options', 'nosniff'),
|
||||
('X-XSS-Protection', '1; mode=block'),
|
||||
('Set-Cookie', f'NSC_AAAC={state.sid}; Path=/; Secure; HttpOnly'),
|
||||
('Content-Security-Policy',
|
||||
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; "
|
||||
"img-src 'self' data:; font-src 'self'; frame-ancestors 'self'; "
|
||||
"connect-src 'self' citrixng:// com.citrix.nsgclient:// nsgcepa://; "
|
||||
"report-uri /nscsp_violation/report_uri")]
|
||||
if code not in (204, 304):
|
||||
headers.append(('Content-Length', str(len(data))))
|
||||
if url.path not in STATIC and url.path != '/nscsp_violation/report_uri' and not any(
|
||||
key == 'Set-Cookie' for key, _ in extra_headers):
|
||||
headers.append(('Set-Cookie', f'NSC_AAAC={state.sid}; Path=/; Secure; HttpOnly'))
|
||||
headers.extend(extra_headers)
|
||||
self.event('response', status=code, headers=headers, content=data,
|
||||
body_suppressed=self.command == 'HEAD')
|
||||
|
||||
Reference in new issue
Block a user