本文最后更新于 2024年10月22日 晚上
前言:对JS中泄露的API接口进行接口未授权测试一直是渗透测试的重点,于是尽可能找到更多的API接口就显得很重要,更重要的是还需要找到怎么拼接接口,怎么精准的找到API接口对应的参数,本文对此进行了简单的总结,一起来看一下吧
API接口手工测试,我认为分为四步:
提取API接口
拼接API接口发包
寻找API接口缺失参数
拼接再次发包
提取API接口 FindSomeThing 最直接的就是使用FindSomeThong插件
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爬取数据
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路径
如下勾选,添加来自于哪一个url,便于后续查找,导出文件
导出后,有一个小问题,就是它所有来自于不同js文件的js代码,都导出在了同一个文件中,只是在代码开头有url标识以下代码来自于哪一个js文件
这样的话,后面使用自写的脚本来提取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 reimport osdef extract_js_files (input_file ): 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() 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) 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)
这样就独立出来了
使用脚本递归提取文件夹中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 jsonimport reimport requestsimport sysimport 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) 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' ) p = re.findall('''(['"]\/[^][^>< \)\(\{\}]*?['"])''' ,line) if p != None : 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模块批量测试,先试试直接把接口拼接到根目录下,看看有多少成功的接口
可以看到有一个200的接口,说明这个接口可能成功了,有一个401的接口,这就是有鉴权的接口,多半不用看了,剩下的全是404的接口,可能会有师傅认为是这些接口被弃用了,于是到这里就放弃了,没有继续测试了。但其实,这些接口都是使用脚本的正则表达式从js文件中提取出来的,大都应该是存在的,有一种可能就是这些接口有一个base路径,也就是需要拼接上这个base路径,才是正确的路径
接下来就是去js文件中去找这个base路径:以/auth/auth/init 为例
直接F12,ctrl+shift+F,全局搜索 /auth/auth/init 接口,定位到js源码
可以看到这三个接口,都使用concat(),拼接了一个”i”,那么我们去找到这个”i”是什么,可能就能找到base路径了
怎么去找”i”呢?我们根据基础的变量的定义与调用可以知道,这里既然调用了”i”并使用concat拼接,那么”i”肯定已经被定义了,而且是在调用之前就定义好了,于是我们就在这个接口处,一直往前翻找看看”i”被定义成了啥
果不其然,往前翻了一会就找到了,根本不需要太懂js代码
于是base路径找到了,为 /api/blade-workbench
还有一种方式也可以试试,就是F12,网络,刷新,查看加载的XHR
有些时候这样也能找到base路径,要看它加不加载咯
找到base路径后,再拼接新的API接口
再次burp发包:
可以看到,状态码发送了很大的改变,说明我们的base路径找对了
401,接口鉴权了,可以先跳过,后面再看
405,请求方式不对,修改为POST,burp再次发包
可以看到,状态码又发生了很大的改变,多出来许多200的接口
那么当然是优先去测试这些200的接口,这要么说明这是公开的接口,要么存在接口未授权的问题,漏洞不就来了吗
在众多200接口中筛选一下比较重要的接口,找到一个重置密码的接口,发到重放模块:
提示重置密码失败,那么不用想,下一步就是去寻找API对应的参数,怎么找API的参数呢?
找之前,先学习一下找API的参数的几种情况与技巧
寻找API接口缺失参数 其实有些接口是可以直接查看到使用参数的,比如有些显式的功能点,重置密码,登录,等等这些接口,只要你抓包就可以直接看到其使用的参数,因为这些是比较明显的功能点的后端接口,这类接口本来就是公开的,是功能点处的逻辑处理接口,也就不存在什么未授权的说法了,因为这类接口本来就是不继续鉴权的公共接口,可以直接查看到其使用的参数,于是可能不会考虑去测试未授权的漏洞,但是不排除有其他逻辑缺失漏洞,比如可以测试水平越权与垂直越权,重放攻击等等……于是我把这种公开的,参数可直接看到的接口称为显式接口,把需要进行鉴权,比较隐蔽,参数不好寻找的接口称为隐式接口,这种接口才是未授权的重点测试对象
显式接口 比如这个重置密码的接口就是显式接口:
可以直接抓重置密码的数据包,就可以查看到参数了,不是未授权接口测试的重点
还有登录接口
等等其他显式的功能点处 调用的后端逻辑处理接口都是显式接口,参数可直接获取
隐式接口 上面说了,这些接口的参数不太好直接获取,就是不能一眼找出触发它的功能点的,进行了严格鉴权的那种接口,有点抽象,比如我们以普通权限的身份登录,但是通过js文件和脚本提取到了需要管理员权限的API接口,这种接口我们是无法使用功能点去触发的,因为我们不是管理员,压根看不到对应的功能点,于是参数的获取就不再是抓包这么简单了,这就是我自称的隐式接口
怎么去寻找这些接口的参数呢?
接口参数在接口附近 注意观察接口的上下文,附近处,看看能不能找到参数
案例一
postData接收接口与”g”,在前面可以看到”g”的定义,就是三个参数
案例二
使用了axios去发起请求,参数是”name”
案例三
使用concat拼接到URL中,参数是error和callbackUrl
案例四
使用ajax请求,参数是objData对象,还需要寻找objData对象是如何被定义的,往前翻一点,就找到了
接口参数离接口较远 案例二
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(“
直接就找到了
同样的方法,找到了resetpasswd的接口参数