This is a write-up for the hardest of the "13. Web Exploitation: Games (CIS-WEBSRV01)" series of challenges in the context of the Central Infosec CTF.
The challenge consists of moving a dot through a maze, but where the time limit makes that impossible. So we need to exploit the game mechanics to make the dot move unexpectedly.
The first step in this challenge is actually finding it. As per the $MACHINE_IP/robots.txt file, the URL for the challenge is $MACHINE_IP/hack-the-maze-game-hard, and as we load it up in a browser, we see the following page:
Here the blue dot is the one that the user controls (either with the "WASD" keys or the arrow keys). You are supposed to go from that dot, through the maze and up to the green dot.
So let's check out what's going on on this page: Let us open the Developer view by pressing Ctrl+Shift+I and visit the Elements tab:
We can see here that the page can frequently use the $.ajax method to do "POST" requests to the /check_maze and the /update_maze endpoints.
Let us now check the Network tab:
Here we see that the previous endpoints are contacted. In red, the update_maze endpoint is called every second, and in green, the check_maze endpoint is called every time I press the movement keys.
Step 2: Source analysis
Let us check what is sent in these POST requests:
For the update_maze request:
This request isn't very interesting, as it doesn't communicate anything to the endpoint.
For the check_maze request:
This request is interesting though, because it communicates 5 variables: movingAllowed, currRectX, currRectY, newX & newY
Simply put, it sends it's current coordinates, and the future coordinates depending on where the user moved to.
By breaking down the source code we find the following:
```javascript
var e,t,n=document.getElementById("mazecanvas"),a=n.getContext("2d"),i=3,l=3,r=1300,c=706;
function o(e,t,n){f(i,l,15,15),i=e,l=t,a.beginPath(),a.rect(e,t,15,15),a.closePath(),a.fillStyle=n,a.fill()}
function d(t){var s,h,u;
switch((t=t||window.event).keyCode){case 38:case 87:t.preventDefault(),s=i,h=l-3;break;case 37:case 65:t.preventDefault(),s=i-3,h=l;break;case 40:case 83:t.preventDefault(),s=i,h=l+3;break;case 39:case 68:t.preventDefault(),s=i+3,h=l;break;default:return}
if(1===(u=function(e,t){
var n=a.getImageData(e,t,15,15).data,i=1;if(e>=0&&e<=r-15&&t>=0&&t<=c-15)
for(var l=0;l<900;l+=4){
if(128===n[l]&&127===n[l+1]&&127===n[l+2]){
i=0;break
}
if(0===n[l]&&255===n[l+1]&&0===n[l+2]){
i=2;break
}
}
else i=0;
return i
}(s,h)))
{
var g={movingAllowed:u,currRectX:i,currRectY:l,newX:s,newY:h};
$.ajax({url:"check_maze",type:"POST",dataType:"json",data:g,
success:function(t){!1===t.movingAllowed&&(clearInterval(e),f(0,0,n.width,n.height),a.font="40px Arial",a.fillStyle="blue",a.textAlign="center",a.textBaseline="middle",a.fillText(t.message,n.width/2,n.height/2),window.removeEventListener("keydown",d,!0)),o(t.newX,t.newY,"#0000FF"),i=t.newX,l=t.newY
}})
}
}
function f(e,t,n,i){a.beginPath(),a.rect(e,t,n,i),a.closePath(),a.fillStyle="black",a.fill()}
!function(e,t){
f(0,0,n.width,n.height);var i=new Image;i.onload=function(){
a.drawImage(i,0,0),o(e,t,"#0000FF"),a.beginPath(),a.arc(1288,122,7,0,2*Math.PI,!1),a.closePath(),a.fillStyle="#00FF00",a.fill()
},i.src="maze.png"
}(3,3),window.addEventListener("keydown",d,!0),t=190,
$.ajax({
url:"create_maze",method:"POST",
success:function(e){console.log("Maze Created")}
}),
e=setInterval(function(){
if(f(n.width/2-40,n.height/2+25-15,80,30),
$.ajax({url:"update_maze",method:"POST",success:function(e){t=e}}),t<=0)
return clearInterval(e),window.removeEventListener("keydown",d,!0),f(0,0,n.width,n.height),a.font="40px Arial",a.fillStyle="red",a.textAlign="center",a.textBaseline="middle",void a.fillText("Time's up!",n.width/2,n.height/2+25);a.font="20px Arial",a.fillStyle=t<=10&&t>5?"orangered":t<=5?"red":"green",a.textAlign="center",a.textBaseline="middle";var i=Math.floor(t/60),l=(t-60*i).toString();1===l.length&&(l="0"+l),a.fillText(i.toString()+":"+l,r/2,n.height/2+25),t--
},
1e3)
```
Sadly, we won't deobfuscate all of this. We will however identify some behavior!
When you press a key, the d(t) function is called, which grabs the key you typed and determines what direction that matches to, and the calls the check_maze endpoint with that generated data.
The f(e, t, n, i) function draws the background of the maze.
The anonymous !function(e,t) function draws the blue dot, the green dot and displays the image.
The event listener just hooks up d(t) with the actual keypress.
The final bit of this code is the e interval, which executes the anonymous function that calls the maze update event.
This is going to be complicated to mess with, so let's just focus on our various POST requests and try messing with those:
If we look at our previous requests as well as our reconnaissance, we can easily identify that the only interesting request will be the check_maze request, so long as the success function remains respected. We can then feasibly force whatever the game to accept all the values in the response body, so long as the new value is at a distance of 3 from the old value, either horizontally or vertically.
Well, our initial coordinates are (3,3) and we will want to move laterally to (6,3).
By opening up the console (Ctrl+Shift+I) and the Console tab, we can play around with $.ajax requests.
So, let's build the request!
```javascript
// Move the function out of the way.
function onSuccess(t) {
!1===t.movingAllowed && (
clearInterval(e),
f(0,0,n.width,n.height),
a.font="40px Arial",a.fillStyle="blue",a.textAlign="center",a.textBaseline="middle",
a.fillText(t.message,n.width/2,n.height/2),
window.removeEventListener("keydown",d,!0)
),
o(t.newX,t.newY,"#0000FF"),
i=t.newX,l=t.newY
}
// The request payload
var payload={
movingAllowed:1, // Never changes
currRectX:3, // Current position is (x=3, y=3)
currRectY:3,
newX:6, // We want to move to (x=6, y=3)
newY:3
};
// Executing the request
$.ajax({url:"check_maze",type:"POST",dataType:"json",data:payload,success:function(t){onSuccess(t)}})
```
And now let's try it out!
Haha! Success! Now instead of just 3 pixels, let us make the dot move through the maze.
Step 4: Automating
DISCLAIMER: The way I personally went about it can be called the "sledgehammer" method. It also probably wasn't the intended method. It is however the one I'll discuss here.
Well, we now have a way to move slightly to the right. Let's make it move slightly to the end of the maze!
So this process is iterative: We go to a set of coordinates, we take note of them, and then we move to the next. And then we script the individual 3-by-3 segments along the way!
It takes a bit, but let us come up with some automation!
```python
# currentX, currentY, newX, newY
coords = [(3,3,3,3)]
def addX(startX, startY, endX, co):
co = co[:] # Cloning the list to avoid overwriting the original list
for i in range(round((endX - startX) / 3)):
co += [(startX + i*3, startY, startX + (i+1) * 3, startY)]
return co
coords = addX(3, 3, 27, coords)
print(coords)
# Output:
# [(3, 3, 3, 3), (3, 3, 6, 3), (6, 3, 9, 3), (9, 3, 12, 3), (12, 3, 15, 3),
# (15, 3, 18, 3), (18, 3, 21, 3), (21, 3, 24, 3), (24, 3, 27, 3)]
```
Ok, now we have a small function, that moves us from one point to the next, but it only works in one direction (to the right).
So now the key is to implement the other directions (down: +Y, left: -X, up: -Y), which will be quite similar to our previous addX function. The main difference is in the loop because the bounds and operators are relevant to the axis they are affecting.
```python
def addY(startX, startY, endY, co):
co = co[:]
for i in range(round((endY - startY) / 3)):
co += [(startX, startY + i * 3, startX, startY + (i+1)*3)]
return co
def subX(startX, startY, endX, co):
co = co[:]
for i in range(round(abs(endX - startX) / 3)):
co += [(startX - i*3, startY, startX - (i+1) * 3, startY)]
return co
def subY(startX, startY, endY, co):
co = co[:]
for i in range(round(abs(endY - startY) / 3)):
co += [(startX, startY - i * 3, startX, startY - (i+1)*3)]
return co
```
But now that we have this, we will want to make it be more than just a coordinate calculation system. We can extend it our $.ajax requests, in such a way as to not kill our console output by exporting our requests to a text file (because it might make it complicated to deal with copying and pasting from the console).
To that end, the build_request function, which takes our current / new coordinates as parameters and generates the request as a string, and the export function, which takes the entire coordinate list and builds the requests iteratively.
You might have noticed the usage of the await keyword on line 13, and wondered why it is there. When I was experimenting with the requests, and getting the maze to process 200 at a time, sometimes a request would be sent after another, but received slightly before the previous one. This is how I found out that the backend verifies that the coordinates match up with the current coordinate system. The await keyword forces the frontend to wait for an answer and process it before moving on, limiting issues such as the one I just described.
We now have a way to build a path, and a way to export the requests for this path. So now what? Well we find each of the new coordinates and we input them.
I did this with some granularity, and saved "snapshots" of my work (basically a copy of my list each time I added something), so that I wouldn't have to start over again.
Also, since typing the function name was an annoyance each time, I shortened it with the following anonymous functions.
And then the hut for coordinates. It basically involved analysing the POST requests for the new coordinates whenever I hit a border and taking note of the move and the new coordinate. For the sake of sanity, here are all of the operations: