1.前言
在本系列的上一篇文章中,我们已经简单介绍了攻击者需要关心JS中的哪些信息,其中最核心的肯定就是从JS中提取出来的各种接口了。但是我们前面也提到过,提取出来接口,将其与后端地址拼接测活,这都不是我们测试的终点,我们最终肯定是要尝试正常调用这个接口的功能。可是,在很多情况下,不给接口提供参数,接口都是无法正常工作的。因此,我们对接口的测试一个绕不开的问题就是——我们应该如何寻找参数的蛛丝马迹?
本文就来分享一下笔者在日常测试过程中的一些常用技巧,希望能帮助师傅们快速寻找到接口对应的参数,从而愉快地测试接口。
2.找不到参数,到底有多痛苦?
开始之前还是先来看看找不到参数是怎样的情况。我们来模拟一下平时测试的流程。我们找到了一个接口,先以GET去访问。
一般会提示405,或者回显GET not supported,就是请求方式不对。那我们改成POST或者其它请求方式试试。
此时,如果出现415错误,或者回显application/x-www-form-urlencoded Not supported,那就需要换一下Content-Type,绝大部分情况下,只需要改成application/json,并加上json请求体即可,极少数情况下遇到过xml传参。
然后?然后就没有然后了,这下,从响应中我们并不能推断出自己下一步要干什么,当然这里还有可能出现各种奇奇怪怪的异常和回显,比如参数缺失、参数验证失败等等
总之,我们应该怎么去寻找参数呢?
3.基于心善的开发者的参数寻找方法
在正式开始介绍寻找参数的方法之前,我们来介绍另一种情况。那就是在一部分情况下,你调用一个接口并且没有传入参数,好心的开发者会告诉你缺了什么参数,对JAVA开发比较熟悉的小伙伴都知道,在Spring中有一个@Validated验证注解,可以和@NotNull、@NotBlank等注解一起使用,去判断参数是否为空,并返回一些信息:
比如这里就对UserDTO做了一个绑定,然后在UserDTO中就可以判断某个属性是否为空,这里可以注意到,判断之后还可以返回message。因此如果开发者好心,这个地方会抛出一些UserDTO中的属性名。这样我们就知道要传入什么参数了:
例如下面这样:
有时候后端虽然给我们抛出了参数信息,但可能是和异常堆栈或者其他错误信息一起给出的,并不能一眼看出需要的参数,这时候需要细心。这个也算是经常遇到的情况,但是突然找不到能配的图了。
这种一般有三种情况,一种是后端一个个抛出缺失的参数,比如我们加一个A参数,后端告诉我们缺了B参数,我们再补一个B参数,后端又告诉我们缺了C参数...以此类推。另一种则是后端一口气告诉缺了A、B、C参数。上述这两种都属于对攻击者很友好的,第三种情况相对没那么友好,那就是后端只模糊的说了一下需要的参数的含义,比如我下面这个接口:
http://api.test.com/api/getuserinfo
访问后端提示,缺少用户id。这种时候我们虽然不知道参数的具体值,但是是能很大程度上缩小范围的,比如如下几个常见的用户id参数:
id、userid、user_id、uid、userId等
部分功能的接口的参数名也相对比较固定,比如文件上传接口,文件上传表单的参数名一般就是file或者files,实际上通过观察接口名字,能够推断出它要实现的是什么功能,是针对什么对象实现的,有时候确实能推断出一些参数。
那么回归正题,上面我们介绍了开发者心善,赏安全从业者饭吃的场景,在上述情况下我们可以快速而精确地获取到缺失的参数名。这实际上都算不上寻找参数,因为对面已经告诉我们了。那我们就假设一个最严峻的场景,后端就是没有任何提示,严格遵循上文演示过的情况,我们又应该怎么办呢?
4.通过常规参数字典进行参数名FUZZ
在上述情况下,第一个比较常规的搞法就是参数名FUZZ
Burp标记参数名部分,用字典去跑即可,这种手法比较依赖字典的强度,参数名字典github上能找到非常多,师傅们综合着使用就行。这种手法有一个显著的问题是,如果目标需要的参数太多,实际上很难用这种方法去找参数效率很低。有时候我们找到一些关键的参数,可能接口就能用了。比如查询类接口,一般有一个query、q之类的参数,传入要查询的信息,虽然这个接口可能还有pageSize、Limit、Order等约束条件作为参数,但是不一定要传入。这种情况下我们找出一个query参数就能愉快调用了。但是如果开发者过于严厉,必须得传入所有参数才能调用,那就很难受了。
因此除了Burp,还有一些专门用于参数名FUZZ的工具,可以应对一下我们上述提到的情况,比如Arjun,这个工具的用法就留给各位师傅自己摸索吧。
5.通过“想当然”法从JS前后文中寻找参数名
除了这样傻傻的去FUZZ参数名,有没有什么方法能比较直白地获取到参数呢?当然是有的,既然网站的功能要正常调用这些接口,说明关键的参数肯定也写在JS里了,我们前文提到的各种工具都会告诉你提取的接口出自哪个JS文件:
那我们就点进对应js文件,搜索部分接口uri,去定位前后文。比如上面的接口
dev-api/system/pass/get/email/code
这里也是成功定位到了。然后你会问,我看不懂这么复杂的JS,是不是就寄了?其实不是,这种纯靠猜和“想当然”就能拿下了,为什么呢?我们放大来看:
首先离他最近的,是不是就指示了它的传参是get模式。Ok,现在知道传参类型了。那么参数名呢?我们还是在这个接口的周围去看,发现离他最近的有个这个
注意看这几个东西,userName、tenantCode、code、newPassword,这几个东西是不是就特别像参数名?那我们直接来试一试,首先不带参数访问
那么加上刚刚的参数呢?
虽然可能因为一些参数值没写好,所以也没有调用成功,但通过返回结果的变化,说明我们已经找对参数名了,后续再FUZZ参数值即可。我们再多来几个不同但是类似的情况。
假设现在又找到一个接口/wxpublic/auth,又没有参数,那么还是老样子,进js去搜这个接口
找到,然后还是看它前后文呗:
这几个什么authorizerAppId、compaanyId、scope、redirect,都看起来很像参数啊,我们前面说过,这些疑似参数的,那直接全部一股脑写上呗,多找几个多写几个又不会掉块肉,那现在我们尝试一下:
可以看到接口已经成功调用了,我们找的参数没问题!
总之,一个比较有效的寻找参数名的方法就是回到JS原文,先定位到接口在JS代码中的位置,再去分析其前后文,寻找一些比较类似参数名的字符串,然后尝试去测试即可。
6.将JS拆分成大字典进行参数FUZZ
上面我们介绍了通过分析JS前后文去寻找参数名,但是有些时候,参数名并不会老老实实的放在目标接口的前后文,各种写法和风格千奇百怪,有时候参数名相关的部分可能和接口相关的部分隔得很远,如果JS基础不好是很难看出来的(更何况这些JS代码没有格式化,密密麻麻的更难看懂)。
这种情况下对于不会JS的玩家还有没有什么比较友好的操作呢?当然有,既然参数值无论如何也会在JS里,还有种办法是粗暴地把你下载回来的所有JS文件打散成参数字典,核心正则如下:
matches = re.findall(r'[a-zA-Z]+[\w-]*\d*|\d+[\w-]*[a-zA-Z]+|[a-zA-Z]+\.[a-zA-Z]+|\w+\.\d+|[a-zA-Z]+~[a-zA-Z]+|\w+~\d+|\d+-\d+|[a-zA-Z]+-[a-zA-Z]+', line)
可以用这个正则替换我在上一篇文章的文末给出的小脚本中的正则,实现提取参数的效果,效果类似下面这样:
结合前文提到的Burp或者Arjun进行参数FUZZ即可,虽然也比较耗时耗力,但是成功率还是要高上一些。
7.使用“输入输出法”寻找参数名
这是笔者自己命名的一个比较有意思的方法。在正式介绍这个方法之前,我们还是要介绍WEB的一些开发思路和习惯(尤其是大部分MVC框架)。我想首先把网站里大部分普通接口划分为两类,第一类就是输出信息返回给用户或前端的(查),另一类则是实现增、删、改功能的。假设一个接口/api/shop/selectinfo,这是一个返回商品信息的接口,后端查询商品信息并返回给用户。另外一个接口/api/shop/edit。很显然地,它是编辑商品信息的接口,用户提供商品信息对应的参数,完成商品信息的修改。那么这和开发习惯、寻找参数有什么关系呢?
简单来说,实际上我们对数据库中的东西进行增删改查,最后都会和代码中预先设置好的类建立映射关系(Model),比方说我现在要实现一个功能,要对商品信息进行增删改查,根据面向对象开发思想,当然需要有一个商品类保存各类商品属性。当我想要获取商品信息的时候,从数据库里查询商品信息并利用这些信息生成商品对象,依此类推更新、删除、插入等操作,也就是建立起了一个类-数据库的关系。而我们增删改查操作的东西,实际上就是商品这个类,具体来说是类中的属性,比如商品价格对应一个属性,商品数量对应一个属性,对应反映到WEB上就是price、count等参数值,既然关于商品的不同操作实际上操作的都是同一个类,那么参数值(类中的属性)肯定都是高度类似的,大部分情况下实际上是完全相同的。
比方说上文提到的返回商品信息的接口和编辑商品信息的接口,它们操作的对象都是“商品”,那么这两个接口涉及到的参数有极大可能是完全一致的,比方说商品名、价格、商品描述等属性,就算有不一致的参数也会是少数。
其实在上文中我们就已经涉及到这方面的知识了,上文给出的图在这里再复用一遍
如图,我们user控制器中的save动作,其将UserDTO类对象作为一个参数传入,UserDTO类里面有一些属性:
也就是说,/save/valid这个接口(对应save动作),应该接受userId、username等等参数。那么既然如此,我们是不是也可以写出下面这样的动作:
public RspDTo delete(@RequestBody @Validated UserDTO userDTO)
同样去操作userDTO对应的用户属性,实现一个删除用户信息的效果?那假如我要获取用户信息呢?是不是同样也是从数据库查询到用户信息后,生成一个userDTO用户对象,然后将这个对象中的属性返回给用户呢?而最有意思的是,在很多java网站中,输出相关的接口(比如返回商品信息)返回的数据基本都会包含属性(参数名)。这也就意味着,如果我们的输入相关接口(如编辑商品信息)找不到参数名,可以去输出相关接口里找。同时输出相关的接口,其参数一般都比较简单,比较好fuzz,比如提到的/api/shop/selectinfo,也许只需要商品名之类的简单参数。我们可以先观察接口名字,找一组“输入输出接口”,当输入相关接口fuzz不出参数,可以找寻对应的输出相关接口,FUZZ其参数,然后通过观察接口返回的信息,来获取具体的参数值,多说无益,我们来给一个案例。
挖掘到了一个接口,根据其名字,是编辑学校信息相关的:
比较难受,找不到参数名,经过fuzz也没有结果。
根据“输入输出”这一思想的指导,如果我们把学校信息视作一个类,我们上面有编辑(输入)学校信息相关的接口,那应该也会有输出学校学校信息相关的接口。可以筛查出:
访问该接口,实际上可以看到很多东西:
注意这些fullTeacherAvgPeriod、sso、logo,根据上述理论,这些应该就对应学校信息类中的某些属性,很有可能作为/edit接口的参数,我们直接把这个返回包中的关键部分,粘贴到输入接口去利用,会怎么样呢?
成功调用。
8.参数搜集之长期建设
前文我们依次介绍了几种搜集参数的方法,其实我们的视角是在不断拓宽的,一开始我们搜集参数纯粹是以攻击者的视角来看待这个问题,后面慢慢拓展到以开发者的视角来看待这个问题。其实还可以再拓宽一点,比较正规一点的开发团队、项目,内部肯定是有很多开发上的规定的,其中当然也包括参数的命名规范、规律。同一个公司的不同业务的各个接口用到的参数,会有相似之处,甚至是相同。因此我们要对某个目标进行长时间的深入测试,其实可以准备一些burp插件,把平时关于该目标相关资产的请求、响应中出现的参数名保存下来,就可以生成一个专属目标的参数名字典。这样,当我们遇到和该目标有关的缺失参数名的测试场景时,就可以用这个专属参数名字典去进行参数的FUZZ了。
Github上也能找到一些干这个事的插件,例如:
https://github.com/Giftedboy/ParasCollector
不过据作者自谦说这个插件不是很好用,师傅们也可以找找有没有别的类似的插件,有好用的也欢迎在评论区推荐一下哦。
9.使用搜索引擎寻找参数
这个姿势主要是针对GET传参的,简单来说就是搜索引擎会收录一些带参数的url,如果你想查的接口访问的人比较多,并且恰好被搜索引擎收录了,也有一定概率把参数查出来,不过这种方法不怎么通用。案例如下:
10.修改前端Uri鉴权逻辑便捷测试接口未鉴权问题
上面我们介绍了很多种获取接口对应的参数的技巧,但其实我们不要忘记自己的目的,不能为了找参数而找参数。我们去测试接口,找接口未鉴权,实际上就约等于找后台功能未鉴权。既然如此,如果我们能想办法看到后台的一些页面,去调用页面里面的一些功能,发出请求,那是不是根本不需要管什么参数的问题?这样正常调用功能点发出来的请求,里面大概率是会携带上你的参数的。
这就和我们上一篇文章产生了联系,上一篇文章中我们提到,我们提取JS中的uri时,实际上会顺带提取出一些前端uri
比如上图中的/login、/menu/list等等,这些都是前端uri。这时候就很开心,我直接进行一个拼接。比如说:
http://test.com/#/login
http://test.com/login
尝试拼接一个
http://test.com/#/menu/list
http://test.com/menu/list
尝试去访问这些别的前端uri,看看能不能访问到对应的页面。这样一般也会有三种情况,第一种是目标站点没有任何对前端uri的鉴权。我们这么去访问,能直接看到页面,也不会给你跳转啥的,这时候就可以调用页面上的功能去抓包,分析这些功能点对应的接口的问题了。
第二种情况稍微好点,就是你这么去访问,有概率看到要访问的页面闪一下,然后给你重定向回登录页面,这种时候有种拼手速的玩法,在重定向回登录页面之前,浏览器上的刷新按钮会变成这个状态:
在他重定向回去之前,快速点一下这个X,那么后续动作就会终止执行,你的页面就会卡住。这时候我们也是开启burp愉快抓包就行。其实就是阻止重定向的思路。
第三种情况就是,我们没法看到想访问的页面,前端要么没反应,要么给你弹出个没有权限,要么直接重定向到/login登录页。这种时候我们又该怎么测试呢?下面的内容就需要一点前端调试和JS分析的基础了,本文不会深入展开,仅作案例介绍。
假设这么一个网站,我们提取出一个前端uri为/home (很常见)
我们自然要尝试访问,一访问,发现直接给我们重定向到登录口
我们自然要尝试访问,一访问,发现直接给我们重定向到登录口
这就说明这个前端至少有两层校验,一层与用户登录状态有关,一层与用户权限有关。怎么办呢?我们在控制台搜索“内测”关键词,定位到用户权限鉴权逻辑。
分析这个局部,我们其实很容易能注意到这里的前端鉴权逻辑。前端获取o.state.account.permission属性,一旦这个属性为false,那么就会提示平台内测这个错误,无法访问到对应的前端uri。
那么用户登录状态对应什么属性呢?往上翻一下,不难看出对应o.state.baseInfo.isLogin属性
因此我们可以推测,state中保存了用户的基本信息,比如登录状态、权限等等。因此这里好办了,我们给上述两个位置打上断点,重新访问/home。进入第一个断点
在控制台将其强行改为true
进入下一断
同样地,再控制台对其赋值为1
再往下走,此时会跳转到一个contract页面
虽然没进入home页面,但是这样,我们就来到了一个原本无法直接访问到的一个前端uri,这样就可以调用它的功能了,看这个页面要填的东西很多,最后的传参肯定巨复杂,就算开发好心告诉你缺了什么参数,你敲都要敲半天,用这种方法测起来就快多了。
当然,我们的目的是进入home页面,重定向到这个完善账号信息的页面,说明还是有哪里的鉴权没搞好,我们再回到刚刚的鉴权逻辑部分
可以发现,后续其实还对o.state.account.permission的值做了一个判断,如果值为1,那么会直接进入contract页面。值只要是大于1的整数,其实都可以访问到home页面
因此在控制台将permission改为3
再访问home,成功:
这样,我们就可以愉快的测试后台的各种功能点了。当然上述操作对不懂JS和前端调试的师傅肯定也不太友好。实际上上述操作和平时测试业务逻辑漏洞中的“修改返回包”操作是有等价关系的。为什么这么说呢?
我们重新访问一下home页面,然后在burp抓包,拦截一下返回包。
可以看到一个/state_user的后端接口,从这个接口的名字我们其实就知道,它是用来判断一个用户状态的。
然后你再注意这里的account_state,默认是0。以及这个is_login,默认是false。他们是不是就特别像我们刚刚在JS里分析出来的o.state.account.permission和o.state.baseInfo.isLogin?我们推测这个前端实际上就是从返回包里拿到account_state和is_login,并分别给o.state.account.permission、o.state.baseInfo.isLogin赋值的。
因此我们将其分别改为3和true,然后把另一个code改为200,发包
还是能看到后台页面。这样虽然操作起来比较简单友好,但是万一目标前端对响应结果的判断设置的比较复杂,那就不是那么好办了,比如我前端判断permission为test123abcdef才为有效,我们常见的改响应包的方法就很难改成这样了,所以还是回到JS去分析调试才能应对大部分奇葩情况,还是比较吃基本功的。
11.关于参数值
上面我们找到了接口,找到了参数名,绝大部分情况下就可以直接开始测试了,但有些时候我们可能还会面临一些关于参数值的FUZZ问题,但是到这里就很简单了。这部分问题大多数情况下集中于json传参的情况,json传参里常见的类型其实也就是下图这些
a为整型,b为字符串类型,c为布尔类型,d为数组类型,e为对象类型。这几种类型都试一下准没错,而且大多数情况下类型错了后端是要直接抛出异常信息的,从异常信息里就很容易能看出来这个地方要传入什么类型,比如异常里看到xxxxx Object xxxxxx,就可以猜测某个参数可能要传入对象类型的传参。
此外还有一点和传参值限制有关的问题就是我们可能会遇到一些字符串格式化问题。比方说一个
Date=
看到这样的参数名,然后发现自己参数值怎么测都测不对的话,可能是后端对这里传入的字符串做了日期类型的格式化、类型限制或正则匹配,这时候可以考虑传入2024-09-07 12:00:00这样符合格式的字符串。还有些可能出现这种问题的参数,一般都和身份证、手机号、邮箱等个人信息有关。不排除开发者自己设定了一些乱七八糟的字符串格式,这些就得具体情况具体分析了。
12.结语
在日常测试中,我们找到了一个可能存在未鉴权问题的接口,但找不到对应的参数,确实是个很令人红温的问题,希望这篇分享能帮助师傅们找到参数,可能有一些没有提到,欢迎师傅们补充。本系列目前已经分析了攻击者可以从JS中提取哪些信息,分析了应该如何提取参数名参数值以辅助接口测试,但还有一个很重要的问题没有解决——在不同场景下攻击者应该如何获取JS。因此后续几篇文章会介绍不同场景下获取JS的方法。