V1.4.0 Execute a command for real if it passes some filters

master
Ng Yat Yan 3 months ago
parent 83a865b71d
commit 7f99e9e2f6

1
.gitignore vendored

@ -3,3 +3,4 @@
/.project /.project
/.settings/ /.settings/
/logs/ /logs/
/data/

@ -7,3 +7,6 @@ ssh-server:
regex-mapping: regex-mapping:
location: "conf/regex-mapping.properties" location: "conf/regex-mapping.properties"
spring:
datasource:
url: "jdbc:h2:file:./data/remote-ip-info-db"

@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>com.example.sshd</groupId> <groupId>com.example.sshd</groupId>
<artifactId>echo-sshd-server</artifactId> <artifactId>echo-sshd-server</artifactId>
<version>1.3.0</version> <version>1.4.0</version>
<name>ECHO SSH SERVER</name> <name>ECHO SSH SERVER</name>
<description>Learning Apache Mina SSHD library</description> <description>Learning Apache Mina SSHD library</description>
<parent> <parent>
@ -13,10 +13,11 @@
<version>3.3.2</version> <version>3.3.2</version>
</parent> </parent>
<properties> <properties>
<java.version>17</java.version>
<mina.version>2.0.25</mina.version> <mina.version>2.0.25</mina.version>
<sshd.version>0.14.0</sshd.version> <sshd.version>0.14.0</sshd.version>
<bouncycastle.version>1.78.1</bouncycastle.version>
<commons-io.version>2.13.0</commons-io.version> <commons-io.version>2.13.0</commons-io.version>
<commons-exec.version>1.4.0</commons-exec.version>
</properties> </properties>
<dependencies> <dependencies>
<dependency> <dependency>
@ -33,6 +34,20 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId> <artifactId>spring-boot-starter-log4j2</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-exec</artifactId>
<version>${commons-exec.version}</version>
</dependency>
<dependency> <dependency>
<groupId>commons-codec</groupId> <groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId> <artifactId>commons-codec</artifactId>
@ -47,16 +62,6 @@
<artifactId>sshd-core</artifactId> <artifactId>sshd-core</artifactId>
<version>${sshd.version}</version> <version>${sshd.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<dependency> <dependency>
<groupId>commons-io</groupId> <groupId>commons-io</groupId>
<artifactId>commons-io</artifactId> <artifactId>commons-io</artifactId>
@ -77,8 +82,8 @@
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>
<configuration> <configuration>
<source>17</source> <source>${java.version}</source>
<target>17</target> <target>${java.version}</target>
</configuration> </configuration>
</plugin> </plugin>
<plugin> <plugin>

@ -1,6 +1,7 @@
package com.example.sshd.core; package com.example.sshd.core;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.util.List;
import java.util.Map; import java.util.Map;
import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
@ -15,6 +16,8 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import com.example.sshd.service.JdbcService;
@Component @Component
public class EchoSessionListener implements SessionListener { public class EchoSessionListener implements SessionListener {
@ -30,6 +33,9 @@ public class EchoSessionListener implements SessionListener {
@Autowired @Autowired
CloseableHttpAsyncClient asyncClient; CloseableHttpAsyncClient asyncClient;
@Autowired
JdbcService jdbcService;
@Value("${ssh-server.ip-info-api.url:http://ip-api.com/json/%s}") @Value("${ssh-server.ip-info-api.url:http://ip-api.com/json/%s}")
private String ipInfoApiUrl; private String ipInfoApiUrl;
@ -48,6 +54,12 @@ public class EchoSessionListener implements SessionListener {
} }
logger.info("new session: {} -> {}", remoteIpAddress, session); logger.info("new session: {} -> {}", remoteIpAddress, session);
remoteSessionMapping.put(remoteIpAddress, session); remoteSessionMapping.put(remoteIpAddress, session);
if (!ipInfoMapping.containsKey(remoteIpAddress)) {
List<Map<String, Object>> ipInfoList = jdbcService.getRemoteIpInfo(remoteIpAddress);
if (!ipInfoList.isEmpty()) {
ipInfoMapping.put(remoteIpAddress, (String) ipInfoList.get(0).get("remote_ip_info"));
}
}
} }
} }
@ -64,15 +76,19 @@ public class EchoSessionListener implements SessionListener {
@Override @Override
public void completed(SimpleHttpResponse result) { public void completed(SimpleHttpResponse result) {
logger.info("[{}] asyncClient.execute completed, result: {}, content-type: {}, body: {}", logger.info(
"[{}] asyncClient.execute completed, result: {}, content-type: {}, body: {}",
remoteIpAddress, result, result.getContentType(), result.getBodyText()); remoteIpAddress, result, result.getContentType(), result.getBodyText());
ipInfoMapping.put(remoteIpAddress, result.getBodyText()); ipInfoMapping.put(remoteIpAddress, result.getBodyText());
ipInfoLogger.info("[{}] {}", remoteIpAddress, ipInfoMapping.get(remoteIpAddress)); int inserted = jdbcService.insertRemoteIpInfo(remoteIpAddress, result.getBodyText());
ipInfoLogger.info("[{}] {}, inserted = {}", remoteIpAddress,
ipInfoMapping.get(remoteIpAddress), inserted);
} }
@Override @Override
public void failed(Exception exception) { public void failed(Exception exception) {
logger.info("[{}] asyncClient.execute failed, exception: {}", remoteIpAddress, exception); logger.info("[{}] asyncClient.execute failed, exception: {}", remoteIpAddress,
exception);
} }
@Override @Override

@ -21,7 +21,7 @@ import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import com.example.sshd.util.ReplyUtil; import com.example.sshd.service.ReplyService;
@Component @Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@ -30,7 +30,7 @@ public class EchoShell implements Command, Runnable, SessionAware {
private static final Logger logger = LoggerFactory.getLogger(EchoShell.class); private static final Logger logger = LoggerFactory.getLogger(EchoShell.class);
@Autowired @Autowired
ReplyUtil replyUtil; ReplyService replyUtil;
@Autowired @Autowired
Properties hashReplies; Properties hashReplies;

@ -0,0 +1,49 @@
package com.example.sshd.service;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Service;
import jakarta.annotation.PostConstruct;
@Service
public class JdbcService {
private static final String createRemoteIpLookupTableSql = "CREATE TABLE IF NOT EXISTS public.remote_ip_lookup (id BIGINT not null, "
+ "remote_ip_address CHARACTER VARYING not null, remote_ip_info CHARACTER VARYING not null, PRIMARY KEY (id));";
private static final String createRemoteIpLookupIndexSql = "CREATE INDEX IF NOT EXISTS public.remote_ip_lookup_idx ON "
+ "public.remote_ip_lookup (remote_ip_address);";
@Autowired
private JdbcTemplate jdbcTemplate;
@PostConstruct
private void init() {
jdbcTemplate.execute(createRemoteIpLookupTableSql);
jdbcTemplate.execute(createRemoteIpLookupIndexSql);
}
public List<Map<String, Object>> getRemoteIpInfo(String remoteIp) {
return jdbcTemplate.query(
"SELECT id, remote_ip_address, remote_ip_info from public.remote_ip_lookup WHERE remote_ip_address = ? ",
new RowMapper<Map<String, Object>>() {
@Override
public Map<String, Object> mapRow(ResultSet rs, int rowNum) throws SQLException {
return Map.of("id", rs.getLong(1), "remote_ip_address", rs.getString(2), "remote_ip_info",
rs.getString(3));
}
}, remoteIp);
}
public int insertRemoteIpInfo(String remoteIpAddress, String remoteIpInfo) {
return jdbcTemplate.update(
"INSERT INTO public.remote_ip_lookup (id, remote_ip_address, remote_ip_info) VALUES (?, ?, ?)",
System.currentTimeMillis(), remoteIpAddress, remoteIpInfo);
}
}

@ -1,25 +1,30 @@
package com.example.sshd.util; package com.example.sshd.service;
import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Properties; import java.util.Properties;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.exec.CommandLine;
import org.apache.commons.exec.DefaultExecutor;
import org.apache.commons.exec.PumpStreamHandler;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Pair;
import org.apache.sshd.server.session.ServerSession; import org.apache.sshd.server.session.ServerSession;
import org.bouncycastle.util.encoders.Base64;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Component @Component
public class ReplyUtil { public class ReplyService {
private static final Logger logger = LoggerFactory.getLogger(ReplyUtil.class); private static final Logger logger = LoggerFactory.getLogger(ReplyService.class);
private static final Logger notFoundLogger = LoggerFactory.getLogger("not_found"); private static final Logger notFoundLogger = LoggerFactory.getLogger("not_found");
@Autowired @Autowired
@ -56,17 +61,30 @@ public class ReplyUtil {
} else if (hashReplies.containsKey(String.format("base64(%s)", cmdHash))) { } else if (hashReplies.containsKey(String.format("base64(%s)", cmdHash))) {
logger.info("[{}] Known base64-hash detected: {}", cmdHash, command.trim()); logger.info("[{}] Known base64-hash detected: {}", cmdHash, command.trim());
String reply = hashReplies.getProperty(String.format("base64(%s)", cmdHash)); String reply = hashReplies.getProperty(String.format("base64(%s)", cmdHash));
reply = new String(Base64.decode(reply)); reply = new String(Base64.decodeBase64(reply));
out.write(String.format("\r\n%s\r\n%s", reply, prompt).getBytes()); out.write(String.format("\r\n%s\r\n%s", reply, prompt).getBytes());
} else { } else {
Optional<Pair<String, String>> o = regexMapping.entrySet().stream() Optional<Pair<String, String>> o = regexMapping.entrySet().stream()
.filter(e -> command.trim().matches(((String) e.getKey()))) .filter(e -> command.trim().matches(((String) e.getKey())))
.map(e -> Pair.of((String) e.getKey(), (String) e.getValue())).findAny(); .map(e -> Pair.of((String) e.getKey(), (String) e.getValue())).findAny();
if (o.isPresent()) { if (o.isPresent()) {
logger.info("[{}] Known pattern detected: {} ({})", cmdHash, command.trim(), o.get());
String reply = hashReplies.getProperty(o.get().getRight(), "").replace("\\r", "\r").replace("\\n", "\n") String reply = hashReplies.getProperty(o.get().getRight(), "").replace("\\r", "\r").replace("\\n", "\n")
.replace("\\t", "\t"); .replace("\\t", "\t");
out.write(String.format("\r\n%s\r\n%s", reply, prompt).getBytes()); if (reply.isEmpty()) {
logger.info("[{}] Execute cmd for real: {} ({})", cmdHash, command.trim(), o.get());
CommandLine cmdLine = CommandLine.parse(command.trim());
DefaultExecutor executor = DefaultExecutor.builder().get();
ByteArrayOutputStream tempOut = new ByteArrayOutputStream();
PumpStreamHandler streamHandler = new PumpStreamHandler(tempOut);
executor.setStreamHandler(streamHandler);
int exitValue = executor.execute(cmdLine);
logger.info("[{}] Result: {} ({})", cmdHash, command.trim(), exitValue);
reply = new String(tempOut.toByteArray()).replace("\n", "\r\n");
out.write(String.format("\r\n%s\r\n%s", reply, prompt).getBytes());
} else {
logger.info("[{}] Known pattern detected: {} ({})", cmdHash, command.trim(), o.get());
out.write(String.format("\r\n%s\r\n%s", reply, prompt).getBytes());
}
} else { } else {
logger.info("[{}] Command not found: {}", cmdHash, command.trim()); logger.info("[{}] Command not found: {}", cmdHash, command.trim());
notFoundLogger.info("[{}] Command not found: {}", cmdHash, command.trim()); notFoundLogger.info("[{}] Command not found: {}", cmdHash, command.trim());
Loading…
Cancel
Save