- vừa được xem lúc

corCTF 2022 Writeup (Part 1)

0 0 51

Người đăng: fairytypean

Theo Viblo Asia

corCTF 2022 is truly like a JS marathon 😄 by the fact there are so many interesting (and panic) web challenges writen in JS. I was not able to solve any of them during the CTF event. After 2.5 days, I finally solve the simplewaf challenge, and that is the only challenge that I can solve on my own 😂.

Anyway, I learn so much about JS from this CTF, so I would like to write this post to share my journey of going through 3 web challenges written in JS: simplewaf, friends, and modernblog. Based on the challenge level, I write up the frist 2 challenges first, then the last challenge will move to the next part. Without any further word, let's dive in now!

1. simplewaf

a. Challenge description

The challenge provide us source code and a Dockerfile. Since the Instancer only create an instance of the challenge that last for 3 minutes, which is so inconvenience, so I bulid and debug localy with the provided resource to play this challenge.

Navigate through the website at localhost:3456, we can see this is just a simple web show the content of specified file. The target of this challenge is reading the content of flag.txt file... but by someway to bypass the check includes('flag') of waf. All of the challenge code can be seen at main.js.

const express = require("express");
const fs = require("fs"); const app = express(); const PORT = process.env.PORT || 3456; app.use((req, res, next) => { if([req.body, req.headers, req.query].some( (item) => item && JSON.stringify(item).includes("flag") )) { return res.send("bad hacker!"); } next();
}); app.get("/", (req, res) => { try { res.setHeader("Content-Type", "text/html"); res.send(fs.readFileSync(req.query.file || "index.html").toString()); } catch(err) { console.log(err); res.status(500).send("Internal server error"); }
}); app.listen(PORT, () => console.log(`web/simplewaf listening on port ${PORT}`));

b. Challenge analysis

After reading the source code, there are 2 question come to my mind.

  1. How to bypass the includes condition? (absolutely... that what we are looking for), and
  2. What type of argument the readFileSync function can take to read a file?

By going around on Google, I found a write up for NodeJS Bypass Filter CTF, which is similar to this challenge at some point:

  • Both challenge doesn't validate the type of input, which means we can pass input as an array instead of a string, and
  • Both challenge require to bypass the includes function to reach the flag!

Bravo! Think that I found the right spot, I try the payload file[]=x&file[]=flag.txt. Unlucky, it can't bypass waf of this challenge

Why can't it bypass the waf? Well, I figure out there is a critial different point between the challenge at this line of code

(item) => item && JSON.stringify(item).includes("flag")

The simplewaf doesn't take the raw input to perform input validation, but transform the raw input a JSON string beforehand. So, the includes function still can check whether the transformed string contains flag or not.

At this point, I can't think of anyway to bypass the includes function... So, I move to the second question, and take a look at NodeJS document of that function.

Oke, so the path parameter can be either a <string> | <Buffer> | <URL> | <integer>. However, the type of requests query value is always string. How could we pass in the readFileSync function a URL, or an integer, or anything else other than a string?

At the first thought, I try format the string as a URL: http://localhost:3456/wow.html. Unlucky, it doesn't works~

Stopping fruitless effort at guessing, I decided to take a closer look of the readFileSync function at NodeJS source code on github.

The code snippet from line 469 and below perform read file process, which is nothing to dive in. The main point we need to dive in is the code at line 467. Follow the code by investigating the fs.openSync function.

Continue following by investigating the getValidatedPath function.

Hold down, some interesting things appear at here. So if the fileURLOrPath value is not null, and there are exists href and origin in it, it will call to fileURLToPath, which transform the fileURLOrPath value to a URL. That is the point! I can feel that I'm going on the right way!

Gaining momemtum, I continue investigating the fileURLToPath function.

One additional condition for the fileURLOrPath value is that its protocol must be file:. After all of the check is passed, it will call the corresponding function to get path from the URL. Since I'm debugging on Linux, so I continue investigating at the getPathFromURLPosix function.

Once again, another check occurs at this code snippet: the hostname must be empty. But, one remarkable things to note down here, and it will helps us bypass the includes check of simplewaf is that it will perform URL decode on the pathname to get the URL. This means if we pass a double URL encoded pathname value from the web application, it will ends up the file path being plaintext. And guess what? Since the value pass the client to the includes check is just URL decoded once, we can easily bypass this check also.

Okay, let's sum it up all the things we need to do to pass a valid argument file as a URL into the readFileSync function.

  • file is not null
  • file.origin exists
  • file.href exists
  • file.protocol = 'file:'
  • file.hostname = ''

And the final requirement to bypass the waf and get the flag is:

  • file.pathname is double URL encoded

c. Challenge solution

From the analysis above, I construct the following payload:

file[origin]=x&file[href]=x&file[protocol]=file:&file[hostname]=&file[pathname]=fla%2567.txt

I just double URL encode the g character to bypass the waf. Using the payload, we succesfully get the tesst flag.

Okay, let's get the real flag for now~

corctf{hmm_th4t_waf_w4snt_s0_s1mple}

2. friends

a. Challenge description

Another instance challenge, which again makes me so inconvenience to debug and investigate, so I create a docker images for this challenge and host it locally. I also upload the Dockerfile at my github repo.

The code is nearly 300 lines... There is a login function, follow and unfollow function, and our target is making admin follow us.

b. Challenge analysis

The source code does not contains any snippet of code that stored the credentials of admin user. Thought that the code could be hidden, or unprovided, I try to follow admin, but the application response "user does not exist". How strange it could be!

So... there is no admin account. Then just create an admin account, then follow other account. However, we can create an admin account, as the username must be at least 6 characters.

At this point, I got stuck. I found no entry point in code that we can input to edit followers data.

Magic JS

Oops! This is not true. After reading the write up from the others (on discord channel), I found that I misunderstood a snippet of code... Magic

 const body = req.body const params = ['u', 'r', 'n'] const mine = followers.get(req.username) // force all params to be strings for safety [body.u, body.r, body.n] = params.map( p => (body[p] ?? '').toString() )

Put it on prettier.io to see how this snippet of code actually look like. And that is:

 const body = req.body const params = ['u', 'r', 'n'] const mine = (followers.get(req.username)[ // force all params to be strings for safety (body.u, body.r, body.n) ] = params.map((p) => (body[p] ?? "").toString()));

It completely change everything. Beforehand, body.u, body.r and body.n seems to be the return value of map function, turns out to be index of a users' followers array. The comma is now become the command operator.

Comma Operator

Let's take a small demo to understand comma operator behavior in array index. Assume that arr = followers.get(req.username), the following code snippet will help us understand the behavior of comamnd operator.

Magic JS! There are 2 things to notice from the results we get:

  • We can assign the javascript array at any index. There is no line of code assign value for arr[0], but the js code still run without any error!
  • In case there is comma operator in array index, only the last index get the assigned value, and other get undefined

At this point, you might think that: "Alright, now I can just assign admin to the first index, then I can succesfully get the flag. My payload wiill be [body.u, body.r, body.n] = ["admin", "", 1]. Unfortunately, this payload will not work because the inserted value is an array ["admin", "", 1], not a string that we want 😄

We almost get to the flag. The remaining problem is that by somehow, there is an element in followers array is value type of string, not array.

__proto__

This magic will completely solve the remaining problems. Talking about __proto__ (and prototype pollution) is long, so I will not do it at this write up. Let's examine the following snippet of code to understand the behavior of array __proto__

There are 2 things to tonice from the results we get:

  • Although there is no line of code assign value for arr1[0], the output of arr1 show that arr1[0]="1". This happens similar to arr2.
  • The filled up missing value is the same as the value we assigned to arr1["__proto__"] or arr2["__proto__"], sequentially.

Gotcha! At this point, we can finally solve the challenge, as we finally fill up the missing value with a controlled value and type, no matter what data is inserted in later!. Let's check it out!

c. Challenge solution

To solve this challenge, I use 4 requets.

The first request just to login, and get the session

The second request is fill up the value of __proto__ of the followers array.

curl http://localhost:3000/follow -v --cookie "token=5cd5304ab52fd2c8a817eab91df84908" --data "u=admin&r=&n=__proto__"

The third request is to fill up the followers array at index 1, so that the index 0 of followers array will be automatically filled up by the value the value of __proto__ of that array.

curl http://localhost:3000/follow -v --cookie "token=5cd5304ab52fd2c8a817eab91df84908" --data "u=&r=&n=1"

And the final request is to get the flag!

The real flag: corctf{friendship_is_magic_colon_sparkles_colon}

That's all about the first part of this write up! Hope you enjoy and learn something from the challenge and the write up! 😄

Bình luận

Bài viết tương tự

- vừa được xem lúc

Tài nguyên nghiên cứu sâu Html

1. Articles and standards. . HTML 5.

0 0 183

- vừa được xem lúc

Embedded Template in Go

Getting Start. Part of developing a web application usually revolves around working with HTML as user interface.

0 0 40

- vừa được xem lúc

Full Stack Developer Roadmap 2021

Cách để trở thành một Full Stack Web Developer trên thế giới hiện nay. Các công ty đang luôn săn đón những developer có nhiều kĩ năng để cung cấp cho họ sự linh hoạt trong các dự án.

0 0 25

- vừa được xem lúc

Những kiến thức hay về Gradient: Gradient đẹp nhất chỉ được tìm thấy ở ngoài thiên nhiên!

. Quen thuộc từ lâu với rất nhiều người, nền Gradient chỉ là những bức nền với 2 hay nhiều dải màu sắc được hòa trộn với nhau. Đơn giản là vậy, nhưng càng ngày Gradient càng phổ biến hơn trong thiết kế Website ngày nay.

0 0 288

- vừa được xem lúc

What Is Session Fixation?

Session Fixation là một kỹ thuật tấn công web. Kẻ tấn công lừa người dùng sử dụng session ID đặc biệt.

0 0 33

- vừa được xem lúc

Làm thế nào để Design của Website thu hút hơn?

Xin chào các bạn. Bởi thế, không phải bàn cãi, thiết kế giao diện vừa thu hút, vừa chuyên nghiệp và ấn tượng là một trong những yếu tố quan trọng nhất trong cả quá trình phát triển 1 website.

0 0 23