If we use GitLab or GitHub, our ssh pubkeys along with our name will be exposed to the public. This can be curious, but the most interesting thing is that through the ssh pubkey we can know if the associated private key has access to a certain server or not. In this way, we can discover the server infrastructure used by a company by having only the pubkey of some sysadmin/programmer. The article I based this on is published on Artem Golubin’s website.
We can get a user’s pubkey by running a Curl against the GitLab server:
ssh-rsa XXXXXXXXXXXXXXXXXXXXXXXXXXXXX NAME (flatland.alfaexploit.com)
NOTE: This system is equally valid for GitLab and GitHub.
We install the necessary Python libraries:
We write the script:
#!/usr/bin/env python3
import logging
import socket
import sys
import paramiko.auth_handler
import requests
import argparse
def valid(self, msg):
self.auth_event.set()
self.authenticated = True
print("Valid key")
def parse_service_accept(self, m):
# https://tools.ietf.org/html/rfc4252#section-7
service = m.get_text()
if not (service == "ssh-userauth" and self.auth_method == "publickey"):
return self._parse_service_accept(m)
m = paramiko.message.Message()
m.add_byte(paramiko.common.cMSG_USERAUTH_REQUEST)
m.add_string(self.username)
m.add_string("ssh-connection")
m.add_string(self.auth_method)
m.add_boolean(False)
m.add_string(self.private_key.public_blob.key_type)
m.add_string(self.private_key.public_blob.key_blob)
self.transport._send_message(m)
def patch_paramiko():
table = paramiko.auth_handler.AuthHandler._client_handler_table
# In order to avoid using a private key, two callbacks must be patched.
# The MSG_USERAUTH_INFO_REQUEST (SSH_MSG_USERAUTH_PK_OK 60) indicates a valid public key.
table[paramiko.common.MSG_USERAUTH_INFO_REQUEST] = valid
# The MSG_SERVICE_ACCEPT event triggers when server sends a request for auth.
# By default, paramiko signs it with the private key. We don't want that.
table[paramiko.common.MSG_SERVICE_ACCEPT] = parse_service_accept
def probe_host(hostname_or_ip, port, username, public_key):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((hostname_or_ip, port))
transport = paramiko.transport.Transport(sock)
transport.start_client()
# For compatibility with paramiko, we need to generate a random private key and replace
# the public key with our data.
key = paramiko.RSAKey.generate(2048)
key.public_blob = paramiko.pkey.PublicBlob.from_string(public_key)
try:
transport.auth_publickey(username, key)
except paramiko.ssh_exception.AuthenticationException:
print("Bad key")
def get_public_key(username):
r = requests.get('https://flatland.alfaexploit.com/%s.keys' % username)
return r.content.decode('utf-8')
def main():
parser = argparse.ArgumentParser()
parser.add_argument('host', type=str, help='Hostname or IP address')
parser.add_argument('--gitlab-username', type=str, default=None)
parser.add_argument('--ssh-username', type=str, default="root")
parser.add_argument('--loglevel', default='INFO')
parser.add_argument('--port', type=int, default=22)
parser.add_argument('--public-key', type=str, default=None)
args = parser.parse_args(sys.argv[1:])
logging.basicConfig(level=args.loglevel)
if args.gitlab_username:
key = get_public_key(args.gitlab_username)
elif args.public_key:
key = open(args.public_key, 'rt').read()
else:
raise ValueError("Public key is missing. Please use --gitlab-username or --public-key")
patch_paramiko()
probe_host(
hostname_or_ip=args.host,
port=args.port,
username=args.ssh_username,
public_key=key
)
if __name__ == '__main__':
main()
We assign the necessary permissions:
We run the script:
If the private key does not have access, it will present the following message:
INFO:paramiko.transport:Connected (version 2.0, client OpenSSH_8.2p1)
INFO:paramiko.transport:Authentication (publickey) failed.
Bad key
On the other hand, if you have access to the following:
INFO:paramiko.transport:Connected (version 2.0, client OpenSSH_5.9p1-hpn13v11)
Valid key