Instantly share code, notes, and snippets.
Created
July 20, 2023 06:49
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save OnceUponALoop/b60b4457c851fee1e22e652d208ac25e to your computer and use it in GitHub Desktop.
Manual SAML Auth with curl against a federated sharepoint server, once authenticated use the token to upload a file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/python | |
#=================================================================================== | |
# Title : sharepoint-upload.py | |
# | |
# Description : SAML Authentication example against a sharepoint site | |
# uploads a file to a document library after auth | |
# | |
# Author : Firas AlShafei | |
# | |
# Version : v1.0 | |
# | |
# Last Revised : 11/05/2016 | |
# | |
#=================================================================================== | |
import os | |
import sys | |
import copy | |
import base64 | |
import requests | |
from lxml import etree as ET | |
import lxml | |
# <editor-fold desc="Header - Process Overview"> | |
# Scripted implementation in python to walk through SAML and understand it | |
# | |
# Getting this damned thing to work took a lot longer than it needs to due to bunch of old information and half | |
# baked solutions found online. It took longer to untangle than it did to actually understand whats happening. | |
# | |
# By far the biggest issue was getting the right information for our corporate sharepoint environment | |
# - Query the MS Home Realm Discovery service for that infomrmation! | |
# - Caused a bunch of "bad request" failures when trying to send requests to the wrong ADFS STS | |
# - If the xml=1 parameter is not provided the MS HRD service returns a limited JSON response | |
# - The limited JSON response doesnt include the STSAuthURL value we need | |
# | |
# +------------------------------------------------------------------------------------+ | |
# Sharepoint File Upload Process Overview | |
# +------------------------------------------------------------------------------------+ | |
# | |
# +-------+ | |
# | | Username & domain +----------------------------------+ | |
# | | --------------------------------------> | | | |
# | (1) | | MS Home Realm Discovery | | |
# | | <-------------------------------------- | | | |
# | | STSAuthURL +----------------------------------+ | |
# | | | |
# | | | |
# | | SAML Request with user/pass +----------------------------------+ | |
# | | --------------------------------------> | | | |
# | (2) | | ADFS Identity Provider STS | | |
# | | <-------------------------------------- | | | |
# | | SAML Response Token with assertion +----------------------------------+ | |
# | | | |
# | | | |
# | | SAML Request wtih assertion +----------------------------------+ | |
# | | --------------------------------------> | | | |
# | (3) | | Microsoft Office STS | | |
# | | <-------------------------------------- | | | |
# | | SAML Response with security token t +----------------------------------+ | |
# | | | |
# | | | |
# | | Security Token t +----------------------------------+ | |
# | | --------------------------------------> | | | |
# | (4) | | Sharepoint Site (Base URL) | | |
# | | <-------------------------------------- | | | |
# | | Auth Cookies (FedAuth & rtFA) +----------------------------------+ | |
# | | | |
# | | | |
# | | REST Req ContextInfo & Auth Cookies +----------------------------------+ | |
# | | --------------------------------------> | | | |
# | (5) | | Sharepoint Site | | |
# | | <-------------------------------------- | | | |
# | | FormDigestValue (X+RequestDigest) +----------------------------------+ | |
# | | | |
# | | | |
# | | SOAP Req List & Auth Cookies +----------------------------------+ | |
# | | --------------------------------------> | | | |
# | (6) | | Sharepoint Site (lists.asmx) | | |
# | | <-------------------------------------- | | | |
# | | List GUID +----------------------------------+ | |
# | | | |
# | | | |
# | | File + Auth Cookies + Digest + GUID +----------------------------------+ | |
# | | --------------------------------------- | | | |
# | (7) | | Sharepoint Site (upload.aspx) | | |
# | | <-------------------------------------+ | | | |
# | | Success! "[1]" +----------------------------------+ | |
# +-------+ | |
# | |
# (1) GET to MS Home Realm Discovery service | |
# Extract the federation service url (STSAuthURL) from response XML | |
# | |
# Site : https://login.microsoftonline.com/GetUserRealm.srf | |
# | |
# (2) POST SAML Request Logon Security Token (RST) to federation STS | |
# Extract the SAML RequestToken Assertion from response xml | |
# | |
# Site : https://example-corp.com/adfs/services/trust/2005/usernamemixed | |
# | |
# (3) POST SAML Request Sevice Security Token (RST) + SAML Assertion to MS STS | |
# Extract service token (t) from the response xml | |
# | |
# Site : https://login.microsoftonline.com/extSTS.srf | |
# | |
# (4) POST service token (t) to sharepoint base sign-in URL | |
# Save authentication cookies (FedAuth & rtFA) | |
# | |
# Site : https://excorp.sharepoint.com/_forms/default.aspx?wa=wsignin1.0 | |
# | |
# (5) POST request with auth cookies to SP API contectinfo | |
# Extract FormsDigestValue from response XML | |
# | |
# Site : https://excorp.sharepoint.com/sites/mysite/_api/contextinfo | |
# | |
# (6) POST SOAP Request GetList with auth cookies to Lists | |
# Queries the sharepoint site for information about the list provided | |
# Extract list id (GUID) from response XML | |
# | |
# Site : https://excorp.sharepoint.com/sites/mysite/_vti_bin/Lists.asmx | |
# | |
# (7) POST File + Auth Cookies + FormsDigest + List GUID | |
# Perform the actual file upload | |
# | |
# Site : https://excorp.sharepoint.com/sites/mysite/_layouts/15/upload.asmx | |
# | |
# </editor-fold> | |
# <editor-fold desc="User Variables"> | |
# Sharepoint username - has to include the @example-corp.com | |
userName = '[email protected]' | |
# Base64 Encoded password just to avoid shoulder surfing disclosure | |
# Encode the password with base64.b64encode('Pa$$word!') | |
userPass = base64.b64decode('UGEkJHdvcmQh') | |
# Sharepoint site | |
spSite = 'https://excorp.sharepoint.com/sites/MYSITE' | |
# Sharepoint document list to upload to | |
spListName = "Submits" | |
# Verify SSL reqeuests | |
# Disabled since the server doesn't have the corporate ADFS cert installed | |
verifySSL = False | |
# Set each url to blank to connect directly without a proxy | |
proxies = { | |
'http' : "http://127.0.0.1:8888", # "http://10.127.146.2:5999", | |
'https' : "http://127.0.0.1:8888" # "http://10.127.146.2:5999" | |
} | |
# </editor-fold> | |
# <editor-fold desc="Script Variables"> | |
### Dynamic Variables | |
# Stupid way to check if a file was provided | |
if len(sys.argv) > 1: | |
uploadFile = sys.argv[1] | |
else: | |
print 'Error: No upload file provided' | |
print sys.argv[0] + " " + "full-file-path" | |
exit(1) | |
# Get file name from full path | |
# Stupid check to see if file exists | |
if os.path.isfile(uploadFile): | |
uploadFileName = os.path.basename(uploadFile) | |
else: | |
sys.exit("Error - File not found! (%s)" % uploadFile) | |
# Parse base URL of sharepoint site | |
spSiteBase = spSite.split('//', 1)[0] + '//' + spSite.split('//', 1)[1].split('/', 1)[0] | |
# Disable SSL Security Warning when verifySSL is set to False | |
# InsecureRequestWarning: Unverified HTTPS request is being made. Adding certificate verification is strongly advised. | |
if not verifySSL: | |
from requests.packages.urllib3.exceptions import InsecureRequestWarning | |
requests.packages.urllib3.disable_warnings(InsecureRequestWarning) | |
### Static Variables | |
# Various URLs | |
msoHrdUrl = "https://login.microsoftonline.com/GetUserRealm.srf" | |
msoStsUrl = "https://login.microsoftonline.com/extSTS.srf" | |
spSignInUrl = "/_forms/default.aspx?wa=wsignin1.0" | |
ctxInfoUrl = "/_api/contextinfo" | |
spUploadUrl = "/_layouts/15/upload.aspx" | |
spListUrl = "/_vti_bin/Lists.asmx" | |
# User Agent to use when connecting | |
# Pretend - Sharepoint doesnt like non-browsers aparently | |
# Getting philosophical with this... | |
userAgent = "Mozilla/5.0 (we know too well that our freedom is incomplete)" | |
# Sharepoint API GetList Request | |
# Populate: | |
# - spListName : String name of sharepoint document list | |
getListRequest = """ | |
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:soap="http://schemas.microsoft.com/sharepoint/soap/"> | |
<soapenv:Header/> | |
<soapenv:Body> | |
<soap:GetList> | |
<soap:listName>{spListName}</soap:listName> | |
</soap:GetList> | |
</soapenv:Body> | |
</soapenv:Envelope>""" | |
# Sharepoint API GetListItems Request | |
# Populate | |
# - spiListName : String name of sharepoint document list | |
# - uploadFileName : String name of uploaded file | |
getListItemsRequest = """ | |
<?xml version='1.0' encoding='utf-8'?> | |
<GetListItems xmlns="http://schemas.microsoft.com/sharepoint/soap/"> | |
<listName>{spListName}</listName> | |
<viewName></viewName> | |
<Query></Query> | |
<ViewFields></ViewFields> | |
<rowLimit></rowLimit> | |
<queryOptions xmlns:SOAPSDK9="http://schemas.microsoft.com/sharepoint/soap/" > | |
<QueryOptions/> | |
</queryOptions> | |
</GetListItems>""" | |
# Logon Token Request | |
# Populate: | |
# - stsUsernameMixedUrl : ADFS Identity provider STS | |
# ex. https://example-corp.com/adfs/services/trust/2005/usernamemixed | |
# - userName : Sharepoint username (ex. [email protected]) | |
# - userPass : Sharepoint user password | |
logonTokenRequest = """ | |
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"> | |
<s:Header> | |
<a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</a:Action> | |
<a:ReplyTo> | |
<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address> | |
</a:ReplyTo> | |
<a:To s:mustUnderstand="1">{stsUsernameMixedUrl}</a:To> | |
<o:Security s:mustUnderstand="1" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"> | |
<o:UsernameToken> | |
<o:Username>{userName}</o:Username> | |
<o:Password>{userPass}</o:Password> | |
</o:UsernameToken> | |
</o:Security> | |
</s:Header> | |
<s:Body> | |
<t:RequestSecurityToken xmlns:t="http://schemas.xmlsoap.org/ws/2005/02/trust"> | |
<wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy"> | |
<a:EndpointReference> | |
<a:Address>urn:federation:MicrosoftOnline</a:Address> | |
</a:EndpointReference> | |
</wsp:AppliesTo> | |
<t:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</t:KeyType> | |
<t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType> | |
<t:TokenType>urn:oasis:names:tc:SAML:1.0:assertion</t:TokenType> | |
</t:RequestSecurityToken> | |
</s:Body> | |
</s:Envelope>""" | |
# Service Token Request Template | |
# Populate: | |
# - msoSTSUrl : ex. https://login.microsoftonline.com/extSTS.srf | |
# - samlAssert : SAML Assertion extracted from response to logonTokenRequest | |
# - spSite : ex. https://excorp.sharepoint.com/sites/mysite | |
serviceTokenRequest = """ | |
<s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"> | |
<s:Header> | |
<a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</a:Action> | |
<a:ReplyTo> | |
<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address> | |
</a:ReplyTo> | |
<a:To s:mustUnderstand="1">{msoStsUrl}</a:To> | |
<o:Security s:mustUnderstand="1" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"> | |
{samlAssert} | |
</o:Security> | |
</s:Header> | |
<s:Body> | |
<t:RequestSecurityToken xmlns:t="http://schemas.xmlsoap.org/ws/2005/02/trust"> | |
<wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy"> | |
<a:EndpointReference> | |
<a:Address>{spSite}</a:Address> | |
</a:EndpointReference> | |
</wsp:AppliesTo> | |
<t:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</t:KeyType> | |
<t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType> | |
<t:TokenType>urn:oasis:names:tc:SAML:1.0:assertion</t:TokenType> | |
</t:RequestSecurityToken> | |
</s:Body> | |
</s:Envelope>""" | |
# SOAP XML namespace map | |
# Needed when using lxml elementree | |
# Allow find in xmlns:element format | |
# Instead of in {xmlns-definition-url}element format | |
namespaceMap = { | |
'a' : 'http://www.w3.org/2005/08/addressing', | |
's' : 'http://www.w3.org/2003/05/soap-envelope', | |
'u' : 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd', | |
'o' : 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd', | |
'wsse' : 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd', | |
't' : 'http://schemas.xmlsoap.org/ws/2005/02/trust', | |
'wsu' : 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd', | |
'wsp' : 'http://schemas.xmlsoap.org/ws/2004/09/policy', | |
'wsa' : 'http://www.w3.org/2005/08/addressing', | |
'saml' : 'urn:oasis:names:tc:SAML:1.0:assertion', | |
'ds' : 'http://www.w3.org/2000/09/xmldsig#', | |
'd' : 'http://schemas.microsoft.com/ado/2007/08/dataservices', | |
'soapenv' : "http://schemas.xmlsoap.org/soap/envelope/" | |
} | |
# </editor-fold> | |
# <editor-fold desc="(1) Get the ADFS Authority URL"> | |
# | |
# Issue a request to the MSO HRD - Home Realm Discovery service | |
# Returns the federation service URL | |
# Strange that it returns limited information unless the xml=1 parameter is provided | |
# We're looking for the STSAuthURL - took a while to realize that the AuthURL wasn't the one we need | |
# Construct request options | |
# - Params : Params needed for proper MS HRD to give us the full response | |
requestOptions = { | |
'url' : msoHrdUrl, | |
'headers' : { 'User-Agent' : userAgent }, | |
'params' : { | |
"handler" : "1", | |
"xml" : "1", | |
"login" : userName | |
}, | |
'verify' : verifySSL, | |
'proxies' : proxies | |
} | |
response = requests.get(**requestOptions) | |
if response.status_code == 200: | |
adfsAuthUrl = ET.fromstring(response.content).find('STSAuthURL').text | |
# </editor-fold> | |
# <editor-fold desc="(2) Request Logon Token"> | |
# Populate the SAML Logon Token Request | |
# Using acquired STS Auth URL returned by MSO HRD | |
samlRequestValues={ | |
'userName' : userName, | |
'userPass' : userPass, | |
'stsUsernameMixedUrl' : adfsAuthUrl | |
} | |
logonTokenRequest = logonTokenRequest.format(**samlRequestValues) | |
# Construct request options | |
# - Headers/Content-Type : SOAP Request | |
# - Data : SAML Request Logon Token XML | |
requestOptions = { | |
'url' : adfsAuthUrl, | |
'headers' : { | |
'User-Agent' : userAgent, | |
'Content-Type' : 'application/soap+xml;charset=utf-8' | |
}, | |
'data' : logonTokenRequest, | |
'verify' : verifySSL, | |
'proxies' : proxies | |
} | |
# Send Request | |
response = requests.post(**requestOptions) | |
# Process Response | |
# Extract SAML Assertion | |
samlAssertElement = ET.fromstring(response.content) | |
samlAssertElement = samlAssertElement.find('.//saml:Assertion',namespaceMap) | |
samlAssert = ET.tostring(samlAssertElement, encoding='utf8') | |
# </editor-fold> | |
# <editor-fold desc="(3) Request Service Token - Receive (t)"> | |
# Populate the SAML Service Token Request | |
samlRequestValues={ | |
'msoStsUrl' : msoStsUrl, | |
'samlAssert' : samlAssert, | |
'spSite' : spSite | |
} | |
serviceTokenRequest = serviceTokenRequest.format(**samlRequestValues) | |
# Construct request options | |
# - Headers/Content-Type : SOAP Request | |
# - Data : SAML Service Token Request XML | |
requestOptions = { | |
'url' : msoStsUrl, | |
'headers' : { | |
'User-Agent' : userAgent, | |
'Content-Type' : 'application/soap+xml;charset=utf-8' | |
}, | |
'data' : serviceTokenRequest, | |
'verify' : verifySSL, | |
'proxies' : proxies | |
} | |
# Send Request | |
response = requests.post(**requestOptions) | |
# Process Response | |
# Extract security token | |
serviceToken = ET.fromstring(response.content) | |
serviceToken = serviceToken.find('.//wsse:BinarySecurityToken',namespaceMap).text | |
# </editor-fold> | |
# <editor-fold desc="(4) Login to spSite with service token (t)"> | |
# Construct request options | |
# - Headers/Content-Type : SOAP Request | |
# - Data : Service Token (t) | |
# - allow_redirects : Dont follow, we don't care enough - the first response will give us the cookies | |
requestOptions = { | |
'url' : spSiteBase + spSignInUrl, | |
'headers' : { | |
'User-Agent' : userAgent, | |
'Content-Type' : 'application/x-www-form-urlencoded' | |
}, | |
'data' : serviceToken, | |
'allow_redirects' : False, | |
'verify' : verifySSL, | |
'proxies' : proxies | |
} | |
# Send Request | |
response = requests.post(**requestOptions) | |
# Process Response | |
# Save Auth Cookies (FedAuth & rtFA) | |
authCookies = response.cookies | |
# </editor-fold> | |
# <editor-fold desc="(5) Get request digest"> | |
# The request digest is needed to upload using the default upload.aspx | |
# Construct request options | |
# - cookies : saved auth cookuies | |
requestOptions = { | |
'url' : spSite + ctxInfoUrl, | |
'headers' : { 'User-Agent' : userAgent }, | |
'cookies' : authCookies, | |
'verify' : verifySSL, | |
'proxies' : proxies | |
} | |
# Send Request | |
response = requests.post(**requestOptions) | |
# Process Response | |
# Extract requestDigest | |
requestDigest = ET.fromstring(response.content) | |
requestDigest = requestDigest.find('.//d:FormDigestValue',namespaceMap).text | |
# </editor-fold> | |
# <editor-fold desc="(6) Get list GUID"> | |
# Add spListName value to getListRequest | |
getListRequest = getListRequest.format(**{'spListName': spListName}) | |
# Construct request options | |
# - Headers/Content-Type : SOAP Request | |
# - Headers/SOAPAction : SOAP Request | |
# - data : POST data (getList SOAP request) | |
# - cookies : saved auth cookuies | |
requestOptions = { | |
'url' : spSite + spListUrl, | |
'headers' : { 'User-Agent' : userAgent, | |
'Content-Type' : 'text/xml;charset=UTF-8', | |
'SOAPAction' : '"http://schemas.microsoft.com/sharepoint/soap/GetList"'}, | |
'data' : getListRequest, | |
'cookies' : authCookies, | |
'verify' : verifySSL, | |
'proxies' : proxies | |
} | |
# Send Request | |
response = requests.post(**requestOptions) | |
# Process Response | |
# Extract Sharepoint List GUID | |
spListGuid = ET.fromstring(response.content) | |
spListGuid = spListGuid.find('.//{http://schemas.microsoft.com/sharepoint/soap/}List').attrib['ID'] | |
# </editor-fold> | |
# <editor-fold desc="(7) Upload file"> | |
# Construct request options | |
# - Headers/Content-Type : SOAP Request | |
# - Headers/SOAPAction : SOAP Request | |
# - data : POST data (getList SOAP request) | |
# - cookies : saved auth cookuies | |
requestOptions = { | |
'url' : spSite + spUploadUrl, | |
'headers' : { | |
'User-Agent' : userAgent, | |
'X-RequestDigest' : requestDigest, | |
'ClientSource' : 'MultiFileUploadDialog' | |
}, | |
'params' : { | |
'isAjax' : '1', | |
'List' : spListGuid | |
}, | |
'files' : { | |
uploadFileName : open(uploadFile, 'rb') | |
}, | |
'cookies' : authCookies, | |
'verify' : verifySSL, | |
'proxies' : proxies | |
} | |
response = requests.post(**requestOptions) | |
print 'File Upload Response: %s' % response.content | |
# </editor-fold> | |
getListItemsRequestValues={ | |
'spListName' : spListName | |
#'uploadFileName' : uploadFileName | |
} | |
# Add spListName value to getListItemsRequest | |
getListItemsRequest = getListItemsRequest.format(**getListItemsRequestValues) | |
# Construct request options | |
# - Headers/Content-Type : SOAP Request | |
# - Headers/SOAPAction : SOAP Request | |
# - data : POST data (getList SOAP request) | |
# - cookies : saved auth cookuies | |
requestOptions = { | |
'url' : spSite + spListUrl, | |
'headers' : { 'User-Agent' : userAgent, | |
'Content-Type' : 'text/xml;vi =UTF-8', | |
'SOAPAction' : '"http://schemas.microsoft.com/sharepoint/soap/GetListItems"'}, | |
'data' : getListItemsRequest, | |
'cookies' : authCookies, | |
'verify' : verifySSL, | |
'proxies' : proxies | |
} | |
response = requests.post(**requestOptions) | |
print response.content |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment