Skip to content

Instantly share code, notes, and snippets.

@djhohnstein
Last active October 22, 2022 23:23
Show Gist options
  • Save djhohnstein/9db9bd477197df9fd02e90643ec058a2 to your computer and use it in GitHub Desktop.
Save djhohnstein/9db9bd477197df9fd02e90643ec058a2 to your computer and use it in GitHub Desktop.
Decrypt jenkins secrets with all key material from disk
#!/usr/bin/env python3
import re
import sys
import base64
from hashlib import sha256
from binascii import hexlify, unhexlify
from Crypto.Cipher import AES
from xml.dom import minidom
from pprint import pprint
import csv
MAGIC = b"::::MAGIC::::"
def usage():
print("./JenkinsDecrypter.py <master.key> <hudson.util.Secret> <credentials.xml>")
sys.exit(0)
def decryptNewPassword(secret, p):
p = p[1:] #Strip the version
# Get the length of the IV, almost certainly 16 bytes, but calculating for completeness sake
iv_length = ((p[0] & 0xff) << 24) | ((p[1] & 0xff) << 16) | ((p[2] & 0xff) << 8) | (p[3] & 0xff)
# Strip the iv length
p = p[4:]
# Get the data length
data_length = ((p[0] & 0xff) << 24) | ((p[1] & 0xff) << 16) | ((p[2] & 0xff) << 8) | (p[3] & 0xff)
# Strip the data length
p = p[4:]
iv = p[:iv_length]
p = p[iv_length:]
o = AES.new(secret, AES.MODE_CBC, iv)
decrypted_p = o.decrypt(p)
# We may need to strip PKCS7 padding
fully_decrypted_blocks = decrypted_p[:-16]
possibly_padded_block = decrypted_p[-16:]
padding_length = possibly_padded_block[-1]
if padding_length <= 16: # Less than size of one block, so we have padding
possibly_padded_block = possibly_padded_block[:-padding_length]
pw = fully_decrypted_blocks + possibly_padded_block
pw = pw.decode('utf-8')
return pw
def decryptOldPassword(secret, p):
# Copying the old code, I have not verified if it works
o = AES.new(secret, AES.MODE_ECB)
x = o.decrypt(p)
assert MAGIC in x
return re.findall('(.*)' + MAGIC, x)[0]
def main():
if len(sys.argv) != 4:
usage()
master_key = open(sys.argv[1], 'rb').read()
hudson_secret_key = open(sys.argv[2], 'rb').read()
hashed_master_key = sha256(master_key).digest()[:16]
o = AES.new(hashed_master_key, AES.MODE_ECB)
secret = o.decrypt(hudson_secret_key)
secret = secret[:-16]
secret = secret[:16]
mydoc = minidom.parse(sys.argv[3])
arrayList = mydoc.getElementsByTagName("java.util.concurrent.CopyOnWriteArrayList")
pwItemRegex = re.compile(r'<p(?:assword|rivateKey)>\{?(.*?)\}?</p(?:assword|rivateKey)>')
credArray = ["credentials", "password", "privatekey"]
credentialResults = []
for node in arrayList:
for cNode in node.childNodes:
if len([x for x in credArray if x in cNode.nodeName]):
cred = {
"type": cNode.nodeName,
}
for gcNode in cNode.childNodes:
credSecret = ""
if gcNode.nodeName == "privateKeySource":
kv = gcNode.getElementsByTagName("privateKey")[0]
credSecret = kv.firstChild.nodeValue
elif gcNode.nodeName in ["password", "secret"]:
credSecret = gcNode.firstChild.nodeValue
if credSecret != "":
if credSecret[0] == "{" and credSecret[-1] == "}":
credSecret = credSecret[1:-1]
cred['credential'] = credSecret
else:
try:
cred[gcNode.nodeName] = gcNode.firstChild.nodeValue
except:
pass
if not cred.get("description"):
cred['description'] = ""
if not cred.get("username"):
cred["username"] = cred["id"]
credentialResults.append(cred)
# print(cNode.tagName)
# break
# You can find the password format at https://github.com/jenkinsci/jenkins/blob/master/core/src/main/java/hudson/util/Secret.java#L167-L216
for c in credentialResults:
p = base64.decodestring(bytes(c['credential'], 'utf-8'))
# Get payload version
payload_version = p[0]
if payload_version == 1:
c['credential'] = decryptNewPassword(secret, p)
else: # Assuming we don't have a V2 payload, seeing as current crypto isn't horrible that's a fair assumption
c['credential'] = decryptOldPassword(secret,p)
if len(credentialResults) > 0:
with open("credentials.csv", "w") as f:
fieldnames = credentialResults[0].keys()
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(credentialResults)
for c in credentialResults:
pprint(c, indent=4)
print("\nCredentials in credentials.csv")
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment