通过JS文件进行API接口未授权手工测试上

本文最后更新于 2024年10月22日 晚上

前言:对JS中泄露的API接口进行接口未授权测试一直是渗透测试的重点,于是尽可能找到更多的API接口就显得很重要,更重要的是还需要找到怎么拼接接口,怎么精准的找到API接口对应的参数,本文对此进行了简单的总结,一起来看一下吧

API接口手工测试,我认为分为四步:

  1. 提取API接口
  2. 拼接API接口发包
  3. 寻找API接口缺失参数
  4. 拼接再次发包

提取API接口

FindSomeThing

最直接的就是使用FindSomeThong插件

image

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
/*
/./
/=
/?
/a/i
/auth/shopCart/myShopCart.html
/banner
/core-js/
/core-js/get-iterator
/core-js/is-iterable
/dist/
/goods/info_11c0.html
/goods/info_12c0.html
/goods/info_14c0.html
/goods/info_1c0.html
/goods/search/k
/helpers/
/lib
/node_modules/regenerator
/regenerator
/tmp
/user/login/doLogin
/user/login/login.html?backurl=
/user/password/findPassword.html
/user/personCenter/getCompanyType/
/user/register/register.html

但是如果我们担心还是API接口搜集不全,可以再试试其他工具,看看效果

URLFinder

使用URLFinder爬取数据

image

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
/user/register/register.html
/auth/shopCart/myShopCart.html
/goods/info_1c0.html
/goods/info_11c0.html
/goods/info_12c0.html
/goods/info_14c0.html
/banner
/user/login/login.html?backurl=
/user/password/findPassword.html
/user/login/smsLogin.html
/resources/font/iconfont.woff2?t=1657336268185
/resources/font/iconfont.woff?t=1657336268185
/resources/font/iconfont.ttf?t=1657336268185
/user/login/login.html
/user/register/registerSuccess.html
/goods/search/k_b_t1_t2_t3_p1_c0.html
:8281/manage/index?type=buyOrder
:8281/manage/index
/text/ecmascript-6
/text/6to5
/transformation/templates
/array/concat
/array/copy-within
/array/entries
/array/every
/array/fill
/array/filter
/array/find-index
/array/find
/array/for-each
/array/from
/array/includes
/array/index-of
/array/join
/array/keys
/array/last-index-of
/array/map
/array/of
/array/pop
/array/push
/array/reduce-right
/array/reduce
/array/reverse
/array/shift
/array/slice
/array/some
/array/sort
/array/splice
/array/turn
/array/unshift
/array/values
/object/assign
/object/classof
/object/create
/object/define
/object/define-properties
/object/define-property
/object/entries
/object/freeze
/object/get-own-property-descriptor
/object/get-own-property-descriptors
/object/get-own-property-names
/object/get-own-property-symbols
/object/get-prototype-of
/object/index
/object/is-extensible
/object/is-frozen
/object/is-object
/object/is-sealed
/object/is
/object/keys
/object/make
/object/prevent-extensions
/object/seal
/object/set-prototype-of
/object/values
/regexp/escape
/function/only
/function/part
/math/acosh
/math/asinh
/math/atanh
/math/cbrt
/math/clz32
/math/cosh
/math/expm1
/math/fround
/math/hypot
/math/pot
/math/imul
/math/log10
/math/log1p
/math/log2
/math/sign
/math/sinh
/math/tanh
/math/trunc
/date/add-locale
/date/format-utc
/date/format
/symbol/for
/symbol/has-instance
/symbol/is-concat-spreadable
/symbol/iterator
/symbol/key-for
/symbol/match
/symbol/replace
/symbol/search
/symbol/species
/symbol/split
/symbol/to-primitive
/symbol/to-string-tag
/symbol/unscopables
/string/at
/string/code-point-at
/string/ends-with
/string/escape-html
/string/from-code-point
/string/includes
/string/raw
/string/repeat
/string/starts-with
/string/unescape-html
/number/epsilon
/number/is-finite
/number/is-integer
/number/is-nan
/number/is-safe-integer
/number/max-safe-integer
/number/min-safe-integer
/number/parse-float
/number/parse-int
/number/random
/reflect/apply
/reflect/construct
/reflect/define-property
/reflect/delete-property
/reflect/enumerate
/reflect/get-own-property-descriptor
/reflect/get-prototype-of
/reflect/get
/reflect/has
/reflect/is-extensible
/reflect/own-keys
/reflect/prevent-extensions
/reflect/set-prototype-of
/reflect/set
/r:Math.ceil(e/t)
/babel/babel
/text/babel
/item.outerChain
/erp/pexetech/moldji.html
/erp/cn/index.xhtml
/erp/pexetech/project/bx_app.xhtml
/user/register/text/css
/user/register/text/javascript
/a/i
/text/javascript
/goods/more_
/goods/text/css
/goods/text/javascript
/sso/text/css
/sso/text/javascript
/sso/result.data
/goods/info_
/goods/url
/dist/
/M/D/yy
/this.gutter/2
/:this.space?this.space:100/(t-(this.isCenter?0:1))
/n/100
/)/100
/tmp
/modules/layer/
/text/xml
/text/plain
/text/html
/lib
/helpers/
/regenerator
/core-js/
/core-js/get-iterator
/core-js/is-iterable
/node_modules/regenerator
/goods/search/k
/url
/user/login/login
/user/personCenter/getCompanyType/
/auth/shopCart/text/css
/auth/shopCart/text/javascript
/auth/shopCart/result.data
/text/css
/video/mp4
/user/register/checkMobile
/goods/getOneModel?modelId=
/user/register/doRegister
/user/login/doLogin
/auth/shopCart/addShopCartList
/auth/shopCart/addShopCart
/user/register/checkUserName
/user/register/checkEmail
/user/register/sendSms

可以看到爬取到了非常多的API接口,工具是相当不错的,但是有些情况下,由于js的嵌入方式的特殊性,或者是动态加载的等等,无法很好的爬取数据,反正为了获取到尽可能多的API,可以几个工具多用用,再合并去重就行了

Burp+自写脚本

如果还是担心遗漏了什么API接口,还可以试试使用burp爬取网页,批量下载js文件到本地 (如果是使用了webpack打包的网站,在webpack存在配置缺陷时,可以还原压缩前的js代码,先使用工具还原出被打包的js代码并下载,再进行API接口提取),再使用自写的脚本,批量提取文件夹中文件的API路径

  • 使用burp爬取目标的js文件,一键下载到本地

image

image

如下勾选,添加来自于哪一个url,便于后续查找,导出文件

image

导出后,有一个小问题,就是它所有来自于不同js文件的js代码,都导出在了同一个文件中,只是在代码开头有url标识以下代码来自于哪一个js文件

image

image

这样的话,后面使用自写的脚本来提取API接口,提取到的API接口就不知道到底来自于哪一个js文件,因为这里burp把所有js文件代码导出在了同一个文件中,为什么我们需要知道提取到的API来自于哪一个js文件呢,因为后面API接口如果缺失参数,或者需要拼接路径,我们可能要回到API接口所在的js文件中去寻找信息,分析js代码,于是知道API接口来自于哪一个js文件很重要,那么我们需要一个脚本来把这些合并在一起的js文件代码区分开来,分别放在不同的js文件中,并使用原js文件名命名

这里是脚本,apart.py,接收input.txt(多个js文件合并在一起的代码文本),独立出js文件,使用标识url中提取的js文件名命名

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
import re
import os

def extract_js_files(input_file):
# 定义正则表达式模式来匹配 URL 和 JS 代码
pattern = re.compile(r'(http[^\s]+\.js)\s*\n\n(.*?)\n(?=http|$)', re.DOTALL)

# 打开输入文件并读取内容
with open(input_file, 'r', encoding='utf-8') as file:
content = file.read()

# 查找所有匹配的 URL 和 JS 代码块
matches = pattern.findall(content)

if not matches:
print("No JS code blocks found.")
return

# 创建输出目录
output_dir = 'extracted_js'
os.makedirs(output_dir, exist_ok=True)

for url, js_code in matches:
# 提取文件名
file_name = url.split('/')[-1]
file_path = os.path.join(output_dir, file_name)

# 保存 JS 代码到独立的文件
with open(file_path, 'w', encoding='utf-8') as js_file:
js_file.write(js_code.strip())

print(f"Extracted {file_name} to {file_path}")

if __name__ == '__main__':
input_file = 'input.txt' # 这里替换成你的输入文件名
extract_js_files(input_file)

image

image

image

这样就独立出来了

使用脚本递归提取文件夹中js文件的API接口,脚本findapi.py

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import json
import re
import requests
import sys
import os

headers = {
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36"}

fileurl=sys.argv[1]

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


# 下载chunk.js
# with open (str(fileurl)) as furl:
# url=furl.readlines()
# print(str(url)+"---is---downloading")
# for url in url:
# url=url.strip('\n')
# file=url.split('/')[-1]

# resp = requests.get(url)
# html = resp.text

# with open ("./"+filemkdir+"/"+file,"a",encoding="utf-8") as f1:
# f1.write(html)



#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')

刚才的目标收集到的接口不多,我换了一个目标来演示效果,这是提取的API接口,并标注了来自于哪一个js文件

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
browser-polyfill.min.js---/./
browser-polyfill.min.js---/a/i
browser.min.js---'/,
browser.min.js---'/g,
browser.min.js---/
browser.min.js---/*
browser.min.js---/./
browser.min.js---//
browser.min.js---/=
browser.min.js---/?
browser.min.js---/a/i
browser.min.js---/core-js/
browser.min.js---/core-js/get-iterator
browser.min.js---/core-js/is-iterable
browser.min.js---/g,'
browser.min.js---/helpers/
browser.min.js---/lib
browser.min.js---/node_modules/regenerator
browser.min.js---/regenerator
browser.min.js---/tmp
index.js---'/g,o=
index.js---/
index.js---/dist/
jquery.min.js---/
jquery.min.js---//
layer.js---/
app.5250ab35.js---/
app.5250ab35.js---/api/blade-workbench
app.5250ab35.js---/api/blade-workbench/nobody/weixin/getRedirectUrl?href=
app.5250ab35.js---/api/blade-workbench/userThirdLogin/auth/
app.5250ab35.js---/auth/auth/init
app.5250ab35.js---/auth/auth/login
app.5250ab35.js---/auth/bindPhoneByCode
app.5250ab35.js---/auth/bindPhoneByPassword
app.5250ab35.js---/auth/createQrCode
app.5250ab35.js---/auth/getQrCode
app.5250ab35.js---/auth/h5/forgetPwd
app.5250ab35.js---/auth/h5/init
app.5250ab35.js---/auth/loginByPhone
app.5250ab35.js---/auth/refreshImageCode
app.5250ab35.js---/auth/sendBindMessageCode
app.5250ab35.js---/auth/sendMessage
app.5250ab35.js---/auth/tokenLogin
app.5250ab35.js---/eduForgetPassword/checkInfo
app.5250ab35.js---/eduForgetPassword/getCheckCode
app.5250ab35.js---/eduForgetPassword/getPassword
app.5250ab35.js---/eduForgetPassword/getQuestion
app.5250ab35.js---/eduForgetPassword/updatePassword
app.5250ab35.js---/getTokenByCasTicket
app.5250ab35.js---/licenseInvalid
app.5250ab35.js---/nobody/cas/getTokenByCasTicket
app.5250ab35.js---/nobody/index/license
app.5250ab35.js---/nobody/weixin/combLogin
app.5250ab35.js---/nobody/weixin/getWxLoginStatus
app.5250ab35.js---/nobody/weixin/login
app.5250ab35.js---/relayPage
app.5250ab35.js---/relayWxPage
app.5250ab35.js---/sForget
app.5250ab35.js---/sLogin
app.5250ab35.js---/userThirdLogin/auth/bindThird
chunk-vendors.55a4f46a.js---'/g,s=
chunk-vendors.55a4f46a.js---/
chunk-vendors.55a4f46a.js---/#
chunk-vendors.55a4f46a.js---/./
chunk-vendors.55a4f46a.js---//
chunk-vendors.55a4f46a.js---/a/b
chunk-vendors.55a4f46a.js---/a/i
chunk-vendors.55a4f46a.js---/dist/
chunk-vendors.55a4f46a.js---/errorCorrectLevel

批量测试接口的时候,有前面的js文件名和”—“也不用担心,直接在记事本中批量替换为空就可以了

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/./
/a/i
'/,
'/g,
/
/*
/./
//
/=
/?
/a/i
/core-js/
/core-js/get-iterator
/core-js/is-iterable
/g,'
/helpers/
/lib
/node_modules/regenerator
/regenerator
/tmp
index.js---'/g,o=
index.js---/
index.js---/dist/
jquery.min.js---/
jquery.min.js---//
layer.js---/
/
/api/blade-workbench
/api/blade-workbench/nobody/weixin/getRedirectUrl?href=
/api/blade-workbench/userThirdLogin/auth/
/auth/auth/init
/auth/auth/login
/auth/bindPhoneByCode
/auth/bindPhoneByPassword
/auth/createQrCode
/auth/getQrCode
/auth/h5/forgetPwd
/auth/h5/init
/auth/loginByPhone
/auth/refreshImageCode
/auth/sendBindMessageCode
/auth/sendMessage
/auth/tokenLogin
/eduForgetPassword/checkInfo
/eduForgetPassword/getCheckCode
/eduForgetPassword/getPassword
/eduForgetPassword/getQuestion
/eduForgetPassword/updatePassword
/getTokenByCasTicket
/licenseInvalid
/nobody/cas/getTokenByCasTicket
/nobody/index/license
/nobody/weixin/combLogin
/nobody/weixin/getWxLoginStatus
/nobody/weixin/login
/relayPage
/relayWxPage
/sForget
/sLogin
/userThirdLogin/auth/bindThird
'/g,s=
/
/#
/./
//
/a/b
/a/i
/dist/
/errorCorrectLevel

拼接API接口发包

把API接口保存为字典,使用burp的intruder模块批量测试,先试试直接把接口拼接到根目录下,看看有多少成功的接口

image

image

image

可以看到有一个200的接口,说明这个接口可能成功了,有一个401的接口,这就是有鉴权的接口,多半不用看了,剩下的全是404的接口,可能会有师傅认为是这些接口被弃用了,于是到这里就放弃了,没有继续测试了。但其实,这些接口都是使用脚本的正则表达式从js文件中提取出来的,大都应该是存在的,有一种可能就是这些接口有一个base路径,也就是需要拼接上这个base路径,才是正确的路径

接下来就是去js文件中去找这个base路径:以/auth/auth/init 为例

直接F12,ctrl+shift+F,全局搜索 /auth/auth/init 接口,定位到js源码

image

可以看到这三个接口,都使用concat(),拼接了一个”i”,那么我们去找到这个”i”是什么,可能就能找到base路径了

image

怎么去找”i”呢?我们根据基础的变量的定义与调用可以知道,这里既然调用了”i”并使用concat拼接,那么”i”肯定已经被定义了,而且是在调用之前就定义好了,于是我们就在这个接口处,一直往前翻找看看”i”被定义成了啥

果不其然,往前翻了一会就找到了,根本不需要太懂js代码

image

于是base路径找到了,为 /api/blade-workbench

还有一种方式也可以试试,就是F12,网络,刷新,查看加载的XHR

image

有些时候这样也能找到base路径,要看它加不加载咯

找到base路径后,再拼接新的API接口

image

再次burp发包:

image

可以看到,状态码发送了很大的改变,说明我们的base路径找对了

401,接口鉴权了,可以先跳过,后面再看

405,请求方式不对,修改为POST,burp再次发包

image

可以看到,状态码又发生了很大的改变,多出来许多200的接口

image

那么当然是优先去测试这些200的接口,这要么说明这是公开的接口,要么存在接口未授权的问题,漏洞不就来了吗

在众多200接口中筛选一下比较重要的接口,找到一个重置密码的接口,发到重放模块:

image

提示重置密码失败,那么不用想,下一步就是去寻找API对应的参数,怎么找API的参数呢?

找之前,先学习一下找API的参数的几种情况与技巧

寻找API接口缺失参数

其实有些接口是可以直接查看到使用参数的,比如有些显式的功能点,重置密码,登录,等等这些接口,只要你抓包就可以直接看到其使用的参数,因为这些是比较明显的功能点的后端接口,这类接口本来就是公开的,是功能点处的逻辑处理接口,也就不存在什么未授权的说法了,因为这类接口本来就是不继续鉴权的公共接口,可以直接查看到其使用的参数,于是可能不会考虑去测试未授权的漏洞,但是不排除有其他逻辑缺失漏洞,比如可以测试水平越权与垂直越权,重放攻击等等……于是我把这种公开的,参数可直接看到的接口称为显式接口,把需要进行鉴权,比较隐蔽,参数不好寻找的接口称为隐式接口,这种接口才是未授权的重点测试对象

显式接口

比如这个重置密码的接口就是显式接口:

image

可以直接抓重置密码的数据包,就可以查看到参数了,不是未授权接口测试的重点

还有登录接口

等等其他显式的功能点处调用的后端逻辑处理接口都是显式接口,参数可直接获取

image

隐式接口

上面说了,这些接口的参数不太好直接获取,就是不能一眼找出触发它的功能点的,进行了严格鉴权的那种接口,有点抽象,比如我们以普通权限的身份登录,但是通过js文件和脚本提取到了需要管理员权限的API接口,这种接口我们是无法使用功能点去触发的,因为我们不是管理员,压根看不到对应的功能点,于是参数的获取就不再是抓包这么简单了,这就是我自称的隐式接口

怎么去寻找这些接口的参数呢?

接口参数在接口附近

注意观察接口的上下文,附近处,看看能不能找到参数

案例一

image

postData接收接口与”g”,在前面可以看到”g”的定义,就是三个参数

案例二

image

使用了axios去发起请求,参数是”name”

案例三

image

使用concat拼接到URL中,参数是error和callbackUrl

案例四

image

使用ajax请求,参数是objData对象,还需要寻找objData对象是如何被定义的,往前翻一点,就找到了

image

接口参数离接口较远

案例二

image

url,请求方式,参数名写在了一起,参数名看似是”data”,其实不是,因为可以看到许多接口都有这个”data”,如果”data”是参数,那为什么多个不同接口的参数一样?所以”data”其实是对参数进行了打包处理的对象,不是具体的参数本身

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
"1fb7": function(e, t, n) {
"use strict";
n.d(t, "e", (function() {
return o
}
)),
n.d(t, "a", (function() {
return i
}
)),
n.d(t, "d", (function() {
return s
}
)),
n.d(t, "g", (function() {
return c
}
)),
n.d(t, "f", (function() {
return u
}
)),
n.d(t, "b", (function() {
return l
}
)),
n.d(t, "c", (function() {
return d
}
));
var r = n("4020")
, a = "/api/blade-workbench";
function o(e) {
return Object(r.a)({
url: "".concat(a, "/eduForgetPassword/getCheckCode"),
method: "post",
data: e
})
}
function i(e) {
return Object(r.a)({
url: "".concat(a, "/eduForgetPassword/checkInfo"),
method: "post",
data: e
})
}
function s() {
return Object(r.a)({
url: "".concat(a, "/eduForgetPassword/getQuestion"),
method: "get"
})
}
function c(e) {
return Object(r.a)({
url: "".concat(a, "/eduForgetPassword/updatePassword"),
method: "post",
data: e
})
}
function u(e) {
return Object(r.a)({
url: "".concat(a, "/auth/refreshImageCode"),
method: "post",
data: e
})
}
function l(e) {
return Object(r.a)({
url: "".concat(a, "/auth/h5/forgetPwd"),
method: "post",
data: e
})
}
function d(e) {
return Object(r.a)({
url: "".concat(a, "/eduForgetPassword/getPassword"),
method: "post",
data: e
})
}
}

以寻找”/api/blade-workbench/eduForgetPassword/getPassword”接口的参数为例:

全局搜索”getPassword(“

image

直接就找到了

同样的方法,找到了resetpasswd的接口参数

image