GYCTF2020-Ez_Express

本文最后更新于:2024年12月14日 下午

从这个题学到不少东西,记录一下。

初识原型链

首先这题是有个原型链污染,js中每个类都有个属性__proto__,指向他的基类,他会继承__proto所拥有的特性。

JavaScript 常被描述为一种基于原型的语言 (prototype-based language)——每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain)。

在js中,当我们调用一个对象的一个属性时,会首先在对象自身寻找这一属性,如果自身找不到则会寻找__proto__,找不到就继续往上找,以此类推。

哪些情况下原型链会被污染

在含有能够控制数组(对象)的“键名”的操作即可,一般是以出现:
merge和clone对象

不安全的对象递归合并

以merge,因为merge执行的就是递归,为例,先构造一个简单的merge函数

js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const merge = (target, source) => {
// Iterate through `source` properties and if an `Object` set property to merge of `target` and `source` properties
for (const key of Object.keys(source)) {
if (source[key] instanceof Object) Object.assign(source[key], merge(target[key], source[key]))
}
// Join `target` and modified `source`
Object.assign(target || {}, source)
return target
}
function Person(name,age,gender){//构造一个person类
this.name=name;
this.age=age;
this.gender=gender;
}
let newperson=new Person("test1",22,"male");
let job=JSON.parse('{"title":"Security Engineer","country":"China","__proto__":{"x":1}}');//新建一个job对象
merge(newperson,job);//这个job对象有用title、contry、__proto__属性,并对其进行赋值
console.log(newperson);
console.log(Person.prototype);//此时per的原型已经被改变为x=1

这里解释一下,merge将这几个键值作为一个属性合并到newperson这个对象中了,merge函数执行的其实可以认为是

1
2
Person.job.title=......
Person.job.__proto__=....

这个时候job的原型是Person,所以Person的值就发生了改变。

按路径定义属性

有些JavaScript库的函数支持根据指定的路径修改或定义对象的属性值。通常这些函数类似以下的形式:theFunction(object, path, value),将对象object的指定路径path上的属性值修改为value。如果攻击者可以控制路径path的值,那么将路径设置为_proto_.theValue,运行theFunction函数之后就有可能将theValue属性注入到object的原型中。

引用自https://hwwg.github.io/2021/07/22/js%E5%8E%9F%E5%9E%8B%E9%93%BE%E6%B1%A1%E6%9F%93/#%E5%93%AA%E4%BA%9B%E6%83%85%E5%86%B5%E4%B8%8B%E5%8E%9F%E5%9E%8B%E9%93%BE%E4%BC%9A%E8%A2%AB%E6%B1%A1%E6%9F%93%EF%BC%9F

审计代码

这个题有个www.zip,下载下来是有源代码的,进行审计。

主要的逻辑如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
var express = require('express');
var router = express.Router();
const isObject = obj => obj && obj.constructor && obj.constructor === Object;
const merge = (a, b) => {
for (var attr in b) {
if (isObject(a[attr]) && isObject(b[attr])) {
merge(a[attr], b[attr]);
} else {
a[attr] = b[attr];
}
}
return a
}
const clone = (a) => {
return merge({}, a);
}
function safeKeyword(keyword) {
if(keyword.match(/(admin)/is)) {
return keyword
}

return undefined
}

router.get('/', function (req, res) {
if(!req.session.user){
res.redirect('/login');
}
res.outputFunctionName=undefined;
res.render('index',data={'user':req.session.user.user});
});


router.get('/login', function (req, res) {
res.render('login');
});



router.post('/login', function (req, res) {
if(req.body.Submit=="register"){
if(safeKeyword(req.body.userid)){
res.end("<script>alert('forbid word');history.go(-1);</script>")
}
req.session.user={
'user':req.body.userid.toUpperCase(),
'passwd': req.body.pwd,
'isLogin':false
}
res.redirect('/');
}
else if(req.body.Submit=="login"){
if(!req.session.user){res.end("<script>alert('register first');history.go(-1);</script>")}
if(req.session.user.user==req.body.userid&&req.body.pwd==req.session.user.passwd){
req.session.user.isLogin=true;
}
else{
res.end("<script>alert('error passwd');history.go(-1);</script>")
}

}
res.redirect('/'); ;
});
router.post('/action', function (req, res) {
req.session.user.data = clone(req.body);
res.end("<script>alert('success');history.go(-1);</script>");
});
router.get('/info', function (req, res) {
res.render('index',data={'user':res.outputFunctionName});
})
module.exports = router;

一般ctf中的原型链污染会出现在mergeclone一类的函数中,这里看到有merge函数,clone又对它封装了一层,最后唯一调用clone的是/action路由,但是这里要求管理员登录,所以先研究一下登录。

他要求admin登录但是过滤了admin,这里可以用上p神博客的一个小trick。

“ı”.toUpperCase() == ‘I’,”ſ”.toUpperCase() == ‘S’

“K”.toLowerCase() == ‘k’

js太神辣🥵

登录解决了

在action路由我们可以通过上传json让其解析为键值对达成控制runtime。

再来看info路由,这里调用了个没有定义的outputFunctionName,可以污染outputFunctionName来rce,具体的原理得分析一下ejs的渲染。

ejs渲染分析

分析思路方法来自于https://evi0s.com/2019/08/30/expresslodashejs-%e4%bb%8e%e5%8e%9f%e5%9e%8b%e9%93%be%e6%b1%a1%e6%9f%93%e5%88%b0rce/

主要是探究一下render函数后面到底发生了什么

—-12.17更—-

时间拖太久了懒得再调一次了,总之就是ejs里面有一坨字符串拼接会把outputFunctionName拼进去直接中断渲染提前return造成代码执行。当然这里的outputFunctionName是由我们的原型链污染来控制的,在我们有原型链污染的前提之下,我们可以控制基类的成员。


GYCTF2020-Ez_Express
http://gensokyo.cn/2022/11/25/GYCTF2020-Ez-Express/
作者
s1ain
发布于
2022年11月25日
许可协议