33Functions related to handling and checking authentication.
44"""
55
6- import pyotp
76import re
8- import time
97
108from .base import Plugin
119
1210
1311class 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