Skip to content

Commit

Permalink
Merge pull request #28 from mepley1/dev-misc
Browse files Browse the repository at this point in the history
Dev misc
  • Loading branch information
mepley1 authored Jan 4, 2025
2 parents d932d8d + 1e1f5d1 commit 2d1fdaf
Show file tree
Hide file tree
Showing 8 changed files with 91 additions and 57 deletions.
2 changes: 1 addition & 1 deletion project/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ def login_post():
answer = r.text
result = json.loads(r.text)
if 'error-codes' in result:
logging.error(result['error-codes'])
logging.error(f'hCaptcha error: {result["error-codes"]}')
if result['success'] != bool(1):
logging.error(f'Failed hCaptcha challenge: {get_ip()}')
flash('Please complete the Captcha correctly.', 'errorn')
Expand Down
113 changes: 62 additions & 51 deletions project/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -882,8 +882,12 @@ def bodyStats():
@login_required
def bodyRawStats():
''' Get records matching the request body. Regex query. (body_raw column, stored as blob) '''
body_pattern = unquote(request.args.get('body', ''))
# NOTE: Unquoting it again is what was causing args containing a % to break.
# Flask url-encodes/decodes automatically.
body_pattern = request.args.get('body', '')
#logging.debug(f'body_raw regex pattern: {body_pattern}')

# Validate pattern
if not body_pattern or not validate_regex(body_pattern):
return create_error_response(400, 'Invalid param', 'Missing or invalid regex pattern.')

Expand Down Expand Up @@ -1153,7 +1157,7 @@ def hostname_stats():
total_pages = pagination_data['total_pages'],
args_for_pagination = pagination_data['args_for_pagination'],
totalHits = len(stats),
statName = f'Hostname: {hostname}'
statName = f'Hostname: {hostname_q}'
)

@main.route('/stats/headers/single/<request_id>', methods = ['GET'])
Expand Down Expand Up @@ -1211,7 +1215,7 @@ def headers_single_json(request_id):
@login_required
def headers_key_search():
""" Find requests which include a given header. """
header_name = request.args.get('key', 'no input')
header_name = request.args.get('key', 'no input').strip()
if not validate_header_key(header_name):
return create_error_response(400, 'Invalid param', 'Invalid header name; value may contain only letters and hyphen.')

Expand All @@ -1223,7 +1227,7 @@ def headers_key_search():
FROM bots
WHERE (JSON_EXTRACT(headers_json, ?)) IS NOT NULL
ORDER BY id DESC
LIMIT 100000;
LIMIT 1000000;
"""
data_tuple = (f'$.{header_name}',)
c.execute(sql_query, data_tuple)
Expand Down Expand Up @@ -1357,14 +1361,14 @@ def full_search_regex():
conn.row_factory = sqlite3.Row
conn.create_function("REGEXP", 2, regexp)
c = conn.cursor()

#Get column names
sql_query = "PRAGMA table_info(bots)"
c.execute(sql_query)
columns = [column[1] for column in c.fetchall()]

# Construct sql query - join "<column> REGEXP <pattern> OR" for each column
sql_query = "SELECT * FROM bots WHERE "
conditions = [f"{column} REGEXP ?" for column in columns] #If using regexp over LIKE
conditions = [f"{column} REGEXP ?" for column in columns]
sql_query += ' OR '.join(conditions)
sql_query += ' ORDER BY id DESC;'
data_list = [q for i in enumerate(columns)]
Expand Down Expand Up @@ -1452,60 +1456,67 @@ def parse_search_form():
""" Redirect to one of the other views, depending on which search was selected. """
#logging.debug(request.args) #testing
chosen_query = request.args.get('chosen_query', '')
query_text = request.args.get('query_text', '')
query_text = request.args.get('query_text', '').strip()

# Flash message if no query input
# Flash message and return search page again if no query input
if not query_text or query_text is None:
flash('No query input', 'error')
return render_template('search.html')

# Same, if no field was selected
if not chosen_query or chosen_query is None:
flash('Must select a query.', 'error')
flash('Must select a field.', 'error')
return render_template('search.html')

#Parse and redirect, based on which field was selected
if chosen_query == 'ip_string':
ip_string = query_text
return redirect(url_for('main.ipStats', ipAddr = ip_string))
elif chosen_query == 'cidr_string':
cidr_string = query_text
return redirect(url_for('main.subnet_stats', net = cidr_string))
elif chosen_query == 'url':
url = query_text
url = '*' + url + '*'
return redirect(url_for('main.urlStats', url = url))
elif chosen_query == 'header_string':
header_string = query_text
return redirect(url_for('main.header_string_search', header_string = header_string))
elif chosen_query == 'header_key':
header_key = query_text.strip().title()
return redirect(url_for('main.headers_key_search', key = header_key))
elif chosen_query == 'content_type':
ct = query_text.strip()
ct = '%' + ct + '%'
return redirect(url_for('main.content_type_stats', ct = ct))
elif chosen_query == 'ua_string':
ua_string = query_text
ua_string = '%25' + ua_string + '%25'
return redirect(url_for('main.uaStats', ua = ua_string))
elif chosen_query == 'body_string':
body_string = query_text
body_string = '%' + body_string + '%'
return redirect(url_for('main.bodyStats', body = body_string))
elif chosen_query == 'body_raw':
q = query_text
return redirect(url_for('main.bodyRawStats', body = q))
elif chosen_query == 'hostname_endswith':
hostname_string = query_text.strip()
return redirect(url_for('main.hostname_stats', hostname = hostname_string))
elif chosen_query == 'hostname_contains':
hostname_string = query_text.strip()
hostname_string = hostname_string + '%'
return redirect(url_for('main.hostname_stats', hostname = hostname_string))
elif chosen_query == 'any_field':
q = query_text
return redirect(url_for('main.full_search', q = q))
match chosen_query:
case 'ip_string':
q = query_text
return redirect(url_for('main.ipStats', ipAddr = q))
case 'cidr_string':
q = query_text
return redirect(url_for('main.subnet_stats', net = q))
case 'url':
q = query_text
q = '*' + q + '*'
return redirect(url_for('main.urlStats', url = q))
case 'header_string':
q = query_text
return redirect(url_for('main.header_string_search', header_string = q))
case 'header_key':
q = query_text.title()
return redirect(url_for('main.headers_key_search', key = q))
case 'content_type':
q = query_text
q = '%' + q + '%'
return redirect(url_for('main.content_type_stats', ct = q))
case 'ua_string':
q = query_text
q = '%25' + q + '%25'
return redirect(url_for('main.uaStats', ua = q))
case 'body_string':
q = query_text
q = '%25' + q + '%25'
return redirect(url_for('main.bodyStats', body = q))
case 'body_raw':
q = query_text
return redirect(url_for('main.bodyRawStats', body = q))
case 'hostname_endswith':
q = query_text
return redirect(url_for('main.hostname_stats', hostname = q))
case 'hostname_contains':
q = query_text
# The view function will prepend another %, so only need to add a trailing one here.
q = q + '%'
return redirect(url_for('main.hostname_stats', hostname = q))
case 'any_field':
q = query_text
return redirect(url_for('main.full_search', q = q))
case 'any_field_regex':
q = query_text
return redirect(url_for('main.full_search_regex', q = q))
case '' | _:
return create_error_response(400, 'Invalid param', 'Invalid value for field name.')

# Misc routes

Expand Down
2 changes: 1 addition & 1 deletion project/templates/nav-links-stats.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@
<a href="{{url_for('main.hostname_stats', hostname='.cn')}}">Hostname: *.cn</a>
<a href="{{url_for('main.uaStats', ua='%http%://%')}}">User-Agent: Contains URL</a>
<a href="{{url_for('main.bodyStats', body='%25_%25') }}">Body: Any included</a>
<a href="{{url_for('main.bodyStats', body='%25%255B%255D=%25') }}">Body: AndroxGh0st/variants</a>{# '0x%255B%255D=%25' #}
<a href="{{url_for('main.bodyRawStats', body='^.*0x.*5B.*5D.*=.*$') }}">Body: AndroxGh0st/variants</a>
5 changes: 3 additions & 2 deletions project/templates/search.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ <h2>Search for HTTP requests where:</h2>
<option value="content_type">Content-Type</option>
<option value="ua_string">User-agent</option>
<option value="body_string">Body (processed) - like</option>
<option value="body_raw">Body (raw) - regex</option>
<option value="any_field">Any field</option>
<option value="body_raw">Body (raw) - regex search</option>
<option value="any_field">Any field (like)</option>
<option value="any_field_regex">Any field (regex search)</option>
</optgroup>
</select>
<br/>
Expand Down
2 changes: 1 addition & 1 deletion project/templates/signup.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ <h2>Create Account</h2>
</p>
<p>
<!-- <label for="password"><b>Password</b></label> -->
<input type="password" id="password" name="password" placeholder="Password" class="form-box" title="Password" />
<input type="password" id="password" name="password" placeholder="Password" class="form-box" title="Password" required />
</p>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<p>
Expand Down
2 changes: 1 addition & 1 deletion project/templates/stats.html
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ <h2>Most recent requests matching query</h2>
<td class="dataPath dataToLink sv1"><a href="{{url_for('main.path_stats', path=row['path'])}}">{{ row['path'] }}</a></td>
<td class="dataQueryString dataToLink mono smaller sv0"><a href="{{url_for('main.queriesStats', query=row['querystring']|urlencode)}}">{{row['querystring']|e}}</a></td>
<td class="dataURL dataToLink hidden sv0"><a href="{{url_for('main.urlStats', url=row['url'])}}">{{row['url']|e}}</a></td>
<td class="dataBodyBytes dataToLink mono smaller hidden sv0"><a href="{{url_for('main.bodyRawStats', body=row['body_raw'].decode(errors='replace')|urlencode)}}">{{row['body_raw'].decode(errors='replace')|e}}</a></td>
<td class="dataBodyBytes dataToLink mono smaller hidden sv0"><a href="{{url_for('main.bodyRawStats', body=row['body_raw'].decode(errors='replace'))}}">{{row['body_raw'].decode(errors='replace')|e}}</a></td>
<td class="dataPostData dataToLink mono smaller sv0"><a href="{{url_for('main.bodyStats', body=row['body_processed']|quote_plus)}}">{{row['body_processed']|e}}</a></td>
<td class="dataContentType dataToLink hidden sv0"><a href="{{url_for('main.content_type_stats', ct=row['contenttype']) }}">{{row['contenttype']|e}}</a></td>
<td class="dataHostname dataToLink sv0"><a href="{{url_for('main.hostname_stats', hostname=row['hostname'])}}">{{row['hostname']}}</a></td>
Expand Down
11 changes: 11 additions & 0 deletions test/files.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env python3
# Send files in request

import requests

url = 'http://localhost:5000/test/post-files'

files = {'file': open('./image.png' ,'rb')}

x = requests.post(url, files=files)
print(x.status_code)
11 changes: 11 additions & 0 deletions test/hikvision.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env python3
# Test detection of hikvision command injection exploit attempts

import requests

url = 'http://localhost:5000/SDK/webLanguage'
data = """<?xml version="1.0" encoding="UTF-8"?>
<language>$(ping -c 1 127.0.0.1)</language>"""
headers = {'content-type': 'application/x-www-form-urlencoded; charset=UTF-8'}
x = requests.put(url, data = data, headers = headers)
print(x.status_code)

0 comments on commit 2d1fdaf

Please sign in to comment.