From 8d0150f686ed0c03f6ec2a224277a3afa15b4213 Mon Sep 17 00:00:00 2001 From: Willy Sudiarto Raharjo Date: Sun, 16 Dec 2012 22:16:20 +0100 Subject: network/sshblock: Added (an SSH dictionary-attack blocker). Signed-off-by: Matteo Bernardini --- network/sshblock/README | 7 ++ network/sshblock/doinst.sh | 14 +++ network/sshblock/rc.sshblock | 56 +++++++++ network/sshblock/slack-desc | 19 +++ network/sshblock/sshblock.SlackBuild | 95 +++++++++++++++ network/sshblock/sshblock.info | 10 ++ network/sshblock/sshblock.pl.tpl | 230 +++++++++++++++++++++++++++++++++++ network/sshblock/sshblock.tpl | 8 ++ network/sshblock/sshunblock.pl.tpl | 179 +++++++++++++++++++++++++++ 9 files changed, 618 insertions(+) create mode 100644 network/sshblock/README create mode 100644 network/sshblock/doinst.sh create mode 100644 network/sshblock/rc.sshblock create mode 100644 network/sshblock/slack-desc create mode 100644 network/sshblock/sshblock.SlackBuild create mode 100644 network/sshblock/sshblock.info create mode 100644 network/sshblock/sshblock.pl.tpl create mode 100644 network/sshblock/sshblock.tpl create mode 100644 network/sshblock/sshunblock.pl.tpl diff --git a/network/sshblock/README b/network/sshblock/README new file mode 100644 index 0000000000000..c601150cc65c8 --- /dev/null +++ b/network/sshblock/README @@ -0,0 +1,7 @@ +SSHblock is intended to dynamically and automatically stop SSH-based +dictionary attacks by blocking any IP address that fails an SSH +login too many times too quickly, and automatically unblocks it +after a while. + +You may change the pre-defined configuration of SSHblock in +sshblock.pl.tpl (whitelist IP, email, and hostname). diff --git a/network/sshblock/doinst.sh b/network/sshblock/doinst.sh new file mode 100644 index 0000000000000..74db18c0e3c99 --- /dev/null +++ b/network/sshblock/doinst.sh @@ -0,0 +1,14 @@ +config() { + NEW="$1" + OLD="$(dirname $NEW)/$(basename $NEW .new)" + # If there's no config file by that name, mv it over: + if [ ! -r $OLD ]; then + mv $NEW $OLD + elif [ "$(cat $OLD | md5sum)" = "$(cat $NEW | md5sum)" ]; then + # toss the redundant copy + rm $NEW + fi + # Otherwise, we leave the .new copy for the admin to consider... +} + +config etc/rc.d/rc.sshblock.new diff --git a/network/sshblock/rc.sshblock b/network/sshblock/rc.sshblock new file mode 100644 index 0000000000000..68221bef384b3 --- /dev/null +++ b/network/sshblock/rc.sshblock @@ -0,0 +1,56 @@ +#!/bin/bash + +if [ ! $UID ]; then + echo "You must be root to use SSHblock." + exit 1; +fi + +case "$1" in + 'start') + swatch -c /etc/swatch/sshblock -t /var/log/messages &> /dev/null & + if [ ! `ls /etc/cron.hourly | grep sshunblock` ]; then + ln -s /usr/sbin/sshunblock.pl /etc/cron.hourly + fi + ;; + 'stop') + pid=`ps auxwww | grep swatch | grep -v grep | grep sshblock | awk '{print $2}'` + kill $pid + ;; + 'clear') + for ip in `iptables -nL INPUT | tail +3 | grep DROP | grep dpt:22 | awk '{print $4}'`; do + iptables -D INPUT -p tcp -s $ip --dport 22 --syn -j DROP + done + ;; + 'list') + echo "Blocked IP addresses:" + iptables -nL INPUT | tail +3 | grep DROP | grep dpt:22 | awk '{print $4}' + ;; + 'status') + blocking=`ps auxwww | grep swatch | grep -v grep | grep sshblock | wc -l` + blocked=`iptables -nL INPUT | tail +3 | grep DROP | grep dpt:22 | wc -l` + unblocking=`ls -l /etc/cron.hourly | grep sshunblock | wc -l` + if [ $blocked -eq 1 ]; then + pl='' + verb='is' + else + pl='es' + verb='are' + fi + if [ $blocking -gt 0 ]; then + echo "SSHblock is active" + else + echo "SSHblock is not running" + fi + echo "There $verb currently $blocked address$pl blocked." + ;; + *) + echo "Usage: $0 [start|stop|clear|status|list]" + echo " " + echo "start: Start SSHblock system" + echo "stop: Stop blocking new IPs; old ones will still expire at the usual rate" + echo "clear: Clear all blocked addresses" + echo "status: Report whether SSHblock is running, how many IPs are blocked" + echo "list: List all blocked IP addresses" + exit + ;; +esac diff --git a/network/sshblock/slack-desc b/network/sshblock/slack-desc new file mode 100644 index 0000000000000..15216f8a35d31 --- /dev/null +++ b/network/sshblock/slack-desc @@ -0,0 +1,19 @@ +# HOW TO EDIT THIS FILE: +# The "handy ruler" below makes it easier to edit a package description. +# Line up the first '|' above the ':' following the base package name, and +# the '|' on the right side marks the last column you can put a character in. +# You must make exactly 11 lines for the formatting to be correct. It's also +# customary to leave one space after the ':' except on otherwise blank lines. + + |-----handy-ruler------------------------------------------------------| +sshblock: sshblock (an SSH Dictionary-Attack Blocker) +sshblock: +sshblock: SSHblock is intended to dynamically and automatically stop SSH-based +sshblock: dictionary attacks by blocking any IP address that fails an SSH +sshblock: login too many times too quickly, and automatically unblocks it +sshblock: after a while. +sshblock: +sshblock: Project Website: http://kagan.mactane.org/software/sshblock/ +sshblock: +sshblock: +sshblock: diff --git a/network/sshblock/sshblock.SlackBuild b/network/sshblock/sshblock.SlackBuild new file mode 100644 index 0000000000000..0b9ef3d66fb79 --- /dev/null +++ b/network/sshblock/sshblock.SlackBuild @@ -0,0 +1,95 @@ +#!/bin/sh + +# Slackware build script for sshblock + +# Copyright 2012 Willy Sudiarto Raharjo +# All rights reserved. +# +# Redistribution and use of this script, with or without modification, is +# permitted provided that the following conditions are met: +# +# 1. Redistributions of this script must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR IMPLIED +# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +# EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +PRGNAM=sshblock +VERSION=${VERSION:-0.5} +BUILD=${BUILD:-1} +TAG=${TAG:-_SBo} + +if [ -z "$ARCH" ]; then + case "$( uname -m )" in + i?86) ARCH=i486 ;; + arm*) ARCH=arm ;; + *) ARCH=$( uname -m ) ;; + esac +fi + +CWD=$(pwd) +TMP=${TMP:-/tmp/SBo} +PKG=$TMP/package-$PRGNAM +OUTPUT=${OUTPUT:-/tmp} + +if [ "$ARCH" = "i486" ]; then + SLKCFLAGS="-O2 -march=i486 -mtune=i686" + LIBDIRSUFFIX="" +elif [ "$ARCH" = "i686" ]; then + SLKCFLAGS="-O2 -march=i686 -mtune=i686" + LIBDIRSUFFIX="" +elif [ "$ARCH" = "x86_64" ]; then + SLKCFLAGS="-O2 -fPIC" + LIBDIRSUFFIX="64" +else + SLKCFLAGS="-O2" + LIBDIRSUFFIX="" +fi + +set -e + +rm -rf $PKG +mkdir -p $TMP $PKG $OUTPUT +cd $TMP +rm -rf $PRGNAM-$VERSION +tar xvf $CWD/$PRGNAM-$VERSION.tar.bz2 +cd $PRGNAM-$VERSION +chown -R root:root . +find . \ + \( -perm 777 -o -perm 775 -o -perm 711 -o -perm 555 -o -perm 511 \) \ + -exec chmod 755 {} \; -o \ + \( -perm 666 -o -perm 664 -o -perm 600 -o -perm 444 -o -perm 440 -o -perm 400 \) \ + -exec chmod 644 {} \; + +mkdir -p $PKG/usr/sbin $PKG/etc/ssh $PKG/etc/swatch $PKG/etc/rc.d $PKG/etc/cron.hourly +touch $PKG/etc/ssh/block-history + +install -m 0644 $CWD/rc.sshblock $PKG/etc/rc.d/rc.sshblock.new +install -m 0755 $CWD/sshblock.pl.tpl $PKG/usr/sbin/sshblock.pl +install -m 0755 $CWD/sshunblock.pl.tpl $PKG/usr/sbin/sshunblock.pl +install -m 0755 $CWD/sshblock.tpl $PKG/etc/swatch/sshblock +install -m 0755 $CWD/sshunblock.pl.tpl $PKG/etc/cron.hourly/sshunblock + +find $PKG -print0 | xargs -0 file | grep -e "executable" -e "shared object" | grep ELF \ + | cut -f 1 -d : | xargs strip --strip-unneeded 2> /dev/null || true + +mkdir -p $PKG/usr/doc/$PRGNAM-$VERSION +cp -a \ + README \ + $PKG/usr/doc/$PRGNAM-$VERSION +cat $CWD/$PRGNAM.SlackBuild > $PKG/usr/doc/$PRGNAM-$VERSION/$PRGNAM.SlackBuild + +mkdir -p $PKG/install +cat $CWD/slack-desc > $PKG/install/slack-desc +cat $CWD/doinst.sh > $PKG/install/doinst.sh + +cd $PKG +/sbin/makepkg -l y -c n $OUTPUT/$PRGNAM-$VERSION-$ARCH-$BUILD$TAG.${PKGTYPE:-tgz} diff --git a/network/sshblock/sshblock.info b/network/sshblock/sshblock.info new file mode 100644 index 0000000000000..100a8376546c6 --- /dev/null +++ b/network/sshblock/sshblock.info @@ -0,0 +1,10 @@ +PRGNAM="sshblock" +VERSION="0.5" +HOMEPAGE="http://kagan.mactane.org/software/sshblock/" +DOWNLOAD="http://kagan.mactane.org/software/libraries/download/sshblock-0.5.tar.bz2" +MD5SUM="dbfaee5f45296de2f9a22d5fe79e7332" +DOWNLOAD_x86_64="" +MD5SUM_x86_64="" +REQUIRES="swatch" +MAINTAINER="Willy Sudiarto Raharjo" +EMAIL="willysr@slackware-id.org" diff --git a/network/sshblock/sshblock.pl.tpl b/network/sshblock/sshblock.pl.tpl new file mode 100644 index 0000000000000..bc2f166d8eb8f --- /dev/null +++ b/network/sshblock/sshblock.pl.tpl @@ -0,0 +1,230 @@ +#!/usr/bin/perl -wT + +# Copyright 2009 Kagan D. MacTane +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require 5.004; +use strict; +use Sys::Syslog; + +# ------------------------------------------------------------------- + +# This is the whitelist. Any IP address that's listed in this array will +# never be blocked. +my @never_block_these = qw(127.0.0.1 192.168.1.1); + +# Where should SSHblock track which addresses it's blocked? This will be +# a text file with tab-delimited fields: +# IP address - # of times blocked # timestamp of last block +my $history_file = '/etc/ssh/block-history'; + +# If email notifications +# Set to an empty string to suppress email notifications. +my $send_email = ''; + +# Only send email if IP has been blocked at least this many times. +# E.g., at $email_level = 3, email will only be sent if an IP is +# blocked for the 3rd (or greater) time. +my $email_level = 2; + +# This is just to keep SSHblock from having to run `hostname` every time +# it wants to notify you that it's blocked something. This text is only +# used in the notification email, and can be safely altered. +my $myhostname = 'localhost'; + +my $Syslog_Level = 'info'; +my $Syslog_Facility = 'user'; +my $Syslog_Options = ''; + +my $VERSION = 0.5; + +# ------------------------------------------------------------------- + +$ENV{PATH} = '/sbin:/usr/sbin:/usr/bin:/bin'; + +unless (scalar @ARGV) { + LogMessage("Called with no arguments; aborting."); + print "Usage: $0 ip_addr\n\"perldoc $0\" for full man page\n"; + exit 1; +} +my $ip_addr = shift; + +# Ensure we were passed a valid IP address as first argument +if ($ip_addr =~ /^((\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3}))$/) { + $ip_addr = $1; +} else { + LogMessage("first arg not IP address: $ip_addr"); + exit 1; +} +if ($2 > 255 || $3 > 255 || $4 > 255 || $5 > 255) { + exit 1; +} + +# Abort if you're not running as root +if ($>) { + print "Only root can run this program.\n"; + exit 1; +} + +# And don't waste processor cycles if the IP's already blocked +exit 0 if (is_blocked($ip_addr)); + +if (grep { $_ eq $ip_addr } @never_block_these) { + LogMessage("Not bothering to block whitelisted IP $ip_addr"); + exit 3; +} + +# block command: +`iptables -A INPUT -p tcp -s $ip_addr --dport 22 --syn -j DROP`; + +if ($?) { + LogMessage("Failed to block IP $ip_addr; error $?: $!"); + exit $?; +} + +# Assuming that succeeded, add a record to the history file. + +my @history; +open FH, "$history_file" || exit 7; +@history = ; +close FH; + +my ($prev) = grep /^$ip_addr\s/, @history; +my $how_many; + +if ($prev) { + my @prev = split /\s+/, $prev; + $prev[1]++; + splice(@prev, 2, 1, time()); + map { if ($_ =~ /^$ip_addr\s/) { $_ = join("\t", @prev)."\n"; } } @history; + $how_many = ordinal($prev[1]); +} else { + push(@history, join("\t", $ip_addr, 1, time()), "\n"); + $how_many = ordinal(1); +} + +open FH, ">$history_file" || exit 8; +print FH @history; +close FH; + +LogMessage("Blocked IP $ip_addr for $how_many time"); + +my $num = $how_many; +$num =~ s/\D//g; + + +if ($send_email && $num >= $email_level) { + open MAIL, "|[[SENDMAIL_PATH]] -t"; + print MAIL "From: sshblock \nTo: $send_email\nSubject: SSH Block: $ip_addr\n\nBlocked IP address $ip_addr for $how_many time.\n\n"; + close MAIL; +} + +exit 0; + +sub is_blocked { + my $ip_addr = shift; + return grep /^$ip_addr/, split /\n/, `iptables -nL | grep DROP | grep 'dpt:22' | grep '0x17/0x02' | awk '{print \$4}'`; +} + +sub ordinal { + my $num = shift; + if (length($num) > 1 && substr($num, -2, 1) == 1) { + return $num . 'th'; + } + if (substr($num, -1) == 1) { + return $num . 'st'; + } elsif (substr($num, -1) == 2) { + return $num . 'nd'; + } elsif (substr($num, -1) == 3) { + return $num . 'rd'; + } else { + return $num . 'th'; + } +} + + + +sub LogMessage { + my $format = shift; + openlog('sshblock', $Syslog_Options, $Syslog_Facility); + syslog($Syslog_Level, $format, @_); + closelog(); +} + + +__END__ + +=head1 NAME + +sshblock.pl - SSH dictionary attack blocker + +=head1 SYNOPSIS + +B I + +=head1 DESCRIPTION + +This is part of the SSHblock system; the B executable is responsible for blocking IP addresses from access to port 22. B does this by adding a firewall rule to B, which must be present on the system. Because of this, SSHblock must be run as root. + +B only blocks addresses; unblocking them is the responsibility of B, which should be run as an hourly cron(8) job. + +=head1 INVOCATION + +B is intended to be called by swatch(1) or a similar automated process. You I call it from the command line, passing it a single IP address to block, and this won't actualyl cause any problems, but it will only take one argument per invocation. + +By default, SSHblock logs its activity to syslog(8), using the "user" facility at level "info". + +=head1 CONFIGURATION + +SSHblock can be configured by changing the following options in the program's source code: + +=over + +=item B<@never_block_these> + +This array holds SSHblock's whitelist. Any IP address found in this array will never be blocked. + +=item B<$history_file> + +Where SSHblock should store its history file. This file keeps a record of all IP addresses SSHblock has ever blocked, one per line. Each line consists of three tab-delimited fields: the IP address; the total number of times it's been blocked; and the timestamp it was last blocked at. + +=item B<$email_level> + +If this number is nonzero, then SSHblock will send an email to the address specified in B<$send_email> whenever an address is blocked for the Nth or greater time. For example, if $email_level is 3, SSHblock will remain silent when it blocks an address for the 1st or 2nd time, but send email on the 3rd time. + +=back + +=head1 FILES + +=over + +=item F + +=back + + +=head1 BUGS + +Please let me know if you find any. + +=head1 AUTHOR + +Kagan D. MacTane (kai@mactane.org) + +=head1 SEE ALSO + +sshunblock(8), iptables(8), L + +=cut + diff --git a/network/sshblock/sshblock.tpl b/network/sshblock/sshblock.tpl new file mode 100644 index 0000000000000..d8d146836b614 --- /dev/null +++ b/network/sshblock/sshblock.tpl @@ -0,0 +1,8 @@ +watchfor /Failed password for invalid user \w+ from ([\d\.]+) port/ + exec "/usr/sbin/sshblock.pl $1" + threshold track_by=$1, type=threshold, count=3, seconds=90 + +watchfor /Failed password for root from ([\d\.]+) port/ + exec "/usr/sbin/sshblock.pl $1" + threshold track_by=$1, type=threshold, count=3, seconds=30 + diff --git a/network/sshblock/sshunblock.pl.tpl b/network/sshblock/sshunblock.pl.tpl new file mode 100644 index 0000000000000..298e96da09101 --- /dev/null +++ b/network/sshblock/sshunblock.pl.tpl @@ -0,0 +1,179 @@ +#!/usr/bin/perl -wT + +# Copyright 2009 Kagan D. MacTane +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require 5.004; +use strict; +use Sys::Syslog; + +# ------------------------------------------------------------------- + +# This option *MUST* match the corresponding value in sshblock.pl! +my $history_file = '/etc/ssh/block-history'; + + +# Blocking duration formula: If blocking for the Nth time, and +# b = $base and m = $mult, then block for: +# +# T = m * ( b ^ (N-1) ) +# +# Time T is expressed in hours. +my $base = 4; +my $mult = 3; + +my $Syslog_Level = 'info'; +my $Syslog_Facility = 'user'; +my $Syslog_Options = ''; + +my $VERSION = 0.5; + +$ENV{'PATH'} = '/bin:/usr/bin:/usr/sbin:/sbin'; + +# ------------------------------------------------------------------- + +# Abort if you're not running as root +if ($>) { + print "Only root can run this program.\n"; + exit 1; +} + +my $now = time(); + +my @history; +open FH, "$history_file" || exit 7; +@history = ; +close FH; + +my @input_chain = `iptables -nL INPUT | tail +3`; + +# Your iptables output needs to look like: +# DROP tcp -- 1.2.3.4 0.0.0.0/0 tcp dpt:22 flags:0x17/0x02 + +if (scalar @input_chain > 0) { + LogMessage("Checking ".scalar @input_chain." blocked IPs against ".scalar @history." block-history entries."); +} + +foreach my $item (@input_chain) { + my @stats = split(/\s+/, $item); + next unless ($stats[0] eq 'DROP'); + next unless ($stats[1] eq 'tcp'); + next unless ($stats[6] eq 'dpt:22'); + next unless ($stats[7] eq 'flags:0x17/0x02'); + my $curr_ip = $stats[3]; + + if ($curr_ip =~ /^(\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b)$/) { + $curr_ip = $1; + } else { + LogMessage("Invalid IP address in output from iptables?!?"); + next; + } + for ( my $i = 0; $i < scalar @history; $i++ ) { +my $hist_ip = (split(/\t/, $history[$i]))[0]; + if ((split(/\t/, $history[$i]))[0] eq $curr_ip) { + my ($times_blocked, $blocked_since) = (split(/\t/, $history[$i]))[1,2]; + my $duration = $now - $blocked_since; + $duration /= 3600; + $duration = sprintf("%.2f", $duration); + my $block_for = $mult * ($base ** ($times_blocked - 1)); + if ($duration > $block_for) { + `iptables -D INPUT -p tcp -s $curr_ip --dport 22 --syn -j DROP`; + if ($?) { + LogMessage("Couldn't unblock IP $curr_ip (now blocked for $duration hours)! Error $?: $!"); + } else { + LogMessage("Unblocked IP $curr_ip after $duration hours."); + } + } + } + } +} + + +exit 0; + + + +sub is_blocked { + my $ip_addr = shift; + return grep /^$ip_addr/, split /\n/, `iptables -nL | grep DROP | grep 'dpt:22' | grep '0x17/0x02' | awk '{print \$4}'`; +} + + +sub LogMessage { + my $format = shift; + openlog('sshblock', $Syslog_Options, $Syslog_Facility); + syslog($Syslog_Level, $format, @_); + closelog(); +} + + +__END__ + +=head1 NAME + +sshunblock.pl - SSH dictionary attack (un)blocker + +=head1 SYNOPSIS + +B + +=head1 DESCRIPTION + +This is part of the SSHblock system; the B executable is responsible for unblocking blocked IP addresses after a suitable length of time has passed. It does this by removing the B firewall rules created by B. In order to use iptables, sshunblock.pl must be run as root. + +=head1 INVOCATION + +B is intended to be called as an hourly cron(8) job. Calling it more or less frequently will not interfere with its operation. + +By default, SSHblock logs its activity to syslog(8), using the "user" facility at level "info". + +=head1 CONFIGURATION + +F can be configured by changing the following options in the program's source code. + +=over + +=item B<$history_file> + +Note that B This is where SSHblock should store its history file. This file keeps a record of all IP addresses SSHblock has ever blocked, one per line. Each line consists of three tab-delimited fields: the IP address; the total number of times it's been blocked; and the timestamp it was last blocked at. + +=item B<$base, $mult> + +These control the behavior of SSHblock's exponential increase algorithm. By tweaking these, you can make SSHblock block attacking IP addresses for longer or shorter periods of time. + +=back + +=head1 FILES + +=over + +=item F + +=back + + +=head1 BUGS + +Please let me know if you find any. + +=head1 AUTHOR + +Kagan D. MacTane (kai@mactane.org) + +=head1 SEE ALSO + +sshblock(8), iptables(8), L + +=cut + -- cgit v1.2.3