常用工具及插件

findsomething浏览器插件、packerfuzzer和JSFinder之类的工具

Webpack情形下提取js及接口

有时候findsomething会看不到接口,wap显示是webpack打包,使用packerfuzzer工具

1
https://github.com/rtcatc/Packer-Fuzzer

这里packerfuzzer需要nodejs环境,并且运行的时候如果报错

1
ImportError: cannot import name 'OxmlElement' from 'docx.oxml.xmlchemy' (C:\Users\Radish\AppData\Local\Programs\Python\Python38\lib\site-packages\docx\oxml\xmlchemy.py)

需要指定安装

1
pip install python-docx==0.8.11

packerfuzzer会把网站的js都下载下来,然后可以处理js文件,提取其中的uri

1
python PackerFuzzer.py -u url -f 1

正则表达式匹配字符串就行,也就是匹配被单引号或者双引号包裹起来的字符串,如果字符串被/分割,则将其提取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import json
import re
import requests
import sys
import os

fileurl=sys.argv[1]

filemkdir=fileurl.split('_')[0]
if not os.path.exists(filemkdir):
os.makedirs(filemkdir)


#get path + 路径名称
paths=[]
for dirpath, dirnames, filenames in os.walk('./'+filemkdir):
for file in filenames:
with open("./"+filemkdir+"/"+file,"r",encoding='gb18030', errors='ignore') as f2:
try:
line=f2.readlines()
for line in line:
line=line.strip('\n').strip('\t')
#print(line)
p = re.findall('''(['"]\/[^][^>< \)\(\{\}]*?['"])''',line)
#print(p)
if p != None:
#print(p)
for path in p:
path=path.replace(':"',"").replace('"',"")
paths.append(file+"---"+path)
except Exception as e:
print(e)


for var in sorted(set(paths)):
with open (fileurl+'_path.txt',"a+",encoding='gb18030', errors='ignore') as paths:
paths.write(var+'\n')

但是packetfuzzer并不是万能的,有时候会下载下来一些.db文件,需要结合数据库工具来查看

findsomething和Unexpected.information这类插件也需要结合着用,有些网站直接查看其前端并没有找到任何和app.js(app.xxxxxxxx.js这种格式) index.js main.js等比较经典的和webpack相关的文件,然而查看burp的history,却发现该网站加载了app.js,并且unexpected information插件成功在其中匹配到了各种信息

目录FUZZ结合接口测试

第一,提取了形如http://test.test.com/api/test/admin/delete 这种接口,那么就有理由测试其他的添加、修改等功能的接口

1
2
3
4
5
6
7
8
查询(获取信息)
search list select query get find
删除(删除某个数据)
del Delete
编辑(更新某个信息)
Update Up edit Change
添加(增加某个信息)
add create new

第二,如果提取了形如http://test.test.com/api/test/user/add 这种接口,那么就有理由测试/admin/add这种接口

1
2
3
http://test.hack.com/api/test/FUZZ/add
或者
http://test.hack.com/api/test/adminEdit

多观察接口,推测其功能,然后根据功能去FUZZ,毕竟要实现一个web功能,基本都要有对应的增删改查接口

推荐FFUF这款工具

从接口提取到定位后端地址

第一种方式简单来说就是猜,简单来说你一个网站,页面上肯定有很多功能,以登录场景为例,肯定是有一个登录的功能嘛。那我们可以抓取登录的数据包,观察其接口的uri规则,而且在你访问某个网站的时候,很多时候会先访问一些接口来完成初始化,或者获取你的某些状态,在没有登录点的情况下,通过这些接口也是可以的。

比方说我们调用http://test.com/#/login的登录点,这时候抓包访问到了/vsk/virsical-auth/oauth/token这个uri

这里我们引入一个“初级目录”的概念,打引号是因为绝大多数情况下这并不是真正的目录。

什么意思呢?我们以Spring开发为例,对SpringSSM比较熟悉的小伙伴应该知道,我们可以在配置文件里添加一个配置项:

1
server.servlet.context-path

比方说

1
server.servlet.context-path=/vsk

这样,你的web应用的所有uri前面都要加一层/test1,比方说我给登录的动作配置一个RequestMapping,为

1
@RequestMapping(“/virsical-auth/oauth/token”);

那么如果我想通过WEB访问这个动作,应该访问的uri应该是:

1
/vsk/virsical-auth/oauth/token

通过上面这个例子你想到了什么?没错,我们上面抓到的那个数据包,有可能是以/vsk作为初始路径。然后我们通过前文的提取接口相关的技术找到的接口uri可能是/virsical-auth/oauth/token,这样我们就知道怎么拼接了,就可以得到/vsk/virsical-auth/oauth/token。

当然实战中绝大部分都是黑盒的情况,你通过现有功能看到这么一个uri,其实也不一定能百分百判断到底哪个是初级uri,比方说上述情况。有可能是/vsk作为初级uri,亦可能是/vsk/virsical-auth/作为初级uri,这个需要靠经验来判断。如果你怕判断有误,最好的办法就是多拼接几次,比如:

1
2
3
http://test.com/vsk/ <---接口依次拼接在这后面
http://test.com/vsk/virsical-auth/ <---接口依次拼接在这后面
http://test.com/vsk/virsical-auth/oauth/ <---接口依次拼接在这后面

以此类推,不过一般比较少出现这种情况。上述是“初级目录”的情况,还有一种情况则是前后端分离。这个也很好理解。比方说你访问的网站是:

http://admin.test.com/#/login

然而负责提供服务的后端地址可能是:

1
2
http://api.admin.test.com/api/admin/login
http://admin.test.com:8080/api/admin/login

这些抓包都很容易看出来

但是还有一些情况。

情况一:某个后台,访问即重定向到SSO登录,能加载出JS,但是在这个过程中不会访问多余的接口,也没有登录之类的现成功能点给我们调用。这样我们没法通过现有功能去获取到接口uri,这对我们判断“初级路径”就会造成很大阻碍,更何况如果目标采用前后端分离,我们都不知道后端在哪里,又该怎么拼接接口测试呢?

情况二:还是某个后台,假如开发的思维非常严谨,给每个Controller都设置了一个RequestMapping,比方说登录相关功能可能放在/login/这个uri下,普通用户的功能放在/user/,管理员的功能放在/admin/。也就是说,我们至少有可能存在三种“初级路径”,在这种情况下,即使我们有一个登录的现有功能,我们也只是获取到了/login/这个初级路径,而我们提取的接口uri肯定绝大部分都是分布在/user/和/admin/两个路径下,在这种情况下,实际上也相当于我们忽略掉了绝大部分的接口。

那么这又该如何破局呢?其实也非常ez,因为他这些后端地址包括“初级路径”,肯定不是无中生有搞出来的,这些东西肯定都在JS里。比方说上面的那个多个初级路径的情况,在JS中大概率存放了一些地址:

1
2
3
http://api.test.com/login/
http://api.test.com/user/
http://api.test.com/admin/

当用户访问对应功能,比方说普通用户修改自己的密码,前端会取出:

1
/userApi/changePwd

然后和对应的地址做拼接:

1
http://api.test.com/user/userApi/changePwd

然后发起一次请求。

因此我们上述提取接口,实际上就是提取了所有类似/userApi/changePwd这样的uri。我们要找后端也很简单,写个正则专门匹配http://或https://开头的字符串即可(Unexpected information和findsomething也可以实现一部分这样的功能)

提取出来之后,后续要做的就是把后端字典和接口字典拼接起来

1
2
3
4
后端字典 接口字典
http://api1.test.com/ /user/getinfo
http://api2.test.com/api/admin/ /user/register
http://api2.test.com/api/ /manager/changeSetting

把列表丢到HTTPX之类的工具跑一遍

FUZZ初级目录

有些开发在写后端的时候,喜欢加一些用于测试的接口和页面,这些接口肯定不会放在js里,并且其uri和js中的正常接口可能截然不同。反正大部分情况下初级路径是不同的。因此有时候习惯性对后端地址FUZZ一下初级路径,会有很大收获。

当然这里我们其实还可以拓展,那就是既然每一级目录下都可能有一些新东西,为什么我们不可以写一个插件去递归测试呢?比如java中常见的swaggerui、api-docs接口文档,再比如actuator端点泄露,druid未授权等问题,关于这块其实早就有很多很好用的插件了,例如Tsojanscan、APIKIT等等

从js中搜寻凭据信息

首先我们很容易想到一些要素,比如手机号、邮箱、身份证,因为很多提取JS并分析的场景都是那种后台登录点,提取这些信息,我们可能可以找到潜藏的用户名,或者我们可以通过这些信息去进行密码重置等操作,再或者结合裤子进一步查找信息。在部分场景下,甚至只能通过邮箱或手机号作为用户名登录。因此搜集这些实际上是为了后续的密码喷洒、爆破相关攻击路线服务的。给出提取正则(python)如下:

1
2
3
4
5
6
#邮箱匹配
matches = re.findall(r'[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}', line)
#手机号匹配
matches = re.findall(r'(?<!\d)(13\d{9}|14[579]\d{8}|15[^4\D]\d{8}|166\d{8}|17[^49\D]\d{8}|18\d{9}|19[189]\d{8})(?!\d)', line)
#身份证匹配
matches = re.findall(r'\b\d{17}[\dXx]|\b\d{14}\d{1}|\b\d{17}[\dXx]', line)

顺这个思路想,我们在密码喷洒、爆破流程中常见验证码以及一些IP限制,在部分情况下,XFF伪造一个内部IP会是绕过方法,如果目标的鉴权机制检测到特定内部IP就直接放行,那就更爽了,因此我们还可以写个正则匹配IP:

1
2
#ip匹配
matches = re.findall(r'\d+\.\d+\.\d+\.\d+', line)

继续思考,既然能找到账号,我们有没有可能找到密码呢?嗯,这个难度就大了,一般只能通过js中变量的赋值来找到蛛丝马迹,我们可以用正则匹配

1
2
Password =
Password :

这样的关键字,从而找到一些密码的蛛丝马迹,通过这种思路,其实也可以去找AK/SK,两个相关的正则如下:

1
2
3
4
#密码匹配小正则
matches = re.findall(r'(?:^|_)((?:username|password|key|auv)_)\s*[:=><]*\s*["\']([^"\']+)["\']', line)
#匹配信息大正则
matches = re.findall(r'(?i)((access_key|username|user|jwtkey|jwt_key|AESKEY|AES_KEY|appsecret|app_secret|access_token|password|admin_pass|admin_user|algolia_admin_key|algolia_api_key|alias_pass|alicloud_access_key|amazon_secret_access_key|amazonaws|ansible_vault_password|phone|aos_key|api_key|api_key_secret|api_key_sid|api_secret|api\.googlemaps\s+AIza|apidocs|apikey|apiSecret|app_debug|app_id|app_key|app_log_level|app_secret|appkey|appkeysecret|application_key|appspot|auth_token|authorizationToken|authsecret|aws_access|aws_access_key_id|aws_bucket|aws_key|aws_secret|aws_secret_key|aws_token|AWSSecretKey|b2_app_key|bashrc\ password|bintray_apikey|bintray_gpg_password|bintray_key|bintraykey|bluemix_api_key|bluemix_pass|browserstack_access_key|bucket_password|bucketeer_aws_access_key_id|bucketeer_aws_secret_access_key|built_branch_deploy_key|bx_password|cache_driver|cache_s3_secret_key|cattle_access_key|cattle_secret_key|certificate_password|ci_deploy_password|client_secret|client_zpk_secret_key|clojars_password|cloud_api_key|cloud_watch_aws_access_key|cloudant_password|cloudflare_api_key|cloudflare_auth_key|cloudinary_api_secret|cloudinary_name|codecov_token|config|conn\.login|connectionstring|consumer_key|consumer_secret|credentials|cypress_record_key|database_password|database_schema_test|datadog_api_key|datadog_app_key|db_password|db_server|db_username|dbpasswd|dbpassword|dbuser|deploy_password|digitalocean_ssh_key_body|digitalocean_ssh_key_ids|docker_hub_password|docker_key|docker_pass|docker_passwd|docker_password|dockerhub_password|dockerhubpassword|dot-files|dotfiles|droplet_travis_password|dynamoaccesskeyid|dynamosecretaccesskey|elastica_host|elastica_port|elasticsearch_password|encryption_key|encryption_password|env\.heroku_api_key|env\.sonatype_password|eureka\.awssecretkey)\s*[:=><]{1,2}\s*[\"\']{0,1}([0-9a-zA-Z\-_=+/]{8,64})[\"\']{0,1})', line)

如果你有其它想匹配的关键字,自己往上述正则中添加即可。

前面我们提了一嘴AK/SK,从JS中找这玩意真是典中典了,尤其是那些把文件传储存桶里的功能点,要尤其注意其是否把AK/SK硬编码在前端了。SK一般是无特征无规律可循的,不过AK是有规律的,AK的前四位一般是固定的,不同的云厂商则不同,详情可以参考这篇文章:

1
https://wiki.teamssix.com/cloudservice/more/

依据这篇文章,我们实际上可以写出一些提取AK的正则,并且SK一般都会和AK写在一起,因此只要在JS中定位到AK,那么离找到SK也不远了,正则如下:

1
2
3
4
5
6
7
8
9
10
11
12
#常见的云AK匹配
matches = re.findall(r'''(['"]\s*(?:GOOG[\w\W]{10,30}|AZ[A-Za-z0-9]{34,40}|AKID[A-Za-z0-9]{13,20}|AKIA[A-Za-z0-9]{16}|IBM[A-Za-z0-9]{10,40}|OCID[A-Za-z0-9]{10,40}|LTAI[A-Za-z0-9]{12,20}|AK[\w\W]{10,62}|AK[A-Za-z0-9]{10,40}|AK[A-Za-z0-9]{10,40}|UC[A-Za-z0-9]{10,40}|QY[A-Za-z0-9]{10,40}|KS3[A-Za-z0-9]{10,40}|LTC[A-Za-z0-9]{10,60}|YD[A-Za-z0-9]{10,60}|CTC[A-Za-z0-9]{10,60}|YYT[A-Za-z0-9]{10,60}|YY[A-Za-z0-9]{10,40}|CI[A-Za-z0-9]{10,40}|gcore[A-Za-z0-9]{10,30})\s*['"])''', line)
#谷歌云 AccessKey ID匹配
matches = re.findall(r'\bAIza[0-9A-Za-z_\-]{35}\b', line)
#金山云 AccessKey ID匹配
matches = re.findall(r'\bAKLT[a-zA-Z0-9-_]{16,28}\b', line)
#火山引擎 AccessKey ID匹配
matches = re.findall(r'\b(?:AKLT|AKTP)[a-zA-Z0-9]{35,50}\b', line)
#亚马逊 AccessKey ID匹配
matches = re.findall(r'["''](?:A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}["'']', line)
#京东云 AccessKey ID匹配
matches = re.findall(r'\bJDC_[0-9A-Z]{25,40}\b', line)

再接下来,我们还能想到什么呢?前面我们提到了一些明文的鉴权因素,比如用户名和密码。然而实际情况下我们经常遇到别的一些鉴权因素,比如JWT,再比如形如”Basic 一串base64编码”这样格式的鉴权凭据,常见于请求头中。开发者有没有可能在测试前端的时候,在前端放了一些测试用户的鉴权凭据,上线后忘记删除呢?当然也是有可能的

这里也给出几个相关正则:

1
2
3
4
5
6
7
8
9
10
#JWT Token匹配
matches = re.findall(r'eyJ[A-Za-z0-9_/+\-]{10,}={0,2}\.[A-Za-z0-9_/+\-\\]{15,}={0,2}\.[A-Za-z0-9_/+\-\\]{10,}={0,2}', line)
#PRIVATE KEY匹配
matches = re.findall(r'-----\s*?BEGIN[ A-Z0-9_-]*?PRIVATE KEY\s*?-----[a-zA-Z0-9\/\n\r=+]*-----\s*?END[ A-Z0-9_-]*? PRIVATE KEY\s*?-----', line)
#Auth Token匹配
matches = re.findall(r'["''\[]*[Aa]uthorization["''\]]*\s*[:=]\s*[''"]?\b(?:[Tt]oken\s+)?[a-zA-Z0-9\-_+/]{20,500}[''"]?', line)
#Basic Token匹配
matches = re.findall(r'\b[Bb]asic\s+[A-Za-z0-9+/]{18,}={0,2}\b', line)
#Bearer Token匹配
matches = re.findall(r'\b[Bb]earer\s+[a-zA-Z0-9\-=._+/\\]{20,500}\b', line)

然后说到上述的Token和key,还有一种情况相信各位师傅也经常见到,那就是硬编码在JS里的钉钉、飞书、企微、微信小程序/公众号之类的key和token,能搞到这些东西并且能利用也大概率能搞个高危,下面也给出部分规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#slack webhook匹配
matches = re.findall(r'\bhttps://hooks.slack.com/services/[a-zA-Z0-9\-_]{6,12}/[a-zA-Z0-9\-_]{6,12}/[a-zA-Z0-9\-_]{15,24}\b', line)
#飞书 webhook匹配
matches = re.findall(r'\bhttps://open.feishu.cn/open-apis/bot/v2/hook/[a-z0-9\-]{25,50}\b', line)
#钉钉 webhook匹配
matches = re.findall(r'\bhttps://oapi.dingtalk.com/robot/send\?access_token=[a-z0-9]{50,80}\b', line)
#企业微信 webhook匹配
matches = re.findall(r'\bhttps://qyapi.weixin.qq.com/cgi-bin/webhook/send\?key=[a-zA-Z0-9\-]{25,50}\b', line)
#微信公众号匹配
matches = re.findall(r'["''](gh_[a-z0-9]{11,13})["'']', line)
#企业微信 corpid匹配
matches = re.findall(r'["''](ww[a-z0-9]{15,18})["'']', line)
#微信 公众号/小程序 APPID匹配
matches = re.findall(r'["''](wx[a-z0-9]{15,18})["'']', line)
#腾讯云 API网关 APPKEY匹配
matches = re.findall(r'\bAPID[a-zA-Z0-9]{32,42}\b', line)

除此之外还有一些乱七八糟的应用的Token:

1
2
3
4
5
6
7
8
9
10
11
12
#grafana service account token匹配1
matches = re.findall(r'\b(?:VUE|APP|REACT)_[A-Z_0-9]{1,15}_(?:KEY|PASS|PASSWORD|TOKEN|APIKEY)[\'"]*[:=]"(?:[A-Za-z0-9_\-]{15,50}|[a-z0-9/+]{50,100}==?)"', line)
#grafana service account token匹配2
matches = re.findall(r'\bglsa_[A-Za-z0-9]{32}_[A-Fa-f0-9]{8}\b', line)
#grafana cloud api token匹配
matches = re.findall(r'\bglc_[A-Za-z0-9\-_+/]{32,200}={0,2}\b', line)
#grafana api key匹配
matches = re.findall(r'\beyJrIjoi[a-zA-Z0-9\-_+/]{50,100}={0,2}\b', line)
#Github Token匹配
matches = re.findall(r'\b((?:ghp|gho|ghu|ghs|ghr|github_pat)_[a-zA-Z0-9_]{36,255})\b', line)
#Gitlab V2 Token匹配
matches = re.findall(r'\b(glpat-[a-zA-Z0-9\-=_]{20,22})\b', line)

把上面的所有正则综合起来,就能得到一个比较好用的敏感信息提取脚本了

提取注释和汉字

从JS和HTML中提取注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
match = re.search(r'\*/(.*)', line)
matches = re.findall(r'//[^\n]*', line)
match = re.search(r'/\*(.*)', line)
matches = re.findall(r'<!--(?:.|\n)*?-->', line)
https://github.com/rtcatc/Packer-Fuzzer

https://github.com/momosecurity/FindSomething

https://github.com/ScriptKid-Beta/Unexpected_information

https://github.com/gh0stkey/HaE

https://github.com/Tsojan/TsojanScan

https://github.com/API-Security/APIKit

定位Next.js网站所有接口的方法

直接控制台输入

1
console.log(__BUILD_MANIFEST.sortedPages)

或者firefox新建一个标签页,网址填

1
javascript:console.log(__BUILD_MANIFEST.sortedPages.join('\n'));

遇到Next.js网站时,点击这个标签就能在控制台看到所有路径