Logo
Overview
This is how we hacked all Vending Machines of BITS PILANI (HYDERABAD VERSION)

This is how we hacked all Vending Machines of BITS PILANI (HYDERABAD VERSION)

June 3, 2026
22 min read

Research by GodSec (0x1622, 0x3024 & 0x4r53).

This all started when I went to the BITS Hyderabad campus for my summer term and saw a new vendor operating the vending machines. I then told Manav (0x4r53) and Prem (0x3024) about it, and we thought it would be banger if we could find some critical vulnerabilities in the machine.

||(spoiler) What started out as a pretty standard web application pentest ended up giving us interactive root shells on the physical Raspberry Pi hardware inside their machines across the country (300+ machines in total).||

image

I will start from the start

The first thing we did was map out all the subdomains associated with go-grab.in. We discovered the domain simply by visiting the company’s website. At that point, we had no idea that the vending machine was operated through this infrastructure, and we had almost no expectation of finding anything particularly interesting. In the beginning, all we had was the main domain: https://go-grab.in/.

Further using tools like subfinder and assetfinder we were able to identify a total of 7 subdomains:

https://taskdashboard.go-grab.in
https://www.go-grab.in
https://dashboard.go-grab.in
https://api.go-grab.in
https://go-grab.in
https://support.go-grab.in
https://webmail.go-grab.in

There was nothing very special on most of the subdomains except the first one, https://taskdashboard.go-grab.in. The rest of the subdomains were either dead or were not very interesting.

The hint for credentials on Taskdashboard

We started by mapping out their external footprint. During subdomain enumeration we found taskdashboard.go-grab.in; it was a Next.js app used by their internal operations team.

While going through the minified JavaScript bundle for the frontend, we noticed how they were handling role-based access control (RBAC):

// Admin check is client-side only:
let s = o === 'role_012' || o === 'role_010' // full admin
let c = o === 'role_007' // limited access

So basically role_012 and role_010 had full access.

Since the check was happening entirely on the client side, anyone could just edit their localStorage, set their role to role_012, which is super admin, and unlock the whole admin UI. The catch was that we still needed a valid token to actually pull data from the backend.

During this whole process, I was doing OSINT as well and found a few valid employee email addresses. We built a targeted wordlist and ran a password spray against the login endpoint. (NOTE: We were very careful of not DOSing and had wordlist of just 15 usernames/passwords)

...
const getAuthToken = async () => {
const token_api = "https://gg-nodejs.onrender.com/api/user-login/v2";
const user = {
id: "sudhanshujha2717@gmail.com",
password: "Sudhanshu"
};
const res = await fetch(token_api, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ user }),
});
const data = await res.json();
return data.data.token;
};

BOOM!

The craziest part comes here: it did not take long to get a hit on an employee account with a weak password, Sudhanshu1. If I remember correctly, it just took us 6-7 :) attempts.

Terminal window
SUCCESS! Password: Sudhanshu1
Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoic3VkaGFuc2h1amhhMjcxN0BnbWFpbC5jb20iLCJyb2xlSWQiOiJyb2xlXzAxMiIsImlhdCI6MTc4MDQwNjE0MiwiZXhwIjoxNzgxMDEwOTQyfQ.9WU4pXZjfYCVeZaokeeMnqtg74rCpZ1IAn05qwcNZDg
Data: {
"success": true,
"message": "Login successful",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoic3VkaGFuc2h1amhhMjcxN0BnbWFpbC5jb20iLCJyb2xlSWQiOiJyb2xlXzAxMiIsImlhdCI6MTc4MDQwNjE0MiwiZXhwIjoxNzgxMDEwOTQyfQ.9WU4pXZjfYCVeZaokeeMnqtg74rCpZ1IAn05qwcNZDg",
"user": "sudhanshujha2717@gmail.com"
}

Now we had access to their login dashboard, from which we could view all the important details.

image

Now that we had a valid JWT token, we started looking at the authenticated endpoints. By analyzing the JavaScript files again, we found a couple of very interesting endpoints that we wanted to test.

We hit /api/teams, expecting it to return the profile details for our specific team. Again the fun part: instead of a specific team, it dumped the entire employee database.

image

This was a massive authorization flaw. The response included 284 employee records. Worse, it included every single employee’s password in plaintext. It also leaked Aadhaar and PAN numbers, bank account details, Google Cloud Storage links to photos, and their physical ID cards.

Since we had the plaintext passwords for everyone, we just pulled out the 14 super admin role_012 accounts and effectively had control over the platform at that point.

Inside the dashboard

Logging in as a super admin gave us access to everything. We could view live fleet monitoring, task management, and the DSI dashboard, which showed raw sales data, profit margins, and inventory stats.

image

There was a huge amount of data available in this dashboard. For example, we could see the live order count, the total number of transactions from the past 30 days, total profits, and waste products.

At this stage, we analyzed the application’s authentication flow. The image below shows how authentication was handled within the platform. Additionally, we identified that the application’s backend was hosted at https://gg-nodejs.onrender.com, which served as the primary API endpoint for the entire system.

image

The login functionality used the /api/user-login/v2 endpoint to authenticate users.

At this point we had enough data to report, but we still thought it was not that critical of a bug. Unless and until we got something crazier, we were not going to stop.

This part was the most challenging. At that stage, we had no clear direction for further pivoting from the application. We knew about the API server at https://api.go-grab.in/, but without knowledge of the available endpoints, targeting it effectively was difficult.

We attempted to discover endpoints through fuzzing, but those efforts did not yield any useful results. Without additional information about the API structure, further progress on this attack path seemed unlikely.

Though we confirmed that all the endpoints that were working for https://gg-nodejs.onrender.com were also working for https://api.go-grab.in/.

We spent roughly 4-6 hours exploring different attack paths, but none of them led anywhere significant. Then, almost by chance, We discovered what turned out to be a goldmine: a publicly accessible GitHub repository.

// require('./utils/newRelicAPIMonitoring.js')
// import 'newrelic';
// let newrelic = null;
// try {
// newrelic = require('newrelic');
// } catch (e) {
// console.warn('New Relic not loaded:', e.message);
// }
const cluster = require('cluster');
const os = require('os');
const express = require('express');
// const ping = require('ping');
const bodyParser = require('body-parser');
const timeout = require('connect-timeout');
require('dotenv').config();
const cors = require('cors');
const path = require('path');
const logger = require('./middlewares/logger'); // Logging middleware
const errorHandler = require('./middlewares/errorHandler'); // Error handling middleware
const authenticate = require('./middlewares/authenticate'); // Authentication middleware (optional placement)
// const paymentRoutes = require('./routes/paymentRoutes');
// const loginVerifyRoutes = require('./routes/loginVerifyRoutes');
// const downtimeRoutes = require('./routes/downtimeRoutes.js');
// const userLoginRoutes = require('./routes/userLoginRoutes');
// const onBoardingRoutes = require('./routes/onBoardingRoutes.js');
// const dashboardLoginRoute = require('./routes/dashboardLoginRoute.js');
// const ImageUploadRoute = require('./imageUpload/ImageUploadRoute.js');
// const productRoutes = require('./routes/productRoutes');
// const categoryRoutes = require('./routes/categoryRoutes');
// const todoTasksRoutes = require('./routes/todoTasksRoutes.js');
// const todoTasksCommentsRoutes = require('./routes/todoTasksCommentsRoutes.js');
// const orderRoutes = require('./routes/orderRoutes');
// const ratingsRoutes = require('./routes/ratingsRoutes');
// const machineDetailsRoutes = require('./routes/machineDetailsRoutes');
// const rfidUsersRoutes = require('./routes/rfidUsersRoutes.js');
// const rfidTransactionRoutes = require('./routes/rfidTransactionRoutes.js');
// const rfidRechargeLogsRoutes = require('./routes/rfidRechargeLogsRoutes.js');
// const machinePaymentRoutes = require('./routes/machinePaymentRoutes');
// const transactionDetailsRoutes = require('./routes/transactionDetailsRoutes');
// const coilRecommendationRoutes = require('./routes/coilRecommendationRoutes.js');
// const coilRoutes = require('./routes/coilRoutes');
const teamRoutes = require('./routes/teamRoutes');
// const refillHistoryRoutes = require('./routes/refillHistoryRoutes');
// const warehouseRoutes = require('./routes/warehouseRoutes');
// const gstRegisteredAddressesRoutes = require('./routes/gstRegisteredAddressesRoutes.js');
// const stockFlavourTransferRoutes = require('./routes/stockFlavourTransferRoutes.js');
// const brandRoutes = require('./routes/brandRoutes');
// const lastIdRoutes = require('./routes/lastIdRoutes');
// const warehouseSlotRoutes = require('./routes/warehouseSlotRoutes');
// const stockExchangeRoutes = require('./routes/stockExchangeRoutes.js');
// const bagSlotRoutes = require('./routes/bagSlotRoutes');
// const vendItemRoutes = require('./routes/vendItemRoutes');
// const organizationRoutes = require('./routes/organizationRoutes');
// const scrapSaleRoutes = require('./routes/scrapSaleRoutes.js');
// const clusterRoutes = require('./routes/clusterRoutes');
// const distributorRoutes = require('./routes/distributorRoutes');
// const purchaseProductRoutes = require('./routes/purchaseProductRoutes');
// const transactionRoute = require('./routes/transactionRoutes');
const roleRoutes = require('./routes/roleRoutes');
// const inventoryOrderingRoutes = require('./routes/inventoryOrderingRoutes.js');
// const productRecommendationRoutes = require('./routes/productRecommendationRoutes.js');
// const pickLogRoute = require('./routes/pickLogRoute');
// const returnLogRoute = require('./routes/returnLogRoutes');
// const productMismatchRoutes = require('./routes/productMismatchRoutes');
// const stockUpdateLogRoutes = require('./routes/stockUpdateLogRoutes');
// const purchaseOrderRoutes = require('./routes/purchaseOrderRoutes.js');
// const draftPurchaseOrderRoutes = require('./routes/draftPurchaseOrderRoutes.js');
// const inventoryDiscrepancyRoutes = require('./routes/inventoryDiscrepancyReportRoutes.js');
// const auditQuestions = require('./routes/auditQuetionsRoutes.js');
// const purchaseOrderProductRoutes = require('./routes/purchaseOrderProductRoutes');
// const responseFromPaytmRoutes = require('./routes/responseFromPaytmRoutes');
const expoPushTokenRoutes = require('./routes/expoPushTokenRoutes.js');
const notificationsRoutes = require('./routes/notificationsRoutes.js');
const salaryRoutes = require('./routes/salaryRoutes.js');
const notificationRecipientsRoutes = require('./routes/notificationRecipientsRoutes.js');
// const ebillRoutes = require('./routes/ebillRoutes.js');
// const SpecialMachineRoutes = require('./specialMachineApis/routes/index');
const SpecialUserRoutes = require('./specialUserApis/routes/index');
const notificationRoute = require('./notification/notificationRoute.js');
const authorize = require('./middlewares/authorize');
const attendanceRoute = require('./routes/attendanceRoutes.js')
const odometerRoute = require('./routes/odometerRoutes.js')
const attendanceOverrideRoute = require('./routes/attendanceOverrideRoutes.js')
// const discountOfferRoutes = require('./routes/discountOfferRoutes.js')
const distanceOverrideRoutes = require('./routes/distanceOverrideRoutes.js')
const shiftRoute = require('./routes/shiftRoutes.js')
const breakRoute = require('./routes/breakRoutes.js')
const leaveRoute = require('./routes/leaveRoutes.js')
const expenseRoute = require('./routes/expenseRoutes.js')
const incentiveRoutes = require('./routes/incentiveRoutes');
const fineRoutes = require('./routes/fine.routes');
const leaveEncashedRoutes = require('./routes/leaveEncashedRoutes');
// const authUserOrMachine = require('./middlewares/authUserOrMachine.js');
const companyAssetRoutes = require('./routes/companyAssetsRoutes');
// const scrapRoutes = require('./routes/scrapRequestRoutes')
// const assetTransferRoutes = require('./routes/assetTransfer')
// const compensatoryRoutes = require('./routes/compensatoryLeaveRoutes')
// const maintenanceQuestionsRoutes = require('./routes/maintenanceQuestionsRoutes')
// const maintenanceLogsRoutes = require('./routes/maintenanceLogsRoutes')
// const maintenanceSlotsRoutes = require('./routes/maintenanceSlotsRoutes')
const releasenoteRoute = require('./routes/releaseNote')
const baseSalaryChangeRoute = require('./routes/baseSalaryChangeRoutes')
const compoffChangeRoute = require('./routes/compoffChangeRoutes')
const meetingRoutes = require('./routes/meetingRoutes')
// const hardwareRoutes = require('./routes/hardwareRoutes')
// const temperatureRoutes = require('./routes/temperatureRoutes')
// const distributorRequestRoutes = require('./routes/distributorRequestRoutes')
// async function queryWithMetrics(text, params, req) {
// const start = Date.now();
// const clientIp = req ? (req.headers['x-forwarded-for'] || req.socket.remoteAddress) : 'Unknown';
// try {
// const res = await pool.query(text, params);
// const duration = Date.now() - start;
// newrelic.recordMetric('Custom/DBQuery', duration);
// newrelic.recordCustomEvent('DatabaseQuery', {
// query: text,
// executionTime: duration,
// clientIp: clientIp
// });
// return res;
// } catch (error) {
// newrelic.noticeError(error);
// throw error;
// }
// }
const numCPUs = os.cpus().length;
if (cluster.isMaster) {
console.log(`Master process ${process.pid} is running`);
require('newrelic');
// Fork workers
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
// Listen for worker exit
cluster.on('exit', async (worker, code, signal) => {
// const publicIp = await getPublicIP();
console.log(`Worker ${worker.process.pid} exited. Restarting...`);
// newrelic.recordCustomEvent('WorkerExit', {
// workerId: worker.process.pid,
// exitCode: code,
// signal: signal,
// // serverIp: publicIp
// });
cluster.fork();
});
// require('./cronJobs/alertTriggerCron.js')
// require('./cronJobs/cronForMaps.js'); // Adjust the path as necessary
// require('./cronJobs/cronForRefilingHours.js');
// require('./cronJobs/poSummaryCron.js')
// require('./cronJobs/cronForEmailBills.js');
// require('./cronJobs/cronToEmailBillSumarriesToZOHOUsers.js');
// require('./cronJobs/cronForSalaryCalcsEveryMonth.js')
// const attendanceCron = require('./cronJobs/attendanceCronJob.js');
// attendanceCron.scheduleCronJob();
// require('./cronJobs/cronForProductScores.js')
// require('./cronJobs/orderingModel.js'); // Adjust the path as necessary
// require('./cronJobs/cronJobForAvailabilityStatus'); // Adjust the path as necessary
// require('./cronJobs/cronForWarehouseInventoryToNR.js'); // Adjust the path as necessary
// require('./cronJobs/cronForAvgSalesClusterAndProduct.js'); // Adjust the path as necessary
// require('./cronJobs/cronForPOautoCancellation.js'); // Adjust the path as necessary
// require('./cronJobs/cronForUsersPointsCalculation.js'); // Adjust the path as necessary
// require('./cronJobs/autoEncash.js');
// require('./new Relic/attendance_newRelic.js');
// require('./new Relic/clusterWiseAttendance.js');
// require('./new Relic/lastAttendance.js')
// require('./cronJobs/refillCountResetCron.js');
// require('./new Relic/todayAttendance.js')
// require('./cronJobs/toSendBillsAdjustmentToNr.js')
// // over ride request summary cron job
// require('./new Relic/override_requests.js')
// require('./new Relic/workingHours/avg_availability.js');
} else {
const app = express();
const port = process.env.PORT || 4000;
const MAX_CONCURRENT_REQUESTS = 150;
let currentRequests = 0;
const requestQueue = [];
// Middleware to manage concurrent requests
const concurrencyLimiter = (req, res, next) => {
if (currentRequests < MAX_CONCURRENT_REQUESTS) {
currentRequests++;
res.on('finish', () => {
currentRequests--;
if (requestQueue.length > 0) {
const nextReq = requestQueue.shift();
nextReq();
}
});
next();
} else {
console.log(`Queueing request to ${req.method} ${req.originalUrl}`);
requestQueue.push(() => {
concurrencyLimiter(req, res, next);
});
}
};
// Apply the middleware globally
// app.use(concurrencyLimiter);
// setInterval(() => {
// console.log(`Current active requests: ${currentRequests}`);
// console.log(`Requests in queue: ${requestQueue.length}`);
// }, 5000); // Log every 5 seconds
app.use(timeout('50s'));
// for emergency use only
// app.use((req, res, next) => {
// // If load is too high, short-circuit with success
// if (process.env.ALWAYS_SUCCESS === "true") {
// return res.status(200).json({ success: true });
// }
// next();
// });
// Middleware to handle timeouts
app.use((req, res, next) => {
if (req.timedout) {
console.log(`Request to ${req.method} ${req.originalUrl} timed out`);
activeSockets.delete(req.socket); // Clean up socket
if (!res.headersSent) {
res.status(503).end('Request timed out');
}
} else {
next();
}
});
app.use((err, req, res, next) => {
console.error(`Error during request to ${req.method} ${req.originalUrl}:`, err);
activeSockets.delete(req.socket); // Clean up socket on error
if (!res.headersSent) {
res.status(500).json({ error: 'Internal Server Error' });
}
});
app.use(cors());
// app.use(cors({
// origin: "*",
// methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
// allowedHeaders: ["Content-Type", "Authorization"]
// }));
app.use(bodyParser.json());
app.use(logger); // Apply logger to every request
// const newrelic = require('newrelic');
const activeSockets = new Map(); // Map to track active sockets by endpoint
// Middleware to track incoming requests and associated sockets
app.use((req, res, next) => {
const startTime = Date.now();
const socket = req.socket;
const endpoint = `${req.method} ${req.originalUrl}`;
if (!activeSockets.has(socket)) {
activeSockets.set(socket, endpoint);
}
req.on('aborted', () => {
console.log(`Request to ${endpoint} was aborted by the client`);
activeSockets.delete(socket); // Clean up socket
});
req.on('timeout', () => {
console.log(`Request to ${endpoint} timed out after 30s`);
activeSockets.delete(socket);
console.log(`Socket deleted for ${endpoint}`);
if (!res.headersSent && !res.finished) {
res.status(503).end();
}
});
res.on('finish', () => {
const duration = Date.now() - startTime;
// console.log(`Request to ${endpoint} finished in ${duration}ms`);
activeSockets.delete(socket); // Remove the socket after response is sent
});
res.on('close', () => {
console.log(`Response closed for ${endpoint}`);
activeSockets.delete(socket); // Ensure socket cleanup
});
next();
});
// api monitoring middleware
app.use((req, res, next) => {
const start = Date.now();
const endpoint = `${req.method} ${req.originalUrl}`;
const clientIp = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
// req.on('aborted', () => {
// console.log(`Request to ${endpoint} was aborted`);
// });
// req.on('timeout', () => {
// console.log(`Request to ${endpoint} timed out`);
// if (!res.headersSent && !res.finished) {
// res.status(503).end('Request timed out');
// }
// });
// res.on('finish', () => {
// const duration = Date.now() - start;
// newrelic.recordMetric(`Custom/API/${endpoint}`, duration);
// newrelic.recordCustomEvent('APIRequest', {
// endpoint: endpoint,
// responseTime: duration,
// clientIp: clientIp,
// statusCode: res.statusCode,
// });
// console.log(`Request from ${clientIp} to ${endpoint} finished in ${duration}ms`);
// });
next();
});
// setInterval(() => {
// const cpuUsage = os.loadavg()[0]; // 1-minute CPU load
// const memoryUsage = process.memoryUsage().rss / 1024 / 1024; // MB
// newrelic.recordCustomEvent('CPUUsageEvent', {
// cpu: parseFloat(cpuUsage.toFixed(2)),
// memory: parseFloat(memoryUsage.toFixed(2))
// });
// console.log(`CPU: ${cpuUsage}, Memory: ${memoryUsage}MB`);
// }, 60000);
// Example: Applying authentication middleware to secure routes
// app.use('/api/secure', authenticate, secureRoute);
// Public route
// app.get('/health', (req, res) => {
// res.send('Vending Machine API is running');
// // res.send(`Hello from Vending Machine Worker ${process.pid}`);
// });
// app.post('/test', (req, res) => {
// setTimeout(() => {
// res.status(200).send('OK');
// }, 2000); // 2 seconds delay
// });
// app.use('/webhook', notificationRoute)
// app.use('/api/categories', authorize, categoryRoutes);
// app.use('/api/todotasks', authorize, todoTasksRoutes);
// app.use('/api/todotask_comments', authorize, todoTasksCommentsRoutes);
// app.use('/api/machine_payment', authorize, machinePaymentRoutes);
// app.use('/api/transactionDetails', authorize, transactionDetailsRoutes);
// app.use("/api/coil-recommendations", authorize, coilRecommendationRoutes);
app.use('/api/teams', teamRoutes);
// app.use('/api/refillHistories', authorize, refillHistoryRoutes);
// app.use('/api/warehouses', authorize, warehouseRoutes);
// app.use('/api/gst-registered-addresses', authorize, gstRegisteredAddressesRoutes);
// app.use('/api/stockFlavourTransfer', authorize, stockFlavourTransferRoutes);
// app.use('/api/brands', authorize, brandRoutes);
// app.use('/api/lastId', authorize, lastIdRoutes);
// app.use('/api/warehouseSlots', authorize, warehouseSlotRoutes);
// app.use('/api/stockexchange', authorize, stockExchangeRoutes);
// app.use('/api/bagSlots', authorize, bagSlotRoutes);
// app.use('/api/vendItems', authorize, vendItemRoutes);
// app.use('/api/organizations', authorize, organizationRoutes);
// app.use('/api/scrapsale', authorize, scrapSaleRoutes);
// app.use('/api/clusters', authorize, clusterRoutes);
// app.use('/api/distributors', authorize, distributorRoutes);
// app.use('/api/purchaseProducts', authorize, purchaseProductRoutes);
// app.use('/api/roles', authorize, roleRoutes);
// app.use('/api/pickLog', authorize, pickLogRoute);
// app.use('/api/returnLog', authorize, returnLogRoute);
// app.use('/api/product-mismatch', authorize, productMismatchRoutes);
// app.use('/api/stockUpdateLog', authorize, stockUpdateLogRoutes);
// app.use('/api/purchaseOrder', authorize, purchaseOrderRoutes);
// app.use('/api/draft-purchaseOrder', authorize, draftPurchaseOrderRoutes);
// app.use('/api/inventoryDiscrepancy', authorize, inventoryDiscrepancyRoutes);
// app.use('/api/audit-questions', authorize, auditQuestions);
// app.use('/api/purchaseOrderProduct', authorize, purchaseOrderProductRoutes);
// app.use('/api/notificationRecipients', authorize, notificationRecipientsRoutes);
// app.use('/api/notificationDetails', authorize, notificationsRoutes);
app.use('/api/expoPushToken', expoPushTokenRoutes);
app.use('/api/userSalary', salaryRoutes);
app.use('/api/baseSalaryChange/',baseSalaryChangeRoute)
app.use('/api/compoffChange',compoffChangeRoute)
app.use('/api/meetings',meetingRoutes)
// app.use('/api/inventory-ordering', authorize, inventoryOrderingRoutes);
// app.use('/api/product-recommendation', authorize, productRecommendationRoutes);
// app.use('/api/responsefrompaytm', responseFromPaytmRoutes);
// app.use('/api/v2', authorize, SpecialUserRoutes);
// app.use('/api/dashboard-login', dashboardLoginRoute);
// app.use('/api/user-login', userLoginRoutes);
// app.use('/api/coils', authUserOrMachine, coilRoutes);
// app.use('/api/rfidusers', rfidUsersRoutes);
// app.use('/api/rfidtransactions', rfidTransactionRoutes);
app.use('/api/releasenote',releasenoteRoute)
// app.use('/api/rfidrechargeLogs', rfidRechargeLogsRoutes);
// app.use('/api/machineDetails', machineDetailsRoutes);
// app.use('/api/orders', orderRoutes);
// app.use('/api/products', productRoutes);
// app.use('/api/ebill', ebillRoutes);
// app.use('/api/login-verify', loginVerifyRoutes);
// app.use('/api/onboarding', onBoardingRoutes);
// app.use('/api/downtime', downtimeRoutes);
// app.use('/api/transactions', transactionRoute);
// app.use('/api/payments', authenticate, paymentRoutes);
// app.use('/api/products', authenticate, productRoutes);
// app.use('/api/ratings', authenticate, ratingsRoutes);
// app.use('/api/v1', authenticate, SpecialMachineRoutes);
// // Global error handler should be the last piece of middleware
// app.use('/api/uploadImage', authorize, ImageUploadRoute);
// app.use(errorHandler);
// new code Empoyee Tracking
app.use("/api/attendance", attendanceRoute);
app.use("/api/odometer", odometerRoute);
app.use("/api/attendanceoverride", attendanceOverrideRoute)
app.use("/api/distanceoverride", distanceOverrideRoutes)
app.use("/api/shifts", shiftRoute);
// app.use("/api/discountOffer", authorize, discountOfferRoutes);
app.use("/api/breaks", breakRoute);
app.use("/api/leaves", leaveRoute);
app.use("/api/expenses", expenseRoute);
app.use('/api/incentives', incentiveRoutes);
// app.use('/api/fines', fineRoutes);
// app.use('/api/leaveEncashed', leaveEncashedRoutes)
// app.use('/api/companyassets', companyAssetRoutes);
// app.use('/api/scrap', authorize, scrapRoutes)
// app.use('/api/assettransfer', authorize, assetTransferRoutes)
// app.use('/api/compensatoryleave', authorize, compensatoryRoutes)
// Maintenance routes
// app.use('/api/maintenance-questions', authorize, maintenanceQuestionsRoutes);
// app.use('/api/maintenance-records', authorize, maintenanceLogsRoutes);
// app.use('/api/maintenance-slots', authorize, maintenanceSlotsRoutes);
// Hardware and Temperature routes
// app.use('/api/hardware', authorize, hardwareRoutes);
// app.use('/api/temperature', authorize, temperatureRoutes);
// Distributor request routes
// app.use('/api/distributor-requests', authorize, distributorRequestRoutes);
// --- Serve the React App ---
// IMPORTANT: Define the route where your React app will be served
const REACT_APP_BASE_ROUTE = '/'; // This should match what you set in vite.config.js and BrowserRouter
// Serve static files from the React app's build directory
app.use(REACT_APP_BASE_ROUTE, express.static(path.join(__dirname, 'gg-main-website', 'dist')));
// For any other GET request that doesn't match an API route or static file
// within the React app's base route, serve the React app's index.html.
// This is crucial for React Router to handle client-side routing.
// app.get(`${REACT_APP_BASE_ROUTE}/*`, (req, res) => {
// res.sendFile(path.join(__dirname, 'gg-main-website', 'dist', 'index.html'));
// });
// app.use(express.static(path.join(__dirname, 'gg-main-website', 'dist')));
// // Fallback route for React Router
// app.get('*', (req, res) => {
// res.sendFile(path.join(__dirname, 'gg-main-website', 'dist', 'index.html'));
// });
app.listen(port, '0.0.0.0', () => {
console.log(`Server running on port ${port} with Worker ${process.pid}`);
});
}
// module.exports = app

The repository appeared to belong to one of the company’s developers and contained source code related to the application. Fortunately or unfortunately, it had been left public, exposing a wealth of internal information that was never intended to be accessible to external users.

While reviewing the repository, we discovered Google Cloud service account credentials committed directly to the source code.

{
"type": "service_account",
"project_id": "go-grab-13dac",
"private_key_id": "b044ac6e633a1fe8d661b1e94d0e8970cxxxxxxxx",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCUBZKxL/qXy3LM\nmyigJ+Qh4BDxmWGGf7y6XqY9+qggX7nyt62b4xxxxxxxueKb5HuJNTr0/\nTXaq86LLvN6JykyujCz87RHf1gTAc1DoV88I5Yos7RC/xJdyraLWdKo/l0joc8VQ\nI9ErPXdIfFDZCEaS9Wo20UQAbzik6ydJi8cK/hXEt7+gS3LqsRzyZ18oUuSD63MR\n3R0XB8U0fvfC5Rcs5rbGuaKTYU3FaEWvL120Bgf8LVKIj+5z1xJTjFixuGr3yuQ/\nWhk5YEX1nerfOqq09qLQx9QoMV3NHuxxxxxxq80d4jaz2SlkeJnD5W1l2tr\naz+9VJPHAgMBAAECggEACuTCxX3cSZnu98Y+01PsPbP8UJOIXQ9/PRC517Df+QtN\nJ42Ys3LGzPtoVuUvfUlg81kMzM5sOuKdxJANDjVtAzqB/uO1CK7coWafHKPnE0oe\np7gipAB5+VF0cfCOxsv8QnUxPF8uvfDF54kjRoqOV0woNQ4fXNxpNeHpHnERBaNY\nwQTI07ZlqzF8axuOozf870IlV5AvPMmU9oAeueMa8pqq4qvr9Ox2uP53kV3GsdQZ\nHbibcKNVo3eo8DSX54t7KMdSRmFjMIdt+9Ae+hOuWEx7YoQQOIk1RvErCLoFvIOD\nIX5QtINObuTfUu5LLSuHjlZJbAUvQIm+guuMFo6+mQKBgQDLtqoNI7LV8V/x5Eed\nAhtCk0pRjRpRK37DUmq0ef3IdYgUfPLCtqW4Bm3uSQm87XnjMenUK/RfE1PRQRMR\nTSovRN4FmhIvmhZUPd8mz6L5fN3Zi0BL+UqXk9Yazn7TbQ2rc/6dYKoulEgCGfeA\nWADO2QRB4Cd9dxl3ZcL6bF2naQKBgQC6A5bMW1gXaU3mGcky5xw384EN1YZcpFxsxxxxxxxezjuB91L2e3HPdQG10uig+IOIGmWhgcjTefQfVv2P7v6ywNVBKit/\nuYmW9Z2tf3A2Uqvo0SC57LrX7/Ashinh0wA+uFHHG0jGibVO5KlW6ySS1+NLYYgv\nhY+yExyrrwKBgCZcRldlEocRBeUx/H5HaES4IbWLoZoJW7yCJ4/GrRBzeWWKRdh+\ntmQZ6deGL2xBN8OdoY+Pm1vP8uejnmiSpE/0Yu6hHa2TEYNZh0zyqpjad5hAjaIA\n1pgGgbnYiq9biMhxdk/CZSmSSKEErMOhTr2kxhV1lVod9FC0e+vWPiiJAoGAMnH4\nulDnUeoH6ygBDWHpoC7UR9kpSAR24A8Hm4fFurwrRZ4uOy/Eev29PqKfpRyeY9Eh\nco6eEy+sM9W1463ms9r/jaCH9NUn2MqLSrovWbbeyoye/FOCiYlunlL+kkSMJSbg\nwSGhY9q9YMJFvjB60AyS7ZPIm0/G7ARPlN5j/xcCgYAvEyEDhICoeD0F86wDjzF/\nG86c7JtsRG6iutsoU2l8PMP/EyiQ2LY6CBJcKH+WDaTVhBRu0kzglVFfIBBzDU/g\nEwc7cG6U08FisxpkHD+StkTUDBgfQlblQ3oFrXfPpQ6rLdSatfMoONrOLxZfUDPh\xxxxxxxxxx/YFhsQapQ==\n-----END PRIVATE KEY-----\n",
"client_email": "firebase-adminsdk-xxxxx@go-grab-13dac.iam.gserviceaccount.com",
"client_id": "11195855701960100xxxxx",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-ou9x6%40go-grab-13dac.iam.gserviceaccount.com",
"universe_domain": "googleapis.com"
}

Even more concerning, the credentials had not been revoked or rotated and were still valid at the time of testing. Depending on the permissions assigned to the service account, this could have provided extensive access to cloud resources associated with the organization.

The hijack of 300+ machines

Returning to the API endpoints, we began systematically testing the various functionalities exposed by the application. During this process, we identified several low- and medium-severity vulnerabilities. However, to keep this write-up focused and concise, I will not be covering those findings in detail.

Instead, I will focus on the issues that ultimately led to the full compromise of the environment.

While testing the available endpoints, we came across a particularly interesting endpoint: /api/machineDetails. This endpoint exposed information related to Dataplicity.

  • Dataplicity is basically a remote access and management service specifically built for small Linux-based IoT devices.

image

We decided to investigate the exposed data further to determine whether it could be leveraged for additional access or privilege escalation opportunities.

Going Beyond Privileges

Using the credentials exposed by the /api/machineDetails endpoint, we attempted to log in to the Dataplicity dashboard. Unsurprisingly, the credentials were valid and granted us access.

image

What made this finding particularly severe was that these credentials were not limited to a single device. The endpoint exposed credentials for every machine managed by the organization, more than 300 machines in total.

The Dataplicity interface provided an overview of all registered devices and their current status, indicating whether each machine was online or offline.

Among the available features, the most interesting and impactful was the Terminal functionality. This feature allowed direct shell access to the underlying machine through the web interface, effectively providing remote command execution on any device for which valid credentials were available.

image

As can be clearly seen in the screenshot above, we did not have permission to access this directory because our session was running as the dataplicity user rather than root or gograb.

We had been reading about a recently disclosed and highly discussed Linux privilege escalation vulnerability known as “copy.fail”. A related vulnerability, known as DirtyFrag, had also attracted significant attention. Unlike classic vulnerabilities such as Dirty COW, which relied on race conditions, these issues stemmed from flaws in Linux kernel subsystems rather than traditional memory corruption bugs.

You can read more about these vulnerabilities here: Copy.fail and DirtyFrag

Since GCC was already installed on the system, I decided to use the DirtyFrag exploit. Initially, however, the exploit failed because none of us realized that the target Raspberry Pi devices were running on an ARM architecture rather than x86.

After quickly searching for an ARM-compatible version of the exploit, we came across an ARM64/AArch64 port of DirtyFrag. The repository was based on the original work by V4bel and had been adapted specifically for ARM-based systems.

With a compatible version of the exploit in hand, we proceeded to test whether the target machines were vulnerable.

And BOOM! We were root.

Root access announcement

image

Literally at this point we could do whatever we wanted from the machine: uninstall the running software, change the UPI parameter so that all the transactions go to us instead of the Go-Grab team, or take the machines offline entirely.

The last part

➜ gg-machine-build tree
.
|-- backend
| `-- backend
`-- frontend
|-- build
| |-- GoGrab.png
| |-- GoGrab2.png
| |-- asset-manifest.json
| |-- assets
| | `-- upi
| | |-- amazonpay.svg
| | |-- googlepay.svg
| | |-- paytm.svg
| | |-- phonepe.svg
| | |-- samsungpay.svg
| | `-- upi-icon.svg
| |-- coffeeD.png
| |-- default_product_image.png
| |-- favicon.ico
| |-- index.html
| |-- info.png
| |-- infoButton.png
| |-- logo192.png
| |-- logo512.png
| |-- manifest.json
| |-- payment-failure.gif
| |-- payment-failure1.gif
| |-- payment-failure11.gif
| |-- payment-success.gif
| |-- payment-success.jpg
| |-- robots.txt
| |-- sadkid.gif
| |-- serverdown.jpeg
| |-- serverdown.png
| |-- static
| | |-- css
| | | |-- 307.2e9dce54.chunk.css
| | | |-- 348.31d6cfe0.chunk.css
| | | |-- 354.f66f4b23.chunk.css
| | | |-- 376.2e288fef.chunk.css
| | | |-- 408.4ae0fefd.chunk.css
| | | |-- 473.0e2deebc.chunk.css
| | | |-- 481.c360b5e2.chunk.css
| | | |-- 519.7fadf8cd.chunk.css
| | | |-- 578.76f2db3f.chunk.css
| | | |-- 660.de896818.chunk.css
| | | |-- 694.6f33be38.chunk.css
| | | |-- 801.f82278c8.chunk.css
| | | |-- 807.1ed4ebb1.chunk.css
| | | |-- 926.322d6e5b.chunk.css
| | | `-- main.6da30c0b.css
| | `-- js
| | |-- 145.beebdc5c.chunk.js
| | |-- 206.f8819006.chunk.js
| | |-- 243.a3eca5e0.chunk.js
| | |-- 289.6a2c5b64.chunk.js
| | |-- 307.7a8743ad.chunk.js
| | |-- 348.7ba1226a.chunk.js
| | |-- 354.3dee829c.chunk.js
| | |-- 355.aacb1184.chunk.js
| | |-- 376.1a5073b0.chunk.js
| | |-- 408.c31f6bc1.chunk.js
| | |-- 473.4b1740cf.chunk.js
| | |-- 481.664f4e62.chunk.js
| | |-- 519.1dc50fa0.chunk.js
| | |-- 578.343a0070.chunk.js
| | |-- 660.27e02022.chunk.js
| | |-- 694.1d53c888.chunk.js
| | |-- 779.6c7dcc4e.chunk.js
| | |-- 801.db765cee.chunk.js
| | |-- 807.00da08e1.chunk.js
| | |-- 807.00da08e1.chunk.js.LICENSE.txt
| | |-- 926.20e3edc5.chunk.js
| | |-- main.39daa36b.js
| | `-- main.39daa36b.js.LICENSE.txt
| |-- support_qr.png
| `-- tom-and-jerry-sad.gif
|-- start_frontend.sh
`-- triggerRelay.py
9 directories, 70 files

This was the tree view of all the files present in it; the backend was a binary. As I like reverse engineering, I quickly thought of seeing what was inside the binary.

After spending some time I was able to extract the whole backend code.

total 184
drwxr-xr-x 17 0x1622 staff 544 Jun 3 11:51 .
drwxr-xr-x 5 0x1622 staff 160 Jun 3 11:49 ..
-rw-r--r--@ 1 0x1622 staff 8196 Jun 3 14:05 .DS_Store
drwxr-xr-x 4 0x1622 staff 128 Jun 3 11:43 controllers
drwxr-xr-x 8 0x1622 staff 256 Jun 3 11:43 imageLoader
drwxr-xr-x 13 0x1622 staff 416 Jun 3 11:43 Interaction
-rw-r--r-- 1 0x1622 staff 19963 Jun 3 11:43 machineAgent.js
-rw-r--r-- 1 0x1622 staff 35752 Jun 3 11:43 machineAgent.js.jsc
-rw-r--r-- 1 0x1622 staff 2169 Jun 3 11:43 machineState.js
-rw-r--r-- 1 0x1622 staff 4200 Jun 3 11:43 machineState.js.jsc
drwxr-xr-x 8 0x1622 staff 256 Jun 3 11:43 middlewares
drwxr-xr-x 163 0x1622 staff 5216 Jun 3 11:49 node_modules
-rw-r--r-- 1 0x1622 staff 1074 Jun 3 11:43 package.json
drwxr-xr-x 6 0x1622 staff 192 Jun 3 11:43 routes
-rw-r--r-- 1 0x1622 staff 3907 Jun 3 13:29 server.js
-rw-r--r-- 1 0x1622 staff 4072 Jun 3 11:43 server.js.jsc
drwxr-xr-x 8 0x1622 staff 256 Jun 3 11:43 src

There were around 866 directories and 8026 files in the backend.

At this point, we had obtained both the frontend and backend source code for the entire system. But the satisfaction of hacking the vending machine and getting the items for free was still missing :(

This section is dedicated to Codex, which helped me set up a complete local testing environment. With its assistance, I was able to configure the application, adapt the API endpoints to work locally, and get the entire stack running on my machine.

Basically I was able to run their whole setup locally to test for vulnerabilities.

image

This is how http://localhost:3000 would look like on the vending machine system.

And this was mine:

image

Yes, I know this does not look great, but it was more than sufficient for testing.

NOTE: The vulnerabilities mentioned below are not yet properly tested. These vulnerabilities are working locally on my laptop, but I am pretty sure they would work on the machine as well.

  1. The first request after selecting an item and checking out is this:

image

  1. It basically generates a QR in this type:

image

  1. The next request continuously checks if the user has paid the money or not. On successful payment, the value of "resultStatus": "PENDING" changes to "resultStatus": "TNX_SUCESS" and "resultcode": "402" changes to "resultcode": "01".

image

  1. The next request is only made if the user has paid the money:

image

  1. The next request is pretty straightforward: a PUT request is sent to this endpoint to get the item dispensed.

image

  1. This is the last request sent, which again is pretty straightforward and basically updates the machine on the number of items available.

image

While testing locally, I was able to bypass the payment and eventually get the item for free.

The vulnerability was basically that there were no checks on /api/v1/createOrderWithVendItems to verify whether the payment was made, which creates a flow that eventually gets us items for free.

Some random stuff we planned but did not do

Our initial idea was to display it across every vending machine managed by the company. However, after discussing the potential impact, we decided against it.

f

When Manav asked about bounty this was the reply he got

Manav&#x27;s bounty discussion

Bounty offer follow-up

As a POC like my previous blog, I was not able to shoot some good video, though I will try to add a video later if possible of shutting down a vending machine remotely. edit: i am back home , did not get time to shoot one :(

Ending Note

This summer, I learned a lot about IoT and discovered many new techniques along the way. I also collaborated with several young hackers on government and private projects. You can view some of them here: 1, 2, and 3.

There are many other projects that I cannot disclose at the moment, but you can expect more technical and detailed blog posts soon :)

At this point, I have almost stopped playing CTFs and have started focusing primarily on real world security research. I am also happy to say that I earned more money this summer than I ever have before, and I will soon be starting my final year of college.

I hope you enjoyed reading my blogs. You can reach out to me anytime if you have questions about cybersecurity or need learning resources.

You can directly ask me on X/Twitter or discord (0x1622).