You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

350 lines
17 KiB

import requests
import pexpect
import re
import json
import subprocess
from bs4 import BeautifulSoup
from tools.ToolBase import ToolBase
class CurlTool(ToolBase):
# def __init__(self):
# super.__init__()
# self.headers = {}
# self.url = None
# self.verify_ssl = True
def get_time_out(self):
return 61*2
def validate_instruction(self, instruction_old):
#instruction = instruction_old
#指令过滤
timeout = self.get_time_out() #curl指令遇到不返回的情况 curl --path-as-is -i http://192.168.204.137:8180/webapps/../conf/tomcat-users.xml
#添加-i 返回信息头
if 'base64 -d' in instruction_old:
return instruction_old
# 分割成单词列表便于处理参数
curl_parts = instruction_old.split()
# 如果第一条命令是 curl,则进行处理
if curl_parts and curl_parts[0] == "curl": #只是处理了第一个curl
# 判断是否需要添加包含响应头的选项:若没有 -I、-i 或 --include,则在 URL前插入 -i
if "-I" not in curl_parts and '-i' not in curl_parts and '--include' not in curl_parts:
# 查找以 http:// 或 https:// 开头的 URL 参数所在位置
url_index = next((i for i, p in enumerate(curl_parts)
if p.startswith('http://') or p.startswith('https://')), None)
if url_index is not None:
curl_parts.insert(url_index, '-i')
else:
curl_parts.append('-i')
# 判断是否已经有 --max-time 参数
if not any(p.startswith("--max-time") for p in curl_parts):
curl_parts.append("--max-time")
curl_parts.append(str(self.get_time_out())) #添加超时时间
final_instruction = ' '.join(curl_parts)
return final_instruction, timeout
def do_worker_pexpect(self,str_instruction,timeout,ext_params):
try:
result = ""
exc_do = pexpect.spawn('bash',['-c',str_instruction],timeout=timeout)#spawn 第一个参数是可执行文件
while True:
index = exc_do.expect([
pexpect.TIMEOUT,
pexpect.EOF,
])
try:
response = exc_do.before.decode('utf-8', errors='replace')
except Exception as e:
response = f"无法解码响应: {str(e)}"
result += response
if index == 0:
result += f"\n执行超时{timeout}"
break
elif index == 1:
break
else:
print("遇到其他输出!")
break
print(result)
return result
except Exception as e:
return f"执行错误: {str(e)}"
def do_worker_subprocess(self,str_instruction,timeout,ext_params):
try:
# 执行命令,捕获输出为字节形式
if timeout:
result = subprocess.run(str_instruction, shell=True,timeout=timeout, capture_output=True)
else:
result = subprocess.run(str_instruction, shell=True, capture_output=True)
if result.returncode == 0:
# 命令成功,处理 stdout
try:
response = result.stdout.decode('utf-8', errors='replace')
except Exception as e:
response = f"无法解码响应: {str(e)}"
return response
else:
# 命令失败,处理 stderr
try:
error = result.stderr.decode('utf-8', errors='replace')
except Exception as e:
error = f"无法解码错误信息: {str(e)}"
return error
except Exception as e:
return (f"命令执行失败: {str(e)}")
def execute_instruction(self, instruction_old):
ext_params = self.create_extparams()
# 第一步:验证指令合法性
instruction,time_out = self.validate_instruction(instruction_old)
if not instruction:
return False, instruction_old, "该指令暂不执行!","",ext_params
# 过滤修改后的指令是否需要判重?同样指令再执行结果一致?待定---#?
# 第二步:执行指令---需要对ftp指令进行区分判断
#output = self.do_worker_pexpect(instruction, time_out, ext_params)
output = self.do_worker_subprocess(instruction, time_out, ext_params)
# 第三步:分析执行结果
analysis = self.analyze_result(output,instruction,"","")
return True, instruction, analysis,output,ext_params
def get_ssl_info(self,stderr,stdout):
# --------------------------
# 解释信息的安全意义:
#
# - 如果证书的 Common Name 与请求的 IP 不匹配(如这里的 'crnn.f3322.net'),
# 则可能表明服务隐藏了真实身份或存在配置错误,这在后续攻击中可以作为信息收集的一部分。
#
# - TLS 连接信息(如 TLS1.3 和加密套件)有助于判断是否存在弱加密或旧版协议问题。
#
# - HTTP 状态和 Content-Type 帮助确认返回的是一个合法的 Web 服务,
# 而 HTML Title 暗示了实际运行的是 SoftEther VPN Server,可能存在默认配置或已知漏洞。
#
# 这些信息可以作为进一步探测、漏洞验证和渗透测试的依据。
# --------------------------
# 从 stderr 中提取证书及 TLS 信息
# 提取 Common Name(CN)
cn_match = re.search(r"common name:\s*([^\s]+)", stderr, re.IGNORECASE)
cert_cn = cn_match.group(1) if cn_match else "N/A"
# 提取 TLS 连接信息(例如 TLS1.3 及加密套件)
tls_match = re.search(r"SSL connection using\s+([^\n]+)", stderr, re.IGNORECASE)
tls_info = tls_match.group(1).strip() if tls_match else "N/A"
# 提取 Issuer 信息
issuer_match = re.search(r"issuer:\s*(.+)", stderr, re.IGNORECASE)
issuer_info = issuer_match.group(1).strip() if issuer_match else "N/A"
# 从 stdout 中提取 HTTP 响应头和 HTML 标题
# 分离 HTTP 头部和 body(假设头部与 body 用两个换行符分隔)
parts = stdout.split("\n\n", 1)
headers_part = parts[0]
body_part = parts[1] if len(parts) > 1 else ""
# 从头部中提取状态行和部分常见头部信息
lines = headers_part.splitlines()
http_status = lines[0] if lines else "N/A"
content_type_match = re.search(r"Content-Type:\s*(.*)", headers_part, re.IGNORECASE)
content_type = content_type_match.group(1).strip() if content_type_match else "N/A"
# 使用 BeautifulSoup 提取 HTML <title>
soup = BeautifulSoup(body_part, "html.parser")
html_title = soup.title.string.strip() if soup.title and soup.title.string else "N/A"
# --------------------------
# 输出提取的信息
# print("=== 提取的有用信息 ===")
# print("HTTP 状态行:", http_status)
# print("Content-Type:", content_type)
# print("HTML Title:", html_title)
# print("TLS 连接信息:", tls_info)
# print("证书 Common Name:", cert_cn)
# print("证书 Issuer:", issuer_info)
result = f"HTTP 状态行:{http_status},Content-Type:{content_type},HTML Title:{html_title},TLS 连接信息:{tls_info},证书 Common Name:{cert_cn},证书 Issuer:{issuer_info}"
return result
def get_info_curl(self,instruction,stdout,stderr):
info = {}
# 处理 stdout: 拆分响应头与正文(假设用空行分隔)
parts = re.split(r'\r?\n\r?\n', stdout, maxsplit=1)
if len(parts) == 2:
headers_str, body = parts
else:
# 如果没有拆分成功,可能 stdout 中只有正文,则从 stderr 尝试提取 HTTP 状态行
headers_str = ""
body = stdout
# 如果没有在 stdout 中找到头信息,则尝试从 stderr 中提取(部分信息可能在 stderr 中)
if not headers_str:
header_lines = stderr.splitlines()
else:
header_lines = headers_str.splitlines()
#status_code
if header_lines:
status_line = header_lines[0]
status_match = re.search(r'HTTP/\d+\.\d+\s+(\d+)', status_line)
info['status_code'] = status_match.group(1) if status_match else "Unknown"
else:
info['status_code'] = "No headers found"
#Server
m = re.search(r'^Server:\s*(.+)$', headers_str, re.MULTILINE)
if m:
info["server"] = m.group(1).strip()
#content-type,content-length
content_type = "Not found"
content_length = "Not found"
for line in header_lines:
if line.lower().startswith("content-type:"):
info['content-type'] = line.split(":", 1)[1].strip()
elif line.lower().startswith("content-length:"):
info['content-length'] = line.split(":", 1)[1].strip()
info.setdefault('content-type', "Not found")
info.setdefault('content-length', "Not found")
# 如果内容为 HTML,则使用 BeautifulSoup 提取 <title> 标签内容
if "html" in info['content-type'].lower():
try:
soup = BeautifulSoup(body, "html.parser")
if soup.title and soup.title.string:
info['html_title'] = soup.title.string.strip()
else:
info['html_title'] = "Not found"
except Exception as e:
info['html_title'] = f"Error: {e}"
else:
info['html_title'] = "N/A"
#------------正文部分解析------------
if "phpinfo.php" in instruction:
info["configurations"] = {}
info["sensitive_info"] = {}
# 提取PHP版本信息,可以尝试从phpinfo表格中提取
m = re.search(r'PHP Version\s*</th>\s*<td[^>]*>\s*([\d.]+)\s*</td>', body, re.IGNORECASE)
if m:
info["php_version"] = m.group(1).strip()
else:
# 备用方案:在页面中查找 "PHP Version" 后面的数字
m = re.search(r'PHP\s*Version\s*([\d.]+)', body, re.IGNORECASE)
if m:
info["php_version"] = m.group(1).strip()
# 提取配置信息(如allow_url_include, display_errors, file_uploads, open_basedir)
configs = ["allow_url_include", "display_errors", "file_uploads", "open_basedir"]
for key in configs:
# 尝试匹配HTML表格形式:<td>key</td><td>value</td>
regex = re.compile(r'<td[^>]*>\s*' + re.escape(key) + r'\s*</td>\s*<td[^>]*>\s*([^<]+?)\s*</td>',
re.IGNORECASE)
m = regex.search(body)
if m:
info["configurations"][key] = m.group(1).strip()
# 提取敏感信息,这里以MYSQL_PASSWORD为例
sensitive_keys = ["MYSQL_PASSWORD"]
for key in sensitive_keys:
regex = re.compile(r'<td[^>]*>\s*' + re.escape(key) + r'\s*</td>\s*<td[^>]*>\s*([^<]+?)\s*</td>',
re.IGNORECASE)
m = regex.search(body)
if m:
info["sensitive_info"][key] = m.group(1).strip()
elif "phpMyAdmin" in instruction:
info["security_info"] = {}
info["login_info"] = {}
# 查找登录表单中用户名、密码字段(例如 name="pma_username" 和 name="pma_password")
m = re.search(r'<input[^>]+name=["\'](pma_username)["\']', body, re.IGNORECASE)
if m:
info["login_info"]["username_field"] = m.group(1).strip()
m = re.search(r'<input[^>]+name=["\'](pma_password)["\']', body, re.IGNORECASE)
if m:
info["login_info"]["password_field"] = m.group(1).strip()
#安全信息
# csrf_protection:尝试查找隐藏域中是否存在 csrf token(例如 name="csrf_token" 或 "token")
m = re.search(r'<input[^>]+name=["\'](csrf_token|token)["\']', stdout, re.IGNORECASE)
info["security_info"]["csrf_protection"] = True if m else False
# httponly_cookie:从响应头中查找 Set-Cookie 行中是否包含 HttpOnly
m = re.search(r'Set-Cookie:.*HttpOnly', stdout, re.IGNORECASE)
info["security_info"]["httponly_cookie"] = True if m else False
# secure_cookie:从响应头中查找 Set-Cookie 行中是否包含 Secure
m = re.search(r'Set-Cookie:.*Secure', stdout, re.IGNORECASE)
info["security_info"]["secure_cookie"] = True if m else False
else: #
#info['body_snippet'] = body[:200] # 前500字符
if stderr:
# 处理 stderr 中的 TLS/证书信息:只提取包含关键字的行
tls_info_lines = []
cert_info_lines = []
for line in stderr.splitlines():
# 过滤与 TLS/SSL 握手、证书相关的信息
if "SSL connection using" in line or "TLS" in line:
tls_info_lines.append(line.strip())
if "certificate" in line.lower():
cert_info_lines.append(line.strip())
info['tls_info'] = tls_info_lines if tls_info_lines else "Not found"
info['certificate_info'] = cert_info_lines if cert_info_lines else "Not found"
#转换成字符串
result = json.dumps(info,ensure_ascii=False)
result = result+"\n结果已经被格式化提取,若需要查看其他内容,可以添加grep参数后返回指令。"
#print(result)
return result
def analyze_result(self, result,instruction,stderr,stdout):
if len(result) < 2000:
return result
if "curl: (28) Operation timed out after" in result or "Dload Upload Total Spent Left Speed" in result:
return "执行超时"
#指令结果分析
if("-H "in instruction and "Host:" in instruction):# 基于证书域名进行跨站劫持
if "HTTP/1.1 200" in result: # and "<urlset" in result:
#result = "存在问题:成功通过 Host 头获取 sitemap 内容。"
result = "返回200,存在问题" #要进一步分析返回内容
else:
result = "未检测到问题"
elif("-H "in instruction and "Range:" in instruction):
if "HTTP/1.1 200 OK" in result:
result = "正常返回了200 OK页面"
elif "HTTP/1.1 416" in result:
if "Content-Range: bytes" in result:
result = "返回 416 且 Content-Range 正常:服务器对 Range 请求处理正确。"
elif "Content-Range:" in result:
result = "返回 416 但缺少或异常 Content-Range 头"
else:#"返回其他状态码(", response.status_code, "):需要进一步分析。"
result = "服务器对Range请求处理正确"
elif("-H "in instruction and "Referer:" in instruction):
if "HTTP/1.1 200" in result:
result="该漏洞无法利用"
else: #保留原结果
pass
elif("resource=config.php" in instruction):
if "base64: 无效的输入" in result:
result="该漏洞无法利用"
elif("-kv https://" in instruction or "-vk https://" in instruction):
result = self.get_ssl_info(stderr,stdout)
elif("grep " in instruction or " -T " in instruction or "Date:" in instruction):
return result
# elif("-X POST " in instruction):
# result = self.get_info_curl(instruction,stdout,stderr)
# elif("-v " in instruction): #curl -v http://192.168.204.137:8180/manager/html --user admin:admin 常规解析curl返回内容
# result = self.get_info_curl(instruction,stdout,stderr)
else:
result = self.get_info_curl(instruction,stdout,stderr)
return result
if __name__ =="__main__":
import subprocess
CT = CurlTool()
strinstruction = "curl -kv -X POST -d \"username=admin&password=admin\" https://58.216.217.70/vpn/index.html --connect-timeout 10"
instruction,time_out = CT.validate_instruction(strinstruction)
if instruction:
result = subprocess.run(instruction, shell=True, capture_output=True, text=True)
res = CT.analyze_result(result.stdout,instruction,result.stderr,result.stdout)
print(res)