~$ Dissecting the Central-Infosec Game Exploitation challenge

Posted on Apr. 20th, 2021.

Tags:InfoSecCTFWrite-Up


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.

Summary


Step 1: Reconnaissance

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:

Challenge 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.

Completed maze

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:

Page script

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:

Page networking

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:

The update_maze request.

This request isn't very interesting, as it doesn't communicate anything to the endpoint.

For the check_maze request:

The update_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:

```javascript 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 }}) $.ajax({ url:"create_maze",method:"POST", success:function(e){console.log("Maze Created")} }), $.ajax({url:"update_maze",method:"POST",success:function(e){t=e}}) ```

Step 3: Prototyping & testing

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!

Experiment successful.

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.

```python def build_request(x,y,nx,ny): return '$.ajax({url:"check_maze",type:"POST",dataType:"json",' + \ 'data:{movingAllowed: "1",currRectX: "' + str(x) + '",currRectY: "' + str(y) + '",newX: "' + str(nx) + '",newY: "' + str(ny) + '"},' + \ "success:function(t){ onSuccess(t) }" + \ "})".replace("\n", "").replace("\t", "") def export(co): # Modify with "C:/Users/..." on Windows. with open("~/hack-the-maze.txt", "w") as f: f.write("""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}\n""") for d in co: f.write("await " + build_request(d[0], d[1], d[2], d[3]) + ";\n") f.close() print(build_request(3,3,6,3)) # Output: # $.ajax({url:"check_maze",type:"POST",dataType:"json", # data:{movingAllowed: "1",currRectX: "3",currRectY: "3",newX: "6",newY: "3"},success:function(t){ onSuccess(t) }}) ```

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.

```python _addx = lambda co, newx: addX(co[-1][2], co[-1][3], newx, co) _subx = lambda co, newx: subX(co[-1][2], co[-1][3], newx, co) _addy = lambda co, newy: addY(co[-1][2], co[-1][3], newy, co) _suby = lambda co, newy: subY(co[-1][2], co[-1][3], newy, co) ```

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:

```python c = [(3,3,3,3)] c1 = _addx(c, 27) c2 = _addy(c1, 27) c3 = _addx(c2, 48) c4 = _addy(c3, 72) c5 = _addx(c4, 72) c6 = _addy(c5, 93) c7 = _subx(c6, 24) c8 = _suby(c7, 48) c9 = _subx(c8, 3) c10 = _addy(c9, 135) c11 = _addx(c10, 159) c12 = _addy(c11, 291) c13 = _addx(c12, 336) c14 = _addy(c13, 315) c14 = _addy(c13, 315) c15 = _addx(c14, 402) c16 = _suby(c15, 288) c17 = _addx(c16, 489) c18 = _suby(c17, 180) c19 = _addx(c18, 555) c20 = _addy(c19, 201) c21 = _addx(c20, 621) c22 = _suby(c21, 180) c23 = _addx(c22, 798) c24 = _addy(c23, 201) c24 = _addy(c23, 201) c25 = _addx(c24, 843) c26 = _suby(c25, 183) c27 = _addx(c26, 885) c28 = _addy(c27, 201) c29 = _addx(c28, 951) c30 = _suby(c29, 183) c31 = _addx(c30, 1017) c32 = _addy(c31, 315) c33 = _addx(c32, 1173) c34 = _addy(c33, 465) c35 = _subx(c34, 1125) c36 = _suby(c35, 444) c37 = _subx(c36, 1059) c38 = _addy(c37, 555) c39 = _subx(c38, 972) c40 = _addy(c39, 576) c41 = _subx(c40, 906) c42 = _suby(c41, 552) c43 = _subx(c42, 663) c44 = _suby(c43, 444) c45 = _subx(c44, 597) c46 = _addy(c45, 465) c47 = _subx(c46, 354) c48 = _suby(c47, 447) c48 = _suby(c47, 447) c49 = _subx(c48, 267) c50 = _addy(c49, 576) c51 = _subx(c50, 165) c52 = _suby(c51, 399) c53 = _subx(c52, 3) c54 = _addy(c53, 513) c55 = _addx(c54, 48) c56 = _addy(c55, 534) c57 = _addx(c56, 72) c58 = _addy(c57, 555) c59 = _subx(c58, 3) c60 = _addy(c59, 579) c61 = _addx(c60, 72) c62 = _addy(c61, 600) c63 = _subx(c62, 3) c64 = _addy(c63, 687) c65 = _addx(c64, 27) c66 = _suby(c65, 621) c67 = _addx(c66, 51) c68 = _addy(c67, 687) c69 = _addx(c68, 423) c70 = _suby(c69, 663) c71 = _addx(c70, 444) c72 = _addy(c71, 687) c73 = _addx(c72, 468) c74 = _suby(c73, 618) c75 = _addx(c74, 489) c76 = _addy(c75, 642) c77 = _addx(c76, 513) c78 = _suby(c77, 618) c79 = _addx(c78, 534) c80 = _addy(c79, 687) c81 = _addx(c80, 711) c82 = _suby(c81, 645) c83 = _addx(c82, 732) c84 = _suby(c83, 621) c85 = _addx(c84, 753) c86 = _addy(c85, 687) c87 = _addx(c86, 774) c88 = _suby(c87, 621) c89 = _addx(c88, 795) c90 = _addy(c89, 687) c91 = _addx(c90, 1107) c92 = _suby(c91, 666) c93 = _addx(c92, 1260) c94 = _suby(c93, 642) c95 = _subx(c94, 1236) c96 = _suby(c95, 618) c97 = _addx(c96, 1257) c98 = _suby(c97, 510) c99 = _subx(c98, 1212) c100 = _suby(c99, 489) c101 = _addx(c100, 1239) c102 = _suby(c101, 465) c103 = _subx(c102, 1212) c104 = _suby(c103, 444) c105 = _addx(c104, 1236) c106 = _suby(c105, 420) c107 = _addx(c106, 1257) c108 = _suby(c107, 378) c109 = _subx(c108, 1236) c110 = _suby(c109, 354) c111 = _addx(c110, 1260) c112 = _suby(c111, 225) c113 = _subx(c112, 1212) c114 = _suby(c113, 69) c115 = _subx(c114, 1191) c116 = _suby(c115, 3) c117 = _addx(c116, 1281) c118 = _addy(c117, 51) c119 = _subx(c118, 1257) c120 = _suby(c119, 27) c121 = _subx(c120, 1236) c122 = _addy(c121, 117) c123 = _addx(c122, 1257) c124 = _suby(c123, 69) c125 = _addx(c124, 1278) c126 = _addy(c125, 117) export(c126) ```


Let's go get the flag!

We have the flag!

I guess you can see why it is called the "sledgehammer" method now. Anyhow, the flag is Central-InfoSec{@RN7_[email protected][email protected]_FUN_700}.

Hope you had fun reading this!