Skip to content

Commit 4e1bd43

Browse files
committed
added Duo plugin and removed Authy
1 parent 4b48b2b commit 4e1bd43

File tree

6 files changed

+254
-341
lines changed

6 files changed

+254
-341
lines changed

docs/src/usage/plugins/auth.md

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,6 @@
22

33
This plugin deals with authenticating the user to Synack.
44

5-
## auth.build_otp()
6-
7-
> Use your stored otp_secret to generate a current OTP code
8-
>
9-
>> Examples
10-
>> ```python3
11-
>> >>> h.auth.build_otp()
12-
>> '1234567'
13-
>> ```
14-
155
## auth.get_api_token()
166

177
> Walks through the whole authentication workflow to get a new api_token
@@ -32,23 +22,6 @@ This plugin deals with authenticating the user to Synack.
3222
>> '45h998h4g5...45wh89g9wh'
3323
>> ```
3424
35-
## auth.get_login_grant_token(csrf, progress_token)
36-
37-
> Get a Login Grant Token by providing an OTP Code
38-
>
39-
> | Argument | Type | Description
40-
> | --- | --- | ---
41-
> | `csrf` | str | A CSRF Token used while logging in
42-
> | `progress_token` | str | A token returned after submitting a valid username and password
43-
>
44-
>> Examples
45-
>> ```python3
46-
>> >>> csrf = h.auth.get_login_csrf()
47-
>> >>> lpt = h.auth.get_login_progress_token(csrf)
48-
>> >>> h.auth.get_login_grant_token(csrf, lpt)
49-
>> '58t7i...rh87g58'
50-
>> ```
51-
5225
## auth.get_login_progress_token(csrf)
5326
5427
> Get the Login Progress Token by authenticating with email and password

src/synack/plugins/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from .auth import Auth
66
from .db import Db
77
from .debug import Debug
8+
from .duo import Duo
89
from .missions import Missions
910
from .notifications import Notifications
1011
from .scratchspace import Scratchspace

src/synack/plugins/api.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,15 @@ def notifications(self, method, path, **kwargs):
6464
self.state.notifications_token = ''
6565
return res
6666

67-
def request(self, method, path, include_std_headers=True, attempt=0, **kwargs):
67+
def request(self, method, path, attempts=0, **kwargs):
6868
"""Send API Request
6969
7070
Arguments:
7171
method -- Request method verb
7272
(GET, POST, etc.)
7373
path -- API endpoint path
7474
Can be an endpoint on platform.synack.com or a full URL
75+
attempts -- Number of times the request has been attempted
7576
headers -- Additional headers to be added for only this request
7677
data -- POST body dictionary
7778
query -- GET query string dictionary
@@ -86,7 +87,7 @@ def request(self, method, path, include_std_headers=True, attempt=0, **kwargs):
8687
verify = False
8788
proxies = self.state.proxies if self.state.use_proxies else None
8889

89-
if include_std_headers:
90+
if 'synack.com/api/' in url:
9091
headers = {
9192
'Authorization': f'Bearer {self.state.api_token}',
9293
'user_id': self.state.user_id
@@ -148,5 +149,5 @@ def request(self, method, path, include_std_headers=True, attempt=0, **kwargs):
148149
if attempts < 5:
149150
time.sleep(30)
150151
attempts += 1
151-
return self.request(method, path, include_std_headers, attempts, **kwargs)
152+
return self.request(method, path, attempts, **kwargs)
152153
return res

src/synack/plugins/auth.py

Lines changed: 4 additions & 276 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,19 @@
33
Functions related to handling and checking authentication.
44
"""
55

6-
import pyotp
76
import re
8-
import time
97

108
from .base import Plugin
119

1210

1311
class Auth(Plugin):
1412
def __init__(self, *args, **kwargs):
1513
super().__init__(*args, **kwargs)
16-
for plugin in ['Api', 'Db', 'Users']:
14+
for plugin in ['Api', 'Db', 'Duo', 'Users']:
1715
setattr(self,
1816
plugin.lower(),
1917
self.registry.get(plugin)(self.state))
2018

21-
def build_otp(self):
22-
"""Generate and return a OTP."""
23-
totp = pyotp.TOTP(self.state.otp_secret)
24-
totp.digits = 7
25-
totp.interval = 10
26-
totp.issuer = 'synack'
27-
return totp.now()
28-
2919
def get_api_token(self):
3020
"""Log in to get a new API token."""
3121
if self.users.get_profile():
@@ -36,10 +26,10 @@ def get_api_token(self):
3626
grant_token = None
3727
if csrf:
3828
auth_response = self.get_authentication_response(csrf)
39-
progress_token = auth_response.get('progress_token')
40-
duo_auth_url = auth_response.get('duo_auth_url')
29+
progress_token = auth_response.get('progress_token', '')
30+
duo_auth_url = auth_response.get('duo_auth_url', '')
4131
if duo_auth_url:
42-
grant_token = self.get_duo_push(duo_auth_url)
32+
grant_token = self.duo.get_grant_token(duo_auth_url)
4333
if grant_token:
4434
url = 'https://platform.synack.com/'
4535
headers = {
@@ -66,268 +56,6 @@ def get_login_csrf(self):
6656
res.text)
6757
return m.group(1)
6858

69-
def get_duo_push_variables(self, duo_auth_url):
70-
headers = {
71-
'Sec-Ch-Ua': '"Chromium";v="131", "Not_A Brand";v="24"',
72-
'Sec-Ch-Ua-Mobile': '?0',
73-
'Sec-Ch-Ua-Platform': '"Linux"',
74-
'Referrer': 'https://login.synack.com/',
75-
'Sec-Fetch-Site': 'cross-site',
76-
'Sec-Fetch-Mode': 'navigate',
77-
'Sec-Fetch-User': '?1',
78-
'Sec-Fetch-Dest': 'document'
79-
}
80-
res = self.api.request('GET', duo_auth_url, include_std_headers=False)
81-
if res.status_code == 200:
82-
return {
83-
'post_data': {
84-
'tx': re.search('<input type="hidden" name="tx" value="([^"]*)"', res.text).group(1),
85-
'parent': re.search('<input type="hidden" name="parent" value="([^"]*)"', res.text).group(1),
86-
'_xsrf': re.search('<input type="hidden" name="_xsrf" value="([^"]*)"', res.text).group(1),
87-
'version': re.search('<input type="hidden" name="version" value="([^"]*)"', res.text).group(1),
88-
'akey': re.search('<input type="hidden" name="akey" value="([^"]*)"', res.text).group(1),
89-
'has_session_trust_analysis_feature': re.search('<input type="hidden" name="has_session_trust_analysis_feature" value="([^"]*)"', res.text).group(1),
90-
'session_trust_extension_id': re.search('<input type="hidden" name="session_trust_extension_id" value="([^"]*)"', res.text).group(1),
91-
'java_version': re.search('<input type="hidden" name="java_version" value="([^"]*)"', res.text).group(1),
92-
'flash_version': re.search('<input type="hidden" name="flash_version" value="([^"]*)"', res.text).group(1),
93-
'screen_resolution_width': '3422',
94-
'screen_resolution_height': '1465',
95-
'extension_instance_key': '',
96-
'color_depth': '24',
97-
'has_touch_capability': 'false',
98-
'ch_ua_error': '',
99-
'client_hints': 'eyJicmFuZHMiOlt7ImJyYW5kIjoiQ2hyb21pdW0iLCJ2ZXJzaW9uIjoiMTMxIn0seyJicmFuZCI6Ik5vdF9BIEJyYW5kIiwidmVyc2lvbiI6IjI0In1dLCJmdWxsVmVyc2lvbkxpc3QiOltdLCJtb2JpbGUiOmZhbHNlLCJwbGF0Zm9ybSI6IkxpbnV4IiwicGxhdGZvcm1WZXJzaW9uIjoiIiwidWFGdWxsVmVyc2lvbiI6IiJ9',
100-
'is_cef_browser': 'false',
101-
'is_ipad_os': 'false',
102-
'is_ie_compatibility_mode': '',
103-
'is_user_verifying_platform_authenticator_available': 'false',
104-
'user_verifying_platform_authenticator_available_error': '',
105-
'acting_ie_version': '',
106-
'react_support': 'false',
107-
'react_support_error_message': ''
108-
},
109-
'sid': re.search('sid=([^&]*)', res.url).group(1),
110-
'url_last': res.url,
111-
'url_base': re.search('(https.*duosecurity\.com)/', res.url).group(1)
112-
}
113-
114-
def get_duo_push_vars_post(self, push_vars):
115-
headers = {
116-
'Sec-Ch-Ua': '"Chromium";v="131", "Not_A Brand";v="24"',
117-
'Sec-Ch-Ua-Mobile': '?0',
118-
'Sec-Ch-Ua-Platform': '"Linux"',
119-
'Referer': push_vars.get('url_last', ''),
120-
'Sec-Fetch-Site': 'same-origin',
121-
'Sec-Fetch-Mode': 'navigate',
122-
'Sec-Fetch-Dest': 'document',
123-
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
124-
'Content-Type': 'application/x-www-form-urlencoded'
125-
}
126-
res = self.api.request('POST', push_vars.get('url_last', ''), include_std_headers=False, headers=headers, data=push_vars.get('post_data', {}))
127-
if res.status_code == 200:
128-
push_vars['url_last'] = res.url
129-
return push_vars
130-
131-
def get_duo_push_xsrf_token(self, push_vars):
132-
headers = {
133-
'Sec-Ch-Ua': '"Chromium";v="131", "Not_A Brand";v="24"',
134-
'Sec-Ch-Ua-Mobile': '?0',
135-
'Sec-Ch-Ua-Platform': '"Linux"',
136-
'Referer': f'{push_vars.get("url_base", "")}/frame/v4/preauth/healthcheck?sid={push_vars.get("sid", "")}',
137-
'Sec-Fetch-Site': 'same-origin',
138-
'Sec-Fetch-Mode': 'navigate',
139-
'Sec-Fetch-Dest': 'document',
140-
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
141-
}
142-
res = self.api.request('GET', f'{push_vars.get("url_base", "")}/frame/v4/return', include_std_headers=False, headers=headers, query={"sid": push_vars.get('sid', '')})
143-
if res.status_code == 200:
144-
push_vars['xsrf'] = re.search('<input type="hidden" name="_xsrf" value="([^"]*)"', res.text).group(1)
145-
return push_vars
146-
147-
def get_duo_push_method_data(self, push_vars):
148-
headers = {
149-
'Sec-Ch-Ua': '"Chromium";v="131", "Not_A Brand";v="24"',
150-
'Sec-Ch-Ua-Mobile': '?0',
151-
'Sec-Ch-Ua-Platform': '"Linux"',
152-
'Referer': f'{push_vars.get("url_base", "")}/frame/v4/auth/prompt?sid={push_vars.get("sid", "")}',
153-
'Sec-Fetch-Site': 'same-origin',
154-
'Sec-Fetch-Mode': 'cors',
155-
'Sec-Fetch-Dest': 'empty',
156-
'Accept': '*/*',
157-
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
158-
'X-Xsrftoken': push_vars.get('xsrf', ''),
159-
}
160-
query = {
161-
'post_auth_action': 'OIDC_EXIT',
162-
'browser_features': '{"touch_supported":false,"platform_authenticator_status":"unavailable","webauthn_supported":true}',
163-
'sid': push_vars.get('sid', '')
164-
}
165-
res = self.api.request('GET', f'{push_vars.get("url_base", "")}/frame/v4/auth/prompt/data', include_std_headers=False, headers=headers, query=query)
166-
if res.status_code == 200:
167-
for method in res.json().get('response', {}).get('auth_method_order', []):
168-
if method.get('factor', '') == 'Duo Push':
169-
push_vars['prompt_device_key'] = method.get('deviceKey', '')
170-
break
171-
172-
for phone in res.json().get('response', {}).get('phones', []):
173-
if phone.get('key', '') == push_vars.get('prompt_device_key', ''):
174-
push_vars['prompt_device_index'] = phone.get('index', '')
175-
return push_vars
176-
177-
def get_duo_hotp(self):
178-
hotp = pyotp.HOTP(s=self.state.otp_secret)
179-
return hotp.generate_otp(self.state.otp_count)
180-
181-
def get_duo_hotp_txid(self, push_vars):
182-
# Doing the POST that should actually send the push notification
183-
headers = {
184-
'Sec-Ch-Ua': '"Chromium";v="131", "Not_A Brand";v="24"',
185-
'Sec-Ch-Ua-Mobile': '?0',
186-
'Sec-Ch-Ua-Platform': '"Linux"',
187-
'Referer': f'{push_vars.get("url_base", "")}/frame/v4/auth/prompt?sid={push_vars.get("sid", "")}',
188-
'Sec-Fetch-Site': 'same-origin',
189-
'Sec-Fetch-Mode': 'cors',
190-
'Sec-Fetch-Dest': 'empty',
191-
'Accept': '*/*',
192-
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
193-
'X-Xsrftoken': push_vars.get('xsrf', ''),
194-
}
195-
data = {
196-
'device': 'null',
197-
'passcode': self.get_duo_hotp(),
198-
'factor': 'Passcode',
199-
'postAuthDestination': 'OIDC_EXIT',
200-
'browser_features': '{"touch_supported":false,"platform_authenticator_status":"unavailable","webauthn_supported":true}',
201-
'sid': push_vars.get('sid', '')
202-
}
203-
res = self.api.request('POST', f'{push_vars.get("url_base", "")}/frame/v4/prompt', include_std_headers=False, headers=headers, data=data)
204-
if res.status_code == 200:
205-
push_vars['txid'] = res.json().get('response', {}).get('txid', '')
206-
self.db.otp_count+=1
207-
return push_vars
208-
209-
def get_duo_push_notification_txid(self, push_vars):
210-
# Doing the POST that should actually send the push notification
211-
headers = {
212-
'Sec-Ch-Ua': '"Chromium";v="131", "Not_A Brand";v="24"',
213-
'Sec-Ch-Ua-Mobile': '?0',
214-
'Sec-Ch-Ua-Platform': '"Linux"',
215-
'Referer': f'{push_vars.get("url_base", "")}/frame/v4/auth/prompt?sid={push_vars.get("sid", "")}',
216-
'Sec-Fetch-Site': 'same-origin',
217-
'Sec-Fetch-Mode': 'cors',
218-
'Sec-Fetch-Dest': 'empty',
219-
'Accept': '*/*',
220-
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
221-
'X-Xsrftoken': push_vars.get('xsrf', ''),
222-
}
223-
data = {
224-
'device': push_vars.get('prompt_device_index', ''),
225-
'factor': 'Duo Push',
226-
'postAuthDestination': 'OIDC_EXIT',
227-
'browser_features': '{"touch_supported":false,"platform_authenticator_status":"unavailable","webauthn_supported":true}',
228-
'sid': push_vars.get('sid', '')
229-
}
230-
res = self.api.request('POST', f'{push_vars.get("url_base", "")}/frame/v4/prompt', include_std_headers=False, headers=headers, data=data)
231-
if res.status_code == 200:
232-
push_vars['txid'] = res.json().get('response', {}).get('txid', '')
233-
return push_vars
234-
235-
def get_duo_push_status(self, push_vars):
236-
headers = {
237-
'Sec-Ch-Ua': '"Chromium";v="131", "Not_A Brand";v="24"',
238-
'Sec-Ch-Ua-Mobile': '?0',
239-
'Sec-Ch-Ua-Platform': '"Linux"',
240-
'Referer': f'{push_vars.get("url_base", "")}/frame/v4/auth/prompt?sid={push_vars.get("sid", "")}',
241-
'Sec-Fetch-Site': 'same-origin',
242-
'Sec-Fetch-Mode': 'cors',
243-
'Sec-Fetch-Dest': 'empty',
244-
'Accept': '*/*',
245-
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
246-
'X-Xsrftoken': push_vars.get('xsrf', ''),
247-
}
248-
data = {
249-
'txid': push_vars.get('txid', ''),
250-
'sid': push_vars.get('sid', '')
251-
}
252-
253-
for i in range(5):
254-
res = self.api.request('POST', f'{push_vars.get("url_base", "")}/frame/v4/status', include_std_headers=False, headers=headers, data=data)
255-
if res.status_code == 200:
256-
status_enum = res.json().get('response', {}).get('status_enum', -1)
257-
status = res.json().get('response', {}).get('status', -1)
258-
if status_enum == 5 or status == 'SUCCESS': # Valid Code
259-
break
260-
elif status_enum == 11: # Bad Code (or Future Code by 20+)
261-
print("Bad OTP Code Sent")
262-
print(res)
263-
print(res.json())
264-
elif status_enum == 44: # Prior Code
265-
self.db.otp_count+=5
266-
break
267-
elif status_enum == -1: # Code Changed
268-
print("Duo OTP Status Code Changed")
269-
print(res)
270-
print(res.json())
271-
time.sleep(5)
272-
273-
def get_duo_push_grant_token(self, push_vars):
274-
headers = {
275-
'Sec-Ch-Ua': '"Chromium";v="131", "Not_A Brand";v="24"',
276-
'Sec-Ch-Ua-Mobile': '?0',
277-
'Sec-Ch-Ua-Platform': '"Linux"',
278-
'Referer': f'{push_vars.get("url_base", "")}/frame/v4/auth/prompt?sid={push_vars.get("sid", "")}',
279-
'Sec-Fetch-Site': 'same-origin',
280-
'Sec-Fetch-Mode': 'cors',
281-
'Sec-Fetch-Dest': 'empty',
282-
'Accept': '*/*',
283-
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
284-
'X-Xsrftoken': push_vars.get('xsrf', ''),
285-
}
286-
data = {
287-
'sid': push_vars.get('sid', ''),
288-
'txid': push_vars.get('txid', ''),
289-
'factor': 'Passcode',
290-
'device_key': 'null',
291-
#'factor': 'Duo Push',
292-
#'device_key': push_vars.get('prompt_device_key', ''),
293-
'_xsrf': push_vars.get('xsrf', ''),
294-
'dampen_choice': 'false'
295-
}
296-
res = self.api.request('POST', f'{push_vars.get("url_base", "")}/frame/v4/oidc/exit', include_std_headers=False, headers=headers, data=data)
297-
if res.status_code == 200:
298-
push_vars['grant_token'] = re.search('grant_token=([^&]*)', res.url).group(1)
299-
300-
return push_vars
301-
302-
def get_duo_push(self, duo_auth_url):
303-
"""Make Duo send a push notification"""
304-
push_vars = self.get_duo_push_variables(duo_auth_url)
305-
self.get_duo_push_vars_post(push_vars)
306-
push_vars = self.get_duo_push_xsrf_token(push_vars)
307-
self.get_duo_push_vars_post(push_vars)
308-
push_vars = self.get_duo_push_method_data(push_vars)
309-
push_vars = self.get_duo_hotp_txid(push_vars)
310-
#push_vars = self.get_duo_push_notification_txid(push_vars)
311-
self.get_duo_push_status(push_vars)
312-
push_vars = self.get_duo_push_grant_token(push_vars)
313-
return push_vars.get('grant_token', '')
314-
315-
def get_login_grant_token(self, csrf, progress_token):
316-
"""Get grant token from authy totp verification"""
317-
headers = {
318-
'X-Csrf-Token': csrf
319-
}
320-
data = {
321-
#"authy_token": self.build_otp(),
322-
"progress_token": progress_token
323-
}
324-
res = self.api.login('POST',
325-
'authenticate',
326-
headers=headers,
327-
data=data)
328-
if res.status_code == 200:
329-
return res.json().get("grant_token")
330-
33159
def get_authentication_response(self, csrf):
33260
"""Get progress_token and duo_auth_url from email and password login"""
33361
headers = {

0 commit comments

Comments
 (0)