一、背景
2025年2月某日,刚放完春节假期回来上班,大伙都还沉浸在放假的氛围里,写了几份春节保障(重保都要常态化了QAQ,脑仁疼)的报告后准备收拾东西下班,突然项目经理打电话说:"客户被通报网站存在涉黄链接了,需要协助客户处理,不用下班了——!",OMG,天塌了!
二、影响范围
公网的一台提供WEB服务的服务器。
问题链接:
三、排查过程
1、文件溯源
现场的安全做的,Emmm....一言难尽,就几台安全设备孤零零的杵在那儿。我们看着探针上面几个大大的零,风中凌乱~(现在行业乱的一塌糊涂,很多项目的安全建设其实都是空壳子自欺欺人罢了),得了设备是一点指望不上了。
先自己在虚拟环境访问看看是怎么个事儿(ˋ( ° ▽、° ) ),
发现访问2.svg这个图片给自动加载了https://nd3.net.cname.odnio.cn这个域名,到微步威胁情报网看看有没有情报标签(微步真好用),一看好家伙,演都不演一下的是吧:
现在估计就是2.svg这图片存在问题,那就找对应厂商大佬一起看看,看看能不能从站点的目录下面找到这个2.svg是什么东西,结果一问发现这个域名对应服务器的站点目录下就没有这个/website/downloadimages/xxx目录,WTF???!!!都访问上了结果说没这个目录,当场懵B~
接着跟着这个链接在代理文件找到了这么一条记录:
Emmm。。。厂商大佬说这是别的系统挂到咱系统上的,这边只是给他们做了个代理,忙活半天是其他的平台的服务器出的问题,不是咱的问题,唉~
跟甲方同步了消息,甲方让等着他来带着去找那边项目的人,协助排查一下T_T
然后跟着甲方跑到别人地盘上,找到对应的负责人说明了一下情况,然后得到的消息更炸裂,因为业务问题他们刚不久才将目录下面的东西全给清理了一遍!!!(毁灭吧~)
不过好在他们将某些目录的文件备份了一下,在这些备份里面发现了2.svg和3.svg这两个图片,又要了份最近的日志发现了记录,但是日志上显示这个图片请求明明已经被拦截了为什么还上传上去了,而且还穿了好几个,百思不得其解。
厂商大佬那边去查怎么上传的和找上传的人去了,我们就先看看这svg文件是个什么情况,看看访问了哪些URL,有什么操作。
2、文件分析
打开文件,发现是一个XML文件伪造的,后面加上参数d,就能加载恶意链接,猜测上面链接中的参数d要么是凭证要么是链接加密生成的,解密的代码就在这个xml文件中;
将后缀修改为XML,再用编辑器看看,好家伙,一段一段的全是编码混淆了的,完全看不明白;
使用代码格式优化一下,稍微能看一下,但是使用了动态编码对于我这种代码菜鸟来说可读性还是太低了:
这就不得不祭出AI大法了(AI这么厉害,感觉我要失业了,呜呜呜~)
下面是主要的几个函数:
// 获取查询字符串的值 function getQueryString(name) { const regex = new RegExp(`(${name}=)([^&]*)`); const results = window.location.search.match(regex); return results ? decodeURIComponent(results[2]) : ''; } // 检查是否是有效的 HTTP 或 HTTPS URL function isHttpOrHttpsUrl(url) { return url.startsWith('http://') || url.startsWith('https://'); } // 异或解密函数 function xorDecrypt(encryptedData) { const key = decryptString(0); // 获取解密密钥 let decrypted = ''; for (let i = 0; i < encryptedData.length; i++) { decrypted += String.fromCharCode(encryptedData.charCodeAt(i) ^ key.charCodeAt(i % key.length)); } return decrypted; }
下面是主函数:
function main() { // 获取 URL 参数 const dParam = getQueryString('d'); if (dParam) { try { // 解密 URL 参数 const decryptedUrl = xorDecrypt(atob(dParam)); if (isHttpOrHttpsUrl(decryptedUrl)) { // 如果是有效的 URL,加载到 iframe 中 document.getElementById('contentFrame').src = decryptedUrl; } else { // 如果不是有效的 URL,显示错误信息 document.getElementById('messageTip').style.display = 'block'; document.getElementById('messageTip').innerHTML = '链接已失效'; } } catch (e) { // 捕获异常并显示错误信息 document.getElementById('messageTip').style.display = 'block'; document.getElementById('messageTip').innerHTML = '链接已失效'; } } }
这么一来整个代码的运行逻辑就出来了,
当请求https://x.x.x.x/2.svg?d=ChYdGRFYQUEMBlpHDAcaQAEMCAQHTAEKDAsGRwEMQRkSDh9dDQFBPisHHl8vW1YpTVNeWlpbX15aVVlbUVNABhYPBQ== 时,程序先通过getQueryString()函数获取参数d,当d不为空时将d的值传给xorDecrypt()函数,再xorDecrypt()经过与密钥key异或后生成一个URL链接,再将这个链接加载到 iframe 框架中展示。
但是又出来一个问题,密钥怎么找呢,代码里面解码太麻烦了。
这里从代码可以看出来密钥key是循环使用的,那我这边偷个懒,既然是最终的结果是https://
nd3.net.cname.odnio.cn,根据异或加密的特征“密文” XOR “密钥” = “明文”,那我们反过来用 “密文” XOR “明文” = “密钥” 就能得到密钥,
拿到密钥"bbiibbnn",测试密钥得到跳转的地址:https://nd3.net.cname.odnio.cn/wplv4oc/PIew6M98G/1738910870231.html
四、后话
我:厂商大哥,这svg咋传上去的啊?
厂商大哥:Emmm...上传逻辑出问题了,这玩意儿它是从附件传上去的,但是操作人没点提交直接关了工单上传窗口,附件没做校验QAQ!
我:。。。。。(看了看时间,完了,十一点了地铁末班车没了,呜呜呜~)
4A评测 - 免责申明
本站提供的一切软件、教程和内容信息仅限用于学习和研究目的。
不得将上述内容用于商业或者非法用途,否则一切后果请用户自负。
本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容。
如果您喜欢该程序,请支持正版,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。敬请谅解!
程序来源网络,不确保不包含木马病毒等危险内容,请在确保安全的情况下或使用虚拟机使用。
侵权违规投诉邮箱:4ablog168#gmail.com(#换成@)