-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA256 NEFARIOUSPLAN-CANONICAL-V1 {"body_md":"## The cookie reaches a deserializer with no type filter\n\nDotNetNuke tracks anonymous personalization in a cookie called `DNNPersonalization`. The cookie body is XML. For an unauthenticated request, `PersonalizationController.LoadProfile` reads the cookie value and hands it to `XmlUtils.DeSerializeHashtable`, which is the function that turns the string into objects.\n\nThat function, today, in `DNN Platform/Library/Common/Utilities/XmlUtils.cs`:\n\n```csharp\npublic static Hashtable DeSerializeHashtable(string xmlSource, string rootname)\n{\n var hashTable = new Hashtable();\n\n if (!string.IsNullOrEmpty(xmlSource))\n {\n try\n {\n var xmlDoc = new XmlDocument { XmlResolver = null };\n using (var xmlReader = XmlReader.Create(new StringReader(xmlSource), new XmlReaderSettings { XmlResolver = null, }))\n {\n xmlDoc.Load(xmlReader);\n }\n\n foreach (XmlElement xmlItem in xmlDoc.SelectNodes(rootname + \"/item\"))\n {\n string key = xmlItem.GetAttribute(\"key\");\n string typeName = xmlItem.GetAttribute(\"type\");\n\n // Create the XmlSerializer\n var serializer = new XmlSerializer(Type.GetType(typeName));\n\n // A reader is needed to read the XML document.\n var reader = new XmlTextReader(new StringReader(xmlItem.InnerXml))\n {\n XmlResolver = null,\n DtdProcessing = DtdProcessing.Prohibit,\n };\n\n hashTable.Add(key, serializer.Deserialize(reader));\n }\n }\n catch (Exception)\n {\n ////Logger.Error(ex); /*Ignore Log because if failed on profile this will log on every request.*/\n }\n }\n\n return hashTable;\n}\n```\n\n`typeName` is a string the function reads from the `type` attribute on each `` element in the XML document. It passes that string to `Type.GetType()`, which constructs a reference to whatever .NET type the caller named, including types from the GAC the application would never reach intentionally. The constructed type becomes the schema for an `XmlSerializer`. The element's children become the values fed into that schema. Whatever object graph the caller wrote down, .NET builds.\n\nThe `catch (Exception) { }` at the bottom swallows any failure silently. The committed-out `Logger.Error` call carries an explanatory comment: do not log because a logging call here would flood the log on every request. The function is reached by every anonymous request that carries a personalization cookie.\n\nThere is no caller-allowlist. There is no type-allowlist. There is no schema. There is no cap on how many items the loop processes. The contract of the function, read off its own signature, is: deserialize whatever this string says it is.\n\n## The Nuclei payload is the entire .NET deserialization gadget chain\n\nThe Nuclei template ships exactly one HTTP request. Strip the headers and the cookie body is this:\n\n```xml\n\n \n \n \n \n WriteFile\n \n C:\\Windows\\win.ini\n \n \n \n \n \n\n```\n\nThe `type` attribute names `ExpandedWrapper`. `ExpandedWrapper` is an Astoria / WCF helper that holds a primary entity plus a projected property. `ObjectDataProvider` is a WPF data-binding helper that calls a method on an object with given parameters and exposes the result. The XmlSerializer reconstructs the wrapper. As it materializes the projected property, it sets `ObjectInstance = new FileSystemUtils()`, `MethodName = \"WriteFile\"`, `MethodParameters = [\"C:\\Windows\\win.ini\"]`. The `ObjectDataProvider` setter for `MethodName` invokes the named method on the named instance with the named parameters, eagerly, during deserialization. This is the standard .NET xaml-style gadget chain documented in every payload generator from ysoserial.net forward.\n\n`FileSystemUtils.WriteFile` is a DotNetNuke helper. The version that shipped at the time of CVE-2017-9822, in `DNN Platform/Library/Common/Utilities/FileSystemUtils.cs`:\n\n```csharp\npublic static void WriteFile(string strFileName)\n{\n HttpResponse objResponse = HttpContext.Current.Response;\n Stream objStream = null;\n try\n {\n objStream = new FileStream(strFileName, FileMode.Open, FileAccess.Read, FileShare.Read);\n WriteStream(objResponse, objStream);\n }\n catch (Exception ex)\n {\n Logger.Error(ex);\n objResponse.Write(\"Error : \" + ex.Message);\n }\n // ...\n}\n```\n\nOpen the named file with the IIS process's permissions, write its bytes into the HTTP response. The Nuclei template requests `C:\\Windows\\win.ini` and matches `[extensions]` and `for 16-bit app support` in the response body, which is how it confirms the instance is vulnerable. The matcher checks for status 404 because the request is to `/__`, a path that does not resolve to a page; the cookie deserialization fires inside the IIS pipeline before the routing layer returns the 404. The deserializer ran. The file read happened. The 404 is the response code on the page that did not exist.\n\n`WriteFile` is a convenient sink. The `ObjectDataProvider` gadget can name any public method on any public type the runtime can resolve. `System.Diagnostics.Process.Start(ProcessStartInfo)` is the canonical RCE escalation, documented for every .NET deserialization payload generator since 2017. The Nuclei template is a probe. The primitive is RCE.\n\n## The patch is fifteen lines, none of them in the deserializer\n\nThe fix commit, DNN-9879, dated June 2, 2017, in full:\n\n```diff\n@@ -65,7 +66,7 @@ public PersonalizationInfo LoadProfile(int userId, int portalId)\n HttpContext context = HttpContext.Current;\n if (context != null && context.Request.Cookies[\"DNNPersonalization\"] != null)\n {\n- profileData = context.Request.Cookies[\"DNNPersonalization\"].Value;\n+ profileData = DecryptData(context.Request.Cookies[\"DNNPersonalization\"].Value);\n }\n }\n\n@@ -137,7 +138,7 @@ public void SaveProfile(PersonalizationInfo personalization, int userId, int por\n var context = HttpContext.Current;\n if (context != null)\n {\n- var personalizationCookie = new HttpCookie(\"DNNPersonalization\", profileData)\n+ var personalizationCookie = new HttpCookie(\"DNNPersonalization\", EncryptData(profileData))\n {\n Expires = DateTime.Now.AddDays(30),\n Path = (!string.IsNullOrEmpty(Globals.ApplicationPath) ? Globals.ApplicationPath : \"/\")\n@@ -147,5 +148,15 @@ public void SaveProfile(PersonalizationInfo personalization, int userId, int por\n+\n+ private string EncryptData(string profileData)\n+ {\n+ return new PortalSecurity().Encrypt(Config.GetDecryptionkey(), profileData);\n+ }\n+\n+ private string DecryptData(string profileData)\n+ {\n+ return new PortalSecurity().Decrypt(Config.GetDecryptionkey(), profileData);\n+ }\n```\n\nTwo call sites changed: the read of `DNNPersonalization` is now wrapped in `DecryptData`, the write in `EncryptData`. Two helper methods added that delegate to `PortalSecurity.Encrypt` and `PortalSecurity.Decrypt`, both keyed by `Config.GetDecryptionkey()`, the machine key DotNetNuke reads from `web.config`. The cookie an unauthenticated client sends is no longer the cookie the deserializer sees: it is the result of decrypting that cookie with a server-side key, and an attacker who does not hold the key cannot construct ciphertext that decrypts to anything the XML parser will accept, let alone a `` document with attacker-controlled `` elements.\n\nThe diff is fifteen lines. None of them are in `XmlUtils.cs`. None of them are in `DeSerializeHashtable`. None of them filter the type the deserializer is allowed to instantiate. The patch does not say `XmlSerializer` is dangerous when the type is caller-controlled. The patch's claim, read off its diff, is that the attacker cannot reach the deserializer with attacker-controlled bytes through this one cookie. That is true of this one cookie. The function the cookie used to reach is unchanged.\n\n## The deserializer is still public, with a deprecated wrapper still callable\n\n`XmlUtils.DeSerializeHashtable` is `public static`. Two call sites in DotNetNuke's own code as of head, both inside `DotNetNuke.dll`. The first is the patched `PersonalizationController.LoadProfile`, gated by decryption. The second is in `DNN Platform/Library/Common/Globals.cs`:\n\n```csharp\n/// DeserializeHashTableXml deserializes a Hashtable using Xml Serialization.\n/// \n/// This is the preferred method of serialization under Medium Trust.\n/// \n[DnnDeprecated(9, 8, 1, \"This API was not meant to be public and only deserializes xml with a root of 'profile'\")]\npublic static partial Hashtable DeserializeHashTableXml(string source)\n{\n return XmlUtils.DeSerializeHashtable(source, \"profile\");\n}\n```\n\nThe deprecation note is a confession. \"This API was not meant to be public\" describes the wrapper that went out the door in a public DLL, was loadable from any module written against `DotNetNuke.dll` for years, and is still loadable today. The `[DnnDeprecated]` attribute emits a compile-time warning. The function still runs. The function still hands whatever string the caller passes into `XmlUtils.DeSerializeHashtable(source, \"profile\")`. Any DotNetNuke module written against this API in the last decade carries CVE-2017-9822 inside its own input surface; the attacker just needs whatever input the module exposes to reach `Globals.DeserializeHashTableXml()`, and the module is back to running attacker-named types.\n\n`XmlUtils.DeSerializeHashtable` itself remains `public static`, callable from any assembly loaded into the application domain. There is no class-allowlist on what `Type.GetType` is asked to construct. There is no type filter on what `XmlSerializer` is asked to deserialize. The `catch (Exception) { }` still hides failures, by explicit comment, to keep the log readable for a function that runs on every anonymous request. The remediation note in the deprecation attribute does not say \"do not call this function with caller-supplied input.\" It says the API was not supposed to be public.\n\nThe patch is not a fix to the bug class. The patch is a key gate on one consumer of a primitive that remains public, remains callable, and still carries the contract that named the bug.\n\n## The KEV listing is not telling you the patch level is wrong\n\nCISA's Known Exploited Vulnerabilities catalog carries CVE-2017-9822. EPSS percentile sits at 99.94, the top hundredth of CVEs by predicted exploitation, and the percentile has held there since the score was introduced. The Nuclei template body for it has been updated as recently as this year, the rule's metadata block carrying the `vkev` and `kev` tags. None of those signals is telling you the Nuclei template is good. They are telling you that internet-facing DotNetNuke instances on versions older than 9.3.1 still exist in numbers high enough that mass scanning for the cookie payload reliably finds them. The patch shipped in 2017. The exploitation curve has not converged.\n\nThe shape of the substrate explains why the curve has not converged. `XmlUtils.DeSerializeHashtable` is a function whose contract, read off its signature, is to instantiate a caller-named type and deserialize caller-supplied XML into it. That contract is the function. No version of the function is safe with caller-controlled input, because there is no version of `XmlSerializer.Deserialize(Type.GetType())` that is safe with caller-controlled input. This is what the unpatchable primitive pattern names: a bug class whose source is the design contract of the component, where each patch closes a specific reachability path and the next path arrives with the next caller. The function is the primitive. The patches are gates around it.\n\nThe CVE description for CVE-2017-9822 reads \"DNNPersonalization cookie.\" It does not name the function the cookie reached. The Nuclei template description reads \"cookie deserialization remote code execution.\" It does not name the function the cookie reached. Patching to 9.3.1 closes the cookie. The function the cookie reached is still in the `DotNetNuke.dll` shipped with version 9.13.x.\n\nPoC: [projectdiscovery/nuclei-templates CVE-2017-9822](https://github.com/projectdiscovery/nuclei-templates/blob/main/http/cves/2017/CVE-2017-9822.yaml)","closing_line":"Fifteen lines closed CVE-2017-9822. None of them are in the function CVE-2017-9822 reached.","hook_md":"CVE-2017-9822 was assigned in 2017. The fix shipped in fifteen lines touching one method. CISA later added the entry to the KEV catalog. EPSS percentile sits at 99.94, the top hundredth of CVEs by predicted exploitation, and that percentile has not moved.\n\nThe patch did not touch `XmlUtils.DeSerializeHashtable`. That function still exists in the DotNetNuke source today, still calls `Type.GetType()` on an attacker-supplied string, still feeds the resulting type into `XmlSerializer.Deserialize()` against attacker-supplied XML. The patch encrypted the one input that reached it. The function did not change.","post_id":39,"slug":"dotnetnuke-cve-2017-9822-deserializer-still-public","title":"CVE-2017-9822: The Patch Encrypted the Cookie. The Deserializer Is Still Public.","type":"initial","unreadable_sentence":"Fifteen lines closed CVE-2017-9822. None of them are in the function CVE-2017-9822 reached."} -----BEGIN PGP SIGNATURE----- iHUEARYIAB0WIQRf0htP5+SjynlxywneZjl4jgkQJgUCaft1cAAKCRDeZjl4jgkQ JprTAP4tP3MKq2Tiyx2th4oRyuglqVW/oCd2ef2CjOShkFo73gEA+hz52FrIFKNN RaDm45Lm1o8fVIN5KkfmSg0wmpBYXgo= =Hqju -----END PGP SIGNATURE-----