Skip to content

Instantly share code, notes, and snippets.

@OnceUponALoop
Created July 20, 2023 06:49
Show Gist options
  • Save OnceUponALoop/b60b4457c851fee1e22e652d208ac25e to your computer and use it in GitHub Desktop.
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.
#!/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