导语:这个周末我安装了Windows 10 Spring Update,并且使用新系统内置OpenSSH工具。系统内置了openssh,win管理员再也不需要使用Putty以及PPK格式的秘钥,于是我决定开始研究Windows如何“安全”地将私钥存储到这个新服务中。
简述:
这个周末我安装了Windows 10 Spring Update,并且使用新系统内置OpenSSH工具。
系统内置了openssh,win管理员再也不需要使用Putty以及PPK格式的秘钥,我开始研究系统都新添加了什么新的功能,研究发包含ssh-agent.exe,
我在MSDN文章中找到关于Windows ssh-agent新的资料,并且这部分立即引起了我的注意:
过去我曾多次劫持SSH,所以我决定开始研究Windows如何“安全”地将私钥存储到这个新服务中。
我将在这篇文章中概述我的方法和步骤以找出答案。这是一个有趣的研究过程,我使用Powershell来完成。
OpenSSH
私钥使用DPAPI进行保护并存储在HKCU注册表配置单元中。我在这里发布了一些PoC代码来从注册表中提取和重建RSA私钥
我测试的第一件事是通常使用OpenSSH实用程序生成几个密钥对并将它们添加到ssh-agent中。
首先,我使用以下命令生成一些密码保护的测试密钥对ssh-keygen.exe。
然后确保ssh-agent服务正在运行,使用ssh-add将私钥对加入正在运行的agent中。
运行会ssh-add.exe -L显示当前由SSH代管理的密钥。
最后,在将公钥添加到Ubuntu机器后,我证实我可以从Windows 10进行SSH连接,而不需要解密我的私钥(因为ssh-agent在后台为我处理):
监控SSH Agent
为了弄清楚SSH Agent如何存储和读取我的私钥,我探索了一下,并开始静态检查ssh-agent.exe。然而,我的静态分析技术比较弱,所以我放弃了,决定动态地跟踪这个过程,看看它在做什么。
我使用了Sysinternals中的procmon.exe,添加了过滤规则,过滤出进程名中包含“ssh”字符串的那些进程
随着procmon捕获事件,我再次使用SSH连接我的Ubuntu机器。仔细查看所有事件,我看到了ssh.exe打开与Ubuntu的TCP连接,然后终于看到ssh-agent.exe行动起来,并从注册表中读取一些值:
我注意到了两件事:
1、ssh-agent.exe进程会读取注册表中的HKCUSoftwareOpenSSHAgentKeys;
2、读取这些注册表键值后,该进程会立刻打开dpapi.dll。
根据这些信息,我知道系统将受保护的某些数据存储到注册表中并进行读取,并且ssh-agent使用的是微软的Data Protection API.aspx)
测试注册表值
果然,在注册表中,我可以看到两个我添加的密钥ssh-add。密钥名称是公钥的指纹,并且存在一些二进制数据块:
我花了一个小时查看StackOverflow上的相关资料,终于成功通过PowerShell导出这些注册表值并进行修改。其中comment字段为经过ASCII编码的文本,对应我之前添加的秘钥名:
而(default)值为一个字节数组,解码后看不到任何有直观意义的信息。我预感这些数据为“经过加密的”私钥,我可以尝试读取并解密这段数据。我将这些数据赋值到一个PowerShell变量中:
解密私钥
我对DPAPI并不是特别熟悉,但我知道有些后渗透利用工具会滥用这一功能来获取敏感数据及凭据数据,因此有一些人已经实现了一些封装包。Google一番后,我找到了atifaziz提供了一条线索,结果比我想象的还要简单(从中我也知道为什么人们会喜欢使用PowerShell)。
我不知道这种方法能否可行,但还是尝试使用DPAPI来解密这个字节数组。我希望能得到一个完美的私钥数据,所以使用base64对结果进行编码:
Add-Type -AssemblyName System.Security $unprotectedbytes = [Security.Cryptography.ProtectedData]::Unprotect($keybytes, $null, 'CurrentUser') [System.Convert]::ToBase64String($unprotectedbytes)
从Base64的结果看起来并非私钥,但我还是解开了这段数据,令人惊喜的是其中竟然包含一个“ssh-rsa”字符串!看起来我选择的方向没有问题。
找出二进制格式
这部分实际上我花了很长的时间。我知道某个键存储二进制数据,但我无法弄清楚二进制格式或如何使用它。
我使用openssl、puttygen以及ssh-keygen生成了各种RSA秘钥,但生成的结果与我现有的二进制数据并不相似。
最后还是需要借助Google的力量,我发现NetSPI曾发表过一篇很棒的文章,介绍如何在Linux上从内存中导出ssh-agent的OpenSSH私钥。
这会不会与我现有的二进制格式相同?我下载了那篇文章中提供的Python脚本,然后输入我从Windows注册表中提取的未经保护的base64数据:
的确成功了!我并不清楚原作者soleblaze如何找到二进制数据的正确格式,但还是向他表示诚挚的感谢,感谢他提供的Python工具以及发表的文章。
技术汇总
经过这些步骤,我们可以从注册表中提取出私钥,因此我将这些步骤汇总成两个脚本,大家可以参考我的Github。
第一个脚本为PowerShell脚本(extract_ssh_keys.ps1),该脚本可以查询注册表中ssh-agent保存的所有秘钥,然后使用当前用户上下文环境来调用DPAPI,解密二进制数据并保存成Base64数据。由于我不知道如何使用PowerShell来处理二进制数据,因此我将所有的秘钥都保存成JSON文件,然后导入Python脚本中。整个PowerShell脚本只包含如下几行:
$path = "HKCU:\Software\OpenSSH\Agent\Keys\" $regkeys = Get-ChildItem $path | Get-ItemProperty if ($regkeys.Length -eq 0) { Write-Host "No keys in registry" exit } $keys = @() Add-Type -AssemblyName System.Security; $regkeys | ForEach-Object { $key = @{} $comment = [System.Text.Encoding]::ASCII.GetString($_.comment) Write-Host "Pulling key: " $comment $encdata = $_.'(default)' $decdata = [Security.Cryptography.ProtectedData]::Unprotect($encdata, $null, 'CurrentUser') $b64key = [System.Convert]::ToBase64String($decdata) $key[$comment] = $b64key $keys += $key } ConvertTo-Json -InputObject $keys | Out-File -FilePath './extracted_keyblobs.json' -Encoding ascii Write-Host "extracted_keyblobs.json written. Use Python script to reconstruct private keys: python extractPrivateKeys.py extracted_keyblobs.json"
我大量借用了soleblaze提供的parse_mem_python.py代码,并且更新了Python3以用于下一个脚本:extractPrivateKeys.py。提供从Powershell脚本生成的JSON将输出找到的所有RSA私钥:
这些RSA私钥都采用明文格式。即使我在创建私钥的过程中添加了密码保护,ssh-agent还是没有将其加密存储。
为了验证私钥的有效性,我将秘钥拷贝回Kali系统中,验证了秘钥的指纹信息,并且可以使用该秘钥来登录SSH。
后续工作
我的PowerShell开发能力比较弱,因此公布的代码仍属于PoC范畴。其实完全可以使用PowerShell来完整重构秘钥,我也没有特别推崇使用Python,这次使用是因为soleblaze原始代码采用的就是Python代码,因此编写起来更加方便。
随着管理员在Windows 10中逐步开始使用OpenSSH,我希望这种技术能加入后续利用框架中,这些私钥的价值很高,对渗透测试人员来说非常有用。