Skip to content

Commit 267a26b

Browse files
committed
code review changes from smcintyre-r7@
1 parent 5cb1968 commit 267a26b

File tree

3 files changed

+93
-107
lines changed

3 files changed

+93
-107
lines changed
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import hashlib
2+
import re
3+
import argparse
4+
import sys
5+
from urllib.parse import urlsplit, parse_qs, unquote, quote
6+
from typing import Dict, List, Tuple
7+
8+
_SIGNATURE_REGEX = re.compile(r'[^A-Za-z0-9/?_.=&{}\[\]":, -]')
9+
10+
def compute_signature(method: str, path: str, body: str = '', key: str = '') -> str:
11+
if not method or not path:
12+
raise ValueError("Method and path must be provided.")
13+
14+
url_parts = urlsplit(path)
15+
base_path = url_parts.path
16+
17+
if not base_path.startswith('/'):
18+
base_path = '/' + base_path
19+
20+
raw_query_params: Dict[str, List[str]] = parse_qs(
21+
url_parts.query, keep_blank_values=True, strict_parsing=False
22+
)
23+
24+
canonical_query: List[Tuple[str, str]] = []
25+
for k, v_list in raw_query_params.items():
26+
if k == '_signature':
27+
continue
28+
29+
value = unquote(v_list[0]) if v_list else ''
30+
canonical_query.append((k, value))
31+
32+
canonical_query.sort(key=lambda item: item[0])
33+
34+
query_string = '&'.join(f"{k}={quote(v)}" for k, v in canonical_query)
35+
36+
if query_string:
37+
canonical_path = f"{base_path}?{query_string}"
38+
else:
39+
canonical_path = base_path
40+
41+
canonical_path = re.sub(_SIGNATURE_REGEX, '-', canonical_path)
42+
43+
body_for_signing = re.sub(_SIGNATURE_REGEX, '-', body)
44+
45+
if not key:
46+
password_hash = "da39a3ee5e6b4b0d3255bfef95601890afd80709"
47+
else:
48+
password_hash = hashlib.sha1(key.encode('utf-8')).hexdigest().lower()
49+
50+
data = f"{method.upper()}:{canonical_path}:{body_for_signing}:{password_hash}"
51+
52+
return hashlib.sha1(data.encode('utf-8')).hexdigest().lower()
53+
54+
def main():
55+
parser = argparse.ArgumentParser(description="Computes a SHA1 signature for an HTTP request.")
56+
57+
parser.add_argument('--method', type=str, required=True,
58+
choices=['GET', 'POST', 'PUT', 'DELETE'],
59+
help="The HTTP method (e.g., GET).")
60+
parser.add_argument('--path', type=str, required=True,
61+
help="The canonical path (e.g., /api/resource?param=value).")
62+
parser.add_argument('--key', type=str, default='',
63+
help="The secret key. Defaults to an empty string.")
64+
parser.add_argument('--body', type=str, default='',
65+
help="The request body as a string. Defaults to an empty string.")
66+
67+
try:
68+
args = parser.parse_args()
69+
70+
signature = compute_signature(
71+
method=args.method,
72+
path=args.path,
73+
body=args.body,
74+
key=args.key
75+
)
76+
77+
print(f"Computed Signature: {signature}")
78+
79+
except ValueError as e:
80+
sys.stderr.write(f"Error: {e}\n")
81+
sys.exit(1)
82+
except Exception as e:
83+
sys.stderr.write(f"An unexpected error occurred: {e}\n")
84+
sys.exit(1)
85+
86+
87+
if __name__ == '__main__':
88+
main()

documentation/modules/exploit/multi/http/motioneye_auth_rce_cve_2025_60787.md renamed to documentation/modules/exploit/linux/http/motioneye_auth_rce_cve_2025_60787.md

Lines changed: 3 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ An attacker can execute arbitrary system commands in the context of the user run
1313
## Exploit Workflow
1414

1515
1. Adds a new camera in MotionEye Frontend.
16-
2. Injects the payload into the image_file_name field (used for naming camera screenshots).
16+
2. Injects the payload into the `image_file_name` field (used for naming camera screenshots).
1717
3. Captures a screenshot ("snapshot" in the terminology of MotionEye), triggering the payload.
1818

1919
## Testing
@@ -104,102 +104,9 @@ BUG_REPORT_URL="https://bugs.debian.org/"
104104

105105
## Script for signing requests
106106

107-
The application verifies request signatures, so I wrote a small script to sign requests manually.
108-
109-
You won't need it if you use the exploit, but it can be useful for debugging.
110-
111-
```
112-
import hashlib
113-
import re
114-
import argparse
115-
import sys
116-
from urllib.parse import urlsplit, parse_qs, unquote, quote
117-
from typing import Dict, List, Tuple
118-
119-
_SIGNATURE_REGEX = re.compile(r'[^A-Za-z0-9/?_.=&{}\[\]":, -]')
120-
121-
def compute_signature(method: str, path: str, body: str = '', key: str = '') -> str:
122-
if not method or not path:
123-
raise ValueError("Method and path must be provided.")
124-
125-
url_parts = urlsplit(path)
126-
base_path = url_parts.path
127-
128-
if not base_path.startswith('/'):
129-
base_path = '/' + base_path
130-
131-
raw_query_params: Dict[str, List[str]] = parse_qs(
132-
url_parts.query, keep_blank_values=True, strict_parsing=False
133-
)
134-
135-
canonical_query: List[Tuple[str, str]] = []
136-
for k, v_list in raw_query_params.items():
137-
if k == '_signature':
138-
continue
139-
140-
value = unquote(v_list[0]) if v_list else ''
141-
canonical_query.append((k, value))
142-
143-
canonical_query.sort(key=lambda item: item[0])
144-
145-
query_string = '&'.join(f"{k}={quote(v)}" for k, v in canonical_query)
146-
147-
if query_string:
148-
canonical_path = f"{base_path}?{query_string}"
149-
else:
150-
canonical_path = base_path
151-
152-
canonical_path = re.sub(_SIGNATURE_REGEX, '-', canonical_path)
153-
154-
body_for_signing = re.sub(_SIGNATURE_REGEX, '-', body)
155-
156-
if not key:
157-
password_hash = "da39a3ee5e6b4b0d3255bfef95601890afd80709"
158-
else:
159-
password_hash = hashlib.sha1(key.encode('utf-8')).hexdigest().lower()
160-
161-
data = f"{method.upper()}:{canonical_path}:{body_for_signing}:{password_hash}"
162-
163-
return hashlib.sha1(data.encode('utf-8')).hexdigest().lower()
164-
165-
def main():
166-
parser = argparse.ArgumentParser(description="Computes a SHA1 signature for an HTTP request.")
167-
168-
parser.add_argument('--method', type=str, required=True,
169-
choices=['GET', 'POST', 'PUT', 'DELETE'],
170-
help="The HTTP method (e.g., GET).")
171-
parser.add_argument('--path', type=str, required=True,
172-
help="The canonical path (e.g., /api/resource?param=value).")
173-
parser.add_argument('--key', type=str, default='',
174-
help="The secret key. Defaults to an empty string.")
175-
parser.add_argument('--body', type=str, default='',
176-
help="The request body as a string. Defaults to an empty string.")
177-
178-
try:
179-
args = parser.parse_args()
180-
181-
signature = compute_signature(
182-
method=args.method,
183-
path=args.path,
184-
body=args.body,
185-
key=args.key
186-
)
187-
188-
print(f"Computed Signature: {signature}")
189-
190-
except ValueError as e:
191-
sys.stderr.write(f"Error: {e}\n")
192-
sys.exit(1)
193-
except Exception as e:
194-
sys.stderr.write(f"An unexpected error occurred: {e}\n")
195-
sys.exit(1)
196-
197-
198-
if __name__ == '__main__':
199-
main()
200-
```
107+
A script for manually signing requests is available in data/exploits/CVE-2025-60787/sign_request.py and can be used for debugging purposes.
201108

202109
Example of usage:
203110
```
204-
python3 ./main.py --method "GET" --path "/config/1/get/?force=true&_=1759747431350&_username=admin" --body "" --key ""
111+
python3 ./sign_request.py --method "GET" --path "/config/1/get/?force=true&_=1759747431350&_username=admin" --body "" --key ""
205112
```

modules/exploits/multi/http/motioneye_auth_rce_cve_2025_60787.rb renamed to modules/exploits/linux/http/motioneye_auth_rce_cve_2025_60787.rb

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -124,12 +124,7 @@ def compute_signature(method, path, body = nil, key = '')
124124
cleaned_path = clean_string(canonical_path)
125125
cleaned_body = clean_string(body)
126126

127-
if key.empty?
128-
# SHA1 hash of an empty string
129-
key_hash = 'da39a3ee5e6b4b0d3255bfef95601890afd80709'
130-
else
131-
key_hash = Digest::SHA1.hexdigest(key).downcase
132-
end
127+
key_hash = Digest::SHA1.hexdigest(key).downcase
133128

134129
data = "#{method}:#{cleaned_path}:#{cleaned_body}:#{key_hash}"
135130

@@ -180,10 +175,6 @@ def send_signed_request_cgi(opts = {})
180175
return send_request_cgi(new_opts)
181176
end
182177

183-
def random_ipv4
184-
Array.new(4) { rand(0..255) }.join('.')
185-
end
186-
187178
def add_camera
188179
print_status('Adding malicious camera...')
189180

@@ -193,7 +184,7 @@ def add_camera
193184
'ctype' => 'application/json',
194185
'data' => {
195186
'scheme' => 'rstp',
196-
'host' => random_ipv4,
187+
'host' => Faker::Internet.ip_v4_address,
197188
'port' => '',
198189
'path' => '/',
199190
'username' => '',

0 commit comments

Comments
 (0)